mirror of
https://github.com/MewoLab/AquaDX.git
synced 2026-02-05 03:07:27 +08:00
[RF] AquaMai moved to new repo
This commit is contained in:
72
.github/workflows/aquamai.yaml
vendored
72
.github/workflows/aquamai.yaml
vendored
@@ -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
374
AquaMai/.gitignore
vendored
@@ -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
|
|
||||||
@@ -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>
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
@@ -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;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
using Mono.Cecil;
|
|
||||||
|
|
||||||
namespace AquaMai.Config.HeadlessLoader;
|
|
||||||
|
|
||||||
public class CustomAssemblyResolver : DefaultAssemblyResolver
|
|
||||||
{
|
|
||||||
public new void RegisterAssembly(AssemblyDefinition assembly)
|
|
||||||
{
|
|
||||||
base.RegisterAssembly(assembly);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
namespace System.Runtime.CompilerServices
|
|
||||||
{
|
|
||||||
internal static class IsExternalInit {}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
namespace AquaMai.Config.Interfaces;
|
|
||||||
|
|
||||||
public interface IConfigComment
|
|
||||||
{
|
|
||||||
string CommentEn { get; init; }
|
|
||||||
string CommentZh { get; init; }
|
|
||||||
public string GetLocalized(string lang);
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
namespace AquaMai.Config.Interfaces;
|
|
||||||
|
|
||||||
public interface IConfigEntryAttribute
|
|
||||||
{
|
|
||||||
IConfigComment Comment { get; }
|
|
||||||
bool HideWhenDefault { get; }
|
|
||||||
}
|
|
||||||
@@ -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; }
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
namespace AquaMai.Config.Interfaces;
|
|
||||||
|
|
||||||
public interface IConfigSectionAttribute
|
|
||||||
{
|
|
||||||
IConfigComment Comment { get; }
|
|
||||||
bool ExampleHidden { get; }
|
|
||||||
bool DefaultOn { get; }
|
|
||||||
bool AlwaysEnabled { get; }
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
namespace System.Runtime.CompilerServices
|
|
||||||
{
|
|
||||||
internal static class IsExternalInit {}
|
|
||||||
}
|
|
||||||
@@ -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";
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
@@ -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
|
|
||||||
{}
|
|
||||||
@@ -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}")
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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)}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
namespace System.Runtime.CompilerServices
|
|
||||||
{
|
|
||||||
internal static class IsExternalInit {}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,
|
|
||||||
}
|
|
||||||
@@ -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,
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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, "游戏功能键需要单独处理")
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
361
AquaMai/AquaMai.Core/Resources/Locale.Designer.cs
generated
361
AquaMai/AquaMai.Core/Resources/Locale.Designer.cs
generated
@@ -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+ => 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 <<.
|
|
||||||
/// </summary>
|
|
||||||
public static string SeekBackward {
|
|
||||||
get {
|
|
||||||
return ResourceManager.GetString("SeekBackward", resourceCulture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Looks up a localized string similar to Seek >>.
|
|
||||||
/// </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 > 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 <<</value>
|
|
||||||
</data>
|
|
||||||
<data name="SeekForward" xml:space="preserve">
|
|
||||||
<value>Seek >></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+ => DXRating += {0}</value>
|
|
||||||
</data>
|
|
||||||
<data name="Skip" xml:space="preserve">
|
|
||||||
<value>Skip</value>
|
|
||||||
</data>
|
|
||||||
<data name="SkipIncompatiblePatch" xml:space="preserve">
|
|
||||||
<value>> 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>
|
|
||||||
@@ -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>倒退 <<</value>
|
|
||||||
</data>
|
|
||||||
<data name="SeekForward" xml:space="preserve">
|
|
||||||
<value>快进 >></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>> 已跳过加载不兼容的功能: {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>
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
@@ -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.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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.
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
Reference in New Issue
Block a user