diff --git a/Game.UI/BasePanel.cs b/Game.UI/BasePanel.cs index 5bd4fe29c..c7a39eed9 100644 --- a/Game.UI/BasePanel.cs +++ b/Game.UI/BasePanel.cs @@ -495,16 +495,77 @@ public override void ApplySchemeSettings(IScheme scheme) { SetTextInset(0, 0); } } +public class SaveBeforeQuitQueryDialog : Frame +{ + public SaveBeforeQuitQueryDialog(Panel parent, ReadOnlySpan name) : base(parent, name) { + LoadControlSettings("resource/SaveBeforeQuitDialog.res"); + SetDeleteSelfOnClose(true); + SetSizeable(false); + } + + public override void DoModal() { + base.Activate(); + Input.SetAppModalSurface(this); + MoveToCenterOfScreen(); + Surface.RestrictPaintToSinglePanel(this); + + gameUI.PreventEngineHideGameUI(); + } + + public override void OnKeyCodeTyped(ButtonCode code) { + if (code == ButtonCode.KeyEscape) + Close(); + else + base.OnKeyCodeTyped(code); + } + + public override void OnKeyCodePressed(ButtonCode code) { + switch (code) { + case ButtonCode.KeyXButtonB: Close(); break; + case ButtonCode.KeyXButtonA: OnCommand("SaveAndQuit"); break; + case ButtonCode.KeyXButtonX: OnCommand("Quit"); break; + default: base.OnKeyCodePressed(code); break; + } + } + + public override void OnCommand(ReadOnlySpan command) { + if (0 == stricmp(command, "Quit")) + PostMessage(GetParent(), new KeyValues("Command", "command", "QuitNoConfirm")); + else if (0 == stricmp(command, "SaveAndQuit")) { + // find a new name to save + Span saveName= stackalloc char[128]; + // TODO: SaveGameDialog.FindSaveSlot(saveName, sizeof(saveName)); + // TODO: if (saveName[0] != '\0') { + // TODO: // save the game + // TODO: Span sz = stackalloc char[256]; + // TODO: sprintf(sz, "save %s\n").S(saveName); + // TODO: engine.ClientCmd_Unrestricted(sz.SliceNullTerminatedString()); + // TODO: } + + // quit + PostMessage(GetParent(), new KeyValues("Command", "command", "QuitNoConfirm")); + } + else if (0 == stricmp(command, "Cancel")) + Close(); + else + base.OnCommand(command); + } + + public override void OnClose() { + base.OnClose(); + Surface.RestrictPaintToSinglePanel(null); + gameUI.AllowEngineHideGameUI(); + } +} + public class QuitQueryBox : QueryBox { public QuitQueryBox(ReadOnlySpan title, ReadOnlySpan queryText, Panel? parent = null) : base(title, queryText, parent) { } - IGameUI GameUI = Singleton(); - public override void DoModal(Frame? frameOver) { base.DoModal(frameOver); Surface.RestrictPaintToSinglePanel(this); - GameUI.PreventEngineHideGameUI(); + gameUI.PreventEngineHideGameUI(); } public override void OnKeyCodeTyped(ButtonCode code) { if (code == ButtonCode.KeyEscape) @@ -515,7 +576,7 @@ public override void OnKeyCodeTyped(ButtonCode code) { public override void OnClose() { base.OnClose(); Surface.RestrictPaintToSinglePanel(null); - GameUI.AllowEngineHideGameUI(); + gameUI.AllowEngineHideGameUI(); } } public class BasePanel : Panel @@ -523,7 +584,6 @@ public class BasePanel : Panel GameMenu? GameMenu; readonly public IFileSystem FileSystem = Singleton(); - readonly public GameUI GameUI; readonly public IEngineClient engine = Singleton(); readonly public ModInfo ModInfo = Singleton(); @@ -587,7 +647,7 @@ private void RunMenuCommand(ReadOnlySpan command) { engine.ClientCmd_Unrestricted("quit\n"); break; case "QuitRestartNoConfirm": break; - case "ResumeGame": GameUI.HideGameUI(); break; + case "ResumeGame": gameUI.HideGameUI(); break; case "Disconnect": engine.ClientCmd_Unrestricted("disconnect"); break; case "DisconnectNoConfirm": break; case "ReleaseModalWindow": Surface.RestrictPaintToSinglePanel(null); break; @@ -635,12 +695,13 @@ public void PositionDialog(Panel? dialog) { } public void OnOpenQuitConfirmationDialog() { - if (GameUI.IsConsoleUI()) { + if (gameUI.IsConsoleUI()) { throw new NotImplementedException(); } - if (GameUI.IsInLevel() && engine.GetMaxClients() == 1) { - throw new NotImplementedException(); + if (gameUI.IsInLevel() && engine.GetMaxClients() == 1) { + SaveBeforeQuitQueryDialog box = new SaveBeforeQuitQueryDialog(this, "SaveBeforeQuitQueryDialog"); + box.DoModal(); } else { QueryBox box = new QuitQueryBox("#GameUI_QuitConfirmationTitle", "#GameUI_QuitConfirmationText", this); @@ -660,8 +721,7 @@ static BackgroundMenuButton CreateMenuButton(BasePanel parent, ReadOnlySpan 0) + if (!gameUI.IsInLevel() || GameUI.LoadingDialog != null || ExitingFrameCount > 0) DrawBackgroundImage(); if (BackgroundFillAlpha > 0) { @@ -753,9 +813,9 @@ public void SetBackgroundRenderState(BackgroundState state) { public void UpdateBackgroundState() { if (ExitingFrameCount != 0) SetBackgroundRenderState(BackgroundState.Exiting); - else if (GameUI.IsInLevel()) + else if (gameUI.IsInLevel()) SetBackgroundRenderState(BackgroundState.Level); - else if (GameUI.IsInBackgroundLevel() && !LevelLoading) + else if (gameUI.IsInBackgroundLevel() && !LevelLoading) SetBackgroundRenderState(BackgroundState.MainMenu); else if (LevelLoading) SetBackgroundRenderState(BackgroundState.Loading); @@ -767,7 +827,7 @@ public void UpdateBackgroundState() { int i; bool haveActiveDialogs = false; - bool isInLevel = GameUI.IsInLevel(); + bool isInLevel = gameUI.IsInLevel(); for (i = 0; i < GetChildCount(); ++i) { Panel? child = GetChild(i); if (child != null && child.IsVisible() && child.IsPopup() && child != GameMenu) @@ -1030,7 +1090,7 @@ internal void OnGameUIActivated() { int LastGameMenuState = -1; private void UpdateGameMenus() { - bool isInGame = GameUI.IsInLevel(); + bool isInGame = gameUI.IsInLevel(); bool isMulti = isInGame && engine.GetMaxClients() > 1; int state = (isInGame ? 1 : 0) | (isMulti ? 2 : 0); diff --git a/Game.UI/GameUI.cs b/Game.UI/GameUI.cs index c1c704e95..6dca43f83 100644 --- a/Game.UI/GameUI.cs +++ b/Game.UI/GameUI.cs @@ -113,7 +113,7 @@ public void Initialize(IEngineAPI engineAPI) { // I have no idea why this one defines needed resource strings... TODO: Is this still needed? localize.AddFile("Resource/itemtest_%language%.txt", "GAME", true); - staticPanel = new BasePanel(this); + staticPanel = new BasePanel(); staticPanel.SetBounds(0, 0, 400, 300); staticPanel.SetPaintBorderEnabled(false); staticPanel.SetPaintBackgroundEnabled(true); @@ -232,7 +232,7 @@ private bool SetProgressBarStatusText(ReadOnlySpan statusText) { return true; } - public LoadingDialog? LoadingDialog; + public static LoadingDialog? LoadingDialog; string? PreviousStatusText; public void StartProgressBar() { diff --git a/Game.UI/LoadingDialog.cs b/Game.UI/LoadingDialog.cs index dc106fe10..597d73c44 100644 --- a/Game.UI/LoadingDialog.cs +++ b/Game.UI/LoadingDialog.cs @@ -10,9 +10,7 @@ namespace Game.UI; public class LoadingDialog : Frame { - readonly public IGameUI GameUI = Singleton(); readonly public ModInfo ModInfo = Singleton(); - readonly IEngineClient engine = Singleton(); ProgressBar Progress; ProgressBar Progress2; @@ -84,7 +82,7 @@ public override void OnCommand(ReadOnlySpan command) { public LoadingDialog(Panel? parent) : base(parent, "LoadingDialog") { SetDeleteSelfOnClose(true); - ConsoleStyle = GameUI.IsConsoleUI(); + ConsoleStyle = gameUI.IsConsoleUI(); if (!ConsoleStyle) { SetSize(416, 100); @@ -94,7 +92,7 @@ public LoadingDialog(Panel? parent) : base(parent, "LoadingDialog") { #if GMOD_DLL Center = false; #else - Center = !GameUI.HasLoadingBackgroundDialog(); + Center = !gameUI.HasLoadingBackgroundDialog(); #endif ShowingSecondaryProgress = false; @@ -146,7 +144,7 @@ public LoadingDialog(Panel? parent) : base(parent, "LoadingDialog") { private void SetupControlSettings(bool forceShowProgressText) { ShowingVACInfo = false; - if (GameUI.IsConsoleUI()) { + if (gameUI.IsConsoleUI()) { // KeyValues controlSettings = BasePanel.GetConsoleControlSettings().FindKey("LoadingDialogNoBanner.res"); // LoadControlSettings("null", null, controlSettings); // return; @@ -250,8 +248,8 @@ internal void Open() { private void HideOtherDialogs(bool hide) { if (hide) { - if (GameUI.HasLoadingBackgroundDialog()) { - GameUI.ShowLoadingBackgroundDialog(); + if (gameUI.HasLoadingBackgroundDialog()) { + gameUI.ShowLoadingBackgroundDialog(); MoveToFront(); Input.SetAppModalSurface(this); } @@ -259,8 +257,8 @@ private void HideOtherDialogs(bool hide) { Surface.RestrictPaintToSinglePanel(this); } else { - if (GameUI.HasLoadingBackgroundDialog()) { - GameUI.HideLoadingBackgroundDialog(); + if (gameUI.HasLoadingBackgroundDialog()) { + gameUI.HideLoadingBackgroundDialog(); Input.SetAppModalSurface(null); } else diff --git a/Game.UI/OptionsSubVideo.cs b/Game.UI/OptionsSubVideo.cs index 5dd06b023..057f14d32 100644 --- a/Game.UI/OptionsSubVideo.cs +++ b/Game.UI/OptionsSubVideo.cs @@ -45,9 +45,6 @@ class OptionsSubVideoAdvancedDlg : Frame int NumAAModes; AAMode[] AAModes = new AAMode[16]; - ICvar cvar = Singleton(); - IGameUI GameUI => Singleton(); - public OptionsSubVideoAdvancedDlg(Panel? parent) : base(parent, "OptionsSubVideoAdvancedDlg") { SetTitle("#GameUI_VideoAdvanced_Title", true); SetSize(260, 400); @@ -535,11 +532,11 @@ public override void OnKeyCodeTyped(ButtonCode code) { } public bool RequiresRestart() { - if (GameUI.IsInLevel()) { - if (GameUI.IsInBackgroundLevel()) + if (gameUI.IsInLevel()) { + if (gameUI.IsInBackgroundLevel()) return false; - // if (!GameUI.IsInMultiplayer()) //todo + // if (!gameUI.IsInMultiplayer()) //todo // return false; if (HDR.IsEnabled()) { diff --git a/Game.UI/SourceDllMain.cs b/Game.UI/SourceDllMain.cs index 9bd7aeec6..7ebbb3b8f 100644 --- a/Game.UI/SourceDllMain.cs +++ b/Game.UI/SourceDllMain.cs @@ -5,6 +5,7 @@ using Source; using Source.Common; using Source.Common.Client; +using Source.Common.Commands; using Source.Common.Filesystem; using Source.Common.GameUI; using Source.Common.GUI; @@ -12,6 +13,10 @@ using Source.Common.Launcher; using Source.Common.MaterialSystem; +using System.Runtime.CompilerServices; + +using DependencyAttribute = Source.DependencyAttribute; + namespace Game.UI; [EngineComponent] @@ -25,8 +30,12 @@ public static class SourceDllMain [Dependency] public static ISurface Surface { get; private set; } = null!; [Dependency] public static IVGuiInput Input { get; private set; } = null!; [Dependency] public static ILocalize Localize { get; private set; } = null!; + [Dependency] public static IGameUI gameUI { get; private set; } = null!; [Dependency] public static IMaterialSystem Materials { get; private set; } = null!; [Dependency] public static ISystem system { get; private set; } = null!; + [Dependency] public static ICvar cvar { get; private set; } = null!; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static IGameUI GameUI() => gameUI; public static void Link(IServiceCollection services) { services.AddSingleton(); diff --git a/Source.Engine/Client/ClientState.cs b/Source.Engine/Client/ClientState.cs index e6e8c260a..482ca8d79 100644 --- a/Source.Engine/Client/ClientState.cs +++ b/Source.Engine/Client/ClientState.cs @@ -531,9 +531,13 @@ public override bool SetSignonState(SignOnState state, int count) { NetChannel.SetTimeout(Source.Common.Networking.NetChannel.SIGNON_TIME_OUT); NetChannel.SetMaxBufferSize(true, Protocol.MAX_PAYLOAD); +#if GMOD_DLL + Host.GMOD_BuildConVarUpdateMessage(NetChannel, FCvar.UserInfo, false); +#else var convars = new NET_SetConVar(); Host.BuildConVarUpdateMessage(convars, FCvar.UserInfo, false); NetChannel.SendNetMsg(convars); +#endif break; case SignOnState.New: EngineVGui?.UpdateProgressBar(LevelLoadingProgress.SignOnNew); diff --git a/Source.Engine/Host.cs b/Source.Engine/Host.cs index 8f47c5304..2c6d421fe 100644 --- a/Source.Engine/Host.cs +++ b/Source.Engine/Host.cs @@ -2,6 +2,7 @@ using Source.Common; using Source.Common.Audio; +using Source.Common.Bitbuffers; using Source.Common.Client; using Source.Common.Commands; using Source.Common.Engine; @@ -1089,8 +1090,13 @@ public void BuildConVarUpdateMessage(NET_SetConVar convars, FCvar flags, bool no if (count <= 0) return; - if (count > 255) { - Sys.Error($"Engine only supported 255 ConVars marked {flags}\n"); +#if GMOD_DLL + const int MAX_CONVARS = 4096; +#else + const int MAX_CONVARS = 255; +#endif + if (count > MAX_CONVARS) { + Sys.Error($"Engine only supported {MAX_CONVARS} ConVars marked {flags}\n"); } foreach (var var in Cvar.GetCommands()) { @@ -1101,7 +1107,8 @@ public void BuildConVarUpdateMessage(NET_SetConVar convars, FCvar flags, bool no if (!convar.IsFlagSet(flags)) continue; - if (nonDefault && convar.GetDefault() != convar.GetString()) + // It's == to the default value, don't count + if (nonDefault && stricmp(convar.GetDefault(), convar.GetString()) == 0) continue; cvar_s acvar = new(); @@ -1111,6 +1118,80 @@ public void BuildConVarUpdateMessage(NET_SetConVar convars, FCvar flags, bool no } } +#if GMOD_DLL + public void GMOD_BuildConVarUpdateMessage(INetChannel chan, FCvar flags, bool nonDefault) { + int count = CountVariablesWithFlags(flags, nonDefault); + if (count <= 0) + return; + + NET_SetConVar convars = new(); + count = 0; + + foreach (var var in Cvar.GetCommands()) { + if (count > 32) { + chan.SendNetMsg(convars); + convars.ConVars.Clear(); + count = 0; + } + + if (var.IsCommand()) + continue; + + ConVar convar = (ConVar)var; + if (!convar.IsFlagSet(flags)) + continue; + + if (nonDefault && stricmp(convar.GetDefault(), convar.GetString()) == 0) + continue; + + cvar_s acvar = new(); + acvar.Name = convar.GetName(); + acvar.Value = CleanupConVarStringValue(convar.GetString()); + convars.ConVars.Add(acvar); + count++; + } + + if (count != 0) + chan.SendNetMsg(convars); + } + + public void GMOD_BuildConVarUpdateMessageBF(bf_write buf, FCvar flags, bool nonDefault) { + int count = CountVariablesWithFlags(flags, nonDefault); + if (count <= 0) + return; + + NET_SetConVar convars = new(); + count = 0; + + foreach (var var in Cvar.GetCommands()) { + if (count > 128) { + convars.WriteToBuffer(buf); + convars.ConVars.Clear(); + count = 0; + } + + if (var.IsCommand()) + continue; + + ConVar convar = (ConVar)var; + if (!convar.IsFlagSet(flags)) + continue; + + if (nonDefault && stricmp(convar.GetDefault(), convar.GetString()) == 0) + continue; + + cvar_s acvar = new(); + acvar.Name = convar.GetName(); + acvar.Value = CleanupConVarStringValue(convar.GetString()); + convars.ConVars.Add(acvar); + count++; + } + + if (count != 0) + convars.WriteToBuffer(buf); + } +#endif + [ConCommand(helpText: "Exits the engine")] void quit(in TokenizedCommand args) { #if !SWDS @@ -1193,7 +1274,8 @@ public int CountVariablesWithFlags(FCvar flags, bool nonDefault) { if (!convar.IsFlagSet(flags)) continue; - if (nonDefault && convar.GetDefault() != convar.GetString()) + // It's == to the default value, don't count + if (nonDefault && stricmp(convar.GetDefault(), convar.GetString()) == 0) continue; count++; diff --git a/Source.Engine/Server/BaseClient.cs b/Source.Engine/Server/BaseClient.cs index 7314aa276..80cf829e6 100644 --- a/Source.Engine/Server/BaseClient.cs +++ b/Source.Engine/Server/BaseClient.cs @@ -271,6 +271,66 @@ public bool IsNameChangeOnCooldown(bool showStatusMessage = false) { return false; } +#if GMOD_DLL + public static readonly ConVar sv_max_userinfo = new("sv_max_userinfo", "4096", 0, "The max amount of userinfo ConVars clients are allowed to send to the server. Setting this too high may result in loss of performance."); + public static readonly ConVar sv_max_userinfo_rate = new("sv_max_userinfo_rate", "1500", 0, "The max amount of userinfo ConVars clients are allowed to send to the server in 1 second.", 0, 60000); + + ushort UserInfoCount; + TimeUnit_t UserInfoResetTime; + + protected virtual bool ProcessSetConVar(NET_SetConVar msg) { + int unique = 0; + for (KeyValues? key = ConVars!.GetFirstSubKey(); key != null; key = key.GetNextKey()) + unique++; + + if (unique >= sv_max_userinfo.GetInt()) { + DevMsg($"Player {GetClientName()} has sent too many unique userinfo ConVars, ignoring.\n"); + return true; + } + + for (int i = 0; i < msg.ConVars.Count; i++) { + ReadOnlySpan name = msg.ConVars[i].Name; + ReadOnlySpan value = msg.ConVars[i].Value; + + bool funky = false; + for (ReadOnlySpan s = name; !s.IsStringEmpty; s = s[1..]) { + if (!char.IsAsciiLetterOrDigit(s[0]) && s[0] != '_') { + funky = true; + break; + } + } + if (funky) { + DevMsg($"Ignoring convar change request for variable '{name}' from client {GetClientName()}; invalid characters in the variable name\n"); + continue; + } + + if (stricmp(name, "name") == 0) { + ClientRequestNameChange(value); + continue; + } + + int count = UserInfoCount; + if (IsActive()) + UserInfoCount = (ushort)(count = UserInfoCount + 1); + + if (count > sv_max_userinfo_rate.GetInt() || UserInfoCount > 60000) { + Disconnect("Client exceeded sv_max_userinfo_rate"); + return false; + } + + if (Platform.Time > UserInfoResetTime) { + UserInfoCount = 0; + UserInfoResetTime = Platform.Time + 1.0; + } + + ConVars!.SetString(name, value); + } + + ConVarsChanged = true; + + return true; + } +#else static double s_dblLastWarned = 0.0; protected virtual bool ProcessSetConVar(NET_SetConVar msg) { for (int i = 0; i < msg.ConVars.Count; i++) { @@ -322,6 +382,7 @@ protected virtual bool ProcessSetConVar(NET_SetConVar msg) { return true; } +#endif protected virtual bool ProcessSignonState(NET_SignonState msg) { Common.TimestampedLog($"BaseClient.ProcessSignonState: {msg.SignOnState} (Current: {SignOnState})"); @@ -1203,10 +1264,14 @@ public bool SendServerInfo() { // Write replicated ConVars to non-listen server clients only if (!NetChannel.IsLoopback()) { +#if GMOD_DLL + Host.GMOD_BuildConVarUpdateMessageBF(msg, FCvar.Replicated, true); +#else NET_SetConVar convars = new(); Host.BuildConVarUpdateMessage(convars, FCvar.Replicated, true); convars.WriteToBuffer(msg); +#endif } NeedSendServerInfo = false; diff --git a/Source.Engine/Source.Engine.csproj b/Source.Engine/Source.Engine.csproj index d13eb5272..86a6fba4f 100644 --- a/Source.Engine/Source.Engine.csproj +++ b/Source.Engine/Source.Engine.csproj @@ -31,8 +31,14 @@ - - build$([System.DateTime]::UtcNow.ToString("yyyy-MM-ddTHH:mm:ss:fffZ")) - + $(MSBuildProjectDirectory)\$(BaseIntermediateOutputPath)$(Configuration)\BuildStamp.txt + + + + + + + +