diff --git a/Source/Client/Debug/DebugActions.cs b/Source/Client/Debug/DebugActions.cs index d37d727dd..3e46f9fc5 100644 --- a/Source/Client/Debug/DebugActions.cs +++ b/Source/Client/Debug/DebugActions.cs @@ -10,8 +10,10 @@ using LudeonTK; using Multiplayer.Client.Desyncs; using Multiplayer.Client.Util; +using Multiplayer.Client.Windows; using RimWorld; using RimWorld.Planet; +using Steamworks; using UnityEngine; using Verse; using Debug = UnityEngine.Debug; @@ -147,6 +149,12 @@ public static void TriggerDesync() Multiplayer.game.sync.TryAddStackTraceForDesyncLogRaw(logItem, depth, hash); } + [DebugAction(MultiplayerCategory, name = "Show pending player", allowedGameStates = AllowedGameStates.Playing)] + public static void ShowPendingPlayer() + { + PendingPlayerWindow.EnqueueJoinRequest(SteamUser.GetSteamID(), (_, _) => { }); + } + [DebugAction(MultiplayerCategory, "Dump Sync Types", allowedGameStates = AllowedGameStates.Entry)] public static void DumpSyncTypes() { diff --git a/Source/Client/Networking/SteamIntegration.cs b/Source/Client/Networking/SteamIntegration.cs index fcd25b33a..1a340a180 100644 --- a/Source/Client/Networking/SteamIntegration.cs +++ b/Source/Client/Networking/SteamIntegration.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Diagnostics; using Multiplayer.Client.Networking; +using Multiplayer.Client.Windows; using RimWorld; using Steamworks; using UnityEngine; @@ -33,8 +34,13 @@ public static void InitCallbacks() if (Multiplayer.settings.autoAcceptSteam) SteamNetworking.AcceptP2PSessionWithUser(req.m_steamIDRemote); else + { session.pendingSteam.Add(req.m_steamIDRemote); - + PendingPlayerWindow.EnqueueJoinRequest(req.m_steamIDRemote, (joinReq, accepted) => + { + if(joinReq.steamId.HasValue && accepted) AcceptPlayerJoinRequest(joinReq.steamId.Value); + }); + } session.knownUsers.Add(req.m_steamIDRemote); session.NotifyChat(); @@ -64,6 +70,14 @@ public static void InitCallbacks() }); } + public static void AcceptPlayerJoinRequest(CSteamID id) + { + SteamNetworking.AcceptP2PSessionWithUser(id); + Multiplayer.session.pendingSteam.Remove(id); + + Messages.Message("MpSteamAccepted".Translate(), MessageTypeDefOf.PositiveEvent, false); + } + private static Stopwatch lastSteamUpdate = Stopwatch.StartNew(); private static bool lastLocalSteam; // running a server with steam networking private static CSteamID? lastRemoteSteam; // connected to a server with steam networking diff --git a/Source/Client/Util/RectExtensions.cs b/Source/Client/Util/RectExtensions.cs index ad4aa368f..8ff068e1e 100644 --- a/Source/Client/Util/RectExtensions.cs +++ b/Source/Client/Util/RectExtensions.cs @@ -1,4 +1,5 @@ using UnityEngine; +using Verse; namespace Multiplayer.Client { @@ -109,5 +110,76 @@ public static Rect AtY(this Rect rect, float y) rect.y = y; return rect; } + + public static Rect FitToText(this Rect rect, string text) + { + var textSize = Text.CalcSize(text); + rect.width = textSize.x; + rect.height = textSize.y; + return rect; + } + + public static Rect MarginLeft(this Rect rect, float margin) + { + rect.x += margin; + rect.width -= margin; + return rect; + } + + public static Rect MarginRight(this Rect rect, float margin) + { + rect.width -= margin; + return rect; + } + + public static Rect MarginTop(this Rect rect, float margin) + { + rect.y += margin; + rect.height -= margin; + return rect; + } + + public static Rect MarginBottom(this Rect rect, float margin) + { + rect.height -= margin; + return rect; + } + + public static Rect CenteredButtonX(this Rect rect, int buttonIndex, + int buttonsCount, + float buttonWidth, + float pad = 10f) + { + rect.x += GenUI.GetCenteredButtonPos(buttonIndex, buttonsCount, rect.width, buttonWidth, pad); + rect.width = buttonWidth; + return rect; + } + + public static Rect SpacedEvenlyX(this Rect rect, int buttonIndex, int buttonsCount, float buttonWidth) + { + float totalWidth = rect.width; + float pad = (float)(((double) totalWidth - (double)buttonsCount * (double) buttonWidth) / (double) (buttonsCount + 1)); + rect.x += Mathf.Floor(pad + buttonIndex * (buttonWidth + pad)); + rect.width = buttonWidth; + return rect; + } + + public static Rect SpacedAroundX(this Rect rect, int buttonIndex, int buttonsCount, float buttonWidth) + { + float totalWidth = rect.width; + float pad = (float)(((double) totalWidth - (double)buttonsCount * (double) buttonWidth) / (double) (buttonsCount)); + rect.x += Mathf.Floor(pad/2 + buttonIndex * (buttonWidth + pad)); + rect.width = buttonWidth; + return rect; + } + + public static Rect SpacedBetweenX(this Rect rect, int buttonIndex, int buttonsCount, float buttonWidth) + { + float totalWidth = rect.width; + float pad = (float)(((double) totalWidth - (double)buttonsCount * (double) buttonWidth) / (double) (buttonsCount - 1)); + rect.x += Mathf.Floor(buttonIndex * (buttonWidth + pad)); + rect.width = buttonWidth; + return rect; + } } } diff --git a/Source/Client/Windows/ChatWindow.cs b/Source/Client/Windows/ChatWindow.cs index c79067f61..a05dce4e5 100644 --- a/Source/Client/Windows/ChatWindow.cs +++ b/Source/Client/Windows/ChatWindow.cs @@ -154,7 +154,7 @@ private void DrawInfo(Rect inRect) SteamFriends.GetFriendPersonaName, ref inRect, ref steamScroll, - AcceptSteamPlayer, + SteamIntegration.AcceptPlayerJoinRequest, true, "MpSteamAcceptDesc".Translate() ); @@ -174,14 +174,6 @@ private void ClickPlayer(PlayerInfo p) } } - private void AcceptSteamPlayer(CSteamID id) - { - SteamNetworking.AcceptP2PSessionWithUser(id); - Multiplayer.session.pendingSteam.Remove(id); - - Messages.Message("MpSteamAccepted".Translate(), MessageTypeDefOf.PositiveEvent, false); - } - private Color GetColor(PlayerStatus status) { switch (status) diff --git a/Source/Client/Windows/ConnectingWindow.cs b/Source/Client/Windows/ConnectingWindow.cs index 804738a7a..65b203905 100644 --- a/Source/Client/Windows/ConnectingWindow.cs +++ b/Source/Client/Windows/ConnectingWindow.cs @@ -134,8 +134,7 @@ public class RejoiningWindow : BaseConnectingWindow public class ConnectingWindow(string address, int port) : BaseConnectingWindow { - protected override string ConnectingString => - string.Format("MpConnectingTo".Translate("{0}", port), address); + protected override string ConnectingString => "MpConnectingTo".Translate(address, port); } public class SteamConnectingWindow(CSteamID hostId) : BaseConnectingWindow diff --git a/Source/Client/Windows/HostWindow.cs b/Source/Client/Windows/HostWindow.cs index 3a0f80002..d976c9ee0 100644 --- a/Source/Client/Windows/HostWindow.cs +++ b/Source/Client/Windows/HostWindow.cs @@ -427,6 +427,8 @@ private void TryHost() else HostFromSpSaveFile(settings); + // No need to return to the server browser since we successfully started a local server. + returnToServerBrowser = false; Close(); } diff --git a/Source/Client/Windows/PendingPlayerWindow.cs b/Source/Client/Windows/PendingPlayerWindow.cs new file mode 100644 index 000000000..b2afa01f2 --- /dev/null +++ b/Source/Client/Windows/PendingPlayerWindow.cs @@ -0,0 +1,192 @@ +#nullable enable +using System; +using System.Collections.Concurrent; +using LudeonTK; +using Multiplayer.Client.Util; +using Steamworks; +using UnityEngine; +using Verse; + +namespace Multiplayer.Client.Windows; + +public class PendingPlayerWindow : Window +{ + private static PendingPlayerWindow? Opened => Find.WindowStack?.WindowOfType(); + + public static void EnqueueJoinRequest(string name, Action? callback = null) => Request.Enqueue(name, callback); + public static void EnqueueJoinRequest(CSteamID steamId, Action? callback = null) => Request.Enqueue(steamId, callback); + + public record Request + { + public readonly CSteamID? steamId; + public readonly string name; + private readonly Action? callback; + + private Request(CSteamID? steamId, string name, Action? callback) + { + this.steamId = steamId; + this.name = name; + this.callback = callback; + } + + internal void RunCallback(bool accepted) + { + try + { + callback?.Invoke(this, accepted); + } + catch (Exception e) + { + MpLog.Warn($"Exception invoking join request callback for {steamId}:{name}: {e}"); + } + } + + private void Enqueue() + { + var window = Opened; + if (window != null) + { + window.queue.Enqueue(this); + return; + } + + window = new PendingPlayerWindow(); + window.queue.Enqueue(this); + Find.WindowStack.Add(window); + } + + public static void Enqueue(string name, Action? callback = null) => + new Request(null, name, callback).Enqueue(); + + public static void Enqueue(CSteamID steamId, Action? callback = null) => + new Request(steamId, SteamFriends.GetFriendPersonaName(steamId), callback).Enqueue(); + } + + private const float BtnMargin = 8f; + private const float BtnWidth = 65f; + private const float AnimTimeSecs = .7f; + private float startTime; + + private readonly ConcurrentQueue queue = []; + private Request? req; + + private PendingPlayerWindow() + { + preventCameraMotion = false; + focusWhenOpened = false; + closeOnClickedOutside = false; + closeOnCancel = false; + closeOnAccept = false; + layer = WindowLayer.GameUI; + } + + public override Vector2 InitialSize => new(200f, req?.steamId.HasValue == true ? 320f : 245f); + public override float Margin => 4f; + + public override void PreOpen() + { + startTime = Time.time; + if (req == null) ShowNextRequestOrClose(); + base.PreOpen(); + } + + public override void SetInitialSizeAndPosition() + { + // Add scaled 1f to hide the right border of the window + windowRect = new Rect(UI.screenWidth - InitialSize.y + UIScaling.AdjustCoordToUIScalingCeil(1f), + InitialSize.x, InitialSize.y, 96f); + } + + private void UpdateWindowRect() + { + var arrivedAgo = Time.time - startTime; + var animFinished = arrivedAgo > AnimTimeSecs; + if (!animFinished) + { + var timeProgress = arrivedAgo / AnimTimeSecs; + var posProgress = 1 - Math.Pow(1 - timeProgress, 2.5); + windowRect.x = UI.screenWidth - (float)posProgress * InitialSize.y + + UIScaling.AdjustCoordToUIScalingCeil(1f); + } else SetInitialSizeAndPosition(); + } + + public override void WindowOnGUI() + { + UpdateWindowRect(); + windowRect = GUI.Window(ID, windowRect, innerWindowOnGUICached, "", windowDrawing.EmptyStyle); + } + + public override void DoWindowContents(Rect inRect) + { + // This should not happen + if (req == null) return; + if (req.steamId.HasValue) { + var avatarTex = SteamImages.GetTexture(SteamFriends.GetLargeFriendAvatar(SteamUser.GetSteamID())); + var avatarRect = new Rect(0, 0, 80, 80).CenteredOnYIn(inRect).Right(4); + InvisibleOpenSteamProfileButton(avatarRect, req.steamId, doMouseoverSound: false); + if (avatarTex != null) + GUI.DrawTextureWithTexCoords(avatarRect, avatarTex, new Rect(0, 1, 1, -1)); + inRect.xMin = avatarRect.xMax + 6f; + } + else + { + inRect.xMin += 15f; + } + + using (MpStyle.Set(TextAnchor.UpperLeft).Set(WordWrap.NoWrap).Set(GameFont.Medium)) + { + var textRect = inRect; + + string usernameClamped = Text.ClampTextWithEllipsis(textRect, req.name); + var nameTextRect = textRect.FitToText(usernameClamped); + + var showTooltip = usernameClamped.Length != req.name.Length; + if (req.steamId.HasValue || showTooltip) + Widgets.DrawHighlightIfMouseover(nameTextRect.ExpandedBy(3f, 0f)); + if (showTooltip) TooltipHandler.TipRegion(nameTextRect, new TipSignal(req.name)); + + Widgets.Label(nameTextRect, usernameClamped); + InvisibleOpenSteamProfileButton(nameTextRect, req.steamId); + inRect = inRect.MarginTop(nameTextRect.height + Text.SpaceBetweenLines); + } + + using (MpStyle.Set(TextAnchor.UpperLeft).Set(WordWrap.NoWrap).Set(GameFont.Small)) + Widgets.Label(inRect, "MpJoinRequestDesc".Translate()); + + var btnGroupRect = inRect.MarginTop(Text.LineHeightOf(GameFont.Small) + BtnMargin).MarginBottom(4f).MarginRight(6f); + + var btnOkRect = btnGroupRect.Width(BtnWidth); + if (Widgets.ButtonText(btnOkRect, "Accept".Translate(), overrideTextAnchor: TextAnchor.MiddleCenter)) + { + req.RunCallback(true); + ShowNextRequestOrClose(); + } + + var btnNoRect = btnOkRect.Right(btnOkRect.width + BtnMargin); + if (Widgets.ButtonText(btnNoRect, "RejectLetter".Translate(), overrideTextAnchor: TextAnchor.MiddleCenter)) { + req.RunCallback(false); + ShowNextRequestOrClose(); + } + } + + // Try our best to make sure there is no possibility of missing a request + public override bool OnCloseRequest() => queue.IsEmpty; + + private static void InvisibleOpenSteamProfileButton(Rect rect, CSteamID? steamId, bool doMouseoverSound = true) + { + if (steamId.HasValue && Widgets.ButtonInvisible(rect, doMouseoverSound)) + SteamFriends.ActivateGameOverlayToUser("steamid", steamId.Value); + } + + private void ShowNextRequestOrClose() + { + if (queue.TryDequeue(out req)) + { + // Window size differs if a Steam ID is present (extra space for avatar) + UpdateWindowRect(); + return; + } + + Close(); + } +}