[RF] AquaMai moved to new repo

This commit is contained in:
Clansty
2024-11-30 15:18:08 +08:00
parent 4afe2160e1
commit 2482881117
249 changed files with 1 additions and 13936 deletions

View File

@@ -1,72 +0,0 @@
name: AquaMai Build
on:
workflow_dispatch:
push:
paths:
- AquaMai/**
branches:
- v1-dev
pull_request_target:
paths:
- AquaMai/**
branches:
- v1-dev
jobs:
build:
runs-on: windows-latest
steps:
- name: Fix Git line encoding bug
run: |
git config --global core.autocrlf false
git config --global core.eol lf
- uses: actions/checkout@v4
- name: Checkout Assets
uses: clansty/checkout@main
with:
repository: MewoLab/AquaMai-Build-Assets
ssh-key: ${{ secrets.BUILD_ASSETS_KEY }}
path: build-assets
max-attempts: 50
min-retry-interval: 1
max-retry-interval: 5
- name: Build AquaMai
shell: cmd
run: |
copy /y build-assets\SDEZ\* AquaMai\Libs
cd AquaMai
dotnet build -c Release /p:DefineConstants="CI"
- name: Prepare artifact
shell: cmd
run: |
cd AquaMai\Output
mkdir Upload
move AquaMai.dll Upload
move AquaMai.*.toml Upload
- uses: actions/upload-artifact@v4
with:
name: AquaMai
path: AquaMai\Output\Upload
- name: Send to Telegram
if: github.event_name != 'pull_request_target'
run: |
$Uri = "https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMediaGroup"
$Form = @{
chat_id = "-1002231087502"
media = @(
@{ type = "document"; media = "attach://aquamai_main"; caption = "${{ github.event.commits[0].message }}" },
@{ type = "document"; media = "attach://aquamai_zh" }
@{ type = "document"; media = "attach://aquamai_en" }
) | ConvertTo-Json
aquamai_main = Get-Item AquaMai\Output\Upload\AquaMai.dll
aquamai_zh = Get-Item AquaMai\Output\Upload\AquaMai.zh.toml
aquamai_en = Get-Item AquaMai\Output\Upload\AquaMai.en.toml
}
Invoke-RestMethod -Uri $uri -Form $Form -Method Post

374
AquaMai/.gitignore vendored
View File

@@ -1,374 +0,0 @@
# Created by https://www.toptal.com/developers/gitignore/api/git,visualstudio
# Edit at https://www.toptal.com/developers/gitignore?templates=git,visualstudio
### Git ###
# Created by git for backups. To disable backups in Git:
# $ git config --global mergetool.keepBackup false
*.orig
# Created by git when using merge tools for conflicts
*.BACKUP.*
*.BASE.*
*.LOCAL.*
*.REMOTE.*
*_BACKUP_*.txt
*_BASE_*.txt
*_LOCAL_*.txt
*_REMOTE_*.txt
### VisualStudio ###
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*[.json, .xml, .info]
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# End of https://www.toptal.com/developers/gitignore/api/git,visualstudio
Output
.idea

View File

@@ -1,50 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Release</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{4C0C68C3-8B2E-4CA8-A26D-AE87CF2A38A5}</ProjectGuid>
<OutputType>Library</OutputType>
<RootNamespace>AquaMai.Build</RootNamespace>
<AssemblyName>AquaMai.Build</AssemblyName>
<TargetFramework>netstandard2.0</TargetFramework>
<FileAlignment>512</FileAlignment>
<Deterministic>true</Deterministic>
<LangVersion>12</LangVersion>
<NoWarn>414;NU1702</NoWarn>
<LibsPath>$(ProjectDir)../Libs/</LibsPath>
<OutputPath>$(ProjectDir)../Output/</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<EnableDefaultEmbeddedResourceItems>false</EnableDefaultEmbeddedResourceItems>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugSymbols>false</DebugSymbols>
<DebugType>None</DebugType>
<Optimize>true</Optimize>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DefineConstants>DEBUG</DefineConstants>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../AquaMai.Config.HeadlessLoader/AquaMai.Config.HeadlessLoader.csproj" />
</ItemGroup>
<ItemGroup>
<Reference Include="Mono.Cecil">
<HintPath>$(LibsPath)Mono.Cecil.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Build.Framework" Version="17.0.0" />
<PackageReference Include="Microsoft.Build.Utilities.Core" Version="17.0.0" />
</ItemGroup>
</Project>

View File

@@ -1,42 +0,0 @@
using System;
using System.IO;
using AquaMai.Config.Interfaces;
using AquaMai.Config.HeadlessLoader;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
public class GenerateExampleConfig : Task
{
[Required]
public string DllPath { get; set; }
[Required]
public string OutputPath { get; set; }
public override bool Execute()
{
try
{
var configInterface = HeadlessConfigLoader.LoadFromPacked(DllPath);
var config = configInterface.CreateConfig();
foreach (var lang in (string[]) ["en", "zh"])
{
var configSerializer = configInterface.CreateConfigSerializer(new IConfigSerializer.Options()
{
Lang = lang,
IncludeBanner = true,
OverrideLocaleValue = true
});
var example = configSerializer.Serialize(config);
File.WriteAllText(Path.Combine(OutputPath, $"AquaMai.{lang}.toml"), example);
}
return true;
}
catch (Exception e)
{
Log.LogErrorFromException(e, true);
return false;
}
}
}

View File

@@ -1,52 +0,0 @@
using System;
using System.IO;
using System.IO.Compression;
using System.Linq;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using Mono.Cecil;
public class PostBuildPatch : Task
{
[Required]
public string DllPath { get; set; }
public override bool Execute()
{
try
{
var assembly = AssemblyDefinition.ReadAssembly(new MemoryStream(File.ReadAllBytes(DllPath)));
CompressEmbeddedAssemblies(assembly);
var outputStream = new MemoryStream();
assembly.Write(outputStream);
File.WriteAllBytes(DllPath, outputStream.ToArray());
return true;
}
catch (Exception e)
{
Log.LogErrorFromException(e, true);
return false;
}
}
private void CompressEmbeddedAssemblies(AssemblyDefinition assembly)
{
foreach (var resource in assembly.MainModule.Resources.ToList())
{
if (resource.Name.EndsWith(".dll") && resource is EmbeddedResource embeddedResource)
{
using var compressedStream = new MemoryStream();
using (var deflateStream = new DeflateStream(compressedStream, CompressionLevel.Optimal))
{
embeddedResource.GetResourceStream().CopyTo(deflateStream);
}
var compressedBytes = compressedStream.ToArray();
Log.LogMessage($"Compressed {resource.Name} from {embeddedResource.GetResourceStream().Length} to {compressedBytes.Length} bytes");
assembly.MainModule.Resources.Remove(resource);
assembly.MainModule.Resources.Add(new EmbeddedResource(resource.Name + ".compressed", resource.Attributes, compressedBytes));
}
}
}
}

View File

@@ -1,45 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Release</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{6B5E1F3E-D012-4CFB-A2FA-26A6CE06BE66}</ProjectGuid>
<OutputType>Library</OutputType>
<RootNamespace>AquaMai.Config.HeadlessLoader</RootNamespace>
<AssemblyName>AquaMai.Config.HeadlessLoader</AssemblyName>
<TargetFramework>netstandard2.0</TargetFramework>
<FileAlignment>512</FileAlignment>
<Deterministic>true</Deterministic>
<LangVersion>12</LangVersion>
<NoWarn>414;NU1702</NoWarn>
<LibsPath>$(ProjectDir)../Libs/</LibsPath>
<OutputPath>$(ProjectDir)../Output/</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<EnableDefaultEmbeddedResourceItems>false</EnableDefaultEmbeddedResourceItems>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugSymbols>false</DebugSymbols>
<DebugType>None</DebugType>
<Optimize>true</Optimize>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DefineConstants>DEBUG</DefineConstants>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../AquaMai.Config.Interfaces/AquaMai.Config.Interfaces.csproj" />
</ItemGroup>
<ItemGroup>
<Reference Include="Mono.Cecil">
<HintPath>$(LibsPath)Mono.Cecil.dll</HintPath>
</Reference>
</ItemGroup>
</Project>

View File

@@ -1,68 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using Mono.Cecil;
namespace AquaMai.Config.HeadlessLoader;
class ConfigAssemblyLoader
{
public static Assembly LoadConfigAssembly(AssemblyDefinition assembly)
{
var references = assembly.MainModule.AssemblyReferences;
foreach (var reference in references)
{
if (reference.Name == "mscorlib" || reference.Name == "System" || reference.Name.StartsWith("System."))
{
reference.Name = "netstandard";
reference.Version = new Version(2, 0, 0, 0);
reference.PublicKeyToken = null;
}
}
var targetFrameworkAttribute = assembly.CustomAttributes.FirstOrDefault(attr => attr.AttributeType.Name == "TargetFrameworkAttribute");
if (targetFrameworkAttribute != null)
{
targetFrameworkAttribute.ConstructorArguments.Clear();
targetFrameworkAttribute.ConstructorArguments.Add(new CustomAttributeArgument(
assembly.MainModule.TypeSystem.String, ".NETStandard,Version=v2.0"));
targetFrameworkAttribute.Properties.Clear();
targetFrameworkAttribute.Properties.Add(new Mono.Cecil.CustomAttributeNamedArgument(
"FrameworkDisplayName", new CustomAttributeArgument(assembly.MainModule.TypeSystem.String, ".NET Standard 2.0")));
}
var stream = new MemoryStream();
assembly.Write(stream);
FixLoadedAssemblyResolution();
return AppDomain.CurrentDomain.Load(stream.ToArray());
}
private static bool FixedLoadedAssemblyResolution = false;
// XXX: Why, without this, the already loaded assemblies are not resolved?
public static void FixLoadedAssemblyResolution()
{
if (FixedLoadedAssemblyResolution)
{
return;
}
FixedLoadedAssemblyResolution = true;
var loadedAssemblies = new Dictionary<string, Assembly>();
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
{
loadedAssemblies[assembly.FullName] = assembly;
}
AppDomain.CurrentDomain.AssemblyResolve += (sender, args) =>
{
if (loadedAssemblies.TryGetValue(args.Name, out var assembly))
{
return assembly;
}
return null;
};
}
}

View File

@@ -1,11 +0,0 @@
using Mono.Cecil;
namespace AquaMai.Config.HeadlessLoader;
public class CustomAssemblyResolver : DefaultAssemblyResolver
{
public new void RegisterAssembly(AssemblyDefinition assembly)
{
base.RegisterAssembly(assembly);
}
}

View File

@@ -1,65 +0,0 @@
using System;
using System.Reflection;
using AquaMai.Config.Interfaces;
using Mono.Cecil;
namespace AquaMai.Config.HeadlessLoader;
public class HeadlessConfigInterface
{
private readonly Assembly loadedConfigAssembly;
public IReflectionProvider ReflectionProvider { get; init; }
public IReflectionManager ReflectionManager { get; init; }
public string ApiVersion { get; init; }
public HeadlessConfigInterface(Assembly loadedConfigAssembly, AssemblyDefinition modsAssembly)
{
this.loadedConfigAssembly = loadedConfigAssembly;
ReflectionProvider = Activator.CreateInstance(
loadedConfigAssembly.GetType("AquaMai.Config.Reflection.MonoCecilReflectionProvider"), [modsAssembly]) as IReflectionProvider;
ReflectionManager = Activator.CreateInstance(
loadedConfigAssembly.GetType("AquaMai.Config.Reflection.ReflectionManager"), [ReflectionProvider]) as IReflectionManager;
ApiVersion = loadedConfigAssembly
.GetType("AquaMai.Config.ApiVersion")
.GetField("Version", BindingFlags.Public | BindingFlags.Static)
.GetRawConstantValue() as string;
}
public IConfigView CreateConfigView(string tomlString = null)
{
return Activator.CreateInstance(
loadedConfigAssembly.GetType("AquaMai.Config.ConfigView"),
tomlString == null ? [] : [tomlString]) as IConfigView;
}
public IConfig CreateConfig()
{
return Activator.CreateInstance(
loadedConfigAssembly.GetType("AquaMai.Config.Config"), [ReflectionManager]) as IConfig;
}
public IConfigParser GetConfigParser()
{
return loadedConfigAssembly
.GetType("AquaMai.Config.ConfigParser")
.GetField("Instance", BindingFlags.Public | BindingFlags.Static)
.GetValue(null) as IConfigParser;
}
public IConfigSerializer CreateConfigSerializer(IConfigSerializer.Options options)
{
return Activator.CreateInstance(
loadedConfigAssembly.GetType("AquaMai.Config.ConfigSerializer"), [options]) as IConfigSerializer;
}
public IConfigMigrationManager GetConfigMigrationManager()
{
return loadedConfigAssembly
.GetType("AquaMai.Config.Migration.ConfigMigrationManager")
.GetField("Instance", BindingFlags.Public | BindingFlags.Static)
.GetValue(null) as IConfigMigrationManager;
}
}

View File

@@ -1,62 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Mono.Cecil;
namespace AquaMai.Config.HeadlessLoader;
public class HeadlessConfigLoader
{
public static HeadlessConfigInterface LoadFromPacked(string fileName)
{
using var file = new FileStream(fileName, FileMode.Open);
return LoadFromPacked(file);
}
public static HeadlessConfigInterface LoadFromPacked(byte[] assemblyBinary)
=> LoadFromPacked(new MemoryStream(assemblyBinary));
public static HeadlessConfigInterface LoadFromPacked(Stream assemblyStream)
=> LoadFromPacked(AssemblyDefinition.ReadAssembly(assemblyStream));
public static HeadlessConfigInterface LoadFromPacked(AssemblyDefinition assembly)
{
return LoadFromUnpacked(
ResourceLoader.LoadEmbeddedAssemblies(assembly).Values);
}
public static HeadlessConfigInterface LoadFromUnpacked(IEnumerable<byte[]> assemblyBinariess) =>
LoadFromUnpacked(assemblyBinariess.Select(binary => new MemoryStream(binary)));
public static HeadlessConfigInterface LoadFromUnpacked(IEnumerable<Stream> assemblyStreams)
{
var resolver = new CustomAssemblyResolver();
var assemblies = assemblyStreams
.Select(
assemblyStream =>
AssemblyDefinition.ReadAssembly(
assemblyStream,
new ReaderParameters() {
AssemblyResolver = resolver
}))
.ToArray();
foreach (var assembly in assemblies)
{
resolver.RegisterAssembly(assembly);
}
var configAssembly = assemblies.First(assembly => assembly.Name.Name == "AquaMai.Config");
if (configAssembly == null)
{
throw new InvalidOperationException("AquaMai.Config assembly not found");
}
var loadedConfigAssembly = ConfigAssemblyLoader.LoadConfigAssembly(configAssembly);
var modsAssembly = assemblies.First(assembly => assembly.Name.Name == "AquaMai.Mods");
if (modsAssembly == null)
{
throw new InvalidOperationException("AquaMai.Mods assembly not found");
}
return new(loadedConfigAssembly, modsAssembly);
}
}

View File

@@ -1,4 +0,0 @@
namespace System.Runtime.CompilerServices
{
internal static class IsExternalInit {}
}

View File

@@ -1,42 +0,0 @@
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using Mono.Cecil;
namespace AquaMai.Config.HeadlessLoader;
public class ResourceLoader
{
private const string DLL_SUFFIX = ".dll";
private const string COMPRESSED_SUFFIX = ".compressed";
private const string DLL_COMPRESSED_SUFFIX = $"{DLL_SUFFIX}{COMPRESSED_SUFFIX}";
public static Dictionary<string, Stream> LoadEmbeddedAssemblies(AssemblyDefinition assembly)
{
return assembly.MainModule.Resources
.Where(resource => resource.Name.ToLower().EndsWith(DLL_SUFFIX) || resource.Name.ToLower().EndsWith(DLL_COMPRESSED_SUFFIX))
.Select(LoadResource)
.Where(data => data.Name != null)
.ToDictionary(data => data.Name, data => data.Stream);
}
public static (string Name, Stream Stream) LoadResource(Resource resource)
{
if (resource is EmbeddedResource embeddedResource)
{
if (resource.Name.ToLower().EndsWith(COMPRESSED_SUFFIX))
{
var decompressedStream = new MemoryStream();
using (var deflateStream = new DeflateStream(embeddedResource.GetResourceStream(), CompressionMode.Decompress))
{
deflateStream.CopyTo(decompressedStream);
}
decompressedStream.Position = 0;
return (resource.Name.Substring(0, resource.Name.Length - COMPRESSED_SUFFIX.Length), decompressedStream);
}
return (resource.Name, embeddedResource.GetResourceStream());
}
return (null, null);
}
}

View File

@@ -1,40 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Release</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{DF1536F9-3B06-4463-B654-4CC3E708B610}</ProjectGuid>
<OutputType>Library</OutputType>
<RootNamespace>AquaMai.Config.Interfaces</RootNamespace>
<AssemblyName>AquaMai.Config.Interfaces</AssemblyName>
<TargetFramework>net472</TargetFramework>
<FileAlignment>512</FileAlignment>
<Deterministic>true</Deterministic>
<LangVersion>12</LangVersion>
<NoWarn>414</NoWarn>
<AssemblySearchPaths>$(ProjectDir)../Libs/;$(AssemblySearchPaths)</AssemblySearchPaths>
<OutputPath>$(ProjectDir)../Output/</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<EnableDefaultEmbeddedResourceItems>false</EnableDefaultEmbeddedResourceItems>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugSymbols>false</DebugSymbols>
<DebugType>None</DebugType>
<Optimize>true</Optimize>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DefineConstants>DEBUG</DefineConstants>
</PropertyGroup>
<ItemGroup>
<Reference Include="mscorlib" />
<Reference Include="System" />
</ItemGroup>
</Project>

View File

@@ -1,28 +0,0 @@
using System;
namespace AquaMai.Config.Interfaces;
public interface IConfig
{
public interface IEntryState
{
public bool IsDefault { get; }
public object DefaultValue { get; }
public object Value { get; set; }
}
public interface ISectionState
{
public bool IsDefault { get; set; }
public bool DefaultEnabled { get; }
public bool Enabled { get; set; }
}
public IReflectionManager ReflectionManager { get; }
public ISectionState GetSectionState(IReflectionManager.ISection section);
public ISectionState GetSectionState(Type type);
public void SetSectionEnabled(IReflectionManager.ISection section, bool enabled);
public IEntryState GetEntryState(IReflectionManager.IEntry entry);
public void SetEntryValue(IReflectionManager.IEntry entry, object value);
}

View File

@@ -1,8 +0,0 @@
namespace AquaMai.Config.Interfaces;
public interface IConfigComment
{
string CommentEn { get; init; }
string CommentZh { get; init; }
public string GetLocalized(string lang);
}

View File

@@ -1,7 +0,0 @@
namespace AquaMai.Config.Interfaces;
public interface IConfigEntryAttribute
{
IConfigComment Comment { get; }
bool HideWhenDefault { get; }
}

View File

@@ -1,8 +0,0 @@
namespace AquaMai.Config.Interfaces;
public interface IConfigMigrationManager
{
public IConfigView Migrate(IConfigView config);
public string GetVersion(IConfigView config);
public string LatestVersion { get; }
}

View File

@@ -1,7 +0,0 @@
namespace AquaMai.Config.Interfaces;
public interface IConfigParser
{
public void Parse(IConfig config, string tomlString);
public void Parse(IConfig config, IConfigView configView);
}

View File

@@ -1,9 +0,0 @@
namespace AquaMai.Config.Interfaces;
public interface IConfigSectionAttribute
{
IConfigComment Comment { get; }
bool ExampleHidden { get; }
bool DefaultOn { get; }
bool AlwaysEnabled { get; }
}

View File

@@ -1,13 +0,0 @@
namespace AquaMai.Config.Interfaces;
public interface IConfigSerializer
{
public record Options
{
public string Lang { get; init; }
public bool IncludeBanner { get; init; }
public bool OverrideLocaleValue { get; init; }
}
public string Serialize(IConfig config);
}

View File

@@ -1,11 +0,0 @@
namespace AquaMai.Config.Interfaces;
public interface IConfigView
{
public void SetValue(string path, object value);
public T GetValueOrDefault<T>(string path, T defaultValue = default);
public bool TryGetValue<T>(string path, out T resultValue);
public bool Remove(string path);
public string ToToml();
public IConfigView Clone();
}

View File

@@ -1,43 +0,0 @@
using System.Collections.Generic;
using System;
namespace AquaMai.Config.Interfaces;
public interface IReflectionManager
{
public interface IEntry
{
public string Path { get; }
public string Name { get; }
public IReflectionField Field { get; }
public IConfigEntryAttribute Attribute { get; init; }
}
public interface ISection
{
public string Path { get; }
public IReflectionType Type { get; }
public List<IEntry> Entries { get; }
public IConfigSectionAttribute Attribute { get; init; }
}
public IEnumerable<ISection> Sections { get; }
public IEnumerable<IEntry> Entries { get; }
public bool ContainsSection(string path);
public bool TryGetSection(string path, out ISection section);
public bool TryGetSection(Type type, out ISection section);
public ISection GetSection(string path);
public ISection GetSection(Type type);
public bool ContainsEntry(string path);
public bool TryGetEntry(string path, out IEntry entry);
public IEntry GetEntry(string path);
}

View File

@@ -1,30 +0,0 @@
using System;
using System.Collections.Generic;
using System.Reflection;
namespace AquaMai.Config.Interfaces;
public interface IReflectionField
{
public string Name { get; }
public Type FieldType { get; }
public T GetCustomAttribute<T>() where T : Attribute;
public object GetValue(object objIsNull);
public void SetValue(object objIsNull, object value);
}
public interface IReflectionType
{
public string FullName { get; }
public string Namespace { get; }
public T GetCustomAttribute<T>() where T : Attribute;
public IReflectionField[] GetFields(BindingFlags bindingAttr);
}
public interface IReflectionProvider
{
public IReflectionType[] GetTypes();
public Dictionary<string, object> GetEnum(string enumName);
}

View File

@@ -1,4 +0,0 @@
namespace System.Runtime.CompilerServices
{
internal static class IsExternalInit {}
}

View File

@@ -1,9 +0,0 @@
namespace AquaMai.Config;
public static class ApiVersion
{
// Using a raw string for API version instead of a constant for maximum compatibility.
// When breaking changes are made, increment the major version.
// When new APIs are added in a backwards-compatible but non-forward-compatible manner, increment the minor version.
public const string Version = "1.0";
}

View File

@@ -1,60 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Release</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{DF1536F9-3B06-4463-B654-4CC3E708B610}</ProjectGuid>
<OutputType>Library</OutputType>
<RootNamespace>AquaMai.Config</RootNamespace>
<AssemblyName>AquaMai.Config</AssemblyName>
<TargetFramework>net472</TargetFramework>
<FileAlignment>512</FileAlignment>
<Deterministic>true</Deterministic>
<LangVersion>12</LangVersion>
<NoWarn>414</NoWarn>
<AssemblySearchPaths>$(ProjectDir)../Libs/;$(AssemblySearchPaths)</AssemblySearchPaths>
<OutputPath>$(ProjectDir)../Output/</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<EnableDefaultEmbeddedResourceItems>false</EnableDefaultEmbeddedResourceItems>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugSymbols>false</DebugSymbols>
<DebugType>None</DebugType>
<Optimize>true</Optimize>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DefineConstants>DEBUG</DefineConstants>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../AquaMai.Config.Interfaces/AquaMai.Config.Interfaces.csproj" />
</ItemGroup>
<ItemGroup>
<Reference Include="mscorlib" />
<Reference Include="Mono.Cecil" />
<Reference Include="System" />
</ItemGroup>
<ItemGroup>
<Content Include="FodyWeavers.xml" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Fody" Version="6.8.1">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="ILMerge.Fody" Version="1.24.0">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Samboy063.Tomlet" Version="5.4.0" />
</ItemGroup>
</Project>

View File

@@ -1,9 +0,0 @@
using System;
namespace AquaMai.Config.Attributes;
// When The most inner namespace is the same name of the class, it should be collapsed.
// The class must be the only class in the namespace with a [ConfigSection] attribute.
[AttributeUsage(AttributeTargets.Class)]
public class ConfigCollapseNamespaceAttribute : Attribute
{}

View File

@@ -1,14 +0,0 @@
using System;
using AquaMai.Config.Interfaces;
namespace AquaMai.Config.Attributes;
public record ConfigComment(string CommentEn, string CommentZh) : IConfigComment
{
public string GetLocalized(string lang) => lang switch
{
"en" => CommentEn ?? "",
"zh" => CommentZh ?? "",
_ => throw new ArgumentException($"Unsupported language: {lang}")
};
}

View File

@@ -1,25 +0,0 @@
using System;
using AquaMai.Config.Interfaces;
namespace AquaMai.Config.Attributes;
public enum SpecialConfigEntry
{
None,
Locale
}
[AttributeUsage(AttributeTargets.Field)]
public class ConfigEntryAttribute(
string en = null,
string zh = null,
// NOTE: Don't use this argument to hide any useful options.
// Only use it to hide options that really won't be used.
bool hideWhenDefault = false,
// NOTE: Use this argument to mark special config entries that need special handling.
SpecialConfigEntry specialConfigEntry = SpecialConfigEntry.None) : Attribute, IConfigEntryAttribute
{
public IConfigComment Comment { get; } = new ConfigComment(en, zh);
public bool HideWhenDefault { get; } = hideWhenDefault;
public SpecialConfigEntry SpecialConfigEntry { get; } = specialConfigEntry;
}

View File

@@ -1,22 +0,0 @@
using System;
using AquaMai.Config.Interfaces;
namespace AquaMai.Config.Attributes;
[AttributeUsage(AttributeTargets.Class)]
public class ConfigSectionAttribute(
string en = null,
string zh = null,
// It will be hidden if the default value is preserved.
bool exampleHidden = false,
// A "Disabled = true" entry is required to disable the section.
bool defaultOn = false,
// NOTE: You probably shouldn't use this. Only the "General" section is using this.
// Implies defaultOn = true.
bool alwaysEnabled = false) : Attribute, IConfigSectionAttribute
{
public IConfigComment Comment { get; } = new ConfigComment(en, zh);
public bool ExampleHidden { get; } = exampleHidden;
public bool DefaultOn { get; } = defaultOn || alwaysEnabled;
public bool AlwaysEnabled { get; } = alwaysEnabled;
}

View File

@@ -1,78 +0,0 @@
using System;
namespace AquaMai.Config.Attributes;
public enum EnableConditionOperator
{
Equal,
NotEqual,
GreaterThan,
LessThan,
GreaterThanOrEqual,
LessThanOrEqual
}
public class EnableCondition(
Type referenceType,
string referenceMember,
EnableConditionOperator @operator,
object rightSideValue) : Attribute
{
public Type ReferenceType { get; } = referenceType;
public string ReferenceMember { get; } = referenceMember;
public EnableConditionOperator Operator { get; } = @operator;
public object RightSideValue { get; } = rightSideValue;
// Referencing a field in another class and checking if it's true.
public EnableCondition(Type referenceType, string referenceMember)
: this(referenceType, referenceMember, EnableConditionOperator.Equal, true)
{ }
// Referencing a field in the same class and comparing it with a value.
public EnableCondition(string referenceMember, EnableConditionOperator condition, object value)
: this(null, referenceMember, condition, value)
{ }
// Referencing a field in the same class and checking if it's true.
public EnableCondition(string referenceMember)
: this(referenceMember, EnableConditionOperator.Equal, true)
{ }
public bool Evaluate(Type selfType)
{
var referenceType = ReferenceType ?? selfType;
var referenceField = referenceType.GetField(
ReferenceMember,
System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic);
var referenceProperty = referenceType.GetProperty(
ReferenceMember,
System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic);
if (referenceField == null && referenceProperty == null)
{
throw new ArgumentException($"Field or property {ReferenceMember} not found in {referenceType.FullName}");
}
var referenceMemberValue = referenceField != null ? referenceField.GetValue(null) : referenceProperty.GetValue(null);
switch (Operator)
{
case EnableConditionOperator.Equal:
return referenceMemberValue.Equals(RightSideValue);
case EnableConditionOperator.NotEqual:
return !referenceMemberValue.Equals(RightSideValue);
case EnableConditionOperator.GreaterThan:
case EnableConditionOperator.LessThan:
case EnableConditionOperator.GreaterThanOrEqual:
case EnableConditionOperator.LessThanOrEqual:
var comparison = (IComparable)referenceMemberValue;
return Operator switch
{
EnableConditionOperator.GreaterThan => comparison.CompareTo(RightSideValue) > 0,
EnableConditionOperator.LessThan => comparison.CompareTo(RightSideValue) < 0,
EnableConditionOperator.GreaterThanOrEqual => comparison.CompareTo(RightSideValue) >= 0,
EnableConditionOperator.LessThanOrEqual => comparison.CompareTo(RightSideValue) <= 0,
_ => throw new NotImplementedException(),
};
default:
throw new NotImplementedException();
}
}
}

View File

@@ -1,105 +0,0 @@
using System;
using System.Collections.Generic;
using AquaMai.Config.Interfaces;
using AquaMai.Config.Reflection;
namespace AquaMai.Config;
public class Config : IConfig
{
// NOTE: If a section's state is default, all underlying entries' states are default as well.
public record SectionState : IConfig.ISectionState
{
public bool IsDefault { get; set; }
public bool DefaultEnabled { get; init; }
public bool Enabled { get; set; }
}
public record EntryState : IConfig.IEntryState
{
public bool IsDefault { get; set; }
public object DefaultValue { get; init; }
public object Value { get; set; }
}
private readonly Dictionary<string, SectionState> sections = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, EntryState> entries = new(StringComparer.OrdinalIgnoreCase);
public readonly ReflectionManager reflectionManager;
public IReflectionManager ReflectionManager => reflectionManager;
public Config(ReflectionManager reflectionManager)
{
this.reflectionManager = reflectionManager;
foreach (var section in reflectionManager.SectionValues)
{
InitializeSection(section);
}
}
private void InitializeSection(ReflectionManager.Section section)
{
sections.Add(section.Path, new SectionState()
{
IsDefault = true,
DefaultEnabled = section.Attribute.DefaultOn,
Enabled = section.Attribute.DefaultOn
});
foreach (var entry in section.Entries)
{
var defaultValue = entry.Field.GetValue(null);
if (defaultValue == null)
{
throw new InvalidOperationException($"Null default value for entry {entry.Path} is not allowed.");
}
entries.Add(entry.Path, new EntryState()
{
IsDefault = true,
DefaultValue = defaultValue,
Value = defaultValue
});
}
}
public IConfig.ISectionState GetSectionState(IReflectionManager.ISection section)
{
return sections[section.Path];
}
public IConfig.ISectionState GetSectionState(Type type)
{
if (!ReflectionManager.TryGetSection(type, out var section))
{
throw new ArgumentException($"Type {type.FullName} is not a config section.");
}
return sections[section.Path];
}
public void SetSectionEnabled(IReflectionManager.ISection section, bool enabled)
{
sections[section.Path].IsDefault = false;
sections[section.Path].Enabled = enabled;
}
public IConfig.IEntryState GetEntryState(IReflectionManager.IEntry entry)
{
return entries[entry.Path];
}
public void SetEntryValue(IReflectionManager.IEntry entry, object value)
{
if (value.GetType() != entry.Field.FieldType)
{
throw new ArgumentException($"Value type {value.GetType().FullName} does not match entry type {entry.Field.FieldType.FullName}.");
}
entry.Field.SetValue(null, value);
entries[entry.Path].IsDefault = false;
entries[entry.Path].Value = value;
}
}

View File

@@ -1,125 +0,0 @@
using System;
using Tomlet.Models;
using AquaMai.Config.Interfaces;
using AquaMai.Config.Reflection;
using AquaMai.Config.Migration;
using System.Linq;
namespace AquaMai.Config;
public class ConfigParser : IConfigParser
{
public readonly static ConfigParser Instance = new();
private readonly static string[] supressUnrecognizedConfigPaths = ["Version"];
private readonly static string[] supressUnrecognizedConfigPathSuffixes = [
".Disabled", // For section enable state.
".Disable", // For section enable state, but the wrong key, warn later.
".Enabled", // For section enable state, but the wrong key, warn later.
".Enable", // For section enable state, but the wrong key, warn later.
];
private ConfigParser()
{}
public void Parse(IConfig config, string tomlString)
{
var configView = new ConfigView(tomlString);
Parse(config, configView);
}
public void Parse(IConfig config, IConfigView configView)
{
var configVersion = ConfigMigrationManager.Instance.GetVersion(configView);
if (configVersion != ConfigMigrationManager.Instance.LatestVersion)
{
throw new InvalidOperationException($"Config version mismatch: expected {ConfigMigrationManager.Instance.LatestVersion}, got {configVersion}");
}
Hydrate((Config)config, ((ConfigView)configView).root, "");
}
private static void Hydrate(Config config, TomlValue value, string path)
{
if (config.ReflectionManager.TryGetSection(path, out var section))
{
ParseSectionEnableState(config, (ReflectionManager.Section)section, value, path);
}
if (value is TomlTable table)
{
bool isLeaf = true;
foreach (var subKey in table.Keys)
{
var subValue = table.GetValue(subKey);
var subPath = path == "" ? subKey : $"{path}.{subKey}";
if (subValue is TomlTable)
{
isLeaf = false;
}
Hydrate(config, subValue, subPath);
}
// A leaf dictionary, which has no child dictionaries, must be a section.
if (isLeaf && section == null)
{
Utility.Log($"Unrecognized config section: {path}");
}
}
else
{
// It's an config entry value (or a primitive type for enabling a section).
if (!config.ReflectionManager.ContainsSection(path) &&
!config.ReflectionManager.ContainsEntry(path) &&
!supressUnrecognizedConfigPaths.Any(s => path.Equals(s, StringComparison.OrdinalIgnoreCase)) &&
!supressUnrecognizedConfigPathSuffixes.Any(suffix => path.EndsWith(suffix, StringComparison.OrdinalIgnoreCase)))
{
Utility.Log($"Unrecognized config entry: {path}");
return;
}
if (config.ReflectionManager.TryGetEntry(path, out var entry))
{
try
{
var parsedValue = Utility.ParseTomlValue(entry.Field.FieldType, value);
config.SetEntryValue(entry, parsedValue);
}
catch (Exception e)
{
Utility.Log($"Error hydrating config ({path} = {value.StringValue}): {e.Message}");
}
}
}
}
public static void ParseSectionEnableState(
Config config,
ReflectionManager.Section section,
TomlValue value,
string path)
{
if (value is TomlTable table)
{
foreach (var unexpectedKey in (string[]) ["Enable", "Enabled", "Disable"])
{
if (Utility.TomlContainsKeyCaseInsensitive(table, unexpectedKey))
{
Utility.Log($"Unexpected key \"{unexpectedKey}\" for enable status under \"{path}\". Only \"Disabled\" is parsed.");
}
}
if (Utility.TomlTryGetValueCaseInsensitive(table, "Disabled", out var disableValue) && !section.Attribute.AlwaysEnabled)
{
var disabled = Utility.IsTruty(disableValue, path + ".Disabled");
config.SetSectionEnabled(section, !disabled);
}
else
{
config.SetSectionEnabled(section, true);
}
}
else
{
config.SetSectionEnabled(section, Utility.IsTruty(value, path));
}
}
}

View File

@@ -1,186 +0,0 @@
using System;
using System.Reflection;
using System.Text;
using AquaMai.Config.Attributes;
using AquaMai.Config.Interfaces;
using AquaMai.Config.Migration;
using Tomlet.Models;
namespace AquaMai.Config;
public class ConfigSerializer(IConfigSerializer.Options Options) : IConfigSerializer
{
private const string BANNER_ZH =
"""
AquaMai TOML
- #
- 便使 VSCode 使 #使 ##
- [OptionalCategory.Section]
-
- Disabled = true/
- =
-
-
-
-
使 MaiChartManager AquaMai
https://github.com/clansty/MaiChartManager
""";
private const string BANNER_EN =
"""
This is the TOML configuration file of AquaMai.
- Lines starting with a hash # are comments. Commented content will not take effect.
- For easily editing with editors (e.g. VSCode), commented configuration content uses a single hash #, while comment text uses two hashes ##.
- Lines with square brackets like [OptionalCategory.Section] are sections.
- Uncomment a section that is commented out by default (i.e. disabled by default) to enable it.
- To disable a section that is enabled by default, add a "Disable = true" entry under the section. Removing/commenting it will not work.
- Lines like "Key = Value" is a configuration entry.
- Configuration entries apply to the nearest section above them. Do not enable a configuration entry when its section is commented out (will be added to the previous section, which is invalid).
- Configuration entries take effect when the corresponding section is enabled, regardless of whether they are uncommented.
- Commented configuration entries retain their default values (shown in the comment), which may change with version updates.
- The format and text comments of this file are fixed. The configuration file will be rewritten at startup, and unrecognizable content will be deleted.
""";
private readonly IConfigSerializer.Options Options = Options;
public string Serialize(IConfig config)
{
StringBuilder sb = new();
if (Options.IncludeBanner)
{
var banner = Options.Lang == "zh" ? BANNER_ZH : BANNER_EN;
if (banner != null)
{
AppendComment(sb, banner.TrimEnd());
sb.AppendLine();
}
}
// Version
AppendEntry(sb, null, "Version", ConfigMigrationManager.Instance.LatestVersion);
foreach (var section in ((Config)config).reflectionManager.SectionValues)
{
var sectionState = config.GetSectionState(section);
// If the state is default, print the example only. If the example is hidden, skip it.
if (sectionState.IsDefault && section.Attribute.ExampleHidden)
{
continue;
}
sb.AppendLine();
AppendComment(sb, section.Attribute.Comment);
if (// If the section is hidden and hidden by default, print it as commented.
sectionState.IsDefault && !sectionState.Enabled &&
// If the section is marked as always enabled, print it normally.
!section.Attribute.AlwaysEnabled)
{
sb.AppendLine($"#[{section.Path}]");
}
else // If the section is overridden, or is enabled by any means, print it normally.
{
sb.AppendLine($"[{section.Path}]");
}
if (!section.Attribute.AlwaysEnabled)
{
// If the section is disabled explicitly, print the "Disabled" meta entry.
if (!sectionState.IsDefault && !sectionState.Enabled)
{
AppendEntry(sb, null, "Disabled", value: true);
}
// If the section is enabled by default, print the "Disabled" meta entry as commented.
else if (sectionState.IsDefault && section.Attribute.DefaultOn)
{
AppendEntry(sb, null, "Disabled", value: false, commented: true);
}
// Otherwise, don't print the "Disabled" meta entry.
}
// Print entries.
foreach (var entry in section.entries)
{
var entryState = config.GetEntryState(entry);
AppendComment(sb, entry.Attribute.Comment);
if (((ConfigEntryAttribute)entry.Attribute).SpecialConfigEntry == SpecialConfigEntry.Locale && Options.OverrideLocaleValue)
{
AppendEntry(sb, section.Path, entry.Name, Options.Lang);
}
else
{
AppendEntry(sb, section.Path, entry.Name, entryState.Value, entryState.IsDefault);
}
}
}
return sb.ToString();
}
private static string SerializeTomlValue(string diagnosticPath, object value)
{
var type = value.GetType();
if (value is bool b)
{
return b ? "true" : "false";
}
else if (value is string str)
{
return new TomlString(str).SerializedValue;
}
else if (type.IsEnum)
{
return new TomlString(value.ToString()).SerializedValue;
}
else if (Utility.IsIntegerType(type))
{
return value.ToString();
}
else if (Utility.IsFloatType(type))
{
return new TomlDouble(Convert.ToDouble(value)).SerializedValue;
}
else
{
var currentMethod = MethodBase.GetCurrentMethod();
throw new NotImplementedException($"Unsupported config entry type: {type.FullName} ({diagnosticPath}). Please implement in {currentMethod.DeclaringType.FullName}.{currentMethod.Name}");
}
}
private void AppendComment(StringBuilder sb, IConfigComment comment)
{
if (comment != null)
{
AppendComment(sb, comment.GetLocalized(Options.Lang));
}
}
private static void AppendComment(StringBuilder sb, string comment)
{
comment = comment.Trim();
if (!string.IsNullOrEmpty(comment))
{
foreach (var line in comment.Split('\n'))
{
sb.AppendLine($"## {line}");
}
}
}
private static void AppendEntry(StringBuilder sb, string diagnosticsSection, string key, object value, bool commented = false)
{
if (commented)
{
sb.Append('#');
}
var diagnosticsPath = string.IsNullOrEmpty(diagnosticsSection)
? key
: $"{diagnosticsSection}.{key}";
sb.AppendLine($"{key} = {SerializeTomlValue(diagnosticsPath, value)}");
}
}

View File

@@ -1,142 +0,0 @@
using System;
using System.Linq;
using AquaMai.Config.Interfaces;
using Tomlet;
using Tomlet.Models;
namespace AquaMai.Config;
public class ConfigView : IConfigView
{
public readonly TomlTable root;
public ConfigView()
{
root = new TomlTable();
}
public ConfigView(TomlTable root)
{
this.root = root;
}
public ConfigView(string tomlString)
{
var tomlValue = new TomlParser().Parse(tomlString);
if (tomlValue is not TomlTable tomlTable)
{
throw new ArgumentException($"Invalid TOML, expected a table, got: {tomlValue.GetType()}");
}
root = tomlTable;
}
public TomlTable EnsureDictionary(string path)
{
var pathComponents = path.Split('.');
var current = root;
foreach (var component in pathComponents)
{
if (!current.TryGetValue(component, out var next))
{
next = new TomlTable();
current.Put(component, next);
}
current = (TomlTable)next;
}
return current;
}
public void SetValue(string path, object value)
{
var pathComponents = path.Split('.');
var current = root;
foreach (var component in pathComponents.Take(pathComponents.Length - 1))
{
if (!current.TryGetValue(component, out var next))
{
next = new TomlTable();
current.Put(component, next);
}
current = (TomlTable)next;
}
if (value == null)
{
current.Keys.Remove(pathComponents.Last());
return;
}
current.Put(pathComponents.Last(), value);
}
public T GetValueOrDefault<T>(string path, T defaultValue = default)
{
return TryGetValue(path, out T resultValue) ? resultValue : defaultValue;
}
public bool TryGetValue<T>(string path, out T resultValue)
{
var pathComponents = path.Split('.');
var current = root;
foreach (var component in pathComponents.Take(pathComponents.Length - 1))
{
if (!Utility.TomlTryGetValueCaseInsensitive(current, component, out var next) || next is not TomlTable nextTable)
{
resultValue = default;
return false;
}
current = nextTable;
}
if (!Utility.TomlTryGetValueCaseInsensitive(current, pathComponents.Last(), out var value))
{
resultValue = default;
return false;
}
if (typeof(T) == typeof(object))
{
resultValue = (T)(object)value;
return true;
}
try
{
resultValue = Utility.ParseTomlValue<T>(value);
return true;
}
catch (Exception e)
{
Utility.Log($"Failed to parse value at {path}: {e.Message}");
resultValue = default;
return false;
}
}
public bool Remove(string path)
{
var pathComponents = path.Split('.');
var current = root;
foreach (var component in pathComponents.Take(pathComponents.Length - 1))
{
if (!Utility.TomlTryGetValueCaseInsensitive(current, component, out var next) || next is not TomlTable nextTable)
{
return false;
}
current = (TomlTable)next;
}
var keyToRemove = pathComponents.Last();
var keysCaseSensitive = current.Keys.Where(k => string.Equals(k, keyToRemove, StringComparison.OrdinalIgnoreCase));
foreach (var key in keysCaseSensitive)
{
current.Entries.Remove(key);
}
return keysCaseSensitive.Any();
}
public string ToToml()
{
return root.SerializedValue;
}
public IConfigView Clone()
{
return new ConfigView(ToToml());
}
}

View File

@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<ILMerge>
<IncludeAssemblies>tomlet</IncludeAssemblies>
<NamespacePrefix>$AquaMai.Config$_</NamespacePrefix>
</ILMerge>
</Weavers>

View File

@@ -1,111 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. -->
<xs:element name="Weavers">
<xs:complexType>
<xs:all>
<xs:element name="ILMerge" minOccurs="0" maxOccurs="1">
<xs:complexType>
<xs:all>
<xs:element name="IncludeAssemblies" type="xs:string" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>A regular expression matching the assembly names to include in merging.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="ExcludeAssemblies" type="xs:string" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>A regular expression matching the assembly names to exclude from merging.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="IncludeResources" type="xs:string" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>A regular expression matching the resource names to include in merging.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="ExcludeResources" type="xs:string" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>A regular expression matching the resource names to exclude from merging.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="HideImportedTypes" type="xs:boolean" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>A switch to control whether the imported types are hidden (made private) or keep their visibility unchanged. Default is 'true'</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="NamespacePrefix" type="xs:string" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>A string that is used as prefix for the namespace of the imported types.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="FullImport" type="xs:boolean" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>A switch to control whether to import the full assemblies or only the referenced types. Default is 'false'</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="CompactMode" type="xs:boolean" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>A switch to enable compacting of the target assembly by skipping properties, events and unused methods. Default is 'false'</xs:documentation>
</xs:annotation>
</xs:element>
</xs:all>
<xs:attribute name="IncludeAssemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A regular expression matching the assembly names to include in merging.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="ExcludeAssemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A regular expression matching the assembly names to exclude from merging.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="IncludeResources" type="xs:string">
<xs:annotation>
<xs:documentation>A regular expression matching the resource names to include in merging.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="ExcludeResources" type="xs:string">
<xs:annotation>
<xs:documentation>A regular expression matching the resource names to exclude from merging.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="HideImportedTypes" type="xs:boolean">
<xs:annotation>
<xs:documentation>A switch to control whether the imported types are hidden (made private) or keep their visibility unchanged. Default is 'true'</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="NamespacePrefix" type="xs:string">
<xs:annotation>
<xs:documentation>A string that is used as prefix for the namespace of the imported types.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="FullImport" type="xs:boolean">
<xs:annotation>
<xs:documentation>A switch to control whether to import the full assemblies or only the referenced types. Default is 'false'</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="CompactMode" type="xs:boolean">
<xs:annotation>
<xs:documentation>A switch to enable compacting of the target assembly by skipping properties, events and unused methods. Default is 'false'</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
</xs:element>
</xs:all>
<xs:attribute name="VerifyAssembly" type="xs:boolean">
<xs:annotation>
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="VerifyIgnoreCodes" type="xs:string">
<xs:annotation>
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="GenerateXsd" type="xs:boolean">
<xs:annotation>
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
</xs:element>
</xs:schema>

View File

@@ -1,62 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AquaMai.Config.Interfaces;
namespace AquaMai.Config.Migration;
public class ConfigMigrationManager : IConfigMigrationManager
{
public static readonly ConfigMigrationManager Instance = new();
private readonly Dictionary<string, IConfigMigration> migrationMap =
new List<IConfigMigration>
{
new ConfigMigration_V1_0_V2_0(),
new ConfigMigration_V2_0_V2_1()
}.ToDictionary(m => m.FromVersion);
public string LatestVersion { get; }
private ConfigMigrationManager()
{
LatestVersion = migrationMap.Values
.Select(m => m.ToVersion)
.OrderByDescending(version =>
{
var versionParts = version.Split('.').Select(int.Parse).ToArray();
return versionParts[0] * 100000 + versionParts[1];
})
.First();
}
public IConfigView Migrate(IConfigView config)
{
var currentVersion = GetVersion(config);
while (migrationMap.ContainsKey(currentVersion))
{
var migration = migrationMap[currentVersion];
Utility.Log($"Migrating config from v{migration.FromVersion} to v{migration.ToVersion}");
config = migration.Migrate(config);
currentVersion = migration.ToVersion;
}
if (currentVersion != LatestVersion)
{
throw new ArgumentException($"Could not migrate the config from v{currentVersion} to v{LatestVersion}");
}
return config;
}
public string GetVersion(IConfigView config)
{
if (config.TryGetValue<string>("Version", out var version))
{
return version;
}
// Assume v1.0 if not found
return "1.0";
}
}

View File

@@ -1,350 +0,0 @@
using System;
using System.Collections.Generic;
using AquaMai.Config.Interfaces;
using AquaMai.Config.Types;
namespace AquaMai.Config.Migration;
public class ConfigMigration_V1_0_V2_0 : IConfigMigration
{
public string FromVersion => "1.0";
public string ToVersion => "2.0";
public IConfigView Migrate(IConfigView src)
{
var dst = new ConfigView();
dst.SetValue("Version", ToVersion);
// UX (legacy)
MapBooleanTrueToSectionEnable(src, dst, "UX.TestProof", "GameSystem.TestProof");
if (src.GetValueOrDefault<bool>("UX.QuickSkip"))
{
// NOTE: UX.QuickSkip was a 4-in-1 large patch in earlier V1, then split since ModKeyMap was introduced.
dst.SetValue("UX.OneKeyEntryEnd.Key", "Service");
dst.SetValue("UX.OneKeyEntryEnd.LongPress", true);
dst.SetValue("UX.OneKeyRetrySkip.RetryKey", "Service");
dst.SetValue("UX.OneKeyRetrySkip.RetryLongPress", false);
dst.SetValue("UX.OneKeyRetrySkip.SkipKey", "Select1P");
dst.SetValue("UX.OneKeyRetrySkip.SkipLongPress", false);
dst.EnsureDictionary("GameSystem.QuickRetry");
}
if (src.GetValueOrDefault<bool>("UX.HideSelfMadeCharts"))
{
dst.SetValue("UX.HideSelfMadeCharts.Key", "Service");
dst.SetValue("UX.HideSelfMadeCharts.LongPress", false);
}
MapBooleanTrueToSectionEnable(src, dst, "UX.LoadJacketPng", "GameSystem.Assets.LoadLocalImages");
MapBooleanTrueToSectionEnable(src, dst, "UX.SkipWarningScreen", "Tweaks.TimeSaving.SkipStartupWarning");
MapBooleanTrueToSectionEnable(src, dst, "UX.SkipToMusicSelection", "Tweaks.TimeSaving.EntryToMusicSelection");
MapBooleanTrueToSectionEnable(src, dst, "UX.SkipEventInfo", "Tweaks.TimeSaving.SkipEventInfo");
MapBooleanTrueToSectionEnable(src, dst, "UX.SelectionDetail", "UX.SelectionDetail");
if (src.GetValueOrDefault<bool>("UX.CustomNoteSkin") ||
src.GetValueOrDefault<bool>("UX.CustomSkins"))
{
dst.SetValue("Fancy.CustomSkins.SkinsDir", "LocalAssets/Skins");
}
MapBooleanTrueToSectionEnable(src, dst, "UX.JudgeDisplay4B", "Fancy.GamePlay.JudgeDisplay4B");
MapBooleanTrueToSectionEnable(src, dst, "UX.CustomTrackStartDiff", "Fancy.CustomTrackStartDiff");
MapBooleanTrueToSectionEnable(src, dst, "UX.TrackStartProcessTweak", "Fancy.GamePlay.TrackStartProcessTweak");
MapBooleanTrueToSectionEnable(src, dst, "UX.DisableTrackStartTabs", "Fancy.GamePlay.DisableTrackStartTabs");
MapBooleanTrueToSectionEnable(src, dst, "UX.RealisticRandomJudge", "Fancy.GamePlay.RealisticRandomJudge");
// Utils (legacy)
if (src.GetValueOrDefault<bool>("Utils.Windowed") ||
src.GetValueOrDefault<int>("Utils.Width") != 0 ||
src.GetValueOrDefault<int>("Utils.Height") != 0)
{
// NOTE: the default "false, 0, 0" was effective earlier in V1, but won't be migrated as enabled in V2.
MapValueOrDefaultToEntryValue(src, dst, "Utils.Windowed", "GameSystem.Window.Windowed", false);
MapValueOrDefaultToEntryValue(src, dst, "Utils.Width", "GameSystem.Window.Width", 0);
MapValueOrDefaultToEntryValue(src, dst, "Utils.Height", "GameSystem.Window.Height", 0);
}
if (src.GetValueOrDefault<bool>("Utils.PracticeMode") || src.GetValueOrDefault<bool>("Utils.PractiseMode")) // Typo of typo is the correct word
{
dst.SetValue("UX.PracticeMode.Key", "Test");
dst.SetValue("UX.PracticeMode.LongPress", false);
}
// Fix (legacy)
MapBooleanTrueToSectionEnable(src, dst, "Fix.SlideJudgeTweak", "Fancy.GamePlay.BreakSlideJudgeBlink");
MapBooleanTrueToSectionEnable(src, dst, "Fix.BreakSlideJudgeBlink", "Fancy.GamePlay.BreakSlideJudgeBlink");
MapBooleanTrueToSectionEnable(src, dst, "Fix.SlideJudgeTweak", "Fancy.GamePlay.FanJudgeFlip");
MapBooleanTrueToSectionEnable(src, dst, "Fix.FanJudgeFlip", "Fancy.GamePlay.FanJudgeFlip");
// NOTE: This (FixCircleSlideJudge) was enabled by default in V1, but non-default in V2 since it has visual changes
MapBooleanTrueToSectionEnable(src, dst, "Fix.SlideJudgeTweak", "Fancy.GamePlay.AlignCircleSlideJudgeDisplay");
MapBooleanTrueToSectionEnable(src, dst, "Fix.FixCircleSlideJudge", "Fancy.GamePlay.AlignCircleSlideJudgeDisplay");
// Performance (legacy)
MapBooleanTrueToSectionEnable(src, dst, "Performance.ImproveLoadSpeed", "Tweaks.TimeSaving.SkipStartupDelays");
// TimeSaving (legacy)
MapBooleanTrueToSectionEnable(src, dst, "TimeSaving.ShowNetErrorDetail", "Utils.ShowNetErrorDetail");
// UX
MapValueToEntryValueIfNonNullOrDefault(src, dst, "UX.Locale", "General.Locale", "");
MapBooleanTrueToSectionEnable(src, dst, "UX.SinglePlayer", "GameSystem.SinglePlayer");
MapBooleanTrueToSectionEnable(src, dst, "UX.HideMask", "Fancy.HideMask");
MapBooleanTrueToSectionEnable(src, dst, "UX.LoadAssetsPng", "GameSystem.Assets.LoadLocalImages");
MapBooleanTrueToSectionEnable(src, dst, "UX.LoadAssetBundleWithoutManifest", "GameSystem.Assets.LoadAssetBundleWithoutManifest");
MapBooleanTrueToSectionEnable(src, dst, "UX.RandomBgm", "Fancy.RandomBgm");
MapBooleanTrueToSectionEnable(src, dst, "UX.DemoMaster", "Fancy.DemoMaster");
MapBooleanTrueToSectionEnable(src, dst, "UX.ExtendTimer", "GameSystem.DisableTimeout");
MapBooleanTrueToSectionEnable(src, dst, "UX.ImmediateSave", "UX.ImmediateSave");
MapBooleanTrueToSectionEnable(src, dst, "UX.LoadLocalBga", "GameSystem.Assets.UseJacketAsDummyMovie");
if (src.GetValueOrDefault<bool>("UX.CustomFont"))
{
dst.SetValue("GameSystem.Assets.Fonts.Paths", "LocalAssets/font.ttf");
dst.SetValue("GameSystem.Assets.Fonts.AddAsFallback", false);
}
MapBooleanTrueToSectionEnable(src, dst, "UX.TouchToButtonInput", "GameSystem.TouchToButtonInput");
MapBooleanTrueToSectionEnable(src, dst, "UX.HideHanabi", "Fancy.GamePlay.HideHanabi");
MapBooleanTrueToSectionEnable(src, dst, "UX.SlideFadeInTweak", "Fancy.GamePlay.SlideFadeInTweak");
MapBooleanTrueToSectionEnable(src, dst, "UX.JudgeAccuracyInfo", "UX.JudgeAccuracyInfo");
MapValueToEntryValueIfNonNullOrDefault(src, dst, "UX.CustomVersionString", "Fancy.CustomVersionString.VersionString", "");
MapValueToEntryValueIfNonNullOrDefault(src, dst, "UX.CustomPlaceName", "Fancy.CustomPlaceName.PlaceName", "");
MapValueToEntryValueIfNonNullOrDefault(src, dst, "UX.ExecOnIdle", "Fancy.Triggers.ExecOnIdle", "");
MapValueToEntryValueIfNonNullOrDefault(src, dst, "UX.ExecOnEntry", "Fancy.Triggers.ExecOnEntry", "");
// Cheat
var unlockTickets = src.GetValueOrDefault<bool>("Cheat.TicketUnlock");
var unlockMaps = src.GetValueOrDefault<bool>("Cheat.MapUnlock");
var unlockUtage = src.GetValueOrDefault<bool>("Cheat.UnlockUtage");
if (unlockTickets ||
unlockMaps ||
unlockUtage)
{
dst.SetValue("GameSystem.Unlock.Tickets", unlockTickets);
dst.SetValue("GameSystem.Unlock.Maps", unlockMaps);
dst.SetValue("GameSystem.Unlock.Utage", unlockUtage);
}
// Fix
MapBooleanTrueToSectionEnable(src, dst, "Fix.SkipVersionCheck", "Tweaks.SkipUserVersionCheck");
if (!src.GetValueOrDefault<bool>("Fix.RemoveEncryption"))
{
dst.SetValue("GameSystem.RemoveEncryption.Disabled", true); // Enabled by default in V2
}
if (!src.GetValueOrDefault<bool>("Fix.ForceAsServer", true))
{
dst.SetValue("GameSettings.ForceAsServer.Disabled", true); // Enabled by default in V2
}
if (src.GetValueOrDefault<bool>("Fix.ForceFreePlay"))
{
dst.SetValue("GameSettings.CreditConfig.IsFreePlay", true);
}
if (src.GetValueOrDefault<bool>("Fix.ForcePaidPlay"))
{
dst.SetValue("GameSettings.CreditConfig.IsFreePlay", false);
dst.SetValue("GameSettings.CreditConfig.LockCredits", 24u);
}
MapValueToEntryValueIfNonNullOrDefault(src, dst, "Fix.ExtendNotesPool", "Fancy.GamePlay.ExtendNotesPool.Count", 0);
MapBooleanTrueToSectionEnable(src, dst, "Fix.FrameRateLock", "Tweaks.LockFrameRate");
if (src.GetValueOrDefault<bool>("Font.FontFix") &&
!src.GetValueOrDefault<bool>("UX.CustomFont"))
{
dst.SetValue("GameSystem.Assets.Fonts.Paths", "%SYSTEMROOT%/Fonts/msyhbd.ttc");
dst.SetValue("GameSystem.Assets.Fonts.AddAsFallback", true);
}
MapBooleanTrueToSectionEnable(src, dst, "Fix.RealisticRandomJudge", "Fancy.GamePlay.RealisticRandomJudge");
if (src.GetValueOrDefault<bool>("UX.SinglePlayer"))
{
if (src.TryGetValue("Fix.HanabiFix", out bool hanabiFix))
{
// If it's enabled or disabled explicitly, use the value, otherwise left empty use the default V2 value (enabled).
dst.SetValue("GameSystem.SinglePlayer.FixHanabi", hanabiFix);
}
}
MapBooleanTrueToSectionEnable(src, dst, "Fix.IgnoreAimeServerError", "Tweaks.IgnoreAimeServerError");
MapBooleanTrueToSectionEnable(src, dst, "Fix.TouchResetAfterTrack", "Tweaks.ResetTouchAfterTrack");
// Utils
MapBooleanTrueToSectionEnable(src, dst, "Utils.LogUserId", "Utils.LogUserId");
MapValueToEntryValueIfNonNullOrDefault<double>(src, dst, "Utils.JudgeAdjustA", "GameSettings.JudgeAdjust.A", 0);
MapValueToEntryValueIfNonNullOrDefault<double>(src, dst, "Utils.JudgeAdjustB", "GameSettings.JudgeAdjust.B", 0);
MapValueToEntryValueIfNonNullOrDefault(src, dst, "Utils.TouchDelay", "GameSettings.JudgeAdjust.TouchDelay", 0u);
MapBooleanTrueToSectionEnable(src, dst, "Utils.SelectionDetail", "UX.SelectionDetail");
MapBooleanTrueToSectionEnable(src, dst, "Utils.ShowNetErrorDetail", "Utils.ShowNetErrorDetail");
MapBooleanTrueToSectionEnable(src, dst, "Utils.ShowErrorLog", "Utils.ShowErrorLog");
MapBooleanTrueToSectionEnable(src, dst, "Utils.FrameRateDisplay", "Utils.DisplayFrameRate");
MapValueToEntryValueIfNonNullOrDefault(src, dst, "Utils.TouchPanelBaudRate", "GameSystem.TouchPanelBaudRate.BaudRate", 0);
// TimeSaving
MapBooleanTrueToSectionEnable(src, dst, "TimeSaving.SkipWarningScreen", "Tweaks.TimeSaving.SkipStartupWarning");
MapBooleanTrueToSectionEnable(src, dst, "TimeSaving.ImproveLoadSpeed", "Tweaks.TimeSaving.SkipStartupDelays");
MapBooleanTrueToSectionEnable(src, dst, "TimeSaving.SkipToMusicSelection", "Tweaks.TimeSaving.EntryToMusicSelection");
MapBooleanTrueToSectionEnable(src, dst, "TimeSaving.SkipEventInfo", "Tweaks.TimeSaving.SkipEventInfo");
MapBooleanTrueToSectionEnable(src, dst, "TimeSaving.IWontTapOrSlideVigorously", "Tweaks.TimeSaving.IWontTapOrSlideVigorously");
MapBooleanTrueToSectionEnable(src, dst, "TimeSaving.SkipGameOverScreen", "Tweaks.TimeSaving.SkipGoodbyeScreen");
MapBooleanTrueToSectionEnable(src, dst, "TimeSaving.SkipTrackStart", "Tweaks.TimeSaving.SkipTrackStart");
MapBooleanTrueToSectionEnable(src, dst, "TimeSaving.ShowQuickEndPlay", "UX.QuickEndPlay");
// Visual
if (src.GetValueOrDefault<bool>("Visual.CustomSkins"))
{
dst.SetValue("Fancy.CustomSkins.SkinsDir", "LocalAssets/Skins");
}
MapBooleanTrueToSectionEnable(src, dst, "Visual.JudgeDisplay4B", "Fancy.GamePlay.JudgeDisplay4B");
MapBooleanTrueToSectionEnable(src, dst, "Visual.CustomTrackStartDiff", "Fancy.CustomTrackStartDiff");
MapBooleanTrueToSectionEnable(src, dst, "Visual.TrackStartProcessTweak", "Fancy.GamePlay.TrackStartProcessTweak");
MapBooleanTrueToSectionEnable(src, dst, "Visual.DisableTrackStartTabs", "Fancy.GamePlay.DisableTrackStartTabs");
MapBooleanTrueToSectionEnable(src, dst, "Visual.FanJudgeFlip", "Fancy.GamePlay.FanJudgeFlip");
MapBooleanTrueToSectionEnable(src, dst, "Visual.BreakSlideJudgeBlink", "Fancy.GamePlay.BreakSlideJudgeBlink");
MapBooleanTrueToSectionEnable(src, dst, "Visual.SlideArrowAnimation", "Fancy.GamePlay.SlideArrowAnimation");
MapBooleanTrueToSectionEnable(src, dst, "Visual.SlideLayerReverse", "Fancy.GamePlay.SlideLayerReverse");
// ModKeyMap
var keyQuickSkip = src.GetValueOrDefault("ModKeyMap.QuickSkip", "None");
var keyInGameRetry = src.GetValueOrDefault("ModKeyMap.InGameRetry", "None");
var keyInGameSkip = src.GetValueOrDefault("ModKeyMap.InGameSkip", "None");
var keyPractiseMode = src.GetValueOrDefault("ModKeyMap.PractiseMode", "None");
var keyHideSelfMadeCharts = src.GetValueOrDefault("ModKeyMap.HideSelfMadeCharts", "None");
if (keyQuickSkip != "None")
{
dst.SetValue("UX.OneKeyEntryEnd.Key", keyQuickSkip);
MapValueToEntryValueIfNonNull<bool>(src, dst, "ModKeyMap.QuickSkipLongPress", "UX.OneKeyEntryEnd.LongPress");
}
if (keyInGameRetry != "None" || keyInGameSkip != "None")
{
dst.SetValue("UX.OneKeyRetrySkip.RetryKey", keyInGameRetry);
if (keyInGameRetry != "None")
{
MapValueToEntryValueIfNonNull<bool>(src, dst, "ModKeyMap.InGameRetryLongPress", "UX.OneKeyRetrySkip.RetryLongPress");
}
dst.SetValue("UX.OneKeyRetrySkip.SkipKey", keyInGameSkip);
if (keyInGameSkip != "None")
{
MapValueToEntryValueIfNonNull<bool>(src, dst, "ModKeyMap.InGameSkipLongPress", "UX.OneKeyRetrySkip.SkipLongPress");
}
}
if (keyPractiseMode != "None")
{
dst.SetValue("UX.PracticeMode.Key", keyPractiseMode);
MapValueToEntryValueIfNonNull<bool>(src, dst, "ModKeyMap.PractiseModeLongPress", "UX.PracticeMode.LongPress");
}
if (keyHideSelfMadeCharts != "None")
{
dst.SetValue("UX.HideSelfMadeCharts.Key", keyHideSelfMadeCharts);
MapValueToEntryValueIfNonNull<bool>(src, dst, "ModKeyMap.HideSelfMadeChartsLongPress", "UX.HideSelfMadeCharts.LongPress");
}
MapBooleanTrueToSectionEnable(src, dst, "ModKeyMap.EnableNativeQuickRetry", "GameSystem.QuickRetry");
if (src.TryGetValue<string>("ModKeyMap.TestMode", out var testMode) &&
testMode != "" &&
testMode != "Test")
{
dst.SetValue("DeprecationWarning.v1_0_ModKeyMap_TestMode", true);
}
MapBooleanTrueToSectionEnable(src, dst, "ModKeyMap.TestModeLongPress", "GameSystem.TestProof");
// WindowState
if (src.GetValueOrDefault<bool>("WindowState.Enable"))
{
MapValueOrDefaultToEntryValue(src, dst, "WindowState.Windowed", "GameSystem.Window.Windowed", false);
MapValueOrDefaultToEntryValue(src, dst, "WindowState.Width", "GameSystem.Window.Width", 0);
MapValueOrDefaultToEntryValue(src, dst, "WindowState.Height", "GameSystem.Window.Height", 0);
}
// CustomCameraId
if (src.GetValueOrDefault<bool>("CustomCameraId.Enable"))
{
dst.EnsureDictionary("GameSystem.CustomCameraId");
MapValueToEntryValueIfNonNullOrDefault(src, dst, "CustomCameraId.PrintCameraList", "GameSystem.CustomCameraId.PrintCameraList", false);
MapValueToEntryValueIfNonNullOrDefault(src, dst, "CustomCameraId.LeftQrCamera", "GameSystem.CustomCameraId.LeftQrCamera", 0);
MapValueToEntryValueIfNonNullOrDefault(src, dst, "CustomCameraId.RightQrCamera", "GameSystem.CustomCameraId.RightQrCamera", 0);
MapValueToEntryValueIfNonNullOrDefault(src, dst, "CustomCameraId.PhotoCamera", "GameSystem.CustomCameraId.PhotoCamera", 0);
MapValueToEntryValueIfNonNullOrDefault(src, dst, "CustomCameraId.ChimeCamera", "GameSystem.CustomCameraId.ChimeCamera", 0);
}
// TouchSensitivity
if (src.GetValueOrDefault<bool>("TouchSensitivity.Enable"))
{
dst.EnsureDictionary("GameSettings.TouchSensitivity");
var areas = new[]
{
"A1", "A2", "A3", "A4", "A5", "A6", "A7", "A8",
"B1", "B2", "B3", "B4", "B5", "B6", "B7", "B8",
"C1", "C2",
"D1", "D2", "D3", "D4", "D5", "D6", "D7", "D8",
"E1", "E2", "E3", "E4", "E5", "E6", "E7", "E8",
};
foreach (var area in areas)
{
MapValueToEntryValueIfNonNull<int>(src, dst, $"TouchSensitivity.{area}", $"GameSettings.TouchSensitivity.{area}");
}
}
// CustomKeyMap
if (src.GetValueOrDefault<bool>("CustomKeyMap.Enable"))
{
dst.EnsureDictionary("GameSystem.KeyMap");
var keys = new[]
{
"Test", "Service",
"Button1_1P", "Button3_1P", "Button4_1P", "Button2_1P", "Button5_1P", "Button6_1P", "Button7_1P", "Button8_1P",
"Select_1P",
"Button1_2P", "Button2_2P", "Button3_2P", "Button4_2P", "Button5_2P", "Button6_2P", "Button7_2P", "Button8_2P",
"Select_2P"
};
foreach (var key in keys)
{
if (src.TryGetValue<string>($"CustomKeyMap.{key}", out var value) &&
Enum.TryParse<KeyCodeID>(value, out var keyCode))
{
dst.SetValue($"GameSystem.KeyMap.{key}", keyCode.ToString());
}
}
}
// MaimaiDX2077 (WTF is the name?)
MapBooleanTrueToSectionEnable(src, dst, "MaimaiDX2077.CustomNoteTypePatch", "Fancy.GamePlay.CustomNoteTypes");
// Default enabled in V2
dst.EnsureDictionary("GameSystem.RemoveEncryption");
dst.EnsureDictionary("GameSettings.ForceAsServer");
return dst;
}
// An value in the old config maps to an entry value in the new config.
// Any existing value, including zero, is valid.
private void MapValueToEntryValueIfNonNull<T>(IConfigView src, ConfigView dst, string srcKey, string dstKey)
{
if (src.TryGetValue<T>(srcKey, out var value))
{
dst.SetValue(dstKey, value);
}
}
// An value in the old config maps to an entry value in the new config.
// Null or default value is ignored.
private void MapValueToEntryValueIfNonNullOrDefault<T>(IConfigView src, ConfigView dst, string srcKey, string dstKey, T defaultValue)
{
if (src.TryGetValue<T>(srcKey, out var value) && !EqualityComparer<T>.Default.Equals(value, defaultValue))
{
dst.SetValue(dstKey, value);
}
}
// An value in the old config maps to an entry value in the new config.
// Null value is replaced with a default value.
private void MapValueOrDefaultToEntryValue<T>(IConfigView src, ConfigView dst, string srcKey, string dstKey, T defaultValue)
{
if (src.TryGetValue<T>(srcKey, out var value))
{
dst.SetValue(dstKey, value);
}
else
{
dst.SetValue(dstKey, defaultValue);
}
}
// An boolean value in the old config maps to a default-off section's enable in the new config.
private void MapBooleanTrueToSectionEnable(IConfigView src, ConfigView dst, string srcKey, string dstKey)
{
if (src.GetValueOrDefault<bool>(srcKey))
{
dst.EnsureDictionary(dstKey);
}
}
}

View File

@@ -1,44 +0,0 @@
using AquaMai.Config.Interfaces;
using Tomlet.Models;
namespace AquaMai.Config.Migration;
public class ConfigMigration_V2_0_V2_1 : IConfigMigration
{
public string FromVersion => "2.0";
public string ToVersion => "2.1";
public IConfigView Migrate(IConfigView src)
{
var dst = src.Clone();
dst.SetValue("Version", ToVersion);
if (IsSectionEnabled(src, "Tweaks.ResetTouchAfterTrack"))
{
dst.Remove("Tweaks.ResetTouchAfterTrack");
dst.SetValue("Tweaks.ResetTouch.AfterTrack", true);
}
return dst;
}
public bool IsSectionEnabled(IConfigView src, string path)
{
if (src.TryGetValue(path, out object section))
{
if (section is bool enabled)
{
return enabled;
}
else if (section is TomlTable table)
{
if (Utility.TomlTryGetValueCaseInsensitive(table, "Disabled", out var disabled))
{
return !Utility.IsTrutyOrDefault(disabled);
}
return true;
}
}
return false;
}
}

View File

@@ -1,10 +0,0 @@
using AquaMai.Config.Interfaces;
namespace AquaMai.Config.Migration;
public interface IConfigMigration
{
public string FromVersion { get; }
public string ToVersion { get; }
public IConfigView Migrate(IConfigView config);
}

View File

@@ -1,4 +0,0 @@
namespace System.Runtime.CompilerServices
{
internal static class IsExternalInit {}
}

View File

@@ -1,254 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using AquaMai.Config.Attributes;
using AquaMai.Config.Interfaces;
using Mono.Cecil;
using Mono.Cecil.Cil;
namespace AquaMai.Config.Reflection;
public class MonoCecilReflectionProvider : IReflectionProvider
{
public record ReflectionField(
string Name,
Type FieldType,
object Value,
IDictionary<Type, object> Attributes) : IReflectionField
{
public object Value { get; set; } = Value;
public T GetCustomAttribute<T>() where T : Attribute => Attributes.TryGetValue(typeof(T), out var value) ? (T)value : null;
public object GetValue(object obj) => Value;
public void SetValue(object obj, object value) => Value = value;
}
public record ReflectionType(
string FullName,
string Namespace,
IReflectionField[] Fields,
IDictionary<Type, object> Attributes) : IReflectionType
{
public T GetCustomAttribute<T>() where T : Attribute => Attributes.TryGetValue(typeof(T), out var value) ? (T)value : null;
public IReflectionField[] GetFields(BindingFlags bindingAttr) => Fields;
}
private static readonly Type[] attributeTypes =
[
typeof(ConfigCollapseNamespaceAttribute),
typeof(ConfigSectionAttribute),
typeof(ConfigEntryAttribute),
];
private readonly IReflectionType[] reflectionTypes = [];
private readonly Dictionary<string, Dictionary<string, object>> enums = [];
public IReflectionType[] GetTypes() => reflectionTypes;
public Dictionary<string, object> GetEnum(string enumName) => enums[enumName];
public MonoCecilReflectionProvider(AssemblyDefinition assembly)
{
reflectionTypes = assembly.MainModule.Types.Select(cType => {
var typeAttributes = InstantiateAttributes(cType.CustomAttributes);
var fields = cType.Fields.Select(cField => {
try
{
var fieldAttributes = InstantiateAttributes(cField.CustomAttributes);
if (fieldAttributes.Count == 0)
{
return null;
}
var type = GetRuntimeType(cField.FieldType);
var defaultValue = GetFieldDefaultValue(cType, cField, type);
return new ReflectionField(cField.Name, type, defaultValue, fieldAttributes);
}
catch (Exception e)
{
Console.WriteLine(e);
}
return null;
}).Where(field => field != null).ToArray();
return new ReflectionType(cType.FullName, cType.Namespace, fields, typeAttributes);
}).ToArray();
enums = assembly.MainModule.Types
.Where(cType => cType.IsEnum)
.ToDictionary(cType =>
cType.FullName,
cType => cType.Fields
.Where(cField => cField.IsPublic && cField.IsStatic && cField.Constant != null)
.ToDictionary(cField => cField.Name, cField => cField.Constant));
}
private Dictionary<Type, object> InstantiateAttributes(ICollection<CustomAttribute> attribute) =>
attribute
.Select(InstantiateAttribute)
.Where(a => a != null)
.ToDictionary(a => a.GetType(), a => a);
private object InstantiateAttribute(CustomAttribute attribute) =>
attributeTypes.FirstOrDefault(t => t.FullName == attribute.AttributeType.FullName) switch
{
Type type => Activator.CreateInstance(type,
attribute.Constructor.Parameters
.Select((parameter, i) =>
{
var runtimeType = GetRuntimeType(parameter.ParameterType);
var value = attribute.ConstructorArguments[i].Value;
if (runtimeType.IsEnum)
{
return Enum.Parse(runtimeType, value.ToString());
}
return value;
})
.ToArray()),
_ => null
};
private Type GetRuntimeType(TypeReference typeReference) {
if (typeReference.IsGenericInstance)
{
var genericInstance = (GenericInstanceType)typeReference;
var genericType = GetRuntimeType(genericInstance.ElementType);
var genericArguments = genericInstance.GenericArguments.Select(GetRuntimeType).ToArray();
return genericType.MakeGenericType(genericArguments);
}
var type = Type.GetType(typeReference.FullName);
if (type == null)
{
throw new TypeLoadException($"Type {typeReference.FullName} not found.");
}
return type;
}
private static object GetFieldDefaultValue(TypeDefinition cType, FieldDefinition cField, Type fieldType)
{
object defaultValue = null;
var cctor = cType.Methods.SingleOrDefault(m => m.Name == ".cctor");
if (cctor != null)
{
var store = cctor.Body.Instructions.SingleOrDefault(i => i.OpCode == OpCodes.Stsfld && i.Operand == cField);
if (store != null)
{
var loadOperand = ParseConstantLoadOperand(store.Previous);
if (fieldType == typeof(bool))
{
defaultValue = Convert.ToBoolean(loadOperand);
}
else
{
defaultValue = loadOperand;
}
}
}
if (defaultValue == null && cField.HasDefault)
{
throw new InvalidOperationException($"Field {cType.FullName}.{cField.Name} has default value but no .cctor stsfld instruction.");
}
defaultValue ??= GetDefaultValue(fieldType);
if (fieldType.IsEnum)
{
var enumType = fieldType.GetEnumUnderlyingType();
// Assume casting is safe since we're getting the default value from the field
var castedValue = Convert.ChangeType(defaultValue, enumType);
if (Enum.IsDefined(fieldType, castedValue))
{
return Enum.ToObject(fieldType, castedValue);
}
}
return defaultValue;
}
private static object ParseConstantLoadOperand(Instruction instruction)
{
if (instruction.OpCode == OpCodes.Ldc_I4_M1)
{
return -1;
}
if (instruction.OpCode == OpCodes.Ldc_I4_0)
{
return 0;
}
if (instruction.OpCode == OpCodes.Ldc_I4_1)
{
return 1;
}
if (instruction.OpCode == OpCodes.Ldc_I4_2)
{
return 2;
}
if (instruction.OpCode == OpCodes.Ldc_I4_3)
{
return 3;
}
if (instruction.OpCode == OpCodes.Ldc_I4_4)
{
return 4;
}
if (instruction.OpCode == OpCodes.Ldc_I4_5)
{
return 5;
}
if (instruction.OpCode == OpCodes.Ldc_I4_6)
{
return 6;
}
if (instruction.OpCode == OpCodes.Ldc_I4_7)
{
return 7;
}
if (instruction.OpCode == OpCodes.Ldc_I4_8)
{
return 8;
}
if (instruction.OpCode == OpCodes.Ldc_I4_S)
{
return Convert.ToInt32((sbyte)instruction.Operand);
}
if (instruction.OpCode == OpCodes.Ldc_I4)
{
return (int)instruction.Operand;
}
if (instruction.OpCode == OpCodes.Ldc_I8)
{
return (long)instruction.Operand;
}
if (instruction.OpCode == OpCodes.Ldc_R4)
{
return (float)instruction.Operand;
}
if (instruction.OpCode == OpCodes.Ldc_R8)
{
return (double)instruction.Operand;
}
if (instruction.OpCode == OpCodes.Ldstr)
{
return (string)instruction.Operand;
}
else
{
var currentMethod = MethodBase.GetCurrentMethod();
throw new NotImplementedException($"Unsupported constant load: {instruction}. Please implement in {currentMethod.DeclaringType.FullName}.{currentMethod.Name}");
}
}
private static object GetDefaultValue(Type type)
{
if (type.IsValueType)
{
return Activator.CreateInstance(type);
}
else if (type == typeof(string))
{
return string.Empty;
}
else
{
return null;
}
}
}

View File

@@ -1,178 +0,0 @@
using System.Reflection;
using System.Linq;
using System.Collections.Generic;
using AquaMai.Config.Attributes;
using AquaMai.Config.Interfaces;
using System;
namespace AquaMai.Config.Reflection;
public class ReflectionManager : IReflectionManager
{
public record Entry : IReflectionManager.IEntry
{
public string Path { get; init; }
public string Name { get; init; }
public IReflectionField Field { get; init; }
public IConfigEntryAttribute Attribute { get; init; }
}
public record Section : IReflectionManager.ISection
{
public string Path { get; init; }
public IReflectionType Type { get; init; }
public IConfigSectionAttribute Attribute { get; init; }
public List<Entry> entries;
public List<IReflectionManager.IEntry> Entries => entries.Cast<IReflectionManager.IEntry>().ToList();
}
private readonly Dictionary<string, Section> sections = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, Entry> entries = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, Section> sectionsByFullName = [];
public ReflectionManager(IReflectionProvider reflectionProvider)
{
var prefix = "AquaMai.Mods.";
var types = reflectionProvider.GetTypes().Where(t => t.FullName.StartsWith(prefix));
var collapsedNamespaces = new HashSet<string>();
foreach (var type in types)
{
var sectionAttribute = type.GetCustomAttribute<ConfigSectionAttribute>();
if (sectionAttribute == null) continue;
if (collapsedNamespaces.Contains(type.Namespace))
{
throw new Exception($"Collapsed namespace {type.Namespace} contains multiple sections");
}
var path = type.FullName.Substring(prefix.Length);
if (type.GetCustomAttribute<ConfigCollapseNamespaceAttribute>() != null)
{
var separated = path.Split('.');
if (separated[separated.Length - 2] != separated[separated.Length - 1])
{
throw new Exception($"Type {type.FullName} is not collapsable");
}
path = string.Join(".", separated.Take(separated.Length - 1));
collapsedNamespaces.Add(type.Namespace);
}
var sectionEntries = new List<Entry>();
foreach (var field in type.GetFields(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic))
{
var entryAttribute = field.GetCustomAttribute<ConfigEntryAttribute>();
if (entryAttribute == null) continue;
var transformedName = Utility.ToPascalCase(field.Name);
var entryPath = $"{path}.{transformedName}";
var entry = new Entry()
{
Path = entryPath,
Name = transformedName,
Field = field,
Attribute = entryAttribute
};
sectionEntries.Add(entry);
entries.Add(entryPath, entry);
}
var section = new Section()
{
Path = path,
Type = type,
Attribute = sectionAttribute,
entries = sectionEntries
};
sections.Add(path, section);
sectionsByFullName.Add(type.FullName, section);
}
var order = reflectionProvider.GetEnum("AquaMai.Mods.SectionNameOrder");
sections = sections
.OrderBy(x => x.Key)
.OrderBy(x =>
{
var parts = x.Key.Split('.');
for (int i = parts.Length; i > 0; i--)
{
var key = string.Join("_", parts.Take(i));
if (order.TryGetValue(key, out var value))
{
return (int)value;
}
}
Utility.Log($"Section {x.Key} has no order defined, defaulting to int.MaxValue");
return int.MaxValue;
})
.ToDictionary(x => x.Key, x => x.Value, StringComparer.OrdinalIgnoreCase);
}
public IEnumerable<Section> SectionValues => sections.Values;
public IEnumerable<IReflectionManager.ISection> Sections => sections.Values.Cast<IReflectionManager.ISection>();
public IEnumerable<Entry> EntryValues => entries.Values;
public IEnumerable<IReflectionManager.IEntry> Entries => entries.Values.Cast<IReflectionManager.IEntry>();
public bool ContainsSection(string path)
{
return sections.ContainsKey(path);
}
public bool TryGetSection(string path, out IReflectionManager.ISection section)
{
if (sections.TryGetValue(path, out var sectionValue))
{
section = sectionValue;
return true;
}
section = null;
return false;
}
public bool TryGetSection(Type type, out IReflectionManager.ISection section)
{
bool result = sectionsByFullName.TryGetValue(type.FullName, out var sectionValue);
section = sectionValue;
return result;
}
public IReflectionManager.ISection GetSection(string path)
{
if (!TryGetSection(path, out var section))
{
throw new KeyNotFoundException($"Section {path} not found");
}
return section;
}
public IReflectionManager.ISection GetSection(Type type)
{
if (!TryGetSection(type.FullName, out var section))
{
throw new KeyNotFoundException($"Section {type.FullName} not found");
}
return section;
}
public bool ContainsEntry(string path)
{
return entries.ContainsKey(path);
}
public bool TryGetEntry(string path, out IReflectionManager.IEntry entry)
{
if (entries.TryGetValue(path, out var entryValue))
{
entry = entryValue;
return true;
}
entry = null;
return false;
}
public IReflectionManager.IEntry GetEntry(string path)
{
if (!TryGetEntry(path, out var entry))
{
throw new KeyNotFoundException($"Entry {path} not found");
}
return entry;
}
}

View File

@@ -1,47 +0,0 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using AquaMai.Config.Interfaces;
namespace AquaMai.Config.Reflection;
public class SystemReflectionProvider(Assembly assembly) : IReflectionProvider
{
public class ReflectionField(FieldInfo field) : IReflectionField
{
public FieldInfo UnderlyingField { get; } = field;
public string Name => UnderlyingField.Name;
public Type FieldType => UnderlyingField.FieldType;
public T GetCustomAttribute<T>() where T : Attribute => UnderlyingField.GetCustomAttribute<T>();
public object GetValue(object obj) => UnderlyingField.GetValue(obj);
public void SetValue(object obj, object value) => UnderlyingField.SetValue(obj, value);
}
public class ReflectionType(Type type) : IReflectionType
{
public Type UnderlyingType { get; } = type;
public string FullName => UnderlyingType.FullName;
public string Namespace => UnderlyingType.Namespace;
public T GetCustomAttribute<T>() where T : Attribute => UnderlyingType.GetCustomAttribute<T>();
public IReflectionField[] GetFields(BindingFlags bindingAttr) => Array.ConvertAll(UnderlyingType.GetFields(bindingAttr), f => new ReflectionField(f));
}
public Assembly UnderlyingAssembly { get; } = assembly;
public IReflectionType[] GetTypes() => Array.ConvertAll(UnderlyingAssembly.GetTypes(), t => new ReflectionType(t));
public Dictionary<string, object> GetEnum(string enumName)
{
var enumType = UnderlyingAssembly.GetType(enumName);
if (enumType == null) return null;
var enumValues = Enum.GetValues(enumType);
var enumDict = new Dictionary<string, object>();
foreach (var enumValue in enumValues)
{
enumDict.Add(enumValue.ToString(), enumValue);
}
return enumDict;
}
}

View File

@@ -1,146 +0,0 @@
namespace AquaMai.Config.Types;
public enum KeyCodeID
{
None,
Backspace,
Tab,
Clear,
Return,
Pause,
Escape,
Space,
Exclaim,
DoubleQuote,
Hash,
Dollar,
Ampersand,
Quote,
LeftParen,
RightParen,
Asterisk,
Plus,
Comma,
Minus,
Period,
Slash,
Alpha0,
Alpha1,
Alpha2,
Alpha3,
Alpha4,
Alpha5,
Alpha6,
Alpha7,
Alpha8,
Alpha9,
Colon,
Semicolon,
Less,
Equals,
Greater,
Question,
At,
LeftBracket,
Backslash,
RightBracket,
Caret,
Underscore,
BackQuote,
A,
B,
C,
D,
E,
F,
G,
H,
I,
J,
K,
L,
M,
N,
O,
P,
Q,
R,
S,
T,
U,
V,
W,
X,
Y,
Z,
Delete,
Keypad0,
Keypad1,
Keypad2,
Keypad3,
Keypad4,
Keypad5,
Keypad6,
Keypad7,
Keypad8,
Keypad9,
KeypadPeriod,
KeypadDivide,
KeypadMultiply,
KeypadMinus,
KeypadPlus,
KeypadEnter,
KeypadEquals,
UpArrow,
DownArrow,
RightArrow,
LeftArrow,
Insert,
Home,
End,
PageUp,
PageDown,
F1,
F2,
F3,
F4,
F5,
F6,
F7,
F8,
F9,
F10,
F11,
F12,
F13,
F14,
F15,
Numlock,
CapsLock,
ScrollLock,
RightShift,
LeftShift,
RightControl,
LeftControl,
RightAlt,
LeftAlt,
RightCommand,
RightApple,
LeftCommand,
LeftApple,
LeftWindows,
RightWindows,
AltGr,
Help,
Print,
SysReq,
Break,
Menu,
Mouse0,
Mouse1,
Mouse2,
Mouse3,
Mouse4,
Mouse5,
Mouse6,
}

View File

@@ -1,53 +0,0 @@
namespace AquaMai.Config.Types;
public enum KeyCodeOrName
{
None,
Alpha0,
Alpha1,
Alpha2,
Alpha3,
Alpha4,
Alpha5,
Alpha6,
Alpha7,
Alpha8,
Alpha9,
Keypad0,
Keypad1,
Keypad2,
Keypad3,
Keypad4,
Keypad5,
Keypad6,
Keypad7,
Keypad8,
Keypad9,
F1,
F2,
F3,
F4,
F5,
F6,
F7,
F8,
F9,
F10,
F11,
F12,
Insert,
Delete,
Home,
End,
PageUp,
PageDown,
UpArrow,
DownArrow,
LeftArrow,
RightArrow,
Select1P,
Select2P,
Service,
Test,
}

View File

@@ -1,193 +0,0 @@
using System;
using System.Reflection;
using Tomlet.Models;
namespace AquaMai.Config;
public static class Utility
{
public static Action<string> LogFunction = Console.WriteLine;
public static bool IsTruty(TomlValue value, string path = null)
{
return value switch
{
TomlBoolean boolean => boolean.Value,
TomlLong @long => @long.Value != 0,
_ => throw new ArgumentException(
path == null
? $"Non-boolish TOML type {value.GetType().Name} value: {value}"
: $"When parsing {path}, got non-boolish TOML type {value.GetType().Name} value: {value}")
};
}
public static bool IsTrutyOrDefault(TomlValue value, bool defaultValue = false)
{
return value switch
{
TomlBoolean boolean => boolean.Value,
TomlLong @long => @long.Value != 0,
_ => defaultValue
};
}
public static bool IsIntegerType(Type type)
{
return type == typeof(sbyte) || type == typeof(short) || type == typeof(int) || type == typeof(long)
|| type == typeof(byte) || type == typeof(ushort) || type == typeof(uint) || type == typeof(ulong);
}
public static bool IsFloatType(Type type)
{
return type == typeof(float) || type == typeof(double);
}
public static bool IsNumberType(Type type)
{
return IsIntegerType(type) || IsFloatType(type);
}
public static T ParseTomlValue<T>(TomlValue value)
{
return (T)ParseTomlValue(typeof(T), value);
}
public static object ParseTomlValue(Type type, TomlValue value)
{
if (type == typeof(bool))
{
return IsTruty(value);
}
else if (IsNumberType(type))
{
if (TryGetTomlNumberObject(value, out var numberObject))
{
return Convert.ChangeType(numberObject, type);
}
else
{
throw new InvalidCastException($"Non-number TOML type: {value.GetType().Name}");
}
}
else if (type == typeof(string))
{
if (value is TomlString @string)
{
return @string.Value;
}
else
{
throw new InvalidCastException($"Non-string TOML type: {value.GetType().Name}");
}
}
else if (type.IsEnum)
{
if (value is TomlString @string)
{
try
{
return Enum.Parse(type, @string.Value);
}
catch
{
throw new InvalidCastException($"Invalid enum {type.FullName} value: {@string.SerializedValue}");
}
}
else if (value is TomlLong @long)
{
if (Enum.IsDefined(type, @long.Value))
{
try
{
return Enum.ToObject(type, @long.Value);
}
catch
{}
}
throw new InvalidCastException($"Invalid enum {type.FullName} value: {@long.Value}");
}
else
{
throw new InvalidCastException($"Non-enum TOML type: {value.GetType().Name}");
}
}
else
{
var currentMethod = MethodBase.GetCurrentMethod();
throw new NotImplementedException($"Unsupported config entry type: {type.FullName}. Please implement in {currentMethod.DeclaringType.FullName}.{currentMethod.Name}");
}
}
private static bool TryGetTomlNumberObject(TomlValue value, out object numberObject)
{
if (value is TomlLong @long)
{
numberObject = @long.Value;
return true;
}
else if (value is TomlDouble @double)
{
numberObject = @double.Value;
return true;
}
else
{
numberObject = null;
return false;
}
}
public static bool TomlTryGetValueCaseInsensitive(TomlTable table, string key, out TomlValue value)
{
// Prefer exact match
if (table.TryGetValue(key, out value))
{
return true;
}
// Fallback to case-insensitive match
foreach (var kvp in table)
{
if (string.Equals(kvp.Key, key, StringComparison.OrdinalIgnoreCase))
{
value = kvp.Value;
return true;
}
}
value = null;
return false;
}
public static bool TomlContainsKeyCaseInsensitive(TomlTable table, string key)
{
// Prefer exact match
if (table.ContainsKey(key))
{
return true;
}
// Fallback to case-insensitive match
foreach (var kvp in table)
{
if (string.Equals(kvp.Key, key, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
return false;
}
public static string ToPascalCase(string str)
{
return str.Length switch
{
0 => str,
1 => char.ToUpperInvariant(str[0]).ToString(),
_ => char.ToUpperInvariant(str[0]) + str.Substring(1)
};
}
// We can test the configuration related code without loading the mod into the game.
public static void Log(string message)
{
LogFunction(message);
}
}

View File

@@ -1,132 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Release</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{33C0D4ED-6A84-4659-9A05-12D43D75D0B3}</ProjectGuid>
<OutputType>Library</OutputType>
<RootNamespace>AquaMai.Core</RootNamespace>
<AssemblyName>AquaMai.Core</AssemblyName>
<TargetFramework>net472</TargetFramework>
<FileAlignment>512</FileAlignment>
<Deterministic>true</Deterministic>
<LangVersion>12</LangVersion>
<NoWarn>414</NoWarn>
<AssemblySearchPaths>$(ProjectDir)../Libs/;$(AssemblySearchPaths)</AssemblySearchPaths>
<OutputPath>$(ProjectDir)../Output/</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<EnableDefaultEmbeddedResourceItems>false</EnableDefaultEmbeddedResourceItems>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugSymbols>false</DebugSymbols>
<DebugType>None</DebugType>
<Optimize>true</Optimize>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DefineConstants>DEBUG</DefineConstants>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../AquaMai.Config/AquaMai.Config.csproj" />
</ItemGroup>
<ItemGroup>
<Reference Include="mscorlib" />
<Reference Include="0Harmony" />
<Reference Include="AMDaemon.NET" />
<Reference Include="Assembly-CSharp" />
<Reference Include="Assembly-CSharp-firstpass" />
<Reference Include="MelonLoader" />
<Reference Include="Mono.Cecil" />
<Reference Include="Mono.Posix" />
<Reference Include="Mono.Security" />
<Reference Include="System" />
<Reference Include="System.Configuration" />
<Reference Include="System.Core" />
<Reference Include="System.Security" />
<Reference Include="System.Xml" />
<Reference Include="Unity.Analytics.DataPrivacy" />
<Reference Include="Unity.TextMeshPro" />
<Reference Include="UnityEngine" />
<Reference Include="UnityEngine.AccessibilityModule" />
<Reference Include="UnityEngine.AIModule" />
<Reference Include="UnityEngine.AnimationModule" />
<Reference Include="UnityEngine.ARModule" />
<Reference Include="UnityEngine.AssetBundleModule" />
<Reference Include="UnityEngine.AudioModule" />
<Reference Include="UnityEngine.BaselibModule" />
<Reference Include="UnityEngine.ClothModule" />
<Reference Include="UnityEngine.ClusterInputModule" />
<Reference Include="UnityEngine.ClusterRendererModule" />
<Reference Include="UnityEngine.CoreModule" />
<Reference Include="UnityEngine.CrashReportingModule" />
<Reference Include="UnityEngine.DirectorModule" />
<Reference Include="UnityEngine.FileSystemHttpModule" />
<Reference Include="UnityEngine.GameCenterModule" />
<Reference Include="UnityEngine.GridModule" />
<Reference Include="UnityEngine.HotReloadModule" />
<Reference Include="UnityEngine.ImageConversionModule" />
<Reference Include="UnityEngine.IMGUIModule" />
<Reference Include="UnityEngine.InputModule" />
<Reference Include="UnityEngine.JSONSerializeModule" />
<Reference Include="UnityEngine.LocalizationModule" />
<Reference Include="UnityEngine.Networking" />
<Reference Include="UnityEngine.ParticleSystemModule" />
<Reference Include="UnityEngine.PerformanceReportingModule" />
<Reference Include="UnityEngine.Physics2DModule" />
<Reference Include="UnityEngine.PhysicsModule" />
<Reference Include="UnityEngine.ProfilerModule" />
<Reference Include="UnityEngine.ScreenCaptureModule" />
<Reference Include="UnityEngine.SharedInternalsModule" />
<Reference Include="UnityEngine.SpatialTracking" />
<Reference Include="UnityEngine.SpriteMaskModule" />
<Reference Include="UnityEngine.SpriteShapeModule" />
<Reference Include="UnityEngine.StreamingModule" />
<Reference Include="UnityEngine.StyleSheetsModule" />
<Reference Include="UnityEngine.SubstanceModule" />
<Reference Include="UnityEngine.TerrainModule" />
<Reference Include="UnityEngine.TerrainPhysicsModule" />
<Reference Include="UnityEngine.TextCoreModule" />
<Reference Include="UnityEngine.TextRenderingModule" />
<Reference Include="UnityEngine.TilemapModule" />
<Reference Include="UnityEngine.Timeline" />
<Reference Include="UnityEngine.TimelineModule" />
<Reference Include="UnityEngine.TLSModule" />
<Reference Include="UnityEngine.UI" />
<Reference Include="UnityEngine.UIElementsModule" />
<Reference Include="UnityEngine.UIModule" />
<Reference Include="UnityEngine.UmbraModule" />
<Reference Include="UnityEngine.UNETModule" />
<Reference Include="UnityEngine.UnityAnalyticsModule" />
<Reference Include="UnityEngine.UnityConnectModule" />
<Reference Include="UnityEngine.UnityTestProtocolModule" />
<Reference Include="UnityEngine.UnityWebRequestAssetBundleModule" />
<Reference Include="UnityEngine.UnityWebRequestAudioModule" />
<Reference Include="UnityEngine.UnityWebRequestModule" />
<Reference Include="UnityEngine.UnityWebRequestTextureModule" />
<Reference Include="UnityEngine.UnityWebRequestWWWModule" />
<Reference Include="UnityEngine.VehiclesModule" />
<Reference Include="UnityEngine.VFXModule" />
<Reference Include="UnityEngine.VideoModule" />
<Reference Include="UnityEngine.VRModule" />
<Reference Include="UnityEngine.WindModule" />
<Reference Include="UnityEngine.XRModule" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Resources/Locale.resx">
<Generator>PublicResXFileCodeGenerator</Generator>
<LastGenOutput>Locale.Designer.cs</LastGenOutput>
</EmbeddedResource>
<EmbeddedResource Include="Resources/Locale.zh.resx" WithCulture="false">
<DependentUpon>Locale.resx</DependentUpon>
</EmbeddedResource>
</ItemGroup>
</Project>

View File

@@ -1,18 +0,0 @@
using System;
namespace AquaMai.Core.Attributes;
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
public class EnableGameVersionAttribute(uint minVersion = 0, uint maxVersion = 0, bool noWarn = false) : Attribute
{
public uint MinVersion { get; } = minVersion;
public uint MaxVersion { get; } = maxVersion;
public bool NoWarn { get; } = noWarn;
public bool ShouldEnable(uint gameVersion)
{
if (MinVersion > 0 && MinVersion > gameVersion) return false;
if (MaxVersion > 0 && MaxVersion < gameVersion) return false;
return true;
}
}

View File

@@ -1,79 +0,0 @@
using System;
namespace AquaMai.Core.Attributes;
public enum EnableConditionOperator
{
Equal,
NotEqual,
GreaterThan,
LessThan,
GreaterThanOrEqual,
LessThanOrEqual
}
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
public class EnableIfAttribute(
Type referenceType,
string referenceMember,
EnableConditionOperator @operator,
object rightSideValue) : Attribute
{
public Type ReferenceType { get; } = referenceType;
public string ReferenceMember { get; } = referenceMember;
public EnableConditionOperator Operator { get; } = @operator;
public object RightSideValue { get; } = rightSideValue;
// Referencing a field in another class and checking if it's true.
public EnableIfAttribute(Type referenceType, string referenceMember)
: this(referenceType, referenceMember, EnableConditionOperator.Equal, true)
{ }
// Referencing a field in the same class and comparing it with a value.
public EnableIfAttribute(string referenceMember, EnableConditionOperator condition, object value)
: this(null, referenceMember, condition, value)
{ }
// Referencing a field in the same class and checking if it's true.
public EnableIfAttribute(string referenceMember)
: this(referenceMember, EnableConditionOperator.Equal, true)
{ }
public bool ShouldEnable(Type selfType)
{
var referenceType = ReferenceType ?? selfType;
var referenceField = referenceType.GetField(
ReferenceMember,
System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic);
var referenceProperty = referenceType.GetProperty(
ReferenceMember,
System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic);
if (referenceField == null && referenceProperty == null)
{
throw new ArgumentException($"Field or property {ReferenceMember} not found in {referenceType.FullName}");
}
var referenceMemberValue = referenceField != null ? referenceField.GetValue(null) : referenceProperty.GetValue(null);
switch (Operator)
{
case EnableConditionOperator.Equal:
return referenceMemberValue.Equals(RightSideValue);
case EnableConditionOperator.NotEqual:
return !referenceMemberValue.Equals(RightSideValue);
case EnableConditionOperator.GreaterThan:
case EnableConditionOperator.LessThan:
case EnableConditionOperator.GreaterThanOrEqual:
case EnableConditionOperator.LessThanOrEqual:
var comparison = (IComparable)referenceMemberValue;
return Operator switch
{
EnableConditionOperator.GreaterThan => comparison.CompareTo(RightSideValue) > 0,
EnableConditionOperator.LessThan => comparison.CompareTo(RightSideValue) < 0,
EnableConditionOperator.GreaterThanOrEqual => comparison.CompareTo(RightSideValue) >= 0,
EnableConditionOperator.LessThanOrEqual => comparison.CompareTo(RightSideValue) <= 0,
_ => throw new NotImplementedException(),
};
default:
throw new NotImplementedException();
}
}
}

View File

@@ -1,12 +0,0 @@
using System;
namespace AquaMai.Core.Attributes;
// If the field or property with this name is true, the patch will be implicitly enabled, regardless of the config state.
// This is handled outside the config module, while The config state won't be actually set to enabled by it.
// Won't bypass the restriction of [EnableIf()] and [EnableGameVersion()].
[AttributeUsage(AttributeTargets.Class)]
public class EnableImplicitlyIf(string memberName) : Attribute
{
public string MemberName { get; } = memberName;
}

View File

@@ -1,83 +0,0 @@
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using MelonLoader;
using AquaMai.Config;
using AquaMai.Config.Interfaces;
using AquaMai.Config.Migration;
namespace AquaMai.Core;
public static class ConfigLoader
{
private static string ConfigFile => "AquaMai.toml";
private static string ConfigExampleFile(string lang) => $"AquaMai.{lang}.toml";
private static string OldConfigFile(string version) => $"AquaMai.toml.old-v{version}.";
private static Config.Config config;
public static Config.Config Config => config;
public static bool LoadConfig(Assembly modsAssembly)
{
Utility.LogFunction = MelonLogger.Msg;
config = new(
new Config.Reflection.ReflectionManager(
new Config.Reflection.SystemReflectionProvider(modsAssembly)));
if (!File.Exists(ConfigFile))
{
var examples = GenerateExamples();
foreach (var (lang, example) in examples)
{
var filename = ConfigExampleFile(lang);
File.WriteAllText(filename, example);
}
MelonLogger.Error("======================================!!!");
MelonLogger.Error("AquaMai.toml not found! Please create it.");
MelonLogger.Error("找不到配置文件 AquaMai.toml请创建。");
MelonLogger.Error("Example copied to AquaMai.en.toml");
MelonLogger.Error("示例已复制到 AquaMai.zh.toml");
MelonLogger.Error("=========================================");
return false;
}
var configText = File.ReadAllText(ConfigFile);
var configView = new ConfigView(configText);
var configVersion = ConfigMigrationManager.Instance.GetVersion(configView);
if (configVersion != ConfigMigrationManager.Instance.LatestVersion)
{
File.WriteAllText(OldConfigFile(configVersion), configText);
configView = (ConfigView)ConfigMigrationManager.Instance.Migrate(configView);
}
// Read AquaMai.toml to load settings
ConfigParser.Instance.Parse(config, configView);
return true;
}
public static void SaveConfig(string lang)
{
File.WriteAllText(ConfigFile, SerailizeCurrentConfig(lang));
}
private static string SerailizeCurrentConfig(string lang) =>
new ConfigSerializer(new IConfigSerializer.Options()
{
Lang = lang,
IncludeBanner = true,
OverrideLocaleValue = true
}).Serialize(config);
private static IDictionary<string, string> GenerateExamples()
{
var examples = new Dictionary<string, string>();
foreach (var lang in (string[]) ["en", "zh"])
{
examples[lang] = SerailizeCurrentConfig(lang);
}
return examples;
}
}

View File

@@ -1,72 +0,0 @@
using System;
using System.Collections;
using System.Reflection;
using AquaMai.Core.Attributes;
using AquaMai.Core.Resources;
using HarmonyLib;
using MelonLoader;
namespace AquaMai.Core.Helpers;
public class EnableConditionHelper
{
[HarmonyPostfix]
[HarmonyPatch("HarmonyLib.PatchTools", "GetPatchMethod")]
public static void PostGetPatchMethod(ref MethodInfo __result)
{
if (__result != null)
{
if (ShouldSkipMethodOrClass(__result.GetCustomAttribute, __result.ReflectedType, __result.Name))
{
__result = null;
}
}
}
[HarmonyPostfix]
[HarmonyPatch("HarmonyLib.PatchTools", "GetPatchMethods")]
public static void PostGetPatchMethods(ref IList __result)
{
for (int i = 0; i < __result.Count; i++)
{
var harmonyMethod = Traverse.Create(__result[i]).Field("info").GetValue() as HarmonyMethod;
var method = harmonyMethod.method;
if (ShouldSkipMethodOrClass(method.GetCustomAttribute, method.ReflectedType, method.Name))
{
__result.RemoveAt(i);
i--;
}
}
}
public static bool ShouldSkipClass(Type type)
{
return ShouldSkipMethodOrClass(type.GetCustomAttribute, type);
}
private static bool ShouldSkipMethodOrClass(Func<Type, object> getCustomAttribute, Type type, string methodName = "")
{
var displayName = type.FullName + (string.IsNullOrEmpty(methodName) ? "" : $".{methodName}");
var enableIf = (EnableIfAttribute)getCustomAttribute(typeof(EnableIfAttribute));
if (enableIf != null && !enableIf.ShouldEnable(type))
{
# if DEBUG
MelonLogger.Msg($"Skipping {displayName} due to EnableIf condition");
# endif
return true;
}
var enableGameVersion = (EnableGameVersionAttribute)getCustomAttribute(typeof(EnableGameVersionAttribute));
if (enableGameVersion != null && !enableGameVersion.ShouldEnable(GameInfo.GameVersion))
{
# if DEBUG
MelonLogger.Msg($"Skipping {displayName} due to EnableGameVersion condition");
# endif
if (!enableGameVersion.NoWarn)
{
MelonLogger.Warning(string.Format(Locale.SkipIncompatiblePatch, type));
}
return true;
}
return false;
}
}

View File

@@ -1,15 +0,0 @@
using System;
using System.IO;
namespace AquaMai.Core.Helpers;
public static class FileSystem
{
public static string ResolvePath(string path)
{
var varExpanded = Environment.ExpandEnvironmentVariables(path);
return Path.IsPathRooted(varExpanded)
? varExpanded
: Path.Combine(Environment.CurrentDirectory, varExpanded);
}
}

View File

@@ -1,21 +0,0 @@
using System.Reflection;
using MAI2System;
namespace AquaMai.Core.Helpers;
public class GameInfo
{
public static uint GameVersion { get; } = GetGameVersion();
private static uint GetGameVersion()
{
return (uint)typeof(ConstParameter).GetField("NowGameVersion", BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy).GetValue(null);
}
public static string GameId { get; } = GetGameId();
private static string GetGameId()
{
return typeof(ConstParameter).GetField("GameIDStr", BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy).GetValue(null) as string;
}
}

View File

@@ -1,57 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using HarmonyLib;
using UnityEngine;
namespace AquaMai.Core.Helpers;
public static class GuiSizes
{
public static bool SinglePlayer { get; set; } = false;
public static float PlayerWidth => Screen.height / 1920f * 1080;
public static float PlayerCenter => SinglePlayer ? Screen.width / 2f : Screen.width / 2f - PlayerWidth / 2;
public static int FontSize => (int)(PlayerWidth * .015f);
public static float LabelHeight => FontSize * 1.5f;
public static float Margin => PlayerWidth * .005f;
private static Color backgroundColor = new(147 / 256f, 160 / 256f, 173 / 256f, .8f);
public static void SetupStyles()
{
var buttonStyle = GUI.skin.button;
buttonStyle.normal.textColor = Color.white;
buttonStyle.normal.background = Texture2D.whiteTexture;
buttonStyle.hover.background = Texture2D.whiteTexture;
buttonStyle.active.background = Texture2D.whiteTexture;
buttonStyle.border = new RectOffset(0, 0, 0, 0);
buttonStyle.margin = new RectOffset(0, 0, 0, 0);
buttonStyle.padding = new RectOffset(10, 10, 10, 10);
buttonStyle.overflow = new RectOffset(0, 0, 0, 0);
var boxStyle = GUI.skin.box;
boxStyle.border = new RectOffset(0, 0, 0, 0);
boxStyle.normal.background = Texture2D.whiteTexture;
GUI.backgroundColor = backgroundColor;
}
[HarmonyPatch]
public class BoxBackground
{
public static IEnumerable<MethodBase> TargetMethods()
{
return typeof(GUI).GetMethods().Where(x => x.Name == "Box");
}
public static void Prefix()
{
GUI.backgroundColor = new Color(62 / 256f, 62 / 256f, 66 / 256f, .6f);
}
public static void Postfix()
{
GUI.backgroundColor = backgroundColor;
}
}
}

View File

@@ -1,146 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using AquaMai.Config.Types;
using HarmonyLib;
using Main;
using Manager;
using MelonLoader;
using UnityEngine;
namespace AquaMai.Core.Helpers;
public static class KeyListener
{
private static readonly Dictionary<KeyCodeOrName, int> _keyPressFrames = [];
private static readonly Dictionary<KeyCodeOrName, int> _keyPressFramesPrev = [];
static KeyListener()
{
foreach (KeyCodeOrName key in Enum.GetValues(typeof(KeyCodeOrName)))
{
_keyPressFrames[key] = 0;
_keyPressFramesPrev[key] = 0;
}
}
[HarmonyPostfix]
[HarmonyPatch(typeof(GameMainObject), "Update")]
public static void CheckLongPush()
{
foreach (KeyCodeOrName key in Enum.GetValues(typeof(KeyCodeOrName)))
{
_keyPressFramesPrev[key] = _keyPressFrames[key];
if (GetKeyPush(key))
{
# if DEBUG
MelonLogger.Msg($"CheckLongPush {key} is push {_keyPressFrames[key]}");
# endif
_keyPressFrames[key]++;
}
else
{
_keyPressFrames[key] = 0;
}
}
}
public static bool GetKeyPush(KeyCodeOrName key) =>
key switch
{
KeyCodeOrName.None => false,
< KeyCodeOrName.Select1P => Input.GetKey(key.GetKeyCode()),
KeyCodeOrName.Test => InputManager.GetSystemInputPush(InputManager.SystemButtonSetting.ButtonTest),
KeyCodeOrName.Service => InputManager.GetSystemInputPush(InputManager.SystemButtonSetting.ButtonService),
KeyCodeOrName.Select1P => InputManager.GetButtonPush(0, InputManager.ButtonSetting.Select),
KeyCodeOrName.Select2P => InputManager.GetButtonPush(1, InputManager.ButtonSetting.Select),
_ => throw new ArgumentOutOfRangeException(nameof(key), key, "我也不知道这是什么键")
};
public static bool GetKeyDown(KeyCodeOrName key)
{
// return key switch
// {
// KeyCodeOrName.None => false,
// < KeyCodeOrName.Select1P => Input.GetKeyDown(key.GetKeyCode()),
// KeyCodeOrName.Test => InputManager.GetSystemInputDown(InputManager.SystemButtonSetting.ButtonTest),
// KeyCodeOrName.Service => InputManager.GetSystemInputDown(InputManager.SystemButtonSetting.ButtonService),
// KeyCodeOrName.Select1P => InputManager.GetButtonDown(0, InputManager.ButtonSetting.Select),
// KeyCodeOrName.Select2P => InputManager.GetButtonDown(1, InputManager.ButtonSetting.Select),
// _ => throw new ArgumentOutOfRangeException(nameof(key), key, "我也不知道这是什么键")
// };
// 不用这个,我们检测按键是否弹起以及弹起之前按下的时间是否小于 30这样可以防止要长按时按下的时候就触发
return _keyPressFrames[key] == 0 && 0 < _keyPressFramesPrev[key] && _keyPressFramesPrev[key] < 30;
}
public static bool GetKeyDownOrLongPress(KeyCodeOrName key, bool isLongPress)
{
bool ret;
if (isLongPress)
{
ret = _keyPressFrames[key] == 60;
}
else
{
ret = GetKeyDown(key);
}
# if DEBUG
if (ret)
{
MelonLogger.Msg($"Key {key} is pressed, long press: {isLongPress}");
MelonLogger.Msg(new StackTrace());
}
# endif
return ret;
}
private static KeyCode GetKeyCode(this KeyCodeOrName keyCodeOrName) =>
keyCodeOrName switch
{
KeyCodeOrName.Alpha0 => KeyCode.Alpha0,
KeyCodeOrName.Alpha1 => KeyCode.Alpha1,
KeyCodeOrName.Alpha2 => KeyCode.Alpha2,
KeyCodeOrName.Alpha3 => KeyCode.Alpha3,
KeyCodeOrName.Alpha4 => KeyCode.Alpha4,
KeyCodeOrName.Alpha5 => KeyCode.Alpha5,
KeyCodeOrName.Alpha6 => KeyCode.Alpha6,
KeyCodeOrName.Alpha7 => KeyCode.Alpha7,
KeyCodeOrName.Alpha8 => KeyCode.Alpha8,
KeyCodeOrName.Alpha9 => KeyCode.Alpha9,
KeyCodeOrName.Keypad0 => KeyCode.Keypad0,
KeyCodeOrName.Keypad1 => KeyCode.Keypad1,
KeyCodeOrName.Keypad2 => KeyCode.Keypad2,
KeyCodeOrName.Keypad3 => KeyCode.Keypad3,
KeyCodeOrName.Keypad4 => KeyCode.Keypad4,
KeyCodeOrName.Keypad5 => KeyCode.Keypad5,
KeyCodeOrName.Keypad6 => KeyCode.Keypad6,
KeyCodeOrName.Keypad7 => KeyCode.Keypad7,
KeyCodeOrName.Keypad8 => KeyCode.Keypad8,
KeyCodeOrName.Keypad9 => KeyCode.Keypad9,
KeyCodeOrName.F1 => KeyCode.F1,
KeyCodeOrName.F2 => KeyCode.F2,
KeyCodeOrName.F3 => KeyCode.F3,
KeyCodeOrName.F4 => KeyCode.F4,
KeyCodeOrName.F5 => KeyCode.F5,
KeyCodeOrName.F6 => KeyCode.F6,
KeyCodeOrName.F7 => KeyCode.F7,
KeyCodeOrName.F8 => KeyCode.F8,
KeyCodeOrName.F9 => KeyCode.F9,
KeyCodeOrName.F10 => KeyCode.F10,
KeyCodeOrName.F11 => KeyCode.F11,
KeyCodeOrName.F12 => KeyCode.F12,
KeyCodeOrName.Insert => KeyCode.Insert,
KeyCodeOrName.Delete => KeyCode.Delete,
KeyCodeOrName.Home => KeyCode.Home,
KeyCodeOrName.End => KeyCode.End,
KeyCodeOrName.PageUp => KeyCode.PageUp,
KeyCodeOrName.PageDown => KeyCode.PageDown,
KeyCodeOrName.UpArrow => KeyCode.UpArrow,
KeyCodeOrName.DownArrow => KeyCode.DownArrow,
KeyCodeOrName.LeftArrow => KeyCode.LeftArrow,
KeyCodeOrName.RightArrow => KeyCode.RightArrow,
_ => throw new ArgumentOutOfRangeException(nameof(keyCodeOrName), keyCodeOrName, "游戏功能键需要单独处理")
};
}

View File

@@ -1,39 +0,0 @@
using DB;
using HarmonyLib;
using Manager;
using MelonLoader;
using Process;
namespace AquaMai.Core.Helpers;
public class MessageHelper
{
private static IGenericManager _genericManager = null;
[HarmonyPostfix]
[HarmonyPatch(typeof(ProcessManager), "SetMessageManager")]
private static void OnSetMessageManager(IGenericManager genericManager)
{
_genericManager = genericManager;
}
public static void ShowMessage(string message, WindowSizeID size = WindowSizeID.Middle, string title = null)
{
if (_genericManager is null)
{
MelonLogger.Error($"[MessageHelper] Unable to show message: `{message}` GenericManager is null");
return;
}
_genericManager.Enqueue(0, WindowMessageID.CollectionAttentionEmptyFavorite, new WindowParam()
{
hideTitle = title is null,
replaceTitle = true,
title = title,
replaceText = true,
text = message,
changeSize = true,
sizeID = size,
});
}
}

View File

@@ -1,31 +0,0 @@
using System.Collections.Generic;
using HarmonyLib;
namespace AquaMai.Core.Helpers;
public class MusicDirHelper
{
private static Dictionary<int, string> _map = new();
[HarmonyPostfix]
[HarmonyPatch(typeof(Manager.MaiStudio.Serialize.MusicData), "AddPath")]
private static void AddPath(Manager.MaiStudio.Serialize.MusicData __instance, string parentPath)
{
_map[__instance.GetID()] = parentPath;
}
public static string LookupPath(int id)
{
return _map.GetValueOrDefault(id);
}
public static string LookupPath(Manager.MaiStudio.Serialize.MusicData musicData)
{
return LookupPath(musicData.GetID());
}
public static string LookupPath(Manager.MaiStudio.MusicData musicData)
{
return LookupPath(musicData.GetID());
}
}

View File

@@ -1,25 +0,0 @@
using HarmonyLib;
using Main;
using Process;
namespace AquaMai.Core.Helpers;
public class SharedInstances
{
public static ProcessDataContainer ProcessDataContainer { get; private set; }
public static GameMainObject GameMainObject { get; private set; }
[HarmonyPrefix]
[HarmonyPatch(typeof(ProcessDataContainer), MethodType.Constructor)]
public static void OnCreateProcessDataContainer(ProcessDataContainer __instance)
{
ProcessDataContainer = __instance;
}
[HarmonyPrefix]
[HarmonyPatch(typeof(GameMainObject), "Awake")]
public static void OnCreateGameMainObject(GameMainObject __instance)
{
GameMainObject = __instance;
}
}

View File

@@ -1,133 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using HarmonyLib;
using MAI2.Util;
using Manager;
using Manager.UserDatas;
using MelonLoader;
using Net;
using Net.Packet;
using Net.Packet.Mai2;
using Net.VO;
namespace AquaMai.Core.Helpers;
public static class Shim
{
private static T Iife<T>(Func<T> func) => func();
public static readonly string apiSuffix = Iife(() =>
{
try
{
var baseNetQueryConstructor = typeof(NetQuery<VOSerializer, VOSerializer>)
.GetConstructors()
.First();
return ((INetQuery)baseNetQueryConstructor.Invoke(
baseNetQueryConstructor
.GetParameters()
.Select((parameter, i) => i == 0 ? "" : parameter.DefaultValue)
.ToArray())).Api;
}
catch (Exception e)
{
MelonLogger.Error($"Failed to resolve the API suffix: {e}");
return null;
}
});
public static string RemoveApiSuffix(string api)
{
return !string.IsNullOrEmpty(apiSuffix) && api.EndsWith(apiSuffix)
? api.Substring(0, api.Length - apiSuffix.Length)
: api;
}
public delegate string GetAccessTokenMethod(int index);
public static readonly GetAccessTokenMethod GetAccessToken = Iife<GetAccessTokenMethod>(() =>
{
var tOperationManager = Traverse.Create(Singleton<OperationManager>.Instance);
var tGetAccessToken = tOperationManager.Method("GetAccessToken", [typeof(int)]);
if (!tGetAccessToken.MethodExists())
{
return (index) => throw new MissingMethodException("No matching OperationManager.GetAccessToken() method found");
}
return (index) => tGetAccessToken.GetValue<string>(index);
});
public delegate PacketUploadUserPlaylog PacketUploadUserPlaylogCreator(int index, UserData src, int trackNo, Action<int> onDone, Action<PacketStatus> onError = null);
public static readonly PacketUploadUserPlaylogCreator CreatePacketUploadUserPlaylog = Iife<PacketUploadUserPlaylogCreator>(() =>
{
var type = typeof(PacketUploadUserPlaylog);
if (type.GetConstructor([typeof(int), typeof(UserData), typeof(int), typeof(Action<int>), typeof(Action<PacketStatus>)]) is ConstructorInfo ctor1)
{
return (index, src, trackNo, onDone, onError) =>
{
var args = new object[] { index, src, trackNo, onDone, onError };
return (PacketUploadUserPlaylog)ctor1.Invoke(args);
};
}
else if (type.GetConstructor([typeof(int), typeof(UserData), typeof(int), typeof(string), typeof(Action<int>), typeof(Action<PacketStatus>)]) is ConstructorInfo ctor2)
{
return (index, src, trackNo, onDone, onError) =>
{
var accessToken = GetAccessToken(index);
var args = new object[] { index, src, trackNo, accessToken, onDone, onError };
return (PacketUploadUserPlaylog)ctor2.Invoke(args);
};
}
else
{
throw new MissingMethodException("No matching PacketUploadUserPlaylog constructor found");
}
});
public delegate PacketUpsertUserAll PacketUpsertUserAllCreator(int index, UserData src, Action<int> onDone, Action<PacketStatus> onError = null);
public static readonly PacketUpsertUserAllCreator CreatePacketUpsertUserAll = Iife<PacketUpsertUserAllCreator>(() =>
{
var type = typeof(PacketUpsertUserAll);
if (type.GetConstructor([typeof(int), typeof(UserData), typeof(Action<int>), typeof(Action<PacketStatus>)]) is ConstructorInfo ctor1)
{
return (index, src, onDone, onError) =>
{
var args = new object[] { index, src, onDone, onError };
return (PacketUpsertUserAll)ctor1.Invoke(args);
};
}
else if (type.GetConstructor([typeof(int), typeof(UserData), typeof(string), typeof(Action<int>), typeof(Action<PacketStatus>)]) is ConstructorInfo ctor2)
{
return (index, src, onDone, onError) =>
{
var accessToken = GetAccessToken(index);
var args = new object[] { index, src, accessToken, onDone, onError };
return (PacketUpsertUserAll)ctor2.Invoke(args);
};
}
else
{
throw new MissingMethodException("No matching PacketUpsertUserAll constructor found");
}
});
public static IEnumerable<UserScore>[] GetUserScoreList(UserData userData)
{
var tUserData = Traverse.Create(userData);
var tScoreList = tUserData.Property("ScoreList");
if (tScoreList.PropertyExists())
{
return tScoreList.GetValue<List<UserScore>[]>();
}
var tScoreDic = tUserData.Property("ScoreDic");
if (tScoreDic.PropertyExists())
{
var scoreDic = tScoreDic.GetValue<Dictionary<int, UserScore>[]>();
return scoreDic.Select(dic => dic.Values).ToArray();
}
throw new MissingFieldException("No matching UserData.ScoreList/ScoreDic found");
}
}

View File

@@ -1,32 +0,0 @@
using System.Globalization;
using System.Resources;
using HarmonyLib;
namespace AquaMai.Core.Resources;
public class I18nSingleAssemblyHook
{
[HarmonyPatch(typeof(ResourceManager), "InternalGetResourceSet", typeof(CultureInfo), typeof(bool), typeof(bool))]
[HarmonyPrefix]
public static bool GetResourceSet(CultureInfo culture, bool createIfNotExists, bool tryParents, ref ResourceSet __result, ResourceManager __instance)
{
var GetResourceFileName = __instance.GetType().GetMethod("GetResourceFileName", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
var resourceFileName = (string)GetResourceFileName.Invoke(__instance, [culture]);
var ResourcesAssembly = typeof(I18nSingleAssemblyHook).Assembly;
var manifestResourceStream = ResourcesAssembly.GetManifestResourceStream(resourceFileName);
if (manifestResourceStream == null)
{
return true;
}
var resourceGroveler = __instance.GetType().GetField("resourceGroveler", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance).GetValue(__instance);
var CreateResourceSet = resourceGroveler.GetType().GetMethod("CreateResourceSet", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
var resourceSet = CreateResourceSet.Invoke(resourceGroveler, [manifestResourceStream, ResourcesAssembly]);
var AddResourceSet = __instance.GetType().GetMethod("AddResourceSet", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);
var localResourceSets = __instance.GetType().GetField("_resourceSets", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance).GetValue(__instance);
object[] args = [localResourceSets, culture.Name, resourceSet];
AddResourceSet.Invoke(null, args);
__result = (ResourceSet)args[2];
return false;
}
}

View File

@@ -1,361 +0,0 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace AquaMai.Core.Resources {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
public class Locale {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Locale() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
public static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("AquaMai.Core.Resources.Locale", typeof(Locale).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
public static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized string similar to You are using AquaMai CI build version. This version is built from the latest mainline code and may contain undocumented configuration changes or potential issues..
/// </summary>
public static string CiBuildAlertContent {
get {
return ResourceManager.GetString("CiBuildAlertContent", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Important Notice: Test Version.
/// </summary>
public static string CiBuildAlertTitle {
get {
return ResourceManager.GetString("CiBuildAlertTitle", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Loaded!.
/// </summary>
public static string Loaded {
get {
return ResourceManager.GetString("Loaded", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Errors detected while loading!
///- Are you using a modified Assembly-CSharp.dll, which will cause inconsistent functions and cannot find the functions that need to be modified
///- Check for conflicting mods, or enabled incompatible options.
/// </summary>
public static string LoadError {
get {
return ResourceManager.GetString("LoadError", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to End.
/// </summary>
public static string MarkRepeatEnd {
get {
return ResourceManager.GetString("MarkRepeatEnd", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Start.
/// </summary>
public static string MarkRepeatStart {
get {
return ResourceManager.GetString("MarkRepeatStart", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Aime reader error.
/// </summary>
public static string NetErrIsAliveAimeReader {
get {
return ResourceManager.GetString("NetErrIsAliveAimeReader", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Aime server error.
/// </summary>
public static string NetErrIsAliveAimeServer {
get {
return ResourceManager.GetString("NetErrIsAliveAimeServer", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Server communication error.
/// </summary>
public static string NetErrIsAliveServer {
get {
return ResourceManager.GetString("NetErrIsAliveServer", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Data download not success.
/// </summary>
public static string NetErrWasDownloadSuccessOnce {
get {
return ResourceManager.GetString("NetErrWasDownloadSuccessOnce", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Pause.
/// </summary>
public static string Pause {
get {
return ResourceManager.GetString("Pause", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Play Count{0}.
/// </summary>
public static string PlayCount {
get {
return ResourceManager.GetString("PlayCount", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Playlog save error.
/// </summary>
public static string PlaylogSaveError {
get {
return ResourceManager.GetString("PlaylogSaveError", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to SSS+ =&gt; DXRating += {0}.
/// </summary>
public static string RatingUpWhenSSSp {
get {
return ResourceManager.GetString("RatingUpWhenSSSp", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Repeat end time cannot be less than repeat start time.
/// </summary>
public static string RepeatEndTimeLessThenStartTime {
get {
return ResourceManager.GetString("RepeatEndTimeLessThenStartTime", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Loop Not Set.
/// </summary>
public static string RepeatNotSet {
get {
return ResourceManager.GetString("RepeatNotSet", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Reset.
/// </summary>
public static string RepeatReset {
get {
return ResourceManager.GetString("RepeatReset", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Loop Set.
/// </summary>
public static string RepeatStartEndSet {
get {
return ResourceManager.GetString("RepeatStartEndSet", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Loop Start Set.
/// </summary>
public static string RepeatStartSet {
get {
return ResourceManager.GetString("RepeatStartSet", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Please set repeat start time first.
/// </summary>
public static string RepeatStartTimeNotSet {
get {
return ResourceManager.GetString("RepeatStartTimeNotSet", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Saving... Do not exit the game.
/// </summary>
public static string SavingDontExit {
get {
return ResourceManager.GetString("SavingDontExit", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Seek &lt;&lt;.
/// </summary>
public static string SeekBackward {
get {
return ResourceManager.GetString("SeekBackward", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Seek &gt;&gt;.
/// </summary>
public static string SeekForward {
get {
return ResourceManager.GetString("SeekForward", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Hide Self-Made Charts.
/// </summary>
public static string SelfMadeChartsHide {
get {
return ResourceManager.GetString("SelfMadeChartsHide", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Show Self-Made Charts.
/// </summary>
public static string SelfMadeChartsShow {
get {
return ResourceManager.GetString("SelfMadeChartsShow", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Skip.
/// </summary>
public static string Skip {
get {
return ResourceManager.GetString("Skip", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to &gt; Skipping incompatible patch: {0}.
/// </summary>
public static string SkipIncompatiblePatch {
get {
return ResourceManager.GetString("SkipIncompatiblePatch", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Speed.
/// </summary>
public static string Speed {
get {
return ResourceManager.GetString("Speed", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Speed -.
/// </summary>
public static string SpeedDown {
get {
return ResourceManager.GetString("SpeedDown", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Speed Reset.
/// </summary>
public static string SpeedReset {
get {
return ResourceManager.GetString("SpeedReset", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Speed +.
/// </summary>
public static string SpeedUp {
get {
return ResourceManager.GetString("SpeedUp", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Touch panel reset.
/// </summary>
public static string TouchPanelReset {
get {
return ResourceManager.GetString("TouchPanelReset", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to UserAll Upsert Error.
/// </summary>
public static string UserAllUpsertError {
get {
return ResourceManager.GetString("UserAllUpsertError", resourceCulture);
}
}
}
}

View File

@@ -1,122 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>1.3</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="SeekBackward" xml:space="preserve">
<value>Seek &lt;&lt;</value>
</data>
<data name="SeekForward" xml:space="preserve">
<value>Seek &gt;&gt;</value>
</data>
<data name="Pause" xml:space="preserve">
<value>Pause</value>
</data>
<data name="MarkRepeatStart" xml:space="preserve">
<value>Start</value>
</data>
<data name="MarkRepeatEnd" xml:space="preserve">
<value>End</value>
</data>
<data name="RepeatReset" xml:space="preserve">
<value>Reset</value>
</data>
<data name="RepeatNotSet" xml:space="preserve">
<value>Loop Not Set</value>
</data>
<data name="RepeatStartSet" xml:space="preserve">
<value>Loop Start Set</value>
</data>
<data name="RepeatStartEndSet" xml:space="preserve">
<value>Loop Set</value>
</data>
<data name="SpeedDown" xml:space="preserve">
<value>Speed -</value>
</data>
<data name="SpeedUp" xml:space="preserve">
<value>Speed +</value>
</data>
<data name="Speed" xml:space="preserve">
<value>Speed</value>
</data>
<data name="SpeedReset" xml:space="preserve">
<value>Speed Reset</value>
</data>
<data name="LoadError" xml:space="preserve">
<value>Errors detected while loading!
- Are you using a modified Assembly-CSharp.dll, which will cause inconsistent functions and cannot find the functions that need to be modified
- Check for conflicting mods, or enabled incompatible options</value>
</data>
<data name="SavingDontExit" xml:space="preserve">
<value>Saving... Do not exit the game</value>
</data>
<data name="Loaded" xml:space="preserve">
<value>Loaded!</value>
</data>
<data name="NetErrIsAliveServer" xml:space="preserve">
<value>Server communication error</value>
</data>
<data name="NetErrIsAliveAimeReader" xml:space="preserve">
<value>Aime reader error</value>
</data>
<data name="NetErrIsAliveAimeServer" xml:space="preserve">
<value>Aime server error</value>
</data>
<data name="NetErrWasDownloadSuccessOnce" xml:space="preserve">
<value>Data download not success</value>
</data>
<data name="RatingUpWhenSSSp" xml:space="preserve">
<value>SSS+ =&gt; DXRating += {0}</value>
</data>
<data name="Skip" xml:space="preserve">
<value>Skip</value>
</data>
<data name="SkipIncompatiblePatch" xml:space="preserve">
<value>&gt; Skipping incompatible patch: {0}</value>
</data>
<data name="RepeatStartTimeNotSet" xml:space="preserve">
<value>Please set repeat start time first</value>
</data>
<data name="RepeatEndTimeLessThenStartTime" xml:space="preserve">
<value>Repeat end time cannot be less than repeat start time</value>
</data>
<data name="CiBuildAlertTitle" xml:space="preserve">
<value>Important Notice: Test Version</value>
</data>
<data name="CiBuildAlertContent" xml:space="preserve">
<value>You are using AquaMai CI build version. This version is built from the latest mainline code and may contain undocumented configuration changes or potential issues.</value>
</data>
<data name="PlayCount" xml:space="preserve">
<value>Play Count{0}</value>
</data>
<data name="SelfMadeChartsHide" xml:space="preserve">
<value>Hide Self-Made Charts</value>
</data>
<data name="SelfMadeChartsShow" xml:space="preserve">
<value>Show Self-Made Charts</value>
</data>
<data name="UserAllUpsertError" xml:space="preserve">
<value>UserAll Upsert Error</value>
</data>
<data name="PlaylogSaveError" xml:space="preserve">
<value>Playlog save error</value>
</data>
<data name="TouchPanelReset" xml:space="preserve">
<value>Touch panel reset</value>
</data>
</root>

View File

@@ -1,115 +0,0 @@
<root>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>1.3</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="SeekBackward" xml:space="preserve">
<value>倒退 &lt;&lt;</value>
</data>
<data name="SeekForward" xml:space="preserve">
<value>快进 &gt;&gt;</value>
</data>
<data name="Pause" xml:space="preserve">
<value>暂停</value>
</data>
<data name="MarkRepeatEnd" xml:space="preserve">
<value>标记结尾</value>
</data>
<data name="MarkRepeatStart" xml:space="preserve">
<value>标记开头</value>
</data>
<data name="RepeatNotSet" xml:space="preserve">
<value>循环未设定</value>
</data>
<data name="RepeatReset" xml:space="preserve">
<value>循环解除</value>
</data>
<data name="RepeatStartEndSet" xml:space="preserve">
<value>循环已设定</value>
</data>
<data name="RepeatStartSet" xml:space="preserve">
<value>循环开头已设定</value>
</data>
<data name="Speed" xml:space="preserve">
<value>速度</value>
</data>
<data name="SpeedDown" xml:space="preserve">
<value>速度 -</value>
</data>
<data name="SpeedReset" xml:space="preserve">
<value>速度重置</value>
</data>
<data name="SpeedUp" xml:space="preserve">
<value>速度 +</value>
</data>
<data name="LoadError" xml:space="preserve">
<value>加载过程中检测到错误!
- 你是否正在使用魔改的 Assembly-CSharp.dll这会导致函数不一致而无法找到需要修改的函数
- 请检查是否有冲突的 Mod或者开启了不兼容的选项</value>
</data>
<data name="SavingDontExit" xml:space="preserve">
<value>正在保存… 请不要关闭游戏</value>
</data>
<data name="Loaded" xml:space="preserve">
<value>加载完成!</value>
</data>
<data name="NetErrIsAliveServer" xml:space="preserve">
<value>主服务器通信错误</value>
</data>
<data name="NetErrIsAliveAimeReader" xml:space="preserve">
<value>Aime 读卡器错误</value>
</data>
<data name="NetErrIsAliveAimeServer" xml:space="preserve">
<value>AimeDB 通信错误</value>
</data>
<data name="NetErrWasDownloadSuccessOnce" xml:space="preserve">
<value>数据下载不成功</value>
</data>
<data name="RatingUpWhenSSSp" xml:space="preserve">
<value>推到鸟加可上 {0} 分</value>
</data>
<data name="Skip" xml:space="preserve">
<value>跳过</value>
</data>
<data name="SkipIncompatiblePatch" xml:space="preserve">
<value>&gt; 已跳过加载不兼容的功能: {0}</value>
</data>
<data name="RepeatEndTimeLessThenStartTime" xml:space="preserve">
<value>循环结束时间不能早于开始时间</value>
</data>
<data name="RepeatStartTimeNotSet" xml:space="preserve">
<value>请先设置循环开始时间</value>
</data>
<data name="CiBuildAlertTitle" xml:space="preserve">
<value>重要提示:测试版本</value>
</data>
<data name="CiBuildAlertContent" xml:space="preserve">
<value>您正在使用的是 AquaMai CI 构建版本。由于该版本基于最新的主线代码构建,可能包含未通知的配置文件变更或潜在问题。</value>
</data>
<data name="PlayCount" xml:space="preserve">
<value>游玩次数: {0}</value>
</data>
<data name="SelfMadeChartsHide" xml:space="preserve">
<value>已隐藏所有自制谱</value>
</data>
<data name="SelfMadeChartsShow" xml:space="preserve">
<value>已显示自制谱</value>
</data>
<data name="UserAllUpsertError" xml:space="preserve">
<value>保存 UserAll 失败</value>
</data>
<data name="PlaylogSaveError" xml:space="preserve">
<value>保存 Playlog 失败</value>
</data>
<data name="TouchPanelReset" xml:space="preserve">
<value>触摸面板已重置</value>
</data>
</root>

View File

@@ -1,199 +0,0 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using AquaMai.Core.Attributes;
using AquaMai.Core.Helpers;
using AquaMai.Core.Resources;
using MelonLoader;
using UnityEngine;
namespace AquaMai.Core;
public class Startup
{
private static HarmonyLib.Harmony _harmony;
private static bool _hasErrors;
private enum ModLifecycleMethod
{
// Invoked before all patches are applied, including core patches
OnBeforeAllPatch,
// Invoked after all patches are applied
OnAfterAllPatch,
// Invoked before the current patch is applied
OnBeforePatch,
// Invoked after the current patch is applied
// Subclasses are treated as separate patches
OnAfterPatch,
// Invoked when an error occurs applying the current patch
// Lifecycle methods' excpetions not included
// Subclasses' error not included
OnPatchError
}
private static bool ShouldEnableImplicitly(Type type)
{
var implicitEnableAttribute = type.GetCustomAttribute<EnableImplicitlyIf>();
if (implicitEnableAttribute == null) return false;
var referenceField = type.GetField(implicitEnableAttribute.MemberName, BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
var referenceProperty = type.GetProperty(implicitEnableAttribute.MemberName, BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
if (referenceField == null && referenceProperty == null)
{
throw new ArgumentException($"Field or property {implicitEnableAttribute.MemberName} not found in {type.FullName}");
}
var referenceMemberValue = referenceField != null ? referenceField.GetValue(null) : referenceProperty.GetValue(null);
if ((bool)referenceMemberValue)
{
MelonLogger.Msg($"Enabled {type.FullName} implicitly");
return true;
}
return false;
}
private static void InvokeLifecycleMethod(Type type, ModLifecycleMethod methodName)
{
var method = type.GetMethod(methodName.ToString(), BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static);
if (method == null)
{
return;
}
var parameters = method.GetParameters();
var arguments = parameters.Select(p =>
{
if (p.ParameterType == typeof(HarmonyLib.Harmony)) return _harmony;
throw new InvalidOperationException($"Unsupported parameter type {p.ParameterType} in lifecycle method {type.FullName}.{methodName}");
}).ToArray();
try
{
method.Invoke(null, arguments);
}
catch (TargetInvocationException e)
{
MelonLogger.Error($"Failed to invoke lifecycle method {type.FullName}.{methodName}: {e.InnerException}");
_hasErrors = true;
}
}
private static void CollectWantedPatches(List<Type> wantedPatches, Type type)
{
if (EnableConditionHelper.ShouldSkipClass(type))
{
return;
}
wantedPatches.Add(type);
foreach (var nested in type.GetNestedTypes())
{
CollectWantedPatches(wantedPatches, nested);
}
}
private static void ApplyPatch(Type type)
{
MelonLogger.Msg($"> Applying {type}");
try
{
InvokeLifecycleMethod(type, ModLifecycleMethod.OnBeforePatch);
_harmony.PatchAll(type);
InvokeLifecycleMethod(type, ModLifecycleMethod.OnAfterPatch);
}
catch (Exception e)
{
MelonLogger.Error($"Failed to patch {type}: {e}");
InvokeLifecycleMethod(type, ModLifecycleMethod.OnPatchError);
_hasErrors = true;
}
}
private static string ResolveLocale()
{
var localeConfigEntry = ConfigLoader.Config.ReflectionManager.GetEntry("General.Locale");
var localeValue = (string)ConfigLoader.Config.GetEntryState(localeConfigEntry).Value;
return localeValue switch
{
"en" => localeValue,
"zh" => localeValue,
_ => Application.systemLanguage switch
{
SystemLanguage.Chinese or SystemLanguage.ChineseSimplified or SystemLanguage.ChineseTraditional => "zh",
SystemLanguage.English => "en",
_ => "en"
}
};
}
public static void Initialize(Assembly modsAssembly, HarmonyLib.Harmony harmony)
{
MelonLogger.Msg("Loading mod settings...");
var configLoaded = ConfigLoader.LoadConfig(modsAssembly);
var lang = ResolveLocale();
if (configLoaded)
{
ConfigLoader.SaveConfig(lang); // Re-save the config as soon as possible
}
_harmony = harmony;
// Init locale with patching C# runtime
// https://stackoverflow.com/questions/1952638/single-assembly-multi-language-windows-forms-deployment-ilmerge-and-satellite-a
ApplyPatch(typeof(I18nSingleAssemblyHook));
Locale.Culture = CultureInfo.GetCultureInfo(lang); // Must be called after I18nSingleAssemblyHook patched
// The patch list is ordered
List<Type> wantedPatches = [];
// Must be patched first to support [EnableIf(...)] and [EnableGameVersion(...)]
CollectWantedPatches(wantedPatches, typeof(EnableConditionHelper));
// Core helpers patched first
CollectWantedPatches(wantedPatches, typeof(MessageHelper));
CollectWantedPatches(wantedPatches, typeof(MusicDirHelper));
CollectWantedPatches(wantedPatches, typeof(SharedInstances));
CollectWantedPatches(wantedPatches, typeof(GuiSizes));
CollectWantedPatches(wantedPatches, typeof(KeyListener));
// Collect patches based on the config
var config = ConfigLoader.Config;
foreach (var section in config.ReflectionManager.Sections)
{
var reflectionType = (Config.Reflection.SystemReflectionProvider.ReflectionType)section.Type;
var type = reflectionType.UnderlyingType;
if (!config.GetSectionState(section).Enabled && !ShouldEnableImplicitly(type)) continue;
CollectWantedPatches(wantedPatches, type);
}
foreach (var type in wantedPatches)
{
InvokeLifecycleMethod(type, ModLifecycleMethod.OnBeforeAllPatch);
}
foreach (var type in wantedPatches)
{
ApplyPatch(type);
}
foreach (var type in wantedPatches)
{
InvokeLifecycleMethod(type, ModLifecycleMethod.OnAfterAllPatch);
}
if (_hasErrors)
{
MelonLogger.Warning("========================================================================!!!\n" + Locale.LoadError);
MelonLogger.Warning("===========================================================================");
}
# if CI
MelonLogger.Warning(Locale.CiBuildAlertTitle);
MelonLogger.Warning(Locale.CiBuildAlertContent);
# endif
MelonLogger.Msg(Locale.Loaded);
}
public static void OnGUI()
{
GuiSizes.SetupStyles();
}
}

View File

@@ -1,128 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Release</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{8731C0E0-53BE-4B1B-9828-193E738C6865}</ProjectGuid>
<OutputType>Library</OutputType>
<RootNamespace>AquaMai.Mods</RootNamespace>
<AssemblyName>AquaMai.Mods</AssemblyName>
<TargetFramework>net472</TargetFramework>
<FileAlignment>512</FileAlignment>
<Deterministic>true</Deterministic>
<LangVersion>12</LangVersion>
<NoWarn>414</NoWarn>
<AssemblySearchPaths>$(ProjectDir)../Libs/;$(AssemblySearchPaths)</AssemblySearchPaths>
<OutputPath>$(ProjectDir)../Output/</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<EnableDefaultEmbeddedResourceItems>false</EnableDefaultEmbeddedResourceItems>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugSymbols>false</DebugSymbols>
<DebugType>None</DebugType>
<Optimize>true</Optimize>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DefineConstants>DEBUG</DefineConstants>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../AquaMai.Config/AquaMai.Config.csproj" />
<ProjectReference Include="../AquaMai.Core/AquaMai.Core.csproj" />
</ItemGroup>
<ItemGroup>
<Reference Include="mscorlib" />
<Reference Include="0Harmony" />
<Reference Include="AMDaemon.NET" />
<Reference Include="Assembly-CSharp" />
<Reference Include="Assembly-CSharp-firstpass" />
<Reference Include="MelonLoader" />
<Reference Include="Mono.Cecil" />
<Reference Include="Mono.Posix" />
<Reference Include="Mono.Security" />
<Reference Include="System" />
<Reference Include="System.Configuration" />
<Reference Include="System.Core" />
<Reference Include="System.Runtime" />
<Reference Include="System.Security" />
<Reference Include="System.Xml" />
<Reference Include="Unity.Analytics.DataPrivacy" />
<Reference Include="Unity.TextMeshPro" />
<Reference Include="UnityEngine" />
<Reference Include="UnityEngine.AccessibilityModule" />
<Reference Include="UnityEngine.AIModule" />
<Reference Include="UnityEngine.AnimationModule" />
<Reference Include="UnityEngine.ARModule" />
<Reference Include="UnityEngine.AssetBundleModule" />
<Reference Include="UnityEngine.AudioModule" />
<Reference Include="UnityEngine.BaselibModule" />
<Reference Include="UnityEngine.ClothModule" />
<Reference Include="UnityEngine.ClusterInputModule" />
<Reference Include="UnityEngine.ClusterRendererModule" />
<Reference Include="UnityEngine.CoreModule" />
<Reference Include="UnityEngine.CrashReportingModule" />
<Reference Include="UnityEngine.DirectorModule" />
<Reference Include="UnityEngine.FileSystemHttpModule" />
<Reference Include="UnityEngine.GameCenterModule" />
<Reference Include="UnityEngine.GridModule" />
<Reference Include="UnityEngine.HotReloadModule" />
<Reference Include="UnityEngine.ImageConversionModule" />
<Reference Include="UnityEngine.IMGUIModule" />
<Reference Include="UnityEngine.InputModule" />
<Reference Include="UnityEngine.JSONSerializeModule" />
<Reference Include="UnityEngine.LocalizationModule" />
<Reference Include="UnityEngine.Networking" />
<Reference Include="UnityEngine.ParticleSystemModule" />
<Reference Include="UnityEngine.PerformanceReportingModule" />
<Reference Include="UnityEngine.Physics2DModule" />
<Reference Include="UnityEngine.PhysicsModule" />
<Reference Include="UnityEngine.ProfilerModule" />
<Reference Include="UnityEngine.ScreenCaptureModule" />
<Reference Include="UnityEngine.SharedInternalsModule" />
<Reference Include="UnityEngine.SpatialTracking" />
<Reference Include="UnityEngine.SpriteMaskModule" />
<Reference Include="UnityEngine.SpriteShapeModule" />
<Reference Include="UnityEngine.StreamingModule" />
<Reference Include="UnityEngine.StyleSheetsModule" />
<Reference Include="UnityEngine.SubstanceModule" />
<Reference Include="UnityEngine.TerrainModule" />
<Reference Include="UnityEngine.TerrainPhysicsModule" />
<Reference Include="UnityEngine.TextCoreModule" />
<Reference Include="UnityEngine.TextRenderingModule" />
<Reference Include="UnityEngine.TilemapModule" />
<Reference Include="UnityEngine.Timeline" />
<Reference Include="UnityEngine.TimelineModule" />
<Reference Include="UnityEngine.TLSModule" />
<Reference Include="UnityEngine.UI" />
<Reference Include="UnityEngine.UIElementsModule" />
<Reference Include="UnityEngine.UIModule" />
<Reference Include="UnityEngine.UmbraModule" />
<Reference Include="UnityEngine.UNETModule" />
<Reference Include="UnityEngine.UnityAnalyticsModule" />
<Reference Include="UnityEngine.UnityConnectModule" />
<Reference Include="UnityEngine.UnityTestProtocolModule" />
<Reference Include="UnityEngine.UnityWebRequestAssetBundleModule" />
<Reference Include="UnityEngine.UnityWebRequestAudioModule" />
<Reference Include="UnityEngine.UnityWebRequestModule" />
<Reference Include="UnityEngine.UnityWebRequestTextureModule" />
<Reference Include="UnityEngine.UnityWebRequestWWWModule" />
<Reference Include="UnityEngine.VehiclesModule" />
<Reference Include="UnityEngine.VFXModule" />
<Reference Include="UnityEngine.VideoModule" />
<Reference Include="UnityEngine.VRModule" />
<Reference Include="UnityEngine.WindModule" />
<Reference Include="UnityEngine.XRModule" />
</ItemGroup>
<ItemGroup>
<Reference Include="System.Numerics" Private="true" />
</ItemGroup>
</Project>

View File

@@ -1,29 +0,0 @@
using AquaMai.Config.Attributes;
namespace AquaMai.Mods;
[ConfigSection(
en: """
These options have been deprecated and no longer work in the current version.
Remove them to get rid of the warning message at startup.
""",
zh: """
这些配置项已经被废弃,在当前版本不再生效
删除它们以去除启动时的警告信息
""",
exampleHidden: true)]
public class DeprecationWarning
{
[ConfigEntry(hideWhenDefault: true)]
public static readonly bool v1_0_ModKeyMap_TestMode;
// Print friendly warning messages here.
// Please keep them up-to-date while refactoring the config.
public static void OnBeforeAllPatch()
{
if (v1_0_ModKeyMap_TestMode)
{
MelonLoader.MelonLogger.Warning("ModKeyMap.TestMode has been deprecated (> v1.0). Please use GameSystem.KeyMap.Test instead.");
}
}
}

View File

@@ -1,73 +0,0 @@
using System.Collections.Generic;
using System.IO;
using AquaMai.Config.Attributes;
using AquaMai.Core.Helpers;
using HarmonyLib;
using Monitor;
using Process;
using UnityEngine;
using UnityEngine.UI;
namespace AquaMai.Mods.Fancy;
[ConfigSection(
en: "Replace the \"SEGA\" and \"ALL.Net\" logos with custom ones.",
zh: "用自定义的图片替换「SEGA」和「ALL.Net」的标志")]
public class CustomLogo
{
[ConfigEntry(
en: "Replace the \"SEGA\" logo with a random PNG image from this directory.",
zh: "从此目录中随机选择一张 PNG 图片用于「SEGA」标志")]
private static readonly string segaLogoDir = "LocalAssets/SegaLogo";
[ConfigEntry(
en: "Replace the \"ALL.Net\" logo with a random PNG image from this directory.",
zh: "从此目录中随机选择一张 PNG 图片用于「ALL.Net」标志")]
private static readonly string allNetLogoDir = "LocalAssets/AllNetLogo";
private readonly static List<Sprite> segaLogo = [];
private readonly static List<Sprite> allNetLogo = [];
public static void OnBeforePatch()
{
EnumSprite(segaLogo, FileSystem.ResolvePath(segaLogoDir));
EnumSprite(allNetLogo, FileSystem.ResolvePath(allNetLogoDir));
}
private static void EnumSprite(List<Sprite> collection, string path)
{
if (!Directory.Exists(path)) return;
foreach (var file in Directory.EnumerateFiles(path, "*.png"))
{
var data = File.ReadAllBytes(file);
var texture2D = new Texture2D(1, 1, TextureFormat.RGBA32, false);
if (texture2D.LoadImage(data))
{
collection.Add(Sprite.Create(texture2D, new Rect(0f, 0f, texture2D.width, texture2D.height), new Vector2(0.5f, 0.5f)));
}
}
}
[HarmonyPatch(typeof(AdvertiseProcess), "OnStart")]
[HarmonyPostfix]
private static void AdvProcessPostFix(AdvertiseMonitor[] ____monitors)
{
if (segaLogo.Count > 0)
{
var logo = segaLogo[UnityEngine.Random.Range(0, segaLogo.Count)];
foreach (var monitor in ____monitors)
{
monitor.transform.Find("Canvas/Main/SegaAllNet_LOGO/NUL_ADT_SegaAllNet_LOGO/SegaLogo").GetComponent<Image>().sprite = logo;
}
}
if (allNetLogo.Count > 0)
{
var logo = allNetLogo[UnityEngine.Random.Range(0, allNetLogo.Count)];
foreach (var monitor in ____monitors)
{
monitor.transform.Find("Canvas/Main/SegaAllNet_LOGO/NUL_ADT_SegaAllNet_LOGO/AllNetLogo").GetComponent<Image>().sprite = logo;
}
}
}
}

View File

@@ -1,45 +0,0 @@
using AquaMai.Config.Attributes;
using HarmonyLib;
using Manager;
namespace AquaMai.Mods.Fancy;
[ConfigSection(
en: """
Custom shop name in photo.
Also enable shop name display in SDGA.
""",
zh: """
自定义拍照的店铺名称
同时在 SDGA 中会启用店铺名称的显示(但是不会在游戏里有设置)
""")]
public class CustomPlaceName
{
[ConfigEntry]
private static readonly string placeName = "";
[HarmonyPostfix]
[HarmonyPatch(typeof(OperationManager), "CheckAuth_Proc")]
public static void CheckAuth_Proc(OperationManager __instance)
{
if (string.IsNullOrEmpty(placeName))
{
return;
}
__instance.ShopData.ShopName = placeName;
__instance.ShopData.ShopNickName = placeName;
}
[HarmonyPostfix]
[HarmonyPatch(typeof(ResultCardBaseController), "Initialize")]
public static void Initialize(ResultCardBaseController __instance)
{
if (string.IsNullOrEmpty(placeName))
{
return;
}
__instance.SetVisibleStoreName(true);
}
}

View File

@@ -1,402 +0,0 @@
using System.Collections.Generic;
using System.IO;
using AquaMai.Config.Attributes;
using AquaMai.Core.Helpers;
using HarmonyLib;
using MelonLoader;
using Monitor;
using Monitor.Game;
using Process;
using UnityEngine;
namespace AquaMai.Mods.Fancy;
[ConfigSection(
en: """
Provide the ability to use custom skins (advanced feature).
Load skin textures from custom paths.
""",
zh: """
提供自定义皮肤的能力(高级功能)
从自定义路径中加载皮肤贴图
""")]
public class CustomSkins
{
[ConfigEntry]
private static readonly string skinsDir = "LocalAssets/Skins";
private static readonly List<string> ImageExts = [".png", ".jpg", ".jpeg"];
private static readonly List<string> SlideFanFields = ["_normalSlideFan", "_eachSlideFan", "_breakSlideFan", "_breakSlideFanEff"];
private static readonly List<string> CustomTrackStartFields = ["_musicBase", "_musicTab", "_musicLvBase", "_musicLvText"];
private static Sprite customOutline;
private readonly static Sprite[,] customSlideFan = new Sprite[4, 11];
public static readonly Sprite[,] CustomJudge = new Sprite[2, ((int)NoteJudge.ETiming.End + 1)];
public static readonly Sprite[,,,] CustomJudgeSlide = new Sprite[2, 3, 2, ((int)NoteJudge.ETiming.End + 1)];
public static readonly Texture2D[] CustomTrackStart = new Texture2D[4];
private static bool LoadIntoGameNoteImageContainer(string fieldName, int? idx1, int? idx2, Texture2D texture)
{
// 先确定确实有这个 Field, 如果没有的话可以直接跳过这个文件
var fieldTraverse = Traverse.Create(typeof(GameNoteImageContainer)).Field(fieldName);
if (!fieldTraverse.FieldExists())
{
MelonLogger.Msg($"[CustomNoteSkin] Cannot found field {fieldName}");
return false;
}
var fieldType = fieldTraverse.GetValueType();
if (!idx1.HasValue)
{
// 目标 Field 应当是单个 Sprite
if (fieldType != typeof(Sprite))
{
MelonLogger.Msg($"[CustomNoteSkin] Field {fieldName} is a {fieldType.Name}, not a Sprite");
return false;
}
var target = fieldTraverse.GetValue<Sprite>();
var pivot = new Vector2(target.pivot.x / target.rect.width, target.pivot.y / target.rect.height);
var custom = Sprite.Create(
texture, new Rect(0, 0, texture.width, texture.height), pivot, 1f,
0, SpriteMeshType.Tight, target.border
);
fieldTraverse.SetValue(custom);
}
else if (!idx2.HasValue)
{
// 目标 Field 是一维数组
if (fieldType != typeof(Sprite[]))
{
MelonLogger.Msg($"[CustomNoteSkin] Field {fieldName} is a {fieldType.Name}, not a Sprite[]");
return false;
}
var targetArray = fieldTraverse.GetValue<Sprite[]>();
var target = targetArray[idx1.Value];
var pivot = new Vector2(target.pivot.x / target.rect.width, target.pivot.y / target.rect.height);
var custom = Sprite.Create(
texture, new Rect(0, 0, texture.width, texture.height), pivot, 1f,
0, SpriteMeshType.Tight, target.border
);
targetArray[idx1.Value] = custom;
}
else
{
// 目标 Field 是二维数组
if (fieldType != typeof(Sprite[,]))
{
MelonLogger.Msg($"[CustomNoteSkin] Field {fieldName} is a {fieldType.Name}, not a Sprite[,]");
return false;
}
var targetArray = fieldTraverse.GetValue<Sprite[,]>();
var target = targetArray[idx1.Value, idx2.Value];
var pivot = new Vector2(target.pivot.x / target.rect.width, target.pivot.y / target.rect.height);
var custom = Sprite.Create(
texture, new Rect(0, 0, texture.width, texture.height), pivot, 1f,
0, SpriteMeshType.Tight, target.border
);
targetArray[idx1.Value, idx2.Value] = custom;
}
return true;
}
[HarmonyPostfix]
[HarmonyPatch(typeof(GameNotePrefabContainer), "Initialize")]
private static void LoadNoteSkin()
{
var resolvedDir = FileSystem.ResolvePath(skinsDir);
if (!Directory.Exists(resolvedDir)) return;
foreach (var laFile in Directory.EnumerateFiles(resolvedDir))
{
if (!ImageExts.Contains(Path.GetExtension(laFile).ToLowerInvariant())) continue;
var texture = new Texture2D(1, 1, TextureFormat.RGBA32, false);
texture.LoadImage(File.ReadAllBytes(laFile));
var name = Path.GetFileNameWithoutExtension(laFile);
var args = name.Split('_');
// 文件名的格式是 XXXXXXXX_A_B 表示 GameNoteImageContainer._XXXXXXXX[A, B]
// 视具体情况, A, B 可能不存在
var fieldName = '_' + args[0];
int? idx1 = (args.Length < 2) ? null : (int.TryParse(args[1], out var temp) ? temp : null);
int? idx2 = (args.Length < 3) ? null : (int.TryParse(args[2], out temp) ? temp : null);
int? idx3 = (args.Length < 4) ? null : (int.TryParse(args[3], out temp) ? temp : null);
Traverse traverse;
if (CustomTrackStartFields.Contains(fieldName))
{
var i = CustomTrackStartFields.IndexOf(fieldName);
CustomTrackStart[i] = texture;
MelonLogger.Msg($"[CustomNoteSkin] Successfully loaded {name}");
continue;
}
if (fieldName == "_outline")
{
customOutline = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), new Vector2(0.5f, 0.5f), 1f);
MelonLogger.Msg($"[CustomNoteSkin] Successfully loaded {name}");
continue;
}
if (fieldName == "_judgeNormal" || fieldName == "_judgeBreak")
{
if (!idx1.HasValue)
{
MelonLogger.Msg($"[CustomNoteSkin] Field {fieldName} needs a index");
continue;
}
var i = (fieldName == "_judgeBreak") ? 1 : 0;
CustomJudge[i, idx1.Value] = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), new Vector2(0.5f, 0.5f), 1f);
MelonLogger.Msg($"[CustomNoteSkin] Successfully loaded {name}");
continue;
}
if (fieldName == "_judgeSlideNormal" || fieldName == "_judgeSlideBreak")
{
if (!idx1.HasValue || !idx2.HasValue || !idx3.HasValue)
{
MelonLogger.Msg($"[CustomNoteSkin] Field {fieldName} needs 3 indices");
continue;
}
var i = (fieldName == "_judgeSlideBreak") ? 1 : 0;
Vector2 pivot;
switch (idx1.Value)
{
case 0 when idx2.Value == 0:
pivot = new Vector2(0f, 0.5f);
break;
case 0 when idx2.Value == 1:
pivot = new Vector2(1f, 0.5f);
break;
case 1 when idx2.Value == 0:
pivot = new Vector2(0f, 0.3f);
break;
case 1 when idx2.Value == 1:
pivot = new Vector2(1f, 0.3f);
break;
case 2 when idx2.Value == 0:
pivot = new Vector2(0.5f, 0.8f);
break;
case 2 when idx2.Value == 1:
pivot = new Vector2(0.5f, 0.2f);
break;
default:
pivot = new Vector2(0.5f, 0.5f);
break;
}
CustomJudgeSlide[i, idx1.Value, idx2.Value, idx3.Value] = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), pivot, 1f);
MelonLogger.Msg($"[CustomNoteSkin] Successfully loaded {name}");
continue;
}
if (SlideFanFields.Contains(fieldName))
{
if (!idx1.HasValue)
{
MelonLogger.Msg($"[CustomNoteSkin] Field {fieldName} needs a index");
continue;
}
var i = SlideFanFields.IndexOf(fieldName);
customSlideFan[i, idx1.Value] = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), new Vector2(1f, 0.5f), 1f);
MelonLogger.Msg($"[CustomNoteSkin] Successfully loaded {name}");
continue;
}
if (fieldName == "_touchJust")
{
traverse = Traverse.Create(GameNotePrefabContainer.TouchTapB);
var noticeObject = traverse.Field<GameObject>("NoticeObject").Value;
var target = noticeObject.GetComponent<SpriteRenderer>();
var pivot = new Vector2(
target.sprite.pivot.x / target.sprite.rect.width,
target.sprite.pivot.y / target.sprite.rect.height
);
var custom = Sprite.Create(
texture, new Rect(0, 0, texture.width, texture.height), pivot, 1f,
0, SpriteMeshType.Tight, target.sprite.border
);
target.sprite = custom;
traverse = Traverse.Create(GameNotePrefabContainer.TouchTapC);
noticeObject = traverse.Field<GameObject>("NoticeObject").Value;
noticeObject.GetComponent<SpriteRenderer>().sprite = custom;
MelonLogger.Msg($"[CustomNoteSkin] Successfully loaded {name}");
continue;
}
if (fieldName == "_touchHold")
{
if (!idx1.HasValue)
{
MelonLogger.Msg($"[CustomNoteSkin] Field {fieldName} needs a index");
continue;
}
traverse = Traverse.Create(GameNotePrefabContainer.TouchHoldC);
var target = traverse.Field<SpriteRenderer[]>("ColorsObject").Value;
var renderer = target[idx1.Value];
var pivot = new Vector2(
renderer.sprite.pivot.x / renderer.sprite.rect.width,
renderer.sprite.pivot.y / renderer.sprite.rect.height
);
var custom = Sprite.Create(
texture, new Rect(0, 0, texture.width, texture.height), pivot, 1f,
0, SpriteMeshType.Tight, renderer.sprite.border
);
renderer.sprite = custom;
MelonLogger.Msg($"[CustomNoteSkin] Successfully loaded {name}");
continue;
}
if (fieldName == "_normalTouchBorder")
{
if (!idx1.HasValue)
{
MelonLogger.Msg($"[CustomNoteSkin] Field {fieldName} needs a index");
continue;
}
traverse = Traverse.Create(GameNotePrefabContainer.TouchReserve);
var target = traverse.Field<Sprite[]>("_reserveSingleSprite").Value;
var targetSprite = target[idx1.Value - 2];
var pivot = new Vector2(
targetSprite.pivot.x / targetSprite.rect.width,
targetSprite.pivot.y / targetSprite.rect.height
);
target[idx1.Value - 2] = Sprite.Create(
texture, new Rect(0, 0, texture.width, texture.height), pivot, 1f,
0, SpriteMeshType.Tight, targetSprite.border
);
MelonLogger.Msg($"[CustomNoteSkin] Successfully loaded {name}");
continue;
}
if (fieldName == "_eachTouchBorder")
{
if (!idx1.HasValue)
{
MelonLogger.Msg($"[CustomNoteSkin] Field {fieldName} needs a index");
continue;
}
traverse = Traverse.Create(GameNotePrefabContainer.TouchReserve);
var target = traverse.Field<Sprite[]>("_reserveEachSprite").Value;
var targetSprite = target[idx1.Value - 2];
var pivot = new Vector2(
targetSprite.pivot.x / targetSprite.rect.width,
targetSprite.pivot.y / targetSprite.rect.height
);
target[idx1.Value - 2] = Sprite.Create(
texture, new Rect(0, 0, texture.width, texture.height), pivot, 1f,
0, SpriteMeshType.Tight, targetSprite.border
);
MelonLogger.Msg($"[CustomNoteSkin] Successfully loaded {name}");
continue;
}
if (LoadIntoGameNoteImageContainer(fieldName, idx1, idx2, texture))
{
MelonLogger.Msg($"[CustomNoteSkin] Successfully loaded {name}");
}
}
}
[HarmonyPostfix]
[HarmonyPatch(typeof(GameCtrl), "Initialize")]
private static void ChangeOutlineTexture(GameObject ____guideEndPointObj)
{
if (____guideEndPointObj != null && customOutline != null)
{
____guideEndPointObj.GetComponent<SpriteRenderer>().sprite = customOutline;
}
}
[HarmonyPostfix]
[HarmonyPatch(typeof(SlideFan), "Initialize")]
private static void ChangeFanTexture(
SpriteRenderer[] ____spriteLines, SpriteRenderer[] ____effectSprites, bool ___BreakFlag, bool ___EachFlag
)
{
Vector3 position;
Sprite sprite;
if (___BreakFlag)
{
for (var i = 0; i < 11; i++)
{
sprite = customSlideFan[2, i];
if (sprite != null)
{
____spriteLines[2 * i].sprite = sprite;
position = ____spriteLines[2 * i].transform.localPosition;
____spriteLines[2 * i].transform.localPosition = new Vector3(0, position.y, position.z);
____spriteLines[2 * i].color = Color.white;
____spriteLines[2 * i + 1].sprite = sprite;
position = ____spriteLines[2 * i + 1].transform.localPosition;
____spriteLines[2 * i + 1].transform.localPosition = new Vector3(0, position.y, position.z);
____spriteLines[2 * i + 1].color = Color.white;
}
sprite = customSlideFan[3, i];
if (sprite != null)
{
____effectSprites[2 * i].sprite = sprite;
position = ____effectSprites[2 * i].transform.localPosition;
____effectSprites[2 * i].transform.localPosition = new Vector3(0, position.y, position.z);
____effectSprites[2 * i].color = Color.white;
____effectSprites[2 * i + 1].sprite = sprite;
position = ____effectSprites[2 * i + 1].transform.localPosition;
____effectSprites[2 * i + 1].transform.localPosition = new Vector3(0, position.y, position.z);
____effectSprites[2 * i + 1].color = Color.white;
}
}
}
else if (___EachFlag)
{
for (var i = 0; i < 11; i++)
{
sprite = customSlideFan[1, i];
if (sprite != null)
{
____spriteLines[2 * i].sprite = sprite;
position = ____spriteLines[2 * i].transform.localPosition;
____spriteLines[2 * i].transform.localPosition = new Vector3(0, position.y, position.z);
____spriteLines[2 * i].color = Color.white;
____spriteLines[2 * i + 1].sprite = sprite;
position = ____spriteLines[2 * i + 1].transform.localPosition;
____spriteLines[2 * i + 1].transform.localPosition = new Vector3(0, position.y, position.z);
____spriteLines[2 * i + 1].color = Color.white;
}
}
}
else
{
for (var i = 0; i < 11; i++)
{
sprite = customSlideFan[0, i];
if (sprite != null)
{
____spriteLines[2 * i].sprite = sprite;
position = ____spriteLines[2 * i].transform.localPosition;
____spriteLines[2 * i].transform.localPosition = new Vector3(0, position.y, position.z);
____spriteLines[2 * i].color = Color.white;
____spriteLines[2 * i + 1].sprite = sprite;
position = ____spriteLines[2 * i + 1].transform.localPosition;
____spriteLines[2 * i + 1].transform.localPosition = new Vector3(0, position.y, position.z);
____spriteLines[2 * i + 1].color = Color.white;
}
}
}
}
}

View File

@@ -1,78 +0,0 @@
using System.Collections.Generic;
using AquaMai.Config.Attributes;
using HarmonyLib;
using Monitor;
using UI;
using UnityEngine;
using UnityEngine.UI;
namespace AquaMai.Mods.Fancy;
[ConfigSection(
en: """
Custom track start difficulty image (not really custom difficulty).
Requires CustomSkins to be enabled.
Will load four image resources through custom skins: musicBase, musicTab, musicLvBase, musicLvText.
""",
zh: """
自定义在歌曲开始界面上显示的难度贴图 (并不是真的自定义难度)
需要启用自定义皮肤功能
会通过自定义皮肤加载四个图片资源: musicBase, musicTab, musicLvBase, musicLvText
""")]
public class CustomTrackStartDiff
{
// 自定义在歌曲开始界面上显示的难度 (并不是真的自定义难度)
// 需要启用自定义皮肤功能
// 会加载四个图片资源: musicBase, musicTab, musicLvBase, musicLvText
[HarmonyPostfix]
[HarmonyPatch(typeof(TrackStartMonitor), "SetTrackStart")]
private static void DisableTabs(
MultipleImage ____musicBaseImage,
MultipleImage ____musicTabImage,
SpriteCounter ____difficultySingle,
SpriteCounter ____difficultyDouble,
Image ____levelTextImage,
List<ResultMonitor.SpriteSheet> ____musicLevelSpriteSheets,
TimelineRoot ____musicDetail
)
{
var texture = CustomSkins.CustomTrackStart[0];
if (texture != null)
{
____musicBaseImage.MultiSprites[6] = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), new Vector2(0.5f, 0.5f), 100f);
____musicBaseImage.ChangeSprite(6);
}
texture = CustomSkins.CustomTrackStart[1];
if (texture != null)
{
____musicTabImage.MultiSprites[6] = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), new Vector2(0.5f, 0.5f), 100f);
____musicTabImage.ChangeSprite(6);
}
texture = CustomSkins.CustomTrackStart[2];
if (texture != null)
{
var lvBase = Traverse.Create(____musicDetail).Field<MultipleImage>("_lv_Base").Value;
lvBase.MultiSprites[6] = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), new Vector2(0.5f, 0.5f), 100f);
lvBase.ChangeSprite(6);
}
texture = CustomSkins.CustomTrackStart[3];
if (texture != null)
{
var original = ____musicLevelSpriteSheets[0].Sheet;
var sheet = new Sprite[original.Length];
for (var i = 0; i < original.Length; i++)
{
var sprite = original[i];
sheet[i] = Sprite.Create(texture, sprite.textureRect, new Vector2(0.5f, 0.5f), 100f);
}
____difficultySingle.SetSpriteSheet(sheet);
____difficultyDouble.SetSpriteSheet(sheet);
____levelTextImage.sprite = sheet[14];
}
}
}

View File

@@ -1,30 +0,0 @@
using AquaMai.Config.Attributes;
using HarmonyLib;
namespace AquaMai.Mods.Fancy;
[ConfigSection(
en: "Set the version string displayed at the top-right corner of the screen.",
zh: "把右上角的版本更改为自定义文本")]
public class CustomVersionString
{
[ConfigEntry]
private static readonly string versionString = "";
/*
* Patch displayVersionString Property Getter
*/
[HarmonyPrefix]
[HarmonyPatch(typeof(MAI2System.Config), "displayVersionString", MethodType.Getter)]
public static bool GetDisplayVersionString(ref string __result)
{
if (string.IsNullOrEmpty(versionString))
{
return true;
}
__result = versionString;
// Return false to block the original method
return false;
}
}

