diff --git a/ClientCore/ClientConfiguration.cs b/ClientCore/ClientConfiguration.cs index d80fb6a05..1bbc27258 100644 --- a/ClientCore/ClientConfiguration.cs +++ b/ClientCore/ClientConfiguration.cs @@ -16,6 +16,7 @@ public class ClientConfiguration private const string SETTINGS = "Settings"; private const string LINKS = "Links"; private const string TRANSLATIONS = "Translations"; + private const string USER_DEFAULTS = "UserDefaults"; private const string CLIENT_SETTINGS = "DTACnCNetClient.ini"; private const string GAME_OPTIONS = "GameOptions.ini"; @@ -193,7 +194,8 @@ public void RefreshSettings() public int MaximumRenderHeight => clientDefinitionsIni.GetIntValue(SETTINGS, "MaximumRenderHeight", 800); - public string[] RecommendedResolutions => clientDefinitionsIni.GetStringValue(SETTINGS, "RecommendedResolutions", "1280x720,2560x1440,3840x2160").Split(','); + public string[] RecommendedResolutions => clientDefinitionsIni.GetStringValue(SETTINGS, "RecommendedResolutions", + $"{MinimumRenderWidth}x{MinimumRenderHeight},{MaximumRenderWidth}x{MaximumRenderHeight}").Split(','); public string WindowTitle => clientDefinitionsIni.GetStringValue(SETTINGS, "WindowTitle", string.Empty) .L10N("INI:ClientDefinitions:WindowTitle"); @@ -405,6 +407,16 @@ public IEnumerable SupplementalMapFileExtensions #endregion + #region User default settings + + public bool UserDefault_BorderlessWindowedClient => clientDefinitionsIni.GetBooleanValue(USER_DEFAULTS, "BorderlessWindowedClient", true); + + public bool UserDefault_IntegerScaledClient => clientDefinitionsIni.GetBooleanValue(USER_DEFAULTS, "IntegerScaledClient", false); + + public bool UserDefault_WriteInstallationPathToRegistry => clientDefinitionsIni.GetBooleanValue(USER_DEFAULTS, "WriteInstallationPathToRegistry", true); + + #endregion + public List GetIRCServers() { List servers = []; @@ -419,7 +431,7 @@ public List GetIRCServers() } public bool DiscordIntegrationGloballyDisabled => string.IsNullOrWhiteSpace(DiscordAppId) || DisableDiscordIntegration; - + public OSVersion GetOperatingSystemVersion() { #if NETFRAMEWORK diff --git a/ClientCore/Settings/UserINISettings.cs b/ClientCore/Settings/UserINISettings.cs index 671c823fe..9a0829d6f 100644 --- a/ClientCore/Settings/UserINISettings.cs +++ b/ClientCore/Settings/UserINISettings.cs @@ -63,7 +63,8 @@ protected UserINISettings(IniFile iniFile) Renderer = new StringSetting(iniFile, COMPATIBILITY, "Renderer", string.Empty); WindowedMode = new BoolSetting(iniFile, VIDEO, WINDOWED_MODE_KEY, false); BorderlessWindowedMode = new BoolSetting(iniFile, VIDEO, "NoWindowFrame", false); - BorderlessWindowedClient = new BoolSetting(iniFile, VIDEO, "BorderlessWindowedClient", true); + BorderlessWindowedClient = new BoolSetting(iniFile, VIDEO, "BorderlessWindowedClient", ClientConfiguration.Instance.UserDefault_BorderlessWindowedClient); + IntegerScaledClient = new BoolSetting(iniFile, VIDEO, "IntegerScaledClient", ClientConfiguration.Instance.UserDefault_IntegerScaledClient); ClientFPS = new IntSetting(iniFile, VIDEO, "ClientFPS", 60); DisplayToggleableExtraTextures = new BoolSetting(iniFile, VIDEO, "DisplayToggleableExtraTextures", true); @@ -86,7 +87,7 @@ protected UserINISettings(IniFile iniFile) ChatColor = new IntSetting(iniFile, MULTIPLAYER, "ChatColor", -1); LANChatColor = new IntSetting(iniFile, MULTIPLAYER, "LANChatColor", -1); PingUnofficialCnCNetTunnels = new BoolSetting(iniFile, MULTIPLAYER, "PingCustomTunnels", true); - WritePathToRegistry = new BoolSetting(iniFile, OPTIONS, "WriteInstallationPathToRegistry", true); + WritePathToRegistry = new BoolSetting(iniFile, OPTIONS, "WriteInstallationPathToRegistry", ClientConfiguration.Instance.UserDefault_WriteInstallationPathToRegistry); PlaySoundOnGameHosted = new BoolSetting(iniFile, MULTIPLAYER, "PlaySoundOnGameHosted", true); SkipConnectDialog = new BoolSetting(iniFile, MULTIPLAYER, "SkipConnectDialog", false); PersistentMode = new BoolSetting(iniFile, MULTIPLAYER, "PersistentMode", false); @@ -151,6 +152,7 @@ protected UserINISettings(IniFile iniFile) public IntSetting ClientResolutionX { get; set; } public IntSetting ClientResolutionY { get; set; } public BoolSetting BorderlessWindowedClient { get; private set; } + public BoolSetting IntegerScaledClient { get; private set; } public IntSetting ClientFPS { get; private set; } public BoolSetting DisplayToggleableExtraTextures { get; private set; } diff --git a/DTAConfig/OptionPanels/DisplayOptionsPanel.cs b/DTAConfig/OptionPanels/DisplayOptionsPanel.cs index 252da14d2..d6347e5a9 100644 --- a/DTAConfig/OptionPanels/DisplayOptionsPanel.cs +++ b/DTAConfig/OptionPanels/DisplayOptionsPanel.cs @@ -42,6 +42,7 @@ public DisplayOptionsPanel(WindowManager windowManager, UserINISettings iniSetti private XNAClientCheckBox chkBackBufferInVRAM; private XNAClientPreferredItemDropDown ddClientResolution; private XNAClientCheckBox chkBorderlessClient; + private XNAClientCheckBox chkIntegerScaledClient; private XNAClientDropDown ddClientTheme; private XNAClientDropDown ddTranslation; @@ -86,7 +87,7 @@ public override void Initialize() var maximumIngameResolution = new ScreenResolution(ClientConfiguration.Instance.MaximumIngameWidth, ClientConfiguration.Instance.MaximumIngameHeight); #if XNA - if (!ScreenResolution.HiDefLimitResolution.Fit(maximumIngameResolution)) + if (!ScreenResolution.HiDefLimitResolution.Fits(maximumIngameResolution)) maximumIngameResolution = ScreenResolution.HiDefLimitResolution; #endif @@ -224,18 +225,36 @@ .. scaledRecommendedResolutions chkBorderlessClient.CheckedChanged += ChkBorderlessMenu_CheckedChanged; chkBorderlessClient.Checked = true; + chkIntegerScaledClient = new XNAClientCheckBox(WindowManager); + chkIntegerScaledClient.Name = nameof(chkIntegerScaledClient); + chkIntegerScaledClient.ClientRectangle = new Rectangle( + lblClientResolution.X, + lblRenderer.Y, 0, 0); + chkIntegerScaledClient.Text = "Integer Scaled Client".L10N("Client:DTAConfig:IntegerScaledClient"); + chkIntegerScaledClient.Checked = IniSettings.IntegerScaledClient.Value; + chkIntegerScaledClient.ToolTipText = + """ + Enable integer scaling for the client. This will cause the client to use + the closest fitting resolution that is required to maintain sharp graphics, + at the expense of black borders that may appear at some resolutions. + + Additionally, enabling this option will also allow the client window + to be resized (does not affect the selected client resolution). + """ + .L10N("Client:DTAConfig:IntegerScaledClientToolTip"); + var lblClientTheme = new XNALabel(WindowManager); lblClientTheme.Name = "lblClientTheme"; lblClientTheme.ClientRectangle = new Rectangle( lblClientResolution.X, - lblRenderer.Y, 0, 0); + chkWindowedMode.Y, 0, 0); lblClientTheme.Text = "Client Theme:".L10N("Client:DTAConfig:ClientTheme"); ddClientTheme = new XNAClientDropDown(WindowManager); ddClientTheme.Name = "ddClientTheme"; ddClientTheme.ClientRectangle = new Rectangle( ddClientResolution.X, - ddRenderer.Y, + chkWindowedMode.Y, ddClientResolution.Width, ddRenderer.Height); @@ -326,6 +345,7 @@ .. scaledRecommendedResolutions AddChild(chkBorderlessWindowedMode); AddChild(chkBackBufferInVRAM); AddChild(chkBorderlessClient); + AddChild(chkIntegerScaledClient); AddChild(lblClientTheme); AddChild(ddClientTheme); AddChild(lblTranslation); @@ -769,6 +789,8 @@ public override bool Save() clientRes.Height != IniSettings.ClientResolutionY.Value) restartRequired = true; + // TODO: since DTAConfig must not rely on DXMainClient, we can't notify the client to dynamically change the resolution or togging borderless windowed mode. Thus, we need to restart the client as a workaround. + (IniSettings.ClientResolutionX.Value, IniSettings.ClientResolutionY.Value) = clientRes; if (IniSettings.BorderlessWindowedClient.Value != chkBorderlessClient.Checked) @@ -776,6 +798,11 @@ public override bool Save() IniSettings.BorderlessWindowedClient.Value = chkBorderlessClient.Checked; + if (IniSettings.IntegerScaledClient.Value != chkIntegerScaledClient.Checked) + restartRequired = true; + + IniSettings.IntegerScaledClient.Value = chkIntegerScaledClient.Checked; + restartRequired = restartRequired || IniSettings.ClientTheme != (string)ddClientTheme.SelectedItem.Tag; IniSettings.ClientTheme.Value = (string)ddClientTheme.SelectedItem.Tag; diff --git a/DTAConfig/ScreenResolution.cs b/DTAConfig/ScreenResolution.cs index 97c01c559..1b13ef38c 100644 --- a/DTAConfig/ScreenResolution.cs +++ b/DTAConfig/ScreenResolution.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; +using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; namespace DTAConfig @@ -28,6 +29,12 @@ public ScreenResolution(int width, int height) Height = height; } + public ScreenResolution(Rectangle rectangle) + { + Width = rectangle.Width; + Height = rectangle.Height; + } + public ScreenResolution(string resolution) { List resolutionList = resolution.Trim().Split('x').Take(2).Select(int.Parse).ToList(); @@ -51,12 +58,16 @@ public void Deconstruct(out int width, out int height) public static implicit operator (int Width, int Height)(ScreenResolution resolution) => new(resolution.Width, resolution.Height); - public bool Fit(ScreenResolution child) => this.Width >= child.Width && this.Height >= child.Height; + public bool Fits(ScreenResolution child) => this.Width >= child.Width && this.Height >= child.Height; public int CompareTo(ScreenResolution other) => (this.Width, this.Height).CompareTo(other); // Accessing GraphicsAdapter.DefaultAdapter requiring DXMainClient.GameClass has been constructed. Lazy loading prevents possible null reference issues for now. private static ScreenResolution _desktopResolution = null; + + /// + /// The resolution of primary monitor. + /// public static ScreenResolution DesktopResolution => _desktopResolution ??= new(GraphicsAdapter.DefaultAdapter.CurrentDisplayMode.Width, GraphicsAdapter.DefaultAdapter.CurrentDisplayMode.Height); @@ -64,12 +75,16 @@ public void Deconstruct(out int width, out int height) public static ScreenResolution HiDefLimitResolution { get; } = "3840x3840"; private static ScreenResolution _safeMaximumResolution = null; + + /// + /// The resolution of primary monitor, or the maximum resolution supported by the graphic profile, whichever is smaller. + /// public static ScreenResolution SafeMaximumResolution { get { #if XNA - return _safeMaximumResolution ??= HiDefLimitResolution.Fit(DesktopResolution) ? DesktopResolution : HiDefLimitResolution; + return _safeMaximumResolution ??= HiDefLimitResolution.Fits(DesktopResolution) ? DesktopResolution : HiDefLimitResolution; #else return _safeMaximumResolution ??= DesktopResolution; #endif @@ -77,6 +92,10 @@ public static ScreenResolution SafeMaximumResolution } private static ScreenResolution _safeFullScreenResolution = null; + + /// + /// The maximum resolution supported by the graphic profile, or the largest full screen resolution supported by the primary monitor, whichever is smaller. + /// public static ScreenResolution SafeFullScreenResolution => _safeFullScreenResolution ??= GetFullScreenResolutions(minWidth: 800, minHeight: 600).Max(); public static SortedSet GetFullScreenResolutions(int minWidth, int minHeight) => @@ -123,7 +142,7 @@ public SortedSet GetIntegerScaledResolutions(ScreenResolution { ScreenResolution scaledResolution = (this.Width * i, this.Height * i); - if (maxResolution.Fit(scaledResolution)) + if (maxResolution.Fits(scaledResolution)) resolutions.Add(scaledResolution); else break; @@ -149,6 +168,9 @@ public static SortedSet GetWindowedResolutions(IEnumerable + //{ + // ScreenResolution currentWindowSize = new(wm.Game.Window.ClientBounds); + + // if (currentWindowSize != lastWindowSizeCaptured) + // { + // Logger.Log($"Window size changed from {lastWindowSizeCaptured} to {currentWindowSize}."); + // lastWindowSizeCaptured = currentWindowSize; + // SetGraphicsMode(wm, currentWindowSize.Width, currentWindowSize.Height, centerOnScreen: false); + // } + //}; + } #endif wm.Cursor.Textures = new Texture2D[] @@ -311,14 +338,32 @@ private void InitializeUISettings() /// TODO move to some helper class? /// /// The window manager - public static void SetGraphicsMode(WindowManager wm) + /// Whether to center the client window on the screen + public static void SetGraphicsMode(WindowManager wm, bool centerOnScreen = true) { - var clientConfiguration = ClientConfiguration.Instance; - int windowWidth = UserINISettings.Instance.ClientResolutionX; int windowHeight = UserINISettings.Instance.ClientResolutionY; + SetGraphicsMode(wm, windowWidth, windowHeight, centerOnScreen); + } + + /// + /// The viewport width + /// The viewport height + public static void SetGraphicsMode(WindowManager wm, int windowWidth, int windowHeight, bool centerOnScreen = true) + { bool borderlessWindowedClient = UserINISettings.Instance.BorderlessWindowedClient; + bool integerScale = UserINISettings.Instance.IntegerScaledClient; + + SetGraphicsMode(wm, windowWidth, windowHeight, borderlessWindowedClient, integerScale, centerOnScreen); + } + + /// + /// Whether to use borderless windowed mode + /// Whether to use integer scaling + public static void SetGraphicsMode(WindowManager wm, int windowWidth, int windowHeight, bool borderlessWindowedClient, bool integerScale, bool centerOnScreen = true) + { + var clientConfiguration = ClientConfiguration.Instance; (int desktopWidth, int desktopHeight) = ScreenResolution.SafeMaximumResolution; @@ -338,53 +383,95 @@ public static void SetGraphicsMode(WindowManager wm) int renderResolutionX = 0; int renderResolutionY = 0; - int initialXRes = Math.Max(windowWidth, clientConfiguration.MinimumRenderWidth); - initialXRes = Math.Min(initialXRes, clientConfiguration.MaximumRenderWidth); + if (!integerScale || windowWidth < clientConfiguration.MinimumRenderWidth || windowHeight < clientConfiguration.MinimumRenderHeight) + { + int initialXRes = Math.Max(windowWidth, clientConfiguration.MinimumRenderWidth); + initialXRes = Math.Min(initialXRes, clientConfiguration.MaximumRenderWidth); - int initialYRes = Math.Max(windowHeight, clientConfiguration.MinimumRenderHeight); - initialYRes = Math.Min(initialYRes, clientConfiguration.MaximumRenderHeight); + int initialYRes = Math.Max(windowHeight, clientConfiguration.MinimumRenderHeight); + initialYRes = Math.Min(initialYRes, clientConfiguration.MaximumRenderHeight); - double xRatio = (windowWidth) / (double)initialXRes; - double yRatio = (windowHeight) / (double)initialYRes; + double xRatio = (windowWidth) / (double)initialXRes; + double yRatio = (windowHeight) / (double)initialYRes; - double ratio = xRatio > yRatio ? yRatio : xRatio; + double ratio = xRatio > yRatio ? yRatio : xRatio; - if ((windowWidth == 1366 || windowWidth == 1360) && windowHeight == 768) - { - renderResolutionX = windowWidth; - renderResolutionY = windowHeight; - } + // Special rule for 1360x768 and 1366x768 + if ((windowWidth == 1366 || windowWidth == 1360) && windowHeight == 768) + { + // Most client interface has been designed for 1280x720 or 1280x800. + // 1280x720 upscaled to 1366x768 doesn't look great, so we allow players with 1366x768 to use their native resolution with small black bars on the sides + // This behavior is enforced even if IntegerScaledClient is turned off. + renderResolutionX = windowWidth; + renderResolutionY = windowHeight; + } - if (ratio > 1.0) - { - // Check whether we could sharp-scale our client window - for (int i = 2; i <= ScreenResolution.MAX_INT_SCALE; i++) + // Special rule: if 1280x720 is a valid render resolution, we allow 1.5x scaling for 1920x1080. + if (windowWidth == 1920 && windowHeight == 1080 + && 1280 >= clientConfiguration.MinimumRenderWidth && 1280 <= clientConfiguration.MaximumRenderWidth + && 720 >= clientConfiguration.MinimumRenderHeight && 720 <= clientConfiguration.MaximumRenderHeight) { - int sharpScaleRenderResX = windowWidth / i; - int sharpScaleRenderResY = windowHeight / i; + renderResolutionX = 1280; + renderResolutionY = 720; + } - if (sharpScaleRenderResX >= clientConfiguration.MinimumRenderWidth && - sharpScaleRenderResX <= clientConfiguration.MaximumRenderWidth && - sharpScaleRenderResY >= clientConfiguration.MinimumRenderHeight && - sharpScaleRenderResY <= clientConfiguration.MaximumRenderHeight) + // Special rule: if 1280x800 is a valid render resolution, we allow 1.5x scaling for 1920x1200. + if (windowWidth == 1920 && windowHeight == 1200 + && 1280 >= clientConfiguration.MinimumRenderWidth && 1280 <= clientConfiguration.MaximumRenderWidth + && 800 >= clientConfiguration.MinimumRenderHeight && 800 <= clientConfiguration.MaximumRenderHeight) + { + renderResolutionX = 1280; + renderResolutionY = 800; + } + + // Check whether we could integer-scale our client window + if (ratio > 1.0) + { + for (int i = 2; i <= ScreenResolution.MAX_INT_SCALE; i++) { - renderResolutionX = sharpScaleRenderResX; - renderResolutionY = sharpScaleRenderResY; - break; + int sharpScaleRenderResX = windowWidth / i; + int sharpScaleRenderResY = windowHeight / i; + + if (sharpScaleRenderResX >= clientConfiguration.MinimumRenderWidth && + sharpScaleRenderResX <= clientConfiguration.MaximumRenderWidth && + sharpScaleRenderResY >= clientConfiguration.MinimumRenderHeight && + sharpScaleRenderResY <= clientConfiguration.MaximumRenderHeight) + { + renderResolutionX = sharpScaleRenderResX; + renderResolutionY = sharpScaleRenderResY; + break; + } } } - } - if (renderResolutionX == 0 || renderResolutionY == 0) - { - renderResolutionX = initialXRes; - renderResolutionY = initialYRes; + // No special rules are triggered. Just zoom the client to the window size with minimal black bars. + if (renderResolutionX == 0 || renderResolutionY == 0) + { + renderResolutionX = initialXRes; + renderResolutionY = initialYRes; - if (ratio == xRatio) - renderResolutionY = (int)(windowHeight / ratio); + if (ratio == xRatio) + renderResolutionY = (int)(windowHeight / ratio); + } + } + else + { + // Compute integer scale ratio using minimum render resolution + // Note: this means we prefer larger scale ratio than render resolution. + // This policy works best when maximum and minimum render resolution are close. + int xScale = windowWidth / clientConfiguration.MinimumRenderWidth; + int yScale = windowHeight / clientConfiguration.MinimumRenderHeight; + int scale = Math.Min(xScale, yScale); + + // Compute render resolution + renderResolutionX = Math.Min(clientConfiguration.MaximumRenderWidth, + clientConfiguration.MinimumRenderWidth + (windowWidth - clientConfiguration.MinimumRenderWidth * scale) / scale); + renderResolutionY = Math.Min(clientConfiguration.MaximumRenderHeight, + clientConfiguration.MinimumRenderHeight + (windowHeight - clientConfiguration.MinimumRenderHeight * scale) / scale); } wm.SetBorderlessMode(borderlessWindowedClient); + #if !XNA if (borderlessWindowedClient) @@ -404,7 +491,11 @@ public static void SetGraphicsMode(WindowManager wm) } #endif - wm.CenterOnScreen(); + if (centerOnScreen) + wm.CenterOnScreen(); + + Logger.Log("Setting render resolution to " + renderResolutionX + "x" + renderResolutionY + ". Integer scaling: " + integerScale); + wm.IntegerScalingOnly = integerScale; wm.SetRenderResolution(renderResolutionX, renderResolutionY); } } diff --git a/DXMainClient/DXMainClient.csproj b/DXMainClient/DXMainClient.csproj index 3f00a7172..a66474c76 100644 --- a/DXMainClient/DXMainClient.csproj +++ b/DXMainClient/DXMainClient.csproj @@ -9,11 +9,13 @@ CnCNet Main Client Library DTAClient clienticon.ico - PerMonitorV2 + SystemAware + PerMonitorV2 clientdx clientogl clientxna - app.manifest + app.SystemAware.manifest + app.PerMonitorV2.manifest true diff --git a/DXMainClient/Program.cs b/DXMainClient/Program.cs index 2b2ccd668..2e9a5505f 100644 --- a/DXMainClient/Program.cs +++ b/DXMainClient/Program.cs @@ -70,7 +70,11 @@ static void InitializeApplicationConfiguration() #else #if NETCOREAPP3_0_OR_GREATER +#if GL + System.Windows.Forms.Application.SetHighDpiMode(System.Windows.Forms.HighDpiMode.SystemAware); +#else System.Windows.Forms.Application.SetHighDpiMode(System.Windows.Forms.HighDpiMode.PerMonitorV2); +#endif #endif System.Windows.Forms.Application.EnableVisualStyles(); diff --git a/DXMainClient/app.manifest b/DXMainClient/app.PerMonitorV2.manifest similarity index 100% rename from DXMainClient/app.manifest rename to DXMainClient/app.PerMonitorV2.manifest index b6bf8dcfb..150ac1989 100644 --- a/DXMainClient/app.manifest +++ b/DXMainClient/app.PerMonitorV2.manifest @@ -54,6 +54,7 @@ true/PM PerMonitorV2, PerMonitor + @@ -73,4 +74,3 @@ - diff --git a/DXMainClient/app.SystemAware.manifest b/DXMainClient/app.SystemAware.manifest new file mode 100644 index 000000000..227756f65 --- /dev/null +++ b/DXMainClient/app.SystemAware.manifest @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true + + + + + + + + + + + + + diff --git a/Directory.Packages.props b/Directory.Packages.props index 182e8f440..522e63ded 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,7 +1,7 @@ true - 2.3.20 + 2.3.22 8.0.0