using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Serilog.Context; using SharedLibraryCore.Database.Models; using SharedLibraryCore.Events; namespace SharedLibraryCore { public class GameEvent : CoreEvent { public enum EventFailReason { /// <summary> /// event execution did not fail /// </summary> None, /// <summary> /// an internal exception prevented the event /// from executing /// </summary> Exception, /// <summary> /// event origin didn't have the necessary privileges /// to execute the command /// </summary> Permission, /// <summary> /// executing the event would cause an invalid state /// </summary> Invalid, /// <summary> /// client is doing too much of something /// </summary> Throttle, /// <summary> /// the event timed out before completion /// </summary> Timeout } [Flags] public enum EventRequiredEntity { None = 1, Origin = 2, Target = 4 } public enum EventSource { Unspecified, Log, Status, Internal } public enum EventType { /// <summary> /// the event wasn't parsed properly /// </summary> Unknown, // events "generated" by the server /// <summary> /// a server started being monitored /// </summary> Start, /// <summary> /// a server stopped being monitored /// </summary> Stop, /// <summary> /// a client was detecting as connecting via log /// </summary> Connect, /// <summary> /// a client was detecting joining by RCon /// </summary> Join, /// <summary> /// a client was detected leaving via log /// </summary> Quit, /// <summary> /// a client was detected leaving by RCon /// </summary> Disconnect, /// <summary> /// the current map ended /// </summary> MapEnd, /// <summary> /// the current map changed /// </summary> MapChange, /// <summary> /// a client was detected as starting to connect /// </summary> PreConnect, /// <summary> /// a client was detecting as starting to disconnect /// </summary> PreDisconnect, /// <summary> /// a client's information was updated /// </summary> Update, /// <summary> /// connection was lost to a server (the server has not responded after a number of attempts) /// </summary> ConnectionLost, /// <summary> /// connection was restored to a server (the server began responding again) /// </summary> ConnectionRestored, SayTeam = 99, // events "generated" by clients /// <summary> /// a client sent a message /// </summary> Say = 100, /// <summary> /// a client was warned /// </summary> Warn = 101, /// <summary> /// all warnings for a client were cleared /// </summary> WarnClear = 102, /// <summary> /// a client was reported /// </summary> Report = 103, /// <summary> /// a client was flagged /// </summary> Flag = 104, /// <summary> /// a client was unflagged /// </summary> Unflag = 105, /// <summary> /// a client was kicked /// </summary> Kick = 106, /// <summary> /// a client was tempbanned /// </summary> TempBan = 107, /// <summary> /// a client was banned /// </summary> Ban = 108, /// <summary> /// a client was unbanned /// </summary> Unban = 109, /// <summary> /// a client entered a command /// </summary> Command = 110, /// <summary> /// a client's permission was changed /// </summary> ChangePermission = 111, /// <summary> /// client logged in to webfront /// </summary> Login = 112, /// <summary> /// client logged out of webfront /// </summary> Logout = 113, /// <summary> /// meta value updated on client /// </summary> MetaUpdated = 114, // events "generated" by IW4MAdmin /// <summary> /// a message is sent to all clients /// </summary> Broadcast = 200, /// <summary> /// a message is sent to a specific client /// </summary> Tell = 201, // events "generated" by script/log /// <summary> /// AC Damage Log /// </summary> ScriptDamage = 300, /// <summary> /// AC Kill Log /// </summary> ScriptKill = 301, /// <summary> /// damage info printed out by game script /// </summary> Damage = 302, /// <summary> /// kill info printed out by game script /// </summary> Kill = 303, /// <summary> /// team info printed out by game script /// </summary> JoinTeam = 304, /// <summary> /// used for community generated plugin events /// </summary> Other } private static long NextEventId; private readonly SemaphoreSlim _eventFinishedWaiter = new(0, 1); public string Data; // Data is usually the message sent by player public string Message; public EFClient Origin; public Server Owner; public EFClient Target; public EventType Type; public string TypeName => Type.ToString(); public GameEvent() { Time = DateTime.UtcNow; IncrementalId = GetNextEventId(); } ~GameEvent() { _eventFinishedWaiter.Dispose(); } /// <summary> /// suptype of the event for more detailed classification /// </summary> public string Subtype { get; set; } public EventRequiredEntity RequiredEntity { get; set; } /// <summary> /// Specifies the game time offset as printed in the log /// </summary> public long? GameTime { get; set; } public EFClient ImpersonationOrigin { get; set; } public bool IsRemote { get; set; } public object Extra { get; set; } public DateTime Time { get; set; } public long IncrementalId { get; } public EventFailReason FailReason { get; set; } public bool Failed => FailReason != EventFailReason.None; public List<string> Output { get; set; } = new(); /// <summary> /// Indicates if the event should block until it is complete /// </summary> public bool IsBlocking { get; set; } private static long GetNextEventId() { return Interlocked.Increment(ref NextEventId); } public void Complete() { if (_eventFinishedWaiter.CurrentCount == 0) { _eventFinishedWaiter.Release(); } } public async Task<GameEvent> WaitAsync() { return await WaitAsync(Utilities.DefaultCommandTimeout, new CancellationToken()); } /// <summary> /// asynchronously wait for GameEvent to be processed /// </summary> /// <returns>waitable task </returns> public async Task<GameEvent> WaitAsync(TimeSpan timeSpan, CancellationToken token) { if (FailReason == EventFailReason.Timeout) { return this; } var processed = false; Utilities.DefaultLogger.LogDebug("Begin wait for {Name}, {Type}, {Id}", Type, GetType().Name, IncrementalId); try { await _eventFinishedWaiter.WaitAsync(timeSpan, token); processed = true; } catch (OperationCanceledException) { processed = false; } finally { if (processed) { Complete(); } } if (!processed) { using (LogContext.PushProperty("Server", Owner?.ToString())) { Utilities.DefaultLogger.LogError("Waiting for event to complete timed out {@eventData}", new { Event = this, Message, Origin = Origin?.ToString(), Target = Target?.ToString() }); } } // this lets us know if the the action timed out FailReason = FailReason == EventFailReason.None && !processed ? EventFailReason.Timeout : FailReason; return this; } } }