View File

@@ -1,34 +0,0 @@
using AquaMai.Config.Attributes;
using DB;
using HarmonyLib;
using MAI2.Util;
using Manager;
using Process;
namespace AquaMai.Mods.Fancy;
[ConfigSection(
en: "Play \"Master\" difficulty on Demo screen.",
zh: "在闲置时的演示画面上播放紫谱而不是绿谱")]
public class DemoMaster
{
[HarmonyPostfix]
[HarmonyPatch(typeof(AdvDemoProcess), "OnStart")]
public static void AdvDemoProcessPostStart()
{
for (int i = 0; i < 2; i++)
{
var userOption = Singleton<GamePlayManager>.Instance.GetGameScore(i).UserOption;
userOption.NoteSpeed = OptionNotespeedID.Speed6_5;
userOption.TouchSpeed = OptionTouchspeedID.Speed7_0;
}
}
[HarmonyPrefix]
[HarmonyPatch(typeof(GamePlayManager), "InitializeAdvertise")]
public static void PreInitializeAdvertise()
{
GameManager.SelectDifficultyID[0] = 3;
GameManager.SelectDifficultyID[1] = 3;
}
}

View File

@@ -1,51 +0,0 @@
using System;
using AquaMai.Config.Attributes;
using HarmonyLib;
using Manager;
using Monitor;
using UnityEngine;
namespace AquaMai.Mods.Fancy.GamePlay;
[ConfigSection(
en: """
Make the judgment display of circular Slides align precisely with the judgment line (originally a bit off).
Just like in majdata.
""",
zh: """
让圆弧形的 Slide 的判定显示与判定线精确对齐 (原本会有一点歪)
就像 majdata 里那样
""")]
public class AlignCircleSlideJudgeDisplay
{
/*
* 这个 Patch 让圆弧形的 Slide 的判定显示与判定线精确对齐 (原本会有一点歪), 就像 majdata 里那样
*/
[HarmonyPostfix]
[HarmonyPatch(typeof(SlideRoot), "Initialize")]
private static void FixJudgePosition(
SlideRoot __instance, SlideType ___EndSlideType, SlideJudge ___JudgeObj
)
{
if (null != ___JudgeObj)
{
float z = ___JudgeObj.transform.localPosition.z;
if (___EndSlideType == SlideType.Slide_Circle_L)
{
float angle = -45.0f - 45.0f * __instance.EndButtonId;
double angleRad = Math.PI / 180.0 * (angle + 90 + 22.5 + 2.6415);
___JudgeObj.transform.localPosition = new Vector3(480f * (float)Math.Cos(angleRad), 480f * (float)Math.Sin(angleRad), z);
___JudgeObj.transform.localRotation = Quaternion.Euler(0.0f, 0.0f, angle);
}
else if (___EndSlideType == SlideType.Slide_Circle_R)
{
float angle = -45.0f * __instance.EndButtonId;
double angleRad = Math.PI / 180.0 * (angle + 90 - 22.5 - 2.6415);
___JudgeObj.transform.localPosition = new Vector3(480f * (float)Math.Cos(angleRad), 480f * (float)Math.Sin(angleRad), z);
___JudgeObj.transform.localRotation = Quaternion.Euler(0.0f, 0.0f, angle);
}
}
}
}

