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