mirror of
https://github.com/MSWS/TTT.git
synced 2025-12-06 14:33:52 -08:00
Compare commits
41 Commits
0.15.0-dev
...
0.16.0-dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
969c872e48 | ||
|
|
b2f19b7ac3 | ||
|
|
742e407bcf | ||
|
|
f8c8e528e2 | ||
|
|
24bd3d5f40 | ||
|
|
f1ff53893f | ||
|
|
54c89e96c0 | ||
|
|
46514a6016 | ||
|
|
73a8f6a9f5 | ||
|
|
f0c239f08e | ||
|
|
bafad884d9 | ||
|
|
1d7e2f7466 | ||
|
|
f9e3d2d324 | ||
|
|
9ce90cccaa | ||
|
|
551f6a09ef | ||
|
|
41db8f9444 | ||
|
|
361bbb0a49 | ||
|
|
228ea40cec | ||
|
|
44f7283145 | ||
|
|
1a52daad7c | ||
|
|
7d0d32998e | ||
|
|
3cda83932e | ||
|
|
7ea57d0a9b | ||
|
|
839be785f0 | ||
|
|
8f0a273f79 | ||
|
|
cb6cb442b1 | ||
|
|
cb2a5a8720 | ||
|
|
10be465d33 | ||
|
|
a0720376d4 | ||
|
|
f5cb87d92c | ||
|
|
bd6c15aca7 | ||
|
|
7e5e34c500 | ||
|
|
8a886a158c | ||
|
|
fc61682669 | ||
|
|
d6e4655674 | ||
|
|
c53a584113 | ||
|
|
c56387d6e4 | ||
|
|
1c8d1a5dd5 | ||
|
|
340dae1b16 | ||
|
|
eff58ab2f1 | ||
|
|
acababeaf5 |
@@ -4,12 +4,14 @@
|
||||
| CounterStrikeSharp.API | 1.0.340 | Expression | GPL-3.0-only | https://licenses.nuget.org/GPL-3.0-only | | Roflmuffin | http://docs.cssharp.dev/ |
|
||||
| Dapper | 2.1.66 | Expression | Apache-2.0 | https://licenses.nuget.org/Apache-2.0 | 2019 Stack Exchange, Inc. | Sam Saffron,Marc Gravell,Nick Craver | https://github.com/DapperLib/Dapper |
|
||||
| JetBrains.Annotations | 2025.2.0 | Expression | MIT | https://licenses.nuget.org/MIT | Copyright (c) 2016-2025 JetBrains s.r.o. | JetBrains | https://www.jetbrains.com/help/resharper/Code_Analysis__Code_Annotations.html |
|
||||
| Microsoft.Data.Sqlite | 9.0.9 | Expression | MIT | https://licenses.nuget.org/MIT | © Microsoft Corporation. All rights reserved. | Microsoft | https://docs.microsoft.com/dotnet/standard/data/sqlite/ |
|
||||
| Microsoft.Extensions.DependencyInjection.Abstractions | 9.0.7 | Expression | MIT | https://licenses.nuget.org/MIT | © Microsoft Corporation. All rights reserved. | Microsoft | https://dot.net/ |
|
||||
| Microsoft.Extensions.Localization.Abstractions | 8.0.3 | Expression | MIT | https://licenses.nuget.org/MIT | © Microsoft Corporation. All rights reserved. | Microsoft | https://asp.net/ |
|
||||
| Microsoft.NET.Test.Sdk | 17.14.1 | Expression | MIT | https://licenses.nuget.org/MIT | © Microsoft Corporation. All rights reserved. | Microsoft | https://github.com/microsoft/vstest |
|
||||
| Microsoft.Reactive.Testing | 6.0.1 | Expression | MIT | https://licenses.nuget.org/MIT | Copyright (c) .NET Foundation and Contributors. | .NET Foundation and Contributors | https://github.com/dotnet/reactive |
|
||||
| Microsoft.Testing.Extensions.CodeCoverage | 17.14.2 | Unknown | | https://aka.ms/deprecateLicenseUrl | © Microsoft Corporation. All rights reserved. | Microsoft | https://github.com/microsoft/codecoverage |
|
||||
| MySqlConnector | 2.4.0 | Expression | MIT | https://licenses.nuget.org/MIT | Copyright 2016–2024 Bradley Grainger | Bradley Grainger | https://mysqlconnector.net/ |
|
||||
| SQLite | 3.13.0 | Unknown | | | Public Domain | SQLite Development Team | |
|
||||
| System.Reactive | 6.0.1 | Expression | MIT | https://licenses.nuget.org/MIT | Copyright (c) .NET Foundation and Contributors. | .NET Foundation and Contributors | https://github.com/dotnet/reactive |
|
||||
| System.Text.Json | 8.0.5 | Expression | MIT | https://licenses.nuget.org/MIT | © Microsoft Corporation. All rights reserved. | Microsoft | https://dot.net/ |
|
||||
| Xunit.DependencyInjection | 10.6.0 | Expression | MIT | https://licenses.nuget.org/MIT | Copyright © 2019 | Wei Peng | https://github.com/pengweiqhca/Xunit.DependencyInjection/tree/main/src/Xunit.DependencyInjection |
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
namespace TTT.API.Events;
|
||||
|
||||
public abstract class Event {
|
||||
public abstract string Id { get; }
|
||||
public class Event {
|
||||
public virtual string Id => GetType().Name.ToLowerInvariant();
|
||||
}
|
||||
@@ -5,5 +5,5 @@ public interface IEventBus {
|
||||
|
||||
void UnregisterListener(IListener listener);
|
||||
|
||||
Task Dispatch(Event ev);
|
||||
void Dispatch(Event ev);
|
||||
}
|
||||
@@ -24,7 +24,7 @@ public static class ServiceCollectionExtensions {
|
||||
collection.AddTransient<ICommand>(provider
|
||||
=> (provider.GetRequiredService<TExtension>() as ICommand)!);
|
||||
|
||||
collection.AddScoped<TExtension>();
|
||||
collection.AddSingleton<TExtension>();
|
||||
|
||||
collection.AddTransient<ITerrorModule, TExtension>(provider
|
||||
=> provider.GetRequiredService<TExtension>());
|
||||
|
||||
@@ -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,4 +1,6 @@
|
||||
using CounterStrikeSharp.API;
|
||||
using Serilog;
|
||||
using TTT.API.Game;
|
||||
using TTT.API.Player;
|
||||
using TTT.Game.Loggers;
|
||||
|
||||
@@ -12,4 +14,8 @@ public class CS2Logger(IServiceProvider provider) : SimpleLogger(provider) {
|
||||
public override void PrintLogs(IOnlinePlayer? player) {
|
||||
Server.NextWorldUpdate(() => base.PrintLogs(player));
|
||||
}
|
||||
|
||||
public override void LogAction(IAction action) {
|
||||
Server.NextWorldUpdate(() => base.LogAction(action));
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,7 @@ public static class CS2ServiceCollection {
|
||||
collection.AddModBehavior<IAliveSpoofer, CS2AliveSpoofer>();
|
||||
collection.AddModBehavior<IIconManager, RoleIconsHandler>();
|
||||
collection.AddModBehavior<NameDisplayer>();
|
||||
collection.AddModBehavior<PlayerPingShopAlias>();
|
||||
|
||||
// Configs
|
||||
collection.AddModBehavior<IStorage<TTTConfig>, CS2GameConfig>();
|
||||
|
||||
@@ -4,6 +4,7 @@ using CounterStrikeSharp.API.Modules.Commands;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using TTT.API;
|
||||
using TTT.API.Command;
|
||||
using TTT.API.Messages;
|
||||
using TTT.API.Player;
|
||||
using TTT.Game;
|
||||
using TTT.Game.Commands;
|
||||
@@ -17,6 +18,9 @@ public class CS2CommandManager(IServiceProvider provider)
|
||||
private readonly IPlayerConverter<CCSPlayerController> converter =
|
||||
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
|
||||
|
||||
private readonly IMessenger messenger = provider
|
||||
.GetRequiredService<IMessenger>();
|
||||
|
||||
private BasePlugin? plugin;
|
||||
|
||||
public void Start(BasePlugin? basePlugin, bool hotReload) {
|
||||
@@ -41,7 +45,9 @@ public class CS2CommandManager(IServiceProvider provider)
|
||||
null :
|
||||
converter.GetPlayer(executor) as IOnlinePlayer;
|
||||
|
||||
if (cmdMap.TryGetValue(info.GetArg(0), out var command))
|
||||
messenger.Debug($"Received command: {cs2Info.Args[0]} from {wrapper?.Id}");
|
||||
|
||||
if (cmdMap.TryGetValue(cs2Info.Args[0], out var command))
|
||||
if (command.MustBeOnMainThread) {
|
||||
processCommandSync(cs2Info, wrapper);
|
||||
return;
|
||||
|
||||
53
TTT/CS2/Command/PlayerPingShopAlias.cs
Normal file
53
TTT/CS2/Command/PlayerPingShopAlias.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
using CounterStrikeSharp.API.Core;
|
||||
using CounterStrikeSharp.API.Modules.Commands;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using ShopAPI;
|
||||
using TTT.API;
|
||||
using TTT.API.Command;
|
||||
using TTT.API.Player;
|
||||
|
||||
namespace TTT.CS2.Command;
|
||||
|
||||
public class PlayerPingShopAlias(IServiceProvider provider) : IPluginModule {
|
||||
private readonly IPlayerConverter<CCSPlayerController> converter =
|
||||
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
|
||||
|
||||
private readonly IItemSorter itemSorter =
|
||||
provider.GetRequiredService<IItemSorter>();
|
||||
|
||||
public void Dispose() { }
|
||||
public void Start() { }
|
||||
|
||||
public void Start(BasePlugin? plugin) {
|
||||
plugin?.AddCommandListener("player_ping", onPlayerPing, HookMode.Post);
|
||||
|
||||
for (var i = 0; i < 10; i++) {
|
||||
var index = i; // Capture variable
|
||||
plugin?.AddCommand($"css_{index}", "",
|
||||
(player, _) => { onButton(player, index); });
|
||||
}
|
||||
}
|
||||
|
||||
private HookResult onPlayerPing(CCSPlayerController? player,
|
||||
CommandInfo commandInfo) {
|
||||
if (player == null) return HookResult.Continue;
|
||||
var gamePlayer = converter.GetPlayer(player) as IOnlinePlayer;
|
||||
var cmdInfo =
|
||||
new CS2CommandInfo(provider, gamePlayer, 0, "css_shop", "list");
|
||||
cmdInfo.CallingContext = CommandCallingContext.Chat;
|
||||
provider.GetRequiredService<ICommandManager>().ProcessCommand(cmdInfo);
|
||||
return HookResult.Continue;
|
||||
}
|
||||
|
||||
private void onButton(CCSPlayerController? player, int index) {
|
||||
if (player == null) return;
|
||||
if (converter.GetPlayer(player) is not IOnlinePlayer gamePlayer) return;
|
||||
|
||||
var lastUpdated = itemSorter.GetLastUpdate(gamePlayer);
|
||||
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;
|
||||
provider.GetRequiredService<ICommandManager>().ProcessCommand(cmdInfo);
|
||||
}
|
||||
}
|
||||
21
TTT/CS2/Command/Test/CreditsCommand.cs
Normal file
21
TTT/CS2/Command/Test/CreditsCommand.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using ShopAPI;
|
||||
using TTT.API.Command;
|
||||
using TTT.API.Player;
|
||||
|
||||
namespace TTT.CS2.Command.Test;
|
||||
|
||||
public class CreditsCommand(IServiceProvider provider) : ICommand {
|
||||
private readonly IShop shop = provider.GetRequiredService<IShop>();
|
||||
public void Dispose() { }
|
||||
public void Start() { }
|
||||
public string Id => "credits";
|
||||
|
||||
public Task<CommandResult>
|
||||
Execute(IOnlinePlayer? executor, ICommandInfo info) {
|
||||
if (executor == null) return Task.FromResult(CommandResult.PLAYER_ONLY);
|
||||
shop.AddBalance(executor, 1000);
|
||||
info.ReplySync("You have been given 1000 credits!");
|
||||
return Task.FromResult(CommandResult.SUCCESS);
|
||||
}
|
||||
}
|
||||
@@ -7,11 +7,11 @@ using TTT.API.Player;
|
||||
namespace TTT.CS2.Command.Test;
|
||||
|
||||
public class EmitSoundCommand(IServiceProvider provider) : ICommand {
|
||||
public string Id => "emitsound";
|
||||
|
||||
private readonly IPlayerConverter<CCSPlayerController> converter =
|
||||
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
|
||||
|
||||
public string Id => "emitsound";
|
||||
|
||||
public void Dispose() { }
|
||||
public void Start() { }
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
using CounterStrikeSharp.API;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using ShopAPI;
|
||||
using ShopAPI.Events;
|
||||
using TTT.API.Command;
|
||||
using TTT.API.Events;
|
||||
using TTT.API.Player;
|
||||
|
||||
namespace TTT.CS2.Command.Test;
|
||||
@@ -44,6 +46,10 @@ public class GiveItemCommand(IServiceProvider provider) : ICommand {
|
||||
target = result;
|
||||
}
|
||||
|
||||
var purchaseEv = new PlayerPurchaseItemEvent(target, item);
|
||||
provider.GetRequiredService<IEventBus>().Dispatch(purchaseEv);
|
||||
if (purchaseEv.IsCanceled) return;
|
||||
|
||||
shop.GiveItem(target, item);
|
||||
info.ReplySync($"Gave item '{item.Name}' to {target.Name}.");
|
||||
});
|
||||
|
||||
@@ -16,7 +16,8 @@ public class IndexCommand : ICommand {
|
||||
|
||||
Server.NextWorldUpdate(() => {
|
||||
foreach (var player in Utilities.GetPlayers())
|
||||
info.ReplySync($"{player.PlayerName} - {player.Slot}");
|
||||
info.ReplySync(
|
||||
$"{player.PlayerName} - {player.Slot} {player.Index} {player.DraftIndex} {player.PawnCharacterDefIndex}");
|
||||
});
|
||||
|
||||
return Task.FromResult(CommandResult.SUCCESS);
|
||||
|
||||
@@ -25,6 +25,7 @@ public class TestCommand(IServiceProvider provider) : ICommand, IPluginModule {
|
||||
subCommands.Add("showicons", new ShowIconsCommand(provider));
|
||||
subCommands.Add("sethealth", new SetHealthCommand());
|
||||
subCommands.Add("emitsound", new EmitSoundCommand(provider));
|
||||
subCommands.Add("credits", new CreditsCommand(provider));
|
||||
}
|
||||
|
||||
public Task<CommandResult>
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace TTT.CS2.Configs;
|
||||
|
||||
public class CS2GameConfig : IStorage<TTTConfig>, IPluginModule {
|
||||
public static readonly FakeConVar<int> CV_ROUND_COUNTDOWN = new(
|
||||
"css_ttt_round_countdown", "Time to wait before starting a round", 10,
|
||||
"css_ttt_round_countdown", "Time to wait before starting a round", 15,
|
||||
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(1, 60));
|
||||
|
||||
public static readonly FakeConVar<int> CV_MINIMUM_PLAYERS = new(
|
||||
@@ -80,7 +80,7 @@ public class CS2GameConfig : IStorage<TTTConfig>, IPluginModule {
|
||||
new ItemValidator(allowMultiple: true));
|
||||
|
||||
public static readonly FakeConVar<int> CV_TIME_BETWEEN_ROUNDS = new(
|
||||
"css_ttt_time_between_rounds", "Time to wait between rounds in seconds", 5,
|
||||
"css_ttt_time_between_rounds", "Time to wait between rounds in seconds", 1,
|
||||
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(1, 60));
|
||||
|
||||
public void Dispose() { }
|
||||
|
||||
@@ -1,40 +1,80 @@
|
||||
using CounterStrikeSharp.API;
|
||||
using TTT.API.Storage;
|
||||
|
||||
namespace TTT.CS2.Configs;
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using CounterStrikeSharp.API;
|
||||
using CounterStrikeSharp.API.Core;
|
||||
using CounterStrikeSharp.API.Modules.Cvars;
|
||||
using CounterStrikeSharp.API.Modules.Cvars.Validators;
|
||||
using TTT.API;
|
||||
using TTT.API.Storage;
|
||||
using TTT.Karma;
|
||||
|
||||
namespace TTT.CS2.Configs;
|
||||
using Karma;
|
||||
|
||||
public class CS2KarmaConfig : IStorage<KarmaConfig>, IPluginModule {
|
||||
public static readonly FakeConVar<string> CV_DB_STRING = new(
|
||||
"css_ttt_karma_dbstring", "Database connection string for Karma storage",
|
||||
"css_ttt_karma_db_string", "Database connection string for Karma storage",
|
||||
"Data Source=karma.db");
|
||||
|
||||
public static readonly FakeConVar<int> CV_MIN_KARMA = new("css_ttt_karma_min",
|
||||
"Minimum possible Karma value", 0, ConVarFlags.FCVAR_NONE,
|
||||
new RangeValidator<int>(0, 1000));
|
||||
"Minimum possible Karma value; falling below executes the low-karma command",
|
||||
0, ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 1000));
|
||||
|
||||
public static readonly FakeConVar<int> CV_DEFAULT_KARMA = new(
|
||||
"css_ttt_karma_default", "Default Karma value for new players", 50,
|
||||
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 1000));
|
||||
"css_ttt_karma_default", "Default Karma assigned to new or reset players",
|
||||
50, ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 1000));
|
||||
|
||||
public static readonly FakeConVar<string> CV_LOW_KARMA_COMMAND = new(
|
||||
"css_ttt_karma_low_command",
|
||||
"Command executed when a player falls below the Karma threshold (use {0} for player name)",
|
||||
"css_ban #{0} 4320 Your karma is too low!");
|
||||
"Command executed when a player's karma falls below the minimum (use {0} for player slot)",
|
||||
"css_ban #{0} 2880 Low Karma");
|
||||
|
||||
public static readonly FakeConVar<int> CV_KARMA_TIMEOUT_THRESHOLD = new(
|
||||
public static readonly FakeConVar<int> CV_TIMEOUT_THRESHOLD = new(
|
||||
"css_ttt_karma_timeout_threshold",
|
||||
"Minimum Karma to avoid punishment or timeout effects", 20,
|
||||
"Minimum Karma before timing a player out for KarmaRoundTimeout rounds", 20,
|
||||
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 1000));
|
||||
|
||||
public static readonly FakeConVar<int> CV_KARMA_ROUND_TIMEOUT = new(
|
||||
"css_ttt_karma_round_timeout", "Number of rounds a Karma penalty persists",
|
||||
public static readonly FakeConVar<int> CV_ROUND_TIMEOUT = new(
|
||||
"css_ttt_karma_round_timeout",
|
||||
"Number of rounds a player is timed out for after falling below threshold",
|
||||
4, ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 100));
|
||||
|
||||
public static readonly FakeConVar<int> CV_WARNING_WINDOW_HOURS = new(
|
||||
"css_ttt_karma_warning_window_hours",
|
||||
"Time window (in hours) preventing repeat warnings for low karma", 24,
|
||||
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(1, 168));
|
||||
|
||||
// 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,
|
||||
new RangeValidator<int>(-50, 50));
|
||||
|
||||
public static readonly FakeConVar<int> CV_TRAITOR_ON_DETECTIVE = new(
|
||||
"css_ttt_karma_traitor_on_detective",
|
||||
"Karma gained when Traitor kills a Detective", 1, ConVarFlags.FCVAR_NONE,
|
||||
new RangeValidator<int>(-50, 50));
|
||||
|
||||
public static readonly FakeConVar<int> CV_INNO_ON_INNO_VICTIM = new(
|
||||
"css_ttt_karma_inno_on_inno_victim",
|
||||
"Karma gained or lost when Innocent kills another Innocent who was a victim",
|
||||
-1, ConVarFlags.FCVAR_NONE, new RangeValidator<int>(-50, 50));
|
||||
|
||||
public static readonly FakeConVar<int> CV_INNO_ON_INNO = new(
|
||||
"css_ttt_karma_inno_on_inno",
|
||||
"Karma lost when Innocent kills another Innocent", -4,
|
||||
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,
|
||||
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,
|
||||
new RangeValidator<int>(-50, 50));
|
||||
|
||||
public void Dispose() { }
|
||||
|
||||
public void Start() { }
|
||||
@@ -50,8 +90,15 @@ public class CS2KarmaConfig : IStorage<KarmaConfig>, IPluginModule {
|
||||
MinKarma = CV_MIN_KARMA.Value,
|
||||
DefaultKarma = CV_DEFAULT_KARMA.Value,
|
||||
CommandUponLowKarma = CV_LOW_KARMA_COMMAND.Value,
|
||||
KarmaTimeoutThreshold = CV_KARMA_TIMEOUT_THRESHOLD.Value,
|
||||
KarmaRoundTimeout = CV_KARMA_ROUND_TIMEOUT.Value
|
||||
KarmaTimeoutThreshold = CV_TIMEOUT_THRESHOLD.Value,
|
||||
KarmaRoundTimeout = CV_ROUND_TIMEOUT.Value,
|
||||
KarmaWarningWindow = TimeSpan.FromHours(CV_WARNING_WINDOW_HOURS.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,
|
||||
INNO_ON_INNO = CV_INNO_ON_INNO.Value,
|
||||
TRAITOR_ON_TRAITOR = CV_TRAITOR_ON_TRAITOR.Value,
|
||||
INNO_ON_DETECTIVE = CV_INNO_ON_DETECTIVE.Value
|
||||
};
|
||||
|
||||
return Task.FromResult<KarmaConfig?>(cfg);
|
||||
|
||||
@@ -73,14 +73,15 @@ public static class PlayerExtensions {
|
||||
|
||||
Utilities.SetStateChanged(pawn, "CCSPlayerPawn", "m_ArmorValue");
|
||||
}
|
||||
|
||||
|
||||
public static (int, bool) GetArmor(this CCSPlayerController player) {
|
||||
if (!player.IsValid) return (0, false);
|
||||
var pawn = player.PlayerPawn.Value;
|
||||
if (pawn == null || !pawn.IsValid) return (0, false);
|
||||
var hasHelmet = false;
|
||||
if (pawn.ItemServices != null)
|
||||
hasHelmet = new CCSPlayer_ItemServices(pawn.ItemServices.Handle).HasHelmet;
|
||||
hasHelmet = new CCSPlayer_ItemServices(pawn.ItemServices.Handle)
|
||||
.HasHelmet;
|
||||
return (pawn.ArmorValue, hasHelmet);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ public static class VectorExtensions {
|
||||
return MathF.Sqrt(vec.DistanceSquared(other));
|
||||
}
|
||||
|
||||
public static float DistanceSquared(this Vector vec, Vector other) {
|
||||
public static float DistanceSquared(this Vector? vec, Vector other) {
|
||||
return (vec.X - other.X) * (vec.X - other.X)
|
||||
+ (vec.Y - other.Y) * (vec.Y - other.Y)
|
||||
+ (vec.Z - other.Z) * (vec.Z - other.Z);
|
||||
|
||||
@@ -16,9 +16,6 @@ public class BuyMenuHandler(IServiceProvider provider) : IPluginModule {
|
||||
private readonly IInventoryManager inventory =
|
||||
provider.GetRequiredService<IInventoryManager>();
|
||||
|
||||
public void Dispose() { }
|
||||
public void Start() { }
|
||||
|
||||
private readonly Dictionary<string, string> shopAliases = new() {
|
||||
{ "item_assaultsuit", "Armor" },
|
||||
{ "item_kevlar", "Armor" },
|
||||
@@ -31,6 +28,9 @@ public class BuyMenuHandler(IServiceProvider provider) : IPluginModule {
|
||||
{ "weapon_decoy", "healthshot" }
|
||||
};
|
||||
|
||||
public void Dispose() { }
|
||||
public void Start() { }
|
||||
|
||||
[UsedImplicitly]
|
||||
[GameEventHandler(HookMode.Pre)]
|
||||
public HookResult OnPurchase(EventItemPurchase ev, GameEventInfo info) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using CounterStrikeSharp.API.Core;
|
||||
using CounterStrikeSharp.API;
|
||||
using CounterStrikeSharp.API.Core;
|
||||
using CounterStrikeSharp.API.Modules.Memory;
|
||||
using CounterStrikeSharp.API.Modules.Memory.DynamicFunctions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@@ -33,7 +34,6 @@ public class DamageCanceler(IServiceProvider provider) : IPluginModule {
|
||||
var damagedEvent = new PlayerDamagedEvent(converter, hook);
|
||||
|
||||
bus.Dispatch(damagedEvent);
|
||||
|
||||
if (damagedEvent.IsCanceled) return HookResult.Handled;
|
||||
|
||||
var info = hook.GetParam<CTakeDamageInfo>(1);
|
||||
|
||||
@@ -9,6 +9,8 @@ namespace TTT.CS2.GameHandlers;
|
||||
public class MapZoneRemover : IPluginModule {
|
||||
private BasePlugin? plugin;
|
||||
|
||||
private bool zonesRemoved;
|
||||
|
||||
public void Dispose() {
|
||||
plugin?.RemoveListener<CounterStrikeSharp.API.Core.Listeners.OnMapStart>(
|
||||
onMapStart);
|
||||
@@ -16,10 +18,8 @@ public class MapZoneRemover : IPluginModule {
|
||||
|
||||
public void Start() { }
|
||||
|
||||
private bool zonesRemoved = false;
|
||||
|
||||
public void Start(BasePlugin? pluginParent) {
|
||||
if (pluginParent != null) this.plugin = pluginParent;
|
||||
if (pluginParent != null) plugin = pluginParent;
|
||||
plugin?.RegisterListener<CounterStrikeSharp.API.Core.Listeners.OnMapStart>(
|
||||
onMapStart);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
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;
|
||||
|
||||
@@ -12,6 +17,11 @@ public class TeamChangeHandler(IServiceProvider provider) : IPluginModule {
|
||||
private readonly IGameManager games =
|
||||
provider.GetRequiredService<IGameManager>();
|
||||
|
||||
private readonly IPlayerConverter<CCSPlayerController> converter =
|
||||
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
|
||||
|
||||
private readonly IEventBus bus = provider.GetRequiredService<IEventBus>();
|
||||
|
||||
public void Dispose() { }
|
||||
public void Start() { }
|
||||
|
||||
@@ -23,16 +33,15 @@ public class TeamChangeHandler(IServiceProvider provider) : IPluginModule {
|
||||
CommandInfo commandInfo) {
|
||||
CsTeam requestedTeam;
|
||||
|
||||
if (int.TryParse(commandInfo.GetArg(1), out var teamIndex)) {
|
||||
if (int.TryParse(commandInfo.GetArg(1), out var teamIndex))
|
||||
requestedTeam = (CsTeam)teamIndex;
|
||||
} else {
|
||||
else
|
||||
requestedTeam = commandInfo.GetArg(1).ToLower() switch {
|
||||
"ct" or "counterterrorist" or "counter" => CsTeam.CounterTerrorist,
|
||||
"t" or "terrorist" => CsTeam.Terrorist,
|
||||
"s" or "spec" or "spectator" or "spectators" => CsTeam.Spectator,
|
||||
_ => CsTeam.None
|
||||
};
|
||||
}
|
||||
|
||||
if (games.ActiveGame is not { State: State.IN_PROGRESS }) {
|
||||
if (player != null && player.LifeState != (int)LifeState_t.LIFE_ALIVE)
|
||||
@@ -40,11 +49,24 @@ public class TeamChangeHandler(IServiceProvider provider) : IPluginModule {
|
||||
return HookResult.Continue;
|
||||
}
|
||||
|
||||
if (requestedTeam is CsTeam.CounterTerrorist or CsTeam.Terrorist) {
|
||||
if (requestedTeam is CsTeam.CounterTerrorist or CsTeam.Terrorist)
|
||||
if (player != null && player.Team is CsTeam.Spectator or CsTeam.None)
|
||||
return HookResult.Continue;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
162
TTT/CS2/Items/Compass/CompassItem.cs
Normal file
162
TTT/CS2/Items/Compass/CompassItem.cs
Normal file
@@ -0,0 +1,162 @@
|
||||
using CounterStrikeSharp.API;
|
||||
using CounterStrikeSharp.API.Core;
|
||||
using CounterStrikeSharp.API.Modules.Timers;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using ShopAPI;
|
||||
using ShopAPI.Configs;
|
||||
using ShopAPI.Configs.Traitor;
|
||||
using TTT.API;
|
||||
using TTT.API.Extensions;
|
||||
using TTT.API.Game;
|
||||
using TTT.API.Player;
|
||||
using TTT.API.Storage;
|
||||
using TTT.CS2.Extensions;
|
||||
using TTT.CS2.Utils;
|
||||
using TTT.Game.Roles;
|
||||
|
||||
namespace TTT.CS2.Items.Compass;
|
||||
|
||||
public static class CompassServiceCollection {
|
||||
public static void AddCompassServices(this IServiceCollection collection) {
|
||||
collection.AddModBehavior<CompassItem>();
|
||||
}
|
||||
}
|
||||
|
||||
public class CompassItem(IServiceProvider provider)
|
||||
: RoleRestrictedItem<TraitorRole>(provider), IPluginModule {
|
||||
private readonly CompassConfig config =
|
||||
provider.GetService<IStorage<CompassConfig>>()
|
||||
?.Load()
|
||||
.GetAwaiter()
|
||||
.GetResult() ?? new CompassConfig();
|
||||
|
||||
private readonly IPlayerConverter<CCSPlayerController> converter =
|
||||
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
|
||||
|
||||
public override string Name => Locale[CompassMsgs.SHOP_ITEM_COMPASS];
|
||||
|
||||
public override string Description
|
||||
=> Locale[CompassMsgs.SHOP_ITEM_COMPASS_DESC];
|
||||
|
||||
public override ShopItemConfig Config => config;
|
||||
|
||||
public void Start(BasePlugin? plugin) {
|
||||
base.Start();
|
||||
plugin?.AddTimer(0.1f, tick, TimerFlags.REPEAT);
|
||||
}
|
||||
|
||||
public override void OnPurchase(IOnlinePlayer player) { }
|
||||
|
||||
public override PurchaseResult CanPurchase(IOnlinePlayer player) {
|
||||
return Shop.HasItem<CompassItem>(player) ?
|
||||
PurchaseResult.ALREADY_OWNED :
|
||||
base.CanPurchase(player);
|
||||
}
|
||||
|
||||
private void tick() {
|
||||
if (Games.ActiveGame is not { State: State.IN_PROGRESS or State.FINISHED })
|
||||
return;
|
||||
|
||||
var traitors = Games.ActiveGame.Players.OfType<IOnlinePlayer>()
|
||||
.Where(p => p.IsAlive)
|
||||
.Where(p => Roles.GetRoles(p).Any(r => r is TraitorRole))
|
||||
.ToList();
|
||||
|
||||
var allies = Games.ActiveGame.Players.OfType<IOnlinePlayer>()
|
||||
.Where(p => p.IsAlive)
|
||||
.Where(p => !Roles.GetRoles(p).Any(r => r is TraitorRole))
|
||||
.ToList();
|
||||
|
||||
foreach (var gamePlayer in Utilities.GetPlayers()) {
|
||||
var player = converter.GetPlayer(gamePlayer);
|
||||
if (player is not IOnlinePlayer online) continue;
|
||||
if (!Shop.HasItem<CompassItem>(online)) continue;
|
||||
showRadarTo(gamePlayer, online, traitors, allies);
|
||||
}
|
||||
}
|
||||
|
||||
private void showRadarTo(CCSPlayerController player, IOnlinePlayer online,
|
||||
IList<IOnlinePlayer> traitors, List<IOnlinePlayer> allies) {
|
||||
if (Games.ActiveGame?.Players == null) return;
|
||||
if (player.PlayerPawn.Value == null) return;
|
||||
|
||||
var enemies = getEnemies(online, traitors, allies);
|
||||
if (enemies.Count == 0) return;
|
||||
var gameEnemies = enemies.Select(e => converter.GetPlayer(e))
|
||||
.Where(e => e != null)
|
||||
.Select(e => e!)
|
||||
.ToList();
|
||||
if (gameEnemies.Count == 0) return;
|
||||
|
||||
var (nearestPlayer, distance) =
|
||||
getNearest(player, gameEnemies) ?? (null, 0);
|
||||
if (nearestPlayer == null || distance > config.MaxRange) return;
|
||||
var src = player.Pawn.Value?.AbsOrigin.Clone();
|
||||
var dst = nearestPlayer.Pawn.Value?.AbsOrigin.Clone();
|
||||
if (src == null || dst == null) return;
|
||||
var normalizedYaw = adjustGameAngle(player.PlayerPawn.Value.EyeAngles.Y);
|
||||
|
||||
var diff = (dst - src).Normalized();
|
||||
var targetYaw = MathF.Atan2(diff.Y, diff.X) * 180f / MathF.PI;
|
||||
targetYaw = adjustGameAngle(targetYaw);
|
||||
|
||||
var compass = generateCompass(normalizedYaw, targetYaw);
|
||||
compass = "<font color=\"#777777\">" + compass;
|
||||
foreach (var c in "NESW".ToCharArray())
|
||||
compass = compass.Replace(c.ToString(),
|
||||
$"</font><font color=\"#FFFF00\">{c}</font><font color=\"#777777\">");
|
||||
compass = compass.Replace("X",
|
||||
"</font><font color=\"#FF0000\">X</font><font color=\"#777777\">");
|
||||
compass += "</font>";
|
||||
|
||||
player.PrintToCenterHtml($"{compass} {getDistanceDescription(distance)}");
|
||||
}
|
||||
|
||||
private float adjustGameAngle(float angle) {
|
||||
return 360 - (angle + 360) % 360 + 90;
|
||||
}
|
||||
|
||||
private string generateCompass(float pointing, float target) {
|
||||
return TextCompass.GenerateCompass(config.CompassFOV, config.CompassLength,
|
||||
pointing, targetDir: target);
|
||||
}
|
||||
|
||||
private string getDistanceDescription(float distance) {
|
||||
return distance switch {
|
||||
> 2000 => "AWP Distance",
|
||||
> 1500 => "Scout Distance",
|
||||
> 1000 => "Rifle Distance",
|
||||
> 500 => "Pistol",
|
||||
> 250 => "Nearby",
|
||||
_ => "Knife Range"
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
private IList<IOnlinePlayer> getEnemies(IOnlinePlayer online,
|
||||
IList<IOnlinePlayer> traitors, IList<IOnlinePlayer> allies) {
|
||||
return Roles.GetRoles(online).Any(r => r is TraitorRole) ?
|
||||
allies :
|
||||
traitors;
|
||||
}
|
||||
|
||||
private (CCSPlayerController?, float)? getNearest(CCSPlayerController source,
|
||||
List<CCSPlayerController> others) {
|
||||
if (others.Count == 0) return null;
|
||||
var minDist = float.MaxValue;
|
||||
var minPlayer = others[0];
|
||||
var src = source.Pawn.Value?.AbsOrigin.Clone();
|
||||
if (src == null) return null;
|
||||
|
||||
foreach (var player in others) {
|
||||
if (player.Pawn.Value == null) continue;
|
||||
|
||||
var dist = player.Pawn.Value.AbsOrigin.Clone().DistanceSquared(src);
|
||||
if (dist >= minDist) continue;
|
||||
minDist = dist;
|
||||
minPlayer = player;
|
||||
}
|
||||
|
||||
return (minPlayer, MathF.Sqrt(minDist));
|
||||
}
|
||||
}
|
||||
11
TTT/CS2/Items/Compass/CompassMsgs.cs
Normal file
11
TTT/CS2/Items/Compass/CompassMsgs.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using TTT.Locale;
|
||||
|
||||
namespace TTT.CS2.Items.Compass;
|
||||
|
||||
public class CompassMsgs {
|
||||
public static IMsg SHOP_ITEM_COMPASS
|
||||
=> MsgFactory.Create(nameof(SHOP_ITEM_COMPASS));
|
||||
|
||||
public static IMsg SHOP_ITEM_COMPASS_DESC
|
||||
=> MsgFactory.Create(nameof(SHOP_ITEM_COMPASS_DESC));
|
||||
}
|
||||
@@ -31,7 +31,7 @@ public class DnaScanner(IServiceProvider provider)
|
||||
public override void OnPurchase(IOnlinePlayer player) { }
|
||||
|
||||
public override PurchaseResult CanPurchase(IOnlinePlayer player) {
|
||||
if (Shop.HasItem(player, this)) return PurchaseResult.ALREADY_OWNED;
|
||||
if (Shop.HasItem<DnaScanner>(player)) return PurchaseResult.ALREADY_OWNED;
|
||||
return base.CanPurchase(player);
|
||||
}
|
||||
}
|
||||
@@ -13,14 +13,14 @@ namespace TTT.CS2.Items.OneHitKnife;
|
||||
|
||||
public class OneHitKnifeListener(IServiceProvider provider)
|
||||
: BaseListener(provider) {
|
||||
private readonly IShop shop = provider.GetRequiredService<IShop>();
|
||||
|
||||
private readonly OneHitKnifeConfig config =
|
||||
provider.GetService<IStorage<OneHitKnifeConfig>>()
|
||||
?.Load()
|
||||
.GetAwaiter()
|
||||
.GetResult() ?? new OneHitKnifeConfig();
|
||||
|
||||
private readonly IShop shop = provider.GetRequiredService<IShop>();
|
||||
|
||||
[UsedImplicitly]
|
||||
[EventHandler]
|
||||
public void OnDamage(PlayerDamagedEvent ev) {
|
||||
|
||||
@@ -21,6 +21,8 @@ namespace TTT.CS2.Items.PoisonShots;
|
||||
|
||||
public class PoisonShotsListener(IServiceProvider provider)
|
||||
: BaseListener(provider), IPluginModule {
|
||||
private readonly IEventBus bus = provider.GetRequiredService<IEventBus>();
|
||||
|
||||
private readonly PoisonShotsConfig config =
|
||||
provider.GetService<IStorage<PoisonShotsConfig>>()
|
||||
?.Load()
|
||||
@@ -32,8 +34,6 @@ public class PoisonShotsListener(IServiceProvider provider)
|
||||
|
||||
private readonly Dictionary<IPlayer, int> poisonShots = new();
|
||||
|
||||
private readonly IEventBus bus = provider.GetRequiredService<IEventBus>();
|
||||
|
||||
private readonly List<IDisposable> poisonTimers = [];
|
||||
|
||||
private readonly IScheduler scheduler =
|
||||
@@ -66,7 +66,6 @@ public class PoisonShotsListener(IServiceProvider provider)
|
||||
if (ev.Attacker == null) return;
|
||||
if (!poisonShots.TryGetValue(ev.Attacker, out var shot) || shot <= 0)
|
||||
return;
|
||||
Messenger.DebugAnnounce("weapon: " + ev.Weapon);
|
||||
if (ev.Weapon == null || !Tag.GUNS.Contains(ev.Weapon)) return;
|
||||
Messenger.Message(ev.Attacker,
|
||||
Locale[PoisonShotMsgs.SHOP_ITEM_POISON_HIT(ev.Player)]);
|
||||
@@ -105,7 +104,7 @@ public class PoisonShotsListener(IServiceProvider provider)
|
||||
if (!online.IsAlive) return false;
|
||||
|
||||
var dmgEvent = new PlayerDamagedEvent(online,
|
||||
effect.Shooter as IOnlinePlayer,
|
||||
effect.Shooter as IOnlinePlayer, online.Health,
|
||||
online.Health - config.PoisonConfig.DamagePerTick) {
|
||||
Weapon = $"[{Locale[PoisonShotMsgs.SHOP_ITEM_POISON_SHOTS]}]"
|
||||
};
|
||||
|
||||
101
TTT/CS2/Items/SilentAWP/SilentAWPItem.cs
Normal file
101
TTT/CS2/Items/SilentAWP/SilentAWPItem.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
using CounterStrikeSharp.API;
|
||||
using CounterStrikeSharp.API.Core;
|
||||
using CounterStrikeSharp.API.Modules.UserMessages;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using ShopAPI;
|
||||
using ShopAPI.Configs;
|
||||
using ShopAPI.Configs.Traitor;
|
||||
using TTT.API;
|
||||
using TTT.API.Extensions;
|
||||
using TTT.API.Player;
|
||||
using TTT.API.Storage;
|
||||
using TTT.CS2.Extensions;
|
||||
using TTT.CS2.RayTrace.Class;
|
||||
using TTT.Game.Roles;
|
||||
using Vector = CounterStrikeSharp.API.Modules.Utils.Vector;
|
||||
|
||||
namespace TTT.CS2.Items.SilentAWP;
|
||||
|
||||
public static class SilentAWPServiceCollection {
|
||||
public static void AddSilentAWPServices(this IServiceCollection services) {
|
||||
services.AddModBehavior<SilentAWPItem>();
|
||||
}
|
||||
}
|
||||
|
||||
public class SilentAWPItem(IServiceProvider provider)
|
||||
: RoleRestrictedItem<TraitorRole>(provider), IPluginModule {
|
||||
private readonly SilentAWPConfig config =
|
||||
provider.GetService<IStorage<SilentAWPConfig>>()
|
||||
?.Load()
|
||||
.GetAwaiter()
|
||||
.GetResult() ?? new SilentAWPConfig();
|
||||
|
||||
private readonly IPlayerConverter<CCSPlayerController> playerConverter =
|
||||
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
|
||||
|
||||
private readonly IDictionary<IOnlinePlayer, int> silentShots =
|
||||
new Dictionary<IOnlinePlayer, int>();
|
||||
|
||||
public override string Name => Locale[SilentAWPMsgs.SHOP_ITEM_SILENT_AWP];
|
||||
|
||||
public override string Description
|
||||
=> Locale[SilentAWPMsgs.SHOP_ITEM_SILENT_AWP_DESC];
|
||||
|
||||
public override ShopItemConfig Config => config;
|
||||
|
||||
public void Start(BasePlugin? plugin) {
|
||||
base.Start();
|
||||
plugin?.HookUserMessage(452, onWeaponSound);
|
||||
}
|
||||
|
||||
public override void OnPurchase(IOnlinePlayer player) {
|
||||
silentShots[player] = config.CurrentAmmo ?? 0 + config.ReserveAmmo ?? 0;
|
||||
Inventory.GiveWeapon(player, config);
|
||||
}
|
||||
|
||||
private HookResult onWeaponSound(UserMessage msg) {
|
||||
var defIndex = msg.ReadUInt("item_def_index");
|
||||
|
||||
if (config.WeaponIndex != defIndex) return HookResult.Continue;
|
||||
var splits = msg.DebugString.Split("\n");
|
||||
if (splits.Length < 5) return HookResult.Continue;
|
||||
var angleLines = msg.DebugString.Split("\n")[1..4]
|
||||
.Select(s => s.Trim())
|
||||
.ToList();
|
||||
if (!angleLines[0].Contains('x') || !angleLines[1].Contains('y')
|
||||
|| !angleLines[2].Contains('z'))
|
||||
return HookResult.Continue;
|
||||
var x = float.Parse(angleLines[0].Split(' ')[1]);
|
||||
var y = float.Parse(angleLines[1].Split(' ')[1]);
|
||||
var z = float.Parse(angleLines[2].Split(' ')[1]);
|
||||
var vec = new Vector(x, y, z);
|
||||
var player = findPlayerByCoord(vec);
|
||||
|
||||
if (player == null) return HookResult.Continue;
|
||||
if (playerConverter.GetPlayer(player) is not IOnlinePlayer apiPlayer)
|
||||
return HookResult.Continue;
|
||||
|
||||
if (!silentShots.TryGetValue(apiPlayer, out var shots) || shots <= 0)
|
||||
return HookResult.Continue;
|
||||
|
||||
silentShots[apiPlayer] = shots - 1;
|
||||
if (silentShots[apiPlayer] == 0) {
|
||||
silentShots.Remove(apiPlayer);
|
||||
Shop.RemoveItem<SilentAWPItem>(apiPlayer);
|
||||
}
|
||||
|
||||
msg.Recipients.Clear();
|
||||
return HookResult.Handled;
|
||||
}
|
||||
|
||||
private CCSPlayerController? findPlayerByCoord(Vector vec) {
|
||||
foreach (var pl in Utilities.GetPlayers()) {
|
||||
var origin = pl.GetEyePosition();
|
||||
if (origin == null) continue;
|
||||
var dist = vec.DistanceSquared(origin);
|
||||
if (dist < 1) return pl;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
11
TTT/CS2/Items/SilentAWP/SilentAWPMsgs.cs
Normal file
11
TTT/CS2/Items/SilentAWP/SilentAWPMsgs.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using TTT.Locale;
|
||||
|
||||
namespace TTT.CS2.Items.SilentAWP;
|
||||
|
||||
public class SilentAWPMsgs {
|
||||
public static IMsg SHOP_ITEM_SILENT_AWP
|
||||
=> MsgFactory.Create(nameof(SHOP_ITEM_SILENT_AWP));
|
||||
|
||||
public static IMsg SHOP_ITEM_SILENT_AWP_DESC
|
||||
=> MsgFactory.Create(nameof(SHOP_ITEM_SILENT_AWP_DESC));
|
||||
}
|
||||
@@ -24,6 +24,8 @@ public class DamageStation(IServiceProvider provider)
|
||||
?.Load()
|
||||
.GetAwaiter()
|
||||
.GetResult() ?? new DamageStationConfig()) {
|
||||
private readonly IEventBus bus = provider.GetRequiredService<IEventBus>();
|
||||
|
||||
private readonly IPlayerConverter<CCSPlayerController> converter =
|
||||
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
|
||||
|
||||
@@ -33,8 +35,6 @@ public class DamageStation(IServiceProvider provider)
|
||||
private readonly IRoleAssigner roles =
|
||||
provider.GetRequiredService<IRoleAssigner>();
|
||||
|
||||
private readonly IEventBus bus = provider.GetRequiredService<IEventBus>();
|
||||
|
||||
public override string Name => Locale[StationMsgs.SHOP_ITEM_STATION_HURT];
|
||||
|
||||
public override string Description
|
||||
@@ -73,9 +73,7 @@ public class DamageStation(IServiceProvider provider)
|
||||
(int)Math.Floor(_Config.HealthIncrements * healthScale);
|
||||
|
||||
var dmgEvent = new PlayerDamagedEvent(player,
|
||||
info.Owner as IOnlinePlayer, player.Health + damageAmount) {
|
||||
Weapon = $"[{Name}]"
|
||||
};
|
||||
info.Owner as IOnlinePlayer, damageAmount) { Weapon = $"[{Name}]" };
|
||||
|
||||
bus.Dispatch(dmgEvent);
|
||||
|
||||
|
||||
@@ -17,12 +17,13 @@ namespace TTT.CS2.Items.Station;
|
||||
public abstract class StationItem<T>(IServiceProvider provider,
|
||||
StationConfig config)
|
||||
: RoleRestrictedItem<T>(provider), IPluginModule where T : IRole {
|
||||
private readonly long PROP_SIZE_SQUARED = 500;
|
||||
protected readonly StationConfig _Config = config;
|
||||
|
||||
protected readonly IPlayerConverter<CCSPlayerController> Converter =
|
||||
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
|
||||
|
||||
private readonly long PROP_SIZE_SQUARED = 500;
|
||||
|
||||
protected readonly Dictionary<CPhysicsPropMultiplayer, StationInfo> props =
|
||||
new();
|
||||
|
||||
|
||||
@@ -21,15 +21,16 @@ public class KarmaBanner(IServiceProvider provider) : BaseListener(provider) {
|
||||
.GetAwaiter()
|
||||
.GetResult() ?? new KarmaConfig();
|
||||
|
||||
private readonly IKarmaService karma =
|
||||
provider.GetRequiredService<IKarmaService>();
|
||||
|
||||
private readonly IPlayerConverter<CCSPlayerController> converter =
|
||||
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
|
||||
|
||||
private readonly Dictionary<IPlayer, DateTime> lastWarned = new();
|
||||
private readonly Dictionary<IPlayer, int> cooldownRounds = new();
|
||||
|
||||
private readonly IKarmaService karma =
|
||||
provider.GetRequiredService<IKarmaService>();
|
||||
|
||||
private readonly Dictionary<IPlayer, DateTime> lastWarned = new();
|
||||
|
||||
[UsedImplicitly]
|
||||
[EventHandler(Priority = Priority.MONITOR, IgnoreCanceled = true)]
|
||||
public void OnKarmaUpdate(KarmaUpdateEvent ev) {
|
||||
|
||||
@@ -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,16 @@ 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);
|
||||
|
||||
@@ -53,7 +53,7 @@ public class CS2AliveSpoofer : IAliveSpoofer, IPluginModule {
|
||||
}
|
||||
|
||||
private void onTick() {
|
||||
_fakeAlivePlayers.RemoveWhere(p => !p.IsValid);
|
||||
_fakeAlivePlayers.RemoveWhere(p => !p.IsValid || p.Handle == IntPtr.Zero);
|
||||
foreach (var player in _fakeAlivePlayers) {
|
||||
player.PawnIsAlive = true;
|
||||
Utilities.SetStateChanged(player, "CCSPlayerController",
|
||||
|
||||
81
TTT/CS2/Utils/TextCompass.cs
Normal file
81
TTT/CS2/Utils/TextCompass.cs
Normal file
@@ -0,0 +1,81 @@
|
||||
namespace TTT.CS2.Utils;
|
||||
|
||||
public static class TextCompass {
|
||||
/// <summary>
|
||||
/// Builds a compass line with at most one character for each of N, E, S, W.
|
||||
/// 0° = North, 90° = East, angles increase clockwise.
|
||||
/// </summary>
|
||||
/// <param name="fov">Field of view in degrees [0..360].</param>
|
||||
/// <param name="width">Output width in characters.</param>
|
||||
/// <param name="direction">Facing direction in degrees.</param>
|
||||
/// <param name="filler">Filler character for empty slots.</param>
|
||||
/// <param name="targetDir"></param>
|
||||
public static string GenerateCompass(float fov, int width, float direction,
|
||||
char filler = '·', float? targetDir = null) {
|
||||
if (width <= 0) return string.Empty;
|
||||
|
||||
fov = Math.Clamp(fov, 0.001f, 360f);
|
||||
direction = Normalize(direction);
|
||||
|
||||
var buf = new char[width];
|
||||
for (var i = 0; i < width; i++) buf[i] = filler;
|
||||
|
||||
var start = direction - fov / 2f; // left edge of view
|
||||
var degPerChar = fov / width;
|
||||
|
||||
PlaceIfVisible('N', 0f);
|
||||
PlaceIfVisible('E', 90f);
|
||||
PlaceIfVisible('S', 180f);
|
||||
PlaceIfVisible('W', 270f);
|
||||
if (targetDir.HasValue) PlaceIfVisible('X', targetDir.Value);
|
||||
|
||||
return new string(buf);
|
||||
|
||||
void PlaceIfVisible(char c, float cardinalAngle) {
|
||||
var delta = ForwardDelta(start, cardinalAngle); // [0..360)
|
||||
if (delta < 0f || delta >= fov) return; // outside view
|
||||
|
||||
// Map degrees to nearest character cell
|
||||
var idx = (int)MathF.Round(delta / degPerChar);
|
||||
if (idx < 0) idx = 0;
|
||||
if (idx >= width) idx = width - 1;
|
||||
|
||||
// Nudge left/right to avoid collisions when possible
|
||||
if (buf[idx] == filler) {
|
||||
buf[idx] = c;
|
||||
return;
|
||||
}
|
||||
|
||||
var maxRadius = Math.Max(idx, width - 1 - idx);
|
||||
for (var r = 1; r <= maxRadius; r++) {
|
||||
var left = idx - r;
|
||||
if (left >= 0 && buf[left] == filler) {
|
||||
buf[left] = c;
|
||||
return;
|
||||
}
|
||||
|
||||
var right = idx + r;
|
||||
if (right < width && buf[right] == filler) {
|
||||
buf[right] = c;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If no space, overwrite the original cell as a last resort
|
||||
buf[idx] = c;
|
||||
}
|
||||
}
|
||||
|
||||
private static float Normalize(float angle) {
|
||||
angle %= 360f;
|
||||
return angle < 0 ? angle + 360f : angle;
|
||||
}
|
||||
|
||||
// Delta moving forward from start to target, wrapped to [0..360)
|
||||
private static float ForwardDelta(float start, float target) {
|
||||
var s = Normalize(start);
|
||||
var t = Normalize(target);
|
||||
var d = t - s;
|
||||
return d < 0 ? d + 360f : d;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
ROLE_SPECTATOR: "Spectator"
|
||||
ROLE_SPECTATOR: "Spectator"
|
||||
TASER_SCANNED: "%PREFIX%You scanned {0}{grey}, they are %an% {1}{grey}!"
|
||||
DNA_PREFIX: "{darkblue}D{blue}N{lightblue}A{grey} | {grey}"
|
||||
|
||||
@@ -32,4 +32,10 @@ SHOP_ITEM_ARMOR: "Armor with Helmet"
|
||||
SHOP_ITEM_ARMOR_DESC: "Wear armor that reduces incoming damage."
|
||||
|
||||
SHOP_ITEM_ONE_HIT_KNIFE: "One-Hit Knife"
|
||||
SHOP_ITEM_ONE_HIT_KNIFE_DESC: "Your next knife hit will be a guaranteed kill."
|
||||
SHOP_ITEM_ONE_HIT_KNIFE_DESC: "Your next knife hit will be a guaranteed kill."
|
||||
|
||||
SHOP_ITEM_COMPASS: "Player Compass"
|
||||
SHOP_ITEM_COMPASS_DESC: "Reveals the direction that the nearest non-Traitor is in."
|
||||
|
||||
SHOP_ITEM_SILENT_AWP: "Silent AWP"
|
||||
SHOP_ITEM_SILENT_AWP_DESC: "Receive a silenced AWP with limited ammo."
|
||||
@@ -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 IMessenger messenger =
|
||||
provider.GetRequiredService<IMessenger>();
|
||||
|
||||
private readonly IMsgLocalizer localizer =
|
||||
provider.GetRequiredService<IMsgLocalizer>();
|
||||
|
||||
private readonly IIconManager? icons = provider.GetService<IIconManager>();
|
||||
|
||||
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,13 @@ 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);
|
||||
}
|
||||
|
||||
@@ -27,29 +27,24 @@ public class EventBus(IServiceProvider provider) : IEventBus, ITerrorModule {
|
||||
}
|
||||
}
|
||||
|
||||
public Task Dispatch(Event ev) {
|
||||
public void Dispatch(Event ev) {
|
||||
var type = ev.GetType();
|
||||
|
||||
handlers.TryGetValue(type, out var list);
|
||||
|
||||
if (list == null || list.Count == 0) return Task.CompletedTask;
|
||||
if (list == null || list.Count == 0) return;
|
||||
|
||||
ICancelableEvent? cancelable = null;
|
||||
if (ev is ICancelableEvent) cancelable = (ICancelableEvent)ev;
|
||||
|
||||
List<Task> tasks = [];
|
||||
|
||||
foreach (var (listener, method) in list) {
|
||||
if (cancelable is { IsCanceled: true } && method
|
||||
.GetCustomAttribute<EventHandlerAttribute>()
|
||||
?.IgnoreCanceled == true)
|
||||
continue;
|
||||
|
||||
var result = method.Invoke(listener, [ev]);
|
||||
if (result is Task task) tasks.Add(task);
|
||||
method.Invoke(listener, [ev]);
|
||||
}
|
||||
|
||||
return Task.WhenAll(tasks);
|
||||
}
|
||||
|
||||
public void Dispose() { handlers.Clear(); }
|
||||
@@ -85,6 +80,11 @@ public class EventBus(IServiceProvider provider) : IEventBus, ITerrorModule {
|
||||
var attr = method.GetCustomAttribute<EventHandlerAttribute>();
|
||||
if (attr == null) return;
|
||||
|
||||
if (method.ReturnType != typeof(void))
|
||||
throw new InvalidOperationException(
|
||||
$"Method {method.Name} in {listener.GetType().Name} "
|
||||
+ "must have void return type.");
|
||||
|
||||
var parameters = method.GetParameters();
|
||||
if (parameters.Length != 1
|
||||
|| !typeof(Event).IsAssignableFrom(parameters[0].ParameterType))
|
||||
|
||||
@@ -73,7 +73,7 @@ public abstract class EventModifiedMessenger(IServiceProvider provider)
|
||||
PlayerMessageEvent ev) {
|
||||
if (player == null) return await SendMessage(null, msg);
|
||||
|
||||
await Bus.Dispatch(ev);
|
||||
Bus.Dispatch(ev);
|
||||
if (ev.IsCanceled) return false;
|
||||
|
||||
return await SendMessage(player, ev.Message, ev.Args);
|
||||
|
||||
@@ -6,7 +6,11 @@ using TTT.API.Player;
|
||||
namespace TTT.Game.Events.Player;
|
||||
|
||||
public class PlayerDamagedEvent(IOnlinePlayer player, IOnlinePlayer? attacker,
|
||||
int hpLeft) : PlayerEvent(player), ICancelableEvent {
|
||||
int originalHp, int hpLeft) : PlayerEvent(player), ICancelableEvent {
|
||||
public PlayerDamagedEvent(IOnlinePlayer player, IOnlinePlayer? attacker,
|
||||
int damageDealt) : this(player, attacker, player.Health - damageDealt,
|
||||
player.Health) { }
|
||||
|
||||
public PlayerDamagedEvent(IPlayerConverter<CCSPlayerController> converter,
|
||||
EventPlayerHurt ev) : this(
|
||||
converter.GetPlayer(ev.Userid!) as IOnlinePlayer
|
||||
@@ -14,22 +18,14 @@ public class PlayerDamagedEvent(IOnlinePlayer player, IOnlinePlayer? attacker,
|
||||
ev.Attacker == null ?
|
||||
null :
|
||||
converter.GetPlayer(ev.Attacker) as IOnlinePlayer,
|
||||
ev.Health + ev.DmgHealth) {
|
||||
ev.Health + ev.DmgHealth, ev.Health) {
|
||||
ArmorDamage = ev.DmgArmor;
|
||||
ArmorRemaining = ev.Armor;
|
||||
Weapon = ev.Weapon;
|
||||
}
|
||||
|
||||
public PlayerDamagedEvent(IPlayerConverter<CCSPlayerController> converter,
|
||||
EventPlayerFalldamage ev) : this(
|
||||
converter.GetPlayer(ev.Userid!) as IOnlinePlayer
|
||||
?? throw new InvalidOperationException(), null,
|
||||
ev.Userid!.Health + (int)ev.Damage) {
|
||||
ArmorRemaining = ev.Userid.PawnArmor;
|
||||
}
|
||||
|
||||
public PlayerDamagedEvent(IPlayerConverter<CCSPlayerController> converter,
|
||||
DynamicHook hook) : this(null!, null, 0) {
|
||||
DynamicHook hook) : this(null!, null, 0, 0) {
|
||||
var playerPawn = hook.GetParam<CCSPlayerPawn>(0);
|
||||
var info = hook.GetParam<CTakeDamageInfo>(1);
|
||||
|
||||
@@ -46,8 +42,8 @@ public class PlayerDamagedEvent(IOnlinePlayer player, IOnlinePlayer? attacker,
|
||||
Attacker = attacker == null || !attacker.IsValid ?
|
||||
null :
|
||||
converter.GetPlayer(attacker) as IOnlinePlayer;
|
||||
// HpLeft = player.Health - DmgDealt;
|
||||
HpLeft = (int)(player.Pawn.Value!.Health - info.Damage);
|
||||
OriginalHp = player.Pawn.Value!.Health;
|
||||
HpLeft = (int)(OriginalHp - info.Damage);
|
||||
}
|
||||
|
||||
public override string Id => "basegame.event.player.damaged";
|
||||
@@ -55,9 +51,10 @@ public class PlayerDamagedEvent(IOnlinePlayer player, IOnlinePlayer? attacker,
|
||||
|
||||
public int ArmorDamage { get; private set; }
|
||||
public int ArmorRemaining { get; set; }
|
||||
public int DmgDealt => player.Health - HpLeft;
|
||||
public int DmgDealt => OriginalHp - HpLeft;
|
||||
|
||||
public int HpLeft { get; set; } = hpLeft;
|
||||
public int OriginalHp { get; } = originalHp;
|
||||
|
||||
public string? Weapon { get; init; }
|
||||
public bool IsCanceled { get; set; }
|
||||
|
||||
@@ -21,7 +21,7 @@ public class SimpleLogger(IServiceProvider provider) : IActionLogger {
|
||||
|
||||
private DateTime? epoch;
|
||||
|
||||
public void LogAction(IAction action) {
|
||||
public virtual void LogAction(IAction action) {
|
||||
#if DEBUG
|
||||
msg.Value.Debug(
|
||||
$"Logging action: {action.GetType().Name} at {scheduler.Now}");
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ public class RoundBasedGame(IServiceProvider provider) : IGame {
|
||||
public virtual State State {
|
||||
set {
|
||||
var ev = new GameStateUpdateEvent(this, value);
|
||||
Bus.Dispatch(ev).GetAwaiter().GetResult();
|
||||
Bus.Dispatch(ev);
|
||||
if (ev.IsCanceled) return;
|
||||
state = value;
|
||||
}
|
||||
@@ -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)));
|
||||
|
||||
@@ -108,4 +108,7 @@ public static class GameMsgs {
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public static IMsg LOGS_VIEWED_ALIVE(IPlayer player)
|
||||
=> MsgFactory.Create(nameof(LOGS_VIEWED_ALIVE), player.Name);
|
||||
}
|
||||
@@ -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."
|
||||
@@ -5,15 +5,58 @@ namespace TTT.Karma;
|
||||
public record KarmaConfig {
|
||||
public string DbString { get; init; } = "Data Source=karma.db";
|
||||
|
||||
public int MinKarma { get; init; } = 0;
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </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.
|
||||
/// </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;
|
||||
/// </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.
|
||||
/// </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.
|
||||
/// </summary>
|
||||
public TimeSpan KarmaWarningWindow { get; init; } = TimeSpan.FromDays(1);
|
||||
|
||||
public int MaxKarma(IPlayer? player) { return 100; }
|
||||
|
||||
public int KarmaTimeoutThreshold { get; init; } = 20;
|
||||
public int KarmaRoundTimeout { get; init; } = 4;
|
||||
/// <summary>
|
||||
/// Amount of karma a player will gain at the end of each round.
|
||||
/// </summary>
|
||||
public int KarmaPerRound { get; init; } = 3;
|
||||
public int KarmaPerRoundWin { get; init; } = 5;
|
||||
|
||||
public TimeSpan KarmaWarningWindow { get; init; } = TimeSpan.FromDays(1);
|
||||
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;
|
||||
}
|
||||
@@ -4,6 +4,7 @@ using TTT.API.Events;
|
||||
using TTT.API.Game;
|
||||
using TTT.API.Player;
|
||||
using TTT.API.Role;
|
||||
using TTT.API.Storage;
|
||||
using TTT.Game.Events.Game;
|
||||
using TTT.Game.Events.Player;
|
||||
using TTT.Game.Listeners;
|
||||
@@ -12,13 +13,6 @@ using TTT.Game.Roles;
|
||||
namespace TTT.Karma;
|
||||
|
||||
public class KarmaListener(IServiceProvider provider) : BaseListener(provider) {
|
||||
private static readonly int INNO_ON_TRAITOR = 2;
|
||||
private static readonly int TRAITOR_ON_DETECTIVE = 1;
|
||||
private static readonly int INNO_ON_INNO_VICTIM = -1;
|
||||
private static readonly int INNO_ON_INNO = -4;
|
||||
private static readonly int TRAITOR_ON_TRAITOR = -5;
|
||||
private static readonly int INNO_ON_DETECTIVE = -6;
|
||||
|
||||
private readonly Dictionary<string, int> badKills = new();
|
||||
|
||||
private readonly IGameManager games =
|
||||
@@ -27,10 +21,16 @@ public class KarmaListener(IServiceProvider provider) : BaseListener(provider) {
|
||||
private readonly IKarmaService karma =
|
||||
provider.GetRequiredService<IKarmaService>();
|
||||
|
||||
private readonly Dictionary<IPlayer, int> queuedKarmaUpdates = new();
|
||||
|
||||
private readonly IRoleAssigner roles =
|
||||
provider.GetRequiredService<IRoleAssigner>();
|
||||
|
||||
private readonly Dictionary<IPlayer, int> queuedKarmaUpdates = new();
|
||||
private readonly KarmaConfig config =
|
||||
provider.GetService<IStorage<KarmaConfig>>()
|
||||
?.Load()
|
||||
.GetAwaiter()
|
||||
.GetResult() ?? new KarmaConfig();
|
||||
|
||||
[EventHandler]
|
||||
[UsedImplicitly]
|
||||
@@ -45,6 +45,7 @@ public class KarmaListener(IServiceProvider provider) : BaseListener(provider) {
|
||||
var killer = ev.Killer;
|
||||
|
||||
if (killer == null) return;
|
||||
if (victim.Id == killer.Id) return;
|
||||
|
||||
var victimRole = roles.GetRoles(victim).First();
|
||||
var killerRole = roles.GetRoles(killer).First();
|
||||
@@ -63,18 +64,20 @@ public class KarmaListener(IServiceProvider provider) : BaseListener(provider) {
|
||||
case InnocentRole when killerRole is TraitorRole:
|
||||
return;
|
||||
case InnocentRole:
|
||||
victimKarmaDelta = INNO_ON_INNO_VICTIM;
|
||||
killerKarmaDelta = INNO_ON_INNO;
|
||||
victimKarmaDelta = config.INNO_ON_INNO_VICTIM;
|
||||
killerKarmaDelta = config.INNO_ON_INNO;
|
||||
break;
|
||||
case TraitorRole:
|
||||
killerKarmaDelta = killerRole is TraitorRole ?
|
||||
TRAITOR_ON_TRAITOR :
|
||||
INNO_ON_TRAITOR;
|
||||
config.TRAITOR_ON_TRAITOR :
|
||||
config.INNO_ON_TRAITOR;
|
||||
break;
|
||||
case DetectiveRole:
|
||||
killerKarmaDelta = killerRole is TraitorRole ?
|
||||
TRAITOR_ON_DETECTIVE :
|
||||
INNO_ON_DETECTIVE;
|
||||
config.TRAITOR_ON_DETECTIVE :
|
||||
config.INNO_ON_DETECTIVE;
|
||||
if (killerRole is DetectiveRole)
|
||||
victimKarmaDelta = config.INNO_ON_INNO_VICTIM;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -88,18 +91,27 @@ public class KarmaListener(IServiceProvider provider) : BaseListener(provider) {
|
||||
|
||||
[UsedImplicitly]
|
||||
[EventHandler]
|
||||
public Task OnRoundEnd(GameStateUpdateEvent ev) {
|
||||
if (ev.NewState != State.FINISHED) return Task.CompletedTask;
|
||||
public void OnRoundEnd(GameStateUpdateEvent ev) {
|
||||
if (ev.NewState != State.FINISHED) return;
|
||||
|
||||
var tasks = new List<Task>();
|
||||
foreach (var (player, karmaDelta) in queuedKarmaUpdates) {
|
||||
tasks.Add(Task.Run(async () => {
|
||||
var newKarma = await karma.Load(player) + karmaDelta;
|
||||
await karma.Write(player, newKarma);
|
||||
}));
|
||||
var winner = ev.Game.WinningRole;
|
||||
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;
|
||||
await karma.Write(player, newKarma);
|
||||
});
|
||||
|
||||
queuedKarmaUpdates.Clear();
|
||||
return Task.WhenAll(tasks);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
using System.Data;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Data;
|
||||
using System.Reactive.Concurrency;
|
||||
using System.Reactive.Linq;
|
||||
using CounterStrikeSharp.API;
|
||||
using System.Reactive.Threading.Tasks;
|
||||
using Dapper;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@@ -12,91 +13,157 @@ using TTT.Karma.Events;
|
||||
|
||||
namespace TTT.Karma;
|
||||
|
||||
public class KarmaStorage(IServiceProvider provider) : IKarmaService {
|
||||
private static readonly bool enableCache = true;
|
||||
private readonly IEventBus bus = provider.GetRequiredService<IEventBus>();
|
||||
public sealed class KarmaStorage(IServiceProvider provider) : IKarmaService {
|
||||
private readonly IEventBus _bus = provider.GetRequiredService<IEventBus>();
|
||||
|
||||
private readonly KarmaConfig config =
|
||||
provider.GetService<IStorage<KarmaConfig>>()?.Load().Result
|
||||
?? new KarmaConfig();
|
||||
private readonly IScheduler _scheduler =
|
||||
provider.GetRequiredService<IScheduler>();
|
||||
|
||||
private readonly IDictionary<IPlayer, int> karmaCache =
|
||||
new Dictionary<IPlayer, int>();
|
||||
private readonly IStorage<KarmaConfig>? _configStorage =
|
||||
provider.GetService<IStorage<KarmaConfig>>();
|
||||
|
||||
private IDbConnection? connection;
|
||||
// Cache keyed by stable player id to avoid relying on IPlayer equality
|
||||
private readonly ConcurrentDictionary<string, int> _karmaCache = new();
|
||||
|
||||
public void Start() {
|
||||
connection = new SqliteConnection(config.DbString);
|
||||
connection.Open();
|
||||
private KarmaConfig _config = new();
|
||||
private IDbConnection? _connection;
|
||||
private IDisposable? _flushSubscription;
|
||||
private readonly SemaphoreSlim _flushGate = new(1, 1);
|
||||
|
||||
Task.Run(async () => {
|
||||
if (connection is not { State: ConnectionState.Open })
|
||||
throw new InvalidOperationException(
|
||||
"Storage connection is not initialized.");
|
||||
|
||||
await connection.ExecuteAsync("CREATE TABLE IF NOT EXISTS PlayerKarma ("
|
||||
+ "PlayerId TEXT PRIMARY KEY, " + "Karma INTEGER NOT NULL)");
|
||||
});
|
||||
|
||||
var scheduler = provider.GetRequiredService<IScheduler>();
|
||||
|
||||
Observable.Interval(TimeSpan.FromSeconds(30), scheduler)
|
||||
.Subscribe(_ => Task.Run(async () => await updateKarmas()));
|
||||
}
|
||||
|
||||
public async Task<int> Load(IPlayer key) {
|
||||
if (enableCache) {
|
||||
karmaCache.TryGetValue(key, out var cachedKarma);
|
||||
if (cachedKarma != 0) return cachedKarma;
|
||||
}
|
||||
|
||||
if (connection is not { State: ConnectionState.Open })
|
||||
throw new InvalidOperationException(
|
||||
"Storage connection is not initialized.");
|
||||
|
||||
return await connection.QuerySingleAsync<int>(
|
||||
$"SELECT COALESCE((SELECT Karma FROM PlayerKarma WHERE PlayerId = @PlayerId), {config.DefaultKarma})",
|
||||
new { PlayerId = key.Id });
|
||||
}
|
||||
|
||||
public void Dispose() { connection?.Dispose(); }
|
||||
// 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 async Task Write(IPlayer key, int newData) {
|
||||
if (newData > config.MaxKarma(key))
|
||||
throw new ArgumentOutOfRangeException(nameof(newData),
|
||||
$"Karma must be less than {config.MaxKarma(key)} for player {key.Id}.");
|
||||
|
||||
if (!karmaCache.TryGetValue(key, out var oldKarma)) {
|
||||
oldKarma = await Load(key);
|
||||
karmaCache[key] = oldKarma;
|
||||
public void Start() {
|
||||
// Load configuration first
|
||||
if (_configStorage is not null) {
|
||||
// Synchronously wait here since IKarmaService.Start is sync
|
||||
_config = _configStorage.Load().GetAwaiter().GetResult()
|
||||
?? new KarmaConfig();
|
||||
}
|
||||
|
||||
if (oldKarma == newData) return;
|
||||
// Open a dedicated connection used only by this service
|
||||
_connection = new SqliteConnection(_config.DbString);
|
||||
_connection.Open();
|
||||
|
||||
var karmaUpdateEvent = new KarmaUpdateEvent(key, oldKarma, newData);
|
||||
await bus.Dispatch(karmaUpdateEvent);
|
||||
if (karmaUpdateEvent.IsCanceled) return;
|
||||
// Ensure schema before any reads or writes
|
||||
_connection.Execute(@"CREATE TABLE IF NOT EXISTS PlayerKarma (
|
||||
PlayerId TEXT PRIMARY KEY,
|
||||
Karma INTEGER NOT NULL
|
||||
)");
|
||||
|
||||
karmaCache[key] = newData;
|
||||
|
||||
if (!enableCache) await updateKarmas();
|
||||
// Periodic flush with proper error handling and serialization
|
||||
_flushSubscription = Observable
|
||||
.Interval(TimeSpan.FromSeconds(30), _scheduler)
|
||||
.SelectMany(_ => FlushAsync().ToObservable())
|
||||
.Subscribe(_ => { }, // no-op on success
|
||||
ex => {
|
||||
// Replace with your logger if available
|
||||
System.Diagnostics.Trace.TraceError($"Karma flush failed: {ex}");
|
||||
});
|
||||
}
|
||||
|
||||
private async Task updateKarmas() {
|
||||
if (connection is not { State: ConnectionState.Open })
|
||||
public async Task<int> Load(IPlayer player) {
|
||||
if (player is null) throw new ArgumentNullException(nameof(player));
|
||||
var key = player.Id;
|
||||
|
||||
if (EnableCache && _karmaCache.TryGetValue(key, out var cached))
|
||||
return cached;
|
||||
|
||||
var conn = EnsureConnection();
|
||||
|
||||
// Parameterize the default value to keep SQL static
|
||||
var sql = @"
|
||||
SELECT COALESCE(
|
||||
(SELECT Karma FROM PlayerKarma WHERE PlayerId = @PlayerId),
|
||||
@DefaultKarma
|
||||
)";
|
||||
var karma = await conn.QuerySingleAsync<int>(sql,
|
||||
new { PlayerId = key, DefaultKarma = _config.DefaultKarma });
|
||||
|
||||
if (EnableCache) _karmaCache[key] = karma;
|
||||
return karma;
|
||||
}
|
||||
|
||||
public async Task Write(IPlayer player, int newValue) {
|
||||
if (player is null) throw new ArgumentNullException(nameof(player));
|
||||
var key = player.Id;
|
||||
|
||||
var max = _config.MaxKarma(player);
|
||||
if (newValue > max)
|
||||
throw new ArgumentOutOfRangeException(nameof(newValue),
|
||||
$"Karma must be less than {max} for player {key}.");
|
||||
|
||||
int 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.");
|
||||
throw;
|
||||
}
|
||||
|
||||
if (evt.IsCanceled) return;
|
||||
|
||||
_karmaCache[key] = newValue;
|
||||
|
||||
if (!EnableCache) await FlushAsync();
|
||||
}
|
||||
|
||||
private async Task FlushAsync() {
|
||||
var conn = EnsureConnection();
|
||||
|
||||
// Fast path if there is nothing to flush
|
||||
if (_karmaCache.IsEmpty) return;
|
||||
|
||||
await _flushGate.WaitAsync().ConfigureAwait(false);
|
||||
try {
|
||||
// Snapshot to avoid long lock on dictionary and to provide a stable view
|
||||
var snapshot = _karmaCache.ToArray();
|
||||
if (snapshot.Length == 0) return;
|
||||
|
||||
using var tx = conn.BeginTransaction();
|
||||
const string upsert = @"
|
||||
INSERT INTO PlayerKarma (PlayerId, Karma)
|
||||
VALUES (@PlayerId, @Karma)
|
||||
ON CONFLICT(PlayerId) DO UPDATE SET Karma = excluded.Karma
|
||||
";
|
||||
foreach (var (playerId, karma) in snapshot) {
|
||||
await conn.ExecuteAsync(upsert,
|
||||
new { PlayerId = playerId, Karma = karma }, tx);
|
||||
}
|
||||
|
||||
tx.Commit();
|
||||
} finally { _flushGate.Release(); }
|
||||
}
|
||||
|
||||
private IDbConnection EnsureConnection() {
|
||||
if (_connection is not { State: ConnectionState.Open })
|
||||
throw new InvalidOperationException(
|
||||
"Storage connection is not initialized.");
|
||||
return _connection;
|
||||
}
|
||||
|
||||
var tasks = new List<Task>();
|
||||
foreach (var (player, karma) in karmaCache)
|
||||
tasks.Add(connection.ExecuteAsync(
|
||||
"INSERT INTO PlayerKarma (PlayerId, Karma) VALUES (@PlayerId, @Karma) "
|
||||
+ "ON CONFLICT(PlayerId) DO UPDATE SET Karma = @Karma",
|
||||
new { PlayerId = player.Id, Karma = karma }));
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,11 @@
|
||||
namespace TTT.Karma.lang;
|
||||
|
||||
public class KarmaMsgs {
|
||||
public static IMsg KARMA_COMMAND(int karma)
|
||||
=> MsgFactory.Create(nameof(KARMA_COMMAND), karma);
|
||||
public static IMsg KARMA_COMMAND(int karma) {
|
||||
return MsgFactory.Create(nameof(KARMA_COMMAND), karma);
|
||||
}
|
||||
|
||||
public static IMsg KARMA_WARNING(int rounds)
|
||||
=> MsgFactory.Create(nameof(KARMA_WARNING), rounds);
|
||||
public static IMsg KARMA_WARNING(int rounds) {
|
||||
return MsgFactory.Create(nameof(KARMA_WARNING), rounds);
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,8 @@ public class BuyCommand(IServiceProvider provider) : ICommand {
|
||||
|
||||
private readonly IShop shop = provider.GetRequiredService<IShop>();
|
||||
|
||||
private readonly IItemSorter? sorter = provider.GetService<IItemSorter>();
|
||||
|
||||
public void Dispose() { }
|
||||
public string Id => "buy";
|
||||
public void Start() { }
|
||||
@@ -41,7 +43,7 @@ public class BuyCommand(IServiceProvider provider) : ICommand {
|
||||
}
|
||||
|
||||
var query = string.Join(" ", info.Args.Skip(1));
|
||||
var item = searchItem(query);
|
||||
var item = searchItem(executor, query);
|
||||
|
||||
if (item == null) {
|
||||
info.ReplySync(locale[ShopMsgs.SHOP_ITEM_NOT_FOUND(query)]);
|
||||
@@ -54,7 +56,13 @@ public class BuyCommand(IServiceProvider provider) : ICommand {
|
||||
CommandResult.ERROR);
|
||||
}
|
||||
|
||||
private IShopItem? searchItem(string query) {
|
||||
private IShopItem? searchItem(IOnlinePlayer? player, string query) {
|
||||
if (sorter != null && int.TryParse(query, out var id)) {
|
||||
var items = sorter.GetSortedItems(player);
|
||||
if (id >= 0 && id < items.Count) return items[id];
|
||||
return null;
|
||||
}
|
||||
|
||||
var item = shop.Items.FirstOrDefault(it
|
||||
=> it.Name.Equals(query, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
|
||||
@@ -1,16 +1,31 @@
|
||||
using CounterStrikeSharp.API.Modules.Utils;
|
||||
using System.Runtime.CompilerServices;
|
||||
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 {
|
||||
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 IDictionary<IOnlinePlayer, DateTime> lastUpdate =
|
||||
new Dictionary<IOnlinePlayer, DateTime>();
|
||||
|
||||
private readonly IRoleAssigner roles = provider
|
||||
.GetRequiredService<IRoleAssigner>();
|
||||
|
||||
private readonly IMsgLocalizer locale = provider
|
||||
.GetRequiredService<IMsgLocalizer>();
|
||||
|
||||
private readonly IShop shop = provider.GetRequiredService<IShop>();
|
||||
|
||||
public void Dispose() { }
|
||||
@@ -21,19 +36,44 @@ public class ListCommand(IServiceProvider provider) : ICommand {
|
||||
|
||||
public async Task<CommandResult> Execute(IOnlinePlayer? executor,
|
||||
ICommandInfo info) {
|
||||
var items = new List<IShopItem>(shop.Items).Where(item
|
||||
=> executor == null
|
||||
|| games.ActiveGame is not { State: State.IN_PROGRESS }
|
||||
|| item.CanPurchase(executor) != PurchaseResult.WRONG_ROLE)
|
||||
.ToList();
|
||||
var items = calculateSortedItems(executor);
|
||||
|
||||
if (executor != null) cache[executor] = items;
|
||||
items = new List<IShopItem>(items);
|
||||
items.Reverse();
|
||||
|
||||
var balance = executor == null ? int.MaxValue : await shop.Load(executor);
|
||||
|
||||
foreach (var (index, item) in items.Select((value, i) => (i, value))) {
|
||||
var canPurchase = executor == null
|
||||
|| item.CanPurchase(executor) == PurchaseResult.SUCCESS;
|
||||
canPurchase = canPurchase && item.Config.Price <= balance;
|
||||
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;
|
||||
}
|
||||
|
||||
private List<IShopItem> calculateSortedItems(IOnlinePlayer? player) {
|
||||
var items = new List<IShopItem>(shop.Items).Where(item
|
||||
=> player == null
|
||||
|| games.ActiveGame is not { State: State.IN_PROGRESS }
|
||||
|| item.CanPurchase(player) != PurchaseResult.WRONG_ROLE)
|
||||
.ToList();
|
||||
items.Sort((a, b) => {
|
||||
var aPrice = a.Config.Price;
|
||||
var bPrice = b.Config.Price;
|
||||
var aCanBuy = executor != null
|
||||
&& a.CanPurchase(executor) == PurchaseResult.SUCCESS;
|
||||
var bCanBuy = executor != null
|
||||
&& b.CanPurchase(executor) == PurchaseResult.SUCCESS;
|
||||
var aCanBuy = player != null
|
||||
&& a.CanPurchase(player) == PurchaseResult.SUCCESS;
|
||||
var bCanBuy = player != null
|
||||
&& b.CanPurchase(player) == PurchaseResult.SUCCESS;
|
||||
|
||||
if (aCanBuy && !bCanBuy) return -1;
|
||||
if (!aCanBuy && bCanBuy) return 1;
|
||||
@@ -41,29 +81,40 @@ public class ListCommand(IServiceProvider provider) : ICommand {
|
||||
return string.Compare(a.Name, b.Name, StringComparison.Ordinal);
|
||||
});
|
||||
|
||||
var balance = info.CallingPlayer == null ?
|
||||
int.MaxValue :
|
||||
await shop.Load(info.CallingPlayer);
|
||||
|
||||
foreach (var item in items)
|
||||
info.ReplySync(formatItem(item,
|
||||
item.Config.Price <= balance
|
||||
&& item.CanPurchase(info.CallingPlayer ?? executor!)
|
||||
== PurchaseResult.SUCCESS));
|
||||
|
||||
return CommandResult.SUCCESS;
|
||||
if (player != null) lastUpdate[player] = DateTime.Now;
|
||||
return items;
|
||||
}
|
||||
|
||||
private string formatPrefix(IShopItem item, bool canBuy = true) {
|
||||
private string formatPrefix(IShopItem item, int index, bool canBuy) {
|
||||
if (!canBuy)
|
||||
return
|
||||
$" {ChatColors.Grey}- [{ChatColors.DarkRed}{item.Config.Price}{ChatColors.Grey}] {ChatColors.Red}{item.Name}";
|
||||
|
||||
if (index > 9) {
|
||||
return
|
||||
$" {ChatColors.Default}- [{ChatColors.Yellow}{item.Config.Price}{ChatColors.Default}] {ChatColors.Green}{item.Name}";
|
||||
}
|
||||
|
||||
return
|
||||
$" {ChatColors.Default}- [{ChatColors.Yellow}{item.Config.Price}{ChatColors.Default}] {ChatColors.Green}{item.Name}";
|
||||
$" {ChatColors.Blue}/{index} {ChatColors.Default}| [{ChatColors.Yellow}{item.Config.Price}{ChatColors.Default}] {ChatColors.Green}{item.Name}";
|
||||
}
|
||||
|
||||
private string formatItem(IShopItem item, bool canBuy) {
|
||||
private string formatItem(IShopItem item, int index, bool canBuy) {
|
||||
return
|
||||
$" {formatPrefix(item, canBuy)} {ChatColors.Grey} | {item.Description}";
|
||||
$" {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;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using CounterStrikeSharp.API.Modules.Utils;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using ShopAPI;
|
||||
using TTT.API.Command;
|
||||
using TTT.API.Player;
|
||||
using TTT.Game;
|
||||
@@ -7,28 +8,38 @@ using TTT.Locale;
|
||||
|
||||
namespace TTT.Shop.Commands;
|
||||
|
||||
public class ShopCommand(IServiceProvider provider) : ICommand {
|
||||
public class ShopCommand(IServiceProvider provider) : ICommand, IItemSorter {
|
||||
private readonly IMsgLocalizer locale = provider
|
||||
.GetRequiredService<IMsgLocalizer>();
|
||||
|
||||
private readonly Dictionary<string, ICommand> subcommands = new() {
|
||||
["list"] = new ListCommand(provider),
|
||||
["buy"] = new BuyCommand(provider),
|
||||
["balance"] = new BalanceCommand(provider),
|
||||
["bal"] = new BalanceCommand(provider)
|
||||
};
|
||||
private readonly ListCommand listCmd = new(provider);
|
||||
|
||||
private Dictionary<string, ICommand>? subcommands;
|
||||
|
||||
public void Dispose() { }
|
||||
public string Id => "shop";
|
||||
public string[] Usage => ["list", "buy [item]", "balance"];
|
||||
|
||||
public void Start() { }
|
||||
public void Start() {
|
||||
subcommands = new Dictionary<string, ICommand> {
|
||||
["list"] = listCmd,
|
||||
["buy"] = new BuyCommand(provider),
|
||||
["balance"] = new BalanceCommand(provider),
|
||||
["bal"] = new BalanceCommand(provider)
|
||||
};
|
||||
}
|
||||
|
||||
public bool MustBeOnMainThread => true;
|
||||
|
||||
public Task<CommandResult>
|
||||
Execute(IOnlinePlayer? executor, ICommandInfo info) {
|
||||
HashSet<string> sent = [];
|
||||
if (subcommands == null) {
|
||||
info.ReplySync(
|
||||
$"{locale[GameMsgs.PREFIX]}{ChatColors.Red}No subcommands available.");
|
||||
return Task.FromResult(CommandResult.ERROR);
|
||||
}
|
||||
|
||||
if (info.ArgCount == 1) {
|
||||
foreach (var (_, cmd) in subcommands) {
|
||||
if (!sent.Add(cmd.Id)) continue;
|
||||
@@ -52,4 +63,13 @@ public class ShopCommand(IServiceProvider provider) : ICommand {
|
||||
return command.Execute(executor, info.Skip());
|
||||
return Task.FromResult(CommandResult.ERROR);
|
||||
}
|
||||
|
||||
public List<IShopItem> GetSortedItems(IOnlinePlayer? player,
|
||||
bool refresh = false) {
|
||||
return listCmd.GetSortedItems(player, refresh);
|
||||
}
|
||||
|
||||
public DateTime? GetLastUpdate(IOnlinePlayer? player) {
|
||||
return listCmd.GetLastUpdate(player);
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,7 @@ public class Stickers(IServiceProvider provider)
|
||||
public override void OnPurchase(IOnlinePlayer player) { }
|
||||
|
||||
public override PurchaseResult CanPurchase(IOnlinePlayer player) {
|
||||
if (Shop.HasItem(player, this)) return PurchaseResult.ALREADY_OWNED;
|
||||
if (Shop.HasItem<Stickers>(player)) return PurchaseResult.ALREADY_OWNED;
|
||||
return base.CanPurchase(player);
|
||||
}
|
||||
}
|
||||
@@ -45,7 +45,7 @@ public class DeagleDamageListener(IServiceProvider provider)
|
||||
var attackerRole = Roles.GetRoles(attacker);
|
||||
var victimRole = Roles.GetRoles(victim);
|
||||
|
||||
shop.RemoveItem(attacker, deagleItem);
|
||||
shop.RemoveItem<OneShotDeagleItem>(attacker);
|
||||
var attackerIsTraitor = attackerRole.Any(r => r is TraitorRole);
|
||||
var victimIsTraitor = victimRole.Any(r => r is TraitorRole);
|
||||
if (attackerIsTraitor == victimIsTraitor) {
|
||||
|
||||
@@ -16,31 +16,34 @@ public class PlayerKillListener(IServiceProvider provider)
|
||||
|
||||
[UsedImplicitly]
|
||||
[EventHandler]
|
||||
public async Task OnKill(PlayerDeathEvent ev) {
|
||||
public void OnKill(PlayerDeathEvent ev) {
|
||||
if (Games.ActiveGame is not { State: State.IN_PROGRESS }) return;
|
||||
if (ev.Killer == null) return;
|
||||
var victimBal = await shop.Load(ev.Victim);
|
||||
|
||||
shop.AddBalance(ev.Killer, victimBal / 6, "Killed " + ev.Victim.Name);
|
||||
Task.Run(async () => {
|
||||
var victimBal = await shop.Load(ev.Victim);
|
||||
shop.AddBalance(ev.Killer, victimBal / 6, "Killed " + ev.Victim.Name);
|
||||
});
|
||||
}
|
||||
|
||||
[UsedImplicitly]
|
||||
[EventHandler(IgnoreCanceled = true)]
|
||||
public async Task OnIdentify(BodyIdentifyEvent ev) {
|
||||
public void OnIdentify(BodyIdentifyEvent ev) {
|
||||
if (ev.Identifier == null) return;
|
||||
var victimBal = await shop.Load(ev.Body.OfPlayer);
|
||||
shop.AddBalance(ev.Identifier, victimBal / 4,
|
||||
"Identified " + ev.Body.OfPlayer.Name);
|
||||
Task.Run(async () => {
|
||||
var victimBal = await shop.Load(ev.Body.OfPlayer);
|
||||
shop.AddBalance(ev.Identifier, victimBal / 4,
|
||||
"Identified " + ev.Body.OfPlayer.Name);
|
||||
|
||||
if (ev.Body.Killer is not IOnlinePlayer killer) return;
|
||||
if (ev.Body.Killer is not IOnlinePlayer killer) return;
|
||||
|
||||
if (!isGoodKill(ev.Body.Killer, ev.Body.OfPlayer)) {
|
||||
var killerBal = await shop.Load(killer);
|
||||
shop.AddBalance(killer, -killerBal / 4 - victimBal / 2, "Bad Kill");
|
||||
return;
|
||||
}
|
||||
if (!isGoodKill(ev.Body.Killer, ev.Body.OfPlayer)) {
|
||||
var killerBal = await shop.Load(killer);
|
||||
shop.AddBalance(killer, -killerBal / 4 - victimBal / 2, "Bad Kill");
|
||||
return;
|
||||
}
|
||||
|
||||
shop.AddBalance(killer, victimBal / 4, "Good Kill");
|
||||
shop.AddBalance(killer, victimBal / 4, "Good Kill");
|
||||
});
|
||||
}
|
||||
|
||||
private bool isGoodKill(IPlayer attacker, IPlayer victim) {
|
||||
|
||||
@@ -17,32 +17,36 @@ public class RoleAssignCreditor(IServiceProvider provider)
|
||||
provider.GetService<IStorage<ShopConfig>>()?.Load().GetAwaiter().GetResult()
|
||||
?? new ShopConfig(provider);
|
||||
|
||||
private readonly IShop shop = provider.GetRequiredService<IShop>();
|
||||
|
||||
private readonly IKarmaService? karmaService =
|
||||
provider.GetService<IKarmaService>();
|
||||
|
||||
private readonly KarmaConfig karmaConfig =
|
||||
provider.GetService<IStorage<KarmaConfig>>()
|
||||
?.Load()
|
||||
.GetAwaiter()
|
||||
.GetResult() ?? new KarmaConfig();
|
||||
|
||||
private readonly IKarmaService? karmaService =
|
||||
provider.GetService<IKarmaService>();
|
||||
|
||||
private readonly IShop shop = provider.GetRequiredService<IShop>();
|
||||
|
||||
[UsedImplicitly]
|
||||
[EventHandler]
|
||||
public async Task OnRoleAssign(PlayerRoleAssignEvent ev) {
|
||||
public void OnRoleAssign(PlayerRoleAssignEvent ev) {
|
||||
var toGive = config.StartingCreditsForRole(ev.Role);
|
||||
if (ev.Player is not IOnlinePlayer online) return;
|
||||
|
||||
if (karmaService != null) {
|
||||
if (karmaService == null) {
|
||||
shop.AddBalance(online, toGive, "Round Start", false);
|
||||
return;
|
||||
}
|
||||
|
||||
Task.Run(async () => {
|
||||
var karma = await karmaService.Load(ev.Player);
|
||||
var percent = (karma + karmaConfig.MinKarma)
|
||||
/ (float)karmaConfig.MaxKarma(ev.Player);
|
||||
var givenScale = toGive * getKarmaScale(percent);
|
||||
toGive = (int)Math.Ceiling(givenScale);
|
||||
}
|
||||
|
||||
shop.AddBalance(online, toGive, "Round Start", false);
|
||||
shop.AddBalance(online, toGive, "Round Start", false);
|
||||
});
|
||||
}
|
||||
|
||||
private float getKarmaScale(float percent) {
|
||||
|
||||
@@ -3,7 +3,6 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
using ShopAPI;
|
||||
using TTT.API.Events;
|
||||
using TTT.API.Game;
|
||||
using TTT.CS2.lang;
|
||||
using TTT.Game.Events.Player;
|
||||
using TTT.Game.Listeners;
|
||||
|
||||
|
||||
@@ -20,14 +20,14 @@ public class PeriodicRewarder(IServiceProvider provider) : ITerrorModule {
|
||||
private readonly IPlayerFinder finder =
|
||||
provider.GetRequiredService<IPlayerFinder>();
|
||||
|
||||
private readonly IGameManager games =
|
||||
provider.GetRequiredService<IGameManager>();
|
||||
|
||||
private readonly IScheduler scheduler =
|
||||
provider.GetRequiredService<IScheduler>();
|
||||
|
||||
private readonly IShop shop = provider.GetRequiredService<IShop>();
|
||||
|
||||
private readonly IGameManager games =
|
||||
provider.GetRequiredService<IGameManager>();
|
||||
|
||||
private IDisposable? timer;
|
||||
|
||||
public void Dispose() { timer?.Dispose(); }
|
||||
|
||||
@@ -105,8 +105,10 @@ public class Shop(IServiceProvider provider) : ITerrorModule, IShop {
|
||||
return items.GetValueOrDefault(player.Id, []);
|
||||
}
|
||||
|
||||
public void RemoveItem(IOnlinePlayer player, IShopItem item) {
|
||||
public void RemoveItem<T>(IOnlinePlayer player) where T : IShopItem {
|
||||
if (!items.TryGetValue(player.Id, out var itemList)) return;
|
||||
var item = itemList.FirstOrDefault(i => i is T);
|
||||
if (item == null) return;
|
||||
itemList.Remove(item);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -4,10 +4,12 @@ using TTT.API.Extensions;
|
||||
using TTT.CS2.Items.Armor;
|
||||
using TTT.CS2.Items.BodyPaint;
|
||||
using TTT.CS2.Items.Camouflage;
|
||||
using TTT.CS2.Items.Compass;
|
||||
using TTT.CS2.Items.DNA;
|
||||
using TTT.CS2.Items.OneHitKnife;
|
||||
using TTT.CS2.Items.PoisonShots;
|
||||
using TTT.CS2.Items.PoisonSmoke;
|
||||
using TTT.CS2.Items.SilentAWP;
|
||||
using TTT.CS2.Items.Station;
|
||||
using TTT.Shop.Commands;
|
||||
using TTT.Shop.Items;
|
||||
@@ -31,7 +33,7 @@ public static class ShopServiceCollection {
|
||||
collection.AddModBehavior<PeriodicRewarder>();
|
||||
collection.AddModBehavior<TaseRewarder>();
|
||||
|
||||
collection.AddModBehavior<ShopCommand>();
|
||||
collection.AddModBehavior<IItemSorter, ShopCommand>();
|
||||
collection.AddModBehavior<BuyCommand>();
|
||||
collection.AddModBehavior<BalanceCommand>();
|
||||
|
||||
@@ -39,6 +41,7 @@ public static class ShopServiceCollection {
|
||||
collection.AddBodyPaintServices();
|
||||
collection.AddC4Services();
|
||||
collection.AddCamoServices();
|
||||
collection.AddCompassServices();
|
||||
collection.AddDamageStation();
|
||||
collection.AddDeagleServices();
|
||||
collection.AddDnaScannerServices();
|
||||
@@ -49,6 +52,7 @@ public static class ShopServiceCollection {
|
||||
collection.AddOneHitKnifeService();
|
||||
collection.AddPoisonShots();
|
||||
collection.AddPoisonSmoke();
|
||||
collection.AddSilentAWPServices();
|
||||
collection.AddStickerServices();
|
||||
collection.AddTaserItem();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using CounterStrikeSharp.API.Modules.Utils;
|
||||
using ShopAPI;
|
||||
using TTT.API.Role;
|
||||
using TTT.Game;
|
||||
using TTT.Locale;
|
||||
|
||||
namespace TTT.Shop;
|
||||
@@ -48,4 +50,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 a%n% {0}, you have {yellow}{1}{grey} %CREDITS_NAME%%s%."
|
||||
|
||||
CREDITS_NAME: "credit"
|
||||
CREDITS_GIVEN: "%SHOP_PREFIX%{0}{1} %CREDITS_NAME%%s%"
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using ShopAPI.Configs;
|
||||
using TTT.API.Game;
|
||||
using TTT.API.Messages;
|
||||
using TTT.API.Player;
|
||||
using TTT.API.Role;
|
||||
using TTT.Locale;
|
||||
@@ -7,12 +9,21 @@ using TTT.Locale;
|
||||
namespace ShopAPI;
|
||||
|
||||
public abstract class BaseItem(IServiceProvider provider) : IShopItem {
|
||||
protected readonly IPlayerFinder Finder =
|
||||
provider.GetRequiredService<IPlayerFinder>();
|
||||
|
||||
protected readonly IGameManager Games =
|
||||
provider.GetRequiredService<IGameManager>();
|
||||
|
||||
protected readonly IInventoryManager Inventory =
|
||||
provider.GetRequiredService<IInventoryManager>();
|
||||
|
||||
protected readonly IMsgLocalizer Locale =
|
||||
provider.GetRequiredService<IMsgLocalizer>();
|
||||
|
||||
protected readonly IMessenger Messenger =
|
||||
provider.GetRequiredService<IMessenger>();
|
||||
|
||||
protected readonly IServiceProvider Provider = provider;
|
||||
|
||||
protected readonly IRoleAssigner Roles =
|
||||
|
||||
8
TTT/ShopAPI/Configs/Traitor/CompassConfig.cs
Normal file
8
TTT/ShopAPI/Configs/Traitor/CompassConfig.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace ShopAPI.Configs.Traitor;
|
||||
|
||||
public record CompassConfig : ShopItemConfig {
|
||||
public override int Price { get; init; } = 70;
|
||||
public float MaxRange { get; init; } = 10000;
|
||||
public float CompassFOV { get; init; } = 120;
|
||||
public int CompassLength { get; init; } = 64;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
11
TTT/ShopAPI/Configs/Traitor/SilentAWPConfig.cs
Normal file
11
TTT/ShopAPI/Configs/Traitor/SilentAWPConfig.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using TTT.API;
|
||||
|
||||
namespace ShopAPI.Configs.Traitor;
|
||||
|
||||
public record SilentAWPConfig : ShopItemConfig, IWeapon {
|
||||
public override int Price { get; init; } = 90;
|
||||
public int WeaponIndex { get; } = 9;
|
||||
public string WeaponId { get; } = "weapon_awp";
|
||||
public int? ReserveAmmo { get; } = 0;
|
||||
public int? CurrentAmmo { get; } = 2;
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
8
TTT/ShopAPI/IItemSorter.cs
Normal file
8
TTT/ShopAPI/IItemSorter.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using TTT.API.Player;
|
||||
|
||||
namespace ShopAPI;
|
||||
|
||||
public interface IItemSorter {
|
||||
List<IShopItem> GetSortedItems(IOnlinePlayer? player, bool refresh = false);
|
||||
DateTime? GetLastUpdate(IOnlinePlayer? player);
|
||||
}
|
||||
@@ -21,18 +21,9 @@ public interface IShop : IKeyedStorage<IPlayer, int>,
|
||||
void GiveItem(IOnlinePlayer player, IShopItem item);
|
||||
IList<IShopItem> GetOwnedItems(IOnlinePlayer player);
|
||||
|
||||
bool HasItem(IOnlinePlayer player, IShopItem item) {
|
||||
return GetOwnedItems(player).Any(i => i.Id == item.Id);
|
||||
}
|
||||
|
||||
bool HasItem<T>(IOnlinePlayer player) where T : IShopItem {
|
||||
return GetOwnedItems(player).Any(i => i is T);
|
||||
}
|
||||
|
||||
void RemoveItem(IOnlinePlayer player, IShopItem item);
|
||||
|
||||
void RemoveItem<T>(IOnlinePlayer player) where T : IShopItem {
|
||||
var owned = GetOwnedItems(player).FirstOrDefault(i => i is T);
|
||||
if (owned != null) RemoveItem(player, owned);
|
||||
}
|
||||
void RemoveItem<T>(IOnlinePlayer player) where T : IShopItem;
|
||||
}
|
||||
@@ -17,7 +17,7 @@ public class MemoryKarmaStorage(IEventBus bus)
|
||||
public override async Task Write(IPlayer key, int value) {
|
||||
var old = await Load(key);
|
||||
var karmaEvent = new KarmaUpdateEvent(key, old, value);
|
||||
await bus.Dispatch(karmaEvent);
|
||||
bus.Dispatch(karmaEvent);
|
||||
|
||||
if (karmaEvent.IsCanceled) return;
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ public class KarmaListenerTests {
|
||||
var deathEvent = new PlayerDeathEvent(victim);
|
||||
deathEvent.WithKiller(attacker);
|
||||
|
||||
await bus.Dispatch(deathEvent);
|
||||
bus.Dispatch(deathEvent);
|
||||
|
||||
var victimKarma = await karma.Load(victim);
|
||||
var attackerKarma = await karma.Load(attacker);
|
||||
@@ -66,7 +66,7 @@ public class KarmaListenerTests {
|
||||
[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.Detective, 44, 50)]
|
||||
[InlineData(RoleEnum.Detective, RoleEnum.Detective, 44, 49)]
|
||||
public async Task OnKill_AffectsKarma(RoleEnum attackerRole,
|
||||
RoleEnum victimRole, int expAttackerKarma, int expVictimKarma) {
|
||||
var victim = TestPlayer.Random();
|
||||
@@ -85,9 +85,13 @@ public class KarmaListenerTests {
|
||||
var deathEvent = new PlayerDeathEvent(victim);
|
||||
deathEvent.WithKiller(attacker);
|
||||
|
||||
await bus.Dispatch(deathEvent);
|
||||
bus.Dispatch(deathEvent);
|
||||
game.EndGame();
|
||||
|
||||
await Task.Delay(TimeSpan.FromMilliseconds(20),
|
||||
TestContext.Current
|
||||
.CancellationToken); // Wait for the karma update to process
|
||||
|
||||
var victimKarma = await karma.Load(victim);
|
||||
var attackerKarma = await karma.Load(attacker);
|
||||
|
||||
@@ -120,12 +124,16 @@ public class KarmaListenerTests {
|
||||
var deathEvent3 = new PlayerDeathEvent(victim3);
|
||||
deathEvent3.WithKiller(attacker);
|
||||
|
||||
await bus.Dispatch(deathEvent1); // First kill => 50 - (4*1) = 46
|
||||
await bus.Dispatch(deathEvent2); // Second kill => 46 - (4*2) = 38
|
||||
await bus.Dispatch(
|
||||
deathEvent3); // Third kill (detective) => 38 - (6*3) = 20
|
||||
bus.Dispatch(deathEvent1); // First kill => 50 - (4*1) = 46
|
||||
bus.Dispatch(deathEvent2); // Second kill => 46 - (4*2) = 38
|
||||
bus.Dispatch(deathEvent3); // Third kill (detective) => 38 - (6*3) = 20
|
||||
|
||||
game.EndGame();
|
||||
|
||||
await Task.Delay(TimeSpan.FromMilliseconds(20),
|
||||
TestContext.Current
|
||||
.CancellationToken); // Wait for the karma update to process
|
||||
|
||||
var killerKarma = await karma.Load(attacker);
|
||||
Assert.Equal(20, killerKarma);
|
||||
}
|
||||
@@ -155,9 +163,10 @@ public class KarmaListenerTests {
|
||||
var deathEvent3 = new PlayerDeathEvent(victim3);
|
||||
deathEvent3.WithKiller(attacker);
|
||||
|
||||
await bus.Dispatch(deathEvent1); // First kill => 50 + 2 = 52
|
||||
await bus.Dispatch(deathEvent2); // Second kill => 52 + 2 = 54
|
||||
await bus.Dispatch(deathEvent3); // Third kill (inno) => 54 - 4 = 50
|
||||
bus.Dispatch(deathEvent1); // First kill => 50 + 2 = 52
|
||||
bus.Dispatch(deathEvent2); // Second kill => 52 + 2 = 54
|
||||
bus.Dispatch(deathEvent3); // Third kill (inno) => 54 - 4 = 50
|
||||
|
||||
var killerKarma = await karma.Load(attacker);
|
||||
Assert.Equal(50, killerKarma);
|
||||
}
|
||||
|
||||
@@ -57,6 +57,9 @@ public class BalanceClearTest(IServiceProvider provider) {
|
||||
var game = games.CreateGame();
|
||||
game?.Start();
|
||||
|
||||
await Task.Delay(TimeSpan.FromMilliseconds(10),
|
||||
TestContext.Current.CancellationToken);
|
||||
|
||||
var newBalance = await shop.Load(player);
|
||||
|
||||
var expected = 100;
|
||||
|
||||
@@ -38,9 +38,7 @@ public class DeagleTests {
|
||||
shop.GiveItem(testPlayer, item);
|
||||
|
||||
var playerDmgEvent =
|
||||
new PlayerDamagedEvent(victim, testPlayer, 99) {
|
||||
Weapon = item.WeaponId
|
||||
};
|
||||
new PlayerDamagedEvent(victim, testPlayer, 1) { Weapon = item.WeaponId };
|
||||
bus.Dispatch(playerDmgEvent);
|
||||
|
||||
Assert.Equal(0, victim.Health);
|
||||
@@ -53,13 +51,11 @@ public class DeagleTests {
|
||||
shop.GiveItem(testPlayer, item);
|
||||
|
||||
var playerDmgEvent =
|
||||
new PlayerDamagedEvent(victim, testPlayer, 99) {
|
||||
Weapon = item.WeaponId
|
||||
};
|
||||
new PlayerDamagedEvent(victim, testPlayer, 1) { Weapon = item.WeaponId };
|
||||
bus.Dispatch(playerDmgEvent);
|
||||
|
||||
var secondDmgEvent =
|
||||
new PlayerDamagedEvent(survivor, testPlayer, 99) {
|
||||
new PlayerDamagedEvent(survivor, testPlayer, 1) {
|
||||
Weapon = item.WeaponId
|
||||
};
|
||||
bus.Dispatch(secondDmgEvent);
|
||||
|
||||
63
licenses/Microsoft.Data.Sqlite__9.0.9.html
Normal file
63
licenses/Microsoft.Data.Sqlite__9.0.9.html
Normal file
@@ -0,0 +1,63 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<link rel="stylesheet" href="/Content/Site.css" />
|
||||
<title>'MIT' reference</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="main-content">
|
||||
|
||||
|
||||
|
||||
<h1>MIT License</h1>
|
||||
|
||||
<h2>SPDX identifier</h2>
|
||||
<div id="license-expression">MIT</div>
|
||||
|
||||
<h2>License text</h2>
|
||||
|
||||
<div class="optional-license-text">
|
||||
<p>MIT License</p>
|
||||
|
||||
</div>
|
||||
<div class="replaceable-license-text">
|
||||
<p>Copyright (c) <year> <copyright holders>
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
||||
<p>Permission is hereby granted, free of charge, to any person obtaining a copy of <var class="replaceable-license-text"> this software and
|
||||
associated documentation files</var> (the "Software"), to deal in the Software without restriction,
|
||||
including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:</p>
|
||||
|
||||
<p>The above copyright notice and this permission notice
|
||||
<var class="optional-license-text"> (including the next paragraph)</var>
|
||||
shall be included in all copies or substantial
|
||||
portions of the Software.</p>
|
||||
|
||||
<p>THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
||||
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
|
||||
NO EVENT SHALL <var class="replaceable-license-text"> THE AUTHORS OR COPYRIGHT HOLDERS</var> BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.</p>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<h2>SPDX web page</h2>
|
||||
<ul>
|
||||
<li><a href="https://spdx.org/licenses/MIT.html">https://spdx.org/licenses/MIT.html</a></li>
|
||||
</ul>
|
||||
|
||||
<h2>Notice</h2>
|
||||
<p>This license content is provided by the <a href="https://spdx.dev/">SPDX project</a>. For more information about <b>licenses.nuget.org</b>, see <a href="https://aka.ms/licenses.nuget.org">our documentation</a>.
|
||||
|
||||
<p><i>Data pulled from <a href="https://github.com/spdx/license-list-data">spdx/license-list-data</a> on November 6, 2024.</i></p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user