View File

@@ -1,33 +0,0 @@
using AquaMai.Config.Attributes;
using HarmonyLib;
using Monitor;
using UnityEngine;
namespace AquaMai.Mods.Fancy.GamePlay;
[ConfigSection(
en: """
This Patch makes the Critical judgment of BreakSlide also flicker like BreakTap.
Recommended to use with custom skins (otherwise the visual effect may not be good).
""",
zh: """
这个 Patch 让 BreakSlide 的 Critical 判定也可以像 BreakTap 一样闪烁
推荐与自定义皮肤一起使用 (否则视觉效果可能并不好)
""")]
public class BreakSlideJudgeBlink
{
/*
* 这个 Patch 让 BreakSlide 的 Critical 判定也可以像 BreakTap 一样闪烁
* 推荐与自定义皮肤一起使用 (否则视觉效果可能并不好)
*/
[HarmonyPostfix]
[HarmonyPatch(typeof(SlideJudge), "UpdateBreakEffectAdd")]
private static void FixBreakSlideJudgeBlink(
SpriteRenderer ___SpriteRenderAdd, int ____addEffectCount
)
{
if (!___SpriteRenderAdd.gameObject.activeSelf) return;
float num = (____addEffectCount & 0b10) >> 1;
___SpriteRenderAdd.color = new Color(num, num, num, 1f);
}
}

