Compare commits

...

8 Commits

Author SHA1 Message Date
MSWS
6f169ef850 Adjust credit dispersement for good/bad kills and iding bodies 2025-10-14 12:34:58 -07:00
MSWS
6f924a82b0 Fix station spawn positioning (resolves #106) 2025-10-14 11:50:58 -07:00
MSWS
06ae0250d0 Additional tweaks to karma balance 2025-10-14 11:50:03 -07:00
MSWS
bd475edd54 Localize no logs shown msg 2025-10-14 11:43:12 -07:00
MSWS
092a676f97 feat: Implement player muting when dead +semver:minor (resolves #121)
- Introduce `PlayerMuter` class in `GameHandlers` for muting dead players and send appropriate messages
- Add `PlayerMuter` behavior to `CS2ServiceCollection` and organize mod behaviors
- Remove unnecessary debug print and simplify logic in `SilentAWPItem`'s `onWeaponSound` method
- Add reminder message in `en.yml` for dead players indicating they cannot be heard
- Add `DEAD_MUTE_REMINDER` message in `CS2Msgs.cs` to notify muted dead players
2025-10-14 11:41:41 -07:00
MSWS
cebf48a9e6 refactor: Refactor dict to use IDs, fix silent awp (#105)
- Change dictionary key types from `IOnlinePlayer` to `string` in `ListCommand` for consistency, using `executor.Id` as the key.
- Update method calls in `ListCommand` to align with new dictionary key types.
- Update `silentShots` dictionary in `SilentAWPItem` to use player IDs (`string`) instead of `IOnlinePlayer` objects.
- Modify `OnPurchase` method in `SilentAWPItem` to handle weapon management asynchronously.
- Add server logging for debug messages in `SilentAWPItem`.
2025-10-14 11:26:05 -07:00
MSWS
303b6de39c Working MAUL integration 2025-10-14 11:05:11 -07:00
MSWS
9f5e96ce33 Add MAUL compatability 2025-10-14 10:45:23 -07:00
18 changed files with 190 additions and 60 deletions

View File

@@ -15,6 +15,12 @@
<ProjectReference Include="..\ShopAPI\ShopAPI.csproj"/>
</ItemGroup>
<ItemGroup>
<Reference Include="MAULActainShared.dll">
<HintPath>./ThirdParties/Binaries/MAULActainShared.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<Folder Include="RayTrace\"/>
</ItemGroup>

View File

@@ -65,6 +65,7 @@ public static class CS2ServiceCollection {
collection.AddModBehavior<BuyMenuHandler>();
collection.AddModBehavior<TeamChangeHandler>();
collection.AddModBehavior<TraitorChatHandler>();
collection.AddModBehavior<PlayerMuter>();
// Damage Cancelers
collection.AddModBehavior<OutOfRoundCanceler>();

View File

@@ -44,7 +44,7 @@ public class CS2KarmaConfig : IStorage<KarmaConfig>, IPluginModule {
// Karma deltas
public static readonly FakeConVar<int> CV_INNO_ON_TRAITOR = new(
"css_ttt_karma_inno_on_traitor",
"Karma gained when Innocent kills a Traitor", 5, ConVarFlags.FCVAR_NONE,
"Karma gained when Innocent kills a Traitor", 4, ConVarFlags.FCVAR_NONE,
new RangeValidator<int>(-50, 50));
public static readonly FakeConVar<int> CV_TRAITOR_ON_DETECTIVE = new(
@@ -59,19 +59,29 @@ public class CS2KarmaConfig : IStorage<KarmaConfig>, IPluginModule {
public static readonly FakeConVar<int> CV_INNO_ON_INNO = new(
"css_ttt_karma_inno_on_inno",
"Karma lost when Innocent kills another Innocent", -4,
"Karma lost when Innocent kills another Innocent", -5,
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(-50, 50));
public static readonly FakeConVar<int> CV_TRAITOR_ON_TRAITOR = new(
"css_ttt_karma_traitor_on_traitor",
"Karma lost when Traitor kills another Traitor", -5, ConVarFlags.FCVAR_NONE,
"Karma lost when Traitor kills another Traitor", -6, ConVarFlags.FCVAR_NONE,
new RangeValidator<int>(-50, 50));
public static readonly FakeConVar<int> CV_INNO_ON_DETECTIVE = new(
"css_ttt_karma_inno_on_detective",
"Karma lost when Innocent kills a Detective", -6, ConVarFlags.FCVAR_NONE,
"Karma lost when Innocent kills a Detective", -8, ConVarFlags.FCVAR_NONE,
new RangeValidator<int>(-50, 50));
public static readonly FakeConVar<int> CV_KARMA_PER_ROUND = new(
"css_ttt_karma_per_round",
"Amount of karma a player will gain at the end of each round", 2,
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 50));
public static readonly FakeConVar<int> CV_KARMA_PER_ROUND_WIN = new(
"css_ttt_karma_per_round_win",
"Amount of karma a player will gain at the end of each round if their team won",
4, ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 50));
public void Dispose() { }
public void Start() { }
@@ -90,6 +100,8 @@ public class CS2KarmaConfig : IStorage<KarmaConfig>, IPluginModule {
KarmaTimeoutThreshold = CV_TIMEOUT_THRESHOLD.Value,
KarmaRoundTimeout = CV_ROUND_TIMEOUT.Value,
KarmaWarningWindow = TimeSpan.FromHours(CV_WARNING_WINDOW_HOURS.Value),
KarmaPerRound = CV_KARMA_PER_ROUND.Value,
KarmaPerRoundWin = CV_KARMA_PER_ROUND_WIN.Value,
INNO_ON_TRAITOR = CV_INNO_ON_TRAITOR.Value,
TRAITOR_ON_DETECTIVE = CV_TRAITOR_ON_DETECTIVE.Value,
INNO_ON_INNO_VICTIM = CV_INNO_ON_INNO_VICTIM.Value,

View File

@@ -12,11 +12,13 @@ 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) : IPluginModule {
public class BodySpawner(IServiceProvider provider) : BaseListener(provider) {
private readonly IEventBus bus = provider.GetRequiredService<IEventBus>();
private readonly IPlayerConverter<CCSPlayerController> converter =
@@ -25,31 +27,37 @@ public class BodySpawner(IServiceProvider provider) : IPluginModule {
private readonly IGameManager games =
provider.GetRequiredService<IGameManager>();
public void Dispose() { }
public void Start() { }
[UsedImplicitly]
[EventHandler]
public void OnLeave(PlayerLeaveEvent ev) {
if (games.ActiveGame is not { State: State.IN_PROGRESS }) return;
spawnRagdoll(ev.Player);
}
[UsedImplicitly]
[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;
[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;
player.SetColor(Color.FromArgb(0, 255, 255, 255));
var ragdollBody = makeGameRagdoll(player);
var body = new CS2Body(ragdollBody, converter.GetPlayer(player));
if (ev.Attacker != null && ev.Attacker.IsValid)
body.WithKiller(converter.GetPlayer(ev.Attacker));
if (killer != null) body.WithKiller(killer);
body.WithWeapon(new BaseWeapon(ev.Weapon));
body.WithWeapon(new BaseWeapon(weapon ?? "unknown"));
var bodyCreatedEvent = new BodyCreateEvent(body);
bus.Dispatch(bodyCreatedEvent);
if (bodyCreatedEvent.IsCanceled) ragdollBody.AcceptInput("Kill");
return HookResult.Continue;
}
[UsedImplicitly]

View File

@@ -0,0 +1,56 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Attributes.Registration;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using TTT.API;
using TTT.API.Messages;
using TTT.API.Player;
using TTT.CS2.lang;
using TTT.Game.Listeners;
using TTT.Locale;
namespace TTT.CS2.GameHandlers;
public class PlayerMuter(IServiceProvider provider) : IPluginModule {
private readonly IMsgLocalizer locale =
provider.GetRequiredService<IMsgLocalizer>();
private readonly IMessenger messenger =
provider.GetRequiredService<IMessenger>();
private readonly IPlayerConverter<CCSPlayerController> converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
public void Dispose() { }
public void Start() { }
public void Start(BasePlugin? plugin) {
plugin
?.RegisterListener<CounterStrikeSharp.API.Core.Listeners.OnClientVoice>(
onVoice);
}
private void onVoice(int playerSlot) {
var player = Utilities.GetPlayerFromSlot(playerSlot);
if (player == null) return;
if (player.Pawn.Value is { Health: > 0 }) return;
if ((player.VoiceFlags & VoiceFlags.Muted) != VoiceFlags.Muted) {
var apiPlayer = converter.GetPlayer(player);
messenger.Message(apiPlayer, locale[CS2Msgs.DEAD_MUTE_REMINDER]);
}
player.VoiceFlags |= VoiceFlags.Muted;
}
[UsedImplicitly]
[GameEventHandler]
public HookResult OnSpawn(EventPlayerSpawn ev, GameEventInfo _) {
var player = ev.Userid;
if (player == null) return HookResult.Continue;
player.VoiceFlags &= ~VoiceFlags.Muted;
return HookResult.Continue;
}
}

View File

@@ -60,13 +60,15 @@ public class TeamChangeHandler(IServiceProvider provider) : IPluginModule {
[GameEventHandler]
public HookResult OnChangeTeam(EventPlayerTeam ev, GameEventInfo _) {
if (ev.Userid == null) return HookResult.Continue;
if (ev.Userid.LifeState == (int)LifeState_t.LIFE_ALIVE)
var team = (CsTeam)ev.Team;
if (team is not (CsTeam.Spectator or CsTeam.None))
return HookResult.Continue;
var apiPlayer = converter.GetPlayer(ev.Userid);
var playerDeath = new PlayerDeathEvent(apiPlayer);
bus.Dispatch(playerDeath);
Server.NextWorldUpdate(() => {
var playerDeath = new PlayerDeathEvent(apiPlayer);
bus.Dispatch(playerDeath);
});
return HookResult.Continue;
}
}

View File

@@ -1,6 +1,7 @@
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Attributes.Registration;
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Commands;
using MAULActainShared.plugin;
using Microsoft.Extensions.DependencyInjection;
using TTT.API;
using TTT.API.Game;
@@ -8,7 +9,7 @@ using TTT.API.Messages;
using TTT.API.Player;
using TTT.API.Role;
using TTT.CS2.lang;
using TTT.Game.Listeners;
using TTT.CS2.ThirdParties.eGO;
using TTT.Game.Roles;
using TTT.Locale;
@@ -30,8 +31,28 @@ public class TraitorChatHandler(IServiceProvider provider) : IPluginModule {
private readonly IMsgLocalizer locale =
provider.GetRequiredService<IMsgLocalizer>();
private IActain? maulService = null;
public void Start(BasePlugin? plugin) {
plugin?.AddCommandListener("say_team", onSay);
try {
maulService ??= EgoApi.MAUL.Get();
if (maulService != null) {
maulService.getChatShareService().OnChatShare += OnOnChatShare;
return;
}
plugin?.AddCommandListener("say_team", onSay);
} catch (KeyNotFoundException) {
plugin?.AddCommandListener("say_team", onSay);
}
}
private void OnOnChatShare(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;
}
private HookResult onSay(CCSPlayerController? player,
@@ -48,14 +69,18 @@ public class TraitorChatHandler(IServiceProvider provider) : IPluginModule {
if (teammates == null) return HookResult.Continue;
var msg = commandInfo.ArgString;
if (msg.StartsWith('\\') && msg.EndsWith('\\') && msg.Length >= 2)
if (msg.StartsWith('"') && msg.EndsWith('"') && msg.Length >= 2)
msg = msg[1..^1];
var formatted = locale[CS2Msgs.TRAITOR_CHAT_FORMAT(apiPlayer, msg)];
foreach (var mate in teammates) messenger.Message(mate, formatted);
return HookResult.Stop;
return HookResult.Handled;
}
public void Dispose() {
if (maulService != null)
maulService.getChatShareService().OnChatShare -= OnOnChatShare;
}
public void Dispose() { }
public void Start() { }
}

View File

@@ -33,8 +33,8 @@ public class SilentAWPItem(IServiceProvider provider)
private readonly IPlayerConverter<CCSPlayerController> playerConverter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
private readonly IDictionary<IOnlinePlayer, int> silentShots =
new Dictionary<IOnlinePlayer, int>();
private readonly IDictionary<string, int> silentShots =
new Dictionary<string, int>();
public override string Name => Locale[SilentAWPMsgs.SHOP_ITEM_SILENT_AWP];
@@ -49,8 +49,11 @@ public class SilentAWPItem(IServiceProvider provider)
}
public override void OnPurchase(IOnlinePlayer player) {
silentShots[player] = config.CurrentAmmo ?? 0 + config.ReserveAmmo ?? 0;
Inventory.GiveWeapon(player, config);
silentShots[player.Id] = config.CurrentAmmo ?? 0 + config.ReserveAmmo ?? 0;
Task.Run(async () => {
await Inventory.RemoveWeaponInSlot(player, 0);
await Inventory.GiveWeapon(player, config);
});
}
private HookResult onWeaponSound(UserMessage msg) {
@@ -75,12 +78,12 @@ public class SilentAWPItem(IServiceProvider provider)
if (playerConverter.GetPlayer(player) is not IOnlinePlayer apiPlayer)
return HookResult.Continue;
if (!silentShots.TryGetValue(apiPlayer, out var shots) || shots <= 0)
if (!silentShots.TryGetValue(apiPlayer.Id, out var shots) || shots <= 0)
return HookResult.Continue;
silentShots[apiPlayer] = shots - 1;
if (silentShots[apiPlayer] == 0) {
silentShots.Remove(apiPlayer);
silentShots[apiPlayer.Id] = shots - 1;
if (silentShots[apiPlayer.Id] == 0) {
silentShots.Remove(apiPlayer.Id);
Shop.RemoveItem<SilentAWPItem>(apiPlayer);
}

View File

@@ -11,6 +11,7 @@ using TTT.API;
using TTT.API.Player;
using TTT.API.Role;
using TTT.CS2.Extensions;
using TTT.CS2.RayTrace.Class;
namespace TTT.CS2.Items.Station;
@@ -127,14 +128,8 @@ public abstract class StationItem<T>(IServiceProvider provider,
if (gamePlayer == null || !gamePlayer.Pawn.IsValid
|| gamePlayer.Pawn.Value == null)
return;
var spawnPos = gamePlayer.Pawn.Value.AbsOrigin.Clone();
if (spawnPos != null && gamePlayer.PlayerPawn.Value != null) {
var forward = gamePlayer.PlayerPawn.Value.EyeAngles.ToForward();
forward.Z = 0;
spawnPos += forward.Normalized() * 8;
}
prop.Teleport(spawnPos);
prop.Teleport(gamePlayer.GetEyePosition());
});
}

Binary file not shown.

View File

@@ -0,0 +1,9 @@
using CounterStrikeSharp.API.Core.Capabilities;
using MAULActainShared.plugin;
namespace TTT.CS2.ThirdParties.eGO;
public class EgoApi {
public static PluginCapability<IActain> MAUL { get; } =
new("maulactain:core");
}

View File

@@ -18,4 +18,7 @@ public static class CS2Msgs {
public static IMsg TRAITOR_CHAT_FORMAT(IOnlinePlayer player, string msg) {
return MsgFactory.Create(nameof(TRAITOR_CHAT_FORMAT), player.Name, msg);
}
public static IMsg DEAD_MUTE_REMINDER
=> MsgFactory.Create(nameof(DEAD_MUTE_REMINDER));
}

View File

@@ -3,6 +3,8 @@ TRAITOR_CHAT_FORMAT: "{darkred}[TRAITORS] {red}{0}: {default}{1}"
TASER_SCANNED: "%PREFIX%You scanned {0}{grey}, they are %an% {1}{grey}!"
DNA_PREFIX: "{darkblue}D{blue}N{lightblue}A{grey} | {grey}"
DEAD_MUTE_REMINDER: "%PREFIX%You are dead and cannot be heard."
SHOP_ITEM_DNA: "DNA Scanner"
SHOP_ITEM_DNA_DESC: "Scan bodies to reveal the person who killed them."
SHOP_ITEM_DNA_SCANNED: "%DNA_PREFIX%You scanned {0}{1}'%s% {grey}body, their killer was {red}{2}{grey}."

View File

@@ -32,15 +32,16 @@ public class LogsCommand(IServiceProvider provider) : ICommand {
if (games.ActiveGame is not {
State: State.IN_PROGRESS or State.FINISHED
}) {
info.ReplySync("No active game to show logs for.");
messenger.Message(executor, localizer[GameMsgs.GAME_LOGS_NONE]);
return Task.FromResult(CommandResult.ERROR);
}
if (executor is { IsAlive: true })
messenger.MessageAll(localizer[GameMsgs.LOGS_VIEWED_ALIVE(executor)]);
else if (icons != null && executor != null)
if (int.TryParse(executor.Id, out var slot))
icons.SetVisiblePlayers(slot, ulong.MaxValue);
else if (icons != null && executor != null) {
icons.SetVisiblePlayers(executor, ulong.MaxValue);
messenger.Message(executor, localizer[GameMsgs.LOGS_VIEWED_INFO]);
}
games.ActiveGame.Logger.PrintLogs(executor);
return Task.FromResult(CommandResult.SUCCESS);

View File

@@ -26,6 +26,9 @@ public static class GameMsgs {
public static IMsg GAME_LOGS_FOOTER
=> MsgFactory.Create(nameof(GAME_LOGS_FOOTER));
public static IMsg GAME_LOGS_NONE
=> MsgFactory.Create(nameof(GAME_LOGS_NONE));
public static IMsg ROLE_REVEAL_DEATH(IRole killerRole) {
return MsgFactory.Create(nameof(ROLE_REVEAL_DEATH),
@@ -78,6 +81,8 @@ public static class GameMsgs {
}
#endregion
public static IMsg LOGS_VIEWED_INFO => MsgFactory.Create(nameof(LOGS_VIEWED_INFO));
public static IMsg LOGS_VIEWED_ALIVE(IPlayer player) {
return MsgFactory.Create(nameof(LOGS_VIEWED_ALIVE), player.Name);

View File

@@ -22,4 +22,6 @@ NOT_ENOUGH_PLAYERS: "%PREFIX%{red}Game was canceled due to having fewer than {ye
BODY_IDENTIFIED: "%PREFIX%{default}{0}{grey} identified the body of {blue}{1}{grey}, they were %an% {2}{grey}!"
GAME_LOGS_HEADER: "---------- Game Logs ----------"
GAME_LOGS_FOOTER: "-------------------------------"
LOGS_VIEWED_ALIVE: "%PREFIX%{red}{0}{grey} viewed the logs while alive."
GAME_LOGS_NONE: "%PREFIX%There is no game active."
LOGS_VIEWED_ALIVE: "%PREFIX%{red}{0}{grey} viewed the logs while alive."
LOGS_VIEWED_INFO: "%PREFIX%Logs printed to console. All players' roles have been shown."

View File

@@ -10,14 +10,14 @@ using TTT.Locale;
namespace TTT.Shop.Commands;
public class ListCommand(IServiceProvider provider) : ICommand, IItemSorter {
private readonly IDictionary<IOnlinePlayer, List<IShopItem>> cache =
new Dictionary<IOnlinePlayer, List<IShopItem>>();
private readonly IDictionary<string, List<IShopItem>> cache =
new Dictionary<string, List<IShopItem>>();
private readonly IGameManager games = provider
.GetRequiredService<IGameManager>();
private readonly IDictionary<IOnlinePlayer, DateTime> lastUpdate =
new Dictionary<IOnlinePlayer, DateTime>();
private readonly IDictionary<string, DateTime> lastUpdate =
new Dictionary<string, DateTime>();
private readonly IMsgLocalizer locale = provider
.GetRequiredService<IMsgLocalizer>();
@@ -37,7 +37,7 @@ public class ListCommand(IServiceProvider provider) : ICommand, IItemSorter {
ICommandInfo info) {
var items = calculateSortedItems(executor);
if (executor != null) cache[executor] = items;
if (executor != null) cache[executor.Id] = items;
items = new List<IShopItem>(items);
items.Reverse();
@@ -63,14 +63,14 @@ public class ListCommand(IServiceProvider provider) : ICommand, IItemSorter {
public List<IShopItem> GetSortedItems(IOnlinePlayer? player,
bool refresh = false) {
if (player == null) return calculateSortedItems(null);
if (refresh || !cache.ContainsKey(player))
cache[player] = calculateSortedItems(player);
return cache[player];
if (refresh || !cache.ContainsKey(player.Id))
cache[player.Id] = calculateSortedItems(player);
return cache[player.Id];
}
public DateTime? GetLastUpdate(IOnlinePlayer? player) {
if (player == null) return null;
lastUpdate.TryGetValue(player, out var time);
lastUpdate.TryGetValue(player.Id, out var time);
return time;
}
@@ -94,7 +94,7 @@ public class ListCommand(IServiceProvider provider) : ICommand, IItemSorter {
return string.Compare(a.Name, b.Name, StringComparison.Ordinal);
});
if (player != null) lastUpdate[player] = DateTime.Now;
if (player != null) lastUpdate[player.Id] = DateTime.Now;
return items;
}

View File

@@ -21,7 +21,7 @@ public class PlayerKillListener(IServiceProvider provider)
if (ev.Killer == null) return;
Task.Run(async () => {
var victimBal = await shop.Load(ev.Victim);
shop.AddBalance(ev.Killer, victimBal / 6, "Killed " + ev.Victim.Name);
shop.AddBalance(ev.Killer, victimBal / 2, "Killed " + ev.Victim.Name);
});
}
@@ -38,7 +38,7 @@ public class PlayerKillListener(IServiceProvider provider)
if (!isGoodKill(ev.Body.Killer, ev.Body.OfPlayer)) {
var killerBal = await shop.Load(killer);
shop.AddBalance(killer, -killerBal / 4 - victimBal / 2, "Bad Kill");
shop.AddBalance(killer, -killerBal / 3 - victimBal / 2, "Bad Kill");
return;
}