Compare commits

...

12 Commits

61 changed files with 3024 additions and 738 deletions

View File

@ -1,65 +0,0 @@
<?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>{F1C1B6BF-626C-4F10-8672-2F9596706CA6}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>CachedDataManager</RootNamespace>
<AssemblyName>Assembly-CSharp.CachedDataManager.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>
</ItemGroup>
<ItemGroup>
<Compile Include="Manager\MaiStudio\Serialize\patch_FilePath.cs" />
<Compile Include="Manager\patch_DataManager.cs" />
<Compile Include="SerializationCache.cs" />
<Compile Include="Properties\AssemblyInfo.cs"/>
</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>

View File

@ -1,205 +0,0 @@
// ReSharper disable CheckNamespace
// ReSharper disable InconsistentNaming
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Xml.Serialization;
namespace Manager;
public class patch_DataManager : DataManager
{
private const string _cacheDirectory = "dataCache";
[Serializable]
[XmlType(TypeName="KeyValuePair")]
public class SerializableKeyValuePair<TK, TV>
{
public TK Key;
public TV Value;
}
private static Dictionary<string, T> LoadCacheFile<T>(string fileName)
{
if (!File.Exists(fileName))
{
return new Dictionary<string, T>();
}
using var fs = File.OpenRead(fileName);
var serializer = new XmlSerializer(typeof(List<SerializableKeyValuePair<string, T>>));
var entries = (List<SerializableKeyValuePair<string, T>>)serializer.Deserialize(fs);
return entries.ToDictionary(e => e.Key, e => e.Value);
}
private static void SaveCacheFile<T>(string destination, Dictionary<string, T> collection)
{
if (collection == null)
{
return;
}
using var fs = File.Open(destination, FileMode.Create);
var serializer = new XmlSerializer(typeof(List<SerializableKeyValuePair<string, T>>));
serializer.Serialize(
fs,
collection
.Select(kvp => new SerializableKeyValuePair<string, T> { Key = kvp.Key, Value = kvp.Value })
.ToList());
}
private static void SaveCache()
{
if (!Directory.Exists(_cacheDirectory))
{
Directory.CreateDirectory(_cacheDirectory);
}
SaveCacheFile(Path.Combine(_cacheDirectory, "RomConfigs.xml"), Cache.RomConfigs);
SaveCacheFile(Path.Combine(_cacheDirectory, "DataConfigs.xml"), Cache.DataConfigs);
SaveCacheFile(Path.Combine(_cacheDirectory, "Charas.xml"), Cache.Charas);
SaveCacheFile(Path.Combine(_cacheDirectory, "CharaAwakes.xml"), Cache.CharaAwakes);
SaveCacheFile(Path.Combine(_cacheDirectory, "CharaGenres.xml"), Cache.CharaGenres);
SaveCacheFile(Path.Combine(_cacheDirectory, "Events.xml"), Cache.Events);
SaveCacheFile(Path.Combine(_cacheDirectory, "Musics.xml"), Cache.Musics);
SaveCacheFile(Path.Combine(_cacheDirectory, "MusicGenres.xml"), Cache.MusicGenres);
SaveCacheFile(Path.Combine(_cacheDirectory, "MusicGroups.xml"), Cache.MusicGroups);
SaveCacheFile(Path.Combine(_cacheDirectory, "MusicVersions.xml"), Cache.MusicVersions);
SaveCacheFile(Path.Combine(_cacheDirectory, "MusicNameSorts.xml"), Cache.MusicNameSorts);
SaveCacheFile(Path.Combine(_cacheDirectory, "MusicClearRanks.xml"), Cache.MusicClearRanks);
SaveCacheFile(Path.Combine(_cacheDirectory, "MusicDifficultys.xml"), Cache.MusicDifficultys);
SaveCacheFile(Path.Combine(_cacheDirectory, "MusicLevels.xml"), Cache.MusicLevels);
SaveCacheFile(Path.Combine(_cacheDirectory, "TournamentMusics.xml"), Cache.TournamentMusics);
SaveCacheFile(Path.Combine(_cacheDirectory, "Courses.xml"), Cache.Courses);
SaveCacheFile(Path.Combine(_cacheDirectory, "CourseModes.xml"), Cache.CourseModes);
SaveCacheFile(Path.Combine(_cacheDirectory, "LoginBonuses.xml"), Cache.LoginBonuses);
SaveCacheFile(Path.Combine(_cacheDirectory, "Maps.xml"), Cache.Maps);
SaveCacheFile(Path.Combine(_cacheDirectory, "MapColors.xml"), Cache.MapColors);
SaveCacheFile(Path.Combine(_cacheDirectory, "MapTreasures.xml"), Cache.MapTreasures);
SaveCacheFile(Path.Combine(_cacheDirectory, "MapBonusMusics.xml"), Cache.MapBonusMusics);
SaveCacheFile(Path.Combine(_cacheDirectory, "MapOtomodachis.xml"), Cache.MapOtomodachis);
SaveCacheFile(Path.Combine(_cacheDirectory, "MapSilhouettes.xml"), Cache.MapSilhouettes);
SaveCacheFile(Path.Combine(_cacheDirectory, "MapTitles.xml"), Cache.MapTitles);
SaveCacheFile(Path.Combine(_cacheDirectory, "ItemMusics.xml"), Cache.ItemMusics);
SaveCacheFile(Path.Combine(_cacheDirectory, "Icons.xml"), Cache.Icons);
SaveCacheFile(Path.Combine(_cacheDirectory, "Plates.xml"), Cache.Plates);
SaveCacheFile(Path.Combine(_cacheDirectory, "Titles.xml"), Cache.Titles);
SaveCacheFile(Path.Combine(_cacheDirectory, "Partners.xml"), Cache.Partners);
SaveCacheFile(Path.Combine(_cacheDirectory, "Frames.xml"), Cache.Frames);
SaveCacheFile(Path.Combine(_cacheDirectory, "Tickets.xml"), Cache.Tickets);
SaveCacheFile(Path.Combine(_cacheDirectory, "CollectionTypes.xml"), Cache.CollectionTypes);
SaveCacheFile(Path.Combine(_cacheDirectory, "CollectionGenres.xml"), Cache.CollectionGenres);
SaveCacheFile(Path.Combine(_cacheDirectory, "PhotoFrames.xml"), Cache.PhotoFrames);
SaveCacheFile(Path.Combine(_cacheDirectory, "Informations.xml"), Cache.Informations);
SaveCacheFile(Path.Combine(_cacheDirectory, "Udemaes.xml"), Cache.Udemaes);
SaveCacheFile(Path.Combine(_cacheDirectory, "Classes.xml"), Cache.Classes);
SaveCacheFile(Path.Combine(_cacheDirectory, "UdemaeBosses.xml"), Cache.UdemaeBosses);
SaveCacheFile(Path.Combine(_cacheDirectory, "UdemaeSeasonEvents.xml"), Cache.UdemaeSeasonEvents);
SaveCacheFile(Path.Combine(_cacheDirectory, "UdemaeSeasonRewards.xml"), Cache.UdemaeSeasonRewards);
SaveCacheFile(Path.Combine(_cacheDirectory, "Cards.xml"), Cache.Cards);
SaveCacheFile(Path.Combine(_cacheDirectory, "CardCharas.xml"), Cache.CardCharas);
SaveCacheFile(Path.Combine(_cacheDirectory, "CardTypes.xml"), Cache.CardTypes);
SaveCacheFile(Path.Combine(_cacheDirectory, "WeekdayBonuses.xml"), Cache.WeekdayBonuses);
SaveCacheFile(Path.Combine(_cacheDirectory, "Challenges.xml"), Cache.Challenges);
SaveCacheFile(Path.Combine(_cacheDirectory, "MusicRankings.xml"), Cache.MusicRankings);
SaveCacheFile(Path.Combine(_cacheDirectory, "MusicSorts.xml"), Cache.MusicSorts);
SaveCacheFile(Path.Combine(_cacheDirectory, "LoginBonusSorts.xml"), Cache.LoginBonusSorts);
SaveCacheFile(Path.Combine(_cacheDirectory, "IconSorts.xml"), Cache.IconSorts);
SaveCacheFile(Path.Combine(_cacheDirectory, "PlateSorts.xml"), Cache.PlateSorts);
SaveCacheFile(Path.Combine(_cacheDirectory, "TitleSorts.xml"), Cache.TitleSorts);
SaveCacheFile(Path.Combine(_cacheDirectory, "PartnerSorts.xml"), Cache.PartnerSorts);
SaveCacheFile(Path.Combine(_cacheDirectory, "FrameSorts.xml"), Cache.FrameSorts);
SaveCacheFile(Path.Combine(_cacheDirectory, "TicketSorts.xml"), Cache.TicketSorts);
SaveCacheFile(Path.Combine(_cacheDirectory, "CollectionGenreSorts.xml"), Cache.CollectionGenreSorts);
SaveCacheFile(Path.Combine(_cacheDirectory, "CharaSorts.xml"), Cache.CharaSorts);
SaveCacheFile(Path.Combine(_cacheDirectory, "CharaGenreSorts.xml"), Cache.CharaGenreSorts);
}
private static extern bool orig_Deserialize<T>(string filePath, out T dsr) where T : new();
private static bool Deserialize<T>(string filePath, out T dsr) where T : new()
{
var fileName = Path.GetFileName(filePath);
var collectionName = fileName switch
{
"UdemaeBoss.xml" => "UdemaeBosses",
"Class.xml" => "Classes",
"LoginBonus.xml" => "LoginBonuses",
"ScoreRanking.xml" => "TournamentMusics",
"Holiday.xml" => "WeekdayBonuses",
_ => fileName.Replace(".xml", "s"),
};
var collectionField =
typeof(CachedDataManager.SerializationCache).GetField(collectionName, BindingFlags.Public | BindingFlags.Instance);
if (collectionField == null)
{
System.Console.WriteLine("[CachedDataManager] [WARN] Could not find suitable collection for {0} (tried name {1})", fileName, collectionName);
return orig_Deserialize(filePath, out dsr);
}
try
{
var collection = (Dictionary<string, T>)collectionField.GetValue(Cache);
if (collection == null)
{
collection = LoadCacheFile<T>(Path.Combine(_cacheDirectory, $"{collectionName}.xml"));
collectionField.SetValue(Cache, collection);
}
if (collection.TryGetValue(filePath, out dsr))
{
return true;
}
if (!orig_Deserialize(filePath, out dsr))
{
return false;
}
collection.Add(filePath, dsr);
_cacheBusted = true;
return true;
}
catch (Exception e)
{
System.Console.WriteLine("[CachedDataManager] [ERROR] Could not load from cache: {0}", e);
return orig_Deserialize(filePath, out dsr);
}
}
private extern bool orig_IsLoaded();
public new bool IsLoaded()
{
var loaded = orig_IsLoaded();
if (!loaded || !_cacheBusted)
{
return loaded;
}
try
{
SaveCache();
}
catch (Exception e)
{
System.Console.WriteLine("[CachedDataManager] [ERROR] Could not save to cache: {0}", e);
}
_cacheBusted = false;
return true;
}
private static readonly CachedDataManager.SerializationCache Cache = new();
private static bool _cacheBusted;
}

