diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0b1625d..a4d7689 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,64 +2,48 @@ name: Build on: push: - branches: - - main tags: - "*" - pull_request: - branches: - - main env: + BUILD_NUMBER: ${{ github.tag }} PROJECT_NAME: "Flashlight" PROJECT_PATH: "./Flashlight/Flashlight.csproj" OUTPUT_PATH: "./Flashlight/Flashlight" PUBLISH_PATH: "./Flashlight/Flashlight" - DOTNET_VERSION: "10.0.x" + jobs: - test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: ${{ env.DOTNET_VERSION }} - - name: Restore - run: dotnet restore - - name: Test - run: dotnet test -c Release --no-restore - build: + permissions: write-all runs-on: ubuntu-latest - needs: test + steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v3 - name: Setup .NET - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v3 with: - dotnet-version: ${{ env.DOTNET_VERSION }} + dotnet-version: 7.0.x - name: Restore run: dotnet restore - name: Build - run: dotnet build ${{ env.PROJECT_PATH }} -c Release -o ${{ env.OUTPUT_PATH }} + run: dotnet build ${{ env.PROJECT_PATH }} -c Flashlight -o ${{ env.OUTPUT_PATH }} publish: - if: startsWith(github.ref, 'refs/tags/') + if: github.event_name == 'push' permissions: write-all runs-on: ubuntu-latest needs: build steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v3 - name: Setup .NET - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v3 with: - dotnet-version: ${{ env.DOTNET_VERSION }} + dotnet-version: 7.0.x - name: Restore run: dotnet restore - name: Build - run: dotnet build ${{ env.PROJECT_PATH }} -c Release -o ${{ env.OUTPUT_PATH }} + run: dotnet build ${{ env.PROJECT_PATH }} -c Flashlight -o ${{ env.OUTPUT_PATH }} - name: Clean files run: | rm -f \ @@ -71,12 +55,11 @@ jobs: type: 'zip' filename: '${{ env.PROJECT_NAME }}.zip' path: ${{ env.PUBLISH_PATH }} - - name: Create Release - uses: ncipollo/release-action@v1.14.0 + - name: GunGameLite + uses: ncipollo/release-action@v1.12.0 with: artifacts: "${{ env.PROJECT_NAME }}.zip" - name: "${{ github.ref_name }}" - tag: "${{ github.ref_name }}" - allowUpdates: true + name: "${{ env.BUILD_NUMBER }}" + tag: "${{ env.BUILD_NUMBER }}" body: | - Place the plugin in game/csgo/addons/counterstrikesharp/plugins/Flashlight + Place the plugin in game/csgo/addons/counterstrikesharp/plugins/Flashlight \ No newline at end of file diff --git a/.gitignore b/.gitignore index 74439e9..992febe 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,3 @@ .idea -**/bin/ -**/obj/ -.DS_Store -docs/ +Flashlight/bin +Flashlight/obj \ No newline at end of file diff --git a/Flashlight.Tests/Flashlight.Tests.csproj b/Flashlight.Tests/Flashlight.Tests.csproj deleted file mode 100644 index fd0ff01..0000000 --- a/Flashlight.Tests/Flashlight.Tests.csproj +++ /dev/null @@ -1,28 +0,0 @@ - - - - net10.0 - enable - enable - false - true - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - diff --git a/Flashlight.Tests/FlashlightLogicTests.cs b/Flashlight.Tests/FlashlightLogicTests.cs deleted file mode 100644 index c42276f..0000000 --- a/Flashlight.Tests/FlashlightLogicTests.cs +++ /dev/null @@ -1,223 +0,0 @@ -using System.Numerics; -using Xunit; - -namespace Flashlight.Tests; - -public class FlashlightLogicTests -{ - [Fact] - public void TryToggle_TogglesWhenAllowed() - { - var isOn = false; - var canToggle = true; - - Assert.True(FlashlightLogic.TryToggle(ref isOn, ref canToggle)); - Assert.True(isOn); - Assert.False(canToggle); - } - - [Fact] - public void TryToggle_BlockedDuringCooldown() - { - 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 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() - { - 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 - { - 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, - 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("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.sln b/Flashlight.sln index 3dbb8ef..8271bde 100644 --- a/Flashlight.sln +++ b/Flashlight.sln @@ -1,9 +1,7 @@ - +๏ปฟ Microsoft Visual Studio Solution File, Format Version 12.00 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Flashlight", "Flashlight\Flashlight.csproj", "{D479E900-33D8-4D58-945B-FB2F79DB5742}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Flashlight.Tests", "Flashlight.Tests\Flashlight.Tests.csproj", "{A1B2C3D4-1234-5678-90AB-CDEF12345678}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -14,9 +12,5 @@ Global {D479E900-33D8-4D58-945B-FB2F79DB5742}.Debug|Any CPU.Build.0 = Debug|Any CPU {D479E900-33D8-4D58-945B-FB2F79DB5742}.Release|Any CPU.ActiveCfg = Release|Any CPU {D479E900-33D8-4D58-945B-FB2F79DB5742}.Release|Any CPU.Build.0 = Release|Any CPU - {A1B2C3D4-1234-5678-90AB-CDEF12345678}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A1B2C3D4-1234-5678-90AB-CDEF12345678}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A1B2C3D4-1234-5678-90AB-CDEF12345678}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A1B2C3D4-1234-5678-90AB-CDEF12345678}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection -EndGlobal \ No newline at end of file +EndGlobal diff --git a/Flashlight/Flashlight.cs b/Flashlight/Flashlight.cs index f3f6267..60b954b 100644 --- a/Flashlight/Flashlight.cs +++ b/Flashlight/Flashlight.cs @@ -1,149 +1,132 @@ ๏ปฟusing System.Drawing; -using System.Numerics; using CounterStrikeSharp.API; using CounterStrikeSharp.API.Core; -using CounterStrikeSharp.API.Core.Attributes; using CounterStrikeSharp.API.Core.Attributes.Registration; using CounterStrikeSharp.API.Modules.Commands; -using CounterStrikeSharp.API.Modules.Utils; -using Microsoft.Extensions.Logging; +using Vector = CounterStrikeSharp.API.Modules.Utils.Vector; namespace Flashlight; -[MinimumApiVersion(371)] -public class FlashlightPlugin : BasePlugin, IPluginConfig +public class Flashlight : BasePlugin { 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.0.3"; - public FlashlightConfig Config { get; set; } = new(); + private static string ModuleDisplayName => "Flashlight"; + + // TODO: Change crouch-tracking to a more elegant solution + // TODO: Add config and make light entity values configurable + // TODO: Maybe replace light_omni2 with light_rect or something else - private readonly Dictionary _playerStates = new(); - - public void OnConfigParsed(FlashlightConfig config) - { - config.Clamp(); - Config = config; - } + public static Flashlight? Instance { get; private set; } + + private readonly List _connectedPlayers = new(); + private readonly Dictionary _playerUsingFlashlight = new(); + private readonly Dictionary _playerIsCrouching = new(); + private readonly Dictionary _playerCanToggle = new(); + private readonly Dictionary _playerFlashlight = new(); public override void Load(bool hotReload) { - Logger.LogInformation("{Name} v{Version} loading...", ModuleName, ModuleVersion); + LogHelper.LogToConsole(ConsoleColor.Green, $"{ModuleName} version {ModuleVersion} loaded"); - RegisterListener(OnTick); - - if (hotReload) + Instance = this; + + RegisterListener(() => { - foreach (var player in Utilities.GetPlayers().Where(IsEligiblePlayer)) + foreach (var player in _connectedPlayers.Where(player => player is { IsValid: true, IsBot: false, PawnIsAlive: true })) { - EnsureState(player); - } - } - - Logger.LogInformation("{Name} v{Version} loaded!", ModuleName, ModuleVersion); - } - - public override void Unload(bool hotReload) - { - foreach (var slot in _playerStates.Keys.ToList()) - { - DestroyLight(slot); - } - - _playerStates.Clear(); - } - - private void OnTick() - { - if (!Config.Enabled) - { - return; - } - - foreach (var player in Utilities.GetPlayers()) - { - if (!IsEligiblePlayer(player)) - { - 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) + ToggleFlashlight(player); + + if (_playerCanToggle[player] == false) continue; + + if ((player.Buttons & PlayerButtons.Use) != 0) { - deadState.IsOn = false; - DestroyLight(player.Slot); + _playerCanToggle[player] = false; + + if (_playerUsingFlashlight[player] == false) + { + _playerUsingFlashlight[player] = true; + + Instance?.AddTimer(0.25f, () => + { + _playerCanToggle[player] = true; + }); + } + else + { + _playerUsingFlashlight[player] = false; + + Instance?.AddTimer(0.25f, () => + { + _playerCanToggle[player] = true; + }); + } } - - continue; - } - - var state = EnsureState(player); - - if (Config.AllowUseKey) - { - var usePressed = (player.Buttons & PlayerButtons.Use) != 0; - - if (FlashlightLogic.IsUsePressedEdge(usePressed, state.WasUsePressed)) + + if ((player.Buttons & PlayerButtons.Duck) != 0) { - TryToggleFlashlight(player, state); + _playerIsCrouching[player] = true; + } + else + { + _playerIsCrouching[player] = false; } - - state.WasUsePressed = usePressed; } + }); - if (state.IsOn) - { - UpdateLight(player, state); - } - } + LogHelper.LogToChatAll($"{ModuleDisplayName} v{ModuleVersion} loaded!"); } - + [GameEventHandler] public HookResult OnPlayerConnectFull(EventPlayerConnectFull @event, GameEventInfo info) { var player = @event.Userid; - if (player is null || !IsEligiblePlayer(player)) - { - return HookResult.Continue; - } - - EnsureState(player); - Logger.LogInformation("{Player} connected", player.PlayerName); + + if (!player.IsValid || player.IsBot) return HookResult.Continue; + + _connectedPlayers.Add(player); + _playerUsingFlashlight[player] = false; + _playerIsCrouching[player] = false; + _playerCanToggle[player] = true; + + LogHelper.LogToConsole(ConsoleColor.Green, $"{player.PlayerName} connected"); + return HookResult.Continue; } - + [GameEventHandler] public HookResult OnPlayerDisconnect(EventPlayerDisconnect @event, GameEventInfo info) { var player = @event.Userid; - if (player is null || !player.IsValid) - { - return HookResult.Continue; - } - - var slot = player.Slot; - DestroyLight(slot); - _playerStates.Remove(slot); + + if (!player.IsValid || player.IsBot) return HookResult.Continue; + + _connectedPlayers.Remove(player); + _playerUsingFlashlight.Remove(player); + _playerIsCrouching.Remove(player); + _playerCanToggle.Remove(player); + + _playerFlashlight.TryGetValue(player, out var flashlight); + flashlight?.Remove(); + _playerFlashlight.Remove(player); + + LogHelper.LogToConsole(ConsoleColor.Green, $"{player.PlayerName} disconnected"); + return HookResult.Continue; } - + [GameEventHandler] public HookResult OnPlayerSpawn(EventPlayerSpawn @event, GameEventInfo info) { var player = @event.Userid; - if (player is null || !IsEligiblePlayer(player)) - { - return HookResult.Continue; - } - - var state = EnsureState(player); - state.IsOn = false; - DestroyLight(player.Slot); + + if (!player.IsValid || player.IsBot) return HookResult.Continue; + + _playerUsingFlashlight[player] = false; + return HookResult.Continue; } @@ -151,218 +134,72 @@ public class FlashlightPlugin : BasePlugin, IPluginConfig public HookResult OnPlayerDeath(EventPlayerDeath @event, GameEventInfo info) { var player = @event.Userid; - if (player is null || !IsEligiblePlayer(player)) - { - return HookResult.Continue; - } - - var state = EnsureState(player); - state.IsOn = false; - DestroyLight(player.Slot); + + if (!player.IsValid || player.IsBot) return HookResult.Continue; + + _playerUsingFlashlight[player] = false; + return HookResult.Continue; } - [GameEventHandler] - public HookResult OnPlayerTeam(EventPlayerTeam @event, GameEventInfo info) + public void ToggleFlashlight(CCSPlayerController player) { - var player = @event.Userid; - if (player is null || !IsEligiblePlayer(player)) + if (_playerUsingFlashlight[player] == false) { - return HookResult.Continue; - } - - var state = EnsureState(player); - state.IsOn = false; - DestroyLight(player.Slot); - return HookResult.Continue; - } - - [ConsoleCommand("css_fl_toggle", "Toggles the flashlight")] - [CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)] - public void OnToggleCommand(CCSPlayerController? caller, CommandInfo info) - { - if (!Config.Enabled || caller is null || !IsEligiblePlayer(caller) || !caller.PawnIsAlive) - { - return; - } - - TryToggleFlashlight(caller, EnsureState(caller)); - } - - 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; - - if (!FlashlightLogic.TryToggle(ref isOn, ref canToggle)) - { - return; - } - - state.IsOn = isOn; - state.CanToggle = canToggle; - - if (state.IsOn) - { - CreateLight(player, state); - } - else - { - DestroyLight(player.Slot); - } - - var slot = player.Slot; - AddTimer(Config.ToggleCooldownSeconds, () => - { - if (_playerStates.TryGetValue(slot, out var current)) + if (_playerFlashlight.TryGetValue(player, out var value)) { - current.CanToggle = true; + value.Remove(); + _playerFlashlight.Remove(player); } + + return; + } + + var entity = _playerFlashlight.TryGetValue(player, out var flashlight) ? flashlight : Utilities.CreateEntityByName("light_omni2"); + + if (entity == null || !entity.IsValid) + { + LogHelper.LogToConsole("Failed to create entity!"); + return; + } + + entity.DirectLight = 3; + + entity.Teleport( + new Vector( + player.PlayerPawn.Value!.AbsOrigin!.X, + player.PlayerPawn.Value!.AbsOrigin!.Y, + player.PlayerPawn.Value!.AbsOrigin!.Z + (_playerIsCrouching[player] ? 46.03f : 64.03f) + ), + player.PlayerPawn.Value!.EyeAngles, + player.PlayerPawn.Value!.AbsVelocity + ); + + entity.OuterAngle = 45f; + entity.Enabled = true; + entity.Color = Color.White; + entity.ColorTemperature = 6500; + entity.Brightness = 1f; + entity.Range = 5000f; + + entity.DispatchSpawn(); + _playerFlashlight[player] = entity; + } + + [ConsoleCommand("fl_toggle", "Toggles the flashlight")] + [CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)] + public void ToggleFlashlight(CCSPlayerController caller, CommandInfo? info) + { + if (!caller.IsValid || !caller.PawnIsAlive) return; + + if (_playerCanToggle[caller] == false) return; + + _playerUsingFlashlight[caller] = !_playerUsingFlashlight[caller]; + _playerCanToggle[caller] = false; + + Instance?.AddTimer(0.25f, () => + { + _playerCanToggle[caller] = true; }); } - - private void CreateLight(CCSPlayerController player, PlayerFlashlightState state) - { - DestroyLight(player.Slot); - - var pawn = player.PlayerPawn.Value; - if (pawn is null || !pawn.IsValid || pawn.AbsOrigin is null || pawn.V_angle is null) - { - Logger.LogWarning("Failed to get pawn data for {Player}", player.PlayerName); - state.IsOn = false; - return; - } - - var light = Utilities.CreateEntityByName("light_barn"); - if (light is null || !light.IsValid) - { - Logger.LogWarning("Failed to create light_barn for {Player}", player.PlayerName); - state.IsOn = false; - return; - } - - light.Enabled = true; - light.Color = Color.FromArgb(255, Config.ColorR, Config.ColorG, Config.ColorB); - light.ColorTemperature = Config.ColorTemperature; - light.Brightness = Config.Brightness; - light.Range = Config.Range; - light.SoftX = Config.SoftX; - light.SoftY = Config.SoftY; - light.Skirt = Config.Skirt; - light.SkirtNear = Config.SkirtNear; - light.CastShadows = Config.CastShadows ? 1 : 0; - light.DirectLight = 3; - light.SizeParams.X = Config.SizeX; - light.SizeParams.Y = Config.SizeY; - light.SizeParams.Z = Config.SizeZ; - - ApplyTransform(light, player, pawn); - - using (var keyValues = new CEntityKeyValues()) - { - keyValues.SetString("lightcookie", Config.LightCookie); - light.DispatchSpawn(keyValues); - } - - 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)) - { - return; - } - - var light = state.Light; - state.Light = null; - - if (light is not null && light.IsValid) - { - light.Remove(); - } - } - - private PlayerFlashlightState EnsureState(CCSPlayerController player) - { - if (_playerStates.TryGetValue(player.Slot, out var state)) - { - return state; - } - - state = new PlayerFlashlightState(); - _playerStates[player.Slot] = state; - return state; - } - - private static bool IsEligiblePlayer(CCSPlayerController? player) - { - return player is { IsValid: true, IsBot: false }; - } -} +} \ No newline at end of file diff --git a/Flashlight/Flashlight.csproj b/Flashlight/Flashlight.csproj index 2be7208..fb1787e 100644 --- a/Flashlight/Flashlight.csproj +++ b/Flashlight/Flashlight.csproj @@ -1,13 +1,13 @@ - net10.0 + net7.0 enable enable - + diff --git a/Flashlight/FlashlightConfig.cs b/Flashlight/FlashlightConfig.cs deleted file mode 100644 index 52525d2..0000000 --- a/Flashlight/FlashlightConfig.cs +++ /dev/null @@ -1,111 +0,0 @@ -using System.Text.Json.Serialization; -using CounterStrikeSharp.API.Core; - -namespace Flashlight; - -public class FlashlightConfig : BasePluginConfig -{ - [JsonPropertyName("Enabled")] - public bool Enabled { get; set; } = true; - - [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; - - [JsonPropertyName("Brightness")] - public float Brightness { get; set; } = 1.0f; - - [JsonPropertyName("Range")] - public float Range { get; set; } = 2048f; - - [JsonPropertyName("ColorR")] - public byte ColorR { get; set; } = 255; - - [JsonPropertyName("ColorG")] - public byte ColorG { get; set; } = 255; - - [JsonPropertyName("ColorB")] - public byte ColorB { get; set; } = 255; - - [JsonPropertyName("ColorTemperature")] - public float ColorTemperature { get; set; } = 6500f; - - [JsonPropertyName("CastShadows")] - public bool CastShadows { get; set; } = true; - - [JsonPropertyName("SoftX")] - public float SoftX { get; set; } = 1.0f; - - [JsonPropertyName("SoftY")] - public float SoftY { get; set; } = 1.0f; - - [JsonPropertyName("Skirt")] - public float Skirt { get; set; } = 0.5f; - - [JsonPropertyName("SkirtNear")] - public float SkirtNear { get; set; } = 1.0f; - - [JsonPropertyName("SizeX")] - public float SizeX { get; set; } = 45f; - - [JsonPropertyName("SizeY")] - public float SizeY { get; set; } = 45f; - - [JsonPropertyName("SizeZ")] - public float SizeZ { get; set; } = 0.03f; - - [JsonPropertyName("ForwardDistance")] - public float ForwardDistance { get; set; } = 54f; - - [JsonPropertyName("StandEyeOffsetZ")] - public float StandEyeOffsetZ { get; set; } = 64f; - - [JsonPropertyName("CrouchEyeOffsetZ")] - public float CrouchEyeOffsetZ { get; set; } = 46f; - - [JsonPropertyName("LightCookie")] - public string LightCookie { get; set; } = "materials/effects/lightcookies/flashlight.vtex"; - - public void Clamp() - { - ToggleCooldownSeconds = Math.Max(0f, ToggleCooldownSeconds); - Brightness = Math.Max(0f, Brightness); - Range = Math.Max(1f, Range); - ColorTemperature = Math.Clamp(ColorTemperature, 1000f, 12000f); - SoftX = Math.Max(0f, SoftX); - SoftY = Math.Max(0f, SoftY); - Skirt = Math.Max(0f, Skirt); - SkirtNear = Math.Max(0f, SkirtNear); - SizeX = Math.Max(0f, SizeX); - SizeY = Math.Max(0f, SizeY); - SizeZ = Math.Max(0f, SizeZ); - ForwardDistance = Math.Max(0f, ForwardDistance); - StandEyeOffsetZ = Math.Max(0f, StandEyeOffsetZ); - CrouchEyeOffsetZ = Math.Max(0f, CrouchEyeOffsetZ); - - if (string.IsNullOrWhiteSpace(LightCookie)) - { - 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 deleted file mode 100644 index e0565a9..0000000 --- a/Flashlight/FlashlightLogic.cs +++ /dev/null @@ -1,115 +0,0 @@ -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) - { - if (!canToggle) - { - return false; - } - - isOn = !isOn; - canToggle = false; - return true; - } - - public static bool IsUsePressedEdge(bool usePressed, bool wasUsePressed) - { - return usePressed && !wasUsePressed; - } - - public static float GetEyeOffsetZ(bool isCrouching, float standOffset, float crouchOffset) - { - return isCrouching ? crouchOffset : standOffset; - } - - public static Vector3 CalculateLightOrigin( - Vector3 pawnOrigin, - Vector3 forward, - float eyeOffsetZ, - float forwardDistance) - { - return new Vector3( - pawnOrigin.X + forward.X * forwardDistance, - pawnOrigin.Y + forward.Y * forwardDistance, - pawnOrigin.Z + eyeOffsetZ + forward.Z * forwardDistance); - } - - public static Vector3 ForwardFromAnglesDegrees(float pitch, float yaw) - { - var pitchRad = pitch * (MathF.PI / 180f); - var yawRad = yaw * (MathF.PI / 180f); - var cosPitch = MathF.Cos(pitchRad); - - return new Vector3( - cosPitch * MathF.Cos(yawRad), - cosPitch * MathF.Sin(yawRad), - -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; - } - - public static bool ShouldDestroyLight(bool isOn, bool hasValidLight) - { - 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/Flashlight/LogHelper.cs b/Flashlight/LogHelper.cs new file mode 100644 index 0000000..4ccdd0b --- /dev/null +++ b/Flashlight/LogHelper.cs @@ -0,0 +1,56 @@ +using CounterStrikeSharp.API; +using CounterStrikeSharp.API.Core; +using CounterStrikeSharp.API.Modules.Utils; + +namespace Flashlight; + +public static class LogHelper +{ + public static void LogToConsole(string messageToLog) + { + Console.WriteLine($"[{Flashlight.Instance?.ModuleName}] -> {messageToLog}"); + } + + public static void LogToConsole(ConsoleColor color, string messageToLog) + { + Console.ForegroundColor = color; + Console.WriteLine($"[{Flashlight.Instance?.ModuleName}] -> {messageToLog}"); + Console.ResetColor(); + } + + public static void LogToChat(CCSPlayerController? player, string messageToLog) + { + player?.PrintToChat($"[{ChatColors.Purple}{Flashlight.Instance?.ModuleName}{ChatColors.Default}]{ReplaceTags(messageToLog + "{DEFAULT}")}"); + } + + public static void LogToChatAll(string messageToLog) + { + Server.PrintToChatAll($"[{ChatColors.Purple}{Flashlight.Instance?.ModuleName}{ChatColors.Default}]{ReplaceTags(messageToLog + "{DEFAULT}")}"); + } + + public static string ReplaceTags(this string text) + { + if (text.StartsWith("{")) text = $" {text}"; + text = text.Replace("{DEFAULT}", $"{ChatColors.Default}"); + text = text.Replace("{WHITE}", $"{ChatColors.White}"); + text = text.Replace("{DARKRED}", $"{ChatColors.Darkred}"); + text = text.Replace("{GREEN}", $"{ChatColors.Green}"); + text = text.Replace("{LIGHTYELLOW}", $"{ChatColors.LightYellow}"); + text = text.Replace("{LIGHTBLUE}", $"{ChatColors.LightBlue}"); + text = text.Replace("{OLIVE}", $"{ChatColors.Olive}"); + text = text.Replace("{LIME}", $"{ChatColors.Lime}"); + text = text.Replace("{RED}", $"{ChatColors.Red}"); + text = text.Replace("{PURPLE}", $"{ChatColors.Purple}"); + text = text.Replace("{GREY}", $"{ChatColors.Grey}"); + text = text.Replace("{YELLOW}", $"{ChatColors.Yellow}"); + text = text.Replace("{GOLD}", $"{ChatColors.Gold}"); + text = text.Replace("{SILVER}", $"{ChatColors.Silver}"); + text = text.Replace("{BLUE}", $"{ChatColors.Blue}"); + text = text.Replace("{DARKBLUE}", $"{ChatColors.DarkBlue}"); + text = text.Replace("{BLUEGREY}", $"{ChatColors.BlueGrey}"); + text = text.Replace("{MAGENTA}", $"{ChatColors.Magenta}"); + text = text.Replace("{LIGHTRED}", $"{ChatColors.LightRed}"); + + return text; + } +} \ No newline at end of file diff --git a/Flashlight/PlayerFlashlightState.cs b/Flashlight/PlayerFlashlightState.cs deleted file mode 100644 index ff2a9e6..0000000 --- a/Flashlight/PlayerFlashlightState.cs +++ /dev/null @@ -1,18 +0,0 @@ -using CounterStrikeSharp.API.Core; - -namespace Flashlight; - -public sealed class PlayerFlashlightState -{ - public bool IsOn { get; set; } - public bool CanToggle { get; set; } = true; - public bool WasUsePressed { get; set; } - public CBarnLight? Light { get; set; } - - public void Reset() - { - IsOn = false; - CanToggle = true; - WasUsePressed = false; - } -} diff --git a/README.md b/README.md index afb2955..13b0fc6 100644 --- a/README.md +++ b/README.md @@ -1,114 +1,37 @@ -# Flashlight +# ๐Ÿ”ฆ 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 plugin for Counter-Strike 2 that adds a flashlight feature for players. It is written in C# and uses the CounterStrikeSharp API. ๐ŸŽฎ -## Features +## โญ 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 -- Optional team restriction (`Any`, `CT`, or `T`) -- Automatically turns off on death, spawn, and team change -- Bots ignored +- ๐Ÿ’ก Players can toggle the flashlight on and off using `Use` key (the default key for this is `E`) or `/fl_toggle` in chat which could be bind to a different key. +- ๐Ÿ’€ The flashlight is automatically turned off when the player dies or respawns. +- ๐Ÿšซ The flashlight is only available to human players, not bots. -## Requirements +## ๐Ÿ”ง Installation -- CounterStrikeSharp **1.0.371+** (API minimum version enforced) -- .NET **10** runtime as provided by your CounterStrikeSharp install +1. โฌ‡๏ธ Download the latest release from the GitHub repository. +2. ๐Ÿ“ Extract the ZIP file. +3. ๐Ÿ“‚ Place the plugin in the `game/csgo/addons/counterstrikesharp/plugins/Flashlight` directory. -## Installation +## ๐Ÿ’ป Usage -1. Download the latest release ZIP. -2. Extract it. -3. Place the plugin folder in `game/csgo/addons/counterstrikesharp/plugins/Flashlight`. +โŒจ๏ธ Use the `Use` key to toggle the flashlight on and off. The default key for this is `E`. -## Usage +## ๐Ÿค Contributing -- Press Use (`E`) to toggle, if enabled in config. -- Or bind a key: +Contributions are welcome. Please open an issue or submit a pull request on GitHub. ๐Ÿ™ -``` -bind f "css_fl_toggle" -``` +## ๐Ÿ“ Development Tasks -## Configuration +- Change crouch-tracking to a more elegant solution +- Add config and make light entity values configurable +- Maybe replace light_omni2 with light_rect or something else -On first load, CounterStrikeSharp writes a JSON config for the plugin. Defaults: +## ๐Ÿ“ƒ License -| Key | Default | Description | -| --- | --- | --- | -| `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 | -| `ColorR` / `ColorG` / `ColorB` | `255` | Light color | -| `ColorTemperature` | `6500` | Kelvin temperature | -| `CastShadows` | `true` | Cast dynamic shadows | -| `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 | -| `StandEyeOffsetZ` | `64` | Standing eye height offset | -| `CrouchEyeOffsetZ` | `46` | Crouching eye height offset | -| `LightCookie` | `materials/effects/lightcookies/flashlight.vtex` | Flashlight cookie texture | +This project is licensed under the GNU General Public License. โš–๏ธ -## Development +## โœ๏ธ Author -### Prerequisites - -- .NET 10 SDK -- CounterStrikeSharp.API 1.0.371+ - -### Build - -```bash -dotnet restore -dotnet build -``` - -### Test - -```bash -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. - -## 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 -- 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` - -### v0.0.6 - -- Updated to .NET 8.0 and CounterStrikeSharp.API v1.0.363 -- Added initial xUnit test project -- Improved CSS API compatibility (`V_angle`, entity validity checks) - -### v0.0.5 - -- Initial release - -## License - -GNU General Public License. See `LICENSE`. - -## Author - -[creazy.eth](https://github.com/creazy231) +This project was created by [creazy.eth](https://github.com/creazy231) \ No newline at end of file