From 37044dae017318cea4acc428fab4373e010eb22d Mon Sep 17 00:00:00 2001 From: Menci Date: Mon, 25 Nov 2024 01:25:19 +0800 Subject: [PATCH] [RF] AquaMai configuration refactor (#82) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 更新了配置文件格式,原有的配置文件将被自动无缝迁移,详情请见新的配置文件中的注释(例外:`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. --- AquaMai/.gitignore | 3 - AquaMai/AquaMai.Build/AquaMai.Build.csproj | 50 +++ .../AquaMai.Build/GenerateExampleConfig.cs | 42 +++ AquaMai/AquaMai.Build/PostBuildPatch.cs | 52 +++ .../AquaMai.Config.HeadlessLoader.csproj | 45 +++ .../ConfigAssemblyLoader.cs | 68 ++++ .../CustomAssemblyResolver.cs | 11 + .../HeadlessConfigInterface.cs | 59 +++ .../HeadlessConfigLoader.cs | 59 +++ .../Polyfills.cs | 4 + .../ResourceLoader.cs | 42 +++ .../AquaMai.Config.Interfaces.csproj | 40 ++ AquaMai/AquaMai.Config.Interfaces/IConfig.cs | 28 ++ .../IConfigMigrationManager.cs | 7 + .../IConfigParser.cs | 7 + .../IConfigSerializer.cs | 13 + .../AquaMai.Config.Interfaces/IConfigView.cs | 9 + .../IReflectionManager.cs | 41 +++ .../IReflectionProvider.cs | 30 ++ .../AquaMai.Config.Interfaces/Polyfills.cs | 4 + AquaMai/AquaMai.Config/AquaMai.Config.csproj | 60 +++ .../ConfigCollapseNamespaceAttribute.cs | 9 + .../Attributes/ConfigComment.cs | 13 + .../Attributes/ConfigEntryAttribute.cs | 24 ++ .../Attributes/ConfigSectionAttribute.cs | 21 ++ .../Attributes/EnableCondition.cs | 78 ++++ AquaMai/AquaMai.Config/Config.cs | 98 +++++ AquaMai/AquaMai.Config/ConfigParser.cs | 125 +++++++ AquaMai/AquaMai.Config/ConfigSerializer.cs | 185 ++++++++++ AquaMai/AquaMai.Config/ConfigView.cs | 105 ++++++ AquaMai/{ => AquaMai.Config}/FodyWeavers.xml | 4 +- AquaMai/{ => AquaMai.Config}/FodyWeavers.xsd | 0 .../Migration/ConfigMigrationManager.cs | 58 +++ .../Migration/ConfigMigration_V1_0_V2_0.cs | 346 ++++++++++++++++++ .../Migration/IConfigMigration.cs | 10 + AquaMai/AquaMai.Config/Polyfills.cs | 4 + .../MonoCecilAssemblyReflectionProvider.cs | 254 +++++++++++++ .../Reflection/ReflectionManager.cs | 178 +++++++++ .../Reflection/SystemReflectionProvider.cs | 47 +++ .../Types}/KeyCodeID.cs | 2 +- .../Types/KeyCodeOrName.cs} | 4 +- AquaMai/AquaMai.Config/Utility.cs | 183 +++++++++ AquaMai/AquaMai.Core/AquaMai.Core.csproj | 132 +++++++ .../Attributes/EnableGameVersionAttribute.cs | 18 + .../Attributes/EnableIfAttribute.cs | 79 ++++ .../Attributes/EnableImplicitlyIfAttribute.cs | 12 + AquaMai/AquaMai.Core/ConfigLoader.cs | 83 +++++ .../Helpers/EnableConditionHelper.cs | 72 ++++ AquaMai/AquaMai.Core/Helpers/FileSystem.cs | 15 + .../{ => AquaMai.Core}/Helpers/GameInfo.cs | 2 +- .../{ => AquaMai.Core}/Helpers/GuiSizes.cs | 6 +- AquaMai/AquaMai.Core/Helpers/KeyListener.cs | 146 ++++++++ .../Helpers/MessageHelper.cs | 2 +- .../Helpers/MusicDirHelper.cs | 5 +- .../Helpers/SharedInstances.cs | 2 +- AquaMai/{ => AquaMai.Core}/Helpers/Shim.cs | 2 +- .../Resources}/I18nSingleAssemblyHook.cs | 9 +- .../Resources/Locale.Designer.cs | 68 ++-- .../{ => AquaMai.Core}/Resources/Locale.resx | 0 .../Resources/Locale.zh.resx | 0 AquaMai/AquaMai.Core/Startup.cs | 199 ++++++++++ AquaMai/AquaMai.Mods/AquaMai.Mods.csproj | 127 +++++++ AquaMai/AquaMai.Mods/DeprecationWarning.cs | 29 ++ .../Fancy}/CustomLogo.cs | 30 +- AquaMai/AquaMai.Mods/Fancy/CustomPlaceName.cs | 45 +++ .../Fancy}/CustomSkins.cs | 26 +- .../Fancy}/CustomTrackStartDiff.cs | 14 +- .../AquaMai.Mods/Fancy/CustomVersionString.cs | 30 ++ AquaMai/AquaMai.Mods/Fancy/DemoMaster.cs | 34 ++ .../GamePlay/AlignCircleSlideJudgeDisplay.cs} | 15 +- .../Fancy/GamePlay}/BreakSlideJudgeBlink.cs | 14 +- .../CustomNoteTypes/CustomNoteTypes.cs} | 79 ++-- .../Libs}/CustomSlideNoteData.cs | 4 +- .../CustomNoteTypes/Libs}/MaiGeometry.cs | 6 +- .../Libs}/ParametricSlidePath.cs | 4 +- .../CustomNoteTypes/Libs}/SlideCodeParser.cs | 4 +- .../CustomNoteTypes/Libs}/SlideDataBuilder.cs | 6 +- .../Libs}/SlidePathGenerator.cs | 5 +- .../Fancy/GamePlay}/DisableTrackStartTabs.cs | 14 +- .../Fancy/GamePlay}/ExtendNotesPool.cs | 18 +- .../Fancy/GamePlay/FanJudgeFlip.cs | 42 +++ .../Fancy/GamePlay}/HideHanabi.cs | 8 +- .../Fancy/GamePlay}/JudgeDisplay4B.cs | 16 +- .../Fancy/GamePlay/RealisticRandomJudge.cs | 39 ++ .../Fancy/GamePlay}/SlideArrowAnimation.cs | 11 +- .../Fancy/GamePlay}/SlideFadeInTweak.cs | 8 +- .../Fancy/GamePlay}/SlideLayerReverse.cs | 14 +- .../Fancy/GamePlay}/TrackStartProcessTweak.cs | 14 +- .../{UX => AquaMai.Mods/Fancy}/HideMask.cs | 8 +- AquaMai/AquaMai.Mods/Fancy/README.md | 7 + AquaMai/AquaMai.Mods/Fancy/RandomBgm.cs | 88 +++++ AquaMai/AquaMai.Mods/Fancy/Triggers.cs | 68 ++++ .../Fix/Common.cs} | 53 ++- .../{ => AquaMai.Mods}/Fix/DebugFeature.cs | 8 +- AquaMai/AquaMai.Mods/Fix/DisableReboot.cs | 87 +++++ .../{ => AquaMai.Mods}/Fix/FixCheckAuth.cs | 4 +- .../{ => AquaMai.Mods}/Fix/FixConnSlide.cs | 10 +- .../Fix}/FixLevelDisplay.cs | 11 +- .../Fix}/FixSlideAutoPlay.cs | 4 +- .../Fix/Legacy/FixQuickRetry130.cs} | 10 +- AquaMai/AquaMai.Mods/Fix/README.md | 7 + .../Fix/Stability/FixMissingCharaCrash.cs | 45 +++ .../AquaMai.Mods/GameSettings/CreditConfig.cs | 49 +++ .../GameSettings}/ForceAsServer.cs | 6 +- .../AquaMai.Mods/GameSettings/JudgeAdjust.cs | 50 +++ AquaMai/AquaMai.Mods/GameSettings/README.md | 5 + .../GameSettings/TouchSensitivity.cs | 154 ++++++++ .../AquaMai.Mods/GameSystem/Assets/Fonts.cs | 146 ++++++++ .../Assets/LoadAssetBundleWithoutManifest.cs | 38 ++ .../GameSystem/Assets/LoadLocalImages.cs} | 50 +-- .../Assets/UseJacketAsDummyMovie.cs} | 16 +- .../GameSystem/CustomCameraId.cs} | 76 +++- .../AquaMai.Mods/GameSystem/DisableTimeout.cs | 65 ++++ AquaMai/AquaMai.Mods/GameSystem/KeyMap.cs | 80 ++++ AquaMai/AquaMai.Mods/GameSystem/QuickRetry.cs | 34 ++ AquaMai/AquaMai.Mods/GameSystem/README.md | 5 + .../GameSystem}/RemoveEncryption.cs | 15 +- .../AquaMai.Mods/GameSystem/SinglePlayer.cs | 114 ++++++ AquaMai/AquaMai.Mods/GameSystem/TestProof.cs | 69 ++++ .../GameSystem/TouchPanelBaudRate.cs | 30 ++ .../GameSystem}/TouchToButtonInput.cs | 9 +- AquaMai/AquaMai.Mods/GameSystem/Unlock.cs | 90 +++++ .../GameSystem/Window.cs} | 41 ++- AquaMai/AquaMai.Mods/General.cs | 42 +++ .../Tweaks/IgnoreAimeServerError.cs | 19 + AquaMai/AquaMai.Mods/Tweaks/LockFrameRate.cs | 22 ++ AquaMai/AquaMai.Mods/Tweaks/README.md | 5 + .../Tweaks/ResetTouchAfterTrack.cs} | 10 +- .../Tweaks/SkipUserVersionCheck.cs | 22 ++ .../TimeSaving/EntryToMusicSelection.cs | 52 +++ .../TimeSaving/IWontTapOrSlideVigorously.cs | 19 + .../Tweaks/TimeSaving/SkipEventInfo.cs | 27 ++ .../Tweaks/TimeSaving/SkipGoodbyeScreen.cs} | 10 +- .../Tweaks/TimeSaving/SkipStartupDelays.cs | 32 ++ .../Tweaks/TimeSaving/SkipStartupWarning.cs | 31 ++ .../Tweaks}/TimeSaving/SkipTrackStart.cs | 8 +- AquaMai/AquaMai.Mods/UX/CiBuildAlert.cs | 26 ++ .../UX}/HideSelfMadeCharts.cs | 43 ++- .../{ => AquaMai.Mods}/UX/ImmediateSave.cs | 20 +- .../UX/JudgeAccuracyInfo.cs | 8 +- .../UX/OneKeyEntryEnd.cs} | 39 +- AquaMai/AquaMai.Mods/UX/OneKeyRetrySkip.cs | 46 +++ .../UX/PracticeMode/Libs}/PractiseModeUI.cs | 48 +-- .../UX/PracticeMode/PracticeMode.cs} | 35 +- .../UX/QuickEndPlay.cs} | 13 +- AquaMai/AquaMai.Mods/UX/README.md | 3 + .../UX}/SelectionDetail.cs | 14 +- .../Utils/DisplayFrameRate.cs} | 10 +- .../Utils/LogNetworkErrors.cs | 4 +- AquaMai/{ => AquaMai.Mods}/Utils/LogUserId.cs | 8 +- AquaMai/AquaMai.Mods/Utils/README.md | 3 + .../{ => AquaMai.Mods}/Utils/ShowErrorLog.cs | 12 +- .../Utils/ShowNetErrorDetail.cs | 11 +- AquaMai/AquaMai.csproj | 320 ---------------- AquaMai/AquaMai.sln | 59 ++- AquaMai/AquaMai/AquaMai.csproj | 74 ++++ AquaMai/AquaMai/AssemblyLoader.cs | 70 ++++ AquaMai/AquaMai/BuildInfo.cs | 12 + AquaMai/AquaMai/Main.cs | 39 ++ .../{ => AquaMai}/Properties/AssemblyInfo.cs | 0 AquaMai/Attributes/ConfigCommentAttribute.cs | 10 - AquaMai/Attributes/GameVersionAttribute.cs | 10 - AquaMai/Cheat/Config.cs | 21 -- AquaMai/Cheat/MapUnlock.cs | 26 -- AquaMai/Cheat/TicketUnlock.cs | 40 -- AquaMai/Cheat/UnlockUtage.cs | 20 - AquaMai/Config.cs | 65 ---- AquaMai/ConfigGenerator.cs | 50 --- AquaMai/CustomCameraId/Config.cs | 40 -- AquaMai/CustomCameraId/PrintCameraList.cs | 31 -- AquaMai/CustomKeyMap/Config.cs | 32 -- AquaMai/CustomKeyMap/Enable.cs | 15 - AquaMai/Fix/Config.cs | 90 ----- AquaMai/Fix/DisableReboot.cs | 86 ----- AquaMai/Fix/FixCharaCrash.cs | 44 --- AquaMai/Fix/FontFix.cs | 32 -- AquaMai/Fix/ForceFreePlay.cs | 14 - AquaMai/Fix/ForcePaidPlay.cs | 30 -- AquaMai/Fix/FrameRateLock.cs | 12 - AquaMai/Fix/HanabiFix.cs | 20 - AquaMai/Fix/IgnoreAimeServerError.cs | 15 - AquaMai/Fix/RealisticRandomJudge.cs | 25 -- AquaMai/Fix/SkipVersionCheck.cs | 16 - AquaMai/Libs/.gitignore | 2 + AquaMai/Libs/Mono.Cecil.dll | Bin 0 -> 339456 bytes AquaMai/Main.cs | 219 ----------- AquaMai/ModKeyMap/Config.cs | 54 --- AquaMai/ModKeyMap/EnableNativeQuickRetry.cs | 19 - AquaMai/ModKeyMap/ModKeyListener.cs | 145 -------- AquaMai/ModKeyMap/TestProof.cs | 28 -- AquaMai/TimeSaving/Config.cs | 49 --- .../TimeSaving/IWontTapOrSlideVigorously.cs | 15 - AquaMai/TimeSaving/ImproveLoadSpeed.cs | 26 -- AquaMai/TimeSaving/SkipEventInfo.cs | 24 -- AquaMai/TimeSaving/SkipToMusicSelection.cs | 49 --- AquaMai/TimeSaving/SkipWarningScreen.cs | 28 -- AquaMai/TouchSensitivity/Config.cs | 72 ---- AquaMai/TouchSensitivity/Enable.cs | 25 -- AquaMai/UX/CiBuildAlert.cs | 16 - AquaMai/UX/Config.cs | 140 ------- AquaMai/UX/CustomFont.cs | 71 ---- AquaMai/UX/CustomPlaceName.cs | 32 -- AquaMai/UX/CustomVersionString.cs | 24 -- AquaMai/UX/DemoMaster.cs | 31 -- AquaMai/UX/ExtendTimer.cs | 56 --- AquaMai/UX/LoadAssetBundleWithoutManifest.cs | 32 -- AquaMai/UX/RandomBgm.cs | 73 ---- AquaMai/UX/RunCommandOnEvents.cs | 53 --- AquaMai/UX/SinglePlayer.cs | 72 ---- AquaMai/Utils/Config.cs | 59 --- AquaMai/Utils/JudgeAdjust.cs | 31 -- AquaMai/Utils/TouchPanelBaudRate.cs | 15 - AquaMai/Visual/Config.cs | 107 ------ AquaMai/Visual/FanJudgeFlip.cs | 32 -- AquaMai/WindowState/Config.cs | 31 -- AquaMai/genConfig.csx | 3 - config/application.properties | 16 +- 217 files changed, 6051 insertions(+), 3040 deletions(-) create mode 100644 AquaMai/AquaMai.Build/AquaMai.Build.csproj create mode 100644 AquaMai/AquaMai.Build/GenerateExampleConfig.cs create mode 100644 AquaMai/AquaMai.Build/PostBuildPatch.cs create mode 100644 AquaMai/AquaMai.Config.HeadlessLoader/AquaMai.Config.HeadlessLoader.csproj create mode 100644 AquaMai/AquaMai.Config.HeadlessLoader/ConfigAssemblyLoader.cs create mode 100644 AquaMai/AquaMai.Config.HeadlessLoader/CustomAssemblyResolver.cs create mode 100644 AquaMai/AquaMai.Config.HeadlessLoader/HeadlessConfigInterface.cs create mode 100644 AquaMai/AquaMai.Config.HeadlessLoader/HeadlessConfigLoader.cs create mode 100644 AquaMai/AquaMai.Config.HeadlessLoader/Polyfills.cs create mode 100644 AquaMai/AquaMai.Config.HeadlessLoader/ResourceLoader.cs create mode 100644 AquaMai/AquaMai.Config.Interfaces/AquaMai.Config.Interfaces.csproj create mode 100644 AquaMai/AquaMai.Config.Interfaces/IConfig.cs create mode 100644 AquaMai/AquaMai.Config.Interfaces/IConfigMigrationManager.cs create mode 100644 AquaMai/AquaMai.Config.Interfaces/IConfigParser.cs create mode 100644 AquaMai/AquaMai.Config.Interfaces/IConfigSerializer.cs create mode 100644 AquaMai/AquaMai.Config.Interfaces/IConfigView.cs create mode 100644 AquaMai/AquaMai.Config.Interfaces/IReflectionManager.cs create mode 100644 AquaMai/AquaMai.Config.Interfaces/IReflectionProvider.cs create mode 100644 AquaMai/AquaMai.Config.Interfaces/Polyfills.cs create mode 100644 AquaMai/AquaMai.Config/AquaMai.Config.csproj create mode 100644 AquaMai/AquaMai.Config/Attributes/ConfigCollapseNamespaceAttribute.cs create mode 100644 AquaMai/AquaMai.Config/Attributes/ConfigComment.cs create mode 100644 AquaMai/AquaMai.Config/Attributes/ConfigEntryAttribute.cs create mode 100644 AquaMai/AquaMai.Config/Attributes/ConfigSectionAttribute.cs create mode 100644 AquaMai/AquaMai.Config/Attributes/EnableCondition.cs create mode 100644 AquaMai/AquaMai.Config/Config.cs create mode 100644 AquaMai/AquaMai.Config/ConfigParser.cs create mode 100644 AquaMai/AquaMai.Config/ConfigSerializer.cs create mode 100644 AquaMai/AquaMai.Config/ConfigView.cs rename AquaMai/{ => AquaMai.Config}/FodyWeavers.xml (62%) rename AquaMai/{ => AquaMai.Config}/FodyWeavers.xsd (100%) create mode 100644 AquaMai/AquaMai.Config/Migration/ConfigMigrationManager.cs create mode 100644 AquaMai/AquaMai.Config/Migration/ConfigMigration_V1_0_V2_0.cs create mode 100644 AquaMai/AquaMai.Config/Migration/IConfigMigration.cs create mode 100644 AquaMai/AquaMai.Config/Polyfills.cs create mode 100644 AquaMai/AquaMai.Config/Reflection/MonoCecilAssemblyReflectionProvider.cs create mode 100644 AquaMai/AquaMai.Config/Reflection/ReflectionManager.cs create mode 100644 AquaMai/AquaMai.Config/Reflection/SystemReflectionProvider.cs rename AquaMai/{CustomKeyMap => AquaMai.Config/Types}/KeyCodeID.cs (97%) rename AquaMai/{ModKeyMap/ModKeyCode.cs => AquaMai.Config/Types/KeyCodeOrName.cs} (90%) create mode 100644 AquaMai/AquaMai.Config/Utility.cs create mode 100644 AquaMai/AquaMai.Core/AquaMai.Core.csproj create mode 100644 AquaMai/AquaMai.Core/Attributes/EnableGameVersionAttribute.cs create mode 100644 AquaMai/AquaMai.Core/Attributes/EnableIfAttribute.cs create mode 100644 AquaMai/AquaMai.Core/Attributes/EnableImplicitlyIfAttribute.cs create mode 100644 AquaMai/AquaMai.Core/ConfigLoader.cs create mode 100644 AquaMai/AquaMai.Core/Helpers/EnableConditionHelper.cs create mode 100644 AquaMai/AquaMai.Core/Helpers/FileSystem.cs rename AquaMai/{ => AquaMai.Core}/Helpers/GameInfo.cs (95%) rename AquaMai/{ => AquaMai.Core}/Helpers/GuiSizes.cs (89%) create mode 100644 AquaMai/AquaMai.Core/Helpers/KeyListener.cs rename AquaMai/{ => AquaMai.Core}/Helpers/MessageHelper.cs (97%) rename AquaMai/{ => AquaMai.Core}/Helpers/MusicDirHelper.cs (90%) rename AquaMai/{ => AquaMai.Core}/Helpers/SharedInstances.cs (95%) rename AquaMai/{ => AquaMai.Core}/Helpers/Shim.cs (99%) rename AquaMai/{Fix => AquaMai.Core/Resources}/I18nSingleAssemblyHook.cs (86%) rename AquaMai/{ => AquaMai.Core}/Resources/Locale.Designer.cs (85%) rename AquaMai/{ => AquaMai.Core}/Resources/Locale.resx (100%) rename AquaMai/{ => AquaMai.Core}/Resources/Locale.zh.resx (100%) create mode 100644 AquaMai/AquaMai.Core/Startup.cs create mode 100644 AquaMai/AquaMai.Mods/AquaMai.Mods.csproj create mode 100644 AquaMai/AquaMai.Mods/DeprecationWarning.cs rename AquaMai/{Visual => AquaMai.Mods/Fancy}/CustomLogo.cs (58%) create mode 100644 AquaMai/AquaMai.Mods/Fancy/CustomPlaceName.cs rename AquaMai/{Visual => AquaMai.Mods/Fancy}/CustomSkins.cs (95%) rename AquaMai/{Visual => AquaMai.Mods/Fancy}/CustomTrackStartDiff.cs (80%) create mode 100644 AquaMai/AquaMai.Mods/Fancy/CustomVersionString.cs create mode 100644 AquaMai/AquaMai.Mods/Fancy/DemoMaster.cs rename AquaMai/{Visual/FixCircleSlideJudge.cs => AquaMai.Mods/Fancy/GamePlay/AlignCircleSlideJudgeDisplay.cs} (76%) rename AquaMai/{Visual => AquaMai.Mods/Fancy/GamePlay}/BreakSlideJudgeBlink.cs (56%) rename AquaMai/{MaimaiDX2077/CustomNoteTypePatch.cs => AquaMai.Mods/Fancy/GamePlay/CustomNoteTypes/CustomNoteTypes.cs} (94%) rename AquaMai/{MaimaiDX2077 => AquaMai.Mods/Fancy/GamePlay/CustomNoteTypes/Libs}/CustomSlideNoteData.cs (96%) rename AquaMai/{MaimaiDX2077 => AquaMai.Mods/Fancy/GamePlay/CustomNoteTypes/Libs}/MaiGeometry.cs (97%) rename AquaMai/{MaimaiDX2077 => AquaMai.Mods/Fancy/GamePlay/CustomNoteTypes/Libs}/ParametricSlidePath.cs (99%) rename AquaMai/{MaimaiDX2077 => AquaMai.Mods/Fancy/GamePlay/CustomNoteTypes/Libs}/SlideCodeParser.cs (99%) rename AquaMai/{MaimaiDX2077 => AquaMai.Mods/Fancy/GamePlay/CustomNoteTypes/Libs}/SlideDataBuilder.cs (99%) rename AquaMai/{MaimaiDX2077 => AquaMai.Mods/Fancy/GamePlay/CustomNoteTypes/Libs}/SlidePathGenerator.cs (98%) rename AquaMai/{Visual => AquaMai.Mods/Fancy/GamePlay}/DisableTrackStartTabs.cs (80%) rename AquaMai/{Fix => AquaMai.Mods/Fancy/GamePlay}/ExtendNotesPool.cs (92%) create mode 100644 AquaMai/AquaMai.Mods/Fancy/GamePlay/FanJudgeFlip.cs rename AquaMai/{UX => AquaMai.Mods/Fancy/GamePlay}/HideHanabi.cs (73%) rename AquaMai/{Visual => AquaMai.Mods/Fancy/GamePlay}/JudgeDisplay4B.cs (80%) create mode 100644 AquaMai/AquaMai.Mods/Fancy/GamePlay/RealisticRandomJudge.cs rename AquaMai/{Visual => AquaMai.Mods/Fancy/GamePlay}/SlideArrowAnimation.cs (93%) rename AquaMai/{Visual => AquaMai.Mods/Fancy/GamePlay}/SlideFadeInTweak.cs (94%) rename AquaMai/{Visual => AquaMai.Mods/Fancy/GamePlay}/SlideLayerReverse.cs (84%) rename AquaMai/{Visual => AquaMai.Mods/Fancy/GamePlay}/TrackStartProcessTweak.cs (88%) rename AquaMai/{UX => AquaMai.Mods/Fancy}/HideMask.cs (61%) create mode 100644 AquaMai/AquaMai.Mods/Fancy/README.md create mode 100644 AquaMai/AquaMai.Mods/Fancy/RandomBgm.cs create mode 100644 AquaMai/AquaMai.Mods/Fancy/Triggers.cs rename AquaMai/{Fix/BasicFix.cs => AquaMai.Mods/Fix/Common.cs} (62%) rename AquaMai/{ => AquaMai.Mods}/Fix/DebugFeature.cs (96%) create mode 100644 AquaMai/AquaMai.Mods/Fix/DisableReboot.cs rename AquaMai/{ => AquaMai.Mods}/Fix/FixCheckAuth.cs (80%) rename AquaMai/{ => AquaMai.Mods}/Fix/FixConnSlide.cs (93%) rename AquaMai/{Visual => AquaMai.Mods/Fix}/FixLevelDisplay.cs (90%) rename AquaMai/{Visual => AquaMai.Mods/Fix}/FixSlideAutoPlay.cs (97%) rename AquaMai/{Fix/FestivalQuickRetryFix.cs => AquaMai.Mods/Fix/Legacy/FixQuickRetry130.cs} (78%) create mode 100644 AquaMai/AquaMai.Mods/Fix/README.md create mode 100644 AquaMai/AquaMai.Mods/Fix/Stability/FixMissingCharaCrash.cs create mode 100644 AquaMai/AquaMai.Mods/GameSettings/CreditConfig.cs rename AquaMai/{Fix => AquaMai.Mods/GameSettings}/ForceAsServer.cs (66%) create mode 100644 AquaMai/AquaMai.Mods/GameSettings/JudgeAdjust.cs create mode 100644 AquaMai/AquaMai.Mods/GameSettings/README.md create mode 100644 AquaMai/AquaMai.Mods/GameSettings/TouchSensitivity.cs create mode 100644 AquaMai/AquaMai.Mods/GameSystem/Assets/Fonts.cs create mode 100644 AquaMai/AquaMai.Mods/GameSystem/Assets/LoadAssetBundleWithoutManifest.cs rename AquaMai/{UX/LoadAssetsPng.cs => AquaMai.Mods/GameSystem/Assets/LoadLocalImages.cs} (91%) rename AquaMai/{UX/LoadLocalBga.cs => AquaMai.Mods/GameSystem/Assets/UseJacketAsDummyMovie.cs} (73%) rename AquaMai/{CustomCameraId/Enable.cs => AquaMai.Mods/GameSystem/CustomCameraId.cs} (50%) create mode 100644 AquaMai/AquaMai.Mods/GameSystem/DisableTimeout.cs create mode 100644 AquaMai/AquaMai.Mods/GameSystem/KeyMap.cs create mode 100644 AquaMai/AquaMai.Mods/GameSystem/QuickRetry.cs create mode 100644 AquaMai/AquaMai.Mods/GameSystem/README.md rename AquaMai/{Fix => AquaMai.Mods/GameSystem}/RemoveEncryption.cs (64%) create mode 100644 AquaMai/AquaMai.Mods/GameSystem/SinglePlayer.cs create mode 100644 AquaMai/AquaMai.Mods/GameSystem/TestProof.cs create mode 100644 AquaMai/AquaMai.Mods/GameSystem/TouchPanelBaudRate.cs rename AquaMai/{UX => AquaMai.Mods/GameSystem}/TouchToButtonInput.cs (90%) create mode 100644 AquaMai/AquaMai.Mods/GameSystem/Unlock.cs rename AquaMai/{WindowState/Enable.cs => AquaMai.Mods/GameSystem/Window.cs} (58%) create mode 100644 AquaMai/AquaMai.Mods/General.cs create mode 100644 AquaMai/AquaMai.Mods/Tweaks/IgnoreAimeServerError.cs create mode 100644 AquaMai/AquaMai.Mods/Tweaks/LockFrameRate.cs create mode 100644 AquaMai/AquaMai.Mods/Tweaks/README.md rename AquaMai/{Fix/TouchResetAfterTrack.cs => AquaMai.Mods/Tweaks/ResetTouchAfterTrack.cs} (73%) create mode 100644 AquaMai/AquaMai.Mods/Tweaks/SkipUserVersionCheck.cs create mode 100644 AquaMai/AquaMai.Mods/Tweaks/TimeSaving/EntryToMusicSelection.cs create mode 100644 AquaMai/AquaMai.Mods/Tweaks/TimeSaving/IWontTapOrSlideVigorously.cs create mode 100644 AquaMai/AquaMai.Mods/Tweaks/TimeSaving/SkipEventInfo.cs rename AquaMai/{TimeSaving/SkipGameOverScreen.cs => AquaMai.Mods/Tweaks/TimeSaving/SkipGoodbyeScreen.cs} (68%) create mode 100644 AquaMai/AquaMai.Mods/Tweaks/TimeSaving/SkipStartupDelays.cs create mode 100644 AquaMai/AquaMai.Mods/Tweaks/TimeSaving/SkipStartupWarning.cs rename AquaMai/{ => AquaMai.Mods/Tweaks}/TimeSaving/SkipTrackStart.cs (55%) create mode 100644 AquaMai/AquaMai.Mods/UX/CiBuildAlert.cs rename AquaMai/{ModKeyMap => AquaMai.Mods/UX}/HideSelfMadeCharts.cs (70%) rename AquaMai/{ => AquaMai.Mods}/UX/ImmediateSave.cs (92%) rename AquaMai/{ => AquaMai.Mods}/UX/JudgeAccuracyInfo.cs (97%) rename AquaMai/{ModKeyMap/QuickSkip.cs => AquaMai.Mods/UX/OneKeyEntryEnd.cs} (61%) create mode 100644 AquaMai/AquaMai.Mods/UX/OneKeyRetrySkip.cs rename AquaMai/{ModKeyMap => AquaMai.Mods/UX/PracticeMode/Libs}/PractiseModeUI.cs (82%) rename AquaMai/{ModKeyMap/PractiseMode.cs => AquaMai.Mods/UX/PracticeMode/PracticeMode.cs} (89%) rename AquaMai/{TimeSaving/ShowQuickEndPlay.cs => AquaMai.Mods/UX/QuickEndPlay.cs} (85%) create mode 100644 AquaMai/AquaMai.Mods/UX/README.md rename AquaMai/{Utils => AquaMai.Mods/UX}/SelectionDetail.cs (93%) rename AquaMai/{Utils/FrameRateDisplay.cs => AquaMai.Mods/Utils/DisplayFrameRate.cs} (85%) rename AquaMai/{ => AquaMai.Mods}/Utils/LogNetworkErrors.cs (92%) rename AquaMai/{ => AquaMai.Mods}/Utils/LogUserId.cs (64%) create mode 100644 AquaMai/AquaMai.Mods/Utils/README.md rename AquaMai/{ => AquaMai.Mods}/Utils/ShowErrorLog.cs (91%) rename AquaMai/{ => AquaMai.Mods}/Utils/ShowNetErrorDetail.cs (88%) delete mode 100644 AquaMai/AquaMai.csproj create mode 100644 AquaMai/AquaMai/AquaMai.csproj create mode 100644 AquaMai/AquaMai/AssemblyLoader.cs create mode 100644 AquaMai/AquaMai/BuildInfo.cs create mode 100644 AquaMai/AquaMai/Main.cs rename AquaMai/{ => AquaMai}/Properties/AssemblyInfo.cs (100%) delete mode 100644 AquaMai/Attributes/ConfigCommentAttribute.cs delete mode 100644 AquaMai/Attributes/GameVersionAttribute.cs delete mode 100644 AquaMai/Cheat/Config.cs delete mode 100644 AquaMai/Cheat/MapUnlock.cs delete mode 100644 AquaMai/Cheat/TicketUnlock.cs delete mode 100644 AquaMai/Cheat/UnlockUtage.cs delete mode 100644 AquaMai/Config.cs delete mode 100644 AquaMai/ConfigGenerator.cs delete mode 100644 AquaMai/CustomCameraId/Config.cs delete mode 100644 AquaMai/CustomCameraId/PrintCameraList.cs delete mode 100644 AquaMai/CustomKeyMap/Config.cs delete mode 100644 AquaMai/CustomKeyMap/Enable.cs delete mode 100644 AquaMai/Fix/Config.cs delete mode 100644 AquaMai/Fix/DisableReboot.cs delete mode 100644 AquaMai/Fix/FixCharaCrash.cs delete mode 100644 AquaMai/Fix/FontFix.cs delete mode 100644 AquaMai/Fix/ForceFreePlay.cs delete mode 100644 AquaMai/Fix/ForcePaidPlay.cs delete mode 100644 AquaMai/Fix/FrameRateLock.cs delete mode 100644 AquaMai/Fix/HanabiFix.cs delete mode 100644 AquaMai/Fix/IgnoreAimeServerError.cs delete mode 100644 AquaMai/Fix/RealisticRandomJudge.cs delete mode 100644 AquaMai/Fix/SkipVersionCheck.cs create mode 100644 AquaMai/Libs/.gitignore create mode 100644 AquaMai/Libs/Mono.Cecil.dll delete mode 100644 AquaMai/Main.cs delete mode 100644 AquaMai/ModKeyMap/Config.cs delete mode 100644 AquaMai/ModKeyMap/EnableNativeQuickRetry.cs delete mode 100644 AquaMai/ModKeyMap/ModKeyListener.cs delete mode 100644 AquaMai/ModKeyMap/TestProof.cs delete mode 100644 AquaMai/TimeSaving/Config.cs delete mode 100644 AquaMai/TimeSaving/IWontTapOrSlideVigorously.cs delete mode 100644 AquaMai/TimeSaving/ImproveLoadSpeed.cs delete mode 100644 AquaMai/TimeSaving/SkipEventInfo.cs delete mode 100644 AquaMai/TimeSaving/SkipToMusicSelection.cs delete mode 100644 AquaMai/TimeSaving/SkipWarningScreen.cs delete mode 100644 AquaMai/TouchSensitivity/Config.cs delete mode 100644 AquaMai/TouchSensitivity/Enable.cs delete mode 100644 AquaMai/UX/CiBuildAlert.cs delete mode 100644 AquaMai/UX/Config.cs delete mode 100644 AquaMai/UX/CustomFont.cs delete mode 100644 AquaMai/UX/CustomPlaceName.cs delete mode 100644 AquaMai/UX/CustomVersionString.cs delete mode 100644 AquaMai/UX/DemoMaster.cs delete mode 100644 AquaMai/UX/ExtendTimer.cs delete mode 100644 AquaMai/UX/LoadAssetBundleWithoutManifest.cs delete mode 100644 AquaMai/UX/RandomBgm.cs delete mode 100644 AquaMai/UX/RunCommandOnEvents.cs delete mode 100644 AquaMai/UX/SinglePlayer.cs delete mode 100644 AquaMai/Utils/Config.cs delete mode 100644 AquaMai/Utils/JudgeAdjust.cs delete mode 100644 AquaMai/Utils/TouchPanelBaudRate.cs delete mode 100644 AquaMai/Visual/Config.cs delete mode 100644 AquaMai/Visual/FanJudgeFlip.cs delete mode 100644 AquaMai/WindowState/Config.cs delete mode 100644 AquaMai/genConfig.csx diff --git a/AquaMai/.gitignore b/AquaMai/.gitignore index 4ddf2a82..df21fd82 100644 --- a/AquaMai/.gitignore +++ b/AquaMai/.gitignore @@ -372,6 +372,3 @@ MigrationBackup/ Output .idea -Libs/Assembly-CSharp.dll -Libs/AMDaemon.NET.dll -packages \ No newline at end of file diff --git a/AquaMai/AquaMai.Build/AquaMai.Build.csproj b/AquaMai/AquaMai.Build/AquaMai.Build.csproj new file mode 100644 index 00000000..ff6b804d --- /dev/null +++ b/AquaMai/AquaMai.Build/AquaMai.Build.csproj @@ -0,0 +1,50 @@ + + + + Release + AnyCPU + {4C0C68C3-8B2E-4CA8-A26D-AE87CF2A38A5} + Library + AquaMai.Build + AquaMai.Build + netstandard2.0 + 512 + true + 12 + 414;NU1702 + $(ProjectDir)../Libs/ + $(ProjectDir)../Output/ + false + false + + + + false + None + true + prompt + 4 + true + false + + + + DEBUG + + + + + + + + + $(LibsPath)Mono.Cecil.dll + + + + + + + + + diff --git a/AquaMai/AquaMai.Build/GenerateExampleConfig.cs b/AquaMai/AquaMai.Build/GenerateExampleConfig.cs new file mode 100644 index 00000000..4e7b0028 --- /dev/null +++ b/AquaMai/AquaMai.Build/GenerateExampleConfig.cs @@ -0,0 +1,42 @@ +using System; +using System.IO; +using AquaMai.Config.Interfaces; +using AquaMai.Config.HeadlessLoader; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +public class GenerateExampleConfig : Task +{ + [Required] + public string DllPath { get; set; } + + [Required] + public string OutputPath { get; set; } + + public override bool Execute() + { + try + { + var configInterface = HeadlessConfigLoader.LoadFromPacked(DllPath); + var config = configInterface.CreateConfig(); + foreach (var lang in (string[]) ["en", "zh"]) + { + var configSerializer = configInterface.CreateConfigSerializer(new IConfigSerializer.Options() + { + Lang = lang, + IncludeBanner = true, + OverrideLocaleValue = true + }); + var example = configSerializer.Serialize(config); + File.WriteAllText(Path.Combine(OutputPath, $"AquaMai.{lang}.toml"), example); + } + + return true; + } + catch (Exception e) + { + Log.LogErrorFromException(e, true); + return false; + } + } +} diff --git a/AquaMai/AquaMai.Build/PostBuildPatch.cs b/AquaMai/AquaMai.Build/PostBuildPatch.cs new file mode 100644 index 00000000..f2bb2a0a --- /dev/null +++ b/AquaMai/AquaMai.Build/PostBuildPatch.cs @@ -0,0 +1,52 @@ +using System; +using System.IO; +using System.IO.Compression; +using System.Linq; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Mono.Cecil; + +public class PostBuildPatch : Task +{ + [Required] + public string DllPath { get; set; } + + public override bool Execute() + { + try + { + var assembly = AssemblyDefinition.ReadAssembly(new MemoryStream(File.ReadAllBytes(DllPath))); + CompressEmbeddedAssemblies(assembly); + var outputStream = new MemoryStream(); + assembly.Write(outputStream); + File.WriteAllBytes(DllPath, outputStream.ToArray()); + return true; + } + catch (Exception e) + { + Log.LogErrorFromException(e, true); + return false; + } + } + + private void CompressEmbeddedAssemblies(AssemblyDefinition assembly) + { + foreach (var resource in assembly.MainModule.Resources.ToList()) + { + if (resource.Name.EndsWith(".dll") && resource is EmbeddedResource embeddedResource) + { + using var compressedStream = new MemoryStream(); + using (var deflateStream = new DeflateStream(compressedStream, CompressionLevel.Optimal)) + { + embeddedResource.GetResourceStream().CopyTo(deflateStream); + } + var compressedBytes = compressedStream.ToArray(); + + Log.LogMessage($"Compressed {resource.Name} from {embeddedResource.GetResourceStream().Length} to {compressedBytes.Length} bytes"); + + assembly.MainModule.Resources.Remove(resource); + assembly.MainModule.Resources.Add(new EmbeddedResource(resource.Name + ".compressed", resource.Attributes, compressedBytes)); + } + } + } +} diff --git a/AquaMai/AquaMai.Config.HeadlessLoader/AquaMai.Config.HeadlessLoader.csproj b/AquaMai/AquaMai.Config.HeadlessLoader/AquaMai.Config.HeadlessLoader.csproj new file mode 100644 index 00000000..e23c3ba3 --- /dev/null +++ b/AquaMai/AquaMai.Config.HeadlessLoader/AquaMai.Config.HeadlessLoader.csproj @@ -0,0 +1,45 @@ + + + + Release + AnyCPU + {6B5E1F3E-D012-4CFB-A2FA-26A6CE06BE66} + Library + AquaMai.Config.HeadlessLoader + AquaMai.Config.HeadlessLoader + netstandard2.0 + 512 + true + 12 + 414;NU1702 + $(ProjectDir)../Libs/ + $(ProjectDir)../Output/ + false + false + + + + false + None + true + prompt + 4 + true + false + + + + DEBUG + + + + + + + + + $(LibsPath)Mono.Cecil.dll + + + + diff --git a/AquaMai/AquaMai.Config.HeadlessLoader/ConfigAssemblyLoader.cs b/AquaMai/AquaMai.Config.HeadlessLoader/ConfigAssemblyLoader.cs new file mode 100644 index 00000000..22e1f96f --- /dev/null +++ b/AquaMai/AquaMai.Config.HeadlessLoader/ConfigAssemblyLoader.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using Mono.Cecil; + +namespace AquaMai.Config.HeadlessLoader; + +class ConfigAssemblyLoader +{ + public static Assembly LoadConfigAssembly(AssemblyDefinition assembly) + { + var references = assembly.MainModule.AssemblyReferences; + foreach (var reference in references) + { + if (reference.Name == "mscorlib" || reference.Name == "System" || reference.Name.StartsWith("System.")) + { + reference.Name = "netstandard"; + reference.Version = new Version(2, 0, 0, 0); + reference.PublicKeyToken = null; + } + } + + var targetFrameworkAttribute = assembly.CustomAttributes.FirstOrDefault(attr => attr.AttributeType.Name == "TargetFrameworkAttribute"); + if (targetFrameworkAttribute != null) + { + targetFrameworkAttribute.ConstructorArguments.Clear(); + targetFrameworkAttribute.ConstructorArguments.Add(new CustomAttributeArgument( + assembly.MainModule.TypeSystem.String, ".NETStandard,Version=v2.0")); + targetFrameworkAttribute.Properties.Clear(); + targetFrameworkAttribute.Properties.Add(new Mono.Cecil.CustomAttributeNamedArgument( + "FrameworkDisplayName", new CustomAttributeArgument(assembly.MainModule.TypeSystem.String, ".NET Standard 2.0"))); + } + + var stream = new MemoryStream(); + assembly.Write(stream); + FixLoadedAssemblyResolution(); + return AppDomain.CurrentDomain.Load(stream.ToArray()); + } + + private static bool FixedLoadedAssemblyResolution = false; + + // XXX: Why, without this, the already loaded assemblies are not resolved? + public static void FixLoadedAssemblyResolution() + { + if (FixedLoadedAssemblyResolution) + { + return; + } + FixedLoadedAssemblyResolution = true; + + var loadedAssemblies = new Dictionary(); + foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) + { + loadedAssemblies[assembly.FullName] = assembly; + } + + AppDomain.CurrentDomain.AssemblyResolve += (sender, args) => + { + if (loadedAssemblies.TryGetValue(args.Name, out var assembly)) + { + return assembly; + } + return null; + }; + } +} diff --git a/AquaMai/AquaMai.Config.HeadlessLoader/CustomAssemblyResolver.cs b/AquaMai/AquaMai.Config.HeadlessLoader/CustomAssemblyResolver.cs new file mode 100644 index 00000000..4bf96750 --- /dev/null +++ b/AquaMai/AquaMai.Config.HeadlessLoader/CustomAssemblyResolver.cs @@ -0,0 +1,11 @@ +using Mono.Cecil; + +namespace AquaMai.Config.HeadlessLoader; + +public class CustomAssemblyResolver : DefaultAssemblyResolver +{ + public new void RegisterAssembly(AssemblyDefinition assembly) + { + base.RegisterAssembly(assembly); + } +} diff --git a/AquaMai/AquaMai.Config.HeadlessLoader/HeadlessConfigInterface.cs b/AquaMai/AquaMai.Config.HeadlessLoader/HeadlessConfigInterface.cs new file mode 100644 index 00000000..ba7797d7 --- /dev/null +++ b/AquaMai/AquaMai.Config.HeadlessLoader/HeadlessConfigInterface.cs @@ -0,0 +1,59 @@ +using System; +using System.Reflection; +using AquaMai.Config.Interfaces; +using Mono.Cecil; + +namespace AquaMai.Config.HeadlessLoader; + +public class HeadlessConfigInterface +{ + private readonly Assembly loadedConfigAssembly; + + public IReflectionProvider ReflectionProvider { get; init; } + public IReflectionManager ReflectionManager { get; init; } + + public HeadlessConfigInterface(Assembly loadedConfigAssembly, AssemblyDefinition modsAssembly) + { + this.loadedConfigAssembly = loadedConfigAssembly; + + ReflectionProvider = Activator.CreateInstance( + loadedConfigAssembly.GetType("AquaMai.Config.Reflection.MonoCecilReflectionProvider"), [modsAssembly]) as IReflectionProvider; + ReflectionManager = Activator.CreateInstance( + loadedConfigAssembly.GetType("AquaMai.Config.Reflection.ReflectionManager"), [ReflectionProvider]) as IReflectionManager; + } + + public IConfigView CreateConfigView(string tomlString = null) + { + return Activator.CreateInstance( + loadedConfigAssembly.GetType("AquaMai.Config.ConfigView"), + tomlString == null ? [] : [tomlString]) as IConfigView; + } + + public IConfig CreateConfig() + { + return Activator.CreateInstance( + loadedConfigAssembly.GetType("AquaMai.Config.Config"), [ReflectionManager]) as IConfig; + } + + public IConfigParser GetConfigParser() + { + return loadedConfigAssembly + .GetType("AquaMai.Config.ConfigParser") + .GetField("Instance", BindingFlags.Public | BindingFlags.Static) + .GetValue(null) as IConfigParser; + } + + public IConfigSerializer CreateConfigSerializer(IConfigSerializer.Options options) + { + return Activator.CreateInstance( + loadedConfigAssembly.GetType("AquaMai.Config.ConfigSerializer"), [options]) as IConfigSerializer; + } + + public IConfigMigrationManager GetConfigMigrationManager() + { + return loadedConfigAssembly + .GetType("AquaMai.Config.Migration.ConfigMigrationManager") + .GetField("Instance", BindingFlags.Public | BindingFlags.Static) + .GetValue(null) as IConfigMigrationManager; + } +} diff --git a/AquaMai/AquaMai.Config.HeadlessLoader/HeadlessConfigLoader.cs b/AquaMai/AquaMai.Config.HeadlessLoader/HeadlessConfigLoader.cs new file mode 100644 index 00000000..052b5565 --- /dev/null +++ b/AquaMai/AquaMai.Config.HeadlessLoader/HeadlessConfigLoader.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Mono.Cecil; + +namespace AquaMai.Config.HeadlessLoader; + +public class HeadlessConfigLoader +{ + public static HeadlessConfigInterface LoadFromPacked(string fileName) + => LoadFromPacked(new FileStream(fileName, FileMode.Open)); + + public static HeadlessConfigInterface LoadFromPacked(byte[] assemblyBinary) + => LoadFromPacked(new MemoryStream(assemblyBinary)); + + public static HeadlessConfigInterface LoadFromPacked(Stream assemblyStream) + => LoadFromPacked(AssemblyDefinition.ReadAssembly(assemblyStream)); + + public static HeadlessConfigInterface LoadFromPacked(AssemblyDefinition assembly) + { + return LoadFromUnpacked( + ResourceLoader.LoadEmbeddedAssemblies(assembly).Values); + } + + public static HeadlessConfigInterface LoadFromUnpacked(IEnumerable assemblyBinariess) => + LoadFromUnpacked(assemblyBinariess.Select(binary => new MemoryStream(binary))); + + public static HeadlessConfigInterface LoadFromUnpacked(IEnumerable assemblyStreams) + { + var resolver = new CustomAssemblyResolver(); + var assemblies = assemblyStreams + .Select( + assemblyStream => + AssemblyDefinition.ReadAssembly( + assemblyStream, + new ReaderParameters() { + AssemblyResolver = resolver + })) + .ToArray(); + foreach (var assembly in assemblies) + { + resolver.RegisterAssembly(assembly); + } + + var configAssembly = assemblies.First(assembly => assembly.Name.Name == "AquaMai.Config"); + if (configAssembly == null) + { + throw new InvalidOperationException("AquaMai.Config assembly not found"); + } + var loadedConfigAssembly = ConfigAssemblyLoader.LoadConfigAssembly(configAssembly); + var modsAssembly = assemblies.First(assembly => assembly.Name.Name == "AquaMai.Mods"); + if (modsAssembly == null) + { + throw new InvalidOperationException("AquaMai.Mods assembly not found"); + } + return new(loadedConfigAssembly, modsAssembly); + } +} diff --git a/AquaMai/AquaMai.Config.HeadlessLoader/Polyfills.cs b/AquaMai/AquaMai.Config.HeadlessLoader/Polyfills.cs new file mode 100644 index 00000000..eb2da113 --- /dev/null +++ b/AquaMai/AquaMai.Config.HeadlessLoader/Polyfills.cs @@ -0,0 +1,4 @@ +namespace System.Runtime.CompilerServices +{ + internal static class IsExternalInit {} +} diff --git a/AquaMai/AquaMai.Config.HeadlessLoader/ResourceLoader.cs b/AquaMai/AquaMai.Config.HeadlessLoader/ResourceLoader.cs new file mode 100644 index 00000000..e57a1357 --- /dev/null +++ b/AquaMai/AquaMai.Config.HeadlessLoader/ResourceLoader.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using Mono.Cecil; + +namespace AquaMai.Config.HeadlessLoader; + +public class ResourceLoader +{ + private const string DLL_SUFFIX = ".dll"; + private const string COMPRESSED_SUFFIX = ".compressed"; + private const string DLL_COMPRESSED_SUFFIX = $"{DLL_SUFFIX}{COMPRESSED_SUFFIX}"; + + public static Dictionary LoadEmbeddedAssemblies(AssemblyDefinition assembly) + { + return assembly.MainModule.Resources + .Where(resource => resource.Name.ToLower().EndsWith(DLL_SUFFIX) || resource.Name.ToLower().EndsWith(DLL_COMPRESSED_SUFFIX)) + .Select(LoadResource) + .Where(data => data.Name != null) + .ToDictionary(data => data.Name, data => data.Stream); + } + + public static (string Name, Stream Stream) LoadResource(Resource resource) + { + if (resource is EmbeddedResource embeddedResource) + { + if (resource.Name.ToLower().EndsWith(COMPRESSED_SUFFIX)) + { + var decompressedStream = new MemoryStream(); + using (var deflateStream = new DeflateStream(embeddedResource.GetResourceStream(), CompressionMode.Decompress)) + { + deflateStream.CopyTo(decompressedStream); + } + decompressedStream.Position = 0; + return (resource.Name.Substring(0, resource.Name.Length - COMPRESSED_SUFFIX.Length), decompressedStream); + } + return (resource.Name, embeddedResource.GetResourceStream()); + } + return (null, null); + } +} diff --git a/AquaMai/AquaMai.Config.Interfaces/AquaMai.Config.Interfaces.csproj b/AquaMai/AquaMai.Config.Interfaces/AquaMai.Config.Interfaces.csproj new file mode 100644 index 00000000..95c911c2 --- /dev/null +++ b/AquaMai/AquaMai.Config.Interfaces/AquaMai.Config.Interfaces.csproj @@ -0,0 +1,40 @@ + + + + Release + AnyCPU + {DF1536F9-3B06-4463-B654-4CC3E708B610} + Library + AquaMai.Config.Interfaces + AquaMai.Config.Interfaces + net472 + 512 + true + 12 + 414 + $(ProjectDir)../Libs/;$(AssemblySearchPaths) + $(ProjectDir)../Output/ + false + false + + + + false + None + true + prompt + 4 + true + false + + + + DEBUG + + + + + + + + diff --git a/AquaMai/AquaMai.Config.Interfaces/IConfig.cs b/AquaMai/AquaMai.Config.Interfaces/IConfig.cs new file mode 100644 index 00000000..4b3859d7 --- /dev/null +++ b/AquaMai/AquaMai.Config.Interfaces/IConfig.cs @@ -0,0 +1,28 @@ +using System; + +namespace AquaMai.Config.Interfaces; + +public interface IConfig +{ + public interface IEntryState + { + public bool IsDefault { get; set; } + public object DefaultValue { get; } + public object Value { get; set; } + } + + public interface ISectionState + { + public bool IsDefault { get; set; } + public bool DefaultEnabled { get; } + public bool Enabled { get; set; } + } + + public IReflectionManager ReflectionManager { get; } + + public ISectionState GetSectionState(IReflectionManager.ISection section); + public ISectionState GetSectionState(Type type); + public void SetSectionEnabled(IReflectionManager.ISection section, bool enabled); + public IEntryState GetEntryState(IReflectionManager.IEntry entry); + public void SetEntryValue(IReflectionManager.IEntry entry, object value); +} diff --git a/AquaMai/AquaMai.Config.Interfaces/IConfigMigrationManager.cs b/AquaMai/AquaMai.Config.Interfaces/IConfigMigrationManager.cs new file mode 100644 index 00000000..aef08812 --- /dev/null +++ b/AquaMai/AquaMai.Config.Interfaces/IConfigMigrationManager.cs @@ -0,0 +1,7 @@ +namespace AquaMai.Config.Interfaces; + +public interface IConfigMigrationManager +{ + public IConfigView Migrate(IConfigView config); + public string GetVersion(IConfigView config); +} diff --git a/AquaMai/AquaMai.Config.Interfaces/IConfigParser.cs b/AquaMai/AquaMai.Config.Interfaces/IConfigParser.cs new file mode 100644 index 00000000..5dda2d83 --- /dev/null +++ b/AquaMai/AquaMai.Config.Interfaces/IConfigParser.cs @@ -0,0 +1,7 @@ +namespace AquaMai.Config.Interfaces; + +public interface IConfigParser +{ + public void Parse(IConfig config, string tomlString); + public void Parse(IConfig config, IConfigView configView); +} diff --git a/AquaMai/AquaMai.Config.Interfaces/IConfigSerializer.cs b/AquaMai/AquaMai.Config.Interfaces/IConfigSerializer.cs new file mode 100644 index 00000000..e1b8fe83 --- /dev/null +++ b/AquaMai/AquaMai.Config.Interfaces/IConfigSerializer.cs @@ -0,0 +1,13 @@ +namespace AquaMai.Config.Interfaces; + +public interface IConfigSerializer +{ + public record Options + { + public string Lang { get; init; } + public bool IncludeBanner { get; init; } + public bool OverrideLocaleValue { get; init; } + } + + public string Serialize(IConfig config); +} diff --git a/AquaMai/AquaMai.Config.Interfaces/IConfigView.cs b/AquaMai/AquaMai.Config.Interfaces/IConfigView.cs new file mode 100644 index 00000000..c1fe089b --- /dev/null +++ b/AquaMai/AquaMai.Config.Interfaces/IConfigView.cs @@ -0,0 +1,9 @@ +namespace AquaMai.Config.Interfaces; + +public interface IConfigView +{ + public void SetValue(string path, object value); + public T GetValueOrDefault(string path, T defaultValue = default); + public bool TryGetValue(string path, out T resultValue); + public string ToToml(); +} diff --git a/AquaMai/AquaMai.Config.Interfaces/IReflectionManager.cs b/AquaMai/AquaMai.Config.Interfaces/IReflectionManager.cs new file mode 100644 index 00000000..cc76c3bd --- /dev/null +++ b/AquaMai/AquaMai.Config.Interfaces/IReflectionManager.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; +using System; + +namespace AquaMai.Config.Interfaces; + +public interface IReflectionManager +{ + public interface IEntry + { + public string Path { get; } + public string Name { get; } + public IReflectionField Field { get; } + } + + public interface ISection + { + public string Path { get; } + public IReflectionType Type { get; } + public List Entries { get; } + } + + public IEnumerable Sections { get; } + + public IEnumerable Entries { get; } + + public bool ContainsSection(string path); + + public bool TryGetSection(string path, out ISection section); + + public bool TryGetSection(Type type, out ISection section); + + public ISection GetSection(string path); + + public ISection GetSection(Type type); + + public bool ContainsEntry(string path); + + public bool TryGetEntry(string path, out IEntry entry); + + public IEntry GetEntry(string path); +} diff --git a/AquaMai/AquaMai.Config.Interfaces/IReflectionProvider.cs b/AquaMai/AquaMai.Config.Interfaces/IReflectionProvider.cs new file mode 100644 index 00000000..19f7fc10 --- /dev/null +++ b/AquaMai/AquaMai.Config.Interfaces/IReflectionProvider.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace AquaMai.Config.Interfaces; + +public interface IReflectionField +{ + public string Name { get; } + public Type FieldType { get; } + + public T GetCustomAttribute() where T : Attribute; + public object GetValue(object objIsNull); + public void SetValue(object objIsNull, object value); +} + +public interface IReflectionType +{ + public string FullName { get; } + public string Namespace { get; } + + public T GetCustomAttribute() where T : Attribute; + public IReflectionField[] GetFields(BindingFlags bindingAttr); +} + +public interface IReflectionProvider +{ + public IReflectionType[] GetTypes(); + public Dictionary GetEnum(string enumName); +} diff --git a/AquaMai/AquaMai.Config.Interfaces/Polyfills.cs b/AquaMai/AquaMai.Config.Interfaces/Polyfills.cs new file mode 100644 index 00000000..eb2da113 --- /dev/null +++ b/AquaMai/AquaMai.Config.Interfaces/Polyfills.cs @@ -0,0 +1,4 @@ +namespace System.Runtime.CompilerServices +{ + internal static class IsExternalInit {} +} diff --git a/AquaMai/AquaMai.Config/AquaMai.Config.csproj b/AquaMai/AquaMai.Config/AquaMai.Config.csproj new file mode 100644 index 00000000..d80f92bd --- /dev/null +++ b/AquaMai/AquaMai.Config/AquaMai.Config.csproj @@ -0,0 +1,60 @@ + + + + Release + AnyCPU + {DF1536F9-3B06-4463-B654-4CC3E708B610} + Library + AquaMai.Config + AquaMai.Config + net472 + 512 + true + 12 + 414 + $(ProjectDir)../Libs/;$(AssemblySearchPaths) + $(ProjectDir)../Output/ + false + false + + + + false + None + true + prompt + 4 + true + false + + + + DEBUG + + + + + + + + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + all + + + + + diff --git a/AquaMai/AquaMai.Config/Attributes/ConfigCollapseNamespaceAttribute.cs b/AquaMai/AquaMai.Config/Attributes/ConfigCollapseNamespaceAttribute.cs new file mode 100644 index 00000000..dab14350 --- /dev/null +++ b/AquaMai/AquaMai.Config/Attributes/ConfigCollapseNamespaceAttribute.cs @@ -0,0 +1,9 @@ +using System; + +namespace AquaMai.Config.Attributes; + +// When The most inner namespace is the same name of the class, it should be collapsed. +// The class must be the only class in the namespace with a [ConfigSection] attribute. +[AttributeUsage(AttributeTargets.Class)] +public class ConfigCollapseNamespaceAttribute : Attribute +{} diff --git a/AquaMai/AquaMai.Config/Attributes/ConfigComment.cs b/AquaMai/AquaMai.Config/Attributes/ConfigComment.cs new file mode 100644 index 00000000..414b4a00 --- /dev/null +++ b/AquaMai/AquaMai.Config/Attributes/ConfigComment.cs @@ -0,0 +1,13 @@ +using System; + +namespace AquaMai.Config.Attributes; + +public record ConfigComment(string CommentEn, string CommentZh) +{ + public string GetLocalized(string lang) => lang switch + { + "en" => CommentEn ?? "", + "zh" => CommentZh ?? "", + _ => throw new ArgumentException($"Unsupported language: {lang}") + }; +} diff --git a/AquaMai/AquaMai.Config/Attributes/ConfigEntryAttribute.cs b/AquaMai/AquaMai.Config/Attributes/ConfigEntryAttribute.cs new file mode 100644 index 00000000..05dc5e90 --- /dev/null +++ b/AquaMai/AquaMai.Config/Attributes/ConfigEntryAttribute.cs @@ -0,0 +1,24 @@ +using System; + +namespace AquaMai.Config.Attributes; + +public enum SpecialConfigEntry +{ + None, + Locale +} + +[AttributeUsage(AttributeTargets.Field)] +public class ConfigEntryAttribute( + string en = null, + string zh = null, + // NOTE: Don't use this argument to hide any useful options. + // Only use it to hide options that really won't be used. + bool hideWhenDefault = false, + // NOTE: Use this argument to mark special config entries that need special handling. + SpecialConfigEntry specialConfigEntry = SpecialConfigEntry.None) : Attribute +{ + public ConfigComment Comment { get; } = new ConfigComment(en, zh); + public bool HideWhenDefault { get; } = hideWhenDefault; + public SpecialConfigEntry SpecialConfigEntry { get; } = specialConfigEntry; +} diff --git a/AquaMai/AquaMai.Config/Attributes/ConfigSectionAttribute.cs b/AquaMai/AquaMai.Config/Attributes/ConfigSectionAttribute.cs new file mode 100644 index 00000000..34ccd639 --- /dev/null +++ b/AquaMai/AquaMai.Config/Attributes/ConfigSectionAttribute.cs @@ -0,0 +1,21 @@ +using System; + +namespace AquaMai.Config.Attributes; + +[AttributeUsage(AttributeTargets.Class)] +public class ConfigSectionAttribute( + string en = null, + string zh = null, + // It will be hidden if the default value is preserved. + bool exampleHidden = false, + // A "Disabled = true" entry is required to disable the section. + bool defaultOn = false, + // NOTE: You probably shouldn't use this. Only the "General" section is using this. + // Implies defaultOn = true. + bool alwaysEnabled = false) : Attribute +{ + public ConfigComment Comment { get; } = new ConfigComment(en, zh); + public bool ExampleHidden { get; } = exampleHidden; + public bool DefaultOn { get; } = defaultOn || alwaysEnabled; + public bool AlwaysEnabled { get; } = alwaysEnabled; +} diff --git a/AquaMai/AquaMai.Config/Attributes/EnableCondition.cs b/AquaMai/AquaMai.Config/Attributes/EnableCondition.cs new file mode 100644 index 00000000..2a1e8d99 --- /dev/null +++ b/AquaMai/AquaMai.Config/Attributes/EnableCondition.cs @@ -0,0 +1,78 @@ +using System; + +namespace AquaMai.Config.Attributes; + +public enum EnableConditionOperator +{ + Equal, + NotEqual, + GreaterThan, + LessThan, + GreaterThanOrEqual, + LessThanOrEqual +} + +public class EnableCondition( + Type referenceType, + string referenceMember, + EnableConditionOperator @operator, + object rightSideValue) : Attribute +{ + public Type ReferenceType { get; } = referenceType; + public string ReferenceMember { get; } = referenceMember; + public EnableConditionOperator Operator { get; } = @operator; + public object RightSideValue { get; } = rightSideValue; + + // Referencing a field in another class and checking if it's true. + public EnableCondition(Type referenceType, string referenceMember) + : this(referenceType, referenceMember, EnableConditionOperator.Equal, true) + { } + + // Referencing a field in the same class and comparing it with a value. + public EnableCondition(string referenceMember, EnableConditionOperator condition, object value) + : this(null, referenceMember, condition, value) + { } + + // Referencing a field in the same class and checking if it's true. + public EnableCondition(string referenceMember) + : this(referenceMember, EnableConditionOperator.Equal, true) + { } + + public bool Evaluate(Type selfType) + { + var referenceType = ReferenceType ?? selfType; + var referenceField = referenceType.GetField( + ReferenceMember, + System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic); + var referenceProperty = referenceType.GetProperty( + ReferenceMember, + System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic); + if (referenceField == null && referenceProperty == null) + { + throw new ArgumentException($"Field or property {ReferenceMember} not found in {referenceType.FullName}"); + } + var referenceMemberValue = referenceField != null ? referenceField.GetValue(null) : referenceProperty.GetValue(null); + switch (Operator) + { + case EnableConditionOperator.Equal: + return referenceMemberValue.Equals(RightSideValue); + case EnableConditionOperator.NotEqual: + return !referenceMemberValue.Equals(RightSideValue); + case EnableConditionOperator.GreaterThan: + case EnableConditionOperator.LessThan: + case EnableConditionOperator.GreaterThanOrEqual: + case EnableConditionOperator.LessThanOrEqual: + var comparison = (IComparable)referenceMemberValue; + return Operator switch + { + EnableConditionOperator.GreaterThan => comparison.CompareTo(RightSideValue) > 0, + EnableConditionOperator.LessThan => comparison.CompareTo(RightSideValue) < 0, + EnableConditionOperator.GreaterThanOrEqual => comparison.CompareTo(RightSideValue) >= 0, + EnableConditionOperator.LessThanOrEqual => comparison.CompareTo(RightSideValue) <= 0, + _ => throw new NotImplementedException(), + }; + default: + throw new NotImplementedException(); + } + } +} diff --git a/AquaMai/AquaMai.Config/Config.cs b/AquaMai/AquaMai.Config/Config.cs new file mode 100644 index 00000000..fdbf67eb --- /dev/null +++ b/AquaMai/AquaMai.Config/Config.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using AquaMai.Config.Interfaces; +using AquaMai.Config.Reflection; + +namespace AquaMai.Config; + +public class Config : IConfig +{ + // NOTE: If a section's state is default, all underlying entries' states are default as well. + + public record SectionState : IConfig.ISectionState + { + public bool IsDefault { get; set; } + public bool DefaultEnabled { get; init; } + public bool Enabled { get; set; } + } + + public record EntryState : IConfig.IEntryState + { + public bool IsDefault { get; set; } + public object DefaultValue { get; init; } + public object Value { get; set; } + } + + private readonly Dictionary sections = new(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary entries = new(StringComparer.OrdinalIgnoreCase); + + public readonly ReflectionManager reflectionManager; + public IReflectionManager ReflectionManager => reflectionManager; + + public Config(ReflectionManager reflectionManager) + { + this.reflectionManager = reflectionManager; + + foreach (var section in reflectionManager.SectionValues) + { + InitializeSection(section); + } + } + + private void InitializeSection(ReflectionManager.Section section) + { + sections.Add(section.Path, new SectionState() + { + IsDefault = true, + DefaultEnabled = section.Attribute.DefaultOn, + Enabled = section.Attribute.DefaultOn + }); + + foreach (var entry in section.Entries) + { + var defaultValue = entry.Field.GetValue(null); + if (defaultValue == null) + { + throw new InvalidOperationException($"Null default value for entry {entry.Path} is not allowed."); + } + entries.Add(entry.Path, new EntryState() + { + IsDefault = true, + DefaultValue = defaultValue, + Value = defaultValue + }); + } + } + + public IConfig.ISectionState GetSectionState(IReflectionManager.ISection section) + { + return sections[section.Path]; + } + + public IConfig.ISectionState GetSectionState(Type type) + { + if (!ReflectionManager.TryGetSection(type, out var section)) + { + throw new ArgumentException($"Type {type.FullName} is not a config section."); + } + return sections[section.Path]; + } + + public void SetSectionEnabled(IReflectionManager.ISection section, bool enabled) + { + sections[section.Path].IsDefault = false; + sections[section.Path].Enabled = enabled; + } + + public IConfig.IEntryState GetEntryState(IReflectionManager.IEntry entry) + { + return entries[entry.Path]; + } + + public void SetEntryValue(IReflectionManager.IEntry entry, object value) + { + entry.Field.SetValue(null, value); + entries[entry.Path].IsDefault = false; + entries[entry.Path].Value = value; + } +} diff --git a/AquaMai/AquaMai.Config/ConfigParser.cs b/AquaMai/AquaMai.Config/ConfigParser.cs new file mode 100644 index 00000000..e84d8cdb --- /dev/null +++ b/AquaMai/AquaMai.Config/ConfigParser.cs @@ -0,0 +1,125 @@ +using System; +using Tomlet.Models; +using AquaMai.Config.Interfaces; +using AquaMai.Config.Reflection; +using AquaMai.Config.Migration; +using System.Linq; + +namespace AquaMai.Config; + +public class ConfigParser : IConfigParser +{ + public readonly static ConfigParser Instance = new(); + + private readonly static string[] supressUnrecognizedConfigPaths = ["Version"]; + private readonly static string[] supressUnrecognizedConfigPathSuffixes = [ + ".Disabled", // For section enable state. + ".Disable", // For section enable state, but the wrong key, warn later. + ".Enabled", // For section enable state, but the wrong key, warn later. + ".Enable", // For section enable state, but the wrong key, warn later. + ]; + + private ConfigParser() + {} + + public void Parse(IConfig config, string tomlString) + { + var configView = new ConfigView(tomlString); + Parse(config, configView); + } + + public void Parse(IConfig config, IConfigView configView) + { + var configVersion = ConfigMigrationManager.Instance.GetVersion(configView); + if (configVersion != ConfigMigrationManager.Instance.latestVersion) + { + throw new InvalidOperationException($"Config version mismatch: expected {ConfigMigrationManager.Instance.latestVersion}, got {configVersion}"); + } + Hydrate((Config)config, ((ConfigView)configView).root, ""); + } + + private static void Hydrate(Config config, TomlValue value, string path) + { + if (config.ReflectionManager.TryGetSection(path, out var section)) + { + ParseSectionEnableState(config, (ReflectionManager.Section)section, value, path); + } + + if (value is TomlTable table) + { + bool isLeaf = true; + foreach (var subKey in table.Keys) + { + var subValue = table.GetValue(subKey); + var subPath = path == "" ? subKey : $"{path}.{subKey}"; + if (subValue is TomlTable) + { + isLeaf = false; + } + Hydrate(config, subValue, subPath); + } + // A leaf dictionary, which has no child dictionaries, must be a section. + if (isLeaf && section == null) + { + Utility.Log($"Unrecognized config section: {path}"); + } + } + else + { + // It's an config entry value (or a primitive type for enabling a section). + if (!config.ReflectionManager.ContainsSection(path) && + !config.ReflectionManager.ContainsEntry(path) && + !supressUnrecognizedConfigPaths.Any(s => path.Equals(s, StringComparison.OrdinalIgnoreCase)) && + !supressUnrecognizedConfigPathSuffixes.Any(suffix => path.EndsWith(suffix, StringComparison.OrdinalIgnoreCase))) + { + Utility.Log($"Unrecognized config entry: {path}"); + return; + } + + if (config.ReflectionManager.TryGetEntry(path, out var entry)) + { + try + { + var parsedValue = Utility.ParseTomlValue(entry.Field.FieldType, value); + config.SetEntryValue(entry, parsedValue); + } + catch (Exception e) + { + Utility.Log($"Error hydrating config ({path} = {value.StringValue}): {e.Message}"); + } + } + } + } + + public static void ParseSectionEnableState( + Config config, + ReflectionManager.Section section, + TomlValue value, + string path) + { + if (value is TomlTable table) + { + foreach (var unexpectedKey in (string[]) ["Enable", "Enabled", "Disable"]) + { + if (Utility.TomlContainsKeyCaseInsensitive(table, unexpectedKey)) + { + Utility.Log($"Unexpected key \"{unexpectedKey}\" for enable status under \"{path}\". Only \"Disabled\" is parsed."); + } + } + + if (Utility.TomlTryGetValueCaseInsensitive(table, "Disabled", out var disableValue) && !section.Attribute.AlwaysEnabled) + { + var disabled = Utility.IsTruty(disableValue, path + ".Disabled"); + config.SetSectionEnabled(section, !disabled); + } + else + { + config.SetSectionEnabled(section, true); + } + } + else + { + config.SetSectionEnabled(section, Utility.IsTruty(value, path)); + } + } +} diff --git a/AquaMai/AquaMai.Config/ConfigSerializer.cs b/AquaMai/AquaMai.Config/ConfigSerializer.cs new file mode 100644 index 00000000..dd4c59ed --- /dev/null +++ b/AquaMai/AquaMai.Config/ConfigSerializer.cs @@ -0,0 +1,185 @@ +using System; +using System.Reflection; +using System.Text; +using AquaMai.Config.Attributes; +using AquaMai.Config.Interfaces; +using Tomlet.Models; + +namespace AquaMai.Config; + +public class ConfigSerializer(IConfigSerializer.Options Options) : IConfigSerializer +{ + private const string BANNER_ZH = + """ + 这是 AquaMai 的 TOML 配置文件 + + - 井号 # 开头的行为注释,被注释掉的内容不会生效 + - 为方便使用 VSCode 等编辑器进行编辑,被注释掉的配置内容使用一个井号 #,而注释文本使用两个井号 ## + - 以方括号包裹的行,如 [OptionalCategory.Section],为一个栏目 + - 将默认被注释(即默认禁用)的栏目取消注释即可启用 + - 若要禁用一个默认启用的栏目,请在栏目下添加「Disabled = true」配置项,删除它/注释它不会有效 + - 形如「键 = 值」为一个配置项 + - 配置项应用到其上方最近的栏目,请不要在一个栏目被注释掉的情况下开启其配置项(会加到上一个栏目,无效) + - 当对应栏目启用时,配置项生效,无论是否将其取消注释 + - 注释掉的配置项保留其注释中的默认值,默认值可能会随版本更新而变化 + - 该文件的格式和文字注释是固定的,配置文件将在启动时被重写,无法解析的内容将被删除 + + 试试使用 MaiChartManager 图形化配置 AquaMai 吧! + https://github.com/clansty/MaiChartManager + """; + + private const string BANNER_EN = + """ + This is the TOML configuration file of AquaMai. + + - Lines starting with a hash # are comments. Commented content will not take effect. + - For easily editing with editors (e.g. VSCode), commented configuration content uses a single hash #, while comment text uses two hashes ##. + - Lines with square brackets like [OptionalCategory.Section] are sections. + - Uncomment a section that is commented out by default (i.e. disabled by default) to enable it. + - To disable a section that is enabled by default, add a "Disable = true" entry under the section. Removing/commenting it will not work. + - Lines like "Key = Value" is a configuration entry. + - Configuration entries apply to the nearest section above them. Do not enable a configuration entry when its section is commented out (will be added to the previous section, which is invalid). + - Configuration entries take effect when the corresponding section is enabled, regardless of whether they are uncommented. + - Commented configuration entries retain their default values (shown in the comment), which may change with version updates. + - The format and text comments of this file are fixed. The configuration file will be rewritten at startup, and unrecognizable content will be deleted. + """; + + private readonly IConfigSerializer.Options Options = Options; + + public string Serialize(IConfig config) + { + StringBuilder sb = new(); + if (Options.IncludeBanner) + { + var banner = Options.Lang == "zh" ? BANNER_ZH : BANNER_EN; + if (banner != null) + { + AppendComment(sb, banner.TrimEnd()); + sb.AppendLine(); + } + } + + // Version + AppendEntry(sb, null, "Version", "2.0"); + + foreach (var section in ((Config)config).reflectionManager.SectionValues) + { + var sectionState = config.GetSectionState(section); + + // If the state is default, print the example only. If the example is hidden, skip it. + if (sectionState.IsDefault && section.Attribute.ExampleHidden) + { + continue; + } + sb.AppendLine(); + + AppendComment(sb, section.Attribute.Comment); + + if (// If the section is hidden and hidden by default, print it as commented. + sectionState.IsDefault && !sectionState.Enabled && + // If the section is marked as always enabled, print it normally. + !section.Attribute.AlwaysEnabled) + { + sb.AppendLine($"#[{section.Path}]"); + } + else // If the section is overridden, or is enabled by any means, print it normally. + { + sb.AppendLine($"[{section.Path}]"); + } + + if (!section.Attribute.AlwaysEnabled) + { + // If the section is disabled explicitly, print the "Disabled" meta entry. + if (!sectionState.IsDefault && !sectionState.Enabled) + { + AppendEntry(sb, null, "Disabled", value: true); + } + // If the section is enabled by default, print the "Disabled" meta entry as commented. + else if (sectionState.IsDefault && section.Attribute.DefaultOn) + { + AppendEntry(sb, null, "Disabled", value: false, commented: true); + } + // Otherwise, don't print the "Disabled" meta entry. + } + + // Print entries. + foreach (var entry in section.entries) + { + var entryState = config.GetEntryState(entry); + AppendComment(sb, entry.Attribute.Comment); + if (entry.Attribute.SpecialConfigEntry == SpecialConfigEntry.Locale && Options.OverrideLocaleValue) + { + AppendEntry(sb, section.Path, entry.Name, Options.Lang); + } + else + { + AppendEntry(sb, section.Path, entry.Name, entryState.Value, entryState.IsDefault); + } + } + } + + return sb.ToString(); + } + + private static string SerializeTomlValue(string diagnosticPath, object value) + { + var type = value.GetType(); + if (value is bool b) + { + return b ? "true" : "false"; + } + else if (value is string str) + { + return new TomlString(str).SerializedValue; + } + else if (type.IsEnum) + { + return new TomlString(value.ToString()).SerializedValue; + } + else if (Utility.IsIntegerType(type)) + { + return value.ToString(); + } + else if (Utility.IsFloatType(type)) + { + return new TomlDouble(Convert.ToDouble(value)).SerializedValue; + } + else + { + var currentMethod = MethodBase.GetCurrentMethod(); + throw new NotImplementedException($"Unsupported config entry type: {type.FullName} ({diagnosticPath}). Please implement in {currentMethod.DeclaringType.FullName}.{currentMethod.Name}"); + } + } + + private void AppendComment(StringBuilder sb, ConfigComment comment) + { + if (comment != null) + { + AppendComment(sb, comment.GetLocalized(Options.Lang)); + } + } + + private static void AppendComment(StringBuilder sb, string comment) + { + comment = comment.Trim(); + if (!string.IsNullOrEmpty(comment)) + { + foreach (var line in comment.Split('\n')) + { + sb.AppendLine($"## {line}"); + } + } + } + + private static void AppendEntry(StringBuilder sb, string diagnosticsSection, string key, object value, bool commented = false) + { + if (commented) + { + sb.Append('#'); + } + var diagnosticsPath = string.IsNullOrEmpty(diagnosticsSection) + ? key + : $"{diagnosticsSection}.{key}"; + sb.AppendLine($"{key} = {SerializeTomlValue(diagnosticsPath, value)}"); + } +} diff --git a/AquaMai/AquaMai.Config/ConfigView.cs b/AquaMai/AquaMai.Config/ConfigView.cs new file mode 100644 index 00000000..ff9afbdf --- /dev/null +++ b/AquaMai/AquaMai.Config/ConfigView.cs @@ -0,0 +1,105 @@ +using System; +using System.Linq; +using AquaMai.Config.Interfaces; +using Tomlet; +using Tomlet.Models; + +namespace AquaMai.Config; + +public class ConfigView : IConfigView +{ + public readonly TomlTable root; + + public ConfigView() + { + root = new TomlTable(); + } + + public ConfigView(TomlTable root) + { + this.root = root; + } + + public ConfigView(string tomlString) + { + var tomlValue = new TomlParser().Parse(tomlString); + if (tomlValue is not TomlTable tomlTable) + { + throw new ArgumentException($"Invalid TOML, expected a table, got: {tomlValue.GetType()}"); + } + root = tomlTable; + } + + public TomlTable EnsureDictionary(string path) + { + var pathComponents = path.Split('.'); + var current = root; + foreach (var component in pathComponents) + { + if (!current.TryGetValue(component, out var next)) + { + next = new TomlTable(); + current.Put(component, next); + } + current = (TomlTable)next; + } + return current; + } + + public void SetValue(string path, object value) + { + var pathComponents = path.Split('.'); + var current = root; + foreach (var component in pathComponents.Take(pathComponents.Length - 1)) + { + if (!current.TryGetValue(component, out var next)) + { + next = new TomlTable(); + current.Put(component, next); + } + current = (TomlTable)next; + } + current.Put(pathComponents.Last(), value); + } + + public T GetValueOrDefault(string path, T defaultValue = default) + { + return TryGetValue(path, out T resultValue) ? resultValue : defaultValue; + } + + public bool TryGetValue(string path, out T resultValue) + { + var pathComponents = path.Split('.'); + var current = root; + foreach (var component in pathComponents.Take(pathComponents.Length - 1)) + { + if (!Utility.TomlTryGetValueCaseInsensitive(current, component, out var next) || next is not TomlTable nextTable) + { + resultValue = default; + return false; + } + current = nextTable; + } + if (!Utility.TomlTryGetValueCaseInsensitive(current, pathComponents.Last(), out var value)) + { + resultValue = default; + return false; + } + try + { + resultValue = Utility.ParseTomlValue(value); + return true; + } + catch (Exception e) + { + Utility.Log($"Failed to parse value at {path}: {e.Message}"); + resultValue = default; + return false; + } + } + + public string ToToml() + { + return root.SerializedValue; + } +} diff --git a/AquaMai/FodyWeavers.xml b/AquaMai/AquaMai.Config/FodyWeavers.xml similarity index 62% rename from AquaMai/FodyWeavers.xml rename to AquaMai/AquaMai.Config/FodyWeavers.xml index 27cb7738..645a09e7 100644 --- a/AquaMai/FodyWeavers.xml +++ b/AquaMai/AquaMai.Config/FodyWeavers.xml @@ -1,7 +1,7 @@  - tomlet|urgui|numerics - $AquaMai$_ + tomlet + $AquaMai.Config$_ diff --git a/AquaMai/FodyWeavers.xsd b/AquaMai/AquaMai.Config/FodyWeavers.xsd similarity index 100% rename from AquaMai/FodyWeavers.xsd rename to AquaMai/AquaMai.Config/FodyWeavers.xsd diff --git a/AquaMai/AquaMai.Config/Migration/ConfigMigrationManager.cs b/AquaMai/AquaMai.Config/Migration/ConfigMigrationManager.cs new file mode 100644 index 00000000..d0be4bfd --- /dev/null +++ b/AquaMai/AquaMai.Config/Migration/ConfigMigrationManager.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using AquaMai.Config.Interfaces; + +namespace AquaMai.Config.Migration; + +public class ConfigMigrationManager : IConfigMigrationManager +{ + public static readonly ConfigMigrationManager Instance = new(); + + private readonly Dictionary migrationMap = + new List + { + new ConfigMigration_V1_0_V2_0() + }.ToDictionary(m => m.FromVersion); + + public readonly string latestVersion; + + private ConfigMigrationManager() + { + latestVersion = migrationMap.Values + .Select(m => m.ToVersion) + .OrderByDescending(version => + { + var versionParts = version.Split('.').Select(int.Parse).ToArray(); + return versionParts[0] * 100000 + versionParts[1]; + }) + .First(); + } + + public IConfigView Migrate(IConfigView config) + { + var currentVersion = GetVersion(config); + while (migrationMap.ContainsKey(currentVersion)) + { + var migration = migrationMap[currentVersion]; + Utility.Log($"Migrating config from v{migration.FromVersion} to v{migration.ToVersion}"); + config = migration.Migrate(config); + currentVersion = migration.ToVersion; + } + if (currentVersion != latestVersion) + { + throw new ArgumentException($"Could not migrate the config from v{currentVersion} to v{latestVersion}"); + } + return config; + } + + public string GetVersion(IConfigView config) + { + if (config.TryGetValue("Version", out var version)) + { + return version; + } + // Assume v1.0 if not found + return "1.0"; + } +} diff --git a/AquaMai/AquaMai.Config/Migration/ConfigMigration_V1_0_V2_0.cs b/AquaMai/AquaMai.Config/Migration/ConfigMigration_V1_0_V2_0.cs new file mode 100644 index 00000000..3b46ca0a --- /dev/null +++ b/AquaMai/AquaMai.Config/Migration/ConfigMigration_V1_0_V2_0.cs @@ -0,0 +1,346 @@ +using System; +using System.Collections.Generic; +using AquaMai.Config.Interfaces; +using AquaMai.Config.Types; + +namespace AquaMai.Config.Migration; + +public class ConfigMigration_V1_0_V2_0 : IConfigMigration +{ + public string FromVersion => "1.0"; + public string ToVersion => "2.0"; + + public IConfigView Migrate(IConfigView src) + { + var dst = new ConfigView(); + + dst.SetValue("Version", ToVersion); + + // UX (legacy) + MapBooleanTrueToSectionEnable(src, dst, "UX.TestProof", "GameSystem.TestProof"); + if (src.GetValueOrDefault("UX.QuickSkip")) + { + // NOTE: UX.QuickSkip was a 4-in-1 large patch in earlier V1, then split since ModKeyMap was introduced. + dst.SetValue("UX.OneKeyEntryEnd.Key", "Service"); + dst.SetValue("UX.OneKeyEntryEnd.LongPress", true); + dst.SetValue("UX.OneKeyRetrySkip.RetryKey", "Service"); + dst.SetValue("UX.OneKeyRetrySkip.RetryLongPress", false); + dst.SetValue("UX.OneKeyRetrySkip.SkipKey", "Select1P"); + dst.SetValue("UX.OneKeyRetrySkip.SkipLongPress", false); + dst.EnsureDictionary("GameSystem.QuickRetry"); + } + if (src.GetValueOrDefault("UX.HideSelfMadeCharts")) + { + dst.SetValue("UX.HideSelfMadeCharts.Key", "Service"); + dst.SetValue("UX.HideSelfMadeCharts.LongPress", false); + } + MapBooleanTrueToSectionEnable(src, dst, "UX.LoadJacketPng", "GameSystem.Assets.LoadLocalImages"); + MapBooleanTrueToSectionEnable(src, dst, "UX.SkipWarningScreen", "Tweaks.TimeSaving.SkipStartupWarning"); + MapBooleanTrueToSectionEnable(src, dst, "UX.SkipToMusicSelection", "Tweaks.TimeSaving.EntryToMusicSelection"); + MapBooleanTrueToSectionEnable(src, dst, "UX.SkipEventInfo", "Tweaks.TimeSaving.SkipEventInfo"); + MapBooleanTrueToSectionEnable(src, dst, "UX.SelectionDetail", "UX.SelectionDetail"); + if (src.GetValueOrDefault("UX.CustomNoteSkin") || + src.GetValueOrDefault("UX.CustomSkins")) + { + dst.SetValue("Fancy.CustomSkins.SkinsDir", "LocalAssets/Skins"); + } + MapBooleanTrueToSectionEnable(src, dst, "UX.JudgeDisplay4B", "Fancy.GamePlay.JudgeDisplay4B"); + MapBooleanTrueToSectionEnable(src, dst, "UX.CustomTrackStartDiff", "Fancy.CustomTrackStartDiff"); + MapBooleanTrueToSectionEnable(src, dst, "UX.TrackStartProcessTweak", "Fancy.GamePlay.TrackStartProcessTweak"); + MapBooleanTrueToSectionEnable(src, dst, "UX.DisableTrackStartTabs", "Fancy.GamePlay.DisableTrackStartTabs"); + MapBooleanTrueToSectionEnable(src, dst, "UX.RealisticRandomJudge", "Fancy.GamePlay.RealisticRandomJudge"); + + // Utils (legacy) + if (src.GetValueOrDefault("Utils.Windowed") || + src.GetValueOrDefault("Utils.Width") != 0 || + src.GetValueOrDefault("Utils.Height") != 0) + { + // NOTE: the default "false, 0, 0" was effective earlier in V1, but won't be migrated as enabled in V2. + MapValueOrDefaultToEntryValue(src, dst, "Utils.Windowed", "GameSystem.Window.Windowed", false); + MapValueOrDefaultToEntryValue(src, dst, "Utils.Width", "GameSystem.Window.Width", 0); + MapValueOrDefaultToEntryValue(src, dst, "Utils.Height", "GameSystem.Window.Height", 0); + } + if (src.GetValueOrDefault("Utils.PracticeMode") || src.GetValueOrDefault("Utils.PractiseMode")) // Typo of typo is the correct word + { + dst.SetValue("UX.PracticeMode.Key", "Test"); + dst.SetValue("UX.PracticeMode.LongPress", false); + } + + // Fix (legacy) + MapBooleanTrueToSectionEnable(src, dst, "Fix.SlideJudgeTweak", "Fancy.GamePlay.BreakSlideJudgeBlink"); + MapBooleanTrueToSectionEnable(src, dst, "Fix.BreakSlideJudgeBlink", "Fancy.GamePlay.BreakSlideJudgeBlink"); + MapBooleanTrueToSectionEnable(src, dst, "Fix.SlideJudgeTweak", "Fancy.GamePlay.FanJudgeFlip"); + MapBooleanTrueToSectionEnable(src, dst, "Fix.FanJudgeFlip", "Fancy.GamePlay.FanJudgeFlip"); + // NOTE: This (FixCircleSlideJudge) was enabled by default in V1, but non-default in V2 since it has visual changes + MapBooleanTrueToSectionEnable(src, dst, "Fix.SlideJudgeTweak", "Fancy.GamePlay.AlignCircleSlideJudgeDisplay"); + MapBooleanTrueToSectionEnable(src, dst, "Fix.FixCircleSlideJudge", "Fancy.GamePlay.AlignCircleSlideJudgeDisplay"); + + // Performance (legacy) + MapBooleanTrueToSectionEnable(src, dst, "Performance.ImproveLoadSpeed", "Tweaks.TimeSaving.SkipStartupDelays"); + + // TimeSaving (legacy) + MapBooleanTrueToSectionEnable(src, dst, "TimeSaving.ShowNetErrorDetail", "Utils.ShowNetErrorDetail"); + + // UX + MapValueToEntryValueIfNonNullOrDefault(src, dst, "UX.Locale", "General.Locale", ""); + MapBooleanTrueToSectionEnable(src, dst, "UX.SinglePlayer", "GameSystem.SinglePlayer"); + MapBooleanTrueToSectionEnable(src, dst, "UX.HideMask", "Fancy.HideMask"); + MapBooleanTrueToSectionEnable(src, dst, "UX.LoadAssetsPng", "GameSystem.Assets.LoadLocalImages"); + MapBooleanTrueToSectionEnable(src, dst, "UX.LoadAssetBundleWithoutManifest", "GameSystem.Assets.LoadAssetBundleWithoutManifest"); + MapBooleanTrueToSectionEnable(src, dst, "UX.RandomBgm", "Fancy.RandomBgm"); + MapBooleanTrueToSectionEnable(src, dst, "UX.DemoMaster", "Fancy.DemoMaster"); + MapBooleanTrueToSectionEnable(src, dst, "UX.ExtendTimer", "GameSystem.DisableTimeout"); + MapBooleanTrueToSectionEnable(src, dst, "UX.ImmediateSave", "UX.ImmediateSave"); + MapBooleanTrueToSectionEnable(src, dst, "UX.LoadLocalBga", "GameSystem.Assets.UseJacketAsDummyMovie"); + if (src.GetValueOrDefault("UX.CustomFont")) + { + dst.SetValue("GameSystem.Assets.Fonts.Paths", "LocalAssets/font.ttf"); + dst.SetValue("GameSystem.Assets.Fonts.AddAsFallback", false); + } + MapBooleanTrueToSectionEnable(src, dst, "UX.TouchToButtonInput", "GameSystem.TouchToButtonInput"); + MapBooleanTrueToSectionEnable(src, dst, "UX.HideHanabi", "Fancy.GamePlay.HideHanabi"); + MapBooleanTrueToSectionEnable(src, dst, "UX.SlideFadeInTweak", "Fancy.GamePlay.SlideFadeInTweak"); + MapBooleanTrueToSectionEnable(src, dst, "UX.JudgeAccuracyInfo", "UX.JudgeAccuracyInfo"); + MapValueToEntryValueIfNonNullOrDefault(src, dst, "UX.CustomVersionString", "Fancy.CustomVersionString.VersionString", ""); + MapValueToEntryValueIfNonNullOrDefault(src, dst, "UX.CustomPlaceName", "Fancy.CustomPlaceName.PlaceName", ""); + MapValueToEntryValueIfNonNullOrDefault(src, dst, "UX.ExecOnIdle", "Fancy.Triggers.ExecOnIdle", ""); + MapValueToEntryValueIfNonNullOrDefault(src, dst, "UX.ExecOnEntry", "Fancy.Triggers.ExecOnEntry", ""); + + // Cheat + var unlockTickets = src.GetValueOrDefault("Cheat.TicketUnlock"); + var unlockMaps = src.GetValueOrDefault("Cheat.MapUnlock"); + var unlockUtage = src.GetValueOrDefault("Cheat.UnlockUtage"); + if (unlockTickets || + unlockMaps || + unlockUtage) + { + dst.SetValue("GameSystem.Unlock.Tickets", unlockTickets); + dst.SetValue("GameSystem.Unlock.Maps", unlockMaps); + dst.SetValue("GameSystem.Unlock.Utage", unlockUtage); + } + + // Fix + MapBooleanTrueToSectionEnable(src, dst, "Fix.SkipVersionCheck", "Tweaks.SkipUserVersionCheck"); + if (!src.GetValueOrDefault("Fix.RemoveEncryption")) + { + dst.SetValue("GameSystem.RemoveEncryption.Disabled", true); // Enabled by default in V2 + } + MapBooleanTrueToSectionEnable(src, dst, "Fix.ForceAsServer", "GameSettings.ForceAsServer"); + if (src.GetValueOrDefault("Fix.ForceFreePlay")) + { + dst.SetValue("GameSettings.CreditConfig.IsFreePlay", true); + } + if (src.GetValueOrDefault("Fix.ForcePaidPlay")) + { + dst.SetValue("GameSettings.CreditConfig.IsFreePlay", false); + dst.SetValue("GameSettings.CreditConfig.LockCredits", 24); + } + MapValueToEntryValueIfNonNullOrDefault(src, dst, "Fix.ExtendNotesPool", "Fancy.GamePlay.ExtendNotesPool.Count", 0); + MapBooleanTrueToSectionEnable(src, dst, "Fix.FrameRateLock", "Tweaks.LockFrameRate"); + if (src.GetValueOrDefault("Font.FontFix") && + !src.GetValueOrDefault("UX.CustomFont")) + { + dst.SetValue("GameSystem.Assets.Fonts.Paths", "%SYSTEMROOT%/Fonts/msyhbd.ttc"); + dst.SetValue("GameSystem.Assets.Fonts.AddAsFallback", true); + } + MapBooleanTrueToSectionEnable(src, dst, "Fix.RealisticRandomJudge", "Fancy.GamePlay.RealisticRandomJudge"); + if (src.GetValueOrDefault("UX.SinglePlayer")) + { + if (src.TryGetValue("Fix.HanabiFix", out bool hanabiFix)) + { + // If it's enabled or disabled explicitly, use the value, otherwise left empty use the default V2 value (enabled). + dst.SetValue("GameSystem.SinglePlayer.FixHanabi", hanabiFix); + } + } + MapBooleanTrueToSectionEnable(src, dst, "Fix.IgnoreAimeServerError", "Tweaks.IgnoreAimeServerError"); + MapBooleanTrueToSectionEnable(src, dst, "Fix.TouchResetAfterTrack", "Tweaks.ResetTouchAfterTrack"); + + // Utils + MapBooleanTrueToSectionEnable(src, dst, "Utils.LogUserId", "Utils.LogUserId"); + MapValueToEntryValueIfNonNullOrDefault(src, dst, "Utils.JudgeAdjustA", "GameSettings.JudgeAdjust.A", 0); + MapValueToEntryValueIfNonNullOrDefault(src, dst, "Utils.JudgeAdjustB", "GameSettings.JudgeAdjust.B", 0); + MapValueToEntryValueIfNonNullOrDefault(src, dst, "Utils.TouchDelay", "GameSettings.JudgeAdjust.TouchDelay", 0); + MapBooleanTrueToSectionEnable(src, dst, "Utils.SelectionDetail", "UX.SelectionDetail"); + MapBooleanTrueToSectionEnable(src, dst, "Utils.ShowNetErrorDetail", "Utils.ShowNetErrorDetail"); + MapBooleanTrueToSectionEnable(src, dst, "Utils.ShowErrorLog", "Utils.ShowErrorLog"); + MapBooleanTrueToSectionEnable(src, dst, "Utils.FrameRateDisplay", "Utils.DisplayFrameRate"); + MapValueToEntryValueIfNonNullOrDefault(src, dst, "Utils.TouchPanelBaudRate", "GameSystem.TouchPanelBaudRate.BaudRate", 0); + + // TimeSaving + MapBooleanTrueToSectionEnable(src, dst, "TimeSaving.SkipWarningScreen", "Tweaks.TimeSaving.SkipStartupWarning"); + MapBooleanTrueToSectionEnable(src, dst, "TimeSaving.ImproveLoadSpeed", "Tweaks.TimeSaving.SkipStartupDelays"); + MapBooleanTrueToSectionEnable(src, dst, "TimeSaving.SkipToMusicSelection", "Tweaks.TimeSaving.EntryToMusicSelection"); + MapBooleanTrueToSectionEnable(src, dst, "TimeSaving.SkipEventInfo", "Tweaks.TimeSaving.SkipEventInfo"); + MapBooleanTrueToSectionEnable(src, dst, "TimeSaving.IWontTapOrSlideVigorously", "Tweaks.TimeSaving.IWontTapOrSlideVigorously"); + MapBooleanTrueToSectionEnable(src, dst, "TimeSaving.SkipGameOverScreen", "Tweaks.TimeSaving.SkipGoodbyeScreen"); + MapBooleanTrueToSectionEnable(src, dst, "TimeSaving.SkipTrackStart", "Tweaks.TimeSaving.SkipTrackStart"); + MapBooleanTrueToSectionEnable(src, dst, "TimeSaving.ShowQuickEndPlay", "UX.QuickEndPlay"); + + // Visual + if (src.GetValueOrDefault("Visual.CustomSkins")) + { + dst.SetValue("Fancy.CustomSkins.SkinsDir", "LocalAssets/Skins"); + } + MapBooleanTrueToSectionEnable(src, dst, "Visual.JudgeDisplay4B", "Fancy.GamePlay.JudgeDisplay4B"); + MapBooleanTrueToSectionEnable(src, dst, "Visual.CustomTrackStartDiff", "Fancy.CustomTrackStartDiff"); + MapBooleanTrueToSectionEnable(src, dst, "Visual.TrackStartProcessTweak", "Fancy.GamePlay.TrackStartProcessTweak"); + MapBooleanTrueToSectionEnable(src, dst, "Visual.DisableTrackStartTabs", "Fancy.GamePlay.DisableTrackStartTabs"); + MapBooleanTrueToSectionEnable(src, dst, "Visual.FanJudgeFlip", "Fancy.GamePlay.FanJudgeFlip"); + MapBooleanTrueToSectionEnable(src, dst, "Visual.BreakSlideJudgeBlink", "Fancy.GamePlay.BreakSlideJudgeBlink"); + MapBooleanTrueToSectionEnable(src, dst, "Visual.SlideArrowAnimation", "Fancy.GamePlay.SlideArrowAnimation"); + MapBooleanTrueToSectionEnable(src, dst, "Visual.SlideLayerReverse", "Fancy.GamePlay.SlideLayerReverse"); + + // ModKeyMap + var keyQuickSkip = src.GetValueOrDefault("ModKeyMap.QuickSkip", "None"); + var keyInGameRetry = src.GetValueOrDefault("ModKeyMap.InGameRetry", "None"); + var keyInGameSkip = src.GetValueOrDefault("ModKeyMap.InGameSkip", "None"); + var keyPractiseMode = src.GetValueOrDefault("ModKeyMap.PractiseMode", "None"); + var keyHideSelfMadeCharts = src.GetValueOrDefault("ModKeyMap.HideSelfMadeCharts", "None"); + if (keyQuickSkip != "None") + { + dst.SetValue("UX.OneKeyEntryEnd.Key", keyQuickSkip); + MapValueToEntryValueIfNonNull(src, dst, "ModKeyMap.QuickSkipLongPress", "UX.OneKeyEntryEnd.LongPress"); + } + if (keyInGameRetry != "None" || keyInGameSkip != "None") + { + dst.SetValue("UX.OneKeyRetrySkip.RetryKey", keyInGameRetry); + if (keyInGameRetry != "None") + { + MapValueToEntryValueIfNonNull(src, dst, "ModKeyMap.InGameRetryLongPress", "UX.OneKeyRetrySkip.RetryLongPress"); + } + dst.SetValue("UX.OneKeyRetrySkip.SkipKey", keyInGameSkip); + if (keyInGameSkip != "None") + { + MapValueToEntryValueIfNonNull(src, dst, "ModKeyMap.InGameSkipLongPress", "UX.OneKeyRetrySkip.SkipLongPress"); + } + } + if (keyPractiseMode != "None") + { + dst.SetValue("UX.PracticeMode.Key", keyPractiseMode); + MapValueToEntryValueIfNonNull(src, dst, "ModKeyMap.PractiseModeLongPress", "UX.PracticeMode.LongPress"); + } + if (keyHideSelfMadeCharts != "None") + { + dst.SetValue("UX.HideSelfMadeCharts.Key", keyHideSelfMadeCharts); + MapValueToEntryValueIfNonNull(src, dst, "ModKeyMap.HideSelfMadeChartsLongPress", "UX.HideSelfMadeCharts.LongPress"); + } + MapBooleanTrueToSectionEnable(src, dst, "ModKeyMap.EnableNativeQuickRetry", "GameSystem.QuickRetry"); + if (src.TryGetValue("ModKeyMap.TestMode", out var testMode) && + testMode != "" && + testMode != "Test") + { + dst.SetValue("DeprecationWarning.v1_0_ModKeyMap_TestMode", true); + } + MapBooleanTrueToSectionEnable(src, dst, "ModKeyMap.TestModeLongPress", "GameSystem.TestProof"); + + // WindowState + if (src.GetValueOrDefault("WindowState.Enable")) + { + MapValueOrDefaultToEntryValue(src, dst, "WindowState.Windowed", "GameSystem.Window.Windowed", false); + MapValueOrDefaultToEntryValue(src, dst, "WindowState.Width", "GameSystem.Window.Width", 0); + MapValueOrDefaultToEntryValue(src, dst, "WindowState.Height", "GameSystem.Window.Height", 0); + } + + // CustomCameraId + if (src.GetValueOrDefault("CustomCameraId.Enable")) + { + dst.EnsureDictionary("GameSystem.CustomCameraId"); + MapValueToEntryValueIfNonNullOrDefault(src, dst, "CustomCameraId.PrintCameraList", "GameSystem.CustomCameraId.PrintCameraList", false); + MapValueToEntryValueIfNonNullOrDefault(src, dst, "CustomCameraId.LeftQrCamera", "GameSystem.CustomCameraId.LeftQrCamera", 0); + MapValueToEntryValueIfNonNullOrDefault(src, dst, "CustomCameraId.RightQrCamera", "GameSystem.CustomCameraId.RightQrCamera", 0); + MapValueToEntryValueIfNonNullOrDefault(src, dst, "CustomCameraId.PhotoCamera", "GameSystem.CustomCameraId.PhotoCamera", 0); + MapValueToEntryValueIfNonNullOrDefault(src, dst, "CustomCameraId.ChimeCamera", "GameSystem.CustomCameraId.ChimeCamera", 0); + } + + // TouchSensitivity + if (src.GetValueOrDefault("TouchSensitivity.Enable")) + { + dst.EnsureDictionary("GameSettings.TouchSensitivity"); + var areas = new[] + { + "A1", "A2", "A3", "A4", "A5", "A6", "A7", "A8", + "B1", "B2", "B3", "B4", "B5", "B6", "B7", "B8", + "C1", "C2", + "D1", "D2", "D3", "D4", "D5", "D6", "D7", "D8", + "E1", "E2", "E3", "E4", "E5", "E6", "E7", "E8", + }; + foreach (var area in areas) + { + MapValueToEntryValueIfNonNull(src, dst, $"TouchSensitivity.{area}", $"GameSettings.TouchSensitivity.{area}"); + } + } + + // CustomKeyMap + if (src.GetValueOrDefault("CustomKeyMap.Enable")) + { + dst.EnsureDictionary("GameSystem.KeyMap"); + var keys = new[] + { + "Test", "Service", + "Button1_1P", "Button3_1P", "Button4_1P", "Button2_1P", "Button5_1P", "Button6_1P", "Button7_1P", "Button8_1P", + "Select_1P", + "Button1_2P", "Button2_2P", "Button3_2P", "Button4_2P", "Button5_2P", "Button6_2P", "Button7_2P", "Button8_2P", + "Select_2P" + }; + foreach (var key in keys) + { + if (src.TryGetValue($"CustomKeyMap.{key}", out var value) && + Enum.TryParse(value, out var keyCode)) + { + dst.SetValue($"GameSystem.KeyMap.{key}", keyCode.ToString()); + } + } + } + + // MaimaiDX2077 (WTF is the name?) + MapBooleanTrueToSectionEnable(src, dst, "MaimaiDX2077.CustomNoteTypePatch", "Fancy.GamePlay.CustomNoteTypes"); + + // Default enabled in V2 + dst.EnsureDictionary("GameSystem.RemoveEncryption"); + + return dst; + } + + // An value in the old config maps to an entry value in the new config. + // Any existing value, including zero, is valid. + private void MapValueToEntryValueIfNonNull(IConfigView src, ConfigView dst, string srcKey, string dstKey) + { + if (src.TryGetValue(srcKey, out var value)) + { + dst.SetValue(dstKey, value); + } + } + + // An value in the old config maps to an entry value in the new config. + // Null or default value is ignored. + private void MapValueToEntryValueIfNonNullOrDefault(IConfigView src, ConfigView dst, string srcKey, string dstKey, T defaultValue) + { + if (src.TryGetValue(srcKey, out var value) && !EqualityComparer.Default.Equals(value, defaultValue)) + { + dst.SetValue(dstKey, value); + } + } + + // An value in the old config maps to an entry value in the new config. + // Null value is replaced with a default value. + private void MapValueOrDefaultToEntryValue(IConfigView src, ConfigView dst, string srcKey, string dstKey, T defaultValue) + { + if (src.TryGetValue(srcKey, out var value)) + { + dst.SetValue(dstKey, value); + } + else + { + dst.SetValue(dstKey, defaultValue); + } + } + + // An boolean value in the old config maps to a default-off section's enable in the new config. + private void MapBooleanTrueToSectionEnable(IConfigView src, ConfigView dst, string srcKey, string dstKey) + { + if (src.GetValueOrDefault(srcKey)) + { + dst.EnsureDictionary(dstKey); + } + } +} diff --git a/AquaMai/AquaMai.Config/Migration/IConfigMigration.cs b/AquaMai/AquaMai.Config/Migration/IConfigMigration.cs new file mode 100644 index 00000000..2c7c9d11 --- /dev/null +++ b/AquaMai/AquaMai.Config/Migration/IConfigMigration.cs @@ -0,0 +1,10 @@ +using AquaMai.Config.Interfaces; + +namespace AquaMai.Config.Migration; + +public interface IConfigMigration +{ + public string FromVersion { get; } + public string ToVersion { get; } + public IConfigView Migrate(IConfigView config); +} diff --git a/AquaMai/AquaMai.Config/Polyfills.cs b/AquaMai/AquaMai.Config/Polyfills.cs new file mode 100644 index 00000000..eb2da113 --- /dev/null +++ b/AquaMai/AquaMai.Config/Polyfills.cs @@ -0,0 +1,4 @@ +namespace System.Runtime.CompilerServices +{ + internal static class IsExternalInit {} +} diff --git a/AquaMai/AquaMai.Config/Reflection/MonoCecilAssemblyReflectionProvider.cs b/AquaMai/AquaMai.Config/Reflection/MonoCecilAssemblyReflectionProvider.cs new file mode 100644 index 00000000..69afece2 --- /dev/null +++ b/AquaMai/AquaMai.Config/Reflection/MonoCecilAssemblyReflectionProvider.cs @@ -0,0 +1,254 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using AquaMai.Config.Attributes; +using AquaMai.Config.Interfaces; +using Mono.Cecil; +using Mono.Cecil.Cil; + +namespace AquaMai.Config.Reflection; + +public class MonoCecilReflectionProvider : IReflectionProvider +{ + public record ReflectionField( + string Name, + Type FieldType, + object Value, + IDictionary Attributes) : IReflectionField + { + public object Value { get; set; } = Value; + + public T GetCustomAttribute() where T : Attribute => Attributes.TryGetValue(typeof(T), out var value) ? (T)value : null; + public object GetValue(object obj) => Value; + public void SetValue(object obj, object value) => Value = value; + } + + public record ReflectionType( + string FullName, + string Namespace, + IReflectionField[] Fields, + IDictionary Attributes) : IReflectionType + { + public T GetCustomAttribute() where T : Attribute => Attributes.TryGetValue(typeof(T), out var value) ? (T)value : null; + public IReflectionField[] GetFields(BindingFlags bindingAttr) => Fields; + } + + private static readonly Type[] attributeTypes = + [ + typeof(ConfigCollapseNamespaceAttribute), + typeof(ConfigSectionAttribute), + typeof(ConfigEntryAttribute), + ]; + + private readonly IReflectionType[] reflectionTypes = []; + private readonly Dictionary> enums = []; + + public IReflectionType[] GetTypes() => reflectionTypes; + public Dictionary GetEnum(string enumName) => enums[enumName]; + + public MonoCecilReflectionProvider(AssemblyDefinition assembly) + { + reflectionTypes = assembly.MainModule.Types.Select(cType => { + var typeAttributes = InstantiateAttributes(cType.CustomAttributes); + var fields = cType.Fields.Select(cField => { + try + { + var fieldAttributes = InstantiateAttributes(cField.CustomAttributes); + if (fieldAttributes.Count == 0) + { + return null; + } + var type = GetRuntimeType(cField.FieldType); + var defaultValue = GetFieldDefaultValue(cType, cField, type); + return new ReflectionField(cField.Name, type, defaultValue, fieldAttributes); + } + catch (Exception e) + { + Console.WriteLine(e); + } + return null; + }).Where(field => field != null).ToArray(); + return new ReflectionType(cType.FullName, cType.Namespace, fields, typeAttributes); + }).ToArray(); + enums = assembly.MainModule.Types + .Where(cType => cType.IsEnum) + .ToDictionary(cType => + cType.FullName, + cType => cType.Fields + .Where(cField => cField.IsPublic && cField.IsStatic && cField.Constant != null) + .ToDictionary(cField => cField.Name, cField => cField.Constant)); + } + + private Dictionary InstantiateAttributes(ICollection attribute) => + attribute + .Select(InstantiateAttribute) + .Where(a => a != null) + .ToDictionary(a => a.GetType(), a => a); + + private object InstantiateAttribute(CustomAttribute attribute) => + attributeTypes.FirstOrDefault(t => t.FullName == attribute.AttributeType.FullName) switch + { + Type type => Activator.CreateInstance(type, + attribute.Constructor.Parameters + .Select((parameter, i) => + { + var runtimeType = GetRuntimeType(parameter.ParameterType); + var value = attribute.ConstructorArguments[i].Value; + if (runtimeType.IsEnum) + { + return Enum.Parse(runtimeType, value.ToString()); + } + return value; + }) + .ToArray()), + _ => null + }; + + private Type GetRuntimeType(TypeReference typeReference) { + if (typeReference.IsGenericInstance) + { + var genericInstance = (GenericInstanceType)typeReference; + var genericType = GetRuntimeType(genericInstance.ElementType); + var genericArguments = genericInstance.GenericArguments.Select(GetRuntimeType).ToArray(); + return genericType.MakeGenericType(genericArguments); + } + + var type = Type.GetType(typeReference.FullName); + if (type == null) + { + throw new TypeLoadException($"Type {typeReference.FullName} not found."); + } + return type; + } + + private static object GetFieldDefaultValue(TypeDefinition cType, FieldDefinition cField, Type fieldType) + { + object defaultValue = null; + var cctor = cType.Methods.SingleOrDefault(m => m.Name == ".cctor"); + if (cctor != null) + { + var store = cctor.Body.Instructions.SingleOrDefault(i => i.OpCode == OpCodes.Stsfld && i.Operand == cField); + if (store != null) + { + var loadOperand = ParseConstantLoadOperand(store.Previous); + if (fieldType == typeof(bool)) + { + defaultValue = Convert.ToBoolean(loadOperand); + } + else + { + defaultValue = loadOperand; + } + } + } + + if (defaultValue == null && cField.HasDefault) + { + throw new InvalidOperationException($"Field {cType.FullName}.{cField.Name} has default value but no .cctor stsfld instruction."); + } + defaultValue ??= GetDefaultValue(fieldType); + + if (fieldType.IsEnum) + { + var enumType = fieldType.GetEnumUnderlyingType(); + // Assume casting is safe since we're getting the default value from the field + var castedValue = Convert.ChangeType(defaultValue, enumType); + if (Enum.IsDefined(fieldType, castedValue)) + { + return Enum.ToObject(fieldType, castedValue); + } + } + + return defaultValue; + } + + private static object ParseConstantLoadOperand(Instruction instruction) + { + if (instruction.OpCode == OpCodes.Ldc_I4_M1) + { + return -1; + } + if (instruction.OpCode == OpCodes.Ldc_I4_0) + { + return 0; + } + if (instruction.OpCode == OpCodes.Ldc_I4_1) + { + return 1; + } + if (instruction.OpCode == OpCodes.Ldc_I4_2) + { + return 2; + } + if (instruction.OpCode == OpCodes.Ldc_I4_3) + { + return 3; + } + if (instruction.OpCode == OpCodes.Ldc_I4_4) + { + return 4; + } + if (instruction.OpCode == OpCodes.Ldc_I4_5) + { + return 5; + } + if (instruction.OpCode == OpCodes.Ldc_I4_6) + { + return 6; + } + if (instruction.OpCode == OpCodes.Ldc_I4_7) + { + return 7; + } + if (instruction.OpCode == OpCodes.Ldc_I4_8) + { + return 8; + } + if (instruction.OpCode == OpCodes.Ldc_I4_S) + { + return Convert.ToInt32((sbyte)instruction.Operand); + } + if (instruction.OpCode == OpCodes.Ldc_I4) + { + return (int)instruction.Operand; + } + if (instruction.OpCode == OpCodes.Ldc_I8) + { + return (long)instruction.Operand; + } + if (instruction.OpCode == OpCodes.Ldc_R4) + { + return (float)instruction.Operand; + } + if (instruction.OpCode == OpCodes.Ldc_R8) + { + return (double)instruction.Operand; + } + if (instruction.OpCode == OpCodes.Ldstr) + { + return (string)instruction.Operand; + } + else + { + var currentMethod = MethodBase.GetCurrentMethod(); + throw new NotImplementedException($"Unsupported constant load: {instruction}. Please implement in {currentMethod.DeclaringType.FullName}.{currentMethod.Name}"); + } + } + + private static object GetDefaultValue(Type type) + { + if (type.IsValueType) + { + return Activator.CreateInstance(type); + } + else if (type == typeof(string)) + { + return string.Empty; + } + else + { + return null; + } + } +} diff --git a/AquaMai/AquaMai.Config/Reflection/ReflectionManager.cs b/AquaMai/AquaMai.Config/Reflection/ReflectionManager.cs new file mode 100644 index 00000000..54d192a6 --- /dev/null +++ b/AquaMai/AquaMai.Config/Reflection/ReflectionManager.cs @@ -0,0 +1,178 @@ +using System.Reflection; +using System.Linq; +using System.Collections.Generic; +using AquaMai.Config.Attributes; +using AquaMai.Config.Interfaces; +using System; + +namespace AquaMai.Config.Reflection; + +public class ReflectionManager : IReflectionManager +{ + public record Entry : IReflectionManager.IEntry + { + public string Path { get; init; } + public string Name { get; init; } + public IReflectionField Field { get; init; } + public ConfigEntryAttribute Attribute { get; init; } + } + + public record Section : IReflectionManager.ISection + { + public string Path { get; init; } + public IReflectionType Type { get; init; } + public ConfigSectionAttribute Attribute { get; init; } + public List entries; + public List Entries => entries.Cast().ToList(); + } + + private readonly Dictionary sections = new(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary entries = new(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary sectionsByFullName = []; + + public ReflectionManager(IReflectionProvider reflectionProvider) + { + var prefix = "AquaMai.Mods."; + var types = reflectionProvider.GetTypes().Where(t => t.FullName.StartsWith(prefix)); + var collapsedNamespaces = new HashSet(); + foreach (var type in types) + { + var sectionAttribute = type.GetCustomAttribute(); + if (sectionAttribute == null) continue; + if (collapsedNamespaces.Contains(type.Namespace)) + { + throw new Exception($"Collapsed namespace {type.Namespace} contains multiple sections"); + } + var path = type.FullName.Substring(prefix.Length); + if (type.GetCustomAttribute() != null) + { + var separated = path.Split('.'); + if (separated[separated.Length - 2] != separated[separated.Length - 1]) + { + throw new Exception($"Type {type.FullName} is not collapsable"); + } + path = string.Join(".", separated.Take(separated.Length - 1)); + collapsedNamespaces.Add(type.Namespace); + } + + var sectionEntries = new List(); + foreach (var field in type.GetFields(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)) + { + var entryAttribute = field.GetCustomAttribute(); + if (entryAttribute == null) continue; + var transformedName = Utility.ToPascalCase(field.Name); + var entryPath = $"{path}.{transformedName}"; + var entry = new Entry() + { + Path = entryPath, + Name = transformedName, + Field = field, + Attribute = entryAttribute + }; + sectionEntries.Add(entry); + entries.Add(entryPath, entry); + } + + var section = new Section() + { + Path = path, + Type = type, + Attribute = sectionAttribute, + entries = sectionEntries + }; + sections.Add(path, section); + sectionsByFullName.Add(type.FullName, section); + } + + var order = reflectionProvider.GetEnum("AquaMai.Mods.SetionNameOrder"); + sections = sections + .OrderBy(x => x.Key) + .OrderBy(x => + { + var parts = x.Key.Split('.'); + for (int i = parts.Length; i > 0; i--) + { + var key = string.Join("_", parts.Take(i)); + if (order.TryGetValue(key, out var value)) + { + return (int)value; + } + } + Utility.Log($"Section {x.Key} has no order defined, defaulting to int.MaxValue"); + return int.MaxValue; + }) + .ToDictionary(x => x.Key, x => x.Value, StringComparer.OrdinalIgnoreCase); + } + + public IEnumerable
SectionValues => sections.Values; + public IEnumerable Sections => sections.Values.Cast(); + + public IEnumerable EntryValues => entries.Values; + public IEnumerable Entries => entries.Values.Cast(); + + public bool ContainsSection(string path) + { + return sections.ContainsKey(path); + } + + public bool TryGetSection(string path, out IReflectionManager.ISection section) + { + if (sections.TryGetValue(path, out var sectionValue)) + { + section = sectionValue; + return true; + } + section = null; + return false; + } + + public bool TryGetSection(Type type, out IReflectionManager.ISection section) + { + bool result = sectionsByFullName.TryGetValue(type.FullName, out var sectionValue); + section = sectionValue; + return result; + } + + public IReflectionManager.ISection GetSection(string path) + { + if (!TryGetSection(path, out var section)) + { + throw new KeyNotFoundException($"Section {path} not found"); + } + return section; + } + + public IReflectionManager.ISection GetSection(Type type) + { + if (!TryGetSection(type.FullName, out var section)) + { + throw new KeyNotFoundException($"Section {type.FullName} not found"); + } + return section; + } + + public bool ContainsEntry(string path) + { + return entries.ContainsKey(path); + } + + public bool TryGetEntry(string path, out IReflectionManager.IEntry entry) + { + if (entries.TryGetValue(path, out var entryValue)) + { + entry = entryValue; + return true; + } + entry = null; + return false; + } + + public IReflectionManager.IEntry GetEntry(string path) + { + if (!TryGetEntry(path, out var entry)) + { + throw new KeyNotFoundException($"Entry {path} not found"); + } + return entry; + } +} diff --git a/AquaMai/AquaMai.Config/Reflection/SystemReflectionProvider.cs b/AquaMai/AquaMai.Config/Reflection/SystemReflectionProvider.cs new file mode 100644 index 00000000..fe03a0de --- /dev/null +++ b/AquaMai/AquaMai.Config/Reflection/SystemReflectionProvider.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using AquaMai.Config.Interfaces; + +namespace AquaMai.Config.Reflection; + +public class SystemReflectionProvider(Assembly assembly) : IReflectionProvider +{ + public class ReflectionField(FieldInfo field) : IReflectionField + { + public FieldInfo UnderlyingField { get; } = field; + + public string Name => UnderlyingField.Name; + public Type FieldType => UnderlyingField.FieldType; + public T GetCustomAttribute() where T : Attribute => UnderlyingField.GetCustomAttribute(); + public object GetValue(object obj) => UnderlyingField.GetValue(obj); + public void SetValue(object obj, object value) => UnderlyingField.SetValue(obj, value); + } + + public class ReflectionType(Type type) : IReflectionType + { + public Type UnderlyingType { get; } = type; + + public string FullName => UnderlyingType.FullName; + public string Namespace => UnderlyingType.Namespace; + public T GetCustomAttribute() where T : Attribute => UnderlyingType.GetCustomAttribute(); + public IReflectionField[] GetFields(BindingFlags bindingAttr) => Array.ConvertAll(UnderlyingType.GetFields(bindingAttr), f => new ReflectionField(f)); + } + + public Assembly UnderlyingAssembly { get; } = assembly; + + public IReflectionType[] GetTypes() => Array.ConvertAll(UnderlyingAssembly.GetTypes(), t => new ReflectionType(t)); + + public Dictionary GetEnum(string enumName) + { + var enumType = UnderlyingAssembly.GetType(enumName); + if (enumType == null) return null; + var enumValues = Enum.GetValues(enumType); + var enumDict = new Dictionary(); + foreach (var enumValue in enumValues) + { + enumDict.Add(enumValue.ToString(), enumValue); + } + return enumDict; + } +} diff --git a/AquaMai/CustomKeyMap/KeyCodeID.cs b/AquaMai/AquaMai.Config/Types/KeyCodeID.cs similarity index 97% rename from AquaMai/CustomKeyMap/KeyCodeID.cs rename to AquaMai/AquaMai.Config/Types/KeyCodeID.cs index b4b9dc8f..f1c7615c 100644 --- a/AquaMai/CustomKeyMap/KeyCodeID.cs +++ b/AquaMai/AquaMai.Config/Types/KeyCodeID.cs @@ -1,4 +1,4 @@ -namespace AquaMai.CustomKeyMap; +namespace AquaMai.Config.Types; public enum KeyCodeID { diff --git a/AquaMai/ModKeyMap/ModKeyCode.cs b/AquaMai/AquaMai.Config/Types/KeyCodeOrName.cs similarity index 90% rename from AquaMai/ModKeyMap/ModKeyCode.cs rename to AquaMai/AquaMai.Config/Types/KeyCodeOrName.cs index cd8a3830..400de783 100644 --- a/AquaMai/ModKeyMap/ModKeyCode.cs +++ b/AquaMai/AquaMai.Config/Types/KeyCodeOrName.cs @@ -1,6 +1,6 @@ -namespace AquaMai.ModKeyMap; +namespace AquaMai.Config.Types; -public enum ModKeyCode +public enum KeyCodeOrName { None, Alpha0, diff --git a/AquaMai/AquaMai.Config/Utility.cs b/AquaMai/AquaMai.Config/Utility.cs new file mode 100644 index 00000000..f6dbc605 --- /dev/null +++ b/AquaMai/AquaMai.Config/Utility.cs @@ -0,0 +1,183 @@ +using System; +using System.Reflection; +using Tomlet.Models; + +namespace AquaMai.Config; + +public static class Utility +{ + public static Action LogFunction = Console.WriteLine; + + public static bool IsTruty(TomlValue value, string path = null) + { + return value switch + { + TomlBoolean boolean => boolean.Value, + TomlLong @long => @long.Value != 0, + _ => throw new ArgumentException( + path == null + ? $"Non-boolish TOML type {value.GetType().Name} value: {value}" + : $"When parsing {path}, got non-boolish TOML type {value.GetType().Name} value: {value}") + }; + } + + public static bool IsIntegerType(Type type) + { + return type == typeof(sbyte) || type == typeof(short) || type == typeof(int) || type == typeof(long) + || type == typeof(byte) || type == typeof(ushort) || type == typeof(uint) || type == typeof(ulong); + } + + public static bool IsFloatType(Type type) + { + return type == typeof(float) || type == typeof(double); + } + + public static bool IsNumberType(Type type) + { + return IsIntegerType(type) || IsFloatType(type); + } + + public static T ParseTomlValue(TomlValue value) + { + return (T)ParseTomlValue(typeof(T), value); + } + + public static object ParseTomlValue(Type type, TomlValue value) + { + if (type == typeof(bool)) + { + return IsTruty(value); + } + else if (IsNumberType(type)) + { + if (TryGetTomlNumberObject(value, out var numberObject)) + { + return Convert.ChangeType(numberObject, type); + } + else + { + throw new InvalidCastException($"Non-number TOML type: {value.GetType().Name}"); + } + } + else if (type == typeof(string)) + { + if (value is TomlString @string) + { + return @string.Value; + } + else + { + throw new InvalidCastException($"Non-string TOML type: {value.GetType().Name}"); + } + } + else if (type.IsEnum) + { + if (value is TomlString @string) + { + try + { + return Enum.Parse(type, @string.Value); + } + catch + { + throw new InvalidCastException($"Invalid enum {type.FullName} value: {@string.SerializedValue}"); + } + } + else if (value is TomlLong @long) + { + if (Enum.IsDefined(type, @long.Value)) + { + try + { + return Enum.ToObject(type, @long.Value); + } + catch + {} + } + throw new InvalidCastException($"Invalid enum {type.FullName} value: {@long.Value}"); + } + else + { + throw new InvalidCastException($"Non-enum TOML type: {value.GetType().Name}"); + } + } + else + { + var currentMethod = MethodBase.GetCurrentMethod(); + throw new NotImplementedException($"Unsupported config entry type: {type.FullName}. Please implement in {currentMethod.DeclaringType.FullName}.{currentMethod.Name}"); + } + } + + private static bool TryGetTomlNumberObject(TomlValue value, out object numberObject) + { + if (value is TomlLong @long) + { + numberObject = @long.Value; + return true; + } + else if (value is TomlDouble @double) + { + numberObject = @double.Value; + return true; + } + else + { + numberObject = null; + return false; + } + } + + public static bool TomlTryGetValueCaseInsensitive(TomlTable table, string key, out TomlValue value) + { + // Prefer exact match + if (table.TryGetValue(key, out value)) + { + return true; + } + // Fallback to case-insensitive match + foreach (var kvp in table) + { + if (string.Equals(kvp.Key, key, StringComparison.OrdinalIgnoreCase)) + { + value = kvp.Value; + return true; + } + } + value = null; + return false; + } + + public static bool TomlContainsKeyCaseInsensitive(TomlTable table, string key) + { + // Prefer exact match + if (table.ContainsKey(key)) + { + return true; + } + // Fallback to case-insensitive match + foreach (var kvp in table) + { + if (string.Equals(kvp.Key, key, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + return false; + } + + public static string ToPascalCase(string str) + { + return str.Length switch + { + 0 => str, + 1 => char.ToUpperInvariant(str[0]).ToString(), + _ => char.ToUpperInvariant(str[0]) + str.Substring(1) + }; + } + + // We can test the configuration related code without loading the mod into the game. + public static void Log(string message) + { + LogFunction(message); + } +} diff --git a/AquaMai/AquaMai.Core/AquaMai.Core.csproj b/AquaMai/AquaMai.Core/AquaMai.Core.csproj new file mode 100644 index 00000000..5a2a9837 --- /dev/null +++ b/AquaMai/AquaMai.Core/AquaMai.Core.csproj @@ -0,0 +1,132 @@ + + + + Release + AnyCPU + {33C0D4ED-6A84-4659-9A05-12D43D75D0B3} + Library + AquaMai.Core + AquaMai.Core + net472 + 512 + true + 12 + 414 + $(ProjectDir)../Libs/;$(AssemblySearchPaths) + $(ProjectDir)../Output/ + false + false + + + + false + None + true + prompt + 4 + true + false + + + + DEBUG + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + PublicResXFileCodeGenerator + Locale.Designer.cs + + + Locale.resx + + + + diff --git a/AquaMai/AquaMai.Core/Attributes/EnableGameVersionAttribute.cs b/AquaMai/AquaMai.Core/Attributes/EnableGameVersionAttribute.cs new file mode 100644 index 00000000..97191007 --- /dev/null +++ b/AquaMai/AquaMai.Core/Attributes/EnableGameVersionAttribute.cs @@ -0,0 +1,18 @@ +using System; + +namespace AquaMai.Core.Attributes; + +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)] +public class EnableGameVersionAttribute(uint minVersion = 0, uint maxVersion = 0, bool noWarn = false) : Attribute +{ + public uint MinVersion { get; } = minVersion; + public uint MaxVersion { get; } = maxVersion; + public bool NoWarn { get; } = noWarn; + + public bool ShouldEnable(uint gameVersion) + { + if (MinVersion > 0 && MinVersion > gameVersion) return false; + if (MaxVersion > 0 && MaxVersion < gameVersion) return false; + return true; + } +} diff --git a/AquaMai/AquaMai.Core/Attributes/EnableIfAttribute.cs b/AquaMai/AquaMai.Core/Attributes/EnableIfAttribute.cs new file mode 100644 index 00000000..43f3cc8f --- /dev/null +++ b/AquaMai/AquaMai.Core/Attributes/EnableIfAttribute.cs @@ -0,0 +1,79 @@ +using System; + +namespace AquaMai.Core.Attributes; + +public enum EnableConditionOperator +{ + Equal, + NotEqual, + GreaterThan, + LessThan, + GreaterThanOrEqual, + LessThanOrEqual +} + +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)] +public class EnableIfAttribute( + Type referenceType, + string referenceMember, + EnableConditionOperator @operator, + object rightSideValue) : Attribute +{ + public Type ReferenceType { get; } = referenceType; + public string ReferenceMember { get; } = referenceMember; + public EnableConditionOperator Operator { get; } = @operator; + public object RightSideValue { get; } = rightSideValue; + + // Referencing a field in another class and checking if it's true. + public EnableIfAttribute(Type referenceType, string referenceMember) + : this(referenceType, referenceMember, EnableConditionOperator.Equal, true) + { } + + // Referencing a field in the same class and comparing it with a value. + public EnableIfAttribute(string referenceMember, EnableConditionOperator condition, object value) + : this(null, referenceMember, condition, value) + { } + + // Referencing a field in the same class and checking if it's true. + public EnableIfAttribute(string referenceMember) + : this(referenceMember, EnableConditionOperator.Equal, true) + { } + + public bool ShouldEnable(Type selfType) + { + var referenceType = ReferenceType ?? selfType; + var referenceField = referenceType.GetField( + ReferenceMember, + System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic); + var referenceProperty = referenceType.GetProperty( + ReferenceMember, + System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic); + if (referenceField == null && referenceProperty == null) + { + throw new ArgumentException($"Field or property {ReferenceMember} not found in {referenceType.FullName}"); + } + var referenceMemberValue = referenceField != null ? referenceField.GetValue(null) : referenceProperty.GetValue(null); + switch (Operator) + { + case EnableConditionOperator.Equal: + return referenceMemberValue.Equals(RightSideValue); + case EnableConditionOperator.NotEqual: + return !referenceMemberValue.Equals(RightSideValue); + case EnableConditionOperator.GreaterThan: + case EnableConditionOperator.LessThan: + case EnableConditionOperator.GreaterThanOrEqual: + case EnableConditionOperator.LessThanOrEqual: + var comparison = (IComparable)referenceMemberValue; + return Operator switch + { + EnableConditionOperator.GreaterThan => comparison.CompareTo(RightSideValue) > 0, + EnableConditionOperator.LessThan => comparison.CompareTo(RightSideValue) < 0, + EnableConditionOperator.GreaterThanOrEqual => comparison.CompareTo(RightSideValue) >= 0, + EnableConditionOperator.LessThanOrEqual => comparison.CompareTo(RightSideValue) <= 0, + _ => throw new NotImplementedException(), + }; + default: + throw new NotImplementedException(); + } + } +} diff --git a/AquaMai/AquaMai.Core/Attributes/EnableImplicitlyIfAttribute.cs b/AquaMai/AquaMai.Core/Attributes/EnableImplicitlyIfAttribute.cs new file mode 100644 index 00000000..71fb02bd --- /dev/null +++ b/AquaMai/AquaMai.Core/Attributes/EnableImplicitlyIfAttribute.cs @@ -0,0 +1,12 @@ +using System; + +namespace AquaMai.Core.Attributes; + +// If the field or property with this name is true, the patch will be implicitly enabled, regardless of the config state. +// This is handled outside the config module, while The config state won't be actually set to enabled by it. +// Won't bypass the restriction of [EnableIf()] and [EnableGameVersion()]. +[AttributeUsage(AttributeTargets.Class)] +public class EnableImplicitlyIf(string memberName) : Attribute +{ + public string MemberName { get; } = memberName; +} diff --git a/AquaMai/AquaMai.Core/ConfigLoader.cs b/AquaMai/AquaMai.Core/ConfigLoader.cs new file mode 100644 index 00000000..92a9eaa1 --- /dev/null +++ b/AquaMai/AquaMai.Core/ConfigLoader.cs @@ -0,0 +1,83 @@ +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using MelonLoader; +using AquaMai.Config; +using AquaMai.Config.Interfaces; +using AquaMai.Config.Migration; + +namespace AquaMai.Core; + +public static class ConfigLoader +{ + private static string ConfigFile => "AquaMai.toml"; + private static string ConfigExampleFile(string lang) => $"AquaMai.{lang}.toml"; + private static string OldConfigFile(string version) => $"AquaMai.toml.old-v{version}."; + + private static Config.Config config; + + public static Config.Config Config => config; + + public static bool LoadConfig(Assembly modsAssembly) + { + Utility.LogFunction = MelonLogger.Msg; + + config = new( + new Config.Reflection.ReflectionManager( + new Config.Reflection.SystemReflectionProvider(modsAssembly))); + + if (!File.Exists(ConfigFile)) + { + var examples = GenerateExamples(); + foreach (var (lang, example) in examples) + { + var filename = ConfigExampleFile(lang); + File.WriteAllText(filename, example); + } + MelonLogger.Error("======================================!!!"); + MelonLogger.Error("AquaMai.toml not found! Please create it."); + MelonLogger.Error("找不到配置文件 AquaMai.toml!请创建。"); + MelonLogger.Error("Example copied to AquaMai.en.toml"); + MelonLogger.Error("示例已复制到 AquaMai.zh.toml"); + MelonLogger.Error("========================================="); + return false; + } + + var configText = File.ReadAllText(ConfigFile); + var configView = new ConfigView(configText); + var configVersion = ConfigMigrationManager.Instance.GetVersion(configView); + if (configVersion != ConfigMigrationManager.Instance.latestVersion) + { + File.WriteAllText(OldConfigFile(configVersion), configText); + configView = (ConfigView)ConfigMigrationManager.Instance.Migrate(configView); + } + + // Read AquaMai.toml to load settings + ConfigParser.Instance.Parse(config, configView); + + return true; + } + + public static void SaveConfig(string lang) + { + File.WriteAllText(ConfigFile, SerailizeCurrentConfig(lang)); + } + + private static string SerailizeCurrentConfig(string lang) => + new ConfigSerializer(new IConfigSerializer.Options() + { + Lang = lang, + IncludeBanner = true, + OverrideLocaleValue = true + }).Serialize(config); + + private static IDictionary GenerateExamples() + { + var examples = new Dictionary(); + foreach (var lang in (string[]) ["en", "zh"]) + { + examples[lang] = SerailizeCurrentConfig(lang); + } + return examples; + } +} diff --git a/AquaMai/AquaMai.Core/Helpers/EnableConditionHelper.cs b/AquaMai/AquaMai.Core/Helpers/EnableConditionHelper.cs new file mode 100644 index 00000000..475c24ae --- /dev/null +++ b/AquaMai/AquaMai.Core/Helpers/EnableConditionHelper.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections; +using System.Reflection; +using AquaMai.Core.Attributes; +using AquaMai.Core.Resources; +using HarmonyLib; +using MelonLoader; + +namespace AquaMai.Core.Helpers; + +public class EnableConditionHelper +{ + [HarmonyPostfix] + [HarmonyPatch("HarmonyLib.PatchTools", "GetPatchMethod")] + public static void PostGetPatchMethod(ref MethodInfo __result) + { + if (__result != null) + { + if (ShouldSkipMethodOrClass(__result.GetCustomAttribute, __result.ReflectedType, __result.Name)) + { + __result = null; + } + } + } + + [HarmonyPostfix] + [HarmonyPatch("HarmonyLib.PatchTools", "GetPatchMethods")] + public static void PostGetPatchMethods(ref IList __result) + { + for (int i = 0; i < __result.Count; i++) + { + var harmonyMethod = Traverse.Create(__result[i]).Field("info").GetValue() as HarmonyMethod; + var method = harmonyMethod.method; + if (ShouldSkipMethodOrClass(method.GetCustomAttribute, method.ReflectedType, method.Name)) + { + __result.RemoveAt(i); + i--; + } + } + } + + public static bool ShouldSkipClass(Type type) + { + return ShouldSkipMethodOrClass(type.GetCustomAttribute, type); + } + + private static bool ShouldSkipMethodOrClass(Func getCustomAttribute, Type type, string methodName = "") + { + var displayName = type.FullName + (string.IsNullOrEmpty(methodName) ? "" : $".{methodName}"); + var enableIf = (EnableIfAttribute)getCustomAttribute(typeof(EnableIfAttribute)); + if (enableIf != null && !enableIf.ShouldEnable(type)) + { +# if DEBUG + MelonLogger.Msg($"Skipping {displayName} due to EnableIf condition"); +# endif + return true; + } + var enableGameVersion = (EnableGameVersionAttribute)getCustomAttribute(typeof(EnableGameVersionAttribute)); + if (enableGameVersion != null && !enableGameVersion.ShouldEnable(GameInfo.GameVersion)) + { +# if DEBUG + MelonLogger.Msg($"Skipping {displayName} due to EnableGameVersion condition"); +# endif + if (!enableGameVersion.NoWarn) + { + MelonLogger.Warning(string.Format(Locale.SkipIncompatiblePatch, type)); + } + return true; + } + return false; + } +} diff --git a/AquaMai/AquaMai.Core/Helpers/FileSystem.cs b/AquaMai/AquaMai.Core/Helpers/FileSystem.cs new file mode 100644 index 00000000..3eab6f0b --- /dev/null +++ b/AquaMai/AquaMai.Core/Helpers/FileSystem.cs @@ -0,0 +1,15 @@ +using System; +using System.IO; + +namespace AquaMai.Core.Helpers; + +public static class FileSystem +{ + public static string ResolvePath(string path) + { + var varExpanded = Environment.ExpandEnvironmentVariables(path); + return Path.IsPathRooted(varExpanded) + ? varExpanded + : Path.Combine(Environment.CurrentDirectory, varExpanded); + } +} diff --git a/AquaMai/Helpers/GameInfo.cs b/AquaMai/AquaMai.Core/Helpers/GameInfo.cs similarity index 95% rename from AquaMai/Helpers/GameInfo.cs rename to AquaMai/AquaMai.Core/Helpers/GameInfo.cs index b29614de..117f724b 100644 --- a/AquaMai/Helpers/GameInfo.cs +++ b/AquaMai/AquaMai.Core/Helpers/GameInfo.cs @@ -1,7 +1,7 @@ using System.Reflection; using MAI2System; -namespace AquaMai.Helpers; +namespace AquaMai.Core.Helpers; public class GameInfo { diff --git a/AquaMai/Helpers/GuiSizes.cs b/AquaMai/AquaMai.Core/Helpers/GuiSizes.cs similarity index 89% rename from AquaMai/Helpers/GuiSizes.cs rename to AquaMai/AquaMai.Core/Helpers/GuiSizes.cs index b3012724..30efa7b2 100644 --- a/AquaMai/Helpers/GuiSizes.cs +++ b/AquaMai/AquaMai.Core/Helpers/GuiSizes.cs @@ -2,15 +2,15 @@ using System.Linq; using System.Reflection; using HarmonyLib; -using MelonLoader; using UnityEngine; -namespace AquaMai.Helpers; +namespace AquaMai.Core.Helpers; public static class GuiSizes { + public static bool SinglePlayer { get; set; } = false; public static float PlayerWidth => Screen.height / 1920f * 1080; - public static float PlayerCenter => AquaMai.AppConfig.UX.SinglePlayer ? Screen.width / 2f : Screen.width / 2f - PlayerWidth / 2; + public static float PlayerCenter => SinglePlayer ? Screen.width / 2f : Screen.width / 2f - PlayerWidth / 2; public static int FontSize => (int)(PlayerWidth * .015f); public static float LabelHeight => FontSize * 1.5f; public static float Margin => PlayerWidth * .005f; diff --git a/AquaMai/AquaMai.Core/Helpers/KeyListener.cs b/AquaMai/AquaMai.Core/Helpers/KeyListener.cs new file mode 100644 index 00000000..0215dd4d --- /dev/null +++ b/AquaMai/AquaMai.Core/Helpers/KeyListener.cs @@ -0,0 +1,146 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using AquaMai.Config.Types; +using HarmonyLib; +using Main; +using Manager; +using MelonLoader; +using UnityEngine; + +namespace AquaMai.Core.Helpers; + +public static class KeyListener +{ + private static readonly Dictionary _keyPressFrames = []; + private static readonly Dictionary _keyPressFramesPrev = []; + + static KeyListener() + { + foreach (KeyCodeOrName key in Enum.GetValues(typeof(KeyCodeOrName))) + { + _keyPressFrames[key] = 0; + _keyPressFramesPrev[key] = 0; + } + } + + [HarmonyPostfix] + [HarmonyPatch(typeof(GameMainObject), "Update")] + public static void CheckLongPush() + { + foreach (KeyCodeOrName key in Enum.GetValues(typeof(KeyCodeOrName))) + { + _keyPressFramesPrev[key] = _keyPressFrames[key]; + if (GetKeyPush(key)) + { +# if DEBUG + MelonLogger.Msg($"CheckLongPush {key} is push {_keyPressFrames[key]}"); +# endif + _keyPressFrames[key]++; + } + else + { + _keyPressFrames[key] = 0; + } + } + } + + public static bool GetKeyPush(KeyCodeOrName key) => + key switch + { + KeyCodeOrName.None => false, + < KeyCodeOrName.Select1P => Input.GetKey(key.GetKeyCode()), + KeyCodeOrName.Test => InputManager.GetSystemInputPush(InputManager.SystemButtonSetting.ButtonTest), + KeyCodeOrName.Service => InputManager.GetSystemInputPush(InputManager.SystemButtonSetting.ButtonService), + KeyCodeOrName.Select1P => InputManager.GetButtonPush(0, InputManager.ButtonSetting.Select), + KeyCodeOrName.Select2P => InputManager.GetButtonPush(1, InputManager.ButtonSetting.Select), + _ => throw new ArgumentOutOfRangeException(nameof(key), key, "我也不知道这是什么键") + }; + + public static bool GetKeyDown(KeyCodeOrName key) + { + // return key switch + // { + // KeyCodeOrName.None => false, + // < KeyCodeOrName.Select1P => Input.GetKeyDown(key.GetKeyCode()), + // KeyCodeOrName.Test => InputManager.GetSystemInputDown(InputManager.SystemButtonSetting.ButtonTest), + // KeyCodeOrName.Service => InputManager.GetSystemInputDown(InputManager.SystemButtonSetting.ButtonService), + // KeyCodeOrName.Select1P => InputManager.GetButtonDown(0, InputManager.ButtonSetting.Select), + // KeyCodeOrName.Select2P => InputManager.GetButtonDown(1, InputManager.ButtonSetting.Select), + // _ => throw new ArgumentOutOfRangeException(nameof(key), key, "我也不知道这是什么键") + // }; + + // 不用这个,我们检测按键是否弹起以及弹起之前按下的时间是否小于 30,这样可以防止要长按时按下的时候就触发 + return _keyPressFrames[key] == 0 && 0 < _keyPressFramesPrev[key] && _keyPressFramesPrev[key] < 30; + } + + public static bool GetKeyDownOrLongPress(KeyCodeOrName key, bool isLongPress) + { + bool ret; + if (isLongPress) + { + ret = _keyPressFrames[key] == 60; + } + else + { + ret = GetKeyDown(key); + } + +# if DEBUG + if (ret) + { + MelonLogger.Msg($"Key {key} is pressed, long press: {isLongPress}"); + MelonLogger.Msg(new StackTrace()); + } +# endif + return ret; + } + + private static KeyCode GetKeyCode(this KeyCodeOrName keyCodeOrName) => + keyCodeOrName switch + { + KeyCodeOrName.Alpha0 => KeyCode.Alpha0, + KeyCodeOrName.Alpha1 => KeyCode.Alpha1, + KeyCodeOrName.Alpha2 => KeyCode.Alpha2, + KeyCodeOrName.Alpha3 => KeyCode.Alpha3, + KeyCodeOrName.Alpha4 => KeyCode.Alpha4, + KeyCodeOrName.Alpha5 => KeyCode.Alpha5, + KeyCodeOrName.Alpha6 => KeyCode.Alpha6, + KeyCodeOrName.Alpha7 => KeyCode.Alpha7, + KeyCodeOrName.Alpha8 => KeyCode.Alpha8, + KeyCodeOrName.Alpha9 => KeyCode.Alpha9, + KeyCodeOrName.Keypad0 => KeyCode.Keypad0, + KeyCodeOrName.Keypad1 => KeyCode.Keypad1, + KeyCodeOrName.Keypad2 => KeyCode.Keypad2, + KeyCodeOrName.Keypad3 => KeyCode.Keypad3, + KeyCodeOrName.Keypad4 => KeyCode.Keypad4, + KeyCodeOrName.Keypad5 => KeyCode.Keypad5, + KeyCodeOrName.Keypad6 => KeyCode.Keypad6, + KeyCodeOrName.Keypad7 => KeyCode.Keypad7, + KeyCodeOrName.Keypad8 => KeyCode.Keypad8, + KeyCodeOrName.Keypad9 => KeyCode.Keypad9, + KeyCodeOrName.F1 => KeyCode.F1, + KeyCodeOrName.F2 => KeyCode.F2, + KeyCodeOrName.F3 => KeyCode.F3, + KeyCodeOrName.F4 => KeyCode.F4, + KeyCodeOrName.F5 => KeyCode.F5, + KeyCodeOrName.F6 => KeyCode.F6, + KeyCodeOrName.F7 => KeyCode.F7, + KeyCodeOrName.F8 => KeyCode.F8, + KeyCodeOrName.F9 => KeyCode.F9, + KeyCodeOrName.F10 => KeyCode.F10, + KeyCodeOrName.F11 => KeyCode.F11, + KeyCodeOrName.F12 => KeyCode.F12, + KeyCodeOrName.Insert => KeyCode.Insert, + KeyCodeOrName.Delete => KeyCode.Delete, + KeyCodeOrName.Home => KeyCode.Home, + KeyCodeOrName.End => KeyCode.End, + KeyCodeOrName.PageUp => KeyCode.PageUp, + KeyCodeOrName.PageDown => KeyCode.PageDown, + KeyCodeOrName.UpArrow => KeyCode.UpArrow, + KeyCodeOrName.DownArrow => KeyCode.DownArrow, + KeyCodeOrName.LeftArrow => KeyCode.LeftArrow, + KeyCodeOrName.RightArrow => KeyCode.RightArrow, + _ => throw new ArgumentOutOfRangeException(nameof(keyCodeOrName), keyCodeOrName, "游戏功能键需要单独处理") + }; +} diff --git a/AquaMai/Helpers/MessageHelper.cs b/AquaMai/AquaMai.Core/Helpers/MessageHelper.cs similarity index 97% rename from AquaMai/Helpers/MessageHelper.cs rename to AquaMai/AquaMai.Core/Helpers/MessageHelper.cs index 348ddd95..b0e6b64f 100644 --- a/AquaMai/Helpers/MessageHelper.cs +++ b/AquaMai/AquaMai.Core/Helpers/MessageHelper.cs @@ -4,7 +4,7 @@ using Manager; using MelonLoader; using Process; -namespace AquaMai.Helpers; +namespace AquaMai.Core.Helpers; public class MessageHelper { diff --git a/AquaMai/Helpers/MusicDirHelper.cs b/AquaMai/AquaMai.Core/Helpers/MusicDirHelper.cs similarity index 90% rename from AquaMai/Helpers/MusicDirHelper.cs rename to AquaMai/AquaMai.Core/Helpers/MusicDirHelper.cs index 8d7b4979..112a4e62 100644 --- a/AquaMai/Helpers/MusicDirHelper.cs +++ b/AquaMai/AquaMai.Core/Helpers/MusicDirHelper.cs @@ -1,10 +1,7 @@ using System.Collections.Generic; -using System.Diagnostics; using HarmonyLib; -using Manager; -using MelonLoader; -namespace AquaMai.Helpers; +namespace AquaMai.Core.Helpers; public class MusicDirHelper { diff --git a/AquaMai/Helpers/SharedInstances.cs b/AquaMai/AquaMai.Core/Helpers/SharedInstances.cs similarity index 95% rename from AquaMai/Helpers/SharedInstances.cs rename to AquaMai/AquaMai.Core/Helpers/SharedInstances.cs index 11017573..6f05f1fa 100644 --- a/AquaMai/Helpers/SharedInstances.cs +++ b/AquaMai/AquaMai.Core/Helpers/SharedInstances.cs @@ -2,7 +2,7 @@ using Main; using Process; -namespace AquaMai.Helpers; +namespace AquaMai.Core.Helpers; public class SharedInstances { diff --git a/AquaMai/Helpers/Shim.cs b/AquaMai/AquaMai.Core/Helpers/Shim.cs similarity index 99% rename from AquaMai/Helpers/Shim.cs rename to AquaMai/AquaMai.Core/Helpers/Shim.cs index 040af605..e2d72e83 100644 --- a/AquaMai/Helpers/Shim.cs +++ b/AquaMai/AquaMai.Core/Helpers/Shim.cs @@ -9,7 +9,7 @@ using Manager.UserDatas; using Net.Packet; using Net.Packet.Mai2; -namespace AquaMai.Helpers; +namespace AquaMai.Core.Helpers; public static class Shim { diff --git a/AquaMai/Fix/I18nSingleAssemblyHook.cs b/AquaMai/AquaMai.Core/Resources/I18nSingleAssemblyHook.cs similarity index 86% rename from AquaMai/Fix/I18nSingleAssemblyHook.cs rename to AquaMai/AquaMai.Core/Resources/I18nSingleAssemblyHook.cs index aebae280..2fdd490d 100644 --- a/AquaMai/Fix/I18nSingleAssemblyHook.cs +++ b/AquaMai/AquaMai.Core/Resources/I18nSingleAssemblyHook.cs @@ -1,9 +1,8 @@ using System.Globalization; using System.Resources; using HarmonyLib; -using MelonLoader; -namespace AquaMai.Fix; +namespace AquaMai.Core.Resources; public class I18nSingleAssemblyHook { @@ -13,8 +12,8 @@ public class I18nSingleAssemblyHook { var GetResourceFileName = __instance.GetType().GetMethod("GetResourceFileName", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); var resourceFileName = (string)GetResourceFileName.Invoke(__instance, [culture]); - var MainAssembly = typeof(AquaMai).Assembly; - var manifestResourceStream = MainAssembly.GetManifestResourceStream(resourceFileName); + var ResourcesAssembly = typeof(I18nSingleAssemblyHook).Assembly; + var manifestResourceStream = ResourcesAssembly.GetManifestResourceStream(resourceFileName); if (manifestResourceStream == null) { return true; @@ -22,7 +21,7 @@ public class I18nSingleAssemblyHook var resourceGroveler = __instance.GetType().GetField("resourceGroveler", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance).GetValue(__instance); var CreateResourceSet = resourceGroveler.GetType().GetMethod("CreateResourceSet", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); - var resourceSet = CreateResourceSet.Invoke(resourceGroveler, [manifestResourceStream, MainAssembly]); + var resourceSet = CreateResourceSet.Invoke(resourceGroveler, [manifestResourceStream, ResourcesAssembly]); var AddResourceSet = __instance.GetType().GetMethod("AddResourceSet", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static); var localResourceSets = __instance.GetType().GetField("_resourceSets", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance).GetValue(__instance); object[] args = [localResourceSets, culture.Name, resourceSet]; diff --git a/AquaMai/Resources/Locale.Designer.cs b/AquaMai/AquaMai.Core/Resources/Locale.Designer.cs similarity index 85% rename from AquaMai/Resources/Locale.Designer.cs rename to AquaMai/AquaMai.Core/Resources/Locale.Designer.cs index a3578c5d..5237bcdd 100644 --- a/AquaMai/Resources/Locale.Designer.cs +++ b/AquaMai/AquaMai.Core/Resources/Locale.Designer.cs @@ -7,7 +7,7 @@ // //------------------------------------------------------------------------------ -namespace AquaMai.Resources { +namespace AquaMai.Core.Resources { using System; @@ -21,24 +21,24 @@ namespace AquaMai.Resources { [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Locale { + public class Locale { private static global::System.Resources.ResourceManager resourceMan; private static global::System.Globalization.CultureInfo resourceCulture; [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Locale() { + public Locale() { } /// /// Returns the cached ResourceManager instance used by this class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { + public static global::System.Resources.ResourceManager ResourceManager { get { if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("AquaMai.Resources.Locale", typeof(Locale).Assembly); + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("AquaMai.Core.Resources.Locale", typeof(Locale).Assembly); resourceMan = temp; } return resourceMan; @@ -50,7 +50,7 @@ namespace AquaMai.Resources { /// resource lookups using this strongly typed resource class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { + public static global::System.Globalization.CultureInfo Culture { get { return resourceCulture; } @@ -62,7 +62,7 @@ namespace AquaMai.Resources { /// /// Looks up a localized string similar to You are using AquaMai CI build version. This version is built from the latest mainline code and may contain undocumented configuration changes or potential issues.. /// - internal static string CiBuildAlertContent { + public static string CiBuildAlertContent { get { return ResourceManager.GetString("CiBuildAlertContent", resourceCulture); } @@ -71,7 +71,7 @@ namespace AquaMai.Resources { /// /// Looks up a localized string similar to Important Notice: Test Version. /// - internal static string CiBuildAlertTitle { + public static string CiBuildAlertTitle { get { return ResourceManager.GetString("CiBuildAlertTitle", resourceCulture); } @@ -80,7 +80,7 @@ namespace AquaMai.Resources { /// /// Looks up a localized string similar to Loaded!. /// - internal static string Loaded { + public static string Loaded { get { return ResourceManager.GetString("Loaded", resourceCulture); } @@ -91,7 +91,7 @@ namespace AquaMai.Resources { ///- Are you using a modified Assembly-CSharp.dll, which will cause inconsistent functions and cannot find the functions that need to be modified ///- Check for conflicting mods, or enabled incompatible options. /// - internal static string LoadError { + public static string LoadError { get { return ResourceManager.GetString("LoadError", resourceCulture); } @@ -100,7 +100,7 @@ namespace AquaMai.Resources { /// /// Looks up a localized string similar to End. /// - internal static string MarkRepeatEnd { + public static string MarkRepeatEnd { get { return ResourceManager.GetString("MarkRepeatEnd", resourceCulture); } @@ -109,7 +109,7 @@ namespace AquaMai.Resources { /// /// Looks up a localized string similar to Start. /// - internal static string MarkRepeatStart { + public static string MarkRepeatStart { get { return ResourceManager.GetString("MarkRepeatStart", resourceCulture); } @@ -118,7 +118,7 @@ namespace AquaMai.Resources { /// /// Looks up a localized string similar to Aime reader error. /// - internal static string NetErrIsAliveAimeReader { + public static string NetErrIsAliveAimeReader { get { return ResourceManager.GetString("NetErrIsAliveAimeReader", resourceCulture); } @@ -127,7 +127,7 @@ namespace AquaMai.Resources { /// /// Looks up a localized string similar to Aime server error. /// - internal static string NetErrIsAliveAimeServer { + public static string NetErrIsAliveAimeServer { get { return ResourceManager.GetString("NetErrIsAliveAimeServer", resourceCulture); } @@ -136,7 +136,7 @@ namespace AquaMai.Resources { /// /// Looks up a localized string similar to Server communication error. /// - internal static string NetErrIsAliveServer { + public static string NetErrIsAliveServer { get { return ResourceManager.GetString("NetErrIsAliveServer", resourceCulture); } @@ -145,7 +145,7 @@ namespace AquaMai.Resources { /// /// Looks up a localized string similar to Data download not success. /// - internal static string NetErrWasDownloadSuccessOnce { + public static string NetErrWasDownloadSuccessOnce { get { return ResourceManager.GetString("NetErrWasDownloadSuccessOnce", resourceCulture); } @@ -154,7 +154,7 @@ namespace AquaMai.Resources { /// /// Looks up a localized string similar to Pause. /// - internal static string Pause { + public static string Pause { get { return ResourceManager.GetString("Pause", resourceCulture); } @@ -163,7 +163,7 @@ namespace AquaMai.Resources { /// /// Looks up a localized string similar to 游玩次数:{0}. /// - internal static string PlayCount { + public static string PlayCount { get { return ResourceManager.GetString("PlayCount", resourceCulture); } @@ -172,7 +172,7 @@ namespace AquaMai.Resources { /// /// Looks up a localized string similar to SSS+ => DXRating += {0}. /// - internal static string RatingUpWhenSSSp { + public static string RatingUpWhenSSSp { get { return ResourceManager.GetString("RatingUpWhenSSSp", resourceCulture); } @@ -181,7 +181,7 @@ namespace AquaMai.Resources { /// /// Looks up a localized string similar to Repeat end time cannot be less than repeat start time. /// - internal static string RepeatEndTimeLessThenStartTime { + public static string RepeatEndTimeLessThenStartTime { get { return ResourceManager.GetString("RepeatEndTimeLessThenStartTime", resourceCulture); } @@ -190,7 +190,7 @@ namespace AquaMai.Resources { /// /// Looks up a localized string similar to Loop Not Set. /// - internal static string RepeatNotSet { + public static string RepeatNotSet { get { return ResourceManager.GetString("RepeatNotSet", resourceCulture); } @@ -199,7 +199,7 @@ namespace AquaMai.Resources { /// /// Looks up a localized string similar to Reset. /// - internal static string RepeatReset { + public static string RepeatReset { get { return ResourceManager.GetString("RepeatReset", resourceCulture); } @@ -208,7 +208,7 @@ namespace AquaMai.Resources { /// /// Looks up a localized string similar to Loop Set. /// - internal static string RepeatStartEndSet { + public static string RepeatStartEndSet { get { return ResourceManager.GetString("RepeatStartEndSet", resourceCulture); } @@ -217,7 +217,7 @@ namespace AquaMai.Resources { /// /// Looks up a localized string similar to Loop Start Set. /// - internal static string RepeatStartSet { + public static string RepeatStartSet { get { return ResourceManager.GetString("RepeatStartSet", resourceCulture); } @@ -226,7 +226,7 @@ namespace AquaMai.Resources { /// /// Looks up a localized string similar to Please set repeat start time first. /// - internal static string RepeatStartTimeNotSet { + public static string RepeatStartTimeNotSet { get { return ResourceManager.GetString("RepeatStartTimeNotSet", resourceCulture); } @@ -235,7 +235,7 @@ namespace AquaMai.Resources { /// /// Looks up a localized string similar to Saving... Do not exit the game. /// - internal static string SavingDontExit { + public static string SavingDontExit { get { return ResourceManager.GetString("SavingDontExit", resourceCulture); } @@ -244,7 +244,7 @@ namespace AquaMai.Resources { /// /// Looks up a localized string similar to Seek <<. /// - internal static string SeekBackward { + public static string SeekBackward { get { return ResourceManager.GetString("SeekBackward", resourceCulture); } @@ -253,7 +253,7 @@ namespace AquaMai.Resources { /// /// Looks up a localized string similar to Seek >>. /// - internal static string SeekForward { + public static string SeekForward { get { return ResourceManager.GetString("SeekForward", resourceCulture); } @@ -262,7 +262,7 @@ namespace AquaMai.Resources { /// /// Looks up a localized string similar to Skip. /// - internal static string Skip { + public static string Skip { get { return ResourceManager.GetString("Skip", resourceCulture); } @@ -271,7 +271,7 @@ namespace AquaMai.Resources { /// /// Looks up a localized string similar to > Skipping incompatible patch: {0}. /// - internal static string SkipIncompatiblePatch { + public static string SkipIncompatiblePatch { get { return ResourceManager.GetString("SkipIncompatiblePatch", resourceCulture); } @@ -280,7 +280,7 @@ namespace AquaMai.Resources { /// /// Looks up a localized string similar to Speed. /// - internal static string Speed { + public static string Speed { get { return ResourceManager.GetString("Speed", resourceCulture); } @@ -289,7 +289,7 @@ namespace AquaMai.Resources { /// /// Looks up a localized string similar to Speed -. /// - internal static string SpeedDown { + public static string SpeedDown { get { return ResourceManager.GetString("SpeedDown", resourceCulture); } @@ -298,7 +298,7 @@ namespace AquaMai.Resources { /// /// Looks up a localized string similar to Speed Reset. /// - internal static string SpeedReset { + public static string SpeedReset { get { return ResourceManager.GetString("SpeedReset", resourceCulture); } @@ -307,7 +307,7 @@ namespace AquaMai.Resources { /// /// Looks up a localized string similar to Speed +. /// - internal static string SpeedUp { + public static string SpeedUp { get { return ResourceManager.GetString("SpeedUp", resourceCulture); } diff --git a/AquaMai/Resources/Locale.resx b/AquaMai/AquaMai.Core/Resources/Locale.resx similarity index 100% rename from AquaMai/Resources/Locale.resx rename to AquaMai/AquaMai.Core/Resources/Locale.resx diff --git a/AquaMai/Resources/Locale.zh.resx b/AquaMai/AquaMai.Core/Resources/Locale.zh.resx similarity index 100% rename from AquaMai/Resources/Locale.zh.resx rename to AquaMai/AquaMai.Core/Resources/Locale.zh.resx diff --git a/AquaMai/AquaMai.Core/Startup.cs b/AquaMai/AquaMai.Core/Startup.cs new file mode 100644 index 00000000..8695ad87 --- /dev/null +++ b/AquaMai/AquaMai.Core/Startup.cs @@ -0,0 +1,199 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Reflection; +using AquaMai.Core.Attributes; +using AquaMai.Core.Helpers; +using AquaMai.Core.Resources; +using MelonLoader; +using UnityEngine; + +namespace AquaMai.Core; + +public class Startup +{ + private static HarmonyLib.Harmony _harmony; + + private static bool _hasErrors; + + private enum ModLifecycleMethod + { + // Invoked before all patches are applied, including core patches + OnBeforeAllPatch, + // Invoked after all patches are applied + OnAfterAllPatch, + // Invoked before the current patch is applied + OnBeforePatch, + // Invoked after the current patch is applied + // Subclasses are treated as separate patches + OnAfterPatch, + // Invoked when an error occurs applying the current patch + // Lifecycle methods' excpetions not included + // Subclasses' error not included + OnPatchError + } + + private static bool ShouldEnableImplicitly(Type type) + { + var implicitEnableAttribute = type.GetCustomAttribute(); + if (implicitEnableAttribute == null) return false; + var referenceField = type.GetField(implicitEnableAttribute.MemberName, BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); + var referenceProperty = type.GetProperty(implicitEnableAttribute.MemberName, BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); + if (referenceField == null && referenceProperty == null) + { + throw new ArgumentException($"Field or property {implicitEnableAttribute.MemberName} not found in {type.FullName}"); + } + var referenceMemberValue = referenceField != null ? referenceField.GetValue(null) : referenceProperty.GetValue(null); + if ((bool)referenceMemberValue) + { + MelonLogger.Msg($"Enabled {type.FullName} implicitly"); + return true; + } + return false; + } + + private static void InvokeLifecycleMethod(Type type, ModLifecycleMethod methodName) + { + var method = type.GetMethod(methodName.ToString(), BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static); + if (method == null) + { + return; + } + var parameters = method.GetParameters(); + var arguments = parameters.Select(p => + { + if (p.ParameterType == typeof(HarmonyLib.Harmony)) return _harmony; + throw new InvalidOperationException($"Unsupported parameter type {p.ParameterType} in lifecycle method {type.FullName}.{methodName}"); + }).ToArray(); + try + { + method.Invoke(null, arguments); + } + catch (TargetInvocationException e) + { + MelonLogger.Error($"Failed to invoke lifecycle method {type.FullName}.{methodName}: {e.InnerException}"); + _hasErrors = true; + } + } + + private static void CollectWantedPatches(List wantedPatches, Type type) + { + if (EnableConditionHelper.ShouldSkipClass(type)) + { + return; + } + + wantedPatches.Add(type); + foreach (var nested in type.GetNestedTypes()) + { + CollectWantedPatches(wantedPatches, nested); + } + } + + private static void ApplyPatch(Type type) + { + MelonLogger.Msg($"> Applying {type}"); + try + { + InvokeLifecycleMethod(type, ModLifecycleMethod.OnBeforePatch); + _harmony.PatchAll(type); + InvokeLifecycleMethod(type, ModLifecycleMethod.OnAfterPatch); + } + catch (Exception e) + { + MelonLogger.Error($"Failed to patch {type}: {e}"); + InvokeLifecycleMethod(type, ModLifecycleMethod.OnPatchError); + _hasErrors = true; + } + } + + private static string ResolveLocale() + { + var localeConfigEntry = ConfigLoader.Config.ReflectionManager.GetEntry("General.Locale"); + var localeValue = (string)ConfigLoader.Config.GetEntryState(localeConfigEntry).Value; + return localeValue switch + { + "en" => localeValue, + "zh" => localeValue, + _ => Application.systemLanguage switch + { + SystemLanguage.Chinese or SystemLanguage.ChineseSimplified or SystemLanguage.ChineseTraditional => "zh", + SystemLanguage.English => "en", + _ => "en" + } + }; + } + + public static void Initialize(Assembly modsAssembly, HarmonyLib.Harmony harmony) + { + MelonLogger.Msg("Loading mod settings..."); + + var configLoaded = ConfigLoader.LoadConfig(modsAssembly); + var lang = ResolveLocale(); + if (configLoaded) + { + ConfigLoader.SaveConfig(lang); // Re-save the config as soon as possible + } + + _harmony = harmony; + + // Init locale with patching C# runtime + // https://stackoverflow.com/questions/1952638/single-assembly-multi-language-windows-forms-deployment-ilmerge-and-satellite-a + ApplyPatch(typeof(I18nSingleAssemblyHook)); + Locale.Culture = CultureInfo.GetCultureInfo(lang); // Must be called after I18nSingleAssemblyHook patched + + // The patch list is ordered + List wantedPatches = []; + + // Must be patched first to support [EnableIf(...)] and [EnableGameVersion(...)] + CollectWantedPatches(wantedPatches, typeof(EnableConditionHelper)); + // Core helpers patched first + CollectWantedPatches(wantedPatches, typeof(MessageHelper)); + CollectWantedPatches(wantedPatches, typeof(MusicDirHelper)); + CollectWantedPatches(wantedPatches, typeof(SharedInstances)); + CollectWantedPatches(wantedPatches, typeof(GuiSizes)); + CollectWantedPatches(wantedPatches, typeof(KeyListener)); + + // Collect patches based on the config + var config = ConfigLoader.Config; + foreach (var section in config.ReflectionManager.Sections) + { + var reflectionType = (Config.Reflection.SystemReflectionProvider.ReflectionType)section.Type; + var type = reflectionType.UnderlyingType; + if (!config.GetSectionState(section).Enabled && !ShouldEnableImplicitly(type)) continue; + CollectWantedPatches(wantedPatches, type); + } + + foreach (var type in wantedPatches) + { + InvokeLifecycleMethod(type, ModLifecycleMethod.OnBeforeAllPatch); + } + foreach (var type in wantedPatches) + { + ApplyPatch(type); + } + foreach (var type in wantedPatches) + { + InvokeLifecycleMethod(type, ModLifecycleMethod.OnAfterAllPatch); + } + + if (_hasErrors) + { + MelonLogger.Warning("========================================================================!!!\n" + Locale.LoadError); + MelonLogger.Warning("==========================================================================="); + } + +# if CI + MelonLogger.Warning(Locale.CiBuildAlertTitle); + MelonLogger.Warning(Locale.CiBuildAlertContent); +# endif + + MelonLogger.Msg(Locale.Loaded); + } + + public static void OnGUI() + { + GuiSizes.SetupStyles(); + } +} diff --git a/AquaMai/AquaMai.Mods/AquaMai.Mods.csproj b/AquaMai/AquaMai.Mods/AquaMai.Mods.csproj new file mode 100644 index 00000000..cb2fb8dd --- /dev/null +++ b/AquaMai/AquaMai.Mods/AquaMai.Mods.csproj @@ -0,0 +1,127 @@ + + + + Release + AnyCPU + {8731C0E0-53BE-4B1B-9828-193E738C6865} + Library + AquaMai.Mods + AquaMai.Mods + net472 + 512 + true + 12 + 414 + $(ProjectDir)../Libs/;$(AssemblySearchPaths) + $(ProjectDir)../Output/ + false + false + + + + false + None + true + prompt + 4 + true + false + + + + DEBUG + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AquaMai/AquaMai.Mods/DeprecationWarning.cs b/AquaMai/AquaMai.Mods/DeprecationWarning.cs new file mode 100644 index 00000000..4afacde8 --- /dev/null +++ b/AquaMai/AquaMai.Mods/DeprecationWarning.cs @@ -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."); + } + } +} diff --git a/AquaMai/Visual/CustomLogo.cs b/AquaMai/AquaMai.Mods/Fancy/CustomLogo.cs similarity index 58% rename from AquaMai/Visual/CustomLogo.cs rename to AquaMai/AquaMai.Mods/Fancy/CustomLogo.cs index 65cead94..54c127f9 100644 --- a/AquaMai/Visual/CustomLogo.cs +++ b/AquaMai/AquaMai.Mods/Fancy/CustomLogo.cs @@ -1,23 +1,37 @@ -using System; -using System.Collections.Generic; +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.Visual; +namespace AquaMai.Mods.Fancy; +[ConfigSection( + en: "Replace the \"SEGA\" and \"ALL.Net\" logos with custom ones.", + zh: "用自定义的图片替换「SEGA」和「ALL.Net」的标志")] public class CustomLogo { - private static List segaLogo = new(); - private static List allNetLogo = new(); + [ConfigEntry( + en: "Replace the \"SEGA\" logo with a random PNG image from this directory.", + zh: "从此目录中随机选择一张 PNG 图片用于「SEGA」标志")] + private static readonly string segaLogoDir = "LocalAssets/SegaLogo"; - public static void DoCustomPatch(HarmonyLib.Harmony h) + [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 segaLogo = []; + private readonly static List allNetLogo = []; + + public static void OnBeforePatch() { - EnumSprite(segaLogo, Path.Combine(Environment.CurrentDirectory, "LocalAssets", "SegaLogo")); - EnumSprite(allNetLogo, Path.Combine(Environment.CurrentDirectory, "LocalAssets", "AllNetLogo")); + EnumSprite(segaLogo, FileSystem.ResolvePath(segaLogoDir)); + EnumSprite(allNetLogo, FileSystem.ResolvePath(allNetLogoDir)); } private static void EnumSprite(List collection, string path) diff --git a/AquaMai/AquaMai.Mods/Fancy/CustomPlaceName.cs b/AquaMai/AquaMai.Mods/Fancy/CustomPlaceName.cs new file mode 100644 index 00000000..a54f3e72 --- /dev/null +++ b/AquaMai/AquaMai.Mods/Fancy/CustomPlaceName.cs @@ -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); + } +} diff --git a/AquaMai/Visual/CustomSkins.cs b/AquaMai/AquaMai.Mods/Fancy/CustomSkins.cs similarity index 95% rename from AquaMai/Visual/CustomSkins.cs rename to AquaMai/AquaMai.Mods/Fancy/CustomSkins.cs index de362869..46f6f422 100644 --- a/AquaMai/Visual/CustomSkins.cs +++ b/AquaMai/AquaMai.Mods/Fancy/CustomSkins.cs @@ -1,6 +1,7 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; +using AquaMai.Config.Attributes; +using AquaMai.Core.Helpers; using HarmonyLib; using MelonLoader; using Monitor; @@ -8,16 +9,28 @@ using Monitor.Game; using Process; using UnityEngine; -namespace AquaMai.Visual; +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 ImageExts = [".png", ".jpg", ".jpeg"]; private static readonly List SlideFanFields = ["_normalSlideFan", "_eachSlideFan", "_breakSlideFan", "_breakSlideFanEff"]; private static readonly List CustomTrackStartFields = ["_musicBase", "_musicTab", "_musicLvBase", "_musicLvText"]; private static Sprite customOutline; - private static Sprite[,] customSlideFan = new Sprite[4, 11]; + 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)]; @@ -95,9 +108,10 @@ public class CustomSkins [HarmonyPatch(typeof(GameNotePrefabContainer), "Initialize")] private static void LoadNoteSkin() { - if (!Directory.Exists(Path.Combine(Environment.CurrentDirectory, "LocalAssets", "Skins"))) return; + var resolvedDir = FileSystem.ResolvePath(skinsDir); + if (!Directory.Exists(resolvedDir)) return; - foreach (var laFile in Directory.EnumerateFiles(Path.Combine(Environment.CurrentDirectory, "LocalAssets", "Skins"))) + foreach (var laFile in Directory.EnumerateFiles(resolvedDir)) { if (!ImageExts.Contains(Path.GetExtension(laFile).ToLowerInvariant())) continue; var texture = new Texture2D(1, 1, TextureFormat.RGBA32, false); diff --git a/AquaMai/Visual/CustomTrackStartDiff.cs b/AquaMai/AquaMai.Mods/Fancy/CustomTrackStartDiff.cs similarity index 80% rename from AquaMai/Visual/CustomTrackStartDiff.cs rename to AquaMai/AquaMai.Mods/Fancy/CustomTrackStartDiff.cs index 1bd2740c..41f3623c 100644 --- a/AquaMai/Visual/CustomTrackStartDiff.cs +++ b/AquaMai/AquaMai.Mods/Fancy/CustomTrackStartDiff.cs @@ -1,12 +1,24 @@ using System.Collections.Generic; +using AquaMai.Config.Attributes; using HarmonyLib; using Monitor; using UI; using UnityEngine; using UnityEngine.UI; -namespace AquaMai.Visual; +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 { // 自定义在歌曲开始界面上显示的难度 (并不是真的自定义难度) diff --git a/AquaMai/AquaMai.Mods/Fancy/CustomVersionString.cs b/AquaMai/AquaMai.Mods/Fancy/CustomVersionString.cs new file mode 100644 index 00000000..cbada55d --- /dev/null +++ b/AquaMai/AquaMai.Mods/Fancy/CustomVersionString.cs @@ -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; + } +} diff --git a/AquaMai/AquaMai.Mods/Fancy/DemoMaster.cs b/AquaMai/AquaMai.Mods/Fancy/DemoMaster.cs new file mode 100644 index 00000000..12939c44 --- /dev/null +++ b/AquaMai/AquaMai.Mods/Fancy/DemoMaster.cs @@ -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.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; + } +} diff --git a/AquaMai/Visual/FixCircleSlideJudge.cs b/AquaMai/AquaMai.Mods/Fancy/GamePlay/AlignCircleSlideJudgeDisplay.cs similarity index 76% rename from AquaMai/Visual/FixCircleSlideJudge.cs rename to AquaMai/AquaMai.Mods/Fancy/GamePlay/AlignCircleSlideJudgeDisplay.cs index 64fdcb34..508e5e88 100644 --- a/AquaMai/Visual/FixCircleSlideJudge.cs +++ b/AquaMai/AquaMai.Mods/Fancy/GamePlay/AlignCircleSlideJudgeDisplay.cs @@ -1,16 +1,25 @@ using System; +using AquaMai.Config.Attributes; using HarmonyLib; using Manager; using Monitor; using UnityEngine; -namespace AquaMai.Visual; +namespace AquaMai.Mods.Fancy.GamePlay; -public class FixCircleSlideJudge +[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 里那样 - * 我觉得这个 Patch 算是无副作用的, 可以默认开启 */ [HarmonyPostfix] [HarmonyPatch(typeof(SlideRoot), "Initialize")] diff --git a/AquaMai/Visual/BreakSlideJudgeBlink.cs b/AquaMai/AquaMai.Mods/Fancy/GamePlay/BreakSlideJudgeBlink.cs similarity index 56% rename from AquaMai/Visual/BreakSlideJudgeBlink.cs rename to AquaMai/AquaMai.Mods/Fancy/GamePlay/BreakSlideJudgeBlink.cs index 3c8ef6cf..175f605c 100644 --- a/AquaMai/Visual/BreakSlideJudgeBlink.cs +++ b/AquaMai/AquaMai.Mods/Fancy/GamePlay/BreakSlideJudgeBlink.cs @@ -1,9 +1,19 @@ -using HarmonyLib; +using AquaMai.Config.Attributes; +using HarmonyLib; using Monitor; using UnityEngine; -namespace AquaMai.Visual; +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 { /* diff --git a/AquaMai/MaimaiDX2077/CustomNoteTypePatch.cs b/AquaMai/AquaMai.Mods/Fancy/GamePlay/CustomNoteTypes/CustomNoteTypes.cs similarity index 94% rename from AquaMai/MaimaiDX2077/CustomNoteTypePatch.cs rename to AquaMai/AquaMai.Mods/Fancy/GamePlay/CustomNoteTypes/CustomNoteTypes.cs index 62bb400b..0614c970 100644 --- a/AquaMai/MaimaiDX2077/CustomNoteTypePatch.cs +++ b/AquaMai/AquaMai.Mods/Fancy/GamePlay/CustomNoteTypes/CustomNoteTypes.cs @@ -3,6 +3,7 @@ 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; @@ -10,10 +11,16 @@ using Manager; using MelonLoader; using Monitor; using UnityEngine; +using AquaMai.Mods.Fancy.GamePlay.CustomNoteTypes.Libs; -namespace AquaMai.MaimaiDX2077; +namespace AquaMai.Mods.Fancy.GamePlay.CustomNoteTypes; -public class CustomNoteTypePatch +[ConfigCollapseNamespace] +[ConfigSection( + en: "Custom Note Types.", + zh: "自定义 Note 类型" +)] +public class CustomNoteTypes { /* * ========== ========== ========== ========== ========== ========== ========== ========== @@ -36,12 +43,12 @@ public class CustomNoteTypePatch public static int TotalMa2RecordCount = -1; public static int LastMa2RecordID = -1; public static Array Ma2FileRecordData; - - public static void DoCustomPatch(HarmonyLib.Harmony h) + + public static void OnAfterPatch() { var arrayTraverse = Traverse.Create(typeof(Ma2fileRecordID)).Field("s_Ma2fileRecord_Data"); var targetArray = arrayTraverse.GetValue(); - + var nextId = targetArray.Length; object[][] newEntries = [ @@ -51,7 +58,7 @@ public class CustomNoteTypePatch [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, @@ -60,7 +67,7 @@ public class CustomNoteTypePatch 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++) { @@ -73,12 +80,12 @@ public class CustomNoteTypePatch 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}"); @@ -97,10 +104,10 @@ public class CustomNoteTypePatch var item = Ma2FileRecordData.GetValue(i); if (Traverse.Create(item).Field("enumName").Value == enumName) { - __result = (Ma2fileRecordID.Def) i; + __result = (Ma2fileRecordID.Def)i; } } - + return false; } @@ -118,20 +125,20 @@ public class CustomNoteTypePatch AccessTools.Method(typeof(Ma2fileRecordID_Extension), "isValid"), ]; } - + public static IEnumerable Transpiler(IEnumerable instructions) { - + foreach (var inst in instructions) { if (inst.LoadsConstant(142)) { - var instNew = new CodeInstruction(OpCodes.Ldsfld, AccessTools.Field(typeof(CustomNoteTypePatch), "TotalMa2RecordCount")); + 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(CustomNoteTypePatch), "LastMa2RecordID")); + var instNew = new CodeInstruction(OpCodes.Ldsfld, AccessTools.Field(typeof(CustomNoteTypes), "LastMa2RecordID")); yield return instNew; } else @@ -146,7 +153,7 @@ public class CustomNoteTypePatch * ========== ========== ========== ========== ========== ========== ========== ========== * 以下内容是给新的 MA2 语法写解析器 */ - + /* * 给新建的 noteData 初始化应有的数据, 仅仅是照搬了 NotesReader.loadNote */ @@ -156,7 +163,7 @@ public class CustomNoteTypePatch 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.startButtonPos = MaiGeometry.MirrorInfo[(int)mirrorMode, record.getPos()]; noteData.index = index; var num = record.getGrid() % 96; if (num == 0) @@ -182,7 +189,7 @@ public class CustomNoteTypePatch noteData.indexNote = noteIndex; ++noteIndex; } - + /* * 给新建的 noteData 填入基本的 slide 相关数据, 仅仅是照搬了 NotesReader.loadNote */ @@ -193,7 +200,7 @@ public class CustomNoteTypePatch var slideData = noteData.slideData; var slideWaitLen = record.getSlideWaitLen(); var slideShootLen = record.getSlideShootLen(); - slideData.targetNote = MaiGeometry.MirrorInfo[(int) mirrorMode, record.getSlideEndPos()]; + 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); @@ -211,7 +218,7 @@ public class CustomNoteTypePatch // 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; @@ -243,13 +250,13 @@ public class CustomNoteTypePatch __result = flag; return false; } - + /* * ========== ========== ========== ========== ========== ========== ========== ========== * 以下内容是为了实现自定义 Slide * */ - + /* * 把 GetSlidePath 和 GetSlideHitArea 和 GetSlideLength 重定向到我可以控制的函数上, 并且多推几个参数进来 */ @@ -266,21 +273,21 @@ public class CustomNoteTypePatch AccessTools.Method(typeof(BreakStarNote), "Initialize"), ]; } - + public static IEnumerable Transpiler(IEnumerable instructions) { var methodGetSlidePath = AccessTools.Method(typeof(SlideManager), "GetSlidePath"); - var methodGetSlidePathRedirect = AccessTools.Method(typeof(CustomNoteTypePatch), "GetSlidePathRedirect"); + var methodGetSlidePathRedirect = AccessTools.Method(typeof(CustomNoteTypes), "GetSlidePathRedirect"); var methodGetSlideHitArea = AccessTools.Method(typeof(SlideManager), "GetSlideHitArea"); - var methodGetSlideHitAreaRedirect = AccessTools.Method(typeof(CustomNoteTypePatch), "GetSlideHitAreaRedirect"); + var methodGetSlideHitAreaRedirect = AccessTools.Method(typeof(CustomNoteTypes), "GetSlideHitAreaRedirect"); var methodGetSlideLength = AccessTools.Method(typeof(SlideManager), "GetSlideLength"); - var methodGetSlideLengthRedirect = AccessTools.Method(typeof(CustomNoteTypePatch), "GetSlideLengthRedirect"); + var methodGetSlideLengthRedirect = AccessTools.Method(typeof(CustomNoteTypes), "GetSlideLengthRedirect"); var fieldSlideData = AccessTools.Field(typeof(NoteData), "slideData"); var oldInstList = new List(instructions); var newInstList = new List(); CodeInstruction instToInject = null; - + for (var i = 0; i < oldInstList.Count; ++i) { var inst = oldInstList[i]; @@ -295,7 +302,7 @@ public class CustomNoteTypePatch // 所以就记录上一次 ldfld NoteData::slideData 的位置, 往前找一个 IL code // 找到的就是 load 这个 noteData 的位置 // 然后在后续调用 GetSlidePath 时, 先重复一遍 load 把这个 noteData 入栈, 然后重定向到一个新的函数上去 - instToInject = oldInstList[i-1]; + instToInject = oldInstList[i - 1]; newInstList.Add(inst); } else if (inst.Calls(methodGetSlidePath)) @@ -324,7 +331,7 @@ public class CustomNoteTypePatch return newInstList; } } - + public static List GetSlidePathRedirect(SlideManager instance, SlideType slideType, int start, int end, int starButton, NoteData noteData) @@ -351,7 +358,7 @@ public class CustomNoteTypePatch } return instance.GetSlideHitArea(slideType, start, end, starButton); } - + public static float GetSlideLengthRedirect(SlideManager instance, SlideType slideType, int start, int end, NoteData noteData) { @@ -364,9 +371,9 @@ public class CustomNoteTypePatch } return instance.GetSlideLength(slideType, start, end); } - - - + + + [HarmonyPatch] public static class Debuging { @@ -383,7 +390,7 @@ public class CustomNoteTypePatch // AccessTools.Method(typeof(SlideJudge), "SetJudgeType"), ]; } - + public static void Prefix(MethodBase __originalMethod, object[] __args) { var msg = "[CustomNoteType] Before "; @@ -419,8 +426,8 @@ public class CustomNoteTypePatch { return $""; } - + return value.ToString(); } } -} \ No newline at end of file +} diff --git a/AquaMai/MaimaiDX2077/CustomSlideNoteData.cs b/AquaMai/AquaMai.Mods/Fancy/GamePlay/CustomNoteTypes/Libs/CustomSlideNoteData.cs similarity index 96% rename from AquaMai/MaimaiDX2077/CustomSlideNoteData.cs rename to AquaMai/AquaMai.Mods/Fancy/GamePlay/CustomNoteTypes/Libs/CustomSlideNoteData.cs index 1abc3f2f..a7531e8a 100644 --- a/AquaMai/MaimaiDX2077/CustomSlideNoteData.cs +++ b/AquaMai/AquaMai.Mods/Fancy/GamePlay/CustomNoteTypes/Libs/CustomSlideNoteData.cs @@ -5,7 +5,7 @@ using Manager; using MelonLoader; using UnityEngine; -namespace AquaMai.MaimaiDX2077; +namespace AquaMai.Mods.Fancy.GamePlay.CustomNoteTypes.Libs; public class CustomSlideNoteData: NoteData { @@ -48,4 +48,4 @@ public class CustomSlideNoteData: NoteData return true; } -} \ No newline at end of file +} diff --git a/AquaMai/MaimaiDX2077/MaiGeometry.cs b/AquaMai/AquaMai.Mods/Fancy/GamePlay/CustomNoteTypes/Libs/MaiGeometry.cs similarity index 97% rename from AquaMai/MaimaiDX2077/MaiGeometry.cs rename to AquaMai/AquaMai.Mods/Fancy/GamePlay/CustomNoteTypes/Libs/MaiGeometry.cs index d0c4fab7..7866424b 100644 --- a/AquaMai/MaimaiDX2077/MaiGeometry.cs +++ b/AquaMai/AquaMai.Mods/Fancy/GamePlay/CustomNoteTypes/Libs/MaiGeometry.cs @@ -1,9 +1,7 @@ using System; -using System.Collections.Generic; using System.Numerics; -using Manager; -namespace AquaMai.MaimaiDX2077; +namespace AquaMai.Mods.Fancy.GamePlay.CustomNoteTypes.Libs; public static class MaiGeometry { @@ -106,4 +104,4 @@ public static class MaiGeometry return new Tuple(new CircleStruct(center, TransferRadius), Math.IEEERemainder(startAngle, Math.PI * 2), Math.IEEERemainder(endAngle, Math.PI * 2)); } -} \ No newline at end of file +} diff --git a/AquaMai/MaimaiDX2077/ParametricSlidePath.cs b/AquaMai/AquaMai.Mods/Fancy/GamePlay/CustomNoteTypes/Libs/ParametricSlidePath.cs similarity index 99% rename from AquaMai/MaimaiDX2077/ParametricSlidePath.cs rename to AquaMai/AquaMai.Mods/Fancy/GamePlay/CustomNoteTypes/Libs/ParametricSlidePath.cs index 5b928025..26832c42 100644 --- a/AquaMai/MaimaiDX2077/ParametricSlidePath.cs +++ b/AquaMai/AquaMai.Mods/Fancy/GamePlay/CustomNoteTypes/Libs/ParametricSlidePath.cs @@ -5,7 +5,7 @@ using System.Numerics; using DB; using Manager; -namespace AquaMai.MaimaiDX2077; +namespace AquaMai.Mods.Fancy.GamePlay.CustomNoteTypes.Libs; public class ParametricSlidePath { @@ -223,4 +223,4 @@ public class ParametricSlidePath return SlideType.Slide_Straight; } -} \ No newline at end of file +} diff --git a/AquaMai/MaimaiDX2077/SlideCodeParser.cs b/AquaMai/AquaMai.Mods/Fancy/GamePlay/CustomNoteTypes/Libs/SlideCodeParser.cs similarity index 99% rename from AquaMai/MaimaiDX2077/SlideCodeParser.cs rename to AquaMai/AquaMai.Mods/Fancy/GamePlay/CustomNoteTypes/Libs/SlideCodeParser.cs index fc3ad74e..1aaeb7a4 100644 --- a/AquaMai/MaimaiDX2077/SlideCodeParser.cs +++ b/AquaMai/AquaMai.Mods/Fancy/GamePlay/CustomNoteTypes/Libs/SlideCodeParser.cs @@ -4,7 +4,7 @@ using System.Linq; using System.Numerics; using MelonLoader; -namespace AquaMai.MaimaiDX2077; +namespace AquaMai.Mods.Fancy.GamePlay.CustomNoteTypes.Libs; public static class SlideCodeParser { @@ -257,4 +257,4 @@ public static class SlideCodeParser return null; } } -} \ No newline at end of file +} diff --git a/AquaMai/MaimaiDX2077/SlideDataBuilder.cs b/AquaMai/AquaMai.Mods/Fancy/GamePlay/CustomNoteTypes/Libs/SlideDataBuilder.cs similarity index 99% rename from AquaMai/MaimaiDX2077/SlideDataBuilder.cs rename to AquaMai/AquaMai.Mods/Fancy/GamePlay/CustomNoteTypes/Libs/SlideDataBuilder.cs index 857db2c2..6f6abb5b 100644 --- a/AquaMai/MaimaiDX2077/SlideDataBuilder.cs +++ b/AquaMai/AquaMai.Mods/Fancy/GamePlay/CustomNoteTypes/Libs/SlideDataBuilder.cs @@ -1,13 +1,11 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Numerics; using DB; using Manager; -using MelonLoader; using Vector4 = UnityEngine.Vector4; -namespace AquaMai.MaimaiDX2077; +namespace AquaMai.Mods.Fancy.GamePlay.CustomNoteTypes.Libs; public static class SlideDataBuilder { @@ -472,4 +470,4 @@ public static class SlideDataBuilder } return hitAreaList; } -} \ No newline at end of file +} diff --git a/AquaMai/MaimaiDX2077/SlidePathGenerator.cs b/AquaMai/AquaMai.Mods/Fancy/GamePlay/CustomNoteTypes/Libs/SlidePathGenerator.cs similarity index 98% rename from AquaMai/MaimaiDX2077/SlidePathGenerator.cs rename to AquaMai/AquaMai.Mods/Fancy/GamePlay/CustomNoteTypes/Libs/SlidePathGenerator.cs index a22484f6..788567a8 100644 --- a/AquaMai/MaimaiDX2077/SlidePathGenerator.cs +++ b/AquaMai/AquaMai.Mods/Fancy/GamePlay/CustomNoteTypes/Libs/SlidePathGenerator.cs @@ -1,9 +1,8 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Numerics; -namespace AquaMai.MaimaiDX2077; +namespace AquaMai.Mods.Fancy.GamePlay.CustomNoteTypes.Libs; public class SlidePathGenerator { @@ -129,4 +128,4 @@ public class SlidePathGenerator { return new ParametricSlidePath(PathSegments); } -} \ No newline at end of file +} diff --git a/AquaMai/Visual/DisableTrackStartTabs.cs b/AquaMai/AquaMai.Mods/Fancy/GamePlay/DisableTrackStartTabs.cs similarity index 80% rename from AquaMai/Visual/DisableTrackStartTabs.cs rename to AquaMai/AquaMai.Mods/Fancy/GamePlay/DisableTrackStartTabs.cs index 92823a48..439b8579 100644 --- a/AquaMai/Visual/DisableTrackStartTabs.cs +++ b/AquaMai/AquaMai.Mods/Fancy/GamePlay/DisableTrackStartTabs.cs @@ -1,11 +1,21 @@ -using HarmonyLib; +using AquaMai.Config.Attributes; +using HarmonyLib; using Monitor; using TMPro; using UI; using UnityEngine; -namespace AquaMai.Visual; +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 diff --git a/AquaMai/Fix/ExtendNotesPool.cs b/AquaMai/AquaMai.Mods/Fancy/GamePlay/ExtendNotesPool.cs similarity index 92% rename from AquaMai/Fix/ExtendNotesPool.cs rename to AquaMai/AquaMai.Mods/Fancy/GamePlay/ExtendNotesPool.cs index 5a4edd07..245a01b0 100644 --- a/AquaMai/Fix/ExtendNotesPool.cs +++ b/AquaMai/AquaMai.Mods/Fancy/GamePlay/ExtendNotesPool.cs @@ -1,18 +1,26 @@ using System.Collections.Generic; -using AquaMai.Attributes; +using AquaMai.Core.Attributes; +using AquaMai.Config.Attributes; using HarmonyLib; using MAI2.Util; using Manager; -using MelonLoader; using Monitor; using Monitor.Game; using UnityEngine; -namespace AquaMai.Fix; +namespace AquaMai.Mods.Fancy.GamePlay; -[GameVersion(23000)] +[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, @@ -33,7 +41,7 @@ public class ExtendNotesPool List ____arrowObjectList, List ____breakArrowObjectList ) { - for (var i = 0; i < AquaMai.AppConfig.Fix.ExtendNotesPool; i++) + for (var i = 0; i < count; i++) { var tapNote = Object.Instantiate(GameNotePrefabContainer.Tap, ____tapListParent.transform); tapNote.gameObject.SetActive(false); diff --git a/AquaMai/AquaMai.Mods/Fancy/GamePlay/FanJudgeFlip.cs b/AquaMai/AquaMai.Mods/Fancy/GamePlay/FanJudgeFlip.cs new file mode 100644 index 00000000..bad70ed8 --- /dev/null +++ b/AquaMai/AquaMai.Mods/Fancy/GamePlay/FanJudgeFlip.cs @@ -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); + } + } + } +} diff --git a/AquaMai/UX/HideHanabi.cs b/AquaMai/AquaMai.Mods/Fancy/GamePlay/HideHanabi.cs similarity index 73% rename from AquaMai/UX/HideHanabi.cs rename to AquaMai/AquaMai.Mods/Fancy/GamePlay/HideHanabi.cs index 4a3a6819..e84e02d0 100644 --- a/AquaMai/UX/HideHanabi.cs +++ b/AquaMai/AquaMai.Mods/Fancy/GamePlay/HideHanabi.cs @@ -1,10 +1,14 @@ -using Fx; +using AquaMai.Config.Attributes; +using Fx; using HarmonyLib; using Monitor; using UnityEngine; -namespace AquaMai.UX; +namespace AquaMai.Mods.Fancy.GamePlay; +[ConfigSection( + en: "Hide hanabi completely.", + zh: "完全隐藏烟花")] public class HideHanabi { [HarmonyPatch(typeof(TapCEffect), "SetUpParticle")] diff --git a/AquaMai/Visual/JudgeDisplay4B.cs b/AquaMai/AquaMai.Mods/Fancy/GamePlay/JudgeDisplay4B.cs similarity index 80% rename from AquaMai/Visual/JudgeDisplay4B.cs rename to AquaMai/AquaMai.Mods/Fancy/GamePlay/JudgeDisplay4B.cs index abaceaac..29c64edd 100644 --- a/AquaMai/Visual/JudgeDisplay4B.cs +++ b/AquaMai/AquaMai.Mods/Fancy/GamePlay/JudgeDisplay4B.cs @@ -1,9 +1,21 @@ -using HarmonyLib; +using AquaMai.Config.Attributes; +using HarmonyLib; using Monitor; using UnityEngine; -namespace AquaMai.Visual; +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 { // 精确到子判定的自定义判定显示, 需要启用自定义皮肤功能 (理论上不启用自定义皮肤不会崩游戏, 只不过此时这个功能显然不会生效) diff --git a/AquaMai/AquaMai.Mods/Fancy/GamePlay/RealisticRandomJudge.cs b/AquaMai/AquaMai.Mods/Fancy/GamePlay/RealisticRandomJudge.cs new file mode 100644 index 00000000..a73ab94e --- /dev/null +++ b/AquaMai/AquaMai.Mods/Fancy/GamePlay/RealisticRandomJudge.cs @@ -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; + } +} diff --git a/AquaMai/Visual/SlideArrowAnimation.cs b/AquaMai/AquaMai.Mods/Fancy/GamePlay/SlideArrowAnimation.cs similarity index 93% rename from AquaMai/Visual/SlideArrowAnimation.cs rename to AquaMai/AquaMai.Mods/Fancy/GamePlay/SlideArrowAnimation.cs index 7439ca25..16db4308 100644 --- a/AquaMai/Visual/SlideArrowAnimation.cs +++ b/AquaMai/AquaMai.Mods/Fancy/GamePlay/SlideArrowAnimation.cs @@ -1,5 +1,5 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; +using AquaMai.Config.Attributes; using HarmonyLib; using Manager; using Monitor; @@ -7,8 +7,11 @@ using Monitor.Game; using Process; using UnityEngine; -namespace AquaMai.Visual; +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 _animatingSpriteRenderers = []; @@ -112,4 +115,4 @@ public class SlideArrowAnimation { _animatingSpriteRenderers.Clear(); } -} \ No newline at end of file +} diff --git a/AquaMai/Visual/SlideFadeInTweak.cs b/AquaMai/AquaMai.Mods/Fancy/GamePlay/SlideFadeInTweak.cs similarity index 94% rename from AquaMai/Visual/SlideFadeInTweak.cs rename to AquaMai/AquaMai.Mods/Fancy/GamePlay/SlideFadeInTweak.cs index 26f9dec0..1a597c52 100644 --- a/AquaMai/Visual/SlideFadeInTweak.cs +++ b/AquaMai/AquaMai.Mods/Fancy/GamePlay/SlideFadeInTweak.cs @@ -1,13 +1,17 @@ using System; using System.Collections.Generic; +using AquaMai.Config.Attributes; using HarmonyLib; using MAI2.Util; using Manager; using Monitor; using UnityEngine; -namespace AquaMai.Visual; +namespace AquaMai.Mods.Fancy.GamePlay; +[ConfigSection( + zh: "让星星在启动拍等待期间从 50% 透明度渐入为 100%,取代原本在击打星星头时就完成渐入", + en: "Slides will fade in instead of instantly appearing.")] public class SlideFadeInTweak { [HarmonyPrefix] @@ -122,4 +126,4 @@ public class SlideFadeInTweak return false; } -} \ No newline at end of file +} diff --git a/AquaMai/Visual/SlideLayerReverse.cs b/AquaMai/AquaMai.Mods/Fancy/GamePlay/SlideLayerReverse.cs similarity index 84% rename from AquaMai/Visual/SlideLayerReverse.cs rename to AquaMai/AquaMai.Mods/Fancy/GamePlay/SlideLayerReverse.cs index 1e6958ab..689b32a6 100644 --- a/AquaMai/Visual/SlideLayerReverse.cs +++ b/AquaMai/AquaMai.Mods/Fancy/GamePlay/SlideLayerReverse.cs @@ -1,10 +1,20 @@ using System.Collections.Generic; +using AquaMai.Config.Attributes; using HarmonyLib; using Monitor; using UnityEngine; -namespace AquaMai.Visual; +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] @@ -63,4 +73,4 @@ public class SlideLayerReverse renderer.sortingOrder = 1000 + orderBase + lastIdx - index; } } -} \ No newline at end of file +} diff --git a/AquaMai/Visual/TrackStartProcessTweak.cs b/AquaMai/AquaMai.Mods/Fancy/GamePlay/TrackStartProcessTweak.cs similarity index 88% rename from AquaMai/Visual/TrackStartProcessTweak.cs rename to AquaMai/AquaMai.Mods/Fancy/GamePlay/TrackStartProcessTweak.cs index a95a608e..0774d9df 100644 --- a/AquaMai/Visual/TrackStartProcessTweak.cs +++ b/AquaMai/AquaMai.Mods/Fancy/GamePlay/TrackStartProcessTweak.cs @@ -1,9 +1,19 @@ -using HarmonyLib; +using AquaMai.Config.Attributes; +using HarmonyLib; using Process; using UnityEngine; -namespace AquaMai.Visual; +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 录谱面确认时用得到, 顺手也写进来了 diff --git a/AquaMai/UX/HideMask.cs b/AquaMai/AquaMai.Mods/Fancy/HideMask.cs similarity index 61% rename from AquaMai/UX/HideMask.cs rename to AquaMai/AquaMai.Mods/Fancy/HideMask.cs index 18c26680..013ee7ab 100644 --- a/AquaMai/UX/HideMask.cs +++ b/AquaMai/AquaMai.Mods/Fancy/HideMask.cs @@ -1,8 +1,12 @@ -using HarmonyLib; +using AquaMai.Config.Attributes; +using HarmonyLib; using UnityEngine; -namespace AquaMai.UX; +namespace AquaMai.Mods.Fancy; +[ConfigSection( + en: "Remove the circle mask of the game screen.", + zh: "移除游戏画面的圆形遮罩")] public class HideMask { [HarmonyPrefix] diff --git a/AquaMai/AquaMai.Mods/Fancy/README.md b/AquaMai/AquaMai.Mods/Fancy/README.md new file mode 100644 index 00000000..2f4f6dbc --- /dev/null +++ b/AquaMai/AquaMai.Mods/Fancy/README.md @@ -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. diff --git a/AquaMai/AquaMai.Mods/Fancy/RandomBgm.cs b/AquaMai/AquaMai.Mods/Fancy/RandomBgm.cs new file mode 100644 index 00000000..bec48456 --- /dev/null +++ b/AquaMai/AquaMai.Mods/Fancy/RandomBgm.cs @@ -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 _acbs = new List(); + + [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.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; + } + } +} diff --git a/AquaMai/AquaMai.Mods/Fancy/Triggers.cs b/AquaMai/AquaMai.Mods/Fancy/Triggers.cs new file mode 100644 index 00000000..7b4ceaf8 --- /dev/null +++ b/AquaMai/AquaMai.Mods/Fancy/Triggers.cs @@ -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(); + } +} diff --git a/AquaMai/Fix/BasicFix.cs b/AquaMai/AquaMai.Mods/Fix/Common.cs similarity index 62% rename from AquaMai/Fix/BasicFix.cs rename to AquaMai/AquaMai.Mods/Fix/Common.cs index 86b9c049..f4e1c54e 100644 --- a/AquaMai/Fix/BasicFix.cs +++ b/AquaMai/AquaMai.Mods/Fix/Common.cs @@ -1,14 +1,20 @@ using System.Net; using HarmonyLib; using Manager; -using Monitor.MusicSelect.ChainList; using Net; using UnityEngine; +using AquaMai.Config.Attributes; +using AquaMai.Core.Attributes; -namespace AquaMai.Fix; +namespace AquaMai.Mods.Fix; -public class BasicFix +[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() @@ -16,6 +22,10 @@ public class BasicFix 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) @@ -24,6 +34,7 @@ public class BasicFix return false; } + [EnableIf(nameof(fixDebugInput))] [HarmonyPrefix] [HarmonyPatch(typeof(DebugInput), "GetKeyDown")] private static bool GetKeyDown(ref bool __result, KeyCode name) @@ -32,6 +43,7 @@ public class BasicFix return false; } + [EnableIf(nameof(fixDebugInput))] [HarmonyPrefix] [HarmonyPatch(typeof(DebugInput), "GetMouseButton")] private static bool GetMouseButton(ref bool __result, int button) @@ -40,6 +52,7 @@ public class BasicFix return false; } + [EnableIf(nameof(fixDebugInput))] [HarmonyPrefix] [HarmonyPatch(typeof(DebugInput), "GetMouseButtonDown")] private static bool GetMouseButtonDown(ref bool __result, int button) @@ -48,6 +61,10 @@ public class BasicFix return false; } + [ConfigEntry] + private readonly static bool bypassCakeHashCheck = true; + + [EnableIf(nameof(bypassCakeHashCheck))] [HarmonyPostfix] [HarmonyPatch(typeof(NetHttpClient), MethodType.Constructor)] private static void OnNetHttpClientConstructor(NetHttpClient __instance) @@ -60,6 +77,10 @@ public class BasicFix } } + [ConfigEntry] + private readonly static bool restoreCertificateValidation = true; + + [EnableIf(nameof(restoreCertificateValidation))] [HarmonyPostfix] [HarmonyPatch(typeof(NetHttpClient), "Create")] private static void OnNetHttpClientCreate() @@ -68,27 +89,41 @@ public class BasicFix ServicePointManager.ServerCertificateValidationCallback = null; } + [ConfigEntry] + private readonly static bool forceNonTarget = true; + + [EnableIf(nameof(forceNonTarget))] [HarmonyPrefix] [HarmonyPatch(typeof(MAI2System.Config), "IsTarget", MethodType.Getter)] - private static bool ForceNonTarget(ref bool __result) + private static bool PreIsTarget(ref bool __result) { - // Who teaching others to set Target=1?! + // 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 ForceIgnoreError(ref bool __result) + private static bool PreIsIgnoreError(ref bool __result) { __result = true; return false; } - public static void DoCustomPatch(HarmonyLib.Harmony h) + [ConfigEntry] + private readonly static bool bypassSpecialNumCheck = true; + + public static void OnAfterPatch(HarmonyLib.Harmony h) { - if (typeof(GameManager).GetMethod("CalcSpecialNum") is null) return; - h.PatchAll(typeof(CalcSpecialNumPatch)); + if (bypassSpecialNumCheck) + { + if (typeof(GameManager).GetMethod("CalcSpecialNum") is null) return; + h.PatchAll(typeof(CalcSpecialNumPatch)); + } } private class CalcSpecialNumPatch diff --git a/AquaMai/Fix/DebugFeature.cs b/AquaMai/AquaMai.Mods/Fix/DebugFeature.cs similarity index 96% rename from AquaMai/Fix/DebugFeature.cs rename to AquaMai/AquaMai.Mods/Fix/DebugFeature.cs index e9ec44ea..fb4b10aa 100644 --- a/AquaMai/Fix/DebugFeature.cs +++ b/AquaMai/AquaMai.Mods/Fix/DebugFeature.cs @@ -1,5 +1,6 @@ using System; using System.Reflection; +using AquaMai.Config.Attributes; using HarmonyLib; using MAI2.Util; using Manager; @@ -8,8 +9,9 @@ using Monitor; using Process; using UnityEngine; -namespace AquaMai.Fix; +namespace AquaMai.Mods.Fix; +[ConfigSection(exampleHidden: true, defaultOn: true)] public class DebugFeature { public static bool IsPolyfill { get; private set; } @@ -29,7 +31,7 @@ public class DebugFeature PolyFill.timer = 0; } - public static void DoCustomPatch(HarmonyLib.Harmony h) + public static void OnBeforePatch(HarmonyLib.Harmony h) { var original = typeof(GameProcess).GetField("debugFeature", BindingFlags.NonPublic | BindingFlags.Instance); if (original is null) @@ -90,7 +92,7 @@ public class DebugFeature public static double CurrentPlayMsec { - [Obsolete("不要用它,它有问题。用 PractiseMode.CurrentPlayMsec")] + [Obsolete("不要用它,它有问题。用 PracticeMode.CurrentPlayMsec")] get { if (IsPolyfill) diff --git a/AquaMai/AquaMai.Mods/Fix/DisableReboot.cs b/AquaMai/AquaMai.Mods/Fix/DisableReboot.cs new file mode 100644 index 00000000..04effc6f --- /dev/null +++ b/AquaMai/AquaMai.Mods/Fix/DisableReboot.cs @@ -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; + } +} diff --git a/AquaMai/Fix/FixCheckAuth.cs b/AquaMai/AquaMai.Mods/Fix/FixCheckAuth.cs similarity index 80% rename from AquaMai/Fix/FixCheckAuth.cs rename to AquaMai/AquaMai.Mods/Fix/FixCheckAuth.cs index 1b350991..136424c1 100644 --- a/AquaMai/Fix/FixCheckAuth.cs +++ b/AquaMai/AquaMai.Mods/Fix/FixCheckAuth.cs @@ -1,10 +1,12 @@ using AMDaemon.Allnet; +using AquaMai.Config.Attributes; using HarmonyLib; using Manager; using Manager.Operation; -namespace AquaMai.Fix; +namespace AquaMai.Mods.Fix; +[ConfigSection(exampleHidden: true, defaultOn: true)] public class FixCheckAuth { [HarmonyPostfix] diff --git a/AquaMai/Fix/FixConnSlide.cs b/AquaMai/AquaMai.Mods/Fix/FixConnSlide.cs similarity index 93% rename from AquaMai/Fix/FixConnSlide.cs rename to AquaMai/AquaMai.Mods/Fix/FixConnSlide.cs index 99a251f3..59b38572 100644 --- a/AquaMai/Fix/FixConnSlide.cs +++ b/AquaMai/AquaMai.Mods/Fix/FixConnSlide.cs @@ -1,14 +1,14 @@ using System.Collections.Generic; using System.Reflection; -using AquaMai.Attributes; +using AquaMai.Core.Attributes; +using AquaMai.Config.Attributes; using HarmonyLib; using Manager; -using MelonLoader; -using Monitor; -namespace AquaMai.Fix; +namespace AquaMai.Mods.Fix.Legacy; -[GameVersion(23000)] +[ConfigSection(exampleHidden: true, defaultOn: true)] +[EnableGameVersion(23000)] public class FixConnSlide { /* 这个 Patch 用于修复以下 bug: diff --git a/AquaMai/Visual/FixLevelDisplay.cs b/AquaMai/AquaMai.Mods/Fix/FixLevelDisplay.cs similarity index 90% rename from AquaMai/Visual/FixLevelDisplay.cs rename to AquaMai/AquaMai.Mods/Fix/FixLevelDisplay.cs index 95eeacb2..7eab928a 100644 --- a/AquaMai/Visual/FixLevelDisplay.cs +++ b/AquaMai/AquaMai.Mods/Fix/FixLevelDisplay.cs @@ -1,4 +1,5 @@ -using AquaMai.Attributes; +using AquaMai.Core.Attributes; +using AquaMai.Config.Attributes; using HarmonyLib; using MAI2.Util; using Manager; @@ -6,11 +7,15 @@ using Monitor; using Monitor.MusicSelect.ChainList; using UnityEngine; -namespace AquaMai.Fix; +namespace AquaMai.Mods.Fix; -[GameVersion(24000)] +[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) diff --git a/AquaMai/Visual/FixSlideAutoPlay.cs b/AquaMai/AquaMai.Mods/Fix/FixSlideAutoPlay.cs similarity index 97% rename from AquaMai/Visual/FixSlideAutoPlay.cs rename to AquaMai/AquaMai.Mods/Fix/FixSlideAutoPlay.cs index ed04d735..75c28c96 100644 --- a/AquaMai/Visual/FixSlideAutoPlay.cs +++ b/AquaMai/AquaMai.Mods/Fix/FixSlideAutoPlay.cs @@ -1,10 +1,12 @@ using System.Collections.Generic; +using AquaMai.Config.Attributes; using HarmonyLib; using Manager; using Monitor; -namespace AquaMai.Visual; +namespace AquaMai.Mods.Fix; +[ConfigSection(exampleHidden: true, defaultOn: true)] public class FixSlideAutoPlay { /* 这个 Patch 用于修复以下 bug: diff --git a/AquaMai/Fix/FestivalQuickRetryFix.cs b/AquaMai/AquaMai.Mods/Fix/Legacy/FixQuickRetry130.cs similarity index 78% rename from AquaMai/Fix/FestivalQuickRetryFix.cs rename to AquaMai/AquaMai.Mods/Fix/Legacy/FixQuickRetry130.cs index a41b6986..ea67cdc2 100644 --- a/AquaMai/Fix/FestivalQuickRetryFix.cs +++ b/AquaMai/AquaMai.Mods/Fix/Legacy/FixQuickRetry130.cs @@ -1,11 +1,13 @@ -using AquaMai.Attributes; +using AquaMai.Core.Attributes; +using AquaMai.Config.Attributes; using HarmonyLib; using Manager; -namespace AquaMai.Fix; +namespace AquaMai.Mods.Fix.Legacy; -[GameVersion(23000, 23499)] -public class FestivalQuickRetryFix +[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 diff --git a/AquaMai/AquaMai.Mods/Fix/README.md b/AquaMai/AquaMai.Mods/Fix/README.md new file mode 100644 index 00000000..047dd121 --- /dev/null +++ b/AquaMai/AquaMai.Mods/Fix/README.md @@ -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. diff --git a/AquaMai/AquaMai.Mods/Fix/Stability/FixMissingCharaCrash.cs b/AquaMai/AquaMai.Mods/Fix/Stability/FixMissingCharaCrash.cs new file mode 100644 index 00000000..6baca63f --- /dev/null +++ b/AquaMai/AquaMai.Mods/Fix/Stability/FixMissingCharaCrash.cs @@ -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 ____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; + } +} diff --git a/AquaMai/AquaMai.Mods/GameSettings/CreditConfig.cs b/AquaMai/AquaMai.Mods/GameSettings/CreditConfig.cs new file mode 100644 index 00000000..132de3d3 --- /dev/null +++ b/AquaMai/AquaMai.Mods/GameSettings/CreditConfig.cs @@ -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; + } +} diff --git a/AquaMai/Fix/ForceAsServer.cs b/AquaMai/AquaMai.Mods/GameSettings/ForceAsServer.cs similarity index 66% rename from AquaMai/Fix/ForceAsServer.cs rename to AquaMai/AquaMai.Mods/GameSettings/ForceAsServer.cs index 71ddbc26..814fc3ee 100644 --- a/AquaMai/Fix/ForceAsServer.cs +++ b/AquaMai/AquaMai.Mods/GameSettings/ForceAsServer.cs @@ -1,8 +1,12 @@ using AMDaemon; +using AquaMai.Config.Attributes; using HarmonyLib; -namespace AquaMai.Fix; +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] diff --git a/AquaMai/AquaMai.Mods/GameSettings/JudgeAdjust.cs b/AquaMai/AquaMai.Mods/GameSettings/JudgeAdjust.cs new file mode 100644 index 00000000..c54adb78 --- /dev/null +++ b/AquaMai/AquaMai.Mods/GameSettings/JudgeAdjust.cs @@ -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); + } +} diff --git a/AquaMai/AquaMai.Mods/GameSettings/README.md b/AquaMai/AquaMai.Mods/GameSettings/README.md new file mode 100644 index 00000000..8aa1d897 --- /dev/null +++ b/AquaMai/AquaMai.Mods/GameSettings/README.md @@ -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. diff --git a/AquaMai/AquaMai.Mods/GameSettings/TouchSensitivity.cs b/AquaMai/AquaMai.Mods/GameSettings/TouchSensitivity.cs new file mode 100644 index 00000000..181901cf --- /dev/null +++ b/AquaMai/AquaMai.Mods/GameSettings/TouchSensitivity.cs @@ -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 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"); + } +} diff --git a/AquaMai/AquaMai.Mods/GameSystem/Assets/Fonts.cs b/AquaMai/AquaMai.Mods/GameSystem/Assets/Fonts.cs new file mode 100644 index 00000000..e05f6218 --- /dev/null +++ b/AquaMai/AquaMai.Mods/GameSystem/Assets/Fonts.cs @@ -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 fontAssets = []; + private static readonly List processedFonts = []; + + private static TMP_FontAsset replacementFontAsset; + private static List 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); + } + } +} diff --git a/AquaMai/AquaMai.Mods/GameSystem/Assets/LoadAssetBundleWithoutManifest.cs b/AquaMai/AquaMai.Mods/GameSystem/Assets/LoadAssetBundleWithoutManifest.cs new file mode 100644 index 00000000..02b7f7ac --- /dev/null +++ b/AquaMai/AquaMai.Mods/GameSystem/Assets/LoadAssetBundleWithoutManifest.cs @@ -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 abFiles = new HashSet(); + + [HarmonyPostfix] + [HarmonyPatch(typeof(OptionDataManager), "CheckAssetBundle")] + public static void PostCheckAssetBundle(ref Safe.ReadonlySortedDictionary 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; + } +} diff --git a/AquaMai/UX/LoadAssetsPng.cs b/AquaMai/AquaMai.Mods/GameSystem/Assets/LoadLocalImages.cs similarity index 91% rename from AquaMai/UX/LoadAssetsPng.cs rename to AquaMai/AquaMai.Mods/GameSystem/Assets/LoadLocalImages.cs index 5a3f5a9d..72152de9 100644 --- a/AquaMai/UX/LoadAssetsPng.cs +++ b/AquaMai/AquaMai.Mods/GameSystem/Assets/LoadLocalImages.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -10,26 +9,31 @@ using MAI2.Util; using Manager; using MelonLoader; using Monitor; -using Object = UnityEngine.Object; +using AquaMai.Config.Attributes; +using AquaMai.Core.Helpers; -namespace AquaMai.UX; +namespace AquaMai.Mods.GameSystem.Assets; -public class LoadAssetsPng +[ConfigSection( + en: "Load asset images from the configured directory (for self-made charts).", + zh: "从指定目录下加载资源图片(自制谱用)")] +public class LoadLocalImages { - private static string[] imageExts = [".jpg", ".png", ".jpeg"]; - private static Dictionary jacketPaths = new(); - private static Dictionary framePaths = new(); - private static Dictionary platePaths = new(); - private static Dictionary framemaskPaths = new(); - private static Dictionary framepatternPaths = new(); - private static Dictionary iconPaths = new(); - private static Dictionary charaPaths = new(); + [ConfigEntry] + private static readonly string localAssetsDir = "LocalAssets"; - private static Dictionary partnerPaths = new(); - - //private static Dictionary navicharaPaths = new(); - private static Dictionary tabTitlePaths = new(); - private static Dictionary localAssetsContents = new(); + private static readonly string[] imageExts = [".jpg", ".png", ".jpeg"]; + private static readonly Dictionary jacketPaths = []; + private static readonly Dictionary framePaths = []; + private static readonly Dictionary platePaths = []; + private static readonly Dictionary framemaskPaths = []; + private static readonly Dictionary framepatternPaths = []; + private static readonly Dictionary iconPaths = []; + private static readonly Dictionary charaPaths = []; + private static readonly Dictionary partnerPaths = []; + //private static readonly Dictionary navicharaPaths = []; + private static readonly Dictionary tabTitlePaths = []; + private static readonly Dictionary localAssetsContents = []; [HarmonyPrefix] [HarmonyPatch(typeof(DataManager), "LoadMusicBase")] @@ -116,16 +120,17 @@ public class LoadAssetsPng } } - MelonLogger.Msg($"[LoadAssetsPng] 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."); + 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."); - if (Directory.Exists(Path.Combine(Environment.CurrentDirectory, "LocalAssets"))) - foreach (var laFile in Directory.EnumerateFiles(Path.Combine(Environment.CurrentDirectory, "LocalAssets"))) + 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($"[LoadAssetsPng] Loaded {localAssetsContents.Count} LocalAssets."); + MelonLogger.Msg($"[LoadLocalImages] Loaded {localAssetsContents.Count} LocalAssets."); } private static string GetJacketPath(string id) @@ -483,7 +488,6 @@ public class LoadAssetsPng return true; } - MelonLogger.Msg(filename); var id = matches[0].Groups[1].Value; var texture = GetIconTexture2D(id); @@ -570,4 +574,4 @@ public class LoadAssetsPng } } */ -} \ No newline at end of file +} diff --git a/AquaMai/UX/LoadLocalBga.cs b/AquaMai/AquaMai.Mods/GameSystem/Assets/UseJacketAsDummyMovie.cs similarity index 73% rename from AquaMai/UX/LoadLocalBga.cs rename to AquaMai/AquaMai.Mods/GameSystem/Assets/UseJacketAsDummyMovie.cs index 2f26f422..a39fb265 100644 --- a/AquaMai/UX/LoadLocalBga.cs +++ b/AquaMai/AquaMai.Mods/GameSystem/Assets/UseJacketAsDummyMovie.cs @@ -1,4 +1,5 @@ using System.Linq; +using AquaMai.Config.Attributes; using HarmonyLib; using MAI2.Util; using Manager; @@ -6,9 +7,18 @@ using MelonLoader; using Monitor.Game; using UnityEngine; -namespace AquaMai.UX; +namespace AquaMai.Mods.GameSystem.Assets; -public class LoadLocalBga +[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")] @@ -20,7 +30,7 @@ public class LoadLocalBga var moviePath = Singleton.Instance.GetMovieDataPath($"{music.movieName.id:000000}") + ".dat"; if (!moviePath.Contains("dummy")) return; - var jacket = LoadAssetsPng.GetJacketTexture2D(music.movieName.id); + var jacket = LoadLocalImages.GetJacketTexture2D(music.movieName.id); if (jacket is null) { MelonLogger.Msg("No jacket found for music " + music); diff --git a/AquaMai/CustomCameraId/Enable.cs b/AquaMai/AquaMai.Mods/GameSystem/CustomCameraId.cs similarity index 50% rename from AquaMai/CustomCameraId/Enable.cs rename to AquaMai/AquaMai.Mods/GameSystem/CustomCameraId.cs index 737bc6b9..f659d644 100644 --- a/AquaMai/CustomCameraId/Enable.cs +++ b/AquaMai/AquaMai.Mods/GameSystem/CustomCameraId.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Collections; using System.Linq; @@ -6,12 +6,47 @@ using HarmonyLib; using Manager; using MelonLoader; using UnityEngine; +using AquaMai.Config.Attributes; -namespace AquaMai.CustomCameraId; +namespace AquaMai.Mods.GameSystem; -public class Enable +[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 { - private static readonly Dictionary CameraTypeMap = new Dictionary + [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 cameraTypeMap = new() { ["LeftQrCamera"] = "QRLeft", ["RightQrCamera"] = "QRRight", @@ -31,9 +66,9 @@ public class Enable { var textureCache = new WebCamTexture[WebCamTexture.devices.Length]; SortedDictionary webCamTextures = []; - foreach (var (configEntry, cameraTypeName) in CameraTypeMap) + foreach (var (configEntry, cameraTypeName) in cameraTypeMap) { - int deviceId = Traverse.Create(AquaMai.AppConfig.CustomCameraId).Field(configEntry).GetValue(); + int deviceId = Traverse.Create(typeof(CustomCameraId)).Field(configEntry).GetValue(); if (deviceId < 0 || deviceId >= WebCamTexture.devices.Length) { MelonLogger.Warning($"[CustomCameraId] Ignoring custom camera {configEntry}: camera ID {deviceId} out of range"); @@ -75,4 +110,33 @@ public class Enable 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}"); + } + } } diff --git a/AquaMai/AquaMai.Mods/GameSystem/DisableTimeout.cs b/AquaMai/AquaMai.Mods/GameSystem/DisableTimeout.cs new file mode 100644 index 00000000..14eee957 --- /dev/null +++ b/AquaMai/AquaMai.Mods/GameSystem/DisableTimeout.cs @@ -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(); + } +} diff --git a/AquaMai/AquaMai.Mods/GameSystem/KeyMap.cs b/AquaMai/AquaMai.Mods/GameSystem/KeyMap.cs new file mode 100644 index 00000000..49971604 --- /dev/null +++ b/AquaMai/AquaMai.Mods/GameSystem/KeyMap.cs @@ -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; + } +} diff --git a/AquaMai/AquaMai.Mods/GameSystem/QuickRetry.cs b/AquaMai/AquaMai.Mods/GameSystem/QuickRetry.cs new file mode 100644 index 00000000..e84f9f65 --- /dev/null +++ b/AquaMai/AquaMai.Mods/GameSystem/QuickRetry.cs @@ -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(); + } + return false; + } +} diff --git a/AquaMai/AquaMai.Mods/GameSystem/README.md b/AquaMai/AquaMai.Mods/GameSystem/README.md new file mode 100644 index 00000000..e9b0be73 --- /dev/null +++ b/AquaMai/AquaMai.Mods/GameSystem/README.md @@ -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). diff --git a/AquaMai/Fix/RemoveEncryption.cs b/AquaMai/AquaMai.Mods/GameSystem/RemoveEncryption.cs similarity index 64% rename from AquaMai/Fix/RemoveEncryption.cs rename to AquaMai/AquaMai.Mods/GameSystem/RemoveEncryption.cs index 2a29d9f8..7b1ce8db 100644 --- a/AquaMai/Fix/RemoveEncryption.cs +++ b/AquaMai/AquaMai.Mods/GameSystem/RemoveEncryption.cs @@ -1,11 +1,24 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using AquaMai.Config.Attributes; using HarmonyLib; using Net.Packet; -namespace AquaMai.Fix; +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] diff --git a/AquaMai/AquaMai.Mods/GameSystem/SinglePlayer.cs b/AquaMai/AquaMai.Mods/GameSystem/SinglePlayer.cs new file mode 100644 index 00000000..be6c1edc --- /dev/null +++ b/AquaMai/AquaMai.Mods/GameSystem/SinglePlayer.cs @@ -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 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(), 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.Instance.UpdateEvent(); + Singleton.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(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; + } +} diff --git a/AquaMai/AquaMai.Mods/GameSystem/TestProof.cs b/AquaMai/AquaMai.Mods/GameSystem/TestProof.cs new file mode 100644 index 00000000..0db5db06 --- /dev/null +++ b/AquaMai/AquaMai.Mods/GameSystem/TestProof.cs @@ -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")) + { + __result = KeyListener.GetKeyDownOrLongPress(KeyCodeOrName.Test, true); + } + + return false; + } +} diff --git a/AquaMai/AquaMai.Mods/GameSystem/TouchPanelBaudRate.cs b/AquaMai/AquaMai.Mods/GameSystem/TouchPanelBaudRate.cs new file mode 100644 index 00000000..e9294249 --- /dev/null +++ b/AquaMai/AquaMai.Mods/GameSystem/TouchPanelBaudRate.cs @@ -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; + } +} diff --git a/AquaMai/UX/TouchToButtonInput.cs b/AquaMai/AquaMai.Mods/GameSystem/TouchToButtonInput.cs similarity index 90% rename from AquaMai/UX/TouchToButtonInput.cs rename to AquaMai/AquaMai.Mods/GameSystem/TouchToButtonInput.cs index 6d0c09fe..ed8ef966 100644 --- a/AquaMai/UX/TouchToButtonInput.cs +++ b/AquaMai/AquaMai.Mods/GameSystem/TouchToButtonInput.cs @@ -1,10 +1,13 @@ -using HarmonyLib; -using Manager; +using AquaMai.Config.Attributes; +using HarmonyLib; using Process; using static Manager.InputManager; -namespace AquaMai.UX; +namespace AquaMai.Mods.GameSystem; +[ConfigSection( + en: "Map touch actions to buttons.", + zh: "映射触摸操作至实体按键")] public class TouchToButtonInput { private static bool _isPlaying = false; diff --git a/AquaMai/AquaMai.Mods/GameSystem/Unlock.cs b/AquaMai/AquaMai.Mods/GameSystem/Unlock.cs new file mode 100644 index 00000000..0e867813 --- /dev/null +++ b/AquaMai/AquaMai.Mods/GameSystem/Unlock.cs @@ -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; + } +} diff --git a/AquaMai/WindowState/Enable.cs b/AquaMai/AquaMai.Mods/GameSystem/Window.cs similarity index 58% rename from AquaMai/WindowState/Enable.cs rename to AquaMai/AquaMai.Mods/GameSystem/Window.cs index 43c9db0c..e203f45b 100644 --- a/AquaMai/WindowState/Enable.cs +++ b/AquaMai/AquaMai.Mods/GameSystem/Window.cs @@ -1,30 +1,55 @@ using System; using System.Runtime.InteropServices; using System.Threading.Tasks; +using AquaMai.Config.Attributes; using UnityEngine; -namespace AquaMai.WindowState; +namespace AquaMai.Mods.GameSystem; -public class Enable +[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 DoCustomPatch(HarmonyLib.Harmony h) + public static void OnBeforePatch() { - if (AquaMai.AppConfig.WindowState.Windowed) + if (windowed) { var alreadyWindowed = Screen.fullScreenMode == FullScreenMode.Windowed; - if (AquaMai.AppConfig.WindowState.Width == 0 || AquaMai.AppConfig.WindowState.Height == 0) + if (width == 0 || height == 0) { Screen.fullScreenMode = FullScreenMode.Windowed; } else { alreadyWindowed = false; - Screen.SetResolution(AquaMai.AppConfig.WindowState.Width, AquaMai.AppConfig.WindowState.Height, FullScreenMode.Windowed); + Screen.SetResolution(width, height, FullScreenMode.Windowed); } hwnd = GetWindowHandle(); @@ -44,8 +69,8 @@ public class Enable } else { - var width = AquaMai.AppConfig.WindowState.Width == 0 ? Display.main.systemWidth : AquaMai.AppConfig.WindowState.Width; - var height = AquaMai.AppConfig.WindowState.Height == 0 ? Display.main.systemHeight : AquaMai.AppConfig.WindowState.Height; + 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); } } diff --git a/AquaMai/AquaMai.Mods/General.cs b/AquaMai/AquaMai.Mods/General.cs new file mode 100644 index 00000000..6c34191c --- /dev/null +++ b/AquaMai/AquaMai.Mods/General.cs @@ -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 +} diff --git a/AquaMai/AquaMai.Mods/Tweaks/IgnoreAimeServerError.cs b/AquaMai/AquaMai.Mods/Tweaks/IgnoreAimeServerError.cs new file mode 100644 index 00000000..99e4fcb3 --- /dev/null +++ b/AquaMai/AquaMai.Mods/Tweaks/IgnoreAimeServerError.cs @@ -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; + } +} diff --git a/AquaMai/AquaMai.Mods/Tweaks/LockFrameRate.cs b/AquaMai/AquaMai.Mods/Tweaks/LockFrameRate.cs new file mode 100644 index 00000000..f80eabbb --- /dev/null +++ b/AquaMai/AquaMai.Mods/Tweaks/LockFrameRate.cs @@ -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; + } +} diff --git a/AquaMai/AquaMai.Mods/Tweaks/README.md b/AquaMai/AquaMai.Mods/Tweaks/README.md new file mode 100644 index 00000000..64233662 --- /dev/null +++ b/AquaMai/AquaMai.Mods/Tweaks/README.md @@ -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. diff --git a/AquaMai/Fix/TouchResetAfterTrack.cs b/AquaMai/AquaMai.Mods/Tweaks/ResetTouchAfterTrack.cs similarity index 73% rename from AquaMai/Fix/TouchResetAfterTrack.cs rename to AquaMai/AquaMai.Mods/Tweaks/ResetTouchAfterTrack.cs index 1f080293..22b0991c 100644 --- a/AquaMai/Fix/TouchResetAfterTrack.cs +++ b/AquaMai/AquaMai.Mods/Tweaks/ResetTouchAfterTrack.cs @@ -1,11 +1,15 @@ -using HarmonyLib; +using AquaMai.Config.Attributes; +using HarmonyLib; using MAI2.Util; using Manager; using Process; -namespace AquaMai.Fix; +namespace AquaMai.Mods.Tweaks; -public class TouchResetAfterTrack +[ConfigSection( + en: "Reset touch panel after playing track.", + zh: "在游玩一首曲目后重置触摸面板")] +public class ResetTouchAfterTrack { [HarmonyPostfix] [HarmonyPatch(typeof(ResultProcess), "OnStart")] diff --git a/AquaMai/AquaMai.Mods/Tweaks/SkipUserVersionCheck.cs b/AquaMai/AquaMai.Mods/Tweaks/SkipUserVersionCheck.cs new file mode 100644 index 00000000..54a7d04e --- /dev/null +++ b/AquaMai/AquaMai.Mods/Tweaks/SkipUserVersionCheck.cs @@ -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; + } +} diff --git a/AquaMai/AquaMai.Mods/Tweaks/TimeSaving/EntryToMusicSelection.cs b/AquaMai/AquaMai.Mods/Tweaks/TimeSaving/EntryToMusicSelection.cs new file mode 100644 index 00000000..4b00be95 --- /dev/null +++ b/AquaMai/AquaMai.Mods/Tweaks/TimeSaving/EntryToMusicSelection.cs @@ -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.Instance.GetUserData(monIndex); + var index = userData.MapList.FindIndex((m) => m.ID == userData.Detail.SelectMapID); + if (index >= 0) return; + userData.MapList.Clear(); + } +} diff --git a/AquaMai/AquaMai.Mods/Tweaks/TimeSaving/IWontTapOrSlideVigorously.cs b/AquaMai/AquaMai.Mods/Tweaks/TimeSaving/IWontTapOrSlideVigorously.cs new file mode 100644 index 00000000..8ec9f44c --- /dev/null +++ b/AquaMai/AquaMai.Mods/Tweaks/TimeSaving/IWontTapOrSlideVigorously.cs @@ -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; + } +} diff --git a/AquaMai/AquaMai.Mods/Tweaks/TimeSaving/SkipEventInfo.cs b/AquaMai/AquaMai.Mods/Tweaks/TimeSaving/SkipEventInfo.cs new file mode 100644 index 00000000..70400ed0 --- /dev/null +++ b/AquaMai/AquaMai.Mods/Tweaks/TimeSaving/SkipEventInfo.cs @@ -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[] ____discoverList) + { + ____discoverList = new Queue[] { new Queue(), new Queue() }; + } +} diff --git a/AquaMai/TimeSaving/SkipGameOverScreen.cs b/AquaMai/AquaMai.Mods/Tweaks/TimeSaving/SkipGoodbyeScreen.cs similarity index 68% rename from AquaMai/TimeSaving/SkipGameOverScreen.cs rename to AquaMai/AquaMai.Mods/Tweaks/TimeSaving/SkipGoodbyeScreen.cs index 17e65c89..e6597d61 100644 --- a/AquaMai/TimeSaving/SkipGameOverScreen.cs +++ b/AquaMai/AquaMai.Mods/Tweaks/TimeSaving/SkipGoodbyeScreen.cs @@ -1,10 +1,14 @@ -using HarmonyLib; +using AquaMai.Config.Attributes; +using HarmonyLib; using Monitor; using Process; -namespace AquaMai.TimeSaving; +namespace AquaMai.Mods.Tweaks.TimeSaving; -public class SkipGameOverScreen +[ConfigSection( + en: "Skip the \"Goodbye\" screen at the end of the game.", + zh: "跳过游戏结束的「再见」界面")] +public class SkipGoodbyeScreen { [HarmonyPrefix] [HarmonyPatch(typeof(GameOverMonitor), "IsPlayEnd")] diff --git a/AquaMai/AquaMai.Mods/Tweaks/TimeSaving/SkipStartupDelays.cs b/AquaMai/AquaMai.Mods/Tweaks/TimeSaving/SkipStartupDelays.cs new file mode 100644 index 00000000..9517fc9b --- /dev/null +++ b/AquaMai/AquaMai.Mods/Tweaks/TimeSaving/SkipStartupDelays.cs @@ -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); + } + } +} diff --git a/AquaMai/AquaMai.Mods/Tweaks/TimeSaving/SkipStartupWarning.cs b/AquaMai/AquaMai.Mods/Tweaks/TimeSaving/SkipStartupWarning.cs new file mode 100644 index 00000000..5b816681 --- /dev/null +++ b/AquaMai/AquaMai.Mods/Tweaks/TimeSaving/SkipStartupWarning.cs @@ -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; + } +} diff --git a/AquaMai/TimeSaving/SkipTrackStart.cs b/AquaMai/AquaMai.Mods/Tweaks/TimeSaving/SkipTrackStart.cs similarity index 55% rename from AquaMai/TimeSaving/SkipTrackStart.cs rename to AquaMai/AquaMai.Mods/Tweaks/TimeSaving/SkipTrackStart.cs index 26c9c2eb..625cfe32 100644 --- a/AquaMai/TimeSaving/SkipTrackStart.cs +++ b/AquaMai/AquaMai.Mods/Tweaks/TimeSaving/SkipTrackStart.cs @@ -1,8 +1,12 @@ -using HarmonyLib; +using AquaMai.Config.Attributes; +using HarmonyLib; using Monitor; -namespace AquaMai.TimeSaving; +namespace AquaMai.Mods.Tweaks.TimeSaving; +[ConfigSection( + en: "Skip TrackStart screen.", + zh: "跳过乐曲开始界面")] public class SkipTrackStart { [HarmonyPrefix] diff --git a/AquaMai/AquaMai.Mods/UX/CiBuildAlert.cs b/AquaMai/AquaMai.Mods/UX/CiBuildAlert.cs new file mode 100644 index 00000000..27caa92f --- /dev/null +++ b/AquaMai/AquaMai.Mods/UX/CiBuildAlert.cs @@ -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); + } +} diff --git a/AquaMai/ModKeyMap/HideSelfMadeCharts.cs b/AquaMai/AquaMai.Mods/UX/HideSelfMadeCharts.cs similarity index 70% rename from AquaMai/ModKeyMap/HideSelfMadeCharts.cs rename to AquaMai/AquaMai.Mods/UX/HideSelfMadeCharts.cs index 58347a62..38e2f1b6 100644 --- a/AquaMai/ModKeyMap/HideSelfMadeCharts.cs +++ b/AquaMai/AquaMai.Mods/UX/HideSelfMadeCharts.cs @@ -1,22 +1,43 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Threading.Tasks; -using AquaMai.Helpers; +using AquaMai.Config.Attributes; +using AquaMai.Config.Types; +using AquaMai.Core.Helpers; using HarmonyLib; using MAI2.Util; using Manager; using MelonLoader; using Process; -using UnityEngine; using Util; -namespace AquaMai.ModKeyMap; +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 _musics; private static Safe.ReadonlySortedDictionary _musicsNoneSelfMade; @@ -58,7 +79,7 @@ public class HideSelfMadeCharts public static void MusicSelectProcessOnUpdate(ref MusicSelectProcess __instance) { if (isForceDisable) return; - if (!ModKeyListener.GetKeyDownOrLongPress(AquaMai.AppConfig.ModKeyMap.HideSelfMadeCharts, AquaMai.AppConfig.ModKeyMap.HideSelfMadeChartsLongPress)) 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))); @@ -73,9 +94,10 @@ public class HideSelfMadeCharts [HarmonyPatch(typeof(MusicSelectProcess), "OnStart")] public static void MusicSelectProcessOnStart(ref MusicSelectProcess __instance) { - if (File.Exists(Path.Combine(Environment.CurrentDirectory, "LocalAssets", "SelfMadeChartsDenyUsers.txt"))) + var denyPath = FileSystem.ResolvePath(selfMadeChartsDenyUsersFile); + if (File.Exists(denyPath)) { - var userIds = File.ReadAllLines(Path.Combine(Environment.CurrentDirectory, "LocalAssets", "SelfMadeChartsDenyUsers.txt")); + var userIds = File.ReadAllLines(denyPath); for (var i = 0; i < 2; i++) { var user = Singleton.Instance.GetUserData(i); @@ -86,9 +108,10 @@ public class HideSelfMadeCharts } } - if (File.Exists(Path.Combine(Environment.CurrentDirectory, "LocalAssets", "SelfMadeChartsWhiteListUsers.txt"))) + var whiteListPath = FileSystem.ResolvePath(selfMadeChartsWhiteListUsersFile); + if (File.Exists(whiteListPath)) { - var userIds = File.ReadAllLines(Path.Combine(Environment.CurrentDirectory, "LocalAssets", "SelfMadeChartsWhiteListUsers.txt")); + var userIds = File.ReadAllLines(whiteListPath); for (var i = 0; i < 2; i++) { var user = Singleton.Instance.GetUserData(i); diff --git a/AquaMai/UX/ImmediateSave.cs b/AquaMai/AquaMai.Mods/UX/ImmediateSave.cs similarity index 92% rename from AquaMai/UX/ImmediateSave.cs rename to AquaMai/AquaMai.Mods/UX/ImmediateSave.cs index f63b06f3..03e71d79 100644 --- a/AquaMai/UX/ImmediateSave.cs +++ b/AquaMai/AquaMai.Mods/UX/ImmediateSave.cs @@ -1,8 +1,9 @@ using System; using System.Collections.Generic; using System.Linq; -using AquaMai.Helpers; -using AquaMai.Resources; +using AquaMai.Config.Attributes; +using AquaMai.Core.Helpers; +using AquaMai.Core.Resources; using DB; using HarmonyLib; using MAI2.Util; @@ -18,8 +19,11 @@ using Process; using Process.UserDataNet.State.UserDataULState; using UnityEngine; -namespace AquaMai.UX; +namespace AquaMai.Mods.UX; +[ConfigSection( + en: "Save immediate after playing a song.", + zh: "打完一首歌的时候立即向服务器保存成绩")] public class ImmediateSave { [HarmonyPrefix] @@ -65,13 +69,13 @@ public class ImmediateSave PacketHelper.StartPacket(Shim.CreatePacketUploadUserPlaylog(i, userData, (int)GameManager.MusicTrackNumber - 1, delegate { - MelonLogger.Msg("Playlog saved"); + MelonLogger.Msg("[ImmediateSave] Playlog saved"); CheckSaveDone(); }, delegate(PacketStatus err) { SoundManager.PlaySE(Mai2.Mai2Cue.Cue.SE_ENTRY_AIME_ERROR, i); - MelonLogger.Error("Playlog save error"); + MelonLogger.Error("[ImmediateSave] Playlog save error"); MelonLogger.Error(err); MessageHelper.ShowMessage("Playlog save error"); CheckSaveDone(); @@ -80,13 +84,13 @@ public class ImmediateSave { if (code == 1) { - MelonLogger.Msg("UserAll saved"); + MelonLogger.Msg("[ImmediateSave] UserAll saved"); CheckSaveDone(); } else { SoundManager.PlaySE(Mai2.Mai2Cue.Cue.SE_ENTRY_AIME_ERROR, i); - MelonLogger.Error("UserAll upsert error"); + MelonLogger.Error("[ImmediateSave] UserAll upsert error"); MelonLogger.Error(code); MessageHelper.ShowMessage("UserAll upsert error"); CheckSaveDone(); @@ -94,7 +98,7 @@ public class ImmediateSave }, delegate(PacketStatus err) { SoundManager.PlaySE(Mai2.Mai2Cue.Cue.SE_ENTRY_AIME_ERROR, i); - MelonLogger.Error("UserAll upsert error"); + MelonLogger.Error("[ImmediateSave] UserAll upsert error"); MelonLogger.Error(err); MessageHelper.ShowMessage("UserAll upsert error"); CheckSaveDone(); diff --git a/AquaMai/UX/JudgeAccuracyInfo.cs b/AquaMai/AquaMai.Mods/UX/JudgeAccuracyInfo.cs similarity index 97% rename from AquaMai/UX/JudgeAccuracyInfo.cs rename to AquaMai/AquaMai.Mods/UX/JudgeAccuracyInfo.cs index 1772d0fd..539184b4 100644 --- a/AquaMai/UX/JudgeAccuracyInfo.cs +++ b/AquaMai/AquaMai.Mods/UX/JudgeAccuracyInfo.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.IO; using System.Linq; using System.Reflection; +using AquaMai.Config.Attributes; using HarmonyLib; using Manager; using Monitor; @@ -13,8 +14,11 @@ using TMPro; using UnityEngine; using Object = UnityEngine.Object; -namespace AquaMai.UX; +namespace AquaMai.Mods.UX; +[ConfigSection( + zh: "在游戏总结的计分板中显示击打误差的详细信息(以帧为单位)", + en: "Show detailed accuracy info in the score board.")] public class JudgeAccuracyInfo { public class AccuracyEntryList @@ -240,4 +244,4 @@ public class JudgeAccuracyInfo return result; } -} \ No newline at end of file +} diff --git a/AquaMai/ModKeyMap/QuickSkip.cs b/AquaMai/AquaMai.Mods/UX/OneKeyEntryEnd.cs similarity index 61% rename from AquaMai/ModKeyMap/QuickSkip.cs rename to AquaMai/AquaMai.Mods/UX/OneKeyEntryEnd.cs index cb9817e7..5e2dee77 100644 --- a/AquaMai/ModKeyMap/QuickSkip.cs +++ b/AquaMai/AquaMai.Mods/UX/OneKeyEntryEnd.cs @@ -1,22 +1,32 @@ using System.Collections.Generic; -using AquaMai.Helpers; +using AquaMai.Config.Attributes; +using AquaMai.Config.Types; +using AquaMai.Core.Helpers; using HarmonyLib; using Mai2.Mai2Cue; -using MAI2.Util; using Main; using Manager; using MelonLoader; using Process; -namespace AquaMai.ModKeyMap; +namespace AquaMai.Mods.UX; -public class QuickSkip +[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 (!ModKeyListener.GetKeyDownOrLongPress(AquaMai.AppConfig.ModKeyMap.QuickSkip, AquaMai.AppConfig.ModKeyMap.QuickSkipLongPress)) return; + if (!KeyListener.GetKeyDownOrLongPress(key, longPress)) return; MelonLogger.Msg("[QuickSkip] Activated"); var traverse = Traverse.Create(SharedInstances.ProcessDataContainer.processManager); @@ -52,23 +62,4 @@ public class QuickSkip SharedInstances.ProcessDataContainer.processManager.AddProcess(new FadeProcess(SharedInstances.ProcessDataContainer, processToRelease, new MusicSelectProcess(SharedInstances.ProcessDataContainer))); } } - - [HarmonyPostfix] - [HarmonyPatch(typeof(GameProcess), "OnUpdate")] - public static void PostGameProcessUpdate(GameProcess __instance, Message[] ____message, ProcessDataContainer ___container) - { - if (ModKeyListener.GetKeyDownOrLongPress(AquaMai.AppConfig.ModKeyMap.InGameSkip, AquaMai.AppConfig.ModKeyMap.InGameSkipLongPress)) - { - var traverse = Traverse.Create(__instance); - ___container.processManager.SendMessage(____message[0]); - Singleton.Instance.SetSyncResult(0); - traverse.Method("SetRelease").GetValue(); - } - - if (ModKeyListener.GetKeyDownOrLongPress(AquaMai.AppConfig.ModKeyMap.InGameRetry, AquaMai.AppConfig.ModKeyMap.InGameRetryLongPress) && GameInfo.GameVersion >= 23000) - { - // This is original typo in Assembly-CSharp - Singleton.Instance.SetQuickRetryFrag(flag: true); - } - } } diff --git a/AquaMai/AquaMai.Mods/UX/OneKeyRetrySkip.cs b/AquaMai/AquaMai.Mods/UX/OneKeyRetrySkip.cs new file mode 100644 index 00000000..2b7c1305 --- /dev/null +++ b/AquaMai/AquaMai.Mods/UX/OneKeyRetrySkip.cs @@ -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.Instance.SetSyncResult(0); + traverse.Method("SetRelease").GetValue(); + } + + if (KeyListener.GetKeyDownOrLongPress(retryKey, retryLongPress) && GameInfo.GameVersion >= 23000) + { + // This is original typo in Assembly-CSharp + Singleton.Instance.SetQuickRetryFrag(flag: true); + } + } +} diff --git a/AquaMai/ModKeyMap/PractiseModeUI.cs b/AquaMai/AquaMai.Mods/UX/PracticeMode/Libs/PractiseModeUI.cs similarity index 82% rename from AquaMai/ModKeyMap/PractiseModeUI.cs rename to AquaMai/AquaMai.Mods/UX/PracticeMode/Libs/PractiseModeUI.cs index 5d8be53b..8bd17b29 100644 --- a/AquaMai/ModKeyMap/PractiseModeUI.cs +++ b/AquaMai/AquaMai.Mods/UX/PracticeMode/Libs/PractiseModeUI.cs @@ -1,13 +1,13 @@ using System; -using AquaMai.Fix; -using AquaMai.Helpers; -using AquaMai.Resources; +using AquaMai.Mods.Fix; +using AquaMai.Core.Helpers; +using AquaMai.Core.Resources; using Manager; using UnityEngine; -namespace AquaMai.ModKeyMap; +namespace AquaMai.Mods.UX.PracticeMode.Libs; -public class PractiseModeUI : MonoBehaviour +public class PracticeModeUI : MonoBehaviour { private static float windowTop => Screen.height - GuiSizes.PlayerWidth + GuiSizes.PlayerWidth * .22f; private static float controlHeight => GuiSizes.PlayerWidth * .13f; @@ -60,12 +60,12 @@ public class PractiseModeUI : MonoBehaviour GUI.Button(GetButtonRect(1, 0), Locale.Pause); GUI.Button(GetButtonRect(2, 0), Locale.SeekForward); - if (PractiseMode.repeatStart == -1) + if (PracticeMode.repeatStart == -1) { GUI.Button(GetButtonRect(0, 1), Locale.MarkRepeatStart); GUI.Label(GetButtonRect(1, 1), Locale.RepeatNotSet); } - else if (PractiseMode.repeatEnd == -1) + else if (PracticeMode.repeatEnd == -1) { GUI.Button(GetButtonRect(0, 1), Locale.MarkRepeatEnd); GUI.Label(GetButtonRect(1, 1), Locale.RepeatStartSet); @@ -78,60 +78,60 @@ public class PractiseModeUI : MonoBehaviour } GUI.Button(GetButtonRect(0, 2), Locale.SpeedDown); - GUI.Label(GetButtonRect(1, 2), $"{Locale.Speed} {PractiseMode.speed * 100:000}%"); + 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(PractiseMode.CurrentPlayMsec):mm\\:ss\\.fff}\n{TimeSpan.FromMilliseconds(NotesManager.Instance().getPlayFinalMsec()):mm\\:ss\\.fff}"); - GUI.Button(GetButtonRect(2, 3), $"保持流速\n{(PractiseMode.keepNoteSpeed ? "ON" : "OFF")}"); + 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)) { - PractiseMode.Seek(-1000); + PracticeMode.Seek(-1000); } else if (InputManager.GetTouchPanelAreaDown(InputManager.TouchPanelArea.E2)) { - PractiseMode.Seek(1000); + PracticeMode.Seek(1000); } else if (InputManager.GetTouchPanelAreaDown(InputManager.TouchPanelArea.B8) || InputManager.GetTouchPanelAreaDown(InputManager.TouchPanelArea.B1)) { DebugFeature.Pause = !DebugFeature.Pause; if (!DebugFeature.Pause) { - PractiseMode.Seek(0); + PracticeMode.Seek(0); } } - else if (InputManager.GetTouchPanelAreaDown(InputManager.TouchPanelArea.B7) && PractiseMode.repeatStart == -1) + else if (InputManager.GetTouchPanelAreaDown(InputManager.TouchPanelArea.B7) && PracticeMode.repeatStart == -1) { - PractiseMode.repeatStart = PractiseMode.CurrentPlayMsec; + PracticeMode.repeatStart = PracticeMode.CurrentPlayMsec; } - else if (InputManager.GetTouchPanelAreaDown(InputManager.TouchPanelArea.B7) && PractiseMode.repeatEnd == -1) + else if (InputManager.GetTouchPanelAreaDown(InputManager.TouchPanelArea.B7) && PracticeMode.repeatEnd == -1) { - PractiseMode.SetRepeatEnd(PractiseMode.CurrentPlayMsec); + PracticeMode.SetRepeatEnd(PracticeMode.CurrentPlayMsec); } else if (InputManager.GetTouchPanelAreaDown(InputManager.TouchPanelArea.B2)) { - PractiseMode.ClearRepeat(); + PracticeMode.ClearRepeat(); } else if (InputManager.GetTouchPanelAreaDown(InputManager.TouchPanelArea.B6)) { - PractiseMode.SpeedDown(); + PracticeMode.SpeedDown(); } else if (InputManager.GetTouchPanelAreaDown(InputManager.TouchPanelArea.B3)) { - PractiseMode.SpeedUp(); + PracticeMode.SpeedUp(); } else if (InputManager.GetTouchPanelAreaDown(InputManager.TouchPanelArea.B5) || InputManager.GetTouchPanelAreaDown(InputManager.TouchPanelArea.B4)) { - PractiseMode.SpeedReset(); + PracticeMode.SpeedReset(); } else if (InputManager.GetTouchPanelAreaDown(InputManager.TouchPanelArea.E4)) { - PractiseMode.keepNoteSpeed = !PractiseMode.keepNoteSpeed; - PractiseMode.gameCtrl?.ResetOptionSpeed(); + PracticeMode.keepNoteSpeed = !PracticeMode.keepNoteSpeed; + PracticeMode.gameCtrl?.ResetOptionSpeed(); } else if ( InputManager.GetTouchPanelAreaDown(InputManager.TouchPanelArea.A1) || @@ -144,7 +144,7 @@ public class PractiseModeUI : MonoBehaviour InputManager.GetTouchPanelAreaDown(InputManager.TouchPanelArea.A8) ) { - PractiseMode.ui = null; + PracticeMode.ui = null; Destroy(this); } } diff --git a/AquaMai/ModKeyMap/PractiseMode.cs b/AquaMai/AquaMai.Mods/UX/PracticeMode/PracticeMode.cs similarity index 89% rename from AquaMai/ModKeyMap/PractiseMode.cs rename to AquaMai/AquaMai.Mods/UX/PracticeMode/PracticeMode.cs index a14fafb2..a0878ebc 100644 --- a/AquaMai/ModKeyMap/PractiseMode.cs +++ b/AquaMai/AquaMai.Mods/UX/PracticeMode/PracticeMode.cs @@ -4,22 +4,35 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Reflection; -using AquaMai.Fix; -using AquaMai.Helpers; -using AquaMai.Resources; -using AquaMai.Utils; +using AquaMai.Mods.Fix; +using AquaMai.Core.Helpers; +using AquaMai.Core.Resources; +using AquaMai.Mods.UX.PracticeMode.Libs; using HarmonyLib; using Manager; -using MelonLoader; using Monitor; using Monitor.Game; using Process; using UnityEngine; +using AquaMai.Config.Attributes; +using AquaMai.Config.Types; -namespace AquaMai.ModKeyMap; +namespace AquaMai.Mods.UX.PracticeMode; -public class PractiseMode +[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; @@ -124,7 +137,7 @@ public class PractiseMode } } - public static PractiseModeUI ui; + public static PracticeModeUI ui; [HarmonyPatch] public class PatchNoteSpeed @@ -176,7 +189,7 @@ public class PractiseMode { if (Input.GetKeyDown(KeyCode.F11)) { - ____monitors[0].gameObject.AddComponent(); + ____monitors[0].gameObject.AddComponent(); } } # endif @@ -185,9 +198,9 @@ public class PractiseMode [HarmonyPostfix] public static void GameProcessPostUpdate(GameProcess __instance, GameMonitor[] ____monitors) { - if (ModKeyListener.GetKeyDownOrLongPress(AquaMai.AppConfig.ModKeyMap.PractiseMode, AquaMai.AppConfig.ModKeyMap.PractiseModeLongPress) && ui is null) + if (KeyListener.GetKeyDownOrLongPress(key, longPress) && ui is null) { - ui = ____monitors[0].gameObject.AddComponent(); + ui = ____monitors[0].gameObject.AddComponent(); } if (repeatStart >= 0 && repeatEnd >= 0) diff --git a/AquaMai/TimeSaving/ShowQuickEndPlay.cs b/AquaMai/AquaMai.Mods/UX/QuickEndPlay.cs similarity index 85% rename from AquaMai/TimeSaving/ShowQuickEndPlay.cs rename to AquaMai/AquaMai.Mods/UX/QuickEndPlay.cs index e575ed53..a8469bc5 100644 --- a/AquaMai/TimeSaving/ShowQuickEndPlay.cs +++ b/AquaMai/AquaMai.Mods/UX/QuickEndPlay.cs @@ -1,6 +1,6 @@ -using System.Collections.Generic; -using AquaMai.Helpers; -using AquaMai.Resources; +using AquaMai.Config.Attributes; +using AquaMai.Core.Helpers; +using AquaMai.Core.Resources; using HarmonyLib; using MAI2.Util; using Manager; @@ -8,9 +8,12 @@ using Monitor; using Process; using UnityEngine; -namespace AquaMai.TimeSaving; +namespace AquaMai.Mods.UX; -public class ShowQuickEndPlay +[ConfigSection( + en: "Show a \"skip\" button like AstroDX after the notes end.", + zh: "音符结束之后显示像 AstroDX 一样的「跳过」按钮")] +public class QuickEndPlay { private static int _timer; diff --git a/AquaMai/AquaMai.Mods/UX/README.md b/AquaMai/AquaMai.Mods/UX/README.md new file mode 100644 index 00000000..80cc09ec --- /dev/null +++ b/AquaMai/AquaMai.Mods/UX/README.md @@ -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. diff --git a/AquaMai/Utils/SelectionDetail.cs b/AquaMai/AquaMai.Mods/UX/SelectionDetail.cs similarity index 93% rename from AquaMai/Utils/SelectionDetail.cs rename to AquaMai/AquaMai.Mods/UX/SelectionDetail.cs index 1f1187af..325600fc 100644 --- a/AquaMai/Utils/SelectionDetail.cs +++ b/AquaMai/AquaMai.Mods/UX/SelectionDetail.cs @@ -1,8 +1,9 @@ using System.Collections.Generic; using System.Linq; -using AquaMai.Attributes; -using AquaMai.Helpers; -using AquaMai.Resources; +using AquaMai.Core.Attributes; +using AquaMai.Config.Attributes; +using AquaMai.Core.Helpers; +using AquaMai.Core.Resources; using HarmonyLib; using MAI2.Util; using Manager; @@ -12,9 +13,12 @@ using Monitor; using Process; using UnityEngine; -namespace AquaMai.Utils; +namespace AquaMai.Mods.UX; -[GameVersion(23500)] +[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]; diff --git a/AquaMai/Utils/FrameRateDisplay.cs b/AquaMai/AquaMai.Mods/Utils/DisplayFrameRate.cs similarity index 85% rename from AquaMai/Utils/FrameRateDisplay.cs rename to AquaMai/AquaMai.Mods/Utils/DisplayFrameRate.cs index e5c108c5..4e5a3f1b 100644 --- a/AquaMai/Utils/FrameRateDisplay.cs +++ b/AquaMai/AquaMai.Mods/Utils/DisplayFrameRate.cs @@ -1,11 +1,15 @@ -using AquaMai.Helpers; +using AquaMai.Config.Attributes; +using AquaMai.Core.Helpers; using HarmonyLib; using Main; using UnityEngine; -namespace AquaMai.Utils; +namespace AquaMai.Mods.Utils; -public class FrameRateDisplay +[ConfigSection( + en: "Display framerate.", + zh: "显示帧率")] +public class DisplayFrameRate { [HarmonyPatch(typeof(GameMainObject), "Awake")] [HarmonyPostfix] diff --git a/AquaMai/Utils/LogNetworkErrors.cs b/AquaMai/AquaMai.Mods/Utils/LogNetworkErrors.cs similarity index 92% rename from AquaMai/Utils/LogNetworkErrors.cs rename to AquaMai/AquaMai.Mods/Utils/LogNetworkErrors.cs index 718eb2f3..73f59923 100644 --- a/AquaMai/Utils/LogNetworkErrors.cs +++ b/AquaMai/AquaMai.Mods/Utils/LogNetworkErrors.cs @@ -1,12 +1,14 @@ using System.Diagnostics; +using AquaMai.Config.Attributes; using HarmonyLib; using Manager; using Manager.Operation; using MelonLoader; using Net.Packet; -namespace AquaMai.Utils; +namespace AquaMai.Mods.Utils; +[ConfigSection(exampleHidden: true, defaultOn: true)] public class LogNetworkErrors { [HarmonyPostfix] diff --git a/AquaMai/Utils/LogUserId.cs b/AquaMai/AquaMai.Mods/Utils/LogUserId.cs similarity index 64% rename from AquaMai/Utils/LogUserId.cs rename to AquaMai/AquaMai.Mods/Utils/LogUserId.cs index ef0ddf13..ff1fc73b 100644 --- a/AquaMai/Utils/LogUserId.cs +++ b/AquaMai/AquaMai.Mods/Utils/LogUserId.cs @@ -1,18 +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.Utils; +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), typeof(Action))] public static void Postfix(ulong userId) { - MelonLogger.Msg($"UserLogin: {userId}"); + MelonLogger.Msg($"[LogUserId] UserLogin: {userId}"); } } diff --git a/AquaMai/AquaMai.Mods/Utils/README.md b/AquaMai/AquaMai.Mods/Utils/README.md new file mode 100644 index 00000000..59023a0e --- /dev/null +++ b/AquaMai/AquaMai.Mods/Utils/README.md @@ -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. diff --git a/AquaMai/Utils/ShowErrorLog.cs b/AquaMai/AquaMai.Mods/Utils/ShowErrorLog.cs similarity index 91% rename from AquaMai/Utils/ShowErrorLog.cs rename to AquaMai/AquaMai.Mods/Utils/ShowErrorLog.cs index b0e5fe6d..276a347c 100644 --- a/AquaMai/Utils/ShowErrorLog.cs +++ b/AquaMai/AquaMai.Mods/Utils/ShowErrorLog.cs @@ -3,14 +3,18 @@ using System.Collections; using System.IO; using System.Text.RegularExpressions; using System.Threading; -using AquaMai.Helpers; +using AquaMai.Config.Attributes; +using AquaMai.Core.Helpers; using HarmonyLib; using Main; using MelonLoader; using UnityEngine; -namespace AquaMai.Utils; +namespace AquaMai.Mods.Utils; +[ConfigSection( + en: "Show error log in the game.", + zh: "在游戏中显示错误日志窗口而不是关闭游戏进程")] public class ShowErrorLog { private static Ui _errorUi; @@ -78,7 +82,7 @@ public class ShowErrorLog GUI.Box(new Rect(x, y, width, height), "", boxStyle); GUI.Label(new Rect(x, y, width, height), _errorLog, labelStyle); - if (!AquaMai.AppConfig.UX.SinglePlayer) + 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); @@ -93,4 +97,4 @@ public class ShowErrorLog } } } -} \ No newline at end of file +} diff --git a/AquaMai/Utils/ShowNetErrorDetail.cs b/AquaMai/AquaMai.Mods/Utils/ShowNetErrorDetail.cs similarity index 88% rename from AquaMai/Utils/ShowNetErrorDetail.cs rename to AquaMai/AquaMai.Mods/Utils/ShowNetErrorDetail.cs index c3691689..c852981b 100644 --- a/AquaMai/Utils/ShowNetErrorDetail.cs +++ b/AquaMai/AquaMai.Mods/Utils/ShowNetErrorDetail.cs @@ -1,16 +1,19 @@ using System.Collections.Generic; -using AquaMai.Helpers; -using AquaMai.Resources; +using AquaMai.Config.Attributes; +using AquaMai.Core.Helpers; +using AquaMai.Core.Resources; using HarmonyLib; using MAI2.Util; using Manager; -using MelonLoader; using Monitor; using Process; using UnityEngine; -namespace AquaMai.Utils; +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")] diff --git a/AquaMai/AquaMai.csproj b/AquaMai/AquaMai.csproj deleted file mode 100644 index 41bd7220..00000000 --- a/AquaMai/AquaMai.csproj +++ /dev/null @@ -1,320 +0,0 @@ - - - - Release - AnyCPU - {788BC472-59F7-46F6-B760-65C18BA74389} - Library - AquaMai - AquaMai - net472 - 512 - true - 12 - x64 - $(SolutionDir)Output\ - false - false - false - - - - false - None - true - prompt - 4 - true - false - - - - SDGA145 - true - true - x64 - - - - DEBUG - - - - - Libs\0Harmony.dll - - - Libs\AMDaemon.NET.dll - - - Libs\Assembly-CSharp.dll - - - Libs\Assembly-CSharp-firstpass.dll - - - packages\ILMerge.Fody.1.24.0\lib\net452\ILMerge.dll - - - Libs\MelonLoader.dll - - - Libs\Mono.Posix.dll - - - Libs\Mono.Security.dll - - - Libs\mscorlib.dll - - - - Libs\System.Configuration.dll - - - Libs\System.Numerics.dll - - - Libs\System.Core.dll - - - Libs\System.Security.dll - - - Libs\System.Xml.dll - - - packages\Samboy063.Tomlet.5.4.0\lib\net35\Tomlet.dll - - - Libs\Unity.Analytics.DataPrivacy.dll - - - Libs\Unity.TextMeshPro.dll - - - Libs\UnityEngine.dll - - - Libs\UnityEngine.AccessibilityModule.dll - - - Libs\UnityEngine.AIModule.dll - - - Libs\UnityEngine.AnimationModule.dll - - - Libs\UnityEngine.ARModule.dll - - - Libs\UnityEngine.AssetBundleModule.dll - - - Libs\UnityEngine.AudioModule.dll - - - Libs\UnityEngine.BaselibModule.dll - - - Libs\UnityEngine.ClothModule.dll - - - Libs\UnityEngine.ClusterInputModule.dll - - - Libs\UnityEngine.ClusterRendererModule.dll - - - Libs\UnityEngine.CoreModule.dll - - - Libs\UnityEngine.CrashReportingModule.dll - - - Libs\UnityEngine.DirectorModule.dll - - - Libs\UnityEngine.FileSystemHttpModule.dll - - - Libs\UnityEngine.GameCenterModule.dll - - - Libs\UnityEngine.GridModule.dll - - - Libs\UnityEngine.HotReloadModule.dll - - - Libs\UnityEngine.ImageConversionModule.dll - - - Libs\UnityEngine.IMGUIModule.dll - - - Libs\UnityEngine.InputModule.dll - - - Libs\UnityEngine.JSONSerializeModule.dll - - - Libs\UnityEngine.LocalizationModule.dll - - - Libs\UnityEngine.Networking.dll - - - Libs\UnityEngine.ParticleSystemModule.dll - - - Libs\UnityEngine.PerformanceReportingModule.dll - - - Libs\UnityEngine.Physics2DModule.dll - - - Libs\UnityEngine.PhysicsModule.dll - - - Libs\UnityEngine.ProfilerModule.dll - - - Libs\UnityEngine.ScreenCaptureModule.dll - - - Libs\UnityEngine.SharedInternalsModule.dll - - - Libs\UnityEngine.SpatialTracking.dll - - - Libs\UnityEngine.SpriteMaskModule.dll - - - Libs\UnityEngine.SpriteShapeModule.dll - - - Libs\UnityEngine.StreamingModule.dll - - - Libs\UnityEngine.StyleSheetsModule.dll - - - Libs\UnityEngine.SubstanceModule.dll - - - Libs\UnityEngine.TerrainModule.dll - - - Libs\UnityEngine.TerrainPhysicsModule.dll - - - Libs\UnityEngine.TextCoreModule.dll - - - Libs\UnityEngine.TextRenderingModule.dll - - - Libs\UnityEngine.TilemapModule.dll - - - Libs\UnityEngine.Timeline.dll - - - Libs\UnityEngine.TimelineModule.dll - - - Libs\UnityEngine.TLSModule.dll - - - Libs\UnityEngine.UI.dll - - - Libs\UnityEngine.UIElementsModule.dll - - - Libs\UnityEngine.UIModule.dll - - - Libs\UnityEngine.UmbraModule.dll - - - Libs\UnityEngine.UNETModule.dll - - - Libs\UnityEngine.UnityAnalyticsModule.dll - - - Libs\UnityEngine.UnityConnectModule.dll - - - Libs\UnityEngine.UnityTestProtocolModule.dll - - - Libs\UnityEngine.UnityWebRequestAssetBundleModule.dll - - - Libs\UnityEngine.UnityWebRequestAudioModule.dll - - - Libs\UnityEngine.UnityWebRequestModule.dll - - - Libs\UnityEngine.UnityWebRequestTextureModule.dll - - - Libs\UnityEngine.UnityWebRequestWWWModule.dll - - - Libs\UnityEngine.VehiclesModule.dll - - - Libs\UnityEngine.VFXModule.dll - - - Libs\UnityEngine.VideoModule.dll - - - Libs\UnityEngine.VRModule.dll - - - Libs\UnityEngine.WindModule.dll - - - Libs\UnityEngine.XRModule.dll - - - - - - ResXFileCodeGenerator - Locale.Designer.cs - - - Locale.resx - - - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - all - - - - - - - PreserveNewest - - - - diff --git a/AquaMai/AquaMai.sln b/AquaMai/AquaMai.sln index 1804c25b..4145c5cc 100644 --- a/AquaMai/AquaMai.sln +++ b/AquaMai/AquaMai.sln @@ -3,7 +3,40 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.6.33815.320 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AquaMai", "AquaMai.csproj", "{788BC472-59F7-46F6-B760-65C18BA74389}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AquaMai.Config.Interfaces", "AquaMai.Config.Interfaces/AquaMai.Config.Interfaces.csproj", "{50C59E41-2939-407E-9CB7-B186F33DDCF9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AquaMai.Config", "AquaMai.Config/AquaMai.Config.csproj", "{B9CA039A-A0F1-4C87-A33E-3F959974BB3A}" + ProjectSection(ProjectDependencies) = postProject + {50C59E41-2939-407E-9CB7-B186F33DDCF9} = {50C59E41-2939-407E-9CB7-B186F33DDCF9} + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AquaMai.Config.HeadlessLoader", "AquaMai.Config.HeadlessLoader/AquaMai.Config.HeadlessLoader.csproj", "{6B5E1F3E-D012-4CFB-A2FA-26A6CE06BE66}" + ProjectSection(ProjectDependencies) = postProject + {50C59E41-2939-407E-9CB7-B186F33DDCF9} = {50C59E41-2939-407E-9CB7-B186F33DDCF9} + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AquaMai.Build", "AquaMai.Build/AquaMai.Build.csproj", "{4C0C68C3-8B2E-4CA8-A26D-AE87CF2A38A5}" + ProjectSection(ProjectDependencies) = postProject + {6B5E1F3E-D012-4CFB-A2FA-26A6CE06BE66} = {6B5E1F3E-D012-4CFB-A2FA-26A6CE06BE66} + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AquaMai.Core", "AquaMai.Core/AquaMai.Core.csproj", "{33C0D4ED-6A84-4659-9A05-12D43D75D0B3}" + ProjectSection(ProjectDependencies) = postProject + {B9CA039A-A0F1-4C87-A33E-3F959974BB3A} = {B9CA039A-A0F1-4C87-A33E-3F959974BB3A} + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AquaMai.Mods", "AquaMai.Mods/AquaMai.Mods.csproj", "{8731C0E0-53BE-4B1B-9828-193E738C6865}" + ProjectSection(ProjectDependencies) = postProject + {B9CA039A-A0F1-4C87-A33E-3F959974BB3A} = {B9CA039A-A0F1-4C87-A33E-3F959974BB3A} + {33C0D4ED-6A84-4659-9A05-12D43D75D0B3} = {33C0D4ED-6A84-4659-9A05-12D43D75D0B3} + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AquaMai", "AquaMai/AquaMai.csproj", "{788BC472-59F7-46F6-B760-65C18BA74389}" + ProjectSection(ProjectDependencies) = postProject + {4C0C68C3-8B2E-4CA8-A26D-AE87CF2A38A5} = {4C0C68C3-8B2E-4CA8-A26D-AE87CF2A38A5} + {33C0D4ED-6A84-4659-9A05-12D43D75D0B3} = {33C0D4ED-6A84-4659-9A05-12D43D75D0B3} + {8731C0E0-53BE-4B1B-9828-193E738C6865} = {8731C0E0-53BE-4B1B-9828-193E738C6865} + EndProjectSection EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -11,6 +44,30 @@ Global Debug|Any CPU = Debug|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution + {50C59E41-2939-407E-9CB7-B186F33DDCF9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {50C59E41-2939-407E-9CB7-B186F33DDCF9}.Release|Any CPU.Build.0 = Release|Any CPU + {50C59E41-2939-407E-9CB7-B186F33DDCF9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {50C59E41-2939-407E-9CB7-B186F33DDCF9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B9CA039A-A0F1-4C87-A33E-3F959974BB3A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B9CA039A-A0F1-4C87-A33E-3F959974BB3A}.Release|Any CPU.Build.0 = Release|Any CPU + {B9CA039A-A0F1-4C87-A33E-3F959974BB3A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B9CA039A-A0F1-4C87-A33E-3F959974BB3A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6B5E1F3E-D012-4CFB-A2FA-26A6CE06BE66}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6B5E1F3E-D012-4CFB-A2FA-26A6CE06BE66}.Release|Any CPU.Build.0 = Release|Any CPU + {6B5E1F3E-D012-4CFB-A2FA-26A6CE06BE66}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6B5E1F3E-D012-4CFB-A2FA-26A6CE06BE66}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4C0C68C3-8B2E-4CA8-A26D-AE87CF2A38A5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4C0C68C3-8B2E-4CA8-A26D-AE87CF2A38A5}.Release|Any CPU.Build.0 = Release|Any CPU + {4C0C68C3-8B2E-4CA8-A26D-AE87CF2A38A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4C0C68C3-8B2E-4CA8-A26D-AE87CF2A38A5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {33C0D4ED-6A84-4659-9A05-12D43D75D0B3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {33C0D4ED-6A84-4659-9A05-12D43D75D0B3}.Release|Any CPU.Build.0 = Release|Any CPU + {33C0D4ED-6A84-4659-9A05-12D43D75D0B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {33C0D4ED-6A84-4659-9A05-12D43D75D0B3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8731C0E0-53BE-4B1B-9828-193E738C6865}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8731C0E0-53BE-4B1B-9828-193E738C6865}.Release|Any CPU.Build.0 = Release|Any CPU + {8731C0E0-53BE-4B1B-9828-193E738C6865}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8731C0E0-53BE-4B1B-9828-193E738C6865}.Debug|Any CPU.Build.0 = Debug|Any CPU {788BC472-59F7-46F6-B760-65C18BA74389}.Release|Any CPU.ActiveCfg = Release|Any CPU {788BC472-59F7-46F6-B760-65C18BA74389}.Release|Any CPU.Build.0 = Release|Any CPU {788BC472-59F7-46F6-B760-65C18BA74389}.Debug|Any CPU.ActiveCfg = Debug|Any CPU diff --git a/AquaMai/AquaMai/AquaMai.csproj b/AquaMai/AquaMai/AquaMai.csproj new file mode 100644 index 00000000..ee3d948d --- /dev/null +++ b/AquaMai/AquaMai/AquaMai.csproj @@ -0,0 +1,74 @@ + + + + Release + AnyCPU + {788BC472-59F7-46F6-B760-65C18BA74389} + Library + AquaMai + AquaMai + net472 + 512 + true + 12 + 414 + $(ProjectDir)../Libs/;$(AssemblySearchPaths) + $(ProjectDir)../Output/ + false + false + false + + + + false + None + true + prompt + 4 + true + false + + + + DEBUG + + + + + + + + + + + + + + + + + + + + + + + + + + + + AquaMai.Config.Interfaces.dll + + + AquaMai.Config.dll + + + AquaMai.Core.dll + + + AquaMai.Mods.dll + + + + diff --git a/AquaMai/AquaMai/AssemblyLoader.cs b/AquaMai/AquaMai/AssemblyLoader.cs new file mode 100644 index 00000000..e5d21cd5 --- /dev/null +++ b/AquaMai/AquaMai/AssemblyLoader.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Reflection; + +namespace AquaMai; + +public static class AssemblyLoader +{ + public enum AssemblyName + { + ConfigInterfaces, + Config, + Core, + Mods, + } + + private static readonly Dictionary Assemblies = new() + { + [AssemblyName.ConfigInterfaces] = "AquaMai.Config.Interfaces.dll", + [AssemblyName.Config] = "AquaMai.Config.dll", + [AssemblyName.Core] = "AquaMai.Core.dll", + [AssemblyName.Mods] = "AquaMai.Mods.dll", + }; + + private static readonly Dictionary LoadedAssemblies = []; + + public static Assembly GetAssembly(AssemblyName assemblyName) => LoadedAssemblies[assemblyName]; + + public static void LoadAssemblies() + { + foreach (var (assemblyName, assemblyFileName) in Assemblies) + { +# if DEBUG + MelonLoader.MelonLogger.Msg($"Loading assembly \"{assemblyFileName}\"..."); +# endif + LoadedAssemblies[assemblyName] = LoadAssemblyFromResource(assemblyFileName); + } + } + + private static Assembly LoadAssemblyFromResource(string assemblyName) + { + var executingAssembly = Assembly.GetExecutingAssembly(); + using var decompressedStream = executingAssembly.GetManifestResourceStream(assemblyName); + if (decompressedStream != null) + { + return AppDomain.CurrentDomain.Load(StreamToBytes(decompressedStream)); + } + using var compressedStream = executingAssembly.GetManifestResourceStream($"{assemblyName}.compressed"); + if (compressedStream != null) + { + return AppDomain.CurrentDomain.Load(DecompressToBytes(compressedStream)); + } + throw new Exception($"Embedded assembly \"{assemblyName}\" not found."); + } + + private static byte[] StreamToBytes(Stream stream) + { + if (stream == null) + { + return []; + } + using var memoryStream = new MemoryStream(); + stream.CopyTo(memoryStream); + return memoryStream.ToArray(); + } + + private static byte[] DecompressToBytes(Stream stream) => StreamToBytes(new DeflateStream(stream, CompressionMode.Decompress)); +} diff --git a/AquaMai/AquaMai/BuildInfo.cs b/AquaMai/AquaMai/BuildInfo.cs new file mode 100644 index 00000000..8f9443cc --- /dev/null +++ b/AquaMai/AquaMai/BuildInfo.cs @@ -0,0 +1,12 @@ +namespace AquaMai; + +public static class BuildInfo +{ + public const string Name = "AquaMai"; + public const string Description = "Mod for Sinmai"; + public const string Author = "Aza ft. Clansty ft. Menci"; + public const string Company = null; + public const string Version = "1.2.2"; + public const string DownloadLink = null; +} + diff --git a/AquaMai/AquaMai/Main.cs b/AquaMai/AquaMai/Main.cs new file mode 100644 index 00000000..505f057f --- /dev/null +++ b/AquaMai/AquaMai/Main.cs @@ -0,0 +1,39 @@ +using System.Reflection; +using System.Runtime.InteropServices; +using MelonLoader; + +namespace AquaMai; + +public class AquaMai : MelonMod +{ + public const string AQUAMAI_SAY = """ + 如果你在 dnSpy / ILSpy 里看到了这行字,请从 resources 中解包 DLLs。 + If you see this line in dnSpy / ILSpy, please unpack the DLLs from resources. + """; + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool SetConsoleOutputCP(uint wCodePageID); + + public override void OnInitializeMelon() + { + // Prevent Chinese characters from being garbled + SetConsoleOutputCP(65001); + + AssemblyLoader.LoadAssemblies(); + + var modsAssembly = AssemblyLoader.GetAssembly(AssemblyLoader.AssemblyName.Mods); + var coreAssembly = AssemblyLoader.GetAssembly(AssemblyLoader.AssemblyName.Core); + coreAssembly.GetType("AquaMai.Core.Startup") + .GetMethod("Initialize", BindingFlags.Public | BindingFlags.Static) + .Invoke(null, [modsAssembly, HarmonyInstance]); + } + + public override void OnGUI() + { + var coreAssembly = AssemblyLoader.GetAssembly(AssemblyLoader.AssemblyName.Core); + coreAssembly.GetType("AquaMai.Core.Startup") + .GetMethod("OnGUI", BindingFlags.Public | BindingFlags.Static) + .Invoke(null, []); + base.OnGUI(); + } +} diff --git a/AquaMai/Properties/AssemblyInfo.cs b/AquaMai/AquaMai/Properties/AssemblyInfo.cs similarity index 100% rename from AquaMai/Properties/AssemblyInfo.cs rename to AquaMai/AquaMai/Properties/AssemblyInfo.cs diff --git a/AquaMai/Attributes/ConfigCommentAttribute.cs b/AquaMai/Attributes/ConfigCommentAttribute.cs deleted file mode 100644 index ee9cee4b..00000000 --- a/AquaMai/Attributes/ConfigCommentAttribute.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; - -namespace AquaMai.Attributes; - -[AttributeUsage(AttributeTargets.Property)] -public class ConfigCommentAttribute(string en = null, string zh = null) : Attribute -{ - public string CommentEn { get; } = en; - public string CommentZh { get; } = zh; -} diff --git a/AquaMai/Attributes/GameVersionAttribute.cs b/AquaMai/Attributes/GameVersionAttribute.cs deleted file mode 100644 index b283849b..00000000 --- a/AquaMai/Attributes/GameVersionAttribute.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; - -namespace AquaMai.Attributes; - -[AttributeUsage(AttributeTargets.Class)] -public class GameVersionAttribute(uint minVersion = 0, uint maxVersion = 0) : Attribute -{ - public uint MinVersion { get; } = minVersion; - public uint MaxVersion { get; } = maxVersion; -} diff --git a/AquaMai/Cheat/Config.cs b/AquaMai/Cheat/Config.cs deleted file mode 100644 index e9593440..00000000 --- a/AquaMai/Cheat/Config.cs +++ /dev/null @@ -1,21 +0,0 @@ -using AquaMai.Attributes; - -namespace AquaMai.Cheat; - -public class Config -{ - [ConfigComment( - en: "Unlock normally event-only tickets", - zh: "解锁游戏里所有可能的跑图券")] - public bool TicketUnlock { get; set; } - - [ConfigComment( - en: "Unlock maps that are not in this version", - zh: "解锁游戏里所有的区域,包括非当前版本的(并不会帮你跑完)")] - public bool MapUnlock { get; set; } - - [ConfigComment( - en: "Unlock Utage without the need of DXRating 10000", - zh: "不需要万分也可以进宴会场")] - public bool UnlockUtage { get; set; } -} diff --git a/AquaMai/Cheat/MapUnlock.cs b/AquaMai/Cheat/MapUnlock.cs deleted file mode 100644 index 49ab1a07..00000000 --- a/AquaMai/Cheat/MapUnlock.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Manager.MaiStudio; -using HarmonyLib; - -namespace AquaMai.Cheat -{ - public class MapUnlock - { - // For any map, return the event ID 1 to unlock it - [HarmonyPrefix] - [HarmonyPatch(typeof(MapData), "get_OpenEventId")] - public static bool get_OpenEventId(ref StringID __result) - { - var id = new Manager.MaiStudio.Serialize.StringID - { - id = 1, - str = "無期限常時解放" - }; - - var sid = new StringID(); - sid.Init(id); - - __result = sid; - return false; - } - } -} \ No newline at end of file diff --git a/AquaMai/Cheat/TicketUnlock.cs b/AquaMai/Cheat/TicketUnlock.cs deleted file mode 100644 index 697a516c..00000000 --- a/AquaMai/Cheat/TicketUnlock.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Manager.MaiStudio; -using HarmonyLib; - -namespace AquaMai.Cheat -{ - /** - * Unlock tickets that are typically locked unless a specific event is open. - */ - public class TicketUnlock - { - // For any ticket, return the event ID 1 to unlock it - [HarmonyPrefix] - [HarmonyPatch(typeof(TicketData), "get_ticketEvent")] - public static bool get_ticketEvent(ref StringID __result) - { - var id = new Manager.MaiStudio.Serialize.StringID - { - id = 1, - str = "無期限常時解放" - }; - - var sid = new StringID(); - sid.Init(id); - - __result = sid; - return false; - } - - // 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 - [HarmonyPrefix] - [HarmonyPatch(typeof(TicketData), "get_maxCount")] - public static bool get_maxCount(ref int __result) - { - __result = 0; - return false; - } - } -} \ No newline at end of file diff --git a/AquaMai/Cheat/UnlockUtage.cs b/AquaMai/Cheat/UnlockUtage.cs deleted file mode 100644 index 7aa8ace4..00000000 --- a/AquaMai/Cheat/UnlockUtage.cs +++ /dev/null @@ -1,20 +0,0 @@ -using AquaMai.Attributes; -using HarmonyLib; -using MAI2System; -using Manager; - -namespace AquaMai.Cheat -{ - [GameVersion(24000)] - public class UnlockUtage - { - [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; - } - } -} diff --git a/AquaMai/Config.cs b/AquaMai/Config.cs deleted file mode 100644 index e4af3865..00000000 --- a/AquaMai/Config.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using AquaMai.Attributes; -using AquaMai.CustomKeyMap; - -namespace AquaMai; - -[SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")] -public class Config -{ - [ConfigComment( - en: "UX: User Experience Improvements", - zh: """ - - 试试使用 MaiChartManager 图形化配置 AquaMai 吧! - https://github.com/clansty/MaiChartManager - - 用户体验改进 - """)] - public UX.Config UX { get; set; } = new(); - - [ConfigComment( - en: "Cheat: You control the buttons you press", - zh: "“作弊”功能")] - public Cheat.Config Cheat { get; set; } = new(); - - [ConfigComment( - en: "Fix: Fix some potential issues", - zh: "修复一些潜在的问题")] - public Fix.Config Fix { get; set; } = new(); - - [ConfigComment( - zh: "实用工具")] - public Utils.Config Utils { get; set; } = new(); - - [ConfigComment( - en: "Time Saving: Skip some unnecessary screens", - zh: "节省一些不知道有用没用的时间,跳过一些不必要的界面")] - public TimeSaving.Config TimeSaving { get; set; } = new(); - - [ConfigComment( - en: "Visual effects of notes and judgment display and some other textures", - zh: "音符和判定表示以及一些其他贴图的视觉效果调整")] - public Visual.Config Visual { get; set; } = new(); - - [ConfigComment( - zh: "Mod 内功能的按键设置")] - public ModKeyMap.Config ModKeyMap { get; set; } = new(); - - [ConfigComment( - zh: "窗口相关设置")] - public WindowState.Config WindowState { get; set; } = new(); - - [ConfigComment( - en: "Custom camera ID settings", - zh: "自定义摄像头 ID")] - public CustomCameraId.Config CustomCameraId { get; set; } = new(); - - [ConfigComment( - zh: "触摸灵敏度设置")] - public TouchSensitivity.Config TouchSensitivity { get; set; } = new(); - - [ConfigComment( - zh: "自定义按键映射")] - public CustomKeyMap.Config CustomKeyMap { get; set; } = new(); -} diff --git a/AquaMai/ConfigGenerator.cs b/AquaMai/ConfigGenerator.cs deleted file mode 100644 index 2e3a30ce..00000000 --- a/AquaMai/ConfigGenerator.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using System.IO; -using System.Reflection; -using AquaMai.Attributes; -using Tomlet; -using Tomlet.Models; - -namespace AquaMai; - -public static class ConfigGenerator -{ - public static void GenerateConfig() - { - var defaultConfig = new Config(); - foreach (var lang in (string[]) ["en", "zh"]) - { - File.WriteAllText($"AquaMai.{lang}.toml", SerializeConfigWithComments(defaultConfig, lang)); - } - } - - public static string SerializeConfigWithComments(Config config, string lang) - { - var tomlDoc = TomletMain.DocumentFrom(config); - MakeComments(tomlDoc, typeof(Config), lang); - return tomlDoc.SerializedValue; - } - - private static void MakeComments(TomlTable table, Type configType, string lang) - { - foreach (var property in configType.GetProperties()) - { - var value = table.GetValue(property.Name); - var comment = property.GetCustomAttribute(); - if (comment != null) - { - value.Comments.PrecedingComment = lang switch - { - "en" => comment.CommentEn, - "zh" => comment.CommentZh, - _ => throw new ArgumentException($"Unsupported language: {lang}") - }; - } - - if (value is TomlTable subTable) - { - MakeComments(subTable, property.PropertyType, lang); - } - } - } -} diff --git a/AquaMai/CustomCameraId/Config.cs b/AquaMai/CustomCameraId/Config.cs deleted file mode 100644 index 3e08055e..00000000 --- a/AquaMai/CustomCameraId/Config.cs +++ /dev/null @@ -1,40 +0,0 @@ -using AquaMai.Attributes; - -namespace AquaMai.CustomCameraId; - -public class Config -{ - [ConfigComment( - en: """ - Enable custom CameraId - If enabled, you can customize the game to use the specified camera - """, - zh: """ - 是否启用自定义摄像头ID - 启用后可以指定游戏使用的摄像头 - """)] - public bool Enable { get; set; } - - [ConfigComment( - en: "Print the camera list to the log when starting, can be used as a basis for modification", - zh: "启动时打印摄像头列表到日志中,可以作为修改的依据")] - public bool PrintCameraList { get; set; } = false; - - [ConfigComment( - en: "DX Pass 1P", - zh: "DX Pass 1P")] - public int LeftQrCamera { get; set; } = 0; - - [ConfigComment( - en: "DX Pass 2P", - zh: "DX Pass 2P")] - public int RightQrCamera { get; set; } = 0; - - [ConfigComment( - zh: "玩家摄像头")] - public int PhotoCamera { get; set; } = 0; - - [ConfigComment( - zh: "二维码扫描摄像头")] - public int ChimeCamera { get; set; } = 0; -} diff --git a/AquaMai/CustomCameraId/PrintCameraList.cs b/AquaMai/CustomCameraId/PrintCameraList.cs deleted file mode 100644 index 5de15aca..00000000 --- a/AquaMai/CustomCameraId/PrintCameraList.cs +++ /dev/null @@ -1,31 +0,0 @@ -using MelonLoader; -using UnityEngine; - -namespace AquaMai.CustomCameraId; - -public class PrintCameraList -{ - public static void DoCustomPatch(HarmonyLib.Harmony _) - { - 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}"); - } - } -} diff --git a/AquaMai/CustomKeyMap/Config.cs b/AquaMai/CustomKeyMap/Config.cs deleted file mode 100644 index 7fe5ed45..00000000 --- a/AquaMai/CustomKeyMap/Config.cs +++ /dev/null @@ -1,32 +0,0 @@ -using AquaMai.Attributes; - -namespace AquaMai.CustomKeyMap; - -public class Config -{ - [ConfigComment( - en: "These settings will work regardless of whether you have enabled segatools' io4 emulation", - zh: "这里的设置无论你是否启用了 segatools 的 io4 模拟都会工作")] - public bool Enable { get; set; } - - public KeyCodeID Test { get; set; } = (KeyCodeID)115; - public KeyCodeID Service { get; set; } = (KeyCodeID)5; - public KeyCodeID Button1_1P { get; set; } = (KeyCodeID)67; - public KeyCodeID Button2_1P { get; set; } = (KeyCodeID)49; - public KeyCodeID Button3_1P { get; set; } = (KeyCodeID)48; - public KeyCodeID Button4_1P { get; set; } = (KeyCodeID)47; - public KeyCodeID Button5_1P { get; set; } = (KeyCodeID)68; - public KeyCodeID Button6_1P { get; set; } = (KeyCodeID)70; - public KeyCodeID Button7_1P { get; set; } = (KeyCodeID)45; - public KeyCodeID Button8_1P { get; set; } = (KeyCodeID)61; - public KeyCodeID Select_1P { get; set; } = (KeyCodeID)25; - public KeyCodeID Button1_2P { get; set; } = (KeyCodeID)80; - public KeyCodeID Button2_2P { get; set; } = (KeyCodeID)81; - public KeyCodeID Button3_2P { get; set; } = (KeyCodeID)78; - public KeyCodeID Button4_2P { get; set; } = (KeyCodeID)75; - public KeyCodeID Button5_2P { get; set; } = (KeyCodeID)74; - public KeyCodeID Button6_2P { get; set; } = (KeyCodeID)73; - public KeyCodeID Button7_2P { get; set; } = (KeyCodeID)76; - public KeyCodeID Button8_2P { get; set; } = (KeyCodeID)79; - public KeyCodeID Select_2P { get; set; } = (KeyCodeID)84; -} diff --git a/AquaMai/CustomKeyMap/Enable.cs b/AquaMai/CustomKeyMap/Enable.cs deleted file mode 100644 index 0b305b6a..00000000 --- a/AquaMai/CustomKeyMap/Enable.cs +++ /dev/null @@ -1,15 +0,0 @@ -using DB; -using HarmonyLib; - -namespace AquaMai.CustomKeyMap; - -public class Enable -{ - [HarmonyPatch(typeof(JvsButtonTableRecord), MethodType.Constructor, typeof(int), typeof(string), typeof(string), typeof(int), typeof(string), typeof(int), typeof(int), typeof(int))] - [HarmonyPostfix] - public static void JvsButtonTableRecordConstructor(JvsButtonTableRecord __instance, string Name) - { - var prop = (DB.KeyCodeID)typeof(Config).GetProperty(Name).GetValue(AquaMai.AppConfig.CustomKeyMap); - __instance.SubstituteKey = prop; - } -} diff --git a/AquaMai/Fix/Config.cs b/AquaMai/Fix/Config.cs deleted file mode 100644 index e63a406f..00000000 --- a/AquaMai/Fix/Config.cs +++ /dev/null @@ -1,90 +0,0 @@ -using AquaMai.Attributes; - -namespace AquaMai.Fix; - -public class Config -{ - [ConfigComment( - en: "Allow login with higher data version", - zh: """ - 原先如果你的账号版本比当前游戏设定的版本高的话,就会不能登录 - 开了这个选项之后就可以登录了,不过你的账号版本还是会被设定为当前游戏的版本 - """)] - public bool SkipVersionCheck { get; set; } - - [ConfigComment( - zh: """ - 如果你在用未经修改的客户端,会默认加密到服务器的连接,而连接私服的时候不应该加密 - 开了这个选项之后就不会加密连接了,同时也会移除不同版本的客户端可能会对 API 接口加的后缀 - 正常情况下,请保持这个选项开启 - """)] - public bool RemoveEncryption { get; set; } = true; - - [ConfigComment( - zh: "如果要配置店内招募的话,应该要把这个关闭")] - public bool ForceAsServer { get; set; } = true; - - [ConfigComment( - en: "Force the game to be in FreePlay mode", - zh: "强制改为免费游玩(FreePlay)")] - public bool ForceFreePlay { get; set; } = true; - - [ConfigComment( - en: "Force the game to be in PaidPlay mode with 24 coins locked, conflicts with ForceFreePlay", - zh: "强制付费游玩并锁定 24 个币,和 ForceFreePlay 冲突")] - public bool ForcePaidPlay { get; set; } - - [ConfigComment( - en: "Add notes sprite to the pool to prevent use up", - zh: "增加更多待命的音符贴图,防止奇怪的自制谱用完音符贴图池")] - public int ExtendNotesPool { get; set; } - - [ConfigComment( - en: "Force the frame rate limit to 60 FPS and disable vSync. Do not use if your game has no issues", - zh: "强制设置帧率上限为 60 帧并关闭垂直同步。如果你的游戏没有问题,请不要使用")] - public bool FrameRateLock { get; set; } - - [ConfigComment( - en: """ - Use Microsoft YaHei Bold to display characters not in the font library - Cannot be used together with CustomFont - """, - zh: """ - 在显示字库里没有的字时使用微软雅黑 Bold 显示 - 不可以和 CustomFont 一起使用 - """)] - public bool FontFix { get; set; } - - [ConfigComment( - 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 bool RealisticRandomJudge { get; set; } - - [ConfigComment( - en: "Cannot be used together with HideHanabi", - zh: """ - 修复 1p 模式下的烟花大小 - 不能和 HideHanabi 一起使用 - """)] - public bool HanabiFix { get; set; } - - [ConfigComment( - en: "Prevent gray network caused by mistakenly thinking it's an AimeDB server issue", - zh: "防止因错误认为 AimeDB 服务器问题引起的灰网,建议开启")] - public bool IgnoreAimeServerError { get; set; } - - [ConfigComment( - en: "Reset touch panel after playing track", - zh: "在游玩一首曲目后重置触摸面板")] - public bool TouchResetAfterTrack { get; set; } -} diff --git a/AquaMai/Fix/DisableReboot.cs b/AquaMai/Fix/DisableReboot.cs deleted file mode 100644 index 984e381b..00000000 --- a/AquaMai/Fix/DisableReboot.cs +++ /dev/null @@ -1,86 +0,0 @@ -using HarmonyLib; -using Manager.Operation; - -namespace AquaMai.Fix -{ - 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; - } - } -} diff --git a/AquaMai/Fix/FixCharaCrash.cs b/AquaMai/Fix/FixCharaCrash.cs deleted file mode 100644 index 36d58b3f..00000000 --- a/AquaMai/Fix/FixCharaCrash.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; -using System.Collections.Generic; -using HarmonyLib; -using Process; -using Util; - -namespace AquaMai.Fix -{ - /** - * Fix character selection crashing due to missing character data - */ - public class FixCharaCrash - { - // 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 ____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; - } - } -} diff --git a/AquaMai/Fix/FontFix.cs b/AquaMai/Fix/FontFix.cs deleted file mode 100644 index 400989c7..00000000 --- a/AquaMai/Fix/FontFix.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System.Collections.Generic; -using HarmonyLib; -using MelonLoader; -using TMPro; -using UnityEngine; -using UnityEngine.TextCore.LowLevel; - -namespace AquaMai.Fix; - -public class FontFix -{ - private static TMP_FontAsset fontAsset; - private static List fixedFonts = []; - - public static void DoCustomPatch(HarmonyLib.Harmony h) - { - var font = new Font(@"C:\Windows\Fonts\msyhbd.ttc"); - fontAsset = TMP_FontAsset.CreateFontAsset(font, 90, 9, GlyphRenderMode.SDFAA, 8192, 8192); - } - - [HarmonyPatch(typeof(TextMeshProUGUI), "Awake")] - [HarmonyPostfix] - public static void PostFix(TextMeshProUGUI __instance) - { - if (fixedFonts.Contains(__instance.font)) return; -# if DEBUG - MelonLogger.Msg($"[FontFix] Fixing font: {__instance.font.name}"); -# endif - __instance.font.fallbackFontAssetTable.Add(fontAsset); - fixedFonts.Add(__instance.font); - } -} diff --git a/AquaMai/Fix/ForceFreePlay.cs b/AquaMai/Fix/ForceFreePlay.cs deleted file mode 100644 index 430f7148..00000000 --- a/AquaMai/Fix/ForceFreePlay.cs +++ /dev/null @@ -1,14 +0,0 @@ -using HarmonyLib; - -namespace AquaMai.Fix; - -public class ForceFreePlay -{ - [HarmonyPrefix] - [HarmonyPatch(typeof(Manager.Credit), "IsFreePlay")] - private static bool PreIsFreePlay(ref bool __result) - { - __result = true; - return false; - } -} diff --git a/AquaMai/Fix/ForcePaidPlay.cs b/AquaMai/Fix/ForcePaidPlay.cs deleted file mode 100644 index 2e66ccbc..00000000 --- a/AquaMai/Fix/ForcePaidPlay.cs +++ /dev/null @@ -1,30 +0,0 @@ -using HarmonyLib; - -namespace AquaMai.Fix; - -public class ForcePaidPlay -{ - [HarmonyPrefix] - [HarmonyPatch(typeof(Manager.Credit), "IsFreePlay")] - private static bool PreIsFreePlay(ref bool __result) - { - __result = false; - return false; - } - - [HarmonyPrefix] - [HarmonyPatch(typeof(Manager.Credit), "IsGameCostEnough")] - private static bool PreIsGameCostEnough(ref bool __result) - { - __result = true; - return false; - } - - [HarmonyPrefix] - [HarmonyPatch(typeof(AMDaemon.CreditUnit), "Credit", MethodType.Getter)] - private static bool PreCredit(ref uint __result) - { - __result = 24; - return false; - } -} diff --git a/AquaMai/Fix/FrameRateLock.cs b/AquaMai/Fix/FrameRateLock.cs deleted file mode 100644 index 0375aba4..00000000 --- a/AquaMai/Fix/FrameRateLock.cs +++ /dev/null @@ -1,12 +0,0 @@ -using UnityEngine; - -namespace AquaMai.Fix; - -public class FrameRateLock -{ - public static void DoCustomPatch(HarmonyLib.Harmony h) - { - Application.targetFrameRate = 60; - QualitySettings.vSyncCount = 0; - } -} diff --git a/AquaMai/Fix/HanabiFix.cs b/AquaMai/Fix/HanabiFix.cs deleted file mode 100644 index 429037fa..00000000 --- a/AquaMai/Fix/HanabiFix.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Fx; -using HarmonyLib; -using Monitor; -using UnityEngine; - -namespace AquaMai.Fix; - -public class HanabiFix -{ - [HarmonyPatch(typeof(TapCEffect), "SetUpParticle")] - [HarmonyPostfix] - public static void FixMaxSize(TapCEffect __instance, FX_Mai2_Note_Color ____particleControler) - { - var entities = ____particleControler.GetComponentsInChildren(true); - foreach (var entity in entities) - { - entity.maxParticleSize = 1f; - } - } -} diff --git a/AquaMai/Fix/IgnoreAimeServerError.cs b/AquaMai/Fix/IgnoreAimeServerError.cs deleted file mode 100644 index 4027026e..00000000 --- a/AquaMai/Fix/IgnoreAimeServerError.cs +++ /dev/null @@ -1,15 +0,0 @@ -using HarmonyLib; -using Manager; - -namespace AquaMai.Fix; - -public class IgnoreAimeServerError -{ - [HarmonyPatch(typeof(OperationManager), "IsAliveAimeServer", MethodType.Getter)] - [HarmonyPrefix] - public static bool Prefix(ref bool __result) - { - __result = true; - return false; - } -} diff --git a/AquaMai/Fix/RealisticRandomJudge.cs b/AquaMai/Fix/RealisticRandomJudge.cs deleted file mode 100644 index f379f7be..00000000 --- a/AquaMai/Fix/RealisticRandomJudge.cs +++ /dev/null @@ -1,25 +0,0 @@ -using HarmonyLib; -using Manager; - -namespace AquaMai.Fix; - -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; - } -} diff --git a/AquaMai/Fix/SkipVersionCheck.cs b/AquaMai/Fix/SkipVersionCheck.cs deleted file mode 100644 index 1d1c2933..00000000 --- a/AquaMai/Fix/SkipVersionCheck.cs +++ /dev/null @@ -1,16 +0,0 @@ -using HarmonyLib; -using Process.Entry.State; - -namespace AquaMai.Fix -{ - public class SkipVersionCheck - { - [HarmonyPrefix] - [HarmonyPatch(typeof(ConfirmPlay), "IsValidVersion")] - public static bool IsValidVersion(ref bool __result) - { - __result = true; - return false; - } - } -} diff --git a/AquaMai/Libs/.gitignore b/AquaMai/Libs/.gitignore new file mode 100644 index 00000000..0f6e1c13 --- /dev/null +++ b/AquaMai/Libs/.gitignore @@ -0,0 +1,2 @@ +/Assembly-CSharp.dll +/AMDaemon.NET.dll diff --git a/AquaMai/Libs/Mono.Cecil.dll b/AquaMai/Libs/Mono.Cecil.dll new file mode 100644 index 0000000000000000000000000000000000000000..18735f93b243c4f3960f64b964366339fcc60ea2 GIT binary patch literal 339456 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 - var compatible = true; - if (versionAttr != null) - { - if (versionAttr.MinVersion > 0 && versionAttr.MinVersion > GameInfo.GameVersion) compatible = false; - if (versionAttr.MaxVersion > 0 && versionAttr.MaxVersion < GameInfo.GameVersion) compatible = false; - } - - if (!compatible) - { - if (!isNested) - { - MelonLogger.Warning(string.Format(Locale.SkipIncompatiblePatch, type)); - } - - return; - } - - MelonLogger.Msg($"> Patching {type}"); - try - { - HarmonyInstance.PatchAll(type); - foreach (var nested in type.GetNestedTypes()) - { - Patch(nested, true); - } - - var customMethod = type.GetMethod("DoCustomPatch", BindingFlags.Public | BindingFlags.Static); - customMethod?.Invoke(null, [HarmonyInstance]); - } - catch (Exception e) - { - MelonLogger.Error($"Failed to patch {type}: {e}"); - _hasErrors = true; - } - } - - /** - * Apply patches using reflection, based on the settings - */ - private void ApplyPatches() - { - // Iterate over all properties of AppConfig - foreach (var categoryProp in AppConfig.GetType().GetProperties()) - { - // Get the value of the category property (e.g., UX, Cheat) - var categoryValue = categoryProp.GetValue(AppConfig); - if (categoryValue == null) continue; - var categoryType = categoryValue.GetType(); - - // Iterate over properties in the category (e.g., SkipWarningScreen, SinglePlayer) - foreach (var settingProp in categoryType.GetProperties()) - { - // The property should be a boolean - if (settingProp.PropertyType != typeof(bool)) continue; - - // Check if the boolean value is true - if (!(bool)settingProp.GetValue(categoryValue)) continue; - - // Get the Type from the config directive name - var directiveType = Type.GetType($"AquaMai.{categoryProp.Name}.{settingProp.Name}"); - - // If the type is found, call the Patch method - if (directiveType != null) - { - Patch(directiveType); - } - } - } - } - - [DllImport("kernel32.dll", SetLastError = true)] - private static extern bool SetConsoleOutputCP(uint wCodePageID); - - private static void InitLocale() - { - if (!string.IsNullOrEmpty(AppConfig.UX.Locale)) - { - Locale.Culture = CultureInfo.GetCultureInfo(AppConfig.UX.Locale); - return; - } - - Locale.Culture = Application.systemLanguage switch - { - SystemLanguage.Chinese or SystemLanguage.ChineseSimplified or SystemLanguage.ChineseTraditional => CultureInfo.GetCultureInfo("zh"), - SystemLanguage.English => CultureInfo.GetCultureInfo("en"), - _ => CultureInfo.InvariantCulture - }; - } - - public override void OnInitializeMelon() - { - // Prevent Chinese characters from being garbled - SetConsoleOutputCP(65001); - - MelonLogger.Msg("Loading mod settings..."); - - // Check if AquaMai.toml exists - if (!File.Exists("AquaMai.toml")) - { - ConfigGenerator.GenerateConfig(); - MelonLogger.Error("======================================!!!"); - MelonLogger.Error("AquaMai.toml not found! Please create it."); - MelonLogger.Error("找不到配置文件 AquaMai.toml!请创建。"); - MelonLogger.Error("Example copied to AquaMai.en.toml"); - MelonLogger.Error("示例已复制到 AquaMai.zh.toml"); - MelonLogger.Error("========================================="); - return; - } - - // Read AquaMai.toml to load settings - AppConfig = TomletMain.To(File.ReadAllText("AquaMai.toml")); - - // Init locale with patching C# runtime - // https://stackoverflow.com/questions/1952638/single-assembly-multi-language-windows-forms-deployment-ilmerge-and-satellite-a - Patch(typeof(I18nSingleAssemblyHook)); - InitLocale(); - - // Fixes that does not have side effects - // These don't need to be configurable - - // Helpers - Patch(typeof(MessageHelper)); - Patch(typeof(MusicDirHelper)); - Patch(typeof(SharedInstances)); - Patch(typeof(GuiSizes)); - // Fixes - Patch(typeof(FixCharaCrash)); - Patch(typeof(BasicFix)); - Patch(typeof(DisableReboot)); - Patch(typeof(ExtendNotesPool)); - Patch(typeof(FixCheckAuth)); - Patch(typeof(DebugFeature)); - Patch(typeof(FixConnSlide)); - Patch(typeof(FestivalQuickRetryFix)); - // Visual - Patch(typeof(FixSlideAutoPlay)); // Rename: SlideAutoPlayTweak -> FixSlideAutoPlay, 不过这个应该无副作用所以不需要改配置文件 - Patch(typeof(FixCircleSlideJudge)); // 这个我觉得算无副作用, 可以常开 - Patch(typeof(FixLevelDisplay)); - Patch(typeof(CustomLogo)); - // UX - Patch(typeof(CustomVersionString)); - Patch(typeof(CustomPlaceName)); - Patch(typeof(RunCommandOnEvents)); - // Utils - Patch(typeof(JudgeAdjust)); - Patch(typeof(TouchPanelBaudRate)); - // ModKeyMap - // TODO: Make it configurable - Patch(typeof(ModKeyListener)); - Patch(typeof(QuickSkip)); - Patch(typeof(TestProof)); - Patch(typeof(PractiseMode)); - Patch(typeof(HideSelfMadeCharts)); - -# if CI - Patch(typeof(CiBuildAlert)); -# endif - - // Apply patches based on the settings - ApplyPatches(); - - if (_hasErrors) - { - MelonLogger.Warning("========================================================================!!!\n" + Locale.LoadError); - MelonLogger.Warning("==========================================================================="); - } - -# if CI - MelonLogger.Warning(Locale.CiBuildAlertTitle); - MelonLogger.Warning(Locale.CiBuildAlertContent); -# endif - - MelonLogger.Msg(Locale.Loaded); - } - - public override void OnGUI() - { - GuiSizes.SetupStyles(); - base.OnGUI(); - } - } -} diff --git a/AquaMai/ModKeyMap/Config.cs b/AquaMai/ModKeyMap/Config.cs deleted file mode 100644 index 36443109..00000000 --- a/AquaMai/ModKeyMap/Config.cs +++ /dev/null @@ -1,54 +0,0 @@ -using AquaMai.Attributes; - -namespace AquaMai.ModKeyMap; - -public class Config -{ - [ConfigComment( - en: "Skip to next step", - zh: """ - 跳过登录过程中的界面直接进入选歌界面 - 在选歌界面直接结束游戏 - """)] - public ModKeyCode QuickSkip { get; set; } = ModKeyCode.None; - - public bool QuickSkipLongPress { get; set; } - - [ConfigComment( - en: "Quick retry in-game", - zh: "游戏内快速重试")] - public ModKeyCode InGameRetry { get; set; } = ModKeyCode.None; - - public bool InGameRetryLongPress { get; set; } - - [ConfigComment( - en: "Quick skip in-game", - zh: "游戏内快速跳过")] - public ModKeyCode InGameSkip { get; set; } = ModKeyCode.None; - - public bool InGameSkipLongPress { get; set; } - - [ConfigComment( - en: "Enter game test mode", - zh: "进入游戏测试模式")] - public ModKeyCode TestMode { get; set; } = ModKeyCode.Test; - - public bool TestModeLongPress { get; set; } - - [ConfigComment( - zh: "练习模式")] - public ModKeyCode PractiseMode { get; set; } = ModKeyCode.None; - - public bool PractiseModeLongPress { get; set; } - - [ConfigComment( - zh: "选歌界面隐藏所有自制谱")] - public ModKeyCode HideSelfMadeCharts { get; set; } = ModKeyCode.None; - - public bool HideSelfMadeChartsLongPress { get; set; } - - [ConfigComment( - en: "Hold the bottom four buttons (3456) for official quick retry (non-utage only)", - zh: "按住下方四个按钮(3456)使用官方快速重开(仅对非宴谱有效)")] - public bool EnableNativeQuickRetry { get; set; } -} diff --git a/AquaMai/ModKeyMap/EnableNativeQuickRetry.cs b/AquaMai/ModKeyMap/EnableNativeQuickRetry.cs deleted file mode 100644 index 30077408..00000000 --- a/AquaMai/ModKeyMap/EnableNativeQuickRetry.cs +++ /dev/null @@ -1,19 +0,0 @@ -using AquaMai.Attributes; -using HarmonyLib; -using Manager; -using Monitor; - -namespace AquaMai.ModKeyMap; - -[GameVersion(23000)] -public class EnableNativeQuickRetry -{ - [HarmonyPrefix] - [HarmonyPatch(typeof(QuickRetry), "IsQuickRetryEnable")] - public static bool OnQuickRetryIsQuickRetryEnable(ref bool __result) - { - var isUtageProperty = Traverse.Create(typeof(GameManager)).Property("IsUtage"); - __result = !isUtageProperty.PropertyExists() || !isUtageProperty.GetValue(); - return false; - } -} diff --git a/AquaMai/ModKeyMap/ModKeyListener.cs b/AquaMai/ModKeyMap/ModKeyListener.cs deleted file mode 100644 index 031437cb..00000000 --- a/AquaMai/ModKeyMap/ModKeyListener.cs +++ /dev/null @@ -1,145 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using HarmonyLib; -using Main; -using Manager; -using MelonLoader; -using UnityEngine; - -namespace AquaMai.ModKeyMap; - -public static class ModKeyListener -{ - private static readonly Dictionary _keyPressFrames = new(); - private static readonly Dictionary _keyPressFramesPrev = new(); - - static ModKeyListener() - { - foreach (ModKeyCode key in Enum.GetValues(typeof(ModKeyCode))) - { - _keyPressFrames[key] = 0; - _keyPressFramesPrev[key] = 0; - } - } - - [HarmonyPostfix] - [HarmonyPatch(typeof(GameMainObject), "Update")] - public static void CheckLongPush() - { - foreach (ModKeyCode key in Enum.GetValues(typeof(ModKeyCode))) - { - _keyPressFramesPrev[key] = _keyPressFrames[key]; - if (GetKeyPush(key)) - { -# if DEBUG - MelonLogger.Msg($"CheckLongPush {key} is push {_keyPressFrames[key]}"); -# endif - _keyPressFrames[key]++; - } - else - { - _keyPressFrames[key] = 0; - } - } - } - - public static bool GetKeyPush(ModKeyCode key) => - key switch - { - ModKeyCode.None => false, - < ModKeyCode.Select1P => Input.GetKey(key.GetKeyCode()), - ModKeyCode.Test => InputManager.GetSystemInputPush(InputManager.SystemButtonSetting.ButtonTest), - ModKeyCode.Service => InputManager.GetSystemInputPush(InputManager.SystemButtonSetting.ButtonService), - ModKeyCode.Select1P => InputManager.GetButtonPush(0, InputManager.ButtonSetting.Select), - ModKeyCode.Select2P => InputManager.GetButtonPush(1, InputManager.ButtonSetting.Select), - _ => throw new ArgumentOutOfRangeException(nameof(key), key, "我也不知道这是什么键") - }; - - public static bool GetKeyDown(ModKeyCode key) - { - // return key switch - // { - // ModKeyCode.None => false, - // < ModKeyCode.Select1P => Input.GetKeyDown(key.GetKeyCode()), - // ModKeyCode.Test => InputManager.GetSystemInputDown(InputManager.SystemButtonSetting.ButtonTest), - // ModKeyCode.Service => InputManager.GetSystemInputDown(InputManager.SystemButtonSetting.ButtonService), - // ModKeyCode.Select1P => InputManager.GetButtonDown(0, InputManager.ButtonSetting.Select), - // ModKeyCode.Select2P => InputManager.GetButtonDown(1, InputManager.ButtonSetting.Select), - // _ => throw new ArgumentOutOfRangeException(nameof(key), key, "我也不知道这是什么键") - // }; - - // 不用这个,我们检测按键是否弹起以及弹起之前按下的时间是否小于 30,这样可以防止要长按时按下的时候就触发 - return _keyPressFrames[key] == 0 && 0 < _keyPressFramesPrev[key] && _keyPressFramesPrev[key] < 30; - } - - public static bool GetKeyDownOrLongPress(ModKeyCode key, bool isLongPress) - { - bool ret; - if (isLongPress) - { - ret = _keyPressFrames[key] == 60; - } - else - { - ret = GetKeyDown(key); - } - -# if DEBUG - if (ret) - { - MelonLogger.Msg($"Key {key} is pressed"); - MelonLogger.Msg(new StackTrace()); - } -# endif - return ret; - } - - private static KeyCode GetKeyCode(this ModKeyCode modKeyCode) => - modKeyCode switch - { - ModKeyCode.Alpha0 => KeyCode.Alpha0, - ModKeyCode.Alpha1 => KeyCode.Alpha1, - ModKeyCode.Alpha2 => KeyCode.Alpha2, - ModKeyCode.Alpha3 => KeyCode.Alpha3, - ModKeyCode.Alpha4 => KeyCode.Alpha4, - ModKeyCode.Alpha5 => KeyCode.Alpha5, - ModKeyCode.Alpha6 => KeyCode.Alpha6, - ModKeyCode.Alpha7 => KeyCode.Alpha7, - ModKeyCode.Alpha8 => KeyCode.Alpha8, - ModKeyCode.Alpha9 => KeyCode.Alpha9, - ModKeyCode.Keypad0 => KeyCode.Keypad0, - ModKeyCode.Keypad1 => KeyCode.Keypad1, - ModKeyCode.Keypad2 => KeyCode.Keypad2, - ModKeyCode.Keypad3 => KeyCode.Keypad3, - ModKeyCode.Keypad4 => KeyCode.Keypad4, - ModKeyCode.Keypad5 => KeyCode.Keypad5, - ModKeyCode.Keypad6 => KeyCode.Keypad6, - ModKeyCode.Keypad7 => KeyCode.Keypad7, - ModKeyCode.Keypad8 => KeyCode.Keypad8, - ModKeyCode.Keypad9 => KeyCode.Keypad9, - ModKeyCode.F1 => KeyCode.F1, - ModKeyCode.F2 => KeyCode.F2, - ModKeyCode.F3 => KeyCode.F3, - ModKeyCode.F4 => KeyCode.F4, - ModKeyCode.F5 => KeyCode.F5, - ModKeyCode.F6 => KeyCode.F6, - ModKeyCode.F7 => KeyCode.F7, - ModKeyCode.F8 => KeyCode.F8, - ModKeyCode.F9 => KeyCode.F9, - ModKeyCode.F10 => KeyCode.F10, - ModKeyCode.F11 => KeyCode.F11, - ModKeyCode.F12 => KeyCode.F12, - ModKeyCode.Insert => KeyCode.Insert, - ModKeyCode.Delete => KeyCode.Delete, - ModKeyCode.Home => KeyCode.Home, - ModKeyCode.End => KeyCode.End, - ModKeyCode.PageUp => KeyCode.PageUp, - ModKeyCode.PageDown => KeyCode.PageDown, - ModKeyCode.UpArrow => KeyCode.UpArrow, - ModKeyCode.DownArrow => KeyCode.DownArrow, - ModKeyCode.LeftArrow => KeyCode.LeftArrow, - ModKeyCode.RightArrow => KeyCode.RightArrow, - _ => throw new ArgumentOutOfRangeException(nameof(modKeyCode), modKeyCode, "游戏功能键需要单独处理") - }; -} diff --git a/AquaMai/ModKeyMap/TestProof.cs b/AquaMai/ModKeyMap/TestProof.cs deleted file mode 100644 index 3ba70177..00000000 --- a/AquaMai/ModKeyMap/TestProof.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Diagnostics; -using System.Linq; -using HarmonyLib; -using Manager; - -namespace AquaMai.ModKeyMap; - -public class TestProof -{ - [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")) - { - __result = ModKeyListener.GetKeyDownOrLongPress(AquaMai.AppConfig.ModKeyMap.TestMode, AquaMai.AppConfig.ModKeyMap.TestModeLongPress); - } - - return false; - } -} diff --git a/AquaMai/TimeSaving/Config.cs b/AquaMai/TimeSaving/Config.cs deleted file mode 100644 index 047600cb..00000000 --- a/AquaMai/TimeSaving/Config.cs +++ /dev/null @@ -1,49 +0,0 @@ -using AquaMai.Attributes; - -namespace AquaMai.TimeSaving; - -public class Config -{ - [ConfigComment( - en: "Skip the warning screen and logo shown after the POST sequence", - zh: "跳过日服启动时候的 WARNING 界面")] - public bool SkipWarningScreen { get; set; } - - [ConfigComment( - en: "Disable some useless delays to speed up the game boot process", - zh: """ - 在自检界面,每个屏幕结束的时候都会等两秒才进入下一个屏幕,很浪费时间 - 开了这个选项之后就不会等了 - """)] - public bool ImproveLoadSpeed { get; set; } - - [ConfigComment( - en: "Directly enter the song selection screen after login", - zh: "登录完成后直接进入选歌界面")] - public bool SkipToMusicSelection { get; set; } - - [ConfigComment( - en: "Skip possible prompts like \"New area discovered\", \"New songs added\", \"There are events\" during game login/registration", - zh: "跳过登录 / 注册游戏时候可能的 “发现了新的区域哟” “乐曲增加” “有活动哟” 之类的提示")] - public bool SkipEventInfo { get; set; } - - [ConfigComment( - en: "Skip the \"Do not tap or slide vigorously\" screen, immediately proceed to the next screen once data is loaded", - zh: "跳过“不要大力拍打或滑动哦”这个界面,数据一旦加载完就立马进入下一个界面")] - public bool IWontTapOrSlideVigorously { get; set; } - - [ConfigComment( - en: "Skip the \"Goodbye\" screen at the end of the game", - zh: "跳过游戏结束的“再见”界面")] - public bool SkipGameOverScreen { get; set; } - - [ConfigComment( - en: "Skip TrackStart screen", - zh: "跳过乐曲开始界面")] - public bool SkipTrackStart { get; set; } - - [ConfigComment( - en: "Show a \"skip\" button like AstroDX after the notes end", - zh: "音符结束之后显示像 AstroDX 一样的“跳过”按钮")] - public bool ShowQuickEndPlay { get; set; } -} diff --git a/AquaMai/TimeSaving/IWontTapOrSlideVigorously.cs b/AquaMai/TimeSaving/IWontTapOrSlideVigorously.cs deleted file mode 100644 index 3f6e9d32..00000000 --- a/AquaMai/TimeSaving/IWontTapOrSlideVigorously.cs +++ /dev/null @@ -1,15 +0,0 @@ -using HarmonyLib; -using Monitor; - -namespace AquaMai.TimeSaving; - -public class IWontTapOrSlideVigorously -{ - [HarmonyPrefix] - [HarmonyPatch(typeof(PlInformationMonitor), "IsPlayPlInfoEnd")] - public static bool Patch(ref bool __result) - { - __result = true; - return false; - } -} diff --git a/AquaMai/TimeSaving/ImproveLoadSpeed.cs b/AquaMai/TimeSaving/ImproveLoadSpeed.cs deleted file mode 100644 index 359cc6f3..00000000 --- a/AquaMai/TimeSaving/ImproveLoadSpeed.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.Diagnostics; -using HarmonyLib; -using Process; - -namespace AquaMai.TimeSaving -{ - public class ImproveLoadSpeed - { - [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); - } - } - } -} diff --git a/AquaMai/TimeSaving/SkipEventInfo.cs b/AquaMai/TimeSaving/SkipEventInfo.cs deleted file mode 100644 index 3dd7ef24..00000000 --- a/AquaMai/TimeSaving/SkipEventInfo.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Collections.Generic; -using HarmonyLib; -using Process; -using Process.Information; - -namespace AquaMai.TimeSaving -{ - 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[] ____discoverList) - { - ____discoverList = new Queue[] { new Queue(), new Queue() }; - } - } -} diff --git a/AquaMai/TimeSaving/SkipToMusicSelection.cs b/AquaMai/TimeSaving/SkipToMusicSelection.cs deleted file mode 100644 index fe8c435b..00000000 --- a/AquaMai/TimeSaving/SkipToMusicSelection.cs +++ /dev/null @@ -1,49 +0,0 @@ -using DB; -using HarmonyLib; -using MAI2.Util; -using Manager; -using Monitor; -using Process; -using Process.Information; - -namespace AquaMai.TimeSaving -{ - public class SkipToMusicSelection - { - /* - * 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.Instance.GetUserData(monIndex); - var index = userData.MapList.FindIndex((UserMapData m) => m.ID == userData.Detail.SelectMapID); - if (index >= 0) return; - userData.MapList.Clear(); - } - } -} diff --git a/AquaMai/TimeSaving/SkipWarningScreen.cs b/AquaMai/TimeSaving/SkipWarningScreen.cs deleted file mode 100644 index 3b0a6428..00000000 --- a/AquaMai/TimeSaving/SkipWarningScreen.cs +++ /dev/null @@ -1,28 +0,0 @@ -using HarmonyLib; -using Monitor; - -namespace AquaMai.TimeSaving -{ - public class SkipWarningScreen - { - /* - * 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; - } - } -} diff --git a/AquaMai/TouchSensitivity/Config.cs b/AquaMai/TouchSensitivity/Config.cs deleted file mode 100644 index 3afd7244..00000000 --- a/AquaMai/TouchSensitivity/Config.cs +++ /dev/null @@ -1,72 +0,0 @@ -using AquaMai.Attributes; - -namespace AquaMai.TouchSensitivity; - -public class Config -{ - [ConfigComment( - en: """ - Enable custom sensitivity - When enabled, the settings in Test mode will not take effect - When disabled, the settings in Test mode can still be used - """, - zh: """ - 是否启用自定义灵敏度 - 这里启用之后 Test 里的就不再起作用了 - 这里禁用之后就还是可以用 Test 里的调 - """)] - public bool Enable { get; set; } - - [ConfigComment( - en: """ - 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 模式下调整的灵敏度不是线性的 - 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 byte A1 { get; set; } = 40; - - public byte A2 { get; set; } = 40; - public byte A3 { get; set; } = 40; - public byte A4 { get; set; } = 40; - public byte A5 { get; set; } = 40; - public byte A6 { get; set; } = 40; - public byte A7 { get; set; } = 40; - public byte A8 { get; set; } = 40; - public byte B1 { get; set; } = 20; - public byte B2 { get; set; } = 20; - public byte B3 { get; set; } = 20; - public byte B4 { get; set; } = 20; - public byte B5 { get; set; } = 20; - public byte B6 { get; set; } = 20; - public byte B7 { get; set; } = 20; - public byte B8 { get; set; } = 20; - public byte C1 { get; set; } = 20; - public byte C2 { get; set; } = 20; - public byte D1 { get; set; } = 20; - public byte D2 { get; set; } = 20; - public byte D3 { get; set; } = 20; - public byte D4 { get; set; } = 20; - public byte D5 { get; set; } = 20; - public byte D6 { get; set; } = 20; - public byte D7 { get; set; } = 20; - public byte D8 { get; set; } = 20; - public byte E1 { get; set; } = 20; - public byte E2 { get; set; } = 20; - public byte E3 { get; set; } = 20; - public byte E4 { get; set; } = 20; - public byte E5 { get; set; } = 20; - public byte E6 { get; set; } = 20; - public byte E7 { get; set; } = 20; - public byte E8 { get; set; } = 20; -} diff --git a/AquaMai/TouchSensitivity/Enable.cs b/AquaMai/TouchSensitivity/Enable.cs deleted file mode 100644 index c7d4b1f6..00000000 --- a/AquaMai/TouchSensitivity/Enable.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Collections.Generic; -using HarmonyLib; -using IO; -using Manager; -using MelonLoader; - -namespace AquaMai.TouchSensitivity; - -public class Enable -{ - [HarmonyPrefix] - [HarmonyPatch(typeof(NewTouchPanel), "SetTouchPanelSensitivity")] - public static void SetTouchPanelSensitivityPrefix(List sensitivity) - { - var configType = AquaMai.AppConfig.TouchSensitivity.GetType(); - for (var i = 0; i < 34; i++) - { - var area = (InputManager.TouchPanelArea)i; - var field = configType.GetProperty(area.ToString()); - var value = (byte)field.GetValue(AquaMai.AppConfig.TouchSensitivity); - sensitivity[i] = value; - } - MelonLogger.Msg("[TouchSensitivity] Applied"); - } -} diff --git a/AquaMai/UX/CiBuildAlert.cs b/AquaMai/UX/CiBuildAlert.cs deleted file mode 100644 index b6c2e307..00000000 --- a/AquaMai/UX/CiBuildAlert.cs +++ /dev/null @@ -1,16 +0,0 @@ -using AquaMai.Helpers; -using AquaMai.Resources; -using HarmonyLib; -using Process; - -namespace AquaMai.UX; - -public class CiBuildAlert -{ - [HarmonyPatch(typeof(AdvertiseProcess), "OnStart")] - [HarmonyPostfix] - public static void OnStart(AdvertiseProcess __instance) - { - MessageHelper.ShowMessage(Locale.CiBuildAlertContent, title: Locale.CiBuildAlertTitle); - } -} diff --git a/AquaMai/UX/Config.cs b/AquaMai/UX/Config.cs deleted file mode 100644 index 96770a05..00000000 --- a/AquaMai/UX/Config.cs +++ /dev/null @@ -1,140 +0,0 @@ -using AquaMai.Attributes; - -namespace AquaMai.UX; - -public class Config -{ - [ConfigComment( - en: "Language for mod UI, supports en and zh", - zh: "Mod 界面的语言,支持 en 和 zh")] - public string Locale { get; set; } = ""; - - [ConfigComment( - en: "Single player: Show 1P only, at the center of the screen", - zh: "单人模式,不显示 2P")] - public bool SinglePlayer { get; set; } - - [ConfigComment( - en: "Remove the circle mask in the game", - zh: "移除遮罩")] - public bool HideMask { get; set; } - - [ConfigComment( - en: "Load Jacket image from folder \"LocalAssets\" and filename \"{MusicID}.png\" for self-made charts", - zh: "通过游戏目录下 `LocalAssets\\000000(歌曲 ID).png` 加载封面,自制谱用")] - public bool LoadAssetsPng { get; set; } - - [ConfigComment( - en: "Add \".ab\" image resources without the need of rebuilding a manifest", - zh: """ - 优化图片资源的加载,就算没有 AssetBundleImages.manifest 也可以正常加载 ab 格式的图片资源 - 导入了删除曲包之类的话,应该需要开启这个 - """)] - public bool LoadAssetBundleWithoutManifest { get; set; } - - [ConfigComment( - en: """ - Random BGM, put Mai2Cue.{acb,awb} of old version of the game in `LocalAssets\Mai2Cue` and rename them - Do not enable when SinglePlayer is off - """, - zh: """ - 在 `LocalAssets\Mai2Cue` 这个目录下放置了旧版游戏的 Mai2Cue.{acb,awb} 并重命名的话,可以在播放游戏 BGM 的时候随机播放这里面的旧版游戏 BGM - 和 2P 模式有冲突,如果你没有开启 'SinglePlayer' 的话,请关闭这个 - """)] - public bool RandomBgm { get; set; } - - [ConfigComment( - en: "Play \"Master\" difficulty on Demo screen", - zh: "在闲置时的演示画面上播放紫谱而不是绿谱")] - public bool DemoMaster { get; set; } - - [ConfigComment( - en: """ - Disable timers - Not recommand to enable when SinglePlayer is off - """, - zh: """ - 关掉那些游戏中的倒计时 - 如果你没有开启 'SinglePlayer' 的话,不建议开这个,不过要开的话也不是不可以 - """)] - public bool ExtendTimer { get; set; } - - [ConfigComment( - en: "Save immediate after playing a song", - zh: "打完一首歌的时候立即向服务器保存成绩")] - public bool ImmediateSave { get; set; } - - [ConfigComment( - en: """ - Use the png jacket above as BGA if BGA is not found for self-made charts - Use together with `LoadJacketPng` - """, - zh: """ - 如果没有 dat 格式的 BGA 的话,就用歌曲的封面做背景,而不是显示迪拉熊的笑脸 - 请和 `LoadJacketPng` 一起用 - """)] - public bool LoadLocalBga { get; set; } - - [ConfigComment( - en: """ - Place font.ttf in the LocalAssets directory to replace the game's global font - Cannot be used together with FontFix - """, - zh: """ - 在 LocalAssets 目录下放置 font.ttf 可以替换游戏的全局字体 - 不可以和 FontFix 一起使用 - """)] - public bool CustomFont { get; set; } - - [ConfigComment( - en: "Map touch actions to buttons", - zh: "映射触摸操作至实体按键")] - public bool TouchToButtonInput { get; set; } - - [ConfigComment( - en: "Cannot be used together with HanabiFix", - zh: """ - 完全隐藏烟花 - 不能和 HanabiFix 一起使用 - """)] - public bool HideHanabi { get; set; } - - [ConfigComment( - zh: "让星星在启动拍等待期间从 50% 透明度渐入为 100%,取代原本在击打星星头时就完成渐入", - en: "Slides will fade in instead of instantly appearing")] - public bool SlideFadeInTweak { get; set; } - - [ConfigComment( - zh: "在游戏总结的计分板中显示击打误差的详细信息(以帧为单位)", - en: "Show detailed accuracy info in the score board")] - public bool JudgeAccuracyInfo { get; set; } - - [ConfigComment( - en: "Set the version string displayed at the top-right corner of the screen", - zh: "把右上角的版本更改为自定义文本")] - public string CustomVersionString { get; set; } = ""; - - [ConfigComment( - en: """ - Custom shop name in photo - Also enable shop name display in SDGA - """, - zh: """ - 自定义拍照的店铺名称 - 同时在 SDGA 中会启用店铺名称的显示(但是不会在游戏里有设置) - """)] - public string CustomPlaceName { get; set; } = ""; - - [ConfigComment( - en: "Execute some command on game idle", - zh: """ - 在游戏闲置的时候执行指定的命令脚本 - 比如说可以在游戏闲置是降低显示器的亮度 - """)] - public string ExecOnIdle { get; set; } = ""; - - [ConfigComment( - en: "Execute some command on game start", - zh: "在玩家登录的时候执行指定的命令脚本")] - public string ExecOnEntry { get; set; } = ""; -} \ No newline at end of file diff --git a/AquaMai/UX/CustomFont.cs b/AquaMai/UX/CustomFont.cs deleted file mode 100644 index 94661311..00000000 --- a/AquaMai/UX/CustomFont.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using HarmonyLib; -using MelonLoader; -using TMPro; -using UnityEngine; -using UnityEngine.TextCore.LowLevel; - -namespace AquaMai.UX; - -public class CustomFont -{ - private static Font font; - private static TMP_FontAsset fontAsset; - - public static void DoCustomPatch(HarmonyLib.Harmony h) - { - var fontPath = Path.Combine(Environment.CurrentDirectory, "LocalAssets", "font.ttf"); - if (!File.Exists(fontPath)) return; - - font = new Font(fontPath); - fontAsset = TMP_FontAsset.CreateFontAsset(font, 90, 9, GlyphRenderMode.SDFAA, 8192, 8192); - } - - [HarmonyPatch(typeof(TextMeshProUGUI), "Awake")] - [HarmonyPostfix] - public static void PostFix(TextMeshProUGUI __instance) - { - if (font is null) return; -# if DEBUG - MelonLogger.Msg($"{__instance.font.name} {__instance.text}"); -# endif - - var materialOrigin = __instance.fontMaterial; - var materialSharedOrigin = __instance.fontSharedMaterial; - __instance.font = fontAsset; -# 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)); - // } - - } -} diff --git a/AquaMai/UX/CustomPlaceName.cs b/AquaMai/UX/CustomPlaceName.cs deleted file mode 100644 index 3fc5e8db..00000000 --- a/AquaMai/UX/CustomPlaceName.cs +++ /dev/null @@ -1,32 +0,0 @@ -using HarmonyLib; -using Manager; - -namespace AquaMai.UX; - -public class CustomPlaceName -{ - [HarmonyPostfix] - [HarmonyPatch(typeof(OperationManager), "CheckAuth_Proc")] - public static void CheckAuth_Proc(OperationManager __instance) - { - if (string.IsNullOrEmpty(AquaMai.AppConfig.UX.CustomPlaceName)) - { - return; - } - - __instance.ShopData.ShopName = AquaMai.AppConfig.UX.CustomPlaceName; - __instance.ShopData.ShopNickName = AquaMai.AppConfig.UX.CustomPlaceName; - } - - [HarmonyPostfix] - [HarmonyPatch(typeof(ResultCardBaseController), "Initialize")] - public static void Initialize(ResultCardBaseController __instance) - { - if (string.IsNullOrEmpty(AquaMai.AppConfig.UX.CustomPlaceName)) - { - return; - } - - __instance.SetVisibleStoreName(true); - } -} diff --git a/AquaMai/UX/CustomVersionString.cs b/AquaMai/UX/CustomVersionString.cs deleted file mode 100644 index 78282ea9..00000000 --- a/AquaMai/UX/CustomVersionString.cs +++ /dev/null @@ -1,24 +0,0 @@ -using HarmonyLib; - -namespace AquaMai.UX -{ - public class CustomVersionString - { - /* - * Patch displayVersionString Property Getter - */ - [HarmonyPrefix] - [HarmonyPatch(typeof(MAI2System.Config), "displayVersionString", MethodType.Getter)] - public static bool GetDisplayVersionString(ref string __result) - { - if (string.IsNullOrEmpty(AquaMai.AppConfig.UX.CustomVersionString)) - { - return true; - } - - __result = AquaMai.AppConfig.UX.CustomVersionString; - // Return false to block the original method - return false; - } - } -} \ No newline at end of file diff --git a/AquaMai/UX/DemoMaster.cs b/AquaMai/UX/DemoMaster.cs deleted file mode 100644 index e3dd5090..00000000 --- a/AquaMai/UX/DemoMaster.cs +++ /dev/null @@ -1,31 +0,0 @@ -using DB; -using HarmonyLib; -using MAI2.Util; -using Manager; -using Process; - -namespace AquaMai.UX -{ - public class DemoMaster - { - [HarmonyPostfix] - [HarmonyPatch(typeof(AdvDemoProcess), "OnStart")] - public static void AdvDemoProcessPostStart() - { - for (int i = 0; i < 2; i++) - { - var userOption = Singleton.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; - } - } -} diff --git a/AquaMai/UX/ExtendTimer.cs b/AquaMai/UX/ExtendTimer.cs deleted file mode 100644 index e91238a2..00000000 --- a/AquaMai/UX/ExtendTimer.cs +++ /dev/null @@ -1,56 +0,0 @@ -using HarmonyLib; -using Manager; -using Monitor; -using Process; -using Process.Entry.State; -using Process.ModeSelect; - -namespace AquaMai.UX -{ - public class ExtendTimer - { - [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(); - } - } -} diff --git a/AquaMai/UX/LoadAssetBundleWithoutManifest.cs b/AquaMai/UX/LoadAssetBundleWithoutManifest.cs deleted file mode 100644 index 90dcb2b4..00000000 --- a/AquaMai/UX/LoadAssetBundleWithoutManifest.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using HarmonyLib; -using UnityEngine; -using Manager; -using Util; - -namespace AquaMai.UX -{ - public class LoadAssetBundleWithoutManifest - { - private static HashSet abFiles = new HashSet(); - - [HarmonyPostfix] - [HarmonyPatch(typeof(OptionDataManager), "CheckAssetBundle")] - public static void PostCheckAssetBundle(ref Safe.ReadonlySortedDictionary 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; - } - } -} \ No newline at end of file diff --git a/AquaMai/UX/RandomBgm.cs b/AquaMai/UX/RandomBgm.cs deleted file mode 100644 index 1b5322cc..00000000 --- a/AquaMai/UX/RandomBgm.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using HarmonyLib; -using Mai2.Mai2Cue; -using MAI2.Util; -using Manager; -using MelonLoader; - -namespace AquaMai.UX -{ - public class RandomBgm - { - private static List _acbs = new List(); - - [HarmonyPostfix] - [HarmonyPatch(typeof(SoundManager), "Initialize")] - public static void Init() - { - if (!Directory.Exists(Path.Combine(Environment.CurrentDirectory, "LocalAssets", "Mai2Cue"))) return; - var files = Directory.EnumerateFiles(Path.Combine(Environment.CurrentDirectory, "LocalAssets", "Mai2Cue")); - 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.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; - } - } - } -} diff --git a/AquaMai/UX/RunCommandOnEvents.cs b/AquaMai/UX/RunCommandOnEvents.cs deleted file mode 100644 index b6a9a791..00000000 --- a/AquaMai/UX/RunCommandOnEvents.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using System.Diagnostics; -using HarmonyLib; -using MelonLoader; -using Process; - -namespace AquaMai.UX -{ - public class RunCommandOnEvents - { - [HarmonyPrefix] - [HarmonyPatch(typeof(AdvertiseProcess), "OnStart")] - public static void AdvertiseProcessPreStart() - { - if (!string.IsNullOrWhiteSpace(AquaMai.AppConfig.UX.ExecOnIdle)) - { - Exec(AquaMai.AppConfig.UX.ExecOnIdle); - } - } - - [HarmonyPrefix] - [HarmonyPatch(typeof(EntryProcess), "OnStart")] - public static void EntryProcessPreStart() - { - if (!string.IsNullOrWhiteSpace(AquaMai.AppConfig.UX.ExecOnEntry)) - { - Exec(AquaMai.AppConfig.UX.ExecOnEntry); - } - } - - [HarmonyPrefix] - [HarmonyPatch(typeof(MusicSelectProcess), "OnStart")] - public static void MusicSelectProcessPreStart() - { - if (!string.IsNullOrWhiteSpace(AquaMai.AppConfig.UX.ExecOnEntry)) - { - Exec(AquaMai.AppConfig.UX.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(); - } - } -} \ No newline at end of file diff --git a/AquaMai/UX/SinglePlayer.cs b/AquaMai/UX/SinglePlayer.cs deleted file mode 100644 index bdc2b3b4..00000000 --- a/AquaMai/UX/SinglePlayer.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System.Collections.Generic; -using System.Reflection; -using AquaMai.Attributes; -using HarmonyLib; -using MAI2.Util; -using Manager; -using MelonLoader; -using Monitor.Common; -using Monitor.Entry; -using Monitor.Entry.Parts.Screens; -using UnityEngine; -using Type = System.Type; - -namespace AquaMai.UX -{ - // 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. - public class SinglePlayer - { - [HarmonyPatch] - public class WhateverInitialize - { - public static IEnumerable 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(), point, Camera.main); - return false; - } - - [GameVersion(21500)] - public class SkipTimer - { - [HarmonyPostfix] - [HarmonyPatch(typeof(EntryMonitor), "DecideEntry")] - public static void PostDecideEntry(EntryMonitor __instance) - { - MelonLogger.Msg("Confirm Entry"); - TimeManager.MarkGameStartTime(); - Singleton.Instance.UpdateEvent(); - Singleton.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; - } - } - } -} diff --git a/AquaMai/Utils/Config.cs b/AquaMai/Utils/Config.cs deleted file mode 100644 index 23f1b9c6..00000000 --- a/AquaMai/Utils/Config.cs +++ /dev/null @@ -1,59 +0,0 @@ -using AquaMai.Attributes; - -namespace AquaMai.Utils; - -public class Config -{ - [ConfigComment( - en: "Log user ID on login", - zh: "登录时将 UserID 输出到日志")] - public bool LogUserId { get; set; } - - [ConfigComment( - en: "Globally increase A judgment, unit is the same as in the game", - zh: "全局增加 A 判,单位和游戏里一样")] - public double JudgeAdjustA { get; set; } - - [ConfigComment( - en: "Globally increase B judgment, unit is the same as in the game", - zh: "全局增加 B 判,单位和游戏里一样")] - public double JudgeAdjustB { get; set; } - - [ConfigComment( - en: "Touch screen delay, unit is milliseconds, one second = 1000 milliseconds. Must be an integer", - zh: "触摸屏延迟,单位为毫秒,一秒 = 1000 毫秒。必须是整数")] - public int TouchDelay { get; set; } - - [ConfigComment( - en: "Show detail of selected song in music selection screen", - zh: "选歌界面显示选择的歌曲的详情")] - public bool SelectionDetail { get; set; } - - [ConfigComment( - en: "Show Network error detail in the game", - zh: "出现灰网时显示原因")] - public bool ShowNetErrorDetail { get; set; } - - [ConfigComment( - en: "Show error log in the game", - zh: "在游戏中显示错误日志窗口而不是关闭游戏进程")] - public bool ShowErrorLog { get; set; } - - [ConfigComment( - en: "Display framerate", - zh: "显示帧率")] - public bool FrameRateDisplay { get; set; } - - [ConfigComment( - en: """ - Adjust the baud rate of the touch screen serial port, default value is 9600 - Requires hardware support. If you are unsure whether you can use it, you cannot use it - Set to 0 to disable - """, - zh: """ - 调整触摸屏串口波特率,默认值 9600 - 需要硬件配合。如果你不清楚你是否可以使用,那你不能使用 - 改为 0 禁用 - """)] - public int TouchPanelBaudRate { get; set; } -} diff --git a/AquaMai/Utils/JudgeAdjust.cs b/AquaMai/Utils/JudgeAdjust.cs deleted file mode 100644 index 071c4127..00000000 --- a/AquaMai/Utils/JudgeAdjust.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Threading; -using HarmonyLib; -using IO; -using Manager.UserDatas; - -namespace AquaMai.Utils; - -public class JudgeAdjust -{ - [HarmonyPostfix] - [HarmonyPatch(typeof(UserOption), "GetAdjustMSec")] - public static void GetAdjustMSec(ref float __result) - { - __result += (float)AquaMai.AppConfig.Utils.JudgeAdjustA * 16.666666f; - } - - [HarmonyPostfix] - [HarmonyPatch(typeof(UserOption), "GetJudgeTimingFrame")] - public static void GetJudgeTimingFrame(ref float __result) - { - __result += (float)AquaMai.AppConfig.Utils.JudgeAdjustB; - } - - [HarmonyPrefix] - [HarmonyPatch(typeof(NewTouchPanel), "Recv")] - public static void NewTouchPanelRecv() - { - if (AquaMai.AppConfig.Utils.TouchDelay <= 0) return; - Thread.Sleep(AquaMai.AppConfig.Utils.TouchDelay); - } -} diff --git a/AquaMai/Utils/TouchPanelBaudRate.cs b/AquaMai/Utils/TouchPanelBaudRate.cs deleted file mode 100644 index 57cdb091..00000000 --- a/AquaMai/Utils/TouchPanelBaudRate.cs +++ /dev/null @@ -1,15 +0,0 @@ -using HarmonyLib; -using IO; - -namespace AquaMai.Utils; - -public class TouchPanelBaudRate -{ - [HarmonyPatch(typeof(NewTouchPanel), "Open")] - [HarmonyPrefix] - private static void OpenPrefix(ref int ___BaudRate) - { - if (AquaMai.AppConfig.Utils.TouchPanelBaudRate <= 0) return; - ___BaudRate = AquaMai.AppConfig.Utils.TouchPanelBaudRate; - } -} diff --git a/AquaMai/Visual/Config.cs b/AquaMai/Visual/Config.cs deleted file mode 100644 index 173419c9..00000000 --- a/AquaMai/Visual/Config.cs +++ /dev/null @@ -1,107 +0,0 @@ -using AquaMai.Attributes; - -namespace AquaMai.Visual; - -public class Config -{ - [ConfigComment( - en: """ - Provide the ability to use custom skins (advanced feature) - Load skin textures from LocalAssets\Skins - """, - zh: """ - 提供自定义皮肤的能力(高级功能) - 从 LocalAssets\Skins 中加载皮肤贴图 - """)] - public bool CustomSkins { get; set; } - - [ConfigComment( - 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 bool JudgeDisplay4B { get; set; } - - [ConfigComment( - 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 bool CustomTrackStartDiff { get; set; } - - [ConfigComment( - en: """ - Delayed the animation of the song start screen - For recording chart confirmation - """, - zh: """ - 推迟了歌曲开始界面的动画 - 录制谱面确认用 - """)] - public bool TrackStartProcessTweak { get; set; } - - [ConfigComment( - 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 bool DisableTrackStartTabs { get; set; } - - [ConfigComment( - 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 bool FanJudgeFlip { get; set; } - - [ConfigComment( - 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 bool BreakSlideJudgeBlink { get; set; } - - [ConfigComment( - en: """ - Make the Slide Track disappear with an inward-shrinking animation, similar to AstroDX - """, - zh: """ - 使 Slide Track 消失时有类似 AstroDX 一样的向内缩入的动画 - """)] - public bool SlideArrowAnimation { get; set; } - - [ConfigComment( - 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 bool SlideLayerReverse { get; set; } -} diff --git a/AquaMai/Visual/FanJudgeFlip.cs b/AquaMai/Visual/FanJudgeFlip.cs deleted file mode 100644 index 6defc766..00000000 --- a/AquaMai/Visual/FanJudgeFlip.cs +++ /dev/null @@ -1,32 +0,0 @@ -using HarmonyLib; -using Monitor; - -namespace AquaMai.Visual; - -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); - } - } - } -} diff --git a/AquaMai/WindowState/Config.cs b/AquaMai/WindowState/Config.cs deleted file mode 100644 index 65fbf8ed..00000000 --- a/AquaMai/WindowState/Config.cs +++ /dev/null @@ -1,31 +0,0 @@ -using AquaMai.Attributes; - -namespace AquaMai.WindowState; - -public class Config -{ - [ConfigComment( - en: "If not enabled, no operations will be performed on the game window", - zh: "不启用的话,不会对游戏窗口做任何操作")] - public bool Enable { get; set; } - - [ConfigComment( - en: "Window the game", - zh: "窗口化游戏")] - public bool Windowed { get; set; } - - [ConfigComment( - en: """ - 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,窗口化将记住用户设定的大小,全屏时将使用当前显示器分辨率 - """)] - public int Width { get; set; } - - [ConfigComment( - zh: "高度")] - public int Height { get; set; } -} diff --git a/AquaMai/genConfig.csx b/AquaMai/genConfig.csx deleted file mode 100644 index 2c53986b..00000000 --- a/AquaMai/genConfig.csx +++ /dev/null @@ -1,3 +0,0 @@ -#r "AquaMai.dll" - -AquaMai.ConfigGenerator.GenerateConfig(); diff --git a/config/application.properties b/config/application.properties index 813c7eb0..94da66f8 100644 --- a/config/application.properties +++ b/config/application.properties @@ -29,7 +29,7 @@ allnet.server.redirect=https://aquadx.net ## Http Server Port ## Only change this if you have a reverse proxy running. ## The game rely on 80 port for boot up command -server.port=80 +server.port=8080 ## Static file server ## This is used to server static files in /web/ directory, which is Aquaviewer @@ -87,15 +87,15 @@ spring.servlet.multipart.max-request-size=20MB ## Database Setting ########## For Sqlite ########## -spring.datasource.driver-class-name=org.sqlite.JDBC -spring.datasource.url=jdbc:sqlite:data/db.sqlite +#spring.datasource.driver-class-name=org.sqlite.JDBC +#spring.datasource.url=jdbc:sqlite:data/db.sqlite ########## For MariaDB ########## -#spring.datasource.driver-class-name=org.mariadb.jdbc.Driver -#spring.datasource.username= -#spring.datasource.password= -#spring.datasource.url=jdbc:mariadb://localhost:3306/insert_db_name_here?allowPublicKeyRetrieval=true&useSSL=false -#spring.datasource.hikari.maximum-pool-size=10 +spring.datasource.driver-class-name=org.mariadb.jdbc.Driver +spring.datasource.username=aqua +spring.datasource.password=aqua +spring.datasource.url=jdbc:mariadb://localhost:3306/aqua?allowPublicKeyRetrieval=true&useSSL=false +spring.datasource.hikari.maximum-pool-size=10 ################################ ## AquaNet Settings