View File

@ -1,35 +0,0 @@
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("CachedDataManager")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("CachedDataManager")]
[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("F1C1B6BF-626C-4F10-8672-2F9596706CA6")]
// 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")]

View File

@ -1,67 +0,0 @@
// ReSharper disable UnassignedField.Global
using System.Collections.Generic;
using Manager.MaiStudio.Serialize;
namespace CachedDataManager;
public class SerializationCache
{
public Dictionary<string, RomConfig> RomConfigs;
public Dictionary<string, DataConfig> DataConfigs;
public Dictionary<string, CharaData> Charas;
public Dictionary<string, CharaAwakeData> CharaAwakes;
public Dictionary<string, CharaGenreData> CharaGenres;
public Dictionary<string, EventData> Events;
public Dictionary<string, MusicData> Musics;
public Dictionary<string, MusicGenreData> MusicGenres;
public Dictionary<string, MusicGroupData> MusicGroups;
public Dictionary<string, MusicVersionData> MusicVersions;
public Dictionary<string, MusicNameSortData> MusicNameSorts;
public Dictionary<string, MusicClearRankData> MusicClearRanks;
public Dictionary<string, MusicDifficultyData> MusicDifficultys;
public Dictionary<string, MusicLevelData> MusicLevels;
public Dictionary<string, ScoreRankingData> TournamentMusics;
public Dictionary<string, CourseData> Courses;
public Dictionary<string, CourseModeData> CourseModes;
public Dictionary<string, LoginBonusData> LoginBonuses;
public Dictionary<string, MapData> Maps;
public Dictionary<string, MapColorData> MapColors;
public Dictionary<string, MapTreasureData> MapTreasures;
public Dictionary<string, MapBonusMusicData> MapBonusMusics;
public Dictionary<string, MapOtomodachiData> MapOtomodachis;
public Dictionary<string, MapSilhouetteData> MapSilhouettes;
public Dictionary<string, MapTitleData> MapTitles;
public Dictionary<string, ItemMusicData> ItemMusics;
public Dictionary<string, IconData> Icons;
public Dictionary<string, PlateData> Plates;
public Dictionary<string, TitleData> Titles;
public Dictionary<string, PartnerData> Partners;
public Dictionary<string, FrameData> Frames;
public Dictionary<string, TicketData> Tickets;
public Dictionary<string, CollectionTypeData> CollectionTypes;
public Dictionary<string, CollectionGenreData> CollectionGenres;
public Dictionary<string, PhotoFrameData> PhotoFrames;
public Dictionary<string, InformationData> Informations;
public Dictionary<string, UdemaeData> Udemaes;
public Dictionary<string, ClassData> Classes;
public Dictionary<string, UdemaeBossData> UdemaeBosses;
public Dictionary<string, UdemaeSeasonEventData> UdemaeSeasonEvents;
public Dictionary<string, UdemaeSeasonRewardData> UdemaeSeasonRewards;
public Dictionary<string, CardData> Cards;
public Dictionary<string, CardCharaData> CardCharas;
public Dictionary<string, CardTypeData> CardTypes;
public Dictionary<string, HolidayData> WeekdayBonuses;
public Dictionary<string, ChallengeData> Challenges;
public Dictionary<string, SerializeSortData> MusicRankings;
public Dictionary<string, SerializeSortData> MusicSorts;
public Dictionary<string, SerializeSortData> LoginBonusSorts;
public Dictionary<string, SerializeSortData> IconSorts;
public Dictionary<string, SerializeSortData> PlateSorts;
public Dictionary<string, SerializeSortData> TitleSorts;
public Dictionary<string, SerializeSortData> PartnerSorts;
public Dictionary<string, SerializeSortData> FrameSorts;
public Dictionary<string, SerializeSortData> TicketSorts;
public Dictionary<string, SerializeSortData> CollectionGenreSorts;
public Dictionary<string, SerializeSortData> CharaSorts;
public Dictionary<string, SerializeSortData> CharaGenreSorts;
}