View File

@@ -1,433 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using AquaMai.Config.Attributes;
using DB;
using HarmonyLib;
using MAI2.Util;
using Manager;
using MelonLoader;
using Monitor;
using UnityEngine;
using AquaMai.Mods.Fancy.GamePlay.CustomNoteTypes.Libs;
namespace AquaMai.Mods.Fancy.GamePlay.CustomNoteTypes;
[ConfigCollapseNamespace]
[ConfigSection(
en: "Custom Note Types.",
zh: "自定义 Note 类型"
)]
public class CustomNoteTypes
{
/*
* ========== ========== ========== ========== ========== ========== ========== ==========
* 以下内容是为了添加新的 MA2 语法用于表示自定义的 note 类型
* The following part is to add new MA2 command to Sinmai (representing custom note types)
*
* New note types:
* 1. Slide Super-new Super-hot (NMSSS, BRSSS, EXSSS, BXSSS, CNSSS):
* Definition: ??SSS [bar] [grid] [start pos] [wait] [duration] [end pos] [slide code (string)]
* Represent a slide note with highly customized path (using slide code)
*
* TODO (?)
* Mine notes (P.S. Mine-slides will automatically progress itself)
* Individual tracing duration in conn. slides
* Touch-slides / slides not ending in group A
* Non-C TouchHold
* Spinning tailless star (something like 1$$)
* Hyper Speed Definition ?
*/
public static int TotalMa2RecordCount = -1;
public static int LastMa2RecordID = -1;
public static Array Ma2FileRecordData;
public static void OnAfterPatch()
{
var arrayTraverse = Traverse.Create(typeof(Ma2fileRecordID)).Field("s_Ma2fileRecord_Data");
var targetArray = arrayTraverse.GetValue<Array>();
var nextId = targetArray.Length;
object[][] newEntries =
[
[nextId++, "NMSSS", "过新过热Slide", NotesTypeID.Def.Slide, SlideType.Slide_MAX, 8, Ma2Category.MA2_Note, 2, 2, 2, 2, 2, 2, 0],
[nextId++, "BRSSS", "过新过热BreakSlide", NotesTypeID.Def.BreakSlide, SlideType.Slide_MAX, 8, Ma2Category.MA2_Note, 2, 2, 2, 2, 2, 2, 0],
[nextId++, "EXSSS", "过新过热ExSlide", NotesTypeID.Def.ExSlide, SlideType.Slide_MAX, 8, Ma2Category.MA2_Note, 2, 2, 2, 2, 2, 2, 0],
[nextId++, "BXSSS", "过新过热ExBreakSlide", NotesTypeID.Def.ExBreakSlide, SlideType.Slide_MAX, 8, Ma2Category.MA2_Note, 2, 2, 2, 2, 2, 2, 0],
[nextId++, "CNSSS", "过新过热ConnSlide", NotesTypeID.Def.ConnectSlide, SlideType.Slide_MAX, 8, Ma2Category.MA2_Note, 2, 2, 2, 2, 2, 2, 0],
];
// Ma2fileRecordID.Ma2fileRecord_Data is private, so we need this shit.
var structType = targetArray.GetValue(0).GetType();
var constructor = AccessTools.Constructor(structType,
[
typeof(int), typeof(string), typeof(string), typeof(NotesTypeID.Def), typeof(SlideType), typeof(int),
typeof(Ma2Category), typeof(int), typeof(int), typeof(int), typeof(int), typeof(int), typeof(int),
typeof(int)
]);
Ma2FileRecordData = Array.CreateInstance(structType, targetArray.Length + newEntries.Length);
for (var i = 0; i < targetArray.Length; i++)
{
Ma2FileRecordData.SetValue(targetArray.GetValue(i), i);
}
for (var i = 0; i < newEntries.Length; i++)
{
var j = targetArray.Length + i;
var obj = constructor.Invoke(newEntries[i]);
Ma2FileRecordData.SetValue(obj, j);
}
arrayTraverse.SetValue(Ma2FileRecordData);
TotalMa2RecordCount = Ma2FileRecordData.Length;
LastMa2RecordID = TotalMa2RecordCount - 1;
MelonLogger.Msg($"[CustomNoteType] MA2 record data extended, total count: {TotalMa2RecordCount}");
// Initialize related classes ...
SlideDataBuilder.InitializeHitAreasLookup();
MelonLogger.Msg($"[CustomNoteType] HitAreasLookup initialized, total count: {SlideDataBuilder.HitAreasLookup.Count}");
}
[HarmonyPrefix]
[HarmonyPatch(typeof(Ma2fileRecordID), "findID")]
public static bool FindIDPrefix(string enumName, ref Ma2fileRecordID.Def __result)
{
// I don't know why but patching findID() leads to a completely invalid result
// Sometimes it will even throw an exception
// So I can only prefix it and override it
__result = Ma2fileRecordID.Def.Invalid;
for (var i = 0; i < TotalMa2RecordCount; i++)
{
var item = Ma2FileRecordData.GetValue(i);
if (Traverse.Create(item).Field<string>("enumName").Value == enumName)
{
__result = (Ma2fileRecordID.Def)i;
}
}
return false;
}
[HarmonyPatch]
public static class Ma2RecordValidation
{
public static IEnumerable<MethodBase> TargetMethods()
{
return
[
// AccessTools.Method(typeof(Ma2fileRecordID), "findID"),
AccessTools.Method(typeof(Ma2fileRecordID), "clamp"),
AccessTools.Method(typeof(Ma2fileRecordID), "getClampValue"),
AccessTools.Method(typeof(Ma2fileRecordID), "isValid"),
AccessTools.Method(typeof(Ma2fileRecordID_Extension), "isValid"),
];
}
public static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions)
{
foreach (var inst in instructions)
{
if (inst.LoadsConstant(142))
{
var instNew = new CodeInstruction(OpCodes.Ldsfld, AccessTools.Field(typeof(CustomNoteTypes), "TotalMa2RecordCount"));
yield return instNew;
}
else if (inst.LoadsConstant(141))
{
var instNew = new CodeInstruction(OpCodes.Ldsfld, AccessTools.Field(typeof(CustomNoteTypes), "LastMa2RecordID"));
yield return instNew;
}
else
{
yield return inst;
}
}
}
}
/*
* ========== ========== ========== ========== ========== ========== ========== ==========
* 以下内容是给新的 MA2 语法写解析器
*/
/*
* 给新建的 noteData 初始化应有的数据, 仅仅是照搬了 NotesReader.loadNote
*/
public static void PrepareBasicNoteData(NoteData noteData, NotesReader reader,
MA2Record record, int index, ref int noteIndex, OptionMirrorID mirrorMode)
{
noteData.type = record.getType().getNotesTypeId();
noteData.time.init(record.getBar(), record.getGrid(), reader);
noteData.end = noteData.time;
noteData.startButtonPos = MaiGeometry.MirrorInfo[(int)mirrorMode, record.getPos()];
noteData.index = index;
var num = record.getGrid() % 96;
if (num == 0)
{
noteData.beatType = NoteData.BeatType.BeatType04;
}
else if (num % 48 == 0)
{
noteData.beatType = NoteData.BeatType.BeatType08;
}
else if (num % 24 == 0)
{
noteData.beatType = NoteData.BeatType.BeatType16;
}
else if (num % 16 == 0)
{
noteData.beatType = NoteData.BeatType.BeatType24;
}
else
{
noteData.beatType = NoteData.BeatType.BeatTypeOther;
}
noteData.indexNote = noteIndex;
++noteIndex;
}
/*
* 给新建的 noteData 填入基本的 slide 相关数据, 仅仅是照搬了 NotesReader.loadNote
*/
public static void PrepareBasicSlideData(NoteData noteData, NotesReader reader, MA2Record record, int noteIndex,
ref int slideIndex, OptionMirrorID mirrorMode)
{
noteData.indexSlide = slideIndex++;
var slideData = noteData.slideData;
var slideWaitLen = record.getSlideWaitLen();
var slideShootLen = record.getSlideShootLen();
slideData.targetNote = MaiGeometry.MirrorInfo[(int)mirrorMode, record.getSlideEndPos()];
slideData.shoot.time.init(record.getBar(), record.getGrid() + slideWaitLen, reader);
slideData.shoot.index = noteIndex;
slideData.arrive.time.init(record.getBar(), record.getGrid() + slideWaitLen + slideShootLen, reader);
slideData.arrive.index = noteIndex;
noteData.end = slideData.arrive.time;
}
[HarmonyPrefix]
[HarmonyPatch(typeof(NotesReader), "loadNote")]
public static bool LoadCustomNote(NotesReader __instance, ref bool __result, NotesData ____note, int ____playerID,
MA2Record rec, int index, ref int noteIndex, ref int slideIndex)
{
if (rec.getType() < Ma2fileRecordID.Def.End)
{
// builtin record type
return true;
}
MelonLogger.Msg($"[CustomNoteType] Custom note | {rec._str.Count} | {rec.getStr(0)} {rec.getStr(1)} {rec.getStr(2)} {rec.getStr(3)} {rec.getStr(4)} {rec.getStr(5)} {rec.getStr(6)} {rec.getStr(7)} {rec.getStr(8)}");
var flag = true;
switch (rec.getType().getEnumName())
{
case "NMSSS":
case "BRSSS":
case "EXSSS":
case "BXSSS":
case "CNSSS":
var noteData = new CustomSlideNoteData();
var mirrorMode = Singleton<GamePlayManager>.Instance.GetGameScore(____playerID).UserOption.MirrorMode;
PrepareBasicNoteData(noteData, __instance, rec, index, ref noteIndex, mirrorMode);
PrepareBasicSlideData(noteData, __instance, rec, noteIndex, ref slideIndex, mirrorMode);
var success = noteData.ParseSlideCode(rec.getStr(7), mirrorMode);
if (success)
{
____note._noteData.Add(noteData);
}
else
{
flag = false;
}
break;
default:
flag = false;
break;
}
__result = flag;
return false;
}
/*
* ========== ========== ========== ========== ========== ========== ========== ==========
* 以下内容是为了实现自定义 Slide
*
*/
/*
* 把 GetSlidePath 和 GetSlideHitArea 和 GetSlideLength 重定向到我可以控制的函数上, 并且多推几个参数进来
*/
[HarmonyPatch]
public static class SlideNoteDataHack
{
public static IEnumerable<MethodBase> TargetMethods()
{
return
[
AccessTools.Method(typeof(SlideRoot), "Initialize"),
AccessTools.Method(typeof(SlideRoot), "GetSlideArrowNum", [typeof(NoteData)]),
AccessTools.Method(typeof(StarNote), "Initialize"),
AccessTools.Method(typeof(BreakStarNote), "Initialize"),
];
}
public static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions)
{
var methodGetSlidePath = AccessTools.Method(typeof(SlideManager), "GetSlidePath");
var methodGetSlidePathRedirect = AccessTools.Method(typeof(CustomNoteTypes), "GetSlidePathRedirect");
var methodGetSlideHitArea = AccessTools.Method(typeof(SlideManager), "GetSlideHitArea");
var methodGetSlideHitAreaRedirect = AccessTools.Method(typeof(CustomNoteTypes), "GetSlideHitAreaRedirect");
var methodGetSlideLength = AccessTools.Method(typeof(SlideManager), "GetSlideLength");
var methodGetSlideLengthRedirect = AccessTools.Method(typeof(CustomNoteTypes), "GetSlideLengthRedirect");
var fieldSlideData = AccessTools.Field(typeof(NoteData), "slideData");
var oldInstList = new List<CodeInstruction>(instructions);
var newInstList = new List<CodeInstruction>();
CodeInstruction instToInject = null;
for (var i = 0; i < oldInstList.Count; ++i)
{
var inst = oldInstList[i];
if (inst.LoadsField(fieldSlideData))
{
// 以 GetSlidePath 为例, 我们需要把下面这个调用:
// Singleton<SlideManager>.Instance.GetSlidePath(
// noteData.slideData.type, noteData.startButtonPos,
// noteData.slideData.targetNote, this.ButtonId
// )
// 里的 noteData 拿到手
// 所以就记录上一次 ldfld NoteData::slideData 的位置, 往前找一个 IL code
// 找到的就是 load 这个 noteData 的位置
// 然后在后续调用 GetSlidePath 时, 先重复一遍 load 把这个 noteData 入栈, 然后重定向到一个新的函数上去
instToInject = oldInstList[i - 1];
newInstList.Add(inst);
}
else if (inst.Calls(methodGetSlidePath))
{
newInstList.Add(instToInject!.Clone());
newInstList.Add(new CodeInstruction(OpCodes.Call, methodGetSlidePathRedirect));
instToInject = null;
}
else if (inst.Calls(methodGetSlideHitArea))
{
newInstList.Add(instToInject!.Clone());
newInstList.Add(new CodeInstruction(OpCodes.Call, methodGetSlideHitAreaRedirect));
instToInject = null;
}
else if (inst.Calls(methodGetSlideLength))
{
newInstList.Add(instToInject!.Clone());
newInstList.Add(new CodeInstruction(OpCodes.Call, methodGetSlideLengthRedirect));
instToInject = null;
}
else
{
newInstList.Add(inst);
}
}
return newInstList;
}
}
public static List<Vector4> GetSlidePathRedirect(SlideManager instance, SlideType slideType, int start, int end,
int starButton, NoteData noteData)
{
// MelonLogger.Msg($"[CustomNoteType] GetSlidePath Redirected!");
// MelonLogger.Msg($"{noteData.indexNote} {noteData.indexSlide} {slideType} {start} {end} {starButton}");
if (noteData is CustomSlideNoteData data)
{
// MelonLogger.Msg($"[CustomNoteType] Successfully injected custom path {data.SlideCode}");
return data.SlidePathList[starButton];
}
return instance.GetSlidePath(slideType, start, end, starButton);
}
public static List<SlideManager.HitArea> GetSlideHitAreaRedirect(SlideManager instance, SlideType slideType,
int start, int end, int starButton, NoteData noteData)
{
// MelonLogger.Msg($"[CustomNoteType] GetSlideHitArea Redirected!");
// MelonLogger.Msg($"{noteData.indexNote} {noteData.indexSlide} {slideType} {start} {end} {starButton}");
if (noteData is CustomSlideNoteData data)
{
// MelonLogger.Msg($"[CustomNoteType] Successfully injected custom hit areas {data.SlideCode}");
return data.SlideHitAreasList[starButton];
}
return instance.GetSlideHitArea(slideType, start, end, starButton);
}
public static float GetSlideLengthRedirect(SlideManager instance, SlideType slideType,
int start, int end, NoteData noteData)
{
// MelonLogger.Msg($"[CustomNoteType] GetSlideLength Redirected!");
// MelonLogger.Msg($"{noteData.indexNote} {noteData.indexSlide} {slideType} {start} {end}");
if (noteData is CustomSlideNoteData data)
{
// MelonLogger.Msg($"[CustomNoteType] Successfully injected custom path length {data.SlideCode}");
return data.SlidePathLength;
}
return instance.GetSlideLength(slideType, start, end);
}
[HarmonyPatch]
public static class Debuging
{
public static IEnumerable<MethodBase> TargetMethods()
{
return
[
AccessTools.Method(typeof(SlideRoot), "Initialize"),
// AccessTools.Method(typeof(SlideRoot), "GetSlideArrowNum", []),
// AccessTools.Method(typeof(SlideRoot), "GetSlideArrowNum", [typeof(NoteData)]),
// AccessTools.Method(typeof(SlideRoot), "GetArrowData"),
// AccessTools.Method(typeof(SlideRoot), "totalDistance"),
// AccessTools.Method(typeof(SlideRoot), "GetActiveArrowNum"),
// AccessTools.Method(typeof(SlideJudge), "SetJudgeType"),
];
}
public static void Prefix(MethodBase __originalMethod, object[] __args)
{
var msg = "[CustomNoteType] Before ";
msg += __originalMethod.DeclaringType!.FullName + "." + __originalMethod.Name + " (";
var infos = __originalMethod.GetParameters()
.Select((x, i) => x.ParameterType.FullName + " " + x.Name + " = " + GetString(__args[i]))
.ToArray();
msg += infos.Length > 0 ? infos.Aggregate((a, b) => a + ", " + b) : "void";
msg += ")";
MelonLogger.Msg(msg);
}
public static void Postfix(MethodBase __originalMethod, object[] __args)
{
var msg = "[CustomNoteType] After ";
msg += __originalMethod.DeclaringType!.FullName + "." + __originalMethod.Name + " (";
var infos = __originalMethod.GetParameters()
.Select((x, i) => x.ParameterType.FullName + " " + x.Name + " = " + GetString(__args[i]))
.ToArray();
msg += infos.Length > 0 ? infos.Aggregate((a, b) => a + ", " + b) : "void";
msg += ")";
MelonLogger.Msg(msg);
}
public static string GetString(object value)
{
if (value is CustomSlideNoteData data)
{
return $"<CustomSlideNoteData {data.indexNote} {data.indexSlide} {data.SlideCode}>";
}
if (value is NoteData data2)
{
return $"<NoteData {data2.indexNote} {data2.indexSlide}>";
}
return value.ToString();
}
}
}

