diff --git a/lang/Jailbreak.English/Logs/LogMessages.cs b/lang/Jailbreak.English/Logs/LogMessages.cs index 3b446565..cb4af609 100644 --- a/lang/Jailbreak.English/Logs/LogMessages.cs +++ b/lang/Jailbreak.English/Logs/LogMessages.cs @@ -17,15 +17,15 @@ public class LogMessages : ILogMessages, ILanguage public IView BEGIN_JAILBREAK_LOGS => new SimpleView() { - { "********************************" }, - { "***** BEGIN JAILBREAK LOGS *****" }, + { "********************************" }, SimpleView.NEWLINE, + { "***** BEGIN JAILBREAK LOGS *****" }, SimpleView.NEWLINE, { "********************************" } }; public IView END_JAILBREAK_LOGS => new SimpleView() { - { "********************************" }, - { "****** END JAILBREAK LOGS ******" }, + { "********************************" }, SimpleView.NEWLINE, + { "****** END JAILBREAK LOGS ******" }, SimpleView.NEWLINE, { "********************************" } }; diff --git a/lang/Jailbreak.English/Teams/RatioNotifications.cs b/lang/Jailbreak.English/Teams/RatioNotifications.cs index b9827c7a..f9ed4476 100644 --- a/lang/Jailbreak.English/Teams/RatioNotifications.cs +++ b/lang/Jailbreak.English/Teams/RatioNotifications.cs @@ -33,7 +33,7 @@ public class RatioNotifications : IRatioNotifications, ILanguage new SimpleView { - { PREFIX, "You were autobalanced to the prisoner team!" }, + { PREFIX, "You were autobalanced to the prisoner team!" }, SimpleView.NEWLINE, { PREFIX, "Please use !guard to join the guard team." } }; @@ -43,7 +43,7 @@ public class RatioNotifications : IRatioNotifications, ILanguage new SimpleView { - { PREFIX, "You are no longer a guard." }, + { PREFIX, "You are no longer a guard." }, SimpleView.NEWLINE, { PREFIX, "Please use !guard if you want to re-join the guard team." } }; diff --git a/lang/Jailbreak.English/Warden/WardenNotifications.cs b/lang/Jailbreak.English/Warden/WardenNotifications.cs index 8313aa5e..1101b8fd 100644 --- a/lang/Jailbreak.English/Warden/WardenNotifications.cs +++ b/lang/Jailbreak.English/Warden/WardenNotifications.cs @@ -22,7 +22,7 @@ public class WardenNotifications : IWardenNotifications, ILanguage new SimpleView { - { PREFIX, "Picking a warden shortly" }, + { PREFIX, "Picking a warden shortly" }, SimpleView.NEWLINE, { PREFIX, "To enter the warden queue, type !warden in chat." } }; diff --git a/mod/Jailbreak.Logs/Listeners/LogDamageListeners.cs b/mod/Jailbreak.Logs/Listeners/LogDamageListeners.cs new file mode 100644 index 00000000..db78ec07 --- /dev/null +++ b/mod/Jailbreak.Logs/Listeners/LogDamageListeners.cs @@ -0,0 +1,72 @@ +using CounterStrikeSharp.API; +using CounterStrikeSharp.API.Core; +using CounterStrikeSharp.API.Core.Attributes.Registration; + +using Jailbreak.Formatting.Views; +using Jailbreak.Public.Behaviors; +using Jailbreak.Public.Extensions; +using Jailbreak.Public.Mod.Logs; + +namespace Jailbreak.Logs; + +public class LogDamageListeners : IPluginBehavior +{ + private readonly IRichLogService _logs; + + public LogDamageListeners(IRichLogService logs) + { + _logs = logs; + } + + + + [GameEventHandler] + public HookResult OnGrenadeThrown(EventGrenadeThrown @event, GameEventInfo info) + { + var player = @event.Userid; + if (!player.IsReal()) + return HookResult.Continue; + var grenade = @event.Weapon; + + _logs.Append(_logs.Player(player), $"threw a {grenade}"); + + return HookResult.Continue; + } + + [GameEventHandler] + public HookResult OnPlayerHurt(EventPlayerHurt @event, GameEventInfo info) + { + var player = @event.Userid; + if (!player.IsReal()) + return HookResult.Continue; + var attacker = @event.Attacker; + + bool isWorld = attacker == null || !attacker.IsReal(); + int health = @event.DmgHealth; + + if (isWorld) + { + if (health > 0) + { + _logs.Append($"The world hurt", _logs.Player(player), $"for {health} damage"); + } + else + { + _logs.Append("The world killed", _logs.Player(player)); + } + } + else + { + if (health > 0) + { + _logs.Append( _logs.Player(attacker), "hurt", _logs.Player(player), $"for {health} damage"); + } + else + { + _logs.Append(_logs.Player(attacker!), "killed", _logs.Player(player)); + } + } + + return HookResult.Continue; + } +} diff --git a/mod/Jailbreak.Logs/Listeners/LogEntityListeners.cs b/mod/Jailbreak.Logs/Listeners/LogEntityListeners.cs new file mode 100644 index 00000000..c2c7fe8a --- /dev/null +++ b/mod/Jailbreak.Logs/Listeners/LogEntityListeners.cs @@ -0,0 +1,38 @@ +using CounterStrikeSharp.API; +using CounterStrikeSharp.API.Core; +using CounterStrikeSharp.API.Core.Attributes.Registration; + +using Jailbreak.Formatting.Views; +using Jailbreak.Public.Behaviors; + +namespace Jailbreak.Logs; + +public class LogEntityListeners : IPluginBehavior +{ + private readonly IRichLogService _logs; + + public LogEntityListeners(IRichLogService logs) + { + _logs = logs; + } + + [EntityOutputHook("func_button", "OnPressed")] + public HookResult OnButtonPressed(CEntityIOOutput output, string name, CEntityInstance activator, + CEntityInstance caller, CVariant value, float delay) + { + if (!activator.IsValid) + return HookResult.Continue; + int index = (int)activator.Index; + CCSPlayerPawn? pawn = Utilities.GetEntityFromIndex(index); + if (!pawn.IsValid) + return HookResult.Continue; + if (!pawn.OriginalController.IsValid) + return HookResult.Continue; + CBaseEntity? ent = Utilities.GetEntityFromIndex((int)caller.Index); + if (!ent.IsValid) + return HookResult.Continue; + _logs.Append( + $"{_logs.Player(pawn.OriginalController.Value!)} pressed a button {ent.Entity?.Name ?? "Unlabeled"} -> {output?.Connections?.TargetDesc ?? "None"}"); + return HookResult.Continue; + } +} diff --git a/mod/Jailbreak.Logs/LogsListeners.cs b/mod/Jailbreak.Logs/LogsListeners.cs deleted file mode 100644 index 12957f6b..00000000 --- a/mod/Jailbreak.Logs/LogsListeners.cs +++ /dev/null @@ -1,92 +0,0 @@ -using CounterStrikeSharp.API; -using CounterStrikeSharp.API.Core; -using Jailbreak.Public.Behaviors; -using Jailbreak.Public.Extensions; -using Jailbreak.Public.Mod.Logs; - -namespace Jailbreak.Logs; - -public class LogsListeners : IPluginBehavior -{ - private ILogService logs; - - public LogsListeners(ILogService logs) - { - this.logs = logs; - } - - public void Start(BasePlugin parent) - { - parent.RegisterEventHandler(OnPlayerHurt); - parent.RegisterEventHandler(OnGrenadeThrown); - parent.HookEntityOutput("func_button", "OnPressed", OnButtonPressed); - } - - private HookResult OnButtonPressed(CEntityIOOutput output, string name, CEntityInstance activator, - CEntityInstance caller, CVariant value, float delay) - { - if (!activator.IsValid) - return HookResult.Continue; - int index = (int)activator.Index; - CCSPlayerPawn? pawn = Utilities.GetEntityFromIndex(index); - if (!pawn.IsValid) - return HookResult.Continue; - if (!pawn.OriginalController.IsValid) - return HookResult.Continue; - CBaseEntity? ent = Utilities.GetEntityFromIndex((int)caller.Index); - if (!ent.IsValid) - return HookResult.Continue; - logs.Append( - $"{logs.FormatPlayer(pawn.OriginalController.Value!)} pressed a button {ent.Entity?.Name ?? "Unlabeled"} -> {output?.Connections?.TargetDesc ?? "None"}"); - return HookResult.Continue; - } - - private HookResult OnGrenadeThrown(EventGrenadeThrown @event, GameEventInfo info) - { - var player = @event.Userid; - if (!player.IsReal()) - return HookResult.Continue; - var grenade = @event.Weapon; - - logs.Append($"{logs.FormatPlayer(player)} threw a {grenade}"); - - return HookResult.Continue; - } - - private HookResult OnPlayerHurt(EventPlayerHurt @event, GameEventInfo info) - { - var player = @event.Userid; - if (!player.IsReal()) - return HookResult.Continue; - var attacker = @event.Attacker; - - bool isWorld = attacker == null || !attacker.IsReal(); - int health = @event.DmgHealth; - - if (isWorld) - { - if (health > 0) - { - logs.Append($"The world hurt {logs.FormatPlayer(player)} for {health} damage"); - } - else - { - logs.Append($"The world killed {logs.FormatPlayer(player)}"); - } - } - else - { - if (health > 0) - { - logs.Append( - $"{logs.FormatPlayer(attacker!)} hurt {logs.FormatPlayer(player)} for {health} damage"); - } - else - { - logs.Append($"{logs.FormatPlayer(attacker!)} killed {logs.FormatPlayer(player)}"); - } - } - - return HookResult.Continue; - } -} diff --git a/mod/Jailbreak.Logs/LogsManager.cs b/mod/Jailbreak.Logs/LogsManager.cs index b4294f12..253f52f4 100644 --- a/mod/Jailbreak.Logs/LogsManager.cs +++ b/mod/Jailbreak.Logs/LogsManager.cs @@ -17,20 +17,21 @@ namespace Jailbreak.Logs; -public class LogsManager : IPluginBehavior, ILogService +public class LogsManager : IPluginBehavior, ILogService, IRichLogService { private readonly List _logMessages = new(); + private IRichPlayerTag _richPlayerTag; private ILogMessages _messages; - public LogsManager(IServiceProvider serviceProvider, ILogMessages messages) + public LogsManager(IServiceProvider serviceProvider, ILogMessages messages, IRichPlayerTag richPlayerTag) { _messages = messages; - + _richPlayerTag = richPlayerTag; } [GameEventHandler] - private HookResult OnRoundEnd(EventRoundEnd @event, GameEventInfo info) + public HookResult OnRoundEnd(EventRoundEnd @event, GameEventInfo info) { _messages.BEGIN_JAILBREAK_LOGS .ToServerConsole() @@ -49,7 +50,7 @@ private HookResult OnRoundEnd(EventRoundEnd @event, GameEventInfo info) } [GameEventHandler] - private HookResult OnRoundStart(EventRoundStart @event, GameEventInfo info) + public HookResult OnRoundStart(EventRoundStart @event, GameEventInfo info) { Clear(); return HookResult.Continue; @@ -60,6 +61,16 @@ public void Append(params FormatObject[] objects) _logMessages.Add(_messages.CREATE_LOG(objects)); } + public FormatObject Player(CCSPlayerController playerController) + { + return new TreeFormatObject() + { + playerController, + $"[{playerController.UserId}]", + _richPlayerTag.Rich(playerController) + }; + } + public void Append(string message) { _logMessages.Add(_messages.CREATE_LOG(message)); diff --git a/mod/Jailbreak.Logs/LogsServiceExtension.cs b/mod/Jailbreak.Logs/LogsServiceExtension.cs index 122f6af5..71e203ac 100644 --- a/mod/Jailbreak.Logs/LogsServiceExtension.cs +++ b/mod/Jailbreak.Logs/LogsServiceExtension.cs @@ -10,10 +10,13 @@ public static class LogsServiceExtension { public static void AddJailbreakLogs(this IServiceCollection services) { - services.AddPluginBehavior(); + services.AddPluginBehavior(); + services.AddTransient(provider => provider.GetRequiredService()); services.AddPluginBehavior(); - services.AddPluginBehavior(); + + services.AddPluginBehavior(); + services.AddPluginBehavior(); // PlayerTagHelper is a lower-level class that avoids dependency loops. services.AddTransient(); diff --git a/mod/Jailbreak.Logs/Tags/PlayerTagHelper.cs b/mod/Jailbreak.Logs/Tags/PlayerTagHelper.cs index 62ef7d07..ce028415 100644 --- a/mod/Jailbreak.Logs/Tags/PlayerTagHelper.cs +++ b/mod/Jailbreak.Logs/Tags/PlayerTagHelper.cs @@ -4,6 +4,7 @@ using Jailbreak.Formatting.Core; using Jailbreak.Formatting.Objects; using Jailbreak.Formatting.Views; +using Jailbreak.Public.Behaviors; using Jailbreak.Public.Extensions; using Jailbreak.Public.Mod.Logs; using Jailbreak.Public.Mod.Rebel; @@ -15,23 +16,23 @@ namespace Jailbreak.Logs.Tags; public class PlayerTagHelper : IRichPlayerTag, IPlayerTag { - private IWardenService _wardenService; - private IRebelService _rebelService; + private Lazy _wardenService; + private Lazy _rebelService; public PlayerTagHelper(IServiceProvider provider) { // Lazy-load dependencies to avoid loops, since we are a lower-level class. - _wardenService = provider.GetRequiredService(); - _rebelService = provider.GetRequiredService(); + _wardenService = new ( () => provider.GetRequiredService() ); + _rebelService = new ( () => provider.GetRequiredService() ); } public FormatObject Rich(CCSPlayerController player) { - if (_wardenService.IsWarden(player)) + if (_wardenService.Value.IsWarden(player)) return new StringFormatObject("(WARDEN)", ChatColors.DarkBlue); if (player.GetTeam() == CsTeam.CounterTerrorist) return new StringFormatObject("(CT)", ChatColors.BlueGrey); - if (_rebelService.IsRebel(player)) + if (_rebelService.Value.IsRebel(player)) return new StringFormatObject("(REBEL)", ChatColors.Darkred); return new StringFormatObject("(T)", ChatColors.Yellow); @@ -39,6 +40,6 @@ public FormatObject Rich(CCSPlayerController player) public string Plain(CCSPlayerController playerController) { - return $"{playerController.PlayerName} [#{playerController.UserId}] {Rich(playerController).ToPlain()}"; + return Rich(playerController).ToPlain(); } } diff --git a/mod/Jailbreak.Rebel/RebelManager.cs b/mod/Jailbreak.Rebel/RebelManager.cs index d2909a58..359c2bdc 100644 --- a/mod/Jailbreak.Rebel/RebelManager.cs +++ b/mod/Jailbreak.Rebel/RebelManager.cs @@ -16,12 +16,12 @@ public class RebelManager : IPluginBehavior, IRebelService { private Dictionary rebelTimes = new(); private IRebelNotifications notifs; - private ILogService logs; + private readonly IRichLogService _logs; - public RebelManager(IRebelNotifications notifs, ILogService logs) + public RebelManager(IRebelNotifications notifs, IRichLogService logs) { this.notifs = notifs; - this.logs = logs; + this._logs = logs; } public void Start(BasePlugin parent) @@ -117,7 +117,7 @@ public bool MarkRebel(CCSPlayerController player, long time = 120) { if (!rebelTimes.ContainsKey(player)) { - logs.Append(player.PlayerName + " is now a rebel."); + _logs.Append(_logs.Player(player), "is now a rebel."); } rebelTimes[player] = DateTimeOffset.Now.ToUnixTimeSeconds() + time; @@ -128,13 +128,13 @@ public bool MarkRebel(CCSPlayerController player, long time = 120) public void UnmarkRebel(CCSPlayerController player) { notifs.NO_LONGER_REBEL.ToPlayerChat(player); - logs.Append(player.PlayerName + " is no longer a rebel."); + _logs.Append(_logs.Player(player), "is no longer a rebel."); rebelTimes.Remove(player); ApplyRebelColor(player); } - // https://www.desmos.com/calculator/g2v6vvg7ax + // https://www.desmos.com/calculator/g2v6vvg7ax private float GetRebelTimePercentage(CCSPlayerController player) { long x = GetRebelTimeLeft(player); @@ -178,4 +178,4 @@ private void SendTimeLeft(CCSPlayerController player) player.PrintToCenterHtml($"You are {formattedColor}rebelling"); } -} \ No newline at end of file +} diff --git a/mod/Jailbreak.Warden/Global/WardenBehavior.cs b/mod/Jailbreak.Warden/Global/WardenBehavior.cs index 05c05dbc..135e1ada 100644 --- a/mod/Jailbreak.Warden/Global/WardenBehavior.cs +++ b/mod/Jailbreak.Warden/Global/WardenBehavior.cs @@ -22,14 +22,14 @@ namespace Jailbreak.Warden.Global; public class WardenBehavior : IPluginBehavior, IWardenService { private ILogger _logger; - private ILogService logs; + private IRichLogService logs; private IWardenNotifications _notifications; private bool _hasWarden; private CCSPlayerController? _warden; - public WardenBehavior(ILogger logger, IWardenNotifications notifications, ILogService logs) + public WardenBehavior(ILogger logger, IWardenNotifications notifications, IRichLogService logs) { _logger = logger; _notifications = notifications; @@ -70,8 +70,8 @@ public bool TrySetWarden(CCSPlayerController controller) _notifications.NEW_WARDEN(_warden) .ToAllChat() .ToAllCenter(); - - logs.Append($"{_warden.PlayerName} is now the warden."); + + logs.Append( logs.Player(_warden), "is now the warden."); return true; } @@ -87,9 +87,9 @@ public bool TryRemoveWarden() _warden.Pawn.Value.RenderMode = RenderMode_t.kRenderTransColor; _warden.Pawn.Value.Render = Color.FromArgb(254, 255, 255, 255); Utilities.SetStateChanged(_warden.Pawn.Value, "CBaseModelEntity", "m_clrRender"); - logs.Append($"{_warden.PlayerName} is no longer the warden."); + logs.Append( logs.Player(_warden), "is no longer the warden."); } - + _warden = null; return true; diff --git a/public/Jailbreak.Formatting/Base/SimpleView.cs b/public/Jailbreak.Formatting/Base/SimpleView.cs index 48210e40..31b956f4 100644 --- a/public/Jailbreak.Formatting/Base/SimpleView.cs +++ b/public/Jailbreak.Formatting/Base/SimpleView.cs @@ -7,6 +7,13 @@ namespace Jailbreak.Formatting.Base; public class SimpleView : IView, IEnumerable> { + public struct Newline + { + + } + + public static Newline NEWLINE = new Newline(); + private List> _lines = new(); public SimpleView() @@ -28,13 +35,24 @@ public void Add(FormatObject item) } /// - /// Add a whole new row to this simpleview - /// Eg, { { 123 }, { abc }, { weeeeee } } are each their own lines. + /// Add multiple items at a time to this SimpleView /// /// public void Add(params FormatObject[] line) { - _lines.Add(new List(line)); + if (_lines.Count == 0) + _lines.Add(new List()); + + _lines[_lines.Count - 1].AddRange(line); + } + + /// + /// Add a new line to this SimpleView + /// + /// + public void Add(Newline newline) + { + _lines.Add(new List()); } public void Render(FormatWriter writer) diff --git a/public/Jailbreak.Formatting/Objects/TreeFormatObject.cs b/public/Jailbreak.Formatting/Objects/TreeFormatObject.cs new file mode 100644 index 00000000..cb616eb2 --- /dev/null +++ b/public/Jailbreak.Formatting/Objects/TreeFormatObject.cs @@ -0,0 +1,86 @@ +using System.Collections; + +using Jailbreak.Formatting.Core; + +namespace Jailbreak.Formatting.Objects; + +/// +/// Merges several FormatObjects into one. +/// This class will throw an error if any of it's descendant formatobjects are itself, +/// to prevent looping. +/// +public class TreeFormatObject : FormatObject, IEnumerable +{ + private List _children = new(); + + private int _locked = 0; + + public TreeFormatObject() + { + + } + + /// + /// Lock this object, execute callback, then unlock. + /// + /// + private T Lock(Func callback) + { + // Achieve a re-entrant mutex for this thread. + // We can re-enter this lock as long as we are in the same thread, but others are blocked. + // this allows us to do the internal increment safely. + lock (this) + { + // set _locked to 1 if value is 0 + int old = Interlocked.CompareExchange(ref _locked, 1, 0); + + if (old == 1) + throw new Exception("Possible loop detected in TreeFormatObject! Already locked during traversal"); + + var result = callback(); + _locked = 0; + + return result; + } + } + + public override string ToPlain() + { + return Lock(() => + { + var childPlain = _children.Select(child => child.ToPlain()); + return string.Join(' ', childPlain); + }); + } + + public override string ToChat() + { + return Lock(() => + { + var childChat = _children.Select(child => child.ToChat()); + return string.Join(' ', childChat); + }); + } + + public override string ToPanorama() + { + return Lock(() => + { + var childPanorama = _children.Select(child => child.ToPanorama()); + return string.Join(' ', childPanorama); + }); + } + + public void Add(FormatObject formatObject) + => _children.Add(formatObject); + + public IEnumerator GetEnumerator() + { + return _children.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } +} diff --git a/public/Jailbreak.Formatting/Views/Logging/IRichLogService.cs b/public/Jailbreak.Formatting/Views/Logging/IRichLogService.cs new file mode 100644 index 00000000..05669315 --- /dev/null +++ b/public/Jailbreak.Formatting/Views/Logging/IRichLogService.cs @@ -0,0 +1,13 @@ +using CounterStrikeSharp.API.Core; + +using Jailbreak.Formatting.Core; +using Jailbreak.Public.Mod.Logs; + +namespace Jailbreak.Formatting.Views; + +public interface IRichLogService : ILogService +{ + void Append(params FormatObject[] objects); + + FormatObject Player(CCSPlayerController playerController); +} diff --git a/public/Jailbreak.Formatting/Views/IRichPlayerTag.cs b/public/Jailbreak.Formatting/Views/Logging/IRichPlayerTag.cs similarity index 81% rename from public/Jailbreak.Formatting/Views/IRichPlayerTag.cs rename to public/Jailbreak.Formatting/Views/Logging/IRichPlayerTag.cs index 0c14809a..ad4bab87 100644 --- a/public/Jailbreak.Formatting/Views/IRichPlayerTag.cs +++ b/public/Jailbreak.Formatting/Views/Logging/IRichPlayerTag.cs @@ -1,10 +1,11 @@ using CounterStrikeSharp.API.Core; using Jailbreak.Formatting.Core; +using Jailbreak.Public.Mod.Logs; namespace Jailbreak.Formatting.Views; -public interface IRichPlayerTag +public interface IRichPlayerTag : IPlayerTag { /// /// Get a tag for this player, which contains context about the player's current actions