From 5f8bddb483681eb981a9f8e7b239d9e8e1584627 Mon Sep 17 00:00:00 2001 From: Starrah Date: Sun, 3 May 2026 19:28:03 +0800 Subject: [PATCH 01/13] =?UTF-8?q?[R]=20ChuNote=E7=9A=84=E9=87=8D=E6=96=B0?= =?UTF-8?q?=E8=AE=BE=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chart/chu/ChuNote.cs | 33 +- .../Virtual To Live/2098_03.c2s" | 1599 +++++++++++++++++ 2 files changed, 1612 insertions(+), 20 deletions(-) create mode 100644 "tests/chu/testset/\345\256\230\350\260\261/Virtual To Live/2098_03.c2s" diff --git a/chart/chu/ChuNote.cs b/chart/chu/ChuNote.cs index f40e182..21f243b 100644 --- a/chart/chu/ChuNote.cs +++ b/chart/chu/ChuNote.cs @@ -1,38 +1,31 @@ +using MuConvert.chart; +using Rationals; + namespace MuConvert.chu; /** * CHUNITHM 通用音符,C2S / UGC / SUS 共用此结构。 */ -public class ChuNote +public class ChuNote: BaseNote { /** 音符类型 (TAP, CHR, HLD, SLD, AIR, AHD 等) */ public string Type { get; set; } = "TAP"; - /** 小节号 */ - public int Measure { get; set; } - /** 小节内偏移 (C2S: 0–383, UGC/SUS: 0–1919) */ - public int Offset { get; set; } /** 起始列 (0–15) */ public int Cell { get; set; } /** 宽度 (1–16) */ public int Width { get; set; } = 1; - /** HLD 持续时长 */ - public int HoldDuration { get; set; } - /** SLD 持续时长 */ - public int SlideDuration { get; set; } + /** HLD/SLD/AHD/ASD等的 持续时长 */ + public Rational Duration { get; set; } = 0; /** SLD 终点列 */ public int EndCell { get; set; } /** SLD 终点宽度 */ public int EndWidth { get; set; } = 1; - /** CHR/FLK 附加数据(方向等) */ - public string Tag { get; set; } = ""; - /** AIR/AHD 关联的目标音符类型 */ + /** Air系列音符/Slide系列音符的 关联的目标音符类型 */ public string TargetNote { get; set; } = ""; - /** AHD 持续时长 */ - public int AirHoldDuration { get; set; } - /** Air Crush 起始高度 */ - public int StartHeight { get; set; } - /** Air Crush 目标高度 */ - public int TargetHeight { get; set; } - /** Air Crush 颜色 */ - public string NoteColor { get; set; } = ""; + /** CHR/FLK/Air系列音符可能会具有的标记(如UP、L、DEF等) */ + public string Tag { get; set; } = ""; + /** ASD/ASC/ALD上具有的、目前含义还不明确的字段,统一收集到这个里面。 */ + public List ExtraData = []; + + public override Rational EndTime => Time + Duration; } diff --git "a/tests/chu/testset/\345\256\230\350\260\261/Virtual To Live/2098_03.c2s" "b/tests/chu/testset/\345\256\230\350\260\261/Virtual To Live/2098_03.c2s" new file mode 100644 index 0000000..bcaddd7 --- /dev/null +++ "b/tests/chu/testset/\345\256\230\350\260\261/Virtual To Live/2098_03.c2s" @@ -0,0 +1,1599 @@ +VERSION 1.10.01 1.10.01 +MUSIC 0 +SEQUENCEID 0 +DIFFICULT 00 +LEVEL 0.0 +CREATOR うさぎランドリー +BPM_DEF 156.000 156.000 156.000 156.000 +MET_DEF 4 4 +RESOLUTION 384 +CLK_DEF 384 +PROGJUDGE_BPM 240.000 +PROGJUDGE_AER 0.999 +TUTORIAL 0 + +BPM 0 0 156.000 +MET 0 0 4 4 + +CHR 4 288 0 4 UP +CHR 4 288 12 4 UP +HLD 5 0 0 4 2208 +TAP 5 0 6 8 +ASD 5 0 6 8 TAP 5 288 9 2 5 DEF +TAP 6 0 6 8 +ASD 6 0 6 8 TAP 5 288 9 2 5 DEF +TAP 7 0 6 8 +ASD 7 0 6 8 TAP 5 288 9 2 5 DEF +TAP 8 0 6 8 +ASD 8 0 6 8 TAP 5 288 9 2 5 DEF +TAP 9 0 6 8 +ASD 9 0 6 8 TAP 5 288 9 2 5 DEF +TAP 10 0 6 8 +ASD 10 0 6 8 TAP 5 288 9 2 5 DEF +CHR 11 0 0 8 UP +ASC 11 0 0 8 CHR 5 384 7 2 5 DEF +CHR 11 0 8 8 UP +ASC 11 0 8 8 CHR 5 384 7 2 5 DEF +ALD 12 0 0 8 1 5 4 0 8 1 +ALD 12 0 8 8 1 5 4 8 8 1 +CHR 13 0 0 6 UP +TAP 13 0 6 4 +TAP 13 96 6 4 +CHR 13 144 10 6 UP +TAP 13 192 6 4 +CHR 13 288 0 6 UP +TAP 13 288 6 4 +TAP 14 0 6 4 +CHR 14 0 13 3 UP +TAP 14 48 6 4 +CHR 14 48 10 3 UP +CHR 14 96 3 3 UP +TAP 14 96 6 4 +CHR 14 144 0 3 UP +TAP 14 144 6 4 +FLK 14 192 0 3 L +HLD 14 192 12 4 96 +FLK 14 208 3 3 L +FLK 14 224 6 3 L +FLK 14 240 9 3 L +FLK 14 256 6 3 L +FLK 14 272 3 3 L +FLK 14 288 0 3 L +AIR 14 288 12 4 HLD DEF +CHR 15 0 0 4 UP +AHD 15 0 0 4 CHR 96 DEF +CHR 15 0 12 4 UP +TAP 15 72 5 6 +TAP 15 96 0 4 +TAP 15 144 5 6 +CHR 15 192 0 4 UP +AHD 15 192 0 4 CHR 96 DEF +CHR 15 192 12 4 UP +TAP 15 264 5 6 +TAP 15 288 0 4 +TAP 15 336 5 6 +CHR 16 0 0 4 UP +AHD 16 0 0 4 CHR 96 DEF +CHR 16 0 12 4 UP +TAP 16 72 5 6 +TAP 16 96 0 4 +TAP 16 144 5 6 +TAP 16 192 0 4 +TAP 16 240 3 4 +TAP 16 288 6 4 +TAP 16 336 9 4 +CHR 17 0 0 4 UP +CHR 17 0 12 4 UP +AHD 17 0 12 4 CHR 96 DEF +TAP 17 72 5 6 +TAP 17 96 12 4 +TAP 17 144 5 6 +CHR 17 192 0 4 UP +CHR 17 192 12 4 UP +AHD 17 192 12 4 CHR 96 DEF +TAP 17 264 5 6 +TAP 17 288 12 4 +TAP 17 336 5 6 +CHR 18 0 0 4 UP +CHR 18 0 12 4 UP +AHD 18 0 12 4 CHR 96 DEF +TAP 18 72 5 6 +TAP 18 96 12 4 +TAP 18 144 5 6 +TAP 18 192 0 4 +TAP 18 192 12 4 +TAP 18 240 4 4 +TAP 18 240 8 4 +TAP 18 288 0 4 +AIR 18 288 0 4 TAP DEF +TAP 18 288 12 4 +AIR 18 288 12 4 TAP DEF +TAP 19 0 0 4 +AHD 19 0 0 4 TAP 96 DEF +SLC 19 0 12 4 4 11 4 SLD +SLC 19 4 11 4 5 10 4 SLD +SLC 19 9 10 4 5 9 4 SLD +SLC 19 14 9 4 7 8 4 SLD +SLC 19 21 8 4 9 7 4 SLD +SLC 19 30 7 4 14 6 4 SLD +SLC 19 44 6 4 4 6 4 SLD +SLC 19 48 6 4 4 6 4 SLD +SLC 19 52 6 4 14 7 4 SLD +SLC 19 66 7 4 9 8 4 SLD +SLC 19 75 8 4 7 9 4 SLD +SLC 19 82 9 4 5 10 4 SLD +SLC 19 87 10 4 5 11 4 SLD +SLD 19 92 11 4 4 12 4 SLD +SLC 19 96 0 4 4 1 4 SLD +AHD 19 96 12 4 SLD 96 DEF +SLC 19 100 1 4 5 2 4 SLD +SLC 19 105 2 4 5 3 4 SLD +SLC 19 110 3 4 7 4 4 SLD +SLC 19 117 4 4 9 5 4 SLD +SLC 19 126 5 4 14 6 4 SLD +SLC 19 140 6 4 4 6 4 SLD +SLC 19 144 6 4 4 6 4 SLD +SLC 19 148 6 4 14 5 4 SLD +SLC 19 162 5 4 9 4 4 SLD +SLC 19 171 4 4 7 3 4 SLD +SLC 19 178 3 4 5 2 4 SLD +SLC 19 183 2 4 5 1 4 SLD +SLC 19 188 1 4 4 0 4 SLD +SLC 19 192 0 4 96 0 4 SLD +TAP 19 192 12 4 +SLD 19 288 0 4 96 4 4 SLD +TAP 19 288 12 4 +TAP 19 336 10 4 +SLD 20 0 8 4 96 12 4 SLD +TAP 20 48 0 4 +TAP 20 96 2 4 +SLD 20 96 12 4 192 12 4 SLD +SLC 20 144 4 4 15 5 4 SLD +SLC 20 159 5 4 18 6 4 SLD +SLC 20 177 6 4 23 7 4 SLD +SLC 20 200 7 4 30 8 4 SLD +SLD 20 230 8 4 10 8 4 SLD +FLK 20 240 0 8 L +FLK 20 288 4 8 L +HLD 21 0 2 3 96 +HLD 21 0 11 3 96 +TAP 21 96 8 3 +TAP 21 120 5 3 +TAP 21 144 8 3 +TAP 21 192 0 4 +TAP 21 192 12 4 +TAP 21 264 2 4 +TAP 21 264 10 4 +TAP 21 336 5 3 +TAP 21 336 8 3 +TAP 22 0 7 2 +AHD 22 0 7 2 TAP 192 DEF +TAP 23 0 2 3 +TAP 23 0 9 2 +TAP 23 48 9 2 +TAP 23 96 2 3 +TAP 23 96 7 2 +TAP 23 144 5 2 +TAP 23 192 2 3 +HLD 23 192 7 2 96 +TAP 23 288 2 3 +TAP 23 336 7 2 +TAP 24 0 2 3 +TAP 24 0 5 2 +TAP 24 48 7 2 +TAP 24 96 2 3 +TAP 24 96 9 2 +HLD 24 192 2 3 96 +HLD 24 192 7 2 96 +FLK 24 288 9 3 L +TAP 24 336 5 2 +TAP 25 0 5 2 +TAP 25 0 11 3 +TAP 25 48 7 2 +TAP 25 96 9 2 +TAP 25 96 11 3 +TAP 25 144 7 2 +HLD 25 192 7 2 96 +TAP 25 192 11 3 +TAP 25 288 11 3 +TAP 25 336 7 2 +TAP 26 0 5 2 +TAP 26 0 11 3 +TAP 26 48 5 2 +TAP 26 96 7 2 +TAP 26 96 11 3 +TAP 26 144 9 2 +HLD 26 192 7 2 96 +HLD 26 192 11 3 96 +FLK 26 288 4 3 L +TAP 26 336 9 3 +TAP 27 0 3 3 +TAP 27 0 7 2 +TAP 27 48 5 3 +TAP 27 96 7 2 +TAP 27 96 10 3 +TAP 27 144 8 3 +TAP 27 192 3 3 +TAP 27 192 7 2 +TAP 27 288 7 2 +TAP 27 288 10 3 +TAP 27 336 8 3 +TAP 28 0 3 3 +TAP 28 0 7 2 +TAP 28 48 10 3 +TAP 28 96 3 3 +TAP 28 96 7 2 +TAP 28 144 5 3 +TAP 28 192 7 2 +TAP 28 192 10 3 +TAP 28 288 3 3 +TAP 28 288 7 2 +TAP 29 0 7 2 +SLC 29 0 13 3 23 12 3 SLD +SLC 29 23 12 3 27 11 3 SLD +SLC 29 50 11 3 35 10 3 SLD +SLD 29 85 10 3 11 10 3 SLD +TAP 29 96 7 2 +TAP 29 144 10 3 +SLC 29 192 0 3 23 1 3 SLD +TAP 29 192 7 2 +SLC 29 215 1 3 27 2 3 SLD +SLC 29 242 2 3 35 3 3 SLD +SLD 29 277 3 3 11 3 3 SLD +TAP 29 288 7 2 +TAP 29 336 3 3 +SLC 30 0 6 4 15 4 4 SLD +SLC 30 0 12 4 8 11 4 SLD +SLC 30 8 11 4 8 10 4 SLD +SLC 30 15 4 4 10 3 4 SLD +SLC 30 16 10 4 10 9 4 SLD +SLC 30 25 3 4 12 2 4 SLD +SLC 30 26 9 4 13 8 4 SLD +SLC 30 37 2 4 17 1 4 SLD +SLC 30 39 8 4 18 7 4 SLD +SLC 30 54 1 4 30 0 4 SLD +SLC 30 57 7 4 28 6 4 SLD +SLC 30 84 0 4 12 0 4 SLD +SLC 30 85 6 4 11 6 4 SLD +SLC 30 96 0 4 11 0 4 SLD +SLC 30 96 6 4 11 6 4 SLD +SLC 30 107 0 4 23 1 4 SLD +SLC 30 107 6 4 28 7 4 SLD +SLC 30 130 1 4 13 2 4 SLD +SLC 30 135 7 4 18 8 4 SLD +SLC 30 143 2 4 10 3 4 SLD +SLC 30 153 3 4 9 4 4 SLD +SLC 30 153 8 4 13 9 4 SLD +SLC 30 162 4 4 16 6 4 SLD +SLC 30 166 9 4 10 10 4 SLD +SLC 30 176 10 4 8 11 4 SLD +SLD 30 178 6 4 14 8 4 SLD +SLC 30 184 11 4 8 12 4 SLD +AHD 30 192 8 4 SLD 96 DEF +SLD 30 192 12 4 96 12 4 SLD +FLK 30 288 4 8 L +ADL 30 288 4 8 FLK DEF +TAP 31 0 9 6 +TAP 31 48 4 4 +TAP 31 96 2 4 +AUL 31 96 2 4 TAP DEF +TAP 31 144 8 4 +TAP 31 192 10 4 +AUR 31 192 10 4 TAP DEF +TAP 31 288 0 4 +TAP 31 288 6 4 +TAP 31 336 0 4 +TAP 31 336 6 4 +TAP 32 0 3 4 +TAP 32 0 9 4 +TAP 32 48 3 4 +TAP 32 48 9 4 +TAP 32 96 6 4 +AUL 32 96 6 4 TAP DEF +TAP 32 96 12 4 +AIR 32 96 12 4 TAP DEF +TAP 32 192 0 4 +SLC 32 192 0 4 6 0 5 SLD +ADW 32 192 0 4 TAP DEF +TAP 32 192 12 4 +SLC 32 192 12 4 6 11 5 SLD +ADW 32 192 12 4 TAP DEF +SLC 32 198 0 5 6 0 4 SLD +SLC 32 198 11 5 6 12 4 SLD +SLC 32 204 0 4 6 0 5 SLD +SLC 32 204 12 4 6 11 5 SLD +SLC 32 210 0 5 6 0 4 SLD +SLC 32 210 11 5 6 12 4 SLD +SLC 32 216 0 4 6 0 5 SLD +SLC 32 216 12 4 6 11 5 SLD +SLC 32 222 0 5 6 0 4 SLD +SLC 32 222 11 5 6 12 4 SLD +SLC 32 228 0 4 6 0 5 SLD +SLC 32 228 12 4 6 11 5 SLD +SLC 32 234 0 5 6 0 4 SLD +SLC 32 234 11 5 6 12 4 SLD +SLC 32 240 0 4 6 0 5 SLD +SLC 32 240 12 4 6 11 5 SLD +SLC 32 246 0 5 6 0 4 SLD +SLC 32 246 11 5 6 12 4 SLD +SLC 32 252 0 4 6 0 5 SLD +SLC 32 252 12 4 6 11 5 SLD +SLC 32 258 0 5 6 0 4 SLD +SLC 32 258 11 5 6 12 4 SLD +SLC 32 264 0 4 6 0 5 SLD +SLC 32 264 12 4 6 11 5 SLD +SLC 32 270 0 5 6 0 4 SLD +SLC 32 270 11 5 6 12 4 SLD +SLC 32 276 0 4 6 0 5 SLD +SLC 32 276 12 4 6 11 5 SLD +SLD 32 282 0 5 6 0 4 SLD +SLD 32 282 11 5 6 12 4 SLD +TAP 32 336 2 4 +SLC 33 0 2 4 192 2 4 SLD +TAP 33 48 6 4 +SLC 33 96 6 4 96 6 4 SLD +TAP 33 144 10 4 +SLC 33 192 2 4 24 10 4 SLD +SLC 33 192 6 4 24 10 4 SLD +HLD 33 192 10 4 384 +SLD 33 216 10 4 360 10 4 SLD +SLD 33 216 10 4 360 10 4 SLD +SLC 33 288 5 4 24 10 4 SLD +SLC 33 312 10 4 263 10 4 SLD +SLC 34 0 3 4 24 9 4 SLD +SLD 34 24 9 4 168 9 4 SLD +SLC 34 96 1 4 24 8 6 SLD +SLD 34 120 8 6 72 8 6 SLD +SLD 34 191 10 4 1 8 4 SLD +HLD 34 192 0 4 96 +AHD 34 192 8 4 SLD 96 DEF +AIR 34 192 8 6 SLD DEF +AIR 34 192 9 4 SLD DEF +AIR 34 192 10 4 SLD DEF +AIR 34 192 10 4 HLD DEF +AIR 34 192 10 4 SLD DEF +FLK 34 288 8 8 L +ADR 34 288 8 8 FLK DEF +TAP 35 0 1 6 +TAP 35 48 8 4 +TAP 35 96 10 4 +AUR 35 96 10 4 TAP DEF +TAP 35 144 4 4 +TAP 35 192 2 4 +AUL 35 192 2 4 TAP DEF +TAP 35 288 6 4 +TAP 35 288 12 4 +TAP 35 336 6 4 +TAP 35 336 12 4 +TAP 36 0 3 4 +TAP 36 0 9 4 +TAP 36 48 3 4 +TAP 36 48 9 4 +TAP 36 96 0 4 +AUR 36 96 0 4 TAP DEF +TAP 36 96 6 4 +AUR 36 96 6 4 TAP DEF +TAP 36 192 6 4 +TAP 36 192 12 4 +TAP 36 288 0 4 +TAP 36 288 6 4 +SLC 37 0 6 4 96 0 4 SLD +SLD 37 0 12 4 96 6 4 SLD +SLD 37 96 0 4 192 0 4 SLD +AHD 37 96 6 4 SLD 96 DEF +SLD 37 192 6 4 96 12 4 SLD +ASD 37 288 0 4 SLD 5 96 0 4 5 DEF +ASD 37 288 12 4 SLD 5 96 12 4 5 DEF +ASC 38 0 0 4 ASD 5 20 1 4 5 DEF +ASC 38 0 12 4 ASD 5 20 11 4 5 DEF +ASC 38 20 1 4 ASC 5 24 2 4 5 DEF +ASC 38 20 11 4 ASC 5 24 10 4 5 DEF +ASC 38 44 2 4 ASC 5 31 3 4 5 DEF +ASC 38 44 10 4 ASC 5 31 9 4 5 DEF +ASC 38 75 3 4 ASC 5 42 4 4 5 DEF +ASC 38 75 9 4 ASC 5 42 8 4 5 DEF +ASC 38 117 4 4 ASC 5 57 5 4 5 DEF +ASC 38 117 8 4 ASC 5 57 7 4 5 DEF +ASD 38 174 5 4 ASC 5 114 5 4 5 DEF +ASD 38 174 7 4 ASC 5 114 7 4 5 DEF +ALD 38 288 4 4 1 5 2 4 4 1 +ALD 38 288 8 4 1 5 2 8 4 1 +SLC 39 0 6 4 7 5 4 SLD +SLC 39 0 12 4 7 11 4 SLD +SLC 39 7 5 4 8 4 4 SLD +SLC 39 7 11 4 8 10 4 SLD +SLC 39 15 4 4 9 3 4 SLD +SLC 39 15 10 4 9 9 4 SLD +SLC 39 24 3 4 13 2 4 SLD +SLC 39 24 9 4 13 8 4 SLD +SLC 39 37 2 4 15 1 4 SLD +SLC 39 37 8 4 15 7 4 SLD +SLC 39 52 1 4 28 0 4 SLD +SLC 39 52 7 4 28 6 4 SLD +SLD 39 80 0 4 208 0 4 SLD +SLD 39 80 6 4 16 6 4 SLD +TAP 39 144 6 4 +SLD 39 192 6 4 96 7 2 SLD +AHD 39 288 0 4 SLD 96 DEF +FLK 39 288 4 3 L +AUR 39 288 4 3 FLK DEF +AHD 39 288 7 2 SLD 96 DEF +FLK 39 288 9 3 L +AUR 39 288 9 3 FLK DEF +SLC 40 0 0 4 7 1 4 SLD +SLC 40 0 6 4 7 7 4 SLD +SLC 40 7 1 4 8 2 4 SLD +SLC 40 7 7 4 8 8 4 SLD +SLC 40 15 2 4 9 3 4 SLD +SLC 40 15 8 4 9 9 4 SLD +SLC 40 24 3 4 13 4 4 SLD +SLC 40 24 9 4 13 10 4 SLD +SLC 40 37 4 4 15 5 4 SLD +SLC 40 37 10 4 15 11 4 SLD +SLC 40 52 5 4 28 6 4 SLD +SLC 40 52 11 4 28 12 4 SLD +SLD 40 80 6 4 16 6 4 SLD +SLD 40 80 12 4 208 12 4 SLD +TAP 40 144 6 4 +SLD 40 192 6 4 96 7 2 SLD +FLK 40 288 4 3 L +AUL 40 288 4 3 FLK DEF +AHD 40 288 7 2 SLD 96 DEF +FLK 40 288 9 3 L +AUL 40 288 9 3 FLK DEF +AHD 40 288 12 4 SLD 96 DEF +SLC 41 0 6 4 7 5 4 SLD +SLC 41 0 12 4 7 11 4 SLD +SLC 41 7 5 4 8 4 4 SLD +SLC 41 7 11 4 8 10 4 SLD +SLC 41 15 4 4 9 3 4 SLD +SLC 41 15 10 4 9 9 4 SLD +SLC 41 24 3 4 13 2 4 SLD +SLC 41 24 9 4 13 8 4 SLD +SLC 41 37 2 4 15 1 4 SLD +SLC 41 37 8 4 15 7 4 SLD +SLC 41 52 1 4 28 0 4 SLD +SLC 41 52 7 4 28 6 4 SLD +SLD 41 80 0 4 208 0 4 SLD +SLD 41 80 6 4 16 6 4 SLD +TAP 41 144 6 4 +SLD 41 192 6 4 96 7 2 SLD +FLK 41 288 4 3 L +FLK 41 288 9 3 L +TAP 42 0 2 4 +AHD 42 0 2 4 TAP 96 DEF +TAP 42 0 10 4 +TAP 42 48 6 4 +TAP 42 96 2 4 +TAP 42 96 10 4 +AHD 42 96 10 4 TAP 96 DEF +TAP 42 144 6 4 +CHR 42 192 0 8 UP +CHR 42 192 10 4 UP +TAP 42 288 0 4 +TAP 42 288 6 4 +TAP 42 336 4 4 +TAP 42 336 10 4 +SLC 43 0 6 4 7 5 4 SLD +SLC 43 0 12 4 7 11 4 SLD +SLC 43 7 5 4 8 4 4 SLD +SLC 43 7 11 4 8 10 4 SLD +SLC 43 15 4 4 9 3 4 SLD +SLC 43 15 10 4 9 9 4 SLD +SLC 43 24 3 4 13 2 4 SLD +SLC 43 24 9 4 13 8 4 SLD +SLC 43 37 2 4 15 1 4 SLD +SLC 43 37 8 4 15 7 4 SLD +SLC 43 52 1 4 28 0 4 SLD +SLC 43 52 7 4 28 6 4 SLD +SLD 43 80 0 4 208 0 4 SLD +SLD 43 80 6 4 16 6 4 SLD +TAP 43 144 6 4 +SLD 43 192 6 4 96 7 2 SLD +AHD 43 288 0 4 SLD 96 DEF +FLK 43 288 4 3 L +AUR 43 288 4 3 FLK DEF +AHD 43 288 7 2 SLD 96 DEF +FLK 43 288 9 3 L +AUR 43 288 9 3 FLK DEF +SLC 44 0 0 4 7 1 4 SLD +SLC 44 0 6 4 7 7 4 SLD +SLC 44 7 1 4 8 2 4 SLD +SLC 44 7 7 4 8 8 4 SLD +SLC 44 15 2 4 9 3 4 SLD +SLC 44 15 8 4 9 9 4 SLD +SLC 44 24 3 4 13 4 4 SLD +SLC 44 24 9 4 13 10 4 SLD +SLC 44 37 4 4 15 5 4 SLD +SLC 44 37 10 4 15 11 4 SLD +SLC 44 52 5 4 28 6 4 SLD +SLC 44 52 11 4 28 12 4 SLD +SLD 44 80 6 4 16 6 4 SLD +SLD 44 80 12 4 208 12 4 SLD +TAP 44 144 6 4 +SLD 44 192 6 4 96 7 2 SLD +FLK 44 288 4 3 L +FLK 44 288 9 3 L +TAP 45 0 0 4 +SLD 45 0 7 8 96 1 6 SLD +SLD 45 96 1 6 96 9 5 SLD +TAP 45 96 11 4 +TAP 45 192 2 4 +SLD 45 192 9 5 96 2 4 SLD +SLD 45 288 2 4 96 11 2 SLD +TAP 45 288 9 4 +SLC 46 0 3 2 96 3 2 SLD +SLC 46 0 11 2 96 11 2 SLD +SLD 46 96 3 2 96 0 16 SLD +SLD 46 96 11 2 96 0 16 SLD +AHD 46 192 0 16 SLD 96 DEF +AHD 46 192 0 16 SLD 96 DEF +CHR 46 288 0 16 UP +ADW 46 288 0 16 CHR DEF +CHR 47 0 0 4 UP +AHD 47 0 0 4 CHR 96 DEF +CHR 47 0 12 4 UP +TAP 47 72 5 6 +TAP 47 96 0 4 +TAP 47 144 5 6 +SLC 47 192 0 4 6 2 4 SLD +TAP 47 192 12 4 +AUL 47 192 12 4 TAP DEF +SLC 47 198 2 4 12 5 4 SLD +SLC 47 210 5 4 15 8 4 SLD +SLC 47 225 8 4 7 9 4 SLD +SLC 47 232 9 4 8 10 4 SLD +SLC 47 240 10 4 10 11 4 SLD +SLC 47 250 11 4 18 12 4 SLD +SLD 47 268 12 4 20 12 4 SLD +TAP 47 288 4 4 +SLC 47 288 4 4 2 5 4 SLD +ADR 47 288 4 4 TAP DEF +SLC 47 290 5 4 9 8 4 SLD +SLC 47 299 8 4 4 9 4 SLD +SLC 47 303 9 4 5 10 4 SLD +SLC 47 308 10 4 6 11 4 SLD +SLC 47 314 11 4 12 12 4 SLD +SLC 47 326 12 4 10 12 4 SLD +TAP 47 336 0 8 +SLC 47 336 12 4 10 12 4 SLD +SLC 47 346 12 4 24 11 4 SLD +SLC 47 370 11 4 13 10 4 SLD +SLC 47 383 10 4 1 2 12 SLD +CHR 48 0 2 4 UP +SLC 48 0 2 12 1 2 4 SLD +SLC 48 1 2 4 12 1 4 SLD +SLC 48 13 1 4 16 0 4 SLD +SLC 48 29 0 4 8 0 4 SLD +SLC 48 37 0 4 11 0 4 SLD +SLC 48 48 0 4 10 0 4 SLD +TAP 48 48 8 8 +SLC 48 58 0 4 23 1 4 SLD +SLC 48 81 1 4 14 2 4 SLD +SLC 48 95 2 4 1 2 12 SLD +SLC 48 96 2 12 1 10 4 SLD +CHR 48 96 10 4 UP +SLC 48 97 10 4 11 11 4 SLD +SLC 48 108 11 4 15 12 4 SLD +SLC 48 123 12 4 10 12 4 SLD +SLC 48 133 12 4 11 12 4 SLD +TAP 48 144 0 8 +SLC 48 144 12 4 10 12 4 SLD +SLC 48 154 12 4 23 11 4 SLD +SLD 48 177 11 4 15 10 4 SLD +CHR 48 192 0 6 UP +AUL 48 192 0 6 CHR DEF +FLK 48 192 6 6 L +AUL 48 192 6 6 FLK DEF +CHR 48 288 0 4 UP +FLK 48 288 4 4 L +ADR 48 288 4 4 FLK DEF +CHR 48 288 8 4 UP +FLK 48 288 12 4 L +ADR 48 288 12 4 FLK DEF +TAP 49 0 0 4 +TAP 49 0 12 4 +AHD 49 0 12 4 TAP 96 DEF +TAP 49 72 5 6 +TAP 49 96 12 4 +TAP 49 144 5 6 +TAP 49 192 0 4 +AIR 49 192 0 4 TAP DEF +SLC 49 192 12 4 4 10 4 SLD +SLC 49 196 10 4 6 8 4 SLD +SLC 49 202 8 4 9 6 4 SLD +SLC 49 211 6 4 6 5 4 SLD +SLC 49 217 5 4 9 4 4 SLD +SLC 49 226 4 4 13 3 4 SLD +SLC 49 239 3 4 8 3 4 SLD +SLC 49 247 3 4 16 4 5 SLD +SLC 49 263 4 5 13 5 6 SLD +SLD 49 276 5 6 12 5 6 SLD +SLC 49 288 0 4 24 5 6 SLD +SLD 49 288 5 6 288 5 6 SLD +SLC 49 312 5 6 264 5 6 SLD +SLC 49 336 12 4 24 5 6 SLD +SLC 49 360 5 6 216 5 6 SLD +SLC 50 0 0 4 24 5 6 SLD +SLC 50 24 5 6 168 5 6 SLD +SLC 50 48 12 4 24 5 6 SLD +SLC 50 72 5 6 120 5 6 SLD +SLC 50 96 0 4 24 5 6 SLD +SLC 50 120 5 6 72 5 6 SLD +SLC 50 144 12 4 24 5 6 SLD +SLC 50 168 5 6 24 5 6 SLD +SLD 50 192 5 6 48 0 4 SLD +SLD 50 192 5 6 56 2 4 SLD +SLD 50 192 5 6 64 4 4 SLD +SLD 50 192 5 6 72 6 4 SLD +SLD 50 192 5 6 80 8 4 SLD +SLD 50 192 5 6 88 10 4 SLD +SLD 50 192 5 6 96 12 4 SLD +TAP 51 0 4 4 +TAP 51 0 12 4 +TAP 51 48 0 4 +TAP 51 48 8 8 +TAP 51 96 0 8 +TAP 51 96 12 4 +TAP 51 144 2 6 +TAP 51 144 8 6 +TAP 51 192 4 4 +ASC 51 192 4 4 TAP 5 12 3 4 5 DEF +SLC 51 192 8 4 12 9 4 SLD +ASC 51 204 3 4 ASC 5 14 2 4 5 DEF +SLC 51 204 9 4 14 10 4 SLD +ASC 51 218 2 4 ASC 5 19 1 4 5 DEF +SLC 51 218 10 4 19 11 4 SLD +ASC 51 237 1 4 ASC 5 37 0 4 5 DEF +SLC 51 237 11 4 37 12 4 SLD +ASD 51 274 0 4 ASC 5 14 0 4 5 DEF +SLD 51 274 12 4 14 12 4 SLD +CHR 51 288 0 4 UP +ADW 51 288 0 4 CHR DEF +TAP 51 336 8 8 +TAP 52 0 0 4 +TAP 52 0 8 4 +TAP 52 48 0 8 +TAP 52 48 12 4 +TAP 52 96 0 4 +TAP 52 96 8 8 +TAP 52 144 2 6 +TAP 52 144 8 6 +SLC 52 192 4 4 12 3 4 SLD +TAP 52 192 8 4 +ASC 52 192 8 4 TAP 5 12 9 4 5 DEF +SLC 52 204 3 4 14 2 4 SLD +ASC 52 204 9 4 ASC 5 14 10 4 5 DEF +SLC 52 218 2 4 19 1 4 SLD +ASC 52 218 10 4 ASC 5 19 11 4 5 DEF +SLC 52 237 1 4 37 0 4 SLD +ASC 52 237 11 4 ASC 5 37 12 4 5 DEF +SLD 52 274 0 4 14 0 4 SLD +ASD 52 274 12 4 ASC 5 14 12 4 5 DEF +CHR 52 288 12 4 UP +ADW 52 288 12 4 CHR DEF +TAP 52 336 0 3 +SLC 53 0 0 3 48 5 3 SLD +SLC 53 0 8 3 48 13 3 SLD +SLC 53 48 5 3 144 5 3 SLD +SLC 53 48 13 3 144 13 3 SLD +TAP 53 96 9 3 +TAP 53 144 9 3 +SLC 53 192 5 3 48 2 3 SLD +TAP 53 192 9 3 +SLC 53 192 13 3 48 11 3 SLD +SLC 53 240 2 3 144 2 3 SLD +SLC 53 240 11 3 144 11 3 SLD +TAP 53 288 6 4 +TAP 53 336 6 4 +SLD 54 0 2 3 96 6 3 SLD +HLD 54 0 6 4 96 +SLD 54 0 11 3 96 7 3 SLD +FLK 54 96 2 4 L +FLK 54 96 10 4 L +CHR 54 192 3 3 UP +AHD 54 192 3 3 CHR 96 DEF +CHR 54 192 10 3 UP +AHD 54 192 10 3 CHR 96 DEF +CHR 54 288 0 3 UP +ADW 54 288 0 3 CHR DEF +CHR 54 288 3 3 UP +ADW 54 288 3 3 CHR DEF +CHR 54 288 6 4 UP +ADW 54 288 6 4 CHR DEF +CHR 54 288 10 3 UP +ADW 54 288 10 3 CHR DEF +CHR 54 288 13 3 UP +ADW 54 288 13 3 CHR DEF +CHR 55 0 0 4 UP +AHD 55 0 0 4 CHR 96 DEF +CHR 55 0 12 4 UP +TAP 55 72 5 6 +TAP 55 96 0 4 +TAP 55 144 5 6 +SLC 55 192 0 4 6 2 4 SLD +TAP 55 192 12 4 +AUL 55 192 12 4 TAP DEF +SLC 55 198 2 4 12 5 4 SLD +SLC 55 210 5 4 15 8 4 SLD +SLC 55 225 8 4 7 9 4 SLD +SLC 55 232 9 4 8 10 4 SLD +SLC 55 240 10 4 10 11 4 SLD +SLC 55 250 11 4 18 12 4 SLD +SLD 55 268 12 4 20 12 4 SLD +TAP 55 288 4 4 +SLC 55 288 4 4 2 5 4 SLD +ADR 55 288 4 4 TAP DEF +SLC 55 290 5 4 9 8 4 SLD +SLC 55 299 8 4 4 9 4 SLD +SLC 55 303 9 4 5 10 4 SLD +SLC 55 308 10 4 6 11 4 SLD +SLC 55 314 11 4 12 12 4 SLD +SLC 55 326 12 4 10 12 4 SLD +TAP 55 336 0 8 +SLC 55 336 12 4 11 12 4 SLD +SLC 55 347 12 4 15 11 4 SLD +SLC 55 362 11 4 9 10 4 SLD +SLC 55 371 10 4 7 9 4 SLD +SLD 55 378 9 4 6 8 4 SLD +SLC 56 0 0 4 2 1 4 SLD +SLC 56 2 1 4 9 4 4 SLD +SLC 56 11 4 4 4 5 4 SLD +SLC 56 15 5 4 5 6 4 SLD +SLC 56 20 6 4 6 7 4 SLD +SLC 56 26 7 4 8 8 4 SLD +SLC 56 34 8 4 11 9 4 SLD +SLC 56 45 9 4 3 9 4 SLD +TAP 56 48 0 6 +SLC 56 48 9 4 3 9 4 SLD +SLC 56 51 9 4 11 8 4 SLD +SLC 56 62 8 4 8 7 4 SLD +SLC 56 70 7 4 6 6 4 SLD +SLC 56 76 6 4 5 5 4 SLD +SLC 56 81 5 4 4 4 4 SLD +SLC 56 85 4 4 9 1 4 SLD +SLD 56 94 1 4 2 0 4 SLD +SLC 56 96 10 4 15 11 4 SLD +SLC 56 111 11 4 21 12 4 SLD +SLC 56 132 12 4 14 12 4 SLD +TAP 56 144 0 8 +SLC 56 146 12 4 17 11 4 SLD +SLC 56 163 11 4 9 10 4 SLD +SLC 56 172 10 4 7 9 4 SLD +SLC 56 179 9 4 5 8 4 SLD +SLD 56 184 8 4 8 6 4 SLD +SLC 56 192 0 4 2 1 4 SLD +AUL 56 192 6 4 SLD DEF +SLC 56 194 1 4 3 2 4 SLD +SLC 56 197 2 4 8 4 4 SLD +SLC 56 205 4 4 5 5 4 SLD +SLC 56 210 5 4 6 6 4 SLD +SLC 56 216 6 4 9 7 4 SLD +SLC 56 225 7 4 12 8 4 SLD +SLC 56 237 8 4 3 8 4 SLD +SLC 56 240 8 4 4 8 4 SLD +SLC 56 244 8 4 13 7 4 SLD +SLC 56 257 7 4 9 6 4 SLD +SLC 56 266 6 4 7 5 4 SLD +SLC 56 273 5 4 6 4 4 SLD +SLC 56 279 4 4 5 3 4 SLD +SLD 56 284 3 4 4 2 4 SLD +CHR 56 288 0 4 UP +AUL 56 288 0 4 CHR DEF +AUL 56 288 2 4 SLD DEF +FLK 56 288 8 8 L +ADR 56 288 8 8 FLK DEF +CHR 57 0 0 4 UP +CHR 57 0 12 4 UP +AHD 57 0 12 4 CHR 96 DEF +TAP 57 72 5 6 +TAP 57 96 12 4 +TAP 57 144 5 6 +TAP 57 192 0 4 +AUR 57 192 0 4 TAP DEF +SLC 57 192 12 4 6 10 4 SLD +SLC 57 198 10 4 12 7 4 SLD +SLC 57 210 7 4 15 4 4 SLD +SLC 57 225 4 4 7 3 4 SLD +SLC 57 232 3 4 8 2 4 SLD +SLC 57 240 2 4 10 1 4 SLD +SLC 57 250 1 4 18 0 4 SLD +SLD 57 268 0 4 20 0 4 SLD +TAP 57 288 8 4 +SLC 57 288 8 4 2 7 4 SLD +ADL 57 288 8 4 TAP DEF +SLC 57 290 7 4 9 4 4 SLD +SLC 57 299 4 4 4 3 4 SLD +SLC 57 303 3 4 5 2 4 SLD +SLC 57 308 2 4 6 1 4 SLD +SLC 57 314 1 4 12 0 4 SLD +SLC 57 326 0 4 10 0 4 SLD +SLC 57 336 0 4 11 0 4 SLD +TAP 57 336 8 8 +SLC 57 347 0 4 15 1 4 SLD +SLC 57 362 1 4 9 2 4 SLD +SLC 57 371 2 4 7 3 4 SLD +SLD 57 378 3 4 6 4 4 SLD +SLC 58 0 12 4 2 11 4 SLD +SLC 58 2 11 4 9 8 4 SLD +SLC 58 11 8 4 4 7 4 SLD +SLC 58 15 7 4 5 6 4 SLD +SLC 58 20 6 4 6 5 4 SLD +SLC 58 26 5 4 8 4 4 SLD +SLC 58 34 4 4 11 3 4 SLD +SLC 58 45 3 4 3 3 4 SLD +SLC 58 48 3 4 3 3 4 SLD +TAP 58 48 10 6 +SLC 58 51 3 4 11 4 4 SLD +SLC 58 62 4 4 8 5 4 SLD +SLC 58 70 5 4 6 6 4 SLD +SLC 58 76 6 4 5 7 4 SLD +SLC 58 81 7 4 4 8 4 SLD +SLC 58 85 8 4 9 11 4 SLD +SLD 58 94 11 4 2 12 4 SLD +SLC 58 96 2 4 15 1 4 SLD +SLC 58 111 1 4 21 0 4 SLD +SLC 58 132 0 4 14 0 4 SLD +TAP 58 144 8 8 +SLC 58 146 0 4 17 1 4 SLD +SLC 58 163 1 4 9 2 4 SLD +SLC 58 172 2 4 7 3 4 SLD +SLC 58 179 3 4 5 4 4 SLD +SLD 58 184 4 4 8 6 4 SLD +AUR 58 192 6 4 SLD DEF +SLC 58 192 12 4 2 11 4 SLD +SLC 58 194 11 4 3 10 4 SLD +SLC 58 197 10 4 8 8 4 SLD +SLC 58 205 8 4 5 7 4 SLD +SLC 58 210 7 4 6 6 4 SLD +SLC 58 216 6 4 9 5 4 SLD +SLC 58 225 5 4 12 4 4 SLD +SLC 58 237 4 4 3 4 4 SLD +SLC 58 240 4 4 4 4 4 SLD +SLC 58 244 4 4 13 5 4 SLD +SLC 58 257 5 4 9 6 4 SLD +SLC 58 266 6 4 7 7 4 SLD +SLC 58 273 7 4 6 8 4 SLD +SLC 58 279 8 4 5 9 4 SLD +SLD 58 284 9 4 4 10 4 SLD +FLK 58 288 0 8 L +ADL 58 288 0 8 FLK DEF +AUR 58 288 10 4 SLD DEF +CHR 58 288 12 4 UP +AUR 58 288 12 4 CHR DEF +TAP 59 0 4 4 +TAP 59 0 10 6 +TAP 59 48 0 6 +TAP 59 48 8 4 +TAP 59 96 4 6 +TAP 59 96 12 4 +TAP 59 144 0 4 +TAP 59 144 6 6 +TAP 59 192 6 4 +ASC 59 192 6 4 TAP 5 7 5 4 5 DEF +SLC 59 192 12 4 7 11 4 SLD +ASC 59 199 5 4 ASC 5 8 4 4 5 DEF +SLC 59 199 11 4 8 10 4 SLD +ASC 59 207 4 4 ASC 5 9 3 4 5 DEF +SLC 59 207 10 4 9 9 4 SLD +ASC 59 216 3 4 ASC 5 11 2 4 5 DEF +SLC 59 216 9 4 11 8 4 SLD +ASC 59 227 2 4 ASC 5 15 1 4 5 DEF +SLC 59 227 8 4 15 7 4 SLD +ASC 59 242 1 4 ASC 5 27 0 4 5 DEF +SLC 59 242 7 4 27 6 4 SLD +ASD 59 269 0 4 ASC 5 19 0 4 5 DEF +SLD 59 269 6 4 19 6 4 SLD +CHR 59 288 0 4 UP +ADW 59 288 0 4 CHR DEF +TAP 59 336 6 4 +TAP 60 0 4 4 +TAP 60 0 10 6 +TAP 60 48 0 6 +TAP 60 48 8 4 +TAP 60 96 4 6 +TAP 60 96 12 4 +TAP 60 144 0 4 +TAP 60 144 6 6 +SLC 60 192 6 4 7 5 4 SLD +TAP 60 192 12 4 +ASC 60 192 12 4 TAP 5 7 11 4 5 DEF +SLC 60 199 5 4 8 4 4 SLD +ASC 60 199 11 4 ASC 5 8 10 4 5 DEF +SLC 60 207 4 4 9 3 4 SLD +ASC 60 207 10 4 ASC 5 9 9 4 5 DEF +SLC 60 216 3 4 11 2 4 SLD +ASC 60 216 9 4 ASC 5 11 8 4 5 DEF +SLC 60 227 2 4 15 1 4 SLD +ASC 60 227 8 4 ASC 5 15 7 4 5 DEF +SLC 60 242 1 4 27 0 4 SLD +ASC 60 242 7 4 ASC 5 27 6 4 5 DEF +SLD 60 269 0 4 19 0 4 SLD +ASD 60 269 6 4 ASC 5 19 6 4 5 DEF +CHR 60 288 6 4 UP +ADW 60 288 6 4 CHR DEF +TAP 60 336 3 4 +TAP 60 336 9 4 +SLC 61 0 0 4 17 1 4 SLD +SLC 61 0 12 4 17 11 4 SLD +SLC 61 17 1 4 19 2 4 SLD +SLC 61 17 11 4 19 10 4 SLD +SLC 61 36 2 4 20 3 4 SLD +SLC 61 36 10 4 20 9 4 SLD +SLC 61 56 3 4 27 4 4 SLD +SLC 61 56 9 4 27 8 4 SLD +SLD 61 83 4 4 13 4 4 SLD +SLD 61 83 8 4 13 8 4 SLD +TAP 61 144 4 3 +TAP 61 144 9 3 +HLD 61 192 0 3 96 +HLD 61 192 3 3 96 +HLD 61 192 6 4 96 +HLD 61 192 10 3 96 +HLD 61 192 13 3 96 +AIR 61 288 0 3 HLD DEF +AIR 61 288 3 3 HLD DEF +AIR 61 288 6 4 HLD DEF +AIR 61 288 10 3 HLD DEF +AIR 61 288 13 3 HLD DEF +SLD 62 0 3 3 96 1 3 SLD +SLD 62 0 3 3 96 5 3 SLD +SLC 62 96 13 2 3 10 2 SLD +SLC 62 99 10 2 2 10 2 SLD +SLC 62 101 10 2 29 10 2 SLD +SLC 62 130 10 2 1 8 6 SLD +SLC 62 131 8 6 4 8 6 SLD +SLC 62 135 8 6 1 10 2 SLD +SLD 62 136 10 2 8 10 2 SLD +SLC 62 192 0 4 1 0 12 SLD +TAP 62 192 4 4 +AHX 62 192 4 4 TAP 96 DEF +TAP 62 192 8 4 +AHX 62 192 8 4 TAP 96 DEF +SLC 62 193 0 12 23 0 12 SLD +SLC 62 216 0 12 1 0 4 SLD +SLD 62 217 0 4 71 0 4 SLD +ALD 62 288 4 8 1 5 5 4 8 1 +CHR 63 0 0 4 UP +CHR 63 0 6 4 UP +ASC 63 0 6 4 CHR 5 6 7 4 5 DEF +ASC 63 6 7 4 ASC 5 7 8 4 5 DEF +ASC 63 13 8 4 ASC 5 8 9 4 5 DEF +ASC 63 21 9 4 ASC 5 11 10 4 5 DEF +ASC 63 32 10 4 ASC 5 13 11 4 5 DEF +ASC 63 45 11 4 ASC 5 25 12 4 5 DEF +ASD 63 70 12 4 ASC 5 26 12 4 5 DEF +TAP 63 72 5 6 +TAP 63 96 12 4 +TAP 63 144 5 6 +CHR 63 192 6 4 UP +ASC 63 192 6 4 CHR 5 6 5 4 5 DEF +CHR 63 192 12 4 UP +ASC 63 198 5 4 ASC 5 7 4 4 5 DEF +ASC 63 205 4 4 ASC 5 8 3 4 5 DEF +ASC 63 213 3 4 ASC 5 11 2 4 5 DEF +ASC 63 224 2 4 ASC 5 13 1 4 5 DEF +ASC 63 237 1 4 ASC 5 25 0 4 5 DEF +ASD 63 262 0 4 ASC 5 26 0 4 5 DEF +TAP 63 264 5 6 +TAP 63 288 0 4 +TAP 63 336 5 6 +CHR 64 0 0 4 UP +CHR 64 0 6 4 UP +ASC 64 0 6 4 CHR 5 6 7 4 5 DEF +ASC 64 6 7 4 ASC 5 7 8 4 5 DEF +ASC 64 13 8 4 ASC 5 8 9 4 5 DEF +ASC 64 21 9 4 ASC 5 11 10 4 5 DEF +ASC 64 32 10 4 ASC 5 13 11 4 5 DEF +ASC 64 45 11 4 ASC 5 25 12 4 5 DEF +ASD 64 70 12 4 ASC 5 26 12 4 5 DEF +TAP 64 72 5 6 +TAP 64 96 12 4 +TAP 64 144 5 6 +TAP 64 192 0 4 +TAP 64 240 4 4 +TAP 64 288 8 4 +TAP 64 336 12 4 +CHR 65 0 6 4 UP +ASC 65 0 6 4 CHR 5 6 5 4 5 DEF +CHR 65 0 12 4 UP +ASC 65 6 5 4 ASC 5 7 4 4 5 DEF +ASC 65 13 4 4 ASC 5 8 3 4 5 DEF +ASC 65 21 3 4 ASC 5 11 2 4 5 DEF +ASC 65 32 2 4 ASC 5 13 1 4 5 DEF +ASC 65 45 1 4 ASC 5 25 0 4 5 DEF +ASD 65 70 0 4 ASC 5 26 0 4 5 DEF +TAP 65 72 5 6 +TAP 65 96 0 4 +TAP 65 144 5 6 +CHR 65 192 0 4 UP +CHR 65 192 6 4 UP +ASC 65 192 6 4 CHR 5 6 7 4 5 DEF +ASC 65 198 7 4 ASC 5 7 8 4 5 DEF +ASC 65 205 8 4 ASC 5 8 9 4 5 DEF +ASC 65 213 9 4 ASC 5 11 10 4 5 DEF +ASC 65 224 10 4 ASC 5 13 11 4 5 DEF +ASC 65 237 11 4 ASC 5 25 12 4 5 DEF +ASD 65 262 12 4 ASC 5 26 12 4 5 DEF +TAP 65 264 5 6 +TAP 65 288 12 4 +TAP 65 336 5 6 +CHR 66 0 6 4 UP +ASC 66 0 6 4 CHR 5 6 5 4 5 DEF +CHR 66 0 12 4 UP +ASC 66 6 5 4 ASC 5 7 4 4 5 DEF +ASC 66 13 4 4 ASC 5 8 3 4 5 DEF +ASC 66 21 3 4 ASC 5 11 2 4 5 DEF +ASC 66 32 2 4 ASC 5 13 1 4 5 DEF +ASC 66 45 1 4 ASC 5 25 0 4 5 DEF +ASD 66 70 0 4 ASC 5 26 0 4 5 DEF +TAP 66 72 5 6 +TAP 66 96 0 4 +TAP 66 144 5 6 +TAP 66 192 0 4 +TAP 66 192 12 4 +TAP 66 240 4 4 +TAP 66 240 8 4 +TAP 66 288 0 4 +AIR 66 288 0 4 TAP DEF +TAP 66 288 12 4 +AIR 66 288 12 4 TAP DEF +SLC 67 0 0 4 4 1 4 SLD +TAP 67 0 12 4 +AHD 67 0 12 4 TAP 96 DEF +SLC 67 4 1 4 5 2 4 SLD +SLC 67 9 2 4 5 3 4 SLD +SLC 67 14 3 4 7 4 4 SLD +SLC 67 21 4 4 9 5 4 SLD +SLC 67 30 5 4 14 6 4 SLD +SLC 67 44 6 4 4 6 4 SLD +SLC 67 48 6 4 4 6 4 SLD +SLC 67 52 6 4 14 5 4 SLD +SLC 67 66 5 4 9 4 4 SLD +SLC 67 75 4 4 7 3 4 SLD +SLC 67 82 3 4 5 2 4 SLD +SLC 67 87 2 4 5 1 4 SLD +SLD 67 92 1 4 4 0 4 SLD +AHD 67 96 0 4 SLD 96 DEF +SLC 67 96 12 4 4 11 4 SLD +SLC 67 100 11 4 5 10 4 SLD +SLC 67 105 10 4 5 9 4 SLD +SLC 67 110 9 4 7 8 4 SLD +SLC 67 117 8 4 9 7 4 SLD +SLC 67 126 7 4 14 6 4 SLD +SLC 67 140 6 4 4 6 4 SLD +SLC 67 144 6 4 4 6 4 SLD +SLC 67 148 6 4 14 7 4 SLD +SLC 67 162 7 4 9 8 4 SLD +SLC 67 171 8 4 7 9 4 SLD +SLC 67 178 9 4 5 10 4 SLD +SLC 67 183 10 4 5 11 4 SLD +SLC 67 188 11 4 4 12 4 SLD +TAP 67 192 0 4 +SLC 67 192 12 4 96 12 4 SLD +TAP 67 288 0 4 +SLD 67 288 12 4 96 8 4 SLD +TAP 67 336 2 4 +SLD 68 0 4 4 96 0 4 SLD +TAP 68 48 12 4 +SLD 68 96 0 4 192 0 4 SLD +TAP 68 96 10 4 +SLC 68 144 8 4 15 7 4 SLD +SLC 68 159 7 4 18 6 4 SLD +SLC 68 177 6 4 23 5 4 SLD +SLC 68 200 5 4 30 4 4 SLD +SLD 68 230 4 4 10 4 4 SLD +FLK 68 240 8 8 L +FLK 68 288 4 8 L +HLD 69 0 2 3 96 +HLD 69 0 11 3 96 +TAP 69 96 5 3 +TAP 69 120 8 3 +TAP 69 144 5 3 +TAP 69 192 0 4 +TAP 69 192 12 4 +TAP 69 264 2 4 +TAP 69 264 10 4 +TAP 69 336 5 3 +TAP 69 336 8 3 +SLC 70 0 6 4 15 5 4 SLD +SLC 70 0 6 4 15 7 4 SLD +SLC 70 15 5 4 17 4 4 SLD +SLC 70 15 7 4 17 8 4 SLD +SLC 70 32 4 4 19 3 4 SLD +SLC 70 32 8 4 19 9 4 SLD +SLC 70 51 3 4 24 2 4 SLD +SLC 70 51 9 4 24 10 4 SLD +SLC 70 75 2 4 33 1 4 SLD +SLC 70 75 10 4 33 11 4 SLD +SLC 70 108 1 4 52 0 4 SLD +SLC 70 108 11 4 52 12 4 SLD +SLD 70 160 0 4 32 0 4 SLD +SLD 70 160 12 4 32 12 4 SLD +ASD 70 192 0 4 SLD 5 192 0 4 5 DEF +AHD 70 192 12 4 SLD 192 DEF +ASC 71 0 0 4 ASD 5 3 1 4 5 DEF +ALD 71 0 0 4 1 5 4 0 4 3 +SLC 71 0 12 4 3 11 4 SLD +ASC 71 3 1 4 ASC 5 10 4 4 5 DEF +SLC 71 3 11 4 10 8 4 SLD +ASC 71 13 4 4 ASC 5 11 7 4 5 DEF +SLC 71 13 8 4 11 5 4 SLD +SLC 71 24 5 4 5 4 4 SLD +ASC 71 24 7 4 ASC 5 5 8 4 5 DEF +SLC 71 29 4 4 5 3 4 SLD +ASC 71 29 8 4 ASC 5 5 9 4 5 DEF +SLC 71 34 3 4 8 2 4 SLD +ASC 71 34 9 4 ASC 5 8 10 4 5 DEF +SLC 71 42 2 4 10 1 4 SLD +ASC 71 42 10 4 ASC 5 10 11 4 5 DEF +SLC 71 52 1 4 19 0 4 SLD +ASC 71 52 11 4 ASC 5 19 12 4 5 DEF +SLD 71 71 0 4 25 0 4 SLD +ASD 71 71 12 4 ASC 5 25 12 4 5 DEF +SLC 71 96 0 4 3 1 4 SLD +ASC 71 96 12 4 ASD 5 3 11 4 5 DEF +ALD 71 96 12 4 1 5 4 12 4 3 +SLC 71 99 1 4 10 4 4 SLD +ASC 71 99 11 4 ASC 5 10 8 4 5 DEF +SLC 71 109 4 4 11 7 4 SLD +ASC 71 109 8 4 ASC 5 11 5 4 5 DEF +ASC 71 120 5 4 ASC 5 5 4 4 5 DEF +SLC 71 120 7 4 5 8 4 SLD +ASC 71 125 4 4 ASC 5 5 3 4 5 DEF +SLC 71 125 8 4 5 9 4 SLD +ASC 71 130 3 4 ASC 5 8 2 4 5 DEF +SLC 71 130 9 4 8 10 4 SLD +ASC 71 138 2 4 ASC 5 10 1 4 5 DEF +SLC 71 138 10 4 10 11 4 SLD +ASC 71 148 1 4 ASC 5 19 0 4 5 DEF +SLC 71 148 11 4 19 12 4 SLD +ASD 71 167 0 4 ASC 5 25 0 4 5 DEF +SLD 71 167 12 4 25 12 4 SLD +ASC 71 192 0 4 ASD 5 3 1 4 5 DEF +ALD 71 192 0 4 1 5 4 0 4 3 +SLC 71 192 12 4 3 11 4 SLD +ASC 71 195 1 4 ASC 5 10 4 4 5 DEF +SLC 71 195 11 4 10 8 4 SLD +ASC 71 205 4 4 ASC 5 11 7 4 5 DEF +SLC 71 205 8 4 11 5 4 SLD +SLC 71 216 5 4 5 4 4 SLD +ASC 71 216 7 4 ASC 5 5 8 4 5 DEF +SLC 71 221 4 4 5 3 4 SLD +ASC 71 221 8 4 ASC 5 5 9 4 5 DEF +SLC 71 226 3 4 8 2 4 SLD +ASC 71 226 9 4 ASC 5 8 10 4 5 DEF +SLC 71 234 2 4 10 1 4 SLD +ASC 71 234 10 4 ASC 5 10 11 4 5 DEF +SLC 71 244 1 4 19 0 4 SLD +ASC 71 244 11 4 ASC 5 19 12 4 5 DEF +SLD 71 263 0 4 25 0 4 SLD +ASD 71 263 12 4 ASC 5 25 12 4 5 DEF +SLC 71 288 0 4 3 1 4 SLD +ASC 71 288 12 4 ASD 5 3 11 4 5 DEF +ALD 71 288 12 4 1 5 4 12 4 3 +SLC 71 291 1 4 10 4 4 SLD +ASC 71 291 11 4 ASC 5 10 8 4 5 DEF +SLC 71 301 4 4 11 7 4 SLD +ASC 71 301 8 4 ASC 5 11 5 4 5 DEF +ASC 71 312 5 4 ASC 5 5 4 4 5 DEF +SLC 71 312 7 4 5 8 4 SLD +ASC 71 317 4 4 ASC 5 5 3 4 5 DEF +SLC 71 317 8 4 5 9 4 SLD +ASC 71 322 3 4 ASC 5 8 2 4 5 DEF +SLC 71 322 9 4 8 10 4 SLD +ASC 71 330 2 4 ASC 5 10 1 4 5 DEF +SLC 71 330 10 4 10 11 4 SLD +ASC 71 340 1 4 ASC 5 19 0 4 5 DEF +SLC 71 340 11 4 19 12 4 SLD +ASD 71 359 0 4 ASC 5 25 0 4 5 DEF +SLD 71 359 12 4 25 12 4 SLD +ASC 72 0 0 4 ASD 5 3 1 4 5 DEF +ALD 72 0 0 4 1 5 4 0 4 3 +SLC 72 0 12 4 3 11 4 SLD +ASC 72 3 1 4 ASC 5 10 4 4 5 DEF +SLC 72 3 11 4 10 8 4 SLD +ASC 72 13 4 4 ASC 5 11 7 4 5 DEF +SLC 72 13 8 4 11 5 4 SLD +SLC 72 24 5 4 5 4 4 SLD +ASC 72 24 7 4 ASC 5 5 8 4 5 DEF +SLC 72 29 4 4 5 3 4 SLD +ASC 72 29 8 4 ASC 5 5 9 4 5 DEF +SLC 72 34 3 4 8 2 4 SLD +ASC 72 34 9 4 ASC 5 8 10 4 5 DEF +SLC 72 42 2 4 10 1 4 SLD +ASC 72 42 10 4 ASC 5 10 11 4 5 DEF +SLC 72 52 1 4 19 0 4 SLD +ASC 72 52 11 4 ASC 5 19 12 4 5 DEF +SLD 72 71 0 4 25 0 4 SLD +ASD 72 71 12 4 ASC 5 25 12 4 5 DEF +SLC 72 96 0 4 3 1 4 SLD +ASC 72 96 12 4 ASD 5 3 11 4 5 DEF +ALD 72 96 12 4 1 5 4 12 4 3 +SLC 72 99 1 4 10 4 4 SLD +ASC 72 99 11 4 ASC 5 10 8 4 5 DEF +SLC 72 109 4 4 11 7 4 SLD +ASC 72 109 8 4 ASC 5 11 5 4 5 DEF +ASC 72 120 5 4 ASC 5 5 4 4 5 DEF +SLC 72 120 7 4 5 8 4 SLD +ASC 72 125 4 4 ASC 5 5 3 4 5 DEF +SLC 72 125 8 4 5 9 4 SLD +ASC 72 130 3 4 ASC 5 8 2 4 5 DEF +SLC 72 130 9 4 8 10 4 SLD +ASC 72 138 2 4 ASC 5 10 1 4 5 DEF +SLC 72 138 10 4 10 11 4 SLD +ASC 72 148 1 4 ASC 5 19 0 4 5 DEF +SLC 72 148 11 4 19 12 4 SLD +ASD 72 167 0 4 ASC 5 25 0 4 5 DEF +SLD 72 167 12 4 25 12 4 SLD +ASC 72 192 0 4 ASD 5 3 1 4 5 DEF +ALD 72 192 0 4 1 5 4 0 4 3 +SLC 72 192 12 4 3 11 4 SLD +ASC 72 195 1 4 ASC 5 10 4 4 5 DEF +SLC 72 195 11 4 10 8 4 SLD +ASC 72 205 4 4 ASC 5 11 7 4 5 DEF +SLC 72 205 8 4 11 5 4 SLD +SLC 72 216 5 4 5 4 4 SLD +ASC 72 216 7 4 ASC 5 5 8 4 5 DEF +SLC 72 221 4 4 5 3 4 SLD +ASC 72 221 8 4 ASC 5 5 9 4 5 DEF +SLC 72 226 3 4 8 2 4 SLD +ASC 72 226 9 4 ASC 5 8 10 4 5 DEF +SLC 72 234 2 4 10 1 4 SLD +ASC 72 234 10 4 ASC 5 10 11 4 5 DEF +SLC 72 244 1 4 19 0 4 SLD +ASC 72 244 11 4 ASC 5 19 12 4 5 DEF +SLD 72 263 0 4 25 0 4 SLD +ASD 72 263 12 4 ASC 5 25 12 4 5 DEF +SLC 72 288 0 4 3 1 4 SLD +ASC 72 288 12 4 ASD 5 3 11 4 5 DEF +ALD 72 288 12 4 1 5 4 12 4 3 +SLC 72 291 1 4 10 4 4 SLD +ASC 72 291 11 4 ASC 5 10 8 4 5 DEF +SLC 72 301 4 4 11 7 4 SLD +ASC 72 301 8 4 ASC 5 11 5 4 5 DEF +ASC 72 312 5 4 ASC 5 5 4 4 5 DEF +SLC 72 312 7 4 5 8 4 SLD +ASC 72 317 4 4 ASC 5 5 3 4 5 DEF +SLC 72 317 8 4 5 9 4 SLD +ASC 72 322 3 4 ASC 5 8 2 4 5 DEF +SLC 72 322 9 4 8 10 4 SLD +ASC 72 330 2 4 ASC 5 10 1 4 5 DEF +SLC 72 330 10 4 10 11 4 SLD +ASC 72 340 1 4 ASC 5 19 0 4 5 DEF +SLC 72 340 11 4 19 12 4 SLD +ASD 72 359 0 4 ASC 5 25 0 4 5 DEF +SLD 72 359 12 4 25 12 4 SLD +ASC 73 0 0 4 ASD 5 3 1 4 5 DEF +ALD 73 0 0 4 1 5 4 0 4 3 +SLC 73 0 12 4 3 11 4 SLD +ASC 73 3 1 4 ASC 5 10 4 4 5 DEF +SLC 73 3 11 4 10 8 4 SLD +ASC 73 13 4 4 ASC 5 11 7 4 5 DEF +SLC 73 13 8 4 11 5 4 SLD +SLC 73 24 5 4 5 4 4 SLD +ASC 73 24 7 4 ASC 5 5 8 4 5 DEF +SLC 73 29 4 4 5 3 4 SLD +ASC 73 29 8 4 ASC 5 5 9 4 5 DEF +SLC 73 34 3 4 8 2 4 SLD +ASC 73 34 9 4 ASC 5 8 10 4 5 DEF +SLC 73 42 2 4 10 1 4 SLD +ASC 73 42 10 4 ASC 5 10 11 4 5 DEF +SLC 73 52 1 4 19 0 4 SLD +ASC 73 52 11 4 ASC 5 19 12 4 5 DEF +SLD 73 71 0 4 25 0 4 SLD +ASD 73 71 12 4 ASC 5 25 12 4 5 DEF +SLC 73 96 0 4 3 1 4 SLD +ASC 73 96 12 4 ASD 5 3 11 4 5 DEF +ALD 73 96 12 4 1 5 4 12 4 3 +SLC 73 99 1 4 10 4 4 SLD +ASC 73 99 11 4 ASC 5 10 8 4 5 DEF +SLC 73 109 4 4 11 7 4 SLD +ASC 73 109 8 4 ASC 5 11 5 4 5 DEF +ASC 73 120 5 4 ASC 5 5 4 4 5 DEF +SLC 73 120 7 4 5 8 4 SLD +ASC 73 125 4 4 ASC 5 5 3 4 5 DEF +SLC 73 125 8 4 5 9 4 SLD +ASC 73 130 3 4 ASC 5 8 2 4 5 DEF +SLC 73 130 9 4 8 10 4 SLD +ASC 73 138 2 4 ASC 5 10 1 4 5 DEF +SLC 73 138 10 4 10 11 4 SLD +ASC 73 148 1 4 ASC 5 19 0 4 5 DEF +SLC 73 148 11 4 19 12 4 SLD +ASD 73 167 0 4 ASC 5 25 0 4 5 DEF +SLD 73 167 12 4 25 12 4 SLD +ASC 73 192 0 4 ASD 5 3 1 4 5 DEF +ALD 73 192 0 4 1 5 4 0 4 3 +SLC 73 192 12 4 3 11 4 SLD +ASC 73 195 1 4 ASC 5 10 4 4 5 DEF +SLC 73 195 11 4 10 8 4 SLD +ASC 73 205 4 4 ASC 5 11 7 4 5 DEF +SLC 73 205 8 4 11 5 4 SLD +SLC 73 216 5 4 5 4 4 SLD +ASC 73 216 7 4 ASC 5 5 8 4 5 DEF +SLC 73 221 4 4 5 3 4 SLD +ASC 73 221 8 4 ASC 5 5 9 4 5 DEF +SLC 73 226 3 4 8 2 4 SLD +ASC 73 226 9 4 ASC 5 8 10 4 5 DEF +SLC 73 234 2 4 10 1 4 SLD +ASC 73 234 10 4 ASC 5 10 11 4 5 DEF +SLC 73 244 1 4 19 0 4 SLD +ASC 73 244 11 4 ASC 5 19 12 4 5 DEF +SLD 73 263 0 4 25 0 4 SLD +ASD 73 263 12 4 ASC 5 25 12 4 5 DEF +SLC 73 288 0 4 3 1 4 SLD +ASC 73 288 12 4 ASD 5 3 11 4 5 DEF +ALD 73 288 12 4 1 5 4 12 4 3 +SLC 73 291 1 4 10 4 4 SLD +ASC 73 291 11 4 ASC 5 10 8 4 5 DEF +SLC 73 301 4 4 11 7 4 SLD +ASC 73 301 8 4 ASC 5 11 5 4 5 DEF +ASC 73 312 5 4 ASC 5 5 4 4 5 DEF +SLC 73 312 7 4 5 8 4 SLD +ASC 73 317 4 4 ASC 5 5 3 4 5 DEF +SLC 73 317 8 4 5 9 4 SLD +ASC 73 322 3 4 ASC 5 8 2 4 5 DEF +SLC 73 322 9 4 8 10 4 SLD +ASC 73 330 2 4 ASC 5 10 1 4 5 DEF +SLC 73 330 10 4 10 11 4 SLD +ASC 73 340 1 4 ASC 5 19 0 4 5 DEF +SLC 73 340 11 4 19 12 4 SLD +ASD 73 359 0 4 ASC 5 25 0 4 5 DEF +SLD 73 359 12 4 25 12 4 SLD +ASC 74 0 0 4 ASD 5 3 1 4 5 DEF +ALD 74 0 0 4 1 5 4 0 4 3 +SLC 74 0 12 4 3 11 4 SLD +ASC 74 3 1 4 ASC 5 10 4 4 5 DEF +SLC 74 3 11 4 10 8 4 SLD +ASC 74 13 4 4 ASC 5 11 7 4 5 DEF +SLC 74 13 8 4 11 5 4 SLD +SLC 74 24 5 4 5 4 4 SLD +ASC 74 24 7 4 ASC 5 5 8 4 5 DEF +SLC 74 29 4 4 5 3 4 SLD +ASC 74 29 8 4 ASC 5 5 9 4 5 DEF +SLC 74 34 3 4 8 2 4 SLD +ASC 74 34 9 4 ASC 5 8 10 4 5 DEF +SLC 74 42 2 4 10 1 4 SLD +ASC 74 42 10 4 ASC 5 10 11 4 5 DEF +SLC 74 52 1 4 19 0 4 SLD +ASC 74 52 11 4 ASC 5 19 12 4 5 DEF +SLD 74 71 0 4 25 0 4 SLD +ASD 74 71 12 4 ASC 5 25 12 4 5 DEF +SLC 74 96 0 4 3 1 4 SLD +ASC 74 96 12 4 ASD 5 3 11 4 5 DEF +ALD 74 96 12 4 1 5 4 12 4 3 +SLC 74 99 1 4 10 4 4 SLD +ASC 74 99 11 4 ASC 5 10 8 4 5 DEF +SLC 74 109 4 4 11 7 4 SLD +ASC 74 109 8 4 ASC 5 11 5 4 5 DEF +ASC 74 120 5 4 ASC 5 5 4 4 5 DEF +SLC 74 120 7 4 5 8 4 SLD +ASC 74 125 4 4 ASC 5 5 3 4 5 DEF +SLC 74 125 8 4 5 9 4 SLD +ASC 74 130 3 4 ASC 5 8 2 4 5 DEF +SLC 74 130 9 4 8 10 4 SLD +ASC 74 138 2 4 ASC 5 10 1 4 5 DEF +SLC 74 138 10 4 10 11 4 SLD +ASC 74 148 1 4 ASC 5 19 0 4 5 DEF +SLC 74 148 11 4 19 12 4 SLD +ASD 74 167 0 4 ASC 5 25 0 4 5 DEF +SLD 74 167 12 4 25 12 4 SLD +ASD 74 192 0 4 ASD 5 96 0 4 5 DEF +ALD 74 192 0 4 1 5 4 0 4 3 +ASD 74 192 12 4 SLD 5 96 12 4 5 DEF +ASD 74 288 0 4 ASD 5 96 0 4 5 DEF +ALD 74 288 0 4 1 5 4 0 4 3 +ASD 74 288 12 4 ASD 5 96 12 4 5 DEF +ALD 74 288 12 4 1 5 4 12 4 3 +SLC 75 0 0 4 3 1 4 SLD +ASC 75 0 12 4 ASD 5 3 11 4 5 DEF +ALD 75 0 12 4 1 5 4 12 4 3 +SLC 75 3 1 4 10 4 4 SLD +ASC 75 3 11 4 ASC 5 10 8 4 5 DEF +SLC 75 13 4 4 11 7 4 SLD +ASC 75 13 8 4 ASC 5 11 5 4 5 DEF +ASC 75 24 5 4 ASC 5 5 4 4 5 DEF +SLC 75 24 7 4 5 8 4 SLD +ASC 75 29 4 4 ASC 5 5 3 4 5 DEF +SLC 75 29 8 4 5 9 4 SLD +ASC 75 34 3 4 ASC 5 8 2 4 5 DEF +SLC 75 34 9 4 8 10 4 SLD +ASC 75 42 2 4 ASC 5 10 1 4 5 DEF +SLC 75 42 10 4 10 11 4 SLD +ASC 75 52 1 4 ASC 5 19 0 4 5 DEF +SLC 75 52 11 4 19 12 4 SLD +ASD 75 71 0 4 ASC 5 25 0 4 5 DEF +SLD 75 71 12 4 25 12 4 SLD +ASC 75 96 0 4 ASD 5 3 1 4 5 DEF +ALD 75 96 0 4 1 5 4 0 4 3 +SLC 75 96 12 4 3 11 4 SLD +ASC 75 99 1 4 ASC 5 10 4 4 5 DEF +SLC 75 99 11 4 10 8 4 SLD +ASC 75 109 4 4 ASC 5 11 7 4 5 DEF +SLC 75 109 8 4 11 5 4 SLD +SLC 75 120 5 4 5 4 4 SLD +ASC 75 120 7 4 ASC 5 5 8 4 5 DEF +SLC 75 125 4 4 5 3 4 SLD +ASC 75 125 8 4 ASC 5 5 9 4 5 DEF +SLC 75 130 3 4 8 2 4 SLD +ASC 75 130 9 4 ASC 5 8 10 4 5 DEF +SLC 75 138 2 4 10 1 4 SLD +ASC 75 138 10 4 ASC 5 10 11 4 5 DEF +SLC 75 148 1 4 19 0 4 SLD +ASC 75 148 11 4 ASC 5 19 12 4 5 DEF +SLD 75 167 0 4 25 0 4 SLD +ASD 75 167 12 4 ASC 5 25 12 4 5 DEF +SLC 75 192 0 4 3 1 4 SLD +ASC 75 192 12 4 ASD 5 3 11 4 5 DEF +ALD 75 192 12 4 1 5 4 12 4 3 +SLC 75 195 1 4 10 4 4 SLD +ASC 75 195 11 4 ASC 5 10 8 4 5 DEF +SLC 75 205 4 4 11 7 4 SLD +ASC 75 205 8 4 ASC 5 11 5 4 5 DEF +ASC 75 216 5 4 ASC 5 5 4 4 5 DEF +SLC 75 216 7 4 5 8 4 SLD +ASC 75 221 4 4 ASC 5 5 3 4 5 DEF +SLC 75 221 8 4 5 9 4 SLD +ASC 75 226 3 4 ASC 5 8 2 4 5 DEF +SLC 75 226 9 4 8 10 4 SLD +ASC 75 234 2 4 ASC 5 10 1 4 5 DEF +SLC 75 234 10 4 10 11 4 SLD +ASC 75 244 1 4 ASC 5 19 0 4 5 DEF +SLC 75 244 11 4 19 12 4 SLD +ASD 75 263 0 4 ASC 5 25 0 4 5 DEF +SLD 75 263 12 4 25 12 4 SLD +ASC 75 288 0 4 ASD 5 3 1 4 5 DEF +ALD 75 288 0 4 1 5 4 0 4 3 +SLC 75 288 12 4 3 11 4 SLD +ASC 75 291 1 4 ASC 5 10 4 4 5 DEF +SLC 75 291 11 4 10 8 4 SLD +ASC 75 301 4 4 ASC 5 11 7 4 5 DEF +SLC 75 301 8 4 11 5 4 SLD +SLC 75 312 5 4 5 4 4 SLD +ASC 75 312 7 4 ASC 5 5 8 4 5 DEF +SLC 75 317 4 4 5 3 4 SLD +ASC 75 317 8 4 ASC 5 5 9 4 5 DEF +SLC 75 322 3 4 8 2 4 SLD +ASC 75 322 9 4 ASC 5 8 10 4 5 DEF +SLC 75 330 2 4 10 1 4 SLD +ASC 75 330 10 4 ASC 5 10 11 4 5 DEF +SLC 75 340 1 4 19 0 4 SLD +ASC 75 340 11 4 ASC 5 19 12 4 5 DEF +SLD 75 359 0 4 25 0 4 SLD +ASD 75 359 12 4 ASC 5 25 12 4 5 DEF +SLC 76 0 0 4 3 1 4 SLD +ASC 76 0 12 4 ASD 5 3 11 4 5 DEF +ALD 76 0 12 4 1 5 4 12 4 3 +SLC 76 3 1 4 10 4 4 SLD +ASC 76 3 11 4 ASC 5 10 8 4 5 DEF +SLC 76 13 4 4 11 7 4 SLD +ASC 76 13 8 4 ASC 5 11 5 4 5 DEF +ASC 76 24 5 4 ASC 5 5 4 4 5 DEF +SLC 76 24 7 4 5 8 4 SLD +ASC 76 29 4 4 ASC 5 5 3 4 5 DEF +SLC 76 29 8 4 5 9 4 SLD +ASC 76 34 3 4 ASC 5 8 2 4 5 DEF +SLC 76 34 9 4 8 10 4 SLD +ASC 76 42 2 4 ASC 5 10 1 4 5 DEF +SLC 76 42 10 4 10 11 4 SLD +ASC 76 52 1 4 ASC 5 19 0 4 5 DEF +SLC 76 52 11 4 19 12 4 SLD +ASD 76 71 0 4 ASC 5 25 0 4 5 DEF +SLD 76 71 12 4 25 12 4 SLD +ASC 76 96 0 4 ASD 5 3 1 4 5 DEF +ALD 76 96 0 4 1 5 4 0 4 3 +SLC 76 96 12 4 3 11 4 SLD +ASC 76 99 1 4 ASC 5 10 4 4 5 DEF +SLC 76 99 11 4 10 8 4 SLD +ASC 76 109 4 4 ASC 5 11 7 4 5 DEF +SLC 76 109 8 4 11 5 4 SLD +SLC 76 120 5 4 5 4 4 SLD +ASC 76 120 7 4 ASC 5 5 8 4 5 DEF +SLC 76 125 4 4 5 3 4 SLD +ASC 76 125 8 4 ASC 5 5 9 4 5 DEF +SLC 76 130 3 4 8 2 4 SLD +ASC 76 130 9 4 ASC 5 8 10 4 5 DEF +SLC 76 138 2 4 10 1 4 SLD +ASC 76 138 10 4 ASC 5 10 11 4 5 DEF +SLC 76 148 1 4 19 0 4 SLD +ASC 76 148 11 4 ASC 5 19 12 4 5 DEF +SLD 76 167 0 4 25 0 4 SLD +ASD 76 167 12 4 ASC 5 25 12 4 5 DEF +AIR 76 192 0 4 SLD DEF +ALD 76 192 12 4 1 5 4 12 4 3 +CHR 76 288 0 3 CE +CHR 76 288 3 3 CE +CHR 76 288 6 4 CE +CHR 76 288 10 3 CE +CHR 76 288 13 3 CE +SLD 77 0 0 3 384 0 3 SLD +SLD 77 96 13 3 288 13 3 SLD +SLD 77 192 3 3 192 3 3 SLD +SLD 77 288 10 3 96 10 3 SLD +SLC 78 0 0 3 16 1 3 SLD +SLC 78 0 3 3 38 4 3 SLD +HLD 78 0 6 4 96 +SLC 78 0 10 3 38 9 3 SLD +SLC 78 0 13 3 16 12 3 SLD +SLC 78 16 1 3 18 2 3 SLD +SLC 78 16 12 3 18 11 3 SLD +SLC 78 34 2 3 21 3 3 SLD +SLC 78 34 11 3 21 10 3 SLD +SLC 78 38 4 3 44 5 3 SLD +SLC 78 38 9 3 44 8 3 SLD +SLC 78 55 3 3 31 4 3 SLD +SLC 78 55 10 3 31 9 3 SLD +SLD 78 82 5 3 14 5 3 SLD +SLD 78 82 8 3 14 8 3 SLD +SLD 78 86 4 3 10 4 3 SLD +SLD 78 86 9 3 10 9 3 SLD +HLD 78 192 0 8 96 +HLD 78 192 8 8 96 +CHR 79 0 0 16 UP +ASD 79 0 0 16 CHR 5 768 7 2 5 DEF + +T_REC_TAP 297 +T_REC_CHR 68 +T_REC_FLK 32 +T_REC_MNE 0 +T_REC_HLD 23 +T_REC_SLD 108 +T_REC_AIR 63 +T_REC_AHD 77 +T_REC_ALL 668 +T_NOTE_TAP 297 +T_NOTE_CHR 68 +T_NOTE_FLK 32 +T_NOTE_MNE 0 +T_NOTE_HLD 23 +T_NOTE_SLD 78 +T_NOTE_AIR 63 +T_NOTE_AHD 56 +T_NOTE_ALL 617 +T_NUM_TAP 398 +T_NUM_CHR 68 +T_NUM_FLK 32 +T_NUM_MNE 0 +T_NUM_HLD 23 +T_NUM_SLD 108 +T_NUM_AIR 119 +T_NUM_AHD 77 +T_NUM_AAC 219 +T_CHRTYPE_UP 63 +T_CHRTYPE_DW 0 +T_CHRTYPE_CE 5 +T_LEN_HLD 18448 +T_LEN_SLD 65819 +T_LEN_AHD 43427 +T_LEN_ALL 127694 +T_JUDGE_TAP 466 +T_JUDGE_HLD 89 +T_JUDGE_SLD 318 +T_JUDGE_AIR 402 +T_JUDGE_FLK 32 +T_JUDGE_ALL 1307 +T_FIRST_MSEC 7307 +T_FIRST_RES 1824 +T_FINAL_MSEC 124615 +T_FINAL_RES 31104 +T_PROG_00 57 +T_PROG_05 62 +T_PROG_10 42 +T_PROG_15 53 +T_PROG_20 46 +T_PROG_25 48 +T_PROG_30 45 +T_PROG_35 83 +T_PROG_40 66 +T_PROG_45 67 +T_PROG_50 60 +T_PROG_55 78 +T_PROG_60 94 +T_PROG_65 68 +T_PROG_70 84 +T_PROG_75 87 +T_PROG_80 66 +T_PROG_85 177 +T_PROG_90 196 +T_PROG_95 56 + From 0b2e9213e4e4d6a5bcb41b8e5a19551140390d53 Mon Sep 17 00:00:00 2001 From: Starrah Date: Sun, 3 May 2026 22:17:16 +0800 Subject: [PATCH 02/13] =?UTF-8?q?[R]=20=E9=80=82=E5=BA=94=205f8bddb=20?= =?UTF-8?q?=E4=B8=AD=E6=89=80=E5=81=9A=E7=9A=84=E4=BF=AE=E6=94=B9=EF=BC=8C?= =?UTF-8?q?=E4=BD=BF=E8=83=BD=E8=BF=87=E7=BC=96=E8=AF=91=20=E4=B8=BB?= =?UTF-8?q?=E8=A6=81=E9=87=8D=E6=9E=84=E6=98=AFAI=E5=86=99=E7=9A=84?= =?UTF-8?q?=EF=BC=8C=E4=BA=BA=E5=B7=A5=E5=81=9A=E4=BA=86=E4=B8=80=E9=83=A8?= =?UTF-8?q?=E5=88=86=E4=BC=98=E5=8C=96=E5=92=8C=E8=BF=9B=E8=A1=8Creview?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chart/chu/C2sChart.cs | 5 --- chart/chu/SusChart.cs | 5 --- chart/chu/UgcChart.cs | 5 --- generator/chu/C2sGenerator.cs | 79 ++++++++++++++++++++--------------- generator/chu/SusGenerator.cs | 40 +++++++----------- generator/chu/UgcGenerator.cs | 57 ++++++++++--------------- parser/chu/C2sParser.cs | 50 +++++++++++++++++----- parser/chu/SusParser.cs | 41 +++++++++--------- parser/chu/UgcParser.cs | 76 ++++++++++++++++++--------------- tests/chu/ChuTests.cs | 38 +++++++---------- utils/Utils.cs | 2 +- 11 files changed, 204 insertions(+), 194 deletions(-) diff --git a/chart/chu/C2sChart.cs b/chart/chu/C2sChart.cs index 7418c5c..3e027f1 100644 --- a/chart/chu/C2sChart.cs +++ b/chart/chu/C2sChart.cs @@ -16,9 +16,4 @@ public class C2sChart : BaseChart, IChuChart public List<(int Measure, int Offset, double Bpm)> BpmEvents = []; public List<(int Measure, int Offset, int Denom, int Num)> MetEvents = []; public List<(int Measure, int Offset, int Duration, double Multiplier)> SflEvents = []; - - public override decimal StartBpm => (decimal)(BpmEvents.Count > 0 ? BpmEvents[0].Bpm : DefBpm); - public override decimal StartTime => Notes.Count > 0 ? Notes.Min(n => n.Measure * Resolution + n.Offset) / (decimal)Resolution * 240m / StartBpm : 0; - public override decimal EndTime => Notes.Count > 0 ? Notes.Max(n => n.Measure * Resolution + n.Offset + Math.Max(n.HoldDuration, Math.Max(n.SlideDuration, n.AirHoldDuration))) / (decimal)Resolution * 240m / StartBpm : 0; - public override int TotalNotes => Notes.Count; } diff --git a/chart/chu/SusChart.cs b/chart/chu/SusChart.cs index 5e2a442..9fde34b 100644 --- a/chart/chu/SusChart.cs +++ b/chart/chu/SusChart.cs @@ -12,9 +12,4 @@ public class SusChart : BaseChart, IChuChart public string Designer { get; set; } = ""; public int TicksPerBeat { get; set; } = 480; public double Bpm { get; set; } = 120.0; - - public override decimal StartBpm => (decimal)Bpm; - public override decimal StartTime => Notes.Count > 0 ? Notes.Min(n => n.Measure * TicksPerBeat * 4 + n.Offset) / (decimal)(TicksPerBeat * 4) * 240m / StartBpm : 0; - public override decimal EndTime => Notes.Count > 0 && StartBpm > 0 ? Notes.Max(n => n.Measure * TicksPerBeat * 4 + n.Offset + Math.Max(n.HoldDuration, Math.Max(n.SlideDuration, n.AirHoldDuration))) / (decimal)(TicksPerBeat * 4) * 240m / StartBpm : 0; - public override int TotalNotes => Notes.Count; } diff --git a/chart/chu/UgcChart.cs b/chart/chu/UgcChart.cs index b0059ca..4cddf4e 100644 --- a/chart/chu/UgcChart.cs +++ b/chart/chu/UgcChart.cs @@ -19,9 +19,4 @@ public class UgcChart : BaseChart, IChuChart public List<(int Measure, int Num, int Den)> BeatEvents = []; public List<(int Measure, int Offset, double Bpm)> BpmEvents = []; public List<(int Measure, int Offset, double Multiplier)> SpeedEvents = []; - - public override decimal StartBpm => (decimal)(BpmEvents.Count > 0 ? BpmEvents[0].Bpm : 120.0); - public override decimal StartTime => Notes.Count > 0 ? Notes.Min(n => n.Measure * TicksPerBeat * 4 + n.Offset) / (decimal)(TicksPerBeat * 4) * 240m / StartBpm : 0; - public override decimal EndTime => Notes.Count > 0 && StartBpm > 0 ? Notes.Max(n => n.Measure * TicksPerBeat * 4 + n.Offset + Math.Max(n.HoldDuration, Math.Max(n.SlideDuration, n.AirHoldDuration))) / (decimal)(TicksPerBeat * 4) * 240m / StartBpm : 0; - public override int TotalNotes => Notes.Count; } diff --git a/generator/chu/C2sGenerator.cs b/generator/chu/C2sGenerator.cs index 7e12a07..3160505 100644 --- a/generator/chu/C2sGenerator.cs +++ b/generator/chu/C2sGenerator.cs @@ -1,6 +1,5 @@ using System.Globalization; using System.Text; -using MuConvert.chart; using MuConvert.generator; using MuConvert.utils; using static MuConvert.utils.Alert.LEVEL; @@ -39,8 +38,7 @@ private static C2sChart ConvertToC2s(IChuChart chart, List alerts) result.BpmEvents.Add((b.Measure, ScaleDown(b.Offset, ugc.TicksPerBeat), b.Bpm)); foreach (var b in ugc.BeatEvents) result.MetEvents.Add((b.Measure, 0, b.Den, b.Num)); - foreach (var n in ugc.Notes) - result.Notes.Add(ScaleNote(n, ugc.TicksPerBeat)); + result.Notes = ugc.Notes; return result; } @@ -48,8 +46,7 @@ private static C2sChart ConvertToC2s(IChuChart chart, List alerts) { var result = new C2sChart { DefBpm = sus.Bpm }; result.BpmEvents.Add((0, 0, sus.Bpm)); - foreach (var n in sus.Notes) - result.Notes.Add(ScaleNote(n, sus.TicksPerBeat)); + result.Notes = sus.Notes; return result; } @@ -57,24 +54,12 @@ private static C2sChart ConvertToC2s(IChuChart chart, List alerts) throw new ConversionException(alerts); } - private static ChuNote ScaleNote(ChuNote n, int tpb) - { - int scaleDown(int v) => (int)((long)v * (C2sResolution / 4) / tpb); - return new ChuNote - { - Type = n.Type, Measure = n.Measure, Offset = scaleDown(n.Offset), - Cell = n.Cell, Width = n.Width, - HoldDuration = scaleDown(n.HoldDuration), SlideDuration = scaleDown(n.SlideDuration), - EndCell = n.EndCell, EndWidth = n.EndWidth, - Tag = n.Tag, TargetNote = n.TargetNote, AirHoldDuration = scaleDown(n.AirHoldDuration), - StartHeight = n.StartHeight, TargetHeight = n.TargetHeight, NoteColor = n.NoteColor, - }; - } - private static int ScaleDown(int ticks, int tpb) => (int)((long)ticks * (C2sResolution / 4) / tpb); private static string Serialize(C2sChart chart) { + chart.Sort(); + var sb = new StringBuilder(); sb.AppendLine($"VERSION\t{chart.Version}"); sb.AppendLine($"MUSIC\t{chart.MusicId}"); @@ -99,26 +84,54 @@ private static string Serialize(C2sChart chart) sb.AppendLine($"SFL\t{s.Measure}\t{s.Offset}\t{s.Duration}\t{Mlt(s.Multiplier)}"); sb.AppendLine(); - foreach (var n in chart.Notes.OrderBy(n => n.Measure * C2sResolution + n.Offset)) - sb.AppendLine(FormatNote(n)); + foreach (var n in chart.Notes) + sb.AppendLine(FormatNote(n, chart.Resolution)); sb.AppendLine(); return sb.ToString(); } - private static string FormatNote(ChuNote n) => n.Type switch + private static string FormatNote(ChuNote n, int tpm) + { + var (m, o) = Utils.BarAndTick(n.Time, tpm, 0); + var durTicks = Utils.Tick(n.Duration, tpm, 0); + return n.Type switch + { + "TAP" => $"TAP\t{m}\t{o}\t{n.Cell}\t{n.Width}", + "CHR" => $"CHR\t{m}\t{o}\t{n.Cell}\t{n.Width}\t{n.Tag}", + "HLD" or "HXD" => $"{n.Type}\t{m}\t{o}\t{n.Cell}\t{n.Width}\t{durTicks}", + "SLD" or "SLC" or "SXD" or "SXC" => $"{n.Type}\t{m}\t{o}\t{n.Cell}\t{n.Width}\t{durTicks}\t{n.EndCell}\t{n.EndWidth}", + "FLK" => $"FLK\t{m}\t{o}\t{n.Cell}\t{n.Width}\t{n.Tag}", + "AIR" or "AUR" or "AUL" or "ADW" or "ADR" or "ADL" => + string.IsNullOrEmpty(n.Tag) + ? $"{n.Type}\t{m}\t{o}\t{n.Cell}\t{n.Width}\t{n.TargetNote}" + : $"{n.Type}\t{m}\t{o}\t{n.Cell}\t{n.Width}\t{n.TargetNote}\t{n.Tag}", + "AHD" or "AHX" => + string.IsNullOrEmpty(n.Tag) + ? $"{n.Type}\t{m}\t{o}\t{n.Cell}\t{n.Width}\t{n.TargetNote}\t{durTicks}" + : $"{n.Type}\t{m}\t{o}\t{n.Cell}\t{n.Width}\t{n.TargetNote}\t{durTicks}\t{n.Tag}", + "ASD" or "ASC" => FormatAsdAsc(n, m, o, durTicks), + "ALD" => FormatAld(n, m, o), + "MNE" => $"MNE\t{m}\t{o}\t{n.Cell}\t{n.Width}", + _ => $"TAP\t{m}\t{o}\t{n.Cell}\t{n.Width}" + }; + } + + private static string FormatAsdAsc(ChuNote n, int m, int o, int durTicks) { - "TAP" => $"TAP\t{n.Measure}\t{n.Offset}\t{n.Cell}\t{n.Width}", - "CHR" => $"CHR\t{n.Measure}\t{n.Offset}\t{n.Cell}\t{n.Width}\t{n.Tag}", - "HLD" or "HXD" => $"{n.Type}\t{n.Measure}\t{n.Offset}\t{n.Cell}\t{n.Width}\t{n.HoldDuration}", - "SLD" or "SLC" or "SXD" or "SXC" => $"{n.Type}\t{n.Measure}\t{n.Offset}\t{n.Cell}\t{n.Width}\t{n.SlideDuration}\t{n.EndCell}\t{n.EndWidth}", - "FLK" => $"FLK\t{n.Measure}\t{n.Offset}\t{n.Cell}\t{n.Width}\t{n.Tag}", - "AIR" or "AUR" or "AUL" or "ADW" or "ADR" or "ADL" => $"{n.Type}\t{n.Measure}\t{n.Offset}\t{n.Cell}\t{n.Width}\t{n.TargetNote}", - "AHD" => $"AHD\t{n.Measure}\t{n.Offset}\t{n.Cell}\t{n.Width}\t{n.TargetNote}\t{n.AirHoldDuration}", - "ALD" or "ASD" => $"{n.Type}\t{n.Measure}\t{n.Offset}\t{n.StartHeight}\t{n.SlideDuration}\t{n.EndCell}\t{n.EndWidth}\t{n.TargetHeight}\t{n.NoteColor}", - "MNE" => $"MNE\t{n.Measure}\t{n.Offset}\t{n.Cell}\t{n.Width}", - _ => $"TAP\t{n.Measure}\t{n.Offset}\t{n.Cell}\t{n.Width}" - }; + var e0 = n.ExtraData.Count > 0 ? n.ExtraData[0] : 0; + var e1 = n.ExtraData.Count > 1 ? n.ExtraData[1] : 0; + return $"{n.Type}\t{m}\t{o}\t{n.Cell}\t{n.Width}\t{n.TargetNote}\t{e0}\t{durTicks}\t{n.EndCell}\t{n.EndWidth}\t{e1}\t{n.Tag}"; + } + + private static string FormatAld(ChuNote n, int m, int o) + { + var a = n.ExtraData.Count > 0 ? n.ExtraData[0] : 0; + var b = n.ExtraData.Count > 1 ? n.ExtraData[1] : 0; + var c = n.ExtraData.Count > 2 ? n.ExtraData[2] : 0; + var tail = n.ExtraData.Count > 3 ? n.ExtraData[3] : 0; + return $"ALD\t{m}\t{o}\t{n.Cell}\t{n.Width}\t{a}\t{b}\t{c}\t{n.EndCell}\t{n.EndWidth}\t{tail}"; + } private static string Fmt(double v) => v.ToString("0.000", CultureInfo.InvariantCulture); private static string Mlt(double v) => v.ToString("0.000000", CultureInfo.InvariantCulture); diff --git a/generator/chu/SusGenerator.cs b/generator/chu/SusGenerator.cs index 25265e1..84bc320 100644 --- a/generator/chu/SusGenerator.cs +++ b/generator/chu/SusGenerator.cs @@ -1,5 +1,4 @@ using System.Text; -using MuConvert.chart; using MuConvert.generator; using MuConvert.utils; using static MuConvert.utils.Alert.LEVEL; @@ -32,9 +31,8 @@ private static SusChart ConvertToSus(IChuChart chart, List alerts) if (chart is C2sChart c2s) { bpm = c2s.BpmEvents.Count > 0 ? c2s.BpmEvents[0].Bpm : c2s.DefBpm; - int c2sTpb = c2s.Resolution / 4; var result = new SusChart { Bpm = bpm, TicksPerBeat = SusTpb, Title = title, Artist = artist }; - foreach (var n in c2s.Notes) result.Notes.Add(ScaleUp(n, c2sTpb)); + result.Notes = c2s.Notes; return result; } @@ -42,7 +40,7 @@ private static SusChart ConvertToSus(IChuChart chart, List alerts) { bpm = ugc.BpmEvents.Count > 0 ? ugc.BpmEvents[0].Bpm : 120.0; var result = new SusChart { Bpm = bpm, TicksPerBeat = SusTpb, Title = ugc.Title, Artist = ugc.Artist }; - foreach (var n in ugc.Notes) result.Notes.Add(ScaleUp(n, ugc.TicksPerBeat)); + result.Notes = ugc.Notes; return result; } @@ -50,21 +48,10 @@ private static SusChart ConvertToSus(IChuChart chart, List alerts) throw new ConversionException(alerts); } - private static ChuNote ScaleUp(ChuNote n, int sourceTicksPerBeat) - { - int s(int v) => (int)((long)v * SusTpb / sourceTicksPerBeat); - return new ChuNote - { - Type = n.Type, Measure = n.Measure, Offset = s(n.Offset), - Cell = n.Cell * 2, Width = n.Width * 2, - HoldDuration = s(n.HoldDuration), SlideDuration = s(n.SlideDuration), - EndCell = n.EndCell * 2, EndWidth = n.EndWidth * 2, - Tag = n.Tag, TargetNote = n.TargetNote, AirHoldDuration = s(n.AirHoldDuration), - }; - } - private static string Serialize(SusChart sus) { + sus.Sort(); + var sb = new StringBuilder(); if (!string.IsNullOrEmpty(sus.Title)) sb.AppendLine($"#TITLE \"{sus.Title}\""); if (!string.IsNullOrEmpty(sus.Artist)) sb.AppendLine($"#ARTIST \"{sus.Artist}\""); @@ -73,22 +60,27 @@ private static string Serialize(SusChart sus) sb.AppendLine($"#REQUEST \"{sus.TicksPerBeat}\""); sb.AppendLine(); - foreach (var n in sus.Notes.OrderBy(n => n.Measure).ThenBy(n => n.Offset)) - sb.AppendLine($"#{n.Measure:X2}{n.Offset:X3}:{FormatData(n)}"); + var tpm = sus.TicksPerBeat * 4; + foreach (var n in sus.Notes) + { + var (m, o) = Utils.BarAndTick(n.Time, tpm, 0); + sb.AppendLine($"#{m:X2}{o:X3}:{FormatData(n, tpm)}"); + } return sb.ToString(); } - private static string FormatData(ChuNote n) + private static string FormatData(ChuNote n, int tpm) { - string lw = $"{n.Cell:X2}{n.Width:X2}"; + string lw = $"{n.Cell*2:X2}{n.Width*2:X2}"; string tc = TypeCode(n.Type); - string dur = $"{(n.HoldDuration > 0 ? n.HoldDuration : n.SlideDuration > 0 ? n.SlideDuration : n.AirHoldDuration):X4}"; + var durTicks = Utils.Tick(n.Duration, tpm); + string dur = $"{durTicks:X4}"; return tc switch { "01" or "02" or "03" or "10" => $"{tc}{lw}", "05" or "08" => $"{tc}{lw}{dur}", - "06" => $"{tc}{lw}{dur}{n.EndCell:X2}{n.EndWidth:X2}", + "06" => $"{tc}{lw}{dur}{n.EndCell*2:X2}{n.EndWidth*2:X2}", "07" or "09" => $"{tc}{lw}{n.TargetNote}", _ => $"01{lw}" }; @@ -99,7 +91,7 @@ private static string FormatData(ChuNote n) "TAP" => "01", "CHR" => "02", "FLK" => "03", "HLD" => "05", "SLD" => "06", "SLC" => "06", "AIR" => "07", "AUR" => "07", "AUL" => "07", - "AHD" => "08", "ADW" => "09", "ADR" => "09", "ADL" => "09", + "AHD" => "08", "AHX" => "08", "ADW" => "09", "ADR" => "09", "ADL" => "09", "MNE" => "10", _ => "01" }; } diff --git a/generator/chu/UgcGenerator.cs b/generator/chu/UgcGenerator.cs index 36515c8..ae9ce89 100644 --- a/generator/chu/UgcGenerator.cs +++ b/generator/chu/UgcGenerator.cs @@ -1,5 +1,4 @@ using System.Text; -using MuConvert.chart; using MuConvert.generator; using MuConvert.utils; using static MuConvert.utils.Alert.LEVEL; @@ -40,8 +39,7 @@ private static UgcChart ConvertToUgc(IChuChart chart, List alerts) result.BpmEvents.Add((b.Measure, ScaleUp(b.Offset), b.Bpm)); foreach (var m in c2s.MetEvents) result.BeatEvents.Add((m.Measure, m.Num, m.Denom)); - foreach (var n in c2s.Notes) - result.Notes.Add(ScaleUpNote(n)); + result.Notes = c2s.Notes; return result; } @@ -49,25 +47,8 @@ private static UgcChart ConvertToUgc(IChuChart chart, List alerts) throw new ConversionException(alerts); } - private static ChuNote ScaleUpNote(ChuNote n) - { - int s(int v) => (int)((long)v * UgcTicksPerBeat / (C2sResolution / 4)); - return new ChuNote - { - Type = n.Type, Measure = n.Measure, Offset = s(n.Offset), - Cell = n.Cell, Width = n.Width, - HoldDuration = s(n.HoldDuration), SlideDuration = s(n.SlideDuration), - EndCell = n.EndCell, EndWidth = n.EndWidth, - Tag = n.Tag, TargetNote = IsAir(n.Type) && string.IsNullOrEmpty(n.TargetNote) ? "N" : n.TargetNote, - AirHoldDuration = s(n.AirHoldDuration), - StartHeight = n.StartHeight, TargetHeight = n.TargetHeight, NoteColor = n.NoteColor, - }; - } - private static int ScaleUp(int v) => (int)((long)v * UgcTicksPerBeat / (C2sResolution / 4)); - private static bool IsAir(string t) => t is "AIR" or "AUR" or "AUL" or "AHD" or "ADW" or "ADR" or "ADL"; - private static string MapDiffId(int id) => id switch { 0 => "BASIC", 1 => "ADVANCED", 2 => "EXPERT", 3 => "MASTER", 4 => "ULTIMA", _ => "0" @@ -75,6 +56,8 @@ private static ChuNote ScaleUpNote(ChuNote n) private static string Serialize(UgcChart ugc) { + ugc.Sort(); + var sb = new StringBuilder(); sb.AppendLine("@VER\t6"); if (!string.IsNullOrEmpty(ugc.Title)) sb.AppendLine($"@TITLE\t{ugc.Title}"); @@ -92,22 +75,26 @@ private static string Serialize(UgcChart ugc) sb.AppendLine("@ENDHEAD"); sb.AppendLine(); - var notes = ugc.Notes.OrderBy(n => n.Measure).ThenBy(n => n.Offset).ToList(); - foreach (var n in notes) + var tpm = ugc.TicksPerBeat * 4; + foreach (var n in ugc.Notes) { - sb.Append($"#{n.Measure}'{n.Offset}:{UCode(n)}"); + var (m, o) = Utils.BarAndTick(n.Time, tpm, 0); + sb.Append($"#{m}'{o}:{UCode(n, tpm)}"); sb.AppendLine(); - if (n.Type == "HLD" && n.HoldDuration > 0) - sb.AppendLine($"#{n.HoldDuration}>s"); - else if (n.Type == "SLD" && n.SlideDuration > 0) - sb.AppendLine($"#{n.SlideDuration}>s{Hx(n.EndCell)}{Hw(n.EndWidth)}"); + var durTicks = Utils.Tick(n.Duration, tpm, 0); + if (n.Type == "HLD" && durTicks > 0) + sb.AppendLine($"#{durTicks}>s"); + else if (n.Type == "SLD" && durTicks > 0) + sb.AppendLine($"#{durTicks}>s{Hx(n.EndCell)}{Hw(n.EndWidth)}"); } return sb.ToString(); } - private static string UCode(ChuNote n) + private static string UCode(ChuNote n, int tpm) { string c = Hx(n.Cell), w = Hw(n.Width); + var durTicks = Utils.Tick(n.Duration, tpm, 0); + var targetNote = string.IsNullOrEmpty(n.TargetNote) ? "N" : n.TargetNote; return n.Type switch { "TAP" => $"t{c}{w}", @@ -117,13 +104,13 @@ private static string UCode(ChuNote n) "SLC" or "SXC" => $"s{c}{w}", "FLK" => $"f{c}{w}A", "MNE" => $"d{c}{w}", - "AIR" => $"a{c}{w}UC{n.TargetNote}", - "AUR" => $"a{c}{w}UR{n.TargetNote}", - "AUL" => $"a{c}{w}UL{n.TargetNote}", - "AHD" => $"a{c}{w}HD{n.TargetNote}_{n.AirHoldDuration}", - "ADW" => $"a{c}{w}DC{n.TargetNote}", - "ADR" => $"a{c}{w}DR{n.TargetNote}", - "ADL" => $"a{c}{w}DL{n.TargetNote}", + "AIR" => $"a{c}{w}UC{targetNote}", + "AUR" => $"a{c}{w}UR{targetNote}", + "AUL" => $"a{c}{w}UL{targetNote}", + "AHD" or "AHX" => $"a{c}{w}HD{targetNote}_{durTicks}", + "ADW" => $"a{c}{w}DC{targetNote}", + "ADR" => $"a{c}{w}DR{targetNote}", + "ADL" => $"a{c}{w}DL{targetNote}", _ => $"t{c}{w}" }; } diff --git a/parser/chu/C2sParser.cs b/parser/chu/C2sParser.cs index 518fbe5..56cb6d6 100644 --- a/parser/chu/C2sParser.cs +++ b/parser/chu/C2sParser.cs @@ -1,7 +1,7 @@ using System.Globalization; -using MuConvert.chart; using MuConvert.parser; using MuConvert.utils; +using Rationals; using static MuConvert.utils.Alert.LEVEL; namespace MuConvert.chu; @@ -86,7 +86,8 @@ private static void ParseTiming(string[] p, C2sChart chart) private static void ParseNote(string[] p, C2sChart chart, List alerts, int lineNum) { var tag = p[0].ToUpperInvariant(); - var note = new ChuNote { Type = tag, Measure = Int(p, 1), Offset = Int(p, 2) }; + var tpm = chart.Resolution; + var note = new ChuNote { Type = tag, Time = Int(p, 1) + new Rational(Int(p, 2), tpm) }; switch (tag) { @@ -95,21 +96,48 @@ private static void ParseNote(string[] p, C2sChart chart, List alerts, in case "CHR": note.Cell = Int(p, 3); note.Width = Math.Max(1, Int(p, 4, 1)); note.Tag = Str(p, 5); break; case "HLD": case "HXD": - note.Cell = Int(p, 3); note.Width = Math.Max(1, Int(p, 4, 1)); note.HoldDuration = Int(p, 5); break; + note.Cell = Int(p, 3); note.Width = Math.Max(1, Int(p, 4, 1)); note.Duration = new Rational(Int(p, 5), tpm); break; case "SLD": case "SLC": case "SXD": case "SXC": note.Cell = Int(p, 3); note.Width = Math.Max(1, Int(p, 4, 1)); - note.SlideDuration = Int(p, 5); note.EndCell = Int(p, 6); note.EndWidth = Math.Max(1, Int(p, 7, 1)); break; + note.Duration = new Rational(Int(p, 5), tpm); + note.EndCell = Int(p, 6); note.EndWidth = Math.Max(1, Int(p, 7, 1)); + break; case "FLK": note.Cell = Int(p, 3); note.Width = Math.Max(1, Int(p, 4, 1)); note.Tag = Str(p, 5); break; case "AIR": case "AUR": case "AUL": case "ADW": case "ADR": case "ADL": - note.Cell = Int(p, 3); note.Width = Math.Max(1, Int(p, 4, 1)); note.TargetNote = Str(p, 5); break; - case "AHD": + note.Cell = Int(p, 3); note.Width = Math.Max(1, Int(p, 4, 1)); note.TargetNote = Str(p, 5); + if (p.Length >= 7) note.Tag = Str(p, 6); + break; + case "AHD": case "AHX": + note.Cell = Int(p, 3); note.Width = Math.Max(1, Int(p, 4, 1)); + note.TargetNote = Str(p, 5); note.Duration = new Rational(Int(p, 6), tpm); + if (p.Length >= 8) note.Tag = Str(p, 7); + break; + case "ASD": case "ASC": + // 文档:M O Cell Width | TargetNote | 未知 | Duration | EndCell | EndWidth | 未知 | Tag + if (p.Length < 12) + { + alerts.Add(new Alert(Warning, $"{tag} 列数不足(期望至少 12 列)") { Line = lineNum }); + return; + } note.Cell = Int(p, 3); note.Width = Math.Max(1, Int(p, 4, 1)); - note.TargetNote = Str(p, 5); note.AirHoldDuration = Int(p, 6); break; - case "ALD": case "ASD": - note.StartHeight = Int(p, 3); note.SlideDuration = Int(p, 4); - note.EndCell = Int(p, 5); note.EndWidth = Math.Max(1, Int(p, 6, 1)); - note.TargetHeight = Int(p, 7); note.NoteColor = Str(p, 8); break; + note.TargetNote = Str(p, 5); + note.ExtraData = [Int(p, 6), Int(p, 10)]; + note.Duration = new Rational(Int(p, 7), tpm); + note.EndCell = Int(p, 8); note.EndWidth = Math.Max(1, Int(p, 9, 1)); + note.Tag = Str(p, 11); + break; + case "ALD": + // 文档:M O Cell Width | 未知×3 | EndCell | EndWidth | 未知(1 或 3) + if (p.Length < 11) + { + alerts.Add(new Alert(Warning, "ALD 列数不足(期望至少 11 列)") { Line = lineNum }); + return; + } + note.Cell = Int(p, 3); note.Width = Math.Max(1, Int(p, 4, 1)); + note.ExtraData = [Int(p, 5), Int(p, 6), Int(p, 7), Int(p, 10)]; + note.EndCell = Int(p, 8); note.EndWidth = Math.Max(1, Int(p, 9, 1)); + break; default: alerts.Add(new Alert(Warning, string.Format(Locale.C2SUnknownNoteType, tag)) { Line = lineNum }); return; } diff --git a/parser/chu/SusParser.cs b/parser/chu/SusParser.cs index 89d0033..20816b0 100644 --- a/parser/chu/SusParser.cs +++ b/parser/chu/SusParser.cs @@ -1,7 +1,7 @@ using System.Globalization; -using MuConvert.chart; using MuConvert.parser; using MuConvert.utils; +using Rationals; using static MuConvert.utils.Alert.LEVEL; namespace MuConvert.chu; @@ -118,6 +118,7 @@ private static void ParseNoteLine(string content, SusChart chart, List al var measure = HexToInt(timingStr[..2]); var tick = HexToInt(timingStr[2..5]); + var tpm = chart.TicksPerBeat * 4; if (dataStr.Length < 6) { @@ -138,8 +139,7 @@ private static void ParseNoteLine(string content, SusChart chart, List al var note = new ChuNote { Type = typeName, - Measure = measure, - Offset = tick, + Time = measure + new Rational(tick, tpm), Cell = lane / 2, Width = Math.Max(1, width / 2), }; @@ -153,47 +153,47 @@ private static void ParseNoteLine(string content, SusChart chart, List al break; case "HLD": - ParseHoldData(dataStr, note, alerts, lineNum); + ParseHoldData(dataStr, note, tpm, alerts, lineNum); break; case "SLD": - ParseSlideData(dataStr, note, alerts, lineNum); + ParseSlideData(dataStr, note, tpm, alerts, lineNum); break; case "AIR": case "ADW": - ParseAirTarget(dataStr, note, alerts, lineNum); + ParseAirTarget(dataStr, note, tpm, alerts, lineNum); break; case "AHD": - ParseAhdData(dataStr, note, alerts, lineNum); + ParseAhdData(dataStr, note, tpm, alerts, lineNum); break; } chart.Notes.Add(note); } - private static void ParseHoldData(string dataStr, ChuNote note, List alerts, int lineNum) + private static void ParseHoldData(string dataStr, ChuNote note, int tpm, List alerts, int lineNum) { if (dataStr.Length >= 10) { - note.HoldDuration = HexToInt(dataStr[6..10]); + note.Duration = new Rational(HexToInt(dataStr[6..10]), tpm); } else { - alerts.Add(new Alert(Warning, $"HLD 音符缺少时长: {dataStr}") { Line = lineNum, RelevantNote = FormatNoteRef(note) }); + alerts.Add(new Alert(Warning, $"HLD 音符缺少时长: {dataStr}") { Line = lineNum, RelevantNote = FormatNoteRef(note, tpm) }); } } - private static void ParseSlideData(string dataStr, ChuNote note, List alerts, int lineNum) + private static void ParseSlideData(string dataStr, ChuNote note, int tpm, List alerts, int lineNum) { if (dataStr.Length >= 10) { - note.SlideDuration = HexToInt(dataStr[6..10]); + note.Duration = new Rational(HexToInt(dataStr[6..10]), tpm); } else { - alerts.Add(new Alert(Warning, $"SLD 音符缺少时长: {dataStr}") { Line = lineNum, RelevantNote = FormatNoteRef(note) }); + alerts.Add(new Alert(Warning, $"SLD 音符缺少时长: {dataStr}") { Line = lineNum, RelevantNote = FormatNoteRef(note, tpm) }); return; } @@ -204,7 +204,7 @@ private static void ParseSlideData(string dataStr, ChuNote note, List ale } } - private static void ParseAirTarget(string dataStr, ChuNote note, List alerts, int lineNum) + private static void ParseAirTarget(string dataStr, ChuNote note, int tpm, List alerts, int lineNum) { if (dataStr.Length >= 8) { @@ -212,19 +212,19 @@ private static void ParseAirTarget(string dataStr, ChuNote note, List ale } else { - alerts.Add(new Alert(Warning, $"AIR/ADW 音符缺少目标: {dataStr}") { Line = lineNum, RelevantNote = FormatNoteRef(note) }); + alerts.Add(new Alert(Warning, $"AIR/ADW 音符缺少目标: {dataStr}") { Line = lineNum, RelevantNote = FormatNoteRef(note, tpm) }); } } - private static void ParseAhdData(string dataStr, ChuNote note, List alerts, int lineNum) + private static void ParseAhdData(string dataStr, ChuNote note, int tpm, List alerts, int lineNum) { if (dataStr.Length >= 10) { - note.AirHoldDuration = HexToInt(dataStr[6..10]); + note.Duration = new Rational(HexToInt(dataStr[6..10]), tpm); } else { - alerts.Add(new Alert(Warning, $"AHD 音符缺少时长: {dataStr}") { Line = lineNum, RelevantNote = FormatNoteRef(note) }); + alerts.Add(new Alert(Warning, $"AHD 音符缺少时长: {dataStr}") { Line = lineNum, RelevantNote = FormatNoteRef(note, tpm) }); } } @@ -239,8 +239,9 @@ private static string Unquote(string s) return trimmed; } - private static string FormatNoteRef(ChuNote note) + private static string FormatNoteRef(ChuNote note, int tpm) { - return $"#{note.Measure:X2}{note.Offset:X2}:{note.Type}"; + var (m, o) = Utils.BarAndTick(note.Time, tpm, 0); + return $"#{m:X2}{o:X3}:{note.Type}"; } } diff --git a/parser/chu/UgcParser.cs b/parser/chu/UgcParser.cs index 90dd03c..b9250b8 100644 --- a/parser/chu/UgcParser.cs +++ b/parser/chu/UgcParser.cs @@ -1,7 +1,7 @@ using System.Globalization; -using MuConvert.chart; using MuConvert.parser; using MuConvert.utils; +using Rationals; using static MuConvert.utils.Alert.LEVEL; namespace MuConvert.chu; @@ -247,10 +247,10 @@ private static int ParseNoteLine(string[] lines, int idx, UgcChart chart, List 3) note.Tag = code[3..]; break; @@ -289,7 +289,7 @@ private static int ParseNoteLine(string[] lines, int idx, UgcChart chart, List alerts, int lineNum) + private static void ParseTapNote(string code, ChuNote note, List alerts, int lineNum, UgcChart chart) { note.Type = "TAP"; - ParseCellWidth(code, 1, note, alerts, lineNum); + ParseCellWidth(code, 1, note, alerts, lineNum, chart); } - private static int ParseHoldNote(string[] lines, int idx, string code, ChuNote note, List alerts) + private static int ParseHoldNote(string[] lines, int idx, string code, ChuNote note, List alerts, UgcChart chart) { note.Type = "HLD"; - ParseCellWidth(code, 1, note, alerts, idx + 1); + var tpm = chart.TicksPerBeat * 4; + ParseCellWidth(code, 1, note, alerts, idx + 1, chart); bool foundFirst = false; while (idx + 1 < lines.Length) @@ -322,20 +323,21 @@ private static int ParseHoldNote(string[] lines, int idx, string code, ChuNote n break; } - note.HoldDuration += duration; + note.Duration += new Rational(duration, tpm); idx++; foundFirst = true; } if (!foundFirst) - alerts.Add(new Alert(Warning, $"HLD 音符缺少时长跟随行") { Line = idx + 1, RelevantNote = FormatNoteRef(note) }); + alerts.Add(new Alert(Warning, $"HLD 音符缺少时长跟随行") { Line = idx + 1, RelevantNote = FormatNoteRef(note, chart) }); return idx; } - private static int ParseSlideNote(string[] lines, int idx, string code, ChuNote note, List alerts) + private static int ParseSlideNote(string[] lines, int idx, string code, ChuNote note, List alerts, UgcChart chart) { note.Type = "SLD"; - ParseCellWidth(code, 1, note, alerts, idx + 1); + var tpm = chart.TicksPerBeat * 4; + ParseCellWidth(code, 1, note, alerts, idx + 1, chart); bool foundFirst = false; while (idx + 1 < lines.Length) @@ -347,7 +349,7 @@ private static int ParseSlideNote(string[] lines, int idx, string code, ChuNote break; } - note.SlideDuration += duration; + note.Duration += new Rational(duration, tpm); note.EndCell = endCell; note.EndWidth = endWidth; idx++; @@ -355,7 +357,7 @@ private static int ParseSlideNote(string[] lines, int idx, string code, ChuNote } if (!foundFirst) - alerts.Add(new Alert(Warning, $"SLD 音符缺少时长跟随行") { Line = idx + 1, RelevantNote = FormatNoteRef(note) }); + alerts.Add(new Alert(Warning, $"SLD 音符缺少时长跟随行") { Line = idx + 1, RelevantNote = FormatNoteRef(note, chart) }); return idx; } @@ -368,13 +370,19 @@ private static bool TryParseStandaloneFollower(string[] lines, int idx, UgcChart if (!TryParseFollowerLine(line, out var duration, out var endCell, out var endWidth)) return false; // find the last SLD or HLD note and attach duration + var tpm = chart.TicksPerBeat * 4; for (int i = chart.Notes.Count - 1; i >= 0; i--) { var n = chart.Notes[i]; if (n.Type is "SLD" or "HLD") { - if (n.Type == "SLD") { n.SlideDuration = duration; n.EndCell = endCell; n.EndWidth = endWidth; } - else { n.HoldDuration = duration; } + if (n.Type == "SLD") + { + n.Duration = new Rational(duration, tpm); + n.EndCell = endCell; + n.EndWidth = endWidth; + } + else n.Duration = new Rational(duration, tpm); return true; } } @@ -410,7 +418,7 @@ private static bool TryParseFollowerLine(string line, out int duration, out int return true; } - private static void ParseCellWidth(string code, int startIdx, ChuNote note, List alerts, int lineNum) + private static void ParseCellWidth(string code, int startIdx, ChuNote note, List alerts, int lineNum, UgcChart chart) { if (code.Length > startIdx) { @@ -418,15 +426,15 @@ private static void ParseCellWidth(string code, int startIdx, ChuNote note, List if (code.Length > startIdx + 1) note.Width = WidthHexCharToInt(code[startIdx + 1]); else - alerts.Add(new Alert(Warning, $"音符缺少 width: {code}") { Line = lineNum, RelevantNote = FormatNoteRef(note) }); + alerts.Add(new Alert(Warning, $"音符缺少 width: {code}") { Line = lineNum, RelevantNote = FormatNoteRef(note, chart) }); } else { - alerts.Add(new Alert(Warning, $"音符缺少 cell 和 width: {code}") { Line = lineNum, RelevantNote = FormatNoteRef(note) }); + alerts.Add(new Alert(Warning, $"音符缺少 cell 和 width: {code}") { Line = lineNum, RelevantNote = FormatNoteRef(note, chart) }); } } - private static void ParseAirNote(string code, ChuNote note, List alerts, int lineNum) + private static void ParseAirNote(string code, ChuNote note, List alerts, int lineNum, UgcChart chart) { // Matches UgcGenerator: "a" + cell + width + two-letter direction + targetNote [ + "_" + airHoldDuration for AHD ] if (code.Length < 5) @@ -436,7 +444,7 @@ private static void ParseAirNote(string code, ChuNote note, List alerts, return; } - ParseCellWidth(code, 1, note, alerts, lineNum); + ParseCellWidth(code, 1, note, alerts, lineNum, chart); var afterCellWidth = code[3..]; var underscoreIdx = afterCellWidth.IndexOf('_'); var mainPart = underscoreIdx >= 0 ? afterCellWidth[..underscoreIdx] : afterCellWidth; @@ -456,7 +464,7 @@ private static void ParseAirNote(string code, ChuNote note, List alerts, else { note.Type = "AIR"; - alerts.Add(new Alert(Warning, $"未知的 AIR 方向: {dir}") { Line = lineNum, RelevantNote = FormatNoteRef(note) }); + alerts.Add(new Alert(Warning, $"未知的 AIR 方向: {dir}") { Line = lineNum, RelevantNote = FormatNoteRef(note, chart) }); } note.TargetNote = mainPart.Length > 2 ? mainPart[2..] : "N"; @@ -465,11 +473,11 @@ private static void ParseAirNote(string code, ChuNote note, List alerts, { var durStr = afterCellWidth[(underscoreIdx + 1)..]; if (int.TryParse(durStr, NumberStyles.Integer, CultureInfo.InvariantCulture, out var ahdDuration)) - note.AirHoldDuration = ahdDuration; + note.Duration = new Rational(ahdDuration, chart.TicksPerBeat * 4); } } - private static void ParseChrNote(string code, ChuNote note, List alerts, int lineNum) + private static void ParseChrNote(string code, ChuNote note, List alerts, int lineNum, UgcChart chart) { note.Type = "CHR"; if (code.Length < 3) @@ -478,7 +486,7 @@ private static void ParseChrNote(string code, ChuNote note, List alerts, return; } - ParseCellWidth(code, 1, note, alerts, lineNum); + ParseCellWidth(code, 1, note, alerts, lineNum, chart); var extraRaw = code.Length > 3 ? code[3..] : ""; if (ChrExtras.TryGetValue(extraRaw, out var chrDir)) note.Tag = chrDir; @@ -508,9 +516,11 @@ private static int WidthHexCharToInt(char c) }; } - private static string FormatNoteRef(ChuNote note) + private static string FormatNoteRef(ChuNote note, UgcChart chart) { - return $"#{note.Measure}'{note.Offset}:{note.Type}"; + var tpm = chart.TicksPerBeat * 4; + var (m, o) = Utils.BarAndTick(note.Time, tpm, 0); + return $"#{m}'{o}:{note.Type}"; } } diff --git a/tests/chu/ChuTests.cs b/tests/chu/ChuTests.cs index 46c12f3..9ae4617 100644 --- a/tests/chu/ChuTests.cs +++ b/tests/chu/ChuTests.cs @@ -1,5 +1,7 @@ using System.Reflection; using MuConvert.chu; +using MuConvert.utils; +using Rationals; namespace MuConvert.Tests.chu; @@ -49,29 +51,21 @@ public void C2sRoundTrip() /// private static string SnapshotNote(ChuNote note) { - var props = typeof(ChuNote).GetProperties(BindingFlags.Instance | BindingFlags.Public); - var parts = props - .OrderBy(p => p.Name) - .Select(p => $"{p.Name}={p.GetValue(note)}"); - return string.Join("|", parts); - } - - /// - /// Same tick scaling as when converting UGC → C2S (384 ticks per measure). - /// - private static ChuNote UgcNoteScaledToC2sTicks(ChuNote n, int ticksPerBeat) - { - const int c2sResolution = 384; - int scaleDown(int v) => (int)((long)v * (c2sResolution / 4) / ticksPerBeat); - return new ChuNote + static string F(object? v) => v switch { - Type = n.Type, Measure = n.Measure, Offset = scaleDown(n.Offset), - Cell = n.Cell, Width = n.Width, - HoldDuration = scaleDown(n.HoldDuration), SlideDuration = scaleDown(n.SlideDuration), - EndCell = n.EndCell, EndWidth = n.EndWidth, - Tag = n.Tag, TargetNote = n.TargetNote, AirHoldDuration = scaleDown(n.AirHoldDuration), - StartHeight = n.StartHeight, TargetHeight = n.TargetHeight, NoteColor = n.NoteColor, + Rational r => r.CanonicalForm.ToString(), + List list => string.Join(",", list), + null => "", + _ => v.ToString() ?? "", }; + + var propParts = typeof(ChuNote).GetProperties(BindingFlags.Instance | BindingFlags.Public) + .OrderBy(p => p.Name) + .Select(p => $"{p.Name}={F(p.GetValue(note))}"); + var fieldParts = typeof(ChuNote).GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly) + .OrderBy(f => f.Name) + .Select(f => $"{f.Name}={F(f.GetValue(note))}"); + return string.Join("|", propParts.Concat(fieldParts)); } /// @@ -82,7 +76,7 @@ private static ChuNote UgcNoteScaledToC2sTicks(ChuNote n, int ticksPerBeat) private static void AssertUgcNotesEquivalentToReparsedC2s(UgcChart ugc, C2sChart c2s, bool isUgcReference) { var ugcSnaps = ugc.Notes - .Select(n => SnapshotNote(UgcNoteScaledToC2sTicks(n, ugc.TicksPerBeat))) + .Select(SnapshotNote) .OrderBy(s => s) .ToArray(); var c2sSnaps = c2s.Notes.Select(SnapshotNote).OrderBy(s => s).ToArray(); diff --git a/utils/Utils.cs b/utils/Utils.cs index 5beebdd..d4784f3 100644 --- a/utils/Utils.cs +++ b/utils/Utils.cs @@ -66,7 +66,7 @@ public static int Tick(Rational time, int resolution, int extraTicks = 0, int? m } } -internal static class ExtensionUtils +public static class ExtensionUtils { internal static void Add(this Dictionary> dict, K key, V value) where K : notnull { From a0ff8b0a6ce03d176eed61f948c1c9011817162f Mon Sep 17 00:00:00 2001 From: Starrah Date: Sun, 3 May 2026 22:59:33 +0800 Subject: [PATCH 03/13] =?UTF-8?q?[R]=20Chart=E7=9B=B8=E5=85=B3=E9=87=8D?= =?UTF-8?q?=E6=9E=84=EF=BC=88=E5=88=9D=E6=AD=A5=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chart/BPMList.cs | 9 +++++++++ chart/chu/C2sChart.cs | 12 ++++++----- chart/chu/SusChart.cs | 2 +- chart/chu/UgcChart.cs | 9 ++++----- generator/chu/C2sGenerator.cs | 20 +++++++++--------- generator/chu/SusGenerator.cs | 8 ++++---- generator/chu/UgcGenerator.cs | 20 +++++++----------- generator/mai/MA2Generator.cs | 11 +--------- parser/chu/C2sParser.cs | 8 +++----- parser/chu/SusParser.cs | 2 +- parser/chu/UgcParser.cs | 38 +++++++++++++++-------------------- tests/chu/ChuTests.cs | 2 +- 12 files changed, 64 insertions(+), 77 deletions(-) diff --git a/chart/BPMList.cs b/chart/BPMList.cs index ade5669..9009278 100644 --- a/chart/BPMList.cs +++ b/chart/BPMList.cs @@ -99,6 +99,15 @@ internal Rational ConvertTime(Rational startTime, Rational value, decimal? srcBp return result.CanonicalForm; } } + + internal (decimal, decimal, decimal, decimal) BPM_DEF() + { + var bpms = this.Select(x => x.Bpm).ToList(); + var max = bpms.Max(); + var min = bpms.Min(); + var modes = bpms.GroupBy(x => x).OrderByDescending(g => g.Count()).First().Key; // 众数 + return (this.First().Bpm, modes, max, min); + } } public record BPM(Rational Time, decimal Bpm); diff --git a/chart/chu/C2sChart.cs b/chart/chu/C2sChart.cs index 3e027f1..3f9bb70 100644 --- a/chart/chu/C2sChart.cs +++ b/chart/chu/C2sChart.cs @@ -7,12 +7,14 @@ namespace MuConvert.chu; */ public class C2sChart : BaseChart, IChuChart { - public string Version { get; set; } = "1.08.00\t1.08.00"; - public int MusicId { get; set; } - public int DifficultId { get; set; } - public string Creator { get; set; } = ""; + public string Title { get; set; } = ""; + public string Artist { get; set; } = ""; + public string Designer { get; set; } = ""; // 谱师 + public int Difficulty { get; set; } = 3; // 难度,0-basic, 1-advanced, ...。大多数情况下都是数字字符串。不直接存成数字是为了,万一自制谱这里写的不是数字、保留鲁棒性 + public string DisplayLevel { get; set; } = ""; // 显示等级,字符串 + public decimal Level { get; set; } // 定数,小数 + public string MusicId { get; set; } = "0"; public int Resolution { get; set; } = 384; - public double DefBpm { get; set; } = 120.0; public List<(int Measure, int Offset, double Bpm)> BpmEvents = []; public List<(int Measure, int Offset, int Denom, int Num)> MetEvents = []; public List<(int Measure, int Offset, int Duration, double Multiplier)> SflEvents = []; diff --git a/chart/chu/SusChart.cs b/chart/chu/SusChart.cs index 9fde34b..c458ea8 100644 --- a/chart/chu/SusChart.cs +++ b/chart/chu/SusChart.cs @@ -11,5 +11,5 @@ public class SusChart : BaseChart, IChuChart public string Artist { get; set; } = ""; public string Designer { get; set; } = ""; public int TicksPerBeat { get; set; } = 480; - public double Bpm { get; set; } = 120.0; + public List<(int Measure, int Offset, double Bpm)> BpmEvents = []; } diff --git a/chart/chu/UgcChart.cs b/chart/chu/UgcChart.cs index 4cddf4e..cba5667 100644 --- a/chart/chu/UgcChart.cs +++ b/chart/chu/UgcChart.cs @@ -7,14 +7,13 @@ namespace MuConvert.chu; */ public class UgcChart : BaseChart, IChuChart { - public string Version { get; set; } = "6"; public string Title { get; set; } = ""; public string Artist { get; set; } = ""; public string Designer { get; set; } = ""; - public string Difficulty { get; set; } = ""; - public int Level { get; set; } - public double Constant { get; set; } - public string SongId { get; set; } = ""; + public int Difficulty { get; set; } = 3; + public string DisplayLevel = ""; + public decimal Level { get; set; } + public string MusicId { get; set; } = ""; public int TicksPerBeat { get; set; } = 480; public List<(int Measure, int Num, int Den)> BeatEvents = []; public List<(int Measure, int Offset, double Bpm)> BpmEvents = []; diff --git a/generator/chu/C2sGenerator.cs b/generator/chu/C2sGenerator.cs index 3160505..7981f87 100644 --- a/generator/chu/C2sGenerator.cs +++ b/generator/chu/C2sGenerator.cs @@ -30,9 +30,7 @@ private static C2sChart ConvertToC2s(IChuChart chart, List alerts) { var result = new C2sChart { - Version = "1.08.00\t1.08.00", - Creator = ugc.Designer, - DefBpm = ugc.BpmEvents.Count > 0 ? ugc.BpmEvents[0].Bpm : 120.0, + Designer = ugc.Designer, }; foreach (var b in ugc.BpmEvents) result.BpmEvents.Add((b.Measure, ScaleDown(b.Offset, ugc.TicksPerBeat), b.Bpm)); @@ -44,8 +42,8 @@ private static C2sChart ConvertToC2s(IChuChart chart, List alerts) if (chart is SusChart sus) { - var result = new C2sChart { DefBpm = sus.Bpm }; - result.BpmEvents.Add((0, 0, sus.Bpm)); + var result = new C2sChart(); + // result.BpmEvents.Add((0, 0, sus.Bpm)); result.Notes = sus.Notes; return result; } @@ -60,14 +58,16 @@ private static string Serialize(C2sChart chart) { chart.Sort(); + int.TryParse(chart.MusicId, out var musicId); var sb = new StringBuilder(); - sb.AppendLine($"VERSION\t{chart.Version}"); - sb.AppendLine($"MUSIC\t{chart.MusicId}"); + sb.AppendLine($"VERSION\t1.08.00\t1.08.00"); + sb.AppendLine($"MUSIC\t{musicId}"); sb.AppendLine("SEQUENCEID\t0"); - sb.AppendLine($"DIFFICULT\t{chart.DifficultId:D2}"); + sb.AppendLine($"DIFFICULT\t{chart.Difficulty:D2}"); sb.AppendLine("LEVEL\t0.0"); - sb.AppendLine($"CREATOR\t{chart.Creator}"); - sb.AppendLine($"BPM_DEF\t{Fmt(chart.DefBpm)}\t{Fmt(chart.DefBpm)}\t{Fmt(chart.DefBpm)}\t{Fmt(chart.DefBpm)}"); + sb.AppendLine($"CREATOR\t{chart.Designer}"); + var bpm_def = chart.BpmList.BPM_DEF(); + sb.AppendLine($"BPM_DEF\t{bpm_def.Item1}\t{bpm_def.Item2}\t{bpm_def.Item3}\t{bpm_def.Item4}"); sb.AppendLine("MET_DEF\t4\t4"); sb.AppendLine($"RESOLUTION\t{chart.Resolution}"); sb.AppendLine($"CLK_DEF\t{chart.Resolution}"); diff --git a/generator/chu/SusGenerator.cs b/generator/chu/SusGenerator.cs index 84bc320..56d1bb8 100644 --- a/generator/chu/SusGenerator.cs +++ b/generator/chu/SusGenerator.cs @@ -30,8 +30,8 @@ private static SusChart ConvertToSus(IChuChart chart, List alerts) if (chart is C2sChart c2s) { - bpm = c2s.BpmEvents.Count > 0 ? c2s.BpmEvents[0].Bpm : c2s.DefBpm; - var result = new SusChart { Bpm = bpm, TicksPerBeat = SusTpb, Title = title, Artist = artist }; + bpm = c2s.BpmEvents[0].Bpm; + var result = new SusChart { TicksPerBeat = SusTpb, Title = title, Artist = artist }; result.Notes = c2s.Notes; return result; } @@ -39,7 +39,7 @@ private static SusChart ConvertToSus(IChuChart chart, List alerts) if (chart is UgcChart ugc) { bpm = ugc.BpmEvents.Count > 0 ? ugc.BpmEvents[0].Bpm : 120.0; - var result = new SusChart { Bpm = bpm, TicksPerBeat = SusTpb, Title = ugc.Title, Artist = ugc.Artist }; + var result = new SusChart { TicksPerBeat = SusTpb, Title = ugc.Title, Artist = ugc.Artist }; result.Notes = ugc.Notes; return result; } @@ -56,7 +56,7 @@ private static string Serialize(SusChart sus) if (!string.IsNullOrEmpty(sus.Title)) sb.AppendLine($"#TITLE \"{sus.Title}\""); if (!string.IsNullOrEmpty(sus.Artist)) sb.AppendLine($"#ARTIST \"{sus.Artist}\""); if (!string.IsNullOrEmpty(sus.Designer)) sb.AppendLine($"#DESIGNER \"{sus.Designer}\""); - sb.AppendLine($"#BPM_DEF {sus.Bpm:F2}"); + sb.AppendLine($"#BPM_DEF {sus.StartBpm:F2}"); sb.AppendLine($"#REQUEST \"{sus.TicksPerBeat}\""); sb.AppendLine(); diff --git a/generator/chu/UgcGenerator.cs b/generator/chu/UgcGenerator.cs index ae9ce89..e6ac0c4 100644 --- a/generator/chu/UgcGenerator.cs +++ b/generator/chu/UgcGenerator.cs @@ -31,9 +31,9 @@ private static UgcChart ConvertToUgc(IChuChart chart, List alerts) var result = new UgcChart { TicksPerBeat = UgcTicksPerBeat, - Designer = c2s.Creator, - Difficulty = MapDiffId(c2s.DifficultId), - SongId = c2s.MusicId.ToString(), + Designer = c2s.Designer, + Difficulty = c2s.Difficulty, + MusicId = c2s.MusicId, }; foreach (var b in c2s.BpmEvents) result.BpmEvents.Add((b.Measure, ScaleUp(b.Offset), b.Bpm)); @@ -49,11 +49,6 @@ private static UgcChart ConvertToUgc(IChuChart chart, List alerts) private static int ScaleUp(int v) => (int)((long)v * UgcTicksPerBeat / (C2sResolution / 4)); - private static string MapDiffId(int id) => id switch - { - 0 => "BASIC", 1 => "ADVANCED", 2 => "EXPERT", 3 => "MASTER", 4 => "ULTIMA", _ => "0" - }; - private static string Serialize(UgcChart ugc) { ugc.Sort(); @@ -63,10 +58,10 @@ private static string Serialize(UgcChart ugc) if (!string.IsNullOrEmpty(ugc.Title)) sb.AppendLine($"@TITLE\t{ugc.Title}"); if (!string.IsNullOrEmpty(ugc.Artist)) sb.AppendLine($"@ARTIST\t{ugc.Artist}"); if (!string.IsNullOrEmpty(ugc.Designer)) sb.AppendLine($"@DESIGN\t{ugc.Designer}"); - sb.AppendLine($"@DIFF\t{DiffId(ugc.Difficulty)}"); - sb.AppendLine($"@LEVEL\t{ugc.Level}"); - sb.AppendLine($"@CONST\t{ugc.Constant:F5}"); - sb.AppendLine($"@SONGID\t{ugc.SongId}"); + sb.AppendLine($"@DIFF\t{ugc.Difficulty}"); + sb.AppendLine($"@LEVEL\t{ugc.DisplayLevel}"); + sb.AppendLine($"@CONST\t{ugc.Level:F5}"); + sb.AppendLine($"@SONGID\t{ugc.MusicId}"); sb.AppendLine($"@TICKS\t{ugc.TicksPerBeat}"); foreach (var b in ugc.BeatEvents) sb.AppendLine($"@BEAT\t{b.Measure}\t{b.Num}\t{b.Den}"); foreach (var b in ugc.BpmEvents) sb.AppendLine($"@BPM\t{b.Measure}'{b.Offset}\t{b.Bpm:F5}"); @@ -117,5 +112,4 @@ private static string UCode(ChuNote n, int tpm) private static string Hx(int v) => "0123456789ABCDEF"[Math.Clamp(v, 0, 15)].ToString(); private static string Hw(int v) => "123456789ABCDEFG"[Math.Clamp(v - 1, 0, 15)].ToString(); - private static int DiffId(string d) => d switch { "BASIC" => 0, "ADVANCED" => 1, "EXPERT" => 2, "MASTER" => 3, "ULTIMA" => 4, _ => 0 }; } diff --git a/generator/mai/MA2Generator.cs b/generator/mai/MA2Generator.cs index 7aed680..2d6ec74 100644 --- a/generator/mai/MA2Generator.cs +++ b/generator/mai/MA2Generator.cs @@ -45,15 +45,6 @@ GENERATED_BY MuConvert v{8} "; private Rational __1_384 = new(1, 384); - - private (decimal, decimal, decimal, decimal) bpmStats() - { - var bpms = chart.BpmList.Select(x => x.Bpm).ToList(); - var max = bpms.Max(); - var min = bpms.Min(); - var modes = bpms.GroupBy(x => x).OrderByDescending(g => g.Count()).First().Key; // 众数 - return (chart.BpmList.First().Bpm, modes, max, min); - } /** * 把Rational的时间近似到RESOLUTION允许的最接近tick上 @@ -196,7 +187,7 @@ protected virtual List AddSlide(Slide slide, int bar, int tick) // 生成文件头 protected void GenerateFileHead(StringBuilder result) { - var bpmStatistics = bpmStats(); + var bpmStatistics = chart.BpmList.BPM_DEF(); string head = string.Format(headTemplate, $"{MA2Version / 100}.{MA2Version % 100:D2}.00", IsUtage?1:0, bpmStatistics.Item1, bpmStatistics.Item2, bpmStatistics.Item3, bpmStatistics.Item4, diff --git a/parser/chu/C2sParser.cs b/parser/chu/C2sParser.cs index 56cb6d6..109a405 100644 --- a/parser/chu/C2sParser.cs +++ b/parser/chu/C2sParser.cs @@ -57,11 +57,9 @@ private static void ParseHeader(string[] p, C2sChart chart) var tag = p[0].ToUpperInvariant(); switch (tag) { - case "VERSION": chart.Version = Str(p, 1); break; - case "MUSIC": chart.MusicId = Int(p, 1); break; - case "DIFFICULT": chart.DifficultId = Int(p, 1); break; - case "CREATOR": chart.Creator = Str(p, 1); break; - case "BPM_DEF": chart.DefBpm = Dbl(p, 1, 120.0); break; + case "MUSIC": chart.MusicId = Int(p, 1).ToString(); break; + case "DIFFICULT": chart.Difficulty = Int(p, 1); break; + case "CREATOR": chart.Designer = Str(p, 1); break; case "RESOLUTION": chart.Resolution = Math.Max(1, Int(p, 1, 384)); break; } } diff --git a/parser/chu/SusParser.cs b/parser/chu/SusParser.cs index 20816b0..f4b2a90 100644 --- a/parser/chu/SusParser.cs +++ b/parser/chu/SusParser.cs @@ -84,7 +84,7 @@ private static void ParseHeaderLine(string content, SusChart chart, List { var bpmStr = content[8..].Trim().Trim('"'); if (double.TryParse(bpmStr, NumberStyles.Float, CultureInfo.InvariantCulture, out var bpm)) - chart.Bpm = bpm; + chart.BpmEvents.Add((0, 0, bpm)); else alerts.Add(new Alert(Warning, $"BPM_DEF 格式错误: {content}") { Line = lineNum }); } diff --git a/parser/chu/UgcParser.cs b/parser/chu/UgcParser.cs index b9250b8..c485ab3 100644 --- a/parser/chu/UgcParser.cs +++ b/parser/chu/UgcParser.cs @@ -77,10 +77,6 @@ private static void ParseHeaderLine(string line, UgcChart chart, List ale switch (tag) { - case "@VER": - chart.Version = value; - break; - case "@TITLE": chart.Title = value; break; @@ -96,38 +92,36 @@ private static void ParseHeaderLine(string line, UgcChart chart, List ale case "@DIFF": if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var diff)) { - chart.Difficulty = diff switch - { - 0 => "BASIC", - 1 => "ADVANCED", - 2 => "EXPERT", - 3 => "MASTER", - 4 => "ULTIMA", - _ => value, - }; + chart.Difficulty = diff; } else { - chart.Difficulty = value; + chart.Difficulty = new string(value.Where(char.IsLetter).ToArray()).ToUpperInvariant() switch + { + "BASIC" => 0, + "ADVANCED" => 1, + "EXPERT" => 2, + "MASTER" => 3, + "WORLDSEND" => 4, + "ULTIMA" => 5, + _ => 3, + }; } break; case "@LEVEL": - if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var level)) - chart.Level = level; - else - alerts.Add(new Alert(Warning, $"@LEVEL 格式错误: {line}") { Line = lineNum }); + chart.DisplayLevel = value; break; case "@CONST": - if (double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var constant)) - chart.Constant = constant; + if (decimal.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var constant)) + chart.Level = constant; else alerts.Add(new Alert(Warning, $"@CONST 格式错误: {line}") { Line = lineNum }); break; case "@SONGID": - chart.SongId = value; + chart.MusicId = value; break; case "@TICKS": @@ -179,7 +173,7 @@ private static void ParseHeaderLine(string line, UgcChart chart, List ale break; // silently ignored metadata tags - case "@EXVER": case "@SORT": case "@BGM": case "@BGMOFS": case "@BGMPRV": + case "@VER": case "@EXVER": case "@SORT": case "@BGM": case "@BGMOFS": case "@BGMPRV": case "@JACKET": case "@BGIMG": case "@BGMODE": case "@FLDCOL": case "@FLDIMG": case "@FLAG": case "@ATINFO": case "@DLURL": case "@COPYRIGHT": case "@LICENSE": case "@MAINTIL": diff --git a/tests/chu/ChuTests.cs b/tests/chu/ChuTests.cs index 9ae4617..f416fc8 100644 --- a/tests/chu/ChuTests.cs +++ b/tests/chu/ChuTests.cs @@ -90,7 +90,7 @@ public void CanParseUgc() if (!File.Exists(UgcPath)) throw new SkipException($"Missing: {UgcPath}"); var (chart, _) = new UgcParser().Parse(File.ReadAllText(UgcPath)); Assert.NotEmpty(chart.Notes); - Assert.Equal("MASTER", chart.Difficulty); + Assert.Equal(3, chart.Difficulty); } [Fact] From 84a39e5102bd72eb10b65abffcc7689a56a31a1e Mon Sep 17 00:00:00 2001 From: Starrah Date: Sun, 3 May 2026 23:47:35 +0800 Subject: [PATCH 04/13] =?UTF-8?q?[R]=20Chart=E7=9B=B8=E5=85=B3=E9=87=8D?= =?UTF-8?q?=E6=9E=84=EF=BC=88=E7=AC=AC=E4=BA=8C=E6=AD=A5=EF=BC=89=E2=80=94?= =?UTF-8?q?=E2=80=94=E4=BD=BF=E7=94=A8BaseChart=E4=B8=AD=E6=8F=90=E4=BE=9B?= =?UTF-8?q?=E7=9A=84List=E5=AF=B9=E8=B1=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chart/chu/C2sChart.cs | 5 +-- chart/chu/SusChart.cs | 1 - chart/chu/UgcChart.cs | 5 +-- generator/chu/C2sGenerator.cs | 48 ++++++++++++++---------- generator/chu/SusGenerator.cs | 7 +++- generator/chu/UgcGenerator.cs | 29 ++++++++------- parser/chu/C2sParser.cs | 12 ++++-- parser/chu/SusParser.cs | 3 +- parser/chu/UgcParser.cs | 14 ++++--- tests/chu/ChuTests.cs | 70 ++++++++++++++++++++++++++++++----- 10 files changed, 131 insertions(+), 63 deletions(-) diff --git a/chart/chu/C2sChart.cs b/chart/chu/C2sChart.cs index 3f9bb70..ee27eae 100644 --- a/chart/chu/C2sChart.cs +++ b/chart/chu/C2sChart.cs @@ -1,4 +1,5 @@ using MuConvert.chart; +using Rationals; namespace MuConvert.chu; @@ -15,7 +16,5 @@ public class C2sChart : BaseChart, IChuChart public decimal Level { get; set; } // 定数,小数 public string MusicId { get; set; } = "0"; public int Resolution { get; set; } = 384; - public List<(int Measure, int Offset, double Bpm)> BpmEvents = []; - public List<(int Measure, int Offset, int Denom, int Num)> MetEvents = []; - public List<(int Measure, int Offset, int Duration, double Multiplier)> SflEvents = []; + public List<(Rational Time, Rational Duration, decimal Multiplier)> SflList = []; // 所有变速声明构成的列表。 } diff --git a/chart/chu/SusChart.cs b/chart/chu/SusChart.cs index c458ea8..a3bd6b2 100644 --- a/chart/chu/SusChart.cs +++ b/chart/chu/SusChart.cs @@ -11,5 +11,4 @@ public class SusChart : BaseChart, IChuChart public string Artist { get; set; } = ""; public string Designer { get; set; } = ""; public int TicksPerBeat { get; set; } = 480; - public List<(int Measure, int Offset, double Bpm)> BpmEvents = []; } diff --git a/chart/chu/UgcChart.cs b/chart/chu/UgcChart.cs index cba5667..908b9d3 100644 --- a/chart/chu/UgcChart.cs +++ b/chart/chu/UgcChart.cs @@ -1,4 +1,5 @@ using MuConvert.chart; +using Rationals; namespace MuConvert.chu; @@ -15,7 +16,5 @@ public class UgcChart : BaseChart, IChuChart public decimal Level { get; set; } public string MusicId { get; set; } = ""; public int TicksPerBeat { get; set; } = 480; - public List<(int Measure, int Num, int Den)> BeatEvents = []; - public List<(int Measure, int Offset, double Bpm)> BpmEvents = []; - public List<(int Measure, int Offset, double Multiplier)> SpeedEvents = []; + public List<(Rational Time, Rational Duration, decimal Multiplier)> SflList = []; // 所有变速声明构成的列表。 } diff --git a/generator/chu/C2sGenerator.cs b/generator/chu/C2sGenerator.cs index 7981f87..f64d822 100644 --- a/generator/chu/C2sGenerator.cs +++ b/generator/chu/C2sGenerator.cs @@ -1,5 +1,6 @@ using System.Globalization; using System.Text; +using MuConvert.chart; using MuConvert.generator; using MuConvert.utils; using static MuConvert.utils.Alert.LEVEL; @@ -12,8 +13,6 @@ namespace MuConvert.chu; */ public class C2sGenerator : IGenerator { - private const int C2sResolution = 384; - public (string, List) Generate(IChuChart chart) { var alerts = new List(); @@ -32,10 +31,8 @@ private static C2sChart ConvertToC2s(IChuChart chart, List alerts) { Designer = ugc.Designer, }; - foreach (var b in ugc.BpmEvents) - result.BpmEvents.Add((b.Measure, ScaleDown(b.Offset, ugc.TicksPerBeat), b.Bpm)); - foreach (var b in ugc.BeatEvents) - result.MetEvents.Add((b.Measure, 0, b.Den, b.Num)); + result.BpmList.AddRange(ugc.BpmList); + result.MetList.AddRange(ugc.MetList); result.Notes = ugc.Notes; return result; } @@ -43,7 +40,10 @@ private static C2sChart ConvertToC2s(IChuChart chart, List alerts) if (chart is SusChart sus) { var result = new C2sChart(); - // result.BpmEvents.Add((0, 0, sus.Bpm)); + if (sus.BpmList.Count > 0) + result.BpmList.AddRange(sus.BpmList); + else + result.BpmList.Add(new BPM(0, 120m)); result.Notes = sus.Notes; return result; } @@ -52,8 +52,6 @@ private static C2sChart ConvertToC2s(IChuChart chart, List alerts) throw new ConversionException(alerts); } - private static int ScaleDown(int ticks, int tpb) => (int)((long)ticks * (C2sResolution / 4) / tpb); - private static string Serialize(C2sChart chart) { chart.Sort(); @@ -76,12 +74,25 @@ private static string Serialize(C2sChart chart) sb.AppendLine("TUTORIAL\t0"); sb.AppendLine(); - foreach (var b in chart.BpmEvents) - sb.AppendLine($"BPM\t{b.Measure}\t{b.Offset}\t{Fmt(b.Bpm)}"); - foreach (var m in chart.MetEvents) - sb.AppendLine($"MET\t{m.Measure}\t{m.Offset}\t{m.Denom}\t{m.Num}"); - foreach (var s in chart.SflEvents) - sb.AppendLine($"SFL\t{s.Measure}\t{s.Offset}\t{s.Duration}\t{Mlt(s.Multiplier)}"); + var res = chart.Resolution; + foreach (var b in chart.BpmList) + { + var (m, o) = Utils.BarAndTick(b.Time, res); + sb.AppendLine($"BPM\t{m}\t{o}\t{b.Bpm:0.000}"); + } + + foreach (var met in chart.MetList) + { + var (m, o) = Utils.BarAndTick(met.Time, res); + sb.AppendLine($"MET\t{m}\t{o}\t{met.Denominator}\t{met.Numerator}"); + } + + foreach (var s in chart.SflList.OrderBy(s => s.Time)) + { + var (m, o) = Utils.BarAndTick(s.Time, res); + var durTicks = Utils.Tick(s.Duration, res); + sb.AppendLine($"SFL\t{m}\t{o}\t{durTicks}\t{s.Multiplier:0.000000}"); + } sb.AppendLine(); foreach (var n in chart.Notes) @@ -93,8 +104,8 @@ private static string Serialize(C2sChart chart) private static string FormatNote(ChuNote n, int tpm) { - var (m, o) = Utils.BarAndTick(n.Time, tpm, 0); - var durTicks = Utils.Tick(n.Duration, tpm, 0); + var (m, o) = Utils.BarAndTick(n.Time, tpm); + var durTicks = Utils.Tick(n.Duration, tpm); return n.Type switch { "TAP" => $"TAP\t{m}\t{o}\t{n.Cell}\t{n.Width}", @@ -132,7 +143,4 @@ private static string FormatAld(ChuNote n, int m, int o) var tail = n.ExtraData.Count > 3 ? n.ExtraData[3] : 0; return $"ALD\t{m}\t{o}\t{n.Cell}\t{n.Width}\t{a}\t{b}\t{c}\t{n.EndCell}\t{n.EndWidth}\t{tail}"; } - - private static string Fmt(double v) => v.ToString("0.000", CultureInfo.InvariantCulture); - private static string Mlt(double v) => v.ToString("0.000000", CultureInfo.InvariantCulture); } diff --git a/generator/chu/SusGenerator.cs b/generator/chu/SusGenerator.cs index 56d1bb8..d1d2696 100644 --- a/generator/chu/SusGenerator.cs +++ b/generator/chu/SusGenerator.cs @@ -1,4 +1,5 @@ using System.Text; +using MuConvert.chart; using MuConvert.generator; using MuConvert.utils; using static MuConvert.utils.Alert.LEVEL; @@ -30,16 +31,18 @@ private static SusChart ConvertToSus(IChuChart chart, List alerts) if (chart is C2sChart c2s) { - bpm = c2s.BpmEvents[0].Bpm; + bpm = c2s.BpmList.Count > 0 ? (double)c2s.BpmList[0].Bpm : 120.0; var result = new SusChart { TicksPerBeat = SusTpb, Title = title, Artist = artist }; + result.BpmList.Add(new BPM(0, (decimal)bpm)); result.Notes = c2s.Notes; return result; } if (chart is UgcChart ugc) { - bpm = ugc.BpmEvents.Count > 0 ? ugc.BpmEvents[0].Bpm : 120.0; + bpm = ugc.BpmList.Count > 0 ? (double)ugc.BpmList[0].Bpm : 120.0; var result = new SusChart { TicksPerBeat = SusTpb, Title = ugc.Title, Artist = ugc.Artist }; + result.BpmList.Add(new BPM(0, (decimal)bpm)); result.Notes = ugc.Notes; return result; } diff --git a/generator/chu/UgcGenerator.cs b/generator/chu/UgcGenerator.cs index e6ac0c4..b3bbeaf 100644 --- a/generator/chu/UgcGenerator.cs +++ b/generator/chu/UgcGenerator.cs @@ -12,7 +12,6 @@ namespace MuConvert.chu; public class UgcGenerator : IGenerator { private const int UgcTicksPerBeat = 480; - private const int C2sResolution = 384; public (string, List) Generate(IChuChart chart) { @@ -35,10 +34,8 @@ private static UgcChart ConvertToUgc(IChuChart chart, List alerts) Difficulty = c2s.Difficulty, MusicId = c2s.MusicId, }; - foreach (var b in c2s.BpmEvents) - result.BpmEvents.Add((b.Measure, ScaleUp(b.Offset), b.Bpm)); - foreach (var m in c2s.MetEvents) - result.BeatEvents.Add((m.Measure, m.Num, m.Denom)); + result.BpmList.AddRange(c2s.BpmList); + result.MetList.AddRange(c2s.MetList); result.Notes = c2s.Notes; return result; } @@ -47,8 +44,6 @@ private static UgcChart ConvertToUgc(IChuChart chart, List alerts) throw new ConversionException(alerts); } - private static int ScaleUp(int v) => (int)((long)v * UgcTicksPerBeat / (C2sResolution / 4)); - private static string Serialize(UgcChart ugc) { ugc.Sort(); @@ -63,20 +58,28 @@ private static string Serialize(UgcChart ugc) sb.AppendLine($"@CONST\t{ugc.Level:F5}"); sb.AppendLine($"@SONGID\t{ugc.MusicId}"); sb.AppendLine($"@TICKS\t{ugc.TicksPerBeat}"); - foreach (var b in ugc.BeatEvents) sb.AppendLine($"@BEAT\t{b.Measure}\t{b.Num}\t{b.Den}"); - foreach (var b in ugc.BpmEvents) sb.AppendLine($"@BPM\t{b.Measure}'{b.Offset}\t{b.Bpm:F5}"); + var tpm = ugc.TicksPerBeat * 4; + foreach (var met in ugc.MetList) + { + var (m, _) = Utils.BarAndTick(met.Time, tpm); + sb.AppendLine($"@BEAT\t{m}\t{met.Numerator}\t{met.Denominator}"); + } + foreach (var b in ugc.BpmList) + { + var (m, o) = Utils.BarAndTick(b.Time, tpm); + sb.AppendLine($"@BPM\t{m}'{o}\t{b.Bpm:F5}"); + } sb.AppendLine("@TIL\t0\t0'0\t1.00000"); sb.AppendLine("@MAINTIL\t0"); sb.AppendLine("@ENDHEAD"); sb.AppendLine(); - var tpm = ugc.TicksPerBeat * 4; foreach (var n in ugc.Notes) { - var (m, o) = Utils.BarAndTick(n.Time, tpm, 0); + var (m, o) = Utils.BarAndTick(n.Time, tpm); sb.Append($"#{m}'{o}:{UCode(n, tpm)}"); sb.AppendLine(); - var durTicks = Utils.Tick(n.Duration, tpm, 0); + var durTicks = Utils.Tick(n.Duration, tpm); if (n.Type == "HLD" && durTicks > 0) sb.AppendLine($"#{durTicks}>s"); else if (n.Type == "SLD" && durTicks > 0) @@ -88,7 +91,7 @@ private static string Serialize(UgcChart ugc) private static string UCode(ChuNote n, int tpm) { string c = Hx(n.Cell), w = Hw(n.Width); - var durTicks = Utils.Tick(n.Duration, tpm, 0); + var durTicks = Utils.Tick(n.Duration, tpm); var targetNote = string.IsNullOrEmpty(n.TargetNote) ? "N" : n.TargetNote; return n.Type switch { diff --git a/parser/chu/C2sParser.cs b/parser/chu/C2sParser.cs index 109a405..3f14ea4 100644 --- a/parser/chu/C2sParser.cs +++ b/parser/chu/C2sParser.cs @@ -1,4 +1,5 @@ using System.Globalization; +using MuConvert.chart; using MuConvert.parser; using MuConvert.utils; using Rationals; @@ -67,16 +68,20 @@ private static void ParseHeader(string[] p, C2sChart chart) private static void ParseTiming(string[] p, C2sChart chart) { var tag = p[0].ToUpperInvariant(); + var tpm = chart.Resolution; switch (tag) { case "BPM": - chart.BpmEvents.Add((Int(p, 1), Int(p, 2), Dbl(p, 3, 120.0))); + chart.BpmList.Add(new BPM(Int(p, 1) + new Rational(Int(p, 2), tpm), decimal.Parse(p[3]))); break; case "MET": - chart.MetEvents.Add((Int(p, 1), Int(p, 2), Int(p, 3, 4), Int(p, 4, 4))); + chart.MetList.Add(new MET(Int(p, 1) + new Rational(Int(p, 2), tpm), Int(p, 4, 4), Int(p, 3, 4))); break; case "SFL": - chart.SflEvents.Add((Int(p, 1), Int(p, 2), Int(p, 3), Dbl(p, 4, 1.0))); + chart.SflList.Add(( + Int(p, 1) + new Rational(Int(p, 2), tpm), + new Rational(Int(p, 3), tpm), + decimal.Parse(p[4]))); break; } } @@ -144,6 +149,5 @@ private static void ParseNote(string[] p, C2sChart chart, List alerts, in } private static int Int(string[] p, int i, int def = 0) => i < p.Length && int.TryParse(p[i], NumberStyles.Integer, CultureInfo.InvariantCulture, out var v) ? v : def; - private static double Dbl(string[] p, int i, double def = 0) => i < p.Length && double.TryParse(p[i], NumberStyles.Float, CultureInfo.InvariantCulture, out var v) ? v : def; private static string Str(string[] p, int i) => i < p.Length ? p[i] : ""; } diff --git a/parser/chu/SusParser.cs b/parser/chu/SusParser.cs index f4b2a90..5ea3e49 100644 --- a/parser/chu/SusParser.cs +++ b/parser/chu/SusParser.cs @@ -1,4 +1,5 @@ using System.Globalization; +using MuConvert.chart; using MuConvert.parser; using MuConvert.utils; using Rationals; @@ -84,7 +85,7 @@ private static void ParseHeaderLine(string content, SusChart chart, List { var bpmStr = content[8..].Trim().Trim('"'); if (double.TryParse(bpmStr, NumberStyles.Float, CultureInfo.InvariantCulture, out var bpm)) - chart.BpmEvents.Add((0, 0, bpm)); + chart.BpmList.Add(new BPM(0, (decimal)bpm)); else alerts.Add(new Alert(Warning, $"BPM_DEF 格式错误: {content}") { Line = lineNum }); } diff --git a/parser/chu/UgcParser.cs b/parser/chu/UgcParser.cs index c485ab3..87b7062 100644 --- a/parser/chu/UgcParser.cs +++ b/parser/chu/UgcParser.cs @@ -1,4 +1,5 @@ using System.Globalization; +using MuConvert.chart; using MuConvert.parser; using MuConvert.utils; using Rationals; @@ -138,7 +139,7 @@ private static void ParseHeaderLine(string line, UgcChart chart, List ale && int.TryParse(beatParts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var beatNum) && int.TryParse(beatParts[2], NumberStyles.Integer, CultureInfo.InvariantCulture, out var beatDen)) { - chart.BeatEvents.Add((beatMeasure, beatNum, beatDen)); + chart.MetList.Add(new MET(beatMeasure, beatNum, beatDen)); } else { @@ -157,9 +158,10 @@ private static void ParseHeaderLine(string line, UgcChart chart, List ale if (apostropheIdx > 0 && int.TryParse(measureOffset[..apostropheIdx], NumberStyles.Integer, CultureInfo.InvariantCulture, out var bpmMeasure) && int.TryParse(measureOffset[(apostropheIdx + 1)..], NumberStyles.Integer, CultureInfo.InvariantCulture, out var bpmOffset) - && double.TryParse(bpmValueStr, NumberStyles.Float, CultureInfo.InvariantCulture, out var bpmValue)) + && decimal.TryParse(bpmValueStr, NumberStyles.Float, CultureInfo.InvariantCulture, out var bpmValue)) { - chart.BpmEvents.Add((bpmMeasure, bpmOffset, bpmValue)); + var tpm = chart.TicksPerBeat * 4; + chart.BpmList.Add(new BPM(bpmMeasure + new Rational(bpmOffset, tpm), bpmValue)); } else { @@ -176,15 +178,15 @@ private static void ParseHeaderLine(string line, UgcChart chart, List ale case "@VER": case "@EXVER": case "@SORT": case "@BGM": case "@BGMOFS": case "@BGMPRV": case "@JACKET": case "@BGIMG": case "@BGMODE": case "@FLDCOL": case "@FLDIMG": case "@FLAG": case "@ATINFO": case "@DLURL": case "@COPYRIGHT": case "@LICENSE": - case "@MAINTIL": + case "@MAINTIL": case "@TIL": break; - case "@TIL": case "@SPDMOD": + case "@SPDMOD": { var parts = value.Split(['\t', ' '], StringSplitOptions.RemoveEmptyEntries); if (parts.Length >= 2 && int.TryParse(parts[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out var tilMeasure) && double.TryParse(parts[1], NumberStyles.Float, CultureInfo.InvariantCulture, out var tilMult)) - chart.SpeedEvents.Add((tilMeasure, 0, tilMult)); + chart.SflList.Add((tilMeasure, Rational.Zero, (decimal)tilMult)); } break; diff --git a/tests/chu/ChuTests.cs b/tests/chu/ChuTests.cs index f416fc8..c6327e6 100644 --- a/tests/chu/ChuTests.cs +++ b/tests/chu/ChuTests.cs @@ -69,19 +69,69 @@ private static string SnapshotNote(ChuNote note) } /// - /// Compares UGC-side notes to C2S-side notes in C2S tick space (384): snapshots of - /// for each note vs snapshots of notes. - /// Use with UgcToC2sViaGenerator (source UGC, C2S from generate+parse) or C2sToUgcViaGenerator (UGC from generate+parse, source C2S). + /// 将 UGC 网格上的 的 Time / Duration 投影为「经 C2S 生成器写出再解析」后等价的分数(C2S 小节 tick = )。 + /// + private static ChuNote UgcNoteScaledToC2sTicks(ChuNote n, int ugcTicksPerBeat, int c2sResolution) + { + var tpmUgc = ugcTicksPerBeat * 4; + var (m, oU) = Utils.BarAndTick(n.Time, tpmUgc, 0); + var oC = (int)((long)oU * c2sResolution / tpmUgc); + var time = m + new Rational(oC, c2sResolution); + var dur = new Rational(Utils.Tick(n.Duration, c2sResolution, 0), c2sResolution); + return CloneChuNoteWithTiming(n, time, dur); + } + + /// + /// 将 C2S 网格上的音符投影为「经 UGC 生成器写出再解析」后等价的分数(UGC 小节 tick = × 4)。 + /// + private static ChuNote C2sNoteScaledToUgcTicks(ChuNote n, int ugcTicksPerBeat, int c2sResolution) + { + var tpmUgc = ugcTicksPerBeat * 4; + var (m, oC) = Utils.BarAndTick(n.Time, c2sResolution, 0); + var oU = (int)((long)oC * tpmUgc / c2sResolution); + var time = m + new Rational(oU, tpmUgc); + var dur = new Rational(Utils.Tick(n.Duration, tpmUgc, 0), tpmUgc); + return CloneChuNoteWithTiming(n, time, dur); + } + + private static ChuNote CloneChuNoteWithTiming(ChuNote n, Rational time, Rational duration) => new() + { + Type = n.Type, + Time = time, + Cell = n.Cell, + Width = n.Width, + Duration = duration, + EndCell = n.EndCell, + EndWidth = n.EndWidth, + TargetNote = n.TargetNote, + Tag = n.Tag, + ExtraData = [..n.ExtraData], + }; + + /// + /// 比较 UGC 与 C2S 的音符 IR:因 tick 网格不同,在各自「经对方格式写回再解析」的量化意义下比较快照。 /// private static void AssertUgcNotesEquivalentToReparsedC2s(UgcChart ugc, C2sChart c2s, bool isUgcReference) { - var ugcSnaps = ugc.Notes - .Select(SnapshotNote) - .OrderBy(s => s) - .ToArray(); - var c2sSnaps = c2s.Notes.Select(SnapshotNote).OrderBy(s => s).ToArray(); - if (isUgcReference) Assert.Equal(ugcSnaps, c2sSnaps); - else Assert.Equal(c2sSnaps, ugcSnaps); + var res = c2s.Resolution; + if (isUgcReference) + { + var ugcSnaps = ugc.Notes + .Select(n => SnapshotNote(UgcNoteScaledToC2sTicks(n, ugc.TicksPerBeat, res))) + .OrderBy(s => s) + .ToArray(); + var c2sSnaps = c2s.Notes.Select(SnapshotNote).OrderBy(s => s).ToArray(); + Assert.Equal(ugcSnaps, c2sSnaps); + } + else + { + var ugcSnaps = ugc.Notes.Select(SnapshotNote).OrderBy(s => s).ToArray(); + var c2sSnaps = c2s.Notes + .Select(n => SnapshotNote(C2sNoteScaledToUgcTicks(n, ugc.TicksPerBeat, res))) + .OrderBy(s => s) + .ToArray(); + Assert.Equal(c2sSnaps, ugcSnaps); + } } [Fact] From ba1bbcc1c7b2bfbdfed96aa4ccb8301ba9ada6a7 Mon Sep 17 00:00:00 2001 From: Starrah Date: Mon, 4 May 2026 00:24:30 +0800 Subject: [PATCH 05/13] =?UTF-8?q?[F&R]=201.=20=E7=8E=B0=E5=9C=A8=E7=9A=84u?= =?UTF-8?q?gcgenerator=E7=BC=BA=E5=B0=91SflEvent=E7=9A=84=E7=94=9F?= =?UTF-8?q?=E6=88=90/=E5=86=99=E5=87=BA=E3=80=82=20=20=20=20=202.=20?= =?UTF-8?q?=E7=8E=B0=E5=9C=A8=E7=9A=84ugcparser=EF=BC=8C@SPDMOD=E7=9A=84?= =?UTF-8?q?=E8=A7=A3=E6=9E=90=E6=98=AF=E9=94=99=E7=9A=84=E3=80=82=20=20=20?= =?UTF-8?q?=20=203.=20=E6=8A=BD=E5=8F=96TryParseUgcMeasureTick=E6=96=B9?= =?UTF-8?q?=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chart/chu/ChuNote.cs | 4 +-- generator/chu/C2sGenerator.cs | 1 + generator/chu/UgcGenerator.cs | 8 +++++ parser/chu/UgcParser.cs | 62 +++++++++++++++++++++++++---------- utils/Utils.cs | 2 ++ 5 files changed, 58 insertions(+), 19 deletions(-) diff --git a/chart/chu/ChuNote.cs b/chart/chu/ChuNote.cs index 21f243b..8faf1fc 100644 --- a/chart/chu/ChuNote.cs +++ b/chart/chu/ChuNote.cs @@ -15,7 +15,7 @@ public class ChuNote: BaseNote /** 宽度 (1–16) */ public int Width { get; set; } = 1; /** HLD/SLD/AHD/ASD等的 持续时长 */ - public Rational Duration { get; set; } = 0; + public Rational Duration { get; set => field = value.CanonicalForm; } = 0; /** SLD 终点列 */ public int EndCell { get; set; } /** SLD 终点宽度 */ @@ -27,5 +27,5 @@ public class ChuNote: BaseNote /** ASD/ASC/ALD上具有的、目前含义还不明确的字段,统一收集到这个里面。 */ public List ExtraData = []; - public override Rational EndTime => Time + Duration; + public override Rational EndTime => (Time + Duration).CanonicalForm; } diff --git a/generator/chu/C2sGenerator.cs b/generator/chu/C2sGenerator.cs index f64d822..fa7b0db 100644 --- a/generator/chu/C2sGenerator.cs +++ b/generator/chu/C2sGenerator.cs @@ -33,6 +33,7 @@ private static C2sChart ConvertToC2s(IChuChart chart, List alerts) }; result.BpmList.AddRange(ugc.BpmList); result.MetList.AddRange(ugc.MetList); + result.SflList.AddRange(ugc.SflList); result.Notes = ugc.Notes; return result; } diff --git a/generator/chu/UgcGenerator.cs b/generator/chu/UgcGenerator.cs index b3bbeaf..a111a72 100644 --- a/generator/chu/UgcGenerator.cs +++ b/generator/chu/UgcGenerator.cs @@ -36,6 +36,7 @@ private static UgcChart ConvertToUgc(IChuChart chart, List alerts) }; result.BpmList.AddRange(c2s.BpmList); result.MetList.AddRange(c2s.MetList); + result.SflList.AddRange(c2s.SflList); result.Notes = c2s.Notes; return result; } @@ -70,6 +71,13 @@ private static string Serialize(UgcChart ugc) sb.AppendLine($"@BPM\t{m}'{o}\t{b.Bpm:F5}"); } sb.AppendLine("@TIL\t0\t0'0\t1.00000"); + + foreach (var s in ugc.SflList.OrderBy(x => x.Time)) + { + var (m, o) = Utils.BarAndTick(s.Time, tpm); + sb.AppendLine($"@SPDMOD\t{m}'{o}\t{s.Multiplier:0.00000}"); + } + sb.AppendLine("@MAINTIL\t0"); sb.AppendLine("@ENDHEAD"); sb.AppendLine(); diff --git a/parser/chu/UgcParser.cs b/parser/chu/UgcParser.cs index 87b7062..4bcb92d 100644 --- a/parser/chu/UgcParser.cs +++ b/parser/chu/UgcParser.cs @@ -61,9 +61,26 @@ public class UgcParser : IParser } } + FinalizeUgcSflDurations(chart); return (chart, alerts); } + private static void FinalizeUgcSflDurations(UgcChart chart) + { + if (chart.SflList.Count == 0) return; + chart.SflList = chart.SflList.OrderBy(s => s.Time).ToList(); + var endTime = Utils.Max(chart.SflList[^1].Time, chart.Notes.Max(x=>x.EndTime)); + + for (var i = 0; i < chart.SflList.Count; i++) + { + var t = chart.SflList[i].Time; + var dur = (i < chart.SflList.Count - 1 ? chart.SflList[i+1].Time : endTime) - t; + chart.SflList[i] = chart.SflList[i] with { Duration = dur.CanonicalForm }; + } + + chart.SflList = chart.SflList.Where(x => x.Multiplier != 1).ToList(); // 倍率为1的,没必要放进来的 + } + private static void ParseHeaderLine(string line, UgcChart chart, List alerts, int lineNum) { if (!line.StartsWith('@')) @@ -154,10 +171,7 @@ private static void ParseHeaderLine(string line, UgcChart chart, List ale { var measureOffset = bpmPart[..bpmSpaceIdx]; var bpmValueStr = bpmPart[(bpmSpaceIdx + 1)..]; - var apostropheIdx = measureOffset.IndexOf('\''); - if (apostropheIdx > 0 - && int.TryParse(measureOffset[..apostropheIdx], NumberStyles.Integer, CultureInfo.InvariantCulture, out var bpmMeasure) - && int.TryParse(measureOffset[(apostropheIdx + 1)..], NumberStyles.Integer, CultureInfo.InvariantCulture, out var bpmOffset) + if (TryParseUgcMeasureTick(measureOffset, out var bpmMeasure, out var bpmOffset) && decimal.TryParse(bpmValueStr, NumberStyles.Float, CultureInfo.InvariantCulture, out var bpmValue)) { var tpm = chart.TicksPerBeat * 4; @@ -182,13 +196,19 @@ private static void ParseHeaderLine(string line, UgcChart chart, List ale break; case "@SPDMOD": + { + var parts = value.Split('\t', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + if (parts.Length >= 2 + && TryParseUgcMeasureTick(parts[0], out var meas, out var tick) + && decimal.TryParse(parts[1], NumberStyles.Float, CultureInfo.InvariantCulture, out var mult)) { - var parts = value.Split(['\t', ' '], StringSplitOptions.RemoveEmptyEntries); - if (parts.Length >= 2 && int.TryParse(parts[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out var tilMeasure) - && double.TryParse(parts[1], NumberStyles.Float, CultureInfo.InvariantCulture, out var tilMult)) - chart.SflList.Add((tilMeasure, Rational.Zero, (decimal)tilMult)); + var tpm = chart.TicksPerBeat * 4; + chart.SflList.Add((meas + new Rational(tick, tpm), Rational.Zero, mult)); } + else + alerts.Add(new Alert(Warning, $"@SPDMOD 格式错误: {line}") { Line = lineNum }); break; + } default: alerts.Add(new Alert(Info, $"未知头部标签: {tag}") { Line = lineNum }); @@ -196,6 +216,20 @@ private static void ParseHeaderLine(string line, UgcChart chart, List ale } } + /** UGC 时刻字符串 measure'tick(@BPM、@SPDMOD、音符行 #m't 共用)。 */ + private static bool TryParseUgcMeasureTick(string measureTick, out int measure, out int tick) + { + measure = 0; + tick = 0; + measureTick = measureTick.Trim(); + var ap = measureTick.IndexOf('\''); + if (ap <= 0) + return false; + + return int.TryParse(measureTick[..ap], NumberStyles.Integer, CultureInfo.InvariantCulture, out measure) + && int.TryParse(measureTick[(ap + 1)..], NumberStyles.Integer, CultureInfo.InvariantCulture, out tick); + } + private static int ParseNoteLine(string[] lines, int idx, UgcChart chart, List alerts) { var line = lines[idx]; @@ -219,21 +253,15 @@ private static int ParseNoteLine(string[] lines, int idx, UgcChart chart, List a > b ? a : b; + public static Rational Max(Rational a, Rational b) => a > b ? a : b; + public static Rational Min(Rational a, Rational b) => a < b ? a : b; private static readonly Dictionary _simaiLexerMap = Enumerable.Range(1, L.ruleNames.Length) From 09c5ead1411ec5ead5806bafee8fd7ed2b61d2b6 Mon Sep 17 00:00:00 2001 From: Starrah Date: Mon, 4 May 2026 01:02:21 +0800 Subject: [PATCH 06/13] =?UTF-8?q?[R]=20Chart=E7=9B=B8=E5=85=B3=E9=87=8D?= =?UTF-8?q?=E6=9E=84=EF=BC=88=E7=AC=AC=E4=B8=89=E6=AD=A5=EF=BC=89=E2=80=94?= =?UTF-8?q?=E2=80=94=E7=A7=BB=E9=99=A4=E5=90=84=E4=B8=AA=E8=B0=B1=E9=9D=A2?= =?UTF-8?q?=E4=B8=AD=E7=9A=84Resolution=EF=BC=8C=E8=80=8C=E6=98=AFparser?= =?UTF-8?q?=E8=A7=A3=E6=9E=90=E5=AD=98=E6=88=90=E5=88=86=E6=95=B0=E3=80=81?= =?UTF-8?q?generator=E4=BD=BF=E7=94=A8=E5=86=99=E6=AD=BB=E7=9A=84=E5=9B=BA?= =?UTF-8?q?=E5=AE=9A=E5=80=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chart/chu/C2sChart.cs | 1 - chart/chu/SusChart.cs | 1 - chart/chu/UgcChart.cs | 1 - generator/chu/C2sGenerator.cs | 17 +++++++++-------- generator/chu/SusGenerator.cs | 13 ++++++------- generator/chu/UgcGenerator.cs | 18 ++++++++---------- parser/chu/C2sParser.cs | 23 +++++++++++------------ parser/chu/SusParser.cs | 15 ++++++++------- parser/chu/UgcParser.cs | 29 ++++++++++++----------------- tests/chu/ChuTests.cs | 6 ++---- 10 files changed, 56 insertions(+), 68 deletions(-) diff --git a/chart/chu/C2sChart.cs b/chart/chu/C2sChart.cs index ee27eae..77801ee 100644 --- a/chart/chu/C2sChart.cs +++ b/chart/chu/C2sChart.cs @@ -15,6 +15,5 @@ public class C2sChart : BaseChart, IChuChart public string DisplayLevel { get; set; } = ""; // 显示等级,字符串 public decimal Level { get; set; } // 定数,小数 public string MusicId { get; set; } = "0"; - public int Resolution { get; set; } = 384; public List<(Rational Time, Rational Duration, decimal Multiplier)> SflList = []; // 所有变速声明构成的列表。 } diff --git a/chart/chu/SusChart.cs b/chart/chu/SusChart.cs index a3bd6b2..35f8625 100644 --- a/chart/chu/SusChart.cs +++ b/chart/chu/SusChart.cs @@ -10,5 +10,4 @@ public class SusChart : BaseChart, IChuChart public string Title { get; set; } = ""; public string Artist { get; set; } = ""; public string Designer { get; set; } = ""; - public int TicksPerBeat { get; set; } = 480; } diff --git a/chart/chu/UgcChart.cs b/chart/chu/UgcChart.cs index 908b9d3..0e35b08 100644 --- a/chart/chu/UgcChart.cs +++ b/chart/chu/UgcChart.cs @@ -15,6 +15,5 @@ public class UgcChart : BaseChart, IChuChart public string DisplayLevel = ""; public decimal Level { get; set; } public string MusicId { get; set; } = ""; - public int TicksPerBeat { get; set; } = 480; public List<(Rational Time, Rational Duration, decimal Multiplier)> SflList = []; // 所有变速声明构成的列表。 } diff --git a/generator/chu/C2sGenerator.cs b/generator/chu/C2sGenerator.cs index fa7b0db..d31f0b9 100644 --- a/generator/chu/C2sGenerator.cs +++ b/generator/chu/C2sGenerator.cs @@ -13,6 +13,8 @@ namespace MuConvert.chu; */ public class C2sGenerator : IGenerator { + private const int RSL = 384; + public (string, List) Generate(IChuChart chart) { var alerts = new List(); @@ -68,36 +70,35 @@ private static string Serialize(C2sChart chart) var bpm_def = chart.BpmList.BPM_DEF(); sb.AppendLine($"BPM_DEF\t{bpm_def.Item1}\t{bpm_def.Item2}\t{bpm_def.Item3}\t{bpm_def.Item4}"); sb.AppendLine("MET_DEF\t4\t4"); - sb.AppendLine($"RESOLUTION\t{chart.Resolution}"); - sb.AppendLine($"CLK_DEF\t{chart.Resolution}"); + sb.AppendLine($"RESOLUTION\t{RSL}"); + sb.AppendLine($"CLK_DEF\t{RSL}"); sb.AppendLine("PROGJUDGE_BPM\t240.000"); sb.AppendLine("PROGJUDGE_AER\t0.999"); sb.AppendLine("TUTORIAL\t0"); sb.AppendLine(); - var res = chart.Resolution; foreach (var b in chart.BpmList) { - var (m, o) = Utils.BarAndTick(b.Time, res); + var (m, o) = Utils.BarAndTick(b.Time, RSL); sb.AppendLine($"BPM\t{m}\t{o}\t{b.Bpm:0.000}"); } foreach (var met in chart.MetList) { - var (m, o) = Utils.BarAndTick(met.Time, res); + var (m, o) = Utils.BarAndTick(met.Time, RSL); sb.AppendLine($"MET\t{m}\t{o}\t{met.Denominator}\t{met.Numerator}"); } foreach (var s in chart.SflList.OrderBy(s => s.Time)) { - var (m, o) = Utils.BarAndTick(s.Time, res); - var durTicks = Utils.Tick(s.Duration, res); + var (m, o) = Utils.BarAndTick(s.Time, RSL); + var durTicks = Utils.Tick(s.Duration, RSL); sb.AppendLine($"SFL\t{m}\t{o}\t{durTicks}\t{s.Multiplier:0.000000}"); } sb.AppendLine(); foreach (var n in chart.Notes) - sb.AppendLine(FormatNote(n, chart.Resolution)); + sb.AppendLine(FormatNote(n, RSL)); sb.AppendLine(); return sb.ToString(); diff --git a/generator/chu/SusGenerator.cs b/generator/chu/SusGenerator.cs index d1d2696..f94ab3e 100644 --- a/generator/chu/SusGenerator.cs +++ b/generator/chu/SusGenerator.cs @@ -12,7 +12,7 @@ namespace MuConvert.chu; */ public class SusGenerator : IGenerator { - private const int SusTpb = 480; + private static int RSL = 480 * 4; public (string, List) Generate(IChuChart chart) { @@ -32,7 +32,7 @@ private static SusChart ConvertToSus(IChuChart chart, List alerts) if (chart is C2sChart c2s) { bpm = c2s.BpmList.Count > 0 ? (double)c2s.BpmList[0].Bpm : 120.0; - var result = new SusChart { TicksPerBeat = SusTpb, Title = title, Artist = artist }; + var result = new SusChart { Title = title, Artist = artist }; result.BpmList.Add(new BPM(0, (decimal)bpm)); result.Notes = c2s.Notes; return result; @@ -41,7 +41,7 @@ private static SusChart ConvertToSus(IChuChart chart, List alerts) if (chart is UgcChart ugc) { bpm = ugc.BpmList.Count > 0 ? (double)ugc.BpmList[0].Bpm : 120.0; - var result = new SusChart { TicksPerBeat = SusTpb, Title = ugc.Title, Artist = ugc.Artist }; + var result = new SusChart { Title = ugc.Title, Artist = ugc.Artist }; result.BpmList.Add(new BPM(0, (decimal)bpm)); result.Notes = ugc.Notes; return result; @@ -60,14 +60,13 @@ private static string Serialize(SusChart sus) if (!string.IsNullOrEmpty(sus.Artist)) sb.AppendLine($"#ARTIST \"{sus.Artist}\""); if (!string.IsNullOrEmpty(sus.Designer)) sb.AppendLine($"#DESIGNER \"{sus.Designer}\""); sb.AppendLine($"#BPM_DEF {sus.StartBpm:F2}"); - sb.AppendLine($"#REQUEST \"{sus.TicksPerBeat}\""); + sb.AppendLine($"#REQUEST \"{RSL / 4}\""); sb.AppendLine(); - var tpm = sus.TicksPerBeat * 4; foreach (var n in sus.Notes) { - var (m, o) = Utils.BarAndTick(n.Time, tpm, 0); - sb.AppendLine($"#{m:X2}{o:X3}:{FormatData(n, tpm)}"); + var (m, o) = Utils.BarAndTick(n.Time, RSL); + sb.AppendLine($"#{m:X2}{o:X3}:{FormatData(n, RSL)}"); } return sb.ToString(); diff --git a/generator/chu/UgcGenerator.cs b/generator/chu/UgcGenerator.cs index a111a72..23cfd4c 100644 --- a/generator/chu/UgcGenerator.cs +++ b/generator/chu/UgcGenerator.cs @@ -11,7 +11,7 @@ namespace MuConvert.chu; */ public class UgcGenerator : IGenerator { - private const int UgcTicksPerBeat = 480; + private static int RSL = 480 * 4; public (string, List) Generate(IChuChart chart) { @@ -29,7 +29,6 @@ private static UgcChart ConvertToUgc(IChuChart chart, List alerts) { var result = new UgcChart { - TicksPerBeat = UgcTicksPerBeat, Designer = c2s.Designer, Difficulty = c2s.Difficulty, MusicId = c2s.MusicId, @@ -58,23 +57,22 @@ private static string Serialize(UgcChart ugc) sb.AppendLine($"@LEVEL\t{ugc.DisplayLevel}"); sb.AppendLine($"@CONST\t{ugc.Level:F5}"); sb.AppendLine($"@SONGID\t{ugc.MusicId}"); - sb.AppendLine($"@TICKS\t{ugc.TicksPerBeat}"); - var tpm = ugc.TicksPerBeat * 4; + sb.AppendLine($"@TICKS\t{RSL / 4}"); foreach (var met in ugc.MetList) { - var (m, _) = Utils.BarAndTick(met.Time, tpm); + var (m, _) = Utils.BarAndTick(met.Time, RSL); sb.AppendLine($"@BEAT\t{m}\t{met.Numerator}\t{met.Denominator}"); } foreach (var b in ugc.BpmList) { - var (m, o) = Utils.BarAndTick(b.Time, tpm); + var (m, o) = Utils.BarAndTick(b.Time, RSL); sb.AppendLine($"@BPM\t{m}'{o}\t{b.Bpm:F5}"); } sb.AppendLine("@TIL\t0\t0'0\t1.00000"); foreach (var s in ugc.SflList.OrderBy(x => x.Time)) { - var (m, o) = Utils.BarAndTick(s.Time, tpm); + var (m, o) = Utils.BarAndTick(s.Time, RSL); sb.AppendLine($"@SPDMOD\t{m}'{o}\t{s.Multiplier:0.00000}"); } @@ -84,10 +82,10 @@ private static string Serialize(UgcChart ugc) foreach (var n in ugc.Notes) { - var (m, o) = Utils.BarAndTick(n.Time, tpm); - sb.Append($"#{m}'{o}:{UCode(n, tpm)}"); + var (m, o) = Utils.BarAndTick(n.Time, RSL); + sb.Append($"#{m}'{o}:{UCode(n, RSL)}"); sb.AppendLine(); - var durTicks = Utils.Tick(n.Duration, tpm); + var durTicks = Utils.Tick(n.Duration, RSL); if (n.Type == "HLD" && durTicks > 0) sb.AppendLine($"#{durTicks}>s"); else if (n.Type == "SLD" && durTicks > 0) diff --git a/parser/chu/C2sParser.cs b/parser/chu/C2sParser.cs index 3f14ea4..4335033 100644 --- a/parser/chu/C2sParser.cs +++ b/parser/chu/C2sParser.cs @@ -13,6 +13,7 @@ namespace MuConvert.chu; */ public class C2sParser : IParser { + private static int RSL = 384; private static readonly HashSet HeadTags = new(StringComparer.OrdinalIgnoreCase) { "VERSION", "MUSIC", "SEQUENCEID", "DIFFICULT", "LEVEL", "CREATOR", "BPM_DEF", "MET_DEF", "RESOLUTION", "CLK_DEF", "PROGJUDGE_BPM", "PROGJUDGE_AER", "TUTORIAL" }; private static readonly HashSet TimingTags = new(StringComparer.OrdinalIgnoreCase) @@ -61,26 +62,25 @@ private static void ParseHeader(string[] p, C2sChart chart) case "MUSIC": chart.MusicId = Int(p, 1).ToString(); break; case "DIFFICULT": chart.Difficulty = Int(p, 1); break; case "CREATOR": chart.Designer = Str(p, 1); break; - case "RESOLUTION": chart.Resolution = Math.Max(1, Int(p, 1, 384)); break; + case "RESOLUTION": RSL = Math.Max(1, Int(p, 1, 384)); break; } } private static void ParseTiming(string[] p, C2sChart chart) { var tag = p[0].ToUpperInvariant(); - var tpm = chart.Resolution; switch (tag) { case "BPM": - chart.BpmList.Add(new BPM(Int(p, 1) + new Rational(Int(p, 2), tpm), decimal.Parse(p[3]))); + chart.BpmList.Add(new BPM(Int(p, 1) + new Rational(Int(p, 2), RSL), decimal.Parse(p[3]))); break; case "MET": - chart.MetList.Add(new MET(Int(p, 1) + new Rational(Int(p, 2), tpm), Int(p, 4, 4), Int(p, 3, 4))); + chart.MetList.Add(new MET(Int(p, 1) + new Rational(Int(p, 2), RSL), Int(p, 4, 4), Int(p, 3, 4))); break; case "SFL": chart.SflList.Add(( - Int(p, 1) + new Rational(Int(p, 2), tpm), - new Rational(Int(p, 3), tpm), + Int(p, 1) + new Rational(Int(p, 2), RSL), + new Rational(Int(p, 3), RSL), decimal.Parse(p[4]))); break; } @@ -89,8 +89,7 @@ private static void ParseTiming(string[] p, C2sChart chart) private static void ParseNote(string[] p, C2sChart chart, List alerts, int lineNum) { var tag = p[0].ToUpperInvariant(); - var tpm = chart.Resolution; - var note = new ChuNote { Type = tag, Time = Int(p, 1) + new Rational(Int(p, 2), tpm) }; + var note = new ChuNote { Type = tag, Time = Int(p, 1) + new Rational(Int(p, 2), RSL) }; switch (tag) { @@ -99,10 +98,10 @@ private static void ParseNote(string[] p, C2sChart chart, List alerts, in case "CHR": note.Cell = Int(p, 3); note.Width = Math.Max(1, Int(p, 4, 1)); note.Tag = Str(p, 5); break; case "HLD": case "HXD": - note.Cell = Int(p, 3); note.Width = Math.Max(1, Int(p, 4, 1)); note.Duration = new Rational(Int(p, 5), tpm); break; + note.Cell = Int(p, 3); note.Width = Math.Max(1, Int(p, 4, 1)); note.Duration = new Rational(Int(p, 5), RSL); break; case "SLD": case "SLC": case "SXD": case "SXC": note.Cell = Int(p, 3); note.Width = Math.Max(1, Int(p, 4, 1)); - note.Duration = new Rational(Int(p, 5), tpm); + note.Duration = new Rational(Int(p, 5), RSL); note.EndCell = Int(p, 6); note.EndWidth = Math.Max(1, Int(p, 7, 1)); break; case "FLK": @@ -113,7 +112,7 @@ private static void ParseNote(string[] p, C2sChart chart, List alerts, in break; case "AHD": case "AHX": note.Cell = Int(p, 3); note.Width = Math.Max(1, Int(p, 4, 1)); - note.TargetNote = Str(p, 5); note.Duration = new Rational(Int(p, 6), tpm); + note.TargetNote = Str(p, 5); note.Duration = new Rational(Int(p, 6), RSL); if (p.Length >= 8) note.Tag = Str(p, 7); break; case "ASD": case "ASC": @@ -126,7 +125,7 @@ private static void ParseNote(string[] p, C2sChart chart, List alerts, in note.Cell = Int(p, 3); note.Width = Math.Max(1, Int(p, 4, 1)); note.TargetNote = Str(p, 5); note.ExtraData = [Int(p, 6), Int(p, 10)]; - note.Duration = new Rational(Int(p, 7), tpm); + note.Duration = new Rational(Int(p, 7), RSL); note.EndCell = Int(p, 8); note.EndWidth = Math.Max(1, Int(p, 9, 1)); note.Tag = Str(p, 11); break; diff --git a/parser/chu/SusParser.cs b/parser/chu/SusParser.cs index 5ea3e49..3fded72 100644 --- a/parser/chu/SusParser.cs +++ b/parser/chu/SusParser.cs @@ -13,6 +13,8 @@ namespace MuConvert.chu; */ public class SusParser : IParser { + private static int RSL = 480 * 4; + private static readonly Dictionary TypeMap = new() { [0x01] = "TAP", @@ -93,7 +95,7 @@ private static void ParseHeaderLine(string content, SusChart chart, List { var reqStr = content[8..].Trim().Trim('"'); if (int.TryParse(reqStr, NumberStyles.Integer, CultureInfo.InvariantCulture, out var ticks)) - chart.TicksPerBeat = ticks; + RSL = ticks * 4; else alerts.Add(new Alert(Warning, $"REQUEST 格式错误: {content}") { Line = lineNum }); } @@ -119,7 +121,6 @@ private static void ParseNoteLine(string content, SusChart chart, List al var measure = HexToInt(timingStr[..2]); var tick = HexToInt(timingStr[2..5]); - var tpm = chart.TicksPerBeat * 4; if (dataStr.Length < 6) { @@ -140,7 +141,7 @@ private static void ParseNoteLine(string content, SusChart chart, List al var note = new ChuNote { Type = typeName, - Time = measure + new Rational(tick, tpm), + Time = measure + new Rational(tick, RSL), Cell = lane / 2, Width = Math.Max(1, width / 2), }; @@ -154,20 +155,20 @@ private static void ParseNoteLine(string content, SusChart chart, List al break; case "HLD": - ParseHoldData(dataStr, note, tpm, alerts, lineNum); + ParseHoldData(dataStr, note, RSL, alerts, lineNum); break; case "SLD": - ParseSlideData(dataStr, note, tpm, alerts, lineNum); + ParseSlideData(dataStr, note, RSL, alerts, lineNum); break; case "AIR": case "ADW": - ParseAirTarget(dataStr, note, tpm, alerts, lineNum); + ParseAirTarget(dataStr, note, RSL, alerts, lineNum); break; case "AHD": - ParseAhdData(dataStr, note, tpm, alerts, lineNum); + ParseAhdData(dataStr, note, RSL, alerts, lineNum); break; } diff --git a/parser/chu/UgcParser.cs b/parser/chu/UgcParser.cs index 4bcb92d..7eb4302 100644 --- a/parser/chu/UgcParser.cs +++ b/parser/chu/UgcParser.cs @@ -13,6 +13,8 @@ namespace MuConvert.chu; */ public class UgcParser : IParser { + private static int RSL = 480 * 4; + private static readonly Dictionary AirDirections = new() { ["UC"] = "AIR", @@ -144,7 +146,7 @@ private static void ParseHeaderLine(string line, UgcChart chart, List ale case "@TICKS": if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var ticks)) - chart.TicksPerBeat = ticks; + RSL = ticks * 4; else alerts.Add(new Alert(Warning, $"@TICKS 格式错误: {line}") { Line = lineNum }); break; @@ -174,8 +176,7 @@ private static void ParseHeaderLine(string line, UgcChart chart, List ale if (TryParseUgcMeasureTick(measureOffset, out var bpmMeasure, out var bpmOffset) && decimal.TryParse(bpmValueStr, NumberStyles.Float, CultureInfo.InvariantCulture, out var bpmValue)) { - var tpm = chart.TicksPerBeat * 4; - chart.BpmList.Add(new BPM(bpmMeasure + new Rational(bpmOffset, tpm), bpmValue)); + chart.BpmList.Add(new BPM(bpmMeasure + new Rational(bpmOffset, RSL), bpmValue)); } else { @@ -202,8 +203,7 @@ private static void ParseHeaderLine(string line, UgcChart chart, List ale && TryParseUgcMeasureTick(parts[0], out var meas, out var tick) && decimal.TryParse(parts[1], NumberStyles.Float, CultureInfo.InvariantCulture, out var mult)) { - var tpm = chart.TicksPerBeat * 4; - chart.SflList.Add((meas + new Rational(tick, tpm), Rational.Zero, mult)); + chart.SflList.Add((meas + new Rational(tick, RSL), Rational.Zero, mult)); } else alerts.Add(new Alert(Warning, $"@SPDMOD 格式错误: {line}") { Line = lineNum }); @@ -271,10 +271,9 @@ private static int ParseNoteLine(string[] lines, int idx, UgcChart chart, List alerts, private static int ParseHoldNote(string[] lines, int idx, string code, ChuNote note, List alerts, UgcChart chart) { note.Type = "HLD"; - var tpm = chart.TicksPerBeat * 4; ParseCellWidth(code, 1, note, alerts, idx + 1, chart); bool foundFirst = false; @@ -347,7 +345,7 @@ private static int ParseHoldNote(string[] lines, int idx, string code, ChuNote n break; } - note.Duration += new Rational(duration, tpm); + note.Duration += new Rational(duration, RSL); idx++; foundFirst = true; } @@ -360,7 +358,6 @@ private static int ParseHoldNote(string[] lines, int idx, string code, ChuNote n private static int ParseSlideNote(string[] lines, int idx, string code, ChuNote note, List alerts, UgcChart chart) { note.Type = "SLD"; - var tpm = chart.TicksPerBeat * 4; ParseCellWidth(code, 1, note, alerts, idx + 1, chart); bool foundFirst = false; @@ -373,7 +370,7 @@ private static int ParseSlideNote(string[] lines, int idx, string code, ChuNote break; } - note.Duration += new Rational(duration, tpm); + note.Duration += new Rational(duration, RSL); note.EndCell = endCell; note.EndWidth = endWidth; idx++; @@ -394,7 +391,6 @@ private static bool TryParseStandaloneFollower(string[] lines, int idx, UgcChart if (!TryParseFollowerLine(line, out var duration, out var endCell, out var endWidth)) return false; // find the last SLD or HLD note and attach duration - var tpm = chart.TicksPerBeat * 4; for (int i = chart.Notes.Count - 1; i >= 0; i--) { var n = chart.Notes[i]; @@ -402,11 +398,11 @@ private static bool TryParseStandaloneFollower(string[] lines, int idx, UgcChart { if (n.Type == "SLD") { - n.Duration = new Rational(duration, tpm); + n.Duration = new Rational(duration, RSL); n.EndCell = endCell; n.EndWidth = endWidth; } - else n.Duration = new Rational(duration, tpm); + else n.Duration = new Rational(duration, RSL); return true; } } @@ -497,7 +493,7 @@ private static void ParseAirNote(string code, ChuNote note, List alerts, { var durStr = afterCellWidth[(underscoreIdx + 1)..]; if (int.TryParse(durStr, NumberStyles.Integer, CultureInfo.InvariantCulture, out var ahdDuration)) - note.Duration = new Rational(ahdDuration, chart.TicksPerBeat * 4); + note.Duration = new Rational(ahdDuration, RSL); } } @@ -542,8 +538,7 @@ private static int WidthHexCharToInt(char c) private static string FormatNoteRef(ChuNote note, UgcChart chart) { - var tpm = chart.TicksPerBeat * 4; - var (m, o) = Utils.BarAndTick(note.Time, tpm, 0); + var (m, o) = Utils.BarAndTick(note.Time, RSL, 0); return $"#{m}'{o}:{note.Type}"; } } diff --git a/tests/chu/ChuTests.cs b/tests/chu/ChuTests.cs index c6327e6..7827750 100644 --- a/tests/chu/ChuTests.cs +++ b/tests/chu/ChuTests.cs @@ -19,7 +19,6 @@ public void CanParseOfficialC2S() if (!File.Exists(C2sPath)) throw new SkipException($"Missing: {C2sPath}"); var (chart, _) = new C2sParser().Parse(File.ReadAllText(C2sPath)); Assert.NotEmpty(chart.Notes); - Assert.Equal(384, chart.Resolution); } [Fact] @@ -113,11 +112,10 @@ private static ChuNote C2sNoteScaledToUgcTicks(ChuNote n, int ugcTicksPerBeat, i /// private static void AssertUgcNotesEquivalentToReparsedC2s(UgcChart ugc, C2sChart c2s, bool isUgcReference) { - var res = c2s.Resolution; if (isUgcReference) { var ugcSnaps = ugc.Notes - .Select(n => SnapshotNote(UgcNoteScaledToC2sTicks(n, ugc.TicksPerBeat, res))) + .Select(n => SnapshotNote(UgcNoteScaledToC2sTicks(n, 480, 384))) .OrderBy(s => s) .ToArray(); var c2sSnaps = c2s.Notes.Select(SnapshotNote).OrderBy(s => s).ToArray(); @@ -127,7 +125,7 @@ private static void AssertUgcNotesEquivalentToReparsedC2s(UgcChart ugc, C2sChart { var ugcSnaps = ugc.Notes.Select(SnapshotNote).OrderBy(s => s).ToArray(); var c2sSnaps = c2s.Notes - .Select(n => SnapshotNote(C2sNoteScaledToUgcTicks(n, ugc.TicksPerBeat, res))) + .Select(n => SnapshotNote(C2sNoteScaledToUgcTicks(n, 480, 384))) .OrderBy(s => s) .ToArray(); Assert.Equal(c2sSnaps, ugcSnaps); From d92a9e39d0eeeec05c04db0f7b07d80b2a9858f3 Mon Sep 17 00:00:00 2001 From: Starrah Date: Mon, 4 May 2026 01:32:18 +0800 Subject: [PATCH 07/13] =?UTF-8?q?[R]=20Chart=E7=9B=B8=E5=85=B3=E9=87=8D?= =?UTF-8?q?=E6=9E=84=EF=BC=88=E6=9C=80=E5=90=8E=E4=B8=80=E6=AD=A5=EF=BC=89?= =?UTF-8?q?=E2=80=94=E2=80=94=E4=B8=89=E7=A7=8DChart=E5=90=88=E5=B9=B6?= =?UTF-8?q?=E4=B8=BA=E7=BB=9F=E4=B8=80=E7=9A=84ChuChart=EF=BC=81=20?= =?UTF-8?q?=E5=AE=9E=E9=99=85=E4=B8=8A=EF=BC=8C=E7=BB=8F=E8=BF=87=E5=89=8D?= =?UTF-8?q?=E5=87=A0=E6=AD=A5=E4=B9=8B=E5=90=8E=EF=BC=8C=E5=90=84=E7=A7=8D?= =?UTF-8?q?Chart=E7=9A=84=E5=86=85=E5=AE=B9=EF=BC=88=E6=97=A0=E8=AE=BA?= =?UTF-8?q?=E6=98=AF=E5=AD=97=E6=AE=B5=E8=BF=98=E6=98=AF=E7=B1=BB=E5=9E=8B?= =?UTF-8?q?=EF=BC=89=E9=83=BD=E5=AE=9E=E7=8E=B0=E4=BA=86=E5=AE=8C=E5=85=A8?= =?UTF-8?q?=E7=BB=9F=E4=B8=80=E4=BA=86=E3=80=82=E6=89=80=E4=BB=A5=E8=BF=99?= =?UTF-8?q?=E9=87=8C=E5=8F=AA=E9=9C=80=E8=A6=81=E4=BF=AE=E6=94=B9=E7=B1=BB?= =?UTF-8?q?=E5=90=8D=E5=B0=B1=E5=8F=AF=E4=BB=A5=E4=BA=86=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Program.cs | 2 +- README.md | 10 +--- chart/chu/{C2sChart.cs => ChuChart.cs} | 5 +- chart/chu/IChuChart.cs | 8 ---- chart/chu/SusChart.cs | 13 ----- chart/chu/UgcChart.cs | 19 -------- generator/chu/C2sGenerator.cs | 48 ++----------------- generator/chu/SusGenerator.cs | 44 ++--------------- generator/chu/UgcGenerator.cs | 37 ++------------- parser/chu/C2sParser.cs | 12 ++--- parser/chu/SusParser.cs | 12 ++--- parser/chu/UgcParser.cs | 66 ++++++++------------------ tests/chu/ChuTests.cs | 10 ++-- 13 files changed, 52 insertions(+), 234 deletions(-) rename chart/chu/{C2sChart.cs => ChuChart.cs} (84%) delete mode 100644 chart/chu/IChuChart.cs delete mode 100644 chart/chu/SusChart.cs delete mode 100644 chart/chu/UgcChart.cs diff --git a/Program.cs b/Program.cs index 7cb11ee..b91b39e 100644 --- a/Program.cs +++ b/Program.cs @@ -474,7 +474,7 @@ private static void RunConvertChuSingleFile(string filePath, string inputKind) var destNote = _outputSpec.Kind == OutputSinkKind.Stdout ? "(标准输出)" : outPath; Console.Error.WriteLine($"{inputKind.ToUpperInvariant()} → {targetFormat.ToUpperInvariant()}: {full} → {destNote}"); - IChuChart chart; + ChuChart chart; List parseAlerts; switch (inputKind) { diff --git a/README.md b/README.md index 76dfb53..e8cf80b 100644 --- a/README.md +++ b/README.md @@ -215,13 +215,7 @@ finally - **parser(解析器)**:把“源格式文本”解析成中间表示 - `SimaiParser.Parse(string)` → `MaiChart` - `MA2Parser.Parse(string)` → `MaiChart` - - CHUNITHM的三种Parser(`C2sParser`、`UgcParser`、`SusParser`):`Parse(string)` → `IChuChart` - >
- > 关于IChuChart - > 当前实现IChuChart是一个通用的接口而非具体的类型,这是因为目前不同Parser解析出的谱面的IR尚未能够完全统一,所以只能都各自继承自IChuChart。 - > 不过不用担心,任意的Generator都接受任意IChuChart对象,因此你可以不在意它们之间的差异,直接拿来用就行了。 - > 未来如果有机会的话,我们会把它们进一步统一成同一个具体类型的IR,以进一步提升代码的可维护性和可读性。 - >
+ - CHUNITHM的三种Parser(`C2sParser`、`UgcParser`、`SusParser`):`Parse(string)` → `ChuChart` - 解析成功时,**返回值会同时带有 `List`**,这是转谱过程中可能遇到的警告等信息,建议打印出来(直接对`Alert`对象`ToString()`即可)。 - 如果解析失败,会抛出 `ConversionException`;该异常对象中同样含有一个 `List`,是导致转谱失败的错误信息,可以同上打印出来。 @@ -232,7 +226,7 @@ finally - **generator(生成器)**:把中间表示转回“目标格式文本” - `SimaiGenerator.Generate(MaiChart)` → Simai 单谱文本(可写入 `maidata.txt` 的 `&inote_*`) - `MA2Generator.Generate(MaiChart)` → MA2 文本 - - CHUNITHM的三种Generator(`C2sGenerator`、`UgcGenerator`、`SusGenerator`):`Generate(IChuChart)` → 目标格式的谱面文本 + - CHUNITHM的三种Generator(`C2sGenerator`、`UgcGenerator`、`SusGenerator`):`Generate(ChuChart)` → 目标格式的谱面文本 - 与parser类似,成功生成时,**返回值会同时带有 `List`**,这是转谱过程中可能遇到的警告等信息,建议打印出来(直接对`Alert`对象`ToString()`即可)。 - 如果生成失败,会抛出 `ConversionException`;该异常对象中同样含有一个 `List`,是导致转谱失败的错误信息,可以同上打印出来。 diff --git a/chart/chu/C2sChart.cs b/chart/chu/ChuChart.cs similarity index 84% rename from chart/chu/C2sChart.cs rename to chart/chu/ChuChart.cs index 77801ee..26624db 100644 --- a/chart/chu/C2sChart.cs +++ b/chart/chu/ChuChart.cs @@ -3,10 +3,7 @@ namespace MuConvert.chu; -/** - * C2S 格式谱面 IR(官方格式,RESOLUTION=384 tick/小节)。 - */ -public class C2sChart : BaseChart, IChuChart +public class ChuChart : BaseChart { public string Title { get; set; } = ""; public string Artist { get; set; } = ""; diff --git a/chart/chu/IChuChart.cs b/chart/chu/IChuChart.cs deleted file mode 100644 index d632bfd..0000000 --- a/chart/chu/IChuChart.cs +++ /dev/null @@ -1,8 +0,0 @@ -using MuConvert.chart; - -namespace MuConvert.chu; - -/** - * CHUNITHM 所有谱面格式的统一接口,作为 Generator 的输入类型。 - */ -public interface IChuChart : IBaseChart; diff --git a/chart/chu/SusChart.cs b/chart/chu/SusChart.cs deleted file mode 100644 index 35f8625..0000000 --- a/chart/chu/SusChart.cs +++ /dev/null @@ -1,13 +0,0 @@ -using MuConvert.chart; - -namespace MuConvert.chu; - -/** - * SUS 格式谱面 IR(REQUEST=480 tick/拍,lane 0–31)。 - */ -public class SusChart : BaseChart, IChuChart -{ - public string Title { get; set; } = ""; - public string Artist { get; set; } = ""; - public string Designer { get; set; } = ""; -} diff --git a/chart/chu/UgcChart.cs b/chart/chu/UgcChart.cs deleted file mode 100644 index 0e35b08..0000000 --- a/chart/chu/UgcChart.cs +++ /dev/null @@ -1,19 +0,0 @@ -using MuConvert.chart; -using Rationals; - -namespace MuConvert.chu; - -/** - * UGC 格式谱面 IR(UMIGURI 格式,@TICKS=480 tick/拍)。 - */ -public class UgcChart : BaseChart, IChuChart -{ - public string Title { get; set; } = ""; - public string Artist { get; set; } = ""; - public string Designer { get; set; } = ""; - public int Difficulty { get; set; } = 3; - public string DisplayLevel = ""; - public decimal Level { get; set; } - public string MusicId { get; set; } = ""; - public List<(Rational Time, Rational Duration, decimal Multiplier)> SflList = []; // 所有变速声明构成的列表。 -} diff --git a/generator/chu/C2sGenerator.cs b/generator/chu/C2sGenerator.cs index d31f0b9..f665f95 100644 --- a/generator/chu/C2sGenerator.cs +++ b/generator/chu/C2sGenerator.cs @@ -1,61 +1,21 @@ -using System.Globalization; using System.Text; -using MuConvert.chart; using MuConvert.generator; using MuConvert.utils; -using static MuConvert.utils.Alert.LEVEL; namespace MuConvert.chu; -/** - * C2S 格式生成器。 - * 输入 IChuChart,内部自动转换后输出 C2S 文本。 - */ -public class C2sGenerator : IGenerator +public class C2sGenerator : IGenerator { private const int RSL = 384; - public (string, List) Generate(IChuChart chart) + public (string, List) Generate(ChuChart chart) { var alerts = new List(); - var c2s = ConvertToC2s(chart, alerts); - var text = Serialize(c2s); + var text = Serialize(chart); return (text, alerts); } - private static C2sChart ConvertToC2s(IChuChart chart, List alerts) - { - if (chart is C2sChart c2s) return c2s; - - if (chart is UgcChart ugc) - { - var result = new C2sChart - { - Designer = ugc.Designer, - }; - result.BpmList.AddRange(ugc.BpmList); - result.MetList.AddRange(ugc.MetList); - result.SflList.AddRange(ugc.SflList); - result.Notes = ugc.Notes; - return result; - } - - if (chart is SusChart sus) - { - var result = new C2sChart(); - if (sus.BpmList.Count > 0) - result.BpmList.AddRange(sus.BpmList); - else - result.BpmList.Add(new BPM(0, 120m)); - result.Notes = sus.Notes; - return result; - } - - alerts.Add(new Alert(Error, string.Format(Locale.ChuGeneratorUnsupported, "→ C2S"))); - throw new ConversionException(alerts); - } - - private static string Serialize(C2sChart chart) + private static string Serialize(ChuChart chart) { chart.Sort(); diff --git a/generator/chu/SusGenerator.cs b/generator/chu/SusGenerator.cs index f94ab3e..e4871ca 100644 --- a/generator/chu/SusGenerator.cs +++ b/generator/chu/SusGenerator.cs @@ -1,57 +1,21 @@ using System.Text; -using MuConvert.chart; using MuConvert.generator; using MuConvert.utils; -using static MuConvert.utils.Alert.LEVEL; namespace MuConvert.chu; -/** - * SUS 格式生成器。 - * 输入 IChuChart,内部自动转换后输出 SUS 文本。 - */ -public class SusGenerator : IGenerator +public class SusGenerator : IGenerator { private static int RSL = 480 * 4; - public (string, List) Generate(IChuChart chart) + public (string, List) Generate(ChuChart chart) { var alerts = new List(); - var sus = ConvertToSus(chart, alerts); - var text = Serialize(sus); + var text = Serialize(chart); return (text, alerts); } - private static SusChart ConvertToSus(IChuChart chart, List alerts) - { - if (chart is SusChart sus) return sus; - - double bpm = 120.0; - string title = "", artist = ""; - - if (chart is C2sChart c2s) - { - bpm = c2s.BpmList.Count > 0 ? (double)c2s.BpmList[0].Bpm : 120.0; - var result = new SusChart { Title = title, Artist = artist }; - result.BpmList.Add(new BPM(0, (decimal)bpm)); - result.Notes = c2s.Notes; - return result; - } - - if (chart is UgcChart ugc) - { - bpm = ugc.BpmList.Count > 0 ? (double)ugc.BpmList[0].Bpm : 120.0; - var result = new SusChart { Title = ugc.Title, Artist = ugc.Artist }; - result.BpmList.Add(new BPM(0, (decimal)bpm)); - result.Notes = ugc.Notes; - return result; - } - - alerts.Add(new Alert(Error, string.Format(Locale.ChuGeneratorUnsupported, "→ SUS"))); - throw new ConversionException(alerts); - } - - private static string Serialize(SusChart sus) + private static string Serialize(ChuChart sus) { sus.Sort(); diff --git a/generator/chu/UgcGenerator.cs b/generator/chu/UgcGenerator.cs index 23cfd4c..ea7400f 100644 --- a/generator/chu/UgcGenerator.cs +++ b/generator/chu/UgcGenerator.cs @@ -1,50 +1,21 @@ using System.Text; using MuConvert.generator; using MuConvert.utils; -using static MuConvert.utils.Alert.LEVEL; namespace MuConvert.chu; -/** - * UGC 格式生成器。 - * 输入 IChuChart,内部自动转换后输出 UGC 文本。 - */ -public class UgcGenerator : IGenerator +public class UgcGenerator : IGenerator { private static int RSL = 480 * 4; - public (string, List) Generate(IChuChart chart) + public (string, List) Generate(ChuChart chart) { var alerts = new List(); - var ugc = ConvertToUgc(chart, alerts); - var text = Serialize(ugc); + var text = Serialize(chart); return (text, alerts); } - private static UgcChart ConvertToUgc(IChuChart chart, List alerts) - { - if (chart is UgcChart ugc) return ugc; - - if (chart is C2sChart c2s) - { - var result = new UgcChart - { - Designer = c2s.Designer, - Difficulty = c2s.Difficulty, - MusicId = c2s.MusicId, - }; - result.BpmList.AddRange(c2s.BpmList); - result.MetList.AddRange(c2s.MetList); - result.SflList.AddRange(c2s.SflList); - result.Notes = c2s.Notes; - return result; - } - - alerts.Add(new Alert(Error, string.Format(Locale.ChuGeneratorUnsupported, "→ UGC"))); - throw new ConversionException(alerts); - } - - private static string Serialize(UgcChart ugc) + private static string Serialize(ChuChart ugc) { ugc.Sort(); diff --git a/parser/chu/C2sParser.cs b/parser/chu/C2sParser.cs index 4335033..5262684 100644 --- a/parser/chu/C2sParser.cs +++ b/parser/chu/C2sParser.cs @@ -11,7 +11,7 @@ namespace MuConvert.chu; * C2S 格式解析器(官方格式,RESOLUTION=384 tick/小节)。 * Tab 分隔文本,识别 HEADER / TIMING / NOTES 区段。 */ -public class C2sParser : IParser +public class C2sParser : IParser { private static int RSL = 384; private static readonly HashSet HeadTags = new(StringComparer.OrdinalIgnoreCase) @@ -19,9 +19,9 @@ public class C2sParser : IParser private static readonly HashSet TimingTags = new(StringComparer.OrdinalIgnoreCase) { "BPM", "MET", "SFL" }; - public (C2sChart, List) Parse(string text) + public (ChuChart, List) Parse(string text) { - var chart = new C2sChart(); + var chart = new ChuChart(); var alerts = new List(); var lines = text.Replace("\r\n", "\n").Split('\n'); bool inNotes = false; @@ -54,7 +54,7 @@ public class C2sParser : IParser return (chart, alerts); } - private static void ParseHeader(string[] p, C2sChart chart) + private static void ParseHeader(string[] p, ChuChart chart) { var tag = p[0].ToUpperInvariant(); switch (tag) @@ -66,7 +66,7 @@ private static void ParseHeader(string[] p, C2sChart chart) } } - private static void ParseTiming(string[] p, C2sChart chart) + private static void ParseTiming(string[] p, ChuChart chart) { var tag = p[0].ToUpperInvariant(); switch (tag) @@ -86,7 +86,7 @@ private static void ParseTiming(string[] p, C2sChart chart) } } - private static void ParseNote(string[] p, C2sChart chart, List alerts, int lineNum) + private static void ParseNote(string[] p, ChuChart chart, List alerts, int lineNum) { var tag = p[0].ToUpperInvariant(); var note = new ChuNote { Type = tag, Time = Int(p, 1) + new Rational(Int(p, 2), RSL) }; diff --git a/parser/chu/SusParser.cs b/parser/chu/SusParser.cs index 3fded72..67bd23b 100644 --- a/parser/chu/SusParser.cs +++ b/parser/chu/SusParser.cs @@ -11,7 +11,7 @@ namespace MuConvert.chu; * SUS 格式解析器(社区工具格式,REQUEST=480 tick/拍,lane 0–31)。 * #MMTT:data 十六进制编码音符。 */ -public class SusParser : IParser +public class SusParser : IParser { private static int RSL = 480 * 4; @@ -28,9 +28,9 @@ public class SusParser : IParser [0x10] = "MNE", }; - public (SusChart, List) Parse(string text) + public (ChuChart, List) Parse(string text) { - var chart = new SusChart(); + var chart = new ChuChart(); var alerts = new List(); var lines = text.Replace("\r\n", "\n").Split('\n'); @@ -69,7 +69,7 @@ private static bool IsHeaderLine(string content) || content.StartsWith("REQUEST "); } - private static void ParseHeaderLine(string content, SusChart chart, List alerts, int lineNum) + private static void ParseHeaderLine(string content, ChuChart chart, List alerts, int lineNum) { if (content.StartsWith("TITLE ")) { @@ -101,7 +101,7 @@ private static void ParseHeaderLine(string content, SusChart chart, List } } - private static void ParseNoteLine(string content, SusChart chart, List alerts, int lineNum) + private static void ParseNoteLine(string content, ChuChart chart, List alerts, int lineNum) { var colonIdx = content.IndexOf(':'); if (colonIdx < 0) @@ -243,7 +243,7 @@ private static string Unquote(string s) private static string FormatNoteRef(ChuNote note, int tpm) { - var (m, o) = Utils.BarAndTick(note.Time, tpm, 0); + var (m, o) = Utils.BarAndTick(note.Time, tpm); return $"#{m:X2}{o:X3}:{note.Type}"; } } diff --git a/parser/chu/UgcParser.cs b/parser/chu/UgcParser.cs index 7eb4302..76060c1 100644 --- a/parser/chu/UgcParser.cs +++ b/parser/chu/UgcParser.cs @@ -11,7 +11,7 @@ namespace MuConvert.chu; * UGC 格式解析器(UMIGURI 格式,@TICKS=480 tick/拍)。 * @HEADER 标签 + #measure'tick:code 音符格式。 */ -public class UgcParser : IParser +public class UgcParser : IParser { private static int RSL = 480 * 4; @@ -33,9 +33,9 @@ public class UgcParser : IParser ["C"] = "CE", }; - public (UgcChart, List) Parse(string text) + public (ChuChart, List) Parse(string text) { - var chart = new UgcChart(); + var chart = new ChuChart(); var alerts = new List(); var lines = text.Replace("\r\n", "\n").Split('\n'); var inHeader = true; @@ -67,7 +67,7 @@ public class UgcParser : IParser return (chart, alerts); } - private static void FinalizeUgcSflDurations(UgcChart chart) + private static void FinalizeUgcSflDurations(ChuChart chart) { if (chart.SflList.Count == 0) return; chart.SflList = chart.SflList.OrderBy(s => s.Time).ToList(); @@ -83,7 +83,7 @@ private static void FinalizeUgcSflDurations(UgcChart chart) chart.SflList = chart.SflList.Where(x => x.Multiplier != 1).ToList(); // 倍率为1的,没必要放进来的 } - private static void ParseHeaderLine(string line, UgcChart chart, List alerts, int lineNum) + private static void ParseHeaderLine(string line, ChuChart chart, List alerts, int lineNum) { if (!line.StartsWith('@')) { @@ -230,7 +230,7 @@ private static bool TryParseUgcMeasureTick(string measureTick, out int measure, && int.TryParse(measureTick[(ap + 1)..], NumberStyles.Integer, CultureInfo.InvariantCulture, out tick); } - private static int ParseNoteLine(string[] lines, int idx, UgcChart chart, List alerts) + private static int ParseNoteLine(string[] lines, int idx, ChuChart chart, List alerts) { var line = lines[idx]; var lineNum = idx + 1; @@ -324,13 +324,13 @@ private static int ParseNoteLine(string[] lines, int idx, UgcChart chart, List
alerts, int lineNum, UgcChart chart) + private static void ParseTapNote(string code, ChuNote note, List alerts, int lineNum, ChuChart chart) { note.Type = "TAP"; ParseCellWidth(code, 1, note, alerts, lineNum, chart); } - private static int ParseHoldNote(string[] lines, int idx, string code, ChuNote note, List alerts, UgcChart chart) + private static int ParseHoldNote(string[] lines, int idx, string code, ChuNote note, List alerts, ChuChart chart) { note.Type = "HLD"; ParseCellWidth(code, 1, note, alerts, idx + 1, chart); @@ -355,7 +355,7 @@ private static int ParseHoldNote(string[] lines, int idx, string code, ChuNote n return idx; } - private static int ParseSlideNote(string[] lines, int idx, string code, ChuNote note, List alerts, UgcChart chart) + private static int ParseSlideNote(string[] lines, int idx, string code, ChuNote note, List alerts, ChuChart chart) { note.Type = "SLD"; ParseCellWidth(code, 1, note, alerts, idx + 1, chart); @@ -382,33 +382,7 @@ private static int ParseSlideNote(string[] lines, int idx, string code, ChuNote return idx; } - - private static bool TryParseStandaloneFollower(string[] lines, int idx, UgcChart chart, List alerts) - { - var line = lines[idx]; - if (!line.StartsWith('#') || !line.Contains(">s") && !line.Contains(">c")) return false; - - if (!TryParseFollowerLine(line, out var duration, out var endCell, out var endWidth)) return false; - - // find the last SLD or HLD note and attach duration - for (int i = chart.Notes.Count - 1; i >= 0; i--) - { - var n = chart.Notes[i]; - if (n.Type is "SLD" or "HLD") - { - if (n.Type == "SLD") - { - n.Duration = new Rational(duration, RSL); - n.EndCell = endCell; - n.EndWidth = endWidth; - } - else n.Duration = new Rational(duration, RSL); - return true; - } - } - return false; - } - + private static bool TryParseFollowerLine(string line, out int duration, out int endCell, out int endWidth, bool requireEndCellWidth = false) { duration = 0; @@ -420,8 +394,8 @@ private static bool TryParseFollowerLine(string line, out int duration, out int // support both >s (SLD) and >c (SLC) follower lines int gtIdx = -1; int markerLen = 0; - if (line.Contains(">s")) { gtIdx = line.IndexOf(">s"); markerLen = 2; } - else if (line.Contains(">c")) { gtIdx = line.IndexOf(">c"); markerLen = 2; } + if (line.Contains(">s")) { gtIdx = line.IndexOf(">s", StringComparison.Ordinal); markerLen = 2; } + else if (line.Contains(">c")) { gtIdx = line.IndexOf(">c", StringComparison.Ordinal); markerLen = 2; } if (gtIdx < 1) return false; var durationStr = line[1..gtIdx]; @@ -438,7 +412,7 @@ private static bool TryParseFollowerLine(string line, out int duration, out int return true; } - private static void ParseCellWidth(string code, int startIdx, ChuNote note, List alerts, int lineNum, UgcChart chart) + private static void ParseCellWidth(string code, int startIdx, ChuNote note, List alerts, int lineNum, ChuChart chart) { if (code.Length > startIdx) { @@ -454,7 +428,7 @@ private static void ParseCellWidth(string code, int startIdx, ChuNote note, List } } - private static void ParseAirNote(string code, ChuNote note, List alerts, int lineNum, UgcChart chart) + private static void ParseAirNote(string code, ChuNote note, List alerts, int lineNum, ChuChart chart) { // Matches UgcGenerator: "a" + cell + width + two-letter direction + targetNote [ + "_" + airHoldDuration for AHD ] if (code.Length < 5) @@ -497,7 +471,7 @@ private static void ParseAirNote(string code, ChuNote note, List alerts, } } - private static void ParseChrNote(string code, ChuNote note, List alerts, int lineNum, UgcChart chart) + private static void ParseChrNote(string code, ChuNote note, List alerts, int lineNum, ChuChart chart) { note.Type = "CHR"; if (code.Length < 3) @@ -508,10 +482,7 @@ private static void ParseChrNote(string code, ChuNote note, List alerts, ParseCellWidth(code, 1, note, alerts, lineNum, chart); var extraRaw = code.Length > 3 ? code[3..] : ""; - if (ChrExtras.TryGetValue(extraRaw, out var chrDir)) - note.Tag = chrDir; - else - note.Tag = extraRaw; + note.Tag = ChrExtras.GetValueOrDefault(extraRaw, extraRaw); } private static int HexCharToInt(char c) @@ -536,9 +507,10 @@ private static int WidthHexCharToInt(char c) }; } - private static string FormatNoteRef(ChuNote note, UgcChart chart) + // ReSharper disable once UnusedParameter.Local + private static string FormatNoteRef(ChuNote note, ChuChart chart) { - var (m, o) = Utils.BarAndTick(note.Time, RSL, 0); + var (m, o) = Utils.BarAndTick(note.Time, RSL); return $"#{m}'{o}:{note.Type}"; } } diff --git a/tests/chu/ChuTests.cs b/tests/chu/ChuTests.cs index 7827750..d6a65e3 100644 --- a/tests/chu/ChuTests.cs +++ b/tests/chu/ChuTests.cs @@ -73,10 +73,10 @@ private static string SnapshotNote(ChuNote note) private static ChuNote UgcNoteScaledToC2sTicks(ChuNote n, int ugcTicksPerBeat, int c2sResolution) { var tpmUgc = ugcTicksPerBeat * 4; - var (m, oU) = Utils.BarAndTick(n.Time, tpmUgc, 0); + var (m, oU) = Utils.BarAndTick(n.Time, tpmUgc); var oC = (int)((long)oU * c2sResolution / tpmUgc); var time = m + new Rational(oC, c2sResolution); - var dur = new Rational(Utils.Tick(n.Duration, c2sResolution, 0), c2sResolution); + var dur = new Rational(Utils.Tick(n.Duration, c2sResolution), c2sResolution); return CloneChuNoteWithTiming(n, time, dur); } @@ -86,10 +86,10 @@ private static ChuNote UgcNoteScaledToC2sTicks(ChuNote n, int ugcTicksPerBeat, i private static ChuNote C2sNoteScaledToUgcTicks(ChuNote n, int ugcTicksPerBeat, int c2sResolution) { var tpmUgc = ugcTicksPerBeat * 4; - var (m, oC) = Utils.BarAndTick(n.Time, c2sResolution, 0); + var (m, oC) = Utils.BarAndTick(n.Time, c2sResolution); var oU = (int)((long)oC * tpmUgc / c2sResolution); var time = m + new Rational(oU, tpmUgc); - var dur = new Rational(Utils.Tick(n.Duration, tpmUgc, 0), tpmUgc); + var dur = new Rational(Utils.Tick(n.Duration, tpmUgc), tpmUgc); return CloneChuNoteWithTiming(n, time, dur); } @@ -110,7 +110,7 @@ private static ChuNote C2sNoteScaledToUgcTicks(ChuNote n, int ugcTicksPerBeat, i /// /// 比较 UGC 与 C2S 的音符 IR:因 tick 网格不同,在各自「经对方格式写回再解析」的量化意义下比较快照。 /// - private static void AssertUgcNotesEquivalentToReparsedC2s(UgcChart ugc, C2sChart c2s, bool isUgcReference) + private static void AssertUgcNotesEquivalentToReparsedC2s(ChuChart ugc, ChuChart c2s, bool isUgcReference) { if (isUgcReference) { From 54bc9f41e56206eceeeda5d9c4ba5c2f872b4e83 Mon Sep 17 00:00:00 2001 From: Starrah Date: Mon, 4 May 2026 01:35:07 +0800 Subject: [PATCH 08/13] =?UTF-8?q?[F]=20=E4=B8=8D=E8=A6=81=E5=B1=8F?= =?UTF-8?q?=E8=94=BD=E6=9C=AC=E5=BA=94=E5=87=BA=E7=8E=B0=E7=9A=84=E8=AD=A6?= =?UTF-8?q?=E5=91=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- parser/chu/UgcParser.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/parser/chu/UgcParser.cs b/parser/chu/UgcParser.cs index 76060c1..5aaeed6 100644 --- a/parser/chu/UgcParser.cs +++ b/parser/chu/UgcParser.cs @@ -239,10 +239,6 @@ private static int ParseNoteLine(string[] lines, int idx, ChuChart chart, Lists") || line.Contains(">c"))) - return idx; - var colonIdx = line.IndexOf(':'); if (colonIdx < 0) { From fa9dbcbe0664117f68f277af896f5c2752c3f3e7 Mon Sep 17 00:00:00 2001 From: Starrah Date: Wed, 6 May 2026 19:49:48 +0800 Subject: [PATCH 09/13] =?UTF-8?q?[F]=20UGCParser=E5=AF=B9Slide=E7=9A=84?= =?UTF-8?q?=E8=A7=A3=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- generator/chu/C2sGenerator.cs | 19 +++++--- parser/chu/UgcParser.cs | 82 ++++++++++++++++++----------------- 2 files changed, 57 insertions(+), 44 deletions(-) diff --git a/generator/chu/C2sGenerator.cs b/generator/chu/C2sGenerator.cs index f665f95..73e56cf 100644 --- a/generator/chu/C2sGenerator.cs +++ b/generator/chu/C2sGenerator.cs @@ -11,11 +11,11 @@ public class C2sGenerator : IGenerator public (string, List) Generate(ChuChart chart) { var alerts = new List(); - var text = Serialize(chart); + var text = Serialize(chart, alerts); return (text, alerts); } - private static string Serialize(ChuChart chart) + private static string Serialize(ChuChart chart, List alerts) { chart.Sort(); @@ -58,13 +58,16 @@ private static string Serialize(ChuChart chart) sb.AppendLine(); foreach (var n in chart.Notes) - sb.AppendLine(FormatNote(n, RSL)); + { + var line = FormatNote(n, RSL, alerts); + if (line != null) sb.AppendLine(line); + } sb.AppendLine(); return sb.ToString(); } - private static string FormatNote(ChuNote n, int tpm) + private static string? FormatNote(ChuNote n, int tpm, List alerts) { var (m, o) = Utils.BarAndTick(n.Time, tpm); var durTicks = Utils.Tick(n.Duration, tpm); @@ -86,8 +89,14 @@ private static string FormatNote(ChuNote n, int tpm) "ASD" or "ASC" => FormatAsdAsc(n, m, o, durTicks), "ALD" => FormatAld(n, m, o), "MNE" => $"MNE\t{m}\t{o}\t{n.Cell}\t{n.Width}", - _ => $"TAP\t{m}\t{o}\t{n.Cell}\t{n.Width}" + _ => alert(), }; + + string? alert() + { + alerts.Add(new Alert(Alert.LEVEL.Warning, Locale.C2SUnknownNoteType, n.Time)); + return null; + } } private static string FormatAsdAsc(ChuNote n, int m, int o, int durTicks) diff --git a/parser/chu/UgcParser.cs b/parser/chu/UgcParser.cs index 5aaeed6..e1840bf 100644 --- a/parser/chu/UgcParser.cs +++ b/parser/chu/UgcParser.cs @@ -267,7 +267,7 @@ private static int ParseNoteLine(string[] lines, int idx, ChuChart chart, List alerts, int lineNum, ChuChart chart) + private static void ParseTapNote(string code, ChuNote note, List alerts, int lineNum, ChuChart chart, bool isCHR) { note.Type = "TAP"; ParseCellWidth(code, 1, note, alerts, lineNum, chart); + if (isCHR) + { + note.Type = "CHR"; + var extraRaw = code.Length > 3 ? code[3..] : ""; + note.Tag = ChrExtras.GetValueOrDefault(extraRaw, extraRaw); + } } private static int ParseHoldNote(string[] lines, int idx, string code, ChuNote note, List alerts, ChuChart chart) @@ -335,7 +342,7 @@ private static int ParseHoldNote(string[] lines, int idx, string code, ChuNote n while (idx + 1 < lines.Length) { var nextLine = lines[idx + 1].Trim(); - if (!TryParseFollowerLine(nextLine, out var duration, out _, out _)) + if (!TryParseFollowerLine(nextLine, out _, out var duration, out _, out _)) { if (nextLine.StartsWith('\'') || nextLine.StartsWith('@')) { idx++; continue; } break; @@ -351,48 +358,59 @@ private static int ParseHoldNote(string[] lines, int idx, string code, ChuNote n return idx; } - private static int ParseSlideNote(string[] lines, int idx, string code, ChuNote note, List alerts, ChuChart chart) + private static int ParseSlideNote(string[] lines, int idx, string code, ChuNote previousNote, List alerts, ChuChart chart) { - note.Type = "SLD"; - ParseCellWidth(code, 1, note, alerts, idx + 1, chart); + // 注:一开始从外面传进来的previousNote,最后并不会被添加进chart里,只是作为第一段的起点参照而已。 + var startTime = previousNote.Time; + ParseCellWidth(code, 1, previousNote, alerts, idx + 1, chart); + previousNote.EndCell = previousNote.Cell; + previousNote.EndWidth = previousNote.Width; bool foundFirst = false; while (idx + 1 < lines.Length) - { + { // 循环处理所有的跟随行。idx始终指向上一条已经处理完的行。 var nextLine = lines[idx + 1].Trim(); - if (!TryParseFollowerLine(nextLine, out var duration, out var endCell, out var endWidth)) + if (!TryParseFollowerLine(nextLine, out var marker, out var duration, out var endCell, out var endWidth, true)) { if (nextLine.StartsWith('\'') || nextLine.StartsWith('@')) { idx++; continue; } break; } - note.Duration += new Rational(duration, RSL); - note.EndCell = endCell; - note.EndWidth = endWidth; + var segmentEnd = startTime + new Rational(duration, RSL); + var note = new ChuNote + { + Type = marker == "s" ? "SLD" : "SLC", + Time = previousNote.EndTime, Cell = previousNote.EndCell, Width = previousNote.EndWidth, + Duration = segmentEnd - previousNote.EndTime, + EndCell = endCell, EndWidth = endWidth, + TargetNote = "SLD" + }; + chart.Notes.Add(note); + previousNote = note; idx++; foundFirst = true; } if (!foundFirst) - alerts.Add(new Alert(Warning, $"SLD 音符缺少时长跟随行") { Line = idx + 1, RelevantNote = FormatNoteRef(note, chart) }); + alerts.Add(new Alert(Warning, $"SLD 音符缺少时长跟随行") { Line = idx + 1, RelevantNote = FormatNoteRef(previousNote, chart) }); return idx; } - private static bool TryParseFollowerLine(string line, out int duration, out int endCell, out int endWidth, bool requireEndCellWidth = false) + private static bool TryParseFollowerLine(string line, out string marker, out int duration, out int endCell, out int endWidth, bool requireEndCellWidth = false) { duration = 0; endCell = 0; endWidth = 1; + marker = ""; if (!line.StartsWith('#')) return false; // support both >s (SLD) and >c (SLC) follower lines - int gtIdx = -1; - int markerLen = 0; - if (line.Contains(">s")) { gtIdx = line.IndexOf(">s", StringComparison.Ordinal); markerLen = 2; } - else if (line.Contains(">c")) { gtIdx = line.IndexOf(">c", StringComparison.Ordinal); markerLen = 2; } + int gtIdx = line.IndexOfAny(['>', ':']); if (gtIdx < 1) return false; + marker = line[gtIdx+1].ToString(); + int markerLen = 2; var durationStr = line[1..gtIdx]; if (!int.TryParse(durationStr, NumberStyles.Integer, CultureInfo.InvariantCulture, out duration)) return false; @@ -467,20 +485,6 @@ private static void ParseAirNote(string code, ChuNote note, List alerts, } } - private static void ParseChrNote(string code, ChuNote note, List alerts, int lineNum, ChuChart chart) - { - note.Type = "CHR"; - if (code.Length < 3) - { - alerts.Add(new Alert(Warning, $"CHR 音符代码过短: {code}") { Line = lineNum }); - return; - } - - ParseCellWidth(code, 1, note, alerts, lineNum, chart); - var extraRaw = code.Length > 3 ? code[3..] : ""; - note.Tag = ChrExtras.GetValueOrDefault(extraRaw, extraRaw); - } - private static int HexCharToInt(char c) { return c switch From 9c13621cbaa1bd790e0051588d633402fd78d371 Mon Sep 17 00:00:00 2001 From: Starrah Date: Thu, 7 May 2026 12:46:04 +0800 Subject: [PATCH 10/13] =?UTF-8?q?[+]=20=E5=9C=A8ChuNote=E4=B8=AD=E6=96=B0?= =?UTF-8?q?=E5=A2=9EPrevious=E5=AD=97=E6=AE=B5=EF=BC=8C=E6=96=B0=E5=A2=9EB?= =?UTF-8?q?aseChuParser=E5=92=8CFillAllPrevious=E9=80=9A=E7=94=A8=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=E6=96=B9=E6=B3=95=E3=80=82=E4=BB=A5=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?slide=E7=9A=84=E9=93=BE=E5=BC=8F=E5=85=B3=E8=81=94=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chart/chu/ChuNote.cs | 32 +++++++++++-- parser/chu/BaseChuParser.cs | 90 +++++++++++++++++++++++++++++++++++++ parser/chu/C2sParser.cs | 18 +++++--- parser/chu/SusParser.cs | 11 ++--- parser/chu/UgcParser.cs | 9 ++-- tests/chu/ChuTests.cs | 3 +- 6 files changed, 140 insertions(+), 23 deletions(-) create mode 100644 parser/chu/BaseChuParser.cs diff --git a/chart/chu/ChuNote.cs b/chart/chu/ChuNote.cs index 8faf1fc..1205520 100644 --- a/chart/chu/ChuNote.cs +++ b/chart/chu/ChuNote.cs @@ -16,16 +16,40 @@ public class ChuNote: BaseNote public int Width { get; set; } = 1; /** HLD/SLD/AHD/ASD等的 持续时长 */ public Rational Duration { get; set => field = value.CanonicalForm; } = 0; + /** SLD 终点列 */ - public int EndCell { get; set; } + public int EndCell + { + get => _endCell ?? Cell; + set => _endCell = value; + } /** SLD 终点宽度 */ - public int EndWidth { get; set; } = 1; - /** Air系列音符/Slide系列音符的 关联的目标音符类型 */ - public string TargetNote { get; set; } = ""; + public int EndWidth + { + get => _endWidth ?? Width; + set => _endWidth = value; + } + + /** + * 当前音符的”前驱“。对不同类型的音符,其定义不同: + * - 对 Slide(SLD/SLC),是该slide对应的前一段slide。(对首段slide,该值为null) + * - 对 Air(AIR/AUR/AUL/ADW/ADR/ADL),是它所依附的音符(可以是tap\hold等任何类型,应该只要不是air系列和aircrush(ALD)都行) + * - 对 Air Slide(ASD/ASC):对首段slide,同Air的情况、是它所依附的音符;对第二段及之后的slide,同Slide的情况,是该slide对应的前一段slide。 + * + * 不难分析出,在完成整个chart之后,这个属性其实可以根据完整chart的列表动态推断的。 + * 因此,在BaseChuParser类中提供了FillAllPrevious方法,该方法应该在所有Note被正常解析完成后调用,填充所有上述类型的音符的targetNote信息。这样就不用每个Parser都写一段相似的逻辑。 + */ + public ChuNote? Previous; + /** CHR/FLK/Air系列音符可能会具有的标记(如UP、L、DEF等) */ public string Tag { get; set; } = ""; /** ASD/ASC/ALD上具有的、目前含义还不明确的字段,统一收集到这个里面。 */ public List ExtraData = []; public override Rational EndTime => (Time + Duration).CanonicalForm; + /** Air系列音符/Slide系列音符的 关联的目标音符类型。仅供向前兼容使用。 */ + public string TargetNote => Previous?.Type ?? "N"; + + private int? _endCell; + private int? _endWidth; } diff --git a/parser/chu/BaseChuParser.cs b/parser/chu/BaseChuParser.cs new file mode 100644 index 0000000..ae1b43b --- /dev/null +++ b/parser/chu/BaseChuParser.cs @@ -0,0 +1,90 @@ +using MuConvert.chu; +using MuConvert.utils; + +namespace MuConvert.parser; + +public abstract class BaseChuParser : IParser +{ + public abstract (ChuChart, List) Parse(string text); + + /** + * 填充所有需要 Previous 的音符(见 注释)。 + * 只会填充当前Previous没有被设置过的音符:如果某个音符的Previous不为null(在Parse过程中已经被设置过了),则会尊重Parse的决定,不会再次设置。 + * + * 推断规则: + * - 前驱音符必须满足“首尾相接”:prev.EndTime == cur.Time 且 prev.EndCell == cur.Cell 且 prev.EndWidth == cur.Width + * - 再按音符类型施加额外约束(slide / air / air-slide) + * + * 该方法应在所有音符解析完成后调用。 + * + * 谱面对象 + * 过程中产生的警告会被放进这个数组里。 + * 可选。对C2S这种,谱面中原始记录了targetNote的类型的格式,可以将相关记录通过这个字典传过来,供本函数作为选择previous时的优先和参考。 + */ + protected virtual void FillAllPrevious(ChuChart chart, List alerts, Dictionary? rawTargetNote = null) + { + if (chart.Notes.Count == 0) return; + + var endDict = new Dictionary<(Rationals.Rational EndTime, int EndCell, int EndWidth), List>(); + foreach (var n in chart.Notes) + { + endDict.Add((n.EndTime, n.EndCell, n.EndWidth), n); + } + + foreach (var cur in chart.Notes) + { + if (!NeedsPrevious(cur)) continue; + if (cur.Previous != null) continue; // 若某些 parser 已提前填了 Previous,则保留 + + var key = (cur.Time, cur.Cell, cur.Width); + var filtered = FilterPreviousCandidates(cur, endDict.GetValueOrDefault(key, [])); + + if (rawTargetNote != null && rawTargetNote.TryGetValue(cur, out var target) && !string.IsNullOrEmpty(target)) + { + var filteredByRaw = filtered.Where(x=>x.Type == target).ToList(); + if (filteredByRaw.Count == 0) + { + alerts.Add(new Alert(Alert.LEVEL.Warning, "未找到声明的前驱/依附音符", cur.Time, (double)chart.ToSecond(cur.Time))); + } + else filtered = filteredByRaw; // 缩小目标范围 + } + + if (filtered.Count > 0) cur.Previous = filtered[0]; // 取第一个 + } + } + + private static bool NeedsPrevious(ChuNote n) + { + return IsSlide(n.Type) || IsAir(n.Type) || IsAirSlide(n.Type) || IsAirHold(n.Type) || IsAirCrush(n.Type); + } + + public static bool IsSlide(string t) => t is "SLD" or "SLC" or "SXD" or "SXC"; + public static bool IsAirSlide(string t) => t is "ASD" or "ASC"; + public static bool IsAir(string t) => t is "AIR" or "AUR" or "AUL" or "ADW" or "ADR" or "ADL"; + public static bool IsAirHold(string t) => t is "AHD" or "AHX"; + public static bool IsAirCrush(string t) => t is "ALD"; + // 是否是广义的air音符(Air/Air Hold/Air Slide/Air Crush) + public static bool IsGeneralizedAir(string t) => IsAir(t) || IsAirHold(t) || IsAirSlide(t) || IsAirCrush(t); + + private static List FilterPreviousCandidates(ChuNote cur, List candidates) + { // 注意:候选列表已满足“首尾相接”,这里仅做类型约束 + List result = []; + + if (IsSlide(cur.Type)) + { // Slide 的 previous:上一段 slide(找不到则说明是首段,则为 null) + result.AddRange(candidates.Where(n => IsSlide(n.Type))); + } + else if (IsAirSlide(cur.Type)) + { // Air Slide:优先匹配“上一段airslide”,其次匹配“上一段其他 + result.AddRange(candidates.Where(n => IsAirSlide(n.Type))); + result.AddRange(candidates.Where(n => IsLegalPreviousForAir(n.Type))); + } + else if (IsAir(cur.Type) || IsAirHold(cur.Type)) + { // Air 系列:依附在一个“非广义Air”的音符上 + return candidates.Where(n => IsLegalPreviousForAir(n.Type)).ToList(); + } + return result; + + bool IsLegalPreviousForAir(string t) => !(IsGeneralizedAir(t) || t == "MNE" || t == "CLICK"); + } +} \ No newline at end of file diff --git a/parser/chu/C2sParser.cs b/parser/chu/C2sParser.cs index 5262684..aa5a460 100644 --- a/parser/chu/C2sParser.cs +++ b/parser/chu/C2sParser.cs @@ -11,7 +11,7 @@ namespace MuConvert.chu; * C2S 格式解析器(官方格式,RESOLUTION=384 tick/小节)。 * Tab 分隔文本,识别 HEADER / TIMING / NOTES 区段。 */ -public class C2sParser : IParser +public class C2sParser: BaseChuParser { private static int RSL = 384; private static readonly HashSet HeadTags = new(StringComparer.OrdinalIgnoreCase) @@ -19,7 +19,10 @@ public class C2sParser : IParser private static readonly HashSet TimingTags = new(StringComparer.OrdinalIgnoreCase) { "BPM", "MET", "SFL" }; - public (ChuChart, List) Parse(string text) + // C2S 会原始记录 targetNote 字符串;用于在 Previous 推断有多个候选时优先匹配。 + private readonly Dictionary _rawTargetNote = new(); + + public override (ChuChart, List) Parse(string text) { var chart = new ChuChart(); var alerts = new List(); @@ -51,6 +54,7 @@ public class C2sParser : IParser } } + FillAllPrevious(chart, alerts, _rawTargetNote); return (chart, alerts); } @@ -86,10 +90,11 @@ private static void ParseTiming(string[] p, ChuChart chart) } } - private static void ParseNote(string[] p, ChuChart chart, List alerts, int lineNum) + private void ParseNote(string[] p, ChuChart chart, List alerts, int lineNum) { var tag = p[0].ToUpperInvariant(); var note = new ChuNote { Type = tag, Time = Int(p, 1) + new Rational(Int(p, 2), RSL) }; + string? targetNote = null; switch (tag) { @@ -107,12 +112,12 @@ private static void ParseNote(string[] p, ChuChart chart, List alerts, in case "FLK": note.Cell = Int(p, 3); note.Width = Math.Max(1, Int(p, 4, 1)); note.Tag = Str(p, 5); break; case "AIR": case "AUR": case "AUL": case "ADW": case "ADR": case "ADL": - note.Cell = Int(p, 3); note.Width = Math.Max(1, Int(p, 4, 1)); note.TargetNote = Str(p, 5); + note.Cell = Int(p, 3); note.Width = Math.Max(1, Int(p, 4, 1)); targetNote = Str(p, 5); if (p.Length >= 7) note.Tag = Str(p, 6); break; case "AHD": case "AHX": note.Cell = Int(p, 3); note.Width = Math.Max(1, Int(p, 4, 1)); - note.TargetNote = Str(p, 5); note.Duration = new Rational(Int(p, 6), RSL); + targetNote = Str(p, 5); note.Duration = new Rational(Int(p, 6), RSL); if (p.Length >= 8) note.Tag = Str(p, 7); break; case "ASD": case "ASC": @@ -123,7 +128,7 @@ private static void ParseNote(string[] p, ChuChart chart, List alerts, in return; } note.Cell = Int(p, 3); note.Width = Math.Max(1, Int(p, 4, 1)); - note.TargetNote = Str(p, 5); + targetNote = Str(p, 5); note.ExtraData = [Int(p, 6), Int(p, 10)]; note.Duration = new Rational(Int(p, 7), RSL); note.EndCell = Int(p, 8); note.EndWidth = Math.Max(1, Int(p, 9, 1)); @@ -144,6 +149,7 @@ private static void ParseNote(string[] p, ChuChart chart, List alerts, in alerts.Add(new Alert(Warning, string.Format(Locale.C2SUnknownNoteType, tag)) { Line = lineNum }); return; } + if (targetNote != null) _rawTargetNote[note] = targetNote; chart.Notes.Add(note); } diff --git a/parser/chu/SusParser.cs b/parser/chu/SusParser.cs index 67bd23b..0b6c63f 100644 --- a/parser/chu/SusParser.cs +++ b/parser/chu/SusParser.cs @@ -11,7 +11,7 @@ namespace MuConvert.chu; * SUS 格式解析器(社区工具格式,REQUEST=480 tick/拍,lane 0–31)。 * #MMTT:data 十六进制编码音符。 */ -public class SusParser : IParser +public class SusParser: BaseChuParser { private static int RSL = 480 * 4; @@ -28,7 +28,7 @@ public class SusParser : IParser [0x10] = "MNE", }; - public (ChuChart, List) Parse(string text) + public override (ChuChart, List) Parse(string text) { var chart = new ChuChart(); var alerts = new List(); @@ -57,6 +57,7 @@ public class SusParser : IParser } } + FillAllPrevious(chart, alerts); return (chart, alerts); } @@ -208,11 +209,7 @@ private static void ParseSlideData(string dataStr, ChuNote note, int tpm, List alerts, int lineNum) { - if (dataStr.Length >= 8) - { - note.TargetNote = HexToInt(dataStr[6..8]).ToString(); - } - else + if (dataStr.Length < 8) { alerts.Add(new Alert(Warning, $"AIR/ADW 音符缺少目标: {dataStr}") { Line = lineNum, RelevantNote = FormatNoteRef(note, tpm) }); } diff --git a/parser/chu/UgcParser.cs b/parser/chu/UgcParser.cs index e1840bf..859d61e 100644 --- a/parser/chu/UgcParser.cs +++ b/parser/chu/UgcParser.cs @@ -11,7 +11,7 @@ namespace MuConvert.chu; * UGC 格式解析器(UMIGURI 格式,@TICKS=480 tick/拍)。 * @HEADER 标签 + #measure'tick:code 音符格式。 */ -public class UgcParser : IParser +public class UgcParser: BaseChuParser { private static int RSL = 480 * 4; @@ -33,7 +33,7 @@ public class UgcParser : IParser ["C"] = "CE", }; - public (ChuChart, List) Parse(string text) + public override (ChuChart, List) Parse(string text) { var chart = new ChuChart(); var alerts = new List(); @@ -64,6 +64,7 @@ public class UgcParser : IParser } FinalizeUgcSflDurations(chart); + FillAllPrevious(chart, alerts); return (chart, alerts); } @@ -383,7 +384,7 @@ private static int ParseSlideNote(string[] lines, int idx, string code, ChuNote Time = previousNote.EndTime, Cell = previousNote.EndCell, Width = previousNote.EndWidth, Duration = segmentEnd - previousNote.EndTime, EndCell = endCell, EndWidth = endWidth, - TargetNote = "SLD" + Previous = foundFirst ? previousNote : null, }; chart.Notes.Add(note); previousNote = note; @@ -475,8 +476,6 @@ private static void ParseAirNote(string code, ChuNote note, List alerts, alerts.Add(new Alert(Warning, $"未知的 AIR 方向: {dir}") { Line = lineNum, RelevantNote = FormatNoteRef(note, chart) }); } - note.TargetNote = mainPart.Length > 2 ? mainPart[2..] : "N"; - if (underscoreIdx >= 0 && note.Type == "AHD") { var durStr = afterCellWidth[(underscoreIdx + 1)..]; diff --git a/tests/chu/ChuTests.cs b/tests/chu/ChuTests.cs index d6a65e3..e2ae05f 100644 --- a/tests/chu/ChuTests.cs +++ b/tests/chu/ChuTests.cs @@ -102,7 +102,7 @@ private static ChuNote C2sNoteScaledToUgcTicks(ChuNote n, int ugcTicksPerBeat, i Duration = duration, EndCell = n.EndCell, EndWidth = n.EndWidth, - TargetNote = n.TargetNote, + Previous = n.Previous, Tag = n.Tag, ExtraData = [..n.ExtraData], }; @@ -115,6 +115,7 @@ private static void AssertUgcNotesEquivalentToReparsedC2s(ChuChart ugc, ChuChart if (isUgcReference) { var ugcSnaps = ugc.Notes + .Where(n=>n.Type != "CLICK") .Select(n => SnapshotNote(UgcNoteScaledToC2sTicks(n, 480, 384))) .OrderBy(s => s) .ToArray(); From 52af0a3bb321b905aad582b4f737abd233b1f147 Mon Sep 17 00:00:00 2001 From: Starrah Date: Thu, 7 May 2026 16:09:33 +0800 Subject: [PATCH 11/13] =?UTF-8?q?[+]=20UGC=20=E5=AF=B9Air=20Slide=E5=92=8C?= =?UTF-8?q?Air=20Hold=E7=9A=84=E8=A7=A3=E6=9E=90=20=E5=8F=82=E8=A7=81?= =?UTF-8?q?=E6=96=87=E6=A1=A3=EF=BC=9A=20https://gist.github.com/inonote/5?= =?UTF-8?q?c01e73781cab17765a1d93641d52298?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- parser/chu/UgcParser.cs | 160 +++++++++++++++++++++++++++------------- tests/chu/ChuTests.cs | 20 +++-- 2 files changed, 122 insertions(+), 58 deletions(-) diff --git a/parser/chu/UgcParser.cs b/parser/chu/UgcParser.cs index 859d61e..efad0f7 100644 --- a/parser/chu/UgcParser.cs +++ b/parser/chu/UgcParser.cs @@ -8,8 +8,7 @@ namespace MuConvert.chu; /** - * UGC 格式解析器(UMIGURI 格式,@TICKS=480 tick/拍)。 - * @HEADER 标签 + #measure'tick:code 音符格式。 + * UMIGURI语法文档: https://gist.github.com/inonote/5c01e73781cab17765a1d93641d52298 */ public class UgcParser: BaseChuParser { @@ -23,7 +22,6 @@ public class UgcParser: BaseChuParser ["DC"] = "ADW", ["DR"] = "ADR", ["DL"] = "ADL", - ["HD"] = "AHD", }; private static readonly Dictionary ChrExtras = new() @@ -273,7 +271,7 @@ private static int ParseNoteLine(string[] lines, int idx, ChuChart chart, List alerts, } } - private static int ParseHoldNote(string[] lines, int idx, string code, ChuNote note, List alerts, ChuChart chart) + private static int ParseHoldNote(bool isAirHold, string[] lines, int idx, string code, ChuNote note, List alerts, ChuChart chart) { - note.Type = "HLD"; + note.Type = isAirHold ? "AHD" : "HLD"; ParseCellWidth(code, 1, note, alerts, idx + 1, chart); + if (isAirHold) + { + // 解析颜色数据。目前只解析、不使用。 + _ = code.Last(); // var colorChar 颜色标记 N/I + } + bool foundFirst = false; while (idx + 1 < lines.Length) { var nextLine = lines[idx + 1].Trim(); - if (!TryParseFollowerLine(nextLine, out _, out var duration, out _, out _)) + if (!TryParseFollowerLine(nextLine, out var marker, out var duration, out _, out _, out _, false)) { if (nextLine.StartsWith('\'') || nextLine.StartsWith('@')) { idx++; continue; } break; } note.Duration += new Rational(duration, RSL); + if (isAirHold && marker == "c") note.Type = "AHX"; // 可能是对应于UMIGURI文档中的 AirHold的 AIR-ACTION 无し终点 idx++; foundFirst = true; } if (!foundFirst) - alerts.Add(new Alert(Warning, $"HLD 音符缺少时长跟随行") { Line = idx + 1, RelevantNote = FormatNoteRef(note, chart) }); + alerts.Add(new Alert(Warning, $"HLD 音符缺少时长跟随行") { Line = idx + 1, RelevantNote = lines[idx] }); return idx; } - private static int ParseSlideNote(string[] lines, int idx, string code, ChuNote previousNote, List alerts, ChuChart chart) + private static int ParseSlideNote(bool isAirSlide, string[] lines, int idx, string code, ChuNote previousNote, List alerts, ChuChart chart) { // 注:一开始从外面传进来的previousNote,最后并不会被添加进chart里,只是作为第一段的起点参照而已。 var startTime = previousNote.Time; @@ -367,21 +384,30 @@ private static int ParseSlideNote(string[] lines, int idx, string code, ChuNote previousNote.EndCell = previousNote.Cell; previousNote.EndWidth = previousNote.Width; + if (isAirSlide) + { + // 解析高度和颜色数据。目前只解析、不使用。 + TryParseUgcBase36Int2(code.AsSpan(3, code.Length - 4), out _); // out var startHeight 起始的高度值 + _ = code.Last(); // var colorChar 颜色标记 N/I + } + bool foundFirst = false; while (idx + 1 < lines.Length) { // 循环处理所有的跟随行。idx始终指向上一条已经处理完的行。 var nextLine = lines[idx + 1].Trim(); - if (!TryParseFollowerLine(nextLine, out var marker, out var duration, out var endCell, out var endWidth, true)) + if (!TryParseFollowerLine(nextLine, out var marker, out var duration, out var endCell, out var endWidth, out _, true)) { if (nextLine.StartsWith('\'') || nextLine.StartsWith('@')) { idx++; continue; } break; } + var type = isAirSlide ? (marker == "s" ? "ASD" : "ASC") : (marker == "s" ? "SLD" : "SLC"); + var segmentEnd = startTime + new Rational(duration, RSL); var note = new ChuNote { - Type = marker == "s" ? "SLD" : "SLC", - Time = previousNote.EndTime, Cell = previousNote.EndCell, Width = previousNote.EndWidth, + Type = type, Time = previousNote.EndTime, + Cell = previousNote.EndCell, Width = previousNote.EndWidth, Duration = segmentEnd - previousNote.EndTime, EndCell = endCell, EndWidth = endWidth, Previous = foundFirst ? previousNote : null, @@ -393,37 +419,56 @@ private static int ParseSlideNote(string[] lines, int idx, string code, ChuNote } if (!foundFirst) - alerts.Add(new Alert(Warning, $"SLD 音符缺少时长跟随行") { Line = idx + 1, RelevantNote = FormatNoteRef(previousNote, chart) }); + alerts.Add(new Alert(Warning, $"SLD 音符缺少时长跟随行") { Line = idx + 1, RelevantNote = lines[idx] }); return idx; } - private static bool TryParseFollowerLine(string line, out string marker, out int duration, out int endCell, out int endWidth, bool requireEndCellWidth = false) + private static bool TryParseUgcBase36Int2(ReadOnlySpan twoChars, out int value) { - duration = 0; + value = 0; + if (twoChars.Length == 0) return false; + else if (twoChars.Length == 1) + { + if (!TryHexCharToInt(twoChars[0], out value)) return false; + } + else + { + if (!TryHexCharToInt(twoChars[0], out var hi) || !TryHexCharToInt(twoChars[1], out var lo)) return false; + value = hi * 36 + lo; + } + return true; + } + + private static bool TryParseFollowerLine(string line, out string marker, out int endTick, out int endCell, out int endWidth, out int? height, bool requireEndCellWidth) + { + endTick = 0; endCell = 0; endWidth = 1; marker = ""; + height = null; if (!line.StartsWith('#')) return false; // support both >s (SLD) and >c (SLC) follower lines - int gtIdx = line.IndexOfAny(['>', ':']); - if (gtIdx < 1) return false; - marker = line[gtIdx+1].ToString(); + int sepIdx = line.IndexOfAny(['>', ':']); + if (sepIdx < 1) return false; + marker = line[sepIdx+1].ToString(); int markerLen = 2; - var durationStr = line[1..gtIdx]; - if (!int.TryParse(durationStr, NumberStyles.Integer, CultureInfo.InvariantCulture, out duration)) return false; + var endTickStr = line[1..sepIdx]; + if (!int.TryParse(endTickStr, NumberStyles.Integer, CultureInfo.InvariantCulture, out endTick)) return false; - var afterMarker = line[(gtIdx + markerLen)..]; + var afterMarker = line[(sepIdx + markerLen)..]; if (afterMarker.Length >= 2) { endCell = HexCharToInt(afterMarker[0]); - endWidth = WidthHexCharToInt(afterMarker[1]); + endWidth = HexCharToInt(afterMarker[1]); } else if (requireEndCellWidth) return false; + if (afterMarker.Length > 2 && TryParseUgcBase36Int2(afterMarker.AsSpan()[2..], out var heightV)) height = heightV; + return true; } @@ -433,19 +478,18 @@ private static void ParseCellWidth(string code, int startIdx, ChuNote note, List { note.Cell = HexCharToInt(code[startIdx]); if (code.Length > startIdx + 1) - note.Width = WidthHexCharToInt(code[startIdx + 1]); + note.Width = HexCharToInt(code[startIdx + 1]); else - alerts.Add(new Alert(Warning, $"音符缺少 width: {code}") { Line = lineNum, RelevantNote = FormatNoteRef(note, chart) }); + alerts.Add(new Alert(Warning, $"音符缺少 width: {code}") { Line = lineNum, RelevantNote = FormatNoteRef(note, code) }); } else { - alerts.Add(new Alert(Warning, $"音符缺少 cell 和 width: {code}") { Line = lineNum, RelevantNote = FormatNoteRef(note, chart) }); + alerts.Add(new Alert(Warning, $"音符缺少 cell 和 width: {code}") { Line = lineNum, RelevantNote = FormatNoteRef(note, code) }); } } private static void ParseAirNote(string code, ChuNote note, List alerts, int lineNum, ChuChart chart) { - // Matches UgcGenerator: "a" + cell + width + two-letter direction + targetNote [ + "_" + airHoldDuration for AHD ] if (code.Length < 5) { alerts.Add(new Alert(Warning, $"AIR 音符代码过短: {code}") { Line = lineNum }); @@ -454,9 +498,7 @@ private static void ParseAirNote(string code, ChuNote note, List alerts, } ParseCellWidth(code, 1, note, alerts, lineNum, chart); - var afterCellWidth = code[3..]; - var underscoreIdx = afterCellWidth.IndexOf('_'); - var mainPart = underscoreIdx >= 0 ? afterCellWidth[..underscoreIdx] : afterCellWidth; + var mainPart = code[3..5]; if (mainPart.Length < 2) { @@ -473,44 +515,58 @@ private static void ParseAirNote(string code, ChuNote note, List alerts, else { note.Type = "AIR"; - alerts.Add(new Alert(Warning, $"未知的 AIR 方向: {dir}") { Line = lineNum, RelevantNote = FormatNoteRef(note, chart) }); + alerts.Add(new Alert(Warning, $"未知的 AIR 方向: {dir}") { Line = lineNum, RelevantNote = FormatNoteRef(note, code) }); } + } - if (underscoreIdx >= 0 && note.Type == "AHD") - { - var durStr = afterCellWidth[(underscoreIdx + 1)..]; - if (int.TryParse(durStr, NumberStyles.Integer, CultureInfo.InvariantCulture, out var ahdDuration)) - note.Duration = new Rational(ahdDuration, RSL); + private static int ParseAirCrushNote(string[] lines, int idx, string code, ChuNote previousNote, List alerts, ChuChart chart) + { + // TODO 尚未实现,所以先给个警告 + alerts.Add(new Alert(Warning, "当前版本尚未实现对Air-Crush(UMIGURI的':C'或':T'音符)的解析。") { Line = idx, RelevantNote = lines[idx] }); + + bool foundFirst = false; + while (idx + 1 < lines.Length) + { // 循环处理所有的跟随行。idx始终指向上一条已经处理完的行。 + var nextLine = lines[idx + 1].Trim(); + if (!TryParseFollowerLine(nextLine, out var marker, out var duration, out _, out _, out _, false)) + { + if (nextLine.StartsWith('\'') || nextLine.StartsWith('@')) { idx++; continue; } + break; + } + + // TODO 尚未实现 + idx++; + foundFirst = true; } + + if (!foundFirst) + alerts.Add(new Alert(Warning, $"air-crush 音符缺少时长跟随行") { Line = idx + 1, RelevantNote = lines[idx] }); + return idx; } private static int HexCharToInt(char c) { - return c switch - { - >= '0' and <= '9' => c - '0', - >= 'A' and <= 'F' => c - 'A' + 10, - >= 'a' and <= 'f' => c - 'a' + 10, - _ => 0, - }; + if (!TryHexCharToInt(c, out var result)) result = 0; + return result; } - private static int WidthHexCharToInt(char c) + private static bool TryHexCharToInt(char c, out int result) { - return c switch + result = c switch { - >= '1' and <= '9' => c - '1' + 1, - >= 'A' and <= 'G' => c - 'A' + 10, - >= 'a' and <= 'g' => c - 'a' + 10, - _ => 1, + >= '0' and <= '9' => c - '0', + >= 'A' and <= 'Z' => c - 'A' + 10, + >= 'a' and <= 'z' => c - 'a' + 10, + _ => -1, }; + return result >= 0; } // ReSharper disable once UnusedParameter.Local - private static string FormatNoteRef(ChuNote note, ChuChart chart) + private static string FormatNoteRef(ChuNote note, string code) { var (m, o) = Utils.BarAndTick(note.Time, RSL); - return $"#{m}'{o}:{note.Type}"; + return $"#{m}'{o}:{code}"; } } diff --git a/tests/chu/ChuTests.cs b/tests/chu/ChuTests.cs index e2ae05f..d6ed774 100644 --- a/tests/chu/ChuTests.cs +++ b/tests/chu/ChuTests.cs @@ -47,6 +47,7 @@ public void C2sRoundTrip() /// /// Builds a stable, comparable string from a note's public instance properties (name-sorted) /// so round-trip tests verify no field loss without hard-coding each property in the test. + /// Omits and (redundant with Time/Duration or not stable across formats). /// private static string SnapshotNote(ChuNote note) { @@ -60,9 +61,11 @@ private static string SnapshotNote(ChuNote note) var propParts = typeof(ChuNote).GetProperties(BindingFlags.Instance | BindingFlags.Public) .OrderBy(p => p.Name) + .Where(p => p.Name != nameof(ChuNote.EndTime)) .Select(p => $"{p.Name}={F(p.GetValue(note))}"); var fieldParts = typeof(ChuNote).GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly) .OrderBy(f => f.Name) + .Where(f => f.Name != nameof(ChuNote.ExtraData)) .Select(f => $"{f.Name}={F(f.GetValue(note))}"); return string.Join("|", propParts.Concat(fieldParts)); } @@ -114,20 +117,25 @@ private static void AssertUgcNotesEquivalentToReparsedC2s(ChuChart ugc, ChuChart { if (isUgcReference) { - var ugcSnaps = ugc.Notes - .Where(n=>n.Type != "CLICK") + var ugcSnaps = ugc.Notes.Where(n=>n.Type != "CLICK") + .OrderBy(n=>n.Time).ThenBy(n=>n.Cell).ThenBy(n=>n.Width).ThenBy(n=>n.Duration).ThenBy(n=>n.Type) .Select(n => SnapshotNote(UgcNoteScaledToC2sTicks(n, 480, 384))) - .OrderBy(s => s) .ToArray(); - var c2sSnaps = c2s.Notes.Select(SnapshotNote).OrderBy(s => s).ToArray(); + var c2sSnaps = c2s.Notes + .OrderBy(n=>n.Time).ThenBy(n=>n.Cell).ThenBy(n=>n.Width).ThenBy(n=>n.Duration).ThenBy(n=>n.Type) + .Select(SnapshotNote) + .ToArray(); Assert.Equal(ugcSnaps, c2sSnaps); } else { - var ugcSnaps = ugc.Notes.Select(SnapshotNote).OrderBy(s => s).ToArray(); + var ugcSnaps = ugc.Notes.Where(n=>n.Type != "CLICK") + .OrderBy(n=>n.Time).ThenBy(n=>n.Cell).ThenBy(n=>n.Width).ThenBy(n=>n.Duration).ThenBy(n=>n.Type) + .Select(SnapshotNote) + .ToArray(); var c2sSnaps = c2s.Notes + .OrderBy(n=>n.Time).ThenBy(n=>n.Cell).ThenBy(n=>n.Width).ThenBy(n=>n.Duration).ThenBy(n=>n.Type) .Select(n => SnapshotNote(C2sNoteScaledToUgcTicks(n, 480, 384))) - .OrderBy(s => s) .ToArray(); Assert.Equal(c2sSnaps, ugcSnaps); } From b54b7e97da944d6a5404ea8adf594a90a7c0d34c Mon Sep 17 00:00:00 2001 From: Starrah Date: Fri, 8 May 2026 00:11:55 +0800 Subject: [PATCH 12/13] =?UTF-8?q?[+]=20UgcGenerator=20Slide=E3=80=81Air=20?= =?UTF-8?q?Hold=E3=80=81Air=20Slide=E7=9A=84=E6=AD=A3=E7=A1=AE=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- generator/chu/C2sGenerator.cs | 22 +++--- generator/chu/UgcGenerator.cs | 143 +++++++++++++++++++++++++++++----- parser/chu/UgcParser.cs | 18 ++++- tests/chu/ChuTests.cs | 1 + 4 files changed, 150 insertions(+), 34 deletions(-) diff --git a/generator/chu/C2sGenerator.cs b/generator/chu/C2sGenerator.cs index 73e56cf..a9f5f25 100644 --- a/generator/chu/C2sGenerator.cs +++ b/generator/chu/C2sGenerator.cs @@ -67,6 +67,13 @@ private static string Serialize(ChuChart chart, List alerts) return sb.ToString(); } + private static List allowedAirColors = ["DEF"]; // TODO 搞清楚UGC里的'I'颜色,在C2S里,对应的字符串是什么 + private static string AirColorTag(ChuNote n) + { + if (allowedAirColors.Contains(n.Tag)) return n.Tag; + else return "DEF"; + } + private static string? FormatNote(ChuNote n, int tpm, List alerts) { var (m, o) = Utils.BarAndTick(n.Time, tpm); @@ -79,13 +86,8 @@ private static string Serialize(ChuChart chart, List alerts) "SLD" or "SLC" or "SXD" or "SXC" => $"{n.Type}\t{m}\t{o}\t{n.Cell}\t{n.Width}\t{durTicks}\t{n.EndCell}\t{n.EndWidth}", "FLK" => $"FLK\t{m}\t{o}\t{n.Cell}\t{n.Width}\t{n.Tag}", "AIR" or "AUR" or "AUL" or "ADW" or "ADR" or "ADL" => - string.IsNullOrEmpty(n.Tag) - ? $"{n.Type}\t{m}\t{o}\t{n.Cell}\t{n.Width}\t{n.TargetNote}" - : $"{n.Type}\t{m}\t{o}\t{n.Cell}\t{n.Width}\t{n.TargetNote}\t{n.Tag}", - "AHD" or "AHX" => - string.IsNullOrEmpty(n.Tag) - ? $"{n.Type}\t{m}\t{o}\t{n.Cell}\t{n.Width}\t{n.TargetNote}\t{durTicks}" - : $"{n.Type}\t{m}\t{o}\t{n.Cell}\t{n.Width}\t{n.TargetNote}\t{durTicks}\t{n.Tag}", + $"{n.Type}\t{m}\t{o}\t{n.Cell}\t{n.Width}\t{n.TargetNote}\t{AirColorTag(n)}", + "AHD" or "AHX" => $"{n.Type}\t{m}\t{o}\t{n.Cell}\t{n.Width}\t{n.TargetNote}\t{durTicks}\t{AirColorTag(n)}", "ASD" or "ASC" => FormatAsdAsc(n, m, o, durTicks), "ALD" => FormatAld(n, m, o), "MNE" => $"MNE\t{m}\t{o}\t{n.Cell}\t{n.Width}", @@ -101,9 +103,9 @@ private static string Serialize(ChuChart chart, List alerts) private static string FormatAsdAsc(ChuNote n, int m, int o, int durTicks) { - var e0 = n.ExtraData.Count > 0 ? n.ExtraData[0] : 0; - var e1 = n.ExtraData.Count > 1 ? n.ExtraData[1] : 0; - return $"{n.Type}\t{m}\t{o}\t{n.Cell}\t{n.Width}\t{n.TargetNote}\t{e0}\t{durTicks}\t{n.EndCell}\t{n.EndWidth}\t{e1}\t{n.Tag}"; + var e0 = n.ExtraData.Count > 0 ? n.ExtraData[0] : 5; + var e1 = n.ExtraData.Count > 1 ? n.ExtraData[1] : 5; + return $"{n.Type}\t{m}\t{o}\t{n.Cell}\t{n.Width}\t{n.TargetNote}\t{e0}\t{durTicks}\t{n.EndCell}\t{n.EndWidth}\t{e1}\t{AirColorTag(n)}"; } private static string FormatAld(ChuNote n, int m, int o) diff --git a/generator/chu/UgcGenerator.cs b/generator/chu/UgcGenerator.cs index ea7400f..713ad43 100644 --- a/generator/chu/UgcGenerator.cs +++ b/generator/chu/UgcGenerator.cs @@ -11,16 +11,16 @@ public class UgcGenerator : IGenerator public (string, List) Generate(ChuChart chart) { var alerts = new List(); - var text = Serialize(chart); + var text = Serialize(chart, alerts); return (text, alerts); } - private static string Serialize(ChuChart ugc) + private static string Serialize(ChuChart ugc, List alerts) { ugc.Sort(); var sb = new StringBuilder(); - sb.AppendLine("@VER\t6"); + sb.AppendLine("@VER\t8"); if (!string.IsNullOrEmpty(ugc.Title)) sb.AppendLine($"@TITLE\t{ugc.Title}"); if (!string.IsNullOrEmpty(ugc.Artist)) sb.AppendLine($"@ARTIST\t{ugc.Artist}"); if (!string.IsNullOrEmpty(ugc.Designer)) sb.AppendLine($"@DESIGN\t{ugc.Designer}"); @@ -51,24 +51,123 @@ private static string Serialize(ChuChart ugc) sb.AppendLine("@ENDHEAD"); sb.AppendLine(); + // UGC Slide / AIR-SLIDE (v8): + // - Chains (ChuNote.Previous) serialize as ONE parent line + follower lines (#OffsetTick from parent time). + // - Ground slide: parent `s`, followers `>s` / `>c` + end cell/width. + // - Air slide: parent `S` + cell/width + hh (base-36 ×2, C2S/UGC height units) + N/I; followers `>s`/`>c` + xw + hh. + // - First segment may attach to TAP/HLD via Previous; only skip emit when Previous is another segment of the same chain. + var slideChains = BuildSlideChains(ugc.Notes); + foreach (var n in ugc.Notes) { + if (IsSlideChainNote(n.Type) && n.Previous != null && IsSlideChainNote(n.Previous.Type)) + continue; // 是Slide且不是第一段Slide,则应当已经被处理过了,直接跳过 + var (m, o) = Utils.BarAndTick(n.Time, RSL); - sb.Append($"#{m}'{o}:{UCode(n, RSL)}"); + var ucode = UCode(n); + if (ucode == "") + { + alerts.Add(new Alert(Alert.LEVEL.Warning, $"UGC Generator遇到了不支持的音符类型: {n.Type}", n.Time, (double)ugc.ToSecond(n.Time))); + continue; + } + sb.Append($"#{m}'{o}:{ucode}"); sb.AppendLine(); + + if (IsSlideChainNote(n.Type)) + { + if (slideChains.TryGetValue(n, out var segments)) + { + var isAir = IsAirSlideType(n.Type); + foreach (var seg in segments) + { + var endTicks = Utils.Tick(seg.EndTime - n.Time, RSL); + if (endTicks <= 0) continue; + if (isAir) + sb.AppendLine($"#{endTicks}>{SlideFollowerMarker(seg.Type)}{IntToHex(seg.EndCell)}{IntToHex(seg.EndWidth)}{EncodeUgcAirHeight2(AirSlideFollowerHeight(seg))}"); + else + sb.AppendLine($"#{endTicks}>{SlideFollowerMarker(seg.Type)}{IntToHex(seg.EndCell)}{IntToHex(seg.EndWidth)}"); + } + } + continue; + } + var durTicks = Utils.Tick(n.Duration, RSL); - if (n.Type == "HLD" && durTicks > 0) + if (n.Type is "HLD" or "HXD" && durTicks > 0) sb.AppendLine($"#{durTicks}>s"); - else if (n.Type == "SLD" && durTicks > 0) - sb.AppendLine($"#{durTicks}>s{Hx(n.EndCell)}{Hw(n.EndWidth)}"); + else if (n.Type is "AHD" or "AHX" && durTicks > 0) + { + var marker = (n.Type == "AHX") ? 'c' : 's'; + sb.AppendLine($"#{durTicks}>{marker}"); + } } return sb.ToString(); } - private static string UCode(ChuNote n, int tpm) + private static Dictionary> BuildSlideChains(List notes) + { + var chains = new Dictionary>(); + foreach (var n in notes) + { + if (!IsSlideChainNote(n.Type)) continue; + var head = GetSlideHead(n); + if (!chains.TryGetValue(head, out var list)) + chains[head] = list = []; + list.Add(n); + } + + // Order segments by their end time so follower ticks are increasing. + foreach (var (_, segs) in chains) + { + segs.Sort((a, b) => + { + var t = a.EndTime.CompareTo(b.EndTime); + if (t != 0) return t; + // stable-ish tie-breakers + t = a.Time.CompareTo(b.Time); + if (t != 0) return t; + t = string.CompareOrdinal(a.Type, b.Type); + if (t != 0) return t; + return 0; + }); + } + + // For a valid chain, follower ticks should be strictly increasing; if the chart has + // degenerate segments, later code simply skips non-positive offsets. + return chains; + } + + private static ChuNote GetSlideHead(ChuNote n) + { + var cur = n; + while (cur.Previous != null && IsSlideChainNote(cur.Previous.Type)) + cur = cur.Previous; + return cur; + } + + private static bool IsSlideType(string t) => t is "SLD" or "SLC" or "SXD" or "SXC"; + private static bool IsAirSlideType(string t) => t is "ASD" or "ASC"; + private static bool IsSlideChainNote(string t) => IsSlideType(t) || IsAirSlideType(t); + private static char SlideFollowerMarker(string t) => t is "SLC" or "SXC" or "ASC" ? 'c' : 's'; + + /// C2S col.6 / follower height: integer stored as two base-36 digits (Umiguri v8 AIR-SLIDE). + private static string EncodeUgcAirHeight2(int value) + { + var v = Math.Clamp(value * 10, 0, 35 * 36 + 35); + var hi = v / 36; + var lo = v % 36; + return $"{IntToHex(hi)}{IntToHex(lo)}"; + } + + private static int AirSlideParentStartHeight(ChuNote head) => 8; // TODO 现在暂时写死,之后应该改成从ExtraData等地方读取 + private static int AirSlideFollowerHeight(ChuNote seg) => 8; // TODO 现在暂时写死,之后应该改成从ExtraData等地方读取 + + private static Dictionary AirDirections = new() { - string c = Hx(n.Cell), w = Hw(n.Width); - var durTicks = Utils.Tick(n.Duration, tpm); + ["AIR"] = "UC", ["AUR"] = "UR", ["AUL"] = "UL", ["ADW"] = "DC", ["ADR"] = "DR", ["ADL"] = "DL", + }; + private static string UCode(ChuNote n) + { + string c = IntToHex(n.Cell), w = IntToHex(n.Width); var targetNote = string.IsNullOrEmpty(n.TargetNote) ? "N" : n.TargetNote; return n.Type switch { @@ -79,17 +178,21 @@ private static string UCode(ChuNote n, int tpm) "SLC" or "SXC" => $"s{c}{w}", "FLK" => $"f{c}{w}A", "MNE" => $"d{c}{w}", - "AIR" => $"a{c}{w}UC{targetNote}", - "AUR" => $"a{c}{w}UR{targetNote}", - "AUL" => $"a{c}{w}UL{targetNote}", - "AHD" or "AHX" => $"a{c}{w}HD{targetNote}_{durTicks}", - "ADW" => $"a{c}{w}DC{targetNote}", - "ADR" => $"a{c}{w}DR{targetNote}", - "ADL" => $"a{c}{w}DL{targetNote}", - _ => $"t{c}{w}" + // AIR-SLIDE (v8): #BarTick:S x w hh c + "ASD" or "ASC" => $"S{c}{w}{EncodeUgcAirHeight2(AirSlideParentStartHeight(n))}{AirHoldColorSuffix(n)}", + "AIR" or "AUR" or "AUL" or "ADW" or "ADR" or "ADL" => $"a{c}{w}{AirDirections[n.Type]}{targetNote}{AirHoldColorSuffix(n)}", + // AIR-HOLD (v8): #BarTick:H x w c + 子行 #OffsetTick:s / :c(见 Umiguri Chart v8 doc) + "AHD" or "AHX" => $"H{c}{w}{AirHoldColorSuffix(n)}", + _ => "" }; } - private static string Hx(int v) => "0123456789ABCDEF"[Math.Clamp(v, 0, 15)].ToString(); - private static string Hw(int v) => "123456789ABCDEFG"[Math.Clamp(v - 1, 0, 15)].ToString(); + private static string IntToHex(int v) => "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"[Math.Clamp(v, 0, 35)].ToString(); + + private static readonly Dictionary AirColor = new() + { + ["DEF"] = "N", + ["I"] = "I", // TODO 搞清楚UGC里的'I'颜色,在C2S里,对应的字符串是什么 + }; + private static string AirHoldColorSuffix(ChuNote n) => AirColor.GetValueOrDefault(n.Tag, "N"); } diff --git a/parser/chu/UgcParser.cs b/parser/chu/UgcParser.cs index efad0f7..e67299d 100644 --- a/parser/chu/UgcParser.cs +++ b/parser/chu/UgcParser.cs @@ -30,6 +30,12 @@ public class UgcParser: BaseChuParser ["D"] = "DW", ["C"] = "CE", }; + + private static readonly Dictionary AirColor = new() + { + ["N"] = "DEF", + ["I"] = "I", // TODO 搞清楚UGC里的'I'颜色,在C2S里,对应的字符串是什么 + }; public override (ChuChart, List) Parse(string text) { @@ -351,8 +357,8 @@ private static int ParseHoldNote(bool isAirHold, string[] lines, int idx, string if (isAirHold) { - // 解析颜色数据。目前只解析、不使用。 - _ = code.Last(); // var colorChar 颜色标记 N/I + var colorChar = code.Last(); // 颜色标记 N/I + note.Tag = AirColor[colorChar.ToString()]; } bool foundFirst = false; @@ -384,11 +390,13 @@ private static int ParseSlideNote(bool isAirSlide, string[] lines, int idx, stri previousNote.EndCell = previousNote.Cell; previousNote.EndWidth = previousNote.Width; + string colorTag = ""; if (isAirSlide) { - // 解析高度和颜色数据。目前只解析、不使用。 + var colorChar = code.Last(); // 颜色标记 N/I + colorTag = AirColor[colorChar.ToString()]; + // 解析高度数据。目前只解析、不使用。 TryParseUgcBase36Int2(code.AsSpan(3, code.Length - 4), out _); // out var startHeight 起始的高度值 - _ = code.Last(); // var colorChar 颜色标记 N/I } bool foundFirst = false; @@ -412,6 +420,8 @@ private static int ParseSlideNote(bool isAirSlide, string[] lines, int idx, stri EndCell = endCell, EndWidth = endWidth, Previous = foundFirst ? previousNote : null, }; + if (isAirSlide) note.Tag = colorTag; + chart.Notes.Add(note); previousNote = note; idx++; diff --git a/tests/chu/ChuTests.cs b/tests/chu/ChuTests.cs index d6ed774..705f9be 100644 --- a/tests/chu/ChuTests.cs +++ b/tests/chu/ChuTests.cs @@ -55,6 +55,7 @@ private static string SnapshotNote(ChuNote note) { Rational r => r.CanonicalForm.ToString(), List list => string.Join(",", list), + string s => s == "DEF" ? "" : s, null => "", _ => v.ToString() ?? "", }; From d580e85243ae02b3994c72029cad7f6c99adbb40 Mon Sep 17 00:00:00 2001 From: Starrah Date: Fri, 8 May 2026 02:31:55 +0800 Subject: [PATCH 13/13] =?UTF-8?q?[F]=20=E4=BF=AE=E4=B8=80=E4=BA=9B?= =?UTF-8?q?=E5=B0=8F=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 ++-- parser/chu/C2sParser.cs | 5 +++-- parser/chu/UgcParser.cs | 4 ++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index e8cf80b..19788f0 100644 --- a/README.md +++ b/README.md @@ -160,13 +160,13 @@ return maidataText; // maidataText即为转谱结果 var (c2sChart, alerts) = new C2sParser().Parse(c2sText); // 解析 C2S 谱面字符串 var (ugcChart, alerts) = new UgcParser().Parse(ugcText); // 解析 UGC 谱面字符串 var (susChart, alerts) = new SusParser().Parse(susText); // 解析 SUS 谱面字符串 -// 以上得到的c2sChart、ugcChart、susChart,都是IChuChart类型的谱面表示对象; +// 以上得到的c2sChart、ugcChart、susChart,都是ChuChart类型的谱面表示对象; // alerts是解析过程中可能产生的警告信息等,建议打印出来。 var (c2sText, alerts) = new C2sGenerator().Generate(ugcChart); // UGC -> C2S var (ugcText, alerts) = new UgcGenerator().Generate(c2sChart); // C2S -> UGC var (susText, alerts) = new SusGenerator().Generate(c2sChart); // C2S -> SUS -// 各种Generator的Generate方法,均接受任意的IChuChart对象。 +// 各种Generator的Generate方法,均接受 ChuChart(可将任一 Parser 产出的 ChuChart 互相传入)。 // 同上,alerts是生成过程中可能产生的警告信息等,建议打印出来。 ``` diff --git a/parser/chu/C2sParser.cs b/parser/chu/C2sParser.cs index aa5a460..e10cdbb 100644 --- a/parser/chu/C2sParser.cs +++ b/parser/chu/C2sParser.cs @@ -76,7 +76,8 @@ private static void ParseTiming(string[] p, ChuChart chart) switch (tag) { case "BPM": - chart.BpmList.Add(new BPM(Int(p, 1) + new Rational(Int(p, 2), RSL), decimal.Parse(p[3]))); + chart.BpmList.Add(new BPM(Int(p, 1) + new Rational(Int(p, 2), RSL), + decimal.Parse(p[3], CultureInfo.InvariantCulture))); break; case "MET": chart.MetList.Add(new MET(Int(p, 1) + new Rational(Int(p, 2), RSL), Int(p, 4, 4), Int(p, 3, 4))); @@ -85,7 +86,7 @@ private static void ParseTiming(string[] p, ChuChart chart) chart.SflList.Add(( Int(p, 1) + new Rational(Int(p, 2), RSL), new Rational(Int(p, 3), RSL), - decimal.Parse(p[4]))); + decimal.Parse(p[4], CultureInfo.InvariantCulture))); break; } } diff --git a/parser/chu/UgcParser.cs b/parser/chu/UgcParser.cs index e67299d..0b78c4d 100644 --- a/parser/chu/UgcParser.cs +++ b/parser/chu/UgcParser.cs @@ -358,7 +358,7 @@ private static int ParseHoldNote(bool isAirHold, string[] lines, int idx, string if (isAirHold) { var colorChar = code.Last(); // 颜色标记 N/I - note.Tag = AirColor[colorChar.ToString()]; + note.Tag = AirColor.GetValueOrDefault(colorChar.ToString(), ""); } bool foundFirst = false; @@ -394,7 +394,7 @@ private static int ParseSlideNote(bool isAirSlide, string[] lines, int idx, stri if (isAirSlide) { var colorChar = code.Last(); // 颜色标记 N/I - colorTag = AirColor[colorChar.ToString()]; + colorTag = AirColor.GetValueOrDefault(colorChar.ToString(), ""); // 解析高度数据。目前只解析、不使用。 TryParseUgcBase36Int2(code.AsSpan(3, code.Length - 4), out _); // out var startHeight 起始的高度值 }