From 930391cfcba2e1161a39ba5f75e3924f70923c7b Mon Sep 17 00:00:00 2001 From: callumok2004 Date: Sun, 22 Feb 2026 23:00:03 +0000 Subject: [PATCH 01/31] Process usercmds --- Game.Server/GameInterface.cs | 45 +++++++++++++- Game.Shared/UserCmd.cs | 79 +++++++++++++++++++++++-- Source.Common/Networking/NetMessages.cs | 10 ++++ Source.Engine/Client/BaseClientState.cs | 1 + Source.Engine/SV.cs | 44 +++++++------- Source.Engine/Server/GameClient.cs | 34 ++++++++--- 6 files changed, 177 insertions(+), 36 deletions(-) diff --git a/Game.Server/GameInterface.cs b/Game.Server/GameInterface.cs index 8d8917c5..9afd1a4c 100644 --- a/Game.Server/GameInterface.cs +++ b/Game.Server/GameInterface.cs @@ -390,8 +390,49 @@ public void NetworkIDValidated(ReadOnlySpan userName, ReadOnlySpan n throw new NotImplementedException(); } - public double ProcessUsercmds(Edict player, bf_read buf, int numCmds, int totalCmds, int droppedPackets, bool ignore, bool paused) { - throw new NotImplementedException(); + public TimeUnit_t ProcessUsercmds(Edict player, bf_read buf, int numCmds, int totalCmds, int droppedPackets, bool ignore, bool paused) { + int i; + + UserCmd from, to; + + UserCmd[] cmds = new UserCmd[64]; // CMD_MAXBACKUP + + UserCmd cmdNull = new(); + + Assert(numCmds >= 0); + Assert((totalCmds - numCmds) >= 0); + + BasePlayer? pl = null; + BaseEntity? ent = BaseEntity.Instance(player); + + if (ent != null && ent.IsPlayer()) + pl = (BasePlayer)ent; + + if (totalCmds < 0 || totalCmds >= (64 /*CMD_MAXBACKUP*/ - 1)) { + ReadOnlySpan name = "unknown"; + if (pl != null) + name = pl.GetPlayerName(); + + Msg($"CBasePlayer::ProcessUsercmds: too many cmds {totalCmds} sent for player {name}\n"); + buf.SetOverflowFlag(); + return 0.0f; + } + + cmdNull.Reset(); + from = cmdNull; + + for (i = totalCmds - 1; i >= 0; i--) { + to = cmds[i]; + UserCmd.ReadUsercmd(buf, ref to, ref from); + from = to; + } + + if (ignore || pl == null) + return 0.0f; + + // pl.ProcessUsercmds(cmds, numCmds, totalCmds, droppedPackets, paused); TODO + + return TICK_INTERVAL; } public void SetCommandClient(int index) { diff --git a/Game.Shared/UserCmd.cs b/Game.Shared/UserCmd.cs index dc933de9..dd438731 100644 --- a/Game.Shared/UserCmd.cs +++ b/Game.Shared/UserCmd.cs @@ -30,14 +30,14 @@ public void Reset() { HasBeenPredicted = false; - for (int i = 0; i < MAX_BUTTONS_PRESSED; i++) + for (int i = 0; i < MAX_BUTTONS_PRESSED; i++) ButtonsPressed[i] = 0; ScrollWheelSpeed = 0; WorldClicking = false; WorldClickDirection = new(0); IsTyping = false; - for (int i = 0; i < MAX_MOTION_SENSOR_POSITIONS; i++) + for (int i = 0; i < MAX_MOTION_SENSOR_POSITIONS; i++) MotionSensorPositions[i] = new(0); Forced = false; } @@ -146,7 +146,78 @@ public void Reset() { public bool Forced; public static void ReadUsercmd(bf_read buf, ref UserCmd move, ref UserCmd from) { - // TODO: implement + move = from; + + if (buf.ReadOneBit() != 0) + move.CommandNumber = (int)buf.ReadUBitLong(32); + else + move.CommandNumber = from.CommandNumber + 1; + + if (buf.ReadOneBit() != 0) + move.TickCount = (int)buf.ReadUBitLong(32); + else + move.TickCount = from.TickCount + 1; + + if (buf.ReadOneBit() != 0) + move.ViewAngles.X = buf.ReadFloat(); + + if (buf.ReadOneBit() != 0) + move.ViewAngles.Y = buf.ReadFloat(); + + if (buf.ReadOneBit() != 0) + move.ViewAngles.Z = buf.ReadFloat(); + + if (buf.ReadOneBit() != 0) + move.ForwardMove = buf.ReadFloat(); + + if (buf.ReadOneBit() != 0) + move.SideMove = buf.ReadFloat(); + + if (buf.ReadOneBit() != 0) + move.UpMove = buf.ReadFloat(); + + if (buf.ReadOneBit() != 0) + move.Buttons = (InButtons)buf.ReadUBitLong(32); + + if (buf.ReadOneBit() != 0) + move.Impulse = (byte)buf.ReadUBitLong(8); + + if (buf.ReadOneBit() != 0) { + move.WeaponSelect = (int)buf.ReadUBitLong(MAX_EDICT_BITS); + if (buf.ReadOneBit() != 0) + move.WeaponSubtype = (int)buf.ReadUBitLong(WEAPON_SUBTYPE_BITS); + } + + move.RandomSeed = move.CommandNumber & 0x7fffffff; // TODO MD5_PseudoRandom + + if (buf.ReadOneBit() != 0) + move.MouseDeltaX = buf.ReadShort(); + + if (buf.ReadOneBit() != 0) + move.MouseDeltaY = buf.ReadShort(); + + if (buf.ReadOneBit() != 0) { + for (int i = 0; i < MAX_BUTTONS_PRESSED; i++) + move.ButtonsPressed[i] = (byte)buf.ReadUBitLong(8); + } + + if (buf.ReadOneBit() != 0) + move.ScrollWheelSpeed = (sbyte)buf.ReadSBitLong(8); + + if (buf.ReadOneBit() != 0) + move.WorldClicking = buf.ReadOneBit() != 0; + + if (buf.ReadOneBit() != 0) + move.WorldClickDirection = buf.ReadBitVec3Normal(); + + move.IsTyping = buf.ReadOneBit() != 0; + + if (buf.ReadOneBit() != 0) { + for (int i = 0; i < MAX_MOTION_SENSOR_POSITIONS; i++) + move.MotionSensorPositions[i] = buf.ReadBitVec3Coord(); + } + + move.Forced = buf.ReadOneBit() != 0; } static bool HasChanged(Source.InlineArray5 from, Source.InlineArray5 to) where T : IEquatable { @@ -319,7 +390,7 @@ public static void WriteUsercmd(bf_write buf, in UserCmd to, in UserCmd from) { if (HasChanged(to.MotionSensorPositions, from.MotionSensorPositions)) { buf.WriteOneBit(1); - for (int i = 0; i < MAX_MOTION_SENSOR_POSITIONS; i++) { + for (int i = 0; i < MAX_MOTION_SENSOR_POSITIONS; i++) { buf.WriteBitVec3Coord(to.MotionSensorPositions[i]); } } diff --git a/Source.Common/Networking/NetMessages.cs b/Source.Common/Networking/NetMessages.cs index 1065ede0..d2a42b45 100644 --- a/Source.Common/Networking/NetMessages.cs +++ b/Source.Common/Networking/NetMessages.cs @@ -577,6 +577,12 @@ public SVC_SetView() : base(SVC.SetView) { } public int EntityIndex; + public override bool WriteToBuffer(bf_write buffer) { + buffer.WriteNetMessageType(this); + buffer.WriteUBitLong((uint)EntityIndex, MAX_EDICT_BITS); + return !buffer.Overflowed; + } + public override bool ReadFromBuffer(bf_read buffer) { EntityIndex = (int)buffer.ReadUBitLong(MAX_EDICT_BITS); return !buffer.Overflowed; @@ -587,6 +593,10 @@ public class SVC_FixAngle : NetMessage public bool Relative; public QAngle Angle; public SVC_FixAngle() : base(SVC.FixAngle) { } + public SVC_FixAngle(bool relative, QAngle angle) : base(SVC.FixAngle) { + Relative = relative; + Angle = angle; + } public override bool ReadFromBuffer(bf_read buffer) { Relative = buffer.ReadBool(); diff --git a/Source.Engine/Client/BaseClientState.cs b/Source.Engine/Client/BaseClientState.cs index 2b9f5dcc..2e3f556c 100644 --- a/Source.Engine/Client/BaseClientState.cs +++ b/Source.Engine/Client/BaseClientState.cs @@ -369,6 +369,7 @@ private bool ProcessUserMessage(SVC_UserMessage msg) { } protected virtual bool ProcessSetView(SVC_SetView msg) { + ViewEntity = msg.EntityIndex; return true; } diff --git a/Source.Engine/SV.cs b/Source.Engine/SV.cs index 6dba0c52..5392ab51 100644 --- a/Source.Engine/SV.cs +++ b/Source.Engine/SV.cs @@ -17,15 +17,15 @@ namespace Source.Engine; /// public class SV(IServiceProvider services, Cbuf Cbuf, ED ED, Host Host, CommonHostState host_state, IEngineVGuiInternal EngineVGui, ICvar cvar, IModelLoader modelloader, ServerGlobalVariables serverGlobalVariables, Con Con, [FromKeyedServices(Realm.Server)] NetworkStringTableContainer networkStringTableContainerServer, IHostState HostState, ServerPlugin serverPluginHandler) { - public IServerGameDLL? ServerGameDLL; - public IServerGameEnts? ServerGameEnts; - public IServerGameClients? ServerGameClients; + public static IServerGameDLL? ServerGameDLL; + public static IServerGameEnts? ServerGameEnts; + public static IServerGameClients? ServerGameClients; - public static readonly ConVar sv_pure_kick_clients = new( "sv_pure_kick_clients", "1", 0, "If set to 1, the server will kick clients with mismatching files. Otherwise, it will issue a warning to the client." ); - public static readonly ConVar sv_pure_trace = new( "sv_pure_trace", "0", 0, "If set to 1, the server will print a message whenever a client is verifying a CRC for a file." ); - public static readonly ConVar sv_pure_consensus = new( "sv_pure_consensus", "5", 0, "Minimum number of file hashes to agree to form a consensus." ); - public static readonly ConVar sv_pure_retiretime = new( "sv_pure_retiretime", "900", 0, "Seconds of server idle time to flush the sv_pure file hash cache." ); - public static readonly ConVar sv_lan = new( "sv_lan", "0", 0, "Server is a lan server ( no heartbeat, no authentication, no non-class C addresses )" ); + public static readonly ConVar sv_pure_kick_clients = new("sv_pure_kick_clients", "1", 0, "If set to 1, the server will kick clients with mismatching files. Otherwise, it will issue a warning to the client."); + public static readonly ConVar sv_pure_trace = new("sv_pure_trace", "0", 0, "If set to 1, the server will print a message whenever a client is verifying a CRC for a file."); + public static readonly ConVar sv_pure_consensus = new("sv_pure_consensus", "5", 0, "Minimum number of file hashes to agree to form a consensus."); + public static readonly ConVar sv_pure_retiretime = new("sv_pure_retiretime", "900", 0, "Seconds of server idle time to flush the sv_pure file hash cache."); + public static readonly ConVar sv_lan = new("sv_lan", "0", 0, "Server is a lan server ( no heartbeat, no authentication, no non-class C addresses )"); public static ConVar sv_cheats = new(nameof(sv_cheats), "0", FCvar.Notify | FCvar.Replicated, "Allow cheats on server", callback: SV_CheatsChanged); @@ -86,11 +86,11 @@ private void InitSendTables(ServerClass? classes) { services.GetRequiredService().Init(tables.AsSpan()[..numTables]); } - readonly ConVar tv_enable = new( "tv_enable", "0", FCvar.Notify, "Activates SourceTV on server." ); + readonly ConVar tv_enable = new("tv_enable", "0", FCvar.Notify, "Activates SourceTV on server."); [ConCommand(helpText: "Change the maximum number of players allowed on this server.")] - void maxplayers(in TokenizedCommand args){ + void maxplayers(in TokenizedCommand args) { if (args.ArgC() != 2) { ConMsg($"\"maxplayers\" is \"{sv.GetMaxClients()}\"\n"); return; @@ -104,7 +104,7 @@ void maxplayers(in TokenizedCommand args){ SetupMaxPlayers(int.TryParse(args[1], out int i) ? i : 0); } - public void SetupMaxPlayers(int desiredMaxPlayers){ + public void SetupMaxPlayers(int desiredMaxPlayers) { int minmaxplayers = 1; int maxmaxplayers = Constants.ABSOLUTE_PLAYER_LIMIT; int defaultmaxplayers = 1; @@ -112,14 +112,14 @@ public void SetupMaxPlayers(int desiredMaxPlayers){ if (ServerGameClients != null) { ServerGameClients.GetPlayerLimits(out minmaxplayers, out maxmaxplayers, out defaultmaxplayers); - if (minmaxplayers < 1) + if (minmaxplayers < 1) Sys.Error($"GetPlayerLimits: min maxplayers must be >= 1 ({minmaxplayers})"); - else if (defaultmaxplayers < 1) + else if (defaultmaxplayers < 1) Sys.Error($"GetPlayerLimits: default maxplayers must be >= 1 ({minmaxplayers})"); - if (minmaxplayers > maxmaxplayers || defaultmaxplayers > maxmaxplayers) + if (minmaxplayers > maxmaxplayers || defaultmaxplayers > maxmaxplayers) Sys.Error($"GetPlayerLimits: min maxplayers {minmaxplayers} > max {maxmaxplayers}"); - if (maxmaxplayers > Constants.ABSOLUTE_PLAYER_LIMIT) + if (maxmaxplayers > Constants.ABSOLUTE_PLAYER_LIMIT) Sys.Error($"GetPlayerLimits: max players limited to {Constants.ABSOLUTE_PLAYER_LIMIT}"); } @@ -246,17 +246,17 @@ public bool IsSimulating() { if (sv.IsPaused()) return false; -# if !SWDS +#if !SWDS if (!sv.IsMultiplayer()) { if (cl.IsActive() && (Con.IsVisible() || EngineVGui.ShouldPause())) return false; } -#endif +#endif return true; } ConVar? sv_noclipduringpause; internal void Frame(bool finalTick) { - if (ServerGameDLL!= null && finalTick) + if (ServerGameDLL != null && finalTick) ServerGameDLL.Think(finalTick); if (!sv.IsActive() || !Host.ShouldRun()) { @@ -266,7 +266,7 @@ internal void Frame(bool finalTick) { serverGlobalVariables.FrameTime = host_state.IntervalPerTick; bool isSimulating = IsSimulating(); - bool sendDuringPause = sv_noclipduringpause != null? sv_noclipduringpause.GetBool() : false; + bool sendDuringPause = sv_noclipduringpause != null ? sv_noclipduringpause.GetBool() : false; sv.RunFrame(); @@ -283,7 +283,7 @@ internal void Frame(bool finalTick) { Think(isSimulating); } else if (sv.IsMultiplayer()) { - Think(false); + Think(false); } sv.SimulatingTicks = simulated; @@ -292,11 +292,11 @@ internal void Frame(bool finalTick) { if (!EngineThreads.IsEngineThreaded() || sv.IsMultiplayer()) SendClientUpdates(isSimulating, sendDuringPause); // else - // DeferredServerWork = CreateFunctor(SendClientUpdates, isSimulating, sendDuringPause); + // DeferredServerWork = CreateFunctor(SendClientUpdates, isSimulating, sendDuringPause); } - if (IsPC() && sv.IsMultiplayer()) + if (IsPC() && sv.IsMultiplayer()) Steam3Server().RunFrame(); } diff --git a/Source.Engine/Server/GameClient.cs b/Source.Engine/Server/GameClient.cs index 88b088d5..a3e5abe4 100644 --- a/Source.Engine/Server/GameClient.cs +++ b/Source.Engine/Server/GameClient.cs @@ -90,17 +90,17 @@ protected override bool ProcessMove(CLC_Move m) { int startBit = m.DataIn.BitsRead; - // processusercmds + SV.ServerGameClients!.ProcessUsercmds(Edict, m.DataIn, m.NewCommands, totalCmds, netDrop, ignore, paused); if (m.DataIn.Overflowed) { - // Disconnect("ProcessUsercmds: Overflowed reading usercmd data (check sending and receiving code for mismatches)!\n"); - // return false; + Disconnect("ProcessUsercmds: Overflowed reading usercmd data (check sending and receiving code for mismatches)!\n"); + return false; } int endBit = m.DataIn.BitsRead; if (m.Length != (endBit - startBit)) { - // Disconnect("ProcessUsercmds: Incorrect reading frame (check sending and receiving code for mismatches)!\n"); - // return false; + Disconnect("ProcessUsercmds: Incorrect reading frame (check sending and receiving code for mismatches)!\n"); + return false; } return true; @@ -195,7 +195,7 @@ protected override void ActivatePlayer() { } protected override bool SendSignonData() { - bool clientHasDirrentTables = false; + bool clientHasDifferentTables = false; if (false) { @@ -234,13 +234,31 @@ protected override void SpawnPlayer() { base.SpawnPlayer(); - // serverGameClient.ClientSpawned(edict); + // SV.ServerGameClients!.ClientSpawned(Edict); } // ClientFrame GetDeltaFrame(int tick) { } void WriteViewAngleUpdate() { - + if (IsFakeClient()) + return; + + PlayerState pl = SV.ServerGameClients!.GetPlayerState(Edict); + Assert(pl != null); + + if (pl != null && pl.FixAngle != (int)FixAngle.None) { + if (pl.FixAngle == (int)FixAngle.Relative) { + SVC_FixAngle fixAngle = new(true, pl.AngleChange); + NetChannel.SendNetMsg(fixAngle); + pl.AngleChange.Init(); + } + else { + SVC_FixAngle fixAngle = new(false, pl.ViewingAngle); + NetChannel.SendNetMsg(fixAngle); + } + + pl.FixAngle = (int)FixAngle.None; + } } // bool IsEngineClientCommand(in TokenizedCommand args) { } From acbf638968ff5b860c876455c6048c6683ce7c3e Mon Sep 17 00:00:00 2001 From: callumok2004 Date: Mon, 23 Feb 2026 19:34:37 +0000 Subject: [PATCH 02/31] more server --- Source.Common/IGameEvents.cs | 8 +- Source.Common/Networking/NetMessages.cs | 30 ++- Source.Common/SoundInfo.cs | 128 ++++++++++++ Source.Engine/ClientFrame.cs | 6 + Source.Engine/GameEventManager.cs | 52 ++++- Source.Engine/Host.cs | 54 ++++- Source.Engine/NetworkStringTable.cs | 12 +- Source.Engine/Server/BaseClient.cs | 102 ++++++++-- Source.Engine/Server/GameClient.cs | 249 ++++++++++++++++++++++-- Source.Engine/Server/GameServer.cs | 34 +++- Source.Engine/SourceDLLMain.cs | 1 + 11 files changed, 621 insertions(+), 55 deletions(-) diff --git a/Source.Common/IGameEvents.cs b/Source.Common/IGameEvents.cs index 83cbde11..3bb9a763 100644 --- a/Source.Common/IGameEvents.cs +++ b/Source.Common/IGameEvents.cs @@ -43,10 +43,12 @@ public interface IGameEventManager2 { int LoadEventsFromFile(ReadOnlySpan filename); void Reset(); + bool AddListener(object listener, GameEventDescriptor descriptor, GameEventListenerType listenerType); bool AddListener(IGameEventListener2 listener, ReadOnlySpan name, bool serverSide); bool FindListener(IGameEventListener2 listener, ReadOnlySpan name); void RemoveListener(IGameEventListener2 listener); IGameEvent? CreateEvent(ReadOnlySpan name, bool force = false); + GameEventDescriptor? GetEventDescriptor(int eventid); bool FireEvent(IGameEvent ev, bool dontBroadcast = false); bool FireEventClientSide(IGameEvent ev); IGameEvent DuplicateEvent(IGameEvent ev); @@ -114,7 +116,8 @@ public GameEvent(GameEventDescriptor descriptor) { public KeyValues? DataKeys; } -public enum GameEventListenerType { +public enum GameEventListenerType +{ Serverside, Clientside, Clientstub, @@ -122,7 +125,8 @@ public enum GameEventListenerType { ClientsideOld } -public enum GameEventType { +public enum GameEventType +{ Local, String, Float, diff --git a/Source.Common/Networking/NetMessages.cs b/Source.Common/Networking/NetMessages.cs index d2a42b45..192d31a9 100644 --- a/Source.Common/Networking/NetMessages.cs +++ b/Source.Common/Networking/NetMessages.cs @@ -458,6 +458,7 @@ public SVC_Sounds() : base(SVC.Sounds) { } public int NumSounds; public int Length; public readonly bf_read DataIn = new(); + public readonly bf_write DataOut = new(); public override bool ReadFromBuffer(bf_read buffer) { ReliableSound = buffer.ReadOneBit() != 0; @@ -475,7 +476,18 @@ public override bool ReadFromBuffer(bf_read buffer) { } public override bool WriteToBuffer(bf_write buffer) { - throw new Exception(); + buffer.WriteNetMessageType(this); + if (ReliableSound) { + buffer.WriteOneBit(1); + buffer.WriteUBitLong((uint)Length, 8); + } + else { + buffer.WriteOneBit(0); + buffer.WriteUBitLong((uint)NumSounds, 8); + buffer.WriteUBitLong((uint)Length, 16); + } + + return buffer.WriteBits(DataOut.BaseArray, Length); } public override string ToString() { return $"number {NumSounds},{(ReliableSound ? " reliable" : " ")} bytes {Bits2Bytes(Length)}"; @@ -518,7 +530,21 @@ public override bool ReadFromBuffer(bf_read buffer) { } public override bool WriteToBuffer(bf_write buffer) { - throw new Exception(); + buffer.WriteNetMessageType(this); + buffer.WriteBitVec3Coord(Pos); + buffer.WriteUBitLong((uint)DecalTextureIndex, MAX_DECAL_INDEX_BITS); + + if (EntityIndex != 0 || ModelIndex != 0) { + buffer.WriteBool(true); + buffer.WriteUBitLong((uint)EntityIndex, MAX_EDICT_BITS); + buffer.WriteUBitLong((uint)ModelIndex, SP_MODEL_INDEX_BITS); + } + else + buffer.WriteBool(false); + + buffer.WriteBool(LowPriority); + + return !buffer.Overflowed; } } public class SVC_GameEvent : NetMessage diff --git a/Source.Common/SoundInfo.cs b/Source.Common/SoundInfo.cs index 764872a2..28635fd3 100644 --- a/Source.Common/SoundInfo.cs +++ b/Source.Common/SoundInfo.cs @@ -22,6 +22,8 @@ public static class SoundConstants public const float SOUND_DELAY_OFFSET = 0.1f; public const int MAX_SOUND_DELAY_MSEC_ENCODE_BITS = 13; + public const int MAX_SOUND_DELAY_MSEC = (1 << (MAX_SOUND_DELAY_MSEC_ENCODE_BITS - 1)) - 1; + public const int SND_FLAG_BITS_ENCODE = 12; public const int PITCH_NORM = 100; @@ -161,6 +163,132 @@ public void ReadDelta(ref SoundInfo delta, bf_read buffer, int nProtoVersion) { } } + public void WriteDelta(ref SoundInfo delta, bf_write buffer, int nProtoVersion) { + if (EntityIndex == delta.EntityIndex) + buffer.WriteOneBit(0); + else { + buffer.WriteOneBit(1); + if (EntityIndex <= 31) { + buffer.WriteOneBit(1); + buffer.WriteUBitLong((uint)EntityIndex, 5); + } + else { + buffer.WriteOneBit(0); + buffer.WriteUBitLong((uint)EntityIndex, Constants.MAX_EDICT_BITS); + } + } + + if (nProtoVersion > 22) { + if (SoundNum == delta.SoundNum) + buffer.WriteOneBit(0); + else { + buffer.WriteOneBit(1); + buffer.WriteUBitLong((uint)SoundNum, StringTableBits.MaxSoundIndexBits); + } + } + else { + if (SoundNum == delta.SoundNum) + buffer.WriteOneBit(0); + else { + buffer.WriteOneBit(1); + buffer.WriteUBitLong((uint)SoundNum, 13); + } + } + + if (nProtoVersion > 18) { + if (Flags == delta.Flags) + buffer.WriteOneBit(0); + else { + buffer.WriteOneBit(1); + buffer.WriteUBitLong((uint)Flags, SND_FLAG_BITS_ENCODE); + } + } + else { + if (Flags == delta.Flags) + buffer.WriteOneBit(0); + else { + buffer.WriteOneBit(1); + buffer.WriteUBitLong((uint)Flags, 9); + } + } + + if (Channel == delta.Channel) + buffer.WriteOneBit(0); + else { + buffer.WriteOneBit(1); + buffer.WriteUBitLong((uint)Channel, 3); + } + + buffer.WriteOneBit(IsAmbient ? 1 : 0); + buffer.WriteOneBit(IsSentence ? 1 : 0); + + if (Flags != SoundFlags.Stop) { + if (SequenceNumber == delta.SequenceNumber) + buffer.WriteOneBit(1); + else if (SequenceNumber == delta.SequenceNumber + 1) { + buffer.WriteOneBit(0); + buffer.WriteOneBit(1); + } + else { + buffer.WriteUBitLong(0, 2); + buffer.WriteUBitLong((uint)SequenceNumber, SOUND_SEQNUMBER_BITS); + } + + if (Volume == delta.Volume) + buffer.WriteOneBit(0); + else { + buffer.WriteOneBit(1); + buffer.WriteUBitLong((uint)(Volume * 127.0f), 7); + } + + if (Soundlevel == delta.Soundlevel) + buffer.WriteOneBit(0); + else { + buffer.WriteOneBit(1); + buffer.WriteUBitLong((uint)Soundlevel, MAX_SNDLVL_BITS); + } + + if (Pitch == delta.Pitch) + buffer.WriteOneBit(0); + else { + buffer.WriteOneBit(1); + buffer.WriteUBitLong((uint)Pitch, 8); + } + + if (nProtoVersion > 21) { + if (SpecialDSP == delta.SpecialDSP) + buffer.WriteOneBit(0); + else { + buffer.WriteOneBit(1); + buffer.WriteUBitLong((uint)SpecialDSP, 8); + } + } + + if (Delay == delta.Delay) + buffer.WriteOneBit(0); + else { + buffer.WriteOneBit(1); + float d = Delay + SOUND_DELAY_OFFSET; + int iDelay = (int)(d * 1000.0f); + iDelay = Math.Clamp(iDelay, -10 * MAX_SOUND_DELAY_MSEC, MAX_SOUND_DELAY_MSEC); + if (iDelay < 0) iDelay /= 10; + buffer.WriteSBitLong(iDelay, MAX_SOUND_DELAY_MSEC_ENCODE_BITS); + } + + const float SCALE = 8.0f; + const int BITS = (int)(BitBuffer.COORD_INTEGER_BITS - 2); + if (Origin.X == delta.Origin.X) buffer.WriteOneBit(0); else { buffer.WriteOneBit(1); buffer.WriteSBitLong((int)(Origin.X / SCALE), BITS); } + if (Origin.Y == delta.Origin.Y) buffer.WriteOneBit(0); else { buffer.WriteOneBit(1); buffer.WriteSBitLong((int)(Origin.Y / SCALE), BITS); } + if (Origin.Z == delta.Origin.Z) buffer.WriteOneBit(0); else { buffer.WriteOneBit(1); buffer.WriteSBitLong((int)(Origin.Z / SCALE), BITS); } + + if (SpeakerEntity == delta.SpeakerEntity) buffer.WriteOneBit(0); + else { buffer.WriteOneBit(1); buffer.WriteSBitLong(SpeakerEntity, Constants.MAX_EDICT_BITS + 1); } + } + else { + ClearStopFields(); + } + } + private void ClearStopFields() { Volume = 0; Soundlevel = SoundLevel.LvlNone; diff --git a/Source.Engine/ClientFrame.cs b/Source.Engine/ClientFrame.cs index 5cded5a2..01620740 100644 --- a/Source.Engine/ClientFrame.cs +++ b/Source.Engine/ClientFrame.cs @@ -17,10 +17,16 @@ public class ClientFrame public MaxEdictsBitVec TransmitAlways; public ClientFrame? Next; + public FrameSnapshot Snapshot; + internal void Init(int tickcount) { TickCount = tickcount; } internal void Init(FrameSnapshot snapshot) { TickCount = snapshot.TickCount; } + + internal FrameSnapshot GetSnapshot() => Snapshot; + + internal void SetSnapshot(FrameSnapshot snapshot) => Snapshot = snapshot; } diff --git a/Source.Engine/GameEventManager.cs b/Source.Engine/GameEventManager.cs index 1648eb77..58253e5d 100644 --- a/Source.Engine/GameEventManager.cs +++ b/Source.Engine/GameEventManager.cs @@ -23,7 +23,7 @@ public bool Init() { return true; } - bool AddListener(object listener, GameEventDescriptor descriptor, GameEventListenerType listenerType) { + public bool AddListener(object listener, GameEventDescriptor descriptor, GameEventListenerType listenerType) { if (listener == null || descriptor == null) return false; @@ -88,7 +88,7 @@ public bool AddListener(IGameEventListener2 listener, ReadOnlySpan ev, boo return gameevent.Descriptor; } - private GameEventDescriptor? GetEventDescriptor(int eventid) { + public GameEventDescriptor? GetEventDescriptor(int eventid) { if (eventid < 0) return null; @@ -162,9 +162,20 @@ public int LoadEventsFromFile(ReadOnlySpan filename) { } static ConVar net_showevents = new("net_showevents", "0", FCvar.Cheat, "Dump game events to console (1=client only, 2=all)."); - public void RemoveListener(IGameEventListener2 listener) { - throw new NotImplementedException(); + GameEventCallback? callback = FindEventListener(listener); + if (callback == null) + return; + + for (int i = 0; i < GameEvents.Count; i++) { + GameEventDescriptor descriptor = GameEvents[i]; + descriptor.Listeners.Remove(callback); + } + + Listeners.Remove(callback); + + if (callback.ListenerType == GameEventListenerType.Clientside) + ClientListenersChanged = true; } public void Reset() { @@ -176,7 +187,38 @@ public void Reset() { } public bool SerializeEvent(IGameEvent ev, bf_write buf) { - throw new NotImplementedException(); + GameEventDescriptor? descriptor = GetEventDescriptor(ev); + Assert(descriptor != null); + + buf.WriteUBitLong((uint)descriptor!.EventID, MAX_EVENT_BITS); + + KeyValues? key = descriptor.Keys?.GetFirstSubKey(); + + if (net_showevents.GetInt() > 1) + ConMsg($"Serializing event '{descriptor.Name}' ({descriptor.EventID}):\n"); + + while (key != null) { + ReadOnlySpan keyName = key.Name; + GameEventType type = (GameEventType)key.GetInt(); + + if (net_showevents.GetInt() > 2) + ConMsg($" - {keyName} ({(int)type})\n"); + + switch (type) { + case GameEventType.Local: break; // don't network this guy + case GameEventType.String: buf.WriteString(ev.GetString(keyName, "")); break; + case GameEventType.Float: buf.WriteFloat(ev.GetFloat(keyName, 0.0f)); break; + case GameEventType.Long: buf.WriteLong(ev.GetInt(keyName, 0)); break; + case GameEventType.Short: buf.WriteShort(ev.GetInt(keyName, 0)); break; + case GameEventType.Byte: buf.WriteByte(ev.GetInt(keyName, 0)); break; + case GameEventType.Bool: buf.WriteOneBit(ev.GetInt(keyName, 0)); break; + default: DevMsg(1, $"GameEventManager: unkown type {type} for key '{key.Name}'.\n"); break; + } + + key = key.GetNextKey(); + } + + return !buf.Overflowed; } public IGameEvent? UnserializeEvent(bf_read buf) { diff --git a/Source.Engine/Host.cs b/Source.Engine/Host.cs index 6328f7e2..32470d45 100644 --- a/Source.Engine/Host.cs +++ b/Source.Engine/Host.cs @@ -797,7 +797,59 @@ void disconnect(in TokenizedCommand args) { } } - public bool CanCheat() { + [ConCommand("pause", "Toggle the server pause state.")] + void pause(in TokenizedCommand args, CommandSource source, int clientSlot) { +#if !SWDS + if (!sv.IsDedicated()) { + if (cl.LevelFileName == null || cl.LevelFileName.Length == 0) + return; + } +#endif + + if (source == CommandSource.Command) { + Cmd.ForwardToServer(args); + return; + } + + if (!sv.IsPausable()) + return; + + sv.SetPaused(!sv.IsPaused()); + + // sv.BroadcastPrintf( "%s %s the game\n", host_client->GetClientName(), sv.IsPaused() ? "paused" : "unpaused" ); + } + + [ConCommand("setpause", "Set the pause state of the server.")] + void setpause(in TokenizedCommand args, CommandSource source, int clientSlot) { +#if !SWDS + if (cl.LevelFileName == null || cl.LevelFileName.Length == 0) + return; +#endif + + if (source == CommandSource.Command) { + Cmd.ForwardToServer(args); + return; + } + + sv.SetPaused(true); + } + + [ConCommand("unpause", "Unpause the game.")] + void unpause(in TokenizedCommand args, CommandSource source, int clientSlot) { +#if !SWDS + if (cl.LevelFileName == null || cl.LevelFileName.Length == 0) + return; +#endif + + if (source == CommandSource.Command) { + Cmd.ForwardToServer(args); + return; + } + + sv.SetPaused(false); + } + + static public bool CanCheat() { return SV.sv_cheats.GetBool(); } diff --git a/Source.Engine/NetworkStringTable.cs b/Source.Engine/NetworkStringTable.cs index 1c3c79e7..18687f9f 100644 --- a/Source.Engine/NetworkStringTable.cs +++ b/Source.Engine/NetworkStringTable.cs @@ -384,6 +384,8 @@ public void EnableRollback() { ChangeHistoryEnabled = true; } + public void SetMirrorTable(INetworkStringTable mirrorTable) => MirrorTable = mirrorTable; + private void DataChanged(int stringNumber, NetworkStringTableItem item) { LastChangedTick = TickCount; @@ -521,7 +523,7 @@ public void ParseUpdate(bf_read buf, int entries) { } } - public int WriteUpdate(BaseClient? client, bf_write buf, int tickAck){ + public int WriteUpdate(BaseClient? client, bf_write buf, int tickAck) { return 0; // todo } @@ -568,12 +570,12 @@ public class NetworkStringTableContainer : INetworkStringTableContainer return null; } - if (Tables.Count() >= INetworkStringTable.MAX_TABLES) { + if (Tables.Count >= INetworkStringTable.MAX_TABLES) { Host.Error($"Only {INetworkStringTable.MAX_TABLES} string tables allowed, can't create '{tableName}'"); return null; } - int id = Tables.Count(); + int id = Tables.Count; pTable = new NetworkStringTable(id, tableName, maxEntries, userDataFixedSize, userDataNetworkBits, isFilenames); if (EnableRollback) { @@ -605,7 +607,7 @@ public void RemoveAllTables() { } public int GetNumTables() { - return Tables.Count(); + return Tables.Count; } public void SetTick(long tick) { } @@ -666,7 +668,7 @@ public void WriteBaselines(bf_write buf) { Host.Error($"Overflow error writing string table baseline {table.GetTableName()}\n"); int after = buf.BytesWritten; - if (sv_dumpstringtables.GetBool()) + if (sv_dumpstringtables.GetBool()) DevMsg($"NetworkStringTableContainer.WriteBaselines wrote {after - before} bytes for table {table.GetTableName()} [space remaining {buf.BytesLeft} bytes]\n"); } } diff --git a/Source.Engine/Server/BaseClient.cs b/Source.Engine/Server/BaseClient.cs index e32ed1b2..96a275e6 100644 --- a/Source.Engine/Server/BaseClient.cs +++ b/Source.Engine/Server/BaseClient.cs @@ -72,18 +72,20 @@ protected virtual bool ProcessGMod_ClientToServer(CLC_GMod_ClientToServer m) { return true;// todo } - protected virtual bool ProcessMove(CLC_Move m) { - return true;// todo - } + protected virtual bool ProcessMove(CLC_Move m) => true; protected virtual bool ProcessTick(NET_Tick m) { - return true;// todo + NetChannel!.SetRemoteFramerate(m.HostFrameTime, m.HostFrameDeviation); + return UpdateAcknowledgedFramecount(m.Tick); } protected virtual bool ProcessStringCmd(NET_StringCmd m) { - return true; // todo + ExecuteStringCommand(m.Command); + return true; } + protected virtual bool UpdateAcknowledgedFramecount(int tick) => true; + public void ClientRequestNameChange(ReadOnlySpan newName) { bool showStatusMessage = (PendingNameChange[0] == '\0'); @@ -272,7 +274,7 @@ protected virtual void ActivatePlayer() { // MapReslistGenerator().OnPlayerSpawn(); // NotifyDedicatedServerUI("UpdatePlayers"); - NET_SignonState signonState = new(SignOnState, Server.GetSpawnCount()); // FIXME: This message should need to be sent here? + NET_SignonState signonState = new(SignOnState, Server.GetSpawnCount()); // FIXME: This message shouldn't need to be sent here? NetChannel.SendNetMsg(signonState); } @@ -317,8 +319,8 @@ protected virtual bool ProcessClientInfo(CLC_ClientInfo msg) { // strcpy(FriendsName, msg.FriendsName); for (int i = 0; i < Constants.MAX_CUSTOM_FILES; i++) { - // CustomFiles[i].CRC = msg.CustomFiles[i]; - // CustomFiles[i].ReqID = 0; + CustomFiles[i].CRC = msg.CustomFiles[i]; + CustomFiles[i].ReqID = 0; } if (msg.ServerCount != Server.GetSpawnCount()) { @@ -335,11 +337,33 @@ protected virtual bool ProcessBaselineAck(CLC_BaselineAck m) { } protected virtual bool ProcessListenEvents(CLC_ListenEvents m) { - return true;// todo + gameEventManager.RemoveListener(this); + + for (int i = 0; i < Constants.MAX_EVENT_NUMBER; i++) { + if (m.EventArray.Get(i) != 0) { + GameEventDescriptor? desc = gameEventManager.GetEventDescriptor(i); + if (desc != null) + gameEventManager.AddListener(this, desc, GameEventListenerType.Clientstub); + else { + DevMsg($"ProcessListenEvents: game event {i} not found.\n"); + return false; + } + } + } + + return true; } protected virtual void SendSnapshot(ClientFrame frame) { + if (ForceWaitForTick > 0 || LastSnapshot == frame.GetSnapshot()) { + NetChannel.Transmit(); + return; + } + + bool failedOnce; // todo + + LastSnapshot = frame.GetSnapshot(); } public int GetClientChallenge() => ClientChallenge; @@ -381,7 +405,7 @@ public ReadOnlySpan GetNetworkIDString() { return GetUserIDString(GetNetworkID()); } - public void Clear() { + public virtual void Clear() { if (NetChannel != null) { NetChannel.Shutdown("Disconnect by server.\n"); NetChannel = null!; @@ -398,7 +422,7 @@ public void Clear() { DeltaTick = -1; SignOnTick = 0; StringTableAckTick = 0; - // LastSnapshot = NULL; + LastSnapshot = null; ForceWaitForTick = -1; FakePlayer = false; HLTV = false; @@ -422,7 +446,12 @@ public void Clear() { } private void FreeBaselines() { + Baseline?.ReleaseReference(); + Baseline = null; + BaselineUpdateTick = -1; + BaselineUsed = 0; + BaselinesSent.ClearAll(); } public bool IsConnected() => SignOnState >= SignOnState.Connected; @@ -484,7 +513,7 @@ public bool SendNetMsg(INetMessage msg, bool forceReliable = false) { public uint SendTableCRC; - public CustomFile[] CustomFiles; + public readonly CustomFile[] CustomFiles = new CustomFile[Constants.MAX_CUSTOM_FILES]; public int FilesDownloaded; public INetChannel NetChannel; @@ -493,7 +522,8 @@ public bool SendNetMsg(INetMessage msg, bool forceReliable = false) { public int StringTableAckTick; public long SignOnTick; // CSmartPtr - // CFrameSnapshot baseline + FrameSnapshot? LastSnapshot; // todo? ^ + FrameSnapshot? Baseline; public int BaselineUpdateTick; MaxEdictsBitVec BaselinesSent; public int BaselineUsed; @@ -516,7 +546,20 @@ public bool SendNetMsg(INetMessage msg, bool forceReliable = false) { public bool IsPlayerNameLocked() => PlayerNameLocked; public void FireGameEvent(IGameEvent ev) { - throw new NotImplementedException(); + byte[] buffer = new byte[Constants.MAX_EVENT_BITS]; + + SVC_GameEvent msg = new(); + msg.DataOut.StartWriting(buffer, Constants.MAX_EVENT_BITS, 0); + + if (gameEventManager.SerializeEvent(ev, msg.DataOut)) { + if (NetChannel != null) { + bool sent = NetChannel.SendNetMsg(msg); + if (!sent) + DevMsg($"GameEventManager: failed to send event '{ev.GetName()}'.\n"); + } + } + else + DevMsg($"GameEventManager: failed to serialize event '{ev.GetName()}'.\n"); } static bool BIgnoreCharInName(char cChar, bool bIsFirstCharacter) { return cChar == '%' || cChar == '~' || cChar < 0x09 || (bIsFirstCharacter && cChar == '#'); @@ -681,15 +724,34 @@ public void SetUpdateRate(int nUpdateRate, bool bForce) { } public int GetUpdateRate() { - throw new NotImplementedException(); + if (SnapshotInterval > 0) + return (int)(1.0f / SnapshotInterval); + else + return 0; } public int GetMaxAckTickCount() { - throw new NotImplementedException(); + long maxTick = SignOnTick; + + if (DeltaTick > maxTick) + maxTick = DeltaTick; + + if (StringTableAckTick > maxTick) + maxTick = StringTableAckTick; + + return (int)maxTick; } - public bool ExecuteStringCommand(ReadOnlySpan s) { - throw new NotImplementedException(); + public virtual bool ExecuteStringCommand(ReadOnlySpan cmd) { + if (cmd.IsEmpty) + return false; + + if (strcmp(cmd, "demorestart") == 0) { + // DemoRestart(); + return false; + } + + return false; } public void ClientPrintf(ReadOnlySpan fmt) { @@ -705,9 +767,7 @@ public bool IsProximityHearingClient(int index) { throw new NotImplementedException(); } - public void SetMaxRoutablePayloadSize(int nMaxRoutablePayloadSize) { - throw new NotImplementedException(); - } + public void SetMaxRoutablePayloadSize(int nMaxRoutablePayloadSize) => NetChannel?.SetMaxRoutablePayloadSize(nMaxRoutablePayloadSize); public bool IsReplay() { return false; diff --git a/Source.Engine/Server/GameClient.cs b/Source.Engine/Server/GameClient.cs index a3e5abe4..25d2c31c 100644 --- a/Source.Engine/Server/GameClient.cs +++ b/Source.Engine/Server/GameClient.cs @@ -5,6 +5,7 @@ using Source.Common.Commands; using Source.Common.Engine; using Source.Common.Networking; +using Source.GUI.Controls; namespace Source.Engine.Server; @@ -140,9 +141,88 @@ void UpdateUserSettings() { } // void Inactivate() { } - // bool UpdateAcknowledgedFramecount(int tick) { } + protected override bool UpdateAcknowledgedFramecount(int tick) { + if (IsFakeClient()) { + DeltaTick = tick; + StringTableAckTick = tick; + return true; + } + + if (ForceWaitForTick > 0) { + if (tick > ForceWaitForTick) + // we should never get here since full updates are transmitted as reliable data now + return true; + else if (tick == -1) { + if (!NetChannel!.HasPendingReliableData()) { + // that's strange: we sent the client a full update, and it was fully received ( no reliable data in waiting buffers ) + // but the client is requesting another full update. + // + // This can happen if they request full updates in succession really quickly (using cl_fullupdate or "record X;stop" quickly). + // There was a bug here where if we just return out, the client will have nuked its entities and we'd send it + // a supposedly uncompressed update but DeltaTick was not -1, so it was delta'd and it'd miss lots of stuff. + // Led to clients getting full spectator mode radar while their player was not a spectator. + ConDMsg("Client forced immediate full update.\n"); + ForceWaitForTick = DeltaTick = -1; + // OnRequestFullUpdate(); TODO + return true; + } + } + else if (tick < ForceWaitForTick) + return true; + else + ForceWaitForTick = -1; + } + else { + if (DeltaTick == -1) + return true; + + if (tick == -1) { + // OnRequestFullUpdate(); + } + else { + if (DeltaTick > tick) { + // client already acknowledged new tick and now switch back to older + // thats not allowed since we always delete older frames + Disconnect("Client delta ticks out of order.\n"); + return false; + } + } + } + + DeltaTick = tick; - // void Clear() { } + if (DeltaTick > -1) + StringTableAckTick = DeltaTick; + + if ((BaselineUpdateTick > -1) && (DeltaTick > BaselineUpdateTick)) + // server sent a baseline update, but it wasn't acknowledged yet so it was probably lost. + BaselineUpdateTick = -1; + + return true; + } + + public override void Clear() { + if (HLTV) { + + } + + if (Replay) { + + } + + base.Clear(); + + cl.DeleteClientFrames(-1); + + Sounds.Clear(); + VoiceStreams.ClearAll(); + VoiceProximity.ClearAll(); + Edict = null!; + ViewEntity = null; + VoiceLoopback = false; + LastMovementTick = 0; + SoundSequence = 0; + } public override void Reconnect() { sv.RemoveClientFromGame(this); @@ -151,15 +231,91 @@ public override void Reconnect() { // void Disconnect(ReadOnlySpan fmt) { } - // bool SetSignonState(int state, int spawncount) { } + protected override bool SetSignOnState(SignOnState state, int spawncount) { + if (state == SignOnState.Connected) { + if (!CheckConnect()) + return false; + + NetChannel!.SetTimeout(Source.Common.Networking.NetChannel.SIGNON_TIME_OUT); + NetChannel.SetFileTransmissionMode(false); + NetChannel.SetMaxBufferSize(true, Protocol.MAX_PAYLOAD); + } + else if (state == SignOnState.New) { + if (!sv.IsMultiplayer()) + sv.InstallClientStringTableMirrors(); + } + else if (state == SignOnState.Full) { + if (sv.LoadGame) { + // sv.FinishRestore(); + } + + NetChannel!.SetTimeout(sv_timeout.GetFloat()); + NetChannel.SetFileTransmissionMode(true); + } + + return base.SetSignOnState(state, spawncount); + } // void SendSound(SoundInfo sound, bool isReliable) { } - // void WriteGameSounds(bf_write buf) { } + void WriteGameSounds(bf_write buf) { + if (Sounds.Count == 0) + return; + + byte[] data = new byte[Protocol.MAX_PAYLOAD]; + SVC_Sounds msg = new(); + msg.DataOut.StartWriting(data, Protocol.MAX_PAYLOAD, 0); + + int soundCount = FillSoundsMessage(msg); + msg.WriteToBuffer(buf); + } + + int FillSoundsMessage(SVC_Sounds msg) { + int i, count = Sounds.Count; + + int max = Server.IsMultiplayer() ? 32 : 255; + + if (count > max) + count = max; + + if (count == 0) + return 0; + + SoundInfo defaultSound = new(); + defaultSound.SetDefault(); + + SoundInfo deltaSound = defaultSound; - // int FillSoundsMessage(SVC_Sounds msg) { } + msg.NumSounds = count; + msg.ReliableSound = false; + msg.SetReliable(false); - // bool CheckConnect() { } + Assert(msg.DataOut.BitsLeft > 0); + + for (i = 0; i < count; i++) { + SoundInfo sound = Sounds[i]; + sound.WriteDelta(ref deltaSound, msg.DataOut, Protocol.VERSION); // FIXME proto version + deltaSound = sound; + } + + int remove = Sounds.Count - (count + max); + + if (remove > 0) { + DevMsg($"Warning! Dropped {remove} unreliable sounds for client {Name}.\n"); + count += remove; + } + + if (count > 0) + Sounds.RemoveRange(0, count); + + Assert(Sounds.Count <= max); + + return msg.NumSounds; + } + + bool CheckConnect() { + return true; // todo + } protected override void ActivatePlayer() { base.ActivatePlayer(); @@ -180,10 +336,6 @@ protected override void ActivatePlayer() { // g_pServerPluginHandler->ClientSettingsChanged(edict); - Common.TimestampedLog("GetTestScriptMgr()->CheckPoint"); - - // GetTestScriptMgr()->CheckPoint("client_connected"); - IGameEvent? evnt = gameEventManager.CreateEvent("player_activate"); if (evnt != null) { @@ -261,11 +413,84 @@ void WriteViewAngleUpdate() { } } - // bool IsEngineClientCommand(in TokenizedCommand args) { } + static readonly string[] CLCommands = [ // Shouldn't be here + "status", + "pause", + "setpause", + "unpause", + "ping", + "rpt_server_enable", + "rpt_client_enable", +#if !SWDS + "rpt", + "rpt_connect", + "rpt_password", + "rpt_screenshot", + "rpt_download_log", +#endif + ]; + + bool IsEngineClientCommand(in TokenizedCommand args) { + if (args.ArgC() == 0) + return false; + + for (int i = 0; i < CLCommands.Length; i++) { + if (args[0].Equals(CLCommands[i], StringComparison.OrdinalIgnoreCase)) + return true; + } + + return false; + } // bool SendNetMsg(INetMessage msg, bool forceReliable) { } - // bool ExecuteStringCommand(ReadOnlySpan pCommandString) { } + + public override bool ExecuteStringCommand(ReadOnlySpan c) { + if (base.ExecuteStringCommand(c)) + return true; + + TokenizedCommand args = new(); + + if (!args.Tokenize(c)) + return false; + + if (args.ArgC() == 0) + return false; + + if (IsEngineClientCommand(args)) { + cmd.ExecuteCommand(ref args, CommandSource.Client, ClientSlot); + return true; + } + + ConCommandBase? command = cvar.FindCommandBase(args[0]); + + if (command != null && command.IsCommand() && command.IsFlagSet(FCvar.GameDLL)) { + // Allow cheat commands in singleplayer, debug, or multiplayer with sv_cheats on + // NOTE: Don't bother with rpt stuff; commands that matter there shouldn't have FCVAR_GAMEDLL set + if (command.IsFlagSet(FCvar.Cheat)) { + if (sv.IsMultiplayer() && !Host.CanCheat()) + return false; + } + + if (command.IsFlagSet(FCvar.SingleplayerOnly)) { + if (sv.IsMultiplayer()) + return false; + } + + // Don't allow clients to execute commands marked as development only. + if (command.IsFlagSet(FCvar.DevelopmentOnly)) + return false; + + // serverPluginHandler.SetCommandClient(ClientSlot); + + cmd.Dispatch(command, args); + } + else { + // serverPluginHandler.ClientCommand(edict, args); + } + + return true; + } protected override void SendSnapshot(ClientFrame frame) { if (HLTV) { diff --git a/Source.Engine/Server/GameServer.cs b/Source.Engine/Server/GameServer.cs index cd08018b..584e48d0 100644 --- a/Source.Engine/Server/GameServer.cs +++ b/Source.Engine/Server/GameServer.cs @@ -197,12 +197,12 @@ private void SetHibernating(bool hibernating) { internal void InitMaxClients() { int newmaxplayers = CommandLine.ParmValue("-maxplayers", -1); - if (newmaxplayers == -1) + if (newmaxplayers == -1) newmaxplayers = CommandLine.ParmValue("+maxplayers", -1); SetupMaxPlayers(newmaxplayers); } - static readonly ConVar tv_enable = new( "tv_enable", "0", FCvar.NotConnected, "Activates SourceTV on server. not implemented!"); + static readonly ConVar tv_enable = new("tv_enable", "0", FCvar.NotConnected, "Activates SourceTV on server. not implemented!"); private void SetupMaxPlayers(int iDesiredMaxPlayers) { int minmaxplayers = 1; @@ -212,15 +212,15 @@ private void SetupMaxPlayers(int iDesiredMaxPlayers) { if (SV.ServerGameClients != null) { SV.ServerGameClients.GetPlayerLimits(out minmaxplayers, out maxmaxplayers, out defaultmaxplayers); - if (minmaxplayers < 1) + if (minmaxplayers < 1) Sys.Error($"GetPlayerLimits: min maxplayers must be >= 1 ({minmaxplayers})"); - else if (defaultmaxplayers < 1) + else if (defaultmaxplayers < 1) Sys.Error($"GetPlayerLimits: default maxplayers must be >= 1 ({minmaxplayers})"); - - if (minmaxplayers > maxmaxplayers || defaultmaxplayers > maxmaxplayers) + + if (minmaxplayers > maxmaxplayers || defaultmaxplayers > maxmaxplayers) Sys.Error($"GetPlayerLimits: min maxplayers {minmaxplayers} > max {maxmaxplayers}"); - if (maxmaxplayers > Constants.ABSOLUTE_PLAYER_LIMIT) + if (maxmaxplayers > Constants.ABSOLUTE_PLAYER_LIMIT) Sys.Error($"GetPlayerLimits: max players limited to {Constants.ABSOLUTE_PLAYER_LIMIT}"); } @@ -492,4 +492,24 @@ private void AssignClassIds() { INetworkStringTable? DynamicModelsTable; bool Hibernating; // Are we hibernating. Hibernation makes server process consume approx 0 CPU when no clients are connected + + public void InstallClientStringTableMirrors() { +#if !SWDS && !SHARED_NET_STRING_TABLES + int numTables = networkStringTableContainerServer.GetNumTables(); + for (int i = 0; i < numTables; i++) { + NetworkStringTable? serverTable = (NetworkStringTable?)networkStringTableContainerServer.GetTable(i); + if (serverTable == null) + continue; + + NetworkStringTable? clientTable = (NetworkStringTable?)networkStringTableContainerClient.FindTable(serverTable.GetTableName()); + + if (clientTable == null) { + DevMsg($"SV_InstallClientStringTableMirrors! Missing client table \"{serverTable.GetTableName()}\".\n "); + continue; + } + + clientTable.SetMirrorTable(serverTable); + } +#endif + } } diff --git a/Source.Engine/SourceDLLMain.cs b/Source.Engine/SourceDLLMain.cs index f750e60a..82fafa0a 100644 --- a/Source.Engine/SourceDLLMain.cs +++ b/Source.Engine/SourceDLLMain.cs @@ -42,6 +42,7 @@ public static class SourceDllMain [Dependency] public static IVideoMode videoMode { get; private set; } = null!; [Dependency] public static Cbuf cbuf { get; private set; } = null!; [Dependency] public static ICvar cvar { get; private set; } = null!; + [Dependency] public static Cmd cmd { get; private set; } = null!; [Dependency] public static IMaterialSystemHardwareConfig HardwareConfig { get; private set; } = null!; [Dependency] public static ICommandLine commandLine { get; private set; } = null!; [Dependency] public static IMatSystemSurface surface { get; private set; } = null!; From 08dc555bbf6b40827828aac67027d4b489e8ef8f Mon Sep 17 00:00:00 2001 From: callumok2004 Date: Mon, 23 Feb 2026 19:36:11 +0000 Subject: [PATCH 03/31] FIx compile --- Source.Engine/Cmd.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source.Engine/Cmd.cs b/Source.Engine/Cmd.cs index 4e8039e8..775997a9 100644 --- a/Source.Engine/Cmd.cs +++ b/Source.Engine/Cmd.cs @@ -141,7 +141,7 @@ public void ForwardToServer(in TokenizedCommand command) { #endif } - private void Dispatch(ConCommandBase commandBase, in TokenizedCommand command) { + public void Dispatch(ConCommandBase commandBase, in TokenizedCommand command) { ConCommand conCommand = (ConCommand)commandBase; conCommand.Dispatch(in command, Source, ClientSlot); } @@ -150,7 +150,7 @@ public bool ShouldPreventClientCommand(ConCommandBase? cmd) { if (filterCommandsByClientCmdCanExecute > 0 && cmd != null && cmd.IsFlagSet(FCvar.ClientCmdCanExecute)) { // If this command is in the game DLL, don't mention it because we're going to forward this // request to the server and let the server handle it. - if (!cmd.IsFlagSet(FCvar.GameDLL)) + if (!cmd.IsFlagSet(FCvar.GameDLL)) Dbg.Warning($"FCvar.ServerCanExecute prevented server running command: {cmd.GetName()}\n"); return true; From a8a28dbeedd192cfa4ec58840c69051f57ee2112 Mon Sep 17 00:00:00 2001 From: callumok2004 Date: Fri, 27 Feb 2026 03:47:11 +0000 Subject: [PATCH 04/31] Server & stringtables impl --- Source.Common/Bitbuffers/bf_write.cs | 50 +++++------ Source.Common/Engine/Edict.cs | 8 +- Source.Engine/CL.cs | 11 +-- Source.Engine/NetworkStringTable.cs | 109 ++++++++++++++++++++++- Source.Engine/Precache.cs | 2 + Source.Engine/Render.cs | 11 +-- Source.Engine/SV.cs | 8 ++ Source.Engine/Server/BaseClient.cs | 39 ++++++--- Source.Engine/Server/GameClient.cs | 68 ++++++++++++--- Source.Engine/Server/GameServer.cs | 124 ++++++++++++++++++++++++--- 10 files changed, 351 insertions(+), 79 deletions(-) diff --git a/Source.Common/Bitbuffers/bf_write.cs b/Source.Common/Bitbuffers/bf_write.cs index 5dd8197c..01d158db 100644 --- a/Source.Common/Bitbuffers/bf_write.cs +++ b/Source.Common/Bitbuffers/bf_write.cs @@ -118,43 +118,45 @@ public void WriteSBitLong(int data, int numbits) { public void WriteVarInt32(uint data) { if ((curBit & 7) == 0 && curBit + (nint)MaxVarInt32Bytes * 8 <= dataBits) { - byte* target = (byte*)data + (curBit >> 3); - - target[0] = (byte)(data | 0x80); - if (data >= 1 << 7) { - target[1] = (byte)(data >> 7 | 0x80); - if (data >= 1 << 14) { - target[2] = (byte)(data >> 14 | 0x80); - if (data >= 1 << 21) { - target[3] = (byte)(data >> 21 | 0x80); - if (data >= 1 << 28) { - target[4] = (byte)(data >> 28); - curBit += 5 * 8; - return; + fixed (byte* pBuf = this.data) { + byte* target = pBuf + (curBit >> 3); + + target[0] = (byte)(data | 0x80); + if (data >= 1 << 7) { + target[1] = (byte)(data >> 7 | 0x80); + if (data >= 1 << 14) { + target[2] = (byte)(data >> 14 | 0x80); + if (data >= 1 << 21) { + target[3] = (byte)(data >> 21 | 0x80); + if (data >= 1 << 28) { + target[4] = (byte)(data >> 28); + curBit += 5 * 8; + return; + } + else { + target[3] &= 0x7F; + curBit += 4 * 8; + return; + } } else { - target[3] &= 0x7F; - curBit += 4 * 8; + target[2] &= 0x7F; + curBit += 3 * 8; return; } } else { - target[2] &= 0x7F; - curBit += 3 * 8; + target[1] &= 0x7F; + curBit += 2 * 8; return; } } else { - target[1] &= 0x7F; - curBit += 2 * 8; + target[0] &= 0x7F; + curBit += 1 * 8; return; } } - else { - target[0] &= 0x7F; - curBit += 1 * 8; - return; - } } else // Slow path { diff --git a/Source.Common/Engine/Edict.cs b/Source.Common/Engine/Edict.cs index 5f596011..ecbb2ea9 100644 --- a/Source.Common/Engine/Edict.cs +++ b/Source.Common/Engine/Edict.cs @@ -48,18 +48,18 @@ public class BaseEdict return null; } public IServerNetworkable? GetNetworkable() { - return null; + return Networkable; } public IServerUnknown? GetUnknown() { - return null; + return Unk; } public void SetEdict(IServerUnknown? unk, bool fullEdict) { Unk = unk; - if (unk != null && fullEdict) + if (unk != null && fullEdict) StateFlags = EdictFlags.Full; - else + else StateFlags = 0; } diff --git a/Source.Engine/CL.cs b/Source.Engine/CL.cs index a3842371..232be9f9 100644 --- a/Source.Engine/CL.cs +++ b/Source.Engine/CL.cs @@ -248,7 +248,7 @@ public void FullyConnected() { // MDL cache end map load if (Host.developer.GetInt() > 0) - ConDMsg("Signon traffic \"%s\": incoming %s, outgoing %s\n", cl.NetChannel.GetName().ToString(), cl.NetChannel.GetTotalData(1 /*FLOW_INCOMING*/), cl.NetChannel.GetTotalData(0 /*FLOW_OUTGOING*/)); + ConDMsg($"Signon traffic \"{cl.NetChannel.GetName()}\": incoming {cl.NetChannel.GetTotalData(1 /*FLOW_INCOMING*/)}, outgoing {cl.NetChannel.GetTotalData(0 /*FLOW_OUTGOING*/)}\n"); Scr.EndLoadingPlaque(); // EndLoadingUpdates(); @@ -326,14 +326,7 @@ internal bool CheckCRCs(ReadOnlySpan levelFileName) { } internal void RegisterResources() { - Model? model = cl.GetModel(1); - -#if DEBUG // TODO TODO Remove once stringables are done - if (model == null && !string.IsNullOrEmpty(cl.LevelFileName)) - model = modelloader.GetModelForName(cl.LevelFileName, ModelLoaderFlags.Client); -#endif - - host_state.SetWorldModel(model); + host_state.SetWorldModel(cl.GetModel(1)); if (host_state.WorldModel == null) Host.Error("CL.RegisterResources: host_state.WorldModel/cl.GetModel(1) == NULL\n"); } diff --git a/Source.Engine/NetworkStringTable.cs b/Source.Engine/NetworkStringTable.cs index 18687f9f..09e516db 100644 --- a/Source.Engine/NetworkStringTable.cs +++ b/Source.Engine/NetworkStringTable.cs @@ -523,8 +523,112 @@ public void ParseUpdate(bf_read buf, int entries) { } } + + private static int CountSimilarCharacters(string str1, string str2) { + int c = 0; + int maxLen = Math.Min(str1.Length, str2.Length); + int limit = (1 << SUBSTRING_BITS) - 1; + while (c < maxLen && str1[c] == str2[c] && c < limit) { + c++; + } + return c; + } + + private static int GetBestPreviousString(List history, string newstring, out int substringsize) { + int bestindex = -1; + int bestcount = 0; + int c = history.Count; + for (int i = 0; i < c; i++) { + string prev = history[i]; + int similar = CountSimilarCharacters(prev, newstring); + + if (similar < 3) + continue; + + if (similar > bestcount) { + bestcount = similar; + bestindex = i; + } + } + + substringsize = bestcount; + return bestindex; + } + public int WriteUpdate(BaseClient? client, bf_write buf, int tickAck) { - return 0; // todo + List history = []; // StringHistoryEntry + + int entriesUpdated = 0; + int lastEntry = -1; + + int count = Items.Count(); + + for (int i = 0; i < count; i++) { + NetworkStringTableItem p = Items.Element(i); + + // Client is up to date + if (p.TickChanged <= tickAck) + continue; + + // Write Entry index + if ((lastEntry + 1) == i) + buf.WriteOneBit(1); + else { + buf.WriteOneBit(0); + buf.WriteUBitLong((uint)i, EntryBits); + } + + // check if string can use older string as base eg "models/weapons/gun1" & "models/weapons/gun2" + string pEntry = Items.String(i); + + if (p.TickCreated > tickAck) { + // this item has just been created, send string itself + buf.WriteOneBit(1); + + int bestprevious = GetBestPreviousString(history, pEntry, out int substringsize); + if (bestprevious != -1) { + buf.WriteOneBit(1); + buf.WriteUBitLong((uint)bestprevious, 5); // history never has more than 32 entries + buf.WriteUBitLong((uint)substringsize, SUBSTRING_BITS); + buf.WriteString(pEntry.AsSpan(substringsize)); + } + else { + buf.WriteOneBit(0); + buf.WriteString(pEntry); + } + } + else + buf.WriteOneBit(0); + + // Write the item's user data. + byte[]? pUserData = p.GetUserData(out int len); + if (pUserData != null && len > 0) { + buf.WriteOneBit(1); + + if (IsUserDataFixedSize()) { + // Don't have to send length, it was sent as part of the table definition + buf.WriteBits(pUserData, GetUserDataSizeBits()); + } + else { + buf.WriteUBitLong((uint)len, NetworkStringTableItem.MAX_USERDATA_BITS); + buf.WriteBits(pUserData, len * 8); + } + } + else + buf.WriteOneBit(0); + + // limit string history to 32 entries + if (history.Count > 31) + history.RemoveAt(0); + + // add string to string history + history.Add(pEntry.Length > (1 << SUBSTRING_BITS) ? pEntry[..(1 << SUBSTRING_BITS)] : pEntry); + + entriesUpdated++; + lastEntry = i; + } + + return entriesUpdated; } public bool WriteBaselines(SVC_CreateStringTable msg, byte[] msg_buffer, nint msg_buffer_size) { @@ -532,6 +636,7 @@ public bool WriteBaselines(SVC_CreateStringTable msg, byte[] msg_buffer, nint ms msg.IsFilenames = IsFilenames; msg.TableName = TableName; + msg.MaxEntries = GetMaxStrings(); msg.NumEntries = GetNumStrings(); msg.UserDataFixedSize = IsUserDataFixedSize(); msg.UserDataSize = GetUserDataSize(); @@ -552,7 +657,7 @@ public class NetworkStringTableContainer : INetworkStringTableContainer private bool EnableRollback; private List Tables = new List(); - public INetworkStringTable? CreateStringTable(ReadOnlySpan tableName, int maxEntries, int userDataFixedSize, int userDataNetworkBits) { + public INetworkStringTable? CreateStringTable(ReadOnlySpan tableName, int maxEntries, int userDataFixedSize = 0, int userDataNetworkBits = 0) { return CreateStringTableEx(tableName, maxEntries, userDataFixedSize, userDataNetworkBits, false); } diff --git a/Source.Engine/Precache.cs b/Source.Engine/Precache.cs index 9d24c53c..553b72a4 100644 --- a/Source.Engine/Precache.cs +++ b/Source.Engine/Precache.cs @@ -21,6 +21,8 @@ public class PrecacheItem public const string SOUND_PRECACHE_TABLENAME = "soundprecache"; public const string DECAL_PRECACHE_TABLENAME = "decalprecache"; + public const int PRECACHE_USER_DATA_NUMBITS = 2; + public const int MAX_MODEL_INDEX_BITS = 12; public const int MAX_MODELS = (1 << MAX_MODEL_INDEX_BITS); diff --git a/Source.Engine/Render.cs b/Source.Engine/Render.cs index df94f9eb..37deb58c 100644 --- a/Source.Engine/Render.cs +++ b/Source.Engine/Render.cs @@ -28,7 +28,8 @@ public struct ViewStack } -public static class RenderAccessors { +public static class RenderAccessors +{ public static ref readonly Vector3 CurrentViewOrigin() => ref R.CurrentViewOrigin; public static ref readonly Vector3 CurrentViewForward() => ref R.CurrentViewForward; @@ -653,7 +654,7 @@ private void RedownloadAllLightmaps() { } double elapsed = (Sys.Time - st) * 1000.0; - DevMsg("R_RedownloadAllLightmaps took %.3f msec!\n", elapsed); + DevMsg($"R_RedownloadAllLightmaps took {elapsed:F3} msec!\n"); rebuildLightmaps = false; } @@ -668,7 +669,7 @@ private void BuildLightMap(ref BSPMSurface2 surfID, in Matrix3x4 entityToWorld, if (MaterialSystem.MaterialSortInfoArray != null) { Assert(ModelLoader.MSurf_MaterialSortID(ref surfID) >= 0 && ModelLoader.MSurf_MaterialSortID(ref surfID) < MaterialSystem.WorldStaticMeshes.Count); if ((MaterialSystem.MaterialSortInfoArray[ModelLoader.MSurf_MaterialSortID(ref surfID)].LightmapPageID == StandardLightmap.White) || - (MaterialSystem.MaterialSortInfoArray[ModelLoader.MSurf_MaterialSortID(ref surfID)].LightmapPageID == StandardLightmap.WhiteBump)) { + (MaterialSystem.MaterialSortInfoArray[ModelLoader.MSurf_MaterialSortID(ref surfID)].LightmapPageID == StandardLightmap.WhiteBump)) { return; } } @@ -872,7 +873,7 @@ public void BuildLightMapGuts(ref BSPMSurface2 surfID, in Matrix3x4 entityToWorl } private void UpdateLightmapTextures(ref BSPMSurface2 surfID, bool needsBumpmap) { - if(MaterialSystem.MaterialSortInfoArray != null) { + if (MaterialSystem.MaterialSortInfoArray != null) { Span lightmapSize = stackalloc int[2]; Span offsetIntoLightmapPage = stackalloc int[2]; lightmapSize[0] = (ModelLoader.MSurf_LightmapExtents(ref surfID)[0]) + 1; @@ -902,7 +903,7 @@ private unsafe void SortSurfacesByLightmapID(Span toSort, int surfaceCount) Span iCounts = stackalloc int[256]; Span iOffsetTable = stackalloc int[256]; - fixed(int* fpToSort = toSort) { + fixed (int* fpToSort = toSort) { int* pToSort = fpToSort; for (int radix = 0; radix < 4; ++radix) { { diff --git a/Source.Engine/SV.cs b/Source.Engine/SV.cs index 5392ab51..bcae2b81 100644 --- a/Source.Engine/SV.cs +++ b/Source.Engine/SV.cs @@ -336,7 +336,15 @@ private void Think(bool isSimulating) { } internal void CreateNetworkStringTables() { + networkStringTableContainerServer.RemoveAllTables(); + networkStringTableContainerServer.SetAllowCreation(true); + + sv.CreateEngineStringTables(); + + serverGameDLL.CreateNetworkStringTables(); + + networkStringTableContainerServer.SetAllowCreation(false); } internal void ClearWorld() { diff --git a/Source.Engine/Server/BaseClient.cs b/Source.Engine/Server/BaseClient.cs index 96a275e6..24206954 100644 --- a/Source.Engine/Server/BaseClient.cs +++ b/Source.Engine/Server/BaseClient.cs @@ -657,7 +657,7 @@ static void ValidateName(Span name) { } } - public void Connect(ReadOnlySpan name, int userID, INetChannel netChannel, bool fakePlayer, int clientChallenge) { + public virtual void Connect(ReadOnlySpan name, int userID, INetChannel netChannel, bool fakePlayer, int clientChallenge) { Common.TimestampedLog("CBaseClient::Connect"); #if !SWDS EngineVGui().UpdateProgressBar(LevelLoadingProgress.SignOnConnect); @@ -686,8 +686,28 @@ public void Connect(ReadOnlySpan name, int userID, INetChannel netChannel, Steam3Server().NotifyLocalClientConnect(this); } - public void Inactivate() { - throw new NotImplementedException(); + public virtual void Inactivate() { + FreeBaselines(); + + DeltaTick = -1; + SignOnTick = 0; + StringTableAckTick = 0; + LastSnapshot = null; + ForceWaitForTick = -1; + + SignOnState = SignOnState.ChangeLevel; + + if (NetChannel != null) { + NetChannel.Clear(); + + // if (Net.IsMultiplayer()) { + // NET_SignonState signonState = new(SignOnState, Server.GetSpawnCount()); + // NetChannel.SendNetMsg(signonState); + // NetChannel.Transmit(); + // } + } + + gameEventManager.RemoveListener(this); } readonly Host Host = Singleton(); @@ -711,16 +731,13 @@ public ReadOnlySpan GetUserSetting(ReadOnlySpan cvar) { throw new NotImplementedException(); } - public void SetRate(int nRate, bool bForce) { - throw new NotImplementedException(); - } + public void SetRate(int nRate, bool force) => NetChannel?.SetDataRate(nRate); - public int GetRate() { - throw new NotImplementedException(); - } + public int GetRate() => NetChannel?.GetDataRate() ?? 0; - public void SetUpdateRate(int nUpdateRate, bool bForce) { - throw new NotImplementedException(); + public void SetUpdateRate(int updateRate, bool force) { + updateRate = Math.Clamp(updateRate, 1, 100); + SnapshotInterval = 1.0f / updateRate; } public int GetUpdateRate() { diff --git a/Source.Engine/Server/GameClient.cs b/Source.Engine/Server/GameClient.cs index 25d2c31c..23de5cca 100644 --- a/Source.Engine/Server/GameClient.cs +++ b/Source.Engine/Server/GameClient.cs @@ -54,12 +54,13 @@ protected override bool ProcessClientInfo(CLC_ClientInfo msg) { base.ProcessClientInfo(msg); if (HLTV) { - + HLTV = false; + Disconnect("ProcessClientInfo: SourceTV can not connect to game directly.\n"); + return false; } - if (sv_allowupload.GetBool()) { - - } + if (sv_allowupload.GetBool()) + DownloadCustomizations(); return true; } @@ -73,7 +74,6 @@ protected override bool ProcessMove(CLC_Move m) { return true; } - LastMovementTick = (int)sv.TickCount; int totalCmds = m.NewCommands + m.BackupCommands; @@ -109,7 +109,10 @@ protected override bool ProcessMove(CLC_Move m) { // bool ProcessVoiceData(CLC_VoiceData msg) { } - // bool ProcessCmdKeyValues(CLC_CmdKeyValues msg) { } + // bool ProcessCmdKeyValues(CLC_CmdKeyValues msg) { + // SV.ServerGameClients.ClientCommandKeyValues(Edict, msg.KeyValues); + // return true; + // } // bool ProcessRespondCvarValue(CLC_RespondCvarValue msg) { } @@ -119,9 +122,36 @@ protected override bool ProcessMove(CLC_Move m) { // bool ProcessSaveReplay(CLC_SaveReplay pMsg) { } - // void DownloadCustomizations() { } + void DownloadCustomizations() { } + + public override void Connect(ReadOnlySpan name, int userID, INetChannel netChannel, bool fakePlayer, int clientChallenge) { + base.Connect(name, userID, netChannel, fakePlayer, clientChallenge); - // void Connect(ReadOnlySpan name, int userID, INetChannel netChannel, bool fakePlayer, int clientChallenge) { } + Edict = sv.Edicts![EntityIndex]; + + // packinfo todo + + IGameEvent? evnt = gameEventManager.CreateEvent("player_connect"); + if (evnt != null) { + evnt.SetInt("userid", GetUserID()); + evnt.SetInt("index", ClientSlot); + evnt.SetString("name", name); + evnt.SetString("networkid", GetNetworkIDString()); + evnt.SetString("address", netChannel != null ? netChannel.GetAddress() : "none"); + evnt.SetInt("bot", fakePlayer ? 1 : 0); + gameEventManager.FireEvent(evnt); + } + + evnt = gameEventManager.CreateEvent("player_connect_client"); + if (evnt != null) { + evnt.SetInt("userid", GetUserID()); + evnt.SetInt("index", ClientSlot); + evnt.SetString("name", name); + evnt.SetString("networkid", GetNetworkIDString()); + evnt.SetInt("bot", fakePlayer ? 1 : 0); + gameEventManager.FireEvent(evnt); + } + } void SetupPackInfo(FrameSnapshot snapshot) { } @@ -133,13 +163,24 @@ void SetupPrevPackInfo() { } void UpdateUserSettings() { } - // bool ProcessIncomingLogo(ReadOnlySpan filename) { } - // bool IsHearingClient(int index) { } // bool IsProximityHearingClient(int index) { } - // void Inactivate() { } + public override void Inactivate() { + if (Edict != null && !Edict.IsFree()) + Server.RemoveClientFromGame(this); + + if (IsHLTV()) { + + } + + base.Inactivate(); + + Sounds.Clear(); + VoiceStreams.ClearAll(); + VoiceProximity.ClearAll(); + } protected override bool UpdateAcknowledgedFramecount(int tick) { if (IsFakeClient()) { @@ -372,8 +413,8 @@ protected override void SpawnPlayer() { if (sv.LoadGame) sv.SetPaused(false); else { - // Assert(SV.ServerGameEnts); - // Edict.InitializeEntityDLLFields(); + Assert(SV.ServerGameEnts); + Edict.InitializeEntityDLLFields(); } EntityIndex = ClientSlot + 1; @@ -444,7 +485,6 @@ bool IsEngineClientCommand(in TokenizedCommand args) { // bool SendNetMsg(INetMessage msg, bool forceReliable) { } - public override bool ExecuteStringCommand(ReadOnlySpan c) { if (base.ExecuteStringCommand(c)) return true; diff --git a/Source.Engine/Server/GameServer.cs b/Source.Engine/Server/GameServer.cs index 584e48d0..5b28b727 100644 --- a/Source.Engine/Server/GameServer.cs +++ b/Source.Engine/Server/GameServer.cs @@ -1,13 +1,17 @@ using Source.Common; using Source.Common.Bitbuffers; -using Source.Common.Client; using Source.Common.Commands; using Source.Common.Engine; using Source.Common.Filesystem; +using Source.Common.Formats.BSP; using Source.Common.Networking; -using Source.Common.Server; using Source.Common.Utilities; +using System.Runtime.InteropServices; + +using CommunityToolkit.HighPerformance; +using System.Runtime.CompilerServices; + namespace Source.Engine.Server; /// @@ -78,7 +82,52 @@ internal bool IsLevelMainMenuBackground() { public bool LoadedPlugins; public void CreateEngineStringTables() { + StringTables!.SetTick(TickCount); + + int size = Unsafe.SizeOf(); + DownloadableFileTable = StringTables.CreateStringTable("downloadables", 8192, 0, 0); // DOWNLOADABLE_FILE_TABLENAME, MAX_DOWNLOADABLE_FILES + ModelPrecacheTable = StringTables.CreateStringTableEx(PrecacheItem.MODEL_PRECACHE_TABLENAME, PrecacheItem.MAX_MODELS, size, PrecacheItem.PRECACHE_USER_DATA_NUMBITS, false); + GenericPrecacheTable = StringTables.CreateStringTableEx(PrecacheItem.GENERIC_PRECACHE_TABLENAME, PrecacheItem.MAX_GENERIC, size, PrecacheItem.PRECACHE_USER_DATA_NUMBITS, false); + SoundPrecacheTable = StringTables.CreateStringTableEx(PrecacheItem.SOUND_PRECACHE_TABLENAME, PrecacheItem.MAX_SOUNDS, size, PrecacheItem.PRECACHE_USER_DATA_NUMBITS, false); + DecalPrecacheTable = StringTables.CreateStringTableEx(PrecacheItem.DECAL_PRECACHE_TABLENAME, PrecacheItem.MAX_BASE_DECAL, size, PrecacheItem.PRECACHE_USER_DATA_NUMBITS, false); + InstanceBaselineTable = StringTables.CreateStringTable(Protocol.INSTANCE_BASELINE_TABLENAME, Constants.MAX_DATATABLES); + LightStyleTable = StringTables.CreateStringTable(Protocol.LIGHT_STYLES_TABLENAME, BSPFileCommon.MAX_LIGHTSTYLES); + UserInfoTable = StringTables.CreateStringTable(Protocol.USER_INFO_TABLENAME, 1 << Constants.ABSOLUTE_PLAYER_LIMIT_DW); + DynamicModelsTable = StringTables.CreateStringTable("DynamicModels", 2048, 1, 1); + // ServerStartupDataTable = StringTables.CreateStringTable(Protocol.SERVER_STARTUP_DATA_TABLENAME, 4); + + SetQueryPortFromSteamServer(); + // CopyPureServerWhitelistToStringTable(); + + Assert( + DownloadableFileTable != null && + ModelPrecacheTable != null && + GenericPrecacheTable != null && + SoundPrecacheTable != null && + DecalPrecacheTable != null && + InstanceBaselineTable != null && + LightStyleTable != null && + UserInfoTable != null && + DynamicModelsTable != null + ); + + int j; + + for (int i = 0; i < BSPFileCommon.MAX_LIGHTSTYLES; i++) { + Span name = stackalloc char[8]; + sprintf(name, "%i").I(i); + j = LightStyleTable.AddString(true, name); + Assert(j == i); + } + + for (int i = 0; i < GetMaxClients(); i++) { + Span name = stackalloc char[8]; + sprintf(name, "%i").I(i); + j = UserInfoTable.AddString(true, name); + Assert(j == i); + } + // DownloadListGenerator.SetStringTable(DownloadableFileTable); } public INetworkStringTable? GetModelPrecacheTable() => ModelPrecacheTable; @@ -95,14 +144,35 @@ public int PrecacheModel(ReadOnlySpan name, Res flags, Model? model = null int idx = ModelPrecacheTable.AddString(true, name); if (idx == INetworkStringTable.INVALID_STRING_INDEX) return -1; - throw new NotImplementedException(); + + PrecacheUserData p = new(); + Span existing = ModelPrecacheTable.GetStringUserData(idx).AsSpan().Cast(); + + if (existing.IsEmpty) + p.Flags = flags; + else { + p = existing[0]; + p.Flags |= flags; + } + + ModelPrecacheTable.SetStringUserData(idx, Unsafe.SizeOf(), MemoryMarshal.AsBytes(MemoryMarshal.CreateSpan(ref p, 1))); + + PrecacheItem slot = ModelPrecache[idx]; + slot ??= ModelPrecache[idx] = new PrecacheItem(); + + if (model != null) + slot.SetModel(model); + + // todo finish + + return idx; } public Model? GetModel(int index) { if (index <= 0 || ModelPrecacheTable == null) return null; if (index >= ModelPrecacheTable.GetNumStrings()) return null; - PrecacheItem slot = ModelPrecache![index]; + PrecacheItem slot = ModelPrecache[index]; return slot.GetModel(); } public int LookupModelIndex(ReadOnlySpan name) { @@ -119,7 +189,24 @@ public int PrecacheSound(ReadOnlySpan name, Res flags) { int idx = SoundPrecacheTable.AddString(true, name); if (idx == INetworkStringTable.INVALID_STRING_INDEX) return -1; - throw new NotImplementedException(); + + PrecacheUserData p = new(); + Span existing = SoundPrecacheTable.GetStringUserData(idx).AsSpan().Cast(); + + if (existing.IsEmpty) + p.Flags = flags; + else { + p = existing[0]; + p.Flags |= flags; + } + + SoundPrecacheTable.SetStringUserData(idx, Unsafe.SizeOf(), MemoryMarshal.AsBytes(MemoryMarshal.CreateSpan(ref p, 1))); + + PrecacheItem slot = SoundPrecache[idx]; + slot ??= SoundPrecache[idx] = new PrecacheItem(); + slot.SetName(new(name)); + + return idx; } public ReadOnlySpan GetSound(int index) { if (index <= 0 || SoundPrecacheTable == null) @@ -142,7 +229,24 @@ public int PrecacheGeneric(ReadOnlySpan name, Res flags) { int idx = GenericPrecacheTable.AddString(true, name); if (idx == INetworkStringTable.INVALID_STRING_INDEX) return -1; - throw new NotImplementedException(); + + PrecacheUserData p = new(); + Span existing = GenericPrecacheTable.GetStringUserData(idx).AsSpan().Cast(); + + if (existing.IsEmpty) + p.Flags = flags; + else { + p = existing[0]; + p.Flags |= flags; + } + + GenericPrecacheTable.SetStringUserData(idx, Unsafe.SizeOf(), MemoryMarshal.AsBytes(MemoryMarshal.CreateSpan(ref p, 1))); + + PrecacheItem slot = GenericPrecache[idx]; + slot ??= GenericPrecache[idx] = new PrecacheItem(); + slot.SetGeneric(new(name)); + + return idx; } public ReadOnlySpan GetGeneric(int index) { if (index <= 0 || GenericPrecacheTable == null) @@ -184,10 +288,10 @@ public void UpdateHibernationState() { } - public PrecacheItem[]? ModelPrecache; - public PrecacheItem[]? GenericPrecache; - public PrecacheItem[]? SoundPrecache; - public PrecacheItem[]? DecalPrecache; + public PrecacheItem[] ModelPrecache = new PrecacheItem[PrecacheItem.MAX_MODELS]; + public PrecacheItem[] GenericPrecache = new PrecacheItem[PrecacheItem.MAX_GENERIC]; + public PrecacheItem[] SoundPrecache = new PrecacheItem[PrecacheItem.MAX_SOUNDS]; + public PrecacheItem[] DecalPrecache = new PrecacheItem[PrecacheItem.MAX_BASE_DECAL]; public GameClient Client(int i) => (GameClient)Clients[i]; From 3ac6b2c991f3e8c223a262c88e582010d2b4bda2 Mon Sep 17 00:00:00 2001 From: callumok2004 Date: Fri, 27 Feb 2026 21:59:44 +0000 Subject: [PATCH 05/31] Some ServerPlugin impl --- Game.Server/GameInterface.cs | 4 +- Game.Server/HL2MP/HL2MP_Player.cs | 4 +- Source.Engine/Server/GameClient.cs | 13 ++-- Source.Engine/ServerPlugin.cs | 99 +++++++++++++++++++++++++++++- Source.Engine/SourceDLLMain.cs | 1 + 5 files changed, 109 insertions(+), 12 deletions(-) diff --git a/Game.Server/GameInterface.cs b/Game.Server/GameInterface.cs index 4480411c..234d5b34 100644 --- a/Game.Server/GameInterface.cs +++ b/Game.Server/GameInterface.cs @@ -1,5 +1,6 @@ global using static Game.Server.EngineCallbacks; +using Game.Server.GarrysMod; using Game.Shared; using Microsoft.Extensions.DependencyInjection; @@ -358,7 +359,8 @@ public void ClientEarPosition(Edict entity, out Vector3 earOrigin) { } public void ClientPutInServer(Edict entity, ReadOnlySpan playerName) { - throw new NotImplementedException(); + // throw new NotImplementedException(); + GMODClient.ClientPutInServer(entity, playerName); } public void ClientSettingsChanged(Edict edict) { diff --git a/Game.Server/HL2MP/HL2MP_Player.cs b/Game.Server/HL2MP/HL2MP_Player.cs index 8a2a35ec..51b2f9cf 100644 --- a/Game.Server/HL2MP/HL2MP_Player.cs +++ b/Game.Server/HL2MP/HL2MP_Player.cs @@ -12,6 +12,7 @@ namespace Game.Server.HL2MP; using FIELD = FIELD; using FIELD_RD = FIELD; +[LinkEntityToClass("player")] public class HL2MP_Player : HL2_Player { public static readonly SendTable DT_HL2MPLocalPlayerExclusive = new([ @@ -64,7 +65,8 @@ public class HL2MP_Player : HL2_Player public bool IsWalking; } -public class HL2MPRagdoll : BaseAnimatingOverlay { +public class HL2MPRagdoll : BaseAnimatingOverlay +{ public static readonly SendTable DT_HL2MPRagdoll = new([ SendPropVector(FIELD_RD.OF(nameof(RagdollOrigin)), 0, PropFlags.Coord), SendPropEHandle(FIELD_RD.OF(nameof(Player))), diff --git a/Source.Engine/Server/GameClient.cs b/Source.Engine/Server/GameClient.cs index 23de5cca..43f69ba6 100644 --- a/Source.Engine/Server/GameClient.cs +++ b/Source.Engine/Server/GameClient.cs @@ -366,16 +366,16 @@ protected override void ActivatePlayer() { if (!sv.LoadGame) { serverGlobalVariables.CurTime = sv.GetTime(); Common.TimestampedLog("g_pServerPluginHandler->ClientPutInServer"); - // g_pServerPluginHandler->ClientPutInServer( edict, m_Name ); + serverPluginHandler.ClientPutInServer(Edict, Name); } Common.TimestampedLog("g_pServerPluginHandler->ClientActivate"); - // g_pServerPluginHandler->ClientActive(edict, sv.m_bLoadgame); + serverPluginHandler.ClientActive(Edict, sv.LoadGame); Common.TimestampedLog("g_pServerPluginHandler->ClientSettingsChanged"); - // g_pServerPluginHandler->ClientSettingsChanged(edict); + serverPluginHandler.ClientSettingsChanged(Edict); IGameEvent? evnt = gameEventManager.CreateEvent("player_activate"); @@ -521,13 +521,12 @@ public override bool ExecuteStringCommand(ReadOnlySpan c) { if (command.IsFlagSet(FCvar.DevelopmentOnly)) return false; - // serverPluginHandler.SetCommandClient(ClientSlot); + serverPluginHandler.SetCommandClient(ClientSlot); cmd.Dispatch(command, args); } - else { - // serverPluginHandler.ClientCommand(edict, args); - } + else + serverPluginHandler.ClientCommand(Edict, args); return true; } diff --git a/Source.Engine/ServerPlugin.cs b/Source.Engine/ServerPlugin.cs index d174ef60..146207fc 100644 --- a/Source.Engine/ServerPlugin.cs +++ b/Source.Engine/ServerPlugin.cs @@ -1,4 +1,5 @@ using Source.Common; +using Source.Common.Commands; using Source.Common.Engine; using Source.Common.Formats.Keyvalues; @@ -10,19 +11,111 @@ namespace Source.Engine; public class ServerPlugin : IServerPluginHelpers { - public void ClientCommand(Edict entity, ReadOnlySpan cmd) { + void LoadPlugins() { throw new NotImplementedException(); } - public void CreateMessage(Edict entity, DialogType type, KeyValues data, IServerPluginCallbacks plugin) { + void UnloadPlugins() { + throw new NotImplementedException(); + } + + bool UnloadPlugin(int index) { + throw new NotImplementedException(); + } + + bool LoadPlugin(ReadOnlySpan fileName) { + throw new NotImplementedException(); + } + + void DisablePlugins() { + throw new NotImplementedException(); + } + + void EnablePlugins() { + throw new NotImplementedException(); + } + + void DisablePlugin(int index) { throw new NotImplementedException(); } - public int StartQueryCvarValue(Edict entity, ReadOnlySpan pName) { + void EnablePlugin(int index) { throw new NotImplementedException(); } + void PrintDetails() { + throw new NotImplementedException(); + } + + void LevelInit(ReadOnlySpan mapName, ReadOnlySpan maxEntities, ReadOnlySpan oldLevel, ReadOnlySpan landmarkName, bool loadGame, bool background) { + throw new NotImplementedException(); + } + + void ServerActivate(Edict[] edictList, int edictCount, int clientMax) { + serverGameDLL.ServerActivate(edictList, edictCount, clientMax); + } + public void GameFrame(bool simulating) { serverGameDLL.GameFrame(simulating); } + + void LevelShutdown() { + serverGameDLL.LevelShutdown(); + } + + public void ClientActive(Edict entity, bool loadGame) { + SV.ServerGameClients!.ClientActive(entity, loadGame); + } + + void ClientDisconnect(Edict entity) { + SV.ServerGameClients!.ClientDisconnect(entity); + } + + public void ClientPutInServer(Edict entity, ReadOnlySpan playername) { + SV.ServerGameClients!.ClientPutInServer(entity, playername); + } + + public void SetCommandClient(int index) { + SV.ServerGameClients!.SetCommandClient(index); + } + + public void ClientSettingsChanged(Edict edict) { + SV.ServerGameClients!.ClientSettingsChanged(edict); + } + + bool ClientConnect(Edict entity, ReadOnlySpan pszName, ReadOnlySpan pszAddress, ReadOnlySpan reject, int maxrejectlen) { + throw new NotImplementedException(); + } + + public void ClientCommand(Edict entity, TokenizedCommand args) { + SV.ServerGameClients!.ClientCommand(entity, args); + } + + public QueryCvarCookie_t StartQueryCvarValue(Edict entity, ReadOnlySpan cvar) { + throw new NotImplementedException(); + } + + void NetworkIDValidated(ReadOnlySpan userName, ReadOnlySpan networkID) { + throw new NotImplementedException(); + } + + void OnQueryCvarValueFinished(QueryCvarCookie_t cookie, Edict playerEntity, QueryCvarValueStatus status, ReadOnlySpan cvar, ReadOnlySpan cvarValue) { + throw new NotImplementedException(); + } + + void OnEdictAllocated(Edict edict) { + throw new NotImplementedException(); + } + + void OnEdictFreed(Edict edict) { + throw new NotImplementedException(); + } + + public void CreateMessage(Edict entity, DialogType type, KeyValues data, IServerPluginCallbacks plugin) { + throw new NotImplementedException(); + } + + public void ClientCommand(Edict entity, ReadOnlySpan cmd) { + throw new NotImplementedException(); + } } diff --git a/Source.Engine/SourceDLLMain.cs b/Source.Engine/SourceDLLMain.cs index 82fafa0a..caf6f087 100644 --- a/Source.Engine/SourceDLLMain.cs +++ b/Source.Engine/SourceDLLMain.cs @@ -27,6 +27,7 @@ public static class SourceDllMain [Dependency] public static GameServer sv { get; private set; } = null!; [Dependency(Required = false)] public static IBaseClientDLL? g_ClientDLL { get; private set; } = null!; [Dependency] public static IServerGameDLL serverGameDLL { get; private set; } = null!; + [Dependency] public static ServerPlugin serverPluginHandler { get; private set; } = null!; [Dependency] public static IClientEntityList entitylist { get; private set; } = null!; [Dependency] public static IModelLoader modelloader { get; private set; } = null!; [Dependency] public static IMaterialSystem materials { get; private set; } = null!; From 9f608c31bb469b710b6a18a0cc6d95aaca9d8b78 Mon Sep 17 00:00:00 2001 From: callumok2004 Date: Sat, 28 Feb 2026 02:36:53 +0000 Subject: [PATCH 06/31] Allocate edicts --- Game.Server/BaseEntity.cs | 29 +++++-- Game.Server/GameInterface.cs | 14 +++- Game.Server/GarrysMod/GMOD_Player.cs | 1 - Game.Server/Player.cs | 81 ++++++++++++++++++ Game.Server/ServerNetworkProperty.cs | 11 ++- Game.Shared/BasePlayerShared.cs | 15 ++-- Source.Engine/EngineServerImpl.cs | 8 +- Source.Engine/PrEdict.cs | 121 +++++++++++++++++++++++++-- Source.Engine/Server/BaseServer.cs | 45 +++++++++- Source.Engine/ServerPlugin.cs | 4 +- Source.Engine/SourceDLLMain.cs | 1 + 11 files changed, 302 insertions(+), 28 deletions(-) diff --git a/Game.Server/BaseEntity.cs b/Game.Server/BaseEntity.cs index 2c7fde8a..6516ba4d 100644 --- a/Game.Server/BaseEntity.cs +++ b/Game.Server/BaseEntity.cs @@ -125,6 +125,23 @@ private static void SendProxy_SimulationTime(SendProp prop, object instance, IFi SendPropInt(FIELD.OF(nameof(MapCreatedID)), 16), ]); + public BaseEntity() { + // todo todo + + // CollisionProp().Init(this); + NetworkProp().Init(this); + + AddEFlags(EFL.NoThinkFunction | EFL.NoGamePhysicsSimulation | EFL.UsePartitionWhenNotSolid); + + SetSolid(SolidType.None); + // ClearSolidFlags(); + + SetMoveType(Source.MoveType.None); + // SetModelIndex(0); + + ClearFlags(); + } + public float Gravity; public void SetPredictionEligible(bool canpredict) { } // nothing in game code public ref readonly Vector3 GetLocalOrigin() => ref AbsOrigin; @@ -147,7 +164,7 @@ private static void SendProxy_Angles(SendProp prop, object instance, IFieldAcces public Team? GetTeam() => GetGlobalTeam(TeamNum); - public static BaseEntity? Instance(Edict ent) => GetContainingEntity(ent); + public static BaseEntity? Instance(Edict ent) => GetContainingEntity(ent); public byte RenderFX; public byte RenderMode; @@ -235,14 +252,14 @@ public bool FClassnameIs(BaseEntity? entity, ReadOnlySpan classname) { return 0 == strcmp(entity.GetClassname(), classname); } - + public bool IsServer() => true; public bool IsClient() => false; public ReadOnlySpan GetDLLType() => "server"; public WaterLevel GetWaterLevel() => (WaterLevel)WaterLevel; public void SetWaterLevel(WaterLevel level) => WaterLevel = (byte)level; - public void SetMoveCollide(MoveCollide moveCollide) => MoveCollide = (byte)moveCollide; + public void SetMoveCollide(MoveCollide moveCollide) => MoveCollide = (byte)moveCollide; public CollisionProperty CollisionProp() => Collision; public bool SetModel(ReadOnlySpan modelName) { @@ -333,13 +350,13 @@ public ref readonly Vector3 GetAbsVelocity() { } string? Classname; - public void SetClassname(ReadOnlySpan classname){ + public void SetClassname(ReadOnlySpan classname) { Classname = new(classname); } public Edict Edict() => NetworkProp().Edict(); - public void PostConstructor(ReadOnlySpan classname){ + public void PostConstructor(ReadOnlySpan classname) { if (!classname.IsEmpty) SetClassname(classname); @@ -404,7 +421,7 @@ public void SetModelIndex(int index) { } public void SetRefEHandle(in BaseHandle handle) { - throw new NotImplementedException(); + // todo } diff --git a/Game.Server/GameInterface.cs b/Game.Server/GameInterface.cs index 234d5b34..275b33a4 100644 --- a/Game.Server/GameInterface.cs +++ b/Game.Server/GameInterface.cs @@ -335,11 +335,19 @@ public void Think(bool finalTick) { public class ServerGameClients : IServerGameClients { public void ClientActive(Edict entity, bool loadGame) { - throw new NotImplementedException(); + GMODClient.ClientActive(entity, loadGame); + + if (gpGlobals.LoadType == MapLoadType.LoadGame) { + // todo + } + + BasePlayer player = (BasePlayer)BaseEntity.Instance(entity)!; + // CSoundEnvelopeController::GetController().CheckLoopingSoundsForPlayer(pPlayer); + // SceneManager_ClientActive(pPlayer); } public void ClientCommand(Edict entity, in TokenizedCommand args) { - throw new NotImplementedException(); + // throw new NotImplementedException(); } public void ClientCommandKeyValues(Edict entity, KeyValues keyValues) { @@ -364,7 +372,7 @@ public void ClientPutInServer(Edict entity, ReadOnlySpan playerName) { } public void ClientSettingsChanged(Edict edict) { - throw new NotImplementedException(); + // throw new NotImplementedException(); } public void ClientSetupVisibility(Edict viewEntity, Edict client, Span pvs) { diff --git a/Game.Server/GarrysMod/GMOD_Player.cs b/Game.Server/GarrysMod/GMOD_Player.cs index 59e48339..db5c9dc0 100644 --- a/Game.Server/GarrysMod/GMOD_Player.cs +++ b/Game.Server/GarrysMod/GMOD_Player.cs @@ -24,7 +24,6 @@ public static class GMOD_PlayerGlobals public class GMOD_Player : HL2MP_Player { - static Edict? s_PlayerEdict; public static GMOD_Player CreatePlayer(ReadOnlySpan classname, Edict ed) { s_PlayerEdict = ed; return (GMOD_Player?)CreateEntityByName(classname); diff --git a/Game.Server/Player.cs b/Game.Server/Player.cs index d06c5adb..d1c83792 100644 --- a/Game.Server/Player.cs +++ b/Game.Server/Player.cs @@ -2,6 +2,7 @@ using Source; using Source.Common; +using Source.Common.Client; using Source.Common.Engine; using Source.Common.Mathematics; using Source.Common.Physics; @@ -74,6 +75,8 @@ public partial class BasePlayer : BaseCombatCharacter SendPropDataTable( "localdata", DT_LocalPlayerExclusive, SendProxy_SendLocalDataTable), ]); + public static Edict? s_PlayerEdict; + public int StuckLast; public float MaxSpeed() => Maxspeed; public void SetMaxSpeed(float maxSpeed) => Maxspeed = maxSpeed; @@ -96,6 +99,84 @@ public static void SendProxy_CropFlagsToPlayerFlagBitsLength(SendProp prop, obje public static readonly new ServerClass ServerClass = new ServerClass("BasePlayer", DT_BasePlayer).WithManualClassID(StaticClassIndices.CBasePlayer); + public BasePlayer() { + AddEFlags(EFL.NoAutoEdictAttach); + + if (s_PlayerEdict != null) { + NetworkProp().AttachEdict(s_PlayerEdict); + s_PlayerEdict = null; + } + + pl.FixAngle = (int)FixAngle.Absolute; + pl.HLTV = false; + pl.Replay = false; + pl.Frags = 0; + pl.Deaths = 0; + + Netname[0] = '\0'; + + Health = 0; + Weapon_SetLast(null); + // BitsDamageType = 0; + + // ForceOrigin = false; + Vehicle.Set(null); + // CurrentCommand = null; + // LockViewanglesTickNumber = 0; + // AngLockViewangles.Init(); + + // Setup our default FOV + // DefaultFOV = GameRules.DefaultFOV(); + + ZoomOwner.Set(null); + + // UpdateRate = 20; // cl_updaterate defualt + // LerpTime = 0.1f; // cl_interp default + // PredictWeapons = true; + // LagCompensation = false; + LaggedMovementValue = 1.0f; + StuckLast = 0; + // ImpactEnergyScale = 1.0f; + // LastPlayerTalkTime = 0.0f; + // PlayerInfo.SetParent(this); + + // ResetObserverMode(); + + SurfaceProps = 0; + SurfaceData = null; + SurfaceFriction = 1.0f; + // TextureType = 0; + // PreviousTextureType = 0; + + // SuicideCustomKillFlags = 0; + // Delay = 0.0f; + // ReplayEnd = -1; + // ReplayEntity = 0; + + // AutoKickDisabled = false; + + // NumCrouches = 0; + // DuckToggled = false; + // PhysicsWasFrozen = false; + + // ButtonDisabled = 0; + // ButtonForced = 0; + + // BodyPitchPoseParam = -1; + // ForwardMove = 0; + // SideMove = 0; + + // // NVNT default to no haptics + // HasHaptics = false; + + ConstraintCenter = vec3_origin; + + // LastUserCommandTime = 0.0f; + // MovementTimeForUserCmdProcessingRemaining = 0.0f; + + // LastObjectiveTime = -1.0f; + } + bool DeadFlag; public readonly PlayerState pl = new(); public readonly PlayerLocalData Local = new(); diff --git a/Game.Server/ServerNetworkProperty.cs b/Game.Server/ServerNetworkProperty.cs index 17a3c70a..d30e8b74 100644 --- a/Game.Server/ServerNetworkProperty.cs +++ b/Game.Server/ServerNetworkProperty.cs @@ -48,7 +48,7 @@ public void Release() { [MethodImpl(MethodImplOptions.AggressiveInlining)] public ref PVSInfo GetPVSInfo() => ref PVSInfo; [MethodImpl(MethodImplOptions.AggressiveInlining)] public void SetNetworkParent(EHANDLE parent) => Parent.Index = parent.Index; - public void AttachEdict(Edict? requiredEdict){ + public void AttachEdict(Edict? requiredEdict) { if (requiredEdict == null) requiredEdict = engine.CreateEdict(); @@ -64,4 +64,13 @@ public void AttachEdict(Edict? requiredEdict){ private EHANDLE Parent = new(); // event register later bool PendingStateChange; + + public void Init(BaseEntity entity) { + Pev = null; + Outer = entity; + ServerClass = null; + PendingStateChange = false; + PVSInfo.ClusterCount = 0; + // timerevent todo + } } diff --git a/Game.Shared/BasePlayerShared.cs b/Game.Shared/BasePlayerShared.cs index 59d677cc..4c049388 100644 --- a/Game.Shared/BasePlayerShared.cs +++ b/Game.Shared/BasePlayerShared.cs @@ -8,11 +8,14 @@ #else global using static Game.Server.BasePlayerGlobals; + global using BasePlayer = Game.Server.BasePlayer; #endif using Source.Common.Mathematics; + using Game.Shared; + using System.Numerics; #if CLIENT_DLL @@ -119,7 +122,7 @@ private void CalcPlayerView(ref Vector3 eyeOrigin, ref QAngle eyeAngles, ref flo public ReadOnlySpan GetPlayerName() { return ((Span)Netname).SliceNullTerminatedString(); } - public void SetPlayerName(ReadOnlySpan name){ + public void SetPlayerName(ReadOnlySpan name) { strcpy(Netname, name); } @@ -200,8 +203,8 @@ public void ViewPunchReset(float tolerance) { Local.PunchAngleVel = vec3_angle; } - void Weapon_SetLast(BaseCombatWeapon pWeapon) { - throw new NotImplementedException(); + void Weapon_SetLast(BaseCombatWeapon? pWeapon) { + LastWeapon.Set(pWeapon); } public void SetAnimationExtension(ReadOnlySpan extension) { @@ -252,7 +255,7 @@ public virtual void ItemPostFrame() { #if CLIENT_DLL if (vehicle.IsPredicted()) #endif - vehicle.ItemPostFrame(this); + vehicle.ItemPostFrame(this); if (!usingStandardWeapons || GetVehicle() == null) return; @@ -276,7 +279,7 @@ public virtual void ItemPostFrame() { // Not predicting this weapon if (GetActiveWeapon()!.IsPredicted()) #endif - GetActiveWeapon()!.ItemPostFrame(); + GetActiveWeapon()!.ItemPostFrame(); } } @@ -296,7 +299,7 @@ public void EyeVectors(out Vector3 forward, out Vector3 right, out Vector3 up) { //AngleVectors(m_vecVehicleViewAngles, pForward, pRight, pUp); forward = right = up = default; } - else + else MathLib.AngleVectors(EyeAngles(), out forward, out right, out up); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void EyeVectors(out Vector3 forward, out Vector3 right) => EyeVectors(out forward, out right, out _); diff --git a/Source.Engine/EngineServerImpl.cs b/Source.Engine/EngineServerImpl.cs index 2b977cf1..d569eff8 100644 --- a/Source.Engine/EngineServerImpl.cs +++ b/Source.Engine/EngineServerImpl.cs @@ -92,8 +92,12 @@ public bool CopyFile(ReadOnlySpan source, ReadOnlySpan destination) throw new NotImplementedException(); } - public Edict CreateEdict(int iForceEdictIndex = -1) { - throw new NotImplementedException(); + public Edict? CreateEdict(int forceEdictIndex = -1) { + Edict? edict = ED.Alloc(forceEdictIndex); + + serverPluginHandler.OnEdictAllocated(edict); + + return edict; } public Edict CreateFakeClient(ReadOnlySpan netname) { diff --git a/Source.Engine/PrEdict.cs b/Source.Engine/PrEdict.cs index 4c3fa4ca..04f8a802 100644 --- a/Source.Engine/PrEdict.cs +++ b/Source.Engine/PrEdict.cs @@ -1,10 +1,121 @@ -using Source.Common.Engine; -using Source.Common.Server; -using Source.Engine.Server; +using Source.Common.Commands; +using Source.Common.Engine; namespace Source.Engine; -public class ED { +public class ED +{ + + static readonly ConVar sv_useexplicitdelete = new("1", FCvar.DevelopmentOnly, "Explicitly delete dormant client entities caused by AllowImmediateReuse()."); + static readonly ConVar sv_lowEdicthreshold = new("8", FCvar.None, "When only this many edicts are free, take the action specified by sv_lowedict_action.", 0, Constants.MAX_EDICTS); + static readonly ConVar sv_lowedict_action = new("0", FCvar.None, "0 - no action, 1 - warn to log file, 2 - attempt to restart the game, if applicable, 3 - restart the map, 4 - go to the next map in the map cycle, 5 - spew all edicts.", 0, 5); + static MaxEdictsBitVec FreeEdicts; + + public static Edict? Alloc(int forceEdictIndex) { + if (forceEdictIndex >= 0) { + if (forceEdictIndex >= sv.NumEdicts) { + Warning("ED_Alloc( %d ) - invalid edict index specified.", forceEdictIndex); + return null; + } + + Edict e = sv.Edicts![forceEdictIndex]; + if (e.IsFree()) { + Assert(forceEdictIndex == e.EdictIndex); + --sv.FreeEdicts; + Assert(FreeEdicts.IsBitSet(forceEdictIndex)); + FreeEdicts.Clear(forceEdictIndex); + ClearEdict(e); + return e; + } + else + return null; + } + + // Check the free list first. + int bit = -1; + Edict edict; + for (; ; ) + { + bit = FreeEdicts.FindNextSetBit(bit + 1) - 1; // FIXME: This is returning 8192, so we must -1 otherwise we are 1 over the limit? + if (bit < 0) + break; + + edict = sv.Edicts![bit]; + + // If this assert goes off, someone most likely called pedict.ClearFree() and not ED_ClearFreeFlag()? + Assert(edict.IsFree()); + Assert(bit == edict.EdictIndex); + if ((edict.FreeTime < 2) || (sv.GetTime() - edict.FreeTime >= 1.0 /*EDICT_FREETIME*/)) { + // If we have no freetime, we've had AllowImmediateReuse() called. We need + // to explicitly delete this old entity. + if (edict.FreeTime == 0 && sv_useexplicitdelete.GetBool()) { + //Warning("ADDING SLOT to snapshot: %d\n", i ); + // framesnapshotmanager.AddExplicitDelete(bit); TODO + } + + --sv.FreeEdicts; + FreeEdicts.Clear(edict.EdictIndex); + ClearEdict(edict); + return edict; + } + } + + // Allocate a new edict. + if (sv.NumEdicts >= sv.MaxEdicts) { + AssertMsg(false, "Can't allocate edict"); + + //todo SpewEdicts(); // Log the entities we have before we die + + if (sv.MaxEdicts == 0) + Sys.Error("ED_Alloc: No edicts yet"); + Sys.Error("ED_Alloc: no free edicts"); + } + + // Do this before clearing since clear now needs to call back into the edict to deduce the index so can get the changeinfo data in the parallel structure + edict = sv.Edicts![sv.NumEdicts++]; + + // We should not be in the free list... + Assert(!FreeEdicts.IsBitSet(edict.EdictIndex)); + ClearEdict(edict); + + if (sv_lowedict_action.GetInt() > 0 && sv.NumEdicts >= sv.MaxEdicts - sv_lowEdicthreshold.GetInt()) { + int edictsRemaining = sv.MaxEdicts - sv.NumEdicts; + // Log.Printf("Warning: free edicts below threshold. %i free edict%s remaining.\n", edictsRemaining, edictsRemaining == 1 ? "" : "s"); todo + + switch (sv_lowedict_action.GetInt()) { + case 2: + // restart the game + { + ConVarRef mp_restartgame_immediate = new("mp_restartgame_immediate"); + if (mp_restartgame_immediate.IsValid()) { + mp_restartgame_immediate.SetValue(1); + } + else { + ConVarRef mp_restartgame = new("mp_restartgame"); + if (mp_restartgame.IsValid()) + mp_restartgame.SetValue(1); + } + } + break; + case 3: + // restart the map + engine.ChangeLevel(sv.GetMapName(), null); + break; + case 4: + // go to the next map + engine.ServerCommand("changelevel_next\n"); + break; + case 5: + // spew all edicts + // SpewEdicts(); todo + break; + } + } + + return edict; + } + + public static void ClearEdict(Edict e) { e.ClearFree(); e.ClearStateChanged(); @@ -15,8 +126,6 @@ public static void ClearEdict(Edict e) { e.NetworkSerialNumber = -1; // must be filled by game.dll } - MaxEdictsBitVec FreeEdicts; - public void ClearFreeEdictList() { FreeEdicts.ClearAll(); } diff --git a/Source.Engine/Server/BaseServer.cs b/Source.Engine/Server/BaseServer.cs index bb6ab1f1..212f63ca 100644 --- a/Source.Engine/Server/BaseServer.cs +++ b/Source.Engine/Server/BaseServer.cs @@ -171,8 +171,51 @@ public virtual void BroadcastMessage(INetMessage msg, bool onlyActive = false, b } } public virtual void BroadcastMessage(INetMessage msg, IRecipientFilter filter) { - throw new NotImplementedException(); + if (filter.IsInitMessage()) { + // This really only applies to the first player to connect, but that works in single player well enought + if (IsActive()) + ConDMsg("SV_BroadcastMessage: Init message being created after signon buffer has been transmitted\n"); + + if (!msg.WriteToBuffer(Signon)) { + Sys.Error("SV_BroadcastMessage: Init message would overflow signon buffer!\n"); + return; + } + } + else { + msg.SetReliable(filter.IsReliable()); + + int num = filter.GetRecipientCount(); + + for (int i = 0; i < num; i++) { + int index = filter.GetRecipientIndex(i); + + if (index < 1 || index > Clients.Count) { + Msg("SV_BroadcastMessage: Recipient Filter for message type %i (reliable: %s, init: %s) with bogus client index (%i) in list of %i clients\n", + msg.GetType(), + filter.IsReliable() ? "yes" : "no", + filter.IsInitMessage() ? "yes" : "no", + index, num); + + if (msg.IsReliable()) + Host.Error($"Reliable message (type {msg.GetType()}) discarded."); + + continue; + } + + BaseClient cl = Clients[index - 1]; + + if (!cl.IsSpawned()) + continue; + + if (!cl.SendNetMsg(msg)) { + if (msg.IsReliable()) { + DevMsg($"BroadcastMessage: Reliable filter message overflow for client {cl.GetClientName()}"); + } + } + } + } } + public virtual void BroadcastPrintf(ReadOnlySpan msg) { throw new NotImplementedException(); } diff --git a/Source.Engine/ServerPlugin.cs b/Source.Engine/ServerPlugin.cs index 146207fc..5ca6509b 100644 --- a/Source.Engine/ServerPlugin.cs +++ b/Source.Engine/ServerPlugin.cs @@ -103,8 +103,8 @@ void OnQueryCvarValueFinished(QueryCvarCookie_t cookie, Edict playerEntity, Quer throw new NotImplementedException(); } - void OnEdictAllocated(Edict edict) { - throw new NotImplementedException(); + public void OnEdictAllocated(Edict edict) { + // throw new NotImplementedException(); } void OnEdictFreed(Edict edict) { diff --git a/Source.Engine/SourceDLLMain.cs b/Source.Engine/SourceDLLMain.cs index caf6f087..20d8c7b6 100644 --- a/Source.Engine/SourceDLLMain.cs +++ b/Source.Engine/SourceDLLMain.cs @@ -19,6 +19,7 @@ namespace Source.Engine; // to constantly be polling for dependencies public static class SourceDllMain { + [Dependency] public static IEngineServer engine { get; private set; } = null!; [Dependency] public static IGameEventManager2 gameEventManager { get; private set; } = null!; [Dependency] public static IEngineVGui __EngineVGui { get; private set; } = null!; public static GameEventManager g_GameEventManager => (GameEventManager)gameEventManager; From 3d3bdc58fe0d078828fa78f1a10ece6cd11114ef Mon Sep 17 00:00:00 2001 From: callumok2004 Date: Sat, 28 Feb 2026 12:53:26 +0000 Subject: [PATCH 07/31] Fix usermessages overflowing --- Game.Server/Util.cs | 8 ++++---- Source.Common/Bitbuffers/bf_write.cs | 4 ++-- Source.Engine/EngineServerImpl.cs | 16 ++++++++++++++-- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/Game.Server/Util.cs b/Game.Server/Util.cs index 5d30c1e1..30143767 100644 --- a/Game.Server/Util.cs +++ b/Game.Server/Util.cs @@ -141,22 +141,22 @@ public static void ClientPrintFilter(scoped in Filter filter, HudPrint d WRITE_BYTE((byte)dest); WRITE_STRING(msgName); - if (param1.IsEmpty) + if (!param1.IsEmpty) WRITE_STRING(param1); else WRITE_STRING(""); - if (param2.IsEmpty) + if (!param2.IsEmpty) WRITE_STRING(param2); else WRITE_STRING(""); - if (param3.IsEmpty) + if (!param3.IsEmpty) WRITE_STRING(param3); else WRITE_STRING(""); - if (param4.IsEmpty) + if (!param4.IsEmpty) WRITE_STRING(param4); else WRITE_STRING(""); diff --git a/Source.Common/Bitbuffers/bf_write.cs b/Source.Common/Bitbuffers/bf_write.cs index 01d158db..5ee46304 100644 --- a/Source.Common/Bitbuffers/bf_write.cs +++ b/Source.Common/Bitbuffers/bf_write.cs @@ -67,9 +67,9 @@ public bf_write(byte[] data, int bytes, int bits) { StartWriting(data, bytes, 0, bits); } - public unsafe void StartWriting(byte[] inData, nuint bytes, int startBit, int bits = -1) + public unsafe void StartWriting(byte[] inData, nuint bytes, int startBit = 0, int bits = -1) => StartWriting(inData, (int)bytes, startBit, bits); - public unsafe void StartWriting(byte[] inData, int bytes, int startBit, int bits = -1) { + public unsafe void StartWriting(byte[] inData, int bytes, int startBit = 0, int bits = -1) { // Ensure d-word alignment Debug.Assert(bytes % 4 == 0); diff --git a/Source.Engine/EngineServerImpl.cs b/Source.Engine/EngineServerImpl.cs index d569eff8..27dd6ae2 100644 --- a/Source.Engine/EngineServerImpl.cs +++ b/Source.Engine/EngineServerImpl.cs @@ -520,14 +520,26 @@ class MsgData { public MsgData() { Reset(); + + EntityMsg.DataOut.StartWriting(EntityData, EntityData.Length); + EntityMsg.DataOut.DebugName = "s_MsgData.EntityMsg.DataOut"; + + UserMsg.DataOut.StartWriting(UserData, UserData.Length); + UserMsg.DataOut.DebugName = "s_MsgData.UserMsg.DataOut"; } public void Reset() { - + Filter = null; + Reliable = false; + SubType = 0; + Started = false; + UserMessageSize = -1; + UserMessageName = null; + CurrentMsg = null; } public readonly byte[] UserData = new byte[PAD_NUMBER(Constants.MAX_USER_MSG_DATA, 4)]; // buffer for outgoing user messages - public readonly byte[] EntityDAta = new byte[PAD_NUMBER(Constants.MAX_ENTITY_MSG_DATA, 4)]; // buffer for outgoing entity messages + public readonly byte[] EntityData = new byte[PAD_NUMBER(Constants.MAX_ENTITY_MSG_DATA, 4)]; // buffer for outgoing entity messages public IRecipientFilter? Filter; // clients who get this message public bool Reliable; From 971a5e8744e8a6e6b00e557b6c1e2d7bdf6ec9e1 Mon Sep 17 00:00:00 2001 From: callumok2004 Date: Sat, 28 Feb 2026 14:57:13 +0000 Subject: [PATCH 08/31] report_entities command --- Game.Server/BaseEntity.cs | 10 +-- Game.Server/EntityList.cs | 111 ++++++++++++++++++++++++++++++++-- Game.Shared/BaseEntityList.cs | 15 ++++- 3 files changed, 122 insertions(+), 14 deletions(-) diff --git a/Game.Server/BaseEntity.cs b/Game.Server/BaseEntity.cs index 6516ba4d..38234fdc 100644 --- a/Game.Server/BaseEntity.cs +++ b/Game.Server/BaseEntity.cs @@ -412,18 +412,14 @@ public ReadOnlySpan GetModelName() { throw new NotImplementedException(); } - public ref readonly BaseHandle GetRefEHandle() { - throw new NotImplementedException(); - } + public ref readonly BaseHandle GetRefEHandle() => ref RefEHandle; public void SetModelIndex(int index) { throw new NotImplementedException(); } - public void SetRefEHandle(in BaseHandle handle) { - // todo - } - + BaseHandle RefEHandle; + public void SetRefEHandle(in BaseHandle handle) => RefEHandle = handle; int flags; EFL eflags; diff --git a/Game.Server/EntityList.cs b/Game.Server/EntityList.cs index 43cb3663..9f2ffc53 100644 --- a/Game.Server/EntityList.cs +++ b/Game.Server/EntityList.cs @@ -4,13 +4,10 @@ using Game.Shared; using Source.Common; +using Source.Common.Commands; using Source.Common.Mathematics; -using System; -using System.Collections.Generic; using System.Numerics; -using System.Runtime.InteropServices; -using System.Text; namespace Game.Server; @@ -18,6 +15,89 @@ public static class EntityListGlobals { public static readonly GlobalEntityList gEntList = new(); public static BaseEntityList g_pEntityList = gEntList; + + [ConCommand("report_entities", "List all entities")] + static void report_entities() { + if (!Util.IsCommandIssuedByServerAdmin()) + return; + + SortedEntityList list = new(); + BaseEntity? ent = gEntList.FirstEnt(); + while (ent != null) { + list.AddEntityToList(ent); + ent = gEntList.NextEnt(ent); + } + + list.ReportEntityList(); + } +} + +class SortedEntityList +{ + private readonly List SortedList = new(); + private readonly EntityReportLess Comparer = new(); + private int EmptyCount; + + private sealed class EntityReportLess : IComparer + { + public int Compare(BaseEntity? src1, BaseEntity? src2) { + if (src1 == null && src2 == null) + return 0; + if (src1 == null) + return -1; + if (src2 == null) + return 1; + + return src1.GetClassname().CompareTo(src2.GetClassname(), StringComparison.Ordinal); + } + } + + public void AddEntityToList(BaseEntity? entity) { + if (entity == null) { + EmptyCount++; + return; + } + + int index = SortedList.BinarySearch(entity, Comparer); + if (index < 0) + index = ~index; + + SortedList.Insert(index, entity); + } + + public void ReportEntityList() { + ReadOnlySpan lastClass = default; + int count = 0; + int edicts = 0; + + for (int i = 0; i < SortedList.Count; i++) { + var entity = SortedList[i]; + if (entity == null) + continue; + + if (entity.Edict() != null) + edicts++; + + ReadOnlySpan className = entity.GetClassname(); + + if (!className.Equals(lastClass, StringComparison.Ordinal)) { + if (count > 0) + Msg($"Class: {lastClass} ({count})\n"); + + lastClass = className; + count = 1; + } + else { + count++; + } + } + + if (!lastClass.IsEmpty && count > 0) + Msg($"Class: {lastClass} ({count})\n"); + + if (SortedList.Count > 0) + Msg($"Total {SortedList.Count} entities ({EmptyCount} empty, {edicts} edicts)\n"); + } } public class GlobalEntityList : BaseEntityList @@ -33,6 +113,29 @@ public class GlobalEntityList : BaseEntityList IServerUnknown? unk = (IServerUnknown?)LookupEntity(ent); return unk == null ? null : (BaseEntity?)unk.GetBaseEntity(); } + + public BaseEntity FirstEnt() => NextEnt(null); + + public BaseEntity? NextEnt(BaseEntity? currentEnt) { + if (currentEnt == null) { + EntInfo? info = FirstEntInfo(); + if (info == null) + return null; + + return (BaseEntity?)info.Entity; + } + + EntInfo? list = GetEntInfoPtr(currentEnt.GetRefEHandle()); + if (list != null) + list = NextEntInfo(list); + + while (list != null) { + return (BaseEntity?)list.Entity; + list = NextEntInfo(list); //?? + } + + return null; + } } public enum NotifySystemEvent diff --git a/Game.Shared/BaseEntityList.cs b/Game.Shared/BaseEntityList.cs index 300ac2fc..c5aaf9f2 100644 --- a/Game.Shared/BaseEntityList.cs +++ b/Game.Shared/BaseEntityList.cs @@ -3,7 +3,8 @@ namespace Game.Shared; -public class EntInfo { +public class EntInfo +{ public IHandleEntity? Entity; public int SerialNumber; public EntInfo? Prev; @@ -26,7 +27,7 @@ private BaseHandle AddEntityAtSlot(IHandleEntity ent, int slot, int forcedSerial Assert(entSlot.Entity == null); entSlot.Entity = ent; - if(forcedSerialNum != -1) + if (forcedSerialNum != -1) entSlot.SerialNumber = forcedSerialNum; ActiveList.AddLast(entSlot); @@ -70,6 +71,14 @@ protected virtual void OnAddEntity(IHandleEntity? pEnt, BaseHandle handle) { } protected virtual void OnRemoveEntity(IHandleEntity? pEnt, BaseHandle handle) { } InlineArrayNumEntEntries EntPtrArray; - EntInfoList ActiveList = []; + public EntInfoList ActiveList = []; EntInfoList FreeNonNetworkableList = []; + + public EntInfo? FirstEntInfo() => ActiveList.First?.Value; + public EntInfo? NextEntInfo(EntInfo? current) => current?.Next; + + public EntInfo GetEntInfoPtr(BaseHandle ent) { + int slot = ent.GetEntryIndex(); + return EntPtrArray[slot]; + } } From 16e4eafffddad028f4563e99420ba55a25d73833 Mon Sep 17 00:00:00 2001 From: callumok2004 Date: Sat, 28 Feb 2026 23:47:20 +0000 Subject: [PATCH 09/31] Parse map entities --- Game.Server/BaseEntity.cs | 2 + Game.Server/EntityList.cs | 19 ++ Game.Server/GameInterface.cs | 110 +++++++- Game.Server/MapEntities.cs | 305 ++++++++++++++++++++++- Game.Server/Util.cs | 39 +++ Game.Shared/MapEntitiesShared.cs | 278 +++++++++++++++++++++ Source.Engine/CollisionModelSubsystem.cs | 9 +- Source.Engine/Host.cs | 2 + Source.Engine/ServerPlugin.cs | 6 +- 9 files changed, 759 insertions(+), 11 deletions(-) create mode 100644 Game.Shared/MapEntitiesShared.cs diff --git a/Game.Server/BaseEntity.cs b/Game.Server/BaseEntity.cs index 38234fdc..929a1587 100644 --- a/Game.Server/BaseEntity.cs +++ b/Game.Server/BaseEntity.cs @@ -41,6 +41,7 @@ public partial class BaseEntity : IServerEntity public virtual bool IsCombatItem() => false; public bool ClassMatches(ReadOnlySpan classOrWildcard) => false; // todo public virtual bool IsPredicted() => false; + public virtual bool IsTemplate() => false; private static void SendProxy_AnimTime(SendProp prop, object instance, IFieldAccessor field, ref DVariant outData, int element, int objectID) => throw new NotImplementedException(); private static void SendProxy_SimulationTime(SendProp prop, object instance, IFieldAccessor field, ref DVariant outData, int element, int objectID) @@ -142,6 +143,7 @@ public BaseEntity() { ClearFlags(); } + public string? Name; public float Gravity; public void SetPredictionEligible(bool canpredict) { } // nothing in game code public ref readonly Vector3 GetLocalOrigin() => ref AbsOrigin; diff --git a/Game.Server/EntityList.cs b/Game.Server/EntityList.cs index 9f2ffc53..8e48a284 100644 --- a/Game.Server/EntityList.cs +++ b/Game.Server/EntityList.cs @@ -136,6 +136,25 @@ public class GlobalEntityList : BaseEntityList return null; } + + public void CleanupDeleteList() { + // todo + } + + public BaseEntity? FindEntityByName(BaseEntity startEntity, ReadOnlySpan name, BaseEntity? searchingEntity, BaseEntity? activator, BaseEntity? caller, int/*IEntityFindFilter*/? filter) { + if (name.IsEmpty) + return null; + + if (name[0] == '!') { // todo + + // if (startEntity == null) + // return FindEntityProcedural(name, searchingEntity, activator, caller); + + return null; + } + + return null; // TODO + } } public enum NotifySystemEvent diff --git a/Game.Server/GameInterface.cs b/Game.Server/GameInterface.cs index 275b33a4..17ddf96c 100644 --- a/Game.Server/GameInterface.cs +++ b/Game.Server/GameInterface.cs @@ -1,5 +1,7 @@ global using static Game.Server.EngineCallbacks; +using CommunityToolkit.HighPerformance; + using Game.Server.GarrysMod; using Game.Shared; @@ -292,7 +294,85 @@ public void InvalidateMdlCache() { } public bool LevelInit(ReadOnlySpan pMapName, ReadOnlySpan pMapEntities, ReadOnlySpan pOldLevel, ReadOnlySpan pLandmarkName, bool loadGame, bool background) { - throw new NotImplementedException(); + // ResetWindspeed(); + // UpdateChapterRestrictions(pMapName); + + //Tony; parse custom manifest if exists! + // ParseParticleEffectsMap(pMapName, false); + + // IGameSystem::LevelInitPreEntityAllSystems() is called when the world is precached + // That happens either in LoadGameState() or in MapEntity_ParseAllEntities() + if (loadGame) { + if (!pOldLevel.IsEmpty) + gpGlobals.LoadType = MapLoadType.Transition; + else + gpGlobals.LoadType = MapLoadType.LoadGame; + + // BeginRestoreEntities(); + if (!engine.LoadGameState(pMapName, true)) { + if (!pOldLevel.IsEmpty) + ParseAllEntities(pMapEntities); + else + // Regular save load case + return false; + } + + if (!pOldLevel.IsEmpty) + engine.LoadAdjacentEnts(pOldLevel, pLandmarkName); + + // if (g_OneWayTransition) + // engine.ClearSaveDirAfterClientLoad(); + + // if (pOldLevel && sv_autosave.GetBool() == true) { + // // This is a single-player style level transition. + // // Queue up an autosave one second into the level + // BaseEntity? pAutosave = BaseEntity::Create("logic_autosave", vec3_origin, vec3_angle, NULL); + // if (pAutosave != null) { + // g_EventQueue.AddEvent(pAutosave, "Save", 1.0, NULL, NULL); + // g_EventQueue.AddEvent(pAutosave, "Kill", 1.1, NULL, NULL); + // } + // } + } + else { + if (background) + gpGlobals.LoadType = MapLoadType.Background; + + else + gpGlobals.LoadType = MapLoadType.NewGame; + + // Clear out entity references, and parse the entities into it. + // g_MapEntityRefs.Purge(); + MapLoadEntityFilter filter = new(); + ParseAllEntities(pMapEntities, filter); + + // g_pServerBenchmark.StartBenchmark(); + + // Now call the mod specific parse + // LevelInit_ParseAllEntities(pMapEntities); + } + + // Check low violence settings for this map + // g_RagdollLVManager.SetLowViolence(pMapName); + + // Now that all of the active entities have been loaded in, precache any entities who need point_template parameters + // to be parsed (the above code has loaded all point_template entities) + // PrecachePointTemplates(); + + // load MOTD from file into stringtable + // LoadMessageOfTheDay(); + + // Sometimes an ent will Remove() itself during its precache, so RemoveImmediate won't happen. + // This makes sure those ents get cleaned up. + gEntList.CleanupDeleteList(); + + // g_AIFriendliesTalkSemaphore.Release(); + // g_AIFoesTalkSemaphore.Release(); + // g_OneWayTransition = false; + + // clear any pending autosavedangerous + // m_fAutoSaveDangerousTime = 0.0f; + // m_fAutoSaveDangerousMinHealthToCommit = 0.0f; + return true; } public void LevelShutdown() { @@ -464,3 +544,31 @@ public void SetDebugEdictBase(Edict[] edict) { } } + +struct MapEntityRef +{ + public int Edict; // Which edict slot this entity got. -1 if CreateEntityByName failed. + public int SerialNumber; // The edict serial number. TODO used anywhere ? +}; + + +class MapLoadEntityFilter : IMapEntityFilter +{ + public bool ShouldCreateEntity(ReadOnlySpan className) => true; + + public BaseEntity? CreateNextEntity(ReadOnlySpan className) { + BaseEntity? ret = CreateEntityByName(className); + MapEntityRef entref = new() { + Edict = -1, + SerialNumber = 0 + }; + + if (ret != null) { + entref.Edict = ret.EntIndex(); + if (ret.Edict() != null) + entref.SerialNumber = ret.Edict()!.NetworkSerialNumber; + } + + return ret; + } +} diff --git a/Game.Server/MapEntities.cs b/Game.Server/MapEntities.cs index a2513bea..f94b22ae 100644 --- a/Game.Server/MapEntities.cs +++ b/Game.Server/MapEntities.cs @@ -1,15 +1,35 @@ global using static Game.Server.MapEntities; +using Game.Shared; + +using Source; using Source.Common; using Source.Common.Engine; -using System; -using System.Collections.Generic; -using System.Net; -using System.Text; - namespace Game.Server; +struct HierarchicalSpawn_t +{ + public BaseEntity? Entity; + public int Depth; + public BaseEntity DeferredParent; // attachment parents can't be set until the parents are spawned + public string DeferredParentAttachment; // so defer setting them up until the second pass +}; + +struct HierarchicalSpawnMapData_t +{ + public string MapData; + public int MapDataLength; +}; + +public interface IMapEntityFilter +{ + bool ShouldCreateEntity(ReadOnlySpan className); + BaseEntity? CreateNextEntity(ReadOnlySpan className); +} + +public class PointTemplate : BaseEntity { } // TODO move this + public static class MapEntities { static ref Edict? g_pForceAttachEdict => ref BaseEntity.g_pForceAttachEdict; @@ -32,4 +52,279 @@ public static class MapEntities return entity; } + public static void ParseAllEntities(ReadOnlySpan mapData, IMapEntityFilter? filter = null, bool activateEntities = false) { + HierarchicalSpawnMapData_t[] pSpawnMapData = new HierarchicalSpawnMapData_t[Constants.NUM_ENT_ENTRIES]; + HierarchicalSpawn_t[] spawnList = new HierarchicalSpawn_t[Constants.NUM_ENT_ENTRIES]; + + List pointTemplates = []; + int numEntities = 0; + + Span tokenBuffer = new char[EntityMapData.MAPKEY_MAXLENGTH]; + + // Allow the tools to spawn different things + // if (serverenginetools) { todo? + // mapData = serverenginetools.GetEntityData(mapData); + // } + + // Loop through all entities in the map data, creating each. + for (; true; mapData = MapEntity.SkipToNextEntity(mapData, tokenBuffer)) { + // Parse the opening brace. + Span token = new char[EntityMapData.MAPKEY_MAXLENGTH]; + mapData = MapEntity.ParseToken(mapData, token); + + // Check to see if we've finished or not. + if (mapData.IsEmpty) + break; + + if (token[0] != '{') { + Error("MapEntity.ParseAllEntities: found %s when expecting {", token.ToString()); + continue; + } + + // Parse the entity and add it to the spawn list. + ReadOnlySpan curMapData = mapData; + mapData = ParseEntity(out BaseEntity entity, mapData, filter); + if (entity == null) + continue; + + if (entity.IsTemplate()) { + // It's a template entity. Squirrel away its keyvalue text so that we can + // recreate the entity later via a spawner. mapData points at the '}' + // so we must add one to include it in the string. + // Templates.Add(entity, curMapData, (mapData.Length - curMapData.Length) + 2); TODO + + // Remove the template entity so that it does not show up in FindEntityXXX searches. + Util.Remove(entity); + gEntList.CleanupDeleteList(); + continue; + } + + // To + if (entity is World) { + // TODO entity.Parent = NULL_STRING; // don't allow a parent on the first entity (worldspawn) + + Util.DispatchSpawn(entity); + continue; + } + + // TODO + + // if (entity is NodeEnt ne) { + // // We overflow the max edicts on large maps that have lots of entities. + // // Nodes & Lights remove themselves immediately on Spawn(), so dispatch their + // // spawn now, to free up the slot inside this loop. + // // NOTE: This solution prevents nodes & lights from being used inside point_templates. + // // + // // NOTE: Nodes spawn other entities (ai_hint) if they need to have a persistent presence. + // // To ensure keys are copied over into the new entity, we pass the mapdata into the + // // node spawn function. + // if (ne.Spawn(curMapData) < 0) { + // gEntList.CleanupDeleteList(); + // } + // continue; + // } + + // if (entity is Light light) { + // // We overflow the max edicts on large maps that have lots of entities. + // // Nodes & Lights remove themselves immediately on Spawn(), so dispatch their + // // spawn now, to free up the slot inside this loop. + // // NOTE: This solution prevents nodes & lights from being used inside point_templates. + // if (Util.DispatchSpawn(light) < 0) { + // gEntList.CleanupDeleteList(); + // } + // continue; + // } + + // Build a list of all point_template's so we can spawn them before everything else + if (entity is PointTemplate pt) + pointTemplates.Add(pt); + else { + // Queue up this entity for spawning + spawnList[numEntities].Entity = entity; + spawnList[numEntities].Depth = 0; + spawnList[numEntities].DeferredParentAttachment = null; + spawnList[numEntities].DeferredParent = null; + + pSpawnMapData[numEntities].MapData = curMapData.ToString(); + pSpawnMapData[numEntities].MapDataLength = (mapData.Length - curMapData.Length) + 2; + numEntities++; + } + } + +#if false // TODO + // Now loop through all our point_template entities and tell them to make templates of everything they're pointing to + int templates = pointTemplates.Count; + for (int i = 0; i < templates; i++) { + PointTemplate pointTemplate = pointTemplates[i]; + + // First, tell the Point template to Spawn + if (Util.DispatchSpawn(pointTemplate) < 0) { + Util.Remove(pointTemplate); + gEntList.CleanupDeleteList(); + continue; + } + + pointTemplate.StartBuildingTemplates(); + + // Now go through all it's templates and turn the entities into templates + int numTemplates = pointTemplate.GetNumTemplateEntities(); + for (int templateNm = 0; templateNm < numTemplates; templateNm++) { + // Find it in the spawn list + BaseEntity entity = pointTemplate.GetTemplateEntity(templateNm); + for (int iEntNum = 0; iEntNum < numEntities; iEntNum++) { + if (spawnList[iEntNum].Entity == entity) { + // Give the point_template the mapdata + pointTemplate.AddTemplate(entity, pSpawnMapData[iEntNum].MapData, pSpawnMapData[iEntNum].m_iMapDataLength); + + if (pointTemplate.ShouldRemoveTemplateEntities()) { + // Remove the template entity so that it does not show up in FindEntityXXX searches. + Util.Remove(entity); + gEntList.CleanupDeleteList(); + + // Remove the entity from the spawn list + spawnList[iEntNum].Entity = null; + } + break; + } + } + } + + pointTemplate.FinishBuildingTemplates(); + } +#endif + + SpawnHierarchicalList(numEntities, spawnList, activateEntities); + } + + static int ComputeSpawnHierarchyDepth_r(BaseEntity? entity) { + if (entity == null) + return 1; + + // if (entity.Parent == NULL_STRING) + // return 1; + + // BaseEntity parent = gEntList.FindEntityByName(null, ExtractParentName(entity.Parent)); + // if (parent == null) + return 1; + + // if (parent == entity) { + // Warning("LEVEL DESIGN ERROR: Entity %s is parented to itself!\n", entity.GetDebugName()); + // return 1; + // } + + // return 1 + ComputeSpawnHierarchyDepth_r(parent); + } + + static void ComputeSpawnHierarchyDepth(int entities, HierarchicalSpawn_t[] spawnList) { + for (int nEntity = 0; nEntity < entities; nEntity++) { + BaseEntity? entity = spawnList[nEntity].Entity; + if (entity != null /* && !entity.IsDormant() */)//todo + spawnList[nEntity].Depth = ComputeSpawnHierarchyDepth_r(entity); + else + spawnList[nEntity].Depth = 1; + } + } + + static void SpawnAllEntities(int numEntities, HierarchicalSpawn_t[] spawnList, bool activeEntities) { + int nEntity; + for (nEntity = 0; nEntity < numEntities; nEntity++) { + BaseEntity? entity = spawnList[nEntity].Entity; + + // TODO + // if (spawnList[nEntity].DeferredParent != null) { + // BaseEntity pParent = spawnList[nEntity].DeferredParent; + // int iAttachment = -1; + // BaseAnimating pAnim = pParent.GetBaseAnimating(); + // if (pAnim != null) { + // iAttachment = pAnim.LookupAttachment(spawnList[nEntity].DeferredParentAttachment); + // } + // entity.SetParent(pParent, iAttachment); + // } + + if (entity != null) { + if (Util.DispatchSpawn(entity) < 0) { + for (int i = nEntity + 1; i < numEntities; i++) { + // this is a child object that will be deleted now + if (spawnList[i].Entity != null && spawnList[i].Entity!.IsMarkedForDeletion()) { + spawnList[i].Entity = null; + } + } + // Spawn failed. + gEntList.CleanupDeleteList(); + // Remove the entity from the spawn list + spawnList[nEntity].Entity = null; + } + } + } + + if (activeEntities) { + // bool asyncAnims = mdlcache.SetAsyncLoad(MDLCACHE_ANIMBLOCK, false); + for (nEntity = 0; nEntity < numEntities; nEntity++) { + BaseEntity? entity = spawnList[nEntity].Entity; + // entity?.Activate(); todo + } + // mdlcache.SetAsyncLoad(MDLCACHE_ANIMBLOCK, asyncAnims); + } + } + + static void SpawnHierarchicalList(int entities, HierarchicalSpawn_t[] spawnList, bool activateEntities) { + // Compute the hierarchical depth of all entities hierarchically attached + ComputeSpawnHierarchyDepth(entities, spawnList); + + // Sort the entities (other than the world) by hierarchy depth, in order to spawn them in + // that order. This insures that each entity's parent spawns before it does so that + // it can properly set up anything that relies on hierarchy. + // SortSpawnListByHierarchy(entities, spawnList); TODO + + // save off entity positions if in edit mode + // if (engine.IsInEditMode()) // TODO + // RememberInitialEntityPositions(entities, spawnList); + + // Set up entity movement hierarchy in reverse hierarchy depth order. This allows each entity + // to use its parent's world spawn origin to calculate its local origin. + // SetupParentsForSpawnList(entities, spawnList); TODO + + // Spawn all the entities in hierarchy depth order so that parents spawn before their children. + SpawnAllEntities(entities, spawnList, activateEntities); + } + + static ReadOnlySpan ParseEntity(out BaseEntity? entity, ReadOnlySpan EntData, IMapEntityFilter filter) { + EntityMapData entData = new(EntData); + Span className = new char[EntityMapData.MAPKEY_MAXLENGTH]; + + if (!entData.ExtractValue("classname", className)) + Error("classname missing from entity!\n"); + + className = className.SliceNullTerminatedString(); + + entity = null; + if (filter == null || filter.ShouldCreateEntity(className)) { + + // Construct via the LINK_ENTITY_TO_CLASS factory. + if (filter != null) + entity = filter.CreateNextEntity(className); + else + entity = CreateEntityByName(className); + + // Set up keyvalues. + if (entity != null) { + // entity.ParseMapData(&entData); + } + else + Warning($"Can't init {className}\n"); + } + else { + // Just skip past all the keys. + Span keyName = new char[EntityMapData.MAPKEY_MAXLENGTH]; + Span value = new char[EntityMapData.MAPKEY_MAXLENGTH]; + if (entData.GetFirstKey(keyName, value)) { + do { + } + while (entData.GetNextKey(keyName, value)); + } + } + + // Return the current parser position in the data block + return entData.CurrentBufferPosition(); + } } diff --git a/Game.Server/Util.cs b/Game.Server/Util.cs index 30143767..2181c954 100644 --- a/Game.Server/Util.cs +++ b/Game.Server/Util.cs @@ -201,4 +201,43 @@ public static void ClientPrintAll(HudPrint dest, ReadOnlySpan msgName, Rea ReliableBroadcastRecipientFilter filter = new(); ClientPrintFilter(filter, dest, msgName, param1, param2, param3, param4); } + + public static int DispatchSpawn(BaseEntity entity) { + if (entity != null) { + // keep a smart pointer that will know if the object gets deleted + EHANDLE pEntSafe = new(); + pEntSafe.Set(entity); + + // TODO: GetBaseAnimating / SetBoneCacheFlags(BCF_IS_IN_SPAWN) + entity.Spawn(); + // TODO: ClearBoneCacheFlags(BCF_IS_IN_SPAWN) + + // Try to get the pointer again, in case the spawn function deleted the entity. + if (!pEntSafe.IsValid() || entity.IsMarkedForDeletion()) + return -1; + + // TODO + // if (entity.m_iGlobalname != NULL_STRING) { + // int globalIndex = GlobalEntity_GetIndex(entity.m_iGlobalname); + // if (globalIndex >= 0) { + // if (GlobalEntity_GetState(globalIndex) == GLOBAL_DEAD) { + // entity.Remove(); + // return -1; + // } else if (!FStrEq(STRING(gpGlobals.mapname), GlobalEntity_GetMap(globalIndex))) { + // entity.MakeDormant(); + // } + // } else { + // GlobalEntity_Add(entity.m_iGlobalname, gpGlobals.mapname, GLOBAL_ON); + // } + // } + + // TODO: gEntList.NotifySpawn(entity); + } + + return 0; + } + + public static void Remove(BaseEntity entity) { + throw new NotImplementedException(); + } } diff --git a/Game.Shared/MapEntitiesShared.cs b/Game.Shared/MapEntitiesShared.cs new file mode 100644 index 00000000..d46f5d71 --- /dev/null +++ b/Game.Shared/MapEntitiesShared.cs @@ -0,0 +1,278 @@ +namespace Game.Shared; + +public class EntityMapData // fixme, why so string heavy +{ + public const int MAPKEY_MAXLENGTH = 2048; + string EntData; + int EntDataSize; + string? CurrentKey; + + public EntityMapData(ReadOnlySpan entBlock, int entBlockSize = -1) { + EntData = entBlock.ToString(); + EntDataSize = entBlockSize; + } + + public bool ExtractValue(ReadOnlySpan keyName, Span value) => MapEntity.ExtractValue(EntData, keyName, value); + + public bool GetFirstKey(ReadOnlySpan keyName, ReadOnlySpan value) { + CurrentKey = EntData; + return GetNextKey(keyName, value); + } + + public ReadOnlySpan CurrentBufferPosition() => CurrentKey; + + public bool GetNextKey(ReadOnlySpan keyName, ReadOnlySpan value) { + Span token = stackalloc char[MAPKEY_MAXLENGTH]; + + string prevKey = CurrentKey; + CurrentKey = MapEntity.ParseToken(CurrentKey, token); + if (token.Length > 0 && token[0] == '}') { + CurrentKey = prevKey; + return false; + } + + if (string.IsNullOrEmpty(CurrentKey)) { + Warning("EntityMapData::GetNextKey: EOF without closing brace\n"); + Assert(false); + return false; + } + + keyName.CopyTo(token); + + int n = keyName.Length - 1; + while (n >= 0 && keyName[n] == ' ') + n--; + + if (n >= 0) + keyName = keyName[..(n + 1)]; + + CurrentKey = MapEntity.ParseToken(CurrentKey, token); + if (string.IsNullOrEmpty(CurrentKey)) { + Warning("EntityMapData::GetNextKey: EOF without closing brace\n"); + Assert(false); + return false; + } + + if (token.Length > 0 && token[0] == '}') { + Warning("EntityMapData::GetNextKey: closing brace without data\n"); + Assert(false); + return false; + } + + value.CopyTo(token); + + return true; + } + + bool SetValue(ReadOnlySpan keyName, ReadOnlySpan NewValue, int nKeyInstance) { + if (EntDataSize == -1) { + Assert(false); + return false; + } + + Span token = stackalloc char[MAPKEY_MAXLENGTH]; + string? inputData = EntData; + string? prevData; + + char[] newvaluebuf = new char[1024]; + int nCurrKeyInstance = 0; + + while (!string.IsNullOrEmpty(inputData)) { + inputData = MapEntity.ParseToken(inputData, token); + if (token.Length > 0 && token[0] == '}') + break; + + if (token.SequenceEqual(keyName)) { + nCurrKeyInstance++; + if (nCurrKeyInstance > nKeyInstance) { + int entLen = EntData.Length; + char[] postData = new char[entLen]; + prevData = inputData; + inputData = MapEntity.ParseToken(inputData, token); + token.CopyTo(postData); + + if (NewValue.Length > 0 && NewValue[0] != '\"') + newvaluebuf = $"\"{NewValue}\"".ToCharArray(); + else + NewValue.CopyTo(newvaluebuf); + + int iNewValueLen = newvaluebuf.Length; + int iPadding = iNewValueLen - token.Length - 2; + + Array.Copy(newvaluebuf, 0, prevData.ToCharArray(), 1, iNewValueLen + 1); + Array.Copy(postData, 0, prevData.ToCharArray(), 1 + iNewValueLen, entLen - ((prevData.Length - inputData.Length) + 1)); + + CurrentKey = CurrentKey[(iPadding)..]; + return true; + } + } + + inputData = MapEntity.ParseToken(inputData, token); + } + + return false; + } +} + +public static class MapEntity +{ + public static ReadOnlySpan SkipToNextEntity(ReadOnlySpan mapData, Span workBuffer) { + if (mapData.IsEmpty) + return null; + + int openBraceCount = 1; + while (!mapData.IsEmpty) { + mapData = ParseToken(mapData, workBuffer); + + if (workBuffer.Length > 0 && workBuffer[0] == '{') + openBraceCount++; + else if (workBuffer.Length > 0 && workBuffer[0] == '}') { + openBraceCount--; + if (openBraceCount == 0) + return mapData; + } + } + + return null; + } + + static readonly char[] s_BraceChars = "{}()\'".ToCharArray(); + static readonly bool[] s_BraceCharacters = new bool[256]; + static bool s_BuildReverseMap = true; + + public static string? ParseToken(ReadOnlySpan data, Span newToken) { + int len = 0; + newToken[0] = '\0'; + + if (data == default || data.IsEmpty) + return null; + + if (s_BuildReverseMap) { + s_BuildReverseMap = false; + Array.Clear(s_BraceCharacters, 0, s_BraceCharacters.Length); + foreach (var chh in s_BraceChars) + s_BraceCharacters[(byte)chh] = true; + } + + skipwhite: + while (true) { + if (data.IsEmpty) + return null; + + int c = data[0]; + if (c > ' ') + break; + + if (c == 0) + return null; + + data = data[1..]; + } + + int ch = data[0]; + + if (ch == '/' && data.Length > 1 && data[1] == '/') { + while (!data.IsEmpty && data[0] != '\n') + data = data[1..]; + goto skipwhite; + } + + if (ch == '"') { + data = data[1..]; + + while (len < EntityMapData.MAPKEY_MAXLENGTH) { + if (data.IsEmpty) + break; + + ch = data[0]; + data = data[1..]; + + if (ch == '"' || ch == 0) { + newToken[len] = '\0'; + return data.ToString(); + } + + newToken[len++] = (char)ch; + } + + if (len >= EntityMapData.MAPKEY_MAXLENGTH) { + len--; + newToken[len] = '\0'; + } + + newToken[len] = '\0'; + return data.ToString(); + } + + if (ch < 256 && s_BraceCharacters[ch]) { + newToken[len++] = (char)ch; + newToken[len] = '\0'; + return data[1..].ToString(); + } + + do { + newToken[len++] = (char)ch; + data = data[1..]; + + if (data.IsEmpty) + break; + + ch = data[0]; + + if (ch < 256 && s_BraceCharacters[ch]) + break; + + if (len >= EntityMapData.MAPKEY_MAXLENGTH) { + len--; + newToken[len] = '\0'; + } + + } while (ch > 32); + + newToken[len] = '\0'; + return data.ToString(); + } + + public static bool ExtractValue(ReadOnlySpan entData, ReadOnlySpan keyName, Span value) { + Span token = stackalloc char[EntityMapData.MAPKEY_MAXLENGTH]; + ReadOnlySpan inputData = entData; + + while (!inputData.IsEmpty) { + var remainder = ParseToken(inputData, token); + if (remainder == null) + break; + + inputData = remainder.AsSpan(); + + if (token[0] == '}') + break; + + if (SequenceEquals(token, keyName)) { + remainder = ParseToken(inputData, token); + if (remainder == null) + return false; + + inputData = remainder.AsSpan(); + int tokenLen = token.IndexOf('\0'); + if (tokenLen < 0) tokenLen = token.Length; + value.Clear(); + token[..tokenLen].CopyTo(value); + return true; + } + + remainder = ParseToken(inputData, token); + if (remainder == null) + break; + + inputData = remainder.AsSpan(); + } + + return false; + } + + static bool SequenceEquals(Span token, ReadOnlySpan key) { + int len = token.IndexOf('\0'); + if (len < 0) len = token.Length; + return token[..len].SequenceEqual(key); + } +} \ No newline at end of file diff --git a/Source.Engine/CollisionModelSubsystem.cs b/Source.Engine/CollisionModelSubsystem.cs index 8d639bc2..e4910f6a 100644 --- a/Source.Engine/CollisionModelSubsystem.cs +++ b/Source.Engine/CollisionModelSubsystem.cs @@ -28,6 +28,7 @@ public class CollisionBSPData public readonly List MapLeafs = []; public readonly List MapLeafBrushes = []; public readonly List TextureNames = []; + public string? MapEntityString; IMaterialSystem? materials; @@ -162,9 +163,9 @@ private void CollisionBSPData_LoadLeafs_Version_1(MapLoadHelper lh) { } - if (mapLeafs[0].Contents != Contents.Solid) + if (mapLeafs[0].Contents != Contents.Solid) Sys.Error("Map leaf 0 is not Contents.Solid"); - + SolidLeaf = 0; EmptyLeaf = NumLeafs; @@ -245,7 +246,9 @@ internal void LoadVisibility() { } internal void LoadEntityString() { - + MapLoadHelper lh = new MapLoadHelper(LumpIndex.Entities); + byte[] inData = lh.LoadLumpData(throwIfNoElements: true, sysErrorIfOOB: true); + MapEntityString = Encoding.ASCII.GetString(inData); } internal void LoadPhysics() { diff --git a/Source.Engine/Host.cs b/Source.Engine/Host.cs index 41741318..48ea9ed6 100644 --- a/Source.Engine/Host.cs +++ b/Source.Engine/Host.cs @@ -980,6 +980,8 @@ public bool NewGame(ReadOnlySpan mapName, bool loadGame, bool backgroundLe EngineVGui.UpdateProgressBar(LevelLoadingProgress.LevelInit); #endif + serverPluginHandler.LevelInit(mapName, GetCollisionBSPData().MapEntityString, oldMap, landmark, loadGame && !oldSave, backgroundLevel); + if (loadGame && !oldSave) { sv.SetPaused(true); sv.LoadGame = true; diff --git a/Source.Engine/ServerPlugin.cs b/Source.Engine/ServerPlugin.cs index 5ca6509b..2d0b50a1 100644 --- a/Source.Engine/ServerPlugin.cs +++ b/Source.Engine/ServerPlugin.cs @@ -47,8 +47,10 @@ void PrintDetails() { throw new NotImplementedException(); } - void LevelInit(ReadOnlySpan mapName, ReadOnlySpan maxEntities, ReadOnlySpan oldLevel, ReadOnlySpan landmarkName, bool loadGame, bool background) { - throw new NotImplementedException(); + public void LevelInit(ReadOnlySpan mapName, ReadOnlySpan mapEntities, ReadOnlySpan oldLevel, ReadOnlySpan landmarkName, bool loadGame, bool background) { + + serverGameDLL.LevelInit(mapName, mapEntities, oldLevel, landmarkName, loadGame, background); + } void ServerActivate(Edict[] edictList, int edictCount, int clientMax) { From a0a7ede90c09d705b0e300048e69218ec317df7d Mon Sep 17 00:00:00 2001 From: callumok2004 Date: Sun, 1 Mar 2026 17:34:44 +0000 Subject: [PATCH 10/31] Revert CLC_GMod_ClientToServer back to how it was --- Game.Server/BaseEntity.cs | 2 +- Game.Server/World.cs | 11 ++++++----- Source.Common/Networking/NetMessages.cs | 13 ++++++------- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Game.Server/BaseEntity.cs b/Game.Server/BaseEntity.cs index 929a1587..9246febd 100644 --- a/Game.Server/BaseEntity.cs +++ b/Game.Server/BaseEntity.cs @@ -417,7 +417,7 @@ public ReadOnlySpan GetModelName() { public ref readonly BaseHandle GetRefEHandle() => ref RefEHandle; public void SetModelIndex(int index) { - throw new NotImplementedException(); + // throw new NotImplementedException(); } BaseHandle RefEHandle; diff --git a/Game.Server/World.cs b/Game.Server/World.cs index 0ce17d1a..ef857638 100644 --- a/Game.Server/World.cs +++ b/Game.Server/World.cs @@ -11,7 +11,8 @@ namespace Game.Server; using FIELD = Source.FIELD; -public static class WorldGlobals { +public static class WorldGlobals +{ public static bool g_fGameOver = false; public static World? GetWorldEntity() => World.g_WorldEntity; } @@ -49,11 +50,11 @@ public override void Precache() { g_fGameOver = false; Assert(g_pGameRules == null); - InstallGameRules(); - Assert(g_pGameRules != null); - g_pGameRules.Init(); + // InstallGameRules(); // fixme + // Assert(g_pGameRules != null); + // g_pGameRules.Init(); - IGameSystem.LevelInitPreEntityAllSystems(GetModelName()); + // IGameSystem.LevelInitPreEntityAllSystems(GetModelName()); } public override void Spawn() { diff --git a/Source.Common/Networking/NetMessages.cs b/Source.Common/Networking/NetMessages.cs index 192d31a9..6b7f8e13 100644 --- a/Source.Common/Networking/NetMessages.cs +++ b/Source.Common/Networking/NetMessages.cs @@ -919,20 +919,19 @@ public CLC_GMod_ClientToServer() : base(CLC.GMod_ClientToServer) { } public readonly bf_read DataIn = new(); public override bool ReadFromBuffer(bf_read buffer) { - Length = (int)buffer.ReadUBitLong(20); - int type = buffer.ReadByte(); - buffer.CopyTo(DataIn); - return buffer.SeekRelative(Length); + buffer.ReadUBitLong(20); + buffer.ReadByte(); + buffer.ReadUBitLong(16); + return true; } public override bool WriteToBuffer(bf_write buffer) { buffer.WriteNetMessageType(this); - buffer.WriteUBitLong(16, 20); - + buffer.WriteUBitLong(24, 20); buffer.WriteByte(4); buffer.WriteUBitLong(0, 16); - return true; + return base.WriteToBuffer(buffer); } } From 8ede6c6057691d538c450b04cee554de66f0a8c1 Mon Sep 17 00:00:00 2001 From: callumok2004 Date: Sun, 1 Mar 2026 17:58:16 +0000 Subject: [PATCH 11/31] Some HUD fixes --- Game.Client/C_BasePlayer.cs | 2 +- Game.Client/HL2/HudDamageIndicator.cs | 2 ++ Game.Client/HUD/HudAnimationInfo.cs | 5 ++++- Game.Client/HistoryResource.cs | 3 ++- Game.Client/WeaponsResource.cs | 2 +- 5 files changed, 10 insertions(+), 4 deletions(-) diff --git a/Game.Client/C_BasePlayer.cs b/Game.Client/C_BasePlayer.cs index 3ea74f3b..ec3a2100 100644 --- a/Game.Client/C_BasePlayer.cs +++ b/Game.Client/C_BasePlayer.cs @@ -179,7 +179,7 @@ public override void OnDataChanged(DataUpdateType updateType) { if (pWeaponData == null || (pWeaponData.Flags & WeaponFlags.NoAmmoPickups) == 0) { // We got more ammo for this ammo index. Add it to the ammo history - HudHistoryResource? pHudHR = GET_HUDELEMENT(); + HudHistoryResource? pHudHR = gHUD.FindElement("CHudHistoryResource") as HudHistoryResource; pHudHR?.AddToHistory(HRType.Ammo, i, Math.Abs(GetAmmoCount(i) - OldAmmo[i])); } } diff --git a/Game.Client/HL2/HudDamageIndicator.cs b/Game.Client/HL2/HudDamageIndicator.cs index 8f80f345..9209f8a3 100644 --- a/Game.Client/HL2/HudDamageIndicator.cs +++ b/Game.Client/HL2/HudDamageIndicator.cs @@ -10,6 +10,8 @@ using System.Numerics; +namespace Game.Client.HL2; + [DeclareHudElement(Name = "CHudDamageIndicator")] class HudDamageIndicator : EditableHudElement, IHudElement { diff --git a/Game.Client/HUD/HudAnimationInfo.cs b/Game.Client/HUD/HudAnimationInfo.cs index 7b52340e..61c6d41f 100644 --- a/Game.Client/HUD/HudAnimationInfo.cs +++ b/Game.Client/HUD/HudAnimationInfo.cs @@ -6,6 +6,8 @@ using Source.Common.GUI; using Source.GUI.Controls; +namespace Game.Client; + [DeclareHudElement(Name = "CHudAnimationInfo")] class HudAnimationInfo : EditableHudElement, IHudElement { @@ -20,6 +22,7 @@ class HudAnimationInfo : EditableHudElement, IHudElement Panel? Watch; public HudAnimationInfo(string panelName) : base(null, "HudAnimationInfo") { + ElementName = panelName; ANIM_INFO_WIDTH = 300 * (ScreenWidth() / 640); Panel parent = clientMode.GetViewport(); @@ -161,7 +164,7 @@ public override void Paint() { [ConCommand("cl_animationinfo", "Hud element to examine.", FCvar.None)] static void func(in TokenizedCommand args) { - if (gHUD.FindElement("HudAnimationInfo") is not HudAnimationInfo info) + if (gHUD.FindElement("CHudAnimationInfo") is not HudAnimationInfo info) return; if (args.ArgC() != 2) { diff --git a/Game.Client/HistoryResource.cs b/Game.Client/HistoryResource.cs index 3c4667f6..c893307d 100644 --- a/Game.Client/HistoryResource.cs +++ b/Game.Client/HistoryResource.cs @@ -1,4 +1,3 @@ -using Game.Client; using Game.Client.HUD; using Game.Shared; @@ -7,6 +6,8 @@ using Source.Common.GUI; using Source.GUI.Controls; +namespace Game.Client; + enum HRType { Empty, diff --git a/Game.Client/WeaponsResource.cs b/Game.Client/WeaponsResource.cs index e9f611fb..3db6ed9e 100644 --- a/Game.Client/WeaponsResource.cs +++ b/Game.Client/WeaponsResource.cs @@ -91,7 +91,7 @@ internal void LoadWeaponSprites(WEAPON_FILE_INFO_HANDLE weaponFileInfo) { weaponInfo.IconZoomedAutoaim = weaponInfo.IconZoomedCrosshair; //default to zoomed crosshair } - HudHistoryResource? hudHR = GET_HUDELEMENT(); + HudHistoryResource? hudHR = gHUD.FindElement("CHudHistoryResource") as HudHistoryResource; if (hudHR != null) { p = FindHudTextureInDict(tempList, "weapon"); if (p != null) { From 35e4b2446c062172092cc056bbfc91f205c2edd3 Mon Sep 17 00:00:00 2001 From: callumok2004 Date: Mon, 2 Mar 2026 21:57:08 +0000 Subject: [PATCH 12/31] Fix remaining issues with console completions --- Source.GUI.Controls/ConsoleDialog.cs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/Source.GUI.Controls/ConsoleDialog.cs b/Source.GUI.Controls/ConsoleDialog.cs index 68fb9c83..513d40f8 100644 --- a/Source.GUI.Controls/ConsoleDialog.cs +++ b/Source.GUI.Controls/ConsoleDialog.cs @@ -140,7 +140,7 @@ public override void OnTextChanged(Panel panel) { bool ctrlKeyDown = Input.IsKeyDown(ButtonCode.KeyLControl) || Input.IsKeyDown(ButtonCode.KeyRControl); if (len > 0 && hitTilde) { - PreviousPartialText[len - 1] = '\0'; + PartialText[len - 1] = '\0'; if (!altKeyDown && !ctrlKeyDown) { Entry.SetText(""); @@ -150,6 +150,7 @@ public override void OnTextChanged(Panel panel) { else { Entry.SetText(PartialText); } + return; } AutoCompleteMode = false; @@ -268,7 +269,7 @@ private void AddToHistory(ReadOnlySpan commandText, ReadOnlySpan ext item.SetText(command.ToString(), extra.IsEmpty ? null : extra.ToString()); NextCompletion = 0; - RebuildCompletionList(command); + RebuildCompletionList(PartialText); } private void ClearCompletionList() { @@ -298,7 +299,7 @@ private void RebuildCompletionList(ReadOnlySpan text) { HistoryItem item = CommandHistory[i]; CompletionItem comp = new(); CompletionItems.Add(comp); - comp.IsCommand = true; + comp.IsCommand = false; comp.Command = null; comp.Text = new HistoryItem(item); } @@ -375,11 +376,11 @@ private void RebuildCompletionList(ReadOnlySpan text) { if (CompletionItems.Count >= 2) { for (int i = 0; i < CompletionItems.Count; i++) { - for (int j = 0; j < CompletionItems.Count; j++) { + for (int j = i + 1; j < CompletionItems.Count; j++) { CompletionItem item1 = CompletionItems[i]; CompletionItem item2 = CompletionItems[j]; - if (item1.GetName().CompareTo(item2.GetName(), StringComparison.Ordinal) > 0) { + if (item1.GetName().CompareTo(item2.GetName(), StringComparison.OrdinalIgnoreCase) > 0) { CompletionItem temp = CompletionItems[i]; CompletionItems[i] = CompletionItems[j]; CompletionItems[j] = temp; @@ -475,7 +476,7 @@ private void OnAutoComplete(bool reverse) { CompletionItem item = CompletionItems[NextCompletion]; Assert(item != default); - if (item.IsCommand && item.Command != null) { + if (!item.IsCommand && item.Command != null) { ReadOnlySpan cmd = item.GetCommand(); strcpy(CompletedText, cmd); } @@ -484,6 +485,9 @@ private void OnAutoComplete(bool reverse) { strcpy(CompletedText, txt); } + if (!CompletedText.SliceNullTerminatedString().Contains(' ')) + strcat(CompletedText, " "); + Entry.SetText(CompletedText.SliceNullTerminatedString()); Entry.GotoTextEnd(); Entry.SelectNone(); From 89b5391f3573d56c44720e5888176a2b23e9444d Mon Sep 17 00:00:00 2001 From: callumok2004 Date: Tue, 3 Mar 2026 11:00:11 +0000 Subject: [PATCH 13/31] EntityList impl --- Game.Server/EntityList.cs | 161 ++++++++++++++++++++++++++++--- Game.Server/MapEntities.cs | 7 ++ Game.Shared/BaseEntityList.cs | 176 +++++++++++++++++++++++++++++++--- Source.Common/Exts.cs | 2 + Source.Engine/PrEdict.cs | 2 +- 5 files changed, 323 insertions(+), 25 deletions(-) diff --git a/Game.Server/EntityList.cs b/Game.Server/EntityList.cs index 8e48a284..fb2e3707 100644 --- a/Game.Server/EntityList.cs +++ b/Game.Server/EntityList.cs @@ -102,19 +102,33 @@ public void ReportEntityList() { public class GlobalEntityList : BaseEntityList { - public int HighestEnt; // the topmost used array index - public int NumEnts; - public int NumEdicts; + int HighestEnt; + int NumEnts; + int NumEdicts; - public bool ClearingEntities; - public readonly List EntityListeners = []; + bool ClearingEntities; + readonly List EntityListeners = []; + + public GlobalEntityList() { + HighestEnt = NumEnts = NumEdicts = 0; + ClearingEntities = false; + } + + public IServerNetworkable? GetServerNetworkable(BaseHandle hEnt) { + IServerUnknown? unk = (IServerUnknown?)LookupEntity(hEnt); + return unk?.GetNetworkable(); + } public BaseEntity? GetBaseEntity(BaseHandle ent) { IServerUnknown? unk = (IServerUnknown?)LookupEntity(ent); return unk == null ? null : (BaseEntity?)unk.GetBaseEntity(); } - public BaseEntity FirstEnt() => NextEnt(null); + public int NumberOfEntities() => NumEnts; + public int NumberOfEdicts() => NumEdicts; + public bool IsClearingEntities() => ClearingEntities; + + public BaseEntity? FirstEnt() => NextEnt(null); public BaseEntity? NextEnt(BaseEntity? currentEnt) { if (currentEnt == null) { @@ -137,23 +151,146 @@ public class GlobalEntityList : BaseEntityList return null; } + public void AddListenerEntity(IEntityListener listener) { + if (EntityListeners.Contains(listener)) { + Assert(false, "Can't add listeners multiple times\n"); + return; + } + EntityListeners.Add(listener); + } + + public void RemoveListenerEntity(IEntityListener listener) => EntityListeners.Remove(listener); + public void CleanupDeleteList() { // todo } - public BaseEntity? FindEntityByName(BaseEntity startEntity, ReadOnlySpan name, BaseEntity? searchingEntity, BaseEntity? activator, BaseEntity? caller, int/*IEntityFindFilter*/? filter) { + public BaseEntity? FindEntityByClassname(BaseEntity? startEntity, ReadOnlySpan className) { + EntInfo? info = startEntity != null ? GetEntInfoPtr(startEntity.GetRefEHandle()).Next : FirstEntInfo(); + + for (; info != null; info = info.Next) { + BaseEntity? ent = (BaseEntity?)info.Entity; + if (ent == null) { + DevWarning("NULL entity in global entity list!\n"); + continue; + } + + if (ent.ClassMatches(className)) + return ent; + } + + return null; + } + + public BaseEntity? FindEntityByName(BaseEntity? startEntity, ReadOnlySpan name, BaseEntity? searchingEntity = null, BaseEntity? activator = null, BaseEntity? caller = null, int/*IEntityFindFilter*/? filter = null) { if (name.IsEmpty) return null; - if (name[0] == '!') { // todo - - // if (startEntity == null) - // return FindEntityProcedural(name, searchingEntity, activator, caller); + if (name[0] == '!') { + if (startEntity == null) + return FindEntityProcedural(name, searchingEntity, activator, caller); return null; } - return null; // TODO + EntInfo? info = startEntity != null ? GetEntInfoPtr(startEntity.GetRefEHandle()).Next : FirstEntInfo(); + + for (; info != null; info = info.Next) { + BaseEntity? ent = (BaseEntity?)info.Entity; + if (ent == null) { + DevWarning("NULL entity in global entity list!\n"); + continue; + } + + if (ent.Name == null) + continue; + + if (ent.NameMatches(name)) { + // if (filter != null && !filter.ShouldFindEntity(ent)) + // continue; + + return ent; + } + } + + return null; + } + + public BaseEntity? FindEntityProcedural(ReadOnlySpan name, BaseEntity? searchingEntity = null, BaseEntity? activator = null, BaseEntity? caller = null) { + if (name[0] == '!') { + ReadOnlySpan pName = name[1..]; + + if (pName.SequenceEqual("player")) + return (BaseEntity?)Util.PlayerByIndex(1); + else if (pName.SequenceEqual("activator")) + return activator; + else if (pName.SequenceEqual("caller")) + return caller; + else if (pName.SequenceEqual("self")) + return searchingEntity; + else { + Warning($"Invalid entity search name {name}\n"); + Assert(false); + } + } + + return null; + } + + public BaseEntity? FindEntityGeneric(BaseEntity? startEntity, ReadOnlySpan name, BaseEntity? searchingEntity = null, BaseEntity? activator = null, BaseEntity? caller = null) { + BaseEntity? entity = FindEntityByName(startEntity, name, searchingEntity, activator, caller); + entity ??= FindEntityByClassname(startEntity, name); + + return entity; + } + + public void NotifyCreateEntity(BaseEntity? ent) { + if (ent == null) + return; + + for (int i = EntityListeners.Count - 1; i >= 0; i--) + EntityListeners[i].OnEntityCreated(ent); + } + + public void NotifySpawn(BaseEntity? ent) { + if (ent == null) + return; + + for (int i = EntityListeners.Count - 1; i >= 0; i--) + EntityListeners[i].OnEntitySpawned(ent); + } + + public void NotifyRemoveEntity(BaseHandle hEnt) { + BaseEntity? ent = GetBaseEntity(hEnt); + if (ent == null) + return; + + for (int i = EntityListeners.Count - 1; i >= 0; i--) + EntityListeners[i].OnEntityDeleted(ent); + } + + protected override void OnAddEntity(IHandleEntity? pEnt, BaseHandle handle) { + int i = handle.GetEntryIndex(); + + NumEnts++; + if (i > HighestEnt) + HighestEnt = i; + + BaseEntity? ent = (BaseEntity?)((IServerUnknown?)pEnt)?.GetBaseEntity(); + if (ent?.Edict() != null) + NumEdicts++; + + Assert(ent != null); + for (i = EntityListeners.Count - 1; i >= 0; i--) + EntityListeners[i].OnEntityCreated(ent!); + } + + protected override void OnRemoveEntity(IHandleEntity? pEnt, BaseHandle handle) { + BaseEntity? ent = (BaseEntity?)((IServerUnknown?)pEnt)?.GetBaseEntity(); + if (ent?.Edict() != null) + NumEdicts--; + + NumEnts--; } } diff --git a/Game.Server/MapEntities.cs b/Game.Server/MapEntities.cs index f94b22ae..9446f1d4 100644 --- a/Game.Server/MapEntities.cs +++ b/Game.Server/MapEntities.cs @@ -312,6 +312,13 @@ static ReadOnlySpan ParseEntity(out BaseEntity? entity, ReadOnlySpan } else Warning($"Can't init {className}\n"); + +#if true // TODO: remove this once ParseMapData is implemented. + Span keyName = new char[EntityMapData.MAPKEY_MAXLENGTH]; + Span value = new char[EntityMapData.MAPKEY_MAXLENGTH]; + if (entData.GetFirstKey(keyName, value)) + do { } while (entData.GetNextKey(keyName, value)); +#endif } else { // Just skip past all the keys. diff --git a/Game.Shared/BaseEntityList.cs b/Game.Shared/BaseEntityList.cs index c5aaf9f2..095127c6 100644 --- a/Game.Shared/BaseEntityList.cs +++ b/Game.Shared/BaseEntityList.cs @@ -9,16 +9,102 @@ public class EntInfo public int SerialNumber; public EntInfo? Prev; public EntInfo? Next; + + public void ClearLinks() => Prev = Next = this; } -public class EntInfoList : LinkedList; +public class EntInfoList +{ + public EntInfo? First; + public EntInfo? Last; + + public void AddToHead(EntInfo element) => LinkAfter(null, element); + public void AddToTail(EntInfo element) => LinkBefore(null, element); + + public void LinkBefore(EntInfo? before, EntInfo element) { + Assert(element != null); + + Unlink(element); + + element.Next = before; + + if (before == null) { + element.Prev = Last; + Last = element; + } + else { + Assert(IsInList(before)); + element.Prev = before.Prev; + before.Prev = element; + } + + if (element.Prev == null) + First = element; + else + element.Prev.Next = element; + } + + public void LinkAfter(EntInfo? after, EntInfo element) { + Assert(element != null); + + if (IsInList(element)) + Unlink(element); + + element.Prev = after; + if (after == null) { + element.Next = First; + First = element; + } + else { + Assert(IsInList(after)); + element.Next = after.Next; + after.Next = element; + } + + if (element.Next == null) + Last = element; + else + element.Next.Prev = element; + } + + public void Unlink(EntInfo element) { + if (IsInList(element)) { + if (element.Prev != null) + element.Prev.Next = element.Next; + else + First = element.Next; + + if (element.Next != null) + element.Next.Prev = element.Prev; + else + Last = element.Prev; + + element.ClearLinks(); + } + } + + public bool IsInList(EntInfo element) => element.Prev != element; +} public class BaseEntityList { + const int SERIAL_MASK = 0x7fff; + public BaseEntityList() { ((Span)EntPtrArray).ClearInstantiatedReferences(); + + for (int i = 0; i < Constants.NUM_ENT_ENTRIES; i++) { + EntPtrArray[i].ClearLinks(); + EntPtrArray[i].SerialNumber = Random.Shared.Next() & SERIAL_MASK; + EntPtrArray[i].Entity = null; + } + + for (int i = Constants.MAX_EDICTS + 1; i < Constants.NUM_ENT_ENTRIES; i++) + FreeNonNetworkableList.AddToTail(EntPtrArray[i]); } + public BaseHandle AddNetworkableEntity(IHandleEntity ent, int index, int forcedSerialNum = -1) { + Assert(index >= 0 && index < Constants.MAX_EDICTS); return AddEntityAtSlot(ent, index, forcedSerialNum); } @@ -27,23 +113,57 @@ private BaseHandle AddEntityAtSlot(IHandleEntity ent, int slot, int forcedSerial Assert(entSlot.Entity == null); entSlot.Entity = ent; - if (forcedSerialNum != -1) + if (forcedSerialNum != -1) { entSlot.SerialNumber = forcedSerialNum; +#if !CLIENT_DLL + Assert(false); +#endif + } + + ActiveList.AddToTail(entSlot); - ActiveList.AddLast(entSlot); BaseHandle ret = new(slot, entSlot.SerialNumber); ent.SetRefEHandle(ret); - OnAddEntity(ent, ret); + OnAddEntity(ent, ret); return ret; } - public BaseHandle AddNonNetworkableEntity(IHandleEntity pEnt) { - throw new NotImplementedException(); + public BaseHandle AddNonNetworkableEntity(IHandleEntity ent) { + EntInfo? slot = FreeNonNetworkableList.First; + if (slot == null) { + Warning("BaseEntityList.AddNonNetworkableEntity: no free slots!\n"); + Assert(false, "BaseEntityList.AddNonNetworkableEntity: no free slots!\n"); + return new(); + } + + FreeNonNetworkableList.Unlink(slot); + int iSlot = GetEntInfoIndex(slot); + + return AddEntityAtSlot(ent, iSlot, -1); } - public void RemoveHandle(BaseHandle handle) { - throw new NotImplementedException(); + + public void RemoveEntity(BaseHandle handle) => RemoveEntityAtSlot(handle.GetEntryIndex()); + + void RemoveEntityAtSlot(int slot) { + Assert(slot >= 0 && slot < Constants.NUM_ENT_ENTRIES); + + EntInfo info = EntPtrArray[slot]; + + if (info.Entity != null) { + info.Entity.SetRefEHandle(new BaseHandle(Constants.INVALID_EHANDLE_INDEX)); + + OnRemoveEntity(info.Entity, new BaseHandle(slot, info.SerialNumber)); + + info.Entity = null; + info.SerialNumber = (info.SerialNumber + 1) & SERIAL_MASK; + + ActiveList.Unlink(info); + + if (slot >= Constants.MAX_EDICTS) + FreeNonNetworkableList.AddToTail(info); + } } public IHandleEntity? LookupEntityByNetworkIndex(int edictIndex) { @@ -52,7 +172,6 @@ public void RemoveHandle(BaseHandle handle) { return EntPtrArray[edictIndex].Entity; } - public IHandleEntity? LookupEntity(in BaseHandle handle) { if (handle.Index == Constants.INVALID_EHANDLE_INDEX) return null; @@ -64,21 +183,54 @@ public void RemoveHandle(BaseHandle handle) { return null; } + public BaseHandle FirstHandle() { + if (ActiveList.First == null) + return new BaseHandle(Constants.INVALID_EHANDLE_INDEX); + + int index = GetEntInfoIndex(ActiveList.First); + return new BaseHandle(index, EntPtrArray[index].SerialNumber); + } + + public BaseHandle NextHandle(BaseHandle ent) { + int slot = ent.GetEntryIndex(); + EntInfo? next = EntPtrArray[slot].Next; + if (next == null) + return new(Constants.INVALID_EHANDLE_INDEX); + + int index = GetEntInfoIndex(next); + return new(index, EntPtrArray[index].SerialNumber); + } + + public static BaseHandle InvalidHandle() => new(Constants.INVALID_EHANDLE_INDEX); + // These are notifications to the derived class. It can cache info here if it wants. protected virtual void OnAddEntity(IHandleEntity? pEnt, BaseHandle handle) { } // It is safe to delete the entity here. We won't be accessing the pointer after // calling OnRemoveEntity. protected virtual void OnRemoveEntity(IHandleEntity? pEnt, BaseHandle handle) { } + int GetEntInfoIndex(EntInfo entInfo) { + Assert(entInfo != null); + Span span = EntPtrArray; + for (int i = 0; i < span.Length; i++) { + if (span[i] == entInfo) + return i; + } + Assert(false, "EntInfo not found in EntPtrArray"); + return -1; + } + InlineArrayNumEntEntries EntPtrArray; - public EntInfoList ActiveList = []; - EntInfoList FreeNonNetworkableList = []; + public EntInfoList ActiveList = new(); + EntInfoList FreeNonNetworkableList = new(); - public EntInfo? FirstEntInfo() => ActiveList.First?.Value; + public EntInfo? FirstEntInfo() => ActiveList.First; public EntInfo? NextEntInfo(EntInfo? current) => current?.Next; public EntInfo GetEntInfoPtr(BaseHandle ent) { int slot = ent.GetEntryIndex(); return EntPtrArray[slot]; } + + public EntInfo GetEntInfoPtrByIndex(int index) => EntPtrArray[index]; } diff --git a/Source.Common/Exts.cs b/Source.Common/Exts.cs index b0d4c4f5..5aee4450 100644 --- a/Source.Common/Exts.cs +++ b/Source.Common/Exts.cs @@ -80,6 +80,8 @@ public static void ClearAll(this Span bytes) { public static int FindNextSetBit(this Span bytes, int startBit) { while ((startBit >> 3) < bytes.Length && !IsBitSet(bytes, startBit)) startBit++; + if ((startBit >> 3) >= bytes.Length) + return -1; return startBit; } } diff --git a/Source.Engine/PrEdict.cs b/Source.Engine/PrEdict.cs index 04f8a802..cb6490fa 100644 --- a/Source.Engine/PrEdict.cs +++ b/Source.Engine/PrEdict.cs @@ -36,7 +36,7 @@ public class ED Edict edict; for (; ; ) { - bit = FreeEdicts.FindNextSetBit(bit + 1) - 1; // FIXME: This is returning 8192, so we must -1 otherwise we are 1 over the limit? + bit = FreeEdicts.FindNextSetBit(bit + 1); if (bit < 0) break; From 39eb3d76178489fdf1273c6ac1900838fe266cd3 Mon Sep 17 00:00:00 2001 From: callumok2004 Date: Tue, 3 Mar 2026 11:01:22 +0000 Subject: [PATCH 14/31] Init gamerules --- Game.Server/GarrysMod/GMODClient.cs | 4 +++- Game.Server/World.cs | 6 +++--- Game.Shared/GameRulesRegister.cs | 2 +- Game.Shared/GarrysMod/GMODGameRules.cs | 3 +++ 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/Game.Server/GarrysMod/GMODClient.cs b/Game.Server/GarrysMod/GMODClient.cs index 3b4d4792..56807919 100644 --- a/Game.Server/GarrysMod/GMODClient.cs +++ b/Game.Server/GarrysMod/GMODClient.cs @@ -69,7 +69,7 @@ public static ReadOnlySpan GetGameDescription() { public static BaseEntity? FindEntity(Edict edict, Span classname) { // If no name was given set bits based on the picked // if (FStrEq(classname, "")) - // todo return (FindPickerEntityClass((CBasePlayer)GetContainingEntity(edict)), classname)); + // todo return (FindPickerEntityClass((CBasePlayer)GetContainingEntity(edict)), classname)); return null; } @@ -117,6 +117,8 @@ public static void GameStartFrame() { //========================================================= // instantiate the proper game rules object //========================================================= + static readonly GameRulesRegister s_GMODRulesRegister = new("CGMODRules", () => CreateEntityByName("gmod_gamerules")); + public static void InstallGameRules() { // vanilla deathmatch GameRulesRegister.CreateGameRulesObject("CGMODRules"); diff --git a/Game.Server/World.cs b/Game.Server/World.cs index ef857638..b21d3340 100644 --- a/Game.Server/World.cs +++ b/Game.Server/World.cs @@ -50,9 +50,9 @@ public override void Precache() { g_fGameOver = false; Assert(g_pGameRules == null); - // InstallGameRules(); // fixme - // Assert(g_pGameRules != null); - // g_pGameRules.Init(); + InstallGameRules(); + Assert(g_pGameRules != null); + g_pGameRules.Init(); // IGameSystem.LevelInitPreEntityAllSystems(GetModelName()); } diff --git a/Game.Shared/GameRulesRegister.cs b/Game.Shared/GameRulesRegister.cs index b6049b74..a469cf27 100644 --- a/Game.Shared/GameRulesRegister.cs +++ b/Game.Shared/GameRulesRegister.cs @@ -67,7 +67,7 @@ void InstallStringTableCallback_GameRules() { #elif GAME_DLL static INetworkStringTable? g_StringTableGameRules = null; - void CreateNetworkStringTables_GameRules() { + public static void CreateNetworkStringTables_GameRules() { // Create the string tables g_StringTableGameRules = networkstringtable.CreateStringTable(GAMERULES_STRINGTABLE_NAME, 1); } diff --git a/Game.Shared/GarrysMod/GMODGameRules.cs b/Game.Shared/GarrysMod/GMODGameRules.cs index d325a2db..b00ab05e 100644 --- a/Game.Shared/GarrysMod/GMODGameRules.cs +++ b/Game.Shared/GarrysMod/GMODGameRules.cs @@ -15,6 +15,9 @@ namespace Game.Server.GarrysMod; using FIELD = Source.FIELD; using Game.Shared; +#if GAME_DLL +[LinkEntityToClass("gmod_gamerules")] +#endif public class #if CLIENT_DLL C_GMODGameRulesProxy From ec6e9f3f34a4958f3fd31d059065470abe05576b Mon Sep 17 00:00:00 2001 From: callumok2004 Date: Tue, 3 Mar 2026 11:01:36 +0000 Subject: [PATCH 15/31] Gamerules stringtable --- Game.Server/GameInterface.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Game.Server/GameInterface.cs b/Game.Server/GameInterface.cs index 17ddf96c..a3e00fc2 100644 --- a/Game.Server/GameInterface.cs +++ b/Game.Server/GameInterface.cs @@ -201,6 +201,8 @@ public void BuildAdjacentMapList() { public void CreateNetworkStringTables() { // throw new NotImplementedException(); + + GameRulesRegister.CreateNetworkStringTables_GameRules(); } public bool DLLInit(IServiceProvider services) { From 6333ab0e466f310b9c1a80d04d37cf572782498b Mon Sep 17 00:00:00 2001 From: callumok2004 Date: Tue, 3 Mar 2026 11:02:50 +0000 Subject: [PATCH 16/31] Impl player spawn spot --- Game.Server/BaseEntity.cs | 5 +- Game.Server/EnvTonemapController.cs | 1 + Game.Server/Lights.cs | 13 +++ Game.Server/LogicAuto.cs | 9 +++ Game.Server/Player.cs | 119 ++++++++++++++++++++++++++++ Game.Server/PointEntity.cs | 7 +- Game.Server/SkyCamera.cs | 8 ++ Game.Shared/GameRules.cs | 31 ++++++-- 8 files changed, 185 insertions(+), 8 deletions(-) create mode 100644 Game.Server/Lights.cs create mode 100644 Game.Server/LogicAuto.cs create mode 100644 Game.Server/SkyCamera.cs diff --git a/Game.Server/BaseEntity.cs b/Game.Server/BaseEntity.cs index 9246febd..f035714f 100644 --- a/Game.Server/BaseEntity.cs +++ b/Game.Server/BaseEntity.cs @@ -39,7 +39,8 @@ public partial class BaseEntity : IServerEntity public virtual bool IsNextBot() => false; public virtual bool IsBaseCombatWeapon() => false; public virtual bool IsCombatItem() => false; - public bool ClassMatches(ReadOnlySpan classOrWildcard) => false; // todo + public bool ClassMatches(ReadOnlySpan classOrWildcard) => Classname.AsSpan().SequenceEqual(classOrWildcard); + public bool NameMatches(ReadOnlySpan name) => false; // todo public virtual bool IsPredicted() => false; public virtual bool IsTemplate() => false; private static void SendProxy_AnimTime(SendProp prop, object instance, IFieldAccessor field, ref DVariant outData, int element, int objectID) @@ -402,6 +403,8 @@ public virtual ReadOnlySpan GetClassname() { public virtual void Spawn() { } public virtual void Precache() { } + public bool HasSpawnFlags(int flags) => (SpawnFlags & flags) != 0; + public int GetModelIndex() { throw new NotImplementedException(); } diff --git a/Game.Server/EnvTonemapController.cs b/Game.Server/EnvTonemapController.cs index adc1867e..d9daa360 100644 --- a/Game.Server/EnvTonemapController.cs +++ b/Game.Server/EnvTonemapController.cs @@ -6,6 +6,7 @@ using FIELD = Source.FIELD; +[LinkEntityToClass("env_tonemap_controller")] public class EnvTonemapController : PointEntity { public static readonly SendTable DT_EnvTonemapController = new(DT_BaseEntity, [ diff --git a/Game.Server/Lights.cs b/Game.Server/Lights.cs new file mode 100644 index 00000000..d0811901 --- /dev/null +++ b/Game.Server/Lights.cs @@ -0,0 +1,13 @@ +using Game.Shared; + +namespace Game.Server; + +[LinkEntityToClass("light")] +class Light : PointEntity +{ +} + +[LinkEntityToClass("light_environment")] +class EnvLight : Light +{ +} \ No newline at end of file diff --git a/Game.Server/LogicAuto.cs b/Game.Server/LogicAuto.cs new file mode 100644 index 00000000..c76cec5d --- /dev/null +++ b/Game.Server/LogicAuto.cs @@ -0,0 +1,9 @@ +using Game.Shared; + +namespace Game.Server; + +[LinkEntityToClass("logic_auto")] +class LogicAuto : BaseEntity +{ + +} \ No newline at end of file diff --git a/Game.Server/Player.cs b/Game.Server/Player.cs index d1c83792..bb3e3df3 100644 --- a/Game.Server/Player.cs +++ b/Game.Server/Player.cs @@ -247,6 +247,125 @@ public virtual void InitialSpawn() { // gamestats todo } + BaseEntity? FindPlayerStart(ReadOnlySpan className) { + BaseEntity? start = gEntList.FindEntityByClassname(null, className); + BaseEntity? startFirst = start; + + while (start != null) { + if (start.HasSpawnFlags(1)) + return start; + + start = gEntList.FindEntityByClassname(start, className); + } + + return startFirst; + } + + public BaseEntity? EntSelectSpawnPoint() { + BaseEntity? spot; + Edict player = Edict(); + + // if coop + // elseif deathmatch todo + + if (gpGlobals.StartSpot == null || gpGlobals.StartSpot.Length == 0) { + spot = FindPlayerStart("info_player_start"); + if (spot != null) + goto ReturnSpawn; + } + else { + spot = gEntList.FindEntityByName(null, gpGlobals.StartSpot); + if (spot != null) + goto ReturnSpawn; + } + + ReturnSpawn: + if (spot == null) { + Warning("PutClientInServer: no info_player_start on level\n"); + return Instance(engine.PEntityOfEntIndex(0)!); + } + + // LastSpawn = spot; todo + return spot; + } + + public override void Spawn() { + // if (Hints()) Hints().ResetHints(); + + SetClassname("player"); + + // SharedSpawn(); + + SetSimulatedEveryTick(true); + SetAnimatedEveryTick(true); + + // ArmorValue = SpawnArmorValue(); + // SetBlockLOS(false); + MaxHealth = Health; + + if ((GetFlags() & EntityFlags.FakeClient) != 0) { + ClearFlags(); + AddFlag(EntityFlags.Client | EntityFlags.FakeClient); + } + else { + ClearFlags(); + AddFlag(EntityFlags.Client); + } + + AddFlag(EntityFlags.AimTarget); + + EntityEffects effects = (EntityEffects)Effects & EntityEffects.NoShadow; + SetEffects(effects); + + // IncrementInterpolationFrame(); + + // InitFogController(); + + // DmgTake = 0; + // DmgSave = 0; + // HUDDamage = -1; + // DamageType = 0; + // PhysicsFlags = 0; + // DrownRestored = DrownDmg; + + // SetFOV(this, 0); + + // NextDecalTime = 0; + + // GeigerDelay = gpGlobals.CurTime + 2.0f; + + // FieldOfView = 0.766; + + // AdditionPVSOrigin = vec3_origin; + // CameraPVSOrigin = vec3_origin; + + // if (!GameHUDInitialized) + // GameRules.SetDefaultPlayerTeam(this); + + GameRules.GetPlayerSpawnSpot(this); + + Local.Ducked = false; + Local.Ducking = false; + SetViewOffset(VEC_VIEW_SCALED(this)); + Precache(); + + // SetPlayerUnderwater(false); + + // Train = TRAIN_NEW; + + // HackedGunPos = new Vector3(0, 32, 0); + // BonusChallenge; + + // SetThink(null); + + // more todo + + // GameRules.PlayerSpawn(this); + LaggedMovementValue = 1.0f; + + base.Spawn(); + } + public TimeUnit_t GetDeathTime() => DeathTime; public virtual void SetAnimation(PlayerAnim playerAnim) { } // todo diff --git a/Game.Server/PointEntity.cs b/Game.Server/PointEntity.cs index 5b7ee191..db2794ea 100644 --- a/Game.Server/PointEntity.cs +++ b/Game.Server/PointEntity.cs @@ -1,6 +1,11 @@ -namespace Game.Server; +using Game.Shared; + +namespace Game.Server; public class PointEntity : BaseEntity { } + +[LinkEntityToClass("info_player_start")] +class PlayerInfoStart : PointEntity { } \ No newline at end of file diff --git a/Game.Server/SkyCamera.cs b/Game.Server/SkyCamera.cs new file mode 100644 index 00000000..66dbe40b --- /dev/null +++ b/Game.Server/SkyCamera.cs @@ -0,0 +1,8 @@ +using Game.Shared; + +namespace Game.Server; + +[LinkEntityToClass("sky_camera")] +class SkyCamera : BaseEntity // todo LogicalEntity +{ +} \ No newline at end of file diff --git a/Game.Shared/GameRules.cs b/Game.Shared/GameRules.cs index dc2710d4..1dcce1bb 100644 --- a/Game.Shared/GameRules.cs +++ b/Game.Shared/GameRules.cs @@ -5,7 +5,7 @@ global using GameRulesProxy = Game.Client.C_GameRulesProxy; namespace Game.Client; #else -global using static Game.Server.GameRules; +global using static Game.Server.GameRules; global using GameRules = Game.Server.GameRules; global using GameRulesProxy = Game.Server.GameRulesProxy; @@ -13,7 +13,9 @@ namespace Game.Server; #endif using Game.Shared; + using System.Numerics; + using Source.Common; public class @@ -30,7 +32,7 @@ public class public static GameRulesProxy? s_GameRulesProxy; public static readonly - #if CLIENT_DLL +#if CLIENT_DLL RecvTable #else SendTable @@ -61,7 +63,7 @@ public abstract class #else GameRules #endif - () : base("GameRules"){ + () : base("GameRules") { g_pGameRules = this; } @@ -69,14 +71,14 @@ public abstract class new Vector3(0, 0, 64), //VEC_VIEW (View) new Vector3(-16, -16, 0), //VEC_HULL_MIN (HullMin) - new Vector3(16, 16, 72), //VEC_HULL_MAX (HullMax) + new Vector3(16, 16, 72), //VEC_HULL_MAX (HullMax) new Vector3(-16, -16, 0), //VEC_DUCK_HULL_MIN (DuckHullMin) - new Vector3(16, 16, 36), //VEC_DUCK_HULL_MAX (DuckHullMax) + new Vector3(16, 16, 36), //VEC_DUCK_HULL_MAX (DuckHullMax) new Vector3(0, 0, 28), //VEC_DUCK_VIEW (DuckView) new Vector3(-10, -10, -10), //VEC_OBS_HULL_MIN (ObsHullMin) - new Vector3(10, 10, 10), //VEC_OBS_HULL_MAX (ObsHullMax) + new Vector3(10, 10, 10), //VEC_OBS_HULL_MAX (ObsHullMax) new Vector3(0, 0, 14) //VEC_DEAD_VIEWHEIGHT (DeadViewHeight) ); @@ -93,5 +95,22 @@ public virtual bool SwitchToNextBestWeapon(BaseCombatCharacter? player, BaseComb public virtual void CreateCustomNetworkStringTables() { } +#if CLIENT_DLL + +#else + public static BaseEntity? GetPlayerSpawnSpot(BasePlayer player) { + BaseEntity? spawnSpot = player.EntSelectSpawnPoint(); + Assert(spawnSpot != null); + + player.SetLocalOrigin(spawnSpot!.GetAbsOrigin() + new Vector3(0, 0, 1)); + player.SetAbsVelocity(vec3_origin); + player.SetLocalAngles(spawnSpot.GetAbsAngles()); + player.Local.PunchAngle = vec3_angle; + player.Local.PunchAngleVel = vec3_angle; + // player.SnapEyeAngles(spawnSpot.GetLocalAngles()); + + return spawnSpot; + } +#endif } #endif From f36e2f3ee95ee2ded8d930ea544517893306fcaa Mon Sep 17 00:00:00 2001 From: callumok2004 Date: Wed, 4 Mar 2026 01:12:26 +0000 Subject: [PATCH 17/31] PackedEntity impl --- Source.Common/DtSend.cs | 2 +- Source.Engine/PackedEntity.cs | 66 +++++++++++++++++++++++++++++++++-- 2 files changed, 64 insertions(+), 4 deletions(-) diff --git a/Source.Common/DtSend.cs b/Source.Common/DtSend.cs index a871aef2..26f27eb8 100644 --- a/Source.Common/DtSend.cs +++ b/Source.Common/DtSend.cs @@ -649,7 +649,7 @@ public SendTable(string name, SendProp[] props) { protected bool Initialized; protected bool HasBeenWritten; - protected bool HasPropsEncodedAgainstCurrentTickCount; + public bool HasPropsEncodedAgainstCurrentTickCount; public ReadOnlySpan GetName() => NetTableName; diff --git a/Source.Engine/PackedEntity.cs b/Source.Engine/PackedEntity.cs index db754db0..e27ea7a5 100644 --- a/Source.Engine/PackedEntity.cs +++ b/Source.Engine/PackedEntity.cs @@ -14,14 +14,16 @@ public class PackedEntity public int EntityIndex; public int ReferenceCount; - readonly List m_Recipients = []; + readonly List Recipients = []; byte[]? Data; int Bits; IChangeFrameList? ChangeFrameList; uint SnapshotCreationTick; - bool ShouldCheckCreationTick; + bool _ShouldCheckCreationTick; public bool AllocAndCopyPadded(Span data) { + FreeData(); + int bytes = PAD_NUMBER(data.Length, 4); Data = new byte[bytes]; @@ -31,10 +33,68 @@ public bool AllocAndCopyPadded(Span data) { return true; } - public void SetNumBits(int bits)=> Bits = bits; + public void FreeData() => Data = null; + + public void SetNumBits(int bits) => Bits = bits; public void SetCompressed() => Bits |= FLAG_IS_COMPRESSED; public bool IsCompressed() => (Bits & FLAG_IS_COMPRESSED) != 0; public int GetNumBits() => Bits & ~FLAG_IS_COMPRESSED; + public int GetNumBytes() => (Bits + 7) >> 3; public byte[]? GetData() => Data; + + public void SetChangeFrameList(IChangeFrameList list) { + Assert(ChangeFrameList == null); + ChangeFrameList = list; + } + + public IChangeFrameList? GetChangeFrameList() => ChangeFrameList; + + public IChangeFrameList? SnagChangeFrameList() { + IChangeFrameList? ret = ChangeFrameList; + ChangeFrameList = null; + return ret; + } + + public int GetPropsChangedAfterTick(int tick, Span outProps) { + if (ChangeFrameList != null) + return ChangeFrameList.GetPropsChangedAfterTick(tick, outProps); + + return -1; + } + + public List GetRecipients() => Recipients; + public int GetNumRecipients() => Recipients.Count; + + public void SetRecipients(ReadOnlySpan recipients) { + Recipients.Clear(); + Recipients.AddRange(recipients); + } + + public bool CompareRecipients(ReadOnlySpan recipients) { + if (recipients.Length != Recipients.Count) + return false; + + for (int i = 0; i < recipients.Length; i++) { + if (!ReferenceEquals(recipients[i], Recipients[i])) + return false; + } + + return true; + } + + public void SetSnapshotCreationTick(int tick) => SnapshotCreationTick = (uint)tick; + public int GetSnapshotCreationTick() => (int)SnapshotCreationTick; + + public void SetShouldCheckCreationTick(bool state) => _ShouldCheckCreationTick = state; + public bool ShouldCheckCreationTick() => _ShouldCheckCreationTick; + + public void SetServerAndClientClass(ServerClass? serverClass, ClientClass? clientClass) { + ServerClass = serverClass; + ClientClass = clientClass; + if (serverClass != null) { + Assert(serverClass.Table != null); + SetShouldCheckCreationTick(serverClass.Table!.HasPropsEncodedAgainstCurrentTickCount); + } + } } From 42250d75dcdaa11bddbb77bb33d21a1bf95797d8 Mon Sep 17 00:00:00 2001 From: callumok2004 Date: Wed, 4 Mar 2026 01:12:38 +0000 Subject: [PATCH 18/31] FrameSnapshot impl --- Source.Engine/FrameSnapshot.cs | 245 ++++++++++++++++++++++++++++++--- 1 file changed, 225 insertions(+), 20 deletions(-) diff --git a/Source.Engine/FrameSnapshot.cs b/Source.Engine/FrameSnapshot.cs index f66db0c1..d24117b5 100644 --- a/Source.Engine/FrameSnapshot.cs +++ b/Source.Engine/FrameSnapshot.cs @@ -1,11 +1,13 @@  using Source.Common; +using Source.Common.Commands; +using Source.Common.Engine; namespace Source.Engine; public class FrameSnapshotEntry { - public ServerClass Class; + public ServerClass? Class; public int SerialNumber; public PackedEntityHandle_t PackedData; } @@ -13,9 +15,12 @@ public class FrameSnapshotEntry public class FrameSnapshot(FrameSnapshotManager frameSnapshotManager) : IDisposable { public void AddReference() { + Assert(References < 0xFFFF); Interlocked.Increment(ref References); } + public void ReleaseReference() { + Assert(References > 0); Interlocked.Decrement(ref References); if (References == 0) frameSnapshotManager.DeleteFrameSnapshot(this); @@ -28,7 +33,9 @@ public void ReleaseReference() { public volatile int ListIndex; public int TickCount; public FrameSnapshotEntry[]? Entities; + public int NumEntities; public ushort[]? ValidEntities; + public int NumValidEntities; public EventInfo[]? TempEntities; public readonly List ExplicitDeleteSlots = []; @@ -46,7 +53,7 @@ public void Dispose() { public struct UnpackedDataCache { - public PackedEntity Entity; + public PackedEntity? Entity; public int Counter; public int Bits; public InlineArrayMaxPackedEntityData Data; @@ -55,46 +62,244 @@ public struct UnpackedDataCache [EngineComponent] public class FrameSnapshotManager { - public const int INVALID_PACKED_ENTITY_HANDLE = 0; + public const PackedEntityHandle_t INVALID_PACKED_ENTITY_HANDLE = 0; + + static readonly ConVar sv_creationtickcheck = new("sv_creationtickcheck", "1", FCvar.Cheat | FCvar.DevelopmentOnly, "Do extended check for encoding of timestamps against tickcount"); + + PackedEntityHandle_t _nextHandle = 1; + readonly Dictionary HandleMap = []; + PackedEntityHandle_t AllocHandle(PackedEntity entity) { + var h = _nextHandle++; + HandleMap[h] = entity; + return h; + } + PackedEntity HandleToEntity(PackedEntityHandle_t handle) => HandleMap[handle]; + void FreeHandle(PackedEntityHandle_t handle) => HandleMap.Remove(handle); + public virtual void LevelChanged() { + Assert(FrameSnapshots.Count == 0); + PackedEntityCache.Clear(); + HandleMap.Clear(); + _nextHandle = 1; + ((Span)PackedData).Clear(); } - public FrameSnapshot CreateEmptySnapshot(int ticknumber, int maxEntities) { - throw new NotImplementedException(); + public FrameSnapshot CreateEmptySnapshot(int tickcount, int maxEntities) { + FrameSnapshot snap = new(this); + snap.AddReference(); + snap.TickCount = tickcount; + snap.NumEntities = maxEntities; + snap.NumValidEntities = 0; + snap.ValidEntities = null; + snap.Entities = new FrameSnapshotEntry[maxEntities]; + + for (int i = 0; i < maxEntities; i++) { + snap.Entities[i] = new FrameSnapshotEntry { + Class = null, + SerialNumber = -1, + PackedData = INVALID_PACKED_ENTITY_HANDLE + }; + } + + FrameSnapshots.AddLast(snap); + snap.ListIndex = FrameSnapshots.Count - 1; + return snap; } - public FrameSnapshot TakeTickSnapshot(int ticknumber) { - throw new NotImplementedException(); + public FrameSnapshot TakeTickSnapshot(int tickcount) { + Span validEntities = stackalloc ushort[Constants.MAX_EDICTS]; + + FrameSnapshot snap = CreateEmptySnapshot(tickcount, sv.NumEdicts); + + int maxclients = sv.GetClientCount(); + int numValid = 0; + + for (int i = 0; i < sv.NumEdicts; i++) { + Edict edict = sv.Edicts![i]; + FrameSnapshotEntry entry = snap.Entities![i]; + + IServerUnknown? unk = edict.GetUnknown(); + + if (unk == null) + continue; + + if (edict.IsFree()) + continue; + + if (i > 0 && i <= maxclients) { + if (!sv.GetClient(i - 1)!.IsActive()) + continue; + } + + Assert(edict.NetworkSerialNumber != -1); + Assert(edict.GetNetworkable() != null); + Assert(edict.GetNetworkable()!.GetServerClass() != null); + + entry.SerialNumber = edict.NetworkSerialNumber; + entry.Class = edict.GetNetworkable()!.GetServerClass(); + validEntities[numValid++] = (ushort)i; + } + + snap.NumValidEntities = numValid; + snap.ValidEntities = validEntities[..numValid].ToArray(); + + snap.ExplicitDeleteSlots.AddRange(ExplicitDeleteSlots); + ExplicitDeleteSlots.Clear(); + + return snap; } - public FrameSnapshot NextSnapshot(FrameSnapshot snapshot) { - throw new NotImplementedException(); + public FrameSnapshot? NextSnapshot(FrameSnapshot? snapshot) { + if (snapshot == null) + return null; + + LinkedListNode? node = FrameSnapshots.Find(snapshot); + if (node == null) + return null; + + return node.Next?.Value; } public PackedEntity CreatePackedEntity(FrameSnapshot snapshot, int entity) { - throw new NotImplementedException(); + PackedEntity packedEntity = PackedEntitiesPool.Alloc(); + PackedEntityHandle_t handle = AllocHandle(packedEntity); + + Assert(entity < snapshot.NumEntities); + + packedEntity.ReferenceCount = 2; + packedEntity.EntityIndex = entity; + snapshot.Entities![entity].PackedData = handle; + + if (PackedData[entity] != INVALID_PACKED_ENTITY_HANDLE) + RemoveEntityReference(PackedData[entity]); + + PackedData[entity] = handle; + SerialNumber[entity] = snapshot.Entities[entity].SerialNumber; + + packedEntity.SetSnapshotCreationTick(snapshot.TickCount); + + return packedEntity; } - public PackedEntity GetPackedEntity(FrameSnapshot snapshot, int entity) { - throw new NotImplementedException(); + + public PackedEntity? GetPackedEntity(FrameSnapshot? snapshot, int entity) { + if (snapshot == null) + return null; + + Assert(entity < snapshot.NumEntities); + + PackedEntityHandle_t index = snapshot.Entities![entity].PackedData; + if (index == INVALID_PACKED_ENTITY_HANDLE) + return null; + + PackedEntity packedEntity = HandleToEntity(index); + Assert(packedEntity.EntityIndex == entity); + return packedEntity; } + public void AddEntityReference(PackedEntityHandle_t handle) { - throw new NotImplementedException(); + Assert(handle != INVALID_PACKED_ENTITY_HANDLE); + HandleToEntity(handle).ReferenceCount++; } + public void RemoveEntityReference(PackedEntityHandle_t handle) { - throw new NotImplementedException(); + Assert(handle != INVALID_PACKED_ENTITY_HANDLE); + + PackedEntity packedEntity = HandleToEntity(handle); + + if (--packedEntity.ReferenceCount <= 0) { + FreeHandle(handle); + PackedEntitiesPool.Free(packedEntity); + + for (int i = 0; i < PackedEntityCache.Count; i++) { + UnpackedDataCache pdc = PackedEntityCache[i]; + if (pdc.Entity == packedEntity) { + pdc.Entity = null; + pdc.Counter = 0; + break; + } + } + } } + public bool UsePreviouslySentPacket(FrameSnapshot snapshot, int entity, int entSerialNumber) { - throw new NotImplementedException(); + PackedEntityHandle_t handle = PackedData[entity]; + if (handle != INVALID_PACKED_ENTITY_HANDLE) { + if (SerialNumber[entity] == entSerialNumber) { + if (ShouldForceRepack(snapshot, entity, handle)) + return false; + + Assert(entity < snapshot.NumEntities); + snapshot.Entities![entity].PackedData = handle; + HandleToEntity(handle).ReferenceCount++; + return true; + } + + return false; + } + + return false; } + public bool ShouldForceRepack(FrameSnapshot snapshot, int entity, PackedEntityHandle_t handle) { - throw new NotImplementedException(); + if (sv_creationtickcheck.GetBool()) { + PackedEntity pe = HandleToEntity(handle); + Assert(pe != null); + if (pe.ShouldCheckCreationTick()) { + long nCurrentNetworkBase = serverGlobalVariables.GetNetworkBase(snapshot.TickCount, entity); + long nPackedEntityNetworkBase = serverGlobalVariables.GetNetworkBase(pe.GetSnapshotCreationTick(), entity); + if (nCurrentNetworkBase != nPackedEntityNetworkBase) + return true; + } + } + + return false; } - public PackedEntity GetPreviouslySentPacket(int iEntity, int iSerialNumber) { - throw new NotImplementedException(); + + public PackedEntity? GetPreviouslySentPacket(int entity, int serialNumber) { + PackedEntityHandle_t handle = PackedData[entity]; + if (handle != INVALID_PACKED_ENTITY_HANDLE) { + if (SerialNumber[entity] == serialNumber) + return HandleToEntity(handle); + } + + return null; } + public UnpackedDataCache GetCachedUncompressedEntity(PackedEntity packedEntity) { - throw new NotImplementedException(); + if (PackedEntityCache.Count == 0) { + PackedEntityCacheCounter = 0; + for (int i = 0; i < 128; i++) { + PackedEntityCache.Add(new UnpackedDataCache { + Entity = null, + Counter = 0 + }); + } + } + + PackedEntityCacheCounter++; + + UnpackedDataCache oldest = default; + int oldestValue = PackedEntityCacheCounter; + + for (int i = 0; i < PackedEntityCache.Count; i++) { + UnpackedDataCache pdc = PackedEntityCache[i]; + + if (pdc.Entity == packedEntity) { + pdc.Counter = PackedEntityCacheCounter; + return pdc; + } + + if (pdc.Counter < oldestValue) { + oldestValue = pdc.Counter; + oldest = pdc; + } + } + + oldest!.Counter = PackedEntityCacheCounter; + oldest.Bits = -1; + oldest.Entity = packedEntity; + return oldest; } public Mutex GetMutex() => WriteMutex; @@ -105,7 +310,7 @@ public void AddExplicitDelete(int slot) { } public void DeleteFrameSnapshot(FrameSnapshot snapshot) { - for (int i = 0; i < (snapshot.Entities?.Length ?? 0); ++i) { + for (int i = 0; i < snapshot.NumEntities; i++) { if (snapshot.Entities![i].PackedData != INVALID_PACKED_ENTITY_HANDLE) { RemoveEntityReference(snapshot.Entities[i].PackedData); } From c1ef6f45ecdd6aa7d2a211c88de4457a7efe4464 Mon Sep 17 00:00:00 2001 From: callumok2004 Date: Wed, 4 Mar 2026 02:09:37 +0000 Subject: [PATCH 19/31] scaffold PackedEntities --- Source.Engine/FrameSnapshot.cs | 1 + Source.Engine/PackedEntities.cs | 94 +++++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 Source.Engine/PackedEntities.cs diff --git a/Source.Engine/FrameSnapshot.cs b/Source.Engine/FrameSnapshot.cs index d24117b5..3d7eaf4e 100644 --- a/Source.Engine/FrameSnapshot.cs +++ b/Source.Engine/FrameSnapshot.cs @@ -37,6 +37,7 @@ public void ReleaseReference() { public ushort[]? ValidEntities; public int NumValidEntities; public EventInfo[]? TempEntities; + public int NumTempEntities; public readonly List ExplicitDeleteSlots = []; volatile int References; diff --git a/Source.Engine/PackedEntities.cs b/Source.Engine/PackedEntities.cs new file mode 100644 index 00000000..728416fe --- /dev/null +++ b/Source.Engine/PackedEntities.cs @@ -0,0 +1,94 @@ +using Source.Common; +using Source.Common.Bitbuffers; +using Source.Common.Commands; +using Source.Common.Engine; +using Source.Engine.Server; + +using System.Runtime.Intrinsics.Arm; + +namespace Source.Engine; + +struct PackWork +{ + public int Id; + public Edict Edict; + public FrameSnapshot Snapshot; + public static void Process(PackWork item) { + throw new NotImplementedException(); + } +} + +static class PackedEntities +{ + static readonly ConVar sv_debugmanualmode = new("sv_debugmanualmode", "0", FCvar.None, "Make sure entities correctly report whether or not their network data has changed."); + static readonly ConVar sv_parallel_packentities = new("sv_parallel_packentities", "1", FCvar.None); + + static bool EnsurePrivateData(Edict edict) { + if (edict.GetUnknown() != null) + return true; + else { + // Host.Error($"SV_EnsurePrivateData: pEdict->pvPrivateData==NULL (ent {edict.EdictIndex}).\n"); + return false; + } + } + + static void EnsureInstanceBasline(ServerClass serverClass, int Edict, ReadOnlySpan data, int bytes) { + throw new NotImplementedException(); + } + + static void PackEntity(int edictId, Edict edict, ServerClass serverClass, FrameSnapshot snapshot) { + throw new NotImplementedException(); + } + + static void FillHLTVData(FrameSnapshot snapshot, Edict edict, int validEdict) { + throw new NotImplementedException(); + } + + static void FillReplayData(FrameSnapshot snapshot, Edict edict, int validEdict) { + throw new NotImplementedException(); + } + + static SendTable GetEntSendTable(Edict edict) { + throw new NotImplementedException(); + } + + static void NetworkBackDoor(int clientCount, GameClient clients, FrameSnapshot snapshot) { + throw new NotImplementedException(); + } + + static void Normal(int clientCount, GameClient clients, FrameSnapshot snapshot) { + throw new NotImplementedException(); + } + + static void ComputeClientPacks(int clientCount, GameClient clients, FrameSnapshot snapshot) { + throw new NotImplementedException(); + } + + static void MaybeWriteSendTable(SendTable table, bf_write buffer, bool needDecover) { + throw new NotImplementedException(); + } + + static void MaybeWriteSendTable_R(SendTable table, bf_write buffer) { + throw new NotImplementedException(); + } + + static void WriteSendTables(ServerClass serverClass, bf_write buffer) { + throw new NotImplementedException(); + } + + static void ComputeClassInfosCRC(Crc32 crc) { + throw new NotImplementedException(); + } + + static void AssignClassIds() { + throw new NotImplementedException(); + } + + static void WriteClassInfos(ServerClass clases, bf_write buffer) { + throw new NotImplementedException(); + } + + static ReadOnlySpan GetOjectClassName(int objectId) { + throw new NotImplementedException(); + } +} \ No newline at end of file From 54a75ded04052eac45ae305c0947af4c38098058 Mon Sep 17 00:00:00 2001 From: callumok2004 Date: Wed, 4 Mar 2026 02:38:08 +0000 Subject: [PATCH 20/31] Some PackedEntities impl --- Source.Engine/PackedEntities.cs | 94 --------- Source.Engine/Server/PackedEntities.cs | 278 +++++++++++++++++++++++++ 2 files changed, 278 insertions(+), 94 deletions(-) delete mode 100644 Source.Engine/PackedEntities.cs create mode 100644 Source.Engine/Server/PackedEntities.cs diff --git a/Source.Engine/PackedEntities.cs b/Source.Engine/PackedEntities.cs deleted file mode 100644 index 728416fe..00000000 --- a/Source.Engine/PackedEntities.cs +++ /dev/null @@ -1,94 +0,0 @@ -using Source.Common; -using Source.Common.Bitbuffers; -using Source.Common.Commands; -using Source.Common.Engine; -using Source.Engine.Server; - -using System.Runtime.Intrinsics.Arm; - -namespace Source.Engine; - -struct PackWork -{ - public int Id; - public Edict Edict; - public FrameSnapshot Snapshot; - public static void Process(PackWork item) { - throw new NotImplementedException(); - } -} - -static class PackedEntities -{ - static readonly ConVar sv_debugmanualmode = new("sv_debugmanualmode", "0", FCvar.None, "Make sure entities correctly report whether or not their network data has changed."); - static readonly ConVar sv_parallel_packentities = new("sv_parallel_packentities", "1", FCvar.None); - - static bool EnsurePrivateData(Edict edict) { - if (edict.GetUnknown() != null) - return true; - else { - // Host.Error($"SV_EnsurePrivateData: pEdict->pvPrivateData==NULL (ent {edict.EdictIndex}).\n"); - return false; - } - } - - static void EnsureInstanceBasline(ServerClass serverClass, int Edict, ReadOnlySpan data, int bytes) { - throw new NotImplementedException(); - } - - static void PackEntity(int edictId, Edict edict, ServerClass serverClass, FrameSnapshot snapshot) { - throw new NotImplementedException(); - } - - static void FillHLTVData(FrameSnapshot snapshot, Edict edict, int validEdict) { - throw new NotImplementedException(); - } - - static void FillReplayData(FrameSnapshot snapshot, Edict edict, int validEdict) { - throw new NotImplementedException(); - } - - static SendTable GetEntSendTable(Edict edict) { - throw new NotImplementedException(); - } - - static void NetworkBackDoor(int clientCount, GameClient clients, FrameSnapshot snapshot) { - throw new NotImplementedException(); - } - - static void Normal(int clientCount, GameClient clients, FrameSnapshot snapshot) { - throw new NotImplementedException(); - } - - static void ComputeClientPacks(int clientCount, GameClient clients, FrameSnapshot snapshot) { - throw new NotImplementedException(); - } - - static void MaybeWriteSendTable(SendTable table, bf_write buffer, bool needDecover) { - throw new NotImplementedException(); - } - - static void MaybeWriteSendTable_R(SendTable table, bf_write buffer) { - throw new NotImplementedException(); - } - - static void WriteSendTables(ServerClass serverClass, bf_write buffer) { - throw new NotImplementedException(); - } - - static void ComputeClassInfosCRC(Crc32 crc) { - throw new NotImplementedException(); - } - - static void AssignClassIds() { - throw new NotImplementedException(); - } - - static void WriteClassInfos(ServerClass clases, bf_write buffer) { - throw new NotImplementedException(); - } - - static ReadOnlySpan GetOjectClassName(int objectId) { - throw new NotImplementedException(); - } -} \ No newline at end of file diff --git a/Source.Engine/Server/PackedEntities.cs b/Source.Engine/Server/PackedEntities.cs new file mode 100644 index 00000000..adfdfa8d --- /dev/null +++ b/Source.Engine/Server/PackedEntities.cs @@ -0,0 +1,278 @@ +using Source.Common; +using Source.Common.Bitbuffers; +using Source.Common.Commands; +using Source.Common.Engine; + +using System.Buffers; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics.Arm; + +namespace Source.Engine.Server; + +struct PackWork +{ + public int Id; + public Edict Edict; + public FrameSnapshot Snapshot; + public static void Process(PackWork item) => PackedEntities.PackEntity(item.Id, item.Edict, item.Snapshot.Entities![item.Id].Class!, item.Snapshot); +} + +static class PackedEntities +{ + static readonly ConVar sv_debugmanualmode = new("sv_debugmanualmode", "0", FCvar.None, "Make sure entities correctly report whether or not their network data has changed."); + static readonly ConVar sv_parallel_packentities = new("sv_parallel_packentities", "0", FCvar.None); // SDN: Defaulted to 0 for now ~Callum + + static bool EnsurePrivateData(Edict edict) { + if (edict.GetUnknown() != null) + return true; + else { + // Host.Error($"SV_EnsurePrivateData: pEdict.pvPrivateData==NULL (ent {edict.EdictIndex}).\n"); + return false; + } + } + + static void EnsureInstanceBasline(ServerClass serverClass, int edictId, ReadOnlySpan data, int bytes) { + throw new NotImplementedException(); + } + + public static void PackEntity(int edictId, Edict edict, ServerClass serverClass, FrameSnapshot snapshot) { + Assert(edictId < snapshot.NumEntities); + +#if false // TODO TODO SendTable, ChangeFrameList, SendProxyRecipients, SV.EnsureInstanceBaseline, AllocChangeFrameList + + int serialNum = snapshot.Entities![edictId].SerialNumber; + + // Check to see if this entity specifies its changes. + // If so, then try to early out making the fullpack + bool usedPrev = false; + if (!edict.HasStateChanged()) { + // Now this may not work if we didn't previously send a packet; + // if not, then we gotta compute it + usedPrev = framesnapshotmanager.UsePreviouslySentPacket(snapshot, edictId, serialNum); + } + + if (usedPrev && !sv_debugmanualmode.GetBool()) { + edict.ClearStateChanged(); + return; + } + + // First encode the entity's data. + byte[] packedData = ArrayPool.Shared.Rent(Constants.MAX_PACKEDENTITY_DATA); + bf_write writeBuf = new(packedData, Constants.MAX_PACKEDENTITY_DATA); + + SendTable sendTable = serverClass.Table; + + // (avoid constructor overhead). + Span tempData = stackalloc byte[Unsafe.SizeOf() * Constants.MAX_DATATABLE_PROXIES]; + Span recip = MemoryMarshal.Cast(tempData); + + if (!SendTable_Encode(sendTable, edict.GetUnknown(), &writeBuf, edictId, &recip, false)) { + Host_Error("SV_PackEntity: SendTable_Encode returned false (ent %d).\n", edictId); + } + + SV_EnsureInstanceBaseline(serverClass, edictId, packedData, writeBuf.BytesWritten); + + int nFlatProps = SendTable_GetNumFlatProps(sendTable); + IChangeFrameList? changeFrame = null; + + // 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. + // + // If not, then we want to setup a new IChangeFrameList. + PackedEntity? prevFrame = framesnapshotmanager.GetPreviouslySentPacket(edictId, snapshot.Entities[edictId].SerialNumber); + if (prevFrame != null) { + // Calculate a delta. + Assert(!prevFrame.IsCompressed()); + + int[] deltaProps = new int[Constants.MAX_DATATABLE_PROPS]; + + int changes = SendTable_CalcDelta( + sendTable, + prevFrame.GetData(), prevFrame.GetNumBits(), + packedData, writeBuf.GetNumBitsWritten(), + + deltaProps, + ARRAYSIZE(deltaProps), + + edictId); + + // 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). + // It would be interesting to hook here and see how many non-manual-mode entities + // are winding up with no changes. + if (changes == 0) { + if (prevFrame.CompareRecipients(recip)) { + if (framesnapshotmanager.UsePreviouslySentPacket(snapshot, edictId, serialNum)) { + edict.ClearStateChanged(); + return; + } + } + } + else { + if (!edict.HasStateChanged()) { + for (int iDeltaProp = 0; iDeltaProp < changes; iDeltaProp++) { + Assert(sendTable.Precalc); + Assert(deltaProps[iDeltaProp] < sendTable.Precalc!.GetNumProps()); + + SendProp prop = sendTable.Precalc.GetProp(deltaProps[iDeltaProp])!; + // If a field changed, but it changed because it encoded against tickcount, + // then it's just like the entity changed the underlying field, not an error, that is. + if ((prop.GetFlags() & PropFlags.EncodedAgainstTickCount) != 0) + continue; + + Msg("Entity %d (class '%s') reported ENTITY_CHANGE_NONE but '%s' changed.\n", + edictId, + edict.GetClassName(), + prop.GetName()); + } + } + } + + if (false /*hltv && hltv.IsActive()*/) { + // in HLTV or Replay mode every PackedEntity keeps it's own ChangeFrameList + // we just copy the ChangeFrameList from prev frame and update it + changeFrame = prevFrame.GetChangeFrameList(); + changeFrame = changeFrame.Copy(); // allocs and copies ChangeFrameList + } + else { + // Ok, now snag the changeframe from the previous frame and update the 'last frame changed' + // for the properties in the delta. + changeFrame = prevFrame.SnagChangeFrameList(); + } + + ErrorIfNot(changeFrame, ("SV_PackEntity: SnagChangeFrameList returned null")); + ErrorIfNot(changeFrame.GetNumProps() == nFlatProps, ("SV_PackEntity: SnagChangeFrameList mismatched number of props[%d vs %d]", nFlatProps, changeFrame.GetNumProps())); + + changeFrame.SetChangeTick(deltaProps, changes, snapshot.TickCount); + } + else { + // Ok, init the change frames for the first time. + changeFrame = AllocChangeFrameList(nFlatProps, snapshot.TickCount); + } + + // Now make a PackedEntity and store the new packed data in there. + PackedEntity packedEntity = framesnapshotmanager.CreatePackedEntity(snapshot, edictId); + packedEntity.SetChangeFrameList(changeFrame); + packedEntity.SetServerAndClientClass(serverClass, null); + packedEntity.AllocAndCopyPadded(packedData); + packedEntity.SetRecipients(recip); + + edict.ClearStateChanged(); +#endif + } + + static void FillHLTVData(FrameSnapshot snapshot, Edict edict, int validEdict) { + throw new NotImplementedException(); + } + + static void FillReplayData(FrameSnapshot snapshot, Edict edict, int validEdict) { + throw new NotImplementedException(); + } + + static SendTable GetEntSendTable(Edict edict) { + throw new NotImplementedException(); + } + + static void NetworkBackDoor(int clientCount, GameClient[] clients, FrameSnapshot snapshot) { + throw new NotImplementedException(); + } + + static void Normal(int clientCount, GameClient[] clients, FrameSnapshot snapshot) { + Assert(snapshot.NumValidEntities >= 0 && snapshot.NumValidEntities <= Constants.MAX_EDICTS); + + List workItems = []; + + // check for all active entities, if they are seen by at least on client, if + // so, bit pack them + for (int iValidEdict = 0; iValidEdict < snapshot.NumValidEntities; ++iValidEdict) { + int index = snapshot.ValidEntities![iValidEdict]; + + Assert(index < snapshot.NumEntities); + + Edict edict = sv.Edicts![index]; + + // if HLTV is running save PVS info for each entity + FillHLTVData(snapshot, edict, iValidEdict); + + // if Replay is running save PVS info for each entity + FillReplayData(snapshot, edict, iValidEdict); + + // Check to see if the entity changed this frame... + // ServerDTI_RegisterNetworkStateChange( sendTable, ent.m_bStateChanged ); + + for (int iClient = 0; iClient < clientCount; ++iClient) { + // entities is seen by at least this client, pack it and exit loop + GameClient client = clients[iClient]; // update variables cl, pInfo, frame for current client + ClientFrame? frame = client.CurrentFrame; + + if (frame!.TransmitEntity.Get(index) != 0) { + PackWork w; + w.Id = index; + w.Edict = edict; + w.Snapshot = snapshot; + + workItems.Add(w); + break; + } + } + } + + if (sv_parallel_packentities.GetBool()) { + // ParallelProcess("PackWork_t::Process", workItems.Base(), workItems.Count(), &PackWork_t::Process); + Debugger.Break(); + } + else { + int c = workItems.Count(); + for (int i = 0; i < c; ++i) { + PackWork w = workItems[i]; + PackEntity(w.Id, w.Edict, w.Snapshot.Entities![w.Id].Class!, w.Snapshot); + } + } + + // InvalidateSharedEdictChangeInfos(); todo + } + + // todo call from CGameServer::SendClientMessages + static void ComputeClientPacks(int clientCount, GameClient[] clients, FrameSnapshot snapshot) { + for (int i = 0; i < clientCount; i++) { + // todo transmit info + } + + if (false /* g_LocalNetworkBackdoor */) { + + + } + else + Normal(clientCount, clients, snapshot); + } + + static void MaybeWriteSendTable(SendTable table, bf_write buffer, bool needDecover) { + throw new NotImplementedException(); + } + + static void MaybeWriteSendTable_R(SendTable table, bf_write buffer) { + throw new NotImplementedException(); + } + + static void WriteSendTables(ServerClass serverClass, bf_write buffer) { + throw new NotImplementedException(); + } + + static void ComputeClassInfosCRC(Crc32 crc) { + throw new NotImplementedException(); + } + + static void AssignClassIds() { + throw new NotImplementedException(); + } + + static void WriteClassInfos(ServerClass clases, bf_write buffer) { + throw new NotImplementedException(); + } + + static ReadOnlySpan GetOjectClassName(int objectId) { + throw new NotImplementedException(); + } +} \ No newline at end of file From ca0d5c3227d0f388b12087db61bfb65b70bddb83 Mon Sep 17 00:00:00 2001 From: callumok2004 Date: Wed, 4 Mar 2026 05:08:29 +0000 Subject: [PATCH 21/31] ChangeFrameList --- Source.Engine/ChangeFrameList.cs | 52 +++++++++++++++++++++++++++++++ Source.Engine/IChangeFrameList.cs | 6 ++-- 2 files changed, 55 insertions(+), 3 deletions(-) create mode 100644 Source.Engine/ChangeFrameList.cs diff --git a/Source.Engine/ChangeFrameList.cs b/Source.Engine/ChangeFrameList.cs new file mode 100644 index 00000000..5eb5b463 --- /dev/null +++ b/Source.Engine/ChangeFrameList.cs @@ -0,0 +1,52 @@ +namespace Source.Engine; + +class ChangeFrameList : IChangeFrameList +{ + readonly List ChangeTicks = []; + + public void Init(int properties, int curTick) { + ChangeTicks.Clear(); + for (int i = 0; i < properties; i++) + ChangeTicks.Add(curTick); + } + + public void Release() { } + + public IChangeFrameList Copy() { + ChangeFrameList ret = new(); + int numProps = ChangeTicks.Count; + ret.Init(numProps, 0); + for (int i = 0; i < numProps; i++) + ret.ChangeTicks[i] = ChangeTicks[i]; + return ret; + } + + public int GetNumProps() => ChangeTicks.Count; + + public void SetChangeTick(ReadOnlySpan propIndices, int iPropIndices, int tick) { + for (int i = 0; i < iPropIndices; i++) + ChangeTicks[propIndices[i]] = tick; + } + + public int GetPropsChangedAfterTick(int tick, Span iOutProps, int maxOutProps) { + int outProps = 0; + int count = ChangeTicks.Count; + + Assert(count <= maxOutProps); + + for (int i = 0; i < count; i++) { + if (ChangeTicks[i] > tick) { + iOutProps[outProps] = i; + outProps++; + } + } + + return outProps; + } + + public static IChangeFrameList AllocChangeFrameList(int properties, int curTick) { + ChangeFrameList ret = new(); + ret.Init(properties, curTick); + return ret; + } +} \ No newline at end of file diff --git a/Source.Engine/IChangeFrameList.cs b/Source.Engine/IChangeFrameList.cs index 14b3d3e4..08adbe1f 100644 --- a/Source.Engine/IChangeFrameList.cs +++ b/Source.Engine/IChangeFrameList.cs @@ -1,9 +1,9 @@ namespace Source.Engine; -public interface IChangeFrameList : IDisposable +public interface IChangeFrameList { int GetNumProps(); - void SetChangeTick(ReadOnlySpan propIndices, int tick); - int GetPropsChangedAfterTick(int tick, Span outProps); + void SetChangeTick(ReadOnlySpan propIndices, int iPropIndices, int tick); + int GetPropsChangedAfterTick(int tick, Span outProps, int maxOutProps); IChangeFrameList Copy(); } From 3d2c43694bdd053aa003071ec1423593cb3a287a Mon Sep 17 00:00:00 2001 From: callumok2004 Date: Wed, 4 Mar 2026 07:06:08 +0000 Subject: [PATCH 22/31] More PackedEntities/SendTable impl --- Game.Server/BaseEntity.cs | 11 +- Game.Server/GameInterface.cs | 8 +- Game.Server/ServerNetworkProperty.cs | 5 +- Source.Common/Engine/DataTableSend.cs | 2 +- Source.Engine/ClientFrame.cs | 1 + Source.Engine/DtEncode.cs | 12 +- Source.Engine/EngineRecvTable.cs | 6 +- Source.Engine/EngineSendTable.cs | 132 ++++++++++++++++++++ Source.Engine/PackedEntity.cs | 2 +- Source.Engine/Server/BaseClient.cs | 63 +++++++++- Source.Engine/Server/BaseServer.cs | 161 ++++++++++++++++++++++++- Source.Engine/Server/GameClient.cs | 18 +-- Source.Engine/Server/GameServer.cs | 55 +++++++++ Source.Engine/Server/PackedEntities.cs | 80 ++++++------ 14 files changed, 493 insertions(+), 63 deletions(-) diff --git a/Game.Server/BaseEntity.cs b/Game.Server/BaseEntity.cs index f035714f..10f29d11 100644 --- a/Game.Server/BaseEntity.cs +++ b/Game.Server/BaseEntity.cs @@ -468,8 +468,15 @@ public void SetAbsVelocity(in Vector3 absVelocity) { public ref readonly Vector3 GetViewOffset() => ref ViewOffset; public ref readonly QAngle GetAbsAngles() => ref AbsRotation; - public void SetLocalOrigin(in Vector3 origin) { } // todo - public void SetLocalAngles(in QAngle origin) { } // todo + public void SetLocalOrigin(in Vector3 origin) { + // This has a lot more logic thats needed later TODO FIXME + Origin = origin; + } + + public void SetLocalAngles(in QAngle angles) { + // This has a lot more logic thats needed later TODO FIXME + Rotation = angles; + } public ref Matrix3x4 EntityToWorldTransform() { diff --git a/Game.Server/GameInterface.cs b/Game.Server/GameInterface.cs index a3e00fc2..4a5f96d9 100644 --- a/Game.Server/GameInterface.cs +++ b/Game.Server/GameInterface.cs @@ -474,8 +474,12 @@ public void GetPlayerLimits(out int minPlayers, out int maxPlayers, out int defa maxPlayers = Constants.MAX_PLAYERS; } - public PlayerState GetPlayerState(Edict player) { - throw new NotImplementedException(); + public PlayerState? GetPlayerState(Edict player) { + if (player == null || player.GetUnknown() == null) + return null; + + BasePlayer? pl = BaseEntity.Instance(player) as BasePlayer; + return pl?.pl; } public void NetworkIDValidated(ReadOnlySpan userName, ReadOnlySpan networkID) { diff --git a/Game.Server/ServerNetworkProperty.cs b/Game.Server/ServerNetworkProperty.cs index d30e8b74..52fb123f 100644 --- a/Game.Server/ServerNetworkProperty.cs +++ b/Game.Server/ServerNetworkProperty.cs @@ -34,7 +34,10 @@ public ReadOnlySpan GetClassName() { } public ServerClass GetServerClass() { - throw new NotImplementedException(); + // if (ServerClass == null) todo + // ServerClass = Outer.GetServerClass(); + + return ServerClass; } public void Release() { diff --git a/Source.Common/Engine/DataTableSend.cs b/Source.Common/Engine/DataTableSend.cs index b224362e..720a8134 100644 --- a/Source.Common/Engine/DataTableSend.cs +++ b/Source.Common/Engine/DataTableSend.cs @@ -2,5 +2,5 @@ public class SendProxyRecipients { - + public const int MAX_DATATABLE_PROXIES = 32; } diff --git a/Source.Engine/ClientFrame.cs b/Source.Engine/ClientFrame.cs index 01620740..664550ba 100644 --- a/Source.Engine/ClientFrame.cs +++ b/Source.Engine/ClientFrame.cs @@ -24,6 +24,7 @@ internal void Init(int tickcount) { } internal void Init(FrameSnapshot snapshot) { TickCount = snapshot.TickCount; + Snapshot = snapshot; } internal FrameSnapshot GetSnapshot() => Snapshot; diff --git a/Source.Engine/DtEncode.cs b/Source.Engine/DtEncode.cs index 55eb8f1c..37284bb0 100644 --- a/Source.Engine/DtEncode.cs +++ b/Source.Engine/DtEncode.cs @@ -55,7 +55,7 @@ public struct PropTypeFns public SkipPropFn SkipProp; public PropTypeFns(EncodeFn encode, DecodeFn decode, CompareDeltasFn compareDeltas, FastCopyFn fastCopy, GetTypeNameStringFn getTypeNameString, - IsZeroFn isZero, DecodeZeroFn decodeZero, IsEncodedZeroFn isEncodedZero, SkipPropFn skipProp) { + IsZeroFn isZero, DecodeZeroFn decodeZero, IsEncodedZeroFn isEncodedZero, SkipPropFn skipProp) { Encode = encode; Decode = decode; CompareDeltas = compareDeltas; @@ -202,7 +202,7 @@ public static void EncodeString(SendProp prop, string? incoming, bf_write outBuf /// /// Indices correlate to enum values. /// - static readonly PropTypeFns[] g_PropTypeFns = [ + public static readonly PropTypeFns[] g_PropTypeFns = [ new(Int_Encode, Int_Decode, Int_CompareDeltas, Generic_FastCopy, Int_GetTypeNameString, Int_IsZero, Int_DecodeZero, Int_IsEncodedZero, Int_SkipProp), new(Float_Encode, Float_Decode, Float_CompareDeltas, Generic_FastCopy, Float_GetTypeNameString, Float_IsZero, Float_DecodeZero, Float_IsEncodedZero, Float_SkipProp), new(Vector_Encode, Vector_Decode, Vector_CompareDeltas, Generic_FastCopy, Vector_GetTypeNameString, Vector_IsZero, Vector_DecodeZero, Vector_IsEncodedZero, Vector_SkipProp), @@ -543,9 +543,9 @@ public static void Array_Decode(ref DecodeInfo decodeInfo) { lengthProxy = decodeInfo.RecvProxyData.RecvProp.GetArrayLengthProxy()!; targetField = subDecodeInfo.FieldInfo; } - else + else targetField = arrayProp.FieldInfo; - + if (targetField is not DynamicArrayAccessor arrayFieldInfo) { if (targetField is not DynamicArrayIndexAccessor arrayFieldindexInfo) { Warning("Cannot Array_Decode on a non-ArrayFieldInfo target!\n"); @@ -563,7 +563,7 @@ public static void Array_Decode(ref DecodeInfo decodeInfo) { for (subDecodeInfo.RecvProxyData.Element = 0; subDecodeInfo.RecvProxyData.Element < nElements; subDecodeInfo.RecvProxyData.Element++) { var element = arrayFieldInfo.AtIndex(subDecodeInfo.RecvProxyData.Element); - if(element == null) { + if (element == null) { Warning($"Invalid element at {subDecodeInfo.RecvProxyData.Element}\n"); continue; } @@ -629,7 +629,7 @@ public static void GModTable_Decode(ref DecodeInfo decodeInfo) { 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) + if (valueType != 0) GmodTableTypeFns.Get(valueType).Read(decodeInfo.In, ref gmodtable[key]); } } diff --git a/Source.Engine/EngineRecvTable.cs b/Source.Engine/EngineRecvTable.cs index c9158d04..de5ba4f2 100644 --- a/Source.Engine/EngineRecvTable.cs +++ b/Source.Engine/EngineRecvTable.cs @@ -2,6 +2,7 @@ using Source.Common; using Source.Common.Bitbuffers; +using Source.Common.Engine; using System.Collections; using System.Diagnostics; @@ -57,7 +58,7 @@ public void CopyPropData(bf_write outWrite, SendProp prop) { Buffer!.Seek(start); outWrite.WriteBitsFromBuffer(Buffer!, len); } - public void ComparePropData(ref DeltaBitsReader inReader, SendProp prop) => PropTypeFns.Get(prop.Type).CompareDeltas(prop, Buffer!, inReader.Buffer!); + public int ComparePropData(ref DeltaBitsReader inReader, SendProp prop) => PropTypeFns.Get(prop.Type).CompareDeltas(prop, Buffer!, inReader.Buffer!); public void Dispose() { Assert(Buffer == null); @@ -119,6 +120,7 @@ public IEnumerable> Visit(TableType table) { public abstract class DatatableStack { public SendTablePrecalc Precalc; + public SendProxyRecipients[]? Recipients; public InlineArray256 Proxies; public object Instance; protected int CurPropIndex; @@ -143,6 +145,8 @@ public void Init(bool explicitRoutes = false) { public abstract void RecurseAndCallProxies(SendNode node, object instance); + public SendProp? GetCurProp() => CurProp; + public bool IsPropProxyValid(int iProp) => Proxies[Precalc.PropProxyIndices[iProp]] != null; public bool IsCurProxyValid() => Proxies[Precalc.PropProxyIndices[CurPropIndex]] != null; public object? GetCurStructBase() => Proxies[Precalc.PropProxyIndices[CurPropIndex]]; public void SeekToProp(uint iProp) { diff --git a/Source.Engine/EngineSendTable.cs b/Source.Engine/EngineSendTable.cs index fcbcb934..b4aac854 100644 --- a/Source.Engine/EngineSendTable.cs +++ b/Source.Engine/EngineSendTable.cs @@ -1,4 +1,6 @@ using Source.Common; +using Source.Common.Bitbuffers; +using Source.Common.Engine; using System.Diagnostics; @@ -93,4 +95,134 @@ private void CalcNextVectorElems(SendTable table) { prop.SetFlags(prop.GetFlags() | PropFlags.IsAVectorElem); } } + + public bool Encode(SendTable table, object data, bf_write dataOut, int objectId, SendProxyRecipients[] recipients, bool nonZeroOnly) { + SendTablePrecalc precalc = table.Precalc!; + ErrorIfNot(precalc != null, $"SendTable_Encode: Missing precalc for table {table.NetTableName}."); + if (recipients.Length > 0) + ErrorIfNot(recipients.Length >= precalc.GetNumDataTableProxies(), $"SendTable_Encode: recipients array too small (got {recipients.Length}, need {precalc.GetNumDataTableProxies()})."); + + EncodeInfo info = new(precalc, data, objectId, dataOut) { + Recipients = recipients + }; + info.Init(); + + int numProps = precalc.GetNumProps(); + for (int i = 0; i < numProps; i++) { + if (!info.IsPropProxyValid(i)) + continue; + + info.SeekToProp((uint)i); + + if (nonZeroOnly && IsPropZero(info, i)) + continue; + + EncodeProp(info, i); + } + + return !dataOut.Overflowed; + } + + + bool IsPropZero(EncodeInfo info, int _) { + SendProp p = info.GetCurProp()!; + + DVariant var = new(); + object baseData = info.GetCurStructBase()!; + + IFieldAccessor accessor = p.FieldInfo; + p.GetProxyFn()(p, baseData, accessor, ref var, 0, info.GetObjectID()); + + return PropTypeFns.g_PropTypeFns[(int)p.Type].IsZero(baseData, ref var, p); + } + + public int GetNumFlatProps(SendTable table) { + SendTablePrecalc precalc = table.Precalc!; + ErrorIfNot(precalc != null, $"SendTable_GetNumFlatProps: missing pPrecalc."); + return precalc.GetNumProps(); + } + + void EncodeProp(EncodeInfo info, int prop) { + DVariant var = new(); + + SendProp p = info.GetCurProp()!; + object baseData = info.GetCurStructBase()!; + + IFieldAccessor accessor = p.FieldInfo; + p.GetProxyFn()(p, baseData, accessor, ref var, 0, info.GetObjectID()); + + info.DeltaBitsWriter.WritePropIndex(prop); + + PropTypeFns.g_PropTypeFns[(int)p.Type].Encode(baseData, ref var, p, info.DeltaBitsWriter.GetBitBuf(), info.GetObjectID()); + } + + public int CalcDelta(SendTable table, byte[]? fromState, int nFromBits, byte[] toState, int nToBits, Span deltaProps, int maxDeltaProps, int objectId) { + int nDeltaProps = 0; + + SendTablePrecalc precalc = table.Precalc!; + + bf_read toBits = new("CalcDelta/toBits", toState, BitBuffer.BitByte(nToBits), nToBits); + DeltaBitsReader toBitsReader = new(toBits); + uint iToProp = toBitsReader.ReadNextPropIndex(); + + if (fromState != null) { + bf_read fromBitsBuf = new("CalcDelta/fromBits", fromState, BitBuffer.BitByte(nFromBits), nFromBits); + DeltaBitsReader fromBitsReader = new(fromBitsBuf); + uint iFromProp = fromBitsReader.ReadNextPropIndex(); + + for (; iToProp < Constants.MAX_DATATABLE_PROPS; iToProp = toBitsReader.ReadNextPropIndex()) { + Assert((int)iToProp >= 0); + + // Skip any properties in the from state that aren't in the to state. + while (iFromProp < iToProp) { + fromBitsReader.SkipPropData(precalc.GetProp((int)iFromProp)!); + iFromProp = fromBitsReader.ReadNextPropIndex(); + } + + if (iFromProp == iToProp) { + // The property is in both states, so compare them and write the index + // if the states are different. + if (fromBitsReader.ComparePropData(ref toBitsReader, precalc.GetProp((int)iToProp)!) != 0) { + deltaProps[nDeltaProps++] = (int)iToProp; + if (nDeltaProps >= maxDeltaProps) + break; + } + + // Seek to the next property. + iFromProp = fromBitsReader.ReadNextPropIndex(); + } + else { + // Only the 'to' state has this property, so just skip its data and register a change. + toBitsReader.SkipPropData(precalc.GetProp((int)iToProp)!); + deltaProps[nDeltaProps++] = (int)iToProp; + if (nDeltaProps >= maxDeltaProps) + break; + } + } + + Assert(iToProp == ~0u); + + fromBitsReader.ForceFinished(); + } + else { + for (; iToProp != unchecked((uint)-1); iToProp = toBitsReader.ReadNextPropIndex()) { + Assert((int)iToProp >= 0 && iToProp < Constants.MAX_DATATABLE_PROPS); + + SendProp prop = precalc.GetProp((int)iToProp)!; + if (!PropTypeFns.g_PropTypeFns[(int)prop.Type].IsEncodedZero(prop, toBits)) { + deltaProps[nDeltaProps++] = (int)iToProp; + if (nDeltaProps >= maxDeltaProps) + break; + } + } + } + + return nDeltaProps; + } } + +class EncodeInfo(SendTablePrecalc precalc, object structData, int objectId, bf_write dataOut) : DatatableStack(precalc, structData, objectId) +{ + public DeltaBitsWriter DeltaBitsWriter = new(dataOut); + public override void RecurseAndCallProxies(SendNode node, object instance) { } +} \ No newline at end of file diff --git a/Source.Engine/PackedEntity.cs b/Source.Engine/PackedEntity.cs index e27ea7a5..9eb40989 100644 --- a/Source.Engine/PackedEntity.cs +++ b/Source.Engine/PackedEntity.cs @@ -58,7 +58,7 @@ public void SetChangeFrameList(IChangeFrameList list) { public int GetPropsChangedAfterTick(int tick, Span outProps) { if (ChangeFrameList != null) - return ChangeFrameList.GetPropsChangedAfterTick(tick, outProps); + return ChangeFrameList.GetPropsChangedAfterTick(tick, outProps, Constants.MAX_DATATABLE_PROPS); return -1; } diff --git a/Source.Engine/Server/BaseClient.cs b/Source.Engine/Server/BaseClient.cs index 24206954..fa5decef 100644 --- a/Source.Engine/Server/BaseClient.cs +++ b/Source.Engine/Server/BaseClient.cs @@ -10,6 +10,7 @@ using Source.Common.Formats.Keyvalues; using Source.Common.Networking; using Source.Common.Server; +using Source.GUI.Controls; using Steamworks; @@ -354,16 +355,70 @@ protected virtual bool ProcessListenEvents(CLC_ListenEvents m) { return true; } - protected virtual void SendSnapshot(ClientFrame frame) { + const int SNAPSHOT_SCRATCH_BUFFER_SIZE = 16000; + byte[] SnapshotScratchBuffer = new byte[SNAPSHOT_SCRATCH_BUFFER_SIZE / 4]; + + public virtual void SendSnapshot(ClientFrame frame) { // TODO This has a lot more to it if (ForceWaitForTick > 0 || LastSnapshot == frame.GetSnapshot()) { NetChannel.Transmit(); return; } bool failedOnce; - // todo + + write_again: + bf_write msg = new(SnapshotScratchBuffer, SNAPSHOT_SCRATCH_BUFFER_SIZE); + + ClientFrame? deltaFrame = null;// GetDeltaFrame(DeltaTick); + if (deltaFrame == null) { + // OnRequestFullUpdate(); + } + + NET_Tick tickmsg = new(frame.TickCount, (int)Host.FrameTime, (int)Host.FrameTimeStandardDeviation); + SendNetMsg(tickmsg); + +#if !SHARED_NET_STRING_TABLES + +#endif + + int deltaStartBit = 0; + + // Server.WriteDeltaEntities(this, frame, deltaFrame, msg); // TODO + + int maxTempEnts = Server.IsMultiplayer() ? 64 : 255; + + // WriteGameSounds(); + + if (msg.Overflowed) {//todo + Disconnect($"Snapshot overflowed\n"); + } LastSnapshot = frame.GetSnapshot(); + + if (FakePlayer && NetChannel == null) { + DeltaTick = (int)frame.TickCount; + StringTableAckTick = DeltaTick; + return; + } + + bool sendOK; + + if (deltaFrame == null) { + sendOK = NetChannel.SendData(msg); + sendOK = sendOK && NetChannel.Transmit(); + + ForceWaitForTick = (int)frame.TickCount; + } + else { + sendOK = NetChannel.SendDatagram(msg) > 0; + } + + if (sendOK) { + + } + else { + Disconnect($"ERROR! Couldn't send snapshot.\n"); + } } public int GetClientChallenge() => ClientChallenge; @@ -523,9 +578,9 @@ public bool SendNetMsg(INetMessage msg, bool forceReliable = false) { public long SignOnTick; // CSmartPtr FrameSnapshot? LastSnapshot; // todo? ^ - FrameSnapshot? Baseline; + public FrameSnapshot? Baseline; public int BaselineUpdateTick; - MaxEdictsBitVec BaselinesSent; + public MaxEdictsBitVec BaselinesSent; public int BaselineUsed; public int ForceWaitForTick; diff --git a/Source.Engine/Server/BaseServer.cs b/Source.Engine/Server/BaseServer.cs index 212f63ca..c40b2b18 100644 --- a/Source.Engine/Server/BaseServer.cs +++ b/Source.Engine/Server/BaseServer.cs @@ -31,6 +31,7 @@ public abstract class BaseServer : IServer protected readonly Net Net = Singleton(); protected readonly Host Host = Singleton(); protected readonly Filter Filter = Singleton(); + protected readonly FrameSnapshotManager framesnapshotmanager = Singleton(); internal static readonly ConVar sv_region = new("sv_region", "-1", FCvar.None, "The region of the world to report this server in."); internal static readonly ConVar sv_instancebaselines = new("sv_instancebaselines", "1", FCvar.DevelopmentOnly, "Enable instanced baselines. Saves network overhead."); internal static readonly ConVar sv_stats = new("sv_stats", "1", 0, "Collect CPU usage stats"); @@ -256,8 +257,165 @@ public virtual void SetPassword(ReadOnlySpan password) { public virtual void DisconnectClient(IClient client, ReadOnlySpan reason) => client.Disconnect(reason); + class EntityWriteInfo : EntityInfo // TODO Move this + { + public bf_write Buffer; + public int ClientEntity; + public PackedEntity OldPack; + public PackedEntity NewPack; + public FrameSnapshot? FromSnapshot; // = From->GetSnapshot(); + public FrameSnapshot ToSnapshot; // = m_pTo->GetSnapshot(); + public FrameSnapshot Baseline; // the clients baseline + public BaseServer Server; // the server who writes this entity + public int FullProps; // number of properties send as full update (Enter PVS) + public bool CullProps; // filter props by clients in recipient lists + }; + public virtual void WriteDeltaEntities(BaseClient client, ClientFrame to, ClientFrame from, bf_write pBuf) { - throw new NotImplementedException(); + EntityWriteInfo u = new(); + u.Buffer = pBuf; + u.To = to; + u.ToSnapshot = to.GetSnapshot(); + u.Baseline = client.Baseline; + u.FullProps = 0; + u.Server = this; + u.ClientEntity = client.EntityIndex; + if (IsHLTV() || IsReplay()) { + // cull props only on master proxy + u.CullProps = sv.IsActive(); + } + else { + u.CullProps = true; // always cull props for players + } + + if (from != null) { + u.AsDelta = true; + u.From = from; + u.FromSnapshot = from.GetSnapshot(); + Assert(u.FromSnapshot); + } + else { + u.AsDelta = false; + u.From = null; + u.FromSnapshot = null; + } + + u.HeaderCount = 0; + + // set FromBaseline pointer if this snapshot may become a baseline update + if (client.BaselineUpdateTick == -1) { + client.BaselinesSent.ClearAll(); + to.FromBaseline = client.BaselinesSent; + } + + // Write the header + + // TRACE_PACKET(("WriteDeltaEntities (%d)\n", u.ToSnapshot.NumEntities)); + + u.Buffer.WriteUBitLong(26, Protocol.NETMSG_TYPE_BITS); + + u.Buffer.WriteUBitLong((uint)u.ToSnapshot.NumEntities, Constants.MAX_EDICT_BITS); + + if (u.AsDelta) { + u.Buffer.WriteOneBit(1); // use delta sequence + + u.Buffer.WriteLong((int)u.From.TickCount); // This is the sequence # that we are updating from. + } + else { + u.Buffer.WriteOneBit(0); // use baseline + } + + u.Buffer.WriteUBitLong((uint)client.BaselineUsed, 1); // tell client what baseline we are using + + // Store off current position + bf_write savepos = u.Buffer; + + // Save room for number of headers to parse, too + u.Buffer.WriteUBitLong(0, Constants.MAX_EDICT_BITS + Constants.DELTASIZE_BITS + 1); + + int startbit = u.Buffer.BitsWritten; + + bool bIsTracing = false;//client.IsTracing(); + if (bIsTracing) { + // client.TraceNetworkData(pBuf, "Delta Entities Overhead"); + } + + // Don't work too hard if we're using the optimized single-player mode. + if (true /*!g_pLocalNetworkBackdoor*/) { // todo + + // Iterate through the in PVS bitfields until we find an entity + // that was either in the old pack or the new pack + u.NextOldEntity(); + u.NextNewEntity(); + + // 9999 = ENTITY_SENTINEL + while ((u.OldEntity != 9999) || (u.NewEntity != 9999)) { + u.NewPack = (u.NewEntity != 9999) ? framesnapshotmanager.GetPackedEntity(u.ToSnapshot, u.NewEntity) : null; + u.OldPack = (u.OldEntity != 9999) ? framesnapshotmanager.GetPackedEntity(u.FromSnapshot, u.OldEntity) : null; + int nEntityStartBit = pBuf.BitsWritten; + + // Figure out how we want to write this entity. + // SV_DetermineUpdateType(u); + // SV_WriteEntityUpdate(u); + + if (!bIsTracing) + continue; + + switch (u.UpdateType) { + default: + case UpdateType.PreserveEnt: + break; + case UpdateType.EnterPVS: { + ReadOnlySpan eString = sv.Edicts[u.NewPack.EntityIndex].GetNetworkable().GetClassName(); + // client.TraceNetworkData(pBuf, "enter [%s]", eString); + // ETWMark1I(eString, pBuf.BitsWritten - nEntityStartBit); + } + break; + case UpdateType.LeavePVS: { + // Note, can't use GetNetworkable() since the edict has been freed at this point + ReadOnlySpan eString = u.OldPack.ServerClass.NetworkName; + // client.TraceNetworkData(pBuf, "leave [%s]", eString); + // ETWMark1I(eString, pBuf.BitsWritten - nEntityStartBit); + } + break; + case UpdateType.DeltaEnt: { + ReadOnlySpan eString = sv.Edicts[u.OldPack.EntityIndex].GetNetworkable().GetClassName(); + // client.TraceNetworkData(pBuf, "delta [%s]", eString); + // ETWMark1I(eString, pBuf.BitsWritten - nEntityStartBit); + } + break; + } + } + + // Now write out the express deletions + int nNumDeletions = 0;//SV_WriteDeletions(u); + if (bIsTracing) { + // client.TraceNetworkData(pBuf, "Delta: [%d] deletions", nNumDeletions); + } + } + + // get number of written bits + int length = u.Buffer.BitsWritten - startbit; + + // go back to header and fill in correct length now + savepos.WriteUBitLong((uint)u.HeaderCount, Constants.MAX_EDICT_BITS); + savepos.WriteUBitLong((uint)length, Constants.DELTASIZE_BITS); + + bool bUpdateBaseline = ((client.BaselineUpdateTick == -1) && + (u.FullProps > 0 || !u.AsDelta)); + + if (bUpdateBaseline && u.Baseline != null) { + // tell client to use this snapshot as baseline update + savepos.WriteOneBit(1); + client.BaselineUpdateTick = (int)to.TickCount; + } + else + savepos.WriteOneBit(0); + + if (bIsTracing) { + // client.TraceNetworkData(pBuf, "Delta Finish"); + } + } public virtual void WriteTempEntities(BaseClient client, FrameSnapshot to, FrameSnapshot from, bf_write pBuf, int nMaxEnts) { throw new NotImplementedException(); @@ -452,6 +610,7 @@ public virtual BaseClient CreateFakeClient(ReadOnlySpan name) { throw new NotImplementedException(); } public virtual void RemoveClientFromGame(BaseClient cl) { } + public virtual void SendClientMessages(bool bSendSnapshots) { for (int i = 0; i < Clients.Count; i++) { BaseClient cl = Clients[i]; diff --git a/Source.Engine/Server/GameClient.cs b/Source.Engine/Server/GameClient.cs index 43f69ba6..6c120937 100644 --- a/Source.Engine/Server/GameClient.cs +++ b/Source.Engine/Server/GameClient.cs @@ -153,7 +153,12 @@ public override void Connect(ReadOnlySpan name, int userID, INetChannel ne } } - void SetupPackInfo(FrameSnapshot snapshot) { } + public void SetupPackInfo(FrameSnapshot snapshot) { + + CurrentFrame = cl.AllocateFrame(); + CurrentFrame.Init(snapshot); + + } void SetupPrevPackInfo() { } @@ -531,7 +536,7 @@ public override bool ExecuteStringCommand(ReadOnlySpan c) { return true; } - protected override void SendSnapshot(ClientFrame frame) { + public override void SendSnapshot(ClientFrame frame) { if (HLTV) { } @@ -559,11 +564,10 @@ public override void PacketStart(int incoming_sequence, int outgoing_acknowledge public override void PacketEnd() => serverGlobalVariables.FrameTime = host_state.IntervalPerTick; - // void ConnectionClosing(ReadOnlySpan reason) { } - - // void ConnectionCrashed(ReadOnlySpan reason) { } - - // ClientFrame GetSendFrame() { } + public ClientFrame GetSendFrame() { + ClientFrame? frame = CurrentFrame; + return frame; + } // bool IgnoreTempEntity(EventInfo evnt) { } diff --git a/Source.Engine/Server/GameServer.cs b/Source.Engine/Server/GameServer.cs index 5b28b727..f21dfc12 100644 --- a/Source.Engine/Server/GameServer.cs +++ b/Source.Engine/Server/GameServer.cs @@ -28,6 +28,7 @@ public class GameServer : BaseServer protected readonly Scr Scr = Singleton(); protected readonly SV SV = Singleton(); protected readonly ICommandLine CommandLine = Singleton(); + protected readonly FrameSnapshotManager FrameSnapshotManager = Singleton(); public override void SetMaxClients(int number) { MaxClients = Math.Clamp(number, 1, MaxClientsLimit); Host.deathmatch.SetValue(MaxClients > 1); @@ -350,6 +351,60 @@ private void SetupMaxPlayers(int iDesiredMaxPlayers) { sv.SetMaxClients(newmaxplayers); } + public override void SendClientMessages(bool bSendSnapshots) { + int receivingClientCount = 0; + GameClient[] receivingClients = new GameClient[Constants.ABSOLUTE_PLAYER_LIMIT]; + + for (int i = 0; i < GetClientCount(); i++) { + GameClient client = Client(i); + + // if (!client.ShouldSendMessages()) todo + // continue; + + if (bSendSnapshots && client.IsActive()) { + receivingClients[receivingClientCount] = client; + receivingClientCount++; + } + else { + if (client.IsFakeClient()) + continue; + + if (Net.IsMultiplayer() && client.NetChannel!.GetSequenceNumber(1) == 0) { + // Net.OutOfBandPrintf(client.NetChannel.RemoteAddress, $"{(char)Protocol.S2C_CONNECTION}00000000000000"); + } + +#if SHARED_NET_STRING_TABLES + StringTables!.TriggerCallbacks(client.DeltaTick); +#endif + + client.NetChannel!.Transmit(); + // client.UpdateSendState(); + } + } + + if (receivingClientCount > 0) { + FrameSnapshot snapshot = FrameSnapshotManager.TakeTickSnapshot((int)TickCount); + // CopyTempEntities(snapshot); + + PackedEntities.ComputeClientPacks(receivingClientCount, receivingClients, snapshot); + + // if (receivingClientCount > 1 && sv_parallel_sendsnapshot.GetBool()) { + // + // } + + for (int i = 0; i < receivingClientCount; i++) { + GameClient client = receivingClients[i]; + if (client == null) + continue; + ClientFrame frame = client.GetSendFrame()!; + client.SendSnapshot(frame); + // client.UpdateSendState(); + } + + snapshot.ReleaseReference(); + } + } + int CurrentSkill; internal bool SpawnServer(ReadOnlySpan mapName, ReadOnlySpan mapFile, ReadOnlySpan startspot) { diff --git a/Source.Engine/Server/PackedEntities.cs b/Source.Engine/Server/PackedEntities.cs index adfdfa8d..cfaae7fa 100644 --- a/Source.Engine/Server/PackedEntities.cs +++ b/Source.Engine/Server/PackedEntities.cs @@ -21,6 +21,10 @@ struct PackWork static class PackedEntities { + static readonly Host Host = Singleton(); + static readonly EngineSendTable EngSendTable = Singleton(); + static readonly FrameSnapshotManager frameSnapshotManager = Singleton(); + static readonly ConVar sv_debugmanualmode = new("sv_debugmanualmode", "0", FCvar.None, "Make sure entities correctly report whether or not their network data has changed."); static readonly ConVar sv_parallel_packentities = new("sv_parallel_packentities", "0", FCvar.None); // SDN: Defaulted to 0 for now ~Callum @@ -40,7 +44,11 @@ static void EnsureInstanceBasline(ServerClass serverClass, int edictId, ReadOnly public static void PackEntity(int edictId, Edict edict, ServerClass serverClass, FrameSnapshot snapshot) { Assert(edictId < snapshot.NumEntities); -#if false // TODO TODO SendTable, ChangeFrameList, SendProxyRecipients, SV.EnsureInstanceBaseline, AllocChangeFrameList +#if DEBUG // HACK remove once transmit stuff is done! + if (serverClass == null) { + return; + } +#endif int serialNum = snapshot.Entities![edictId].SerialNumber; @@ -50,7 +58,7 @@ public static void PackEntity(int edictId, Edict edict, ServerClass serverClass, if (!edict.HasStateChanged()) { // Now this may not work if we didn't previously send a packet; // if not, then we gotta compute it - usedPrev = framesnapshotmanager.UsePreviouslySentPacket(snapshot, edictId, serialNum); + usedPrev = frameSnapshotManager.UsePreviouslySentPacket(snapshot, edictId, serialNum); } if (usedPrev && !sv_debugmanualmode.GetBool()) { @@ -64,39 +72,28 @@ public static void PackEntity(int edictId, Edict edict, ServerClass serverClass, SendTable sendTable = serverClass.Table; - // (avoid constructor overhead). - Span tempData = stackalloc byte[Unsafe.SizeOf() * Constants.MAX_DATATABLE_PROXIES]; - Span recip = MemoryMarshal.Cast(tempData); + SendProxyRecipients[] recip = new SendProxyRecipients[SendProxyRecipients.MAX_DATATABLE_PROXIES]; - if (!SendTable_Encode(sendTable, edict.GetUnknown(), &writeBuf, edictId, &recip, false)) { - Host_Error("SV_PackEntity: SendTable_Encode returned false (ent %d).\n", edictId); - } + if (!EngSendTable.Encode(sendTable, edict.GetUnknown(), writeBuf, edictId, recip, false)) + Host.Error($"SV_PackEntity: SendTable_Encode returned false (ent {edictId}).\n"); - SV_EnsureInstanceBaseline(serverClass, edictId, packedData, writeBuf.BytesWritten); + // SV.EnsureInstanceBaseline(serverClass, edictId, packedData, writeBuf.BytesWritten); TODO TODO - int nFlatProps = SendTable_GetNumFlatProps(sendTable); - IChangeFrameList? changeFrame = null; + int flatProps = EngSendTable.GetNumFlatProps(sendTable); + IChangeFrameList? changeFrame; // 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. // // If not, then we want to setup a new IChangeFrameList. - PackedEntity? prevFrame = framesnapshotmanager.GetPreviouslySentPacket(edictId, snapshot.Entities[edictId].SerialNumber); + PackedEntity? prevFrame = frameSnapshotManager.GetPreviouslySentPacket(edictId, snapshot.Entities[edictId].SerialNumber); if (prevFrame != null) { // Calculate a delta. Assert(!prevFrame.IsCompressed()); int[] deltaProps = new int[Constants.MAX_DATATABLE_PROPS]; - int changes = SendTable_CalcDelta( - sendTable, - prevFrame.GetData(), prevFrame.GetNumBits(), - packedData, writeBuf.GetNumBitsWritten(), - - deltaProps, - ARRAYSIZE(deltaProps), - - edictId); + int changes = EngSendTable.CalcDelta(sendTable, prevFrame.GetData(), prevFrame.GetNumBits(), packedData, writeBuf.BitsWritten, deltaProps, Constants.MAX_DATATABLE_PROPS, edictId); // 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). @@ -104,7 +101,7 @@ public static void PackEntity(int edictId, Edict edict, ServerClass serverClass, // are winding up with no changes. if (changes == 0) { if (prevFrame.CompareRecipients(recip)) { - if (framesnapshotmanager.UsePreviouslySentPacket(snapshot, edictId, serialNum)) { + if (frameSnapshotManager.UsePreviouslySentPacket(snapshot, edictId, serialNum)) { edict.ClearStateChanged(); return; } @@ -122,10 +119,7 @@ public static void PackEntity(int edictId, Edict edict, ServerClass serverClass, if ((prop.GetFlags() & PropFlags.EncodedAgainstTickCount) != 0) continue; - Msg("Entity %d (class '%s') reported ENTITY_CHANGE_NONE but '%s' changed.\n", - edictId, - edict.GetClassName(), - prop.GetName()); + Msg($"Entity {edictId} (class '{edict.GetClassName()}') reported ENTITY_CHANGE_NONE but '{prop.GetName()}' changed.\n"); } } } @@ -142,33 +136,32 @@ public static void PackEntity(int edictId, Edict edict, ServerClass serverClass, changeFrame = prevFrame.SnagChangeFrameList(); } - ErrorIfNot(changeFrame, ("SV_PackEntity: SnagChangeFrameList returned null")); - ErrorIfNot(changeFrame.GetNumProps() == nFlatProps, ("SV_PackEntity: SnagChangeFrameList mismatched number of props[%d vs %d]", nFlatProps, changeFrame.GetNumProps())); + ErrorIfNot(changeFrame != null, "SV_PackEntity: SnagChangeFrameList returned null"); + ErrorIfNot(changeFrame.GetNumProps() == flatProps, $"SV_PackEntity: SnagChangeFrameList mismatched number of props[{flatProps} vs {changeFrame.GetNumProps()}]"); changeFrame.SetChangeTick(deltaProps, changes, snapshot.TickCount); } else { // Ok, init the change frames for the first time. - changeFrame = AllocChangeFrameList(nFlatProps, snapshot.TickCount); + changeFrame = ChangeFrameList.AllocChangeFrameList(flatProps, snapshot.TickCount); } // Now make a PackedEntity and store the new packed data in there. - PackedEntity packedEntity = framesnapshotmanager.CreatePackedEntity(snapshot, edictId); + PackedEntity packedEntity = frameSnapshotManager.CreatePackedEntity(snapshot, edictId); packedEntity.SetChangeFrameList(changeFrame); packedEntity.SetServerAndClientClass(serverClass, null); packedEntity.AllocAndCopyPadded(packedData); packedEntity.SetRecipients(recip); edict.ClearStateChanged(); -#endif } static void FillHLTVData(FrameSnapshot snapshot, Edict edict, int validEdict) { - throw new NotImplementedException(); + // throw new NotImplementedException(); } static void FillReplayData(FrameSnapshot snapshot, Edict edict, int validEdict) { - throw new NotImplementedException(); + // throw new NotImplementedException(); } static SendTable GetEntSendTable(Edict edict) { @@ -186,6 +179,7 @@ static void Normal(int clientCount, GameClient[] clients, FrameSnapshot snapshot // check for all active entities, if they are seen by at least on client, if // so, bit pack them + // Console.WriteLine($"Packing entities for snapshot {snapshot.TickCount} with {snapshot.NumValidEntities} valid entities."); for (int iValidEdict = 0; iValidEdict < snapshot.NumValidEntities; ++iValidEdict) { int index = snapshot.ValidEntities![iValidEdict]; @@ -224,7 +218,7 @@ static void Normal(int clientCount, GameClient[] clients, FrameSnapshot snapshot Debugger.Break(); } else { - int c = workItems.Count(); + int c = workItems.Count; for (int i = 0; i < c; ++i) { PackWork w = workItems[i]; PackEntity(w.Id, w.Edict, w.Snapshot.Entities![w.Id].Class!, w.Snapshot); @@ -234,15 +228,27 @@ static void Normal(int clientCount, GameClient[] clients, FrameSnapshot snapshot // InvalidateSharedEdictChangeInfos(); todo } - // todo call from CGameServer::SendClientMessages - static void ComputeClientPacks(int clientCount, GameClient[] clients, FrameSnapshot snapshot) { + public static void ComputeClientPacks(int clientCount, GameClient[] clients, FrameSnapshot snapshot) { for (int i = 0; i < clientCount; i++) { // todo transmit info + + clients[i].SetupPackInfo(snapshot); + +#if DEBUG // HACK until transmit stuff is done! + for (int j = 0; j < snapshot.NumValidEntities; j++) { + int index = snapshot.ValidEntities![j]; + Edict edict = sv.Edicts![index]; + + if (clients[i].CurrentFrame!.TransmitEntity.Get(index) == 0) { + clients[i].CurrentFrame!.TransmitEntity.Set(index); + } + } +#endif + } if (false /* g_LocalNetworkBackdoor */) { - } else Normal(clientCount, clients, snapshot); From c2f64eaf4943309f5e8dec092d16be3d777c3b8b Mon Sep 17 00:00:00 2001 From: callumok2004 Date: Wed, 4 Mar 2026 07:18:48 +0000 Subject: [PATCH 23/31] Cleanup some stuff --- Game.Server/GameInterface.cs | 12 +++++++---- Source.Engine/CL.cs | 2 +- Source.Engine/PrEdict.cs | 3 ++- Source.Engine/Server/BaseServer.cs | 32 ++++++++++-------------------- Source.Engine/Server/EntsWrite.cs | 17 ++++++++++++++++ 5 files changed, 38 insertions(+), 28 deletions(-) create mode 100644 Source.Engine/Server/EntsWrite.cs diff --git a/Game.Server/GameInterface.cs b/Game.Server/GameInterface.cs index 4a5f96d9..dd815ed1 100644 --- a/Game.Server/GameInterface.cs +++ b/Game.Server/GameInterface.cs @@ -416,6 +416,8 @@ public void Think(bool finalTick) { public class ServerGameClients : IServerGameClients { + const int CMD_MAXBACKUP = 64; + public void ClientActive(Edict entity, bool loadGame) { GMODClient.ClientActive(entity, loadGame); @@ -491,7 +493,7 @@ public TimeUnit_t ProcessUsercmds(Edict player, bf_read buf, int numCmds, int to UserCmd from, to; - UserCmd[] cmds = new UserCmd[64]; // CMD_MAXBACKUP + UserCmd[] cmds = new UserCmd[CMD_MAXBACKUP]; UserCmd cmdNull = new(); @@ -504,7 +506,7 @@ public TimeUnit_t ProcessUsercmds(Edict player, bf_read buf, int numCmds, int to if (ent != null && ent.IsPlayer()) pl = (BasePlayer)ent; - if (totalCmds < 0 || totalCmds >= (64 /*CMD_MAXBACKUP*/ - 1)) { + if (totalCmds < 0 || totalCmds >= (CMD_MAXBACKUP - 1)) { ReadOnlySpan name = "unknown"; if (pl != null) name = pl.GetPlayerName(); @@ -553,8 +555,10 @@ public void SetDebugEdictBase(Edict[] edict) { struct MapEntityRef { - public int Edict; // Which edict slot this entity got. -1 if CreateEntityByName failed. - public int SerialNumber; // The edict serial number. TODO used anywhere ? + /// Which edict slot this entity got. -1 if CreateEntityByName failed. + public int Edict; + /// The edict serial number. + public int SerialNumber; }; diff --git a/Source.Engine/CL.cs b/Source.Engine/CL.cs index 232be9f9..59810f08 100644 --- a/Source.Engine/CL.cs +++ b/Source.Engine/CL.cs @@ -248,7 +248,7 @@ public void FullyConnected() { // MDL cache end map load if (Host.developer.GetInt() > 0) - ConDMsg($"Signon traffic \"{cl.NetChannel.GetName()}\": incoming {cl.NetChannel.GetTotalData(1 /*FLOW_INCOMING*/)}, outgoing {cl.NetChannel.GetTotalData(0 /*FLOW_OUTGOING*/)}\n"); + ConDMsg($"Signon traffic \"{cl.NetChannel.GetName()}\": incoming {cl.NetChannel.GetTotalData(NetFlow.FLOW_INCOMING)}, outgoing {cl.NetChannel.GetTotalData(NetFlow.FLOW_OUTGOING)}\n"); Scr.EndLoadingPlaque(); // EndLoadingUpdates(); diff --git a/Source.Engine/PrEdict.cs b/Source.Engine/PrEdict.cs index cb6490fa..2de8e00f 100644 --- a/Source.Engine/PrEdict.cs +++ b/Source.Engine/PrEdict.cs @@ -5,6 +5,7 @@ namespace Source.Engine; public class ED { + const float EDICT_FREETIME = 1.0f; static readonly ConVar sv_useexplicitdelete = new("1", FCvar.DevelopmentOnly, "Explicitly delete dormant client entities caused by AllowImmediateReuse()."); static readonly ConVar sv_lowEdicthreshold = new("8", FCvar.None, "When only this many edicts are free, take the action specified by sv_lowedict_action.", 0, Constants.MAX_EDICTS); @@ -45,7 +46,7 @@ public class ED // If this assert goes off, someone most likely called pedict.ClearFree() and not ED_ClearFreeFlag()? Assert(edict.IsFree()); Assert(bit == edict.EdictIndex); - if ((edict.FreeTime < 2) || (sv.GetTime() - edict.FreeTime >= 1.0 /*EDICT_FREETIME*/)) { + if ((edict.FreeTime < 2) || (sv.GetTime() - edict.FreeTime >= EDICT_FREETIME)) { // If we have no freetime, we've had AllowImmediateReuse() called. We need // to explicitly delete this old entity. if (edict.FreeTime == 0 && sv_useexplicitdelete.GetBool()) { diff --git a/Source.Engine/Server/BaseServer.cs b/Source.Engine/Server/BaseServer.cs index c40b2b18..5102204b 100644 --- a/Source.Engine/Server/BaseServer.cs +++ b/Source.Engine/Server/BaseServer.cs @@ -257,29 +257,17 @@ public virtual void SetPassword(ReadOnlySpan password) { public virtual void DisconnectClient(IClient client, ReadOnlySpan reason) => client.Disconnect(reason); - class EntityWriteInfo : EntityInfo // TODO Move this - { - public bf_write Buffer; - public int ClientEntity; - public PackedEntity OldPack; - public PackedEntity NewPack; - public FrameSnapshot? FromSnapshot; // = From->GetSnapshot(); - public FrameSnapshot ToSnapshot; // = m_pTo->GetSnapshot(); - public FrameSnapshot Baseline; // the clients baseline - public BaseServer Server; // the server who writes this entity - public int FullProps; // number of properties send as full update (Enter PVS) - public bool CullProps; // filter props by clients in recipient lists - }; - public virtual void WriteDeltaEntities(BaseClient client, ClientFrame to, ClientFrame from, bf_write pBuf) { - EntityWriteInfo u = new(); - u.Buffer = pBuf; - u.To = to; - u.ToSnapshot = to.GetSnapshot(); - u.Baseline = client.Baseline; - u.FullProps = 0; - u.Server = this; - u.ClientEntity = client.EntityIndex; + EntityWriteInfo u = new() { + Buffer = pBuf, + To = to, + ToSnapshot = to.GetSnapshot(), + Baseline = client.Baseline, + FullProps = 0, + Server = this, + ClientEntity = client.EntityIndex + }; + if (IsHLTV() || IsReplay()) { // cull props only on master proxy u.CullProps = sv.IsActive(); diff --git a/Source.Engine/Server/EntsWrite.cs b/Source.Engine/Server/EntsWrite.cs new file mode 100644 index 00000000..4c007aca --- /dev/null +++ b/Source.Engine/Server/EntsWrite.cs @@ -0,0 +1,17 @@ +using Source.Common.Bitbuffers; + +namespace Source.Engine.Server; + +class EntityWriteInfo : EntityInfo +{ + public bf_write Buffer; + public int ClientEntity; + public PackedEntity OldPack; + public PackedEntity NewPack; + public FrameSnapshot? FromSnapshot; // = From->GetSnapshot(); + public FrameSnapshot ToSnapshot; // = m_pTo->GetSnapshot(); + public FrameSnapshot Baseline; // the clients baseline + public BaseServer Server; // the server who writes this entity + public int FullProps; // number of properties send as full update (Enter PVS) + public bool CullProps; // filter props by clients in recipient lists +}; \ No newline at end of file From 77d998d1946aaab68d5b3896d4137a7cfe23f2e9 Mon Sep 17 00:00:00 2001 From: callumok2004 Date: Thu, 5 Mar 2026 18:33:55 +0000 Subject: [PATCH 24/31] Allow server to run commands --- Source.Engine/Cmd.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Source.Engine/Cmd.cs b/Source.Engine/Cmd.cs index 775997a9..350ce6e7 100644 --- a/Source.Engine/Cmd.cs +++ b/Source.Engine/Cmd.cs @@ -92,9 +92,10 @@ private void HandleExecutionMarker(ReadOnlySpan command, ReadOnlySpan Date: Thu, 5 Mar 2026 18:42:18 +0000 Subject: [PATCH 25/31] CommandClient stuff --- Game.Server/GameInterface.cs | 5 ++--- Game.Server/Util.cs | 13 +++++++++++-- Source.Engine/Cmd.cs | 13 +++++++------ 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/Game.Server/GameInterface.cs b/Game.Server/GameInterface.cs index dd815ed1..d604533a 100644 --- a/Game.Server/GameInterface.cs +++ b/Game.Server/GameInterface.cs @@ -533,9 +533,8 @@ public TimeUnit_t ProcessUsercmds(Edict player, bf_read buf, int numCmds, int to return TICK_INTERVAL; } - public void SetCommandClient(int index) { - throw new NotImplementedException(); - } + public static int CommandClientIndex = 0; + public void SetCommandClient(int index) => CommandClientIndex = index; } public class ServerGameEnts : IServerGameEnts diff --git a/Game.Server/Util.cs b/Game.Server/Util.cs index 2181c954..c7e350f6 100644 --- a/Game.Server/Util.cs +++ b/Game.Server/Util.cs @@ -188,8 +188,7 @@ public static void ClientPrintFilter(scoped in Filter filter, HudPrint d } public static bool IsCommandIssuedByServerAdmin() { - // int issuingPlayerIndex = GetCommandClientIndex(); - int issuingPlayerIndex = 0; // TODO TODO + int issuingPlayerIndex = GetCommandClientIndex(); if (engine.IsDedicatedServer() && issuingPlayerIndex > 0) return false; @@ -240,4 +239,14 @@ public static int DispatchSpawn(BaseEntity entity) { public static void Remove(BaseEntity entity) { throw new NotImplementedException(); } + + public static int GetCommandClientIndex() => ServerGameClients.CommandClientIndex; + + public static BasePlayer? GetCommandClient() { + int id = GetCommandClientIndex(); + if (id > 0) + return PlayerByIndex(id)!; + + return null; + } } diff --git a/Source.Engine/Cmd.cs b/Source.Engine/Cmd.cs index 350ce6e7..92c55148 100644 --- a/Source.Engine/Cmd.cs +++ b/Source.Engine/Cmd.cs @@ -77,7 +77,11 @@ private void HandleExecutionMarker(ReadOnlySpan command, ReadOnlySpan command, ReadOnlySpan Date: Thu, 5 Mar 2026 18:58:42 +0000 Subject: [PATCH 26/31] Fix command completions --- Source.GUI.Controls/ConsoleDialog.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Source.GUI.Controls/ConsoleDialog.cs b/Source.GUI.Controls/ConsoleDialog.cs index 513d40f8..0b8878a6 100644 --- a/Source.GUI.Controls/ConsoleDialog.cs +++ b/Source.GUI.Controls/ConsoleDialog.cs @@ -280,7 +280,11 @@ private void ClearCompletionList() { Span command = stackalloc char[256]; strcpy(command, text); - ConCommand? cmd = Cvar.FindCommand(command); + int space = command.IndexOf(' '); + if (space != -1) + command[space] = '\0'; + + ConCommand? cmd = Cvar.FindCommand(command.SliceNullTerminatedString()); if (cmd == null) return null; @@ -316,7 +320,7 @@ private void RebuildCompletionList(ReadOnlySpan text) { NormalBuild = false; - IEnumerable commands = cmd.AutoCompleteSuggest(text.ToString()); + IEnumerable commands = cmd.AutoCompleteSuggest(text[..len].ToString()); int count = commands.Count(); //Assert(count <= COMMAND_COMPLETION_MAXITEMS); Assert(count <= 64); From bab9e88e353c6946416ddbb4dab37c05649c75ee Mon Sep 17 00:00:00 2001 From: callumok2004 Date: Thu, 5 Mar 2026 19:06:20 +0000 Subject: [PATCH 27/31] temp autocomplete for map command --- Source.Engine/Host.cs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/Source.Engine/Host.cs b/Source.Engine/Host.cs index 48ea9ed6..fcb8fa55 100644 --- a/Source.Engine/Host.cs +++ b/Source.Engine/Host.cs @@ -873,11 +873,28 @@ public bool ChangeLevel(bool loadFromSavedGame, ReadOnlySpan levelName, Re return true; } - [ConCommand("map", "Start playing on specified map.", FCvar.DontRecord)] + [ConCommand("map", "Start playing on specified map.", FCvar.DontRecord, autoCompleteMethod: nameof(Map_CompletionFunc))] public void Map_f(in TokenizedCommand args, CommandSource source, int clientSlot = -1) { Map_Helper(in args, source, false, false, false); } + IEnumerable Map_CompletionFunc(string partial) { // todo: properly implement this once host maplist stuff is done + int space = partial.IndexOf(' '); + string prefix = space >= 0 ? partial[..(space + 1)] : "map "; + string arg = space >= 0 ? partial[(space + 1)..] : ""; + + ReadOnlySpan filename = fileSystem.FindFirstEx("maps/*.bsp", null, out FileFindHandle_t findHandle); + while (!filename.IsEmpty) { + Span mapName = stackalloc char[256]; + filename.StripExtension(mapName); + string name = mapName.SliceNullTerminatedString().ToString(); + if (name.StartsWith(arg, StringComparison.OrdinalIgnoreCase)) + yield return prefix + name; + filename = fileSystem.FindNext(findHandle); + } + fileSystem.FindClose(findHandle); + } + private void Map_Helper(in TokenizedCommand args, CommandSource source, bool editmode, bool background, bool commentary) { if (source != CommandSource.Command) return; From a855388344a1fcf9475a870fd56ec0761994346a Mon Sep 17 00:00:00 2001 From: callumok2004 Date: Thu, 5 Mar 2026 19:24:04 +0000 Subject: [PATCH 28/31] Cleanups --- Source.Engine/PerfUIPanel.cs | 18 ++++++++++-------- Source.Engine/VGui_BudgetPanel.cs | 4 ++-- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/Source.Engine/PerfUIPanel.cs b/Source.Engine/PerfUIPanel.cs index 2de3d51a..df52ff7b 100644 --- a/Source.Engine/PerfUIPanel.cs +++ b/Source.Engine/PerfUIPanel.cs @@ -1,4 +1,5 @@ using Source.Common.Commands; +using Source.Engine; using Source.GUI.Controls; // TODO: Remove uses of ConVarRef when those cvars exist @@ -279,10 +280,10 @@ public PerfUIPanel(Panel parent) : base(parent, "PerfUIPanel") { int w = 250; int h = 400; - // int x = videomode->GetModeStereoWidth() - w - 10; - // int y = (videomode->GetModeStereoHeight() - h) / 2 + videomode->GetModeStereoHeight() * 0.2; - int x = 1600 - w - 10; - int y = (int)((900 - h) / 2 + 900 * 0.2); + VideoMode_Common vm = (VideoMode_Common)videoMode; + int x = vm.GetModeStereoWidth() - w - 10; + int y = (int)((vm.GetModeStereoHeight() - h) / 2 + vm.GetModeStereoHeight() * 0.2); + SetBounds(x, y, w, h); ToolPanels[(int)PerformanceTool_t.PERF_TOOL_NONE] = new PerfUIChildPanel(this, "PerfNone"); @@ -326,8 +327,9 @@ private void PopulateControls() { } public override void OnTick() { - // if (!CanCheat()) - // Shutdown(); + if (!Host.CanCheat()) + Shutdown(); + base.OnTick(); } @@ -353,8 +355,8 @@ private void OnPerfToolSelected() { } public override void Activate() { - // if (!CanCheat()) - // return; + if (!Host.CanCheat()) + return; Init(); base.Activate(); diff --git a/Source.Engine/VGui_BudgetPanel.cs b/Source.Engine/VGui_BudgetPanel.cs index 15d3f0ae..87a5eeda 100644 --- a/Source.Engine/VGui_BudgetPanel.cs +++ b/Source.Engine/VGui_BudgetPanel.cs @@ -45,8 +45,8 @@ void UserCmd_HideBudgetPanel() { } public override void OnTick() { - // if (ShowBudgetPanelHeld && !CanCheat()) // todo - // UserCmd_HideBudgetPanel(); + if (ShowBudgetPanelHeld && !Host.CanCheat()) + UserCmd_HideBudgetPanel(); base.OnTick(); SetVisible(ShowBudgetPanelHeld); From c12157935c53b735d1e17bd7484dd656cc7b0183 Mon Sep 17 00:00:00 2001 From: callumok2004 Date: Fri, 6 Mar 2026 18:10:39 +0000 Subject: [PATCH 29/31] More server impl --- Game.Server/Lights.cs | 16 +- Game.Server/PointEntity.cs | 4 +- Game.Server/ServerNetworkProperty.cs | 3 +- Source.Common/Bitbuffers/bf_write.cs | 13 + Source.Common/Networking/NetMessages.cs | 9 +- Source.Common/Networking/Protocol.cs | 7 +- Source.Common/ServerClass.cs | 8 +- Source.Engine/Client/BaseClientState.cs | 2 +- Source.Engine/Client/ClientState.cs | 4 +- Source.Engine/DtCommonEng.cs | 22 +- Source.Engine/EngineSendTable.cs | 7 +- Source.Engine/EntsShared.cs | 6 +- Source.Engine/GameEventManager.cs | 3 +- Source.Engine/Net.cs | 12 +- Source.Engine/NetworkStringTable.cs | 26 ++ Source.Engine/PackedEntity.cs | 1 + Source.Engine/SV.cs | 69 ++++++ Source.Engine/Server/BaseClient.cs | 294 +++++++++++++++++++++-- Source.Engine/Server/BaseServer.cs | 72 +++--- Source.Engine/Server/EntsWrite.cs | 300 +++++++++++++++++++++++- Source.Engine/Server/GameClient.cs | 68 +----- Source.Engine/Server/GameServer.cs | 17 +- Source.Engine/Server/PackedEntities.cs | 18 +- 23 files changed, 835 insertions(+), 146 deletions(-) diff --git a/Game.Server/Lights.cs b/Game.Server/Lights.cs index d0811901..95ed52dc 100644 --- a/Game.Server/Lights.cs +++ b/Game.Server/Lights.cs @@ -2,12 +2,12 @@ namespace Game.Server; -[LinkEntityToClass("light")] -class Light : PointEntity -{ -} +// [LinkEntityToClass("light")] +// class Light : PointEntity +// { +// } -[LinkEntityToClass("light_environment")] -class EnvLight : Light -{ -} \ No newline at end of file +// [LinkEntityToClass("light_environment")] +// class EnvLight : Light +// { +// } \ No newline at end of file diff --git a/Game.Server/PointEntity.cs b/Game.Server/PointEntity.cs index db2794ea..b08147bb 100644 --- a/Game.Server/PointEntity.cs +++ b/Game.Server/PointEntity.cs @@ -1,10 +1,12 @@ using Game.Shared; +using Source.Common; + namespace Game.Server; public class PointEntity : BaseEntity { - + public static readonly new ServerClass ServerClass = new ServerClass("PointEntity", DT_BaseEntity); } [LinkEntityToClass("info_player_start")] diff --git a/Game.Server/ServerNetworkProperty.cs b/Game.Server/ServerNetworkProperty.cs index 52fb123f..bc47313e 100644 --- a/Game.Server/ServerNetworkProperty.cs +++ b/Game.Server/ServerNetworkProperty.cs @@ -34,8 +34,7 @@ public ReadOnlySpan GetClassName() { } public ServerClass GetServerClass() { - // if (ServerClass == null) todo - // ServerClass = Outer.GetServerClass(); + ServerClass ??= ServerClassRetriever.GetOrError(Outer.GetType()); return ServerClass; } diff --git a/Source.Common/Bitbuffers/bf_write.cs b/Source.Common/Bitbuffers/bf_write.cs index 5ee46304..b43a9a85 100644 --- a/Source.Common/Bitbuffers/bf_write.cs +++ b/Source.Common/Bitbuffers/bf_write.cs @@ -116,6 +116,19 @@ public void WriteSBitLong(int data, int numbits) { #endif } + public void WriteUBitVar(uint data) { + if (data < 0x10u) + WriteUBitLong((data << 2) | 0, 6); + else if (data < 0x100u) + WriteUBitLong((data << 2) | 1, 10); + else if (data < 0x1000u) + WriteUBitLong((data << 2) | 2, 14); + else { + WriteUBitLong(3, 2); + WriteUBitLong(data, 32); + } + } + public void WriteVarInt32(uint data) { if ((curBit & 7) == 0 && curBit + (nint)MaxVarInt32Bytes * 8 <= dataBits) { fixed (byte* pBuf = this.data) { diff --git a/Source.Common/Networking/NetMessages.cs b/Source.Common/Networking/NetMessages.cs index 6b7f8e13..e5cd4600 100644 --- a/Source.Common/Networking/NetMessages.cs +++ b/Source.Common/Networking/NetMessages.cs @@ -594,7 +594,14 @@ public override bool ReadFromBuffer(bf_read buffer) { } public override bool WriteToBuffer(bf_write buffer) { - throw new Exception(); + Length = DataOut.BitsWritten; + if (Length >= (1 << 20)) + return false; + + buffer.WriteNetMessageType(this); + buffer.WriteUBitLong((uint)NumEvents, MAX_EVENT_BITS); + buffer.WriteUBitLong((uint)Length, 20); + return buffer.WriteBits(DataOut.BaseArray, Length); } } public class SVC_SetView : NetMessage diff --git a/Source.Common/Networking/Protocol.cs b/Source.Common/Networking/Protocol.cs index 87d1c003..c63f9b83 100644 --- a/Source.Common/Networking/Protocol.cs +++ b/Source.Common/Networking/Protocol.cs @@ -6,7 +6,7 @@ public static class C2S } public static class S2C { - public const uint MagicVersion = 1515145523; + public const uint MagicVersion = 1515145523; public const byte Challenge = (byte)'A'; public const byte Connection = (byte)'B'; @@ -248,4 +248,9 @@ public static class Protocol public const int PROTOCOL_VERSION_17 = 17; public const int PROTOCOL_VERSION_14 = 14; public const int PROTOCOL_VERSION_12 = 12; + + public const int FHDR_ZERO = 0x0000; + public const int FHDR_LEAVEPVS = 0x0001; + public const int FHDR_DELETE = 0x0002; + public const int FHDR_ENTERPVS = 0x0004; } diff --git a/Source.Common/ServerClass.cs b/Source.Common/ServerClass.cs index 8a375b66..82ba7cda 100644 --- a/Source.Common/ServerClass.cs +++ b/Source.Common/ServerClass.cs @@ -1,4 +1,6 @@ -using System.Reflection; +using Source.Common.Engine; + +using System.Reflection; using System.Runtime.CompilerServices; namespace Source.Common; @@ -11,7 +13,7 @@ public static ServerClass GetOrError(Type t) { if (ClassList.TryGetValue(t, out ServerClass? c)) return c; - FieldInfo? field = t.GetField(nameof(ServerClass), BindingFlags.Static | BindingFlags.Public); + FieldInfo? field = t.GetField(nameof(ServerClass), BindingFlags.Static | BindingFlags.Public | BindingFlags.FlattenHierarchy); if (field == null) throw new NullReferenceException(nameof(field)); @@ -28,7 +30,7 @@ public class ServerClass public SendTable Table; public ServerClass? Next; public int ClassID; - public int InstanceBaselineIndex; + public int InstanceBaselineIndex = INetworkStringTable.INVALID_STRING_INDEX; public ServerClass(ReadOnlySpan networkName, SendTable table, [CallerArgumentExpression(nameof(table))] string? nameOfTable = null) { NetworkName = new(networkName); Table = table; diff --git a/Source.Engine/Client/BaseClientState.cs b/Source.Engine/Client/BaseClientState.cs index 2e3f556c..bbdac77a 100644 --- a/Source.Engine/Client/BaseClientState.cs +++ b/Source.Engine/Client/BaseClientState.cs @@ -67,7 +67,7 @@ public abstract class BaseClientState( public InlineArray2> EntityBaselines; - public C_ServerClassInfo[] ServerClasses = new C_ServerClassInfo[Constants.TEMP_TOTAL_SERVER_CLASSES]; + public C_ServerClassInfo[]? ServerClasses = new C_ServerClassInfo[Constants.TEMP_TOTAL_SERVER_CLASSES]; public int NumServerClasses = Constants.TEMP_TOTAL_SERVER_CLASSES; public int ServerClassBits; public InlineArraySteamKeysize EncryptionKey; diff --git a/Source.Engine/Client/ClientState.cs b/Source.Engine/Client/ClientState.cs index e8ac6c71..b8220b19 100644 --- a/Source.Engine/Client/ClientState.cs +++ b/Source.Engine/Client/ClientState.cs @@ -158,7 +158,7 @@ public override void Clear() { DownloadResources = false; PrepareClientDLL = false; - // DeleteClientFrames(-1); + DeleteClientFrames(-1); ViewAngles.Init(); LastServerTickTime = 0.0; OldTickCount = 0; @@ -898,7 +898,7 @@ internal void DeleteClientFrames(int tick) { } } - private void RemoveOldestFrame() { + public void RemoveOldestFrame() { ClientFrame? frame = Frames; if (frame == null) diff --git a/Source.Engine/DtCommonEng.cs b/Source.Engine/DtCommonEng.cs index 4f19edfb..4d749df4 100644 --- a/Source.Engine/DtCommonEng.cs +++ b/Source.Engine/DtCommonEng.cs @@ -152,13 +152,27 @@ private bool SetupReceiveTableFromSendTable(SendTable sendTable, bool needsDecod internal void CreateClientClassInfosFromServerClasses(BaseClientState state) { ServerClass? classes = serverGameDLL.GetAllServerClasses(); - state.NumServerClasses = Constants.TEMP_TOTAL_SERVER_CLASSES; + int nClasses = 0; + for (ServerClass? cur = classes; cur != null; cur = cur.Next) + nClasses++; + + state.ServerClasses = null; + + Assert(nClasses > 0); + + state.NumServerClasses = nClasses; state.ServerClasses = new C_ServerClassInfo[state.NumServerClasses]; + if (state.ServerClasses == null) { + Host.EndGame(true, $"CL_ParseClassInfo: can't allocate {state.NumServerClasses} C_ServerClassInfos.\n"); + return; + } + for (ServerClass? svclass = classes; svclass != null; svclass = svclass.Next) { - state.ServerClasses[svclass.ClassID] = new(); - state.ServerClasses[svclass.ClassID].ClassName = svclass.NetworkName; - state.ServerClasses[svclass.ClassID].DatatableName = new(svclass.Table.GetName()); + state.ServerClasses[svclass.ClassID] = new() { + ClassName = svclass.NetworkName, + DatatableName = new(svclass.Table.GetName()) + }; } } } diff --git a/Source.Engine/EngineSendTable.cs b/Source.Engine/EngineSendTable.cs index b4aac854..3ef4f8bf 100644 --- a/Source.Engine/EngineSendTable.cs +++ b/Source.Engine/EngineSendTable.cs @@ -96,10 +96,10 @@ private void CalcNextVectorElems(SendTable table) { } } - public bool Encode(SendTable table, object data, bf_write dataOut, int objectId, SendProxyRecipients[] recipients, bool nonZeroOnly) { + public bool Encode(SendTable table, object data, bf_write dataOut, int objectId, SendProxyRecipients[]? recipients, bool nonZeroOnly) { SendTablePrecalc precalc = table.Precalc!; ErrorIfNot(precalc != null, $"SendTable_Encode: Missing precalc for table {table.NetTableName}."); - if (recipients.Length > 0) + if (recipients?.Length > 0) ErrorIfNot(recipients.Length >= precalc.GetNumDataTableProxies(), $"SendTable_Encode: recipients array too small (got {recipients.Length}, need {precalc.GetNumDataTableProxies()})."); EncodeInfo info = new(precalc, data, objectId, dataOut) { @@ -123,6 +123,9 @@ public bool Encode(SendTable table, object data, bf_write dataOut, int objectId, return !dataOut.Overflowed; } + public void WritePropList(SendTable table, byte[]? fromState, int nFromBits, byte[] toState, int nToBits, bf_write dataOut, int objectId, int[] checkProps) { + DevMsg($"TODO: WritePropList: table={table.NetTableName}\n"); + } bool IsPropZero(EncodeInfo info, int _) { SendProp p = info.GetCurProp()!; diff --git a/Source.Engine/EntsShared.cs b/Source.Engine/EntsShared.cs index 12a6fca4..7ff27a0f 100644 --- a/Source.Engine/EntsShared.cs +++ b/Source.Engine/EntsShared.cs @@ -19,16 +19,16 @@ public void NextOldEntity() { OldEntity = From.TransmitEntity.FindNextSetBit(OldEntity + 1); if (OldEntity < 0) - OldEntity = int.MaxValue; + OldEntity = PackedEntity.ENTITY_SENTINEL; } else - OldEntity = int.MaxValue; + OldEntity = PackedEntity.ENTITY_SENTINEL; } public void NextNewEntity() { NewEntity = To!.TransmitEntity.FindNextSetBit(NewEntity + 1); if (NewEntity < 0) - NewEntity = int.MaxValue; + NewEntity = PackedEntity.ENTITY_SENTINEL; } public virtual void Reset() { diff --git a/Source.Engine/GameEventManager.cs b/Source.Engine/GameEventManager.cs index 58253e5d..bd1983d9 100644 --- a/Source.Engine/GameEventManager.cs +++ b/Source.Engine/GameEventManager.cs @@ -447,7 +447,8 @@ internal void WriteListenEventList(CLC_ListenEvents msg) { continue; if (descriptor.EventID == -1) { - DevMsg($"Warning! Client listens to event '{descriptor.Name}' unknown by server.\n"); + ReadOnlySpan name = descriptor.Name; + DevMsg($"Warning! Client listens to event '{name.SliceNullTerminatedString()}' unknown by server.\n"); continue; } diff --git a/Source.Engine/Net.cs b/Source.Engine/Net.cs index 8087cf54..913d0ae2 100644 --- a/Source.Engine/Net.cs +++ b/Source.Engine/Net.cs @@ -53,7 +53,7 @@ public Net() { public readonly NetAddress LocalAdr = new(); - + public List SplitPackets = []; @@ -293,7 +293,7 @@ public bool GetLoopPacket(NetPacket packet) { if (packet.Source > NetSocketType.Server) return false; - if (!Loopbacks[(int)packet.Source].TryDequeue(out Loopback? loop)) + if (!Loopbacks[(int)packet.Source].TryDequeue(out Loopback? loop)) return false; if (loop.Length == 0) { @@ -305,10 +305,10 @@ public bool GetLoopPacket(NetPacket packet) { packet.Size = loop.Length; packet.WireSize = loop.Length; memcpy(packet.Data, loop.Data.AsSpan()[..packet.Size]); - loop.Length = 0; + loop.Length = 0; if (loop.Data != loop.DefBuffer) { - ArrayPool.Shared.Return(loop.Data!, true); + ArrayPool.Shared.Return(loop.Data!, true); // FIXME: 'The buffer is not associated with this pool and may not be returned to it.' loop.Data = loop.DefBuffer; } @@ -1013,7 +1013,7 @@ public unsafe int SendPacket(NetChannel chan, NetSocketType sock, NetAddress? to else ret = -1; - end: + end: if (ret == -1) { Warning("Net.SendPacket went wrong!!!\n"); ret = length; @@ -1119,7 +1119,7 @@ public unsafe void SendLoopPacket(NetSocketType sock, int length, byte[] data, N Loopback loop = ObjectPool.Shared.Alloc(); - if (length <= DEF_LOOPBACK_SIZE) + if (length <= DEF_LOOPBACK_SIZE) loop.Data = loop.DefBuffer; else loop.Data = new byte[length]; diff --git a/Source.Engine/NetworkStringTable.cs b/Source.Engine/NetworkStringTable.cs index 09e516db..18293fbb 100644 --- a/Source.Engine/NetworkStringTable.cs +++ b/Source.Engine/NetworkStringTable.cs @@ -781,4 +781,30 @@ public void WriteBaselines(bf_write buf) { public void SetAllowCreation(bool state) { AllowCreation = state; } + + public void WriteUpdateMessage(BaseClient? client, int tickAck, bf_write buf) { + byte[] msg_buffer = new byte[Protocol.MAX_PAYLOAD]; + + for (int i = 0; i < Tables.Count; i++) { + NetworkStringTable table = (NetworkStringTable)GetTable(i)!; + + if (!table.ChangedSinceTick(tickAck)) + continue; + + SVC_UpdateStringTable msg = new() { + TableID = table.GetTableId() + }; + msg.ChangedEntries = table.WriteUpdate(client, msg.DataOut, tickAck); + + if (msg.ChangedEntries <= 0) + continue; + + msg.DataOut.StartWriting(msg_buffer, Protocol.MAX_PAYLOAD, 0); + if (!msg.WriteToBuffer(buf)) + Host.Error($"Overflow error writing string table update for {table.GetTableName()}\n"); + + if (client != null && client.Tracing != 0) + client.TraceNetworkMsg(0, $"Sent update for string table {table.GetTableName()} with {msg.ChangedEntries} changed entries\n"); + } + } } diff --git a/Source.Engine/PackedEntity.cs b/Source.Engine/PackedEntity.cs index 9eb40989..36d4da0f 100644 --- a/Source.Engine/PackedEntity.cs +++ b/Source.Engine/PackedEntity.cs @@ -7,6 +7,7 @@ namespace Source.Engine; public class PackedEntity { + public const int ENTITY_SENTINEL = 9999; public const int FLAG_IS_COMPRESSED = 1 << 31; public ServerClass? ServerClass; public ClientClass? ClientClass; diff --git a/Source.Engine/SV.cs b/Source.Engine/SV.cs index bcae2b81..a7765d06 100644 --- a/Source.Engine/SV.cs +++ b/Source.Engine/SV.cs @@ -2,8 +2,10 @@ using Microsoft.Extensions.DependencyInjection; using Source.Common; +using Source.Common.Bitbuffers; using Source.Common.Commands; using Source.Common.Engine; +using Source.Common.Networking; using Source.Common.Server; using Source.Engine.Server; @@ -237,8 +239,75 @@ internal bool ActivateServer() { return true; } + static readonly EngineSendTable EngSendTable = Singleton(); private void CreateBaseline() { + // WriteVoiceCodec(sv.Signon); + ServerClass? pClasses = serverGameDLL.GetAllServerClasses(); + + if (sv_sendtables.GetBool()) { + sv.FullSendTablesBuffer.EnsureCapacity(288000 /*NET_MAX_PAYLOAD*/); + sv.FullSendTables.StartWriting(sv.FullSendTablesBuffer.Base(), sv.FullSendTablesBuffer.Count()); + + // WriteSendTables(pClasses, sv.FullSendTables); + + if (sv.FullSendTables.Overflowed) { + Host.Error("SV_CreateBaseline: WriteSendTables overflow.\n"); + return; + } + + // WriteClassInfos(pClasses, sv.FullSendTables); + + if (sv.FullSendTables.Overflowed) { + Host.Error("SV_CreateBaseline: WriteClassInfos overflow.\n"); + return; + } + } + + if (true /*!g_pLocalNetworkBackdoor*/) { + int count = 0; + int bytes = 0; + + for (int entnum = 0; entnum < sv.NumEdicts; entnum++) { + Edict edict = sv.Edicts![entnum]; + + if (edict.IsFree() || edict.GetUnknown() == null) + continue; + + ServerClass? pClass = edict.GetNetworkable()?.GetServerClass(); + + if (pClass == null) { + Assert(pClass); + continue; + } + + if (pClass.InstanceBaselineIndex != INetworkStringTable.INVALID_STRING_INDEX) + continue; + + SendTable pSendTable = pClass.Table; + + byte[] packedData = new byte[Constants.MAX_PACKEDENTITY_DATA]; + bf_write writeBuf = new(packedData, Constants.MAX_PACKEDENTITY_DATA); + + if (!EngSendTable.Encode(pSendTable, edict.GetUnknown(), writeBuf, entnum, null, false)) + Host.Error($"SV_CreateBaseline: SendTable_Encode returned false (ent {entnum}).\n"); + + PackedEntities.EnsureInstanceBaseline(pClass, entnum, packedData, writeBuf.BytesWritten); + + bytes += writeBuf.BytesWritten; + count++; + } + DevMsg("Created class baseline: %i classes, %i bytes.\n", count, bytes); + } + + g_GameEventManager.ReloadEventDefinitions(); + + SVC_GameEventList gameevents = new(); + byte[] data = new byte[288000 /*NET_MAX_PAYLOAD*/]; + gameevents.DataOut.StartWriting(data, 288000); + + // gameEventManager.WriteEventList(&gameevents); + gameevents.WriteToBuffer(sv.Signon); } public bool HasPlayers() => sv.GetClientCount() > 0; diff --git a/Source.Engine/Server/BaseClient.cs b/Source.Engine/Server/BaseClient.cs index fa5decef..ba4944f7 100644 --- a/Source.Engine/Server/BaseClient.cs +++ b/Source.Engine/Server/BaseClient.cs @@ -15,12 +15,15 @@ using Steamworks; using System.Runtime.CompilerServices; +using System.Text; namespace Source.Engine.Server; public abstract class BaseClient : IGameEventListener2, IClient, IClientMessageHandler, IDisposable { + protected readonly FrameSnapshotManager framesnapshotmanager = Singleton(); + public int GetPlayerSlot() => ClientSlot; public int GetUserID() => UserID; // NetworkID? @@ -85,7 +88,74 @@ protected virtual bool ProcessStringCmd(NET_StringCmd m) { return true; } - protected virtual bool UpdateAcknowledgedFramecount(int tick) => true; + protected void OnRequestFullUpdate() { + LastSnapshot = null; + + FreeBaselines(); + + Baseline = framesnapshotmanager.CreateEmptySnapshot(0, Constants.MAX_EDICTS); + + DevMsg($"Sending full update to Client {GetClientName()}\n"); + } + + protected virtual bool UpdateAcknowledgedFramecount(int tick) { + if (IsFakeClient()) { + DeltaTick = tick; + StringTableAckTick = tick; + return true; + } + + if (ForceWaitForTick > 0) { + if (tick > ForceWaitForTick) + // we should never get here since full updates are transmitted as reliable data now + return true; + else if (tick == -1) { + if (!NetChannel.HasPendingReliableData()) { + // that's strange: we sent the client a full update, and it was fully received ( no reliable data in waiting buffers ) + // but the client is requesting another full update. + // + // This can happen if they request full updates in succession really quickly (using cl_fullupdate or "record X;stop" quickly). + // There was a bug here where if we just return out, the client will have nuked its entities and we'd send it + // a supposedly uncompressed update but DeltaTick was not -1, so it was delta'd and it'd miss lots of stuff. + // Led to clients getting full spectator mode radar while their player was not a spectator. + ConDMsg("Client forced immediate full update.\n"); + ForceWaitForTick = DeltaTick = -1; + OnRequestFullUpdate(); + return true; + } + } + else if (tick < ForceWaitForTick) + return true; + else + ForceWaitForTick = -1; + } + else { + if (DeltaTick == -1) + return true; + + if (tick == -1) + OnRequestFullUpdate(); + else { + if (DeltaTick > tick) { + // client already acknowledged new tick and now switch back to older + // thats not allowed since we always delete older frames + Disconnect("Client delta ticks out of order.\n"); + return false; + } + } + } + + DeltaTick = tick; + + if (DeltaTick > -1) + StringTableAckTick = DeltaTick; + + if ((BaselineUpdateTick > -1) && (DeltaTick > BaselineUpdateTick)) + // server sent a baseline update, but it wasn't acknowledged yet so it was probably lost. + BaselineUpdateTick = -1; + + return true; + } public void ClientRequestNameChange(ReadOnlySpan newName) { bool showStatusMessage = (PendingNameChange[0] == '\0'); @@ -332,9 +402,53 @@ protected virtual bool ProcessClientInfo(CLC_ClientInfo msg) { return true; } - protected virtual bool ProcessBaselineAck(CLC_BaselineAck m) { - Common.TimestampedLog($"BaseClient.ProcessBaselineAck: BaselineTick={m.BaselineTick}"); - return true;// todo + protected virtual bool ProcessBaselineAck(CLC_BaselineAck msg) { + if (msg.BaselineTick != BaselineUpdateTick) + return true; + + if (msg.BaselineNumber != BaselineUsed) { + DevMsg($"CBaseClient::ProcessBaselineAck: wrong baseline nr received ({msg.BaselineTick})\n"); + return true; + } + + Assert(Baseline != null); + + ClientFrame? frame = GetDeltaFrame(BaselineUpdateTick); + if (frame == null) + return true; + + FrameSnapshot? snapshot = frame.GetSnapshot(); + if (snapshot == null) { + DevMsg($"CBaseClient::ProcessBaselineAck: invalid frame snapshot ({BaselineUpdateTick})\n"); + return true; + } + + int index = BaselinesSent.FindNextSetBit(0); + while (index > 0) { + PackedEntityHandle_t newEntity = snapshot.Entities![index].PackedData; + if (newEntity == FrameSnapshotManager.INVALID_PACKED_ENTITY_HANDLE) { + DevMsg($"CBaseClient::ProcessBaselineAck: invalid packet handle ({index})\n"); + return false; + } + + PackedEntityHandle_t oldEntity = Baseline.Entities![index].PackedData; + + if (oldEntity != FrameSnapshotManager.INVALID_PACKED_ENTITY_HANDLE) + framesnapshotmanager.RemoveEntityReference(oldEntity); + + framesnapshotmanager.AddEntityReference(newEntity); + + Baseline.Entities[index] = snapshot.Entities[index]; + + index = BaselinesSent.FindNextSetBit(index + 1); + } + + Baseline.TickCount = BaselineUpdateTick; + + BaselineUsed = (BaselineUsed == 1) ? 0 : 1; + BaselineUpdateTick = -1; + + return true; } protected virtual bool ProcessListenEvents(CLC_ListenEvents m) { @@ -355,7 +469,12 @@ protected virtual bool ProcessListenEvents(CLC_ListenEvents m) { return true; } - const int SNAPSHOT_SCRATCH_BUFFER_SIZE = 16000; + protected virtual ClientFrame? GetDeltaFrame(int tick) { + Assert(false); + return null; + } + + const int SNAPSHOT_SCRATCH_BUFFER_SIZE = 160000; byte[] SnapshotScratchBuffer = new byte[SNAPSHOT_SCRATCH_BUFFER_SIZE / 4]; public virtual void SendSnapshot(ClientFrame frame) { // TODO This has a lot more to it @@ -364,33 +483,74 @@ public virtual void SendSnapshot(ClientFrame frame) { // TODO This has a lot mor return; } - bool failedOnce; + bool failedOnce = false; write_again: bf_write msg = new(SnapshotScratchBuffer, SNAPSHOT_SCRATCH_BUFFER_SIZE); - ClientFrame? deltaFrame = null;// GetDeltaFrame(DeltaTick); - if (deltaFrame == null) { - // OnRequestFullUpdate(); - } + ClientFrame? deltaFrame = GetDeltaFrame(DeltaTick); + if (deltaFrame == null) + OnRequestFullUpdate(); NET_Tick tickmsg = new(frame.TickCount, (int)Host.FrameTime, (int)Host.FrameTimeStandardDeviation); - SendNetMsg(tickmsg); -#if !SHARED_NET_STRING_TABLES + StartTrace(msg); + + tickmsg.WriteToBuffer(msg); + + if (Tracing != 0) + TraceNetworkData(msg, "NET_Tick"); +#if !SHARED_NET_STRING_TABLES + // if (LocalNetworkBackdoor == null) + Server.StringTables!.WriteUpdateMessage(this, GetMaxAckTickCount(), msg); #endif int deltaStartBit = 0; + if (Tracing != 0) + deltaStartBit = msg.BitsWritten; + + Server.WriteDeltaEntities(this, frame, deltaFrame, msg); - // Server.WriteDeltaEntities(this, frame, deltaFrame, msg); // TODO + if (Tracing != 0) { + int bits = msg.BitsWritten - deltaStartBit; + TraceNetworkData(msg, "Total Delta"); + } int maxTempEnts = Server.IsMultiplayer() ? 64 : 255; + Server.WriteTempEntities(this, frame.GetSnapshot(), LastSnapshot, msg, maxTempEnts); // WriteGameSounds(); - if (msg.Overflowed) {//todo - Disconnect($"Snapshot overflowed\n"); + if (msg.Overflowed) { + bool wasTracing = Tracing != 0; + if (wasTracing) { + TraceNetworkMsg(0, $"Finished [delta {(deltaFrame != null ? "yes" : "no")}]"); + EndTrace(msg); + } + + if (deltaFrame == null) { + if (!wasTracing) { + + if (sv_netspike_on_reliable_snapshot_overflow.GetBool()) { + if (!failedOnce) { + Warning(" RELIABLE SNAPSHOT OVERFLOW! Triggering trace to see what is so large\n"); + failedOnce = true; + Tracing = 2; + goto write_again; + } + + Tracing = 0; + } + + Disconnect("ERROR! Reliable snapshot overflow.\n"); + return; + } + else { + ConMsg($"WARNING: msg overflowed for {Name}\n"); + msg.Reset(); + } + } } LastSnapshot = frame.GetSnapshot(); @@ -409,16 +569,17 @@ public virtual void SendSnapshot(ClientFrame frame) { // TODO This has a lot mor ForceWaitForTick = (int)frame.TickCount; } - else { + else sendOK = NetChannel.SendDatagram(msg) > 0; - } if (sendOK) { - + if (Tracing != 0) { + TraceNetworkMsg(0, $"Finished [delta {(deltaFrame != null ? "yes" : "no")}]"); + EndTrace(msg); + } } - else { + else Disconnect($"ERROR! Couldn't send snapshot.\n"); - } } public int GetClientChallenge() => ClientChallenge; @@ -925,4 +1086,97 @@ public bool SendServerInfo() { return true; } + + struct Spike() + { + public InlineArray64 Desc; + public int Bits = 0; + } + + struct NetworkStatTrace() + { + public int MinWarningBytes = 0; + public int StartBit = 0; + public int CurBit = 0; + public readonly List Records = []; + public double StartSendTime = 0.0; + } + + public int Tracing; + NetworkStatTrace Trace = new(); + + void StartTrace(bf_write msg) { + Trace.MinWarningBytes = 0; + if (!IsHLTV() && !IsReplay() && !IsFakeClient()) + Trace.MinWarningBytes = -1; + + if (Tracing < 2) { + if (Trace.MinWarningBytes <= 0 && sv_netspike_sendtime_ms.GetFloat() <= 0.0f) { + Tracing = 0; + return; + } + Tracing = 1; + } + Trace.StartBit = msg.BitsWritten; + Trace.CurBit = Trace.StartBit; + Trace.StartSendTime = Platform.Time; + } + + void EndTrace(bf_write msg) { + if (Tracing == 0) + return; + + int bits = Trace.CurBit - Trace.StartBit; + float elapsedMs = (float)((Platform.Time - Trace.StartSendTime) * 1000.0); + int threshold = Trace.MinWarningBytes << 3; + if (Tracing < 2 // not forced + && (threshold <= 0 || bits < threshold) // didn't exceed data threshold + && (sv_netspike_sendtime_ms.GetFloat() <= 0.0f || elapsedMs < sv_netspike_sendtime_ms.GetFloat())) // didn't exceed time threshold + { + Trace.Records.Clear(); + Tracing = 0; + return; + } + + StringBuilder logData = new(); + logData.Append($"{Platform.Time}/{Host.TickCount} Player [{GetClientName()}][{GetPlayerSlot()}][adr:{NetChannel.GetAddress()}] was sent a datagram {bits} bits ({(float)bits / 8.0f} bytes), took {elapsedMs:F2}ms\n"); + + if ((sv_netspike_output.GetInt() & 2) == 0) + Log("netspike: %s", logData.ToString()); + + for (int i = 0; i < Trace.Records.Count; ++i) { + Spike sp = Trace.Records[i]; + logData.Append($"{sp.Desc} : {sp.Bits} bits ({(float)sp.Bits / 8.0f} bytes)\n"); + } + + if ((sv_netspike_output.GetInt() & 1) != 0) + // COM_LogString(SERVER_PACKETS_LOG, logData.String()); + + if ((sv_netspike_output.GetInt() & 2) != 0) + Log("%s", logData.ToString()); + + Trace.Records.Clear(); + Tracing = 0; + } + + public void TraceNetworkData(bf_write msg, ReadOnlySpan fmt) { + if (Tracing == 0) + return; + + Spike sp = new(); + fmt.CopyTo(sp.Desc); + sp.Bits = msg.BitsWritten - Trace.CurBit; + Trace.Records.Add(sp); + Trace.CurBit = msg.BitsWritten; + } + + public void TraceNetworkMsg(int bits, ReadOnlySpan fmt) { + if (Tracing == 0) + return; + + Spike sp = new(); + fmt.CopyTo(sp.Desc); + sp.Bits = bits; + Trace.Records.Add(sp); + } } diff --git a/Source.Engine/Server/BaseServer.cs b/Source.Engine/Server/BaseServer.cs index 5102204b..597eb1c3 100644 --- a/Source.Engine/Server/BaseServer.cs +++ b/Source.Engine/Server/BaseServer.cs @@ -319,14 +319,17 @@ public virtual void WriteDeltaEntities(BaseClient client, ClientFrame to, Client bf_write savepos = u.Buffer; // Save room for number of headers to parse, too - u.Buffer.WriteUBitLong(0, Constants.MAX_EDICT_BITS + Constants.DELTASIZE_BITS + 1); + + // u.Buffer.WriteUBitLong(0, Constants.MAX_EDICT_BITS + Constants.DELTASIZE_BITS + 1); + int bits = Constants.MAX_EDICT_BITS + Constants.DELTASIZE_BITS + 1; + Span headerBits = stackalloc byte[Net.Bits2Bytes(bits)]; + u.Buffer.WriteBits(headerBits, bits); int startbit = u.Buffer.BitsWritten; - bool bIsTracing = false;//client.IsTracing(); - if (bIsTracing) { - // client.TraceNetworkData(pBuf, "Delta Entities Overhead"); - } + bool isTracing = client.Tracing != 0; + if (isTracing) + client.TraceNetworkData(pBuf, "Delta Entities Overhead"); // Don't work too hard if we're using the optimized single-player mode. if (true /*!g_pLocalNetworkBackdoor*/) { // todo @@ -336,17 +339,17 @@ public virtual void WriteDeltaEntities(BaseClient client, ClientFrame to, Client u.NextOldEntity(); u.NextNewEntity(); - // 9999 = ENTITY_SENTINEL - while ((u.OldEntity != 9999) || (u.NewEntity != 9999)) { - u.NewPack = (u.NewEntity != 9999) ? framesnapshotmanager.GetPackedEntity(u.ToSnapshot, u.NewEntity) : null; - u.OldPack = (u.OldEntity != 9999) ? framesnapshotmanager.GetPackedEntity(u.FromSnapshot, u.OldEntity) : null; - int nEntityStartBit = pBuf.BitsWritten; + while ((u.OldEntity != PackedEntity.ENTITY_SENTINEL) || (u.NewEntity != PackedEntity.ENTITY_SENTINEL)) { + u.NewPack = (u.NewEntity != PackedEntity.ENTITY_SENTINEL) ? framesnapshotmanager.GetPackedEntity(u.ToSnapshot, u.NewEntity) : null; + u.OldPack = (u.OldEntity != PackedEntity.ENTITY_SENTINEL) ? framesnapshotmanager.GetPackedEntity(u.FromSnapshot, u.OldEntity) : null; + + int entStartBit = pBuf.BitsWritten; // Figure out how we want to write this entity. - // SV_DetermineUpdateType(u); - // SV_WriteEntityUpdate(u); + EntsWrite.DetermineUpdateType(u); + EntsWrite.WriteEntityUpdate(u); - if (!bIsTracing) + if (!isTracing) continue; switch (u.UpdateType) { @@ -355,31 +358,27 @@ public virtual void WriteDeltaEntities(BaseClient client, ClientFrame to, Client break; case UpdateType.EnterPVS: { ReadOnlySpan eString = sv.Edicts[u.NewPack.EntityIndex].GetNetworkable().GetClassName(); - // client.TraceNetworkData(pBuf, "enter [%s]", eString); - // ETWMark1I(eString, pBuf.BitsWritten - nEntityStartBit); + client.TraceNetworkData(pBuf, $"enter [{eString}]"); } break; case UpdateType.LeavePVS: { // Note, can't use GetNetworkable() since the edict has been freed at this point ReadOnlySpan eString = u.OldPack.ServerClass.NetworkName; - // client.TraceNetworkData(pBuf, "leave [%s]", eString); - // ETWMark1I(eString, pBuf.BitsWritten - nEntityStartBit); + client.TraceNetworkData(pBuf, $"leave [{eString}]"); } break; case UpdateType.DeltaEnt: { ReadOnlySpan eString = sv.Edicts[u.OldPack.EntityIndex].GetNetworkable().GetClassName(); - // client.TraceNetworkData(pBuf, "delta [%s]", eString); - // ETWMark1I(eString, pBuf.BitsWritten - nEntityStartBit); + client.TraceNetworkData(pBuf, $"delta [{eString}]"); } break; } } // Now write out the express deletions - int nNumDeletions = 0;//SV_WriteDeletions(u); - if (bIsTracing) { - // client.TraceNetworkData(pBuf, "Delta: [%d] deletions", nNumDeletions); - } + int nNumDeletions = EntsWrite.WriteDeletions(u); + if (isTracing) + client.TraceNetworkData(pBuf, $"Delta: [{nNumDeletions}] deletions"); } // get number of written bits @@ -400,13 +399,13 @@ public virtual void WriteDeltaEntities(BaseClient client, ClientFrame to, Client else savepos.WriteOneBit(0); - if (bIsTracing) { - // client.TraceNetworkData(pBuf, "Delta Finish"); - } + if (isTracing) + client.TraceNetworkData(pBuf, "Delta Finish"); } public virtual void WriteTempEntities(BaseClient client, FrameSnapshot to, FrameSnapshot from, bf_write pBuf, int nMaxEnts) { - throw new NotImplementedException(); + // throw new NotImplementedException(); + // todo } @@ -635,7 +634,21 @@ public virtual void UserInfoChanged(int nClientIndex) { } public bool GetClassBaseline(ServerClass pClass, out ReadOnlySpan pData) { - throw new NotImplementedException(); + if (sv_instancebaselines.GetBool()) { + if (pClass.InstanceBaselineIndex == INetworkStringTable.INVALID_STRING_INDEX) { + Host.Error($"SV_GetInstanceBaseline: missing instance baseline for class '{pClass.NetworkName}'\n"); + pData = default; + return false; + } + + pData = GetInstanceBaselineTable()!.GetStringUserData(pClass.InstanceBaselineIndex); + if (pData.IsEmpty) pData = [0]; + return true; + } + else { + pData = [0]; + return true; + } } public const double CHALLENGE_NONCE_LIFETIME = 6d; @@ -696,7 +709,8 @@ public ReadOnlySpan UncompressPackedEntity(PackedEntity pPackedEntity, out } public INetworkStringTable? GetInstanceBaselineTable() { - throw new NotImplementedException(); + InstanceBaselineTable ??= StringTables!.FindTable(Protocol.INSTANCE_BASELINE_TABLENAME); + return InstanceBaselineTable; } public INetworkStringTable? GetLightStyleTable() { throw new NotImplementedException(); diff --git a/Source.Engine/Server/EntsWrite.cs b/Source.Engine/Server/EntsWrite.cs index 4c007aca..6c98208d 100644 --- a/Source.Engine/Server/EntsWrite.cs +++ b/Source.Engine/Server/EntsWrite.cs @@ -1,4 +1,6 @@ +using Source.Common; using Source.Common.Bitbuffers; +using Source.Common.Networking; namespace Source.Engine.Server; @@ -6,12 +8,304 @@ class EntityWriteInfo : EntityInfo { public bf_write Buffer; public int ClientEntity; - public PackedEntity OldPack; - public PackedEntity NewPack; + public PackedEntity? OldPack; + public PackedEntity? NewPack; + public MaxEdictsBitVec DeletionFlags; public FrameSnapshot? FromSnapshot; // = From->GetSnapshot(); public FrameSnapshot ToSnapshot; // = m_pTo->GetSnapshot(); public FrameSnapshot Baseline; // the clients baseline public BaseServer Server; // the server who writes this entity public int FullProps; // number of properties send as full update (Enter PVS) public bool CullProps; // filter props by clients in recipient lists -}; \ No newline at end of file +} + +static class EntsWrite +{ + static readonly FrameSnapshotManager framesnapshotmanager = Singleton(); + static readonly EngineSendTable EngSendTable = Singleton(); + static readonly Host host = Singleton(); + + public static bool NeedsExplicitDestroy(int entnum, FrameSnapshot? from, FrameSnapshot to) { + if (entnum >= to.NumEntities || to.Entities![entnum].Class == null) { + if (entnum >= from!.NumEntities) + return false; + + if (from.Entities![entnum].Class != null) + return true; + } + + return false; + } + + static void UpdateHeaderDelta(EntityWriteInfo u, int entnum) { + u.HeaderCount++; + u.HeaderBase = entnum; + } + + public static void WriteDeltaHeader(EntityWriteInfo u, int entnum, int flags) { + bf_write buffer = u.Buffer; + + int offset = entnum - u.HeaderBase - 1; + + Assert(offset >= 0); + + buffer.WriteUBitVar((uint)offset); + + if ((flags & Protocol.FHDR_LEAVEPVS) != 0) { + buffer.WriteOneBit(1); + buffer.WriteOneBit(flags & Protocol.FHDR_DELETE); + } + else { + buffer.WriteOneBit(0); + buffer.WriteOneBit(flags & Protocol.FHDR_ENTERPVS); + } + + UpdateHeaderDelta(u, entnum); + } + + static int CalcDeltaAndWriteProps(EntityWriteInfo u, byte[]? fromData, int nFromBits, PackedEntity to) { + throw new NotImplementedException(); + } + + static void WritePropsFromPackedEntity(EntityWriteInfo u, Span checkProps, int nCheckProps) { + throw new NotImplementedException(); + } + + static bool NeedsExplicitCreate(EntityWriteInfo u) { + if (!u.AsDelta) + return false; + + int index = u.NewEntity; + if (index >= u.FromSnapshot!.NumEntities) + return true; + + FrameSnapshotEntry fromEnt = u.FromSnapshot.Entities![index]; + FrameSnapshotEntry toEnt = u.ToSnapshot.Entities![index]; + + return (fromEnt.Class == null) || fromEnt.SerialNumber != toEnt.SerialNumber; + } + + public static void DetermineUpdateType(EntityWriteInfo u) { + if (u.NewEntity < u.OldEntity) { + u.UpdateType = UpdateType.EnterPVS; + return; + } + + if (u.NewEntity > u.OldEntity) { + u.UpdateType = UpdateType.LeavePVS; + return; + } + + Assert(u.ToSnapshot.Entities![u.NewEntity].Class != null); + + bool recreate = NeedsExplicitCreate(u); + + if (recreate) { + u.UpdateType = UpdateType.EnterPVS; + return; + } + + Assert(u.OldPack!.ServerClass == u.NewPack!.ServerClass); + + if (u.OldPack == u.NewPack) { + Assert(u.OldPack != null); + u.UpdateType = UpdateType.PreserveEnt; + return; + } + + int[] checkProps = new int[Constants.MAX_DATATABLE_PROPS]; + int nCheckProps = u.NewPack.GetPropsChangedAfterTick(u.FromSnapshot!.TickCount, checkProps); + + if (nCheckProps == -1) { + + byte[]? oldData; + int nOldBits; + + if (u.OldPack.IsCompressed()) + throw new NotImplementedException(); + else { + oldData = u.OldPack.GetData(); + nOldBits = u.OldPack.GetNumBits(); + } + + byte[]? newData; + int nNewBits; + + if (u.NewPack.IsCompressed()) { + throw new NotImplementedException(); + } + else { + newData = u.NewPack.GetData(); + nNewBits = u.NewPack.GetNumBits(); + } + + nCheckProps = EngSendTable.CalcDelta( + u.OldPack.ServerClass!.Table, + oldData, + nOldBits, + newData!, + nNewBits, + checkProps, + checkProps.Length, + u.NewEntity); + } + + if (nCheckProps > 0) { + WriteDeltaHeader(u, u.NewEntity, Protocol.FHDR_ZERO); + + WritePropsFromPackedEntity(u, checkProps, nCheckProps); + + u.UpdateType = UpdateType.DeltaEnt; + } + else + u.UpdateType = UpdateType.PreserveEnt; + } + + static void WriteEnterPVS(EntityWriteInfo u) { + WriteDeltaHeader(u, u.NewEntity, Protocol.FHDR_ENTERPVS); + + Assert(u.NewEntity < u.ToSnapshot.NumEntities); + + FrameSnapshotEntry entry = u.ToSnapshot.Entities![u.NewEntity]; + + ServerClass? entryClass = entry.Class; + + if (entryClass == null) + host.Error($"SV_CreatePacketEntities: GetEntServerClass failed for ent {u.NewEntity}.\n"); + + if (entryClass.ClassID >= u.Server.ServerClasses) { + ConMsg($"entryClass.ClassID({entryClass.ClassID}) >= {u.Server.ServerClasses}\n"); + Assert(false); + } + + u.Buffer.WriteUBitLong((uint)entryClass.ClassID, u.Server.ServerClassBits); + u.Buffer.WriteUBitLong((uint)entry.SerialNumber, Constants.NUM_NETWORKED_EHANDLE_SERIAL_NUMBER_BITS); + + PackedEntity? baseline = u.AsDelta ? framesnapshotmanager.GetPackedEntity(u.Baseline, u.NewEntity) : null; + byte[]? fromData; + int nFromBits; + + if (baseline != null && (baseline.ServerClass == u.NewPack!.ServerClass)) { + Assert(!baseline.IsCompressed()); + fromData = baseline.GetData(); + nFromBits = baseline.GetNumBits(); + } + else { + if (!u.Server.GetClassBaseline(entryClass, out ReadOnlySpan pFromData)) + Error($"SV_WriteEnterPVS: missing instance baseline for '{entryClass.NetworkName}'."); + + ErrorIfNot(!pFromData.IsEmpty, $"SV_WriteEnterPVS: missing pFromData for '{entryClass.NetworkName}'."); + + fromData = pFromData.ToArray(); + nFromBits = pFromData.Length * 8; + } + + if (u.To?.FromBaseline != null) + u.To.FromBaseline.Set(u.NewEntity); + + byte[]? toData; + int nToBits; + + if (u.NewPack!.IsCompressed()) + throw new NotImplementedException(); + else { + toData = u.NewPack.GetData(); + nToBits = u.NewPack.GetNumBits(); + } + + u.FullProps += WriteAllDeltaProps(entryClass.Table, fromData!, nFromBits, toData!, nToBits, u.NewPack.EntityIndex, u.Buffer); + + if (u.NewEntity == u.OldEntity) + u.NextOldEntity(); + + u.NextNewEntity(); + } + + static void WriteLeavePVS(EntityWriteInfo u) { + int headerflags = Protocol.FHDR_LEAVEPVS; + bool deleteentity = false; + + if (u.AsDelta) + deleteentity = NeedsExplicitDestroy(u.OldEntity, u.FromSnapshot, u.ToSnapshot); + + if (deleteentity) { + u.DeletionFlags.Set(u.OldEntity); + headerflags |= Protocol.FHDR_DELETE; + } + + WriteDeltaHeader(u, u.OldEntity, headerflags); + + u.NextOldEntity(); + } + + static void WriteDeltaEnt(EntityWriteInfo u) { + u.NextOldEntity(); + u.NextNewEntity(); + } + + static void PreserveEnt(EntityWriteInfo u) { + u.NextOldEntity(); + u.NextNewEntity(); + } + + public static void WriteEntityUpdate(EntityWriteInfo u) { + switch (u.UpdateType) { + case UpdateType.EnterPVS: + WriteEnterPVS(u); + break; + case UpdateType.LeavePVS: + WriteLeavePVS(u); + break; + case UpdateType.DeltaEnt: + WriteDeltaEnt(u); + break; + case UpdateType.PreserveEnt: + PreserveEnt(u); + break; + } + } + + public static int WriteDeletions(EntityWriteInfo u) { + if (!u.AsDelta) + return 0; + + int numDeletions = 0; + + FrameSnapshot fromSnapshot = u.FromSnapshot!; + FrameSnapshot toSnapshot = u.ToSnapshot; + + // fixme: why is fromSnapshot null here? + int last = Math.Max(fromSnapshot.NumEntities, toSnapshot.NumEntities); + for (int i = 0; i < last; i++) { + if (u.DeletionFlags.Get(i) != 0) + continue; + + if (u.To!.TransmitEntity.Get(i) != 0) + continue; + + bool needsExplicitDelete = NeedsExplicitDestroy(i, fromSnapshot, toSnapshot); + if (!needsExplicitDelete && u.To != null) + needsExplicitDelete = toSnapshot.ExplicitDeleteSlots.Contains(i); + + if (needsExplicitDelete) { + u.Buffer.WriteOneBit(1); + u.Buffer.WriteUBitLong((uint)i, Constants.MAX_EDICT_BITS); + ++numDeletions; + } + } + + u.Buffer.WriteOneBit(0); + + return numDeletions; + } + + static int WriteAllDeltaProps(SendTable table, byte[] fromData, int nFromBits, byte[] toData, int nToBits, int objectId, bf_write bufOut) { + int[] deltaProps = new int[Constants.MAX_DATATABLE_PROPS]; + + int nDeltaProps = EngSendTable.CalcDelta(table, fromData, nFromBits, toData, nToBits, deltaProps, deltaProps.Length, objectId); + + EngSendTable.WritePropList(table, fromData, nFromBits, toData, nToBits, bufOut, objectId, deltaProps); + + return nDeltaProps; + } +} \ No newline at end of file diff --git a/Source.Engine/Server/GameClient.cs b/Source.Engine/Server/GameClient.cs index 6c120937..789b9a37 100644 --- a/Source.Engine/Server/GameClient.cs +++ b/Source.Engine/Server/GameClient.cs @@ -158,6 +158,10 @@ public void SetupPackInfo(FrameSnapshot snapshot) { CurrentFrame = cl.AllocateFrame(); CurrentFrame.Init(snapshot); + int maxFrames = MAX_CLIENT_FRAMES; + if (maxFrames < cl.AddClientFrame(CurrentFrame)) + cl.RemoveOldestFrame(); + } void SetupPrevPackInfo() { } @@ -188,63 +192,14 @@ public override void Inactivate() { } protected override bool UpdateAcknowledgedFramecount(int tick) { - if (IsFakeClient()) { - DeltaTick = tick; - StringTableAckTick = tick; - return true; - } + if (tick != DeltaTick) { + int removeTick = tick; - if (ForceWaitForTick > 0) { - if (tick > ForceWaitForTick) - // we should never get here since full updates are transmitted as reliable data now - return true; - else if (tick == -1) { - if (!NetChannel!.HasPendingReliableData()) { - // that's strange: we sent the client a full update, and it was fully received ( no reliable data in waiting buffers ) - // but the client is requesting another full update. - // - // This can happen if they request full updates in succession really quickly (using cl_fullupdate or "record X;stop" quickly). - // There was a bug here where if we just return out, the client will have nuked its entities and we'd send it - // a supposedly uncompressed update but DeltaTick was not -1, so it was delta'd and it'd miss lots of stuff. - // Led to clients getting full spectator mode radar while their player was not a spectator. - ConDMsg("Client forced immediate full update.\n"); - ForceWaitForTick = DeltaTick = -1; - // OnRequestFullUpdate(); TODO - return true; - } - } - else if (tick < ForceWaitForTick) - return true; - else - ForceWaitForTick = -1; + if (removeTick > 0) + cl.DeleteClientFrames(removeTick); } - else { - if (DeltaTick == -1) - return true; - if (tick == -1) { - // OnRequestFullUpdate(); - } - else { - if (DeltaTick > tick) { - // client already acknowledged new tick and now switch back to older - // thats not allowed since we always delete older frames - Disconnect("Client delta ticks out of order.\n"); - return false; - } - } - } - - DeltaTick = tick; - - if (DeltaTick > -1) - StringTableAckTick = DeltaTick; - - if ((BaselineUpdateTick > -1) && (DeltaTick > BaselineUpdateTick)) - // server sent a baseline update, but it wasn't acknowledged yet so it was probably lost. - BaselineUpdateTick = -1; - - return true; + return base.UpdateAcknowledgedFramecount(tick); } public override void Clear() { @@ -435,7 +390,10 @@ protected override void SpawnPlayer() { // SV.ServerGameClients!.ClientSpawned(Edict); } - // ClientFrame GetDeltaFrame(int tick) { } + protected override ClientFrame? GetDeltaFrame(int tick) { + Assert(!IsHLTV()); + return cl.GetClientFrame(tick); + } void WriteViewAngleUpdate() { if (IsFakeClient()) diff --git a/Source.Engine/Server/GameServer.cs b/Source.Engine/Server/GameServer.cs index f21dfc12..f164e06e 100644 --- a/Source.Engine/Server/GameServer.cs +++ b/Source.Engine/Server/GameServer.cs @@ -639,9 +639,24 @@ private void InitializeEntityDLLFields(Edict edict) { } private void AssignClassIds() { + ServerClass classes = serverGameDLL.GetAllServerClasses(); - } + int nClasses = 0; + for (ServerClass count = classes; count != null; count = count.Next) + nClasses++; + + ErrorIfNot(nClasses <= Constants.MAX_SERVER_CLASSES, $"CGameServer::AssignClassIds: too many server classes ({nClasses}, MAX = {Constants.MAX_SERVER_CLASSES}).\n"); + + ServerClasses = nClasses; + ServerClassBits = (int)(Math.Log2(ServerClasses) + 1); + int curID = 0; + for (ServerClass c = classes; c != null; c = c.Next) { + c.ClassID = curID++; + + // Msg($"{c.ClassID} == '{c.NetworkName}'\n"); + } + } INetworkStringTable? ModelPrecacheTable; INetworkStringTable? SoundPrecacheTable; diff --git a/Source.Engine/Server/PackedEntities.cs b/Source.Engine/Server/PackedEntities.cs index cfaae7fa..62c53ff2 100644 --- a/Source.Engine/Server/PackedEntities.cs +++ b/Source.Engine/Server/PackedEntities.cs @@ -37,8 +37,20 @@ static bool EnsurePrivateData(Edict edict) { } } - static void EnsureInstanceBasline(ServerClass serverClass, int edictId, ReadOnlySpan data, int bytes) { - throw new NotImplementedException(); + public static void EnsureInstanceBaseline(ServerClass serverClass, int edictId, ReadOnlySpan data, int bytes) { + Edict ent = sv.Edicts![edictId]; + ErrorIfNot(EnsurePrivateData(ent), $"SV_EnsureInstanceBaseline: EnsurePrivateData failed for ent {edictId}."); + + ServerClass entClass = ent.GetNetworkable()?.GetServerClass(); + + if (entClass.InstanceBaselineIndex == INetworkStringTable.INVALID_STRING_INDEX) { + Span idString = stackalloc char[32]; + sprintf(idString, "%d").D(entClass.ClassID); + int storeBytes = Math.Max(bytes, 1); + int temp = sv.InstanceBaselineTable!.AddString(true, idString, storeBytes, data); + entClass.InstanceBaselineIndex = temp; + Assert(entClass.InstanceBaselineIndex != INetworkStringTable.INVALID_STRING_INDEX); + } } public static void PackEntity(int edictId, Edict edict, ServerClass serverClass, FrameSnapshot snapshot) { @@ -77,7 +89,7 @@ public static void PackEntity(int edictId, Edict edict, ServerClass serverClass, if (!EngSendTable.Encode(sendTable, edict.GetUnknown(), writeBuf, edictId, recip, false)) Host.Error($"SV_PackEntity: SendTable_Encode returned false (ent {edictId}).\n"); - // SV.EnsureInstanceBaseline(serverClass, edictId, packedData, writeBuf.BytesWritten); TODO TODO + EnsureInstanceBaseline(serverClass, edictId, packedData, writeBuf.BytesWritten); int flatProps = EngSendTable.GetNumFlatProps(sendTable); IChangeFrameList? changeFrame; From 48950933b669222227b712be944ca8710a6d5914 Mon Sep 17 00:00:00 2001 From: callumok2004 Date: Fri, 6 Mar 2026 20:50:44 +0000 Subject: [PATCH 30/31] DatatableStack funcs --- Source.Common/DtSend.cs | 7 ++++++- Source.Engine/ClientFrame.cs | 16 ++++++++++++---- Source.Engine/EngineRecvTable.cs | 30 +++++++++++++++++++++++++++++- Source.Engine/EngineSendTable.cs | 10 +++++++--- Source.Engine/Server/BaseClient.cs | 10 +++++----- Source.Engine/Server/EntsWrite.cs | 8 +++++++- Source.Engine/Server/GameClient.cs | 1 - 7 files changed, 66 insertions(+), 16 deletions(-) diff --git a/Source.Common/DtSend.cs b/Source.Common/DtSend.cs index 26f27eb8..3b4cf60f 100644 --- a/Source.Common/DtSend.cs +++ b/Source.Common/DtSend.cs @@ -521,7 +521,12 @@ public SendProp(string? name, IFieldAccessor field, SendPropType type, int bits SendTable? DataTable; [MethodImpl(MethodImplOptions.AggressiveInlining)] - public T GetValue(object instance) => FieldInfo.GetValue(instance); + public T GetValue(object instance) { +#if DEBUG + ErrorIfNot(FieldInfo != null, $"SendProp.GetValue: FieldInfo is null for prop {GetName()}"); +#endif + return FieldInfo.GetValue(instance); + } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void SetValue(object instance, in T value) => FieldInfo.SetValue(instance, in value); diff --git a/Source.Engine/ClientFrame.cs b/Source.Engine/ClientFrame.cs index 664550ba..cdf94ecb 100644 --- a/Source.Engine/ClientFrame.cs +++ b/Source.Engine/ClientFrame.cs @@ -17,17 +17,25 @@ public class ClientFrame public MaxEdictsBitVec TransmitAlways; public ClientFrame? Next; - public FrameSnapshot Snapshot; + public FrameSnapshot? Snapshot; internal void Init(int tickcount) { TickCount = tickcount; } internal void Init(FrameSnapshot snapshot) { TickCount = snapshot.TickCount; - Snapshot = snapshot; + SetSnapshot(snapshot); } - internal FrameSnapshot GetSnapshot() => Snapshot; + internal FrameSnapshot? GetSnapshot() => Snapshot; + + internal void SetSnapshot(FrameSnapshot? snapshot) { + if (Snapshot == snapshot) + return; + + snapshot?.AddReference(); + Snapshot?.ReleaseReference(); - internal void SetSnapshot(FrameSnapshot snapshot) => Snapshot = snapshot; + Snapshot = snapshot; + } } diff --git a/Source.Engine/EngineRecvTable.cs b/Source.Engine/EngineRecvTable.cs index de5ba4f2..393b3f96 100644 --- a/Source.Engine/EngineRecvTable.cs +++ b/Source.Engine/EngineRecvTable.cs @@ -143,7 +143,35 @@ public void Init(bool explicitRoutes = false) { Initted = true; } - public abstract void RecurseAndCallProxies(SendNode node, object instance); + public virtual void RecurseAndCallProxies(SendNode node, object? instance) { + Proxies[node.GetRecursiveProxyIndex()] = instance; + + for (int iChild = 0; iChild < node.GetNumChildren(); iChild++) { + SendNode? curChild = node.GetChild(iChild); + + object? newStructBase = null; + if (instance != null) + newStructBase = CallPropProxy(curChild, curChild.DataTableProp, instance); + + RecurseAndCallProxies(curChild, newStructBase); + } + } + + public object? CallPropProxy(SendNode curChild, int prop, object instance) { + SendProp sendprop = Precalc.GetDatatableProp(prop)!; + + SendProxyRecipients? recipients = default; + if (Recipients != null && curChild.GetRecursiveProxyIndex() != Constants.DATATABLE_PROXY_INDEX_NOPROXY) + recipients = Recipients[curChild.GetRecursiveProxyIndex()]; + + return sendprop.GetDataTableProxyFn()( + sendprop, + instance, + sendprop.FieldInfo, + recipients, + GetObjectID() + ); + } public SendProp? GetCurProp() => CurProp; public bool IsPropProxyValid(int iProp) => Proxies[Precalc.PropProxyIndices[iProp]] != null; diff --git a/Source.Engine/EngineSendTable.cs b/Source.Engine/EngineSendTable.cs index 3ef4f8bf..f216710a 100644 --- a/Source.Engine/EngineSendTable.cs +++ b/Source.Engine/EngineSendTable.cs @@ -123,8 +123,13 @@ public bool Encode(SendTable table, object data, bf_write dataOut, int objectId, return !dataOut.Overflowed; } - public void WritePropList(SendTable table, byte[]? fromState, int nFromBits, byte[] toState, int nToBits, bf_write dataOut, int objectId, int[] checkProps) { - DevMsg($"TODO: WritePropList: table={table.NetTableName}\n"); + public void WritePropList(SendTable table, byte[]? state, int nBits, bf_write outBuf, int objectId, Span checkProps, int nCheckProps) { + if (nCheckProps == 0) { + outBuf.WriteOneBit(0); + return; + } + + DevMsg($"TODO: WritePropList: table={table.NetTableName} checkProps={nCheckProps} objectId={objectId}\n"); } bool IsPropZero(EncodeInfo info, int _) { @@ -227,5 +232,4 @@ public int CalcDelta(SendTable table, byte[]? fromState, int nFromBits, byte[] t class EncodeInfo(SendTablePrecalc precalc, object structData, int objectId, bf_write dataOut) : DatatableStack(precalc, structData, objectId) { public DeltaBitsWriter DeltaBitsWriter = new(dataOut); - public override void RecurseAndCallProxies(SendNode node, object instance) { } } \ No newline at end of file diff --git a/Source.Engine/Server/BaseClient.cs b/Source.Engine/Server/BaseClient.cs index ba4944f7..cc9f7f1c 100644 --- a/Source.Engine/Server/BaseClient.cs +++ b/Source.Engine/Server/BaseClient.cs @@ -55,8 +55,8 @@ public virtual void FileDenied(ReadOnlySpan fileName, uint transferID) { } public virtual void FileSent(ReadOnlySpan fileName, uint transferID) { } public bool ProcessMessage(T message) where T : INetMessage { - if (message is not NET_Tick && message is not CLC_Move) - Common.TimestampedLog($"BaseClient.ProcessMessage: {message.GetType().Name} (IsReliable: {message.IsReliable()})"); + // if (message is not NET_Tick && message is not CLC_Move) + // Common.TimestampedLog($"BaseClient.ProcessMessage: {message.GetType().Name} (IsReliable: {message.IsReliable()})"); switch (message) { case NET_Tick m: return ProcessTick(m); @@ -383,7 +383,7 @@ protected virtual bool ProcessClientInfo(CLC_ClientInfo msg) { // if ((hltv && hltv.IsTVRelay()) || tv_enable.GetBool()) { // HLTV = msg.IsHLTV; // else - // HLTV = false; + HLTV = false; FilesDownloaded = 0; FriendsID = (uint)msg.FriendsID; @@ -475,9 +475,9 @@ protected virtual bool ProcessListenEvents(CLC_ListenEvents m) { } const int SNAPSHOT_SCRATCH_BUFFER_SIZE = 160000; - byte[] SnapshotScratchBuffer = new byte[SNAPSHOT_SCRATCH_BUFFER_SIZE / 4]; + readonly byte[] SnapshotScratchBuffer = new byte[SNAPSHOT_SCRATCH_BUFFER_SIZE / 4]; - public virtual void SendSnapshot(ClientFrame frame) { // TODO This has a lot more to it + public virtual void SendSnapshot(ClientFrame frame) { if (ForceWaitForTick > 0 || LastSnapshot == frame.GetSnapshot()) { NetChannel.Transmit(); return; diff --git a/Source.Engine/Server/EntsWrite.cs b/Source.Engine/Server/EntsWrite.cs index 6c98208d..d8b67ffa 100644 --- a/Source.Engine/Server/EntsWrite.cs +++ b/Source.Engine/Server/EntsWrite.cs @@ -275,6 +275,12 @@ public static int WriteDeletions(EntityWriteInfo u) { FrameSnapshot toSnapshot = u.ToSnapshot; // fixme: why is fromSnapshot null here? +#if DEBUG + if (fromSnapshot == null) { + u.Buffer.WriteOneBit(0); + return 0; + } +#endif int last = Math.Max(fromSnapshot.NumEntities, toSnapshot.NumEntities); for (int i = 0; i < last; i++) { if (u.DeletionFlags.Get(i) != 0) @@ -304,7 +310,7 @@ static int WriteAllDeltaProps(SendTable table, byte[] fromData, int nFromBits, b int nDeltaProps = EngSendTable.CalcDelta(table, fromData, nFromBits, toData, nToBits, deltaProps, deltaProps.Length, objectId); - EngSendTable.WritePropList(table, fromData, nFromBits, toData, nToBits, bufOut, objectId, deltaProps); + EngSendTable.WritePropList(table, toData, nToBits, bufOut, objectId, deltaProps, nDeltaProps); return nDeltaProps; } diff --git a/Source.Engine/Server/GameClient.cs b/Source.Engine/Server/GameClient.cs index 789b9a37..012b2306 100644 --- a/Source.Engine/Server/GameClient.cs +++ b/Source.Engine/Server/GameClient.cs @@ -161,7 +161,6 @@ public void SetupPackInfo(FrameSnapshot snapshot) { int maxFrames = MAX_CLIENT_FRAMES; if (maxFrames < cl.AddClientFrame(CurrentFrame)) cl.RemoveOldestFrame(); - } void SetupPrevPackInfo() { } From 26713f48611808fe0fca1b7c4ca36332aad591e7 Mon Sep 17 00:00:00 2001 From: callumok2004 Date: Fri, 6 Mar 2026 23:57:12 +0000 Subject: [PATCH 31/31] Some SendProxy funcs impl --- Game.Server/BaseAnimating.cs | 6 +- Game.Server/BaseEntity.cs | 76 ++++++++++++++++++++++---- Game.Server/Player.cs | 7 ++- Game.Server/Team.cs | 13 ++++- Game.Shared/BaseCombatWeaponShared.cs | 22 +++++++- Game.Shared/CollisionProperty.cs | 4 +- Game.Shared/GarrysMod/GMODGameRules.cs | 3 +- Source.Common/DtSend.cs | 1 + Source.Common/FieldAccessIL.cs | 14 +++-- 9 files changed, 118 insertions(+), 28 deletions(-) diff --git a/Game.Server/BaseAnimating.cs b/Game.Server/BaseAnimating.cs index 9a2a0500..08b9c83b 100644 --- a/Game.Server/BaseAnimating.cs +++ b/Game.Server/BaseAnimating.cs @@ -156,9 +156,9 @@ public float GetSequenceMoveDist(StudioHdr? studioHdr, int sequence) { public float GetSequenceGroundSpeed(StudioHdr? studioHdr, int sequence) { TimeUnit_t t = SequenceDuration(studioHdr, sequence); - if (t > 0) + if (t > 0) return (GetSequenceMoveDist(studioHdr, sequence) / (float)t); - else + else return 0; } public void ResetSequenceInfo() { @@ -200,4 +200,6 @@ public int FindTransitionSequence(int currentSequence, int goalSequence) { else return sequence; } + + public override BaseAnimating? GetBaseAnimating() => this; } diff --git a/Game.Server/BaseEntity.cs b/Game.Server/BaseEntity.cs index 10f29d11..6aefdee8 100644 --- a/Game.Server/BaseEntity.cs +++ b/Game.Server/BaseEntity.cs @@ -43,16 +43,52 @@ public partial class BaseEntity : IServerEntity public bool NameMatches(ReadOnlySpan name) => false; // todo public virtual bool IsPredicted() => false; public virtual bool IsTemplate() => false; - private static void SendProxy_AnimTime(SendProp prop, object instance, IFieldAccessor field, ref DVariant outData, int element, int objectID) - => throw new NotImplementedException(); - private static void SendProxy_SimulationTime(SendProp prop, object instance, IFieldAccessor field, ref DVariant outData, int element, int objectID) - => throw new NotImplementedException(); + + private static void SendProxy_AnimTime(SendProp prop, object instance, IFieldAccessor field, ref DVariant outData, int element, int objectID) { + BaseEntity entity = (BaseEntity)instance; + +#if false + BaseAnimating? animating = entity.GetBaseAnimating(); + Assert(animating != null); + if (animating != null) + Assert(!animating.IsUsingClientSideAnimation()); +#endif + + int tickNumber = TIME_TO_TICKS(entity.AnimTime); + long tickBase = gpGlobals.GetNetworkBase(gpGlobals.TickCount, entity.EntIndex()); + + int addt = 0; + if (tickNumber >= tickBase) + addt = (int)((tickNumber - tickBase) & 0xff); + + outData.Int = addt; + } + + private static void SendProxy_SimulationTime(SendProp prop, object instance, IFieldAccessor field, ref DVariant outData, int element, int objectID) { + BaseEntity entity = (BaseEntity)instance; + + int tickNumber = TIME_TO_TICKS(entity.SimulationTime); + long tickBase = gpGlobals.GetNetworkBase(gpGlobals.TickCount, entity.EntIndex()); + + int addt = 0; + if (tickNumber >= tickBase) + addt = (int)((tickNumber - tickBase) & 0xff); + + outData.Int = addt; + } public static SendTable DT_AnimTimeMustBeFirst = new(nameof(DT_AnimTimeMustBeFirst), [ SendPropInt (FIELD.OF(nameof(AnimTime)), 8, PropFlags.Unsigned|PropFlags.ChangesOften|PropFlags.EncodedAgainstTickCount, proxyFn: SendProxy_AnimTime), ]); + public static object? SendProxy_ClientSideAnimation(SendProp prop, object instance, IFieldAccessor data, SendProxyRecipients recipients, int objectID) { - throw new NotImplementedException(); + BaseEntity entity = (BaseEntity)instance; + BaseAnimating? animating = entity.GetBaseAnimating(); + + if (animating != null /*&& !animating.IsUsingClientSideAnimation()*/) + return data.GetValue(instance); + else + return null; } public static SendTable DT_PredictableId = new(nameof(DT_PredictableId), [ SendPropPredictableId(FIELD.OF(nameof(PredictableId))), @@ -148,15 +184,32 @@ public BaseEntity() { public float Gravity; public void SetPredictionEligible(bool canpredict) { } // nothing in game code public ref readonly Vector3 GetLocalOrigin() => ref AbsOrigin; + public ref readonly QAngle GetLocalAngles() => ref AbsRotation; private static void SendProxy_OverrideMaterial(SendProp prop, object instance, IFieldAccessor field, ref DVariant outData, int element, int objectID) { - Warning("SendProxy_OverrideMaterial not yet implemented\n"); + BaseEntity entity = (BaseEntity)instance; + outData.Int = entity.OverrideMaterial; } private static void SendProxy_Angles(SendProp prop, object instance, IFieldAccessor field, ref DVariant outData, int element, int objectID) { - Warning("SendProxy_Angles not yet implemented\n"); + BaseEntity entity = (BaseEntity)instance; + Assert(entity != null); + + QAngle angles; + if (true /*entity.UseStepSimulationNetworkAngles*/) + angles = entity.GetLocalAngles(); + + outData.Vector[0] = MathLib.AngleMod(angles.X); + outData.Vector[1] = MathLib.AngleMod(angles.Y); + outData.Vector[2] = MathLib.AngleMod(angles.Z); } protected static object? SendProxy_SendPredictableId(SendProp prop, object instance, IFieldAccessor data, SendProxyRecipients recipients, int objectID) { - Warning("SendProxy_SendPredictableId not yet implemented\n"); - return null; + BaseEntity entity = (BaseEntity)instance; + if (entity == null || !entity.PredictableId.IsActive()) + return null; + + int id_player_index = entity.PredictableId.GetPlayer(); + // recipients.SetOnly(id_player_index); + + return data.GetValue(instance); } public static BaseEntity? GetContainingEntity(Edict ent) { @@ -489,7 +542,6 @@ public ref Matrix3x4 EntityToWorldTransform() { public float GetGravity() => Gravity; public void SetGravity(float gravity) => Gravity = gravity; - public object? GetBaseEntity() { - return this; - } + public object? GetBaseEntity() => this; + public virtual BaseAnimating? GetBaseAnimating() => null; } diff --git a/Game.Server/Player.cs b/Game.Server/Player.cs index bb3e3df3..f8315a71 100644 --- a/Game.Server/Player.cs +++ b/Game.Server/Player.cs @@ -87,11 +87,14 @@ public partial class BasePlayer : BaseCombatCharacter public float SurfaceFriction; public static void SendProxy_CropFlagsToPlayerFlagBitsLength(SendProp prop, object instance, IFieldAccessor field, ref DVariant outData, int element, int objectID) { - throw new NotImplementedException(); + int mask = (1 << Constants.PLAYER_FLAG_BITS) - 1; + int data = field.GetValue(instance); + outData.Int = data & mask; } public static object? SendProxy_SendLocalDataTable(SendProp prop, object instance, IFieldAccessor data, SendProxyRecipients recipients, int objectID) { - throw new NotImplementedException(); + // recipients.SetOnly(objectID - 1); + return data; } public static object? SendProxy_SendNonLocalDataTable(SendProp prop, object instance, IFieldAccessor data, SendProxyRecipients recipients, int objectID) { throw new NotImplementedException(); diff --git a/Game.Server/Team.cs b/Game.Server/Team.cs index 43b7854b..06257f4f 100644 --- a/Game.Server/Team.cs +++ b/Game.Server/Team.cs @@ -1,15 +1,19 @@ global using static Game.Server.TeamGlobals; + using Source.Common; using Source; + using Game.Shared; namespace Game.Server; + using FIELD = Source.FIELD; -public static class TeamGlobals { +public static class TeamGlobals +{ public static readonly List g_Teams = []; public static int GetNumberOfTeams() => g_Teams.Count; - public static Team? GetGlobalTeam(int index){ + public static Team? GetGlobalTeam(int index) { if (index < 0 || index >= GetNumberOfTeams()) return null; @@ -31,6 +35,11 @@ public class Team : BaseEntity private static int SendProxyArrayLength_PlayerArray(object instance, int objectID) => ((Team)instance).Players.Count; private static void SendProxy_PlayerList(SendProp prop, object instance, IFieldAccessor field, ref DVariant outData, int element, int objectID) { + Team team = (Team)instance; + Assert(element < team.Players.Count); + + BasePlayer player = team.Players[element]; + outData.Int = player.EntIndex(); } public static readonly new ServerClass ServerClass = new ServerClass("Team", DT_Team).WithManualClassID(StaticClassIndices.CTeam); diff --git a/Game.Shared/BaseCombatWeaponShared.cs b/Game.Shared/BaseCombatWeaponShared.cs index 4b1b82a1..9ecc29ca 100644 --- a/Game.Shared/BaseCombatWeaponShared.cs +++ b/Game.Shared/BaseCombatWeaponShared.cs @@ -113,10 +113,28 @@ private static void RecvProxy_WeaponState(ref readonly RecvProxyData data, objec #else private static object? SendProxy_SendLocalWeaponDataTable(SendProp prop, object instance, IFieldAccessor data, SendProxyRecipients recipients, int objectID) { - throw new NotImplementedException(); + BaseCombatWeapon weapon = (BaseCombatWeapon)instance; + if (weapon != null) { + BasePlayer? player = ToBasePlayer(weapon.GetOwner()); + if (player != null) { + // recipients.SetOnly(player.GetClientIndex()); + return data; + } + } + + return null; } private static object? SendProxy_SendActiveLocalWeaponDataTable(SendProp prop, object instance, IFieldAccessor data, SendProxyRecipients recipients, int objectID) { - throw new NotImplementedException(); + BaseCombatWeapon weapon = (BaseCombatWeapon)instance; + if (weapon != null) { + BasePlayer? player = ToBasePlayer(weapon.GetOwner()); + if (player != null) { + // recipients.SetOnly(player.GetClientIndex()); + return data; + } + } + + return null; } #endif public static readonly new Class diff --git a/Game.Shared/CollisionProperty.cs b/Game.Shared/CollisionProperty.cs index 02799198..261d1525 100644 --- a/Game.Shared/CollisionProperty.cs +++ b/Game.Shared/CollisionProperty.cs @@ -76,11 +76,11 @@ private static void RecvProxy_IntDirtySurround(ref readonly RecvProxyData data, } #else private static void SendProxy_SolidFlags(SendProp prop, object instance, IFieldAccessor field, ref DVariant outData, int element, int objectID) { - throw new NotImplementedException(); + outData.Int = ((CollisionProperty)(instance)).SolidFlags; } private static void SendProxy_Solid(SendProp prop, object instance, IFieldAccessor field, ref DVariant outData, int element, int objectID) { - throw new NotImplementedException(); + outData.Int = ((CollisionProperty)(instance)).SolidType; } #endif diff --git a/Game.Shared/GarrysMod/GMODGameRules.cs b/Game.Shared/GarrysMod/GMODGameRules.cs index b00ab05e..2ac3999a 100644 --- a/Game.Shared/GarrysMod/GMODGameRules.cs +++ b/Game.Shared/GarrysMod/GMODGameRules.cs @@ -13,6 +13,7 @@ namespace Game.Server.GarrysMod; using Source; using FIELD = Source.FIELD; + using Game.Shared; #if GAME_DLL @@ -54,7 +55,7 @@ public static readonly #if CLIENT_DLL RecvPropDataTable(nameof(gmod_gamerules_data), FIELD.OF(nameof(gmod_gamerules_data)), DT_GMODRules, 0, DataTableRecvProxy_PointerDataTable) #else - SendPropDataTable(nameof(gmod_gamerules_data), DT_GMODRules) + SendPropDataTable(nameof(gmod_gamerules_data), FIELD.OF(nameof(gmod_gamerules_data)), DT_GMODRules) #endif ]); #if CLIENT_DLL diff --git a/Source.Common/DtSend.cs b/Source.Common/DtSend.cs index 3b4cf60f..15287caa 100644 --- a/Source.Common/DtSend.cs +++ b/Source.Common/DtSend.cs @@ -524,6 +524,7 @@ public SendProp(string? name, IFieldAccessor field, SendPropType type, int bits public T GetValue(object instance) { #if DEBUG ErrorIfNot(FieldInfo != null, $"SendProp.GetValue: FieldInfo is null for prop {GetName()}"); + Msg($"SendProp.GetValue for Field '{GetName()}' - '{FieldInfo.Name}' - '{FieldInfo.DeclaringType}' ({Type})\n"); #endif return FieldInfo.GetValue(instance); } diff --git a/Source.Common/FieldAccessIL.cs b/Source.Common/FieldAccessIL.cs index 12061c31..f1d70784 100644 --- a/Source.Common/FieldAccessIL.cs +++ b/Source.Common/FieldAccessIL.cs @@ -45,7 +45,7 @@ file static class ILCast static ILCast() { if (typeof(From) == typeof(To)) AssertMsg(false, "Tried to ILCast where From == To. Re-evaluate."); - + DynamicMethod method = new DynamicMethod($"ILCast<{typeof(From)}, {typeof(To)}", typeof(void), [typeof(From).MakeByRefType(), typeof(To).MakeByRefType()]); ILGenerator generator = method.GetILGenerator(); @@ -223,8 +223,12 @@ public static GetFn Get(DynamicAccessor accessor) { else if (typeof(T) == typeof(double)) il.LoggedEmit(OpCodes.Conv_R8); else il.LoggedEmit(OpCodes.Castclass, enumTypeUnderflying); } - else - il.LoggedEmit(OpCodes.Castclass, typeof(T)); + else { + if (accessor.StoringType.IsValueType && typeof(T) == typeof(object)) + il.LoggedEmit(OpCodes.Box, accessor.StoringType); + else + il.LoggedEmit(OpCodes.Castclass, typeof(T)); + } } il.LoggedEmit(OpCodes.Ret); @@ -722,9 +726,9 @@ public static bool GetConvOpcode(Type from, Type to, out OpCode opcode, out bool _ => false }; - if (from == to) + if (from == to) return true; - + if (!from.IsPrimitive || !to.IsPrimitive) return false;