[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:
Menci
2024-11-25 01:25:19 +08:00
committed by GitHub
parent e9ee31b22a
commit 37044dae01
217 changed files with 6051 additions and 3040 deletions

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

View File

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

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

View File

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

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

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

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

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

View 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).

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

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

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

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

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

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

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