diff --git a/libraries/Protobufs b/libraries/Protobufs index b46090af9..157162d97 160000 --- a/libraries/Protobufs +++ b/libraries/Protobufs @@ -1 +1 @@ -Subproject commit b46090af9cc219763c20eda3cc906675bca4157a +Subproject commit 157162d97b40f0757ab2e44c7afe8dbc272579b5 diff --git a/managed/CounterStrikeSharp.API/Core/API.cs b/managed/CounterStrikeSharp.API/Core/API.cs index 12fe56a27..94c794624 100644 --- a/managed/CounterStrikeSharp.API/Core/API.cs +++ b/managed/CounterStrikeSharp.API/Core/API.cs @@ -206,6 +206,18 @@ public static void SetFakeClientConvarValue(int clientindex, string convarname, } } + public static void ReplicateConvar(int clientslot, string convarname, string convarvalue){ + lock (ScriptContext.GlobalScriptContext.Lock) { + ScriptContext.GlobalScriptContext.Reset(); + ScriptContext.GlobalScriptContext.Push(clientslot); + ScriptContext.GlobalScriptContext.Push(convarname); + ScriptContext.GlobalScriptContext.Push(convarvalue); + ScriptContext.GlobalScriptContext.SetIdentifier(0xC8728BEC); + ScriptContext.GlobalScriptContext.Invoke(); + ScriptContext.GlobalScriptContext.CheckErrors(); + } + } + public static T DynamicHookGetReturn(IntPtr hook, int datatype){ lock (ScriptContext.GlobalScriptContext.Lock) { ScriptContext.GlobalScriptContext.Reset(); diff --git a/managed/CounterStrikeSharp.API/Core/CoreConfig.cs b/managed/CounterStrikeSharp.API/Core/CoreConfig.cs index 7771456bf..1bca56675 100644 --- a/managed/CounterStrikeSharp.API/Core/CoreConfig.cs +++ b/managed/CounterStrikeSharp.API/Core/CoreConfig.cs @@ -114,7 +114,6 @@ public partial class CoreConfig public static bool UnlockConCommands => _coreConfig.UnlockConCommands; public static bool UnlockConVars => _coreConfig.UnlockConVars; - } public partial class CoreConfig : IStartupService diff --git a/managed/CounterStrikeSharp.API/Core/Model/CCSPlayerController.cs b/managed/CounterStrikeSharp.API/Core/Model/CCSPlayerController.cs index 989c5bb44..d79747856 100644 --- a/managed/CounterStrikeSharp.API/Core/Model/CCSPlayerController.cs +++ b/managed/CounterStrikeSharp.API/Core/Model/CCSPlayerController.cs @@ -347,4 +347,9 @@ public VoiceFlags VoiceFlags { base.Teleport(position, angles, velocity); } + + public void ReplicateConVar(string conVar, string value) + { + NativeAPI.ReplicateConvar(Slot, conVar, value); + } } diff --git a/managed/CounterStrikeSharp.API/Modules/Extensions/PluginConfigExtensions.cs b/managed/CounterStrikeSharp.API/Modules/Extensions/PluginConfigExtensions.cs index cdd666912..c2c3662a9 100644 --- a/managed/CounterStrikeSharp.API/Modules/Extensions/PluginConfigExtensions.cs +++ b/managed/CounterStrikeSharp.API/Modules/Extensions/PluginConfigExtensions.cs @@ -7,7 +7,8 @@ public static class PluginConfigExtensions { private static readonly JsonSerializerOptions _jsonSerializerOptions = new() { - WriteIndented = true + WriteIndented = true, + ReadCommentHandling = JsonCommentHandling.Skip }; public static JsonSerializerOptions JsonSerializerOptions => _jsonSerializerOptions; @@ -62,7 +63,7 @@ public static class PluginConfigExtensions var configContent = File.ReadAllText(configPath); - var newConfig = JsonSerializer.Deserialize(configContent) + var newConfig = JsonSerializer.Deserialize(configContent, JsonSerializerOptions) ?? throw new JsonException($"Deserialization failed for configuration file '{configPath}'."); foreach (var property in typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public)) diff --git a/managed/CounterStrikeSharp.API/Modules/Menu/BaseMenu.cs b/managed/CounterStrikeSharp.API/Modules/Menu/BaseMenu.cs index fcadaa97a..ef4b4aa16 100644 --- a/managed/CounterStrikeSharp.API/Modules/Menu/BaseMenu.cs +++ b/managed/CounterStrikeSharp.API/Modules/Menu/BaseMenu.cs @@ -89,7 +89,7 @@ protected BaseMenuInstance(CCSPlayerController player, IMenu menu) } protected bool HasPrevButton => Page > 0; - protected bool HasNextButton => Menu.MenuOptions.Count > NumPerPage && CurrentOffset + NumPerPage < Menu.MenuOptions.Count; + protected virtual bool HasNextButton => Menu.MenuOptions.Count > NumPerPage && CurrentOffset + NumPerPage < Menu.MenuOptions.Count; protected bool HasExitButton => Menu.ExitButton; protected virtual int MenuItemsPerPage => NumPerPage; diff --git a/managed/CounterStrikeSharp.API/Modules/Menu/CenterHtmlMenu.cs b/managed/CounterStrikeSharp.API/Modules/Menu/CenterHtmlMenu.cs index fcf2390f9..22e1bde6c 100644 --- a/managed/CounterStrikeSharp.API/Modules/Menu/CenterHtmlMenu.cs +++ b/managed/CounterStrikeSharp.API/Modules/Menu/CenterHtmlMenu.cs @@ -28,15 +28,24 @@ public class CenterHtmlMenu : BaseMenu public string PrevPageColor { get; set; } = "yellow"; public string NextPageColor { get; set; } = "yellow"; public string CloseColor { get; set; } = "red"; + + public bool InlinePageOptions { get; set; } = true; + public int MaxTitleLength { get; set; } = 0; // defaults to 0 = no limit, if enabled, recommended value is 32 + public int MaxOptionLength { get; set; } = 0; // defaults to 0 = no limit, if enabled, recommended value is 26 - public CenterHtmlMenu(string title, BasePlugin plugin) : base(title) + public CenterHtmlMenu(string title, BasePlugin plugin, bool inlinePageOptions = true, int maxTitleLength = 0, int maxOptionLength = 0): base(title) { + Title = title.TruncateHtml(MaxTitleLength); _plugin = plugin; + InlinePageOptions = inlinePageOptions; + MaxTitleLength = maxTitleLength; + MaxOptionLength = maxOptionLength; } [Obsolete("Use the constructor that takes a BasePlugin")] public CenterHtmlMenu(string title) : base(title) { + Title = title.TruncateHtml(MaxTitleLength); } public override void Open(CCSPlayerController player) @@ -53,7 +62,7 @@ public override void Open(CCSPlayerController player) public override ChatMenuOption AddMenuOption(string display, Action onSelect, bool disabled = false) { - var option = new ChatMenuOption(display, disabled, onSelect); + var option = new ChatMenuOption(display.TruncateHtml(MaxOptionLength), disabled, onSelect); MenuOptions.Add(option); return option; } @@ -63,13 +72,40 @@ public class CenterHtmlMenuInstance : BaseMenuInstance { private readonly BasePlugin _plugin; public override int NumPerPage => 5; // one less than the actual number of items per page to avoid truncated options - protected override int MenuItemsPerPage => (Menu.ExitButton ? 0 : 1) + ((HasPrevButton && HasNextButton) ? NumPerPage - 1 : NumPerPage); + protected override bool HasNextButton => Menu.MenuOptions.Count > NumPerPage + 1 && CurrentOffset + NumPerPage < Menu.MenuOptions.Count; + public bool InlinePageOptions { get; set; } = true; + protected override int MenuItemsPerPage + { + get + { + int count = NumPerPage; + if (InlinePageOptions == false) + { + if (!HasPrevButton) + count++; + + if (!HasNextButton) + count++; + } + else + { + count++; + if (!HasExitButton && !HasPrevButton && !HasNextButton) + count++; + } + + return count; + } + } public CenterHtmlMenuInstance(BasePlugin plugin, CCSPlayerController player, IMenu menu) : base(player, menu) { _plugin = plugin; RemoveOnTickListener(); plugin.RegisterListener(Display); + + if (menu is CenterHtmlMenu centerHtmlMenu) + InlinePageOptions = centerHtmlMenu.InlinePageOptions; } public override void Display() @@ -98,28 +134,82 @@ public override void Display() builder.Append($"!{keyOffset++} {option.Text}"); builder.AppendLine("
"); } + + AddPageOptions(centerHtmlMenu, builder); + + var currentPageText = builder.ToString(); + Player.PrintToCenterHtml(currentPageText); + } + + private void AddPageOptions(CenterHtmlMenu centerHtmlMenu, StringBuilder builder) + { + string prevText = $"!7 < Prev"; + string closeText = $"!9 X Close"; + string nextText = $"!8 > Next"; + + if (InlinePageOptions) + AddInlinePageOptions(prevText, closeText, nextText, centerHtmlMenu.ExitButton, builder); + else + AddMultilinePageOptions(prevText, closeText, nextText, centerHtmlMenu.ExitButton, builder); + } + + + private void AddInlinePageOptions(string prevText, string closeText, string nextText, bool hasExitButton, StringBuilder builder) + { + if (HasPrevButton && HasExitButton && HasNextButton) + { + builder.Append($"{prevText} | {closeText} | {nextText}"); + return; + } + string doubleOptionSplitString = " \u200e \u200e \u200e \u200e | \u200e \u200e \u200e \u200e "; // empty characters that are not trimmed + + int optionsCount = 0; if (HasPrevButton) { - builder.AppendFormat($"!7 <- Prev"); - builder.AppendLine("
"); + builder.AppendFormat(prevText); + optionsCount++; + } + + if (hasExitButton) + { + if (optionsCount++ > 0) + builder.Append(doubleOptionSplitString); + + builder.AppendFormat(closeText); } if (HasNextButton) { - builder.AppendFormat($"!8 -> Next"); - builder.AppendLine("
"); + if (optionsCount > 0) + builder.Append(doubleOptionSplitString); + + builder.AppendFormat(nextText); } + } - if (centerHtmlMenu.ExitButton) + private void AddMultilinePageOptions(string prevText, string closeText, string nextText, bool hasExitButton, StringBuilder builder) + { + if (HasPrevButton) { - builder.AppendFormat($"!9 -> Close"); + builder.AppendFormat(prevText); builder.AppendLine("
"); } - var currentPageText = builder.ToString(); - Player.PrintToCenterHtml(currentPageText); + if (HasNextButton) + { + builder.AppendFormat(nextText); + builder.AppendLine("
"); + } + + if (hasExitButton) + { + builder.AppendFormat(closeText); + builder.AppendLine("
"); + } } + + public override void Close() { diff --git a/managed/CounterStrikeSharp.API/StringExtensions.cs b/managed/CounterStrikeSharp.API/StringExtensions.cs new file mode 100644 index 000000000..cfd91003c --- /dev/null +++ b/managed/CounterStrikeSharp.API/StringExtensions.cs @@ -0,0 +1,68 @@ +using System.Text; +using System.Text.RegularExpressions; + +namespace CounterStrikeSharp.API +{ + public static class StringExtensions + { + private const string HTML_TAG_REGEX_PATTERN = "<[^>]+>"; + private static readonly Regex TagRegex = new(HTML_TAG_REGEX_PATTERN, RegexOptions.Compiled); + + public static string TruncateHtml(this string msg, int maxLength) + { + if (maxLength <= 0) + return msg; + + if (string.IsNullOrEmpty(msg)) + return string.Empty; + + string textOnly = Regex.Replace(msg, HTML_TAG_REGEX_PATTERN, ""); + if (textOnly.Length <= maxLength) + return msg; + + Stack tagStack = new Stack(); + StringBuilder result = new System.Text.StringBuilder(); + int visibleLength = 0, + i = 0; + + while (i < msg.Length && visibleLength < maxLength) + { + if (msg[i] == '<') + { + Match match = TagRegex.Match(msg, i); + if (match.Success && match.Index == i) + { + string tag = match.Value; + result.Append(tag); + i += tag.Length; + + if (!tag.StartsWith("' }, StringSplitOptions.RemoveEmptyEntries)[0].Trim('<'); + if (!tag.EndsWith("/>") && !tagName.StartsWith("!")) + tagStack.Push(tagName); + } + else if (tagStack.Count > 0) + { + tagStack.Pop(); + } + + continue; + } + } + else + { + result.Append(msg[i]); + visibleLength++; + } + + i++; + } + + while (tagStack.Count > 0) + result.Append($""); + + return result.ToString(); + } + } +} diff --git a/src/scripting/natives/natives_commands.cpp b/src/scripting/natives/natives_commands.cpp index 141b3e23a..f1e8b4a13 100644 --- a/src/scripting/natives/natives_commands.cpp +++ b/src/scripting/natives/natives_commands.cpp @@ -15,13 +15,17 @@ */ #include +#include #include "scripting/autonative.h" #include "scripting/callback_manager.h" #include "core/managers/con_command_manager.h" #include "core/managers/player_manager.h" +#include "core/recipientfilters.h" +#include "igameeventsystem.h" #include "scripting/script_engine.h" #include "core/log.h" +#include namespace counterstrikesharp { @@ -191,6 +195,25 @@ void SetConVarStringValue(ScriptContext& script_context) pCvar->values = reinterpret_cast((char*)value); } +void ReplicateConVar(ScriptContext& script_context) +{ + auto slot = script_context.GetArgument(0); + auto name = script_context.GetArgument(1); + auto value = script_context.GetArgument(2); + + INetworkMessageInternal* pNetMsg = globals::networkMessages->FindNetworkMessagePartial("SetConVar"); + auto msg = pNetMsg->AllocateMessage()->ToPB(); + + CMsg_CVars_CVar* cvarMsg = msg->mutable_convars()->add_cvars(); + cvarMsg->set_name(name); + cvarMsg->set_value(value); + + CSingleRecipientFilter filter(slot); + globals::gameEventSystem->PostEventAbstract(-1, false, &filter, pNetMsg, msg, 0); + + delete msg; +} + REGISTER_NATIVES(commands, { ScriptEngine::RegisterNativeHandler("ADD_COMMAND", AddCommand); ScriptEngine::RegisterNativeHandler("REMOVE_COMMAND", RemoveCommand); @@ -211,5 +234,6 @@ REGISTER_NATIVES(commands, { IssueClientCommandFromServer); ScriptEngine::RegisterNativeHandler("GET_CLIENT_CONVAR_VALUE", GetClientConVarValue); ScriptEngine::RegisterNativeHandler("SET_FAKE_CLIENT_CONVAR_VALUE", SetFakeClientConVarValue); + ScriptEngine::RegisterNativeHandler("REPLICATE_CONVAR", ReplicateConVar); }) } // namespace counterstrikesharp diff --git a/src/scripting/natives/natives_commands.yaml b/src/scripting/natives/natives_commands.yaml index 3504386ae..b59871a32 100644 --- a/src/scripting/natives/natives_commands.yaml +++ b/src/scripting/natives/natives_commands.yaml @@ -12,4 +12,5 @@ ISSUE_CLIENT_COMMAND_FROM_SERVER: slot:int,command:string -> void FIND_CONVAR: name:string -> pointer SET_CONVAR_STRING_VALUE: convar:pointer,value:string -> void GET_CLIENT_CONVAR_VALUE: clientIndex:int,convarName:string -> string -SET_FAKE_CLIENT_CONVAR_VALUE: clientIndex:int,convarName:string,convarValue:string -> void \ No newline at end of file +SET_FAKE_CLIENT_CONVAR_VALUE: clientIndex:int,convarName:string,convarValue:string -> void +REPLICATE_CONVAR: clientSlot:int,convarName:string,convarValue:string -> void