View File

@@ -1,51 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using DB;
using Manager;
using MelonLoader;
using UnityEngine;
namespace AquaMai.Mods.Fancy.GamePlay.CustomNoteTypes.Libs;
public class CustomSlideNoteData: NoteData
{
public string SlideCode;
public List<List<Vector4>> SlidePathList = new List<List<Vector4>>();
public List<List<SlideManager.HitArea>> SlideHitAreasList = new List<List<SlideManager.HitArea>>();
public float SlidePathLength;
public bool ParseSlideCode(string slideCode, OptionMirrorID mirrorMode)
{
if (string.IsNullOrEmpty(slideCode))
{
return false;
}
SlidePathList.Clear();
SlideHitAreasList.Clear();
this.SlideCode = slideCode;
var path = SlideCodeParser.Parse(slideCode);
if (path == null)
{
return false;
}
var arrowData = SlideDataBuilder.BuildArrowData(path);
SlidePathLength = (float)path.GetPathLength();
var hitAreaData = SlideDataBuilder.BuildHitAreas(path);
for (var i = 0; i < 8; i++)
{
SlidePathList.Add(SlideDataBuilder.ConvertAndRotateArrowData(arrowData, i, mirrorMode));
SlideHitAreasList.Add(SlideDataBuilder.ConvertAndRotateHitAreas(hitAreaData, i, mirrorMode));
}
var msg = string.Join(", ",
hitAreaData.Select(x => x.PanelAreas).Select(x => string.Join("/", x.Cast<InputManager.TouchPanelArea>())));
MelonLogger.Msg(msg);
this.slideData.type = path.GetEndType(mirrorMode);
return true;
}
}

