From db1eab764e012d2f6001339542b8991cc96b390e Mon Sep 17 00:00:00 2001 From: Flechazo <2558755403@qq.com> Date: Sat, 3 Oct 2026 11:14:03 +0800 Subject: [PATCH] fix(all): launch game fullscreen and hide debug overlay --- go/internal/client/app/game_launch.go | 8 ++ go/internal/client/app/platform_darwin.go | 3 +- go/internal/client/app/platform_windows.go | 10 +- .../client/app/platform_windows_test.go | 15 +- plugins/LocalIdentity/Plugin.cs | 132 ++++++++++++++++++ versions.json | 2 +- 6 files changed, 163 insertions(+), 7 deletions(-) create mode 100644 go/internal/client/app/game_launch.go diff --git a/go/internal/client/app/game_launch.go b/go/internal/client/app/game_launch.go new file mode 100644 index 0000000..d1d44bc --- /dev/null +++ b/go/internal/client/app/game_launch.go @@ -0,0 +1,8 @@ +package app + +// gameLaunchArguments asks Unity for borderless fullscreen on both supported +// desktop platforms. The LocalIdentity plug-in applies the game's native +// fullscreen routine during initialization as a second line of defence. +func gameLaunchArguments() []string { + return []string{"-screen-fullscreen", "1", "-window-mode", "borderless"} +} diff --git a/go/internal/client/app/platform_darwin.go b/go/internal/client/app/platform_darwin.go index f8e4015..7f798c9 100644 --- a/go/internal/client/app/platform_darwin.go +++ b/go/internal/client/app/platform_darwin.go @@ -25,5 +25,6 @@ func launchGame(target string) error { if err := exec.Command("pgrep", "-x", "BrownDust II").Run(); err == nil { return errGameAlreadyRunning } - return exec.Command("open", target).Start() + args := append([]string{target, "--args"}, gameLaunchArguments()...) + return exec.Command("open", args...).Start() } diff --git a/go/internal/client/app/platform_windows.go b/go/internal/client/app/platform_windows.go index a9117b3..8288c12 100644 --- a/go/internal/client/app/platform_windows.go +++ b/go/internal/client/app/platform_windows.go @@ -20,6 +20,7 @@ var ( enumWindowsProc = platformUser32DLL.NewProc("EnumWindows") getWindowThreadProcessIDProc = platformUser32DLL.NewProc("GetWindowThreadProcessId") isWindowVisibleProc = platformUser32DLL.NewProc("IsWindowVisible") + isIconicProc = platformUser32DLL.NewProc("IsIconic") showWindowAsyncProc = platformUser32DLL.NewProc("ShowWindowAsync") setForegroundWindowProc = platformUser32DLL.NewProc("SetForegroundWindow") ) @@ -76,7 +77,7 @@ func launchGame(target string) error { } return errGameAlreadyRunning } - command := visibleCommand(target) + command := visibleCommand(target, gameLaunchArguments()...) command.Dir = filepath.Dir(target) if err := command.Start(); err != nil { return err @@ -114,8 +115,11 @@ func activateProcessWindow(processID uint32, timeout time.Duration) bool { deadline := time.Now().Add(timeout) for { if window := topLevelWindowForProcess(processID); window != 0 { - const swRestore = 9 - showWindowAsyncProc.Call(window, swRestore) + iconic, _, _ := isIconicProc.Call(window) + if iconic != 0 { + const swRestore = 9 + showWindowAsyncProc.Call(window, swRestore) + } setForegroundWindowProc.Call(window) return true } diff --git a/go/internal/client/app/platform_windows_test.go b/go/internal/client/app/platform_windows_test.go index 80de274..70aed13 100644 --- a/go/internal/client/app/platform_windows_test.go +++ b/go/internal/client/app/platform_windows_test.go @@ -2,7 +2,10 @@ package app -import "testing" +import ( + "reflect" + "testing" +) func TestHiddenCommandNeverAllocatesVisibleConsole(t *testing.T) { command := hiddenCommand("powershell.exe", "-NoProfile") @@ -18,7 +21,7 @@ func TestHiddenCommandNeverAllocatesVisibleConsole(t *testing.T) { } func TestVisibleCommandDoesNotHideGUIWindow(t *testing.T) { - command := visibleCommand("Brown Dust II.exe") + command := visibleCommand("Brown Dust II.exe", gameLaunchArguments()...) if command.SysProcAttr == nil { t.Fatal("visible command has no Windows process attributes") } @@ -28,4 +31,12 @@ func TestVisibleCommandDoesNotHideGUIWindow(t *testing.T) { if command.SysProcAttr.CreationFlags&createNoWindow == 0 { t.Fatalf("visible command creation flags %#x omit CREATE_NO_WINDOW", command.SysProcAttr.CreationFlags) } + want := []string{ + "Brown Dust II.exe", + "-screen-fullscreen", "1", + "-window-mode", "borderless", + } + if !reflect.DeepEqual(command.Args, want) { + t.Fatalf("visible game command args = %#v, want %#v", command.Args, want) + } } diff --git a/plugins/LocalIdentity/Plugin.cs b/plugins/LocalIdentity/Plugin.cs index 01d7a84..59cb156 100644 --- a/plugins/LocalIdentity/Plugin.cs +++ b/plugins/LocalIdentity/Plugin.cs @@ -18,6 +18,7 @@ public sealed class Plugin : BaseUnityPlugin public const string Version = Bd2Build.Versions.Plugin; private static ManualLogSource Log; private static ClientRouting Routing; + private static MethodInfo SwitchToFullScreenMethod; private static int ShutdownHooksInstalled; internal static void LogWarning(string message) @@ -59,6 +60,8 @@ public sealed class Plugin : BaseUnityPlugin Type introUI = FindType("IntroUI"); InstallClientRouting(harmony, introUI); + TryInstall("startup fullscreen", () => InstallStartupFullscreen(harmony, appManager)); + TryInstall("performance overlay suppression", () => InstallPerformanceOverlaySuppression(harmony)); TryInstall("maintenance timeout guard", () => InstallMaintenanceTimeoutGuard(harmony, introUI)); TryInstall("age-gate persistence", () => InstallAgeGatePersistence(harmony)); TryInstall("local purchase bypass", () => InstallLocalPurchaseBypass(harmony)); @@ -100,6 +103,135 @@ public sealed class Plugin : BaseUnityPlugin return false; } + private static void InstallStartupFullscreen(Harmony harmony, Type appManager) + { + MethodInfo initializeResolution = appManager?.GetMethod( + "ὪὣὬὤὪὥὣὨὤὦὧ", + BindingFlags.Instance | BindingFlags.NonPublic, + null, + Type.EmptyTypes, + null); + SwitchToFullScreenMethod = appManager?.GetMethod( + "SwitchToFullScreen", + BindingFlags.Instance | BindingFlags.Public, + null, + new[] { typeof(bool) }, + null); + if (initializeResolution == null || SwitchToFullScreenMethod == null) + { + throw new MissingMethodException("AppManager fullscreen methods were not found (client version mismatch)"); + } + harmony.Patch( + initializeResolution, + postfix: new HarmonyMethod(typeof(Plugin), nameof(InitializeResolutionPostfix))); + Log?.LogInfo("Native startup fullscreen patch installed"); + } + + private static void InitializeResolutionPostfix(object __instance) + { + try + { + // Use the game's own FullScreenWindow path so the behaviour is + // identical on Windows and macOS. This runs once during startup; + // later user-initiated switches to windowed mode remain intact. + SwitchToFullScreenMethod?.Invoke(__instance, new object[] { false }); + } + catch (Exception ex) + { + Log?.LogError("Could not apply native startup fullscreen: " + ex); + } + } + + private static void InstallPerformanceOverlaySuppression(Harmony harmony) + { + Type fpsCheck = FindType("FPS_Check"); + MethodInfo start = fpsCheck?.GetMethod( + "StartFPS", + BindingFlags.Instance | BindingFlags.Public, + null, + Type.EmptyTypes, + null); + MethodInfo render = fpsCheck?.GetMethod( + "ὬὡὡὬὢὭὦὣὥὬὯ", + BindingFlags.Instance | BindingFlags.NonPublic, + null, + Type.EmptyTypes, + null); + MethodInfo onEnable = fpsCheck?.GetMethod( + "OnEnable", + BindingFlags.Instance | BindingFlags.NonPublic, + null, + Type.EmptyTypes, + null); + MethodInfo onInitialize = fpsCheck?.GetMethod( + "OnInitialize", + BindingFlags.Instance | BindingFlags.Public, + null, + Type.EmptyTypes, + null); + if (start == null || render == null || onEnable == null || onInitialize == null) + { + throw new MissingMethodException("FPS_Check overlay methods were not found (client version mismatch)"); + } + + HarmonyMethod skip = new HarmonyMethod(typeof(Plugin), nameof(SkipPerformanceOverlay)); + HarmonyMethod hide = new HarmonyMethod(typeof(Plugin), nameof(HidePerformanceOverlayPostfix)); + harmony.Patch(start, prefix: skip); + harmony.Patch(render, prefix: skip); + harmony.Patch(onEnable, postfix: hide); + harmony.Patch(onInitialize, postfix: hide); + Log?.LogInfo("FPS_Check performance overlay disabled"); + } + + private static bool SkipPerformanceOverlay(object __instance) + { + HidePerformanceOverlay(__instance); + return false; + } + + private static void HidePerformanceOverlayPostfix(object __instance) + { + HidePerformanceOverlay(__instance); + } + + private static void HidePerformanceOverlay(object instance) + { + if (instance == null) + { + return; + } + try + { + MethodInfo disposeRecorders = instance.GetType().GetMethod( + "DisposeRecorders", + BindingFlags.Instance | BindingFlags.Public, + null, + Type.EmptyTypes, + null); + disposeRecorders?.Invoke(instance, null); + + FieldInfo textField = instance.GetType().GetField( + "_text", + BindingFlags.Instance | BindingFlags.Public); + object text = textField?.GetValue(instance); + PropertyInfo textProperty = text?.GetType().GetProperty( + "text", + BindingFlags.Instance | BindingFlags.Public); + if (textProperty?.CanWrite == true) + { + textProperty.SetValue(text, string.Empty); + } + if (text is Component component && component != null) + { + component.gameObject.SetActive(false); + } + } + catch (Exception ex) + { + Log?.LogError("Could not suppress FPS_Check overlay: " + ex); + } + } + private static void InstallClientRouting(Harmony harmony, Type introUI) { Type serverURLInfo = FindType("ὫὡὩὤὣὨὯὭὥὠὩ") ?? FindType("BDNetwork.ServerURLInfo"); diff --git a/versions.json b/versions.json index b891382..f7fb1c8 100644 --- a/versions.json +++ b/versions.json @@ -4,7 +4,7 @@ "bundle_version": "20260921135230", "seed_directory": "go/seed/v2_35_10", "plugins": { - "local_identity": "0.6.0", + "local_identity": "0.6.1", "capture_environment": "0.2.0", "login_ui": "0.1.4" }