cs2-css-flashlight/Flashlight/Flashlight.cs
Vesper bd2ad516f4 feat: Update to latest CS2/CSS API and .NET 8.0
- Update TargetFramework from net7.0 to net8.0
- Update CounterStrikeSharp.API from 1.0.126 to 1.0.363
- Update MinimumApiVersion from 126 to 363
- Bump ModuleVersion to 0.0.6
- FIX: Replace deprecated EyeAngles with V_angle (fixes #5)
- FIX: Fix closure capture bug in timer callbacks (race condition)
- Add null checks for player pawn data
- Add xUnit test project with core logic tests
- Update solution file to include test project
- Update README with development instructions and changelog

Workflow update will be handled separately due to GitHub token scope requirements.

This update ensures compatibility with the latest Counter-Strike 2
and CounterStrikeSharp API versions.
2026-02-28 22:16:15 +00:00

226 lines
No EOL
7.6 KiB
C#

using System.Drawing;
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Attributes;
using CounterStrikeSharp.API.Core.Attributes.Registration;
using CounterStrikeSharp.API.Modules.Commands;
using Vector = CounterStrikeSharp.API.Modules.Utils.Vector;
namespace Flashlight;
[MinimumApiVersion(363)]
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.0.6";
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
// FIXED: EyeAngles -> V_angle for CSS API v1.0.363+ compatibility
public static Flashlight? Instance { get; private set; }
private readonly List<CCSPlayerController> _connectedPlayers = new();
private readonly Dictionary<CCSPlayerController, bool> _playerUsingFlashlight = new();
private readonly Dictionary<CCSPlayerController, bool> _playerIsCrouching = new();
private readonly Dictionary<CCSPlayerController, bool> _playerCanToggle = new();
private readonly Dictionary<CCSPlayerController, COmniLight> _playerFlashlight = new();
public override void Load(bool hotReload)
{
Instance = this;
LogHelper.LogToConsole(ConsoleColor.Green, $"{ModuleName} v{ModuleVersion} loading...");
RegisterListener<Listeners.OnTick>(() =>
{
foreach (var player in _connectedPlayers.Where(player => player is { IsValid: true, IsBot: false, PawnIsAlive: true }))
{
ToggleFlashlight(player);
if (_playerCanToggle[player] == false) continue;
if ((player.Buttons & PlayerButtons.Use) != 0)
{
_playerCanToggle[player] = false;
var currentPlayer = player; // Capture for closure
if (_playerUsingFlashlight[player] == false)
{
_playerUsingFlashlight[player] = true;
Instance?.AddTimer(0.25f, () =>
{
_playerCanToggle[currentPlayer] = true;
});
}
else
{
_playerUsingFlashlight[player] = false;
Instance?.AddTimer(0.25f, () =>
{
_playerCanToggle[currentPlayer] = true;
});
}
}
if ((player.Buttons & PlayerButtons.Duck) != 0)
{
_playerIsCrouching[player] = true;
}
else
{
_playerIsCrouching[player] = false;
}
}
});
LogHelper.LogToConsole($"{ModuleDisplayName} v{ModuleVersion} loaded!");
}
[GameEventHandler]
public HookResult OnPlayerConnectFull(EventPlayerConnectFull @event, GameEventInfo info)
{
var player = @event.Userid;
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.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.IsValid || player.IsBot) return HookResult.Continue;
if (_connectedPlayers.Contains(player) == false)
{
_connectedPlayers.Add(player);
_playerUsingFlashlight[player] = false;
_playerIsCrouching[player] = false;
_playerCanToggle[player] = true;
}
_playerUsingFlashlight[player] = false;
return HookResult.Continue;
}
[GameEventHandler]
public HookResult OnPlayerDeath(EventPlayerDeath @event, GameEventInfo info)
{
var player = @event.Userid;
if (!player.IsValid || player.IsBot) return HookResult.Continue;
_playerUsingFlashlight[player] = false;
return HookResult.Continue;
}
public void ToggleFlashlight(CCSPlayerController player)
{
if (_playerUsingFlashlight[player] == false)
{
if (_playerFlashlight.TryGetValue(player, out var value))
{
value.Remove();
_playerFlashlight.Remove(player);
}
return;
}
var entity = _playerFlashlight.TryGetValue(player, out var flashlight) ? flashlight : Utilities.CreateEntityByName<COmniLight>("light_omni2");
if (entity == null || !entity.IsValid)
{
LogHelper.LogToConsole("Failed to create entity!");
return;
}
entity.DirectLight = 3;
var pawn = player.PlayerPawn.Value;
if (pawn?.AbsOrigin == null || pawn.V_angle == null)
{
LogHelper.LogToConsole("Failed to get player pawn data!");
return;
}
entity.Teleport(
new Vector(
pawn.AbsOrigin.X,
pawn.AbsOrigin.Y,
pawn.AbsOrigin.Z + (_playerIsCrouching[player] ? 46.03f : 64.03f)
),
pawn.V_angle,
pawn.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 ToggleFlashlight(CCSPlayerController caller, CommandInfo? info)
{
if (!caller.IsValid || !caller.PawnIsAlive) return;
if (_playerCanToggle[caller] == false) return;
_playerUsingFlashlight[caller] = !_playerUsingFlashlight[caller];
_playerCanToggle[caller] = false;
var currentCaller = caller; // Capture for closure
Instance?.AddTimer(0.25f, () =>
{
_playerCanToggle[currentCaller] = true;
});
}
}