diff --git a/Flashlight.Tests/FlashlightLogicTests.cs b/Flashlight.Tests/FlashlightLogicTests.cs index c42276f..5276d5c 100644 --- a/Flashlight.Tests/FlashlightLogicTests.cs +++ b/Flashlight.Tests/FlashlightLogicTests.cs @@ -70,79 +70,6 @@ public class FlashlightLogicTests Assert.Equal(0f, forward.Z, 3); } - [Fact] - public void ForwardFromAnglesDegrees_PitchDrivesVerticalComponent() - { - // Source angles are inverted on pitch: positive pitch looks down. - Assert.True(FlashlightLogic.ForwardFromAnglesDegrees(45f, 0f).Z < 0f); - Assert.True(FlashlightLogic.ForwardFromAnglesDegrees(-45f, 0f).Z > 0f); - } - - [Fact] - public void HorizontalForwardFromYawDegrees_HasNoVerticalComponent() - { - foreach (var yaw in new[] { -180f, -90f, -45f, 0f, 45f, 90f, 180f }) - { - Assert.Equal(0f, FlashlightLogic.HorizontalForwardFromYawDegrees(yaw).Z, 5); - } - } - - [Fact] - public void CalculateLightTransform_AnglesCarryFullViewRotation() - { - var transform = FlashlightLogic.CalculateLightTransform( - pawnOrigin: new Vector3(0f, 0f, 0f), - pitch: -35f, - yaw: 90f, - roll: 12f, - eyeOffsetZ: 64f, - forwardDistance: 54f); - - // Pitch must survive into the light's angles, otherwise the beam only tracks yaw. - Assert.Equal(-35f, transform.Angles.X, 3); - Assert.Equal(90f, transform.Angles.Y, 3); - Assert.Equal(12f, transform.Angles.Z, 3); - } - - [Theory] - [InlineData(-89f)] - [InlineData(-45f)] - [InlineData(0f)] - [InlineData(45f)] - [InlineData(89f)] - public void CalculateLightTransform_PitchNeverMovesTheOrigin(float pitch) - { - var transform = FlashlightLogic.CalculateLightTransform( - new Vector3(0f, 0f, 0f), - pitch, - yaw: 0f, - roll: 0f, - eyeOffsetZ: 64f, - forwardDistance: 54f); - - // The muzzle stays at eye height and eye-forward regardless of pitch, so - // looking up or down can never shove the light through the ceiling or floor. - Assert.Equal(54f, transform.Origin.X, 3); - Assert.Equal(0f, transform.Origin.Y, 3); - Assert.Equal(64f, transform.Origin.Z, 3); - } - - [Fact] - public void CalculateLightTransform_YawDrivesHorizontalOffset() - { - var transform = FlashlightLogic.CalculateLightTransform( - new Vector3(10f, 20f, 30f), - pitch: 0f, - yaw: 90f, - roll: 0f, - eyeOffsetZ: 64f, - forwardDistance: 54f); - - Assert.Equal(10f, transform.Origin.X, 3); - Assert.Equal(74f, transform.Origin.Y, 3); - Assert.Equal(94f, transform.Origin.Z, 3); - } - [Fact] public void ShouldCreateAndDestroyLight_Policies() { @@ -171,6 +98,7 @@ public class FlashlightLogicTests ForwardDistance = -10f, StandEyeOffsetZ = -1f, CrouchEyeOffsetZ = -1f, + AttachmentName = " ", LightCookie = "" }; @@ -190,6 +118,7 @@ public class FlashlightLogicTests 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); } diff --git a/Flashlight/Flashlight.cs b/Flashlight/Flashlight.cs index f3f6267..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.2"; + public override string ModuleVersion => "0.1.1"; public FlashlightConfig Config { get; set; } = new(); @@ -57,49 +57,27 @@ public class FlashlightPlugin : BasePlugin, IPluginConfig private void OnTick() { - if (!Config.Enabled) + if (!Config.Enabled || !Config.AllowUseKey) { return; } foreach (var player in Utilities.GetPlayers()) { - if (!IsEligiblePlayer(player)) + if (!IsEligiblePlayer(player) || !player.PawnIsAlive) { continue; } - if (!player.PawnIsAlive) - { - // An un-parented light no longer dies together with the pawn, so sweep it up here - // in case a death or round-end never reached the event handlers. - if (_playerStates.TryGetValue(player.Slot, out var deadState) && deadState.IsOn) - { - deadState.IsOn = false; - DestroyLight(player.Slot); - } - - continue; - } - var state = EnsureState(player); + var usePressed = (player.Buttons & PlayerButtons.Use) != 0; - if (Config.AllowUseKey) + if (FlashlightLogic.IsUsePressedEdge(usePressed, state.WasUsePressed)) { - var usePressed = (player.Buttons & PlayerButtons.Use) != 0; - - if (FlashlightLogic.IsUsePressedEdge(usePressed, state.WasUsePressed)) - { - TryToggleFlashlight(player, state); - } - - state.WasUsePressed = usePressed; + TryToggleFlashlight(player, state); } - if (state.IsOn) - { - UpdateLight(player, state); - } + state.WasUsePressed = usePressed; } } @@ -210,7 +188,7 @@ public class FlashlightPlugin : BasePlugin, IPluginConfig if (state.IsOn) { - CreateLight(player, state); + CreateAndParentLight(player, state); } else { @@ -227,7 +205,7 @@ public class FlashlightPlugin : BasePlugin, IPluginConfig }); } - private void CreateLight(CCSPlayerController player, PlayerFlashlightState state) + private void CreateAndParentLight(CCSPlayerController player, PlayerFlashlightState state) { DestroyLight(player.Slot); @@ -247,6 +225,20 @@ public class FlashlightPlugin : BasePlugin, IPluginConfig return; } + var isCrouching = (player.Buttons & PlayerButtons.Duck) != 0; + var eyeOffsetZ = FlashlightLogic.GetEyeOffsetZ( + isCrouching, + Config.StandEyeOffsetZ, + Config.CrouchEyeOffsetZ); + + var angles = pawn.V_angle; + var forward = FlashlightLogic.ForwardFromAnglesDegrees(angles.X, angles.Y); + var origin = FlashlightLogic.CalculateLightOrigin( + new Vector3(pawn.AbsOrigin.X, pawn.AbsOrigin.Y, pawn.AbsOrigin.Z), + forward, + eyeOffsetZ, + Config.ForwardDistance); + light.Enabled = true; light.Color = Color.FromArgb(255, Config.ColorR, Config.ColorG, Config.ColorB); light.ColorTemperature = Config.ColorTemperature; @@ -262,7 +254,10 @@ public class FlashlightPlugin : BasePlugin, IPluginConfig light.SizeParams.Y = Config.SizeY; light.SizeParams.Z = Config.SizeZ; - ApplyTransform(light, player, pawn); + light.Teleport( + origin, + new Vector3(angles.X, angles.Y, angles.Z), + null); using (var keyValues = new CEntityKeyValues()) { @@ -270,69 +265,13 @@ public class FlashlightPlugin : BasePlugin, IPluginConfig light.DispatchSpawn(keyValues); } + light.AcceptInput("SetParent", pawn, light, "!activator"); + light.AcceptInput("SetParentAttachmentMaintainOffset", null, null, Config.AttachmentName); + state.Light = light; state.IsOn = true; } - /// - /// Keeps the light glued to the player's eye every tick. - /// - /// - /// The light is deliberately not parented to the pawn. Handing it to the engine via - /// SetParent / SetParentAttachmentMaintainOffset locks its orientation to a model - /// attachment, and those attachments only carry the body's yaw, so the beam could never follow - /// the player looking up or down. Driving the transform ourselves keeps pitch and yaw in sync. - /// - private void UpdateLight(CCSPlayerController player, PlayerFlashlightState state) - { - var light = state.Light; - - if (light is null || !light.IsValid) - { - // The engine can reap the entity underneath us (round restart, cleanup); rebuild it. - state.Light = null; - CreateLight(player, state); - return; - } - - var pawn = player.PlayerPawn.Value; - if (pawn is null || !pawn.IsValid || pawn.AbsOrigin is null || pawn.V_angle is null) - { - return; - } - - ApplyTransform(light, player, pawn); - } - - private void ApplyTransform(CBarnLight light, CCSPlayerController player, CCSPlayerPawn pawn) - { - var isCrouching = (player.Buttons & PlayerButtons.Duck) != 0; - var eyeOffsetZ = FlashlightLogic.GetEyeOffsetZ( - isCrouching, - Config.StandEyeOffsetZ, - Config.CrouchEyeOffsetZ); - - var origin = pawn.AbsOrigin!; - var angles = pawn.V_angle; - - var transform = FlashlightLogic.CalculateLightTransform( - new Vector3(origin.X, origin.Y, origin.Z), - angles.X, - angles.Y, - angles.Z, - eyeOffsetZ, - Config.ForwardDistance); - - // Handing the pawn's velocity over lets clients interpolate the light between ticks - // instead of visibly stepping it. - var velocity = pawn.AbsVelocity; - - light.Teleport( - transform.Origin, - transform.Angles, - velocity is null ? null : new Vector3(velocity.X, velocity.Y, velocity.Z)); - } - private void DestroyLight(int slot) { if (!_playerStates.TryGetValue(slot, out var state)) diff --git a/Flashlight/FlashlightConfig.cs b/Flashlight/FlashlightConfig.cs index 52525d2..3da2af8 100644 --- a/Flashlight/FlashlightConfig.cs +++ b/Flashlight/FlashlightConfig.cs @@ -71,6 +71,9 @@ public class FlashlightConfig : BasePluginConfig [JsonPropertyName("CrouchEyeOffsetZ")] public float CrouchEyeOffsetZ { get; set; } = 46f; + [JsonPropertyName("AttachmentName")] + public string AttachmentName { get; set; } = "axis_of_intent"; + [JsonPropertyName("LightCookie")] public string LightCookie { get; set; } = "materials/effects/lightcookies/flashlight.vtex"; @@ -91,6 +94,11 @@ public class FlashlightConfig : BasePluginConfig StandEyeOffsetZ = Math.Max(0f, StandEyeOffsetZ); CrouchEyeOffsetZ = Math.Max(0f, CrouchEyeOffsetZ); + if (string.IsNullOrWhiteSpace(AttachmentName)) + { + AttachmentName = "axis_of_intent"; + } + if (string.IsNullOrWhiteSpace(LightCookie)) { LightCookie = "materials/effects/lightcookies/flashlight.vtex"; diff --git a/Flashlight/FlashlightLogic.cs b/Flashlight/FlashlightLogic.cs index e0565a9..7827943 100644 --- a/Flashlight/FlashlightLogic.cs +++ b/Flashlight/FlashlightLogic.cs @@ -2,12 +2,6 @@ using System.Numerics; namespace Flashlight; -/// -/// Position and orientation to apply to the flashlight entity. -/// is a Source QAngle laid out as (pitch, yaw, roll). -/// -public readonly record struct LightTransform(Vector3 Origin, Vector3 Angles); - public static class FlashlightLogic { public static bool TryToggle(ref bool isOn, ref bool canToggle) @@ -56,39 +50,6 @@ public static class FlashlightLogic -MathF.Sin(pitchRad)); } - /// - /// Forward vector on the horizontal plane only, ignoring pitch. - /// - public static Vector3 HorizontalForwardFromYawDegrees(float yaw) - { - var yawRad = yaw * (MathF.PI / 180f); - - return new Vector3(MathF.Cos(yawRad), MathF.Sin(yawRad), 0f); - } - - /// - /// World transform for the flashlight given the player's current pawn origin and view angles. - /// - /// - /// The angles carry the full view rotation so the beam tracks pitch as well as yaw, while the - /// origin is only pushed forward on the horizontal plane. Offsetting the origin along the full - /// pitched forward vector would drop the light through the floor when looking straight down - /// (a crouched eye height of 46 minus a 54 unit offset ends up below the ground). - /// - public static LightTransform CalculateLightTransform( - Vector3 pawnOrigin, - float pitch, - float yaw, - float roll, - float eyeOffsetZ, - float forwardDistance) - { - var forward = HorizontalForwardFromYawDegrees(yaw); - var origin = CalculateLightOrigin(pawnOrigin, forward, eyeOffsetZ, forwardDistance); - - return new LightTransform(origin, new Vector3(pitch, yaw, roll)); - } - public static bool ShouldCreateLight(bool isOn, bool hasValidLight) { return isOn && !hasValidLight; diff --git a/README.md b/README.md index afb2955..93a1018 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ # Flashlight -Flashlight is a Counter-Strike 2 server plugin written in C# with [CounterStrikeSharp](https://docs.cssharp.dev). It gives human players a toggleable flashlight using a `light_barn` entity. +Flashlight is a Counter-Strike 2 server plugin written in C# with [CounterStrikeSharp](https://docs.cssharp.dev). It gives human players a toggleable flashlight using a parented `light_barn` entity. ## Features - Toggle with the Use key (`E` by default) or `css_fl_toggle` -- One `light_barn` per player, re-aimed each tick so the beam tracks both pitch and yaw -- Configurable brightness, range, color, shadows, and offsets +- 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 @@ -49,9 +49,10 @@ On first load, CounterStrikeSharp writes a JSON config for the plugin. Defaults: | `SoftX` / `SoftY` | `1.0` | Softness | | `Skirt` / `SkirtNear` | `0.5` / `1.0` | Skirt falloff | | `SizeX` / `SizeY` / `SizeZ` | `45` / `45` / `0.03` | Beam size params | -| `ForwardDistance` | `54` | Horizontal offset in front of the eye, so the beam clears the player model | +| `ForwardDistance` | `54` | Spawn offset along view forward | | `StandEyeOffsetZ` | `64` | Standing eye height offset | | `CrouchEyeOffsetZ` | `46` | Crouching eye height offset | +| `AttachmentName` | `axis_of_intent` | Parent attachment | | `LightCookie` | `materials/effects/lightcookies/flashlight.vtex` | Flashlight cookie texture | ## Development @@ -74,17 +75,10 @@ dotnet build dotnet test ``` -Unit tests cover toggle/cooldown logic, Use-key edge detection, transform math (origin and pitch/yaw angles), create/destroy policy, and config clamping. Entity behaviour itself requires a live CS2 server. +Unit tests cover toggle/cooldown logic, Use-key edge detection, origin math, create/destroy policy, and config clamping. Entity parenting requires a live CS2 server. ## Changelog -### v0.1.2 - -- Fixed the beam only following horizontal aim: the light was parented to the pawn's `axis_of_intent` attachment, which carries body yaw but not view pitch, so looking up or down never moved it. The light is now un-parented and re-aimed every tick from the pawn's live `V_angle`. -- Fixed the flashlight never updating when `AllowUseKey` was `false`, which previously short-circuited the whole tick loop. -- `ForwardDistance` now offsets the light horizontally only, so looking straight down no longer pushes it through the floor. -- Removed the obsolete `AttachmentName` config key (leaving it in an existing config file is harmless and ignored). - ### v0.1.1 - Updated to .NET 10 and CounterStrikeSharp.API 1.0.371