View File

@@ -1,107 +0,0 @@
using System;
using System.Numerics;
namespace AquaMai.Mods.Fancy.GamePlay.CustomNoteTypes.Libs;
public static class MaiGeometry
{
public struct CircleStruct(Complex center, double radius)
{
public Complex Center = center;
public double Radius = radius;
}
public static readonly double CanvasWidth = 1080.0;
public static readonly double MainRadius = 480.0;
public static readonly double CenterRadius = MainRadius * Math.Cos(Math.PI * 3 / 8);
public static readonly double GroupBRadius = CenterRadius / Math.Cos(Math.PI / 8);
private static readonly double _b = Math.Cos(Math.PI / 8) / 2;
private static readonly double _a = 1 - _b;
private static readonly double _theta = Math.PI / 4;
private static readonly double _s = (_a * _a + _b * _b - 2 * _a * _b * Math.Cos(_theta)) /
(2 * _a - 2 * _b * Math.Cos(_theta));
public static readonly double PPQQRadius = MainRadius * _b;
public static readonly double TransferRadius = MainRadius * (_b + _s);
public static readonly double EdgeTransferAngle = _theta;
public static readonly double PPQQTransferAngle =
Math.Acos((_s * _s + _b * _b - (_a - _s) * (_a - _s)) / (2 * _b * _s));
public static readonly double DefaultDistance = MainRadius * Math.PI / 32;
public static readonly int[,] MirrorInfo = new int[4, 17]
{
{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 }, // Normal
{ 7, 6, 5, 4, 3, 2, 1, 0, 15, 14, 13, 12, 11, 10, 9, 8, 16 }, // L <-> R
{ 3, 2, 1, 0, 7, 6, 5, 4, 11, 10, 9, 8, 15, 14, 13, 12, 16 }, // U <-> D
{ 4, 5, 6, 7, 0, 1, 2, 3, 12, 13, 14, 15, 8, 9, 10, 11, 16 } // rotate 180 deg
};
/// <summary>
/// Note: idx is 1-based, not 0-based
/// </summary>
public static Complex PointGroupA(int idx)
{
var angle = Math.PI * (5.0 / 8.0 - idx / 4.0);
return Complex.FromPolarCoordinates(MainRadius, angle);
}
/// <summary>
/// Note: idx is 1-based, not 0-based
/// </summary>
public static Complex PointGroupB(int idx)
{
var angle = Math.PI * (5.0 / 8.0 - idx / 4.0);
return Complex.FromPolarCoordinates(GroupBRadius, angle);
}
public static Complex Center()
{
return Complex.Zero;
}
/// <summary>
/// idx 0 is center circle, idx 1~8 are ppqq circles, idx 9 is outer circle
/// </summary>
public static CircleStruct GetCircle(int idx)
{
if (idx == 0)
{
return new CircleStruct(Complex.Zero, CenterRadius);
}
if (idx == 9)
{
return new CircleStruct(Complex.Zero, MainRadius);
}
var angle = Math.PI * (3.0 / 4.0 - idx / 4.0);
var center = Complex.FromPolarCoordinates(PPQQRadius, angle);
return new CircleStruct(center, PPQQRadius);
}
/// <summary>
/// Note: idx is 1-based, not 0-based
/// </summary>
/// <returns>CircleStruct TransferCircle, double TransferStartAngle, double TransferEndAngle</returns>
public static Tuple<CircleStruct, double, double> TransferOutData(int idx, bool isccw)
{
var ppqqRad = Math.PI * (3.0 / 4.0 - idx / 4.0);
double startAngle, endAngle;
if (isccw)
{
startAngle = ppqqRad - PPQQTransferAngle;
endAngle = ppqqRad + EdgeTransferAngle;
}
else
{
startAngle = ppqqRad + PPQQTransferAngle;
endAngle = ppqqRad - EdgeTransferAngle;
}
var d = MainRadius - TransferRadius;
var center = Complex.FromPolarCoordinates(d, endAngle);
return new Tuple<CircleStruct, double, double>(new CircleStruct(center, TransferRadius),
Math.IEEERemainder(startAngle, Math.PI * 2), Math.IEEERemainder(endAngle, Math.PI * 2));
}
}

View File

@@ -1,226 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using DB;
using Manager;
namespace AquaMai.Mods.Fancy.GamePlay.CustomNoteTypes.Libs;
public class ParametricSlidePath
{
public enum ParseMarker
{
None = 0,
SmoothAlign,
ForceAlign,
SharpCorner
}
public abstract class PathSegment
{
public ParseMarker ParseMarker = ParseMarker.None;
public double ArrowDistance = MaiGeometry.DefaultDistance;
public abstract bool DoAngleLerp { get; }
public abstract Complex GetPointAt(double t);
public abstract Complex GetTangentAt(double t);
public abstract double GetSegmentLength();
public void SetParseMarker(ParseMarker marker) => ParseMarker = marker;
public void SetArrowDistance(double distance) => ArrowDistance = distance;
}
public class LineSegment(Complex start, Complex end) : PathSegment
{
public readonly Complex StartPoint = start;
public readonly Complex EndPoint = end;
public override bool DoAngleLerp { get; } = false;
public override Complex GetPointAt(double t)
{
return StartPoint + (EndPoint - StartPoint) * t;
}
public override Complex GetTangentAt(double t)
{
var v = EndPoint - StartPoint;
return v / v.Magnitude;
}
public override double GetSegmentLength()
{
return (EndPoint - StartPoint).Magnitude;
}
}
public class ArcSegment(MaiGeometry.CircleStruct circle, double startAngle, double endAngle) : PathSegment
{
public readonly MaiGeometry.CircleStruct Circle = circle;
public readonly double StartAngle = startAngle;
public readonly double EndAngle = endAngle;
public override bool DoAngleLerp { get; } = true;
public override Complex GetPointAt(double t)
{
var angle = StartAngle + t * (EndAngle - StartAngle);
return Circle.Center + Complex.FromPolarCoordinates(Circle.Radius, angle);
}
public override Complex GetTangentAt(double t)
{
var angle = StartAngle + t * (EndAngle - StartAngle);
if (StartAngle < EndAngle)
{
return Complex.FromPolarCoordinates(1, angle) * Complex.ImaginaryOne;
}
else
{
return Complex.FromPolarCoordinates(-1, angle) * Complex.ImaginaryOne;
}
}
public override double GetSegmentLength()
{
return Math.Abs(EndAngle - StartAngle) * Circle.Radius;
}
}
public class CircleSegment(MaiGeometry.CircleStruct circle, double startAngle, bool isCcw) : PathSegment
{
public readonly MaiGeometry.CircleStruct Circle = circle;
public readonly double StartAngle = startAngle;
public readonly bool IsCcw = isCcw;
public override bool DoAngleLerp { get; } = true;
public override Complex GetPointAt(double t)
{
double angle;
if (IsCcw)
{
angle = StartAngle + t * Math.PI * 2f;
}
else
{
angle = StartAngle - t * Math.PI * 2f;
}
return Circle.Center + Complex.FromPolarCoordinates(Circle.Radius, angle);
}
public override Complex GetTangentAt(double t)
{
double angle;
if (IsCcw)
{
angle = StartAngle + t * Math.PI * 2f;
return Complex.FromPolarCoordinates(1, angle) * Complex.ImaginaryOne;
}
else
{
angle = StartAngle - t * Math.PI * 2f;
return Complex.FromPolarCoordinates(-1, angle) * Complex.ImaginaryOne;
}
}
public override double GetSegmentLength()
{
return Math.PI * Circle.Radius * 2;
}
}
public readonly PathSegment[] Segments;
public readonly double[] Fractions;
public readonly double[] AccumulatedLengths;
public ParametricSlidePath(IEnumerable<PathSegment> pathSegments)
{
Segments = pathSegments.ToArray();
if (Segments.Length == 0)
{
throw new ArgumentException("At least one path segment is required.");
}
var lengths = Segments.Select(s => s.GetSegmentLength());
var sum = 0.0;
AccumulatedLengths = lengths.Select(x => (sum += x)).ToArray();
Fractions = AccumulatedLengths.Select(x => x / sum).ToArray();
}
public PathSegment GetSegmentAt(double t, out double segmentT)
{
if (t <= 0.0)
{
segmentT = 0.0;
return Segments[0];
}
if (t >= 1.0)
{
segmentT = 1.0;
return Segments[Segments.Length - 1];
}
var idx = Array.BinarySearch(Fractions, t);
if (idx < 0)
{
idx = ~idx; // first entry > t
}
// if idx >= 0 then idx is the entry == t
// so Fractions[idx-1] < t and Fractions[idx] >= t
// Note: Fractions[i] marks the end point of Segments[i]
if (idx >= Segments.Length)
{
segmentT = 1.0;
return Segments[Segments.Length - 1];
}
if (idx == 0)
{
segmentT = t / Fractions[0];
return Segments[0];
}
segmentT = (t - Fractions[idx - 1]) / (Fractions[idx] - Fractions[idx - 1]);
return Segments[idx];
}
public double GetPathLength() => AccumulatedLengths[AccumulatedLengths.Length - 1];
public Complex GetPointAt(double t)
{
var segment = GetSegmentAt(t, out var segT);
return segment.GetPointAt(segT);
}
public Complex GetTangentAt(double t)
{
var segment = GetSegmentAt(t, out var segT);
return segment.GetTangentAt(segT);
}
public SlideType GetEndType(OptionMirrorID mirrorMode)
{
var lastSegment = Segments[Segments.Length - 1];
var flip = mirrorMode == OptionMirrorID.LR || mirrorMode == OptionMirrorID.UD;
if (lastSegment is CircleSegment circle)
{
return circle.IsCcw != flip ? SlideType.Slide_Circle_L : SlideType.Slide_Circle_R;
}
if (lastSegment is ArcSegment arc)
{
return (arc.EndAngle > arc.StartAngle) != flip ? SlideType.Slide_Circle_L : SlideType.Slide_Circle_R;
}
return SlideType.Slide_Straight;
}
}

View File

@@ -1,260 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using MelonLoader;
namespace AquaMai.Mods.Fancy.GamePlay.CustomNoteTypes.Libs;
public static class SlideCodeParser
{
public enum CommandType
{
Invalid = -1,
NodeA = 0,
NodeB = 1,
NodeC = 2,
OrbitCCW = 3,
OrbitCW = 4,
NodeEnd = 5
}
public struct Command(CommandType type, int value)
{
public CommandType Type = type;
public int Value = value;
public static bool IsSame(Command a, Command b)
{
return a.Type == b.Type && a.Value == b.Value;
}
}
public static readonly char[] CommandChars =
[
'A', 'B', 'C', 'P', 'Q', 'K'
];
public static int TryParseDigit(char c)
{
if (c >= '0' && c <= '9') return c - '0';
return -1;
}
public static List<Command> ParseCommands(string code)
{
if (!CommandChars.Contains(code[1]))
{
throw new ArgumentException($"the 2nd char should be a command");
}
if (code[code.Length - 2] != 'K')
{
throw new ArgumentException($"should end with 'K' command");
}
var commands = new List<Command>();
var currentType = CommandType.NodeA;
var value = TryParseDigit(code[0]);
if (value < 0) throw new ArgumentException($"invalid char '{code[0]}'");
commands.Add(new Command(currentType, value));
for (var ptr = 1; ptr < code.Length; ptr++)
{
var ch = code[ptr];
if (CommandChars.Contains(ch))
{
currentType = (CommandType) Array.IndexOf(CommandChars, ch);
if (currentType == CommandType.NodeC)
{
commands.Add(new Command(CommandType.NodeC, 0));
}
}
else
{
value = TryParseDigit(ch);
if (value < 0) throw new ArgumentException($"invalid char '{ch}'");
if (currentType == CommandType.NodeC)
{
throw new ArgumentException($"digit should not follow 'C'");
}
commands.Add(new Command(currentType, value));
}
}
return commands;
}
public static Complex GetNodePosition(Command cmd)
{
switch (cmd.Type)
{
case CommandType.NodeA:
case CommandType.NodeEnd:
return MaiGeometry.PointGroupA(cmd.Value);
case CommandType.NodeB:
return MaiGeometry.PointGroupB(cmd.Value);
case CommandType.NodeC:
return MaiGeometry.Center();
default:
throw new ArgumentException($"invalid type for node: {cmd.Type}");
}
}
public static void NodeToNode(SlidePathGenerator generator, Command last, Command current)
{
if (Command.IsSame(last, current)) return;
generator.LineToPoint(GetNodePosition(current));
}
public static void NodeToOrbit(SlidePathGenerator generator, Command last, Command current)
{
var isCcw = (current.Type == CommandType.OrbitCCW);
var node = GetNodePosition(last);
var orbit = MaiGeometry.GetCircle(current.Value);
var diff = node - orbit.Center;
if (Math.Abs(diff.Magnitude - orbit.Radius) < 0.1)
{
if (last.Type == CommandType.NodeA && current.Value == 9)
{
generator.TrySetLastParseMarker(ParametricSlidePath.ParseMarker.ForceAlign);
}
return; // node on circle, do nothing
}
if (diff.Magnitude < orbit.Radius)
throw new ArgumentException($"impossible: {last.Type}{last.Value} -> Orbit{current.Value}");
generator.TangentToCircle(orbit, isCcw);
}
public static void OrbitToNode(SlidePathGenerator generator, Command last, Command current)
{
var isCcw = (last.Type == CommandType.OrbitCCW);
var node = GetNodePosition(current);
var orbit = MaiGeometry.GetCircle(last.Value);
var diff = node - orbit.Center;
if (Math.Abs(diff.Magnitude - orbit.Radius) < 0.1)
{
generator.ArcToAngle(orbit.Center, diff.Phase, isCcw, false);
return;
}
if (diff.Magnitude < orbit.Radius)
throw new ArgumentException($"impossible: Orbit{last.Value} -> {current.Type}{current.Value}");
generator.ArcToTangentTowards(node, orbit.Center, isCcw);
generator.LineToPoint(node);
}
public static void OrbitToOrbit(SlidePathGenerator generator, Command last, Command current)
{
if (current.Type != last.Type) throw new ArgumentException($"orbit type mismatch");
var isCcw = (last.Type == CommandType.OrbitCCW);
var lastOrbit = MaiGeometry.GetCircle(last.Value);
var currentOrbit = MaiGeometry.GetCircle(current.Value);
if (current.Value == last.Value)
{
generator.FullCircle(lastOrbit.Center, isCcw);
return;
}
if (last.Value == 0 && current.Value == 9 || last.Value == 9 && current.Value == 0)
throw new ArgumentException($"impossible: Orbit{last.Value} -> Orbit{current.Value}");
if (current.Value == 9)
{
var data = MaiGeometry.TransferOutData(last.Value, isCcw);
generator.ArcToAngle(lastOrbit.Center, data.Item2, isCcw, false);
generator.ArcToAngle(data.Item1.Center, data.Item3, isCcw, false);
generator.TrySetLastParseMarker(ParametricSlidePath.ParseMarker.SmoothAlign);
return;
}
if (last.Value == 9)
{
var data = MaiGeometry.TransferOutData(current.Value, !isCcw);
generator.ArcToAngle(lastOrbit.Center, data.Item3, isCcw, true);
generator.ArcToAngle(data.Item1.Center, data.Item2, isCcw, false);
return;
}
generator.ExternTangentTransfer(lastOrbit.Center, currentOrbit, isCcw);
}
public static ParametricSlidePath Parse(string code)
{
try
{
var commands = ParseCommands(code);
var lastCmd = commands[0];
// The first command is guarantee to be 'A'
var generator = SlidePathGenerator.BeginAt(MaiGeometry.PointGroupA(lastCmd.Value));
for (var i = 1; i < commands.Count; i++)
{
var cmd = commands[i];
switch (cmd.Type)
{
case CommandType.NodeA:
case CommandType.NodeB:
case CommandType.NodeC:
case CommandType.NodeEnd:
switch (lastCmd.Type)
{
case CommandType.NodeA:
case CommandType.NodeB:
case CommandType.NodeC:
NodeToNode(generator, lastCmd, cmd);
break;
case CommandType.OrbitCCW:
case CommandType.OrbitCW:
OrbitToNode(generator, lastCmd, cmd);
break;
case CommandType.NodeEnd:
throw new ArgumentException($"'K' should be the last command");
default:
throw new ArgumentOutOfRangeException();
}
break;
case CommandType.OrbitCCW:
case CommandType.OrbitCW:
switch (lastCmd.Type)
{
case CommandType.NodeA:
case CommandType.NodeB:
case CommandType.NodeC:
NodeToOrbit(generator, lastCmd, cmd);
break;
case CommandType.OrbitCCW:
case CommandType.OrbitCW:
OrbitToOrbit(generator, lastCmd, cmd);
break;
case CommandType.NodeEnd:
throw new ArgumentException($"'K' should be the last command");
default:
throw new ArgumentOutOfRangeException();
}
break;
default:
throw new ArgumentOutOfRangeException();
}
lastCmd = cmd;
}
return generator.GeneratePath();
}
catch (ArgumentException e)
{
var msg = $"Invalid code: {code}";
if (e.Message != "")
{
msg += $", {e.Message}";
}
MelonLogger.Error(msg);
return null;
}
}
}

