diff --git a/.gitignore b/.gitignore index 1a95523..74439e9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .idea **/bin/ **/obj/ -.DS_Store \ No newline at end of file +.DS_Store +docs/ diff --git a/Flashlight.Tests/FlashlightLogicTests.cs b/Flashlight.Tests/FlashlightLogicTests.cs index 06e0d47..5276d5c 100644 --- a/Flashlight.Tests/FlashlightLogicTests.cs +++ b/Flashlight.Tests/FlashlightLogicTests.cs @@ -121,4 +121,32 @@ public class FlashlightLogicTests Assert.Equal("axis_of_intent", config.AttachmentName); Assert.Equal("materials/effects/lightcookies/flashlight.vtex", config.LightCookie); } + + [Theory] + [InlineData("Any", 2, true)] + [InlineData("Any", 3, true)] + [InlineData("Any", 1, true)] + [InlineData("T", 2, true)] + [InlineData("T", 3, false)] + [InlineData("CT", 3, true)] + [InlineData("CT", 2, false)] + public void IsTeamAllowed_RespectsConfiguredSide(string allowedTeam, byte team, bool expected) + { + Assert.Equal(expected, FlashlightLogic.IsTeamAllowed(allowedTeam, team)); + } + + [Theory] + [InlineData("t", "T")] + [InlineData("Terrorist", "T")] + [InlineData("ct", "CT")] + [InlineData("CounterTerrorist", "CT")] + [InlineData("any", "Any")] + [InlineData("something-else", "Any")] + [InlineData(null, "Any")] + public void ConfigClamp_NormalizesAllowedTeam(string? input, string expected) + { + var config = new FlashlightConfig { AllowedTeam = input! }; + config.Clamp(); + Assert.Equal(expected, config.AllowedTeam); + } } diff --git a/Flashlight/Flashlight.cs b/Flashlight/Flashlight.cs index 75935b9..910c8a9 100644 --- a/Flashlight/Flashlight.cs +++ b/Flashlight/Flashlight.cs @@ -16,7 +16,7 @@ public class FlashlightPlugin : BasePlugin, IPluginConfig public override string ModuleAuthor => "creazy.eth"; public override string ModuleName => "Flashlight"; public override string ModuleDescription => "Flashlight for Counter-Strike 2"; - public override string ModuleVersion => "0.1.0"; + public override string ModuleVersion => "0.1.1"; public FlashlightConfig Config { get; set; } = new(); @@ -169,6 +169,12 @@ public class FlashlightPlugin : BasePlugin, IPluginConfig private void TryToggleFlashlight(CCSPlayerController player, PlayerFlashlightState state) { + // Turning on is restricted by AllowedTeam; turning off is always allowed. + if (!state.IsOn && !FlashlightLogic.IsTeamAllowed(Config.AllowedTeam, (byte)player.Team)) + { + return; + } + var isOn = state.IsOn; var canToggle = state.CanToggle; diff --git a/Flashlight/FlashlightConfig.cs b/Flashlight/FlashlightConfig.cs index 14c131f..3da2af8 100644 --- a/Flashlight/FlashlightConfig.cs +++ b/Flashlight/FlashlightConfig.cs @@ -11,6 +11,12 @@ public class FlashlightConfig : BasePluginConfig [JsonPropertyName("AllowUseKey")] public bool AllowUseKey { get; set; } = true; + /// + /// Which team may use the flashlight: Any, CT, or T. + /// + [JsonPropertyName("AllowedTeam")] + public string AllowedTeam { get; set; } = "Any"; + [JsonPropertyName("ToggleCooldownSeconds")] public float ToggleCooldownSeconds { get; set; } = 0.25f; @@ -97,5 +103,17 @@ public class FlashlightConfig : BasePluginConfig { LightCookie = "materials/effects/lightcookies/flashlight.vtex"; } + + AllowedTeam = NormalizeAllowedTeam(AllowedTeam); + } + + public static string NormalizeAllowedTeam(string? value) + { + return value?.Trim().ToUpperInvariant() switch + { + "T" or "TERRORIST" or "TERRORISTS" => "T", + "CT" or "COUNTERTERRORIST" or "COUNTERTERRORISTS" or "COUNTER-TERRORIST" or "COUNTER-TERRORISTS" => "CT", + _ => "Any" + }; } } diff --git a/Flashlight/FlashlightLogic.cs b/Flashlight/FlashlightLogic.cs index f938225..7827943 100644 --- a/Flashlight/FlashlightLogic.cs +++ b/Flashlight/FlashlightLogic.cs @@ -59,4 +59,18 @@ public static class FlashlightLogic { return !isOn && hasValidLight; } + + /// + /// Returns whether may use the flashlight. + /// Team values match CS2: 2 = Terrorist, 3 = Counter-Terrorist. + /// + public static bool IsTeamAllowed(string allowedTeam, byte team) + { + return allowedTeam switch + { + "T" => team == 2, + "CT" => team == 3, + _ => true + }; + } } diff --git a/README.md b/README.md index f866d8e..93a1018 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ Flashlight is a Counter-Strike 2 server plugin written in C# with [CounterStrike - Toggle with the Use key (`E` by default) or `css_fl_toggle` - One `light_barn` per player, parented to the pawn attachment (no per-tick spawn/teleport) - Configurable brightness, range, color, shadows, offsets, and attachment +- Optional team restriction (`Any`, `CT`, or `T`) - Automatically turns off on death, spawn, and team change - Bots ignored @@ -38,6 +39,7 @@ On first load, CounterStrikeSharp writes a JSON config for the plugin. Defaults: | --- | --- | --- | | `Enabled` | `true` | Master switch | | `AllowUseKey` | `true` | Allow Use-key toggle | +| `AllowedTeam` | `Any` | Who may use it: `Any`, `CT`, or `T` | | `ToggleCooldownSeconds` | `0.25` | Toggle cooldown | | `Brightness` | `1.0` | Light brightness | | `Range` | `2048` | Light range | @@ -77,11 +79,12 @@ Unit tests cover toggle/cooldown logic, Use-key edge detection, origin math, cre ## Changelog -### v0.1.0 +### v0.1.1 - Updated to .NET 10 and CounterStrikeSharp.API 1.0.371 - Replaced per-tick `light_omni2` spawn/teleport with parented `light_barn` - Added `IPluginConfig` settings for light and toggle behavior +- Added `AllowedTeam` config (`Any` / `CT` / `T`) to restrict flashlight by side - Added focused xUnit tests for pure helpers - Updated GitHub Actions for .NET 10, PR tests, and tag releases - Switched logging to `BasePlugin.Logger` diff --git a/docs/superpowers/plans/2026-07-21-flashlight-modernization.md b/docs/superpowers/plans/2026-07-21-flashlight-modernization.md deleted file mode 100644 index edbae99..0000000 --- a/docs/superpowers/plans/2026-07-21-flashlight-modernization.md +++ /dev/null @@ -1,61 +0,0 @@ -# Flashlight Modernization Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Modernize the flashlight plugin to .NET 10 / CSS 1.0.371 with a parented `light_barn` implementation, config, real unit tests, fixed CI, and updated README. - -**Architecture:** Pure helpers in `FlashlightLogic` + config clamps in `FlashlightConfig`; plugin owns lifecycle and wires Use/command/events; one `CBarnLight` per active player, parented once, toggled via create/destroy. - -**Tech Stack:** .NET 10, CounterStrikeSharp.API 1.0.371, xUnit, GitHub Actions - -## Global Constraints - -- Target framework: `net10.0` -- Package: `CounterStrikeSharp.API` `1.0.371` -- `[MinimumApiVersion(371)]` -- Module version: `0.1.0` -- No per-tick entity create/spawn/teleport in steady state -- Spec: `docs/superpowers/specs/2026-07-21-flashlight-modernization-design.md` - ---- - -### Task 1: Project targets + pure logic + tests - -**Files:** -- Modify: `Flashlight/Flashlight.csproj` -- Modify: `Flashlight.Tests/Flashlight.Tests.csproj` -- Create: `Flashlight/FlashlightConfig.cs` -- Create: `Flashlight/FlashlightLogic.cs` -- Create: `Flashlight/PlayerFlashlightState.cs` -- Modify: `Flashlight.Tests/FlashlightLogicTests.cs` - -- [x] Retarget both projects to `net10.0`; bump CSS to `1.0.371`; bump test packages as needed -- [x] Implement `FlashlightConfig` with defaults from the spec and a `Clamp()` method -- [x] Implement pure `FlashlightLogic` helpers: try-toggle with cooldown, eye Z offset, origin from base+forward -- [x] Replace smoke tests with tests against those helpers -- [x] Run `dotnet test` and confirm pass - -### Task 2: Plugin rewrite (parented light_barn) - -**Files:** -- Modify: `Flashlight/Flashlight.cs` -- Delete or gut: `Flashlight/LogHelper.cs` (prefer `Logger`) - -- [x] Implement `BasePlugin, IPluginConfig` -- [x] OnTick: Use-key edge + cooldown only when `AllowUseKey` -- [x] Create/parent/enable `light_barn` once on toggle on; remove on toggle off -- [x] Cleanup on death/spawn/team/disconnect/unload -- [x] Keep `css_fl_toggle` -- [x] Run `dotnet build` and confirm success - -### Task 3: CI + README - -**Files:** -- Modify: `.github/workflows/build.yml` -- Modify: `README.md` - -- [x] CI: .NET 10, restore, test, release build -- [x] README: versions, parented light, config table, build/test, changelog 0.1.0 -- [x] Run `dotnet test` once more - ---- diff --git a/docs/superpowers/specs/2026-07-21-flashlight-modernization-design.md b/docs/superpowers/specs/2026-07-21-flashlight-modernization-design.md deleted file mode 100644 index dcc2079..0000000 --- a/docs/superpowers/specs/2026-07-21-flashlight-modernization-design.md +++ /dev/null @@ -1,148 +0,0 @@ -# Flashlight Plugin Modernization Design - -**Date:** 2026-07-21 -**Status:** Approved -**Version target:** 0.1.0 - -## Goal - -Modernize the CounterStrikeSharp flashlight plugin to the latest CSS/.NET stack, replace the per-tick spawn/teleport light path with a parented `light_barn` implementation, add `IPluginConfig` for admin tuning, improve testability, and update the README. - -## Background - -The current plugin (`Flashlight` v0.0.7) targets CounterStrikeSharp.API `1.0.363` on .NET 8. While the flashlight is on, `OnTick` creates a `light_omni2` and calls `DispatchSpawn` every server frame. That is the dominant performance problem. Existing tests only exercise inlined dictionary/bool logic and do not cover extractable plugin helpers. CI still installs .NET 7. - -Latest CounterStrikeSharp.API is `1.0.371` and targets .NET 10. CS2Fixes demonstrates the preferred flashlight pattern: spawn `light_barn` once, set a flashlight lightcookie via entity keyvalues, parent to the player pawn attachment, and toggle enablement rather than teleporting every tick. - -## Decisions - -| Topic | Decision | -| --- | --- | -| Runtime | .NET 10 + CounterStrikeSharp.API 1.0.371 | -| Light entity | `light_barn` (`CBarnLight`), parented once | -| Position updates | Engine parenting; no per-tick teleport | -| Config | `IPluginConfig` with JSON config | -| Logging | Prefer `BasePlugin.Logger` | -| README | Fully updated for new runtime, behavior, config, build/test | -| Module version | 0.1.0 | - -## Architecture - -``` -FlashlightPlugin (BasePlugin, IPluginConfig) - ├── FlashlightConfig // JSON-backed settings + clamps - ├── PlayerFlashlightState // per-player flags + entity handle - ├── FlashlightService // create / parent / enable / destroy - └── FlashlightLogic // pure helpers (toggle, cooldown, offsets) -``` - -### Runtime flow - -1. `OnConfigParsed` validates/clamps config. -2. `Load` registers `OnTick` (Use-key edge + cooldown only), game event handlers, and command `css_fl_toggle`. -3. When a player turns the light on: - - Create `CBarnLight` via `Utilities.CreateEntityByName("light_barn")`. - - Apply config (brightness, range, color, temperature, soft/skirt/size, cast shadows, direct light). - - Compute initial origin: pawn origin + eye Z offset + forward * `ForwardDistance`. - - `Teleport` once using `System.Numerics.Vector3` overloads (avoid legacy `Vector` allocs). - - `DispatchSpawn(CEntityKeyValues)` with `lightcookie` = configured path (default flashlight vtex). - - `AcceptInput("SetParent", pawn, …)` then `AcceptInput("SetParentAttachmentMaintainOffset", …, AttachmentName)`. - - Set `Enabled = true`. -4. When turned off: set `Enabled = false` and remove the entity (or disable and keep — prefer remove to avoid orphaned entities across pawn changes). -5. Cleanup on death, team change, disconnect, and plugin unload. - -### Why not keep tick teleport? - -Parenting follows view/attachment with far less managed work and no entity churn. Tick work is limited to scanning connected humans for Use-button edges and cooldown expiry. - -## Config surface - -File written/loaded by CSS config system (standard plugin config JSON). - -| Key | Type | Default | Notes | -| --- | --- | --- | --- | -| `Enabled` | bool | `true` | Master switch | -| `AllowUseKey` | bool | `true` | Toggle via Use (`E`) | -| `ToggleCooldownSeconds` | float | `0.25` | Clamp ≥ 0 | -| `Brightness` | float | `1.0` | | -| `Range` | float | `2048` | Match CS2Fixes-style defaults | -| `ColorR` / `ColorG` / `ColorB` | byte | `255` | White | -| `ColorTemperature` | float | `6500` | | -| `CastShadows` | bool | `true` | Maps to `CastShadows` int | -| `SoftX` / `SoftY` | float | `1.0` | | -| `Skirt` | float | `0.5` | | -| `SkirtNear` | float | `1.0` | | -| `SizeX` / `SizeY` / `SizeZ` | float | `45` / `45` / `0.03` | `SizeParams` | -| `ForwardDistance` | float | `54` | Avoid AWP blocking beam | -| `StandEyeOffsetZ` | float | `64` | | -| `CrouchEyeOffsetZ` | float | `46` | Used at spawn time only | -| `AttachmentName` | string | `axis_of_intent` | Parent attachment | -| `LightCookie` | string | `materials/effects/lightcookies/flashlight.vtex` | | - -Invalid values are clamped or rejected in `OnConfigParsed` with log warnings; plugin remains loadable when possible. - -## Player state - -Replace multiple `Dictionary` maps with one structure keyed by player slot (or controller), holding: - -- `IsOn` -- `CanToggle` -- `Light` (`CBarnLight?`) - -Crouch tracking for continuous Z updates is unnecessary once the light is parented; crouch offset is only applied at creation time. Optional: read duck state at spawn for initial Z only. - -## Event / command behavior (unchanged UX) - -- Use key toggles when `AllowUseKey` is true and cooldown allows. -- `css_fl_toggle` remains client-only command alternative. -- Flashlight turns off on death and spawn; entity cleaned on team change and disconnect. -- Bots ignored. - -## Testing strategy - -Full CSS entity lifecycle cannot be unit-tested without a game server. Extract and test pure logic: - -1. Toggle state transitions and cooldown gating. -2. Config clamp helpers (range, cooldown, color channels). -3. Initial position offset calculation (stand/crouch Z + forward distance given basis vectors). -4. Enable/disable policy: when entity should be created vs destroyed. - -Use xUnit on .NET 10. Keep Moq only if needed; prefer plain helpers over mocking CSS types. - -## CI / packaging - -- GitHub Actions: .NET 10 SDK, `dotnet restore`, `dotnet test`, release build on tags. -- Remove stale .NET 7 setup. -- Publish zip layout unchanged: `plugins/Flashlight/`. - -## README updates - -- Prerequisites: .NET 10, CSS 1.0.371+. -- Behavior: parented `light_barn`, Use + command. -- Config table with defaults. -- Build / test instructions. -- Changelog entry for 0.1.0 (API bump, performance rewrite, config, tests, CI). - -## Out of scope - -- Particle flashlight mode (CS2Fixes mode 2). -- Admin permissions / VIP-only flashlight. -- Client-side HUD indicators. -- Migrating to CounterStrikeSharp 2.0 alpha. - -## Risks / mitigations - -| Risk | Mitigation | -| --- | --- | -| Attachment name missing on some models | Configurable `AttachmentName`; fall back to parent-only if attachment input fails | -| `CEntityKeyValues` lightcookie path differs | Use CS2Fixes-proven path; document override | -| .NET 10 server prerequisite | Document clearly; MinimumApiVersion 371 | -| Parenting breaks on pawn swap | Recreate light on spawn; destroy on death/team | - -## Success criteria - -- Builds against CounterStrikeSharp.API 1.0.371 on net10.0. -- No entity create/spawn/teleport in the steady-state OnTick path. -- Config file generated and honored. -- Unit tests cover pure helpers and pass in CI. -- README matches shipped behavior and versions.