🔦 Modernize flashlight for CSS/.NET 10

Bump to CounterStrikeSharp 1.0.371 and .NET 10, replace per-tick light
spawns with a parented light_barn, add plugin config, real unit tests,
and refresh CI plus docs.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Tobias Thiele 2026-07-21 07:26:30 +02:00
parent 081278a8ae
commit a0ef6311ee
13 changed files with 847 additions and 434 deletions

View file

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
@ -9,21 +9,20 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="xunit" Version="2.6.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.4">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.1">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="6.0.0">
<PackageReference Include="coverlet.collector" Version="6.0.4">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Moq" Version="4.20.69" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../Flashlight/Flashlight.csproj" />
</ItemGroup>
</Project>
</Project>

View file

@ -1,3 +1,4 @@
using System.Numerics;
using Xunit;
namespace Flashlight.Tests;
@ -5,137 +6,119 @@ namespace Flashlight.Tests;
public class FlashlightLogicTests
{
[Fact]
public void FlashlightState_TogglesCorrectly()
public void TryToggle_TogglesWhenAllowed()
{
// Test the core logic of flashlight state toggling
var playerKey = "test_player_1";
var flashlightState = new Dictionary<string, bool>();
// Initial state should be off
flashlightState[playerKey] = false;
Assert.False(flashlightState[playerKey]);
// Toggle on
flashlightState[playerKey] = !flashlightState[playerKey];
Assert.True(flashlightState[playerKey]);
// Toggle off
flashlightState[playerKey] = !flashlightState[playerKey];
Assert.False(flashlightState[playerKey]);
}
[Fact]
public void FlashlightState_TracksMultiplePlayers()
{
var playerStates = new Dictionary<string, bool>();
var player1 = "player_1";
var player2 = "player_2";
var player3 = "player_3";
// Initialize all off
playerStates[player1] = false;
playerStates[player2] = false;
playerStates[player3] = false;
// Toggle player 1 on
playerStates[player1] = !playerStates[player1];
Assert.True(playerStates[player1]);
Assert.False(playerStates[player2]);
Assert.False(playerStates[player3]);
// Toggle player 2 on
playerStates[player2] = !playerStates[player2];
Assert.True(playerStates[player1]);
Assert.True(playerStates[player2]);
Assert.False(playerStates[player3]);
}
[Fact]
public void ToggleCooldown_PreventsRapidToggling()
{
// Simulate the cooldown mechanism
var isOn = false;
var canToggle = true;
// First toggle - should work
Assert.True(canToggle);
canToggle = false; // Simulate setting cooldown
// Second toggle - should be blocked
Assert.True(FlashlightLogic.TryToggle(ref isOn, ref canToggle));
Assert.True(isOn);
Assert.False(canToggle);
// After cooldown expires
canToggle = true;
Assert.True(canToggle);
}
[Fact]
public void CrouchState_UpdatesCorrectly()
public void TryToggle_BlockedDuringCooldown()
{
var isCrouching = false;
var buttons = 0;
const int DuckButton = 1 << 2; // Typical duck button bit
// Not crouching initially
Assert.False(isCrouching);
// Press duck button
buttons |= DuckButton;
if ((buttons & DuckButton) != 0)
var isOn = true;
var canToggle = false;
Assert.False(FlashlightLogic.TryToggle(ref isOn, ref canToggle));
Assert.True(isOn);
Assert.False(canToggle);
}
[Fact]
public void IsUsePressedEdge_OnlyTrueOnRisingEdge()
{
Assert.True(FlashlightLogic.IsUsePressedEdge(true, false));
Assert.False(FlashlightLogic.IsUsePressedEdge(true, true));
Assert.False(FlashlightLogic.IsUsePressedEdge(false, true));
Assert.False(FlashlightLogic.IsUsePressedEdge(false, false));
}
[Theory]
[InlineData(false, 64f, 46f, 64f)]
[InlineData(true, 64f, 46f, 46f)]
public void GetEyeOffsetZ_UsesCrouchWhenDucking(
bool crouching,
float stand,
float crouch,
float expected)
{
Assert.Equal(expected, FlashlightLogic.GetEyeOffsetZ(crouching, stand, crouch));
}
[Fact]
public void CalculateLightOrigin_AppliesEyeAndForwardOffsets()
{
var origin = FlashlightLogic.CalculateLightOrigin(
new Vector3(10f, 20f, 30f),
new Vector3(1f, 0f, 0f),
eyeOffsetZ: 64f,
forwardDistance: 54f);
Assert.Equal(new Vector3(64f, 20f, 94f), origin);
}
[Fact]
public void ForwardFromAnglesDegrees_YawZeroLooksAlongX()
{
var forward = FlashlightLogic.ForwardFromAnglesDegrees(0f, 0f);
Assert.Equal(1f, forward.X, 3);
Assert.Equal(0f, forward.Y, 3);
Assert.Equal(0f, forward.Z, 3);
}
[Fact]
public void ShouldCreateAndDestroyLight_Policies()
{
Assert.True(FlashlightLogic.ShouldCreateLight(isOn: true, hasValidLight: false));
Assert.False(FlashlightLogic.ShouldCreateLight(isOn: true, hasValidLight: true));
Assert.True(FlashlightLogic.ShouldDestroyLight(isOn: false, hasValidLight: true));
Assert.False(FlashlightLogic.ShouldDestroyLight(isOn: false, hasValidLight: false));
}
[Fact]
public void ConfigClamp_FixesInvalidValues()
{
var config = new FlashlightConfig
{
isCrouching = true;
}
Assert.True(isCrouching);
// Release duck button
buttons &= ~DuckButton;
if ((buttons & DuckButton) == 0)
{
isCrouching = false;
}
Assert.False(isCrouching);
ToggleCooldownSeconds = -1f,
Brightness = -5f,
Range = 0f,
ColorTemperature = 50f,
SoftX = -1f,
SoftY = -1f,
Skirt = -1f,
SkirtNear = -1f,
SizeX = -1f,
SizeY = -1f,
SizeZ = -1f,
ForwardDistance = -10f,
StandEyeOffsetZ = -1f,
CrouchEyeOffsetZ = -1f,
AttachmentName = " ",
LightCookie = ""
};
config.Clamp();
Assert.Equal(0f, config.ToggleCooldownSeconds);
Assert.Equal(0f, config.Brightness);
Assert.Equal(1f, config.Range);
Assert.Equal(1000f, config.ColorTemperature);
Assert.Equal(0f, config.SoftX);
Assert.Equal(0f, config.SoftY);
Assert.Equal(0f, config.Skirt);
Assert.Equal(0f, config.SkirtNear);
Assert.Equal(0f, config.SizeX);
Assert.Equal(0f, config.SizeY);
Assert.Equal(0f, config.SizeZ);
Assert.Equal(0f, config.ForwardDistance);
Assert.Equal(0f, config.StandEyeOffsetZ);
Assert.Equal(0f, config.CrouchEyeOffsetZ);
Assert.Equal("axis_of_intent", config.AttachmentName);
Assert.Equal("materials/effects/lightcookies/flashlight.vtex", config.LightCookie);
}
[Fact]
public void LightPosition_CalculatesCrouchOffsetCorrectly()
{
// Test the position calculation logic
var baseZ = 100f;
var standOffset = 64.03f;
var crouchOffset = 46.03f;
// Standing position
var standPosition = baseZ + standOffset;
Assert.Equal(164.03f, standPosition);
// Crouching position
var crouchPosition = baseZ + crouchOffset;
Assert.Equal(146.03f, crouchPosition);
}
[Fact]
public void FlashlightEntity_Management()
{
// Test entity tracking dictionary behavior
var playerEntities = new Dictionary<string, FakeLightEntity>();
var playerKey = "test_player";
// No entity initially
Assert.False(playerEntities.TryGetValue(playerKey, out _));
// Add entity
var light = new FakeLightEntity { IsValid = true };
playerEntities[playerKey] = light;
Assert.True(playerEntities.TryGetValue(playerKey, out var retrieved));
Assert.True(retrieved?.IsValid);
// Remove entity
playerEntities.Remove(playerKey);
Assert.False(playerEntities.TryGetValue(playerKey, out _));
}
private class FakeLightEntity
{
public bool IsValid { get; set; }
public void Remove() => IsValid = false;
}
}
}