#nullable enable using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using Barotrauma.Networking; using Microsoft.Xna.Framework; namespace Barotrauma { internal partial class ReadyCheck { private static LocalizedString ReadyCheckBody(string name) => string.IsNullOrWhiteSpace(name) ? TextManager.Get("readycheck.serverbody") : TextManager.GetWithVariable("readycheck.body", "[player]", name); private static LocalizedString ReadyCheckStatus(int ready, int total) => TextManager.GetWithVariables("readycheck.readycount", ("[ready]", ready.ToString()), ("[total]", total.ToString())); private static LocalizedString ReadyCheckPleaseWait(int seconds) => TextManager.GetWithVariable("readycheck.pleasewait", "[seconds]", seconds.ToString()); private static readonly LocalizedString readyCheckHeader = TextManager.Get("ReadyCheck.Title"); private static readonly LocalizedString noButton = TextManager.Get("No"), yesButton = TextManager.Get("Yes"), closeButton = TextManager.Get("Close"); private const string TimerData = "Timer", PromptData = "ReadyCheck", ResultData = "ReadyCheckResults", UserListData = "ReadyUserList", ReadySpriteData = "ReadySprite"; private int lastSecond = 1; private GUIMessageBox? msgBox; private GUIMessageBox? resultsBox; public static DateTime ReadyCheckCooldown = DateTime.MinValue; public static bool IsReadyCheck(GUIComponent? msgBox) => msgBox?.UserData as string == PromptData || msgBox?.UserData as string == ResultData; private void CreateMessageBox(string author) { Vector2 relativeSize = new Vector2(0.2f / GUI.AspectRatioAdjustment, 0.15f); Point minSize = new Point(300, 200); msgBox = new GUIMessageBox(readyCheckHeader, ReadyCheckBody(author), new[] { yesButton, noButton }, relativeSize, minSize, type: GUIMessageBox.Type.Vote) { UserData = PromptData, Draggable = true }; GUILayoutGroup contentLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.125f), msgBox.Content.RectTransform), childAnchor: Anchor.Center); new GUIProgressBar(new RectTransform(new Vector2(0.8f, 1f), contentLayout.RectTransform), 0.0f, GUIStyle.Orange) { UserData = TimerData }; // Yes msgBox.Buttons[0].OnClicked = delegate { msgBox.Close(); if (GameMain.Client == null) { return true; } SendState(ReadyStatus.Yes); CreateResultsMessage(); return true; }; // No msgBox.Buttons[1].OnClicked = delegate { msgBox.Close(); if (GameMain.Client == null) { return true; } SendState(ReadyStatus.No); CreateResultsMessage(); return true; }; } private void CreateResultsMessage() { if (GameMain.Client == null) { return; } Vector2 relativeSize = new Vector2(0.2f, 0.3f); Point minSize = new Point(300, 400); resultsBox = new GUIMessageBox(readyCheckHeader, string.Empty, new[] { closeButton }, relativeSize, minSize, type: GUIMessageBox.Type.Vote) { UserData = ResultData, Draggable = true }; if (msgBox != null) { resultsBox.RectTransform.ScreenSpaceOffset = msgBox.RectTransform.ScreenSpaceOffset; } GUIListBox listBox = new GUIListBox(new RectTransform(new Vector2(1f, 0.8f), resultsBox.Content.RectTransform)) { UserData = UserListData }; foreach (var (id, status) in Clients) { Client? client = GameMain.Client.ConnectedClients.FirstOrDefault(c => c.SessionId == id); if (client == null) { string list = GameMain.Client.ConnectedClients.Aggregate("Available clients:\n", (current, c) => current + $"{c.SessionId}: {c.Name}\n"); DebugConsole.AddWarning($"Client ID {id} was reported in ready check but was not found.\n" + list.TrimEnd('\n')); continue; } GUIFrame container = new GUIFrame(new RectTransform(new Vector2(1f, 0.15f), listBox.Content.RectTransform), style: "ListBoxElement") { UserData = id }; GUILayoutGroup frame = new GUILayoutGroup(new RectTransform(Vector2.One, container.RectTransform), isHorizontal: true) { Stretch = true }; int height = frame.Rect.Height; JobPrefab? jobPrefab = client?.Character?.Info?.Job?.Prefab; if (jobPrefab?.Icon != null) { // job icon new GUIImage(new RectTransform(new Point(height, height), frame.RectTransform), jobPrefab.Icon, scaleToFit: true) { Color = jobPrefab.UIColor }; } new GUITextBlock(new RectTransform(new Vector2(0.75f, 1), frame.RectTransform), client?.Name ?? $"Unknown ID {id}", jobPrefab?.UIColor ?? Color.White, textAlignment: Alignment.Center) { AutoScaleHorizontal = true }; var statusIcon = new GUIImage(new RectTransform(new Point(height, height), frame.RectTransform), null, scaleToFit: true) { UserData = ReadySpriteData }; UpdateStatusIcon(statusIcon, status); } resultsBox.Buttons[0].OnClicked = delegate { resultsBox.Close(); return true; }; } private void UpdateBar() { double elapsedTime = (DateTime.Now - startTime).TotalSeconds; if (msgBox is { Closed: false } && GUIMessageBox.MessageBoxes.Contains(msgBox)) { if (msgBox.FindChild(TimerData, true) is GUIProgressBar bar) { bar.BarSize = (float)(elapsedTime / (endTime - startTime).TotalSeconds); } } // play click sound after a second has passed int second = (int)Math.Ceiling(elapsedTime); if (second > lastSecond) { if (msgBox is { Closed: false }) { SoundPlayer.PlayUISound(GUISoundType.PopupMenu); } lastSecond = second; } } private static void CloseLingeringPopups() { foreach (GUIComponent box in GUIMessageBox.MessageBoxes.ToImmutableArray()) { if (box is not GUIMessageBox msgBox) { continue; } if (msgBox.UserData is PromptData or ResultData) { msgBox.Close(); } } } public static void ClientRead(IReadMessage inc) { ReadyCheckState state = (ReadyCheckState)inc.ReadByte(); CrewManager? crewManager = GameMain.GameSession?.CrewManager; var otherClients = GameMain.Client.ConnectedClients; if (crewManager == null || otherClients == null) { if (state == ReadyCheckState.Start) { SendState(ReadyStatus.No); } return; } switch (state) { case ReadyCheckState.Start: CloseLingeringPopups(); bool isOwn = false; byte authorId = 0; long startTime = inc.ReadInt64(); long endTime = inc.ReadInt64(); string author = inc.ReadString(); bool hasAuthor = inc.ReadBoolean(); if (hasAuthor) { authorId = inc.ReadByte(); isOwn = authorId == GameMain.Client.SessionId; } ushort clientCount = inc.ReadUInt16(); List clients = new List(); for (int i = 0; i < clientCount; i++) { clients.Add(inc.ReadByte()); } ReadyCheck rCheck = new ReadyCheck(clients, DateTimeOffset.FromUnixTimeSeconds(startTime).LocalDateTime, DateTimeOffset.FromUnixTimeSeconds(endTime).LocalDateTime); crewManager.ActiveReadyCheck = rCheck; if (isOwn) { SendState(ReadyStatus.Yes); rCheck.CreateResultsMessage(); } else { rCheck.CreateMessageBox(author); } if (hasAuthor && rCheck.Clients.ContainsKey(authorId)) { rCheck.Clients[authorId] = ReadyStatus.Yes; } break; case ReadyCheckState.Update: ReadyStatus newState = (ReadyStatus)inc.ReadByte(); byte targetId = inc.ReadByte(); crewManager.ActiveReadyCheck?.UpdateState(targetId, newState); break; case ReadyCheckState.End: ushort count = inc.ReadUInt16(); for (int i = 0; i < count; i++) { byte id = inc.ReadByte(); ReadyStatus status = (ReadyStatus)inc.ReadByte(); crewManager.ActiveReadyCheck?.UpdateState(id, status); } crewManager.ActiveReadyCheck?.EndReadyCheck(); crewManager.ActiveReadyCheck?.msgBox?.Close(); crewManager.ActiveReadyCheck = null; break; } } partial void EndReadyCheck() { if (IsFinished) { return; } IsFinished = true; int readyCount = Clients.Count(static pair => pair.Value == ReadyStatus.Yes); int totalCount = Clients.Count; GameMain.Client.AddChatMessage(ChatMessage.Create(string.Empty, ReadyCheckStatus(readyCount, totalCount).Value, ChatMessageType.Server, null)); } private void UpdateState(byte id, ReadyStatus status) { if (Clients.ContainsKey(id)) { Clients[id] = status; } if (resultsBox == null || resultsBox.Closed || !GUIMessageBox.MessageBoxes.Contains(resultsBox)) { return; } if (resultsBox.Content.FindChild(UserListData) is not GUIListBox userList) { return; } var child = userList.Content.FindChild(id); if (child?.GetChild().FindChild(ReadySpriteData) is not GUIImage image) { return; } UpdateStatusIcon(image, status); } private static void UpdateStatusIcon(GUIImage image, ReadyStatus status) { string style; switch (status) { case ReadyStatus.Yes: style = "MissionCompletedIcon"; break; case ReadyStatus.No: style = "MissionFailedIcon"; break; default: return; } image.ApplyStyle(GUIStyle.GetComponentStyle(style)); } private static void SendState(ReadyStatus status) { IWriteMessage msg = new WriteOnlyMessage(); msg.WriteByte((byte)ClientPacketHeader.READY_CHECK); msg.WriteByte((byte)ReadyCheckState.Update); msg.WriteByte((byte)status); GameMain.Client?.ClientPeer?.Send(msg, DeliveryMethod.Reliable); } public static void CreateReadyCheck() { if (ReadyCheckCooldown < DateTime.Now) { #if !DEBUG ReadyCheckCooldown = DateTime.Now.AddMinutes(1); #endif IWriteMessage msg = new WriteOnlyMessage(); msg.WriteByte((byte)ClientPacketHeader.READY_CHECK); msg.WriteByte((byte)ReadyCheckState.Start); GameMain.Client?.ClientPeer?.Send(msg, DeliveryMethod.Reliable); return; } GUIMessageBox msgBox = new GUIMessageBox(readyCheckHeader, ReadyCheckPleaseWait((ReadyCheckCooldown - DateTime.Now).Seconds), new[] { closeButton }); msgBox.Buttons[0].OnClicked = delegate { msgBox.Close(); return true; }; } } }