diff --git a/Assembly-CSharp.csproj b/Assembly-CSharp.csproj index 78afc6f2..6e6ff5ab 100644 --- a/Assembly-CSharp.csproj +++ b/Assembly-CSharp.csproj @@ -241,6 +241,7 @@ + @@ -250,6 +251,7 @@ + diff --git a/Assets.Scripts.Core.Buriko/BurikoScriptFile.cs b/Assets.Scripts.Core.Buriko/BurikoScriptFile.cs index c0043b6f..76b69428 100644 --- a/Assets.Scripts.Core.Buriko/BurikoScriptFile.cs +++ b/Assets.Scripts.Core.Buriko/BurikoScriptFile.cs @@ -2776,6 +2776,19 @@ public BurikoVariable OperationMODClearArtsets() return BurikoVariable.Null; } + private void ShowSetupMenuIfRequired() + { + if (MODAudioSet.Instance.HasAudioSetsDefined() && !MODAudioSet.Instance.GetCurrentAudioSet(out _)) + { + GameSystem.Instance.MainUIController.modMenu.PushSubMenuAndShow(ModSubMenu.AudioSetup); + } + + if (!MODWindowManager.FullscreenLockConfigured()) + { + GameSystem.Instance.MainUIController.modMenu.PushSubMenuAndShow(ModSubMenu.WindowSetup); + } + } + public BurikoVariable OperationMODGenericCall() { SetOperationType("MODGenericCall"); @@ -2784,11 +2797,7 @@ public BurikoVariable OperationMODGenericCall() switch(callID) { case "ShowSetupMenuIfRequired": - if(MODAudioSet.Instance.HasAudioSetsDefined() && !MODAudioSet.Instance.GetCurrentAudioSet(out _)) - { - GameSystem.Instance.MainUIController.modMenu.SetSubMenu(ModSubMenu.AudioSetup); - GameSystem.Instance.MainUIController.modMenu.Show(); - } + ShowSetupMenuIfRequired(); break; case "LipSyncSettings": diff --git a/Assets.Scripts.Core.State/StateNormal.cs b/Assets.Scripts.Core.State/StateNormal.cs index 9c14ecf6..a26394ca 100644 --- a/Assets.Scripts.Core.State/StateNormal.cs +++ b/Assets.Scripts.Core.State/StateNormal.cs @@ -189,21 +189,7 @@ public bool InputHandler() // Fullscreen if (Input.GetKeyDown(KeyCode.F)) { - if (GameSystem.Instance.IsFullscreen) - { - int num14 = PlayerPrefs.GetInt("width"); - int num15 = PlayerPrefs.GetInt("height"); - if (num14 == 0 || num15 == 0) - { - num14 = 640; - num15 = 480; - } - GameSystem.Instance.DeFullscreen(width: num14, height: num15); - } - else - { - GameSystem.Instance.GoFullscreen(); - } + MODWindowManager.FullscreenToggle(showToast: true); } // Toggle Language diff --git a/Assets.Scripts.Core/GameSystem.cs b/Assets.Scripts.Core/GameSystem.cs index ea9ad470..8f68a96e 100644 --- a/Assets.Scripts.Core/GameSystem.cs +++ b/Assets.Scripts.Core/GameSystem.cs @@ -158,10 +158,6 @@ public class GameSystem : MonoBehaviour private int SystemInit = 1; - private Resolution fullscreenResolution; - - private int screenModeSet = -1; - private Stack stateStack = new Stack(); public bool HasFocus; @@ -178,13 +174,6 @@ public class GameSystem : MonoBehaviour // Unity will attempt to deserialize public properties and these aren't in the AssetBundle, // so use private ones with public accessors - private bool _isFullscreen; - public bool IsFullscreen - { - get => _isFullscreen; - private set => _isFullscreen = value; - } - private float _configMenuFontSize = 0; public float ConfigMenuFontSize { @@ -247,34 +236,17 @@ private void Initialize() { PlayerPrefs.SetInt("height", 720); } - if (PlayerPrefs.GetInt("width") < 640) + if (PlayerPrefs.GetInt("width") < 853) { - PlayerPrefs.SetInt("width", 640); + PlayerPrefs.SetInt("width", 853); } if (PlayerPrefs.GetInt("height") < 480) { PlayerPrefs.SetInt("height", 480); } - IsFullscreen = PlayerPrefs.GetInt("is_fullscreen", 0) == 1; - fullscreenResolution.width = 0; - fullscreenResolution.height = 0; - fullscreenResolution = GetFullscreenResolution(); - if (IsFullscreen) - { - Screen.SetResolution(fullscreenResolution.width, fullscreenResolution.height, fullscreen: true); - } - else if (PlayerPrefs.HasKey("height") && PlayerPrefs.HasKey("width")) - { - int width = PlayerPrefs.GetInt("width"); - int height = PlayerPrefs.GetInt("height"); - Debug.Log("Requesting window size " + width + "x" + height + " based on config file"); - Screen.SetResolution(width, height, fullscreen: false); - } - if ((Screen.width < 640 || Screen.height < 480) && !IsFullscreen) - { - Screen.SetResolution(640, 480, fullscreen: false); - } + MODWindowManager.GameSystemInitSetResolution(); + Debug.Log("Starting compile thread..."); CompileThread = new Thread(CompileScripts) { @@ -326,12 +298,7 @@ public void PostLoading() public void UpdateAspectRatio(float newratio) { AspectRatio = newratio; - if (!IsFullscreen) - { - int width = Mathf.RoundToInt((float)Screen.height * AspectRatio); - Screen.SetResolution(width, Screen.height, fullscreen: false); - } - PlayerPrefs.SetInt("width", Mathf.RoundToInt(PlayerPrefs.GetInt("height") * AspectRatio)); + MODWindowManager.RefreshWindowAspect(); MainUIController.UpdateBlackBars(); SceneController.UpdateScreenSize(); } @@ -816,37 +783,6 @@ public void UpdateWaits() }); } - public IEnumerator FrameWaitForFullscreen(int width, int height, bool fullscreen) - { - yield return (object)new WaitForEndOfFrame(); - yield return (object)new WaitForFixedUpdate(); - IsFullscreen = fullscreen; - PlayerPrefs.SetInt("is_fullscreen", fullscreen ? 1 : 0); - Screen.SetResolution(width, height, fullscreen); - while (Screen.width != width || Screen.height != height) - { - yield return (object)null; - } - } - - public void GoFullscreen() - { - IsFullscreen = true; - PlayerPrefs.SetInt("is_fullscreen", 1); - Resolution resolution = GetFullscreenResolution(); - Screen.SetResolution(resolution.width, resolution.height, fullscreen: true); - Debug.Log(resolution.width + " , " + resolution.height); - PlayerPrefs.SetInt("fullscreen_width", resolution.width); - PlayerPrefs.SetInt("fullscreen_height", resolution.height); - } - - public void DeFullscreen(int width, int height) - { - IsFullscreen = false; - PlayerPrefs.SetInt("is_fullscreen", 0); - Screen.SetResolution(width, height, fullscreen: false); - } - private void OnApplicationFocus(bool focusStatus) { HasFocus = focusStatus; @@ -854,17 +790,7 @@ private void OnApplicationFocus(bool focusStatus) private void LateUpdate() { - if (screenModeSet == -1) - { - screenModeSet = 0; - fullscreenResolution = Screen.currentResolution; - if (PlayerPrefs.HasKey("fullscreen_width") && PlayerPrefs.HasKey("fullscreen_height") && Screen.fullScreen) - { - fullscreenResolution.width = PlayerPrefs.GetInt("fullscreen_width"); - fullscreenResolution.height = PlayerPrefs.GetInt("fullscreen_height"); - } - Debug.Log("Fullscreen Resolution: " + fullscreenResolution.width + ", " + fullscreenResolution.height); - } + MODWindowManager.GetFullScreenResolutionLateUpdate(); } private bool CheckInitialization() @@ -1008,6 +934,11 @@ private void OnApplicationQuit() { SteamController.Close(); } + + if(CanExit) + { + MODWindowManager.OnApplicationReallyQuit("GameSystem.OnApplicationQuit()"); + } } /// @@ -1028,80 +959,6 @@ public T ChooseJapaneseEnglish(T japanese, T english) } } - public Resolution GetFullscreenResolution() - { - Resolution resolution = new Resolution(); - string source = ""; - // Try to guess resolution from Screen.currentResolution - if (!Screen.fullScreen || Application.platform == RuntimePlatform.OSXPlayer) - { - resolution.width = this.fullscreenResolution.width = Screen.currentResolution.width; - resolution.height = this.fullscreenResolution.height = Screen.currentResolution.height; - source = "Screen.currentResolution"; - } - else if (this.fullscreenResolution.width > 0 && this.fullscreenResolution.height > 0) - { - resolution.width = this.fullscreenResolution.width; - resolution.height = this.fullscreenResolution.height; - source = "Stored fullscreenResolution"; - } - else if (PlayerPrefs.HasKey("fullscreen_width") && PlayerPrefs.HasKey("fullscreen_height")) - { - resolution.width = PlayerPrefs.GetInt("fullscreen_width"); - resolution.height = PlayerPrefs.GetInt("fullscreen_height"); - source = "PlayerPrefs"; - } - else - { - resolution.width = Screen.currentResolution.width; - resolution.height = Screen.currentResolution.height; - source = "Screen.currentResolution as Fallback"; - } - - // Above can be glitchy on Linux, so also check the maximum resolution of a single monitor - // If it's bigger than that, then switch over - // Note that this (from what I can tell) gives you the biggest resolution of any of your monitors, - // not just the one the game is running under, so it could *also* be wrong, which is why we check both methods - if (Screen.resolutions.Length > 0) - { - int index = 0; - Resolution best = Screen.resolutions[0]; - for (int i = 1; i < Screen.resolutions.Length; i++) - { - if (Screen.resolutions[i].height * Screen.resolutions[i].width > best.height * best.width) - { - best = Screen.resolutions[i]; - index = i; - } - } - if (best.width <= resolution.width && best.height <= resolution.height) { - resolution = best; - source = "Screen.resolutions #" + index; - } - } - if (!PlayerPrefs.HasKey("fullscreen_width_override")) - { - PlayerPrefs.SetInt("fullscreen_width_override", 0); - } - if (!PlayerPrefs.HasKey("fullscreen_height_override")) - { - PlayerPrefs.SetInt("fullscreen_height_override", 0); - } - - if (PlayerPrefs.GetInt("fullscreen_width_override") > 0) - { - resolution.width = PlayerPrefs.GetInt("fullscreen_width_override"); - source += " + Width Override"; - } - if (PlayerPrefs.GetInt("fullscreen_height_override") > 0) - { - resolution.height = PlayerPrefs.GetInt("fullscreen_height_override"); - source += " + Height Override"; - } - Debug.Log("Using resolution " + resolution.width + "x" + resolution.height + " as the fullscreen resolution based on " + source + "."); - return resolution; - } - /// /// Gets the amount you should offset gui elements to center them properly based on the current aspect ratio. /// Add this number to GUI elements' positions to center them, subtract it from window positions. @@ -1113,18 +970,7 @@ public float GetGUIOffset() { ~GameSystem() { - // Fixes an issue where Unity would write garbage values to its saved state on Linux - // If we do this while the game is running, Unity will overwrite the values - // So do it in the finalizer, which will run as the game quits and the GameSystem is deallocated - if (PlayerPrefs.HasKey("width") && PlayerPrefs.HasKey("height")) - { - int width = PlayerPrefs.GetInt("width"); - int height = PlayerPrefs.GetInt("height"); - PlayerPrefs.SetInt("Screenmanager Resolution Width", width); - PlayerPrefs.SetInt("Screenmanager Resolution Height", height); - PlayerPrefs.SetInt("is_fullscreen", IsFullscreen ? 1 : 0); - PlayerPrefs.SetInt("Screenmanager Is Fullscreen mode", 0); - } + MODWindowManager.OnApplicationReallyQuit("GameSystem Destructor"); } static GameSystem() diff --git a/Assets.Scripts.Core/KeyHook.cs b/Assets.Scripts.Core/KeyHook.cs index 2cee1bbc..fe019d4f 100644 --- a/Assets.Scripts.Core/KeyHook.cs +++ b/Assets.Scripts.Core/KeyHook.cs @@ -1,3 +1,4 @@ +using MOD.Scripts.Core; using System; using System.Diagnostics; using System.Runtime.InteropServices; @@ -46,14 +47,7 @@ private static IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam) int num = Marshal.ReadInt32(lParam); if (num == 13 && GameSystem.Instance.HasFocus) { - if (GameSystem.Instance.IsFullscreen) - { - GameSystem.Instance.DeFullscreen(PlayerPrefs.GetInt("width"), PlayerPrefs.GetInt("height")); - } - else - { - GameSystem.Instance.GoFullscreen(); - } + MODWindowManager.FullscreenToggle(showToast: true); return (IntPtr)1; } } diff --git a/Assets.Scripts.UI.Config/ScreenSwitcherButton.cs b/Assets.Scripts.UI.Config/ScreenSwitcherButton.cs index 0a54159e..7e41b4d0 100644 --- a/Assets.Scripts.UI.Config/ScreenSwitcherButton.cs +++ b/Assets.Scripts.UI.Config/ScreenSwitcherButton.cs @@ -1,4 +1,5 @@ using Assets.Scripts.Core; +using MOD.Scripts.Core; using UnityEngine; namespace Assets.Scripts.UI.Config @@ -7,6 +8,9 @@ public class ScreenSwitcherButton : MonoBehaviour { public int Width; + // This variable doesn't actually change/doesn't really relate to the game's fullscreen state + // It is actually used to indicate which button is labelled as "fullscreen" (it is true only for that button) + public bool IsFullscreen; private UIButton button; @@ -31,26 +35,24 @@ private int Height() private void OnClick() { - if (IsFullscreen) + if(IsFullscreen) { - GameSystem.Instance.GoFullscreen(); + MODWindowManager.FullscreenToggle(showToast: true); } else { - int height = Height(); - int width = Mathf.RoundToInt(height * GameSystem.Instance.AspectRatio); - GameSystem.Instance.DeFullscreen(width: width, height: height); - PlayerPrefs.SetInt("width", width); - PlayerPrefs.SetInt("height", height); + MODWindowManager.SetResolution(Height(), showToast: true); } } private bool ShouldBeDown() { + // Make the 'fullscreen' button always clickable because it toggles fullscreen if (IsFullscreen) { - return GameSystem.Instance.IsFullscreen; + return false; } + return Screen.height == Height(); } diff --git a/Assets.Scripts.UI.Config/SwitchButton.cs b/Assets.Scripts.UI.Config/SwitchButton.cs index cc91918a..7d03d644 100644 --- a/Assets.Scripts.UI.Config/SwitchButton.cs +++ b/Assets.Scripts.UI.Config/SwitchButton.cs @@ -131,7 +131,7 @@ public void Click() } break; case ConfigButtonType.FullscreenMode: - GameSystem.Instance.GoFullscreen(); + MODWindowManager.GoFullscreen(showToast: true); break; case ConfigButtonType.ClickToCutVoice: GameSystem.Instance.StopVoiceOnClick = !GameSystem.Instance.StopVoiceOnClick; diff --git a/MOD.Scripts.Core/MODWindowManager.cs b/MOD.Scripts.Core/MODWindowManager.cs new file mode 100644 index 00000000..30bc98c2 --- /dev/null +++ b/MOD.Scripts.Core/MODWindowManager.cs @@ -0,0 +1,416 @@ +using Assets.Scripts.Core; +using MOD.Scripts.UI; +using System; +using System.Collections.Generic; +using System.Text; +using UnityEngine; +using System.Linq; + +namespace MOD.Scripts.Core +{ + class MODWindowManager + { + static string playerPrefsCrashDetectorKey = "crash_detector"; + const string FULLSCREEN_LOCK_KEY = "fullscreen_lock"; + + private static bool _IsFullscreen; + public static bool IsFullscreen { get { return _IsFullscreen; } } + private static Resolution fullscreenResolution; + private static int screenModeSet = -1; + + private static int lastSetWidth = 853; + private static int lastSetHeight = 480; + + private static string[] prefsToPrint = { + "width", + "height", + "is_fullscreen", + "fullscreen_width", + "fullscreen_height", + "Screenmanager Resolution Width", + "Screenmanager Resolution Height", + "Screenmanager Is Fullscreen mode", + playerPrefsCrashDetectorKey + }; + + private static void PrintPlayerPrefs(string caption) + { + string PrintPref(string key) + { + return $"{key}: {(PlayerPrefs.HasKey(key) ? PlayerPrefs.GetInt(key).ToString() : "")}"; + } + + // Print out certain playerprefs values for debugging + Debug.Log($"PlayerPrefs [{caption}]:\n{string.Join("\n", prefsToPrint.Select(key => PrintPref(key)).ToArray())}"); + } + + // Toggle between windowed and fullscreen. + // Windowed mode will use the last windowed resolution. + // Fullscreen mode will use the detected fullscreen resolution. + public static void FullscreenToggle(bool showToast=false) + { + SetResolution(maybe_width: null, maybe_height: null, maybe_fullscreen: !IsFullscreen, showToast: showToast); + } + + // Set the screen resolution, where the width will be set according to the current AspectRatio + // The windowed/fullscreen state won't be changed. + public static void SetResolution(int height, bool showToast = false) + { + SetResolution(maybe_width: null, maybe_height: height, maybe_fullscreen: null, showToast: showToast); + } + + // Go fullscreen. The new resolution will be detected automatically. + public static void GoFullscreen(bool showToast = false) + { + SetResolution(maybe_width: null, maybe_height: null, maybe_fullscreen: true, showToast: showToast); + } + + // This function does the following: + // - If full screen is enabled, the resolution will set according to the monitor resolution + // - If windowed, the window width will be set according to the height and AspectRatio + public static void RefreshWindowAspect(bool showToast = false) + { + SetResolution(maybe_width: null, maybe_height: null, maybe_fullscreen: null, showToast: showToast); + } + + public static void SetResolution(int? maybe_width, int? maybe_height, bool? maybe_fullscreen, bool showToast=false) + { + int height = 480; + int width = 853; + + // Default to keeping current fullscreen state if fullscreen not specified + TrySetIsFullscreen(maybe_fullscreen ?? IsFullscreen, "SetResolution"); + + if (maybe_width == null && maybe_height == null) + { + if (IsFullscreen) + { + // If going fullscreen, and width and height wasn't specified, use detected fullscreen resolution + Resolution resolution = GetFullscreenResolution(); + height = resolution.height; + width = resolution.width; + } + else if(PlayerPrefs.HasKey("height") && PlayerPrefs.HasKey("width")) + { + // If width and height both not specified, use saved player prefs width and height + int player_prefs_height = PlayerPrefs.GetInt("height"); + int player_prefs_width = PlayerPrefs.GetInt("width"); + + if (player_prefs_width != 0 && player_prefs_height != 0) + { + height = player_prefs_height; + width = player_prefs_width; + } + } + } + else if (maybe_width == null && maybe_height != null) + { + // If only height specified, use aspect ratio to set width + height = maybe_height.Value; + width = Mathf.RoundToInt(height * GameSystem.Instance.AspectRatio); + } + else if (maybe_width != null && maybe_height != null) + { + // If both specified, just use directly (ignore current aspect ratio) + height = maybe_height.Value; + width = maybe_width.Value; + } + + // Do some sanity checks on the width and height + if (height < 480) + { + MODToaster.Show("Height too small - must be at least 480 pixels"); + Debug.Log("Height too small - must be at least 480 pixels"); + height = 480; + } + else if (height > 15360) + { + MODToaster.Show("Height too big - must be less than 15360 pixels"); + Debug.Log("Height too big - must be less than 15360 pixels"); + height = 15360; + } + + if (width < 853) + { + MODToaster.Show("Width too small - must be at least 853 pixels"); + Debug.Log("Width too small - must be at least 853 pixels"); + width = 853; + } + else if (width > 15360) + { + MODToaster.Show("Width too big - must be less than 15360 pixels"); + Debug.Log("Width too big - must be less than 15360 pixels"); + width = 15360; + } + + Screen.SetResolution(width, height, IsFullscreen); + + lastSetWidth = width; + lastSetHeight = height; + + // Update playerprefs (won't be saved until game exits or PlayerPrefs.Save() is called + SetPlayerPrefs(); + + if (showToast) + { + string prefix = "Set Res"; + if(maybe_fullscreen == false && FullscreenLocked()) + { + prefix = "Fullscreen Locked"; + } + MODToaster.Show($"{prefix}: {width}x{height}"); + } + } + + // NOTE: this function does not save playerprefs + // playerprefs are saved when the game exits cleanly, or on manual calls to PlayerPrefs.Save() + private static void SetPlayerPrefs() + { + PlayerPrefs.SetInt(IsFullscreen ? "fullscreen_width" : "width", lastSetWidth); + PlayerPrefs.SetInt(IsFullscreen ? "fullscreen_height" : "height", lastSetHeight); + PlayerPrefs.SetInt("is_fullscreen", IsFullscreen ? 1 : 0); + + PlayerPrefs.SetInt("Screenmanager Resolution Width", lastSetWidth); + PlayerPrefs.SetInt("Screenmanager Resolution Height", lastSetHeight); + + // This used to be always set false, but on Linux Gnome this caused + // TODO: decide whether to set this to IsFullscreen, or to just always set true. + // On Windows this doesn't seem to make any difference + PlayerPrefs.SetInt("Screenmanager Is Fullscreen mode", 1); + } + + private static bool PlayerPrefsNeedsReset() + { + // If there was a crash, then assume playerprefs needs reset to prevent further crashes + if (PlayerPrefs.HasKey(playerPrefsCrashDetectorKey)) + { + return true; + } + + // TODO: not sure if should enable this, as if Unity resets 'Screenmanager Is Fullscreen mode' to windowed + // because the player was playing in windowed, and our mod wasn't able to override it, + // some users will have their settings reset unexpectedly. + // + // Additionally, the above crash detection seems sufficient to fix the Gnome crash bug. + // On Linux, if 'Screenmanager Is Fullscreen mode' is set to 0, assume game needs playerprefs reset? + //if (Application.platform == RuntimePlatform.LinuxPlayer) + //{ + // if (PlayerPrefs.HasKey("Screenmanager Is Fullscreen mode")) + // { + // return PlayerPrefs.GetInt("Screenmanager Is Fullscreen mode") == 0; + // } + // else + // { + // return true; + // } + //} + + return false; + } + + public static void GameSystemInitSetResolution() + { + PrintPlayerPrefs("On Startup"); + + // On OS other than Linux, default to disabling fullscreen lock rather than asking the user, + // as it doesn't help with any known bugs on Windows/MacOS + if (Application.platform != RuntimePlatform.LinuxPlayer && !FullscreenLockConfigured()) + { + SetFullScreenLock(false); + } + + // If crash detected on linux, force fullscreen mode, in case crash was caused by gnome windowed mode bug. + // Also, show the window setup menu again so user can configure fullscreen lock. + if (Application.platform == RuntimePlatform.LinuxPlayer && PlayerPrefsNeedsReset()) + { + ForceUnconfigureFullscreenLock(); + // TODO: could fully reset playerprefs by calling PlayerPrefs.DeleteAll(), but not sure if such drastic measures are necessary? + GoFullscreen(); + Debug.Log("WARNING: Crash or corrupted playerprefs detected. Reverting to fullscreen mode!"); + PrintPlayerPrefs("After Fixing due to Crash or Corrupted PlayerPrefs"); + } + + PlayerPrefs.SetInt(playerPrefsCrashDetectorKey, 1); + + // Restore IsFullscreen variable from playerprefs + TrySetIsFullscreen(PlayerPrefs.GetInt("is_fullscreen", 1) == 1, "On Startup"); + + // Restore fullscreenResolution variable using GetFullscreenResolution() + fullscreenResolution.width = 0; + fullscreenResolution.height = 0; + fullscreenResolution = GetFullscreenResolution(); + + // TODO: fix this when restoring from fullscreen on startup + // Now that variables restored set the actual resolution + SetResolution(null, null, null); + + // If the playerprefs is corrupted, the game will crash shortly after starting up + // This prevents us from repairing the playerprefs, because the game normally only saves when the game exits cleanly + // To fix this, save the playerprefs immediately as soon as the game starts up + PlayerPrefs.Save(); + + PrintPlayerPrefs("After Init Resolution"); + } + + public static void GetFullScreenResolutionLateUpdate() + { + if (screenModeSet == -1) + { + screenModeSet = 0; + fullscreenResolution = Screen.currentResolution; + if (PlayerPrefs.HasKey("fullscreen_width") && PlayerPrefs.HasKey("fullscreen_height") && Screen.fullScreen) + { + fullscreenResolution.width = PlayerPrefs.GetInt("fullscreen_width"); + fullscreenResolution.height = PlayerPrefs.GetInt("fullscreen_height"); + } + Debug.Log("Fullscreen Resolution: " + fullscreenResolution.width + ", " + fullscreenResolution.height); + } + } + + // This is inserted into both: + // - ~GameSystem(), which DOES NOT execute on Windows when the program exits (only Linux?) + // - OnApplicationQuit, to be called only if the application is really quitting, which runs on Windows + // - Calling this function more than once shouldn't cause any problems. + // + // Also note that on Windows: + // + // - if DISPLAY CONFIRMATION is OFF, and the 'task' is ended via normal task manager (not via the process list) + // this function is still called, because Windows will first try to close the program normally. + // + // - if DISPLAY CONFIRMATION is ON, since the program can't close on its own, windows will then + // force close the program which is detected as a crash + // + // - Additionally, if you go to the process instead of the program and kill it that way, it will always + // be detected as a crash + // + // On Linux, closing via task manager generally is treated as a crash. + public static void OnApplicationReallyQuit(string context) + { + // Fixes an issue where Unity would write garbage values to its saved state on Linux + // If we do this while the game is running, Unity will overwrite the values + // So do it in the finalizer, which will run as the game quits and the GameSystem is deallocated + // + // This also allows us to override the Screenmanager playerprefs variables on Windows, + // which are normally overwritten when the game exits + SetPlayerPrefs(); + + // Clear the crash detector key if program closed normally + PlayerPrefs.DeleteKey(playerPrefsCrashDetectorKey); + + PrintPlayerPrefs($"OnApplicationReallyQuit() called from {context}"); + } + + public static void SetFullScreenLock(bool enableLock) + { + PlayerPrefs.SetInt(FULLSCREEN_LOCK_KEY, enableLock ? 1 : 0); + if(FullscreenLocked()) + { + GoFullscreen(); + } + } + + public static bool FullscreenLocked() + { + return PlayerPrefs.GetInt(FULLSCREEN_LOCK_KEY, 0) != 0; + } + + public static bool FullscreenLockConfigured() + { + return PlayerPrefs.HasKey(FULLSCREEN_LOCK_KEY); + } + public static void ForceUnconfigureFullscreenLock() + { + PlayerPrefs.DeleteKey(FULLSCREEN_LOCK_KEY); + } + public static void ClearPlayerPrefsFullscreenResolution() + { + PlayerPrefs.DeleteKey("fullscreen_width"); + PlayerPrefs.DeleteKey("fullscreen_height"); + } + + private static Resolution GetFullscreenResolution() + { + Resolution resolution = new Resolution(); + string source = ""; + // Try to guess resolution from Screen.currentResolution + if (!Screen.fullScreen || Application.platform == RuntimePlatform.OSXPlayer) + { + resolution.width = fullscreenResolution.width = Screen.currentResolution.width; + resolution.height = fullscreenResolution.height = Screen.currentResolution.height; + source = "Screen.currentResolution"; + } + else if (fullscreenResolution.width > 0 && fullscreenResolution.height > 0) + { + resolution.width = fullscreenResolution.width; + resolution.height = fullscreenResolution.height; + source = "Stored fullscreenResolution"; + } + else if (PlayerPrefs.HasKey("fullscreen_width") && PlayerPrefs.HasKey("fullscreen_height")) + { + resolution.width = PlayerPrefs.GetInt("fullscreen_width"); + resolution.height = PlayerPrefs.GetInt("fullscreen_height"); + source = "PlayerPrefs"; + } + else + { + resolution.width = Screen.currentResolution.width; + resolution.height = Screen.currentResolution.height; + source = "Screen.currentResolution as Fallback"; + } + + // Above can be glitchy on Linux, so also check the maximum resolution of a single monitor + // If it's bigger than that, then switch over + // Note that this (from what I can tell) gives you the biggest resolution of any of your monitors, + // not just the one the game is running under, so it could *also* be wrong, which is why we check both methods + if (Screen.resolutions.Length > 0) + { + int index = 0; + Resolution best = Screen.resolutions[0]; + for (int i = 1; i < Screen.resolutions.Length; i++) + { + if (Screen.resolutions[i].height * Screen.resolutions[i].width > best.height * best.width) + { + best = Screen.resolutions[i]; + index = i; + } + } + if (best.width <= resolution.width && best.height <= resolution.height) + { + resolution = best; + source = "Screen.resolutions #" + index; + } + } + if (!PlayerPrefs.HasKey("fullscreen_width_override")) + { + PlayerPrefs.SetInt("fullscreen_width_override", 0); + } + if (!PlayerPrefs.HasKey("fullscreen_height_override")) + { + PlayerPrefs.SetInt("fullscreen_height_override", 0); + } + + if (PlayerPrefs.GetInt("fullscreen_width_override") > 0) + { + resolution.width = PlayerPrefs.GetInt("fullscreen_width_override"); + source += " + Width Override"; + } + if (PlayerPrefs.GetInt("fullscreen_height_override") > 0) + { + resolution.height = PlayerPrefs.GetInt("fullscreen_height_override"); + source += " + Height Override"; + } + Debug.Log("Using resolution " + resolution.width + "x" + resolution.height + " as the fullscreen resolution based on " + source + "."); + return resolution; + } + private static void TrySetIsFullscreen(bool isFullscreen, string context) + { + if (!isFullscreen && FullscreenLocked()) + { + Debug.Log($"WARNING [{context}]: Attempted to change to windowed mode, but 'fullscreen lock' enabled, so staying in fullscreen mode!"); + return; + } + + _IsFullscreen = isFullscreen; + } + } +} diff --git a/MOD.Scripts.UI/MODMenu.cs b/MOD.Scripts.UI/MODMenu.cs index 4c2fd81c..928d87d4 100644 --- a/MOD.Scripts.UI/MODMenu.cs +++ b/MOD.Scripts.UI/MODMenu.cs @@ -17,16 +17,46 @@ public enum ModSubMenu { Normal, AudioSetup, + WindowSetup, } public class MODMenu { + private class SubMenuManager + { + private Stack subMenuStack; + + public SubMenuManager(MODMenuModuleInterface defaultItem) + { + this.subMenuStack = new Stack(); + this.subMenuStack.Push(defaultItem); + } + + public MODMenuModuleInterface CurrentMenu() => this.subMenuStack.Peek(); + public void Push(MODMenuModuleInterface subMenu) => this.subMenuStack.Push(subMenu); + + // Returns true if successfully removed an item off the stack + // Returns false if only the default item is left on the stack (no action taken) + public bool TryPop() + { + if (this.subMenuStack.Count > 1) + { + this.subMenuStack.Pop(); + return true; + } + else + { + return false; + } + } + } + private const int DEBUG_WINDOW_ID = 1; private readonly GameSystem gameSystem; public bool visible; public bool debug; - private bool lastMenuVisibleStatus; + private bool onBeforeMenuVisibleAlreadyCalled; private MODSimpleTimer defaultToolTipTimer; private MODSimpleTimer startupWatchdogTimer; private bool startupFailed; @@ -38,7 +68,9 @@ public class MODMenu private MODMenuNormal normalMenu; private MODMenuAudioOptions audioOptionsMenu; private MODMenuAudioSetup audioSetupMenu; - private MODMenuModuleInterface currentMenu; // The menu that is currently visible + private MODSubMenuWindowSetup windowSetupMenu; + + private SubMenuManager subMenuManager; string lastToolTip = String.Empty; @@ -62,7 +94,7 @@ public MODMenu(GameSystem gameSystem) this.gameSystem = gameSystem; this.visible = false; this.debug = false; - this.lastMenuVisibleStatus = false; + this.onBeforeMenuVisibleAlreadyCalled = false; this.defaultToolTipTimer = new MODSimpleTimer(); this.startupWatchdogTimer = new MODSimpleTimer(); this.startupFailed = false; @@ -73,7 +105,9 @@ public MODMenu(GameSystem gameSystem) this.audioOptionsMenu = new MODMenuAudioOptions(this); this.normalMenu = new MODMenuNormal(this, this.audioOptionsMenu); this.audioSetupMenu = new MODMenuAudioSetup(this, this.audioOptionsMenu); - this.currentMenu = this.normalMenu; + this.windowSetupMenu = new MODSubMenuWindowSetup(this, this.normalMenu); + + subMenuManager = new SubMenuManager(this.normalMenu); this.debugWindowRect = new Rect(0, 0, Screen.width / 3, Screen.height - 50); } @@ -82,6 +116,7 @@ public void Update() { defaultToolTipTimer.Update(); startupWatchdogTimer.Update(); + windowSetupMenu.Update(); } public void LateUpdate() @@ -210,14 +245,17 @@ public void OnGUIFragment() GUILayout.EndArea(); } + // The rest of this function assumes currentMenu does not change while the function is executing, + // so this cached value of the current menu is used instead of subMenuManager.CurrentMenu() + MODMenuModuleInterface currentMenu = subMenuManager.CurrentMenu(); + // If you need to initialize things just once before the menu opens, rather than every frame // you can do it in the OnBeforeMenuVisible() function below. - if (visible && !lastMenuVisibleStatus) + if (visible && !onBeforeMenuVisibleAlreadyCalled) { currentMenu.OnBeforeMenuVisible(); + onBeforeMenuVisibleAlreadyCalled = true; } - lastMenuVisibleStatus = visible; - if (visible) { @@ -324,20 +362,45 @@ public void OverrideClickSound(GUISound sound) buttonClickSound = sound; } - // The mod menu has different sub-menus, which can be switched between by calling this function. + // Switch to the given submenu, without discarding any previous submenus. // If the sub-menus have any state, it will be retained during switching, and even if the menu is closed and reopened. - public void SetSubMenu(ModSubMenu subMenu) + public void PushSubMenuAndShow(ModSubMenu subMenu) { + onBeforeMenuVisibleAlreadyCalled = false; + + MODMenuModuleInterface subMenuToPush = normalMenu; + switch (subMenu) { case ModSubMenu.AudioSetup: - currentMenu = audioSetupMenu; + subMenuToPush = audioSetupMenu; + break; + + case ModSubMenu.WindowSetup: + subMenuToPush = windowSetupMenu; break; case ModSubMenu.Normal: - default: - currentMenu = normalMenu; + subMenuToPush = normalMenu; break; + + default: + return; + } + + subMenuManager.Push(subMenuToPush); + + Show(); + } + + // Switch to the next submenu on the stack. If only the default submenu remains, just hide the entire menu. + public void PopSubMenu() + { + onBeforeMenuVisibleAlreadyCalled = false; + + if (!subMenuManager.TryPop()) + { + UserHide(); } } @@ -352,6 +415,7 @@ void ForceShow() gameSystem.SetMODIgnoreInputs(menuStopsGameUpdate); gameSystem.HideUIControls(); this.visible = true; + onBeforeMenuVisibleAlreadyCalled = false; } if (gameSystem.GameState == GameState.SaveLoadScreen) @@ -378,7 +442,7 @@ void ForceShow() /// public void UserHide() { - if (currentMenu.UserCanClose()) + if (subMenuManager.CurrentMenu().UserCanClose()) { ForceHide(); } @@ -390,7 +454,7 @@ public void UserHide() /// public void UserToggleVisibility() { - if (currentMenu.UserCanClose()) + if (subMenuManager.CurrentMenu().UserCanClose()) { ForceToggleVisibility(); } diff --git a/MOD.Scripts.UI/MODMenuAudioSetup.cs b/MOD.Scripts.UI/MODMenuAudioSetup.cs index df96c873..4bcf4a68 100644 --- a/MOD.Scripts.UI/MODMenuAudioSetup.cs +++ b/MOD.Scripts.UI/MODMenuAudioSetup.cs @@ -33,13 +33,12 @@ public void OnGUI() if (GetGlobal("GAudioSet") != 0 && Button(new GUIContent("Click here when you're finished."))) { - modMenu.SetSubMenu(ModSubMenu.Normal); - modMenu.ForceHide(); + modMenu.PopSubMenu(); } } public bool UserCanClose() => false; - public string Heading() => "First-Time Setup Menu"; + public string Heading() => "Audio Setup Menu"; public string DefaultTooltip() => "Please choose the options on the left before continuing. You can hover over a button to view its description."; } } diff --git a/MOD.Scripts.UI/MODMenuNormal.cs b/MOD.Scripts.UI/MODMenuNormal.cs index 1180b3e7..7391010f 100644 --- a/MOD.Scripts.UI/MODMenuNormal.cs +++ b/MOD.Scripts.UI/MODMenuNormal.cs @@ -1,4 +1,5 @@ using Assets.Scripts.Core; +using MOD.Scripts.Core; using MOD.Scripts.Core.Audio; using System; using System.Collections.Generic; @@ -31,6 +32,7 @@ class MODMenuNormal : MODMenuModuleInterface private readonly MODRadio radioStretchBackgrounds; private readonly MODRadio radioTextWindowModeAndCrop; private readonly MODRadio radioForceComputedLipsync; + private readonly MODRadio radioFullscreenLock; private readonly MODTabControl tabControl; @@ -136,6 +138,11 @@ Sets the script censorship level new GUIContent("Computed Always", "Always use computed lipsync for all voices. Any 'spectrum' files will be ignored.") }); + radioFullscreenLock = new MODRadio("Fullscreen Lock", new GUIContent[] { + new GUIContent("No Lock", "Allow switching to Windowed mode at any time"), + new GUIContent("Force Fullscreen Always", "Force fullscreen mode always - do not allow switching to windowed mode.") + }); + tabControl = new MODTabControl(new List { new MODTabControl.TabProperties("Gameplay", "Voice Matching and Opening Videos", GameplayTabOnGUI), @@ -282,6 +289,8 @@ private void GraphicsTabOnGUI() HeadingLabel("Resolution"); resolutionMenu.OnGUI(); + + OnGUIFullscreenLock(); } private void GameplayTabOnGUI() @@ -485,6 +494,23 @@ private void OnGUIComputedLipsync() } } + public void OnGUIFullscreenLock() + { + HeadingLabel("Fullscreen Lock"); + + int currentValue = -1; + if(MODWindowManager.FullscreenLockConfigured()) + { + currentValue = MODWindowManager.FullscreenLocked() ? 1 : 0; + } + + if (this.radioFullscreenLock.OnGUIFragment(currentValue) is int newFullscreenlock) + { + MODWindowManager.SetFullScreenLock(newFullscreenlock == 0 ? false : true); + MODToaster.Show(MODWindowManager.FullscreenLocked() ? "Fullscreen lock enabled" : "Fullscreen lock disabled"); + } + } + public bool UserCanClose() => true; public string Heading() => "Mod Options Menu"; diff --git a/MOD.Scripts.UI/MODMenuResolution.cs b/MOD.Scripts.UI/MODMenuResolution.cs index c8bc2f75..c4924f61 100644 --- a/MOD.Scripts.UI/MODMenuResolution.cs +++ b/MOD.Scripts.UI/MODMenuResolution.cs @@ -1,4 +1,5 @@ using Assets.Scripts.Core; +using MOD.Scripts.Core; using System; using System.Collections.Generic; using System.Text; @@ -23,26 +24,30 @@ public void OnBeforeMenuVisible() public void OnGUI() { - Label("Resolution Settings"); + Label($"Resolution Settings (Current: {Screen.width}x{Screen.height})"); + + // I noticed a bug on Linux where going Windowed sometimes doesn't let you change window size, + // probably due res settings Playerprefs that Unity reads on startup. + // Restarting the game after changing settings fixes this. + if(Application.platform == RuntimePlatform.LinuxPlayer) + { + Label($"NOTE: You may need to restart the game after changing to Windowed!"); + } + { GUILayout.BeginHorizontal(); if (Button(new GUIContent("480p", "Set resolution to 853 x 480"))) { SetAndSaveResolution(480); } if (Button(new GUIContent("720p", "Set resolution to 1280 x 720"))) { SetAndSaveResolution(720); } if (Button(new GUIContent("1080p", "Set resolution to 1920 x 1080"))) { SetAndSaveResolution(1080); } if (Button(new GUIContent("1440p", "Set resolution to 2560 x 1440"))) { SetAndSaveResolution(1440); } - if (GameSystem.Instance.IsFullscreen) + + GUIContent buttonContent = MODWindowManager.IsFullscreen ? + new GUIContent("Go Windowed", "Toggle Fullscreen") : + new GUIContent("Go Fullscreen", "Toggle Fullscreen"); + + if(Button(buttonContent)) { - if (Button(new GUIContent("Windowed", "Toggle Fullscreen"))) - { - GameSystem.Instance.DeFullscreen(PlayerPrefs.GetInt("width"), PlayerPrefs.GetInt("height")); - } - } - else - { - if (Button(new GUIContent("Fullscreen", "Toggle Fullscreen"))) - { - GameSystem.Instance.GoFullscreen(); - } + MODWindowManager.FullscreenToggle(showToast: true); } screenHeightString = GUILayout.TextField(screenHeightString); @@ -51,44 +56,27 @@ public void OnGUI() { if (int.TryParse(screenHeightString, out int new_height)) { - if (new_height < 480) - { - MODToaster.Show("Height too small - must be at least 480 pixels"); - new_height = 480; - } - else if (new_height > 15360) - { - MODToaster.Show("Height too big - must be less than 15360 pixels"); - new_height = 15360; - } - screenHeightString = $"{new_height}"; - int new_width = Mathf.RoundToInt(new_height * 16f / 9f); - Screen.SetResolution(new_width, new_height, Screen.fullScreen); - PlayerPrefs.SetInt("width", new_width); - PlayerPrefs.SetInt("height", new_height); + SetAndSaveResolution(new_height); } } GUILayout.EndHorizontal(); + + if (Button(new GUIContent( + "Reset Fullscreen Resolution", + "Force re-detection of the fullscreen resolution (matching your monitor's max resolution).\n\n" + + "You may want to do this if you change to a monitor with a different fullscreen resolution, and the game detects the wrong resolution.") + )) + { + MODWindowManager.ClearPlayerPrefsFullscreenResolution(); + MODWindowManager.GoFullscreen(); + } } } private void SetAndSaveResolution(int height) { - if (height < 480) - { - MODToaster.Show("Height too small - must be at least 480 pixels"); - height = 480; - } - else if (height > 15360) - { - MODToaster.Show("Height too big - must be less than 15360 pixels"); - height = 15360; - } screenHeightString = $"{height}"; - int width = Mathf.RoundToInt(height * 16f / 9f); - Screen.SetResolution(width, height, Screen.fullScreen); - PlayerPrefs.SetInt("width", width); - PlayerPrefs.SetInt("height", height); + MODWindowManager.SetResolution(height, showToast: true); } } } diff --git a/MOD.Scripts.UI/MODMenuSupport.cs b/MOD.Scripts.UI/MODMenuSupport.cs index c0a94159..1ad4c2d8 100644 --- a/MOD.Scripts.UI/MODMenuSupport.cs +++ b/MOD.Scripts.UI/MODMenuSupport.cs @@ -99,6 +99,31 @@ public static void ShowSupportButtons(Func buttonRenderer) } GUILayout.EndHorizontal(); + + if(Application.platform == RuntimePlatform.WindowsPlayer) + { + string playerPrefsPath = $"Computer\\HKEY_CURRENT_USER\\SOFTWARE\\{Application.companyName}\\{Application.productName}"; + if (buttonRenderer(new GUIContent( + "Copy PlayerPrefs Registry Path", + $"Click to copy the playerprefs registry folder to clipboard. This registry folder mainly contains Unity resolution settings.\n\n" + + $"Paste the path into the Windows Registry Editor (regedit) to view playerprefs.\n\n" + + $"Registry Path: {playerPrefsPath}" + ))) + { + GUIUtility.systemCopyBuffer = playerPrefsPath; + } + } + else + { + string playerPrefsPath = Application.platform == RuntimePlatform.LinuxPlayer ? + $"~/.config/unity3d/{Application.companyName}/{Application.productName}" : + "~/Library/Preferences"; + + if (buttonRenderer(new GUIContent("Show PlayerPrefs Folder", $"Click to show the folder containing the playerprefs config file. This config file mainly contains Unity resolution settings.\n\nPath: {playerPrefsPath}"))) + { + MODActions.ShowFile(playerPrefsPath); + } + } } MODMenuCommon.Label("Support Pages"); diff --git a/MOD.Scripts.UI/MODSubMenuWindowSetup.cs b/MOD.Scripts.UI/MODSubMenuWindowSetup.cs new file mode 100644 index 00000000..57818744 --- /dev/null +++ b/MOD.Scripts.UI/MODSubMenuWindowSetup.cs @@ -0,0 +1,115 @@ +using MOD.Scripts.Core; +using System; +using System.Collections.Generic; +using System.Text; +using UnityEngine; +using static MOD.Scripts.UI.MODMenuCommon; + +namespace MOD.Scripts.UI +{ + class MODSubMenuWindowSetup : MODMenuModuleInterface + { + MODMenu modMenu; + MODMenuNormal normalMenu; + MODSimpleTimer setWindowAgainDelay; + + public MODSubMenuWindowSetup(MODMenu modMenu, MODMenuNormal normalMenu) + { + this.modMenu = modMenu; + this.normalMenu = normalMenu; + this.setWindowAgainDelay = new MODSimpleTimer(); + } + + public void OnBeforeMenuVisible() + { + + } + + public void Update() + { + setWindowAgainDelay.Update(); + } + + public void OnGUI() + { + // Fix a bug where windowed resolution becomes 640x480 when set the first time game starts (if playerprefs doesn't exist). + // Resolution is set correctly if set again after a short delay. + // Bug was first noticed on Ep1 Native Ubuntu 22.04. + if(setWindowAgainDelay.Finished()) + { + MODWindowManager.SetResolution(maybe_width: null, maybe_height: null, maybe_fullscreen: false, showToast: true); + setWindowAgainDelay.Cancel(); + } + + HeadingLabel("Linux Resolution/Windowed Mode Setup"); + + GUILayout.Space(20); + + Label("Some Native Linux users experience crashes or softlocks when entering windowed mode, or when moving the window around (the 'Gnome Crash Bug').\n\n" + + "Please click the button below to enter windowed mode, then drag the window around."); + + GUILayout.Space(20); + + Label("NOTE: This may crash your entire desktop environment! Please save your work before this test!"); + + GUILayout.Space(20); + + if ( + Button( + new GUIContent( + MODWindowManager.IsFullscreen ? "Click here to test Windowed Mode (may flicker)" : "Now try dragging the window around!", + "This button will switch the game to Windowed mode.\n\n" + + "Please try moving the window around to see if the game crashes\n\n" + + "If the game freezes, you may need to force close it and open the game again." + ), + selected: !MODWindowManager.IsFullscreen + ) + ) + { + MODWindowManager.SetResolution(maybe_width: null, maybe_height: null, maybe_fullscreen: false, showToast: true); + setWindowAgainDelay.Start(1); + } + + GUILayout.Space(20); + + Label("If your game crashed in windowed mode, choose 'Force Fullscreen Always' to avoid future crashes."); + + GUILayout.Space(20); + + Label("If your game runs fine even when the window is dragged, choose 'No Lock' to run the game normally."); + + GUILayout.Space(20); + + normalMenu.OnGUIFullscreenLock(); + + GUILayout.Space(20); + + if (MODWindowManager.FullscreenLockConfigured()) + { + if(Button(new GUIContent("Click here when you're finished."))) + { + modMenu.PopSubMenu(); + } + } + else + { + Label("You must choose an option to continue."); + } + } + + public bool UserCanClose() => false; + public string Heading() + { + if (setWindowAgainDelay.Running()) + { + return $"Please Wait ({setWindowAgainDelay.timeLeft:F1})"; + } + else + { + return "Linux Resolution/Windowed Mode Setup Menu"; + } + } + + public string DefaultTooltip() => "Please choose the options on the left before continuing. You can hover over a button to view its description."; + } +}