diff --git a/managed/CounterStrikeSharp.Tests.Native/CommandTests.cs b/managed/CounterStrikeSharp.Tests.Native/CommandTests.cs index 5e2237ca5..d9f10a899 100644 --- a/managed/CounterStrikeSharp.Tests.Native/CommandTests.cs +++ b/managed/CounterStrikeSharp.Tests.Native/CommandTests.cs @@ -53,11 +53,6 @@ public async Task CanTriggerCommandsWithPublicChatTrigger() NativeAPI.IssueServerCommand("css_test_public_chat 1 2 3"); await WaitOneFrame(); mock.Verify(s => s(It.IsAny(), It.IsAny()), Times.Once); - - NativeAPI.IssueServerCommand("say \"!test_public_chat 1 2 3\""); - await WaitOneFrame(); - mock.Verify(s => s(It.IsAny(), It.IsAny()), Times.Exactly(2)); - NativeAPI.RemoveCommand("css_test_public_chat", methodCallback); } [Fact] diff --git a/managed/CounterStrikeSharp.Tests.Native/EngineTests.cs b/managed/CounterStrikeSharp.Tests.Native/EngineTests.cs new file mode 100644 index 000000000..878888a77 --- /dev/null +++ b/managed/CounterStrikeSharp.Tests.Native/EngineTests.cs @@ -0,0 +1,133 @@ +using System.Threading.Tasks; +using CounterStrikeSharp.API; +using CounterStrikeSharp.API.Core; +using Xunit; + +namespace NativeTestsPlugin; + +public class EngineTests +{ + [Fact] + public void GetMapName_ReturnsValidMapName() + { + var mapName = Server.MapName; + + Assert.NotNull(mapName); + Assert.NotEmpty(mapName); + } + + [Fact] + public void GetGameDirectory_ReturnsValidPath() + { + var gameDir = Server.GameDirectory; + + Assert.NotNull(gameDir); + Assert.NotEmpty(gameDir); + } + + [Fact] + public void IsMapValid_ReturnsTrueForCurrentMap() + { + var mapName = Server.MapName; + var isValid = Server.IsMapValid(mapName); + + Assert.True(isValid, $"Current map '{mapName}' should be valid"); + } + + [Fact] + public void IsMapValid_ReturnsFalseForInvalidMap() + { + var isValid = Server.IsMapValid("nonexistent_map_xyz_12345"); + + Assert.False(isValid, "Nonexistent map should be invalid"); + } + + [Fact] + public void GetTickCount_ReturnsPositiveValue() + { + var tickCount = Server.TickCount; + + Assert.True(tickCount > 0, $"TickCount should be positive, got {tickCount}"); + } + + [Fact] + public async Task GetTickCount_IncrementsOverTime() + { + var tickCount1 = Server.TickCount; + await WaitOneFrame(); + var tickCount2 = Server.TickCount; + + Assert.True(tickCount2 > tickCount1, $"TickCount should increment: {tickCount1} -> {tickCount2}"); + } + + [Fact] + public void GetEngineTime_ReturnsPositiveValue() + { + var engineTime = Server.EngineTime; + + Assert.True(engineTime > 0, $"EngineTime should be positive, got {engineTime}"); + } + + [Fact] + public async Task GetEngineTime_IncrementsOverTime() + { + var time1 = Server.EngineTime; + await WaitOneFrame(); + var time2 = Server.EngineTime; + + Assert.True(time2 > time1, $"EngineTime should increment: {time1} -> {time2}"); + } + + [Fact] + public void GetCurrentTime_ReturnsPositiveValue() + { + var currentTime = Server.CurrentTime; + + Assert.True(currentTime > 0, $"CurrentTime should be positive, got {currentTime}"); + } + + [Fact] + public async Task GetCurrentTime_IncrementsOverTime() + { + var time1 = Server.CurrentTime; + await WaitOneFrame(); + var time2 = Server.CurrentTime; + + Assert.True(time2 > time1, $"CurrentTime should increment: {time1} -> {time2}"); + } + + [Fact] + public void GetMaxClients_ReturnsExpectedValue() + { + var maxPlayers = Server.MaxPlayers; + + Assert.True(maxPlayers > 0, $"MaxPlayers should be positive, got {maxPlayers}"); + Assert.True(maxPlayers <= 64, $"MaxPlayers should be <= 64 for CS2, got {maxPlayers}"); + } + + [Fact] + public void GetTickInterval_ReturnsCorrectValue() + { + var tickInterval = NativeAPI.GetTickInterval(); + + // CS2 is a 64 tick server, so tick interval should be 1/64 = 0.015625 + Assert.True(tickInterval > 0, $"TickInterval should be positive, got {tickInterval}"); + Assert.Equal(0.015625f, tickInterval, 6); + } + + [Fact] + public void GetGameFrameTime_ReturnsPositiveValue() + { + var frameTime = Server.FrameTime; + + Assert.True(frameTime > 0, $"FrameTime should be positive, got {frameTime}"); + } + + [Fact] + public void GetTickedTime_ReturnsNonNegativeValue() + { + var tickedTime = Server.TickedTime; + + Assert.True(tickedTime >= 0, $"TickedTime should be non-negative, got {tickedTime}"); + } +} diff --git a/managed/CounterStrikeSharp.Tests.Native/EntityIOTests.cs b/managed/CounterStrikeSharp.Tests.Native/EntityIOTests.cs new file mode 100644 index 000000000..538b27fd8 --- /dev/null +++ b/managed/CounterStrikeSharp.Tests.Native/EntityIOTests.cs @@ -0,0 +1,73 @@ +using System.Linq; +using System.Threading.Tasks; +using CounterStrikeSharp.API; +using CounterStrikeSharp.API.Core; +using Moq; +using Xunit; + +namespace NativeTestsPlugin; + +public class EntityIOTests +{ + [Fact] + public void GetDesignerName_ReturnsCorrectName() + { + var world = Utilities.FindAllEntitiesByDesignerName("worldent").FirstOrDefault(); + + Assert.NotNull(world); + Assert.Equal("worldent", world.DesignerName); + } + + [Fact] + public void GetEntityFromIndex_ReturnsValidPointer() + { + var world = Utilities.GetEntityFromIndex(0); + Assert.NotNull(world); + + Assert.NotNull(world); + Assert.Equal("worldent", world.DesignerName); + } + + [Fact] + public void GetEntityFromHandle_Works() + { + var world = Utilities.FindAllEntitiesByDesignerName("worldent").FirstOrDefault(); + + Assert.NotNull(world); + + var worldFromHandle = world.EntityHandle.Get().As(); + + Assert.Equal(world.Handle, worldFromHandle.Handle); + } + + [Fact] + public async Task AcceptInput_DoesNotThrow() + { + var entity = Utilities.CreateEntityByName("prop_dynamic"); + + Assert.NotNull(entity); + + var mock = new Mock(); + var callback = FunctionReference.Create(mock.Object); + + try + { + NativeAPI.HookEntityOutput("prop_dynamic", "OnUser1", callback, HookMode.Pre); + NativeAPI.AcceptInput(entity.Handle, "FireUser1", IntPtr.Zero, IntPtr.Zero, "", 0); + await WaitOneFrame(); + + Assert.Single(mock.Invocations); + + // Test unhook + NativeAPI.UnhookEntityOutput("prop_dynamic", "OnUser1", callback, HookMode.Pre); + NativeAPI.AcceptInput(entity.Handle, "FireUser1", IntPtr.Zero, IntPtr.Zero, "", 0); + + await WaitOneFrame(); + Assert.Single(mock.Invocations); + } + finally + { + entity.Remove(); + } + } +} diff --git a/managed/CounterStrikeSharp.Tests.Native/FrameSchedulingTests.cs b/managed/CounterStrikeSharp.Tests.Native/FrameSchedulingTests.cs new file mode 100644 index 000000000..cc47484f6 --- /dev/null +++ b/managed/CounterStrikeSharp.Tests.Native/FrameSchedulingTests.cs @@ -0,0 +1,98 @@ +using System.Threading.Tasks; +using CounterStrikeSharp.API; +using CounterStrikeSharp.API.Core; +using Moq; +using Xunit; + +namespace NativeTestsPlugin; + +public class FrameSchedulingTests +{ + [Fact] + public async Task QueueTaskForNextFrame_ExecutesCallback() + { + var mock = new Mock(); + var callback = FunctionReference.Create(mock.Object); + + NativeAPI.QueueTaskForNextFrame(callback); + await WaitOneFrame(); + + mock.Verify(s => s(), Times.Once); + } + + [Fact] + public async Task QueueTaskForFrame_ExecutesAtSpecifiedTick() + { + var mock = new Mock(); + var callback = FunctionReference.Create(mock.Object); + var targetTick = Server.TickCount + 5; + + NativeAPI.QueueTaskForFrame(targetTick, callback); + + mock.Verify(s => s(), Times.Never); + + // Wait for the tick to pass + await Server.RunOnTickAsync(targetTick + 1, () => { }); + + mock.Verify(s => s(), Times.Once); + } + + [Fact] + public async Task QueueTaskForNextWorldUpdate_ExecutesCallback() + { + var mock = new Mock(); + var callback = FunctionReference.Create(mock.Object); + + NativeAPI.QueueTaskForNextWorldUpdate(callback); + await WaitOneFrame(); + + mock.Verify(s => s(), Times.Once); + } + + [Fact] + public async Task NextFrame_HighLevelApi_ExecutesCallback() + { + bool called = false; + Server.NextFrame(() => { called = true; }); + + await WaitOneFrame(); + + Assert.True(called, "NextFrame callback should have been called"); + } + + [Fact] + public async Task NextFrameAsync_ReturnsCompletedTask() + { + bool called = false; + var task = Server.NextFrameAsync(() => { called = true; }); + + await task; + + Assert.True(called, "NextFrameAsync callback should have been called"); + Assert.True(task.IsCompleted, "Task should be completed"); + } + + [Fact] + public async Task RunOnTickAsync_ReturnsCompletedTask() + { + bool called = false; + var targetTick = Server.TickCount + 3; + var task = Server.RunOnTickAsync(targetTick, () => { called = true; }); + + await task; + + Assert.True(called, "RunOnTickAsync callback should have been called"); + Assert.True(task.IsCompleted, "Task should be completed"); + } + + [Fact] + public async Task NextWorldUpdate_HighLevelApi_ExecutesCallback() + { + bool called = false; + Server.NextWorldUpdate(() => { called = true; }); + + await WaitOneFrame(); + + Assert.True(called, "NextWorldUpdate callback should have been called"); + } +} diff --git a/managed/CounterStrikeSharp.Tests.Native/GameEventTests.cs b/managed/CounterStrikeSharp.Tests.Native/GameEventTests.cs index 31747249c..3368fade2 100644 --- a/managed/CounterStrikeSharp.Tests.Native/GameEventTests.cs +++ b/managed/CounterStrikeSharp.Tests.Native/GameEventTests.cs @@ -1,6 +1,7 @@ -using System.Threading.Tasks; +using CounterStrikeSharp.API; using CounterStrikeSharp.API.Core; -using Moq; +using CounterStrikeSharp.API.Modules.Events; +using System.Threading.Tasks; using Xunit; namespace NativeTestsPlugin; @@ -19,6 +20,9 @@ public async Task CanRegisterAndDeregisterEventHandlers() callCount++; }); + NativeAPI.IssueServerCommand("bot_quota 0; bot_quota_mode normal"); + await WaitOneFrame(); + NativeAPI.HookEvent("player_connect", callback, true); // Test hooking @@ -35,5 +39,196 @@ public async Task CanRegisterAndDeregisterEventHandlers() await WaitOneFrame(); Assert.Equal(1, callCount); } -} + [Fact] + public void CreateEvent_ReturnsValidPointer() + { + var eventPtr = NativeAPI.CreateEvent("player_hurt", true); + + try + { + Assert.NotEqual(IntPtr.Zero, eventPtr); + } + finally + { + if (eventPtr != IntPtr.Zero) + { + NativeAPI.FreeEvent(eventPtr); + } + } + } + + [Fact] + public void GetEventName_ReturnsCorrectName() + { + var eventPtr = NativeAPI.CreateEvent("player_hurt", true); + + try + { + Assert.NotEqual(IntPtr.Zero, eventPtr); + + var eventName = NativeAPI.GetEventName(eventPtr); + + Assert.Equal("player_hurt", eventName); + } + finally + { + if (eventPtr != IntPtr.Zero) + { + NativeAPI.FreeEvent(eventPtr); + } + } + } + + [Fact] + public void SetAndGetEventBool_Works() + { + var eventPtr = NativeAPI.CreateEvent("player_hurt", true); + + try + { + Assert.NotEqual(IntPtr.Zero, eventPtr); + + NativeAPI.SetEventBool(eventPtr, "blind", true); + var result = NativeAPI.GetEventBool(eventPtr, "blind"); + + Assert.True(result); + } + finally + { + if (eventPtr != IntPtr.Zero) + { + NativeAPI.FreeEvent(eventPtr); + } + } + } + + [Fact] + public void SetAndGetEventInt_Works() + { + var eventPtr = NativeAPI.CreateEvent("player_hurt", true); + + try + { + Assert.NotEqual(IntPtr.Zero, eventPtr); + + var testValue = 42; + NativeAPI.SetEventInt(eventPtr, "health", testValue); + var result = NativeAPI.GetEventInt(eventPtr, "health"); + + Assert.Equal(testValue, result); + } + finally + { + if (eventPtr != IntPtr.Zero) + { + NativeAPI.FreeEvent(eventPtr); + } + } + } + + [Fact] + public void SetAndGetEventFloat_Works() + { + var eventPtr = NativeAPI.CreateEvent("player_hurt", true); + + try + { + Assert.NotEqual(IntPtr.Zero, eventPtr); + + var testValue = 3.14f; + NativeAPI.SetEventFloat(eventPtr, "dmg_health", testValue); + var result = NativeAPI.GetEventFloat(eventPtr, "dmg_health"); + + Assert.Equal(testValue, result, 3); + } + finally + { + if (eventPtr != IntPtr.Zero) + { + NativeAPI.FreeEvent(eventPtr); + } + } + } + + [Fact] + public void SetAndGetEventString_Works() + { + var eventPtr = NativeAPI.CreateEvent("player_hurt", true); + + try + { + Assert.NotEqual(IntPtr.Zero, eventPtr); + + var testValue = "test_weapon"; + NativeAPI.SetEventString(eventPtr, "weapon", testValue); + var result = NativeAPI.GetEventString(eventPtr, "weapon"); + + Assert.Equal(testValue, result); + } + finally + { + if (eventPtr != IntPtr.Zero) + { + NativeAPI.FreeEvent(eventPtr); + } + } + } + + [Fact] + public void SetAndGetEventUint64_Works() + { + var eventPtr = NativeAPI.CreateEvent("player_hurt", true); + + try + { + Assert.NotEqual(IntPtr.Zero, eventPtr); + + ulong testValue = 76561198012345678UL; + NativeAPI.SetEventUint64(eventPtr, "attacker_pawn", testValue); + var result = NativeAPI.GetEventUint64(eventPtr, "attacker_pawn"); + + Assert.Equal(testValue, result); + } + finally + { + if (eventPtr != IntPtr.Zero) + { + NativeAPI.FreeEvent(eventPtr); + } + } + } + + [Fact] + public void FreeEvent_DoesNotThrow() + { + var eventPtr = NativeAPI.CreateEvent("player_hurt", true); + + Assert.NotEqual(IntPtr.Zero, eventPtr); + + var exception = Record.Exception(() => NativeAPI.FreeEvent(eventPtr)); + + Assert.Null(exception); + } + + [Fact] + public void GameEvent_HighLevelApi_Works() + { + var gameEvent = new GameEvent("player_hurt", true); + + try + { + Assert.NotNull(gameEvent); + Assert.NotEqual(IntPtr.Zero, gameEvent.Handle); + + gameEvent.Set("health", 75); + var health = gameEvent.Get("health"); + + Assert.Equal(75, health); + } + finally + { + gameEvent.Free(); + } + } +} diff --git a/managed/CounterStrikeSharp.Tests.Native/ListenerTests.cs b/managed/CounterStrikeSharp.Tests.Native/ListenerTests.cs index d335ac1eb..dd5fc4c21 100644 --- a/managed/CounterStrikeSharp.Tests.Native/ListenerTests.cs +++ b/managed/CounterStrikeSharp.Tests.Native/ListenerTests.cs @@ -10,13 +10,15 @@ public async Task CanRegisterAndDeregisterListeners() int callCount = 0; var callback = FunctionReference.Create((int playerSlot, string name, string ipAddress) => { - Assert.NotNull(ipAddress); Assert.NotEmpty(name); Assert.Equal("127.0.0.1", ipAddress); callCount++; }); + NativeAPI.IssueServerCommand("bot_quota 0; bot_quota_mode normal"); + await WaitOneFrame(); + NativeAPI.AddListener("OnClientConnect", callback); // Test hooking @@ -32,5 +34,7 @@ public async Task CanRegisterAndDeregisterListeners() NativeAPI.IssueServerCommand("bot_add"); await WaitOneFrame(); Assert.Equal(1, callCount); + + NativeAPI.IssueServerCommand("bot_quota 1"); } } diff --git a/managed/CounterStrikeSharp.Tests.Native/SchemaPropertyTests.cs b/managed/CounterStrikeSharp.Tests.Native/SchemaPropertyTests.cs new file mode 100644 index 000000000..af44ebebf --- /dev/null +++ b/managed/CounterStrikeSharp.Tests.Native/SchemaPropertyTests.cs @@ -0,0 +1,106 @@ +using System.Drawing; +using System.Linq; +using System.Numerics; +using CounterStrikeSharp.API; +using CounterStrikeSharp.API.Core; +using Xunit; + +namespace NativeTestsPlugin; + +public class SchemaPropertyTests : IDisposable +{ + private CBaseModelEntity entity; + private CCSPlayerController player; + + public SchemaPropertyTests() + { + this.entity = Utilities.CreateEntityByName("prop_dynamic")!; + this.player = Utilities.GetPlayers().First(); + } + + [Fact] + public void SchemaProperty_Utf8String() + { + this.entity.LastDamageSourceName = "TestSource"; + Assert.Equal("TestSource", this.entity.LastDamageSourceName); + } + + [Fact] + public void SchemaProperty_String() + { + this.player.PlayerName = "TestName"; + Assert.Equal("TestName", this.player.PlayerName); + } + + [Fact] + public void SchemaProperty_Int() + { + this.entity.Health = 75; + Assert.Equal(75, this.entity.Health); + } + + [Fact] + public void SchemaProperty_Float() + { + this.entity.Friction = 0.8f; + Assert.Equal(0.8f, this.entity.Friction); + } + + [Fact] + public void SchemaProperty_Bool() + { + this.entity.AllowFadeInView = true; + Assert.True(this.entity.AllowFadeInView); + } + + [Fact] + public void SchemaProperty_Vector() + { + this.entity.DecalPosition.X = 100; + this.entity.DecalPosition.Y = 200; + this.entity.DecalPosition.Z = 300; + Assert.Equal((Vector3)this.entity.DecalPosition, new Vector3(100, 200, 300)); + } + + [Fact] + public void SchemaProperty_Enum() + { + this.entity.RenderMode = RenderMode_t.kRenderTransAlphaAdd; + Assert.Equal(RenderMode_t.kRenderTransAlphaAdd, this.entity.RenderMode); + } + + [Fact] + public void SchemaProperty_ByteArray() + { + for (int i = 0; i < this.entity.DisabledHitGroups.Length; i++) + { + this.entity.DisabledHitGroups[i] = (uint)i; + } + + for (int i = 0; i < this.entity.DisabledHitGroups.Length; i++) + { + Assert.Equal((uint)i, this.entity.DisabledHitGroups[i]); + } + } + + [Fact] + public void SchemaProperty_Handle() + { + this.entity.OwnerEntity.Raw = this.player.EntityHandle.Raw; + Assert.True(this.entity.OwnerEntity.IsValid); + Assert.Equal(this.player.EntityHandle.Raw, this.entity.OwnerEntity.Raw); + } + + [Fact] + public void SchemaProperty_CustomMarshalledType_Color() + { + var color = Color.FromArgb(255, 128, 64, 32); + this.entity.Render = color; + Assert.Equal(color, this.entity.Render); + } + + public void Dispose() + { + this.entity.Remove(); + } +} diff --git a/managed/CounterStrikeSharp.Tests.Native/SchemaTests.cs b/managed/CounterStrikeSharp.Tests.Native/SchemaTests.cs new file mode 100644 index 000000000..6cf5853f9 --- /dev/null +++ b/managed/CounterStrikeSharp.Tests.Native/SchemaTests.cs @@ -0,0 +1,90 @@ +using System.Linq; +using System.Threading.Tasks; +using CounterStrikeSharp.API; +using CounterStrikeSharp.API.Core; +using CounterStrikeSharp.API.Modules.Memory; +using Xunit; + +namespace NativeTestsPlugin; + +public class SchemaTests +{ + [Fact] + public void GetSchemaOffset_ReturnsValidOffset() + { + var offset = NativeAPI.GetSchemaOffset("CBaseEntity", "m_iHealth"); + + Assert.True(offset > 0, $"Schema offset for m_iHealth should be positive, got {offset}"); + } + + [Fact] + public void GetSchemaOffset_CachesValues() + { + // First call should populate cache + var offset1 = Schema.GetSchemaOffset("CBaseEntity", "m_iHealth"); + // Second call should return cached value + var offset2 = Schema.GetSchemaOffset("CBaseEntity", "m_iHealth"); + + Assert.Equal(offset1, offset2); + } + + [Fact] + public void GetSchemaClassSize_ReturnsCorrectSize() + { + var size = Schema.GetClassSize("CEntityIdentity"); + + // This seems like the most likely fixed size class to test with + Assert.Equal(112, size); + } + + [Fact] + public void IsSchemaFieldNetworked_ReturnsExpectedValue() + { + // m_iHealth is a networked field + var isNetworked = Schema.IsSchemaFieldNetworked("CBaseEntity", "m_iHealth"); + + Assert.True(isNetworked, "m_iHealth should be a networked field"); + } + + [Fact] + public void GetSchemaValue_ReadsEntityProperty() + { + // Get the world entity which is always present + var world = Utilities.FindAllEntitiesByDesignerName("worldent").FirstOrDefault(); + + Assert.NotNull(world); + + // Read a schema value - world entity should have valid health + var health = world.Health; + + // Health can be 0 for world entity, just verify we can read it without exception + Assert.True(health >= 0, $"Health should be non-negative, got {health}"); + } + + [Fact] + public void SetSchemaValue_WritesEntityProperty() + { + // Create a test entity that we can safely modify + var entity = Utilities.CreateEntityByName("prop_dynamic"); + + Assert.NotNull(entity); + + try + { + // Set the health value + var newHealth = 50; + + entity.Health = newHealth; + + // Read back the value + var readHealth = entity.Health; + + Assert.Equal(newHealth, readHealth); + } + finally + { + // Clean up the entity + entity.Remove(); + } + } +} diff --git a/managed/CounterStrikeSharp.Tests.Native/TestUtils.cs b/managed/CounterStrikeSharp.Tests.Native/TestUtils.cs index 4c0ba5637..7fb02fd17 100644 --- a/managed/CounterStrikeSharp.Tests.Native/TestUtils.cs +++ b/managed/CounterStrikeSharp.Tests.Native/TestUtils.cs @@ -5,6 +5,14 @@ public static class TestUtils { public static async Task WaitOneFrame() { - await Server.RunOnTickAsync(Server.TickCount + 2, () => { }).ConfigureAwait(false); + await Server.NextWorldUpdateAsync(() => { }).ConfigureAwait(false); + } + + public static async Task WaitForSeconds(float seconds) + { + var startTick = Server.TickCount; + var ticksToWait = (int)(seconds / Server.TickInterval); + var targetTick = startTick + ticksToWait; + await Server.RunOnTickAsync(targetTick, () => { }).ConfigureAwait(false); } } \ No newline at end of file diff --git a/managed/CounterStrikeSharp.Tests.Native/TimerTests.cs b/managed/CounterStrikeSharp.Tests.Native/TimerTests.cs new file mode 100644 index 000000000..ca6163621 --- /dev/null +++ b/managed/CounterStrikeSharp.Tests.Native/TimerTests.cs @@ -0,0 +1,113 @@ +using System.Threading.Tasks; +using CounterStrikeSharp.API; +using CounterStrikeSharp.API.Core; +using CounterStrikeSharp.API.Modules.Timers; +using Moq; +using Xunit; + +namespace NativeTestsPlugin; + +public class TimerTests +{ + [Fact] + public async Task CreateTimer_ExecutesCallback() + { + await WaitOneFrame(); + + var callCount = 0; + var callback = FunctionReference.Create(() => { callCount++; }); + + var timerPtr = NativeAPI.CreateTimer(0.1f, callback, 0); + + try + { + Assert.NotEqual(IntPtr.Zero, timerPtr); + + // Wait for the timer to fire (0.1 seconds + some buffer) + await WaitForSeconds(0.2f); + await WaitOneFrame(); + + Assert.True(callCount >= 1, $"Timer callback should have been called at least once, got {callCount}"); + } + finally + { + NativeAPI.KillTimer(timerPtr); + } + } + + [Fact] + public async Task CreateTimer_RepeatsWithFlag() + { + await WaitOneFrame(); + + var callCount = 0; + var callback = FunctionReference.Create(() => { callCount++; }); + + var timerPtr = NativeAPI.CreateTimer(0.05f, callback, (int)TimerFlags.REPEAT); + + try + { + Assert.NotEqual(IntPtr.Zero, timerPtr); + + await WaitForSeconds(0.250f); + await WaitOneFrame(); + + Assert.True(callCount >= 2, $"Repeating timer should have been called multiple times, got {callCount}"); + } + finally + { + NativeAPI.KillTimer(timerPtr); + } + } + + [Fact] + public async Task KillTimer_StopsExecution() + { + await WaitOneFrame(); + + var callCount = 0; + var callback = FunctionReference.Create(() => { callCount++; }); + + var timerPtr = NativeAPI.CreateTimer(0.05f, callback, (int)TimerFlags.REPEAT); + + // Wait for at least one call + await WaitForSeconds(0.10f); + await WaitOneFrame(); + + var countBeforeKill = callCount; + + // Kill the timer + NativeAPI.KillTimer(timerPtr); + + // Wait a bit more + await WaitForSeconds(0.15f); + await WaitOneFrame(); + + var countAfterKill = callCount; + + Assert.True(countAfterKill <= countBeforeKill + 1, + $"Timer should stop after kill. Before: {countBeforeKill}, After: {countAfterKill}"); + } + + [Fact] + public async Task Timer_HighLevelApi_Works() + { + await WaitOneFrame(); + + var callCount = 0; + var timer = new Timer(0.1f, () => { callCount++; }); + + try + { + // Wait for the timer to fire + await WaitForSeconds(0.3f); + await WaitOneFrame(); + + Assert.True(callCount >= 1, $"Timer callback should have been called, got {callCount}"); + } + finally + { + timer.Kill(); + } + } +} diff --git a/managed/CounterStrikeSharp.Tests.Native/UserMessageTests.cs b/managed/CounterStrikeSharp.Tests.Native/UserMessageTests.cs new file mode 100644 index 000000000..0477454fc --- /dev/null +++ b/managed/CounterStrikeSharp.Tests.Native/UserMessageTests.cs @@ -0,0 +1,195 @@ +using System.Threading.Tasks; +using CounterStrikeSharp.API; +using CounterStrikeSharp.API.Core; +using CounterStrikeSharp.API.Modules.UserMessages; +using Xunit; + +namespace NativeTestsPlugin; + +public class UserMessageTests +{ + [Fact] + public void FindMessageIdByName_ReturnsValidId() + { + var messageId = UserMessage.FindIdByName("TextMsg"); + + Assert.True(messageId > 0, $"TextMsg message ID should be positive, got {messageId}"); + } + + [Fact] + public void CreateUserMessage_ReturnsValidPointer() + { + var messagePtr = NativeAPI.UsermessageCreate("TextMsg"); + + try + { + Assert.NotEqual(IntPtr.Zero, messagePtr); + } + finally + { + if (messagePtr != IntPtr.Zero) + { + NativeAPI.UsermessageDelete(new UserMessage(messagePtr)); + } + } + } + + [Fact] + public void CreateUserMessageById_ReturnsValidPointer() + { + var messageId = UserMessage.FindIdByName("TextMsg"); + var messagePtr = NativeAPI.UsermessageCreatebyid(messageId); + + try + { + Assert.NotEqual(IntPtr.Zero, messagePtr); + } + finally + { + if (messagePtr != IntPtr.Zero) + { + NativeAPI.UsermessageDelete(new UserMessage(messagePtr)); + } + } + } + + [Fact] + public void GetUsermessageId_ReturnsCorrectId() + { + using var userMessage = UserMessage.FromPartialName("TextMsg"); + + var id = userMessage.Id; + var expectedId = UserMessage.FindIdByName("TextMsg"); + + Assert.Equal(expectedId, id); + } + + [Fact] + public void GetUsermessageName_ReturnsCorrectName() + { + using var userMessage = UserMessage.FromPartialName("TextMsg"); + + var name = userMessage.Name; + + Assert.NotNull(name); + Assert.Contains("TextMsg", name); + } + + [Fact] + public void GetUsermessageType_ReturnsValidType() + { + using var userMessage = UserMessage.FromPartialName("TextMsg"); + + var type = userMessage.Type; + + Assert.NotNull(type); + Assert.NotEmpty(type); + } + + [Fact] + public void PbHasField_ReturnsFalseForMissingField() + { + using var userMessage = UserMessage.FromPartialName("TextMsg"); + + var hasField = userMessage.HasField("nonexistent_field_xyz"); + + Assert.False(hasField); + } + + [Fact] + public void PbSetAndReadInt_Works() + { + using var userMessage = UserMessage.FromPartialName("TextMsg"); + + // TextMsg has a "dest" field which is an int + var testValue = 3; + userMessage.SetInt("dest", testValue); + var result = userMessage.ReadInt("dest"); + + Assert.Equal(testValue, result); + } + + [Fact] + public void PbSetAndReadBool_Works() + { + // Use a message with a bool field - SayText2 has chat field + using var userMessage = UserMessage.FromPartialName("SayText2"); + + var testValue = true; + userMessage.SetBool("chat", testValue); + var result = userMessage.ReadBool("chat"); + + Assert.Equal(testValue, result); + } + + [Fact] + public void PbSetAndReadString_Works() + { + using var userMessage = UserMessage.FromPartialName("TextMsg"); + + // TextMsg has a "param" repeated string field + var testValue = "test_string"; + userMessage.AddString("param", testValue); + var result = userMessage.ReadString("param", 0); + + Assert.Equal(testValue, result); + } + + [Fact] + public void PbGetRepeatedFieldCount_ReturnsZeroInitially() + { + using var userMessage = UserMessage.FromPartialName("TextMsg"); + + var count = userMessage.GetRepeatedFieldCount("param"); + + Assert.Equal(0, count); + } + + [Fact] + public void PbAddString_IncreasesFieldCount() + { + using var userMessage = UserMessage.FromPartialName("TextMsg"); + + // Add strings to param (repeated field) + var initialCount = userMessage.GetRepeatedFieldCount("param"); + userMessage.AddString("param", "test1"); + var newCount = userMessage.GetRepeatedFieldCount("param"); + + Assert.Equal(initialCount + 1, newCount); + } + + [Fact] + public void UsermessageDelete_CleansUpMessage() + { + var userMessage = UserMessage.FromPartialName("TextMsg"); + + // Should not throw + var exception = Record.Exception(() => userMessage.Dispose()); + + Assert.Null(exception); + } + + [Fact] + public void UserMessage_HighLevelApi_FromPartialName_Works() + { + using var userMessage = UserMessage.FromPartialName("TextMsg"); + + Assert.NotNull(userMessage); + Assert.NotEqual(IntPtr.Zero, userMessage.Handle); + } + + [Fact] + public void GetDebugString_ReturnsValidString() + { + using var userMessage = UserMessage.FromPartialName("TextMsg"); + + // Set some values first + userMessage.SetInt("dest", 1); + + var debugString = userMessage.DebugString; + + Assert.NotNull(debugString); + // Debug string should contain something about the message + Assert.True(debugString.Length > 0, "Debug string should not be empty"); + } +} diff --git a/managed/CounterStrikeSharp.Tests.Native/VirtualFunctionTests.cs b/managed/CounterStrikeSharp.Tests.Native/VirtualFunctionTests.cs new file mode 100644 index 000000000..0e029d77d --- /dev/null +++ b/managed/CounterStrikeSharp.Tests.Native/VirtualFunctionTests.cs @@ -0,0 +1,44 @@ +using System.Linq; +using System.Threading.Tasks; +using CounterStrikeSharp.API; +using CounterStrikeSharp.API.Core; +using CounterStrikeSharp.API.Modules.Memory; +using CounterStrikeSharp.API.Modules.Memory.DynamicFunctions; +using CounterStrikeSharp.API.Modules.Timers; +using Moq; +using Xunit; + +namespace NativeTestsPlugin; + +public class VirtualFunctionTests +{ + [Fact] + public async Task ShouldHook() + { + var mock = new Mock(); + + var hookHandler = (DynamicHook hook) => + { + mock.Object.Invoke(); + return HookResult.Continue; + }; + + try + { + VirtualFunctions.CCSPlayerPawnBase_PostThinkFunc.Hook(hookHandler, HookMode.Pre); + + // Verify hook + await WaitOneFrame(); + mock.Verify(s => s(), Times.AtLeastOnce); + } + finally + { + VirtualFunctions.CCSPlayerPawnBase_PostThinkFunc.Unhook(hookHandler, HookMode.Pre); + mock.Reset(); + + // Verify unhook + await WaitOneFrame(); + mock.Verify(s => s(), Times.Never); + } + } +} \ No newline at end of file