mirror of
https://github.com/MewoLab/AquaDX.git
synced 2026-02-16 05:37:28 +08:00
[RF] AquaMai configuration refactor (#82)
更新了配置文件格式,原有的配置文件将被自动无缝迁移,详情请见新的配置文件中的注释(例外:`SlideJudgeTweak` 不再默认启用) 旧配置文件将被重命名备份,如果更新到此版本遇到 Bug 请联系我们 Updated configuration file schema. The old config file will be migrated automatically and seamlessly. See the comments in the new configuration file for details. (Except for `SlideJudgeTweak` is no longer enabled by default) Your old configuration file will be renamed as a backup. If you encounter any bug with this version, please contact us.
This commit is contained in:
127
AquaMai/AquaMai.Mods/AquaMai.Mods.csproj
Normal file
127
AquaMai/AquaMai.Mods/AquaMai.Mods.csproj
Normal file
@@ -0,0 +1,127 @@
|
||||
<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.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>
|
||||
29
AquaMai/AquaMai.Mods/DeprecationWarning.cs
Normal file
29
AquaMai/AquaMai.Mods/DeprecationWarning.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
73
AquaMai/AquaMai.Mods/Fancy/CustomLogo.cs
Normal file
73
AquaMai/AquaMai.Mods/Fancy/CustomLogo.cs
Normal file
@@ -0,0 +1,73 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
45
AquaMai/AquaMai.Mods/Fancy/CustomPlaceName.cs
Normal file
45
AquaMai/AquaMai.Mods/Fancy/CustomPlaceName.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
402
AquaMai/AquaMai.Mods/Fancy/CustomSkins.cs
Normal file
402
AquaMai/AquaMai.Mods/Fancy/CustomSkins.cs
Normal file
@@ -0,0 +1,402 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
78
AquaMai/AquaMai.Mods/Fancy/CustomTrackStartDiff.cs
Normal file
78
AquaMai/AquaMai.Mods/Fancy/CustomTrackStartDiff.cs
Normal file
@@ -0,0 +1,78 @@
|
||||
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];
|
||||
}
|
||||
}
|
||||
}
|
||||
30
AquaMai/AquaMai.Mods/Fancy/CustomVersionString.cs
Normal file
30
AquaMai/AquaMai.Mods/Fancy/CustomVersionString.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
34
AquaMai/AquaMai.Mods/Fancy/DemoMaster.cs
Normal file
34
AquaMai/AquaMai.Mods/Fancy/DemoMaster.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
33
AquaMai/AquaMai.Mods/Fancy/GamePlay/BreakSlideJudgeBlink.cs
Normal file
33
AquaMai/AquaMai.Mods/Fancy/GamePlay/BreakSlideJudgeBlink.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,433 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,473 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
50
AquaMai/AquaMai.Mods/Fancy/GamePlay/DisableTrackStartTabs.cs
Normal file
50
AquaMai/AquaMai.Mods/Fancy/GamePlay/DisableTrackStartTabs.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
129
AquaMai/AquaMai.Mods/Fancy/GamePlay/ExtendNotesPool.cs
Normal file
129
AquaMai/AquaMai.Mods/Fancy/GamePlay/ExtendNotesPool.cs
Normal file
@@ -0,0 +1,129 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
42
AquaMai/AquaMai.Mods/Fancy/GamePlay/FanJudgeFlip.cs
Normal file
42
AquaMai/AquaMai.Mods/Fancy/GamePlay/FanJudgeFlip.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
24
AquaMai/AquaMai.Mods/Fancy/GamePlay/HideHanabi.cs
Normal file
24
AquaMai/AquaMai.Mods/Fancy/GamePlay/HideHanabi.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
86
AquaMai/AquaMai.Mods/Fancy/GamePlay/JudgeDisplay4B.cs
Normal file
86
AquaMai/AquaMai.Mods/Fancy/GamePlay/JudgeDisplay4B.cs
Normal file
@@ -0,0 +1,86 @@
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
39
AquaMai/AquaMai.Mods/Fancy/GamePlay/RealisticRandomJudge.cs
Normal file
39
AquaMai/AquaMai.Mods/Fancy/GamePlay/RealisticRandomJudge.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
118
AquaMai/AquaMai.Mods/Fancy/GamePlay/SlideArrowAnimation.cs
Normal file
118
AquaMai/AquaMai.Mods/Fancy/GamePlay/SlideArrowAnimation.cs
Normal file
@@ -0,0 +1,118 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
129
AquaMai/AquaMai.Mods/Fancy/GamePlay/SlideFadeInTweak.cs
Normal file
129
AquaMai/AquaMai.Mods/Fancy/GamePlay/SlideFadeInTweak.cs
Normal file
@@ -0,0 +1,129 @@
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
76
AquaMai/AquaMai.Mods/Fancy/GamePlay/SlideLayerReverse.cs
Normal file
76
AquaMai/AquaMai.Mods/Fancy/GamePlay/SlideLayerReverse.cs
Normal file
@@ -0,0 +1,76 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
18
AquaMai/AquaMai.Mods/Fancy/HideMask.cs
Normal file
18
AquaMai/AquaMai.Mods/Fancy/HideMask.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
7
AquaMai/AquaMai.Mods/Fancy/README.md
Normal file
7
AquaMai/AquaMai.Mods/Fancy/README.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# 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.
|
||||
88
AquaMai/AquaMai.Mods/Fancy/RandomBgm.cs
Normal file
88
AquaMai/AquaMai.Mods/Fancy/RandomBgm.cs
Normal file
@@ -0,0 +1,88 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
68
AquaMai/AquaMai.Mods/Fancy/Triggers.cs
Normal file
68
AquaMai/AquaMai.Mods/Fancy/Triggers.cs
Normal file
@@ -0,0 +1,68 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
139
AquaMai/AquaMai.Mods/Fix/Common.cs
Normal file
139
AquaMai/AquaMai.Mods/Fix/Common.cs
Normal file
@@ -0,0 +1,139 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
212
AquaMai/AquaMai.Mods/Fix/DebugFeature.cs
Normal file
212
AquaMai/AquaMai.Mods/Fix/DebugFeature.cs
Normal file
@@ -0,0 +1,212 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
87
AquaMai/AquaMai.Mods/Fix/DisableReboot.cs
Normal file
87
AquaMai/AquaMai.Mods/Fix/DisableReboot.cs
Normal file
@@ -0,0 +1,87 @@
|
||||
using AquaMai.Config.Attributes;
|
||||
using HarmonyLib;
|
||||
using Manager.Operation;
|
||||
|
||||
namespace AquaMai.Mods.Fix;
|
||||
|
||||
[ConfigSection(exampleHidden: true, defaultOn: true)]
|
||||
public class DisableReboot
|
||||
{
|
||||
// IsAutoRebootNeeded
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(MaintenanceTimer), "IsAutoRebootNeeded")]
|
||||
public static bool IsAutoRebootNeeded(ref bool __result)
|
||||
{
|
||||
__result = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
// IsUnderServerMaintenance
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(MaintenanceTimer), "IsUnderServerMaintenance")]
|
||||
public static bool IsUnderServerMaintenance(ref bool __result)
|
||||
{
|
||||
__result = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
// RemainingMinutes
|
||||
// Original: private int RemainingMinutes => (this._secServerMaintenance + 59) / 60;
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(MaintenanceTimer), "RemainingMinutes", MethodType.Getter)]
|
||||
public static bool RemainingMinutes(ref int __result)
|
||||
{
|
||||
__result = 600;
|
||||
return false;
|
||||
}
|
||||
|
||||
// GetAutoRebootSec
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(MaintenanceTimer), "GetAutoRebootSec")]
|
||||
public static bool GetAutoRebootSec(ref int __result)
|
||||
{
|
||||
__result = 60 * 60 * 10;
|
||||
return false;
|
||||
}
|
||||
|
||||
// GetServerMaintenanceSec
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(MaintenanceTimer), "GetServerMaintenanceSec")]
|
||||
public static bool GetServerMaintenanceSec(ref int __result)
|
||||
{
|
||||
__result = 60 * 60 * 10;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Execute
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(MaintenanceTimer), "Execute")]
|
||||
public static bool Execute(MaintenanceTimer __instance)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// UpdateTimes
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(MaintenanceTimer), "UpdateTimes")]
|
||||
public static bool UpdateTimes(MaintenanceTimer __instance)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(ClosingTimer), "IsShowRemainingMinutes")]
|
||||
public static bool IsShowRemainingMinutes(ref bool __result)
|
||||
{
|
||||
__result = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(ClosingTimer), "IsClosed")]
|
||||
public static bool IsClosed(ref bool __result)
|
||||
{
|
||||
__result = false;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
21
AquaMai/AquaMai.Mods/Fix/FixCheckAuth.cs
Normal file
21
AquaMai/AquaMai.Mods/Fix/FixCheckAuth.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using AMDaemon.Allnet;
|
||||
using AquaMai.Config.Attributes;
|
||||
using HarmonyLib;
|
||||
using Manager;
|
||||
using Manager.Operation;
|
||||
|
||||
namespace AquaMai.Mods.Fix;
|
||||
|
||||
[ConfigSection(exampleHidden: true, defaultOn: true)]
|
||||
public class FixCheckAuth
|
||||
{
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(OperationManager), "CheckAuth_Proc")]
|
||||
private static void PostCheckAuthProc(ref OperationData ____operationData)
|
||||
{
|
||||
if (Auth.GameServerUri.StartsWith("http://") || Auth.GameServerUri.StartsWith("https://"))
|
||||
{
|
||||
____operationData.ServerUri = Auth.GameServerUri;
|
||||
}
|
||||
}
|
||||
}
|
||||
66
AquaMai/AquaMai.Mods/Fix/FixConnSlide.cs
Normal file
66
AquaMai/AquaMai.Mods/Fix/FixConnSlide.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using AquaMai.Core.Attributes;
|
||||
using AquaMai.Config.Attributes;
|
||||
using HarmonyLib;
|
||||
using Manager;
|
||||
|
||||
namespace AquaMai.Mods.Fix.Legacy;
|
||||
|
||||
[ConfigSection(exampleHidden: true, defaultOn: true)]
|
||||
[EnableGameVersion(23000)]
|
||||
public class FixConnSlide
|
||||
{
|
||||
/* 这个 Patch 用于修复以下 bug:
|
||||
* 非 ConnSlide 被错误解析为 ConnSlide (Fes 首日刹那旅程爆机 bug)
|
||||
* 原 method 逻辑如下:
|
||||
*
|
||||
* if (this.IsSlideAll(noteData1.type) && (index1 + 1 < this._note._noteData.Count ? 1 : 0) != 0)
|
||||
* {
|
||||
* int targetNote = noteData1.slideData.targetNote;
|
||||
* if (noteData1.slideData != null)
|
||||
* targetNote = noteData1.slideData.targetNote;
|
||||
* for (int index3 = index1; index3 < this._note._noteData.Count; ++index3)
|
||||
* {
|
||||
* NoteData noteData3 = this._note._noteData[index3];
|
||||
* if (this.IsSlideAll(noteData3.type) && noteData3.time == noteData1.end && noteData3.startButtonPos == targetNote && noteData3.parent == null)
|
||||
* {
|
||||
* noteData3.parent = noteData1.parent;
|
||||
* noteData1.child.Add(noteData3);
|
||||
* noteData3.isUsed = true;
|
||||
* noteData3.isJudged = true;
|
||||
* break;
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* 修复 bug 需要把第二次调用 this.IsSlideAll() 更改为 this.IsConnectNote(), 这里使用 Transpiler 解决
|
||||
*/
|
||||
[HarmonyTranspiler]
|
||||
[HarmonyPatch(typeof(NotesReader), "calcSlide")]
|
||||
private static IEnumerable<CodeInstruction> Fix(IEnumerable<CodeInstruction> instructions)
|
||||
{
|
||||
List<CodeInstruction> instList = new List<CodeInstruction>(instructions);
|
||||
bool found = false;
|
||||
MethodInfo methodIsSlideAll = AccessTools.Method(typeof(NotesReader), "IsSlideAll");
|
||||
MethodInfo methodIsConnectNote = AccessTools.Method(typeof(NotesReader), "IsConnectNote");
|
||||
|
||||
for (int i = 0; i < instList.Count; i++)
|
||||
{
|
||||
CodeInstruction inst = instList[i];
|
||||
if (!found && inst.Calls(methodIsSlideAll))
|
||||
{
|
||||
found = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (found && inst.Calls(methodIsSlideAll))
|
||||
{
|
||||
inst.operand = methodIsConnectNote;
|
||||
// MelonLogger.Msg($"[FixConnSlide] Successfully patched NotesReader::calcSlide");
|
||||
break;
|
||||
}
|
||||
}
|
||||
return instList;
|
||||
}
|
||||
}
|
||||
80
AquaMai/AquaMai.Mods/Fix/FixLevelDisplay.cs
Normal file
80
AquaMai/AquaMai.Mods/Fix/FixLevelDisplay.cs
Normal file
@@ -0,0 +1,80 @@
|
||||
using AquaMai.Core.Attributes;
|
||||
using AquaMai.Config.Attributes;
|
||||
using HarmonyLib;
|
||||
using MAI2.Util;
|
||||
using Manager;
|
||||
using Monitor;
|
||||
using Monitor.MusicSelect.ChainList;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AquaMai.Mods.Fix;
|
||||
|
||||
[EnableGameVersion(24000)]
|
||||
[ConfigSection(exampleHidden: true, defaultOn: true)]
|
||||
public class FixLevelDisplay
|
||||
{
|
||||
// Fix wrong position of level number's display for music levels with non-consistant display level and rate level (difficuly constant)
|
||||
// Stock game charts have no such inconsistency, but custom charts may have (e.g. 10+ but unrated)
|
||||
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(MusicChainCardObejct), "SetLevel")]
|
||||
private static void FixLevelShiftMusicChainCardObejct(MusicLevelID levelID, SpriteCounter ____digitLevel, SpriteCounter ____doubleDigitLevel, bool utage, GameObject ____difficultyUtageQuesionMarkSingleDigit, GameObject ____difficultyUtageQuesionMarkDoubleDigit)
|
||||
{
|
||||
switch (levelID)
|
||||
{
|
||||
case > MusicLevelID.Level9P:
|
||||
____digitLevel.gameObject.SetActive(value: false);
|
||||
____doubleDigitLevel.gameObject.SetActive(value: true);
|
||||
____doubleDigitLevel.ChangeText(levelID.GetLevelNum().PadRight(3));
|
||||
break;
|
||||
case >= MusicLevelID.None:
|
||||
____digitLevel.gameObject.SetActive(value: true);
|
||||
____doubleDigitLevel.gameObject.SetActive(value: false);
|
||||
____digitLevel.ChangeText(levelID.GetLevelNum().PadRight(2));
|
||||
break;
|
||||
}
|
||||
|
||||
if (!utage) return;
|
||||
switch (levelID)
|
||||
{
|
||||
case > MusicLevelID.Level9P:
|
||||
____difficultyUtageQuesionMarkSingleDigit.SetActive(value: false);
|
||||
____difficultyUtageQuesionMarkDoubleDigit.SetActive(value: true);
|
||||
break;
|
||||
case >= MusicLevelID.None:
|
||||
____difficultyUtageQuesionMarkSingleDigit.SetActive(value: true);
|
||||
____difficultyUtageQuesionMarkDoubleDigit.SetActive(value: false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(SingleResultCardController), "SetLevel")]
|
||||
private static void FixLevelShiftSingleResultCardController(MusicLevelID levelID, bool isUtage, ref SpriteCounter ____difficultySingle, ref SpriteCounter ____difficultyDouble, GameObject ____utageQuestionMarkSingleDigit, GameObject ____utageQuestionMarkDoubleDigit)
|
||||
{
|
||||
FixLevelShiftMusicChainCardObejct(levelID, ____difficultySingle, ____difficultyDouble, isUtage, ____utageQuestionMarkSingleDigit, ____utageQuestionMarkDoubleDigit);
|
||||
}
|
||||
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(TotalResultPlayer), "SetLevel")]
|
||||
private static void FixLevelShiftTotalResultPlayer(MusicLevelID levelID, bool isUtage, ref SpriteCounter ____difficultySingle, ref SpriteCounter ____difficultyDouble, GameObject ____utageQuestionMarkSingleDigit, GameObject ____utageQuestionMarkDoubleDigit)
|
||||
{
|
||||
FixLevelShiftMusicChainCardObejct(levelID, ____difficultySingle, ____difficultyDouble, isUtage, ____utageQuestionMarkSingleDigit, ____utageQuestionMarkDoubleDigit);
|
||||
}
|
||||
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(ResultMonitor), "SetLevel")]
|
||||
private static void FixLevelShiftTotalResultPlayer(MusicLevelID levelID, ref SpriteCounter ____difficultySingle, ref SpriteCounter ____difficultyDouble)
|
||||
{
|
||||
FixLevelShiftMusicChainCardObejct(levelID, ____difficultySingle, ____difficultyDouble, false, null, null);
|
||||
}
|
||||
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(TrackStartMonitor), "SetTrackStart")]
|
||||
private static void FixLevelShiftTrackStartMonitor(int ___monitorIndex, ref SpriteCounter ____difficultySingle, ref SpriteCounter ____difficultyDouble, GameObject ____utageQuestionSingleDigit, GameObject ____utageQuestionDoubleDigit)
|
||||
{
|
||||
var music = Singleton<DataManager>.Instance.GetMusic(GameManager.SelectMusicID[___monitorIndex]);
|
||||
var levelID = (MusicLevelID)music.notesData[GameManager.SelectDifficultyID[___monitorIndex]].musicLevelID;
|
||||
FixLevelShiftMusicChainCardObejct(levelID, ____difficultySingle, ____difficultyDouble, music.name.id >= 100000, ____utageQuestionSingleDigit, ____utageQuestionDoubleDigit);
|
||||
}
|
||||
}
|
||||
104
AquaMai/AquaMai.Mods/Fix/FixSlideAutoPlay.cs
Normal file
104
AquaMai/AquaMai.Mods/Fix/FixSlideAutoPlay.cs
Normal file
@@ -0,0 +1,104 @@
|
||||
using System.Collections.Generic;
|
||||
using AquaMai.Config.Attributes;
|
||||
using HarmonyLib;
|
||||
using Manager;
|
||||
using Monitor;
|
||||
|
||||
namespace AquaMai.Mods.Fix;
|
||||
|
||||
[ConfigSection(exampleHidden: true, defaultOn: true)]
|
||||
public class FixSlideAutoPlay
|
||||
{
|
||||
/* 这个 Patch 用于修复以下 bug:
|
||||
* SlideFan 在 AutoPlay 时, 只有第一个箭头会消失
|
||||
* 原 method 逻辑如下:
|
||||
*
|
||||
* if (this.IsNoteCheckTimeStartIgnoreJudgeWait())
|
||||
* {
|
||||
* // do something ...
|
||||
* if (!GameManager.IsAutoPlay())
|
||||
* {
|
||||
* // do something ...
|
||||
* for (int index = 0; index < this._arrowPrefubs.Length && (double) index < (double) num2 * 11.0; ++index)
|
||||
* {
|
||||
* // do something about displaying arrows ...
|
||||
* }
|
||||
* }
|
||||
* else
|
||||
* {
|
||||
* float num4 = (currentMsec - this.StarLaunchMsec) / (this.StarArriveMsec - this.StarLaunchMsec - this.lastWaitTime);
|
||||
* for (int index = 0; index < this._arrowPrefubs.Length && (double) index < (double) num4 * 1.0; ++index)
|
||||
* {
|
||||
* // do something about displaying arrows ...
|
||||
* }
|
||||
* if ((double) num4 > 1.0)
|
||||
* num1 = 3;
|
||||
* }
|
||||
* // do something ...
|
||||
* }
|
||||
*
|
||||
* 导致这个 bug 的原因是 else 分支的 for 循环终止条件写错了, 应该是 11.0 (因为有 11 个箭头), SBGA 写成了 1.0
|
||||
* 这个 method 中一共只有 5 处 ldc.r4 的 IL Code, 依次为 10.0, 11.0, 1.0, 1.0, 0.0
|
||||
* 修复 bug 需要把第三处的 1.0 更改为 11.0, 这里使用 Transpiler 解决
|
||||
*/
|
||||
[HarmonyTranspiler]
|
||||
[HarmonyPatch(typeof(SlideFan), "NoteCheck")]
|
||||
private static IEnumerable<CodeInstruction> FixFanAutoPlayArrow(IEnumerable<CodeInstruction> instructions)
|
||||
{
|
||||
List<CodeInstruction> instList = new List<CodeInstruction>(instructions);
|
||||
bool found = false;
|
||||
for (int i = 0; i < instList.Count; i++)
|
||||
{
|
||||
CodeInstruction inst = instList[i];
|
||||
if (inst.LoadsConstant(11.0))
|
||||
{
|
||||
found = true;
|
||||
}
|
||||
|
||||
if (found && inst.LoadsConstant(1.0))
|
||||
{
|
||||
inst.operand = 11.0f;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return instList;
|
||||
}
|
||||
|
||||
/* 这个 Patch 让 Slide 在 AutoPlay 的时候, 每个区仍然会分按下和松开两段进行推进 (加上 this._hitIn 的变化)
|
||||
* 原 method 逻辑如下:
|
||||
*
|
||||
* if (!GameManager.IsAutoPlay())
|
||||
* {
|
||||
* // do somethings ...
|
||||
* }
|
||||
* else
|
||||
* {
|
||||
* float num1 = (currentMsec - this.StarLaunchMsec) / (this.StarArriveMsec - this.StarLaunchMsec - this.lastWaitTime);
|
||||
* this._hitIndex = (int) ((double) this._hitAreaList.Count * (double) num1);
|
||||
* if (this._hitIndex >= this._hitAreaList.Count)
|
||||
* this._hitIndex = this._hitAreaList.Count - 1;
|
||||
* if (this._hitIndex < 0)
|
||||
* this._hitIndex = 0;
|
||||
* int num2 = (int) ((double) this._dispLaneNum * this.GetDeleteArrowDistance());
|
||||
* // do somethings ...
|
||||
* }
|
||||
*
|
||||
* 现在要在 this.GetDeleteArrowDistance() 之前插入
|
||||
* this._hitIn = ((float)this._hitAreaList.Count * num1 > (float)this._hitIndex + 0.5f);
|
||||
* 这段代码, 可以采用 Prefix, GetDeleteArrowDistance() 只在两个地方调用过, 另一处就在上面的 if 分支中 (即非 AutoPlay 情况)
|
||||
*/
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(SlideRoot), "GetDeleteArrowDistance")]
|
||||
private static void FixSlideAutoPlayArrow(
|
||||
SlideRoot __instance, ref bool ____hitIn, int ____hitIndex, List<SlideManager.HitArea> ____hitAreaList,
|
||||
float ___StarLaunchMsec, float ___StarArriveMsec, float ___lastWaitTime
|
||||
)
|
||||
{
|
||||
if (GameManager.IsAutoPlay())
|
||||
{
|
||||
float prop = (NotesManager.GetCurrentMsec() - ___StarLaunchMsec) / (___StarArriveMsec - ___StarLaunchMsec - ___lastWaitTime);
|
||||
____hitIn = ____hitAreaList.Count * prop > ____hitIndex + 0.5f;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
30
AquaMai/AquaMai.Mods/Fix/Legacy/FixQuickRetry130.cs
Normal file
30
AquaMai/AquaMai.Mods/Fix/Legacy/FixQuickRetry130.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using AquaMai.Core.Attributes;
|
||||
using AquaMai.Config.Attributes;
|
||||
using HarmonyLib;
|
||||
using Manager;
|
||||
|
||||
namespace AquaMai.Mods.Fix.Legacy;
|
||||
|
||||
[ConfigSection(exampleHidden: true, defaultOn: true)]
|
||||
[EnableGameVersion(23000, 23499, noWarn: true)]
|
||||
public class FixQuickRetry130
|
||||
{
|
||||
// Fix for the game not resetting Fast and Late counts when quick retrying
|
||||
// For game version < 1.35.0
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(GamePlayManager), "SetQuickRetryFrag")]
|
||||
public static void PostGamePlayManagerSetQuickRetryFrag(GamePlayManager __instance, bool flag)
|
||||
{
|
||||
// Since 1.35.0, `GameScoreList.Initialize()` resets the Fast and Late counts
|
||||
if (flag && !Traverse.Create(typeof(GameScoreList)).Methods().Contains("Initialize"))
|
||||
{
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
var gameScoreList = __instance.GetGameScore(i);
|
||||
var traverse = Traverse.Create(gameScoreList);
|
||||
traverse.Property("Fast").SetValue((uint)0);
|
||||
traverse.Property("Late").SetValue((uint)0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
7
AquaMai/AquaMai.Mods/Fix/README.md
Normal file
7
AquaMai/AquaMai.Mods/Fix/README.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Fix
|
||||
|
||||
Fix of the game's bugs or removal of the game's annoying unuseful "features".
|
||||
|
||||
Non-removal "Fix" patches should have no (negative, or any visual changing) side-effects on the original game.
|
||||
|
||||
All patches under "Fix" should enabled by default and hide in example. They could be still turned off manually in the config.
|
||||
45
AquaMai/AquaMai.Mods/Fix/Stability/FixMissingCharaCrash.cs
Normal file
45
AquaMai/AquaMai.Mods/Fix/Stability/FixMissingCharaCrash.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using AquaMai.Config.Attributes;
|
||||
using HarmonyLib;
|
||||
using Process;
|
||||
using Util;
|
||||
|
||||
namespace AquaMai.Mods.Fix.Stability;
|
||||
|
||||
/**
|
||||
* Fix character selection crashing due to missing character data
|
||||
*/
|
||||
[ConfigSection(exampleHidden: true, defaultOn: true)]
|
||||
public class FixMissingCharaCrash
|
||||
{
|
||||
// Check if the return is null. If it is, make up a color
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(CharacterSelectProces), "GetMapColorData")]
|
||||
public static void GetMapColorData(ref CharacterSelectProces __instance, ref CharacterMapColorData __result)
|
||||
{
|
||||
if (__result != null) return;
|
||||
|
||||
// 1 is a color that definitely exists
|
||||
if (MapMaster.GetSlotData(1) == null)
|
||||
{
|
||||
MapMaster.GetSlotData(1).Load();
|
||||
}
|
||||
__result = MapMaster.GetSlotData(1);
|
||||
}
|
||||
|
||||
// This is called when loading the music selection screen, to display characters on the top screen
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(Monitor.CommonMonitor), "SetCharacterSlot", new Type[] { typeof(MessageCharactorInfomationData) })]
|
||||
public static bool SetCharacterSlot(ref MessageCharactorInfomationData data, Dictionary<int, CharacterSlotData> ____characterSlotData)
|
||||
{
|
||||
// Some characters are not found in this dictionary. We simply skip loading those characters
|
||||
if (!____characterSlotData.ContainsKey(data.MapKey))
|
||||
{
|
||||
Console.Log($"Could not get CharacterSlotData for character [Index={data.Index}, MapKey={data.MapKey}], ignoring...");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
49
AquaMai/AquaMai.Mods/GameSettings/CreditConfig.cs
Normal file
49
AquaMai/AquaMai.Mods/GameSettings/CreditConfig.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
using AquaMai.Config.Attributes;
|
||||
using AquaMai.Core.Attributes;
|
||||
using HarmonyLib;
|
||||
|
||||
namespace AquaMai.Mods.GameSettings;
|
||||
|
||||
[ConfigSection(
|
||||
en: "Set the game to Paid Play (lock credits) or Free Play.",
|
||||
zh: "设置游戏为付费游玩(锁定可用点数)或免费游玩")]
|
||||
public class CreditConfig
|
||||
{
|
||||
[ConfigEntry(
|
||||
en: "Set to Free Play (set to false for Paid Play).",
|
||||
zh: "是否免费游玩(设为 false 时为付费游玩)")]
|
||||
private static readonly bool isFreePlay = true;
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(Manager.Credit), "IsFreePlay")]
|
||||
private static bool PreIsFreePlay(ref bool __result)
|
||||
{
|
||||
__result = isFreePlay;
|
||||
return false;
|
||||
}
|
||||
|
||||
[ConfigEntry(
|
||||
en: "Lock credits amount (only valid in Paid Play). Set to 0 to disable.",
|
||||
zh: "锁定可用点数数量(仅在付费游玩时有效),设为 0 以禁用")]
|
||||
private static readonly uint lockCredits = 24;
|
||||
|
||||
private static bool ShouldLockCredits => !isFreePlay && lockCredits > 0;
|
||||
|
||||
[EnableIf(nameof(ShouldLockCredits))]
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(Manager.Credit), "IsGameCostEnough")]
|
||||
private static bool PreIsGameCostEnough(ref bool __result)
|
||||
{
|
||||
__result = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
[EnableIf(nameof(ShouldLockCredits))]
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(AMDaemon.CreditUnit), "Credit", MethodType.Getter)]
|
||||
private static bool PreCredit(ref uint __result)
|
||||
{
|
||||
__result = 24;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
27
AquaMai/AquaMai.Mods/GameSettings/ForceAsServer.cs
Normal file
27
AquaMai/AquaMai.Mods/GameSettings/ForceAsServer.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using AMDaemon;
|
||||
using AquaMai.Config.Attributes;
|
||||
using HarmonyLib;
|
||||
|
||||
namespace AquaMai.Mods.GameSettings;
|
||||
|
||||
[ConfigSection(
|
||||
en: "If you want to configure in-shop party-link, you should turn this off.",
|
||||
zh: "如果要配置店内招募的话,应该要把这个关闭")]
|
||||
public class ForceAsServer
|
||||
{
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(LanInstall), "IsServer", MethodType.Getter)]
|
||||
private static bool PreIsServer(ref bool __result)
|
||||
{
|
||||
__result = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(Network), "IsLanAvailable", MethodType.Getter)]
|
||||
private static bool PreIsLanAvailable(ref bool __result)
|
||||
{
|
||||
__result = false;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
50
AquaMai/AquaMai.Mods/GameSettings/JudgeAdjust.cs
Normal file
50
AquaMai/AquaMai.Mods/GameSettings/JudgeAdjust.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
using System.Threading;
|
||||
using AquaMai.Config.Attributes;
|
||||
using HarmonyLib;
|
||||
using IO;
|
||||
using Manager.UserDatas;
|
||||
|
||||
namespace AquaMai.Mods.GameSettings;
|
||||
|
||||
[ConfigSection(
|
||||
en: "Globally adjust A/B judgment (unit same as in-game options) or increase touch delay.",
|
||||
zh: "全局调整 A/B 判(单位和游戏里一样)或增加触摸延迟")]
|
||||
public class JudgeAdjust
|
||||
{
|
||||
[ConfigEntry(
|
||||
en: "Adjust A judgment.",
|
||||
zh: "调整 A 判")]
|
||||
private static readonly double a = 0;
|
||||
|
||||
[ConfigEntry(
|
||||
en: "Adjust B judgment.",
|
||||
zh: "调整 B 判")]
|
||||
private static readonly double b = 0;
|
||||
|
||||
[ConfigEntry(
|
||||
en: "Increase touch delay.",
|
||||
zh: "增加触摸延迟")]
|
||||
private static readonly int touchDelay = 0;
|
||||
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(UserOption), "GetAdjustMSec")]
|
||||
public static void GetAdjustMSec(ref float __result)
|
||||
{
|
||||
__result += (float)(a * 16.666666d);
|
||||
}
|
||||
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(UserOption), "GetJudgeTimingFrame")]
|
||||
public static void GetJudgeTimingFrame(ref float __result)
|
||||
{
|
||||
__result += (float)b;
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(NewTouchPanel), "Recv")]
|
||||
public static void NewTouchPanelRecv()
|
||||
{
|
||||
if (touchDelay <= 0) return;
|
||||
Thread.Sleep(touchDelay);
|
||||
}
|
||||
}
|
||||
5
AquaMai/AquaMai.Mods/GameSettings/README.md
Normal file
5
AquaMai/AquaMai.Mods/GameSettings/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# GameSettings
|
||||
|
||||
Overriding or adjusting the game settings that're originally configurable / modifiable, but made into patches for unification, flexibility or convenience.
|
||||
|
||||
Patches changing the way the game running / behaving which are not possible in the stock game may need to go to the GameSystem category.
|
||||
154
AquaMai/AquaMai.Mods/GameSettings/TouchSensitivity.cs
Normal file
154
AquaMai/AquaMai.Mods/GameSettings/TouchSensitivity.cs
Normal file
@@ -0,0 +1,154 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using AquaMai.Config.Attributes;
|
||||
using HarmonyLib;
|
||||
using IO;
|
||||
using Manager;
|
||||
using MelonLoader;
|
||||
|
||||
namespace AquaMai.Mods.GameSettings;
|
||||
|
||||
[ConfigSection(
|
||||
en: """
|
||||
Use custom touch sensitivity.
|
||||
When enabled, the settings in Test mode will not take effect.
|
||||
When disabled, the settings in Test mode is used.
|
||||
|
||||
Sensitivity adjustments in Test mode are not linear.
|
||||
Default sensitivity in area A: 90, 80, 70, 60, 50, 40, 30, 26, 23, 20, 10.
|
||||
Default sensitivity in other areas: 70, 60, 50, 40, 30, 20, 15, 10, 5, 1, 1.
|
||||
A setting of 0 in Test mode corresponds to 40, 20 here, -5 corresponds to 90, 70, +5 corresponds to 10, 1.
|
||||
The higher the number in Test mode, the lower the number here, resulting in higher sensitivity for official machines.
|
||||
For ADX, the sensitivity is reversed, so the higher the number here, the higher the sensitivity.
|
||||
""",
|
||||
zh: """
|
||||
使用自定义触摸灵敏度
|
||||
这里启用之后 Test 里的就不再起作用了
|
||||
这里禁用之后就还是用 Test 里的调
|
||||
|
||||
在 Test 模式下调整的灵敏度不是线性的
|
||||
A 区默认灵敏度 90, 80, 70, 60, 50, 40, 30, 26, 23, 20, 10
|
||||
其他区域默认灵敏度 70, 60, 50, 40, 30, 20, 15, 10, 5, 1, 1
|
||||
Test 里设置的 0 对应的是 40, 20 这一档,-5 是 90, 70,+5 是 10, 1
|
||||
Test 里的数字越大,这里的数字越小,对于官机来说,灵敏度更大
|
||||
而 ADX 的灵敏度是反的,所以对于 ADX,这里的数字越大,灵敏度越大
|
||||
""")]
|
||||
public class TouchSensitivity
|
||||
{
|
||||
[ConfigEntry]
|
||||
private static readonly byte A1 = 40;
|
||||
|
||||
[ConfigEntry]
|
||||
private static readonly byte A2 = 40;
|
||||
|
||||
[ConfigEntry]
|
||||
private static readonly byte A3 = 40;
|
||||
|
||||
[ConfigEntry]
|
||||
private static readonly byte A4 = 40;
|
||||
|
||||
[ConfigEntry]
|
||||
private static readonly byte A5 = 40;
|
||||
|
||||
[ConfigEntry]
|
||||
private static readonly byte A6 = 40;
|
||||
|
||||
[ConfigEntry]
|
||||
private static readonly byte A7 = 40;
|
||||
|
||||
[ConfigEntry]
|
||||
private static readonly byte A8 = 40;
|
||||
|
||||
[ConfigEntry]
|
||||
private static readonly byte B1 = 20;
|
||||
|
||||
[ConfigEntry]
|
||||
private static readonly byte B2 = 20;
|
||||
|
||||
[ConfigEntry]
|
||||
private static readonly byte B3 = 20;
|
||||
|
||||
[ConfigEntry]
|
||||
private static readonly byte B4 = 20;
|
||||
|
||||
[ConfigEntry]
|
||||
private static readonly byte B5 = 20;
|
||||
|
||||
[ConfigEntry]
|
||||
private static readonly byte B6 = 20;
|
||||
|
||||
[ConfigEntry]
|
||||
private static readonly byte B7 = 20;
|
||||
|
||||
[ConfigEntry]
|
||||
private static readonly byte B8 = 20;
|
||||
|
||||
[ConfigEntry]
|
||||
private static readonly byte C1 = 20;
|
||||
|
||||
[ConfigEntry]
|
||||
private static readonly byte C2 = 20;
|
||||
|
||||
[ConfigEntry]
|
||||
private static readonly byte D1 = 20;
|
||||
|
||||
[ConfigEntry]
|
||||
private static readonly byte D2 = 20;
|
||||
|
||||
[ConfigEntry]
|
||||
private static readonly byte D3 = 20;
|
||||
|
||||
[ConfigEntry]
|
||||
private static readonly byte D4 = 20;
|
||||
|
||||
[ConfigEntry]
|
||||
private static readonly byte D5 = 20;
|
||||
|
||||
[ConfigEntry]
|
||||
private static readonly byte D6 = 20;
|
||||
|
||||
[ConfigEntry]
|
||||
private static readonly byte D7 = 20;
|
||||
|
||||
[ConfigEntry]
|
||||
private static readonly byte D8 = 20;
|
||||
|
||||
[ConfigEntry]
|
||||
private static readonly byte E1 = 20;
|
||||
|
||||
[ConfigEntry]
|
||||
private static readonly byte E2 = 20;
|
||||
|
||||
[ConfigEntry]
|
||||
private static readonly byte E3 = 20;
|
||||
|
||||
[ConfigEntry]
|
||||
private static readonly byte E4 = 20;
|
||||
|
||||
[ConfigEntry]
|
||||
private static readonly byte E5 = 20;
|
||||
|
||||
[ConfigEntry]
|
||||
private static readonly byte E6 = 20;
|
||||
|
||||
[ConfigEntry]
|
||||
private static readonly byte E7 = 20;
|
||||
|
||||
[ConfigEntry]
|
||||
private static readonly byte E8 = 20;
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(NewTouchPanel), "SetTouchPanelSensitivity")]
|
||||
public static void SetTouchPanelSensitivityPrefix(List<byte> sensitivity)
|
||||
{
|
||||
var configType = typeof(TouchSensitivity);
|
||||
for (var i = 0; i < 34; i++)
|
||||
{
|
||||
var area = (InputManager.TouchPanelArea)i;
|
||||
var field = configType.GetField(area.ToString(), BindingFlags.NonPublic | BindingFlags.Static);
|
||||
var value = (byte)field.GetValue(null);
|
||||
sensitivity[i] = value;
|
||||
}
|
||||
MelonLogger.Msg("[TouchSensitivity] Applied");
|
||||
}
|
||||
}
|
||||
146
AquaMai/AquaMai.Mods/GameSystem/Assets/Fonts.cs
Normal file
146
AquaMai/AquaMai.Mods/GameSystem/Assets/Fonts.cs
Normal file
@@ -0,0 +1,146 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AquaMai.Config.Attributes;
|
||||
using AquaMai.Core.Helpers;
|
||||
using HarmonyLib;
|
||||
using MelonLoader;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.TextCore.LowLevel;
|
||||
|
||||
namespace AquaMai.Mods.GameSystem.Assets;
|
||||
|
||||
[ConfigSection(
|
||||
en: "Use custom font(s) as fallback or fully replace the original game font.",
|
||||
zh: "使用自定义字体作为回退(解决中文字形缺失问题),或完全替换游戏原字体")]
|
||||
public class Fonts
|
||||
{
|
||||
[ConfigEntry(
|
||||
en: """
|
||||
Font path(s).
|
||||
Use semicolon to separate multiple paths for a fallback chain.
|
||||
Microsoft YaHei Bold by default.
|
||||
""",
|
||||
zh: """
|
||||
字体路径
|
||||
使用分号分隔多个路径以构成 Fallback 链
|
||||
默认为微软雅黑 Bold
|
||||
""")]
|
||||
private static readonly string paths = "%SYSTEMROOT%/Fonts/msyhbd.ttc";
|
||||
|
||||
[ConfigEntry(
|
||||
en: "Add custom font(s) as fallback, use original game font when possible.",
|
||||
zh: "将自定义字体作为游戏原字体的回退,尽可能使用游戏原字体")]
|
||||
private static readonly bool addAsFallback = true;
|
||||
|
||||
private static List<TMP_FontAsset> fontAssets = [];
|
||||
private static readonly List<TMP_FontAsset> processedFonts = [];
|
||||
|
||||
private static TMP_FontAsset replacementFontAsset;
|
||||
private static List<TMP_FontAsset> fallbackFontAssets = [];
|
||||
|
||||
public static void OnBeforePatch()
|
||||
{
|
||||
var paths = Fonts.paths
|
||||
.Split(';')
|
||||
.Where(p => !string.IsNullOrWhiteSpace(p))
|
||||
.Select(FileSystem.ResolvePath);
|
||||
var fonts = paths
|
||||
.Select(p =>
|
||||
{
|
||||
var font = new Font(p);
|
||||
if (font == null)
|
||||
{
|
||||
MelonLogger.Warning($"[Fonts] Font not found: {p}");
|
||||
}
|
||||
return font;
|
||||
})
|
||||
.Where(f => f != null);
|
||||
fontAssets = fonts
|
||||
.Select(f => TMP_FontAsset.CreateFontAsset(f, 90, 9, GlyphRenderMode.SDFAA, 8192, 8192))
|
||||
.ToList();
|
||||
|
||||
if (fontAssets.Count == 0)
|
||||
{
|
||||
MelonLogger.Warning("[Fonts] No font loaded.");
|
||||
}
|
||||
else if (addAsFallback)
|
||||
{
|
||||
fallbackFontAssets = fontAssets;
|
||||
}
|
||||
else
|
||||
{
|
||||
replacementFontAsset = fontAssets[0];
|
||||
fallbackFontAssets = fontAssets.Skip(1).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(TextMeshProUGUI), "Awake")]
|
||||
[HarmonyPostfix]
|
||||
public static void PostAwake(TextMeshProUGUI __instance)
|
||||
{
|
||||
if (fontAssets.Count == 0) return;
|
||||
if (processedFonts.Contains(__instance.font)) return;
|
||||
|
||||
if (replacementFontAsset != null)
|
||||
{
|
||||
ProcessReplacement(__instance);
|
||||
}
|
||||
if (fallbackFontAssets.Count > 0)
|
||||
{
|
||||
ProcessFallback(__instance);
|
||||
}
|
||||
|
||||
processedFonts.Add(__instance.font);
|
||||
}
|
||||
|
||||
private static void ProcessReplacement(TextMeshProUGUI __instance)
|
||||
{
|
||||
# if DEBUG
|
||||
MelonLogger.Msg($"{__instance.font.name} {__instance.text}");
|
||||
# endif
|
||||
|
||||
var materialOrigin = __instance.fontMaterial;
|
||||
var materialSharedOrigin = __instance.fontSharedMaterial;
|
||||
__instance.font = replacementFontAsset;
|
||||
|
||||
# if DEBUG
|
||||
MelonLogger.Msg($"shaderKeywords {materialOrigin.shaderKeywords.Join()} {__instance.fontMaterial.shaderKeywords.Join()}");
|
||||
# endif
|
||||
// __instance.fontSharedMaterial = materialSharedOrigin;
|
||||
|
||||
// 这样之后该有描边的地方整个字后面都是阴影,它不知道哪里是边
|
||||
// materialOrigin.mainTexture = __instance.fontMaterial.mainTexture;
|
||||
// materialOrigin.mainTextureOffset = __instance.fontMaterial.mainTextureOffset;
|
||||
// materialOrigin.mainTextureScale = __instance.fontMaterial.mainTextureScale;
|
||||
// __instance.fontMaterial.CopyPropertiesFromMaterial(materialOrigin);
|
||||
|
||||
// 这样了之后有描边了,但是描边很细
|
||||
// __instance.fontMaterial.shader = materialOrigin.shader;
|
||||
foreach (var keyword in materialOrigin.shaderKeywords)
|
||||
{
|
||||
__instance.fontMaterial.EnableKeyword(keyword);
|
||||
}
|
||||
// __instance.fontMaterial.globalIlluminationFlags = materialOrigin.globalIlluminationFlags;
|
||||
|
||||
// 原来是 underlay,但是复制这三个属性之后就又变成整个字后面都是阴影了
|
||||
// __instance.fontMaterial.SetFloat(ShaderUtilities.ID_UnderlayOffsetY, materialOrigin.GetFloat(ShaderUtilities.ID_UnderlayOffsetY));
|
||||
// __instance.fontMaterial.SetFloat(ShaderUtilities.ID_UnderlayOffsetX, materialOrigin.GetFloat(ShaderUtilities.ID_UnderlayOffsetX));
|
||||
// __instance.fontMaterial.SetFloat(ShaderUtilities.ID_UnderlayDilate, materialOrigin.GetFloat(ShaderUtilities.ID_UnderlayDilate));
|
||||
|
||||
// if(materialOrigin.shaderKeywords.Contains(ShaderUtilities.Keyword_Underlay))
|
||||
// {
|
||||
// __instance.fontMaterial.EnableKeyword(ShaderUtilities.Keyword_Glow);
|
||||
// __instance.fontMaterial.SetFloat(ShaderUtilities.ID_GlowOuter, .5f);
|
||||
// // __instance.fontMaterial.SetFloat(ShaderUtilities.ID_UnderlayOffsetX, materialOrigin.GetFloat(ShaderUtilities.ID_UnderlayOffsetX));
|
||||
// }
|
||||
}
|
||||
|
||||
private static void ProcessFallback(TextMeshProUGUI __instance)
|
||||
{
|
||||
foreach (var fontAsset in fallbackFontAssets)
|
||||
{
|
||||
__instance.font.fallbackFontAssetTable.Add(fontAsset);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using HarmonyLib;
|
||||
using UnityEngine;
|
||||
using Manager;
|
||||
using Util;
|
||||
using AquaMai.Config.Attributes;
|
||||
|
||||
namespace AquaMai.Mods.GameSystem.Assets;
|
||||
|
||||
[ConfigSection(
|
||||
en: "Load all existing \".ab\" image resources regardless of the AssetBundleImages manifest.",
|
||||
zh: """
|
||||
加载所有存在的 .ab 图片资源(无视 AssetBundleImages.manifest)
|
||||
导入了删除曲包之类的话,应该需要开启这个
|
||||
""")]
|
||||
public class LoadAssetBundleWithoutManifest
|
||||
{
|
||||
private static HashSet<string> abFiles = new HashSet<string>();
|
||||
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(OptionDataManager), "CheckAssetBundle")]
|
||||
public static void PostCheckAssetBundle(ref Safe.ReadonlySortedDictionary<string, string> abs)
|
||||
{
|
||||
foreach (var ab in abs)
|
||||
{
|
||||
abFiles.Add(ab.Key);
|
||||
}
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(AssetBundleManifest), "GetAllAssetBundles")]
|
||||
public static bool PreGetAllAssetBundles(AssetBundleManifest __instance, ref string[] __result)
|
||||
{
|
||||
__result = abFiles.ToArray();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
577
AquaMai/AquaMai.Mods/GameSystem/Assets/LoadLocalImages.cs
Normal file
577
AquaMai/AquaMai.Mods/GameSystem/Assets/LoadLocalImages.cs
Normal file
@@ -0,0 +1,577 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using HarmonyLib;
|
||||
using UnityEngine;
|
||||
using System.Text.RegularExpressions;
|
||||
using MAI2.Util;
|
||||
using Manager;
|
||||
using MelonLoader;
|
||||
using Monitor;
|
||||
using AquaMai.Config.Attributes;
|
||||
using AquaMai.Core.Helpers;
|
||||
|
||||
namespace AquaMai.Mods.GameSystem.Assets;
|
||||
|
||||
[ConfigSection(
|
||||
en: "Load asset images from the configured directory (for self-made charts).",
|
||||
zh: "从指定目录下加载资源图片(自制谱用)")]
|
||||
public class LoadLocalImages
|
||||
{
|
||||
[ConfigEntry]
|
||||
private static readonly string localAssetsDir = "LocalAssets";
|
||||
|
||||
private static readonly string[] imageExts = [".jpg", ".png", ".jpeg"];
|
||||
private static readonly Dictionary<string, string> jacketPaths = [];
|
||||
private static readonly Dictionary<string, string> framePaths = [];
|
||||
private static readonly Dictionary<string, string> platePaths = [];
|
||||
private static readonly Dictionary<string, string> framemaskPaths = [];
|
||||
private static readonly Dictionary<string, string> framepatternPaths = [];
|
||||
private static readonly Dictionary<string, string> iconPaths = [];
|
||||
private static readonly Dictionary<string, string> charaPaths = [];
|
||||
private static readonly Dictionary<string, string> partnerPaths = [];
|
||||
//private static readonly Dictionary<string, string> navicharaPaths = [];
|
||||
private static readonly Dictionary<string, string> tabTitlePaths = [];
|
||||
private static readonly Dictionary<string, string> localAssetsContents = [];
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(DataManager), "LoadMusicBase")]
|
||||
public static void LoadMusicPostfix(List<string> ____targetDirs)
|
||||
{
|
||||
foreach (var aDir in ____targetDirs)
|
||||
{
|
||||
if (Directory.Exists(Path.Combine(aDir, @"AssetBundleImages\jacket")))
|
||||
foreach (var file in Directory.GetFiles(Path.Combine(aDir, @"AssetBundleImages\jacket")))
|
||||
{
|
||||
if (!imageExts.Contains(Path.GetExtension(file).ToLowerInvariant())) continue;
|
||||
var idStr = Path.GetFileName(file).Substring("ui_jacket_".Length, 6);
|
||||
jacketPaths[idStr] = file;
|
||||
}
|
||||
|
||||
if (Directory.Exists(Path.Combine(aDir, @"AssetBundleImages\frame")))
|
||||
foreach (var file in Directory.GetFiles(Path.Combine(aDir, @"AssetBundleImages\frame")))
|
||||
{
|
||||
if (!imageExts.Contains(Path.GetExtension(file).ToLowerInvariant())) continue;
|
||||
var idStr = Path.GetFileName(file).Substring("ui_frame_".Length, 6);
|
||||
framePaths[idStr] = file;
|
||||
}
|
||||
|
||||
if (Directory.Exists(Path.Combine(aDir, @"AssetBundleImages\nameplate")))
|
||||
foreach (var file in Directory.GetFiles(Path.Combine(aDir, @"AssetBundleImages\nameplate")))
|
||||
{
|
||||
if (!imageExts.Contains(Path.GetExtension(file).ToLowerInvariant())) continue;
|
||||
var idStr = Path.GetFileName(file).Substring("ui_plate_".Length, 6);
|
||||
platePaths[idStr] = file;
|
||||
}
|
||||
|
||||
if (Directory.Exists(Path.Combine(aDir, @"AssetBundleImages\framemask")))
|
||||
foreach (var file in Directory.GetFiles(Path.Combine(aDir, @"AssetBundleImages\framemask")))
|
||||
{
|
||||
if (!imageExts.Contains(Path.GetExtension(file).ToLowerInvariant())) continue;
|
||||
var idStr = Path.GetFileName(file).Substring("ui_framemask_".Length, 6);
|
||||
framemaskPaths[idStr] = file;
|
||||
}
|
||||
|
||||
if (Directory.Exists(Path.Combine(aDir, @"AssetBundleImages\framepattern")))
|
||||
foreach (var file in Directory.GetFiles(Path.Combine(aDir, @"AssetBundleImages\framepattern")))
|
||||
{
|
||||
if (!imageExts.Contains(Path.GetExtension(file).ToLowerInvariant())) continue;
|
||||
var idStr = Path.GetFileName(file).Substring("ui_framepattern_".Length, 6);
|
||||
framepatternPaths[idStr] = file;
|
||||
}
|
||||
|
||||
if (Directory.Exists(Path.Combine(aDir, @"AssetBundleImages\icon")))
|
||||
foreach (var file in Directory.GetFiles(Path.Combine(aDir, @"AssetBundleImages\icon")))
|
||||
{
|
||||
if (!imageExts.Contains(Path.GetExtension(file).ToLowerInvariant())) continue;
|
||||
var idStr = Path.GetFileName(file).Substring("ui_icon_".Length, 6);
|
||||
iconPaths[idStr] = file;
|
||||
}
|
||||
|
||||
if (Directory.Exists(Path.Combine(aDir, @"AssetBundleImages\chara")))
|
||||
foreach (var file in Directory.GetFiles(Path.Combine(aDir, @"AssetBundleImages\chara")))
|
||||
{
|
||||
if (!imageExts.Contains(Path.GetExtension(file).ToLowerInvariant())) continue;
|
||||
var idStr = Path.GetFileName(file).Substring("ui_chara_".Length, 6);
|
||||
charaPaths[idStr] = file;
|
||||
}
|
||||
|
||||
if (Directory.Exists(Path.Combine(aDir, @"AssetBundleImages\partner")))
|
||||
foreach (var file in Directory.GetFiles(Path.Combine(aDir, @"AssetBundleImages\partner")))
|
||||
{
|
||||
if (!imageExts.Contains(Path.GetExtension(file).ToLowerInvariant())) continue;
|
||||
var idStr = Path.GetFileName(file).Substring("ui_Partner_".Length, 6);
|
||||
partnerPaths[idStr] = file;
|
||||
}
|
||||
//if (Directory.Exists(Path.Combine(aDir, @"AssetBundleImages\navichara\sprite\parts\ui_navichara_21")))
|
||||
// foreach (var file in Directory.GetFiles(Path.Combine(aDir, @"AssetBundleImages\navichara\sprite\parts\ui_navichara_", charaid)))
|
||||
//{
|
||||
// if (!imageExts.Contains(Path.GetExtension(file).ToLowerInvariant())) continue;
|
||||
//var idStr = Path.GetFileName(file).Substring("ui_navichara_".Length, 6);
|
||||
// navicharaPaths[idStr] = file;
|
||||
// }
|
||||
|
||||
if (Directory.Exists(Path.Combine(aDir, @"Common\Sprites\Tab\Title")))
|
||||
foreach (var file in Directory.GetFiles(Path.Combine(aDir, @"Common\Sprites\Tab\Title")))
|
||||
{
|
||||
if (!imageExts.Contains(Path.GetExtension(file).ToLowerInvariant())) continue;
|
||||
tabTitlePaths[Path.GetFileNameWithoutExtension(file).ToLowerInvariant()] = file;
|
||||
}
|
||||
}
|
||||
|
||||
MelonLogger.Msg($"[LoadLocalImages] Loaded {jacketPaths.Count} Jacket, {platePaths.Count} NamePlate, {framePaths.Count} Frame, {framemaskPaths.Count} FrameMask, {framepatternPaths.Count} FramePattern, {iconPaths.Count} Icon, {charaPaths.Count} Chara, {partnerPaths.Count} PartnerLogo, {tabTitlePaths.Count} Tab Titles from AssetBundleImages.");
|
||||
|
||||
var resolvedDir = FileSystem.ResolvePath(localAssetsDir);
|
||||
if (Directory.Exists(resolvedDir))
|
||||
foreach (var laFile in Directory.EnumerateFiles(resolvedDir))
|
||||
{
|
||||
if (!imageExts.Contains(Path.GetExtension(laFile).ToLowerInvariant())) continue;
|
||||
localAssetsContents[Path.GetFileNameWithoutExtension(laFile).ToLowerInvariant()] = laFile;
|
||||
}
|
||||
|
||||
MelonLogger.Msg($"[LoadLocalImages] Loaded {localAssetsContents.Count} LocalAssets.");
|
||||
}
|
||||
|
||||
private static string GetJacketPath(string id)
|
||||
{
|
||||
return localAssetsContents.TryGetValue(id, out var laPath) ? laPath : jacketPaths.GetValueOrDefault(id);
|
||||
}
|
||||
|
||||
public static Texture2D GetJacketTexture2D(string id)
|
||||
{
|
||||
var path = GetJacketPath(id);
|
||||
if (path == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var texture = new Texture2D(1, 1, TextureFormat.RGBA32, false);
|
||||
texture.LoadImage(File.ReadAllBytes(path));
|
||||
return texture;
|
||||
}
|
||||
|
||||
public static Texture2D GetJacketTexture2D(int id)
|
||||
{
|
||||
return GetJacketTexture2D($"{id:000000}");
|
||||
}
|
||||
|
||||
private static string GetFramePath(string id)
|
||||
{
|
||||
return framePaths.GetValueOrDefault(id);
|
||||
}
|
||||
|
||||
public static Texture2D GetFrameTexture2D(string id)
|
||||
{
|
||||
var path = GetFramePath(id);
|
||||
if (path == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var texture = new Texture2D(1, 1, TextureFormat.RGBA32, false);
|
||||
texture.LoadImage(File.ReadAllBytes(path));
|
||||
return texture;
|
||||
}
|
||||
|
||||
private static string GetPlatePath(string id)
|
||||
{
|
||||
return platePaths.GetValueOrDefault(id);
|
||||
}
|
||||
|
||||
public static Texture2D GetPlateTexture2D(string id)
|
||||
{
|
||||
var path = GetPlatePath(id);
|
||||
if (path == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var texture = new Texture2D(1, 1, TextureFormat.RGBA32, false);
|
||||
texture.LoadImage(File.ReadAllBytes(path));
|
||||
return texture;
|
||||
}
|
||||
|
||||
private static string GetFrameMaskPath(string id)
|
||||
{
|
||||
return framemaskPaths.GetValueOrDefault(id);
|
||||
}
|
||||
|
||||
public static Texture2D GetFrameMaskTexture2D(string id)
|
||||
{
|
||||
var path = GetFrameMaskPath(id);
|
||||
if (path == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var texture = new Texture2D(1, 1, TextureFormat.RGBA32, false);
|
||||
texture.LoadImage(File.ReadAllBytes(path));
|
||||
return texture;
|
||||
}
|
||||
|
||||
private static string GetFramePatternPath(string id)
|
||||
{
|
||||
return framepatternPaths.GetValueOrDefault(id);
|
||||
}
|
||||
|
||||
public static Texture2D GetFramePatternTexture2D(string id)
|
||||
{
|
||||
var path = GetFramePatternPath(id);
|
||||
if (path == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var texture = new Texture2D(1, 1, TextureFormat.RGBA32, false);
|
||||
texture.LoadImage(File.ReadAllBytes(path));
|
||||
return texture;
|
||||
}
|
||||
|
||||
private static string GetIconPath(string id)
|
||||
{
|
||||
return iconPaths.GetValueOrDefault(id);
|
||||
}
|
||||
|
||||
public static Texture2D GetIconTexture2D(string id)
|
||||
{
|
||||
var path = GetIconPath(id);
|
||||
if (path == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var texture = new Texture2D(1, 1, TextureFormat.RGBA32, false);
|
||||
texture.LoadImage(File.ReadAllBytes(path));
|
||||
return texture;
|
||||
}
|
||||
|
||||
private static string GetCharaPath(string id)
|
||||
{
|
||||
return charaPaths.GetValueOrDefault(id);
|
||||
}
|
||||
|
||||
public static Texture2D GetCharaTexture2D(string id)
|
||||
{
|
||||
var path = GetCharaPath(id);
|
||||
if (path == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var texture = new Texture2D(1, 1, TextureFormat.RGBA32, false);
|
||||
texture.LoadImage(File.ReadAllBytes(path));
|
||||
return texture;
|
||||
}
|
||||
|
||||
private static string GetPartnerPath(string id)
|
||||
{
|
||||
return partnerPaths.GetValueOrDefault(id);
|
||||
}
|
||||
|
||||
public static Texture2D GetPartnerTexture2D(string id)
|
||||
{
|
||||
var path = GetPartnerPath(id);
|
||||
if (path == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var texture = new Texture2D(1, 1, TextureFormat.RGBA32, false);
|
||||
texture.LoadImage(File.ReadAllBytes(path));
|
||||
return texture;
|
||||
}
|
||||
|
||||
/*
|
||||
[HarmonyPatch]
|
||||
public static class TabTitleLoader
|
||||
{
|
||||
public static IEnumerable<MethodBase> TargetMethods()
|
||||
{
|
||||
// Fxxk unity
|
||||
// game load tab title by call Resources.Load<Sprite> directly
|
||||
// patching Resources.Load<Sprite> need this stuff
|
||||
// var method = typeof(Resources).GetMethods(BindingFlags.Public | BindingFlags.Static).First(it => it.Name == "Load" && it.IsGenericMethod).MakeGenericMethod(typeof(Sprite));
|
||||
// return [method];
|
||||
// but it not work, game will blackscreen if add prefix or postfix
|
||||
//
|
||||
// patching AssetBundleManager.LoadAsset will lead game memory error
|
||||
// return [AccessTools.Method(typeof(AssetBundleManager), "LoadAsset", [typeof(string)], [typeof(Object)])];
|
||||
// and this is not work because game not using this
|
||||
//
|
||||
// we load them manually after game load and no need to hook the load progress
|
||||
}
|
||||
|
||||
public static bool Prefix(string path, ref Object __result)
|
||||
{
|
||||
if (!path.StartsWith("Common/Sprites/Tab/Title/")) return true;
|
||||
var filename = Path.GetFileNameWithoutExtension(path).ToLowerInvariant();
|
||||
var locPath = localAssetsContents.TryGetValue(filename, out var laPath) ? laPath : tabTitlePaths.GetValueOrDefault(filename);
|
||||
if (locPath is null) return true;
|
||||
|
||||
var texture = new Texture2D(1, 1);
|
||||
texture.LoadImage(File.ReadAllBytes(locPath));
|
||||
__result = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), new Vector2(0.5f, 0.5f));
|
||||
MelonLogger.Msg($"GetTabTitleSpritePrefix {locPath} {__result}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(MusicSelectMonitor), "Initialize")]
|
||||
public static void TabTitleLoader(MusicSelectMonitor __instance, Dictionary<int, Sprite> ____genreSprite, Dictionary<int, Sprite> ____versionSprite)
|
||||
{
|
||||
var genres = Singleton<DataManager>.Instance.GetMusicGenres();
|
||||
foreach (var (id, genre) in genres)
|
||||
{
|
||||
if (____genreSprite.GetValueOrDefault(id) is not null) continue;
|
||||
var filename = genre.FileName.ToLowerInvariant();
|
||||
var locPath = localAssetsContents.TryGetValue(filename, out var laPath) ? laPath : tabTitlePaths.GetValueOrDefault(filename);
|
||||
if (locPath is null) continue;
|
||||
var texture = new Texture2D(1, 1, TextureFormat.RGBA32, false);
|
||||
texture.LoadImage(File.ReadAllBytes(locPath));
|
||||
____genreSprite[id] = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), new Vector2(0.5f, 0.5f));
|
||||
}
|
||||
|
||||
var versions = Singleton<DataManager>.Instance.GetMusicVersions();
|
||||
foreach (var (id, version) in versions)
|
||||
{
|
||||
if (____versionSprite.GetValueOrDefault(id) is not null) continue;
|
||||
var filename = version.FileName.ToLowerInvariant();
|
||||
var locPath = localAssetsContents.TryGetValue(filename, out var laPath) ? laPath : tabTitlePaths.GetValueOrDefault(filename);
|
||||
if (locPath is null) continue;
|
||||
var texture = new Texture2D(1, 1, TextureFormat.RGBA32, false);
|
||||
texture.LoadImage(File.ReadAllBytes(locPath));
|
||||
____versionSprite[id] = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), new Vector2(0.5f, 0.5f));
|
||||
}
|
||||
}
|
||||
|
||||
[HarmonyPatch]
|
||||
public static class JacketLoader
|
||||
{
|
||||
public static IEnumerable<MethodBase> TargetMethods()
|
||||
{
|
||||
var AM = typeof(AssetManager);
|
||||
return [AM.GetMethod("GetJacketThumbTexture2D", [typeof(string)]), AM.GetMethod("GetJacketTexture2D", [typeof(string)])];
|
||||
}
|
||||
|
||||
public static bool Prefix(string filename, ref Texture2D __result, AssetManager __instance)
|
||||
{
|
||||
var matches = Regex.Matches(filename, @"UI_Jacket_(\d+)(_s)?\.png");
|
||||
if (matches.Count < 1)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var id = matches[0].Groups[1].Value;
|
||||
|
||||
var texture = GetJacketTexture2D(id);
|
||||
__result = texture ?? __instance.LoadAsset<Texture2D>($"Jacket/UI_Jacket_{id}.png");
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
[HarmonyPatch]
|
||||
public static class FrameLoader
|
||||
{
|
||||
public static IEnumerable<MethodBase> TargetMethods()
|
||||
{
|
||||
var AM = typeof(AssetManager);
|
||||
return [AM.GetMethod("GetFrameThumbTexture2D", [typeof(string)]), AM.GetMethod("GetFrameTexture2D", [typeof(string)])];
|
||||
}
|
||||
|
||||
public static bool Prefix(string filename, ref Texture2D __result, AssetManager __instance)
|
||||
{
|
||||
var matches = Regex.Matches(filename, @"UI_Frame_(\d+)(_s)?\.png");
|
||||
if (matches.Count < 1)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var id = matches[0].Groups[1].Value;
|
||||
|
||||
var texture = GetFrameTexture2D(id);
|
||||
__result = texture ?? __instance.LoadAsset<Texture2D>($"Frame/UI_Frame_{id}.png");
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
[HarmonyPatch]
|
||||
public static class PlateLoader
|
||||
{
|
||||
public static IEnumerable<MethodBase> TargetMethods()
|
||||
{
|
||||
var AM = typeof(AssetManager);
|
||||
return [AM.GetMethod("GetPlateTexture2D", [typeof(string)])];
|
||||
}
|
||||
|
||||
public static bool Prefix(string filename, ref Texture2D __result, AssetManager __instance)
|
||||
{
|
||||
var matches = Regex.Matches(filename, @"UI_Plate_(\d+)\.png");
|
||||
if (matches.Count < 1)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var id = matches[0].Groups[1].Value;
|
||||
|
||||
var texture = GetPlateTexture2D(id);
|
||||
__result = texture ?? __instance.LoadAsset<Texture2D>($"NamePlate/UI_Plate_{id}.png");
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
[HarmonyPatch]
|
||||
public static class FrameMaskLoader
|
||||
{
|
||||
public static IEnumerable<MethodBase> TargetMethods()
|
||||
{
|
||||
var AM = typeof(AssetManager);
|
||||
return [AM.GetMethod("GetFrameMaskTexture2D", [typeof(string)])];
|
||||
}
|
||||
|
||||
public static bool Prefix(string filename, ref Texture2D __result, AssetManager __instance)
|
||||
{
|
||||
var matches = Regex.Matches(filename, @"UI_FrameMask_(\d+)\.png");
|
||||
if (matches.Count < 1)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var id = matches[0].Groups[1].Value;
|
||||
|
||||
var texture = GetFrameMaskTexture2D(id);
|
||||
__result = texture ?? __instance.LoadAsset<Texture2D>($"FrameMask/UI_FrameMask_{id}.png");
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
[HarmonyPatch]
|
||||
public static class FramePatternLoader
|
||||
{
|
||||
public static IEnumerable<MethodBase> TargetMethods()
|
||||
{
|
||||
var AM = typeof(AssetManager);
|
||||
return [AM.GetMethod("GetFramePatternTexture2D", [typeof(string)])];
|
||||
}
|
||||
|
||||
public static bool Prefix(string filename, ref Texture2D __result, AssetManager __instance)
|
||||
{
|
||||
var matches = Regex.Matches(filename, @"UI_FramePattern_(\d+)\.png");
|
||||
if (matches.Count < 1)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var id = matches[0].Groups[1].Value;
|
||||
|
||||
var texture = GetFramePatternTexture2D(id);
|
||||
__result = texture ?? __instance.LoadAsset<Texture2D>($"FramePattern/UI_FramePattern_{id}.png");
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Private | Instance
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(AssetManager), "GetIconTexture2D", typeof(string))]
|
||||
public static bool IconLoader(string filename, ref Texture2D __result, AssetManager __instance)
|
||||
{
|
||||
var matches = Regex.Matches(filename, @"UI_Icon_(\d+)\.png");
|
||||
if (matches.Count < 1)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var id = matches[0].Groups[1].Value;
|
||||
|
||||
var texture = GetIconTexture2D(id);
|
||||
__result = texture ?? __instance.LoadAsset<Texture2D>($"Icon/UI_Icon_{id}.png");
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
[HarmonyPatch]
|
||||
public static class CharaLoader
|
||||
{
|
||||
public static IEnumerable<MethodBase> TargetMethods()
|
||||
{
|
||||
var AM = typeof(AssetManager);
|
||||
return [AM.GetMethod("GetCharacterTexture2D", [typeof(string)])];
|
||||
}
|
||||
|
||||
public static bool Prefix(string filename, ref Texture2D __result, AssetManager __instance)
|
||||
{
|
||||
var matches = Regex.Matches(filename, @"UI_Chara_(\d+)\.png");
|
||||
if (matches.Count < 1)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var id = matches[0].Groups[1].Value;
|
||||
|
||||
var texture = GetCharaTexture2D(id);
|
||||
__result = texture ?? __instance.LoadAsset<Texture2D>($"Chara/UI_Chara_{id}.png");
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
[HarmonyPatch]
|
||||
public static class PartnerLoader
|
||||
{
|
||||
public static IEnumerable<MethodBase> TargetMethods()
|
||||
{
|
||||
var AM = typeof(AssetManager);
|
||||
return [AM.GetMethod("GetPartnerTexture2D", [typeof(string)])];
|
||||
}
|
||||
|
||||
public static bool Prefix(string filename, ref Texture2D __result, AssetManager __instance)
|
||||
{
|
||||
var matches = Regex.Matches(filename, @"UI_Partner_(\d+)\.png");
|
||||
if (matches.Count < 1)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var id = matches[0].Groups[1].Value;
|
||||
|
||||
var texture = GetPartnerTexture2D(id);
|
||||
__result = texture ?? __instance.LoadAsset<Texture2D>($"Partner/UI_Partner_{id}.png");
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
/*
|
||||
[HarmonyPatch]
|
||||
public static class FrameLoader
|
||||
{
|
||||
public static IEnumerable<MethodBase> TargetMethods()
|
||||
{
|
||||
var AM = typeof(AssetManager);
|
||||
return [AM.GetMethod("GetFrameThumbTexture2D", [typeof(string)]), AM.GetMethod("GetFrameTexture2D", [typeof(string)])];
|
||||
}
|
||||
|
||||
public static bool Prefix(string filename, ref Texture2D __result, AssetManager __instance)
|
||||
{
|
||||
var matches = Regex.Matches(filename, @"UI_Frame_(\d+)\.png");
|
||||
if (matches.Count < 1)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var id = matches[0].Groups[1].Value;
|
||||
|
||||
var texture = GetFrameTexture2D(id);
|
||||
__result = texture ?? __instance.LoadAsset<Texture2D>($"Frame/UI_Frame_{id}.png");
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
using System.Linq;
|
||||
using AquaMai.Config.Attributes;
|
||||
using HarmonyLib;
|
||||
using MAI2.Util;
|
||||
using Manager;
|
||||
using MelonLoader;
|
||||
using Monitor.Game;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AquaMai.Mods.GameSystem.Assets;
|
||||
|
||||
[ConfigSection(
|
||||
en: """
|
||||
Use the png jacket above as MV if no .dat found in the movie folder.
|
||||
Use together with `LoadLocalImages`.
|
||||
""",
|
||||
zh: """
|
||||
如果 movie 文件夹中没有 dat 格式的 MV 的话,就用歌曲的封面做背景,而不是显示迪拉熊的笑脸
|
||||
请和 `LoadLocalImages` 一起用
|
||||
""")]
|
||||
public class UseJacketAsDummyMovie
|
||||
{
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(GameCtrl), "IsReady")]
|
||||
public static void LoadLocalBgaAwake(GameObject ____movieMaskObj)
|
||||
{
|
||||
var music = Singleton<DataManager>.Instance.GetMusic(GameManager.SelectMusicID[0]);
|
||||
if (music is null) return;
|
||||
|
||||
var moviePath = Singleton<OptionDataManager>.Instance.GetMovieDataPath($"{music.movieName.id:000000}") + ".dat";
|
||||
if (!moviePath.Contains("dummy")) return;
|
||||
|
||||
var jacket = LoadLocalImages.GetJacketTexture2D(music.movieName.id);
|
||||
if (jacket is null)
|
||||
{
|
||||
MelonLogger.Msg("No jacket found for music " + music);
|
||||
return;
|
||||
}
|
||||
|
||||
var components = ____movieMaskObj.GetComponentsInChildren<Component>(false);
|
||||
var movies = components.Where(it => it.name == "Movie");
|
||||
|
||||
foreach (var movie in movies)
|
||||
{
|
||||
// If I create a new RawImage component, the jacket will be not be displayed
|
||||
// I think it will be difficult to make it work with RawImage
|
||||
// So I change the material that plays video to default sprite material
|
||||
// The original player is actually a sprite renderer and plays video with a custom material
|
||||
var sprite = movie.GetComponent<SpriteRenderer>();
|
||||
sprite.sprite = Sprite.Create(jacket, new Rect(0, 0, jacket.width, jacket.height), new Vector2(0.5f, 0.5f));
|
||||
sprite.material = new Material(Shader.Find("Sprites/Default"));
|
||||
}
|
||||
}
|
||||
}
|
||||
142
AquaMai/AquaMai.Mods/GameSystem/CustomCameraId.cs
Normal file
142
AquaMai/AquaMai.Mods/GameSystem/CustomCameraId.cs
Normal file
@@ -0,0 +1,142 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections;
|
||||
using System.Linq;
|
||||
using HarmonyLib;
|
||||
using Manager;
|
||||
using MelonLoader;
|
||||
using UnityEngine;
|
||||
using AquaMai.Config.Attributes;
|
||||
|
||||
namespace AquaMai.Mods.GameSystem;
|
||||
|
||||
[ConfigSection(
|
||||
en: """
|
||||
Use custom CameraId rather than the default ones.
|
||||
If enabled, you can customize the game to use the specified camera.
|
||||
""",
|
||||
zh: """
|
||||
使用自定义的摄像头 ID 而不是默认的
|
||||
启用后可以指定游戏使用的摄像头
|
||||
""")]
|
||||
public class CustomCameraId
|
||||
{
|
||||
[ConfigEntry(
|
||||
en: "Print the camera list to the log when starting, can be used as a basis for modification.",
|
||||
zh: "启动时打印摄像头列表到日志中,可以作为修改的依据")]
|
||||
public static bool printCameraList;
|
||||
|
||||
[ConfigEntry(
|
||||
en: "DX Pass 1P.",
|
||||
zh: "DX Pass 1P")]
|
||||
public static int leftQrCamera;
|
||||
|
||||
[ConfigEntry(
|
||||
en: "DX Pass 2P.",
|
||||
zh: "DX Pass 2P")]
|
||||
public static int rightQrCamera;
|
||||
|
||||
[ConfigEntry(
|
||||
en: "Player Camera.",
|
||||
zh: "玩家摄像头")]
|
||||
public static int photoCamera;
|
||||
|
||||
[ConfigEntry(
|
||||
en: "WeChat QRCode Camera.",
|
||||
zh: "二维码扫描摄像头")]
|
||||
public static int chimeCamera;
|
||||
|
||||
private static readonly Dictionary<string, string> cameraTypeMap = new()
|
||||
{
|
||||
["LeftQrCamera"] = "QRLeft",
|
||||
["RightQrCamera"] = "QRRight",
|
||||
["PhotoCamera"] = "Photo",
|
||||
["ChimeCamera"] = "Chime",
|
||||
};
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(CameraManager), "CameraInitialize")]
|
||||
public static bool CameraInitialize(CameraManager __instance, ref IEnumerator __result)
|
||||
{
|
||||
__result = CameraInitialize(__instance);
|
||||
return false;
|
||||
}
|
||||
|
||||
private static IEnumerator CameraInitialize(CameraManager __instance)
|
||||
{
|
||||
var textureCache = new WebCamTexture[WebCamTexture.devices.Length];
|
||||
SortedDictionary<CameraManager.CameraTypeEnum, WebCamTexture> webCamTextures = [];
|
||||
foreach (var (configEntry, cameraTypeName) in cameraTypeMap)
|
||||
{
|
||||
int deviceId = Traverse.Create(typeof(CustomCameraId)).Field(configEntry).GetValue<int>();
|
||||
if (deviceId < 0 || deviceId >= WebCamTexture.devices.Length)
|
||||
{
|
||||
MelonLogger.Warning($"[CustomCameraId] Ignoring custom camera {configEntry}: camera ID {deviceId} out of range");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!Enum.TryParse<CameraManager.CameraTypeEnum>(cameraTypeName, out var cameraType))
|
||||
{
|
||||
MelonLogger.Warning($"[CustomCameraId] Ignoring custom camera {configEntry}: camera type {cameraTypeName} not present");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (textureCache[deviceId] != null)
|
||||
{
|
||||
webCamTextures[cameraType] = textureCache[deviceId];
|
||||
}
|
||||
else
|
||||
{
|
||||
var webCamTexture = new WebCamTexture(WebCamTexture.devices[deviceId].name);
|
||||
webCamTextures[cameraType] = webCamTexture;
|
||||
textureCache[deviceId] = webCamTexture;
|
||||
}
|
||||
}
|
||||
|
||||
int textureCount = webCamTextures.Count;
|
||||
__instance.isAvailableCamera = new bool[textureCount];
|
||||
__instance.cameraProcMode = new CameraManager.CameraProcEnum[textureCount];
|
||||
|
||||
int textureIndex = 0;
|
||||
foreach (var (cameraType, webCamTexture) in webCamTextures)
|
||||
{
|
||||
__instance.isAvailableCamera[textureIndex] = true;
|
||||
__instance.cameraProcMode[textureIndex] = CameraManager.CameraProcEnum.Good;
|
||||
CameraManager.DeviceId[(int)cameraType] = textureIndex;
|
||||
textureIndex++;
|
||||
}
|
||||
Traverse.Create(__instance).Field("_webcamtex").SetValue(webCamTextures.Values.ToArray());
|
||||
|
||||
CameraManager.IsReady = true;
|
||||
yield break;
|
||||
}
|
||||
|
||||
public static void OnBeforePatch()
|
||||
{
|
||||
if (!printCameraList)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
WebCamDevice[] devices = WebCamTexture.devices;
|
||||
string cameraList = "Connected Web Cameras:\n";
|
||||
for (int i = 0; i < devices.Length; i++)
|
||||
{
|
||||
WebCamDevice webCamDevice = devices[i];
|
||||
WebCamTexture webCamTexture = new WebCamTexture(webCamDevice.name);
|
||||
webCamTexture.Play();
|
||||
cameraList += "==================================================\n";
|
||||
cameraList += "Name: " + webCamDevice.name + "\n";
|
||||
cameraList += $"ID: {i}\n";
|
||||
cameraList += $"Resolution: {webCamTexture.width} * {webCamTexture.height}\n";
|
||||
cameraList += $"FPS: {webCamTexture.requestedFPS}\n";
|
||||
webCamTexture.Stop();
|
||||
}
|
||||
cameraList += "==================================================";
|
||||
|
||||
foreach (var line in cameraList.Split('\n'))
|
||||
{
|
||||
MelonLogger.Msg($"[CustomCameraId] {line}");
|
||||
}
|
||||
}
|
||||
}
|
||||
65
AquaMai/AquaMai.Mods/GameSystem/DisableTimeout.cs
Normal file
65
AquaMai/AquaMai.Mods/GameSystem/DisableTimeout.cs
Normal file
@@ -0,0 +1,65 @@
|
||||
using AquaMai.Config.Attributes;
|
||||
using HarmonyLib;
|
||||
using Manager;
|
||||
using Monitor;
|
||||
using Process;
|
||||
using Process.Entry.State;
|
||||
using Process.ModeSelect;
|
||||
|
||||
namespace AquaMai.Mods.GameSystem;
|
||||
|
||||
[ConfigSection(
|
||||
en: """
|
||||
Disable timers (hidden and set to 65535 seconds).
|
||||
Not recommand to enable when SinglePlayer is off.
|
||||
""",
|
||||
zh: """
|
||||
去除游戏中的倒计时(隐藏并设为 65535 秒)
|
||||
没有开启单人模式时,不建议启用
|
||||
""")]
|
||||
public class DisableTimeout
|
||||
{
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(TimerController), "PrepareTimer")]
|
||||
public static void PrePrepareTimer(ref int second)
|
||||
{
|
||||
second = 65535;
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(CommonTimer), "SetVisible")]
|
||||
public static void CommonTimerSetVisible(ref bool isVisible)
|
||||
{
|
||||
isVisible = false;
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(EntryProcess), "DecrementTimerSecond")]
|
||||
public static bool EntryProcessDecrementTimerSecond(ContextEntry ____context)
|
||||
{
|
||||
SoundManager.PlaySE(Mai2.Mai2Cue.Cue.SE_SYS_SKIP, 0);
|
||||
____context.SetState(StateType.DoneEntry);
|
||||
return false;
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(ModeSelectProcess), "UpdateInput")]
|
||||
public static bool ModeSelectProcessUpdateInput(ModeSelectProcess __instance)
|
||||
{
|
||||
if (!InputManager.GetButtonDown(0, InputManager.ButtonSetting.Button05)) return true;
|
||||
__instance.TimeSkipButtonAnim(InputManager.ButtonSetting.Button05);
|
||||
SoundManager.PlaySE(Mai2.Mai2Cue.Cue.SE_SYS_SKIP, 0);
|
||||
Traverse.Create(__instance).Method("TimeUp").GetValue();
|
||||
return false;
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(PhotoEditProcess), "MainMenuUpdate")]
|
||||
public static void PhotoEditProcess(PhotoEditMonitor[] ____monitors, PhotoEditProcess __instance)
|
||||
{
|
||||
if (!InputManager.GetButtonDown(0, InputManager.ButtonSetting.Button04)) return;
|
||||
SoundManager.PlaySE(Mai2.Mai2Cue.Cue.SE_SYS_SKIP, 0);
|
||||
____monitors[0].SetButtonPressed(InputManager.ButtonSetting.Button04);
|
||||
Traverse.Create(__instance).Method("OnTimeUp").GetValue();
|
||||
}
|
||||
}
|
||||
80
AquaMai/AquaMai.Mods/GameSystem/KeyMap.cs
Normal file
80
AquaMai/AquaMai.Mods/GameSystem/KeyMap.cs
Normal file
@@ -0,0 +1,80 @@
|
||||
using System.Reflection;
|
||||
using AquaMai.Config.Attributes;
|
||||
using AquaMai.Config.Types;
|
||||
using HarmonyLib;
|
||||
|
||||
namespace AquaMai.Mods.GameSystem;
|
||||
|
||||
[ConfigSection(
|
||||
en: "These settings will work regardless of whether you have enabled segatools' io4 emulation.",
|
||||
zh: "这里的设置无论你是否启用了 segatools 的 io4 模拟都会工作")]
|
||||
public class KeyMap
|
||||
{
|
||||
[ConfigEntry]
|
||||
public static readonly KeyCodeID Test = (KeyCodeID)115;
|
||||
|
||||
[ConfigEntry]
|
||||
private static readonly KeyCodeID Service = (KeyCodeID)5;
|
||||
|
||||
[ConfigEntry]
|
||||
private static readonly KeyCodeID Button1_1P = (KeyCodeID)67;
|
||||
|
||||
[ConfigEntry]
|
||||
private static readonly KeyCodeID Button2_1P = (KeyCodeID)49;
|
||||
|
||||
[ConfigEntry]
|
||||
private static readonly KeyCodeID Button3_1P = (KeyCodeID)48;
|
||||
|
||||
[ConfigEntry]
|
||||
private static readonly KeyCodeID Button4_1P = (KeyCodeID)47;
|
||||
|
||||
[ConfigEntry]
|
||||
private static readonly KeyCodeID Button5_1P = (KeyCodeID)68;
|
||||
|
||||
[ConfigEntry]
|
||||
private static readonly KeyCodeID Button6_1P = (KeyCodeID)70;
|
||||
|
||||
[ConfigEntry]
|
||||
private static readonly KeyCodeID Button7_1P = (KeyCodeID)45;
|
||||
|
||||
[ConfigEntry]
|
||||
private static readonly KeyCodeID Button8_1P = (KeyCodeID)61;
|
||||
|
||||
[ConfigEntry]
|
||||
private static readonly KeyCodeID Select_1P = (KeyCodeID)25;
|
||||
|
||||
[ConfigEntry]
|
||||
private static readonly KeyCodeID Button1_2P = (KeyCodeID)80;
|
||||
|
||||
[ConfigEntry]
|
||||
private static readonly KeyCodeID Button2_2P = (KeyCodeID)81;
|
||||
|
||||
[ConfigEntry]
|
||||
private static readonly KeyCodeID Button3_2P = (KeyCodeID)78;
|
||||
|
||||
[ConfigEntry]
|
||||
private static readonly KeyCodeID Button4_2P = (KeyCodeID)75;
|
||||
|
||||
[ConfigEntry]
|
||||
private static readonly KeyCodeID Button5_2P = (KeyCodeID)74;
|
||||
|
||||
[ConfigEntry]
|
||||
private static readonly KeyCodeID Button6_2P = (KeyCodeID)73;
|
||||
|
||||
[ConfigEntry]
|
||||
private static readonly KeyCodeID Button7_2P = (KeyCodeID)76;
|
||||
|
||||
[ConfigEntry]
|
||||
private static readonly KeyCodeID Button8_2P = (KeyCodeID)79;
|
||||
|
||||
[ConfigEntry]
|
||||
private static readonly KeyCodeID Select_2P = (KeyCodeID)84;
|
||||
|
||||
[HarmonyPatch(typeof(DB.JvsButtonTableRecord), MethodType.Constructor, typeof(int), typeof(string), typeof(string), typeof(int), typeof(string), typeof(int), typeof(int), typeof(int))]
|
||||
[HarmonyPostfix]
|
||||
public static void JvsButtonTableRecordConstructor(DB.JvsButtonTableRecord __instance, string Name)
|
||||
{
|
||||
var prop = (DB.KeyCodeID)typeof(KeyMap).GetField(Name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static).GetValue(null);
|
||||
__instance.SubstituteKey = prop;
|
||||
}
|
||||
}
|
||||
34
AquaMai/AquaMai.Mods/GameSystem/QuickRetry.cs
Normal file
34
AquaMai/AquaMai.Mods/GameSystem/QuickRetry.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using AquaMai.Core.Attributes;
|
||||
using AquaMai.Config.Attributes;
|
||||
using HarmonyLib;
|
||||
using Manager;
|
||||
|
||||
namespace AquaMai.Mods.GameSystem;
|
||||
|
||||
[ConfigSection(
|
||||
en: "Hold the bottom four buttons (3456) for quick retry (like in Freedom Mode, default non-utage only).",
|
||||
zh: "按住下方四个按钮(3456)快速重开本局游戏(像在 Freedom Mode 中一样,默认仅对非宴谱有效)")]
|
||||
[EnableGameVersion(23000)]
|
||||
public class QuickRetry
|
||||
{
|
||||
[ConfigEntry(
|
||||
en: "Force enable in Utage.",
|
||||
zh: "在宴谱中强制启用")]
|
||||
private static readonly bool enableInUtage = false;
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(Monitor.QuickRetry), "IsQuickRetryEnable")]
|
||||
public static bool OnQuickRetryIsQuickRetryEnable(ref bool __result)
|
||||
{
|
||||
if (enableInUtage)
|
||||
{
|
||||
__result = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
var isUtageProperty = Traverse.Create(typeof(GameManager)).Property("IsUtage");
|
||||
__result = !isUtageProperty.PropertyExists() || !isUtageProperty.GetValue<bool>();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
5
AquaMai/AquaMai.Mods/GameSystem/README.md
Normal file
5
AquaMai/AquaMai.Mods/GameSystem/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# GameSystem
|
||||
|
||||
Patches changing the way the game running / behaving which are not possible in the stock game. See also the [GameSettings README](../GameSettings/README.md) for differences.
|
||||
|
||||
Game asset related patches should go to the Assets subcategory (or the Fancy category if they're too fancy).
|
||||
63
AquaMai/AquaMai.Mods/GameSystem/RemoveEncryption.cs
Normal file
63
AquaMai/AquaMai.Mods/GameSystem/RemoveEncryption.cs
Normal file
@@ -0,0 +1,63 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using AquaMai.Config.Attributes;
|
||||
using HarmonyLib;
|
||||
using Net.Packet;
|
||||
|
||||
namespace AquaMai.Mods.GameSystem;
|
||||
|
||||
[ConfigSection(
|
||||
en: """
|
||||
If you are using an unmodified client, requests to the server will be encrypted by default, but requests to the private server should not be encrypted.
|
||||
With this option enabled, the connection will not be encrypted, and the suffix added by different versions of the client to the API names are also removed.
|
||||
Please keep this option enabled normally.
|
||||
""",
|
||||
zh: """
|
||||
如果你在用未经修改的客户端,会默认加密到服务器的连接,而连接私服的时候不应该加密
|
||||
开了这个选项之后就不会加密连接了,同时也会移除不同版本的客户端可能会对 API 接口加的后缀
|
||||
正常情况下,请保持这个选项开启
|
||||
""",
|
||||
defaultOn: true)]
|
||||
public class RemoveEncryption
|
||||
{
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(Packet), "Obfuscator", typeof(string))]
|
||||
public static bool PreObfuscator(string srcStr, ref string __result)
|
||||
{
|
||||
__result = srcStr.Replace("MaimaiExp", "").Replace("MaimaiChn", "");
|
||||
return false;
|
||||
}
|
||||
|
||||
[HarmonyPatch]
|
||||
public class EncryptDecrypt
|
||||
{
|
||||
public static IEnumerable<MethodBase> TargetMethods()
|
||||
{
|
||||
var methods = AccessTools.TypeByName("Net.CipherAES").GetMethods();
|
||||
return
|
||||
[
|
||||
methods.FirstOrDefault(it => it.Name == "Encrypt" && it.IsPublic),
|
||||
methods.FirstOrDefault(it => it.Name == "Decrypt" && it.IsPublic)
|
||||
];
|
||||
}
|
||||
|
||||
public static bool Prefix(object[] __args, ref object __result)
|
||||
{
|
||||
if (__args.Length == 1)
|
||||
{
|
||||
// public static byte[] Encrypt(byte[] data)
|
||||
// public static byte[] Decrypt(byte[] encryptData)
|
||||
__result = __args[0];
|
||||
}
|
||||
else if (__args.Length == 2)
|
||||
{
|
||||
// public static bool Encrypt(byte[] data, out byte[] encryptData)
|
||||
// public static bool Decrypt(byte[] encryptData, out byte[] plainData)
|
||||
__args[1] = __args[0];
|
||||
__result = true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
114
AquaMai/AquaMai.Mods/GameSystem/SinglePlayer.cs
Normal file
114
AquaMai/AquaMai.Mods/GameSystem/SinglePlayer.cs
Normal file
@@ -0,0 +1,114 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using AquaMai.Core;
|
||||
using AquaMai.Core.Attributes;
|
||||
using AquaMai.Config.Attributes;
|
||||
using AquaMai.Mods.Fancy.GamePlay;
|
||||
using HarmonyLib;
|
||||
using MAI2.Util;
|
||||
using Manager;
|
||||
using MelonLoader;
|
||||
using Monitor;
|
||||
using Monitor.Common;
|
||||
using Monitor.Entry;
|
||||
using Monitor.Entry.Parts.Screens;
|
||||
using UnityEngine;
|
||||
using Fx;
|
||||
using Type = System.Type;
|
||||
|
||||
namespace AquaMai.Mods.GameSystem;
|
||||
|
||||
// Hides the 2p (right hand side) UI.
|
||||
// Note: this is not my original work. I simply interpreted the code and rewrote it as a mod.
|
||||
[ConfigSection(
|
||||
en: "Single player: Show 1P only, at the center of the screen.",
|
||||
zh: "单人模式,不显示 2P")]
|
||||
public class SinglePlayer
|
||||
{
|
||||
[HarmonyPatch]
|
||||
public class WhateverInitialize
|
||||
{
|
||||
public static IEnumerable<MethodBase> TargetMethods()
|
||||
{
|
||||
var lateInitialize = AccessTools.Method(typeof(Main.GameMain), "LateInitialize", [typeof(MonoBehaviour), typeof(Transform), typeof(Transform)]);
|
||||
if (lateInitialize is not null) return [lateInitialize];
|
||||
return [AccessTools.Method(typeof(Main.GameMain), "Initialize", [typeof(MonoBehaviour), typeof(Transform), typeof(Transform)])];
|
||||
}
|
||||
|
||||
public static void Prefix(MonoBehaviour gameMainObject, ref Transform left, ref Transform right)
|
||||
{
|
||||
left.transform.position = Vector3.zero;
|
||||
right.localScale = Vector3.zero;
|
||||
GameObject.Find("Mask").transform.position = new Vector3(540f, 0f, 0f);
|
||||
}
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(MeshButton), "IsPointInPolygon", new Type[] { typeof(Vector2[]), typeof(Vector2) })]
|
||||
public static bool IsPointInPolygon(Vector2[] polygon, ref Vector2 point, MeshButton __instance, ref bool __result)
|
||||
{
|
||||
__result = RectTransformUtility.RectangleContainsScreenPoint(__instance.GetComponent<RectTransform>(), point, Camera.main);
|
||||
return false;
|
||||
}
|
||||
|
||||
[EnableGameVersion(21500, noWarn: true)]
|
||||
public class SkipTimer
|
||||
{
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(EntryMonitor), "DecideEntry")]
|
||||
public static void PostDecideEntry(EntryMonitor __instance)
|
||||
{
|
||||
# if DEBUG
|
||||
MelonLogger.Msg("Confirm Entry");
|
||||
# endif
|
||||
TimeManager.MarkGameStartTime();
|
||||
Singleton<EventManager>.Instance.UpdateEvent();
|
||||
Singleton<ScoreRankingManager>.Instance.UpdateData();
|
||||
__instance.Process.CreateDownloadProcess();
|
||||
__instance.ProcessManager.SendMessage(new Message(ProcessType.CommonProcess, 30001));
|
||||
__instance.ProcessManager.SendMessage(new Message(ProcessType.CommonProcess, 40000, 0, OperationInformationController.InformationType.Hide));
|
||||
__instance.Process.SetNextProcess();
|
||||
}
|
||||
|
||||
// To prevent the "長押受付終了" overlay from appearing
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(WaitPartner), "Open")]
|
||||
public static bool WaitPartnerPreOpen()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
[ConfigEntry(
|
||||
en: "Fix hanabi effect under single-player mode (disabled automatically if HideHanabi is enabled).",
|
||||
zh: "修复单人模式下的烟花效果(如果启用了 HideHanabi,则会自动禁用)")]
|
||||
public static bool fixHanabi = true;
|
||||
|
||||
private static bool fixHanabiDisableImplied = false;
|
||||
private static bool FixHanabiEnabled => fixHanabi && !fixHanabiDisableImplied;
|
||||
|
||||
[EnableIf(nameof(FixHanabiEnabled))]
|
||||
[HarmonyPatch(typeof(TapCEffect), "SetUpParticle")]
|
||||
[HarmonyPostfix]
|
||||
public static void PostSetUpParticle(TapCEffect __instance, FX_Mai2_Note_Color ____particleControler)
|
||||
{
|
||||
var entities = ____particleControler.GetComponentsInChildren<ParticleSystemRenderer>(true);
|
||||
foreach (var entity in entities)
|
||||
{
|
||||
entity.maxParticleSize = 1f;
|
||||
}
|
||||
}
|
||||
|
||||
public static void OnBeforePatch()
|
||||
{
|
||||
if (ConfigLoader.Config.GetSectionState(typeof(HideHanabi)).Enabled)
|
||||
{
|
||||
fixHanabiDisableImplied = true;
|
||||
}
|
||||
}
|
||||
|
||||
public static void OnAfterPatch()
|
||||
{
|
||||
Core.Helpers.GuiSizes.SinglePlayer = true;
|
||||
}
|
||||
}
|
||||
69
AquaMai/AquaMai.Mods/GameSystem/TestProof.cs
Normal file
69
AquaMai/AquaMai.Mods/GameSystem/TestProof.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using AquaMai.Config.Attributes;
|
||||
using AquaMai.Config.Types;
|
||||
using AquaMai.Core;
|
||||
using AquaMai.Core.Attributes;
|
||||
using AquaMai.Core.Helpers;
|
||||
using AquaMai.Mods.UX;
|
||||
using AquaMai.Mods.UX.PracticeMode;
|
||||
using HarmonyLib;
|
||||
using Manager;
|
||||
|
||||
namespace AquaMai.Mods.GameSystem;
|
||||
|
||||
[ConfigSection(
|
||||
en: """
|
||||
When enabled, test button must be long pressed to enter game test mode.
|
||||
When test button is bound to other features, this option is enabled automatically.
|
||||
""",
|
||||
zh: """
|
||||
启用后,测试键必须长按才能进入游戏测试模式
|
||||
当测试键被绑定到其它功能时,此选项自动开启
|
||||
""")]
|
||||
[EnableImplicitlyIf(nameof(ShouldEnableImplicitly))]
|
||||
public class TestProof
|
||||
{
|
||||
public static bool ShouldEnableImplicitly
|
||||
{
|
||||
get
|
||||
{
|
||||
(System.Type section, KeyCodeOrName key)[] featureKeys =
|
||||
[
|
||||
(typeof(OneKeyEntryEnd), OneKeyEntryEnd.key),
|
||||
(typeof(OneKeyRetrySkip), OneKeyRetrySkip.retryKey),
|
||||
(typeof(OneKeyRetrySkip), OneKeyRetrySkip.skipKey),
|
||||
(typeof(HideSelfMadeCharts), HideSelfMadeCharts.key),
|
||||
(typeof(PracticeMode), PracticeMode.key),
|
||||
];
|
||||
var keyMapEnabled = ConfigLoader.Config.GetSectionState(typeof(KeyMap)).Enabled;
|
||||
return featureKeys.Any(it =>
|
||||
// The feature is enabled and...
|
||||
ConfigLoader.Config.GetSectionState(it.section).Enabled &&
|
||||
(
|
||||
// and the key is test, or...
|
||||
it.key == KeyCodeOrName.Test ||
|
||||
// or the key have been mapped to the same key as test.
|
||||
(keyMapEnabled && it.key.ToString() == KeyMap.Test.ToString())));
|
||||
}
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(InputManager), "GetSystemInputDown")]
|
||||
public static bool GetSystemInputDown(ref bool __result, InputManager.SystemButtonSetting button, bool[] ___SystemButtonDown)
|
||||
{
|
||||
__result = ___SystemButtonDown[(int)button];
|
||||
if (button != InputManager.SystemButtonSetting.ButtonTest)
|
||||
return false;
|
||||
|
||||
var stackTrace = new StackTrace(); // get call stack
|
||||
var stackFrames = stackTrace.GetFrames(); // get method calls (frames)
|
||||
|
||||
if (stackFrames.Any(it => it.GetMethod().Name == "DMD<Main.GameMainObject::Update>"))
|
||||
{
|
||||
__result = KeyListener.GetKeyDownOrLongPress(KeyCodeOrName.Test, true);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
30
AquaMai/AquaMai.Mods/GameSystem/TouchPanelBaudRate.cs
Normal file
30
AquaMai/AquaMai.Mods/GameSystem/TouchPanelBaudRate.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using AquaMai.Config.Attributes;
|
||||
using HarmonyLib;
|
||||
using IO;
|
||||
|
||||
namespace AquaMai.Mods.GameSystem;
|
||||
|
||||
[ConfigSection(
|
||||
en: """
|
||||
Adjust the baud rate of the touch screen serial port, default value is 9600.
|
||||
Requires hardware support. If you are unsure, don't use it.
|
||||
""",
|
||||
zh: """
|
||||
调整触摸屏串口波特率,默认值 9600
|
||||
需要硬件配合,如果你不清楚你是否可以使用,请不要使用
|
||||
""")]
|
||||
public class TouchPanelBaudRate
|
||||
{
|
||||
[ConfigEntry(
|
||||
en: "Baud rate.",
|
||||
zh: "波特率")]
|
||||
private static readonly int baudRate = 9600;
|
||||
|
||||
[HarmonyPatch(typeof(NewTouchPanel), "Open")]
|
||||
[HarmonyPrefix]
|
||||
private static void OpenPrefix(ref int ___BaudRate)
|
||||
{
|
||||
if (baudRate <= 0) return;
|
||||
___BaudRate = baudRate;
|
||||
}
|
||||
}
|
||||
59
AquaMai/AquaMai.Mods/GameSystem/TouchToButtonInput.cs
Normal file
59
AquaMai/AquaMai.Mods/GameSystem/TouchToButtonInput.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
using AquaMai.Config.Attributes;
|
||||
using HarmonyLib;
|
||||
using Process;
|
||||
using static Manager.InputManager;
|
||||
|
||||
namespace AquaMai.Mods.GameSystem;
|
||||
|
||||
[ConfigSection(
|
||||
en: "Map touch actions to buttons.",
|
||||
zh: "映射触摸操作至实体按键")]
|
||||
public class TouchToButtonInput
|
||||
{
|
||||
private static bool _isPlaying = false;
|
||||
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(GameProcess), "OnStart")]
|
||||
public static void OnGameProcessStart(GameProcess __instance)
|
||||
{
|
||||
_isPlaying = true;
|
||||
}
|
||||
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(GameProcess), "OnRelease")]
|
||||
public static void OnGameProcessRelease(GameProcess __instance)
|
||||
{
|
||||
_isPlaying = false;
|
||||
}
|
||||
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(Manager.InputManager), "GetButtonDown")]
|
||||
public static void GetButtonDown(ref bool __result, int monitorId, ButtonSetting button)
|
||||
{
|
||||
if (_isPlaying || __result) return;
|
||||
if (button.ToString().StartsWith("Button"))
|
||||
{
|
||||
__result = GetTouchPanelAreaDown(monitorId, (TouchPanelArea)button);
|
||||
}
|
||||
else if (button.ToString().Equals("Select"))
|
||||
{
|
||||
__result = GetTouchPanelAreaLongPush(monitorId, TouchPanelArea.C1, 500L) || GetTouchPanelAreaLongPush(monitorId, TouchPanelArea.C2, 500L);
|
||||
}
|
||||
}
|
||||
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(Manager.InputManager), "GetButtonPush")]
|
||||
public static void GetButtonPush(ref bool __result, int monitorId, ButtonSetting button)
|
||||
{
|
||||
if (_isPlaying || __result) return;
|
||||
if (button.ToString().StartsWith("Button")) __result = GetTouchPanelAreaPush(monitorId, (TouchPanelArea)button);
|
||||
}
|
||||
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(Manager.InputManager), "GetButtonLongPush")]
|
||||
public static void GetButtonLongPush(ref bool __result, int monitorId, ButtonSetting button, long msec)
|
||||
{
|
||||
if (_isPlaying || __result) return;
|
||||
if (button.ToString().StartsWith("Button")) __result = GetTouchPanelAreaLongPush(monitorId, (TouchPanelArea)button, msec);
|
||||
}
|
||||
}
|
||||
90
AquaMai/AquaMai.Mods/GameSystem/Unlock.cs
Normal file
90
AquaMai/AquaMai.Mods/GameSystem/Unlock.cs
Normal file
@@ -0,0 +1,90 @@
|
||||
using AquaMai.Config.Attributes;
|
||||
using AquaMai.Core.Attributes;
|
||||
using MAI2System;
|
||||
using Manager;
|
||||
using Manager.MaiStudio;
|
||||
using HarmonyLib;
|
||||
|
||||
namespace AquaMai.Mods.GameSystem;
|
||||
|
||||
[ConfigSection(
|
||||
en: "Unlock normally locked (including normally non-unlockable) game content.",
|
||||
zh: "解锁原本锁定(包括正常途径无法解锁)的游戏内容")]
|
||||
public class Unlock
|
||||
{
|
||||
[ConfigEntry(
|
||||
en: "Unlock maps that are not in this version.",
|
||||
zh: "解锁游戏里所有的区域,包括非当前版本的(并不会帮你跑完)")]
|
||||
private static readonly bool maps = true;
|
||||
|
||||
[EnableIf(nameof(maps))]
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(MapData), "get_OpenEventId")]
|
||||
public static bool get_OpenEventId(ref StringID __result)
|
||||
{
|
||||
// For any map, return the event ID 1 to unlock it
|
||||
var id = new Manager.MaiStudio.Serialize.StringID
|
||||
{
|
||||
id = 1,
|
||||
str = "無期限常時解放"
|
||||
};
|
||||
|
||||
var sid = new StringID();
|
||||
sid.Init(id);
|
||||
|
||||
__result = sid;
|
||||
return false;
|
||||
}
|
||||
|
||||
[ConfigEntry(
|
||||
en: "Unlock normally event-only tickets.",
|
||||
zh: "解锁游戏里所有可能的跑图券")]
|
||||
private static readonly bool tickets = true;
|
||||
|
||||
[EnableIf(nameof(tickets))]
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(TicketData), "get_ticketEvent")]
|
||||
public static bool get_ticketEvent(ref StringID __result)
|
||||
{
|
||||
// For any ticket, return the event ID 1 to unlock it
|
||||
var id = new Manager.MaiStudio.Serialize.StringID
|
||||
{
|
||||
id = 1,
|
||||
str = "無期限常時解放"
|
||||
};
|
||||
|
||||
var sid = new StringID();
|
||||
sid.Init(id);
|
||||
|
||||
__result = sid;
|
||||
return false;
|
||||
}
|
||||
|
||||
[EnableIf(nameof(tickets))]
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(TicketData), "get_maxCount")]
|
||||
public static bool get_maxCount(ref int __result)
|
||||
{
|
||||
// Modify the maxTicketNum to 0
|
||||
// this is because TicketManager.GetTicketData adds the ticket to the list if either
|
||||
// the player owns at least one ticket or the maxTicketNum = 0
|
||||
__result = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
[ConfigEntry(
|
||||
en: "Unlock Utage without the need of DXRating 10000.",
|
||||
zh: "不需要万分也可以进宴会场")]
|
||||
private static readonly bool utage = true;
|
||||
|
||||
[EnableIf(nameof(utage))]
|
||||
[EnableGameVersion(24000)]
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(GameManager), "CanUnlockUtageTotalJudgement")]
|
||||
public static bool CanUnlockUtageTotalJudgement(out ConstParameter.ResultOfUnlockUtageJudgement result1P, out ConstParameter.ResultOfUnlockUtageJudgement result2P)
|
||||
{
|
||||
result1P = ConstParameter.ResultOfUnlockUtageJudgement.Unlocked;
|
||||
result2P = ConstParameter.ResultOfUnlockUtageJudgement.Unlocked;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
107
AquaMai/AquaMai.Mods/GameSystem/Window.cs
Normal file
107
AquaMai/AquaMai.Mods/GameSystem/Window.cs
Normal file
@@ -0,0 +1,107 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading.Tasks;
|
||||
using AquaMai.Config.Attributes;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AquaMai.Mods.GameSystem;
|
||||
|
||||
[ConfigSection(
|
||||
en: "Windowed Mode / Window Settings.",
|
||||
zh: "窗口化/窗口设置")]
|
||||
public class Window
|
||||
{
|
||||
[ConfigEntry(
|
||||
en: "Window the game.",
|
||||
zh: "窗口化游戏")]
|
||||
private static readonly bool windowed = false;
|
||||
|
||||
[ConfigEntry(
|
||||
en: """
|
||||
Window width (and height) for windowed mode, rendering resolution for fullscreen mode.
|
||||
If set to 0, windowed mode will remember the user-set size, fullscreen mode will use the current display resolution.
|
||||
""",
|
||||
zh: """
|
||||
宽度(和高度)窗口化时为游戏窗口大小,全屏时为渲染分辨率
|
||||
如果设为 0,窗口化将记住用户设定的大小,全屏时将使用当前显示器分辨率
|
||||
""")]
|
||||
private static readonly int width = 0;
|
||||
|
||||
[ConfigEntry(
|
||||
en: "Height, as above.",
|
||||
zh: "高度,同上")]
|
||||
private static readonly int height = 0;
|
||||
|
||||
private const int GWL_STYLE = -16;
|
||||
private const int WS_WHATEVER = 0x14CF0000;
|
||||
|
||||
private static IntPtr hwnd = IntPtr.Zero;
|
||||
|
||||
public static void OnBeforePatch()
|
||||
{
|
||||
if (windowed)
|
||||
{
|
||||
var alreadyWindowed = Screen.fullScreenMode == FullScreenMode.Windowed;
|
||||
if (width == 0 || height == 0)
|
||||
{
|
||||
Screen.fullScreenMode = FullScreenMode.Windowed;
|
||||
}
|
||||
else
|
||||
{
|
||||
alreadyWindowed = false;
|
||||
Screen.SetResolution(width, height, FullScreenMode.Windowed);
|
||||
}
|
||||
|
||||
hwnd = GetWindowHandle();
|
||||
if (alreadyWindowed)
|
||||
{
|
||||
SetResizeable();
|
||||
}
|
||||
else
|
||||
{
|
||||
Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(3000);
|
||||
// Screen.SetResolution has delay
|
||||
SetResizeable();
|
||||
});
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var width = Window.width == 0 ? Display.main.systemWidth : Window.width;
|
||||
var height = Window.height == 0 ? Display.main.systemHeight : Window.height;
|
||||
Screen.SetResolution(width, height, FullScreenMode.FullScreenWindow);
|
||||
}
|
||||
}
|
||||
|
||||
public static void SetResizeable()
|
||||
{
|
||||
if (hwnd == IntPtr.Zero) return;
|
||||
SetWindowLongPtr(hwnd, GWL_STYLE, WS_WHATEVER);
|
||||
}
|
||||
|
||||
private delegate bool EnumThreadDelegate(IntPtr hwnd, IntPtr lParam);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
static extern bool EnumThreadWindows(int dwThreadId, EnumThreadDelegate lpfn, IntPtr lParam);
|
||||
|
||||
[DllImport("Kernel32.dll")]
|
||||
static extern int GetCurrentThreadId();
|
||||
|
||||
static IntPtr GetWindowHandle()
|
||||
{
|
||||
IntPtr returnHwnd = IntPtr.Zero;
|
||||
var threadId = GetCurrentThreadId();
|
||||
EnumThreadWindows(threadId,
|
||||
(hWnd, lParam) =>
|
||||
{
|
||||
if (returnHwnd == IntPtr.Zero) returnHwnd = hWnd;
|
||||
return true;
|
||||
}, IntPtr.Zero);
|
||||
return returnHwnd;
|
||||
}
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
static extern IntPtr SetWindowLongPtr(IntPtr hWnd, int nIndex, int dwNewLong);
|
||||
}
|
||||
42
AquaMai/AquaMai.Mods/General.cs
Normal file
42
AquaMai/AquaMai.Mods/General.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using AquaMai.Config.Attributes;
|
||||
|
||||
namespace AquaMai.Mods;
|
||||
|
||||
// This class is for settings only. Don't patch anything here.
|
||||
|
||||
[ConfigSection(
|
||||
en: "AquaMai's general settings.",
|
||||
zh: "AquaMai 的通用设置",
|
||||
alwaysEnabled: true)]
|
||||
public class General
|
||||
{
|
||||
[ConfigEntry(
|
||||
en: """
|
||||
Language for mod UI (en and zh supported).
|
||||
If empty, the system language will be used.
|
||||
The config file will also be saved in this language.
|
||||
""",
|
||||
zh: """
|
||||
Mod 界面的语言,支持 en 和 zh
|
||||
如果为空,将使用系统语言
|
||||
配置文件也将以此语言保存
|
||||
""",
|
||||
specialConfigEntry: SpecialConfigEntry.Locale)]
|
||||
public static readonly string locale = "";
|
||||
}
|
||||
|
||||
// Please add/remove corresponding entries in SetionNameOrder enum when adding/removing sections.
|
||||
public enum SetionNameOrder
|
||||
{
|
||||
DeprecationWarning,
|
||||
General,
|
||||
Fix,
|
||||
GameSystem_Assets,
|
||||
GameSystem,
|
||||
GameSettings,
|
||||
Tweaks,
|
||||
Tweaks_TimeSaving,
|
||||
UX,
|
||||
Utils,
|
||||
Fancy
|
||||
}
|
||||
19
AquaMai/AquaMai.Mods/Tweaks/IgnoreAimeServerError.cs
Normal file
19
AquaMai/AquaMai.Mods/Tweaks/IgnoreAimeServerError.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using AquaMai.Config.Attributes;
|
||||
using HarmonyLib;
|
||||
using Manager;
|
||||
|
||||
namespace AquaMai.Mods.Tweaks;
|
||||
|
||||
[ConfigSection(
|
||||
en: "Prevent gray network caused by mistakenly thinking it's an AimeDB server issue.",
|
||||
zh: "防止因错误认为 AimeDB 服务器问题引起的灰网,建议开启")]
|
||||
public class IgnoreAimeServerError
|
||||
{
|
||||
[HarmonyPatch(typeof(OperationManager), "IsAliveAimeServer", MethodType.Getter)]
|
||||
[HarmonyPrefix]
|
||||
public static bool Prefix(ref bool __result)
|
||||
{
|
||||
__result = true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
22
AquaMai/AquaMai.Mods/Tweaks/LockFrameRate.cs
Normal file
22
AquaMai/AquaMai.Mods/Tweaks/LockFrameRate.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using AquaMai.Config.Attributes;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AquaMai.Mods.Tweaks;
|
||||
|
||||
[ConfigSection(
|
||||
en: """
|
||||
Force the frame rate limit to 60 FPS and disable vSync.
|
||||
Do not use if your game has no issues.
|
||||
""",
|
||||
zh: """
|
||||
强制设置帧率上限为 60 帧并关闭垂直同步
|
||||
如果你的游戏没有问题,请不要使用
|
||||
""")]
|
||||
public class LockFrameRate
|
||||
{
|
||||
public static void OnBeforePatch()
|
||||
{
|
||||
Application.targetFrameRate = 60;
|
||||
QualitySettings.vSyncCount = 0;
|
||||
}
|
||||
}
|
||||
5
AquaMai/AquaMai.Mods/Tweaks/README.md
Normal file
5
AquaMai/AquaMai.Mods/Tweaks/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Tweaks
|
||||
|
||||
Patches to make the game more stable, more robust and less annoying. The game is playable at all without them, but sometimes they help a lot.
|
||||
|
||||
These patches don't change the way the game behaving, otherwise they may go to the GameSystem category.
|
||||
29
AquaMai/AquaMai.Mods/Tweaks/ResetTouchAfterTrack.cs
Normal file
29
AquaMai/AquaMai.Mods/Tweaks/ResetTouchAfterTrack.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using AquaMai.Config.Attributes;
|
||||
using HarmonyLib;
|
||||
using MAI2.Util;
|
||||
using Manager;
|
||||
using Process;
|
||||
|
||||
namespace AquaMai.Mods.Tweaks;
|
||||
|
||||
[ConfigSection(
|
||||
en: "Reset touch panel after playing track.",
|
||||
zh: "在游玩一首曲目后重置触摸面板")]
|
||||
public class ResetTouchAfterTrack
|
||||
{
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(ResultProcess), "OnStart")]
|
||||
public static void ResultProcessOnStart()
|
||||
{
|
||||
SingletonStateMachine<AmManager, AmManager.EState>.Instance.StartTouchPanel();
|
||||
MelonLoader.MelonLogger.Msg("[TouchResetAfterTrack] Touch panel reset");
|
||||
}
|
||||
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(GamePlayManager), "SetQuickRetryFrag")]
|
||||
public static void OnStart()
|
||||
{
|
||||
SingletonStateMachine<AmManager, AmManager.EState>.Instance.StartTouchPanel();
|
||||
MelonLoader.MelonLogger.Msg("[TouchResetAfterTrack] Touch panel reset");
|
||||
}
|
||||
}
|
||||
22
AquaMai/AquaMai.Mods/Tweaks/SkipUserVersionCheck.cs
Normal file
22
AquaMai/AquaMai.Mods/Tweaks/SkipUserVersionCheck.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using AquaMai.Config.Attributes;
|
||||
using HarmonyLib;
|
||||
using Process.Entry.State;
|
||||
|
||||
namespace AquaMai.Mods.Tweaks;
|
||||
|
||||
[ConfigSection(
|
||||
en: "Allow login with higher data version.",
|
||||
zh: """
|
||||
原先如果你的账号版本比当前游戏设定的版本高的话,就会不能登录
|
||||
开了这个选项之后就可以登录了,不过你的账号版本还是会被设定为当前游戏的版本
|
||||
""")]
|
||||
public class SkipUserVersionCheck
|
||||
{
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(ConfirmPlay), "IsValidVersion")]
|
||||
public static bool IsValidVersion(ref bool __result)
|
||||
{
|
||||
__result = true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
using AquaMai.Config.Attributes;
|
||||
using DB;
|
||||
using HarmonyLib;
|
||||
using MAI2.Util;
|
||||
using Manager;
|
||||
using Monitor;
|
||||
using Process;
|
||||
using Process.Information;
|
||||
|
||||
namespace AquaMai.Mods.Tweaks.TimeSaving;
|
||||
|
||||
[ConfigSection(
|
||||
en: "Directly enter the song selection screen after login.",
|
||||
zh: "登录完成后直接进入选歌界面")]
|
||||
public class EntryToMusicSelection
|
||||
{
|
||||
/*
|
||||
* Highly experimental, may well break some stuff
|
||||
* Works by overriding the info screen (where it shows new events and stuff)
|
||||
* to directly exit to the music selection screen, skipping character and
|
||||
* event selection, among others
|
||||
*/
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(InformationProcess), "OnUpdate")]
|
||||
public static bool OnUpdate(InformationProcess __instance, ProcessDataContainer ___container)
|
||||
{
|
||||
GameManager.SetMaxTrack();
|
||||
// Set headphone volume
|
||||
for (var i = 0; i < 2; i++)
|
||||
{
|
||||
var userData = UserDataManager.Instance.GetUserData(i);
|
||||
if (userData.IsEntry)
|
||||
{
|
||||
OptionHeadphonevolumeID headPhoneVolume = userData.Option.HeadPhoneVolume;
|
||||
SoundManager.SetHeadPhoneVolume(i, headPhoneVolume.GetValue());
|
||||
}
|
||||
}
|
||||
___container.processManager.AddProcess(new MusicSelectProcess(___container));
|
||||
___container.processManager.ReleaseProcess(__instance);
|
||||
return false;
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(MapResultMonitor), "Initialize")]
|
||||
public static void MapResultMonitorPreInitialize(int monIndex)
|
||||
{
|
||||
var userData = Singleton<UserDataManager>.Instance.GetUserData(monIndex);
|
||||
var index = userData.MapList.FindIndex((m) => m.ID == userData.Detail.SelectMapID);
|
||||
if (index >= 0) return;
|
||||
userData.MapList.Clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using AquaMai.Config.Attributes;
|
||||
using HarmonyLib;
|
||||
using Monitor;
|
||||
|
||||
namespace AquaMai.Mods.Tweaks.TimeSaving;
|
||||
|
||||
[ConfigSection(
|
||||
en: "Skip the \"Do not tap or slide vigorously\" screen, immediately proceed to the next screen once data is loaded.",
|
||||
zh: "跳过“不要大力拍打或滑动哦”这个界面,数据一旦加载完就立马进入下一个界面")]
|
||||
public class IWontTapOrSlideVigorously
|
||||
{
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(PlInformationMonitor), "IsPlayPlInfoEnd")]
|
||||
public static bool Patch(ref bool __result)
|
||||
{
|
||||
__result = true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
27
AquaMai/AquaMai.Mods/Tweaks/TimeSaving/SkipEventInfo.cs
Normal file
27
AquaMai/AquaMai.Mods/Tweaks/TimeSaving/SkipEventInfo.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using System.Collections.Generic;
|
||||
using AquaMai.Config.Attributes;
|
||||
using HarmonyLib;
|
||||
using Process;
|
||||
using Process.Information;
|
||||
|
||||
namespace AquaMai.Mods.Tweaks.TimeSaving;
|
||||
|
||||
[ConfigSection(
|
||||
en: "Skip possible prompts like \"New area discovered\", \"New songs added\", \"There are events\" during game login/registration.",
|
||||
zh: "跳过登录 / 注册游戏时候可能的 “发现了新的区域哟” “乐曲增加” “有活动哟” 之类的提示")]
|
||||
public class SkipEventInfo
|
||||
{
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(InformationProcess), "OnStart")]
|
||||
public static void InformationProcessPostStart(ref uint ____state)
|
||||
{
|
||||
____state = 3;
|
||||
}
|
||||
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(RegionalSelectProcess), "OnStart")]
|
||||
public static void RegionalSelectProcessPreStart(ref Queue<int>[] ____discoverList)
|
||||
{
|
||||
____discoverList = new Queue<int>[] { new Queue<int>(), new Queue<int>() };
|
||||
}
|
||||
}
|
||||
30
AquaMai/AquaMai.Mods/Tweaks/TimeSaving/SkipGoodbyeScreen.cs
Normal file
30
AquaMai/AquaMai.Mods/Tweaks/TimeSaving/SkipGoodbyeScreen.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using AquaMai.Config.Attributes;
|
||||
using HarmonyLib;
|
||||
using Monitor;
|
||||
using Process;
|
||||
|
||||
namespace AquaMai.Mods.Tweaks.TimeSaving;
|
||||
|
||||
[ConfigSection(
|
||||
en: "Skip the \"Goodbye\" screen at the end of the game.",
|
||||
zh: "跳过游戏结束的「再见」界面")]
|
||||
public class SkipGoodbyeScreen
|
||||
{
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(GameOverMonitor), "IsPlayEnd")]
|
||||
public static bool GameOverMonitorPlayEnd(ref bool __result)
|
||||
{
|
||||
__result = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(GameOverProcess), "OnUpdate")]
|
||||
public static void GameOverProcessOnUpdate(ref GameOverProcess.GameOverSequence ____state)
|
||||
{
|
||||
if (____state == GameOverProcess.GameOverSequence.SkyChange)
|
||||
{
|
||||
____state = GameOverProcess.GameOverSequence.Disp;
|
||||
}
|
||||
}
|
||||
}
|
||||
32
AquaMai/AquaMai.Mods/Tweaks/TimeSaving/SkipStartupDelays.cs
Normal file
32
AquaMai/AquaMai.Mods/Tweaks/TimeSaving/SkipStartupDelays.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using System.Diagnostics;
|
||||
using AquaMai.Config.Attributes;
|
||||
using HarmonyLib;
|
||||
using Process;
|
||||
|
||||
namespace AquaMai.Mods.Tweaks.TimeSaving;
|
||||
|
||||
[ConfigSection(
|
||||
en: "Skip useless 2s delays to speed up the game boot process.",
|
||||
zh: """
|
||||
在自检界面,每个屏幕结束的时候都会等两秒才进入下一个屏幕,很浪费时间
|
||||
开了这个选项之后就不会等了
|
||||
""")]
|
||||
public class SkipStartupDelays
|
||||
{
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(PowerOnProcess), "OnStart")]
|
||||
public static void PrePowerOnStart(ref float ____waitTime)
|
||||
{
|
||||
____waitTime = 0f;
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(StartupProcess), "OnUpdate")]
|
||||
public static void PreStartupUpdate(byte ____state, ref Stopwatch ___timer)
|
||||
{
|
||||
if (____state == 8)
|
||||
{
|
||||
Traverse.Create(___timer).Field("elapsed").SetValue(2 * 10000000L);
|
||||
}
|
||||
}
|
||||
}
|
||||
31
AquaMai/AquaMai.Mods/Tweaks/TimeSaving/SkipStartupWarning.cs
Normal file
31
AquaMai/AquaMai.Mods/Tweaks/TimeSaving/SkipStartupWarning.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using AquaMai.Config.Attributes;
|
||||
using HarmonyLib;
|
||||
using Monitor;
|
||||
|
||||
namespace AquaMai.Mods.Tweaks.TimeSaving;
|
||||
|
||||
[ConfigSection(
|
||||
en: "Skip SDEZ's warning screen and logo shown after the POST sequence.",
|
||||
zh: "跳过 SDEZ 启动时的 WARNING 界面")]
|
||||
public class SkipStartupWarning
|
||||
{
|
||||
/*
|
||||
* Patch PlayLogo to disable the warning screen
|
||||
*/
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof (WarningMonitor), "PlayLogo")]
|
||||
public static bool PlayLogo()
|
||||
{
|
||||
// Return false to block the original method
|
||||
return false;
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof (WarningMonitor), "IsLogoAnimationEnd")]
|
||||
public static bool IsLogoAnimationEnd(ref bool __result)
|
||||
{
|
||||
// Always return true to indicate the animation has ended
|
||||
__result = true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
19
AquaMai/AquaMai.Mods/Tweaks/TimeSaving/SkipTrackStart.cs
Normal file
19
AquaMai/AquaMai.Mods/Tweaks/TimeSaving/SkipTrackStart.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using AquaMai.Config.Attributes;
|
||||
using HarmonyLib;
|
||||
using Monitor;
|
||||
|
||||
namespace AquaMai.Mods.Tweaks.TimeSaving;
|
||||
|
||||
[ConfigSection(
|
||||
en: "Skip TrackStart screen.",
|
||||
zh: "跳过乐曲开始界面")]
|
||||
public class SkipTrackStart
|
||||
{
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof (TrackStartMonitor), "IsEnd")]
|
||||
public static bool IsEnd(ref bool __result)
|
||||
{
|
||||
__result = true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
26
AquaMai/AquaMai.Mods/UX/CiBuildAlert.cs
Normal file
26
AquaMai/AquaMai.Mods/UX/CiBuildAlert.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using AquaMai.Config.Attributes;
|
||||
using AquaMai.Core.Attributes;
|
||||
using AquaMai.Core.Helpers;
|
||||
using AquaMai.Core.Resources;
|
||||
using HarmonyLib;
|
||||
using Process;
|
||||
|
||||
namespace AquaMai.Mods.UX;
|
||||
|
||||
[ConfigSection(exampleHidden: true, defaultOn: true)]
|
||||
[EnableIf(nameof(isCiBuild))]
|
||||
public class CiBuildAlert
|
||||
{
|
||||
# if CI
|
||||
private static readonly bool isCiBuild = true;
|
||||
# else
|
||||
private static readonly bool isCiBuild = false;
|
||||
# endif
|
||||
|
||||
[HarmonyPatch(typeof(AdvertiseProcess), "OnStart")]
|
||||
[HarmonyPostfix]
|
||||
public static void OnStart(AdvertiseProcess __instance)
|
||||
{
|
||||
MessageHelper.ShowMessage(Locale.CiBuildAlertContent, title: Locale.CiBuildAlertTitle);
|
||||
}
|
||||
}
|
||||
136
AquaMai/AquaMai.Mods/UX/HideSelfMadeCharts.cs
Normal file
136
AquaMai/AquaMai.Mods/UX/HideSelfMadeCharts.cs
Normal file
@@ -0,0 +1,136 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using AquaMai.Config.Attributes;
|
||||
using AquaMai.Config.Types;
|
||||
using AquaMai.Core.Helpers;
|
||||
using HarmonyLib;
|
||||
using MAI2.Util;
|
||||
using Manager;
|
||||
using MelonLoader;
|
||||
using Process;
|
||||
using Util;
|
||||
|
||||
namespace AquaMai.Mods.UX;
|
||||
|
||||
[ConfigSection(
|
||||
en: "One key to hide all self-made charts in the music select process. Or hide for some users.",
|
||||
zh: "在选曲界面一键隐藏所有自制谱,或对一部分用户进行隐藏")]
|
||||
public class HideSelfMadeCharts
|
||||
{
|
||||
[ConfigEntry(
|
||||
en: "Key to toggle self-made charts.",
|
||||
zh: "切换自制谱显示的按键")]
|
||||
public static readonly KeyCodeOrName key = KeyCodeOrName.Test;
|
||||
|
||||
[ConfigEntry]
|
||||
public static readonly bool longPress = false;
|
||||
|
||||
[ConfigEntry(
|
||||
en: "One user ID per line in the file. Hide self-made charts when these users login.",
|
||||
zh: "该文件中每行一个用户 ID,当这些用户登录时隐藏自制谱")]
|
||||
private static readonly string selfMadeChartsDenyUsersFile = "LocalAssets/SelfMadeChartsDenyUsers.txt";
|
||||
|
||||
[ConfigEntry(
|
||||
en: "One user ID per line in the file. Only show self-made charts when these users login.",
|
||||
zh: "该文件中每行一个用户 ID,只有这些用户登录时才显示自制谱")]
|
||||
private static readonly string selfMadeChartsWhiteListUsersFile = "LocalAssets/SelfMadeChartsWhiteListUsers.txt";
|
||||
|
||||
private static Safe.ReadonlySortedDictionary<int, Manager.MaiStudio.MusicData> _musics;
|
||||
private static Safe.ReadonlySortedDictionary<int, Manager.MaiStudio.MusicData> _musicsNoneSelfMade;
|
||||
|
||||
private static bool isShowSelfMadeCharts = true;
|
||||
private static bool isForceDisable;
|
||||
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(DataManager), "GetMusics")]
|
||||
public static void GetMusics(ref Safe.ReadonlySortedDictionary<int, Manager.MaiStudio.MusicData> __result, List<string> ____targetDirs)
|
||||
{
|
||||
if (_musics is null)
|
||||
{
|
||||
// init musics for the first time
|
||||
if (__result.Count == 0) return;
|
||||
_musics = __result;
|
||||
var nonSelfMadeList = new SortedDictionary<int, Manager.MaiStudio.MusicData>();
|
||||
var officialDirs = ____targetDirs.Where(it => File.Exists(Path.Combine(it, "DataConfig.xml")) || File.Exists(Path.Combine(it, "OfficialChartsMark.txt")));
|
||||
foreach (var music in __result)
|
||||
{
|
||||
if (officialDirs.Any(it => MusicDirHelper.LookupPath(music.Value).StartsWith(it)))
|
||||
{
|
||||
nonSelfMadeList.Add(music.Key, music.Value);
|
||||
}
|
||||
}
|
||||
|
||||
_musicsNoneSelfMade = new Safe.ReadonlySortedDictionary<int, Manager.MaiStudio.MusicData>(nonSelfMadeList);
|
||||
MelonLogger.Msg($"[HideSelfMadeCharts] All music count: {__result.Count}, Official music count: {_musicsNoneSelfMade.Count}");
|
||||
}
|
||||
|
||||
var stackTrace = new StackTrace(); // get call stack
|
||||
var stackFrames = stackTrace.GetFrames(); // get method calls (frames)
|
||||
if (stackFrames.All(it => it.GetMethod().DeclaringType.Name != "MusicSelectProcess")) return;
|
||||
if (isShowSelfMadeCharts && !isForceDisable) return;
|
||||
__result = _musicsNoneSelfMade;
|
||||
}
|
||||
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(MusicSelectProcess), "OnUpdate")]
|
||||
public static void MusicSelectProcessOnUpdate(ref MusicSelectProcess __instance)
|
||||
{
|
||||
if (isForceDisable) return;
|
||||
if (!KeyListener.GetKeyDownOrLongPress(key, longPress)) return;
|
||||
isShowSelfMadeCharts = !isShowSelfMadeCharts;
|
||||
MelonLogger.Msg($"[HideSelfMadeCharts] isShowSelfMadeCharts: {isShowSelfMadeCharts}");
|
||||
SharedInstances.ProcessDataContainer.processManager.AddProcess(new FadeProcess(SharedInstances.ProcessDataContainer, __instance, new MusicSelectProcess(SharedInstances.ProcessDataContainer)));
|
||||
Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(1000);
|
||||
MessageHelper.ShowMessage($"{(isShowSelfMadeCharts ? "Show" : "Hide")} Self-Made Charts");
|
||||
});
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(MusicSelectProcess), "OnStart")]
|
||||
public static void MusicSelectProcessOnStart(ref MusicSelectProcess __instance)
|
||||
{
|
||||
var denyPath = FileSystem.ResolvePath(selfMadeChartsDenyUsersFile);
|
||||
if (File.Exists(denyPath))
|
||||
{
|
||||
var userIds = File.ReadAllLines(denyPath);
|
||||
for (var i = 0; i < 2; i++)
|
||||
{
|
||||
var user = Singleton<UserDataManager>.Instance.GetUserData(i);
|
||||
if (!user.IsEntry) continue;
|
||||
if (!userIds.Contains(user.Detail.UserID.ToString())) continue;
|
||||
isForceDisable = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var whiteListPath = FileSystem.ResolvePath(selfMadeChartsWhiteListUsersFile);
|
||||
if (File.Exists(whiteListPath))
|
||||
{
|
||||
var userIds = File.ReadAllLines(whiteListPath);
|
||||
for (var i = 0; i < 2; i++)
|
||||
{
|
||||
var user = Singleton<UserDataManager>.Instance.GetUserData(i);
|
||||
if (!user.IsEntry) continue;
|
||||
if (userIds.Contains(user.Detail.UserID.ToString())) continue;
|
||||
isForceDisable = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
isForceDisable = false;
|
||||
}
|
||||
|
||||
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(EntryProcess), "OnStart")]
|
||||
public static void EntryProcessOnStart(ref EntryProcess __instance)
|
||||
{
|
||||
// reset status on login
|
||||
isShowSelfMadeCharts = true;
|
||||
}
|
||||
}
|
||||
240
AquaMai/AquaMai.Mods/UX/ImmediateSave.cs
Normal file
240
AquaMai/AquaMai.Mods/UX/ImmediateSave.cs
Normal file
@@ -0,0 +1,240 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AquaMai.Config.Attributes;
|
||||
using AquaMai.Core.Helpers;
|
||||
using AquaMai.Core.Resources;
|
||||
using DB;
|
||||
using HarmonyLib;
|
||||
using MAI2.Util;
|
||||
using MAI2System;
|
||||
using Manager;
|
||||
using Manager.MaiStudio;
|
||||
using Manager.UserDatas;
|
||||
using MelonLoader;
|
||||
using Monitor.Entry.Parts.Screens;
|
||||
using Net.Packet;
|
||||
using Net.Packet.Helper;
|
||||
using Process;
|
||||
using Process.UserDataNet.State.UserDataULState;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AquaMai.Mods.UX;
|
||||
|
||||
[ConfigSection(
|
||||
en: "Save immediate after playing a song.",
|
||||
zh: "打完一首歌的时候立即向服务器保存成绩")]
|
||||
public class ImmediateSave
|
||||
{
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(StateULUserAime), "RequestUploadUserPlayLogData")]
|
||||
public static bool PreRequestUploadUserPlayLogData(StateULUserAime __instance)
|
||||
{
|
||||
Traverse.Create(__instance).Method("RequestUploadUserPortraitData").GetValue();
|
||||
return false;
|
||||
}
|
||||
|
||||
private static SavingUi ui;
|
||||
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(ResultProcess), "OnStart")]
|
||||
public static void ResultProcessOnStart()
|
||||
{
|
||||
var doneCount = 0;
|
||||
|
||||
void CheckSaveDone()
|
||||
{
|
||||
doneCount++;
|
||||
if (doneCount != 4) return;
|
||||
if (ui == null) return;
|
||||
UnityEngine.Object.Destroy(ui);
|
||||
ui = null;
|
||||
}
|
||||
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
var userData = Singleton<UserDataManager>.Instance.GetUserData(i);
|
||||
if (!userData.IsEntry || userData.IsGuest())
|
||||
{
|
||||
doneCount += 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ui == null)
|
||||
{
|
||||
ui = SharedInstances.GameMainObject.gameObject.AddComponent<SavingUi>();
|
||||
}
|
||||
|
||||
SaveDataFix(userData);
|
||||
PacketHelper.StartPacket(Shim.CreatePacketUploadUserPlaylog(i, userData, (int)GameManager.MusicTrackNumber - 1,
|
||||
delegate
|
||||
{
|
||||
MelonLogger.Msg("[ImmediateSave] Playlog saved");
|
||||
CheckSaveDone();
|
||||
},
|
||||
delegate(PacketStatus err)
|
||||
{
|
||||
SoundManager.PlaySE(Mai2.Mai2Cue.Cue.SE_ENTRY_AIME_ERROR, i);
|
||||
MelonLogger.Error("[ImmediateSave] Playlog save error");
|
||||
MelonLogger.Error(err);
|
||||
MessageHelper.ShowMessage("Playlog save error");
|
||||
CheckSaveDone();
|
||||
}));
|
||||
PacketHelper.StartPacket(Shim.CreatePacketUpsertUserAll(i, userData, delegate(int code)
|
||||
{
|
||||
if (code == 1)
|
||||
{
|
||||
MelonLogger.Msg("[ImmediateSave] UserAll saved");
|
||||
CheckSaveDone();
|
||||
}
|
||||
else
|
||||
{
|
||||
SoundManager.PlaySE(Mai2.Mai2Cue.Cue.SE_ENTRY_AIME_ERROR, i);
|
||||
MelonLogger.Error("[ImmediateSave] UserAll upsert error");
|
||||
MelonLogger.Error(code);
|
||||
MessageHelper.ShowMessage("UserAll upsert error");
|
||||
CheckSaveDone();
|
||||
}
|
||||
}, delegate(PacketStatus err)
|
||||
{
|
||||
SoundManager.PlaySE(Mai2.Mai2Cue.Cue.SE_ENTRY_AIME_ERROR, i);
|
||||
MelonLogger.Error("[ImmediateSave] UserAll upsert error");
|
||||
MelonLogger.Error(err);
|
||||
MessageHelper.ShowMessage("UserAll upsert error");
|
||||
CheckSaveDone();
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static void SaveDataFix(UserData userData)
|
||||
{
|
||||
UserDetail detail = userData.Detail;
|
||||
detail.EventWatchedDate = TimeManager.GetDateString(TimeManager.PlayBaseTime);
|
||||
userData.CalcTotalValue();
|
||||
float num = 0f;
|
||||
try
|
||||
{
|
||||
if (userData.RatingList.RatingList.Any())
|
||||
{
|
||||
num = userData.RatingList.RatingList.Last().SingleRate;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
num = (float)Math.Ceiling((double)((num + 1f) / GameManager.TheoryRateBorderNum) * 10.0);
|
||||
float num2 = 0f;
|
||||
try
|
||||
{
|
||||
if (userData.RatingList.NextRatingList.Any())
|
||||
{
|
||||
num2 = userData.RatingList.NewRatingList.Last().SingleRate;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
num2 = (float)Math.Ceiling((double)((num2 + 1f) / GameManager.TheoryRateBorderNum) * 10.0);
|
||||
string logDateString = TimeManager.GetLogDateString(TimeManager.PlayBaseTime);
|
||||
string timeJp = (string.IsNullOrEmpty(userData.Detail.DailyBonusDate) ? TimeManager.GetLogDateString(0L) : userData.Detail.DailyBonusDate);
|
||||
if (userData.IsEntry && userData.Detail.IsNetMember >= 2 && !GameManager.IsEventMode && TimeManager.GetUnixTime(logDateString) > TimeManager.GetUnixTime(timeJp) && Singleton<UserDataManager>.Instance.IsSingleUser() && !GameManager.IsFreedomMode && !GameManager.IsCourseMode && !DoneEntry.IsWeekdayBonus(userData))
|
||||
{
|
||||
userData.Detail.DailyBonusDate = logDateString;
|
||||
}
|
||||
|
||||
List<UserRate> list = new List<UserRate>();
|
||||
List<UserRate> list2 = new List<UserRate>();
|
||||
IEnumerable<UserScore>[] scoreList = Shim.GetUserScoreList(userData);
|
||||
List<UserRate> ratingList = userData.RatingList.RatingList;
|
||||
List<UserRate> newRatingList = userData.RatingList.NewRatingList;
|
||||
int achive = RatingTableID.Rate_22.GetAchive();
|
||||
for (int j = 0; j < scoreList.Length; j++)
|
||||
{
|
||||
if (scoreList[j] == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (UserScore item2 in scoreList[j])
|
||||
{
|
||||
if (achive <= item2.achivement)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
MusicData music = Singleton<DataManager>.Instance.GetMusic(item2.id);
|
||||
if (music == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
UserRate item = new UserRate(item2.id, j, item2.achivement, (uint)music.version);
|
||||
if (item.OldFlag)
|
||||
{
|
||||
if (num <= (float)item.Level && !ratingList.Contains(item))
|
||||
{
|
||||
list.Add(item);
|
||||
}
|
||||
}
|
||||
else if (num2 <= (float)item.Level && !newRatingList.Contains(item))
|
||||
{
|
||||
list2.Add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
list.Sort();
|
||||
list.Reverse();
|
||||
if (list.Count > 10)
|
||||
{
|
||||
list.RemoveRange(10, list.Count - 10);
|
||||
}
|
||||
|
||||
userData.RatingList.NextRatingList = list;
|
||||
list2.Sort();
|
||||
list2.Reverse();
|
||||
if (list2.Count > 10)
|
||||
{
|
||||
list2.RemoveRange(10, list2.Count - 10);
|
||||
}
|
||||
|
||||
userData.RatingList.NextNewRatingList = list2;
|
||||
|
||||
userData.Detail.LastPlayCredit = 0;
|
||||
userData.Detail.LastPlayMode = 0;
|
||||
if (GameManager.IsFreedomMode)
|
||||
{
|
||||
userData.Detail.LastPlayMode = 1;
|
||||
}
|
||||
|
||||
if (GameManager.IsCourseMode)
|
||||
{
|
||||
userData.Detail.LastPlayMode = 2;
|
||||
}
|
||||
|
||||
userData.Detail.LastGameId = ConstParameter.GameIDStr;
|
||||
userData.Detail.LastRomVersion = Singleton<SystemConfig>.Instance.config.romVersionInfo.versionNo.versionString;
|
||||
userData.Detail.LastDataVersion = Singleton<SystemConfig>.Instance.config.dataVersionInfo.versionNo.versionString;
|
||||
}
|
||||
|
||||
private class SavingUi : MonoBehaviour
|
||||
{
|
||||
public void OnGUI()
|
||||
{
|
||||
var y = Screen.height * .075f;
|
||||
var width = GuiSizes.FontSize * 20f;
|
||||
var x = GuiSizes.PlayerCenter + GuiSizes.PlayerWidth / 2f - width;
|
||||
var rect = new Rect(x, y, width, GuiSizes.LabelHeight * 2.5f);
|
||||
|
||||
var labelStyle = GUI.skin.GetStyle("label");
|
||||
labelStyle.fontSize = (int)(GuiSizes.FontSize * 1.2);
|
||||
labelStyle.alignment = TextAnchor.MiddleCenter;
|
||||
|
||||
GUI.Box(rect, "");
|
||||
GUI.Label(rect, Locale.SavingDontExit);
|
||||
}
|
||||
}
|
||||
}
|
||||
247
AquaMai/AquaMai.Mods/UX/JudgeAccuracyInfo.cs
Normal file
247
AquaMai/AquaMai.Mods/UX/JudgeAccuracyInfo.cs
Normal file
@@ -0,0 +1,247 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using AquaMai.Config.Attributes;
|
||||
using HarmonyLib;
|
||||
using Manager;
|
||||
using Monitor;
|
||||
using Monitor.Result;
|
||||
using Process;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace AquaMai.Mods.UX;
|
||||
|
||||
[ConfigSection(
|
||||
zh: "在游戏总结的计分板中显示击打误差的详细信息(以帧为单位)",
|
||||
en: "Show detailed accuracy info in the score board.")]
|
||||
public class JudgeAccuracyInfo
|
||||
{
|
||||
public class AccuracyEntryList
|
||||
{
|
||||
public List<float>[] DiffList = new List<float>[TableRowNames.Length];
|
||||
public List<float>[] RawDiffList = new List<float>[TableRowNames.Length];
|
||||
public HashSet<int> NoteIndices = new();
|
||||
|
||||
public AccuracyEntryList()
|
||||
{
|
||||
for (int i = 0; i < TableRowNames.Length; i++)
|
||||
{
|
||||
DiffList[i] = new List<float>();
|
||||
RawDiffList[i] = new List<float>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static AccuracyEntryList[] EntryList = new AccuracyEntryList[2];
|
||||
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(GameProcess), "OnStart")]
|
||||
private static void OnGameProcessStartFinish()
|
||||
{
|
||||
for (int i = 0; i < EntryList.Length; i++)
|
||||
{
|
||||
EntryList[i] = new AccuracyEntryList();
|
||||
}
|
||||
}
|
||||
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(ResultProcess), "OnRelease")]
|
||||
private static void OnResultProcessReleaseFinish()
|
||||
{
|
||||
for (int i = 0; i < EntryList.Length; i++)
|
||||
{
|
||||
EntryList[i] = null;
|
||||
Controllers[i] = null;
|
||||
}
|
||||
}
|
||||
|
||||
[HarmonyPatch]
|
||||
public static class NoteBaseJudgePatch
|
||||
{
|
||||
public static IEnumerable<MethodBase> TargetMethods()
|
||||
{
|
||||
return
|
||||
[
|
||||
AccessTools.Method(typeof(NoteBase), "Judge"),
|
||||
AccessTools.Method(typeof(HoldNote), "JudgeHoldHead"),
|
||||
AccessTools.Method(typeof(BreakHoldNote), "JudgeHoldHead"),
|
||||
AccessTools.Method(typeof(TouchNoteB), "Judge"),
|
||||
AccessTools.Method(typeof(TouchHoldC), "JudgeHoldHead"),
|
||||
];
|
||||
}
|
||||
|
||||
public static void Postfix(
|
||||
NoteBase __instance, bool __result,
|
||||
float ___JudgeTimingDiffMsec, float ___AppearMsec, NoteJudge.EJudgeType ___JudgeType, int ___NoteIndex
|
||||
)
|
||||
{
|
||||
var monitor = __instance.MonitorId;
|
||||
if (!__result || EntryList[monitor].NoteIndices.Contains(___NoteIndex)) return;
|
||||
|
||||
EntryList[monitor].NoteIndices.Add(___NoteIndex);
|
||||
|
||||
var raw = (NotesManager.GetCurrentMsec() - ___AppearMsec) - NoteJudge.JudgeAdjustMs;
|
||||
switch (___JudgeType)
|
||||
{
|
||||
case NoteJudge.EJudgeType.Tap:
|
||||
case NoteJudge.EJudgeType.Break:
|
||||
{
|
||||
EntryList[monitor].DiffList[0].Add(___JudgeTimingDiffMsec);
|
||||
EntryList[monitor].RawDiffList[0].Add(raw);
|
||||
break;
|
||||
}
|
||||
case NoteJudge.EJudgeType.Touch:
|
||||
{
|
||||
EntryList[monitor].DiffList[2].Add(___JudgeTimingDiffMsec);
|
||||
EntryList[monitor].RawDiffList[2].Add(raw);
|
||||
break;
|
||||
}
|
||||
case NoteJudge.EJudgeType.ExTap:
|
||||
{
|
||||
EntryList[monitor].DiffList[3].Add(___JudgeTimingDiffMsec);
|
||||
EntryList[monitor].RawDiffList[3].Add(raw);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// MelonLogger.Msg($"{___JudgeType}: {___JudgeTimingDiffMsec}, {raw}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(SlideRoot), "Judge")]
|
||||
private static void SlideRootJudgePatch(
|
||||
SlideRoot __instance, bool __result,
|
||||
float ___JudgeTimingDiffMsec, float ___TailMsec, float ___lastWaitTimeForJudge,
|
||||
NoteJudge.EJudgeType ___JudgeType, int ___NoteIndex
|
||||
)
|
||||
{
|
||||
var monitor = __instance.MonitorId;
|
||||
if (!__result || EntryList[monitor].NoteIndices.Contains(___NoteIndex)) return;
|
||||
|
||||
EntryList[monitor].NoteIndices.Add(___NoteIndex);
|
||||
|
||||
var raw = (NotesManager.GetCurrentMsec() - ___TailMsec + ___lastWaitTimeForJudge) - NoteJudge.JudgeAdjustMs;
|
||||
EntryList[monitor].DiffList[1].Add(___JudgeTimingDiffMsec - NoteJudge.JudgeAdjustMs);
|
||||
EntryList[monitor].RawDiffList[1].Add(raw);
|
||||
|
||||
// MelonLogger.Msg($"{___JudgeType}: {___JudgeTimingDiffMsec}, {raw}");
|
||||
}
|
||||
|
||||
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(ResultProcess), "OnStart")]
|
||||
private static void OnResultProcessStartFinish(
|
||||
ResultMonitor[] ____monitors, ResultProcess.ResultScoreViewType[] ____resultScoreViewType, UserData[] ____userData
|
||||
)
|
||||
{
|
||||
foreach (var monitor in ____monitors)
|
||||
{
|
||||
var idx = monitor.MonitorIndex;
|
||||
if (!____userData[idx].IsEntry) continue;
|
||||
|
||||
var fileName = $"Acc_Track_{GameManager.MusicTrackNumber}_Player_{idx}.txt";
|
||||
var filePath = Path.Combine(Environment.CurrentDirectory, fileName);
|
||||
|
||||
using (var writer = new StreamWriter(filePath))
|
||||
{
|
||||
for (int i = 0; i < TableRowNames.Length; i++)
|
||||
{
|
||||
writer.WriteLine($"Row: {TableRowNames[i]}");
|
||||
writer.WriteLine(" DiffList:");
|
||||
writer.WriteLine($" {string.Join(", ", EntryList[idx].DiffList[i])}");
|
||||
writer.WriteLine(" RawDiffList:");
|
||||
writer.WriteLine($" {string.Join(", ", EntryList[idx].RawDiffList[i])}");
|
||||
writer.WriteLine();
|
||||
}
|
||||
}
|
||||
|
||||
var controller = Traverse.Create(monitor).Field<ScoreBoardController>("_scoreBoardController").Value;
|
||||
var newController = Object.Instantiate(controller, controller.transform);
|
||||
newController.gameObject.GetComponent<Animator>().enabled = false;
|
||||
newController.transform.localPosition = Vector3.zero;
|
||||
var table = ExtractTextObjs(newController);
|
||||
for (var i = 0; i < TableHead.Length; i++)
|
||||
{
|
||||
table[0, i].text = TableHead[i];
|
||||
}
|
||||
|
||||
for (var i = 0; i < TableRowNames.Length; i++)
|
||||
{
|
||||
table[i + 1, 0].text = TableRowNames[i];
|
||||
var num = EntryList[idx].DiffList[i].Count;
|
||||
table[i + 1, 1].text = num.ToString();
|
||||
if (num <= 0)
|
||||
{
|
||||
table[i + 1, 2].text = "——";
|
||||
table[i + 1, 3].text = "——";
|
||||
table[i + 1, 4].text = "——";
|
||||
continue;
|
||||
}
|
||||
|
||||
var average = EntryList[idx].DiffList[i].Average();
|
||||
var averageFrame = average * 0.06f;
|
||||
table[i + 1, 2].text = averageFrame.ToString("+0.00;-0.00;0.00", CultureInfo.InvariantCulture);
|
||||
var averageRawFrame = EntryList[idx].RawDiffList[i].Average() * 0.06f;
|
||||
table[i + 1, 3].text = averageRawFrame.ToString("+0.00;-0.00;0.00", CultureInfo.InvariantCulture);
|
||||
|
||||
if (num <= 1)
|
||||
{
|
||||
table[i + 1, 4].text = "——";
|
||||
}
|
||||
else
|
||||
{
|
||||
var deviSqr = EntryList[idx].DiffList[i].Sum(x => (x - average) * (x - average)) / (num - 1);
|
||||
var devi = Mathf.Sqrt(deviSqr) * 0.06f;
|
||||
table[i + 1, 4].text = devi.ToString("0.00", CultureInfo.InvariantCulture);
|
||||
}
|
||||
}
|
||||
|
||||
newController.gameObject.SetActive(____resultScoreViewType[idx] == ResultProcess.ResultScoreViewType.VSResult);
|
||||
Controllers[idx] = newController;
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly ScoreBoardController[] Controllers = new ScoreBoardController[2];
|
||||
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(ResultMonitor), "ChangeScoreBoard")]
|
||||
private static void OnChangeScoreBoard(
|
||||
ResultMonitor __instance, ResultProcess.ResultScoreViewType resultScoreType
|
||||
)
|
||||
{
|
||||
Controllers[__instance.MonitorIndex].gameObject.SetActive(resultScoreType == ResultProcess.ResultScoreViewType.VSResult);
|
||||
}
|
||||
|
||||
|
||||
private static readonly string[] RowNames = ["_tap", "_hold", "_slide", "_touch", "_break"];
|
||||
private static readonly string[] ColumnNames = ["_critical", "_perfect", "_great", "_good", "_miss"];
|
||||
private static readonly string[] TableHead = ["", "NUM", "AVG", "RAW", "S.D."];
|
||||
private static readonly string[] TableRowNames = ["TAP", "SLD", "TCH", "EX"];
|
||||
|
||||
private static TextMeshProUGUI[,] ExtractTextObjs(ScoreBoardController controller)
|
||||
{
|
||||
var result = new TextMeshProUGUI[RowNames.Length, ColumnNames.Length];
|
||||
for (var i = 0; i < RowNames.Length; i++)
|
||||
{
|
||||
for (int j = 0; j < ColumnNames.Length; j++)
|
||||
{
|
||||
var trav = Traverse.Create(controller)
|
||||
.Field(RowNames[i])
|
||||
.Field(ColumnNames[j]);
|
||||
var text = trav.Field<TextMeshProUGUI>("_numberText").Value;
|
||||
text.color = Color.black;
|
||||
result[i, j] = text;
|
||||
trav.GetValue<ScoreBoardColumnObject>().SetVisibleCloseBox(false);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
65
AquaMai/AquaMai.Mods/UX/OneKeyEntryEnd.cs
Normal file
65
AquaMai/AquaMai.Mods/UX/OneKeyEntryEnd.cs
Normal file
@@ -0,0 +1,65 @@
|
||||
using System.Collections.Generic;
|
||||
using AquaMai.Config.Attributes;
|
||||
using AquaMai.Config.Types;
|
||||
using AquaMai.Core.Helpers;
|
||||
using HarmonyLib;
|
||||
using Mai2.Mai2Cue;
|
||||
using Main;
|
||||
using Manager;
|
||||
using MelonLoader;
|
||||
using Process;
|
||||
|
||||
namespace AquaMai.Mods.UX;
|
||||
|
||||
[ConfigSection(
|
||||
en: "One key to proceed to music select (during entry) or end current PC (during music select).",
|
||||
zh: "一键跳过登录过程直接进入选歌界面,或在选歌界面直接结束本局游戏")]
|
||||
public class OneKeyEntryEnd
|
||||
{
|
||||
[ConfigEntry]
|
||||
public static readonly KeyCodeOrName key = KeyCodeOrName.Service;
|
||||
|
||||
[ConfigEntry]
|
||||
public static readonly bool longPress = true;
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(GameMainObject), "Update")]
|
||||
public static void OnGameMainObjectUpdate()
|
||||
{
|
||||
if (!KeyListener.GetKeyDownOrLongPress(key, longPress)) return;
|
||||
MelonLogger.Msg("[QuickSkip] Activated");
|
||||
|
||||
var traverse = Traverse.Create(SharedInstances.ProcessDataContainer.processManager);
|
||||
var processList = traverse.Field("_processList").GetValue<LinkedList<ProcessManager.ProcessControle>>();
|
||||
|
||||
ProcessBase processToRelease = null;
|
||||
|
||||
foreach (ProcessManager.ProcessControle process in processList)
|
||||
{
|
||||
switch (process.Process.ToString())
|
||||
{
|
||||
// After login
|
||||
case "Process.ModeSelect.ModeSelectProcess":
|
||||
case "Process.LoginBonus.LoginBonusProcess":
|
||||
case "Process.RegionalSelectProcess":
|
||||
case "Process.CharacterSelectProcess":
|
||||
case "Process.TicketSelect.TicketSelectProcess":
|
||||
processToRelease = process.Process;
|
||||
break;
|
||||
|
||||
case "Process.MusicSelectProcess":
|
||||
// Skip to save
|
||||
SoundManager.PreviewEnd();
|
||||
SoundManager.PlayBGM(Cue.BGM_COLLECTION, 2);
|
||||
SharedInstances.ProcessDataContainer.processManager.AddProcess(new FadeProcess(SharedInstances.ProcessDataContainer, process.Process, new UnlockMusicProcess(SharedInstances.ProcessDataContainer)));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (processToRelease != null)
|
||||
{
|
||||
GameManager.SetMaxTrack();
|
||||
SharedInstances.ProcessDataContainer.processManager.AddProcess(new FadeProcess(SharedInstances.ProcessDataContainer, processToRelease, new MusicSelectProcess(SharedInstances.ProcessDataContainer)));
|
||||
}
|
||||
}
|
||||
}
|
||||
46
AquaMai/AquaMai.Mods/UX/OneKeyRetrySkip.cs
Normal file
46
AquaMai/AquaMai.Mods/UX/OneKeyRetrySkip.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using AquaMai.Config.Attributes;
|
||||
using AquaMai.Config.Types;
|
||||
using AquaMai.Core.Helpers;
|
||||
using HarmonyLib;
|
||||
using MAI2.Util;
|
||||
using Manager;
|
||||
using Process;
|
||||
|
||||
namespace AquaMai.Mods.UX;
|
||||
|
||||
[ConfigSection(
|
||||
en: "One key to retry (1.30+) or skip current chart in gameplay.",
|
||||
zh: "在游戏中途一键重试(1.30+)或跳过当前谱面")]
|
||||
public class OneKeyRetrySkip
|
||||
{
|
||||
[ConfigEntry]
|
||||
public static readonly KeyCodeOrName retryKey = KeyCodeOrName.Service;
|
||||
|
||||
[ConfigEntry]
|
||||
public static readonly bool retryLongPress = false;
|
||||
|
||||
[ConfigEntry]
|
||||
public static readonly KeyCodeOrName skipKey = KeyCodeOrName.Service;
|
||||
|
||||
[ConfigEntry]
|
||||
public static readonly bool skipLongPress = true;
|
||||
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(GameProcess), "OnUpdate")]
|
||||
public static void PostGameProcessUpdate(GameProcess __instance, Message[] ____message, ProcessDataContainer ___container)
|
||||
{
|
||||
if (KeyListener.GetKeyDownOrLongPress(skipKey, skipLongPress))
|
||||
{
|
||||
var traverse = Traverse.Create(__instance);
|
||||
___container.processManager.SendMessage(____message[0]);
|
||||
Singleton<GamePlayManager>.Instance.SetSyncResult(0);
|
||||
traverse.Method("SetRelease").GetValue();
|
||||
}
|
||||
|
||||
if (KeyListener.GetKeyDownOrLongPress(retryKey, retryLongPress) && GameInfo.GameVersion >= 23000)
|
||||
{
|
||||
// This is original typo in Assembly-CSharp
|
||||
Singleton<GamePlayManager>.Instance.SetQuickRetryFrag(flag: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
151
AquaMai/AquaMai.Mods/UX/PracticeMode/Libs/PractiseModeUI.cs
Normal file
151
AquaMai/AquaMai.Mods/UX/PracticeMode/Libs/PractiseModeUI.cs
Normal file
@@ -0,0 +1,151 @@
|
||||
using System;
|
||||
using AquaMai.Mods.Fix;
|
||||
using AquaMai.Core.Helpers;
|
||||
using AquaMai.Core.Resources;
|
||||
using Manager;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AquaMai.Mods.UX.PracticeMode.Libs;
|
||||
|
||||
public class PracticeModeUI : MonoBehaviour
|
||||
{
|
||||
private static float windowTop => Screen.height - GuiSizes.PlayerWidth + GuiSizes.PlayerWidth * .22f;
|
||||
private static float controlHeight => GuiSizes.PlayerWidth * .13f;
|
||||
private static float sideButtonWidth => GuiSizes.PlayerWidth * .1f;
|
||||
private static float centerButtonWidth => GuiSizes.PlayerWidth * .28f;
|
||||
private static int fontSize => (int)(GuiSizes.PlayerWidth * .02f);
|
||||
|
||||
private static Rect GetButtonRect(int pos, int row)
|
||||
{
|
||||
float x;
|
||||
float width;
|
||||
switch (pos)
|
||||
{
|
||||
case 0:
|
||||
x = GuiSizes.PlayerCenter - centerButtonWidth / 2 - sideButtonWidth - GuiSizes.Margin;
|
||||
width = sideButtonWidth;
|
||||
break;
|
||||
case 1:
|
||||
x = GuiSizes.PlayerCenter - centerButtonWidth / 2;
|
||||
width = centerButtonWidth;
|
||||
break;
|
||||
case 2:
|
||||
x = GuiSizes.PlayerCenter + centerButtonWidth / 2 + GuiSizes.Margin;
|
||||
width = sideButtonWidth;
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(pos), pos, null);
|
||||
}
|
||||
|
||||
return new Rect(x, windowTop + (GuiSizes.Margin + controlHeight) * row + GuiSizes.Margin, width, controlHeight);
|
||||
}
|
||||
|
||||
public void OnGUI()
|
||||
{
|
||||
var labelStyle = GUI.skin.GetStyle("label");
|
||||
labelStyle.fontSize = fontSize;
|
||||
labelStyle.alignment = TextAnchor.MiddleCenter;
|
||||
|
||||
var buttonStyle = GUI.skin.GetStyle("button");
|
||||
buttonStyle.fontSize = fontSize;
|
||||
|
||||
GUI.Box(new Rect(
|
||||
GuiSizes.PlayerCenter - centerButtonWidth / 2 - sideButtonWidth - GuiSizes.Margin * 2,
|
||||
windowTop,
|
||||
centerButtonWidth + sideButtonWidth * 2 + GuiSizes.Margin * 4,
|
||||
controlHeight * 4 + GuiSizes.Margin * 5
|
||||
), "");
|
||||
|
||||
GUI.Button(GetButtonRect(0, 0), Locale.SeekBackward);
|
||||
GUI.Button(GetButtonRect(1, 0), Locale.Pause);
|
||||
GUI.Button(GetButtonRect(2, 0), Locale.SeekForward);
|
||||
|
||||
if (PracticeMode.repeatStart == -1)
|
||||
{
|
||||
GUI.Button(GetButtonRect(0, 1), Locale.MarkRepeatStart);
|
||||
GUI.Label(GetButtonRect(1, 1), Locale.RepeatNotSet);
|
||||
}
|
||||
else if (PracticeMode.repeatEnd == -1)
|
||||
{
|
||||
GUI.Button(GetButtonRect(0, 1), Locale.MarkRepeatEnd);
|
||||
GUI.Label(GetButtonRect(1, 1), Locale.RepeatStartSet);
|
||||
GUI.Button(GetButtonRect(2, 1), Locale.RepeatReset);
|
||||
}
|
||||
else
|
||||
{
|
||||
GUI.Label(GetButtonRect(1, 1), Locale.RepeatStartEndSet);
|
||||
GUI.Button(GetButtonRect(2, 1), Locale.RepeatReset);
|
||||
}
|
||||
|
||||
GUI.Button(GetButtonRect(0, 2), Locale.SpeedDown);
|
||||
GUI.Label(GetButtonRect(1, 2), $"{Locale.Speed} {PracticeMode.speed * 100:000}%");
|
||||
GUI.Button(GetButtonRect(2, 2), Locale.SpeedUp);
|
||||
GUI.Button(GetButtonRect(1, 3), Locale.SpeedReset);
|
||||
|
||||
GUI.Label(GetButtonRect(0, 3), $"{TimeSpan.FromMilliseconds(PracticeMode.CurrentPlayMsec):mm\\:ss\\.fff}\n{TimeSpan.FromMilliseconds(NotesManager.Instance().getPlayFinalMsec()):mm\\:ss\\.fff}");
|
||||
GUI.Button(GetButtonRect(2, 3), $"保持流速\n{(PracticeMode.keepNoteSpeed ? "ON" : "OFF")}");
|
||||
}
|
||||
|
||||
public void Update()
|
||||
{
|
||||
if (InputManager.GetTouchPanelAreaDown(InputManager.TouchPanelArea.E8))
|
||||
{
|
||||
PracticeMode.Seek(-1000);
|
||||
}
|
||||
else if (InputManager.GetTouchPanelAreaDown(InputManager.TouchPanelArea.E2))
|
||||
{
|
||||
PracticeMode.Seek(1000);
|
||||
}
|
||||
else if (InputManager.GetTouchPanelAreaDown(InputManager.TouchPanelArea.B8) || InputManager.GetTouchPanelAreaDown(InputManager.TouchPanelArea.B1))
|
||||
{
|
||||
DebugFeature.Pause = !DebugFeature.Pause;
|
||||
if (!DebugFeature.Pause)
|
||||
{
|
||||
PracticeMode.Seek(0);
|
||||
}
|
||||
}
|
||||
else if (InputManager.GetTouchPanelAreaDown(InputManager.TouchPanelArea.B7) && PracticeMode.repeatStart == -1)
|
||||
{
|
||||
PracticeMode.repeatStart = PracticeMode.CurrentPlayMsec;
|
||||
}
|
||||
else if (InputManager.GetTouchPanelAreaDown(InputManager.TouchPanelArea.B7) && PracticeMode.repeatEnd == -1)
|
||||
{
|
||||
PracticeMode.SetRepeatEnd(PracticeMode.CurrentPlayMsec);
|
||||
}
|
||||
else if (InputManager.GetTouchPanelAreaDown(InputManager.TouchPanelArea.B2))
|
||||
{
|
||||
PracticeMode.ClearRepeat();
|
||||
}
|
||||
else if (InputManager.GetTouchPanelAreaDown(InputManager.TouchPanelArea.B6))
|
||||
{
|
||||
PracticeMode.SpeedDown();
|
||||
}
|
||||
else if (InputManager.GetTouchPanelAreaDown(InputManager.TouchPanelArea.B3))
|
||||
{
|
||||
PracticeMode.SpeedUp();
|
||||
}
|
||||
else if (InputManager.GetTouchPanelAreaDown(InputManager.TouchPanelArea.B5) || InputManager.GetTouchPanelAreaDown(InputManager.TouchPanelArea.B4))
|
||||
{
|
||||
PracticeMode.SpeedReset();
|
||||
}
|
||||
else if (InputManager.GetTouchPanelAreaDown(InputManager.TouchPanelArea.E4))
|
||||
{
|
||||
PracticeMode.keepNoteSpeed = !PracticeMode.keepNoteSpeed;
|
||||
PracticeMode.gameCtrl?.ResetOptionSpeed();
|
||||
}
|
||||
else if (
|
||||
InputManager.GetTouchPanelAreaDown(InputManager.TouchPanelArea.A1) ||
|
||||
InputManager.GetTouchPanelAreaDown(InputManager.TouchPanelArea.A2) ||
|
||||
InputManager.GetTouchPanelAreaDown(InputManager.TouchPanelArea.A3) ||
|
||||
InputManager.GetTouchPanelAreaDown(InputManager.TouchPanelArea.A4) ||
|
||||
InputManager.GetTouchPanelAreaDown(InputManager.TouchPanelArea.A5) ||
|
||||
InputManager.GetTouchPanelAreaDown(InputManager.TouchPanelArea.A6) ||
|
||||
InputManager.GetTouchPanelAreaDown(InputManager.TouchPanelArea.A7) ||
|
||||
InputManager.GetTouchPanelAreaDown(InputManager.TouchPanelArea.A8)
|
||||
)
|
||||
{
|
||||
PracticeMode.ui = null;
|
||||
Destroy(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
282
AquaMai/AquaMai.Mods/UX/PracticeMode/PracticeMode.cs
Normal file
282
AquaMai/AquaMai.Mods/UX/PracticeMode/PracticeMode.cs
Normal file
@@ -0,0 +1,282 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using AquaMai.Mods.Fix;
|
||||
using AquaMai.Core.Helpers;
|
||||
using AquaMai.Core.Resources;
|
||||
using AquaMai.Mods.UX.PracticeMode.Libs;
|
||||
using HarmonyLib;
|
||||
using Manager;
|
||||
using Monitor;
|
||||
using Monitor.Game;
|
||||
using Process;
|
||||
using UnityEngine;
|
||||
using AquaMai.Config.Attributes;
|
||||
using AquaMai.Config.Types;
|
||||
|
||||
namespace AquaMai.Mods.UX.PracticeMode;
|
||||
|
||||
[ConfigCollapseNamespace]
|
||||
[ConfigSection(
|
||||
en: "Practice Mode.",
|
||||
zh: "练习模式")]
|
||||
public class PracticeMode
|
||||
{
|
||||
[ConfigEntry(
|
||||
en: "Key to show Practice Mode UI.",
|
||||
zh: "显示练习模式 UI 的按键")]
|
||||
public static readonly KeyCodeOrName key = KeyCodeOrName.Test;
|
||||
|
||||
[ConfigEntry]
|
||||
public static readonly bool longPress = false;
|
||||
|
||||
public static double repeatStart = -1;
|
||||
public static double repeatEnd = -1;
|
||||
public static float speed = 1;
|
||||
private static CriAtomExPlayer player;
|
||||
private static MovieMaterialMai2 movie;
|
||||
public static GameCtrl gameCtrl;
|
||||
public static bool keepNoteSpeed = false;
|
||||
|
||||
public static void SetRepeatEnd(double time)
|
||||
{
|
||||
if (repeatStart == -1)
|
||||
{
|
||||
MessageHelper.ShowMessage(Locale.RepeatStartTimeNotSet);
|
||||
return;
|
||||
}
|
||||
|
||||
if (time < repeatStart)
|
||||
{
|
||||
MessageHelper.ShowMessage(Locale.RepeatEndTimeLessThenStartTime);
|
||||
return;
|
||||
}
|
||||
|
||||
repeatEnd = time;
|
||||
}
|
||||
|
||||
public static void ClearRepeat()
|
||||
{
|
||||
repeatStart = -1;
|
||||
repeatEnd = -1;
|
||||
}
|
||||
|
||||
public static void SetSpeed()
|
||||
{
|
||||
player.SetPitch((float)(1200 * Math.Log(speed, 2)));
|
||||
// player.SetDspTimeStretchRatio(1 / speed);
|
||||
player.UpdateAll();
|
||||
|
||||
movie.player.SetSpeed(speed);
|
||||
gameCtrl?.ResetOptionSpeed();
|
||||
}
|
||||
|
||||
private static IEnumerator SetSpeedCoroutineInner()
|
||||
{
|
||||
yield return null;
|
||||
SetSpeed();
|
||||
}
|
||||
|
||||
public static void SetSpeedCoroutine()
|
||||
{
|
||||
SharedInstances.GameMainObject.StartCoroutine(SetSpeedCoroutineInner());
|
||||
}
|
||||
|
||||
public static void SpeedUp()
|
||||
{
|
||||
speed += .05f;
|
||||
if (speed > 2)
|
||||
{
|
||||
speed = 2;
|
||||
}
|
||||
|
||||
SetSpeed();
|
||||
}
|
||||
|
||||
public static void SpeedDown()
|
||||
{
|
||||
speed -= .05f;
|
||||
if (speed < 0.05)
|
||||
{
|
||||
speed = 0.05f;
|
||||
}
|
||||
|
||||
SetSpeed();
|
||||
}
|
||||
|
||||
public static void SpeedReset()
|
||||
{
|
||||
speed = 1;
|
||||
SetSpeed();
|
||||
}
|
||||
|
||||
public static void Seek(int addMsec)
|
||||
{
|
||||
// Debug feature 里面那个 timer 不能感知变速
|
||||
// 为了和魔改版本统一,polyfill 里面不修这个
|
||||
// 这里重新实现一个能感知变速的 Seek
|
||||
var msec = CurrentPlayMsec + addMsec;
|
||||
if (msec < 0)
|
||||
{
|
||||
msec = 0;
|
||||
}
|
||||
|
||||
CurrentPlayMsec = msec;
|
||||
}
|
||||
|
||||
public static double CurrentPlayMsec
|
||||
{
|
||||
get => NotesManager.GetCurrentMsec() - 91;
|
||||
set
|
||||
{
|
||||
DebugFeature.CurrentPlayMsec = value;
|
||||
SetSpeedCoroutine();
|
||||
}
|
||||
}
|
||||
|
||||
public static PracticeModeUI ui;
|
||||
|
||||
[HarmonyPatch]
|
||||
public class PatchNoteSpeed
|
||||
{
|
||||
public static IEnumerable<MethodBase> TargetMethods()
|
||||
{
|
||||
yield return AccessTools.Method(typeof(GameManager), "GetNoteSpeed");
|
||||
yield return AccessTools.Method(typeof(GameManager), "GetTouchSpeed");
|
||||
}
|
||||
|
||||
public static void Postfix(ref float __result)
|
||||
{
|
||||
if (!keepNoteSpeed) return;
|
||||
__result /= speed;
|
||||
}
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(GameProcess), "OnStart")]
|
||||
[HarmonyPostfix]
|
||||
public static void GameProcessPostStart()
|
||||
{
|
||||
repeatStart = -1;
|
||||
repeatEnd = -1;
|
||||
speed = 1;
|
||||
ui = null;
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(GameProcess), "OnRelease")]
|
||||
[HarmonyPostfix]
|
||||
public static void GameProcessPostRelease()
|
||||
{
|
||||
repeatStart = -1;
|
||||
repeatEnd = -1;
|
||||
speed = 1;
|
||||
ui = null;
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(GameCtrl), "Initialize")]
|
||||
[HarmonyPostfix]
|
||||
public static void GameCtrlPostInitialize(GameCtrl __instance)
|
||||
{
|
||||
gameCtrl = __instance;
|
||||
}
|
||||
|
||||
# if DEBUG
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(GenericProcess), "OnUpdate")]
|
||||
public static void OnGenericProcessUpdate(GenericMonitor[] ____monitors)
|
||||
{
|
||||
if (Input.GetKeyDown(KeyCode.F11))
|
||||
{
|
||||
____monitors[0].gameObject.AddComponent<PracticeModeUI>();
|
||||
}
|
||||
}
|
||||
# endif
|
||||
|
||||
[HarmonyPatch(typeof(GameProcess), "OnUpdate")]
|
||||
[HarmonyPostfix]
|
||||
public static void GameProcessPostUpdate(GameProcess __instance, GameMonitor[] ____monitors)
|
||||
{
|
||||
if (KeyListener.GetKeyDownOrLongPress(key, longPress) && ui is null)
|
||||
{
|
||||
ui = ____monitors[0].gameObject.AddComponent<PracticeModeUI>();
|
||||
}
|
||||
|
||||
if (repeatStart >= 0 && repeatEnd >= 0)
|
||||
{
|
||||
if (CurrentPlayMsec >= repeatEnd)
|
||||
{
|
||||
CurrentPlayMsec = repeatStart;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static float startGap = -1f;
|
||||
|
||||
[HarmonyPatch(typeof(NotesManager), "StartPlay")]
|
||||
[HarmonyPostfix]
|
||||
public static void NotesManagerPostUpdateTimer(float msecStartGap)
|
||||
{
|
||||
startGap = msecStartGap;
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(NotesManager), "UpdateTimer")]
|
||||
[HarmonyPrefix]
|
||||
public static bool NotesManagerPostUpdateTimer(bool ____isPlaying, Stopwatch ____stopwatch, ref float ____curMSec, ref float ____curMSecPre, float ____msecStartGap)
|
||||
{
|
||||
var stackTrace = new StackTrace(); // get call stack
|
||||
var stackFrames = stackTrace.GetFrames(); // get method calls (frames)
|
||||
if(stackFrames.Select(it => it.GetMethod().DeclaringType.Name).Contains("AdvDemoProcess"))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (startGap != -1f)
|
||||
{
|
||||
____curMSec = startGap;
|
||||
____curMSecPre = startGap;
|
||||
____stopwatch?.Reset();
|
||||
startGap = -1f;
|
||||
}
|
||||
else
|
||||
{
|
||||
____curMSecPre = ____curMSec;
|
||||
if (____isPlaying && ____stopwatch != null && !DebugFeature.Pause)
|
||||
{
|
||||
var num = (double)____stopwatch.ElapsedTicks / Stopwatch.Frequency * 1000.0 * speed;
|
||||
____curMSec += (float)num;
|
||||
____stopwatch.Reset();
|
||||
____stopwatch.Start();
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(SoundCtrl), "Initialize")]
|
||||
[HarmonyPostfix]
|
||||
public static void SoundCtrlPostInitialize(SoundCtrl.InitParam param, Dictionary<int, object> ____players)
|
||||
{
|
||||
var wrapper = ____players[2];
|
||||
player = (CriAtomExPlayer)wrapper.GetType().GetField("Player").GetValue(wrapper);
|
||||
// var pool = new CriAtomExStandardVoicePool(1, 8, 96000, true, 2);
|
||||
// pool.AttachDspTimeStretch();
|
||||
// player.SetVoicePoolIdentifier(pool.identifier);
|
||||
|
||||
// debug
|
||||
// var wrapper1 = ____players[7];
|
||||
// var player1 = (CriAtomExPlayer)wrapper1.GetType().GetField("Player").GetValue(wrapper1);
|
||||
// var pool = new CriAtomExStandardVoicePool(1, 8, 96000, true, 2);
|
||||
// pool.AttachDspTimeStretch();
|
||||
// player1.SetVoicePoolIdentifier(pool.identifier);
|
||||
// player1.SetDspTimeStretchRatio(2);
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(MovieController), "Awake")]
|
||||
[HarmonyPostfix]
|
||||
public static void MovieControllerPostAwake(MovieMaterialMai2 ____moviePlayers)
|
||||
{
|
||||
movie = ____moviePlayers;
|
||||
}
|
||||
}
|
||||
70
AquaMai/AquaMai.Mods/UX/QuickEndPlay.cs
Normal file
70
AquaMai/AquaMai.Mods/UX/QuickEndPlay.cs
Normal file
@@ -0,0 +1,70 @@
|
||||
using AquaMai.Config.Attributes;
|
||||
using AquaMai.Core.Helpers;
|
||||
using AquaMai.Core.Resources;
|
||||
using HarmonyLib;
|
||||
using MAI2.Util;
|
||||
using Manager;
|
||||
using Monitor;
|
||||
using Process;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AquaMai.Mods.UX;
|
||||
|
||||
[ConfigSection(
|
||||
en: "Show a \"skip\" button like AstroDX after the notes end.",
|
||||
zh: "音符结束之后显示像 AstroDX 一样的「跳过」按钮")]
|
||||
public class QuickEndPlay
|
||||
{
|
||||
private static int _timer;
|
||||
|
||||
[HarmonyPatch(typeof(GameProcess), "OnStart")]
|
||||
[HarmonyPostfix]
|
||||
public static void GameProcessPostStart(GameMonitor[] ____monitors)
|
||||
{
|
||||
_timer = 0;
|
||||
____monitors[0].gameObject.AddComponent<Ui>();
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(GameProcess), "OnUpdate")]
|
||||
[HarmonyPostfix]
|
||||
public static void GameProcessPostUpdate(GameProcess __instance, Message[] ____message, ProcessDataContainer ___container, byte ____sequence)
|
||||
{
|
||||
switch (____sequence)
|
||||
{
|
||||
case 9:
|
||||
_timer = 0;
|
||||
break;
|
||||
case > 4:
|
||||
_timer++;
|
||||
break;
|
||||
default:
|
||||
_timer = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
if (_timer > 60 && (InputManager.GetTouchPanelAreaDown(InputManager.TouchPanelArea.B4) || InputManager.GetTouchPanelAreaDown(InputManager.TouchPanelArea.E4)))
|
||||
{
|
||||
var traverse = Traverse.Create(__instance);
|
||||
___container.processManager.SendMessage(____message[0]);
|
||||
Singleton<GamePlayManager>.Instance.SetSyncResult(0);
|
||||
traverse.Method("SetRelease").GetValue();
|
||||
}
|
||||
}
|
||||
|
||||
private class Ui : MonoBehaviour
|
||||
{
|
||||
public void OnGUI()
|
||||
{
|
||||
if (_timer < 60) return;
|
||||
|
||||
// 这里重新 setup 一下 style 也可以
|
||||
var x = GuiSizes.PlayerCenter;
|
||||
var y = Screen.height - GuiSizes.PlayerWidth * .37f;
|
||||
var width = GuiSizes.PlayerWidth * .25f;
|
||||
var height = GuiSizes.PlayerWidth * .13f;
|
||||
|
||||
GUI.Box(new Rect(x, y, width, height), "");
|
||||
GUI.Button(new Rect(x, y, width, height), Locale.Skip);
|
||||
}
|
||||
}
|
||||
}
|
||||
3
AquaMai/AquaMai.Mods/UX/README.md
Normal file
3
AquaMai/AquaMai.Mods/UX/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# UX
|
||||
|
||||
Features aiming at improving the user experience and can't fit in other categories go here. Generally, UX features are triggered by some user action or let the user perceive.
|
||||
143
AquaMai/AquaMai.Mods/UX/SelectionDetail.cs
Normal file
143
AquaMai/AquaMai.Mods/UX/SelectionDetail.cs
Normal file
@@ -0,0 +1,143 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AquaMai.Core.Attributes;
|
||||
using AquaMai.Config.Attributes;
|
||||
using AquaMai.Core.Helpers;
|
||||
using AquaMai.Core.Resources;
|
||||
using HarmonyLib;
|
||||
using MAI2.Util;
|
||||
using Manager;
|
||||
using Manager.MaiStudio;
|
||||
using Manager.UserDatas;
|
||||
using Monitor;
|
||||
using Process;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AquaMai.Mods.UX;
|
||||
|
||||
[EnableGameVersion(23500)]
|
||||
[ConfigSection(
|
||||
en: "Show detail of selected song in music selection screen.",
|
||||
zh: "选歌界面显示选择的歌曲的详情")]
|
||||
public class SelectionDetail
|
||||
{
|
||||
private static readonly Window[] window = new Window[2];
|
||||
private static MusicSelectProcess.MusicSelectData SelectData { get; set; }
|
||||
private static readonly int[] difficulty = new int[2];
|
||||
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(MusicSelectMonitor), "UpdateRivalScore")]
|
||||
public static void ScrollUpdate(MusicSelectProcess ____musicSelect, MusicSelectMonitor __instance)
|
||||
{
|
||||
int player;
|
||||
if (__instance == ____musicSelect.MonitorArray[0])
|
||||
{
|
||||
player = 0;
|
||||
}
|
||||
else if (__instance == ____musicSelect.MonitorArray[1])
|
||||
{
|
||||
player = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (window[player] != null)
|
||||
{
|
||||
window[player].Close();
|
||||
}
|
||||
|
||||
var userData = Singleton<UserDataManager>.Instance.GetUserData(player);
|
||||
if (!userData.IsEntry) return;
|
||||
|
||||
if (____musicSelect.IsRandomIndex()) return;
|
||||
|
||||
SelectData = ____musicSelect.GetMusic(0);
|
||||
if (SelectData == null) return;
|
||||
difficulty[player] = ____musicSelect.GetDifficulty(player);
|
||||
|
||||
window[player] = player == 0 ? __instance.gameObject.AddComponent<P1Window>() : __instance.gameObject.AddComponent<P2Window>();
|
||||
}
|
||||
|
||||
private class P1Window : Window
|
||||
{
|
||||
protected override int player => 0;
|
||||
}
|
||||
|
||||
private class P2Window : Window
|
||||
{
|
||||
protected override int player => 1;
|
||||
}
|
||||
|
||||
private abstract class Window : MonoBehaviour
|
||||
{
|
||||
protected abstract int player { get; }
|
||||
private UserData userData => Singleton<UserDataManager>.Instance.GetUserData(player);
|
||||
|
||||
public void OnGUI()
|
||||
{
|
||||
var dataToShow = new List<string>();
|
||||
dataToShow.Add($"ID: {SelectData.MusicData.name.id}");
|
||||
dataToShow.Add(MusicDirHelper.LookupPath(SelectData.MusicData.name.id).Split('/').Reverse().ToArray()[3]);
|
||||
if (SelectData.MusicData.genreName is not null) // SelectData.MusicData.genreName.str may not correct
|
||||
dataToShow.Add(Singleton<DataManager>.Instance.GetMusicGenre(SelectData.MusicData.genreName.id)?.genreName);
|
||||
if (SelectData.MusicData.AddVersion is not null)
|
||||
dataToShow.Add(Singleton<DataManager>.Instance.GetMusicVersion(SelectData.MusicData.AddVersion.id)?.genreName);
|
||||
var notesData = SelectData.MusicData.notesData[difficulty[player]];
|
||||
dataToShow.Add($"{notesData?.level}.{notesData?.levelDecimal}");
|
||||
|
||||
var rate = CalcB50(SelectData.MusicData, difficulty[player]);
|
||||
if (rate > 0)
|
||||
{
|
||||
dataToShow.Add(string.Format(Locale.RatingUpWhenSSSp, rate));
|
||||
}
|
||||
|
||||
var playCount = Shim.GetUserScoreList(userData)[difficulty[player]].FirstOrDefault(it => it.id == SelectData.MusicData.name.id)?.playcount ?? 0;
|
||||
if (playCount > 0)
|
||||
{
|
||||
dataToShow.Add(string.Format(Locale.PlayCount, playCount));
|
||||
}
|
||||
|
||||
|
||||
var width = GuiSizes.FontSize * 15f;
|
||||
var x = GuiSizes.PlayerCenter - width / 2f + GuiSizes.PlayerWidth * player;
|
||||
var y = Screen.height * 0.87f;
|
||||
|
||||
var labelStyle = GUI.skin.GetStyle("label");
|
||||
labelStyle.fontSize = GuiSizes.FontSize;
|
||||
labelStyle.alignment = TextAnchor.MiddleCenter;
|
||||
|
||||
GUI.Box(new Rect(x, y, width, dataToShow.Count * GuiSizes.LabelHeight + 2 * GuiSizes.Margin), "");
|
||||
for (var i = 0; i < dataToShow.Count; i++)
|
||||
{
|
||||
GUI.Label(new Rect(x, y + GuiSizes.Margin + i * GuiSizes.LabelHeight, width, GuiSizes.LabelHeight), dataToShow[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private uint CalcB50(MusicData musicData, int difficulty)
|
||||
{
|
||||
var theory = new UserRate(musicData.name.id, difficulty, 1010000, (uint)musicData.version);
|
||||
var list = theory.OldFlag ? userData.RatingList.RatingList : userData.RatingList.NewRatingList;
|
||||
var userLowRate = list.Last();
|
||||
var userSongRate = list.FirstOrDefault(it => it.MusicId == musicData.name.id && it.Level == difficulty);
|
||||
|
||||
if (!userSongRate.Equals(default(UserRate)))
|
||||
{
|
||||
return theory.SingleRate - userSongRate.SingleRate;
|
||||
}
|
||||
|
||||
if (theory.SingleRate > userLowRate.SingleRate)
|
||||
{
|
||||
return theory.SingleRate - userLowRate.SingleRate;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public void Close()
|
||||
{
|
||||
Destroy(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
53
AquaMai/AquaMai.Mods/Utils/DisplayFrameRate.cs
Normal file
53
AquaMai/AquaMai.Mods/Utils/DisplayFrameRate.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
using AquaMai.Config.Attributes;
|
||||
using AquaMai.Core.Helpers;
|
||||
using HarmonyLib;
|
||||
using Main;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AquaMai.Mods.Utils;
|
||||
|
||||
[ConfigSection(
|
||||
en: "Display framerate.",
|
||||
zh: "显示帧率")]
|
||||
public class DisplayFrameRate
|
||||
{
|
||||
[HarmonyPatch(typeof(GameMainObject), "Awake")]
|
||||
[HarmonyPostfix]
|
||||
public static void ShowUi(GameMainObject __instance)
|
||||
{
|
||||
__instance.gameObject.AddComponent<Ui>();
|
||||
}
|
||||
|
||||
private class Ui : MonoBehaviour
|
||||
{
|
||||
private static float sampleTime = 1f;
|
||||
private static int frame;
|
||||
private static float time;
|
||||
private static float fps;
|
||||
|
||||
public void OnGUI()
|
||||
{
|
||||
var labelStyle = GUI.skin.GetStyle("label");
|
||||
labelStyle.fontSize = GuiSizes.FontSize;
|
||||
labelStyle.alignment = TextAnchor.MiddleCenter;
|
||||
|
||||
const float x = 10f;
|
||||
const float y = 10f;
|
||||
var width = GuiSizes.FontSize * 7f;
|
||||
var height = GuiSizes.LabelHeight * 1.5f;
|
||||
|
||||
frame += 1;
|
||||
time += Time.deltaTime;
|
||||
|
||||
if (time >= sampleTime)
|
||||
{
|
||||
fps = frame / time;
|
||||
frame = 0;
|
||||
time = 0;
|
||||
}
|
||||
|
||||
GUI.Box(new Rect(x, y, width, height), "");
|
||||
GUI.Label(new Rect(x, y, width, height), $"{fps:0.0} FPS");
|
||||
}
|
||||
}
|
||||
}
|
||||
49
AquaMai/AquaMai.Mods/Utils/LogNetworkErrors.cs
Normal file
49
AquaMai/AquaMai.Mods/Utils/LogNetworkErrors.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
using System.Diagnostics;
|
||||
using AquaMai.Config.Attributes;
|
||||
using HarmonyLib;
|
||||
using Manager;
|
||||
using Manager.Operation;
|
||||
using MelonLoader;
|
||||
using Net.Packet;
|
||||
|
||||
namespace AquaMai.Mods.Utils;
|
||||
|
||||
[ConfigSection(exampleHidden: true, defaultOn: true)]
|
||||
public class LogNetworkErrors
|
||||
{
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(Packet), "ProcImpl")]
|
||||
public static void Postfix(PacketState __result, Packet __instance)
|
||||
{
|
||||
if (__result == PacketState.Error)
|
||||
{
|
||||
MelonLogger.Msg($"[LogNetworkErrors] {__instance.Query.Api}: {__instance.Status}");
|
||||
}
|
||||
}
|
||||
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(DataDownloader), "NotifyOffline")]
|
||||
public static void DataDownloader()
|
||||
{
|
||||
MelonLogger.Msg("[LogNetworkErrors] DataDownloader NotifyOffline");
|
||||
var stackTrace = new StackTrace();
|
||||
MelonLogger.Msg(stackTrace.ToString());
|
||||
}
|
||||
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(OnlineCheckInterval), "NotifyOffline")]
|
||||
public static void OnlineCheckInterval()
|
||||
{
|
||||
MelonLogger.Msg("[LogNetworkErrors] OnlineCheckInterval NotifyOffline");
|
||||
var stackTrace = new StackTrace();
|
||||
MelonLogger.Msg(stackTrace.ToString());
|
||||
}
|
||||
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(OperationManager), "IsAliveAimeServer", MethodType.Getter)]
|
||||
public static void IsAliveAimeServer(bool __result)
|
||||
{
|
||||
if (__result == false)
|
||||
MelonLogger.Msg($"[LogNetworkErrors] IsAliveAimeServer Is {__result}");
|
||||
}
|
||||
}
|
||||
22
AquaMai/AquaMai.Mods/Utils/LogUserId.cs
Normal file
22
AquaMai/AquaMai.Mods/Utils/LogUserId.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using System;
|
||||
using AquaMai.Config.Attributes;
|
||||
using HarmonyLib;
|
||||
using MelonLoader;
|
||||
using Net.Packet;
|
||||
using Net.Packet.Mai2;
|
||||
using Net.VO.Mai2;
|
||||
|
||||
namespace AquaMai.Mods.Utils;
|
||||
|
||||
[ConfigSection(
|
||||
en: "Log user ID on login.",
|
||||
zh: "登录时将 UserID 输出到日志")]
|
||||
public class LogUserId
|
||||
{
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(PacketGetUserPreview), MethodType.Constructor, typeof(ulong), typeof(string), typeof(Action<ulong, UserPreviewResponseVO>), typeof(Action<PacketStatus>))]
|
||||
public static void Postfix(ulong userId)
|
||||
{
|
||||
MelonLogger.Msg($"[LogUserId] UserLogin: {userId}");
|
||||
}
|
||||
}
|
||||
3
AquaMai/AquaMai.Mods/Utils/README.md
Normal file
3
AquaMai/AquaMai.Mods/Utils/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Utils
|
||||
|
||||
Debugging or diagnostic purpose utilities goes here. They don't affect the game play at all, but may show UIs if needed.
|
||||
100
AquaMai/AquaMai.Mods/Utils/ShowErrorLog.cs
Normal file
100
AquaMai/AquaMai.Mods/Utils/ShowErrorLog.cs
Normal file
@@ -0,0 +1,100 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using AquaMai.Config.Attributes;
|
||||
using AquaMai.Core.Helpers;
|
||||
using HarmonyLib;
|
||||
using Main;
|
||||
using MelonLoader;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AquaMai.Mods.Utils;
|
||||
|
||||
[ConfigSection(
|
||||
en: "Show error log in the game.",
|
||||
zh: "在游戏中显示错误日志窗口而不是关闭游戏进程")]
|
||||
public class ShowErrorLog
|
||||
{
|
||||
private static Ui _errorUi;
|
||||
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(GameMain), "ExceptionHandler")]
|
||||
private static void ExceptionHandler(GameMain __instance, Exception e)
|
||||
{
|
||||
if (_errorUi == null)
|
||||
{
|
||||
_errorUi = new GameObject("ErrorUI").AddComponent<Ui>();
|
||||
_errorUi.gameObject.SetActive(true);
|
||||
}
|
||||
string logFile = $"{MAI2System.Path.ErrorLogPath}{DateTime.Now:yyyyMMddHHmmss}.log";
|
||||
MelonLogger.Msg("Error Log:");
|
||||
if (File.Exists(logFile))
|
||||
{
|
||||
MelonLogger.Error(File.ReadAllText(logFile));
|
||||
_errorUi.SetErrorLog(File.ReadAllText(logFile));
|
||||
}
|
||||
else
|
||||
{
|
||||
MelonLogger.Error(e);
|
||||
_errorUi.SetErrorLog(e.ToString());
|
||||
}
|
||||
|
||||
Application.quitting += ApplicationOnQuitting;
|
||||
_errorUi.StartCoroutine(_errorUi.Show());
|
||||
}
|
||||
|
||||
private static void ApplicationOnQuitting()
|
||||
{
|
||||
Thread.Sleep(Timeout.Infinite);
|
||||
}
|
||||
|
||||
private class Ui : MonoBehaviour
|
||||
{
|
||||
private string _errorLog = "";
|
||||
|
||||
public void SetErrorLog(string text)
|
||||
{
|
||||
_errorLog = "Error Log:\n" + text;
|
||||
}
|
||||
|
||||
public void OnGUI()
|
||||
{
|
||||
var labelStyle = new GUIStyle(GUI.skin.label)
|
||||
{
|
||||
fontSize = GuiSizes.FontSize,
|
||||
alignment = TextAnchor.MiddleLeft,
|
||||
normal = new GUIStyleState(){textColor = Color.black}
|
||||
};
|
||||
|
||||
var boxStyle = new GUIStyle(GUI.skin.box)
|
||||
{
|
||||
normal = new GUIStyleState() { background = Texture2D.whiteTexture }
|
||||
};
|
||||
|
||||
int logLineCount = Regex.Matches(_errorLog, "\n").Count + 1;
|
||||
float offset = GuiSizes.PlayerCenter * 0.12f;
|
||||
var x = GuiSizes.PlayerCenter / 2f + offset / 2f;
|
||||
var y = Screen.height / 1.8f;
|
||||
var width = GuiSizes.PlayerCenter - offset;
|
||||
var height = GuiSizes.LabelHeight * logLineCount + GuiSizes.Margin * 2;
|
||||
|
||||
GUI.Box(new Rect(x, y, width, height), "", boxStyle);
|
||||
GUI.Label(new Rect(x, y, width, height), _errorLog, labelStyle);
|
||||
if (!GuiSizes.SinglePlayer)
|
||||
{
|
||||
GUI.Box(new Rect(x + GuiSizes.PlayerWidth, y, width, height), "", boxStyle);
|
||||
GUI.Label(new Rect(x + GuiSizes.PlayerWidth, y, width, height), _errorLog, labelStyle);
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerator Show()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
yield return null; // 让 Unity 处理一帧
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
72
AquaMai/AquaMai.Mods/Utils/ShowNetErrorDetail.cs
Normal file
72
AquaMai/AquaMai.Mods/Utils/ShowNetErrorDetail.cs
Normal file
@@ -0,0 +1,72 @@
|
||||
using System.Collections.Generic;
|
||||
using AquaMai.Config.Attributes;
|
||||
using AquaMai.Core.Helpers;
|
||||
using AquaMai.Core.Resources;
|
||||
using HarmonyLib;
|
||||
using MAI2.Util;
|
||||
using Manager;
|
||||
using Monitor;
|
||||
using Process;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AquaMai.Mods.Utils;
|
||||
|
||||
[ConfigSection(
|
||||
en: "Show Network error detail in the game when gray network icon appears.",
|
||||
zh: "出现灰网时显示原因")]
|
||||
public class ShowNetErrorDetail
|
||||
{
|
||||
[HarmonyPatch(typeof(CommonProcess), "OnStart")]
|
||||
[HarmonyPostfix]
|
||||
public static void SetIconStatus(CommonMonitor[] ____monitors)
|
||||
{
|
||||
____monitors[0].gameObject.AddComponent<DetailUi>();
|
||||
}
|
||||
|
||||
private class DetailUi : MonoBehaviour
|
||||
{
|
||||
public void OnGUI()
|
||||
{
|
||||
var errors = new List<string>();
|
||||
if (!Singleton<OperationManager>.Instance.IsAliveServer)
|
||||
{
|
||||
errors.Add(Locale.NetErrIsAliveServer);
|
||||
}
|
||||
|
||||
if (!Singleton<OperationManager>.Instance.IsAliveAimeReader)
|
||||
{
|
||||
errors.Add(Locale.NetErrIsAliveAimeReader);
|
||||
}
|
||||
|
||||
if (!Singleton<OperationManager>.Instance.IsAliveAimeServer)
|
||||
{
|
||||
errors.Add(Locale.NetErrIsAliveAimeServer);
|
||||
}
|
||||
|
||||
if (!Singleton<OperationManager>.Instance.WasDownloadSuccessOnce)
|
||||
{
|
||||
errors.Add(Locale.NetErrWasDownloadSuccessOnce);
|
||||
}
|
||||
|
||||
if (errors.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var labelStyle = GUI.skin.GetStyle("label");
|
||||
labelStyle.fontSize = GuiSizes.FontSize;
|
||||
labelStyle.alignment = TextAnchor.MiddleCenter;
|
||||
|
||||
var x = GuiSizes.PlayerCenter + GuiSizes.PlayerWidth * .2f;
|
||||
var y = Screen.height * .01f;
|
||||
var width = GuiSizes.FontSize * 15f;
|
||||
var height = GuiSizes.LabelHeight * errors.Count + GuiSizes.Margin * 2;
|
||||
|
||||
GUI.Box(new Rect(x, y, width, height), "");
|
||||
for (var i = 0; i < errors.Count; i++)
|
||||
{
|
||||
GUI.Label(new Rect(x, y + GuiSizes.Margin + GuiSizes.LabelHeight * i, width, GuiSizes.LabelHeight), errors[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user