View File

@@ -1,473 +0,0 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using DB;
using Manager;
using Vector4 = UnityEngine.Vector4;
namespace AquaMai.Mods.Fancy.GamePlay.CustomNoteTypes.Libs;
public static class SlideDataBuilder
{
public readonly struct ArrowData(Complex point, Complex tangent, double length)
{
public readonly Complex Point = point;
public readonly Complex Tangent = tangent;
public readonly double Length = length;
}
public static List<ArrowData> BuildArrowData(ParametricSlidePath path)
{
var result = new List<ArrowData>();
var totalLength = path.GetPathLength();
var totalSegCount = path.Segments.Length;
var length = 0.0;
var segIdx = 0;
var isSwitching = false;
while (length < totalLength)
{
var t = length / totalLength;
var pt = path.GetPointAt(t);
var tg = path.GetTangentAt(t);
var t2 = (length + 10.0) / totalLength;
if ((path.GetTangentAt(t2) - tg).Magnitude < 0.2) // 0.2 -> ~ 11.48 deg apart
{
// use secant instead of tangent (for better visual quality)
tg = path.GetPointAt(t2) - pt;
tg /= tg.Magnitude;
}
if (isSwitching)
{
// around connecting point of 2 segments, smoothing the transition
var last = result[result.Count - 1];
var vec = pt - last.Point;
vec /= vec.Magnitude;
if ((tg - last.Tangent).Magnitude < 0.2)
{
var x = 0.5 * (last.Tangent + vec);
x /= x.Magnitude;
result[result.Count - 1] = new ArrowData(last.Point, x, last.Length);
tg = 0.5 * (tg + vec);
tg /= tg.Magnitude;
}
}
result.Add(new ArrowData(pt, tg, length));
isSwitching = false;
var nextLength = length + path.Segments[segIdx].ArrowDistance;
if (segIdx < totalSegCount - 1 && nextLength >= path.AccumulatedLengths[segIdx])
{
isSwitching = true;
if (path.Segments[segIdx].ParseMarker == ParametricSlidePath.ParseMarker.ForceAlign)
{
// in this case the next point is forced to be 1 unit after the connecting point
nextLength = path.AccumulatedLengths[segIdx] + path.Segments[segIdx + 1].ArrowDistance;
// P.S. 这种情况一般是出现在一条直线连接到外圈, 这个处理是为了让外圈的箭头对齐
}
if (path.Segments[segIdx + 1].ParseMarker == ParametricSlidePath.ParseMarker.SmoothAlign)
{
// arrow distance of the next segment is tempered in order to align arrow
var delta = path.AccumulatedLengths[segIdx + 1] - length;
var n = Math.Round(delta / MaiGeometry.DefaultDistance);
path.Segments[segIdx + 1].SetArrowDistance(delta / n);
nextLength = length + delta / n;
// P.S. 这种情况出现在 ppqq 圈进入外圈, 可以把转移轨道的箭头间距微调一下, 也是让外圈对齐
}
segIdx++;
}
length = nextLength;
}
// 把路径终点补上
result.Add(new ArrowData(path.GetPointAt(1.0), path.GetTangentAt(1.0), totalLength));
return result;
}
/// <summary>
/// Convert arrow data to sinmai format (Vector4)
/// </summary>
/// <param name="data">arrow data generated by BuildArrowData()</param>
/// <param name="starButton">button index of slide-star</param>
/// <param name="mirrorMode">mirror mode in user option</param>
/// <returns>sinmai format arrow data, referenced to slide-star</returns>
public static List<Vector4> ConvertAndRotateArrowData(IEnumerable<ArrowData> data, int starButton,
OptionMirrorID mirrorMode)
{
// SBGA 用 Vector4 存储了 slide 箭头的坐标与取向
// x, y 是平面坐标, z 是从起点到此处的路径长度 (px), w 是旋转的角度 (0 ~ 360 deg) (注意与切线方向差了 180 度)
// 坐标原点是屏幕中心, x 轴向右, y 轴向上
// w 的零点是朝向正右 (对应于箭头朝向正左), 逆时针为正方向
// 此外, sinmai 实际上是把所有 slide 路径相对于星星头存储的, 再在 SlideRoot 里通过 transform 转到合适的位置
// 判定区也是相对于星星头存储, 用 InputManager.ConvertTouchPanelRotatePush() 执行旋转
// 但是 slide code 定义的是绝对位置, 所以要逆向转回去, 以保证无论星星头在哪个键获取到的路径在处理过后都是一样的
// 然后还需要处理镜像的问题
var arrowList = new List<Vector4>();
var rotor = Complex.FromPolarCoordinates(1.0, Math.PI / 4.0 * starButton);
foreach (var arrow in data)
{
var pos = arrow.Point;
var tangent = arrow.Tangent;
switch (mirrorMode)
{
case OptionMirrorID.Normal:
break;
case OptionMirrorID.LR:
pos = Complex.Conjugate(pos) * -1.0;
tangent = Complex.Conjugate(tangent) * -1.0;
break;
case OptionMirrorID.UD:
pos = Complex.Conjugate(pos);
tangent = Complex.Conjugate(tangent);
break;
case OptionMirrorID.UDLR:
pos *= -1.0;
tangent *= -1.0;
break;
default:
break;
}
pos *= rotor;
tangent *= rotor;
var angle = tangent.Phase * 180.0 / Math.PI + 180.0; // Phase is in [-PI, PI]
arrowList.Add(new Vector4((float) pos.Real, (float) pos.Imaginary, (float) arrow.Length, (float) angle));
}
return arrowList;
}
public readonly struct HitAreaData(double push, double release, int[] areas)
{
public readonly double PushDistance = push;
public readonly double ReleaseDistance = release;
public readonly int[] PanelAreas = areas;
}
public static readonly Dictionary<int, HitAreaData[]> HitAreasLookup = new Dictionary<int, HitAreaData[]>();
public static void InitializeHitAreasLookup()
{
for (var i = 0; i < 8; i++)
{
for (var j = 0; j < 8; j++)
{
var diff = (j - i) & 7; // you know this is actually % 8 ... for same negative number compat
int tmp, tmp2;
// Ai -> Aj
var key = (i << 5) | j;
switch (diff)
{
case 1:
case 7:
HitAreasLookup[key] =
[
new HitAreaData(0.32, 0.68, [i]),
new HitAreaData(1.00, 1.00, [j])
];
break;
case 2:
tmp = (i + 1) & 7;
HitAreasLookup[key] =
[
new HitAreaData(0.20, 0.38, [i]),
new HitAreaData(0.62, 0.80, [tmp, tmp | 8]),
new HitAreaData(1.00, 1.00, [j])
];
break;
case 6:
tmp = (i - 1) & 7;
HitAreasLookup[key] =
[
new HitAreaData(0.20, 0.38, [i]),
new HitAreaData(0.62, 0.80, [tmp, tmp | 8]),
new HitAreaData(1.00, 1.00, [j])
];
break;
default:
break;
}
// Bi -> Bj
key = ((i | 8) << 5) | (j | 8);
switch (diff)
{
case 1:
case 7:
HitAreasLookup[key] =
[
new HitAreaData(0.44, 0.56, [i | 8]),
new HitAreaData(1.00, 1.00, [j | 8])
];
break;
case 2:
tmp = (i + 1) & 7;
HitAreasLookup[key] =
[
new HitAreaData(0.22, 0.35, [i | 8]),
new HitAreaData(0.65, 0.78, [tmp | 8, 16]),
new HitAreaData(1.00, 1.00, [j | 8])
];
break;
case 6:
tmp = (i - 1) & 7;
HitAreasLookup[key] =
[
new HitAreaData(0.22, 0.35, [i | 8]),
new HitAreaData(0.65, 0.78, [tmp | 8, 16]),
new HitAreaData(1.00, 1.00, [j | 8])
];
break;
case 3:
tmp = (i + 1) & 7;
tmp2 = (i + 2) & 7;
HitAreasLookup[key] =
[
new HitAreaData(0.15, 0.28, [i | 8]),
new HitAreaData(0.48, 0.52, [tmp | 8, 16]),
new HitAreaData(0.72, 0.85, [tmp2 | 8, 16]),
new HitAreaData(1.00, 1.00, [j | 8])
];
break;
case 5:
tmp = (i - 1) & 7;
tmp2 = (i - 2) & 7;
HitAreasLookup[key] =
[
new HitAreaData(0.15, 0.28, [i | 8]),
new HitAreaData(0.48, 0.52, [tmp | 8, 16]),
new HitAreaData(0.72, 0.85, [tmp2 | 8, 16]),
new HitAreaData(1.00, 1.00, [j | 8])
];
break;
default:
break;
}
// Ai <-> Bj
key = (i << 5) | (j | 8);
var key2 = ((j | 8) << 5) | i;
switch (diff)
{
case 0:
HitAreasLookup[key] =
[
new HitAreaData(0.60, 0.75, [i]),
new HitAreaData(1.00, 1.00, [j | 8])
];
HitAreasLookup[key2] =
[
new HitAreaData(0.25, 0.40, [j | 8]),
new HitAreaData(1.00, 1.00, [i])
];
break;
case 1:
case 7:
HitAreasLookup[key] =
[
new HitAreaData(0.45, 0.77, [i]),
new HitAreaData(1.00, 1.00, [j | 8])
];
HitAreasLookup[key2] =
[
new HitAreaData(0.23, 0.55, [j | 8]),
new HitAreaData(1.00, 1.00, [i])
];
break;
case 3:
tmp = (i + 1) & 7;
tmp2 = (i + 2) & 7;
HitAreasLookup[key] =
[
new HitAreaData(0.25, 0.34, [i]),
new HitAreaData(0.54, 0.68, [i | 8, tmp | 8]),
new HitAreaData(0.85, 0.90, [tmp2 | 8, 16]),
new HitAreaData(1.00, 1.00, [j | 8])
];
HitAreasLookup[key2] =
[
new HitAreaData(0.10, 0.15, [j | 8]),
new HitAreaData(0.32, 0.46, [tmp2 | 8, 16]),
new HitAreaData(0.66, 0.75, [i | 8, tmp | 8]),
new HitAreaData(1.00, 1.00, [i])
];
break;
case 5:
tmp = (i - 1) & 7;
tmp2 = (i - 2) & 7;
HitAreasLookup[key] =
[
new HitAreaData(0.25, 0.34, [i]),
new HitAreaData(0.54, 0.68, [i | 8, tmp | 8]),
new HitAreaData(0.85, 0.90, [tmp2 | 8, 16]),
new HitAreaData(1.00, 1.00, [j | 8])
];
HitAreasLookup[key2] =
[
new HitAreaData(0.10, 0.15, [j | 8]),
new HitAreaData(0.32, 0.46, [tmp2 | 8, 16]),
new HitAreaData(0.66, 0.75, [i | 8, tmp | 8]),
new HitAreaData(1.00, 1.00, [i])
];
break;
default:
break;
}
// C <-> Bj
key = (16 << 5) | (j | 8);
key2 = ((j | 8) << 5) | 16;
HitAreasLookup[key] =
[
new HitAreaData(0.50, 0.70, [16]),
new HitAreaData(1.00, 1.00, [j | 8])
];
HitAreasLookup[key2] =
[
new HitAreaData(0.30, 0.50, [j | 8]),
new HitAreaData(1.00, 1.00, [16])
];
}
}
}
public static List<HitAreaData> BuildHitAreas(ParametricSlidePath path)
{
var nodeList = new List<Tuple<int, double>>();
var totalLength = path.GetPathLength();
var count = (int)Math.Round(totalLength / 10.0);
int? lastNode = null;
var enterLength = 0.0;
for (var i = 0; i < count; i++)
{
var t = (double)i / count;
var pt = path.GetPointAt(t);
int? node = null;
if (pt.Magnitude < 55.0)
{
node = 16;
}
else for (var j = 0; j < 8; j++)
{
var phi = Math.PI * (3.0 / 8.0 - j / 4.0);
if ((pt - Complex.FromPolarCoordinates(440.0, phi)).Magnitude < 80.0)
{
node = j;
break;
}
if ((pt - Complex.FromPolarCoordinates(210.0, phi)).Magnitude < 45.0)
{
node = j | 8;
break;
}
}
if (lastNode != node)
{
var length = t * totalLength;
if (lastNode == null)
{
enterLength = length;
}
else
{
nodeList.Add(new Tuple<int, double>(lastNode.Value, (length + enterLength) / 2.0));
if (node != null)
{
enterLength = length;
}
}
}
lastNode = node;
}
nodeList.Add(new Tuple<int, double>(lastNode!.Value, totalLength));
nodeList[0] = new Tuple<int, double>(nodeList[0].Item1, 0.0);
var result = new List<HitAreaData>();
result.Add(new HitAreaData(0.0, 0.0, [nodeList[0].Item1]));
for (var i = 1; i < nodeList.Count; i++)
{
var key = (nodeList[i - 1].Item1 << 5) | nodeList[i].Item1;
var segmentLength = nodeList[i].Item2 - nodeList[i - 1].Item2;
var data = HitAreasLookup[key];
var area = result[result.Count - 1];
result[result.Count - 1] = new HitAreaData(
area.PushDistance + segmentLength * data[0].PushDistance,
area.ReleaseDistance + segmentLength * data[0].ReleaseDistance,
area.PanelAreas
);
for (var j = 1; j < data.Length; j++)
{
result.Add(new HitAreaData(
segmentLength * (data[j].PushDistance - data[j - 1].ReleaseDistance),
segmentLength * (data[j].ReleaseDistance - data[j].PushDistance),
data[j].PanelAreas
));
}
}
double lastPushDistance = 0.0;
if (path.GetEndType(OptionMirrorID.Normal) == SlideType.Slide_Straight)
{
var diff = nodeList[nodeList.Count - 1].Item1 - nodeList[nodeList.Count - 2].Item1;
diff %= 8;
lastPushDistance = diff switch
{
1 or 2 or 6 or 7 => 130.0,
_ => 159.0
};
}
else
{
lastPushDistance = 175.0;
}
var last2ndArea = result[result.Count - 2];
var lastArea = result[result.Count - 1];
var distance = last2ndArea.ReleaseDistance + lastArea.PushDistance + lastArea.ReleaseDistance;
result[result.Count - 2] = new HitAreaData(last2ndArea.PushDistance, distance - lastPushDistance, last2ndArea.PanelAreas);
result[result.Count - 1] = new HitAreaData(lastPushDistance, 0.0, lastArea.PanelAreas);
return result;
}
/// <summary>
/// Convert hit area data to sinmai format (Vector4)
/// </summary>
/// <param name="data">hit area data generated by BuildHitAreas()</param>
/// <param name="starButton">button index of slide-star</param>
/// <param name="mirrorMode">mirror mode in user option</param>
/// <returns>sinmai format arrow data, referenced to slide-star</returns>
public static List<SlideManager.HitArea> ConvertAndRotateHitAreas(IEnumerable<HitAreaData> data, int starButton,
OptionMirrorID mirrorMode)
{
var hitAreaList = new List<SlideManager.HitArea>();
foreach (var hitAreaData in data)
{
var hitArea = new SlideManager.HitArea();
hitArea.PushDistance = hitAreaData.PushDistance;
hitArea.ReleaseDistance = hitAreaData.ReleaseDistance;
foreach (var pad in hitAreaData.PanelAreas)
{
var converted = MaiGeometry.MirrorInfo[(int) mirrorMode, pad];
converted = converted == 16 ? 16 : (converted - starButton) & 0b111 | converted & 0b1000;
hitArea.HitPoints.Add((InputManager.TouchPanelArea) converted);
}
hitAreaList.Add(hitArea);
}
return hitAreaList;
}
}

View File

@@ -1,131 +0,0 @@
using System;
using System.Collections.Generic;
using System.Numerics;
namespace AquaMai.Mods.Fancy.GamePlay.CustomNoteTypes.Libs;
public class SlidePathGenerator
{
public List<ParametricSlidePath.PathSegment> PathSegments = new List<ParametricSlidePath.PathSegment>();
public Complex CurrentEndPoint = Complex.Zero;
public static SlidePathGenerator BeginAt(Complex point)
{
var obj = new SlidePathGenerator();
obj.CurrentEndPoint = point;
return obj;
}
public static double CalcTangentAngle(Complex point, MaiGeometry.CircleStruct circle, bool isCcw)
{
var hypot = point - circle.Center;
var angleDelta = Math.Acos(circle.Radius / hypot.Magnitude);
var tanAngle = hypot.Phase + (isCcw ? angleDelta : -angleDelta);
return Math.IEEERemainder(tanAngle, Math.PI * 2.0);
}
public void TrySetLastParseMarker(ParametricSlidePath.ParseMarker marker)
{
if (PathSegments.Count <= 0) return;
PathSegments[PathSegments.Count - 1].SetParseMarker(marker);
}
public void LineToPoint(Complex point)
{
PathSegments.Add(new ParametricSlidePath.LineSegment(CurrentEndPoint, point));
CurrentEndPoint = point;
}
public void TangentToCircle(MaiGeometry.CircleStruct circle, bool isCcw)
{
var inAngle = CalcTangentAngle(CurrentEndPoint, circle, isCcw);
var inPoint = Complex.FromPolarCoordinates(circle.Radius, inAngle) + circle.Center;
LineToPoint(inPoint);
}
/// <summary>Note: endAngle should be in range [-PI, PI]</summary>
public void ArcToAngle(Complex center, double endAngle, bool isCcw, bool skipIfZero)
{
var diff = CurrentEndPoint - center;
var circle = new MaiGeometry.CircleStruct(center, diff.Magnitude);
var startAngle = diff.Phase;
// startAngle and endAngle in range [-PI, PI]
if (isCcw)
{
if (startAngle > endAngle)
{
startAngle -= 2 * Math.PI;
}
if (Math.Abs(endAngle - startAngle) < 0.001)
{
if (skipIfZero) return;
endAngle += 2 * Math.PI;
}
}
else
{
if (startAngle < endAngle)
{
startAngle += 2 * Math.PI;
}
if (Math.Abs(endAngle - startAngle) < 0.001)
{
if (skipIfZero) return;
endAngle -= 2 * Math.PI;
}
}
var seg = new ParametricSlidePath.ArcSegment(circle, startAngle, endAngle);
PathSegments.Add(seg);
CurrentEndPoint = seg.GetPointAt(1f);
}
public void ArcToTangentTowards(Complex target, Complex center, bool isCcw)
{
var diff = CurrentEndPoint - center;
var endAngle = CalcTangentAngle(target, new MaiGeometry.CircleStruct(center, diff.Magnitude), !isCcw);
ArcToAngle(center, endAngle, isCcw, false);
}
public void FullCircle(Complex center, bool isCcw)
{
var diff = CurrentEndPoint - center;
var circle = new MaiGeometry.CircleStruct(center, diff.Magnitude);
PathSegments.Add(new ParametricSlidePath.CircleSegment(circle, diff.Phase, isCcw));
// CurrentEndPoint not changed
}
public void ExternTangentTransfer(Complex currentCenter, MaiGeometry.CircleStruct targetCircle, bool isCcw)
{
var diff = CurrentEndPoint - currentCenter;
double endAngle;
if (Math.Abs(diff.Magnitude - targetCircle.Radius) < 0.001)
{
// two circles are approximately same radius
var vector = targetCircle.Center - currentCenter;
vector *= isCcw ? -Complex.ImaginaryOne : Complex.ImaginaryOne;
endAngle = vector.Phase;
}
else if (targetCircle.Radius > diff.Magnitude)
{
// target circle larger
var helperCircle = new MaiGeometry.CircleStruct(targetCircle.Center, targetCircle.Radius - diff.Magnitude);
endAngle = CalcTangentAngle(currentCenter, helperCircle, isCcw);
}
else
{
var helperCircle = new MaiGeometry.CircleStruct(currentCenter, diff.Magnitude - targetCircle.Radius);
endAngle = CalcTangentAngle(targetCircle.Center, helperCircle, !isCcw);
}
ArcToAngle(currentCenter, endAngle, isCcw, false);
var inPoint = Complex.FromPolarCoordinates(targetCircle.Radius, endAngle) + targetCircle.Center;
LineToPoint(inPoint);
}
public ParametricSlidePath GeneratePath()
{
return new ParametricSlidePath(PathSegments);
}
}

View File

@@ -1,50 +0,0 @@
using AquaMai.Config.Attributes;
using HarmonyLib;
using Monitor;
using TMPro;
using UI;
using UnityEngine;
namespace AquaMai.Mods.Fancy.GamePlay;
[ConfigSection(
en: """
Disable the TRACK X text, DX/Standard display box, and the derakkuma at the bottom of the screen in the song start screen.
For recording chart confirmation.
""",
zh: """
在歌曲开始界面, 把 TRACK X 字样, DX/标准谱面的显示框, 以及画面下方的滴蜡熊隐藏掉
录制谱面确认用
""")]
public class DisableTrackStartTabs
{
// 在歌曲开始界面, 把 TRACK X 字样, DX/标准谱面的显示框, 以及画面下方的滴蜡熊隐藏掉, 让他看起来不那么 sinmai, 更像是 majdata
[HarmonyPostfix]
[HarmonyPatch(typeof(TrackStartMonitor), "SetTrackStart")]
private static void DisableTabs(
SpriteCounter ____trackNumber, SpriteCounter ____bossTrackNumber, SpriteCounter ____utageTrackNumber,
MultipleImage ____musicTabImage, GameObject[] ____musicTabObj, GameObject ____derakkumaRoot,
TimelineRoot ____musicDetail
)
{
____trackNumber.transform.parent.gameObject.SetActive(false);
____bossTrackNumber.transform.parent.gameObject.SetActive(false);
____utageTrackNumber.transform.parent.gameObject.SetActive(false);
____musicTabImage.gameObject.SetActive(false);
____musicTabObj[0].gameObject.SetActive(false);
____musicTabObj[1].gameObject.SetActive(false);
____musicTabObj[2].gameObject.SetActive(false);
____derakkumaRoot.SetActive(false);
var traverse = Traverse.Create(____musicDetail);
traverse.Field<MultipleImage>("_achivement_Base").Value.ChangeSprite(1);
traverse.Field<MultipleImage>("_clearRank_Base").Value.ChangeSprite(1);
traverse.Field<TextMeshProUGUI>("_achivement_Text").Value.gameObject.SetActive(false);
traverse.Field<TextMeshProUGUI>("_achivement_decimal_Text").Value.gameObject.SetActive(false);
traverse.Field<TextMeshProUGUI>("_achivement_percent_Text").Value.gameObject.SetActive(false);
traverse.Field<MultipleImage>("_clearRank_Image").Value.gameObject.SetActive(false);
traverse.Field<GameObject>("_deluxScore_Obj").Value.SetActive(false);
traverse.Field<MultipleImage>("_comboRank_Image").Value.ChangeSprite(0);
traverse.Field<MultipleImage>("_syncRank_Image").Value.ChangeSprite(0);
}
}

View File

@@ -1,129 +0,0 @@
using System.Collections.Generic;
using AquaMai.Core.Attributes;
using AquaMai.Config.Attributes;
using HarmonyLib;
using MAI2.Util;
using Manager;
using Monitor;
using Monitor.Game;
using UnityEngine;
namespace AquaMai.Mods.Fancy.GamePlay;
[ConfigSection(
en: "Add notes sprite to the pool to prevent use up.",
zh: "增加更多待命的音符贴图,防止奇怪的自制谱用完音符贴图池")]
[EnableGameVersion(23000)]
public class ExtendNotesPool
{
[ConfigEntry(
en: "Number of objects to add.",
zh: "要增加的对象数量")]
private readonly static int count = 128;
[HarmonyPostfix]
[HarmonyPatch(typeof(GameCtrl), "CreateNotePool")]
public static void CreateNotePool(ref GameCtrl __instance,
GameObject ____tapListParent, List<TapNote> ____tapObjectList,
GameObject ____holdListParent, List<HoldNote> ____holdObjectList,
GameObject ____breakHoldListParent, List<BreakHoldNote> ____breakHoldObjectList,
GameObject ____starListParent, List<StarNote> ____starObjectList,
GameObject ____breakStarListParent, List<BreakStarNote> ____breakStarObjectList,
GameObject ____breakListParent, List<BreakNote> ____breakObjectList,
GameObject ____touchListParent, List<TouchNoteB> ____touchBObjectList,
GameObject ____touchCTapListParent, List<TouchNoteC> ____touchCTapObjectList,
GameObject ____touchCHoldListParent, List<TouchHoldC> ____touchCHoldObjectList,
GameObject ____slideListParent, List<SlideRoot> ____slideObjectList,
GameObject ____fanSlideListParent, List<SlideFan> ____fanSlideObjectList,
GameObject ____slideJudgeListParent, List<SlideJudge> ____judgeSlideObjectList,
GameObject ____guideListParent, List<NoteGuide> ____guideObjectList,
GameObject ____barGuideListParent, List<BarGuide> ____barGuideObjectList,
List<SpriteRenderer> ____arrowObjectList, List<BreakSlide> ____breakArrowObjectList
)
{
for (var i = 0; i < count; i++)
{
var tapNote = Object.Instantiate(GameNotePrefabContainer.Tap, ____tapListParent.transform);
tapNote.gameObject.SetActive(false);
tapNote.ParentTransform = ____tapListParent.transform;
____tapObjectList.Add(tapNote);
var holdNote = Object.Instantiate(GameNotePrefabContainer.Hold, ____holdListParent.transform);
holdNote.gameObject.SetActive(false);
holdNote.ParentTransform = ____holdListParent.transform;
____holdObjectList.Add(holdNote);
var breakHoldNote = Object.Instantiate(GameNotePrefabContainer.BreakHold, ____breakHoldListParent.transform);
breakHoldNote.gameObject.SetActive(false);
breakHoldNote.ParentTransform = ____holdListParent.transform;
____breakHoldObjectList.Add(breakHoldNote);
var starNote = Object.Instantiate(GameNotePrefabContainer.Star, ____starListParent.transform);
starNote.gameObject.SetActive(false);
starNote.ParentTransform = ____starListParent.transform;
____starObjectList.Add(starNote);
var breakStarNote = Object.Instantiate(GameNotePrefabContainer.BreakStar, ____breakStarListParent.transform);
breakStarNote.gameObject.SetActive(false);
breakStarNote.ParentTransform = ____breakStarListParent.transform;
____breakStarObjectList.Add(breakStarNote);
var breakNote = Object.Instantiate(GameNotePrefabContainer.Break, ____breakListParent.transform);
breakNote.gameObject.SetActive(false);
breakNote.ParentTransform = ____breakListParent.transform;
____breakObjectList.Add(breakNote);
var touchNoteB = Object.Instantiate(GameNotePrefabContainer.TouchTapB, ____touchListParent.transform);
touchNoteB.gameObject.SetActive(false);
touchNoteB.ParentTransform = ____touchListParent.transform;
____touchBObjectList.Add(touchNoteB);
var touchNoteC = Object.Instantiate(GameNotePrefabContainer.TouchTapC, ____touchCTapListParent.transform);
touchNoteC.gameObject.SetActive(false);
touchNoteC.ParentTransform = ____touchCTapListParent.transform;
____touchCTapObjectList.Add(touchNoteC);
var touchHoldC = Object.Instantiate(GameNotePrefabContainer.TouchHoldC, ____touchCHoldListParent.transform);
touchHoldC.gameObject.SetActive(false);
touchHoldC.ParentTransform = ____touchCHoldListParent.transform;
____touchCHoldObjectList.Add(touchHoldC);
var slideRoot = Object.Instantiate(GameNotePrefabContainer.Slide, ____slideListParent.transform);
slideRoot.gameObject.SetActive(false);
slideRoot.ParentTransform = ____slideListParent.transform;
____slideObjectList.Add(slideRoot);
var slideFan = Object.Instantiate(GameNotePrefabContainer.SlideFan, ____fanSlideListParent.transform);
slideFan.gameObject.SetActive(false);
slideFan.ParentTransform = ____fanSlideListParent.transform;
____fanSlideObjectList.Add(slideFan);
var slideJudge = Object.Instantiate(GameNotePrefabContainer.SlideJudge, ____slideJudgeListParent.transform);
slideJudge.gameObject.SetActive(false);
slideJudge.ParentTransform = ____slideJudgeListParent.transform;
slideJudge.SetOption(Singleton<GamePlayManager>.Instance.GetGameScore(__instance.MonitorIndex).UserOption.DispJudge);
____judgeSlideObjectList.Add(slideJudge);
for (var j = 0; j < 50; j++)
{
var spriteRenderer = Object.Instantiate(GameNotePrefabContainer.Arrow, ____slideListParent.transform);
spriteRenderer.gameObject.SetActive(false);
____arrowObjectList.Add(spriteRenderer);
var breakSlide = Object.Instantiate(GameNotePrefabContainer.BreakArrow, ____slideListParent.transform);
breakSlide.gameObject.SetActive(false);
____breakArrowObjectList.Add(breakSlide);
}
var noteGuide = Object.Instantiate(GameNotePrefabContainer.Guide, ____guideListParent.transform);
noteGuide.gameObject.SetActive(false);
noteGuide.ParentTransform = ____guideListParent.transform;
____guideObjectList.Add(noteGuide);
var barGuide = Object.Instantiate(GameNotePrefabContainer.BarGuide, ____barGuideListParent.transform);
barGuide.gameObject.SetActive(false);
barGuide.ParentTransform = ____barGuideListParent.transform;
____barGuideObjectList.Add(barGuide);
}
}
}

View File

@@ -1,42 +0,0 @@
using AquaMai.Config.Attributes;
using HarmonyLib;
using Monitor;
namespace AquaMai.Mods.Fancy.GamePlay;
[ConfigSection(
en: """
Make the judgment display of WiFi Slide different in up and down (originally all WiFi judgment displays are towards the center), just like in majdata.
The reason for this bug is that SEGA forgot to assign EndButtonId to WiFi.
""",
zh: """
这个 Patch 让 WiFi Slide 的判定显示有上下的区别 (原本所有 WiFi 的判定显示都是朝向圆心的), 就像 majdata 里那样
这个 bug 产生的原因是 SBGA 忘记给 WiFi 的 EndButtonId 赋值了
""")]
public class FanJudgeFlip
{
/*
* 这个 Patch 让 WiFi Slide 的判定显示有上下的区别 (原本所有 WiFi 的判定显示都是朝向圆心的), 就像 majdata 里那样
* 这个 bug 产生的原因是 SBGA 忘记给 WiFi 的 EndButtonId 赋值了
* 不过需要注意的是, 考虑到圆弧形 Slide 的判定显示就是永远朝向圆心的, 我个人会觉得这个 Patch 关掉更好看一点
*/
[HarmonyPostfix]
[HarmonyPatch(typeof(SlideFan), "Initialize")]
private static void FixFanJudgeFilp(
int[] ___GoalButtonId, SlideJudge ___JudgeObj
)
{
if (null != ___JudgeObj)
{
if (2 <= ___GoalButtonId[1] && ___GoalButtonId[1] <= 5)
{
___JudgeObj.Flip(false);
___JudgeObj.transform.Rotate(0.0f, 0.0f, 180f);
}
else
{
___JudgeObj.Flip(true);
}
}
}
}

View File

@@ -1,24 +0,0 @@
using AquaMai.Config.Attributes;
using Fx;
using HarmonyLib;
using Monitor;
using UnityEngine;
namespace AquaMai.Mods.Fancy.GamePlay;
[ConfigSection(
en: "Hide hanabi completely.",
zh: "完全隐藏烟花")]
public class HideHanabi
{
[HarmonyPatch(typeof(TapCEffect), "SetUpParticle")]
[HarmonyPostfix]
public static void FixZeroSize(TapCEffect __instance, FX_Mai2_Note_Color ____particleControler)
{
var entities = ____particleControler.GetComponentsInChildren<ParticleSystemRenderer>(true);
foreach (var entity in entities)
{
entity.maxParticleSize = 0f;
}
}
}

View File

