diff --git a/Content.Server/Administration/Systems/BwoinkSystem.cs b/Content.Server/Administration/Systems/BwoinkSystem.cs index a0f5b95ae2..2a22a4b9a9 100644 --- a/Content.Server/Administration/Systems/BwoinkSystem.cs +++ b/Content.Server/Administration/Systems/BwoinkSystem.cs @@ -3,10 +3,10 @@ using System.Net.Http; using System.Text; using System.Text.Json; using System.Text.Json.Nodes; +using System.Text.Json.Serialization; using System.Text.RegularExpressions; using System.Threading.Tasks; using Content.Server.Administration.Managers; -using Content.Server.Discord; using Content.Server.GameTicking; using Content.Shared.Administration; using Content.Shared.CCVar; @@ -346,13 +346,13 @@ namespace Content.Server.Administration.Systems { Username = username, AvatarUrl = string.IsNullOrWhiteSpace(_avatarUrl) ? null : _avatarUrl, - Embeds = new List + Embeds = new List { new() { Description = messages, Color = color, - Footer = new WebhookEmbedFooter + Footer = new EmbedFooter { Text = $"{serverName} ({round})", IconUrl = string.IsNullOrWhiteSpace(_footerIconUrl) ? null : _footerIconUrl @@ -495,7 +495,7 @@ namespace Content.Server.Administration.Systems private static string GenerateAHelpMessage(string username, string message, bool admin, string roundTime, GameRunLevel roundState, bool noReceivers = false) { var stringbuilder = new StringBuilder(); - + if (admin) stringbuilder.Append(":outbox_tray:"); else if (noReceivers) @@ -509,6 +509,118 @@ namespace Content.Server.Administration.Systems stringbuilder.Append(message); return stringbuilder.ToString(); } + + // https://discord.com/developers/docs/resources/channel#message-object-message-structure + private struct WebhookPayload + { + [JsonPropertyName("username")] + public string Username { get; set; } = ""; + + [JsonPropertyName("avatar_url")] + public string? AvatarUrl { get; set; } = ""; + + [JsonPropertyName("embeds")] + public List? Embeds { get; set; } = null; + + [JsonPropertyName("allowed_mentions")] + public Dictionary AllowedMentions { get; set; } = + new() + { + { "parse", Array.Empty() }, + }; + + public WebhookPayload() + { + } + } + + // https://discord.com/developers/docs/resources/channel#embed-object-embed-structure + private struct Embed + { + [JsonPropertyName("description")] + public string Description { get; set; } = ""; + + [JsonPropertyName("color")] + public int Color { get; set; } = 0; + + [JsonPropertyName("footer")] + public EmbedFooter? Footer { get; set; } = null; + + public Embed() + { + } + } + + // https://discord.com/developers/docs/resources/channel#embed-object-embed-footer-structure + private struct EmbedFooter + { + [JsonPropertyName("text")] + public string Text { get; set; } = ""; + + [JsonPropertyName("icon_url")] + public string? IconUrl { get; set; } + + public EmbedFooter() + { + } + } + + // https://discord.com/developers/docs/resources/webhook#webhook-object-webhook-structure + private struct WebhookData + { + [JsonPropertyName("guild_id")] + public string? GuildId { get; set; } = null; + + [JsonPropertyName("channel_id")] + public string? ChannelId { get; set; } = null; + + public WebhookData() + { + } + } + + //WD-EDIT + public void SendUtkaBwoinkMessage(NetUserId receiver, string sender, string text) + { + var bwoinkText = $"[color=red](D) {sender}[/color]: {text}"; + _playerManager.TryGetUserId(sender, out var senderId); + var msg = new BwoinkTextMessage(receiver, senderId, bwoinkText); + + LogBwoink(msg); + + var admins = GetTargetAdmins(); + + // Notify all admins + foreach (var channel in admins) + { + RaiseNetworkEvent(msg, channel); + } + + // Notify player + if (_playerManager.TryGetSessionById(receiver, out var session)) + { + if (!admins.Contains(session.ConnectedClient)) + RaiseNetworkEvent(msg, session.ConnectedClient); + } + + var sendsWebhook = _webhookUrl != string.Empty; + if (sendsWebhook) + { + if (!_messageQueues.ContainsKey(msg.UserId)) + _messageQueues[msg.UserId] = new Queue(); + + var str = text; + var unameLength = sender.Length; + + if (unameLength + str.Length + _maxAdditionalChars > DescriptionMax) + { + str = str[..(DescriptionMax - _maxAdditionalChars - unameLength)]; + } + _messageQueues[msg.UserId].Enqueue(GenerateAHelpMessage(sender, str, true, + _gameTicker.RoundDuration().ToString("hh\\:mm\\:ss"), _gameTicker.RunLevel)); + } + } + //WD-EDIT } } diff --git a/Content.Server/Chat/Managers/ChatManager.cs b/Content.Server/Chat/Managers/ChatManager.cs index 55eba489c4..7324ec31fb 100644 --- a/Content.Server/Chat/Managers/ChatManager.cs +++ b/Content.Server/Chat/Managers/ChatManager.cs @@ -6,6 +6,7 @@ using Content.Server.Administration.Managers; using Content.Server.Administration.Systems; using Content.Server.MoMMI; using Content.Server.Preferences.Managers; +using Content.Server.UtkaIntegration; using Content.Server.White.Sponsors; using Content.Shared.Administration; using Content.Shared.CCVar; @@ -49,6 +50,7 @@ namespace Content.Server.Chat.Managers /// WD-EDIT [Dependency] private readonly SponsorsManager _sponsorsManager = default!; + [Dependency] private readonly UtkaTCPWrapper _utkaSocketWrapper = default!; /// WD-EDIT /// @@ -170,7 +172,7 @@ namespace Content.Server.Chat.Managers public void SendHookOOC(string sender, string message) { - if (!_oocEnabled && _configurationManager.GetCVar(CCVars.DisablingOOCDisablesRelay)) + if (_configurationManager.GetCVar(CCVars.DisableHookedOOC)) { return; } @@ -179,6 +181,28 @@ namespace Content.Server.Chat.Managers _adminLogger.Add(LogType.Chat, LogImpact.Low, $"Hook OOC from {sender}: {message}"); } + //WD-EDIT + public void SendHookAdminChat(string sender, string message) + { + var admins = _adminManager.ActiveAdmins; + + var wrappedMessage = Loc.GetString("chat-manager-send-admin-chat-wrap-message", + ("adminChannelName", Loc.GetString("chat-manager-admin-discord-channel-name")), + ("playerName", sender), ("message", FormattedMessage.EscapeText(message))); + + ChatMessageToMany(ChatChannel.Admin, message, wrappedMessage, EntityUid.Invalid, false, true, admins.Select(p => p.ConnectedClient)); + + var asayEventMessage = new UtkaChatEventMessage() + { + Command = "asay", + Ckey = sender, + Message = message + }; + + _utkaSocketWrapper.SendMessageToAll(asayEventMessage); + } + //WD-EDIT + #endregion #region Public OOC Chat API @@ -254,6 +278,17 @@ namespace Content.Server.Chat.Managers ChatMessageToAll(ChatChannel.OOC, message, wrappedMessage, EntityUid.Invalid, hideChat: false, recordReplay: true, colorOverride: colorOverride, author: player.UserId); _mommiLink.SendOOCMessage(player.Name, message); _adminLogger.Add(LogType.Chat, LogImpact.Low, $"OOC from {player:Player}: {message}"); + + //WD-EDIT + var toUtkaMessage = new UtkaChatEventMessage() + { + Command = "ooc", + Ckey = player.Name, + Message = message, + }; + + _utkaSocketWrapper.SendMessageToAll(toUtkaMessage); + //WD-EDIT } private void SendAdminChat(ICommonSession player, string message) @@ -284,6 +319,17 @@ namespace Content.Server.Chat.Managers } _adminLogger.Add(LogType.Chat, $"Admin chat from {player:Player}: {message}"); + + //WD-EDIT + var asayEventMessage = new UtkaChatEventMessage() + { + Command = "asay", + Ckey = player.Name, + Message = message + }; + + _utkaSocketWrapper.SendMessageToAll(asayEventMessage); + //WD-EDIT } #endregion diff --git a/Content.Server/Chat/Managers/IChatManager.cs b/Content.Server/Chat/Managers/IChatManager.cs index e5fa8d5f4d..9c0a0b1c93 100644 --- a/Content.Server/Chat/Managers/IChatManager.cs +++ b/Content.Server/Chat/Managers/IChatManager.cs @@ -21,6 +21,7 @@ namespace Content.Server.Chat.Managers void TrySendOOCMessage(ICommonSession player, string message, OOCChatType type); void SendHookOOC(string sender, string message); + void SendHookAdminChat(string sender, string message); // WD-EDIT void SendAdminAnnouncement(string message); void SendAdminAlert(string message); void SendAdminAlert(EntityUid player, string message); diff --git a/Content.Server/Chat/Systems/ChatSystem.cs b/Content.Server/Chat/Systems/ChatSystem.cs index 65bee2190f..9d9c1543c4 100644 --- a/Content.Server/Chat/Systems/ChatSystem.cs +++ b/Content.Server/Chat/Systems/ChatSystem.cs @@ -8,6 +8,7 @@ using Content.Server.Speech.Components; using Content.Server.Speech.EntitySystems; using Content.Server.Station.Components; using Content.Server.Station.Systems; +using Content.Server.UtkaIntegration; using Content.Shared.ActionBlocker; using Content.Shared.CCVar; using Content.Shared.Chat; @@ -56,6 +57,10 @@ public sealed partial class ChatSystem : SharedChatSystem [Dependency] private readonly SharedInteractionSystem _interactionSystem = default!; [Dependency] private readonly ReplacementAccentSystem _wordreplacement = default!; + //WD-EDIT + [Dependency] private readonly UtkaTCPWrapper _utkaSockets = default!; + //WD-EDIT + public const int VoiceRange = 10; // how far voice goes in world units public const int WhisperClearRange = 2; // how far whisper goes while still being understandable, in world units public const int WhisperMuffledRange = 5; // how far whisper goes at all, in world units @@ -571,6 +576,28 @@ public sealed partial class ChatSystem : SharedChatSystem _adminLogger.Add(LogType.Chat, LogImpact.Low, $"Emote from {ToPrettyString(source):user} as {name}: {action}"); else _adminLogger.Add(LogType.Chat, LogImpact.Low, $"Emote from {ToPrettyString(source):user}: {action}"); + + //WD-EDIT + + string ckey = string.Empty; + + if (TryComp(source, out var actorComponent)) + { + ckey = actorComponent.PlayerSession.Name; + } + + if(string.IsNullOrEmpty(ckey)) return; + + var utkaEmoteEvent = new UtkaChatMeEvent() + { + Ckey = ckey, + Message = action, + CharacterName = MetaData(source).EntityName + }; + + _utkaSockets.SendMessageToAll(utkaEmoteEvent); + + //WD-EDIT } // ReSharper disable once InconsistentNaming diff --git a/Content.Server/Content.Server.csproj b/Content.Server/Content.Server.csproj index 3aba36c788..8fd1e33626 100644 --- a/Content.Server/Content.Server.csproj +++ b/Content.Server/Content.Server.csproj @@ -15,6 +15,8 @@ + + diff --git a/Content.Server/Entry/EntryPoint.cs b/Content.Server/Entry/EntryPoint.cs index 3f0e150674..41c9574e8c 100644 --- a/Content.Server/Entry/EntryPoint.cs +++ b/Content.Server/Entry/EntryPoint.cs @@ -29,6 +29,7 @@ using Robust.Shared.ContentPack; using Robust.Shared.Prototypes; using Robust.Shared.Timing; using Robust.Shared.Utility; +using Content.Server.UtkaIntegration; using Content.Server.White.JoinQueue; using Content.Server.White.Sponsors; @@ -146,6 +147,14 @@ namespace Content.Server.Entry IoCManager.Resolve().Initialize(); IoCManager.Resolve().GetEntitySystem().PostInitialize(); IoCManager.Resolve().Initialize(); + IoCManager.Resolve().DoAutoRegistrations(); + IoCManager.Resolve().Initialize(); + + //WD-EDIT + IoCManager.Resolve().Initialize(); + UtkaTCPServer.RegisterCommands(); + //WD-EDIT + } } diff --git a/Content.Server/GameTicking/GameTicker.RoundFlow.cs b/Content.Server/GameTicking/GameTicker.RoundFlow.cs index bc8788646a..7444016510 100644 --- a/Content.Server/GameTicking/GameTicker.RoundFlow.cs +++ b/Content.Server/GameTicking/GameTicker.RoundFlow.cs @@ -20,6 +20,8 @@ using Robust.Shared.Network; using Robust.Shared.Player; using Robust.Shared.Random; using Robust.Shared.Utility; +using Content.Server.UtkaIntegration; +using System.Threading.Tasks; namespace Content.Server.GameTicking { @@ -28,6 +30,10 @@ namespace Content.Server.GameTicking [Dependency] private readonly DiscordWebhook _discord = default!; [Dependency] private readonly ITaskManager _taskManager = default!; + //WD-EDIT + [Dependency] private readonly UtkaTCPWrapper _utkaSocketWrapper = default!; + //WD-EDIT + private static readonly Counter RoundNumberMetric = Metrics.CreateCounter( "ss14_round_number", "Round number."); @@ -255,6 +261,7 @@ namespace Content.Server.GameTicking AnnounceRound(); UpdateInfoText(); RaiseLocalEvent(new RoundStartedEvent(RoundId)); // WD-EDIT + SendRoundStatus("game_started"); //WD-EDIT #if EXCEPTION_TOLERANCE } @@ -435,6 +442,7 @@ namespace Content.Server.GameTicking UpdateInfoText(); ReqWindowAttentionAll(); + SendRoundStatus("lobby_loaded"); //WD-EDIT } } @@ -517,6 +525,22 @@ namespace Content.Server.GameTicking return true; } + //WD-EDIT + private void SendRoundStatus(string status) + { + if (!_postInitialized) + return; + + var utkaRoundStatusEvent = new UtkaRoundStatusEvent() + { + Message = status + }; + + _utkaSocketWrapper.SendMessageToAll(utkaRoundStatusEvent); + + } + //WD-EDIT + private void UpdateRoundFlow(float frameTime) { if (RunLevel == GameRunLevel.InRound) diff --git a/Content.Server/IoC/ServerContentIoC.cs b/Content.Server/IoC/ServerContentIoC.cs index 4d4f6894e2..592817cab0 100644 --- a/Content.Server/IoC/ServerContentIoC.cs +++ b/Content.Server/IoC/ServerContentIoC.cs @@ -19,6 +19,7 @@ using Content.Server.ServerInfo; using Content.Server.ServerUpdates; using Content.Server.Voting.Managers; using Content.Server.Worldgen.Tools; +using Content.Server.UtkaIntegration; using Content.Server.White.JoinQueue; using Content.Server.White.Sponsors; using Content.Shared.Administration.Logs; @@ -64,6 +65,7 @@ namespace Content.Server.IoC // WD-EDIT IoCManager.Register(); IoCManager.Register(); + IoCManager.Register(); // WD-EDIT } } diff --git a/Content.Server/RoundEnd/RoundEndSystem.cs b/Content.Server/RoundEnd/RoundEndSystem.cs index 88c55154a3..618b9ba765 100644 --- a/Content.Server/RoundEnd/RoundEndSystem.cs +++ b/Content.Server/RoundEnd/RoundEndSystem.cs @@ -12,6 +12,7 @@ using Content.Server.Shuttles.Components; using Content.Server.Shuttles.Systems; using Content.Server.Station.Components; using Content.Server.Station.Systems; +using Content.Server.UtkaIntegration; using Content.Shared.Database; using Content.Shared.GameTicking; using Robust.Shared.Audio.Systems; @@ -45,6 +46,10 @@ namespace Content.Server.RoundEnd [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly StationSystem _stationSystem = default!; + //WD-EDIT + [Dependency] private readonly UtkaTCPWrapper _utkaSocketWrapper = default!; + //WD-EDIT + public TimeSpan DefaultCooldownDuration { get; set; } = TimeSpan.FromSeconds(30); /// @@ -52,15 +57,19 @@ namespace Content.Server.RoundEnd /// public TimeSpan DefaultCountdownDuration { get; set; } = TimeSpan.FromMinutes(10); - private CancellationTokenSource? _countdownTokenSource = null; - private CancellationTokenSource? _cooldownTokenSource = null; - public TimeSpan? LastCountdownStart { get; set; } = null; - public TimeSpan? ExpectedCountdownEnd { get; set; } = null; + private CancellationTokenSource? _countdownTokenSource; + private CancellationTokenSource? _cooldownTokenSource; + + public TimeSpan? LastCountdownStart { get; set; } + + public TimeSpan? ExpectedCountdownEnd { get; set; } + public TimeSpan? ExpectedShuttleLength => ExpectedCountdownEnd - LastCountdownStart; + public TimeSpan? ShuttleTimeLeft => ExpectedCountdownEnd - _gameTiming.CurTime; public TimeSpan AutoCallStartTime; - private bool _autoCalledBefore = false; + private bool _autoCalledBefore; public override void Initialize() { @@ -100,9 +109,12 @@ namespace Content.Server.RoundEnd /// public EntityUid? GetStation() { - AllEntityQuery().MoveNext(out _, out _, out var data); + AllEntityQuery() + .MoveNext(out _, out _, out var data); + if (data == null) return null; + var targetGrid = _stationSystem.GetLargestGrid(data); return targetGrid == null ? null : Transform(targetGrid.Value).MapUid; } @@ -112,13 +124,9 @@ namespace Content.Server.RoundEnd /// public EntityUid? GetCentcomm() { - if (AllEntityQuery() - .MoveNext(out var centcomm, out var xform)) - { - return xform.MapUid; - } - - return null; + return AllEntityQuery().MoveNext(out _, out var xform) + ? xform.MapUid + : null; } public bool CanCallOrRecall() @@ -131,7 +139,11 @@ namespace Content.Server.RoundEnd return _countdownTokenSource != null; } - public void RequestRoundEnd(EntityUid? requester = null, bool checkCooldown = true, string text = "round-end-system-shuttle-called-announcement", string name = "Station") + public void RequestRoundEnd( + EntityUid? requester = null, + bool checkCooldown = true, + string text = "round-end-system-shuttle-called-announcement", + string name = "Station") { var duration = DefaultCountdownDuration; @@ -149,18 +161,28 @@ namespace Content.Server.RoundEnd RequestRoundEnd(duration, requester, checkCooldown, text, name); } - public void RequestRoundEnd(TimeSpan countdownTime, EntityUid? requester = null, bool checkCooldown = true, string text = "round-end-system-shuttle-called-announcement", string name = "Station") + public void RequestRoundEnd( + TimeSpan countdownTime, + EntityUid? requester = null, + bool checkCooldown = true, + string text = "round-end-system-shuttle-called-announcement", + string name = "Station") { - if (_gameTicker.RunLevel != GameRunLevel.InRound) return; + if (_gameTicker.RunLevel != GameRunLevel.InRound) + return; - if (checkCooldown && _cooldownTokenSource != null) return; + if (checkCooldown && _cooldownTokenSource != null) + return; - if (_countdownTokenSource != null) return; - _countdownTokenSource = new(); + if (_countdownTokenSource != null) + return; + + _countdownTokenSource = new CancellationTokenSource(); if (requester != null) { - _adminLogger.Add(LogType.ShuttleCalled, LogImpact.High, $"Shuttle called by {ToPrettyString(requester.Value):user}"); + _adminLogger.Add(LogType.ShuttleCalled, LogImpact.High, + $"Shuttle called by {ToPrettyString(requester.Value):user}"); } else { @@ -183,8 +205,8 @@ namespace Content.Server.RoundEnd } _chatSystem.DispatchGlobalAnnouncement(Loc.GetString(text, - ("time", time), - ("units", Loc.GetString(units))), + ("time", time), + ("units", Loc.GetString(units))), name, false, null, @@ -199,6 +221,8 @@ namespace Content.Server.RoundEnd ActivateCooldown(); RaiseLocalEvent(RoundEndSystemChangedEvent.Default); + SendRoundStatus("shuttle_called"); + var shuttle = _shuttle.GetShuttle(); if (shuttle != null && TryComp(shuttle, out var net)) { @@ -208,25 +232,33 @@ namespace Content.Server.RoundEnd [ShuttleTimerMasks.SourceMap] = GetCentcomm(), [ShuttleTimerMasks.DestMap] = GetStation(), [ShuttleTimerMasks.ShuttleTime] = countdownTime, - [ShuttleTimerMasks.SourceTime] = countdownTime + TimeSpan.FromSeconds(_shuttle.TransitTime + _cfg.GetCVar(CCVars.EmergencyShuttleDockTime)), + [ShuttleTimerMasks.SourceTime] = countdownTime + + TimeSpan.FromSeconds(_shuttle.TransitTime + _cfg.GetCVar(CCVars.EmergencyShuttleDockTime)), [ShuttleTimerMasks.DestTime] = countdownTime, }; + _deviceNetworkSystem.QueuePacket(shuttle.Value, null, payload, net.TransmitFrequency); } } public void CancelRoundEndCountdown(EntityUid? requester = null, bool checkCooldown = true) { - if (_gameTicker.RunLevel != GameRunLevel.InRound) return; - if (checkCooldown && _cooldownTokenSource != null) return; + if (_gameTicker.RunLevel != GameRunLevel.InRound) + return; + + if (checkCooldown && _cooldownTokenSource != null) + return; + + if (_countdownTokenSource == null) + return; - if (_countdownTokenSource == null) return; _countdownTokenSource.Cancel(); _countdownTokenSource = null; if (requester != null) { - _adminLogger.Add(LogType.ShuttleRecalled, LogImpact.High, $"Shuttle recalled by {ToPrettyString(requester.Value):user}"); + _adminLogger.Add(LogType.ShuttleRecalled, LogImpact.High, + $"Shuttle recalled by {ToPrettyString(requester.Value):user}"); } else { @@ -243,6 +275,10 @@ namespace Content.Server.RoundEnd ActivateCooldown(); RaiseLocalEvent(RoundEndSystemChangedEvent.Default); + //WD-EDIT + SendRoundStatus("shuttle_recalled"); + //WD-EDIT + // remove all active shuttle timers var zero = TimeSpan.Zero; var shuttle = _shuttle.GetShuttle(); @@ -256,21 +292,36 @@ namespace Content.Server.RoundEnd [ShuttleTimerMasks.ShuttleTime] = zero, [ShuttleTimerMasks.SourceTime] = zero, [ShuttleTimerMasks.DestTime] = zero, - [ShuttleTimerMasks.Text] = new string?[] { string.Empty, string.Empty } + [ShuttleTimerMasks.Text] = new[] { string.Empty, string.Empty } }; + _deviceNetworkSystem.QueuePacket(shuttle.Value, null, payload, net.TransmitFrequency); } } + //WD-EDIT + private void SendRoundStatus(string status) + { + var utkaRoundStatusEvent = new UtkaRoundStatusEvent() + { + Message = status + }; + + _utkaSocketWrapper.SendMessageToAll(utkaRoundStatusEvent); + } + //WD-EDIT + public void EndRound(TimeSpan? countdownTime = null) { - if (_gameTicker.RunLevel != GameRunLevel.InRound) return; + if (_gameTicker.RunLevel != GameRunLevel.InRound) + return; + LastCountdownStart = null; ExpectedCountdownEnd = null; RaiseLocalEvent(RoundEndSystemChangedEvent.Default); _gameTicker.EndRound(); _countdownTokenSource?.Cancel(); - _countdownTokenSource = new(); + _countdownTokenSource = new CancellationTokenSource(); countdownTime ??= TimeSpan.FromSeconds(_cfg.GetCVar(CCVars.RoundRestartTime)); int time; @@ -285,11 +336,13 @@ namespace Content.Server.RoundEnd time = countdownTime.Value.Minutes; unitsLocString = "eta-units-minutes"; } + _chatManager.DispatchServerAnnouncement( Loc.GetString( "round-end-system-round-restart-eta-announcement", ("time", time), ("units", Loc.GetString(unitsLocString)))); + Timer.Spawn(countdownTime.Value, AfterEndRoundRestart, _countdownTokenSource.Token); } @@ -301,7 +354,8 @@ namespace Content.Server.RoundEnd /// /// /// - public void DoRoundEndBehavior(RoundEndBehavior behavior, + public void DoRoundEndBehavior( + RoundEndBehavior behavior, TimeSpan time, string sender = "comms-console-announcement-title-centcom", string textCall = "round-end-system-shuttle-called-announcement", @@ -325,13 +379,16 @@ namespace Content.Server.RoundEnd RequestRoundEnd(time, null, false, textCall, Loc.GetString(sender)); } + break; } } private void AfterEndRoundRestart() { - if (_gameTicker.RunLevel != GameRunLevel.PostRound) return; + if (_gameTicker.RunLevel != GameRunLevel.PostRound) + return; + Reset(); _gameTicker.RestartRound(); } @@ -351,8 +408,10 @@ namespace Content.Server.RoundEnd public override void Update(float frameTime) { // Check if we should auto-call. - int mins = _autoCalledBefore ? _cfg.GetCVar(CCVars.EmergencyShuttleAutoCallExtensionTime) - : _cfg.GetCVar(CCVars.EmergencyShuttleAutoCallTime); + int mins = _autoCalledBefore + ? _cfg.GetCVar(CCVars.EmergencyShuttleAutoCallExtensionTime) + : _cfg.GetCVar(CCVars.EmergencyShuttleAutoCallTime); + if (mins != 0 && _gameTiming.CurTime - AutoCallStartTime > TimeSpan.FromMinutes(mins)) { if (!_shuttle.EmergencyShuttleArrived && ExpectedCountdownEnd is null) diff --git a/Content.Server/Shuttles/Systems/EmergencyShuttleSystem.Console.cs b/Content.Server/Shuttles/Systems/EmergencyShuttleSystem.Console.cs index d14c5be438..29200b7097 100644 --- a/Content.Server/Shuttles/Systems/EmergencyShuttleSystem.Console.cs +++ b/Content.Server/Shuttles/Systems/EmergencyShuttleSystem.Console.cs @@ -234,6 +234,11 @@ public sealed partial class EmergencyShuttleSystem _shuttle.AddFTLDestination(comp.Entity.Value, true); } + + if (EarlyLaunchAuthorized) + SendRoundStatus("shuttle_escaped"); + else + SendRoundStatus("shuttle_left"); } } diff --git a/Content.Server/Shuttles/Systems/EmergencyShuttleSystem.cs b/Content.Server/Shuttles/Systems/EmergencyShuttleSystem.cs index eb205cae63..93fe9cbef4 100644 --- a/Content.Server/Shuttles/Systems/EmergencyShuttleSystem.cs +++ b/Content.Server/Shuttles/Systems/EmergencyShuttleSystem.cs @@ -15,6 +15,7 @@ using Content.Server.Shuttles.Components; using Content.Server.Shuttles.Events; using Content.Server.Station.Components; using Content.Server.Station.Systems; +using Content.Server.UtkaIntegration; using Content.Shared.Access.Systems; using Content.Shared.CCVar; using Content.Shared.Database; @@ -62,6 +63,10 @@ public sealed partial class EmergencyShuttleSystem : EntitySystem [Dependency] private readonly TransformSystem _transformSystem = default!; [Dependency] private readonly UserInterfaceSystem _uiSystem = default!; + //WD-EDIT + [Dependency] private readonly UtkaTCPWrapper _utkaSocketWrapper = default!; + //WD-EDIT + private ISawmill _sawmill = default!; private const float ShuttleSpawnBuffer = 1f; @@ -185,6 +190,7 @@ public sealed partial class EmergencyShuttleSystem : EntitySystem if (config == null) return; + SendRoundStatus("shuttle_docked"); RaiseNetworkEvent(new EmergencyShuttlePositionMessage() { StationUid = GetNetEntity(targetGrid), @@ -197,25 +203,25 @@ public sealed partial class EmergencyShuttleSystem : EntitySystem /// private void OnEmergencyFTL(EntityUid uid, EmergencyShuttleComponent component, ref FTLStartedEvent args) { - TimeSpan ftlTime = TimeSpan.FromSeconds + var ftlTime = TimeSpan.FromSeconds ( - TryComp(uid, out var ftlComp) ? - ftlComp.TravelTime : ShuttleSystem.DefaultTravelTime + TryComp(uid, out var ftlComp) ? ftlComp.TravelTime : ShuttleSystem.DefaultTravelTime ); - if (TryComp(uid, out var netComp)) + if (!TryComp(uid, out var netComp)) + return; + + var payload = new NetworkPayload { - var payload = new NetworkPayload - { - [ShuttleTimerMasks.ShuttleMap] = uid, - [ShuttleTimerMasks.SourceMap] = args.FromMapUid, - [ShuttleTimerMasks.DestMap] = args.TargetCoordinates.GetMapUid(_entityManager), - [ShuttleTimerMasks.ShuttleTime] = ftlTime, - [ShuttleTimerMasks.SourceTime] = ftlTime, - [ShuttleTimerMasks.DestTime] = ftlTime - }; - _deviceNetworkSystem.QueuePacket(uid, null, payload, netComp.TransmitFrequency); - } + [ShuttleTimerMasks.ShuttleMap] = uid, + [ShuttleTimerMasks.SourceMap] = args.FromMapUid, + [ShuttleTimerMasks.DestMap] = args.TargetCoordinates.GetMapUid(_entityManager), + [ShuttleTimerMasks.ShuttleTime] = ftlTime, + [ShuttleTimerMasks.SourceTime] = ftlTime, + [ShuttleTimerMasks.DestTime] = ftlTime + }; + + _deviceNetworkSystem.QueuePacket(uid, null, payload, netComp.TransmitFrequency); } /// @@ -225,20 +231,21 @@ public sealed partial class EmergencyShuttleSystem : EntitySystem { var countdownTime = TimeSpan.FromSeconds(_configManager.GetCVar(CCVars.RoundRestartTime)); var shuttle = args.Entity; - if (TryComp(shuttle, out var net)) + if (!TryComp(shuttle, out var net)) + return; + + var payload = new NetworkPayload { - var payload = new NetworkPayload - { - [ShuttleTimerMasks.ShuttleMap] = shuttle, - [ShuttleTimerMasks.SourceMap] = _roundEnd.GetCentcomm(), - [ShuttleTimerMasks.DestMap] = _roundEnd.GetStation(), - [ShuttleTimerMasks.ShuttleTime] = countdownTime, - [ShuttleTimerMasks.SourceTime] = countdownTime, - [ShuttleTimerMasks.DestTime] = countdownTime, - [ShuttleTimerMasks.Text] = new string?[] { "BYE!" } - }; - _deviceNetworkSystem.QueuePacket(shuttle, null, payload, net.TransmitFrequency); - } + [ShuttleTimerMasks.ShuttleMap] = shuttle, + [ShuttleTimerMasks.SourceMap] = _roundEnd.GetCentcomm(), + [ShuttleTimerMasks.DestMap] = _roundEnd.GetStation(), + [ShuttleTimerMasks.ShuttleTime] = countdownTime, + [ShuttleTimerMasks.SourceTime] = countdownTime, + [ShuttleTimerMasks.DestTime] = countdownTime, + [ShuttleTimerMasks.Text] = new[] { "BYE!" } + }; + + _deviceNetworkSystem.QueuePacket(shuttle, null, payload, net.TransmitFrequency); } /// @@ -258,8 +265,12 @@ public sealed partial class EmergencyShuttleSystem : EntitySystem // UHH GOOD LUCK if (targetGrid == null) { - _logger.Add(LogType.EmergencyShuttle, LogImpact.High, $"Emergency shuttle {ToPrettyString(stationUid)} unable to dock with station {ToPrettyString(stationUid)}"); - _chatSystem.DispatchStationAnnouncement(stationUid, Loc.GetString("emergency-shuttle-good-luck"), playDefaultSound: false); + _logger.Add(LogType.EmergencyShuttle, LogImpact.High, + $"Emergency shuttle {ToPrettyString(stationUid)} unable to dock with station {ToPrettyString(stationUid)}"); + + _chatSystem.DispatchStationAnnouncement(stationUid, Loc.GetString("emergency-shuttle-good-luck"), + playDefaultSound: false); + // TODO: Need filter extensions or something don't blame me. _audio.PlayGlobal("/Audio/Misc/notice1.ogg", Filter.Broadcast(), true); return; @@ -271,8 +282,12 @@ public sealed partial class EmergencyShuttleSystem : EntitySystem { if (TryComp(targetGrid.Value, out var targetXform)) { - var angle = _dock.GetAngle(stationShuttle.EmergencyShuttle.Value, xform, targetGrid.Value, targetXform, xformQuery); - _chatSystem.DispatchStationAnnouncement(stationUid, Loc.GetString("emergency-shuttle-docked", ("time", $"{_consoleAccumulator:0}"), ("direction", angle.GetDir())), playDefaultSound: false); + var angle = _dock.GetAngle(stationShuttle.EmergencyShuttle.Value, xform, targetGrid.Value, targetXform, + xformQuery); + + _chatSystem.DispatchStationAnnouncement(stationUid, + Loc.GetString("emergency-shuttle-docked", ("time", $"{_consoleAccumulator:0}"), + ("direction", angle.GetDir())), playDefaultSound: false); } // shuttle timers @@ -289,10 +304,14 @@ public sealed partial class EmergencyShuttleSystem : EntitySystem [ShuttleTimerMasks.DestTime] = time + TimeSpan.FromSeconds(TransitTime), [ShuttleTimerMasks.Docked] = true }; - _deviceNetworkSystem.QueuePacket(stationShuttle.EmergencyShuttle.Value, null, payload, netComp.TransmitFrequency); + + _deviceNetworkSystem.QueuePacket(stationShuttle.EmergencyShuttle.Value, null, payload, + netComp.TransmitFrequency); } - _logger.Add(LogType.EmergencyShuttle, LogImpact.High, $"Emergency shuttle {ToPrettyString(stationUid)} docked with stations"); + _logger.Add(LogType.EmergencyShuttle, LogImpact.High, + $"Emergency shuttle {ToPrettyString(stationUid)} docked with stations"); + // TODO: Need filter extensions or something don't blame me. _audio.PlayGlobal("/Audio/Announcements/shuttle_dock.ogg", Filter.Broadcast(), true); } @@ -300,11 +319,16 @@ public sealed partial class EmergencyShuttleSystem : EntitySystem { if (TryComp(targetGrid.Value, out var targetXform)) { - var angle = _dock.GetAngle(stationShuttle.EmergencyShuttle.Value, xform, targetGrid.Value, targetXform, xformQuery); - _chatSystem.DispatchStationAnnouncement(stationUid, Loc.GetString("emergency-shuttle-nearby", ("direction", angle.GetDir())), playDefaultSound: false); + var angle = _dock.GetAngle(stationShuttle.EmergencyShuttle.Value, xform, targetGrid.Value, targetXform, + xformQuery); + + _chatSystem.DispatchStationAnnouncement(stationUid, + Loc.GetString("emergency-shuttle-nearby", ("direction", angle.GetDir())), playDefaultSound: false); } - _logger.Add(LogType.EmergencyShuttle, LogImpact.High, $"Emergency shuttle {ToPrettyString(stationUid)} unable to find a valid docking port for {ToPrettyString(stationUid)}"); + _logger.Add(LogType.EmergencyShuttle, LogImpact.High, + $"Emergency shuttle {ToPrettyString(stationUid)} unable to find a valid docking port for {ToPrettyString(stationUid)}"); + // TODO: Need filter extensions or something don't blame me. _audio.PlayGlobal("/Audio/Misc/notice1.ogg", Filter.Broadcast(), true); } @@ -372,7 +396,9 @@ public sealed partial class EmergencyShuttleSystem : EntitySystem var query = AllEntityQuery(); while (query.MoveNext(out var uid, out var comp)) + { AddEmergencyShuttle(uid, comp); + } } private void AddCentcomm(StationCentcommComponent component) @@ -409,10 +435,11 @@ public sealed partial class EmergencyShuttleSystem : EntitySystem } var mapId = _mapManager.CreateMap(); - var grid = _map.LoadGrid(mapId, component.Map.ToString(), new MapLoadOptions() + var grid = _map.LoadGrid(mapId, component.Map.ToString(), new MapLoadOptions { LoadMap = false, }); + var map = _mapManager.GetMapEntityId(mapId); if (!Exists(map)) @@ -535,11 +562,26 @@ public sealed partial class EmergencyShuttleSystem : EntitySystem return false; } - private bool IsOnGrid(TransformComponent xform, EntityUid shuttle, MapGridComponent? grid = null, TransformComponent? shuttleXform = null) + private bool IsOnGrid( + TransformComponent xform, + EntityUid shuttle, + MapGridComponent? grid = null, + TransformComponent? shuttleXform = null) { if (!Resolve(shuttle, ref grid, ref shuttleXform)) return false; - return _transformSystem.GetWorldMatrix(shuttleXform).TransformBox(grid.LocalAABB).Contains(_transformSystem.GetWorldPosition(xform)); + return _transformSystem.GetWorldMatrix(shuttleXform).TransformBox(grid.LocalAABB) + .Contains(_transformSystem.GetWorldPosition(xform)); + } + + private void SendRoundStatus(string status) + { + var utkaRoundStatusEvent = new UtkaRoundStatusEvent() + { + Message = status + }; + + _utkaSocketWrapper.SendMessageToAll(utkaRoundStatusEvent); } } diff --git a/Content.Server/UtkaIntegration/Commands/IUtkaCommand.cs b/Content.Server/UtkaIntegration/Commands/IUtkaCommand.cs new file mode 100644 index 0000000000..fd69e5f725 --- /dev/null +++ b/Content.Server/UtkaIntegration/Commands/IUtkaCommand.cs @@ -0,0 +1,11 @@ +using System.Net; +using Content.Server.UtkaIntegration.TCP; + +namespace Content.Server.UtkaIntegration; + +public interface IUtkaCommand +{ + string Name { get; } + Type RequestMessageType { get; } + public void Execute(UtkaTCPSession session, UtkaBaseMessage baseMessage); +} diff --git a/Content.Server/UtkaIntegration/Commands/UtkaAdminWhoCommand.cs b/Content.Server/UtkaIntegration/Commands/UtkaAdminWhoCommand.cs new file mode 100644 index 0000000000..bb3cacc58d --- /dev/null +++ b/Content.Server/UtkaIntegration/Commands/UtkaAdminWhoCommand.cs @@ -0,0 +1,43 @@ +using System.Linq; +using System.Net; +using System.Text; +using System.Text.Json; +using Content.Server.Administration.Managers; +using Content.Server.UtkaIntegration.TCP; +using Content.Shared.CCVar; +using Robust.Shared; +using Robust.Shared.Configuration; + +namespace Content.Server.UtkaIntegration; + +public sealed class UtkaAdminWhoCommand : IUtkaCommand +{ + public string Name => "adminwho"; + public Type RequestMessageType => typeof(UtkaAdminWhoRequest); + + [Dependency] private readonly UtkaTCPWrapper _utkaSocketWrapper = default!; + + public void Execute(UtkaTCPSession session, UtkaBaseMessage baseMessage) + { + if(baseMessage is not UtkaAdminWhoRequest message) return; + IoCManager.InjectDependencies(this); + + var adminManager = IoCManager.Resolve(); + + var admins = adminManager.ActiveAdmins.ToList(); + + var adminsList = new List(); + + foreach (var admin in admins) + { + adminsList.Add(admin.Name); + } + + var toUtkaMessage = new UtkaAdminWhoResponse() + { + Admins = adminsList + }; + + _utkaSocketWrapper.SendMessageToAll(toUtkaMessage); + } +} diff --git a/Content.Server/UtkaIntegration/Commands/UtkaAssayCommand.cs b/Content.Server/UtkaIntegration/Commands/UtkaAssayCommand.cs new file mode 100644 index 0000000000..22c32f2ab2 --- /dev/null +++ b/Content.Server/UtkaIntegration/Commands/UtkaAssayCommand.cs @@ -0,0 +1,29 @@ +using System.Linq; +using System.Net; +using Content.Server.Administration.Managers; +using Content.Server.Chat.Managers; +using Content.Server.UtkaIntegration.TCP; +using Content.Shared.Chat; +using Robust.Shared.Utility; + +namespace Content.Server.UtkaIntegration; + +public sealed class UtkaAssayCommand : IUtkaCommand +{ + public string Name => "asay"; + public Type RequestMessageType => typeof(UtkaAsayRequest); + + public void Execute(UtkaTCPSession session, UtkaBaseMessage baseMessage) + { + if(baseMessage is not UtkaAsayRequest message) return; + + var ckey = message.ACkey; + + if(string.IsNullOrWhiteSpace(message.Message) || string.IsNullOrWhiteSpace(ckey)) return; + + var chatManager = IoCManager.Resolve(); + + chatManager.SendHookAdminChat(ckey, message.Message); + + } +} diff --git a/Content.Server/UtkaIntegration/Commands/UtkaAuthenticationCommand.cs b/Content.Server/UtkaIntegration/Commands/UtkaAuthenticationCommand.cs new file mode 100644 index 0000000000..6ccf916787 --- /dev/null +++ b/Content.Server/UtkaIntegration/Commands/UtkaAuthenticationCommand.cs @@ -0,0 +1,57 @@ +using Content.Server.UtkaIntegration.TCP; +using Content.Shared.CCVar; +using Content.Shared.White; +using Robust.Shared.Configuration; + +namespace Content.Server.UtkaIntegration; + +public sealed class UtkaAuthenticationCommand : IUtkaCommand +{ + [Dependency] private readonly IConfigurationManager _configurationManager = default!; + [Dependency] private readonly UtkaTCPWrapper _utkaTcpWrapper = default!; + + public string Name => "handshake"; + public Type RequestMessageType => typeof(UtkaHandshakeMessage); + + public void Execute(UtkaTCPSession session, UtkaBaseMessage baseMessage) + { + if (baseMessage is not UtkaHandshakeMessage message) + return; + + IoCManager.InjectDependencies(this); + + if (string.IsNullOrWhiteSpace(message.Key)) + { + SendMessage(session, "key_missmatch"); + return; + } + + var key = _configurationManager.GetCVar(WhiteCVars.UtkaSocketKey); + + if (key != message.Key) + { + SendMessage(session, "key_missmatch"); + return; + } + + if (session.Authenticated) + { + SendMessage(session, "already_authentificated"); + return; + } + + session.Authenticated = true; + SendMessage(session, "handshake_accepted"); + } + + private void SendMessage(UtkaTCPSession session, string message) + { + var response = new UtkaHandshakeMessage() + { + Key = _configurationManager.GetCVar(WhiteCVars.UtkaSocketKey), + Message = message + }; + + _utkaTcpWrapper.SendMessageToClient(session, response); + } +} diff --git a/Content.Server/UtkaIntegration/Commands/UtkaPmCommand.cs b/Content.Server/UtkaIntegration/Commands/UtkaPmCommand.cs new file mode 100644 index 0000000000..a52a3edf32 --- /dev/null +++ b/Content.Server/UtkaIntegration/Commands/UtkaPmCommand.cs @@ -0,0 +1,36 @@ +using Content.Server.Administration.Systems; +using Content.Server.UtkaIntegration.TCP; +using Robust.Server.Player; + +namespace Content.Server.UtkaIntegration; + +public sealed class UtkaPmCommand : IUtkaCommand +{ + public string Name => "discord_pm"; + public Type RequestMessageType => typeof(UtkaPmRequest); + + [Dependency] private readonly IPlayerManager _playerManager = default!; + [Dependency] private UtkaTCPWrapper _utkaSocketWrapper = default!; + + public void Execute(UtkaTCPSession session, UtkaBaseMessage baseMessage) + { + if(baseMessage is not UtkaPmRequest message) return; + var _bwoink = EntitySystem.Get(); + IoCManager.InjectDependencies(this); + + if(string.IsNullOrWhiteSpace(message.Message) || string.IsNullOrWhiteSpace(message.Sender) || string.IsNullOrWhiteSpace(message.Reciever)) return; + + var toUtkaMessage = new UtkaPmResponse(); + if (!_playerManager.TryGetUserId(message.Reciever, out var reciever)) + { + toUtkaMessage.Message = false; + _utkaSocketWrapper.SendMessageToAll(toUtkaMessage); + return; + } + + _bwoink.SendUtkaBwoinkMessage(reciever, message.Sender, message.Message); + + toUtkaMessage.Message = true; + _utkaSocketWrapper.SendMessageToAll(toUtkaMessage); + } +} diff --git a/Content.Server/UtkaIntegration/Commands/UtkaSendOOCMessage.cs b/Content.Server/UtkaIntegration/Commands/UtkaSendOOCMessage.cs new file mode 100644 index 0000000000..c1923391b4 --- /dev/null +++ b/Content.Server/UtkaIntegration/Commands/UtkaSendOOCMessage.cs @@ -0,0 +1,21 @@ +using System.Net; +using Content.Server.Chat.Managers; +using Content.Server.UtkaIntegration.TCP; + +namespace Content.Server.UtkaIntegration; + +public sealed class UtkaSendOOCMessage : IUtkaCommand +{ + public string Name => "ooc"; + public Type RequestMessageType => typeof(UtkaOOCRequest); + + public void Execute(UtkaTCPSession session, UtkaBaseMessage baseMessage) + { + if (baseMessage is not UtkaOOCRequest message) return; + if(string.IsNullOrWhiteSpace(message.Message) || string.IsNullOrWhiteSpace(message.CKey)) return; + + + var chatSystem = IoCManager.Resolve(); + chatSystem.SendHookOOC($"{message.CKey}", $"{message.Message}"); + } +} diff --git a/Content.Server/UtkaIntegration/Commands/UtkaStatusCommand.cs b/Content.Server/UtkaIntegration/Commands/UtkaStatusCommand.cs new file mode 100644 index 0000000000..5b74b8122f --- /dev/null +++ b/Content.Server/UtkaIntegration/Commands/UtkaStatusCommand.cs @@ -0,0 +1,85 @@ +using System.Linq; +using System.Net; +using Content.Server.Administration.Managers; +using Content.Server.AlertLevel; +using Content.Server.GameTicking; +using Content.Server.Maps; +using Content.Server.RoundEnd; +using Content.Server.Station.Systems; +using Content.Server.UtkaIntegration.TCP; +using Robust.Server.Player; +using Robust.Shared.Configuration; +using Robust.Shared.Enums; +using Robust.Shared.Player; + +namespace Content.Server.UtkaIntegration; + +public sealed class UtkaStatusCommand : IUtkaCommand +{ + public string Name => "status"; + public Type RequestMessageType => typeof(UtkaStatusRequsets); + + [Dependency] private readonly IPlayerManager _playerManager = default!; + [Dependency] private readonly IAdminManager _adminManager = default!; + [Dependency] private readonly IConfigurationManager _cfg = default!; + [Dependency] private readonly UtkaTCPWrapper _utkaSocketWrapper = default!; + [Dependency] private readonly IEntityManager _entMan = default!; + [Dependency] private readonly IGameMapManager _gameMapManager = default!; + + public void Execute(UtkaTCPSession session, UtkaBaseMessage baseMessage) + { + if(baseMessage is not UtkaStatusRequsets message) return; + var _gameTicker = EntitySystem.Get(); + var _roundEndSystem = EntitySystem.Get(); + var _station = EntitySystem.Get(); + IoCManager.InjectDependencies(this); + + + var players = Filter.GetAllPlayers().ToList().Count; + + var admins = _adminManager.ActiveAdmins.Select(x => x.Name).ToList().Count; + + string shuttleData = string.Empty; + + if (_roundEndSystem.ExpectedCountdownEnd == null) + { + shuttleData = "idle"; + } + else + { + shuttleData = "called"; + } + + var roundDuration = _gameTicker.RoundDuration().TotalSeconds; + + string? gameMap = null; + string? stationCode = null; + foreach (var station in _station.Stations) + { + if (!_entMan.TryGetComponent(station, out AlertLevelComponent? alert) || stationCode != null) + { + continue; + } + + if (alert is { CurrentLevel: { } }) + { + stationCode = alert.CurrentLevel; + + var map = _gameMapManager.GetSelectedMap(); + gameMap = map?.MapName ?? Loc.GetString("discord-round-unknown-map"); + } + } + + var toUtkaMessage = new UtkaStatusResponse() + { + Players = players, + Admins = admins, + Map = gameMap, + ShuttleStatus = shuttleData, + RoundDuration = roundDuration, + StationCode = stationCode + }; + + _utkaSocketWrapper.SendMessageToAll(toUtkaMessage); + } +} diff --git a/Content.Server/UtkaIntegration/Commands/UtkaWhoCommand.cs b/Content.Server/UtkaIntegration/Commands/UtkaWhoCommand.cs new file mode 100644 index 0000000000..a08cad435c --- /dev/null +++ b/Content.Server/UtkaIntegration/Commands/UtkaWhoCommand.cs @@ -0,0 +1,40 @@ +using System.Linq; +using System.Net; +using System.Text; +using System.Text.Json; +using Content.Server.UtkaIntegration.TCP; +using Content.Shared.CCVar; +using Robust.Server.Player; +using Robust.Shared.Configuration; +using Robust.Shared.Enums; +using Robust.Shared.Player; + +namespace Content.Server.UtkaIntegration; + +public sealed class UtkaWhoCommand : IUtkaCommand +{ + public string Name => "who"; + public Type RequestMessageType => typeof(UtkaWhoRequest); + + [Dependency] private readonly IConfigurationManager _cfg = default!; + [Dependency] private UtkaTCPWrapper _utkaSocketWrapper = default!; + + public void Execute(UtkaTCPSession session, UtkaBaseMessage baseMessage) + { + if(baseMessage is not UtkaWhoRequest _) return; + + IoCManager.InjectDependencies(this); + + var players = Filter.GetAllPlayers().ToList(); + var playerNames = players + .Where(player => player.Status != SessionStatus.Disconnected) + .Select(x => x.Name); + + var toUtkaMessage = new UtkaWhoResponse() + { + Players = playerNames.ToList() + }; + + _utkaSocketWrapper.SendMessageToAll(toUtkaMessage); + } +} diff --git a/Content.Server/UtkaIntegration/TCP/UtkaTCPServer.cs b/Content.Server/UtkaIntegration/TCP/UtkaTCPServer.cs new file mode 100644 index 0000000000..11263e1127 --- /dev/null +++ b/Content.Server/UtkaIntegration/TCP/UtkaTCPServer.cs @@ -0,0 +1,113 @@ +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Reflection; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Content.Server.UtkaIntegration.TCP; +using Content.Shared.CCVar; +using Content.Shared.White; +using NetCoreServer; +using Robust.Shared.Asynchronous; +using Robust.Shared.Configuration; +using Robust.Shared.Timing; +using YamlDotNet.Core.Tokens; +using Timer = Robust.Shared.Timing.Timer; + +namespace Content.Server.UtkaIntegration; + +public sealed class UtkaTCPServer : TcpServer +{ + + [Dependency] private readonly IConfigurationManager _cfg = default!; + [Dependency] private readonly ITaskManager _taskManager = default!; + [Dependency] private readonly ITimerManager _timerManager = default!; + + public static readonly Dictionary Commands = new(); + private List _authenticatedSessions = new(); + + private string? _key; + + protected override TcpSession CreateSession() { return new UtkaTCPSession(this); } + public UtkaTCPServer(IPAddress address, int port) : base(address, port) + { + IoCManager.InjectDependencies(this); + _cfg.OnValueChanged(WhiteCVars.UtkaSocketKey, key => _key = key, true); + OptionKeepAlive = true; + } + + public void SendMessageToAll(UtkaBaseMessage message) + { + foreach (var session in Sessions.Values.Cast()) + { + if(!session.Authenticated) continue; + + session.SendAsync(JsonSerializer.Serialize(message, message.GetType())); + } + } + + public void SendMessageToClient(UtkaTCPSession session, UtkaBaseMessage message) + { + session.SendAsync(JsonSerializer.Serialize(message, message.GetType())); + } + + protected override void OnConnected(TcpSession session) + { + var utkaSession = (UtkaTCPSession) session; + var cancellationToken = new CancellationTokenSource(); + + utkaSession.OnMessageReceived += (sender, message) => + { + ExecuteCommand(utkaSession, message); + }; + + var autoDisconnectionTimer = new Timer(25000, false, () => + { + if (!utkaSession.Authenticated) + { + utkaSession.Disconnect(); + } + }); + + _timerManager.AddTimer(autoDisconnectionTimer, cancellationToken.Token); + } + + protected override void OnDisconnecting(TcpSession session) + { + _authenticatedSessions.Remove((session as UtkaTCPSession)!); + base.OnDisconnecting(session); + } + + protected override void OnError(SocketError error) + { + } + private void ExecuteCommand(UtkaTCPSession session, UtkaBaseMessage fromUtkaMessage) + { + var command = fromUtkaMessage.Command!; + + if (!Commands.ContainsKey(command)) + { + return; + } + + _taskManager.RunOnMainThread(() => Commands[command].Execute(session, fromUtkaMessage)); + } + + public static void RegisterCommands() + { + var assembly = Assembly.GetExecutingAssembly(); + var types = assembly.GetTypes(); + + var commands = types.Where(type => typeof(IUtkaCommand).IsAssignableFrom(type) && type.GetInterfaces().Contains(typeof(IUtkaCommand))).ToList(); + + foreach (var command in commands) + { + if (Activator.CreateInstance(command) is IUtkaCommand utkaCommand) + { + Commands[utkaCommand.Name] = utkaCommand; + } + } + } +} diff --git a/Content.Server/UtkaIntegration/TCP/UtkaTCPSession.cs b/Content.Server/UtkaIntegration/TCP/UtkaTCPSession.cs new file mode 100644 index 0000000000..7e26f1a2fa --- /dev/null +++ b/Content.Server/UtkaIntegration/TCP/UtkaTCPSession.cs @@ -0,0 +1,81 @@ +using System.Linq; +using System.Net.Sockets; +using System.Text; +using System.Text.Json; +using NetCoreServer; +using Newtonsoft.Json.Linq; + +namespace Content.Server.UtkaIntegration.TCP; + +public sealed class UtkaTCPSession : TcpSession +{ + public event EventHandler? OnMessageReceived; + + public bool Authenticated { get; set; } + + public UtkaTCPSession(TcpServer server) : base(server) + { + } + + protected override void OnReceived(byte[] buffer, long offset, long size) + { + if (!ValidateMessage(buffer, offset, size, out var message)) + { + this.SendAsync("Validation fail"); + return; + } + + OnMessageReceived?.Invoke(this, message!); + } + + protected override void OnError(SocketError error) + { + SendAsync($"{error.ToString()}"); + base.OnError(error); + } + + protected override void OnConnected() + { + SendAsync("Hello from грабли, знай утка я ебал тебя в зад!!!"); + base.OnConnected(); + } + + private bool ValidateMessage(byte[] buffer, long offset, long size, out UtkaBaseMessage? fromDiscordMessage) + { + var message = Encoding.UTF8.GetString(buffer, (int) offset, (int) size); + fromDiscordMessage = null; + + if (string.IsNullOrEmpty(message)) + { + return false; + } + + var commandName = JObject.Parse(message)["command"]; + if (commandName == null) + return false; + + var utkaCommand = UtkaTCPServer.Commands.Values.FirstOrDefault(x => x.Name == commandName.ToString()); + + if (utkaCommand == null) + return false; + + var messageType = utkaCommand.RequestMessageType; + + try + { + fromDiscordMessage = JsonSerializer.Deserialize(message, messageType) as UtkaBaseMessage; + } + catch (Exception e) + { + return false; + } + + return true; + } + + protected override void OnDisconnected() + { + base.OnDisconnecting(); + Dispose(); + } +} diff --git a/Content.Server/UtkaIntegration/TCP/UtkaTCPWrapper.cs b/Content.Server/UtkaIntegration/TCP/UtkaTCPWrapper.cs new file mode 100644 index 0000000000..56e3ec8f59 --- /dev/null +++ b/Content.Server/UtkaIntegration/TCP/UtkaTCPWrapper.cs @@ -0,0 +1,64 @@ +using System.Net; +using Content.Server.UtkaIntegration.TCP; +using Content.Shared.CCVar; +using Content.Shared.White; +using Robust.Shared; +using Robust.Shared.Configuration; + +namespace Content.Server.UtkaIntegration; + +public sealed class UtkaTCPWrapper +{ + [Dependency] private readonly IConfigurationManager _cfg = default!; + + private UtkaTCPServer _server = default!; + private string _key = string.Empty; + + private bool _initialized; + + public void Initialize() + { + if(_initialized) return; + + _key = _cfg.GetCVar(WhiteCVars.UtkaSocketKey); + + if (string.IsNullOrEmpty(_key)) + { + return; + } + + var port = _cfg.GetCVar(CVars.NetPort) + 100; + + try + { + _server = new UtkaTCPServer(IPAddress.Any, port); + + } + catch (Exception e) + { + return; + } + + _server.Start(); + + _initialized = true; + } + + public void SendMessageToAll(UtkaBaseMessage message) + { + _server.SendMessageToAll(message); + } + + public void SendMessageToClient(UtkaTCPSession session, UtkaBaseMessage message) + { + _server.SendMessageToClient(session, message); + } + + public void Shutdown() + { + _server.Stop(); + _server.Multicast("Server shutting down."); + _server.DisconnectAll(); + _server.Dispose(); + } +} diff --git a/Content.Server/UtkaIntegration/UtkaCommunication.cs b/Content.Server/UtkaIntegration/UtkaCommunication.cs new file mode 100644 index 0000000000..24359d02a3 --- /dev/null +++ b/Content.Server/UtkaIntegration/UtkaCommunication.cs @@ -0,0 +1,172 @@ +using System.Text.Json.Serialization; + +namespace Content.Server.UtkaIntegration; + +public class UtkaBaseMessage +{ + [JsonPropertyName("command")] + public virtual string? Command { get; set; } +} + +public class UtkaHandshakeMessage : UtkaBaseMessage +{ + [JsonPropertyName("command")] + public override string Command => "handshake"; + + [JsonPropertyName("key")] + public string? Key { get; set; } + + [JsonPropertyName("message")] + public string? Message { get; set; } +} + +public class UtkaOOCRequest : UtkaBaseMessage +{ + [JsonPropertyName("command")] + public override string? Command => "ooc"; + + [JsonPropertyName("ckey")] + public string? CKey { get; set; } + + [JsonPropertyName("message")] + public string? Message { get; set; } +} + +public class UtkaAsayRequest : UtkaBaseMessage +{ + [JsonPropertyName("command")] + public override string? Command => "asay"; + + [JsonPropertyName("a_ckey")] + public string? ACkey { get; set; } + + [JsonPropertyName("message")] + public string? Message { get; set; } +} + +public class UtkaPmRequest : UtkaBaseMessage +{ + [JsonPropertyName("command")] + public override string? Command => "discord_pm"; + + [JsonPropertyName("sender")] + public string? Sender { get; set; } + + [JsonPropertyName("receiver")] + public string? Reciever { get; set; } + + [JsonPropertyName("message")] + public string? Message { get; set; } +} + +public class UtkaPmResponse : UtkaBaseMessage +{ + [JsonPropertyName("command")] + public override string? Command => "discord_pm"; + + [JsonPropertyName("message")] + public bool? Message { get; set; } +} + +public class UtkaWhoRequest : UtkaBaseMessage +{ + [JsonPropertyName("command")] + public override string? Command => "who"; +} + +public class UtkaWhoResponse : UtkaBaseMessage +{ + [JsonPropertyName("command")] + public override string? Command => "who"; + + [JsonPropertyName("players")] + public List? Players { get; set; } +} + +public class UtkaAdminWhoRequest : UtkaBaseMessage +{ + [JsonPropertyName("command")] + public override string? Command => "adminwho"; +} + +public class UtkaAdminWhoResponse : UtkaBaseMessage +{ + [JsonPropertyName("command")] + public override string? Command => "adminwho"; + + [JsonPropertyName("admins")] + public List? Admins { get; set; } +} + +public class UtkaStatusRequsets : UtkaBaseMessage +{ + [JsonPropertyName("command")] + public override string? Command => "status"; +} + +public class UtkaStatusResponse : UtkaBaseMessage +{ + [JsonPropertyName("command")] + public override string? Command => "status"; + + [JsonPropertyName("players")] + public int? Players { get; set; } + + [JsonPropertyName("admins")] + public int? Admins { get; set; } + + [JsonPropertyName("map")] + public string? Map { get; set; } + + [JsonPropertyName("round_duration")] + public double RoundDuration { get; set; } + + [JsonPropertyName("shuttle_status")] + public string? ShuttleStatus { get; set; } + + [JsonPropertyName("station_code")] + public string? StationCode { get; set; } +} + +public class UtkaRoundstatusUpdate : UtkaBaseMessage +{ + [JsonPropertyName("command")] + public override string? Command => "roundstatus"; + + [JsonPropertyName("message")] + public string? Message { get; set; } +} + + +public sealed class UtkaChatEventMessage : UtkaBaseMessage +{ + [JsonPropertyName("ckey")] + public string? Ckey { get; set; } + + [JsonPropertyName("message")] + public string? Message { get; set; } +} + +public sealed class UtkaRoundStatusEvent : UtkaBaseMessage +{ + [JsonPropertyName("command")] + public override string? Command => "roundstatus"; + + [JsonPropertyName("message")] + public string? Message { get; set; } +} + +public sealed class UtkaChatMeEvent : UtkaBaseMessage +{ + [JsonPropertyName("command")] + public override string? Command => "me"; + + [JsonPropertyName("ckey")] + public string? Ckey { get; set; } + + [JsonPropertyName("message")] + public string? Message { get; set; } + + [JsonPropertyName("character_name")] + public string? CharacterName { get; set; } +} diff --git a/Content.Shared/CCVar/CCVars.cs b/Content.Shared/CCVar/CCVars.cs index 6a01bcf2f7..a34d2f762e 100644 --- a/Content.Shared/CCVar/CCVars.cs +++ b/Content.Shared/CCVar/CCVars.cs @@ -1166,7 +1166,7 @@ namespace Content.Shared.CCVar /// /// If true, whenever OOC is disabled the Discord OOC relay will also be disabled. /// - public static readonly CVarDef DisablingOOCDisablesRelay = CVarDef.Create("ooc.disabling_ooc_disables_relay", true, CVar.SERVERONLY); + public static readonly CVarDef DisableHookedOOC = CVarDef.Create("ooc.disabling_ooc_disables_relay", false, CVar.SERVERONLY); //WD-EDIT /// /// Whether or not OOC chat should be enabled during a round. @@ -1361,6 +1361,12 @@ namespace Content.Shared.CCVar public static readonly CVarDef ArrivalsReturns = CVarDef.Create("shuttle.arrivals_returns", false, CVar.SERVERONLY); + /// + /// Whether cargo shuttles are enabled. + /// + public static readonly CVarDef CargoShuttles = + CVarDef.Create("shuttle.cargo", true, CVar.SERVERONLY); + /// /// Whether to automatically spawn escape shuttles. /// diff --git a/Content.Shared/White/WhiteCVars.cs b/Content.Shared/White/WhiteCVars.cs index ae2ae4bc75..0d5d348af9 100644 --- a/Content.Shared/White/WhiteCVars.cs +++ b/Content.Shared/White/WhiteCVars.cs @@ -5,7 +5,7 @@ namespace Content.Shared.White; /* * PUT YOUR CUSTOM VARS HERE * DO IT OR I WILL KILL YOU - * with love, by Hail-Rakes + * with love, by hailrakes */ @@ -56,4 +56,10 @@ public sealed class WhiteCVars public static readonly CVarDef DiscordRoundStartOnly = CVarDef.Create("discord.round_start_only", false, CVar.SERVERONLY); + /* + * Sockets + */ + + public static readonly CVarDef UtkaSocketKey = CVarDef.Create("utka.socket_key", "ass", CVar.SERVERONLY | CVar.CONFIDENTIAL); + } diff --git a/Tools/package_server_build.py b/Tools/package_server_build.py index 78ef15d8d0..853ee75944 100644 --- a/Tools/package_server_build.py +++ b/Tools/package_server_build.py @@ -71,6 +71,7 @@ SERVER_CONTENT_ASSEMBLIES = [ SERVER_EXTRA_ASSEMBLIES = [ "Npgsql.", "Microsoft", + "NetCoreServer" ] SERVER_NOT_EXTRA_ASSEMBLIES = [