From 74852256de2db03696cb8cfc7ab73e35df2f1499 Mon Sep 17 00:00:00 2001 From: callumok2004 Date: Fri, 16 Oct 2026 20:22:57 +0100 Subject: [PATCH 1/3] Cleanups & lua dumper --- Game.Client/GarrysMod/LuaPanel.cs | 10 +- Game.Shared/GarrysMod/LuaAngle.cs | 10 +- Game.Shared/GarrysMod/LuaClass.cs | 5 +- Game.Shared/GarrysMod/LuaDump.cs | 273 +++++++++++++++++++++++ Game.Shared/GarrysMod/LuaVector.cs | 10 +- Source.Engine/GarrysMod/Resources.cs | 2 +- Source.Engine/MatSysInterface.cs | 6 +- Source.Filesystem/PackStoreSearchPath.cs | 3 +- 8 files changed, 302 insertions(+), 17 deletions(-) create mode 100644 Game.Shared/GarrysMod/LuaDump.cs diff --git a/Game.Client/GarrysMod/LuaPanel.cs b/Game.Client/GarrysMod/LuaPanel.cs index 96f128e3f..1e49f5744 100644 --- a/Game.Client/GarrysMod/LuaPanel.cs +++ b/Game.Client/GarrysMod/LuaPanel.cs @@ -59,8 +59,8 @@ static int Panel____index(ILuaInterface lua) { return 1; if (panel != null) { - string? key = lua.GetString(2); - if (key != null && key.Length == 1) { + ReadOnlySpan key = lua.GetStringBytes(2); + if (key.Length == 1) { if (key[0] == 'x' || key[0] == 'X') { panel.GetPos(out int x, out _); lua.PushNumber(x); @@ -107,13 +107,13 @@ static int Panel____newindex(ILuaInterface lua) { ILuaObject key = lua.GetObject(2)!; if (key.isString()) { ILuaObject value = lua.GetObject(3)!; - string? name = key.GetString(); - if (name == "x" || name == "X") { + ReadOnlySpan name = lua.GetStringBytes(2); + if (name.Length == 1 && (name[0] == 'x' || name[0] == 'X')) { panel.GetPos(out _, out int y); panel.SetPos((int)value.GetFloat(), y); return 0; } - if (name == "y" || name == "Y") { + if (name.Length == 1 && (name[0] == 'y' || name[0] == 'Y')) { panel.GetPos(out int x, out _); panel.SetPos(x, (int)value.GetFloat()); return 0; diff --git a/Game.Shared/GarrysMod/LuaAngle.cs b/Game.Shared/GarrysMod/LuaAngle.cs index fedd10d23..281012d09 100644 --- a/Game.Shared/GarrysMod/LuaAngle.cs +++ b/Game.Shared/GarrysMod/LuaAngle.cs @@ -21,7 +21,11 @@ public static partial class LuaAngle public static void Push_Angle(in QAngle ang) => g_Lua!.PushAngle(in ang); - static char FirstChar(ReadOnlySpan str) => str.IsEmpty ? '\0' : str[0]; + static char FirstChar(int stackPos) { + g_Lua!.CheckType(stackPos, LuaType.String); + ReadOnlySpan str = g_Lua.GetStringBytes(stackPos); + return str.IsEmpty ? '\0' : (char)str[0]; + } [LuaMethod] static int Angle____newindex(ILuaInterface lua) { @@ -35,7 +39,7 @@ static int Angle____newindex(ILuaInterface lua) { return 0; } else { - switch (FirstChar(g_Lua.CheckString(2))) { + switch (FirstChar(2)) { case 'P': case 'X': case 'p': case 'x': index = 0; break; case 'Y': case 'y': index = 1; break; case 'R': case 'Z': case 'r': case 'z': index = 2; break; @@ -63,7 +67,7 @@ static int Angle____index(ILuaInterface lua) { return 0; } else { - switch (FirstChar(g_Lua.CheckString(2))) { + switch (FirstChar(2)) { case 'P': case 'X': case 'p': case 'x': index = 0; break; case 'Y': case 'y': index = 1; break; case 'R': case 'Z': case 'r': case 'z': index = 2; break; diff --git a/Game.Shared/GarrysMod/LuaClass.cs b/Game.Shared/GarrysMod/LuaClass.cs index e21928d5d..85df1569a 100644 --- a/Game.Shared/GarrysMod/LuaClass.cs +++ b/Game.Shared/GarrysMod/LuaClass.cs @@ -44,11 +44,12 @@ static int __index_Derived(ILuaInterface lua) { } public object? Get(int stackPos) { - if (!g_Lua!.IsType(stackPos, Type)) { + object? obj = g_Lua!.GetObjectUserType(stackPos, Type); + if (obj == null && !g_Lua.IsType(stackPos, Type)) { g_Lua.TypeError(Name, stackPos); return null; } - return g_Lua.GetObjectUserType(stackPos, Type); + return obj; } public ref T GetValue(int stackPos) where T : unmanaged { diff --git a/Game.Shared/GarrysMod/LuaDump.cs b/Game.Shared/GarrysMod/LuaDump.cs new file mode 100644 index 000000000..020b0603e --- /dev/null +++ b/Game.Shared/GarrysMod/LuaDump.cs @@ -0,0 +1,273 @@ +#if DEBUG && (CLIENT_DLL || GAME_DLL || MENU_DLL) +using Source; +using Source.Common.Commands; +using Source.Common.GarrysMod.Lua; + +using System.IO; +using System.Text; +using System.Text.RegularExpressions; + +namespace Game.Client.GarrysMod; + +public static class LuaDump +{ + const string OutDir = "sdn_luadump"; + + const string Script = """ + local lower, sort, concat = string.lower, table.sort, table.concat + local getinfo = debug.getinfo + local out, metas, sources = {}, {}, {} + + local function Kind(f) + local info = getinfo(f, "S") + if not info then return "lua" end + if info.what == "C" then return "c" end + if isstring(info.short_src) and string.sub(info.short_src, 1, 9) == "[builtin:" then return "c" end + if isstring(info.source) and string.sub(info.source, 1, 1) == "@" then sources[string.sub(info.source, 2)] = true end + return "lua" + end + + local function Emit(addr, lang) + addr = lower(addr) + if not out[addr] then out[addr] = lang end + end + + local function TableKind(t) + for _, v in pairs(t) do + if isfunction(v) and Kind(v) == "c" then return "c" end + end + return "lua" + end + + local function Scan(prefix, sep, t) + for k, v in pairs(t) do + if isstring(k) then + if isfunction(v) then + Emit(prefix .. sep .. k, Kind(v)) + elseif istable(v) and v ~= t then + Emit(prefix .. "." .. k, TableKind(v)) + end + end + end + end + + local classNames = { + "Angle", "Vector", "VMatrix", "Color", "Entity", "Player", "Weapon", "NPC", "NextBot", "Vehicle", "CSEnt", + "PhysObj", "PhysCollide", "Panel", "IMaterial", "ITexture", "IMesh", "IVideoWriter", "IGModAudioChannel", + "IRestore", "ISave", "File", "ConVar", "CEffectData", "CLuaEmitter", "CLuaParticle", "CLuaLocomotion", + "CMoveData", "CUserCmd", "CTakeDamageInfo", "CSoundPatch", "CNavArea", "CNavLadder", "CNewParticleEffect", + "PathFollower", "ProjectedTexture", "MarkupObject", "Schedule", "Task", "SurfaceInfo", "Stack", "Tool", + "bf_read", "pixelvis_handle_t", "sensor", "SchedulingObject", "Collection", + } + local reg = debug.getregistry and debug.getregistry() + if istable(reg) then + for name in pairs(reg) do + if isstring(name) then classNames[#classNames + 1] = name end + end + end + for _, name in ipairs(classNames) do + local mt = FindMetaTable and FindMetaTable(name) + if istable(mt) and not metas[mt] then + metas[mt] = true + Scan(name, ":", mt) + end + end + + if derma and derma.GetControlList then + for _, ctrl in pairs(derma.GetControlList()) do + if istable(ctrl) and isstring(ctrl.ClassName) and istable(ctrl.PanelTable) then + metas[ctrl.PanelTable] = true + Scan(ctrl.ClassName, ":", ctrl.PanelTable) + end + end + end + + for name, v in pairs(_G) do + if isstring(name) then + if isfunction(v) then + Emit("global." .. name, Kind(v)) + elseif istable(v) and v ~= _G and not metas[v] and name ~= "package" and name ~= "_LOADED" and name ~= "GAMEMODE" and name ~= "GM" then + local isPanel = isfunction(rawget(v, "Init")) or isstring(rawget(v, "Base")) + Scan(name, isPanel and ":" or ".", v) + end + end + end + + local lines, files = {}, {} + for addr, lang in pairs(out) do lines[#lines + 1] = addr .. " = " .. lang end + for src in pairs(sources) do files[#files + 1] = src end + sort(lines) + SDN_LUADUMP = concat(lines, "\n") + SDN_LUADUMP_SRC = concat(files, "\n") + """; + + static readonly (string Project, string[] Realms)[] Projects = [ + ("Game.Client", ["client"]), + ("Game.Server", ["server"]), + ("Game.Shared", ["client", "server"]), + ("Game.MenuSystem", ["menu"]), + ("Source.GUI.Controls", ["client", "menu"]), + ]; + + static readonly Regex GamemodeHook = new(@"gGM[!?]?\.(?:Call|CallWithArgs)\(\s*(?:\(int\))?(?:PooledStrings\.)?LUA_POOLEDSTRING\.(\w+)"); + static readonly Regex GamemodeHookString = new(@"\bCall(?:WithArgs)?\(""(\w+)""\)"); + static readonly Regex PanelHook = new(@"PushLuaHook\(LUA_POOLEDSTRING\.(\w+)|UpdateLuaHook\(panel, ""(\w+)""|GetField\(-1, ""(On\w+)""\)"); + static readonly Regex EntityHook = new(@"ScriptedEntity\.(?:StartFunction|CallFunction)\(\s*\(int\)LUA_POOLEDSTRING\.(\w+)"); + static readonly Regex LuaHookCall = new(@"hook\.(?:Run|Call)\(\s*[""'](\w+)[""']|(? hooks) { + string? root = FindRepoRoot(); + if (root == null) { + Warning("sdn_luadump: Source.NET.slnx not found above the binary, C# hook calls skipped\n"); + return; + } + + foreach (var (project, realms) in Projects) { + if (Array.IndexOf(realms, realm) == -1) + continue; + + string dir = Path.Combine(root, project); + if (!Directory.Exists(dir)) + continue; + + foreach (string file in Directory.EnumerateFiles(dir, "*.cs", SearchOption.AllDirectories)) { + string text = File.ReadAllText(file); + string name = Path.GetFileName(file); + + foreach (Match m in GamemodeHook.Matches(text)) + hooks["gm:" + m.Groups[1].Value.ToLowerInvariant()] = "c"; + if (name == "LuaGamemode.cs") + foreach (Match m in GamemodeHookString.Matches(text)) + hooks["gm:" + m.Groups[1].Value.ToLowerInvariant()] = "c"; + foreach (Match m in PanelHook.Matches(text)) { + string hook = m.Groups[1].Success ? m.Groups[1].Value : m.Groups[2].Success ? m.Groups[2].Value : m.Groups[3].Value; + hooks[(hook == "DoClick" ? "dlabel:" : "panel:") + hook.ToLowerInvariant()] = "c"; + } + string owner = name.Contains("SWEP") ? "weapon:" : "entity:"; + foreach (Match m in EntityHook.Matches(text)) + hooks[owner + m.Groups[1].Value.ToLowerInvariant()] = "c"; + } + } + } + + static string? ReadFile(string path, string pathID) { + long size = filesystem.Size(path, pathID); + if (size <= 0) + return null; + byte[] buf = new byte[size]; + if (!filesystem.ReadFile(path, pathID, buf, 0)) + return null; + return Encoding.UTF8.GetString(buf); + } + + static string? ReadLuaSource(ILuaInterface lua, string src) { + string? text = ReadFile(src, "GAME") ?? ReadFile(src, lua.GetPathID()); + if (text == null && src.StartsWith("lua/")) + text = ReadFile(src["lua/".Length..], lua.GetPathID()); + if (text == null && src.StartsWith("gamemodes/")) + text = ReadFile(src["gamemodes/".Length..], lua.GetPathID()); + return text; + } + + static void ScanLuaHooks(ILuaInterface lua, string sources, SortedDictionary hooks) { + string[] files = sources.Split('\n', StringSplitOptions.RemoveEmptyEntries); + int unread = 0; + string? firstUnread = null; + foreach (string src in files) { + string? text = ReadLuaSource(lua, src); + if (text == null) { + unread++; + firstUnread ??= src; + continue; + } + foreach (Match m in LuaHookCall.Matches(text)) + hooks.TryAdd("gm:" + (m.Groups[1].Success ? m.Groups[1].Value : m.Groups[2].Value).ToLowerInvariant(), "lua"); + } + Msg($"sdn_luadump: scanned {files.Length - unread}/{files.Length} Lua sources for hook calls\n"); + if (firstUnread != null) + Warning($"sdn_luadump: {unread} Lua sources unreadable, first: {firstUnread}\n"); + } + + static string? GetGlobalString(ILuaInterface lua, string name) { + lua.PushSpecial(Special.Glob); + lua.GetField(-1, name); + string? value = lua.GetString(-1); + lua.Pop(2); + return value; + } + + public static void Dump(ILuaInterface? lua, string realm) { + if (!commandLine.CheckParm("-noaddons") || !commandLine.CheckParm("-noworkshop")) { + Warning("launch with -noaddons -noworkshop so data isn't corrupted :(\n"); + return; + } + + if (lua == null) { + Warning($"sdn_luadump: no {realm} Lua state\n"); + return; + } + + if (!lua.RunString("sdn_luadump", "", Script, true, true)) { + Warning($"sdn_luadump: {realm} dump script failed\n"); + return; + } + + string functions = GetGlobalString(lua, "SDN_LUADUMP") ?? ""; + string sources = GetGlobalString(lua, "SDN_LUADUMP_SRC") ?? ""; + lua.RunString("sdn_luadump", "", "SDN_LUADUMP = nil SDN_LUADUMP_SRC = nil", true, true); + + SortedDictionary hooks = []; + ScanCSharpHooks(realm, hooks); + ScanLuaHooks(lua, sources, hooks); + + StringBuilder sb = new(); + sb.Append("# sdn_luadump 2\n# realm ").Append(realm).Append('\n'); + sb.Append(functions).Append('\n'); + sb.Append("# hooks\n"); + foreach (var (addr, lang) in hooks) + sb.Append(addr).Append(" = ").Append(lang).Append('\n'); + + Directory.CreateDirectory(OutDir); + string path = Path.Combine(OutDir, "sdn_luadump_" + realm + ".txt"); + File.WriteAllText(path, sb.ToString()); + Msg($"sdn_luadump: {realm} realm dumped to {Path.GetFullPath(path)} ({hooks.Count} hooks)\n"); + } + +#if CLIENT_DLL + [ConCommand("sdn_luadump_client")] + static void CC_LuaDumpClient() => Dump(g_Lua, "client"); + + [ConCommand("sdn_luadump")] + static void CC_LuaDump() { + StringBuilder sb = new(); + foreach (string realm in (string[])["server", "client", "menu"]) { + string path = Path.Combine(OutDir, "sdn_luadump_" + realm + ".txt"); + if (!File.Exists(path)) { + Warning($"sdn_luadump: missing {path}, run sdn_luadump_{realm} first\n"); + continue; + } + sb.Append(File.ReadAllText(path)); + } + + Directory.CreateDirectory(OutDir); + string outPath = Path.Combine(OutDir, "sdn_luadump.txt"); + File.WriteAllText(outPath, sb.ToString()); + Msg($"sdn_luadump: combined dump written to {Path.GetFullPath(outPath)}\n"); + } +#elif GAME_DLL + [ConCommand("sdn_luadump_server")] + static void CC_LuaDumpServer() => Dump(g_Lua, "server"); +#elif MENU_DLL + [ConCommand("sdn_luadump_menu")] + static void CC_LuaDumpMenu() => Dump(g_Lua, "menu"); +#endif +} +#endif diff --git a/Game.Shared/GarrysMod/LuaVector.cs b/Game.Shared/GarrysMod/LuaVector.cs index 2c918e80b..a07ff486b 100644 --- a/Game.Shared/GarrysMod/LuaVector.cs +++ b/Game.Shared/GarrysMod/LuaVector.cs @@ -21,7 +21,11 @@ public static partial class LuaVector public static void Push_Vector(in Vector3 vec) => g_Lua!.PushVector(in vec); - static char FirstChar(ReadOnlySpan str) => str.IsEmpty ? '\0' : str[0]; + static char FirstChar(int stackPos) { + g_Lua!.CheckType(stackPos, LuaType.String); + ReadOnlySpan str = g_Lua.GetStringBytes(stackPos); + return str.IsEmpty ? '\0' : (char)str[0]; + } [LuaMethod] static int Vector____index(ILuaInterface lua) { @@ -36,7 +40,7 @@ static int Vector____index(ILuaInterface lua) { return 0; } else { - switch (FirstChar(g_Lua.CheckString(2))) { + switch (FirstChar(2)) { case 'X': case 'r': case 'x': index = 0; break; case 'Y': case 'g': case 'y': index = 1; break; case 'Z': case 'b': case 'z': index = 2; break; @@ -60,7 +64,7 @@ static int Vector____newindex(ILuaInterface lua) { return 0; } else { - switch (FirstChar(g_Lua.CheckString(2))) { + switch (FirstChar(2)) { case 'X': case 'r': case 'x': index = 0; break; case 'Y': case 'g': case 'y': index = 1; break; case 'Z': case 'b': case 'z': index = 2; break; diff --git a/Source.Engine/GarrysMod/Resources.cs b/Source.Engine/GarrysMod/Resources.cs index fb5cabd64..fd8ec1598 100644 --- a/Source.Engine/GarrysMod/Resources.cs +++ b/Source.Engine/GarrysMod/Resources.cs @@ -74,7 +74,7 @@ public bool Load() { return false; } - byte[] data = new byte[size]; + byte[] data = GC.AllocateUninitializedArray((int)size); file.Stream.ReadExactly(data); file.Dispose(); diff --git a/Source.Engine/MatSysInterface.cs b/Source.Engine/MatSysInterface.cs index a6cab83e7..79d0f5611 100644 --- a/Source.Engine/MatSysInterface.cs +++ b/Source.Engine/MatSysInterface.cs @@ -226,10 +226,12 @@ internal void AddSurfaceToTail(ref BSPMSurface2 surface, int sortGroup, short so public List GetSortList(int sortGroup) => SortGroupLists[sortGroup]; public void GetSurfaceListForGroup(List list, in SurfaceSortGroup group) { - for (short blockIndex = group.ListHead; blockIndex != -1; blockIndex = GetSurfaceBlock(blockIndex).NextBlock) { - ref MaterialList matList = ref GetSurfaceBlock(blockIndex); + Span blocks = List.AsSpan(); + for (short blockIndex = group.ListHead; blockIndex != -1;) { + ref MaterialList matList = ref blocks[blockIndex]; for (int index = 0; index < matList.Count; ++index) list.Add((SurfaceHandle_t)matList.Surfaces[index]); + blockIndex = matList.NextBlock; } } diff --git a/Source.Filesystem/PackStoreSearchPath.cs b/Source.Filesystem/PackStoreSearchPath.cs index bef6f48d1..90e73e40f 100644 --- a/Source.Filesystem/PackStoreSearchPath.cs +++ b/Source.Filesystem/PackStoreSearchPath.cs @@ -183,6 +183,7 @@ public override void PrepareFinds(List files, List dirs, string? int wildcardAsteriskAtDir = wildcardDir.IndexOf('*'); int wildcardAsteriskAtFile = wildcardFile.IndexOf('*'); int wildcardAsteriskAtExt = wildcardExt.IndexOf('*'); + ReadOnlySpan wildcardName = Path.GetFileName(wildcard.AsSpan()); foreach (var directory in vpkDirectoryLookups) { VpkDirectory dir = directory.Value; @@ -198,7 +199,7 @@ public override void PrepareFinds(List files, List dirs, string? continue; } - if (wildcard != null && !System.IO.Enumeration.FileSystemName.MatchesSimpleExpression(Path.GetFileName(wildcard), baseName)) + if (wildcard != null && !System.IO.Enumeration.FileSystemName.MatchesSimpleExpression(wildcardName, baseName)) continue; dirs.Add(new(baseName)); From 1c216cdccfc24141f2be85c4e091cf792813e0f7 Mon Sep 17 00:00:00 2001 From: callumok2004 Date: Fri, 16 Oct 2026 20:49:58 +0100 Subject: [PATCH 2/3] Shaders fix --- Game.MenuSystem/Game.MenuSystem.csproj | 2 +- Source.ShaderAPI.Gl46/VcsTranslator.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Game.MenuSystem/Game.MenuSystem.csproj b/Game.MenuSystem/Game.MenuSystem.csproj index 7a94582e1..48df5d37f 100644 --- a/Game.MenuSystem/Game.MenuSystem.csproj +++ b/Game.MenuSystem/Game.MenuSystem.csproj @@ -17,7 +17,7 @@ - + diff --git a/Source.ShaderAPI.Gl46/VcsTranslator.cs b/Source.ShaderAPI.Gl46/VcsTranslator.cs index 10987fd8e..794611fd3 100644 --- a/Source.ShaderAPI.Gl46/VcsTranslator.cs +++ b/Source.ShaderAPI.Gl46/VcsTranslator.cs @@ -346,7 +346,7 @@ string Rows(uint vec, uint matrix, int rows, bool dot3) { if (pixel) head.Append("layout(std140, binding = 3) uniform source_pixel_sharedUBO { bool isAlphaTesting; int alphaTestFunc; float alphaTestRef; };\nlayout(std140, binding = 6) uniform source_ps_constants { vec4 ps_const[256]; };\nlayout(location = 0) out vec4 oC0;\n"); else - head.Append("layout(std140, binding = 0) uniform source_matrices { mat4 viewMatrix; mat4 projectionMatrix; mat4 modelMatrix; };\nlayout(std140, binding = 5) uniform source_vs_constants { vec4 vs_const[256]; };\n"); + head.Append("layout(std140, binding = 0) uniform source_matrices { mat4 viewMatrix; mat4 projectionMatrix; mat4 modelMatrix; };\nlayout(std140, binding = 5) uniform source_vs_constants { vec4 vs_const[256]; };\nout gl_PerVertex { vec4 gl_Position; };\n"); StringBuilder body = new("void main() {\n"); body.Append(pixel From 48070d7776370291ebc33adf75680ae4ae7ef803 Mon Sep 17 00:00:00 2001 From: callumok2004 Date: Mon, 19 Oct 2026 15:01:55 +0100 Subject: [PATCH 3/3] Misc lua impl --- Game.Client/GarrysMod/LuaPanel.cs | 4 +-- Game.Lua/LuaInterfaceImpl.cs | 10 ++++++++ Game.MenuSystem/MenuPanels.cs | 28 +++++++++++++++++++++ Game.MenuSystem/MenuSystem.cs | 25 ++++++++++++++++++ Game.Shared/GarrysMod/LuaDump.cs | 11 ++++++-- Game.Shared/GarrysMod/LuaEngine.cs | 24 +++++++++--------- Game.Shared/GarrysMod/LuaEnums.cs | 10 ++++++++ Game.Shared/GarrysMod/LuaGlobalFunctions.cs | 5 ++++ Source.Common/GarrysMod/IMenuSystem.cs | 1 + Source.GUI.Controls/Panel.cs | 11 ++++++++ Source.GUI.Controls/TextEntry.cs | 13 ++++++++-- 11 files changed, 124 insertions(+), 18 deletions(-) diff --git a/Game.Client/GarrysMod/LuaPanel.cs b/Game.Client/GarrysMod/LuaPanel.cs index 67a10a216..66e74a69e 100644 --- a/Game.Client/GarrysMod/LuaPanel.cs +++ b/Game.Client/GarrysMod/LuaPanel.cs @@ -88,7 +88,7 @@ static int Panel__PaintManual(ILuaInterface lua) { lua.Error("Tried to use a NULL Panel!"); return 0; } - panel.PaintManual(lua.GetType(2) != LuaType.Nil &&lua.GetBool(2)); + panel.PaintManual(lua.GetType(2) != LuaType.Nil && lua.GetBool(2)); return 0; } @@ -904,7 +904,6 @@ static int Panel__IsLoading(ILuaInterface lua) { return 1; } -#if !MENU_DLL [LuaMethod] static int Panel__UpdateHTMLTexture(ILuaInterface lua) { Panel? panel = (Panel?)PanelClass.Get(1); @@ -918,6 +917,7 @@ static int Panel__UpdateHTMLTexture(ILuaInterface lua) { return 0; } +#if !MENU_DLL [LuaMethod] static int Panel__GetHTMLMaterial(ILuaInterface lua) { Panel? panel = (Panel?)PanelClass.Get(1); diff --git a/Game.Lua/LuaInterfaceImpl.cs b/Game.Lua/LuaInterfaceImpl.cs index 645c40a3f..102175802 100644 --- a/Game.Lua/LuaInterfaceImpl.cs +++ b/Game.Lua/LuaInterfaceImpl.cs @@ -129,6 +129,16 @@ public bool Init(ILuaGameCallback callbacks, bool isServer) { luaL_openlibs(state); DoStackCheck(); + GetField(LuaIndex.Global, "string"); + GetField(-1, "gmatch"); + SetField(-2, "gfind"); + lua_settop(state, -2); + + GetField(LuaIndex.Global, "math"); + GetField(-1, "fmod"); + SetField(-2, "mod"); + lua_settop(state, -2); + GetField(LuaIndex.Global, "debug"); PushNil(); SetField(-2, "setlocal"); diff --git a/Game.MenuSystem/MenuPanels.cs b/Game.MenuSystem/MenuPanels.cs index 594f7008a..76b8f98ad 100644 --- a/Game.MenuSystem/MenuPanels.cs +++ b/Game.MenuSystem/MenuPanels.cs @@ -1,5 +1,7 @@ #if MENU_DLL +using Source; using Source.Common.Formats.Keyvalues; +using Source.Common.MaterialSystem; using Source.Common.GUI; using Source.GUI.Controls; @@ -44,6 +46,32 @@ public override void OnThink() { Surface.MovePopupToFront(this); MenuSystem.g_MenuSystem.Think(); } + + static TimeUnit_t LastPaintTime; + public static TimeUnit_t FrameTime; + + public override void Paint() { + if (IsMarkedForDeletion()) + return; + + using MatRenderContextPtr renderContext = new(materials); + renderContext.ClearBuffers(false, true); + + double now = Platform.Time; + LastPaintTime = now - LastPaintTime; + FrameTime = 0.0; + if (LastPaintTime >= 0.0) { + FrameTime = LastPaintTime; + if (LastPaintTime >= 0.1) + FrameTime = 0.1; + } + LastPaintTime = now; + + PaintDrawOnTopPanels(); + + MenuSystem.RunHook(LUA_POOLEDSTRING.DrawOverlay); + MenuSystem.RunHook(LUA_POOLEDSTRING.CaptureVideo); + } } public static class MenuPanels diff --git a/Game.MenuSystem/MenuSystem.cs b/Game.MenuSystem/MenuSystem.cs index 28519954c..b80e70afd 100644 --- a/Game.MenuSystem/MenuSystem.cs +++ b/Game.MenuSystem/MenuSystem.cs @@ -65,6 +65,25 @@ internal static void CallHook(ReadOnlySpan name) { hook.UnReference(); } + internal static void RunHook(LUA_POOLEDSTRING name) { + if (g_Lua == null || g_Lua.Global() == null) + return; + + LuaObject hook = new(); + g_Lua.Global().GetMember("hook", hook); + if (hook.isTable()) { + LuaObject run = new(); + hook.GetMember("Run", run); + if (run.isFunction()) { + run.Push(); + g_Lua.PushPooledString((int)name); + g_Lua.CallInternalNoReturns(1); + } + run.UnReference(); + } + hook.UnReference(); + } + public void Think() { if (g_Lua != null && g_Lua.Global() != null) { g_Lua.Cycle(); @@ -257,6 +276,12 @@ public void SendProblemToMenu(ReadOnlySpan id, int severity, ReadOnlySpan< #endif } + public void OnPauseMenuBlockedTooManyTimes() { +#if MENU_DLL + CallHook("OnPauseMenuBlockedTooManyTimes"); +#endif + } + public void ServerDetails(ReadOnlySpan serverName, ReadOnlySpan loadingURL, ReadOnlySpan mapName, byte maxPlayers, short playerSlots, ReadOnlySpan gamemode) { #if MENU_DLL filesystem.Gamemodes().OnJoinServer(gamemode); diff --git a/Game.Shared/GarrysMod/LuaDump.cs b/Game.Shared/GarrysMod/LuaDump.cs index 020b0603e..2482a4427 100644 --- a/Game.Shared/GarrysMod/LuaDump.cs +++ b/Game.Shared/GarrysMod/LuaDump.cs @@ -39,13 +39,17 @@ local function TableKind(t) return "lua" end + local visited = {} + local function Scan(prefix, sep, t) + visited[t] = true for k, v in pairs(t) do if isstring(k) then if isfunction(v) then Emit(prefix .. sep .. k, Kind(v)) elseif istable(v) and v ~= t then Emit(prefix .. "." .. k, TableKind(v)) + if not visited[v] and v ~= _G and not metas[v] then Scan(prefix .. "." .. k, ".", v) end end end end @@ -111,7 +115,8 @@ static readonly (string Project, string[] Realms)[] Projects = [ static readonly Regex GamemodeHook = new(@"gGM[!?]?\.(?:Call|CallWithArgs)\(\s*(?:\(int\))?(?:PooledStrings\.)?LUA_POOLEDSTRING\.(\w+)"); static readonly Regex GamemodeHookString = new(@"\bCall(?:WithArgs)?\(""(\w+)""\)"); - static readonly Regex PanelHook = new(@"PushLuaHook\(LUA_POOLEDSTRING\.(\w+)|UpdateLuaHook\(panel, ""(\w+)""|GetField\(-1, ""(On\w+)""\)"); + static readonly Regex MenuHook = new(@"\b(?:CallHook|Begin)\(""(\w+)""\)|\bRunHook\(LUA_POOLEDSTRING\.(\w+)\)"); + static readonly Regex PanelHook = new(@"PushLuaHook\(LUA_POOLEDSTRING\.(\w+)|UpdateLuaHook\(panel, ""(\w+)""|GetField\(-1, ""(On\w+)""\)|CanCallLuaHook\((?:\w+\.)?Lua(\w+)\)"); static readonly Regex EntityHook = new(@"ScriptedEntity\.(?:StartFunction|CallFunction)\(\s*\(int\)LUA_POOLEDSTRING\.(\w+)"); static readonly Regex LuaHookCall = new(@"hook\.(?:Run|Call)\(\s*[""'](\w+)[""']|(? hooks if (name == "LuaGamemode.cs") foreach (Match m in GamemodeHookString.Matches(text)) hooks["gm:" + m.Groups[1].Value.ToLowerInvariant()] = "c"; + foreach (Match m in MenuHook.Matches(text)) + hooks["gm:" + (m.Groups[1].Success ? m.Groups[1].Value : m.Groups[2].Value).ToLowerInvariant()] = "c"; foreach (Match m in PanelHook.Matches(text)) { - string hook = m.Groups[1].Success ? m.Groups[1].Value : m.Groups[2].Success ? m.Groups[2].Value : m.Groups[3].Value; + string hook = m.Groups[1].Success ? m.Groups[1].Value : m.Groups[2].Success ? m.Groups[2].Value : m.Groups[3].Success ? m.Groups[3].Value : m.Groups[4].Value; hooks[(hook == "DoClick" ? "dlabel:" : "panel:") + hook.ToLowerInvariant()] = "c"; } string owner = name.Contains("SWEP") ? "weapon:" : "entity:"; diff --git a/Game.Shared/GarrysMod/LuaEngine.cs b/Game.Shared/GarrysMod/LuaEngine.cs index ac1b7b3c7..d26e2f4d2 100644 --- a/Game.Shared/GarrysMod/LuaEngine.cs +++ b/Game.Shared/GarrysMod/LuaEngine.cs @@ -21,6 +21,18 @@ static int TickInterval(ILuaInterface lua) { return 1; } + [LuaFunction] + static int AbsoluteFrameTime(ILuaInterface lua) { + lua.PushNumber(gpGlobals.AbsoluteFrameTime); + return 1; + } + + [LuaFunction] + static int TickCount(ILuaInterface lua) { + lua.PushNumber(gpGlobals.TickCount); + return 1; + } + [LuaFunction] static int ActiveGamemode(ILuaInterface lua) { g_Lua!.PushString(filesystem.Gamemodes().Active().Name); @@ -64,18 +76,6 @@ static int GetDemoPlaybackTotalTicks(ILuaInterface lua) { return 1; } - [LuaFunction] - static int AbsoluteFrameTime(ILuaInterface lua) { - lua.PushNumber(gpGlobals.AbsoluteFrameTime); - return 1; - } - - [LuaFunction] - static int TickCount(ILuaInterface lua) { - lua.PushNumber(gpGlobals.TickCount); - return 1; - } - [LuaFunction] static int GetUserContent(ILuaInterface lua) { LinkedList list = filesystem.Addons().GetUGCList(); diff --git a/Game.Shared/GarrysMod/LuaEnums.cs b/Game.Shared/GarrysMod/LuaEnums.cs index adba4d759..1ef90896f 100644 --- a/Game.Shared/GarrysMod/LuaEnums.cs +++ b/Game.Shared/GarrysMod/LuaEnums.cs @@ -1959,6 +1959,16 @@ public override void InitLibraries(ILuaInterface lua) { global.SetMemberDouble("RIGHT", 3.0); global.SetMemberDouble("FILL", 1.0); + global.SetMemberDouble("BOTTOM_LEFT", 1.0); + global.SetMemberDouble("BOTTOM_CENTER", 2.0); + global.SetMemberDouble("BOTTOM_RIGHT", 3.0); + global.SetMemberDouble("MIDDLE_LEFT", 4.0); + global.SetMemberDouble("CENTER", 5.0); + global.SetMemberDouble("MIDDLE_RIGHT", 6.0); + global.SetMemberDouble("TOP_LEFT", 7.0); + global.SetMemberDouble("TOP_CENTER", 8.0); + global.SetMemberDouble("TOP_RIGHT", 9.0); + #if MENU_DLL global.SetMember("MENU_DLL", true); #else diff --git a/Game.Shared/GarrysMod/LuaGlobalFunctions.cs b/Game.Shared/GarrysMod/LuaGlobalFunctions.cs index a6a250904..ba9e01eb2 100644 --- a/Game.Shared/GarrysMod/LuaGlobalFunctions.cs +++ b/Game.Shared/GarrysMod/LuaGlobalFunctions.cs @@ -686,8 +686,13 @@ static double UnPredictedCurTime() { static float RealTime() => (float)gpGlobals.RealTime; #endif +#if MENU_DLL + [LuaGlobal] + static double FrameTime() => OverlayPanel.FrameTime; +#else [LuaGlobal] static float FrameTime() => (float)gpGlobals.FrameTime; +#endif #if !MENU_DLL [LuaGlobal] diff --git a/Source.Common/GarrysMod/IMenuSystem.cs b/Source.Common/GarrysMod/IMenuSystem.cs index 7fbc422e5..769d8bc85 100644 --- a/Source.Common/GarrysMod/IMenuSystem.cs +++ b/Source.Common/GarrysMod/IMenuSystem.cs @@ -14,4 +14,5 @@ public interface IMenuSystem void OnLuaError(in LuaError err, IAddonSystem.Information? addonInfo); void SendProblemToMenu(ReadOnlySpan id, int severity, ReadOnlySpan parms); bool IsServerBlacklisted(ReadOnlySpan address, ReadOnlySpan hostname, ReadOnlySpan description, ReadOnlySpan gamemode, ReadOnlySpan map); + void OnPauseMenuBlockedTooManyTimes(); } diff --git a/Source.GUI.Controls/Panel.cs b/Source.GUI.Controls/Panel.cs index e64ed9ca7..9b9f8a8ea 100644 --- a/Source.GUI.Controls/Panel.cs +++ b/Source.GUI.Controls/Panel.cs @@ -1165,6 +1165,17 @@ public virtual void OnScreenSizeChanged(int oldWide, int oldTall) { if (x == 0 && y == 0 && wide == oldWide && tall == oldTall) SetBounds(0, 0, screenWide, screenTall); +#if GMOD_DLL + if (PushLuaHook(LUA_POOLEDSTRING.OnScreenSizeChanged)) { + PushLua(Lua!, LuaType.Panel); + Lua!.PushNumber(oldWide); + Lua.PushNumber(oldTall); + Lua.PushNumber(screenWide); + Lua.PushNumber(screenTall); + Lua.CallInternalNoReturns(5); + } +#endif + Flags |= PanelFlags.NeedsLayout; InvalidateLayout(); diff --git a/Source.GUI.Controls/TextEntry.cs b/Source.GUI.Controls/TextEntry.cs index 228cf69e4..5c6c08094 100644 --- a/Source.GUI.Controls/TextEntry.cs +++ b/Source.GUI.Controls/TextEntry.cs @@ -3,6 +3,7 @@ using Source.Common.Formats.Keyvalues; using Source.Common.GUI; using Source.Common.Input; +using Source.Common.GarrysMod.Lua; namespace Source.GUI.Controls; @@ -721,7 +722,12 @@ public override void OnKeyCodeTyped(ButtonCode code) { engine.ClientCmd_Unrestricted(command); return; } - // todo: OnKeyCodeTyped hook + if (PushLuaHook(LUA_POOLEDSTRING.OnKeyCodeTyped)) { + PushLua(Lua!, LuaType.Panel); + Lua!.PushNumber((int)code); + if (Lua.CallInternalGetBool(2)) + return; + } #endif CursorIsAtEnd = PutCursorAtEnd; PutCursorAtEnd = false; @@ -1816,7 +1822,10 @@ public void FireActionSignal() { InvalidateLayout(); #if GMOD_DLL RecalculateLineBreaks(); - // todo: OnTextChanged hook + if (PushLuaHook(LUA_POOLEDSTRING.OnTextChanged)) { + PushLua(Lua!, LuaType.Panel); + Lua!.CallInternalNoReturns(1); + } FiringActionSignal = false; #endif }