From e23d522a4e966920651802ebc67ab636599bb466 Mon Sep 17 00:00:00 2001 From: Pierce Thompson Date: Tue, 8 Aug 2023 00:26:19 -0400 Subject: [PATCH 01/34] Begin creating the server browser menu This is still very incomplete, and is just laying the groundwork for future progress. --- Multiplayer/Locale.cs | 2 + .../MainMenu/RightPaneControllerPatch.cs | 60 +++++++++++++------ locale.csv | 1 + 3 files changed, 44 insertions(+), 19 deletions(-) diff --git a/Multiplayer/Locale.cs b/Multiplayer/Locale.cs index 46a1115..c70c689 100644 --- a/Multiplayer/Locale.cs +++ b/Multiplayer/Locale.cs @@ -32,6 +32,8 @@ public static class Locale public static string SERVER_BROWSER__TITLE => Get(SERVER_BROWSER__TITLE_KEY); public const string SERVER_BROWSER__TITLE_KEY = $"{PREFIX_SERVER_BROWSER}/title"; + public static string SERVER_BROWSER__MANUAL_CONNECT => Get(SERVER_BROWSER__MANUAL_CONNECT_KEY); + public const string SERVER_BROWSER__MANUAL_CONNECT_KEY = $"{PREFIX_SERVER_BROWSER}/manual_connect"; public static string SERVER_BROWSER__IP => Get(SERVER_BROWSER__IP_KEY); private const string SERVER_BROWSER__IP_KEY = $"{PREFIX_SERVER_BROWSER}/ip"; diff --git a/Multiplayer/Patches/MainMenu/RightPaneControllerPatch.cs b/Multiplayer/Patches/MainMenu/RightPaneControllerPatch.cs index 8302d8a..da66224 100644 --- a/Multiplayer/Patches/MainMenu/RightPaneControllerPatch.cs +++ b/Multiplayer/Patches/MainMenu/RightPaneControllerPatch.cs @@ -13,36 +13,58 @@ public static class RightPaneController_OnEnable_Patch { private static void Prefix(RightPaneController __instance) { - if (__instance.FindChildByName("PaneRight Multiplayer")) + if (__instance.HasChildWithName("PaneRight Multiplayer")) return; - GameObject launcher = __instance.FindChildByName("PaneRight Launcher"); - if (launcher == null) - { - Multiplayer.LogError("Failed to find Launcher pane!"); - return; - } + GameObject basePane = __instance.FindChildByName("PaneRight Settings"); - launcher.SetActive(false); - GameObject multiplayerPane = Object.Instantiate(launcher, launcher.transform.parent); - launcher.SetActive(true); + basePane.SetActive(false); + GameObject multiplayerPane = Object.Instantiate(basePane, basePane.transform.parent); + basePane.SetActive(true); multiplayerPane.name = "PaneRight Multiplayer"; __instance.menuController.controlledMenus.Add(multiplayerPane.GetComponent()); MainMenuController_Awake_Patch.MultiplayerButton.GetComponent().requestedMenuIndex = __instance.menuController.controlledMenus.Count - 1; - Object.Destroy(multiplayerPane.GetComponent()); - Object.Destroy(multiplayerPane.FindChildByName("Thumb Background")); - Object.Destroy(multiplayerPane.FindChildByName("Thumbnail")); - Object.Destroy(multiplayerPane.FindChildByName("Savegame Details Background")); - Object.Destroy(multiplayerPane.FindChildByName("ButtonTextIcon Run")); + Object.Destroy(multiplayerPane.GetComponent()); + Object.Destroy(multiplayerPane.GetComponent()); + Object.Destroy(multiplayerPane.FindChildByName("Left Buttons")); + Object.Destroy(multiplayerPane.FindChildByName("Text Content")); - GameObject titleObj = multiplayerPane.FindChildByName("Title"); - if (titleObj == null) + GameObject rightSubMenus = multiplayerPane.FindChildByName("Right Submenus"); + GameObject languagePane = rightSubMenus.FindChildByName("PaneRight Language"); + + RectTransform langRect = languagePane.GetComponent(); + RectTransform subMenusRect = rightSubMenus.GetComponent(); + + Vector2 sizeDelta = new(1290, 600); + subMenusRect.sizeDelta = sizeDelta; + langRect.sizeDelta = sizeDelta; + + foreach (GameObject go in rightSubMenus.GetChildren()) { - Multiplayer.LogError("Failed to find title object!"); - return; + if (go.name == "PaneRight Language") + continue; + Object.Destroy(go); } + GameObject viewport = languagePane.FindChildByName("Viewport"); + foreach (GameObject go in viewport.GetChildren()) + Object.Destroy(go); + + Object.Destroy(languagePane.FindChildByName("Title")); + Object.Destroy(languagePane.FindChildByName("Help button")); + Object.Destroy(languagePane.FindChildByName("Text Content")); + Object.Destroy(languagePane.FindChildByName("ButtonTextIcon")); + Object.Destroy(languagePane.GetComponent()); + Object.Destroy(languagePane.GetComponent()); + Object.Destroy(multiplayerPane.FindChildByName("Selector Preset")); + Object.Destroy(multiplayerPane.FindChildByName("ButtonTextIcon Discard")); + + GameObject manualConnect = multiplayerPane.FindChildByName("ButtonTextIcon Apply"); + manualConnect.GetComponentInChildren().key = Locale.SERVER_BROWSER__MANUAL_CONNECT_KEY; + Object.Destroy(manualConnect.GetComponentInChildren()); + + GameObject titleObj = multiplayerPane.FindChildByName("Title"); titleObj.GetComponentInChildren().key = Locale.SERVER_BROWSER__TITLE_KEY; Object.Destroy(titleObj.GetComponentInChildren()); diff --git a/locale.csv b/locale.csv index 62c1545..4a250c0 100644 --- a/locale.csv +++ b/locale.csv @@ -10,6 +10,7 @@ mm/join_server__tooltip_disabled,Unused,,,,,,,,,,,,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,, ,Server Browser,,,,,,,,,,,,,,,,,,,,,,,,,, sb/title,The title of the Server Browser tab,Server Browser,,,,,,,,,,,,,,,,,,,,,,,,, +sb/manual_connect,The Manual Connect button,Connect Manually,,,,,,,,,,,,,,,,,,,,,,,,, sb/ip,IP popup,Enter IP Address,,,,,,,,,,,,,,,,,,,,,,,,, sb/ip_invalid,Invalid IP popup.,Invalid IP Address!,,,,,,,,,,,,,,,,,,,,,,,,, sb/port,Port popup.,Enter Port (7777 by default),,,,,,,,,,,,,,,,,,,,,,,,, From 764bfc70fadbd61e87fb4a926db4469c45f3abde Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 12 May 2024 09:48:19 +1000 Subject: [PATCH 02/34] Fixed minor issue with CSV parsing so that unix/windows line breaks don't matter. --- Multiplayer/Utils/Csv.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Multiplayer/Utils/Csv.cs b/Multiplayer/Utils/Csv.cs index 560fb24..6ddde1e 100644 --- a/Multiplayer/Utils/Csv.cs +++ b/Multiplayer/Utils/Csv.cs @@ -1,3 +1,4 @@ +using System; using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; @@ -14,7 +15,8 @@ public static class Csv /// public static ReadOnlyDictionary> Parse(string data) { - string[] lines = data.Split('\n'); + string[] separators = new string[]{"\r\n" }; + string[] lines = data.Split(separators, StringSplitOptions.None); // Dictionary> OrderedDictionary columns = new(lines.Length - 1); From c691e32b1c9466bdb69528b7aeade87d778be035 Mon Sep 17 00:00:00 2001 From: morm075 <124874578+morm075@users.noreply.github.com> Date: Sat, 25 May 2024 21:02:41 +0930 Subject: [PATCH 03/34] refactoring und updating update to network game --- .../MainMenu/MainMenuThingsAndStuff.cs | 1 + .../Components/MainMenu/MultiplayerPane.cs | 269 +++++++++++++----- ...pupTextInputFieldControllerNoValidation.cs | 93 ++++++ .../SaveGame/StartGameData_ServerSave.cs | 7 +- Multiplayer/Locale.cs | 214 ++++++++------ Multiplayer/Multiplayer.cs | 1 - Multiplayer/Multiplayer.csproj | 3 + .../CommsRadio/CommsRadioCarDeleterPatch.cs | 4 +- .../MainMenu/LocalizationManagerPatch.cs | 33 ++- .../MainMenu/MainMenuControllerPatch.cs | 80 ++++-- .../MainMenu/RightPaneControllerPatch.cs | 163 +++++++---- Multiplayer/Settings.cs | 12 +- Multiplayer/Utils/Csvnew.cs | 94 ++++++ compare | 0 locale.csv | 50 +++- 15 files changed, 740 insertions(+), 284 deletions(-) create mode 100644 Multiplayer/Components/MainMenu/PopupTextInputFieldControllerNoValidation.cs create mode 100644 Multiplayer/Utils/Csvnew.cs create mode 100644 compare diff --git a/Multiplayer/Components/MainMenu/MainMenuThingsAndStuff.cs b/Multiplayer/Components/MainMenu/MainMenuThingsAndStuff.cs index 02a6d6b..9920071 100644 --- a/Multiplayer/Components/MainMenu/MainMenuThingsAndStuff.cs +++ b/Multiplayer/Components/MainMenu/MainMenuThingsAndStuff.cs @@ -63,6 +63,7 @@ public void SwitchToMenu(byte index) [CanBeNull] public Popup ShowRenamePopup() { + Debug.Log("public Popup ShowRenamePopup() ..."); return ShowPopup(renamePopupPrefab); } diff --git a/Multiplayer/Components/MainMenu/MultiplayerPane.cs b/Multiplayer/Components/MainMenu/MultiplayerPane.cs index be42068..a3d2f16 100644 --- a/Multiplayer/Components/MainMenu/MultiplayerPane.cs +++ b/Multiplayer/Components/MainMenu/MultiplayerPane.cs @@ -1,120 +1,249 @@ -using System; +using System; +using System.Net; using System.Text.RegularExpressions; +using DV.Localization; +using DV.UI; using DV.UIFramework; using DV.Utils; +using Multiplayer.Components.MainMenu; +using Multiplayer; using Multiplayer.Components.Networking; +using Multiplayer.Patches.MainMenu; +using Multiplayer.Utils; +using TMPro; using UnityEngine; -namespace Multiplayer.Components.MainMenu; - -public class MultiplayerPane : MonoBehaviour +namespace Multiplayer.Components.MainMenu { - // @formatter:off - // Patterns from https://ihateregex.io/ - private static readonly Regex IPv4 = new(@"(\b25[0-5]|\b2[0-4][0-9]|\b[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}"); - private static readonly Regex IPv6 = new(@"(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))"); - private static readonly Regex PORT = new(@"^((6553[0-5])|(655[0-2][0-9])|(65[0-4][0-9]{2})|(6[0-4][0-9]{3})|([1-5][0-9]{4})|([0-5]{0,5})|([0-9]{1,4}))$"); - // @formatter:on + public class MultiplayerPane : MonoBehaviour + { + // Regular expressions for IP and port validation + private static readonly Regex IPv4Regex = new Regex(@"(\b25[0-5]|\b2[0-4][0-9]|\b[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}"); + private static readonly Regex IPv6Regex = new Regex(@"(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))"); + private static readonly Regex PortRegex = new Regex(@"^((6553[0-5])|(655[0-2][0-9])|(65[0-4][0-9]{2})|(6[0-4][0-9]{3})|([1-5][0-9]{4})|([0-5]{0,5})|([0-9]{1,4}))$"); - private bool why; + private string ipAddress; + private ushort portNumber; + private ButtonDV directButton; - private string address; - private ushort port; + private void Awake() + { + Multiplayer.Log("MultiplayerPane Awake()"); + SetupMultiplayerButtons(); + } - private void OnEnable() - { - if (!why) + private void SetupMultiplayerButtons() { - why = true; - return; + GameObject buttonDirectIP = GameObject.Find("ButtonTextIcon Manual"); + GameObject buttonHost = GameObject.Find("ButtonTextIcon Host"); + GameObject buttonJoin = GameObject.Find("ButtonTextIcon Join"); + GameObject buttonRefresh = GameObject.Find("ButtonTextIcon Refresh"); + + if (buttonDirectIP == null || buttonHost == null || buttonJoin == null || buttonRefresh == null) + { + Multiplayer.LogError("One or more buttons not found."); + return; + } + + // Modify the existing buttons' properties + ModifyButton(buttonDirectIP, Locale.SERVER_BROWSER__MANUAL_CONNECT_KEY); + ModifyButton(buttonHost, Locale.SERVER_BROWSER__HOST_KEY); + ModifyButton(buttonJoin, Locale.SERVER_BROWSER__JOIN_KEY); + //ModifyButton(buttonRefresh, Locale.SERVER_BROWSER__REFRESH); + + // Set up event listeners and localization for DirectIP button + ButtonDV buttonDirectIPDV = buttonDirectIP.GetComponent(); + buttonDirectIPDV.onClick.AddListener(ShowIpPopup); + + // Set up event listeners and localization for Host button + ButtonDV buttonHostDV = buttonHost.GetComponent(); + buttonHostDV.onClick.AddListener(HostAction); + + // Set up event listeners and localization for Join button + ButtonDV buttonJoinDV = buttonJoin.GetComponent(); + buttonJoinDV.onClick.AddListener(JoinAction); + + // Set up event listeners and localization for Refresh button + //ButtonDV buttonRefreshDV = buttonRefresh.GetComponent(); + //buttonRefreshDV.onClick.AddListener(RefreshAction); + + //Debug.Log("Setting buttons active: " + buttonDirectIP.name + ", " + buttonHost.name + ", " + buttonJoin.name + ", " + buttonRefresh.name ); + Debug.Log("Setting buttons active: " + buttonDirectIP.name + ", " + buttonHost.name + ", " + buttonJoin.name ); + buttonDirectIP.SetActive(true); + buttonHost.SetActive(true); + buttonJoin.SetActive(true); + //buttonRefresh.SetActive(true); } - ShowIpPopup(); - } + private GameObject FindButton(string name) + { + return GameObject.Find(name); + } - private void ShowIpPopup() - { - Popup popup = MainMenuThingsAndStuff.Instance.ShowRenamePopup(); - if (popup == null) - return; + private void ModifyButton(GameObject button, string key) + { + button.GetComponentInChildren().key = key; - popup.labelTMPro.text = Locale.SERVER_BROWSER__IP; + } - popup.Closed += result => + private void ShowIpPopup() { - if (result.closedBy == PopupClosedByAction.Abortion) + Debug.Log("In ShowIpPpopup"); + var popup = MainMenuThingsAndStuff.Instance.ShowRenamePopup(); + if (popup == null) { - MainMenuThingsAndStuff.Instance.SwitchToDefaultMenu(); + Multiplayer.LogError("Popup not found."); return; } - if (!IPv4.IsMatch(result.data) && !IPv6.IsMatch(result.data)) + popup.labelTMPro.text = Locale.SERVER_BROWSER__IP; + popup.GetComponentInChildren().text = Multiplayer.Settings.LastRemoteIP; + + popup.Closed += result => + { + if (result.closedBy == PopupClosedByAction.Abortion) + { + MainMenuThingsAndStuff.Instance.SwitchToDefaultMenu(); + return; + } + + HandleIpAddressInput(result.data); + }; + } + + private void HandleIpAddressInput(string input) + { + if (!IPv4Regex.IsMatch(input) && !IPv6Regex.IsMatch(input)) { ShowOkPopup(Locale.SERVER_BROWSER__IP_INVALID, ShowIpPopup); return; } - address = result.data; - + ipAddress = input; ShowPortPopup(); - }; - } + } - private void ShowPortPopup() - { - Popup popup = MainMenuThingsAndStuff.Instance.ShowRenamePopup(); - if (popup == null) - return; + private void ShowPortPopup() + { + var popup = MainMenuThingsAndStuff.Instance.ShowRenamePopup(); + if (popup == null) + { + Multiplayer.LogError("Popup not found."); + return; + } + + popup.labelTMPro.text = Locale.SERVER_BROWSER__PORT; + popup.GetComponentInChildren().text = Multiplayer.Settings.LastRemotePort.ToString(); - popup.labelTMPro.text = Locale.SERVER_BROWSER__PORT; + popup.Closed += result => + { + if (result.closedBy == PopupClosedByAction.Abortion) + { + MainMenuThingsAndStuff.Instance.SwitchToDefaultMenu(); + return; + } + + HandlePortInput(result.data); + }; + } - popup.Closed += result => + private void HandlePortInput(string input) { - if (result.closedBy == PopupClosedByAction.Abortion) + if (!PortRegex.IsMatch(input)) { - MainMenuThingsAndStuff.Instance.SwitchToDefaultMenu(); + ShowOkPopup(Locale.SERVER_BROWSER__PORT_INVALID, ShowPortPopup); return; } - if (!PORT.IsMatch(result.data)) + portNumber = ushort.Parse(input); + ShowPasswordPopup(); + } + + private void ShowPasswordPopup() + { + var popup = MainMenuThingsAndStuff.Instance.ShowRenamePopup(); + if (popup == null) { - ShowOkPopup(Locale.SERVER_BROWSER__PORT_INVALID, ShowPortPopup); + Multiplayer.LogError("Popup not found."); return; } - port = ushort.Parse(result.data); + popup.labelTMPro.text = Locale.SERVER_BROWSER__PASSWORD; + popup.GetComponentInChildren().text = Multiplayer.Settings.LastRemotePassword; - ShowPasswordPopup(); - }; - } + DestroyImmediate(popup.GetComponentInChildren()); + popup.GetOrAddComponent(); - private void ShowPasswordPopup() - { - Popup popup = MainMenuThingsAndStuff.Instance.ShowRenamePopup(); - if (popup == null) - return; + popup.Closed += result => + { + if (result.closedBy == PopupClosedByAction.Abortion) return; + + directButton.enabled = false; + SingletonBehaviour.Instance.StartClient(ipAddress, portNumber, result.data); + + Multiplayer.Settings.LastRemoteIP = ipAddress; + Multiplayer.Settings.LastRemotePort = portNumber; + Multiplayer.Settings.LastRemotePassword = result.data; + + //ShowConnectingPopup(); // Show a connecting message + //SingletonBehaviour.Instance.ConnectionFailed += HandleConnectionFailed; + //SingletonBehaviour.Instance.ConnectionEstablished += HandleConnectionEstablished; + }; + } + + // Example of handling connection success + private void HandleConnectionEstablished() + { + // Connection established, handle the UI or game state accordingly + Debug.Log("Connection established!"); + // HideConnectingPopup(); // Hide the connecting message + } + + // Example of handling connection failure + private void HandleConnectionFailed() + { + // Connection failed, show an error message or handle the failure scenario + Debug.LogError("Connection failed!"); + // ShowConnectionFailedPopup(); + } - popup.labelTMPro.text = Locale.SERVER_BROWSER__PASSWORD; + private void RefreshAction() + { + // Implement refresh action logic here + Debug.Log("Refresh button clicked."); + // Add your code to refresh the multiplayer list or perform any other refresh-related action + } - popup.Closed += result => + + private static void ShowOkPopup(string text, Action onClick) { - if (result.closedBy == PopupClosedByAction.Abortion) + var popup = MainMenuThingsAndStuff.Instance.ShowOkPopup(); + if (popup == null) return; + + popup.labelTMPro.text = text; + popup.Closed += _ => onClick(); + } + + private void SetButtonsActive(params GameObject[] buttons) + { + foreach (var button in buttons) { - MainMenuThingsAndStuff.Instance.SwitchToDefaultMenu(); - return; + button.SetActive(true); } + } - SingletonBehaviour.Instance.StartClient(address, port, result.data); - }; - } - - private static void ShowOkPopup(string text, Action onClick) - { - Popup popup = MainMenuThingsAndStuff.Instance.ShowOkPopup(); - if (popup == null) - return; + private void HostAction() + { + // Implement host action logic here + Debug.Log("Host button clicked."); + // Add your code to handle hosting a game + } - popup.labelTMPro.text = text; - popup.Closed += _ => { onClick(); }; + private void JoinAction() + { + // Implement join action logic here + Debug.Log("Join button clicked."); + // Add your code to handle joining a game + } } } diff --git a/Multiplayer/Components/MainMenu/PopupTextInputFieldControllerNoValidation.cs b/Multiplayer/Components/MainMenu/PopupTextInputFieldControllerNoValidation.cs new file mode 100644 index 0000000..1cda123 --- /dev/null +++ b/Multiplayer/Components/MainMenu/PopupTextInputFieldControllerNoValidation.cs @@ -0,0 +1,93 @@ +using System; +using System.Reflection; +using DV.UIFramework; +using TMPro; +using UnityEngine; +using UnityEngine.Events; + +namespace Multiplayer.Components.MainMenu +{ + public class PopupTextInputFieldControllerNoValidation : MonoBehaviour, IPopupSubmitHandler + { + public Popup popup; + public TMP_InputField field; + public ButtonDV confirmButton; + + private void Awake() + { + // Find the components + popup = this.GetComponentInParent(); + field = popup.GetComponentInChildren(); + + foreach (ButtonDV btn in popup.GetComponentsInChildren()) + { + if (btn.name == "ButtonYes") + { + confirmButton = btn; + } + } + + // Set this instance as the new handler for the dialog + typeof(Popup).GetField("handler", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(popup, this); + } + + private void Start() + { + // Add listener for input field value changes + field.onValueChanged.AddListener(new UnityAction(OnInputValueChanged)); + OnInputValueChanged(field.text); + field.Select(); + field.ActivateInputField(); + } + + private void OnInputValueChanged(string value) + { + // Toggle confirm button interactability based on input validity + confirmButton.ToggleInteractable(IsInputValid(value)); + } + + public void HandleAction(PopupClosedByAction action) + { + switch (action) + { + case PopupClosedByAction.Positive: + if (IsInputValid(field.text)) + { + RequestPositive(); + return; + } + break; + case PopupClosedByAction.Negative: + RequestNegative(); + return; + case PopupClosedByAction.Abortion: + RequestAbortion(); + return; + default: + Debug.LogError(string.Format("Unhandled action {0}", action), this); + break; + } + } + + private bool IsInputValid(string value) + { + // Always return true to disable validation + return true; + } + + private void RequestPositive() + { + this.popup.RequestClose(PopupClosedByAction.Positive, this.field.text); + } + + private void RequestNegative() + { + this.popup.RequestClose(PopupClosedByAction.Negative, null); + } + + private void RequestAbortion() + { + this.popup.RequestClose(PopupClosedByAction.Abortion, null); + } + } +} diff --git a/Multiplayer/Components/SaveGame/StartGameData_ServerSave.cs b/Multiplayer/Components/SaveGame/StartGameData_ServerSave.cs index 7417012..8d0db2b 100644 --- a/Multiplayer/Components/SaveGame/StartGameData_ServerSave.cs +++ b/Multiplayer/Components/SaveGame/StartGameData_ServerSave.cs @@ -58,7 +58,7 @@ public override IEnumerator DoLoad(Transform playerContainer) LicenseManager.Instance.LoadData(saveGameData); if (saveGameData.GetString(SaveGameKeys.Game_mode) == "FreeRoam") - LicenseManager.Instance.GrabAllUnlockables(); + LicenseManager.Instance.GrabAllGameModeSpecificUnlockables(SaveGameKeys.Game_mode); else StartingItemsController.Instance.AddStartingItems(saveGameData, true); @@ -90,4 +90,9 @@ public override bool ShouldCreateSaveGameAfterLoad() { return false; } + + public override void MakeCurrent() + { + } } + diff --git a/Multiplayer/Locale.cs b/Multiplayer/Locale.cs index c70c689..274c5d6 100644 --- a/Multiplayer/Locale.cs +++ b/Multiplayer/Locale.cs @@ -2,129 +2,163 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.IO; -using System.Linq; using I2.Loc; using Multiplayer.Utils; -namespace Multiplayer; - -public static class Locale +namespace Multiplayer { - private const string DEFAULT_LOCALE_FILE = "locale.csv"; + public static class Locale + { + private const string DEFAULT_LOCALE_FILE = "locale.csv"; + private const string DEFAULT_LANGUAGE = "English"; + public const string MISSING_TRANSLATION = "[ MISSING TRANSLATION ]"; + public const string PREFIX = "multiplayer/"; - private const string DEFAULT_LANGUAGE = "English"; - public const string MISSING_TRANSLATION = "[ MISSING TRANSLATION ]"; - public const string PREFIX = "multiplayer/"; + private const string PREFIX_MAIN_MENU = $"{PREFIX}mm"; + private const string PREFIX_SERVER_BROWSER = $"{PREFIX}sb"; + private const string PREFIX_DISCONN_REASON = $"{PREFIX}dr"; + private const string PREFIX_CAREER_MANAGER = $"{PREFIX}carman"; + private const string PREFIX_PLAYER_LIST = $"{PREFIX}plist"; + private const string PREFIX_LOADING_INFO = $"{PREFIX}linfo"; - private const string PREFIX_MAIN_MENU = $"{PREFIX}mm"; - private const string PREFIX_SERVER_BROWSER = $"{PREFIX}sb"; - private const string PREFIX_DISCONN_REASON = $"{PREFIX}dr"; - private const string PREFIX_CAREER_MANAGER = $"{PREFIX}carman"; + #region Main Menu + public static string MAIN_MENU__JOIN_SERVER => Get(MAIN_MENU__JOIN_SERVER_KEY); + public const string MAIN_MENU__JOIN_SERVER_KEY = $"{PREFIX_MAIN_MENU}/join_server"; + #endregion - #region Main Menu + #region Server Browser + public static string SERVER_BROWSER__TITLE => Get(SERVER_BROWSER__TITLE_KEY); + public const string SERVER_BROWSER__TITLE_KEY = $"{PREFIX_SERVER_BROWSER}/title"; - public static string MAIN_MENU__JOIN_SERVER => Get(MAIN_MENU__JOIN_SERVER_KEY); - public const string MAIN_MENU__JOIN_SERVER_KEY = $"{PREFIX_MAIN_MENU}/join_server"; + public static string SERVER_BROWSER__MANUAL_CONNECT => Get(SERVER_BROWSER__MANUAL_CONNECT_KEY); + public const string SERVER_BROWSER__MANUAL_CONNECT_KEY = $"{PREFIX_SERVER_BROWSER}/manual_connect"; - #endregion + public static string SERVER_BROWSER__HOST => Get(SERVER_BROWSER__HOST_KEY); + public const string SERVER_BROWSER__HOST_KEY = $"{PREFIX_SERVER_BROWSER}/host"; + + public static string SERVER_BROWSER__REFRESH => Get(SERVER_BROWSER__REFRESH_KEY); + public const string SERVER_BROWSER__REFRESH_KEY = $"{PREFIX_SERVER_BROWSER}/refresh"; - #region Server Browser + public static string SERVER_BROWSER__JOIN => Get(SERVER_BROWSER__JOIN_KEY); + public const string SERVER_BROWSER__JOIN_KEY = $"{PREFIX_SERVER_BROWSER}/join_game"; - public static string SERVER_BROWSER__TITLE => Get(SERVER_BROWSER__TITLE_KEY); - public const string SERVER_BROWSER__TITLE_KEY = $"{PREFIX_SERVER_BROWSER}/title"; - public static string SERVER_BROWSER__MANUAL_CONNECT => Get(SERVER_BROWSER__MANUAL_CONNECT_KEY); - public const string SERVER_BROWSER__MANUAL_CONNECT_KEY = $"{PREFIX_SERVER_BROWSER}/manual_connect"; + public static string SERVER_BROWSER__IP => Get(SERVER_BROWSER__IP_KEY); + private const string SERVER_BROWSER__IP_KEY = $"{PREFIX_SERVER_BROWSER}/ip"; - public static string SERVER_BROWSER__IP => Get(SERVER_BROWSER__IP_KEY); - private const string SERVER_BROWSER__IP_KEY = $"{PREFIX_SERVER_BROWSER}/ip"; - public static string SERVER_BROWSER__IP_INVALID => Get(SERVER_BROWSER__IP_INVALID_KEY); - private const string SERVER_BROWSER__IP_INVALID_KEY = $"{PREFIX_SERVER_BROWSER}/ip_invalid"; - public static string SERVER_BROWSER__PORT => Get(SERVER_BROWSER__PORT_KEY); - private const string SERVER_BROWSER__PORT_KEY = $"{PREFIX_SERVER_BROWSER}/port"; - public static string SERVER_BROWSER__PORT_INVALID => Get(SERVER_BROWSER__PORT_INVALID_KEY); - private const string SERVER_BROWSER__PORT_INVALID_KEY = $"{PREFIX_SERVER_BROWSER}/port_invalid"; - public static string SERVER_BROWSER__PASSWORD => Get(SERVER_BROWSER__PASSWORD_KEY); - private const string SERVER_BROWSER__PASSWORD_KEY = $"{PREFIX_SERVER_BROWSER}/password"; + public static string SERVER_BROWSER__IP_INVALID => Get(SERVER_BROWSER__IP_INVALID_KEY); + private const string SERVER_BROWSER__IP_INVALID_KEY = $"{PREFIX_SERVER_BROWSER}/ip_invalid"; - #endregion + public static string SERVER_BROWSER__PORT => Get(SERVER_BROWSER__PORT_KEY); + private const string SERVER_BROWSER__PORT_KEY = $"{PREFIX_SERVER_BROWSER}/port"; - #region Disconnect Reason + public static string SERVER_BROWSER__PORT_INVALID => Get(SERVER_BROWSER__PORT_INVALID_KEY); + private const string SERVER_BROWSER__PORT_INVALID_KEY = $"{PREFIX_SERVER_BROWSER}/port_invalid"; - public static string DISCONN_REASON__INVALID_PASSWORD => Get(DISCONN_REASON__INVALID_PASSWORD_KEY); - public const string DISCONN_REASON__INVALID_PASSWORD_KEY = $"{PREFIX_DISCONN_REASON}/invalid_password"; - public static string DISCONN_REASON__GAME_VERSION => Get(DISCONN_REASON__GAME_VERSION_KEY); - public const string DISCONN_REASON__GAME_VERSION_KEY = $"{PREFIX_DISCONN_REASON}/game_version"; - public static string DISCONN_REASON__FULL_SERVER => Get(DISCONN_REASON__FULL_SERVER_KEY); - public const string DISCONN_REASON__FULL_SERVER_KEY = $"{PREFIX_DISCONN_REASON}/full_server"; - public static string DISCONN_REASON__MODS => Get(DISCONN_REASON__MODS_KEY); - public const string DISCONN_REASON__MODS_KEY = $"{PREFIX_DISCONN_REASON}/mods"; - public static string DISCONN_REASON__MOD_LIST => Get(DISCONN_REASON__MOD_LIST_KEY); - public const string DISCONN_REASON__MOD_LIST_KEY = $"{PREFIX_DISCONN_REASON}/mod_list"; + public static string SERVER_BROWSER__PASSWORD => Get(SERVER_BROWSER__PASSWORD_KEY); + private const string SERVER_BROWSER__PASSWORD_KEY = $"{PREFIX_SERVER_BROWSER}/password"; + #endregion - #endregion + #region Disconnect Reason + public static string DISCONN_REASON__INVALID_PASSWORD => Get(DISCONN_REASON__INVALID_PASSWORD_KEY); + public const string DISCONN_REASON__INVALID_PASSWORD_KEY = $"{PREFIX_DISCONN_REASON}/invalid_password"; - #region Career Manager + public static string DISCONN_REASON__GAME_VERSION => Get(DISCONN_REASON__GAME_VERSION_KEY); + public const string DISCONN_REASON__GAME_VERSION_KEY = $"{PREFIX_DISCONN_REASON}/game_version"; - public static string CAREER_MANAGER__FEES_HOST_ONLY => Get(CAREER_MANAGER__FEES_HOST_ONLY_KEY); - private const string CAREER_MANAGER__FEES_HOST_ONLY_KEY = $"{PREFIX_CAREER_MANAGER}/fees_host_only"; + public static string DISCONN_REASON__FULL_SERVER => Get(DISCONN_REASON__FULL_SERVER_KEY); + public const string DISCONN_REASON__FULL_SERVER_KEY = $"{PREFIX_DISCONN_REASON}/full_server"; - #endregion + public static string DISCONN_REASON__MODS => Get(DISCONN_REASON__MODS_KEY); + public const string DISCONN_REASON__MODS_KEY = $"{PREFIX_DISCONN_REASON}/mods"; - private static bool initializeAttempted; - private static ReadOnlyDictionary> csv; + public static string DISCONN_REASON__MOD_LIST => Get(DISCONN_REASON__MOD_LIST_KEY); + public const string DISCONN_REASON__MOD_LIST_KEY = $"{PREFIX_DISCONN_REASON}/mod_list"; - public static void Load(string localeDir) - { - initializeAttempted = true; - string path = Path.Combine(localeDir, DEFAULT_LOCALE_FILE); - if (!File.Exists(path)) - { - Multiplayer.LogError($"Failed to find locale file at '{path}'! Please make sure it's there."); - return; - } + public static string DISCONN_REASON__MODS_MISSING => Get(DISCONN_REASON__MODS_MISSING_KEY); + public const string DISCONN_REASON__MODS_MISSING_KEY = $"{PREFIX_DISCONN_REASON}/mods_missing"; - csv = Csv.Parse(File.ReadAllText(path)); - Multiplayer.LogDebug(() => $"Locale dump:{Csv.Dump(csv)}"); - } + public static string DISCONN_REASON__MODS_EXTRA => Get(DISCONN_REASON__MODS_EXTRA_KEY); + public const string DISCONN_REASON__MODS_EXTRA_KEY = $"{PREFIX_DISCONN_REASON}/mods_extra"; + #endregion - public static string Get(string key, string overrideLanguage = null) - { - if (!initializeAttempted) - throw new InvalidOperationException("Not initialized"); + #region Career Manager + public static string CAREER_MANAGER__FEES_HOST_ONLY => Get(CAREER_MANAGER__FEES_HOST_ONLY_KEY); + private const string CAREER_MANAGER__FEES_HOST_ONLY_KEY = $"{PREFIX_CAREER_MANAGER}/fees_host_only"; + #endregion - if (csv == null) - return MISSING_TRANSLATION; + #region Player List + public static string PLAYER_LIST__TITLE => Get(PLAYER_LIST__TITLE_KEY); + private const string PLAYER_LIST__TITLE_KEY = $"{PREFIX_PLAYER_LIST}/title"; + #endregion - string locale = overrideLanguage ?? LocalizationManager.CurrentLanguage; - if (!csv.ContainsKey(locale)) + #region Loading Info + public static string LOADING_INFO__WAIT_FOR_SERVER => Get(LOADING_INFO__WAIT_FOR_SERVER_KEY); + private const string LOADING_INFO__WAIT_FOR_SERVER_KEY = $"{PREFIX_LOADING_INFO}/wait_for_server"; + + public static string LOADING_INFO__SYNC_WORLD_STATE => Get(LOADING_INFO__SYNC_WORLD_STATE_KEY); + private const string LOADING_INFO__SYNC_WORLD_STATE_KEY = $"{PREFIX_LOADING_INFO}/sync_world_state"; + #endregion + + private static bool initializeAttempted; + private static ReadOnlyDictionary> csv; + + public static void Load(string localeDir) { - if (locale == DEFAULT_LANGUAGE) + initializeAttempted = true; + string path = Path.Combine(localeDir, DEFAULT_LOCALE_FILE); + if (!File.Exists(path)) { - Multiplayer.LogError($"Failed to find locale language {locale}! Something is broken, this shouldn't happen. Dumping CSV data:"); - Multiplayer.LogError($"\n{Csv.Dump(csv)}"); - return MISSING_TRANSLATION; + Multiplayer.LogError($"Failed to find locale file at '{path}'! Please make sure it's there."); + return; } - locale = DEFAULT_LANGUAGE; - Multiplayer.LogWarning($"Failed to find locale language {locale}"); + csv = Csv.Parse(File.ReadAllText(path)); + Multiplayer.LogDebug(() => $"Locale dump: {Csv.Dump(csv)}"); } - Dictionary localeDict = csv[locale]; - string actualKey = key.StartsWith(PREFIX) ? key.Substring(PREFIX.Length) : key; - if (localeDict.TryGetValue(actualKey, out string value)) - return value == string.Empty ? Get(actualKey, DEFAULT_LANGUAGE) : value; + public static string Get(string key, string overrideLanguage = null) + { + if (!initializeAttempted) + throw new InvalidOperationException("Not initialized"); - Multiplayer.LogDebug(() => $"Failed to find locale key '{actualKey}'!"); - return MISSING_TRANSLATION; - } + if (csv == null) + return MISSING_TRANSLATION; - public static string Get(string key, params object[] placeholders) - { - return string.Format(Get(key), placeholders); - } + string locale = overrideLanguage ?? LocalizationManager.CurrentLanguage; + if (!csv.ContainsKey(locale)) + { + if (locale == DEFAULT_LANGUAGE) + { + Multiplayer.LogError($"Failed to find locale language {locale}! Something is broken, this shouldn't happen. Dumping CSV data:"); + Multiplayer.LogError($"\n{Csv.Dump(csv)}"); + return MISSING_TRANSLATION; + } + + locale = DEFAULT_LANGUAGE; + Multiplayer.LogWarning($"Failed to find locale language {locale}"); + } - public static string Get(string key, params string[] placeholders) - { - return Get(key, placeholders.Cast()); + Dictionary localeDict = csv[locale]; + string actualKey = key.StartsWith(PREFIX) ? key.Substring(PREFIX.Length) : key; + if (localeDict.TryGetValue(actualKey, out string value)) + { + if (string.IsNullOrEmpty(value)) + return overrideLanguage == null && locale != DEFAULT_LANGUAGE ? Get(actualKey, DEFAULT_LANGUAGE) : MISSING_TRANSLATION; + return value; + } + + Multiplayer.LogDebug(() => $"Failed to find locale key '{actualKey}'!"); + return MISSING_TRANSLATION; + } + + public static string Get(string key, params object[] placeholders) + { + return string.Format(Get(key), placeholders); + } + + public static string Get(string key, params string[] placeholders) + { + return Get(key, (object[])placeholders); + } } } diff --git a/Multiplayer/Multiplayer.cs b/Multiplayer/Multiplayer.cs index 87ca8b0..04af71f 100644 --- a/Multiplayer/Multiplayer.cs +++ b/Multiplayer/Multiplayer.cs @@ -1,4 +1,3 @@ -using System; using System.IO; using HarmonyLib; using JetBrains.Annotations; diff --git a/Multiplayer/Multiplayer.csproj b/Multiplayer/Multiplayer.csproj index 8f3bdb8..e9b86a6 100644 --- a/Multiplayer/Multiplayer.csproj +++ b/Multiplayer/Multiplayer.csproj @@ -8,6 +8,7 @@ + @@ -17,6 +18,7 @@ + @@ -78,6 +80,7 @@ + diff --git a/Multiplayer/Patches/CommsRadio/CommsRadioCarDeleterPatch.cs b/Multiplayer/Patches/CommsRadio/CommsRadioCarDeleterPatch.cs index c1dc805..0cd194a 100644 --- a/Multiplayer/Patches/CommsRadio/CommsRadioCarDeleterPatch.cs +++ b/Multiplayer/Patches/CommsRadio/CommsRadioCarDeleterPatch.cs @@ -15,7 +15,7 @@ public static class CommsRadioCarDeleterPatch [HarmonyPatch(nameof(CommsRadioCarDeleter.OnUse))] private static bool OnUse_Prefix(CommsRadioCarDeleter __instance) { - if (__instance.state != CommsRadioCarDeleter.State.ConfirmDelete) + if (__instance.CurrentState != CommsRadioCarDeleter.State.ConfirmDelete) return true; if (NetworkLifecycle.Instance.IsHost() && NetworkLifecycle.Instance.Server.PlayerCount == 1) return true; @@ -50,7 +50,7 @@ private static IEnumerator PlaySoundsLater(CommsRadioCarDeleter __instance, Vect [HarmonyPatch(nameof(CommsRadioCarDeleter.OnUpdate))] private static bool OnUpdate_Prefix(CommsRadioCarDeleter __instance) { - if (__instance.state != CommsRadioCarDeleter.State.ScanCarToDelete) + if (__instance.CurrentState != CommsRadioCarDeleter.State.ScanCarToDelete) return true; if (NetworkLifecycle.Instance.IsHost() && NetworkLifecycle.Instance.Server.PlayerCount == 1) return true; diff --git a/Multiplayer/Patches/MainMenu/LocalizationManagerPatch.cs b/Multiplayer/Patches/MainMenu/LocalizationManagerPatch.cs index 0f799cb..317b053 100644 --- a/Multiplayer/Patches/MainMenu/LocalizationManagerPatch.cs +++ b/Multiplayer/Patches/MainMenu/LocalizationManagerPatch.cs @@ -1,20 +1,27 @@ using HarmonyLib; using I2.Loc; -namespace Multiplayer.Patches.MainMenu; - -[HarmonyPatch(typeof(LocalizationManager))] -public static class LocalizationManagerPatch +namespace Multiplayer.Patches.MainMenu { - [HarmonyPrefix] - [HarmonyPatch(nameof(LocalizationManager.TryGetTranslation))] - private static bool TryGetTranslation_Prefix(ref bool __result, string Term, out string Translation) + [HarmonyPatch(typeof(LocalizationManager))] + public static class LocalizationManagerPatch { - Translation = string.Empty; - if (!Term.StartsWith(Locale.PREFIX)) - return true; - Translation = Locale.Get(Term); - __result = Translation == Locale.MISSING_TRANSLATION; - return false; + [HarmonyPrefix] + [HarmonyPatch(nameof(LocalizationManager.TryGetTranslation))] + private static bool TryGetTranslation_Prefix(ref bool __result, string Term, out string Translation) + { + Translation = string.Empty; + + // Check if the term starts with the specified locale prefix + if (!Term.StartsWith(Locale.PREFIX)) + return true; + + // Attempt to get the translation for the term + Translation = Locale.Get(Term); + + // If the translation is missing, set the result to true and skip the original method + __result = Translation == Locale.MISSING_TRANSLATION; + return false; + } } } diff --git a/Multiplayer/Patches/MainMenu/MainMenuControllerPatch.cs b/Multiplayer/Patches/MainMenu/MainMenuControllerPatch.cs index be04935..1aa18dc 100644 --- a/Multiplayer/Patches/MainMenu/MainMenuControllerPatch.cs +++ b/Multiplayer/Patches/MainMenu/MainMenuControllerPatch.cs @@ -1,50 +1,68 @@ -using DV.Localization; +using DV.Localization; using DV.UI; using HarmonyLib; using Multiplayer.Utils; using UnityEngine; using UnityEngine.UI; -namespace Multiplayer.Patches.MainMenu; - -[HarmonyPatch(typeof(MainMenuController), "Awake")] -public static class MainMenuController_Awake_Patch +namespace Multiplayer.Patches.MainMenu { - public static GameObject MultiplayerButton; - - private static void Prefix(MainMenuController __instance) + [HarmonyPatch(typeof(MainMenuController), "Awake")] + public static class MainMenuController_Awake_Patch { - GameObject button = __instance.FindChildByName("ButtonSelectable Sessions"); - if (button == null) + public static GameObject multiplayerButton; + + private static void Prefix(MainMenuController __instance) { - Multiplayer.LogError("Failed to find Sessions button!"); - return; - } + // Find the Sessions button to base the Multiplayer button on + GameObject sessionsButton = __instance.FindChildByName("ButtonSelectable Sessions"); + if (sessionsButton == null) + { + Multiplayer.LogError("Failed to find Sessions button!"); + return; + } + + // Deactivate the sessions button temporarily to duplicate it + sessionsButton.SetActive(false); + multiplayerButton = Object.Instantiate(sessionsButton, sessionsButton.transform.parent); + sessionsButton.SetActive(true); - button.SetActive(false); - MultiplayerButton = Object.Instantiate(button, button.transform.parent); - button.SetActive(true); + // Configure the new Multiplayer button + multiplayerButton.transform.SetSiblingIndex(sessionsButton.transform.GetSiblingIndex() + 1); + multiplayerButton.name = "ButtonSelectable Multiplayer"; - MultiplayerButton.transform.SetSiblingIndex(button.transform.GetSiblingIndex() + 1); - MultiplayerButton.name = "ButtonSelectable Multiplayer"; + // Set the localization key for the new button + Localize localize = multiplayerButton.GetComponentInChildren(); + localize.key = Locale.MAIN_MENU__JOIN_SERVER_KEY; - Localize localize = MultiplayerButton.GetComponentInChildren(); - localize.key = Locale.MAIN_MENU__JOIN_SERVER_KEY; + // Remove existing localization components to reset them + Object.Destroy(multiplayerButton.GetComponentInChildren()); + ResetTooltip(multiplayerButton); - // Reset existing localization components that were added when the Sessions button was initialized. - Object.Destroy(MultiplayerButton.GetComponentInChildren()); - UIElementTooltip tooltip = MultiplayerButton.GetComponent(); - tooltip.disabledKey = null; - tooltip.enabledKey = null; + // Set the icon for the new Multiplayer button + SetButtonIcon(multiplayerButton); - GameObject icon = MultiplayerButton.FindChildByName("icon"); - if (icon == null) + multiplayerButton.SetActive(true); + } + + private static void ResetTooltip(GameObject button) { - Multiplayer.LogError("Failed to find icon on Sessions button, destroying the Multiplayer button!"); - Object.Destroy(MultiplayerButton); - return; + UIElementTooltip tooltip = button.GetComponent(); + tooltip.disabledKey = null; + tooltip.enabledKey = null; } - icon.GetComponent().sprite = Multiplayer.AssetIndex.multiplayerIcon; + private static void SetButtonIcon(GameObject button) + { + GameObject icon = button.FindChildByName("icon"); + if (icon == null) + { + Multiplayer.LogError("Failed to find icon on Sessions button, destroying the Multiplayer button!"); + Object.Destroy(multiplayerButton); + return; + } + + icon.GetComponent().sprite = Multiplayer.AssetIndex.multiplayerIcon; + } } } diff --git a/Multiplayer/Patches/MainMenu/RightPaneControllerPatch.cs b/Multiplayer/Patches/MainMenu/RightPaneControllerPatch.cs index da66224..7f27547 100644 --- a/Multiplayer/Patches/MainMenu/RightPaneControllerPatch.cs +++ b/Multiplayer/Patches/MainMenu/RightPaneControllerPatch.cs @@ -1,86 +1,129 @@ -using DV.Localization; +using System.Linq; +using System; +using DV.Common; +using DV.Localization; +using DV.Scenarios.Common; using DV.UI; using DV.UIFramework; using HarmonyLib; using Multiplayer.Components.MainMenu; using Multiplayer.Utils; +using TMPro; using UnityEngine; +using UnityEngine.UI; +using LiteNetLib; -namespace Multiplayer.Patches.MainMenu; - -[HarmonyPatch(typeof(RightPaneController), "OnEnable")] -public static class RightPaneController_OnEnable_Patch +namespace Multiplayer.Patches.MainMenu { - private static void Prefix(RightPaneController __instance) + [HarmonyPatch(typeof(RightPaneController), "OnEnable")] + public static class RightPaneController_OnEnable_Patch { - if (__instance.HasChildWithName("PaneRight Multiplayer")) - return; - GameObject basePane = __instance.FindChildByName("PaneRight Settings"); + private static void Prefix(RightPaneController __instance) + { + // Check if the multiplayer pane already exists + if (__instance.HasChildWithName("PaneRight Multiplayer")) + return; - basePane.SetActive(false); - GameObject multiplayerPane = Object.Instantiate(basePane, basePane.transform.parent); - basePane.SetActive(true); + // Find the base pane for Load/Save + GameObject basePane = __instance.FindChildByName("PaneRight Load/Save"); + if (basePane == null) + { + Multiplayer.LogError("Failed to find Launcher pane!"); + return; + } - multiplayerPane.name = "PaneRight Multiplayer"; - __instance.menuController.controlledMenus.Add(multiplayerPane.GetComponent()); - MainMenuController_Awake_Patch.MultiplayerButton.GetComponent().requestedMenuIndex = __instance.menuController.controlledMenus.Count - 1; + // Create a new multiplayer pane based on the base pane + basePane.SetActive(false); + GameObject multiplayerPane = GameObject.Instantiate(basePane, basePane.transform.parent); + basePane.SetActive(true); - Object.Destroy(multiplayerPane.GetComponent()); - Object.Destroy(multiplayerPane.GetComponent()); - Object.Destroy(multiplayerPane.FindChildByName("Left Buttons")); - Object.Destroy(multiplayerPane.FindChildByName("Text Content")); + multiplayerPane.name = "PaneRight Multiplayer"; - GameObject rightSubMenus = multiplayerPane.FindChildByName("Right Submenus"); - GameObject languagePane = rightSubMenus.FindChildByName("PaneRight Language"); + multiplayerPane.AddComponent(); - RectTransform langRect = languagePane.GetComponent(); - RectTransform subMenusRect = rightSubMenus.GetComponent(); + __instance.menuController.controlledMenus.Add(multiplayerPane.GetComponent()); + MainMenuController_Awake_Patch.multiplayerButton.GetComponent().requestedMenuIndex = __instance.menuController.controlledMenus.Count - 1; - Vector2 sizeDelta = new(1290, 600); - subMenusRect.sizeDelta = sizeDelta; - langRect.sizeDelta = sizeDelta; + // Clean up unnecessary components and child objects + GameObject.Destroy(multiplayerPane.GetComponent()); + GameObject.Destroy(multiplayerPane.GetComponent()); + GameObject.Destroy(multiplayerPane.GetComponent()); + GameObject.Destroy(multiplayerPane.FindChildByName("ButtonIcon OpenFolder")); + GameObject.Destroy(multiplayerPane.FindChildByName("ButtonIcon Rename")); + GameObject.Destroy(multiplayerPane.FindChildByName("Text Content")); + + + // Update UI elements + GameObject titleObj = multiplayerPane.FindChildByName("Title"); + titleObj.GetComponentInChildren().key = Locale.SERVER_BROWSER__TITLE_KEY; + GameObject.Destroy(titleObj.GetComponentInChildren()); + + GameObject content = multiplayerPane.FindChildByName("text main"); + content.GetComponentInChildren().text = "Server browser not yet implemented."; + + GameObject serverWindow = multiplayerPane.FindChildByName("Save Description"); + serverWindow.GetComponentInChildren().text = "Server information not yet implemented."; + + UpdateButton(multiplayerPane, "ButtonTextIcon Overwrite", "ButtonTextIcon Manual", Locale.SERVER_BROWSER__MANUAL_CONNECT_KEY, null, Multiplayer.AssetIndex.multiplayerIcon); + UpdateButton(multiplayerPane, "ButtonTextIcon Load", "ButtonTextIcon Host", Locale.SERVER_BROWSER__HOST_KEY, null, Multiplayer.AssetIndex.multiplayerIcon); + UpdateButton(multiplayerPane, "ButtonTextIcon Save", "ButtonTextIcon Join", Locale.SERVER_BROWSER__JOIN_KEY, null, null); + UpdateButton(multiplayerPane, "ButtonIcon Delete", "ButtonTextIcon Refresh", Locale.SERVER_BROWSER__REFRESH, null, null); + + multiplayerPane.AddComponent(); + + MainMenuThingsAndStuff.Create(manager => + { + PopupManager popupManager = null; + __instance.FindPopupManager(ref popupManager); + manager.popupManager = popupManager; + manager.renamePopupPrefab = __instance.continueLoadNewController.career.renamePopupPrefab; + manager.okPopupPrefab = __instance.continueLoadNewController.career.okPopupPrefab; + manager.uiMenuController = __instance.menuController; + }); + + MainMenuController_Awake_Patch.multiplayerButton.SetActive(true); - foreach (GameObject go in rightSubMenus.GetChildren()) - { - if (go.name == "PaneRight Language") - continue; - Object.Destroy(go); } - GameObject viewport = languagePane.FindChildByName("Viewport"); - foreach (GameObject go in viewport.GetChildren()) - Object.Destroy(go); + private static void UpdateButton(GameObject pane, string oldButtonName, string newButtonName, string localeKey, string toolTipKey, Sprite icon) + { + GameObject button = pane.FindChildByName(oldButtonName); + button.name = newButtonName; - Object.Destroy(languagePane.FindChildByName("Title")); - Object.Destroy(languagePane.FindChildByName("Help button")); - Object.Destroy(languagePane.FindChildByName("Text Content")); - Object.Destroy(languagePane.FindChildByName("ButtonTextIcon")); - Object.Destroy(languagePane.GetComponent()); - Object.Destroy(languagePane.GetComponent()); - Object.Destroy(multiplayerPane.FindChildByName("Selector Preset")); - Object.Destroy(multiplayerPane.FindChildByName("ButtonTextIcon Discard")); + if (button.GetComponentInChildren() != null) + { + button.GetComponentInChildren().key = localeKey; + GameObject.Destroy(button.GetComponentInChildren()); + ResetTooltip(button); + } - GameObject manualConnect = multiplayerPane.FindChildByName("ButtonTextIcon Apply"); - manualConnect.GetComponentInChildren().key = Locale.SERVER_BROWSER__MANUAL_CONNECT_KEY; - Object.Destroy(manualConnect.GetComponentInChildren()); + if (icon != null) + { + SetButtonIcon(button, icon); + } - GameObject titleObj = multiplayerPane.FindChildByName("Title"); - titleObj.GetComponentInChildren().key = Locale.SERVER_BROWSER__TITLE_KEY; - Object.Destroy(titleObj.GetComponentInChildren()); + button.GetComponentInChildren().ToggleInteractable(true); - multiplayerPane.AddComponent(); - MainMenuThingsAndStuff.Create(manager => + } + + private static void SetButtonIcon(GameObject button, Sprite icon) { - PopupManager popupManager = null; - __instance.FindPopupManager(ref popupManager); - manager.popupManager = popupManager; - manager.renamePopupPrefab = __instance.continueLoadNewController.career.renamePopupPrefab; - manager.okPopupPrefab = __instance.continueLoadNewController.career.okPopupPrefab; - manager.uiMenuController = __instance.menuController; - }); - - multiplayerPane.SetActive(true); - MainMenuController_Awake_Patch.MultiplayerButton.SetActive(true); + GameObject goIcon = button.FindChildByName("[icon]"); + if (goIcon == null) + { + Multiplayer.LogError("Failed to find icon!"); + return; + } + + goIcon.GetComponent().sprite = icon; + } + + private static void ResetTooltip(GameObject button) + { + UIElementTooltip tooltip = button.GetComponent(); + tooltip.disabledKey = null; + tooltip.enabledKey = null; + } } } diff --git a/Multiplayer/Settings.cs b/Multiplayer/Settings.cs index c01fe67..4e2087b 100644 --- a/Multiplayer/Settings.cs +++ b/Multiplayer/Settings.cs @@ -1,4 +1,4 @@ -using System; +using System; using Humanizer; using UnityEngine; using UnityModManagerNet; @@ -28,6 +28,16 @@ public class Settings : UnityModManager.ModSettings, IDrawable [Draw("Port", Tooltip = "The port that your server will listen on. You generally don't need to change this.")] public int Port = 7777; + [Space(10)] + [Header("Last Server Connected to by IP")] + [Draw("Last Remote IP", Tooltip = "The IP for the last server connected to by IP.")] + public string LastRemoteIP = ""; + [Draw("Last Remote Port", Tooltip = "The port for the last server connected to by IP.")] + public int LastRemotePort = 7777; + [Draw("Last Remote Password", Tooltip = "The password for the last server connected to by IP.")] + public string LastRemotePassword = ""; + + [Space(10)] [Header("Preferences")] [Draw("Show Name Tags", Tooltip = "Whether to show player names above their heads.")] diff --git a/Multiplayer/Utils/Csvnew.cs b/Multiplayer/Utils/Csvnew.cs new file mode 100644 index 0000000..ef66263 --- /dev/null +++ b/Multiplayer/Utils/Csvnew.cs @@ -0,0 +1,94 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Text; + +namespace Multiplayer.Utils +{ + public static class Csv + { + public static ReadOnlyDictionary> Parse(string data) + { + var columns = new Dictionary>(); + var lines = data.Split('\n'); + + var keys = ParseLine(lines[0]); + foreach (var key in keys) + columns[key] = new Dictionary(); + + for (int i = 0; i < lines.Length; i++) + { + var values = ParseLine(lines[i]); + if (values.Count == 0 || string.IsNullOrWhiteSpace(values[0])) + continue; + + string key = values[0]; + for (int j = 0; j < values.Count; j++) + columns[keys[j]][key] = values[j]; + } + + return new ReadOnlyDictionary>(columns); + } + + private static List ParseLine(string line) + { + var values = new List(); + var builder = new StringBuilder(); + + bool inQuotes = false; + foreach (char c in line) + { + if (c == ',' && !inQuotes) + { + values.Add(builder.ToString()); + builder.Clear(); + } + else if (c == '"') + { + inQuotes = !inQuotes; + } + else + { + builder.Append(c); + } + } + + values.Add(builder.ToString()); + return values; + } + + public static string Dump(ReadOnlyDictionary> data) + { + var result = new StringBuilder(); + + foreach (var column in data) + result.Append($"{column.Key},"); + + result.Length--; + result.Append('\n'); + + int rowCount = data.Values.FirstOrDefault()?.Count ?? 0; + + for (int i = 0; i < rowCount; i++) + { + foreach (var column in data) + { + if (column.Value.Count > i) + { + string value = column.Value.ElementAt(i).Value.Replace("\n", "\\n"); + result.Append(value.Contains(',') ? $"\"{value}\"," : $"{value},"); + } + else + { + result.Append(','); + } + } + + result.Length--; + result.Append('\n'); + } + + return result.ToString(); + } + } +} diff --git a/compare b/compare new file mode 100644 index 0000000..e69de29 diff --git a/locale.csv b/locale.csv index 4a250c0..de7e86a 100644 --- a/locale.csv +++ b/locale.csv @@ -2,27 +2,47 @@ Key,Description,English,Bulgarian,Chinese (Simplified),Chinese (Traditional),Cze ,,,,,,,,,,,,,,,,,,,,,,,,,,, ,"Do not translate ‘{x}’ with x being a number, or ‘\n’.",,,,,,,,,,,,,,,,,,,,,,,,,, ,"If a translation has a comma, the entire line MUST be wrapped in double quotes! Most editors (Excel, LibreCalc) will do this for you.",,,,,,,,,,,,,,,,,,,,,,,,,, +,"When saving the file, ensure to save it using UTF-8 encoding!",,,,,,,,,,,,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,, ,Main Menu,,,,,,,,,,,,,,,,,,,,,,,,,, -mm/join_server,The 'Join Server' button in the main menu.,Join Server,,,,,,,,,,,,,,,,,,,,,,,,, -mm/join_server__tooltip,The tooltip shown when hovering over the 'Join Server' button.,Join a multiplayer session.,,,,,,,,,,,,,,,,,,,,,,,,, +mm/join_server,The 'Join Server' button in the main menu.,Join Server,,,,,,,,Rejoindre le serveur,Spiel beitreten,,,Entra in un Server,,,,,,,,,,Unirse a un servidor,,, +mm/join_server__tooltip,The tooltip shown when hovering over the 'Join Server' button.,Join a multiplayer session.,,,,,,,,Rejoindre une session multijoueur,Trete einer Mehrspielersitzung bei.,,,Entra in una sessione multiplayer.,,,,,,,,,,Únete a una sesión multijugador.,,, mm/join_server__tooltip_disabled,Unused,,,,,,,,,,,,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,, ,Server Browser,,,,,,,,,,,,,,,,,,,,,,,,,, -sb/title,The title of the Server Browser tab,Server Browser,,,,,,,,,,,,,,,,,,,,,,,,, -sb/manual_connect,The Manual Connect button,Connect Manually,,,,,,,,,,,,,,,,,,,,,,,,, -sb/ip,IP popup,Enter IP Address,,,,,,,,,,,,,,,,,,,,,,,,, -sb/ip_invalid,Invalid IP popup.,Invalid IP Address!,,,,,,,,,,,,,,,,,,,,,,,,, -sb/port,Port popup.,Enter Port (7777 by default),,,,,,,,,,,,,,,,,,,,,,,,, -sb/port_invalid,Invalid port popup.,Invalid Port!,,,,,,,,,,,,,,,,,,,,,,,,, -sb/password,Password popup.,Enter Password,,,,,,,,,,,,,,,,,,,,,,,,, +sb/title,The title of the Server Browser tab,Server Browser,,,,,,,,Navigateur de serveurs,Server Liste,,,Ricerca Server,,,,,,,,,,Buscar servidores,,, +sb/manual_connect,Connect to IP,Connect to IP,,,,,,,,,,,,,,,,,,,,,,,, +sb/manual_connect__tooltip,The tooltip shown when hovering over the 'manualconnect' button.,Direct connection to a multiplayer session.,,,,,,,,,,,,,,,,,,,,,,,, +sb/manual_connect__tooltip_disabled,Unused,,,,,,,,,,,,,,,,,,,,,,,,,, +sb/host,Host Game,Host Game,,,,,,,,,,,,,,,,,,,,,,,, +sb/host__tooltip,The tooltip shown when hovering over the 'Host Server' button.,Host a multiplayer session.,,,,,,,,,,,,,,,,,,,,,,,, +sb/host__tooltip_disabled,Unused,,,,,,,,,,,,,,,,,,,,,,,,,, +sb/join_game,Join Game,Join Game,,,,,,,,,,,,,,,,,,,,,,,, +sb/join_game__tooltip,The tooltip shown when hovering over the 'Join Server' button.,Join a multiplayer session.,,,,,,,,,,,,,,,,,,,,,,,, +sb/join_game__tooltip_disabled,Unused,,,,,,,,,,,,,,,,,,,,,,,,,, +sb/Refresh,refresh,Refresh,,,,,,,,,,,,,,,,,,,,,,,, +sb/Refresh__tooltip,The tooltip shown when hovering over the 'Refresh Server' button.,Refresh Server list.,,,,,,,,,,,,,,,,,,,,,,,, +sb/Refresh__tooltip_disabled,Unused,,,,,,,,,,,,,,,,,,,,,,,,,, +sb/ip,IP popup,Enter IP Address,,,,,,,,Entrer l’adresse IP,IP Adresse eingeben,,,Inserire Indirizzo IP,,,,,,,,,,Ingrese la dirección IP,,, +sb/ip_invalid,Invalid IP popup.,Invalid IP Address!,,,,,,,,Adresse IP invalide,Ungültige IP Adresse!,,,Indirizzo IP Invalido!,,,,,,,,,,¡Dirección IP inválida!,,, +sb/port,Port popup.,Enter Port (7777 by default),,,,,,,,Entrer le port (7777 par défaut),Port eingeben (Standard: 7777),,,Inserire Porta (7777 di default),,,,,,,,,,Introduzca el número de puerto(7777 por defecto),,, +sb/port_invalid,Invalid port popup.,Invalid Port!,,,,,,,,Port invalide !,Ungültiger Port!,,,Porta Invalida!,,,,,,,,,,¡Número de Puerto no válido!,,, +sb/password,Password popup.,Enter Password,,,,,,,,Entrer le mot de passe,Passwort eingeben,,,Inserire Password,,,,,,,,,,Introducir la contraseña,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,, ,Disconnect Reason,,,,,,,,,,,,,,,,,,,,,,,,,, -dr/invalid_password,Invalid password popup.,Invalid Password!,,,,,,,,,,,,,,,,,,,,,,,,, -dr/game_version,Different game versions.,"Game version mismatch! Server version: {0}, your version: {1}.",,,,,,,,,,,,,,,,,,,,,,,,, -dr/full_server,The server is already full.,The server is full!,,,,,,,,,,,,,,,,,,,,,,,,, -dr/mods,"The client is missing, or has extra mods.",Mod mismatch!,,,,,,,,,,,,,,,,,,,,,,,,, -dr/mod_list,"The list of mods the client is missing, or has extra.",\n\nMissing Mods:\n{0}\nExtra Mods:\n{1},,,,,,,,,,,,,,,,,,,,,,,,, +dr/invalid_password,Invalid password popup.,Invalid Password!,,,,,,,,Mot de passe incorrect !,Ungültiges Passwort!,,,Password non valida!,,,,,,,,,,¡Contraseña invalida!,,, +dr/game_version,Different game versions.,"Game version mismatch! Server version: {0}, your version: {1}.",,,,,,,,"Version du jeu incompatible ! Version du serveur : {0}, version locale : {1}","Spielversion stimmt nicht überein! Server Version: {0}, Lokale Version: {1}.",,,"Versioni del gioco non combacianti! Versione del Server: {0}, La tua versione: {1}.",,,,,,,,,,"¡La versión del juego no coincide! Versión del servidor: {0}, tu versión: {1}.",,, +dr/full_server,The server is already full.,The server is full!,,,,,,,,Le serveur est complet !,Der Server ist voll!,,,Il Server è pieno!,,,,,,,,,,¡El servidor está lleno!,,, +dr/mods,"The client is missing, or has extra mods.",Mod mismatch!,,,,,,,,Mod incompatible !,Mods stimmen nicht überein!,,,Mod non combacianti!,,,,,,,,,,"Falta el cliente, o tiene modificaciones adicionales.",,, +dr/mods_missing,The list of missing mods.,Missing Mods:\n- {0},,,,,,,,Mods manquants:\n-{0},Fehlende Mods:\n- {0},,,Mod Mancanti:\n- {0},,,,,,,,,,Mods faltantes:\n- {0},,, +dr/mods_extra,The list of extra mods.,Extra Mods:\n- {0},,,,,,,,Mods extras:\n-{0},Zusätzliche Mods:\n- {0},,,Mod Extra:\n- {0},,,,,,,,,,Modificaciones adicionales:\n- {0},,, ,,,,,,,,,,,,,,,,,,,,,,,,,,, ,Career Manager,,,,,,,,,,,,,,,,,,,,,,,,,, -carman/fees_host_only,Text shown when a client tries to manage fees.,Only the host can manage fees!,,,,,,,,,,,,,,,,,,,,,,,,, +carman/fees_host_only,Text shown when a client tries to manage fees.,Only the host can manage fees!,,,,,,,,Seul l'hôte peut gérer les frais !,Nur der Host kann Gebühren verwalten!,,,Solo l’Host può gestire gli addebiti!,,,,,,,,,,¡Solo el anfitrión puede administrar las tarifas!,,, +,,,,,,,,,,,,,,,,,,,,,,,,,,, +,Player List,,,,,,,,,,,,,,,,,,,,,,,,,, +plist/title,The title of the player list.,Online Players,,,,,,,,Joueurs en ligne,Verbundene Spieler,,,Giocatori Online,,,,,,,,,,Jugadores en línea,,, +,,,,,,,,,,,,,,,,,,,,,,,,,,, +,Loading Info,,,,,,,,,,,,,,,,,,,,,,,,,, +linfo/wait_for_server,Text shown in the loading screen.,Waiting for server to load,,,,,,,,En attente du chargement du serveur,Warte auf das Laden des Servers,,,In attesa del caricamento del Server,,,,,,,,,,Esperando a que cargue el servidor...,,, +linfo/sync_world_state,Text shown in the loading screen.,Syncing world state,,,,,,,,Synchronisation des données du monde,Synchronisiere Daten,,,Sincronizzazione dello stato del mondo,,,,,,,,,,Sincronizando estado global,,, \ No newline at end of file From 44471ca0e8ac210162b8313ae39f1fad41409482 Mon Sep 17 00:00:00 2001 From: N95JPL <37276225+N95JPL@users.noreply.github.com> Date: Sun, 26 May 2024 21:03:46 +0100 Subject: [PATCH 04/34] Fix CSV.cs Now ignores blank/whitespace keys --- Multiplayer/Utils/Csv.cs | 198 ++++++++++++++++++++++----------------- 1 file changed, 111 insertions(+), 87 deletions(-) diff --git a/Multiplayer/Utils/Csv.cs b/Multiplayer/Utils/Csv.cs index 560fb24..ab9d8a0 100644 --- a/Multiplayer/Utils/Csv.cs +++ b/Multiplayer/Utils/Csv.cs @@ -1,3 +1,4 @@ +using System; using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; @@ -5,124 +6,147 @@ using System.Linq; using System.Text; -namespace Multiplayer.Utils; - -public static class Csv +namespace Multiplayer.Utils { - /// - /// Parses a CSV string into a dictionary of columns, each of which is a dictionary of rows, keyed by the first column. - /// - public static ReadOnlyDictionary> Parse(string data) + public static class Csv { - string[] lines = data.Split('\n'); + /// + /// Parses a CSV string into a dictionary of columns, each of which is a dictionary of rows, keyed by the first column. + /// + public static ReadOnlyDictionary> Parse(string data) + { + // Split the input data into lines + string[] separators = new string[] { "\r\n" }; + string[] lines = data.Split(separators, StringSplitOptions.None); - // Dictionary> - OrderedDictionary columns = new(lines.Length - 1); + // Use an OrderedDictionary to preserve the insertion order of keys + var columns = new OrderedDictionary(); - List keys = ParseLine(lines[0]); - foreach (string key in keys) - columns.Add(key, new Dictionary()); + // Parse the header line to get the column keys + List keys = ParseLine(lines[0]); + foreach (string key in keys) + { + if (!string.IsNullOrWhiteSpace(key)) + columns.Add(key, new Dictionary()); + } - for (int i = 1; i < lines.Length; i++) - { - string line = lines[i]; - List values = ParseLine(line); - if (values.Count == 0 || string.IsNullOrWhiteSpace(values[0])) - continue; - string key = values[0]; - for (int j = 0; j < values.Count; j++) - ((Dictionary)columns[j]).Add(key, values[j]); - } + // Iterate through the remaining lines (rows) + for (int i = 1; i < lines.Length; i++) + { + string line = lines[i]; + List values = ParseLine(line); + if (values.Count == 0 || string.IsNullOrWhiteSpace(values[0])) + continue; - return new ReadOnlyDictionary>(columns.Cast() - .ToDictionary(entry => (string)entry.Key, entry => (Dictionary)entry.Value)); - } + string rowKey = values[0]; - private static List ParseLine(string line) - { - bool inQuotes = false; - bool wasBackslash = false; - List values = new(); - StringBuilder builder = new(); + // Add the row values to the appropriate column dictionaries + for (int j = 0; j < values.Count && j < keys.Count; j++) + { + string columnKey = keys[j]; + if (!string.IsNullOrWhiteSpace(columnKey)) + { + var columnDict = (Dictionary)columns[columnKey]; + columnDict[rowKey] = values[j]; + } + } + } - void FinishLine() - { - values.Add(builder.ToString()); - builder.Clear(); + // Convert the OrderedDictionary to a ReadOnlyDictionary + return new ReadOnlyDictionary>( + columns.Cast() + .ToDictionary(entry => (string)entry.Key, entry => (Dictionary)entry.Value) + ); } - foreach (char c in line) + private static List ParseLine(string line) { - if (c == '\n' || (!inQuotes && c == ',')) - { - FinishLine(); - continue; - } + bool inQuotes = false; + bool wasBackslash = false; + List values = new(); + StringBuilder builder = new(); - switch (c) + void FinishValue() { - case '\r': - Multiplayer.LogWarning("Encountered carriage return in CSV! Please use Unix-style line endings (LF)."); - continue; - case '"': - inQuotes = !inQuotes; - continue; - case '\\': - wasBackslash = true; - continue; + values.Add(builder.ToString()); + builder.Clear(); } - if (wasBackslash) + foreach (char c in line) { - wasBackslash = false; - if (c == 'n') + if (c == ',' && !inQuotes) { - builder.Append('\n'); + FinishValue(); continue; } - // Not a special character, so just append the backslash - builder.Append('\\'); - } + switch (c) + { + case '\r': + Multiplayer.LogWarning("Encountered carriage return in CSV! Please use Unix-style line endings (LF)."); + continue; + case '"': + inQuotes = !inQuotes; + continue; + case '\\': + wasBackslash = true; + continue; + } - builder.Append(c); - } + if (wasBackslash) + { + wasBackslash = false; + if (c == 'n') + { + builder.Append('\n'); + continue; + } + + // Not a special character, so just append the backslash + builder.Append('\\'); + } - if (builder.Length > 0) - FinishLine(); + builder.Append(c); + } - return values; - } + if (builder.Length > 0) + FinishValue(); - public static string Dump(ReadOnlyDictionary> data) - { - StringBuilder result = new("\n"); + return values; + } - foreach (KeyValuePair> column in data) - result.Append($"{column.Key},"); + public static string Dump(ReadOnlyDictionary> data) + { + StringBuilder result = new("\n"); - result.Remove(result.Length - 1, 1); - result.Append('\n'); + foreach (KeyValuePair> column in data) + result.Append($"{column.Key},"); + + result.Remove(result.Length - 1, 1); + result.Append('\n'); - int rowCount = data.Values.FirstOrDefault()?.Count ?? 0; + int rowCount = data.Values.FirstOrDefault()?.Count ?? 0; - for (int i = 0; i < rowCount; i++) - { - foreach (KeyValuePair> column in data) - if (column.Value.Count > i) - { - string value = column.Value.ElementAt(i).Value.Replace("\n", "\\n"); - result.Append(value.Contains(',') ? $"\"{value}\"," : $"{value},"); - } - else + for (int i = 0; i < rowCount; i++) + { + foreach (KeyValuePair> column in data) { - result.Append(','); + if (column.Value.Count > i) + { + string value = column.Value.ElementAt(i).Value.Replace("\n", "\\n"); + result.Append(value.Contains(',') ? $"\"{value}\"," : $"{value},"); + } + else + { + result.Append(','); + } } - result.Remove(result.Length - 1, 1); - result.Append('\n'); - } + result.Remove(result.Length - 1, 1); + result.Append('\n'); + } - return result.ToString(); + return result.ToString(); + } } } From a4c84533a38295bdb5af537926cfa57e36b98435 Mon Sep 17 00:00:00 2001 From: morm075 <124874578+morm075@users.noreply.github.com> Date: Sun, 16 Jun 2024 13:30:56 +0930 Subject: [PATCH 05/34] Continuing to update server browser --- .../MainMenu/IServerBrowserGameDetails.cs | 25 +++++ .../Components/MainMenu/MultiplayerPane.cs | 54 +++++++++-- .../MainMenu/ServerBrowserElement.cs | 56 +++++++++++ .../MainMenu/ServerBrowserGridView.cs | 32 +++++++ Multiplayer/Multiplayer.cs | 1 + Multiplayer/Multiplayer.csproj | 6 +- .../MainMenu/RightPaneControllerPatch.cs | 12 +-- Multiplayer/Utils/Csvnew.cs | 94 ------------------- 8 files changed, 169 insertions(+), 111 deletions(-) create mode 100644 Multiplayer/Components/MainMenu/IServerBrowserGameDetails.cs create mode 100644 Multiplayer/Components/MainMenu/ServerBrowserElement.cs create mode 100644 Multiplayer/Components/MainMenu/ServerBrowserGridView.cs delete mode 100644 Multiplayer/Utils/Csvnew.cs diff --git a/Multiplayer/Components/MainMenu/IServerBrowserGameDetails.cs b/Multiplayer/Components/MainMenu/IServerBrowserGameDetails.cs new file mode 100644 index 0000000..f199c7c --- /dev/null +++ b/Multiplayer/Components/MainMenu/IServerBrowserGameDetails.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Runtime.CompilerServices; +using Newtonsoft.Json.Linq; +using UnityEngine; + +namespace Multiplayer.Components.MainMenu +{ + // + public interface IServerBrowserGameDetails : IDisposable + { + // + // + int ServerID { get; } + + // + // + // + string Name { get; set; } + + } +} diff --git a/Multiplayer/Components/MainMenu/MultiplayerPane.cs b/Multiplayer/Components/MainMenu/MultiplayerPane.cs index a3d2f16..c75a9b4 100644 --- a/Multiplayer/Components/MainMenu/MultiplayerPane.cs +++ b/Multiplayer/Components/MainMenu/MultiplayerPane.cs @@ -1,14 +1,12 @@ using System; -using System.Net; using System.Text.RegularExpressions; +using DV.Common; using DV.Localization; using DV.UI; using DV.UIFramework; +using DV.Util; using DV.Utils; -using Multiplayer.Components.MainMenu; -using Multiplayer; using Multiplayer.Components.Networking; -using Multiplayer.Patches.MainMenu; using Multiplayer.Utils; using TMPro; using UnityEngine; @@ -24,12 +22,16 @@ public class MultiplayerPane : MonoBehaviour private string ipAddress; private ushort portNumber; - private ButtonDV directButton; + //private ButtonDV directButton; + + private ObservableCollectionExt gridViewModel = new ObservableCollectionExt(); + private ServerBrowserGridView gridView; private void Awake() { Multiplayer.Log("MultiplayerPane Awake()"); SetupMultiplayerButtons(); + SetupServerBrowser(); } private void SetupMultiplayerButtons() @@ -75,8 +77,41 @@ private void SetupMultiplayerButtons() //buttonRefresh.SetActive(true); } + private void SetupServerBrowser() + { + /*GameObject.Destroy(this.FindChildByName("GRID VIEW")); + GameObject Viewport = GameObject.Find("Viewport"); + + GameObject serverBrowserGridView = new GameObject("GRID VIEW", typeof (ServerBrowserGridView)); + serverBrowserGridView.transform.SetParent(Viewport.transform); + gridView = serverBrowserGridView.GetComponent(); + Debug.Log("found Grid View"); + + RectTransform rt = serverBrowserGridView.GetComponent(); + rt.SetInsetAndSizeFromParentEdge(RectTransform.Edge.Top, 0, 5292); + rt.SetInsetAndSizeFromParentEdge(RectTransform.Edge.Left, 0, 662); + */ + GameObject GridviewGO = this.FindChildByName("GRID VIEW"); + SaveLoadGridView slgv = GridviewGO.GetComponent(); + GridviewGO.SetActive(false); + + gridView = GridviewGO.AddComponent(); + gridView.dummyElementPrefab = Instantiate(slgv.viewElementPrefab); + gridView.dummyElementPrefab.name = "prefabServerBrowser"; + GameObject.Destroy(slgv); + GridviewGO.SetActive(true); + + + //gridView.dummyElementPrefab = null; + //gridViewModel.Add(); + + + + } + private GameObject FindButton(string name) { + return GameObject.Find(name); } @@ -178,7 +213,7 @@ private void ShowPasswordPopup() { if (result.closedBy == PopupClosedByAction.Abortion) return; - directButton.enabled = false; + //directButton.enabled = false; SingletonBehaviour.Instance.StartClient(ipAddress, portNumber, result.data); Multiplayer.Settings.LastRemoteIP = ipAddress; @@ -237,6 +272,13 @@ private void HostAction() // Implement host action logic here Debug.Log("Host button clicked."); // Add your code to handle hosting a game + gridView.showDummyElement = true; + gridViewModel.Clear(); + //gridView.dummyElementPrefab = ; + + Debug.Log($"gridViewPrefab exists : {gridView.dummyElementPrefab != null} showDummyElement : {gridView.showDummyElement}"); + gridView.SetModel(gridViewModel); + } private void JoinAction() diff --git a/Multiplayer/Components/MainMenu/ServerBrowserElement.cs b/Multiplayer/Components/MainMenu/ServerBrowserElement.cs new file mode 100644 index 0000000..9aa7154 --- /dev/null +++ b/Multiplayer/Components/MainMenu/ServerBrowserElement.cs @@ -0,0 +1,56 @@ +using DV.Common; +using DV.Localization; +using DV.UIFramework; +using Multiplayer.Utils; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using TMPro; +using UnityEngine; + +namespace Multiplayer.Components.MainMenu; + + +// +public class ServerBrowserElement : AViewElement +{ + private TextMeshProUGUI networkName; + private TextMeshProUGUI playerCount; + private TextMeshProUGUI ping; + private IServerBrowserGameDetails data; + + private void Awake() + { + //Find existing fields to duplicate + networkName = this.FindChildByName("name [noloc]").GetComponent(); + playerCount = this.FindChildByName("date [noloc]").GetComponent(); + ping = this.FindChildByName("time [noloc]").GetComponent(); + + networkName.text = "Test Network"; + playerCount.text = "1/4"; + ping.text = "102"; + } + + public override void SetData(IServerBrowserGameDetails data, AGridView _) + { + if (this.data != null) + { + this.data = null; + } + if (data != null) + { + this.data = data; + } + UpdateView(null, null); + } + + // + private void UpdateView(object sender = null, PropertyChangedEventArgs e = null) + { + networkName.text = data.Name; + } + +} diff --git a/Multiplayer/Components/MainMenu/ServerBrowserGridView.cs b/Multiplayer/Components/MainMenu/ServerBrowserGridView.cs new file mode 100644 index 0000000..ba61ae2 --- /dev/null +++ b/Multiplayer/Components/MainMenu/ServerBrowserGridView.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using DV.Common; +using DV.UI; +using DV.UIFramework; +using UnityEngine; +using UnityEngine.UI; + +namespace Multiplayer.Components.MainMenu +{ + [RequireComponent(typeof(ContentSizeFitter))] + [RequireComponent(typeof(VerticalLayoutGroup))] + // + public class ServerBrowserGridView : AGridView + { + + private void Awake() + { + Debug.Log("serverBrowserGridview Awake"); + this.dummyElementPrefab.SetActive(false); + GameObject.Destroy(this.dummyElementPrefab.GetComponent()); + this.dummyElementPrefab.AddComponent(); + + this.dummyElementPrefab.SetActive(true); + // GameObject defaultPrefab = GameObject.Find("SaveLoadViewElement"); + // this.dummyElementPrefab = Instantiate(defaultPrefab); + } + } +} diff --git a/Multiplayer/Multiplayer.cs b/Multiplayer/Multiplayer.cs index 04af71f..cdbc6cb 100644 --- a/Multiplayer/Multiplayer.cs +++ b/Multiplayer/Multiplayer.cs @@ -1,3 +1,4 @@ +using System; using System.IO; using HarmonyLib; using JetBrains.Annotations; diff --git a/Multiplayer/Multiplayer.csproj b/Multiplayer/Multiplayer.csproj index e9b86a6..42304b6 100644 --- a/Multiplayer/Multiplayer.csproj +++ b/Multiplayer/Multiplayer.csproj @@ -1,4 +1,4 @@ - + net48 latest @@ -78,10 +78,6 @@ - - - - diff --git a/Multiplayer/Patches/MainMenu/RightPaneControllerPatch.cs b/Multiplayer/Patches/MainMenu/RightPaneControllerPatch.cs index 7f27547..bf7d62f 100644 --- a/Multiplayer/Patches/MainMenu/RightPaneControllerPatch.cs +++ b/Multiplayer/Patches/MainMenu/RightPaneControllerPatch.cs @@ -26,6 +26,7 @@ private static void Prefix(RightPaneController __instance) // Find the base pane for Load/Save GameObject basePane = __instance.FindChildByName("PaneRight Load/Save"); + //GameObject basePane = __instance.FindChildByName("PaneRight Launcher"); if (basePane == null) { Multiplayer.LogError("Failed to find Launcher pane!"); @@ -39,19 +40,18 @@ private static void Prefix(RightPaneController __instance) multiplayerPane.name = "PaneRight Multiplayer"; - multiplayerPane.AddComponent(); + //multiplayerPane.AddComponent(); __instance.menuController.controlledMenus.Add(multiplayerPane.GetComponent()); MainMenuController_Awake_Patch.multiplayerButton.GetComponent().requestedMenuIndex = __instance.menuController.controlledMenus.Count - 1; - + Multiplayer.LogError("before Past Destroyed stuff!"); // Clean up unnecessary components and child objects GameObject.Destroy(multiplayerPane.GetComponent()); - GameObject.Destroy(multiplayerPane.GetComponent()); GameObject.Destroy(multiplayerPane.GetComponent()); GameObject.Destroy(multiplayerPane.FindChildByName("ButtonIcon OpenFolder")); GameObject.Destroy(multiplayerPane.FindChildByName("ButtonIcon Rename")); GameObject.Destroy(multiplayerPane.FindChildByName("Text Content")); - + Multiplayer.LogError("Past Destroyed stuff!"); // Update UI elements GameObject titleObj = multiplayerPane.FindChildByName("Title"); @@ -68,7 +68,7 @@ private static void Prefix(RightPaneController __instance) UpdateButton(multiplayerPane, "ButtonTextIcon Load", "ButtonTextIcon Host", Locale.SERVER_BROWSER__HOST_KEY, null, Multiplayer.AssetIndex.multiplayerIcon); UpdateButton(multiplayerPane, "ButtonTextIcon Save", "ButtonTextIcon Join", Locale.SERVER_BROWSER__JOIN_KEY, null, null); UpdateButton(multiplayerPane, "ButtonIcon Delete", "ButtonTextIcon Refresh", Locale.SERVER_BROWSER__REFRESH, null, null); - + multiplayerPane.AddComponent(); MainMenuThingsAndStuff.Create(manager => @@ -82,7 +82,7 @@ private static void Prefix(RightPaneController __instance) }); MainMenuController_Awake_Patch.multiplayerButton.SetActive(true); - + Multiplayer.LogError("At end!"); } private static void UpdateButton(GameObject pane, string oldButtonName, string newButtonName, string localeKey, string toolTipKey, Sprite icon) diff --git a/Multiplayer/Utils/Csvnew.cs b/Multiplayer/Utils/Csvnew.cs deleted file mode 100644 index ef66263..0000000 --- a/Multiplayer/Utils/Csvnew.cs +++ /dev/null @@ -1,94 +0,0 @@ -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; -using System.Text; - -namespace Multiplayer.Utils -{ - public static class Csv - { - public static ReadOnlyDictionary> Parse(string data) - { - var columns = new Dictionary>(); - var lines = data.Split('\n'); - - var keys = ParseLine(lines[0]); - foreach (var key in keys) - columns[key] = new Dictionary(); - - for (int i = 0; i < lines.Length; i++) - { - var values = ParseLine(lines[i]); - if (values.Count == 0 || string.IsNullOrWhiteSpace(values[0])) - continue; - - string key = values[0]; - for (int j = 0; j < values.Count; j++) - columns[keys[j]][key] = values[j]; - } - - return new ReadOnlyDictionary>(columns); - } - - private static List ParseLine(string line) - { - var values = new List(); - var builder = new StringBuilder(); - - bool inQuotes = false; - foreach (char c in line) - { - if (c == ',' && !inQuotes) - { - values.Add(builder.ToString()); - builder.Clear(); - } - else if (c == '"') - { - inQuotes = !inQuotes; - } - else - { - builder.Append(c); - } - } - - values.Add(builder.ToString()); - return values; - } - - public static string Dump(ReadOnlyDictionary> data) - { - var result = new StringBuilder(); - - foreach (var column in data) - result.Append($"{column.Key},"); - - result.Length--; - result.Append('\n'); - - int rowCount = data.Values.FirstOrDefault()?.Count ?? 0; - - for (int i = 0; i < rowCount; i++) - { - foreach (var column in data) - { - if (column.Value.Count > i) - { - string value = column.Value.ElementAt(i).Value.Replace("\n", "\\n"); - result.Append(value.Contains(',') ? $"\"{value}\"," : $"{value},"); - } - else - { - result.Append(','); - } - } - - result.Length--; - result.Append('\n'); - } - - return result.ToString(); - } - } -} From af9cd6d884177d2990e2205008ed7a87b20d40aa Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 16 Jun 2024 22:04:12 +1000 Subject: [PATCH 06/34] Minor fixes and improvements Fixed main menu highlight bug added random server generation for testing fixed gridview element layout implemented a server data object --- .../MainMenu/IServerBrowserGameDetails.cs | 29 ++ .../MainMenu/MainMenuThingsAndStuff.cs | 1 + .../Components/MainMenu/MultiplayerPane.cs | 320 ++++++++++++++---- ...pupTextInputFieldControllerNoValidation.cs | 93 +++++ .../MainMenu/ServerBrowserElement.cs | 90 +++++ .../MainMenu/ServerBrowserGridView.cs | 36 ++ Multiplayer/Locale.cs | 221 ++++++------ Multiplayer/Multiplayer.cs | 4 +- Multiplayer/Multiplayer.csproj | 6 +- .../MainMenu/LocalizationManagerPatch.cs | 33 +- .../MainMenu/MainMenuControllerPatch.cs | 80 +++-- .../MainMenu/RightPaneControllerPatch.cs | 149 +++++--- Multiplayer/Settings.cs | 12 +- Multiplayer/Utils/Sprites.cs | 111 ++++++ Sprites/lock.png | Bin 0 -> 327 bytes locale.csv | 22 +- 16 files changed, 937 insertions(+), 270 deletions(-) create mode 100644 Multiplayer/Components/MainMenu/IServerBrowserGameDetails.cs create mode 100644 Multiplayer/Components/MainMenu/PopupTextInputFieldControllerNoValidation.cs create mode 100644 Multiplayer/Components/MainMenu/ServerBrowserElement.cs create mode 100644 Multiplayer/Components/MainMenu/ServerBrowserGridView.cs create mode 100644 Multiplayer/Utils/Sprites.cs create mode 100644 Sprites/lock.png diff --git a/Multiplayer/Components/MainMenu/IServerBrowserGameDetails.cs b/Multiplayer/Components/MainMenu/IServerBrowserGameDetails.cs new file mode 100644 index 0000000..9c1271c --- /dev/null +++ b/Multiplayer/Components/MainMenu/IServerBrowserGameDetails.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Runtime.CompilerServices; +using Newtonsoft.Json.Linq; +using UnityEngine; + +namespace Multiplayer.Components.MainMenu +{ + // + public interface IServerBrowserGameDetails : IDisposable + { + // + // + int ServerID { get; } + + // + // + // + string Name { get; set; } + int MaxPlayers { get; set; } + int CurrentPlayers { get; set; } + int Ping { get; set; } + bool HasPassword { get; set; } + + } +} diff --git a/Multiplayer/Components/MainMenu/MainMenuThingsAndStuff.cs b/Multiplayer/Components/MainMenu/MainMenuThingsAndStuff.cs index 02a6d6b..9920071 100644 --- a/Multiplayer/Components/MainMenu/MainMenuThingsAndStuff.cs +++ b/Multiplayer/Components/MainMenu/MainMenuThingsAndStuff.cs @@ -63,6 +63,7 @@ public void SwitchToMenu(byte index) [CanBeNull] public Popup ShowRenamePopup() { + Debug.Log("public Popup ShowRenamePopup() ..."); return ShowPopup(renamePopupPrefab); } diff --git a/Multiplayer/Components/MainMenu/MultiplayerPane.cs b/Multiplayer/Components/MainMenu/MultiplayerPane.cs index be42068..8b6844f 100644 --- a/Multiplayer/Components/MainMenu/MultiplayerPane.cs +++ b/Multiplayer/Components/MainMenu/MultiplayerPane.cs @@ -1,120 +1,306 @@ -using System; +using System; +using System.Collections.Generic; using System.Text.RegularExpressions; +using DV.Common; +using DV.Localization; +using DV.UI; using DV.UIFramework; +using DV.Util; using DV.Utils; using Multiplayer.Components.Networking; +using Multiplayer.Utils; +using TMPro; using UnityEngine; -namespace Multiplayer.Components.MainMenu; - -public class MultiplayerPane : MonoBehaviour +namespace Multiplayer.Components.MainMenu { - // @formatter:off - // Patterns from https://ihateregex.io/ - private static readonly Regex IPv4 = new(@"(\b25[0-5]|\b2[0-4][0-9]|\b[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}"); - private static readonly Regex IPv6 = new(@"(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))"); - private static readonly Regex PORT = new(@"^((6553[0-5])|(655[0-2][0-9])|(65[0-4][0-9]{2})|(6[0-4][0-9]{3})|([1-5][0-9]{4})|([0-5]{0,5})|([0-9]{1,4}))$"); - // @formatter:on + public class MultiplayerPane : MonoBehaviour + { + // Regular expressions for IP and port validation + private static readonly Regex IPv4Regex = new Regex(@"(\b25[0-5]|\b2[0-4][0-9]|\b[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}"); + private static readonly Regex IPv6Regex = new Regex(@"(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))"); + private static readonly Regex PortRegex = new Regex(@"^((6553[0-5])|(655[0-2][0-9])|(65[0-4][0-9]{2})|(6[0-4][0-9]{3})|([1-5][0-9]{4})|([0-5]{0,5})|([0-9]{1,4}))$"); - private bool why; + private string ipAddress; + private ushort portNumber; + //private ButtonDV directButton; - private string address; - private ushort port; + private ObservableCollectionExt gridViewModel = new ObservableCollectionExt(); + private ServerBrowserGridView gridView; - private void OnEnable() - { - if (!why) + private string[] testNames = new string[] { "ChooChooExpress", "RailwayRascals", "FreightFrenzy", "SteamDream", "DieselDynasty", "CargoKings", "TrackMasters", "RailwayRevolution", "ExpressElders", "IronHorseHeroes", "LocomotiveLegends", "TrainTitans", "HeavyHaulers", "RapidRails", "TimberlineTransport", "CoalCountry", "SilverRailway", "GoldenGauge", "SteelStream", "MountainMoguls", "RailRiders", "TrackTrailblazers", "FreightFanatics", "SteamSensation", "DieselDaredevils", "CargoChampions", "TrackTacticians", "RailwayRoyals", "ExpressExperts", "IronHorseInnovators", "LocomotiveLeaders", "TrainTacticians", "HeavyHitters", "RapidRunners", "TimberlineTrains", "CoalCrushers", "SilverStreamliners", "GoldenGears", "SteelSurge", "MountainMovers", "RailwayWarriors", "TrackTerminators", "FreightFighters", "SteamStreak", "DieselDynamos", "CargoCommanders", "TrackTrailblazers", "RailwayRangers", "ExpressEngineers", "IronHorseInnovators", "LocomotiveLovers", "TrainTrailblazers", "HeavyHaulersHub", "RapidRailsRacers", "TimberlineTrackers", "CoalCountryCarriers", "SilverSpeedsters", "GoldenGaugeGang", "SteelStalwarts", "MountainMoversClub", "RailRunners", "TrackTitans", "FreightFalcons", "SteamSprinters", "DieselDukes", "CargoCommandos", "TrackTracers", "RailwayRebels", "ExpressElite", "IronHorseIcons", "LocomotiveLunatics", "TrainTornadoes", "HeavyHaulersCrew", "RapidRailsRunners", "TimberlineTrackMasters", "CoalCountryCrew", "SilverSprinters", "GoldenGale", "SteelSpeedsters", "MountainMarauders", "RailwayRiders", "TrackTactics", "FreightFury", "SteamSquires", "DieselDefenders", "CargoCrusaders", "TrackTechnicians", "RailwayRaiders", "ExpressEnthusiasts", "IronHorseIlluminati", "LocomotiveLoyalists", "TrainTurbulence", "HeavyHaulersHeroes", "RapidRailsRiders", "TimberlineTrackTitans", "CoalCountryCaravans", "SilverSpeedRacers", "GoldenGaugeGangsters", "SteelStorm", "MountainMasters", "RailwayRoadrunners", "TrackTerror", "FreightFleets", "SteamSurgeons", "DieselDragons", "CargoCrushers", "TrackTaskmasters", "RailwayRevolutionaries", "ExpressExplorers", "IronHorseInquisitors", "LocomotiveLegion", "TrainTriumph", "HeavyHaulersHorde", "RapidRailsRenegades", "TimberlineTrackTeam", "CoalCountryCrusade", "SilverSprintersSquad", "GoldenGaugeGroup", "SteelStrike", "MountainMonarchs", "RailwayRaid", "TrackTacticiansTeam", "FreightForce", "SteamSquad", "DieselDynastyClan", "CargoCrew", "TrackTeam", "RailwayRalliers", "ExpressExpedition", "IronHorseInitiative", "LocomotiveLeague", "TrainTribe", "HeavyHaulersHustle", "RapidRailsRevolution", "TimberlineTrackersTeam", "CoalCountryConvoy", "SilverSprint", "GoldenGaugeGuild", "SteelSpirits", "MountainMayhem", "RailwayRaidersCrew", "TrackTrailblazersTribe", "FreightFleetForce", "SteamStalwarts", "DieselDragonsDen", "CargoCaptains", "TrackTrailblazersTeam", "RailwayRidersRevolution", "ExpressEliteExpedition", "IronHorseInsiders", "LocomotiveLords", "TrainTacticiansTribe", "HeavyHaulersHeroesHorde", "RapidRailsRacersTeam", "TimberlineTrackMastersTeam", "CoalCountryCarriersCrew", "SilverSpeedstersSprint", "GoldenGaugeGangGuild", "SteelSurgeStrike", "MountainMoversMonarchs" }; + + private void Awake() { - why = true; - return; + Multiplayer.Log("MultiplayerPane Awake()"); + SetupMultiplayerButtons(); + SetupServerBrowser(); } - ShowIpPopup(); - } + private void SetupMultiplayerButtons() + { + GameObject buttonDirectIP = GameObject.Find("ButtonTextIcon Manual"); + GameObject buttonHost = GameObject.Find("ButtonTextIcon Host"); + GameObject buttonJoin = GameObject.Find("ButtonTextIcon Join"); + GameObject buttonRefresh = GameObject.Find("ButtonTextIcon Refresh"); - private void ShowIpPopup() - { - Popup popup = MainMenuThingsAndStuff.Instance.ShowRenamePopup(); - if (popup == null) - return; + if (buttonDirectIP == null || buttonHost == null || buttonJoin == null || buttonRefresh == null) + { + Multiplayer.LogError("One or more buttons not found."); + return; + } + + // Modify the existing buttons' properties + ModifyButton(buttonDirectIP, Locale.SERVER_BROWSER__MANUAL_CONNECT_KEY); + ModifyButton(buttonHost, Locale.SERVER_BROWSER__HOST_KEY); + ModifyButton(buttonJoin, Locale.SERVER_BROWSER__JOIN_KEY); + //ModifyButton(buttonRefresh, Locale.SERVER_BROWSER__REFRESH); + + // Set up event listeners and localization for DirectIP button + ButtonDV buttonDirectIPDV = buttonDirectIP.GetComponent(); + buttonDirectIPDV.onClick.AddListener(ShowIpPopup); + + // Set up event listeners and localization for Host button + ButtonDV buttonHostDV = buttonHost.GetComponent(); + buttonHostDV.onClick.AddListener(HostAction); + + // Set up event listeners and localization for Join button + ButtonDV buttonJoinDV = buttonJoin.GetComponent(); + buttonJoinDV.onClick.AddListener(JoinAction); + + // Set up event listeners and localization for Refresh button + //ButtonDV buttonRefreshDV = buttonRefresh.GetComponent(); + //buttonRefreshDV.onClick.AddListener(RefreshAction); + + //Debug.Log("Setting buttons active: " + buttonDirectIP.name + ", " + buttonHost.name + ", " + buttonJoin.name + ", " + buttonRefresh.name ); + Debug.Log("Setting buttons active: " + buttonDirectIP.name + ", " + buttonHost.name + ", " + buttonJoin.name ); + buttonDirectIP.SetActive(true); + buttonHost.SetActive(true); + buttonJoin.SetActive(true); + //buttonRefresh.SetActive(true); + } + + private void SetupServerBrowser() + { + GameObject GridviewGO = this.FindChildByName("GRID VIEW"); + SaveLoadGridView slgv = GridviewGO.GetComponent(); + + GridviewGO.SetActive(false); + + gridView = GridviewGO.AddComponent(); + gridView.dummyElementPrefab = Instantiate(slgv.viewElementPrefab); + gridView.dummyElementPrefab.name = "prefabServerBrowser"; + + GameObject.Destroy(slgv); + + GridviewGO.SetActive(true); + } + + private GameObject FindButton(string name) + { + + return GameObject.Find(name); + } + + private void ModifyButton(GameObject button, string key) + { + button.GetComponentInChildren().key = key; - popup.labelTMPro.text = Locale.SERVER_BROWSER__IP; + } - popup.Closed += result => + private void ShowIpPopup() { - if (result.closedBy == PopupClosedByAction.Abortion) + Debug.Log("In ShowIpPpopup"); + var popup = MainMenuThingsAndStuff.Instance.ShowRenamePopup(); + if (popup == null) { - MainMenuThingsAndStuff.Instance.SwitchToDefaultMenu(); + Multiplayer.LogError("Popup not found."); return; } - if (!IPv4.IsMatch(result.data) && !IPv6.IsMatch(result.data)) + popup.labelTMPro.text = Locale.SERVER_BROWSER__IP; + popup.GetComponentInChildren().text = Multiplayer.Settings.LastRemoteIP; + + popup.Closed += result => + { + if (result.closedBy == PopupClosedByAction.Abortion) + { + MainMenuThingsAndStuff.Instance.SwitchToDefaultMenu(); + return; + } + + HandleIpAddressInput(result.data); + }; + } + + private void HandleIpAddressInput(string input) + { + if (!IPv4Regex.IsMatch(input) && !IPv6Regex.IsMatch(input)) { ShowOkPopup(Locale.SERVER_BROWSER__IP_INVALID, ShowIpPopup); return; } - address = result.data; - + ipAddress = input; ShowPortPopup(); - }; - } + } - private void ShowPortPopup() - { - Popup popup = MainMenuThingsAndStuff.Instance.ShowRenamePopup(); - if (popup == null) - return; + private void ShowPortPopup() + { + var popup = MainMenuThingsAndStuff.Instance.ShowRenamePopup(); + if (popup == null) + { + Multiplayer.LogError("Popup not found."); + return; + } + + popup.labelTMPro.text = Locale.SERVER_BROWSER__PORT; + popup.GetComponentInChildren().text = Multiplayer.Settings.LastRemotePort.ToString(); - popup.labelTMPro.text = Locale.SERVER_BROWSER__PORT; + popup.Closed += result => + { + if (result.closedBy == PopupClosedByAction.Abortion) + { + MainMenuThingsAndStuff.Instance.SwitchToDefaultMenu(); + return; + } + + HandlePortInput(result.data); + }; + } - popup.Closed += result => + private void HandlePortInput(string input) { - if (result.closedBy == PopupClosedByAction.Abortion) + if (!PortRegex.IsMatch(input)) { - MainMenuThingsAndStuff.Instance.SwitchToDefaultMenu(); + ShowOkPopup(Locale.SERVER_BROWSER__PORT_INVALID, ShowPortPopup); return; } - if (!PORT.IsMatch(result.data)) + portNumber = ushort.Parse(input); + ShowPasswordPopup(); + } + + private void ShowPasswordPopup() + { + var popup = MainMenuThingsAndStuff.Instance.ShowRenamePopup(); + if (popup == null) { - ShowOkPopup(Locale.SERVER_BROWSER__PORT_INVALID, ShowPortPopup); + Multiplayer.LogError("Popup not found."); return; } - port = ushort.Parse(result.data); + popup.labelTMPro.text = Locale.SERVER_BROWSER__PASSWORD; + popup.GetComponentInChildren().text = Multiplayer.Settings.LastRemotePassword; - ShowPasswordPopup(); - }; - } + DestroyImmediate(popup.GetComponentInChildren()); + popup.GetOrAddComponent(); - private void ShowPasswordPopup() - { - Popup popup = MainMenuThingsAndStuff.Instance.ShowRenamePopup(); - if (popup == null) - return; + popup.Closed += result => + { + if (result.closedBy == PopupClosedByAction.Abortion) return; + + //directButton.enabled = false; + SingletonBehaviour.Instance.StartClient(ipAddress, portNumber, result.data); + + Multiplayer.Settings.LastRemoteIP = ipAddress; + Multiplayer.Settings.LastRemotePort = portNumber; + Multiplayer.Settings.LastRemotePassword = result.data; + + //ShowConnectingPopup(); // Show a connecting message + //SingletonBehaviour.Instance.ConnectionFailed += HandleConnectionFailed; + //SingletonBehaviour.Instance.ConnectionEstablished += HandleConnectionEstablished; + }; + } + + // Example of handling connection success + private void HandleConnectionEstablished() + { + // Connection established, handle the UI or game state accordingly + Debug.Log("Connection established!"); + // HideConnectingPopup(); // Hide the connecting message + } + + // Example of handling connection failure + private void HandleConnectionFailed() + { + // Connection failed, show an error message or handle the failure scenario + Debug.LogError("Connection failed!"); + // ShowConnectionFailedPopup(); + } - popup.labelTMPro.text = Locale.SERVER_BROWSER__PASSWORD; + private void RefreshAction() + { + // Implement refresh action logic here + Debug.Log("Refresh button clicked."); + // Add your code to refresh the multiplayer list or perform any other refresh-related action + } + + + private static void ShowOkPopup(string text, Action onClick) + { + var popup = MainMenuThingsAndStuff.Instance.ShowOkPopup(); + if (popup == null) return; + + popup.labelTMPro.text = text; + popup.Closed += _ => onClick(); + } - popup.Closed += result => + private void SetButtonsActive(params GameObject[] buttons) { - if (result.closedBy == PopupClosedByAction.Abortion) + foreach (var button in buttons) { - MainMenuThingsAndStuff.Instance.SwitchToDefaultMenu(); - return; + button.SetActive(true); } + } + + private void HostAction() + { + // Implement host action logic here + Debug.Log("Host button clicked."); + // Add your code to handle hosting a game + - SingletonBehaviour.Instance.StartClient(address, port, result.data); - }; + //gridView.showDummyElement = true; + gridViewModel.Clear(); + + + IServerBrowserGameDetails item = null; + + for (int i = 0; i < UnityEngine.Random.Range(1, 50); i++) { + + item = new ServerData(); + item.Name = testNames[UnityEngine.Random.Range(0, testNames.Length-1)]; + item.MaxPlayers = UnityEngine.Random.Range(1, 10); + item.CurrentPlayers = UnityEngine.Random.Range(1, item.MaxPlayers); + item.Ping = UnityEngine.Random.Range(5, 1500); + item.HasPassword = UnityEngine.Random.Range(0, 10) > 5; + + Debug.Log(item.HasPassword); + gridViewModel.Add(item); + } + + gridView.SetModel(gridViewModel); + + } + + private void JoinAction() + { + // Implement join action logic here + Debug.Log("Join button clicked."); + // Add your code to handle joining a game + } } - private static void ShowOkPopup(string text, Action onClick) + public class ServerData : IServerBrowserGameDetails { - Popup popup = MainMenuThingsAndStuff.Instance.ShowOkPopup(); - if (popup == null) - return; + public int ServerID { get; } + public string Name { get; set; } + public int MaxPlayers { get; set; } + public int CurrentPlayers { get; set; } + public int Ping { get; set; } + public bool HasPassword { get; set; } - popup.labelTMPro.text = text; - popup.Closed += _ => { onClick(); }; + public void Dispose() {} } } diff --git a/Multiplayer/Components/MainMenu/PopupTextInputFieldControllerNoValidation.cs b/Multiplayer/Components/MainMenu/PopupTextInputFieldControllerNoValidation.cs new file mode 100644 index 0000000..1cda123 --- /dev/null +++ b/Multiplayer/Components/MainMenu/PopupTextInputFieldControllerNoValidation.cs @@ -0,0 +1,93 @@ +using System; +using System.Reflection; +using DV.UIFramework; +using TMPro; +using UnityEngine; +using UnityEngine.Events; + +namespace Multiplayer.Components.MainMenu +{ + public class PopupTextInputFieldControllerNoValidation : MonoBehaviour, IPopupSubmitHandler + { + public Popup popup; + public TMP_InputField field; + public ButtonDV confirmButton; + + private void Awake() + { + // Find the components + popup = this.GetComponentInParent(); + field = popup.GetComponentInChildren(); + + foreach (ButtonDV btn in popup.GetComponentsInChildren()) + { + if (btn.name == "ButtonYes") + { + confirmButton = btn; + } + } + + // Set this instance as the new handler for the dialog + typeof(Popup).GetField("handler", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(popup, this); + } + + private void Start() + { + // Add listener for input field value changes + field.onValueChanged.AddListener(new UnityAction(OnInputValueChanged)); + OnInputValueChanged(field.text); + field.Select(); + field.ActivateInputField(); + } + + private void OnInputValueChanged(string value) + { + // Toggle confirm button interactability based on input validity + confirmButton.ToggleInteractable(IsInputValid(value)); + } + + public void HandleAction(PopupClosedByAction action) + { + switch (action) + { + case PopupClosedByAction.Positive: + if (IsInputValid(field.text)) + { + RequestPositive(); + return; + } + break; + case PopupClosedByAction.Negative: + RequestNegative(); + return; + case PopupClosedByAction.Abortion: + RequestAbortion(); + return; + default: + Debug.LogError(string.Format("Unhandled action {0}", action), this); + break; + } + } + + private bool IsInputValid(string value) + { + // Always return true to disable validation + return true; + } + + private void RequestPositive() + { + this.popup.RequestClose(PopupClosedByAction.Positive, this.field.text); + } + + private void RequestNegative() + { + this.popup.RequestClose(PopupClosedByAction.Negative, null); + } + + private void RequestAbortion() + { + this.popup.RequestClose(PopupClosedByAction.Abortion, null); + } + } +} diff --git a/Multiplayer/Components/MainMenu/ServerBrowserElement.cs b/Multiplayer/Components/MainMenu/ServerBrowserElement.cs new file mode 100644 index 0000000..94e7687 --- /dev/null +++ b/Multiplayer/Components/MainMenu/ServerBrowserElement.cs @@ -0,0 +1,90 @@ +using DV.UIFramework; +using Multiplayer.Utils; +using System.ComponentModel; +using TMPro; +using UnityEngine; +using UnityEngine.UI; + +namespace Multiplayer.Components.MainMenu; + + +// +public class ServerBrowserElement : AViewElement +{ + private TextMeshProUGUI networkName; + private TextMeshProUGUI playerCount; + private TextMeshProUGUI ping; + private GameObject goIcon; + private Image icon; + private IServerBrowserGameDetails data; + + private const int PING_WIDTH = 62 * 2; + private const int PING_POS_X = 650; + private void Awake() + { + //Find existing fields to duplicate + networkName = this.FindChildByName("name [noloc]").GetComponent(); + playerCount = this.FindChildByName("date [noloc]").GetComponent(); + ping = this.FindChildByName("time [noloc]").GetComponent(); + goIcon = this.FindChildByName("autosave icon"); + icon = goIcon.GetComponent(); + + //Fix alignment + Vector3 namePos = networkName.transform.position; + Vector2 nameSize = networkName.rectTransform.sizeDelta; + + playerCount.transform.position = new Vector3(namePos.x + nameSize.x, namePos.y, namePos.z); + + + Vector2 rowSize = this.transform.GetComponentInParent().sizeDelta; + Vector3 pingPos = ping.transform.position; + Vector2 pingSize = ping.rectTransform.sizeDelta; + + + ping.rectTransform.sizeDelta = new Vector2(PING_WIDTH, pingSize.y); + pingSize = ping.rectTransform.sizeDelta; + + ping.transform.position = new Vector3(PING_POS_X, pingPos.y, pingPos.z); + + ping.alignment = TextAlignmentOptions.Right; + + + //Update clock Icon + icon.sprite = Sprites.Padlock; + + + + /* + networkName.text = "Test Network"; + playerCount.text = "1/4"; + ping.text = "102"; + */ + } + + public override void SetData(IServerBrowserGameDetails data, AGridView _) + { + if (this.data != null) + { + this.data = null; + } + if (data != null) + { + this.data = data; + } + UpdateView(null, null); + } + + // + private void UpdateView(object sender = null, PropertyChangedEventArgs e = null) + { + networkName.text = data.Name; + playerCount.text = $"{data.CurrentPlayers} / {data.MaxPlayers}"; + ping.text = $"{data.Ping} ms"; + + if (!data.HasPassword) + { + goIcon.SetActive(false); + } + } + +} diff --git a/Multiplayer/Components/MainMenu/ServerBrowserGridView.cs b/Multiplayer/Components/MainMenu/ServerBrowserGridView.cs new file mode 100644 index 0000000..a4a2196 --- /dev/null +++ b/Multiplayer/Components/MainMenu/ServerBrowserGridView.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using DV.Common; +using DV.UI; +using DV.UIFramework; +using UnityEngine; +using UnityEngine.UI; + +namespace Multiplayer.Components.MainMenu +{ + [RequireComponent(typeof(ContentSizeFitter))] + [RequireComponent(typeof(VerticalLayoutGroup))] + // + public class ServerBrowserGridView : AGridView + { + + private void Awake() + { + Debug.Log("serverBrowserGridview Awake"); + + this.dummyElementPrefab.SetActive(false); + + //swap controller + GameObject.Destroy(this.dummyElementPrefab.GetComponent()); + this.dummyElementPrefab.AddComponent(); + + this.dummyElementPrefab.SetActive(true); + this.viewElementPrefab = this.dummyElementPrefab; + // GameObject defaultPrefab = GameObject.Find("SaveLoadViewElement"); + // this.dummyElementPrefab = Instantiate(defaultPrefab); + } + } +} diff --git a/Multiplayer/Locale.cs b/Multiplayer/Locale.cs index e6d1544..274c5d6 100644 --- a/Multiplayer/Locale.cs +++ b/Multiplayer/Locale.cs @@ -5,149 +5,160 @@ using I2.Loc; using Multiplayer.Utils; -namespace Multiplayer; - -public static class Locale +namespace Multiplayer { - private const string DEFAULT_LOCALE_FILE = "locale.csv"; + public static class Locale + { + private const string DEFAULT_LOCALE_FILE = "locale.csv"; + private const string DEFAULT_LANGUAGE = "English"; + public const string MISSING_TRANSLATION = "[ MISSING TRANSLATION ]"; + public const string PREFIX = "multiplayer/"; - private const string DEFAULT_LANGUAGE = "English"; - public const string MISSING_TRANSLATION = "[ MISSING TRANSLATION ]"; - public const string PREFIX = "multiplayer/"; + private const string PREFIX_MAIN_MENU = $"{PREFIX}mm"; + private const string PREFIX_SERVER_BROWSER = $"{PREFIX}sb"; + private const string PREFIX_DISCONN_REASON = $"{PREFIX}dr"; + private const string PREFIX_CAREER_MANAGER = $"{PREFIX}carman"; + private const string PREFIX_PLAYER_LIST = $"{PREFIX}plist"; + private const string PREFIX_LOADING_INFO = $"{PREFIX}linfo"; - private const string PREFIX_MAIN_MENU = $"{PREFIX}mm"; - private const string PREFIX_SERVER_BROWSER = $"{PREFIX}sb"; - private const string PREFIX_DISCONN_REASON = $"{PREFIX}dr"; - private const string PREFIX_CAREER_MANAGER = $"{PREFIX}carman"; - private const string PREFIX_PLAYER_LIST = $"{PREFIX}plist"; - private const string PREFIX_LOADING_INFO = $"{PREFIX}linfo"; + #region Main Menu + public static string MAIN_MENU__JOIN_SERVER => Get(MAIN_MENU__JOIN_SERVER_KEY); + public const string MAIN_MENU__JOIN_SERVER_KEY = $"{PREFIX_MAIN_MENU}/join_server"; + #endregion - #region Main Menu + #region Server Browser + public static string SERVER_BROWSER__TITLE => Get(SERVER_BROWSER__TITLE_KEY); + public const string SERVER_BROWSER__TITLE_KEY = $"{PREFIX_SERVER_BROWSER}/title"; - public static string MAIN_MENU__JOIN_SERVER => Get(MAIN_MENU__JOIN_SERVER_KEY); - public const string MAIN_MENU__JOIN_SERVER_KEY = $"{PREFIX_MAIN_MENU}/join_server"; + public static string SERVER_BROWSER__MANUAL_CONNECT => Get(SERVER_BROWSER__MANUAL_CONNECT_KEY); + public const string SERVER_BROWSER__MANUAL_CONNECT_KEY = $"{PREFIX_SERVER_BROWSER}/manual_connect"; - #endregion + public static string SERVER_BROWSER__HOST => Get(SERVER_BROWSER__HOST_KEY); + public const string SERVER_BROWSER__HOST_KEY = $"{PREFIX_SERVER_BROWSER}/host"; + + public static string SERVER_BROWSER__REFRESH => Get(SERVER_BROWSER__REFRESH_KEY); + public const string SERVER_BROWSER__REFRESH_KEY = $"{PREFIX_SERVER_BROWSER}/refresh"; - #region Server Browser + public static string SERVER_BROWSER__JOIN => Get(SERVER_BROWSER__JOIN_KEY); + public const string SERVER_BROWSER__JOIN_KEY = $"{PREFIX_SERVER_BROWSER}/join_game"; - public static string SERVER_BROWSER__TITLE => Get(SERVER_BROWSER__TITLE_KEY); - public const string SERVER_BROWSER__TITLE_KEY = $"{PREFIX_SERVER_BROWSER}/title"; + public static string SERVER_BROWSER__IP => Get(SERVER_BROWSER__IP_KEY); + private const string SERVER_BROWSER__IP_KEY = $"{PREFIX_SERVER_BROWSER}/ip"; - public static string SERVER_BROWSER__IP => Get(SERVER_BROWSER__IP_KEY); - private const string SERVER_BROWSER__IP_KEY = $"{PREFIX_SERVER_BROWSER}/ip"; - public static string SERVER_BROWSER__IP_INVALID => Get(SERVER_BROWSER__IP_INVALID_KEY); - private const string SERVER_BROWSER__IP_INVALID_KEY = $"{PREFIX_SERVER_BROWSER}/ip_invalid"; - public static string SERVER_BROWSER__PORT => Get(SERVER_BROWSER__PORT_KEY); - private const string SERVER_BROWSER__PORT_KEY = $"{PREFIX_SERVER_BROWSER}/port"; - public static string SERVER_BROWSER__PORT_INVALID => Get(SERVER_BROWSER__PORT_INVALID_KEY); - private const string SERVER_BROWSER__PORT_INVALID_KEY = $"{PREFIX_SERVER_BROWSER}/port_invalid"; - public static string SERVER_BROWSER__PASSWORD => Get(SERVER_BROWSER__PASSWORD_KEY); - private const string SERVER_BROWSER__PASSWORD_KEY = $"{PREFIX_SERVER_BROWSER}/password"; + public static string SERVER_BROWSER__IP_INVALID => Get(SERVER_BROWSER__IP_INVALID_KEY); + private const string SERVER_BROWSER__IP_INVALID_KEY = $"{PREFIX_SERVER_BROWSER}/ip_invalid"; - #endregion + public static string SERVER_BROWSER__PORT => Get(SERVER_BROWSER__PORT_KEY); + private const string SERVER_BROWSER__PORT_KEY = $"{PREFIX_SERVER_BROWSER}/port"; - #region Disconnect Reason + public static string SERVER_BROWSER__PORT_INVALID => Get(SERVER_BROWSER__PORT_INVALID_KEY); + private const string SERVER_BROWSER__PORT_INVALID_KEY = $"{PREFIX_SERVER_BROWSER}/port_invalid"; - public static string DISCONN_REASON__INVALID_PASSWORD => Get(DISCONN_REASON__INVALID_PASSWORD_KEY); - public const string DISCONN_REASON__INVALID_PASSWORD_KEY = $"{PREFIX_DISCONN_REASON}/invalid_password"; - public static string DISCONN_REASON__GAME_VERSION => Get(DISCONN_REASON__GAME_VERSION_KEY); - public const string DISCONN_REASON__GAME_VERSION_KEY = $"{PREFIX_DISCONN_REASON}/game_version"; - public static string DISCONN_REASON__FULL_SERVER => Get(DISCONN_REASON__FULL_SERVER_KEY); - public const string DISCONN_REASON__FULL_SERVER_KEY = $"{PREFIX_DISCONN_REASON}/full_server"; - public static string DISCONN_REASON__MODS => Get(DISCONN_REASON__MODS_KEY); - public const string DISCONN_REASON__MODS_KEY = $"{PREFIX_DISCONN_REASON}/mods"; - public static string DISCONN_REASON__MODS_MISSING => Get(DISCONN_REASON__MODS_MISSING_KEY); - public const string DISCONN_REASON__MODS_MISSING_KEY = $"{PREFIX_DISCONN_REASON}/mods_missing"; - public static string DISCONN_REASON__MODS_EXTRA => Get(DISCONN_REASON__MODS_EXTRA_KEY); - public const string DISCONN_REASON__MODS_EXTRA_KEY = $"{PREFIX_DISCONN_REASON}/mods_extra"; + public static string SERVER_BROWSER__PASSWORD => Get(SERVER_BROWSER__PASSWORD_KEY); + private const string SERVER_BROWSER__PASSWORD_KEY = $"{PREFIX_SERVER_BROWSER}/password"; + #endregion - #endregion + #region Disconnect Reason + public static string DISCONN_REASON__INVALID_PASSWORD => Get(DISCONN_REASON__INVALID_PASSWORD_KEY); + public const string DISCONN_REASON__INVALID_PASSWORD_KEY = $"{PREFIX_DISCONN_REASON}/invalid_password"; - #region Career Manager + public static string DISCONN_REASON__GAME_VERSION => Get(DISCONN_REASON__GAME_VERSION_KEY); + public const string DISCONN_REASON__GAME_VERSION_KEY = $"{PREFIX_DISCONN_REASON}/game_version"; - public static string CAREER_MANAGER__FEES_HOST_ONLY => Get(CAREER_MANAGER__FEES_HOST_ONLY_KEY); - private const string CAREER_MANAGER__FEES_HOST_ONLY_KEY = $"{PREFIX_CAREER_MANAGER}/fees_host_only"; + public static string DISCONN_REASON__FULL_SERVER => Get(DISCONN_REASON__FULL_SERVER_KEY); + public const string DISCONN_REASON__FULL_SERVER_KEY = $"{PREFIX_DISCONN_REASON}/full_server"; - #endregion + public static string DISCONN_REASON__MODS => Get(DISCONN_REASON__MODS_KEY); + public const string DISCONN_REASON__MODS_KEY = $"{PREFIX_DISCONN_REASON}/mods"; - #region Player List + public static string DISCONN_REASON__MOD_LIST => Get(DISCONN_REASON__MOD_LIST_KEY); + public const string DISCONN_REASON__MOD_LIST_KEY = $"{PREFIX_DISCONN_REASON}/mod_list"; - public static string PLAYER_LIST__TITLE => Get(PLAYER_LIST__TITLE_KEY); - private const string PLAYER_LIST__TITLE_KEY = $"{PREFIX_PLAYER_LIST}/title"; + public static string DISCONN_REASON__MODS_MISSING => Get(DISCONN_REASON__MODS_MISSING_KEY); + public const string DISCONN_REASON__MODS_MISSING_KEY = $"{PREFIX_DISCONN_REASON}/mods_missing"; - #endregion + public static string DISCONN_REASON__MODS_EXTRA => Get(DISCONN_REASON__MODS_EXTRA_KEY); + public const string DISCONN_REASON__MODS_EXTRA_KEY = $"{PREFIX_DISCONN_REASON}/mods_extra"; + #endregion - #region Loading Info + #region Career Manager + public static string CAREER_MANAGER__FEES_HOST_ONLY => Get(CAREER_MANAGER__FEES_HOST_ONLY_KEY); + private const string CAREER_MANAGER__FEES_HOST_ONLY_KEY = $"{PREFIX_CAREER_MANAGER}/fees_host_only"; + #endregion - public static string LOADING_INFO__WAIT_FOR_SERVER => Get(LOADING_INFO__WAIT_FOR_SERVER_KEY); - private const string LOADING_INFO__WAIT_FOR_SERVER_KEY = $"{PREFIX_LOADING_INFO}/wait_for_server"; + #region Player List + public static string PLAYER_LIST__TITLE => Get(PLAYER_LIST__TITLE_KEY); + private const string PLAYER_LIST__TITLE_KEY = $"{PREFIX_PLAYER_LIST}/title"; + #endregion - public static string LOADING_INFO__SYNC_WORLD_STATE => Get(LOADING_INFO__SYNC_WORLD_STATE_KEY); - private const string LOADING_INFO__SYNC_WORLD_STATE_KEY = $"{PREFIX_LOADING_INFO}/sync_world_state"; + #region Loading Info + public static string LOADING_INFO__WAIT_FOR_SERVER => Get(LOADING_INFO__WAIT_FOR_SERVER_KEY); + private const string LOADING_INFO__WAIT_FOR_SERVER_KEY = $"{PREFIX_LOADING_INFO}/wait_for_server"; - #endregion + public static string LOADING_INFO__SYNC_WORLD_STATE => Get(LOADING_INFO__SYNC_WORLD_STATE_KEY); + private const string LOADING_INFO__SYNC_WORLD_STATE_KEY = $"{PREFIX_LOADING_INFO}/sync_world_state"; + #endregion - private static bool initializeAttempted; - private static ReadOnlyDictionary> csv; + private static bool initializeAttempted; + private static ReadOnlyDictionary> csv; - public static void Load(string localeDir) - { - initializeAttempted = true; - string path = Path.Combine(localeDir, DEFAULT_LOCALE_FILE); - if (!File.Exists(path)) + public static void Load(string localeDir) { - Multiplayer.LogError($"Failed to find locale file at '{path}'! Please make sure it's there."); - return; + initializeAttempted = true; + string path = Path.Combine(localeDir, DEFAULT_LOCALE_FILE); + if (!File.Exists(path)) + { + Multiplayer.LogError($"Failed to find locale file at '{path}'! Please make sure it's there."); + return; + } + + csv = Csv.Parse(File.ReadAllText(path)); + Multiplayer.LogDebug(() => $"Locale dump: {Csv.Dump(csv)}"); } - csv = Csv.Parse(File.ReadAllText(path)); - Multiplayer.LogDebug(() => $"Locale dump:{Csv.Dump(csv)}"); - } + public static string Get(string key, string overrideLanguage = null) + { + if (!initializeAttempted) + throw new InvalidOperationException("Not initialized"); - public static string Get(string key, string overrideLanguage = null) - { - if (!initializeAttempted) - throw new InvalidOperationException("Not initialized"); + if (csv == null) + return MISSING_TRANSLATION; - if (csv == null) - return MISSING_TRANSLATION; + string locale = overrideLanguage ?? LocalizationManager.CurrentLanguage; + if (!csv.ContainsKey(locale)) + { + if (locale == DEFAULT_LANGUAGE) + { + Multiplayer.LogError($"Failed to find locale language {locale}! Something is broken, this shouldn't happen. Dumping CSV data:"); + Multiplayer.LogError($"\n{Csv.Dump(csv)}"); + return MISSING_TRANSLATION; + } + + locale = DEFAULT_LANGUAGE; + Multiplayer.LogWarning($"Failed to find locale language {locale}"); + } - string locale = overrideLanguage ?? LocalizationManager.CurrentLanguage; - if (!csv.ContainsKey(locale)) - { - if (locale == DEFAULT_LANGUAGE) + Dictionary localeDict = csv[locale]; + string actualKey = key.StartsWith(PREFIX) ? key.Substring(PREFIX.Length) : key; + if (localeDict.TryGetValue(actualKey, out string value)) { - Multiplayer.LogError($"Failed to find locale language {locale}! Something is broken, this shouldn't happen. Dumping CSV data:"); - Multiplayer.LogError($"\n{Csv.Dump(csv)}"); - return MISSING_TRANSLATION; + if (string.IsNullOrEmpty(value)) + return overrideLanguage == null && locale != DEFAULT_LANGUAGE ? Get(actualKey, DEFAULT_LANGUAGE) : MISSING_TRANSLATION; + return value; } - locale = DEFAULT_LANGUAGE; - Multiplayer.LogWarning($"Failed to find locale language {locale}"); + Multiplayer.LogDebug(() => $"Failed to find locale key '{actualKey}'!"); + return MISSING_TRANSLATION; } - Dictionary localeDict = csv[locale]; - string actualKey = key.StartsWith(PREFIX) ? key.Substring(PREFIX.Length) : key; - if (localeDict.TryGetValue(actualKey, out string value)) + public static string Get(string key, params object[] placeholders) { - if (value == string.Empty) - return overrideLanguage == null && locale != DEFAULT_LANGUAGE ? Get(actualKey, DEFAULT_LANGUAGE) : MISSING_TRANSLATION; - return value; + return string.Format(Get(key), placeholders); } - Multiplayer.LogDebug(() => $"Failed to find locale key '{actualKey}'!"); - return MISSING_TRANSLATION; - } - - public static string Get(string key, params object[] placeholders) - { - return string.Format(Get(key), placeholders); - } - - public static string Get(string key, params string[] placeholders) - { - // ReSharper disable once CoVariantArrayConversion - return Get(key, (object[])placeholders); + public static string Get(string key, params string[] placeholders) + { + return Get(key, (object[])placeholders); + } } } diff --git a/Multiplayer/Multiplayer.cs b/Multiplayer/Multiplayer.cs index 87ca8b0..e8a1494 100644 --- a/Multiplayer/Multiplayer.cs +++ b/Multiplayer/Multiplayer.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; using HarmonyLib; using JetBrains.Annotations; @@ -16,7 +16,7 @@ public static class Multiplayer { private const string LOG_FILE = "multiplayer.log"; - private static UnityModManager.ModEntry ModEntry; + public static UnityModManager.ModEntry ModEntry; public static Settings Settings; private static AssetBundle assetBundle; diff --git a/Multiplayer/Multiplayer.csproj b/Multiplayer/Multiplayer.csproj index 70448c4..df191dc 100644 --- a/Multiplayer/Multiplayer.csproj +++ b/Multiplayer/Multiplayer.csproj @@ -1,4 +1,4 @@ - + net48 latest @@ -73,14 +73,12 @@ + - - - diff --git a/Multiplayer/Patches/MainMenu/LocalizationManagerPatch.cs b/Multiplayer/Patches/MainMenu/LocalizationManagerPatch.cs index 0f799cb..317b053 100644 --- a/Multiplayer/Patches/MainMenu/LocalizationManagerPatch.cs +++ b/Multiplayer/Patches/MainMenu/LocalizationManagerPatch.cs @@ -1,20 +1,27 @@ using HarmonyLib; using I2.Loc; -namespace Multiplayer.Patches.MainMenu; - -[HarmonyPatch(typeof(LocalizationManager))] -public static class LocalizationManagerPatch +namespace Multiplayer.Patches.MainMenu { - [HarmonyPrefix] - [HarmonyPatch(nameof(LocalizationManager.TryGetTranslation))] - private static bool TryGetTranslation_Prefix(ref bool __result, string Term, out string Translation) + [HarmonyPatch(typeof(LocalizationManager))] + public static class LocalizationManagerPatch { - Translation = string.Empty; - if (!Term.StartsWith(Locale.PREFIX)) - return true; - Translation = Locale.Get(Term); - __result = Translation == Locale.MISSING_TRANSLATION; - return false; + [HarmonyPrefix] + [HarmonyPatch(nameof(LocalizationManager.TryGetTranslation))] + private static bool TryGetTranslation_Prefix(ref bool __result, string Term, out string Translation) + { + Translation = string.Empty; + + // Check if the term starts with the specified locale prefix + if (!Term.StartsWith(Locale.PREFIX)) + return true; + + // Attempt to get the translation for the term + Translation = Locale.Get(Term); + + // If the translation is missing, set the result to true and skip the original method + __result = Translation == Locale.MISSING_TRANSLATION; + return false; + } } } diff --git a/Multiplayer/Patches/MainMenu/MainMenuControllerPatch.cs b/Multiplayer/Patches/MainMenu/MainMenuControllerPatch.cs index be04935..65f3c3b 100644 --- a/Multiplayer/Patches/MainMenu/MainMenuControllerPatch.cs +++ b/Multiplayer/Patches/MainMenu/MainMenuControllerPatch.cs @@ -1,50 +1,68 @@ -using DV.Localization; +using DV.Localization; using DV.UI; using HarmonyLib; using Multiplayer.Utils; using UnityEngine; using UnityEngine.UI; -namespace Multiplayer.Patches.MainMenu; - -[HarmonyPatch(typeof(MainMenuController), "Awake")] -public static class MainMenuController_Awake_Patch +namespace Multiplayer.Patches.MainMenu { - public static GameObject MultiplayerButton; - - private static void Prefix(MainMenuController __instance) + [HarmonyPatch(typeof(MainMenuController), "Awake")] + public static class MainMenuController_Awake_Patch { - GameObject button = __instance.FindChildByName("ButtonSelectable Sessions"); - if (button == null) + public static GameObject multiplayerButton; + + private static void Prefix(MainMenuController __instance) { - Multiplayer.LogError("Failed to find Sessions button!"); - return; - } + // Find the Sessions button to base the Multiplayer button on + GameObject sessionsButton = __instance.FindChildByName("ButtonSelectable Sessions"); + if (sessionsButton == null) + { + Multiplayer.LogError("Failed to find Sessions button!"); + return; + } + + // Deactivate the sessions button temporarily to duplicate it + sessionsButton.SetActive(false); + multiplayerButton = Object.Instantiate(sessionsButton, sessionsButton.transform.parent); + sessionsButton.SetActive(true); - button.SetActive(false); - MultiplayerButton = Object.Instantiate(button, button.transform.parent); - button.SetActive(true); + // Configure the new Multiplayer button + multiplayerButton.transform.SetSiblingIndex(sessionsButton.transform.GetSiblingIndex() + 1); + multiplayerButton.name = "ButtonSelectable Multiplayer"; - MultiplayerButton.transform.SetSiblingIndex(button.transform.GetSiblingIndex() + 1); - MultiplayerButton.name = "ButtonSelectable Multiplayer"; + // Set the localization key for the new button + Localize localize = multiplayerButton.GetComponentInChildren(); + localize.key = Locale.MAIN_MENU__JOIN_SERVER_KEY; - Localize localize = MultiplayerButton.GetComponentInChildren(); - localize.key = Locale.MAIN_MENU__JOIN_SERVER_KEY; + // Remove existing localization components to reset them + Object.Destroy(multiplayerButton.GetComponentInChildren()); + ResetTooltip(multiplayerButton); - // Reset existing localization components that were added when the Sessions button was initialized. - Object.Destroy(MultiplayerButton.GetComponentInChildren()); - UIElementTooltip tooltip = MultiplayerButton.GetComponent(); - tooltip.disabledKey = null; - tooltip.enabledKey = null; + // Set the icon for the new Multiplayer button + SetButtonIcon(multiplayerButton); - GameObject icon = MultiplayerButton.FindChildByName("icon"); - if (icon == null) + //multiplayerButton.SetActive(true); + } + + private static void ResetTooltip(GameObject button) { - Multiplayer.LogError("Failed to find icon on Sessions button, destroying the Multiplayer button!"); - Object.Destroy(MultiplayerButton); - return; + UIElementTooltip tooltip = button.GetComponent(); + tooltip.disabledKey = null; + tooltip.enabledKey = null; } - icon.GetComponent().sprite = Multiplayer.AssetIndex.multiplayerIcon; + private static void SetButtonIcon(GameObject button) + { + GameObject icon = button.FindChildByName("icon"); + if (icon == null) + { + Multiplayer.LogError("Failed to find icon on Sessions button, destroying the Multiplayer button!"); + Object.Destroy(multiplayerButton); + return; + } + + icon.GetComponent().sprite = Multiplayer.AssetIndex.multiplayerIcon; + } } } diff --git a/Multiplayer/Patches/MainMenu/RightPaneControllerPatch.cs b/Multiplayer/Patches/MainMenu/RightPaneControllerPatch.cs index 33467b4..bf7d62f 100644 --- a/Multiplayer/Patches/MainMenu/RightPaneControllerPatch.cs +++ b/Multiplayer/Patches/MainMenu/RightPaneControllerPatch.cs @@ -1,64 +1,129 @@ -using DV.Localization; +using System.Linq; +using System; +using DV.Common; +using DV.Localization; +using DV.Scenarios.Common; using DV.UI; using DV.UIFramework; using HarmonyLib; using Multiplayer.Components.MainMenu; using Multiplayer.Utils; +using TMPro; using UnityEngine; +using UnityEngine.UI; +using LiteNetLib; -namespace Multiplayer.Patches.MainMenu; - -[HarmonyPatch(typeof(RightPaneController), "OnEnable")] -public static class RightPaneController_OnEnable_Patch +namespace Multiplayer.Patches.MainMenu { - private static void Prefix(RightPaneController __instance) + [HarmonyPatch(typeof(RightPaneController), "OnEnable")] + public static class RightPaneController_OnEnable_Patch { - if (__instance.HasChildWithName("PaneRight Multiplayer")) - return; - GameObject launcher = __instance.FindChildByName("PaneRight Launcher"); - if (launcher == null) + private static void Prefix(RightPaneController __instance) { - Multiplayer.LogError("Failed to find Launcher pane!"); - return; - } + // Check if the multiplayer pane already exists + if (__instance.HasChildWithName("PaneRight Multiplayer")) + return; + + // Find the base pane for Load/Save + GameObject basePane = __instance.FindChildByName("PaneRight Load/Save"); + //GameObject basePane = __instance.FindChildByName("PaneRight Launcher"); + if (basePane == null) + { + Multiplayer.LogError("Failed to find Launcher pane!"); + return; + } + + // Create a new multiplayer pane based on the base pane + basePane.SetActive(false); + GameObject multiplayerPane = GameObject.Instantiate(basePane, basePane.transform.parent); + basePane.SetActive(true); + + multiplayerPane.name = "PaneRight Multiplayer"; - launcher.SetActive(false); - GameObject multiplayerPane = Object.Instantiate(launcher, launcher.transform.parent); - launcher.SetActive(true); + //multiplayerPane.AddComponent(); - multiplayerPane.name = "PaneRight Multiplayer"; - __instance.menuController.controlledMenus.Add(multiplayerPane.GetComponent()); - MainMenuController_Awake_Patch.MultiplayerButton.GetComponent().requestedMenuIndex = __instance.menuController.controlledMenus.Count - 1; + __instance.menuController.controlledMenus.Add(multiplayerPane.GetComponent()); + MainMenuController_Awake_Patch.multiplayerButton.GetComponent().requestedMenuIndex = __instance.menuController.controlledMenus.Count - 1; + Multiplayer.LogError("before Past Destroyed stuff!"); + // Clean up unnecessary components and child objects + GameObject.Destroy(multiplayerPane.GetComponent()); + GameObject.Destroy(multiplayerPane.GetComponent()); + GameObject.Destroy(multiplayerPane.FindChildByName("ButtonIcon OpenFolder")); + GameObject.Destroy(multiplayerPane.FindChildByName("ButtonIcon Rename")); + GameObject.Destroy(multiplayerPane.FindChildByName("Text Content")); + Multiplayer.LogError("Past Destroyed stuff!"); - Object.Destroy(multiplayerPane.GetComponent()); - Object.Destroy(multiplayerPane.FindChildByName("Thumb Background")); - Object.Destroy(multiplayerPane.FindChildByName("Thumbnail")); - Object.Destroy(multiplayerPane.FindChildByName("Savegame Details Background")); - Object.Destroy(multiplayerPane.FindChildByName("ButtonTextIcon Run")); + // Update UI elements + GameObject titleObj = multiplayerPane.FindChildByName("Title"); + titleObj.GetComponentInChildren().key = Locale.SERVER_BROWSER__TITLE_KEY; + GameObject.Destroy(titleObj.GetComponentInChildren()); - GameObject titleObj = multiplayerPane.FindChildByName("Title"); - if (titleObj == null) + GameObject content = multiplayerPane.FindChildByName("text main"); + content.GetComponentInChildren().text = "Server browser not yet implemented."; + + GameObject serverWindow = multiplayerPane.FindChildByName("Save Description"); + serverWindow.GetComponentInChildren().text = "Server information not yet implemented."; + + UpdateButton(multiplayerPane, "ButtonTextIcon Overwrite", "ButtonTextIcon Manual", Locale.SERVER_BROWSER__MANUAL_CONNECT_KEY, null, Multiplayer.AssetIndex.multiplayerIcon); + UpdateButton(multiplayerPane, "ButtonTextIcon Load", "ButtonTextIcon Host", Locale.SERVER_BROWSER__HOST_KEY, null, Multiplayer.AssetIndex.multiplayerIcon); + UpdateButton(multiplayerPane, "ButtonTextIcon Save", "ButtonTextIcon Join", Locale.SERVER_BROWSER__JOIN_KEY, null, null); + UpdateButton(multiplayerPane, "ButtonIcon Delete", "ButtonTextIcon Refresh", Locale.SERVER_BROWSER__REFRESH, null, null); + + multiplayerPane.AddComponent(); + + MainMenuThingsAndStuff.Create(manager => + { + PopupManager popupManager = null; + __instance.FindPopupManager(ref popupManager); + manager.popupManager = popupManager; + manager.renamePopupPrefab = __instance.continueLoadNewController.career.renamePopupPrefab; + manager.okPopupPrefab = __instance.continueLoadNewController.career.okPopupPrefab; + manager.uiMenuController = __instance.menuController; + }); + + MainMenuController_Awake_Patch.multiplayerButton.SetActive(true); + Multiplayer.LogError("At end!"); + } + + private static void UpdateButton(GameObject pane, string oldButtonName, string newButtonName, string localeKey, string toolTipKey, Sprite icon) { - Multiplayer.LogError("Failed to find title object!"); - return; + GameObject button = pane.FindChildByName(oldButtonName); + button.name = newButtonName; + + if (button.GetComponentInChildren() != null) + { + button.GetComponentInChildren().key = localeKey; + GameObject.Destroy(button.GetComponentInChildren()); + ResetTooltip(button); + } + + if (icon != null) + { + SetButtonIcon(button, icon); + } + + button.GetComponentInChildren().ToggleInteractable(true); + + } - titleObj.GetComponentInChildren().key = Locale.SERVER_BROWSER__TITLE_KEY; - Object.Destroy(titleObj.GetComponentInChildren()); + private static void SetButtonIcon(GameObject button, Sprite icon) + { + GameObject goIcon = button.FindChildByName("[icon]"); + if (goIcon == null) + { + Multiplayer.LogError("Failed to find icon!"); + return; + } - multiplayerPane.AddComponent(); + goIcon.GetComponent().sprite = icon; + } - MainMenuThingsAndStuff.Create(manager => + private static void ResetTooltip(GameObject button) { - PopupManager popupManager = null; - __instance.FindPopupManager(ref popupManager); - manager.popupManager = popupManager; - manager.renamePopupPrefab = __instance.continueLoadNewController.career.renamePopupPrefab; - manager.okPopupPrefab = __instance.continueLoadNewController.career.okPopupPrefab; - manager.uiMenuController = __instance.menuController; - }); - - multiplayerPane.SetActive(true); - MainMenuController_Awake_Patch.MultiplayerButton.SetActive(true); + UIElementTooltip tooltip = button.GetComponent(); + tooltip.disabledKey = null; + tooltip.enabledKey = null; + } } } diff --git a/Multiplayer/Settings.cs b/Multiplayer/Settings.cs index c01fe67..4e2087b 100644 --- a/Multiplayer/Settings.cs +++ b/Multiplayer/Settings.cs @@ -1,4 +1,4 @@ -using System; +using System; using Humanizer; using UnityEngine; using UnityModManagerNet; @@ -28,6 +28,16 @@ public class Settings : UnityModManager.ModSettings, IDrawable [Draw("Port", Tooltip = "The port that your server will listen on. You generally don't need to change this.")] public int Port = 7777; + [Space(10)] + [Header("Last Server Connected to by IP")] + [Draw("Last Remote IP", Tooltip = "The IP for the last server connected to by IP.")] + public string LastRemoteIP = ""; + [Draw("Last Remote Port", Tooltip = "The port for the last server connected to by IP.")] + public int LastRemotePort = 7777; + [Draw("Last Remote Password", Tooltip = "The password for the last server connected to by IP.")] + public string LastRemotePassword = ""; + + [Space(10)] [Header("Preferences")] [Draw("Show Name Tags", Tooltip = "Whether to show player names above their heads.")] diff --git a/Multiplayer/Utils/Sprites.cs b/Multiplayer/Utils/Sprites.cs new file mode 100644 index 0000000..dc63f6f --- /dev/null +++ b/Multiplayer/Utils/Sprites.cs @@ -0,0 +1,111 @@ +using System; +using UnityEngine; + + +namespace Multiplayer.Utils +{ + public static class Sprites + { + private static UnityEngine.Sprite _padlock = null; + + private const int textureWidth = 50; + private const int textureHeight = 50; + + public static UnityEngine.Sprite Padlock + { + get + { + if (_padlock == null) + { + _padlock = DrawPadlock(); + } + return _padlock; + } + } + + private static UnityEngine.Sprite DrawPadlock() + { + Texture2D texture = new Texture2D(2, 2, TextureFormat.DXT5, false); ;//, TextureFormat.BGRA32, false);// (textureWidth, textureHeight); + + Debug.Log($"loading from {System.IO.Path.Combine(Multiplayer.ModEntry.Path, "lock.png")}"); + // Load the PNG file from the specified file path + byte[] fileData = System.IO.File.ReadAllBytes(System.IO.Path.Combine(Multiplayer.ModEntry.Path, "lock.png")); + + ImageConversion.LoadImage(texture, fileData); + //texture.LoadRawTextureData(pngBytes); // Load the PNG data into the texture + + //int border = 5; + + + //Color padlockColor = Color.white; + //Color transparentColor = new Color(0, 0, 0, 0); // Fully transparent + + //// Clear the texture with the transparent color + //for (int y = 0; y < textureHeight; y++) + //{ + // for (int x = 0; x < textureWidth; x++) + // { + // texture.SetPixel(x, y, transparentColor); + // } + //} + + //// Draw the padlock body (rectangle) + //int bodyWidth = (textureWidth - 2 * border)/2; + //int bodyHeight = (textureHeight - 2 * border) / 3; // Adjusting body height + //int bodyX = border; + //int bodyY = border; + + //for (int y = bodyY; y < bodyY + bodyHeight; y++) + //{ + // for (int x = bodyX; x < bodyX + bodyWidth; x++) + // { + // texture.SetPixel(x, y, padlockColor); + // } + //} + + ////Draw shanks + //int shankThickness = 6; + //int shankOffset = 2; + //int shankHeight = bodyHeight * 2/3; + + //for (int y = bodyHeight+border; y < bodyHeight+border+shankHeight; y++) + //{ + // for (int x = 0; x < shankThickness; x++) + // { + // texture.SetPixel(border + shankOffset + x, y, padlockColor); + // texture.SetPixel(textureWidth-( bodyWidth + border + shankOffset + x) , y, padlockColor); + // } + //} + + //// Draw the padlock shackle (semi-circle) + //int shackleRadius = (bodyWidth - 2* shankOffset)/ 2; + //int shackleCenterX = textureWidth / 2; + //int shackleCenterY = bodyHeight + border + shankHeight; //bodyY + bodyHeight; + + //// Adjust the length of the straight part of the shackle + //int shackleStraightLength = bodyHeight / 2; + + //// Adjust the thickness of the shackle + //int shackleThickness = 1; // Set the shackle thickness to 1 pixel + + //for (int y = shackleCenterY - shackleRadius; y <= shackleCenterY; y++) + //{ + // for (int x = shackleCenterX - shackleRadius; x <= shackleCenterX + shackleRadius; x++) + // { + // float distanceToCenter = Mathf.Sqrt((x - shackleCenterX) * (x - shackleCenterX) + (y - shackleCenterY) * (y - shackleCenterY)); + + // // Check if the current pixel is within the semicircle and thickness + // if (distanceToCenter <= shackleRadius && distanceToCenter >= shackleRadius - shankThickness && y >= shackleCenterY) + // { + // texture.SetPixel(x, y, padlockColor); + // } + // } + //} + + //texture.Apply(); + + return Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), new Vector2(0.5f, 0.5f)); + } + + } +} diff --git a/Sprites/lock.png b/Sprites/lock.png new file mode 100644 index 0000000000000000000000000000000000000000..dd86c0f0d900289499ceafefbb97a1890580efb8 GIT binary patch literal 327 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nC-#^NA%Cx&(BWL^R}Ea{HEjtmSN z`?>!lvI6;>1s;*b3=Co_Aj~*h^q2@x@Q$a8V@QVc+iQXR%?dm$33vYgKkp~xF^yGc z%k{UL?tWjyA5?wHs@tP)vFyTg-qx~KdT&M8!Y*X}<5P-Q;jrZAL=T1UP9+`Zq>pXY z&~|ywxp{Ab>5(-vU;LW6wmW=QL@q<*>L%8kJ6~#toPEk|3{-imzpSLYZH0j8A;*Z8 zCWUz^;%k?hA98cN@yAD?IOxvb$l6J!SGR1vp}`^TyH<3u=i{~Cx2`nZz47XV>2=CE zA}zjA6K Date: Tue, 18 Jun 2024 19:54:47 +0930 Subject: [PATCH 07/34] Minor adjustments and commenting --- .../MainMenu/MainMenuThingsAndStuff.cs | 145 +++++++------ .../Components/MainMenu/MultiplayerPane.cs | 177 ++++------------ .../MainMenu/ServerBrowserElement.cs | 138 ++++++------ .../MainMenu/LocalizationManagerPatch.cs | 8 + .../MainMenu/MainMenuControllerPatch.cs | 15 ++ .../MainMenu/RightPaneControllerPatch.cs | 22 +- Multiplayer/Utils/Csv.cs | 198 ++++++++++-------- 7 files changed, 330 insertions(+), 373 deletions(-) diff --git a/Multiplayer/Components/MainMenu/MainMenuThingsAndStuff.cs b/Multiplayer/Components/MainMenu/MainMenuThingsAndStuff.cs index 9920071..b081b36 100644 --- a/Multiplayer/Components/MainMenu/MainMenuThingsAndStuff.cs +++ b/Multiplayer/Components/MainMenu/MainMenuThingsAndStuff.cs @@ -4,91 +4,108 @@ using JetBrains.Annotations; using UnityEngine; -namespace Multiplayer.Components.MainMenu; - -public class MainMenuThingsAndStuff : SingletonBehaviour +namespace Multiplayer.Components.MainMenu { - public PopupManager popupManager; - public Popup renamePopupPrefab; - public Popup okPopupPrefab; - public UIMenuController uiMenuController; - - protected override void Awake() + public class MainMenuThingsAndStuff : SingletonBehaviour { - bool shouldDestroy = false; + public PopupManager popupManager; + public Popup renamePopupPrefab; + public Popup okPopupPrefab; + public UIMenuController uiMenuController; - if (popupManager == null) + protected override void Awake() { - Multiplayer.LogError("Failed to find PopupManager! Destroying self."); - shouldDestroy = true; + bool shouldDestroy = false; + + // Check if PopupManager is assigned + if (popupManager == null) + { + Multiplayer.LogError("Failed to find PopupManager! Destroying self."); + shouldDestroy = true; + } + + // Check if renamePopupPrefab is assigned + if (renamePopupPrefab == null) + { + Multiplayer.LogError($"{nameof(renamePopupPrefab)} is null! Destroying self."); + shouldDestroy = true; + } + + // Check if okPopupPrefab is assigned + if (okPopupPrefab == null) + { + Multiplayer.LogError($"{nameof(okPopupPrefab)} is null! Destroying self."); + shouldDestroy = true; + } + + // Check if uiMenuController is assigned + if (uiMenuController == null) + { + Multiplayer.LogError($"{nameof(uiMenuController)} is null! Destroying self."); + shouldDestroy = true; + } + + // If all required components are assigned, call base.Awake(), otherwise destroy self + if (!shouldDestroy) + { + base.Awake(); + return; + } + + Destroy(this); } - if (renamePopupPrefab == null) + // Switch to the default menu + public void SwitchToDefaultMenu() { - Multiplayer.LogError($"{nameof(renamePopupPrefab)} is null! Destroying self."); - shouldDestroy = true; + uiMenuController.SwitchMenu(uiMenuController.defaultMenuIndex); } - if (okPopupPrefab == null) + // Switch to a specific menu by index + public void SwitchToMenu(byte index) { - Multiplayer.LogError($"{nameof(okPopupPrefab)} is null! Destroying self."); - shouldDestroy = true; + uiMenuController.SwitchMenu(index); } - if (uiMenuController == null) + // Show the rename popup if possible + [CanBeNull] + public Popup ShowRenamePopup() { - Multiplayer.LogError($"{nameof(uiMenuController)} is null! Destroying self."); - shouldDestroy = true; + Debug.Log("public Popup ShowRenamePopup() ..."); + return ShowPopup(renamePopupPrefab); } - if (!shouldDestroy) + // Show the OK popup if possible + [CanBeNull] + public Popup ShowOkPopup() { - base.Awake(); - return; + return ShowPopup(okPopupPrefab); } - Destroy(this); - } - - public void SwitchToDefaultMenu() - { - uiMenuController.SwitchMenu(uiMenuController.defaultMenuIndex); - } - - public void SwitchToMenu(byte index) - { - uiMenuController.SwitchMenu(index); - } + // Generic method to show a popup if the PopupManager can show it + [CanBeNull] + private Popup ShowPopup(Popup popup) + { + if (popupManager.CanShowPopup()) + return popupManager.ShowPopup(popup); - [CanBeNull] - public Popup ShowRenamePopup() - { - Debug.Log("public Popup ShowRenamePopup() ..."); - return ShowPopup(renamePopupPrefab); - } + Multiplayer.LogError($"{nameof(PopupManager)} cannot show popup!"); + return null; + } - [CanBeNull] - public Popup ShowOkPopup() - { - return ShowPopup(okPopupPrefab); - } + /// A function to apply to the MainMenuPopupManager while the object is disabled + public static void Create(Action func) + { + // Create a new GameObject for MainMenuThingsAndStuff and disable it + GameObject go = new($"[{nameof(MainMenuThingsAndStuff)}]"); + go.SetActive(false); - [CanBeNull] - private Popup ShowPopup(Popup popup) - { - if (popupManager.CanShowPopup()) - return popupManager.ShowPopup(popup); - Multiplayer.LogError($"{nameof(PopupManager)} cannot show popup!"); - return null; - } + // Add MainMenuThingsAndStuff component and apply the provided function + MainMenuThingsAndStuff manager = go.AddComponent(); + func.Invoke(manager); - /// A function to apply to the MainMenuPopupManager while the object is disabled - public static void Create(Action func) - { - GameObject go = new($"[{nameof(MainMenuThingsAndStuff)}]"); - go.SetActive(false); - MainMenuThingsAndStuff manager = go.AddComponent(); - func.Invoke(manager); - go.SetActive(true); + // Re-enable the GameObject + go.SetActive(true); + } } } diff --git a/Multiplayer/Components/MainMenu/MultiplayerPane.cs b/Multiplayer/Components/MainMenu/MultiplayerPane.cs index b7348c5..75ed863 100644 --- a/Multiplayer/Components/MainMenu/MultiplayerPane.cs +++ b/Multiplayer/Components/MainMenu/MultiplayerPane.cs @@ -11,6 +11,8 @@ using Multiplayer.Utils; using TMPro; using UnityEngine; +using UnityEngine.Events; +using UnityEngine.UI; namespace Multiplayer.Components.MainMenu { @@ -27,8 +29,11 @@ public class MultiplayerPane : MonoBehaviour private ObservableCollectionExt gridViewModel = new ObservableCollectionExt(); private ServerBrowserGridView gridView; + private ScrollRect parentScroller; + private int indexToSelectOnRefresh; private string[] testNames = new string[] { "ChooChooExpress", "RailwayRascals", "FreightFrenzy", "SteamDream", "DieselDynasty", "CargoKings", "TrackMasters", "RailwayRevolution", "ExpressElders", "IronHorseHeroes", "LocomotiveLegends", "TrainTitans", "HeavyHaulers", "RapidRails", "TimberlineTransport", "CoalCountry", "SilverRailway", "GoldenGauge", "SteelStream", "MountainMoguls", "RailRiders", "TrackTrailblazers", "FreightFanatics", "SteamSensation", "DieselDaredevils", "CargoChampions", "TrackTacticians", "RailwayRoyals", "ExpressExperts", "IronHorseInnovators", "LocomotiveLeaders", "TrainTacticians", "HeavyHitters", "RapidRunners", "TimberlineTrains", "CoalCrushers", "SilverStreamliners", "GoldenGears", "SteelSurge", "MountainMovers", "RailwayWarriors", "TrackTerminators", "FreightFighters", "SteamStreak", "DieselDynamos", "CargoCommanders", "TrackTrailblazers", "RailwayRangers", "ExpressEngineers", "IronHorseInnovators", "LocomotiveLovers", "TrainTrailblazers", "HeavyHaulersHub", "RapidRailsRacers", "TimberlineTrackers", "CoalCountryCarriers", "SilverSpeedsters", "GoldenGaugeGang", "SteelStalwarts", "MountainMoversClub", "RailRunners", "TrackTitans", "FreightFalcons", "SteamSprinters", "DieselDukes", "CargoCommandos", "TrackTracers", "RailwayRebels", "ExpressElite", "IronHorseIcons", "LocomotiveLunatics", "TrainTornadoes", "HeavyHaulersCrew", "RapidRailsRunners", "TimberlineTrackMasters", "CoalCountryCrew", "SilverSprinters", "GoldenGale", "SteelSpeedsters", "MountainMarauders", "RailwayRiders", "TrackTactics", "FreightFury", "SteamSquires", "DieselDefenders", "CargoCrusaders", "TrackTechnicians", "RailwayRaiders", "ExpressEnthusiasts", "IronHorseIlluminati", "LocomotiveLoyalists", "TrainTurbulence", "HeavyHaulersHeroes", "RapidRailsRiders", "TimberlineTrackTitans", "CoalCountryCaravans", "SilverSpeedRacers", "GoldenGaugeGangsters", "SteelStorm", "MountainMasters", "RailwayRoadrunners", "TrackTerror", "FreightFleets", "SteamSurgeons", "DieselDragons", "CargoCrushers", "TrackTaskmasters", "RailwayRevolutionaries", "ExpressExplorers", "IronHorseInquisitors", "LocomotiveLegion", "TrainTriumph", "HeavyHaulersHorde", "RapidRailsRenegades", "TimberlineTrackTeam", "CoalCountryCrusade", "SilverSprintersSquad", "GoldenGaugeGroup", "SteelStrike", "MountainMonarchs", "RailwayRaid", "TrackTacticiansTeam", "FreightForce", "SteamSquad", "DieselDynastyClan", "CargoCrew", "TrackTeam", "RailwayRalliers", "ExpressExpedition", "IronHorseInitiative", "LocomotiveLeague", "TrainTribe", "HeavyHaulersHustle", "RapidRailsRevolution", "TimberlineTrackersTeam", "CoalCountryConvoy", "SilverSprint", "GoldenGaugeGuild", "SteelSpirits", "MountainMayhem", "RailwayRaidersCrew", "TrackTrailblazersTribe", "FreightFleetForce", "SteamStalwarts", "DieselDragonsDen", "CargoCaptains", "TrackTrailblazersTeam", "RailwayRidersRevolution", "ExpressEliteExpedition", "IronHorseInsiders", "LocomotiveLords", "TrainTacticiansTribe", "HeavyHaulersHeroesHorde", "RapidRailsRacersTeam", "TimberlineTrackMastersTeam", "CoalCountryCarriersCrew", "SilverSpeedstersSprint", "GoldenGaugeGangGuild", "SteelSurgeStrike", "MountainMoversMonarchs" }; + private void Awake() { Multiplayer.Log("MultiplayerPane Awake()"); @@ -36,6 +41,23 @@ private void Awake() SetupServerBrowser(); } + private void OnEnable() + { + if (!this.parentScroller) + { + this.parentScroller = this.gridView.GetComponentInParent(); + } + this.SetupListeners(true); + this.indexToSelectOnRefresh = 0; + this.RefreshData(); + } + + // Token: 0x060001C2 RID: 450 RVA: 0x00007D0C File Offset: 0x00005F0C + private void OnDisable() + { + this.SetupListeners(false); + } + private void SetupMultiplayerButtons() { GameObject buttonDirectIP = GameObject.Find("ButtonTextIcon Manual"); @@ -72,7 +94,7 @@ private void SetupMultiplayerButtons() //buttonRefreshDV.onClick.AddListener(RefreshAction); //Debug.Log("Setting buttons active: " + buttonDirectIP.name + ", " + buttonHost.name + ", " + buttonJoin.name + ", " + buttonRefresh.name ); - Debug.Log("Setting buttons active: " + buttonDirectIP.name + ", " + buttonHost.name + ", " + buttonJoin.name ); + Debug.Log("Setting buttons active: " + buttonDirectIP.name + ", " + buttonHost.name + ", " + buttonJoin.name); buttonDirectIP.SetActive(true); buttonHost.SetActive(true); buttonJoin.SetActive(true); @@ -107,56 +129,6 @@ private void ModifyButton(GameObject button, string key) } - private void ShowIpPopup() - { - - // Set up event listeners and localization for Host button - ButtonDV buttonHostDV = buttonHost.GetComponent(); - buttonHostDV.onClick.AddListener(HostAction); - - // Set up event listeners and localization for Join button - ButtonDV buttonJoinDV = buttonJoin.GetComponent(); - buttonJoinDV.onClick.AddListener(JoinAction); - - // Set up event listeners and localization for Refresh button - //ButtonDV buttonRefreshDV = buttonRefresh.GetComponent(); - //buttonRefreshDV.onClick.AddListener(RefreshAction); - - //Debug.Log("Setting buttons active: " + buttonDirectIP.name + ", " + buttonHost.name + ", " + buttonJoin.name + ", " + buttonRefresh.name ); - Debug.Log("Setting buttons active: " + buttonDirectIP.name + ", " + buttonHost.name + ", " + buttonJoin.name ); - buttonDirectIP.SetActive(true); - buttonHost.SetActive(true); - buttonJoin.SetActive(true); - //buttonRefresh.SetActive(true); - } - - private void SetupServerBrowser() - { - - GameObject GridviewGO = this.FindChildByName("GRID VIEW"); - SaveLoadGridView slgv = GridviewGO.GetComponent(); - GridviewGO.SetActive(false); - - gridView = GridviewGO.AddComponent(); - gridView.dummyElementPrefab = Instantiate(slgv.viewElementPrefab); - gridView.dummyElementPrefab.name = "prefabServerBrowser"; - GameObject.Destroy(slgv); - GridviewGO.SetActive(true); - - } - - private GameObject FindButton(string name) - { - - return GameObject.Find(name); - } - - private void ModifyButton(GameObject button, string key) - { - button.GetComponentInChildren().key = key; - - } - private void ShowIpPopup() { Debug.Log("In ShowIpPpopup"); @@ -218,20 +190,6 @@ private void ShowPortPopup() }; } - popup.labelTMPro.text = Locale.SERVER_BROWSER__PORT; - popup.GetComponentInChildren().text = Multiplayer.Settings.LastRemotePort.ToString(); - - popup.Closed += result => - { - if (result.closedBy == PopupClosedByAction.Abortion) - { - MainMenuThingsAndStuff.Instance.SwitchToDefaultMenu(); - return; - } - - HandlePortInput(result.data); - }; - } private void HandlePortInput(string input) { if (!PortRegex.IsMatch(input)) @@ -263,53 +221,6 @@ private void ShowPasswordPopup() { if (result.closedBy == PopupClosedByAction.Abortion) return; - SingletonBehaviour.Instance.StartClient(ipAddress, portNumber, result.data); - - Multiplayer.Settings.LastRemoteIP = ipAddress; - Multiplayer.Settings.LastRemotePort = portNumber; - Multiplayer.Settings.LastRemotePassword = result.data; - - //ShowConnectingPopup(); // Show a connecting message - //SingletonBehaviour.Instance.ConnectionFailed += HandleConnectionFailed; - //SingletonBehaviour.Instance.ConnectionEstablished += HandleConnectionEstablished; - }; - } - - // Example of handling connection success - private void HandleConnectionEstablished() - { - // Connection established, handle the UI or game state accordingly - Debug.Log("Connection established!"); - // HideConnectingPopup(); // Hide the connecting message - } - - // Example of handling connection failure - private void HandleConnectionFailed() - { - // Connection failed, show an error message or handle the failure scenario - Debug.LogError("Connection failed!"); - // ShowConnectionFailedPopup(); - } - - private void RefreshAction() - { - // Implement refresh action logic here - Debug.Log("Refresh button clicked."); - // Add your code to refresh the multiplayer list or perform any other refresh-related action - } - - - private static void ShowOkPopup(string text, Action onClick) - { - var popup = MainMenuThingsAndStuff.Instance.ShowOkPopup(); - if (popup == null) return; - - popup.labelTMPro.text = text; - popup.Closed += _ => onClick(); - } - - private void SetButtonsActive(params GameObject[] buttons) - { //directButton.enabled = false; SingletonBehaviour.Instance.StartClient(ipAddress, portNumber, result.data); @@ -373,14 +284,15 @@ private void HostAction() //gridView.showDummyElement = true; gridViewModel.Clear(); - + IServerBrowserGameDetails item = null; - for (int i = 0; i < UnityEngine.Random.Range(1, 50); i++) { + for (int i = 0; i < UnityEngine.Random.Range(1, 50); i++) + { item = new ServerData(); - item.Name = testNames[UnityEngine.Random.Range(0, testNames.Length-1)]; + item.Name = testNames[UnityEngine.Random.Range(0, testNames.Length - 1)]; item.MaxPlayers = UnityEngine.Random.Range(1, 10); item.CurrentPlayers = UnityEngine.Random.Range(1, item.MaxPlayers); item.Ping = UnityEngine.Random.Range(5, 1500); @@ -400,6 +312,18 @@ private void JoinAction() Debug.Log("Join button clicked."); // Add your code to handle joining a game } + private void SetupListeners(bool on) + { + if (on) + { + return; + } + + } + private void RefreshData() + { + + } } public class ServerData : IServerBrowserGameDetails @@ -411,27 +335,6 @@ public class ServerData : IServerBrowserGameDetails public int Ping { get; set; } public bool HasPassword { get; set; } - public void Dispose() {} - - private void HostAction() - { - // Implement host action logic here - Debug.Log("Host button clicked."); - // Add your code to handle hosting a game - gridView.showDummyElement = true; - gridViewModel.Clear(); - //gridView.dummyElementPrefab = ; - - Debug.Log($"gridViewPrefab exists : {gridView.dummyElementPrefab != null} showDummyElement : {gridView.showDummyElement}"); - gridView.SetModel(gridViewModel); - - } - - private void JoinAction() - { - // Implement join action logic here - Debug.Log("Join button clicked."); - // Add your code to handle joining a game - } + public void Dispose() { } } } diff --git a/Multiplayer/Components/MainMenu/ServerBrowserElement.cs b/Multiplayer/Components/MainMenu/ServerBrowserElement.cs index 318aa6c..6f6be23 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowserElement.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowserElement.cs @@ -4,93 +4,79 @@ using TMPro; using UnityEngine; using UnityEngine.UI; -using DV.Common; -using DV.Localization; -using DV.UIFramework; -using Multiplayer.Utils; -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using TMPro; -using UnityEngine; - -namespace Multiplayer.Components.MainMenu; -public class ServerBrowserElement : AViewElement +namespace Multiplayer.Components.MainMenu { - private TextMeshProUGUI networkName; - private TextMeshProUGUI playerCount; - private TextMeshProUGUI ping; - private GameObject goIcon; - private Image icon; - private IServerBrowserGameDetails data; - - private const int PING_WIDTH = 62 * 2; - private const int PING_POS_X = 650; - private IServerBrowserGameDetails data; - - private void Awake() + public class ServerBrowserElement : AViewElement { - //Find existing fields to duplicate - networkName = this.FindChildByName("name [noloc]").GetComponent(); - playerCount = this.FindChildByName("date [noloc]").GetComponent(); - ping = this.FindChildByName("time [noloc]").GetComponent(); - goIcon = this.FindChildByName("autosave icon"); - icon = goIcon.GetComponent(); - - //Fix alignment - Vector3 namePos = networkName.transform.position; - Vector2 nameSize = networkName.rectTransform.sizeDelta; - - playerCount.transform.position = new Vector3(namePos.x + nameSize.x, namePos.y, namePos.z); - - - Vector2 rowSize = this.transform.GetComponentInParent().sizeDelta; - Vector3 pingPos = ping.transform.position; - Vector2 pingSize = ping.rectTransform.sizeDelta; + private TextMeshProUGUI networkName; + private TextMeshProUGUI playerCount; + private TextMeshProUGUI ping; + private GameObject goIcon; + private Image icon; + private IServerBrowserGameDetails data; + private const int PING_WIDTH = 124; // Adjusted width for the ping text + private const int PING_POS_X = 650; // X position for the ping text - ping.rectTransform.sizeDelta = new Vector2(PING_WIDTH, pingSize.y); - pingSize = ping.rectTransform.sizeDelta; - - ping.transform.position = new Vector3(PING_POS_X, pingPos.y, pingPos.z); - - ping.alignment = TextAlignmentOptions.Right; - - //Update clock Icon - icon.sprite = Sprites.Padlock; - - networkName.text = "Test Network"; - playerCount.text = "1/4"; - ping.text = "102"; - } - - public override void SetData(IServerBrowserGameDetails data, AGridView _) - { - if (this.data != null) + private void Awake() { - this.data = null; + // Find and assign TextMeshProUGUI components for displaying server details + networkName = this.FindChildByName("name [noloc]").GetComponent(); + playerCount = this.FindChildByName("date [noloc]").GetComponent(); + ping = this.FindChildByName("time [noloc]").GetComponent(); + goIcon = this.FindChildByName("autosave icon"); + icon = goIcon.GetComponent(); + + // Fix alignment of the player count text relative to the network name text + Vector3 namePos = networkName.transform.position; + Vector2 nameSize = networkName.rectTransform.sizeDelta; + playerCount.transform.position = new Vector3(namePos.x + nameSize.x, namePos.y, namePos.z); + + // Adjust the size and position of the ping text + Vector2 rowSize = this.transform.GetComponentInParent().sizeDelta; + Vector3 pingPos = ping.transform.position; + Vector2 pingSize = ping.rectTransform.sizeDelta; + + ping.rectTransform.sizeDelta = new Vector2(PING_WIDTH, pingSize.y); + ping.transform.position = new Vector3(PING_POS_X, pingPos.y, pingPos.z); + ping.alignment = TextAlignmentOptions.Right; + + // Set initial icon and text values for testing purposes + icon.sprite = Sprites.Padlock; + networkName.text = "Test Network"; + playerCount.text = "1/4"; + ping.text = "102 ms"; } - if (data != null) + + public override void SetData(IServerBrowserGameDetails data, AGridView _) { - this.data = data; + // Clear existing data + if (this.data != null) + { + this.data = null; + } + // Set new data + if (data != null) + { + this.data = data; + } + // Update the view with the new data + UpdateView(); } - UpdateView(null, null); - } - private void UpdateView(object sender = null, PropertyChangedEventArgs e = null) - { - networkName.text = data.Name; - playerCount.text = $"{data.CurrentPlayers} / {data.MaxPlayers}"; - ping.text = $"{data.Ping} ms"; - - if (!data.HasPassword) + private void UpdateView(object sender = null, PropertyChangedEventArgs e = null) { - goIcon.SetActive(false); + // Update the text fields with the data from the server + networkName.text = data.Name; + playerCount.text = $"{data.CurrentPlayers} / {data.MaxPlayers}"; + ping.text = $"{data.Ping} ms"; + + // Hide the icon if the server does not have a password + if (!data.HasPassword) + { + goIcon.SetActive(false); + } } } - } diff --git a/Multiplayer/Patches/MainMenu/LocalizationManagerPatch.cs b/Multiplayer/Patches/MainMenu/LocalizationManagerPatch.cs index 317b053..7fd486f 100644 --- a/Multiplayer/Patches/MainMenu/LocalizationManagerPatch.cs +++ b/Multiplayer/Patches/MainMenu/LocalizationManagerPatch.cs @@ -6,6 +6,13 @@ namespace Multiplayer.Patches.MainMenu [HarmonyPatch(typeof(LocalizationManager))] public static class LocalizationManagerPatch { + /// + /// Harmony prefix patch for LocalizationManager.TryGetTranslation. + /// + /// The result to be set by the prefix method. + /// The localization term to be translated. + /// The translated text to be set by the prefix method. + /// False if the custom translation logic handles the term, otherwise true to continue to the original method. [HarmonyPrefix] [HarmonyPatch(nameof(LocalizationManager.TryGetTranslation))] private static bool TryGetTranslation_Prefix(ref bool __result, string Term, out string Translation) @@ -25,3 +32,4 @@ private static bool TryGetTranslation_Prefix(ref bool __result, string Term, out } } } + diff --git a/Multiplayer/Patches/MainMenu/MainMenuControllerPatch.cs b/Multiplayer/Patches/MainMenu/MainMenuControllerPatch.cs index 9fc2e6d..992959b 100644 --- a/Multiplayer/Patches/MainMenu/MainMenuControllerPatch.cs +++ b/Multiplayer/Patches/MainMenu/MainMenuControllerPatch.cs @@ -7,11 +7,18 @@ namespace Multiplayer.Patches.MainMenu { + /// + /// Harmony patch for the Awake method of MainMenuController to add a Multiplayer button. + /// [HarmonyPatch(typeof(MainMenuController), "Awake")] public static class MainMenuController_Awake_Patch { public static GameObject multiplayerButton; + /// + /// Prefix method to run before MainMenuController's Awake method. + /// + /// The instance of MainMenuController. private static void Prefix(MainMenuController __instance) { // Find the Sessions button to base the Multiplayer button on @@ -43,6 +50,10 @@ private static void Prefix(MainMenuController __instance) SetButtonIcon(multiplayerButton); } + /// + /// Resets the tooltip for a given button. + /// + /// The button to reset the tooltip for. private static void ResetTooltip(GameObject button) { UIElementTooltip tooltip = button.GetComponent(); @@ -50,6 +61,10 @@ private static void ResetTooltip(GameObject button) tooltip.enabledKey = null; } + /// + /// Sets the icon for the Multiplayer button. + /// + /// The button to set the icon for. private static void SetButtonIcon(GameObject button) { GameObject icon = button.FindChildByName("icon"); diff --git a/Multiplayer/Patches/MainMenu/RightPaneControllerPatch.cs b/Multiplayer/Patches/MainMenu/RightPaneControllerPatch.cs index bf7d62f..3813179 100644 --- a/Multiplayer/Patches/MainMenu/RightPaneControllerPatch.cs +++ b/Multiplayer/Patches/MainMenu/RightPaneControllerPatch.cs @@ -26,7 +26,6 @@ private static void Prefix(RightPaneController __instance) // Find the base pane for Load/Save GameObject basePane = __instance.FindChildByName("PaneRight Load/Save"); - //GameObject basePane = __instance.FindChildByName("PaneRight Launcher"); if (basePane == null) { Multiplayer.LogError("Failed to find Launcher pane!"); @@ -37,21 +36,18 @@ private static void Prefix(RightPaneController __instance) basePane.SetActive(false); GameObject multiplayerPane = GameObject.Instantiate(basePane, basePane.transform.parent); basePane.SetActive(true); - multiplayerPane.name = "PaneRight Multiplayer"; - //multiplayerPane.AddComponent(); - + // Add the multiplayer pane to the menu controller __instance.menuController.controlledMenus.Add(multiplayerPane.GetComponent()); MainMenuController_Awake_Patch.multiplayerButton.GetComponent().requestedMenuIndex = __instance.menuController.controlledMenus.Count - 1; - Multiplayer.LogError("before Past Destroyed stuff!"); + // Clean up unnecessary components and child objects GameObject.Destroy(multiplayerPane.GetComponent()); GameObject.Destroy(multiplayerPane.GetComponent()); GameObject.Destroy(multiplayerPane.FindChildByName("ButtonIcon OpenFolder")); GameObject.Destroy(multiplayerPane.FindChildByName("ButtonIcon Rename")); GameObject.Destroy(multiplayerPane.FindChildByName("Text Content")); - Multiplayer.LogError("Past Destroyed stuff!"); // Update UI elements GameObject titleObj = multiplayerPane.FindChildByName("Title"); @@ -64,13 +60,16 @@ private static void Prefix(RightPaneController __instance) GameObject serverWindow = multiplayerPane.FindChildByName("Save Description"); serverWindow.GetComponentInChildren().text = "Server information not yet implemented."; + // Update buttons on the multiplayer pane UpdateButton(multiplayerPane, "ButtonTextIcon Overwrite", "ButtonTextIcon Manual", Locale.SERVER_BROWSER__MANUAL_CONNECT_KEY, null, Multiplayer.AssetIndex.multiplayerIcon); UpdateButton(multiplayerPane, "ButtonTextIcon Load", "ButtonTextIcon Host", Locale.SERVER_BROWSER__HOST_KEY, null, Multiplayer.AssetIndex.multiplayerIcon); UpdateButton(multiplayerPane, "ButtonTextIcon Save", "ButtonTextIcon Join", Locale.SERVER_BROWSER__JOIN_KEY, null, null); UpdateButton(multiplayerPane, "ButtonIcon Delete", "ButtonTextIcon Refresh", Locale.SERVER_BROWSER__REFRESH, null, null); - + + // Add the MultiplayerPane component multiplayerPane.AddComponent(); + // Create and initialize MainMenuThingsAndStuff MainMenuThingsAndStuff.Create(manager => { PopupManager popupManager = null; @@ -81,15 +80,18 @@ private static void Prefix(RightPaneController __instance) manager.uiMenuController = __instance.menuController; }); + // Activate the multiplayer button MainMenuController_Awake_Patch.multiplayerButton.SetActive(true); Multiplayer.LogError("At end!"); } private static void UpdateButton(GameObject pane, string oldButtonName, string newButtonName, string localeKey, string toolTipKey, Sprite icon) { + // Find and rename the button GameObject button = pane.FindChildByName(oldButtonName); button.name = newButtonName; + // Update localization and tooltip if (button.GetComponentInChildren() != null) { button.GetComponentInChildren().key = localeKey; @@ -97,18 +99,19 @@ private static void UpdateButton(GameObject pane, string oldButtonName, string n ResetTooltip(button); } + // Set the button icon if provided if (icon != null) { SetButtonIcon(button, icon); } + // Enable button interaction button.GetComponentInChildren().ToggleInteractable(true); - - } private static void SetButtonIcon(GameObject button, Sprite icon) { + // Find and set the icon for the button GameObject goIcon = button.FindChildByName("[icon]"); if (goIcon == null) { @@ -121,6 +124,7 @@ private static void SetButtonIcon(GameObject button, Sprite icon) private static void ResetTooltip(GameObject button) { + // Reset the tooltip keys for the button UIElementTooltip tooltip = button.GetComponent(); tooltip.disabledKey = null; tooltip.enabledKey = null; diff --git a/Multiplayer/Utils/Csv.cs b/Multiplayer/Utils/Csv.cs index 560fb24..a58ceb0 100644 --- a/Multiplayer/Utils/Csv.cs +++ b/Multiplayer/Utils/Csv.cs @@ -5,124 +5,148 @@ using System.Linq; using System.Text; -namespace Multiplayer.Utils; - -public static class Csv +namespace Multiplayer.Utils { - /// - /// Parses a CSV string into a dictionary of columns, each of which is a dictionary of rows, keyed by the first column. - /// - public static ReadOnlyDictionary> Parse(string data) + public static class Csv { - string[] lines = data.Split('\n'); + /// + /// Parses a CSV string into a dictionary of columns, each of which is a dictionary of rows, keyed by the first column. + /// + /// The CSV data as a string. + /// A read-only dictionary where each key is a column name and the value is a dictionary of rows. + public static ReadOnlyDictionary> Parse(string data) + { + // Split the input data into lines + string[] lines = data.Split('\n'); - // Dictionary> - OrderedDictionary columns = new(lines.Length - 1); + // Initialize an ordered dictionary to maintain the column order + OrderedDictionary columns = new(lines.Length - 1); - List keys = ParseLine(lines[0]); - foreach (string key in keys) - columns.Add(key, new Dictionary()); + // Parse the first line to get the column headers + List keys = ParseLine(lines[0]); + foreach (string key in keys) + columns.Add(key, new Dictionary()); - for (int i = 1; i < lines.Length; i++) - { - string line = lines[i]; - List values = ParseLine(line); - if (values.Count == 0 || string.IsNullOrWhiteSpace(values[0])) - continue; - string key = values[0]; - for (int j = 0; j < values.Count; j++) - ((Dictionary)columns[j]).Add(key, values[j]); - } + // Parse the remaining lines to fill in the column data + for (int i = 1; i < lines.Length; i++) + { + string line = lines[i]; + List values = ParseLine(line); - return new ReadOnlyDictionary>(columns.Cast() - .ToDictionary(entry => (string)entry.Key, entry => (Dictionary)entry.Value)); - } + // Skip empty lines or lines with a blank first value + if (values.Count == 0 || string.IsNullOrWhiteSpace(values[0])) + continue; - private static List ParseLine(string line) - { - bool inQuotes = false; - bool wasBackslash = false; - List values = new(); - StringBuilder builder = new(); + string key = values[0]; + for (int j = 0; j < values.Count; j++) + ((Dictionary)columns[j]).Add(key, values[j]); + } - void FinishLine() - { - values.Add(builder.ToString()); - builder.Clear(); + // Convert the ordered dictionary to a read-only dictionary + return new ReadOnlyDictionary>(columns.Cast() + .ToDictionary(entry => (string)entry.Key, entry => (Dictionary)entry.Value)); } - foreach (char c in line) + /// + /// Parses a single line of CSV data. + /// + /// The line to parse. + /// A list of values from the line. + private static List ParseLine(string line) { - if (c == '\n' || (!inQuotes && c == ',')) - { - FinishLine(); - continue; - } + bool inQuotes = false; + bool wasBackslash = false; + List values = new(); + StringBuilder builder = new(); - switch (c) + // Helper method to add the current value to the list and reset the builder + void FinishLine() { - case '\r': - Multiplayer.LogWarning("Encountered carriage return in CSV! Please use Unix-style line endings (LF)."); - continue; - case '"': - inQuotes = !inQuotes; - continue; - case '\\': - wasBackslash = true; - continue; + values.Add(builder.ToString()); + builder.Clear(); } - if (wasBackslash) + // Iterate through each character in the line + foreach (char c in line) { - wasBackslash = false; - if (c == 'n') + if (c == '\n' || (!inQuotes && c == ',')) { - builder.Append('\n'); + FinishLine(); continue; } - // Not a special character, so just append the backslash - builder.Append('\\'); - } + switch (c) + { + case '\r': + Multiplayer.LogWarning("Encountered carriage return in CSV! Please use Unix-style line endings (LF)."); + continue; + case '"': + inQuotes = !inQuotes; + continue; + case '\\': + wasBackslash = true; + continue; + } - builder.Append(c); - } + if (wasBackslash) + { + wasBackslash = false; + if (c == 'n') + { + builder.Append('\n'); + continue; + } + + // Not a special character, so just append the backslash + builder.Append('\\'); + } - if (builder.Length > 0) - FinishLine(); + builder.Append(c); + } - return values; - } + if (builder.Length > 0) + FinishLine(); - public static string Dump(ReadOnlyDictionary> data) - { - StringBuilder result = new("\n"); + return values; + } - foreach (KeyValuePair> column in data) - result.Append($"{column.Key},"); + /// + /// Converts the dictionary data back to a CSV string. + /// + /// The dictionary data. + /// The CSV string representation of the data. + public static string Dump(ReadOnlyDictionary> data) + { + StringBuilder result = new("\n"); - result.Remove(result.Length - 1, 1); - result.Append('\n'); + // Write the column headers + foreach (KeyValuePair> column in data) + result.Append($"{column.Key},"); + result.Remove(result.Length - 1, 1); + result.Append('\n'); - int rowCount = data.Values.FirstOrDefault()?.Count ?? 0; + int rowCount = data.Values.FirstOrDefault()?.Count ?? 0; - for (int i = 0; i < rowCount; i++) - { - foreach (KeyValuePair> column in data) - if (column.Value.Count > i) - { - string value = column.Value.ElementAt(i).Value.Replace("\n", "\\n"); - result.Append(value.Contains(',') ? $"\"{value}\"," : $"{value},"); - } - else + // Write the rows + for (int i = 0; i < rowCount; i++) + { + foreach (KeyValuePair> column in data) { - result.Append(','); + if (column.Value.Count > i) + { + string value = column.Value.ElementAt(i).Value.Replace("\n", "\\n"); + result.Append(value.Contains(',') ? $"\"{value}\"," : $"{value},"); + } + else + { + result.Append(','); + } } + result.Remove(result.Length - 1, 1); + result.Append('\n'); + } - result.Remove(result.Length - 1, 1); - result.Append('\n'); + return result.ToString(); } - - return result.ToString(); } } From 6e721e56390389bbe8bb3765154fd136fc31045b Mon Sep 17 00:00:00 2001 From: AMacro Date: Thu, 20 Jun 2024 21:16:32 +1000 Subject: [PATCH 08/34] added button icons --- .../MainMenu/ServerBrowserElement.cs | 7 +- .../MainMenu/RightPaneControllerPatch.cs | 13 +- Multiplayer/Utils/Sprites.cs | 111 ------------------ MultiplayerAssets/Assets/AssetIndex.asset | 3 + .../Assets/Scripts/Multiplayer/AssetIndex.cs | 3 + MultiplayerAssets/Assets/Textures/Connect.png | Bin 0 -> 2648 bytes .../Assets/Textures/Connect.png.meta | 104 ++++++++++++++++ MultiplayerAssets/Assets/Textures/Refresh.png | Bin 0 -> 5304 bytes .../Assets/Textures/Refresh.png.meta | 104 ++++++++++++++++ .../Assets/Textures/lock_icon.png | Bin 0 -> 3724 bytes .../Assets/Textures/lock_icon.png.meta | 104 ++++++++++++++++ MultiplayerAssets/Packages/manifest.json | 1 + MultiplayerAssets/Packages/packages-lock.json | 7 ++ Sprites/lock.png | Bin 327 -> 0 bytes compare | 0 15 files changed, 333 insertions(+), 124 deletions(-) delete mode 100644 Multiplayer/Utils/Sprites.cs create mode 100644 MultiplayerAssets/Assets/Textures/Connect.png create mode 100644 MultiplayerAssets/Assets/Textures/Connect.png.meta create mode 100644 MultiplayerAssets/Assets/Textures/Refresh.png create mode 100644 MultiplayerAssets/Assets/Textures/Refresh.png.meta create mode 100644 MultiplayerAssets/Assets/Textures/lock_icon.png create mode 100644 MultiplayerAssets/Assets/Textures/lock_icon.png.meta delete mode 100644 Sprites/lock.png delete mode 100644 compare diff --git a/Multiplayer/Components/MainMenu/ServerBrowserElement.cs b/Multiplayer/Components/MainMenu/ServerBrowserElement.cs index 6f6be23..b269f54 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowserElement.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowserElement.cs @@ -42,11 +42,8 @@ private void Awake() ping.transform.position = new Vector3(PING_POS_X, pingPos.y, pingPos.z); ping.alignment = TextAlignmentOptions.Right; - // Set initial icon and text values for testing purposes - icon.sprite = Sprites.Padlock; - networkName.text = "Test Network"; - playerCount.text = "1/4"; - ping.text = "102 ms"; + // Set change icon + icon.sprite = Multiplayer.AssetIndex.lockIcon; } public override void SetData(IServerBrowserGameDetails data, AGridView _) diff --git a/Multiplayer/Patches/MainMenu/RightPaneControllerPatch.cs b/Multiplayer/Patches/MainMenu/RightPaneControllerPatch.cs index 3813179..7f31c20 100644 --- a/Multiplayer/Patches/MainMenu/RightPaneControllerPatch.cs +++ b/Multiplayer/Patches/MainMenu/RightPaneControllerPatch.cs @@ -1,8 +1,4 @@ -using System.Linq; -using System; -using DV.Common; using DV.Localization; -using DV.Scenarios.Common; using DV.UI; using DV.UIFramework; using HarmonyLib; @@ -11,7 +7,7 @@ using TMPro; using UnityEngine; using UnityEngine.UI; -using LiteNetLib; + namespace Multiplayer.Patches.MainMenu { @@ -62,10 +58,11 @@ private static void Prefix(RightPaneController __instance) // Update buttons on the multiplayer pane UpdateButton(multiplayerPane, "ButtonTextIcon Overwrite", "ButtonTextIcon Manual", Locale.SERVER_BROWSER__MANUAL_CONNECT_KEY, null, Multiplayer.AssetIndex.multiplayerIcon); - UpdateButton(multiplayerPane, "ButtonTextIcon Load", "ButtonTextIcon Host", Locale.SERVER_BROWSER__HOST_KEY, null, Multiplayer.AssetIndex.multiplayerIcon); - UpdateButton(multiplayerPane, "ButtonTextIcon Save", "ButtonTextIcon Join", Locale.SERVER_BROWSER__JOIN_KEY, null, null); - UpdateButton(multiplayerPane, "ButtonIcon Delete", "ButtonTextIcon Refresh", Locale.SERVER_BROWSER__REFRESH, null, null); + UpdateButton(multiplayerPane, "ButtonTextIcon Load", "ButtonTextIcon Host", Locale.SERVER_BROWSER__HOST_KEY, null, Multiplayer.AssetIndex.lockIcon); + UpdateButton(multiplayerPane, "ButtonTextIcon Save", "ButtonTextIcon Join", Locale.SERVER_BROWSER__JOIN_KEY, null, Multiplayer.AssetIndex.connectIcon); + UpdateButton(multiplayerPane, "ButtonIcon Delete", "ButtonTextIcon Refresh", Locale.SERVER_BROWSER__REFRESH, null, Multiplayer.AssetIndex.refreshIcon); + // Add the MultiplayerPane component multiplayerPane.AddComponent(); diff --git a/Multiplayer/Utils/Sprites.cs b/Multiplayer/Utils/Sprites.cs deleted file mode 100644 index dc63f6f..0000000 --- a/Multiplayer/Utils/Sprites.cs +++ /dev/null @@ -1,111 +0,0 @@ -using System; -using UnityEngine; - - -namespace Multiplayer.Utils -{ - public static class Sprites - { - private static UnityEngine.Sprite _padlock = null; - - private const int textureWidth = 50; - private const int textureHeight = 50; - - public static UnityEngine.Sprite Padlock - { - get - { - if (_padlock == null) - { - _padlock = DrawPadlock(); - } - return _padlock; - } - } - - private static UnityEngine.Sprite DrawPadlock() - { - Texture2D texture = new Texture2D(2, 2, TextureFormat.DXT5, false); ;//, TextureFormat.BGRA32, false);// (textureWidth, textureHeight); - - Debug.Log($"loading from {System.IO.Path.Combine(Multiplayer.ModEntry.Path, "lock.png")}"); - // Load the PNG file from the specified file path - byte[] fileData = System.IO.File.ReadAllBytes(System.IO.Path.Combine(Multiplayer.ModEntry.Path, "lock.png")); - - ImageConversion.LoadImage(texture, fileData); - //texture.LoadRawTextureData(pngBytes); // Load the PNG data into the texture - - //int border = 5; - - - //Color padlockColor = Color.white; - //Color transparentColor = new Color(0, 0, 0, 0); // Fully transparent - - //// Clear the texture with the transparent color - //for (int y = 0; y < textureHeight; y++) - //{ - // for (int x = 0; x < textureWidth; x++) - // { - // texture.SetPixel(x, y, transparentColor); - // } - //} - - //// Draw the padlock body (rectangle) - //int bodyWidth = (textureWidth - 2 * border)/2; - //int bodyHeight = (textureHeight - 2 * border) / 3; // Adjusting body height - //int bodyX = border; - //int bodyY = border; - - //for (int y = bodyY; y < bodyY + bodyHeight; y++) - //{ - // for (int x = bodyX; x < bodyX + bodyWidth; x++) - // { - // texture.SetPixel(x, y, padlockColor); - // } - //} - - ////Draw shanks - //int shankThickness = 6; - //int shankOffset = 2; - //int shankHeight = bodyHeight * 2/3; - - //for (int y = bodyHeight+border; y < bodyHeight+border+shankHeight; y++) - //{ - // for (int x = 0; x < shankThickness; x++) - // { - // texture.SetPixel(border + shankOffset + x, y, padlockColor); - // texture.SetPixel(textureWidth-( bodyWidth + border + shankOffset + x) , y, padlockColor); - // } - //} - - //// Draw the padlock shackle (semi-circle) - //int shackleRadius = (bodyWidth - 2* shankOffset)/ 2; - //int shackleCenterX = textureWidth / 2; - //int shackleCenterY = bodyHeight + border + shankHeight; //bodyY + bodyHeight; - - //// Adjust the length of the straight part of the shackle - //int shackleStraightLength = bodyHeight / 2; - - //// Adjust the thickness of the shackle - //int shackleThickness = 1; // Set the shackle thickness to 1 pixel - - //for (int y = shackleCenterY - shackleRadius; y <= shackleCenterY; y++) - //{ - // for (int x = shackleCenterX - shackleRadius; x <= shackleCenterX + shackleRadius; x++) - // { - // float distanceToCenter = Mathf.Sqrt((x - shackleCenterX) * (x - shackleCenterX) + (y - shackleCenterY) * (y - shackleCenterY)); - - // // Check if the current pixel is within the semicircle and thickness - // if (distanceToCenter <= shackleRadius && distanceToCenter >= shackleRadius - shankThickness && y >= shackleCenterY) - // { - // texture.SetPixel(x, y, padlockColor); - // } - // } - //} - - //texture.Apply(); - - return Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), new Vector2(0.5f, 0.5f)); - } - - } -} diff --git a/MultiplayerAssets/Assets/AssetIndex.asset b/MultiplayerAssets/Assets/AssetIndex.asset index b1c4785..735f514 100644 --- a/MultiplayerAssets/Assets/AssetIndex.asset +++ b/MultiplayerAssets/Assets/AssetIndex.asset @@ -15,3 +15,6 @@ MonoBehaviour: playerPrefab: {fileID: 1707366875631224182, guid: 720cc4622be79f701b73d41dbf0472ea, type: 3} multiplayerIcon: {fileID: 21300000, guid: 981b3e40e34126c43a32b7a54238d2d6, type: 3} + lockIcon: {fileID: 21300000, guid: b8a707a2b12db584fad32aed46912dd0, type: 3} + refreshIcon: {fileID: 21300000, guid: 7c3f2166549e6e144ae26c8d527d59b0, type: 3} + connectIcon: {fileID: 21300000, guid: dad0fda7f8df3cd41a278a839fe12d23, type: 3} diff --git a/MultiplayerAssets/Assets/Scripts/Multiplayer/AssetIndex.cs b/MultiplayerAssets/Assets/Scripts/Multiplayer/AssetIndex.cs index b0a87a0..2a89138 100644 --- a/MultiplayerAssets/Assets/Scripts/Multiplayer/AssetIndex.cs +++ b/MultiplayerAssets/Assets/Scripts/Multiplayer/AssetIndex.cs @@ -10,5 +10,8 @@ public class AssetIndex : ScriptableObject [Header("Textures")] public Sprite multiplayerIcon; + public Sprite lockIcon; + public Sprite refreshIcon; + public Sprite connectIcon; } } diff --git a/MultiplayerAssets/Assets/Textures/Connect.png b/MultiplayerAssets/Assets/Textures/Connect.png new file mode 100644 index 0000000000000000000000000000000000000000..6b22b32a8b3f35e03380278ed784069e8a5a980b GIT binary patch literal 2648 zcmeAS@N?(olHy`uVBq!ia0y~yU;;9k7&zE~)R&4Yzkn2Dage(c!@6@aFM%AEbVpxD z28NCO+M1MG8>tt*47)NJZS+yBG6rd5Jh&-0|}N|3c&I zVKQ6IIa!)~8}?31xyfIAy{EU{ehWj)yZagjtIHAs^^;#Pzj?3Ac4iH4@sOu_f$n@$P6aj$Ct?r@rC064SYag?8ybnD3|y7ASRpnfQ>u;J(WN&OO`@{_C(+ z?|k}x=k)st-)(LBPgt|{1$cn6?!Ql8_&-F{h!(W8G6J*rhf|mB4yH*z5RcTnKkL|u{XFMyGwf{3wEcYXbWMc(w{JG`r_+0<%F;fm_$K#!{j_58 zKY@Y^Tju?Ik!rVwKj-oHnm?6$ZO-wLT6(>>GM&-ZNZ;y_)Sfrb4D^{FNzHqn{BzC5 zP0XZ*33UogP})1a<_}YW=;f+?uKJ$~PuJVuIP|$jXMSl|8F|HV!}bM?Hdgsee_Fuh z{D)I|30SN(>M7N&HI}U^w2%(C|Cq$9zwQn%qtXMTUae z(+=*xvzYh4F=xYNpxo_8EB`YwJ+LWbW~iID{2wnz!+Sv{hU@7c@;w-0p6Rk2VK`uS zuWjGK{f3MiWDOZM@H+qJ`Xln;Yvc#D#EORbOy@qy|EQY*a&bK4XpJ>mF^|{=%ADoP b|1-R8Y`*wCg~1ls8f5Tv^>bP0l+XkK>BsVI literal 0 HcmV?d00001 diff --git a/MultiplayerAssets/Assets/Textures/Connect.png.meta b/MultiplayerAssets/Assets/Textures/Connect.png.meta new file mode 100644 index 0000000..30a876c --- /dev/null +++ b/MultiplayerAssets/Assets/Textures/Connect.png.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: dad0fda7f8df3cd41a278a839fe12d23 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 11 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 1 + wrapV: 1 + wrapW: 0 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 0 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 8 + textureShape: 1 + singleChannelComponent: 0 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + applyGammaDecoding: 0 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: 5e97eb03825dee720800000000000000 + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Textures/Refresh.png b/MultiplayerAssets/Assets/Textures/Refresh.png new file mode 100644 index 0000000000000000000000000000000000000000..9f9062d833219c7fd7bdb053cd41b4105f87bc1a GIT binary patch literal 5304 zcmds5iCa@g*FX1i12+N51w#`SC2SHDBPt>kG$9BgD2rA>WC^aQs8~dyqPc7;K?HF} zDxg-{s(==)N-IGH>w-YBiYpN;ifbraiXrzM-uJKgzWY3RW;t_ybLLFu%+EC?A>+h5%+f!Lj^4k~h`SEVmw6pac2>bupCwg~JHYz`3v1 z_=!#a+yzq38h=HY_DNPH{J@6n?wU}@1SeFR!Dp)4f?qjjni1EHv2W~v+P6rO(PJ2f zbOanpE{UIZ1}bx_WS*sQ;bBmfmkhf|XVM1=FfG?CDilg5;!xZ6slwBj<`J;9>tX2h zPexQRsKh11YlBFkD@HHy8pzrYQ`wN%_>=9+mN7yeM*rnCaC7lBEZYvMCsyo?oHLWY z$%b@$&)EpIVW0%i_ zKiUX*>G`Z?!=vW)i!~Al>RixFx`GY5&WDUUi!sIP+p^(=J!oXTiuSbD2u-O*Opyln zNX%zGNP89cf6`1Ewx$d}*%KNHEAwh##35gdJdF)9RVgyqHWsWF>G^4a}H|kxU|kFN0EZunN~H!nM{EkqsNiLUEWbPNW+%gNw@`f(>It zdXD6L7EP)Aap`7dT~ooY#g**$G?$=n6!R4oJ1f^c9Yf)%ejxgJb8m!p>A84{!nOV& zTKBa5>z(&XoAP(N8{^kSd-%+B=5%4&bZhIS{8Jjn>vx`F{IuCKlf!khe{X#A)5VTA zvogE4fbtlBYsI6wMUu1YF77&KP0&Hb7R-f=V*|hBrcGXK%O=YOHX{{DjR#jV`%z|~O){}Su-oBmv&xGQour|#ypVS%UBJxoo#Zo`3c z9viy` z{#OY>^o2)touR?3M;9(FD>Rj+FOFh7sHYQK*lrwAAdu(6kTDg%<8~@hnE(@B=2R#e zY**j$Q?=(^=U^!$_*!_4OwwyYGAKDOEE}kZoHjetCr;JBOEgEdAdn9q4F1sj(-4tZHBn?OC;>&NHf*2Yn=+tSYdJDysMV(ZcabXEi zff;Re!3qtX_F`o%_kUf%2my6n**k=7!OX`(X5h}@$HTjwoyM`=;*JyG*8YM1D(%gn zqFo5`G(m&;C}iZ)8>5g1f%1Das$ZRv7PIOa??>apLP)B9U7%`VcAF)0Lx_(B)54mOssXN1kUPiCFi-}b0hb4n zM{X6y^W-Ll*-$;bLZJ@N4M4lZCgDr+u+jL#D@V}gPKnI= z5kKj?YtRVwZ1{5bp&mJ+{p2=mFP1qLt~B=8;=zBigl?GJ`OZ@&gemmv7t>Rva$L1@ z+9l?Dy&tyt99t()QYkDuT`@p`~r< ztAZ%H)*5?VOUVhSz8tQbtBugDuu_QQ^6d%w!F?v9GJs5Az9(>%VfRYQ5xndf1Nm%t zreR4Tdjf#e50{2^Gl0U#tYgg0t?3q<(oKEGV)9k<#DH5LyE!n_<3!wY*GH#wRdXOR zuHTq0nvyu5B!hb;4$yyUgzHV=H^OxtW&j@cM0UA0P6&_~NRqy9`fv>7E0TMAxAohu zAf^nR5K{nyuXLz6-Z&{h`+ln0 zt3hFhVAjY~t9gt!iAc!Nyo1gLk5=7JGr~aadBP;JSv&N$sBauW&vrE>8%6RNQ4E)Y z;TEebI-9O=(R0la@tZ*S%~h3ch?qGX*A!i=WtXZLr3!o!$uHpL@F0vPXYgd-H_cGY?xFmdHtmBfVJo? zjXYrF4B&pD&#s|sCpxIVrpfL@M*y`#<=&&So$6(b9( zHhqqLJwzYkL3jeZSSV%^Nqz^QT$nrF@5s;^z+xUi#;|ktcx~(J;s6doVH_XOKvzM%3~vjF6$-9*BldEq4mD;bn6OEWNTz- z-?gVjDS6(o|Gx3GXEAjB4v4IOu$`_QB~$hDt)-VY8Tpvu>Ul=z6~1LI+o1DKDqk*e z{z?%~`6SF9shTwuxBT3TwiQ{8I10n{_t`$rhVYUrJd%6#aZ_toT`%*oNbV2-80(42 z_*FdEMqYavx+0@Kp-hAcf82kg!q)OLZXyHMLdOrwn*A-YTZab3s-$&J>l|)u=z2N7 zKcsXZ8r|a$OD@-iR^7K#{UE)vT>RtQ=;7_z1JQ**wcFqqV@aE6PPTDTjou{$_B4xt zGcQVad%XLgJ!do9Ed#G$9oZubMqK=f80?h3?3A#ghWfgKb@cZ<4S_*JB}S;53WYeS<6z^4J<7& zl}_^yz_LPMl?C}OiWx(K*J1$+t?6%`NFT*45MYjrWlmcF!DG=<+bX zZFo~7DZDB>-t4s@aG_|4?)=r&1dV4yJEDGD1Aukk_}9BT=mQsivNEC;UTE3yspB9b z!2Y#x)ARdO8BKMk93DBtcvUcs1TEw2rt1S5$~l!&*8l8aK@X))jnc!=kb{dk$-8U^P3;{UtV6gwWompHTYZ1 zx*FZ-e)DH#)wuULaVQVShARb~q0T5jXp+&!nLaCHo&ORKf4@lguM`r>!bt&veYHMJ zYt&GhN6-_Bb7)^m{quwzMVRht5G0(ACk2W0Z>%cW+(vO`X6D@jqu6O4X$2<# zEY#k9BmeH16S4hy!SO9nymrg*)mxEe<^N9aD1V`N&{G0Rx? z-HG^(A~@@!N2hE+=~6ZwM`Yl|e7$e)8!;|TmZ_{Ogl?n(yToFoeEln1p5oI45E9X6 zCQ(P*^wvrV*9=nah-fF0IH^&TY5UwTIxd?&9^xyf47YAs(r=W*FfMu@e^8IHlbglM zS19!rJjIC#pvmxfACsMNNHm=1qCY1C^~4u+?f1Jge1+QVlZqp4Z|=QOE+*l*BjMvO z^z5r_FBJ{n^Bnl)Ym5$MV`tY|kYZkXgMO+~a-4$5Ib{2-FH*PDCajR5D^8~=?ia3PW|zDZtt38)bBmXo+BGy zp;0%x&Tc4;A;D+yRq9}c~6I5H< z(W^^_*$Y4(5rDg^ABJ*plot@9z*o)|7z)NXuS|0Qy>XDAj0z8WN!Frw2c&EtGIAQd zE$kCY60%phO5L809JB|jcq3q*1B;LfClidqv@Q!kc<7hWEll)%Rl&mOnk}rz z)}P*TIg-gFoOUKPM8%G$<{ZUEAd|9gaHnJR#c09G)z`Uv^sFK@sd-() zor!8I=7bfu&31K!mcx}&OXX}IDbTrYh5Jr&L(M@wE?(&BZ@UCN*NpjmLnWj>y!lPd zRn9&)`lv$^#}{hi=i=bwD@;{pa*fPE?$5{Tg}ijv!L0waS47C_!)6FXgNW~g(N!~( zkdubl!7P2raxO{-sE3(!5|1?={*Qe0x}9G>G7ze~A<3DXwtCd+dHx}DDrNG_{{YL~ B$bkR= literal 0 HcmV?d00001 diff --git a/MultiplayerAssets/Assets/Textures/Refresh.png.meta b/MultiplayerAssets/Assets/Textures/Refresh.png.meta new file mode 100644 index 0000000..7239c66 --- /dev/null +++ b/MultiplayerAssets/Assets/Textures/Refresh.png.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 7c3f2166549e6e144ae26c8d527d59b0 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 11 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 1 + wrapV: 1 + wrapW: 0 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 0 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 8 + textureShape: 1 + singleChannelComponent: 0 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + applyGammaDecoding: 0 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: 5e97eb03825dee720800000000000000 + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Textures/lock_icon.png b/MultiplayerAssets/Assets/Textures/lock_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..dcb097ec838b0c497976efdfba678e64f51c7cbd GIT binary patch literal 3724 zcmeHKdsGu=7QZuukOVLxSd4-ufDc+&idv0OAmLSTt2Ee(P#*+oi%*hPii$`EP=XeT zJ=H}G8th97C}F{3LC9dB)+)*tX%S2lR35UViOVY_BrTQH%F; z{TaiK^@VmWB;a}^IGe|Iyr0;TTLQgFL zKeg^sT9%_a3z^{J&vLbM)0M3rqsz^&dT`+ zUP<^hqhuu&!FLg2)Dfs+LcrZhw#PscwvhrJ?{?Fx7)U@oC@EF8K(B`4pPr};6Kj~l z1Qes!jcD6~)rTiq#mwv)H|k&i?1}r3U|!uwO8e^tEq5vnC*q86Cl_iUd>x40s%$Z# z+)-)EMV`A;zAkN)EMAV_P4jK@j(JWF9Y}59&%G|A=zLJD?)<}QO}Tb5Q`c7+_Q}@# zkGxVu+tSO{gV>|WR#!IXF!#~XP%66I7}A6gR^w8&JtlG5v+h-~hZ~+uf|?aGig%l? zx@S3UTZ|s~1avWl_LTTd7P3UzhV!DKh6$m!jrQF-ZegzXvgIcip+49=D%jk@XJiFh zoQ-YGi}k_M2@5lH#?{t()Ici&n>Uzz#U{2iH_a%e%t74n6cWfHr7C%16Kk5f9MJ#oV#x ze8>VcX<8jZ)=Dm{aA>8+vA(469GztYIb0dD!o_s{p6kH z{P)rF3bcx_g@0+SFWw4?zA!R!ctV@|qt+;Oej%8qbQYWep{u@dAl=$ua6B4fs91i8 z*48jVcYcY1lpsR?F1bD{dwkg5r9J_Poj-3LJ5(L3*ZN#!y$S*C-?I~+tiisZ!k>gD zH94dNZh|8f+djxm77f|MpV2+fYIiqVOMvGpI0+S(8rb&T{%#7BlizD#Oh~jVn`x@W zMR%!yZ4a>pz)=bi*{#oo@Eg-SK7n|d*hUwe0>JnLHK9}h%3=V{$=?nCVm&%Da^Fj{ zSZ!nD;{(R-ribff2I=i8%X13DUntJxUHgsWR&(IWJ2dP$>iw|;``?6_vYH2&@Bdxl z>*b0EJ-I5?`3t#TE&1Jt<%{Uj+g#0T-CpmxqseHn1gF#?h0n`p}Iekz<XhNbeUc=AGumf%i1u8 zaOXdsb?(_?PHQKu~&TOXJzqFnm>oET`zC-IQCsa(cck)(3VmptI%!wLOl+x07J}tI=71Cl`Uk*%hHPvO$&l8#~2KNt|=6JN*YO z>H$WtwmlFsUjx3RFv-dereWb!7{0xu${t&Smh*@lEAEf5hf4v=b@|53>#u`OH{xYh z_Jn~2W}n|6{PhzSgm^PM>D<}fo8zhsA!cO?4{PxX zytyj&Zb!er?i>k}Uw8!nAq(?&2VlejpI2z}!t|2ikBI!mwqWS9&!&ewVr}n!mo}@i zi6}caKS_x_j!hf>_F;C9OM;d?t2j^auBLMC)x@-9L5Sff<4^_WS(E=z{xM6}Z< zK+QHUgL2V;4{nq$AiOo}NG#iIF&8w-21Ni)`c-t=_BvajVmhb$;>SzqEfXczTagD40*-Vt@MLlT7>#-(|b z|3$zyO`knMoT`6oE=hM9o<0*_+w!nu+?0FmNV`U#)ub3(<;j3e#Qw1P!0JI_5x=+m ze-GpRy`9-Z!wv`MI5G^m4Sm)NHUIHHE!|gdC~OYVF59`5#CC$k0oKOEM_-8)=Kl&) C{I#$E literal 0 HcmV?d00001 diff --git a/MultiplayerAssets/Assets/Textures/lock_icon.png.meta b/MultiplayerAssets/Assets/Textures/lock_icon.png.meta new file mode 100644 index 0000000..9d0ce88 --- /dev/null +++ b/MultiplayerAssets/Assets/Textures/lock_icon.png.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: b8a707a2b12db584fad32aed46912dd0 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 11 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 1 + wrapV: 1 + wrapW: 0 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 0 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 8 + textureShape: 1 + singleChannelComponent: 0 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + applyGammaDecoding: 0 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: 5e97eb03825dee720800000000000000 + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/MultiplayerAssets/Packages/manifest.json b/MultiplayerAssets/Packages/manifest.json index b4953ac..d948b24 100644 --- a/MultiplayerAssets/Packages/manifest.json +++ b/MultiplayerAssets/Packages/manifest.json @@ -1,5 +1,6 @@ { "dependencies": { + "com.unity.assetbundlebrowser": "1.7.0", "com.unity.ide.rider": "1.2.1", "com.unity.ide.visualstudio": "2.0.18", "com.unity.ide.vscode": "1.2.5", diff --git a/MultiplayerAssets/Packages/packages-lock.json b/MultiplayerAssets/Packages/packages-lock.json index 38fde5f..d638f04 100644 --- a/MultiplayerAssets/Packages/packages-lock.json +++ b/MultiplayerAssets/Packages/packages-lock.json @@ -1,5 +1,12 @@ { "dependencies": { + "com.unity.assetbundlebrowser": { + "version": "1.7.0", + "depth": 0, + "source": "registry", + "dependencies": {}, + "url": "https://packages.unity.com" + }, "com.unity.ext.nunit": { "version": "1.0.6", "depth": 2, diff --git a/Sprites/lock.png b/Sprites/lock.png deleted file mode 100644 index dd86c0f0d900289499ceafefbb97a1890580efb8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 327 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nC-#^NA%Cx&(BWL^R}Ea{HEjtmSN z`?>!lvI6;>1s;*b3=Co_Aj~*h^q2@x@Q$a8V@QVc+iQXR%?dm$33vYgKkp~xF^yGc z%k{UL?tWjyA5?wHs@tP)vFyTg-qx~KdT&M8!Y*X}<5P-Q;jrZAL=T1UP9+`Zq>pXY z&~|ywxp{Ab>5(-vU;LW6wmW=QL@q<*>L%8kJ6~#toPEk|3{-imzpSLYZH0j8A;*Z8 zCWUz^;%k?hA98cN@yAD?IOxvb$l6J!SGR1vp}`^TyH<3u=i{~Cx2`nZz47XV>2=CE zA}zjA6K Date: Sat, 22 Jun 2024 10:26:05 +0930 Subject: [PATCH 09/34] minor correction --- Multiplayer/Components/MainMenu/MultiplayerPane.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Multiplayer/Components/MainMenu/MultiplayerPane.cs b/Multiplayer/Components/MainMenu/MultiplayerPane.cs index 75ed863..1ed7502 100644 --- a/Multiplayer/Components/MainMenu/MultiplayerPane.cs +++ b/Multiplayer/Components/MainMenu/MultiplayerPane.cs @@ -19,9 +19,12 @@ namespace Multiplayer.Components.MainMenu public class MultiplayerPane : MonoBehaviour { // Regular expressions for IP and port validation + // @formatter:off + // Patterns from https://ihateregex.io/ private static readonly Regex IPv4Regex = new Regex(@"(\b25[0-5]|\b2[0-4][0-9]|\b[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}"); private static readonly Regex IPv6Regex = new Regex(@"(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))"); private static readonly Regex PortRegex = new Regex(@"^((6553[0-5])|(655[0-2][0-9])|(65[0-4][0-9]{2})|(6[0-4][0-9]{3})|([1-5][0-9]{4})|([0-5]{0,5})|([0-9]{1,4}))$"); + // @formatter:on private string ipAddress; private ushort portNumber; @@ -52,7 +55,7 @@ private void OnEnable() this.RefreshData(); } - // Token: 0x060001C2 RID: 450 RVA: 0x00007D0C File Offset: 0x00005F0C + // Disable listeners private void OnDisable() { this.SetupListeners(false); From d236a90b1ae1104d02a4293f36772c8bf6ccb60b Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 22 Jun 2024 18:01:46 +1000 Subject: [PATCH 10/34] PHP and Rust servers implemented --- .gitignore | 1 + Lobby Servers/PHP Server/config.php | 14 + Lobby Servers/PHP Server/index.php | 176 +++ Lobby Servers/RestAPI.md | 241 ++++ Lobby Servers/Rust Server/Cargo.lock | 1538 +++++++++++++++++++++++++ Lobby Servers/Rust Server/Cargo.toml | 16 + Lobby Servers/Rust Server/Read Me.md | 42 + Lobby Servers/Rust Server/src/main.rs | 270 +++++ 8 files changed, 2298 insertions(+) create mode 100644 Lobby Servers/PHP Server/config.php create mode 100644 Lobby Servers/PHP Server/index.php create mode 100644 Lobby Servers/RestAPI.md create mode 100644 Lobby Servers/Rust Server/Cargo.lock create mode 100644 Lobby Servers/Rust Server/Cargo.toml create mode 100644 Lobby Servers/Rust Server/Read Me.md create mode 100644 Lobby Servers/Rust Server/src/main.rs diff --git a/.gitignore b/.gitignore index 87860e1..145bee5 100644 --- a/.gitignore +++ b/.gitignore @@ -306,3 +306,4 @@ MultiplayerAssets/ProjectSettings/* !MultiplayerAssets/ProjectSettings/ProjectVersion.txt # Packages !MultiplayerAssets/Packages +/Lobby Servers/Rust Server/target diff --git a/Lobby Servers/PHP Server/config.php b/Lobby Servers/PHP Server/config.php new file mode 100644 index 0000000..f4942fd --- /dev/null +++ b/Lobby Servers/PHP Server/config.php @@ -0,0 +1,14 @@ + 'localhost', + 'dbname' => 'your_database', + 'username' => 'your_username', + 'password' => 'your_password' +]; + +?> \ No newline at end of file diff --git a/Lobby Servers/PHP Server/index.php b/Lobby Servers/PHP Server/index.php new file mode 100644 index 0000000..2a57cf3 --- /dev/null +++ b/Lobby Servers/PHP Server/index.php @@ -0,0 +1,176 @@ +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + + // Now you can use $pdo to execute queries +} catch (PDOException $e) { + // Handle database connection errors + echo "Connection failed: " . $e->getMessage(); +} + + +// Define routes +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + + $data = json_decode(file_get_contents('php://input'), true); + + switch ($_SERVER['REQUEST_URI']) { + case '/add_game_server': + echo add_game_server($pdo, $data); + break; + + case '/update_game_server': + echo update_game_server($pdo, $data); + break; + + case '/remove_game_server': + echo remove_game_server($pdo, $data); + break; + + default: + http_response_code(404); + break; + } + +} elseif ($_SERVER['REQUEST_METHOD'] === 'GET') { + if ($_SERVER['REQUEST_URI'] === '/list_game_servers') { + echo list_game_servers($pdo); + } else { + http_response_code(404); + } +} else { + http_response_code(405); // Method Not Allowed +} + + +function add_game_server($pdo, $data) { + // Validation + if (!validate_server_info($data)) { + return json_encode(["error" => "Invalid server information"]); + } + + // Generate a UUID for the game server + $game_server_id = uuid_create(); + + // Insert server information into the database + $stmt = $pdo->prepare("INSERT INTO game_servers (game_server_id, ip, port, server_name, password_protected, game_mode, difficulty, time_passed, current_players, max_players, required_mods, game_version, multiplayer_version, server_info, last_update) + VALUES (:game_server_id, :ip, :port, :server_name, :password_protected, :game_mode, :difficulty, :time_passed, :current_players, :max_players, :required_mods, :game_version, :multiplayer_version, :server_info, :last_update)"); + $stmt->execute([ + ':game_server_id' => $game_server_id, + ':ip' => $data['ip'], + ':port' => $data['port'], + ':server_name' => $data['server_name'], + ':password_protected' => $data['password_protected'], + ':game_mode' => $data['game_mode'], + ':difficulty' => $data['difficulty'], + ':time_passed' => $data['time_passed'], + ':current_players' => $data['current_players'], + ':max_players' => $data['max_players'], + ':required_mods' => $data['required_mods'], + ':game_version' => $data['game_version'], + ':multiplayer_version' => $data['multiplayer_version'], + ':server_info' => $data['server_info'], + ':last_update' => time() // Assuming Unix timestamp for last_update + ]); + + // Return game server ID + return json_encode(["game_server_id" => $game_server_id]); +} + + +function update_game_server($pdo, $data) { + // Update current players count and time passed for the specified game server + $stmt = $pdo->prepare("UPDATE game_servers + SET current_players = :current_players, time_passed = :time_passed, last_update = :last_update + WHERE game_server_id = :game_server_id"); + $stmt->execute([ + ':current_players' => $data['current_players'], + ':time_passed' => $data['time_passed'], + ':last_update' => time(), // Assuming Unix timestamp for last_update + ':game_server_id' => $data['game_server_id'] + ]); + + // Check if update was successful + if ($stmt->rowCount() > 0) { + return json_encode(["message" => "Server updated"]); + } else { + return json_encode(["error" => "Failed to update server"]); + } +} + + +function remove_game_server($pdo, $data) { + // Delete the specified game server from the database + $stmt = $pdo->prepare("DELETE FROM game_servers WHERE game_server_id = :game_server_id"); + $stmt->execute([':game_server_id' => $data['game_server_id']]); + + // Check if deletion was successful + if ($stmt->rowCount() > 0) { + return json_encode(["message" => "Server removed"]); + } else { + return json_encode(["error" => "Failed to remove server"]); + } +} + + +function list_game_servers($pdo) { + // Retrieve the list of game servers from the database + $stmt = $pdo->query("SELECT * FROM game_servers"); + $servers = $stmt->fetchAll(PDO::FETCH_ASSOC); + + // Return the list of game servers + return json_encode($servers); +} + + +/* + ************************************** + + Helper functions + + ************************************* +*/ + + +function validate_server_info($data) { + // Check if server name length exceeds 25 characters + if (strlen($data['server_name']) > 25) { + return false; + } + + // Check if server info length exceeds 500 characters + if (strlen($data['server_info']) > 500) { + return false; + } + + // Check if current players exceed max players + if ($data['current_players'] > $data['max_players']) { + return false; + } + + // Check if max players is at least 1 + if ($data['max_players'] < 1) { + return false; + } + + // If all checks pass, return true + return true; +} + + +// Function to generate UUID +function uuid_create() { + return sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x', + mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff), + mt_rand(0, 0x0fff) | 0x4000, + mt_rand(0, 0x3fff) | 0x8000, + mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff) + ); +} \ No newline at end of file diff --git a/Lobby Servers/RestAPI.md b/Lobby Servers/RestAPI.md new file mode 100644 index 0000000..927c696 --- /dev/null +++ b/Lobby Servers/RestAPI.md @@ -0,0 +1,241 @@ +# Derail Valley Lobby Server REST API Documentation + +Revision: A +Date: 2024-06-22 + +## Overview + +This document describes the REST API endpoints for the Derail Valley Lobby Server service. The service allows game servers to register, update, and deregister themselves, and provides a list of active servers to clients. +This spec does not provide the server address, as new servers can be created by anyone wishing to host their own lobby server. + +## Enums + +### Game Modes + +The game_mode field in the request body for adding a game server must be one of the following integer values, each representing a specific game mode: + +- 0: Career +- 1: Sandbox + +### Difficulty Levels + +The difficulty field in the request body for adding a game server must be one of the following integer values, each representing a specific difficulty level: + +- 0: Standard +- 1: Comfort +- 2: Realistic +- 3: Custom + +## Endpoints + +### Add Game Server + +- **URL:** `/add_game_server` +- **Method:** `POST` +- **Content-Type:** `application/json` +- **Request Body:** + ```json + { + "ip": "string", + "port": "integer", + "server_name": "string", + "password_protected": "boolean", + "game_mode": "integer", + "difficulty": "integer", + "time_passed": "string" + "current_players": "integer", + "max_players": "integer", + "required_mods": "string", + "game_version": "string", + "multiplayer_version": "string", + "server_info": "string" + } + ``` + - **Fields:** + - ip (string): The IP address of the game server. + - port (integer): The port number of the game server. + - server_name (string): The name of the game server (maximum 25 characters). + - password_protected (boolean): Indicates if the server is password-protected. + - game_mode (integer): The game mode (see [Game Modes](#game-modes)). + - difficulty (integer): The difficulty level (see [Difficulty Levels](#difficulty-levels)). + - time_passed (string): The in-game time passed since the game/session was started. + - current_players (integer): The current number of players on the server (0 - max_players). + - max_players (integer): The maximum number of players allowed on the server (>= 1). + - required_mods (string): The required mods for the server, supplied as a JSON string. + - game_version (string): The game version the server is running. + - multiplayer_version (string): The Multiplayer Mod version the server is running. + - server_info (string): Additional information about the server (maximum 500 characters). +- **Response:** + - **Success:** + - **Code:** 200 OK + - **Content-Type:** `application/json` + - **Content:** + ```json + { + "game_server_id": "string" + } + ``` + - game_server_id (string): A GUID assigned to the game server. This GUID uniquely identifies the game server and is used when updating the lobby server. + - **Error:** + - **Code:** 500 Internal Server Error + - **Content:** `"Failed to add server"` + +### Update Server + +- **URL:** `/update_game_server` +- **Method:** `POST` +- **Content-Type:** `application/json` +- **Request Body:** + ```json + { + "game_server_id": "string", + "current_players": "integer", + "time_passed": "string" + } + ``` + - **Fields:** + - game_server_id (string): The GUID assigned to the game server (returned from `add_game_server`). + - current_players (integer): The current number of players on the server (0 - max_players). + - time_passed (string): The in-game time passed since the game/session was started. +- **Response:** + - **Success:** + - **Code:** 200 OK + - **Content:** `"Server updated"` + - **Error:** + - **Code:** 500 Internal Server Error + - **Content:** `"Failed to update server"` + +### Remove Server + +- **URL:** `/remove_game_server` +- **Method:** `POST` +- **Content-Type:** `application/json` +- **Request Body:** + ```json + { + "game_server_id": "string" + } + ``` + - **Fields:** + - game_server_id (string): The GUID assigned to the game server (returned from `add_game_server`). +- **Response:** + - **Success:** + - **Code:** 200 OK + - **Content:** `"Server removed"` + - **Error:** + - **Code:** 500 Internal Server Error + - **Content:** `"Failed to remove server"` + +### List Game Servers + +- **URL:** `/list_game_servers` +- **Method:** `GET` +- **Response:** + - **Success:** + - **Code:** 200 OK + - **Content-Type:** `application/json` + - **Content:** + ```json + [ + { + "ip": "string", + "port": "integer", + "server_name": "string", + "password_protected": "boolean", + "game_mode": "integer", + "difficulty": "integer", + "time_passed": "string" + "current_players": "integer", + "max_players": "integer", + "required_mods": "string", + "game_version": "string", + "multiplayer_version": "string", + "server_info": "string" + }, + ... + ] + ``` + - **Error:** + - **Code:** 500 Internal Server Error + - **Content:** `"Failed to retrieve servers"` + +## Example Requests + +### Add Game Server +Example request: +```bash +curl -X POST -H "Content-Type: application/json" -d '{ + "ip": "127.0.0.1", + "port": 7777, + "server_name": "My Derail Valley Server", + "password_protected": false, + "current_players": 1, + "max_players": 10, + "game_mode": 0, + "difficulty": 0, + "time_passed": "0d 10h 45m 12s", + "required_mods": "", + "game_version": "98", + "multiplayer_version": "0.1.0", + "server_info": "License unlocked server
Join our discord and have fun!" +}' http:///add_game_server +``` +Example response: +```json +{ + "game_server_id": "0e1759fd-ba6e-4476-ace2-f173af9db342" +} +``` + +### Update Game Server +Example request: +```bash +curl -X POST -H "Content-Type: application/json" -d '{ + "game_server_id": "0e1759fd-ba6e-4476-ace2-f173af9db342", + "current_players": 2, + "time_passed": "0d 10h 47m 12s" +}' http:///update_game_server +``` +Example response: +```json +{ + "message": "Server updated" +} +``` +### Remove Game Server +Example request: +```bash +curl -X POST -H "Content-Type: application/json" -d '{ + "game_server_id": "0e1759fd-ba6e-4476-ace2-f173af9db342" +}' http:///remove_game_server +``` +Example response: +```json +{ + "message": "Server removed" +} +``` + +### List Game Servers + +```bash +curl http:///list_game_servers +``` + +## Error Handling + +In case of an error, the API will return a JSON response with a message indicating the failure. + +```json +{ + "error": "string" +} +``` + +### Common Error Responses + +- **500 Internal Server Error** + - **Content:** `"Failed to add server"` + - **Content:** `"Failed to update server"` + - **Content:** `"Failed to remove server"` + - **Content:** `"Failed to retrieve servers"` diff --git a/Lobby Servers/Rust Server/Cargo.lock b/Lobby Servers/Rust Server/Cargo.lock new file mode 100644 index 0000000..c5fbb5b --- /dev/null +++ b/Lobby Servers/Rust Server/Cargo.lock @@ -0,0 +1,1538 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "actix-codec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" +dependencies = [ + "bitflags", + "bytes", + "futures-core", + "futures-sink", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "actix-http" +version = "3.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae682f693a9cd7b058f2b0b5d9a6d7728a8555779bedbbc35dd88528611d020" +dependencies = [ + "actix-codec", + "actix-rt", + "actix-service", + "actix-tls", + "actix-utils", + "ahash", + "base64", + "bitflags", + "brotli", + "bytes", + "bytestring", + "derive_more", + "encoding_rs", + "flate2", + "futures-core", + "h2", + "http", + "httparse", + "httpdate", + "itoa", + "language-tags", + "local-channel", + "mime", + "percent-encoding", + "pin-project-lite", + "rand", + "sha1", + "smallvec", + "tokio", + "tokio-util", + "tracing", + "zstd", +] + +[[package]] +name = "actix-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "actix-router" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13d324164c51f63867b57e73ba5936ea151b8a41a1d23d1031eeb9f70d0236f8" +dependencies = [ + "bytestring", + "cfg-if", + "http", + "regex", + "regex-lite", + "serde", + "tracing", +] + +[[package]] +name = "actix-rt" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24eda4e2a6e042aa4e55ac438a2ae052d3b5da0ecf83d7411e1a368946925208" +dependencies = [ + "futures-core", + "tokio", +] + +[[package]] +name = "actix-server" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b02303ce8d4e8be5b855af6cf3c3a08f3eff26880faad82bab679c22d3650cb5" +dependencies = [ + "actix-rt", + "actix-service", + "actix-utils", + "futures-core", + "futures-util", + "mio", + "socket2", + "tokio", + "tracing", +] + +[[package]] +name = "actix-service" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b894941f818cfdc7ccc4b9e60fa7e53b5042a2e8567270f9147d5591893373a" +dependencies = [ + "futures-core", + "paste", + "pin-project-lite", +] + +[[package]] +name = "actix-tls" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac453898d866cdbecdbc2334fe1738c747b4eba14a677261f2b768ba05329389" +dependencies = [ + "actix-rt", + "actix-service", + "actix-utils", + "futures-core", + "impl-more", + "openssl", + "pin-project-lite", + "tokio", + "tokio-openssl", + "tokio-util", + "tracing", +] + +[[package]] +name = "actix-utils" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" +dependencies = [ + "local-waker", + "pin-project-lite", +] + +[[package]] +name = "actix-web" +version = "4.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1988c02af8d2b718c05bc4aeb6a66395b7cdf32858c2c71131e5637a8c05a9ff" +dependencies = [ + "actix-codec", + "actix-http", + "actix-macros", + "actix-router", + "actix-rt", + "actix-server", + "actix-service", + "actix-tls", + "actix-utils", + "actix-web-codegen", + "ahash", + "bytes", + "bytestring", + "cfg-if", + "cookie", + "derive_more", + "encoding_rs", + "futures-core", + "futures-util", + "itoa", + "language-tags", + "log", + "mime", + "once_cell", + "pin-project-lite", + "regex", + "regex-lite", + "serde", + "serde_json", + "serde_urlencoded", + "smallvec", + "socket2", + "time", + "url", +] + +[[package]] +name = "actix-web-codegen" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f591380e2e68490b5dfaf1dd1aa0ebe78d84ba7067078512b4ea6e4492d622b8" +dependencies = [ + "actix-router", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "addr2line" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "getrandom", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "backtrace" +version = "0.3.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "brotli" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74f7971dbd9326d58187408ab83117d8ac1bb9c17b085fdacd1cf2f598719b6b" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a45bd2e4095a8b518033b128020dd4a55aab1c0a381ba4404a472630f4bc362" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bytes" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" + +[[package]] +name = "bytestring" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74d80203ea6b29df88012294f62733de21cfeab47f17b41af3a38bc30a03ee72" +dependencies = [ + "bytes", +] + +[[package]] +name = "cc" +version = "1.0.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c51067fd44124faa7f870b4b1c969379ad32b2ba805aa959430ceaa384f695" +dependencies = [ + "jobserver", + "libc", + "once_cell", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "cookie" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "cpufeatures" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derive_more" +version = "0.99.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "encoding_rs" +version = "0.8.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "env_logger" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a12e6657c4c97ebab115a42dcee77225f7f482cdd841cf7088c657a42e9e00e7" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "flate2" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "pin-utils", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" + +[[package]] +name = "h2" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "httparse" +version = "1.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "impl-more" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "206ca75c9c03ba3d4ace2460e57b189f39f43de612c2f85836e65c929701bb2d" + +[[package]] +name = "indexmap" +version = "2.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "jobserver" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2b099aaa34a9751c5bf0878add70444e1ed2dd73f347be99003d4577277de6e" +dependencies = [ + "libc", +] + +[[package]] +name = "language-tags" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" + +[[package]] +name = "libc" +version = "0.2.155" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" + +[[package]] +name = "lobby_server" +version = "0.1.0" +dependencies = [ + "actix-web", + "env_logger", + "log", + "serde", + "serde_json", + "tokio", + "uuid", +] + +[[package]] +name = "local-channel" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6cbc85e69b8df4b8bb8b89ec634e7189099cea8927a276b7384ce5488e53ec8" +dependencies = [ + "futures-core", + "futures-sink", + "local-waker", +] + +[[package]] +name = "local-waker" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.48.0", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi 0.3.9", + "libc", +] + +[[package]] +name = "object" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "576dfe1fc8f9df304abb159d767a29d0476f7750fbf8aa7ad07816004a207434" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "openssl" +version = "0.10.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-sys" +version = "0.9.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.5", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-lite" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" + +[[package]] +name = "regex-syntax" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" + +[[package]] +name = "serde" +version = "1.0.203" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.203" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "socket2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "syn" +version = "2.0.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff8655ed1d86f3af4ee3fd3263786bc14245ad17c4c7e85ba7187fb3ae028c90" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "time" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "num_cpus", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.48.0", +] + +[[package]] +name = "tokio-macros" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-openssl" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ffab79df67727f6acf57f1ff743091873c24c579b1e2ce4d8f53e47ded4d63d" +dependencies = [ + "futures-util", + "openssl", + "openssl-sys", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "log", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unicode-bidi" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-normalization" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "url" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "uuid" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" +dependencies = [ + "getrandom", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.5", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +dependencies = [ + "windows_aarch64_gnullvm 0.52.5", + "windows_aarch64_msvc 0.52.5", + "windows_i686_gnu 0.52.5", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.5", + "windows_x86_64_gnu 0.52.5", + "windows_x86_64_gnullvm 0.52.5", + "windows_x86_64_msvc 0.52.5", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" + +[[package]] +name = "zerocopy" +version = "0.7.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zstd" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d789b1514203a1120ad2429eae43a7bd32b90976a7bb8a05f7ec02fa88cc23a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd99b45c6bc03a018c8b8a86025678c87e55526064e38f9df301989dce7ec0a" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.11+zstd.1.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75652c55c0b6f3e6f12eb786fe1bc960396bf05a1eb3bf1f3691c3610ac2e6d4" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/Lobby Servers/Rust Server/Cargo.toml b/Lobby Servers/Rust Server/Cargo.toml new file mode 100644 index 0000000..8f96b38 --- /dev/null +++ b/Lobby Servers/Rust Server/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "lobby_server" +version = "0.1.0" +edition = "2018" + +[dependencies] +actix-web = "4.0" +tokio = { version = "1", features = ["full"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +log = "0.4" +env_logger = "0.9" +uuid = { version = "1.0", features = ["v4"] } + +[features] +default = ["actix-web/openssl"] \ No newline at end of file diff --git a/Lobby Servers/Rust Server/Read Me.md b/Lobby Servers/Rust Server/Read Me.md new file mode 100644 index 0000000..d654573 --- /dev/null +++ b/Lobby Servers/Rust Server/Read Me.md @@ -0,0 +1,42 @@ +# Lobby Server - Rust + +This is a [Rust](https://www.rust-lang.org/) implementation of the Derail Valley Lobby Server REST API service. The server can be run in either HTTP or HTTPS (SSL) modes (cert and key PEM files will need to be provided for SSL mode). + +## Building the Code + +To build the Lobby Server code, you'll need Rust, Cargo and OpenSSL installed on your system. + + +### Installing OpenSSL (Windows) +OpenSSL can be installed as follows [[source](https://stackoverflow.com/a/61921362)]: +1. Download and extract the latest version of [vcpkg](https://github.com/microsoft/vcpkg/releases/) +2. Run `bootstrap-vcpkg.bat` +3. Run `vcpkg.exe install openssl-windows:x64-windows` +4. Run `vcpkg.exe install openssl:x64-windows-static` +5. Run `vcpkg.exe integrate install` +6. Run `set VCPKGRS_DYNAMIC=1` + +### Building +The code can be built using `cargo build --release` or built and run (for testing purposes) using `cargo run --release` + +## Configuration Parameters +The server can be configured using a `config.json` file; if one is not supplied, the server will create one with the defaults. + +Below are the available parameters along with their defaults: +- `port` (u16): The port number on which the server will listen. Default: `8080` +- `timeout` (u64): The time-out period in seconds for server removal. Default: `120` +- `ssl_enabled` (bool): Whether SSL is enabled. Default: `false` +- `ssl_cert_path` (string): Path to the SSL certificate file. Default: `"cert.pem"` +- `ssl_key_path` (string): Path to the SSL private key file. Default: `"key.pem"` + +To customize these parameters, create a `config.json` file in the project directory with the desired values. Here's an example `config.json`: +```json +{ + "port": 8080, + "timeout": 120, + "ssl_enabled": false, + "ssl_cert_path": "cert.pem", + "ssl_key_path": "key.pem" +} +``` + diff --git a/Lobby Servers/Rust Server/src/main.rs b/Lobby Servers/Rust Server/src/main.rs new file mode 100644 index 0000000..c83f021 --- /dev/null +++ b/Lobby Servers/Rust Server/src/main.rs @@ -0,0 +1,270 @@ +use actix_web::{web, App, HttpResponse, HttpServer, Responder}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::fs::{File}; +use std::io::{Read, Write}; +use std::sync::{Arc, Mutex}; +use tokio::time::{interval, Duration}; +use std::time::{SystemTime, UNIX_EPOCH}; +use env_logger::Env; +use log::{info, error}; +use uuid::Uuid; +use openssl::ssl::{SslAcceptor, SslFiletype, SslMethod}; +use std::path::Path; + +#[derive(Serialize, Deserialize, Clone)] +struct ServerInfo { + ip: String, + port: u16, + server_name: String, + password_protected: bool, + game_mode: u8, + difficulty: u8, + time_passed: String, + current_players: u32, + max_players: u32, + required_mods: String, + game_version: String, + multiplayer_version: String, + server_info: String, + #[serde(skip_serializing)] + last_update: u64, +} + +#[derive(Serialize, Deserialize, Clone)] +struct AddServerResponse { + game_server_id: String, +} + +#[derive(Clone)] +struct AppState { + servers: Arc>>, +} + +#[derive(Serialize, Deserialize, Clone)] +struct Config { + port: u16, + timeout: u64, + ssl_enabled: bool, + ssl_cert_path: String, + ssl_key_path: String, +} + +impl Default for Config { + fn default() -> Self { + Config { + port: 8080, + timeout: 120, + ssl_enabled: false, + ssl_cert_path: String::from("cert.pem"), + ssl_key_path: String::from("key.pem"), + } + } +} + +fn read_or_create_config() -> Config { + let config_path = "config.json"; + let mut config = Config::default(); + + if let Ok(mut file) = File::open(config_path) { + let mut contents = String::new(); + if file.read_to_string(&mut contents).is_ok() { + if let Ok(parsed_config) = serde_json::from_str(&contents) { + config = parsed_config; + } + } + } else { + if let Ok(mut file) = File::create(config_path) { + let _ = file.write_all(serde_json::to_string_pretty(&config).unwrap().as_bytes()); + } + } + + config +} + +#[tokio::main] +async fn main() -> std::io::Result<()> { + env_logger::Builder::from_env(Env::default().default_filter_or("info")).init(); + + let config = read_or_create_config(); + let state = AppState { + servers: Arc::new(Mutex::new(HashMap::new())), + }; + let cleanup_state = state.clone(); + + if config.ssl_enabled { + let ssl_builder = setup_ssl(&config)?; + HttpServer::new(move || { + App::new() + .app_data(web::Data::new(state.clone())) + .route("/add_game_server", web::post().to(add_server)) + .route("/update_game_server", web::post().to(update_server)) + .route("/remove_game_server", web::post().to(remove_server)) + .route("/list_game_servers", web::get().to(list_servers)) + }) + .bind_openssl(format!("0.0.0.0:{}", config.port), move || ssl_builder.clone())? + .run() + .await + } else { + HttpServer::new(move || { + App::new() + .app_data(web::Data::new(state.clone())) + .route("/add_game_server", web::post().to(add_server)) + .route("/update_game_server", web::post().to(update_server)) + .route("/remove_game_server", web::post().to(remove_server)) + .route("/list_game_servers", web::get().to(list_servers)) + }) + .bind(format!("0.0.0.0:{}", config.port))? + .run() + .await + } +} + +fn setup_ssl(config: &Config) -> std::io::Result { + let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls())?; + builder.set_private_key_file(&config.ssl_key_path, SslFiletype::PEM)?; + builder.set_certificate_chain_file(&config.ssl_cert_path)?; + Ok(builder.build()) +} + +fn validate_server_info(info: &ServerInfo) -> Result<(), &'static str> { + if info.server_name.len() > 25 { + return Err("Server name exceeds 25 characters"); + } + if info.server_info.len() > 500 { + return Err("Server info exceeds 500 characters"); + } + if info.current_players > info.max_players { + return Err("Current players exceed max players"); + } + if info.max_players < 1 { + return Err("Max players must be at least 1"); + } + Ok(()) +} + +#[derive(Deserialize)] +struct AddServerRequest { + ip: String, + port: u16, + server_name: String, + password_protected: bool, + game_mode: u8, + difficulty: u8, + time_passed: String, + current_players: u32, + max_players: u32, + required_mods: String, + game_version: String, + multiplayer_version: String, + server_info: String, +} + +async fn add_server(data: web::Data, server_info: web::Json) -> impl Responder { + let info = ServerInfo { + ip: server_info.ip.clone(), + port: server_info.port, + server_name: server_info.server_name.clone(), + password_protected: server_info.password_protected, + game_mode: server_info.game_mode, + difficulty: server_info.difficulty, + time_passed: server_info.time_passed.clone(), + current_players: server_info.current_players, + max_players: server_info.max_players, + required_mods: server_info.required_mods.clone(), + game_version: server_info.game_version.clone(), + multiplayer_version: server_info.multiplayer_version.clone(), + server_info: server_info.server_info.clone(), + last_update: SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(), + }; + + if let Err(e) = validate_server_info(&info) { + error!("Validation failed: {}", e); + return HttpResponse::BadRequest().json(e); + } + + let game_server_id = Uuid::new_v4().to_string(); + let key = game_server_id.clone(); + match data.servers.lock() { + Ok(mut servers) => { + servers.insert(key.clone(), info); + info!("Server added: {}", key); + HttpResponse::Ok().json(AddServerResponse { game_server_id: key }) + } + Err(_) => { + error!("Failed to add server: {}", key); + HttpResponse::InternalServerError().json("Failed to add server") + } + } +} + +#[derive(Deserialize)] +struct UpdateServerRequest { + game_server_id: String, + current_players: u32, + time_passed: String, +} + +async fn update_server(data: web::Data, server_info: web::Json) -> impl Responder { + let mut updated = false; + match data.servers.lock() { + Ok(mut servers) => { + if let Some(info) = servers.get_mut(&server_info.game_server_id) { + if server_info.current_players <= info.max_players { + info.current_players = server_info.current_players; + info.time_passed = server_info.time_passed.clone(); + info.last_update = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(); + updated = true; + } + } + } + Err(_) => { + error!("Failed to update server: {}", server_info.game_server_id); + return HttpResponse::InternalServerError().json("Failed to update server"); + } + } + + if updated { + info!("Server updated: {}", server_info.game_server_id); + HttpResponse::Ok().json("Server updated") + } else { + error!("Server not found or invalid current players: {}", server_info.game_server_id); + HttpResponse::BadRequest().json("Server not found or invalid current players") + } +} + +#[derive(Deserialize)] +struct RemoveServerRequest { + game_server_id: String, +} + +async fn remove_server(data: web::Data, server_info: web::Json) -> impl Responder { + let removed = match data.servers.lock() { + Ok(mut servers) => servers.remove(&server_info.game_server_id).is_some(), + Err(_) => { + error!("Failed to remove server: {}", server_info.game_server_id); + false + } + }; + + if removed { + info!("Server removed: {}", server_info.game_server_id); + HttpResponse::Ok().json("Server removed") + } else { + error!("Server not found: {}", server_info.game_server_id); + HttpResponse::BadRequest().json("Server not found") + } +} + +async fn list_servers(data: web::Data) -> impl Responder { + match data.servers.lock() { + Ok(servers) => { + let servers_list: Vec = servers.values().cloned().collect(); + HttpResponse::Ok().json(servers_list) + } + Err(_) => { + error!("Failed to retrieve servers"); + HttpResponse::InternalServerError().json("Failed to retrieve servers") + } + } +} \ No newline at end of file From c19054565decb32ec10a91720677db78555ca467 Mon Sep 17 00:00:00 2001 From: morm075 <124874578+morm075@users.noreply.github.com> Date: Sat, 22 Jun 2024 20:16:38 +0930 Subject: [PATCH 11/34] another correction --- Multiplayer/Components/MainMenu/MultiplayerPane.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Multiplayer/Components/MainMenu/MultiplayerPane.cs b/Multiplayer/Components/MainMenu/MultiplayerPane.cs index 1ed7502..6837f32 100644 --- a/Multiplayer/Components/MainMenu/MultiplayerPane.cs +++ b/Multiplayer/Components/MainMenu/MultiplayerPane.cs @@ -313,7 +313,7 @@ private void JoinAction() { // Implement join action logic here Debug.Log("Join button clicked."); - // Add your code to handle joining a game + // Add code to handle joining a game } private void SetupListeners(bool on) { From 4b2c6bb503f6c3f1350e17ce5879bce96fd16370 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 23 Jun 2024 10:44:32 +1000 Subject: [PATCH 12/34] Fixed SSL compilation issues --- .gitignore | 1 + Lobby Servers/Rust Server/Cargo.lock | 1 + Lobby Servers/Rust Server/Cargo.toml | 1 + Lobby Servers/Rust Server/config.json | 7 ++++ Lobby Servers/Rust Server/src/main.rs | 47 ++++++++++++--------------- 5 files changed, 30 insertions(+), 27 deletions(-) create mode 100644 Lobby Servers/Rust Server/config.json diff --git a/.gitignore b/.gitignore index 145bee5..d792194 100644 --- a/.gitignore +++ b/.gitignore @@ -307,3 +307,4 @@ MultiplayerAssets/ProjectSettings/* # Packages !MultiplayerAssets/Packages /Lobby Servers/Rust Server/target +*.pem diff --git a/Lobby Servers/Rust Server/Cargo.lock b/Lobby Servers/Rust Server/Cargo.lock index c5fbb5b..2b81e2d 100644 --- a/Lobby Servers/Rust Server/Cargo.lock +++ b/Lobby Servers/Rust Server/Cargo.lock @@ -694,6 +694,7 @@ dependencies = [ "actix-web", "env_logger", "log", + "openssl", "serde", "serde_json", "tokio", diff --git a/Lobby Servers/Rust Server/Cargo.toml b/Lobby Servers/Rust Server/Cargo.toml index 8f96b38..7023cda 100644 --- a/Lobby Servers/Rust Server/Cargo.toml +++ b/Lobby Servers/Rust Server/Cargo.toml @@ -11,6 +11,7 @@ serde_json = "1.0" log = "0.4" env_logger = "0.9" uuid = { version = "1.0", features = ["v4"] } +openssl = "0.10" [features] default = ["actix-web/openssl"] \ No newline at end of file diff --git a/Lobby Servers/Rust Server/config.json b/Lobby Servers/Rust Server/config.json new file mode 100644 index 0000000..e863e8b --- /dev/null +++ b/Lobby Servers/Rust Server/config.json @@ -0,0 +1,7 @@ +{ + "port": 8080, + "timeout": 120, + "ssl_enabled": false, + "ssl_cert_path": "cert.pem", + "ssl_key_path": "key.pem" +} \ No newline at end of file diff --git a/Lobby Servers/Rust Server/src/main.rs b/Lobby Servers/Rust Server/src/main.rs index c83f021..0729ae6 100644 --- a/Lobby Servers/Rust Server/src/main.rs +++ b/Lobby Servers/Rust Server/src/main.rs @@ -1,16 +1,15 @@ use actix_web::{web, App, HttpResponse, HttpServer, Responder}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; -use std::fs::{File}; +use std::fs::File; use std::io::{Read, Write}; use std::sync::{Arc, Mutex}; -use tokio::time::{interval, Duration}; use std::time::{SystemTime, UNIX_EPOCH}; use env_logger::Env; use log::{info, error}; use uuid::Uuid; use openssl::ssl::{SslAcceptor, SslFiletype, SslMethod}; -use std::path::Path; +use openssl::ssl::SslAcceptorBuilder; #[derive(Serialize, Deserialize, Clone)] struct ServerInfo { @@ -90,41 +89,35 @@ async fn main() -> std::io::Result<()> { let state = AppState { servers: Arc::new(Mutex::new(HashMap::new())), }; - let cleanup_state = state.clone(); - if config.ssl_enabled { - let ssl_builder = setup_ssl(&config)?; - HttpServer::new(move || { + let server = { + let server_builder = HttpServer::new(move || { App::new() .app_data(web::Data::new(state.clone())) .route("/add_game_server", web::post().to(add_server)) .route("/update_game_server", web::post().to(update_server)) .route("/remove_game_server", web::post().to(remove_server)) .route("/list_game_servers", web::get().to(list_servers)) - }) - .bind_openssl(format!("0.0.0.0:{}", config.port), move || ssl_builder.clone())? - .run() - .await - } else { - HttpServer::new(move || { - App::new() - .app_data(web::Data::new(state.clone())) - .route("/add_game_server", web::post().to(add_server)) - .route("/update_game_server", web::post().to(update_server)) - .route("/remove_game_server", web::post().to(remove_server)) - .route("/list_game_servers", web::get().to(list_servers)) - }) - .bind(format!("0.0.0.0:{}", config.port))? - .run() - .await - } + }); + + if config.ssl_enabled { + let ssl_builder = setup_ssl(&config)?; + server_builder.bind_openssl(format!("0.0.0.0:{}", config.port), (move || ssl_builder)())? + } else { + server_builder.bind(format!("0.0.0.0:{}", config.port))? + } + }; + + + // Start the server + server.run().await } -fn setup_ssl(config: &Config) -> std::io::Result { +fn setup_ssl(config: &Config) -> std::io::Result { let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls())?; builder.set_private_key_file(&config.ssl_key_path, SslFiletype::PEM)?; builder.set_certificate_chain_file(&config.ssl_cert_path)?; - Ok(builder.build()) + Ok(builder) } fn validate_server_info(info: &ServerInfo) -> Result<(), &'static str> { @@ -267,4 +260,4 @@ async fn list_servers(data: web::Data) -> impl Responder { HttpResponse::InternalServerError().json("Failed to retrieve servers") } } -} \ No newline at end of file +} From 492938ed57775b4a02ccab61491ae12bda2325a1 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 23 Jun 2024 11:53:14 +1000 Subject: [PATCH 13/34] Update Read Me.md --- Lobby Servers/Rust Server/Read Me.md | 31 +++++++++++++++++++++------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/Lobby Servers/Rust Server/Read Me.md b/Lobby Servers/Rust Server/Read Me.md index d654573..e8d937b 100644 --- a/Lobby Servers/Rust Server/Read Me.md +++ b/Lobby Servers/Rust Server/Read Me.md @@ -1,3 +1,4 @@ + # Lobby Server - Rust This is a [Rust](https://www.rust-lang.org/) implementation of the Derail Valley Lobby Server REST API service. The server can be run in either HTTP or HTTPS (SSL) modes (cert and key PEM files will need to be provided for SSL mode). @@ -8,13 +9,26 @@ To build the Lobby Server code, you'll need Rust, Cargo and OpenSSL installed on ### Installing OpenSSL (Windows) -OpenSSL can be installed as follows [[source](https://stackoverflow.com/a/61921362)]: -1. Download and extract the latest version of [vcpkg](https://github.com/microsoft/vcpkg/releases/) -2. Run `bootstrap-vcpkg.bat` -3. Run `vcpkg.exe install openssl-windows:x64-windows` -4. Run `vcpkg.exe install openssl:x64-windows-static` -5. Run `vcpkg.exe integrate install` -6. Run `set VCPKGRS_DYNAMIC=1` +OpenSSL can be installed as follows [[source](https://stackoverflow.com/a/70949736)]: +1. Install OpenSSL from [http://slproweb.com/products/Win32OpenSSL.html](http://slproweb.com/products/Win32OpenSSL.html) into `C:\Program Files\OpenSSL-Win64` +2. In an elevated terminal +``` +$env:path = $env:path+ ";C:\Program Files\OpenSSL-Win64\bin" +cd "C:\Program Files\OpenSSL-Win64" +mkdir certs +cd certs +wget https://curl.se/ca/cacert.pem -o cacert.pem +``` +4. In the VSCode Rust Server terminal set the following environment variables +``` +$env:OPENSSL_CONF='C:\Program Files\OpenSSL-Win64\bin\openssl.cfg' +$env:OPENSSL_NO_VENDOR=1 +$env:RUSTFLAGS='-Ctarget-feature=+crt-static' +$env:SSL_CERT = 'C:\Program Files\OpenSSL-Win64\certs\cacert.pem' +$env:OPENSSL_DIR = 'C:\Program Files\OpenSSL-Win64' +$env:OPENSSL_LIB_DIR = "C:\Program Files\OpenSSL-Win64\lib\VC\x64\MD" +``` + ### Building The code can be built using `cargo build --release` or built and run (for testing purposes) using `cargo run --release` @@ -29,7 +43,8 @@ Below are the available parameters along with their defaults: - `ssl_cert_path` (string): Path to the SSL certificate file. Default: `"cert.pem"` - `ssl_key_path` (string): Path to the SSL private key file. Default: `"key.pem"` -To customize these parameters, create a `config.json` file in the project directory with the desired values. Here's an example `config.json`: +To customise these parameters, create a `config.json` file in the project directory with the desired values. +Example `config.json`: ```json { "port": 8080, From 94f344f0da46135fbe05064250218b686aab3401 Mon Sep 17 00:00:00 2001 From: AMacro Date: Thu, 27 Jun 2024 22:29:00 +1000 Subject: [PATCH 14/34] Improved servers and server browser code Updated API spec to include private_key requirements Modularised the Rust server and compliance to new spec Updated the PHP server to comply with new spec, additional config to allow flatfile and MySQL databases. Added ReadMe. ServerBrowser major refactor. Now loads data from the lobby server --- .../PHP Server/DatabaseInterface.php | 10 + Lobby Servers/PHP Server/FlatfileDatabase.php | 96 +++++ Lobby Servers/PHP Server/MySQLDatabase.php | 74 ++++ Lobby Servers/PHP Server/Read Me.md | 146 +++++++ Lobby Servers/PHP Server/config.php | 4 +- Lobby Servers/PHP Server/index.php | 181 +++------ Lobby Servers/PHP Server/install.php | 54 +++ Lobby Servers/RestAPI.md | 21 +- Lobby Servers/Rust Server/Cargo.lock | 1 + Lobby Servers/Rust Server/Cargo.toml | 1 + Lobby Servers/Rust Server/Read Me.md | 1 - Lobby Servers/Rust Server/src/config.rs | 44 +++ Lobby Servers/Rust Server/src/handlers.rs | 175 +++++++++ Lobby Servers/Rust Server/src/main.rs | 291 +++----------- Lobby Servers/Rust Server/src/server.rs | 62 +++ Lobby Servers/Rust Server/src/ssl.rs | 10 + Lobby Servers/Rust Server/src/state.rs | 7 + Lobby Servers/Rust Server/src/utils.rs | 8 + .../Components/MainMenu/HostGamePane.cs | 77 ++++ .../IServerBrowserGameDetails.cs | 23 +- ...pupTextInputFieldControllerNoValidation.cs | 0 .../ServerBrowserElement.cs | 4 +- .../ServerBrowserGridView.cs | 1 + ...ultiplayerPane.cs => ServerBrowserPane.cs} | 369 ++++++++++++------ Multiplayer/Multiplayer.csproj | 2 +- Multiplayer/Networking/Data/ServerData.cs | 67 ++++ .../MainMenu/LauncherControllerPatch.cs | 72 ++++ .../MainMenu/RightPaneControllerPatch.cs | 81 ++-- Multiplayer/Settings.cs | 5 +- Multiplayer/Utils/DvExtensions.cs | 55 +++ 30 files changed, 1397 insertions(+), 545 deletions(-) create mode 100644 Lobby Servers/PHP Server/DatabaseInterface.php create mode 100644 Lobby Servers/PHP Server/FlatfileDatabase.php create mode 100644 Lobby Servers/PHP Server/MySQLDatabase.php create mode 100644 Lobby Servers/PHP Server/Read Me.md create mode 100644 Lobby Servers/PHP Server/install.php create mode 100644 Lobby Servers/Rust Server/src/config.rs create mode 100644 Lobby Servers/Rust Server/src/handlers.rs create mode 100644 Lobby Servers/Rust Server/src/server.rs create mode 100644 Lobby Servers/Rust Server/src/ssl.rs create mode 100644 Lobby Servers/Rust Server/src/state.rs create mode 100644 Lobby Servers/Rust Server/src/utils.rs create mode 100644 Multiplayer/Components/MainMenu/HostGamePane.cs rename Multiplayer/Components/MainMenu/{ => ServerBrowser}/IServerBrowserGameDetails.cs (54%) rename Multiplayer/Components/MainMenu/{ => ServerBrowser}/PopupTextInputFieldControllerNoValidation.cs (100%) rename Multiplayer/Components/MainMenu/{ => ServerBrowser}/ServerBrowserElement.cs (95%) rename Multiplayer/Components/MainMenu/{ => ServerBrowser}/ServerBrowserGridView.cs (94%) rename Multiplayer/Components/MainMenu/{MultiplayerPane.cs => ServerBrowserPane.cs} (58%) create mode 100644 Multiplayer/Networking/Data/ServerData.cs create mode 100644 Multiplayer/Patches/MainMenu/LauncherControllerPatch.cs diff --git a/Lobby Servers/PHP Server/DatabaseInterface.php b/Lobby Servers/PHP Server/DatabaseInterface.php new file mode 100644 index 0000000..ae751d4 --- /dev/null +++ b/Lobby Servers/PHP Server/DatabaseInterface.php @@ -0,0 +1,10 @@ + diff --git a/Lobby Servers/PHP Server/FlatfileDatabase.php b/Lobby Servers/PHP Server/FlatfileDatabase.php new file mode 100644 index 0000000..13f7566 --- /dev/null +++ b/Lobby Servers/PHP Server/FlatfileDatabase.php @@ -0,0 +1,96 @@ +filePath = $dbConfig['flatfile_path']; + } + + private function readData() { + if (!file_exists($this->filePath)) { + return []; + } + return json_decode(file_get_contents($this->filePath), true) ?? []; + } + + private function writeData($data) { + file_put_contents($this->filePath, json_encode($data, JSON_PRETTY_PRINT)); + } + + public function addGameServer($data) { + $data['last_update'] = time(); // Set current time as last_update + + $servers = $this->readData(); + $servers[] = $data; + $this->writeData($servers); + + return json_encode(["game_server_id" => $data['game_server_id']]); + } + + public function updateGameServer($data) { + $servers = $this->readData(); + $updated = false; + + foreach ($servers as &$server) { + if ($server['game_server_id'] === $data['game_server_id']) { + $server['current_players'] = $data['current_players']; + $server['time_passed'] = $data['time_passed']; + $server['last_update'] = time(); // Update with current time + $updated = true; + break; + } + } + + if ($updated) { + $this->writeData($servers); + return json_encode(["message" => "Server updated"]); + } else { + return json_encode(["error" => "Failed to update server"]); + } + } + + public function removeGameServer($data) { + $servers = $this->readData(); + $servers = array_filter($servers, function($server) use ($data) { + return $server['game_server_id'] !== $data['game_server_id']; + }); + $this->writeData(array_values($servers)); + return json_encode(["message" => "Server removed"]); + } + + public function listGameServers() { + $servers = $this->readData(); + $current_time = time(); + $active_servers = []; + $changed = false; + + foreach ($servers as $key => $server) { + if ($current_time - $server['last_update'] <= TIMEOUT) { + $active_servers[] = $server; + } else { + $changed = true; // Indicates there's a change if any server is removed + } + } + + if ($changed) { + $this->writeData($active_servers); // Write back only if there are changes + } + + return json_encode($active_servers); + } + + + + public function getGameServer($game_server_id) { + $servers = $this->readData(); + foreach ($servers as $server) { + if ($server['game_server_id'] === $game_server_id) { + return json_encode($server); + } + } + return json_encode(null); + } +} + +?> diff --git a/Lobby Servers/PHP Server/MySQLDatabase.php b/Lobby Servers/PHP Server/MySQLDatabase.php new file mode 100644 index 0000000..b92119d --- /dev/null +++ b/Lobby Servers/PHP Server/MySQLDatabase.php @@ -0,0 +1,74 @@ +pdo = new PDO("mysql:host={$dbConfig['host']};dbname={$dbConfig['dbname']}", $dbConfig['username'], $dbConfig['password']); + $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + } + + public function addGameServer($data) { + $stmt = $this->pdo->prepare("INSERT INTO game_servers (game_server_id, private_key, ip, port, server_name, password_protected, game_mode, difficulty, time_passed, current_players, max_players, required_mods, game_version, multiplayer_version, server_info, last_update) + VALUES (:game_server_id, :private_key, :ip, :port, :server_name, :password_protected, :game_mode, :difficulty, :time_passed, :current_players, :max_players, :required_mods, :game_version, :multiplayer_version, :server_info, :last_update)"); + $stmt->execute([ + ':game_server_id' => $data['game_server_id'], + ':private_key' => $data['private_key'], + ':ip' => $data['ip'], + ':port' => $data['port'], + ':server_name' => $data['server_name'], + ':password_protected' => $data['password_protected'], + ':game_mode' => $data['game_mode'], + ':difficulty' => $data['difficulty'], + ':time_passed' => $data['time_passed'], + ':current_players' => $data['current_players'], + ':max_players' => $data['max_players'], + ':required_mods' => $data['required_mods'], + ':game_version' => $data['game_version'], + ':multiplayer_version' => $data['multiplayer_version'], + ':server_info' => $data['server_info'], + ':last_update' => time() //use current time + ]); + return json_encode(["game_server_id" => $data['game_server_id']]); + } + + public function updateGameServer($data) { + $stmt = $this->pdo->prepare("UPDATE game_servers + SET current_players = :current_players, time_passed = :time_passed, last_update = :last_update + WHERE game_server_id = :game_server_id"); + $stmt->execute([ + ':current_players' => $data['current_players'], + ':time_passed' => $data['time_passed'], + ':last_update' => time(), // Update with current time + ':game_server_id' => $data['game_server_id'] + ]); + + return $stmt->rowCount() > 0 ? json_encode(["message" => "Server updated"]) : json_encode(["error" => "Failed to update server"]); + } + + public function removeGameServer($data) { + $stmt = $this->pdo->prepare("DELETE FROM game_servers WHERE game_server_id = :game_server_id"); + $stmt->execute([':game_server_id' => $data['game_server_id']]); + return $stmt->rowCount() > 0 ? json_encode(["message" => "Server removed"]) : json_encode(["error" => "Failed to remove server"]); + } + + public function listGameServers() { + // Remove servers that exceed TIMEOUT directly in the SQL query + $stmt = $this->pdo->prepare("DELETE FROM game_servers WHERE last_update < :timeout"); + $stmt->execute([':timeout' => time() - TIMEOUT]); + + // Fetch remaining servers + $stmt = $this->pdo->query("SELECT * FROM game_servers"); + $servers = $stmt->fetchAll(PDO::FETCH_ASSOC); + + return json_encode($servers); + } + + public function getGameServer($game_server_id) { + $stmt = $this->pdo->prepare("SELECT * FROM game_servers WHERE game_server_id = :game_server_id"); + $stmt->execute([':game_server_id' => $game_server_id]); + return json_encode($stmt->fetch(PDO::FETCH_ASSOC)); + } +} + +?> diff --git a/Lobby Servers/PHP Server/Read Me.md b/Lobby Servers/PHP Server/Read Me.md new file mode 100644 index 0000000..77e65ba --- /dev/null +++ b/Lobby Servers/PHP Server/Read Me.md @@ -0,0 +1,146 @@ +# Lobby Server - PHP + +This is a PHP implementation of the Derail Valley Lobby Server REST API service. It is designed to run on any standard web hosting and does not rely on long-running/persistent behaviour. +HTTPS support depends on the configuration of the hosting environment. + +As this implementation is not persistent in memory, a database is used to store server information. Two options are available for the database engine - a JSON based flatfile or a MySQL database. + +## Installing + +1. Copy the following files to your public html folder (consult your web server/web host's documentation) +``` +index.php +DatabaseInterface.php +FlatfileDatabase.php +MySQLDatabase.php +``` +2. Copy `config.php` to a secure location outside of your public html directory +3. Edit `index.php` and update the path to the config file on line 2: +```php + 'mysql', + 'host' => 'localhost', + 'dbname' => 'dv_lobby', + 'username' => 'dv_lobby_server', + 'password' => 'n16O5+LMpeqI`{E', + 'flatfile_path' => '' // Path to store the flatfile database +]; +?> +``` + +Example `config.php` using Flatfile: +```php + 'flatfile', + 'host' => '', + 'dbname' => '', + 'username' => '', + 'password' => '', + 'flatfile_path' => '/dv_lobby/flatfile.db' // Path to store the flatfile database +]; +?> +``` + +## Security Considerations +This is a non-comprehensive overview of security considerations. You should always use up-to-date best practices and seek professional advice where required. + +### Environment variables +Consider using environment variables to store sensitive database credentials (e.g. `dbConfig`.`host`, `dbConfig`.`dbname`, `dbConfig`.`username`, `dbConfig`.`password`) instead of hardcoding them in config.php. +Your `config.php` can be updated to reference the environment variables. + +Example: +```php +$dbConfig = [ + 'type' => 'mysql', + 'host' => getenv('DB_HOST'), + 'dbname' => getenv('DB_NAME'), + 'username' => getenv('DB_USER'), + 'password' => getenv('DB_PASSWORD'), + 'flatfile_path' => '/path/to/flatfile.db' +]; +``` + + +### File Permissions +Ensure that `config.php` and any other sensitive files outside the web root are only readable by the web server user (chmod 600). +For directories containing flatfile databases, restrict permissions (chmod 700 or 750) to prevent unauthorised access. + +### HTTPS (SSL) +Configure your server to use https. Many web hosts provide free SSL certificates via Let's Encrypt. +Consider forcing https via server config/`.httaccess`. + +Example: +```apacheconf +RewriteEngine On +RewriteCond %{HTTPS} off +RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301] +``` diff --git a/Lobby Servers/PHP Server/config.php b/Lobby Servers/PHP Server/config.php index f4942fd..52073ea 100644 --- a/Lobby Servers/PHP Server/config.php +++ b/Lobby Servers/PHP Server/config.php @@ -5,10 +5,12 @@ // Database configuration $dbConfig = [ + 'type' => 'mysql', // Change to 'flatfile' to use flatfile database 'host' => 'localhost', 'dbname' => 'your_database', 'username' => 'your_username', - 'password' => 'your_password' + 'password' => 'your_password', + 'flatfile_path' => '/path/to/flatfile.db' // Path to store the flatfile database ]; ?> \ No newline at end of file diff --git a/Lobby Servers/PHP Server/index.php b/Lobby Servers/PHP Server/index.php index 2a57cf3..ca44d4f 100644 --- a/Lobby Servers/PHP Server/index.php +++ b/Lobby Servers/PHP Server/index.php @@ -1,47 +1,42 @@ setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - - // Now you can use $pdo to execute queries -} catch (PDOException $e) { - // Handle database connection errors - echo "Connection failed: " . $e->getMessage(); +// Determine the database type and include the appropriate module +switch ($dbConfig['type']) { + case 'mysql': + include 'MySQLDatabase.php'; + $db = new MySQLDatabase($dbConfig); + break; + case 'flatfile': + include 'FlatfileDatabase.php'; + $db = new FlatfileDatabase($dbConfig); + break; + default: + die('Unsupported database type'); } - // Define routes if ($_SERVER['REQUEST_METHOD'] === 'POST') { - - $data = json_decode(file_get_contents('php://input'), true); - + $data = json_decode(file_get_contents('php://input'), true); + switch ($_SERVER['REQUEST_URI']) { case '/add_game_server': - echo add_game_server($pdo, $data); + echo add_game_server($db, $data); break; - case '/update_game_server': - echo update_game_server($pdo, $data); + echo update_game_server($db, $data); break; - case '/remove_game_server': - echo remove_game_server($pdo, $data); + echo remove_game_server($db, $data); break; - default: http_response_code(404); break; } - + } elseif ($_SERVER['REQUEST_METHOD'] === 'GET') { if ($_SERVER['REQUEST_URI'] === '/list_game_servers') { - echo list_game_servers($pdo); + echo list_game_servers($db); } else { http_response_code(404); } @@ -49,123 +44,61 @@ http_response_code(405); // Method Not Allowed } - -function add_game_server($pdo, $data) { - // Validation +function add_game_server($db, $data) { if (!validate_server_info($data)) { return json_encode(["error" => "Invalid server information"]); } - // Generate a UUID for the game server - $game_server_id = uuid_create(); - - // Insert server information into the database - $stmt = $pdo->prepare("INSERT INTO game_servers (game_server_id, ip, port, server_name, password_protected, game_mode, difficulty, time_passed, current_players, max_players, required_mods, game_version, multiplayer_version, server_info, last_update) - VALUES (:game_server_id, :ip, :port, :server_name, :password_protected, :game_mode, :difficulty, :time_passed, :current_players, :max_players, :required_mods, :game_version, :multiplayer_version, :server_info, :last_update)"); - $stmt->execute([ - ':game_server_id' => $game_server_id, - ':ip' => $data['ip'], - ':port' => $data['port'], - ':server_name' => $data['server_name'], - ':password_protected' => $data['password_protected'], - ':game_mode' => $data['game_mode'], - ':difficulty' => $data['difficulty'], - ':time_passed' => $data['time_passed'], - ':current_players' => $data['current_players'], - ':max_players' => $data['max_players'], - ':required_mods' => $data['required_mods'], - ':game_version' => $data['game_version'], - ':multiplayer_version' => $data['multiplayer_version'], - ':server_info' => $data['server_info'], - ':last_update' => time() // Assuming Unix timestamp for last_update - ]); - - // Return game server ID - return json_encode(["game_server_id" => $game_server_id]); -} - + $data['game_server_id'] = uuid_create(); + $data['private_key'] = generate_private_key(); -function update_game_server($pdo, $data) { - // Update current players count and time passed for the specified game server - $stmt = $pdo->prepare("UPDATE game_servers - SET current_players = :current_players, time_passed = :time_passed, last_update = :last_update - WHERE game_server_id = :game_server_id"); - $stmt->execute([ - ':current_players' => $data['current_players'], - ':time_passed' => $data['time_passed'], - ':last_update' => time(), // Assuming Unix timestamp for last_update - ':game_server_id' => $data['game_server_id'] - ]); - - // Check if update was successful - if ($stmt->rowCount() > 0) { - return json_encode(["message" => "Server updated"]); - } else { - return json_encode(["error" => "Failed to update server"]); + if (!isset($data['ip']) || !filter_var($data['ip'], FILTER_VALIDATE_IP)) { + $data['ip'] = $_SERVER['REMOTE_ADDR']; } -} - -function remove_game_server($pdo, $data) { - // Delete the specified game server from the database - $stmt = $pdo->prepare("DELETE FROM game_servers WHERE game_server_id = :game_server_id"); - $stmt->execute([':game_server_id' => $data['game_server_id']]); + $data['last_update'] = time(); - // Check if deletion was successful - if ($stmt->rowCount() > 0) { - return json_encode(["message" => "Server removed"]); - } else { - return json_encode(["error" => "Failed to remove server"]); - } + return $db->addGameServer($data); } +function update_game_server($db, $data) { + if (!validate_server_update($db, $data)) { + return json_encode(["error" => "Invalid game server ID or private key"]); + } -function list_game_servers($pdo) { - // Retrieve the list of game servers from the database - $stmt = $pdo->query("SELECT * FROM game_servers"); - $servers = $stmt->fetchAll(PDO::FETCH_ASSOC); - - // Return the list of game servers - return json_encode($servers); + $data['last_update'] = time(); + return $db->updateGameServer($data); } - -/* - ************************************** - - Helper functions - - ************************************* -*/ - - -function validate_server_info($data) { - // Check if server name length exceeds 25 characters - if (strlen($data['server_name']) > 25) { - return false; +function remove_game_server($db, $data) { + if (!validate_server_update($db, $data)) { + return json_encode(["error" => "Invalid game server ID or private key"]); } - // Check if server info length exceeds 500 characters - if (strlen($data['server_info']) > 500) { - return false; - } + return $db->removeGameServer($data); +} - // Check if current players exceed max players - if ($data['current_players'] > $data['max_players']) { - return false; +function list_game_servers($db) { + $servers = json_decode($db->listGameServers(), true); + // Remove private keys from the servers before returning + foreach ($servers as &$server) { + unset($server['private_key']); } + return json_encode($servers); +} - // Check if max players is at least 1 - if ($data['max_players'] < 1) { +function validate_server_info($data) { + if (strlen($data['server_name']) > 25 || strlen($data['server_info']) > 500 || $data['current_players'] > $data['max_players'] || $data['max_players'] < 1) { return false; } - - // If all checks pass, return true return true; } +function validate_server_update($db, $data) { + $server = json_decode($db->getGameServer($data['game_server_id']), true); + return $server && $server['private_key'] === $data['private_key']; +} -// Function to generate UUID function uuid_create() { return sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x', mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff), @@ -173,4 +106,16 @@ function uuid_create() { mt_rand(0, 0x3fff) | 0x8000, mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff) ); -} \ No newline at end of file +} + +function generate_private_key() { + // Generate a 128-bit (16 bytes) random binary string + $random_bytes = random_bytes(16); + + // Convert the binary string to a hexadecimal representation + $private_key = bin2hex($random_bytes); + + return $private_key; +} + +?> diff --git a/Lobby Servers/PHP Server/install.php b/Lobby Servers/PHP Server/install.php new file mode 100644 index 0000000..f383314 --- /dev/null +++ b/Lobby Servers/PHP Server/install.php @@ -0,0 +1,54 @@ +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + + // Create the database if it doesn't exist + $sql = "CREATE DATABASE IF NOT EXISTS " . $dbConfig['dbname']; + $pdo->exec($sql); + echo "Database created successfully.
"; + + // Connect to the newly created database + $dsn = 'mysql:host=' . $dbConfig['host'] . ';dbname=' . $dbConfig['dbname']; + $pdo = new PDO($dsn, $dbConfig['username'], $dbConfig['password']); + $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + + // Create the game_servers table + $sql = " + CREATE TABLE IF NOT EXISTS game_servers ( + game_server_id VARCHAR(50) PRIMARY KEY, + private_key VARCHAR(255) NOT NULL, + ip VARCHAR(45) NOT NULL, + port INT NOT NULL, + server_name VARCHAR(100) NOT NULL, + password_protected BOOLEAN NOT NULL, + game_mode VARCHAR(50) NOT NULL, + difficulty VARCHAR(50) NOT NULL, + time_passed INT NOT NULL, + current_players INT NOT NULL, + max_players INT NOT NULL, + required_mods TEXT NOT NULL, + game_version VARCHAR(50) NOT NULL, + multiplayer_version VARCHAR(50) NOT NULL, + server_info TEXT NOT NULL, + last_update INT NOT NULL + ); + "; + + // Execute the SQL to create the table + $pdo->exec($sql); + echo "Table 'game_servers' created successfully.
"; + +} catch (PDOException $e) { + die("DB ERROR: " . $e->getMessage()); +} +?> diff --git a/Lobby Servers/RestAPI.md b/Lobby Servers/RestAPI.md index 927c696..ce5ed94 100644 --- a/Lobby Servers/RestAPI.md +++ b/Lobby Servers/RestAPI.md @@ -42,7 +42,7 @@ The difficulty field in the request body for adding a game server must be one of "password_protected": "boolean", "game_mode": "integer", "difficulty": "integer", - "time_passed": "string" + "time_passed": "string", "current_players": "integer", "max_players": "integer", "required_mods": "string", @@ -52,7 +52,7 @@ The difficulty field in the request body for adding a game server must be one of } ``` - **Fields:** - - ip (string): The IP address of the game server. + - ip (optional string): The IP address of the game server. If not supplied, the requestor's IP shall be used. - port (integer): The port number of the game server. - server_name (string): The name of the game server (maximum 25 characters). - password_protected (boolean): Indicates if the server is password-protected. @@ -72,10 +72,12 @@ The difficulty field in the request body for adding a game server must be one of - **Content:** ```json { - "game_server_id": "string" + "game_server_id": "string", + "private_key": "string" } ``` - game_server_id (string): A GUID assigned to the game server. This GUID uniquely identifies the game server and is used when updating the lobby server. + - private_key (string): A shared secret between the lobby server and the game server. Must be supplied when updating the lobby server. - **Error:** - **Code:** 500 Internal Server Error - **Content:** `"Failed to add server"` @@ -89,12 +91,14 @@ The difficulty field in the request body for adding a game server must be one of ```json { "game_server_id": "string", + "private_key": "string", "current_players": "integer", "time_passed": "string" } ``` - **Fields:** - game_server_id (string): The GUID assigned to the game server (returned from `add_game_server`). + - private_key (string): The shared secret between the lobby server and the game server (returned from `add_game_server`). - current_players (integer): The current number of players on the server (0 - max_players). - time_passed (string): The in-game time passed since the game/session was started. - **Response:** @@ -113,11 +117,13 @@ The difficulty field in the request body for adding a game server must be one of - **Request Body:** ```json { - "game_server_id": "string" + "game_server_id": "string", + "private_key": "string" } ``` - **Fields:** - game_server_id (string): The GUID assigned to the game server (returned from `add_game_server`). + - private_key (string): The shared secret between the lobby server and the game server (returned from `add_game_server`). - **Response:** - **Success:** - **Code:** 200 OK @@ -183,7 +189,8 @@ curl -X POST -H "Content-Type: application/json" -d '{ Example response: ```json { - "game_server_id": "0e1759fd-ba6e-4476-ace2-f173af9db342" + "game_server_id": "0e1759fd-ba6e-4476-ace2-f173af9db342", + "private_key": "6fca6e1499dab0358f79dc0b251b4e23" } ``` @@ -192,6 +199,7 @@ Example request: ```bash curl -X POST -H "Content-Type: application/json" -d '{ "game_server_id": "0e1759fd-ba6e-4476-ace2-f173af9db342", + "private_key": "6fca6e1499dab0358f79dc0b251b4e23", "current_players": 2, "time_passed": "0d 10h 47m 12s" }' http:///update_game_server @@ -206,7 +214,8 @@ Example response: Example request: ```bash curl -X POST -H "Content-Type: application/json" -d '{ - "game_server_id": "0e1759fd-ba6e-4476-ace2-f173af9db342" + "game_server_id": "0e1759fd-ba6e-4476-ace2-f173af9db342", + "private_key": "6fca6e1499dab0358f79dc0b251b4e23" }' http:///remove_game_server ``` Example response: diff --git a/Lobby Servers/Rust Server/Cargo.lock b/Lobby Servers/Rust Server/Cargo.lock index 2b81e2d..f80e1d8 100644 --- a/Lobby Servers/Rust Server/Cargo.lock +++ b/Lobby Servers/Rust Server/Cargo.lock @@ -695,6 +695,7 @@ dependencies = [ "env_logger", "log", "openssl", + "rand", "serde", "serde_json", "tokio", diff --git a/Lobby Servers/Rust Server/Cargo.toml b/Lobby Servers/Rust Server/Cargo.toml index 7023cda..2e80b78 100644 --- a/Lobby Servers/Rust Server/Cargo.toml +++ b/Lobby Servers/Rust Server/Cargo.toml @@ -12,6 +12,7 @@ log = "0.4" env_logger = "0.9" uuid = { version = "1.0", features = ["v4"] } openssl = "0.10" +rand = "0.8" [features] default = ["actix-web/openssl"] \ No newline at end of file diff --git a/Lobby Servers/Rust Server/Read Me.md b/Lobby Servers/Rust Server/Read Me.md index e8d937b..db84e87 100644 --- a/Lobby Servers/Rust Server/Read Me.md +++ b/Lobby Servers/Rust Server/Read Me.md @@ -1,4 +1,3 @@ - # Lobby Server - Rust This is a [Rust](https://www.rust-lang.org/) implementation of the Derail Valley Lobby Server REST API service. The server can be run in either HTTP or HTTPS (SSL) modes (cert and key PEM files will need to be provided for SSL mode). diff --git a/Lobby Servers/Rust Server/src/config.rs b/Lobby Servers/Rust Server/src/config.rs new file mode 100644 index 0000000..bc25a1f --- /dev/null +++ b/Lobby Servers/Rust Server/src/config.rs @@ -0,0 +1,44 @@ +use serde::{Deserialize, Serialize}; +use std::fs::File; +use std::io::{Read, Write}; + +#[derive(Serialize, Deserialize, Clone)] +pub struct Config { + pub port: u16, + pub timeout: u64, + pub ssl_enabled: bool, + pub ssl_cert_path: String, + pub ssl_key_path: String, +} + +impl Default for Config { + fn default() -> Self { + Config { + port: 8080, + timeout: 120, + ssl_enabled: false, + ssl_cert_path: String::from("cert.pem"), + ssl_key_path: String::from("key.pem"), + } + } +} + +pub fn read_or_create_config() -> Config { + let config_path = "config.json"; + let mut config = Config::default(); + + if let Ok(mut file) = File::open(config_path) { + let mut contents = String::new(); + if file.read_to_string(&mut contents).is_ok() { + if let Ok(parsed_config) = serde_json::from_str(&contents) { + config = parsed_config; + } + } + } else { + if let Ok(mut file) = File::create(config_path) { + let _ = file.write_all(serde_json::to_string_pretty(&config).unwrap().as_bytes()); + } + } + + config +} diff --git a/Lobby Servers/Rust Server/src/handlers.rs b/Lobby Servers/Rust Server/src/handlers.rs new file mode 100644 index 0000000..71bc9a5 --- /dev/null +++ b/Lobby Servers/Rust Server/src/handlers.rs @@ -0,0 +1,175 @@ +use actix_web::{web, HttpResponse, HttpRequest, Responder}; +use serde::{Deserialize, Serialize}; +use crate::state::AppState; +use crate::server::{ServerInfo, PublicServerInfo, AddServerResponse, validate_server_info}; +use crate::utils::generate_private_key; +use uuid::Uuid; + +#[derive(Deserialize)] +pub struct AddServerRequest { + pub ip: Option, + pub port: u16, + pub server_name: String, + pub password_protected: bool, + pub game_mode: u8, + pub difficulty: u8, + pub time_passed: String, + pub current_players: u32, + pub max_players: u32, + pub required_mods: String, + pub game_version: String, + pub multiplayer_version: String, + pub server_info: String, +} + +pub async fn add_server(data: web::Data, server_info: web::Json, req: HttpRequest) -> impl Responder { + let client_ip = req.connection_info().realip_remote_addr().unwrap_or("unknown").to_string(); + + let ip = match server_info.ip.as_deref() { + Some(ip_str) => { + // Attempt to parse the IP address + match ip_str.parse::() { + Ok(_) => ip_str.to_string(), // Valid IP address, use it + Err(_) => client_ip.clone(), // Invalid IP address, use client IP + } + }, + None => client_ip.clone(), // server_info.ip is absent, use client IP + }; + + let private_key = generate_private_key(); // Generate a private key + let info = ServerInfo { + ip, + port: server_info.port, + server_name: server_info.server_name.clone(), + password_protected: server_info.password_protected, + game_mode: server_info.game_mode, + difficulty: server_info.difficulty, + time_passed: server_info.time_passed.clone(), + current_players: server_info.current_players, + max_players: server_info.max_players, + required_mods: server_info.required_mods.clone(), + game_version: server_info.game_version.clone(), + multiplayer_version: server_info.multiplayer_version.clone(), + server_info: server_info.server_info.clone(), + last_update: std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs(), + private_key: private_key.clone(), + }; + + if let Err(e) = validate_server_info(&info) { + log::error!("Validation failed: {}", e); + return HttpResponse::BadRequest().json(e); + } + + let game_server_id = Uuid::new_v4().to_string(); + let key = game_server_id.clone(); + match data.servers.lock() { + Ok(mut servers) => { + servers.insert(key.clone(), info); + log::info!("Server added: {}", key); + HttpResponse::Ok().json(AddServerResponse { game_server_id: key, private_key }) + } + Err(_) => { + log::error!("Failed to add server: {}", key); + HttpResponse::InternalServerError().json("Failed to add server") + } + } +} + +#[derive(Deserialize)] +pub struct UpdateServerRequest { + pub game_server_id: String, + pub private_key: String, + pub current_players: u32, + pub time_passed: String, +} + +pub async fn update_server(data: web::Data, server_info: web::Json) -> impl Responder { + let mut updated = false; + match data.servers.lock() { + Ok(mut servers) => { + if let Some(info) = servers.get_mut(&server_info.game_server_id) { + if info.private_key == server_info.private_key { + if server_info.current_players <= info.max_players { + info.current_players = server_info.current_players; + info.time_passed = server_info.time_passed.clone(); + info.last_update = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs(); + updated = true; + } + } else { + return HttpResponse::Unauthorized().json("Invalid private key"); + } + } + } + Err(_) => { + log::error!("Failed to update server: {}", server_info.game_server_id); + return HttpResponse::InternalServerError().json("Failed to update server"); + } + } + + if updated { + log::info!("Server updated: {}", server_info.game_server_id); + HttpResponse::Ok().json("Server updated") + } else { + log::error!("Server not found or invalid current players: {}", server_info.game_server_id); + HttpResponse::BadRequest().json("Server not found or invalid current players") + } +} + +#[derive(Deserialize)] +pub struct RemoveServerRequest { + pub game_server_id: String, + pub private_key: String, +} + +pub async fn remove_server(data: web::Data, server_info: web::Json) -> impl Responder { + let mut removed = false; + match data.servers.lock() { + Ok(mut servers) => { + if let Some(info) = servers.get(&server_info.game_server_id) { + if info.private_key == server_info.private_key { + servers.remove(&server_info.game_server_id); + removed = true; + } else { + return HttpResponse::Unauthorized().json("Invalid private key"); + } + } + } + Err(_) => { + log::error!("Failed to remove server: {}", server_info.game_server_id); + return HttpResponse::InternalServerError().json("Failed to remove server"); + } + }; + + if removed { + log::info!("Server removed: {}", server_info.game_server_id); + HttpResponse::Ok().json("Server removed") + } else { + log::error!("Server not found: {}", server_info.game_server_id); + HttpResponse::BadRequest().json("Server not found or invalid private key") + } +} + +pub async fn list_servers(data: web::Data) -> impl Responder { + match data.servers.lock() { + Ok(servers) => { + let public_servers: Vec = servers.iter().map(|(id, info)| PublicServerInfo { + id: id.clone(), + ip: info.ip.clone(), + port: info.port, + server_name: info.server_name.clone(), + password_protected: info.password_protected, + game_mode: info.game_mode, + difficulty: info.difficulty, + time_passed: info.time_passed.clone(), + current_players: info.current_players, + max_players: info.max_players, + required_mods: info.required_mods.clone(), + game_version: info.game_version.clone(), + multiplayer_version: info.multiplayer_version.clone(), + server_info: info.server_info.clone(), + }).collect(); + HttpResponse::Ok().json(public_servers) + } + Err(_) => HttpResponse::InternalServerError().json("Failed to list servers"), + } +} diff --git a/Lobby Servers/Rust Server/src/main.rs b/Lobby Servers/Rust Server/src/main.rs index 0729ae6..286a442 100644 --- a/Lobby Servers/Rust Server/src/main.rs +++ b/Lobby Servers/Rust Server/src/main.rs @@ -1,263 +1,74 @@ -use actix_web::{web, App, HttpResponse, HttpServer, Responder}; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::fs::File; -use std::io::{Read, Write}; +mod config; +mod server; +mod state; +mod handlers; +mod ssl; +mod utils; + +use crate::config::read_or_create_config; +use crate::state::AppState; +use crate::ssl::setup_ssl; +use actix_web::{web, App, HttpServer}; use std::sync::{Arc, Mutex}; -use std::time::{SystemTime, UNIX_EPOCH}; -use env_logger::Env; -use log::{info, error}; -use uuid::Uuid; -use openssl::ssl::{SslAcceptor, SslFiletype, SslMethod}; -use openssl::ssl::SslAcceptorBuilder; - -#[derive(Serialize, Deserialize, Clone)] -struct ServerInfo { - ip: String, - port: u16, - server_name: String, - password_protected: bool, - game_mode: u8, - difficulty: u8, - time_passed: String, - current_players: u32, - max_players: u32, - required_mods: String, - game_version: String, - multiplayer_version: String, - server_info: String, - #[serde(skip_serializing)] - last_update: u64, -} - -#[derive(Serialize, Deserialize, Clone)] -struct AddServerResponse { - game_server_id: String, -} - -#[derive(Clone)] -struct AppState { - servers: Arc>>, -} - -#[derive(Serialize, Deserialize, Clone)] -struct Config { - port: u16, - timeout: u64, - ssl_enabled: bool, - ssl_cert_path: String, - ssl_key_path: String, -} - -impl Default for Config { - fn default() -> Self { - Config { - port: 8080, - timeout: 120, - ssl_enabled: false, - ssl_cert_path: String::from("cert.pem"), - ssl_key_path: String::from("key.pem"), - } - } -} - -fn read_or_create_config() -> Config { - let config_path = "config.json"; - let mut config = Config::default(); - - if let Ok(mut file) = File::open(config_path) { - let mut contents = String::new(); - if file.read_to_string(&mut contents).is_ok() { - if let Ok(parsed_config) = serde_json::from_str(&contents) { - config = parsed_config; - } - } - } else { - if let Ok(mut file) = File::create(config_path) { - let _ = file.write_all(serde_json::to_string_pretty(&config).unwrap().as_bytes()); - } - } - - config -} +use tokio::time::{interval, Duration}; #[tokio::main] async fn main() -> std::io::Result<()> { - env_logger::Builder::from_env(Env::default().default_filter_or("info")).init(); + env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init(); let config = read_or_create_config(); let state = AppState { - servers: Arc::new(Mutex::new(HashMap::new())), + servers: Arc::new(Mutex::new(std::collections::HashMap::new())), }; + let cleanup_state = state.clone(); + let config_clone = config.clone(); + + tokio::spawn(async move { + let mut interval = interval(Duration::from_secs(60)); + loop { + interval.tick().await; + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(); + if let Ok(mut servers) = cleanup_state.servers.lock() { + let keys_to_remove: Vec = servers + .iter() + .filter_map(|(key, info)| { + if now - info.last_update > config_clone.timeout { + Some(key.clone()) + } else { + None + } + }) + .collect(); + for key in keys_to_remove { + servers.remove(&key); + } + } + } + }); + let server = { let server_builder = HttpServer::new(move || { App::new() .app_data(web::Data::new(state.clone())) - .route("/add_game_server", web::post().to(add_server)) - .route("/update_game_server", web::post().to(update_server)) - .route("/remove_game_server", web::post().to(remove_server)) - .route("/list_game_servers", web::get().to(list_servers)) + .route("/add_game_server", web::post().to(handlers::add_server)) + .route("/update_game_server", web::post().to(handlers::update_server)) + .route("/remove_game_server", web::post().to(handlers::remove_server)) + .route("/list_game_servers", web::get().to(handlers::list_servers)) }); - + if config.ssl_enabled { let ssl_builder = setup_ssl(&config)?; - server_builder.bind_openssl(format!("0.0.0.0:{}", config.port), (move || ssl_builder)())? + server_builder + .bind_openssl(format!("0.0.0.0:{}", config.port), (move || ssl_builder)())? } else { server_builder.bind(format!("0.0.0.0:{}", config.port))? } }; - // Start the server server.run().await -} - -fn setup_ssl(config: &Config) -> std::io::Result { - let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls())?; - builder.set_private_key_file(&config.ssl_key_path, SslFiletype::PEM)?; - builder.set_certificate_chain_file(&config.ssl_cert_path)?; - Ok(builder) -} - -fn validate_server_info(info: &ServerInfo) -> Result<(), &'static str> { - if info.server_name.len() > 25 { - return Err("Server name exceeds 25 characters"); - } - if info.server_info.len() > 500 { - return Err("Server info exceeds 500 characters"); - } - if info.current_players > info.max_players { - return Err("Current players exceed max players"); - } - if info.max_players < 1 { - return Err("Max players must be at least 1"); - } - Ok(()) -} - -#[derive(Deserialize)] -struct AddServerRequest { - ip: String, - port: u16, - server_name: String, - password_protected: bool, - game_mode: u8, - difficulty: u8, - time_passed: String, - current_players: u32, - max_players: u32, - required_mods: String, - game_version: String, - multiplayer_version: String, - server_info: String, -} - -async fn add_server(data: web::Data, server_info: web::Json) -> impl Responder { - let info = ServerInfo { - ip: server_info.ip.clone(), - port: server_info.port, - server_name: server_info.server_name.clone(), - password_protected: server_info.password_protected, - game_mode: server_info.game_mode, - difficulty: server_info.difficulty, - time_passed: server_info.time_passed.clone(), - current_players: server_info.current_players, - max_players: server_info.max_players, - required_mods: server_info.required_mods.clone(), - game_version: server_info.game_version.clone(), - multiplayer_version: server_info.multiplayer_version.clone(), - server_info: server_info.server_info.clone(), - last_update: SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(), - }; - - if let Err(e) = validate_server_info(&info) { - error!("Validation failed: {}", e); - return HttpResponse::BadRequest().json(e); - } - - let game_server_id = Uuid::new_v4().to_string(); - let key = game_server_id.clone(); - match data.servers.lock() { - Ok(mut servers) => { - servers.insert(key.clone(), info); - info!("Server added: {}", key); - HttpResponse::Ok().json(AddServerResponse { game_server_id: key }) - } - Err(_) => { - error!("Failed to add server: {}", key); - HttpResponse::InternalServerError().json("Failed to add server") - } - } -} - -#[derive(Deserialize)] -struct UpdateServerRequest { - game_server_id: String, - current_players: u32, - time_passed: String, -} - -async fn update_server(data: web::Data, server_info: web::Json) -> impl Responder { - let mut updated = false; - match data.servers.lock() { - Ok(mut servers) => { - if let Some(info) = servers.get_mut(&server_info.game_server_id) { - if server_info.current_players <= info.max_players { - info.current_players = server_info.current_players; - info.time_passed = server_info.time_passed.clone(); - info.last_update = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(); - updated = true; - } - } - } - Err(_) => { - error!("Failed to update server: {}", server_info.game_server_id); - return HttpResponse::InternalServerError().json("Failed to update server"); - } - } - - if updated { - info!("Server updated: {}", server_info.game_server_id); - HttpResponse::Ok().json("Server updated") - } else { - error!("Server not found or invalid current players: {}", server_info.game_server_id); - HttpResponse::BadRequest().json("Server not found or invalid current players") - } -} - -#[derive(Deserialize)] -struct RemoveServerRequest { - game_server_id: String, -} - -async fn remove_server(data: web::Data, server_info: web::Json) -> impl Responder { - let removed = match data.servers.lock() { - Ok(mut servers) => servers.remove(&server_info.game_server_id).is_some(), - Err(_) => { - error!("Failed to remove server: {}", server_info.game_server_id); - false - } - }; - - if removed { - info!("Server removed: {}", server_info.game_server_id); - HttpResponse::Ok().json("Server removed") - } else { - error!("Server not found: {}", server_info.game_server_id); - HttpResponse::BadRequest().json("Server not found") - } -} - -async fn list_servers(data: web::Data) -> impl Responder { - match data.servers.lock() { - Ok(servers) => { - let servers_list: Vec = servers.values().cloned().collect(); - HttpResponse::Ok().json(servers_list) - } - Err(_) => { - error!("Failed to retrieve servers"); - HttpResponse::InternalServerError().json("Failed to retrieve servers") - } - } -} +} \ No newline at end of file diff --git a/Lobby Servers/Rust Server/src/server.rs b/Lobby Servers/Rust Server/src/server.rs new file mode 100644 index 0000000..3ffa009 --- /dev/null +++ b/Lobby Servers/Rust Server/src/server.rs @@ -0,0 +1,62 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Clone)] +pub struct ServerInfo { + pub ip: String, + pub port: u16, + pub server_name: String, + pub password_protected: bool, + pub game_mode: u8, + pub difficulty: u8, + pub time_passed: String, + pub current_players: u32, + pub max_players: u32, + pub required_mods: String, + pub game_version: String, + pub multiplayer_version: String, + pub server_info: String, + #[serde(skip_serializing)] + pub last_update: u64, + #[serde(skip_serializing)] + pub private_key: String, +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct PublicServerInfo { + pub id: String, + pub ip: String, + pub port: u16, + pub server_name: String, + pub password_protected: bool, + pub game_mode: u8, + pub difficulty: u8, + pub time_passed: String, + pub current_players: u32, + pub max_players: u32, + pub required_mods: String, + pub game_version: String, + pub multiplayer_version: String, + pub server_info: String, +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct AddServerResponse { + pub game_server_id: String, + pub private_key: String, +} + +pub fn validate_server_info(info: &ServerInfo) -> Result<(), &'static str> { + if info.server_name.len() > 25 { + return Err("Server name exceeds 25 characters"); + } + if info.server_info.len() > 500 { + return Err("Server info exceeds 500 characters"); + } + if info.current_players > info.max_players { + return Err("Current players exceed max players"); + } + if info.max_players < 1 { + return Err("Max players must be at least 1"); + } + Ok(()) +} diff --git a/Lobby Servers/Rust Server/src/ssl.rs b/Lobby Servers/Rust Server/src/ssl.rs new file mode 100644 index 0000000..f8c9f70 --- /dev/null +++ b/Lobby Servers/Rust Server/src/ssl.rs @@ -0,0 +1,10 @@ +use crate::config::Config; +use openssl::ssl::{SslAcceptor, SslFiletype, SslMethod}; +use openssl::ssl::SslAcceptorBuilder; + +pub fn setup_ssl(config: &Config) -> std::io::Result { + let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls())?; + builder.set_private_key_file(&config.ssl_key_path, SslFiletype::PEM)?; + builder.set_certificate_chain_file(&config.ssl_cert_path)?; + Ok(builder) +} diff --git a/Lobby Servers/Rust Server/src/state.rs b/Lobby Servers/Rust Server/src/state.rs new file mode 100644 index 0000000..a1335a9 --- /dev/null +++ b/Lobby Servers/Rust Server/src/state.rs @@ -0,0 +1,7 @@ +use std::sync::{Arc, Mutex}; +use crate::server::ServerInfo; + +#[derive(Clone)] +pub struct AppState { + pub servers: Arc>>, +} diff --git a/Lobby Servers/Rust Server/src/utils.rs b/Lobby Servers/Rust Server/src/utils.rs new file mode 100644 index 0000000..b89c13c --- /dev/null +++ b/Lobby Servers/Rust Server/src/utils.rs @@ -0,0 +1,8 @@ +use rand::Rng; + +pub fn generate_private_key() -> String { + let mut rng = rand::thread_rng(); + let random_bytes: Vec = (0..16).map(|_| rng.gen::()).collect(); + let private_key: String = random_bytes.iter().map(|b| format!("{:02x}", b)).collect(); + private_key +} diff --git a/Multiplayer/Components/MainMenu/HostGamePane.cs b/Multiplayer/Components/MainMenu/HostGamePane.cs new file mode 100644 index 0000000..aee1b4c --- /dev/null +++ b/Multiplayer/Components/MainMenu/HostGamePane.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections; +using System.Text.RegularExpressions; +using DV.Localization; +using DV.UI; +using DV.UIFramework; +using DV.Util; +using DV.Utils; +using Multiplayer.Components.Networking; +using Multiplayer.Utils; +using TMPro; +using UnityEngine; +using UnityEngine.UI; +using UnityEngine.Networking; +using System.Linq; +using Multiplayer.Networking.Data; + + + +namespace Multiplayer.Components.MainMenu; + +public class HostGamePane : MonoBehaviour +{ + + + #region setup + + private void Awake() + { + Multiplayer.Log("HostGamePane Awake()"); + + + BuildUI(); + + + } + + private void OnEnable() + { + Multiplayer.Log("HostGamePane OnEnable()"); + this.SetupListeners(true); + } + + // Disable listeners + private void OnDisable() + { + this.SetupListeners(false); + } + + private void BuildUI() + { + + + } + + + private void SetupListeners(bool on) + { + if (on) + { + //this.gridView.SelectedIndexChanged += this.IndexChanged; + } + else + { + //this.gridView.SelectedIndexChanged -= this.IndexChanged; + } + + } + + #endregion + + #region UI callbacks + + #endregion + + +} diff --git a/Multiplayer/Components/MainMenu/IServerBrowserGameDetails.cs b/Multiplayer/Components/MainMenu/ServerBrowser/IServerBrowserGameDetails.cs similarity index 54% rename from Multiplayer/Components/MainMenu/IServerBrowserGameDetails.cs rename to Multiplayer/Components/MainMenu/ServerBrowser/IServerBrowserGameDetails.cs index 533b21a..20fc5e6 100644 --- a/Multiplayer/Components/MainMenu/IServerBrowserGameDetails.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowser/IServerBrowserGameDetails.cs @@ -6,23 +6,28 @@ using System.Runtime.CompilerServices; using Newtonsoft.Json.Linq; using UnityEngine; +using Newtonsoft.Json; namespace Multiplayer.Components.MainMenu { // public interface IServerBrowserGameDetails : IDisposable { - // - // - int ServerID { get; } - - // - // - // + string id { get; set; } + string ip { get; set; } + public ushort port { get; set; } string Name { get; set; } - int MaxPlayers { get; set; } + bool HasPassword { get; set; } + int GameMode { get; set; } + int Difficulty { get; set; } + string TimePassed { get; set; } int CurrentPlayers { get; set; } + int MaxPlayers { get; set; } + string RequiredMods { get; set; } + string GameVersion { get; set; } + string MultiplayerVersion { get; set; } + public string ServerDetails { get; set; } int Ping { get; set; } - bool HasPassword { get; set; } + } } diff --git a/Multiplayer/Components/MainMenu/PopupTextInputFieldControllerNoValidation.cs b/Multiplayer/Components/MainMenu/ServerBrowser/PopupTextInputFieldControllerNoValidation.cs similarity index 100% rename from Multiplayer/Components/MainMenu/PopupTextInputFieldControllerNoValidation.cs rename to Multiplayer/Components/MainMenu/ServerBrowser/PopupTextInputFieldControllerNoValidation.cs diff --git a/Multiplayer/Components/MainMenu/ServerBrowserElement.cs b/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserElement.cs similarity index 95% rename from Multiplayer/Components/MainMenu/ServerBrowserElement.cs rename to Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserElement.cs index b269f54..e1c122b 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowserElement.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserElement.cs @@ -5,7 +5,7 @@ using UnityEngine; using UnityEngine.UI; -namespace Multiplayer.Components.MainMenu +namespace Multiplayer.Components.MainMenu.ServerBrowser { public class ServerBrowserElement : AViewElement { @@ -34,7 +34,7 @@ private void Awake() playerCount.transform.position = new Vector3(namePos.x + nameSize.x, namePos.y, namePos.z); // Adjust the size and position of the ping text - Vector2 rowSize = this.transform.GetComponentInParent().sizeDelta; + Vector2 rowSize = transform.GetComponentInParent().sizeDelta; Vector3 pingPos = ping.transform.position; Vector2 pingSize = ping.rectTransform.sizeDelta; diff --git a/Multiplayer/Components/MainMenu/ServerBrowserGridView.cs b/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserGridView.cs similarity index 94% rename from Multiplayer/Components/MainMenu/ServerBrowserGridView.cs rename to Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserGridView.cs index 97d033b..f43c789 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowserGridView.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserGridView.cs @@ -6,6 +6,7 @@ using DV.Common; using DV.UI; using DV.UIFramework; +using Multiplayer.Components.MainMenu.ServerBrowser; using UnityEngine; using UnityEngine.UI; diff --git a/Multiplayer/Components/MainMenu/MultiplayerPane.cs b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs similarity index 58% rename from Multiplayer/Components/MainMenu/MultiplayerPane.cs rename to Multiplayer/Components/MainMenu/ServerBrowserPane.cs index 6837f32..4a5eb7b 100644 --- a/Multiplayer/Components/MainMenu/MultiplayerPane.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs @@ -1,7 +1,6 @@ using System; -using System.Collections.Generic; +using System.Collections; using System.Text.RegularExpressions; -using DV.Common; using DV.Localization; using DV.UI; using DV.UIFramework; @@ -11,12 +10,16 @@ using Multiplayer.Utils; using TMPro; using UnityEngine; -using UnityEngine.Events; using UnityEngine.UI; +using UnityEngine.Networking; +using System.Linq; +using Multiplayer.Networking.Data; + + namespace Multiplayer.Components.MainMenu { - public class MultiplayerPane : MonoBehaviour + public class ServerBrowserPane : MonoBehaviour { // Regular expressions for IP and port validation // @formatter:off @@ -26,33 +29,57 @@ public class MultiplayerPane : MonoBehaviour private static readonly Regex PortRegex = new Regex(@"^((6553[0-5])|(655[0-2][0-9])|(65[0-4][0-9]{2})|(6[0-4][0-9]{3})|([1-5][0-9]{4})|([0-5]{0,5})|([0-9]{1,4}))$"); // @formatter:on - private string ipAddress; - private ushort portNumber; - //private ButtonDV directButton; + + //Gridview variables private ObservableCollectionExt gridViewModel = new ObservableCollectionExt(); private ServerBrowserGridView gridView; private ScrollRect parentScroller; - private int indexToSelectOnRefresh; + private string serverIDOnRefresh; + private IServerBrowserGameDetails selectedServer; + + //Button variables + private Button buttonJoin; + //private Button buttonHost; + private Button buttonRefresh; + private Button buttonDirectIP; + + private bool serverRefreshing = false; + + //connection parameters + private string ipAddress; + private ushort portNumber; + string password = null; + bool direct = false; private string[] testNames = new string[] { "ChooChooExpress", "RailwayRascals", "FreightFrenzy", "SteamDream", "DieselDynasty", "CargoKings", "TrackMasters", "RailwayRevolution", "ExpressElders", "IronHorseHeroes", "LocomotiveLegends", "TrainTitans", "HeavyHaulers", "RapidRails", "TimberlineTransport", "CoalCountry", "SilverRailway", "GoldenGauge", "SteelStream", "MountainMoguls", "RailRiders", "TrackTrailblazers", "FreightFanatics", "SteamSensation", "DieselDaredevils", "CargoChampions", "TrackTacticians", "RailwayRoyals", "ExpressExperts", "IronHorseInnovators", "LocomotiveLeaders", "TrainTacticians", "HeavyHitters", "RapidRunners", "TimberlineTrains", "CoalCrushers", "SilverStreamliners", "GoldenGears", "SteelSurge", "MountainMovers", "RailwayWarriors", "TrackTerminators", "FreightFighters", "SteamStreak", "DieselDynamos", "CargoCommanders", "TrackTrailblazers", "RailwayRangers", "ExpressEngineers", "IronHorseInnovators", "LocomotiveLovers", "TrainTrailblazers", "HeavyHaulersHub", "RapidRailsRacers", "TimberlineTrackers", "CoalCountryCarriers", "SilverSpeedsters", "GoldenGaugeGang", "SteelStalwarts", "MountainMoversClub", "RailRunners", "TrackTitans", "FreightFalcons", "SteamSprinters", "DieselDukes", "CargoCommandos", "TrackTracers", "RailwayRebels", "ExpressElite", "IronHorseIcons", "LocomotiveLunatics", "TrainTornadoes", "HeavyHaulersCrew", "RapidRailsRunners", "TimberlineTrackMasters", "CoalCountryCrew", "SilverSprinters", "GoldenGale", "SteelSpeedsters", "MountainMarauders", "RailwayRiders", "TrackTactics", "FreightFury", "SteamSquires", "DieselDefenders", "CargoCrusaders", "TrackTechnicians", "RailwayRaiders", "ExpressEnthusiasts", "IronHorseIlluminati", "LocomotiveLoyalists", "TrainTurbulence", "HeavyHaulersHeroes", "RapidRailsRiders", "TimberlineTrackTitans", "CoalCountryCaravans", "SilverSpeedRacers", "GoldenGaugeGangsters", "SteelStorm", "MountainMasters", "RailwayRoadrunners", "TrackTerror", "FreightFleets", "SteamSurgeons", "DieselDragons", "CargoCrushers", "TrackTaskmasters", "RailwayRevolutionaries", "ExpressExplorers", "IronHorseInquisitors", "LocomotiveLegion", "TrainTriumph", "HeavyHaulersHorde", "RapidRailsRenegades", "TimberlineTrackTeam", "CoalCountryCrusade", "SilverSprintersSquad", "GoldenGaugeGroup", "SteelStrike", "MountainMonarchs", "RailwayRaid", "TrackTacticiansTeam", "FreightForce", "SteamSquad", "DieselDynastyClan", "CargoCrew", "TrackTeam", "RailwayRalliers", "ExpressExpedition", "IronHorseInitiative", "LocomotiveLeague", "TrainTribe", "HeavyHaulersHustle", "RapidRailsRevolution", "TimberlineTrackersTeam", "CoalCountryConvoy", "SilverSprint", "GoldenGaugeGuild", "SteelSpirits", "MountainMayhem", "RailwayRaidersCrew", "TrackTrailblazersTribe", "FreightFleetForce", "SteamStalwarts", "DieselDragonsDen", "CargoCaptains", "TrackTrailblazersTeam", "RailwayRidersRevolution", "ExpressEliteExpedition", "IronHorseInsiders", "LocomotiveLords", "TrainTacticiansTribe", "HeavyHaulersHeroesHorde", "RapidRailsRacersTeam", "TimberlineTrackMastersTeam", "CoalCountryCarriersCrew", "SilverSpeedstersSprint", "GoldenGaugeGangGuild", "SteelSurgeStrike", "MountainMoversMonarchs" }; + #region setup + private void Awake() { Multiplayer.Log("MultiplayerPane Awake()"); SetupMultiplayerButtons(); SetupServerBrowser(); + FillDummyServers(); } private void OnEnable() { + Multiplayer.Log("MultiplayerPane OnEnable()"); if (!this.parentScroller) { + Multiplayer.Log("Find ScrollRect"); this.parentScroller = this.gridView.GetComponentInParent(); + Multiplayer.Log("Found ScrollRect"); } this.SetupListeners(true); - this.indexToSelectOnRefresh = 0; - this.RefreshData(); + this.serverIDOnRefresh = ""; + + buttonDirectIP.interactable = true; + buttonRefresh.interactable = true; + //buttonHost.interactable = true; + } // Disable listeners @@ -63,45 +90,42 @@ private void OnDisable() private void SetupMultiplayerButtons() { - GameObject buttonDirectIP = GameObject.Find("ButtonTextIcon Manual"); - GameObject buttonHost = GameObject.Find("ButtonTextIcon Host"); - GameObject buttonJoin = GameObject.Find("ButtonTextIcon Join"); - GameObject buttonRefresh = GameObject.Find("ButtonTextIcon Refresh"); + GameObject goDirectIP = GameObject.Find("ButtonTextIcon Manual"); + //GameObject goHost = GameObject.Find("ButtonTextIcon Host"); + GameObject goJoin = GameObject.Find("ButtonTextIcon Join"); + GameObject goRefresh = GameObject.Find("ButtonIcon Refresh"); - if (buttonDirectIP == null || buttonHost == null || buttonJoin == null || buttonRefresh == null) + if (goDirectIP == null || /*goHost == null ||*/ goJoin == null || goRefresh == null) { Multiplayer.LogError("One or more buttons not found."); return; } // Modify the existing buttons' properties - ModifyButton(buttonDirectIP, Locale.SERVER_BROWSER__MANUAL_CONNECT_KEY); - ModifyButton(buttonHost, Locale.SERVER_BROWSER__HOST_KEY); - ModifyButton(buttonJoin, Locale.SERVER_BROWSER__JOIN_KEY); - //ModifyButton(buttonRefresh, Locale.SERVER_BROWSER__REFRESH); + ModifyButton(goDirectIP, Locale.SERVER_BROWSER__MANUAL_CONNECT_KEY); + //ModifyButton(goHost, Locale.SERVER_BROWSER__HOST_KEY); + ModifyButton(goJoin, Locale.SERVER_BROWSER__JOIN_KEY); - // Set up event listeners and localization for DirectIP button - ButtonDV buttonDirectIPDV = buttonDirectIP.GetComponent(); - buttonDirectIPDV.onClick.AddListener(ShowIpPopup); - // Set up event listeners and localization for Host button - ButtonDV buttonHostDV = buttonHost.GetComponent(); - buttonHostDV.onClick.AddListener(HostAction); + // Set up event listeners and localization for DirectIP button + buttonDirectIP = goDirectIP.GetComponent(); + buttonDirectIP.onClick.AddListener(DirectAction); // Set up event listeners and localization for Join button - ButtonDV buttonJoinDV = buttonJoin.GetComponent(); - buttonJoinDV.onClick.AddListener(JoinAction); + buttonJoin = goJoin.GetComponent(); + buttonJoin.onClick.AddListener(JoinAction); // Set up event listeners and localization for Refresh button - //ButtonDV buttonRefreshDV = buttonRefresh.GetComponent(); - //buttonRefreshDV.onClick.AddListener(RefreshAction); - - //Debug.Log("Setting buttons active: " + buttonDirectIP.name + ", " + buttonHost.name + ", " + buttonJoin.name + ", " + buttonRefresh.name ); - Debug.Log("Setting buttons active: " + buttonDirectIP.name + ", " + buttonHost.name + ", " + buttonJoin.name); - buttonDirectIP.SetActive(true); - buttonHost.SetActive(true); - buttonJoin.SetActive(true); - //buttonRefresh.SetActive(true); + buttonRefresh = goRefresh.GetComponent(); + buttonRefresh.onClick.AddListener(RefreshAction); + + goDirectIP.SetActive(true); + //goHost.SetActive(true); + goJoin.SetActive(true); + goRefresh.SetActive(true); + + buttonJoin.interactable = false; + } private void SetupServerBrowser() @@ -119,19 +143,108 @@ private void SetupServerBrowser() GridviewGO.SetActive(true); } + private void SetupListeners(bool on) + { + if (on) + { + this.gridView.SelectedIndexChanged += this.IndexChanged; + } + else + { + this.gridView.SelectedIndexChanged -= this.IndexChanged; + } + + } + + private void ModifyButton(GameObject button, string key) + { + button.GetComponentInChildren().key = key; + } private GameObject FindButton(string name) { return GameObject.Find(name); } - private void ModifyButton(GameObject button, string key) + #endregion + + #region UI callbacks + + private void RefreshAction() { - button.GetComponentInChildren().key = key; + if (serverRefreshing) + return; + + serverRefreshing = true; + buttonJoin.interactable = false; + + if (selectedServer != null) + { + serverIDOnRefresh = selectedServer.id; + } + + StartCoroutine(GetRequest($"{Multiplayer.Settings.LobbyServerAddress}/list_game_servers")); + + } + private void JoinAction() + { + if (selectedServer != null) + { + buttonDirectIP.interactable = false; + buttonJoin.interactable = false; + //buttonHost.interactable = false; + + if (selectedServer.HasPassword) + { + //not making a direct connection + direct = false; + ipAddress = selectedServer.ip; + portNumber = selectedServer.port; + + ShowPasswordPopup(); + + return; + } + + SingletonBehaviour.Instance.StartClient(selectedServer.ip, selectedServer.port, null); + } + } + + private void DirectAction() + { + Debug.Log($"DirectAction()"); + buttonDirectIP.interactable = false; + buttonJoin.interactable = false; + //buttonHost.interactable = false; + + //making a direct connection + direct = true; + + ShowIpPopup(); + } + + private void IndexChanged(AGridView gridView) + { + Debug.Log($"Index: {gridView.SelectedModelIndex}"); + if (serverRefreshing) + return; + + if (gridView.SelectedModelIndex >= 0) + { + Debug.Log($"Selected server: {gridViewModel[gridView.SelectedModelIndex].Name}"); + selectedServer = gridViewModel[gridView.SelectedModelIndex]; + buttonJoin.interactable = true; + } + else + { + buttonJoin.interactable = false; + } } + #endregion + private void ShowIpPopup() { Debug.Log("In ShowIpPpopup"); @@ -148,29 +261,23 @@ private void ShowIpPopup() popup.Closed += result => { if (result.closedBy == PopupClosedByAction.Abortion) - { - MainMenuThingsAndStuff.Instance.SwitchToDefaultMenu(); return; - } - HandleIpAddressInput(result.data); + if (!IPv4Regex.IsMatch(result.data) && !IPv6Regex.IsMatch(result.data)) + { + ShowOkPopup(Locale.SERVER_BROWSER__IP_INVALID, ShowIpPopup); + } + else + { + ipAddress = result.data; + ShowPortPopup(); + } }; } - private void HandleIpAddressInput(string input) - { - if (!IPv4Regex.IsMatch(input) && !IPv6Regex.IsMatch(input)) - { - ShowOkPopup(Locale.SERVER_BROWSER__IP_INVALID, ShowIpPopup); - return; - } - - ipAddress = input; - ShowPortPopup(); - } - private void ShowPortPopup() { + var popup = MainMenuThingsAndStuff.Instance.ShowRenamePopup(); if (popup == null) { @@ -179,30 +286,24 @@ private void ShowPortPopup() } popup.labelTMPro.text = Locale.SERVER_BROWSER__PORT; - popup.GetComponentInChildren().text = Multiplayer.Settings.LastRemotePort.ToString(); + popup.GetComponentInChildren().text = $"{Multiplayer.Settings.LastRemotePort}"; popup.Closed += result => { if (result.closedBy == PopupClosedByAction.Abortion) - { - MainMenuThingsAndStuff.Instance.SwitchToDefaultMenu(); return; - } - HandlePortInput(result.data); + if (!PortRegex.IsMatch(result.data)) + { + ShowOkPopup(Locale.SERVER_BROWSER__PORT_INVALID, ShowIpPopup); + } + else + { + portNumber = ushort.Parse(result.data); + ShowPasswordPopup(); + } }; - } - private void HandlePortInput(string input) - { - if (!PortRegex.IsMatch(input)) - { - ShowOkPopup(Locale.SERVER_BROWSER__PORT_INVALID, ShowPortPopup); - return; - } - - portNumber = ushort.Parse(input); - ShowPasswordPopup(); } private void ShowPasswordPopup() @@ -215,21 +316,33 @@ private void ShowPasswordPopup() } popup.labelTMPro.text = Locale.SERVER_BROWSER__PASSWORD; - popup.GetComponentInChildren().text = Multiplayer.Settings.LastRemotePassword; - DestroyImmediate(popup.GetComponentInChildren()); - popup.GetOrAddComponent(); + //direct IP connection + if (direct) + { + //Prefill with stored password + popup.GetComponentInChildren().text = Multiplayer.Settings.LastRemotePassword; + + //Set us up to allow a blank password + DestroyImmediate(popup.GetComponentInChildren()); + popup.GetOrAddComponent(); + } popup.Closed += result => { - if (result.closedBy == PopupClosedByAction.Abortion) return; + if (result.closedBy == PopupClosedByAction.Abortion) + return; - //directButton.enabled = false; - SingletonBehaviour.Instance.StartClient(ipAddress, portNumber, result.data); + if (direct) + { + //store params for later + Multiplayer.Settings.LastRemoteIP = ipAddress; + Multiplayer.Settings.LastRemotePort = portNumber; + Multiplayer.Settings.LastRemotePassword = result.data; - Multiplayer.Settings.LastRemoteIP = ipAddress; - Multiplayer.Settings.LastRemotePort = portNumber; - Multiplayer.Settings.LastRemotePassword = result.data; + } + + SingletonBehaviour.Instance.StartClient(ipAddress, portNumber, result.data); //ShowConnectingPopup(); // Show a connecting message //SingletonBehaviour.Instance.ConnectionFailed += HandleConnectionFailed; @@ -253,13 +366,62 @@ private void HandleConnectionFailed() // ShowConnectionFailedPopup(); } - private void RefreshAction() + + + IEnumerator GetRequest(string uri) { - // Implement refresh action logic here - Debug.Log("Refresh button clicked."); - // Add your code to refresh the multiplayer list or perform any other refresh-related action - } + using (UnityWebRequest webRequest = UnityWebRequest.Get(uri)) + { + // Request and wait for the desired page. + yield return webRequest.SendWebRequest(); + + string[] pages = uri.Split('/'); + int page = pages.Length - 1; + + if (webRequest.isNetworkError) + { + Debug.Log(pages[page] + ": Error: " + webRequest.error); + } + else + { + Debug.Log(pages[page] + ":\nReceived: " + webRequest.downloadHandler.text); + + ServerData[] response; + + response = Newtonsoft.Json.JsonConvert.DeserializeObject(webRequest.downloadHandler.text); + Debug.Log($"servers: {response.Length}"); + + foreach (ServerData server in response) + { + Debug.Log($"Name: {server.Name}\tIP: {server.ip}"); + } + + gridViewModel.Clear(); + gridView.SetModel(gridViewModel); + gridViewModel.AddRange(response); + + //if we have a server selected, we need to re-select it after refresh + if (serverIDOnRefresh != null) + { + int selID = Array.FindIndex(gridViewModel.ToArray(), server => server.id == serverIDOnRefresh); + if (selID >= 0) + { + gridView.SetSelected(selID); + + if (this.parentScroller) + { + this.parentScroller.verticalNormalizedPosition = 1f - (float)selID / (float)gridView.Model.Count; + } + } + + serverIDOnRefresh = null; + } + + serverRefreshing = false; + } + } + } private static void ShowOkPopup(string text, Action onClick) { @@ -278,13 +440,8 @@ private void SetButtonsActive(params GameObject[] buttons) } } - private void HostAction() + private void FillDummyServers() { - // Implement host action logic here - Debug.Log("Host button clicked."); - // Add your code to handle hosting a game - - //gridView.showDummyElement = true; gridViewModel.Clear(); @@ -306,38 +463,8 @@ private void HostAction() } gridView.SetModel(gridViewModel); - - } - - private void JoinAction() - { - // Implement join action logic here - Debug.Log("Join button clicked."); - // Add code to handle joining a game - } - private void SetupListeners(bool on) - { - if (on) - { - return; - } - - } - private void RefreshData() - { - } } - public class ServerData : IServerBrowserGameDetails - { - public int ServerID { get; } - public string Name { get; set; } - public int MaxPlayers { get; set; } - public int CurrentPlayers { get; set; } - public int Ping { get; set; } - public bool HasPassword { get; set; } - - public void Dispose() { } - } + } diff --git a/Multiplayer/Multiplayer.csproj b/Multiplayer/Multiplayer.csproj index df191dc..0b78777 100644 --- a/Multiplayer/Multiplayer.csproj +++ b/Multiplayer/Multiplayer.csproj @@ -73,11 +73,11 @@ - + diff --git a/Multiplayer/Networking/Data/ServerData.cs b/Multiplayer/Networking/Data/ServerData.cs new file mode 100644 index 0000000..c0b3a47 --- /dev/null +++ b/Multiplayer/Networking/Data/ServerData.cs @@ -0,0 +1,67 @@ +using Multiplayer.Components.MainMenu; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Multiplayer.Networking.Data +{ + public class ServerData : IServerBrowserGameDetails + { + + public string id { get; set; } //not yet used + public string ip { get; set; } + public ushort port { get; set; } + + + [JsonProperty("server_name")] + public string Name { get; set; } + + + [JsonProperty("password_protected")] + public bool HasPassword { get; set; } + + + [JsonProperty("game_mode")] + public int GameMode { get; set; } + + + [JsonProperty("difficulty")] + public int Difficulty { get; set; } + + + [JsonProperty("time_passed")] + public string TimePassed { get; set; } + + + [JsonProperty("current_players")] + public int CurrentPlayers { get; set; } + + + [JsonProperty("max_players")] + public int MaxPlayers { get; set; } + + + [JsonProperty("required_mods")] + public string RequiredMods { get; set; } + + + [JsonProperty("game_version")] + public string GameVersion { get; set; } + + + [JsonProperty("multiplayer_version")] + public string MultiplayerVersion { get; set; } + + + [JsonProperty("server_info")] + public string ServerDetails { get; set; } + + public int Ping { get; set; } + + + public void Dispose() { } + } +} diff --git a/Multiplayer/Patches/MainMenu/LauncherControllerPatch.cs b/Multiplayer/Patches/MainMenu/LauncherControllerPatch.cs new file mode 100644 index 0000000..2f64990 --- /dev/null +++ b/Multiplayer/Patches/MainMenu/LauncherControllerPatch.cs @@ -0,0 +1,72 @@ +using DV.Localization; +using DV.UI; +using DV.UIFramework; +using HarmonyLib; +using Multiplayer.Components.MainMenu; +using Multiplayer.Utils; +using UnityEngine; +using UnityEngine.UI; + + +namespace Multiplayer.Patches.MainMenu; + +[HarmonyPatch(typeof(LauncherController), "OnEnable")] +public static class LauncherController_Patch +{ + private const int PADDING = 10; + + private static GameObject goHost; + + private static void Postfix(LauncherController __instance) + { + + Multiplayer.Log("LauncherController_Patch()"); + + if (goHost != null) + return; + + GameObject goRun = __instance.FindChildByName("ButtonTextIcon Run"); + + if(goRun != null) + { + goRun.SetActive(false); + goHost = GameObject.Instantiate(goRun); + goRun.SetActive(true); + + goHost.name = "ButtonTextIcon Host"; + goHost.transform.SetParent(goRun.transform.parent, false); + + RectTransform btnHostRT = goHost.GetComponentInChildren(); + + Vector3 curPos = btnHostRT.localPosition; + Vector2 curSize = btnHostRT.sizeDelta; + + btnHostRT.localPosition = new Vector3(curPos.x - curSize.x - PADDING, curPos.y,curPos.z); + + __instance.transform.gameObject.UpdateButton("ButtonTextIcon Host", "ButtonTextIcon Host", Locale.SERVER_BROWSER__HOST_KEY, null, Multiplayer.AssetIndex.lockIcon); + + + // Set up event listeners + Button btnHost = goHost.GetComponent(); + //UIMenuRequester uim = btnHost.GetOrAddComponent(); + //uim.targetMenuController = RightPaneController_OnEnable_Patch.uIMenuController; + //uim.requestedMenuIndex = RightPaneController_OnEnable_Patch.hostMenuIndex; + + btnHost.onClick.AddListener(HostAction); + + goHost.SetActive(true); + + Multiplayer.Log("LauncherController_Patch() complete"); + } + } + + private static void HostAction() + { + // Implement host action logic here + Debug.Log("Host button clicked."); + // Add your code to handle hosting a game + + RightPaneController_OnEnable_Patch.uIMenuController.SwitchMenu(RightPaneController_OnEnable_Patch.hostMenuIndex); + + } +} diff --git a/Multiplayer/Patches/MainMenu/RightPaneControllerPatch.cs b/Multiplayer/Patches/MainMenu/RightPaneControllerPatch.cs index 7f31c20..36b361e 100644 --- a/Multiplayer/Patches/MainMenu/RightPaneControllerPatch.cs +++ b/Multiplayer/Patches/MainMenu/RightPaneControllerPatch.cs @@ -6,7 +6,7 @@ using Multiplayer.Utils; using TMPro; using UnityEngine; -using UnityEngine.UI; + namespace Multiplayer.Patches.MainMenu @@ -14,8 +14,11 @@ namespace Multiplayer.Patches.MainMenu [HarmonyPatch(typeof(RightPaneController), "OnEnable")] public static class RightPaneController_OnEnable_Patch { + public static int hostMenuIndex; + public static UIMenuController uIMenuController; private static void Prefix(RightPaneController __instance) { + uIMenuController = __instance.menuController; // Check if the multiplayer pane already exists if (__instance.HasChildWithName("PaneRight Multiplayer")) return; @@ -23,7 +26,7 @@ private static void Prefix(RightPaneController __instance) // Find the base pane for Load/Save GameObject basePane = __instance.FindChildByName("PaneRight Load/Save"); if (basePane == null) - { + { Multiplayer.LogError("Failed to find Launcher pane!"); return; } @@ -43,6 +46,7 @@ private static void Prefix(RightPaneController __instance) GameObject.Destroy(multiplayerPane.GetComponent()); GameObject.Destroy(multiplayerPane.FindChildByName("ButtonIcon OpenFolder")); GameObject.Destroy(multiplayerPane.FindChildByName("ButtonIcon Rename")); + GameObject.Destroy(multiplayerPane.FindChildByName("ButtonTextIcon Load")); GameObject.Destroy(multiplayerPane.FindChildByName("Text Content")); // Update UI elements @@ -51,20 +55,20 @@ private static void Prefix(RightPaneController __instance) GameObject.Destroy(titleObj.GetComponentInChildren()); GameObject content = multiplayerPane.FindChildByName("text main"); - content.GetComponentInChildren().text = "Server browser not yet implemented."; + //content.GetComponentInChildren().text = "Server browser not yet implemented."; GameObject serverWindow = multiplayerPane.FindChildByName("Save Description"); - serverWindow.GetComponentInChildren().text = "Server information not yet implemented."; + serverWindow.GetComponentInChildren().textWrappingMode = TextWrappingModes.Normal; + serverWindow.GetComponentInChildren().text = "Server browser not yet implemented.

Dummy servers are shown for demonstration purposes only.

Press refresh to load real servers."; // Update buttons on the multiplayer pane - UpdateButton(multiplayerPane, "ButtonTextIcon Overwrite", "ButtonTextIcon Manual", Locale.SERVER_BROWSER__MANUAL_CONNECT_KEY, null, Multiplayer.AssetIndex.multiplayerIcon); - UpdateButton(multiplayerPane, "ButtonTextIcon Load", "ButtonTextIcon Host", Locale.SERVER_BROWSER__HOST_KEY, null, Multiplayer.AssetIndex.lockIcon); - UpdateButton(multiplayerPane, "ButtonTextIcon Save", "ButtonTextIcon Join", Locale.SERVER_BROWSER__JOIN_KEY, null, Multiplayer.AssetIndex.connectIcon); - UpdateButton(multiplayerPane, "ButtonIcon Delete", "ButtonTextIcon Refresh", Locale.SERVER_BROWSER__REFRESH, null, Multiplayer.AssetIndex.refreshIcon); + multiplayerPane.UpdateButton("ButtonTextIcon Overwrite", "ButtonTextIcon Manual", Locale.SERVER_BROWSER__MANUAL_CONNECT_KEY, null, Multiplayer.AssetIndex.multiplayerIcon); + //multiplayerPane.UpdateButton("ButtonTextIcon Load", "ButtonTextIcon Host", Locale.SERVER_BROWSER__HOST_KEY, null, Multiplayer.AssetIndex.lockIcon); + multiplayerPane.UpdateButton("ButtonTextIcon Save", "ButtonTextIcon Join", Locale.SERVER_BROWSER__JOIN_KEY, null, Multiplayer.AssetIndex.connectIcon); + multiplayerPane.UpdateButton("ButtonIcon Delete", "ButtonIcon Refresh", Locale.SERVER_BROWSER__REFRESH_KEY, null, Multiplayer.AssetIndex.refreshIcon); - // Add the MultiplayerPane component - multiplayerPane.AddComponent(); + multiplayerPane.AddComponent(); // Create and initialize MainMenuThingsAndStuff MainMenuThingsAndStuff.Create(manager => @@ -80,51 +84,38 @@ private static void Prefix(RightPaneController __instance) // Activate the multiplayer button MainMenuController_Awake_Patch.multiplayerButton.SetActive(true); Multiplayer.LogError("At end!"); - } - private static void UpdateButton(GameObject pane, string oldButtonName, string newButtonName, string localeKey, string toolTipKey, Sprite icon) - { - // Find and rename the button - GameObject button = pane.FindChildByName(oldButtonName); - button.name = newButtonName; - // Update localization and tooltip - if (button.GetComponentInChildren() != null) - { - button.GetComponentInChildren().key = localeKey; - GameObject.Destroy(button.GetComponentInChildren()); - ResetTooltip(button); - } - // Set the button icon if provided - if (icon != null) - { - SetButtonIcon(button, icon); - } - // Enable button interaction - button.GetComponentInChildren().ToggleInteractable(true); - } - private static void SetButtonIcon(GameObject button, Sprite icon) - { - // Find and set the icon for the button - GameObject goIcon = button.FindChildByName("[icon]"); - if (goIcon == null) + + + + // Check if the host pane already exists + if (__instance.HasChildWithName("PaneRight Host")) + return; + + if (basePane == null) { - Multiplayer.LogError("Failed to find icon!"); + Multiplayer.LogError("Failed to find Load/Save pane!"); return; } - goIcon.GetComponent().sprite = icon; - } + // Create a new host pane based on the base pane + basePane.SetActive(false); + GameObject hostPane = GameObject.Instantiate(basePane, basePane.transform.parent); + basePane.SetActive(true); + hostPane.name = "PaneRight Host"; - private static void ResetTooltip(GameObject button) - { - // Reset the tooltip keys for the button - UIElementTooltip tooltip = button.GetComponent(); - tooltip.disabledKey = null; - tooltip.enabledKey = null; + GameObject.Destroy(hostPane.GetComponent()); + GameObject.Destroy(hostPane.GetComponent()); + HostGamePane hp = hostPane.GetOrAddComponent(); + + // Add the host pane to the menu controller + __instance.menuController.controlledMenus.Add(hostPane.GetComponent()); + hostMenuIndex = __instance.menuController.controlledMenus.Count - 1; + //MainMenuController_Awake_Patch.multiplayerButton.GetComponent().requestedMenuIndex = __instance.menuController.controlledMenus.Count - 1; } } } diff --git a/Multiplayer/Settings.cs b/Multiplayer/Settings.cs index 4e2087b..add6071 100644 --- a/Multiplayer/Settings.cs +++ b/Multiplayer/Settings.cs @@ -29,11 +29,14 @@ public class Settings : UnityModManager.ModSettings, IDrawable public int Port = 7777; [Space(10)] + [Header("Lobby Server")] + [Draw("Lobby Server address", Tooltip = "Address of lobby server for finding multiplayer games")] + public string LobbyServerAddress = "http://localhost:8080"; [Header("Last Server Connected to by IP")] [Draw("Last Remote IP", Tooltip = "The IP for the last server connected to by IP.")] public string LastRemoteIP = ""; [Draw("Last Remote Port", Tooltip = "The port for the last server connected to by IP.")] - public int LastRemotePort = 7777; + public ushort LastRemotePort = 7777; [Draw("Last Remote Password", Tooltip = "The password for the last server connected to by IP.")] public string LastRemotePassword = ""; diff --git a/Multiplayer/Utils/DvExtensions.cs b/Multiplayer/Utils/DvExtensions.cs index 745ef94..080c30d 100644 --- a/Multiplayer/Utils/DvExtensions.cs +++ b/Multiplayer/Utils/DvExtensions.cs @@ -1,6 +1,13 @@ using System; +using DV.UI; +using DV.UIFramework; +using DV.Localization; using Multiplayer.Components.Networking.Train; using Multiplayer.Components.Networking.World; +using UnityEngine; +using UnityEngine.UI; + + namespace Multiplayer.Utils; @@ -36,4 +43,52 @@ public static NetworkedRailTrack Networked(this RailTrack railTrack) } #endregion + + #region UI + public static void UpdateButton(this GameObject pane, string oldButtonName, string newButtonName, string localeKey, string toolTipKey, Sprite icon) + { + // Find and rename the button + GameObject button = pane.FindChildByName(oldButtonName); + button.name = newButtonName; + + // Update localization and tooltip + if (button.GetComponentInChildren() != null) + { + button.GetComponentInChildren().key = localeKey; + GameObject.Destroy(button.GetComponentInChildren()); + ResetTooltip(button); + } + + // Set the button icon if provided + if (icon != null) + { + SetButtonIcon(button, icon); + } + + // Enable button interaction + button.GetComponentInChildren().ToggleInteractable(true); + } + + private static void SetButtonIcon(this GameObject button, Sprite icon) + { + // Find and set the icon for the button + GameObject goIcon = button.FindChildByName("[icon]"); + if (goIcon == null) + { + Multiplayer.LogError("Failed to find icon!"); + return; + } + + goIcon.GetComponent().sprite = icon; + } + + private static void ResetTooltip(this GameObject button) + { + // Reset the tooltip keys for the button + UIElementTooltip tooltip = button.GetComponent(); + tooltip.disabledKey = null; + tooltip.enabledKey = null; + } + + #endregion } From 8329b6372dd1fbcd6e8a219d9c7777bb8b62ff11 Mon Sep 17 00:00:00 2001 From: morm075 <124874578+morm075@users.noreply.github.com> Date: Sun, 30 Jun 2024 20:15:56 +0930 Subject: [PATCH 15/34] Updates to locale.csv Added translations to locale.csv, Translations to be verified. --- locale.csv | 72 ++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 46 insertions(+), 26 deletions(-) diff --git a/locale.csv b/locale.csv index 336dac6..42fcd33 100644 --- a/locale.csv +++ b/locale.csv @@ -5,44 +5,64 @@ Key,Description,English,Bulgarian,Chinese (Simplified),Chinese (Traditional),Cze ,"When saving the file, ensure to save it using UTF-8 encoding!",,,,,,,,,,,,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,, ,Main Menu,,,,,,,,,,,,,,,,,,,,,,,,,, -mm/join_server,The 'Join Server' button in the main menu.,Join Server,,,,,,,,Rejoindre le serveur,Spiel beitreten,,,Entra in un Server,,,,,,,,,,Unirse a un servidor,,, -mm/join_server__tooltip,The tooltip shown when hovering over the 'Join Server' button.,Join a multiplayer session.,,,,,,,,Rejoindre une session multijoueur,Trete einer Mehrspielersitzung bei.,,,Entra in una sessione multiplayer.,,,,,,,,,,Únete a una sesión multijugador.,,, +mm/join_server,The 'Join Server' button in the main menu.,Join Server,Присъединете се към сървъра ,加入服务器 ,加入伺服器 ,Připojte se k serveru ,Tilmeld dig server,Kom bij de server,Liity palvelimelle,Rejoindre le serveur,Spiel beitreten,सर्वर में शामिल हों,Csatlakozz a szerverhez,Entra in un Server,サーバーに参加する,서버에 가입,Bli med server,Dołącz do serwera,Conectar-se ao servidor ,Ligar-se ao servidor ,Alăturați-vă serverului,Присоединиться к серверу,Pripojte sa k serveru,Unirse a un servidor,Gå med i servern,Sunucuya katıl,Приєднатися до сервера +mm/join_server__tooltip,The tooltip shown when hovering over the 'Join Server' button.,Join a multiplayer session.,Присъединете се към мултиплейър сесия. ,加入多人游戏会话。 ,加入多人遊戲會話。 ,Připojte se k relaci pro více hráčů. ,Deltag i en multiplayer session.,Neem deel aan een multiplayersessie.,Liity moninpeliistuntoon.,Rejoindre une session multijoueur,Trete einer Mehrspielersitzung bei.,मल्टीप्लेयर सत्र में शामिल हों.,Csatlakozz egy többjátékos munkamenethez.,Entra in una sessione multiplayer.,マルチプレイヤー セッションに参加します。,멀티플레이어 세션에 참여하세요.,Bli med på en flerspillerøkt.,Dołącz do sesji wieloosobowej.,Participe de uma sessão multijogador. ,Participe numa sessão multijogador. ,Alăturați-vă unei sesiuni multiplayer.,Присоединяйтесь к многопользовательской сессии.,Pripojte sa k relácii pre viacerých hráčov.,Únete a una sesión multijugador.,Gå med i en multiplayer-session.,Çok oyunculu bir oturuma katılın.,Приєднуйтеся до багатокористувацької сесії. mm/join_server__tooltip_disabled,Unused,,,,,,,,,,,,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,, ,Server Browser,,,,,,,,,,,,,,,,,,,,,,,,,, -sb/title,The title of the Server Browser tab,Server Browser,,,,,,,,Navigateur de serveurs,Server Liste,,,Ricerca Server,,,,,,,,,,Buscar servidores,,, -sb/manual_connect,Connect to IP,Connect to IP,,,,,,,,,,,,,,,,,,,,,,,, -sb/manual_connect__tooltip,The tooltip shown when hovering over the 'manualconnect' button.,Direct connection to a multiplayer session.,,,,,,,,,,,,,,,,,,,,,,,, +sb/title,The title of the Server Browser tab,Server Browser,Браузър на сървъра ,服务器浏览器 ,伺服器瀏覽器 ,Serverový prohlížeč ,Server browser,Server browser,Palvelimen selain,Navigateur de serveurs,Server-Browser ,सर्वर ब्राउजर,Szerverböngésző,Ricerca Server,サーバーブラウザ,서버 브라우저,Servernettleser,Przeglądarka serwerów,Navegador do servidor ,Navegador do servidor ,Browser server,Браузер серверов,Serverový prehliadač,Buscar servidores,Serverbläddrare,Sunucu tarayıcısı,Браузер сервера +sb/manual_connect,Connect to IP,Connect to IP,Свържете се с IP ,连接到IP ,連接到IP ,Připojte se k IP ,Opret forbindelse til IP,Maak verbinding met IP,Yhdistä IP-osoitteeseen,Connectez-vous à IP,Mit IP verbinden,आईपी ​​से कनेक्ट करें,Csatlakozzon az IP-hez,Connettiti all'IP,IPに接続する,IP에 연결,Koble til IP,Połącz się z IP,Conecte-se ao IP ,Ligue-se ao IP ,Conectați-vă la IP,Подключиться к IP,Pripojte sa k IP,Conéctese a IP,Anslut till IP,IP'ye bağlan,Підключитися до IP +sb/manual_connect__tooltip,The tooltip shown when hovering over the 'manualconnect' button.,Direct connection to a multiplayer session.,Директна връзка към мултиплейър сесия. ,直接连接到多人游戏会话。 ,直接連接到多人遊戲會話。 ,Přímé připojení k relaci pro více hráčů. ,Direkte forbindelse til en multiplayer-session.,Directe verbinding met een multiplayersessie.,Suora yhteys moninpeliistuntoon.,Connexion directe à une session multijoueur.,Direkte Verbindung zu einer Multiplayer-Sitzung.,मल्टीप्लेयर सत्र से सीधा कनेक्शन।,Közvetlen kapcsolat egy többjátékos munkamenethez.,Connessione diretta a una sessione multiplayer.,マルチプレイヤー セッションへの直接接続。,멀티플레이어 세션에 직접 연결됩니다.,Direkte tilkobling til en flerspillerøkt.,Bezpośrednie połączenie z sesją wieloosobową.,Conexão direta a uma sessão multijogador. ,Ligação direta a uma sessão multijogador. ,Conexiune directă la o sesiune multiplayer.,Прямое подключение к многопользовательской сессии.,Priame pripojenie k relácii pre viacerých hráčov.,Conexión directa a una sesión multijugador.,Direktanslutning till en multiplayer-session.,Çok oyunculu bir oturuma doğrudan bağlantı.,Пряме підключення до багатокористувацької сесії. sb/manual_connect__tooltip_disabled,Unused,,,,,,,,,,,,,,,,,,,,,,,,,, -sb/host,Host Game,Host Game,,,,,,,,,,,,,,,,,,,,,,,, -sb/host__tooltip,The tooltip shown when hovering over the 'Host Server' button.,Host a multiplayer session.,,,,,,,,,,,,,,,,,,,,,,,, +sb/host,Host Game,Host Game,Домакин на играта ,主机游戏 ,主機遊戲 ,Hostitelská hra ,Værtsspil,Gastheerspel,Isäntäpeli,Partie hôte,Gastspiel,मेज़बान खेल,Gazdajáték,Ospita il gioco,ホストゲーム,호스트 게임,Vertsspill,Gra gospodarza,Jogo anfitrião ,Jogo anfitrião ,Găzduire joc,Хост-игра,Hostiteľská hra,Juego de acogida,Värdspel,Sunucu Oyunu,Ведуча гра +sb/host__tooltip,The tooltip shown when hovering over the 'Host Server' button.,Host a multiplayer session.,Организирайте сесия за мултиплейър. ,主持多人游戏会话。 ,主持多人遊戲會話。 ,Uspořádejte relaci pro více hráčů. ,Vær vært for en multiplayer-session.,Organiseer een multiplayersessie.,Järjestä moninpeliistunto.,Organisez une session multijoueur.,Veranstalten Sie eine Multiplayer-Sitzung.,एक मल्टीप्लेयर सत्र की मेजबानी करें.,Hozz létre egy többjátékos munkamenetet.,Ospita una sessione multigiocatore.,マルチプレイヤー セッションをホストします。,멀티플레이어 세션을 호스팅하세요.,Vær vert for en flerspillerøkt.,Zorganizuj sesję wieloosobową.,Hospede uma sessão multijogador. ,Acolhe uma sessão multijogador. ,Găzduiește o sesiune multiplayer.,Организуйте многопользовательский сеанс.,Usporiadajte reláciu pre viacerých hráčov.,Organiza una sesión multijugador.,Var värd för en session för flera spelare.,Çok oyunculu bir oturuma ev sahipliği yapın.,Проведіть сеанс для кількох гравців. sb/host__tooltip_disabled,Unused,,,,,,,,,,,,,,,,,,,,,,,,,, -sb/join_game,Join Game,Join Game,,,,,,,,,,,,,,,,,,,,,,,, -sb/join_game__tooltip,The tooltip shown when hovering over the 'Join Server' button.,Join a multiplayer session.,,,,,,,,,,,,,,,,,,,,,,,, +sb/join_game,Join Game,Join Game,Присъединете се към играта ,加入游戏 ,加入遊戲 ,Připojte se ke hře,Deltag i spil,Speel mee,Liity peliin,Rejoins une partie,Spiel beitreten,खेल में शामिल हो,Belépni a játékba,Unisciti al gioco,ゲームに参加します,게임 참여,Bli med i spillet,Dołącz do gry,Entrar no jogo ,Entrar no jogo ,Alatura-te jocului,Присоединиться к игре,Pridať sa do hry,Unete al juego,Gå med i spel,Oyuna katılmak,Приєднуйся до гри +sb/join_game__tooltip,The tooltip shown when hovering over the 'Join Server' button.,Join a multiplayer session.,Присъединете се към мултиплейър сесия. ,加入多人游戏会话。 ,加入多人遊戲會話。 ,Připojte se k relaci pro více hráčů. ,Deltag i en multiplayer session.,Neem deel aan een multiplayersessie.,Liity moninpeliistuntoon.,Rejoignez une session multijoueur.,Nehmen Sie an einer Multiplayer-Sitzung teil.,मल्टीप्लेयर सत्र में शामिल हों.,Csatlakozz egy többjátékos munkamenethez.,Partecipa a una sessione multigiocatore.,マルチプレイヤー セッションに参加します。,멀티플레이어 세션에 참여하세요.,Bli med på en flerspillerøkt.,Dołącz do sesji wieloosobowej.,Participe de uma sessão multijogador.,Participe numa sessão multijogador. ,Alăturați-vă unei sesiuni multiplayer.,Присоединяйтесь к многопользовательской сессии.,Pripojte sa k relácii pre viacerých hráčov.,Únete a una sesión multijugador.,Gå med i en multiplayer-session.,Çok oyunculu bir oturuma katılın.,Приєднуйтеся до багатокористувацької сесії. sb/join_game__tooltip_disabled,Unused,,,,,,,,,,,,,,,,,,,,,,,,,, -sb/Refresh,refresh,Refresh,,,,,,,,,,,,,,,,,,,,,,,, -sb/Refresh__tooltip,The tooltip shown when hovering over the 'Refresh Server' button.,Refresh Server list.,,,,,,,,,,,,,,,,,,,,,,,, +sb/Refresh,refresh,Refresh,Опресняване ,刷新 ,重新整理 ,Obnovit ,Opdater,Vernieuwen,virkistää,Rafraîchir,Aktualisierung,ताज़ा करना,Frissítés,ricaricare,リフレッシュ,새로 고치다,Forfriske,Odświeżać,Atualizar ,Atualizar ,Reîmprospăta,Обновить,Obnoviť,Actualizar,Uppdatera,Yenile,Оновити +sb/Refresh__tooltip,The tooltip shown when hovering over the 'Refresh Server' button.,Refresh Server list.,Обновяване на списъка със сървъри. ,刷新服务器列表。 ,刷新伺服器清單。 ,Obnovit seznam serverů. ,Opdater serverliste.,Vernieuw de serverlijst.,Päivitä palvelinluettelo.,Actualiser la liste des serveurs.,Serverliste aktualisieren.,सर्वर सूची ताज़ा करें.,Szerverlista frissítése.,Aggiorna l'elenco dei server.,サーバーリストを更新します。,서버 목록을 새로 고칩니다.,Oppdater serverlisten.,Odśwież listę serwerów.,Atualizar lista de servidores.,Atualizar lista de servidores. ,Actualizează lista de servere.,Обновить список серверов.,Obnoviť zoznam serverov.,Actualizar la lista de servidores.,Uppdatera serverlistan.,Sunucu listesini yenileyin.,Оновити список серверів. sb/Refresh__tooltip_disabled,Unused,,,,,,,,,,,,,,,,,,,,,,,,,, -sb/ip,IP popup,Enter IP Address,,,,,,,,Entrer l’adresse IP,IP Adresse eingeben,,,Inserire Indirizzo IP,,,,,,,,,,Ingrese la dirección IP,,, -sb/ip_invalid,Invalid IP popup.,Invalid IP Address!,,,,,,,,Adresse IP invalide,Ungültige IP Adresse!,,,Indirizzo IP Invalido!,,,,,,,,,,¡Dirección IP inválida!,,, -sb/port,Port popup.,Enter Port (7777 by default),,,,,,,,Entrer le port (7777 par défaut),Port eingeben (Standard: 7777),,,Inserire Porta (7777 di default),,,,,,,,,,Introduzca el número de puerto(7777 por defecto),,, -sb/port_invalid,Invalid port popup.,Invalid Port!,,,,,,,,Port invalide !,Ungültiger Port!,,,Porta Invalida!,,,,,,,,,,¡Número de Puerto no válido!,,, -sb/password,Password popup.,Enter Password,,,,,,,,Entrer le mot de passe,Passwort eingeben,,,Inserire Password,,,,,,,,,,Introducir la contraseña,,, +sb/ip,IP popup,Enter IP Address,Въведете IP адрес,输入IP地址 ,輸入IP位址 ,Zadejte IP adresu ,Indtast IP-adresse,Voer het IP-adres in,Anna IP-osoite,Entrer l’adresse IP,IP Adresse eingeben,आईपी ​​पता दर्ज करें,Írja be az IP-címet,Inserire Indirizzo IP,IPアドレスを入力してください,IP 주소를 입력하세요,Skriv inn IP-adresse,Wprowadź adres IP,Digite o endereço IP ,Introduza o endereço IP ,Introduceți adresa IP,Введите IP-адрес,Zadajte IP adresu,Ingrese la dirección IP,Ange IP-adress,IP Adresini Girin,Введіть IP-адресу +sb/ip_invalid,Invalid IP popup.,Invalid IP Address!,Невалиден IP адрес! ,IP 地址无效! ,IP 位址無效! ,Neplatná IP adresa! ,Ugyldig IP-adresse!,Ongeldig IP-adres!,Virheellinen IP-osoite!,Adresse IP invalide,Ungültige IP Adresse!,अमान्य आईपी पता!,Érvénytelen IP-cím!,Indirizzo IP Invalido!,IP アドレスが無効です!,IP 주소가 잘못되었습니다!,Ugyldig IP-adresse!,Nieprawidłowy adres IP!,Endereço IP inválido! ,Endereço IP inválido! ,Adresă IP nevalidă!,Неверный IP-адрес!,Neplatná IP adresa!,¡Dirección IP inválida!,Ogiltig IP-adress!,Geçersiz IP adresi!,Недійсна IP-адреса! +sb/port,Port popup.,Enter Port (7777 by default),Въведете порт (7777 по подразбиране) ,输入端口(默认为 7777) ,輸入連接埠(預設為 7777) ,Zadejte port (ve výchozím nastavení 7777),Indtast port (7777 som standard),Poort invoeren (standaard 7777),Anna portti (oletuksena 7777),Entrer le port (7777 par défaut),Port eingeben (Standard: 7777),पोर्ट दर्ज करें (डिफ़ॉल्ट रूप से 7777),Írja be a portot (alapértelmezés szerint 7777),Inserire Porta (7777 di default),ポートを入力します (デフォルトでは 7777),포트 입력(기본적으로 7777),Angi port (7777 som standard),Wprowadź port (domyślnie 7777),Insira a porta (7777 por padrão) ,Introduza a porta (7777 por defeito) ,Introduceți port (7777 implicit),Введите порт (7777 по умолчанию),Zadajte port (predvolene 7777),Introduzca el número de puerto(7777 por defecto),Ange port (7777 som standard),Bağlantı Noktasını Girin (varsayılan olarak 7777),Введіть порт (7777 за замовчуванням) +sb/port_invalid,Invalid port popup.,Invalid Port!,Невалиден порт! ,端口无效! ,埠無效! ,Neplatný port!,Ugyldig port!,Ongeldige poort!,Virheellinen portti!,Port invalide !,Ungültiger Port!,अमान्य पोर्ट!,Érvénytelen port!,Porta Invalida!,ポートが無効です!,포트가 잘못되었습니다!,Ugyldig port!,Nieprawidłowy port!,Porta inválida! ,Porta inválida! ,Port nevalid!,Неверный порт!,Neplatný port!,¡Número de Puerto no válido!,Ogiltig port!,Geçersiz Bağlantı Noktası!,Недійсний порт! +sb/password,Password popup.,Enter Password,Въведете паролата,输入密码 ,輸入密碼 ,Zadejte heslo ,Indtast adgangskode,Voer wachtwoord in,Kirjoita salasana,Entrer le mot de passe,Passwort eingeben,पास वर्ड दर्ज करें,Írd be a jelszót,Inserire Password,パスワードを入力する,암호를 입력,Oppgi passord,Wprowadź hasło,Digite a senha ,Introduza a senha ,Introdu parola,Введите пароль,Zadajte heslo,Introducir la contraseña,Skriv in lösenord,Parolanı Gir,Введіть пароль +,,,,,,,,,,,,,,,,,,,,,,,,,,, +,Server Host,,,,,,,,,,,,,,,,,,,,,,,,,, +host/title,The title of the Host Game page,Host Game,Домакин на играта ,主机游戏 ,主機遊戲 ,Hostitelská hra,Værtsspil,Gastheerspel,Isäntäpeli,Partie hôte,Gastspiel,मेज़बान खेल,Gazdajáték,Ospita il gioco,ホストゲーム,호스트 게임,Vertsspill,Gra gospodarza,Jogo anfitrião ,Jogo anfitrião ,Găzduire joc,Хост-игра,Hostiteľská hra,Juego de acogida,Värdspel,Sunucu Oyunu,Ведуча гра +host/name,Server name field placeholder,Server Name,Име на сървъра ,服务器名称 ,伺服器名稱 ,Název serveru,Server navn,Server naam,Palvelimen nimi,Nom du serveur,Servername,सर्वर का नाम,Szerver név,Nome del server,サーバーの名前,서버 이름,Server navn,Nazwa serwera,Nome do servidor ,Nome do servidor ,Numele serverului,Имя сервера,Názov servera,Nombre del servidor,Server namn,Sunucu adı,Ім'я сервера +host/name__tooltip,Server name field tooltip,The name of the server that other players will see in the server browser,"Името на сървъра, което другите играчи ще видят в сървърния браузър ",其他玩家在服务器浏览器中看到的服务器名称 ,其他玩家在伺服器瀏覽器中看到的伺服器名稱 ,"Název serveru, který ostatní hráči uvidí v prohlížeči serveru","Navnet på den server, som andre spillere vil se i serverbrowseren ",De naam van de server die andere spelers in de serverbrowser zien,"Palvelimen nimi, jonka muut pelaajat näkevät palvelimen selaimessa",Le nom du serveur que les autres joueurs verront dans le navigateur du serveur,"Der Name des Servers, den andere Spieler im Serverbrowser sehen",सर्वर का नाम जो अन्य खिलाड़ी सर्वर ब्राउज़र में देखेंगे,"A szerver neve, amelyet a többi játékos látni fog a szerver böngészőjében",Il nome del server che gli altri giocatori vedranno nel browser del server,他のプレイヤーがサーバー ブラウザに表示するサーバーの名前,다른 플레이어가 서버 브라우저에서 볼 수 있는 서버 이름,Navnet på serveren som andre spillere vil se i servernettleseren,"Nazwa serwera, którą inni gracze zobaczą w przeglądarce serwerów",O nome do servidor que outros jogadores verão no navegador do servidor ,O nome do servidor que os outros jogadores verão no navegador do servidor ,The name of the server that other players will see in the server browser,"Имя сервера, которое другие игроки увидят в браузере серверов.","Názov servera, ktorý ostatní hráči uvidia v prehliadači servera",El nombre del servidor que otros jugadores verán en el navegador del servidor.,Namnet på servern som andra spelare kommer att se i serverwebbläsaren,Diğer oyuncuların sunucu tarayıcısında göreceği sunucunun adı,"Назва сервера, яку інші гравці бачитимуть у браузері сервера" +host/password,Password field placeholder,Password (leave blank for no password),Парола (оставете празно за липса на парола) ,密码(无密码则留空) ,密碼(無密碼則留空) ,"Heslo (nechte prázdné, pokud nechcete heslo)",Adgangskode (lad tom for ingen adgangskode) ,Wachtwoord (leeg laten als er geen wachtwoord is),"Salasana (jätä tyhjäksi, jos et salasanaa)",Mot de passe (laisser vide s'il n'y a pas de mot de passe),"Passwort (leer lassen, wenn kein Passwort vorhanden ist)",पासवर्ड (बिना पासवर्ड के खाली छोड़ें),Jelszó (jelszó nélkül hagyja üresen),Password (lascia vuoto per nessuna password),パスワード (パスワードを使用しない場合は空白のままにします),비밀번호(비밀번호가 없으면 비워두세요),Passord (la det stå tomt for ingen passord),"Hasło (pozostaw puste, jeśli nie ma hasła)",Senha (deixe em branco se não houver senha) ,"Palavra-passe (deixe em branco se não existir palavra-passe) + +",Parola (lasa necompletat pentru nicio parola),"Пароль (оставьте пустым, если пароль отсутствует)","Heslo (nechávajte prázdne, ak nechcete zadať heslo)",Contraseña (dejar en blanco si no hay contraseña),Lösenord (lämna tomt för inget lösenord),Şifre (Şifre yoksa boş bırakın),"Пароль (залиште порожнім, якщо немає пароля)" +host/password__tooltip,Password field placeholder,Password for joining the game. Leave blank if no password is required,"Парола за присъединяване към играта. Оставете празно, ако не се изисква парола ",加入游戏的密码。如果不需要密码则留空 ,加入遊戲的密碼。如果不需要密碼則留空 ,"Heslo pro vstup do hry. Pokud není vyžadováno heslo, ponechte prázdné","Adgangskode for at deltage i spillet. Lad stå tomt, hvis der ikke kræves adgangskode ",Wachtwoord voor deelname aan het spel. Laat dit leeg als er geen wachtwoord vereist is,"Salasana peliin liittymiseen. Jätä tyhjäksi, jos salasanaa ei vaadita",Mot de passe pour rejoindre le jeu. Laisser vide si aucun mot de passe n'est requis,"Passwort für die Teilnahme am Spiel. Lassen Sie das Feld leer, wenn kein Passwort erforderlich ist",गेम में शामिल होने के लिए पासवर्ड. यदि पासवर्ड की आवश्यकता नहीं है तो खाली छोड़ दें,"Jelszó a játékhoz való csatlakozáshoz. Ha nincs szükség jelszóra, hagyja üresen",Password per partecipare al gioco. Lascia vuoto se non è richiesta alcuna password,ゲームに参加するためのパスワード。パスワードが必要ない場合は空白のままにしてください,게임에 참여하기 위한 비밀번호입니다. 비밀번호가 필요하지 않으면 비워두세요,Passord for å bli med i spillet. La det stå tomt hvis du ikke trenger passord,"Hasło umożliwiające dołączenie do gry. Pozostaw puste, jeśli hasło nie jest wymagane",Senha para entrar no jogo. Deixe em branco se nenhuma senha for necessária,Palavra-passe para entrar no jogo. Deixe em branco se não for necessária nenhuma palavra-passe,Parola pentru a intra in joc. Lăsați necompletat dacă nu este necesară o parolă,"Пароль для входа в игру. Оставьте пустым, если пароль не требуется","Heslo pre vstup do hry. Ak heslo nie je potrebné, ponechajte pole prázdne",Contraseña para unirse al juego. Déjelo en blanco si no se requiere contraseña,Lösenord för att gå med i spelet. Lämna tomt om inget lösenord krävs,Oyuna katılmak için şifre. Şifre gerekmiyorsa boş bırakın,"Пароль для входу в гру. Залиште поле порожнім, якщо пароль не потрібен" +host/public,Public checkbox label,Public Game,Публична игра ,公共游戏 ,公開遊戲 ,Veřejná hra,Offentligt spil ,Openbaar spel,Julkinen peli,Jeu public,Öffentliches Spiel,,,Gioco pubblico,パブリックゲーム,공개 게임,Offentlig spill,Gra publiczna,Jogo Público ,Jogo Público ,Joc public,Публичная игра,Verejná hra,Juego público,Offentligt spel,Halka Açık Oyun,Громадська гра +host/public__tooltip,Public checkbox tooltip,List this game in the server browser.,Избройте тази игра в браузъра на сървъра. ,在服务器浏览器中列出该游戏。 ,在伺服器瀏覽器中列出該遊戲。 ,Vypište tuto hru v prohlížeči serveru. ,List dette spil i serverbrowseren.,Geef dit spel weer in de serverbrowser.,Listaa tämä peli palvelimen selaimeen.,Listez ce jeu dans le navigateur du serveur.,Listen Sie dieses Spiel im Serverbrowser auf.,इस गेम को सर्वर ब्राउज़र में सूचीबद्ध करें।,Listázza ezt a játékot a szerver böngészőjében.,Elenca questo gioco nel browser del server.,このゲームをサーバー ブラウザーにリストします。,서버 브라우저에 이 게임을 나열하세요.,List dette spillet i servernettleseren.,Dodaj tę grę do przeglądarki serwerów.,Liste este jogo no navegador do servidor. ,Liste este jogo no browser do servidor. ,Listați acest joc în browserul serverului.,Добавьте эту игру в браузер серверов.,Uveďte túto hru v prehliadači servera.,Incluya este juego en el navegador del servidor.,Lista detta spel i serverwebbläsaren.,Bu oyunu sunucu tarayıcısında listeleyin.,Показати цю гру в браузері сервера. +host/details,Details field placeholder,Enter some details about your server,Въведете някои подробности за вашия сървър ,输入有关您的服务器的一些详细信息 ,輸入有關您的伺服器的一些詳細信息 ,Zadejte nějaké podrobnosti o vašem serveru ,Indtast nogle detaljer om din server,Voer enkele gegevens over uw server in,Anna joitain tietoja palvelimestasi,Entrez quelques détails sur votre serveur,Geben Sie einige Details zu Ihrem Server ein,अपने सर्वर के बारे में कुछ विवरण दर्ज करें,Adjon meg néhány adatot a szerveréről,Inserisci alcuni dettagli sul tuo server,サーバーに関する詳細を入力します,서버에 대한 세부 정보를 입력하세요.,Skriv inn noen detaljer om serveren din,Wprowadź kilka szczegółów na temat swojego serwera,Insira alguns detalhes sobre o seu servidor ,Introduza alguns detalhes sobre o seu servidor ,Introduceți câteva detalii despre serverul dvs,Введите некоторые сведения о вашем сервере,Zadajte nejaké podrobnosti o svojom serveri,Ingrese algunos detalles sobre su servidor,Ange några detaljer om din server,Sunucunuzla ilgili bazı ayrıntıları girin,Введіть деякі відомості про ваш сервер +host/details__tooltip,Details field tooltip,Details about your server visible in the server browser.,"Подробности за вашия сървър, видими в сървърния браузър. ",有关服务器的详细信息在服务器浏览器中可见。 ,有關伺服器的詳細資訊在伺服器瀏覽器中可見。 ,Podrobnosti o vašem serveru viditelné v prohlížeči serveru. ,Detaljer om din server er synlige i serverbrowseren.,Details over uw server zichtbaar in de serverbrowser.,Palvelimesi tiedot näkyvät palvelimen selaimessa.,Détails sur votre serveur visibles dans le navigateur du serveur.,Details zu Ihrem Server im Serverbrowser sichtbar.,आपके सर्वर के बारे में विवरण सर्वर ब्राउज़र में दिखाई देता है।,A szerver böngészőjében láthatók a szerver adatai.,Dettagli sul tuo server visibili nel browser del server.,サーバーブラウザに表示されるサーバーに関する詳細。,서버 브라우저에 표시되는 서버에 대한 세부정보입니다.,Detaljer om serveren din er synlig i servernettleseren.,Szczegóły dotyczące Twojego serwera widoczne w przeglądarce serwerów.,Detalhes sobre o seu servidor visíveis no navegador do servidor.,Detalhes sobre o seu servidor visíveis no browser do servidor.,Detalii despre serverul dvs. vizibile în browserul serverului.,Подробная информация о вашем сервере отображается в браузере серверов.,Podrobnosti o vašom serveri viditeľné v prehliadači servera.,Detalles sobre su servidor visibles en el navegador del servidor.,Detaljer om din server visas i serverwebbläsaren.,Sunucunuzla ilgili ayrıntılar sunucu tarayıcısında görünür.,Детальна інформація про ваш сервер відображається в браузері сервера. +host/max_players,Maximum players slider label,Maximum Players,Максимален брой играчи ,最大玩家数 ,最大玩家數 ,Maximální počet hráčů ,Maksimalt antal spillere,Maximale spelers,Pelaajien enimmäismäärä,,Maximale Spielerzahl,अधिकतम खिलाड़ी,Maximális játékosok száma,Giocatori massimi,最大プレイヤー数,최대 플레이어,Maksimalt antall spillere,Maksymalna liczba graczy,Máximo de jogadores ,Máximo de jogadores ,Jucători maxim,Максимальное количество игроков,Maximálny počet hráčov,Personas máximas,Maximalt antal spelare,Maksimum Oyuncu,Максимальна кількість гравців +host/max_players__tooltip,Maximum players slider tooltip,Maximum players allowed to join the game.,"Максимален брой играчи, разрешени да се присъединят към играта. ",允许加入游戏的最大玩家数。 ,允許加入遊戲的最大玩家數。 ,"Maximální počet hráčů, kteří se mohou připojit ke hře.",Maksimalt antal spillere tilladt at deltage i spillet.,Maximaal aantal spelers dat aan het spel mag deelnemen.,Peliin saa osallistua maksimissaan pelaajia.,Nombre maximum de joueurs autorisés à rejoindre le jeu.,"Maximal zulässige Anzahl an Spielern, die dem Spiel beitreten dürfen.",अधिकतम खिलाड़ियों को खेल में शामिल होने की अनुमति।,Maximum játékos csatlakozhat a játékhoz.,Numero massimo di giocatori autorizzati a partecipare al gioco.,ゲームに参加できる最大プレイヤー数。,게임에 참여할 수 있는 최대 플레이어 수입니다.,Maksimalt antall spillere som får være med i spillet.,"Maksymalna liczba graczy, którzy mogą dołączyć do gry.",Máximo de jogadores autorizados a entrar no jogo. ,Máximo de jogadores autorizados a entrar no jogo. ,Numărul maxim de jucători permis să se alăture jocului.,"Максимальное количество игроков, которым разрешено присоединиться к игре.",Do hry sa môže zapojiť maximálny počet hráčov.,Número máximo de jugadores permitidos para unirse al juego.,Maximalt antal spelare som får gå med i spelet.,Oyuna katılmasına izin verilen maksimum oyuncu.,"Максимальна кількість гравців, які можуть приєднатися до гри." +host/start,Maximum players slider label,Start,Започнете,开始 ,開始,Start,Start,Begin,alkaa,Commencer,Start,शुरू,Rajt,Inizio,始める,시작,Start,Początek,Começar ,Iniciar ,start,Начинать,Štart,Comenzar,Start,Başlangıç,Почніть +host/start__tooltip,Maximum players slider tooltip,Start the server.,Стартирайте сървъра. ,启动服务器。 ,啟動伺服器。 ,Spusťte server.,Start serveren.,Start de server.,Käynnistä palvelin.,Démarrez le serveur.,Starten Sie den Server.,सर्वर प्रारंभ करें.,Indítsa el a szervert.,Avviare il server.,サーバーを起動します。,서버를 시작합니다.,Start serveren.,Uruchom serwer.,Inicie o servidor. ,Inicie o servidor. ,Porniți serverul.,Запустите сервер.,Spustite server.,Inicie el servidor.,Starta servern.,Sunucuyu başlatın.,Запустіть сервер. +host/start__tooltip_disabled,Maximum players slider tooltip,Check your settings are valid.,Проверете дали вашите настройки са валидни. ,检查您的设置是否有效。 ,檢查您的設定是否有效。 ,"Zkontrolujte, zda jsou vaše nastavení platná. ",Tjek at dine indstillinger er gyldige.,Controleer of uw instellingen geldig zijn.,"Tarkista, että asetuksesi ovat oikein.",Vérifiez que vos paramètres sont valides.,"Überprüfen Sie, ob Ihre Einstellungen gültig sind.",जांचें कि आपकी सेटिंग्स वैध हैं।,"Ellenőrizze, hogy a beállítások érvényesek-e.",Controlla che le tue impostazioni siano valide.,設定が有効であることを確認してください。,설정이 유효한지 확인하세요.,Sjekk at innstillingene dine er gyldige.,"Sprawdź, czy ustawienia są prawidłowe.",Verifique se suas configurações são válidas. ,Verifique se as suas definições são válidas. ,Verificați că setările dvs. sunt valide.,"Убедитесь, что ваши настройки действительны.","Skontrolujte, či sú vaše nastavenia platné.",Verifique que su configuración sea válida.,Kontrollera att dina inställningar är giltiga.,Ayarlarınızın geçerli olup olmadığını kontrol edin.,Перевірте правильність ваших налаштувань. ,,,,,,,,,,,,,,,,,,,,,,,,,,, ,Disconnect Reason,,,,,,,,,,,,,,,,,,,,,,,,,, -dr/invalid_password,Invalid password popup.,Invalid Password!,,,,,,,,Mot de passe incorrect !,Ungültiges Passwort!,,,Password non valida!,,,,,,,,,,¡Contraseña invalida!,,, -dr/game_version,Different game versions.,"Game version mismatch! Server version: {0}, your version: {1}.",,,,,,,,"Version du jeu incompatible ! Version du serveur : {0}, version locale : {1}","Spielversion stimmt nicht überein! Server Version: {0}, Lokale Version: {1}.",,,"Versioni del gioco non combacianti! Versione del Server: {0}, La tua versione: {1}.",,,,,,,,,,"¡La versión del juego no coincide! Versión del servidor: {0}, tu versión: {1}.",,, -dr/full_server,The server is already full.,The server is full!,,,,,,,,Le serveur est complet !,Der Server ist voll!,,,Il Server è pieno!,,,,,,,,,,¡El servidor está lleno!,,, -dr/mods,"The client is missing, or has extra mods.",Mod mismatch!,,,,,,,,Mod incompatible !,Mods stimmen nicht überein!,,,Mod non combacianti!,,,,,,,,,,"Falta el cliente, o tiene modificaciones adicionales.",,, -dr/mods_missing,The list of missing mods.,Missing Mods:\n- {0},,,,,,,,Mods manquants:\n-{0},Fehlende Mods:\n- {0},,,Mod Mancanti:\n- {0},,,,,,,,,,Mods faltantes:\n- {0},,, -dr/mods_extra,The list of extra mods.,Extra Mods:\n- {0},,,,,,,,Mods extras:\n-{0},Zusätzliche Mods:\n- {0},,,Mod Extra:\n- {0},,,,,,,,,,Modificaciones adicionales:\n- {0},,, +dr/invalid_password,Invalid password popup.,Invalid Password!,Невалидна парола! ,无效的密码! ,無效的密碼! ,Neplatné heslo! ,Forkert kodeord!,Ongeldig wachtwoord!,Väärä salasana!,Mot de passe incorrect !,Ungültiges Passwort!,अवैध पासवर्ड!,Érvénytelen jelszó!,Password non valida!,無効なパスワード!,유효하지 않은 비밀번호!,Ugyldig passord!,Nieprawidłowe hasło!,Senha inválida! ,Verifique se as suas definições são válidas. ,Parolă Invalidă!,Неверный пароль!,Nesprávne heslo!,¡Contraseña invalida!,Felaktigt lösenord!,Geçersiz şifre!,Невірний пароль! +dr/game_version,Different game versions.,"Game version mismatch! Server version: {0}, your version: {1}.","Несъответствие на версията на играта! Версия на сървъра: {0}, вашата версия: {1}. ",游戏版本不匹配!服务器版本:{0},您的版本:{1}。 ,遊戲版本不符!伺服器版本:{0},您的版本:{1}。 ,"Nesoulad verze hry! Verze serveru: {0}, vaše verze: {1}.","Spilversionen stemmer ikke overens! Serverversion: {0}, din version: {1}.","Spelversie komt niet overeen! Serverversie: {0}, jouw versie: {1}.","Peliversio ei täsmää! Palvelimen versio: {0}, sinun versiosi: {1}.","Version du jeu incompatible ! Version du serveur : {0}, version locale : {1}","Spielversion stimmt nicht überein! Server Version: {0}, Lokale Version: {1}.","गेम संस्करण बेमेल! सर्वर संस्करण: {0}, आपका संस्करण: {1}.","Nem egyezik a játék verziója! Szerververzió: {0}, az Ön verziója: {1}.","Versioni del gioco non combacianti! Versione del Server: {0}, La tua versione: {1}.",ゲームのバージョンが不一致です!サーバーのバージョン: {0}、あなたのバージョン: {1}。,"게임 버전이 일치하지 않습니다! 서버 버전: {0}, 귀하의 버전: {1}.","Spillversjonen samsvarer ikke! Serverversjon: {0}, din versjon: {1}.","Niezgodna wersja gry! Wersja serwera: {0}, Twoja wersja: {1}.","Incompatibilidade de versão do jogo! Versão do servidor: {0}, sua versão: {1}. ","Incompatibilidade de versão do jogo! Versão do servidor: {0}, a sua versão: {1}. ","Versiunea jocului nepotrivită! Versiunea serverului: {0}, versiunea dvs.: {1}.","Несоответствие версии игры! Версия сервера: {0}, ваша версия: {1}.","Nesúlad verzie hry! Verzia servera: {0}, vaša verzia: {1}.","¡La versión del juego no coincide! Versión del servidor: {0}, tu versión: {1}.","Spelversionen matchar inte! Serverversion: {0}, din version: {1}.","Oyun sürümü uyuşmazlığı! Sunucu sürümü: {0}, sürümünüz: {1}.","Невідповідність версії гри! Версія сервера: {0}, ваша версія: {1}." +dr/full_server,The server is already full.,The server is full!,Сървърът е пълен! ,服务器已满! ,伺服器已滿! ,Server je plný! ,Serveren er fuld!,De server is vol!,Palvelin täynnä!,Le serveur est complet !,Der Server ist voll!,सर्वर पूर्ण है!,Tele a szerver!,Il Server è pieno!,サーバーがいっぱいです!,서버가 꽉 찼어요!,Serveren er full!,Serwer jest pełny!,O servidor está cheio! ,O servidor está cheio! ,Serverul este plin!,Сервер переполнен!,Server je plný!,¡El servidor está lleno!,Servern är full!,Sunucu dolu!,Сервер заповнений! +dr/mods,"The client is missing, or has extra mods.",Mod mismatch!,Несъответствие на мода! ,模组不匹配! ,模組不符! ,Neshoda modů!,Mod uoverensstemmelse! ,Mod-mismatch!,Modi ei täsmää!,Mod incompatible !,Mods stimmen nicht überein!,मॉड बेमेल!,Mod eltérés!,Mod non combacianti!,モジュールが不一致です!,모드 불일치!,Moduoverensstemmelse!,Niezgodność modów!,Incompatibilidade de mod! ,"Incompatibilidade de mod! + +",Nepotrivire mod!,Несоответствие модов!,Nezhoda modov!,"Falta el cliente, o tiene modificaciones adicionales.",Mod-felmatchning!,Mod uyumsuzluğu!,Невідповідність модів! +dr/mods_missing,The list of missing mods.,Missing Mods:\n- {0},Липсващи модификации:\n- {0} ,缺少模组:\n- {0} ,缺少模組:\n- {0} ,Chybějící mody:\n- {0},Manglende mods:\n- {0} ,Ontbrekende mods:\n- {0},Puuttuvat modit:\n- {0},Mods manquants:\n-{0},Fehlende Mods:\n- {0},गुम मॉड्स:\n- {0},Hiányzó modok:\n- {0},Mod Mancanti:\n- {0},不足している MOD:\n- {0},누락된 모드:\n- {0},Manglende modi:\n- {0},Brakujące mody:\n- {0},Modificações ausentes:\n- {0} ,Modificações em falta:\n- {0} ,Moduri lipsă:\n- {0},Отсутствующие моды:\n- {0},Chýbajúce modifikácie:\n- {0},Mods faltantes:\n- {0},Mods saknas:\n- {0},Eksik Modlar:\n- {0},Відсутні моди:\n- {0} +dr/mods_extra,The list of extra mods.,Extra Mods:\n- {0},Допълнителни модификации:\n- {0} ,额外模组:\n- {0} ,額外模組:\n- {0} ,Extra modifikace:\n- {0},Ekstra mods:\n- {0} ,Extra aanpassingen:\n- {0},Lisämodit:\n- {0},Mods extras:\n-{0},Zusätzliche Mods:\n- {0},अतिरिक्त मॉड:\n- {0},Extra modok:\n- {0},Mod Extra:\n- {0},追加の Mod:\n- {0},추가 모드:\n- {0},Ekstra modi:\n- {0},Dodatkowe mody:\n- {0},Modificações extras:\n- {0} ,Modificações extra:\n- {0} ,Moduri suplimentare:\n- {0},Дополнительные моды:\n- {0},Extra modifikácie:\n- {0},Modificaciones adicionales:\n- {0},Extra mods:\n- {0},Ekstra Modlar:\n- {0},Додаткові моди:\n- {0} ,,,,,,,,,,,,,,,,,,,,,,,,,,, ,Career Manager,,,,,,,,,,,,,,,,,,,,,,,,,, -carman/fees_host_only,Text shown when a client tries to manage fees.,Only the host can manage fees!,,,,,,,,Seul l'hôte peut gérer les frais !,Nur der Host kann Gebühren verwalten!,,,Solo l’Host può gestire gli addebiti!,,,,,,,,,,¡Solo el anfitrión puede administrar las tarifas!,,, +carman/fees_host_only,Text shown when a client tries to manage fees.,Only the host can manage fees!,Само домакинът може да управлява таксите! ,只有房东可以管理费用! ,只有房東可以管理費用! ,Poplatky může spravovat pouze hostitel!,Kun værten kan administrere gebyrer!,Alleen de host kan de kosten beheren!,Vain isäntä voi hallita maksuja!,Seul l'hôte peut gérer les frais !,Nur der Host kann Gebühren verwalten!,केवल मेज़बान ही फीस का प्रबंधन कर सकता है!,Csak a házigazda kezelheti a díjakat!,Solo l’Host può gestire gli addebiti!,料金を管理できるのはホストだけです。,호스트만이 수수료를 관리할 수 있습니다!,Bare verten kan administrere gebyrer!,Tylko gospodarz może zarządzać opłatami!,Somente o anfitrião pode gerenciar as taxas! ,Só o anfitrião pode gerir as taxas! ,Doar gazda poate gestiona taxele!,Только хозяин может управлять комиссией!,Poplatky môže spravovať iba hostiteľ!,¡Solo el anfitrión puede administrar las tarifas!,Endast värden kan hantera avgifter!,Ücretleri yalnızca ev sahibi yönetebilir!,Тільки господар може керувати оплатою! ,,,,,,,,,,,,,,,,,,,,,,,,,,, ,Player List,,,,,,,,,,,,,,,,,,,,,,,,,, -plist/title,The title of the player list.,Online Players,,,,,,,,Joueurs en ligne,Verbundene Spieler,,,Giocatori Online,,,,,,,,,,Jugadores en línea,,, +plist/title,The title of the player list.,Online Players,Онлайн играчи ,在线玩家 ,線上玩家 ,Online hráči,Online spillere,Online spelers,Online-pelaajat,Joueurs en ligne,Verbundene Spieler,ऑनलाइन खिलाड़ी,Online játékosok,Giocatori Online,,온라인 플레이어,Online spillere,Gracze sieciowi,Jogadores on-line ,Jogadores on-line ,Jucători online,Онлайн-игроки,Online hráči,Jugadores en línea,Spelare online,Çevrimiçi Oyuncular,Онлайн гравці ,,,,,,,,,,,,,,,,,,,,,,,,,,, ,Loading Info,,,,,,,,,,,,,,,,,,,,,,,,,, -linfo/wait_for_server,Text shown in the loading screen.,Waiting for server to load,,,,,,,,En attente du chargement du serveur,Warte auf das Laden des Servers,,,In attesa del caricamento del Server,,,,,,,,,,Esperando a que cargue el servidor...,,, -linfo/sync_world_state,Text shown in the loading screen.,Syncing world state,,,,,,,,Synchronisation des données du monde,Synchronisiere Daten,,,Sincronizzazione dello stato del mondo,,,,,,,,,,Sincronizando estado global,,, +linfo/wait_for_server,Text shown in the loading screen.,Waiting for server to load,Изчаква се зареждане на сървъра ,等待服务器加载 ,等待伺服器加載 ,Čekání na načtení serveru ,"Venter på, at serveren indlæses ",Wachten tot de server is geladen,Odotetaan palvelimen latautumista,En attente du chargement du serveur,Warte auf das Laden des Servers,सर्वर लोड होने की प्रतीक्षा की जा रही है,Várakozás a szerver betöltésére,In attesa del caricamento del Server,サーバーがロードされるのを待っています,서버가 로드되기를 기다리는 중,Venter på at serveren skal lastes,Czekam na załadowanie serwera,Esperando o servidor carregar ,sperando que o servidor carregue ,Se așteaptă încărcarea serverului,Ожидание загрузки сервера,Čaká sa na načítanie servera,Esperando a que cargue el servidor...,Väntar på att servern ska laddas,Sunucunun yüklenmesi bekleniyor,Очікування завантаження сервера +linfo/sync_world_state,Text shown in the loading screen.,Syncing world state,Синхронизиране на световното състояние ,同步世界状态 ,同步世界狀態 ,Synchronizace světového stavu ,Synkroniserer verdensstaten,Het synchroniseren van de wereldstaat,Synkronoidaan maailmantila,Synchronisation des données du monde,Synchronisiere Daten,सिंक हो रही विश्व स्थिति,Szinkronizáló világállapot,Sincronizzazione dello stato del mondo,世界状態を同期しています,세계 상태 동기화 중,Synkroniserer verdensstaten,Synchronizacja stanu świata,Sincronizando o estado mundial ,Sincronizando o estado mundial ,Sincronizarea stării mondiale,Синхронизация состояния мира,Synchronizácia svetového štátu,Sincronizando estado global,Synkroniserar världsstaten,Dünya durumunu senkronize etme,Синхронізація стану світу From a7ae049063b1f25be82516ae66c0983171e26937 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 30 Jun 2024 20:51:08 +1000 Subject: [PATCH 16/34] Server browser and lobby server working Server browser now works, as well as the host game panel. When a multiplayer game starts, the game registers itself with the lobby server and continues to provide updates while the session is active. When the session deactivates the lobby server is notified to remove the game server. More work required on GUI --- Lobby Servers/PHP Server/.htaccess | 7 + Lobby Servers/PHP Server/FlatfileDatabase.php | 6 +- Lobby Servers/PHP Server/MySQLDatabase.php | 6 +- Lobby Servers/PHP Server/Read Me.md | 3 + Lobby Servers/PHP Server/index.php | 7 +- Lobby Servers/RestAPI.md | 1 + Lobby Servers/Rust Server/src/handlers.rs | 2 +- .../Components/MainMenu/HostGamePane.cs | 354 ++++++++++++++++-- .../IServerBrowserGameDetails.cs | 4 +- .../Components/MainMenu/ServerBrowserPane.cs | 19 +- .../Components/Networking/NetworkLifecycle.cs | 31 +- Multiplayer/Locale.cs | 26 +- .../Networking/Data/LobbyServerData.cs | 150 ++++++++ .../Data/LobbyServerResponseData.cs | 23 ++ .../Networking/Data/LobbyServerUpdateData.cs | 36 ++ Multiplayer/Networking/Data/ServerData.cs | 67 ---- .../Networking/Managers/NetworkManager.cs | 2 +- .../Managers/Server/LobbyServerManager.cs | 176 +++++++++ .../Managers/Server/NetworkServer.cs | 31 +- .../MainMenu/LauncherControllerPatch.cs | 46 ++- .../MainMenu/MainMenuControllerPatch.cs | 14 +- .../MainMenu/RightPaneControllerPatch.cs | 9 +- .../Patches/World/SaveGameManagerPatch.cs | 2 +- Multiplayer/Settings.cs | 6 +- Multiplayer/Utils/DvExtensions.cs | 19 +- locale.csv | 24 +- 26 files changed, 935 insertions(+), 136 deletions(-) create mode 100644 Lobby Servers/PHP Server/.htaccess create mode 100644 Multiplayer/Networking/Data/LobbyServerData.cs create mode 100644 Multiplayer/Networking/Data/LobbyServerResponseData.cs create mode 100644 Multiplayer/Networking/Data/LobbyServerUpdateData.cs delete mode 100644 Multiplayer/Networking/Data/ServerData.cs create mode 100644 Multiplayer/Networking/Managers/Server/LobbyServerManager.cs diff --git a/Lobby Servers/PHP Server/.htaccess b/Lobby Servers/PHP Server/.htaccess new file mode 100644 index 0000000..44f3fb2 --- /dev/null +++ b/Lobby Servers/PHP Server/.htaccess @@ -0,0 +1,7 @@ +# Enable the RewriteEngine +RewriteEngine On + +# Redirect all non-existing paths to index.php +RewriteCond %{REQUEST_FILENAME} !-f +RewriteCond %{REQUEST_FILENAME} !-d +RewriteRule ^ index.php [QSA,L] \ No newline at end of file diff --git a/Lobby Servers/PHP Server/FlatfileDatabase.php b/Lobby Servers/PHP Server/FlatfileDatabase.php index 13f7566..9634991 100644 --- a/Lobby Servers/PHP Server/FlatfileDatabase.php +++ b/Lobby Servers/PHP Server/FlatfileDatabase.php @@ -1,4 +1,5 @@ writeData($servers); - return json_encode(["game_server_id" => $data['game_server_id']]); + return json_encode([ + "game_server_id" => $data['game_server_id'], + "private_key" => $data['private_key'] + ]); } public function updateGameServer($data) { diff --git a/Lobby Servers/PHP Server/MySQLDatabase.php b/Lobby Servers/PHP Server/MySQLDatabase.php index b92119d..32a774e 100644 --- a/Lobby Servers/PHP Server/MySQLDatabase.php +++ b/Lobby Servers/PHP Server/MySQLDatabase.php @@ -1,4 +1,5 @@ $data['server_info'], ':last_update' => time() //use current time ]); - return json_encode(["game_server_id" => $data['game_server_id']]); + return json_encode([ + "game_server_id" => $data['game_server_id'], + "private_key" => $data['private_key'] + ]); } public function updateGameServer($data) { diff --git a/Lobby Servers/PHP Server/Read Me.md b/Lobby Servers/PHP Server/Read Me.md index 77e65ba..4753196 100644 --- a/Lobby Servers/PHP Server/Read Me.md +++ b/Lobby Servers/PHP Server/Read Me.md @@ -7,12 +7,15 @@ As this implementation is not persistent in memory, a database is used to store ## Installing +The following instructions assume you will be using an Apache web server and may need to be modified for other configurations. + 1. Copy the following files to your public html folder (consult your web server/web host's documentation) ``` index.php DatabaseInterface.php FlatfileDatabase.php MySQLDatabase.php +.htaccess ``` 2. Copy `config.php` to a secure location outside of your public html directory 3. Edit `index.php` and update the path to the config file on line 2: diff --git a/Lobby Servers/PHP Server/index.php b/Lobby Servers/PHP Server/index.php index ca44d4f..556e828 100644 --- a/Lobby Servers/PHP Server/index.php +++ b/Lobby Servers/PHP Server/index.php @@ -49,14 +49,12 @@ function add_game_server($db, $data) { return json_encode(["error" => "Invalid server information"]); } - $data['game_server_id'] = uuid_create(); - $data['private_key'] = generate_private_key(); - if (!isset($data['ip']) || !filter_var($data['ip'], FILTER_VALIDATE_IP)) { $data['ip'] = $_SERVER['REMOTE_ADDR']; } - $data['last_update'] = time(); + $data['game_server_id'] = uuid_create(); + $data['private_key'] = generate_private_key(); return $db->addGameServer($data); } @@ -83,6 +81,7 @@ function list_game_servers($db) { // Remove private keys from the servers before returning foreach ($servers as &$server) { unset($server['private_key']); + unset($server['last_update']); } return json_encode($servers); } diff --git a/Lobby Servers/RestAPI.md b/Lobby Servers/RestAPI.md index ce5ed94..4309b2c 100644 --- a/Lobby Servers/RestAPI.md +++ b/Lobby Servers/RestAPI.md @@ -16,6 +16,7 @@ The game_mode field in the request body for adding a game server must be one of - 0: Career - 1: Sandbox +- 2: Scenario ### Difficulty Levels diff --git a/Lobby Servers/Rust Server/src/handlers.rs b/Lobby Servers/Rust Server/src/handlers.rs index 71bc9a5..76eec8d 100644 --- a/Lobby Servers/Rust Server/src/handlers.rs +++ b/Lobby Servers/Rust Server/src/handlers.rs @@ -38,7 +38,7 @@ pub async fn add_server(data: web::Data, server_info: web::Json continueCareerRequested; #region setup private void Awake() { Multiplayer.Log("HostGamePane Awake()"); - + CleanUI(); BuildUI(); + ValidateInputs(null); + } - + private void Start() + { + Multiplayer.Log("HostGamePane Start()"); + } private void OnEnable() { - Multiplayer.Log("HostGamePane OnEnable()"); + //Multiplayer.Log("HostGamePane OnEnable()"); this.SetupListeners(true); } @@ -47,22 +77,226 @@ private void OnDisable() this.SetupListeners(false); } + private void CleanUI() + { + //top elements + GameObject.Destroy(this.FindChildByName("Text Content")); + + //body elements + GameObject.Destroy(this.FindChildByName("GRID VIEW")); + GameObject.Destroy(this.FindChildByName("HardcoreSavingBanner")); + GameObject.Destroy(this.FindChildByName("TutorialSavingBanner")); + + //footer elements + GameObject.Destroy(this.FindChildByName("ButtonIcon OpenFolder")); + GameObject.Destroy(this.FindChildByName("ButtonIcon Rename")); + GameObject.Destroy(this.FindChildByName("ButtonIcon Delete")); + GameObject.Destroy(this.FindChildByName("ButtonTextIcon Load")); + GameObject.Destroy(this.FindChildByName("ButtonTextIcon Overwrite")); + + } private void BuildUI() { - + //Create Prefabs + GameObject goMMC = GameObject.FindObjectOfType().gameObject; + + GameObject dividerPrefab = goMMC.FindChildByName("Divider"); + if (dividerPrefab == null) + { + Debug.Log("Divider not found!"); + return; + } + + GameObject cbPrefab = goMMC.FindChildByName("CheckboxFreeCam"); + if (cbPrefab == null) + { + Debug.Log("CheckboxFreeCam not found!"); + return; + } + + GameObject sliderPrefab = goMMC.FindChildByName("SliderLimitSession"); + if (sliderPrefab == null) + { + Debug.Log("SliderLimitSession not found!"); + return; + } + + GameObject inputPrefab = MainMenuThingsAndStuff.Instance.renamePopupPrefab.gameObject.FindChildByName("TextFieldTextIcon"); + if (inputPrefab == null) + { + Debug.Log("TextFieldTextIcon not found!"); + return; + } + + + lcInstance = goMMC.FindChildByName("PaneRight Launcher").GetComponent(); + if (lcInstance == null) + { + Debug.Log("No Run Button"); + return; + } + Sprite playSprite = lcInstance.runButton.FindChildByName("[icon]").GetComponent().sprite; + + + //update title + GameObject titleObj = this.FindChildByName("Title"); + GameObject.Destroy(titleObj.GetComponentInChildren()); + titleObj.GetComponentInChildren().key = Locale.SERVER_HOST__TITLE_KEY; + titleObj.GetComponentInChildren().UpdateLocalization(); + + + //Find scrolling viewport + ScrollRect scroller = this.FindChildByName("Scroll View").GetComponent(); + RectTransform scrollerRT = scroller.transform.GetComponent(); + scrollerRT.sizeDelta = new Vector2(scrollerRT.sizeDelta.x, 504); + + // Create the content object + GameObject controls = new GameObject("Controls"); + controls.SetLayersRecursive(Layers.UI); + controls.transform.SetParent(scroller.viewport.transform, false); + + // Assign the content object to the ScrollRect + RectTransform contentRect = controls.AddComponent(); + contentRect.anchorMin = new Vector2(0, 1); + contentRect.anchorMax = new Vector2(1, 1); + contentRect.pivot = new Vector2(0f, 1); + contentRect.anchoredPosition = new Vector2(0, 21); + contentRect.sizeDelta = scroller.viewport.sizeDelta; + scroller.content = contentRect; + + // Add VerticalLayoutGroup and ContentSizeFitter + VerticalLayoutGroup layoutGroup = controls.AddComponent(); + layoutGroup.childControlWidth = false; + layoutGroup.childControlHeight = false; + layoutGroup.childScaleWidth = false; + layoutGroup.childScaleHeight = false; + layoutGroup.childForceExpandWidth = true; + layoutGroup.childForceExpandHeight = true; + + layoutGroup.spacing = 0; // Adjust the spacing as needed + layoutGroup.padding = new RectOffset(0,0,0,0); + + ContentSizeFitter sizeFitter = controls.AddComponent(); + sizeFitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize; + + GameObject go = GameObject.Instantiate(inputPrefab, NewContentGroup(controls, scroller.viewport.sizeDelta).transform,false); + go.name = "Server Name"; + //go.AddComponent(); + serverName = go.GetComponent(); + serverName.text = Multiplayer.Settings.ServerName?.Trim().Substring(0,Mathf.Min(Multiplayer.Settings.ServerName.Trim().Length,MAX_SERVER_NAME_LEN)); + serverName.placeholder.GetComponent().text = Locale.SERVER_HOST_NAME; + serverName.characterLimit = MAX_SERVER_NAME_LEN; + go.AddComponent(); + go.ResetTooltip(); + + + go = GameObject.Instantiate(inputPrefab, NewContentGroup(controls, scroller.viewport.sizeDelta).transform, false); + go.name = "Password"; + password = go.GetComponent(); + password.text = Multiplayer.Settings.Password; + password.contentType = TMP_InputField.ContentType.Password; + password.placeholder.GetComponent().text = Locale.SERVER_HOST_PASSWORD; + go.AddComponent();//.enabledKey = Locale.SERVER_HOST_PASSWORD__TOOLTIP_KEY; + go.ResetTooltip(); + + + go = GameObject.Instantiate(cbPrefab, NewContentGroup(controls, scroller.viewport.sizeDelta).transform, false); + go.name = "Public"; + TMP_Text label = go.FindChildByName("text").GetComponent(); + label.text = "Public Game"; + gamePublic = go.GetComponent(); + gamePublic.isOn = Multiplayer.Settings.PublicGame; + gamePublic.interactable = true; + go.GetComponentInChildren().key = Locale.SERVER_HOST_PUBLIC_KEY; + GameObject.Destroy(go.GetComponentInChildren()); + go.ResetTooltip(); + + + go = GameObject.Instantiate(inputPrefab, NewContentGroup(controls, scroller.viewport.sizeDelta,106).transform, false); + go.name = "Details"; + go.transform.GetComponent().sizeDelta = new Vector2(go.transform.GetComponent().sizeDelta.x, 106); + details = go.GetComponent(); + details.characterLimit = MAX_DETAILS_LEN; + details.lineType = TMP_InputField.LineType.MultiLineSubmit; + details.FindChildByName("text [noloc]").GetComponent().alignment = TextAlignmentOptions.TopLeft; + + details.placeholder.GetComponent().text = Locale.SERVER_HOST_DETAILS; + + + go = GameObject.Instantiate(dividerPrefab, NewContentGroup(controls, scroller.viewport.sizeDelta).transform, false); + go.name = "Divider"; + + + go = GameObject.Instantiate(sliderPrefab, NewContentGroup(controls, scroller.viewport.sizeDelta).transform, false); + go.name = "Max Players"; + go.FindChildByName("[text label]").GetComponent().key = Locale.SERVER_HOST_MAX_PLAYERS_KEY; + go.ResetTooltip(); + go.FindChildByName("[text label]").GetComponent().UpdateLocalization(); + maxPlayers = go.GetComponent(); + maxPlayers.minValue = MIN_PLAYERS; + maxPlayers.maxValue = MAX_PLAYERS; + maxPlayers.value = Mathf.Clamp(Multiplayer.Settings.MaxPlayers,MIN_PLAYERS,MAX_PLAYERS); + maxPlayers.interactable = true; + + + go = GameObject.Instantiate(inputPrefab, NewContentGroup(controls, scroller.viewport.sizeDelta).transform, false); + go.name = "Port"; + port = go.GetComponent(); + port.characterValidation = TMP_InputField.CharacterValidation.Integer; + port.characterLimit = MAX_PORT_LEN; + port.placeholder.GetComponent().text = (Multiplayer.Settings.Port >= MIN_PORT && Multiplayer.Settings.Port <= MAX_PORT) ? Multiplayer.Settings.Port.ToString() : DEFAULT_PORT.ToString(); + + + go = this.gameObject.UpdateButton("ButtonTextIcon Save", "ButtonTextIcon Start", Locale.SERVER_HOST_START_KEY, null, playSprite); + go.FindChildByName("[text]").GetComponent().UpdateLocalization(); + + startButton = go.GetComponent(); + startButton.onClick.RemoveAllListeners(); + startButton.onClick.AddListener(StartClick); + + + } + + private GameObject NewContentGroup(GameObject parent, Vector2 sizeDelta, int cellMaxHeight = 53) + { + // Create a content group + GameObject contentGroup = new GameObject("ContentGroup"); + contentGroup.SetLayersRecursive(Layers.UI); + RectTransform groupRect = contentGroup.AddComponent(); + contentGroup.transform.SetParent(parent.transform, false); + groupRect.sizeDelta = sizeDelta; + ContentSizeFitter sizeFitter = contentGroup.AddComponent(); + sizeFitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize; + + // Add VerticalLayoutGroup and ContentSizeFitter + GridLayoutGroup glayoutGroup = contentGroup.AddComponent(); + glayoutGroup.startCorner = GridLayoutGroup.Corner.LowerLeft; + glayoutGroup.startAxis = GridLayoutGroup.Axis.Vertical; + glayoutGroup.cellSize = new Vector2(617.5f, cellMaxHeight); + glayoutGroup.spacing = new Vector2(0, 0); + glayoutGroup.constraint = GridLayoutGroup.Constraint.FixedColumnCount; + glayoutGroup.constraintCount = 1; + glayoutGroup.padding = new RectOffset(10, 0, 0, 10); + + return contentGroup; } - - private void SetupListeners(bool on) + + +private void SetupListeners(bool on) { if (on) { - //this.gridView.SelectedIndexChanged += this.IndexChanged; + serverName.onValueChanged.RemoveAllListeners(); + serverName.onValueChanged.AddListener(new UnityAction(ValidateInputs)); + + port.onValueChanged.RemoveAllListeners(); + port.onValueChanged.AddListener(new UnityAction(ValidateInputs)); } else { - //this.gridView.SelectedIndexChanged -= this.IndexChanged; + this.serverName.onValueChanged.RemoveAllListeners(); } } @@ -70,8 +304,86 @@ private void SetupListeners(bool on) #endregion #region UI callbacks + private void ValidateInputs(string text) + { + bool valid = true; + int portNum=0; + + if (serverName.text.Trim() == "" || serverName.text.Length >= MAX_SERVER_NAME_LEN) + valid = false; + + if (port.text != "") + { + portNum = int.Parse(port.text); + if(portNum < MIN_PORT || portNum > MAX_PORT) + return; + + } + + if( port.text == "" && (Multiplayer.Settings.Port < MIN_PORT || Multiplayer.Settings.Port > MAX_PORT)) + valid = false; + + startButton.interactable = valid; + + Debug.Log($"Validated: {valid}"); + } + + + private void StartClick() + { + + LobbyServerData serverData = new LobbyServerData(); + + serverData.port = (port.text == "") ? Multiplayer.Settings.Port : int.Parse(port.text); ; + serverData.Name = serverName.text.Trim(); + serverData.HasPassword = password.text != ""; + + serverData.GameMode = 0; //replaced with details from save / new game + serverData.Difficulty = 0; //replaced with details from save / new game + serverData.TimePassed = "N/A"; //replaced with details from save, or persisted if new game (will be updated in lobby server update cycle) + + serverData.CurrentPlayers = 0; + serverData.MaxPlayers = (int)maxPlayers.value; + + serverData.RequiredMods = ""; //FIX THIS - get the mods required + serverData.GameVersion = BuildInfo.BUILD_VERSION_MAJOR.ToString(); + serverData.MultiplayerVersion = Multiplayer.ModEntry.Version.ToString(); + + serverData.ServerDetails = details.text.Trim(); + + if (saveGame != null) + { + ISaveGameplayInfo saveGameplayInfo = this.userProvider.GetSaveGameplayInfo(this.saveGame); + if (!saveGameplayInfo.IsCorrupt) + { + serverData.TimePassed = (saveGameplayInfo.InGameDate != DateTime.MinValue) ? saveGameplayInfo.InGameTimePassed.ToString("d\\d\\ hh\\h\\ mm\\m\\ ss\\s") : "N/A"; + serverData.Difficulty = LobbyServerData.GetDifficultyFromString(this.userProvider.GetSessionDifficulty(saveGame.ParentSession).Name); + serverData.GameMode = LobbyServerData.GetGameModeFromString(saveGame.GameMode); + } + } + else if(startGameData != null) + { + serverData.Difficulty = LobbyServerData.GetDifficultyFromString(this.startGameData.difficulty.Name); + serverData.GameMode = LobbyServerData.GetGameModeFromString(startGameData.session.GameMode); + } + + + //Pass the server data to the NetworkLifecycle manager + NetworkLifecycle.Instance.serverData = serverData; + //Mark the game as public/private + NetworkLifecycle.Instance.isPublicGame = gamePublic.isOn; + //Mark it as a real multiplayer game + NetworkLifecycle.Instance.isSinglePlayer = false; + + + var ContinueGameRequested = lcInstance.GetType().GetMethod("OnRunClicked", BindingFlags.NonPublic | BindingFlags.Instance); + Debug.Log($"OnRunClicked exists: {ContinueGameRequested != null}"); + ContinueGameRequested?.Invoke(lcInstance, null); + } + + #endregion - + } diff --git a/Multiplayer/Components/MainMenu/ServerBrowser/IServerBrowserGameDetails.cs b/Multiplayer/Components/MainMenu/ServerBrowser/IServerBrowserGameDetails.cs index 20fc5e6..28d4d38 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowser/IServerBrowserGameDetails.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowser/IServerBrowserGameDetails.cs @@ -15,7 +15,7 @@ public interface IServerBrowserGameDetails : IDisposable { string id { get; set; } string ip { get; set; } - public ushort port { get; set; } + int port { get; set; } string Name { get; set; } bool HasPassword { get; set; } int GameMode { get; set; } @@ -26,7 +26,7 @@ public interface IServerBrowserGameDetails : IDisposable string RequiredMods { get; set; } string GameVersion { get; set; } string MultiplayerVersion { get; set; } - public string ServerDetails { get; set; } + string ServerDetails { get; set; } int Ping { get; set; } } diff --git a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs index 4a5eb7b..82555fc 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs @@ -48,7 +48,7 @@ public class ServerBrowserPane : MonoBehaviour //connection parameters private string ipAddress; - private ushort portNumber; + private int portNumber; string password = null; bool direct = false; @@ -261,7 +261,10 @@ private void ShowIpPopup() popup.Closed += result => { if (result.closedBy == PopupClosedByAction.Abortion) + { + buttonDirectIP.interactable = true; return; + } if (!IPv4Regex.IsMatch(result.data) && !IPv6Regex.IsMatch(result.data)) { @@ -291,7 +294,10 @@ private void ShowPortPopup() popup.Closed += result => { if (result.closedBy == PopupClosedByAction.Abortion) + { + buttonDirectIP.interactable = true; return; + } if (!PortRegex.IsMatch(result.data)) { @@ -331,7 +337,10 @@ private void ShowPasswordPopup() popup.Closed += result => { if (result.closedBy == PopupClosedByAction.Abortion) + { + buttonDirectIP.interactable = true; return; + } if (direct) { @@ -386,13 +395,13 @@ IEnumerator GetRequest(string uri) { Debug.Log(pages[page] + ":\nReceived: " + webRequest.downloadHandler.text); - ServerData[] response; + LobbyServerData[] response; - response = Newtonsoft.Json.JsonConvert.DeserializeObject(webRequest.downloadHandler.text); + response = Newtonsoft.Json.JsonConvert.DeserializeObject(webRequest.downloadHandler.text); Debug.Log($"servers: {response.Length}"); - foreach (ServerData server in response) + foreach (LobbyServerData server in response) { Debug.Log($"Name: {server.Name}\tIP: {server.ip}"); } @@ -451,7 +460,7 @@ private void FillDummyServers() for (int i = 0; i < UnityEngine.Random.Range(1, 50); i++) { - item = new ServerData(); + item = new LobbyServerData(); item.Name = testNames[UnityEngine.Random.Range(0, testNames.Length - 1)]; item.MaxPlayers = UnityEngine.Random.Range(1, 10); item.CurrentPlayers = UnityEngine.Random.Range(1, item.MaxPlayers); diff --git a/Multiplayer/Components/Networking/NetworkLifecycle.cs b/Multiplayer/Components/Networking/NetworkLifecycle.cs index 7c14288..734e407 100644 --- a/Multiplayer/Components/Networking/NetworkLifecycle.cs +++ b/Multiplayer/Components/Networking/NetworkLifecycle.cs @@ -6,8 +6,10 @@ using DV.Utils; using LiteNetLib; using LiteNetLib.Utils; +using Multiplayer.Networking.Data; using Multiplayer.Networking.Listeners; using Multiplayer.Utils; +using Newtonsoft.Json; using UnityEngine; using UnityEngine.SceneManagement; @@ -19,6 +21,11 @@ public class NetworkLifecycle : SingletonBehaviour public const byte TICK_RATE = 24; private const float TICK_INTERVAL = 1.0f / TICK_RATE; + public LobbyServerData serverData; + public bool isPublicGame { get; set; } = false; + public bool isSinglePlayer { get; set; } = true; + + public NetworkServer Server { get; private set; } public NetworkClient Client { get; private set; } @@ -35,6 +42,8 @@ public class NetworkLifecycle : SingletonBehaviour private readonly ExecutionTimer tickTimer = new(); private readonly ExecutionTimer tickWatchdog = new(0.25f); + float timeElapsed = 0f; //time since last lobby server update + /// /// Whether the provided NetPeer is the host. /// Note that this does NOT check authority, and should only be used for client-only logic. @@ -111,12 +120,29 @@ public void QueueMainMenuEvent(Action action) mainMenuLoadedQueue.Enqueue(action); } - public bool StartServer(int port, IDifficulty difficulty) + public bool StartServer(IDifficulty difficulty) { + int port = Multiplayer.Settings.Port; + if (Server != null) throw new InvalidOperationException("NetworkManager already exists!"); + + if (!isSinglePlayer) + { + if(serverData != null) + { + port = serverData.port; + } + } + Multiplayer.Log($"Starting server on port {port}"); - NetworkServer server = new(difficulty, Multiplayer.Settings); + NetworkServer server = new(difficulty, Multiplayer.Settings, isPublicGame, isSinglePlayer, serverData); + + //reset for next game + isPublicGame = false; + isSinglePlayer = true; + serverData = null; + if (!server.Start(port)) return false; Server = server; @@ -206,4 +232,5 @@ public static void CreateLifecycle() gameObject.AddComponent(); DontDestroyOnLoad(gameObject); } + } diff --git a/Multiplayer/Locale.cs b/Multiplayer/Locale.cs index 274c5d6..bc30ea9 100644 --- a/Multiplayer/Locale.cs +++ b/Multiplayer/Locale.cs @@ -16,6 +16,7 @@ public static class Locale private const string PREFIX_MAIN_MENU = $"{PREFIX}mm"; private const string PREFIX_SERVER_BROWSER = $"{PREFIX}sb"; + private const string PREFIX_SERVER_HOST = $"{PREFIX}host"; private const string PREFIX_DISCONN_REASON = $"{PREFIX}dr"; private const string PREFIX_CAREER_MANAGER = $"{PREFIX}carman"; private const string PREFIX_PLAYER_LIST = $"{PREFIX}plist"; @@ -32,10 +33,9 @@ public static class Locale public static string SERVER_BROWSER__MANUAL_CONNECT => Get(SERVER_BROWSER__MANUAL_CONNECT_KEY); public const string SERVER_BROWSER__MANUAL_CONNECT_KEY = $"{PREFIX_SERVER_BROWSER}/manual_connect"; - - public static string SERVER_BROWSER__HOST => Get(SERVER_BROWSER__HOST_KEY); - public const string SERVER_BROWSER__HOST_KEY = $"{PREFIX_SERVER_BROWSER}/host"; + public static string SERVER_BROWSER__HOST => Get(SERVER_BROWSER__HOST_KEY); + public const string SERVER_BROWSER__HOST_KEY = $"{PREFIX_SERVER_BROWSER}/host"; public static string SERVER_BROWSER__REFRESH => Get(SERVER_BROWSER__REFRESH_KEY); public const string SERVER_BROWSER__REFRESH_KEY = $"{PREFIX_SERVER_BROWSER}/refresh"; @@ -58,6 +58,26 @@ public static class Locale private const string SERVER_BROWSER__PASSWORD_KEY = $"{PREFIX_SERVER_BROWSER}/password"; #endregion + #region Server Host + public static string SERVER_HOST__TITLE => Get(SERVER_HOST__TITLE_KEY); + public const string SERVER_HOST__TITLE_KEY = $"{PREFIX_SERVER_HOST}/title"; + + public static string SERVER_HOST_PASSWORD => Get(SERVER_HOST_PASSWORD_KEY); + public const string SERVER_HOST_PASSWORD_KEY = $"{PREFIX_SERVER_HOST}/password"; + public static string SERVER_HOST_NAME => Get(SERVER_HOST_NAME_KEY); + public const string SERVER_HOST_NAME_KEY = $"{PREFIX_SERVER_HOST}/name"; + public static string SERVER_HOST_PUBLIC => Get(SERVER_HOST_PUBLIC_KEY); + public const string SERVER_HOST_PUBLIC_KEY = $"{PREFIX_SERVER_HOST}/public"; + public static string SERVER_HOST_DETAILS => Get(SERVER_HOST_DETAILS_KEY); + public const string SERVER_HOST_DETAILS_KEY = $"{PREFIX_SERVER_HOST}/details"; + public static string SERVER_HOST_MAX_PLAYERS => Get(SERVER_HOST_MAX_PLAYERS_KEY); + public const string SERVER_HOST_MAX_PLAYERS_KEY = $"{PREFIX_SERVER_HOST}/max_players"; + public static string SERVER_HOST_START => Get(SERVER_HOST_START_KEY); + public const string SERVER_HOST_START_KEY = $"{PREFIX_SERVER_HOST}/start"; + + + + #endregion #region Disconnect Reason public static string DISCONN_REASON__INVALID_PASSWORD => Get(DISCONN_REASON__INVALID_PASSWORD_KEY); public const string DISCONN_REASON__INVALID_PASSWORD_KEY = $"{PREFIX_DISCONN_REASON}/invalid_password"; diff --git a/Multiplayer/Networking/Data/LobbyServerData.cs b/Multiplayer/Networking/Data/LobbyServerData.cs new file mode 100644 index 0000000..ffed4f0 --- /dev/null +++ b/Multiplayer/Networking/Data/LobbyServerData.cs @@ -0,0 +1,150 @@ +using Multiplayer.Components.MainMenu; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Multiplayer.Networking.Data +{ + public class LobbyServerData : IServerBrowserGameDetails + { + + public string id { get; set; } + + public string ip { get; set; } + public int port { get; set; } + + [JsonProperty("server_name")] + public string Name { get; set; } + + + [JsonProperty("password_protected")] + public bool HasPassword { get; set; } + + + [JsonProperty("game_mode")] + public int GameMode { get; set; } + + + [JsonProperty("difficulty")] + public int Difficulty { get; set; } + + + [JsonProperty("time_passed")] + public string TimePassed { get; set; } + + + [JsonProperty("current_players")] + public int CurrentPlayers { get; set; } + + + [JsonProperty("max_players")] + public int MaxPlayers { get; set; } + + + [JsonProperty("required_mods")] + public string RequiredMods { get; set; } + + + [JsonProperty("game_version")] + public string GameVersion { get; set; } + + + [JsonProperty("multiplayer_version")] + public string MultiplayerVersion { get; set; } + + + [JsonProperty("server_info")] + public string ServerDetails { get; set; } + + [JsonIgnore] + public int Ping { get; set; } + + + public void Dispose() { } + public static int GetDifficultyFromString(string difficulty) + { + int diff = 0; + + switch (difficulty) + { + case "Standard": + diff = 0; + break; + case "Comfort": + diff = 1; + break; + case "Realistic": + diff = 2; + break; + default: + diff = 3; + break; + } + return diff; + } + + public static string GetDifficultyFromInt(int difficulty) + { + string diff = "Standard"; + + switch (difficulty) + { + case 0: + diff = "Standard"; + break; + case 1: + diff = "Comfort"; + break; + case 2: + diff = "Realistic"; + break; + default: + diff = "Custom"; + break; + } + return diff; + } + + public static int GetGameModeFromString(string difficulty) + { + int diff = 0; + + switch (difficulty) + { + case "Career": + diff = 0; + break; + case "Sandbox": + diff = 1; + break; + case "Scenario": + diff = 2; + break; + } + return diff; + } + + public static string GetGameModeFromInt(int difficulty) + { + string diff = "Career"; + + switch (difficulty) + { + case 0: + diff = "Career"; + break; + case 1: + diff = "Sandbox"; + break; + case 2: + diff = "Scenario"; + break; + } + return diff; + } + + } +} diff --git a/Multiplayer/Networking/Data/LobbyServerResponseData.cs b/Multiplayer/Networking/Data/LobbyServerResponseData.cs new file mode 100644 index 0000000..70d093b --- /dev/null +++ b/Multiplayer/Networking/Data/LobbyServerResponseData.cs @@ -0,0 +1,23 @@ +using Multiplayer.Components.MainMenu; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Multiplayer.Networking.Data +{ + public class LobbyServerResponseData + { + + public string game_server_id { get; set; } + public string private_key { get; set; } + + public LobbyServerResponseData(string game_server_id, string private_key) + { + this.game_server_id = game_server_id; + this.private_key = private_key; + } + } +} diff --git a/Multiplayer/Networking/Data/LobbyServerUpdateData.cs b/Multiplayer/Networking/Data/LobbyServerUpdateData.cs new file mode 100644 index 0000000..f592f9a --- /dev/null +++ b/Multiplayer/Networking/Data/LobbyServerUpdateData.cs @@ -0,0 +1,36 @@ +using Multiplayer.Components.MainMenu; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Multiplayer.Networking.Data +{ + public class LobbyServerUpdateData + { + public string game_server_id { get; set; } + + public string private_key { get; set; } + + [JsonProperty("time_passed")] + public string TimePassed { get; set; } + + + [JsonProperty("current_players")] + public int CurrentPlayers { get; set; } + + + public LobbyServerUpdateData(string game_server_id, string private_key, string timePassed,int currentPlayers) + { + this.game_server_id = game_server_id; + this.private_key = private_key; + this.TimePassed = timePassed; + this.CurrentPlayers = currentPlayers; + } + + + + } +} diff --git a/Multiplayer/Networking/Data/ServerData.cs b/Multiplayer/Networking/Data/ServerData.cs deleted file mode 100644 index c0b3a47..0000000 --- a/Multiplayer/Networking/Data/ServerData.cs +++ /dev/null @@ -1,67 +0,0 @@ -using Multiplayer.Components.MainMenu; -using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Multiplayer.Networking.Data -{ - public class ServerData : IServerBrowserGameDetails - { - - public string id { get; set; } //not yet used - public string ip { get; set; } - public ushort port { get; set; } - - - [JsonProperty("server_name")] - public string Name { get; set; } - - - [JsonProperty("password_protected")] - public bool HasPassword { get; set; } - - - [JsonProperty("game_mode")] - public int GameMode { get; set; } - - - [JsonProperty("difficulty")] - public int Difficulty { get; set; } - - - [JsonProperty("time_passed")] - public string TimePassed { get; set; } - - - [JsonProperty("current_players")] - public int CurrentPlayers { get; set; } - - - [JsonProperty("max_players")] - public int MaxPlayers { get; set; } - - - [JsonProperty("required_mods")] - public string RequiredMods { get; set; } - - - [JsonProperty("game_version")] - public string GameVersion { get; set; } - - - [JsonProperty("multiplayer_version")] - public string MultiplayerVersion { get; set; } - - - [JsonProperty("server_info")] - public string ServerDetails { get; set; } - - public int Ping { get; set; } - - - public void Dispose() { } - } -} diff --git a/Multiplayer/Networking/Managers/NetworkManager.cs b/Multiplayer/Networking/Managers/NetworkManager.cs index 93b5cd8..7d4d4dc 100644 --- a/Multiplayer/Networking/Managers/NetworkManager.cs +++ b/Multiplayer/Networking/Managers/NetworkManager.cs @@ -64,7 +64,7 @@ public void PollEvents() netManager.PollEvents(); } - public void Stop() + public virtual void Stop() { netManager.Stop(true); Settings.OnSettingsUpdated -= OnSettingsUpdated; diff --git a/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs b/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs new file mode 100644 index 0000000..17f674a --- /dev/null +++ b/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs @@ -0,0 +1,176 @@ +using System; +using Multiplayer.Networking.Data; +using Multiplayer.Networking.Listeners; +using Newtonsoft.Json; +using System.Collections; +using UnityEngine; +using UnityEngine.Networking; +using Multiplayer.Components.Networking; +using DV.WeatherSystem; +using DV.UserManagement; + +namespace Multiplayer.Networking.Managers.Server; +public class LobbyServerManager : MonoBehaviour +{ + private const int UPDATE_TIME_BUFFER = 10; + private const int UPDATE_TIME = 120 - UPDATE_TIME_BUFFER; //how often to update the lobby server + private const int PLAYER_CHANGE_TIME = 5; //update server early if the number of players has changed in this time frame + + private NetworkServer server; + public string server_id { get; set; } + public string private_key { get; set; } + + private bool sendUpdates = false; + + + private float timePassed = 0f; + + private void Awake() + { + this.server = NetworkLifecycle.Instance.Server; + + Debug.Log($"LobbyServerManager New({server != null})"); + Debug.Log($"StartingCoroutine {Multiplayer.Settings.LobbyServerAddress}/add_game_server\")"); + StartCoroutine(this.RegisterWithLobbyServer($"{Multiplayer.Settings.LobbyServerAddress}/add_game_server")); + } + + private void OnDestroy() + { + Debug.Log($"LobbyServerManager OnDestroy()"); + sendUpdates = false; + this.StopAllCoroutines(); + StartCoroutine(this.RemoveFromLobbyServer($"{Multiplayer.Settings.LobbyServerAddress}/remove_game_server")); + } + + private void Update() + { + if (sendUpdates) + { + timePassed += Time.deltaTime; + + if(timePassed > UPDATE_TIME || (server.serverData.CurrentPlayers != server.PlayerCount && timePassed > PLAYER_CHANGE_TIME)){ + timePassed = 0f; + server.serverData.CurrentPlayers = server.PlayerCount; + StartCoroutine(this.UpdateLobbyServer($"{Multiplayer.Settings.LobbyServerAddress}/update_game_server")); + } + } + } + public void RemoveFromLobbyServer() + { + Debug.Log($"RemoveFromLobbyServer OnDestroy()"); + sendUpdates = false; + this.StopAllCoroutines(); + StartCoroutine(this.RemoveFromLobbyServer($"{Multiplayer.Settings.LobbyServerAddress}/remove_game_server")); + } + + + IEnumerator RegisterWithLobbyServer(string uri) + { + JsonSerializerSettings jsonSettings = new JsonSerializerSettings(); + jsonSettings.NullValueHandling = NullValueHandling.Ignore; + + string json = JsonConvert.SerializeObject(server.serverData, jsonSettings); + Debug.Log($"JsonRequest: {json}"); + + using (UnityWebRequest webRequest = UnityWebRequest.Post(uri, json)) + { + UploadHandler customUploadHandler = new UploadHandlerRaw(System.Text.Encoding.UTF8.GetBytes(json)); + customUploadHandler.contentType = "application/json"; + webRequest.uploadHandler = customUploadHandler; + + // Request and wait for the desired page. + yield return webRequest.SendWebRequest(); + + string[] pages = uri.Split('/'); + int page = pages.Length - 1; + + if (webRequest.isNetworkError || webRequest.isHttpError) + { + Debug.Log(pages[page] + ": Error: " + webRequest.error + "\r\n" + webRequest.downloadHandler.text); + } + else + { + Debug.Log(pages[page] + ":\nReceived: " + webRequest.downloadHandler.text); + + LobbyServerResponseData response; + + response = JsonConvert.DeserializeObject(webRequest.downloadHandler.text); + + if (response != null) + { + this.private_key = response.private_key; + this.server_id = response.game_server_id; + this.sendUpdates = true; + } + } + } + } + + IEnumerator RemoveFromLobbyServer(string uri) + { + JsonSerializerSettings jsonSettings = new JsonSerializerSettings(); + jsonSettings.NullValueHandling = NullValueHandling.Ignore; + + string json = JsonConvert.SerializeObject(new LobbyServerResponseData(this.server_id, this.private_key), jsonSettings); + Debug.Log($"JsonRequest: {json}"); + + using (UnityWebRequest webRequest = UnityWebRequest.Post(uri, json)) + { + UploadHandler customUploadHandler = new UploadHandlerRaw(System.Text.Encoding.UTF8.GetBytes(json)); + customUploadHandler.contentType = "application/json"; + webRequest.uploadHandler = customUploadHandler; + + // Request and wait for the desired page. + yield return webRequest.SendWebRequest(); + + string[] pages = uri.Split('/'); + int page = pages.Length - 1; + + if (webRequest.isNetworkError || webRequest.isHttpError) + { + Debug.Log(pages[page] + ": Error: " + webRequest.error + "\r\n" + webRequest.downloadHandler.text); + } + else + { + Debug.Log(pages[page] + ":\nReceived: " + webRequest.downloadHandler.text); + } + } + } + + IEnumerator UpdateLobbyServer(string uri) + { + JsonSerializerSettings jsonSettings = new JsonSerializerSettings(); + jsonSettings.NullValueHandling = NullValueHandling.Ignore; + + DateTime start = AStartGameData.BaseTimeAndDate; + DateTime current = WeatherDriver.Instance.manager.DateTime; + + TimeSpan inGame = current - start; + + + string json = JsonConvert.SerializeObject(new LobbyServerUpdateData(this.server_id, this.private_key, inGame.ToString("d\\d\\ hh\\h\\ mm\\m\\ ss\\s"), server.serverData.CurrentPlayers), jsonSettings); + Debug.Log($"UpdateLobbyServer JsonRequest: {json}"); + + using (UnityWebRequest webRequest = UnityWebRequest.Post(uri, json)) + { + UploadHandler customUploadHandler = new UploadHandlerRaw(System.Text.Encoding.UTF8.GetBytes(json)); + customUploadHandler.contentType = "application/json"; + webRequest.uploadHandler = customUploadHandler; + + // Request and wait for the desired page. + yield return webRequest.SendWebRequest(); + + string[] pages = uri.Split('/'); + int page = pages.Length - 1; + + if (webRequest.isNetworkError || webRequest.isHttpError) + { + Debug.Log(pages[page] + ": Error: " + webRequest.error + "\r\n" + webRequest.downloadHandler.text); + } + else + { + Debug.Log(pages[page] + ":\nReceived: " + webRequest.downloadHandler.text); + } + } + } +} diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index f5129b2..842bc17 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Net; using DV; using DV.InventorySystem; using DV.Logic.Job; @@ -15,6 +16,7 @@ using Multiplayer.Components.Networking.Train; using Multiplayer.Components.Networking.World; using Multiplayer.Networking.Data; +using Multiplayer.Networking.Managers.Server; using Multiplayer.Networking.Packets.Clientbound; using Multiplayer.Networking.Packets.Clientbound.SaveGame; using Multiplayer.Networking.Packets.Clientbound.Train; @@ -36,6 +38,11 @@ public class NetworkServer : NetworkManager private readonly Dictionary serverPlayers = new(); private readonly Dictionary netPeers = new(); + private LobbyServerManager lobbyServerManager; + public bool isPublic; + public bool isSinglePlayer; + public LobbyServerData serverData; + public IReadOnlyCollection ServerPlayers => serverPlayers.Values; public int PlayerCount => netManager.ConnectedPeersCount; @@ -46,8 +53,12 @@ public class NetworkServer : NetworkManager public readonly IDifficulty Difficulty; private bool IsLoaded; - public NetworkServer(IDifficulty difficulty, Settings settings) : base(settings) + public NetworkServer(IDifficulty difficulty, Settings settings, bool isPublic, bool isSinglePlayer, LobbyServerData serverData) : base(settings) { + this.isPublic = isPublic; + this.isSinglePlayer = isSinglePlayer; + this.serverData = serverData; + Difficulty = difficulty; serverMods = ModInfo.FromModEntries(UnityModManager.modEntries); } @@ -58,6 +69,16 @@ public bool Start(int port) return netManager.Start(port); } + public override void Stop() + { + if (lobbyServerManager != null) + { + lobbyServerManager.RemoveFromLobbyServer(); + } + + base.Stop(); + } + protected override void Subscribe() { netPacketProcessor.SubscribeReusable(OnServerboundClientLoginPacket); @@ -87,6 +108,12 @@ protected override void Subscribe() private void OnLoaded() { + Debug.Log($"Server loaded, isSinglePlayer: {isSinglePlayer} isPublic: {isPublic}"); + if (!isSinglePlayer && isPublic) + { + lobbyServerManager = NetworkLifecycle.Instance.GetOrAddComponent(); + } + Log($"Server loaded, processing {joinQueue.Count} queued players"); IsLoaded = true; while (joinQueue.Count > 0) @@ -310,7 +337,7 @@ private void OnServerboundClientLoginPacket(ServerboundClientLoginPacket packet, return; } - if (netManager.ConnectedPeersCount >= Multiplayer.Settings.MaxPlayers) + if (netManager.ConnectedPeersCount >= Multiplayer.Settings.MaxPlayers || isSinglePlayer && netManager.ConnectedPeersCount >= 1) { LogWarning("Denied login due to server being full!"); ClientboundServerDenyPacket denyPacket = new() { diff --git a/Multiplayer/Patches/MainMenu/LauncherControllerPatch.cs b/Multiplayer/Patches/MainMenu/LauncherControllerPatch.cs index 2f64990..4954212 100644 --- a/Multiplayer/Patches/MainMenu/LauncherControllerPatch.cs +++ b/Multiplayer/Patches/MainMenu/LauncherControllerPatch.cs @@ -1,5 +1,7 @@ -using DV.Localization; +using System; +using DV.Common; using DV.UI; +using DV.UI.PresetEditors; using DV.UIFramework; using HarmonyLib; using Multiplayer.Components.MainMenu; @@ -10,14 +12,19 @@ namespace Multiplayer.Patches.MainMenu; -[HarmonyPatch(typeof(LauncherController), "OnEnable")] +[HarmonyPatch(typeof(LauncherController))] public static class LauncherController_Patch { private const int PADDING = 10; private static GameObject goHost; + private static LauncherController lcInstance; + + - private static void Postfix(LauncherController __instance) + [HarmonyPostfix] + [HarmonyPatch(typeof(LauncherController), "OnEnable")] + private static void OnEnable(LauncherController __instance) { Multiplayer.Log("LauncherController_Patch()"); @@ -48,9 +55,6 @@ private static void Postfix(LauncherController __instance) // Set up event listeners Button btnHost = goHost.GetComponent(); - //UIMenuRequester uim = btnHost.GetOrAddComponent(); - //uim.targetMenuController = RightPaneController_OnEnable_Patch.uIMenuController; - //uim.requestedMenuIndex = RightPaneController_OnEnable_Patch.hostMenuIndex; btnHost.onClick.AddListener(HostAction); @@ -59,12 +63,40 @@ private static void Postfix(LauncherController __instance) Multiplayer.Log("LauncherController_Patch() complete"); } } + + [HarmonyPostfix] + [HarmonyPatch(typeof(LauncherController), "SetData", new Type[] { typeof(ISaveGame), typeof(AUserProfileProvider) , typeof(AScenarioProvider) , typeof(LauncherController.UpdateRequest) })] + private static void SetData(LauncherController __instance, ISaveGame saveGame, AUserProfileProvider userProvider, AScenarioProvider scenarioProvider, LauncherController.UpdateRequest updateCallback) + { + if (RightPaneController_OnEnable_Patch.hgpInstance == null) + return; + + RightPaneController_OnEnable_Patch.hgpInstance.saveGame = saveGame; + RightPaneController_OnEnable_Patch.hgpInstance.userProvider = userProvider; + RightPaneController_OnEnable_Patch.hgpInstance.scenarioProvider = scenarioProvider; + + + } + + [HarmonyPostfix] + [HarmonyPatch(typeof(LauncherController), "SetData", new Type[] { typeof(UIStartGameData), typeof(AUserProfileProvider), typeof(AScenarioProvider), typeof(LauncherController.UpdateRequest) })] + private static void SetData(LauncherController __instance, UIStartGameData startGameData, AUserProfileProvider userProvider, AScenarioProvider scenarioProvider, LauncherController.UpdateRequest updateCallback) + { + if (RightPaneController_OnEnable_Patch.hgpInstance == null) + return; + + RightPaneController_OnEnable_Patch.hgpInstance.startGameData = startGameData; + RightPaneController_OnEnable_Patch.hgpInstance.userProvider = userProvider; + RightPaneController_OnEnable_Patch.hgpInstance.scenarioProvider = scenarioProvider; + + } private static void HostAction() { // Implement host action logic here Debug.Log("Host button clicked."); - // Add your code to handle hosting a game + + RightPaneController_OnEnable_Patch.uIMenuController.SwitchMenu(RightPaneController_OnEnable_Patch.hostMenuIndex); diff --git a/Multiplayer/Patches/MainMenu/MainMenuControllerPatch.cs b/Multiplayer/Patches/MainMenu/MainMenuControllerPatch.cs index 992959b..3ece983 100644 --- a/Multiplayer/Patches/MainMenu/MainMenuControllerPatch.cs +++ b/Multiplayer/Patches/MainMenu/MainMenuControllerPatch.cs @@ -44,7 +44,7 @@ private static void Prefix(MainMenuController __instance) // Remove existing localization components to reset them Object.Destroy(multiplayerButton.GetComponentInChildren()); - ResetTooltip(multiplayerButton); + multiplayerButton.ResetTooltip(); // Set the icon for the new Multiplayer button SetButtonIcon(multiplayerButton); @@ -54,12 +54,12 @@ private static void Prefix(MainMenuController __instance) /// Resets the tooltip for a given button. /// /// The button to reset the tooltip for. - private static void ResetTooltip(GameObject button) - { - UIElementTooltip tooltip = button.GetComponent(); - tooltip.disabledKey = null; - tooltip.enabledKey = null; - } + //private static void ResetTooltip(GameObject button) + //{ + // UIElementTooltip tooltip = button.GetComponent(); + // tooltip.disabledKey = null; + // tooltip.enabledKey = null; + //} /// /// Sets the icon for the Multiplayer button. diff --git a/Multiplayer/Patches/MainMenu/RightPaneControllerPatch.cs b/Multiplayer/Patches/MainMenu/RightPaneControllerPatch.cs index 36b361e..4bff4d3 100644 --- a/Multiplayer/Patches/MainMenu/RightPaneControllerPatch.cs +++ b/Multiplayer/Patches/MainMenu/RightPaneControllerPatch.cs @@ -4,6 +4,7 @@ using HarmonyLib; using Multiplayer.Components.MainMenu; using Multiplayer.Utils; +using System.Reflection; using TMPro; using UnityEngine; @@ -16,6 +17,7 @@ public static class RightPaneController_OnEnable_Patch { public static int hostMenuIndex; public static UIMenuController uIMenuController; + public static HostGamePane hgpInstance; private static void Prefix(RightPaneController __instance) { uIMenuController = __instance.menuController; @@ -59,13 +61,14 @@ private static void Prefix(RightPaneController __instance) GameObject serverWindow = multiplayerPane.FindChildByName("Save Description"); serverWindow.GetComponentInChildren().textWrappingMode = TextWrappingModes.Normal; - serverWindow.GetComponentInChildren().text = "Server browser not yet implemented.

Dummy servers are shown for demonstration purposes only.

Press refresh to load real servers."; + serverWindow.GetComponentInChildren().text = "Server browser not fully implemented.

Dummy servers are shown for demonstration purposes only.

Press refresh to attempt loading real servers."; // Update buttons on the multiplayer pane multiplayerPane.UpdateButton("ButtonTextIcon Overwrite", "ButtonTextIcon Manual", Locale.SERVER_BROWSER__MANUAL_CONNECT_KEY, null, Multiplayer.AssetIndex.multiplayerIcon); //multiplayerPane.UpdateButton("ButtonTextIcon Load", "ButtonTextIcon Host", Locale.SERVER_BROWSER__HOST_KEY, null, Multiplayer.AssetIndex.lockIcon); multiplayerPane.UpdateButton("ButtonTextIcon Save", "ButtonTextIcon Join", Locale.SERVER_BROWSER__JOIN_KEY, null, Multiplayer.AssetIndex.connectIcon); - multiplayerPane.UpdateButton("ButtonIcon Delete", "ButtonIcon Refresh", Locale.SERVER_BROWSER__REFRESH_KEY, null, Multiplayer.AssetIndex.refreshIcon); + GameObject go = multiplayerPane.UpdateButton("ButtonIcon Delete", "ButtonIcon Refresh", Locale.SERVER_BROWSER__REFRESH_KEY, null, Multiplayer.AssetIndex.refreshIcon); + // Add the MultiplayerPane component multiplayerPane.AddComponent(); @@ -110,7 +113,7 @@ private static void Prefix(RightPaneController __instance) GameObject.Destroy(hostPane.GetComponent()); GameObject.Destroy(hostPane.GetComponent()); - HostGamePane hp = hostPane.GetOrAddComponent(); + hgpInstance = hostPane.GetOrAddComponent(); // Add the host pane to the menu controller __instance.menuController.controlledMenus.Add(hostPane.GetComponent()); diff --git a/Multiplayer/Patches/World/SaveGameManagerPatch.cs b/Multiplayer/Patches/World/SaveGameManagerPatch.cs index 0c8067f..c014da7 100644 --- a/Multiplayer/Patches/World/SaveGameManagerPatch.cs +++ b/Multiplayer/Patches/World/SaveGameManagerPatch.cs @@ -19,7 +19,7 @@ private static void Postfix(AStartGameData __result) private static void StartServer(IDifficulty difficulty) { - if (NetworkLifecycle.Instance.StartServer(Multiplayer.Settings.Port, difficulty)) + if (NetworkLifecycle.Instance.StartServer(difficulty)) return; NetworkLifecycle.Instance.QueueMainMenuEvent(() => diff --git a/Multiplayer/Settings.cs b/Multiplayer/Settings.cs index add6071..903c5c5 100644 --- a/Multiplayer/Settings.cs +++ b/Multiplayer/Settings.cs @@ -21,8 +21,12 @@ public class Settings : UnityModManager.ModSettings, IDrawable [Space(10)] [Header("Server")] + [Draw("Server Name", Tooltip = "Name of your server in the lobby browser.")] + public string ServerName = ""; [Draw("Password", Tooltip = "The password required to join your server. Leave blank for no password.")] public string Password = ""; + [Draw("Public Game", Tooltip = "Public servers are listed in the lobby browser")] + public bool PublicGame = true; [Draw("Max Players", Tooltip = "The maximum number of players that can join your server, including yourself.")] public int MaxPlayers = 4; [Draw("Port", Tooltip = "The port that your server will listen on. You generally don't need to change this.")] @@ -36,7 +40,7 @@ public class Settings : UnityModManager.ModSettings, IDrawable [Draw("Last Remote IP", Tooltip = "The IP for the last server connected to by IP.")] public string LastRemoteIP = ""; [Draw("Last Remote Port", Tooltip = "The port for the last server connected to by IP.")] - public ushort LastRemotePort = 7777; + public int LastRemotePort = 7777; [Draw("Last Remote Password", Tooltip = "The password for the last server connected to by IP.")] public string LastRemotePassword = ""; diff --git a/Multiplayer/Utils/DvExtensions.cs b/Multiplayer/Utils/DvExtensions.cs index 080c30d..5241d93 100644 --- a/Multiplayer/Utils/DvExtensions.cs +++ b/Multiplayer/Utils/DvExtensions.cs @@ -6,6 +6,7 @@ using Multiplayer.Components.Networking.World; using UnityEngine; using UnityEngine.UI; +using System.Linq; @@ -45,7 +46,7 @@ public static NetworkedRailTrack Networked(this RailTrack railTrack) #endregion #region UI - public static void UpdateButton(this GameObject pane, string oldButtonName, string newButtonName, string localeKey, string toolTipKey, Sprite icon) + public static GameObject UpdateButton(this GameObject pane, string oldButtonName, string newButtonName, string localeKey, string toolTipKey, Sprite icon) { // Find and rename the button GameObject button = pane.FindChildByName(oldButtonName); @@ -55,8 +56,16 @@ public static void UpdateButton(this GameObject pane, string oldButtonName, stri if (button.GetComponentInChildren() != null) { button.GetComponentInChildren().key = localeKey; - GameObject.Destroy(button.GetComponentInChildren()); + foreach(var child in button.GetComponentsInChildren()) + { + GameObject.Destroy(child); + } ResetTooltip(button); + button.GetComponentInChildren().UpdateLocalization(); + }else if(button.GetComponentInChildren() != null) + { + button.GetComponentInChildren().enabledKey = localeKey + "__tooltip"; + button.GetComponentInChildren().disabledKey = localeKey + "__tooltip_disabled"; } // Set the button icon if provided @@ -67,6 +76,8 @@ public static void UpdateButton(this GameObject pane, string oldButtonName, stri // Enable button interaction button.GetComponentInChildren().ToggleInteractable(true); + + return button; } private static void SetButtonIcon(this GameObject button, Sprite icon) @@ -82,13 +93,15 @@ private static void SetButtonIcon(this GameObject button, Sprite icon) goIcon.GetComponent().sprite = icon; } - private static void ResetTooltip(this GameObject button) + public static void ResetTooltip(this GameObject button) { // Reset the tooltip keys for the button UIElementTooltip tooltip = button.GetComponent(); tooltip.disabledKey = null; tooltip.enabledKey = null; + } #endregion + } diff --git a/locale.csv b/locale.csv index 336dac6..4217e66 100644 --- a/locale.csv +++ b/locale.csv @@ -19,16 +19,32 @@ sb/host__tooltip,The tooltip shown when hovering over the 'Host Server' button., sb/host__tooltip_disabled,Unused,,,,,,,,,,,,,,,,,,,,,,,,,, sb/join_game,Join Game,Join Game,,,,,,,,,,,,,,,,,,,,,,,, sb/join_game__tooltip,The tooltip shown when hovering over the 'Join Server' button.,Join a multiplayer session.,,,,,,,,,,,,,,,,,,,,,,,, -sb/join_game__tooltip_disabled,Unused,,,,,,,,,,,,,,,,,,,,,,,,,, -sb/Refresh,refresh,Refresh,,,,,,,,,,,,,,,,,,,,,,,, -sb/Refresh__tooltip,The tooltip shown when hovering over the 'Refresh Server' button.,Refresh Server list.,,,,,,,,,,,,,,,,,,,,,,,, -sb/Refresh__tooltip_disabled,Unused,,,,,,,,,,,,,,,,,,,,,,,,,, +sb/join_game__tooltip_disabled,The tooltip shown when hovering over the 'Join Server' button.,Select a game to join.,,,,,,,,,,,,,,,,,,,,,,,, +sb/refresh,refresh,Refresh,,,,,,,,,,,,,,,,,,,,,,,, +sb/refresh__tooltip,The tooltip shown when hovering over the 'Refresh Server' button.,Refresh server list.,,,,,,,,,,,,,,,,,,,,,,,, +sb/refresh__tooltip_disabled,Unused,,,,,,,,,,,,,,,,,,,,,,,,,, sb/ip,IP popup,Enter IP Address,,,,,,,,Entrer l’adresse IP,IP Adresse eingeben,,,Inserire Indirizzo IP,,,,,,,,,,Ingrese la dirección IP,,, sb/ip_invalid,Invalid IP popup.,Invalid IP Address!,,,,,,,,Adresse IP invalide,Ungültige IP Adresse!,,,Indirizzo IP Invalido!,,,,,,,,,,¡Dirección IP inválida!,,, sb/port,Port popup.,Enter Port (7777 by default),,,,,,,,Entrer le port (7777 par défaut),Port eingeben (Standard: 7777),,,Inserire Porta (7777 di default),,,,,,,,,,Introduzca el número de puerto(7777 por defecto),,, sb/port_invalid,Invalid port popup.,Invalid Port!,,,,,,,,Port invalide !,Ungültiger Port!,,,Porta Invalida!,,,,,,,,,,¡Número de Puerto no válido!,,, sb/password,Password popup.,Enter Password,,,,,,,,Entrer le mot de passe,Passwort eingeben,,,Inserire Password,,,,,,,,,,Introducir la contraseña,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,, +,Server Host,,,,,,,,,,,,,,,,,,,,,,,,,, +host/title,The title of the Host Game page,Host Game,,,,,,,,,,,,,,,,,,,,,,,,, +host/name,Server name field placeholder,Server Name,,,,,,,,,,,,,,,,,,,,,,,,, +host/name__tooltip,Server name field tooltip,The name of the server that other players will see in the server browser,,,,,,,,,,,,,,,,,,,,,,,,, +host/password,Password field placeholder,Password (leave blank for no password),,,,,,,,,,,,,,,,,,,,,,,,, +host/password__tooltip,Password field placeholder,Password for joining the game. Leave blank if no password is required,,,,,,,,,,,,,,,,,,,,,,,,, +host/public,Public checkbox label,Public Game,,,,,,,,,,,,,,,,,,,,,,,,, +host/public__tooltip,Public checkbox tooltip,List this game in the server browser.,,,,,,,,,,,,,,,,,,,,,,,,, +host/details,Details field placeholder,Enter some details about your server,,,,,,,,,,,,,,,,,,,,,,,,, +host/details__tooltip,Details field tooltip,Details about your server visible in the server browser.,,,,,,,,,,,,,,,,,,,,,,,,, +host/max_players,Maximum players slider label,Maximum Players,,,,,,,,,,,,,,,,,,,,,,,,, +host/max_players__tooltip,Maximum players slider tooltip,Maximum players allowed to join the game.,,,,,,,,,,,,,,,,,,,,,,,,, +host/start,Maximum players slider label,Start,,,,,,,,,,,,,,,,,,,,,,,,, +host/start__tooltip,Maximum players slider tooltip,Start the server.,,,,,,,,,,,,,,,,,,,,,,,,, +host/start__tooltip_disabled,Maximum players slider tooltip,Check your settings are valid.,,,,,,,,,,,,,,,,,,,,,,,,, +,,,,,,,,,,,,,,,,,,,,,,,,,,, ,Disconnect Reason,,,,,,,,,,,,,,,,,,,,,,,,,, dr/invalid_password,Invalid password popup.,Invalid Password!,,,,,,,,Mot de passe incorrect !,Ungültiges Passwort!,,,Password non valida!,,,,,,,,,,¡Contraseña invalida!,,, dr/game_version,Different game versions.,"Game version mismatch! Server version: {0}, your version: {1}.",,,,,,,,"Version du jeu incompatible ! Version du serveur : {0}, version locale : {1}","Spielversion stimmt nicht überein! Server Version: {0}, Lokale Version: {1}.",,,"Versioni del gioco non combacianti! Versione del Server: {0}, La tua versione: {1}.",,,,,,,,,,"¡La versión del juego no coincide! Versión del servidor: {0}, tu versión: {1}.",,, From 0fa44a6890255b8e51856e7a90924be8f4adcecb Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 30 Jun 2024 21:45:06 +1000 Subject: [PATCH 17/34] Minor UI fixes and version update --- .../Components/MainMenu/HostGamePane.cs | 10 +++++- .../Components/MainMenu/ServerBrowserPane.cs | 32 +++++++++---------- .../MainMenu/LauncherControllerPatch.cs | 4 +-- Multiplayer/Settings.cs | 5 ++- info.json | 2 +- locale.csv | 20 ++---------- 6 files changed, 34 insertions(+), 39 deletions(-) diff --git a/Multiplayer/Components/MainMenu/HostGamePane.cs b/Multiplayer/Components/MainMenu/HostGamePane.cs index aa93d61..f50484c 100644 --- a/Multiplayer/Components/MainMenu/HostGamePane.cs +++ b/Multiplayer/Components/MainMenu/HostGamePane.cs @@ -323,7 +323,7 @@ private void ValidateInputs(string text) if( port.text == "" && (Multiplayer.Settings.Port < MIN_PORT || Multiplayer.Settings.Port > MAX_PORT)) valid = false; - startButton.interactable = valid; + startButton.ToggleInteractable(valid); Debug.Log($"Validated: {valid}"); } @@ -368,6 +368,14 @@ private void StartClick() } + Multiplayer.Settings.ServerName = serverData.Name; + Multiplayer.Settings.Password = password.text; + Multiplayer.Settings.PublicGame = gamePublic.isOn; + Multiplayer.Settings.Port = serverData.port; + Multiplayer.Settings.MaxPlayers = serverData.MaxPlayers; + Multiplayer.Settings.Details = serverData.ServerDetails; + + //Pass the server data to the NetworkLifecycle manager NetworkLifecycle.Instance.serverData = serverData; //Mark the game as public/private diff --git a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs index 82555fc..a8709e3 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs @@ -39,10 +39,10 @@ public class ServerBrowserPane : MonoBehaviour private IServerBrowserGameDetails selectedServer; //Button variables - private Button buttonJoin; + private ButtonDV buttonJoin; //private Button buttonHost; - private Button buttonRefresh; - private Button buttonDirectIP; + private ButtonDV buttonRefresh; + private ButtonDV buttonDirectIP; private bool serverRefreshing = false; @@ -76,8 +76,8 @@ private void OnEnable() this.SetupListeners(true); this.serverIDOnRefresh = ""; - buttonDirectIP.interactable = true; - buttonRefresh.interactable = true; + buttonDirectIP.ToggleInteractable(true); + buttonRefresh.ToggleInteractable(true); //buttonHost.interactable = true; } @@ -124,7 +124,7 @@ private void SetupMultiplayerButtons() goJoin.SetActive(true); goRefresh.SetActive(true); - buttonJoin.interactable = false; + buttonJoin.ToggleInteractable(false); } @@ -177,7 +177,7 @@ private void RefreshAction() return; serverRefreshing = true; - buttonJoin.interactable = false; + buttonJoin.ToggleInteractable(false); if (selectedServer != null) { @@ -191,8 +191,8 @@ private void JoinAction() { if (selectedServer != null) { - buttonDirectIP.interactable = false; - buttonJoin.interactable = false; + buttonDirectIP.ToggleInteractable(false); + buttonJoin.ToggleInteractable(false); //buttonHost.interactable = false; if (selectedServer.HasPassword) @@ -214,8 +214,8 @@ private void JoinAction() private void DirectAction() { Debug.Log($"DirectAction()"); - buttonDirectIP.interactable = false; - buttonJoin.interactable = false; + buttonDirectIP.ToggleInteractable(false); + buttonJoin.ToggleInteractable(false) ; //buttonHost.interactable = false; //making a direct connection @@ -235,11 +235,11 @@ private void IndexChanged(AGridView gridView) Debug.Log($"Selected server: {gridViewModel[gridView.SelectedModelIndex].Name}"); selectedServer = gridViewModel[gridView.SelectedModelIndex]; - buttonJoin.interactable = true; + buttonJoin.ToggleInteractable(true); } else { - buttonJoin.interactable = false; + buttonJoin.ToggleInteractable(false); } } @@ -262,7 +262,7 @@ private void ShowIpPopup() { if (result.closedBy == PopupClosedByAction.Abortion) { - buttonDirectIP.interactable = true; + buttonDirectIP.ToggleInteractable(true); return; } @@ -295,7 +295,7 @@ private void ShowPortPopup() { if (result.closedBy == PopupClosedByAction.Abortion) { - buttonDirectIP.interactable = true; + buttonDirectIP.ToggleInteractable(true); return; } @@ -338,7 +338,7 @@ private void ShowPasswordPopup() { if (result.closedBy == PopupClosedByAction.Abortion) { - buttonDirectIP.interactable = true; + buttonDirectIP.ToggleInteractable(true); return; } diff --git a/Multiplayer/Patches/MainMenu/LauncherControllerPatch.cs b/Multiplayer/Patches/MainMenu/LauncherControllerPatch.cs index 4954212..38122f5 100644 --- a/Multiplayer/Patches/MainMenu/LauncherControllerPatch.cs +++ b/Multiplayer/Patches/MainMenu/LauncherControllerPatch.cs @@ -50,8 +50,8 @@ private static void OnEnable(LauncherController __instance) btnHostRT.localPosition = new Vector3(curPos.x - curSize.x - PADDING, curPos.y,curPos.z); - __instance.transform.gameObject.UpdateButton("ButtonTextIcon Host", "ButtonTextIcon Host", Locale.SERVER_BROWSER__HOST_KEY, null, Multiplayer.AssetIndex.lockIcon); - + Sprite arrowSprite = GameObject.FindObjectOfType().continueButton.FindChildByName("icon").GetComponent().sprite; + __instance.transform.gameObject.UpdateButton("ButtonTextIcon Host", "ButtonTextIcon Host", Locale.SERVER_BROWSER__HOST_KEY, null, arrowSprite); // Set up event listeners Button btnHost = goHost.GetComponent(); diff --git a/Multiplayer/Settings.cs b/Multiplayer/Settings.cs index 903c5c5..053ab2e 100644 --- a/Multiplayer/Settings.cs +++ b/Multiplayer/Settings.cs @@ -31,11 +31,14 @@ public class Settings : UnityModManager.ModSettings, IDrawable public int MaxPlayers = 4; [Draw("Port", Tooltip = "The port that your server will listen on. You generally don't need to change this.")] public int Port = 7777; + [Draw("Details", Tooltip = "Details shown in the server browser")] + public string Details = ""; + [Space(10)] [Header("Lobby Server")] [Draw("Lobby Server address", Tooltip = "Address of lobby server for finding multiplayer games")] - public string LobbyServerAddress = "http://localhost:8080"; + public string LobbyServerAddress = "http://dv.mineit.space";//"http://localhost:8080"; [Header("Last Server Connected to by IP")] [Draw("Last Remote IP", Tooltip = "The IP for the last server connected to by IP.")] public string LastRemoteIP = ""; diff --git a/info.json b/info.json index b6f7b0e..03d0d3e 100644 --- a/info.json +++ b/info.json @@ -1,6 +1,6 @@ { "Id": "Multiplayer", - "Version": "0.1.0", + "Version": "0.1.5", "DisplayName": "Multiplayer", "Author": "Insprill", "EntryMethod": "Multiplayer.Multiplayer.Load", diff --git a/locale.csv b/locale.csv index 7b8eacd..8845a6f 100644 --- a/locale.csv +++ b/locale.csv @@ -20,8 +20,8 @@ sb/host__tooltip_disabled,Unused,,,,,,,,,,,,,,,,,,,,,,,,,, sb/join_game,Join Game,Join Game,Присъединете се към играта ,加入游戏 ,加入遊戲 ,Připojte se ke hře,Deltag i spil,Speel mee,Liity peliin,Rejoins une partie,Spiel beitreten,खेल में शामिल हो,Belépni a játékba,Unisciti al gioco,ゲームに参加します,게임 참여,Bli med i spillet,Dołącz do gry,Entrar no jogo ,Entrar no jogo ,Alatura-te jocului,Присоединиться к игре,Pridať sa do hry,Unete al juego,Gå med i spel,Oyuna katılmak,Приєднуйся до гри sb/join_game__tooltip,The tooltip shown when hovering over the 'Join Server' button.,Join a multiplayer session.,Присъединете се към мултиплейър сесия. ,加入多人游戏会话。 ,加入多人遊戲會話。 ,Připojte se k relaci pro více hráčů. ,Deltag i en multiplayer session.,Neem deel aan een multiplayersessie.,Liity moninpeliistuntoon.,Rejoignez une session multijoueur.,Nehmen Sie an einer Multiplayer-Sitzung teil.,मल्टीप्लेयर सत्र में शामिल हों.,Csatlakozz egy többjátékos munkamenethez.,Partecipa a una sessione multigiocatore.,マルチプレイヤー セッションに参加します。,멀티플레이어 세션에 참여하세요.,Bli med på en flerspillerøkt.,Dołącz do sesji wieloosobowej.,Participe de uma sessão multijogador.,Participe numa sessão multijogador. ,Alăturați-vă unei sesiuni multiplayer.,Присоединяйтесь к многопользовательской сессии.,Pripojte sa k relácii pre viacerých hráčov.,Únete a una sesión multijugador.,Gå med i en multiplayer-session.,Çok oyunculu bir oturuma katılın.,Приєднуйтеся до багатокористувацької сесії. sb/join_game__tooltip_disabled,The tooltip shown when hovering over the 'Join Server' button.,Select a game to join.,,,,,,,,,,,,,,,,,,,,,,,, -sb/Refresh,refresh,Refresh,Опресняване ,刷新 ,重新整理 ,Obnovit ,Opdater,Vernieuwen,virkistää,Rafraîchir,Aktualisierung,ताज़ा करना,Frissítés,ricaricare,リフレッシュ,새로 고치다,Forfriske,Odświeżać,Atualizar ,Atualizar ,Reîmprospăta,Обновить,Obnoviť,Actualizar,Uppdatera,Yenile,Оновити -sb/Refresh__tooltip,The tooltip shown when hovering over the 'Refresh Server' button.,Refresh Server list.,Обновяване на списъка със сървъри. ,刷新服务器列表。 ,刷新伺服器清單。 ,Obnovit seznam serverů. ,Opdater serverliste.,Vernieuw de serverlijst.,Päivitä palvelinluettelo.,Actualiser la liste des serveurs.,Serverliste aktualisieren.,सर्वर सूची ताज़ा करें.,Szerverlista frissítése.,Aggiorna l'elenco dei server.,サーバーリストを更新します。,서버 목록을 새로 고칩니다.,Oppdater serverlisten.,Odśwież listę serwerów.,Atualizar lista de servidores.,Atualizar lista de servidores. ,Actualizează lista de servere.,Обновить список серверов.,Obnoviť zoznam serverov.,Actualizar la lista de servidores.,Uppdatera serverlistan.,Sunucu listesini yenileyin.,Оновити список серверів. +sb/refresh,refresh,Refresh,Опресняване ,刷新 ,重新整理 ,Obnovit ,Opdater,Vernieuwen,virkistää,Rafraîchir,Aktualisierung,ताज़ा करना,Frissítés,ricaricare,リフレッシュ,새로 고치다,Forfriske,Odświeżać,Atualizar ,Atualizar ,Reîmprospăta,Обновить,Obnoviť,Actualizar,Uppdatera,Yenile,Оновити +sb/refresh__tooltip,The tooltip shown when hovering over the 'Refresh Server' button.,Refresh server list.,Обновяване на списъка със сървъри. ,刷新服务器列表。 ,刷新伺服器清單。 ,Obnovit seznam serverů. ,Opdater serverliste.,Vernieuw de serverlijst.,Päivitä palvelinluettelo.,Actualiser la liste des serveurs.,Serverliste aktualisieren.,सर्वर सूची ताज़ा करें.,Szerverlista frissítése.,Aggiorna l'elenco dei server.,サーバーリストを更新します。,서버 목록을 새로 고칩니다.,Oppdater serverlisten.,Odśwież listę serwerów.,Atualizar lista de servidores.,Atualizar lista de servidores. ,Actualizează lista de servere.,Обновить список серверов.,Obnoviť zoznam serverov.,Actualizar la lista de servidores.,Uppdatera serverlistan.,Sunucu listesini yenileyin.,Оновити список серверів. sb/refresh__tooltip_disabled,Unused,,,,,,,,,,,,,,,,,,,,,,,,,, sb/ip,IP popup,Enter IP Address,Въведете IP адрес,输入IP地址 ,輸入IP位址 ,Zadejte IP adresu ,Indtast IP-adresse,Voer het IP-adres in,Anna IP-osoite,Entrer l’adresse IP,IP Adresse eingeben,आईपी ​​पता दर्ज करें,Írja be az IP-címet,Inserire Indirizzo IP,IPアドレスを入力してください,IP 주소를 입력하세요,Skriv inn IP-adresse,Wprowadź adres IP,Digite o endereço IP ,Introduza o endereço IP ,Introduceți adresa IP,Введите IP-адрес,Zadajte IP adresu,Ingrese la dirección IP,Ange IP-adress,IP Adresini Girin,Введіть IP-адресу sb/ip_invalid,Invalid IP popup.,Invalid IP Address!,Невалиден IP адрес! ,IP 地址无效! ,IP 位址無效! ,Neplatná IP adresa! ,Ugyldig IP-adresse!,Ongeldig IP-adres!,Virheellinen IP-osoite!,Adresse IP invalide,Ungültige IP Adresse!,अमान्य आईपी पता!,Érvénytelen IP-cím!,Indirizzo IP Invalido!,IP アドレスが無効です!,IP 주소가 잘못되었습니다!,Ugyldig IP-adresse!,Nieprawidłowy adres IP!,Endereço IP inválido! ,Endereço IP inválido! ,Adresă IP nevalidă!,Неверный IP-адрес!,Neplatná IP adresa!,¡Dirección IP inválida!,Ogiltig IP-adress!,Geçersiz IP adresi!,Недійсна IP-адреса! @@ -45,22 +45,6 @@ host/start,Maximum players slider label,Start,Започнете,开始 ,開始, host/start__tooltip,Maximum players slider tooltip,Start the server.,Стартирайте сървъра. ,启动服务器。 ,啟動伺服器。 ,Spusťte server.,Start serveren.,Start de server.,Käynnistä palvelin.,Démarrez le serveur.,Starten Sie den Server.,सर्वर प्रारंभ करें.,Indítsa el a szervert.,Avviare il server.,サーバーを起動します。,서버를 시작합니다.,Start serveren.,Uruchom serwer.,Inicie o servidor. ,Inicie o servidor. ,Porniți serverul.,Запустите сервер.,Spustite server.,Inicie el servidor.,Starta servern.,Sunucuyu başlatın.,Запустіть сервер. host/start__tooltip_disabled,Maximum players slider tooltip,Check your settings are valid.,Проверете дали вашите настройки са валидни. ,检查您的设置是否有效。 ,檢查您的設定是否有效。 ,"Zkontrolujte, zda jsou vaše nastavení platná. ",Tjek at dine indstillinger er gyldige.,Controleer of uw instellingen geldig zijn.,"Tarkista, että asetuksesi ovat oikein.",Vérifiez que vos paramètres sont valides.,"Überprüfen Sie, ob Ihre Einstellungen gültig sind.",जांचें कि आपकी सेटिंग्स वैध हैं।,"Ellenőrizze, hogy a beállítások érvényesek-e.",Controlla che le tue impostazioni siano valide.,設定が有効であることを確認してください。,설정이 유효한지 확인하세요.,Sjekk at innstillingene dine er gyldige.,"Sprawdź, czy ustawienia są prawidłowe.",Verifique se suas configurações são válidas. ,Verifique se as suas definições são válidas. ,Verificați că setările dvs. sunt valide.,"Убедитесь, что ваши настройки действительны.","Skontrolujte, či sú vaše nastavenia platné.",Verifique que su configuración sea válida.,Kontrollera att dina inställningar är giltiga.,Ayarlarınızın geçerli olup olmadığını kontrol edin.,Перевірте правильність ваших налаштувань. ,,,,,,,,,,,,,,,,,,,,,,,,,,, -,Server Host,,,,,,,,,,,,,,,,,,,,,,,,,, -host/title,The title of the Host Game page,Host Game,,,,,,,,,,,,,,,,,,,,,,,,, -host/name,Server name field placeholder,Server Name,,,,,,,,,,,,,,,,,,,,,,,,, -host/name__tooltip,Server name field tooltip,The name of the server that other players will see in the server browser,,,,,,,,,,,,,,,,,,,,,,,,, -host/password,Password field placeholder,Password (leave blank for no password),,,,,,,,,,,,,,,,,,,,,,,,, -host/password__tooltip,Password field placeholder,Password for joining the game. Leave blank if no password is required,,,,,,,,,,,,,,,,,,,,,,,,, -host/public,Public checkbox label,Public Game,,,,,,,,,,,,,,,,,,,,,,,,, -host/public__tooltip,Public checkbox tooltip,List this game in the server browser.,,,,,,,,,,,,,,,,,,,,,,,,, -host/details,Details field placeholder,Enter some details about your server,,,,,,,,,,,,,,,,,,,,,,,,, -host/details__tooltip,Details field tooltip,Details about your server visible in the server browser.,,,,,,,,,,,,,,,,,,,,,,,,, -host/max_players,Maximum players slider label,Maximum Players,,,,,,,,,,,,,,,,,,,,,,,,, -host/max_players__tooltip,Maximum players slider tooltip,Maximum players allowed to join the game.,,,,,,,,,,,,,,,,,,,,,,,,, -host/start,Maximum players slider label,Start,,,,,,,,,,,,,,,,,,,,,,,,, -host/start__tooltip,Maximum players slider tooltip,Start the server.,,,,,,,,,,,,,,,,,,,,,,,,, -host/start__tooltip_disabled,Maximum players slider tooltip,Check your settings are valid.,,,,,,,,,,,,,,,,,,,,,,,,, -,,,,,,,,,,,,,,,,,,,,,,,,,,, ,Disconnect Reason,,,,,,,,,,,,,,,,,,,,,,,,,, dr/invalid_password,Invalid password popup.,Invalid Password!,Невалидна парола! ,无效的密码! ,無效的密碼! ,Neplatné heslo! ,Forkert kodeord!,Ongeldig wachtwoord!,Väärä salasana!,Mot de passe incorrect !,Ungültiges Passwort!,अवैध पासवर्ड!,Érvénytelen jelszó!,Password non valida!,無効なパスワード!,유효하지 않은 비밀번호!,Ugyldig passord!,Nieprawidłowe hasło!,Senha inválida! ,Verifique se as suas definições são válidas. ,Parolă Invalidă!,Неверный пароль!,Nesprávne heslo!,¡Contraseña invalida!,Felaktigt lösenord!,Geçersiz şifre!,Невірний пароль! dr/game_version,Different game versions.,"Game version mismatch! Server version: {0}, your version: {1}.","Несъответствие на версията на играта! Версия на сървъра: {0}, вашата версия: {1}. ",游戏版本不匹配!服务器版本:{0},您的版本:{1}。 ,遊戲版本不符!伺服器版本:{0},您的版本:{1}。 ,"Nesoulad verze hry! Verze serveru: {0}, vaše verze: {1}.","Spilversionen stemmer ikke overens! Serverversion: {0}, din version: {1}.","Spelversie komt niet overeen! Serverversie: {0}, jouw versie: {1}.","Peliversio ei täsmää! Palvelimen versio: {0}, sinun versiosi: {1}.","Version du jeu incompatible ! Version du serveur : {0}, version locale : {1}","Spielversion stimmt nicht überein! Server Version: {0}, Lokale Version: {1}.","गेम संस्करण बेमेल! सर्वर संस्करण: {0}, आपका संस्करण: {1}.","Nem egyezik a játék verziója! Szerververzió: {0}, az Ön verziója: {1}.","Versioni del gioco non combacianti! Versione del Server: {0}, La tua versione: {1}.",ゲームのバージョンが不一致です!サーバーのバージョン: {0}、あなたのバージョン: {1}。,"게임 버전이 일치하지 않습니다! 서버 버전: {0}, 귀하의 버전: {1}.","Spillversjonen samsvarer ikke! Serverversjon: {0}, din versjon: {1}.","Niezgodna wersja gry! Wersja serwera: {0}, Twoja wersja: {1}.","Incompatibilidade de versão do jogo! Versão do servidor: {0}, sua versão: {1}. ","Incompatibilidade de versão do jogo! Versão do servidor: {0}, a sua versão: {1}. ","Versiunea jocului nepotrivită! Versiunea serverului: {0}, versiunea dvs.: {1}.","Несоответствие версии игры! Версия сервера: {0}, ваша версия: {1}.","Nesúlad verzie hry! Verzia servera: {0}, vaša verzia: {1}.","¡La versión del juego no coincide! Versión del servidor: {0}, tu versión: {1}.","Spelversionen matchar inte! Serverversion: {0}, din version: {1}.","Oyun sürümü uyuşmazlığı! Sunucu sürümü: {0}, sürümünüz: {1}.","Невідповідність версії гри! Версія сервера: {0}, ваша версія: {1}." From e5051da7001c195cf0a3dbe3d32b8f9c9c87902e Mon Sep 17 00:00:00 2001 From: AMacro Date: Mon, 1 Jul 2024 19:05:32 +1000 Subject: [PATCH 18/34] Updated default server to https --- Lobby Servers/PHP Server/.htaccess | 4 ++++ Lobby Servers/PHP Server/Read Me.md | 2 +- Multiplayer/Settings.cs | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Lobby Servers/PHP Server/.htaccess b/Lobby Servers/PHP Server/.htaccess index 44f3fb2..c8f0917 100644 --- a/Lobby Servers/PHP Server/.htaccess +++ b/Lobby Servers/PHP Server/.htaccess @@ -1,6 +1,10 @@ # Enable the RewriteEngine RewriteEngine On +# Uncomment below to force HTTPS +# RewriteCond %{HTTPS} off +# RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301] + # Redirect all non-existing paths to index.php RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d diff --git a/Lobby Servers/PHP Server/Read Me.md b/Lobby Servers/PHP Server/Read Me.md index 4753196..5bc4c50 100644 --- a/Lobby Servers/PHP Server/Read Me.md +++ b/Lobby Servers/PHP Server/Read Me.md @@ -145,5 +145,5 @@ Example: ```apacheconf RewriteEngine On RewriteCond %{HTTPS} off -RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301] +RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301] ``` diff --git a/Multiplayer/Settings.cs b/Multiplayer/Settings.cs index 053ab2e..57ef567 100644 --- a/Multiplayer/Settings.cs +++ b/Multiplayer/Settings.cs @@ -38,7 +38,7 @@ public class Settings : UnityModManager.ModSettings, IDrawable [Space(10)] [Header("Lobby Server")] [Draw("Lobby Server address", Tooltip = "Address of lobby server for finding multiplayer games")] - public string LobbyServerAddress = "http://dv.mineit.space";//"http://localhost:8080"; + public string LobbyServerAddress = "https://dv.mineit.space";//"http://localhost:8080"; [Header("Last Server Connected to by IP")] [Draw("Last Remote IP", Tooltip = "The IP for the last server connected to by IP.")] public string LastRemoteIP = ""; From eb3b948160311756d8cfa7faa4b44c3dafc8f173 Mon Sep 17 00:00:00 2001 From: AMacro Date: Mon, 1 Jul 2024 20:15:34 +1000 Subject: [PATCH 19/34] Refactored server browser for consistency ServerBrowserPane is now responsible for cleanup tasks and building the UI, rather than the RightPaneControllerPatch --- .../Components/MainMenu/ServerBrowserPane.cs | 84 ++++++++++--------- .../MainMenu/RightPaneControllerPatch.cs | 30 ------- 2 files changed, 43 insertions(+), 71 deletions(-) diff --git a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs index a8709e3..df13cf2 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs @@ -29,7 +29,9 @@ public class ServerBrowserPane : MonoBehaviour private static readonly Regex PortRegex = new Regex(@"^((6553[0-5])|(655[0-2][0-9])|(65[0-4][0-9]{2})|(6[0-4][0-9]{3})|([1-5][0-9]{4})|([0-5]{0,5})|([0-9]{1,4}))$"); // @formatter:on - + private const int MAX_PORT_LEN = 5; + private const int MIN_PORT = 1024; + private const int MAX_PORT = 49151; //Gridview variables private ObservableCollectionExt gridViewModel = new ObservableCollectionExt(); @@ -40,7 +42,6 @@ public class ServerBrowserPane : MonoBehaviour //Button variables private ButtonDV buttonJoin; - //private Button buttonHost; private ButtonDV buttonRefresh; private ButtonDV buttonDirectIP; @@ -59,9 +60,12 @@ public class ServerBrowserPane : MonoBehaviour private void Awake() { Multiplayer.Log("MultiplayerPane Awake()"); - SetupMultiplayerButtons(); + CleanUI(); + BuildUI(); + SetupServerBrowser(); FillDummyServers(); + } private void OnEnable() @@ -78,8 +82,6 @@ private void OnEnable() buttonDirectIP.ToggleInteractable(true); buttonRefresh.ToggleInteractable(true); - //buttonHost.interactable = true; - } // Disable listeners @@ -88,46 +90,56 @@ private void OnDisable() this.SetupListeners(false); } - private void SetupMultiplayerButtons() + private void CleanUI() { - GameObject goDirectIP = GameObject.Find("ButtonTextIcon Manual"); - //GameObject goHost = GameObject.Find("ButtonTextIcon Host"); - GameObject goJoin = GameObject.Find("ButtonTextIcon Join"); - GameObject goRefresh = GameObject.Find("ButtonIcon Refresh"); + GameObject.Destroy(this.FindChildByName("Text Content")); + + GameObject.Destroy(this.FindChildByName("HardcoreSavingBanner")); + GameObject.Destroy(this.FindChildByName("TutorialSavingBanner")); - if (goDirectIP == null || /*goHost == null ||*/ goJoin == null || goRefresh == null) + GameObject.Destroy(this.FindChildByName("ButtonIcon OpenFolder")); + GameObject.Destroy(this.FindChildByName("ButtonIcon Rename")); + GameObject.Destroy(this.FindChildByName("ButtonTextIcon Load")); + + } + private void BuildUI() + { + + // Update title + GameObject titleObj = this.FindChildByName("Title"); + GameObject.Destroy(titleObj.GetComponentInChildren()); + titleObj.GetComponentInChildren().key = Locale.SERVER_BROWSER__TITLE_KEY; + titleObj.GetComponentInChildren().UpdateLocalization(); + + GameObject serverWindow = this.FindChildByName("Save Description"); + serverWindow.GetComponentInChildren().textWrappingMode = TextWrappingModes.Normal; + serverWindow.GetComponentInChildren().text = "Server browser not fully implemented.

Dummy servers are shown for demonstration purposes only.

Press refresh to attempt loading real servers."; + + // Update buttons on the multiplayer pane + GameObject goDirectIP = this.gameObject.UpdateButton("ButtonTextIcon Overwrite", "ButtonTextIcon Manual", Locale.SERVER_BROWSER__MANUAL_CONNECT_KEY, null, Multiplayer.AssetIndex.multiplayerIcon); + GameObject goJoin = this.gameObject.UpdateButton("ButtonTextIcon Save", "ButtonTextIcon Join", Locale.SERVER_BROWSER__JOIN_KEY, null, Multiplayer.AssetIndex.connectIcon); + GameObject goRefresh = this.gameObject.UpdateButton("ButtonIcon Delete", "ButtonIcon Refresh", Locale.SERVER_BROWSER__REFRESH_KEY, null, Multiplayer.AssetIndex.refreshIcon); + + + if (goDirectIP == null || goJoin == null || goRefresh == null) { Multiplayer.LogError("One or more buttons not found."); return; } - // Modify the existing buttons' properties - ModifyButton(goDirectIP, Locale.SERVER_BROWSER__MANUAL_CONNECT_KEY); - //ModifyButton(goHost, Locale.SERVER_BROWSER__HOST_KEY); - ModifyButton(goJoin, Locale.SERVER_BROWSER__JOIN_KEY); - - - // Set up event listeners and localization for DirectIP button + // Set up event listeners buttonDirectIP = goDirectIP.GetComponent(); buttonDirectIP.onClick.AddListener(DirectAction); - // Set up event listeners and localization for Join button buttonJoin = goJoin.GetComponent(); buttonJoin.onClick.AddListener(JoinAction); - // Set up event listeners and localization for Refresh button buttonRefresh = goRefresh.GetComponent(); buttonRefresh.onClick.AddListener(RefreshAction); - goDirectIP.SetActive(true); - //goHost.SetActive(true); - goJoin.SetActive(true); - goRefresh.SetActive(true); - + //Lock out the join button until a server has been selected buttonJoin.ToggleInteractable(false); - } - private void SetupServerBrowser() { GameObject GridviewGO = this.FindChildByName("GRID VIEW"); @@ -156,21 +168,9 @@ private void SetupListeners(bool on) } - private void ModifyButton(GameObject button, string key) - { - button.GetComponentInChildren().key = key; - - } - private GameObject FindButton(string name) - { - - return GameObject.Find(name); - } - #endregion #region UI callbacks - private void RefreshAction() { if (serverRefreshing) @@ -193,7 +193,6 @@ private void JoinAction() { buttonDirectIP.ToggleInteractable(false); buttonJoin.ToggleInteractable(false); - //buttonHost.interactable = false; if (selectedServer.HasPassword) { @@ -207,6 +206,7 @@ private void JoinAction() return; } + //No password, just connect SingletonBehaviour.Instance.StartClient(selectedServer.ip, selectedServer.port, null); } } @@ -216,7 +216,6 @@ private void DirectAction() Debug.Log($"DirectAction()"); buttonDirectIP.ToggleInteractable(false); buttonJoin.ToggleInteractable(false) ; - //buttonHost.interactable = false; //making a direct connection direct = true; @@ -263,6 +262,7 @@ private void ShowIpPopup() if (result.closedBy == PopupClosedByAction.Abortion) { buttonDirectIP.ToggleInteractable(true); + IndexChanged(gridView); //re-enable the join button if a valid gridview item is selected return; } @@ -290,6 +290,8 @@ private void ShowPortPopup() popup.labelTMPro.text = Locale.SERVER_BROWSER__PORT; popup.GetComponentInChildren().text = $"{Multiplayer.Settings.LastRemotePort}"; + popup.GetComponentInChildren().contentType = TMP_InputField.ContentType.IntegerNumber; + popup.GetComponentInChildren().characterLimit = MAX_PORT_LEN; popup.Closed += result => { diff --git a/Multiplayer/Patches/MainMenu/RightPaneControllerPatch.cs b/Multiplayer/Patches/MainMenu/RightPaneControllerPatch.cs index 4bff4d3..e0efd7c 100644 --- a/Multiplayer/Patches/MainMenu/RightPaneControllerPatch.cs +++ b/Multiplayer/Patches/MainMenu/RightPaneControllerPatch.cs @@ -46,31 +46,6 @@ private static void Prefix(RightPaneController __instance) // Clean up unnecessary components and child objects GameObject.Destroy(multiplayerPane.GetComponent()); GameObject.Destroy(multiplayerPane.GetComponent()); - GameObject.Destroy(multiplayerPane.FindChildByName("ButtonIcon OpenFolder")); - GameObject.Destroy(multiplayerPane.FindChildByName("ButtonIcon Rename")); - GameObject.Destroy(multiplayerPane.FindChildByName("ButtonTextIcon Load")); - GameObject.Destroy(multiplayerPane.FindChildByName("Text Content")); - - // Update UI elements - GameObject titleObj = multiplayerPane.FindChildByName("Title"); - titleObj.GetComponentInChildren().key = Locale.SERVER_BROWSER__TITLE_KEY; - GameObject.Destroy(titleObj.GetComponentInChildren()); - - GameObject content = multiplayerPane.FindChildByName("text main"); - //content.GetComponentInChildren().text = "Server browser not yet implemented."; - - GameObject serverWindow = multiplayerPane.FindChildByName("Save Description"); - serverWindow.GetComponentInChildren().textWrappingMode = TextWrappingModes.Normal; - serverWindow.GetComponentInChildren().text = "Server browser not fully implemented.

Dummy servers are shown for demonstration purposes only.

Press refresh to attempt loading real servers."; - - // Update buttons on the multiplayer pane - multiplayerPane.UpdateButton("ButtonTextIcon Overwrite", "ButtonTextIcon Manual", Locale.SERVER_BROWSER__MANUAL_CONNECT_KEY, null, Multiplayer.AssetIndex.multiplayerIcon); - //multiplayerPane.UpdateButton("ButtonTextIcon Load", "ButtonTextIcon Host", Locale.SERVER_BROWSER__HOST_KEY, null, Multiplayer.AssetIndex.lockIcon); - multiplayerPane.UpdateButton("ButtonTextIcon Save", "ButtonTextIcon Join", Locale.SERVER_BROWSER__JOIN_KEY, null, Multiplayer.AssetIndex.connectIcon); - GameObject go = multiplayerPane.UpdateButton("ButtonIcon Delete", "ButtonIcon Refresh", Locale.SERVER_BROWSER__REFRESH_KEY, null, Multiplayer.AssetIndex.refreshIcon); - - - // Add the MultiplayerPane component multiplayerPane.AddComponent(); // Create and initialize MainMenuThingsAndStuff @@ -90,11 +65,6 @@ private static void Prefix(RightPaneController __instance) - - - - - // Check if the host pane already exists if (__instance.HasChildWithName("PaneRight Host")) return; From 6e8df4677bf4d4b1b00e7084bf17a79f6d5ad87e Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 6 Jul 2024 14:28:13 +1000 Subject: [PATCH 20/34] Added auto refresh Once the first refresh has been done, auto refresh will occur every 30 seconds. Refresh can no longer be spammed and will be locked out for 10 seconds following the last refresh (auto or manual) --- .../Components/MainMenu/ServerBrowserPane.cs | 38 +++++++++++++++++-- locale.csv | 2 +- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs index df13cf2..c8b5e8a 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs @@ -45,7 +45,12 @@ public class ServerBrowserPane : MonoBehaviour private ButtonDV buttonRefresh; private ButtonDV buttonDirectIP; + private bool serverRefreshing = false; + private bool autoRefresh = false; + private float timePassed = 0f; //time since last refresh + private const int AUTO_REFRESH_TIME = 30; //how often to refresh in auto + private const int REFRESH_MIN_TIME = 10; //Stop refresh spam //connection parameters private string ipAddress; @@ -90,6 +95,24 @@ private void OnDisable() this.SetupListeners(false); } + private void Update() + { + + timePassed += Time.deltaTime; + + if (autoRefresh && !serverRefreshing) + { + if (timePassed >= AUTO_REFRESH_TIME) + { + RefreshAction(); + } + else if(timePassed >= REFRESH_MIN_TIME) + { + buttonRefresh.ToggleInteractable(true); + } + } + } + private void CleanUI() { GameObject.Destroy(this.FindChildByName("Text Content")); @@ -113,7 +136,7 @@ private void BuildUI() GameObject serverWindow = this.FindChildByName("Save Description"); serverWindow.GetComponentInChildren().textWrappingMode = TextWrappingModes.Normal; - serverWindow.GetComponentInChildren().text = "Server browser not fully implemented.

Dummy servers are shown for demonstration purposes only.

Press refresh to attempt loading real servers."; + serverWindow.GetComponentInChildren().text = "Server browser not fully implemented.

Dummy servers are shown for demonstration purposes only.

Press refresh to attempt loading real servers.
After pressing refresh, auto refresh will occur every 30 seconds."; // Update buttons on the multiplayer pane GameObject goDirectIP = this.gameObject.UpdateButton("ButtonTextIcon Overwrite", "ButtonTextIcon Manual", Locale.SERVER_BROWSER__MANUAL_CONNECT_KEY, null, Multiplayer.AssetIndex.multiplayerIcon); @@ -176,14 +199,18 @@ private void RefreshAction() if (serverRefreshing) return; - serverRefreshing = true; - buttonJoin.ToggleInteractable(false); + if (selectedServer != null) { serverIDOnRefresh = selectedServer.id; } + serverRefreshing = true; + autoRefresh = true; + buttonJoin.ToggleInteractable(false); + buttonRefresh.ToggleInteractable(false); + StartCoroutine(GetRequest($"{Multiplayer.Settings.LobbyServerAddress}/list_game_servers")); } @@ -429,9 +456,12 @@ IEnumerator GetRequest(string uri) serverIDOnRefresh = null; } - serverRefreshing = false; + } } + + serverRefreshing = false; + timePassed = 0; } private static void ShowOkPopup(string text, Action onClick) diff --git a/locale.csv b/locale.csv index 8845a6f..94acd69 100644 --- a/locale.csv +++ b/locale.csv @@ -22,7 +22,7 @@ sb/join_game__tooltip,The tooltip shown when hovering over the 'Join Server' but sb/join_game__tooltip_disabled,The tooltip shown when hovering over the 'Join Server' button.,Select a game to join.,,,,,,,,,,,,,,,,,,,,,,,, sb/refresh,refresh,Refresh,Опресняване ,刷新 ,重新整理 ,Obnovit ,Opdater,Vernieuwen,virkistää,Rafraîchir,Aktualisierung,ताज़ा करना,Frissítés,ricaricare,リフレッシュ,새로 고치다,Forfriske,Odświeżać,Atualizar ,Atualizar ,Reîmprospăta,Обновить,Obnoviť,Actualizar,Uppdatera,Yenile,Оновити sb/refresh__tooltip,The tooltip shown when hovering over the 'Refresh Server' button.,Refresh server list.,Обновяване на списъка със сървъри. ,刷新服务器列表。 ,刷新伺服器清單。 ,Obnovit seznam serverů. ,Opdater serverliste.,Vernieuw de serverlijst.,Päivitä palvelinluettelo.,Actualiser la liste des serveurs.,Serverliste aktualisieren.,सर्वर सूची ताज़ा करें.,Szerverlista frissítése.,Aggiorna l'elenco dei server.,サーバーリストを更新します。,서버 목록을 새로 고칩니다.,Oppdater serverlisten.,Odśwież listę serwerów.,Atualizar lista de servidores.,Atualizar lista de servidores. ,Actualizează lista de servere.,Обновить список серверов.,Obnoviť zoznam serverov.,Actualizar la lista de servidores.,Uppdatera serverlistan.,Sunucu listesini yenileyin.,Оновити список серверів. -sb/refresh__tooltip_disabled,Unused,,,,,,,,,,,,,,,,,,,,,,,,,, +sb/refresh__tooltip_disabled,Unused,Refreshing, please wait...,,,,,,,,,,,,,,,,,,,,,,,,, sb/ip,IP popup,Enter IP Address,Въведете IP адрес,输入IP地址 ,輸入IP位址 ,Zadejte IP adresu ,Indtast IP-adresse,Voer het IP-adres in,Anna IP-osoite,Entrer l’adresse IP,IP Adresse eingeben,आईपी ​​पता दर्ज करें,Írja be az IP-címet,Inserire Indirizzo IP,IPアドレスを入力してください,IP 주소를 입력하세요,Skriv inn IP-adresse,Wprowadź adres IP,Digite o endereço IP ,Introduza o endereço IP ,Introduceți adresa IP,Введите IP-адрес,Zadajte IP adresu,Ingrese la dirección IP,Ange IP-adress,IP Adresini Girin,Введіть IP-адресу sb/ip_invalid,Invalid IP popup.,Invalid IP Address!,Невалиден IP адрес! ,IP 地址无效! ,IP 位址無效! ,Neplatná IP adresa! ,Ugyldig IP-adresse!,Ongeldig IP-adres!,Virheellinen IP-osoite!,Adresse IP invalide,Ungültige IP Adresse!,अमान्य आईपी पता!,Érvénytelen IP-cím!,Indirizzo IP Invalido!,IP アドレスが無効です!,IP 주소가 잘못되었습니다!,Ugyldig IP-adresse!,Nieprawidłowy adres IP!,Endereço IP inválido! ,Endereço IP inválido! ,Adresă IP nevalidă!,Неверный IP-адрес!,Neplatná IP adresa!,¡Dirección IP inválida!,Ogiltig IP-adress!,Geçersiz IP adresi!,Недійсна IP-адреса! sb/port,Port popup.,Enter Port (7777 by default),Въведете порт (7777 по подразбиране) ,输入端口(默认为 7777) ,輸入連接埠(預設為 7777) ,Zadejte port (ve výchozím nastavení 7777),Indtast port (7777 som standard),Poort invoeren (standaard 7777),Anna portti (oletuksena 7777),Entrer le port (7777 par défaut),Port eingeben (Standard: 7777),पोर्ट दर्ज करें (डिफ़ॉल्ट रूप से 7777),Írja be a portot (alapértelmezés szerint 7777),Inserire Porta (7777 di default),ポートを入力します (デフォルトでは 7777),포트 입력(기본적으로 7777),Angi port (7777 som standard),Wprowadź port (domyślnie 7777),Insira a porta (7777 por padrão) ,Introduza a porta (7777 por defeito) ,Introduceți port (7777 implicit),Введите порт (7777 по умолчанию),Zadajte port (predvolene 7777),Introduzca el número de puerto(7777 por defecto),Ange port (7777 som standard),Bağlantı Noktasını Girin (varsayılan olarak 7777),Введіть порт (7777 за замовчуванням) From a99179a2895b3b4736643f606fde666233c4fe1b Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 7 Jul 2024 01:20:13 +1000 Subject: [PATCH 21/34] Server details displayed in pane When selecting a server, its details are now shown in the adjacent pane --- .../Components/MainMenu/ServerBrowserPane.cs | 147 +++++++++++++++++- Multiplayer/Multiplayer.csproj | 1 + locale.csv | 2 +- 3 files changed, 142 insertions(+), 8 deletions(-) diff --git a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs index c8b5e8a..deb95c8 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs @@ -14,6 +14,7 @@ using UnityEngine.Networking; using System.Linq; using Multiplayer.Networking.Data; +using DV; @@ -45,6 +46,11 @@ public class ServerBrowserPane : MonoBehaviour private ButtonDV buttonRefresh; private ButtonDV buttonDirectIP; + //Misc GUI Elements + private TextMeshProUGUI serverName; + private TextMeshProUGUI detailsPane; + private ScrollRect serverInfo; + private bool serverRefreshing = false; private bool autoRefresh = false; @@ -120,6 +126,8 @@ private void CleanUI() GameObject.Destroy(this.FindChildByName("HardcoreSavingBanner")); GameObject.Destroy(this.FindChildByName("TutorialSavingBanner")); + GameObject.Destroy(this.FindChildByName("Thumbnail")); + GameObject.Destroy(this.FindChildByName("ButtonIcon OpenFolder")); GameObject.Destroy(this.FindChildByName("ButtonIcon Rename")); GameObject.Destroy(this.FindChildByName("ButtonTextIcon Load")); @@ -134,9 +142,94 @@ private void BuildUI() titleObj.GetComponentInChildren().key = Locale.SERVER_BROWSER__TITLE_KEY; titleObj.GetComponentInChildren().UpdateLocalization(); - GameObject serverWindow = this.FindChildByName("Save Description"); - serverWindow.GetComponentInChildren().textWrappingMode = TextWrappingModes.Normal; - serverWindow.GetComponentInChildren().text = "Server browser not fully implemented.

Dummy servers are shown for demonstration purposes only.

Press refresh to attempt loading real servers.
After pressing refresh, auto refresh will occur every 30 seconds."; + //Rebuild the save description pane + GameObject serverWindowGO = this.FindChildByName("Save Description"); + GameObject serverNameGO = serverWindowGO.FindChildByName("text list [noloc]"); + GameObject scrollViewGO = this.FindChildByName("Scroll View"); + + //Create new objects + GameObject serverScroll = Instantiate(scrollViewGO, serverNameGO.transform.position, Quaternion.identity, serverWindowGO.transform); + + + /* + * Setup server name + */ + serverNameGO.name = "Server Title"; + + //Positioning + RectTransform serverNameRT = serverNameGO.GetComponent(); + serverNameRT.pivot = new Vector2(1f, 1f); + serverNameRT.anchorMin = new Vector2(0f, 1f); + serverNameRT.anchorMax = new Vector2(1f, 1f); + serverNameRT.SetInsetAndSizeFromParentEdge(RectTransform.Edge.Top, 0, 54); + + //Text + serverName = serverNameGO.GetComponentInChildren(); + serverName.alignment = TextAlignmentOptions.Center; + serverName.textWrappingMode = TextWrappingModes.Normal; + serverName.fontSize = 22; + serverName.text = "Server Browser Info"; + + // Create new ScrollRect object + GameObject viewport = serverScroll.FindChildByName("Viewport"); + serverScroll.transform.SetParent(serverWindowGO.transform, false); + + // Positioning ScrollRect + RectTransform serverScrollRT = serverScroll.GetComponent(); + serverScrollRT.pivot = new Vector2(1f, 1f); + serverScrollRT.anchorMin = new Vector2(0f, 1f); + serverScrollRT.anchorMax = new Vector2(1f, 1f); + serverScrollRT.localEulerAngles = Vector3.zero; + serverScrollRT.SetInsetAndSizeFromParentEdge(RectTransform.Edge.Top, 54, 400); + serverScrollRT.SetInsetAndSizeFromParentEdge(RectTransform.Edge.Left, 0, serverNameGO.GetComponent().rect.width); + + RectTransform viewportRT = viewport.GetComponent(); + + // Assign Viewport to ScrollRect + ScrollRect scrollRect = serverScroll.GetComponent(); + scrollRect.viewport = viewportRT; + + // Create Content + GameObject.Destroy(serverScroll.FindChildByName("GRID VIEW").gameObject); + GameObject content = new GameObject("Content", typeof(RectTransform), typeof(ContentSizeFitter), typeof(VerticalLayoutGroup)); + content.transform.SetParent(viewport.transform, false); + ContentSizeFitter contentSF = content.GetComponent(); + contentSF.verticalFit = ContentSizeFitter.FitMode.PreferredSize; + VerticalLayoutGroup contentVLG = content.GetComponent(); + contentVLG.childControlWidth = true; + contentVLG.childControlHeight = true; + RectTransform contentRT = content.GetComponent(); + contentRT.pivot = new Vector2(0f, 1f); + contentRT.anchorMin = new Vector2(0f, 1f); + contentRT.anchorMax = new Vector2(1f, 1f); + contentRT.offsetMin = Vector2.zero; + contentRT.offsetMax = Vector2.zero; + scrollRect.content = contentRT; + + // Create TextMeshProUGUI object + GameObject textContainerGO = new GameObject("Details Container", typeof(HorizontalLayoutGroup)); + textContainerGO.transform.SetParent(content.transform, false); + contentRT.localPosition = new Vector3(contentRT.localPosition.x + 10, contentRT.localPosition.y, contentRT.localPosition.z); + + + GameObject textGO = new GameObject("Details Text", typeof(TextMeshProUGUI)); + textGO.transform.SetParent(textContainerGO.transform, false); + HorizontalLayoutGroup textHLG = textGO.GetComponent(); + detailsPane = textGO.GetComponent(); + detailsPane.textWrappingMode = TextWrappingModes.Normal; + detailsPane.fontSize = 18; + detailsPane.text = "Server browser not fully implemented.

Dummy servers are shown for demonstration purposes only.

Press refresh to attempt loading real servers.
After pressing refresh, auto refresh will occur every 30 seconds."; + + // Adjust text RectTransform to fit content + RectTransform textRT = textGO.GetComponent(); + textRT.pivot = new Vector2(0.5f, 1f); + textRT.anchorMin = new Vector2(0, 1); + textRT.anchorMax = new Vector2(1, 1); + textRT.offsetMin = new Vector2(0, -detailsPane.preferredHeight); + textRT.offsetMax = new Vector2(0, 0); + + // Set content size to fit text + contentRT.sizeDelta = new Vector2(contentRT.sizeDelta.x -50, detailsPane.preferredHeight); // Update buttons on the multiplayer pane GameObject goDirectIP = this.gameObject.UpdateButton("ButtonTextIcon Overwrite", "ButtonTextIcon Manual", Locale.SERVER_BROWSER__MANUAL_CONNECT_KEY, null, Multiplayer.AssetIndex.multiplayerIcon); @@ -165,7 +258,7 @@ private void BuildUI() } private void SetupServerBrowser() { - GameObject GridviewGO = this.FindChildByName("GRID VIEW"); + GameObject GridviewGO = this.FindChildByName("Scroll View").FindChildByName("GRID VIEW"); SaveLoadGridView slgv = GridviewGO.GetComponent(); GridviewGO.SetActive(false); @@ -261,7 +354,19 @@ private void IndexChanged(AGridView gridView) Debug.Log($"Selected server: {gridViewModel[gridView.SelectedModelIndex].Name}"); selectedServer = gridViewModel[gridView.SelectedModelIndex]; - buttonJoin.ToggleInteractable(true); + + UpdateDetailsPane(); + + //Check if we can connect to this server + + Debug.Log($"server: \"{selectedServer.GameVersion}\" \"{selectedServer.MultiplayerVersion}\""); + Debug.Log($"client: \"{BuildInfo.BUILD_VERSION_MAJOR.ToString()}\" \"{Multiplayer.ModEntry.Version.ToString()}\""); + Debug.Log($"result: \"{selectedServer.GameVersion == BuildInfo.BUILD_VERSION_MAJOR.ToString()}\" \"{selectedServer.MultiplayerVersion == Multiplayer.ModEntry.Version.ToString()}\""); + + bool canConnect = selectedServer.GameVersion == BuildInfo.BUILD_VERSION_MAJOR.ToString() && + selectedServer.MultiplayerVersion == Multiplayer.ModEntry.Version.ToString(); + + buttonJoin.ToggleInteractable(canConnect); } else { @@ -271,6 +376,32 @@ private void IndexChanged(AGridView gridView) #endregion + private void UpdateDetailsPane() + { + string details=""; + + if (selectedServer != null) + { + Debug.Log("Prepping Data"); + serverName.text = selectedServer.Name; + + details = "Game mode: " + LobbyServerData.GetGameModeFromInt(selectedServer.GameMode) + "
"; + details += "Game difficulty: " + LobbyServerData.GetDifficultyFromInt(selectedServer.Difficulty) + "
"; + details += "In-game time passed: " + selectedServer.TimePassed + "
"; + details += "Players: " + selectedServer.CurrentPlayers + '/' + selectedServer.MaxPlayers + "
"; + details += "Password required: " + (selectedServer.HasPassword ? "Yes" : "No") + "
"; + details += "Requires mods: " + (selectedServer.RequiredMods != null? "Yes" : "No") + "
"; + details += "
"; + details += "Game version: " + (selectedServer.GameVersion != BuildInfo.BUILD_VERSION_MAJOR.ToString() ? "" : "") + selectedServer.GameVersion + "
"; + details += "Multiplayer version: " + (selectedServer.MultiplayerVersion != Multiplayer.ModEntry.Version.ToString() ? "" : "") + selectedServer.MultiplayerVersion + "
"; + details += "
"; + details += selectedServer.ServerDetails; + + Debug.Log("Finished Prepping Data"); + detailsPane.text = details; + } + } + private void ShowIpPopup() { Debug.Log("In ShowIpPpopup"); @@ -404,8 +535,6 @@ private void HandleConnectionFailed() // ShowConnectionFailedPopup(); } - - IEnumerator GetRequest(string uri) { using (UnityWebRequest webRequest = UnityWebRequest.Get(uri)) @@ -499,6 +628,10 @@ private void FillDummyServers() item.Ping = UnityEngine.Random.Range(5, 1500); item.HasPassword = UnityEngine.Random.Range(0, 10) > 5; + item.GameVersion = UnityEngine.Random.Range(1, 10) > 3 ? BuildInfo.BUILD_VERSION_MAJOR.ToString() : "97"; + item.MultiplayerVersion = UnityEngine.Random.Range(1, 10) > 3 ? Multiplayer.ModEntry.Version.ToString() : "0.1.0"; + + Debug.Log(item.HasPassword); gridViewModel.Add(item); } diff --git a/Multiplayer/Multiplayer.csproj b/Multiplayer/Multiplayer.csproj index 0b78777..a2e5fb9 100644 --- a/Multiplayer/Multiplayer.csproj +++ b/Multiplayer/Multiplayer.csproj @@ -77,6 +77,7 @@ + diff --git a/locale.csv b/locale.csv index 94acd69..718e29d 100644 --- a/locale.csv +++ b/locale.csv @@ -22,7 +22,7 @@ sb/join_game__tooltip,The tooltip shown when hovering over the 'Join Server' but sb/join_game__tooltip_disabled,The tooltip shown when hovering over the 'Join Server' button.,Select a game to join.,,,,,,,,,,,,,,,,,,,,,,,, sb/refresh,refresh,Refresh,Опресняване ,刷新 ,重新整理 ,Obnovit ,Opdater,Vernieuwen,virkistää,Rafraîchir,Aktualisierung,ताज़ा करना,Frissítés,ricaricare,リフレッシュ,새로 고치다,Forfriske,Odświeżać,Atualizar ,Atualizar ,Reîmprospăta,Обновить,Obnoviť,Actualizar,Uppdatera,Yenile,Оновити sb/refresh__tooltip,The tooltip shown when hovering over the 'Refresh Server' button.,Refresh server list.,Обновяване на списъка със сървъри. ,刷新服务器列表。 ,刷新伺服器清單。 ,Obnovit seznam serverů. ,Opdater serverliste.,Vernieuw de serverlijst.,Päivitä palvelinluettelo.,Actualiser la liste des serveurs.,Serverliste aktualisieren.,सर्वर सूची ताज़ा करें.,Szerverlista frissítése.,Aggiorna l'elenco dei server.,サーバーリストを更新します。,서버 목록을 새로 고칩니다.,Oppdater serverlisten.,Odśwież listę serwerów.,Atualizar lista de servidores.,Atualizar lista de servidores. ,Actualizează lista de servere.,Обновить список серверов.,Obnoviť zoznam serverov.,Actualizar la lista de servidores.,Uppdatera serverlistan.,Sunucu listesini yenileyin.,Оновити список серверів. -sb/refresh__tooltip_disabled,Unused,Refreshing, please wait...,,,,,,,,,,,,,,,,,,,,,,,,, +sb/refresh__tooltip_disabled,Unused,"Refreshing, please wait...",,,,,,,,,,,,,,,,,,,,,,,,, sb/ip,IP popup,Enter IP Address,Въведете IP адрес,输入IP地址 ,輸入IP位址 ,Zadejte IP adresu ,Indtast IP-adresse,Voer het IP-adres in,Anna IP-osoite,Entrer l’adresse IP,IP Adresse eingeben,आईपी ​​पता दर्ज करें,Írja be az IP-címet,Inserire Indirizzo IP,IPアドレスを入力してください,IP 주소를 입력하세요,Skriv inn IP-adresse,Wprowadź adres IP,Digite o endereço IP ,Introduza o endereço IP ,Introduceți adresa IP,Введите IP-адрес,Zadajte IP adresu,Ingrese la dirección IP,Ange IP-adress,IP Adresini Girin,Введіть IP-адресу sb/ip_invalid,Invalid IP popup.,Invalid IP Address!,Невалиден IP адрес! ,IP 地址无效! ,IP 位址無效! ,Neplatná IP adresa! ,Ugyldig IP-adresse!,Ongeldig IP-adres!,Virheellinen IP-osoite!,Adresse IP invalide,Ungültige IP Adresse!,अमान्य आईपी पता!,Érvénytelen IP-cím!,Indirizzo IP Invalido!,IP アドレスが無効です!,IP 주소가 잘못되었습니다!,Ugyldig IP-adresse!,Nieprawidłowy adres IP!,Endereço IP inválido! ,Endereço IP inválido! ,Adresă IP nevalidă!,Неверный IP-адрес!,Neplatná IP adresa!,¡Dirección IP inválida!,Ogiltig IP-adress!,Geçersiz IP adresi!,Недійсна IP-адреса! sb/port,Port popup.,Enter Port (7777 by default),Въведете порт (7777 по подразбиране) ,输入端口(默认为 7777) ,輸入連接埠(預設為 7777) ,Zadejte port (ve výchozím nastavení 7777),Indtast port (7777 som standard),Poort invoeren (standaard 7777),Anna portti (oletuksena 7777),Entrer le port (7777 par défaut),Port eingeben (Standard: 7777),पोर्ट दर्ज करें (डिफ़ॉल्ट रूप से 7777),Írja be a portot (alapértelmezés szerint 7777),Inserire Porta (7777 di default),ポートを入力します (デフォルトでは 7777),포트 입력(기본적으로 7777),Angi port (7777 som standard),Wprowadź port (domyślnie 7777),Insira a porta (7777 por padrão) ,Introduza a porta (7777 por defeito) ,Introduceți port (7777 implicit),Введите порт (7777 по умолчанию),Zadajte port (predvolene 7777),Introduzca el número de puerto(7777 por defecto),Ange port (7777 som standard),Bağlantı Noktasını Girin (varsayılan olarak 7777),Введіть порт (7777 за замовчуванням) From 86f8245f99f638fce848148f33135ca87d9852aa Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 7 Jul 2024 09:26:18 +1000 Subject: [PATCH 22/34] Updated translations for server browser details pane Added some CSV parsing logic to detect missing quotes in CSVs (flag too many columns), preventing crashes. --- .../Components/MainMenu/ServerBrowserPane.cs | 22 ++-- Multiplayer/Locale.cs | 25 ++-- Multiplayer/Utils/Csv.cs | 7 ++ locale.csv | 112 +++++++++--------- 4 files changed, 95 insertions(+), 71 deletions(-) diff --git a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs index deb95c8..4d04259 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs @@ -170,6 +170,10 @@ private void BuildUI() serverName.fontSize = 22; serverName.text = "Server Browser Info"; + /* + * Setup server details + */ + // Create new ScrollRect object GameObject viewport = serverScroll.FindChildByName("Viewport"); serverScroll.transform.SetParent(serverWindowGO.transform, false); @@ -385,15 +389,17 @@ private void UpdateDetailsPane() Debug.Log("Prepping Data"); serverName.text = selectedServer.Name; - details = "Game mode: " + LobbyServerData.GetGameModeFromInt(selectedServer.GameMode) + "
"; - details += "Game difficulty: " + LobbyServerData.GetDifficultyFromInt(selectedServer.Difficulty) + "
"; - details += "In-game time passed: " + selectedServer.TimePassed + "
"; - details += "Players: " + selectedServer.CurrentPlayers + '/' + selectedServer.MaxPlayers + "
"; - details += "Password required: " + (selectedServer.HasPassword ? "Yes" : "No") + "
"; - details += "Requires mods: " + (selectedServer.RequiredMods != null? "Yes" : "No") + "
"; + //note: built-in localisations have a trailing colon e.g. 'Game mode:' + + details = "" + LocalizationAPI.L("launcher/game_mode", Array.Empty()) + " " + LobbyServerData.GetGameModeFromInt(selectedServer.GameMode) + "
"; + details += "" + LocalizationAPI.L("launcher/difficulty", Array.Empty()) + " " + LobbyServerData.GetDifficultyFromInt(selectedServer.Difficulty) + "
"; + details += "" + LocalizationAPI.L("launcher/in_game_time_passed", Array.Empty()) + " " + selectedServer.TimePassed + "
"; + details += "" + Locale.SERVER_BROWSER__PLAYERS + ": " + selectedServer.CurrentPlayers + '/' + selectedServer.MaxPlayers + "
"; + details += "" + Locale.SERVER_BROWSER__PASSWORD_REQUIRED + ": " + (selectedServer.HasPassword ? Locale.SERVER_BROWSER__YES : Locale.SERVER_BROWSER__NO) + "
"; + details += "" + Locale.SERVER_BROWSER__MODS_REQUIRED + ": " + (selectedServer.RequiredMods != null? Locale.SERVER_BROWSER__YES : Locale.SERVER_BROWSER__NO) + "
"; details += "
"; - details += "Game version: " + (selectedServer.GameVersion != BuildInfo.BUILD_VERSION_MAJOR.ToString() ? "" : "") + selectedServer.GameVersion + "
"; - details += "Multiplayer version: " + (selectedServer.MultiplayerVersion != Multiplayer.ModEntry.Version.ToString() ? "" : "") + selectedServer.MultiplayerVersion + "
"; + details += "" + Locale.SERVER_BROWSER__GAME_VERSION + ": " + (selectedServer.GameVersion != BuildInfo.BUILD_VERSION_MAJOR.ToString() ? "" : "") + selectedServer.GameVersion + "
"; + details += "" + Locale.SERVER_BROWSER__MOD_VERSION + ": " + (selectedServer.MultiplayerVersion != Multiplayer.ModEntry.Version.ToString() ? "" : "") + selectedServer.MultiplayerVersion + "
"; details += "
"; details += selectedServer.ServerDetails; diff --git a/Multiplayer/Locale.cs b/Multiplayer/Locale.cs index bc30ea9..4d6dca5 100644 --- a/Multiplayer/Locale.cs +++ b/Multiplayer/Locale.cs @@ -30,38 +30,43 @@ public static class Locale #region Server Browser public static string SERVER_BROWSER__TITLE => Get(SERVER_BROWSER__TITLE_KEY); public const string SERVER_BROWSER__TITLE_KEY = $"{PREFIX_SERVER_BROWSER}/title"; - public static string SERVER_BROWSER__MANUAL_CONNECT => Get(SERVER_BROWSER__MANUAL_CONNECT_KEY); - public const string SERVER_BROWSER__MANUAL_CONNECT_KEY = $"{PREFIX_SERVER_BROWSER}/manual_connect"; - + public const string SERVER_BROWSER__MANUAL_CONNECT_KEY = $"{PREFIX_SERVER_BROWSER}/manual_connect"; public static string SERVER_BROWSER__HOST => Get(SERVER_BROWSER__HOST_KEY); public const string SERVER_BROWSER__HOST_KEY = $"{PREFIX_SERVER_BROWSER}/host"; public static string SERVER_BROWSER__REFRESH => Get(SERVER_BROWSER__REFRESH_KEY); public const string SERVER_BROWSER__REFRESH_KEY = $"{PREFIX_SERVER_BROWSER}/refresh"; - public static string SERVER_BROWSER__JOIN => Get(SERVER_BROWSER__JOIN_KEY); public const string SERVER_BROWSER__JOIN_KEY = $"{PREFIX_SERVER_BROWSER}/join_game"; - public static string SERVER_BROWSER__IP => Get(SERVER_BROWSER__IP_KEY); private const string SERVER_BROWSER__IP_KEY = $"{PREFIX_SERVER_BROWSER}/ip"; - public static string SERVER_BROWSER__IP_INVALID => Get(SERVER_BROWSER__IP_INVALID_KEY); private const string SERVER_BROWSER__IP_INVALID_KEY = $"{PREFIX_SERVER_BROWSER}/ip_invalid"; - public static string SERVER_BROWSER__PORT => Get(SERVER_BROWSER__PORT_KEY); private const string SERVER_BROWSER__PORT_KEY = $"{PREFIX_SERVER_BROWSER}/port"; - public static string SERVER_BROWSER__PORT_INVALID => Get(SERVER_BROWSER__PORT_INVALID_KEY); private const string SERVER_BROWSER__PORT_INVALID_KEY = $"{PREFIX_SERVER_BROWSER}/port_invalid"; - public static string SERVER_BROWSER__PASSWORD => Get(SERVER_BROWSER__PASSWORD_KEY); private const string SERVER_BROWSER__PASSWORD_KEY = $"{PREFIX_SERVER_BROWSER}/password"; + public static string SERVER_BROWSER__PLAYERS => Get(SERVER_BROWSER__PLAYERS_KEY); + private const string SERVER_BROWSER__PLAYERS_KEY = $"{PREFIX_SERVER_BROWSER}/players"; + public static string SERVER_BROWSER__PASSWORD_REQUIRED => Get(SERVER_BROWSER__PASSWORD_REQUIRED_KEY); + private const string SERVER_BROWSER__PASSWORD_REQUIRED_KEY = $"{PREFIX_SERVER_BROWSER}/password_required"; + public static string SERVER_BROWSER__MODS_REQUIRED => Get(SERVER_BROWSER__MODS_REQUIRED_KEY); + private const string SERVER_BROWSER__MODS_REQUIRED_KEY = $"{PREFIX_SERVER_BROWSER}/mods_required"; + public static string SERVER_BROWSER__GAME_VERSION => Get(SERVER_BROWSER__GAME_VERSION_KEY); + private const string SERVER_BROWSER__GAME_VERSION_KEY = $"{PREFIX_SERVER_BROWSER}/game_version"; + public static string SERVER_BROWSER__MOD_VERSION => Get(SERVER_BROWSER__MOD_VERSION_KEY); + private const string SERVER_BROWSER__MOD_VERSION_KEY = $"{PREFIX_SERVER_BROWSER}/mod_version"; + public static string SERVER_BROWSER__YES => Get(SERVER_BROWSER__YES_KEY); + private const string SERVER_BROWSER__YES_KEY = $"{PREFIX_SERVER_BROWSER}/yes"; + public static string SERVER_BROWSER__NO => Get(SERVER_BROWSER__NO_KEY); + private const string SERVER_BROWSER__NO_KEY = $"{PREFIX_SERVER_BROWSER}/no"; #endregion #region Server Host public static string SERVER_HOST__TITLE => Get(SERVER_HOST__TITLE_KEY); public const string SERVER_HOST__TITLE_KEY = $"{PREFIX_SERVER_HOST}/title"; - public static string SERVER_HOST_PASSWORD => Get(SERVER_HOST_PASSWORD_KEY); public const string SERVER_HOST_PASSWORD_KEY = $"{PREFIX_SERVER_HOST}/password"; public static string SERVER_HOST_NAME => Get(SERVER_HOST_NAME_KEY); diff --git a/Multiplayer/Utils/Csv.cs b/Multiplayer/Utils/Csv.cs index a58ceb0..0aca45a 100644 --- a/Multiplayer/Utils/Csv.cs +++ b/Multiplayer/Utils/Csv.cs @@ -37,6 +37,13 @@ public static ReadOnlyDictionary> Parse(strin if (values.Count == 0 || string.IsNullOrWhiteSpace(values[0])) continue; + //ensure we don't have too many + if (values.Count > columns.Count) + { + Multiplayer.LogWarning($"CSV Line {i + 1}: Found {values.Count} columns, expected {columns.Count}\r\n\t{line}"); + continue; + } + string key = values[0]; for (int j = 0; j < values.Count; j++) ((Dictionary)columns[j]).Add(key, values[j]); diff --git a/locale.csv b/locale.csv index 718e29d..9d797c6 100644 --- a/locale.csv +++ b/locale.csv @@ -1,66 +1,72 @@ Key,Description,English,Bulgarian,Chinese (Simplified),Chinese (Traditional),Czech,Danish,Dutch,Finnish,French,German,Hindi,Hungarian,Italian,Japanese,Korean,Norwegian,Polish,Portuguese (Brazil),Portuguese,Romanian,Russian,Slovak,Spanish,Swedish,Turkish,Ukrainian -,,,,,,,,,,,,,,,,,,,,,,,,,,, -,"Do not translate ‘{x}’ with x being a number, or ‘\n’.",,,,,,,,,,,,,,,,,,,,,,,,,, -,"If a translation has a comma, the entire line MUST be wrapped in double quotes! Most editors (Excel, LibreCalc) will do this for you.",,,,,,,,,,,,,,,,,,,,,,,,,, -,"When saving the file, ensure to save it using UTF-8 encoding!",,,,,,,,,,,,,,,,,,,,,,,,,, -,,,,,,,,,,,,,,,,,,,,,,,,,,, -,Main Menu,,,,,,,,,,,,,,,,,,,,,,,,,, -mm/join_server,The 'Join Server' button in the main menu.,Join Server,Присъединете се към сървъра ,加入服务器 ,加入伺服器 ,Připojte se k serveru ,Tilmeld dig server,Kom bij de server,Liity palvelimelle,Rejoindre le serveur,Spiel beitreten,सर्वर में शामिल हों,Csatlakozz a szerverhez,Entra in un Server,サーバーに参加する,서버에 가입,Bli med server,Dołącz do serwera,Conectar-se ao servidor ,Ligar-se ao servidor ,Alăturați-vă serverului,Присоединиться к серверу,Pripojte sa k serveru,Unirse a un servidor,Gå med i servern,Sunucuya katıl,Приєднатися до сервера -mm/join_server__tooltip,The tooltip shown when hovering over the 'Join Server' button.,Join a multiplayer session.,Присъединете се към мултиплейър сесия. ,加入多人游戏会话。 ,加入多人遊戲會話。 ,Připojte se k relaci pro více hráčů. ,Deltag i en multiplayer session.,Neem deel aan een multiplayersessie.,Liity moninpeliistuntoon.,Rejoindre une session multijoueur,Trete einer Mehrspielersitzung bei.,मल्टीप्लेयर सत्र में शामिल हों.,Csatlakozz egy többjátékos munkamenethez.,Entra in una sessione multiplayer.,マルチプレイヤー セッションに参加します。,멀티플레이어 세션에 참여하세요.,Bli med på en flerspillerøkt.,Dołącz do sesji wieloosobowej.,Participe de uma sessão multijogador. ,Participe numa sessão multijogador. ,Alăturați-vă unei sesiuni multiplayer.,Присоединяйтесь к многопользовательской сессии.,Pripojte sa k relácii pre viacerých hráčov.,Únete a una sesión multijugador.,Gå med i en multiplayer-session.,Çok oyunculu bir oturuma katılın.,Приєднуйтеся до багатокористувацької сесії. -mm/join_server__tooltip_disabled,Unused,,,,,,,,,,,,,,,,,,,,,,,,,, -,,,,,,,,,,,,,,,,,,,,,,,,,,, -,Server Browser,,,,,,,,,,,,,,,,,,,,,,,,,, -sb/title,The title of the Server Browser tab,Server Browser,Браузър на сървъра ,服务器浏览器 ,伺服器瀏覽器 ,Serverový prohlížeč ,Server browser,Server browser,Palvelimen selain,Navigateur de serveurs,Server-Browser ,सर्वर ब्राउजर,Szerverböngésző,Ricerca Server,サーバーブラウザ,서버 브라우저,Servernettleser,Przeglądarka serwerów,Navegador do servidor ,Navegador do servidor ,Browser server,Браузер серверов,Serverový prehliadač,Buscar servidores,Serverbläddrare,Sunucu tarayıcısı,Браузер сервера -sb/manual_connect,Connect to IP,Connect to IP,Свържете се с IP ,连接到IP ,連接到IP ,Připojte se k IP ,Opret forbindelse til IP,Maak verbinding met IP,Yhdistä IP-osoitteeseen,Connectez-vous à IP,Mit IP verbinden,आईपी ​​से कनेक्ट करें,Csatlakozzon az IP-hez,Connettiti all'IP,IPに接続する,IP에 연결,Koble til IP,Połącz się z IP,Conecte-se ao IP ,Ligue-se ao IP ,Conectați-vă la IP,Подключиться к IP,Pripojte sa k IP,Conéctese a IP,Anslut till IP,IP'ye bağlan,Підключитися до IP -sb/manual_connect__tooltip,The tooltip shown when hovering over the 'manualconnect' button.,Direct connection to a multiplayer session.,Директна връзка към мултиплейър сесия. ,直接连接到多人游戏会话。 ,直接連接到多人遊戲會話。 ,Přímé připojení k relaci pro více hráčů. ,Direkte forbindelse til en multiplayer-session.,Directe verbinding met een multiplayersessie.,Suora yhteys moninpeliistuntoon.,Connexion directe à une session multijoueur.,Direkte Verbindung zu einer Multiplayer-Sitzung.,मल्टीप्लेयर सत्र से सीधा कनेक्शन।,Közvetlen kapcsolat egy többjátékos munkamenethez.,Connessione diretta a una sessione multiplayer.,マルチプレイヤー セッションへの直接接続。,멀티플레이어 세션에 직접 연결됩니다.,Direkte tilkobling til en flerspillerøkt.,Bezpośrednie połączenie z sesją wieloosobową.,Conexão direta a uma sessão multijogador. ,Ligação direta a uma sessão multijogador. ,Conexiune directă la o sesiune multiplayer.,Прямое подключение к многопользовательской сессии.,Priame pripojenie k relácii pre viacerých hráčov.,Conexión directa a una sesión multijugador.,Direktanslutning till en multiplayer-session.,Çok oyunculu bir oturuma doğrudan bağlantı.,Пряме підключення до багатокористувацької сесії. -sb/manual_connect__tooltip_disabled,Unused,,,,,,,,,,,,,,,,,,,,,,,,,, -sb/host,Host Game,Host Game,Домакин на играта ,主机游戏 ,主機遊戲 ,Hostitelská hra ,Værtsspil,Gastheerspel,Isäntäpeli,Partie hôte,Gastspiel,मेज़बान खेल,Gazdajáték,Ospita il gioco,ホストゲーム,호스트 게임,Vertsspill,Gra gospodarza,Jogo anfitrião ,Jogo anfitrião ,Găzduire joc,Хост-игра,Hostiteľská hra,Juego de acogida,Värdspel,Sunucu Oyunu,Ведуча гра -sb/host__tooltip,The tooltip shown when hovering over the 'Host Server' button.,Host a multiplayer session.,Организирайте сесия за мултиплейър. ,主持多人游戏会话。 ,主持多人遊戲會話。 ,Uspořádejte relaci pro více hráčů. ,Vær vært for en multiplayer-session.,Organiseer een multiplayersessie.,Järjestä moninpeliistunto.,Organisez une session multijoueur.,Veranstalten Sie eine Multiplayer-Sitzung.,एक मल्टीप्लेयर सत्र की मेजबानी करें.,Hozz létre egy többjátékos munkamenetet.,Ospita una sessione multigiocatore.,マルチプレイヤー セッションをホストします。,멀티플레이어 세션을 호스팅하세요.,Vær vert for en flerspillerøkt.,Zorganizuj sesję wieloosobową.,Hospede uma sessão multijogador. ,Acolhe uma sessão multijogador. ,Găzduiește o sesiune multiplayer.,Организуйте многопользовательский сеанс.,Usporiadajte reláciu pre viacerých hráčov.,Organiza una sesión multijugador.,Var värd för en session för flera spelare.,Çok oyunculu bir oturuma ev sahipliği yapın.,Проведіть сеанс для кількох гравців. -sb/host__tooltip_disabled,Unused,,,,,,,,,,,,,,,,,,,,,,,,,, -sb/join_game,Join Game,Join Game,Присъединете се към играта ,加入游戏 ,加入遊戲 ,Připojte se ke hře,Deltag i spil,Speel mee,Liity peliin,Rejoins une partie,Spiel beitreten,खेल में शामिल हो,Belépni a játékba,Unisciti al gioco,ゲームに参加します,게임 참여,Bli med i spillet,Dołącz do gry,Entrar no jogo ,Entrar no jogo ,Alatura-te jocului,Присоединиться к игре,Pridať sa do hry,Unete al juego,Gå med i spel,Oyuna katılmak,Приєднуйся до гри -sb/join_game__tooltip,The tooltip shown when hovering over the 'Join Server' button.,Join a multiplayer session.,Присъединете се към мултиплейър сесия. ,加入多人游戏会话。 ,加入多人遊戲會話。 ,Připojte se k relaci pro více hráčů. ,Deltag i en multiplayer session.,Neem deel aan een multiplayersessie.,Liity moninpeliistuntoon.,Rejoignez une session multijoueur.,Nehmen Sie an einer Multiplayer-Sitzung teil.,मल्टीप्लेयर सत्र में शामिल हों.,Csatlakozz egy többjátékos munkamenethez.,Partecipa a una sessione multigiocatore.,マルチプレイヤー セッションに参加します。,멀티플레이어 세션에 참여하세요.,Bli med på en flerspillerøkt.,Dołącz do sesji wieloosobowej.,Participe de uma sessão multijogador.,Participe numa sessão multijogador. ,Alăturați-vă unei sesiuni multiplayer.,Присоединяйтесь к многопользовательской сессии.,Pripojte sa k relácii pre viacerých hráčov.,Únete a una sesión multijugador.,Gå med i en multiplayer-session.,Çok oyunculu bir oturuma katılın.,Приєднуйтеся до багатокористувацької сесії. -sb/join_game__tooltip_disabled,The tooltip shown when hovering over the 'Join Server' button.,Select a game to join.,,,,,,,,,,,,,,,,,,,,,,,, -sb/refresh,refresh,Refresh,Опресняване ,刷新 ,重新整理 ,Obnovit ,Opdater,Vernieuwen,virkistää,Rafraîchir,Aktualisierung,ताज़ा करना,Frissítés,ricaricare,リフレッシュ,새로 고치다,Forfriske,Odświeżać,Atualizar ,Atualizar ,Reîmprospăta,Обновить,Obnoviť,Actualizar,Uppdatera,Yenile,Оновити -sb/refresh__tooltip,The tooltip shown when hovering over the 'Refresh Server' button.,Refresh server list.,Обновяване на списъка със сървъри. ,刷新服务器列表。 ,刷新伺服器清單。 ,Obnovit seznam serverů. ,Opdater serverliste.,Vernieuw de serverlijst.,Päivitä palvelinluettelo.,Actualiser la liste des serveurs.,Serverliste aktualisieren.,सर्वर सूची ताज़ा करें.,Szerverlista frissítése.,Aggiorna l'elenco dei server.,サーバーリストを更新します。,서버 목록을 새로 고칩니다.,Oppdater serverlisten.,Odśwież listę serwerów.,Atualizar lista de servidores.,Atualizar lista de servidores. ,Actualizează lista de servere.,Обновить список серверов.,Obnoviť zoznam serverov.,Actualizar la lista de servidores.,Uppdatera serverlistan.,Sunucu listesini yenileyin.,Оновити список серверів. -sb/refresh__tooltip_disabled,Unused,"Refreshing, please wait...",,,,,,,,,,,,,,,,,,,,,,,,, -sb/ip,IP popup,Enter IP Address,Въведете IP адрес,输入IP地址 ,輸入IP位址 ,Zadejte IP adresu ,Indtast IP-adresse,Voer het IP-adres in,Anna IP-osoite,Entrer l’adresse IP,IP Adresse eingeben,आईपी ​​पता दर्ज करें,Írja be az IP-címet,Inserire Indirizzo IP,IPアドレスを入力してください,IP 주소를 입력하세요,Skriv inn IP-adresse,Wprowadź adres IP,Digite o endereço IP ,Introduza o endereço IP ,Introduceți adresa IP,Введите IP-адрес,Zadajte IP adresu,Ingrese la dirección IP,Ange IP-adress,IP Adresini Girin,Введіть IP-адресу -sb/ip_invalid,Invalid IP popup.,Invalid IP Address!,Невалиден IP адрес! ,IP 地址无效! ,IP 位址無效! ,Neplatná IP adresa! ,Ugyldig IP-adresse!,Ongeldig IP-adres!,Virheellinen IP-osoite!,Adresse IP invalide,Ungültige IP Adresse!,अमान्य आईपी पता!,Érvénytelen IP-cím!,Indirizzo IP Invalido!,IP アドレスが無効です!,IP 주소가 잘못되었습니다!,Ugyldig IP-adresse!,Nieprawidłowy adres IP!,Endereço IP inválido! ,Endereço IP inválido! ,Adresă IP nevalidă!,Неверный IP-адрес!,Neplatná IP adresa!,¡Dirección IP inválida!,Ogiltig IP-adress!,Geçersiz IP adresi!,Недійсна IP-адреса! -sb/port,Port popup.,Enter Port (7777 by default),Въведете порт (7777 по подразбиране) ,输入端口(默认为 7777) ,輸入連接埠(預設為 7777) ,Zadejte port (ve výchozím nastavení 7777),Indtast port (7777 som standard),Poort invoeren (standaard 7777),Anna portti (oletuksena 7777),Entrer le port (7777 par défaut),Port eingeben (Standard: 7777),पोर्ट दर्ज करें (डिफ़ॉल्ट रूप से 7777),Írja be a portot (alapértelmezés szerint 7777),Inserire Porta (7777 di default),ポートを入力します (デフォルトでは 7777),포트 입력(기본적으로 7777),Angi port (7777 som standard),Wprowadź port (domyślnie 7777),Insira a porta (7777 por padrão) ,Introduza a porta (7777 por defeito) ,Introduceți port (7777 implicit),Введите порт (7777 по умолчанию),Zadajte port (predvolene 7777),Introduzca el número de puerto(7777 por defecto),Ange port (7777 som standard),Bağlantı Noktasını Girin (varsayılan olarak 7777),Введіть порт (7777 за замовчуванням) -sb/port_invalid,Invalid port popup.,Invalid Port!,Невалиден порт! ,端口无效! ,埠無效! ,Neplatný port!,Ugyldig port!,Ongeldige poort!,Virheellinen portti!,Port invalide !,Ungültiger Port!,अमान्य पोर्ट!,Érvénytelen port!,Porta Invalida!,ポートが無効です!,포트가 잘못되었습니다!,Ugyldig port!,Nieprawidłowy port!,Porta inválida! ,Porta inválida! ,Port nevalid!,Неверный порт!,Neplatný port!,¡Número de Puerto no válido!,Ogiltig port!,Geçersiz Bağlantı Noktası!,Недійсний порт! -sb/password,Password popup.,Enter Password,Въведете паролата,输入密码 ,輸入密碼 ,Zadejte heslo ,Indtast adgangskode,Voer wachtwoord in,Kirjoita salasana,Entrer le mot de passe,Passwort eingeben,पास वर्ड दर्ज करें,Írd be a jelszót,Inserire Password,パスワードを入力する,암호를 입력,Oppgi passord,Wprowadź hasło,Digite a senha ,Introduza a senha ,Introdu parola,Введите пароль,Zadajte heslo,Introducir la contraseña,Skriv in lösenord,Parolanı Gir,Введіть пароль +,,,,,,,,,,,,,,,,,,,,,,,,,,,, +,"Do not translate ‘{x}’ with x being a number, or ‘\n’.",,,,,,,,,,,,,,,,,,,,,,,,,,, +,"If a translation has a comma, the entire line MUST be wrapped in double quotes! Most editors (Excel, LibreCalc) will do this for you.",,,,,,,,,,,,,,,,,,,,,,,,,,, +,"When saving the file, ensure to save it using UTF-8 encoding!",,,,,,,,,,,,,,,,,,,,,,,,,,, +,,,,,,,,,,,,,,,,,,,,,,,,,,,, +,Main Menu,,,,,,,,,,,,,,,,,,,,,,,,,,, +mm/join_server__tooltip,The tooltip shown when hovering over the 'Join Server' button.,Join a multiplayer session.,Присъединете се към мултиплейър сесия.,加入多人游戏会话。,加入多人遊戲會話。,Připojte se k relaci pro více hráčů.,Deltag i en multiplayer session.,Neem deel aan een multiplayersessie.,Liity moninpeliistuntoon.,Rejoindre une session multijoueur,Trete einer Mehrspielersitzung bei.,मल्टीप्लेयर सत्र में शामिल हों.,Csatlakozz egy többjátékos munkamenethez.,Entra in una sessione multiplayer.,マルチプレイヤー セッションに参加します。,멀티플레이어 세션에 참여하세요.,Bli med på en flerspillerøkt.,Dołącz do sesji wieloosobowej.,Participe de uma sessão multijogador.,Participe numa sessão multijogador.,Alăturați-vă unei sesiuni multiplayer.,Присоединяйтесь к многопользовательской сессии.,Pripojte sa k relácii pre viacerých hráčov.,Únete a una sesión multijugador.,Gå med i en multiplayer-session.,Çok oyunculu bir oturuma katılın.,Приєднуйтеся до багатокористувацької сесії. +mm/join_server__tooltip_disabled,Unused,,,,,,,,,,,,,,,,,,,,,,,,,,, +,,,,,,,,,,,,,,,,,,,,,,,,,,,, +,Server Browser,,,,,,,,,,,,,,,,,,,,,,,,,,, +sb/title,The title of the Server Browser tab,Server Browser,Браузър на сървъра,服务器浏览器,伺服器瀏覽器,Serverový prohlížeč,Server browser,Server browser,Palvelimen selain,Navigateur de serveurs,Server-Browser,सर्वर ब्राउजर,Szerverböngésző,Ricerca Server,サーバーブラウザ,서버 브라우저,Servernettleser,Przeglądarka serwerów,Navegador do servidor,Navegador do servidor,Browser server,Браузер серверов,Serverový prehliadač,Buscar servidores,Serverbläddrare,Sunucu tarayıcısı,Браузер сервера +sb/manual_connect,Connect to IP,Connect to IP,Свържете се с IP,连接到IP,連接到IP,Připojte se k IP,Opret forbindelse til IP,Maak verbinding met IP,Yhdistä IP-osoitteeseen,Connectez-vous à IP,Mit IP verbinden,आईपी ​​से कनेक्ट करें,Csatlakozzon az IP-hez,Connettiti all'IP,IPに接続する,IP에 연결,Koble til IP,Połącz się z IP,Conecte-se ao IP,Ligue-se ao IP,Conectați-vă la IP,Подключиться к IP,Pripojte sa k IP,Conéctese a IP,Anslut till IP,IP'ye bağlan,Підключитися до IP +sb/manual_connect__tooltip,The tooltip shown when hovering over the 'manualconnect' button.,Direct connection to a multiplayer session.,Директна връзка към мултиплейър сесия.,直接连接到多人游戏会话。,直接連接到多人遊戲會話。,Přímé připojení k relaci pro více hráčů.,Direkte forbindelse til en multiplayer-session.,Directe verbinding met een multiplayersessie.,Suora yhteys moninpeliistuntoon.,Connexion directe à une session multijoueur.,Direkte Verbindung zu einer Multiplayer-Sitzung.,मल्टीप्लेयर सत्र से सीधा कनेक्शन।,Közvetlen kapcsolat egy többjátékos munkamenethez.,Connessione diretta a una sessione multiplayer.,マルチプレイヤー セッションへの直接接続。,멀티플레이어 세션에 직접 연결됩니다.,Direkte tilkobling til en flerspillerøkt.,Bezpośrednie połączenie z sesją wieloosobową.,Conexão direta a uma sessão multijogador.,Ligação direta a uma sessão multijogador.,Conexiune directă la o sesiune multiplayer.,Прямое подключение к многопользовательской сессии.,Priame pripojenie k relácii pre viacerých hráčov.,Conexión directa a una sesión multijugador.,Direktanslutning till en multiplayer-session.,Çok oyunculu bir oturuma doğrudan bağlantı.,Пряме підключення до багатокористувацької сесії. +sb/manual_connect__tooltip_disabled,Unused,,,,,,,,,,,,,,,,,,,,,,,,,,, +sb/host,Host Game,Host Game,Домакин на играта,主机游戏,主機遊戲,Hostitelská hra,Værtsspil,Gastheerspel,Isäntäpeli,Partie hôte,Gastspiel,मेज़बान खेल,Gazdajáték,Ospita il gioco,ホストゲーム,호스트 게임,Vertsspill,Gra gospodarza,Jogo anfitrião,Jogo anfitrião,Găzduire joc,Хост-игра,Hostiteľská hra,Juego de acogida,Värdspel,Sunucu Oyunu,Ведуча гра +sb/host__tooltip,The tooltip shown when hovering over the 'Host Server' button.,Host a multiplayer session.,Организирайте сесия за мултиплейър.,主持多人游戏会话。,主持多人遊戲會話。,Uspořádejte relaci pro více hráčů.,Vær vært for en multiplayer-session.,Organiseer een multiplayersessie.,Järjestä moninpeliistunto.,Organisez une session multijoueur.,Veranstalten Sie eine Multiplayer-Sitzung.,एक मल्टीप्लेयर सत्र की मेजबानी करें.,Hozz létre egy többjátékos munkamenetet.,Ospita una sessione multigiocatore.,マルチプレイヤー セッションをホストします。,멀티플레이어 세션을 호스팅하세요.,Vær vert for en flerspillerøkt.,Zorganizuj sesję wieloosobową.,Hospede uma sessão multijogador.,Acolhe uma sessão multijogador.,Găzduiește o sesiune multiplayer.,Организуйте многопользовательский сеанс.,Usporiadajte reláciu pre viacerých hráčov.,Organiza una sesión multijugador.,Var värd för en session för flera spelare.,Çok oyunculu bir oturuma ev sahipliği yapın.,Проведіть сеанс для кількох гравців. +sb/host__tooltip_disabled,Unused,,,,,,,,,,,,,,,,,,,,,,,,,,, +sb/join_game,Join Game,Join Game,Присъединете се към играта,加入游戏,加入遊戲,Připojte se ke hře,Deltag i spil,Speel mee,Liity peliin,Rejoins une partie,Spiel beitreten,खेल में शामिल हो,Belépni a játékba,Unisciti al gioco,ゲームに参加します,게임 참여,Bli med i spillet,Dołącz do gry,Entrar no jogo,Entrar no jogo,Alatura-te jocului,Присоединиться к игре,Pridať sa do hry,Unete al juego,Gå med i spel,Oyuna katılmak,Приєднуйся до гри +sb/join_game__tooltip,The tooltip shown when hovering over the 'Join Server' button.,Join a multiplayer session.,Присъединете се към мултиплейър сесия.,加入多人游戏会话。,加入多人遊戲會話。,Připojte se k relaci pro více hráčů.,Deltag i en multiplayer session.,Neem deel aan een multiplayersessie.,Liity moninpeliistuntoon.,Rejoignez une session multijoueur.,Nehmen Sie an einer Multiplayer-Sitzung teil.,मल्टीप्लेयर सत्र में शामिल हों.,Csatlakozz egy többjátékos munkamenethez.,Partecipa a una sessione multigiocatore.,マルチプレイヤー セッションに参加します。,멀티플레이어 세션에 참여하세요.,Bli med på en flerspillerøkt.,Dołącz do sesji wieloosobowej.,Participe de uma sessão multijogador.,Participe numa sessão multijogador.,Alăturați-vă unei sesiuni multiplayer.,Присоединяйтесь к многопользовательской сессии.,Pripojte sa k relácii pre viacerých hráčov.,Únete a una sesión multijugador.,Gå med i en multiplayer-session.,Çok oyunculu bir oturuma katılın.,Приєднуйтеся до багатокористувацької сесії. +sb/join_game__tooltip_disabled,The tooltip shown when hovering over the 'Join Server' button.,Select a game to join.,Изберете игра за присъединяване,选择要加入的游戏,選擇要加入的遊戲,Vyberte si hru pro připojení,Vælg et spil at deltage i,Kies een spel om deel te nemen,Valitse peli liittyäksesi,Sélectionnez une partie à rejoindre,Wählen Sie ein Spiel zum Beitritt,खेल में शामिल होने के लिए चुनें,Válasszon egy játékot a csatlakozáshoz,Seleziona un gioco da unirti,参加するゲームを選択,게임을 선택하십시오,Velg et spill å bli med på,"Wybierz grę, aby dołączyć",Selecione um jogo para entrar,Selecione um jogo para participar,Alegeți un joc pentru a vă alătura,Выберите игру для присоединения,Vyberte si hru,Seleccione un juego para unirse,Välj ett spel att gå med,Katılmak için bir oyun seçin,Виберіть гру для приєднання +sb/refresh,refresh,Refresh,Опресняване,刷新,重新整理,Obnovit,Opdater,Vernieuwen,virkistää,Rafraîchir,Aktualisierung,ताज़ा करना,Frissítés,ricaricare,リフレッシュ,새로 고치다,Forfriske,Odświeżać,Atualizar,Atualizar,Reîmprospăta,Обновить,Obnoviť,Actualizar,Uppdatera,Yenile,Оновити +sb/refresh__tooltip,The tooltip shown when hovering over the 'Refresh Server' button.,Refresh server list.,Обновяване на списъка със сървъри.,刷新服务器列表。,刷新伺服器清單。,Obnovit seznam serverů.,Opdater serverliste.,Vernieuw de serverlijst.,Päivitä palvelinluettelo.,Actualiser la liste des serveurs.,Serverliste aktualisieren.,सर्वर सूची ताज़ा करें.,Szerverlista frissítése.,Aggiorna l'elenco dei server.,サーバーリストを更新します。,서버 목록을 새로 고칩니다.,Oppdater serverlisten.,Odśwież listę serwerów.,Atualizar lista de servidores.,Atualizar lista de servidores.,Actualizează lista de servere.,Обновить список серверов.,Obnoviť zoznam serverov.,Actualizar la lista de servidores.,Uppdatera serverlistan.,Sunucu listesini yenileyin.,Оновити список серверів. +sb/refresh__tooltip_disabled,Tooltip for refresh button while refreshing,"Refreshing, please wait...","Опресняване, моля, изчакайте...","正在刷新,请稍候...","正在刷新,請稍候...","Obnovuje se, prosím, počkejte...","Opdaterer, vent venligst...","Vernieuwen, een ogenblik geduld...","Päivitetään, odota hetki...","Actualisation en cours, veuillez patienter...","Aktualisierung läuft, bitte warten...","ताज़ा कर रहा है, कृपया प्रतीक्षा करें...","Frissítés, kérjük, várjon...","Aggiornamento in corso, attendere prego...","リフレッシュ中、お待ちください...","새로고침 중, 잠시만 기다려 주세요...","Oppdaterer, vennligst vent...","Odświeżanie, proszę czekać...","Atualizando, por favor, aguarde...","Atualizando, por favor, aguarde...","Se actualizează, vă rugăm să așteptați...","Обновление, подождите...","Obnovuje sa, čakajte...","Actualizando, por favor, espere...","Uppdaterar, vänligen vänta...","Güncelleniyor, lütfen bekleyin...","Оновлення, будь ласка, зачекайте..." +sb/ip,IP popup,Enter IP Address,Въведете IP адрес,输入IP地址,輸入IP位址,Zadejte IP adresu,Indtast IP-adresse,Voer het IP-adres in,Anna IP-osoite,Entrer l’adresse IP,IP Adresse eingeben,आईपी ​​पता दर्ज करें,Írja be az IP-címet,Inserire Indirizzo IP,IPアドレスを入力してください,IP 주소를 입력하세요,Skriv inn IP-adresse,Wprowadź adres IP,Digite o endereço IP,Introduza o endereço IP,Introduceți adresa IP,Введите IP-адрес,Zadajte IP adresu,Ingrese la dirección IP,Ange IP-adress,IP Adresini Girin,Введіть IP-адресу +sb/ip_invalid,Invalid IP popup.,Invalid IP Address!,Невалиден IP адрес!,IP 地址无效!,IP 位址無效!,Neplatná IP adresa!,Ugyldig IP-adresse!,Ongeldig IP-adres!,Virheellinen IP-osoite!,Adresse IP invalide,Ungültige IP Adresse!,अमान्य आईपी पता!,Érvénytelen IP-cím!,Indirizzo IP Invalido!,IP アドレスが無効です!,IP 주소가 잘못되었습니다!,Ugyldig IP-adresse!,Nieprawidłowy adres IP!,Endereço IP inválido!,Endereço IP inválido!,Adresă IP nevalidă!,Неверный IP-адрес!,Neplatná IP adresa!,¡Dirección IP inválida!,Ogiltig IP-adress!,Geçersiz IP adresi!,Недійсна IP-адреса! +sb/port,Port popup.,Enter Port (7777 by default),Въведете порт (7777 по подразбиране),输入端口(默认为 7777),輸入連接埠(預設為 7777),Zadejte port (ve výchozím nastavení 7777),Indtast port (7777 som standard),Poort invoeren (standaard 7777),Anna portti (oletuksena 7777),Entrer le port (7777 par défaut),Port eingeben (Standard: 7777),पोर्ट दर्ज करें (डिफ़ॉल्ट रूप से 7777),Írja be a portot (alapértelmezés szerint 7777),Inserire Porta (7777 di default),ポートを入力します (デフォルトでは 7777),포트 입력(기본적으로 7777),Angi port (7777 som standard),Wprowadź port (domyślnie 7777),Insira a porta (7777 por padrão),Introduza a porta (7777 por defeito),Introduceți port (7777 implicit),Введите порт (7777 по умолчанию),Zadajte port (predvolene 7777),Introduzca el número de puerto(7777 por defecto),Ange port (7777 som standard),Bağlantı Noktasını Girin (varsayılan olarak 7777),Введіть порт (7777 за замовчуванням) +sb/port_invalid,Invalid port popup.,Invalid Port!,Невалиден порт!,端口无效!,埠無效!,Neplatný port!,Ugyldig port!,Ongeldige poort!,Virheellinen portti!,Port invalide !,Ungültiger Port!,अमान्य पोर्ट!,Érvénytelen port!,Porta Invalida!,ポートが無効です!,포트가 잘못되었습니다!,Ugyldig port!,Nieprawidłowy port!,Porta inválida!,Porta inválida!,Port nevalid!,Неверный порт!,Neplatný port!,¡Número de Puerto no válido!,Ogiltig port!,Geçersiz Bağlantı Noktası!,Недійсний порт! +sb/password,Password popup.,Enter Password,Въведете паролата,输入密码,輸入密碼,Zadejte heslo,Indtast adgangskode,Voer wachtwoord in,Kirjoita salasana,Entrer le mot de passe,Passwort eingeben,पास वर्ड दर्ज करें,Írd be a jelszót,Inserire Password,パスワードを入力する,암호를 입력,Oppgi passord,Wprowadź hasło,Digite a senha,Introduza a senha,Introdu parola,Введите пароль,Zadajte heslo,Introducir la contraseña,Skriv in lösenord,Parolanı Gir,Введіть пароль +sb/players,Player count in details text,Players,Играчите,玩家,玩家,Hráči,Spillere,Spelers,Pelaajat,Joueurs,Spieler,खिलाड़ी,Hráči,Giocatori,プレイヤー,플레이어,Spillere,Gracze,Jogadores,Jogadores,Jucători,Игроки,Hráči,Jugadores,Spelare,Oyuncular,Гравці +sb/password_required,Password required in details text,Password,Парола,密码,密碼,Heslo,Adgangskode,Wachtwoord,Salasana,Mot de passe,Passwort,पासवर्ड,Heslo,Password,パスワード,비밀번호,Passord,Hasło,Senha,Senha,Parola,Пароль,Heslo,Contraseña,Lösenord,Parola,Пароль +sb/mods_required,Mods required in details text,Requires mods,Изисква модове,需要模组,需要模組,Požaduje módy,Kræver mods,Vereist mods,Vaatii modit,Nécessite des mods,Benötigt Mods,मॉड की आवश्यकता है,Modokat igényel,Richiede mod,モッズが必要,모드 필요,Krever modifikasjoner,Wymaga modyfikacji,Requer mods,Requer mods,Necesită moduri,Требуются модификации,Požaduje módy,Requiere mods,Kräver moddar,Mod gerektirir,Потрібні модифікації +sb/game_version,Game version in details text,Game version,Версия на играта,游戏版本,遊戲版本,Verze hry,Spilversion,Spelversie,Pelin versio,Version du jeu,Spielversion,गेम संस्करण,Verze hry,Versione del gioco,ゲームバージョン,게임 버전,Spillversjon,Wersja gry,Versão do jogo,Versão do jogo,Versiunea jocului,Версия игры,Verzia hry,Versión del juego,Spelversion,Oyun versiyonu,Версія гри +sb/mod_version,Multiplayer version in details text,Multiplayer version,Мултиплейър версия,多人游戏版本,多人遊戲版本,Multiplayer verze,Multiplayer version,Multiplayer versie,Moninpeliversio,Version multijoueur,Multiplayer-Version,मल्टीप्लेयर संस्करण,Multiplayer verze,Versione multiplayer,マルチプレイヤーバージョン,멀티플레이어 버전,Multiplayer versjon,Wersja multiplayer,Versão multiplayer,Versão multiplayer,Versiunea multiplayer,Мультиплеерная версия,Multiplayer verzia,Versión multijugador,Multiplayer-version,Çok oyunculu sürüm,Багатокористувацька версія +sb/yes,Response 'yes' for details text,Yes,Да,是,是,Ano,Ja,Ja,Kyllä,Oui,Ja,हां,Ano,Sì,はい,네,Ja,Tak,Sim,Sim,Da,Да,Áno,Sí,Ja,Evet,Так +sb/no,Response 'no' for details text,No,Не,否,否,Ne,Nej,Nee,Ei,Non,Nein,नहीं,Ne,No,いいえ,아니요,Nei,Nie,Não,Não,Nu,Нет,Nie,Nie,Nej,Hayır,Ні ,,,,,,,,,,,,,,,,,,,,,,,,,,, ,Server Host,,,,,,,,,,,,,,,,,,,,,,,,,, -host/title,The title of the Host Game page,Host Game,Домакин на играта ,主机游戏 ,主機遊戲 ,Hostitelská hra,Værtsspil,Gastheerspel,Isäntäpeli,Partie hôte,Gastspiel,मेज़बान खेल,Gazdajáték,Ospita il gioco,ホストゲーム,호스트 게임,Vertsspill,Gra gospodarza,Jogo anfitrião ,Jogo anfitrião ,Găzduire joc,Хост-игра,Hostiteľská hra,Juego de acogida,Värdspel,Sunucu Oyunu,Ведуча гра -host/name,Server name field placeholder,Server Name,Име на сървъра ,服务器名称 ,伺服器名稱 ,Název serveru,Server navn,Server naam,Palvelimen nimi,Nom du serveur,Servername,सर्वर का नाम,Szerver név,Nome del server,サーバーの名前,서버 이름,Server navn,Nazwa serwera,Nome do servidor ,Nome do servidor ,Numele serverului,Имя сервера,Názov servera,Nombre del servidor,Server namn,Sunucu adı,Ім'я сервера -host/name__tooltip,Server name field tooltip,The name of the server that other players will see in the server browser,"Името на сървъра, което другите играчи ще видят в сървърния браузър ",其他玩家在服务器浏览器中看到的服务器名称 ,其他玩家在伺服器瀏覽器中看到的伺服器名稱 ,"Název serveru, který ostatní hráči uvidí v prohlížeči serveru","Navnet på den server, som andre spillere vil se i serverbrowseren ",De naam van de server die andere spelers in de serverbrowser zien,"Palvelimen nimi, jonka muut pelaajat näkevät palvelimen selaimessa",Le nom du serveur que les autres joueurs verront dans le navigateur du serveur,"Der Name des Servers, den andere Spieler im Serverbrowser sehen",सर्वर का नाम जो अन्य खिलाड़ी सर्वर ब्राउज़र में देखेंगे,"A szerver neve, amelyet a többi játékos látni fog a szerver böngészőjében",Il nome del server che gli altri giocatori vedranno nel browser del server,他のプレイヤーがサーバー ブラウザに表示するサーバーの名前,다른 플레이어가 서버 브라우저에서 볼 수 있는 서버 이름,Navnet på serveren som andre spillere vil se i servernettleseren,"Nazwa serwera, którą inni gracze zobaczą w przeglądarce serwerów",O nome do servidor que outros jogadores verão no navegador do servidor ,O nome do servidor que os outros jogadores verão no navegador do servidor ,The name of the server that other players will see in the server browser,"Имя сервера, которое другие игроки увидят в браузере серверов.","Názov servera, ktorý ostatní hráči uvidia v prehliadači servera",El nombre del servidor que otros jugadores verán en el navegador del servidor.,Namnet på servern som andra spelare kommer att se i serverwebbläsaren,Diğer oyuncuların sunucu tarayıcısında göreceği sunucunun adı,"Назва сервера, яку інші гравці бачитимуть у браузері сервера" -host/password,Password field placeholder,Password (leave blank for no password),Парола (оставете празно за липса на парола) ,密码(无密码则留空) ,密碼(無密碼則留空) ,"Heslo (nechte prázdné, pokud nechcete heslo)",Adgangskode (lad tom for ingen adgangskode) ,Wachtwoord (leeg laten als er geen wachtwoord is),"Salasana (jätä tyhjäksi, jos et salasanaa)",Mot de passe (laisser vide s'il n'y a pas de mot de passe),"Passwort (leer lassen, wenn kein Passwort vorhanden ist)",पासवर्ड (बिना पासवर्ड के खाली छोड़ें),Jelszó (jelszó nélkül hagyja üresen),Password (lascia vuoto per nessuna password),パスワード (パスワードを使用しない場合は空白のままにします),비밀번호(비밀번호가 없으면 비워두세요),Passord (la det stå tomt for ingen passord),"Hasło (pozostaw puste, jeśli nie ma hasła)",Senha (deixe em branco se não houver senha) ,"Palavra-passe (deixe em branco se não existir palavra-passe)",Parola (lasa necompletat pentru nicio parola),"Пароль (оставьте пустым, если пароль отсутствует)","Heslo (nechávajte prázdne, ak nechcete zadať heslo)",Contraseña (dejar en blanco si no hay contraseña),Lösenord (lämna tomt för inget lösenord),Şifre (Şifre yoksa boş bırakın),"Пароль (залиште порожнім, якщо немає пароля)" -host/password__tooltip,Password field placeholder,Password for joining the game. Leave blank if no password is required,"Парола за присъединяване към играта. Оставете празно, ако не се изисква парола ",加入游戏的密码。如果不需要密码则留空 ,加入遊戲的密碼。如果不需要密碼則留空 ,"Heslo pro vstup do hry. Pokud není vyžadováno heslo, ponechte prázdné","Adgangskode for at deltage i spillet. Lad stå tomt, hvis der ikke kræves adgangskode ",Wachtwoord voor deelname aan het spel. Laat dit leeg als er geen wachtwoord vereist is,"Salasana peliin liittymiseen. Jätä tyhjäksi, jos salasanaa ei vaadita",Mot de passe pour rejoindre le jeu. Laisser vide si aucun mot de passe n'est requis,"Passwort für die Teilnahme am Spiel. Lassen Sie das Feld leer, wenn kein Passwort erforderlich ist",गेम में शामिल होने के लिए पासवर्ड. यदि पासवर्ड की आवश्यकता नहीं है तो खाली छोड़ दें,"Jelszó a játékhoz való csatlakozáshoz. Ha nincs szükség jelszóra, hagyja üresen",Password per partecipare al gioco. Lascia vuoto se non è richiesta alcuna password,ゲームに参加するためのパスワード。パスワードが必要ない場合は空白のままにしてください,게임에 참여하기 위한 비밀번호입니다. 비밀번호가 필요하지 않으면 비워두세요,Passord for å bli med i spillet. La det stå tomt hvis du ikke trenger passord,"Hasło umożliwiające dołączenie do gry. Pozostaw puste, jeśli hasło nie jest wymagane",Senha para entrar no jogo. Deixe em branco se nenhuma senha for necessária,Palavra-passe para entrar no jogo. Deixe em branco se não for necessária nenhuma palavra-passe,Parola pentru a intra in joc. Lăsați necompletat dacă nu este necesară o parolă,"Пароль для входа в игру. Оставьте пустым, если пароль не требуется","Heslo pre vstup do hry. Ak heslo nie je potrebné, ponechajte pole prázdne",Contraseña para unirse al juego. Déjelo en blanco si no se requiere contraseña,Lösenord för att gå med i spelet. Lämna tomt om inget lösenord krävs,Oyuna katılmak için şifre. Şifre gerekmiyorsa boş bırakın,"Пароль для входу в гру. Залиште поле порожнім, якщо пароль не потрібен" -host/public,Public checkbox label,Public Game,Публична игра ,公共游戏 ,公開遊戲 ,Veřejná hra,Offentligt spil ,Openbaar spel,Julkinen peli,Jeu public,Öffentliches Spiel,,,Gioco pubblico,パブリックゲーム,공개 게임,Offentlig spill,Gra publiczna,Jogo Público ,Jogo Público ,Joc public,Публичная игра,Verejná hra,Juego público,Offentligt spel,Halka Açık Oyun,Громадська гра -host/public__tooltip,Public checkbox tooltip,List this game in the server browser.,Избройте тази игра в браузъра на сървъра. ,在服务器浏览器中列出该游戏。 ,在伺服器瀏覽器中列出該遊戲。 ,Vypište tuto hru v prohlížeči serveru. ,List dette spil i serverbrowseren.,Geef dit spel weer in de serverbrowser.,Listaa tämä peli palvelimen selaimeen.,Listez ce jeu dans le navigateur du serveur.,Listen Sie dieses Spiel im Serverbrowser auf.,इस गेम को सर्वर ब्राउज़र में सूचीबद्ध करें।,Listázza ezt a játékot a szerver böngészőjében.,Elenca questo gioco nel browser del server.,このゲームをサーバー ブラウザーにリストします。,서버 브라우저에 이 게임을 나열하세요.,List dette spillet i servernettleseren.,Dodaj tę grę do przeglądarki serwerów.,Liste este jogo no navegador do servidor. ,Liste este jogo no browser do servidor. ,Listați acest joc în browserul serverului.,Добавьте эту игру в браузер серверов.,Uveďte túto hru v prehliadači servera.,Incluya este juego en el navegador del servidor.,Lista detta spel i serverwebbläsaren.,Bu oyunu sunucu tarayıcısında listeleyin.,Показати цю гру в браузері сервера. -host/details,Details field placeholder,Enter some details about your server,Въведете някои подробности за вашия сървър ,输入有关您的服务器的一些详细信息 ,輸入有關您的伺服器的一些詳細信息 ,Zadejte nějaké podrobnosti o vašem serveru ,Indtast nogle detaljer om din server,Voer enkele gegevens over uw server in,Anna joitain tietoja palvelimestasi,Entrez quelques détails sur votre serveur,Geben Sie einige Details zu Ihrem Server ein,अपने सर्वर के बारे में कुछ विवरण दर्ज करें,Adjon meg néhány adatot a szerveréről,Inserisci alcuni dettagli sul tuo server,サーバーに関する詳細を入力します,서버에 대한 세부 정보를 입력하세요.,Skriv inn noen detaljer om serveren din,Wprowadź kilka szczegółów na temat swojego serwera,Insira alguns detalhes sobre o seu servidor ,Introduza alguns detalhes sobre o seu servidor ,Introduceți câteva detalii despre serverul dvs,Введите некоторые сведения о вашем сервере,Zadajte nejaké podrobnosti o svojom serveri,Ingrese algunos detalles sobre su servidor,Ange några detaljer om din server,Sunucunuzla ilgili bazı ayrıntıları girin,Введіть деякі відомості про ваш сервер -host/details__tooltip,Details field tooltip,Details about your server visible in the server browser.,"Подробности за вашия сървър, видими в сървърния браузър. ",有关服务器的详细信息在服务器浏览器中可见。 ,有關伺服器的詳細資訊在伺服器瀏覽器中可見。 ,Podrobnosti o vašem serveru viditelné v prohlížeči serveru. ,Detaljer om din server er synlige i serverbrowseren.,Details over uw server zichtbaar in de serverbrowser.,Palvelimesi tiedot näkyvät palvelimen selaimessa.,Détails sur votre serveur visibles dans le navigateur du serveur.,Details zu Ihrem Server im Serverbrowser sichtbar.,आपके सर्वर के बारे में विवरण सर्वर ब्राउज़र में दिखाई देता है।,A szerver böngészőjében láthatók a szerver adatai.,Dettagli sul tuo server visibili nel browser del server.,サーバーブラウザに表示されるサーバーに関する詳細。,서버 브라우저에 표시되는 서버에 대한 세부정보입니다.,Detaljer om serveren din er synlig i servernettleseren.,Szczegóły dotyczące Twojego serwera widoczne w przeglądarce serwerów.,Detalhes sobre o seu servidor visíveis no navegador do servidor.,Detalhes sobre o seu servidor visíveis no browser do servidor.,Detalii despre serverul dvs. vizibile în browserul serverului.,Подробная информация о вашем сервере отображается в браузере серверов.,Podrobnosti o vašom serveri viditeľné v prehliadači servera.,Detalles sobre su servidor visibles en el navegador del servidor.,Detaljer om din server visas i serverwebbläsaren.,Sunucunuzla ilgili ayrıntılar sunucu tarayıcısında görünür.,Детальна інформація про ваш сервер відображається в браузері сервера. -host/max_players,Maximum players slider label,Maximum Players,Максимален брой играчи ,最大玩家数 ,最大玩家數 ,Maximální počet hráčů ,Maksimalt antal spillere,Maximale spelers,Pelaajien enimmäismäärä,,Maximale Spielerzahl,अधिकतम खिलाड़ी,Maximális játékosok száma,Giocatori massimi,最大プレイヤー数,최대 플레이어,Maksimalt antall spillere,Maksymalna liczba graczy,Máximo de jogadores ,Máximo de jogadores ,Jucători maxim,Максимальное количество игроков,Maximálny počet hráčov,Personas máximas,Maximalt antal spelare,Maksimum Oyuncu,Максимальна кількість гравців -host/max_players__tooltip,Maximum players slider tooltip,Maximum players allowed to join the game.,"Максимален брой играчи, разрешени да се присъединят към играта. ",允许加入游戏的最大玩家数。 ,允許加入遊戲的最大玩家數。 ,"Maximální počet hráčů, kteří se mohou připojit ke hře.",Maksimalt antal spillere tilladt at deltage i spillet.,Maximaal aantal spelers dat aan het spel mag deelnemen.,Peliin saa osallistua maksimissaan pelaajia.,Nombre maximum de joueurs autorisés à rejoindre le jeu.,"Maximal zulässige Anzahl an Spielern, die dem Spiel beitreten dürfen.",अधिकतम खिलाड़ियों को खेल में शामिल होने की अनुमति।,Maximum játékos csatlakozhat a játékhoz.,Numero massimo di giocatori autorizzati a partecipare al gioco.,ゲームに参加できる最大プレイヤー数。,게임에 참여할 수 있는 최대 플레이어 수입니다.,Maksimalt antall spillere som får være med i spillet.,"Maksymalna liczba graczy, którzy mogą dołączyć do gry.",Máximo de jogadores autorizados a entrar no jogo. ,Máximo de jogadores autorizados a entrar no jogo. ,Numărul maxim de jucători permis să se alăture jocului.,"Максимальное количество игроков, которым разрешено присоединиться к игре.",Do hry sa môže zapojiť maximálny počet hráčov.,Número máximo de jugadores permitidos para unirse al juego.,Maximalt antal spelare som får gå med i spelet.,Oyuna katılmasına izin verilen maksimum oyuncu.,"Максимальна кількість гравців, які можуть приєднатися до гри." -host/start,Maximum players slider label,Start,Започнете,开始 ,開始,Start,Start,Begin,alkaa,Commencer,Start,शुरू,Rajt,Inizio,始める,시작,Start,Początek,Começar ,Iniciar ,start,Начинать,Štart,Comenzar,Start,Başlangıç,Почніть -host/start__tooltip,Maximum players slider tooltip,Start the server.,Стартирайте сървъра. ,启动服务器。 ,啟動伺服器。 ,Spusťte server.,Start serveren.,Start de server.,Käynnistä palvelin.,Démarrez le serveur.,Starten Sie den Server.,सर्वर प्रारंभ करें.,Indítsa el a szervert.,Avviare il server.,サーバーを起動します。,서버를 시작합니다.,Start serveren.,Uruchom serwer.,Inicie o servidor. ,Inicie o servidor. ,Porniți serverul.,Запустите сервер.,Spustite server.,Inicie el servidor.,Starta servern.,Sunucuyu başlatın.,Запустіть сервер. -host/start__tooltip_disabled,Maximum players slider tooltip,Check your settings are valid.,Проверете дали вашите настройки са валидни. ,检查您的设置是否有效。 ,檢查您的設定是否有效。 ,"Zkontrolujte, zda jsou vaše nastavení platná. ",Tjek at dine indstillinger er gyldige.,Controleer of uw instellingen geldig zijn.,"Tarkista, että asetuksesi ovat oikein.",Vérifiez que vos paramètres sont valides.,"Überprüfen Sie, ob Ihre Einstellungen gültig sind.",जांचें कि आपकी सेटिंग्स वैध हैं।,"Ellenőrizze, hogy a beállítások érvényesek-e.",Controlla che le tue impostazioni siano valide.,設定が有効であることを確認してください。,설정이 유효한지 확인하세요.,Sjekk at innstillingene dine er gyldige.,"Sprawdź, czy ustawienia są prawidłowe.",Verifique se suas configurações são válidas. ,Verifique se as suas definições são válidas. ,Verificați că setările dvs. sunt valide.,"Убедитесь, что ваши настройки действительны.","Skontrolujte, či sú vaše nastavenia platné.",Verifique que su configuración sea válida.,Kontrollera att dina inställningar är giltiga.,Ayarlarınızın geçerli olup olmadığını kontrol edin.,Перевірте правильність ваших налаштувань. +host/title,The title of the Host Game page,Host Game,Домакин на играта,主机游戏,主機遊戲,Hostitelská hra,Værtsspil,Gastheerspel,Isäntäpeli,Partie hôte,Gastspiel,मेज़बान खेल,Gazdajáték,Ospita il gioco,ホストゲーム,호스트 게임,Vertsspill,Gra gospodarza,Jogo anfitrião,Jogo anfitrião,Găzduire joc,Хост-игра,Hostiteľská hra,Juego de acogida,Värdspel,Sunucu Oyunu,Ведуча гра +host/name,Server name field placeholder,Server Name,Име на сървъра,服务器名称,伺服器名稱,Název serveru,Server navn,Server naam,Palvelimen nimi,Nom du serveur,Servername,सर्वर का नाम,Szerver név,Nome del server,サーバーの名前,서버 이름,Server navn,Nazwa serwera,Nome do servidor,Nome do servidor,Numele serverului,Имя сервера,Názov servera,Nombre del servidor,Server namn,Sunucu adı,Ім'я сервера +host/name__tooltip,Server name field tooltip,The name of the server that other players will see in the server browser,"Името на сървъра,което другите играчи ще видят в сървърния браузър",其他玩家在服务器浏览器中看到的服务器名称,其他玩家在伺服器瀏覽器中看到的伺服器名稱,"Název serveru, který ostatní hráči uvidí v prohlížeči serveru","Navnet på den server, som andre spillere vil se i serverbrowseren",De naam van de server die andere spelers in de serverbrowser zien,"Palvelimen nimi, jonka muut pelaajat näkevät palvelimen selaimessa",Le nom du serveur que les autres joueurs verront dans le navigateur du serveur,"Der Name des Servers, den andere Spieler im Serverbrowser sehen",सर्वर का नाम जो अन्य खिलाड़ी सर्वर ब्राउज़र में देखेंगे,"A szerver neve, amelyet a többi játékos látni fog a szerver böngészőjében",Il nome del server che gli altri giocatori vedranno nel browser del server,他のプレイヤーがサーバー ブラウザに表示するサーバーの名前,다른 플레이어가 서버 브라우저에서 볼 수 있는 서버 이름,Navnet på serveren som andre spillere vil se i servernettleseren,"Nazwa serwera, którą inni gracze zobaczą w przeglądarce serwerów",O nome do servidor que outros jogadores verão no navegador do servidor,O nome do servidor que os outros jogadores verão no navegador do servidor,The name of the server that other players will see in the server browser,"Имя сервера, которое другие игроки увидят в браузере серверов.","Názov servera, ktorý ostatní hráči uvidia v prehliadači servera",El nombre del servidor que otros jugadores verán en el navegador del servidor.,Namnet på servern som andra spelare kommer att se i serverwebbläsaren,Diğer oyuncuların sunucu tarayıcısında göreceği sunucunun adı,"Назва сервера, яку інші гравці бачитимуть у браузері сервера" +host/password,Password field placeholder,Password (leave blank for no password),Парола (оставете празно за липса на парола),密码(无密码则留空),密碼(無密碼則留空),"Heslo (nechte prázdné, pokud nechcete heslo)",Adgangskode (lad tom for ingen adgangskode),Wachtwoord (leeg laten als er geen wachtwoord is),"Salasana (jätä tyhjäksi, jos et salasanaa)",Mot de passe (laisser vide s'il n'y a pas de mot de passe),"Passwort (leer lassen, wenn kein Passwort vorhanden ist)",पासवर्ड (बिना पासवर्ड के खाली छोड़ें),Jelszó (jelszó nélkül hagyja üresen),Password (lascia vuoto per nessuna password),パスワード (パスワードを使用しない場合は空白のままにします),비밀번호(비밀번호가 없으면 비워두세요),Passord (la det stå tomt for ingen passord),"Hasło (pozostaw puste, jeśli nie ma hasła)",Senha (deixe em branco se não houver senha),"Palavra-passe (deixe em branco se não existir palavra-passe)",Parola (lasa necompletat pentru nicio parola),"Пароль (оставьте пустым, если пароль отсутствует)","Heslo (nechávajte prázdne, ak nechcete zadať heslo)",Contraseña (dejar en blanco si no hay contraseña),Lösenord (lämna tomt för inget lösenord),Şifre (Şifre yoksa boş bırakın),"Пароль (залиште порожнім, якщо немає пароля)" +host/password__tooltip,Password field placeholder,Password for joining the game. Leave blank if no password is required,"Парола за присъединяване към играта. Оставете празно, ако не се изисква парола",加入游戏的密码。如果不需要密码则留空,加入遊戲的密碼。如果不需要密碼則留空,"Heslo pro vstup do hry. Pokud není vyžadováno heslo, ponechte prázdné","Adgangskode for at deltage i spillet. Lad stå tomt, hvis der ikke kræves adgangskode",Wachtwoord voor deelname aan het spel. Laat dit leeg als er geen wachtwoord vereist is,"Salasana peliin liittymiseen. Jätä tyhjäksi, jos salasanaa ei vaadita",Mot de passe pour rejoindre le jeu. Laisser vide si aucun mot de passe n'est requis,"Passwort für die Teilnahme am Spiel. Lassen Sie das Feld leer, wenn kein Passwort erforderlich ist",गेम में शामिल होने के लिए पासवर्ड. यदि पासवर्ड की आवश्यकता नहीं है तो खाली छोड़ दें,"Jelszó a játékhoz való csatlakozáshoz. Ha nincs szükség jelszóra, hagyja üresen",Password per partecipare al gioco. Lascia vuoto se non è richiesta alcuna password,ゲームに参加するためのパスワード。パスワードが必要ない場合は空白のままにしてください,게임에 참여하기 위한 비밀번호입니다. 비밀번호가 필요하지 않으면 비워두세요,Passord for å bli med i spillet. La det stå tomt hvis du ikke trenger passord,"Hasło umożliwiające dołączenie do gry. Pozostaw puste, jeśli hasło nie jest wymagane",Senha para entrar no jogo. Deixe em branco se nenhuma senha for necessária,Palavra-passe para entrar no jogo. Deixe em branco se não for necessária nenhuma palavra-passe,Parola pentru a intra in joc. Lăsați necompletat dacă nu este necesară o parolă,"Пароль для входа в игру. Оставьте пустым, если пароль не требуется","Heslo pre vstup do hry. Ak heslo nie je potrebné, ponechajte pole prázdne",Contraseña para unirse al juego. Déjelo en blanco si no se requiere contraseña,Lösenord för att gå med i spelet. Lämna tomt om inget lösenord krävs,Oyuna katılmak için şifre. Şifre gerekmiyorsa boş bırakın,"Пароль для входу в гру. Залиште поле порожнім, якщо пароль не потрібен" +host/public,Public checkbox label,Public Game,Публична игра,公共游戏,公開遊戲,Veřejná hra,Offentligt spil,Openbaar spel,Julkinen peli,Jeu public,Öffentliches Spiel,,,Gioco pubblico,パブリックゲーム,공개 게임,Offentlig spill,Gra publiczna,Jogo Público,Jogo Público,Joc public,Публичная игра,Verejná hra,Juego público,Offentligt spel,Halka Açık Oyun,Громадська гра +host/public__tooltip,Public checkbox tooltip,List this game in the server browser.,Избройте тази игра в браузъра на сървъра.,在服务器浏览器中列出该游戏。,在伺服器瀏覽器中列出該遊戲。,Vypište tuto hru v prohlížeči serveru.,List dette spil i serverbrowseren.,Geef dit spel weer in de serverbrowser.,Listaa tämä peli palvelimen selaimeen.,Listez ce jeu dans le navigateur du serveur.,Listen Sie dieses Spiel im Serverbrowser auf.,इस गेम को सर्वर ब्राउज़र में सूचीबद्ध करें।,Listázza ezt a játékot a szerver böngészőjében.,Elenca questo gioco nel browser del server.,このゲームをサーバー ブラウザーにリストします。,서버 브라우저에 이 게임을 나열하세요.,List dette spillet i servernettleseren.,Dodaj tę grę do przeglądarki serwerów.,Liste este jogo no navegador do servidor.,Liste este jogo no browser do servidor.,Listați acest joc în browserul serverului.,Добавьте эту игру в браузер серверов.,Uveďte túto hru v prehliadači servera.,Incluya este juego en el navegador del servidor.,Lista detta spel i serverwebbläsaren.,Bu oyunu sunucu tarayıcısında listeleyin.,Показати цю гру в браузері сервера. +host/details,Details field placeholder,Enter some details about your server,Въведете някои подробности за вашия сървър,输入有关您的服务器的一些详细信息,輸入有關您的伺服器的一些詳細信息,Zadejte nějaké podrobnosti o vašem serveru,Indtast nogle detaljer om din server,Voer enkele gegevens over uw server in,Anna joitain tietoja palvelimestasi,Entrez quelques détails sur votre serveur,Geben Sie einige Details zu Ihrem Server ein,अपने सर्वर के बारे में कुछ विवरण दर्ज करें,Adjon meg néhány adatot a szerveréről,Inserisci alcuni dettagli sul tuo server,サーバーに関する詳細を入力します,서버에 대한 세부 정보를 입력하세요.,Skriv inn noen detaljer om serveren din,Wprowadź kilka szczegółów na temat swojego serwera,Insira alguns detalhes sobre o seu servidor,Introduza alguns detalhes sobre o seu servidor,Introduceți câteva detalii despre serverul dvs,Введите некоторые сведения о вашем сервере,Zadajte nejaké podrobnosti o svojom serveri,Ingrese algunos detalles sobre su servidor,Ange några detaljer om din server,Sunucunuzla ilgili bazı ayrıntıları girin,Введіть деякі відомості про ваш сервер +host/details__tooltip,Details field tooltip,Details about your server visible in the server browser.,"Подробности за вашия сървър, видими в сървърния браузър.",有关服务器的详细信息在服务器浏览器中可见。,有關伺服器的詳細資訊在伺服器瀏覽器中可見。,Podrobnosti o vašem serveru viditelné v prohlížeči serveru.,Detaljer om din server er synlige i serverbrowseren.,Details over uw server zichtbaar in de serverbrowser.,Palvelimesi tiedot näkyvät palvelimen selaimessa.,Détails sur votre serveur visibles dans le navigateur du serveur.,Details zu Ihrem Server im Serverbrowser sichtbar.,आपके सर्वर के बारे में विवरण सर्वर ब्राउज़र में दिखाई देता है।,A szerver böngészőjében láthatók a szerver adatai.,Dettagli sul tuo server visibili nel browser del server.,サーバーブラウザに表示されるサーバーに関する詳細。,서버 브라우저에 표시되는 서버에 대한 세부정보입니다.,Detaljer om serveren din er synlig i servernettleseren.,Szczegóły dotyczące Twojego serwera widoczne w przeglądarce serwerów.,Detalhes sobre o seu servidor visíveis no navegador do servidor.,Detalhes sobre o seu servidor visíveis no browser do servidor.,Detalii despre serverul dvs. vizibile în browserul serverului.,Подробная информация о вашем сервере отображается в браузере серверов.,Podrobnosti o vašom serveri viditeľné v prehliadači servera.,Detalles sobre su servidor visibles en el navegador del servidor.,Detaljer om din server visas i serverwebbläsaren.,Sunucunuzla ilgili ayrıntılar sunucu tarayıcısında görünür.,Детальна інформація про ваш сервер відображається в браузері сервера. +host/max_players,Maximum players slider label,Maximum Players,Максимален брой играчи,最大玩家数,最大玩家數,Maximální počet hráčů,Maksimalt antal spillere,Maximale spelers,Pelaajien enimmäismäärä,,Maximale Spielerzahl,अधिकतम खिलाड़ी,Maximális játékosok száma,Giocatori massimi,最大プレイヤー数,최대 플레이어,Maksimalt antall spillere,Maksymalna liczba graczy,Máximo de jogadores,Máximo de jogadores,Jucători maxim,Максимальное количество игроков,Maximálny počet hráčov,Personas máximas,Maximalt antal spelare,Maksimum Oyuncu,Максимальна кількість гравців +host/max_players__tooltip,Maximum players slider tooltip,Maximum players allowed to join the game.,"Максимален брой играчи, разрешени да се присъединят към играта.",允许加入游戏的最大玩家数。,允許加入遊戲的最大玩家數。,"Maximální počet hráčů, kteří se mohou připojit ke hře.",Maksimalt antal spillere tilladt at deltage i spillet.,Maximaal aantal spelers dat aan het spel mag deelnemen.,Peliin saa osallistua maksimissaan pelaajia.,Nombre maximum de joueurs autorisés à rejoindre le jeu.,"Maximal zulässige Anzahl an Spielern, die dem Spiel beitreten dürfen.",अधिकतम खिलाड़ियों को खेल में शामिल होने की अनुमति।,Maximum játékos csatlakozhat a játékhoz.,Numero massimo di giocatori autorizzati a partecipare al gioco.,ゲームに参加できる最大プレイヤー数。,게임에 참여할 수 있는 최대 플레이어 수입니다.,Maksimalt antall spillere som får være med i spillet.,"Maksymalna liczba graczy, którzy mogą dołączyć do gry.",Máximo de jogadores autorizados a entrar no jogo.,Máximo de jogadores autorizados a entrar no jogo.,Numărul maxim de jucători permis să se alăture jocului.,"Максимальное количество игроков, которым разрешено присоединиться к игре.",Do hry sa môže zapojiť maximálny počet hráčov.,Número máximo de jugadores permitidos para unirse al juego.,Maximalt antal spelare som får gå med i spelet.,Oyuna katılmasına izin verilen maksimum oyuncu.,"Максимальна кількість гравців, які можуть приєднатися до гри." +host/start,Maximum players slider label,Start,Започнете,开始,開始,Start,Start,Begin,alkaa,Commencer,Start,शुरू,Rajt,Inizio,始める,시작,Start,Początek,Começar,Iniciar,start,Начинать,Štart,Comenzar,Start,Başlangıç,Почніть +host/start__tooltip,Maximum players slider tooltip,Start the server.,Стартирайте сървъра.,启动服务器。,啟動伺服器。,Spusťte server.,Start serveren.,Start de server.,Käynnistä palvelin.,Démarrez le serveur.,Starten Sie den Server.,सर्वर प्रारंभ करें.,Indítsa el a szervert.,Avviare il server.,サーバーを起動します。,서버를 시작합니다.,Start serveren.,Uruchom serwer.,Inicie o servidor.,Inicie o servidor.,Porniți serverul.,Запустите сервер.,Spustite server.,Inicie el servidor.,Starta servern.,Sunucuyu başlatın.,Запустіть сервер. +host/start__tooltip_disabled,Maximum players slider tooltip,Check your settings are valid.,Проверете дали вашите настройки са валидни.,检查您的设置是否有效。,檢查您的設定是否有效。,"Zkontrolujte, zda jsou vaše nastavení platná.",Tjek at dine indstillinger er gyldige.,Controleer of uw instellingen geldig zijn.,"Tarkista, että asetuksesi ovat oikein.",Vérifiez que vos paramètres sont valides.,"Überprüfen Sie, ob Ihre Einstellungen gültig sind.",जांचें कि आपकी सेटिंग्स वैध हैं।,"Ellenőrizze, hogy a beállítások érvényesek-e.",Controlla che le tue impostazioni siano valide.,設定が有効であることを確認してください。,설정이 유효한지 확인하세요.,Sjekk at innstillingene dine er gyldige.,"Sprawdź, czy ustawienia są prawidłowe.",Verifique se suas configurações são válidas.,Verifique se as suas definições são válidas.,Verificați că setările dvs. sunt valide.,"Убедитесь, что ваши настройки действительны.","Skontrolujte, či sú vaše nastavenia platné.",Verifique que su configuración sea válida.,Kontrollera att dina inställningar är giltiga.,Ayarlarınızın geçerli olup olmadığını kontrol edin.,Перевірте правильність ваших налаштувань. ,,,,,,,,,,,,,,,,,,,,,,,,,,, ,Disconnect Reason,,,,,,,,,,,,,,,,,,,,,,,,,, -dr/invalid_password,Invalid password popup.,Invalid Password!,Невалидна парола! ,无效的密码! ,無效的密碼! ,Neplatné heslo! ,Forkert kodeord!,Ongeldig wachtwoord!,Väärä salasana!,Mot de passe incorrect !,Ungültiges Passwort!,अवैध पासवर्ड!,Érvénytelen jelszó!,Password non valida!,無効なパスワード!,유효하지 않은 비밀번호!,Ugyldig passord!,Nieprawidłowe hasło!,Senha inválida! ,Verifique se as suas definições são válidas. ,Parolă Invalidă!,Неверный пароль!,Nesprávne heslo!,¡Contraseña invalida!,Felaktigt lösenord!,Geçersiz şifre!,Невірний пароль! -dr/game_version,Different game versions.,"Game version mismatch! Server version: {0}, your version: {1}.","Несъответствие на версията на играта! Версия на сървъра: {0}, вашата версия: {1}. ",游戏版本不匹配!服务器版本:{0},您的版本:{1}。 ,遊戲版本不符!伺服器版本:{0},您的版本:{1}。 ,"Nesoulad verze hry! Verze serveru: {0}, vaše verze: {1}.","Spilversionen stemmer ikke overens! Serverversion: {0}, din version: {1}.","Spelversie komt niet overeen! Serverversie: {0}, jouw versie: {1}.","Peliversio ei täsmää! Palvelimen versio: {0}, sinun versiosi: {1}.","Version du jeu incompatible ! Version du serveur : {0}, version locale : {1}","Spielversion stimmt nicht überein! Server Version: {0}, Lokale Version: {1}.","गेम संस्करण बेमेल! सर्वर संस्करण: {0}, आपका संस्करण: {1}.","Nem egyezik a játék verziója! Szerververzió: {0}, az Ön verziója: {1}.","Versioni del gioco non combacianti! Versione del Server: {0}, La tua versione: {1}.",ゲームのバージョンが不一致です!サーバーのバージョン: {0}、あなたのバージョン: {1}。,"게임 버전이 일치하지 않습니다! 서버 버전: {0}, 귀하의 버전: {1}.","Spillversjonen samsvarer ikke! Serverversjon: {0}, din versjon: {1}.","Niezgodna wersja gry! Wersja serwera: {0}, Twoja wersja: {1}.","Incompatibilidade de versão do jogo! Versão do servidor: {0}, sua versão: {1}. ","Incompatibilidade de versão do jogo! Versão do servidor: {0}, a sua versão: {1}. ","Versiunea jocului nepotrivită! Versiunea serverului: {0}, versiunea dvs.: {1}.","Несоответствие версии игры! Версия сервера: {0}, ваша версия: {1}.","Nesúlad verzie hry! Verzia servera: {0}, vaša verzia: {1}.","¡La versión del juego no coincide! Versión del servidor: {0}, tu versión: {1}.","Spelversionen matchar inte! Serverversion: {0}, din version: {1}.","Oyun sürümü uyuşmazlığı! Sunucu sürümü: {0}, sürümünüz: {1}.","Невідповідність версії гри! Версія сервера: {0}, ваша версія: {1}." -dr/full_server,The server is already full.,The server is full!,Сървърът е пълен! ,服务器已满! ,伺服器已滿! ,Server je plný! ,Serveren er fuld!,De server is vol!,Palvelin täynnä!,Le serveur est complet !,Der Server ist voll!,सर्वर पूर्ण है!,Tele a szerver!,Il Server è pieno!,サーバーがいっぱいです!,서버가 꽉 찼어요!,Serveren er full!,Serwer jest pełny!,O servidor está cheio! ,O servidor está cheio! ,Serverul este plin!,Сервер переполнен!,Server je plný!,¡El servidor está lleno!,Servern är full!,Sunucu dolu!,Сервер заповнений! -dr/mods,"The client is missing, or has extra mods.",Mod mismatch!,Несъответствие на мода! ,模组不匹配! ,模組不符! ,Neshoda modů!,Mod uoverensstemmelse! ,Mod-mismatch!,Modi ei täsmää!,Mod incompatible !,Mods stimmen nicht überein!,मॉड बेमेल!,Mod eltérés!,Mod non combacianti!,モジュールが不一致です!,모드 불일치!,Moduoverensstemmelse!,Niezgodność modów!,Incompatibilidade de mod! ,"Incompatibilidade de mod! +dr/invalid_password,Invalid password popup.,Invalid Password!,Невалидна парола!,无效的密码!,無效的密碼!,Neplatné heslo!,Forkert kodeord!,Ongeldig wachtwoord!,Väärä salasana!,Mot de passe incorrect !,Ungültiges Passwort!,अवैध पासवर्ड!,Érvénytelen jelszó!,Password non valida!,無効なパスワード!,유효하지 않은 비밀번호!,Ugyldig passord!,Nieprawidłowe hasło!,Senha inválida!,Verifique se as suas definições são válidas.,Parolă Invalidă!,Неверный пароль!,Nesprávne heslo!,¡Contraseña invalida!,Felaktigt lösenord!,Geçersiz şifre!,Невірний пароль! +dr/game_version,Different game versions.,"Game version mismatch! Server version: {0}, your version: {1}.","Несъответствие на версията на играта! Версия на сървъра: {0}, вашата версия: {1}.",游戏版本不匹配!服务器版本:{0},您的版本:{1}。,遊戲版本不符!伺服器版本:{0},您的版本:{1}。,"Nesoulad verze hry! Verze serveru: {0}, vaše verze: {1}.","Spilversionen stemmer ikke overens! Serverversion: {0}, din version: {1}.","Spelversie komt niet overeen! Serverversie: {0}, jouw versie: {1}.","Peliversio ei täsmää! Palvelimen versio: {0}, sinun versiosi: {1}.","Version du jeu incompatible ! Version du serveur : {0}, version locale : {1}","Spielversion stimmt nicht überein! Server Version: {0}, Lokale Version: {1}.","गेम संस्करण बेमेल! सर्वर संस्करण: {0}, आपका संस्करण: {1}.","Nem egyezik a játék verziója! Szerververzió: {0}, az Ön verziója: {1}.","Versioni del gioco non combacianti! Versione del Server: {0}, La tua versione: {1}.",ゲームのバージョンが不一致です!サーバーのバージョン: {0}、あなたのバージョン: {1}。,"게임 버전이 일치하지 않습니다! 서버 버전: {0}, 귀하의 버전: {1}.","Spillversjonen samsvarer ikke! Serverversjon: {0}, din versjon: {1}.","Niezgodna wersja gry! Wersja serwera: {0}, Twoja wersja: {1}.","Incompatibilidade de versão do jogo! Versão do servidor: {0}, sua versão: {1}.","Incompatibilidade de versão do jogo! Versão do servidor: {0}, a sua versão: {1}.","Versiunea jocului nepotrivită! Versiunea serverului: {0}, versiunea dvs.: {1}.","Несоответствие версии игры! Версия сервера: {0}, ваша версия: {1}.","Nesúlad verzie hry! Verzia servera: {0}, vaša verzia: {1}.","¡La versión del juego no coincide! Versión del servidor: {0}, tu versión: {1}.","Spelversionen matchar inte! Serverversion: {0}, din version: {1}.","Oyun sürümü uyuşmazlığı! Sunucu sürümü: {0}, sürümünüz: {1}.","Невідповідність версії гри! Версія сервера: {0}, ваша версія: {1}." +dr/full_server,The server is already full.,The server is full!,Сървърът е пълен!,服务器已满!,伺服器已滿!,Server je plný!,Serveren er fuld!,De server is vol!,Palvelin täynnä!,Le serveur est complet !,Der Server ist voll!,सर्वर पूर्ण है!,Tele a szerver!,Il Server è pieno!,サーバーがいっぱいです!,서버가 꽉 찼어요!,Serveren er full!,Serwer jest pełny!,O servidor está cheio!,O servidor está cheio!,Serverul este plin!,Сервер переполнен!,Server je plný!,¡El servidor está lleno!,Servern är full!,Sunucu dolu!,Сервер заповнений! +dr/mods,"The client is missing, or has extra mods.",Mod mismatch!,Несъответствие на мода!,模组不匹配!,模組不符!,Neshoda modů!,Mod uoverensstemmelse!,Mod-mismatch!,Modi ei täsmää!,Mod incompatible !,Mods stimmen nicht überein!,मॉड बेमेल!,Mod eltérés!,Mod non combacianti!,モジュールが不一致です!,모드 불일치!,Moduoverensstemmelse!,Niezgodność modów!,Incompatibilidade de mod!,"Incompatibilidade de mod! ",Nepotrivire mod!,Несоответствие модов!,Nezhoda modov!,"Falta el cliente, o tiene modificaciones adicionales.",Mod-felmatchning!,Mod uyumsuzluğu!,Невідповідність модів! -dr/mods_missing,The list of missing mods.,Missing Mods:\n- {0},Липсващи модификации:\n- {0} ,缺少模组:\n- {0} ,缺少模組:\n- {0} ,Chybějící mody:\n- {0},Manglende mods:\n- {0} ,Ontbrekende mods:\n- {0},Puuttuvat modit:\n- {0},Mods manquants:\n-{0},Fehlende Mods:\n- {0},गुम मॉड्स:\n- {0},Hiányzó modok:\n- {0},Mod Mancanti:\n- {0},不足している MOD:\n- {0},누락된 모드:\n- {0},Manglende modi:\n- {0},Brakujące mody:\n- {0},Modificações ausentes:\n- {0} ,Modificações em falta:\n- {0} ,Moduri lipsă:\n- {0},Отсутствующие моды:\n- {0},Chýbajúce modifikácie:\n- {0},Mods faltantes:\n- {0},Mods saknas:\n- {0},Eksik Modlar:\n- {0},Відсутні моди:\n- {0} -dr/mods_extra,The list of extra mods.,Extra Mods:\n- {0},Допълнителни модификации:\n- {0} ,额外模组:\n- {0} ,額外模組:\n- {0} ,Extra modifikace:\n- {0},Ekstra mods:\n- {0} ,Extra aanpassingen:\n- {0},Lisämodit:\n- {0},Mods extras:\n-{0},Zusätzliche Mods:\n- {0},अतिरिक्त मॉड:\n- {0},Extra modok:\n- {0},Mod Extra:\n- {0},追加の Mod:\n- {0},추가 모드:\n- {0},Ekstra modi:\n- {0},Dodatkowe mody:\n- {0},Modificações extras:\n- {0} ,Modificações extra:\n- {0} ,Moduri suplimentare:\n- {0},Дополнительные моды:\n- {0},Extra modifikácie:\n- {0},Modificaciones adicionales:\n- {0},Extra mods:\n- {0},Ekstra Modlar:\n- {0},Додаткові моди:\n- {0} +dr/mods_missing,The list of missing mods.,Missing Mods:\n- {0},Липсващи модификации:\n- {0},缺少模组:\n- {0},缺少模組:\n- {0},Chybějící mody:\n- {0},Manglende mods:\n- {0},Ontbrekende mods:\n- {0},Puuttuvat modit:\n- {0},Mods manquants:\n-{0},Fehlende Mods:\n- {0},गुम मॉड्स:\n- {0},Hiányzó modok:\n- {0},Mod Mancanti:\n- {0},不足している MOD:\n- {0},누락된 모드:\n- {0},Manglende modi:\n- {0},Brakujące mody:\n- {0},Modificações ausentes:\n- {0},Modificações em falta:\n- {0},Moduri lipsă:\n- {0},Отсутствующие моды:\n- {0},Chýbajúce modifikácie:\n- {0},Mods faltantes:\n- {0},Mods saknas:\n- {0},Eksik Modlar:\n- {0},Відсутні моди:\n- {0} +dr/mods_extra,The list of extra mods.,Extra Mods:\n- {0},Допълнителни модификации:\n- {0},额外模组:\n- {0},額外模組:\n- {0},Extra modifikace:\n- {0},Ekstra mods:\n- {0},Extra aanpassingen:\n- {0},Lisämodit:\n- {0},Mods extras:\n-{0},Zusätzliche Mods:\n- {0},अतिरिक्त मॉड:\n- {0},Extra modok:\n- {0},Mod Extra:\n- {0},追加の Mod:\n- {0},추가 모드:\n- {0},Ekstra modi:\n- {0},Dodatkowe mody:\n- {0},Modificações extras:\n- {0},Modificações extra:\n- {0},Moduri suplimentare:\n- {0},Дополнительные моды:\n- {0},Extra modifikácie:\n- {0},Modificaciones adicionales:\n- {0},Extra mods:\n- {0},Ekstra Modlar:\n- {0},Додаткові моди:\n- {0} ,,,,,,,,,,,,,,,,,,,,,,,,,,, ,Career Manager,,,,,,,,,,,,,,,,,,,,,,,,,, -carman/fees_host_only,Text shown when a client tries to manage fees.,Only the host can manage fees!,Само домакинът може да управлява таксите! ,只有房东可以管理费用! ,只有房東可以管理費用! ,Poplatky může spravovat pouze hostitel!,Kun værten kan administrere gebyrer!,Alleen de host kan de kosten beheren!,Vain isäntä voi hallita maksuja!,Seul l'hôte peut gérer les frais !,Nur der Host kann Gebühren verwalten!,केवल मेज़बान ही फीस का प्रबंधन कर सकता है!,Csak a házigazda kezelheti a díjakat!,Solo l’Host può gestire gli addebiti!,料金を管理できるのはホストだけです。,호스트만이 수수료를 관리할 수 있습니다!,Bare verten kan administrere gebyrer!,Tylko gospodarz może zarządzać opłatami!,Somente o anfitrião pode gerenciar as taxas! ,Só o anfitrião pode gerir as taxas! ,Doar gazda poate gestiona taxele!,Только хозяин может управлять комиссией!,Poplatky môže spravovať iba hostiteľ!,¡Solo el anfitrión puede administrar las tarifas!,Endast värden kan hantera avgifter!,Ücretleri yalnızca ev sahibi yönetebilir!,Тільки господар може керувати оплатою! +carman/fees_host_only,Text shown when a client tries to manage fees.,Only the host can manage fees!,Само домакинът може да управлява таксите!,只有房东可以管理费用!,只有房東可以管理費用!,Poplatky může spravovat pouze hostitel!,Kun værten kan administrere gebyrer!,Alleen de host kan de kosten beheren!,Vain isäntä voi hallita maksuja!,Seul l'hôte peut gérer les frais !,Nur der Host kann Gebühren verwalten!,केवल मेज़बान ही फीस का प्रबंधन कर सकता है!,Csak a házigazda kezelheti a díjakat!,Solo l’Host può gestire gli addebiti!,料金を管理できるのはホストだけです。,호스트만이 수수료를 관리할 수 있습니다!,Bare verten kan administrere gebyrer!,Tylko gospodarz może zarządzać opłatami!,Somente o anfitrião pode gerenciar as taxas!,Só o anfitrião pode gerir as taxas!,Doar gazda poate gestiona taxele!,Только хозяин может управлять комиссией!,Poplatky môže spravovať iba hostiteľ!,¡Solo el anfitrión puede administrar las tarifas!,Endast värden kan hantera avgifter!,Ücretleri yalnızca ev sahibi yönetebilir!,Тільки господар може керувати оплатою! ,,,,,,,,,,,,,,,,,,,,,,,,,,, ,Player List,,,,,,,,,,,,,,,,,,,,,,,,,, -plist/title,The title of the player list.,Online Players,Онлайн играчи ,在线玩家 ,線上玩家 ,Online hráči,Online spillere,Online spelers,Online-pelaajat,Joueurs en ligne,Verbundene Spieler,ऑनलाइन खिलाड़ी,Online játékosok,Giocatori Online,,온라인 플레이어,Online spillere,Gracze sieciowi,Jogadores on-line ,Jogadores on-line ,Jucători online,Онлайн-игроки,Online hráči,Jugadores en línea,Spelare online,Çevrimiçi Oyuncular,Онлайн гравці +plist/title,The title of the player list.,Online Players,Онлайн играчи,在线玩家,線上玩家,Online hráči,Online spillere,Online spelers,Online-pelaajat,Joueurs en ligne,Verbundene Spieler,ऑनलाइन खिलाड़ी,Online játékosok,Giocatori Online,,온라인 플레이어,Online spillere,Gracze sieciowi,Jogadores on-line,Jogadores on-line,Jucători online,Онлайн-игроки,Online hráči,Jugadores en línea,Spelare online,Çevrimiçi Oyuncular,Онлайн гравці ,,,,,,,,,,,,,,,,,,,,,,,,,,, ,Loading Info,,,,,,,,,,,,,,,,,,,,,,,,,, -linfo/wait_for_server,Text shown in the loading screen.,Waiting for server to load,Изчаква се зареждане на сървъра ,等待服务器加载 ,等待伺服器加載 ,Čekání na načtení serveru ,"Venter på, at serveren indlæses ",Wachten tot de server is geladen,Odotetaan palvelimen latautumista,En attente du chargement du serveur,Warte auf das Laden des Servers,सर्वर लोड होने की प्रतीक्षा की जा रही है,Várakozás a szerver betöltésére,In attesa del caricamento del Server,サーバーがロードされるのを待っています,서버가 로드되기를 기다리는 중,Venter på at serveren skal lastes,Czekam na załadowanie serwera,Esperando o servidor carregar ,sperando que o servidor carregue ,Se așteaptă încărcarea serverului,Ожидание загрузки сервера,Čaká sa na načítanie servera,Esperando a que cargue el servidor...,Väntar på att servern ska laddas,Sunucunun yüklenmesi bekleniyor,Очікування завантаження сервера -linfo/sync_world_state,Text shown in the loading screen.,Syncing world state,Синхронизиране на световното състояние ,同步世界状态 ,同步世界狀態 ,Synchronizace světového stavu ,Synkroniserer verdensstaten,Het synchroniseren van de wereldstaat,Synkronoidaan maailmantila,Synchronisation des données du monde,Synchronisiere Daten,सिंक हो रही विश्व स्थिति,Szinkronizáló világállapot,Sincronizzazione dello stato del mondo,世界状態を同期しています,세계 상태 동기화 중,Synkroniserer verdensstaten,Synchronizacja stanu świata,Sincronizando o estado mundial ,Sincronizando o estado mundial ,Sincronizarea stării mondiale,Синхронизация состояния мира,Synchronizácia svetového štátu,Sincronizando estado global,Synkroniserar världsstaten,Dünya durumunu senkronize etme,Синхронізація стану світу +linfo/wait_for_server,Text shown in the loading screen.,Waiting for server to load,Изчаква се зареждане на сървъра,等待服务器加载,等待伺服器加載,Čekání na načtení serveru,"Venter på, at serveren indlæses",Wachten tot de server is geladen,Odotetaan palvelimen latautumista,En attente du chargement du serveur,Warte auf das Laden des Servers,सर्वर लोड होने की प्रतीक्षा की जा रही है,Várakozás a szerver betöltésére,In attesa del caricamento del Server,サーバーがロードされるのを待っています,서버가 로드되기를 기다리는 중,Venter på at serveren skal lastes,Czekam na załadowanie serwera,Esperando o servidor carregar,sperando que o servidor carregue,Se așteaptă încărcarea serverului,Ожидание загрузки сервера,Čaká sa na načítanie servera,Esperando a que cargue el servidor...,Väntar på att servern ska laddas,Sunucunun yüklenmesi bekleniyor,Очікування завантаження сервера +linfo/sync_world_state,Text shown in the loading screen.,Syncing world state,Синхронизиране на световното състояние,同步世界状态,同步世界狀態,Synchronizace světového stavu,Synkroniserer verdensstaten,Het synchroniseren van de wereldstaat,Synkronoidaan maailmantila,Synchronisation des données du monde,Synchronisiere Daten,सिंक हो रही विश्व स्थिति,Szinkronizáló világállapot,Sincronizzazione dello stato del mondo,世界状態を同期しています,세계 상태 동기화 중,Synkroniserer verdensstaten,Synchronizacja stanu świata,Sincronizando o estado mundial,Sincronizando o estado mundial,Sincronizarea stării mondiale,Синхронизация состояния мира,Synchronizácia svetového štátu,Sincronizando estado global,Synkroniserar världsstaten,Dünya durumunu senkronize etme,Синхронізація стану світу From 30c598849a02c2ba601f5cd1a483c2bcf267e406 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 7 Jul 2024 09:31:45 +1000 Subject: [PATCH 23/34] Fixed translation issue --- locale.csv | 1 + 1 file changed, 1 insertion(+) diff --git a/locale.csv b/locale.csv index 9d797c6..9852356 100644 --- a/locale.csv +++ b/locale.csv @@ -5,6 +5,7 @@ Key,Description,English,Bulgarian,Chinese (Simplified),Chinese (Traditional),Cze ,"When saving the file, ensure to save it using UTF-8 encoding!",,,,,,,,,,,,,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,,, ,Main Menu,,,,,,,,,,,,,,,,,,,,,,,,,,, +mm/join_server,The 'Join Server' button in the main menu.,Join Server,Присъединете се към сървъра,加入服务器,加入伺服器,Připojte se k serveru,Tilmeld dig server,Kom bij de server,Liity palvelimelle,Rejoindre le serveur,Spiel beitreten,सर्वर में शामिल हों,Csatlakozz a szerverhez,Entra in un Server,サーバーに参加する,서버에 가입,Bli med server,Dołącz do serwera,Conectar-se ao servidor,Ligar-se ao servidor,Alăturați-vă serverului,Присоединиться к серверу,Pripojte sa k serveru,Unirse a un servidor,Gå med i servern,Sunucuya katıl,Приєднатися до сервера mm/join_server__tooltip,The tooltip shown when hovering over the 'Join Server' button.,Join a multiplayer session.,Присъединете се към мултиплейър сесия.,加入多人游戏会话。,加入多人遊戲會話。,Připojte se k relaci pro více hráčů.,Deltag i en multiplayer session.,Neem deel aan een multiplayersessie.,Liity moninpeliistuntoon.,Rejoindre une session multijoueur,Trete einer Mehrspielersitzung bei.,मल्टीप्लेयर सत्र में शामिल हों.,Csatlakozz egy többjátékos munkamenethez.,Entra in una sessione multiplayer.,マルチプレイヤー セッションに参加します。,멀티플레이어 세션에 참여하세요.,Bli med på en flerspillerøkt.,Dołącz do sesji wieloosobowej.,Participe de uma sessão multijogador.,Participe numa sessão multijogador.,Alăturați-vă unei sesiuni multiplayer.,Присоединяйтесь к многопользовательской сессии.,Pripojte sa k relácii pre viacerých hráčov.,Únete a una sesión multijugador.,Gå med i en multiplayer-session.,Çok oyunculu bir oturuma katılın.,Приєднуйтеся до багатокористувацької сесії. mm/join_server__tooltip_disabled,Unused,,,,,,,,,,,,,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,,, From 252745db58f6a72aeb4afd2495a936fd07ee19d0 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 7 Jul 2024 09:37:26 +1000 Subject: [PATCH 24/34] Updated default server browser text. Server browser is now fully implemented, noting that a future feature may be to display the required mods, rather than just show they are/aren't required --- Multiplayer/Components/MainMenu/ServerBrowserPane.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs index 4d04259..335dcf5 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs @@ -222,7 +222,7 @@ private void BuildUI() detailsPane = textGO.GetComponent(); detailsPane.textWrappingMode = TextWrappingModes.Normal; detailsPane.fontSize = 18; - detailsPane.text = "Server browser not fully implemented.

Dummy servers are shown for demonstration purposes only.

Press refresh to attempt loading real servers.
After pressing refresh, auto refresh will occur every 30 seconds."; + detailsPane.text = "Dummy servers are shown for demonstration purposes only.

Press refresh to attempt loading real servers.
After pressing refresh, auto refresh will occur every 30 seconds."; // Adjust text RectTransform to fit content RectTransform textRT = textGO.GetComponent(); From ab36de5ab0209174e030c2cac9fce167573c27f8 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 7 Jul 2024 09:40:32 +1000 Subject: [PATCH 25/34] Reorganised UI components --- Multiplayer/Components/Networking/NetworkLifecycle.cs | 1 + Multiplayer/Components/Networking/{ => UI}/NetworkStatsGui.cs | 2 +- Multiplayer/Components/Networking/{ => UI}/PlayerListGUI.cs | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) rename Multiplayer/Components/Networking/{ => UI}/NetworkStatsGui.cs (98%) rename Multiplayer/Components/Networking/{ => UI}/PlayerListGUI.cs (97%) diff --git a/Multiplayer/Components/Networking/NetworkLifecycle.cs b/Multiplayer/Components/Networking/NetworkLifecycle.cs index 734e407..dcaaf5f 100644 --- a/Multiplayer/Components/Networking/NetworkLifecycle.cs +++ b/Multiplayer/Components/Networking/NetworkLifecycle.cs @@ -6,6 +6,7 @@ using DV.Utils; using LiteNetLib; using LiteNetLib.Utils; +using Multiplayer.Components.Networking.UI; using Multiplayer.Networking.Data; using Multiplayer.Networking.Listeners; using Multiplayer.Utils; diff --git a/Multiplayer/Components/Networking/NetworkStatsGui.cs b/Multiplayer/Components/Networking/UI/NetworkStatsGui.cs similarity index 98% rename from Multiplayer/Components/Networking/NetworkStatsGui.cs rename to Multiplayer/Components/Networking/UI/NetworkStatsGui.cs index ab05efe..e80cf80 100644 --- a/Multiplayer/Components/Networking/NetworkStatsGui.cs +++ b/Multiplayer/Components/Networking/UI/NetworkStatsGui.cs @@ -5,7 +5,7 @@ using LiteNetLib; using UnityEngine; -namespace Multiplayer.Components.Networking; +namespace Multiplayer.Components.Networking.UI; public class NetworkStatsGui : MonoBehaviour { diff --git a/Multiplayer/Components/Networking/PlayerListGUI.cs b/Multiplayer/Components/Networking/UI/PlayerListGUI.cs similarity index 97% rename from Multiplayer/Components/Networking/PlayerListGUI.cs rename to Multiplayer/Components/Networking/UI/PlayerListGUI.cs index 8a516fa..471d050 100644 --- a/Multiplayer/Components/Networking/PlayerListGUI.cs +++ b/Multiplayer/Components/Networking/UI/PlayerListGUI.cs @@ -2,7 +2,7 @@ using Multiplayer.Components.Networking.Player; using UnityEngine; -namespace Multiplayer.Components.Networking; +namespace Multiplayer.Components.Networking.UI; public class PlayerListGUI : MonoBehaviour { From 830cd1cb2024791e9887b005014d3d929ac1d581 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 7 Jul 2024 21:24:27 +1000 Subject: [PATCH 26/34] Initial commit Create chat panel blocked input from player when shown. Still need to block number keys for toolbelt and implement network logic --- .../Components/MainMenu/ServerBrowserPane.cs | 42 +- .../Components/Networking/NetworkLifecycle.cs | 6 +- .../Components/Networking/UI/ChatGUI.cs | 399 ++++++++++++++++++ Multiplayer/Multiplayer.csproj | 2 + .../Managers/Client/NetworkClient.cs | 16 +- 5 files changed, 459 insertions(+), 6 deletions(-) create mode 100644 Multiplayer/Components/Networking/UI/ChatGUI.cs diff --git a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs index 335dcf5..d329565 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs @@ -15,6 +15,7 @@ using System.Linq; using Multiplayer.Networking.Data; using DV; +using Multiplayer.Components.Networking.UI; @@ -71,6 +72,43 @@ public class ServerBrowserPane : MonoBehaviour private void Awake() { Multiplayer.Log("MultiplayerPane Awake()"); + /* + * + * Temp testing code + * + */ + + //GameObject chat = new GameObject("ChatUI", typeof(ChatGUI)); + //chat.transform.SetParent(GameObject.Find("MenuOpeningScene").transform,false); + + //////////Debug.Log("Instantiating Overlay"); + //////////GameObject overlay = new GameObject("Overlay", typeof(ChatGUI)); + //////////GameObject parent = GameObject.Find("MenuOpeningScene"); + //////////if (parent != null) + //////////{ + ////////// overlay.transform.SetParent(parent.transform, false); + ////////// Debug.Log("Overlay parent set to MenuOpeningScene"); + //////////} + //////////else + //////////{ + ////////// Debug.LogError("MenuOpeningScene not found"); + //////////} + + //////////Debug.Log("Overlay instantiated with components:"); + //////////foreach (Transform child in overlay.transform) + //////////{ + ////////// Debug.Log("Child: " + child.name); + ////////// foreach (Transform grandChild in child) + ////////// { + ////////// Debug.Log("GrandChild: " + grandChild.name); + ////////// } + //////////} + + /* + * + * End Temp testing code + * + */ CleanUI(); BuildUI(); @@ -331,7 +369,7 @@ private void JoinAction() } //No password, just connect - SingletonBehaviour.Instance.StartClient(selectedServer.ip, selectedServer.port, null); + SingletonBehaviour.Instance.StartClient(selectedServer.ip, selectedServer.port, null, false); } } @@ -517,7 +555,7 @@ private void ShowPasswordPopup() } - SingletonBehaviour.Instance.StartClient(ipAddress, portNumber, result.data); + SingletonBehaviour.Instance.StartClient(ipAddress, portNumber, result.data, false); //ShowConnectingPopup(); // Show a connecting message //SingletonBehaviour.Instance.ConnectionFailed += HandleConnectionFailed; diff --git a/Multiplayer/Components/Networking/NetworkLifecycle.cs b/Multiplayer/Components/Networking/NetworkLifecycle.cs index dcaaf5f..e07dda8 100644 --- a/Multiplayer/Components/Networking/NetworkLifecycle.cs +++ b/Multiplayer/Components/Networking/NetworkLifecycle.cs @@ -147,16 +147,16 @@ public bool StartServer(IDifficulty difficulty) if (!server.Start(port)) return false; Server = server; - StartClient("localhost", port, Multiplayer.Settings.Password); + StartClient("localhost", port, Multiplayer.Settings.Password, isSinglePlayer); return true; } - public void StartClient(string address, int port, string password) + public void StartClient(string address, int port, string password, bool isSinglePlayer) { if (Client != null) throw new InvalidOperationException("NetworkManager already exists!"); NetworkClient client = new(Multiplayer.Settings); - client.Start(address, port, password); + client.Start(address, port, password, isSinglePlayer); Client = client; OnSettingsUpdated(Multiplayer.Settings); // Show stats if enabled } diff --git a/Multiplayer/Components/Networking/UI/ChatGUI.cs b/Multiplayer/Components/Networking/UI/ChatGUI.cs new file mode 100644 index 0000000..f35100f --- /dev/null +++ b/Multiplayer/Components/Networking/UI/ChatGUI.cs @@ -0,0 +1,399 @@ +using System; +using System.Collections.Generic; +using DV; +using DV.UI; +using Multiplayer.Components.MainMenu; +using Multiplayer.Utils; +using TMPro; +using UnityEngine; +using UnityEngine.UI; + +namespace Multiplayer.Components.Networking.UI; + +//[RequireComponent(typeof(Canvas))] +//[RequireComponent(typeof(CanvasScaler))] +[RequireComponent(typeof(RectTransform))] +public class ChatGUI : MonoBehaviour +{ + private const float MESSAGE_INSET = 15f; + private const int MAX_MESSAGES = 50; + private const int MESSAGE_TIMEOUT = 10; + + private GameObject messagePrefab; + + public List messageList = new List(); + + private TMP_InputField chatInputIF; + private ScrollRect scrollRect; + private RectTransform chatPanel; + + private GameObject panelGO; + private GameObject textInputGO; + private GameObject scrollViewGO; + + private bool isOpen = false; + private bool showingMessage = false; + + private CustomFirstPersonController player; + + private float timeOut; + + private void Awake() + { + Debug.Log("OverlayUI Awake() called"); + + // Create a new UI Canvas + GameObject canvasGO = new GameObject("OverlayCanvas"); + canvasGO.transform.SetParent(this.transform, false); + /* + Canvas canvas = canvasGO.AddComponent(); + canvas.renderMode = RenderMode.ScreenSpaceOverlay; + canvas.worldCamera = Camera.main; + canvas.sortingOrder = 100; // Ensure this canvas is rendered above others + + Debug.Log("Canvas created and configured"); + + // Add a CanvasScaler and GraphicRaycaster + CanvasScaler canvasScaler = canvasGO.AddComponent(); + canvasScaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize; + canvasScaler.referenceResolution = new Vector2(1920, 1080); + canvasGO.AddComponent(); + + Debug.Log("CanvasScaler and GraphicRaycaster added"); + */ + + // Create a Panel + panelGO = new GameObject("OverlayPanel"); + panelGO.transform.SetParent(canvasGO.transform, false); + RectTransform rectTransform = panelGO.AddComponent(); + rectTransform.sizeDelta = new Vector2(Screen.width * 0.3f, Screen.height * 0.3f); + rectTransform.anchorMin = Vector2.zero;//new Vector2(0.5f, 0.5f); + rectTransform.anchorMax = Vector2.zero;//new Vector2(0.5f, 0.5f); + rectTransform.pivot = new Vector2(0.5f, 0.5f); + rectTransform.anchoredPosition = Vector2.zero; + + // Debug.Log("Panel created and positioned"); + + // Add an Image component for coloring + /* + Image image = panelGO.AddComponent(); + image.color = new Color(1f, 0f, 0f, 0.5f); + + Debug.Log("Image component added and colored"); + */ + + //// Add a Text element to the panel + //GameObject textGO = new GameObject("TestText"); + //textGO.transform.SetParent(panelGO.transform, false); + //Text text = textGO.AddComponent(); + //text.text = "Overlay Test"; + ////text.font = Resources.GetBuiltinResource("Arial.ttf"); + ////text.alignment = TextAnchor.MiddleCenter; + //text.color = Color.white; + //RectTransform textRectTransform = textGO.GetComponent(); + //textRectTransform.sizeDelta = rectTransform.sizeDelta; + //textRectTransform.anchorMin = new Vector2(0.5f, 0.5f); + //textRectTransform.anchorMax = new Vector2(0.5f, 0.5f); + //textRectTransform.pivot = new Vector2(0.5f, 0.5f); + //textRectTransform.anchoredPosition = Vector2.zero; + + //Debug.Log("Test text added to panel"); + + + //this.GetComponent().worldCamera = Camera.main; + //this.GetComponent().renderMode = RenderMode.ScreenSpaceOverlay; + //this.GetComponent().uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize; + + BuildUI(); + panelGO.SetActive(false); + + //Find the player + player = GameObject.FindObjectOfType(); + if(player == null) + { + Debug.Log("Failed to find CustomFirstPersonController"); + } + + } + + private void OnEnable() + { + chatInputIF.onSubmit.AddListener(SendChat); + + } + + private void OnDisable() + { + chatInputIF.onSubmit.RemoveAllListeners(); + } + + private void Update() + { + //Debug.Log($"ChatGUI Update: isOpen {isOpen} Key Enter: {Input.GetKeyDown(KeyCode.Return)}"); + + if (!isOpen && Input.GetKeyDown(KeyCode.Return)) + { + isOpen = true; //whole panel is open + showingMessage = false; //We don't want to time out + + panelGO.SetActive(isOpen); + textInputGO.SetActive(isOpen); + + chatInputIF.ActivateInputField(); + //InputFocusManager.Instance.TakeKeyboardFocus(); + player.Locomotion.inputEnabled = false; + + } + else if (isOpen && Input.GetKeyDown(KeyCode.Escape) || Input.GetKeyDown(KeyCode.Return)) + { + isOpen = false; + if (showingMessage) + { + textInputGO.SetActive(isOpen); + //InputFocusManager.Instance.ReleaseKeyboardFocus(); + player.Locomotion.inputEnabled = true; + } + else + { + panelGO.SetActive(isOpen); + } + } + + if (showingMessage) + { + timeOut += Time.deltaTime; + + if (timeOut >= MESSAGE_TIMEOUT) + { + showingMessage = false ; + panelGO.SetActive(false); + } + } + } + + public void SendChat(string text) + { + if (text.Trim().Length > 0) + { + if (messageList.Count > MAX_MESSAGES) + { + messageList.RemoveAt(0); + } + + Message newMessage = new Message(text); + messageList.Add(newMessage); + + GameObject messageObj = Instantiate(messagePrefab, chatPanel); + messageObj.GetComponent().text = text; + + } + + chatInputIF.text = ""; + timeOut = 0; + showingMessage = true; + textInputGO.SetActive(false); + + return; + } + + public void ReceiveMessage(Message received) + { + + } + + + #region UI + + private void BuildUI() + { + GameObject scrollViewPrefab = null; + GameObject inputPrefab; + + //get prefabs + PopupNotificationReferences popup = GameObject.FindObjectOfType(); + SaveLoadController saveLoad = GameObject.FindObjectOfType(); + + if (popup == null) + { + Debug.Log("Could not find PopupNotificationReferences"); + return; + } + else + { + inputPrefab = popup.popupTextInput.FindChildByName("TextFieldTextIcon");//MainMenuThingsAndStuff.Instance.renamePopupPrefab.gameObject.FindChildByName("TextFieldTextIcon"); + } + + if (saveLoad == null) + { + Debug.Log("Could not find SaveLoadController, attempting to instanciate"); + AppUtil.Instance.PauseGame(); + + Debug.Log("Paused"); + + saveLoad = FindObjectOfType().saveLoadController; + + if (saveLoad == null) + { + Debug.Log("Failed to get SaveLoadController"); + } + else + { + Debug.Log("Made a SaveLoadController!"); + scrollViewPrefab = saveLoad.FindChildByName("Scroll View"); + + if (scrollViewPrefab == null) + { + Debug.Log("Could not find scrollViewPrefab"); + + } + else + { + scrollViewPrefab = Instantiate(scrollViewPrefab); + } + } + + AppUtil.Instance.UnpauseGame(); + } + else + { + scrollViewPrefab = saveLoad.FindChildByName("Scroll View"); + } + + + /// + + if (inputPrefab == null) + { + Debug.Log("Could not find inputPrefab"); + return; + } + if (scrollViewPrefab == null) + { + Debug.Log("Could not find scrollViewPrefab"); + return; + } + + + //Add an input box + textInputGO = Instantiate(inputPrefab); + textInputGO.name = "Chat Input"; + textInputGO.transform.SetParent(panelGO.transform, false); + + //Remove redundant components + GameObject.Destroy(textInputGO.FindChildByName("icon")); + GameObject.Destroy(textInputGO.FindChildByName("image select")); + GameObject.Destroy(textInputGO.FindChildByName("image hover")); + GameObject.Destroy(textInputGO.FindChildByName("image click")); + + //Position input + RectTransform textInputRT = textInputGO.GetComponent(); + textInputRT.pivot = Vector3.zero; + textInputRT.anchorMin = Vector2.zero; + textInputRT.anchorMax = new Vector2(1f, 0); + + textInputRT.SetInsetAndSizeFromParentEdge(RectTransform.Edge.Bottom, 0, 20f); + textInputRT.SetInsetAndSizeFromParentEdge(RectTransform.Edge.Left, 0, 1f); + + RectTransform panelRT = panelGO.GetComponent(); + textInputRT.sizeDelta = new Vector2 (panelRT.rect.width, 40f); + + //Setup input + chatInputIF = textInputGO.GetComponent(); + textInputGO.FindChildByName("text [noloc]").GetComponent().fontSize = 18; + chatInputIF.placeholder.GetComponent().text = "Type a message and press Enter!"; + + + + + //Add a new scroll pane + scrollViewGO = Instantiate(scrollViewPrefab); + scrollViewGO.name = "Chat Scroll"; + scrollViewGO.transform.SetParent(panelGO.transform, false); + + //Position scroll pane + RectTransform scrollViewRT = scrollViewGO.GetComponent(); + scrollViewRT.pivot = Vector3.zero; + scrollViewRT.anchorMin = Vector2.zero; + scrollViewRT.anchorMax = new Vector2(1f, 0); + + scrollViewRT.SetInsetAndSizeFromParentEdge(RectTransform.Edge.Bottom, textInputRT.rect.height, 20f); + scrollViewRT.SetInsetAndSizeFromParentEdge(RectTransform.Edge.Left, 0, 1f); + + scrollViewRT.sizeDelta = new Vector2(panelRT.rect.width, panelRT.rect.height - textInputRT.rect.height); + + + //Setup scroll pane + GameObject viewport = scrollViewGO.FindChildByName("Viewport"); + RectTransform viewportRT = viewport.GetComponent(); + ScrollRect scrollRect = scrollViewGO.GetComponent(); + + viewportRT.pivot = new Vector2(0.5f, 0.5f); + viewportRT.anchorMin = Vector2.zero; + viewportRT.anchorMax = Vector2.one; + viewportRT.offsetMin = Vector2.zero; + viewportRT.offsetMax = Vector2.zero; + + scrollRect.viewport = scrollViewRT; + + //set up content + GameObject.Destroy(scrollViewGO.FindChildByName("GRID VIEW").gameObject); + GameObject content = new GameObject("Content", typeof(RectTransform), typeof(ContentSizeFitter), typeof(VerticalLayoutGroup)); + content.transform.SetParent(viewport.transform, false); + + ContentSizeFitter contentSF = content.GetComponent(); + contentSF.verticalFit = ContentSizeFitter.FitMode.PreferredSize; + + VerticalLayoutGroup contentVLG = content.GetComponent(); + contentVLG.childAlignment = TextAnchor.LowerLeft; + contentVLG.childControlWidth = false; + contentVLG.childControlHeight = true; + contentVLG.childForceExpandWidth = true; + contentVLG.childForceExpandHeight = false; + + chatPanel = content.GetComponent(); + chatPanel.pivot = Vector2.zero; + chatPanel.anchorMin = Vector2.zero; + chatPanel.anchorMax = new Vector2(1f, 0f); + chatPanel.offsetMin = Vector2.zero; + chatPanel.offsetMax = Vector2.zero; + scrollRect.content = chatPanel; + + chatPanel.SetInsetAndSizeFromParentEdge(RectTransform.Edge.Left, MESSAGE_INSET, chatPanel.rect.width - MESSAGE_INSET); + + //Realign vertical scroll bar + RectTransform scrollBarRT = scrollRect.verticalScrollbar.transform.GetComponent(); + Vector3 origPos = scrollBarRT.localPosition; + + scrollBarRT.localPosition = new Vector3(origPos.x, viewportRT.rect.height, origPos.z); + scrollBarRT.sizeDelta = new Vector2(scrollBarRT.sizeDelta.x, viewportRT.rect.height); + + + + + //Build message prefab + messagePrefab = new GameObject("Message Text", typeof(TextMeshProUGUI)); + + RectTransform messagePrefabRT = messagePrefab.GetComponent(); + messagePrefabRT.pivot = new Vector2(0.5f, 0.5f); + messagePrefabRT.anchorMin = new Vector2(0f, 1f); + messagePrefabRT.anchorMax = new Vector2(0f, 1f); + messagePrefabRT.offsetMin = new Vector2(0f, 0f); + messagePrefabRT.offsetMax = Vector2.zero; + messagePrefabRT.sizeDelta = new Vector2(chatPanel.rect.width, messagePrefabRT.rect.height); + + TextMeshProUGUI messageTM = messagePrefab.GetComponent(); + messageTM.textWrappingMode = TextWrappingModes.Normal; + messageTM.fontSize = 18; + messageTM.text = "Morm: Hurry up!"; + } + #endregion +} + +public class Message +{ + public string text; + + public Message(string text) { + this.text = text; + } +} diff --git a/Multiplayer/Multiplayer.csproj b/Multiplayer/Multiplayer.csproj index a2e5fb9..a10601f 100644 --- a/Multiplayer/Multiplayer.csproj +++ b/Multiplayer/Multiplayer.csproj @@ -78,7 +78,9 @@ + + diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index b44d387..5a158b3 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -14,6 +14,7 @@ using Multiplayer.Components.MainMenu; using Multiplayer.Components.Networking; using Multiplayer.Components.Networking.Train; +using Multiplayer.Components.Networking.UI; using Multiplayer.Components.Networking.World; using Multiplayer.Components.SaveGame; using Multiplayer.Networking.Data; @@ -44,12 +45,15 @@ public class NetworkClient : NetworkManager public int Ping { get; private set; } private NetPeer serverPeer; + private ChatGUI chatGUI; + public bool isSinglePlayer; + public NetworkClient(Settings settings) : base(settings) { PlayerManager = new ClientPlayerManager(); } - public void Start(string address, int port, string password) + public void Start(string address, int port, string password, bool isSinglePlayer) { netManager.Start(); ServerboundClientLoginPacket serverboundClientLoginPacket = new() { @@ -308,6 +312,16 @@ private void OnClientboundRemoveLoadingScreen(ClientboundRemoveLoadingScreenPack } displayLoadingInfo.OnLoadingFinished(); + + //if not single player, add in chat + GameObject common = GameObject.Find("[MAIN]/[GameUI]/[NewCanvasController]/Auxiliary Canvas, EventSystem, Input Module"); + if (common != null) + { + + GameObject chat = new GameObject("Chat GUI", typeof(ChatGUI)); + chat.transform.SetParent(common.transform, false); + + } } private void OnClientboundTimeAdvancePacket(ClientboundTimeAdvancePacket packet) From ea633a3b879eed03280681426d1c5adfc537191a Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 7 Jul 2024 22:35:05 +1000 Subject: [PATCH 27/34] Added hotbar blocking and aligned chat window with lower left corner --- .../Components/Networking/UI/ChatGUI.cs | 138 ++++++++---------- 1 file changed, 63 insertions(+), 75 deletions(-) diff --git a/Multiplayer/Components/Networking/UI/ChatGUI.cs b/Multiplayer/Components/Networking/UI/ChatGUI.cs index f35100f..800b48c 100644 --- a/Multiplayer/Components/Networking/UI/ChatGUI.cs +++ b/Multiplayer/Components/Networking/UI/ChatGUI.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using DV; using DV.UI; +using DV.UI.Inventory; using Multiplayer.Components.MainMenu; using Multiplayer.Utils; using TMPro; @@ -15,6 +16,9 @@ namespace Multiplayer.Components.Networking.UI; [RequireComponent(typeof(RectTransform))] public class ChatGUI : MonoBehaviour { + private const float PANEL_LEFT_MARGIN = 20f; + private const float PANEL_BOTTOM_MARGIN = 50f; + private const float MESSAGE_INSET = 15f; private const int MAX_MESSAGES = 50; private const int MESSAGE_TIMEOUT = 10; @@ -35,85 +39,33 @@ public class ChatGUI : MonoBehaviour private bool showingMessage = false; private CustomFirstPersonController player; + private HotbarController hotbarController; private float timeOut; private void Awake() { - Debug.Log("OverlayUI Awake() called"); - - // Create a new UI Canvas - GameObject canvasGO = new GameObject("OverlayCanvas"); - canvasGO.transform.SetParent(this.transform, false); - /* - Canvas canvas = canvasGO.AddComponent(); - canvas.renderMode = RenderMode.ScreenSpaceOverlay; - canvas.worldCamera = Camera.main; - canvas.sortingOrder = 100; // Ensure this canvas is rendered above others + Debug.Log("ChatGUI Awake() called"); - Debug.Log("Canvas created and configured"); + SetupOverlay(); //sizes and positions panel - // Add a CanvasScaler and GraphicRaycaster - CanvasScaler canvasScaler = canvasGO.AddComponent(); - canvasScaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize; - canvasScaler.referenceResolution = new Vector2(1920, 1080); - canvasGO.AddComponent(); + BuildUI(); //Creates input fields and scroll area - Debug.Log("CanvasScaler and GraphicRaycaster added"); - */ + panelGO.SetActive(false); //We don't need this to be visible when the game launches - // Create a Panel - panelGO = new GameObject("OverlayPanel"); - panelGO.transform.SetParent(canvasGO.transform, false); - RectTransform rectTransform = panelGO.AddComponent(); - rectTransform.sizeDelta = new Vector2(Screen.width * 0.3f, Screen.height * 0.3f); - rectTransform.anchorMin = Vector2.zero;//new Vector2(0.5f, 0.5f); - rectTransform.anchorMax = Vector2.zero;//new Vector2(0.5f, 0.5f); - rectTransform.pivot = new Vector2(0.5f, 0.5f); - rectTransform.anchoredPosition = Vector2.zero; - - // Debug.Log("Panel created and positioned"); - - // Add an Image component for coloring - /* - Image image = panelGO.AddComponent(); - image.color = new Color(1f, 0f, 0f, 0.5f); - - Debug.Log("Image component added and colored"); - */ - - //// Add a Text element to the panel - //GameObject textGO = new GameObject("TestText"); - //textGO.transform.SetParent(panelGO.transform, false); - //Text text = textGO.AddComponent(); - //text.text = "Overlay Test"; - ////text.font = Resources.GetBuiltinResource("Arial.ttf"); - ////text.alignment = TextAnchor.MiddleCenter; - //text.color = Color.white; - //RectTransform textRectTransform = textGO.GetComponent(); - //textRectTransform.sizeDelta = rectTransform.sizeDelta; - //textRectTransform.anchorMin = new Vector2(0.5f, 0.5f); - //textRectTransform.anchorMax = new Vector2(0.5f, 0.5f); - //textRectTransform.pivot = new Vector2(0.5f, 0.5f); - //textRectTransform.anchoredPosition = Vector2.zero; - - //Debug.Log("Test text added to panel"); - - - //this.GetComponent().worldCamera = Camera.main; - //this.GetComponent().renderMode = RenderMode.ScreenSpaceOverlay; - //this.GetComponent().uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize; - - BuildUI(); - panelGO.SetActive(false); - - //Find the player + //Find the player and toolbar so we can block input player = GameObject.FindObjectOfType(); if(player == null) { Debug.Log("Failed to find CustomFirstPersonController"); } + hotbarController = GameObject.FindObjectOfType(); + if (hotbarController == null) + { + Debug.Log("Failed to find HotbarController"); + } + } private void OnEnable() @@ -129,8 +81,8 @@ private void OnDisable() private void Update() { - //Debug.Log($"ChatGUI Update: isOpen {isOpen} Key Enter: {Input.GetKeyDown(KeyCode.Return)}"); - + + //Handle keypresses to open/close the chat window if (!isOpen && Input.GetKeyDown(KeyCode.Return)) { isOpen = true; //whole panel is open @@ -139,26 +91,31 @@ private void Update() panelGO.SetActive(isOpen); textInputGO.SetActive(isOpen); - chatInputIF.ActivateInputField(); - //InputFocusManager.Instance.TakeKeyboardFocus(); - player.Locomotion.inputEnabled = false; - + BlockInput(true); } - else if (isOpen && Input.GetKeyDown(KeyCode.Escape) || Input.GetKeyDown(KeyCode.Return)) + else if (isOpen && (Input.GetKeyDown(KeyCode.Escape) || Input.GetKeyDown(KeyCode.Return))) { isOpen = false; if (showingMessage) { textInputGO.SetActive(isOpen); - //InputFocusManager.Instance.ReleaseKeyboardFocus(); - player.Locomotion.inputEnabled = true; } else { panelGO.SetActive(isOpen); } + + BlockInput(false); + } + + //Maintain focus on the text input field + if(isOpen && !chatInputIF.isFocused) + { + chatInputIF.ActivateInputField(); } + //After a message is sent/received, keep displaying it for the timeout period + //Would be nice to add a fadeout in future if (showingMessage) { timeOut += Time.deltaTime; @@ -192,6 +149,7 @@ public void SendChat(string text) timeOut = 0; showingMessage = true; textInputGO.SetActive(false); + BlockInput(false); return; } @@ -204,6 +162,28 @@ public void ReceiveMessage(Message received) #region UI + private void SetupOverlay() + { + //Setup the host object + RectTransform myRT = this.transform.GetComponent(); + myRT.sizeDelta = new Vector2(Screen.width, Screen.height); + myRT.anchorMin = Vector2.zero; + myRT.anchorMax = Vector2.zero; + myRT.pivot = Vector2.zero; + myRT.anchoredPosition = Vector2.zero; + + + // Create a Panel + panelGO = new GameObject("OverlayPanel"); + panelGO.transform.SetParent(this.transform, false); + RectTransform rectTransform = panelGO.AddComponent(); + rectTransform.sizeDelta = new Vector2(Screen.width * 0.25f, Screen.height * 0.25f); + rectTransform.anchorMin = Vector2.zero; + rectTransform.anchorMax = Vector2.zero; + rectTransform.pivot = Vector2.zero; + rectTransform.anchoredPosition = new Vector2(PANEL_LEFT_MARGIN, PANEL_BOTTOM_MARGIN); + } + private void BuildUI() { GameObject scrollViewPrefab = null; @@ -259,8 +239,6 @@ private void BuildUI() scrollViewPrefab = saveLoad.FindChildByName("Scroll View"); } - - /// if (inputPrefab == null) { @@ -299,6 +277,7 @@ private void BuildUI() //Setup input chatInputIF = textInputGO.GetComponent(); + chatInputIF.onFocusSelectAll = false; textInputGO.FindChildByName("text [noloc]").GetComponent().fontSize = 18; chatInputIF.placeholder.GetComponent().text = "Type a message and press Enter!"; @@ -386,12 +365,21 @@ private void BuildUI() messageTM.fontSize = 18; messageTM.text = "Morm: Hurry up!"; } + + private void BlockInput(bool block) + { + player.Locomotion.inputEnabled = !block; + hotbarController.enabled = !block; + } + + #endregion } public class Message { public string text; + public GameObject message; public Message(string text) { this.text = text; From 2d3d2df9d1ce42e72a0010bb5c8d99187f53f322 Mon Sep 17 00:00:00 2001 From: AMacro Date: Fri, 12 Jul 2024 22:18:55 +1000 Subject: [PATCH 28/34] Bug fixes --- .../Components/Networking/UI/ChatGUI.cs | 87 +++++++++++-------- .../Managers/Client/NetworkClient.cs | 19 +++- .../Networking/Managers/Server/ChatManager.cs | 17 ++++ .../Managers/Server/NetworkServer.cs | 11 +++ .../Packets/Common/CommonChatPacket.cs | 22 +++++ info.json | 7 +- 6 files changed, 121 insertions(+), 42 deletions(-) create mode 100644 Multiplayer/Networking/Managers/Server/ChatManager.cs create mode 100644 Multiplayer/Networking/Packets/Common/CommonChatPacket.cs diff --git a/Multiplayer/Components/Networking/UI/ChatGUI.cs b/Multiplayer/Components/Networking/UI/ChatGUI.cs index 800b48c..e2ff62b 100644 --- a/Multiplayer/Components/Networking/UI/ChatGUI.cs +++ b/Multiplayer/Components/Networking/UI/ChatGUI.cs @@ -3,11 +3,13 @@ using DV; using DV.UI; using DV.UI.Inventory; -using Multiplayer.Components.MainMenu; using Multiplayer.Utils; +using Multiplayer.Networking.Packets.Common; using TMPro; using UnityEngine; using UnityEngine.UI; +using static System.Net.Mime.MediaTypeNames; + namespace Multiplayer.Components.Networking.UI; @@ -25,7 +27,7 @@ public class ChatGUI : MonoBehaviour private GameObject messagePrefab; - public List messageList = new List(); + public List messageList = new List(); private TMP_InputField chatInputIF; private ScrollRect scrollRect; @@ -42,6 +44,7 @@ public class ChatGUI : MonoBehaviour private HotbarController hotbarController; private float timeOut; + private float testTimeOut; private void Awake() { @@ -52,25 +55,28 @@ private void Awake() BuildUI(); //Creates input fields and scroll area panelGO.SetActive(false); //We don't need this to be visible when the game launches + textInputGO.SetActive(false); //Find the player and toolbar so we can block input player = GameObject.FindObjectOfType(); if(player == null) { Debug.Log("Failed to find CustomFirstPersonController"); + return; } hotbarController = GameObject.FindObjectOfType(); if (hotbarController == null) { Debug.Log("Failed to find HotbarController"); + return; } } private void OnEnable() { - chatInputIF.onSubmit.AddListener(SendChat); + chatInputIF.onSubmit.AddListener(Submit); } @@ -81,9 +87,8 @@ private void OnDisable() private void Update() { - //Handle keypresses to open/close the chat window - if (!isOpen && Input.GetKeyDown(KeyCode.Return)) + if (!isOpen && Input.GetKeyDown(KeyCode.Return) && !AppUtil.Instance.IsPauseMenuOpen) { isOpen = true; //whole panel is open showingMessage = false; //We don't want to time out @@ -116,7 +121,7 @@ private void Update() //After a message is sent/received, keep displaying it for the timeout period //Would be nice to add a fadeout in future - if (showingMessage) + if (showingMessage && !textInputGO.activeSelf) { timeOut += Time.deltaTime; @@ -126,23 +131,22 @@ private void Update() panelGO.SetActive(false); } } + + //testTimeOut += Time.deltaTime; + //if (testTimeOut >= 60) + //{ + // testTimeOut = 0; + // ReceiveMessage("Morm: Test TimeOut"); + //} } - public void SendChat(string text) + public void Submit(string text) { if (text.Trim().Length > 0) { - if (messageList.Count > MAX_MESSAGES) - { - messageList.RemoveAt(0); - } - - Message newMessage = new Message(text); - messageList.Add(newMessage); - - GameObject messageObj = Instantiate(messagePrefab, chatPanel); - messageObj.GetComponent().text = text; - + //add locally + AddMessage("You: " + text + ""); + NetworkLifecycle.Instance.Client.SendChat(text, MessageType.Chat,null); } chatInputIF.text = ""; @@ -154,9 +158,32 @@ public void SendChat(string text) return; } - public void ReceiveMessage(Message received) + public void ReceiveMessage(string message) + { + + if (message.Trim().Length > 0) + { + //add locally + AddMessage(message); + } + + timeOut = 0; + showingMessage = true; + + panelGO.SetActive(true); + } + + private void AddMessage(string text) { + if (messageList.Count > MAX_MESSAGES) + { + GameObject.Destroy(messageList[0]); + messageList.RemoveAt(0); + } + GameObject newMessage = Instantiate(messagePrefab, chatPanel); + newMessage.GetComponent().text = text; + messageList.Add(newMessage); } @@ -200,7 +227,7 @@ private void BuildUI() } else { - inputPrefab = popup.popupTextInput.FindChildByName("TextFieldTextIcon");//MainMenuThingsAndStuff.Instance.renamePopupPrefab.gameObject.FindChildByName("TextFieldTextIcon"); + inputPrefab = popup.popupTextInput.FindChildByName("TextFieldTextIcon"); } if (saveLoad == null) @@ -279,6 +306,7 @@ private void BuildUI() chatInputIF = textInputGO.GetComponent(); chatInputIF.onFocusSelectAll = false; textInputGO.FindChildByName("text [noloc]").GetComponent().fontSize = 18; + chatInputIF.placeholder.GetComponent().richText = false; chatInputIF.placeholder.GetComponent().text = "Type a message and press Enter!"; @@ -304,7 +332,7 @@ private void BuildUI() //Setup scroll pane GameObject viewport = scrollViewGO.FindChildByName("Viewport"); RectTransform viewportRT = viewport.GetComponent(); - ScrollRect scrollRect = scrollViewGO.GetComponent(); + ScrollRect scrollRect = scrollViewGO.GetComponent(); viewportRT.pivot = new Vector2(0.5f, 0.5f); viewportRT.anchorMin = Vector2.zero; @@ -341,11 +369,7 @@ private void BuildUI() //Realign vertical scroll bar RectTransform scrollBarRT = scrollRect.verticalScrollbar.transform.GetComponent(); - Vector3 origPos = scrollBarRT.localPosition; - - scrollBarRT.localPosition = new Vector3(origPos.x, viewportRT.rect.height, origPos.z); - scrollBarRT.sizeDelta = new Vector2(scrollBarRT.sizeDelta.x, viewportRT.rect.height); - + scrollBarRT.SetInsetAndSizeFromParentEdge(RectTransform.Edge.Top, 0, scrollViewRT.rect.height); @@ -372,16 +396,5 @@ private void BlockInput(bool block) hotbarController.enabled = !block; } - #endregion } - -public class Message -{ - public string text; - public GameObject message; - - public Message(string text) { - this.text = text; - } -} diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 5a158b3..35f93f9 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -110,6 +110,7 @@ protected override void Subscribe() netPacketProcessor.SubscribeReusable(OnClientboundLicenseAcquiredPacket); netPacketProcessor.SubscribeReusable(OnClientboundGarageUnlockPacket); netPacketProcessor.SubscribeReusable(OnClientboundDebtStatusPacket); + netPacketProcessor.SubscribeReusable(OnCommonChatPacket); } #region Net Events @@ -317,10 +318,10 @@ private void OnClientboundRemoveLoadingScreen(ClientboundRemoveLoadingScreenPack GameObject common = GameObject.Find("[MAIN]/[GameUI]/[NewCanvasController]/Auxiliary Canvas, EventSystem, Input Module"); if (common != null) { - + // GameObject chat = new GameObject("Chat GUI", typeof(ChatGUI)); chat.transform.SetParent(common.transform, false); - + chatGUI = chat.GetComponent(); } } @@ -606,6 +607,11 @@ private void OnClientboundDebtStatusPacket(ClientboundDebtStatusPacket packet) { CareerManagerDebtControllerPatch.HasDebt = packet.HasDebt; } + private void OnCommonChatPacket(CommonChatPacket packet) + { + + chatGUI.ReceiveMessage(packet.message); + } #endregion @@ -805,5 +811,14 @@ public void SendLicensePurchaseRequest(string id, bool isJobLicense) }, DeliveryMethod.ReliableUnordered); } + public void SendChat(string message, MessageType type, string whisperTo) + { + SendPacketToServer(new CommonChatPacket + { + message = message, + type = type, + }, DeliveryMethod.ReliableUnordered); + } + #endregion } diff --git a/Multiplayer/Networking/Managers/Server/ChatManager.cs b/Multiplayer/Networking/Managers/Server/ChatManager.cs new file mode 100644 index 0000000..87b042c --- /dev/null +++ b/Multiplayer/Networking/Managers/Server/ChatManager.cs @@ -0,0 +1,17 @@ +using LiteNetLib; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Multiplayer.Networking.Managers.Server; + +public class ChatManager +{ + public void ProcessMessage(string message, NetPeer peer) + { + + } + +} diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index 842bc17..db0ebd6 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -104,6 +104,7 @@ protected override void Subscribe() netPacketProcessor.SubscribeReusable(OnCommonHandbrakePositionPacket); netPacketProcessor.SubscribeReusable(OnCommonTrainPortsPacket); netPacketProcessor.SubscribeReusable(OnCommonTrainFusesPacket); + netPacketProcessor.SubscribeReusable(OnCommonChatPacket); } private void OnLoaded() @@ -675,5 +676,15 @@ private void OnServerboundLicensePurchaseRequestPacket(ServerboundLicensePurchas LicenseManager.Instance.AcquireGeneralLicense(generalLicense); } + private void OnCommonChatPacket(CommonChatPacket packet, NetPeer peer) + { + + if (TryGetServerPlayer(peer, out var player)) + { + packet.message = "" + player.Username + ": " + packet.message + ""; + SendPacketToAll(packet, DeliveryMethod.ReliableUnordered, peer); + } + + } #endregion } diff --git a/Multiplayer/Networking/Packets/Common/CommonChatPacket.cs b/Multiplayer/Networking/Packets/Common/CommonChatPacket.cs new file mode 100644 index 0000000..5f4d235 --- /dev/null +++ b/Multiplayer/Networking/Packets/Common/CommonChatPacket.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Multiplayer.Networking.Packets.Common; + +public class CommonChatPacket +{ + + public string message { get; set; } + public MessageType type { get; set; } + +} + +public enum MessageType +{ + ServerMessage, + Chat, + Whisper +} diff --git a/info.json b/info.json index 03d0d3e..37a82f6 100644 --- a/info.json +++ b/info.json @@ -1,9 +1,10 @@ { "Id": "Multiplayer", - "Version": "0.1.5", + "Version": "0.1.5.0", "DisplayName": "Multiplayer", - "Author": "Insprill", + "Author": "Insprill, Macka, Morm", "EntryMethod": "Multiplayer.Multiplayer.Load", "ManagerVersion": "0.27.3", - "LoadAfter": [ "RemoteDispatch" ] + "LoadAfter": [ "RemoteDispatch" ], + "Repository": "https://www.andrewcraigmackenzie.com/unitymods/Releases.json" } From 14e5aaec0815483b9d00d4c04aba2933c40a01a0 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 13 Jul 2024 16:40:48 +1000 Subject: [PATCH 29/34] Added chat commands Added server messages Added whispers Added help (displays commands) --- .../Components/Networking/UI/ChatGUI.cs | 185 ++++++++++++++-- .../Networking/Managers/Server/ChatManager.cs | 199 +++++++++++++++++- .../Managers/Server/NetworkServer.cs | 41 +++- 3 files changed, 392 insertions(+), 33 deletions(-) diff --git a/Multiplayer/Components/Networking/UI/ChatGUI.cs b/Multiplayer/Components/Networking/UI/ChatGUI.cs index e2ff62b..2c28f01 100644 --- a/Multiplayer/Components/Networking/UI/ChatGUI.cs +++ b/Multiplayer/Components/Networking/UI/ChatGUI.cs @@ -8,6 +8,11 @@ using TMPro; using UnityEngine; using UnityEngine.UI; +using System.Text.RegularExpressions; +using DV.Common; +using System.Collections; +using CommandTerminal; +using Multiplayer.Networking.Managers.Server; using static System.Net.Mime.MediaTypeNames; @@ -18,12 +23,16 @@ namespace Multiplayer.Components.Networking.UI; [RequireComponent(typeof(RectTransform))] public class ChatGUI : MonoBehaviour { - private const float PANEL_LEFT_MARGIN = 20f; - private const float PANEL_BOTTOM_MARGIN = 50f; + private const float PANEL_LEFT_MARGIN = 20f; //How far to inset the chat window from the left edge of the screen + private const float PANEL_BOTTOM_MARGIN = 50f; //How far to inset the chat window from the bottom of the screen + private const float PANEL_FADE_DURATION = 1f; + private const float MESSAGE_INSET = 15f; //How far to inset the message text from the edge of chat the window + + private const int MESSAGE_MAX_HISTORY = 50; //Maximum messages to keep in the queue + private const int MESSAGE_TIMEOUT = 10; //Maximum time to show an incoming message before fade + private const int MESSAGE_MAX_LENGTH = 500; //Maximum length of a single message + private const int MESSAGE_RATE_LIMIT = 10; //Limit how quickly a user can send messages (also enforced server side) - private const float MESSAGE_INSET = 15f; - private const int MAX_MESSAGES = 50; - private const int MESSAGE_TIMEOUT = 10; private GameObject messagePrefab; @@ -32,6 +41,7 @@ public class ChatGUI : MonoBehaviour private TMP_InputField chatInputIF; private ScrollRect scrollRect; private RectTransform chatPanel; + private CanvasGroup canvasGroup; private GameObject panelGO; private GameObject textInputGO; @@ -46,6 +56,8 @@ public class ChatGUI : MonoBehaviour private float timeOut; private float testTimeOut; + private GameFeatureFlags.Flag denied; + private void Awake() { Debug.Log("ChatGUI Awake() called"); @@ -93,7 +105,8 @@ private void Update() isOpen = true; //whole panel is open showingMessage = false; //We don't want to time out - panelGO.SetActive(isOpen); + //panelGO.SetActive(isOpen); + ShowPanel(); textInputGO.SetActive(isOpen); BlockInput(true); @@ -107,7 +120,9 @@ private void Update() } else { - panelGO.SetActive(isOpen); + //panelGO.SetActive(isOpen); + HidePanel(); + } BlockInput(false); @@ -128,11 +143,12 @@ private void Update() if (timeOut >= MESSAGE_TIMEOUT) { showingMessage = false ; - panelGO.SetActive(false); + //panelGO.SetActive(false); + HidePanel(); } } - //testTimeOut += Time.deltaTime; + ////testTimeOut += Time.deltaTime; //if (testTimeOut >= 60) //{ // testTimeOut = 0; @@ -142,22 +158,94 @@ private void Update() public void Submit(string text) { - if (text.Trim().Length > 0) + text = text.Trim(); + + if (text.Length > 0) { + //Strip any injected formatting + text = Regex.Replace(text, "", string.Empty, RegexOptions.IgnoreCase); + + //check for whisper + if(CheckForWhisper(text, out string localMessage)) + { + AddMessage(localMessage); + } + else + { + AddMessage("You: " + text + ""); + } + //add locally - AddMessage("You: " + text + ""); NetworkLifecycle.Instance.Client.SendChat(text, MessageType.Chat,null); + + //reset any timeouts + timeOut = 0; + showingMessage = true; } chatInputIF.text = ""; - timeOut = 0; - showingMessage = true; + textInputGO.SetActive(false); BlockInput(false); return; } + private bool CheckForWhisper(string message, out string localMessage) + { + string peerName = ""; + + if (message.StartsWith("/")) + { + string command = message.Substring(1).Split(' ')[0]; + switch (command) + { + case ChatManager.COMMAND_WHISPER_SHORT: + localMessage = message.Substring(ChatManager.COMMAND_WHISPER_SHORT.Length + 2); + break; + case ChatManager.COMMAND_WHISPER: + localMessage = message.Substring(ChatManager.COMMAND_WHISPER.Length + 2); + break; + + //allow messages that are not whispers to go through + default: + localMessage = message; + return false; + } + + if (localMessage == null || localMessage == string.Empty) + { + localMessage = message; + return false; + } + + //Check if name is in Quotes e.g. '/w "Mr Noname" my message' + if (localMessage.StartsWith("\"")) + { + int endQuote = localMessage.Substring(1).IndexOf('"'); + if (endQuote == -1 || endQuote == 0) + { + localMessage = message; + return false; + } + + peerName = localMessage.Substring(1, endQuote); + localMessage = localMessage.Substring(peerName.Length + 3); + } + else + { + peerName = localMessage.Split(' ')[0]; + localMessage = localMessage.Substring(peerName.Length + 1); + } + + localMessage = "You (" + peerName + "): " + localMessage + ""; + return true; + } + + localMessage = message; + return false; + } + public void ReceiveMessage(string message) { @@ -170,12 +258,13 @@ public void ReceiveMessage(string message) timeOut = 0; showingMessage = true; - panelGO.SetActive(true); + ShowPanel(); + //panelGO.SetActive(true); } private void AddMessage(string text) { - if (messageList.Count > MAX_MESSAGES) + if (messageList.Count > MESSAGE_MAX_HISTORY) { GameObject.Destroy(messageList[0]); messageList.RemoveAt(0); @@ -184,11 +273,42 @@ private void AddMessage(string text) GameObject newMessage = Instantiate(messagePrefab, chatPanel); newMessage.GetComponent().text = text; messageList.Add(newMessage); + + scrollRect.verticalNormalizedPosition = 0f; //scroll to the bottom - maybe later we need some logic for this? } #region UI + + public void ShowPanel() + { + StopCoroutine(FadeOut()); + panelGO.SetActive(true); + canvasGroup.alpha = 1f; + } + + public void HidePanel() + { + StartCoroutine(FadeOut()); + } + + private IEnumerator FadeOut() + { + float startAlpha = canvasGroup.alpha; + float elapsed = 0f; + + while (elapsed < PANEL_FADE_DURATION) + { + elapsed += Time.deltaTime; + canvasGroup.alpha = Mathf.Lerp(startAlpha, 0f, elapsed / PANEL_FADE_DURATION); + yield return null; + } + + canvasGroup.alpha = 0f; + panelGO.SetActive(false); + } + private void SetupOverlay() { //Setup the host object @@ -209,6 +329,8 @@ private void SetupOverlay() rectTransform.anchorMax = Vector2.zero; rectTransform.pivot = Vector2.zero; rectTransform.anchoredPosition = new Vector2(PANEL_LEFT_MARGIN, PANEL_BOTTOM_MARGIN); + + canvasGroup = panelGO.AddComponent(); // Add CanvasGroup for fade effect } private void BuildUI() @@ -305,10 +427,17 @@ private void BuildUI() //Setup input chatInputIF = textInputGO.GetComponent(); chatInputIF.onFocusSelectAll = false; - textInputGO.FindChildByName("text [noloc]").GetComponent().fontSize = 18; + chatInputIF.characterLimit = MESSAGE_MAX_LENGTH; + chatInputIF.richText=false; + + //Setup placeholder chatInputIF.placeholder.GetComponent().richText = false; chatInputIF.placeholder.GetComponent().text = "Type a message and press Enter!"; - + //Setup input renderer + TMP_Text chatInputRenderer = textInputGO.FindChildByName("text [noloc]").GetComponent(); + chatInputRenderer.fontSize = 18; + chatInputRenderer.richText = false; + chatInputRenderer.parseCtrlCharacters = false; @@ -332,7 +461,7 @@ private void BuildUI() //Setup scroll pane GameObject viewport = scrollViewGO.FindChildByName("Viewport"); RectTransform viewportRT = viewport.GetComponent(); - ScrollRect scrollRect = scrollViewGO.GetComponent(); + scrollRect = scrollViewGO.GetComponent(); viewportRT.pivot = new Vector2(0.5f, 0.5f); viewportRT.anchorMin = Vector2.zero; @@ -392,8 +521,24 @@ private void BuildUI() private void BlockInput(bool block) { - player.Locomotion.inputEnabled = !block; - hotbarController.enabled = !block; + //player.Locomotion.inputEnabled = !block; + //hotbarController.enabled = !block; + if (block) + { + denied = GameFeatureFlags.DeniedFlags; + + GameFeatureFlags.Deny(GameFeatureFlags.Flag.ALL); + CursorManager.Instance.RequestCursor(this, true); + //InputFocusManager.Instance.TakeKeyboardFocus(); + } + else + { + GameFeatureFlags.Allow(GameFeatureFlags.Flag.ALL); + GameFeatureFlags.Deny(denied); + CursorManager.Instance.RequestCursor(this, false); + + //InputFocusManager.Instance.ReleaseKeyboardFocus(); + } } #endregion diff --git a/Multiplayer/Networking/Managers/Server/ChatManager.cs b/Multiplayer/Networking/Managers/Server/ChatManager.cs index 87b042c..ffaf47d 100644 --- a/Multiplayer/Networking/Managers/Server/ChatManager.cs +++ b/Multiplayer/Networking/Managers/Server/ChatManager.cs @@ -1,17 +1,204 @@ using LiteNetLib; -using System; -using System.Collections.Generic; +using Multiplayer.Components.Networking; using System.Linq; -using System.Text; -using System.Threading.Tasks; +using Multiplayer.Networking.Data; +using System.Text.RegularExpressions; +using UnityEngine; namespace Multiplayer.Networking.Managers.Server; -public class ChatManager +public static class ChatManager { - public void ProcessMessage(string message, NetPeer peer) + public const string COMMAND_SERVER = "server"; + public const string COMMAND_SERVER_SHORT = "s"; + public const string COMMAND_WHISPER = "whisper"; + public const string COMMAND_WHISPER_SHORT = "w"; + public const string COMMAND_HELP_SHORT = "?"; + public const string COMMAND_HELP = "help"; + + public const string MESSAGE_COLOUR_SERVER = "9CDCFE"; + public const string MESSAGE_COLOUR_HELP = "00FF00"; + + public static void ProcessMessage(string message, NetPeer sender) + { + + if (message == null || message == string.Empty) + return; + + //Check we could find the sender player data + if (!NetworkLifecycle.Instance.Server.TryGetServerPlayer(sender, out var player)) + return; + + + //Check if we have a command + if (message.StartsWith("/")) + { + string command = message.Substring(1).Split(' ')[0]; + + switch (command) + { + case COMMAND_SERVER_SHORT: + ServerMessage(message, sender, null, COMMAND_SERVER_SHORT.Length); + break; + case COMMAND_SERVER: + ServerMessage(message, sender, null, COMMAND_SERVER.Length); + break; + + case COMMAND_WHISPER_SHORT: + WhisperMessage(message, COMMAND_WHISPER_SHORT.Length, player.Username, sender); + break; + case COMMAND_WHISPER: + WhisperMessage(message, COMMAND_WHISPER.Length, player.Username, sender); + break; + + case COMMAND_HELP_SHORT: + HelpMessage(sender); + break; + case COMMAND_HELP: + HelpMessage(sender); + break; + + //allow messages that are not commands to go through + default: + ChatMessage(message,player.Username, sender); + break; + } + + return; + + } + + //not a server command, process as normal message + ChatMessage(message, player.Username, sender); + } + + private static void ChatMessage(string message, string sender, NetPeer peer) + { + //clean up the message to stop format injection + message = Regex.Replace(message, "", string.Empty, RegexOptions.IgnoreCase); + + message = $"{sender}: {message}"; + NetworkLifecycle.Instance.Server.SendChat(message, peer); + } + + public static void ServerMessage(string message, NetPeer sender, NetPeer exclude = null, int commandLength =-1) + { + //If user is not the host, we should ignore - will require changes for dedicated server + if (!NetworkLifecycle.Instance.IsHost(sender)) + return; + + //Remove the command "/server" or "/s" + if (commandLength > 0) + { + message = message.Substring(commandLength + 2); + } + + message = $"{message}"; + NetworkLifecycle.Instance.Server.SendChat(message, exclude); + } + + private static void WhisperMessage(string message, int commandLength, string senderName, NetPeer sender) + { + NetPeer recipient = null; + string recipientName = ""; + + Multiplayer.Log($"Whispering: \"{message}\", sender: {senderName}, senderID: {sender?.Id}"); + + //Remove the command "/whisper" or "/w" + message = message.Substring(commandLength + 2); + + if (message == null || message == string.Empty) + return; + + //Check if name is in Quotes e.g. '/w "Mr Noname" my message' + if (message.StartsWith("\"")) + { + int endQuote = message.Substring(1).IndexOf('"'); + if (endQuote == -1 || endQuote == 0) + return; + + recipientName = message.Substring(1, endQuote); + + //Remove the peer name + message = message.Substring(recipientName.Length + 3); + } + else + { + recipientName = message.Split(' ')[0]; + + //Remove the peer name + message = message.Substring(recipientName.Length + 1); + } + + Multiplayer.Log($"Whispering parse 1: \"{message}\", sender: {senderName}, senderID: {sender?.Id}, peerName: {recipientName}"); + + //look up the peer ID + recipient = NetPeerFromName(recipientName); + if(recipient == null) + { + Multiplayer.Log($"Whispering failed: \"{message}\", sender: {senderName}, senderID: {sender?.Id}, peerName: {recipientName}"); + + message = $"{recipientName} not found - you're whispering into the void!"; + NetworkLifecycle.Instance.Server.SendWhisper(message, sender); + return; + } + + Multiplayer.Log($"Whispering parse 2: \"{message}\", sender: {senderName}, senderID: {sender?.Id}, peerName: {recipientName}, peerID: {recipient?.Id}"); + + //clean up the message to stop format injection + message = Regex.Replace(message, "", string.Empty, RegexOptions.IgnoreCase); + + message = "" + senderName + ": " + message + ""; + + NetworkLifecycle.Instance.Server.SendWhisper(message, recipient); + } + + private static void HelpMessage(NetPeer peer) { + string message = $"Available commands:" + + + "\r\n\r\n\tSend a message as the server (host only)" + + "\r\n\t\t/server " + + "\r\n\t\t/s " + + + "\r\n\r\n\tWhisper to a player" + + "\r\n\t\t/whisper " + + "\r\n\t\t/w " + + + "\r\n\r\n\tDisplay this help message" + + "\r\n\t\t/help" + + "\r\n\t\t/?" + + + ""; + + NetworkLifecycle.Instance.Server.SendWhisper(message, peer); + } + + + private static NetPeer NetPeerFromName(string peerName) + { + + if(peerName == null || peerName == string.Empty) + return null; + + ServerPlayer player = NetworkLifecycle.Instance.Server.ServerPlayers.Where(p => p.Username == peerName).FirstOrDefault(); + if (player == null) + return null; + + if(NetworkLifecycle.Instance.Server.TryGetNetPeer(player.Id, out NetPeer peer)) + { + return peer; + } + + return null; } + + + //if (NetworkLifecycle.Instance.Server.TryGetServerPlayer(peer, out var player)) + //{ + // message = "" + player.Username + ": " + message + ""; + // NetworkLifecycle.Instance.Server.SendPacketToAll(packet, DeliveryMethod.ReliableUnordered, peer); + //} } diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index db0ebd6..a645f9d 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -294,6 +294,37 @@ public void SendDebtStatus(bool hasDebt) }, DeliveryMethod.ReliableUnordered, selfPeer); } + public void SendChat(string message, NetPeer exclude = null) + { + + if (exclude != null) + { + NetworkLifecycle.Instance.Server.SendPacketToAll(new CommonChatPacket + { + message = message + }, DeliveryMethod.ReliableUnordered, exclude); + } + else + { + NetworkLifecycle.Instance.Server.SendPacketToAll(new CommonChatPacket + { + message = message + }, DeliveryMethod.ReliableUnordered); + } + } + + public void SendWhisper(string message, NetPeer recipient) + { + if(message != null || recipient != null) + { + NetworkLifecycle.Instance.Server.SendPacket(recipient, new CommonChatPacket + { + message = message + }, DeliveryMethod.ReliableUnordered); + } + + } + #endregion #region Listeners @@ -416,6 +447,8 @@ private void OnServerboundClientReadyPacket(ServerboundClientReadyPacket packet, }; SendPacketToAll(clientboundPlayerJoinedPacket, DeliveryMethod.ReliableOrdered, peer); + ChatManager.ServerMessage(serverPlayer.Username + " joined the game", null, peer); + Log($"Client {peer.Id} is ready. Sending world state"); // No need to sync the world state if the player is the host @@ -678,13 +711,7 @@ private void OnServerboundLicensePurchaseRequestPacket(ServerboundLicensePurchas private void OnCommonChatPacket(CommonChatPacket packet, NetPeer peer) { - - if (TryGetServerPlayer(peer, out var player)) - { - packet.message = "" + player.Username + ": " + packet.message + ""; - SendPacketToAll(packet, DeliveryMethod.ReliableUnordered, peer); - } - + ChatManager.ProcessMessage(packet.message,peer); } #endregion } From 9ee085c9766adc69a5e721a2a019ca263a451073 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 13 Jul 2024 17:52:24 +1000 Subject: [PATCH 30/34] Added sent message history --- .../Components/Networking/UI/ChatGUI.cs | 138 ++++++++++++------ .../Managers/Client/NetworkClient.cs | 5 +- .../Networking/Managers/Server/ChatManager.cs | 8 - .../Packets/Common/CommonChatPacket.cs | 8 - 4 files changed, 99 insertions(+), 60 deletions(-) diff --git a/Multiplayer/Components/Networking/UI/ChatGUI.cs b/Multiplayer/Components/Networking/UI/ChatGUI.cs index 2c28f01..a321335 100644 --- a/Multiplayer/Components/Networking/UI/ChatGUI.cs +++ b/Multiplayer/Components/Networking/UI/ChatGUI.cs @@ -28,15 +28,17 @@ public class ChatGUI : MonoBehaviour private const float PANEL_FADE_DURATION = 1f; private const float MESSAGE_INSET = 15f; //How far to inset the message text from the edge of chat the window - private const int MESSAGE_MAX_HISTORY = 50; //Maximum messages to keep in the queue + private const int MESSAGE_MAX_HISTORY = 50; //Maximum messages to keep in the queue private const int MESSAGE_TIMEOUT = 10; //Maximum time to show an incoming message before fade private const int MESSAGE_MAX_LENGTH = 500; //Maximum length of a single message private const int MESSAGE_RATE_LIMIT = 10; //Limit how quickly a user can send messages (also enforced server side) + private const int SEND_MAX_HISTORY = 10; //How many previous messages to remember private GameObject messagePrefab; - public List messageList = new List(); + private List messageList = new List(); + private List sendHistory = new List(); private TMP_InputField chatInputIF; private ScrollRect scrollRect; @@ -50,17 +52,21 @@ public class ChatGUI : MonoBehaviour private bool isOpen = false; private bool showingMessage = false; - private CustomFirstPersonController player; - private HotbarController hotbarController; + private int sendHistoryIndex = -1; + private bool whispering = false; + private string lastRecipient; - private float timeOut; - private float testTimeOut; + //private CustomFirstPersonController player; + //private HotbarController hotbarController; + + private float timeOut; //time-out counter for hiding the messages + //private float testTimeOut; private GameFeatureFlags.Flag denied; private void Awake() { - Debug.Log("ChatGUI Awake() called"); + Multiplayer.Log("ChatGUI Awake() called"); SetupOverlay(); //sizes and positions panel @@ -70,19 +76,21 @@ private void Awake() textInputGO.SetActive(false); //Find the player and toolbar so we can block input + /* player = GameObject.FindObjectOfType(); if(player == null) { - Debug.Log("Failed to find CustomFirstPersonController"); + Multiplayer.Log("Failed to find CustomFirstPersonController"); return; } hotbarController = GameObject.FindObjectOfType(); if (hotbarController == null) { - Debug.Log("Failed to find HotbarController"); + Multiplayer.Log("Failed to find HotbarController"); return; } + */ } @@ -105,28 +113,53 @@ private void Update() isOpen = true; //whole panel is open showingMessage = false; //We don't want to time out - //panelGO.SetActive(isOpen); ShowPanel(); textInputGO.SetActive(isOpen); + sendHistoryIndex = sendHistory.Count; + + if (whispering) + { + chatInputIF.text = "/w " + lastRecipient + ' '; + chatInputIF.caretPosition = chatInputIF.text.Length; + } + BlockInput(true); } - else if (isOpen && (Input.GetKeyDown(KeyCode.Escape) || Input.GetKeyDown(KeyCode.Return))) + else if (isOpen) { - isOpen = false; - if (showingMessage) + //Check for closing window + if (Input.GetKeyDown(KeyCode.Escape) || Input.GetKeyDown(KeyCode.Return)) { - textInputGO.SetActive(isOpen); - } - else - { - //panelGO.SetActive(isOpen); - HidePanel(); + isOpen = false; + if (showingMessage) + { + textInputGO.SetActive(isOpen); + } + else + { + HidePanel(); + } + BlockInput(false); + }else if (Input.GetKeyDown(KeyCode.UpArrow)) + { + sendHistoryIndex--; + if (sendHistory.Count > 0 && sendHistoryIndex < sendHistory.Count) + { + chatInputIF.text = sendHistory[sendHistoryIndex]; + chatInputIF.caretPosition = chatInputIF.text.Length; + } + }else if (Input.GetKeyDown(KeyCode.DownArrow)) + { + sendHistoryIndex++; + if (sendHistory.Count > 0 && sendHistoryIndex >= 0) + { + chatInputIF.text = sendHistory[sendHistoryIndex]; + chatInputIF.caretPosition = chatInputIF.text.Length; + } } - - BlockInput(false); - } + } //Maintain focus on the text input field if(isOpen && !chatInputIF.isFocused) @@ -166,17 +199,39 @@ public void Submit(string text) text = Regex.Replace(text, "", string.Empty, RegexOptions.IgnoreCase); //check for whisper - if(CheckForWhisper(text, out string localMessage)) + if(CheckForWhisper(text, out string localMessage, out string recipient)) { + whispering = true; + lastRecipient = recipient; + + if (lastRecipient.Contains(" ")) + { + lastRecipient = '"' + lastRecipient + '"'; + } + AddMessage(localMessage); } else { + whispering = false; AddMessage("You: " + text + ""); } - //add locally - NetworkLifecycle.Instance.Client.SendChat(text, MessageType.Chat,null); + //add to send history + if (sendHistory.Count >= SEND_MAX_HISTORY) + { + sendHistory.RemoveAt(0); + } + + //add to the history - if already there, we'll relocate it to the end + int exists = sendHistory.IndexOf(text); + if (exists != -1) + sendHistory.RemoveAt(exists); + + sendHistory.Add(text); + + //send to server + NetworkLifecycle.Instance.Client.SendChat(text); //reset any timeouts timeOut = 0; @@ -191,9 +246,10 @@ public void Submit(string text) return; } - private bool CheckForWhisper(string message, out string localMessage) + private bool CheckForWhisper(string message, out string localMessage, out string recipient) { - string peerName = ""; + recipient = ""; + if (message.StartsWith("/")) { @@ -229,16 +285,16 @@ private bool CheckForWhisper(string message, out string localMessage) return false; } - peerName = localMessage.Substring(1, endQuote); - localMessage = localMessage.Substring(peerName.Length + 3); + recipient = localMessage.Substring(1, endQuote); + localMessage = localMessage.Substring(recipient.Length + 3); } else { - peerName = localMessage.Split(' ')[0]; - localMessage = localMessage.Substring(peerName.Length + 1); + recipient = localMessage.Split(' ')[0]; + localMessage = localMessage.Substring(recipient.Length + 1); } - localMessage = "You (" + peerName + "): " + localMessage + ""; + localMessage = "You (" + recipient + "): " + localMessage + ""; return true; } @@ -264,7 +320,7 @@ public void ReceiveMessage(string message) private void AddMessage(string text) { - if (messageList.Count > MESSAGE_MAX_HISTORY) + if (messageList.Count >= MESSAGE_MAX_HISTORY) { GameObject.Destroy(messageList[0]); messageList.RemoveAt(0); @@ -344,7 +400,7 @@ private void BuildUI() if (popup == null) { - Debug.Log("Could not find PopupNotificationReferences"); + Multiplayer.Log("Could not find PopupNotificationReferences"); return; } else @@ -354,25 +410,25 @@ private void BuildUI() if (saveLoad == null) { - Debug.Log("Could not find SaveLoadController, attempting to instanciate"); + Multiplayer.Log("Could not find SaveLoadController, attempting to instanciate"); AppUtil.Instance.PauseGame(); - Debug.Log("Paused"); + Multiplayer.Log("Paused"); saveLoad = FindObjectOfType().saveLoadController; if (saveLoad == null) { - Debug.Log("Failed to get SaveLoadController"); + Multiplayer.Log("Failed to get SaveLoadController"); } else { - Debug.Log("Made a SaveLoadController!"); + Multiplayer.Log("Made a SaveLoadController!"); scrollViewPrefab = saveLoad.FindChildByName("Scroll View"); if (scrollViewPrefab == null) { - Debug.Log("Could not find scrollViewPrefab"); + Multiplayer.Log("Could not find scrollViewPrefab"); } else @@ -391,12 +447,12 @@ private void BuildUI() if (inputPrefab == null) { - Debug.Log("Could not find inputPrefab"); + Multiplayer.Log("Could not find inputPrefab"); return; } if (scrollViewPrefab == null) { - Debug.Log("Could not find scrollViewPrefab"); + Multiplayer.Log("Could not find scrollViewPrefab"); return; } diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 35f93f9..09d5c51 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -811,12 +811,11 @@ public void SendLicensePurchaseRequest(string id, bool isJobLicense) }, DeliveryMethod.ReliableUnordered); } - public void SendChat(string message, MessageType type, string whisperTo) + public void SendChat(string message) { SendPacketToServer(new CommonChatPacket { - message = message, - type = type, + message = message }, DeliveryMethod.ReliableUnordered); } diff --git a/Multiplayer/Networking/Managers/Server/ChatManager.cs b/Multiplayer/Networking/Managers/Server/ChatManager.cs index ffaf47d..d1c80a8 100644 --- a/Multiplayer/Networking/Managers/Server/ChatManager.cs +++ b/Multiplayer/Networking/Managers/Server/ChatManager.cs @@ -193,12 +193,4 @@ private static NetPeer NetPeerFromName(string peerName) return null; } - - - //if (NetworkLifecycle.Instance.Server.TryGetServerPlayer(peer, out var player)) - //{ - // message = "" + player.Username + ": " + message + ""; - // NetworkLifecycle.Instance.Server.SendPacketToAll(packet, DeliveryMethod.ReliableUnordered, peer); - //} - } diff --git a/Multiplayer/Networking/Packets/Common/CommonChatPacket.cs b/Multiplayer/Networking/Packets/Common/CommonChatPacket.cs index 5f4d235..1c511ad 100644 --- a/Multiplayer/Networking/Packets/Common/CommonChatPacket.cs +++ b/Multiplayer/Networking/Packets/Common/CommonChatPacket.cs @@ -10,13 +10,5 @@ public class CommonChatPacket { public string message { get; set; } - public MessageType type { get; set; } } - -public enum MessageType -{ - ServerMessage, - Chat, - Whisper -} From 13184b83e32eadd65a1fe35fc7d8cbc95f19b3a7 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 13 Jul 2024 21:46:34 +1000 Subject: [PATCH 31/34] Added auto complete for whisper usernames, enforced some username control Usernames must now be unique and cannot contain spaces (automatically replaced with underscores) - this is enforced by adding a number to the end if there is a conflict --- .../Components/Networking/UI/ChatGUI.cs | 104 +++++++++++++++--- Multiplayer/Networking/Data/ServerPlayer.cs | 1 + .../Networking/Managers/Server/ChatManager.cs | 11 +- .../Managers/Server/NetworkServer.cs | 15 ++- 4 files changed, 110 insertions(+), 21 deletions(-) diff --git a/Multiplayer/Components/Networking/UI/ChatGUI.cs b/Multiplayer/Components/Networking/UI/ChatGUI.cs index a321335..a5675d1 100644 --- a/Multiplayer/Components/Networking/UI/ChatGUI.cs +++ b/Multiplayer/Components/Networking/UI/ChatGUI.cs @@ -1,18 +1,17 @@ using System; using System.Collections.Generic; +using System.Linq; using DV; using DV.UI; -using DV.UI.Inventory; using Multiplayer.Utils; -using Multiplayer.Networking.Packets.Common; using TMPro; using UnityEngine; using UnityEngine.UI; using System.Text.RegularExpressions; using DV.Common; using System.Collections; -using CommandTerminal; using Multiplayer.Networking.Managers.Server; +using Multiplayer.Components.Networking.Player; using static System.Net.Mime.MediaTypeNames; @@ -97,12 +96,14 @@ private void Awake() private void OnEnable() { chatInputIF.onSubmit.AddListener(Submit); + chatInputIF.onValueChanged.AddListener(ChatInputChange); } private void OnDisable() { chatInputIF.onSubmit.RemoveAllListeners(); + chatInputIF.onValueChanged.RemoveAllListeners(); } private void Update() @@ -132,12 +133,9 @@ private void Update() if (Input.GetKeyDown(KeyCode.Escape) || Input.GetKeyDown(KeyCode.Return)) { isOpen = false; - if (showingMessage) + if (!showingMessage) { textInputGO.SetActive(isOpen); - } - else - { HidePanel(); } @@ -204,12 +202,15 @@ public void Submit(string text) whispering = true; lastRecipient = recipient; + if (localMessage == null || localMessage == string.Empty) + return; + if (lastRecipient.Contains(" ")) { lastRecipient = '"' + lastRecipient + '"'; } - AddMessage(localMessage); + AddMessage("You (" + recipient + "): " + localMessage + ""); } else { @@ -246,13 +247,75 @@ public void Submit(string text) return; } + private void ChatInputChange(string message) + { + Multiplayer.Log($"ChatInputChange({message})"); + + //allow the user to clear text + if(Input.GetKeyDown(KeyCode.Backspace) || Input.GetKeyDown(KeyCode.Delete)) + return; + + if (CheckForWhisper(message, out string localMessage, out string recipient)) + { + Multiplayer.Log($"ChatInputChange: message: \"{message}\", localMessage: \"{(localMessage == null ? "null" : localMessage)}" + + $"\", recipient: \"{(recipient == null ? "null" : recipient)}\""); + + if (localMessage == null || localMessage == string.Empty) + { + + string closestMatch = NetworkLifecycle.Instance.Client.PlayerManager.Players + .Where(player => player.Username.ToLower().StartsWith(recipient.ToLower())) + .OrderBy(player => player.Username.Length) + .ThenByDescending(player => player.Username) + .ToList() + .FirstOrDefault().Username; + + /* + Multiplayer.Log($"ChatInputChange: closesMatch: {(closestMatch == null? "null" : closestMatch.Username)}"); + + + if(closestMatch == null) + return; + + bool quoteFlag = false; + if (match.Contains(' ')) + { + match = '"' + match + '"'; + quoteFlag = true; + } + + Multiplayer.Log($"ChatInput: recipient {recipient}, qF: {quoteFlag}, match: {match}, compare {recipient == closestMatch}"); + */ + + //if we have a match, allow the client to type + if (closestMatch == null || recipient == closestMatch) + return; + + //update the textbox + chatInputIF.SetTextWithoutNotify("/w " + closestMatch); + + //Multiplayer.Log($"ChatInput: length {chatInputIF.text.Length}, anchor: {"/w ".Length + recipient.Length + (quoteFlag ? 1 : 0)}"); + + //select the trailing match chars + chatInputIF.caretPosition = chatInputIF.text.Length; // Set caret to end of text + //chatInputIF.selectionAnchorPosition = chatInputIF.text.Length - "/w ".Length - recipient.Length - (quoteFlag?1:0) + 1; + chatInputIF.selectionAnchorPosition = "/w ".Length + recipient.Length;// + (quoteFlag?1:0); + + + } + } + + } + private bool CheckForWhisper(string message, out string localMessage, out string recipient) { recipient = ""; + localMessage = ""; - if (message.StartsWith("/")) + if (message.StartsWith("/") && message.Length > (ChatManager.COMMAND_WHISPER_SHORT.Length + 2)) { + Multiplayer.Log("CheckForWhisper() starts with /"); string command = message.Substring(1).Split(' ')[0]; switch (command) { @@ -275,26 +338,39 @@ private bool CheckForWhisper(string message, out string localMessage, out string return false; } + /* //Check if name is in Quotes e.g. '/w "Mr Noname" my message' if (localMessage.StartsWith("\"")) { + Multiplayer.Log("CheckForWhisper() starts with \""); int endQuote = localMessage.Substring(1).IndexOf('"'); - if (endQuote == -1 || endQuote == 0) + Multiplayer.Log($"CheckForWhisper() starts with \" - indexOf, eQ: {endQuote}"); + if (endQuote <=1) { - localMessage = message; - return false; + recipient = localMessage.Substring(1); + localMessage = string.Empty;//message; + return true; } + Multiplayer.Log("CheckForWhisper() remove quote"); recipient = localMessage.Substring(1, endQuote); localMessage = localMessage.Substring(recipient.Length + 3); } else { - recipient = localMessage.Split(' ')[0]; + Multiplayer.Log("CheckForWhisper() no quote"); + */ + recipient = localMessage.Split(' ')[0]; + if (localMessage.Length > (recipient.Length + 2)) + { localMessage = localMessage.Substring(recipient.Length + 1); } + else + { + localMessage = ""; + } + //} - localMessage = "You (" + recipient + "): " + localMessage + ""; return true; } diff --git a/Multiplayer/Networking/Data/ServerPlayer.cs b/Multiplayer/Networking/Data/ServerPlayer.cs index 4e367e5..613f25e 100644 --- a/Multiplayer/Networking/Data/ServerPlayer.cs +++ b/Multiplayer/Networking/Data/ServerPlayer.cs @@ -9,6 +9,7 @@ public class ServerPlayer public byte Id { get; set; } public bool IsLoaded { get; set; } public string Username { get; set; } + public string OriginalUsername { get; set; } public Guid Guid { get; set; } public Vector3 RawPosition { get; set; } public float RawRotationY { get; set; } diff --git a/Multiplayer/Networking/Managers/Server/ChatManager.cs b/Multiplayer/Networking/Managers/Server/ChatManager.cs index d1c80a8..bee85ed 100644 --- a/Multiplayer/Networking/Managers/Server/ChatManager.cs +++ b/Multiplayer/Networking/Managers/Server/ChatManager.cs @@ -84,7 +84,7 @@ private static void ChatMessage(string message, string sender, NetPeer peer) public static void ServerMessage(string message, NetPeer sender, NetPeer exclude = null, int commandLength =-1) { //If user is not the host, we should ignore - will require changes for dedicated server - if (!NetworkLifecycle.Instance.IsHost(sender)) + if (sender !=null && !NetworkLifecycle.Instance.IsHost(sender)) return; //Remove the command "/server" or "/s" @@ -99,8 +99,8 @@ public static void ServerMessage(string message, NetPeer sender, NetPeer exclude private static void WhisperMessage(string message, int commandLength, string senderName, NetPeer sender) { - NetPeer recipient = null; - string recipientName = ""; + NetPeer recipient; + string recipientName; Multiplayer.Log($"Whispering: \"{message}\", sender: {senderName}, senderID: {sender?.Id}"); @@ -110,6 +110,7 @@ private static void WhisperMessage(string message, int commandLength, string sen if (message == null || message == string.Empty) return; + /* //Check if name is in Quotes e.g. '/w "Mr Noname" my message' if (message.StartsWith("\"")) { @@ -123,12 +124,12 @@ private static void WhisperMessage(string message, int commandLength, string sen message = message.Substring(recipientName.Length + 3); } else - { + {*/ recipientName = message.Split(' ')[0]; //Remove the peer name message = message.Substring(recipientName.Length + 1); - } + //} Multiplayer.Log($"Whispering parse 1: \"{message}\", sender: {senderName}, senderID: {sender?.Id}, peerName: {recipientName}"); diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index a645f9d..055325a 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -331,7 +331,17 @@ public void SendWhisper(string message, NetPeer recipient) private void OnServerboundClientLoginPacket(ServerboundClientLoginPacket packet, ConnectionRequest request) { - packet.Username = packet.Username.Truncate(Settings.MAX_USERNAME_LENGTH); + // clean up username - remove leading/trailing white space, swap spaces for underscores and truncate + packet.Username = packet.Username.Trim().Replace(' ', '_').Truncate(Settings.MAX_USERNAME_LENGTH); + string overrideUsername = packet.Username; + + //ensure the username is unique + int uniqueName = ServerPlayers.Where(player => player.OriginalUsername.ToLower() == packet.Username.ToLower()).Count(); + + if (uniqueName > 0) + { + overrideUsername += uniqueName; + } Guid guid; try @@ -398,7 +408,8 @@ private void OnServerboundClientLoginPacket(ServerboundClientLoginPacket packet, ServerPlayer serverPlayer = new() { Id = (byte)peer.Id, - Username = packet.Username, + Username = overrideUsername, + OriginalUsername = packet.Username, Guid = guid }; From 99257833a0ceb948c8444140e6276594d557fb04 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 14 Jul 2024 00:01:33 +1000 Subject: [PATCH 32/34] General tidy up and QoL for server browser --- .../Components/MainMenu/HostGamePane.cs | 8 ++- .../ServerBrowserDummyElement.cs | 62 +++++++++++++++++++ .../ServerBrowser/ServerBrowserElement.cs | 6 ++ .../ServerBrowser/ServerBrowserGridView.cs | 24 +++---- .../Components/MainMenu/ServerBrowserPane.cs | 16 ++++- Multiplayer/Locale.cs | 2 + locale.csv | 3 + 7 files changed, 106 insertions(+), 15 deletions(-) create mode 100644 Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserDummyElement.cs diff --git a/Multiplayer/Components/MainMenu/HostGamePane.cs b/Multiplayer/Components/MainMenu/HostGamePane.cs index f50484c..a072dd1 100644 --- a/Multiplayer/Components/MainMenu/HostGamePane.cs +++ b/Multiplayer/Components/MainMenu/HostGamePane.cs @@ -144,6 +144,12 @@ private void BuildUI() titleObj.GetComponentInChildren().key = Locale.SERVER_HOST__TITLE_KEY; titleObj.GetComponentInChildren().UpdateLocalization(); + //update right hand info pane (this will be used later for more settings or information + GameObject serverWindowGO = this.FindChildByName("Save Description"); + GameObject serverDetailsGO = serverWindowGO.FindChildByName("text list [noloc]"); + serverWindowGO.name = "Host Details"; + serverDetailsGO.GetComponent().text = ""; + //Find scrolling viewport ScrollRect scroller = this.FindChildByName("Scroll View").GetComponent(); @@ -194,7 +200,7 @@ private void BuildUI() go.name = "Password"; password = go.GetComponent(); password.text = Multiplayer.Settings.Password; - password.contentType = TMP_InputField.ContentType.Password; + //password.contentType = TMP_InputField.ContentType.Password; //re-introduce later when code for toggling has been implemented password.placeholder.GetComponent().text = Locale.SERVER_HOST_PASSWORD; go.AddComponent();//.enabledKey = Locale.SERVER_HOST_PASSWORD__TOOLTIP_KEY; go.ResetTooltip(); diff --git a/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserDummyElement.cs b/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserDummyElement.cs new file mode 100644 index 0000000..a566ef7 --- /dev/null +++ b/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserDummyElement.cs @@ -0,0 +1,62 @@ +using DV.UI; +using DV.UIFramework; +using DV.Localization; +using Multiplayer.Utils; +using System.ComponentModel; +using TMPro; +using UnityEngine; +using UnityEngine.UI; + +namespace Multiplayer.Components.MainMenu.ServerBrowser +{ + public class ServerBrowserDummyElement : AViewElement + { + private TextMeshProUGUI networkName; + private TextMeshProUGUI playerCount; + private TextMeshProUGUI ping; + private GameObject goIcon; + private Image icon; + private IServerBrowserGameDetails data; + + + + private void Awake() + { + // Find and assign TextMeshProUGUI components for displaying server details + GameObject networkNameGO = this.FindChildByName("name [noloc]"); + networkName = networkNameGO.GetComponent(); + this.FindChildByName("date [noloc]").SetActive(false); + this.FindChildByName("time [noloc]").SetActive(false); + this.FindChildByName("autosave icon").SetActive(false); + + //Remove doubled up components + GameObject.Destroy(this.transform.GetComponent()); + GameObject.Destroy(this.transform.GetComponent()); + GameObject.Destroy(this.transform.GetComponent()); + GameObject.Destroy(this.transform.GetComponent()); + + RectTransform networkNameRT = networkNameGO.transform.GetComponent(); + networkNameRT.sizeDelta = new Vector2(600, networkNameRT.sizeDelta.y); + + this.SetInteractable(false); + + Localize loc = networkNameGO.GetOrAddComponent(); + loc.key = Locale.SERVER_BROWSER__NO_SERVERS_KEY ; + loc.UpdateLocalization(); + + this.GetOrAddComponent().enabled = true;//.enabledKey = Locale.SERVER_HOST_PASSWORD__TOOLTIP_KEY; + this.gameObject.ResetTooltip(); + //networkName.text = "No servers found. Refresh or start your own!"; + } + + public override void SetData(IServerBrowserGameDetails data, AGridView _) + { + //do nothing + } + + private void UpdateView(object sender = null, PropertyChangedEventArgs e = null) + { + //do nothing + } + } +} diff --git a/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserElement.cs b/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserElement.cs index e1c122b..f0ecf14 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserElement.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserElement.cs @@ -28,6 +28,12 @@ private void Awake() goIcon = this.FindChildByName("autosave icon"); icon = goIcon.GetComponent(); + //Remove additional components + GameObject.Destroy(this.transform.GetComponent()); + GameObject.Destroy(this.transform.GetComponent()); + GameObject.Destroy(this.transform.GetComponent()); + GameObject.Destroy(this.transform.GetComponent()); + // Fix alignment of the player count text relative to the network name text Vector3 namePos = networkName.transform.position; Vector2 nameSize = networkName.rectTransform.sizeDelta; diff --git a/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserGridView.cs b/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserGridView.cs index f43c789..7f13fb3 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserGridView.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserGridView.cs @@ -1,9 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using DV.Common; using DV.UI; using DV.UIFramework; using Multiplayer.Components.MainMenu.ServerBrowser; @@ -20,17 +15,24 @@ public class ServerBrowserGridView : AGridView private void Awake() { - Debug.Log("serverBrowserGridview Awake"); - - this.dummyElementPrefab.SetActive(false); + Multiplayer.Log("serverBrowserGridview Awake"); //swap controller - this.dummyElementPrefab.SetActive(false); + this.viewElementPrefab.SetActive(false); + this.dummyElementPrefab = Instantiate(this.viewElementPrefab); + + GameObject.Destroy(this.viewElementPrefab.GetComponent()); GameObject.Destroy(this.dummyElementPrefab.GetComponent()); - this.dummyElementPrefab.AddComponent(); + this.viewElementPrefab.AddComponent(); + this.dummyElementPrefab.AddComponent(); + + this.viewElementPrefab.name = "prefabServerBrowserElement"; + this.dummyElementPrefab.name = "prefabServerBrowserDummyElement"; + + this.viewElementPrefab.SetActive(true); this.dummyElementPrefab.SetActive(true); - this.viewElementPrefab = this.dummyElementPrefab; + } } } diff --git a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs index 335dcf5..37aed64 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs @@ -268,8 +268,9 @@ private void SetupServerBrowser() GridviewGO.SetActive(false); gridView = GridviewGO.AddComponent(); - gridView.dummyElementPrefab = Instantiate(slgv.viewElementPrefab); - gridView.dummyElementPrefab.name = "prefabServerBrowser"; + slgv.viewElementPrefab.SetActive(false); + gridView.viewElementPrefab = Instantiate(slgv.viewElementPrefab); + GameObject.Destroy(slgv); @@ -570,6 +571,15 @@ IEnumerator GetRequest(string uri) Debug.Log($"Name: {server.Name}\tIP: {server.ip}"); } + if (response.Length == 0) + { + gridView.showDummyElement = true; + buttonJoin.ToggleInteractable(false); + } + else + { + gridView.showDummyElement = false; + } gridViewModel.Clear(); gridView.SetModel(gridViewModel); gridViewModel.AddRange(response); @@ -618,7 +628,7 @@ private void SetButtonsActive(params GameObject[] buttons) private void FillDummyServers() { - //gridView.showDummyElement = true; + gridView.showDummyElement = false; gridViewModel.Clear(); diff --git a/Multiplayer/Locale.cs b/Multiplayer/Locale.cs index 4d6dca5..dbfd637 100644 --- a/Multiplayer/Locale.cs +++ b/Multiplayer/Locale.cs @@ -62,6 +62,8 @@ public static class Locale private const string SERVER_BROWSER__YES_KEY = $"{PREFIX_SERVER_BROWSER}/yes"; public static string SERVER_BROWSER__NO => Get(SERVER_BROWSER__NO_KEY); private const string SERVER_BROWSER__NO_KEY = $"{PREFIX_SERVER_BROWSER}/no"; + public static string SERVER_BROWSER__NO_SERVERS => Get(SERVER_BROWSER__NO_SERVERS_KEY); + public const string SERVER_BROWSER__NO_SERVERS_KEY = $"{PREFIX_SERVER_BROWSER}/no_servers"; #endregion #region Server Host diff --git a/locale.csv b/locale.csv index 9852356..05fdbf5 100644 --- a/locale.csv +++ b/locale.csv @@ -35,6 +35,9 @@ sb/game_version,Game version in details text,Game version,Версия на иг sb/mod_version,Multiplayer version in details text,Multiplayer version,Мултиплейър версия,多人游戏版本,多人遊戲版本,Multiplayer verze,Multiplayer version,Multiplayer versie,Moninpeliversio,Version multijoueur,Multiplayer-Version,मल्टीप्लेयर संस्करण,Multiplayer verze,Versione multiplayer,マルチプレイヤーバージョン,멀티플레이어 버전,Multiplayer versjon,Wersja multiplayer,Versão multiplayer,Versão multiplayer,Versiunea multiplayer,Мультиплеерная версия,Multiplayer verzia,Versión multijugador,Multiplayer-version,Çok oyunculu sürüm,Багатокористувацька версія sb/yes,Response 'yes' for details text,Yes,Да,是,是,Ano,Ja,Ja,Kyllä,Oui,Ja,हां,Ano,Sì,はい,네,Ja,Tak,Sim,Sim,Da,Да,Áno,Sí,Ja,Evet,Так sb/no,Response 'no' for details text,No,Не,否,否,Ne,Nej,Nee,Ei,Non,Nein,नहीं,Ne,No,いいえ,아니요,Nei,Nie,Não,Não,Nu,Нет,Nie,Nie,Nej,Hayır,Ні +sb/no_servers,Label for no servers,No servers found. Refresh or start your own!,Няма намерени сървъри. Обновете или стартирайте свой собствен!,未找到服务器。 刷新或创建您自己的!,未找到伺服器。 刷新或創建您自己的!,"Žádné servery nebyly nalezeny. Obnovte nebo spusťte vlastní!",Ingen servere fundet. Opdater eller start din egen!,Geen servers gevonden. Ververs of start je eigen!,Ei palvelimia löytynyt. Päivitä tai aloita oma!,Aucun serveur trouvé. Rafraîchissez ou créez le vôtre !,Keine Server gefunden. Aktualisieren oder eigenen starten!,कोई सर्वर नहीं मिला। ताज़ा करें या अपना स्वयं का प्रारंभ करें!,"Nem található szerver. Frissítsen, vagy indítson sajátot!","Nessun server trovato. Aggiorna o avvia il tuo!","サーバーが見つかりませんでした。 更新するか、自分で始めてください!","서버를 찾을 수 없습니다. 새로 고치거나 직접 시작하십시오!",Ingen servere funnet. Oppdater eller start din egen!,Nie znaleziono serwerów. Odśwież lub zacznij własny!,"Nenhum servidor encontrado. Atualize ou inicie o seu próprio!","Nenhum servidor encontrado. Atualize ou inicie o seu!",Nu au fost găsite servere. Reîmprospătați sau începeți propriul dvs!,"Серверы не найдены. Обновите или начните свой собственный!","Žiadne servery sa nenašli. Obnovte alebo spustite vlastný!","No se encontraron servidores. ¡Actualiza o empieza uno propio!",Inga servrar hittades. Uppdatera eller starta din egen!,Sunucu bulunamadı. Yenileyin veya kendi sunucunuzu başlatın!,Сервери не знайдено. Оновіть або почніть власний! +sb/no_servers__tooltip,Label for no servers,No servers found. Refresh or start your own!,Няма намерени сървъри. Обновете или стартирайте свой собствен!,未找到服务器。 刷新或创建您自己的!,未找到伺服器。 刷新或創建您自己的!,"Žádné servery nebyly nalezeny. Obnovte nebo spusťte vlastní!",Ingen servere fundet. Opdater eller start din egen!,Geen servers gevonden. Ververs of start je eigen!,Ei palvelimia löytynyt. Päivitä tai aloita oma!,Aucun serveur trouvé. Rafraîchissez ou créez le vôtre !,Keine Server gefunden. Aktualisieren oder eigenen starten!,कोई सर्वर नहीं मिला। ताज़ा करें या अपना स्वयं का प्रारंभ करें!,"Nem található szerver. Frissítsen, vagy indítson sajátot!","Nessun server trovato. Aggiorna o avvia il tuo!","サーバーが見つかりませんでした。 更新するか、自分で始めてください!","서버를 찾을 수 없습니다. 새로 고치거나 직접 시작하십시오!",Ingen servere funnet. Oppdater eller start din egen!,Nie znaleziono serwerów. Odśwież lub zacznij własny!,"Nenhum servidor encontrado. Atualize ou inicie o seu próprio!","Nenhum servidor encontrado. Atualize ou inicie o seu!",Nu au fost găsite servere. Reîmprospătați sau începeți propriul dvs!,"Серверы не найдены. Обновите или начните свой собственный!","Žiadne servery sa nenašli. Obnovte alebo spustite vlastný!","No se encontraron servidores. ¡Actualiza o empieza uno propio!",Inga servrar hittades. Uppdatera eller starta din egen!,Sunucu bulunamadı. Yenileyin veya kendi sunucunuzu başlatın!,Сервери не знайдено. Оновіть або почніть власний! +sb/no_servers__tooltip_disabled,Label for no servers,No servers found. Refresh or start your own!,Няма намерени сървъри. Обновете или стартирайте свой собствен!,未找到服务器。 刷新或创建您自己的!,未找到伺服器。 刷新或創建您自己的!,"Žádné servery nebyly nalezeny. Obnovte nebo spusťte vlastní!",Ingen servere fundet. Opdater eller start din egen!,Geen servers gevonden. Ververs of start je eigen!,Ei palvelimia löytynyt. Päivitä tai aloita oma!,Aucun serveur trouvé. Rafraîchissez ou créez le vôtre !,Keine Server gefunden. Aktualisieren oder eigenen starten!,कोई सर्वर नहीं मिला। ताज़ा करें या अपना स्वयं का प्रारंभ करें!,"Nem található szerver. Frissítsen, vagy indítson sajátot!","Nessun server trovato. Aggiorna o avvia il tuo!","サーバーが見つかりませんでした。 更新するか、自分で始めてください!","서버를 찾을 수 없습니다. 새로 고치거나 직접 시작하십시오!",Ingen servere funnet. Oppdater eller start din egen!,Nie znaleziono serwerów. Odśwież lub zacznij własny!,"Nenhum servidor encontrado. Atualize ou inicie o seu próprio!","Nenhum servidor encontrado. Atualize ou inicie o seu!",Nu au fost găsite servere. Reîmprospătați sau începeți propriul dvs!,"Серверы не найдены. Обновите или начните свой собственный!","Žiadne servery sa nenašli. Obnovte alebo spustite vlastný!","No se encontraron servidores. ¡Actualiza o empieza uno propio!",Inga servrar hittades. Uppdatera eller starta din egen!,Sunucu bulunamadı. Yenileyin veya kendi sunucunuzu başlatın!,Сервери не знайдено. Оновіть або почніть власний! ,,,,,,,,,,,,,,,,,,,,,,,,,,, ,Server Host,,,,,,,,,,,,,,,,,,,,,,,,,, host/title,The title of the Host Game page,Host Game,Домакин на играта,主机游戏,主機遊戲,Hostitelská hra,Værtsspil,Gastheerspel,Isäntäpeli,Partie hôte,Gastspiel,मेज़बान खेल,Gazdajáték,Ospita il gioco,ホストゲーム,호스트 게임,Vertsspill,Gra gospodarza,Jogo anfitrião,Jogo anfitrião,Găzduire joc,Хост-игра,Hostiteľská hra,Juego de acogida,Värdspel,Sunucu Oyunu,Ведуча гра From 00359ad38d94cdb5393dd058c1066544bfc750fa Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 14 Jul 2024 09:42:07 +1000 Subject: [PATCH 33/34] Bug fixes for lobby server redirects --- .../Managers/Server/LobbyServerManager.cs | 205 +++++++++--------- .../Managers/Server/NetworkServer.cs | 1 + 2 files changed, 109 insertions(+), 97 deletions(-) diff --git a/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs b/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs index 17f674a..90f9ce8 100644 --- a/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs +++ b/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs @@ -7,39 +7,43 @@ using UnityEngine.Networking; using Multiplayer.Components.Networking; using DV.WeatherSystem; -using DV.UserManagement; namespace Multiplayer.Networking.Managers.Server; public class LobbyServerManager : MonoBehaviour { - private const int UPDATE_TIME_BUFFER = 10; - private const int UPDATE_TIME = 120 - UPDATE_TIME_BUFFER; //how often to update the lobby server - private const int PLAYER_CHANGE_TIME = 5; //update server early if the number of players has changed in this time frame + //API endpoints + private const string ENDPOINT_ADD_SERVER = "add_game_server"; + private const string ENDPOINT_UPDATE_SERVER = "update_game_server"; + private const string ENDPOINT_REMOVE_SERVER = "remove_game_server"; + + private const int REDIRECT_MAX = 5; + + private const int UPDATE_TIME_BUFFER = 10; //We don't want to miss our update, let's phone in just a little early + private const int UPDATE_TIME = 120 - UPDATE_TIME_BUFFER; //How often to update the lobby server - this should match the lobby server's time-out period + private const int PLAYER_CHANGE_TIME = 5; //Update server early if the number of players has changed in this time frame private NetworkServer server; public string server_id { get; set; } public string private_key { get; set; } private bool sendUpdates = false; - - private float timePassed = 0f; private void Awake() { - this.server = NetworkLifecycle.Instance.Server; + server = NetworkLifecycle.Instance.Server; - Debug.Log($"LobbyServerManager New({server != null})"); - Debug.Log($"StartingCoroutine {Multiplayer.Settings.LobbyServerAddress}/add_game_server\")"); - StartCoroutine(this.RegisterWithLobbyServer($"{Multiplayer.Settings.LobbyServerAddress}/add_game_server")); + Multiplayer.Log($"LobbyServerManager New({server != null})"); + Multiplayer.Log($"StartingCoroutine {Multiplayer.Settings.LobbyServerAddress}/{ENDPOINT_ADD_SERVER}"); + StartCoroutine(RegisterWithLobbyServer($"{Multiplayer.Settings.LobbyServerAddress}/{ENDPOINT_ADD_SERVER}")); } private void OnDestroy() { - Debug.Log($"LobbyServerManager OnDestroy()"); + Multiplayer.Log($"LobbyServerManager OnDestroy()"); sendUpdates = false; - this.StopAllCoroutines(); - StartCoroutine(this.RemoveFromLobbyServer($"{Multiplayer.Settings.LobbyServerAddress}/remove_game_server")); + StopAllCoroutines(); + StartCoroutine(RemoveFromLobbyServer($"{Multiplayer.Settings.LobbyServerAddress}/{ENDPOINT_REMOVE_SERVER}")); } private void Update() @@ -48,128 +52,135 @@ private void Update() { timePassed += Time.deltaTime; - if(timePassed > UPDATE_TIME || (server.serverData.CurrentPlayers != server.PlayerCount && timePassed > PLAYER_CHANGE_TIME)){ + if (timePassed > UPDATE_TIME || (server.serverData.CurrentPlayers != server.PlayerCount && timePassed > PLAYER_CHANGE_TIME)) + { timePassed = 0f; server.serverData.CurrentPlayers = server.PlayerCount; - StartCoroutine(this.UpdateLobbyServer($"{Multiplayer.Settings.LobbyServerAddress}/update_game_server")); + StartCoroutine(UpdateLobbyServer($"{Multiplayer.Settings.LobbyServerAddress}/{ENDPOINT_UPDATE_SERVER}")); } } } + public void RemoveFromLobbyServer() { - Debug.Log($"RemoveFromLobbyServer OnDestroy()"); + Multiplayer.Log($"RemoveFromLobbyServer OnDestroy()"); sendUpdates = false; - this.StopAllCoroutines(); - StartCoroutine(this.RemoveFromLobbyServer($"{Multiplayer.Settings.LobbyServerAddress}/remove_game_server")); + StopAllCoroutines(); + StartCoroutine(RemoveFromLobbyServer($"{Multiplayer.Settings.LobbyServerAddress}/{ENDPOINT_REMOVE_SERVER}")); } - - IEnumerator RegisterWithLobbyServer(string uri) + private IEnumerator RegisterWithLobbyServer(string uri) { - JsonSerializerSettings jsonSettings = new JsonSerializerSettings(); - jsonSettings.NullValueHandling = NullValueHandling.Ignore; - + JsonSerializerSettings jsonSettings = new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }; string json = JsonConvert.SerializeObject(server.serverData, jsonSettings); - Debug.Log($"JsonRequest: {json}"); - - using (UnityWebRequest webRequest = UnityWebRequest.Post(uri, json)) - { - UploadHandler customUploadHandler = new UploadHandlerRaw(System.Text.Encoding.UTF8.GetBytes(json)); - customUploadHandler.contentType = "application/json"; - webRequest.uploadHandler = customUploadHandler; - - // Request and wait for the desired page. - yield return webRequest.SendWebRequest(); - - string[] pages = uri.Split('/'); - int page = pages.Length - 1; + Multiplayer.LogDebug(()=>$"JsonRequest: {json}"); - if (webRequest.isNetworkError || webRequest.isHttpError) + yield return SendWebRequest( + uri, + json, + webRequest => { - Debug.Log(pages[page] + ": Error: " + webRequest.error + "\r\n" + webRequest.downloadHandler.text); - } - else - { - Debug.Log(pages[page] + ":\nReceived: " + webRequest.downloadHandler.text); - - LobbyServerResponseData response; - - response = JsonConvert.DeserializeObject(webRequest.downloadHandler.text); - + LobbyServerResponseData response = JsonConvert.DeserializeObject(webRequest.downloadHandler.text); if (response != null) { - this.private_key = response.private_key; - this.server_id = response.game_server_id; - this.sendUpdates = true; + private_key = response.private_key; + server_id = response.game_server_id; + sendUpdates = true; } - } - } + }, + webRequest => Multiplayer.LogError("Failed to register with lobby server") + ); } - IEnumerator RemoveFromLobbyServer(string uri) + private IEnumerator RemoveFromLobbyServer(string uri) { - JsonSerializerSettings jsonSettings = new JsonSerializerSettings(); - jsonSettings.NullValueHandling = NullValueHandling.Ignore; - - string json = JsonConvert.SerializeObject(new LobbyServerResponseData(this.server_id, this.private_key), jsonSettings); - Debug.Log($"JsonRequest: {json}"); - - using (UnityWebRequest webRequest = UnityWebRequest.Post(uri, json)) - { - UploadHandler customUploadHandler = new UploadHandlerRaw(System.Text.Encoding.UTF8.GetBytes(json)); - customUploadHandler.contentType = "application/json"; - webRequest.uploadHandler = customUploadHandler; - - // Request and wait for the desired page. - yield return webRequest.SendWebRequest(); - - string[] pages = uri.Split('/'); - int page = pages.Length - 1; - - if (webRequest.isNetworkError || webRequest.isHttpError) - { - Debug.Log(pages[page] + ": Error: " + webRequest.error + "\r\n" + webRequest.downloadHandler.text); - } - else - { - Debug.Log(pages[page] + ":\nReceived: " + webRequest.downloadHandler.text); - } - } + JsonSerializerSettings jsonSettings = new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }; + string json = JsonConvert.SerializeObject(new LobbyServerResponseData(server_id, private_key), jsonSettings); + Multiplayer.LogDebug(() => $"JsonRequest: {json}"); + + yield return SendWebRequest( + uri, + json, + webRequest => Multiplayer.Log("Successfully removed from lobby server"), + webRequest => Multiplayer.LogError("Failed to remove from lobby server") + ); } - IEnumerator UpdateLobbyServer(string uri) + private IEnumerator UpdateLobbyServer(string uri) { - JsonSerializerSettings jsonSettings = new JsonSerializerSettings(); - jsonSettings.NullValueHandling = NullValueHandling.Ignore; + JsonSerializerSettings jsonSettings = new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }; DateTime start = AStartGameData.BaseTimeAndDate; DateTime current = WeatherDriver.Instance.manager.DateTime; - TimeSpan inGame = current - start; - - string json = JsonConvert.SerializeObject(new LobbyServerUpdateData(this.server_id, this.private_key, inGame.ToString("d\\d\\ hh\\h\\ mm\\m\\ ss\\s"), server.serverData.CurrentPlayers), jsonSettings); - Debug.Log($"UpdateLobbyServer JsonRequest: {json}"); + string json = JsonConvert.SerializeObject(new LobbyServerUpdateData( + server_id, + private_key, + inGame.ToString("d\\d\\ hh\\h\\ mm\\m\\ ss\\s"), + server.serverData.CurrentPlayers), + jsonSettings + ); + Multiplayer.LogDebug(() => $"UpdateLobbyServer JsonRequest: {json}"); + + yield return SendWebRequest( + uri, + json, + webRequest => Multiplayer.Log("Successfully updated lobby server"), + webRequest => + { + Multiplayer.LogError("Failed to update lobby server, attempting to re-register"); + + //cleanup + sendUpdates = false; + private_key = null; + server_id = null; + + //Attempt to re-register + StartCoroutine(RegisterWithLobbyServer($"{Multiplayer.Settings.LobbyServerAddress}/{ENDPOINT_ADD_SERVER}")); + } + ); + } + private IEnumerator SendWebRequest(string uri, string json, Action onSuccess, Action onError, int depth=0) + { + if (depth > REDIRECT_MAX) + { + Multiplayer.LogError($"Reached maximum redirects: {uri}"); + yield break; + } using (UnityWebRequest webRequest = UnityWebRequest.Post(uri, json)) { - UploadHandler customUploadHandler = new UploadHandlerRaw(System.Text.Encoding.UTF8.GetBytes(json)); - customUploadHandler.contentType = "application/json"; - webRequest.uploadHandler = customUploadHandler; + webRequest.redirectLimit = 0; - // Request and wait for the desired page. - yield return webRequest.SendWebRequest(); + webRequest.uploadHandler = new UploadHandlerRaw(System.Text.Encoding.UTF8.GetBytes(json)){contentType = "application/json"}; + webRequest.downloadHandler = new DownloadHandlerBuffer(); - string[] pages = uri.Split('/'); - int page = pages.Length - 1; + yield return webRequest.SendWebRequest(); - if (webRequest.isNetworkError || webRequest.isHttpError) + //check for redirect + if (webRequest.responseCode >= 300 && webRequest.responseCode < 400) { - Debug.Log(pages[page] + ": Error: " + webRequest.error + "\r\n" + webRequest.downloadHandler.text); + string redirectUrl = webRequest.GetResponseHeader("Location"); + Multiplayer.LogWarning($"Lobby Server redirected, check address is up to date: '{redirectUrl}'"); + + if (redirectUrl != null && redirectUrl.StartsWith("https://") && redirectUrl.Replace("https://", "http://") == uri) + { + yield return SendWebRequest(redirectUrl, json, onSuccess, onError, ++depth); + } } else { - Debug.Log(pages[page] + ":\nReceived: " + webRequest.downloadHandler.text); + if (webRequest.isNetworkError || webRequest.isHttpError) + { + Multiplayer.LogError($"Error: {webRequest.error}\r\n{webRequest.downloadHandler.text}"); + onError?.Invoke(webRequest); + } + else + { + Multiplayer.Log($"Received: {webRequest.downloadHandler.text}"); + onSuccess?.Invoke(webRequest); + } } } } diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index 842bc17..a5bc5ad 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -74,6 +74,7 @@ public override void Stop() if (lobbyServerManager != null) { lobbyServerManager.RemoveFromLobbyServer(); + GameObject.Destroy(lobbyServerManager); } base.Stop(); From 1827ded9adbcc4d5de54a399a315174e49a40412 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 14 Jul 2024 12:16:28 +1000 Subject: [PATCH 34/34] Minor update to CSV parsing --- Multiplayer/Utils/Csv.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Multiplayer/Utils/Csv.cs b/Multiplayer/Utils/Csv.cs index b4a18a2..b3fc68e 100644 --- a/Multiplayer/Utils/Csv.cs +++ b/Multiplayer/Utils/Csv.cs @@ -16,7 +16,7 @@ public static class Csv public static ReadOnlyDictionary> Parse(string data) { // Split the input data into lines - string[] separators = new string[] { "\r\n" }; + string[] separators = new string[] { "\r\n", "\n" }; string[] lines = data.Split(separators, StringSplitOptions.None); // Use an OrderedDictionary to preserve the insertion order of keys