Compare commits

...

5 Commits

Author SHA1 Message Date
MSWS
1a4e5e3e77 +semver:minor 2025-09-28 01:24:45 -07:00
MSWS
b427dc370e refactor: Remove debug logs and adjust event priorities
- Remove debug logging statements and simplify service registration logic in `ServiceCollectionExtensions.cs`
- Adjust the event handler's priority in `RoundShopClearer.cs` without functional changes
- Update wording in `README.md` for clearer public API usage
- Modify event handler priority in `PlayerStatsTracker.cs` while maintaining existing functionality
- Streamline `IOnlinePlayer.cs` by removing obsolete commented-out code and refining interface properties
- Lower event handler priority in `PlayerActionsLogger.cs` for player kills
2025-09-28 00:46:44 -07:00
MSWS
ede9badbd9 Add additional unit tests for shop 2025-09-28 00:34:55 -07:00
MSWS
5736588484 refactor: Rename Id to WeaponId across the codebase
- Rename property `Id` to `WeaponId` in `IWeapon.cs`, `BaseWeapon.cs`, and `OneShotDeagle.cs` for improved clarity.
- Update weapon removal method in `IInventoryManager.cs` to use `weapon.WeaponId`.
- Refactor `PlayerDamagedEvent.cs` to initialize `Weapon` property with `init` for stricter immutability.
- Revise `IsAlive` logic in `TestPlayer.cs` to adjust `Health` based on `IsAlive` status; deprecate the `Roles` property.
- Add `using` directive and `[UsedImplicitly]` attribute to `DeagleDamageListener.cs` for dependency management and traceability.
- Develop `DeagleTests.cs` to ensure proper functionality of Deagle weapon behaviors using Xunit.
2025-09-28 00:16:28 -07:00
MSWS
8a894c65e8 refactor: Adjust player color handling logic
- Adjust the alpha value handling in `SetColor` method within `PlayerExtensions.cs` to ensure it stays within limits.
- Simplify player color setting in `RoundTimerListener.cs` by using `Color.White` for improved readability.
- Update `BodySpawner.cs` to change player post-death color to fully opaque white and simplify round start color setting using `Color.White`.
2025-09-27 20:30:07 -07:00
19 changed files with 152 additions and 32 deletions

View File

