diff --git a/configs/addons/counterstrikesharp/gamedata/gamedata.json b/configs/addons/counterstrikesharp/gamedata/gamedata.json
index 8cd5627d7..4602186af 100644
--- a/configs/addons/counterstrikesharp/gamedata/gamedata.json
+++ b/configs/addons/counterstrikesharp/gamedata/gamedata.json
@@ -22,28 +22,28 @@
},
"CCSPlayerController_ChangeTeam": {
"offsets": {
- "windows": 109,
- "linux": 108
+ "windows": 102,
+ "linux": 101
}
},
"CCSPlayerController_Respawn": {
"offsets": {
- "windows": 277,
- "linux": 279
+ "windows": 272,
+ "linux": 274
}
},
"CBasePlayerController_SetPawn": {
"signatures": {
"library": "server",
"windows": "44 88 4C 24 ? 53 57",
- "linux": "55 48 8D 87 ? ? ? ? 48 89 E5 41 57 41 56 41 89 CE 41 55 45 89 CD"
+ "linux": "55 48 8D 87 ? ? ? ? 48 89 E5 41 57 41 89 CF"
}
},
"CCSPlayerPawnBase_PostThink": {
"signatures": {
"library": "server",
"windows": "48 ? ? 55 53 56 57 41 ? 48 ? ? ? 48 ? ? ? ? ? ? 4C 89 68",
- "linux": "55 48 89 E5 41 56 41 55 41 54 53 48 89 FB 48 83 EC 40 E8 ? ? ? ? F3 0F 10 83"
+ "linux": "55 48 89 E5 41 57 49 89 FF 41 56 41 55 41 54 53 48 81 EC ? ? ? ? E8 ? ? ? ? 41 80 BF"
}
},
"CGameEventManager_Init": {
@@ -92,31 +92,31 @@
"signatures": {
"library": "server",
"windows": "48 89 5C 24 ? 57 48 83 EC ? 33 FF 4C 8B CA 8B D9",
- "linux": "55 31 D2 48 89 E5 41 56 41 55 41 54"
+ "linux": "55 31 D2 48 89 E5 41 57 41 56 41 55 41 54 41 89 FC"
}
},
"CCSPlayer_ItemServices_GiveNamedItem": {
"offsets": {
- "windows": 18,
- "linux": 19
+ "windows": 19,
+ "linux": 20
}
},
"CCSPlayer_ItemServices_DropActivePlayerWeapon": {
"offsets": {
- "windows": 21,
- "linux": 22
+ "windows": 22,
+ "linux": 23
}
},
"CCSPlayer_ItemServices_RemoveWeapons": {
"offsets": {
- "windows": 22,
- "linux": 23
+ "windows": 23,
+ "linux": 24
}
},
"CGameSceneNode_GetSkeletonInstance": {
"offsets": {
"windows": 8,
- "linux": 8
+ "linux": 9
}
},
"CCSGameRules_TerminateRound": {
@@ -168,8 +168,8 @@
},
"CBaseEntity_IsPlayerPawn": {
"offsets": {
- "windows": 174,
- "linux": 173
+ "windows": 168,
+ "linux": 167
}
},
"CEntitySystem_AddEntityIOEvent": {
@@ -182,14 +182,14 @@
"LegacyGameEventListener": {
"signatures": {
"library": "server",
- "windows": "48 8B 15 ? ? ? ? 48 85 D2 74 ? 83 F9 ? 77 ? 48 63 C1 48 C1 E0",
+ "windows": "48 8B 15 ? ? ? ? 48 85 D2 74 ? 83 F9 ? 77",
"linux": "48 8B 05 ? ? ? ? 48 85 C0 74 ? 83 FF ? 77 ? 48 63 FF 48 C1 E7 ? 48 8D 44 38"
}
},
"CBasePlayerPawn_CommitSuicide": {
"offsets": {
- "windows": 408,
- "linux": 408
+ "windows": 400,
+ "linux": 400
}
},
"CBasePlayerPawn_RemovePlayerItem": {
@@ -201,15 +201,15 @@
},
"CBaseEntity_Teleport": {
"offsets": {
- "windows": 168,
- "linux": 167
+ "windows": 162,
+ "linux": 161
}
},
"CBaseEntity_TakeDamageOld": {
"signatures": {
"library": "server",
"windows": "40 55 41 54 41 55 41 56 41 57 48 81 EC ? ? ? ? 48 8D 6C 24 ? 48 89 9D ? ? ? ? 45 33 ED",
- "linux": "55 48 89 E5 41 57 41 56 49 89 F6 41 55 41 54 49 89 FC 53 48 89 D3 48 83 EC ? 48 85 D2"
+ "linux": "55 48 89 E5 41 57 41 56 41 55 49 89 FD 41 54 49 89 F4 53 48 89 D3 48 83 EC ? 48 85 D2"
}
},
"CBaseTrigger_StartTouch": {
@@ -242,7 +242,7 @@
"signatures": {
"library": "server",
"windows": "4C 89 4C 24 ? 48 89 4C 24 ? 53 56",
- "linux": "55 48 89 E5 41 57 49 89 FF 41 56 41 55 41 54 49 89 D4 53 48 89 F3"
+ "linux": "55 48 89 E5 41 57 49 89 FF 41 56 41 55 41 54 49 89 D4 53 48 89 F3 48 83 EC ? 48 8D 05"
}
},
"IGameSystem_InitAllSystems_pFirst": {
@@ -297,4 +297,4 @@
"linux": "55 48 89 E5 41 57 49 89 F7 41 56 41 55 41 54 4D 89 C4"
}
}
-}
+}
\ No newline at end of file
diff --git a/libraries/hl2sdk-cs2 b/libraries/hl2sdk-cs2
index aba345d55..2530f5d98 160000
--- a/libraries/hl2sdk-cs2
+++ b/libraries/hl2sdk-cs2
@@ -1 +1 @@
-Subproject commit aba345d55e0d17fd1472bf321c192643906737b0
+Subproject commit 2530f5d9836dfa49a86d261df3aa9387e0e1ae24
diff --git a/managed/CounterStrikeSharp.API/Core/Model/CBaseEntity.cs b/managed/CounterStrikeSharp.API/Core/Model/CBaseEntity.cs
index 7533fc920..6a10eb3f9 100644
--- a/managed/CounterStrikeSharp.API/Core/Model/CBaseEntity.cs
+++ b/managed/CounterStrikeSharp.API/Core/Model/CBaseEntity.cs
@@ -120,4 +120,14 @@ public uint EmitSound(string soundEventName, RecipientFilter? recipients = null,
return NativeAPI.EmitSoundFilter(recipients.GetRecipientMask(), this.Index, soundEventName, volume, pitch);
}
+
+ ///
+ /// Returns true if the entity is a player pawn.
+ ///
+ public bool IsPlayerPawn()
+ {
+ Guard.IsValidEntity(this);
+
+ return VirtualFunction.Create(Handle, GameData.GetOffset("CBaseEntity_IsPlayerPawn"))(Handle);
+ }
}
diff --git a/managed/CounterStrikeSharp.Tests.Native/GameEventTests.cs b/managed/CounterStrikeSharp.Tests.Native/GameEventTests.cs
index 3368fade2..6c723f20b 100644
--- a/managed/CounterStrikeSharp.Tests.Native/GameEventTests.cs
+++ b/managed/CounterStrikeSharp.Tests.Native/GameEventTests.cs
@@ -20,24 +20,31 @@ public async Task CanRegisterAndDeregisterEventHandlers()
callCount++;
});
- NativeAPI.IssueServerCommand("bot_quota 0; bot_quota_mode normal");
- await WaitOneFrame();
+ try
+ {
+ NativeAPI.IssueServerCommand("bot_quota 0; bot_quota_mode normal");
+ await WaitOneFrame();
- NativeAPI.HookEvent("player_connect", callback, true);
+ NativeAPI.HookEvent("player_connect", callback, true);
- // Test hooking
- NativeAPI.IssueServerCommand("bot_kick");
- NativeAPI.IssueServerCommand("bot_add");
- await WaitOneFrame();
+ // Test hooking
+ NativeAPI.IssueServerCommand("bot_kick");
+ NativeAPI.IssueServerCommand("bot_add");
+ await WaitOneFrame();
- Assert.Equal(1, callCount);
- NativeAPI.UnhookEvent("player_connect", callback, true);
+ Assert.Equal(1, callCount);
+ NativeAPI.UnhookEvent("player_connect", callback, true);
- // Test unhooking
- NativeAPI.IssueServerCommand("bot_kick");
- NativeAPI.IssueServerCommand("bot_add");
- await WaitOneFrame();
- Assert.Equal(1, callCount);
+ // Test unhooking
+ NativeAPI.IssueServerCommand("bot_kick");
+ NativeAPI.IssueServerCommand("bot_add");
+ await WaitOneFrame();
+ Assert.Equal(1, callCount);
+ }
+ finally
+ {
+ NativeAPI.IssueServerCommand("bot_quota 5");
+ }
}
[Fact]
diff --git a/managed/CounterStrikeSharp.Tests.Native/GameTests.cs b/managed/CounterStrikeSharp.Tests.Native/GameTests.cs
new file mode 100644
index 000000000..1037715d1
--- /dev/null
+++ b/managed/CounterStrikeSharp.Tests.Native/GameTests.cs
@@ -0,0 +1,124 @@
+using System.Linq;
+using System.Numerics;
+using System.Threading.Tasks;
+using CounterStrikeSharp.API;
+using CounterStrikeSharp.API.Core;
+using CounterStrikeSharp.API.Modules.Entities.Constants;
+using CounterStrikeSharp.API.Modules.Utils;
+using Xunit;
+
+namespace NativeTestsPlugin;
+
+public class GameTests
+{
+ private CCSPlayerController player;
+ private CCSPlayerPawn? pawn;
+
+ public async Task InitializeAsync()
+ {
+ Server.ExecuteCommand("bot_kick; bot_quota 5; bot_quota_mode normal");
+ await WaitOneFrame();
+ this.player = Utilities.GetPlayers().Last(p => p.LifeState == (byte)LifeState_t.LIFE_ALIVE);
+ if (player.PlayerPawn.Value == null)
+ {
+ throw new Exception("No valid player pawn found for test player.");
+ }
+
+ this.pawn = player.PlayerPawn.Value;
+ }
+
+ [Fact]
+ public async Task Offset_CBasePlayerPawn_CommitSuicide()
+ {
+ await InitializeAsync();
+ Assert.Equal((byte)LifeState_t.LIFE_ALIVE, pawn.LifeState);
+
+ pawn.CommitSuicide(true, true);
+ await WaitOneFrame();
+ Assert.NotEqual((byte)LifeState_t.LIFE_ALIVE, pawn.LifeState);
+ }
+
+ [Fact]
+ public async Task Offset_CCSPlayer_ItemServices_RemoveWeapons()
+ {
+ await InitializeAsync();
+ Assert.True(pawn.WeaponServices.MyWeapons.Any());
+
+ player.RemoveWeapons();
+ await WaitOneFrame();
+
+ Assert.False(pawn.WeaponServices.MyWeapons.Any());
+ }
+
+ [Fact]
+ public async Task Offset_CBaseEntity_Teleport()
+ {
+ await InitializeAsync();
+ var originalPosition = pawn.AbsOrigin;
+ var newPosition = (Vector3)originalPosition + new Vector3(0, 0, 100);
+
+ pawn.Teleport(newPosition, null, null);
+ await WaitOneFrame();
+
+ Assert.Equal(newPosition.X, pawn.AbsOrigin.X, 1f);
+ Assert.Equal(newPosition.Y, pawn.AbsOrigin.Y, 1f);
+ Assert.Equal(newPosition.Z, pawn.AbsOrigin.Z, 1f);
+ }
+
+ [Fact]
+ public async Task Offset_CCSPlayerController_ChangeTeam()
+ {
+ await InitializeAsync();
+ var originalTeam = player.Team;
+ var newTeam = originalTeam == CsTeam.Terrorist ? CsTeam.CounterTerrorist : CsTeam.Terrorist;
+
+ player.ChangeTeam(newTeam);
+ await WaitOneFrame();
+
+ Assert.Equal(newTeam, player.Team);
+ }
+
+ [Fact]
+ public async Task Offset_CCSPlayer_ItemServices_GiveNamedItem()
+ {
+ await InitializeAsync();
+
+ player.RemoveWeapons();
+ await WaitOneFrame();
+
+ Assert.False(pawn.WeaponServices.MyWeapons.Any());
+
+ var weapon = player.GiveNamedItem("weapon_ak47");
+ await WaitOneFrame();
+
+ Assert.NotNull(weapon);
+ Assert.Equal("weapon_ak47", weapon.DesignerName);
+ Assert.Single(pawn.WeaponServices.MyWeapons);
+ }
+
+ [Fact]
+ public async Task Offset_CCSPlayerController_Respawn()
+ {
+ await InitializeAsync();
+ pawn.CommitSuicide(false, false);
+ await WaitOneFrame();
+ Assert.NotEqual((byte)LifeState_t.LIFE_ALIVE, pawn.LifeState);
+
+ player.Respawn();
+ await WaitOneFrame();
+
+ var newPawn = player.PlayerPawn.Value;
+ Assert.NotNull(newPawn);
+ Assert.Equal((byte)LifeState_t.LIFE_ALIVE, newPawn.LifeState);
+ }
+
+ [Fact]
+ public async Task Offset_CBaseEntity_IsPlayerPawn()
+ {
+ await InitializeAsync();
+ Assert.True(pawn.IsPlayerPawn());
+
+ var worldEnt = Utilities.GetEntityFromIndex(0);
+ Assert.False(worldEnt.IsPlayerPawn());
+ }
+}
\ No newline at end of file
diff --git a/managed/CounterStrikeSharp.Tests.Native/ListenerTests.cs b/managed/CounterStrikeSharp.Tests.Native/ListenerTests.cs
index 237edf893..b563ffd8d 100644
--- a/managed/CounterStrikeSharp.Tests.Native/ListenerTests.cs
+++ b/managed/CounterStrikeSharp.Tests.Native/ListenerTests.cs
@@ -5,6 +5,7 @@
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Memory;
using CounterStrikeSharp.API.Modules.Memory.DynamicFunctions;
+using Moq;
using Xunit;
public class ListenerTests
@@ -44,6 +45,47 @@ public async Task CanRegisterAndDeregisterListeners()
}
[Fact]
+ public async Task EntityListenersAreFired()
+ {
+ var createMock = new Mock>();
+ createMock.Setup(s => s(It.IsAny())).Callback((entityPtr) =>
+ {
+ var entity = new CBaseEntity(entityPtr);
+ Assert.Equal("prop_dynamic", entity.DesignerName);
+ });
+ var createCallback = FunctionReference.Create(createMock.Object);
+
+ var deleteMock = new Mock>();
+ deleteMock.Setup(s => s(It.IsAny())).Callback((entityPtr) =>
+ {
+ var entity = new CBaseEntity(entityPtr);
+ Assert.Equal("prop_dynamic", entity.DesignerName);
+ });
+ var deleteCallback = FunctionReference.Create(deleteMock.Object);
+
+ try
+ {
+ NativeAPI.AddListener("OnEntityCreated", createCallback);
+ NativeAPI.AddListener("OnEntityDeleted", deleteCallback);
+
+ var ent = Utilities.CreateEntityByName("prop_dynamic");
+ await WaitOneFrame();
+
+ Assert.Single(createMock.Invocations);
+
+ ent.Remove();
+ await WaitOneFrame();
+
+ Assert.Single(deleteMock.Invocations);
+ }
+ finally
+ {
+ NativeAPI.RemoveListener("OnEntityCreated", createCallback);
+ NativeAPI.RemoveListener("OnEntityDeleted", deleteCallback);
+ }
+ }
+
+ [Fact(Skip = "Damage func broken")]
public async Task TakeDamageListenersAreFired()
{
int preCallCount = 0;
@@ -80,7 +122,7 @@ public async Task TakeDamageListenersAreFired()
}
}
- [Fact]
+ [Fact(Skip = "Damage func broken")]
public async Task TakeDamageListenerCanBeCancelled()
{
int preCallCount = 0;
diff --git a/managed/CounterStrikeSharp.Tests.Native/NativeTestsPlugin.cs b/managed/CounterStrikeSharp.Tests.Native/NativeTestsPlugin.cs
index f0fc76944..8f5582d59 100644
--- a/managed/CounterStrikeSharp.Tests.Native/NativeTestsPlugin.cs
+++ b/managed/CounterStrikeSharp.Tests.Native/NativeTestsPlugin.cs
@@ -44,7 +44,8 @@ public override void Load(bool hotReload)
{
gameThreadId = Thread.CurrentThread.ManagedThreadId;
// Loading blocks the game thread, so we use NextFrame to run our tests asynchronously.
- Server.NextWorldUpdate(() => RunTests());
+ // Uncomment to run the tests on load
+ // Server.NextWorldUpdate(() => RunTests());
AddCommand("css_run_tests", "Runs the xUnit tests for the native plugin.", (player, info) => { RunTests(); });
}
diff --git a/src/mm_plugin.cpp b/src/mm_plugin.cpp
index 46f381e08..1c611badc 100644
--- a/src/mm_plugin.cpp
+++ b/src/mm_plugin.cpp
@@ -207,17 +207,7 @@ bool CounterStrikeSharpMMPlugin::Load(PluginId id, ISmmAPI* ismm, char* error, s
void CounterStrikeSharpMMPlugin::Hook_StartupServer(const GameSessionConfiguration_t& config, ISource2WorldSession*, const char*)
{
globals::entitySystem = interfaces::pGameResourceServiceServer->GetGameEntitySystem();
-
- // Temporary hack until CGameEntitySystem is updated in the sdk
-#ifdef PLATFORM_LINUX
- int offset = 8512;
-#else
- int offset = 8480;
-#endif
-
- auto pListeners = (CUtlVector*)((byte*)globals::entitySystem + offset);
-
- if (pListeners->Find(&globals::entityManager.entityListener) == -1) pListeners->AddToTail(&globals::entityManager.entityListener);
+ globals::entitySystem->AddListenerEntity(&globals::entityManager.entityListener);
globals::timerSystem.OnStartupServer();
diff --git a/tooling/CodeGen.Natives/Scripts/GenerateGameEvents.cs b/tooling/CodeGen.Natives/Scripts/GenerateGameEvents.cs
index 2eacabbe7..4871bc002 100644
--- a/tooling/CodeGen.Natives/Scripts/GenerateGameEvents.cs
+++ b/tooling/CodeGen.Natives/Scripts/GenerateGameEvents.cs
@@ -186,7 +186,8 @@ namespace CounterStrikeSharp.API.Core;
foreach (var (gameEvent, definition) in events)
{
- File.WriteAllText(Path.Join(outputPath, $"Event{gameEvent.NamePascalCase}.g.cs"), template.Replace("", definition));
+ File.WriteAllText(Path.Join(outputPath, $"Event{gameEvent.NamePascalCase}.g.cs"),
+ template.Replace("", definition).ReplaceLineEndings("\r\n"));
}
Console.WriteLine($"Generated C# bindings for {allGameEvents.Count} game events successfully.");