View File

@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net46</TargetFramework>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AssemblyName>Assembly-CSharp.DisableEncryption.mm</AssemblyName>
</PropertyGroup>
<ItemGroup>
<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>
</Project>

View File

@ -0,0 +1,20 @@
// ReSharper disable CheckNamespace
// ReSharper disable InconsistentNaming
using MonoMod;
namespace Net.Packet;
public class patch_Packet : Packet
{
[MonoModIgnore]
public override PacketState Proc()
{
throw new NotImplementedException();
}
[MonoModReplace]
public new static string Obfuscator(string srcStr)
{
return srcStr;
}
}

View File

@ -0,0 +1,21 @@
// ReSharper disable CheckNamespace
// ReSharper disable InconsistentNaming
using MonoMod;
namespace Net;
[MonoModPatch("global::Net.CipherAES")]
public class patch_CipherAES
{
[MonoModReplace]
public static byte[] Encrypt(byte[] data)
{
return data;
}
[MonoModReplace]
public static byte[] Decrypt(byte[] data)
{
return data;
}
}

View File

@ -0,0 +1,17 @@
// ReSharper disable CheckNamespace
// ReSharper disable InconsistentNaming
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using MonoMod;
namespace Net;
public class patch_NetHttpClient : NetHttpClient
{
[MonoModReplace]
public new static bool CheckServerHash(object sender, X509Certificate certificate, X509Chain chain,
SslPolicyErrors sslPolicyErrors)
{
return true;
}
}

View File

@ -0,0 +1,17 @@
// ReSharper disable CheckNamespace
// ReSharper disable InconsistentNaming
namespace System.Net;
public class patch_WebHeaderCollection : WebHeaderCollection
{
public extern void orig_Add(string header, string value);
public new void Add(string header, string value)
{
if (header.ToLowerInvariant() == "mai-encoding")
{
return;
}
orig_Add(header, value);
}
}

View File

@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net46</TargetFramework>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AssemblyName>Assembly-CSharp.DisableReboot.mm</AssemblyName>
</PropertyGroup>
<ItemGroup>
<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>
</Project>

View File

@ -0,0 +1,21 @@
// ReSharper disable CheckNamespace
// ReSharper disable InconsistentNaming
using MonoMod;
namespace Manager.Operation;
public class patch_MaintenanceTimer : MaintenanceTimer
{
[MonoModReplace]
public new bool IsAutoRebootNeeded()
{
return false;
}
[MonoModReplace]
public new int GetAutoRebootSec()
{
return 86400;
}
}

View File

@ -0,0 +1,39 @@
// ReSharper disable CheckNamespace
// ReSharper disable InconsistentNaming
using MonoMod;
namespace Manager.Operation;
public class patch_SegaBootTimer : SegaBootTimer
{
[MonoModReplace]
public new bool IsSegaBootTime()
{
return false;
}
[MonoModReplace]
public new int GetGotoSegaBootSec()
{
return 86400;
}
[MonoModReplace]
public new bool IsSegaBootNeeded()
{
return false;
}
[MonoModReplace]
public new int GetRemainingMinutes()
{
return 1440;
}
[MonoModReplace]
public new bool IsCoinAcceptable()
{
return true;
}
}

View File

@ -1,43 +1,14 @@
<?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')"/>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{48B5F480-D749-48E9-9D26-E0E5260D95DE}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>FixLocaleIssues</RootNamespace>
<AssemblyName>Assembly-CSharp.FixLocaleIssues.mm</AssemblyName>
<TargetFrameworkVersion>v4.6.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<TargetFramework>net46</TargetFramework>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AssemblyName>Assembly-CSharp.FixLocaleIssues.mm</AssemblyName>
</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>
@ -55,19 +26,5 @@
<Private>False</Private>
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="MAI2System\patch_IniSection.cs" />
<Compile Include="Manager\patch_MA2Record.cs" />
<Compile Include="Manager\patch_SlideManager.cs" />
<Compile Include="Properties\AssemblyInfo.cs"/>
</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>

View File

@ -18,4 +18,4 @@ class patch_IniSection : IniSection
{
return float.TryParse(_variableValue, NumberStyles.Float, CultureInfo.InvariantCulture, out value);
}
}
}

View File

@ -6,7 +6,7 @@ using MonoMod;
namespace Manager;
class patch_MA2Record : MA2Record
public class patch_MA2Record : MA2Record
{
[MonoModIgnore]
private static extern Ma2fileParamID.Def getParamIDFromRec(Ma2fileRecordID.Def rec, int index);

View File

@ -1,9 +1,7 @@
// ReSharper disable CheckNamespace
// ReSharper disable InconsistentNaming
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text.RegularExpressions;
using System.Xml;
using MonoMod;
@ -16,7 +14,9 @@ public class patch_SlideManager : SlideManager
[MonoModIgnore]
private struct SlidePath
{
#pragma warning disable CS0649
public List<ControlPoint> Points;
#pragma warning restore CS0649
}
[MonoModIgnore]

View File

@ -1,35 +0,0 @@
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("FixLocaleIssues")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("FixLocaleIssues")]
[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("48B5F480-D749-48E9-9D26-E0E5260D95DE")]
// 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")]