@@ -13,28 +13,16 @@ public static class ServiceCollectionExtensions {
this IServiceCollection collection)
where TExtension : class, ITerrorModule {
if (typeof(TExtension).IsAssignableTo(typeof(IPluginModule))) {
# if DEBUG
Console.WriteLine(
$"[DEBUG] Registering {typeof(TExtension).Name} as IPluginModule");
# endif
collection.AddTransient<IPluginModule>(provider
=> (provider.GetRequiredService<TExtension>() as IPluginModule)!);
}
if (typeof(TExtension).IsAssignableTo(typeof(IListener))) {
#if DEBUG
Console.WriteLine(
$"[DEBUG] Registering {typeof(TExtension).Name} as IListener");
# endif
collection.AddTransient<IListener>(provider
=> (provider.GetRequiredService<TExtension>() as IListener)!);
}
if (typeof(TExtension).IsAssignableTo(typeof(ICommand))) {
#if DEBUG
Console.WriteLine(
$"[DEBUG] Registering {typeof(TExtension).Name} as ICommand");
#endif
collection.AddTransient<ICommand>(provider
=> (provider.GetRequiredService<TExtension>() as ICommand)!);
}

View File

@@ -4,7 +4,7 @@ public interface IWeapon {
/// <summary>
/// The internal ID of the weapon, should match the ID of the weapon in the underlying game.
/// </summary>
public string Id { get; }
public string WeaponId { get; }
/// <summary>
/// The amount of ammo that is in reserve for this weapon.

View File

@@ -16,7 +16,7 @@ public interface IInventoryManager {
void RemoveWeapon(IOnlinePlayer player, string weaponId);
void RemoveWeapon(IOnlinePlayer player, IWeapon weapon) {
RemoveWeapon(player, weapon.Id);
RemoveWeapon(player, weapon.WeaponId);
}
void RemoveWeaponInSlot(IOnlinePlayer player, int slot);

View File

@@ -1,10 +1,6 @@
namespace TTT.API.Player;
public interface IOnlinePlayer : IPlayer {
// [Obsolete(
// "Roles are now managed via IRoleAssigner. Use IRoleAssigner.GetRoles(IPlayer) instead.")]
// ICollection<IRole> Roles { get; }
public int Health { get; set; }
public int MaxHealth { get; set; }
public int Armor { get; set; }

View File

@@ -32,7 +32,8 @@ public static class PlayerExtensions {
if (!player.IsValid || pawn == null || !pawn.IsValid) return;
if (color.A == 255)
color = Color.FromArgb(pawn.Render.A, color.R, color.G, color.B);
color = Color.FromArgb(pawn.Render.A == 255 ? 255 : 254, color.R, color.G,
color.B);
pawn.SetColor(color);
}

View File

@@ -33,7 +33,7 @@ public class BodySpawner(IServiceProvider provider) : IPluginModule {
return HookResult.Continue;
var player = ev.Userid;
if (player == null || !player.IsValid) return HookResult.Continue;
player.SetColor(Color.FromArgb(0, 0, 0, 0));
player.SetColor(Color.FromArgb(0, 255, 255, 255));
var ragdollBody = makeGameRagdoll(player);
var body = new CS2Body(provider, ragdollBody, converter.GetPlayer(player));
@@ -54,7 +54,7 @@ public class BodySpawner(IServiceProvider provider) : IPluginModule {
public HookResult OnStart(EventRoundStart ev, GameEventInfo _) {
Server.NextWorldUpdate(() => {
foreach (var player in Utilities.GetPlayers())
player.SetColor(Color.FromArgb(254, 255, 255, 255));
player.SetColor(Color.White);
});
return HookResult.Continue;
}

View File

@@ -44,7 +44,7 @@ public class PlayerStatsTracker(IServiceProvider provider) : IListener {
// Needs to be higher so we detect the kill before the game ends
// in the case that this is the last player
[EventHandler(Priority = Priority.HIGHER)]
[EventHandler(Priority = Priority.HIGH)]
public void OnKill(PlayerDeathEvent ev) {
var killer = ev.Killer == null ? null : converter.GetPlayer(ev.Killer);
var assister =

View File

@@ -43,7 +43,7 @@ public class RoundTimerListener(IServiceProvider provider)
player.Respawn();
foreach (var player in Utilities.GetPlayers())
player.SetColor(Color.FromArgb(254, 255, 255, 255));
player.SetColor(Color.White);
});
return;

View File

@@ -22,10 +22,10 @@ public class CS2InventoryManager(
if (player.Team is CsTeam.None or CsTeam.Spectator) return;
// Give the weapon
player.GiveNamedItem(weapon.Id);
player.GiveNamedItem(weapon.WeaponId);
// Set ammo if applicable
var weaponBase = player.GetWeaponBase(weapon.Id);
var weaponBase = player.GetWeaponBase(weapon.WeaponId);
if (weaponBase == null) return;
if (weapon.CurrentAmmo != null) weaponBase.Clip1 = weapon.CurrentAmmo.Value;
if (weapon.ReserveAmmo != null) weaponBase.Clip2 = weapon.ReserveAmmo.Value;

View File

@@ -80,6 +80,6 @@ public class PlayerDamagedEvent(IOnlinePlayer player, IOnlinePlayer? attacker,
}
}
public string? Weapon { get; private set; }
public string? Weapon { get; init; }
public bool IsCanceled { get; set; }
}

View File

@@ -9,7 +9,7 @@ namespace TTT.Game.Listeners.Loggers;
public class PlayerActionsLogger(IServiceProvider provider)
: BaseListener(provider) {
// Needs to be higher so we detect the kill before the game ends
[EventHandler(Priority = Priority.HIGHER)]
[EventHandler(Priority = Priority.HIGH)]
[UsedImplicitly]
public void OnPlayerKill(PlayerDeathEvent ev) {
if (Games.ActiveGame is not { State: State.IN_PROGRESS }) return;

View File

@@ -4,7 +4,7 @@ namespace TTT.Game.Roles;
public class BaseWeapon(string id, int? reserve = null,
int? current = null) : IWeapon {
public string Id { get; } = id;
public string WeaponId { get; } = id;
public int? ReserveAmmo { get; } = reserve;
public int? CurrentAmmo { get; } = current;
}

View File

@@ -5,7 +5,7 @@ the subsidiary projects (i.e. not [Versioning](../Versioning)).
# Structure
## [API](./API)
The public API for TTT. Use this to add-on extra features, modules, roles, etc.
The public API for TTT. Include this to add-on extra features, modules, roles, etc.
## [CS2](./CS2)
A linker for CS2 to TTT. This adds support for CS2 to TTT.

View File

@@ -1,3 +1,4 @@
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using TTT.API.Events;
using TTT.API.Game;
@@ -18,6 +19,7 @@ public class DeagleDamageListener(IServiceProvider provider)
private readonly IShop shop = provider.GetRequiredService<IShop>();
[UsedImplicitly]
[EventHandler]
public void OnDamage(PlayerDamagedEvent ev) {
Messenger.Debug("DeagleDamageListener: OnDamage");

View File

@@ -47,7 +47,7 @@ public class OneShotDeagle(IServiceProvider provider) : IWeapon, IShopItem {
string IShopItem.Id => ID;
public string Id => deagleConfigStorage.Weapon;
public string WeaponId => deagleConfigStorage.Weapon;
public int? ReserveAmmo { get; init; } = 0;
public int? CurrentAmmo { get; init; } = 1;

View File

@@ -12,7 +12,7 @@ public class RoundShopClearer(IServiceProvider provider) : IListener {
public void Dispose() { bus.UnregisterListener(this); }
[EventHandler(IgnoreCanceled = true, Priority = Priority.LOWER)]
[EventHandler(IgnoreCanceled = true, Priority = Priority.LOW)]
[UsedImplicitly]
public void OnRoundStart(GameStateUpdateEvent ev) {
// Only clear balances if the round is in progress

View File

@@ -0,0 +1,69 @@
using Microsoft.Extensions.DependencyInjection;
using TTT.API.Events;
using TTT.API.Game;
using TTT.API.Player;
using TTT.Game.Events.Player;
using TTT.Shop;
using TTT.Shop.Items;
using Xunit;
namespace TTT.Test.Shop.Items;
public class DeagleTests {
private readonly IServiceProvider provider;
private readonly IEventBus bus;
private readonly TestPlayer testPlayer;
private readonly IOnlinePlayer victim, survivor;
private readonly IShop shop;
private readonly OneShotDeagle item;
public DeagleTests(IServiceProvider provider) {
this.provider = provider;
var games = provider.GetRequiredService<IGameManager>();
var finder = provider.GetRequiredService<IPlayerFinder>();
shop = provider.GetRequiredService<IShop>();
bus = provider.GetRequiredService<IEventBus>();
item = new OneShotDeagle(provider);
testPlayer = (finder.AddPlayer(TestPlayer.Random()) as TestPlayer)!;
victim = finder.AddPlayer(TestPlayer.Random());
survivor = finder.AddPlayer(TestPlayer.Random());
games.CreateGame()?.Start();
}
[Fact]
public void Deagle_Kills_OnDamage() {
bus.RegisterListener(new DeagleDamageListener(provider));
shop.GiveItem(testPlayer, item);
var playerDmgEvent =
new PlayerDamagedEvent(victim, testPlayer, 1, 99) {
Weapon = item.WeaponId
};
bus.Dispatch(playerDmgEvent);
Assert.Equal(0, victim.Health);
Assert.False(victim.IsAlive);
}
[Fact]
public void Deagle_DoesNotKill_AfterFirstKill() {
bus.RegisterListener(new DeagleDamageListener(provider));
shop.GiveItem(testPlayer, item);
var playerDmgEvent =
new PlayerDamagedEvent(victim, testPlayer, 1, 99) {
Weapon = item.WeaponId
};
bus.Dispatch(playerDmgEvent);
var secondDmgEvent =
new PlayerDamagedEvent(survivor, testPlayer, 1, 99) {
Weapon = item.WeaponId
};
bus.Dispatch(secondDmgEvent);
Assert.NotEqual(0, survivor.Health);
}
}

View File

@@ -0,0 +1,56 @@
using Microsoft.Extensions.DependencyInjection;
using TTT.API.Events;
using TTT.API.Game;
using TTT.API.Player;
using TTT.Shop;
using TTT.Shop.Listeners;
using Xunit;
namespace TTT.Test.Shop;
public class ShopTests(IServiceProvider provider) {
private readonly IShop shop = provider.GetRequiredService<IShop>();
private readonly IOnlinePlayer player = TestPlayer.Random();
private readonly IEventBus bus = provider.GetRequiredService<IEventBus>();
private readonly IGameManager games =
provider.GetRequiredService<IGameManager>();
private readonly IPlayerFinder finder =
provider.GetRequiredService<IPlayerFinder>();
[Fact]
public void GiveItem_ShowsInInventory() {
shop.GiveItem(player, new TestShopItem());
Assert.Single(shop.GetOwnedItems(player));
Assert.Equal(TestShopItem.ID, shop.GetOwnedItems(player)[0].Id);
}
[Fact]
public async Task ClearBalances_ClearsBalances() {
shop.AddBalance(player, 500, "Test");
Assert.Equal(500, await shop.Load(player));
shop.ClearBalances();
Assert.Equal(0, await shop.Load(player));
}
[Fact]
public void ClearItems_ClearsItems() {
shop.GiveItem(player, new TestShopItem());
shop.ClearItems();
Assert.Empty(shop.GetOwnedItems(player));
}
[Fact]
public void Shop_ClearsItems_OnNewRound() {
bus.RegisterListener(new RoundShopClearer(provider));
finder.AddPlayers(player, TestPlayer.Random());
games.CreateGame()?.Start();
shop.GiveItem(player, new TestShopItem());
games.ActiveGame?.EndGame();
games.CreateGame()?.Start();
Assert.Empty(shop.GetOwnedItems(player));
}
}

View File

@@ -16,7 +16,15 @@ public class TestPlayer(string id, string name) : IOnlinePlayer {
public int Health { get; set; } = 100;
public int MaxHealth { get; set; } = 100;
public int Armor { get; set; } = 100;
public bool IsAlive { get; set; } = true;
public bool IsAlive {
get => Health > 0;
set {
if (!value)
Health = 0;
else if (Health <= 0) { Health = 1; }
}
}
public static TestPlayer Random() {
return new TestPlayer(new Random().NextInt64().ToString(),