├── .gitignore
├── Aff2Preview.sln
├── Aff2Preview
├── AffTools.csproj
├── AffTools
│ ├── Aff2Preview
│ │ └── AffRenderer.cs
│ ├── AffAnalyzer
│ │ ├── Analyzer.cs
│ │ └── Note.cs
│ └── AffReader
│ │ ├── AffReader.cs
│ │ ├── AffStringParser.cs
│ │ ├── ArcAlgorithm.cs
│ │ └── ArcaeaFileFormat.cs
├── MyGraphics
│ ├── GdiPlusAdapter.cs
│ ├── GraphicsAdapter.cs
│ └── SkiaAdapter.cs
└── Program.cs
├── LICENSE
├── README.md
├── assets
├── 2.aff
├── arc_body.png
├── base.jpg
├── note.png
└── note_hold.png
└── output.jpg
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.rsuser
8 | *.suo
9 | *.user
10 | *.userosscache
11 | *.sln.docstates
12 |
13 | # User-specific files (MonoDevelop/Xamarin Studio)
14 | *.userprefs
15 |
16 | # Mono auto generated files
17 | mono_crash.*
18 |
19 | # Build results
20 | [Dd]ebug/
21 | [Dd]ebugPublic/
22 | [Rr]elease/
23 | [Rr]eleases/
24 | x64/
25 | x86/
26 | [Aa][Rr][Mm]/
27 | [Aa][Rr][Mm]64/
28 | bld/
29 | [Bb]in/
30 | [Oo]bj/
31 | [Ll]og/
32 | [Ll]ogs/
33 |
34 | # Visual Studio 2015/2017 cache/options directory
35 | .vs/
36 | # Uncomment if you have tasks that create the project's static files in wwwroot
37 | #wwwroot/
38 |
39 | # Visual Studio 2017 auto generated files
40 | Generated\ Files/
41 |
42 | # MSTest test Results
43 | [Tt]est[Rr]esult*/
44 | [Bb]uild[Ll]og.*
45 |
46 | # NUnit
47 | *.VisualState.xml
48 | TestResult.xml
49 | nunit-*.xml
50 |
51 | # Build Results of an ATL Project
52 | [Dd]ebugPS/
53 | [Rr]eleasePS/
54 | dlldata.c
55 |
56 | # Benchmark Results
57 | BenchmarkDotNet.Artifacts/
58 |
59 | # .NET Core
60 | project.lock.json
61 | project.fragment.lock.json
62 | artifacts/
63 |
64 | # StyleCop
65 | StyleCopReport.xml
66 |
67 | # Files built by Visual Studio
68 | *_i.c
69 | *_p.c
70 | *_h.h
71 | *.ilk
72 | *.meta
73 | *.obj
74 | *.iobj
75 | *.pch
76 | *.pdb
77 | *.ipdb
78 | *.pgc
79 | *.pgd
80 | *.rsp
81 | *.sbr
82 | *.tlb
83 | *.tli
84 | *.tlh
85 | *.tmp
86 | *.tmp_proj
87 | *_wpftmp.csproj
88 | *.log
89 | *.vspscc
90 | *.vssscc
91 | .builds
92 | *.pidb
93 | *.svclog
94 | *.scc
95 |
96 | # Chutzpah Test files
97 | _Chutzpah*
98 |
99 | # Visual C++ cache files
100 | ipch/
101 | *.aps
102 | *.ncb
103 | *.opendb
104 | *.opensdf
105 | *.sdf
106 | *.cachefile
107 | *.VC.db
108 | *.VC.VC.opendb
109 |
110 | # Visual Studio profiler
111 | *.psess
112 | *.vsp
113 | *.vspx
114 | *.sap
115 |
116 | # Visual Studio Trace Files
117 | *.e2e
118 |
119 | # TFS 2012 Local Workspace
120 | $tf/
121 |
122 | # Guidance Automation Toolkit
123 | *.gpState
124 |
125 | # ReSharper is a .NET coding add-in
126 | _ReSharper*/
127 | *.[Rr]e[Ss]harper
128 | *.DotSettings.user
129 |
130 | # TeamCity is a build add-in
131 | _TeamCity*
132 |
133 | # DotCover is a Code Coverage Tool
134 | *.dotCover
135 |
136 | # AxoCover is a Code Coverage Tool
137 | .axoCover/*
138 | !.axoCover/settings.json
139 |
140 | # Visual Studio code coverage results
141 | *.coverage
142 | *.coveragexml
143 |
144 | # NCrunch
145 | _NCrunch_*
146 | .*crunch*.local.xml
147 | nCrunchTemp_*
148 |
149 | # MightyMoose
150 | *.mm.*
151 | AutoTest.Net/
152 |
153 | # Web workbench (sass)
154 | .sass-cache/
155 |
156 | # Installshield output folder
157 | [Ee]xpress/
158 |
159 | # DocProject is a documentation generator add-in
160 | DocProject/buildhelp/
161 | DocProject/Help/*.HxT
162 | DocProject/Help/*.HxC
163 | DocProject/Help/*.hhc
164 | DocProject/Help/*.hhk
165 | DocProject/Help/*.hhp
166 | DocProject/Help/Html2
167 | DocProject/Help/html
168 |
169 | # Click-Once directory
170 | publish/
171 |
172 | # Publish Web Output
173 | *.[Pp]ublish.xml
174 | *.azurePubxml
175 | # Note: Comment the next line if you want to checkin your web deploy settings,
176 | # but database connection strings (with potential passwords) will be unencrypted
177 | *.pubxml
178 | *.publishproj
179 |
180 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
181 | # checkin your Azure Web App publish settings, but sensitive information contained
182 | # in these scripts will be unencrypted
183 | PublishScripts/
184 |
185 | # NuGet Packages
186 | *.nupkg
187 | # NuGet Symbol Packages
188 | *.snupkg
189 | # The packages folder can be ignored because of Package Restore
190 | **/[Pp]ackages/*
191 | # except build/, which is used as an MSBuild target.
192 | !**/[Pp]ackages/build/
193 | # Uncomment if necessary however generally it will be regenerated when needed
194 | #!**/[Pp]ackages/repositories.config
195 | # NuGet v3's project.json files produces more ignorable files
196 | *.nuget.props
197 | *.nuget.targets
198 |
199 | # Microsoft Azure Build Output
200 | csx/
201 | *.build.csdef
202 |
203 | # Microsoft Azure Emulator
204 | ecf/
205 | rcf/
206 |
207 | # Windows Store app package directories and files
208 | AppPackages/
209 | BundleArtifacts/
210 | Package.StoreAssociation.xml
211 | _pkginfo.txt
212 | *.appx
213 | *.appxbundle
214 | *.appxupload
215 |
216 | # Visual Studio cache files
217 | # files ending in .cache can be ignored
218 | *.[Cc]ache
219 | # but keep track of directories ending in .cache
220 | !?*.[Cc]ache/
221 |
222 | # Others
223 | ClientBin/
224 | ~$*
225 | *~
226 | *.dbmdl
227 | *.dbproj.schemaview
228 | *.jfm
229 | *.pfx
230 | *.publishsettings
231 | orleans.codegen.cs
232 |
233 | # Including strong name files can present a security risk
234 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
235 | #*.snk
236 |
237 | # Since there are multiple workflows, uncomment next line to ignore bower_components
238 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
239 | #bower_components/
240 |
241 | # RIA/Silverlight projects
242 | Generated_Code/
243 |
244 | # Backup & report files from converting an old project file
245 | # to a newer Visual Studio version. Backup files are not needed,
246 | # because we have git ;-)
247 | _UpgradeReport_Files/
248 | Backup*/
249 | UpgradeLog*.XML
250 | UpgradeLog*.htm
251 | ServiceFabricBackup/
252 | *.rptproj.bak
253 |
254 | # SQL Server files
255 | *.mdf
256 | *.ldf
257 | *.ndf
258 |
259 | # Business Intelligence projects
260 | *.rdl.data
261 | *.bim.layout
262 | *.bim_*.settings
263 | *.rptproj.rsuser
264 | *- [Bb]ackup.rdl
265 | *- [Bb]ackup ([0-9]).rdl
266 | *- [Bb]ackup ([0-9][0-9]).rdl
267 |
268 | # Microsoft Fakes
269 | FakesAssemblies/
270 |
271 | # GhostDoc plugin setting file
272 | *.GhostDoc.xml
273 |
274 | # Node.js Tools for Visual Studio
275 | .ntvs_analysis.dat
276 | node_modules/
277 |
278 | # Visual Studio 6 build log
279 | *.plg
280 |
281 | # Visual Studio 6 workspace options file
282 | *.opt
283 |
284 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
285 | *.vbw
286 |
287 | # Visual Studio LightSwitch build output
288 | **/*.HTMLClient/GeneratedArtifacts
289 | **/*.DesktopClient/GeneratedArtifacts
290 | **/*.DesktopClient/ModelManifest.xml
291 | **/*.Server/GeneratedArtifacts
292 | **/*.Server/ModelManifest.xml
293 | _Pvt_Extensions
294 |
295 | # Paket dependency manager
296 | .paket/paket.exe
297 | paket-files/
298 |
299 | # FAKE - F# Make
300 | .fake/
301 |
302 | # CodeRush personal settings
303 | .cr/personal
304 |
305 | # Python Tools for Visual Studio (PTVS)
306 | __pycache__/
307 | *.pyc
308 |
309 | # Cake - Uncomment if you are using it
310 | # tools/**
311 | # !tools/packages.config
312 |
313 | # Tabs Studio
314 | *.tss
315 |
316 | # Telerik's JustMock configuration file
317 | *.jmconfig
318 |
319 | # BizTalk build output
320 | *.btp.cs
321 | *.btm.cs
322 | *.odx.cs
323 | *.xsd.cs
324 |
325 | # OpenCover UI analysis results
326 | OpenCover/
327 |
328 | # Azure Stream Analytics local run output
329 | ASALocalRun/
330 |
331 | # MSBuild Binary and Structured Log
332 | *.binlog
333 |
334 | # NVidia Nsight GPU debugger configuration file
335 | *.nvuser
336 |
337 | # MFractors (Xamarin productivity tool) working folder
338 | .mfractor/
339 |
340 | # Local History for Visual Studio
341 | .localhistory/
342 |
343 | # BeatPulse healthcheck temp database
344 | healthchecksdb
345 |
346 | # Backup folder for Package Reference Convert tool in Visual Studio 2017
347 | MigrationBackup/
348 |
349 | # Ionide (cross platform F# VS Code tools) working folder
350 | .ionide/
351 |
--------------------------------------------------------------------------------
/Aff2Preview.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.1.32319.34
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AffTools", "Aff2Preview\AffTools.csproj", "{EDFDBF2E-7FDB-4A72-941B-4951E71CF069}"
7 | EndProject
8 | Global
9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
10 | Debug|Any CPU = Debug|Any CPU
11 | Release|Any CPU = Release|Any CPU
12 | EndGlobalSection
13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
14 | {EDFDBF2E-7FDB-4A72-941B-4951E71CF069}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
15 | {EDFDBF2E-7FDB-4A72-941B-4951E71CF069}.Debug|Any CPU.Build.0 = Debug|Any CPU
16 | {EDFDBF2E-7FDB-4A72-941B-4951E71CF069}.Release|Any CPU.ActiveCfg = Release|Any CPU
17 | {EDFDBF2E-7FDB-4A72-941B-4951E71CF069}.Release|Any CPU.Build.0 = Release|Any CPU
18 | EndGlobalSection
19 | GlobalSection(SolutionProperties) = preSolution
20 | HideSolutionNode = FALSE
21 | EndGlobalSection
22 | GlobalSection(ExtensibilityGlobals) = postSolution
23 | SolutionGuid = {13BA9946-ABA4-4971-8825-F0B7E6DB8709}
24 | EndGlobalSection
25 | EndGlobal
26 |
--------------------------------------------------------------------------------
/Aff2Preview/AffTools.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net6.0
6 | enable
7 | enable
8 |
9 |
10 |
11 | False
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/Aff2Preview/AffTools/Aff2Preview/AffRenderer.cs:
--------------------------------------------------------------------------------
1 | using System.Numerics;
2 | using System.Text.RegularExpressions;
3 |
4 | using AffTools.AffReader;
5 | using AffTools.MyGraphics;
6 |
7 | namespace AffTools.Aff2Preview;
8 |
9 | internal class AffRenderer
10 | {
11 | private abstract class DrawObjectBase
12 | {
13 | public Vector3 Location { get; init; }
14 | public string? Property { get; init; }
15 | public bool IsEnwiden { get; init; }
16 | public abstract void Draw(GraphicsAdapter g);
17 | }
18 |
19 | private class ArcSegment : DrawObjectBase
20 | {
21 | public Vector3 End { get; init; }
22 | public int ColorId { get; init; }
23 | public bool IsVoid { get; init; }
24 | public override void Draw(GraphicsAdapter g)
25 | {
26 | float drawX = IsEnwiden ? Location.X * 4 / 6 + 0.25f * Config.DrawingTrackWidth * 4 / 6 : Location.X;
27 | float endX = IsEnwiden ? End.X * 4 / 6 + 0.25f * Config.DrawingTrackWidth * 4 / 6 : End.X;
28 | ColorDesc color = new();
29 | float w;
30 | if (IsVoid)
31 | {
32 | w = Config.ArcVoidWidth;
33 | color.SetColor(Config.GetArcVoidColor());
34 | }
35 | else
36 | {
37 | w = Config.ArcWidth;
38 | color.SetColor(Config.GetArcColor(ColorId));
39 | }
40 | color.SetColorA((byte)Math.Clamp((int)(47 + (230 - 47) * Location.Y), 0, 255));
41 | g.SetColor(color);
42 | g.DrawLine(w, drawX, Config.TimingToY(Location.Z), endX, Config.TimingToY(End.Z));
43 | }
44 | }
45 |
46 | private class ArcTap : DrawObjectBase
47 | {
48 | public override void Draw(GraphicsAdapter g)
49 | {
50 | var airTap = Property switch
51 | {
52 | "voice_wav" => Config.Side == 1 ? Config.SfxDTap : Config.SfxLTap,
53 | "glass_wav" => Config.Side == 1 ? Config.SfxDTap : Config.SfxLTap,
54 | _ => Config.AirTap
55 | };
56 | int drawX = IsEnwiden ? (int)(Location.X * 4 / 6 + Config.EnwidenTrackWidth / 2 + 3) :
57 | (int)(Location.X - Config.SingleTrackWidth / 2 + 1);
58 |
59 | g.DrawImageScaled(
60 | airTap,
61 | drawX,
62 | (int)(Config.TimingToY(Location.Z) - Config.SkyNoteHeight / 4) - (IsEnwiden ? 1 : 0),
63 | IsEnwiden ? Config.EnwidenTrackWidth - 2 : Config.SingleTrackWidth - 2,
64 | Config.SkyNoteHeight / 4,
65 | Math.Clamp(0.3f + Location.Y * 0.7f, 0, 1)
66 | );
67 | }
68 | }
69 |
70 | private class ConnectLine : DrawObjectBase
71 | {
72 | public Vector3 End { get; init; }
73 | public override void Draw(GraphicsAdapter g)
74 | {
75 | g.DrawLine(
76 | ColorDesc.FromArgb(Config.GetConnectLineColor()),
77 | 2f,
78 | Location.X, Config.TimingToY(Location.Z),
79 | End.X, Config.TimingToY(Location.Z));
80 | }
81 | }
82 |
83 | private class TextObject : DrawObjectBase
84 | {
85 | public string Text { get; set; } = "";
86 | public FontDesc Font { get; set; }
87 | public ColorDesc Color { get; set; }
88 | public override void Draw(GraphicsAdapter g)
89 | {
90 | g.DrawString(Text, Color, Font, Location.X, Config.TimingToY(Location.Z));
91 | }
92 | }
93 |
94 | internal class ChartConfig
95 | {
96 | public ImageDesc Background = new GdiImage();
97 | public ImageDesc Cover = new GdiImage();
98 |
99 | public ImageDesc Tap = new GdiImage();
100 | public ImageDesc Hold = new GdiImage();
101 | public ImageDesc AirTap = new GdiImage();
102 |
103 | public readonly ImageDesc SfxDTap = new GdiImage();
104 | public readonly ImageDesc SfxLTap = new GdiImage();
105 |
106 | public int NoteHeight { get; set; } = 64;
107 | public int SkyNoteHeight { get; set; } = 61;
108 | public float ArcWidth { get; set; } = 20f;
109 | public float ArcVoidWidth { get; set; } = 3f;
110 |
111 | public int TotalTrackWidth { get; set; } = 248;
112 | public int DrawingTrackWidth => TotalTrackWidth - 4;
113 | public int SingleTrackWidth => DrawingTrackWidth / 4;
114 | public int EnwidenTrackWidth => DrawingTrackWidth / 6;
115 |
116 | public int TimingScale { get; set; } = 5;
117 |
118 | public int Cols { get; set; } = 1;
119 | public int ColWidth { get; set; } = 0;
120 | public int Rows { get; set; } = 1;
121 | public float SegmentLengthInBaseBpm { get; set; } = 0;
122 |
123 | ///
124 | /// 0:hikari 1:conflict 2:finale
125 | ///
126 | public int Side { get; set; } = 0;
127 |
128 | public uint TrackColor
129 | => Side switch
130 | {
131 | 0 => 0xffffffff,
132 | 1 => 0xff382a47,
133 | 2 => 0xffffffff,
134 | _ => 0
135 | };
136 |
137 | public uint TrackLineColor
138 | => Side switch
139 | {
140 | 0 => 0xffd3d3d3,
141 | 1 => 0xff2e1f3c,
142 | 2 => 0xffd3d3d3,
143 | _ => 0
144 | };
145 |
146 | public uint TrackSegmentLineColor
147 | => Side switch
148 | {
149 | 0 => 0xa0d3d3d3,
150 | 1 => 0xc02e1f3c,
151 | 2 => 0xa0d3d3d3,
152 | _ => 0
153 | };
154 |
155 | public uint TrackStripColor
156 | => Side switch
157 | {
158 | 0 => 0x0f808080,
159 | 1 => 0x08f0f8ff,
160 | 2 => 0x0f808080,
161 | _ => 0
162 | };
163 |
164 | public uint GetArcVoidColor()
165 | => Side switch
166 | {
167 | 0 => 0xffd3d3d3,
168 | 1 => 0xffa9a9a9,
169 | 2 => 0xffd3d3d3,
170 | _ => 0,
171 | };
172 |
173 | public uint GetArcColor(int type)
174 | => Side switch
175 | {
176 | 0 => type switch
177 | {
178 | 0 => 0xff31dae7,
179 | 1 => 0xffff69b4,
180 | _ => 0,
181 | },
182 | 1 => type switch
183 | {
184 | 0 => 0xff00ced1,
185 | 1 => 0xffff1493,
186 | _ => 0,
187 | },
188 | 2 => type switch
189 | {
190 | 0 => 0xff31dae7,
191 | 1 => 0xffff69b4,
192 | _ => 0,
193 | },
194 | _ => 0xff31dae7,
195 | };
196 |
197 | public uint GetConnectLineColor()
198 | => Side switch
199 | {
200 | 0 => 0xdc90ee90,
201 | 1 => 0xdcff1493,
202 | 2 => 0xdc90ee90,
203 | _ => 0,
204 | };
205 |
206 | public int TotalTrackLength { get; set; } = 0;
207 |
208 | public float TimingToY(float timing)
209 | => (TotalTrackLength - timing) / TimingScale - 3;
210 | }
211 |
212 | private readonly ArcaeaAffReader _affReader = new();
213 |
214 | public static ChartConfig Config = new();
215 |
216 | private AffAnalyzer.Analyzer _affAnalyzer;
217 |
218 | public AffRenderer(string affFile)
219 | {
220 | AffFile = affFile;
221 | }
222 |
223 | public string AffFile { get; set; } = "";
224 | public string Title { get; set; } = "";
225 | public float Rating { get; set; } = 0f;
226 | public int Notes { get; set; } = 0;
227 | public int Side { get; set; } = 0;
228 | public float ChartBpm { get; set; } = 0f;
229 | public string Artist { get; set; } = "";
230 | public string Charter { get; set; } = "";
231 | public int Difficulty { get; set; } = 0;
232 | public bool IsMirror { get; set; } = false;
233 | private List<(int, int)> Interval4K { get; set; } = new();
234 | public string DiffStr => Difficulty switch
235 | {
236 | 0 => "Past",
237 | 1 => "Present",
238 | 2 => "Future",
239 | 3 => "Beyond",
240 | _ => ""
241 | };
242 |
243 | public void LoadResource(string tap, string hold, string airTap, string bg, string cover)
244 | {
245 | Config.Background.FromFile(bg);
246 |
247 | Config.Cover.FromFile(cover);
248 |
249 | Config.Tap.FromFile(tap);
250 | Config.Hold.FromFile(hold);
251 | Config.AirTap.FromFile(airTap);
252 | }
253 |
254 | private void MirrorAff()
255 | {
256 | foreach (var affEvent in _affReader.Events)
257 | {
258 | switch (affEvent)
259 | {
260 | case ArcaeaAffTap tap:
261 | tap.Track = 5 - tap.Track;
262 | break;
263 | case ArcaeaAffHold hold:
264 | hold.Track = 5 - hold.Track;
265 | break;
266 | case ArcaeaAffArc arc:
267 | arc.XStart = 1f - arc.XStart;
268 | arc.XEnd = 1f - arc.XEnd;
269 | arc.Color = 1 - arc.Color;
270 | break;
271 | }
272 | }
273 | }
274 |
275 | private void LoadAff()
276 | {
277 | _affReader.Parse(AffFile);
278 |
279 | _affAnalyzer = new(_affReader);
280 | _affAnalyzer.AnalyzeSegments();
281 | _affAnalyzer.AnalyzeNotes();
282 |
283 | Config.TotalTrackLength = (int)_affAnalyzer.totalTime;
284 | }
285 |
286 | public ImageDesc Draw()
287 | {
288 | Config.Side = Side;
289 |
290 | LoadAff();
291 | if (IsMirror) MirrorAff();
292 |
293 | _affAnalyzer.CalcNotes();
294 | Interval4K = _affAnalyzer.Get4LaneInterval(Config.TotalTrackLength);
295 |
296 | var trackImg = DrawTrackObjects();
297 |
298 | float segmentLengthInBaseBpm = _affAnalyzer.baseTimePerSegment / Config.TimingScale;
299 | int rows = _affAnalyzer.segmentCountInBaseBpm;
300 | int cols = 1;
301 | int colWidth = Config.TotalTrackWidth + 75;
302 |
303 | for (; rows > 0; rows--)
304 | {
305 | cols = _affAnalyzer.segmentCountInBaseBpm / rows + (_affAnalyzer.segmentCountInBaseBpm % rows == 0 ? 0 : 1);
306 | double w = cols * colWidth;
307 | double h = rows * segmentLengthInBaseBpm;
308 | if (w / h >= 4f / 3f)
309 | break;
310 | }
311 |
312 | Config.Cols = cols;
313 | Config.Rows = rows;
314 | Config.ColWidth = colWidth;
315 | Config.SegmentLengthInBaseBpm = segmentLengthInBaseBpm;
316 |
317 | int outputWidth = Config.Cols * colWidth + 100;
318 | int outputHeight = Config.Rows * (int)segmentLengthInBaseBpm + 200 + 25 + 25;
319 |
320 | ColorDesc? rectColor = ColorDesc.FromArgb(Config.Side switch
321 | {
322 | 0 => 0xc8f0f0f0,
323 | 1 => 0xc8202020,
324 | 2 => 0xc8f0f0f0,
325 | });
326 |
327 | GraphicsAdapter g = new GdiPlusAdapter();
328 | g.BeginContext(outputWidth, outputHeight);
329 | g.Fill(rectColor);
330 |
331 | int bw = Config.Background.GetWidth();
332 | int bh = Config.Background.GetHeight();
333 | if ((float)outputWidth / bw > (float)outputHeight / bh)
334 | {
335 | int dw = outputWidth;
336 | float rate = (float)dw / bw;
337 | float dh = bh * rate;
338 | g.DrawImageScaled(Config.Background, 0, -Math.Abs(outputHeight - dh) / 2, dw, dh);
339 | }
340 | else
341 | {
342 | int dh = outputHeight;
343 | float rate = (float)dh / bh;
344 | float dw = bw * rate;
345 | g.DrawImageScaled(Config.Background, -Math.Abs(outputWidth - dw) / 2, 0, dw, dh);
346 | }
347 |
348 | g.FillRectangle(rectColor,
349 | 25, 25, Config.Cols * Config.ColWidth + 50, Config.Rows * (int)Config.SegmentLengthInBaseBpm + 50 + 25);
350 |
351 | for (int x = 0; x < Config.Cols; x++)
352 | {
353 | double y = trackImg.GetHeight() - (x + 1) * Config.Rows * Config.SegmentLengthInBaseBpm;
354 | g.DrawImageCliped(trackImg, x * colWidth + colWidth - Config.TotalTrackWidth + 50, 50,
355 | 0, (int)y, Config.TotalTrackWidth, (int)(Config.Rows * Config.SegmentLengthInBaseBpm));
356 | }
357 |
358 | DrawComboNumber(g);
359 | //DrawSegmentNumber(g);
360 | DrawSegmentBpm(g);
361 | DrawNoteLength(g);
362 |
363 | DrawFooter(g);
364 |
365 | return g.EndContext();
366 | }
367 |
368 | public void DrawTrack(GraphicsAdapter g)
369 | {
370 | foreach (var (initial, end) in Interval4K)
371 | {
372 | for (int i = 0; i < 5; i++)
373 | {
374 | g.DrawLine(ColorDesc.FromArgb(Config.TrackLineColor), i is > 0 and < 4 ? 2f : 4f,
375 | Config.DrawingTrackWidth / 4 * i + 2, (Config.TotalTrackLength - initial) / Config.TimingScale,
376 | Config.DrawingTrackWidth / 4 * i + 2, (Config.TotalTrackLength - end) / Config.TimingScale);
377 | }
378 | }
379 |
380 | var widen = _affAnalyzer.GetPairEnwidenLanes();
381 | foreach (var (initial, end) in widen)
382 | {
383 | for (int i = 0; i < 7; i++)
384 | {
385 | g.DrawLine(ColorDesc.FromArgb(Config.TrackLineColor), i is > 0 and < 6 ? 2f : 4f,
386 | Config.DrawingTrackWidth * i / 6 + 2, (Config.TotalTrackLength - initial.Timing) / Config.TimingScale,
387 | Config.DrawingTrackWidth * i / 6 + 2, (Config.TotalTrackLength - end.Timing) / Config.TimingScale);
388 | }
389 | }
390 |
391 | g.SetColor(ColorDesc.FromArgb(Config.TrackSegmentLineColor));
392 | foreach (float t in _affAnalyzer.SegmentTimings)
393 | {
394 | if (t >= 0)
395 | g.DrawLine(3f,
396 | 0, Config.TimingToY(t),
397 | Config.TotalTrackWidth, Config.TimingToY(t));
398 | }
399 |
400 | g.SetColor(ColorDesc.FromArgb(Config.TrackStripColor));
401 | for (int i = 0; i < Config.TotalTrackLength; i += 45)
402 | {
403 | g.DrawLine(57f, -20, i, 300, i - 200);
404 | }
405 |
406 | foreach (var note in _affAnalyzer.Notes)
407 | {
408 | if (note.TimePoint > Config.TotalTrackLength)
409 | break;
410 |
411 | float density = note.InvDuration - 6;
412 |
413 | if (density < 0) continue;
414 |
415 | density = density * density * 0.5f;
416 |
417 | g.SetColor(ColorDesc.FromArgb((byte)density, 255, 0, 0));
418 | g.FillRectangle(ColorDesc.FromArgb((byte)density, 255, 0, 0),
419 | 0, Config.TimingToY(note.TimePoint + note.Duration) - 1,
420 | Config.TotalTrackWidth, (note.Duration + 3) / Config.TimingScale);
421 | }
422 | }
423 |
424 | public ImageDesc DrawTrackObjects()
425 | {
426 | GraphicsAdapter g = new GdiPlusAdapter();
427 | g.BeginContext(Config.TotalTrackWidth, Config.TotalTrackLength / Config.TimingScale);
428 |
429 | g.Fill(ColorDesc.FromArgb(Config.TrackColor));
430 |
431 | DrawTrack(g);
432 | DrawFloorNotes(g);
433 | DrawAirObjects(g);
434 |
435 | return g.EndContext();
436 | }
437 |
438 | void DrawFloorNotes(GraphicsAdapter g)
439 | {
440 | foreach (var ev in _affReader.Events)
441 | {
442 | if (ev is ArcaeaAffTap tap)
443 | {
444 | if (Interval4K.Any(x => tap.Timing > x.Item1 && tap.Timing < x.Item2))
445 | {
446 | float x = Config.DrawingTrackWidth * (tap.Track - 1) / 4;
447 | float y = Config.TimingToY(tap.Timing);
448 | g.DrawImageScaled(Config.Tap,
449 | x + 3, y - Config.NoteHeight / 4,
450 | Config.SingleTrackWidth - 2, Config.NoteHeight / 4, 0);
451 | }
452 | else
453 | {
454 | float x = Config.DrawingTrackWidth * (tap.Track) / 6;
455 | float y = Config.TimingToY(tap.Timing);
456 | g.DrawImageScaled(Config.Tap,
457 | x + 3, y - Config.NoteHeight / 4,
458 | Config.EnwidenTrackWidth - 2, Config.NoteHeight / 4, 0);
459 | }
460 | }
461 | else if (ev is ArcaeaAffHold hold)
462 | {
463 | if (Interval4K.Any(x => hold.Timing > x.Item1 && hold.Timing < x.Item2))
464 | {
465 | float x = Config.DrawingTrackWidth * (hold.Track - 1) / 4;
466 | float ys = Config.TimingToY(hold.Timing);
467 | float ye = Config.TimingToY(hold.EndTiming);
468 | g.DrawImageScaled(Config.Hold, x + 3, ye, Config.SingleTrackWidth - 2, ys - ye, 0);
469 | }
470 | else
471 | {
472 | float x = Config.DrawingTrackWidth * (hold.Track) / 6;
473 | float ys = Config.TimingToY(hold.Timing);
474 | float ye = Config.TimingToY(hold.EndTiming);
475 | g.DrawImageScaled(Config.Hold, x + 3, ye, Config.EnwidenTrackWidth - 2, ys - ye, 0);
476 | }
477 | }
478 | }
479 | }
480 |
481 | public void DrawAirObjects(GraphicsAdapter g)
482 | {
483 | List airObjects = new();
484 | List airTaps = new();
485 |
486 | var AddDoubleTip = (float x, float z, bool isEnwiden) =>
487 | {
488 | int sw = isEnwiden ? Config.EnwidenTrackWidth : Config.SingleTrackWidth;
489 | float sx = x <= Config.DrawingTrackWidth / 2 ? x + sw / 2 + 5 : x - sw / 2 - 20;
490 | airObjects.Add(new TextObject()
491 | {
492 | Text = "x2",
493 | Location = new Vector3(sx, 1.5f, z + 80),
494 | Color = Config.Side == 1 ? ColorDesc.FromArgb(0xddffffff) : ColorDesc.FromArgb(0xff000000),
495 | Font = new FontDesc("exo", 10f, FontDescStyle.Bold)
496 | });
497 | };
498 |
499 | foreach (var ev in _affReader.Events)
500 | {
501 | if (ev is not ArcaeaAffArc t) continue;
502 |
503 | int duration = t.EndTiming - t.Timing;
504 | bool isEnwiden = !Interval4K.Any(inv => t.Timing > inv.Item1 && t.Timing < inv.Item2);
505 |
506 | int segSize = duration / (duration < 1000 ? 14 : 7);
507 | int segmentCount = (segSize == 0 ? 0 : duration / segSize) + 1;
508 |
509 | List segments = new();
510 |
511 | Vector3 start = new();
512 | Vector3 end = new((t.XStart + 0.5f) * Config.DrawingTrackWidth / 2 + 3, t.YStart, t.Timing);
513 | segments.Add(end);
514 |
515 | for (int i = 0; i < segmentCount - 1; i++)
516 | {
517 | start = end;
518 | float x = ArcAlgorithm.X(t.XStart, t.XEnd, (i + 1f) * segSize / duration, ArcaeaAffArc.ToArcLineType(t.LineType));
519 | float y = ArcAlgorithm.Y(t.YStart, t.YEnd, (i + 1f) * segSize / duration, ArcaeaAffArc.ToArcLineType(t.LineType));
520 | end = new Vector3((x + 0.5f) * Config.DrawingTrackWidth / 2 + 3,
521 | y,
522 | t.Timing + segSize * (i + 1));
523 | segments.Add(end);
524 | }
525 |
526 | // last segment
527 | {
528 | start = end;
529 | end = new Vector3((t.XEnd + 0.5f) * Config.DrawingTrackWidth / 2 + 3,
530 | t.YEnd,
531 | t.EndTiming);
532 | segments.Add(end);
533 | }
534 |
535 | for (int i = 0; i < segments.Count - 1; i++)
536 | {
537 | var st = segments[i];
538 | var ed = segments[i + 1];
539 |
540 | airObjects.Add(new ArcSegment()
541 | {
542 | IsVoid = t.IsVoid,
543 | ColorId = t.Color,
544 | Location = st,
545 | End = ed,
546 | IsEnwiden = isEnwiden,
547 | });
548 | }
549 |
550 | if (t.ArcTaps is null)
551 | continue;
552 |
553 | foreach (int airTapTiming in t.ArcTaps)
554 | {
555 | float tm = airTapTiming - t.Timing;
556 | float x = ArcAlgorithm.X(t.XStart, t.XEnd, tm / duration, ArcaeaAffArc.ToArcLineType(t.LineType));
557 | float y = ArcAlgorithm.Y(t.YStart, t.YEnd, tm / duration, ArcaeaAffArc.ToArcLineType(t.LineType));
558 |
559 | bool isInEnwiden = !Interval4K.Any(inv => airTapTiming > inv.Item1 && airTapTiming < inv.Item2);
560 |
561 | x = (x + 0.5f) * Config.DrawingTrackWidth / 2;
562 |
563 | airTaps.Add(new ArcTap()
564 | {
565 | Location = new Vector3(x + 2, y, airTapTiming),
566 | Property = (ev as ArcaeaAffArc)?.Fx,
567 | IsEnwiden = isInEnwiden,
568 | });
569 |
570 | if (isEnwiden)
571 | x = x * 4 / 6 + Config.EnwidenTrackWidth + 3;
572 |
573 | // detect underneath notes
574 | foreach (var evOther in _affReader.Events)
575 | {
576 | if (evOther is ArcaeaAffTap evAt)
577 | {
578 | if (Math.Abs(evAt.Timing - airTapTiming) > 3) continue;
579 |
580 | float x_t = Config.DrawingTrackWidth * (evAt.Track - 1) / 4 + Config.SingleTrackWidth / 2;
581 |
582 | if (!Interval4K.Any(inv => evAt.Timing > inv.Item1 && evAt.Timing < inv.Item2))
583 | x_t = Config.DrawingTrackWidth * evAt.Track / 6 + Config.EnwidenTrackWidth / 2;
584 |
585 | float y_t = Config.TimingToY(evAt.Timing);
586 |
587 | airObjects.Add(new ConnectLine()
588 | {
589 | Location = new Vector3(x_t + 3, y, airTapTiming + 6),
590 | End = new Vector3(x + 3, 0, airTapTiming + 6)
591 | });
592 |
593 | if (Math.Abs(x - x_t) <= 5)
594 | AddDoubleTip(x, airTapTiming, isEnwiden);
595 | }
596 | else if (!t.Equals(evOther) && evOther is ArcaeaAffArc evArc)
597 | {
598 | if (evArc.ArcTaps is null)
599 | continue;
600 |
601 | foreach (int arcT in evArc.ArcTaps)
602 | {
603 | if (Math.Abs(arcT - airTapTiming) > 3) continue;
604 |
605 | float arc_t_x = ArcAlgorithm.X(evArc.XStart, evArc.XEnd, evArc.Timing / duration, ArcaeaAffArc.ToArcLineType(evArc.LineType));
606 | float arc_t_y = ArcAlgorithm.Y(evArc.YStart, evArc.YEnd, evArc.Timing / duration, ArcaeaAffArc.ToArcLineType(evArc.LineType));
607 | arc_t_x = (arc_t_x + 0.5f) * Config.DrawingTrackWidth / 2;
608 | if (!Interval4K.Any(inv => arcT > inv.Item1 && arcT < inv.Item2))
609 | arc_t_x = arc_t_x * 4 / 6 + Config.EnwidenTrackWidth + 3;
610 |
611 | if (Math.Abs(arc_t_x - x) <= 5 && arc_t_y < y)
612 | AddDoubleTip(x, airTapTiming, isEnwiden);
613 | }
614 |
615 | }
616 | }
617 |
618 | }
619 | }
620 |
621 | airObjects.OrderBy(x => x.Location.Y).ToList().ForEach(x => x.Draw(g));
622 | airTaps.ForEach(x => x.Draw(g));
623 | }
624 |
625 | public void DrawSegmentNumber(GraphicsAdapter g)
626 | {
627 | ColorDesc c = ColorDesc.FromArgb(Config.Side == 1 ? 0xffffffff : 0xff000000);
628 | c.SetColorA(240);
629 | g.SetColor(c);
630 | g.SetFont("exo", 10f, FontDescStyle.Bold);
631 |
632 | for (int i = 0, t = 1; i < _affAnalyzer.SegmentTimings.Count; i++)
633 | {
634 | int segmentTiming = (int)_affAnalyzer.SegmentTimings[i];
635 |
636 | if (segmentTiming > _affAnalyzer.totalTime)
637 | break;
638 |
639 | if (segmentTiming < 0)
640 | continue;
641 |
642 | int next = i + 1;
643 | if (next < _affAnalyzer.SegmentTimings.Count)
644 | {
645 | int segmentTiming2 = (int)_affAnalyzer.SegmentTimings[next];
646 | if (segmentTiming2 - segmentTiming < 100)
647 | continue;
648 | }
649 |
650 | float colHeight = Config.Rows * Config.SegmentLengthInBaseBpm;
651 | int sy = segmentTiming / Config.TimingScale;
652 |
653 | float y = colHeight - sy % colHeight;
654 | float x = Config.ColWidth * (sy / (int)colHeight);
655 | if (y <= 5)
656 | y = Config.Rows * Config.SegmentLengthInBaseBpm;
657 |
658 | g.DrawString(t.ToString(), x + Config.ColWidth - 220 - 25, y + 37);
659 | t++;
660 | }
661 | }
662 |
663 | public void DrawComboNumber(GraphicsAdapter g)
664 | {
665 | ColorDesc c = ColorDesc.FromArgb(Config.Side == 1 ? 0xffffffff : 0xff000000);
666 | c.SetColorA(240);
667 | g.SetColor(c);
668 | g.SetFont("exo", 10f, FontDescStyle.Bold);
669 |
670 | int prevCombo = -1;
671 | float endTime = _affAnalyzer.realTotalTime;
672 | int fullCombo = _affAnalyzer.Total;
673 | float colHeight = Config.Rows * Config.SegmentLengthInBaseBpm;
674 |
675 | for (int i = 0; i < _affAnalyzer.SegmentTimings.Count; i++)
676 | {
677 | int segmentTiming = (int)_affAnalyzer.SegmentTimings[i];
678 |
679 | if (segmentTiming > endTime - 10)
680 | break;
681 |
682 | if (segmentTiming < 0)
683 | continue;
684 |
685 | int next = i + 1;
686 | if (next < _affAnalyzer.SegmentTimings.Count)
687 | {
688 | int segmentTimingNext = (int)_affAnalyzer.SegmentTimings[next];
689 | if (segmentTimingNext - segmentTiming < 100)
690 | continue;
691 | }
692 |
693 | int combo = _affAnalyzer.GetCombo(segmentTiming);
694 | if (combo == prevCombo)
695 | continue;
696 |
697 | prevCombo = combo;
698 |
699 | int sy = segmentTiming / Config.TimingScale;
700 |
701 | float y = colHeight - sy % colHeight;
702 | float x = Config.ColWidth * (sy / (int)colHeight);
703 | if (y <= 5)
704 | y = Config.Rows * Config.SegmentLengthInBaseBpm;
705 |
706 | g.DrawString(combo.ToString(), x + Config.ColWidth - 220 - 29, y + 37);
707 | }
708 | {
709 | int sy = (int)endTime / Config.TimingScale;
710 |
711 | float y = colHeight - sy % colHeight;
712 | float x = Config.ColWidth * (sy / (int)colHeight);
713 | if (y <= 5)
714 | y = Config.Rows * Config.SegmentLengthInBaseBpm;
715 |
716 | g.DrawString(fullCombo.ToString(), x + Config.ColWidth - 220 - 29, y + 37);
717 | }
718 | }
719 |
720 | public void DrawSegmentBpm(GraphicsAdapter g)
721 | {
722 | ColorDesc c = ColorDesc.FromArgb(0xffff7f50);
723 |
724 | g.SetColor(c);
725 | g.SetFont("exo", 10f, FontDescStyle.Bold);
726 |
727 | float lastBpl = 0;
728 | float lastBpm = 0;
729 |
730 | foreach (var ev in _affReader.Events)
731 | {
732 | if (ev is not ArcaeaAffTiming t) continue;
733 |
734 | if (t.Timing > Config.TotalTrackLength)
735 | break;
736 |
737 | if (t.Timing == 0 && t.TimingGroup != 0)
738 | continue;
739 |
740 | if (t.Bpm is 0 or > 1000)
741 | continue;
742 |
743 | float colHeight = Config.Rows * Config.SegmentLengthInBaseBpm;
744 | float rate = t.Bpm / _affAnalyzer.baseBpm;
745 | float y = colHeight - t.Timing / Config.TimingScale % colHeight;
746 | float x = Config.ColWidth * (t.Timing / Config.TimingScale / (int)colHeight);
747 | if (y <= 5)
748 | y = (int)colHeight;
749 |
750 | if (lastBpm != t.Bpm)
751 | {
752 | g.DrawStringLayoutLTRB($"{(int)t.Bpm}",
753 | x + Config.ColWidth - 220 - 20 - 60, y + 37,
754 | x + Config.ColWidth - 220 - 20 - 6, y + 60,
755 | StringAdapterAlignment.Far);
756 |
757 | lastBpm = t.Bpm;
758 | }
759 |
760 | if (lastBpl == t.BeatsPerLine) continue;
761 | float bpl = t.BeatsPerLine;
762 | string? bplText = "";
763 |
764 | if ((int)(bpl * 100) % 100 == 0)
765 | bplText = $"{(int)bpl}/4";
766 | else if ((int)(bpl * 200) % 100 == 0)
767 | {
768 | bplText = $"{(int)(bpl * 2)}/8";
769 | }
770 | else if ((int)(bpl * 400) % 100 == 0)
771 | {
772 | bplText = $"{(int)(bpl * 2)}/16";
773 | }
774 | if (t.Bpm > 0)
775 | g.DrawStringLayoutLTRB($"{bplText}",
776 | x + Config.ColWidth - 220 - 20 - 60, y + 52,
777 | x + Config.ColWidth - 220 - 20 - 6, y + 80,
778 | StringAdapterAlignment.Far);
779 |
780 | lastBpl = t.BeatsPerLine;
781 | }
782 | }
783 |
784 | public void DrawNoteLength(GraphicsAdapter g)
785 | {
786 | ColorDesc c = ColorDesc.FromArgb(0xff008b8b);
787 |
788 | g.SetColor(c);
789 | g.SetFont("exo", 10f, FontDescStyle.Bold);
790 |
791 | foreach (var note in _affAnalyzer.Notes)
792 | {
793 | if (note.TimePoint > Config.TotalTrackLength)
794 | break;
795 |
796 | float colHeight = Config.Rows * Config.SegmentLengthInBaseBpm;
797 | float y = colHeight - note.TimePoint / Config.TimingScale % colHeight;
798 | float x = Config.ColWidth * (note.TimePoint / Config.TimingScale / (int)colHeight);
799 | if (y <= 1)
800 | y = (int)colHeight;
801 |
802 | string? s =
803 | (note.Divide > 0 ? note.Divide.ToString() : "-") +
804 | (note.hasDot ? "." : "") +
805 | (note.beyondFull ? "-" : "");
806 |
807 | g.DrawString(s, x + Config.ColWidth - 218, y + 37);
808 | }
809 | }
810 |
811 | public void DrawFooter(GraphicsAdapter g)
812 | {
813 | int footerX = 25;
814 | int footerY = Config.Rows * (int)Config.SegmentLengthInBaseBpm + 200 - 100 + 25;
815 | int footerW = Config.Cols * Config.ColWidth + 50;
816 | int footerH = 100;
817 |
818 | ColorDesc? rectColor = ColorDesc.FromArgb(Config.Side switch
819 | {
820 | 0 => 0xc8f0f0f0,
821 | 1 => 0xc8202020,
822 | 2 => 0xc8f0f0f0,
823 | _ => 0,
824 | });
825 |
826 | g.FillRectangle(rectColor, footerX, footerY, footerW, footerH);
827 |
828 | g.SetColor(ColorDesc.FromArgb(Config.Side == 1 ? 0xffffffff : 0xff000000));
829 |
830 | bool hasCover = Config.Cover.InnerImage is not null;
831 | if (hasCover)
832 | g.DrawImageScaled(Config.Cover, footerX, footerY, 100, 100);
833 |
834 | Regex enRegex = new(@"^[A-Za-z\d_\s/\(\)\+\=\-\.\[\]:\(\)&']+$");
835 |
836 | string? title = Title +
837 | $" [ {DiffStr} {Rating:F1} ]" +
838 | $" Tap{_affAnalyzer.Tap} " +
839 | $"Hold{_affAnalyzer.Hold} " +
840 | $"Arc{_affAnalyzer.Arc[0] + _affAnalyzer.Arc[1]} " +
841 | $"[ Blue{_affAnalyzer.Arc[0]} Red{_affAnalyzer.Arc[1]} ] " +
842 | $"ArcTap{_affAnalyzer.ArcTap} " +
843 | $"Total{_affAnalyzer.Total}";
844 |
845 | string? secondLine = (ChartBpm > 0 ? $"Bpm {ChartBpm} " : "") + $"{Artist} / {Charter}";
846 |
847 | g.SetColor(ColorDesc.FromArgb(Config.Side == 1 ? 0xffffffff : 0xff000000));
848 | //g.SetFont(enRegex.IsMatch(title) || enRegex.IsMatch(secondLine) ? "GeosansLight" : "Kazesawa Regular", 26f, FontDescStyle.Regular);
849 | g.SetFont("GeosansLight", 26f, FontDescStyle.Regular);
850 | g.DrawString(title, footerX + 15 + (hasCover ? 100 : 0), footerY + 10);
851 |
852 | g.DrawString(secondLine, footerX + 15 + (hasCover ? 100 : 0), footerY + 50);
853 |
854 | g.SetColor(ColorDesc.FromArgb(0xffff7f50));
855 | g.SetFont("exo", 18f, FontDescStyle.Bold);
856 |
857 | g.DrawStringLayout("Generate by AffTools.Aff2Preview 2.1 ",
858 | footerX + footerW - 500, footerY + footerH - 30,
859 | 500, 30,
860 | StringAdapterAlignment.Far);
861 | }
862 |
863 | }
864 |
--------------------------------------------------------------------------------
/Aff2Preview/AffTools/AffAnalyzer/Analyzer.cs:
--------------------------------------------------------------------------------
1 | using AffTools.AffReader;
2 |
3 | namespace AffTools.AffAnalyzer;
4 |
5 | internal class Analyzer
6 | {
7 | private List _noteRaws = new();
8 |
9 | public List Notes { get; private set; } = new();
10 |
11 | private readonly ArcaeaAffReader _affReader;
12 |
13 | public readonly List SegmentTimings = new();
14 |
15 | public float totalTime;
16 | public float realTotalTime;
17 | public float baseBpm;
18 | public float baseBpl;
19 | public float baseTimePerSegment;
20 | public int segmentCountInBaseBpm;
21 |
22 | public readonly Dictionary timingCombos = new();
23 | public readonly Dictionary timingTaps = new();
24 |
25 | public int Tap = 0;
26 | public int Hold = 0;
27 | public readonly List Arc = new() { 0, 0, 0, 0 };
28 | public int ArcTap = 0;
29 | public int Total = 0;
30 | public int TapTotal => Tap + ArcTap;
31 |
32 | public Analyzer(ArcaeaAffReader affReader)
33 | {
34 | _affReader = affReader;
35 |
36 | var globalTimingGroup = affReader.Events[0] as ArcaeaAffTiming;
37 | if (globalTimingGroup is not null)
38 | {
39 | baseBpm = globalTimingGroup.Bpm;
40 | baseBpl = globalTimingGroup.BeatsPerLine;
41 | baseTimePerSegment = 60 * 1000 * (int)baseBpl / baseBpm;
42 | }
43 |
44 | foreach (var ev in affReader.Events)
45 | {
46 | if (IsGroupNoInput(ev.TimingGroup))
47 | continue;
48 |
49 | totalTime = ev switch
50 | {
51 | ArcaeaAffTap => MathF.Max(totalTime, ev.Timing),
52 | ArcaeaAffArc arc => MathF.Max(totalTime, arc.EndTiming),
53 | ArcaeaAffHold hd => MathF.Max(totalTime, hd.EndTiming),
54 | ArcaeaAffTiming tm => MathF.Max(totalTime, tm.Timing),
55 | _ => totalTime
56 | };
57 | }
58 |
59 | realTotalTime = totalTime;
60 | totalTime += baseTimePerSegment / 4;
61 |
62 | for (double i = 0; i < totalTime; i += baseTimePerSegment)
63 | {
64 | segmentCountInBaseBpm++;
65 | }
66 | }
67 |
68 | ///
69 | /// To pair the scenecontrol statement
70 | ///
71 | ///
72 | public List<(ArcaeaAffSceneControl, ArcaeaAffSceneControl)> GetPairEnwidenLanes()
73 | {
74 | var list = new List();
75 | var result = new List<(ArcaeaAffSceneControl, ArcaeaAffSceneControl)>();
76 |
77 | foreach (var affEvent in _affReader.Events)
78 | {
79 | if (affEvent.Type != EventType.SceneControl) continue;
80 | if ((affEvent as ArcaeaAffSceneControl)?.SceneControlTypeName != "enwidenlanes") continue;
81 |
82 | list.Add((ArcaeaAffSceneControl)affEvent);
83 | }
84 |
85 | if (list.Count % 2 != 0) // Pair the enwidenlane
86 | {
87 | var end = new ArcaeaAffSceneControl
88 | {
89 | Timing = _affReader.Events.Last().Timing,
90 | Type = EventType.SceneControl,
91 | Parameters = new List