View File

@ -0,0 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net46</TargetFramework>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AssemblyName>Assembly-CSharp.ImproveLoadTimes.mm</AssemblyName>
<NoWarn>CS0626</NoWarn>
</PropertyGroup>
<ItemGroup>
<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>
</Project>

View File

@ -0,0 +1,12 @@
// ReSharper disable CheckNamespace
using MonoMod;
namespace Manager.MaiStudio.Serialize;
[MonoModReplace]
[Serializable]
public class SerializeBase
{
public virtual void AddPath(string parentPath) { }
}

View File

@ -17,4 +17,4 @@ public class patch_FilePath : FilePath
path = parentPath + path;
}
}
}

View File

@ -0,0 +1,99 @@
// ReSharper disable CheckNamespace
// ReSharper disable InconsistentNaming
using System.Runtime.Serialization.Formatters.Binary;
namespace Manager;
public class patch_DataManager : DataManager
{
private const string _cacheFilename = "data_cache.bin";
private static Dictionary<string, object>? _cache;
private static bool _cacheBusted;
private static extern bool orig_Deserialize<T>(string filePath, out T dsr) where T : new();
private static bool Deserialize<T>(string filePath, out T dsr) where T : new()
{
try
{
_cache ??= LoadCache(_cacheFilename);
}
catch (Exception e)
{
System.Console.WriteLine("[ImproveLoadTimes] Could not load data cache: {0}", e);
_cache ??= new Dictionary<string, object>();
}
try
{
if (_cache.TryGetValue(filePath, out var dsrObject))
{
dsr = (T)dsrObject;
return true;
}
if (!orig_Deserialize(filePath, out dsr))
{
return false;
}
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
if (filePath == null || dsr == null)
{
return true;
}
_cache.Add(filePath, dsr);
_cacheBusted = true;
return true;
}
catch (Exception e)
{
System.Console.WriteLine("[ImproveLoadTimes] [ERROR] Could not load from cache: {0}", e);
return orig_Deserialize(filePath, out dsr);
}
}
private extern bool orig_IsLoaded();
public new bool IsLoaded()
{
var loaded = orig_IsLoaded();
if (!loaded || !_cacheBusted || _cache == null)
{
return loaded;
}
try
{
SaveCache(_cacheFilename, _cache);
}
catch (Exception e)
{
System.Console.WriteLine("[ImproveLoadTimes] [ERROR] Could not save to cache: {0}", e);
}
_cacheBusted = false;
return true;
}
private static Dictionary<string, object> LoadCache(string fileName)
{
if (!File.Exists(fileName))
{
return new Dictionary<string, object>();
}
System.Console.WriteLine("[ImproveLoadTimes] Loading data cache...");
using var fs = File.OpenRead(fileName);
return (Dictionary<string, object>)new BinaryFormatter().Deserialize(fs);
}
private static void SaveCache(string fileName, Dictionary<string, object> cache)
{
using var fs = File.Open(fileName, FileMode.Create, FileAccess.Write);
new BinaryFormatter().Serialize(fs, cache);
}
}

View File

@ -0,0 +1,21 @@
// ReSharper disable CheckNamespace
// ReSharper disable InconsistentNaming
using MonoMod;
using Process;
namespace Manager;
public class patch_PowerOnProcess : PowerOnProcess
{
#pragma warning disable CS0414 // Field is assigned but its value is never used
[MonoModIgnore]
private float _waitTime;
#pragma warning restore CS0414 // Field is assigned but its value is never used
[MonoModConstructor]
public patch_PowerOnProcess(ProcessDataContainer dataContainer) : base(dataContainer)
{
_waitTime = 0f;
}
}

View File

@ -1,14 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.IO.Ports" Version="9.0.0-preview.4.24266.19" />
</ItemGroup>
</Project>

View File

@ -1,154 +0,0 @@
// See https://aka.ms/new-console-template for more information
using System.Collections.Immutable;
using System.Reflection;
using System.Text;
var assemblyPath = args[0];
var targetPath = args[1];
var assembly = Assembly.LoadFrom(assemblyPath);
var types = assembly.GetTypes().Where(t => t.FullName.StartsWith("DB.") && t.FullName.EndsWith("IDEnum")).ToImmutableList();
foreach (var type in types)
{
var tableRecordType = assembly.GetType(type.FullName.Replace("IDEnum", "TableRecord"))!;
var patchClassName = $"patch_{type.Name}";
var readCommands = new StringBuilder();
var writeCommands = new StringBuilder();
var tableRecordFields = new StringBuilder();
foreach (var field in tableRecordType.GetFields(BindingFlags.Public | BindingFlags.Instance))
{
tableRecordFields.Append("public ");
if (field.FieldType.IsEnum)
{
tableRecordFields.Append("System.Int32 ");
}
else
{
tableRecordFields.Append(field.FieldType.FullName);
tableRecordFields.Append(" ");
}
tableRecordFields.Append(field.Name);
tableRecordFields.AppendLine(";");
readCommands.Append(field.Name)
.Append(" = (")
.Append(field.FieldType.FullName)
.Append(")src[i].")
.Append(field.Name)
.Append(", ");
writeCommands.Append(field.Name)
.Append(" = (")
.Append(field.FieldType.IsEnum ? "int" : field.FieldType.FullName)
.Append(")src[i].")
.Append(field.Name)
.Append(", ");
}
using var sw = File.CreateText(Path.Combine(targetPath, patchClassName + ".cs"));
sw.WriteLine($$"""
// ReSharper disable CheckNamespace
// ReSharper disable InconsistentNaming
using System;
using System.CodeDom.Compiler;
using System.IO;
using System.Text;
using MonoMod;
using LooseDBTables;
using UnityEngine;
namespace DB;
[MonoModIgnore]
[GeneratedCode("LooseDBTables.GeneratePatches", "1.0.0.0")]
public class {{type.Name}}
{
public static extern bool LoadFromFile(string filename);
public static extern void DumpToFile(string filename);
protected static {{tableRecordType.Name}}[] records;
}
[GeneratedCode("LooseDBTables.GeneratePatches", "1.0.0.0")]
[Serializable]
public class Serializable{{tableRecordType.Name}} {
{{tableRecordFields}}
}
[GeneratedCode("LooseDBTables.GeneratePatches", "1.0.0.0")]
public class {{patchClassName}} : {{type.Name}} {
public new static bool LoadFromFile(string filename) {
if (!File.Exists(filename)) {
return false;
}
var table = JsonUtility.FromJson<Table<Serializable{{tableRecordType.Name}}>>(File.ReadAllText(filename));
try
{
if (table.records.Length != records.Length) {
Debug.LogError($"Count read error: {filename}");
return false;
}
var src = table.records;
var dst = records;
for (var i = 0; i < table.records.Length; i++) {
dst[i] = new {{tableRecordType.Name}} { {{readCommands}} };
}
}
catch
{
Debug.LogError($"File read error: {filename}");
return false;
}
return true;
}
public new static void DumpToFile(string filename) {
var table = new Table<Serializable{{tableRecordType.Name}}>() {
records = new Serializable{{tableRecordType.Name}}[records.Length]
};
var src = records;
var dst = table.records;
for (var i = 0; i < records.Length; i++) {
dst[i] = new Serializable{{tableRecordType.Name}} { {{writeCommands}} };
}
File.WriteAllText(filename, JsonUtility.ToJson(table, true), Encoding.UTF8);
}
}
""");
}
using var dbLoaderSw = File.CreateText(Path.Combine(targetPath, "DBLoader.cs"));
dbLoaderSw.WriteLine($$"""
// ReSharper disable CheckNamespace
using System;
using System.CodeDom.Compiler;
using System.IO;
using System.Text;
namespace DB;
[GeneratedCode("LooseDBTables.GeneratePatches", "1.0.0.0")]
public class DBLoader
{
public static void LoadAll(string dirPath)
{
{{string.Join("\n", types.Select(t => $"{t.Name}.LoadFromFile(Path.Combine(dirPath, \"{t.Name.Replace("IDEnum", "TableRecord")}.json\"));"))}}
}
public static void DumpAll(string dirPath)
{
{{string.Join("\n", types.Select(t => $"{t.Name}.DumpToFile(Path.Combine(dirPath, \"{t.Name.Replace("IDEnum", "TableRecord")}.json\"));"))}}
}
}
""");

