fix: launch game fullscreen and hide debug overlay

This commit is contained in:
2026-10-03 11:14:03 +08:00
parent 9468fb3968
commit b3cd135331
6 changed files with 163 additions and 7 deletions
+8
View File
@@ -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"}
}
+2 -1
View File
@@ -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()
}
+7 -3
View File
@@ -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
}
@@ -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)
}
}
+132
View File
@@ -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");
+1 -1
View File
@@ -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"
}