mirror of
https://gitea.tendokyu.moe/beerpsi/sinmai-mods.git
synced 2024-11-27 17:10:48 +01:00
Add MoreChartFormats
This commit is contained in:
parent
688836b131
commit
7d8c3d10bf
79
MoreChartFormats/MaiSxt/SrtReader.cs
Normal file
79
MoreChartFormats/MaiSxt/SrtReader.cs
Normal file
@ -0,0 +1,79 @@
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Manager;
|
||||
using MoreChartFormats.MaiSxt.Structures;
|
||||
|
||||
namespace MoreChartFormats.MaiSxt;
|
||||
|
||||
public class SrtReader(NotesReferences refs) : SxtReaderBase(refs)
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void LoadRow(int rowIdx, in SxtRow row)
|
||||
{
|
||||
var rowTime = row.NotesTime(Refs.Reader);
|
||||
var note = new NoteData
|
||||
{
|
||||
type = row.NoteType switch
|
||||
{
|
||||
0 when row.SlideId != 0 => NotesTypeID.Def.Star,
|
||||
0 => NotesTypeID.Def.Tap,
|
||||
2 => NotesTypeID.Def.Hold,
|
||||
4 when row.SlideId != 0 => NotesTypeID.Def.BreakStar,
|
||||
4 => NotesTypeID.Def.Break,
|
||||
128 => NotesTypeID.Def.Slide,
|
||||
_ => throw new Exception($"Unknown note type ID {row.NoteType} at row {rowIdx}, expected 0/2/4/128")
|
||||
},
|
||||
startButtonPos = row.Position,
|
||||
time = rowTime,
|
||||
end = rowTime,
|
||||
beatType = ParserUtilities.GetBeatType(rowTime.grid),
|
||||
index = rowIdx,
|
||||
indexNote = NoteIndex
|
||||
};
|
||||
|
||||
if (note.type.isHold())
|
||||
{
|
||||
note.end += ParserUtilities.NotesTimeFromBars(Refs, row.HoldDuration);
|
||||
note.end.calcMsec(Refs.Reader);
|
||||
}
|
||||
|
||||
if (note.type.isSlide())
|
||||
{
|
||||
if (!SlideHeads.TryGetValue(row.SlideId, out var starNoteRow))
|
||||
{
|
||||
throw new Exception($"Slide body (ID {row.SlideId}) declared without or before its head");
|
||||
}
|
||||
|
||||
note.startButtonPos = starNoteRow.Position;
|
||||
note.slideData = new SlideData
|
||||
{
|
||||
type = row.SlidePattern switch
|
||||
{
|
||||
0 => SlideType.Slide_Straight,
|
||||
1 => SlideType.Slide_Circle_R,
|
||||
2 => SlideType.Slide_Circle_L,
|
||||
_ => throw new Exception($"Unknown slide type {row.SlidePattern} at row {rowIdx}, expected 0/1/2"),
|
||||
},
|
||||
index = row.SlideId,
|
||||
targetNote = row.Position,
|
||||
shoot = new TimingBase { index = rowIdx },
|
||||
arrive = new TimingBase { index = rowIdx },
|
||||
};
|
||||
|
||||
note.time = starNoteRow.NotesTime(Refs.Reader);
|
||||
note.beatType = ParserUtilities.GetBeatType(note.time.grid);
|
||||
note.slideData.shoot.time = note.time + starNoteRow.SlideDelayNotesTime(Refs);
|
||||
note.slideData.shoot.time.calcMsec(Refs.Reader);
|
||||
note.slideData.arrive.time.copy(rowTime);
|
||||
note.end.copy(rowTime);
|
||||
}
|
||||
|
||||
if (note.type.isStar())
|
||||
{
|
||||
SlideHeads[row.SlideId] = row;
|
||||
}
|
||||
|
||||
Refs.Notes._noteData.Add(note);
|
||||
NoteIndex++;
|
||||
}
|
||||
}
|
26
MoreChartFormats/MaiSxt/Structures/SxtRow.cs
Normal file
26
MoreChartFormats/MaiSxt/Structures/SxtRow.cs
Normal file
@ -0,0 +1,26 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using Manager;
|
||||
using MoreChartFormats.Simai.Structures;
|
||||
|
||||
namespace MoreChartFormats.MaiSxt.Structures;
|
||||
|
||||
public struct SxtRow
|
||||
{
|
||||
public float Bar;
|
||||
public float Grid;
|
||||
public float HoldDuration;
|
||||
public int Position;
|
||||
public int NoteType;
|
||||
public int SlideId;
|
||||
public int SlidePattern;
|
||||
// public int SlideCount;
|
||||
public float? SlideDelay;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly NotesTime NotesTime(NotesReader nr) => new NotesTime((int)Bar, (int)(Grid * nr.getResolution()), nr);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly NotesTime SlideDelayNotesTime(NotesReferences refs) => SlideDelay.HasValue
|
||||
? ParserUtilities.NotesTimeFromBars(refs, SlideDelay.Value)
|
||||
: new NotesTime(0, refs.Header._resolutionTime / 4, refs.Reader);
|
||||
}
|
81
MoreChartFormats/MaiSxt/SxtReader.cs
Normal file
81
MoreChartFormats/MaiSxt/SxtReader.cs
Normal file
@ -0,0 +1,81 @@
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Manager;
|
||||
using MoreChartFormats.MaiSxt.Structures;
|
||||
|
||||
namespace MoreChartFormats.MaiSxt;
|
||||
|
||||
public class SxtReader(NotesReferences refs) : SxtReaderBase(refs)
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void LoadRow(int rowIdx, in SxtRow row)
|
||||
{
|
||||
if (row.NoteType == 0)
|
||||
{
|
||||
if (row.SlideId == 0)
|
||||
{
|
||||
throw new Exception($"Slide head at row {rowIdx} does not declare a valid slide ID");
|
||||
}
|
||||
|
||||
System.Console.WriteLine("[MoreChartFormats] Saving slide head with ID {0}", row.SlideId);
|
||||
SlideHeads[row.SlideId] = row;
|
||||
return;
|
||||
}
|
||||
|
||||
var rowTime = row.NotesTime(Refs.Reader);
|
||||
var note = new NoteData
|
||||
{
|
||||
type = row.NoteType switch
|
||||
{
|
||||
1 => NotesTypeID.Def.Tap,
|
||||
2 => NotesTypeID.Def.Hold,
|
||||
3 => NotesTypeID.Def.Break,
|
||||
4 => NotesTypeID.Def.Star,
|
||||
5 => NotesTypeID.Def.BreakStar,
|
||||
128 => NotesTypeID.Def.Slide,
|
||||
_ => throw new Exception($"Unknown note type ID {row.NoteType} at row {rowIdx}, expected 1/2/3/4/5/128")
|
||||
},
|
||||
startButtonPos = row.Position,
|
||||
time = rowTime,
|
||||
end = rowTime,
|
||||
beatType = ParserUtilities.GetBeatType(rowTime.grid),
|
||||
index = rowIdx,
|
||||
indexNote = NoteIndex
|
||||
};
|
||||
|
||||
if (note.type.isHold())
|
||||
{
|
||||
note.end += ParserUtilities.NotesTimeFromBars(Refs, row.HoldDuration);
|
||||
note.end.calcMsec(Refs.Reader);
|
||||
}
|
||||
|
||||
if (note.type.isSlide())
|
||||
{
|
||||
if (!SlideHeads.TryGetValue(row.SlideId, out var slideHeadRow))
|
||||
{
|
||||
throw new Exception($"Slide body (ID {row.SlideId}) declared without or before its head");
|
||||
}
|
||||
|
||||
note.startButtonPos = slideHeadRow.Position;
|
||||
note.slideData = new SlideData
|
||||
{
|
||||
type = (SlideType)row.SlidePattern,
|
||||
index = row.SlideId,
|
||||
targetNote = row.Position,
|
||||
shoot = new TimingBase { index = rowIdx },
|
||||
arrive = new TimingBase { index = rowIdx },
|
||||
};
|
||||
|
||||
note.time = slideHeadRow.NotesTime(Refs.Reader);
|
||||
note.beatType = ParserUtilities.GetBeatType(note.time.grid);
|
||||
note.slideData.shoot.time = note.time + slideHeadRow.SlideDelayNotesTime(Refs);
|
||||
note.slideData.shoot.time.calcMsec(Refs.Reader);
|
||||
note.slideData.arrive.time.copy(rowTime);
|
||||
note.end.copy(rowTime);
|
||||
}
|
||||
|
||||
System.Console.WriteLine("[MoreChartFormats] [SXT] Adding note {0} at {1}", note.type.getEnumName(), note.time.getBar());
|
||||
Refs.Notes._noteData.Add(note);
|
||||
NoteIndex++;
|
||||
}
|
||||
}
|
95
MoreChartFormats/MaiSxt/SxtReaderBase.cs
Normal file
95
MoreChartFormats/MaiSxt/SxtReaderBase.cs
Normal file
@ -0,0 +1,95 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Manager;
|
||||
using MoreChartFormats.MaiSxt.Structures;
|
||||
using MoreChartFormats.Simai.Structures;
|
||||
|
||||
namespace MoreChartFormats.MaiSxt;
|
||||
|
||||
public abstract class SxtReaderBase(NotesReferences refs)
|
||||
{
|
||||
protected readonly NotesReferences Refs = refs;
|
||||
protected int NoteIndex;
|
||||
protected readonly Dictionary<int, SxtRow> SlideHeads = new();
|
||||
|
||||
public void Deserialize(string content)
|
||||
{
|
||||
var table = GetSxtTable(content);
|
||||
|
||||
for (var rowIdx = 0; rowIdx < table.Length; rowIdx++)
|
||||
{
|
||||
var row = ParseRow(rowIdx, table[rowIdx]);
|
||||
LoadRow(rowIdx, in row);
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected abstract void LoadRow(int rowIdx, in SxtRow row);
|
||||
|
||||
private static SxtRow ParseRow(int rowIdx, string[] row)
|
||||
{
|
||||
var srtRow = new SxtRow();
|
||||
|
||||
if (!float.TryParse(row[0], NumberStyles.Float, CultureInfo.InvariantCulture, out srtRow.Bar))
|
||||
{
|
||||
throw new Exception($"Invalid whole measure at row {rowIdx}: {row[0]}");
|
||||
}
|
||||
|
||||
if (!float.TryParse(row[1], NumberStyles.Float, CultureInfo.InvariantCulture, out srtRow.Grid))
|
||||
{
|
||||
throw new Exception($"Invalid fractional measure at row {rowIdx}: {row[1]}");
|
||||
}
|
||||
|
||||
if (!float.TryParse(row[2], NumberStyles.Float, CultureInfo.InvariantCulture, out srtRow.HoldDuration))
|
||||
{
|
||||
throw new Exception($"Invalid hold duration at row {rowIdx}: {row[2]}");
|
||||
}
|
||||
|
||||
if (!int.TryParse(row[3], NumberStyles.Integer, CultureInfo.InvariantCulture, out srtRow.Position))
|
||||
{
|
||||
throw new Exception($"Invalid position at row {rowIdx}: {row[3]}");
|
||||
}
|
||||
|
||||
if (!int.TryParse(row[4], NumberStyles.Integer, CultureInfo.InvariantCulture, out srtRow.NoteType))
|
||||
{
|
||||
throw new Exception($"Invalid note type ID at row {rowIdx}: {row[4]}");
|
||||
}
|
||||
|
||||
if (!int.TryParse(row[5], NumberStyles.Integer, CultureInfo.InvariantCulture, out srtRow.SlideId))
|
||||
{
|
||||
throw new Exception($"Invalid slide ID at row {rowIdx}: {row[5]}");
|
||||
}
|
||||
|
||||
if (!int.TryParse(row[6], NumberStyles.Integer, CultureInfo.InvariantCulture, out srtRow.SlidePattern))
|
||||
{
|
||||
throw new Exception($"Invalid slide type at row {rowIdx}: {row[6]}");
|
||||
}
|
||||
|
||||
// if (row.Length > 7 && !int.TryParse(row[7], NumberStyles.Integer, CultureInfo.InvariantCulture,
|
||||
// out srtRow.SlideCount))
|
||||
// {
|
||||
// throw new Exception($"Invalid slide count at row {rowIdx}: {row[7]}");
|
||||
// }
|
||||
|
||||
if (row.Length > 8)
|
||||
{
|
||||
if (!float.TryParse(row[8], NumberStyles.Float, CultureInfo.InvariantCulture,
|
||||
out var slideDelay))
|
||||
throw new Exception($"Invalid slide delay at row {rowIdx}: {row[8]}");
|
||||
|
||||
srtRow.SlideDelay = slideDelay;
|
||||
}
|
||||
|
||||
return srtRow;
|
||||
}
|
||||
|
||||
private static string[][] GetSxtTable(string content)
|
||||
{
|
||||
return content.Split(["\n", "\r\n"], StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(r => r.Split([","], StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim()).Where(s => s.Length > 0).ToArray())
|
||||
.ToArray();
|
||||
}
|
||||
}
|
91
MoreChartFormats/MoreChartFormats.csproj
Normal file
91
MoreChartFormats/MoreChartFormats.csproj
Normal file
@ -0,0 +1,91 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props"
|
||||
Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')"/>
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||
<ProjectGuid>{A375F626-7238-4227-95C9-2BB1E5E099F6}</ProjectGuid>
|
||||
<OutputType>Library</OutputType>
|
||||
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||
<RootNamespace>MoreChartFormats</RootNamespace>
|
||||
<AssemblyName>Assembly-CSharp.MoreChartFormats.mm</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.6.2</TargetFrameworkVersion>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<LangVersion>latest</LangVersion>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<DebugType>full</DebugType>
|
||||
<Optimize>false</Optimize>
|
||||
<OutputPath>bin\Debug\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<Optimize>true</Optimize>
|
||||
<OutputPath>bin\Release\</OutputPath>
|
||||
<DefineConstants>TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="System"/>
|
||||
<Reference Include="System.Core"/>
|
||||
<Reference Include="System.Data"/>
|
||||
<Reference Include="System.Xml"/>
|
||||
<Reference Include="MonoMod">
|
||||
<HintPath>..\External\MonoMod.exe</HintPath>
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
<Reference Include="Assembly-CSharp">
|
||||
<HintPath>..\External\Assembly-CSharp.dll</HintPath>
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine">
|
||||
<HintPath>..\External\UnityEngine.dll</HintPath>
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.CoreModule">
|
||||
<HintPath>..\External\UnityEngine.CoreModule.dll</HintPath>
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="MaiSxt\SrtReader.cs" />
|
||||
<Compile Include="MaiSxt\Structures\SxtRow.cs" />
|
||||
<Compile Include="MaiSxt\SxtReader.cs" />
|
||||
<Compile Include="MaiSxt\SxtReaderBase.cs" />
|
||||
<Compile Include="NotesReferences.cs" />
|
||||
<Compile Include="ParserUtilities.cs" />
|
||||
<Compile Include="patch_NotesReader.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs"/>
|
||||
<Compile Include="Simai\BpmChangeDataExtensions.cs" />
|
||||
<Compile Include="Simai\SimaiReader.cs" />
|
||||
<Compile Include="Simai\LexicalAnalysis\Token.cs" />
|
||||
<Compile Include="Simai\LexicalAnalysis\Tokenizer.cs" />
|
||||
<Compile Include="Simai\LexicalAnalysis\TokenType.cs" />
|
||||
<Compile Include="Simai\Errors\*.cs" />
|
||||
<Compile Include="Simai\Structures\SlideSegment.cs" />
|
||||
<Compile Include="Simai\Structures\SlideTiming.cs" />
|
||||
<Compile Include="Simai\SyntacticAnalysis\Deserializer.cs" />
|
||||
<Compile Include="Simai\SyntacticAnalysis\States\NoteReader.cs" />
|
||||
<Compile Include="Simai\SyntacticAnalysis\States\SlideReader.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="Simai\LexicalAnalysis\" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets"/>
|
||||
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
|
||||
Other similar extension points exist, see Microsoft.Common.targets.
|
||||
<Target Name="BeforeBuild">
|
||||
</Target>
|
||||
<Target Name="AfterBuild">
|
||||
</Target>
|
||||
-->
|
||||
|
||||
</Project>
|
11
MoreChartFormats/NotesReferences.cs
Normal file
11
MoreChartFormats/NotesReferences.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using Manager;
|
||||
|
||||
namespace MoreChartFormats;
|
||||
|
||||
public class NotesReferences
|
||||
{
|
||||
public NotesReader Reader;
|
||||
public NotesHeader Header;
|
||||
public NotesComposition Composition;
|
||||
public NotesData Notes;
|
||||
}
|
48
MoreChartFormats/ParserUtilities.cs
Normal file
48
MoreChartFormats/ParserUtilities.cs
Normal file
@ -0,0 +1,48 @@
|
||||
using Manager;
|
||||
using MoreChartFormats.Simai.Structures;
|
||||
|
||||
namespace MoreChartFormats;
|
||||
|
||||
public class ParserUtilities
|
||||
{
|
||||
internal static NoteData.BeatType GetBeatType(int grid)
|
||||
{
|
||||
if (grid % 96 == 0)
|
||||
{
|
||||
return NoteData.BeatType.BeatType04;
|
||||
}
|
||||
|
||||
if (grid % 48 == 0)
|
||||
{
|
||||
return NoteData.BeatType.BeatType08;
|
||||
}
|
||||
|
||||
if (grid % 24 == 0)
|
||||
{
|
||||
return NoteData.BeatType.BeatType16;
|
||||
}
|
||||
|
||||
if (grid % 16 == 0)
|
||||
{
|
||||
return NoteData.BeatType.BeatType24;
|
||||
}
|
||||
|
||||
return NoteData.BeatType.BeatTypeOther;
|
||||
}
|
||||
|
||||
internal static NotesTime NotesTimeFromBars(NotesReferences refs, float bars)
|
||||
{
|
||||
var bar = (int)bars;
|
||||
var grid = (int)((bars - bar) * refs.Header._resolutionTime);
|
||||
|
||||
return new NotesTime(bar, grid, refs.Reader);
|
||||
}
|
||||
|
||||
internal static NotesTime NotesTimeFromGrids(NotesReferences refs, int grids)
|
||||
{
|
||||
var nt = new NotesTime(grids);
|
||||
nt.calcMsec(refs.Reader);
|
||||
|
||||
return nt;
|
||||
}
|
||||
}
|
35
MoreChartFormats/Properties/AssemblyInfo.cs
Normal file
35
MoreChartFormats/Properties/AssemblyInfo.cs
Normal file
@ -0,0 +1,35 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
// General Information about an assembly is controlled through the following
|
||||
// set of attributes. Change these attribute values to modify the information
|
||||
// associated with an assembly.
|
||||
[assembly: AssemblyTitle("MoreChartFormats")]
|
||||
[assembly: AssemblyDescription("")]
|
||||
[assembly: AssemblyConfiguration("")]
|
||||
[assembly: AssemblyCompany("")]
|
||||
[assembly: AssemblyProduct("MoreChartFormats")]
|
||||
[assembly: AssemblyCopyright("Copyright © 2024")]
|
||||
[assembly: AssemblyTrademark("")]
|
||||
[assembly: AssemblyCulture("")]
|
||||
|
||||
// Setting ComVisible to false makes the types in this assembly not visible
|
||||
// to COM components. If you need to access a type in this assembly from
|
||||
// COM, set the ComVisible attribute to true on that type.
|
||||
[assembly: ComVisible(false)]
|
||||
|
||||
// The following GUID is for the ID of the typelib if this project is exposed to COM
|
||||
[assembly: Guid("A375F626-7238-4227-95C9-2BB1E5E099F6")]
|
||||
|
||||
// Version information for an assembly consists of the following four values:
|
||||
//
|
||||
// Major Version
|
||||
// Minor Version
|
||||
// Build Number
|
||||
// Revision
|
||||
//
|
||||
// You can specify all the values or you can default the Build and Revision Numbers
|
||||
// by using the '*' as shown below:
|
||||
// [assembly: AssemblyVersion("1.0.*")]
|
||||
[assembly: AssemblyVersion("1.0.0.0")]
|
||||
[assembly: AssemblyFileVersion("1.0.0.0")]
|
14
MoreChartFormats/Simai/BpmChangeDataExtensions.cs
Normal file
14
MoreChartFormats/Simai/BpmChangeDataExtensions.cs
Normal file
@ -0,0 +1,14 @@
|
||||
using Manager;
|
||||
|
||||
namespace MoreChartFormats.Simai;
|
||||
|
||||
public static class BpmChangeDataExtensions
|
||||
{
|
||||
public static float SecondsPerBar(this BPMChangeData timing)
|
||||
{
|
||||
// Work under the assumption that 1 bar = 4 beats
|
||||
// which is kinda true because the meter is always 4/4.
|
||||
// 240 is 60 * 4.
|
||||
return timing.bpm == 0 ? 0 : 240f / timing.bpm;
|
||||
}
|
||||
}
|
9
MoreChartFormats/Simai/Errors/InvalidSyntaxException.cs
Normal file
9
MoreChartFormats/Simai/Errors/InvalidSyntaxException.cs
Normal file
@ -0,0 +1,9 @@
|
||||
namespace MoreChartFormats.Simai.Errors
|
||||
{
|
||||
public class InvalidSyntaxException : SimaiException
|
||||
{
|
||||
public InvalidSyntaxException(int line, int character) : base(line, character)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
22
MoreChartFormats/Simai/Errors/ScopeMismatchException.cs
Normal file
22
MoreChartFormats/Simai/Errors/ScopeMismatchException.cs
Normal file
@ -0,0 +1,22 @@
|
||||
using System;
|
||||
|
||||
namespace MoreChartFormats.Simai.Errors
|
||||
{
|
||||
public class ScopeMismatchException : SimaiException
|
||||
{
|
||||
public readonly ScopeType correctScope;
|
||||
|
||||
public ScopeMismatchException(int line, int character, ScopeType correctScope) : base(line, character)
|
||||
{
|
||||
this.correctScope = correctScope;
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum ScopeType
|
||||
{
|
||||
Note = 1,
|
||||
Slide = 1 << 1,
|
||||
Global = 1 << 2
|
||||
}
|
||||
}
|
||||
}
|
19
MoreChartFormats/Simai/Errors/SimaiException.cs
Normal file
19
MoreChartFormats/Simai/Errors/SimaiException.cs
Normal file
@ -0,0 +1,19 @@
|
||||
using System;
|
||||
|
||||
namespace MoreChartFormats.Simai.Errors
|
||||
{
|
||||
[Serializable]
|
||||
public class SimaiException : Exception
|
||||
{
|
||||
public readonly int line;
|
||||
public readonly int character;
|
||||
|
||||
/// <param name="line">The line on which the error occurred</param>
|
||||
/// <param name="character">The first character involved in the error</param>
|
||||
public SimaiException(int line, int character)
|
||||
{
|
||||
this.character = character;
|
||||
this.line = line;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
namespace MoreChartFormats.Simai.Errors
|
||||
{
|
||||
internal class UnexpectedCharacterException : SimaiException
|
||||
{
|
||||
public readonly string expected;
|
||||
|
||||
/// <summary>
|
||||
/// <para>
|
||||
/// This is thrown when reading a character that is not fit for the expected syntax
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This issue is commonly caused by a typo or a syntax error.
|
||||
/// </para>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="line">The line on which the error occurred</param>
|
||||
/// <param name="character">The first character involved in the error</param>
|
||||
/// <param name="expected">The expected syntax</param>
|
||||
public UnexpectedCharacterException(int line, int character, string expected) : base(line, character)
|
||||
{
|
||||
this.expected = expected;
|
||||
}
|
||||
}
|
||||
}
|
16
MoreChartFormats/Simai/Errors/UnsupportedSyntaxException.cs
Normal file
16
MoreChartFormats/Simai/Errors/UnsupportedSyntaxException.cs
Normal file
@ -0,0 +1,16 @@
|
||||
namespace MoreChartFormats.Simai.Errors
|
||||
{
|
||||
public class UnsupportedSyntaxException : SimaiException
|
||||
{
|
||||
/// <summary>
|
||||
/// <para>
|
||||
/// This is thrown when an unsupported syntax is encountered when attempting to tokenize or deserialize the simai file.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <param name="line">The line on which the error occurred</param>
|
||||
/// <param name="character">The first character involved in the error</param>
|
||||
public UnsupportedSyntaxException(int line, int character) : base(line, character)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
namespace MoreChartFormats.Simai.Errors
|
||||
{
|
||||
public class UnterminatedSectionException : SimaiException
|
||||
{
|
||||
public UnterminatedSectionException(int line, int character) : base(line, character)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
23
MoreChartFormats/Simai/LexicalAnalysis/Token.cs
Normal file
23
MoreChartFormats/Simai/LexicalAnalysis/Token.cs
Normal file
@ -0,0 +1,23 @@
|
||||
using System;
|
||||
|
||||
namespace MoreChartFormats.Simai.LexicalAnalysis;
|
||||
|
||||
public readonly struct Token
|
||||
{
|
||||
public readonly TokenType type;
|
||||
public readonly string lexeme;
|
||||
|
||||
public readonly int line;
|
||||
public readonly int character;
|
||||
|
||||
public Token(TokenType type,
|
||||
string lexeme,
|
||||
int line,
|
||||
int character)
|
||||
{
|
||||
this.type = type;
|
||||
this.lexeme = lexeme;
|
||||
this.line = line;
|
||||
this.character = character;
|
||||
}
|
||||
}
|
46
MoreChartFormats/Simai/LexicalAnalysis/TokenType.cs
Normal file
46
MoreChartFormats/Simai/LexicalAnalysis/TokenType.cs
Normal file
@ -0,0 +1,46 @@
|
||||
namespace MoreChartFormats.Simai.LexicalAnalysis
|
||||
{
|
||||
public enum TokenType
|
||||
{
|
||||
None,
|
||||
Tempo,
|
||||
Subdivision,
|
||||
|
||||
/// <summary>
|
||||
/// <para>Touch locations (A~E + 1~8) and tap locations (1~8)</para>
|
||||
/// <para>
|
||||
/// Takes either only a number (1 ~ 8) or a character (A ~ E) followed by a number (1 ~ 8 for A, B, D, E and 1 or
|
||||
/// 2 for C)
|
||||
/// </para>
|
||||
/// </summary>
|
||||
Location,
|
||||
|
||||
/// <summary>
|
||||
/// Applies note styles and note types
|
||||
/// </summary>
|
||||
Decorator,
|
||||
|
||||
/// <summary>
|
||||
/// Takes a <see cref="SlideType" /> and target vertices
|
||||
/// </summary>
|
||||
Slide,
|
||||
|
||||
/// <summary>
|
||||
/// Usually denotes the length of a hold or a <see cref="SlidePath" />
|
||||
/// </summary>
|
||||
Duration,
|
||||
|
||||
/// <summary>
|
||||
/// Allows multiple slides to share the same parent note
|
||||
/// </summary>
|
||||
SlideJoiner,
|
||||
|
||||
/// <summary>
|
||||
/// Progresses the time by 1 beat
|
||||
/// </summary>
|
||||
TimeStep,
|
||||
|
||||
EachDivider,
|
||||
EndOfFile
|
||||
}
|
||||
}
|
257
MoreChartFormats/Simai/LexicalAnalysis/Tokenizer.cs
Normal file
257
MoreChartFormats/Simai/LexicalAnalysis/Tokenizer.cs
Normal file
@ -0,0 +1,257 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using MoreChartFormats.Simai.Errors;
|
||||
|
||||
namespace MoreChartFormats.Simai.LexicalAnalysis
|
||||
{
|
||||
internal sealed class Tokenizer
|
||||
{
|
||||
private const char Space = (char)0x0020;
|
||||
private const char EnSpace = (char)0x2002;
|
||||
private const char PunctuationSpace = (char)0x2008;
|
||||
private const char IdeographicSpace = (char)0x3000;
|
||||
|
||||
private const char LineSeparator = (char)0x2028;
|
||||
private const char ParagraphSeparator = (char)0x2029;
|
||||
|
||||
private const char EndOfFileChar = 'E';
|
||||
|
||||
private static readonly HashSet<char> EachDividerChars = new()
|
||||
{
|
||||
'/', '`'
|
||||
};
|
||||
|
||||
private static readonly HashSet<char> DecoratorChars = new()
|
||||
{
|
||||
'f', 'b', 'x', 'h', 'm',
|
||||
'!', '?',
|
||||
'@', '$'
|
||||
};
|
||||
|
||||
private static readonly HashSet<char> SlideChars = new()
|
||||
{
|
||||
'-',
|
||||
'>', '<', '^',
|
||||
'p', 'q',
|
||||
'v', 'V',
|
||||
's', 'z',
|
||||
'w'
|
||||
};
|
||||
|
||||
private static readonly HashSet<char> SeparatorChars = new()
|
||||
{
|
||||
'\r', '\t',
|
||||
LineSeparator,
|
||||
ParagraphSeparator,
|
||||
Space,
|
||||
EnSpace,
|
||||
PunctuationSpace,
|
||||
IdeographicSpace
|
||||
};
|
||||
|
||||
private readonly char[] _sequence;
|
||||
private int _current;
|
||||
private int _charIndex;
|
||||
private int _line = 1;
|
||||
|
||||
private int _start;
|
||||
|
||||
public Tokenizer(string sequence)
|
||||
{
|
||||
_sequence = sequence.ToCharArray();
|
||||
}
|
||||
|
||||
private bool IsAtEnd => _current >= _sequence.Length;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public IEnumerable<Token> GetTokens()
|
||||
{
|
||||
while (!IsAtEnd)
|
||||
{
|
||||
_start = _current;
|
||||
|
||||
var nextToken = ScanToken();
|
||||
|
||||
if (nextToken.HasValue)
|
||||
yield return nextToken.Value;
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private Token? ScanToken()
|
||||
{
|
||||
_charIndex++;
|
||||
var c = Advance();
|
||||
switch (c)
|
||||
{
|
||||
case ',':
|
||||
return CompileToken(TokenType.TimeStep);
|
||||
|
||||
case '(':
|
||||
return CompileSectionToken(TokenType.Tempo, '(', ')');
|
||||
case '{':
|
||||
return CompileSectionToken(TokenType.Subdivision, '{', '}');
|
||||
case '[':
|
||||
return CompileSectionToken(TokenType.Duration, '[', ']');
|
||||
|
||||
case var _ when TryScanLocationToken(out var length):
|
||||
_current += length - 1;
|
||||
return CompileToken(TokenType.Location);
|
||||
|
||||
case var _ when DecoratorChars.Contains(c):
|
||||
return CompileToken(TokenType.Decorator);
|
||||
|
||||
case var _ when IsReadingSlideDeclaration(out var length):
|
||||
_current += length - 1;
|
||||
return CompileToken(TokenType.Slide);
|
||||
|
||||
case '*':
|
||||
return CompileToken(TokenType.SlideJoiner);
|
||||
|
||||
case var _ when EachDividerChars.Contains(c):
|
||||
return CompileToken(TokenType.EachDivider);
|
||||
|
||||
case var _ when SeparatorChars.Contains(c):
|
||||
// Ignore whitespace.
|
||||
return null;
|
||||
|
||||
case '\n':
|
||||
_line++;
|
||||
_charIndex = 0;
|
||||
return null;
|
||||
|
||||
case 'E':
|
||||
return CompileToken(TokenType.EndOfFile);
|
||||
|
||||
case '|':
|
||||
{
|
||||
if (Peek() != '|')
|
||||
throw new UnexpectedCharacterException(_line, _charIndex, "|");
|
||||
|
||||
while (Peek() != '\n' && !IsAtEnd)
|
||||
Advance();
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
default:
|
||||
throw new UnsupportedSyntaxException(_line, _charIndex);
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryScanLocationToken(out int length)
|
||||
{
|
||||
var firstLocationChar = PeekPrevious();
|
||||
|
||||
if (IsButtonLocation(firstLocationChar))
|
||||
{
|
||||
length = 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
length = 0;
|
||||
|
||||
if (!IsSensorLocation(firstLocationChar))
|
||||
return false;
|
||||
|
||||
var secondLocationChar = Peek();
|
||||
|
||||
if (IsButtonLocation(secondLocationChar))
|
||||
{
|
||||
length = 2;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (firstLocationChar == 'C')
|
||||
{
|
||||
length = 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
var secondCharIsEmpty = SeparatorChars.Contains(secondLocationChar) ||
|
||||
secondLocationChar is '\n' or '\0';
|
||||
|
||||
// This is the notation for EOF.
|
||||
if (firstLocationChar == EndOfFileChar && secondCharIsEmpty)
|
||||
return false;
|
||||
|
||||
throw new UnexpectedCharacterException(_line, _charIndex, "1, 2, 3, 4, 5, 6, 7, 8");
|
||||
}
|
||||
|
||||
private bool IsReadingSlideDeclaration(out int length)
|
||||
{
|
||||
if (!SlideChars.Contains(PeekPrevious()))
|
||||
{
|
||||
length = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
var nextChar = Peek();
|
||||
|
||||
length = nextChar is 'p' or 'q' ? 2 : 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
private Token? CompileSectionToken(TokenType tokenType, char initiator, char terminator)
|
||||
{
|
||||
_start++;
|
||||
while (Peek() != terminator)
|
||||
{
|
||||
if (IsAtEnd || Peek() == initiator)
|
||||
throw new UnterminatedSectionException(_line, _charIndex);
|
||||
|
||||
Advance();
|
||||
}
|
||||
|
||||
var token = CompileToken(tokenType);
|
||||
|
||||
// The terminator.
|
||||
Advance();
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
private Token CompileToken(TokenType type)
|
||||
{
|
||||
var text = new string(_sequence.Skip(_start).Take(_current - _start).ToArray());
|
||||
|
||||
return new Token(type, text, _line, _charIndex);
|
||||
}
|
||||
|
||||
private static bool IsSensorLocation(char value)
|
||||
{
|
||||
return value is >= 'A' and <= 'E';
|
||||
}
|
||||
|
||||
private static bool IsButtonLocation(char value)
|
||||
{
|
||||
return value is >= '0' and <= '8';
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the <see cref="_current" /> glyph, and increments by one.
|
||||
/// </summary>
|
||||
private char Advance()
|
||||
{
|
||||
return _sequence[_current++];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the <see cref="_current" /> glyph without incrementing.
|
||||
/// </summary>
|
||||
private char Peek()
|
||||
{
|
||||
return IsAtEnd ? default : _sequence[_current];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the last glyph without decrementing.
|
||||
/// </summary>
|
||||
private char PeekPrevious()
|
||||
{
|
||||
return _current == 0 ? default : _sequence[_current - 1];
|
||||
}
|
||||
}
|
||||
}
|
90
MoreChartFormats/Simai/SimaiReader.cs
Normal file
90
MoreChartFormats/Simai/SimaiReader.cs
Normal file
@ -0,0 +1,90 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Manager;
|
||||
using MoreChartFormats.Simai.Errors;
|
||||
using MoreChartFormats.Simai.LexicalAnalysis;
|
||||
using MoreChartFormats.Simai.Structures;
|
||||
using MoreChartFormats.Simai.SyntacticAnalysis;
|
||||
|
||||
namespace MoreChartFormats.Simai;
|
||||
|
||||
public class SimaiReader
|
||||
{
|
||||
public static List<Token> Tokenize(string value)
|
||||
{
|
||||
return new Tokenizer(value).GetTokens().ToList();
|
||||
}
|
||||
|
||||
public static void Deserialize(IEnumerable<Token> tokens, NotesReferences refs)
|
||||
{
|
||||
new Deserializer(tokens).GetChart(refs);
|
||||
}
|
||||
|
||||
public static void ReadBpmChanges(IEnumerable<Token> tokens, NotesReferences refs)
|
||||
{
|
||||
var currentTime = new NotesTime();
|
||||
var subdivision = 4f;
|
||||
var deltaGrids = 0f;
|
||||
|
||||
var nr = refs.Reader;
|
||||
var header = refs.Header;
|
||||
var composition = refs.Composition;
|
||||
|
||||
foreach (var token in tokens)
|
||||
{
|
||||
switch (token.type)
|
||||
{
|
||||
case TokenType.Tempo:
|
||||
{
|
||||
var deltaBars = deltaGrids / header._resolutionTime;
|
||||
var bar = (int)deltaBars;
|
||||
var grid = (int)((deltaBars - bar) * header._resolutionTime);
|
||||
|
||||
currentTime += new NotesTime(bar, grid, nr);
|
||||
deltaGrids -= bar * header._resolutionTime + grid;
|
||||
|
||||
var bpmChangeData = new BPMChangeData();
|
||||
bpmChangeData.time.copy(currentTime);
|
||||
|
||||
if (!float.TryParse(token.lexeme, NumberStyles.Any, CultureInfo.InvariantCulture,
|
||||
out bpmChangeData.bpm))
|
||||
{
|
||||
throw new UnexpectedCharacterException(token.line, token.character, "0-9 or \".\"");
|
||||
}
|
||||
|
||||
composition._bpmList.Add(bpmChangeData);
|
||||
break;
|
||||
}
|
||||
case TokenType.Subdivision:
|
||||
{
|
||||
if (token.lexeme[0] == '#')
|
||||
{
|
||||
throw new UnsupportedSyntaxException(token.line, token.character);
|
||||
}
|
||||
|
||||
if (!float.TryParse(token.lexeme, NumberStyles.Any, CultureInfo.InvariantCulture, out subdivision))
|
||||
{
|
||||
throw new UnexpectedCharacterException(token.line, token.character, "0-9 or \".\"");
|
||||
}
|
||||
break;
|
||||
}
|
||||
case TokenType.TimeStep:
|
||||
{
|
||||
deltaGrids += header._resolutionTime / subdivision;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (composition._bpmList.Count == 0)
|
||||
{
|
||||
var dummyBpmChange = new BPMChangeData { bpm = ReaderConst.DEFAULT_BPM };
|
||||
dummyBpmChange.time.init(0, 0, nr);
|
||||
|
||||
composition._bpmList.Add(dummyBpmChange);
|
||||
}
|
||||
}
|
||||
}
|
9
MoreChartFormats/Simai/Structures/SlideSegment.cs
Normal file
9
MoreChartFormats/Simai/Structures/SlideSegment.cs
Normal file
@ -0,0 +1,9 @@
|
||||
using Manager;
|
||||
|
||||
namespace MoreChartFormats.Simai.Structures;
|
||||
|
||||
public class SlideSegment
|
||||
{
|
||||
public NoteData NoteData;
|
||||
public SlideTiming Timing;
|
||||
}
|
9
MoreChartFormats/Simai/Structures/SlideTiming.cs
Normal file
9
MoreChartFormats/Simai/Structures/SlideTiming.cs
Normal file
@ -0,0 +1,9 @@
|
||||
using Manager;
|
||||
|
||||
namespace MoreChartFormats.Simai.Structures;
|
||||
|
||||
public class SlideTiming
|
||||
{
|
||||
public NotesTime? Delay;
|
||||
public NotesTime Duration;
|
||||
}
|
242
MoreChartFormats/Simai/SyntacticAnalysis/Deserializer.cs
Normal file
242
MoreChartFormats/Simai/SyntacticAnalysis/Deserializer.cs
Normal file
@ -0,0 +1,242 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Manager;
|
||||
using MoreChartFormats.Simai.Errors;
|
||||
using MoreChartFormats.Simai.LexicalAnalysis;
|
||||
using MoreChartFormats.Simai.Structures;
|
||||
using MoreChartFormats.Simai.SyntacticAnalysis.States;
|
||||
|
||||
namespace MoreChartFormats.Simai.SyntacticAnalysis;
|
||||
|
||||
internal class Deserializer(IEnumerable<Token> sequence) : IDisposable
|
||||
{
|
||||
internal readonly IEnumerator<Token> TokenEnumerator = sequence.GetEnumerator();
|
||||
|
||||
internal BPMChangeData CurrentBpmChange;
|
||||
internal NotesTime CurrentTime = new(0);
|
||||
|
||||
// References all notes between two TimeSteps
|
||||
internal LinkedList<List<NoteData>> CurrentNoteDataCollections;
|
||||
|
||||
// References the current note between EACH dividers
|
||||
internal List<NoteData> CurrentNoteData;
|
||||
|
||||
internal float Subdivision = 4f;
|
||||
internal bool IsEndOfFile;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
TokenEnumerator.Dispose();
|
||||
}
|
||||
|
||||
public void GetChart(NotesReferences refs)
|
||||
{
|
||||
var manuallyMoved = false;
|
||||
var deltaGrids = 0f;
|
||||
var index = 1;
|
||||
var noteIndex = 0;
|
||||
var slideIndex = 0;
|
||||
var currentBpmChangeIndex = 0;
|
||||
var firstBpmChangeEventIgnored = false;
|
||||
var fakeEach = false;
|
||||
|
||||
CurrentBpmChange = refs.Composition._bpmList[currentBpmChangeIndex];
|
||||
|
||||
while (!IsEndOfFile && (manuallyMoved || MoveNext()))
|
||||
{
|
||||
var token = TokenEnumerator.Current;
|
||||
manuallyMoved = false;
|
||||
|
||||
switch (token.type)
|
||||
{
|
||||
case TokenType.Subdivision:
|
||||
{
|
||||
if (token.lexeme[0] == '#')
|
||||
{
|
||||
if (!float.TryParse(token.lexeme.Substring(1), NumberStyles.Any, CultureInfo.InvariantCulture, out var absoluteSubdivision))
|
||||
{
|
||||
throw new UnexpectedCharacterException(token.line, token.character, "0-9 or \".\"");
|
||||
}
|
||||
|
||||
Subdivision = CurrentBpmChange.SecondsPerBar() / absoluteSubdivision;
|
||||
}
|
||||
else if (!float.TryParse(token.lexeme, NumberStyles.Any, CultureInfo.InvariantCulture, out Subdivision))
|
||||
{
|
||||
throw new UnexpectedCharacterException(token.line, token.character, "0-9 or \".\"");
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case TokenType.Location:
|
||||
{
|
||||
// We have to stack up all deltas before adding them because adding it every time
|
||||
// we reach a TimeStep would cause precision loss due to casting floating points
|
||||
// back to integers.
|
||||
var delta = ParserUtilities.NotesTimeFromGrids(refs, (int)deltaGrids);
|
||||
|
||||
CurrentTime += delta;
|
||||
deltaGrids -= delta.grid;
|
||||
|
||||
CurrentNoteDataCollections ??= [];
|
||||
CurrentNoteData = [];
|
||||
CurrentNoteDataCollections.AddLast(CurrentNoteData);
|
||||
|
||||
// ForceEach not supported
|
||||
if (token.lexeme[0] == '0')
|
||||
{
|
||||
throw new UnsupportedSyntaxException(token.line, token.character);
|
||||
}
|
||||
|
||||
NoteReader.Process(this, in token, refs, ref index, ref noteIndex, ref slideIndex);
|
||||
manuallyMoved = true;
|
||||
break;
|
||||
}
|
||||
case TokenType.TimeStep:
|
||||
{
|
||||
if (CurrentNoteDataCollections != null)
|
||||
{
|
||||
if (fakeEach)
|
||||
{
|
||||
ProcessFakeEach(refs);
|
||||
fakeEach = false;
|
||||
}
|
||||
refs.Notes._noteData.AddRange(CurrentNoteDataCollections.SelectMany(c => c));
|
||||
CurrentNoteDataCollections = null;
|
||||
CurrentNoteData = null;
|
||||
}
|
||||
|
||||
deltaGrids += refs.Header._resolutionTime / Subdivision;
|
||||
break;
|
||||
}
|
||||
case TokenType.EachDivider:
|
||||
fakeEach = fakeEach || token.lexeme[0] == '`';
|
||||
break;
|
||||
case TokenType.Decorator:
|
||||
throw new ScopeMismatchException(token.line, token.character,
|
||||
ScopeMismatchException.ScopeType.Note);
|
||||
case TokenType.Slide:
|
||||
throw new ScopeMismatchException(token.line, token.character,
|
||||
ScopeMismatchException.ScopeType.Note);
|
||||
case TokenType.Duration:
|
||||
throw new ScopeMismatchException(token.line, token.character,
|
||||
ScopeMismatchException.ScopeType.Note |
|
||||
ScopeMismatchException.ScopeType.Slide);
|
||||
case TokenType.SlideJoiner:
|
||||
throw new ScopeMismatchException(token.line, token.character,
|
||||
ScopeMismatchException.ScopeType.Slide);
|
||||
case TokenType.EndOfFile:
|
||||
// There isn't a way to signal to the game engine that the chart
|
||||
// is ending prematurely, as it expects that every note in the
|
||||
// chart is actually in the chart.
|
||||
IsEndOfFile = true;
|
||||
break;
|
||||
case TokenType.Tempo:
|
||||
if (!firstBpmChangeEventIgnored)
|
||||
{
|
||||
firstBpmChangeEventIgnored = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
CurrentBpmChange = refs.Composition._bpmList[++currentBpmChangeIndex];
|
||||
}
|
||||
break;
|
||||
case TokenType.None:
|
||||
break;
|
||||
default:
|
||||
throw new UnsupportedSyntaxException(token.line, token.character);
|
||||
}
|
||||
|
||||
index++;
|
||||
}
|
||||
|
||||
if (CurrentNoteDataCollections == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (fakeEach)
|
||||
{
|
||||
ProcessFakeEach(refs);
|
||||
}
|
||||
refs.Notes._noteData.AddRange(CurrentNoteDataCollections.SelectMany(c => c));
|
||||
CurrentNoteDataCollections = null;
|
||||
CurrentNoteData = null;
|
||||
}
|
||||
|
||||
private void ProcessFakeEach(NotesReferences refs)
|
||||
{
|
||||
var node = CurrentNoteDataCollections.First.Next;
|
||||
var singleTick = new NotesTime(refs.Header._resolutionTime / 384);
|
||||
var delta = singleTick;
|
||||
|
||||
while (node != null)
|
||||
{
|
||||
foreach (var note in node.Value)
|
||||
{
|
||||
note.time += delta;
|
||||
note.end += delta;
|
||||
note.beatType = ParserUtilities.GetBeatType(note.time.grid);
|
||||
|
||||
if (note.type.isSlide())
|
||||
{
|
||||
note.slideData.shoot.time += delta;
|
||||
note.slideData.arrive.time += delta;
|
||||
}
|
||||
|
||||
delta += singleTick;
|
||||
}
|
||||
|
||||
node = node.Next;
|
||||
}
|
||||
}
|
||||
|
||||
internal static bool TryReadLocation(in Token token, out int position, out TouchSensorType touchGroup)
|
||||
{
|
||||
var isSensor = token.lexeme[0] is >= 'A' and <= 'E';
|
||||
var index = isSensor ? token.lexeme.Substring(1) : token.lexeme;
|
||||
|
||||
touchGroup = TouchSensorType.Invalid;
|
||||
|
||||
if (isSensor)
|
||||
{
|
||||
touchGroup = token.lexeme[0] switch
|
||||
{
|
||||
'A' => TouchSensorType.A,
|
||||
'B' => TouchSensorType.B,
|
||||
'C' => TouchSensorType.C,
|
||||
'D' => TouchSensorType.D,
|
||||
'E' => TouchSensorType.E,
|
||||
_ => TouchSensorType.Invalid,
|
||||
};
|
||||
|
||||
switch (touchGroup)
|
||||
{
|
||||
case TouchSensorType.Invalid:
|
||||
position = -1;
|
||||
return false;
|
||||
case TouchSensorType.C:
|
||||
position = 0;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!int.TryParse(index, out position))
|
||||
{
|
||||
position = -1;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Convert to 0-indexed position
|
||||
position -= 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
internal bool MoveNext()
|
||||
{
|
||||
IsEndOfFile = !TokenEnumerator.MoveNext();
|
||||
return !IsEndOfFile;
|
||||
}
|
||||
}
|
255
MoreChartFormats/Simai/SyntacticAnalysis/States/NoteReader.cs
Normal file
255
MoreChartFormats/Simai/SyntacticAnalysis/States/NoteReader.cs
Normal file
@ -0,0 +1,255 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using Manager;
|
||||
using MoreChartFormats.Simai.Errors;
|
||||
using MoreChartFormats.Simai.LexicalAnalysis;
|
||||
|
||||
namespace MoreChartFormats.Simai.SyntacticAnalysis.States;
|
||||
|
||||
internal static class NoteReader
|
||||
{
|
||||
public static void Process(Deserializer parent, in Token identityToken, NotesReferences refs, ref int index, ref int noteIndex, ref int slideIndex)
|
||||
{
|
||||
if (!Deserializer.TryReadLocation(in identityToken, out var position, out var touchGroup))
|
||||
{
|
||||
throw new InvalidSyntaxException(identityToken.line, identityToken.character);
|
||||
}
|
||||
|
||||
var forceNormal = false;
|
||||
var forceTapless = false;
|
||||
var noteData = new NoteData
|
||||
{
|
||||
type = NotesTypeID.Def.Tap,
|
||||
index = index,
|
||||
startButtonPos = position,
|
||||
beatType = ParserUtilities.GetBeatType(parent.CurrentTime.grid),
|
||||
indexNote = noteIndex++,
|
||||
};
|
||||
noteData.time.copy(parent.CurrentTime);
|
||||
noteData.end = noteData.time;
|
||||
|
||||
if (touchGroup != TouchSensorType.Invalid)
|
||||
{
|
||||
// simai does not have syntax for specifying touch size.
|
||||
noteData.touchSize = NoteSize.M1;
|
||||
noteData.touchArea = touchGroup;
|
||||
noteData.type = NotesTypeID.Def.TouchTap;
|
||||
}
|
||||
|
||||
// Some readers (e.g. NoteReader) moves the enumerator automatically.
|
||||
// We can skip moving the pointer if that's satisfied.
|
||||
var manuallyMoved = false;
|
||||
var noteDataAdded = false;
|
||||
|
||||
while (!parent.IsEndOfFile && (manuallyMoved || parent.MoveNext()))
|
||||
{
|
||||
var token = parent.TokenEnumerator.Current;
|
||||
manuallyMoved = false;
|
||||
|
||||
switch (token.type)
|
||||
{
|
||||
case TokenType.Tempo:
|
||||
case TokenType.Subdivision:
|
||||
throw new ScopeMismatchException(token.line, token.character, ScopeMismatchException.ScopeType.Global);
|
||||
|
||||
case TokenType.Decorator:
|
||||
DecorateNote(in token, ref noteData, ref forceNormal, ref forceTapless);
|
||||
break;
|
||||
|
||||
case TokenType.Slide:
|
||||
{
|
||||
if (!forceNormal && !forceTapless)
|
||||
{
|
||||
if (!StarMapping.TryGetValue(noteData.type, out var def))
|
||||
{
|
||||
throw new InvalidSyntaxException(token.line, token.character);
|
||||
}
|
||||
|
||||
noteData.type = def;
|
||||
}
|
||||
|
||||
if (!forceTapless)
|
||||
{
|
||||
parent.CurrentNoteData.Add(noteData);
|
||||
}
|
||||
|
||||
SlideReader.Process(parent, in token, refs, noteData, ref index, ref noteIndex, ref slideIndex);
|
||||
noteDataAdded = true;
|
||||
manuallyMoved = true;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case TokenType.Duration:
|
||||
ReadDuration(refs, parent.CurrentBpmChange, in token, ref noteData);
|
||||
break;
|
||||
|
||||
case TokenType.SlideJoiner:
|
||||
throw new ScopeMismatchException(token.line, token.character, ScopeMismatchException.ScopeType.Slide);
|
||||
|
||||
case TokenType.TimeStep:
|
||||
case TokenType.EachDivider:
|
||||
case TokenType.EndOfFile:
|
||||
case TokenType.Location:
|
||||
// note terminates here
|
||||
if (!noteDataAdded)
|
||||
{
|
||||
parent.CurrentNoteData.Add(noteData);
|
||||
}
|
||||
return;
|
||||
|
||||
case TokenType.None:
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new UnsupportedSyntaxException(token.line, token.character);
|
||||
}
|
||||
}
|
||||
|
||||
parent.CurrentNoteData.Add(noteData);
|
||||
}
|
||||
|
||||
private static readonly Dictionary<NotesTypeID, NotesTypeID.Def> BreakMapping = new()
|
||||
{
|
||||
{ NotesTypeID.Def.Tap, NotesTypeID.Def.Break },
|
||||
{ NotesTypeID.Def.ExTap, NotesTypeID.Def.ExBreakTap },
|
||||
{ NotesTypeID.Def.Star, NotesTypeID.Def.BreakStar },
|
||||
{ NotesTypeID.Def.ExStar, NotesTypeID.Def.ExBreakStar },
|
||||
{ NotesTypeID.Def.Hold, NotesTypeID.Def.BreakHold },
|
||||
{ NotesTypeID.Def.ExHold, NotesTypeID.Def.ExBreakHold },
|
||||
{ NotesTypeID.Def.Slide, NotesTypeID.Def.BreakSlide },
|
||||
};
|
||||
|
||||
private static readonly Dictionary<NotesTypeID, NotesTypeID.Def> ExMapping = new()
|
||||
{
|
||||
{ NotesTypeID.Def.Tap, NotesTypeID.Def.ExTap },
|
||||
{ NotesTypeID.Def.Break, NotesTypeID.Def.ExBreakTap },
|
||||
{ NotesTypeID.Def.Star, NotesTypeID.Def.ExStar },
|
||||
{ NotesTypeID.Def.BreakStar, NotesTypeID.Def.ExBreakStar },
|
||||
{ NotesTypeID.Def.Hold, NotesTypeID.Def.ExHold },
|
||||
{ NotesTypeID.Def.BreakHold, NotesTypeID.Def.ExBreakHold },
|
||||
};
|
||||
|
||||
private static readonly Dictionary<NotesTypeID, NotesTypeID.Def> HoldMapping = new()
|
||||
{
|
||||
{ NotesTypeID.Def.Tap, NotesTypeID.Def.Hold },
|
||||
{ NotesTypeID.Def.Break, NotesTypeID.Def.BreakHold },
|
||||
{ NotesTypeID.Def.ExTap, NotesTypeID.Def.ExHold },
|
||||
{ NotesTypeID.Def.ExBreakTap, NotesTypeID.Def.ExBreakHold },
|
||||
{ NotesTypeID.Def.TouchTap, NotesTypeID.Def.TouchHold },
|
||||
};
|
||||
|
||||
private static readonly Dictionary<NotesTypeID, NotesTypeID.Def> StarMapping = new()
|
||||
{
|
||||
{ NotesTypeID.Def.Tap, NotesTypeID.Def.Star },
|
||||
{ NotesTypeID.Def.Break, NotesTypeID.Def.BreakStar },
|
||||
{ NotesTypeID.Def.ExTap, NotesTypeID.Def.ExStar },
|
||||
{ NotesTypeID.Def.ExBreakTap, NotesTypeID.Def.ExBreakStar },
|
||||
};
|
||||
|
||||
private static void DecorateNote(in Token token, ref NoteData noteData, ref bool forceNormal, ref bool forceTapless)
|
||||
{
|
||||
switch (token.lexeme[0])
|
||||
{
|
||||
case 'f' when noteData.type.isTouch():
|
||||
noteData.effect = TouchEffectType.Eff1;
|
||||
return;
|
||||
case 'b':
|
||||
{
|
||||
if (!BreakMapping.TryGetValue(noteData.type, out var def))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
noteData.type = def;
|
||||
return;
|
||||
}
|
||||
case 'x':
|
||||
{
|
||||
if (!ExMapping.TryGetValue(noteData.type, out var def))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
noteData.type = def;
|
||||
return;
|
||||
}
|
||||
case 'h':
|
||||
{
|
||||
if (!HoldMapping.TryGetValue(noteData.type, out var def))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
noteData.type = def;
|
||||
return;
|
||||
}
|
||||
case '@':
|
||||
forceNormal = true;
|
||||
return;
|
||||
case '?':
|
||||
forceTapless = true;
|
||||
return;
|
||||
case '!':
|
||||
forceTapless = true;
|
||||
return;
|
||||
case '$':
|
||||
{
|
||||
if (!StarMapping.TryGetValue(noteData.type, out var def))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
noteData.type = def;
|
||||
return;
|
||||
}
|
||||
default:
|
||||
throw new UnsupportedSyntaxException(token.line, token.character);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ReadDuration(NotesReferences refs, BPMChangeData timing, in Token token, ref NoteData note)
|
||||
{
|
||||
if (HoldMapping.TryGetValue(note.type, out var def))
|
||||
{
|
||||
note.type = def;
|
||||
}
|
||||
|
||||
var hashIndex = token.lexeme.IndexOf('#');
|
||||
|
||||
if (hashIndex == 0)
|
||||
{
|
||||
if (!float.TryParse(token.lexeme.Substring(1),
|
||||
NumberStyles.Any,
|
||||
CultureInfo.InvariantCulture,
|
||||
out var explicitValue))
|
||||
throw new UnexpectedCharacterException(token.line, token.character + 1, "0-9 or \".\"");
|
||||
|
||||
note.end = note.time + ParserUtilities.NotesTimeFromBars(refs, explicitValue / timing.SecondsPerBar());
|
||||
return;
|
||||
}
|
||||
|
||||
if (hashIndex != -1)
|
||||
{
|
||||
// The [BPM#a:b] syntax doesn't really make sense for holds? Unless we're adding
|
||||
// a BPM change event just for this hold. I am not bothering.
|
||||
throw new UnsupportedSyntaxException(token.line, token.character);
|
||||
}
|
||||
|
||||
var separatorIndex = token.lexeme.IndexOf(':');
|
||||
|
||||
if (!float.TryParse(token.lexeme.Substring(0, separatorIndex),
|
||||
NumberStyles.Any,
|
||||
CultureInfo.InvariantCulture,
|
||||
out var denominator))
|
||||
throw new UnexpectedCharacterException(token.line, token.character, "0-9 or \".\"");
|
||||
|
||||
if (!float.TryParse(token.lexeme.Substring(separatorIndex + 1),
|
||||
NumberStyles.Any,
|
||||
CultureInfo.InvariantCulture,
|
||||
out var nominator))
|
||||
throw new UnexpectedCharacterException(token.line, token.character + separatorIndex + 1, "0-9 or \".\"");
|
||||
|
||||
note.end = note.time + ParserUtilities.NotesTimeFromBars(refs, nominator / denominator);
|
||||
}
|
||||
}
|
393
MoreChartFormats/Simai/SyntacticAnalysis/States/SlideReader.cs
Normal file
393
MoreChartFormats/Simai/SyntacticAnalysis/States/SlideReader.cs
Normal file
@ -0,0 +1,393 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using MAI2.Util;
|
||||
using Manager;
|
||||
using MoreChartFormats.Simai.Errors;
|
||||
using MoreChartFormats.Simai.LexicalAnalysis;
|
||||
using MoreChartFormats.Simai.Structures;
|
||||
|
||||
namespace MoreChartFormats.Simai.SyntacticAnalysis.States;
|
||||
|
||||
internal static class SlideReader
|
||||
{
|
||||
public static void Process(Deserializer parent, in Token identityToken, NotesReferences refs, NoteData starNote, ref int index, ref int noteIndex,
|
||||
ref int slideIndex)
|
||||
{
|
||||
var segments = new LinkedList<SlideSegment>();
|
||||
|
||||
|
||||
var firstSlide = ReadSegment(parent, in identityToken, starNote.startButtonPos, index, ref noteIndex, ref slideIndex);
|
||||
firstSlide.time = new NotesTime(parent.CurrentTime);
|
||||
|
||||
var firstSegment = new SlideSegment { NoteData = firstSlide };
|
||||
segments.AddLast(firstSegment);
|
||||
|
||||
var currentSegment = firstSegment;
|
||||
var slideStartPos = firstSlide.slideData.targetNote;
|
||||
|
||||
// Some readers (e.g. NoteReader) moves the enumerator automatically.
|
||||
// We can skip moving the pointer if that's satisfied.
|
||||
var manuallyMoved = true;
|
||||
|
||||
while (!parent.IsEndOfFile && (manuallyMoved || parent.MoveNext()))
|
||||
{
|
||||
var token = parent.TokenEnumerator.Current;
|
||||
manuallyMoved = false;
|
||||
|
||||
switch (token.type)
|
||||
{
|
||||
case TokenType.Tempo:
|
||||
case TokenType.Subdivision:
|
||||
throw new ScopeMismatchException(token.line, token.character, ScopeMismatchException.ScopeType.Global);
|
||||
case TokenType.Decorator:
|
||||
DecorateSlide(in token, ref firstSlide);
|
||||
break;
|
||||
case TokenType.Slide:
|
||||
currentSegment = new SlideSegment
|
||||
{
|
||||
NoteData = ReadSegment(parent, in token, slideStartPos, ++index,
|
||||
ref noteIndex, ref slideIndex),
|
||||
};
|
||||
segments.AddLast(currentSegment);
|
||||
slideStartPos = currentSegment.NoteData.slideData.targetNote;
|
||||
manuallyMoved = true;
|
||||
break;
|
||||
case TokenType.Duration:
|
||||
currentSegment.Timing = ReadDuration(refs, parent.CurrentBpmChange, in token);
|
||||
break;
|
||||
case TokenType.SlideJoiner:
|
||||
ProcessSlideSegments(parent, refs, in identityToken, segments);
|
||||
slideStartPos = starNote.startButtonPos;
|
||||
segments.Clear();
|
||||
break;
|
||||
case TokenType.TimeStep:
|
||||
case TokenType.EachDivider:
|
||||
case TokenType.EndOfFile:
|
||||
case TokenType.Location:
|
||||
ProcessSlideSegments(parent, refs, in identityToken, segments);
|
||||
return;
|
||||
case TokenType.None:
|
||||
break;
|
||||
default:
|
||||
throw new UnsupportedSyntaxException(token.line, token.character);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ProcessSlideSegments(Deserializer parent, NotesReferences refs, in Token identityToken, LinkedList<SlideSegment> segments)
|
||||
{
|
||||
// Fast path for non-festival slides
|
||||
if (segments.Count == 1)
|
||||
{
|
||||
ProcessSingleSlideSegment(parent, refs, segments.First.Value);
|
||||
return;
|
||||
}
|
||||
|
||||
var segmentsWithTiming = segments.Count(s => s.Timing != null);
|
||||
|
||||
if (segmentsWithTiming != 1 && segmentsWithTiming != segments.Count)
|
||||
{
|
||||
throw new InvalidSyntaxException(identityToken.line, identityToken.character);
|
||||
}
|
||||
|
||||
if (segmentsWithTiming == segments.Count)
|
||||
{
|
||||
ProcessSlideSegmentsAllDurations(parent, refs, segments);
|
||||
}
|
||||
else
|
||||
{
|
||||
ProcessSlideSegmentsSingleDuration(parent, refs, in identityToken, segments);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ProcessSingleSlideSegment(Deserializer parent, NotesReferences refs, SlideSegment segment)
|
||||
{
|
||||
segment.NoteData.time = new NotesTime(parent.CurrentTime);
|
||||
segment.NoteData.beatType = ParserUtilities.GetBeatType(segment.NoteData.time.grid);
|
||||
segment.NoteData.slideData.shoot.time = new NotesTime(segment.NoteData.time);
|
||||
|
||||
if (segment.Timing.Delay.HasValue)
|
||||
{
|
||||
segment.NoteData.slideData.shoot.time += segment.Timing.Delay.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
segment.NoteData.slideData.shoot.time += new NotesTime(0, refs.Header._resolutionTime / 4, refs.Reader);
|
||||
}
|
||||
|
||||
segment.NoteData.slideData.arrive.time = segment.NoteData.slideData.shoot.time + segment.Timing.Duration;
|
||||
segment.NoteData.end = new NotesTime(segment.NoteData.slideData.arrive.time);
|
||||
|
||||
parent.CurrentNoteData.Add(segment.NoteData);
|
||||
}
|
||||
|
||||
private static void ProcessSlideSegmentsAllDurations(Deserializer parent, NotesReferences refs, LinkedList<SlideSegment> segments)
|
||||
{
|
||||
var time = parent.CurrentTime;
|
||||
var isFirstSegment = true;
|
||||
|
||||
foreach (var segment in segments)
|
||||
{
|
||||
if (!isFirstSegment)
|
||||
{
|
||||
segment.NoteData.type = NotesTypeID.Def.ConnectSlide;
|
||||
}
|
||||
|
||||
segment.NoteData.time = new NotesTime(time);
|
||||
segment.NoteData.beatType = ParserUtilities.GetBeatType(segment.NoteData.time.grid);
|
||||
segment.NoteData.slideData.shoot.time = new NotesTime(segment.NoteData.time);
|
||||
|
||||
if (segment.Timing.Delay.HasValue)
|
||||
{
|
||||
segment.NoteData.slideData.shoot.time += segment.Timing.Delay.Value;
|
||||
}
|
||||
else if (isFirstSegment)
|
||||
{
|
||||
segment.NoteData.slideData.shoot.time += new NotesTime(0, refs.Header._resolutionTime / 4, refs.Reader);
|
||||
}
|
||||
|
||||
segment.NoteData.slideData.arrive.time =
|
||||
segment.NoteData.slideData.shoot.time + segment.Timing.Duration;
|
||||
segment.NoteData.end = new NotesTime(segment.NoteData.slideData.arrive.time);
|
||||
time = segment.NoteData.end;
|
||||
|
||||
parent.CurrentNoteData.Add(segment.NoteData);
|
||||
|
||||
isFirstSegment = false;
|
||||
}
|
||||
}
|
||||
|
||||
private static void ProcessSlideSegmentsSingleDuration(Deserializer parent, NotesReferences refs,
|
||||
in Token identityToken, LinkedList<SlideSegment> segments)
|
||||
{
|
||||
var time = parent.CurrentTime;
|
||||
var slideTiming = segments.Last.Value.Timing;
|
||||
|
||||
if (slideTiming == null)
|
||||
{
|
||||
throw new InvalidSyntaxException(identityToken.line, identityToken.character);
|
||||
}
|
||||
|
||||
var slideManager = Singleton<SlideManager>.Instance;
|
||||
var slideLengths = segments.Select(
|
||||
s => slideManager.GetSlideLength(
|
||||
s.NoteData.slideData.type, s.NoteData.startButtonPos, s.NoteData.slideData.targetNote)).ToList();
|
||||
var totalSlideLength = slideLengths.Sum();
|
||||
|
||||
var segmentNode = segments.First;
|
||||
var i = 0;
|
||||
|
||||
while (segmentNode != null)
|
||||
{
|
||||
var slideLength = slideLengths[i];
|
||||
var segment = segmentNode.Value;
|
||||
var isFirstSegment = i == 0;
|
||||
|
||||
segment.NoteData.time = new NotesTime(time);
|
||||
segment.NoteData.beatType = ParserUtilities.GetBeatType(segment.NoteData.time.grid);
|
||||
segment.NoteData.slideData.shoot.time = new NotesTime(segment.NoteData.time);
|
||||
|
||||
if (isFirstSegment)
|
||||
{
|
||||
if (slideTiming.Delay.HasValue)
|
||||
{
|
||||
segment.NoteData.slideData.shoot.time += slideTiming.Delay.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
segment.NoteData.slideData.shoot.time += new NotesTime(0, refs.Header._resolutionTime / 4, refs.Reader);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
segment.NoteData.type = NotesTypeID.Def.ConnectSlide;
|
||||
}
|
||||
|
||||
var slideDurationSlice = slideLength / totalSlideLength;
|
||||
var slideDuration = new NotesTime((int)Math.Round(slideTiming.Duration.grid * slideDurationSlice));
|
||||
|
||||
segment.NoteData.slideData.arrive.time =
|
||||
segment.NoteData.slideData.shoot.time + slideDuration;
|
||||
segment.NoteData.end = new NotesTime(segment.NoteData.slideData.arrive.time);
|
||||
time = segment.NoteData.end;
|
||||
|
||||
parent.CurrentNoteData.Add(segment.NoteData);
|
||||
|
||||
segmentNode = segmentNode.Next;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
private static NoteData ReadSegment(Deserializer parent, in Token identityToken, int startingPosition, int index, ref int noteIndex, ref int slideIndex)
|
||||
{
|
||||
var length = identityToken.lexeme.Length;
|
||||
var vertices = RetrieveVertices(parent, in identityToken);
|
||||
|
||||
return new NoteData
|
||||
{
|
||||
type = NotesTypeID.Def.Slide,
|
||||
index = index,
|
||||
indexNote = noteIndex++,
|
||||
indexSlide = slideIndex++,
|
||||
startButtonPos = startingPosition,
|
||||
slideData = new SlideData
|
||||
{
|
||||
targetNote = vertices.Last(),
|
||||
type = DetermineSlideType(in identityToken, startingPosition, length, vertices),
|
||||
shoot = new TimingBase { index = noteIndex },
|
||||
arrive = new TimingBase { index = noteIndex },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private static SlideTiming ReadDuration(NotesReferences refs, BPMChangeData timing, in Token token)
|
||||
{
|
||||
// Accepted slide duration formats:
|
||||
// - {BPM}#{denominator}:{nominator}
|
||||
// - #{slide duration in seconds}
|
||||
// - {BPM}#{slide duration in seconds}
|
||||
// - {seconds}##{slide duration in seconds}
|
||||
var result = new SlideTiming { Duration = new NotesTime() };
|
||||
|
||||
var hashIndex = token.lexeme.IndexOf('#');
|
||||
var statesIntroDelayDuration = hashIndex > 0;
|
||||
var durationDeclarationStart = hashIndex + 1;
|
||||
|
||||
if (statesIntroDelayDuration)
|
||||
{
|
||||
result.Delay = new NotesTime();
|
||||
|
||||
var secondHashIndex = token.lexeme.LastIndexOf('#');
|
||||
var isAbsoluteDelay = secondHashIndex > -1;
|
||||
|
||||
if (!float.TryParse(token.lexeme.Substring(0, hashIndex),
|
||||
NumberStyles.Any,
|
||||
CultureInfo.InvariantCulture,
|
||||
out var value))
|
||||
throw new UnexpectedCharacterException(token.line, token.character + 1, "0-9 or \".\"");
|
||||
|
||||
if (isAbsoluteDelay)
|
||||
{
|
||||
durationDeclarationStart = secondHashIndex + 1;
|
||||
result.Delay.Value.copy(ParserUtilities.NotesTimeFromBars(refs, value / timing.SecondsPerBar()));
|
||||
}
|
||||
else
|
||||
{
|
||||
var grids = (int)Math.Round((float)refs.Header._resolutionTime / 4 * (timing.bpm / value));
|
||||
result.Delay.Value.copy(ParserUtilities.NotesTimeFromGrids(refs, grids));
|
||||
}
|
||||
}
|
||||
|
||||
var durationDeclaration = token.lexeme.Substring(durationDeclarationStart);
|
||||
var separatorIndex = durationDeclaration.IndexOf(':');
|
||||
|
||||
if (separatorIndex > -1)
|
||||
{
|
||||
if (!float.TryParse(durationDeclaration.Substring(0, separatorIndex),
|
||||
NumberStyles.Any,
|
||||
CultureInfo.InvariantCulture,
|
||||
out var denominator))
|
||||
throw new UnexpectedCharacterException(token.line, token.character, "0-9 or \".\"");
|
||||
|
||||
if (!float.TryParse(durationDeclaration.Substring(separatorIndex + 1),
|
||||
NumberStyles.Any,
|
||||
CultureInfo.InvariantCulture,
|
||||
out var nominator))
|
||||
throw new UnexpectedCharacterException(token.line, token.character + separatorIndex + 1, "0-9 or \".\"");
|
||||
|
||||
result.Duration.copy(ParserUtilities.NotesTimeFromBars(refs, nominator / denominator));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!float.TryParse(durationDeclaration,
|
||||
NumberStyles.Any,
|
||||
CultureInfo.InvariantCulture,
|
||||
out var seconds))
|
||||
throw new UnexpectedCharacterException(token.line, token.character, "0-9 or \".\"");
|
||||
|
||||
result.Duration.copy(ParserUtilities.NotesTimeFromBars(refs, seconds / timing.SecondsPerBar()));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static SlideType DetermineSlideType(in Token identityToken, int startingPosition, int length, List<int> vertices)
|
||||
{
|
||||
return identityToken.lexeme[0] switch
|
||||
{
|
||||
'-' => SlideType.Slide_Straight,
|
||||
'^' => DetermineRingType(startingPosition, vertices[0]),
|
||||
'>' => DetermineRingType(startingPosition, vertices[0], 1),
|
||||
'<' => DetermineRingType(startingPosition, vertices[0], -1),
|
||||
'V' => DetermineRingType(startingPosition, vertices[0]) switch
|
||||
{
|
||||
SlideType.Slide_Circle_L => SlideType.Slide_Skip_L,
|
||||
SlideType.Slide_Circle_R => SlideType.Slide_Skip_R,
|
||||
_ => throw new ArgumentOutOfRangeException(),
|
||||
},
|
||||
'v' => SlideType.Slide_Corner,
|
||||
's' => SlideType.Slide_Thunder_R,
|
||||
'z' => SlideType.Slide_Thunder_L,
|
||||
'p' when length == 2 && identityToken.lexeme[1] == 'p' => SlideType.Slide_Bend_L,
|
||||
'q' when length == 2 && identityToken.lexeme[1] == 'q' => SlideType.Slide_Bend_R,
|
||||
'p' => SlideType.Slide_Curve_L,
|
||||
'q' => SlideType.Slide_Curve_R,
|
||||
'w' => SlideType.Slide_Fan,
|
||||
_ => throw new UnexpectedCharacterException(identityToken.line, identityToken.character, "-, ^, >, <, v, V, s, z, pp, qq, p, q, w"),
|
||||
};
|
||||
}
|
||||
|
||||
private static void DecorateSlide(in Token token, ref NoteData note)
|
||||
{
|
||||
switch (token.lexeme[0])
|
||||
{
|
||||
case 'b':
|
||||
note.type = NotesTypeID.Def.BreakSlide;
|
||||
return;
|
||||
default:
|
||||
throw new UnsupportedSyntaxException(token.line, token.character);
|
||||
}
|
||||
}
|
||||
|
||||
private static SlideType DetermineRingType(int startPosition, int endPosition, int direction = 0)
|
||||
{
|
||||
switch (direction)
|
||||
{
|
||||
case 1:
|
||||
return (startPosition + 2) % 8 >= 4 ? SlideType.Slide_Circle_L : SlideType.Slide_Circle_R;
|
||||
case -1:
|
||||
return (startPosition + 2) % 8 >= 4 ? SlideType.Slide_Circle_R : SlideType.Slide_Circle_L;
|
||||
default:
|
||||
{
|
||||
var difference = endPosition - startPosition;
|
||||
|
||||
var rotation = difference >= 0
|
||||
? difference > 4 ? -1 : 1
|
||||
: difference < -4 ? 1 : -1;
|
||||
|
||||
return rotation > 0 ? SlideType.Slide_Circle_R : SlideType.Slide_Circle_L;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static List<int> RetrieveVertices(Deserializer parent, in Token identityToken)
|
||||
{
|
||||
var vertices = new List<int>();
|
||||
|
||||
do
|
||||
{
|
||||
if (!parent.TokenEnumerator.MoveNext())
|
||||
throw new UnexpectedCharacterException(identityToken.line, identityToken.character,
|
||||
"1, 2, 3, 4, 5, 6, 7, 8");
|
||||
|
||||
var current = parent.TokenEnumerator.Current;
|
||||
|
||||
if (Deserializer.TryReadLocation(in current, out var location, out _))
|
||||
vertices.Add(location);
|
||||
} while (parent.TokenEnumerator.Current.type == TokenType.Location);
|
||||
|
||||
return vertices;
|
||||
}
|
||||
}
|
209
MoreChartFormats/patch_NotesReader.cs
Normal file
209
MoreChartFormats/patch_NotesReader.cs
Normal file
@ -0,0 +1,209 @@
|
||||
// ReSharper disable CheckNamespace
|
||||
// ReSharper disable InconsistentNaming
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Xml.Serialization;
|
||||
using MAI2.Util;
|
||||
using Manager.MaiStudio.Serialize;
|
||||
using MonoMod;
|
||||
using MoreChartFormats;
|
||||
using MoreChartFormats.MaiSxt;
|
||||
using MoreChartFormats.Simai;
|
||||
using MoreChartFormats.Simai.Errors;
|
||||
using MoreChartFormats.Simai.Structures;
|
||||
|
||||
namespace Manager;
|
||||
|
||||
class patch_NotesReader : NotesReader
|
||||
{
|
||||
private new FormatType checkFormat(string fileName) => Path.GetExtension(fileName) switch
|
||||
{
|
||||
".simai" => FormatType.FORMAT_M2S,
|
||||
".srt" => FormatType.FORMAT_SRT,
|
||||
".szt" => FormatType.FORMAT_SZT,
|
||||
".sct" => FormatType.FORMAT_SCT,
|
||||
".sdt" => FormatType.FORMAT_SDT,
|
||||
_ => FormatType.FORMAT_MA2,
|
||||
};
|
||||
|
||||
[MonoModIgnore]
|
||||
private extern bool loadMa2(string fileName, LoadType loadType = LoadType.LOAD_FULL);
|
||||
|
||||
[MonoModReplace]
|
||||
public new bool load(string fileName, LoadType loadType = LoadType.LOAD_FULL)
|
||||
{
|
||||
if (!File.Exists(fileName))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var format = checkFormat(fileName);
|
||||
|
||||
return format switch
|
||||
{
|
||||
FormatType.FORMAT_MA2 => loadMa2(fileName, loadType),
|
||||
FormatType.FORMAT_SRT or FormatType.FORMAT_SZT or FormatType.FORMAT_SCT or FormatType.FORMAT_SDT =>
|
||||
loadSxt(format, fileName),
|
||||
FormatType.FORMAT_M2S => loadSimai(fileName),
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
|
||||
private bool loadSxt(FormatType format, string fileName)
|
||||
{
|
||||
init(_playerID);
|
||||
fillDummyHeader(fileName);
|
||||
_loadType = LoadType.LOAD_FULL;
|
||||
|
||||
try
|
||||
{
|
||||
// HACK: we are assuming that the chart file is stored in the same folder
|
||||
// as the Music.xml, which it must be due to how this game loads assets.
|
||||
// refer to `Manager.MaiStudio.Serialize.FilePath.AddPath(string parentPath)`.
|
||||
//
|
||||
// There must be a better way to get the song's BPM...
|
||||
var musicFolder = Path.GetDirectoryName(fileName);
|
||||
|
||||
if (musicFolder == null)
|
||||
{
|
||||
throw new Exception("Music.xml is contained in the root directory?!");
|
||||
}
|
||||
|
||||
var serializer = new XmlSerializer(typeof(MusicData));
|
||||
using (var musicXml = File.OpenRead(Path.Combine(musicFolder, "Music.xml")))
|
||||
{
|
||||
var music = (MusicData)serializer.Deserialize(musicXml);
|
||||
var bpmChangeData = new BPMChangeData
|
||||
{
|
||||
bpm = music.bpm,
|
||||
time = new NotesTime(0, 0, this),
|
||||
};
|
||||
_composition._bpmList.Add(bpmChangeData);
|
||||
calcBPMList();
|
||||
}
|
||||
|
||||
var content = File.ReadAllText(fileName);
|
||||
var refs = new NotesReferences
|
||||
{
|
||||
Reader = this,
|
||||
Header = _header,
|
||||
Composition = _composition,
|
||||
Notes = _note,
|
||||
};
|
||||
SxtReaderBase reader = format == FormatType.FORMAT_SRT ? new SrtReader(refs) : new SxtReader(refs);
|
||||
|
||||
reader.Deserialize(content);
|
||||
calcAll();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
System.Console.WriteLine("[MoreChartFormats] [SXT] Could not load SXT chart: {0}", e);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool loadSimai(string fileName)
|
||||
{
|
||||
init(_playerID);
|
||||
fillDummyHeader(fileName);
|
||||
_loadType = LoadType.LOAD_FULL;
|
||||
|
||||
try
|
||||
{
|
||||
System.Console.WriteLine("[MoreChartFormats] [Simai] Tokenizing chart");
|
||||
var tokens = SimaiReader.Tokenize(File.ReadAllText(fileName));
|
||||
var refs = new NotesReferences
|
||||
{
|
||||
Reader = this,
|
||||
Header = _header,
|
||||
Composition = _composition,
|
||||
Notes = _note,
|
||||
};
|
||||
|
||||
System.Console.WriteLine("[MoreChartFormats] [Simai] Processing BPM changes");
|
||||
SimaiReader.ReadBpmChanges(tokens, refs);
|
||||
calcBPMList();
|
||||
|
||||
System.Console.WriteLine("[MoreChartFormats] [Simai] Reading chart");
|
||||
SimaiReader.Deserialize(tokens, refs);
|
||||
|
||||
System.Console.WriteLine("[MoreChartFormats] [Simai] Mirroring chart and calculating timings");
|
||||
foreach (var note in _note._noteData)
|
||||
{
|
||||
note.time.calcMsec(this);
|
||||
note.end.calcMsec(this);
|
||||
note.startButtonPos = ConvertMirrorPosition(note.startButtonPos);
|
||||
|
||||
if (note.type.isSlide() || note.type == NotesTypeID.Def.ConnectSlide)
|
||||
{
|
||||
note.slideData.shoot.time.calcMsec(this);
|
||||
note.slideData.arrive.time.calcMsec(this);
|
||||
note.slideData.targetNote = ConvertMirrorPosition(note.slideData.targetNote);
|
||||
note.slideData.type = ConvertMirrorSlide(note.slideData.type);
|
||||
}
|
||||
|
||||
if (note.type.isTouch() && note.touchArea is TouchSensorType.D or TouchSensorType.E)
|
||||
{
|
||||
note.startButtonPos = ConvertMirrorTouchEPosition(note.startButtonPos);
|
||||
}
|
||||
}
|
||||
|
||||
System.Console.WriteLine("[MoreChartFormats] [Simai] Calculating chart data");
|
||||
calcAll();
|
||||
|
||||
System.Console.WriteLine("[MoreChartFormats] [Simai] Loaded {0} notes", _total.GetAllNoteNum());
|
||||
System.Console.WriteLine("[MoreChartFormats] [Simai] > {0} taps", _total.GetTapNum());
|
||||
System.Console.WriteLine("[MoreChartFormats] [Simai] > {0} holds", _total.GetHoldNum());
|
||||
System.Console.WriteLine("[MoreChartFormats] [Simai] > {0} slides", _total.GetSlideNum());
|
||||
System.Console.WriteLine("[MoreChartFormats] [Simai] > {0} touches", _total.GetTouchNum());
|
||||
System.Console.WriteLine("[MoreChartFormats] [Simai] > {0} break", _total.GetBreakNum());
|
||||
}
|
||||
catch (SimaiException e)
|
||||
{
|
||||
System.Console.WriteLine($"[MoreChartFormats] There was an error loading the chart at line {e.line}, col {e.character}: {e} ");
|
||||
return false;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
System.Console.WriteLine($"[MoreChartFormats] There was an error loading the chart: {e}");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void fillDummyHeader(string fileName)
|
||||
{
|
||||
_header._notesName = fileName;
|
||||
_header._resolutionTime = ReaderConst.DEFAULT_RESOLUTION_TIME;
|
||||
_header._version[0].major = 0;
|
||||
_header._version[0].minor = 0;
|
||||
_header._version[0].release = 0;
|
||||
_header._version[1].major = 1;
|
||||
_header._version[1].minor = 4;
|
||||
_header._version[0].release = 0;
|
||||
_header._metInfo.denomi = 4;
|
||||
_header._metInfo.num = 4;
|
||||
_header._clickFirst = ReaderConst.DEFAULT_RESOLUTION_TIME;
|
||||
|
||||
// The game doesn't care if a non-fes-mode chart uses utage mechanics.
|
||||
// It's just a flag.
|
||||
_header._isFes = false;
|
||||
}
|
||||
|
||||
private void calcAll()
|
||||
{
|
||||
calcNoteTiming();
|
||||
calcEach();
|
||||
calcSlide();
|
||||
calcEndTiming();
|
||||
calcBPMInfo();
|
||||
calcBarList();
|
||||
calcSoflanList();
|
||||
calcClickList();
|
||||
calcTotal();
|
||||
}
|
||||
}
|
36
README.md
36
README.md
@ -17,3 +17,39 @@ Enables loading tables from loose `.json` files in `Sinmai_Data/StreamingAssets/
|
||||
Useful for string edits (a.k.a. english patch).
|
||||
|
||||
Tables are automatically generated if `Sinmai_Data/StreamingAssets/DB` doesn't exist.
|
||||
|
||||
### MoreChartFormats
|
||||
|
||||
Loads charts written in various known formats:
|
||||
- [simai](https://w.atwiki.jp/simai) (powered by a custom fork of [SimaiSharp](https://github.com/reflektone-games/SimaiSharp))
|
||||
- srt/szt/sct/sdt (maimai classic chart format)
|
||||
|
||||
To use, edit Music.xml to point the chart file path to your chart file:
|
||||
```xml
|
||||
<Notes>
|
||||
<file>
|
||||
<path>{filename}.sdt</path>
|
||||
</file>
|
||||
<!-- snip -->
|
||||
</Notes>
|
||||
```
|
||||
|
||||
The chart loader used depends on the file extension:
|
||||
- simai chart files must end with `.simai`
|
||||
- srt chart files must end with `.srt`
|
||||
- szt/sct/sdt files can use `.szt`/`.sct`/`.sdt` interchangeably, since they only
|
||||
differ by two ending columns. [details](https://listed.to/@donmai/18173/the-four-chart-formats-of-maimai-classic)
|
||||
|
||||
#### Simai caveats
|
||||
- **`maidata.txt` is not supported. If you have one, paste the content of each
|
||||
`inote_x` into their own `.simai` file.**
|
||||
- Both `?` and `!` will create a slide without a star note, but both of them will
|
||||
make the slide fade in (`!` makes the slide suddenly appear in standard simai).
|
||||
- `$` cannot be used to create a tapless slide (maiPad PLUS syntax).
|
||||
- `$$` is ignored, as star notes only spin when there's an associated slide.
|
||||
- `[BPM#a:b]` is not supported for specifying hold time.
|
||||
- `` ` `` (fake EACH) makes taps 1/384 measures apart.
|
||||
|
||||
#### SXT caveats
|
||||
- Encrypted chart files (`.srb`/`.szb`/`.scb`/`.sdb`) are not supported. Decrypt
|
||||
them before loading into the game.
|
@ -4,6 +4,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CachedDataManager", "Cached
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FixLocaleIssues", "FixLocaleIssues\FixLocaleIssues.csproj", "{48B5F480-D749-48E9-9D26-E0E5260D95DE}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LooseDBTables", "LooseDBTables\LooseDBTables.csproj", "{F15988CC-BDF0-4F86-811B-BAE18EEA6519}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LooseDBTables.GeneratePatches", "LooseDBTables.GeneratePatches\LooseDBTables.GeneratePatches.csproj", "{7DF53594-C7B2-44D1-ADF7-CCE4BC9E7625}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MoreChartFormats", "MoreChartFormats\MoreChartFormats.csproj", "{A375F626-7238-4227-95C9-2BB1E5E099F6}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@ -18,5 +24,17 @@ Global
|
||||
{48B5F480-D749-48E9-9D26-E0E5260D95DE}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{48B5F480-D749-48E9-9D26-E0E5260D95DE}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{48B5F480-D749-48E9-9D26-E0E5260D95DE}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{F15988CC-BDF0-4F86-811B-BAE18EEA6519}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{F15988CC-BDF0-4F86-811B-BAE18EEA6519}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{F15988CC-BDF0-4F86-811B-BAE18EEA6519}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{F15988CC-BDF0-4F86-811B-BAE18EEA6519}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{7DF53594-C7B2-44D1-ADF7-CCE4BC9E7625}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{7DF53594-C7B2-44D1-ADF7-CCE4BC9E7625}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{7DF53594-C7B2-44D1-ADF7-CCE4BC9E7625}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{7DF53594-C7B2-44D1-ADF7-CCE4BC9E7625}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{A375F626-7238-4227-95C9-2BB1E5E099F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A375F626-7238-4227-95C9-2BB1E5E099F6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A375F626-7238-4227-95C9-2BB1E5E099F6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A375F626-7238-4227-95C9-2BB1E5E099F6}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
Loading…
Reference in New Issue
Block a user