@@ -1,86 +0,0 @@
using AquaMai.Config.Attributes;
using HarmonyLib;
using Monitor;
using UnityEngine;
namespace AquaMai.Mods.Fancy.GamePlay;
[ConfigSection(
en: """
More detailed judgment display.
Requires CustomSkins to be enabled and the resource file to be downloaded.
https://github.com/hykilpikonna/AquaDX/releases/download/nightly/JudgeDisplay4B.7z
""",
zh: """
更精细的判定表示
需开启 CustomSkins 并下载资源文件
https://github.com/hykilpikonna/AquaDX/releases/download/nightly/JudgeDisplay4B.7z
""")]
public class JudgeDisplay4B
{
// 精确到子判定的自定义判定显示, 需要启用自定义皮肤功能 (理论上不启用自定义皮肤不会崩游戏, 只不过此时这个功能显然不会生效)
[HarmonyPostfix]
[HarmonyPatch(typeof(SlideJudge), "Initialize")]
private static void SlideJudgeDisplay4B(
SpriteRenderer ___SpriteRenderAdd, SpriteRenderer ___SpriteRender,
SlideJudge.SlideJudgeType ____judgeType, SlideJudge.SlideAngle ____angle,
NoteJudge.ETiming judge, float msec, bool isBreak
)
{
var i = isBreak ? 1 : 0;
Sprite sprite = CustomSkins.CustomJudgeSlide[i, (int)____judgeType, (int)____angle, (int)judge];
if (sprite != null) {
___SpriteRender.sprite = sprite;
}
if (isBreak && judge == NoteJudge.ETiming.Critical)
{
sprite = CustomSkins.CustomJudgeSlide[i, (int)____judgeType, (int)____angle, (int) NoteJudge.ETiming.End];
if (sprite != null)
{
___SpriteRenderAdd.sprite = sprite;
}
}
}
[HarmonyPostfix]
[HarmonyPatch(typeof(JudgeGrade), "Initialize")]
private static void JudgeGradeDisplay4B(
SpriteRenderer ___SpriteRender,
NoteJudge.ETiming judge, float msec, NoteJudge.EJudgeType type
)
{
var i = (type == NoteJudge.EJudgeType.Break) ? 1 : 0;
Sprite sprite = CustomSkins.CustomJudge[i, (int)judge];
if (sprite != null) {
___SpriteRender.sprite = sprite;
}
}
[HarmonyPostfix]
[HarmonyPatch(typeof(JudgeGrade), "InitializeBreak")]
private static void JudgeGradeBreakDisplay4B(
SpriteRenderer ___SpriteRenderAdd,
NoteJudge.ETiming judge, float msec, NoteJudge.EJudgeType type
)
{
if (judge == NoteJudge.ETiming.Critical)
{
var sprite = CustomSkins.CustomJudge[1, (int) NoteJudge.ETiming.End];
if (sprite != null)
{
___SpriteRenderAdd.sprite = sprite;
}
}
}
[HarmonyPrefix]
[HarmonyPatch(typeof(JudgeGrade), "InitializeBreak")]
private static void InitializeBreakFix(ref NoteJudge.EJudgeType type)
{
type = NoteJudge.EJudgeType.Break;
}
}

View File

@@ -1,39 +0,0 @@
using AquaMai.Config.Attributes;
using HarmonyLib;
using Manager;
namespace AquaMai.Mods.Fancy.GamePlay;
[ConfigSection(
en: """
Make the AutoPlay random judgment mode really randomize all judgments (down to sub-judgments).
The original random judgment will only produce all 15 judgment results from Miss(TooFast) ~ Critical ~ Miss(TooLate).
Here, it is changed to a triangular distribution to produce all 15 judgment results from Miss(TooFast) ~ Critical ~ Miss(TooLate).
Of course, it will not consider whether the original Note really has a corresponding judgment (such as Slide should not have non-Critical Prefect).
""",
zh: """
让 AutoPlay 的随机判定模式真的会随机产生所有的判定 (精确到子判定)
原本的随机判定只会等概率产生 Critical, LateGreat1st, LateGood, Miss(TooLate)
这里改成三角分布产生从 Miss(TooFast) ~ Critical ~ Miss(TooLate) 的所有 15 种判定结果
当然, 此处并不会考虑原本那个 Note 是不是真的有对应的判定 (比如 Slide 实际上不应该有小 p 之类的)
""")]
public class RealisticRandomJudge
{
// 让 AutoPlay 的随机判定模式真的会随机产生所有的判定 (精确到子判定)
// 原本的随机判定只会等概率产生 Critical, LateGreat1st, LateGood, Miss(TooLate)
// 这里改成三角分布产生从 Miss(TooFast) ~ Critical ~ Miss(TooLate) 的所有 15 种判定结果
// 当然, 此处并不会考虑原本那个 Note 是不是真的有对应的判定 (比如 Slide 实际上不应该有小 p 之类的)
[HarmonyPostfix]
[HarmonyPatch(typeof(GameManager), "AutoJudge")]
private static NoteJudge.ETiming RealAutoJudgeRandom(NoteJudge.ETiming retval)
{
if (GameManager.AutoPlay == GameManager.AutoPlayMode.Random)
{
var x = UnityEngine.Random.Range(0, 8);
x += UnityEngine.Random.Range(0, 8);
return (NoteJudge.ETiming) x;
}
return retval;
}
}

View File

@@ -1,118 +0,0 @@
using System.Collections.Generic;
using AquaMai.Config.Attributes;
using HarmonyLib;
using Manager;
using Monitor;
using Monitor.Game;
using Process;
using UnityEngine;
namespace AquaMai.Mods.Fancy.GamePlay;
[ConfigSection(
en: "Make the Slide Track disappear with an inward-shrinking animation, similar to AstroDX.",
zh: "使 Slide Track 消失时有类似 AstroDX 一样的向内缩入的动画")]
public class SlideArrowAnimation
{
private static List<SpriteRenderer> _animatingSpriteRenderers = [];
[HarmonyTranspiler]
[HarmonyPatch(typeof(SlideRoot), "NoteCheck")]
private static IEnumerable<CodeInstruction> GetUnVisibleColorHook(IEnumerable<CodeInstruction> instructions)
{
var methodGetUnVisibleColor = AccessTools.Method(typeof(SlideRoot), "GetUnVisibleColor");
var oldInstList = new List<CodeInstruction>(instructions);
var newInstList = new List<CodeInstruction>();
for (var i = 0; i < oldInstList.Count; i++)
{
var inst = oldInstList[i];
if (inst.Calls(methodGetUnVisibleColor))
{
// 现在栈上应该有: SpriteRenderer, SlideRoot(this)
// 这一条 IL 会消耗 this, 调用 GetUnVisibleColor(), 推一个 Color 到栈上
// 然后接下来的一条 IL 是调用 SpriteRenderer.color 的 setter 把 SpriteRenderer 和 Color 一起消耗掉
// 我们现在直接用一个 static method 消耗掉 SpriteRenderer 和 this
// 所以要忽略当前 IL, 再忽略下一条 IL, 然后构造一个 Call
// ReSharper disable once ConvertClosureToMethodGroup
var redirect = CodeInstruction.Call((SpriteRenderer r, SlideRoot s) => OnSlideArrowDisable(r, s));
newInstList.Add(redirect);
i++; // 跳过下一条 IL
}
else
{
newInstList.Add(inst);
}
}
return newInstList;
}
public static void OnSlideArrowDisable(SpriteRenderer renderer, SlideRoot slideRoot)
{
_animatingSpriteRenderers.Add(renderer);
}
[HarmonyPostfix]
[HarmonyPatch(typeof(SlideRoot), "SetArrowObject")]
private static void RemoveArrowAnimation(GameObject arrowobj)
{
var spriteRenderer = arrowobj.GetComponent<SpriteRenderer>();
spriteRenderer.transform.localScale = Vector3.one;
if (_animatingSpriteRenderers.Contains(spriteRenderer))
{
_animatingSpriteRenderers.Remove(spriteRenderer);
}
}
[HarmonyPostfix]
[HarmonyPatch(typeof(SlideRoot), "SetBreakArrowObject")]
private static void RemoveBreakArrowAnimation(GameObject breakArrowobj)
{
var breakSlideObj = breakArrowobj.GetComponent<BreakSlide>();
breakSlideObj.SpriteRender.transform.localScale = Vector3.one;
if (_animatingSpriteRenderers.Contains(breakSlideObj.SpriteRender))
{
_animatingSpriteRenderers.Remove(breakSlideObj.SpriteRender);
}
}
[HarmonyPostfix]
[HarmonyPatch(typeof(GameCtrl), "UpdateNotes")]
private static void OnGameCtrlUpdateNotesLast()
{
for (var num = _animatingSpriteRenderers.Count - 1; num >= 0; num--)
{
var spriteRenderer = _animatingSpriteRenderers[num];
if (spriteRenderer == null || !spriteRenderer.gameObject.activeSelf)
{
_animatingSpriteRenderers.RemoveAt(num);
}
else
{
var localScale = spriteRenderer.transform.localScale;
var scale = localScale.y - NotesManager.GetAddMSec() / 150f;
if (scale <= 0)
{
spriteRenderer.transform.localScale = new Vector3(1f, 0f, 1f);
spriteRenderer.color = new Color(1f, 1f, 1f, 0f);
_animatingSpriteRenderers.RemoveAt(num);
}
else
{
localScale.y = scale;
spriteRenderer.color = new Color(1f, 1f, 1f, Mathf.Sqrt(scale));
spriteRenderer.transform.localScale = localScale;
}
}
}
}
[HarmonyPrefix]
[HarmonyPatch(typeof(GameProcess), "SetRelease")]
private static void OnBeforeGameProcessSetRelease()
{
_animatingSpriteRenderers.Clear();
}
}

View File

@@ -1,129 +0,0 @@
using System;
using System.Collections.Generic;
using AquaMai.Config.Attributes;
using HarmonyLib;
using MAI2.Util;
using Manager;
using Monitor;
using UnityEngine;
namespace AquaMai.Mods.Fancy.GamePlay;
[ConfigSection(
zh: "让星星在启动拍等待期间从 50% 透明度渐入为 100%,取代原本在击打星星头时就完成渐入",
en: "Slides will fade in instead of instantly appearing.")]
public class SlideFadeInTweak
{
[HarmonyPrefix]
[HarmonyPatch(typeof(SlideRoot), "UpdateAlpha")]
private static bool UpdateAlphaOverwrite(
SlideRoot __instance,
ref bool ___UpdateAlphaFlag,
float ___StartMsec, float ___AppearMsec, float ___StarLaunchMsec, float ___DefaultMsec,
int ____dispLaneNum, bool ___BreakFlag,
List<SpriteRenderer> ____spriteRenders, List<BreakSlide> ____breakSpriteRenders
)
{
if (!___UpdateAlphaFlag)
return false;
var currentMsec = NotesManager.GetCurrentMsec();
var slideSpeed = (int) Singleton<GamePlayManager>.Instance.GetGameScore(__instance.MonitorId).UserOption.SlideSpeed;
var defaultFadeInLength = (21 - slideSpeed) / 10.5f * ___DefaultMsec;
var fadeInFirstMsec = Math.Max(___StartMsec, ___AppearMsec - defaultFadeInLength);
var fadeInSecondMsec = Math.Max(___AppearMsec, ___StarLaunchMsec - defaultFadeInLength);
// var fadeInSecondMsec = ___AppearMsec;
var color = new Color(1f, 1f, 1f, 1f);
if (currentMsec >= ___StarLaunchMsec)
{
___UpdateAlphaFlag = false;
}
else if (currentMsec < fadeInFirstMsec)
{
color.a = 0.0f;
}
else if (fadeInFirstMsec <= currentMsec && currentMsec < ___AppearMsec)
{
var fadeInLength = Math.Min(200.0f, ___AppearMsec - fadeInFirstMsec);
color.a = 0.5f * Math.Min(1f, (currentMsec - fadeInFirstMsec) / fadeInLength);
}
else if (___AppearMsec <= currentMsec && currentMsec < fadeInSecondMsec)
{
color.a = 0.5f;
}
else if (fadeInSecondMsec <= currentMsec && currentMsec < ___StarLaunchMsec)
{
var fadeInLength = Math.Min(200.0f, ___StarLaunchMsec - fadeInSecondMsec);
// var fadeInLength = ___StarLaunchMsec - fadeInSecondMsec;
color.a = 0.5f + 0.5f * Math.Min(1f, (currentMsec - fadeInSecondMsec) / fadeInLength);
}
if (!___BreakFlag)
{
for (var index = 0; index < ____dispLaneNum; ++index)
{
if (index >= ____spriteRenders.Count) break;
____spriteRenders[index].color = color;
}
}
else
{
for (var index = 0; index < ____dispLaneNum; ++index)
{
if (index >= ____breakSpriteRenders.Count) break;
____breakSpriteRenders[index].SpriteRender.color = color;
}
}
return false;
}
[HarmonyPrefix]
[HarmonyPatch(typeof(SlideFan), "UpdateAlpha")]
private static bool UpdateFanAlphaOverwrite(
SlideRoot __instance,
float ___StartMsec, float ___AppearMsec, float ___StarLaunchMsec, float ___DefaultMsec,
Color ____defaultColor, SpriteRenderer[] ____spriteLines
)
{
var currentMsec = NotesManager.GetCurrentMsec();
var slideSpeed = (int) Singleton<GamePlayManager>.Instance.GetGameScore(__instance.MonitorId).UserOption.SlideSpeed;
var defaultFadeInLength = (21 - slideSpeed) / 10.5f * ___DefaultMsec;
var fadeInFirstMsec = Math.Max(___StartMsec, ___AppearMsec - defaultFadeInLength);
var fadeInSecondMsec = Math.Max(___AppearMsec, ___StarLaunchMsec - defaultFadeInLength);
// var fadeInSecondMsec = ___AppearMsec;
var color = ____defaultColor;
if (currentMsec < fadeInFirstMsec)
{
color.a = 0.0f;
}
else if (fadeInFirstMsec <= currentMsec && currentMsec < ___AppearMsec)
{
var fadeInLength = Math.Min(200.0f, ___AppearMsec - fadeInFirstMsec);
color.a = 0.3f * Math.Min(1f, (currentMsec - fadeInFirstMsec) / fadeInLength);
}
else if (___AppearMsec <= currentMsec && currentMsec < fadeInSecondMsec)
{
color.a = 0.3f;
}
else if (fadeInSecondMsec <= currentMsec && currentMsec < ___StarLaunchMsec)
{
var fadeInLength = Math.Min(200.0f, ___StarLaunchMsec - fadeInSecondMsec);
// var fadeInLength = ___StarLaunchMsec - fadeInSecondMsec;
color.a = 0.3f + 0.3f * Math.Min(1f, (currentMsec - fadeInSecondMsec) / fadeInLength);
}
else
{
color.a = 0.6f;
}
foreach (SpriteRenderer spriteLine in ____spriteLines)
spriteLine.color = color;
return false;
}
}

View File

@@ -1,76 +0,0 @@
using System.Collections.Generic;
using AquaMai.Config.Attributes;
using HarmonyLib;
using Monitor;
using UnityEngine;
namespace AquaMai.Mods.Fancy.GamePlay;
[ConfigSection(
en: """
Invert the Slide hierarchy, so that the new Slide appears on top like Maimai classic.
Enable to support color changing effects achieved by overlaying multiple stars.
""",
zh: """
反转 Slide 层级, 使新出现的 Slide 像旧框一样显示在上层
启用以支持通过叠加多个星星达成的变色效果
""")]
public class SlideLayerReverse
{
[HarmonyPostfix]
[HarmonyPatch(typeof(SlideRoot), "Initialize")]
private static void CalcArrowLayer(
bool ___BreakFlag, List<SpriteRenderer> ____spriteRenders, List<BreakSlide> ____breakSpriteRenders,
int ___SlideIndex, int ____baseArrowSortingOrder
)
{
// 原本的 sortingOrder 是 -(SlideIndex + _baseArrowSortingOrder + index)
// 令 orderBase = SlideIndex + _baseArrowSortingOrder
// 分配给这条 slide 的 sortingOrder 范围是 -(orderBase + count - 1) ~ -(orderBase)
// 现在要保留 slide 内部箭头顺序, 但使得 slide 间次序反转
// 范围会变成 orderBase ~ orderBase + count - 1
// 其中原本是 -(orderBase) 的箭头应该调整为 orderBase + count - 1
var orderBase = ___SlideIndex + ____baseArrowSortingOrder; // SlideIndex + _baseArrowSortingOrder
if (!___BreakFlag)
{
var lastIdx = ____spriteRenders.Count - 1;
for (var index = 0; index < ____spriteRenders.Count; index++)
{
var renderer = ____spriteRenders[index];
renderer.sortingOrder = -32700 + orderBase + lastIdx - index;
}
}
else
{
var lastIdx = ____breakSpriteRenders.Count - 1;
for (var index = 0; index < ____breakSpriteRenders.Count; index++)
{
var breakSlide = ____breakSpriteRenders[index];
breakSlide.SetSortingOrder(-32700 + orderBase + lastIdx - index);
}
}
}
[HarmonyPostfix]
[HarmonyPatch(typeof(SlideFan), "Initialize")]
private static void CalcFanArrowLayer(
SpriteRenderer[] ____spriteLines, SpriteRenderer[] ____effectSprites,
int ___SlideIndex, int ____baseArrowSortingOrder
)
{
var orderBase = ___SlideIndex + ____baseArrowSortingOrder; // SlideIndex + _baseArrowSortingOrder
var lastIdx = ____spriteLines.Length - 1;
for (var index = 0; index < ____spriteLines.Length; index++)
{
var renderer = ____spriteLines[index];
renderer.sortingOrder = -32700 + orderBase + lastIdx - index;
}
lastIdx = ____effectSprites.Length - 1;
for (var index = 0; index < ____effectSprites.Length; index++)
{
var renderer = ____effectSprites[index];
renderer.sortingOrder = 1000 + orderBase + lastIdx - index;
}
}
}

View File

@@ -1,77 +0,0 @@
using AquaMai.Config.Attributes;
using HarmonyLib;
using Process;
using UnityEngine;
namespace AquaMai.Mods.Fancy.GamePlay;
[ConfigSection(
en: """
Delayed the animation of the song start screen.
For recording chart confirmation.
""",
zh: """
推迟了歌曲开始界面的动画
录制谱面确认用
""")]
public class TrackStartProcessTweak
{
// 总之这个 Patch 没啥用, 是我个人用 sinmai 录谱面确认时用得到, 顺手也写进来了
// 具体而言就是推迟了歌曲开始界面的动画便于后期剪辑
[HarmonyPrefix]
[HarmonyPatch(typeof(TrackStartProcess), "OnUpdate")]
private static bool DelayAnimation(
TrackStartProcess.TrackStartSequence ____state,
ref float ____timeCounter,
ProcessDataContainer ___container
)
{
if (____state == TrackStartProcess.TrackStartSequence.Wait)
{
// 将开始动画(就是“噔噔, 噔 噔噔”)推迟
float temp = ____timeCounter + Time.deltaTime;
if (____timeCounter < 1.0f && temp >= 1.0f)
{
// 这是用来让转场动画继续播放的, 原本就是这个时候 notify 的同时开始播放开始动画
// 现在把开始动画往后延
___container.processManager.NotificationFadeIn();
}
____timeCounter = temp;
if (____timeCounter >= 3.0f)
{
return true;
// 原 method 的逻辑是这样
// case TrackStartProcess.TrackStartSequence.Wait:
// this._timeCounter += Time.deltaTime;
// if ((double) this._timeCounter >= 1.0)
// {
// this._timeCounter = 0.0f;
// this._state = TrackStartProcess.TrackStartSequence.Disp;
// /* 一些开始播放开始动画的代码 */
// this.container.processManager.NotificationFadeIn();
// break;
// }
// break;
// 所以只要在 prefix 里面等到 timeCounter 达到我们想要的值以后再执行原 method 就好
// 这里有个细节: NotificationFadeIn() 会被执行两遍, 这其实不好, 是个潜在 bug
// 不过由于此处把开始动画往后推了 2s, 转场动画已经结束把 Process 释放掉了, 所以第二遍会找不到 Process 就没效果
}
return false;
}
else if (____state == TrackStartProcess.TrackStartSequence.DispEnd)
{
// 将开始动画结束以后的转场动画推迟
____timeCounter += Time.deltaTime; // timeCounter 会在先前由原本的 method 归零
if (____timeCounter >= 1.0f)
{
return true;
}
return false;
}
return true;
}
}

View File

@@ -1,18 +0,0 @@
using AquaMai.Config.Attributes;
using HarmonyLib;
using UnityEngine;
namespace AquaMai.Mods.Fancy;
[ConfigSection(
en: "Remove the circle mask of the game screen.",
zh: "移除游戏画面的圆形遮罩")]
public class HideMask
{
[HarmonyPrefix]
[HarmonyPatch(typeof(Main.GameMain), "LateInitialize", typeof(MonoBehaviour), typeof(Transform), typeof(Transform))]
public static void LateInitialize(MonoBehaviour gameMainObject)
{
GameObject.Find("Mask").SetActive(false);
}
}

View File

@@ -1,7 +0,0 @@
# Fancy
All the fancy features, even if not required by most players, are welcomed to this category, whether for personalization, for beautify, for self-made charts or for other uncommon purposes.
These patches may not well-tested by the project maintainers and could be enabled only if you know what you're doing.
Patches affect the gameplay should go to the GamePlay subcategory.

View File

@@ -1,88 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using AquaMai.Config.Attributes;
using AquaMai.Core.Helpers;
using HarmonyLib;
using Mai2.Mai2Cue;
using MAI2.Util;
using Manager;
using MelonLoader;
namespace AquaMai.Mods.Fancy;
[ConfigSection(
en: """
Random BGM.
Put Mai2Cue.{acb,awb} of old version of the game in the configured directory and rename them.
Won't work with 2P mode.
""",
zh: """
在配置的目录下放置了旧版游戏的 Mai2Cue.{acb,awb} 并重命名的话,可以在播放游戏 BGM 的时候随机播放这里面的旧版游戏 BGM
无法在 2P 模式下工作
""")]
public class RandomBgm
{
[ConfigEntry]
private static readonly string mai2CueDir = "LocalAssets/Mai2Cue";
private static List<string> _acbs = new List<string>();
[HarmonyPostfix]
[HarmonyPatch(typeof(SoundManager), "Initialize")]
public static void Init()
{
var resolvedDir = FileSystem.ResolvePath(mai2CueDir);
if (!Directory.Exists(resolvedDir)) return;
var files = Directory.EnumerateFiles(resolvedDir);
foreach (var file in files)
{
if (!file.EndsWith(".acb")) continue;
// Seems there's limit for max opened ACB files
_acbs.Add(Path.ChangeExtension(file, null));
}
MelonLogger.Msg($"Random BGM loaded {_acbs.Count} files");
}
[HarmonyPrefix]
[HarmonyPatch(typeof(SoundManager), "Play")]
public static void PrePlay(ref SoundManager.AcbID acbID, int cueID)
{
if (acbID != SoundManager.AcbID.Default) return;
if (_acbs.Count == 0) return;
var cueIndex = (Cue)cueID;
switch (cueIndex)
{
case Cue.BGM_ENTRY:
case Cue.BGM_COLLECTION:
case Cue.BGM_RESULT_CLEAR:
case Cue.BGM_RESULT:
var acb = _acbs[UnityEngine.Random.Range(0, _acbs.Count)];
acbID = SoundManager.AcbID.Max;
var result = Singleton<SoundCtrl>.Instance.LoadCueSheet((int)acbID, acb);
MelonLogger.Msg($"Picked {acb} for {cueIndex}, result: {result}");
return;
default:
return;
}
}
[HarmonyPrefix]
[HarmonyPatch(typeof(SoundManager), "PlayBGM")]
public static bool PrePlayBGM(ref int target)
{
switch (target)
{
case 0:
return true;
case 1:
return false;
case 2:
target = 0;
return true;
default:
return false;
}
}
}

View File

@@ -1,68 +0,0 @@
using System;
using System.Diagnostics;
using AquaMai.Config.Attributes;
using HarmonyLib;
using Process;
namespace AquaMai.Mods.Fancy;
[ConfigSection(
en: "Triggers for executing commands at certain events.",
zh: "在一定时机执行命令的触发器")]
public class Triggers
{
[ConfigEntry(
en: "Execute some command on game idle.",
zh: """
在游戏闲置的时候执行指定的命令脚本
比如说可以在游戏闲置是降低显示器的亮度
""")]
private static readonly string execOnIdle = "";
[ConfigEntry(
en: "Execute some command on game start.",
zh: "在玩家登录的时候执行指定的命令脚本")]
private static readonly string execOnEntry = "";
[HarmonyPrefix]
[HarmonyPatch(typeof(AdvertiseProcess), "OnStart")]
public static void AdvertiseProcessPreStart()
{
if (!string.IsNullOrWhiteSpace(execOnIdle))
{
Exec(execOnIdle);
}
}
[HarmonyPrefix]
[HarmonyPatch(typeof(EntryProcess), "OnStart")]
public static void EntryProcessPreStart()
{
if (!string.IsNullOrWhiteSpace(execOnEntry))
{
Exec(execOnEntry);
}
}
[HarmonyPrefix]
[HarmonyPatch(typeof(MusicSelectProcess), "OnStart")]
public static void MusicSelectProcessPreStart()
{
if (!string.IsNullOrWhiteSpace(execOnEntry))
{
Exec(execOnEntry);
}
}
private static void Exec(string command)
{
var process = new System.Diagnostics.Process();
process.StartInfo.FileName = "cmd.exe";
process.StartInfo.Arguments = "/c " + command;
process.StartInfo.UseShellExecute = true;
process.StartInfo.WindowStyle = ProcessWindowStyle.Hidden;
process.StartInfo.WorkingDirectory = Environment.CurrentDirectory;
process.Start();
}
}

View File

@@ -1,139 +0,0 @@
using System.Net;
using HarmonyLib;
using Manager;
using Net;
using UnityEngine;
using AquaMai.Config.Attributes;
using AquaMai.Core.Attributes;
namespace AquaMai.Mods.Fix;
[ConfigSection(exampleHidden: true, defaultOn: true)]
public class Common
{
[ConfigEntry]
private readonly static bool preventIniFileClear = true;
[EnableIf(nameof(preventIniFileClear))]
[HarmonyPrefix]
[HarmonyPatch(typeof(MAI2System.IniFile), "clear")]
private static bool PreIniFileClear()
{
return false;
}
[ConfigEntry]
private readonly static bool fixDebugInput = true;
[EnableIf(nameof(fixDebugInput))]
[HarmonyPrefix]
[HarmonyPatch(typeof(DebugInput), "GetKey")]
private static bool GetKey(ref bool __result, KeyCode name)
{
__result = UnityEngine.Input.GetKey(name);
return false;
}
[EnableIf(nameof(fixDebugInput))]
[HarmonyPrefix]
[HarmonyPatch(typeof(DebugInput), "GetKeyDown")]
private static bool GetKeyDown(ref bool __result, KeyCode name)
{
__result = UnityEngine.Input.GetKeyDown(name);
return false;
}
[EnableIf(nameof(fixDebugInput))]
[HarmonyPrefix]
[HarmonyPatch(typeof(DebugInput), "GetMouseButton")]
private static bool GetMouseButton(ref bool __result, int button)
{
__result = UnityEngine.Input.GetMouseButton(button);
return false;
}
[EnableIf(nameof(fixDebugInput))]
[HarmonyPrefix]
[HarmonyPatch(typeof(DebugInput), "GetMouseButtonDown")]
private static bool GetMouseButtonDown(ref bool __result, int button)
{
__result = UnityEngine.Input.GetMouseButtonDown(button);
return false;
}
[ConfigEntry]
private readonly static bool bypassCakeHashCheck = true;
[EnableIf(nameof(bypassCakeHashCheck))]
[HarmonyPostfix]
[HarmonyPatch(typeof(NetHttpClient), MethodType.Constructor)]
private static void OnNetHttpClientConstructor(NetHttpClient __instance)
{
// Bypass Cake.dll hash check
var tInstance = Traverse.Create(__instance).Field("isTrueDll");
if (tInstance.FieldExists())
{
tInstance.SetValue(true);
}
}
[ConfigEntry]
private readonly static bool restoreCertificateValidation = true;
[EnableIf(nameof(restoreCertificateValidation))]
[HarmonyPostfix]
[HarmonyPatch(typeof(NetHttpClient), "Create")]
private static void OnNetHttpClientCreate()
{
// Unset the certificate validation callback (SSL pinning) to restore the default behavior
ServicePointManager.ServerCertificateValidationCallback = null;
}
[ConfigEntry]
private readonly static bool forceNonTarget = true;
[EnableIf(nameof(forceNonTarget))]
[HarmonyPrefix]
[HarmonyPatch(typeof(MAI2System.Config), "IsTarget", MethodType.Getter)]
private static bool PreIsTarget(ref bool __result)
{
// Who is teaching others to set `Target = 1`?!
__result = false;
return false;
}
[ConfigEntry]
private readonly static bool forceIgnoreError = true;
[EnableIf(nameof(forceIgnoreError))]
[HarmonyPrefix]
[HarmonyPatch(typeof(MAI2System.Config), "IsIgnoreError", MethodType.Getter)]
private static bool PreIsIgnoreError(ref bool __result)
{
__result = true;
return false;
}
[ConfigEntry]
private readonly static bool bypassSpecialNumCheck = true;
public static void OnAfterPatch(HarmonyLib.Harmony h)
{
if (bypassSpecialNumCheck)
{
if (typeof(GameManager).GetMethod("CalcSpecialNum") is null) return;
h.PatchAll(typeof(CalcSpecialNumPatch));
}
}
private class CalcSpecialNumPatch
{
[HarmonyPrefix]
[HarmonyPatch(typeof(GameManager), "CalcSpecialNum")]
private static bool CalcSpecialNum(ref int __result)
{
__result = 1024;
return false;
}
}
}

View File

@@ -1,212 +0,0 @@
using System;
using System.Reflection;
using AquaMai.Config.Attributes;
using HarmonyLib;
using MAI2.Util;
using Manager;
using MelonLoader;
using Monitor;
using Process;
using UnityEngine;
namespace AquaMai.Mods.Fix;
[ConfigSection(exampleHidden: true, defaultOn: true)]
public class DebugFeature
{
public static bool IsPolyfill { get; private set; }
private static GameProcess _gameProcess;
private static MovieController _gameMovie;
private static GameMonitor[] _monitors;
private static object _debugFeatureOriginal;
private static System.Type _debugFeatureType;
[HarmonyPatch(typeof(GameProcess), "OnStart")]
[HarmonyPostfix]
public static void Init(GameProcess __instance, MovieController ____gameMovie, GameMonitor[] ____monitors)
{
_gameProcess = __instance;
_gameMovie = ____gameMovie;
_monitors = ____monitors;
PolyFill.timer = 0;
}
public static void OnBeforePatch(HarmonyLib.Harmony h)
{
var original = typeof(GameProcess).GetField("debugFeature", BindingFlags.NonPublic | BindingFlags.Instance);
if (original is null)
{
MelonLogger.Msg(" > [DebugFeature] Running Polyfill");
IsPolyfill = true;
h.PatchAll(typeof(PolyFill));
}
else
{
MelonLogger.Msg(" > [DebugFeature] Already included");
_debugFeatureType = typeof(GameProcess).GetNestedType("DebugFeature", BindingFlags.Instance | BindingFlags.NonPublic);
h.PatchAll(typeof(GetOriginal));
}
}
public static bool Pause
{
get
{
if (IsPolyfill)
{
return PolyFill.isPause;
}
return (bool)_debugFeatureType.GetField("_debugPause", BindingFlags.Instance | BindingFlags.Public).GetValue(_debugFeatureOriginal);
}
set
{
if (IsPolyfill)
{
PolyFill.isPause = value;
}
else
{
_debugFeatureType.GetField("_debugPause", BindingFlags.Instance | BindingFlags.Public).SetValue(_debugFeatureOriginal, value);
}
SoundManager.PauseMusic(value);
_gameMovie.Pause(value);
NotesManager.Pause(value);
}
}
public static void Seek(int msec)
{
Singleton<GamePlayManager>.Instance.Initialize();
if (IsPolyfill)
{
PolyFill.DebugTimeSkip(msec);
}
else
{
_debugFeatureType.GetMethod("DebugTimeSkip", BindingFlags.Instance | BindingFlags.Public).Invoke(_debugFeatureOriginal, new object[] { msec });
}
}
public static double CurrentPlayMsec
{
[Obsolete("不要用它,它有问题。用 PracticeMode.CurrentPlayMsec")]
get
{
if (IsPolyfill)
{
return PolyFill.timer;
}
return (double)_debugFeatureType.GetField("_debugTimer", BindingFlags.Instance | BindingFlags.Public).GetValue(_debugFeatureOriginal);
}
set
{
if (IsPolyfill)
{
PolyFill.timer = value;
}
else
{
_debugFeatureType.GetField("_debugTimer", BindingFlags.Instance | BindingFlags.Public).SetValue(_debugFeatureOriginal, value);
}
Seek(0);
}
}
private static class GetOriginal
{
[HarmonyPatch(typeof(GameProcess), "OnStart")]
[HarmonyPostfix]
public static void Postfix(object ___debugFeature)
{
_debugFeatureOriginal = ___debugFeature;
}
}
private static class PolyFill
{
public static bool isPause;
public static double timer;
public static void DebugTimeSkip(int addMsec)
{
_gameMovie.Pause(pauseFlag: true);
NotesManager.Pause(true);
if (addMsec >= 0)
{
timer += addMsec;
}
else
{
timer = timer + addMsec >= 0.0 ? timer + addMsec : 0.0;
}
_gameMovie.SetSeekFrame(timer);
SoundManager.SeekMusic((int)timer);
for (int i = 0; i < _monitors.Length; i++)
{
_monitors[i].Seek((int)timer);
}
// magic number, dont know why
NotesManager.StartPlay((int)timer + 91);
NotesManager.Pause(isPause);
if (!isPause)
{
SoundManager.PauseMusic(pause: false);
_gameMovie.Pause(pauseFlag: false);
}
else
{
_gameMovie.Pause(pauseFlag: true);
}
_gameProcess.UpdateNotes();
}
[HarmonyPatch(typeof(GameProcess), "OnUpdate")]
[HarmonyPostfix]
public static void Postfix(byte ____sequence)
{
if (____sequence != 4) return;
// GameSequence.Play
if (!isPause)
{
timer += GameManager.GetGameMSecAddD();
}
if (Input.GetKeyDown(KeyCode.Home))
{
GameManager.AutoPlay = (GameManager.AutoPlayMode)((int)(GameManager.AutoPlay + 1) % Enum.GetNames(typeof(GameManager.AutoPlayMode)).Length);
}
else if (Input.GetKeyDown(KeyCode.Return))
{
isPause = !isPause;
SoundManager.PauseMusic(isPause);
_gameMovie.Pause(isPause);
NotesManager.Pause(isPause);
}
else if (DebugInput.GetKeyDown(KeyCode.LeftArrow) || DebugInput.GetKeyDown(KeyCode.RightArrow))
{
var num23 = 0;
if (DebugInput.GetKeyDown(KeyCode.LeftArrow))
{
num23 = -1000;
}
if (DebugInput.GetKeyDown(KeyCode.RightArrow))
{
num23 = 1000;
}
int addMsec = ((!DebugInput.GetKey(KeyCode.LeftShift) && !DebugInput.GetKey(KeyCode.RightShift)) ? ((!DebugInput.GetKey(KeyCode.LeftControl) && !DebugInput.GetKey(KeyCode.RightControl)) ? num23 : (num23 * 10)) : (num23 * 5));
Singleton<GamePlayManager>.Instance.Initialize();
DebugTimeSkip(addMsec);
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More