mirror of
https://github.com/creazy231/cs2-css-flashlight.git
synced 2026-09-27 20:17:04 +02:00
Compare commits
No commits in common. "main" and "v0.0.3" have entirely different histories.
12 changed files with 248 additions and 952 deletions
51
.github/workflows/build.yml
vendored
51
.github/workflows/build.yml
vendored
|
|
@ -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
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
|
|
@ -1,5 +1,3 @@
|
|||
.idea
|
||||
**/bin/
|
||||
**/obj/
|
||||
.DS_Store
|
||||
docs/
|
||||
Flashlight/bin
|
||||
Flashlight/obj
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.1">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.4">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../Flashlight/Flashlight.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -1,119 +1,99 @@
|
|||
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<FlashlightConfig>
|
||||
public class Flashlight : BasePlugin
|
||||
{
|
||||
public override string ModuleAuthor => "creazy.eth";
|
||||
public override string ModuleName => "Flashlight";
|
||||
public override string ModuleDescription => "Flashlight for Counter-Strike 2";
|
||||
public override string ModuleVersion => "0.1.2";
|
||||
public override string ModuleVersion => "0.0.3";
|
||||
|
||||
public FlashlightConfig Config { get; set; } = new();
|
||||
private static string ModuleDisplayName => "Flashlight";
|
||||
|
||||
private readonly Dictionary<int, PlayerFlashlightState> _playerStates = 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
|
||||
|
||||
public void OnConfigParsed(FlashlightConfig config)
|
||||
{
|
||||
config.Clamp();
|
||||
Config = config;
|
||||
}
|
||||
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)
|
||||
{
|
||||
Logger.LogInformation("{Name} v{Version} loading...", ModuleName, ModuleVersion);
|
||||
LogHelper.LogToConsole(ConsoleColor.Green, $"{ModuleName} version {ModuleVersion} loaded");
|
||||
|
||||
RegisterListener<Listeners.OnTick>(OnTick);
|
||||
Instance = this;
|
||||
|
||||
if (hotReload)
|
||||
RegisterListener<Listeners.OnTick>(() =>
|
||||
{
|
||||
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);
|
||||
ToggleFlashlight(player);
|
||||
|
||||
if (_playerCanToggle[player] == false) continue;
|
||||
|
||||
if ((player.Buttons & PlayerButtons.Use) != 0)
|
||||
{
|
||||
_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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Logger.LogInformation("{Name} v{Version} loaded!", ModuleName, ModuleVersion);
|
||||
if ((player.Buttons & PlayerButtons.Duck) != 0)
|
||||
{
|
||||
_playerIsCrouching[player] = true;
|
||||
}
|
||||
|
||||
public override void Unload(bool hotReload)
|
||||
else
|
||||
{
|
||||
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)
|
||||
{
|
||||
deadState.IsOn = false;
|
||||
DestroyLight(player.Slot);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
var state = EnsureState(player);
|
||||
|
||||
if (Config.AllowUseKey)
|
||||
{
|
||||
var usePressed = (player.Buttons & PlayerButtons.Use) != 0;
|
||||
|
||||
if (FlashlightLogic.IsUsePressedEdge(usePressed, state.WasUsePressed))
|
||||
{
|
||||
TryToggleFlashlight(player, state);
|
||||
}
|
||||
|
||||
state.WasUsePressed = usePressed;
|
||||
}
|
||||
|
||||
if (state.IsOn)
|
||||
{
|
||||
UpdateLight(player, state);
|
||||
_playerIsCrouching[player] = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
LogHelper.LogToChatAll($"{ModuleDisplayName} v{ModuleVersion} loaded!");
|
||||
}
|
||||
|
||||
[GameEventHandler]
|
||||
public HookResult OnPlayerConnectFull(EventPlayerConnectFull @event, GameEventInfo info)
|
||||
{
|
||||
var player = @event.Userid;
|
||||
if (player is null || !IsEligiblePlayer(player))
|
||||
{
|
||||
return HookResult.Continue;
|
||||
}
|
||||
|
||||
EnsureState(player);
|
||||
Logger.LogInformation("{Player} connected", player.PlayerName);
|
||||
if (!player.IsValid || player.IsBot) return HookResult.Continue;
|
||||
|
||||
_connectedPlayers.Add(player);
|
||||
_playerUsingFlashlight[player] = false;
|
||||
_playerIsCrouching[player] = false;
|
||||
_playerCanToggle[player] = true;
|
||||
|
||||
LogHelper.LogToConsole(ConsoleColor.Green, $"{player.PlayerName} connected");
|
||||
|
||||
return HookResult.Continue;
|
||||
}
|
||||
|
||||
|
|
@ -121,14 +101,20 @@ public class FlashlightPlugin : BasePlugin, IPluginConfig<FlashlightConfig>
|
|||
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;
|
||||
}
|
||||
|
||||
|
|
@ -136,14 +122,11 @@ public class FlashlightPlugin : BasePlugin, IPluginConfig<FlashlightConfig>
|
|||
public HookResult OnPlayerSpawn(EventPlayerSpawn @event, GameEventInfo info)
|
||||
{
|
||||
var player = @event.Userid;
|
||||
if (player is null || !IsEligiblePlayer(player))
|
||||
{
|
||||
return HookResult.Continue;
|
||||
}
|
||||
|
||||
var state = EnsureState(player);
|
||||
state.IsOn = false;
|
||||
DestroyLight(player.Slot);
|
||||
if (!player.IsValid || player.IsBot) return HookResult.Continue;
|
||||
|
||||
_playerUsingFlashlight[player] = false;
|
||||
|
||||
return HookResult.Continue;
|
||||
}
|
||||
|
||||
|
|
@ -151,218 +134,72 @@ public class FlashlightPlugin : BasePlugin, IPluginConfig<FlashlightConfig>
|
|||
public HookResult OnPlayerDeath(EventPlayerDeath @event, GameEventInfo info)
|
||||
{
|
||||
var player = @event.Userid;
|
||||
if (player is null || !IsEligiblePlayer(player))
|
||||
|
||||
if (!player.IsValid || player.IsBot) return HookResult.Continue;
|
||||
|
||||
_playerUsingFlashlight[player] = false;
|
||||
|
||||
return HookResult.Continue;
|
||||
}
|
||||
|
||||
public void ToggleFlashlight(CCSPlayerController player)
|
||||
{
|
||||
return HookResult.Continue;
|
||||
}
|
||||
|
||||
var state = EnsureState(player);
|
||||
state.IsOn = false;
|
||||
DestroyLight(player.Slot);
|
||||
return HookResult.Continue;
|
||||
}
|
||||
|
||||
[GameEventHandler]
|
||||
public HookResult OnPlayerTeam(EventPlayerTeam @event, GameEventInfo info)
|
||||
if (_playerUsingFlashlight[player] == false)
|
||||
{
|
||||
var player = @event.Userid;
|
||||
if (player is null || !IsEligiblePlayer(player))
|
||||
if (_playerFlashlight.TryGetValue(player, out var value))
|
||||
{
|
||||
return HookResult.Continue;
|
||||
value.Remove();
|
||||
_playerFlashlight.Remove(player);
|
||||
}
|
||||
|
||||
var state = EnsureState(player);
|
||||
state.IsOn = false;
|
||||
DestroyLight(player.Slot);
|
||||
return HookResult.Continue;
|
||||
return;
|
||||
}
|
||||
|
||||
[ConsoleCommand("css_fl_toggle", "Toggles the flashlight")]
|
||||
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;
|
||||
|
||||
entity.Teleport(
|
||||
new Vector(
|
||||
player.PlayerPawn.Value!.AbsOrigin!.X,
|
||||
player.PlayerPawn.Value!.AbsOrigin!.Y,
|
||||
player.PlayerPawn.Value!.AbsOrigin!.Z + (_playerIsCrouching[player] ? 46.03f : 64.03f)
|
||||
),
|
||||
player.PlayerPawn.Value!.EyeAngles,
|
||||
player.PlayerPawn.Value!.AbsVelocity
|
||||
);
|
||||
|
||||
entity.OuterAngle = 45f;
|
||||
entity.Enabled = true;
|
||||
entity.Color = Color.White;
|
||||
entity.ColorTemperature = 6500;
|
||||
entity.Brightness = 1f;
|
||||
entity.Range = 5000f;
|
||||
|
||||
entity.DispatchSpawn();
|
||||
_playerFlashlight[player] = entity;
|
||||
}
|
||||
|
||||
[ConsoleCommand("fl_toggle", "Toggles the flashlight")]
|
||||
[CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)]
|
||||
public void OnToggleCommand(CCSPlayerController? caller, CommandInfo info)
|
||||
public void ToggleFlashlight(CCSPlayerController caller, CommandInfo? info)
|
||||
{
|
||||
if (!Config.Enabled || caller is null || !IsEligiblePlayer(caller) || !caller.PawnIsAlive)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!caller.IsValid || !caller.PawnIsAlive) return;
|
||||
|
||||
TryToggleFlashlight(caller, EnsureState(caller));
|
||||
}
|
||||
if (_playerCanToggle[caller] == false) return;
|
||||
|
||||
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;
|
||||
}
|
||||
_playerUsingFlashlight[caller] = !_playerUsingFlashlight[caller];
|
||||
_playerCanToggle[caller] = false;
|
||||
|
||||
var isOn = state.IsOn;
|
||||
var canToggle = state.CanToggle;
|
||||
|
||||
if (!FlashlightLogic.TryToggle(ref isOn, ref canToggle))
|
||||
Instance?.AddTimer(0.25f, () =>
|
||||
{
|
||||
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<CBarnLight>("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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Keeps the light glued to the player's eye every tick.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The light is deliberately not parented to the pawn. Handing it to the engine via
|
||||
/// <c>SetParent</c> / <c>SetParentAttachmentMaintainOffset</c> 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.
|
||||
/// </remarks>
|
||||
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 };
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CounterStrikeSharp.API" Version="1.0.371" />
|
||||
<PackageReference Include="CounterStrikeSharp.API" Version="1.0.124" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Which team may use the flashlight: Any, CT, or T.
|
||||
/// </summary>
|
||||
[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"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,115 +0,0 @@
|
|||
using System.Numerics;
|
||||
|
||||
namespace Flashlight;
|
||||
|
||||
/// <summary>
|
||||
/// Position and orientation to apply to the flashlight entity.
|
||||
/// <paramref name="Angles"/> is a Source QAngle laid out as (pitch, yaw, roll).
|
||||
/// </summary>
|
||||
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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Forward vector on the horizontal plane only, ignoring pitch.
|
||||
/// </summary>
|
||||
public static Vector3 HorizontalForwardFromYawDegrees(float yaw)
|
||||
{
|
||||
var yawRad = yaw * (MathF.PI / 180f);
|
||||
|
||||
return new Vector3(MathF.Cos(yawRad), MathF.Sin(yawRad), 0f);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// World transform for the flashlight given the player's current pawn origin and view angles.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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).
|
||||
/// </remarks>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns whether <paramref name="team"/> may use the flashlight.
|
||||
/// Team values match CS2: 2 = Terrorist, 3 = Counter-Terrorist.
|
||||
/// </summary>
|
||||
public static bool IsTeamAllowed(string allowedTeam, byte team)
|
||||
{
|
||||
return allowedTeam switch
|
||||
{
|
||||
"T" => team == 2,
|
||||
"CT" => team == 3,
|
||||
_ => true
|
||||
};
|
||||
}
|
||||
}
|
||||
56
Flashlight/LogHelper.cs
Normal file
56
Flashlight/LogHelper.cs
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
121
README.md
121
README.md
|
|
@ -1,114 +1,37 @@
|
|||
# Flashlight
|
||||
# 🔦 Flashlight
|
||||
|
||||
Flashlight is a Counter-Strike 2 server plugin written in C# with [CounterStrikeSharp](https://docs.cssharp.dev). It gives human players a toggleable flashlight using a `light_barn` entity.
|
||||
Flashlight is a plugin for Counter-Strike 2 that adds a flashlight feature for players. It is written in C# and uses the CounterStrikeSharp API. 🎮
|
||||
|
||||
## Features
|
||||
## ⭐ Features
|
||||
|
||||
- Toggle with the Use key (`E` by default) or `css_fl_toggle`
|
||||
- One `light_barn` per player, re-aimed each tick so the beam tracks both pitch and yaw
|
||||
- Configurable brightness, range, color, shadows, and offsets
|
||||
- Optional team restriction (`Any`, `CT`, or `T`)
|
||||
- Automatically turns off on death, spawn, and team change
|
||||
- Bots ignored
|
||||
- 💡 Players can toggle the flashlight on and off using `Use` key (the default key for this is `E`) or `/fl_toggle` in chat which could be bind to a different key.
|
||||
- 💀 The flashlight is automatically turned off when the player dies or respawns.
|
||||
- 🚫 The flashlight is only available to human players, not bots.
|
||||
|
||||
## Requirements
|
||||
## 🔧 Installation
|
||||
|
||||
- CounterStrikeSharp **1.0.371+** (API minimum version enforced)
|
||||
- .NET **10** runtime as provided by your CounterStrikeSharp install
|
||||
1. ⬇️ Download the latest release from the GitHub repository.
|
||||
2. 📁 Extract the ZIP file.
|
||||
3. 📂 Place the plugin in the `game/csgo/addons/counterstrikesharp/plugins/Flashlight` directory.
|
||||
|
||||
## Installation
|
||||
## 💻 Usage
|
||||
|
||||
1. Download the latest release ZIP.
|
||||
2. Extract it.
|
||||
3. Place the plugin folder in `game/csgo/addons/counterstrikesharp/plugins/Flashlight`.
|
||||
⌨️ Use the `Use` key to toggle the flashlight on and off. The default key for this is `E`.
|
||||
|
||||
## Usage
|
||||
## 🤝 Contributing
|
||||
|
||||
- Press Use (`E`) to toggle, if enabled in config.
|
||||
- Or bind a key:
|
||||
Contributions are welcome. Please open an issue or submit a pull request on GitHub. 🐙
|
||||
|
||||
```
|
||||
bind f "css_fl_toggle"
|
||||
```
|
||||
## 📝 Development Tasks
|
||||
|
||||
## Configuration
|
||||
- Change crouch-tracking to a more elegant solution
|
||||
- Add config and make light entity values configurable
|
||||
- Maybe replace light_omni2 with light_rect or something else
|
||||
|
||||
On first load, CounterStrikeSharp writes a JSON config for the plugin. Defaults:
|
||||
## 📃 License
|
||||
|
||||
| Key | Default | Description |
|
||||
| --- | --- | --- |
|
||||
| `Enabled` | `true` | Master switch |
|
||||
| `AllowUseKey` | `true` | Allow Use-key toggle |
|
||||
| `AllowedTeam` | `Any` | Who may use it: `Any`, `CT`, or `T` |
|
||||
| `ToggleCooldownSeconds` | `0.25` | Toggle cooldown |
|
||||
| `Brightness` | `1.0` | Light brightness |
|
||||
| `Range` | `2048` | Light range |
|
||||
| `ColorR` / `ColorG` / `ColorB` | `255` | Light color |
|
||||
| `ColorTemperature` | `6500` | Kelvin temperature |
|
||||
| `CastShadows` | `true` | Cast dynamic shadows |
|
||||
| `SoftX` / `SoftY` | `1.0` | Softness |
|
||||
| `Skirt` / `SkirtNear` | `0.5` / `1.0` | Skirt falloff |
|
||||
| `SizeX` / `SizeY` / `SizeZ` | `45` / `45` / `0.03` | Beam size params |
|
||||
| `ForwardDistance` | `54` | Horizontal offset in front of the eye, so the beam clears the player model |
|
||||
| `StandEyeOffsetZ` | `64` | Standing eye height offset |
|
||||
| `CrouchEyeOffsetZ` | `46` | Crouching eye height offset |
|
||||
| `LightCookie` | `materials/effects/lightcookies/flashlight.vtex` | Flashlight cookie texture |
|
||||
This project is licensed under the GNU General Public License. ⚖️
|
||||
|
||||
## Development
|
||||
## ✏️ Author
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- .NET 10 SDK
|
||||
- CounterStrikeSharp.API 1.0.371+
|
||||
|
||||
### Build
|
||||
|
||||
```bash
|
||||
dotnet restore
|
||||
dotnet build
|
||||
```
|
||||
|
||||
### Test
|
||||
|
||||
```bash
|
||||
dotnet test
|
||||
```
|
||||
|
||||
Unit tests cover toggle/cooldown logic, Use-key edge detection, transform math (origin and pitch/yaw angles), create/destroy policy, and config clamping. Entity behaviour itself requires a live CS2 server.
|
||||
|
||||
## Changelog
|
||||
|
||||
### v0.1.2
|
||||
|
||||
- Fixed the beam only following horizontal aim: the light was parented to the pawn's `axis_of_intent` attachment, which carries body yaw but not view pitch, so looking up or down never moved it. The light is now un-parented and re-aimed every tick from the pawn's live `V_angle`.
|
||||
- Fixed the flashlight never updating when `AllowUseKey` was `false`, which previously short-circuited the whole tick loop.
|
||||
- `ForwardDistance` now offsets the light horizontally only, so looking straight down no longer pushes it through the floor.
|
||||
- Removed the obsolete `AttachmentName` config key (leaving it in an existing config file is harmless and ignored).
|
||||
|
||||
### v0.1.1
|
||||
|
||||
- Updated to .NET 10 and CounterStrikeSharp.API 1.0.371
|
||||
- Replaced per-tick `light_omni2` spawn/teleport with parented `light_barn`
|
||||
- Added `IPluginConfig` settings for light and toggle behavior
|
||||
- Added `AllowedTeam` config (`Any` / `CT` / `T`) to restrict flashlight by side
|
||||
- Added focused xUnit tests for pure helpers
|
||||
- Updated GitHub Actions for .NET 10, PR tests, and tag releases
|
||||
- Switched logging to `BasePlugin.Logger`
|
||||
|
||||
### v0.0.6
|
||||
|
||||
- Updated to .NET 8.0 and CounterStrikeSharp.API v1.0.363
|
||||
- Added initial xUnit test project
|
||||
- Improved CSS API compatibility (`V_angle`, entity validity checks)
|
||||
|
||||
### v0.0.5
|
||||
|
||||
- Initial release
|
||||
|
||||
## License
|
||||
|
||||
GNU General Public License. See `LICENSE`.
|
||||
|
||||
## Author
|
||||
|
||||
[creazy.eth](https://github.com/creazy231)
|
||||
This project was created by [creazy.eth](https://github.com/creazy231)
|
||||
Loading…
Add table
Add a link
Reference in a new issue