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..07f2ce2 100644
--- a/Flashlight/Flashlight.cs
+++ b/Flashlight/Flashlight.cs
@@ -1,149 +1,142 @@
๏ปฟ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
+[MinimumApiVersion(126)]
+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.5";
- 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);
-
- RegisterListener(OnTick);
-
- if (hotReload)
+ Instance = this;
+
+ LogHelper.LogToConsole(ConsoleColor.Green, $"{ModuleName} v{ModuleVersion} loading...");
+
+ 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.LogToConsole($"{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))
+
+ if (!player.IsValid || player.IsBot) return HookResult.Continue;
+
+ if (_connectedPlayers.Contains(player) == false)
{
- return HookResult.Continue;
+ _connectedPlayers.Add(player);
+ _playerUsingFlashlight[player] = false;
+ _playerIsCrouching[player] = false;
+ _playerCanToggle[player] = true;
}
-
- var state = EnsureState(player);
- state.IsOn = false;
- DestroyLight(player.Slot);
+
+ _playerUsingFlashlight[player] = false;
+
return HookResult.Continue;
}
@@ -151,218 +144,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;
+ if (_playerFlashlight.TryGetValue(player, out var value))
+ {
+ value.Remove();
+ _playerFlashlight.Remove(player);
+ }
+
+ return;
}
+
+ var entity = _playerFlashlight.TryGetValue(player, out var flashlight) ? flashlight : Utilities.CreateEntityByName("light_omni2");
- var state = EnsureState(player);
- state.IsOn = false;
- DestroyLight(player.Slot);
- return HookResult.Continue;
+ 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("css_fl_toggle", "Toggles the flashlight")]
[CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)]
- public void OnToggleCommand(CCSPlayerController? caller, CommandInfo info)
+ public void ToggleFlashlight(CCSPlayerController caller, CommandInfo? info)
{
- if (!Config.Enabled || caller is null || !IsEligiblePlayer(caller) || !caller.PawnIsAlive)
+ if (!caller.IsValid || !caller.PawnIsAlive) return;
+
+ if (_playerCanToggle[caller] == false) return;
+
+ _playerUsingFlashlight[caller] = !_playerUsingFlashlight[caller];
+ _playerCanToggle[caller] = false;
+
+ Instance?.AddTimer(0.25f, () =>
{
- 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))
- {
- current.CanToggle = true;
- }
+ _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..136ca57 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..bcd35e6 100644
--- a/README.md
+++ b/README.md
@@ -1,114 +1,38 @@
-# 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`. Or use the `/fl_toggle` command in chat which could then be bind to a different key.
-## 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 to make flashlight enabled or disabled (to be able to allow or disallow flashlight on some maps)
+- 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