mirror of
https://github.com/creazy231/cs2-css-flashlight.git
synced 2026-09-27 20:17:04 +02:00
🔦 Modernize flashlight for CSS/.NET 10
Bump to CounterStrikeSharp 1.0.371 and .NET 10, replace per-tick light spawns with a parented light_barn, add plugin config, real unit tests, and refresh CI plus docs. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
081278a8ae
commit
a0ef6311ee
13 changed files with 847 additions and 434 deletions
50
.github/workflows/build.yml
vendored
50
.github/workflows/build.yml
vendored
|
|
@ -2,48 +2,64 @@ name: Build
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
tags:
|
tags:
|
||||||
- "*"
|
- "*"
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
env:
|
env:
|
||||||
BUILD_NUMBER: ${{ github.tag }}
|
|
||||||
PROJECT_NAME: "Flashlight"
|
PROJECT_NAME: "Flashlight"
|
||||||
PROJECT_PATH: "./Flashlight/Flashlight.csproj"
|
PROJECT_PATH: "./Flashlight/Flashlight.csproj"
|
||||||
OUTPUT_PATH: "./Flashlight/Flashlight"
|
OUTPUT_PATH: "./Flashlight/Flashlight"
|
||||||
PUBLISH_PATH: "./Flashlight/Flashlight"
|
PUBLISH_PATH: "./Flashlight/Flashlight"
|
||||||
|
DOTNET_VERSION: "10.0.x"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
test:
|
||||||
permissions: write-all
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- name: Setup .NET
|
- name: Setup .NET
|
||||||
uses: actions/setup-dotnet@v3
|
uses: actions/setup-dotnet@v4
|
||||||
with:
|
with:
|
||||||
dotnet-version: 7.0.x
|
dotnet-version: ${{ env.DOTNET_VERSION }}
|
||||||
|
- name: Restore
|
||||||
|
run: dotnet restore
|
||||||
|
- name: Test
|
||||||
|
run: dotnet test -c Release --no-restore
|
||||||
|
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: test
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Setup .NET
|
||||||
|
uses: actions/setup-dotnet@v4
|
||||||
|
with:
|
||||||
|
dotnet-version: ${{ env.DOTNET_VERSION }}
|
||||||
- name: Restore
|
- name: Restore
|
||||||
run: dotnet restore
|
run: dotnet restore
|
||||||
- name: Build
|
- name: Build
|
||||||
run: dotnet build ${{ env.PROJECT_PATH }} -c Flashlight -o ${{ env.OUTPUT_PATH }}
|
run: dotnet build ${{ env.PROJECT_PATH }} -c Release -o ${{ env.OUTPUT_PATH }}
|
||||||
|
|
||||||
publish:
|
publish:
|
||||||
if: github.event_name == 'push'
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
permissions: write-all
|
permissions: write-all
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: build
|
needs: build
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- name: Setup .NET
|
- name: Setup .NET
|
||||||
uses: actions/setup-dotnet@v3
|
uses: actions/setup-dotnet@v4
|
||||||
with:
|
with:
|
||||||
dotnet-version: 7.0.x
|
dotnet-version: ${{ env.DOTNET_VERSION }}
|
||||||
- name: Restore
|
- name: Restore
|
||||||
run: dotnet restore
|
run: dotnet restore
|
||||||
- name: Build
|
- name: Build
|
||||||
run: dotnet build ${{ env.PROJECT_PATH }} -c Flashlight -o ${{ env.OUTPUT_PATH }}
|
run: dotnet build ${{ env.PROJECT_PATH }} -c Release -o ${{ env.OUTPUT_PATH }}
|
||||||
- name: Clean files
|
- name: Clean files
|
||||||
run: |
|
run: |
|
||||||
rm -f \
|
rm -f \
|
||||||
|
|
@ -56,11 +72,11 @@ jobs:
|
||||||
filename: '${{ env.PROJECT_NAME }}.zip'
|
filename: '${{ env.PROJECT_NAME }}.zip'
|
||||||
path: ${{ env.PUBLISH_PATH }}
|
path: ${{ env.PUBLISH_PATH }}
|
||||||
- name: Create Release
|
- name: Create Release
|
||||||
uses: ncipollo/release-action@v1.12.0
|
uses: ncipollo/release-action@v1.14.0
|
||||||
with:
|
with:
|
||||||
artifacts: "${{ env.PROJECT_NAME }}.zip"
|
artifacts: "${{ env.PROJECT_NAME }}.zip"
|
||||||
name: "${{ env.BUILD_NUMBER }}"
|
name: "${{ github.ref_name }}"
|
||||||
tag: "${{ env.BUILD_NUMBER }}"
|
tag: "${{ github.ref_name }}"
|
||||||
allowUpdates: true
|
allowUpdates: true
|
||||||
body: |
|
body: |
|
||||||
Place the plugin in game/csgo/addons/counterstrikesharp/plugins/Flashlight
|
Place the plugin in game/csgo/addons/counterstrikesharp/plugins/Flashlight
|
||||||
|
|
|
||||||
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -1,3 +1,4 @@
|
||||||
.idea
|
.idea
|
||||||
Flashlight/bin
|
**/bin/
|
||||||
Flashlight/obj
|
**/obj/
|
||||||
|
.DS_Store
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<IsPackable>false</IsPackable>
|
<IsPackable>false</IsPackable>
|
||||||
|
|
@ -9,21 +9,20 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||||
<PackageReference Include="xunit" Version="2.6.2" />
|
<PackageReference Include="xunit" Version="2.9.3" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.4">
|
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.1">
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="coverlet.collector" Version="6.0.0">
|
<PackageReference Include="coverlet.collector" Version="6.0.4">
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Moq" Version="4.20.69" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="../Flashlight/Flashlight.csproj" />
|
<ProjectReference Include="../Flashlight/Flashlight.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
using System.Numerics;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Flashlight.Tests;
|
namespace Flashlight.Tests;
|
||||||
|
|
@ -5,137 +6,119 @@ namespace Flashlight.Tests;
|
||||||
public class FlashlightLogicTests
|
public class FlashlightLogicTests
|
||||||
{
|
{
|
||||||
[Fact]
|
[Fact]
|
||||||
public void FlashlightState_TogglesCorrectly()
|
public void TryToggle_TogglesWhenAllowed()
|
||||||
{
|
{
|
||||||
// Test the core logic of flashlight state toggling
|
var isOn = false;
|
||||||
var playerKey = "test_player_1";
|
|
||||||
var flashlightState = new Dictionary<string, bool>();
|
|
||||||
|
|
||||||
// Initial state should be off
|
|
||||||
flashlightState[playerKey] = false;
|
|
||||||
Assert.False(flashlightState[playerKey]);
|
|
||||||
|
|
||||||
// Toggle on
|
|
||||||
flashlightState[playerKey] = !flashlightState[playerKey];
|
|
||||||
Assert.True(flashlightState[playerKey]);
|
|
||||||
|
|
||||||
// Toggle off
|
|
||||||
flashlightState[playerKey] = !flashlightState[playerKey];
|
|
||||||
Assert.False(flashlightState[playerKey]);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void FlashlightState_TracksMultiplePlayers()
|
|
||||||
{
|
|
||||||
var playerStates = new Dictionary<string, bool>();
|
|
||||||
var player1 = "player_1";
|
|
||||||
var player2 = "player_2";
|
|
||||||
var player3 = "player_3";
|
|
||||||
|
|
||||||
// Initialize all off
|
|
||||||
playerStates[player1] = false;
|
|
||||||
playerStates[player2] = false;
|
|
||||||
playerStates[player3] = false;
|
|
||||||
|
|
||||||
// Toggle player 1 on
|
|
||||||
playerStates[player1] = !playerStates[player1];
|
|
||||||
Assert.True(playerStates[player1]);
|
|
||||||
Assert.False(playerStates[player2]);
|
|
||||||
Assert.False(playerStates[player3]);
|
|
||||||
|
|
||||||
// Toggle player 2 on
|
|
||||||
playerStates[player2] = !playerStates[player2];
|
|
||||||
Assert.True(playerStates[player1]);
|
|
||||||
Assert.True(playerStates[player2]);
|
|
||||||
Assert.False(playerStates[player3]);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void ToggleCooldown_PreventsRapidToggling()
|
|
||||||
{
|
|
||||||
// Simulate the cooldown mechanism
|
|
||||||
var canToggle = true;
|
var canToggle = true;
|
||||||
|
|
||||||
// First toggle - should work
|
Assert.True(FlashlightLogic.TryToggle(ref isOn, ref canToggle));
|
||||||
Assert.True(canToggle);
|
Assert.True(isOn);
|
||||||
canToggle = false; // Simulate setting cooldown
|
|
||||||
|
|
||||||
// Second toggle - should be blocked
|
|
||||||
Assert.False(canToggle);
|
Assert.False(canToggle);
|
||||||
|
|
||||||
// After cooldown expires
|
|
||||||
canToggle = true;
|
|
||||||
Assert.True(canToggle);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void CrouchState_UpdatesCorrectly()
|
public void TryToggle_BlockedDuringCooldown()
|
||||||
{
|
{
|
||||||
var isCrouching = false;
|
var isOn = true;
|
||||||
var buttons = 0;
|
var canToggle = false;
|
||||||
const int DuckButton = 1 << 2; // Typical duck button bit
|
|
||||||
|
Assert.False(FlashlightLogic.TryToggle(ref isOn, ref canToggle));
|
||||||
// Not crouching initially
|
Assert.True(isOn);
|
||||||
Assert.False(isCrouching);
|
Assert.False(canToggle);
|
||||||
|
}
|
||||||
// Press duck button
|
|
||||||
buttons |= DuckButton;
|
[Fact]
|
||||||
if ((buttons & DuckButton) != 0)
|
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 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
|
||||||
{
|
{
|
||||||
isCrouching = true;
|
ToggleCooldownSeconds = -1f,
|
||||||
}
|
Brightness = -5f,
|
||||||
Assert.True(isCrouching);
|
Range = 0f,
|
||||||
|
ColorTemperature = 50f,
|
||||||
// Release duck button
|
SoftX = -1f,
|
||||||
buttons &= ~DuckButton;
|
SoftY = -1f,
|
||||||
if ((buttons & DuckButton) == 0)
|
Skirt = -1f,
|
||||||
{
|
SkirtNear = -1f,
|
||||||
isCrouching = false;
|
SizeX = -1f,
|
||||||
}
|
SizeY = -1f,
|
||||||
Assert.False(isCrouching);
|
SizeZ = -1f,
|
||||||
|
ForwardDistance = -10f,
|
||||||
|
StandEyeOffsetZ = -1f,
|
||||||
|
CrouchEyeOffsetZ = -1f,
|
||||||
|
AttachmentName = " ",
|
||||||
|
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("axis_of_intent", config.AttachmentName);
|
||||||
|
Assert.Equal("materials/effects/lightcookies/flashlight.vtex", config.LightCookie);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
[Fact]
|
|
||||||
public void LightPosition_CalculatesCrouchOffsetCorrectly()
|
|
||||||
{
|
|
||||||
// Test the position calculation logic
|
|
||||||
var baseZ = 100f;
|
|
||||||
var standOffset = 64.03f;
|
|
||||||
var crouchOffset = 46.03f;
|
|
||||||
|
|
||||||
// Standing position
|
|
||||||
var standPosition = baseZ + standOffset;
|
|
||||||
Assert.Equal(164.03f, standPosition);
|
|
||||||
|
|
||||||
// Crouching position
|
|
||||||
var crouchPosition = baseZ + crouchOffset;
|
|
||||||
Assert.Equal(146.03f, crouchPosition);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void FlashlightEntity_Management()
|
|
||||||
{
|
|
||||||
// Test entity tracking dictionary behavior
|
|
||||||
var playerEntities = new Dictionary<string, FakeLightEntity>();
|
|
||||||
var playerKey = "test_player";
|
|
||||||
|
|
||||||
// No entity initially
|
|
||||||
Assert.False(playerEntities.TryGetValue(playerKey, out _));
|
|
||||||
|
|
||||||
// Add entity
|
|
||||||
var light = new FakeLightEntity { IsValid = true };
|
|
||||||
playerEntities[playerKey] = light;
|
|
||||||
Assert.True(playerEntities.TryGetValue(playerKey, out var retrieved));
|
|
||||||
Assert.True(retrieved?.IsValid);
|
|
||||||
|
|
||||||
// Remove entity
|
|
||||||
playerEntities.Remove(playerKey);
|
|
||||||
Assert.False(playerEntities.TryGetValue(playerKey, out _));
|
|
||||||
}
|
|
||||||
|
|
||||||
private class FakeLightEntity
|
|
||||||
{
|
|
||||||
public bool IsValid { get; set; }
|
|
||||||
public void Remove() => IsValid = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,149 +1,127 @@
|
||||||
using System.Drawing;
|
using System.Drawing;
|
||||||
|
using System.Numerics;
|
||||||
using CounterStrikeSharp.API;
|
using CounterStrikeSharp.API;
|
||||||
using CounterStrikeSharp.API.Core;
|
using CounterStrikeSharp.API.Core;
|
||||||
using CounterStrikeSharp.API.Core.Attributes;
|
using CounterStrikeSharp.API.Core.Attributes;
|
||||||
using CounterStrikeSharp.API.Core.Attributes.Registration;
|
using CounterStrikeSharp.API.Core.Attributes.Registration;
|
||||||
using CounterStrikeSharp.API.Modules.Commands;
|
using CounterStrikeSharp.API.Modules.Commands;
|
||||||
using Vector = CounterStrikeSharp.API.Modules.Utils.Vector;
|
using CounterStrikeSharp.API.Modules.Utils;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Flashlight;
|
namespace Flashlight;
|
||||||
|
|
||||||
[MinimumApiVersion(363)]
|
[MinimumApiVersion(371)]
|
||||||
public class Flashlight : BasePlugin
|
public class FlashlightPlugin : BasePlugin, IPluginConfig<FlashlightConfig>
|
||||||
{
|
{
|
||||||
public override string ModuleAuthor => "creazy.eth";
|
public override string ModuleAuthor => "creazy.eth";
|
||||||
public override string ModuleName => "Flashlight";
|
public override string ModuleName => "Flashlight";
|
||||||
public override string ModuleDescription => "Flashlight for Counter-Strike 2";
|
public override string ModuleDescription => "Flashlight for Counter-Strike 2";
|
||||||
public override string ModuleVersion => "0.0.7";
|
public override string ModuleVersion => "0.1.0";
|
||||||
|
|
||||||
private static string ModuleDisplayName => "Flashlight";
|
public FlashlightConfig Config { get; set; } = new();
|
||||||
|
|
||||||
// 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
|
|
||||||
// FIXED: Entity validity check before Remove() to prevent crashes on team switch (Issue #2)
|
|
||||||
|
|
||||||
public static Flashlight? Instance { get; private set; }
|
private readonly Dictionary<int, PlayerFlashlightState> _playerStates = new();
|
||||||
|
|
||||||
private readonly List<CCSPlayerController> _connectedPlayers = new();
|
public void OnConfigParsed(FlashlightConfig config)
|
||||||
private readonly Dictionary<CCSPlayerController, bool> _playerUsingFlashlight = new();
|
{
|
||||||
private readonly Dictionary<CCSPlayerController, bool> _playerIsCrouching = new();
|
config.Clamp();
|
||||||
private readonly Dictionary<CCSPlayerController, bool> _playerCanToggle = new();
|
Config = config;
|
||||||
private readonly Dictionary<CCSPlayerController, COmniLight> _playerFlashlight = new();
|
}
|
||||||
|
|
||||||
public override void Load(bool hotReload)
|
public override void Load(bool hotReload)
|
||||||
{
|
{
|
||||||
Instance = this;
|
Logger.LogInformation("{Name} v{Version} loading...", ModuleName, ModuleVersion);
|
||||||
|
|
||||||
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!");
|
RegisterListener<Listeners.OnTick>(OnTick);
|
||||||
|
|
||||||
|
if (hotReload)
|
||||||
|
{
|
||||||
|
foreach (var player in Utilities.GetPlayers().Where(IsEligiblePlayer))
|
||||||
|
{
|
||||||
|
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 || !Config.AllowUseKey)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var player in Utilities.GetPlayers())
|
||||||
|
{
|
||||||
|
if (!IsEligiblePlayer(player) || !player.PawnIsAlive)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var state = EnsureState(player);
|
||||||
|
var usePressed = (player.Buttons & PlayerButtons.Use) != 0;
|
||||||
|
|
||||||
|
if (FlashlightLogic.IsUsePressedEdge(usePressed, state.WasUsePressed))
|
||||||
|
{
|
||||||
|
TryToggleFlashlight(player, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
state.WasUsePressed = usePressed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[GameEventHandler]
|
[GameEventHandler]
|
||||||
public HookResult OnPlayerConnectFull(EventPlayerConnectFull @event, GameEventInfo info)
|
public HookResult OnPlayerConnectFull(EventPlayerConnectFull @event, GameEventInfo info)
|
||||||
{
|
{
|
||||||
var player = @event.Userid;
|
var player = @event.Userid;
|
||||||
|
if (player is null || !IsEligiblePlayer(player))
|
||||||
if (!player.IsValid || player.IsBot) return HookResult.Continue;
|
{
|
||||||
|
return HookResult.Continue;
|
||||||
_connectedPlayers.Add(player);
|
}
|
||||||
_playerUsingFlashlight[player] = false;
|
|
||||||
_playerIsCrouching[player] = false;
|
EnsureState(player);
|
||||||
_playerCanToggle[player] = true;
|
Logger.LogInformation("{Player} connected", player.PlayerName);
|
||||||
|
|
||||||
LogHelper.LogToConsole(ConsoleColor.Green, $"{player.PlayerName} connected");
|
|
||||||
|
|
||||||
return HookResult.Continue;
|
return HookResult.Continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
[GameEventHandler]
|
[GameEventHandler]
|
||||||
public HookResult OnPlayerDisconnect(EventPlayerDisconnect @event, GameEventInfo info)
|
public HookResult OnPlayerDisconnect(EventPlayerDisconnect @event, GameEventInfo info)
|
||||||
{
|
{
|
||||||
var player = @event.Userid;
|
var player = @event.Userid;
|
||||||
|
if (player is null || !player.IsValid)
|
||||||
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);
|
|
||||||
// Fix #2: Check entity validity before removing
|
|
||||||
if (flashlight != null && flashlight.IsValid)
|
|
||||||
{
|
{
|
||||||
flashlight.Remove();
|
return HookResult.Continue;
|
||||||
}
|
}
|
||||||
_playerFlashlight.Remove(player);
|
|
||||||
|
var slot = player.Slot;
|
||||||
LogHelper.LogToConsole(ConsoleColor.Green, $"{player.PlayerName} disconnected");
|
DestroyLight(slot);
|
||||||
|
_playerStates.Remove(slot);
|
||||||
return HookResult.Continue;
|
return HookResult.Continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
[GameEventHandler]
|
[GameEventHandler]
|
||||||
public HookResult OnPlayerSpawn(EventPlayerSpawn @event, GameEventInfo info)
|
public HookResult OnPlayerSpawn(EventPlayerSpawn @event, GameEventInfo info)
|
||||||
{
|
{
|
||||||
var player = @event.Userid;
|
var player = @event.Userid;
|
||||||
|
if (player is null || !IsEligiblePlayer(player))
|
||||||
if (!player.IsValid || player.IsBot) return HookResult.Continue;
|
|
||||||
|
|
||||||
if (_connectedPlayers.Contains(player) == false)
|
|
||||||
{
|
{
|
||||||
_connectedPlayers.Add(player);
|
return HookResult.Continue;
|
||||||
_playerUsingFlashlight[player] = false;
|
|
||||||
_playerIsCrouching[player] = false;
|
|
||||||
_playerCanToggle[player] = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_playerUsingFlashlight[player] = false;
|
var state = EnsureState(player);
|
||||||
|
state.IsOn = false;
|
||||||
|
DestroyLight(player.Slot);
|
||||||
return HookResult.Continue;
|
return HookResult.Continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -151,108 +129,173 @@ public class Flashlight : BasePlugin
|
||||||
public HookResult OnPlayerDeath(EventPlayerDeath @event, GameEventInfo info)
|
public HookResult OnPlayerDeath(EventPlayerDeath @event, GameEventInfo info)
|
||||||
{
|
{
|
||||||
var player = @event.Userid;
|
var player = @event.Userid;
|
||||||
|
if (player is null || !IsEligiblePlayer(player))
|
||||||
if (!player.IsValid || player.IsBot) return HookResult.Continue;
|
{
|
||||||
|
return HookResult.Continue;
|
||||||
_playerUsingFlashlight[player] = false;
|
}
|
||||||
|
|
||||||
|
var state = EnsureState(player);
|
||||||
|
state.IsOn = false;
|
||||||
|
DestroyLight(player.Slot);
|
||||||
return HookResult.Continue;
|
return HookResult.Continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
[GameEventHandler]
|
[GameEventHandler]
|
||||||
public HookResult OnPlayerTeam(EventPlayerTeam @event, GameEventInfo info)
|
public HookResult OnPlayerTeam(EventPlayerTeam @event, GameEventInfo info)
|
||||||
{
|
{
|
||||||
// Fix #2: Handle team switch - clean up flashlight entity
|
|
||||||
var player = @event.Userid;
|
var player = @event.Userid;
|
||||||
|
if (player is null || !IsEligiblePlayer(player))
|
||||||
if (!player.IsValid || player.IsBot) return HookResult.Continue;
|
|
||||||
|
|
||||||
// Turn off flashlight and clean up entity when switching teams
|
|
||||||
_playerUsingFlashlight[player] = false;
|
|
||||||
|
|
||||||
if (_playerFlashlight.TryGetValue(player, out var flashlight))
|
|
||||||
{
|
{
|
||||||
if (flashlight != null && flashlight.IsValid)
|
return HookResult.Continue;
|
||||||
{
|
|
||||||
flashlight.Remove();
|
|
||||||
}
|
|
||||||
_playerFlashlight.Remove(player);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var state = EnsureState(player);
|
||||||
|
state.IsOn = false;
|
||||||
|
DestroyLight(player.Slot);
|
||||||
return HookResult.Continue;
|
return HookResult.Continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ToggleFlashlight(CCSPlayerController player)
|
|
||||||
{
|
|
||||||
if (_playerUsingFlashlight[player] == false)
|
|
||||||
{
|
|
||||||
if (_playerFlashlight.TryGetValue(player, out var value))
|
|
||||||
{
|
|
||||||
// Fix #2: Check entity validity before removing
|
|
||||||
if (value.IsValid)
|
|
||||||
{
|
|
||||||
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")]
|
[ConsoleCommand("css_fl_toggle", "Toggles the flashlight")]
|
||||||
[CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)]
|
[CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)]
|
||||||
public void ToggleFlashlight(CCSPlayerController caller, CommandInfo? info)
|
public void OnToggleCommand(CCSPlayerController? caller, CommandInfo info)
|
||||||
{
|
{
|
||||||
if (!caller.IsValid || !caller.PawnIsAlive) return;
|
if (!Config.Enabled || caller is null || !IsEligiblePlayer(caller) || !caller.PawnIsAlive)
|
||||||
|
|
||||||
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;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
TryToggleFlashlight(caller, EnsureState(caller));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TryToggleFlashlight(CCSPlayerController player, PlayerFlashlightState state)
|
||||||
|
{
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
CreateAndParentLight(player, state);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
DestroyLight(player.Slot);
|
||||||
|
}
|
||||||
|
|
||||||
|
var slot = player.Slot;
|
||||||
|
AddTimer(Config.ToggleCooldownSeconds, () =>
|
||||||
|
{
|
||||||
|
if (_playerStates.TryGetValue(slot, out var current))
|
||||||
|
{
|
||||||
|
current.CanToggle = true;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
private void CreateAndParentLight(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<CBarnLight>("light_barn");
|
||||||
|
if (light is null || !light.IsValid)
|
||||||
|
{
|
||||||
|
Logger.LogWarning("Failed to create light_barn for {Player}", player.PlayerName);
|
||||||
|
state.IsOn = false;
|
||||||
|
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;
|
||||||
|
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;
|
||||||
|
|
||||||
|
light.Teleport(
|
||||||
|
origin,
|
||||||
|
new Vector3(angles.X, angles.Y, angles.Z),
|
||||||
|
null);
|
||||||
|
|
||||||
|
using (var keyValues = new CEntityKeyValues())
|
||||||
|
{
|
||||||
|
keyValues.SetString("lightcookie", Config.LightCookie);
|
||||||
|
light.DispatchSpawn(keyValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
light.AcceptInput("SetParent", pawn, light, "!activator");
|
||||||
|
light.AcceptInput("SetParentAttachmentMaintainOffset", null, null, Config.AttachmentName);
|
||||||
|
|
||||||
|
state.Light = light;
|
||||||
|
state.IsOn = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="CounterStrikeSharp.API" Version="1.0.363" />
|
<PackageReference Include="CounterStrikeSharp.API" Version="1.0.371" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
101
Flashlight/FlashlightConfig.cs
Normal file
101
Flashlight/FlashlightConfig.cs
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
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;
|
||||||
|
|
||||||
|
[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("AttachmentName")]
|
||||||
|
public string AttachmentName { get; set; } = "axis_of_intent";
|
||||||
|
|
||||||
|
[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(AttachmentName))
|
||||||
|
{
|
||||||
|
AttachmentName = "axis_of_intent";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(LightCookie))
|
||||||
|
{
|
||||||
|
LightCookie = "materials/effects/lightcookies/flashlight.vtex";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
62
Flashlight/FlashlightLogic.cs
Normal file
62
Flashlight/FlashlightLogic.cs
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
using System.Numerics;
|
||||||
|
|
||||||
|
namespace Flashlight;
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool ShouldCreateLight(bool isOn, bool hasValidLight)
|
||||||
|
{
|
||||||
|
return isOn && !hasValidLight;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool ShouldDestroyLight(bool isOn, bool hasValidLight)
|
||||||
|
{
|
||||||
|
return !isOn && hasValidLight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,56 +0,0 @@
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
18
Flashlight/PlayerFlashlightState.cs
Normal file
18
Flashlight/PlayerFlashlightState.cs
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
99
README.md
99
README.md
|
|
@ -1,68 +1,105 @@
|
||||||
# 🔦 Flashlight
|
# Flashlight
|
||||||
|
|
||||||
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. 🎮
|
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
|
## Features
|
||||||
|
|
||||||
- 💡 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 bound to a different key.
|
- Toggle with the Use key (`E` by default) or `css_fl_toggle`
|
||||||
- 💀 The flashlight is automatically turned off when the player dies or respawns.
|
- One `light_barn` per player, parented to the pawn attachment (no per-tick spawn/teleport)
|
||||||
- 🚫 The flashlight is only available to human players, not bots.
|
- Configurable brightness, range, color, shadows, offsets, and attachment
|
||||||
|
- Automatically turns off on death, spawn, and team change
|
||||||
|
- Bots ignored
|
||||||
|
|
||||||
## 🔧 Installation
|
## Requirements
|
||||||
|
|
||||||
1. ⬇️ Download the latest release from the GitHub repository.
|
- CounterStrikeSharp **1.0.371+** (API minimum version enforced)
|
||||||
2. 📁 Extract the ZIP file.
|
- .NET **10** runtime as provided by your CounterStrikeSharp install
|
||||||
3. 📂 Place the plugin in the `game/csgo/addons/counterstrikesharp/plugins/Flashlight` directory.
|
|
||||||
|
|
||||||
## 💻 Usage
|
## Installation
|
||||||
|
|
||||||
⌨️ 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 bound to a different key.
|
1. Download the latest release ZIP.
|
||||||
|
2. Extract it.
|
||||||
|
3. Place the plugin folder in `game/csgo/addons/counterstrikesharp/plugins/Flashlight`.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
- Press Use (`E`) to toggle, if enabled in config.
|
||||||
|
- Or bind a key:
|
||||||
|
|
||||||
Example bind:
|
|
||||||
```
|
```
|
||||||
bind f "css_fl_toggle"
|
bind f "css_fl_toggle"
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🛠️ Development
|
## Configuration
|
||||||
|
|
||||||
|
On first load, CounterStrikeSharp writes a JSON config for the plugin. Defaults:
|
||||||
|
|
||||||
|
| Key | Default | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `Enabled` | `true` | Master switch |
|
||||||
|
| `AllowUseKey` | `true` | Allow Use-key toggle |
|
||||||
|
| `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` | 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
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
- .NET 8.0 SDK
|
- .NET 10 SDK
|
||||||
- CounterStrikeSharp API v1.0.363+
|
- CounterStrikeSharp.API 1.0.371+
|
||||||
|
|
||||||
### Building
|
### Build
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
dotnet restore
|
dotnet restore
|
||||||
dotnet build
|
dotnet build
|
||||||
```
|
```
|
||||||
|
|
||||||
### Testing
|
### Test
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
dotnet test
|
dotnet test
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🤝 Contributing
|
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.
|
||||||
|
|
||||||
Contributions are welcome. Please open an issue or submit a pull request on GitHub. 🐙
|
## Changelog
|
||||||
|
|
||||||
## 📋 Changelog
|
### v0.1.0
|
||||||
|
|
||||||
### v0.0.6 (Latest)
|
- Updated to .NET 10 and CounterStrikeSharp.API 1.0.371
|
||||||
- ✅ Updated to .NET 8.0
|
- Replaced per-tick `light_omni2` spawn/teleport with parented `light_barn`
|
||||||
- ✅ Updated to CounterStrikeSharp.API v1.0.363
|
- Added `IPluginConfig` settings for light and toggle behavior
|
||||||
- ✅ Added xUnit test project with core logic tests
|
- Added focused xUnit tests for pure helpers
|
||||||
- ✅ Updated GitHub Actions workflow with testing
|
- Updated GitHub Actions for .NET 10, PR tests, and tag releases
|
||||||
- ✅ Improved code compatibility with latest CS2/CSS API
|
- 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
|
### v0.0.5
|
||||||
|
|
||||||
- Initial release
|
- Initial release
|
||||||
|
|
||||||
## 📃 License
|
## License
|
||||||
|
|
||||||
This project is licensed under the GNU General Public License. ⚖️
|
GNU General Public License. See `LICENSE`.
|
||||||
|
|
||||||
## ✏️ Author
|
## Author
|
||||||
|
|
||||||
This project was created by [creazy.eth](https://github.com/creazy231)
|
[creazy.eth](https://github.com/creazy231)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
# Flashlight Modernization Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Modernize the flashlight plugin to .NET 10 / CSS 1.0.371 with a parented `light_barn` implementation, config, real unit tests, fixed CI, and updated README.
|
||||||
|
|
||||||
|
**Architecture:** Pure helpers in `FlashlightLogic` + config clamps in `FlashlightConfig`; plugin owns lifecycle and wires Use/command/events; one `CBarnLight` per active player, parented once, toggled via create/destroy.
|
||||||
|
|
||||||
|
**Tech Stack:** .NET 10, CounterStrikeSharp.API 1.0.371, xUnit, GitHub Actions
|
||||||
|
|
||||||
|
## Global Constraints
|
||||||
|
|
||||||
|
- Target framework: `net10.0`
|
||||||
|
- Package: `CounterStrikeSharp.API` `1.0.371`
|
||||||
|
- `[MinimumApiVersion(371)]`
|
||||||
|
- Module version: `0.1.0`
|
||||||
|
- No per-tick entity create/spawn/teleport in steady state
|
||||||
|
- Spec: `docs/superpowers/specs/2026-07-21-flashlight-modernization-design.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Project targets + pure logic + tests
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `Flashlight/Flashlight.csproj`
|
||||||
|
- Modify: `Flashlight.Tests/Flashlight.Tests.csproj`
|
||||||
|
- Create: `Flashlight/FlashlightConfig.cs`
|
||||||
|
- Create: `Flashlight/FlashlightLogic.cs`
|
||||||
|
- Create: `Flashlight/PlayerFlashlightState.cs`
|
||||||
|
- Modify: `Flashlight.Tests/FlashlightLogicTests.cs`
|
||||||
|
|
||||||
|
- [x] Retarget both projects to `net10.0`; bump CSS to `1.0.371`; bump test packages as needed
|
||||||
|
- [x] Implement `FlashlightConfig` with defaults from the spec and a `Clamp()` method
|
||||||
|
- [x] Implement pure `FlashlightLogic` helpers: try-toggle with cooldown, eye Z offset, origin from base+forward
|
||||||
|
- [x] Replace smoke tests with tests against those helpers
|
||||||
|
- [x] Run `dotnet test` and confirm pass
|
||||||
|
|
||||||
|
### Task 2: Plugin rewrite (parented light_barn)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `Flashlight/Flashlight.cs`
|
||||||
|
- Delete or gut: `Flashlight/LogHelper.cs` (prefer `Logger`)
|
||||||
|
|
||||||
|
- [x] Implement `BasePlugin, IPluginConfig<FlashlightConfig>`
|
||||||
|
- [x] OnTick: Use-key edge + cooldown only when `AllowUseKey`
|
||||||
|
- [x] Create/parent/enable `light_barn` once on toggle on; remove on toggle off
|
||||||
|
- [x] Cleanup on death/spawn/team/disconnect/unload
|
||||||
|
- [x] Keep `css_fl_toggle`
|
||||||
|
- [x] Run `dotnet build` and confirm success
|
||||||
|
|
||||||
|
### Task 3: CI + README
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `.github/workflows/build.yml`
|
||||||
|
- Modify: `README.md`
|
||||||
|
|
||||||
|
- [x] CI: .NET 10, restore, test, release build
|
||||||
|
- [x] README: versions, parented light, config table, build/test, changelog 0.1.0
|
||||||
|
- [x] Run `dotnet test` once more
|
||||||
|
|
||||||
|
---
|
||||||
|
|
@ -0,0 +1,148 @@
|
||||||
|
# Flashlight Plugin Modernization Design
|
||||||
|
|
||||||
|
**Date:** 2026-07-21
|
||||||
|
**Status:** Approved
|
||||||
|
**Version target:** 0.1.0
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Modernize the CounterStrikeSharp flashlight plugin to the latest CSS/.NET stack, replace the per-tick spawn/teleport light path with a parented `light_barn` implementation, add `IPluginConfig` for admin tuning, improve testability, and update the README.
|
||||||
|
|
||||||
|
## Background
|
||||||
|
|
||||||
|
The current plugin (`Flashlight` v0.0.7) targets CounterStrikeSharp.API `1.0.363` on .NET 8. While the flashlight is on, `OnTick` creates a `light_omni2` and calls `DispatchSpawn` every server frame. That is the dominant performance problem. Existing tests only exercise inlined dictionary/bool logic and do not cover extractable plugin helpers. CI still installs .NET 7.
|
||||||
|
|
||||||
|
Latest CounterStrikeSharp.API is `1.0.371` and targets .NET 10. CS2Fixes demonstrates the preferred flashlight pattern: spawn `light_barn` once, set a flashlight lightcookie via entity keyvalues, parent to the player pawn attachment, and toggle enablement rather than teleporting every tick.
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
| Topic | Decision |
|
||||||
|
| --- | --- |
|
||||||
|
| Runtime | .NET 10 + CounterStrikeSharp.API 1.0.371 |
|
||||||
|
| Light entity | `light_barn` (`CBarnLight`), parented once |
|
||||||
|
| Position updates | Engine parenting; no per-tick teleport |
|
||||||
|
| Config | `IPluginConfig<FlashlightConfig>` with JSON config |
|
||||||
|
| Logging | Prefer `BasePlugin.Logger` |
|
||||||
|
| README | Fully updated for new runtime, behavior, config, build/test |
|
||||||
|
| Module version | 0.1.0 |
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
FlashlightPlugin (BasePlugin, IPluginConfig)
|
||||||
|
├── FlashlightConfig // JSON-backed settings + clamps
|
||||||
|
├── PlayerFlashlightState // per-player flags + entity handle
|
||||||
|
├── FlashlightService // create / parent / enable / destroy
|
||||||
|
└── FlashlightLogic // pure helpers (toggle, cooldown, offsets)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Runtime flow
|
||||||
|
|
||||||
|
1. `OnConfigParsed` validates/clamps config.
|
||||||
|
2. `Load` registers `OnTick` (Use-key edge + cooldown only), game event handlers, and command `css_fl_toggle`.
|
||||||
|
3. When a player turns the light on:
|
||||||
|
- Create `CBarnLight` via `Utilities.CreateEntityByName<CBarnLight>("light_barn")`.
|
||||||
|
- Apply config (brightness, range, color, temperature, soft/skirt/size, cast shadows, direct light).
|
||||||
|
- Compute initial origin: pawn origin + eye Z offset + forward * `ForwardDistance`.
|
||||||
|
- `Teleport` once using `System.Numerics.Vector3` overloads (avoid legacy `Vector` allocs).
|
||||||
|
- `DispatchSpawn(CEntityKeyValues)` with `lightcookie` = configured path (default flashlight vtex).
|
||||||
|
- `AcceptInput("SetParent", pawn, …)` then `AcceptInput("SetParentAttachmentMaintainOffset", …, AttachmentName)`.
|
||||||
|
- Set `Enabled = true`.
|
||||||
|
4. When turned off: set `Enabled = false` and remove the entity (or disable and keep — prefer remove to avoid orphaned entities across pawn changes).
|
||||||
|
5. Cleanup on death, team change, disconnect, and plugin unload.
|
||||||
|
|
||||||
|
### Why not keep tick teleport?
|
||||||
|
|
||||||
|
Parenting follows view/attachment with far less managed work and no entity churn. Tick work is limited to scanning connected humans for Use-button edges and cooldown expiry.
|
||||||
|
|
||||||
|
## Config surface
|
||||||
|
|
||||||
|
File written/loaded by CSS config system (standard plugin config JSON).
|
||||||
|
|
||||||
|
| Key | Type | Default | Notes |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `Enabled` | bool | `true` | Master switch |
|
||||||
|
| `AllowUseKey` | bool | `true` | Toggle via Use (`E`) |
|
||||||
|
| `ToggleCooldownSeconds` | float | `0.25` | Clamp ≥ 0 |
|
||||||
|
| `Brightness` | float | `1.0` | |
|
||||||
|
| `Range` | float | `2048` | Match CS2Fixes-style defaults |
|
||||||
|
| `ColorR` / `ColorG` / `ColorB` | byte | `255` | White |
|
||||||
|
| `ColorTemperature` | float | `6500` | |
|
||||||
|
| `CastShadows` | bool | `true` | Maps to `CastShadows` int |
|
||||||
|
| `SoftX` / `SoftY` | float | `1.0` | |
|
||||||
|
| `Skirt` | float | `0.5` | |
|
||||||
|
| `SkirtNear` | float | `1.0` | |
|
||||||
|
| `SizeX` / `SizeY` / `SizeZ` | float | `45` / `45` / `0.03` | `SizeParams` |
|
||||||
|
| `ForwardDistance` | float | `54` | Avoid AWP blocking beam |
|
||||||
|
| `StandEyeOffsetZ` | float | `64` | |
|
||||||
|
| `CrouchEyeOffsetZ` | float | `46` | Used at spawn time only |
|
||||||
|
| `AttachmentName` | string | `axis_of_intent` | Parent attachment |
|
||||||
|
| `LightCookie` | string | `materials/effects/lightcookies/flashlight.vtex` | |
|
||||||
|
|
||||||
|
Invalid values are clamped or rejected in `OnConfigParsed` with log warnings; plugin remains loadable when possible.
|
||||||
|
|
||||||
|
## Player state
|
||||||
|
|
||||||
|
Replace multiple `Dictionary<CCSPlayerController, …>` maps with one structure keyed by player slot (or controller), holding:
|
||||||
|
|
||||||
|
- `IsOn`
|
||||||
|
- `CanToggle`
|
||||||
|
- `Light` (`CBarnLight?`)
|
||||||
|
|
||||||
|
Crouch tracking for continuous Z updates is unnecessary once the light is parented; crouch offset is only applied at creation time. Optional: read duck state at spawn for initial Z only.
|
||||||
|
|
||||||
|
## Event / command behavior (unchanged UX)
|
||||||
|
|
||||||
|
- Use key toggles when `AllowUseKey` is true and cooldown allows.
|
||||||
|
- `css_fl_toggle` remains client-only command alternative.
|
||||||
|
- Flashlight turns off on death and spawn; entity cleaned on team change and disconnect.
|
||||||
|
- Bots ignored.
|
||||||
|
|
||||||
|
## Testing strategy
|
||||||
|
|
||||||
|
Full CSS entity lifecycle cannot be unit-tested without a game server. Extract and test pure logic:
|
||||||
|
|
||||||
|
1. Toggle state transitions and cooldown gating.
|
||||||
|
2. Config clamp helpers (range, cooldown, color channels).
|
||||||
|
3. Initial position offset calculation (stand/crouch Z + forward distance given basis vectors).
|
||||||
|
4. Enable/disable policy: when entity should be created vs destroyed.
|
||||||
|
|
||||||
|
Use xUnit on .NET 10. Keep Moq only if needed; prefer plain helpers over mocking CSS types.
|
||||||
|
|
||||||
|
## CI / packaging
|
||||||
|
|
||||||
|
- GitHub Actions: .NET 10 SDK, `dotnet restore`, `dotnet test`, release build on tags.
|
||||||
|
- Remove stale .NET 7 setup.
|
||||||
|
- Publish zip layout unchanged: `plugins/Flashlight/`.
|
||||||
|
|
||||||
|
## README updates
|
||||||
|
|
||||||
|
- Prerequisites: .NET 10, CSS 1.0.371+.
|
||||||
|
- Behavior: parented `light_barn`, Use + command.
|
||||||
|
- Config table with defaults.
|
||||||
|
- Build / test instructions.
|
||||||
|
- Changelog entry for 0.1.0 (API bump, performance rewrite, config, tests, CI).
|
||||||
|
|
||||||
|
## Out of scope
|
||||||
|
|
||||||
|
- Particle flashlight mode (CS2Fixes mode 2).
|
||||||
|
- Admin permissions / VIP-only flashlight.
|
||||||
|
- Client-side HUD indicators.
|
||||||
|
- Migrating to CounterStrikeSharp 2.0 alpha.
|
||||||
|
|
||||||
|
## Risks / mitigations
|
||||||
|
|
||||||
|
| Risk | Mitigation |
|
||||||
|
| --- | --- |
|
||||||
|
| Attachment name missing on some models | Configurable `AttachmentName`; fall back to parent-only if attachment input fails |
|
||||||
|
| `CEntityKeyValues` lightcookie path differs | Use CS2Fixes-proven path; document override |
|
||||||
|
| .NET 10 server prerequisite | Document clearly; MinimumApiVersion 371 |
|
||||||
|
| Parenting breaks on pawn swap | Recreate light on spawn; destroy on death/team |
|
||||||
|
|
||||||
|
## Success criteria
|
||||||
|
|
||||||
|
- Builds against CounterStrikeSharp.API 1.0.371 on net10.0.
|
||||||
|
- No entity create/spawn/teleport in the steady-state OnTick path.
|
||||||
|
- Config file generated and honored.
|
||||||
|
- Unit tests cover pure helpers and pass in CI.
|
||||||
|
- README matches shipped behavior and versions.
|
||||||
Loading…
Add table
Add a link
Reference in a new issue