View File

@ -1,43 +1,15 @@
<?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')"/>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{F15988CC-BDF0-4F86-811B-BAE18EEA6519}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>LooseDBTables</RootNamespace>
<AssemblyName>Assembly-CSharp.LooseDBTables.mm</AssemblyName>
<TargetFrameworkVersion>v4.6.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<TargetFramework>net46</TargetFramework>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AssemblyName>Assembly-CSharp.LooseDBTables.mm</AssemblyName>
<NoWarn>CS0626,CS0436,CS8618</NoWarn>
</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>
@ -59,19 +31,5 @@
<Private>False</Private>
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="DB\*.cs" />
<Compile Include="Process\patch_PowerOnProcess.cs" />
<Compile Include="Properties\AssemblyInfo.cs"/>
<Compile Include="Table.cs" />
</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>

View File

@ -1,35 +0,0 @@
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("LooseDBTables")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("LooseDBTables")]
[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("F15988CC-BDF0-4F86-811B-BAE18EEA6519")]
// 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")]

View 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++;
}
}

View 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);
}

View File

@ -0,0 +1,79 @@
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");
}
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);
}
Refs.Notes._noteData.Add(note);
NoteIndex++;
}
}

View 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();
}
}

View File

@ -0,0 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net46</TargetFramework>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AssemblyName>Assembly-CSharp.MoreChartFormats.mm</AssemblyName>
<NoWarn>CS8618,CS8625</NoWarn>
</PropertyGroup>
<ItemGroup>
<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>
</Project>

View File

@ -0,0 +1,11 @@
using Manager;
namespace MoreChartFormats;
public class NotesReferences
{
public NotesReader Reader;
public NotesHeader Header;
public NotesComposition Composition;
public NotesData Notes;
}

View 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;
}
}

View 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;
}
}

View File

@ -0,0 +1,9 @@
namespace MoreChartFormats.Simai.Errors
{
public class InvalidSyntaxException : SimaiException
{
public InvalidSyntaxException(int line, int character) : base(line, character)
{
}
}
}

View 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
}
}
}

View 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;
}
}
}

View File

@ -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;
}
}
}

View 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)
{
}
}
}

View File

@ -0,0 +1,9 @@
namespace MoreChartFormats.Simai.Errors
{
public class UnterminatedSectionException : SimaiException
{
public UnterminatedSectionException(int line, int character) : base(line, character)
{
}
}
}

View 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;
}
}

View 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
}
}

View 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];
}
}
}

View 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);
}
}
}

View File

@ -0,0 +1,9 @@
using Manager;
namespace MoreChartFormats.Simai.Structures;
public class SlideSegment
{
public NoteData NoteData;
public SlideTiming Timing;
}

View File

@ -0,0 +1,9 @@
using Manager;
namespace MoreChartFormats.Simai.Structures;
public class SlideTiming
{
public NotesTime? Delay;
public NotesTime Duration;
}

View 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;
}
}

View 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);
}
}

View 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;
}
}

View File

@ -0,0 +1,211 @@
// ReSharper disable CheckNamespace
// ReSharper disable InconsistentNaming
using System.Xml.Serialization;
using Manager.MaiStudio.Serialize;
using MonoMod;
using MoreChartFormats;
using MoreChartFormats.MaiSxt;
using MoreChartFormats.Simai;
using MoreChartFormats.Simai.Errors;
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);
if (note.type.isTouch() && note.touchArea is TouchSensorType.D or TouchSensorType.E)
{
note.startButtonPos = ConvertMirrorTouchEPosition(note.startButtonPos);
}
else
{
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 DEBUG
System.Console.WriteLine("[MoreChartFormats] [Simai] Calculating chart data");
#endif
calcAll();
#if DEBUG
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());
#endif
}
catch (SimaiException e)
{
System.Console.WriteLine($"[MoreChartFormats] [Simai] There was an error loading the chart at line {e.line}, col {e.character}: {e} ");
return false;
}
catch (Exception e)
{
System.Console.WriteLine($"[MoreChartFormats] [Simai] 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();
}
}

121
README.md
View File

