From 5198f1d834729276295b1914cc84578049bb219b Mon Sep 17 00:00:00 2001 From: march <106459595+marchc1@users.noreply.github.com> Date: Wed, 7 Oct 2026 12:23:27 -0700 Subject: [PATCH 01/20] Add stringtables in InstallStringTableCallback --- Game.Client/HLClient.cs | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/Game.Client/HLClient.cs b/Game.Client/HLClient.cs index 873fe9933..c7f6221d5 100644 --- a/Game.Client/HLClient.cs +++ b/Game.Client/HLClient.cs @@ -1,5 +1,6 @@ using CommunityToolkit.HighPerformance; +using Game.Client.GarrysMod; using Game.Client.HL2; using Game.Client.HUD; using Game.Shared; @@ -266,16 +267,28 @@ public void View_Render(ViewRects rects) { public void InstallStringTableCallback(ReadOnlySpan tableName) { switch (tableName) { - case "networkstring": + case "VguiScreen": Warning("FIXME: VguiScreen string table not being processed yet\n"); break; + case "Materials": Warning("FIXME: Materials string table not being processed yet\n"); break; + case "EffectDispatch": Warning("FIXME: EffectDispatch string table not being processed yet\n"); break; + // I don't think InfoPanel is a thing? + case "Scenes": + g_pStringTableClientSideChoreoScenes = networkstringtable.FindTable(tableName); + break; + case "ParticleEffectNames": Warning("FIXME: ParticleEffectNames string table not being processed yet\n"); break; +#if GMOD_DLL + case Protocol.NETWORKSTRING_TABLENAME: Game.Client.GarrysMod.NetworkString.Install(); break; + case Protocol.NETWORKVARS_TABLENAME: + Game.Client.GarrysMod.NetworkVarNames.Install(); + break; case Protocol.CLIENT_LUA_FILES_TABLENAME: Game.Client.GarrysMod.GModDataPack.DataPack().Initialize(); break; - case "Scenes": - g_pStringTableClientSideChoreoScenes = networkstringtable.FindTable(tableName); - break; +#endif } + // GarrysMod.GarrysMod.StringTable.Install(); + GameRulesRegister.InstallStringTableCallback_GameRules(); } From 6c05f313503a60b5ddba675c873a923b57abd1ac Mon Sep 17 00:00:00 2001 From: march <106459595+marchc1@users.noreply.github.com> Date: Wed, 7 Oct 2026 12:33:27 -0700 Subject: [PATCH 02/20] IGMODDataTable reworking --- Source.Common/DtRecv.cs | 6 + Source.Common/DtSend.cs | 3 +- Source.Common/GModTable.cs | 174 ---------------------- Source.Common/GarrysMod/GMODVariant.cs | 172 ++++++++++++++++++--- Source.Common/GarrysMod/IGMODDataTable.cs | 14 ++ 5 files changed, 176 insertions(+), 193 deletions(-) delete mode 100644 Source.Common/GModTable.cs diff --git a/Source.Common/DtRecv.cs b/Source.Common/DtRecv.cs index 14e12f16f..1f72b7bdc 100644 --- a/Source.Common/DtRecv.cs +++ b/Source.Common/DtRecv.cs @@ -506,6 +506,9 @@ public void DumpNames() { public class SendTablePrecalc { +#if GMOD_DLL + public SendProp? GMODTableProp; +#endif public SendTablePrecalc() { } @@ -531,6 +534,9 @@ public bool SetupFlatPropertyArray() { Props.Clear(); Props.AddRange(bhs.Props[..bhs.NumProps]); +#if GMOD_DLL + GMODTableProp = Props.FirstOrDefault(static p => p.Type == SendPropType.GModTable); +#endif DataTableProps.Clear(); DataTableProps.AddRange(bhs.DataTableProps[..bhs.NumDataTableProps]); PropProxyIndices.Clear(); PropProxyIndices.AddRange(bhs.PropProxyIndices[..bhs.NumProps]); diff --git a/Source.Common/DtSend.cs b/Source.Common/DtSend.cs index bc008877a..06786b541 100644 --- a/Source.Common/DtSend.cs +++ b/Source.Common/DtSend.cs @@ -1,6 +1,7 @@ using CommunityToolkit.HighPerformance; using Source.Common.Bitbuffers; +using Source.Common.GarrysMod; using Source.Common.Engine; using Source.Common.Mathematics; @@ -580,7 +581,7 @@ public static SendProp SendPropGModTable(IFieldAccessor field) { ret.Type = SendPropType.GModTable; ret.FieldInfo = field; - ret.SetProxyFn(static (prop, baseData, fieldInfo, ref value, element, objectID) => { value.Data = fieldInfo.GetValue(baseData); }); + ret.SetProxyFn(static (prop, baseData, fieldInfo, ref value, element, objectID) => { value.Data = fieldInfo.GetValue(baseData); }); return ret; } diff --git a/Source.Common/GModTable.cs b/Source.Common/GModTable.cs deleted file mode 100644 index 48625e4ad..000000000 --- a/Source.Common/GModTable.cs +++ /dev/null @@ -1,174 +0,0 @@ -#if GMOD_DLL -using Source.Common.Bitbuffers; -using Source.Common.GarrysMod; -using Source.Common.Mathematics; - -using System.Numerics; -using System.Reflection; -using System.Runtime.CompilerServices; - -namespace Source.Common; - -public delegate void GModReadFn(bf_read buf, ref GMODVariant dvariant); -public delegate bool GModCompareFn(bf_read buf1, bf_read buf2); -public delegate void GModSkipFn(bf_read buf); - -public struct GmodTableTypeFns { - public static readonly GmodTableTypeFns Empty = new( - (bf_read _, ref GMODVariant _) => Warning("Attempted to call a Read function with an invalid GModTableType!\n"), - (_, _) => { Warning("Attempted to call a Compare function with an invalid GModTableType!\n"); return false; }, - (_) => Warning("Attempted to call a Skip function with an invalid GModTableType!\n") - ); - - public GModReadFn Read; - public GModCompareFn Compare; - public GModSkipFn Skip; - public GmodTableTypeFns(GModReadFn read, GModCompareFn compare, GModSkipFn skip) { - Read = read; - Compare = compare; - Skip = skip; - } - - public static readonly GmodTableTypeFns[] Fns = [ - new(Float_Read, Float_Compare, Float_Skip), - new(Int_Read, Int_Compare, Int_Skip), - new(Bool_Read, Bool_Compare, Bool_Skip), - new(Vector_Read, Vector_Compare, Vector_Skip), - new(Angle_Read, Angle_Compare, Angle_Skip), - new(Entity_Read, Entity_Compare, Entity_Skip), - new(String_Read, String_Compare, String_Skip), - ]; - - public static ref readonly GmodTableTypeFns Get(GMODVariantType type) => ref Get((int)type); - public static ref readonly GmodTableTypeFns Get(int type) { - int ptr = type - 1; - if (ptr < 0 || ptr >= Fns.Length) - return ref Empty; - - return ref Fns[ptr]; - } - - static void Float_Read(bf_read buf, ref GMODVariant dvariant) => dvariant.Float = buf.ReadBitFloat(); - static bool Float_Compare(bf_read buf1, bf_read buf2) => buf1.ReadBitFloat() != buf2.ReadBitFloat(); - static void Float_Skip(bf_read buf) => buf.SeekRelative(32); - - static void Int_Read(bf_read buf, ref GMODVariant dvariant) => dvariant.Int = (int)buf.ReadUBitLong(32); - static bool Int_Compare(bf_read buf1, bf_read buf2) => buf1.ReadUBitLong(32) != buf2.ReadUBitLong(32); - static void Int_Skip(bf_read buf) => buf.SeekRelative(32); - - static void Bool_Read(bf_read buf, ref GMODVariant dvariant) => dvariant.Int = buf.ReadOneBit(); - static bool Bool_Compare(bf_read buf1, bf_read buf2) => buf1.ReadBool() != buf2.ReadBool(); - static void Bool_Skip(bf_read buf) => buf.SeekRelative(1); - - static void Vector_Read(bf_read buf, ref GMODVariant dvariant) => dvariant.Vec = new(buf.ReadBitFloat(), buf.ReadBitFloat(), buf.ReadBitFloat()); - static bool Vector_Compare(bf_read buf1, bf_read buf2) { - float x1 = buf1.ReadBitFloat(), y1 = buf1.ReadBitFloat(), z1 = buf1.ReadBitFloat(); - float x2 = buf2.ReadBitFloat(), y2 = buf2.ReadBitFloat(), z2 = buf2.ReadBitFloat(); - return x1 != x2 || y1 != y2 || z1 != z2; - } - static void Vector_Skip(bf_read buf) => buf.SeekRelative(32 * 3); - - static void Angle_Read(bf_read buf, ref GMODVariant dvariant) => dvariant.Ang = new(buf.ReadBitFloat(), buf.ReadBitFloat(), buf.ReadBitFloat()); - static bool Angle_Compare(bf_read buf1, bf_read buf2) => Vector_Compare(buf1, buf2); - static void Angle_Skip(bf_read buf) => Vector_Skip(buf); - - static void Entity_Read(bf_read buf, ref GMODVariant dvariant) { - uint val = buf.ReadUBitLong(Constants.NUM_NETWORKED_EHANDLE_BITS); - - if(val != Constants.INVALID_NETWORKED_EHANDLE_VALUE) { - uint entity = val & ((1 << Constants.MAX_EDICT_BITS) - 1); - uint serialNum = val >> Constants.MAX_EDICT_BITS; - dvariant.Ent.Init((int)entity, (int)serialNum); - } - } - static bool Entity_Compare(bf_read buf1, bf_read buf2) => buf1.ReadUBitLong(Constants.NUM_NETWORKED_EHANDLE_BITS) != buf2.ReadUBitLong(Constants.NUM_NETWORKED_EHANDLE_BITS); - static void Entity_Skip(bf_read buf) => buf.SeekRelative(Constants.NUM_NETWORKED_EHANDLE_BITS); - - static void String_Read(bf_read buf, ref GMODVariant dvariant) { - int len = (int)buf.ReadUBitLong(Constants.DT_MAX_STRING_BITS); - Span data = stackalloc char[len]; - buf.ReadString(data); - dvariant.String = new(data); - } - static bool String_Compare(bf_read buf1, bf_read buf2) { - int len1 = (int)buf1.ReadUBitLong(Constants.DT_MAX_STRING_BITS); - int len2 = (int)buf2.ReadUBitLong(Constants.DT_MAX_STRING_BITS); - - if(len1 == len2) { - if (len1 == 0) - return false; - - Span data1 = stackalloc char[len1]; - Span data2 = stackalloc char[len2]; - - buf1.ReadString(data1); - buf2.ReadString(data2); - - return !((ReadOnlySpan)data1).Equals(data2, StringComparison.Ordinal); - } - else { - buf1.SeekRelative(len1 * 8); - buf1.SeekRelative(len2 * 8); - return true; - } - } - static void String_Skip(bf_read buf) => buf.SeekRelative((int)buf.ReadUBitLong(Constants.DT_MAX_STRING_BITS)); -} - -public class GModTable { - /// - /// Never used but can freely be edited. - /// - public static GMODVariant Empty; - - // This sucks. But I need this function, and its internal to a Dictionary... - static MethodInfo FindValueMethod = typeof(Dictionary).GetMethod("FindValue", (BindingFlags)~0)!; - delegate ref GMODVariant GetRefVariant(int key); - readonly Dictionary Values; - readonly GetRefVariant FindValue; - - public bool IsEmpty() => Values.Count == 0; - - public GModTable() { - Values = []; - FindValue = FindValueMethod.CreateDelegate(Values); - } - - public const int ENTRIES_BITS = 12; - public const int MAX_ENTRIES= 1 << ENTRIES_BITS; - - public const int ENTRY_KEY_BITS = 12; - public const int MAX_ENTRY_KEYS = 1 << ENTRY_KEY_BITS; - - public const int ENTRY_VALUE_TYPE_BITS = 3; - public const int MAX_VALUE_TYPE = 1 << ENTRY_VALUE_TYPE_BITS; - - public static readonly string WARNING_UNDERFLOW = "Attempted to index below 0 on a GModTable!\n"; - public static readonly string WARNING_OVERFLOW = "Attempted to index above " + (MAX_ENTRY_KEYS - 1) + " on a GModTable!\n"; - - public ref GMODVariant this[int index] { - get { - if(index < 0) { - DevWarning(WARNING_UNDERFLOW); - return ref Empty; - } - - if(index >= MAX_ENTRY_KEYS) { - DevWarning(WARNING_OVERFLOW); - return ref Empty; - } - - ref GMODVariant valRef = ref FindValue(index); - if (!Unsafe.IsNullRef(ref valRef)) - return ref valRef; - else { - Values[index] = new(); - return ref FindValue(index); - } - } - } - public void Clear() { - Values.Clear(); - } -} -#endif diff --git a/Source.Common/GarrysMod/GMODVariant.cs b/Source.Common/GarrysMod/GMODVariant.cs index 763da0a85..13fbd744a 100644 --- a/Source.Common/GarrysMod/GMODVariant.cs +++ b/Source.Common/GarrysMod/GMODVariant.cs @@ -1,13 +1,16 @@ using Source.Common.Mathematics; using System.Numerics; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics.X86; +using System.Text; namespace Source.Common.GarrysMod; -public enum GMODVariantType +public enum GMODVariantType : byte { NIL, - Number, + Float, Int, Bool, Vector, @@ -17,22 +20,155 @@ public enum GMODVariantType Count } +[StructLayout(LayoutKind.Explicit)] public struct GMODVariant { - public GMODVariantType Type; - public string? String; - public bool Bool; - public float Float; - public int Int; - public BaseHandle Ent; // Raphael defined this as a short; not sure if that's appropriate though - public Vector3 Vec; - public QAngle Ang; - - - public static implicit operator int(in GMODVariant v) => v.Int; - public static implicit operator bool(in GMODVariant v) => v.Bool; - public static implicit operator float(in GMODVariant v) => v.Float; - public static implicit operator Vector3(in GMODVariant v) => v.Vec; - public static implicit operator QAngle(in GMODVariant v) => v.Ang; - public static implicit operator string?(in GMODVariant v) => v.String; + public const int MAX_STRING_LENGTH = 0x200; + + [FieldOffset(0)] public GMODVariantType Type; + [FieldOffset(4)] public float Float; + [FieldOffset(4)] public int Int; + [FieldOffset(4)] public Vector3 Vec; + [FieldOffset(4)] public QAngle Ang; + [FieldOffset(4)] public ushort StringLength; + [FieldOffset(16)] public string? String; + + public readonly bool IsIntLike => Type is GMODVariantType.Int or GMODVariantType.Bool or GMODVariantType.Entity; + public readonly bool IsFloatLike => Type is GMODVariantType.Float or GMODVariantType.Vector or GMODVariantType.Angle; + + public void SetFloat(float value) { + Type = GMODVariantType.Float; + Vec = new(value, 0, 0); + String = null; + } + + public void SetInt(int value) { + Type = GMODVariantType.Int; + Vec = default; + Int = value; + String = null; + } + + public void SetBool(bool value) { + Type = GMODVariantType.Bool; + Vec = default; + Int = value ? 1 : 0; + String = null; + } + + public void SetVector(in Vector3 value) { + Type = GMODVariantType.Vector; + Vec = value; + String = null; + } + + public void SetAngle(in QAngle value) { + Type = GMODVariantType.Angle; + Ang = value; + String = null; + } + + public void SetEntity(int handle) { + Type = GMODVariantType.Entity; + Vec = default; + Int = handle; + String = null; + } + + public void SetString(ReadOnlySpan value) { + value = value.SliceNullTerminatedString(); + int bytes = Encoding.UTF8.GetByteCount(value); + if (bytes + 1 > MAX_STRING_LENGTH) { + byte[] buffer = new byte[bytes]; + Encoding.UTF8.GetBytes(value, buffer); + String = Encoding.UTF8.GetString(buffer, 0, MAX_STRING_LENGTH - 1); + StringLength = MAX_STRING_LENGTH; + } + else { + String = new string(value); + StringLength = (ushort)(bytes + 1); + } + Type = GMODVariantType.String; + } + + public void Clear() { + Type = GMODVariantType.NIL; + Vec = default; + String = null; + } + + public readonly int ToInt() { + if (IsIntLike) + return Int; + if (IsFloatLike) + return (int)Float; + if (Type == GMODVariantType.String) + return atoi(String); + return 0; + } + + public readonly float ToFloat() { + if (IsIntLike) + return Int; + if (IsFloatLike) + return Float; + if (Type == GMODVariantType.String) + return (float)atof(String); + return 0; + } + + public readonly bool ToBool() { + if (IsIntLike) + return Int != 0; + if (IsFloatLike) + return Float != 0 || float.IsNaN(Float); + if (Type == GMODVariantType.String) { + if (string.IsNullOrEmpty(String) || String.Equals("false", StringComparison.OrdinalIgnoreCase)) + return false; + return atoi(String) != 0; + } + return false; + } + + public readonly Vector3 ToVector() { + if (IsIntLike) { + ReadOnlySpan raw = MemoryMarshal.Cast(new ReadOnlySpan(in Vec)); + return new(raw[0], raw[1], raw[2]); + } + if (IsFloatLike) + return Vec; + if (Type == GMODVariantType.String) { + Span values = stackalloc float[3]; + if (ScanFloats(String, values) == 3) + return new(values[0], values[1], values[2]); + } + return default; + } + + public readonly QAngle ToAngle() { + Vector3 v = ToVector(); + return new(v.X, v.Y, v.Z); + } + + public readonly int ToHandle() { + if (IsIntLike) + return Int; + if (IsFloatLike) + return (int)Float; + if (Type == GMODVariantType.String) + return atoi(String); + return 0; + } + + // todo: I don't like that these use .ToString("G6"), it's not + // gonna be the same string we would've gotten... we really need to + // flesh out the cformatting stuff + public override readonly string ToString() => Type switch { + GMODVariantType.Float => (Float).ToString("G6"), + GMODVariantType.Int or GMODVariantType.Entity => Int.ToString(), + GMODVariantType.Bool => Int != 0 ? "true" : "false", + GMODVariantType.Vector or GMODVariantType.Angle => $"{(Vec.X).ToString("G6")} {(Vec.Y).ToString("G6")} {(Vec.Z).ToString("G6")}", + GMODVariantType.String => String ?? "", + _ => "" + }; } diff --git a/Source.Common/GarrysMod/IGMODDataTable.cs b/Source.Common/GarrysMod/IGMODDataTable.cs index 7554d5465..bbd0a5721 100644 --- a/Source.Common/GarrysMod/IGMODDataTable.cs +++ b/Source.Common/GarrysMod/IGMODDataTable.cs @@ -1,5 +1,19 @@ namespace Source.Common.GarrysMod; +public delegate void GMODDataTableCallbackFn(object? entity, int key, in GMODVariant value); + public interface IGMODDataTable { + int GetKey(int it); + ref readonly GMODVariant GetValue(int it); + void IncrementIterator(ref int it); + ref readonly GMODVariant Get(int key); + void Set(int key, in GMODVariant value); + bool HasKey(int key); + ref readonly GMODVariant GetLocal(ReadOnlySpan name); + void SetLocal(ReadOnlySpan name, in GMODVariant value); + void ClearLocal(ReadOnlySpan name); + void Clear(); + int Begin(); + int End(); } From 1f0f5919ba7228930493002d13abf5f3ea0d7531 Mon Sep 17 00:00:00 2001 From: march <106459595+marchc1@users.noreply.github.com> Date: Wed, 7 Oct 2026 12:46:48 -0700 Subject: [PATCH 03/20] The new GMODDataTable --- Source.Engine/GMODDataTable.cs | 418 +++++++++++++++++++++++++++++++++ 1 file changed, 418 insertions(+) create mode 100644 Source.Engine/GMODDataTable.cs diff --git a/Source.Engine/GMODDataTable.cs b/Source.Engine/GMODDataTable.cs new file mode 100644 index 000000000..0016730fd --- /dev/null +++ b/Source.Engine/GMODDataTable.cs @@ -0,0 +1,418 @@ +#if GMOD_DLL +using Source.Common.Bitbuffers; +using Source.Common.GarrysMod; + +using System.Numerics; +using System.Text; + +namespace Source.Engine; + +public delegate void GMODVariantWriteFn(bf_write buf, in GMODVariant variant); +public delegate void GMODVariantReadFn(bf_read buf, ref GMODVariant variant); +public delegate void GMODVariantSkipFn(bf_read buf); +public delegate bool GMODVariantCompareFn(bf_read buf1, bf_read buf2); + +public readonly record struct GMODVariantInfo(GMODVariantWriteFn? Write, GMODVariantReadFn? Read, GMODVariantSkipFn? Skip, GMODVariantCompareFn? Compare); + +public class GMODDataTable(GMODDataTableCallbackFn? callback) : IGMODDataTable +{ + public const int ENTRIES_BITS = 12; + public const int KEY_BITS = 12; + public const int TYPE_BITS = 3; + public const int STRING_LENGTH_BITS = 9; + public const int MAX_CHANGED_KEYS = 0x400; + + class Entry + { + public int Tick; + public GMODVariant Value; + public int Order; + } + + [ThreadStatic] public static GMODDataTable? s_CurrentTable; + [ThreadStatic] public static int s_TargetTick; + [ThreadStatic] public static int s_ReferenceTick; + + static GMODVariant s_Null; + static GMODVariant s_NullLocal; + + readonly SortedList Entries = []; + readonly Dictionary Locals = new(StringComparer.OrdinalIgnoreCase); + bool FullUpdate = true; + int NextOrder; + + public int GetKey(int it) => Entries.Keys[it]; + public ref readonly GMODVariant GetValue(int it) => ref Entries.Values[it].Value; + public void IncrementIterator(ref int it) => it = it + 1 < Entries.Count ? it + 1 : End(); + public int Begin() => Entries.Count > 0 ? 0 : End(); + public int End() => 0xFFFF; + + public ref readonly GMODVariant Get(int key) { + if (Entries.TryGetValue((ushort)key, out Entry? entry)) + return ref entry.Value; + s_Null = default; + return ref s_Null; + } + + Entry FindOrInsert(ushort key) { + if (!Entries.TryGetValue(key, out Entry? entry)) { + entry = new() { Order = NextOrder++ }; + Entries.Add(key, entry); + } + return entry; + } + + public void Set(int key, in GMODVariant value) => FindOrInsert((ushort)key).Value = value; + + public bool HasKey(int key) => Entries.ContainsKey((ushort)key); + + public ref readonly GMODVariant GetLocal(ReadOnlySpan name) { + s_NullLocal = default; + if (name == null) + return ref s_NullLocal; + if (Locals.GetAlternateLookup>().TryGetValue(name, out GMODVariant value)) + s_NullLocal = value; + return ref s_NullLocal; + } + + public void SetLocal(ReadOnlySpan name, in GMODVariant value) => Locals[new string(name)] = value; + + public void ClearLocal(ReadOnlySpan name) { + if (name == null) + return; + Locals.GetAlternateLookup>().Remove(name); + } + + public void Clear() { + Entries.Clear(); + Locals.Clear(); + } + + public bool IsEmpty() => Entries.Count == 0; + + public void Encode(object? entity, bf_write buf) { + buf.WriteUBitLong((uint)Entries.Count, ENTRIES_BITS); + buf.WriteOneBit(0); + + for (int it = Begin(); it != End(); IncrementIterator(ref it)) { + Entry entry = Entries.Values[it]; + buf.WriteUBitLong(Entries.Keys[it], KEY_BITS); + int type = (int)entry.Value.Type; + if (type < 0 || type > 7) + continue; + buf.WriteUBitLong((uint)type, TYPE_BITS); + s_VariantInfo[type].Write?.Invoke(buf, entry.Value); + } + } + + public void Decode(object? entity, bf_read buf) { + int count = (int)buf.ReadUBitLong(ENTRIES_BITS); + if (buf.ReadOneBit() != 0) + Entries.Clear(); + + for (int i = 0; i < count; i++) { + ushort key = (ushort)buf.ReadUBitLong(KEY_BITS); + int type = (int)buf.ReadUBitLong(TYPE_BITS); + + GMODVariant value = default; + s_VariantInfo[type].Read?.Invoke(buf, ref value); + + callback?.Invoke(entity, key, in value); + + Entry entry = FindOrInsert(key); + entry.Tick = 0; + entry.Value = value; + } + } + + public static bool Skip(bf_read buf) { + int count = (int)buf.ReadUBitLong(ENTRIES_BITS); + bool empty = count == 0; + buf.SeekRelative(1); + for (int i = 0; i < count; i++) { + buf.SeekRelative(KEY_BITS); + SkipVariant(buf); + } + return empty; + } + + static void SkipVariant(bf_read buf) { + int type = (int)buf.ReadUBitLong(TYPE_BITS); + s_VariantInfo[type].Skip?.Invoke(buf); + } + + static int NextIndex(bf_read buf, ref int remaining) { + remaining--; + return (int)buf.ReadUBitLong(KEY_BITS); + } + + public static bool Compare(bf_read p1, bf_read p2, GMODDataTable? dt, int tick) { + int n1 = (int)p1.ReadUBitLong(ENTRIES_BITS); + int n2 = (int)p2.ReadUBitLong(ENTRIES_BITS); + int n2Start = n2; + p1.SeekRelative(1); + p2.SeekRelative(1); + int p2Start = p2.BitsRead; + + int k1 = n1 > 0 ? NextIndex(p1, ref n1) : 0xFFFF; + bool changed = k1 != 0xFFFF; + int k2 = n2 > 0 ? NextIndex(p2, ref n2) : 0xFFFF; + bool removed; + + if (changed && k2 == 0xFFFF) + removed = true; + else { + removed = false; + changed = false; + if (k2 != 0xFFFF) { + int diffs = 0; + while (true) { + bool countChange; + if (k1 < k2 && !removed) { + p2.Seek(p2Start); + removed = true; + n2 = n2Start; + countChange = false; + } + else if (k1 == k2 && !removed) { + int t1 = (int)p1.ReadUBitLong(TYPE_BITS); + int t2 = (int)p2.ReadUBitLong(TYPE_BITS); + if (t1 != t2) { + s_VariantInfo[t1].Skip?.Invoke(p1); + s_VariantInfo[t2].Skip?.Invoke(p2); + } + else if (s_VariantInfo[t1].Compare == null || !s_VariantInfo[t1].Compare!(p1, p2)) + k2 = 0xFFFF; + + k1 = n1 > 0 ? NextIndex(p1, ref n1) : 0xFFFF; + if (k2 == 0xFFFF) { + removed = false; + countChange = false; + } + else + countChange = true; + } + else { + SkipVariant(p2); + countChange = true; + } + + if (countChange) { + diffs++; + if (dt != null) { + if (dt.Entries.TryGetValue((ushort)k2, out Entry? entry)) + entry.Tick = tick; + else + Error($"GMODDataTable: Invalid key in packed entity data ({k2})"); + } + } + + if (n2 <= 0) + break; + k2 = NextIndex(p2, ref n2); + if (k2 == 0xFFFF) + break; + } + changed = diffs > 0 || removed; + } + } + + while (k1 != 0xFFFF) { + SkipVariant(p1); + if (n1 <= 0) + break; + k1 = NextIndex(p1, ref n1); + } + + if (dt != null) + dt.FullUpdate = removed; + return changed; + } + + public void WriteProps(bf_read input, bf_write output, int referenceTick) { + Span changedKeys = stackalloc int[MAX_CHANGED_KEYS]; + int changedCount = -1; + + if (referenceTick != 0 && !FullUpdate) { + int n = 0; + bool overflow = false; + for (int it = Begin(); it != End(); IncrementIterator(ref it)) { + if (referenceTick < Entries.Values[it].Tick) + changedKeys[n++] = Entries.Keys[it]; + if (n > MAX_CHANGED_KEYS - 1) { + overflow = true; + break; + } + } + if (!overflow) + changedCount = n; + } + + int count = (int)input.ReadUBitLong(ENTRIES_BITS); + input.SeekRelative(1); + + int deltaNum = changedCount == -1 ? count : changedCount; + output.WriteUBitLong((uint)deltaNum, ENTRIES_BITS); + output.WriteOneBit(FullUpdate ? 1 : 0); + + int written = 0; + int changedIndex = 0; + for (int i = 0; i < count; i++) { + int start = input.BitsRead; + int key = (int)input.ReadUBitLong(KEY_BITS); + SkipVariant(input); + int end = input.BitsRead; + + if (changedCount != -1 && (changedIndex >= changedCount || key != changedKeys[changedIndex])) + continue; + + input.Seek(start); + output.WriteBitsFromBuffer(input, end - start); + written++; + changedIndex++; + } + + if (written != deltaNum) + Error("GMODDataTable: writtenNum != deltaNum"); + } + + public void CopyFrom(object? destEntity, object? srcEntity, GMODDataTable src) { + foreach (KeyValuePair pair in src.Entries.OrderBy(x => x.Value.Order)) { + ushort key = pair.Key; + int srcTick = pair.Value.Tick; + GMODVariant value = pair.Value.Value; + + if (value.Type == GMODVariantType.Entity) { + int handle = value.Int; + int serial = (handle >> 14) & 0x3FF; + int index = handle & 0x3FF_F; + int rebuilt = (serial << 14) | index; + if (index != (rebuilt & 0x3FFF) || serial != (rebuilt >> 14)) + Warning($"CBaseHandle::Init got a bad handle! {index} {serial} => {rebuilt & 0x3FFF} {rebuilt >> 14}\n"); + value.SetEntity(rebuilt); + srcTick = 0; + } + + if (Entries.TryGetValue(key, out Entry? existing)) { + if (existing.Value.Type == value.Type && VariantsEqualForCopy(existing.Value, value)) + continue; + callback?.Invoke(destEntity, key, in value); + existing.Tick = srcTick; + existing.Value = value; + } + else { + callback?.Invoke(destEntity, key, in value); + Entry entry = FindOrInsert(key); + entry.Tick = srcTick; + entry.Value = value; + } + } + } + + static bool VariantsEqualForCopy(in GMODVariant a, in GMODVariant b) => a.Type switch { + GMODVariantType.Float => a.Float == b.Float, + GMODVariantType.Int => a.Int == b.Int, + GMODVariantType.Bool => (a.Int != 0) == (b.Int != 0), + GMODVariantType.Vector or GMODVariantType.Angle => a.Vec.X == b.Vec.X && a.Vec.Y == b.Vec.Y && a.Vec.Z == b.Vec.Z, + GMODVariantType.String => a.StringLength == b.StringLength && string.Equals(a.String, b.String, StringComparison.Ordinal), + _ => true + }; + + static void F_Write(bf_write buf, in GMODVariant v) => buf.WriteFloat(v.ToFloat()); + static void F_Read(bf_read buf, ref GMODVariant v) => v.SetFloat(buf.ReadFloat()); + static void F_Skip(bf_read buf) => buf.SeekRelative(32); + static bool F_Compare(bf_read a, bf_read b) => a.ReadFloat() != b.ReadFloat(); + + static void I_Write(bf_write buf, in GMODVariant v) => buf.WriteUBitLong((uint)v.ToInt(), 32); + static void I_Read(bf_read buf, ref GMODVariant v) => v.SetInt((int)buf.ReadUBitLong(32)); + static void I_Skip(bf_read buf) => buf.SeekRelative(32); + static bool I_Compare(bf_read a, bf_read b) => a.ReadUBitLong(32) != b.ReadUBitLong(32); + + static void B_Write(bf_write buf, in GMODVariant v) => buf.WriteOneBit(v.ToBool() ? 1 : 0); + static void B_Read(bf_read buf, ref GMODVariant v) => v.SetBool(buf.ReadOneBit() != 0); + static void B_Skip(bf_read buf) => buf.SeekRelative(1); + static bool B_Compare(bf_read a, bf_read b) => a.ReadOneBit() != b.ReadOneBit(); + + static void V_Write(bf_write buf, in GMODVariant v) { + Vector3 vec = v.ToVector(); + buf.WriteFloat(vec.X); + buf.WriteFloat(vec.Y); + buf.WriteFloat(vec.Z); + } + static void V_Read(bf_read buf, ref GMODVariant v) { + float x = buf.ReadFloat(), y = buf.ReadFloat(), z = buf.ReadFloat(); + v.SetVector(new(x, y, z)); + } + static void A_Read(bf_read buf, ref GMODVariant v) { + float x = buf.ReadFloat(), y = buf.ReadFloat(), z = buf.ReadFloat(); + v.SetAngle(new(x, y, z)); + } + static void V_Skip(bf_read buf) => buf.SeekRelative(96); + static bool V_Compare(bf_read a, bf_read b) { + float x1 = a.ReadFloat(), y1 = a.ReadFloat(), z1 = a.ReadFloat(); + float x2 = b.ReadFloat(), y2 = b.ReadFloat(), z2 = b.ReadFloat(); + return x1 != x2 || y1 != y2 || z1 != z2; + } + + static void E_Write(bf_write buf, in GMODVariant v) { + int h = v.IsIntLike ? v.Int : v.IsFloatLike ? GMODVariant.cvttss2si(v.Float) : v.Type == GMODVariantType.String ? atoi(v.String) : 0; + uint encoded = (uint)((h & 0x3FFF) | ((h >> 1) & 0x7FE000)); + buf.WriteUBitLong(encoded, Constants.NUM_NETWORKED_EHANDLE_BITS); + } + static void E_Read(bf_read buf, ref GMODVariant v) { + uint value = buf.ReadUBitLong(Constants.NUM_NETWORKED_EHANDLE_BITS); + int handle = -1; + if ((value & 0x7FFFFF) != 0x7FFFFF) { + int index = (int)(value & 0x1FFF); + int serial = (int)(value >> 13); + handle = (serial << 14) | index; + if (index != (handle & 0x3FFF) || serial != (handle >> 14)) + Warning($"CBaseHandle::Init got a bad handle! {index} {serial} => {handle & 0x3FFF} {handle >> 14}\n"); + } + v.SetEntity(handle); + } + static void E_Skip(bf_read buf) => buf.SeekRelative(Constants.NUM_NETWORKED_EHANDLE_BITS); + static bool E_Compare(bf_read a, bf_read b) => a.ReadUBitLong(Constants.NUM_NETWORKED_EHANDLE_BITS) != b.ReadUBitLong(Constants.NUM_NETWORKED_EHANDLE_BITS); + + static void S_Write(bf_write buf, in GMODVariant v) { + string str = v.Type == GMODVariantType.Entity ? "" : v.ToString(); + byte[] bytes = Encoding.UTF8.GetBytes(str); + int len = bytes.Length < 0x200 ? bytes.Length : 0x1FF; + buf.WriteUBitLong((uint)len, STRING_LENGTH_BITS); + buf.WriteBytes(bytes.AsSpan(0, len)); + } + static void S_Read(bf_read buf, ref GMODVariant v) { + int len = (int)buf.ReadUBitLong(STRING_LENGTH_BITS); + Span bytes = stackalloc byte[len]; + buf.ReadBytes(bytes); + int nul = bytes.IndexOf((byte)0); + v.SetString(Encoding.UTF8.GetString(nul >= 0 ? bytes[..nul] : bytes)); + } + static void S_Skip(bf_read buf) => buf.SeekRelative((int)buf.ReadUBitLong(STRING_LENGTH_BITS) * 8); + static bool S_Compare(bf_read a, bf_read b) { + int len1 = (int)a.ReadUBitLong(STRING_LENGTH_BITS); + int len2 = (int)b.ReadUBitLong(STRING_LENGTH_BITS); + if (len1 != len2) { + a.SeekRelative(len1 * 8); + b.SeekRelative(len2 * 8); + return true; + } + Span s1 = stackalloc byte[len1]; + Span s2 = stackalloc byte[len2]; + a.ReadBytes(s1); + b.ReadBytes(s2); + return !s1.SequenceEqual(s2); + } + + public static readonly GMODVariantInfo[] s_VariantInfo = [ + new(null, null, null, null), + new(F_Write, F_Read, F_Skip, F_Compare), + new(I_Write, I_Read, I_Skip, I_Compare), + new(B_Write, B_Read, B_Skip, B_Compare), + new(V_Write, V_Read, V_Skip, V_Compare), + new(V_Write, A_Read, V_Skip, V_Compare), + new(E_Write, E_Read, E_Skip, E_Compare), + new(S_Write, S_Read, S_Skip, S_Compare), + ]; +} +#endif From ae292343635220c0d3f17d0d4a6398ba7da42f76 Mon Sep 17 00:00:00 2001 From: march <106459595+marchc1@users.noreply.github.com> Date: Wed, 7 Oct 2026 12:47:15 -0700 Subject: [PATCH 04/20] engine datatable wiring --- Source.Common/Client/IEngineClient.cs | 4 +- Source.Engine/CdllEngineInterface.cs | 8 +-- Source.Engine/DtEncode.cs | 87 +++++++------------------- Source.Engine/EngineSendTable.cs | 10 +++ Source.Engine/EngineServerImpl.cs | 8 +-- Source.Engine/PackedEntity.cs | 6 ++ Source.Engine/Server/EntsWrite.cs | 20 ++++++ Source.Engine/Server/PackedEntities.cs | 15 +++++ 8 files changed, 80 insertions(+), 78 deletions(-) diff --git a/Source.Common/Client/IEngineClient.cs b/Source.Common/Client/IEngineClient.cs index 0cad5a552..08e41ec99 100644 --- a/Source.Common/Client/IEngineClient.cs +++ b/Source.Common/Client/IEngineClient.cs @@ -18,8 +18,6 @@ namespace Source.Common.Client; -public delegate void GMOD_CreateDataTableFn(int idx, ref GMODVariant variant); - /// /// Engine player info. (replica of player_info_s) /// @@ -478,7 +476,7 @@ public interface IEngineClient void GMOD_BrushMaterialOverride(IMaterial? matOverride); void GMOD_R_RedownloadAllLightmaps(bool unk); void GMOD_RawClientCmd_Unrestricted( ReadOnlySpan command ); - IGMODDataTable GMOD_CreateDataTable(GMOD_CreateDataTableFn fnCallback); + IGMODDataTable GMOD_CreateDataTable(GMODDataTableCallbackFn fnCallback); void GMOD_DestroyDataTable(IGMODDataTable dataTable); MDLHandle_t GMOD_LoadModel( ReadOnlySpan path ); void GMOD_DecalRemoveEntity(int index); diff --git a/Source.Engine/CdllEngineInterface.cs b/Source.Engine/CdllEngineInterface.cs index 4b49b24f2..2730937ee 100644 --- a/Source.Engine/CdllEngineInterface.cs +++ b/Source.Engine/CdllEngineInterface.cs @@ -660,13 +660,9 @@ public void GMOD_RawClientCmd_Unrestricted(ReadOnlySpan command) { throw new NotImplementedException(); } - public IGMODDataTable GMOD_CreateDataTable(GMOD_CreateDataTableFn fnCallback) { - throw new NotImplementedException(); - } + public IGMODDataTable GMOD_CreateDataTable(GMODDataTableCallbackFn fnCallback) => new GMODDataTable(fnCallback); - public void GMOD_DestroyDataTable(IGMODDataTable dataTable) { - throw new NotImplementedException(); - } + public void GMOD_DestroyDataTable(IGMODDataTable dataTable) { } public MDLHandle_t GMOD_LoadModel(ReadOnlySpan path) { throw new NotImplementedException(); diff --git a/Source.Engine/DtEncode.cs b/Source.Engine/DtEncode.cs index 99f28364e..600f539f7 100644 --- a/Source.Engine/DtEncode.cs +++ b/Source.Engine/DtEncode.cs @@ -1,5 +1,6 @@ using Source.Common; using Source.Common.Bitbuffers; +using Source.Common.GarrysMod; using Source.Common.Mathematics; using System.Numerics; @@ -211,7 +212,7 @@ public static void EncodeString(SendProp prop, string? incoming, bf_write outBuf new(Array_Encode, Array_Decode, Array_CompareDeltas, Array_FastCopy, Array_GetTypeNameString, Array_IsZero, Array_DecodeZero, Array_IsEncodedZero, Array_SkipProp), new(DataTable_Encode, DataTable_Decode, DataTable_CompareDeltas, Generic_FastCopy, DataTable_GetTypeNameString, DataTable_IsZero, DataTable_DecodeZero, DataTable_IsEncodedZero, DataTable_SkipProp), #if GMOD_DLL - new(GModTable_Encode, GModTable_Decode, GModTable_CompareDeltas, Generic_FastCopy, GModTable_GetTypeNameString, GModTable_IsZero, GModTable_DecodeZero, GModTable_IsEncodedZero, GModTable_SkipProp), + new(GModTable_Encode, GModTable_Decode, GModTable_CompareDeltas, GModTable_FastCopy, GModTable_GetTypeNameString, GModTable_IsZero, GModTable_DecodeZero, GModTable_IsEncodedZero, GModTable_SkipProp), #endif ]; @@ -680,75 +681,35 @@ public static void Array_SkipProp(SendProp prop, bf_read p) { #region SendPropType.GModTable #if GMOD_DLL public static int GModTable_CompareDeltas(SendProp prop, bf_read p1, bf_read p2) { - int start1 = p1.BitsRead; - GModTable_Skip(p1); - int bits1 = p1.BitsRead - start1; - - int start2 = p2.BitsRead; - GModTable_Skip(p2); - int bits2 = p2.BitsRead - start2; - - if (bits1 != bits2) - return 1; - - int end1 = p1.BitsRead, end2 = p2.BitsRead; - p1.Seek(start1); - p2.Seek(start2); - - int different = 0; - for (int remaining = bits1; remaining > 0 && different == 0; remaining -= 32) - different = p1.CompareBits(p2, Math.Min(32, remaining)) ? 1 : 0; - - p1.Seek(end1); - p2.Seek(end2); - return different; + GMODDataTable? dt = GMODDataTable.s_CurrentTable; + int tick = GMODDataTable.s_TargetTick; + if (dt == null) + Error("GMODTable_CompareDeltas: dt is null!"); + return GMODDataTable.Compare(p1, p2, dt, tick) ? 1 : 0; } public static void GModTable_Decode(ref DecodeInfo decodeInfo) { - var gmodtable = decodeInfo.FieldInfo.GetValue(decodeInfo.Object); - - int len = (int)decodeInfo.In.ReadUBitLong(GModTable.ENTRIES_BITS); - bool clear = decodeInfo.In.ReadBool(); - - if (clear) - gmodtable.Clear(); - - for (int i = 0; i < len; i++) { - int key = (int)decodeInfo.In.ReadUBitLong(GModTable.ENTRY_KEY_BITS); - int valueType = (int)decodeInfo.In.ReadUBitLong(GModTable.ENTRY_VALUE_TYPE_BITS); - if (valueType != 0) - GmodTableTypeFns.Get(valueType).Read(decodeInfo.In, ref gmodtable[key]); - } - } - - private static bool GModTable_Skip(bf_read p) { - int len = (int)p.ReadUBitLong(GModTable.ENTRIES_BITS); - bool clear = p.ReadBool(); - - for (int i = 0; i < len; i++) { - int key = (int)p.ReadUBitLong(GModTable.ENTRY_KEY_BITS); - int valueType = (int)p.ReadUBitLong(GModTable.ENTRY_VALUE_TYPE_BITS); - - if (valueType != 0) - GmodTableTypeFns.Get(valueType).Skip(p); - } - - return len == 0; + GMODDataTable? dt = decodeInfo.FieldInfo?.GetValue(decodeInfo.Object) as GMODDataTable; + if (dt != null) + dt.Decode(decodeInfo.Object, decodeInfo.In); + else + GMODDataTable.Skip(decodeInfo.In); } - public static void GModTable_DecodeZero(ref DecodeInfo info) { } public static void GModTable_Encode(object instance, ref DVariant var, SendProp prop, bf_write writeOut, int objectID) { - GModTable? gmodtable = (GModTable?)var.Data; - - writeOut.WriteUBitLong(0, GModTable.ENTRIES_BITS); - writeOut.WriteOneBit(0); + GMODDataTable? dt = var.Data as GMODDataTable; + if (dt == null) + Error("GMODTable_Encode: dt is null!"); + dt!.Encode(instance, writeOut); } - public static ReadOnlySpan GModTable_GetTypeNameString() => "DPT_GMODTable"; - public static bool GModTable_IsEncodedZero(SendProp prop, bf_read p) => GModTable_Skip(p); - public static bool GModTable_IsZero(object instance, ref DVariant var, SendProp prop) { - GModTable? dt = (GModTable?)var.Data; - return dt!.IsEmpty(); + public static void GModTable_FastCopy(SendProp sendProp, RecvProp recvProp, object sendData, IFieldAccessor sendFieldInfo, object recvData, IFieldAccessor recvFieldInfo, int objectID) { + GMODDataTable src = (GMODDataTable)sendFieldInfo.GetValue(sendData)!; + GMODDataTable dest = (GMODDataTable)recvFieldInfo.GetValue(recvData)!; + dest.CopyFrom(recvData, sendData, src); } - public static void GModTable_SkipProp(SendProp prop, bf_read p) => GModTable_Skip(p); + public static ReadOnlySpan GModTable_GetTypeNameString() => "DPT_GMODTable"; + public static bool GModTable_IsEncodedZero(SendProp prop, bf_read p) => GMODDataTable.Skip(p); + public static bool GModTable_IsZero(object instance, ref DVariant var, SendProp prop) => ((GMODDataTable)var.Data!).IsEmpty(); + public static void GModTable_SkipProp(SendProp prop, bf_read p) => GMODDataTable.Skip(p); #endif #endregion } diff --git a/Source.Engine/EngineSendTable.cs b/Source.Engine/EngineSendTable.cs index 5189020e1..9ab156bbe 100644 --- a/Source.Engine/EngineSendTable.cs +++ b/Source.Engine/EngineSendTable.cs @@ -199,6 +199,16 @@ public static void WritePropList(SendTable table, byte[]? state, int nBits, bf_w if (prop == (uint)checkProps[i]) { SendProp p = precalc.GetProp((int)prop)!; bitsWriter.WritePropIndex((int)prop); +#if GMOD_DLL + if (p.Type == SendPropType.GModTable) { + GMODDataTable? dt = GMODDataTable.s_CurrentTable; + int referenceTick = GMODDataTable.s_ReferenceTick; + if (dt == null) + Error("SendTable_WritePropList: dt is null???"); + dt!.WriteProps(inputBuf, bitsWriter.GetBitBuf(), referenceTick); + } + else +#endif bitsReader.CopyPropData(bitsWriter.GetBitBuf(), p); prop = bitsReader.ReadNextPropIndex(); } diff --git a/Source.Engine/EngineServerImpl.cs b/Source.Engine/EngineServerImpl.cs index 46b8c59d6..bfe0fc7c4 100644 --- a/Source.Engine/EngineServerImpl.cs +++ b/Source.Engine/EngineServerImpl.cs @@ -1090,13 +1090,9 @@ public void GMOD_RawServerCommand(ReadOnlySpan command) { throw new NotImplementedException(); } - public IGMODDataTable GMOD_CreateDataTable() { - throw new NotImplementedException(); - } + public IGMODDataTable GMOD_CreateDataTable() => new GMODDataTable(null); - public void GMOD_DestroyDataTable(IGMODDataTable dataTable) { - throw new NotImplementedException(); - } + public void GMOD_DestroyDataTable(IGMODDataTable dataTable) { } public ReadOnlySpan GMOD_GetServerAddress() { throw new NotImplementedException(); diff --git a/Source.Engine/PackedEntity.cs b/Source.Engine/PackedEntity.cs index 9544bcfaa..175a7b0a0 100644 --- a/Source.Engine/PackedEntity.cs +++ b/Source.Engine/PackedEntity.cs @@ -14,6 +14,9 @@ public class PackedEntity : IPoolableObject public int EntityIndex; public int ReferenceCount; +#if GMOD_DLL + public GMODDataTable? GMODDataTable; +#endif readonly List Recipients = []; byte[]? Data; @@ -26,6 +29,9 @@ public void Init() { } public void Reset() { EntityIndex = 0; ReferenceCount = 0; +#if GMOD_DLL + GMODDataTable = null; +#endif Recipients.Clear(); Data = null; Bits = 0; diff --git a/Source.Engine/Server/EntsWrite.cs b/Source.Engine/Server/EntsWrite.cs index fc1acc8de..618fface6 100644 --- a/Source.Engine/Server/EntsWrite.cs +++ b/Source.Engine/Server/EntsWrite.cs @@ -190,11 +190,21 @@ public static void DetermineUpdateType(EntityWriteInfo u) { } if (nCheckProps > 0) { +#if GMOD_DLL + GMODDataTable.s_CurrentTable = u.NewPack.GMODDataTable; + GMODDataTable.s_ReferenceTick = u.FromSnapshot.TickCount; + GMODDataTable.s_TargetTick = u.ToSnapshot.TickCount; +#endif WriteDeltaHeader(u, u.NewEntity, DeltaEncodingFlags.Zero); WritePropsFromPackedEntity(u, checkProps, nCheckProps); u.UpdateType = UpdateType.DeltaEnt; +#if GMOD_DLL + GMODDataTable.s_CurrentTable = null; + GMODDataTable.s_ReferenceTick = 0; + GMODDataTable.s_TargetTick = 0; +#endif } else u.UpdateType = UpdateType.PreserveEnt; @@ -251,7 +261,17 @@ static void WriteEnterPVS(EntityWriteInfo u) { nToBits = u.NewPack.GetNumBits(); } +#if GMOD_DLL + GMODDataTable.s_CurrentTable = u.NewPack.GMODDataTable; + GMODDataTable.s_ReferenceTick = u.Baseline != null ? u.Baseline.TickCount : 0; + GMODDataTable.s_TargetTick = u.ToSnapshot.TickCount; +#endif u.FullProps += WriteAllDeltaProps(entryClass.Table, fromData!, nFromBits, toData!, nToBits, u.NewPack.EntityIndex, u.Buffer); +#if GMOD_DLL + GMODDataTable.s_CurrentTable = null; + GMODDataTable.s_ReferenceTick = 0; + GMODDataTable.s_TargetTick = 0; +#endif if (u.NewEntity == u.OldEntity) u.NextOldEntity(); diff --git a/Source.Engine/Server/PackedEntities.cs b/Source.Engine/Server/PackedEntities.cs index 5f20f08b6..e7c25d91d 100644 --- a/Source.Engine/Server/PackedEntities.cs +++ b/Source.Engine/Server/PackedEntities.cs @@ -2,6 +2,7 @@ using Source.Common.Bitbuffers; using Source.Common.Commands; using Source.Common.Engine; +using Source.Common.GarrysMod; using Source.Common.Networking; using System.Buffers; @@ -89,6 +90,9 @@ public static void PackEntity(int edictId, Edict edict, ServerClass serverClass, int flatProps = EngineSendTable.GetNumFlatProps(sendTable); IChangeFrameList? changeFrame; +#if GMOD_DLL + GMODDataTable? gmodDataTable = sendTable.Precalc!.GMODTableProp?.FieldInfo.GetValue(edict.GetUnknown()!) as GMODDataTable; +#endif // If this entity was previously in there, then it should have a valid IChangeFrameList // which we can delta against to figure out which properties have changed. @@ -101,7 +105,15 @@ public static void PackEntity(int edictId, Edict edict, ServerClass serverClass, int[] deltaProps = new int[Constants.MAX_DATATABLE_PROPS]; +#if GMOD_DLL + GMODDataTable.s_CurrentTable = gmodDataTable; + GMODDataTable.s_TargetTick = (int)snapshot.TickCount; +#endif int changes = EngineSendTable.CalcDelta(sendTable, prevFrame.GetData(), prevFrame.GetNumBits(), packedData, writeBuf.BitsWritten, deltaProps, Constants.MAX_DATATABLE_PROPS, edictId); +#if GMOD_DLL + GMODDataTable.s_CurrentTable = null; + GMODDataTable.s_TargetTick = 0; +#endif // If it's non-manual-mode, but we detect that there are no changes here, then just // use the previous snapshot if it's available (as though the entity were manual mode). @@ -157,6 +169,9 @@ public static void PackEntity(int edictId, Edict edict, ServerClass serverClass, // Now make a PackedEntity and store the new packed data in there. PackedEntity packedEntity = frameSnapshotManager.CreatePackedEntity(snapshot, edictId); packedEntity.SetChangeFrameList(changeFrame); +#if GMOD_DLL + packedEntity.GMODDataTable = gmodDataTable; +#endif packedEntity.SetServerAndClientClass(serverClass, null); packedEntity.AllocAndCopyPadded(packedData); packedEntity.SetRecipients(recip); From b57a25abf0190fa200d4bd113b5044849d42ba5b Mon Sep 17 00:00:00 2001 From: march <106459595+marchc1@users.noreply.github.com> Date: Wed, 7 Oct 2026 12:48:21 -0700 Subject: [PATCH 05/20] add missing network table names --- Source.Common/Networking/Protocol.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Source.Common/Networking/Protocol.cs b/Source.Common/Networking/Protocol.cs index d80f9e597..700fa6b0a 100644 --- a/Source.Common/Networking/Protocol.cs +++ b/Source.Common/Networking/Protocol.cs @@ -244,6 +244,8 @@ public static class Protocol public const string DOWNLOADABLE_FILE_TABLENAME = "downloadables"; public const string DYNAMIC_MODELS_TABLENAME = "DynamicModels"; #if GMOD_DLL + public const string NETWORKSTRING_TABLENAME = "networkstring"; + public const string NETWORKVARS_TABLENAME = "networkvars"; public const string CLIENT_LUA_FILES_TABLENAME = "client_lua_files"; #endif From 91c69bb7e5fb0ab56cdaa3248745b9cb6eff08d3 Mon Sep 17 00:00:00 2001 From: march <106459595+marchc1@users.noreply.github.com> Date: Wed, 7 Oct 2026 12:49:10 -0700 Subject: [PATCH 06/20] LuaNetworkedVars impl + usermessage register fn --- Game.Shared/GarrysMod/LuaNetworkedVars.cs | 479 ++++++++++++++++++++++ Game.Shared/HL2/UserMessages.cs | 6 +- 2 files changed, 484 insertions(+), 1 deletion(-) create mode 100644 Game.Shared/GarrysMod/LuaNetworkedVars.cs diff --git a/Game.Shared/GarrysMod/LuaNetworkedVars.cs b/Game.Shared/GarrysMod/LuaNetworkedVars.cs new file mode 100644 index 000000000..cd0056156 --- /dev/null +++ b/Game.Shared/GarrysMod/LuaNetworkedVars.cs @@ -0,0 +1,479 @@ +#if CLIENT_DLL || GAME_DLL +using Game.Shared; + +using Source; +using Source.Common; +using Source.Common.Bitbuffers; +using Source.Common.Commands; +using Source.Common.GarrysMod.Lua; +using Source.Common.Mathematics; + +using System.Numerics; +using System.Text; + +#if CLIENT_DLL +namespace Game.Client.GarrysMod; +#else +namespace Game.Server.GarrysMod; +#endif + +public class LuaNetworkedVars +{ + public class LuaNetworkedVar + { + public readonly LuaObject Value = new(); + public readonly LuaObject Proxy = new(); +#if CLIENT_DLL + public float Time = -1.0f; +#else + public float NextUpdate = -1.0f; + public int NameID = -1; +#endif + } + + class CaselessStringLessThan : IComparer + { + public static readonly CaselessStringLessThan Instance = new(); + public int Compare(string? x, string? y) => stricmp(x, y); + } + + public class LuaNetworkedEntity + { + public BaseHandle Handle = new(0xFFFFFFFF); + public readonly SortedDictionary Vars = new(CaselessStringLessThan.Instance); + } + + public static LuaNetworkedVars? g_LuaNetworkedVars; + + public const int MAX_ENTITIES = 0x4000; + + readonly LuaNetworkedEntity[] Entities = new LuaNetworkedEntity[MAX_ENTITIES]; + +#if GAME_DLL + public static readonly ConVar lua_networkvar_bytespertick = new("lua_networkvar_bytespertick", "256", 0); + + int BytesSent; + readonly uint[] PlayerCursors = new uint[0x80]; + static float fNextTime; +#endif + + public LuaNetworkedVars() { + for (int i = 0; i < MAX_ENTITIES; i++) + Entities[i] = new(); +#if GAME_DLL + Array.Fill(PlayerCursors, 0xFFFFFFFF); +#endif + } + + static int stricmp(string? a, string? b) { + a ??= ""; + b ??= ""; + int len = Math.Min(a.Length, b.Length); + for (int i = 0; i < len; i++) { + int ca = char.ToLowerInvariant(a[i]); + int cb = char.ToLowerInvariant(b[i]); + if (ca != cb) + return ca - cb; + } + return a.Length - b.Length; + } + + static BaseEntity? EntityFromHandle(uint handle) => LuaEntity.GetEntityFromHandle(handle); + + public SortedDictionary GetEntityVars(uint handle) { + if (handle == 0xFFFFFFFF || (handle & 0x3FFF) == 0) + return Entities[0].Vars; + + LuaNetworkedEntity slot = Entities[handle & 0x3FFF]; + if ((slot.Handle.Index >> 14) != (handle >> 14)) { + slot.Vars.Clear(); + slot.Handle = new(handle); + } + return slot.Vars; + } + + public void ClearEntity(uint handle) { + LuaNetworkedEntity slot = Entities[handle & 0x3FFF]; + slot.Vars.Clear(); + slot.Handle = new(handle); + } + + public LuaNetworkedVar? FindEntityVar(uint handle, string? name, bool create) { + SortedDictionary vars = GetEntityVars(handle); + if (vars.Count == 0 && !create) + return null; + + name ??= ""; + if (vars.TryGetValue(name, out LuaNetworkedVar? var)) + return var; + + if (!create) + return null; + + var = new(); + vars.Add(new string(name), var); + return var; + } + + public bool GetNetworkedVarBool(BaseEntity? ent, string name, bool def) { + LuaNetworkedVar? var = FindEntityVar(ent != null ? ent.GetRefEHandle().Index : 0xFFFFFFFF, name, false); + if (var == null || var.Value.isNil()) + return def; + return var.Value.isBool() ? var.Value.GetBool() : def; + } + + public Vector3 GetNetworkedVarVector(BaseEntity? ent, string name, in Vector3 def) { + LuaNetworkedVar? var = FindEntityVar(ent != null ? ent.GetRefEHandle().Index : 0xFFFFFFFF, name, false); + if (var == null || var.Value.isNil()) + return def; + return var.Value.GetType() == LuaType.Vector ? var.Value.GetVector() : def; + } + + public float GetNetworkedVarFloat(BaseEntity? ent, string name, float def) { + LuaNetworkedVar? var = FindEntityVar(ent != null ? ent.GetRefEHandle().Index : 0xFFFFFFFF, name, false); + if (var == null || var.Value.isNil()) + return def; + return var.Value.isNumber() ? var.Value.GetFloat() : def; + } + + public int PushNetworkedVar(BaseEntity? ent, string name) { + LuaNetworkedVar? var = FindEntityVar(ent != null ? ent.GetRefEHandle().Index : 0xFFFFFFFF, name, false); + if (var == null || var.Value.isNil()) + return 0; + var.Value.Push(); + return 1; + } + + static void TruncateString(ILuaObject value) { + if (!value.isString()) + return; + string? str = value.GetString(); + if (str == null || str.Length <= 0xC8) + return; + g_Lua!.PushString(str.AsSpan(0, 0xC7)); + value.SetFromStack(-1); + g_Lua.Pop(1); + } + + void CallProxy(LuaNetworkedVar var, uint handle, string name, ILuaObject value) { + if (g_Lua == null || !var.Proxy.isFunction()) + return; + + var.Proxy.Push(); + LuaEntity.Push_Entity(EntityFromHandle(handle)); + g_Lua.PushString(name); + var.Value.Push(); + value.Push(); + g_Lua.CallInternalNoReturns(4); + } + + public void SetNetworkedVar(uint handle, string name, ILuaObject value) { + LuaNetworkedVar? var = FindEntityVar(handle, name, true); + if (var == null) { + Warning($"Setting Networked Var {name} Failed!!\n"); + return; + } + + TruncateString(value); + +#if GAME_DLL + if (g_Lua!.IsEqual(var.Value, value)) + return; + + var.Value.Set(value); + var.NextUpdate = 0; + if (var.NameID == -1) + var.NameID = NetworkString.Add(name); +#else + var.Value.Set(value); + var.Time = (float)gpGlobals.CurTime; +#endif + + CallProxy(var, handle, name, value); + } + + public void SetNetworkedVarProxy(uint handle, string name, ILuaObject func) { + LuaNetworkedVar? var = FindEntityVar(handle, name, true); + if (var == null) { + Warning("Setting Networked Var Proxy Failed!!\n"); + return; + } + var.Proxy.Set(func); + } + + static void FillVarTable(LuaTable table, SortedDictionary vars) { + foreach (KeyValuePair pair in vars) + table.SetMember(pair.Key, pair.Value.Value); + } + + static ILuaObject PopTable(LuaTable table) { + table.Push(); + ILuaObject ret = g_Lua!.GetObject(-1); + g_Lua.Pop(1); + table.UnReference(); + return ret; + } + + public ILuaObject BuildEntityNetworkVarTable(BaseEntity? ent) { + LuaTable result = new(); + SortedDictionary vars = GetEntityVars(ent != null ? ent.GetRefEHandle().Index : 0xFFFFFFFF); + if (vars.Count != 0) + FillVarTable(result, vars); + return PopTable(result); + } + + public ILuaObject BuildNetworkVarTables() { + LuaTable result = new(); + + SortedDictionary globals = GetEntityVars(0xFFFFFFFF); + if (globals.Count != 0) { + LuaTable table = new(); + FillVarTable(table, globals); + result.SetMember(0, table); + table.UnReference(); + } + +#if GAME_DLL + for (BaseEntity? ent = gEntList.FirstEnt(); ent != null; ent = gEntList.NextEnt(ent)) { + if (ent.Edict() == null || (uint)(ent.EntIndex() - 1) > 0x3FFE) + continue; +#else + for (int i = 1; i < MAX_ENTITIES; i++) { + C_BaseEntity? ent = cl_entitylist.GetBaseEntity(i); + if (ent == null) + continue; +#endif + SortedDictionary vars = GetEntityVars(ent.GetRefEHandle().Index); + if (vars.Count == 0) + continue; + + LuaTable table = new(); + FillVarTable(table, vars); + LuaEntity.Push_Entity(ent); + ILuaObject key = g_Lua!.GetObject(-1); + g_Lua.Pop(1); + result.SetMember(key, table); + table.UnReference(); + } + + return PopTable(result); + } + +#if GAME_DLL + public static void RegisterUserMessages(UserMessages messages) => messages.Register("NetworkedVar", -1); + + void UpdateEntityVar(LuaNetworkedEntity slot, LuaNetworkedVar var, float interval, RecipientFilter filter, bool force) { + if (!force && var.NextUpdate != 0.0f && var.NextUpdate > gpGlobals.CurTime - interval) + return; + + if (var.Value.isNil()) { + var.NextUpdate = (float)gpGlobals.RealTime; + return; + } + + if ((BytesSent > lua_networkvar_bytespertick.GetInt() || BytesSent == -1) && !force) { + BytesSent = -1; + return; + } + + UserMessageBegin(filter, "NetworkedVar"); + MessageWriteEHandle(EntityFromHandle(slot.Handle.Index)); + LuaType type = var.Value.GetType(); + MessageWriteChar((sbyte)type); + MessageWriteString(NetworkString.Convert(var.NameID)); + switch (type) { + case LuaType.Bool: + MessageWriteBool(var.Value.GetBool()); + BytesSent += 1; + break; + case LuaType.Number: + MessageWriteFloat(var.Value.GetFloat()); + BytesSent += 4; + break; + case LuaType.String: + string str = var.Value.GetString() ?? ""; + MessageWriteString(str); + BytesSent += Encoding.UTF8.GetByteCount(str); + break; + case LuaType.Entity: + BytesSent += 4; + MessageWriteEHandle(var.Value.GetUserData() != 0 ? (BaseEntity?)var.Value.GetEntity() : null); + break; + case LuaType.Vector: + MessageWriteVec3Coord(var.Value.GetVector()); + BytesSent += 12; + break; + case LuaType.Angle: + MessageWriteAngles(var.Value.GetAngle()); + BytesSent += 12; + break; + default: + Msg($"Error: Trying to network unacceptable type ({g_Lua!.GetTypeName(type)})\n"); + break; + } + MessageEnd(); + + if (!force) + var.NextUpdate = (float)(gpGlobals.CurTime + RandomFloat(-0.05f, 0.05f)); + } + + void UpdateEntityVars(LuaNetworkedEntity slot, RecipientFilter filter, bool force) { + if (slot.Vars.Count == 0) + return; + + float interval = RandomFloat(0.5f, 1.0f) * 360.0f; + uint handle = slot.Handle.Index; + if (handle == 0xFFFFFFFF || (handle & 0x3FFF) == 0) + interval = 30.0f; + else { + BaseEntity? ent = EntityFromHandle(handle); + if (ent == null) + return; + + if (ent.IsWeapon()) { + BaseCombatWeapon weapon = (BaseCombatWeapon)ent; + if (weapon.GetOwner() != null && weapon.GetOwner()!.GetActiveWeapon() == weapon) + interval = 0.5f; + } + + if (ent.IsPlayer()) + interval = 10.0f; + } + + foreach (LuaNetworkedVar var in slot.Vars.Values) + UpdateEntityVar(slot, var, interval, filter, force); + } + + LuaNetworkedEntity SlotOf(BaseEntity ent) => Entities[ent.Edict() != null ? ent.EntIndex() : 0]; + + public void Cycle() { + if (g_Lua == null || fNextTime > (float)gpGlobals.RealTime) + return; + + BytesSent = 0; + fNextTime = (float)(gpGlobals.RealTime + 0.1); + + BroadcastRecipientFilter broadcast = new(); + if (Entities[0].Vars.Count != 0) + UpdateEntityVars(Entities[0], broadcast, false); + + BytesSent = 0; + for (BaseEntity? ent = gEntList.FirstEnt(); ent != null; ent = gEntList.NextEnt(ent)) { + if (ent.Edict() == null || (uint)(ent.EntIndex() - 1) > 0x3FFE) + continue; + LuaNetworkedEntity slot = Entities[ent.EntIndex()]; + if (slot.Vars.Count != 0) + UpdateEntityVars(slot, broadcast, false); + } + + BytesSent = 0; + for (int i = 1; i <= gpGlobals.MaxClients; i++) { + if (EntityFromHandle(PlayerCursors[i - 1]) == null) + continue; + + BasePlayer? player = Util.PlayerByIndex(i); + if (player == null) { + PlayerCursors[i - 1] = 0xFFFFFFFF; + continue; + } + + BytesSent = 0; + SingleUserRecipientFilter filter = new(player); + filter.MakeReliable(); + + do { + BaseEntity? ent = EntityFromHandle(PlayerCursors[i - 1]); + if (ent == null) + break; + + if (ent.Edict() != null && (uint)(ent.EntIndex() - 1) <= 0x3FFE) { + LuaNetworkedEntity slot = Entities[ent.EntIndex()]; + if (slot.Vars.Count != 0) + UpdateEntityVars(slot, filter, true); + } + + BaseEntity? next = gEntList.NextEnt(EntityFromHandle(PlayerCursors[i - 1])); + PlayerCursors[i - 1] = next != null ? next.GetRefEHandle().Index : 0xFFFFFFFF; + } + while (BytesSent <= 0x200); + } + } + + public void PlayerInsert(BasePlayer player) { + BaseEntity? first = gEntList.FirstEnt(); + int slot = player.Edict() != null ? player.EntIndex() - 1 : -1; + PlayerCursors[slot] = first != null ? first.GetRefEHandle().Index : 0xFFFFFFFF; + + SingleUserRecipientFilter filter = new(player); + filter.MakeReliable(); + + if (Entities[0].Vars.Count != 0) + UpdateEntityVars(Entities[0], filter, true); + + LuaNetworkedEntity own = SlotOf(player); + if (own.Vars.Count != 0) + UpdateEntityVars(own, filter, true); + } +#else + public static void RegisterUserMessages(UserMessages messages) { + messages.Register("NetworkedVar", -1); + messages.HookMessage("NetworkedVar", MsgFunc_NetworkedVar); + } + + static uint ReadEHandle(bf_read msg) { + uint value = (uint)msg.ReadLong(); + if (value == Constants.INVALID_NETWORKED_EHANDLE_VALUE) + return 0xFFFFFFFF; + uint index = value & ((1 << Constants.MAX_EDICT_BITS) - 1); + uint serial = (uint)((int)value >> Constants.MAX_EDICT_BITS); + uint handle = (serial << 14) | index; + if (index != (handle & 0x3FFF) || serial != (handle >> 14)) + Warning($"CBaseHandle::Init got a bad handle! {index} {serial} => {handle & 0x3FFF} {handle >> 14}\n"); + return handle; + } + + static void MsgFunc_NetworkedVar(bf_read msg) { + if (g_Lua == null || g_LuaNetworkedVars == null) + return; + + uint handle = ReadEHandle(msg); + LuaType type = (LuaType)msg.ReadChar(); + string name = msg.ReadString(0x80) ?? ""; + + switch (type) { + case LuaType.String: + g_Lua.PushString(msg.ReadString(0x100) ?? ""); + break; + case LuaType.Number: + g_Lua.PushNumber(msg.ReadFloat()); + break; + case LuaType.Vector: + LuaVector.Push_Vector(msg.ReadBitVec3Coord()); + break; + case LuaType.Angle: + LuaAngle.Push_Angle(msg.ReadBitAngles()); + break; + case LuaType.Bool: + g_Lua.PushBool(msg.ReadOneBit() != 0); + break; + case LuaType.Entity: + uint entHandle = ReadEHandle(msg); + if (g_Lua == null) + Error("LuaNetworkVars: !g_Lua"); + LuaEntity.Push_Entity(EntityFromHandle(entHandle)); + break; + default: + Msg($"Error: Trying to network (receive) unacceptable type ({g_Lua.GetTypeName(type)})\n"); + return; + } + + LuaObject value = new(); + value.SetFromStack(-1); + g_Lua.Pop(1); + + g_LuaNetworkedVars.SetNetworkedVar(handle, name, value); + value.UnReference(); + } +#endif +} +#endif diff --git a/Game.Shared/HL2/UserMessages.cs b/Game.Shared/HL2/UserMessages.cs index 2f5800dae..9c76864a1 100644 --- a/Game.Shared/HL2/UserMessages.cs +++ b/Game.Shared/HL2/UserMessages.cs @@ -34,7 +34,11 @@ public void RegisterUsermessages() { Register("SWEPCmd", -1); Register("AmmoPickup", -1); Register("WeaponPickup", -1); - Register("NetworkedVar", -1); +#if CLIENT_DLL + Game.Client.GarrysMod.LuaNetworkedVars.RegisterUserMessages(this); +#else + Game.Server.GarrysMod.LuaNetworkedVars.RegisterUserMessages(this); +#endif Register("BreakModel", -1); Register("CheapBreakModel", -1); } From 1540608a6c84a02fcd19b22d22b52e53815991ba Mon Sep 17 00:00:00 2001 From: march <106459595+marchc1@users.noreply.github.com> Date: Wed, 7 Oct 2026 12:49:24 -0700 Subject: [PATCH 07/20] NetworkVarNames class --- Game.Shared/GarrysMod/NetworkVarNames.cs | 53 ++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 Game.Shared/GarrysMod/NetworkVarNames.cs diff --git a/Game.Shared/GarrysMod/NetworkVarNames.cs b/Game.Shared/GarrysMod/NetworkVarNames.cs new file mode 100644 index 000000000..e794751f9 --- /dev/null +++ b/Game.Shared/GarrysMod/NetworkVarNames.cs @@ -0,0 +1,53 @@ +#if CLIENT_DLL || GAME_DLL +using Source.Common.Engine; +using Source.Common.Networking; + +#if CLIENT_DLL +namespace Game.Client.GarrysMod; +#else +namespace Game.Server.GarrysMod; +#endif + +public static class NetworkVarNames +{ + public const int INVALID = 0xFFFF; + + public static INetworkStringTable? pStringTable; + +#if GAME_DLL + public static void Create() => pStringTable = networkstringtable.CreateStringTable(Protocol.NETWORKVARS_TABLENAME, 0x1000, 0, 0); +#endif + + public static void Reset() => pStringTable = null; + + public static void Install() => pStringTable = networkstringtable.FindTable(Protocol.NETWORKVARS_TABLENAME); + + public static int Add(ReadOnlySpan name) { + if (name != null) { + int index = pStringTable!.FindStringIndex(name); + if (index != INVALID) + return index; + } + + if (pStringTable!.GetNumStrings() > 0xFFE) { + Warning($"Too many NWVar names, could not add name {name}\n"); + return INVALID; + } + + return pStringTable.AddString(true, name); + } + + public static string? Convert(int index) { + if ((uint)index > 0xFFFE) + return null; + ReadOnlySpan str = pStringTable!.GetString(index); + return str == null ? null : new(str); + } + + public static int Get(ReadOnlySpan name) { + if (name == null) + return INVALID; + return pStringTable!.FindStringIndex(name); + } +} +#endif From d6a0f191aeb4ce6f7327e211c5645b765f786605 Mon Sep 17 00:00:00 2001 From: march <106459595+marchc1@users.noreply.github.com> Date: Wed, 7 Oct 2026 12:55:12 -0700 Subject: [PATCH 08/20] conceding to using cformatting functions --- Source.Common/CFormatting.cs | 75 ++++++++++++++++++++++++-- Source.Common/GarrysMod/GMODVariant.cs | 7 +-- 2 files changed, 74 insertions(+), 8 deletions(-) diff --git a/Source.Common/CFormatting.cs b/Source.Common/CFormatting.cs index 89c200d27..6439393aa 100644 --- a/Source.Common/CFormatting.cs +++ b/Source.Common/CFormatting.cs @@ -1,7 +1,11 @@ -using Steamworks; +/* + This file is a bit of a mess. It was an attempt at implementing a C# version of C-style formatting methods. + However, I think it's kind of lost the plot + + I have some ideas to rework it, I am particularly interested in sprintf/sscanf functions that use https://github.com/FiniteReality/VariadicGenerics + for their arguments instead, along with not trying to reinvent the wheel and read actual C stdlib implementations. +*/ -using System; -using System.Formats.Asn1; using System.Globalization; using System.Numerics; using System.Runtime.CompilerServices; @@ -518,6 +522,8 @@ public static double strtod(ReadOnlySpan input, out ReadOnlySpan out public static double atof(ReadOnlySpan str) => strtod(str, out _); + // TODO: I really don't like these... we need to really give sprintf and other + // similar functions some love. public static string FormatFixed(double value, int precision) { long bits = BitConverter.DoubleToInt64Bits(value); bool negative = bits < 0; @@ -556,6 +562,69 @@ public static string FormatFixed(double value, int precision) { return negative ? "-" + result : result; } + public static string FormatG(double value, int precision = 6) { + long bits = BitConverter.DoubleToInt64Bits(value); + bool negative = bits < 0; + long fraction = bits & 0xFFFFFFFFFFFFFL; + int exponent = (int)((bits >> 52) & 0x7FF); + + if (exponent == 0x7FF) + return FormatFixed(value, 0); + + if (precision == 0) + precision = 1; + + if (exponent == 0 && fraction == 0) + return negative ? "-0" : "0"; + + BigInteger num = exponent == 0 ? fraction : fraction | (1L << 52); + BigInteger den = BigInteger.One; + int shift = (exponent == 0 ? 1 : exponent) - 1075; + if (shift >= 0) + num <<= shift; + else + den <<= -shift; + + int exp10 = (int)Math.Floor(BigInteger.Log10(num) - BigInteger.Log10(den)); + if (exp10 >= 0 ? num < den * BigInteger.Pow(10, exp10) : num * BigInteger.Pow(10, -exp10) < den) + exp10--; + else if (exp10 + 1 >= 0 ? num >= den * BigInteger.Pow(10, exp10 + 1) : num * BigInteger.Pow(10, -(exp10 + 1)) >= den) + exp10++; + + int scale = precision - 1 - exp10; + BigInteger sn = scale >= 0 ? num * BigInteger.Pow(10, scale) : num; + BigInteger sd = scale >= 0 ? den : den * BigInteger.Pow(10, -scale); + BigInteger digits = BigInteger.DivRem(sn, sd, out BigInteger rem); + + int cmp = (rem << 1).CompareTo(sd); + if (cmp > 0 || (cmp == 0 && !digits.IsEven)) + digits += 1; + + if (digits >= BigInteger.Pow(10, precision)) { + digits /= 10; + exp10++; + } + + string d = digits.ToString(CultureInfo.InvariantCulture); + string result; + if (exp10 < -4 || exp10 >= precision) { + string mant = d.Length > 1 ? $"{d[0]}.{d[1..]}".TrimEnd('0').TrimEnd('.') : d; + int absExp = Math.Abs(exp10); + result = $"{mant}e{(exp10 < 0 ? '-' : '+')}{(absExp < 10 ? "0" : "")}{absExp}"; + } + else if (exp10 >= 0) { + string whole = d[..(exp10 + 1)]; + string frac = d[(exp10 + 1)..].TrimEnd('0'); + result = frac.Length > 0 ? $"{whole}.{frac}" : whole; + } + else { + string frac = (new string('0', -exp10 - 1) + d).TrimEnd('0'); + result = $"0.{frac}"; + } + + return negative ? $"-{result}" : result; + } + public static int ScanFloats(ReadOnlySpan str, Span values) { str = str.SliceNullTerminatedString(); for (int i = 0; i < values.Length; i++) { diff --git a/Source.Common/GarrysMod/GMODVariant.cs b/Source.Common/GarrysMod/GMODVariant.cs index 13fbd744a..22b72d020 100644 --- a/Source.Common/GarrysMod/GMODVariant.cs +++ b/Source.Common/GarrysMod/GMODVariant.cs @@ -160,14 +160,11 @@ public readonly int ToHandle() { return 0; } - // todo: I don't like that these use .ToString("G6"), it's not - // gonna be the same string we would've gotten... we really need to - // flesh out the cformatting stuff public override readonly string ToString() => Type switch { - GMODVariantType.Float => (Float).ToString("G6"), + GMODVariantType.Float => FormatG(Float), GMODVariantType.Int or GMODVariantType.Entity => Int.ToString(), GMODVariantType.Bool => Int != 0 ? "true" : "false", - GMODVariantType.Vector or GMODVariantType.Angle => $"{(Vec.X).ToString("G6")} {(Vec.Y).ToString("G6")} {(Vec.Z).ToString("G6")}", + GMODVariantType.Vector or GMODVariantType.Angle => $"{FormatG(Vec.X)} {FormatG(Vec.Y)} {FormatG(Vec.Z)}", GMODVariantType.String => String ?? "", _ => "" }; From da8c8258494f94487f4c7dd94cbc71ea22561b21 Mon Sep 17 00:00:00 2001 From: march <106459595+marchc1@users.noreply.github.com> Date: Wed, 7 Oct 2026 12:57:50 -0700 Subject: [PATCH 09/20] plug in g_LuaNetworkedVars into garrysmod class --- Game.Shared/GarrysMod/GarrysMod.cs | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/Game.Shared/GarrysMod/GarrysMod.cs b/Game.Shared/GarrysMod/GarrysMod.cs index 383851168..ebdcf2c89 100644 --- a/Game.Shared/GarrysMod/GarrysMod.cs +++ b/Game.Shared/GarrysMod/GarrysMod.cs @@ -267,9 +267,15 @@ public void Startup() { Msg("Clientside Lua startup!\n"); enginevgui.UpdateCustomProgressBar(0.95f, "Starting Lua..."); #endif - // if (g_LuaNetworkedVars != null) - // Error("g_LuaNetworkedVars"); - // g_LuaNetworkedVars = new LuaNetworkedVars(); +#if CLIENT_DLL + if (LuaNetworkedVars.g_LuaNetworkedVars != null) + Error("g_LuaNetworkedVars"); + LuaNetworkedVars.g_LuaNetworkedVars = new LuaNetworkedVars(); +#else + if (Game.Server.GarrysMod.LuaNetworkedVars.g_LuaNetworkedVars != null) + Error("g_LuaNetworkedVars"); + Game.Server.GarrysMod.LuaNetworkedVars.g_LuaNetworkedVars = new Game.Server.GarrysMod.LuaNetworkedVars(); +#endif if (g_Lua != null) Error("CLuaManager::Startup Lua already exsits?\n"); @@ -379,10 +385,15 @@ public void Shutdown() { #endif get.LuaShared()!.CloseLuaInterface(g_Lua!); g_Lua = null; - // if (g_LuaNetworkedVars == null) - // Error("!g_LuaNetworkedVars"); - // todo: free every entry of g_LuaNetworkedVars - // g_LuaNetworkedVars = null; +#if CLIENT_DLL + if (LuaNetworkedVars.g_LuaNetworkedVars == null) + Error("!g_LuaNetworkedVars"); + LuaNetworkedVars.g_LuaNetworkedVars = null; +#else + if (Game.Server.GarrysMod.LuaNetworkedVars.g_LuaNetworkedVars == null) + Error("!g_LuaNetworkedVars"); + Game.Server.GarrysMod.LuaNetworkedVars.g_LuaNetworkedVars = null; +#endif } } @@ -468,12 +479,11 @@ public void Think() { if (g_Lua != null) { // CheckForFilesystemChanges(); g_Lua.Cycle(); - // if (g_LuaNetworkedVars != null) - // g_LuaNetworkedVars.Cycle(); #if CLIENT_DLL LuaTimer.Cycle(); LuaFileLibrary.AsyncCycle(); #else + Game.Server.GarrysMod.LuaNetworkedVars.g_LuaNetworkedVars?.Cycle(); Game.Server.GarrysMod.LuaTimer.Cycle(); Game.Server.GarrysMod.LuaFileLibrary.AsyncCycle(); #endif From d2a3978f66ec14d3b3cc6e1be41d6e60a6c9c3fc Mon Sep 17 00:00:00 2001 From: march <106459595+marchc1@users.noreply.github.com> Date: Wed, 7 Oct 2026 12:58:00 -0700 Subject: [PATCH 10/20] fix baseentities gmodtables --- Game.Client/C_BaseEntity.cs | 6 +++++- Game.Server/BaseEntity.cs | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/Game.Client/C_BaseEntity.cs b/Game.Client/C_BaseEntity.cs index 308ef6a1e..f06fee221 100644 --- a/Game.Client/C_BaseEntity.cs +++ b/Game.Client/C_BaseEntity.cs @@ -7,6 +7,7 @@ using Source; using Source.Common; +using Source.Common.GarrysMod; using Source.Common.Bitbuffers; using Source.Common.Commands; using Source.Common.Engine; @@ -680,6 +681,9 @@ public C_BaseEntity() { ReadyToDraw = true; PredictionContext = null; Clear(); +#if GMOD_DLL + GMOD_DataTable = engine.GMOD_CreateDataTable(Game.Client.GarrysMod.LuaDataTable.GMOD_DataTableRecvProxy); +#endif } public virtual bool IsTwoPass() => modelinfo.IsTranslucentTwoPass(GetModel()); @@ -794,7 +798,7 @@ public C_BaseEntity() { public InlineArray512 GMOD_String3; [NetworkName("m_GMOD_DataTable")] - public readonly GModTable GMOD_DataTable = new(); + public readonly IGMODDataTable GMOD_DataTable; [NetworkName("m_nSpeed")] public int Speed; diff --git a/Game.Server/BaseEntity.cs b/Game.Server/BaseEntity.cs index 2e8b26209..89dfa95f9 100644 --- a/Game.Server/BaseEntity.cs +++ b/Game.Server/BaseEntity.cs @@ -4,6 +4,7 @@ using Source; using Source.Common; +using Source.Common.GarrysMod; using Source.Common.Audio; using Source.Common.Commands; using Source.Common.Engine; @@ -818,6 +819,9 @@ public BaseEntity(bool serverOnly) { NetworkProp().MarkPVSInformationDirty(); AddEFlags(EFL.UsePartitionWhenNotSolid); +#if GMOD_DLL + GMOD_DataTable = engine.GMOD_CreateDataTable(); +#endif } public virtual void StopLoopingSounds() { } @@ -1596,7 +1600,7 @@ public bool IsFloating() { public readonly PredictableId PredictableId = new(); [NetworkName("m_GMOD_DataTable")] - public readonly GModTable GMOD_DataTable = new(); + public readonly IGMODDataTable GMOD_DataTable; public float Speed; From d44534ed0ec940be1f00679687717f08ba879ce9 Mon Sep 17 00:00:00 2001 From: march <106459595+marchc1@users.noreply.github.com> Date: Wed, 7 Oct 2026 12:58:12 -0700 Subject: [PATCH 11/20] add NetworkVarNames calls in gameinterface --- Game.Server/GameInterface.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Game.Server/GameInterface.cs b/Game.Server/GameInterface.cs index b343b497f..be9f654ff 100644 --- a/Game.Server/GameInterface.cs +++ b/Game.Server/GameInterface.cs @@ -238,7 +238,7 @@ public void CreateNetworkStringTables() { #if GMOD_DLL // TODO: GarrysMod::StringTable::Create Game.Server.GarrysMod.NetworkString.Create(); - // TODO: NetworkVarNames::Create + Game.Server.GarrysMod.NetworkVarNames.Create(); Game.Server.GarrysMod.GModDataPack.DataPack().Initialize(); StringTableBits.SV_SetupNetworkStringTableBits(); @@ -416,7 +416,7 @@ public void GMOD_OnStringTablesRemoved() { GameRulesRegister.ResetNetworkStringTables_GameRules(); #if GMOD_DLL Game.Server.GarrysMod.NetworkString.Reset(); - // todo: NetworkVarNames::Reset + Game.Server.GarrysMod.NetworkVarNames.Reset(); Game.Server.GarrysMod.GModDataPack.DataPack().Reset(); #endif } From f99d55a414b6997b58b236f104524fa436153deb Mon Sep 17 00:00:00 2001 From: march <106459595+marchc1@users.noreply.github.com> Date: Wed, 7 Oct 2026 13:01:41 -0700 Subject: [PATCH 12/20] luadatatable --- Game.Client/HLClient.cs | 2 +- Game.Server/GarrysMod/GMOD_Player.cs | 1 + Game.Shared/GarrysMod/LuaDataTable.cs | 718 ++++++++++++++++++++++++++ 3 files changed, 720 insertions(+), 1 deletion(-) create mode 100644 Game.Shared/GarrysMod/LuaDataTable.cs diff --git a/Game.Client/HLClient.cs b/Game.Client/HLClient.cs index c7f6221d5..7303db04e 100644 --- a/Game.Client/HLClient.cs +++ b/Game.Client/HLClient.cs @@ -287,9 +287,9 @@ public void InstallStringTableCallback(ReadOnlySpan tableName) { break; #endif } + // todo: review if this exists. Came from Raphaeli's attempts, may not be present now? // GarrysMod.GarrysMod.StringTable.Install(); - GameRulesRegister.InstallStringTableCallback_GameRules(); } diff --git a/Game.Server/GarrysMod/GMOD_Player.cs b/Game.Server/GarrysMod/GMOD_Player.cs index ef9392b06..dece38a48 100644 --- a/Game.Server/GarrysMod/GMOD_Player.cs +++ b/Game.Server/GarrysMod/GMOD_Player.cs @@ -57,6 +57,7 @@ public partial class GMOD_Player : HL2MP_Player public override void InitialSpawn() { base.InitialSpawn(); + LuaNetworkedVars.g_LuaNetworkedVars!.PlayerInsert(this); } public override BaseEntity EntSelectSpawnPoint() { diff --git a/Game.Shared/GarrysMod/LuaDataTable.cs b/Game.Shared/GarrysMod/LuaDataTable.cs new file mode 100644 index 000000000..fa6a34723 --- /dev/null +++ b/Game.Shared/GarrysMod/LuaDataTable.cs @@ -0,0 +1,718 @@ +#if CLIENT_DLL || GAME_DLL +using Source; +using Source.Common; +using Source.Common.Commands; +using Source.Common.GarrysMod; +using Source.Common.GarrysMod.Lua; +using Source.Common.Mathematics; + +using System.Numerics; + +#if CLIENT_DLL +namespace Game.Client.GarrysMod; +#else +namespace Game.Server.GarrysMod; +#endif + +public static partial class LuaDataTable +{ + [LuaClass] + static readonly LuaClass LC_Entity = LuaEntity.LC_Entity; + + static GMODVariant s_nullVariant; + +#if CLIENT_DLL + static IGMODDataTable? g_pClientWorldTable; +#else + static IGMODDataTable? g_pServerWorldTable; + static readonly IFieldAccessor GMOD_DataTableField = FIELD.OF(nameof(BaseEntity.GMOD_DataTable)); +#endif + + public static IGMODDataTable GetGlobalDataTable() { +#if CLIENT_DLL + C_World? world = C_World.GetClientWorldEntity(); + if (world != null && world.GMOD_DataTable != null) + return world.GMOD_DataTable; + return g_pClientWorldTable ??= engine.GMOD_CreateDataTable(GMOD_DataTableRecvProxy); +#else + BaseEntity? world = GetWorldEntity(); + if (world != null) + return world.GMOD_DataTable; + return g_pServerWorldTable ??= engine.GMOD_CreateDataTable(); +#endif + } + + public static ref readonly GMODVariant GetDataTableVar(IGMODDataTable dt, ReadOnlySpan name) { + int index = NetworkVarNames.Get(name); +#if CLIENT_DLL + if (index != NetworkVarNames.INVALID && dt.HasKey(index)) + return ref dt.Get(index); + return ref dt.GetLocal(name); +#else + if (index == NetworkVarNames.INVALID) { + s_nullVariant = default; + return ref s_nullVariant; + } + return ref dt.Get(index); +#endif + } + + public static GMODVariant Get_GMODVariant(int stackPos, bool asInt) { + GMODVariant variant = default; + switch (g_Lua!.GetType(stackPos)) { + case LuaType.Bool: + variant.SetBool(g_Lua.GetBool(stackPos)); + break; + case LuaType.Number: + if (asInt) + variant.SetInt(LuaHelper.cvttsd2si(g_Lua.GetNumber(stackPos))); + else + variant.SetFloat((float)g_Lua.GetNumber(stackPos)); + break; + case LuaType.String: + variant.SetString(g_Lua.GetString(stackPos)); + break; + case LuaType.Entity: + BaseEntity? ent = LuaEntity.Get_Entity(stackPos, true); + variant.SetEntity(ent != null ? (int)ent.GetRefEHandle().Index : -1); + break; + case LuaType.Vector: + variant.SetVector(LuaVector.Get_Vector(stackPos)); + break; + case LuaType.Angle: + variant.SetAngle(LuaAngle.Get_Angle(stackPos)); + break; + } + return variant; + } + + public static void Push_GMODVariant(in GMODVariant variant) { + switch (variant.Type) { + case GMODVariantType.NIL: + g_Lua!.PushNil(); + break; + case GMODVariantType.Float: + g_Lua!.PushNumber(variant.Float); + break; + case GMODVariantType.Int: + g_Lua!.PushNumber(variant.Int); + break; + case GMODVariantType.Bool: + g_Lua!.PushBool(variant.Int != 0); + break; + case GMODVariantType.Vector: + LuaVector.Push_Vector(variant.Vec); + break; + case GMODVariantType.Angle: + LuaAngle.Push_Angle(variant.Ang); + break; + case GMODVariantType.Entity: + LuaEntity.Push_Entity(LuaEntity.GetEntityFromHandle((uint)variant.Int)); + break; + case GMODVariantType.String: + g_Lua!.PushString(variant.String); + break; + default: + g_Lua!.PushNil(); + break; + } + } + + static bool VariantsEqual(in GMODVariant a, in GMODVariant b) { + if (a.Type != b.Type) + return false; + return a.Type switch { + GMODVariantType.Float => a.Float == b.Float, + GMODVariantType.Int or GMODVariantType.Entity => a.Int == b.Int, + GMODVariantType.Bool => (a.Int != 0) == (b.Int != 0), + GMODVariantType.Vector or GMODVariantType.Angle => a.Vec.X == b.Vec.X && a.Vec.Y == b.Vec.Y && a.Vec.Z == b.Vec.Z, + GMODVariantType.String => a.StringLength == b.StringLength && string.Equals(a.String, b.String, StringComparison.Ordinal), + _ => true + }; + } + + public static void CallEntityNetworkedVarChanged(BaseEntity ent, ReadOnlySpan name, in GMODVariant oldValue, in GMODVariant newValue) { + if (VariantsEqual(oldValue, newValue)) + return; + +#if GAME_DLL + Util.DisableRemoveImmediate(); +#endif + if (gGM != null && gGM.CallWithArgs(0x19)) { + LuaEntity.Push_Entity(ent); + g_Lua!.PushString(name); + Push_GMODVariant(oldValue); + Push_GMODVariant(newValue); + gGM.CallNoReturns(4); + } +#if GAME_DLL + Util.EnableRemoveImmediate(); +#endif + } + + public static void GMOD_DataTableRecvProxy(object? entity, int key, in GMODVariant newValue) { + if (entity is not BaseEntity ent) + return; + + string? name = NetworkVarNames.Convert(key); + if (name == null) + return; + + ref readonly GMODVariant oldValue = ref ent.GMOD_DataTable.Get(key); + CallEntityNetworkedVarChanged(ent, name, oldValue, newValue); + + if (oldValue.Type == GMODVariantType.NIL && newValue.Type != GMODVariantType.NIL && ent.GMOD_DataTable.GetLocal(name).Type != GMODVariantType.NIL) + ent.GMOD_DataTable.ClearLocal(name); + } + + public static void SetDataTableVar(BaseEntity? ent, IGMODDataTable dt, ReadOnlySpan name, in GMODVariant value) { +#if CLIENT_DLL + int index = NetworkVarNames.Get(name); + if (index == NetworkVarNames.INVALID) { + CallEntityNetworkedVarChanged(ent!, name, dt.GetLocal(name), value); + dt.SetLocal(name, value); + } + else { + CallEntityNetworkedVarChanged(ent!, name, dt.Get(index), value); + dt.Set(index, value); + } +#else + int index = NetworkVarNames.Add(name); + if (index == NetworkVarNames.INVALID) + return; + + if (ent != null) + CallEntityNetworkedVarChanged(ent, name, ent.GMOD_DataTable.Get(index), value); + + dt.Set(index, value); + + ent?.NetworkStateChanged(GMOD_DataTableField); +#endif + } + + static int GetNetworkedVarDefault(ILuaInterface lua, int defaultPos) { + ILuaObject? obj = lua.GetObject(defaultPos); + if (obj != null && !obj.isNil()) { + lua.PushLuaObject(obj); + return 1; + } + return 0; + } + + static uint NetworkedHandle(BaseEntity? ent) => ent != null ? ent.GetRefEHandle().Index : 0xFFFFFFFF; + + static void ValidateInputType(ILuaInterface lua, int stackPos) { + LuaType type = lua.GetType(stackPos); + if (type is LuaType.Number or LuaType.String or LuaType.Nil or LuaType.Bool or LuaType.Entity or LuaType.Vector or LuaType.Angle) + return; + lua.ErrorFromLua($"SetNetworkedVar: Non-networkable type '{lua.GetTypeName(type)}' encountered!\n"); + } + + [LuaMethod("SetNetworkedVarProxy")] + [LuaMethod("SetNWVarProxy")] + static int Entity__SetNetworkedVarProxy(ILuaInterface lua) { + uint handle = NetworkedHandle(LuaEntity.Get_Entity(1, false)); + ILuaObject func = g_Lua!.GetObject(3); + LuaNetworkedVars.g_LuaNetworkedVars!.SetNetworkedVarProxy(handle, g_Lua.CheckString(2), func); + return 0; + } + + [LuaMethod("GetNetworkedVar")] + static int Entity__GetNetworkedVar(ILuaInterface lua) { + BaseEntity? ent = LuaEntity.Get_Entity(1, false); + if (LuaNetworkedVars.g_LuaNetworkedVars!.PushNetworkedVar(ent, g_Lua!.CheckString(2)) == 0) + g_Lua.PushLuaObject(g_Lua.GetObject(3)); + return 1; + } + + static bool PushNetworkedVarOrDefault(int namePos) { + BaseEntity? ent = LuaEntity.Get_Entity(1, false); + if (ent != null && LuaNetworkedVars.g_LuaNetworkedVars!.PushNetworkedVar(ent, g_Lua!.CheckString(namePos)) != 0) + return true; + return GetNetworkedVarDefault(g_Lua!, namePos + 1) != 0; + } + + [LuaMethod("GetNetworkedFloat")] + [LuaMethod("GetNetworkedInt")] + [LuaMethod("GetNWInt")] + [LuaMethod("GetNWFloat")] + static int Entity__GetNetworkedFloat(ILuaInterface lua) { + if (!PushNetworkedVarOrDefault(2)) + g_Lua!.PushNumber(0); + return 1; + } + + [LuaMethod("GetNetworkedVector")] + [LuaMethod("GetNWVector")] + static int Entity__GetNetworkedVector(ILuaInterface lua) { + if (!PushNetworkedVarOrDefault(2)) + LuaVector.Push_Vector(vec3_origin); + return 1; + } + + [LuaMethod("GetNetworkedAngle")] + [LuaMethod("GetNWAngle")] + static int Entity__GetNetworkedAngle(ILuaInterface lua) { + if (!PushNetworkedVarOrDefault(2)) + LuaAngle.Push_Angle(vec3_angle); + return 1; + } + + [LuaMethod("GetNetworkedBool")] + [LuaMethod("GetNWBool")] + static int Entity__GetNetworkedBool(ILuaInterface lua) { + if (!PushNetworkedVarOrDefault(2)) + g_Lua!.PushBool(false); + return 1; + } + + [LuaMethod("GetNetworkedEntity")] + [LuaMethod("GetNWEntity")] + static int Entity__GetNetworkedEntity(ILuaInterface lua) { + if (!PushNetworkedVarOrDefault(2)) + LuaEntity.Push_Entity(null); + return 1; + } + + [LuaMethod("GetNetworkedString")] + [LuaMethod("GetNWString")] + static int Entity__GetNetworkedString(ILuaInterface lua) { + if (!PushNetworkedVarOrDefault(2)) + g_Lua!.PushString(""); + return 1; + } + + [LuaMethod("SetNetworkedVar")] + static int Entity__SetNetworkedVar(ILuaInterface lua) { + ValidateInputType(lua, 3); + uint handle = NetworkedHandle(LuaEntity.Get_Entity(1, false)); + ILuaObject value = g_Lua!.GetObject(3); + LuaNetworkedVars.g_LuaNetworkedVars!.SetNetworkedVar(handle, g_Lua.CheckString(2), value); + return 0; + } + + [LuaMethod("SetNetworkedNumber")] + static int Entity__SetNetworkedNumber(ILuaInterface lua) { + ValidateInputType(lua, 3); + uint handle = NetworkedHandle(LuaEntity.Get_Entity(1, false)); + double number = g_Lua!.CheckNumber(3); + LuaObject value = new(); + value.SetFloat((float)number); + LuaNetworkedVars.g_LuaNetworkedVars!.SetNetworkedVar(handle, g_Lua.CheckString(2), value); + value.UnReference(); + return 0; + } + + [LuaMethod("GetNWVarTable")] + [LuaMethod("GetNetworkedVarTable")] + static int Entity__GetNWVarTable(ILuaInterface lua) { + LuaNetworkedVars.g_LuaNetworkedVars!.BuildEntityNetworkVarTable(LuaEntity.Get_Entity(1, false)).Push(); + return 1; + } + + [LuaMethod("SetNetworkedString")] + static int Entity__SetNetworkedString(ILuaInterface lua) => Entity__SetNetworkedVar(lua); + + [LuaMethod("SetNetworkedInt")] + [LuaMethod("SetNetworkedFloat")] + static int Entity__SetNetworkedInt(ILuaInterface lua) => Entity__SetNetworkedNumber(lua); + + [LuaMethod("SetNetworkedVector")] + [LuaMethod("SetNetworkedAngle")] + [LuaMethod("SetNetworkedEntity")] + [LuaMethod("SetNetworkedBool")] + [LuaMethod("SetNWString")] + [LuaMethod("SetNWInt")] + [LuaMethod("SetNWFloat")] + [LuaMethod("SetNWVector")] + [LuaMethod("SetNWAngle")] + [LuaMethod("SetNWEntity")] + [LuaMethod("SetNWBool")] + static int Entity__SetNetworkedVector(ILuaInterface lua) => Entity__SetNetworkedVar(lua); + + [LuaGlobal("SetGlobalVar")] + [LuaGlobal("SetGlobalString")] + [LuaGlobal("SetGlobalInt")] + [LuaGlobal("SetGlobalFloat")] + [LuaGlobal("SetGlobalVector")] + [LuaGlobal("SetGlobalAngle")] + [LuaGlobal("SetGlobalEntity")] + [LuaGlobal("SetGlobalBool")] + static int SetGlobalVar(ILuaInterface lua) { + ValidateInputType(lua, 2); + ILuaObject value = g_Lua!.GetObject(2); + LuaNetworkedVars.g_LuaNetworkedVars!.SetNetworkedVar(0xFFFFFFFF, g_Lua.CheckString(1), value); + return 0; + } + + [LuaGlobal] + static int GetGlobalVar(ILuaInterface lua) { + if (LuaNetworkedVars.g_LuaNetworkedVars!.PushNetworkedVar(null, g_Lua!.CheckString(1)) == 0) + g_Lua.PushLuaObject(g_Lua.GetObject(2)); + return 1; + } + + static bool PushGlobalVarOrDefault() { + if (LuaNetworkedVars.g_LuaNetworkedVars!.PushNetworkedVar(null, g_Lua!.CheckString(1)) != 0) + return true; + return GetNetworkedVarDefault(g_Lua, 2) != 0; + } + + [LuaGlobal] + static int GetGlobalFloat(ILuaInterface lua) { + if (!PushGlobalVarOrDefault()) + g_Lua!.PushNumber(0); + return 1; + } + + [LuaGlobal] + static int GetGlobalInt(ILuaInterface lua) { + if (!PushGlobalVarOrDefault()) + g_Lua!.PushNumber(0); + return 1; + } + + [LuaGlobal] + static int GetGlobalVector(ILuaInterface lua) { + if (!PushGlobalVarOrDefault()) + LuaVector.Push_Vector(vec3_origin); + return 1; + } + + [LuaGlobal] + static int GetGlobalAngle(ILuaInterface lua) { + if (!PushGlobalVarOrDefault()) + LuaAngle.Push_Angle(vec3_angle); + return 1; + } + + [LuaGlobal] + static int GetGlobalBool(ILuaInterface lua) { + if (!PushGlobalVarOrDefault()) + g_Lua!.PushBool(false); + return 1; + } + + [LuaGlobal] + static int GetGlobalEntity(ILuaInterface lua) { + if (!PushGlobalVarOrDefault()) + LuaEntity.Push_Entity(null); + return 1; + } + + [LuaGlobal] + static int GetGlobalString(ILuaInterface lua) { + if (!PushGlobalVarOrDefault()) + g_Lua!.PushString(""); + return 1; + } + + [LuaGlobal] + static int BuildNetworkedVarsTable(ILuaInterface lua) { + LuaNetworkedVars.g_LuaNetworkedVars!.BuildNetworkVarTables().Push(); + return 1; + } + + static int SetNetworked2(bool asInt) { + BaseEntity ent = LuaEntity.Get_Entity(1, false)!; + GMODVariant value = Get_GMODVariant(3, asInt); + SetDataTableVar(ent, ent.GMOD_DataTable, g_Lua!.CheckString(2), value); + return 0; + } + + static int SetGlobal2(bool asInt) { + GMODVariant value = Get_GMODVariant(2, asInt); + string name = g_Lua!.CheckString(1); + IGMODDataTable dt = GetGlobalDataTable(); +#if CLIENT_DLL + SetDataTableVar(C_World.GetClientWorldEntity(), dt, name, value); +#else + SetDataTableVar(GetWorldEntity(), dt, name, value); +#endif + return 0; + } + + static bool GetNetworked2(out GMODVariant value, out int result) { + BaseEntity ent = LuaEntity.Get_Entity(1, false)!; + value = GetDataTableVar(ent.GMOD_DataTable, g_Lua!.CheckString(2)); + result = value.Type == GMODVariantType.NIL ? GetNetworkedVarDefault(g_Lua, 3) : 0; + return result == 0; + } + + static bool GetGlobal2(out GMODVariant value, out int result) { + string name = g_Lua!.CheckString(1); + value = GetDataTableVar(GetGlobalDataTable(), name); + result = value.Type == GMODVariantType.NIL ? GetNetworkedVarDefault(g_Lua, 2) : 0; + return result == 0; + } + + static void PushEntityFromHandle(int handle) => LuaEntity.Push_Entity(handle == -1 ? null : LuaEntity.GetEntityFromHandle((uint)handle)); + + [LuaMethod("SetNetworked2Int")] + static int Entity__SetNetworked2Int(ILuaInterface lua) => SetNetworked2(true); + + [LuaMethod("GetNetworked2Int")] + static int Entity__GetNetworked2Int(ILuaInterface lua) { + if (GetNetworked2(out GMODVariant value, out _)) + g_Lua!.PushNumber(value.ToInt()); + return 1; + } + + [LuaGlobal] + static int SetGlobal2Int(ILuaInterface lua) => SetGlobal2(true); + + [LuaGlobal] + static int GetGlobal2Int(ILuaInterface lua) { + if (GetGlobal2(out GMODVariant value, out _)) + g_Lua!.PushNumber(value.ToInt()); + return 1; + } + + [LuaMethod("SetNW2Int")] + static int Entity__SetNW2Int(ILuaInterface lua) => Entity__SetNetworked2Int(lua); + + [LuaMethod("GetNW2Int")] + static int Entity__GetNW2Int(ILuaInterface lua) => Entity__GetNetworked2Int(lua); + + [LuaMethod("SetNetworked2Float")] + static int Entity__SetNetworked2Float(ILuaInterface lua) => SetNetworked2(false); + + [LuaMethod("GetNetworked2Float")] + static int Entity__GetNetworked2Float(ILuaInterface lua) { + if (GetNetworked2(out GMODVariant value, out _)) + g_Lua!.PushNumber(value.ToFloat()); + return 1; + } + + [LuaGlobal] + static int SetGlobal2Float(ILuaInterface lua) => SetGlobal2(false); + + [LuaGlobal] + static int GetGlobal2Float(ILuaInterface lua) { + if (GetGlobal2(out GMODVariant value, out _)) + g_Lua!.PushNumber(value.ToFloat()); + return 1; + } + + [LuaMethod("SetNW2Float")] + static int Entity__SetNW2Float(ILuaInterface lua) => Entity__SetNetworked2Float(lua); + + [LuaMethod("GetNW2Float")] + static int Entity__GetNW2Float(ILuaInterface lua) => Entity__GetNetworked2Float(lua); + + [LuaMethod("SetNetworked2Bool")] + static int Entity__SetNetworked2Bool(ILuaInterface lua) => SetNetworked2(false); + + [LuaMethod("GetNetworked2Bool")] + static int Entity__GetNetworked2Bool(ILuaInterface lua) { + if (GetNetworked2(out GMODVariant value, out _)) + g_Lua!.PushBool(value.ToBool()); + return 1; + } + + [LuaGlobal] + static int SetGlobal2Bool(ILuaInterface lua) => SetGlobal2(false); + + [LuaGlobal] + static int GetGlobal2Bool(ILuaInterface lua) { + if (GetGlobal2(out GMODVariant value, out _)) + g_Lua!.PushBool(value.ToBool()); + return 1; + } + + [LuaMethod("SetNW2Bool")] + static int Entity__SetNW2Bool(ILuaInterface lua) => Entity__SetNetworked2Bool(lua); + + [LuaMethod("GetNW2Bool")] + static int Entity__GetNW2Bool(ILuaInterface lua) => Entity__GetNetworked2Bool(lua); + + [LuaMethod("SetNetworked2String")] + static int Entity__SetNetworked2String(ILuaInterface lua) => SetNetworked2(false); + + [LuaMethod("GetNetworked2String")] + static int Entity__GetNetworked2String(ILuaInterface lua) { + if (GetNetworked2(out GMODVariant value, out _)) + g_Lua!.PushString(value.ToString()); + return 1; + } + + [LuaGlobal] + static int SetGlobal2String(ILuaInterface lua) => SetGlobal2(false); + + [LuaGlobal] + static int GetGlobal2String(ILuaInterface lua) { + if (GetGlobal2(out GMODVariant value, out _)) + g_Lua!.PushString(value.ToString()); + return 1; + } + + [LuaMethod("SetNW2String")] + static int Entity__SetNW2String(ILuaInterface lua) => Entity__SetNetworked2String(lua); + + [LuaMethod("GetNW2String")] + static int Entity__GetNW2String(ILuaInterface lua) => Entity__GetNetworked2String(lua); + + [LuaMethod("SetNetworked2Vector")] + static int Entity__SetNetworked2Vector(ILuaInterface lua) => SetNetworked2(false); + + [LuaMethod("GetNetworked2Vector")] + static int Entity__GetNetworked2Vector(ILuaInterface lua) { + if (GetNetworked2(out GMODVariant value, out _)) + LuaVector.Push_Vector(value.ToVector()); + return 1; + } + + [LuaGlobal] + static int SetGlobal2Vector(ILuaInterface lua) => SetGlobal2(false); + + [LuaGlobal] + static int GetGlobal2Vector(ILuaInterface lua) { + if (GetGlobal2(out GMODVariant value, out _)) + LuaVector.Push_Vector(value.ToVector()); + return 1; + } + + [LuaMethod("SetNW2Vector")] + static int Entity__SetNW2Vector(ILuaInterface lua) => Entity__SetNetworked2Vector(lua); + + [LuaMethod("GetNW2Vector")] + static int Entity__GetNW2Vector(ILuaInterface lua) => Entity__GetNetworked2Vector(lua); + + [LuaMethod("SetNetworked2Angle")] + static int Entity__SetNetworked2Angle(ILuaInterface lua) => SetNetworked2(false); + + [LuaMethod("GetNetworked2Angle")] + static int Entity__GetNetworked2Angle(ILuaInterface lua) { + if (GetNetworked2(out GMODVariant value, out _)) + LuaAngle.Push_Angle(value.ToAngle()); + return 1; + } + + [LuaGlobal] + static int SetGlobal2Angle(ILuaInterface lua) => SetGlobal2(false); + + [LuaGlobal] + static int GetGlobal2Angle(ILuaInterface lua) { + if (GetGlobal2(out GMODVariant value, out _)) + LuaAngle.Push_Angle(value.ToAngle()); + return 1; + } + + [LuaMethod("SetNW2Angle")] + static int Entity__SetNW2Angle(ILuaInterface lua) => Entity__SetNetworked2Angle(lua); + + [LuaMethod("GetNW2Angle")] + static int Entity__GetNW2Angle(ILuaInterface lua) => Entity__GetNetworked2Angle(lua); + + [LuaMethod("SetNetworked2Entity")] + static int Entity__SetNetworked2Entity(ILuaInterface lua) => SetNetworked2(false); + + [LuaMethod("GetNetworked2Entity")] + static int Entity__GetNetworked2Entity(ILuaInterface lua) { + if (GetNetworked2(out GMODVariant value, out _)) + PushEntityFromHandle(value.ToHandle()); + return 1; + } + + [LuaGlobal] + static int SetGlobal2Entity(ILuaInterface lua) => SetGlobal2(false); + + [LuaGlobal] + static int GetGlobal2Entity(ILuaInterface lua) { + if (GetGlobal2(out GMODVariant value, out _)) + PushEntityFromHandle(value.ToHandle()); + return 1; + } + + [LuaMethod("SetNW2Entity")] + static int Entity__SetNW2Entity(ILuaInterface lua) => Entity__SetNetworked2Entity(lua); + + [LuaMethod("GetNW2Entity")] + static int Entity__GetNW2Entity(ILuaInterface lua) => Entity__GetNetworked2Entity(lua); + + [LuaMethod("SetNetworked2Var")] + static int Entity__SetNetworked2Var(ILuaInterface lua) => SetNetworked2(false); + + [LuaMethod("GetNetworked2Var")] + static int Entity__GetNetworked2Var(ILuaInterface lua) { + if (GetNetworked2(out GMODVariant value, out _)) + Push_GMODVariant(value); + return 1; + } + + [LuaGlobal] + static int SetGlobal2Var(ILuaInterface lua) => SetGlobal2(false); + + [LuaGlobal] + static int GetGlobal2Var(ILuaInterface lua) { + if (GetGlobal2(out GMODVariant value, out _)) + Push_GMODVariant(value); + return 1; + } + + [LuaMethod("SetNW2Var")] + static int Entity__SetNW2Var(ILuaInterface lua) => Entity__SetNetworked2Var(lua); + + [LuaMethod("GetNW2Var")] + static int Entity__GetNW2Var(ILuaInterface lua) => Entity__GetNetworked2Var(lua); + + [LuaMethod("GetNetworked2VarTable")] + [LuaMethod("GetNW2VarTable")] + static int Entity__GetNetworked2VarTable(ILuaInterface lua) { + IGMODDataTable dt = LuaEntity.Get_Entity(1, false)!.GMOD_DataTable; + LuaTable result = new(); + for (int it = dt.Begin(); it != dt.End(); dt.IncrementIterator(ref it)) { + string? name = NetworkVarNames.Convert(dt.GetKey(it)); + if (name == null) + continue; + + LuaTable entry = new(); + ref readonly GMODVariant value = ref dt.GetValue(it); + switch (value.Type) { + case GMODVariantType.Float: + entry.SetMember("type", "Float"); + entry.SetMember("value", value.ToFloat()); + break; + case GMODVariantType.Int: + entry.SetMember("type", "Int"); + entry.SetMemberDouble("value", value.ToInt()); + break; + case GMODVariantType.Bool: + entry.SetMember("type", "Bool"); + entry.SetMember("value", value.ToBool()); + break; + case GMODVariantType.Vector: + entry.SetMember("type", "Vector"); + entry.SetMemberVector("value", value.ToVector()); + break; + case GMODVariantType.Angle: + entry.SetMember("type", "Angle"); + entry.SetMemberAngle("value", value.ToAngle()); + break; + case GMODVariantType.Entity: + entry.SetMember("type", "Entity"); + entry.SetMemberEntity("value", value.Int == -1 ? null : LuaEntity.GetEntityFromHandle((uint)value.Int)); + break; + case GMODVariantType.String: + entry.SetMember("type", "String"); + entry.SetMember("value", value.ToString()); + break; + } + result.SetMember(name, entry); + entry.UnReference(); + } + result.Push(); + result.UnReference(); + return 1; + } + + [LuaGlobal] + static int GetHostName(ILuaInterface lua) { + ConVar? hostname = cvar.FindVar("hostname"); + if (hostname == null) + return 0; + lua.PushString(hostname.IsFlagSet(FCvar.NeverAsString) ? "FCVAR_NEVER_AS_STRING" : hostname.GetString()); + return 1; + } +} +#endif From 158a40cbde8741b12a4e97a9334e83b5ed533bee Mon Sep 17 00:00:00 2001 From: march <106459595+marchc1@users.noreply.github.com> Date: Wed, 7 Oct 2026 13:03:12 -0700 Subject: [PATCH 13/20] missed disassembly fix cvttss2si should be (int) cast since I believe the JIT compiles down to that anyway --- Source.Engine/GMODDataTable.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source.Engine/GMODDataTable.cs b/Source.Engine/GMODDataTable.cs index 0016730fd..ea7a21275 100644 --- a/Source.Engine/GMODDataTable.cs +++ b/Source.Engine/GMODDataTable.cs @@ -355,7 +355,7 @@ static bool V_Compare(bf_read a, bf_read b) { } static void E_Write(bf_write buf, in GMODVariant v) { - int h = v.IsIntLike ? v.Int : v.IsFloatLike ? GMODVariant.cvttss2si(v.Float) : v.Type == GMODVariantType.String ? atoi(v.String) : 0; + int h = v.IsIntLike ? v.Int : v.IsFloatLike ? (int)v.Float : v.Type == GMODVariantType.String ? atoi(v.String) : 0; uint encoded = (uint)((h & 0x3FFF) | ((h >> 1) & 0x7FE000)); buf.WriteUBitLong(encoded, Constants.NUM_NETWORKED_EHANDLE_BITS); } From 973fb203690f81e8d37d9407bf924268aab7c17f Mon Sep 17 00:00:00 2001 From: march <106459595+marchc1@users.noreply.github.com> Date: Wed, 7 Oct 2026 13:15:24 -0700 Subject: [PATCH 14/20] fix ROS == null comparison --- Game.Shared/GarrysMod/NetworkString.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Game.Shared/GarrysMod/NetworkString.cs b/Game.Shared/GarrysMod/NetworkString.cs index 9122fd8c2..dd89f0b0a 100644 --- a/Game.Shared/GarrysMod/NetworkString.cs +++ b/Game.Shared/GarrysMod/NetworkString.cs @@ -26,7 +26,7 @@ public static void Create() { if (id <= 0 || pStringTable == null) return null; ReadOnlySpan str = pStringTable.GetString(id); - return str == null ? null : new(str); + return str.IsEmpty ? null : new(str); } public static int Get(ReadOnlySpan name) { From 5ec91b045d0e3d53c653cc4571f593ba1dff4622 Mon Sep 17 00:00:00 2001 From: march <106459595+marchc1@users.noreply.github.com> Date: Wed, 7 Oct 2026 14:22:21 -0700 Subject: [PATCH 15/20] Push/Set TableFromTrace turned to generics Need to check the IL output in linqpad, but I believe this would allow the compiler to devirtualize the ILuaObject calls --- Game.Shared/GarrysMod/LuaUtil.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Game.Shared/GarrysMod/LuaUtil.cs b/Game.Shared/GarrysMod/LuaUtil.cs index 5059ccc1a..f3c24924f 100644 --- a/Game.Shared/GarrysMod/LuaUtil.cs +++ b/Game.Shared/GarrysMod/LuaUtil.cs @@ -116,7 +116,7 @@ static int FilterText(ILuaInterface lua) { } #endif - public static void SetTableFromTrace(ref Trace tr, ILuaObject table) { + public static void SetTableFromTrace(ref Trace tr, T table) where T : ILuaObject { table.SetMember("Hit", tr.DidHit()); table.SetMember("HitWorld", tr.DidHitWorld()); table.SetMember("HitNonWorld", tr.DidHitNonWorldEntity()); @@ -175,7 +175,7 @@ public static void SetTableFromTrace(ref Trace tr, ILuaObject table) { table.SetMemberVector("Normal", normal); } - public static void PushTableFromTrace(ref Trace tr, ILuaObject? table) { + public static void PushTableFromTrace(ref Trace tr, T? table) where T : ILuaObject { if (table != null && table.isTable()) { SetTableFromTrace(ref tr, table); table.Push(); From 7e09be2c423598dcaac7752e0b4ee339caf81ae7 Mon Sep 17 00:00:00 2001 From: march <106459595+marchc1@users.noreply.github.com> Date: Thu, 8 Oct 2026 16:44:24 -0700 Subject: [PATCH 16/20] Add Entity check for VariantsEqualForCopy --- Source.Engine/GMODDataTable.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Source.Engine/GMODDataTable.cs b/Source.Engine/GMODDataTable.cs index ea7a21275..c6e28fb77 100644 --- a/Source.Engine/GMODDataTable.cs +++ b/Source.Engine/GMODDataTable.cs @@ -315,6 +315,7 @@ public void CopyFrom(object? destEntity, object? srcEntity, GMODDataTable src) { GMODVariantType.Bool => (a.Int != 0) == (b.Int != 0), GMODVariantType.Vector or GMODVariantType.Angle => a.Vec.X == b.Vec.X && a.Vec.Y == b.Vec.Y && a.Vec.Z == b.Vec.Z, GMODVariantType.String => a.StringLength == b.StringLength && string.Equals(a.String, b.String, StringComparison.Ordinal), + GMODVariantType.Entity => a.Int == b.Int, _ => true }; From a5f94f02cd41fa8db0069bae281fa2e97eb75893 Mon Sep 17 00:00:00 2001 From: march <106459595+marchc1@users.noreply.github.com> Date: Thu, 8 Oct 2026 18:15:58 -0700 Subject: [PATCH 17/20] fix s_NullLocal being modified --- Source.Engine/GMODDataTable.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/Source.Engine/GMODDataTable.cs b/Source.Engine/GMODDataTable.cs index c6e28fb77..ba469202c 100644 --- a/Source.Engine/GMODDataTable.cs +++ b/Source.Engine/GMODDataTable.cs @@ -3,6 +3,8 @@ using Source.Common.GarrysMod; using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using System.Text; namespace Source.Engine; @@ -67,14 +69,13 @@ Entry FindOrInsert(ushort key) { public bool HasKey(int key) => Entries.ContainsKey((ushort)key); public ref readonly GMODVariant GetLocal(ReadOnlySpan name) { - s_NullLocal = default; - if (name == null) + if (name.IsEmpty) + return ref s_NullLocal; + ref GMODVariant value = ref CollectionsMarshal.GetValueRefOrNullRef(Locals.GetAlternateLookup>(), name); + if (Unsafe.IsNullRef(ref value)) return ref s_NullLocal; - if (Locals.GetAlternateLookup>().TryGetValue(name, out GMODVariant value)) - s_NullLocal = value; - return ref s_NullLocal; + return ref value; } - public void SetLocal(ReadOnlySpan name, in GMODVariant value) => Locals[new string(name)] = value; public void ClearLocal(ReadOnlySpan name) { From 72269bc4142103ad42695bc6c010fb3a9d721c06 Mon Sep 17 00:00:00 2001 From: march <106459595+marchc1@users.noreply.github.com> Date: Thu, 8 Oct 2026 18:22:38 -0700 Subject: [PATCH 18/20] Replace LINQ operation on CopyFrom Instead store node order in a separate list for iteration --- Source.Engine/GMODDataTable.cs | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/Source.Engine/GMODDataTable.cs b/Source.Engine/GMODDataTable.cs index ba469202c..f887e1988 100644 --- a/Source.Engine/GMODDataTable.cs +++ b/Source.Engine/GMODDataTable.cs @@ -28,9 +28,10 @@ class Entry { public int Tick; public GMODVariant Value; - public int Order; } + readonly List<(ushort Key, Entry Entry)> NodeOrder = []; + [ThreadStatic] public static GMODDataTable? s_CurrentTable; [ThreadStatic] public static int s_TargetTick; [ThreadStatic] public static int s_ReferenceTick; @@ -41,7 +42,6 @@ class Entry readonly SortedList Entries = []; readonly Dictionary Locals = new(StringComparer.OrdinalIgnoreCase); bool FullUpdate = true; - int NextOrder; public int GetKey(int it) => Entries.Keys[it]; public ref readonly GMODVariant GetValue(int it) => ref Entries.Values[it].Value; @@ -58,11 +58,16 @@ public ref readonly GMODVariant Get(int key) { Entry FindOrInsert(ushort key) { if (!Entries.TryGetValue(key, out Entry? entry)) { - entry = new() { Order = NextOrder++ }; + entry = new(); Entries.Add(key, entry); + NodeOrder.Add((key, entry)); } return entry; } + void RemoveAllEntries() { + Entries.Clear(); + NodeOrder.Clear(); + } public void Set(int key, in GMODVariant value) => FindOrInsert((ushort)key).Value = value; @@ -79,13 +84,13 @@ public ref readonly GMODVariant GetLocal(ReadOnlySpan name) { public void SetLocal(ReadOnlySpan name, in GMODVariant value) => Locals[new string(name)] = value; public void ClearLocal(ReadOnlySpan name) { - if (name == null) + if (name.IsEmpty) return; Locals.GetAlternateLookup>().Remove(name); } public void Clear() { - Entries.Clear(); + RemoveAllEntries(); Locals.Clear(); } @@ -109,7 +114,7 @@ public void Encode(object? entity, bf_write buf) { public void Decode(object? entity, bf_read buf) { int count = (int)buf.ReadUBitLong(ENTRIES_BITS); if (buf.ReadOneBit() != 0) - Entries.Clear(); + RemoveAllEntries(); for (int i = 0; i < count; i++) { ushort key = (ushort)buf.ReadUBitLong(KEY_BITS); @@ -278,10 +283,9 @@ public void WriteProps(bf_read input, bf_write output, int referenceTick) { } public void CopyFrom(object? destEntity, object? srcEntity, GMODDataTable src) { - foreach (KeyValuePair pair in src.Entries.OrderBy(x => x.Value.Order)) { - ushort key = pair.Key; - int srcTick = pair.Value.Tick; - GMODVariant value = pair.Value.Value; + foreach ((ushort key, Entry srcEntry) in src.NodeOrder) { + int srcTick = srcEntry.Tick; + ref GMODVariant value = ref srcEntry.Value; if (value.Type == GMODVariantType.Entity) { int handle = value.Int; From cd197c46b23e2b73e51c56281e27aeccb3dcc395 Mon Sep 17 00:00:00 2001 From: march <106459595+marchc1@users.noreply.github.com> Date: Thu, 8 Oct 2026 18:48:17 -0700 Subject: [PATCH 19/20] refactoring strings --- Game.Shared/GarrysMod/LuaDataTable.cs | 20 ++++++--- Source.Common/GarrysMod/GMODVariant.cs | 59 ++++++++++++++------------ Source.Engine/GMODDataTable.cs | 15 ++++--- 3 files changed, 54 insertions(+), 40 deletions(-) diff --git a/Game.Shared/GarrysMod/LuaDataTable.cs b/Game.Shared/GarrysMod/LuaDataTable.cs index fa6a34723..c39fe10bd 100644 --- a/Game.Shared/GarrysMod/LuaDataTable.cs +++ b/Game.Shared/GarrysMod/LuaDataTable.cs @@ -70,7 +70,7 @@ public static GMODVariant Get_GMODVariant(int stackPos, bool asInt) { variant.SetFloat((float)g_Lua.GetNumber(stackPos)); break; case LuaType.String: - variant.SetString(g_Lua.GetString(stackPos)); + variant.SetString(g_Lua.GetStringBytes(stackPos)); break; case LuaType.Entity: BaseEntity? ent = LuaEntity.Get_Entity(stackPos, true); @@ -110,7 +110,7 @@ public static void Push_GMODVariant(in GMODVariant variant) { LuaEntity.Push_Entity(LuaEntity.GetEntityFromHandle((uint)variant.Int)); break; case GMODVariantType.String: - g_Lua!.PushString(variant.String); + g_Lua!.PushString(variant.StringBytes); break; default: g_Lua!.PushNil(); @@ -118,6 +118,13 @@ public static void Push_GMODVariant(in GMODVariant variant) { } } + static void PushVariantString(in GMODVariant variant) { + if (variant.Type == GMODVariantType.String) + g_Lua!.PushString(variant.StringBytes); + else + g_Lua!.PushString(variant.ToString()); + } + static bool VariantsEqual(in GMODVariant a, in GMODVariant b) { if (a.Type != b.Type) return false; @@ -126,7 +133,7 @@ static bool VariantsEqual(in GMODVariant a, in GMODVariant b) { GMODVariantType.Int or GMODVariantType.Entity => a.Int == b.Int, GMODVariantType.Bool => (a.Int != 0) == (b.Int != 0), GMODVariantType.Vector or GMODVariantType.Angle => a.Vec.X == b.Vec.X && a.Vec.Y == b.Vec.Y && a.Vec.Z == b.Vec.Z, - GMODVariantType.String => a.StringLength == b.StringLength && string.Equals(a.String, b.String, StringComparison.Ordinal), + GMODVariantType.String => a.StringLength == b.StringLength && a.StringBytes.AsSpan().SequenceEqual(b.StringBytes), _ => true }; } @@ -532,7 +539,7 @@ static int GetGlobal2Bool(ILuaInterface lua) { [LuaMethod("GetNetworked2String")] static int Entity__GetNetworked2String(ILuaInterface lua) { if (GetNetworked2(out GMODVariant value, out _)) - g_Lua!.PushString(value.ToString()); + PushVariantString(value); return 1; } @@ -542,7 +549,7 @@ static int Entity__GetNetworked2String(ILuaInterface lua) { [LuaGlobal] static int GetGlobal2String(ILuaInterface lua) { if (GetGlobal2(out GMODVariant value, out _)) - g_Lua!.PushString(value.ToString()); + PushVariantString(value); return 1; } @@ -695,7 +702,8 @@ static int Entity__GetNetworked2VarTable(ILuaInterface lua) { break; case GMODVariantType.String: entry.SetMember("type", "String"); - entry.SetMember("value", value.ToString()); + g_Lua!.PushString(value.StringBytes); + entry.SetMember("value"); break; } result.SetMember(name, entry); diff --git a/Source.Common/GarrysMod/GMODVariant.cs b/Source.Common/GarrysMod/GMODVariant.cs index 22b72d020..2e62c2e9a 100644 --- a/Source.Common/GarrysMod/GMODVariant.cs +++ b/Source.Common/GarrysMod/GMODVariant.cs @@ -31,7 +31,7 @@ public struct GMODVariant [FieldOffset(4)] public Vector3 Vec; [FieldOffset(4)] public QAngle Ang; [FieldOffset(4)] public ushort StringLength; - [FieldOffset(16)] public string? String; + [FieldOffset(16)] public byte[]? StringBytes; public readonly bool IsIntLike => Type is GMODVariantType.Int or GMODVariantType.Bool or GMODVariantType.Entity; public readonly bool IsFloatLike => Type is GMODVariantType.Float or GMODVariantType.Vector or GMODVariantType.Angle; @@ -39,62 +39,64 @@ public struct GMODVariant public void SetFloat(float value) { Type = GMODVariantType.Float; Vec = new(value, 0, 0); - String = null; + StringBytes = null; } public void SetInt(int value) { Type = GMODVariantType.Int; Vec = default; Int = value; - String = null; + StringBytes = null; } public void SetBool(bool value) { Type = GMODVariantType.Bool; Vec = default; Int = value ? 1 : 0; - String = null; + StringBytes = null; } public void SetVector(in Vector3 value) { Type = GMODVariantType.Vector; Vec = value; - String = null; + StringBytes = null; } public void SetAngle(in QAngle value) { Type = GMODVariantType.Angle; Ang = value; - String = null; + StringBytes = null; } public void SetEntity(int handle) { Type = GMODVariantType.Entity; Vec = default; Int = handle; - String = null; + StringBytes = null; } - public void SetString(ReadOnlySpan value) { - value = value.SliceNullTerminatedString(); - int bytes = Encoding.UTF8.GetByteCount(value); - if (bytes + 1 > MAX_STRING_LENGTH) { - byte[] buffer = new byte[bytes]; - Encoding.UTF8.GetBytes(value, buffer); - String = Encoding.UTF8.GetString(buffer, 0, MAX_STRING_LENGTH - 1); - StringLength = MAX_STRING_LENGTH; - } - else { - String = new string(value); - StringLength = (ushort)(bytes + 1); - } + public void SetString(ReadOnlySpan value) { + int nul = value.IndexOf((byte)0); + if (nul >= 0) + value = value[..nul]; + if (value.Length > MAX_STRING_LENGTH - 1) + value = value[..(MAX_STRING_LENGTH - 1)]; Type = GMODVariantType.String; + Vec = default; + StringBytes = value.ToArray(); + StringLength = (ushort)(value.Length + 1); + } + + readonly ReadOnlySpan StringAsChars(Span buffer) { + ReadOnlySpan bytes = StringBytes; + Encoding.Latin1.GetChars(bytes, buffer); + return buffer[..bytes.Length]; } public void Clear() { Type = GMODVariantType.NIL; Vec = default; - String = null; + StringBytes = null; } public readonly int ToInt() { @@ -103,7 +105,7 @@ public readonly int ToInt() { if (IsFloatLike) return (int)Float; if (Type == GMODVariantType.String) - return atoi(String); + return atoi(StringAsChars(stackalloc char[MAX_STRING_LENGTH])); return 0; } @@ -113,7 +115,7 @@ public readonly float ToFloat() { if (IsFloatLike) return Float; if (Type == GMODVariantType.String) - return (float)atof(String); + return (float)atof(StringAsChars(stackalloc char[MAX_STRING_LENGTH])); return 0; } @@ -123,9 +125,10 @@ public readonly bool ToBool() { if (IsFloatLike) return Float != 0 || float.IsNaN(Float); if (Type == GMODVariantType.String) { - if (string.IsNullOrEmpty(String) || String.Equals("false", StringComparison.OrdinalIgnoreCase)) + ReadOnlySpan bytes = StringBytes; + if (bytes.IsEmpty || Ascii.EqualsIgnoreCase(bytes, "false"u8)) return false; - return atoi(String) != 0; + return atoi(StringAsChars(stackalloc char[MAX_STRING_LENGTH])) != 0; } return false; } @@ -139,7 +142,7 @@ public readonly Vector3 ToVector() { return Vec; if (Type == GMODVariantType.String) { Span values = stackalloc float[3]; - if (ScanFloats(String, values) == 3) + if (ScanFloats(StringAsChars(stackalloc char[MAX_STRING_LENGTH]), values) == 3) return new(values[0], values[1], values[2]); } return default; @@ -156,7 +159,7 @@ public readonly int ToHandle() { if (IsFloatLike) return (int)Float; if (Type == GMODVariantType.String) - return atoi(String); + return atoi(StringAsChars(stackalloc char[MAX_STRING_LENGTH])); return 0; } @@ -165,7 +168,7 @@ public readonly int ToHandle() { GMODVariantType.Int or GMODVariantType.Entity => Int.ToString(), GMODVariantType.Bool => Int != 0 ? "true" : "false", GMODVariantType.Vector or GMODVariantType.Angle => $"{FormatG(Vec.X)} {FormatG(Vec.Y)} {FormatG(Vec.Z)}", - GMODVariantType.String => String ?? "", + GMODVariantType.String => StringBytes == null ? "" : Encoding.UTF8.GetString(StringBytes), _ => "" }; } diff --git a/Source.Engine/GMODDataTable.cs b/Source.Engine/GMODDataTable.cs index f887e1988..d7e80e65f 100644 --- a/Source.Engine/GMODDataTable.cs +++ b/Source.Engine/GMODDataTable.cs @@ -319,7 +319,7 @@ public void CopyFrom(object? destEntity, object? srcEntity, GMODDataTable src) { GMODVariantType.Int => a.Int == b.Int, GMODVariantType.Bool => (a.Int != 0) == (b.Int != 0), GMODVariantType.Vector or GMODVariantType.Angle => a.Vec.X == b.Vec.X && a.Vec.Y == b.Vec.Y && a.Vec.Z == b.Vec.Z, - GMODVariantType.String => a.StringLength == b.StringLength && string.Equals(a.String, b.String, StringComparison.Ordinal), + GMODVariantType.String => a.StringLength == b.StringLength && a.StringBytes.AsSpan().SequenceEqual(b.StringBytes), GMODVariantType.Entity => a.Int == b.Int, _ => true }; @@ -381,18 +381,21 @@ static void E_Read(bf_read buf, ref GMODVariant v) { static bool E_Compare(bf_read a, bf_read b) => a.ReadUBitLong(Constants.NUM_NETWORKED_EHANDLE_BITS) != b.ReadUBitLong(Constants.NUM_NETWORKED_EHANDLE_BITS); static void S_Write(bf_write buf, in GMODVariant v) { - string str = v.Type == GMODVariantType.Entity ? "" : v.ToString(); - byte[] bytes = Encoding.UTF8.GetBytes(str); + Span formatted = stackalloc byte[GMODVariant.MAX_STRING_LENGTH]; + ReadOnlySpan bytes = v.Type switch { + GMODVariantType.String => v.StringBytes, + GMODVariantType.Entity => default, + _ => formatted[..Encoding.ASCII.GetBytes(v.ToString(), formatted)] + }; int len = bytes.Length < 0x200 ? bytes.Length : 0x1FF; buf.WriteUBitLong((uint)len, STRING_LENGTH_BITS); - buf.WriteBytes(bytes.AsSpan(0, len)); + buf.WriteBytes(bytes[..len]); } static void S_Read(bf_read buf, ref GMODVariant v) { int len = (int)buf.ReadUBitLong(STRING_LENGTH_BITS); Span bytes = stackalloc byte[len]; buf.ReadBytes(bytes); - int nul = bytes.IndexOf((byte)0); - v.SetString(Encoding.UTF8.GetString(nul >= 0 ? bytes[..nul] : bytes)); + v.SetString(bytes); } static void S_Skip(bf_read buf) => buf.SeekRelative((int)buf.ReadUBitLong(STRING_LENGTH_BITS) * 8); static bool S_Compare(bf_read a, bf_read b) { From 497dc99f4264bd6c5ae92c6d79e62e09e3b2200a Mon Sep 17 00:00:00 2001 From: march <106459595+marchc1@users.noreply.github.com> Date: Thu, 8 Oct 2026 18:49:04 -0700 Subject: [PATCH 20/20] GMODVariant.ToInt inlined here --- Source.Engine/GMODDataTable.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source.Engine/GMODDataTable.cs b/Source.Engine/GMODDataTable.cs index d7e80e65f..6a8884c3d 100644 --- a/Source.Engine/GMODDataTable.cs +++ b/Source.Engine/GMODDataTable.cs @@ -361,7 +361,7 @@ static bool V_Compare(bf_read a, bf_read b) { } static void E_Write(bf_write buf, in GMODVariant v) { - int h = v.IsIntLike ? v.Int : v.IsFloatLike ? (int)v.Float : v.Type == GMODVariantType.String ? atoi(v.String) : 0; + int h = v.ToInt(); uint encoded = (uint)((h & 0x3FFF) | ((h >> 1) & 0x7FE000)); buf.WriteUBitLong(encoded, Constants.NUM_NETWORKED_EHANDLE_BITS); }