Compare commits

...

16 Commits

Author SHA1 Message Date
MSWS
bd8125b7a0 Prevent traitor chat metagming 2025-10-19 15:51:09 -07:00
MSWS
695d34c10c Revert "Refresh AliveSpoofer per map"
This reverts commit 9d3ecbe7fb.
2025-10-19 15:35:00 -07:00
MSWS
9d3ecbe7fb Refresh AliveSpoofer per map 2025-10-18 01:16:51 -07:00
MSWS
85dac3622a Merge branch 'dev' of github.com:MSWS/TTT into dev 2025-10-17 23:20:28 -07:00
MSWS
9e4c29e3f7 Bump taser cost 2025-10-17 23:20:22 -07:00
Isaac
453ba14126 Update TTT/CS2/Items/PoisonShots/PoisonShotsListener.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Isaac <git@msws.xyz>
2025-10-17 22:02:16 -07:00
Isaac
91750a1067 Update TTT/CS2/Items/Station/DamageStation.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Isaac <git@msws.xyz>
2025-10-17 22:00:44 -07:00
Isaac
dd6b8c00fe Update TTT/CS2/Utils/DamageDealingHelper.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Isaac <git@msws.xyz>
2025-10-17 22:00:38 -07:00
MSWS
d9ad08aa27 Improve alive checks 2025-10-17 21:55:36 -07:00
MSWS
35191f23e1 refactor: Refactor data structures for kill tracking +semver:patch
- Change `killedWithStation` data structure to `Dictionary` for enhanced player interaction tracking in `DamageStation.cs`
- Update `PoisonShotsListener.cs` to use `Dictionary` for poison kill tracking and adjust related logic
- Specify priority levels for event handlers in `GlovesListener.cs` to optimize execution order
2025-10-17 21:45:25 -07:00
MSWS
ad29de1bc5 Revert 2025-10-17 21:33:19 -07:00
MSWS
0a0416bff0 Try using native damage dealing method 2025-10-17 20:28:33 -07:00
MSWS
62c96123d1 Remove verbose debug module: 2025-10-17 19:18:51 -07:00
MSWS
274716267f Add null checks to body spawner 2025-10-16 16:24:41 -07:00
MSWS
c20842575b Merge branch 'dev' 2025-10-16 16:01:00 -07:00
Isaac
3dcc3a7de5 Item Rebalancing, Karma Updates, New Compass, Cluster Grenade | Bug Fixes (#125)
This PR implements a comprehensive set of game balancing changes, bug fixes, and new features for a Trouble in Terrorist Town (TTT) game mode in Counter-Strike 2.

Key Changes:

    Shop item pricing rebalance: Adjusted prices across multiple traitor and detective items to improve game economy balance
    New cluster grenade item: Added a new traitor shop item that splits into multiple grenades on detonation
    Compass system refactor: Split the single compass into two separate items (player compass and body compass) with a shared abstract base class
    Karma system improvements: Updated karma calculation values and added proper storage/disposal patterns
    Bug fixes: Fixed damage application, ragdoll spawning, and team change handling issues
2025-10-16 13:38:34 -07:00
15 changed files with 208 additions and 86 deletions

View File

@@ -84,7 +84,6 @@ public static class CS2ServiceCollection {
// Commands
#if DEBUG
collection.AddModBehavior<TestCommand>();
collection.AddModBehavior<DebugMessage>();
#endif
collection.AddScoped<IGameManager, CS2GameManager>();

View File

@@ -0,0 +1,41 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Utils;
using Microsoft.Extensions.DependencyInjection;
using TTT.API.Command;
using TTT.API.Player;
namespace TTT.CS2.Command.Test;
public class SpecCommand(IServiceProvider provider) : ICommand {
public void Dispose() { }
public void Start() { }
public Task<CommandResult>
Execute(IOnlinePlayer? executor, ICommandInfo info) {
var target = executor;
if (info.ArgCount == 2) {
var finder = provider.GetRequiredService<IPlayerFinder>();
var result = finder.GetPlayerByName(info.Args[1]);
if (result == null) {
info.ReplySync($"Player '{info.Args[1]}' not found.");
return Task.FromResult(CommandResult.ERROR);
}
target = result;
} else if (target == null) {
return Task.FromResult(CommandResult.PLAYER_ONLY);
}
var converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
Server.NextWorldUpdate(() => {
var player = converter.GetPlayer(target);
player?.ChangeTeam(CsTeam.Spectator);
info.ReplySync($"{target.Name} has been moved to Spectators.");
});
return Task.FromResult(CommandResult.SUCCESS);
}
}

View File

@@ -26,6 +26,7 @@ public class TestCommand(IServiceProvider provider) : ICommand, IPluginModule {
subCommands.Add("sethealth", new SetHealthCommand());
subCommands.Add("emitsound", new EmitSoundCommand(provider));
subCommands.Add("credits", new CreditsCommand(provider));
subCommands.Add("spec", new SpecCommand(provider));
}
public Task<CommandResult>

View File

@@ -11,7 +11,7 @@ namespace TTT.CS2.Configs;
public class CS2TaserConfig : IStorage<TaserConfig>, IPluginModule {
public static readonly FakeConVar<int> CV_PRICE = new(
"css_ttt_shop_taser_price", "Price of the Taser item", 100,
"css_ttt_shop_taser_price", "Price of the Taser item", 120,
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 10000));
public static readonly FakeConVar<string> CV_WEAPON = new(

View File

@@ -6,18 +6,17 @@ using CounterStrikeSharp.API.Modules.Entities.Constants;
using CounterStrikeSharp.API.Modules.Utils;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using TTT.API;
using TTT.API.Events;
using TTT.API.Game;
using TTT.API.Player;
using TTT.CS2.Extensions;
using TTT.Game.Events.Body;
using TTT.Game.Events.Player;
using TTT.Game.Listeners;
using TTT.Game.Roles;
namespace TTT.CS2.GameHandlers;
public class BodySpawner(IServiceProvider provider) : BaseListener(provider) {
public class BodySpawner(IServiceProvider provider) : IPluginModule {
private readonly IEventBus bus = provider.GetRequiredService<IEventBus>();
private readonly IPlayerConverter<CCSPlayerController> converter =
@@ -26,39 +25,31 @@ public class BodySpawner(IServiceProvider provider) : BaseListener(provider) {
private readonly IGameManager games =
provider.GetRequiredService<IGameManager>();
[UsedImplicitly]
[EventHandler]
public void OnLeave(PlayerLeaveEvent ev) {
if (games.ActiveGame is not { State: State.IN_PROGRESS }) return;
spawnRagdoll(ev.Player);
}
public void Dispose() { }
public void Start() { }
[UsedImplicitly]
[EventHandler]
public void OnDeath(PlayerDeathEvent ev) {
if (games.ActiveGame is not { State: State.IN_PROGRESS }) return;
spawnRagdoll(ev.Player, ev.Killer, ev.Weapon);
}
private void spawnRagdoll(IPlayer apiPlayer, IOnlinePlayer? killer = null,
string? weapon = null) {
var player = converter.GetPlayer(apiPlayer);
if (player == null || !player.IsValid) return;
[GameEventHandler]
public HookResult OnDeath(EventPlayerDeath ev, GameEventInfo _) {
if (games.ActiveGame is not { State: State.IN_PROGRESS })
return HookResult.Continue;
var player = ev.Userid;
if (player == null || !player.IsValid) return HookResult.Continue;
player.SetColor(Color.FromArgb(0, 255, 255, 255));
var ragdollBody = makeGameRagdoll(player);
var body = new CS2Body(ragdollBody, converter.GetPlayer(player));
if (killer != null) body.WithKiller(killer);
if (ev.Attacker != null && ev.Attacker.IsValid)
body.WithKiller(converter.GetPlayer(ev.Attacker));
body.WithWeapon(new BaseWeapon(weapon ?? "unknown"));
body.WithWeapon(new BaseWeapon(ev.Weapon));
var bodyCreatedEvent = new BodyCreateEvent(body);
bus.Dispatch(bodyCreatedEvent);
if (bodyCreatedEvent.IsCanceled) {
ragdollBody.AcceptInput("Kill");
}
if (bodyCreatedEvent.IsCanceled) ragdollBody.AcceptInput("Kill");
return HookResult.Continue;
}
[UsedImplicitly]

View File

@@ -1,25 +0,0 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.UserMessages;
using TTT.API;
namespace TTT.CS2.GameHandlers;
public class DebugMessage : IPluginModule {
public void Dispose() { }
public void Start() { }
public void Start(BasePlugin? plugin) {
for (var i = 0; i < 10000; i++) {
if (i is 325 or 124) continue;
var j = i;
plugin.HookUserMessage(i, msg => debug(msg, j));
}
}
private HookResult debug(UserMessage native, int id) {
Server.PrintToConsole(id + "");
Server.PrintToConsole(native.DebugString);
return HookResult.Continue;
}
}

View File

@@ -1,19 +1,24 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Utils;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using TTT.API.Events;
using TTT.API.Game;
using TTT.API.Player;
using TTT.CS2.Extensions;
using TTT.Game.Events.Game;
using TTT.Game.Events.Player;
using TTT.Game.Listeners;
namespace TTT.CS2.Listeners;
namespace TTT.CS2.GameHandlers;
public class LateSpawnListener(IServiceProvider provider)
: BaseListener(provider) {
private readonly IPlayerConverter<CCSPlayerController> converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
[UsedImplicitly]
[EventHandler]
public void OnJoin(PlayerJoinEvent ev) {
if (Games.ActiveGame is { State: State.IN_PROGRESS }) return;
@@ -24,4 +29,17 @@ public class LateSpawnListener(IServiceProvider provider)
player.Respawn();
});
}
[UsedImplicitly]
[EventHandler]
public void GameState(GameStateUpdateEvent ev) {
if (ev.NewState == State.FINISHED) return;
Server.NextWorldUpdate(() => {
foreach (var player in Utilities.GetPlayers()
.Where(p => p.GetHealth() <= 0 && p.Team != CsTeam.Spectator
&& p.Team != CsTeam.None))
player.Respawn();
});
}
}

View File

@@ -9,6 +9,7 @@ using TTT.API;
using TTT.API.Events;
using TTT.API.Game;
using TTT.API.Player;
using TTT.CS2.Extensions;
using TTT.Game.Events.Player;
namespace TTT.CS2.GameHandlers;
@@ -44,7 +45,7 @@ public class TeamChangeHandler(IServiceProvider provider) : IPluginModule {
};
if (games.ActiveGame is not { State: State.IN_PROGRESS }) {
if (player != null && player.LifeState != (int)LifeState_t.LIFE_ALIVE)
if (player != null && player.GetHealth() <= 0)
Server.NextWorldUpdate(player.Respawn);
return HookResult.Continue;
}
@@ -56,19 +57,19 @@ public class TeamChangeHandler(IServiceProvider provider) : IPluginModule {
return HookResult.Handled;
}
// [UsedImplicitly]
// [GameEventHandler]
// public HookResult OnChangeTeam(EventPlayerTeam ev, GameEventInfo _) {
// if (ev.Userid == null) return HookResult.Continue;
// var team = (CsTeam)ev.Team;
// if (team is not (CsTeam.Spectator or CsTeam.None))
// return HookResult.Continue;
// var apiPlayer = converter.GetPlayer(ev.Userid);
//
// Server.NextWorldUpdate(() => {
// var playerDeath = new PlayerDeathEvent(apiPlayer);
// bus.Dispatch(playerDeath);
// });
// return HookResult.Continue;
// }
[UsedImplicitly]
[GameEventHandler]
public HookResult OnChangeTeam(EventPlayerTeam ev, GameEventInfo _) {
if (ev.Userid == null) return HookResult.Continue;
var team = (CsTeam)ev.Team;
if (team is not (CsTeam.Spectator or CsTeam.None))
return HookResult.Continue;
var apiPlayer = converter.GetPlayer(ev.Userid);
Server.NextWorldUpdate(() => {
var playerDeath = new PlayerDeathEvent(apiPlayer);
bus.Dispatch(playerDeath);
});
return HookResult.Continue;
}
}

View File

@@ -36,7 +36,7 @@ public class TraitorChatHandler(IServiceProvider provider) : IPluginModule {
try {
maulService ??= EgoApi.MAUL.Get();
if (maulService != null) {
maulService.getChatShareService().OnChatShare += OnOnChatShare;
maulService.getChatShareService().OnChatShare += OnChatShare;
return;
}
@@ -48,17 +48,19 @@ public class TraitorChatHandler(IServiceProvider provider) : IPluginModule {
public void Dispose() {
if (maulService != null)
maulService.getChatShareService().OnChatShare -= OnOnChatShare;
maulService.getChatShareService().OnChatShare -= OnChatShare;
}
public void Start() { }
private void OnOnChatShare(CCSPlayerController? player, CommandInfo info,
private void OnChatShare(CCSPlayerController? player, CommandInfo info,
ref bool canceled) {
if (!info.GetArg(0).Equals("say_team", StringComparison.OrdinalIgnoreCase))
return;
var result = onSay(player, info);
if (result == HookResult.Handled) canceled = true;
var result = onSay(player, info);
canceled = true;
if (result == HookResult.Handled) return;
player?.ExecuteClientCommandFromServer("say " + info.ArgString);
}
private HookResult onSay(CCSPlayerController? player,

View File

@@ -42,7 +42,7 @@ public class PoisonShotsListener(IServiceProvider provider)
private readonly IShop shop = provider.GetRequiredService<IShop>();
private readonly ISet<string> killedWithPoison = new HashSet<string>();
private readonly Dictionary<string, IPlayer> killedWithPoison = new();
public override void Dispose() {
base.Dispose();
@@ -118,7 +118,7 @@ public class PoisonShotsListener(IServiceProvider provider)
if (dmgEvent.IsCanceled) return true;
if (online.Health - config.PoisonConfig.DamagePerTick <= 0) {
killedWithPoison.Add(online.Id);
killedWithPoison[online.Id] = effect.Shooter;
var deathEvent = new PlayerDeathEvent(online)
.WithKiller(effect.Shooter as IOnlinePlayer)
.WithWeapon($"[{Locale[PoisonShotMsgs.SHOP_ITEM_POISON_SHOTS]}]");
@@ -167,8 +167,10 @@ public class PoisonShotsListener(IServiceProvider provider)
[UsedImplicitly]
[EventHandler]
public void OnRagdollSpawn(BodyCreateEvent ev) {
if (!killedWithPoison.Contains(ev.Body.OfPlayer.Id)) return;
if (ev.Body.Killer == null || ev.Body.Killer.Id == ev.Body.OfPlayer.Id)
ev.IsCanceled = true;
if (!killedWithPoison.TryGetValue(ev.Body.OfPlayer.Id, out var shooter))
return;
if (ev.Body.Killer != null && ev.Body.Killer.Id != ev.Body.OfPlayer.Id)
return;
ev.Body.Killer = shooter as IOnlinePlayer;
}
}

View File

@@ -10,6 +10,7 @@ using TTT.API.Player;
using TTT.API.Role;
using TTT.API.Storage;
using TTT.CS2.Extensions;
using TTT.CS2.Utils;
using TTT.Game.Events.Body;
using TTT.Game.Events.Game;
using TTT.Game.Events.Player;
@@ -45,7 +46,8 @@ public class DamageStation(IServiceProvider provider)
public override string Description
=> Locale[StationMsgs.SHOP_ITEM_STATION_HURT_DESC];
private readonly ISet<string> killedWithStation = new HashSet<string>();
private Dictionary<string, StationInfo> killedWithStation =
new Dictionary<string, StationInfo>();
override protected void onInterval() {
var players = finder.GetOnline();
@@ -87,7 +89,7 @@ public class DamageStation(IServiceProvider provider)
damageAmount = -dmgEvent.DmgDealt;
if (player.Health + damageAmount <= 0) {
killedWithStation.Add(player.Id);
killedWithStation[player.Id] = info;
var playerDeath = new PlayerDeathEvent(player)
.WithKiller(info.Owner as IOnlinePlayer)
.WithWeapon($"[{Name}]");
@@ -115,8 +117,11 @@ public class DamageStation(IServiceProvider provider)
[UsedImplicitly]
[EventHandler]
public void OnRagdollSpawn(BodyCreateEvent ev) {
if (!killedWithStation.Contains(ev.Body.OfPlayer.Id)) return;
if (ev.Body.Killer == null || ev.Body.Killer.Id == ev.Body.OfPlayer.Id)
ev.IsCanceled = true;
if (!killedWithStation.TryGetValue(ev.Body.OfPlayer.Id,
out var stationInfo))
return;
if (ev.Body.Killer != null && ev.Body.Killer.Id != ev.Body.OfPlayer.Id)
return;
ev.Body.Killer = stationInfo.Owner as IOnlinePlayer;
}
}

View File

@@ -45,7 +45,7 @@ public class RoundTimerListener(IServiceProvider provider)
.TotalSeconds);
Server.ExecuteCommand("mp_ignore_round_win_conditions 1");
foreach (var player in Utilities.GetPlayers()
.Where(p => p.LifeState != (int)LifeState_t.LIFE_ALIVE && p is {
.Where(p => p.GetHealth() <= 0 && p is {
Team: CsTeam.CounterTerrorist or CsTeam.Terrorist
}))
player.Respawn();
@@ -60,7 +60,7 @@ public class RoundTimerListener(IServiceProvider provider)
if (ev.NewState == State.IN_PROGRESS)
Server.NextWorldUpdate(() => {
foreach (var player in Utilities.GetPlayers()
.Where(p => p.LifeState != (int)LifeState_t.LIFE_ALIVE && p is {
.Where(p => p.GetHealth() <= 0 && p is {
Team: CsTeam.CounterTerrorist or CsTeam.Terrorist
}))
player.Respawn();

View File

@@ -0,0 +1,87 @@
using System.Net;
using System.Numerics;
using System.Runtime.InteropServices;
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Memory;
using Vector = CounterStrikeSharp.API.Modules.Utils.Vector;
namespace TTT.CS2.Utils;
public class DamageDealingHelper {
public static void DealDamage(CCSPlayerController target,
CCSPlayerController? attacker, int damage, string source,
DamageTypes_t type = DamageTypes_t.DMG_BLAST_SURFACE) {
if (target.Pawn.Value == null) return;
var infoSize = Schema.GetClassSize("CTakeDamageInfo");
var infoPtr = Marshal.AllocHGlobal(infoSize);
for (var i = 0; i < infoSize; i++) Marshal.WriteByte(infoPtr, i, 0);
var damageInfo = new CTakeDamageInfo(infoPtr);
Schema.SetSchemaValue(damageInfo.Handle, "CTakeDamageInfo", "m_hInflictor",
attacker != null ? attacker.Pawn.Raw : 0);
Schema.SetSchemaValue(damageInfo.Handle, "CTakeDamageInfo", "m_hAttacker",
attacker != null ? attacker.EntityHandle.Raw : 0);
damageInfo.Damage = damage;
damageInfo.BitsDamageType = type;
if (target.Pawn.Value?.AbsOrigin != null)
Schema.SetSchemaValue(damageInfo.Handle, "CTakeDamageInfo",
"m_vecDamagePosition",
target.Pawn.Value != null ?
target.Pawn.Value.AbsOrigin.Handle :
Vector.Zero.Handle);
Schema.SetSchemaValue(damageInfo.Handle, "CTakeDamageInfo",
"m_vecDamageForce", Vector.Zero.Handle);
var damageResultSize = Schema.GetClassSize("CTakeDamageResult");
var damageResultPtr = Marshal.AllocHGlobal(damageResultSize);
for (var i = 0; i < damageResultSize; i++)
Marshal.WriteByte(damageResultPtr, i, 0);
var damageResult = new CTakeDamageResult(damageResultPtr);
Schema.SetSchemaValue(damageResult.Handle, "CTakeDamageResult",
"m_pOriginatingInfo", damageInfo.Handle);
damageResult.HealthLost = damage;
damageResult.DamageDealt = damage;
damageResult.TotalledHealthLost = damage;
damageResult.TotalledDamageDealt = damage;
damageResult.WasDamageSuppressed = false;
if (target.EntityHandle.Value != null)
VirtualFunctions.CBaseEntity_TakeDamageOldFunc.Invoke(
target.EntityHandle.Value, damageInfo, damageResult);
Marshal.FreeHGlobal(infoPtr);
Marshal.FreeHGlobal(damageResultPtr);
}
}
[StructLayout(LayoutKind.Explicit)]
public struct CAttackerInfo {
[FieldOffset(0x0)]
public bool NeedInit;
[FieldOffset(0x1)]
public bool IsPawn;
[FieldOffset(0x2)]
public bool IsWorld;
[FieldOffset(0x4)]
public UInt32 AttackerPawn;
[FieldOffset(0x8)]
public ushort AttackerUserId;
[FieldOffset(0x0C)]
public int TeamChecked;
[FieldOffset(0x10)]
public int TeamNum;
}

View File

@@ -24,7 +24,7 @@ public class GlovesListener(IServiceProvider provider)
private readonly Dictionary<IPlayer, int> uses = new();
[UsedImplicitly]
[EventHandler]
[EventHandler(Priority = Priority.LOW)]
public void BodyCreate(BodyCreateEvent ev) {
if (ev.Body.Killer == null || !useGloves(ev.Body.Killer)) return;
if (ev.Body.Killer is not IOnlinePlayer online) return;