@ -1,13 +1,108 @@
## sinmai-mods
Miscellaneous mods for maimai DX
### CachedDataManager
Speeds up game reboots by caching loaded data. Cache is stored in the `dataCache` folder
and should be deleted if any issues arise.
### FixLocaleIssues
Attempts to fix charts not loading on some system locales.
Adds `CultureInfo.InvariantCulture` to all instances of `float.Parse` and `float.TryParse`
so charts will still load on locales where the comma is the decimal separator.
## sinmai-mods
Mods for maimai DX. Patch with MonoMod except specified
otherwise.
## Mod list
### DisableEncryption
Disables title server encryption.
### DisableReboot
Disables automatic reboots.
### FixLocaleIssues
Attempt to fix charts not loading on regions where the system's
decimal separator is not the dot (`.`).
### ImproveLoadTimes
Speed up game reboots by caching game data. Cache data is stored
in `data_cache.bin` and can be safely deleted if issues arise.
### LooseDBTables
Load game tables from `.json` files in `Sinmai_Data/StreamingAssets/DB`.
Useful for string edits (e.g. translation).
Tables are automatically generated if the `DB` folder does not
exist.
### MoreChartFormats
Load charts written in various formats:
- [simai](https://w.atwiki.jp/simai)
- 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 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.
<details>
<summary>Simai caveats</summary>
- **`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.
</details>
<details>
<summary>SXT caveats</summary>
- Encrypted chart files (`.srb`/`.szb`/`.scb`/`.sdb`) are not
supported. Decrypt them before loading into the game.
- Since this chart format does not contain timing data, the
song's BPM is retrieved by loading the `Music.xml` associated
with the chart, **and it is assumed that the chart and
`Music.xml` is in the same folder**. The SXT loader will not
work if the chart file is somehow in a different folder
from `Music.xml`.
</details>
### Singleplayer
Show only the left monitor at the center of the screen.
### TouchPanel
Set touch panel sensitivity for individual regions, or introduce
delay on the touch panel. It is configured by adding an optional
section to `mai2.ini`:
```ini
[TouchPanel]
; Introduce delay to touch inputs to make it line up with
; button inputs
DelayMs=0
; Customize the activation threshold for specific touch
; sensors (optional). The touch sensor values can be found by
; going into input test in the game test menu.
SensitivityA1=90
SensitivityA2=90
...
```
### UnlockFrameRate
**BepInEx mod.**
Change the target FPS. Also comes with an FPS counter. The
configuration file for changing the FPS is at
`BepInEx/config/io.github.beerpsi.sinmai.framerate.cfg` and is
already documented.

View File

@ -0,0 +1,30 @@
// ReSharper disable CheckNamespace
// ReSharper disable InconsistentNaming
using MonoMod;
using UnityEngine;
namespace Main;
public class patch_GameMainObject : GameMainObject
{
[MonoModIgnore]
private Transform rightMonitor;
private extern void orig_Start();
private void Start()
{
orig_Start();
var mainCamera = Camera.main;
if (mainCamera == null)
{
return;
}
var position = mainCamera.gameObject.transform.position;
mainCamera.gameObject.transform.position = new Vector3(-540f, position.y, position.z);
rightMonitor.transform.localScale = Vector3.zero;
}
}

View File

@ -0,0 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net46</TargetFramework>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AssemblyName>Assembly-CSharp.Singleplayer.mm</AssemblyName>
<NoWarn>CS0626,CS0649,CS8618</NoWarn>
</PropertyGroup>
<ItemGroup>
<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>
</Project>

View File

@ -0,0 +1,65 @@
// ReSharper disable CheckNamespace
// ReSharper disable InconsistentNaming
using System.Globalization;
using MAI2System;
using Manager;
namespace IO;
class patch_NewTouchPanel : NewTouchPanel
{
private int _touchPanelDelayMsec;
private Dictionary<InputManager.TouchPanelArea, byte> _sensitivityOverrides = new();
public extern void orig_Initialize(uint index);
public new void Initialize(uint index)
{
orig_Initialize(index);
using var ini = new IniFile("mai2.ini");
_touchPanelDelayMsec = ini.getIntValue("TouchPanel", "DelayMs", 0);
foreach (InputManager.TouchPanelArea sensor in Enum.GetValues(typeof(InputManager.TouchPanelArea)))
{
var sensitivityStr = ini.getValue("TouchPanel", $"Sensitivity{sensor}", null);
if (sensitivityStr == null)
{
continue;
}
if (byte.TryParse(sensitivityStr, NumberStyles.Integer, CultureInfo.InvariantCulture, out var sensitivity))
{
System.Console.WriteLine($"[TouchPanel] [WARN] Invalid value for TouchPanel.Sensitivity{sensor}: {sensitivityStr}");
continue;
}
_sensitivityOverrides[sensor] = sensitivity;
}
}
private extern void orig_Recv();
private void Recv()
{
if (_touchPanelDelayMsec > 0)
{
Thread.Sleep(_touchPanelDelayMsec);
}
orig_Recv();
}
private extern void orig_SetTouchPanelSensitivity(List<byte> sensitivity);
private void SetTouchPanelSensitivity(List<byte> sensitivity)
{
foreach (var s in _sensitivityOverrides)
{
System.Console.WriteLine($"[TouchPanel] Overriding sensitivity for panel {s.Key}: {s.Value}");
sensitivity[(int)s.Key] = s.Value;
}
orig_SetTouchPanelSensitivity(sensitivity);
}
}

View File

@ -0,0 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net46</TargetFramework>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AssemblyName>Assembly-CSharp.TouchPanel.mm</AssemblyName>
<NoWarn>CS0626</NoWarn>
</PropertyGroup>
<ItemGroup>
<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>
</Project>

View File

@ -0,0 +1,112 @@
#pragma warning disable IDE0051
// ReSharper disable UnusedMember.Local
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;
using UnityEngine;
namespace UnlockFrameRate;
[BepInPlugin("io.github.beerpsi.sinmai.framerate", "FrameRate", "0.1.0")]
[BepInProcess("Sinmai.exe")]
public class FrameRatePlugin : BaseUnityPlugin
{
public new static ManualLogSource Logger = BepInEx.Logging.Logger.CreateLogSource("FrameRate");
public static FrameRatePlugin Instance { get; private set; }
public int TargetFrameRate => _configFrameRate.Value;
public bool PatchJudgementTiming => _configChangeJudgementTiming.Value;
private ConfigEntry<int> _configFrameRate;
private ConfigEntry<int> _configVSyncCount;
private ConfigEntry<bool> _configDisplayFps;
private ConfigEntry<bool> _configChangeJudgementTiming;
private Harmony _harmony;
private void Awake()
{
Instance = this;
_configFrameRate = Config.Bind(
"Config",
"FrameRate",
60,
"The frame rate to run the game at");
_configVSyncCount = Config.Bind(
"Config",
"VSyncCount",
0,
"Supported values are 0 to 4. 0 disables VSync and the rest enables it.\nWhen this is enabled, FrameRate is ignored and the target frame rate\nis calculated by taking the current refresh rate divided by VSyncCount\n(e.g. 120Hz at VSyncCount 2 => 60fps).");
_configDisplayFps = Config.Bind(
"Config",
"DisplayFPS",
false,
"Show an FPS counter");
_configChangeJudgementTiming = Config.Bind(
"Config",
"ChangeJudgementTiming",
true,
"Adjusts JUDGEMENT TIMING options to match the new frame rate.\nIf this is enabled, 0.1 in-game offset is equivalent to 10000 / FrameRate.");
if (_configVSyncCount.Value is > 0 and <= 4)
{
QualitySettings.vSyncCount = _configVSyncCount.Value;
Logger.LogInfo(
"VSync is enabled (VSyncCount={0}), target frame rate is {1}fps",
_configVSyncCount.Value,
TargetFrameRate);
}
else
{
Application.targetFrameRate = TargetFrameRate;
QualitySettings.vSyncCount = 0;
Logger.LogInfo(
"VSync is disabled, target frame rate is {0}fps",
TargetFrameRate);
}
Time.fixedDeltaTime = 1f / TargetFrameRate * Time.timeScale;
Logger.LogDebug(
"Setting Time.fixedDeltaTime to {0}s",
Time.fixedDeltaTime);
Logger.LogDebug("Patching hardcoded frame time usages");
_harmony = new Harmony("io.github.beerpsi.sinmai.framerate");
_harmony.PatchAll();
}
private const int FpsSamples = 100;
private int _currentFps = 0;
private int _currentSampleCount = FpsSamples;
private float _totalTime = 0;
private void OnGUI()
{
if (!_configDisplayFps.Value)
{
return;
}
_totalTime += Time.deltaTime;
_currentSampleCount--;
if (_currentSampleCount == 0)
{
_currentFps = (int)(FpsSamples / _totalTime);
_totalTime = 0f;
_currentSampleCount = FpsSamples;
}
GUI.Label(new Rect(10f, 10f, 150f, 100f), "FPS: " + _currentFps);
}
private void OnDestroy()
{
_harmony?.UnpatchSelf();
}
}

View File

@ -0,0 +1,17 @@
using BepInEx.Logging;
namespace UnlockFrameRate;
internal static class ManualLogSourceExtensions
{
public static void LogInfo(this ManualLogSource logger, string format, params object[] args)
{
logger.LogInfo(string.Format(format, args));
}
public static void LogDebug(this ManualLogSource logger, string format, params object[] args)
{
logger.LogDebug(string.Format(format, args));
}
}

View File

@ -0,0 +1,97 @@
// ReSharper disable InconsistentNaming
using HarmonyLib;
using Manager;
using Manager.UserDatas;
using Monitor;
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;
namespace UnlockFrameRate;
[HarmonyPatch]
internal class PatchFrameTime
{
private const float OriginalFrameRate = 60f;
private const float OriginalFrameTime = 1000f / OriginalFrameRate;
private const float OriginalFramePerMilliseconds = OriginalFrameRate / 1000;
private static IEnumerable<MethodBase> TargetMethods()
{
// This shouldn't be patched, because they make the judgements
// harder or easier depending on your frame rate.
// var noteJudge = AccessTools.TypeByName("NoteJudge");
// var juggeTiming = AccessTools.Inner(noteJudge, "JuggeTiming"); // lol
// yield return AccessTools.Constructor(juggeTiming);
// This changes the effect of judgement timing based on the set FPS,
// so +2.0 at 120Hz will only add 17ms instead of 33ms.
if (FrameRatePlugin.Instance.PatchJudgementTiming)
{
yield return AccessTools.Method(typeof(NoteJudge), nameof(NoteJudge.GetJudgeTiming));
yield return AccessTools.Method(typeof(NoteJudge), nameof(NoteJudge.GetSlideJudgeTiming));
yield return AccessTools.Method(typeof(UserOption), nameof(UserOption.GetAdjustMSec));
}
yield return AccessTools.Method(typeof(NoteBase), "IsNoteCheckTimeStart");
yield return AccessTools.Method(typeof(TouchNoteB), "GetNoteYPosition");
yield return AccessTools.Method(typeof(SlideRoot), "IsNoteCheckTimeStart");
yield return AccessTools.Method(typeof(SlideJudge), nameof(SlideJudge.Initialize));
yield return AccessTools.Method(typeof(JudgeGrade), nameof(JudgeGrade.Initialize));
yield return AccessTools.Method(typeof(NotesManager), nameof(NotesManager.getPlayFirstMsec));
yield return AccessTools.Method(typeof(NotesManager), nameof(NotesManager.getPlayFinalMsec));
yield return AccessTools.Method(typeof(NotesManager), nameof(NotesManager.getCurrentDrawFrame));
yield return AccessTools.Method(typeof(NotesReader), nameof(NotesReader.calcFrame));
yield return AccessTools.Method(typeof(NotesReader), nameof(NotesReader.GetBPM_Frame));
yield return AccessTools.Method(typeof(NotesReader), nameof(NotesReader.getMeter_Frame));
yield return AccessTools.Method(typeof(NoteData), nameof(NoteData.getLengthFrame));
yield return AccessTools.Method(typeof(GameManager), nameof(GameManager.UpdateGameTimer));
yield return AccessTools.PropertyGetter(typeof(NotesTime), nameof(NotesTime.frame));
}
private static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions, MethodBase __originalMethod)
{
var targetFrameTime = 1000f / FrameRatePlugin.Instance.TargetFrameRate;
var targetFramePerMs = (float)FrameRatePlugin.Instance.TargetFrameRate / 1000;
var i = 0;
foreach (var instruction in instructions)
{
if (instruction.opcode != OpCodes.Ldc_R4 || instruction.operand is not float operand)
{
yield return instruction;
i++;
continue;
}
var overridden = false;
if (Math.Abs(operand - OriginalFrameTime) < float.Epsilon)
{
instruction.operand = targetFrameTime;
overridden = true;
}
else if (Math.Abs(operand - OriginalFramePerMilliseconds) < float.Epsilon)
{
instruction.operand = targetFramePerMs;
overridden = true;
}
if (overridden)
{
FrameRatePlugin.Logger.LogDebug(
"Overrode constant at opcode index {0} in {1}: {2} => {3}",
i,
__originalMethod.Name,
operand,
instruction.operand);
}
yield return instruction;
i++;
}
}
}

View File

@ -0,0 +1,43 @@
using HarmonyLib;
using Monitor;
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;
namespace UnlockFrameRate;
[HarmonyPatch(typeof(NoteBase), "GetMaiBugAdjustMSec")]
internal class PatchGetMaiBugAdjustMSec
{
private const float OriginalFrameRate = 60f;
private static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions, MethodBase __originalMethod)
{
var i = 0;
foreach (var instruction in instructions)
{
if (instruction.opcode != OpCodes.Ldc_R4 || instruction.operand is not float operand)
{
yield return instruction;
i++;
continue;
}
if (Math.Abs(operand - OriginalFrameRate) < float.Epsilon)
{
instruction.operand = (float)FrameRatePlugin.Instance.TargetFrameRate;
FrameRatePlugin.Logger.LogDebug(
"Overrode constant at opcode index {0} in {1}: {2} => {3}",
i,
__originalMethod.Name,
operand,
instruction.operand);
}
yield return instruction;
i++;
}
}
}

View File

@ -0,0 +1,37 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net46</TargetFramework>
<LangVersion>latest</LangVersion>
<AssemblyName>UnlockFrameRate</AssemblyName>
<Version>1.0.0</Version>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BepInEx.Analyzers" Version="1.*" PrivateAssets="all"/>
<PackageReference Include="BepInEx.Core" Version="5.*"/>
<PackageReference Include="BepInEx.PluginInfoProps" Version="1.*"/>
<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>
<Reference Include="UnityEngine.IMGUIModule">
<HintPath>..\External\UnityEngine.IMGUIModule.dll</HintPath>
<Private>False</Private>
</Reference>
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework.TrimEnd(`0123456789`))' == 'net'">
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.2" PrivateAssets="all"/>
</ItemGroup>
</Project>

View File

@ -1,22 +1,70 @@

Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CachedDataManager", "CachedDataManager\CachedDataManager.csproj", "{F1C1B6BF-626C-4F10-8672-2F9596706CA6}"
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FixLocaleIssues", "FixLocaleIssues\FixLocaleIssues.csproj", "{C1D1272C-5BC5-4EFD-A9DA-BB8BE8B16280}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FixLocaleIssues", "FixLocaleIssues\FixLocaleIssues.csproj", "{48B5F480-D749-48E9-9D26-E0E5260D95DE}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImproveLoadTimes", "ImproveLoadTimes\ImproveLoadTimes.csproj", "{B18FA6C6-79C3-4851-A88E-161D273CBC0F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TouchPanel", "TouchPanel\TouchPanel.csproj", "{B8E4A429-DE84-4565-AD51-87ECCBD629C5}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Singleplayer", "Singleplayer\Singleplayer.csproj", "{0A9D8A7E-984F-4406-B497-E45CFF1073E6}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DisableEncryption", "DisableEncryption\DisableEncryption.csproj", "{FCBAB1FC-A47F-4939-B17B-F07F1A9DF4F7}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DisableReboot", "DisableReboot\DisableReboot.csproj", "{B645EB54-CC74-437F-9255-2D9BFED87DF2}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MoreChartFormats", "MoreChartFormats\MoreChartFormats.csproj", "{1966AAE3-19E7-4BA0-862A-A3C54DB51B3F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LooseDBTables", "LooseDBTables\LooseDBTables.csproj", "{10E43267-ADB6-44AC-BABA-80D4CF4C7BC8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnlockFrameRate", "UnlockFrameRate\UnlockFrameRate.csproj", "{54622F97-9187-482C-A9DC-06E1855EF2D7}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{F1C1B6BF-626C-4F10-8672-2F9596706CA6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F1C1B6BF-626C-4F10-8672-2F9596706CA6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F1C1B6BF-626C-4F10-8672-2F9596706CA6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F1C1B6BF-626C-4F10-8672-2F9596706CA6}.Release|Any CPU.Build.0 = Release|Any CPU
{48B5F480-D749-48E9-9D26-E0E5260D95DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{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
{C1D1272C-5BC5-4EFD-A9DA-BB8BE8B16280}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C1D1272C-5BC5-4EFD-A9DA-BB8BE8B16280}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C1D1272C-5BC5-4EFD-A9DA-BB8BE8B16280}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C1D1272C-5BC5-4EFD-A9DA-BB8BE8B16280}.Release|Any CPU.Build.0 = Release|Any CPU
{B18FA6C6-79C3-4851-A88E-161D273CBC0F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B18FA6C6-79C3-4851-A88E-161D273CBC0F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B18FA6C6-79C3-4851-A88E-161D273CBC0F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B18FA6C6-79C3-4851-A88E-161D273CBC0F}.Release|Any CPU.Build.0 = Release|Any CPU
{B8E4A429-DE84-4565-AD51-87ECCBD629C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B8E4A429-DE84-4565-AD51-87ECCBD629C5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B8E4A429-DE84-4565-AD51-87ECCBD629C5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B8E4A429-DE84-4565-AD51-87ECCBD629C5}.Release|Any CPU.Build.0 = Release|Any CPU
{0A9D8A7E-984F-4406-B497-E45CFF1073E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0A9D8A7E-984F-4406-B497-E45CFF1073E6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0A9D8A7E-984F-4406-B497-E45CFF1073E6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0A9D8A7E-984F-4406-B497-E45CFF1073E6}.Release|Any CPU.Build.0 = Release|Any CPU
{FCBAB1FC-A47F-4939-B17B-F07F1A9DF4F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FCBAB1FC-A47F-4939-B17B-F07F1A9DF4F7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FCBAB1FC-A47F-4939-B17B-F07F1A9DF4F7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FCBAB1FC-A47F-4939-B17B-F07F1A9DF4F7}.Release|Any CPU.Build.0 = Release|Any CPU
{B645EB54-CC74-437F-9255-2D9BFED87DF2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B645EB54-CC74-437F-9255-2D9BFED87DF2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B645EB54-CC74-437F-9255-2D9BFED87DF2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B645EB54-CC74-437F-9255-2D9BFED87DF2}.Release|Any CPU.Build.0 = Release|Any CPU
{1966AAE3-19E7-4BA0-862A-A3C54DB51B3F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1966AAE3-19E7-4BA0-862A-A3C54DB51B3F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1966AAE3-19E7-4BA0-862A-A3C54DB51B3F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1966AAE3-19E7-4BA0-862A-A3C54DB51B3F}.Release|Any CPU.Build.0 = Release|Any CPU
{10E43267-ADB6-44AC-BABA-80D4CF4C7BC8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{10E43267-ADB6-44AC-BABA-80D4CF4C7BC8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{10E43267-ADB6-44AC-BABA-80D4CF4C7BC8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{10E43267-ADB6-44AC-BABA-80D4CF4C7BC8}.Release|Any CPU.Build.0 = Release|Any CPU
{54622F97-9187-482C-A9DC-06E1855EF2D7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{54622F97-9187-482C-A9DC-06E1855EF2D7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{54622F97-9187-482C-A9DC-06E1855EF2D7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{54622F97-9187-482C-A9DC-06E1855EF2D7}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal