├── .gitignore ├── MuvluvMod.sln ├── MuvluvMod ├── Behaviour.cs ├── Config.cs ├── MuvluvMod.csproj ├── Patch.cs ├── Plugin.cs └── Translation.cs ├── README.md └── README_EN.md /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | *.env 13 | 14 | # User-specific files (MonoDevelop/Xamarin Studio) 15 | *.userprefs 16 | 17 | # Mono auto generated files 18 | mono_crash.* 19 | 20 | # Build results 21 | [Dd]ebug/ 22 | [Dd]ebugPublic/ 23 | [Rr]elease/ 24 | [Rr]eleases/ 25 | 26 | [Dd]ebug/x64/ 27 | [Dd]ebugPublic/x64/ 28 | [Rr]elease/x64/ 29 | [Rr]eleases/x64/ 30 | bin/x64/ 31 | obj/x64/ 32 | 33 | [Dd]ebug/x86/ 34 | [Dd]ebugPublic/x86/ 35 | [Rr]elease/x86/ 36 | [Rr]eleases/x86/ 37 | bin/x86/ 38 | obj/x86/ 39 | 40 | [Ww][Ii][Nn]32/ 41 | [Aa][Rr][Mm]/ 42 | [Aa][Rr][Mm]64/ 43 | [Aa][Rr][Mm]64[Ee][Cc]/ 44 | bld/ 45 | [Oo]bj/ 46 | [Oo]ut/ 47 | [Ll]og/ 48 | [Ll]ogs/ 49 | 50 | # Build results on 'Bin' directories 51 | **/[Bb]in/* 52 | # Uncomment if you have tasks that rely on *.refresh files to move binaries 53 | # (https://github.com/github/gitignore/pull/3736) 54 | #!**/[Bb]in/*.refresh 55 | 56 | # Visual Studio 2015/2017 cache/options directory 57 | .vs/ 58 | # Uncomment if you have tasks that create the project's static files in wwwroot 59 | #wwwroot/ 60 | 61 | # Visual Studio 2017 auto generated files 62 | Generated\ Files/ 63 | 64 | # MSTest test Results 65 | [Tt]est[Rr]esult*/ 66 | [Bb]uild[Ll]og.* 67 | *.trx 68 | 69 | # NUnit 70 | *.VisualState.xml 71 | TestResult.xml 72 | nunit-*.xml 73 | 74 | # Approval Tests result files 75 | *.received.* 76 | 77 | # Build Results of an ATL Project 78 | [Dd]ebugPS/ 79 | [Rr]eleasePS/ 80 | dlldata.c 81 | 82 | # Benchmark Results 83 | BenchmarkDotNet.Artifacts/ 84 | 85 | # .NET Core 86 | project.lock.json 87 | project.fragment.lock.json 88 | artifacts/ 89 | 90 | # ASP.NET Scaffolding 91 | ScaffoldingReadMe.txt 92 | 93 | # StyleCop 94 | StyleCopReport.xml 95 | 96 | # Files built by Visual Studio 97 | *_i.c 98 | *_p.c 99 | *_h.h 100 | *.ilk 101 | *.meta 102 | *.obj 103 | *.idb 104 | *.iobj 105 | *.pch 106 | *.pdb 107 | *.ipdb 108 | *.pgc 109 | *.pgd 110 | *.rsp 111 | # but not Directory.Build.rsp, as it configures directory-level build defaults 112 | !Directory.Build.rsp 113 | *.sbr 114 | *.tlb 115 | *.tli 116 | *.tlh 117 | *.tmp 118 | *.tmp_proj 119 | *_wpftmp.csproj 120 | *.log 121 | *.tlog 122 | *.vspscc 123 | *.vssscc 124 | .builds 125 | *.pidb 126 | *.svclog 127 | *.scc 128 | 129 | # Chutzpah Test files 130 | _Chutzpah* 131 | 132 | # Visual C++ cache files 133 | ipch/ 134 | *.aps 135 | *.ncb 136 | *.opendb 137 | *.opensdf 138 | *.sdf 139 | *.cachefile 140 | *.VC.db 141 | *.VC.VC.opendb 142 | 143 | # Visual Studio profiler 144 | *.psess 145 | *.vsp 146 | *.vspx 147 | *.sap 148 | 149 | # Visual Studio Trace Files 150 | *.e2e 151 | 152 | # TFS 2012 Local Workspace 153 | $tf/ 154 | 155 | # Guidance Automation Toolkit 156 | *.gpState 157 | 158 | # ReSharper is a .NET coding add-in 159 | _ReSharper*/ 160 | *.[Rr]e[Ss]harper 161 | *.DotSettings.user 162 | 163 | # TeamCity is a build add-in 164 | _TeamCity* 165 | 166 | # DotCover is a Code Coverage Tool 167 | *.dotCover 168 | 169 | # AxoCover is a Code Coverage Tool 170 | .axoCover/* 171 | !.axoCover/settings.json 172 | 173 | # Coverlet is a free, cross platform Code Coverage Tool 174 | coverage*.json 175 | coverage*.xml 176 | coverage*.info 177 | 178 | # Visual Studio code coverage results 179 | *.coverage 180 | *.coveragexml 181 | 182 | # NCrunch 183 | _NCrunch_* 184 | .NCrunch_* 185 | .*crunch*.local.xml 186 | nCrunchTemp_* 187 | 188 | # MightyMoose 189 | *.mm.* 190 | AutoTest.Net/ 191 | 192 | # Web workbench (sass) 193 | .sass-cache/ 194 | 195 | # Installshield output folder 196 | [Ee]xpress/ 197 | 198 | # DocProject is a documentation generator add-in 199 | DocProject/buildhelp/ 200 | DocProject/Help/*.HxT 201 | DocProject/Help/*.HxC 202 | DocProject/Help/*.hhc 203 | DocProject/Help/*.hhk 204 | DocProject/Help/*.hhp 205 | DocProject/Help/Html2 206 | DocProject/Help/html 207 | 208 | # Click-Once directory 209 | publish/ 210 | 211 | # Publish Web Output 212 | *.[Pp]ublish.xml 213 | *.azurePubxml 214 | # Note: Comment the next line if you want to checkin your web deploy settings, 215 | # but database connection strings (with potential passwords) will be unencrypted 216 | *.pubxml 217 | *.publishproj 218 | 219 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 220 | # checkin your Azure Web App publish settings, but sensitive information contained 221 | # in these scripts will be unencrypted 222 | PublishScripts/ 223 | 224 | # NuGet Packages 225 | *.nupkg 226 | # NuGet Symbol Packages 227 | *.snupkg 228 | # The packages folder can be ignored because of Package Restore 229 | **/[Pp]ackages/* 230 | # except build/, which is used as an MSBuild target. 231 | !**/[Pp]ackages/build/ 232 | # Uncomment if necessary however generally it will be regenerated when needed 233 | #!**/[Pp]ackages/repositories.config 234 | # NuGet v3's project.json files produces more ignorable files 235 | *.nuget.props 236 | *.nuget.targets 237 | 238 | # Microsoft Azure Build Output 239 | csx/ 240 | *.build.csdef 241 | 242 | # Microsoft Azure Emulator 243 | ecf/ 244 | rcf/ 245 | 246 | # Windows Store app package directories and files 247 | AppPackages/ 248 | BundleArtifacts/ 249 | Package.StoreAssociation.xml 250 | _pkginfo.txt 251 | *.appx 252 | *.appxbundle 253 | *.appxupload 254 | 255 | # Visual Studio cache files 256 | # files ending in .cache can be ignored 257 | *.[Cc]ache 258 | # but keep track of directories ending in .cache 259 | !?*.[Cc]ache/ 260 | 261 | # Others 262 | ClientBin/ 263 | ~$* 264 | *~ 265 | *.dbmdl 266 | *.dbproj.schemaview 267 | *.jfm 268 | *.pfx 269 | *.publishsettings 270 | orleans.codegen.cs 271 | 272 | # Including strong name files can present a security risk 273 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 274 | #*.snk 275 | 276 | # Since there are multiple workflows, uncomment next line to ignore bower_components 277 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 278 | #bower_components/ 279 | 280 | # RIA/Silverlight projects 281 | Generated_Code/ 282 | 283 | # Backup & report files from converting an old project file 284 | # to a newer Visual Studio version. Backup files are not needed, 285 | # because we have git ;-) 286 | _UpgradeReport_Files/ 287 | Backup*/ 288 | UpgradeLog*.XML 289 | UpgradeLog*.htm 290 | ServiceFabricBackup/ 291 | *.rptproj.bak 292 | 293 | # SQL Server files 294 | *.mdf 295 | *.ldf 296 | *.ndf 297 | 298 | # Business Intelligence projects 299 | *.rdl.data 300 | *.bim.layout 301 | *.bim_*.settings 302 | *.rptproj.rsuser 303 | *- [Bb]ackup.rdl 304 | *- [Bb]ackup ([0-9]).rdl 305 | *- [Bb]ackup ([0-9][0-9]).rdl 306 | 307 | # Microsoft Fakes 308 | FakesAssemblies/ 309 | 310 | # GhostDoc plugin setting file 311 | *.GhostDoc.xml 312 | 313 | # Node.js Tools for Visual Studio 314 | .ntvs_analysis.dat 315 | node_modules/ 316 | 317 | # Visual Studio 6 build log 318 | *.plg 319 | 320 | # Visual Studio 6 workspace options file 321 | *.opt 322 | 323 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 324 | *.vbw 325 | 326 | # Visual Studio 6 workspace and project file (working project files containing files to include in project) 327 | *.dsw 328 | *.dsp 329 | 330 | # Visual Studio 6 technical files 331 | *.ncb 332 | *.aps 333 | 334 | # Visual Studio LightSwitch build output 335 | **/*.HTMLClient/GeneratedArtifacts 336 | **/*.DesktopClient/GeneratedArtifacts 337 | **/*.DesktopClient/ModelManifest.xml 338 | **/*.Server/GeneratedArtifacts 339 | **/*.Server/ModelManifest.xml 340 | _Pvt_Extensions 341 | 342 | # Paket dependency manager 343 | **/.paket/paket.exe 344 | paket-files/ 345 | 346 | # FAKE - F# Make 347 | **/.fake/ 348 | 349 | # CodeRush personal settings 350 | **/.cr/personal 351 | 352 | # Python Tools for Visual Studio (PTVS) 353 | **/__pycache__/ 354 | *.pyc 355 | 356 | # Cake - Uncomment if you are using it 357 | #tools/** 358 | #!tools/packages.config 359 | 360 | # Tabs Studio 361 | *.tss 362 | 363 | # Telerik's JustMock configuration file 364 | *.jmconfig 365 | 366 | # BizTalk build output 367 | *.btp.cs 368 | *.btm.cs 369 | *.odx.cs 370 | *.xsd.cs 371 | 372 | # OpenCover UI analysis results 373 | OpenCover/ 374 | 375 | # Azure Stream Analytics local run output 376 | ASALocalRun/ 377 | 378 | # MSBuild Binary and Structured Log 379 | *.binlog 380 | MSBuild_Logs/ 381 | 382 | # AWS SAM Build and Temporary Artifacts folder 383 | .aws-sam 384 | 385 | # NVidia Nsight GPU debugger configuration file 386 | *.nvuser 387 | 388 | # MFractors (Xamarin productivity tool) working folder 389 | **/.mfractor/ 390 | 391 | # Local History for Visual Studio 392 | **/.localhistory/ 393 | 394 | # Visual Studio History (VSHistory) files 395 | .vshistory/ 396 | 397 | # BeatPulse healthcheck temp database 398 | healthchecksdb 399 | 400 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 401 | MigrationBackup/ 402 | 403 | # Ionide (cross platform F# VS Code tools) working folder 404 | **/.ionide/ 405 | 406 | # Fody - auto-generated XML schema 407 | FodyWeavers.xsd 408 | 409 | # VS Code files for those working on multiple tools 410 | .vscode/* 411 | !.vscode/settings.json 412 | !.vscode/tasks.json 413 | !.vscode/launch.json 414 | !.vscode/extensions.json 415 | !.vscode/*.code-snippets 416 | 417 | # Local History for Visual Studio Code 418 | .history/ 419 | 420 | # Built Visual Studio Code Extensions 421 | *.vsix 422 | 423 | # Windows Installer files from build outputs 424 | *.cab 425 | *.msi 426 | *.msix 427 | *.msm 428 | *.msp -------------------------------------------------------------------------------- /MuvluvMod.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.10.34928.147 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MuvluvMod", "MuvluvMod\MuvluvMod.csproj", "{A5007B97-4846-411D-9DFF-51916085B5CD}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {A5007B97-4846-411D-9DFF-51916085B5CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {A5007B97-4846-411D-9DFF-51916085B5CD}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {A5007B97-4846-411D-9DFF-51916085B5CD}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {A5007B97-4846-411D-9DFF-51916085B5CD}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {9FED9D3A-073D-4F26-9D6C-C6E9CE95672B} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /MuvluvMod/Behaviour.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UnityEngine; 3 | using UnityEngine.InputSystem; 4 | 5 | namespace MuvluvMod 6 | { 7 | public class PluginBehaviour : MonoBehaviour 8 | { 9 | void Update() 10 | { 11 | if (Keyboard.current.f2Key.wasPressedThisFrame) 12 | { 13 | Config.Translation.Value = !Config.Translation.Value; 14 | } 15 | if (Keyboard.current.f3Key.wasPressedThisFrame) 16 | { 17 | Config.EnableSkipButton.Value = !Config.EnableSkipButton.Value; 18 | } 19 | if (Keyboard.current.f4Key.wasPressedThisFrame) 20 | { 21 | Config.VoiceInterruption.Value = !Config.VoiceInterruption.Value; 22 | } 23 | if (Keyboard.current.f5Key.wasPressedThisFrame) 24 | { 25 | Config.AutoSkipBattle.Value = !Config.AutoSkipBattle.Value; 26 | } 27 | } 28 | 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /MuvluvMod/Config.cs: -------------------------------------------------------------------------------- 1 | using BepInEx.Configuration; 2 | 3 | namespace MuvluvMod 4 | { 5 | public static class Config 6 | { 7 | public static ConfigEntry DynamicMosaic; 8 | public static ConfigEntry EnableSkipButton; 9 | public static ConfigEntry VoiceInterruption; 10 | public static ConfigEntry AutoSkipBattle; 11 | 12 | public static ConfigEntry Translation; 13 | public static ConfigEntry TranslationCDN; 14 | public static ConfigEntry FontBundlePath; 15 | public static ConfigEntry FontAssetName; 16 | 17 | public static void Initialize() 18 | { 19 | DynamicMosaic = Plugin.Config.Bind("Gerneral", "DynamicMosaic", false, "是否开启游戏内动态马赛克"); 20 | EnableSkipButton = Plugin.Config.Bind("Gerneral", "EnableSkipButton", false, "是否总是开启跳过按钮"); 21 | VoiceInterruption = Plugin.Config.Bind("Gerneral", "VoiceInterruption", true, "剧情中播放下一句话时是否中断当前语音"); 22 | AutoSkipBattle = Plugin.Config.Bind("Gerneral", "AutoSkipBattle", false, "自动跳过战斗(自动按跳过键,不受跳过键开关影响)"); 23 | 24 | Translation = Plugin.Config.Bind("Translation", "Enable", true, "是否开启汉化"); 25 | TranslationCDN = Plugin.Config.Bind("Translation", "CdnURL", "https://raw.githubusercontent.com/anosu/muvluvgg-translation/refs/heads/main", "翻译加载的CDN"); 26 | FontBundlePath = Plugin.Config.Bind("Translation", "FontBundlePath", "font/sarasagothicsc-bold", "TMP字体AssetBundle的路径"); 27 | FontAssetName = Plugin.Config.Bind("Translation", "FontAssetName", "SarasaGothicSC-Bold SDF", "AssetBundle中TMP_FontAsset的名称"); 28 | 29 | Plugin.Log.LogInfo("Translation: " + (Translation.Value ? "Enabled" : "Disabled")); 30 | Plugin.Log.LogInfo("Translation CDN: " + TranslationCDN.Value); 31 | Plugin.Log.LogInfo("Font Bundle Path: " + FontBundlePath.Value); 32 | Plugin.Log.LogInfo("Font Asset Name: " + FontAssetName.Value); 33 | 34 | Plugin.Config.SettingChanged += (object sender, SettingChangedEventArgs e) => 35 | { 36 | var config = e.ChangedSetting; 37 | Plugin.Log.LogInfo($"[{config.Definition.Section}] {config.Definition.Key} => {config.BoxedValue}"); 38 | }; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /MuvluvMod/MuvluvMod.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | E:\Games\DMM GAMES\muv_luv_girlsgardenx_cl 5 | Jitsu 6 | 7 | 8 | 9 | net6.0 10 | MuvluvMod 11 | MuvluvMod 12 | 1.0.5 13 | true 14 | latest 15 | 16 | https://api.nuget.org/v3/index.json; 17 | https://nuget.bepinex.dev/v3/index.json; 18 | https://nuget.samboy.dev/v3/index.json 19 | 20 | MuvluvMod 21 | $(GameDir)\BepInEx\plugins 22 | True 23 | 24 | 25 | 26 | none 27 | 28 | 29 | 30 | none 31 | 32 | 33 | 34 | 35 | 36 | 37 | false 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /MuvluvMod/Patch.cs: -------------------------------------------------------------------------------- 1 | using Assets.Api.Client; 2 | using Assets.Battle.Overseers; 3 | using Assets.CustomRendererFeatures; 4 | using Assets.GameUi.Externals; 5 | using Assets.GameUi.Scenario; 6 | using Assets.GameUi.Scenario.Animation; 7 | using Assets.GameUi.Scenario.Choice; 8 | using Assets.GameUi.Scenario.History; 9 | using Assets.GameUi.Scenario.Text; 10 | using Assets.GameUi.Service; 11 | using Assets.VisualEffectData.VisualEffects; 12 | using BepInEx.Unity.IL2CPP.Utils; 13 | using Cysharp.Threading.Tasks; 14 | using DMM.Games.Net.Unity; 15 | using HarmonyLib; 16 | using Il2CppInterop.Runtime.InteropTypes.Arrays; 17 | using Il2CppSystem; 18 | using System.Linq; 19 | using System.Text.Json.Nodes; 20 | using TMPro; 21 | using UniRx; 22 | using UniRx.Triggers; 23 | using UnityEngine; 24 | 25 | namespace MuvluvMod 26 | { 27 | public class Patch 28 | { 29 | public static long sceneId; 30 | public static AdventureTitle adventureTitle; 31 | public static bool isPlayingScenario = false; 32 | 33 | public static void Initialize() 34 | { 35 | Harmony.CreateAndPatchAll(typeof(Patch)); 36 | } 37 | 38 | // 去马赛克 39 | [HarmonyPostfix] 40 | [HarmonyPatch(typeof(MosaicRendererFeature), nameof(MosaicRendererFeature.Create))] 41 | public static void RemoveMosaic(MosaicRendererFeature __instance) 42 | { 43 | if (!Config.DynamicMosaic.Value) 44 | { 45 | __instance.passSettings.Keyword = "114514"; 46 | } 47 | } 48 | 49 | // 开启跳过按钮/自动跳过战斗 50 | [HarmonyPrefix] 51 | [HarmonyPatch(typeof(HudOverseer), nameof(HudOverseer.SetSkipAvaiability))] 52 | public static void EnableSkipButton(HudOverseer __instance, ref bool available) 53 | { 54 | if (Config.EnableSkipButton.Value) 55 | { 56 | available = true; 57 | } 58 | if (Config.AutoSkipBattle.Value) 59 | { 60 | __instance.ProcessSkipButtonClick(); 61 | } 62 | } 63 | 64 | // 语音不中断 65 | [HarmonyPrefix] 66 | [HarmonyPatch(typeof(AudioManager), nameof(AudioManager.StopVoice))] 67 | public static bool DisableStopVoice() 68 | { 69 | return Config.VoiceInterruption.Value || !isPlayingScenario; 70 | } 71 | 72 | // 记录剧情开始播放 73 | [HarmonyPrefix] 74 | [HarmonyPatch(typeof(ScenarioController), nameof(ScenarioController.Refresh), [])] 75 | public static void SetIsPlayingScenario() 76 | { 77 | isPlayingScenario = true; 78 | } 79 | 80 | // 记录剧情结束播放 81 | [HarmonyPrefix] 82 | [HarmonyPatch(typeof(ScenarioController), nameof(ScenarioController.Leave))] 83 | public static void SetIsNotPlayingScenario() 84 | { 85 | isPlayingScenario = false; 86 | } 87 | 88 | // 翻译加载 89 | [HarmonyPrefix] 90 | [HarmonyPatch(typeof(EpisodeService), nameof(EpisodeService.DownloadSceneFrameMasters))] 91 | public static void LoadTranslation(EpisodeService __instance, long sceneMasterId) 92 | { 93 | Plugin.Log.LogInfo($"Scene: {sceneMasterId}"); 94 | 95 | __instance.sceneFrameMastersCache.Remove(sceneMasterId); 96 | 97 | if (!Config.Translation.Value) return; 98 | 99 | sceneId = sceneMasterId; 100 | 101 | Translation.GetScenarioTranslationAsync(sceneMasterId).Wait(); 102 | 103 | if (Translation.IsTranslated) 104 | { 105 | Plugin.Instance.StartCoroutine(Translation.LoadFontAsset()); 106 | } 107 | } 108 | 109 | // 翻译替换 110 | [HarmonyPrefix] 111 | [HarmonyPatch(typeof(ScenarioController), nameof(ScenarioController.GenerateFrames))] 112 | public static void ReplaceTranslation(Il2CppReferenceArray masters) 113 | { 114 | if (!Config.Translation.Value || !Translation.IsTranslated) return; 115 | 116 | try 117 | { 118 | foreach (var frame in masters) 119 | { 120 | if (string.IsNullOrEmpty(frame.ConfigurationJson)) continue; 121 | 122 | var config = JsonNode.Parse(frame.ConfigurationJson); 123 | 124 | if (config?["Phrase"] is JsonObject phrase) 125 | { 126 | if (phrase.TryGetPropertyValue("SpeakerName", out var nameNode) 127 | && Translation.names.TryGetValue(nameNode.ToString(), out var speakerName)) 128 | phrase["SpeakerName"] = speakerName; 129 | 130 | if (phrase.TryGetPropertyValue("TeamName", out var teamNode) 131 | && Translation.teamNames.TryGetValue(teamNode.ToString(), out var teamName)) 132 | phrase["TeamName"] = teamName; 133 | 134 | if (phrase.TryGetPropertyValue("Text", out var textNode) 135 | && Translation.scenes[sceneId].TryGetValue(textNode.ToString(), out var text)) 136 | phrase["Text"] = text; 137 | } 138 | 139 | frame.ConfigurationJson = config.ToJsonString(); 140 | } 141 | } 142 | catch (System.Exception e) 143 | { 144 | Plugin.Log.LogError($"Error in ReplaceTranslation: {e.StackTrace}"); 145 | } 146 | } 147 | 148 | [HarmonyPostfix] 149 | [HarmonyPatch(typeof(ScenarioController), nameof(ScenarioController.GenerateFrame))] 150 | public static void ReplaceTitle(ScenarioController.ScenarioFrameViewModel __result) 151 | { 152 | if (!Config.Translation.Value || !Translation.IsTranslated || __result.TitleAnimation == null) return; 153 | 154 | if (Translation.titles.TryGetValue(__result.TitleAnimation.TitleHead, out string titleHead)) 155 | { 156 | __result.TitleAnimation.TitleHead = titleHead; 157 | } 158 | if (Translation.subTitles.TryGetValue(__result.TitleAnimation.Title, out string title)) 159 | { 160 | __result.TitleAnimation.Title = title; 161 | } 162 | } 163 | 164 | [HarmonyPostfix] 165 | [HarmonyPatch(typeof(ScenarioAnimationComponent), nameof(ScenarioAnimationComponent.Initialize))] 166 | public static void ReplaceTitleFont(ScenarioAnimationComponent __instance) 167 | { 168 | var parent = __instance.gameObject.transform.Find("ScreenAnimationParent"); 169 | parent.OnTransformChildrenChangedAsObservable().Subscribe((Action)(_ => 170 | { 171 | Transform title = Enumerable.Range(0, parent.childCount) 172 | .Select(i => parent.GetChild(i)) 173 | .FirstOrDefault(child => child.name.Contains("title", System.StringComparison.OrdinalIgnoreCase)); 174 | 175 | if (title == null) 176 | { 177 | Plugin.Log.LogWarning("Title not found"); 178 | return; 179 | } 180 | 181 | adventureTitle = title.GetComponent(); 182 | if (adventureTitle != null && Config.Translation.Value && Translation.IsTranslated) 183 | { 184 | adventureTitle.Title.font = Translation.fontAsset; 185 | adventureTitle.Body.font = Translation.fontAsset; 186 | } 187 | })); 188 | } 189 | 190 | // 字体替换 191 | [HarmonyPostfix] 192 | [HarmonyPatch(typeof(ScenarioTextComponent), nameof(ScenarioTextComponent.OnEnable))] 193 | public static void ReplaceFont(ScenarioTextComponent __instance) 194 | { 195 | if (!Config.Translation.Value || !Translation.IsTranslated) 196 | { 197 | if (adventureTitle != null) 198 | { 199 | RestoreFontAsset(adventureTitle.Title); 200 | RestoreFontAsset(adventureTitle.Body); 201 | } 202 | RestoreFontAsset(__instance.nameText, true); 203 | RestoreFontAsset(__instance.affiliationText, true); 204 | RestoreFontAsset(__instance.sentenceText.tmpText, true); 205 | return; 206 | } 207 | 208 | if (Translation.rawFontAsset == null) 209 | { 210 | Translation.rawFontAsset = __instance.nameText.font; 211 | Translation.rawOutlineMaterial = __instance.nameText.fontMaterial; 212 | } 213 | 214 | if (adventureTitle != null) 215 | { 216 | adventureTitle.Title.font = Translation.fontAsset; 217 | adventureTitle.Body.font = Translation.fontAsset; 218 | } 219 | __instance.nameText.font = Translation.fontAsset; 220 | __instance.nameText.fontMaterial = Translation.outlineMaterial; 221 | __instance.affiliationText.font = Translation.fontAsset; 222 | __instance.affiliationText.fontMaterial = Translation.outlineMaterial; 223 | __instance.sentenceText.tmpText.font = Translation.fontAsset; 224 | __instance.sentenceText.tmpText.fontMaterial = Translation.outlineMaterial; 225 | } 226 | 227 | // 修正行距 TODO: 修正由于剧情播放结束后没有恢复原行距导致的主线中剧情结尾部分会有原字体行距异常 228 | [HarmonyPostfix] 229 | [HarmonyPatch(typeof(ScenarioTextComponent), nameof(ScenarioTextComponent.ApplySentence))] 230 | public static void FixLineSpacing(ScenarioTextComponent __instance) 231 | { 232 | if (!Config.Translation.Value || !Translation.IsTranslated) return; 233 | 234 | if (__instance.sentenceText.tmpText.font.name == Config.FontAssetName.Value) 235 | { 236 | __instance.sentenceText.tmpText.lineSpacing = 40f; 237 | } 238 | } 239 | 240 | [HarmonyPrefix] 241 | [HarmonyPatch(typeof(ScenarioHistoryCell), nameof(ScenarioHistoryCell.ApplyText))] 242 | public static void ReplaceHistoryChoice(ref string phrase, bool isAnswer) 243 | { 244 | if (!Config.Translation.Value || !Translation.IsTranslated) return; 245 | 246 | if (isAnswer && Translation.scenes[sceneId].TryGetValue(phrase, out string text)) 247 | { 248 | phrase = text; 249 | } 250 | } 251 | 252 | [HarmonyPostfix] 253 | [HarmonyPatch(typeof(ScenarioHistoryCell), nameof(ScenarioHistoryCell.ApplySync))] 254 | public static void ReplaceHistoryFont(ScenarioHistoryCell __instance) 255 | { 256 | if (!Config.Translation.Value || !Translation.IsTranslated) 257 | { 258 | RestoreFontAsset(__instance.speakerName); 259 | RestoreFontAsset(__instance.text, false, -80f); 260 | return; 261 | } 262 | 263 | __instance.speakerName.font = Translation.fontAsset; 264 | __instance.text.font = Translation.fontAsset; 265 | 266 | __instance.text.lineSpacing = 0f; 267 | } 268 | 269 | [HarmonyPrefix] 270 | [HarmonyPatch(typeof(ScenarioChoiceElementComponent), nameof(ScenarioChoiceElementComponent.Apply))] 271 | public static void ReplaceChoice(ScenarioChoiceElementComponent __instance, ScenarioChoiceElementComponent.Args args) 272 | { 273 | if (!Config.Translation.Value || !Translation.IsTranslated) 274 | { 275 | RestoreFontAsset(__instance.text); 276 | return; 277 | } 278 | 279 | if (Translation.scenes[sceneId].TryGetValue(args.Text, out string text)) 280 | { 281 | args.Text = text; 282 | __instance.text.font = Translation.fontAsset; 283 | } 284 | else 285 | { 286 | RestoreFontAsset(__instance.text); 287 | } 288 | } 289 | 290 | public static void RestoreFontAsset(TMP_Text text, bool restoreMaterial = false, float? lineSpacing = null) 291 | { 292 | if (Translation.rawFontAsset == null) return; 293 | 294 | text.font = Translation.rawFontAsset; 295 | if (restoreMaterial) text.fontMaterial = Translation.rawOutlineMaterial; 296 | if (lineSpacing.HasValue) text.lineSpacing = lineSpacing.Value; 297 | } 298 | } 299 | } 300 | -------------------------------------------------------------------------------- /MuvluvMod/Plugin.cs: -------------------------------------------------------------------------------- 1 | using BepInEx; 2 | using BepInEx.Configuration; 3 | using BepInEx.Logging; 4 | using System; 5 | using System.Text; 6 | using BepInEx.Unity.IL2CPP; 7 | using UnityEngine; 8 | 9 | namespace MuvluvMod; 10 | 11 | [BepInPlugin(MyPluginInfo.PLUGIN_GUID, MyPluginInfo.PLUGIN_NAME, MyPluginInfo.PLUGIN_VERSION)] 12 | public class Plugin : BasePlugin 13 | { 14 | public static new ConfigFile Config; 15 | public static new ManualLogSource Log; 16 | public static MonoBehaviour Instance; 17 | 18 | public override void Load() 19 | { 20 | try { 21 | Console.OutputEncoding = Encoding.UTF8; 22 | } catch (Exception) 23 | { 24 | } 25 | 26 | // Plugin startup logic 27 | Log = base.Log; 28 | Config = base.Config; 29 | Log.LogInfo($"Plugin {MyPluginInfo.PLUGIN_GUID} is loaded!"); 30 | MuvluvMod.Config.Initialize(); 31 | Patch.Initialize(); 32 | Instance = AddComponent(); 33 | Translation.Initialize(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /MuvluvMod/Translation.cs: -------------------------------------------------------------------------------- 1 | using BepInEx; 2 | using System; 3 | using System.Collections; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using System.Net.Http; 7 | using System.Net.Http.Json; 8 | using System.Threading.Tasks; 9 | using TMPro; 10 | using UnityEngine; 11 | using BepInEx.Unity.IL2CPP.Utils; 12 | 13 | 14 | namespace MuvluvMod 15 | { 16 | public class Translation 17 | { 18 | public static string cdn = "http://localhost:5000"; 19 | public static HttpClient client = new(); 20 | public static Dictionary names = []; 21 | public static Dictionary teamNames = []; 22 | public static Dictionary titles = []; 23 | public static Dictionary subTitles = []; 24 | public static Dictionary> scenes = []; 25 | public static AssetBundle fontBundle = null; 26 | public static TMP_FontAsset fontAsset = null; 27 | public static Material outlineMaterial = null; 28 | 29 | public static TMP_FontAsset rawFontAsset = null; 30 | public static Material rawOutlineMaterial = null; 31 | 32 | public static bool IsTranslated => scenes.ContainsKey(Patch.sceneId); 33 | 34 | public static void Initialize() 35 | { 36 | cdn = Config.TranslationCDN.Value; 37 | Task.Run(LoadTranslation); 38 | Plugin.Instance.StartCoroutine(LoadFontAsset()); 39 | } 40 | 41 | public static async Task GetAsync(string url) where T : class 42 | { 43 | try 44 | { 45 | var response = await client.GetAsync(url); 46 | if (response.IsSuccessStatusCode) 47 | { 48 | return await response.Content.ReadFromJsonAsync(); 49 | } 50 | else 51 | { 52 | Plugin.Log.LogWarning($"GET {url} {response.StatusCode}"); 53 | } 54 | } 55 | catch (Exception e) 56 | { 57 | Plugin.Log.LogError($"Error: {e.Message}"); 58 | } 59 | return null; 60 | } 61 | 62 | public static async Task LoadTranslation() 63 | { 64 | if (!Config.Translation.Value) 65 | { 66 | return; 67 | } 68 | var nameTask = GetAsync>>($"{cdn}/translation/names/zh_Hans.json"); 69 | var titleTask = GetAsync>>($"{cdn}/translation/titles/zh_Hans.json"); 70 | await Task.WhenAll(nameTask, titleTask); 71 | 72 | if (nameTask.Result != null) 73 | { 74 | names = nameTask.Result["speakerNames"]; 75 | teamNames = nameTask.Result["teamNames"]; 76 | Plugin.Log.LogInfo($"Character names translation loaded. Total: {names.Count}"); 77 | Plugin.Log.LogInfo($"Team names translation loaded. Total: {teamNames.Count}"); 78 | } 79 | else 80 | { 81 | Plugin.Log.LogWarning($"Names translation load failed"); 82 | } 83 | 84 | if (titleTask.Result != null) 85 | { 86 | titles = titleTask.Result["titles"]; 87 | subTitles = titleTask.Result["subTitles"]; 88 | Plugin.Log.LogInfo($"Scenario titles translation loaded. Total: {titles.Count}"); 89 | Plugin.Log.LogInfo($"Scenario subtitles translation loaded. Total: {subTitles.Count}"); 90 | } 91 | else 92 | { 93 | Plugin.Log.LogWarning($"Titles translation load failed"); 94 | } 95 | } 96 | 97 | public static void LoadFontBundle() 98 | { 99 | string path = Config.FontBundlePath.Value; 100 | string bundlePath = Path.IsPathRooted(path) ? path : Path.Combine(Paths.PluginPath, path); 101 | if (!File.Exists(bundlePath) || fontBundle != null) 102 | { 103 | return; 104 | } 105 | fontBundle = AssetBundle.LoadFromMemory(File.ReadAllBytes(bundlePath)); 106 | } 107 | 108 | public static IEnumerator LoadFontAsset() 109 | { 110 | if (fontAsset != null || !Config.Translation.Value) 111 | { 112 | yield break; 113 | } 114 | LoadFontBundle(); 115 | if (fontBundle == null) 116 | { 117 | Plugin.Log.LogWarning("Font bundle load failed"); 118 | yield break; 119 | } 120 | var request = fontBundle.LoadAssetAsync(Config.FontAssetName.Value); 121 | yield return request; 122 | 123 | fontAsset = request.asset.TryCast(); 124 | Plugin.Log.LogInfo($"TMP_FontAsset {fontAsset.name} is loaded"); 125 | 126 | var materialRequest = fontBundle.LoadAssetAsync($"{Config.FontAssetName.Value} Outline"); 127 | yield return materialRequest; 128 | 129 | outlineMaterial = materialRequest.asset.TryCast(); 130 | Plugin.Log.LogInfo($"Material {outlineMaterial.name} is loaded"); 131 | } 132 | 133 | public static async Task GetScenarioTranslationAsync(long sceneId) 134 | { 135 | if (scenes.ContainsKey(sceneId)) 136 | { 137 | return; 138 | } 139 | var translations = await GetAsync>($"{cdn}/translation/scenes/{sceneId}/zh_Hans.json"); 140 | if (translations != null) 141 | { 142 | scenes[sceneId] = translations; 143 | Plugin.Log.LogInfo($"Scenario translation loaded. Total: {translations.Count}"); 144 | } 145 | else 146 | { 147 | Plugin.Log.LogWarning($"Scenario translations load failed: {sceneId}"); 148 | } 149 | } 150 | 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MuvluvMod 2 | 3 | ## [English](README_EN.md) 4 | 5 | 本仓库的文件适用于 **Windows 平台 DMM Game Player 版本**的游戏客户端 6 | 7 | --- 8 | 9 | ## 功能特性 10 | 11 | - 提供剧情翻译(包括主线与活动角色剧情) 12 | - 去除游戏内动态添加的马赛克 13 | - 始终启用跳过按钮 14 | - 剧情语音不中断播放 15 | - 自动跳过战斗 16 | 17 | --- 18 | 19 | ## 使用方法 20 | 21 | ### 1. 准备工作 22 | 23 | - 确保已安装游戏的客户端(DMM Game Player 版) 24 | - 确认游戏可执行文件 `muv_luv_girlsgardenx_cl.exe` 所在目录 25 | 26 | ### 2. 下载插件 27 | 28 | - 前往 [Releases 页面](https://github.com/anosu/MuvluvMod/releases) 下载最新版本(带绿色 `Latest` 标记) 29 | - 展开 `Assets`,下载 `MuvluvMod.7z`(不要下载 `Source code`,那是源码) 30 | 31 | ### 3. 安装插件 32 | 33 | - 将压缩包解压,得到 `winhttp.dll`、`BepInEx` 文件夹等内容 34 | - 将它们复制到与 `muv_luv_girlsgardenx_cl.exe` 相同的目录下 35 | - 你的`winhttp.dll`、`BepInEx`文件夹和`muv_luv_girlsgardenx_cl.exe`应当处于同一目录下 36 | - 若已存在旧版本,可以先删除或直接覆盖 37 | 38 | ### 4. 启动游戏 39 | 40 | - 正常启动游戏:我是指从 DMM Game Player 启动或者从第三方 DMM 启动器启动,**而不是直接双击`muv_luv_girlsgardenx_cl.exe`启动!!!** 41 | - 第一次启动或游戏更新后,会出现控制台窗口并执行初始化 42 | - 初始化过程中,BepInEx 会从官网获取对应 Unity 版本的补丁 43 | - 若在第一次启动初始化时控制台出现红色报错(常见于无法直连 BepInEx 官网),请使用代理/梯子而不是加速器,确保你能够正常访问[https://unity.bepinex.dev/libraries/](https://unity.bepinex.dev/libraries/) 44 | - 初始化完成后,游戏会正常启动 45 | 46 | ### 5. 配置文件 47 | 48 | - 首次运行后会在 `BepInEx\config` 文件夹生成: 49 | - `BepInEx.cfg`(BepInEx 配置) 50 | - `MuvluvMod.cfg`(插件配置,可用于关闭翻译等) 51 | - 修改配置后需重启游戏生效 52 | - 如需隐藏控制台窗口,请在 `BepInEx.cfg` 的 `[Logging.Console]` 中将 `Enabled` 设置为 `false` 53 | 54 | --- 55 | 56 | ## 快捷键 57 | 58 | - `F2`: 开启/关闭翻译 59 | - `F3`: 开启/关闭始终启用跳过按钮 60 | - `F4`: 开启/关闭语音中断 61 | - `F5`: 开启/关闭自动跳过战斗 62 | 63 | --- 64 | 65 | ## 交流群 66 | 67 | - QQ 群 1: [660247178](https://qm.qq.com/q/N1GMXxIBCG)(已满) 68 | - QQ 群 2: [485328718](https://qm.qq.com/q/rCHcfhnW6G) 69 | 70 | 如有问题,请在群内反馈 71 | 72 | --- 73 | 74 | ## 免责声明 75 | 76 | - 本插件为 **第三方爱好者作品**,与官方开发商及发行商无任何关联 77 | - 本插件仅供学习与技术研究使用,请在 **合法合规** 的前提下使用 78 | - 使用本插件可能会影响游戏的正常运行,作者不对因使用本插件导致的任何问题(包括但不限于封号、数据丢失、程序崩溃)负责 79 | - 下载与使用本插件即视为您已同意自行承担相关风险 80 | -------------------------------------------------------------------------------- /README_EN.md: -------------------------------------------------------------------------------- 1 | This repository provides files for the **Windows DMM Game Player version** of the game. 2 | 3 | --- 4 | 5 | ## Features 6 | 7 | - Story chinese translation (including main and event character scenarios) 8 | - Remove in-game dynamic mosaics 9 | - Always enable the skip button 10 | - Prevent scenario voice interruptions 11 | - Auto-skip battles 12 | 13 | --- 14 | 15 | ## Installation 16 | 17 | ### 1. Preparation 18 | 19 | - Install the game via **DMM Game Player**. 20 | - Locate the game executable file: `muv_luv_girlsgardenx_cl.exe`. 21 | 22 | ### 2. Download 23 | 24 | - Go to the [Releases page](https://github.com/anosu/MuvluvMod/releases). 25 | - Download the latest release marked with `Latest`. 26 | - In the `Assets` section, download `MuvluvMod.7z` (do **not** download `Source code`). 27 | 28 | ### 3. Installation 29 | 30 | - Extract the archive; you will get `winhttp.dll`, `BepInEx`, and other files. 31 | - Copy all files to the same folder as `muv_luv_girlsgardenx_cl.exe`. 32 | - If an older version exists, delete or overwrite it. 33 | 34 | ### 4. Launching 35 | 36 | - On the first run (or after a game update), a console window will appear for initialization. 37 | - BepInEx will download the proper Unity patch from its official website. 38 | - After initialization, the game will start normally. 39 | 40 | ### 5. Configuration 41 | 42 | - After the first run, two config files will be created under `BepInEx\config`: 43 | - `BepInEx.cfg` (general settings) 44 | - `MuvluvMod.cfg` (mod settings, e.g. disable translation) 45 | - Restart the game after editing configs. 46 | - To hide the console window, set `Enabled = false` under `[Logging.Console]` in `BepInEx.cfg`. 47 | 48 | --- 49 | 50 | ## Hotkeys 51 | 52 | - `F2`: Toggle translation 53 | - `F3`: Toggle always-enabled skip button 54 | - `F4`: Toggle scenario voice interruption 55 | - `F5`: Toggle auto battle skip 56 | 57 | --- 58 | 59 | ## Disclaimer 60 | 61 | - This mod is a **fan-made third-party project** and has no affiliation with the official developers or publishers. 62 | - It is intended for educational and technical research purposes only. 63 | - Use of this mod may affect the normal operation of the game. The author is **not responsible** for any consequences (including but not limited to account bans, data loss, or crashes). 64 | - By downloading and using this mod, you agree to bear all risks yourself. 65 | --------------------------------------------------------------------------------