mirror of
https://github.com/MSWS/TTT.git
synced 2025-12-06 14:33:52 -08:00
Compare commits
17 Commits
0.15.0-dev
...
0.17.0-dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83e90deb44 | ||
|
|
658eecef02 | ||
|
|
c90af8dfcf | ||
|
|
6cd1788992 | ||
|
|
1288ccbd7b | ||
|
|
2596289f58 | ||
|
|
d13403d7a7 | ||
|
|
d4fa621a03 | ||
|
|
969c872e48 | ||
|
|
b2f19b7ac3 | ||
|
|
742e407bcf | ||
|
|
f8c8e528e2 | ||
|
|
24bd3d5f40 | ||
|
|
f1ff53893f | ||
|
|
54c89e96c0 | ||
|
|
46514a6016 | ||
|
|
73a8f6a9f5 |
@@ -33,9 +33,6 @@ public interface IGame : IDisposable {
|
||||
|
||||
bool CheckEndConditions();
|
||||
|
||||
[Obsolete("This method is ambiguous, check the game state directly.")]
|
||||
bool IsInProgress() { return State is State.COUNTDOWN or State.IN_PROGRESS; }
|
||||
|
||||
ISet<IOnlinePlayer> GetAlive() {
|
||||
return Players.OfType<IOnlinePlayer>().Where(p => p.IsAlive).ToHashSet();
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ public interface IIconManager {
|
||||
void RevealToAll(int client);
|
||||
void AddVisiblePlayer(int client, int player);
|
||||
void RemoveVisiblePlayer(int client, int player);
|
||||
void SetVisiblePlayers(IOnlinePlayer online, ulong playersBitmask);
|
||||
|
||||
void ClearAllVisibility();
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
using CounterStrikeSharp.API;
|
||||
using Serilog;
|
||||
using TTT.API.Game;
|
||||
using TTT.API.Player;
|
||||
using TTT.Game.Loggers;
|
||||
|
||||
@@ -64,6 +64,7 @@ public static class CS2ServiceCollection {
|
||||
collection.AddModBehavior<MapZoneRemover>();
|
||||
collection.AddModBehavior<BuyMenuHandler>();
|
||||
collection.AddModBehavior<TeamChangeHandler>();
|
||||
collection.AddModBehavior<TraitorChatHandler>();
|
||||
|
||||
// Damage Cancelers
|
||||
collection.AddModBehavior<OutOfRoundCanceler>();
|
||||
|
||||
@@ -44,7 +44,9 @@ public class PlayerPingShopAlias(IServiceProvider provider) : IPluginModule {
|
||||
if (converter.GetPlayer(player) is not IOnlinePlayer gamePlayer) return;
|
||||
|
||||
var lastUpdated = itemSorter.GetLastUpdate(gamePlayer);
|
||||
if (lastUpdated == null || DateTime.Now - lastUpdated > TimeSpan.FromSeconds(20)) return;
|
||||
if (lastUpdated == null
|
||||
|| DateTime.Now - lastUpdated > TimeSpan.FromSeconds(20))
|
||||
return;
|
||||
var cmdInfo = new CS2CommandInfo(provider, gamePlayer, 0, "css_shop", "buy",
|
||||
(index - 1).ToString());
|
||||
cmdInfo.CallingContext = CommandCallingContext.Chat;
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
using TTT.API.Storage;
|
||||
|
||||
namespace TTT.CS2.Configs;
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using CounterStrikeSharp.API;
|
||||
using CounterStrikeSharp.API;
|
||||
using CounterStrikeSharp.API.Core;
|
||||
using CounterStrikeSharp.API.Modules.Cvars;
|
||||
using CounterStrikeSharp.API.Modules.Cvars.Validators;
|
||||
using TTT.API;
|
||||
using Karma;
|
||||
using TTT.API.Storage;
|
||||
using TTT.Karma;
|
||||
|
||||
namespace TTT.CS2.Configs;
|
||||
|
||||
public class CS2KarmaConfig : IStorage<KarmaConfig>, IPluginModule {
|
||||
public static readonly FakeConVar<string> CV_DB_STRING = new(
|
||||
@@ -27,7 +24,7 @@ public class CS2KarmaConfig : IStorage<KarmaConfig>, IPluginModule {
|
||||
public static readonly FakeConVar<string> CV_LOW_KARMA_COMMAND = new(
|
||||
"css_ttt_karma_low_command",
|
||||
"Command executed when a player's karma falls below the minimum (use {0} for player slot)",
|
||||
"karmaban {0} Bad Player!");
|
||||
"css_ban #{0} 2880 Low Karma");
|
||||
|
||||
public static readonly FakeConVar<int> CV_TIMEOUT_THRESHOLD = new(
|
||||
"css_ttt_karma_timeout_threshold",
|
||||
@@ -47,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", 2, ConVarFlags.FCVAR_NONE,
|
||||
"Karma gained when Innocent kills a Traitor", 5, ConVarFlags.FCVAR_NONE,
|
||||
new RangeValidator<int>(-50, 50));
|
||||
|
||||
public static readonly FakeConVar<int> CV_TRAITOR_ON_DETECTIVE = new(
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
using System.Reactive.Linq;
|
||||
using CounterStrikeSharp.API;
|
||||
using CounterStrikeSharp.API.Core;
|
||||
using CounterStrikeSharp.API.Modules.Utils;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using TTT.API.Game;
|
||||
using TTT.API.Player;
|
||||
using TTT.API.Role;
|
||||
using TTT.CS2.Roles;
|
||||
using TTT.CS2.Utils;
|
||||
@@ -30,6 +33,9 @@ public class CS2Game(IServiceProvider provider) : RoundBasedGame(provider) {
|
||||
new TraitorRole(provider), new DetectiveRole(provider)
|
||||
];
|
||||
|
||||
private readonly IPlayerConverter<CCSPlayerController> converter =
|
||||
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
|
||||
|
||||
override protected void StartRound() {
|
||||
Server.NextWorldUpdate(() => {
|
||||
base.StartRound();
|
||||
@@ -70,4 +76,13 @@ public class CS2Game(IServiceProvider provider) : RoundBasedGame(provider) {
|
||||
|
||||
return timer;
|
||||
}
|
||||
|
||||
override protected ISet<IOnlinePlayer> GetParticipants() {
|
||||
var players = Utilities.GetPlayers()
|
||||
.Where(p => p is { Team: CsTeam.Terrorist or CsTeam.CounterTerrorist });
|
||||
|
||||
return players.Select(p => converter.GetPlayer(p))
|
||||
.OfType<IOnlinePlayer>()
|
||||
.ToHashSet();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
using CounterStrikeSharp.API;
|
||||
using CounterStrikeSharp.API.Core;
|
||||
using CounterStrikeSharp.API.Core;
|
||||
using CounterStrikeSharp.API.Modules.Memory;
|
||||
using CounterStrikeSharp.API.Modules.Memory.DynamicFunctions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
@@ -69,6 +69,12 @@ public class RoleIconsHandler(IServiceProvider provider)
|
||||
visibilities[client] &= ~(1UL << player);
|
||||
}
|
||||
|
||||
public void SetVisiblePlayers(IOnlinePlayer online, ulong playersBitmask) {
|
||||
var gamePlayer = players.GetPlayer(online);
|
||||
if (gamePlayer == null || !gamePlayer.IsValid) return;
|
||||
SetVisiblePlayers(gamePlayer.Slot, playersBitmask);
|
||||
}
|
||||
|
||||
public void ClearAllVisibility() {
|
||||
Array.Clear(visibilities, 0, visibilities.Length);
|
||||
}
|
||||
|
||||
@@ -39,4 +39,18 @@ public class RoundStart_GameStartHandler(IServiceProvider provider)
|
||||
game?.Start(config.RoundCfg.CountDownDuration);
|
||||
return HookResult.Continue;
|
||||
}
|
||||
|
||||
[UsedImplicitly]
|
||||
[GameEventHandler]
|
||||
public HookResult OnWarmupEnd(EventWarmupEnd ev, GameEventInfo _1) {
|
||||
if (games.ActiveGame is { State: State.IN_PROGRESS or State.COUNTDOWN })
|
||||
return HookResult.Continue;
|
||||
|
||||
var count = finder.GetOnline().Count;
|
||||
if (count < config.RoundCfg.MinimumPlayers) return HookResult.Continue;
|
||||
|
||||
var game = games.CreateGame();
|
||||
game?.Start(config.RoundCfg.CountDownDuration);
|
||||
return HookResult.Continue;
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,24 @@
|
||||
using CounterStrikeSharp.API;
|
||||
using CounterStrikeSharp.API.Core;
|
||||
using CounterStrikeSharp.API.Core.Attributes.Registration;
|
||||
using CounterStrikeSharp.API.Modules.Commands;
|
||||
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.Game.Events.Player;
|
||||
|
||||
namespace TTT.CS2.GameHandlers;
|
||||
|
||||
public class TeamChangeHandler(IServiceProvider provider) : IPluginModule {
|
||||
private readonly IEventBus bus = provider.GetRequiredService<IEventBus>();
|
||||
|
||||
private readonly IPlayerConverter<CCSPlayerController> converter =
|
||||
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
|
||||
|
||||
private readonly IGameManager games =
|
||||
provider.GetRequiredService<IGameManager>();
|
||||
|
||||
@@ -45,4 +55,18 @@ public class TeamChangeHandler(IServiceProvider provider) : IPluginModule {
|
||||
|
||||
return HookResult.Handled;
|
||||
}
|
||||
|
||||
[UsedImplicitly]
|
||||
[GameEventHandler]
|
||||
public HookResult OnChangeTeam(EventPlayerTeam ev, GameEventInfo _) {
|
||||
if (ev.Userid == null) return HookResult.Continue;
|
||||
if (ev.Userid.LifeState == (int)LifeState_t.LIFE_ALIVE)
|
||||
return HookResult.Continue;
|
||||
|
||||
var apiPlayer = converter.GetPlayer(ev.Userid);
|
||||
|
||||
var playerDeath = new PlayerDeathEvent(apiPlayer);
|
||||
bus.Dispatch(playerDeath);
|
||||
return HookResult.Continue;
|
||||
}
|
||||
}
|
||||
61
TTT/CS2/GameHandlers/TraitorChatHandler.cs
Normal file
61
TTT/CS2/GameHandlers/TraitorChatHandler.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
using CounterStrikeSharp.API.Core;
|
||||
using CounterStrikeSharp.API.Core.Attributes.Registration;
|
||||
using CounterStrikeSharp.API.Modules.Commands;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using TTT.API;
|
||||
using TTT.API.Game;
|
||||
using TTT.API.Messages;
|
||||
using TTT.API.Player;
|
||||
using TTT.API.Role;
|
||||
using TTT.CS2.lang;
|
||||
using TTT.Game.Listeners;
|
||||
using TTT.Game.Roles;
|
||||
using TTT.Locale;
|
||||
|
||||
namespace TTT.CS2.GameHandlers;
|
||||
|
||||
public class TraitorChatHandler(IServiceProvider provider) : IPluginModule {
|
||||
private readonly IGameManager game =
|
||||
provider.GetRequiredService<IGameManager>();
|
||||
|
||||
private readonly IRoleAssigner roles =
|
||||
provider.GetRequiredService<IRoleAssigner>();
|
||||
|
||||
private readonly IPlayerConverter<CCSPlayerController> converter =
|
||||
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
|
||||
|
||||
private readonly IMessenger messenger =
|
||||
provider.GetRequiredService<IMessenger>();
|
||||
|
||||
private readonly IMsgLocalizer locale =
|
||||
provider.GetRequiredService<IMsgLocalizer>();
|
||||
|
||||
public void Start(BasePlugin? plugin) {
|
||||
plugin?.AddCommandListener("say_team", onSay);
|
||||
}
|
||||
|
||||
private HookResult onSay(CCSPlayerController? player,
|
||||
CommandInfo commandInfo) {
|
||||
if (player == null
|
||||
|| game.ActiveGame is not { State: State.IN_PROGRESS or State.FINISHED }
|
||||
|| converter.GetPlayer(player) is not IOnlinePlayer apiPlayer
|
||||
|| !roles.GetRoles(apiPlayer).Any(r => r is TraitorRole))
|
||||
return HookResult.Continue;
|
||||
|
||||
var teammates = game.ActiveGame?.Players.Where(p
|
||||
=> roles.GetRoles(p).Any(r => r is TraitorRole))
|
||||
.ToList();
|
||||
if (teammates == null) return HookResult.Continue;
|
||||
|
||||
var msg = commandInfo.ArgString;
|
||||
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;
|
||||
}
|
||||
|
||||
public void Dispose() { }
|
||||
public void Start() { }
|
||||
}
|
||||
@@ -84,6 +84,7 @@ public class SilentAWPItem(IServiceProvider provider)
|
||||
Shop.RemoveItem<SilentAWPItem>(apiPlayer);
|
||||
}
|
||||
|
||||
msg.Recipients.Clear();
|
||||
return HookResult.Handled;
|
||||
}
|
||||
|
||||
|
||||
@@ -45,7 +45,9 @@ 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))
|
||||
.Where(p => p.LifeState != (int)LifeState_t.LIFE_ALIVE && p is {
|
||||
Team: CsTeam.CounterTerrorist or CsTeam.Terrorist
|
||||
}))
|
||||
player.Respawn();
|
||||
|
||||
foreach (var player in Utilities.GetPlayers())
|
||||
@@ -55,6 +57,15 @@ public class RoundTimerListener(IServiceProvider provider)
|
||||
return;
|
||||
}
|
||||
|
||||
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 {
|
||||
Team: CsTeam.CounterTerrorist or CsTeam.Terrorist
|
||||
}))
|
||||
player.Respawn();
|
||||
});
|
||||
|
||||
if (ev.NewState == State.FINISHED) endTimer?.Dispose();
|
||||
if (ev.NewState != State.IN_PROGRESS) return;
|
||||
var duration = config.RoundCfg.RoundDuration(ev.Game.Players.Count);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using CounterStrikeSharp.API.Core;
|
||||
using CounterStrikeSharp.API.Modules.Admin;
|
||||
using CounterStrikeSharp.API.Modules.Entities;
|
||||
using TTT.API.Player;
|
||||
|
||||
namespace TTT.CS2.Player;
|
||||
@@ -9,12 +8,9 @@ public class CS2PermManager(IPlayerConverter<CCSPlayerController> converter)
|
||||
: IPermissionManager {
|
||||
public bool HasFlags(IPlayer player, params string[] flags) {
|
||||
if (flags.Length == 0) return true;
|
||||
Console.WriteLine("Checking flags for player: " + player.Id);
|
||||
var gamePlayer = converter.GetPlayer(player);
|
||||
if (gamePlayer == null) return false;
|
||||
|
||||
ulong.TryParse(player.Id, out var steamId);
|
||||
return AdminManager.PlayerHasPermissions(new SteamID(steamId), flags);
|
||||
return AdminManager.PlayerHasPermissions(gamePlayer, flags);
|
||||
}
|
||||
|
||||
public bool InGroups(IPlayer player, params string[] groups) {
|
||||
|
||||
@@ -14,4 +14,8 @@ public static class CS2Msgs {
|
||||
return MsgFactory.Create(nameof(TASER_SCANNED),
|
||||
rolePrefix + scannedPlayer.Name, role.Name);
|
||||
}
|
||||
|
||||
public static IMsg TRAITOR_CHAT_FORMAT(IOnlinePlayer player, string msg) {
|
||||
return MsgFactory.Create(nameof(TRAITOR_CHAT_FORMAT), player.Name, msg);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
ROLE_SPECTATOR: "Spectator"
|
||||
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}"
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using TTT.API.Command;
|
||||
using TTT.API.Game;
|
||||
using TTT.API.Messages;
|
||||
using TTT.API.Player;
|
||||
using TTT.Locale;
|
||||
|
||||
namespace TTT.Game.Commands;
|
||||
|
||||
@@ -9,13 +11,22 @@ public class LogsCommand(IServiceProvider provider) : ICommand {
|
||||
private readonly IGameManager games =
|
||||
provider.GetRequiredService<IGameManager>();
|
||||
|
||||
private readonly IIconManager? icons = provider.GetService<IIconManager>();
|
||||
|
||||
private readonly IMsgLocalizer localizer =
|
||||
provider.GetRequiredService<IMsgLocalizer>();
|
||||
|
||||
private readonly IMessenger messenger =
|
||||
provider.GetRequiredService<IMessenger>();
|
||||
|
||||
public void Dispose() { }
|
||||
public string[] RequiredFlags => ["@ttt/admin"];
|
||||
|
||||
public bool MustBeOnMainThread => true;
|
||||
|
||||
public string Id => "logs";
|
||||
public void Start() { }
|
||||
|
||||
// TODO: Restrict and verbalize usage
|
||||
|
||||
public Task<CommandResult>
|
||||
Execute(IOnlinePlayer? executor, ICommandInfo info) {
|
||||
if (games.ActiveGame is not {
|
||||
@@ -25,6 +36,12 @@ public class LogsCommand(IServiceProvider provider) : ICommand {
|
||||
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);
|
||||
|
||||
games.ActiveGame.Logger.PrintLogs(executor);
|
||||
return Task.FromResult(CommandResult.SUCCESS);
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ public class RoleAssigner(IServiceProvider provider) : IRoleAssigner {
|
||||
assignedRoles[player].Add(ev.Role);
|
||||
ev.Role.OnAssign(player);
|
||||
|
||||
onlineMessenger?.BackgroundMsgAll(
|
||||
onlineMessenger?.Debug(
|
||||
$"{player.Name} was assigned the role of {role.Name}.");
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -159,7 +159,7 @@ public class RoundBasedGame(IServiceProvider provider) : IGame {
|
||||
}
|
||||
|
||||
virtual protected void StartRound() {
|
||||
var online = finder.GetOnline();
|
||||
var online = GetParticipants();
|
||||
|
||||
if (online.Count < config.RoundCfg.MinimumPlayers) {
|
||||
Messenger?.MessageAll(
|
||||
@@ -170,6 +170,7 @@ public class RoundBasedGame(IServiceProvider provider) : IGame {
|
||||
|
||||
StartedAt = DateTime.Now;
|
||||
RoleAssigner.AssignRoles(online, Roles);
|
||||
|
||||
players.AddRange(online.Where(p
|
||||
=> RoleAssigner.GetRoles(p)
|
||||
.Any(r => r is TraitorRole or DetectiveRole or InnocentRole)));
|
||||
@@ -182,6 +183,8 @@ public class RoundBasedGame(IServiceProvider provider) : IGame {
|
||||
GameMsgs.GAME_STATE_STARTED(traitors, nonTraitors)]);
|
||||
}
|
||||
|
||||
virtual protected ISet<IOnlinePlayer> GetParticipants() => finder.GetOnline();
|
||||
|
||||
#region classDeps
|
||||
|
||||
protected readonly IEventBus Bus = provider.GetRequiredService<IEventBus>();
|
||||
|
||||
@@ -79,6 +79,10 @@ public static class GameMsgs {
|
||||
|
||||
#endregion
|
||||
|
||||
public static IMsg LOGS_VIEWED_ALIVE(IPlayer player) {
|
||||
return MsgFactory.Create(nameof(LOGS_VIEWED_ALIVE), player.Name);
|
||||
}
|
||||
|
||||
#region GENERIC
|
||||
|
||||
public static IMsg GENERIC_UNKNOWN(string command) {
|
||||
|
||||
@@ -21,4 +21,5 @@ GAME_STATE_ENDED_OTHER: "%PREFIX%{blue}GAME! {default}{0}{grey}."
|
||||
NOT_ENOUGH_PLAYERS: "%PREFIX%{red}Game was canceled due to having fewer than {yellow}{0}{red} player%s%."
|
||||
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: "-------------------------------"
|
||||
GAME_LOGS_FOOTER: "-------------------------------"
|
||||
LOGS_VIEWED_ALIVE: "%PREFIX%{red}{0}{grey} viewed the logs while alive."
|
||||
@@ -6,51 +6,58 @@ public record KarmaConfig {
|
||||
public string DbString { get; init; } = "Data Source=karma.db";
|
||||
|
||||
/// <summary>
|
||||
/// The minimum amount of karma a player can have.
|
||||
/// If a player's karma falls below this value, the CommandUponLowKarma
|
||||
/// will be executed.
|
||||
/// The minimum amount of karma a player can have.
|
||||
/// If a player's karma falls below this value, the CommandUponLowKarma
|
||||
/// will be executed.
|
||||
/// </summary>
|
||||
public int MinKarma { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The default amount of karma a player starts with.
|
||||
/// Once a player falls below MinKarma, their karma will
|
||||
/// also be reset to this value.
|
||||
/// The default amount of karma a player starts with.
|
||||
/// Once a player falls below MinKarma, their karma will
|
||||
/// also be reset to this value.
|
||||
/// </summary>
|
||||
public int DefaultKarma { get; init; } = 50;
|
||||
|
||||
/// <summary>
|
||||
/// The command to execute when a player's karma falls below MinKarma.
|
||||
/// The first argument will be the player's slot.
|
||||
/// The command to execute when a player's karma falls below MinKarma.
|
||||
/// The first argument will be the player's slot.
|
||||
/// </summary>
|
||||
public string CommandUponLowKarma { get; init; } = "karmaban {0} Bad Player!";
|
||||
|
||||
/// <summary>
|
||||
/// The minimum threshold that a player's karma must reach
|
||||
/// before timing them out for KarmaRoundTimeout rounds;
|
||||
/// The minimum threshold that a player's karma must reach
|
||||
/// before timing them out for KarmaRoundTimeout rounds;
|
||||
/// </summary>
|
||||
public int KarmaTimeoutThreshold { get; init; } = 20;
|
||||
|
||||
/// <summary>
|
||||
/// The number of rounds a player will be timed out for
|
||||
/// if their karma falls below KarmaTimeoutThreshold.
|
||||
/// The number of rounds a player will be timed out for
|
||||
/// if their karma falls below KarmaTimeoutThreshold.
|
||||
/// </summary>
|
||||
public int KarmaRoundTimeout { get; init; } = 4;
|
||||
|
||||
/// <summary>
|
||||
/// The time window in which a player will receive a warning
|
||||
/// if their karma falls below KarmaWarningThreshold.
|
||||
/// If the player has already received a warning within this time window,
|
||||
/// no warning will be sent.
|
||||
/// The time window in which a player will receive a warning
|
||||
/// if their karma falls below KarmaWarningThreshold.
|
||||
/// If the player has already received a warning within this time window,
|
||||
/// no warning will be sent.
|
||||
/// </summary>
|
||||
public TimeSpan KarmaWarningWindow { get; init; } = TimeSpan.FromDays(1);
|
||||
|
||||
public int MaxKarma(IPlayer? player) { return 100; }
|
||||
/// <summary>
|
||||
/// Amount of karma a player will gain at the end of each round.
|
||||
/// </summary>
|
||||
public int KarmaPerRound { get; init; } = 3;
|
||||
|
||||
public int INNO_ON_TRAITOR { get; init; } = 2;
|
||||
public int KarmaPerRoundWin { get; init; } = 5;
|
||||
|
||||
public int INNO_ON_TRAITOR { get; init; } = 5;
|
||||
public int TRAITOR_ON_DETECTIVE { get; init; } = 1;
|
||||
public int INNO_ON_INNO_VICTIM { get; init; } = -1;
|
||||
public int INNO_ON_INNO { get; init; } = -4;
|
||||
public int TRAITOR_ON_TRAITOR { get; init; } = -5;
|
||||
public int INNO_ON_DETECTIVE { get; init; } = -6;
|
||||
|
||||
public int MaxKarma(IPlayer? player) { return 100; }
|
||||
}
|
||||
@@ -15,6 +15,12 @@ namespace TTT.Karma;
|
||||
public class KarmaListener(IServiceProvider provider) : BaseListener(provider) {
|
||||
private readonly Dictionary<string, int> badKills = new();
|
||||
|
||||
private readonly KarmaConfig config =
|
||||
provider.GetService<IStorage<KarmaConfig>>()
|
||||
?.Load()
|
||||
.GetAwaiter()
|
||||
.GetResult() ?? new KarmaConfig();
|
||||
|
||||
private readonly IGameManager games =
|
||||
provider.GetRequiredService<IGameManager>();
|
||||
|
||||
@@ -26,11 +32,7 @@ public class KarmaListener(IServiceProvider provider) : BaseListener(provider) {
|
||||
private readonly IRoleAssigner roles =
|
||||
provider.GetRequiredService<IRoleAssigner>();
|
||||
|
||||
private readonly KarmaConfig config =
|
||||
provider.GetService<IStorage<KarmaConfig>>()
|
||||
?.Load()
|
||||
.GetAwaiter()
|
||||
.GetResult() ?? new KarmaConfig();
|
||||
public bool GiveKarmaOnRoundEnd = true;
|
||||
|
||||
[EventHandler]
|
||||
[UsedImplicitly]
|
||||
@@ -45,7 +47,7 @@ public class KarmaListener(IServiceProvider provider) : BaseListener(provider) {
|
||||
var killer = ev.Killer;
|
||||
|
||||
if (killer == null) return;
|
||||
if (victim == killer) return;
|
||||
if (victim.Id == killer.Id) return;
|
||||
|
||||
var victimRole = roles.GetRoles(victim).First();
|
||||
var killerRole = roles.GetRoles(killer).First();
|
||||
@@ -94,6 +96,18 @@ public class KarmaListener(IServiceProvider provider) : BaseListener(provider) {
|
||||
public void OnRoundEnd(GameStateUpdateEvent ev) {
|
||||
if (ev.NewState != State.FINISHED) return;
|
||||
|
||||
var winner = ev.Game.WinningRole;
|
||||
if (GiveKarmaOnRoundEnd)
|
||||
foreach (var player in ev.Game.Players)
|
||||
if (Roles.GetRoles(player).Any(r => r.GetType() == winner?.GetType()))
|
||||
queuedKarmaUpdates[player] =
|
||||
queuedKarmaUpdates.GetValueOrDefault(player, 0)
|
||||
+ config.KarmaPerRoundWin;
|
||||
else
|
||||
queuedKarmaUpdates[player] =
|
||||
queuedKarmaUpdates.GetValueOrDefault(player, 0)
|
||||
+ config.KarmaPerRound;
|
||||
|
||||
foreach (var (player, karmaDelta) in queuedKarmaUpdates)
|
||||
Task.Run(async () => {
|
||||
var newKarma = await karma.Load(player) + karmaDelta;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Data;
|
||||
using System.Diagnostics;
|
||||
using System.Reactive.Concurrency;
|
||||
using System.Reactive.Linq;
|
||||
using System.Reactive.Threading.Tasks;
|
||||
@@ -14,35 +15,34 @@ using TTT.Karma.Events;
|
||||
namespace TTT.Karma;
|
||||
|
||||
public sealed class KarmaStorage(IServiceProvider provider) : IKarmaService {
|
||||
// Toggle immediate writes. If false, every Write triggers a flush
|
||||
private const bool EnableCache = true;
|
||||
private readonly IEventBus _bus = provider.GetRequiredService<IEventBus>();
|
||||
|
||||
private readonly IScheduler _scheduler =
|
||||
provider.GetRequiredService<IScheduler>();
|
||||
|
||||
private readonly IStorage<KarmaConfig>? _configStorage =
|
||||
provider.GetService<IStorage<KarmaConfig>>();
|
||||
|
||||
private readonly SemaphoreSlim _flushGate = new(1, 1);
|
||||
|
||||
// Cache keyed by stable player id to avoid relying on IPlayer equality
|
||||
private readonly ConcurrentDictionary<string, int> _karmaCache = new();
|
||||
|
||||
private readonly IScheduler _scheduler =
|
||||
provider.GetRequiredService<IScheduler>();
|
||||
|
||||
private KarmaConfig _config = new();
|
||||
private IDbConnection? _connection;
|
||||
private IDisposable? _flushSubscription;
|
||||
private readonly SemaphoreSlim _flushGate = new(1, 1);
|
||||
|
||||
// Toggle immediate writes. If false, every Write triggers a flush
|
||||
private const bool EnableCache = true;
|
||||
|
||||
public string Id => nameof(KarmaStorage);
|
||||
public string Version => GitVersionInformation.FullSemVer;
|
||||
|
||||
public void Start() {
|
||||
// Load configuration first
|
||||
if (_configStorage is not null) {
|
||||
if (_configStorage is not null)
|
||||
// Synchronously wait here since IKarmaService.Start is sync
|
||||
_config = _configStorage.Load().GetAwaiter().GetResult()
|
||||
?? new KarmaConfig();
|
||||
}
|
||||
|
||||
// Open a dedicated connection used only by this service
|
||||
_connection = new SqliteConnection(_config.DbString);
|
||||
@@ -61,7 +61,7 @@ public sealed class KarmaStorage(IServiceProvider provider) : IKarmaService {
|
||||
.Subscribe(_ => { }, // no-op on success
|
||||
ex => {
|
||||
// Replace with your logger if available
|
||||
System.Diagnostics.Trace.TraceError($"Karma flush failed: {ex}");
|
||||
Trace.TraceError($"Karma flush failed: {ex}");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ SELECT COALESCE(
|
||||
@DefaultKarma
|
||||
)";
|
||||
var karma = await conn.QuerySingleAsync<int>(sql,
|
||||
new { PlayerId = key, DefaultKarma = _config.DefaultKarma });
|
||||
new { PlayerId = key, _config.DefaultKarma });
|
||||
|
||||
if (EnableCache) _karmaCache[key] = karma;
|
||||
return karma;
|
||||
@@ -97,17 +97,15 @@ SELECT COALESCE(
|
||||
$"Karma must be less than {max} for player {key}.");
|
||||
|
||||
int oldValue;
|
||||
if (!_karmaCache.TryGetValue(key, out oldValue)) {
|
||||
if (!_karmaCache.TryGetValue(key, out oldValue))
|
||||
oldValue = await Load(player);
|
||||
}
|
||||
|
||||
if (oldValue == newValue) return;
|
||||
|
||||
var evt = new KarmaUpdateEvent(player, oldValue, newValue);
|
||||
try { _bus.Dispatch(evt); } catch {
|
||||
// Replace with your logger if available
|
||||
System.Diagnostics.Trace.TraceError(
|
||||
"Exception during KarmaUpdateEvent dispatch.");
|
||||
Trace.TraceError("Exception during KarmaUpdateEvent dispatch.");
|
||||
throw;
|
||||
}
|
||||
|
||||
@@ -118,6 +116,20 @@ SELECT COALESCE(
|
||||
if (!EnableCache) await FlushAsync();
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
try {
|
||||
_flushSubscription?.Dispose();
|
||||
// Best effort final flush
|
||||
if (_connection is { State: ConnectionState.Open })
|
||||
FlushAsync().GetAwaiter().GetResult();
|
||||
} catch (Exception ex) {
|
||||
Trace.TraceError($"Dispose flush failed: {ex}");
|
||||
} finally {
|
||||
_connection?.Dispose();
|
||||
_flushGate.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task FlushAsync() {
|
||||
var conn = EnsureConnection();
|
||||
|
||||
@@ -136,10 +148,9 @@ INSERT INTO PlayerKarma (PlayerId, Karma)
|
||||
VALUES (@PlayerId, @Karma)
|
||||
ON CONFLICT(PlayerId) DO UPDATE SET Karma = excluded.Karma
|
||||
";
|
||||
foreach (var (playerId, karma) in snapshot) {
|
||||
foreach (var (playerId, karma) in snapshot)
|
||||
await conn.ExecuteAsync(upsert,
|
||||
new { PlayerId = playerId, Karma = karma }, tx);
|
||||
}
|
||||
|
||||
tx.Commit();
|
||||
} finally { _flushGate.Release(); }
|
||||
@@ -151,19 +162,4 @@ ON CONFLICT(PlayerId) DO UPDATE SET Karma = excluded.Karma
|
||||
"Storage connection is not initialized.");
|
||||
return _connection;
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
try {
|
||||
_flushSubscription?.Dispose();
|
||||
// Best effort final flush
|
||||
if (_connection is { State: ConnectionState.Open }) {
|
||||
FlushAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
System.Diagnostics.Trace.TraceError($"Dispose flush failed: {ex}");
|
||||
} finally {
|
||||
_connection?.Dispose();
|
||||
_flushGate.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,30 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using CounterStrikeSharp.API.Modules.Utils;
|
||||
using CounterStrikeSharp.API.Modules.Utils;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using ShopAPI;
|
||||
using TTT.API.Command;
|
||||
using TTT.API.Game;
|
||||
using TTT.API.Player;
|
||||
using TTT.API.Role;
|
||||
using TTT.Locale;
|
||||
|
||||
namespace TTT.Shop.Commands;
|
||||
|
||||
public class ListCommand(IServiceProvider provider) : ICommand, IItemSorter {
|
||||
private readonly IGameManager games = provider
|
||||
.GetRequiredService<IGameManager>();
|
||||
|
||||
private readonly IDictionary<IOnlinePlayer, List<IShopItem>> cache =
|
||||
new Dictionary<IOnlinePlayer, List<IShopItem>>();
|
||||
|
||||
private readonly IGameManager games = provider
|
||||
.GetRequiredService<IGameManager>();
|
||||
|
||||
private readonly IDictionary<IOnlinePlayer, DateTime> lastUpdate =
|
||||
new Dictionary<IOnlinePlayer, DateTime>();
|
||||
|
||||
private readonly IMsgLocalizer locale = provider
|
||||
.GetRequiredService<IMsgLocalizer>();
|
||||
|
||||
private readonly IRoleAssigner roles = provider
|
||||
.GetRequiredService<IRoleAssigner>();
|
||||
|
||||
private readonly IShop shop = provider.GetRequiredService<IShop>();
|
||||
|
||||
public void Dispose() { }
|
||||
@@ -43,9 +50,30 @@ public class ListCommand(IServiceProvider provider) : ICommand, IItemSorter {
|
||||
info.ReplySync(formatItem(item, items.Count - index, canPurchase));
|
||||
}
|
||||
|
||||
if (games.ActiveGame is not { State: State.IN_PROGRESS }
|
||||
|| executor == null)
|
||||
return CommandResult.SUCCESS;
|
||||
var role = roles.GetRoles(executor).FirstOrDefault();
|
||||
if (role == null) return CommandResult.SUCCESS;
|
||||
|
||||
info.ReplySync(locale[ShopMsgs.SHOP_LIST_FOOTER(role, balance)]);
|
||||
return CommandResult.SUCCESS;
|
||||
}
|
||||
|
||||
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];
|
||||
}
|
||||
|
||||
public DateTime? GetLastUpdate(IOnlinePlayer? player) {
|
||||
if (player == null) return null;
|
||||
lastUpdate.TryGetValue(player, out var time);
|
||||
return time;
|
||||
}
|
||||
|
||||
private List<IShopItem> calculateSortedItems(IOnlinePlayer? player) {
|
||||
var items = new List<IShopItem>(shop.Items).Where(item
|
||||
=> player == null
|
||||
@@ -75,10 +103,9 @@ public class ListCommand(IServiceProvider provider) : ICommand, IItemSorter {
|
||||
return
|
||||
$" {ChatColors.Grey}- [{ChatColors.DarkRed}{item.Config.Price}{ChatColors.Grey}] {ChatColors.Red}{item.Name}";
|
||||
|
||||
if (index > 9) {
|
||||
if (index > 9)
|
||||
return
|
||||
$" {ChatColors.Default}- [{ChatColors.Yellow}{item.Config.Price}{ChatColors.Default}] {ChatColors.Green}{item.Name}";
|
||||
}
|
||||
|
||||
return
|
||||
$" {ChatColors.Blue}/{index} {ChatColors.Default}| [{ChatColors.Yellow}{item.Config.Price}{ChatColors.Default}] {ChatColors.Green}{item.Name}";
|
||||
@@ -88,18 +115,4 @@ public class ListCommand(IServiceProvider provider) : ICommand, IItemSorter {
|
||||
return
|
||||
$" {formatPrefix(item, index, canBuy)} {ChatColors.Grey} | {item.Description}";
|
||||
}
|
||||
|
||||
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];
|
||||
}
|
||||
|
||||
public DateTime? GetLastUpdate(IOnlinePlayer? player) {
|
||||
if (player == null) return null;
|
||||
lastUpdate.TryGetValue(player, out var time);
|
||||
return time;
|
||||
}
|
||||
}
|
||||
@@ -9,11 +9,11 @@ using TTT.Locale;
|
||||
namespace TTT.Shop.Commands;
|
||||
|
||||
public class ShopCommand(IServiceProvider provider) : ICommand, IItemSorter {
|
||||
private readonly ListCommand listCmd = new(provider);
|
||||
|
||||
private readonly IMsgLocalizer locale = provider
|
||||
.GetRequiredService<IMsgLocalizer>();
|
||||
|
||||
private readonly ListCommand listCmd = new(provider);
|
||||
|
||||
private Dictionary<string, ICommand>? subcommands;
|
||||
|
||||
public void Dispose() { }
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
xmlns:s="clr-namespace:System;assembly=mscorlib"
|
||||
xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml:space="preserve">
|
||||
<s:Boolean
|
||||
x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=items_005Coneshotdeagle/@EntryIndexedValue">True</s:Boolean>
|
||||
x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=items_005Coneshotdeagle/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean
|
||||
x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=lang/@EntryIndexedValue">True</s:Boolean>
|
||||
x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=lang/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean
|
||||
x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=shop/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
||||
x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=shop/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
||||
@@ -1,5 +1,6 @@
|
||||
using CounterStrikeSharp.API.Modules.Utils;
|
||||
using ShopAPI;
|
||||
using TTT.API.Role;
|
||||
using TTT.Locale;
|
||||
|
||||
namespace TTT.Shop;
|
||||
@@ -48,4 +49,8 @@ public static class ShopMsgs {
|
||||
public static IMsg COMMAND_BALANCE(int bal) {
|
||||
return MsgFactory.Create(nameof(COMMAND_BALANCE), bal);
|
||||
}
|
||||
|
||||
public static IMsg SHOP_LIST_FOOTER(IRole role, int bal) {
|
||||
return MsgFactory.Create(nameof(SHOP_LIST_FOOTER), role.Name, bal);
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,7 @@ SHOP_INSUFFICIENT_BALANCE: "%SHOP_PREFIX%You cannot afford {white}{0}{grey}, it
|
||||
SHOP_CANNOT_PURCHASE: "%SHOP_PREFIX%You cannot purchase this item."
|
||||
SHOP_CANNOT_PURCHASE_WITH_REASON: "%SHOP_PREFIX%You cannot purchase this item: {red}{0}{grey}."
|
||||
SHOP_PURCHASED: "%SHOP_PREFIX%You purchased {white}{0}{grey}."
|
||||
SHOP_LIST_FOOTER: "%SHOP_PREFIX%You are %an% {0}{grey}, you have {yellow}{1}{grey} %CREDITS_NAME%%s%."
|
||||
|
||||
CREDITS_NAME: "credit"
|
||||
CREDITS_GIVEN: "%SHOP_PREFIX%{0}{1} %CREDITS_NAME%%s%"
|
||||
|
||||
@@ -4,7 +4,7 @@ namespace ShopAPI.Configs.Traitor;
|
||||
|
||||
public record PoisonShotsConfig : ShopItemConfig {
|
||||
public override int Price { get; init; } = 65;
|
||||
public int TotalShots { get; init; } = 3;
|
||||
public int TotalShots { get; init; } = 5;
|
||||
public Color PoisonColor { get; init; } = Color.FromArgb(128, Color.Purple);
|
||||
public PoisonConfig PoisonConfig { get; init; } = new();
|
||||
}
|
||||
@@ -3,6 +3,6 @@
|
||||
namespace ShopAPI;
|
||||
|
||||
public record HealthshotConfig : ShopItemConfig {
|
||||
public override int Price { get; init; } = 25;
|
||||
public override int Price { get; init; } = 30;
|
||||
public string Weapon { get; init; } = "weapon_healthshot";
|
||||
}
|
||||
@@ -35,7 +35,10 @@ public class KarmaListenerTests {
|
||||
new DetectiveRole(provider)
|
||||
};
|
||||
|
||||
bus.RegisterListener(new KarmaListener(provider));
|
||||
var listener = new KarmaListener(provider);
|
||||
listener.GiveKarmaOnRoundEnd = false;
|
||||
|
||||
bus.RegisterListener(listener);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -59,13 +62,13 @@ public class KarmaListenerTests {
|
||||
|
||||
[Theory]
|
||||
[InlineData(RoleEnum.Innocent, RoleEnum.Innocent, 46, 49)]
|
||||
[InlineData(RoleEnum.Innocent, RoleEnum.Traitor, 52, 50)]
|
||||
[InlineData(RoleEnum.Innocent, RoleEnum.Traitor, 55, 50)]
|
||||
[InlineData(RoleEnum.Innocent, RoleEnum.Detective, 44, 50)]
|
||||
[InlineData(RoleEnum.Traitor, RoleEnum.Innocent, 50, 50)]
|
||||
[InlineData(RoleEnum.Traitor, RoleEnum.Traitor, 45, 50)]
|
||||
[InlineData(RoleEnum.Traitor, RoleEnum.Detective, 51, 50)]
|
||||
[InlineData(RoleEnum.Detective, RoleEnum.Innocent, 46, 49)]
|
||||
[InlineData(RoleEnum.Detective, RoleEnum.Traitor, 52, 50)]
|
||||
[InlineData(RoleEnum.Detective, RoleEnum.Traitor, 55, 50)]
|
||||
[InlineData(RoleEnum.Detective, RoleEnum.Detective, 44, 49)]
|
||||
public async Task OnKill_AffectsKarma(RoleEnum attackerRole,
|
||||
RoleEnum victimRole, int expAttackerKarma, int expVictimKarma) {
|
||||
|
||||
Reference in New Issue
Block a user