From e23d522a4e966920651802ebc67ab636599bb466 Mon Sep 17 00:00:00 2001 From: Pierce Thompson Date: Tue, 8 Aug 2023 00:26:19 -0400 Subject: [PATCH 001/188] 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 002/188] 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 6daa671d082bbc6cd2b85119f51e23c32b9a3b3f Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 12 May 2024 10:45:10 +1000 Subject: [PATCH 003/188] Enhanced "join" interface Default remote IP can now be set through the settings Popup/prompt for IP, port and password now auto-fill from the defaults --- Multiplayer/Components/MainMenu/MultiplayerPane.cs | 6 +++++- Multiplayer/Settings.cs | 4 +++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Multiplayer/Components/MainMenu/MultiplayerPane.cs b/Multiplayer/Components/MainMenu/MultiplayerPane.cs index be42068..c11ca96 100644 --- a/Multiplayer/Components/MainMenu/MultiplayerPane.cs +++ b/Multiplayer/Components/MainMenu/MultiplayerPane.cs @@ -1,8 +1,9 @@ -using System; +using System; using System.Text.RegularExpressions; using DV.UIFramework; using DV.Utils; using Multiplayer.Components.Networking; +using TMPro; using UnityEngine; namespace Multiplayer.Components.MainMenu; @@ -39,6 +40,7 @@ private void ShowIpPopup() return; popup.labelTMPro.text = Locale.SERVER_BROWSER__IP; + popup.GetComponentInChildren().text = Multiplayer.Settings.DefaultRemoteIP; popup.Closed += result => { @@ -67,6 +69,7 @@ private void ShowPortPopup() return; popup.labelTMPro.text = Locale.SERVER_BROWSER__PORT; + popup.GetComponentInChildren().text = Multiplayer.Settings.Port.ToString(); popup.Closed += result => { @@ -95,6 +98,7 @@ private void ShowPasswordPopup() return; popup.labelTMPro.text = Locale.SERVER_BROWSER__PASSWORD; + popup.GetComponentInChildren().text = Multiplayer.Settings.Password; popup.Closed += result => { diff --git a/Multiplayer/Settings.cs b/Multiplayer/Settings.cs index c01fe67..e7c8da3 100644 --- a/Multiplayer/Settings.cs +++ b/Multiplayer/Settings.cs @@ -1,4 +1,4 @@ -using System; +using System; using Humanizer; using UnityEngine; using UnityModManagerNet; @@ -21,6 +21,8 @@ public class Settings : UnityModManager.ModSettings, IDrawable [Space(10)] [Header("Server")] + [Draw("Default Remote IP", Tooltip = "The default server IP when joining as a client.")] + public string DefaultRemoteIP = ""; [Draw("Password", Tooltip = "The password required to join your server. Leave blank for no password.")] public string Password = ""; [Draw("Max Players", Tooltip = "The maximum number of players that can join your server, including yourself.")] From e1a3e97cdb439599db5c63e5763112b9ac186c26 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 12 May 2024 18:46:23 +1000 Subject: [PATCH 004/188] Reworked the saving of last direct connection details Separated server and client settings --- .../Components/MainMenu/MultiplayerPane.cs | 75 ++++++++++++++- ...pupTextInputFieldControllerNoValidation.cs | 91 +++++++++++++++++++ Multiplayer/Locale.cs | 3 + .../MainMenu/RightPaneControllerPatch.cs | 4 + Multiplayer/Settings.cs | 12 ++- locale.csv | 1 + 6 files changed, 179 insertions(+), 7 deletions(-) create mode 100644 Multiplayer/Components/MainMenu/PopupTextInputFieldControllerNoValidation.cs diff --git a/Multiplayer/Components/MainMenu/MultiplayerPane.cs b/Multiplayer/Components/MainMenu/MultiplayerPane.cs index c11ca96..e3f5861 100644 --- a/Multiplayer/Components/MainMenu/MultiplayerPane.cs +++ b/Multiplayer/Components/MainMenu/MultiplayerPane.cs @@ -1,10 +1,14 @@ using System; using System.Text.RegularExpressions; +using DV.Localization; +using DV.UI; using DV.UIFramework; using DV.Utils; using Multiplayer.Components.Networking; +using Multiplayer.Utils; using TMPro; using UnityEngine; +using UnityEngine.UI; namespace Multiplayer.Components.MainMenu; @@ -22,6 +26,54 @@ public class MultiplayerPane : MonoBehaviour private string address; private ushort port; + private GameObject directButton; + private ButtonDV direct; + + private void Awake() + { + Multiplayer.Log("MultiplayerPane Awake()"); + + GameObject button = GameObject.Find("ButtonTextIcon Run"); + + button.SetActive(false); + directButton = GameObject.Instantiate(button, this.transform); + button.SetActive(true); + + directButton.name = "ButtonTextIcon DirectIP"; + + direct = directButton.GetComponent(); + direct.onClick.AddListener(ShowIpPopup); + + + + directButton.GetComponentInChildren().key = Locale.SERVER_BROWSER__DIRECT_KEY; + + foreach (I2.Loc.Localize loc in directButton.GetComponentsInChildren()) + { + Component.DestroyImmediate(loc); + } + + + UIElementTooltip tooltip = directButton.GetComponent(); + tooltip.disabledKey = null; + tooltip.enabledKey = Locale.SERVER_BROWSER__DIRECT_KEY; + + + GameObject icon = directButton.FindChildByName("[icon]"); + if (icon == null) + { + Multiplayer.LogError("Failed to find icon on Direct IP button, destroying the Multiplayer button!"); + GameObject.Destroy(directButton); + return; + } + + icon.GetComponent().sprite = Multiplayer.AssetIndex.multiplayerIcon; + + directButton.SetActive(true); + + + } + private void OnEnable() { if (!why) @@ -30,7 +82,9 @@ private void OnEnable() return; } - ShowIpPopup(); + Multiplayer.Log("MultiplayerPane OnEnable()"); + //ShowIpPopup(); + direct.enabled = true; } private void ShowIpPopup() @@ -40,7 +94,7 @@ private void ShowIpPopup() return; popup.labelTMPro.text = Locale.SERVER_BROWSER__IP; - popup.GetComponentInChildren().text = Multiplayer.Settings.DefaultRemoteIP; + popup.GetComponentInChildren().text = Multiplayer.Settings.LastRemoteIP; popup.Closed += result => { @@ -69,7 +123,7 @@ private void ShowPortPopup() return; popup.labelTMPro.text = Locale.SERVER_BROWSER__PORT; - popup.GetComponentInChildren().text = Multiplayer.Settings.Port.ToString(); + popup.GetComponentInChildren().text = Multiplayer.Settings.LastRemotePort.ToString(); popup.Closed += result => { @@ -98,17 +152,28 @@ private void ShowPasswordPopup() return; popup.labelTMPro.text = Locale.SERVER_BROWSER__PASSWORD; - popup.GetComponentInChildren().text = Multiplayer.Settings.Password; + popup.GetComponentInChildren().text = Multiplayer.Settings.LastRemotePassword; + + //we need to remove the default controller and replace it with our own to override validation + Component.DestroyImmediate(popup.GetComponentInChildren()); + popup.GetOrAddComponent(); popup.Closed += result => { if (result.closedBy == PopupClosedByAction.Abortion) { - MainMenuThingsAndStuff.Instance.SwitchToDefaultMenu(); + //MainMenuThingsAndStuff.Instance.SwitchToDefaultMenu(); return; } + direct.enabled = false; + + SingletonBehaviour.Instance.StartClient(address, port, result.data); + + Multiplayer.Settings.LastRemoteIP = address; + Multiplayer.Settings.LastRemotePort = port; + Multiplayer.Settings.LastRemotePassword = result.data; }; } diff --git a/Multiplayer/Components/MainMenu/PopupTextInputFieldControllerNoValidation.cs b/Multiplayer/Components/MainMenu/PopupTextInputFieldControllerNoValidation.cs new file mode 100644 index 0000000..b3c4016 --- /dev/null +++ b/Multiplayer/Components/MainMenu/PopupTextInputFieldControllerNoValidation.cs @@ -0,0 +1,91 @@ +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; + } + } + + //patch us in as the new handler for the dialogue + typeof(Popup).GetField("handler", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(popup, this); + + } + + private void Start() + { + field.onValueChanged.AddListener(new UnityAction(OnInputValueChanged)); + OnInputValueChanged(field.text); + field.Select(); + field.ActivateInputField(); + + } + private void OnInputValueChanged(string value) + { + 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) + { + return true;// !string.IsNullOrWhiteSpace(value); + } + 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/Locale.cs b/Multiplayer/Locale.cs index e6d1544..af4998e 100644 --- a/Multiplayer/Locale.cs +++ b/Multiplayer/Locale.cs @@ -34,6 +34,9 @@ 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__DIRECT => Get(SERVER_BROWSER__DIRECT_KEY); + public const string SERVER_BROWSER__DIRECT_KEY = $"{PREFIX_SERVER_BROWSER}/direct"; + 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); diff --git a/Multiplayer/Patches/MainMenu/RightPaneControllerPatch.cs b/Multiplayer/Patches/MainMenu/RightPaneControllerPatch.cs index 33467b4..f393cc4 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 TMPro; using UnityEngine; namespace Multiplayer.Patches.MainMenu; @@ -43,6 +44,9 @@ private static void Prefix(RightPaneController __instance) return; } + GameObject content = multiplayerPane.FindChildByName("text header"); + content.GetComponentInChildren().text = "Server browser not yet implemented."; + titleObj.GetComponentInChildren().key = Locale.SERVER_BROWSER__TITLE_KEY; Object.Destroy(titleObj.GetComponentInChildren()); diff --git a/Multiplayer/Settings.cs b/Multiplayer/Settings.cs index e7c8da3..b29e88a 100644 --- a/Multiplayer/Settings.cs +++ b/Multiplayer/Settings.cs @@ -21,8 +21,6 @@ public class Settings : UnityModManager.ModSettings, IDrawable [Space(10)] [Header("Server")] - [Draw("Default Remote IP", Tooltip = "The default server IP when joining as a client.")] - public string DefaultRemoteIP = ""; [Draw("Password", Tooltip = "The password required to join your server. Leave blank for no password.")] public string Password = ""; [Draw("Max Players", Tooltip = "The maximum number of players that can join your server, including yourself.")] @@ -30,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/locale.csv b/locale.csv index f8b269b..6a08bcd 100644 --- a/locale.csv +++ b/locale.csv @@ -11,6 +11,7 @@ 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/direct,Connect to IP button,Connect to IP,Свързване към IP,连接到IP,連接到IP,Připojit k IP,Opret forbindelse til IP,Verbinding maken met IP,Yhdistä IP-osoitteeseen,Connectez-vous à IP,Mit IP verbinden,आईपी से कनेक्ट करें,Csatlakozás az IP-hez,Connetti all'IP,IPに接続する,IP에 연결,Koble til IP,Połącz się z IP,Conecte-se ao IP,Conecte-se ao IP,Conectați-vă la IP,Подключиться к IP,Pripojiť k IP,Conectarse a IP,Anslut till IP,IP'ye Bağlan,Підключитися до IP 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),,, 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 005/188] 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 006/188] 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 0c9d431bb074303fa28ff2d6874c025e31f26f1d Mon Sep 17 00:00:00 2001 From: N95JPL <37276225+N95JPL@users.noreply.github.com> Date: Sun, 26 May 2024 21:31:49 +0100 Subject: [PATCH 007/188] Dynamic DNS Added the ability to join games using a Dynamic DNS URL, such as "example.tplinkdns.com". The script then gets the host IP, and saves the "Direct IP" to the "Last Remote IP" section. --- .../Components/MainMenu/MultiplayerPane.cs | 131 ++++++++---------- 1 file changed, 60 insertions(+), 71 deletions(-) diff --git a/Multiplayer/Components/MainMenu/MultiplayerPane.cs b/Multiplayer/Components/MainMenu/MultiplayerPane.cs index e3f5861..9cb9b73 100644 --- a/Multiplayer/Components/MainMenu/MultiplayerPane.cs +++ b/Multiplayer/Components/MainMenu/MultiplayerPane.cs @@ -1,14 +1,10 @@ using System; +using System.Net; using System.Text.RegularExpressions; -using DV.Localization; -using DV.UI; using DV.UIFramework; using DV.Utils; using Multiplayer.Components.Networking; -using Multiplayer.Utils; -using TMPro; using UnityEngine; -using UnityEngine.UI; namespace Multiplayer.Components.MainMenu; @@ -26,54 +22,6 @@ public class MultiplayerPane : MonoBehaviour private string address; private ushort port; - private GameObject directButton; - private ButtonDV direct; - - private void Awake() - { - Multiplayer.Log("MultiplayerPane Awake()"); - - GameObject button = GameObject.Find("ButtonTextIcon Run"); - - button.SetActive(false); - directButton = GameObject.Instantiate(button, this.transform); - button.SetActive(true); - - directButton.name = "ButtonTextIcon DirectIP"; - - direct = directButton.GetComponent(); - direct.onClick.AddListener(ShowIpPopup); - - - - directButton.GetComponentInChildren().key = Locale.SERVER_BROWSER__DIRECT_KEY; - - foreach (I2.Loc.Localize loc in directButton.GetComponentsInChildren()) - { - Component.DestroyImmediate(loc); - } - - - UIElementTooltip tooltip = directButton.GetComponent(); - tooltip.disabledKey = null; - tooltip.enabledKey = Locale.SERVER_BROWSER__DIRECT_KEY; - - - GameObject icon = directButton.FindChildByName("[icon]"); - if (icon == null) - { - Multiplayer.LogError("Failed to find icon on Direct IP button, destroying the Multiplayer button!"); - GameObject.Destroy(directButton); - return; - } - - icon.GetComponent().sprite = Multiplayer.AssetIndex.multiplayerIcon; - - directButton.SetActive(true); - - - } - private void OnEnable() { if (!why) @@ -82,9 +30,7 @@ private void OnEnable() return; } - Multiplayer.Log("MultiplayerPane OnEnable()"); - //ShowIpPopup(); - direct.enabled = true; + ShowIpPopup(); } private void ShowIpPopup() @@ -94,7 +40,6 @@ private void ShowIpPopup() return; popup.labelTMPro.text = Locale.SERVER_BROWSER__IP; - popup.GetComponentInChildren().text = Multiplayer.Settings.LastRemoteIP; popup.Closed += result => { @@ -106,6 +51,43 @@ private void ShowIpPopup() if (!IPv4.IsMatch(result.data) && !IPv6.IsMatch(result.data)) { + + string inputUrl = result.data; + + if (!inputUrl.StartsWith("http://") && !inputUrl.StartsWith("https://")) + { + inputUrl = "http://" + inputUrl; + } + + bool isValidURL = Uri.TryCreate(inputUrl, UriKind.RelativeOrAbsolute, out Uri uriResult) + && (uriResult.Scheme == Uri.UriSchemeHttp || uriResult.Scheme == Uri.UriSchemeHttps); + + + if (isValidURL) + { + string domainName = ExtractDomainName(result.data); + try + { + IPHostEntry hostEntry = Dns.GetHostEntry(domainName); + IPAddress[] addresses = hostEntry.AddressList; + + if (addresses.Length > 0) + { + string address2 = addresses[0].ToString(); + + address = address2; + Multiplayer.Log(address); + + ShowPortPopup(); + return; + } + } + catch (Exception ex) + { + Multiplayer.LogError($"An error occurred: {ex.Message}"); + } + } + ShowOkPopup(Locale.SERVER_BROWSER__IP_INVALID, ShowIpPopup); return; } @@ -116,6 +98,26 @@ private void ShowIpPopup() }; } + static string ExtractDomainName(string input) + { + if (input.StartsWith("http://")) + { + input = input.Substring(7); + } + else if (input.StartsWith("https://")) + { + input = input.Substring(8); + } + + int portIndex = input.IndexOf(':'); + if (portIndex != -1) + { + input = input.Substring(0, portIndex); + } + + return input; + } + private void ShowPortPopup() { Popup popup = MainMenuThingsAndStuff.Instance.ShowRenamePopup(); @@ -123,7 +125,6 @@ private void ShowPortPopup() return; popup.labelTMPro.text = Locale.SERVER_BROWSER__PORT; - popup.GetComponentInChildren().text = Multiplayer.Settings.LastRemotePort.ToString(); popup.Closed += result => { @@ -152,28 +153,16 @@ private void ShowPasswordPopup() return; popup.labelTMPro.text = Locale.SERVER_BROWSER__PASSWORD; - popup.GetComponentInChildren().text = Multiplayer.Settings.LastRemotePassword; - - //we need to remove the default controller and replace it with our own to override validation - Component.DestroyImmediate(popup.GetComponentInChildren()); - popup.GetOrAddComponent(); popup.Closed += result => { if (result.closedBy == PopupClosedByAction.Abortion) { - //MainMenuThingsAndStuff.Instance.SwitchToDefaultMenu(); + MainMenuThingsAndStuff.Instance.SwitchToDefaultMenu(); return; } - direct.enabled = false; - - SingletonBehaviour.Instance.StartClient(address, port, result.data); - - Multiplayer.Settings.LastRemoteIP = address; - Multiplayer.Settings.LastRemotePort = port; - Multiplayer.Settings.LastRemotePassword = result.data; }; } From bb69a062dd4ba3867df0eed5cebeaa1be28e8e09 Mon Sep 17 00:00:00 2001 From: N95JPL <37276225+N95JPL@users.noreply.github.com> Date: Sun, 26 May 2024 22:17:25 +0100 Subject: [PATCH 008/188] Correct UriKind Check --- 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 9cb9b73..dcf588e 100644 --- a/Multiplayer/Components/MainMenu/MultiplayerPane.cs +++ b/Multiplayer/Components/MainMenu/MultiplayerPane.cs @@ -59,7 +59,7 @@ private void ShowIpPopup() inputUrl = "http://" + inputUrl; } - bool isValidURL = Uri.TryCreate(inputUrl, UriKind.RelativeOrAbsolute, out Uri uriResult) + bool isValidURL = Uri.TryCreate(inputUrl, UriKind.Absolute, out Uri uriResult) && (uriResult.Scheme == Uri.UriSchemeHttp || uriResult.Scheme == Uri.UriSchemeHttps); 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 009/188] 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 010/188] 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 011/188] 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 012/188] 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 013/188] 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 014/188] 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 015/188] 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 016/188] 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 017/188] 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 018/188] 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 019/188] 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 020/188] 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 021/188] 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 022/188] 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 023/188] 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 024/188] 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 025/188] 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 026/188] 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 027/188] 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 028/188] 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 029/188] 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 030/188] 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 031/188] 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 032/188] 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 033/188] 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 034/188] 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 035/188] 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 036/188] 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 037/188] 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 038/188] 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 From 9d3fb9951246bc92cf1dfdb38bc341ab525acb1b Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 14 Jul 2024 12:41:53 +1000 Subject: [PATCH 039/188] Squashed commit of the following: commit 633bdc03e33ad60796eefc5aa7f995fdbfdfd15d Author: AMacro Date: Sun Jul 14 12:40:25 2024 +1000 Cleaved up excessive logging commit c30a2e497f494349443977291abd1a84adf77fd3 Author: AMacro Date: Sun Jul 14 12:14:41 2024 +1000 Back-merged beta to feature/sync-jobs commit 5db5133b5fa4a700fe75fe58c1c86c9f2b565ade Merge: c0d547e fa0fbfb Author: AMacro Date: Sun Jul 14 11:39:10 2024 +1000 Merge branch 'beta' into feature/sync-jobs commit c0d547e62654abd746bea16666ad4685a39f3cec Author: AMacro Date: Sun Jul 14 11:26:17 2024 +1000 Preparing to merge to beta commit 263cc55a9069fc656fe042e4bce1f52e3ed5f340 Author: AMacro Date: Sat May 18 15:29:02 2024 +1000 Fix car plate sync and added better job sync pt2 Added car plate syncing Changed time format into UTC for logging Jobs now sync in a batch when a player connects and then progressively sync as new items are added commit eff0205c25e7e3b1028fd3c05c148d15c4c37667 Author: AMacro Date: Sat May 18 15:27:24 2024 +1000 Fix car plate sync and added better job sync Added car plate syncing Changed time format into UTC for logging Jobs now sync in a batch when a player connects and then progressively sync as new items are added commit 8adb497661adc8e161983a0cdb7ef806e06fb1b5 Author: AMacro Date: Sat May 18 11:09:05 2024 +1000 Fixed infinite blank spawning (CargoType data Type) CaregoType is now held as a CargoType, rather than byte in TaskDataData CargoType is now serialised as int (byte is not wide enough to store all values of CargoType) commit 440b9172b20b4c7c5af61805b7b91b4ee16644e2 Author: AMacro Date: Sat May 18 10:56:38 2024 +1000 Fixed PlayerSqrDistanceFrom*() calculation Calcs now use player WorldPosition instead of RawPosition. This is inline with the game's internal calcs and gives the correct result. commit 220a04aad5915ff9a4b0a6cd690d0af86166834e Merge: c42f868 e1a3e97 Author: AMacro Date: Sat May 18 09:58:58 2024 +1000 Merge branch 'Join-Menu-Improvements' into feature/sync-jobs commit c42f86829bb202382a0e6d81d35464ef55c8bb3a Author: AMacro Date: Sat May 18 09:41:26 2024 +1000 Revert "Merge branch 'Join-Menu-Improvements' into feature/sync-jobs" This reverts commit c376c80d4071478aba46084a48325b398eea2bb2, reversing changes made to 8df7a754455b3eb3331247b8faf69d678d226a3a. commit e1a3e97cdb439599db5c63e5763112b9ac186c26 Author: AMacro Date: Sun May 12 18:46:23 2024 +1000 Reworked the saving of last direct connection details Separated server and client settings commit c376c80d4071478aba46084a48325b398eea2bb2 Merge: 8df7a75 6daa671 Author: AMacro Date: Sun May 12 11:32:55 2024 +1000 Merge branch 'Join-Menu-Improvements' into feature/sync-jobs commit 8df7a754455b3eb3331247b8faf69d678d226a3a Merge: e37dd3e 764bfc7 Author: AMacro Date: Sun May 12 11:32:45 2024 +1000 Merge branch 'Localisation-Parsing-Fix' into feature/sync-jobs commit 6daa671d082bbc6cd2b85119f51e23c32b9a3b3f Author: AMacro Date: Sun May 12 10:45:10 2024 +1000 Enhanced "join" interface Default remote IP can now be set through the settings Popup/prompt for IP, port and password now auto-fill from the defaults commit e37dd3e07bda9344a2856ef5cdd0fdf7d6dfd82c Author: ChaoticWagon Date: Mon Sep 18 18:00:07 2023 -0400 Job syncing almost works --- .../Components/MainMenu/HostGamePane.cs | 14 +- .../MainMenu/MainMenuThingsAndStuff.cs | 2 +- ...pupTextInputFieldControllerNoValidation.cs | 2 +- .../Components/MainMenu/ServerBrowserPane.cs | 76 +--- .../Networking/Jobs/NetworkedJob.cs | 342 ++++++++++++++++ .../Train/NetworkTrainsetWatcher.cs | 1 + .../Networking/Train/NetworkedTrainCar.cs | 40 ++ .../Networking/World/NetworkedStation.cs | 120 ++++++ .../Components/StationComponentLookup.cs | 50 +++ Multiplayer/Multiplayer.cs | 2 +- Multiplayer/Networking/Data/JobData.cs | 139 +++++++ Multiplayer/Networking/Data/ModInfo.cs | 4 +- Multiplayer/Networking/Data/TaskDataData.cs | 371 ++++++++++++++++++ .../Managers/Client/NetworkClient.cs | 190 ++++++++- .../Networking/Managers/NetworkManager.cs | 5 +- .../Managers/Server/NetworkServer.cs | 147 +++++-- .../Jobs/ClientboundJobCreatePacket.cs | 22 ++ .../Clientbound/Jobs/ClientboundJobPacket.cs | 11 + .../Jobs/ClientboundJobTakeResponsePacket.cs | 12 + .../Jobs/ServerboundJobTakeRequestPacket.cs | 10 + .../Patches/Jobs/JobOverviewUsePatch.cs | 66 ++++ .../Patches/Jobs/StationControllerPatch.cs | 13 + .../Jobs/StationJobGenerationRangePatch.cs | 53 +++ Multiplayer/Patches/Jobs/StationPatch.cs | 34 ++ .../StationProceduralJobsControllerPatch.cs | 2 +- .../MainMenu/LauncherControllerPatch.cs | 5 +- Multiplayer/Utils/Csv.cs | 14 +- info.json | 2 +- 28 files changed, 1616 insertions(+), 133 deletions(-) create mode 100644 Multiplayer/Components/Networking/Jobs/NetworkedJob.cs create mode 100644 Multiplayer/Components/Networking/World/NetworkedStation.cs create mode 100644 Multiplayer/Components/StationComponentLookup.cs create mode 100644 Multiplayer/Networking/Data/JobData.cs create mode 100644 Multiplayer/Networking/Data/TaskDataData.cs create mode 100644 Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobCreatePacket.cs create mode 100644 Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobPacket.cs create mode 100644 Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobTakeResponsePacket.cs create mode 100644 Multiplayer/Networking/Packets/Serverbound/Jobs/ServerboundJobTakeRequestPacket.cs create mode 100644 Multiplayer/Patches/Jobs/JobOverviewUsePatch.cs create mode 100644 Multiplayer/Patches/Jobs/StationControllerPatch.cs create mode 100644 Multiplayer/Patches/Jobs/StationJobGenerationRangePatch.cs create mode 100644 Multiplayer/Patches/Jobs/StationPatch.cs rename Multiplayer/Patches/{World => Jobs}/StationProceduralJobsControllerPatch.cs (90%) diff --git a/Multiplayer/Components/MainMenu/HostGamePane.cs b/Multiplayer/Components/MainMenu/HostGamePane.cs index a072dd1..043c683 100644 --- a/Multiplayer/Components/MainMenu/HostGamePane.cs +++ b/Multiplayer/Components/MainMenu/HostGamePane.cs @@ -103,28 +103,28 @@ private void BuildUI() GameObject dividerPrefab = goMMC.FindChildByName("Divider"); if (dividerPrefab == null) { - Debug.Log("Divider not found!"); + Multiplayer.LogError("Divider not found!"); return; } GameObject cbPrefab = goMMC.FindChildByName("CheckboxFreeCam"); if (cbPrefab == null) { - Debug.Log("CheckboxFreeCam not found!"); + Multiplayer.LogError("CheckboxFreeCam not found!"); return; } GameObject sliderPrefab = goMMC.FindChildByName("SliderLimitSession"); if (sliderPrefab == null) { - Debug.Log("SliderLimitSession not found!"); + Multiplayer.LogError("SliderLimitSession not found!"); return; } GameObject inputPrefab = MainMenuThingsAndStuff.Instance.renamePopupPrefab.gameObject.FindChildByName("TextFieldTextIcon"); if (inputPrefab == null) { - Debug.Log("TextFieldTextIcon not found!"); + Multiplayer.LogError("TextFieldTextIcon not found!"); return; } @@ -132,7 +132,7 @@ private void BuildUI() lcInstance = goMMC.FindChildByName("PaneRight Launcher").GetComponent(); if (lcInstance == null) { - Debug.Log("No Run Button"); + Multiplayer.LogError("No Run Button"); return; } Sprite playSprite = lcInstance.runButton.FindChildByName("[icon]").GetComponent().sprite; @@ -331,7 +331,7 @@ private void ValidateInputs(string text) startButton.ToggleInteractable(valid); - Debug.Log($"Validated: {valid}"); + //Multiplayer.Log($"HostPane validated: {valid}"); } @@ -391,7 +391,7 @@ private void StartClick() var ContinueGameRequested = lcInstance.GetType().GetMethod("OnRunClicked", BindingFlags.NonPublic | BindingFlags.Instance); - Debug.Log($"OnRunClicked exists: {ContinueGameRequested != null}"); + //Multiplayer.Log($"OnRunClicked exists: {ContinueGameRequested != null}"); ContinueGameRequested?.Invoke(lcInstance, null); } diff --git a/Multiplayer/Components/MainMenu/MainMenuThingsAndStuff.cs b/Multiplayer/Components/MainMenu/MainMenuThingsAndStuff.cs index b081b36..732a941 100644 --- a/Multiplayer/Components/MainMenu/MainMenuThingsAndStuff.cs +++ b/Multiplayer/Components/MainMenu/MainMenuThingsAndStuff.cs @@ -71,7 +71,7 @@ public void SwitchToMenu(byte index) [CanBeNull] public Popup ShowRenamePopup() { - Debug.Log("public Popup ShowRenamePopup() ..."); + Multiplayer.Log("public Popup ShowRenamePopup() ..."); return ShowPopup(renamePopupPrefab); } diff --git a/Multiplayer/Components/MainMenu/ServerBrowser/PopupTextInputFieldControllerNoValidation.cs b/Multiplayer/Components/MainMenu/ServerBrowser/PopupTextInputFieldControllerNoValidation.cs index 1cda123..170caab 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowser/PopupTextInputFieldControllerNoValidation.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowser/PopupTextInputFieldControllerNoValidation.cs @@ -64,7 +64,7 @@ public void HandleAction(PopupClosedByAction action) RequestAbortion(); return; default: - Debug.LogError(string.Format("Unhandled action {0}", action), this); + Multiplayer.LogError(string.Format("Unhandled action {0}", action)); break; } } diff --git a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs index 113b3f0..f74576a 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs @@ -71,44 +71,8 @@ public class ServerBrowserPane : MonoBehaviour private void Awake() { - Multiplayer.Log("MultiplayerPane Awake()"); - /* - * - * Temp testing code - * - */ + //Multiplayer.Log("MultiplayerPane Awake()"); - //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(); @@ -119,12 +83,12 @@ private void Awake() private void OnEnable() { - Multiplayer.Log("MultiplayerPane OnEnable()"); + //Multiplayer.Log("MultiplayerPane OnEnable()"); if (!this.parentScroller) { - Multiplayer.Log("Find ScrollRect"); + //Multiplayer.Log("Find ScrollRect"); this.parentScroller = this.gridView.GetComponentInParent(); - Multiplayer.Log("Found ScrollRect"); + //Multiplayer.Log("Found ScrollRect"); } this.SetupListeners(true); this.serverIDOnRefresh = ""; @@ -376,7 +340,7 @@ private void JoinAction() private void DirectAction() { - Debug.Log($"DirectAction()"); + //Debug.Log($"DirectAction()"); buttonDirectIP.ToggleInteractable(false); buttonJoin.ToggleInteractable(false) ; @@ -388,13 +352,13 @@ private void DirectAction() private void IndexChanged(AGridView gridView) { - Debug.Log($"Index: {gridView.SelectedModelIndex}"); + //Debug.Log($"Index: {gridView.SelectedModelIndex}"); if (serverRefreshing) return; if (gridView.SelectedModelIndex >= 0) { - Debug.Log($"Selected server: {gridViewModel[gridView.SelectedModelIndex].Name}"); + //Debug.Log($"Selected server: {gridViewModel[gridView.SelectedModelIndex].Name}"); selectedServer = gridViewModel[gridView.SelectedModelIndex]; @@ -402,9 +366,9 @@ private void IndexChanged(AGridView gridView) //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()}\""); + Multiplayer.Log($"Server: \"{selectedServer.GameVersion}\" \"{selectedServer.MultiplayerVersion}\""); + Multiplayer.Log($"Client: \"{BuildInfo.BUILD_VERSION_MAJOR.ToString()}\" \"{Multiplayer.ModEntry.Version.ToString()}\""); + Multiplayer.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(); @@ -425,7 +389,7 @@ private void UpdateDetailsPane() if (selectedServer != null) { - Debug.Log("Prepping Data"); + //Multiplayer.Log("Prepping Data"); serverName.text = selectedServer.Name; //note: built-in localisations have a trailing colon e.g. 'Game mode:' @@ -442,14 +406,14 @@ private void UpdateDetailsPane() details += "
"; details += selectedServer.ServerDetails; - Debug.Log("Finished Prepping Data"); + //Multiplayer.Log("Finished Prepping Data"); detailsPane.text = details; } } private void ShowIpPopup() { - Debug.Log("In ShowIpPpopup"); + Multiplayer.Log("In ShowIpPpopup"); var popup = MainMenuThingsAndStuff.Instance.ShowRenamePopup(); if (popup == null) { @@ -568,7 +532,7 @@ private void ShowPasswordPopup() private void HandleConnectionEstablished() { // Connection established, handle the UI or game state accordingly - Debug.Log("Connection established!"); + Multiplayer.Log("Connection established!"); // HideConnectingPopup(); // Hide the connecting message } @@ -576,7 +540,7 @@ private void HandleConnectionEstablished() private void HandleConnectionFailed() { // Connection failed, show an error message or handle the failure scenario - Debug.LogError("Connection failed!"); + Multiplayer.LogError("Connection failed!"); // ShowConnectionFailedPopup(); } @@ -592,21 +556,21 @@ IEnumerator GetRequest(string uri) if (webRequest.isNetworkError) { - Debug.Log(pages[page] + ": Error: " + webRequest.error); + Multiplayer.LogError(pages[page] + ": Error: " + webRequest.error); } else { - Debug.Log(pages[page] + ":\nReceived: " + webRequest.downloadHandler.text); + Multiplayer.Log(pages[page] + ":\nReceived: " + webRequest.downloadHandler.text); LobbyServerData[] response; response = Newtonsoft.Json.JsonConvert.DeserializeObject(webRequest.downloadHandler.text); - Debug.Log($"servers: {response.Length}"); + Multiplayer.Log($"Serverbrowser servers: {response.Length}"); foreach (LobbyServerData server in response) { - Debug.Log($"Name: {server.Name}\tIP: {server.ip}"); + Multiplayer.Log($"Server name: {server.Name}\tIP: {server.ip}"); } if (response.Length == 0) @@ -686,7 +650,7 @@ private void FillDummyServers() item.MultiplayerVersion = UnityEngine.Random.Range(1, 10) > 3 ? Multiplayer.ModEntry.Version.ToString() : "0.1.0"; - Debug.Log(item.HasPassword); + //Debug.Log(item.HasPassword); gridViewModel.Add(item); } diff --git a/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs b/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs new file mode 100644 index 0000000..1980329 --- /dev/null +++ b/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs @@ -0,0 +1,342 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using DV.Logic.Job; +using DV.ThingTypes; +using DV.Utils; +using Multiplayer.Components.Networking.Player; +using Multiplayer.Components.Networking.World; +using Multiplayer.Networking.Data; +using Multiplayer.Utils; +using UnityEngine; +using static System.Collections.Specialized.BitVector32; + +namespace Multiplayer.Components.Networking.Jobs; + +public class NetworkedJob : IdMonoBehaviour +{ + #region Lookup Cache + + private static readonly Dictionary jobToNetworkedJob = new(); + private static readonly Dictionary jobIdToNetworkedJob = new(); + private static readonly Dictionary jobIdToJob = new(); + + public static bool Get(ushort netId, out NetworkedJob obj) + { + bool b = Get(netId, out IdMonoBehaviour rawObj); + obj = (NetworkedJob)rawObj; + return b; + } + + public static bool GetJob(ushort netId, out Job obj) + { + bool b = Get(netId, out NetworkedJob networkedJob); + obj = b ? networkedJob.job : null; + return b; + } + + + public static NetworkedJob GetFromJob(Job job) + { + return jobToNetworkedJob[job]; + } + + public static bool TryGetFromJob(Job job, out NetworkedJob networkedJob) + { + return jobToNetworkedJob.TryGetValue(job, out networkedJob); + } + + /*public static NetworkedJob AddJob(string stationID, Job job) + { + NetworkedJob netJob = new NetworkedJob(stationID, job); + + jobToNetworkedJob[job] = netJob; + jobIdToNetworkedJob[job.ID] = netJob; + jobIdToJob[job.ID] = job; + + Multiplayer.Log($"NetworkedJob Added with netId: {jobToNetworkedJob[job].NetId}, jobId: {job.ID}"); + return jobToNetworkedJob[job]; + }*/ + #endregion + + public Job job; + public JobOverview jobOverview; + public JobBooklet jobBooklet; + public string stationID; + public bool isJobNew = true; + public bool isJobDirty = false; + public bool isTaskDirty = false; + + public bool? allowTake = null; + public Guid takenBy; //GUID of player who took the job + public JobValidator jobValidator; + + //might be useful when a job is taken? + //public bool HasPlayers => PlayerManager.Car == Job || GetComponentInChildren() != null; + + #region Client + + private bool client_Initialized; + + #endregion + + protected override bool IsIdServerAuthoritative => true; + + protected override void Awake() + { + Multiplayer.Log("NetworkJob.Awake()"); + base.Awake(); + + + /* + job = GetComponent(); + jobToNetworkedJob[job] = this; + + + + if (NetworkLifecycle.Instance.IsHost()) + { + //do we need a job watcher - probably not, but maybe or maybe we need a task watcher + //NetworkTrainsetWatcher.Instance.CheckInstance(); // Ensure the NetworkTrainsetWatcher is initialized + } + else + { + //Networked task?? + + //Client_trainSpeedQueue = TrainCar.GetOrAddComponent(); + //Client_trainRigidbodyQueue = TrainCar.GetOrAddComponent(); + //StartCoroutine(Client_InitLater()); + } + */ + } + + private void Start() + { + //startup stuff + Multiplayer.Log("NetworkedJob.Start()"); + + jobToNetworkedJob[job] = this; + jobIdToNetworkedJob[job.ID] = this; + jobIdToJob[job.ID] = job; + + isJobNew = true; //Send new jobs on tick + + StationController station; + if (!StationComponentLookup.Instance.StationControllerFromId(stationID, out station)) + { + Multiplayer.LogWarning($"NetworkJob.Start() Could not get staion for stationId: {stationID}"); + return; + } + + if (!NetworkLifecycle.Instance.IsHost()) + { + //station.logicStation.AddJobToStation(job); + if (station.logicStation.availableJobs.Contains(job)) + { + Multiplayer.LogError("Trying to add the same job[" + job.ID + "] multiple times to station! Skipping, trying to recover."); + return; + } + + station.logicStation.availableJobs.Add(job); + job.JobTaken += this.OnJobTaken; + job.JobExpired += this.OnJobExpired; + //job.JobAddedToStation?.Invoke(); + SingletonBehaviour.Instance.StartCoroutine(NetworkedStation.UpdateCarPlates(job.tasks, job.ID)); + } + else + { + //setup even handlers + job.JobTaken += this.OnJobTaken; + job.JobExpired += this.OnJobExpired; + NetworkLifecycle.Instance.OnTick += Server_OnTick; + } + + Multiplayer.Log("NetworkedJob.Start() Started"); + //possibly capture tasks at this point for tracking?? + } + + private void OnDisable() + { + if (UnloadWatcher.isQuitting) + return; + + NetworkLifecycle.Instance.OnTick -= Common_OnTick; + NetworkLifecycle.Instance.OnTick -= Server_OnTick; + + if (UnloadWatcher.isUnloading) + return; + + job.JobTaken -= this.OnJobTaken; + + jobToNetworkedJob.Remove(job); + jobIdToNetworkedJob.Remove(job.ID); + jobIdToNetworkedJob.Remove(job.ID); + + //Clean up any actions we added + + if (NetworkLifecycle.Instance.IsHost()) + { + //actions relating only to host + } + + Destroy(this); + } + + /*public NetworkedJob(string stationID, Job job) + { + this.job = job; + this.stationID = stationID; + + //setup even handlers + //job.JobTaken += + + isJobNew = true; //Send new jobs on tick + + }*/ + + #region Server + + //wait for tasks? + + /* + public bool Server_ValidateClientTakeJob(ServerPlayer player, CommonTrainPortsPacket packet) + { + + return false; + } + */ + + /* + public bool Server_ValidateClientAbandonedJob(ServerPlayer player, CommonTrainPortsPacket packet) + { + + return false; + } + */ + + /* + public bool Server_ValidateClientCompleteJob(ServerPlayer player, CommonTrainPortsPacket packet) + { + + return false; + } + */ + + + private void Server_OnTick(uint tick) + { + if (UnloadWatcher.isUnloading) + return; + + Server_SendNewJob(); + //Server_SendJobStatus(); + //Server_SendTaskStatus(); + //Server_SendJobDestroy(); + + } + + private void Server_SendNewJob() + { + if (!isJobNew) + return; + + isJobNew = false; + NetworkLifecycle.Instance.Server.SendJobCreatePacket(this); + } + /* + private void Server_SendJobStatus() + { + if (!sendCouplers) + return; + sendCouplers = false; + + if (Job.frontCoupler.hoseAndCock.IsHoseConnected) + NetworkLifecycle.Instance.Client.SendHoseConnected(Job.frontCoupler, Job.frontCoupler.coupledTo, false); + + if (Job.rearCoupler.hoseAndCock.IsHoseConnected) + NetworkLifecycle.Instance.Client.SendHoseConnected(Job.rearCoupler, Job.rearCoupler.coupledTo, false); + + NetworkLifecycle.Instance.Client.SendCockState(NetId, Job.frontCoupler, Job.frontCoupler.IsCockOpen); + NetworkLifecycle.Instance.Client.SendCockState(NetId, Job.rearCoupler, Job.rearCoupler.IsCockOpen); + } + */ + + + #endregion + + #region Common + + private void Common_OnTick(uint tick) + { + if (UnloadWatcher.isUnloading) + return; + /* + Common_SendHandbrakePosition(); + Common_SendFuses(); + Common_SendPorts(); + */ + } + + public void OnJobTaken(Job jobTaken,bool _) + { + Multiplayer.Log($"JobTaken: {jobTaken.ID}"); + jobTaken.JobTaken -= this.OnJobTaken; + jobTaken.JobExpired -= this.OnJobExpired; + + /* + takenJob.JobCompleted += OnJobCompleted; + takenJob.JobAbandoned += OnJobAbandoned; + availableJobs.Remove(takenJob); + takenJobs.Add(takenJob); + */ + + isJobDirty = true; + /* + jobTaken.JobExpired -= this.OnJobExpired; + jobTaken.JobCompleted += this.OnJobCompleted; + jobTaken.JobAbandoned += this.OnJobAbandoned; + */ + } + + public void OnJobExpired(Job jobExpired) + { + Multiplayer.Log($"Job Expired: {job.ID}"); + jobExpired.JobTaken -= this.OnJobTaken; + jobExpired.JobExpired -= this.OnJobExpired; + //jobExpired.JobCompleted += this.OnJobCompleted; + //jobExpired.JobAbandoned += this.OnJobAbandoned; + + isJobDirty = true; + + } + + #endregion + + #region Client + + /* + public void Client_ReceiveJopStatus(in TrainsetMovementPart movementPart, uint tick) + { + if (!client_Initialized) + return; + if (Job.isEligibleForSleep) + Job.ForceOptimizationState(false); + + if (movementPart.IsRigidbodySnapshot) + { + Job.Derail(); + Job.stress.ResetTrainStress(); + Client_trainRigidbodyQueue.ReceiveSnapshot(movementPart.RigidbodySnapshot, tick); + } + else + { + Client_trainSpeedQueue.ReceiveSnapshot(movementPart.Speed, tick); + Job.stress.slowBuildUpStress = movementPart.SlowBuildUpStress; + client_bogie1Queue.ReceiveSnapshot(movementPart.Bogie1, tick); + client_bogie2Queue.ReceiveSnapshot(movementPart.Bogie2, tick); + } + } + */ + #endregion +} diff --git a/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs b/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs index 249b47f..03ee184 100644 --- a/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs +++ b/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs @@ -73,6 +73,7 @@ private void Server_TickSet(Trainset set) TrainCar trainCar = set.cars[i]; if (!trainCar.TryNetworked(out NetworkedTrainCar _)) { + Multiplayer.LogDebug(() => $"TrainCar UNKNOWN is not networked! Is active? {trainCar.gameObject.activeInHierarchy}"); Multiplayer.LogDebug(() => $"TrainCar {trainCar.ID} is not networked! Is active? {trainCar.gameObject.activeInHierarchy}"); continue; } diff --git a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs index 436649c..c50a714 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs @@ -20,6 +20,8 @@ public class NetworkedTrainCar : IdMonoBehaviour #region Lookup Cache private static readonly Dictionary trainCarsToNetworkedTrainCars = new(); + private static readonly Dictionary trainCarIdToNetworkedTrainCars = new(); + private static readonly Dictionary trainCarIdToTrainCars = new(); private static readonly Dictionary hoseToCoupler = new(); public static bool Get(ushort netId, out NetworkedTrainCar obj) @@ -45,6 +47,14 @@ public static NetworkedTrainCar GetFromTrainCar(TrainCar trainCar) { return trainCarsToNetworkedTrainCars[trainCar]; } + public static bool GetFromTrainId(string carId, out NetworkedTrainCar networkedTrainCar) + { + return trainCarIdToNetworkedTrainCars.TryGetValue(carId, out networkedTrainCar); + } + public static bool GetTrainCarFromTrainId(string carId, out TrainCar trainCar) + { + return trainCarIdToTrainCars.TryGetValue(carId, out trainCar); + } public static bool TryGetFromTrainCar(TrainCar trainCar, out NetworkedTrainCar networkedTrainCar) { @@ -97,6 +107,8 @@ protected override void Awake() TrainCar = GetComponent(); trainCarsToNetworkedTrainCars[TrainCar] = this; + TrainCar.LogicCarInitialized += OnLogicCarInitialised; + bogie1 = TrainCar.Bogies[0]; bogie2 = TrainCar.Bogies[1]; @@ -157,7 +169,14 @@ private void OnDisable() NetworkLifecycle.Instance.OnTick -= Server_OnTick; if (UnloadWatcher.isUnloading) return; + trainCarsToNetworkedTrainCars.Remove(TrainCar); + if (TrainCar.logicCar != null) + { + trainCarIdToNetworkedTrainCars.Remove(TrainCar.ID); + trainCarIdToTrainCars.Remove(TrainCar.ID); + } + foreach (Coupler coupler in TrainCar.couplers) hoseToCoupler.Remove(coupler.hoseAndCock); brakeSystem.HandbrakePositionChanged -= Common_OnHandbrakePositionChanged; @@ -179,10 +198,27 @@ private void OnDisable() #region Server + private void OnLogicCarInitialised() + { + //Multiplayer.LogWarning("OnLogicCarInitialised"); + if (TrainCar.logicCar != null) + { + trainCarIdToNetworkedTrainCars[TrainCar.ID] = this; + trainCarIdToTrainCars[TrainCar.ID] = TrainCar; + + TrainCar.LogicCarInitialized -= OnLogicCarInitialised; + } + else + { + Multiplayer.LogWarning("OnLogicCarInitialised Car Not Initialised!"); + } + + } private IEnumerator Server_WaitForLogicCar() { while (TrainCar.logicCar == null) yield return null; + TrainCar.logicCar.CargoLoaded += Server_OnCargoLoaded; TrainCar.logicCar.CargoUnloaded += Server_OnCargoUnloaded; NetworkLifecycle.Instance.Server.SendSpawnTrainCar(this); @@ -334,6 +370,8 @@ public void Common_DirtyPorts(string[] portIds) { if (!simulationFlow.TryGetPort(portId, out Port _)) { + + Multiplayer.LogWarning($"Tried to dirty port {portId} on UNKNOWN but it doesn't exist!"); Multiplayer.LogWarning($"Tried to dirty port {portId} on {TrainCar.ID} but it doesn't exist!"); continue; } @@ -351,6 +389,7 @@ public void Common_DirtyFuses(string[] fuseIds) { if (!simulationFlow.TryGetFuse(fuseId, out Fuse _)) { + Multiplayer.LogWarning($"Tried to dirty port {fuseId} on UNKOWN but it doesn't exist!"); Multiplayer.LogWarning($"Tried to dirty port {fuseId} on {TrainCar.ID} but it doesn't exist!"); continue; } @@ -462,6 +501,7 @@ private IEnumerator Client_InitLater() yield return null; while ((client_bogie2Queue = bogie2.GetComponent()) == null) yield return null; + client_Initialized = true; } diff --git a/Multiplayer/Components/Networking/World/NetworkedStation.cs b/Multiplayer/Components/Networking/World/NetworkedStation.cs new file mode 100644 index 0000000..141dd5b --- /dev/null +++ b/Multiplayer/Components/Networking/World/NetworkedStation.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using DV.Logic.Job; +using Multiplayer.Components.Networking.Train; +using UnityEngine; +using static DV.Common.GameFeatureFlags; +using static DV.UI.ATutorialsMenuProvider; + +namespace Multiplayer.Components.Networking.World; + +public class NetworkedStation : MonoBehaviour +{ + private StationController stationController; + + private void Awake() + { + Multiplayer.Log("NetworkedStation.Awake()"); + + stationController = GetComponent(); + StartCoroutine(WaitForLogicStation()); + } + + private IEnumerator WaitForLogicStation() + { + while (stationController.logicStation == null) + yield return null; + + StationComponentLookup.Instance.RegisterStation(stationController); + + Multiplayer.Log("NetworkedStation.Awake() done"); + } + + public static IEnumerator UpdateCarPlates(List tasks, string jobId) + { + + List cars = new List(); + UpdateCarPlatesRecursive(tasks, jobId, ref cars); + + + if (cars != null) + { + Multiplayer.Log("NetworkedStation.UpdateCarPlates() Cars count: " + cars.Count); + + foreach (Car car in cars) + { + Multiplayer.Log("NetworkedStation.UpdateCarPlates() Car: " + car.ID); + + TrainCar trainCar = null; + int loopCtr = 0; + while (!NetworkedTrainCar.GetTrainCarFromTrainId(car.ID, out trainCar)) + { + loopCtr++; + if (loopCtr > 5000) + { + Multiplayer.Log("NetworkedStation.UpdateCarPlates() TimeOut"); + break; + } + + + yield return null; + } + + trainCar?.UpdateJobIdOnCarPlates(jobId); + } + } + } + private static void UpdateCarPlatesRecursive(List tasks, string jobId, ref List cars) + { + Multiplayer.Log("NetworkedStation.UpdateCarPlatesRecursive() Starting"); + + foreach (Task task in tasks) + { + if (task is WarehouseTask) + { + Multiplayer.Log("NetworkedStation.UpdateCarPlatesRecursive() WarehouseTask"); + cars = cars.Union(((WarehouseTask)task).cars).ToList(); + } + else if (task is TransportTask) + { + Multiplayer.Log("NetworkedStation.UpdateCarPlatesRecursive() TransportTask"); + cars = cars.Union(((TransportTask)task).cars).ToList(); + } + else if (task is SequentialTasks) + { + Multiplayer.Log("NetworkedStation.UpdateCarPlatesRecursive() SequentialTasks"); + List seqTask = new(); + + for (LinkedListNode node = ((SequentialTasks)task).tasks.First; node != null; node = node.Next) + { + Multiplayer.Log($"NetworkedStation.UpdateCarPlatesRecursive() SequentialTask Adding node"); + seqTask.Add(node.Value); + } + + Multiplayer.Log($"NetworkedStation.UpdateCarPlatesRecursive() SequentialTask Node Count:{seqTask.Count}"); + + Multiplayer.Log("NetworkedStation.UpdateCarPlatesRecursive() Calling UpdateCarPlates()"); + //drill down + UpdateCarPlatesRecursive(seqTask, jobId, ref cars); + Multiplayer.Log($"NetworkedStation.UpdateCarPlatesRecursive() SequentialTask RETURNED"); + } + else if (task is ParallelTasks) + { + //not implemented + Multiplayer.Log("NetworkedStation.UpdateCarPlatesRecursive() ParallelTasks"); + + Multiplayer.Log("NetworkedStation.UpdateCarPlatesRecursive() Calling UpdateCarPlates()"); + //drill down + UpdateCarPlatesRecursive(((ParallelTasks)task).tasks, jobId, ref cars); + } + else + { + throw new ArgumentException("NetworkedStation.UpdateCarPlatesRecursive() Unknown task type: " + task.GetType()); + } + } + + Multiplayer.Log("NetworkedStation.UpdateCarPlatesRecursive() Returning"); + } +} diff --git a/Multiplayer/Components/StationComponentLookup.cs b/Multiplayer/Components/StationComponentLookup.cs new file mode 100644 index 0000000..3f30f62 --- /dev/null +++ b/Multiplayer/Components/StationComponentLookup.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using DV.Logic.Job; +using DV.Utils; +using JetBrains.Annotations; +using Multiplayer.Components.Networking.World; + +namespace Multiplayer.Components; + +public class StationComponentLookup : SingletonBehaviour +{ + private readonly Dictionary stationToNetworkedStationController = new(); + private readonly Dictionary stationIdToNetworkedStation = new(); + private readonly Dictionary stationIdToStationController = new(); + + public void RegisterStation(StationController stationController) + { + var networkedStation = stationController.GetComponent(); + stationToNetworkedStationController[stationController.logicStation] = networkedStation; + stationIdToNetworkedStation[stationController.logicStation.ID] = networkedStation; + stationIdToStationController[stationController.logicStation.ID] = stationController; + } + + public void UnregisterStation(StationController stationController) + { + stationToNetworkedStationController.Remove(stationController.logicStation); + stationIdToNetworkedStation.Remove(stationController.logicStation.ID); + stationIdToStationController.Remove(stationController.logicStation.ID); + } + + public bool NetworkedStationFromStation(Station station, out NetworkedStation networkedStation) + { + return stationToNetworkedStationController.TryGetValue(station, out networkedStation); + } + + public bool NetworkedStationFromId(string stationId, out NetworkedStation networkedStation) + { + return stationIdToNetworkedStation.TryGetValue(stationId, out networkedStation); + } + + public bool StationControllerFromId(string stationId, out StationController stationController) + { + return stationIdToStationController.TryGetValue(stationId, out stationController); + } + + [UsedImplicitly] + public new static string AllowAutoCreate() + { + return $"[{nameof(StationComponentLookup)}]"; + } +} diff --git a/Multiplayer/Multiplayer.cs b/Multiplayer/Multiplayer.cs index e8a1494..b54272e 100644 --- a/Multiplayer/Multiplayer.cs +++ b/Multiplayer/Multiplayer.cs @@ -130,7 +130,7 @@ public static void LogException(object msg, Exception e) private static void WriteLog(string msg) { - string str = $"[{DateTime.Now:HH:mm:ss.fff}] {msg}"; + string str = $"[{DateTime.Now.ToUniversalTime():HH:mm:ss.fff}] {msg}"; if (Settings.EnableLogFile) File.AppendAllLines(LOG_FILE, new[] { str }); ModEntry.Logger.Log(str); diff --git a/Multiplayer/Networking/Data/JobData.cs b/Multiplayer/Networking/Data/JobData.cs new file mode 100644 index 0000000..799199d --- /dev/null +++ b/Multiplayer/Networking/Data/JobData.cs @@ -0,0 +1,139 @@ +using System.Linq; +using DV.Logic.Job; +using LiteNetLib.Utils; +using Newtonsoft.Json; + +namespace Multiplayer.Networking.Data; + +public class JobData +{ + public byte JobType { get; set; } + public string ID { get; set; } + public TaskBeforeDataData[] Tasks { get; set; } + public StationsChainDataData ChainData { get; set; } + public int RequiredLicenses { get; set; } + public float StartTime { get; set; } + public float FinishTime { get; set; } + public float InitialWage { get; set; } + public byte State { get; set; } + public float TimeLimit { get; set; } + + public static JobData FromJob(Job job) + { + return new JobData + { + JobType = (byte)job.jobType, + ID = job.ID, + Tasks = job.tasks.Select(x => TaskBeforeDataData.FromTask(x)).ToArray(), + ChainData = StationsChainDataData.FromStationData(job.chainData), + RequiredLicenses = (int)job.requiredLicenses, + StartTime = job.startTime, + FinishTime = job.finishTime, + InitialWage = job.initialWage, + State = (byte)job.State, + TimeLimit = job.TimeLimit + }; + } + + public static void Serialize(NetDataWriter writer, JobData data) + { + writer.Put(data.JobType); + writer.Put(data.ID); + writer.Put((byte)data.Tasks.Length); + foreach (var taskBeforeDataData in data.Tasks) + TaskBeforeDataData.SerializeTask(taskBeforeDataData, writer); + StationsChainDataData.Serialize(writer, data.ChainData); + writer.Put(data.RequiredLicenses); + writer.Put(data.StartTime); + writer.Put(data.FinishTime); + writer.Put(data.InitialWage); + writer.Put(data.State); + writer.Put(data.TimeLimit); + Multiplayer.Log(JsonConvert.SerializeObject(data, Formatting.None)); + } + + public static JobData Deserialize(NetDataReader reader) + { + Multiplayer.Log("JobData.Deserialize()"); + var jobType = reader.GetByte(); + Multiplayer.Log("JobData.Deserialize() jobType: " + jobType); + var id = reader.GetString(); + Multiplayer.Log("JobData.Deserialize() id: " + id); + var tasksLength = reader.GetByte(); + Multiplayer.Log("JobData.Deserialize() tasksLength: " + tasksLength); + var tasks = new TaskBeforeDataData[tasksLength]; + for (int i = 0; i < tasksLength; i++) + tasks[i] = TaskBeforeDataData.DeserializeTask(reader); + //Multiplayer.Log("JobData.Deserialize() tasks: " + JsonConvert.SerializeObject(tasks, Formatting.None)); + var chainData = StationsChainDataData.Deserialize(reader); + //Multiplayer.Log("JobData.Deserialize() chainData: " + JsonConvert.SerializeObject(chainData, Formatting.Indented)); + var requiredLicenses = reader.GetInt(); + Multiplayer.Log("JobData.Deserialize() requiredLicenses: " + requiredLicenses); + var startTime = reader.GetFloat(); + Multiplayer.Log("JobData.Deserialize() startTime: " + startTime); + var finishTime = reader.GetFloat(); + Multiplayer.Log("JobData.Deserialize() finishTime: " + finishTime); + var initialWage = reader.GetFloat(); + Multiplayer.Log("JobData.Deserialize() initialWage: " + initialWage); + var state = reader.GetByte(); + Multiplayer.Log("JobData.Deserialize() state: " + state); + var timeLimit = reader.GetFloat(); + Multiplayer.Log(JsonConvert.SerializeObject(new JobData + { + JobType = jobType, + ID = id, + Tasks = tasks, + ChainData = chainData, + RequiredLicenses = requiredLicenses, + StartTime = startTime, + FinishTime = finishTime, + InitialWage = initialWage, + State = state, + TimeLimit = timeLimit + }, Formatting.None)); + + return new JobData + { + JobType = jobType, + ID = id, + Tasks = tasks, + ChainData = chainData, + RequiredLicenses = requiredLicenses, + StartTime = startTime, + FinishTime = finishTime, + InitialWage = initialWage, + State = state, + TimeLimit = timeLimit + }; + } +} + +public struct StationsChainDataData +{ + public string ChainOriginYardId { get; set; } + public string ChainDestinationYardId { get; set; } + + public static StationsChainDataData FromStationData(StationsChainData data) + { + return new StationsChainDataData + { + ChainOriginYardId = data.chainOriginYardId, + ChainDestinationYardId = data.chainDestinationYardId + }; + } + + public static void Serialize(NetDataWriter writer, StationsChainDataData data) + { + writer.Put(data.ChainOriginYardId); + writer.Put(data.ChainDestinationYardId); + } + + public static StationsChainDataData Deserialize(NetDataReader reader) + { + return new StationsChainDataData + { + ChainOriginYardId = reader.GetString(), + ChainDestinationYardId = reader.GetString() + }; + } +} diff --git a/Multiplayer/Networking/Data/ModInfo.cs b/Multiplayer/Networking/Data/ModInfo.cs index 323884e..451bbdb 100644 --- a/Multiplayer/Networking/Data/ModInfo.cs +++ b/Multiplayer/Networking/Data/ModInfo.cs @@ -11,8 +11,8 @@ public readonly struct ModInfo { public readonly string Id; public readonly string Version; - - private ModInfo(string id, string version) + + public ModInfo(string id, string version) { Id = id; Version = version; diff --git a/Multiplayer/Networking/Data/TaskDataData.cs b/Multiplayer/Networking/Data/TaskDataData.cs new file mode 100644 index 0000000..eb1be23 --- /dev/null +++ b/Multiplayer/Networking/Data/TaskDataData.cs @@ -0,0 +1,371 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using DV.Logic.Job; +using DV.ThingTypes; +using HarmonyLib; +using LiteNetLib.Utils; +using Newtonsoft.Json; + +namespace Multiplayer.Networking.Data; + +public abstract class TaskBeforeDataData +{ + public byte State { get; set; } + public float TaskStartTime { get; set; } + public float TaskFinishTime { get; set; } + public bool IsLastTask { get; set; } + public float TimeLimit { get; set; } + public byte TaskType { get; set; } + + + public static TaskBeforeDataData FromTask(Task task) + { + TaskBeforeDataData taskData = task switch + { + WarehouseTask warehouseTask => WarehouseTaskData.FromWarehouseTask(warehouseTask), + TransportTask transportTask => TransportTaskData.FromTransportTask(transportTask), + SequentialTasks sequentialTasks => SequentialTasksData.FromSequentialTask(sequentialTasks), + ParallelTasks parallelTasks => ParallelTasksData.FromParallelTask(parallelTasks), + _ => throw new ArgumentException("Unknown task type: " + task.GetType()) + }; + + taskData.State = (byte)task.state; + taskData.TaskStartTime = task.taskStartTime; + taskData.TaskFinishTime = task.taskFinishTime; + taskData.IsLastTask = task.IsLastTask; + taskData.TimeLimit = task.TimeLimit; + taskData.TaskType = (byte)task.InstanceTaskType; + + return taskData; + } + + public static Task ToTask(object data) + { + if (data is WarehouseTaskData) + { + var task = (WarehouseTaskData)data; + return WarehouseTaskData.ToWarehouseTask(task); + } + + if (data is TransportTaskData) + { + var task = (TransportTaskData)data; + return TransportTaskData.ToTransportTask(task); + } + + if (data is SequentialTasksData) + { + var task = (SequentialTasksData)data; + List tasks = new List(); + + foreach (TaskBeforeDataData taskBeforeDataData in task.Tasks) + tasks.Add(ToTask(taskBeforeDataData)); + + + return new SequentialTasks(tasks); + } + + if (data is ParallelTasksData) + { + var task = (ParallelTasksData)data; + List tasks = new List(); + + foreach (TaskBeforeDataData taskBeforeDataData in task.Tasks) + tasks.Add(ToTask(taskBeforeDataData)); + + + return new ParallelTasks(tasks); + } + + throw new ArgumentException("Unknown task type: " + data.GetType()); + } + + public static void SerializeTask(object data, NetDataWriter writer) + { + if (data is WarehouseTaskData) + { + var task = (WarehouseTaskData)data; + WarehouseTaskData.Serialize(writer, task); + return; + } + + if (data is TransportTaskData) + { + var task = (TransportTaskData)data; + TransportTaskData.Serialize(writer, task); + return; + } + + if (data is SequentialTasksData) + { + var task = (SequentialTasksData)data; + + SequentialTasksData.Serialize(writer, task); + + return; + } + + if (data is ParallelTasksData) + { + var task = (ParallelTasksData)data; + + ParallelTasksData.Serialize(writer, task); + + + return; + } + + throw new ArgumentException("Unknown task type: " + data.GetType()); + } + + public static TaskBeforeDataData DeserializeTask(NetDataReader reader) + { + TaskType taskType = (TaskType)reader.GetByte(); + Multiplayer.Log("Task type: " + taskType + ""); + + return taskType switch + { + DV.Logic.Job.TaskType.Warehouse => WarehouseTaskData.Deserialize(reader), + DV.Logic.Job.TaskType.Transport => TransportTaskData.Deserialize(reader), + DV.Logic.Job.TaskType.Sequential => SequentialTasksData.Deserialize(reader), + DV.Logic.Job.TaskType.Parallel => ParallelTasksData.Deserialize(reader), + _ => throw new ArgumentException("Unknown task type: " + taskType) + }; + } + + public static void Serialize(NetDataWriter writer, TaskBeforeDataData data) + { + writer.Put(data.TaskType); + writer.Put(data.State); + writer.Put(data.TaskStartTime); + writer.Put(data.TaskFinishTime); + writer.Put(data.IsLastTask); + writer.Put(data.TimeLimit); + writer.Put(data.TaskType); + } + + public static void Deserialize(NetDataReader reader, TaskBeforeDataData data) + { + data.State = reader.GetByte(); + data.TaskStartTime = reader.GetFloat(); + data.TaskFinishTime = reader.GetFloat(); + data.IsLastTask = reader.GetBool(); + data.TimeLimit = reader.GetFloat(); + data.TaskType = reader.GetByte(); + } +} + +public class ParallelTasksData : TaskBeforeDataData +{ + public TaskBeforeDataData[] Tasks { get; set; } + + public static ParallelTasksData FromParallelTask(ParallelTasks task) + { + return new ParallelTasksData + { + Tasks = task.tasks.Select(x => FromTask(x)).ToArray() + }; + } + + public static void Serialize(NetDataWriter writer, ParallelTasksData data) + { + TaskBeforeDataData.Serialize(writer, data); + writer.Put((byte)data.Tasks.Length); + foreach (var taskBeforeDataData in data.Tasks) + SerializeTask(taskBeforeDataData, writer); + } + + public static ParallelTasksData Deserialize(NetDataReader reader) + { + var parallelTask = new ParallelTasksData(); + Deserialize(reader, parallelTask); + var tasksLength = reader.GetByte(); + var tasks = new TaskBeforeDataData[tasksLength]; + for (int i = 0; i < tasksLength; i++) + tasks[i] = DeserializeTask(reader); + parallelTask.Tasks = tasks; + return parallelTask; + } +} + +public class SequentialTasksData : TaskBeforeDataData +{ + public TaskBeforeDataData[] Tasks { get; set; } + + + public static SequentialTasksData FromSequentialTask(SequentialTasks task) + { + return new SequentialTasksData + { + Tasks = task.tasks.Select(x => FromTask(x)).ToArray(), + }; + } + + public static void Serialize(NetDataWriter writer, SequentialTasksData data) + { + TaskBeforeDataData.Serialize(writer, data); + writer.Put((byte)data.Tasks.Length); + foreach (var taskBeforeDataData in data.Tasks) + SerializeTask(taskBeforeDataData, writer); + } + + public static SequentialTasksData Deserialize(NetDataReader reader) + { + var sequentialTask = new SequentialTasksData(); + Deserialize(reader, sequentialTask); + var tasksLength = reader.GetByte(); + var tasks = new TaskBeforeDataData[tasksLength]; + for (int i = 0; i < tasksLength; i++) + tasks[i] = DeserializeTask(reader); + sequentialTask.Tasks = tasks; + return sequentialTask; + } +} + +public class WarehouseTaskData : TaskBeforeDataData +{ + public string[] Cars { get; set; } + public byte WarehouseTaskType { get; set; } + public string WarehouseMachine { get; set; } + public CargoType CargoType { get; set; } + public float CargoAmount { get; set; } + public bool ReadyForMachine { get; set; } + + public static WarehouseTaskData FromWarehouseTask(WarehouseTask task) + { + return new WarehouseTaskData + { + Cars = task.cars.Select(x => x.ID).ToArray(), + WarehouseTaskType = (byte)task.warehouseTaskType, + WarehouseMachine = task.warehouseMachine.ID, + CargoType = task.cargoType, + CargoAmount = task.cargoAmount, + ReadyForMachine = task.readyForMachine + }; + } + + public static WarehouseTask ToWarehouseTask(WarehouseTaskData data) + { + return new WarehouseTask( + CarSpawner.Instance.allCars.FindAll(x => data.Cars.Contains(x.ID)).Select(x => x.logicCar).ToList(), + (WarehouseTaskType)data.WarehouseTaskType, + JobSaveManager.Instance.GetWarehouseMachineWithId(data.WarehouseMachine), + (CargoType)data.CargoType, + data.CargoAmount + ); + } + + public static void Serialize(NetDataWriter writer, WarehouseTaskData data) + { + TaskBeforeDataData.Serialize(writer, data); + writer.PutArray(data.Cars); + writer.Put(data.WarehouseTaskType); + writer.Put(data.WarehouseMachine); + writer.Put((int)data.CargoType); + writer.Put(data.CargoAmount); + writer.Put(data.ReadyForMachine); + } + + public static WarehouseTaskData Deserialize(NetDataReader reader) + { + WarehouseTaskData data = new WarehouseTaskData(); + Deserialize(reader, data); + data.Cars = reader.GetStringArray(); + data.WarehouseTaskType = reader.GetByte(); + data.WarehouseMachine = reader.GetString(); + data.CargoType = (CargoType)reader.GetInt(); + data.CargoAmount = reader.GetFloat(); + data.ReadyForMachine = reader.GetBool(); + + return data; + } +} + +public class TransportTaskData : TaskBeforeDataData +{ + public string[] Cars { get; set; } + public string StartingTrack { get; set; } + public string DestinationTrack { get; set; } + public CargoType[] TransportedCargoPerCar { get; set; } + public bool CouplingRequiredAndNotDone { get; set; } + public bool AnyHandbrakeRequiredAndNotDone { get; set; } + + public static TransportTaskData FromTransportTask(TransportTask task) + { + Multiplayer.Log("Cars: " + task.cars.Select(x => x.ID).ToArray().Join()); + Multiplayer.Log("FromTransportTask.TransportedCargoPerCar: " + task.transportedCargoPerCar?.Select(x => (int)x).ToArray().Join() + "\r\n\t"+ task.transportedCargoPerCar?.ToArray().Join()); + + return new TransportTaskData + { + Cars = task.cars.Select(x => x.ID).ToArray(), + StartingTrack = task.startingTrack.ID.RailTrackGameObjectID, + DestinationTrack = task.destinationTrack.ID.RailTrackGameObjectID, + TransportedCargoPerCar = task.transportedCargoPerCar?.ToArray(), + CouplingRequiredAndNotDone = task.couplingRequiredAndNotDone, + AnyHandbrakeRequiredAndNotDone = task.anyHandbrakeRequiredAndNotDone + }; + } + + public static TransportTask ToTransportTask(TransportTaskData data) + { + return new TransportTask( + CarSpawner.Instance.allCars.FindAll(x => data.Cars.Contains(x.ID)).Select(x => x.logicCar).ToList(), + RailTrackRegistry.Instance.GetTrackWithName(data.DestinationTrack).logicTrack, + RailTrackRegistry.Instance.GetTrackWithName(data.StartingTrack).logicTrack, + data.TransportedCargoPerCar?.ToList() + ); + } + + public static void Serialize(NetDataWriter writer, TransportTaskData data) + { + TaskBeforeDataData.Serialize(writer, data); + writer.PutArray(data.Cars); + writer.Put(data.StartingTrack); + writer.Put(data.DestinationTrack); + + //transport cargo data exists? + writer.Put(data.TransportedCargoPerCar != null); + + //write data if it exists + if (data.TransportedCargoPerCar != null) + { + writer.PutArray(data.TransportedCargoPerCar?.Select(x => (int)x).ToArray()); + // transportedCargoPerCar?.Select(x => (int)x).ToArray() + Multiplayer.Log("Serialising cargo: " + (int)data.TransportedCargoPerCar[0]); + } + + writer.Put(data.CouplingRequiredAndNotDone); + writer.Put(data.AnyHandbrakeRequiredAndNotDone); + } + + public static TransportTaskData Deserialize(NetDataReader reader) + { + Multiplayer.Log("TransportTaskData.Deserialize"); + TransportTaskData data = new TransportTaskData(); + Multiplayer.Log("1"); + Deserialize(reader, data); + Multiplayer.Log("2"); + data.Cars = reader.GetStringArray(); + Multiplayer.Log("3"); + data.StartingTrack = reader.GetString(); + Multiplayer.Log("4"); + data.DestinationTrack = reader.GetString(); + Multiplayer.Log("5"); + + if (reader.GetBool()) + { + //transport data exists + data.TransportedCargoPerCar = reader.GetArray(sizeof(int))?.Select(x => (CargoType)x).ToArray(); + } + + Multiplayer.Log("TransportedCargoPerCar: " + data.TransportedCargoPerCar?.Select(x => (int)x).ToArray().Join() + "\r\n\t" + data.TransportedCargoPerCar?.ToArray().Join()); + Multiplayer.Log("6"); + data.CouplingRequiredAndNotDone = reader.GetBool(); + Multiplayer.Log("7"); + data.AnyHandbrakeRequiredAndNotDone = reader.GetBool(); + //Multiplayer.Log(JsonConvert.SerializeObject(data, Formatting.Indented)); + + return data; + } +} diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 09d5c51..cfdb9b1 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -1,4 +1,7 @@ using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography; using System.Text; using DV; using DV.Damage; @@ -11,14 +14,18 @@ using DV.UIFramework; using DV.WeatherSystem; using LiteNetLib; +using Multiplayer.Components; using Multiplayer.Components.MainMenu; using Multiplayer.Components.Networking; +using Multiplayer.Components.Networking.Jobs; +using Multiplayer.Components.Networking.Player; using Multiplayer.Components.Networking.Train; using Multiplayer.Components.Networking.UI; using Multiplayer.Components.Networking.World; using Multiplayer.Components.SaveGame; using Multiplayer.Networking.Data; using Multiplayer.Networking.Packets.Clientbound; +using Multiplayer.Networking.Packets.Clientbound.Jobs; using Multiplayer.Networking.Packets.Clientbound.SaveGame; using Multiplayer.Networking.Packets.Clientbound.Train; using Multiplayer.Networking.Packets.Clientbound.World; @@ -56,7 +63,8 @@ public NetworkClient(Settings settings) : base(settings) public void Start(string address, int port, string password, bool isSinglePlayer) { netManager.Start(); - ServerboundClientLoginPacket serverboundClientLoginPacket = new() { + ServerboundClientLoginPacket serverboundClientLoginPacket = new() + { Username = Multiplayer.Settings.Username, Guid = Multiplayer.Settings.GetGuid().ToByteArray(), Password = password, @@ -110,6 +118,9 @@ protected override void Subscribe() netPacketProcessor.SubscribeReusable(OnClientboundLicenseAcquiredPacket); netPacketProcessor.SubscribeReusable(OnClientboundGarageUnlockPacket); netPacketProcessor.SubscribeReusable(OnClientboundDebtStatusPacket); + netPacketProcessor.SubscribeReusable(OnClientboundJobsPacket); + netPacketProcessor.SubscribeReusable(OnClientboundJobCreatePacket); + netPacketProcessor.SubscribeReusable(OnClientboundJobTakeResponsePacket); netPacketProcessor.SubscribeReusable(OnCommonChatPacket); } @@ -613,6 +624,115 @@ private void OnCommonChatPacket(CommonChatPacket packet) chatGUI.ReceiveMessage(packet.message); } + private void OnClientboundJobCreatePacket(ClientboundJobCreatePacket packet) + { + if (NetworkLifecycle.Instance.IsHost()) + return; + + List tasks = new List(); + foreach (TaskBeforeDataData taskBeforeDataData in packet.job.Tasks) + tasks.Add(TaskBeforeDataData.ToTask(taskBeforeDataData)); + + StationsChainDataData chainData = packet.job.ChainData; + //packet.job.JobType + Job newJob = new Job( + tasks, + (JobType)packet.job.JobType, + packet.job.TimeLimit, + packet.job.InitialWage, + new StationsChainData(chainData.ChainOriginYardId, chainData.ChainDestinationYardId), + packet.job.ID, + (JobLicenses)packet.job.RequiredLicenses + ); + + //NetworkedJob netJob = NetworkedJob.AddJob(packet.stationId, newJob); + //netJob.NetId = packet.netId; + + //Find the station + StationController station; + if(!StationComponentLookup.Instance.StationControllerFromId(packet.stationId, out station)) + { + Multiplayer.LogWarning($"OnClientboundJobCreatePacket Could not get staion for stationId: {packet.stationId}"); + return; + } + + //create a new game object + NetworkedJob netJob = station.gameObject.AddComponent(); + if (netJob != null) + { + netJob.job = newJob; + netJob.stationID = packet.stationId; + netJob.NetId = packet.netId; + } + + } + private void OnClientboundJobsPacket(ClientboundJobsPacket packet) + { + if (NetworkLifecycle.Instance.IsHost()) + return; + + if (!StationComponentLookup.Instance.StationControllerFromId(packet.stationId, out StationController station)) + { + LogError("Received job packet but couldn't find station!"); + return; + } + + Multiplayer.Log($"Received job packet. Job count:{packet.Jobs.Count()}"); + + for (int i=0;i < packet.Jobs.Count(); i++) + { + JobData job = packet.Jobs[i]; + ushort netId = packet.netIds[i]; + + var tasks = new List(); + foreach (TaskBeforeDataData taskBeforeDataData in job.Tasks) + tasks.Add(TaskBeforeDataData.ToTask(taskBeforeDataData)); + + StationsChainDataData chainData = job.ChainData; + + Job newJob = new Job( + tasks, + (JobType)job.JobType, + job.TimeLimit, + job.InitialWage, + new StationsChainData(chainData.ChainOriginYardId, chainData.ChainDestinationYardId), + job.ID, + (JobLicenses)job.RequiredLicenses + ); + + Multiplayer.Log($"Attempting to add Job with ID {newJob.ID} to station.");//\r\nExisting jobs are: {station.logicStation.availableJobs.Select(x=>x.ID + "\r\n\t").ToArray().Join()}\r\nDoes the Job already exist in station? {station.logicStation.availableJobs.Where(x => x.ID == newJob.ID).Count() > 0}"); + + //create a new game object + NetworkedJob netJob = station.gameObject.AddComponent(); + if (netJob != null) + { + netJob.job = newJob; + netJob.stationID = packet.stationId; + netJob.NetId = netId; + } + } + } + + private void OnClientboundJobTakeResponsePacket(ClientboundJobTakeResponsePacket packet) + { + NetworkedJob networkedJob; + + if(!NetworkedJob.Get(packet.netId, out networkedJob)) + return; + + NetworkedPlayer player; + if (PlayerManager.TryGetPlayer(packet.playerId, out player)) + { + networkedJob.takenBy = player.Guid; + } + + Multiplayer.Log($"OnClientboundJobTakeResponsePacket jobId: {networkedJob.job.ID}, Status: {packet.granted}"); + networkedJob.allowTake = packet.granted; + networkedJob.jobValidator.ProcessJobOverview(networkedJob.jobOverview); + networkedJob.jobValidator = null; + networkedJob.jobOverview = null; + } + #endregion #region Senders @@ -635,7 +755,8 @@ private void SendReadyPacket() public void SendPlayerPosition(Vector3 position, Vector3 moveDir, float rotationY, bool isJumping, bool isOnCar, bool reliable) { - SendPacketToServer(new ServerboundPlayerPositionPacket { + SendPacketToServer(new ServerboundPlayerPositionPacket + { Position = position, MoveDir = new Vector2(moveDir.x, moveDir.z), RotationY = rotationY, @@ -645,21 +766,24 @@ public void SendPlayerPosition(Vector3 position, Vector3 moveDir, float rotation public void SendPlayerCar(ushort carId) { - SendPacketToServer(new ServerboundPlayerCarPacket { + SendPacketToServer(new ServerboundPlayerCarPacket + { CarId = carId }, DeliveryMethod.ReliableOrdered); } public void SendTimeAdvance(float amountOfTimeToSkipInSeconds) { - SendPacketToServer(new ServerboundTimeAdvancePacket { + SendPacketToServer(new ServerboundTimeAdvancePacket + { amountOfTimeToSkipInSeconds = amountOfTimeToSkipInSeconds }, DeliveryMethod.ReliableUnordered); } public void SendJunctionSwitched(ushort netId, byte selectedBranch, Junction.SwitchMode mode) { - SendPacketToServer(new CommonChangeJunctionPacket { + SendPacketToServer(new CommonChangeJunctionPacket + { NetId = netId, SelectedBranch = selectedBranch, Mode = (byte)mode @@ -668,7 +792,8 @@ public void SendJunctionSwitched(ushort netId, byte selectedBranch, Junction.Swi public void SendTurntableRotation(byte netId, float rotation) { - SendPacketToServer(new CommonRotateTurntablePacket { + SendPacketToServer(new CommonRotateTurntablePacket + { NetId = netId, rotation = rotation }, DeliveryMethod.ReliableOrdered); @@ -676,7 +801,8 @@ public void SendTurntableRotation(byte netId, float rotation) public void SendTrainCouple(Coupler coupler, Coupler otherCoupler, bool playAudio, bool viaChainInteraction) { - SendPacketToServer(new CommonTrainCouplePacket { + SendPacketToServer(new CommonTrainCouplePacket + { NetId = coupler.train.GetNetId(), IsFrontCoupler = coupler.isFrontCoupler, OtherNetId = otherCoupler.train.GetNetId(), @@ -688,7 +814,8 @@ public void SendTrainCouple(Coupler coupler, Coupler otherCoupler, bool playAudi public void SendTrainUncouple(Coupler coupler, bool playAudio, bool dueToBrokenCouple, bool viaChainInteraction) { - SendPacketToServer(new CommonTrainUncouplePacket { + SendPacketToServer(new CommonTrainUncouplePacket + { NetId = coupler.train.GetNetId(), IsFrontCoupler = coupler.isFrontCoupler, PlayAudio = playAudio, @@ -699,7 +826,8 @@ public void SendTrainUncouple(Coupler coupler, bool playAudio, bool dueToBrokenC public void SendHoseConnected(Coupler coupler, Coupler otherCoupler, bool playAudio) { - SendPacketToServer(new CommonHoseConnectedPacket { + SendPacketToServer(new CommonHoseConnectedPacket + { NetId = coupler.train.GetNetId(), IsFront = coupler.isFrontCoupler, OtherNetId = otherCoupler.train.GetNetId(), @@ -710,7 +838,8 @@ public void SendHoseConnected(Coupler coupler, Coupler otherCoupler, bool playAu public void SendHoseDisconnected(Coupler coupler, bool playAudio) { - SendPacketToServer(new CommonHoseDisconnectedPacket { + SendPacketToServer(new CommonHoseDisconnectedPacket + { NetId = coupler.train.GetNetId(), IsFront = coupler.isFrontCoupler, PlayAudio = playAudio @@ -719,7 +848,8 @@ public void SendHoseDisconnected(Coupler coupler, bool playAudio) public void SendMuConnected(MultipleUnitCable cable, MultipleUnitCable otherCable, bool playAudio) { - SendPacketToServer(new CommonMuConnectedPacket { + SendPacketToServer(new CommonMuConnectedPacket + { NetId = cable.muModule.train.GetNetId(), IsFront = cable.isFront, OtherNetId = otherCable.muModule.train.GetNetId(), @@ -730,7 +860,8 @@ public void SendMuConnected(MultipleUnitCable cable, MultipleUnitCable otherCabl public void SendMuDisconnected(ushort netId, MultipleUnitCable cable, bool playAudio) { - SendPacketToServer(new CommonMuDisconnectedPacket { + SendPacketToServer(new CommonMuDisconnectedPacket + { NetId = netId, IsFront = cable.isFront, PlayAudio = playAudio @@ -739,7 +870,8 @@ public void SendMuDisconnected(ushort netId, MultipleUnitCable cable, bool playA public void SendCockState(ushort netId, Coupler coupler, bool isOpen) { - SendPacketToServer(new CommonCockFiddlePacket { + SendPacketToServer(new CommonCockFiddlePacket + { NetId = netId, IsFront = coupler.isFrontCoupler, IsOpen = isOpen @@ -748,14 +880,16 @@ public void SendCockState(ushort netId, Coupler coupler, bool isOpen) public void SendBrakeCylinderReleased(ushort netId) { - SendPacketToServer(new CommonBrakeCylinderReleasePacket { + SendPacketToServer(new CommonBrakeCylinderReleasePacket + { NetId = netId }, DeliveryMethod.ReliableUnordered); } public void SendHandbrakePositionChanged(ushort netId, float position) { - SendPacketToServer(new CommonHandbrakePositionPacket { + SendPacketToServer(new CommonHandbrakePositionPacket + { NetId = netId, Position = position }, DeliveryMethod.ReliableOrdered); @@ -763,7 +897,8 @@ public void SendHandbrakePositionChanged(ushort netId, float position) public void SendPorts(ushort netId, string[] portIds, float[] portValues) { - SendPacketToServer(new CommonTrainPortsPacket { + SendPacketToServer(new CommonTrainPortsPacket + { NetId = netId, PortIds = portIds, PortValues = portValues @@ -772,7 +907,8 @@ public void SendPorts(ushort netId, string[] portIds, float[] portValues) public void SendFuses(ushort netId, string[] fuseIds, bool[] fuseValues) { - SendPacketToServer(new CommonTrainFusesPacket { + SendPacketToServer(new CommonTrainFusesPacket + { NetId = netId, FuseIds = fuseIds, FuseValues = fuseValues @@ -781,21 +917,24 @@ public void SendFuses(ushort netId, string[] fuseIds, bool[] fuseValues) public void SendTrainSyncRequest(ushort netId) { - SendPacketToServer(new ServerboundTrainSyncRequestPacket { + SendPacketToServer(new ServerboundTrainSyncRequestPacket + { NetId = netId }, DeliveryMethod.ReliableUnordered); } public void SendTrainDeleteRequest(ushort netId) { - SendPacketToServer(new ServerboundTrainDeleteRequestPacket { + SendPacketToServer(new ServerboundTrainDeleteRequestPacket + { NetId = netId }, DeliveryMethod.ReliableUnordered); } public void SendTrainRerailRequest(ushort netId, ushort trackId, Vector3 position, Vector3 forward) { - SendPacketToServer(new ServerboundTrainRerailRequestPacket { + SendPacketToServer(new ServerboundTrainRerailRequestPacket + { NetId = netId, TrackId = trackId, Position = position, @@ -805,11 +944,20 @@ public void SendTrainRerailRequest(ushort netId, ushort trackId, Vector3 positio public void SendLicensePurchaseRequest(string id, bool isJobLicense) { - SendPacketToServer(new ServerboundLicensePurchaseRequestPacket { + SendPacketToServer(new ServerboundLicensePurchaseRequestPacket + { Id = id, IsJobLicense = isJobLicense }, DeliveryMethod.ReliableUnordered); } + public void SendJobTakeRequest(ushort netId) + { + SendPacketToServer(new ServerboundJobTakeRequestPacket + { + netId = netId + }, DeliveryMethod.ReliableUnordered); + } + public void SendChat(string message) { diff --git a/Multiplayer/Networking/Managers/NetworkManager.cs b/Multiplayer/Networking/Managers/NetworkManager.cs index 7d4d4dc..0a88130 100644 --- a/Multiplayer/Networking/Managers/NetworkManager.cs +++ b/Multiplayer/Networking/Managers/NetworkManager.cs @@ -22,7 +22,8 @@ public abstract class NetworkManager : INetEventListener protected NetworkManager(Settings settings) { - netManager = new NetManager(this) { + netManager = new NetManager(this) + { DisconnectTimeout = 10000 }; netPacketProcessor = new NetPacketProcessor(netManager); @@ -36,8 +37,10 @@ protected NetworkManager(Settings settings) private void RegisterNestedTypes() { netPacketProcessor.RegisterNestedType(BogieData.Serialize, BogieData.Deserialize); + netPacketProcessor.RegisterNestedType(JobData.Serialize, JobData.Deserialize); netPacketProcessor.RegisterNestedType(ModInfo.Serialize, ModInfo.Deserialize); netPacketProcessor.RegisterNestedType(RigidbodySnapshot.Serialize, RigidbodySnapshot.Deserialize); + netPacketProcessor.RegisterNestedType(StationsChainDataData.Serialize, StationsChainDataData.Deserialize); netPacketProcessor.RegisterNestedType(TrainsetMovementPart.Serialize, TrainsetMovementPart.Deserialize); netPacketProcessor.RegisterNestedType(TrainsetSpawnPart.Serialize, TrainsetSpawnPart.Deserialize); netPacketProcessor.RegisterNestedType(Vector2Serializer.Serialize, Vector2Serializer.Deserialize); diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index 87ab470..301ea27 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -15,9 +15,11 @@ using Multiplayer.Components.Networking; using Multiplayer.Components.Networking.Train; using Multiplayer.Components.Networking.World; +using Multiplayer.Components.Networking.Jobs; using Multiplayer.Networking.Data; using Multiplayer.Networking.Managers.Server; using Multiplayer.Networking.Packets.Clientbound; +using Multiplayer.Networking.Packets.Clientbound.Jobs; using Multiplayer.Networking.Packets.Clientbound.SaveGame; using Multiplayer.Networking.Packets.Clientbound.Train; using Multiplayer.Networking.Packets.Clientbound.World; @@ -27,6 +29,7 @@ using Multiplayer.Utils; using UnityEngine; using UnityModManagerNet; +using Unity.Jobs; namespace Multiplayer.Networking.Listeners; @@ -105,12 +108,14 @@ protected override void Subscribe() netPacketProcessor.SubscribeReusable(OnCommonHandbrakePositionPacket); netPacketProcessor.SubscribeReusable(OnCommonTrainPortsPacket); netPacketProcessor.SubscribeReusable(OnCommonTrainFusesPacket); + + netPacketProcessor.SubscribeReusable(OnServerboundJobTakeRequestPacket); netPacketProcessor.SubscribeReusable(OnCommonChatPacket); } private void OnLoaded() { - Debug.Log($"Server loaded, isSinglePlayer: {isSinglePlayer} isPublic: {isPublic}"); + //Debug.Log($"Server loaded, isSinglePlayer: {isSinglePlayer} isPublic: {isPublic}"); if (!isSinglePlayer && isPublic) { lobbyServerManager = NetworkLifecycle.Instance.GetOrAddComponent(); @@ -139,7 +144,8 @@ public bool TryGetNetPeer(byte id, out NetPeer peer) #region Net Events public override void OnPeerConnected(NetPeer peer) - { } + { + } public override void OnPeerDisconnected(NetPeer peer, DisconnectInfo disconnectInfo) { @@ -151,21 +157,24 @@ public override void OnPeerDisconnected(NetPeer peer, DisconnectInfo disconnectI serverPlayers.Remove(id); netPeers.Remove(id); - netManager.SendToAll(WritePacket(new ClientboundPlayerDisconnectPacket { + netManager.SendToAll(WritePacket(new ClientboundPlayerDisconnectPacket + { Id = id }), DeliveryMethod.ReliableUnordered); } public override void OnNetworkLatencyUpdate(NetPeer peer, int latency) { - ClientboundPingUpdatePacket clientboundPingUpdatePacket = new() { + ClientboundPingUpdatePacket clientboundPingUpdatePacket = new() + { Id = (byte)peer.Id, Ping = latency }; SendPacketToAll(clientboundPingUpdatePacket, DeliveryMethod.ReliableUnordered, peer); - SendPacket(peer, new ClientboundTickSyncPacket { + SendPacket(peer, new ClientboundTickSyncPacket + { ServerTick = NetworkLifecycle.Instance.Tick }, DeliveryMethod.ReliableUnordered); } @@ -209,7 +218,8 @@ public void SendSpawnTrainCar(NetworkedTrainCar networkedTrainCar) public void SendDestroyTrainCar(TrainCar trainCar) { - SendPacketToAll(new ClientboundDestroyTrainCarPacket { + SendPacketToAll(new ClientboundDestroyTrainCarPacket + { NetId = trainCar.GetNetId() }, DeliveryMethod.ReliableOrdered, selfPeer); } @@ -223,7 +233,8 @@ public void SendCargoState(TrainCar trainCar, ushort netId, bool isLoading, byte { Car logicCar = trainCar.logicCar; CargoType cargoType = isLoading ? logicCar.CurrentCargoTypeInCar : logicCar.LastUnloadedCargoType; - SendPacketToAll(new ClientboundCargoStatePacket { + SendPacketToAll(new ClientboundCargoStatePacket + { NetId = netId, IsLoading = isLoading, CargoType = (ushort)cargoType, @@ -235,7 +246,8 @@ public void SendCargoState(TrainCar trainCar, ushort netId, bool isLoading, byte public void SendCarHealthUpdate(ushort netId, float health) { - SendPacketToAll(new ClientboundCarHealthUpdatePacket { + SendPacketToAll(new ClientboundCarHealthUpdatePacket + { NetId = netId, Health = health }, DeliveryMethod.ReliableOrdered, selfPeer); @@ -243,7 +255,8 @@ public void SendCarHealthUpdate(ushort netId, float health) public void SendRerailTrainCar(ushort netId, ushort rerailTrack, Vector3 worldPos, Vector3 forward) { - SendPacketToAll(new ClientboundRerailTrainPacket { + SendPacketToAll(new ClientboundRerailTrainPacket + { NetId = netId, TrackId = rerailTrack, Position = worldPos, @@ -253,7 +266,8 @@ public void SendRerailTrainCar(ushort netId, ushort rerailTrack, Vector3 worldPo public void SendWindowsBroken(ushort netId, Vector3 forceDirection) { - SendPacketToAll(new ClientboundWindowsBrokenPacket { + SendPacketToAll(new ClientboundWindowsBrokenPacket + { NetId = netId, ForceDirection = forceDirection }, DeliveryMethod.ReliableUnordered, selfPeer); @@ -261,21 +275,24 @@ public void SendWindowsBroken(ushort netId, Vector3 forceDirection) public void SendWindowsRepaired(ushort netId) { - SendPacketToAll(new ClientboundWindowsBrokenPacket { + SendPacketToAll(new ClientboundWindowsBrokenPacket + { NetId = netId }, DeliveryMethod.ReliableUnordered, selfPeer); } public void SendMoney(float amount) { - SendPacketToAll(new ClientboundMoneyPacket { + SendPacketToAll(new ClientboundMoneyPacket + { Amount = amount }, DeliveryMethod.ReliableUnordered, selfPeer); } public void SendLicense(string id, bool isJobLicense) { - SendPacketToAll(new ClientboundLicenseAcquiredPacket { + SendPacketToAll(new ClientboundLicenseAcquiredPacket + { Id = id, IsJobLicense = isJobLicense }, DeliveryMethod.ReliableUnordered, selfPeer); @@ -283,18 +300,26 @@ public void SendLicense(string id, bool isJobLicense) public void SendGarage(string id) { - SendPacketToAll(new ClientboundGarageUnlockPacket { + SendPacketToAll(new ClientboundGarageUnlockPacket + { Id = id }, DeliveryMethod.ReliableUnordered, selfPeer); } public void SendDebtStatus(bool hasDebt) { - SendPacketToAll(new ClientboundDebtStatusPacket { + SendPacketToAll(new ClientboundDebtStatusPacket + { HasDebt = hasDebt }, DeliveryMethod.ReliableUnordered, selfPeer); } + public void SendJobCreatePacket(NetworkedJob job) + { + Multiplayer.Log("Sending JobCreatePacket with netId: " + job.NetId + ", Job ID: " + job.job.ID); + SendPacketToAll(ClientboundJobCreatePacket.FromNetworkedJob(job),DeliveryMethod.ReliableSequenced); + } + public void SendChat(string message, NetPeer exclude = null) { @@ -362,7 +387,8 @@ private void OnServerboundClientLoginPacket(ServerboundClientLoginPacket packet, if (Multiplayer.Settings.Password != packet.Password) { LogWarning("Denied login due to invalid password!"); - ClientboundServerDenyPacket denyPacket = new() { + ClientboundServerDenyPacket denyPacket = new() + { ReasonKey = Locale.DISCONN_REASON__INVALID_PASSWORD_KEY }; request.Reject(WritePacket(denyPacket)); @@ -372,7 +398,8 @@ private void OnServerboundClientLoginPacket(ServerboundClientLoginPacket packet, if (packet.BuildMajorVersion != BuildInfo.BUILD_VERSION_MAJOR) { LogWarning($"Denied login to incorrect game version! Got: {packet.BuildMajorVersion}, expected: {BuildInfo.BUILD_VERSION_MAJOR}"); - ClientboundServerDenyPacket denyPacket = new() { + ClientboundServerDenyPacket denyPacket = new() + { ReasonKey = Locale.DISCONN_REASON__GAME_VERSION_KEY, ReasonArgs = new[] { BuildInfo.BUILD_VERSION_MAJOR.ToString(), packet.BuildMajorVersion.ToString() } }; @@ -383,7 +410,8 @@ private void OnServerboundClientLoginPacket(ServerboundClientLoginPacket packet, if (netManager.ConnectedPeersCount >= Multiplayer.Settings.MaxPlayers || isSinglePlayer && netManager.ConnectedPeersCount >= 1) { LogWarning("Denied login due to server being full!"); - ClientboundServerDenyPacket denyPacket = new() { + ClientboundServerDenyPacket denyPacket = new() + { ReasonKey = Locale.DISCONN_REASON__FULL_SERVER_KEY }; request.Reject(WritePacket(denyPacket)); @@ -396,7 +424,8 @@ private void OnServerboundClientLoginPacket(ServerboundClientLoginPacket packet, ModInfo[] missing = serverMods.Except(clientMods).ToArray(); ModInfo[] extra = clientMods.Except(serverMods).ToArray(); LogWarning($"Denied login due to mod mismatch! {missing.Length} missing, {extra.Length} extra"); - ClientboundServerDenyPacket denyPacket = new() { + ClientboundServerDenyPacket denyPacket = new() + { ReasonKey = Locale.DISCONN_REASON__MODS_KEY, Missing = missing, Extra = extra @@ -407,7 +436,8 @@ private void OnServerboundClientLoginPacket(ServerboundClientLoginPacket packet, NetPeer peer = request.Accept(); - ServerPlayer serverPlayer = new() { + ServerPlayer serverPlayer = new() + { Id = (byte)peer.Id, Username = overrideUsername, OriginalUsername = packet.Username, @@ -452,7 +482,8 @@ private void OnServerboundClientReadyPacket(ServerboundClientReadyPacket packet, // Send the new player to all other players ServerPlayer serverPlayer = serverPlayers[peerId]; - ClientboundPlayerJoinedPacket clientboundPlayerJoinedPacket = new() { + ClientboundPlayerJoinedPacket clientboundPlayerJoinedPacket = new() + { Id = peerId, Username = serverPlayer.Username, Guid = serverPlayer.Guid.ToByteArray() @@ -476,7 +507,8 @@ private void OnServerboundClientReadyPacket(ServerboundClientReadyPacket packet, SendPacket(peer, WeatherDriver.Instance.GetSaveData().ToObject(), DeliveryMethod.ReliableOrdered); // Send junctions and turntables - SendPacket(peer, new ClientboundRailwayStatePacket { + SendPacket(peer, new ClientboundRailwayStatePacket + { SelectedJunctionBranches = NetworkedJunction.IndexedJunctions.Select(j => (byte)j.Junction.selectedBranch).ToArray(), TurntableRotations = NetworkedTurntable.IndexedTurntables.Select(j => j.TurntableRailTrack.currentYRotation).ToArray() }, DeliveryMethod.ReliableOrdered); @@ -488,12 +520,38 @@ private void OnServerboundClientReadyPacket(ServerboundClientReadyPacket packet, SendPacket(peer, ClientboundSpawnTrainSetPacket.FromTrainSet(set), DeliveryMethod.ReliableOrdered); } + //send jobs - do we need a job manager/job IDs to make this easier? + foreach(StationController station in StationController.allStations) + { + List jobData = new List(); + List netIds = new List(); + + foreach(Job job in station.logicStation.availableJobs) + { + jobData.Add(JobData.FromJob(job)); + netIds.Add(NetworkedJob.GetFromJob(job).NetId); + } + + SendPacket(peer, + new ClientboundJobsPacket + { + stationId = station.logicStation.ID, + netIds = netIds.ToArray(), + Jobs = jobData.ToArray(), + }, + DeliveryMethod.ReliableOrdered + ); + + } + + // Send existing players foreach (ServerPlayer player in ServerPlayers) { if (player.Id == peer.Id) continue; - SendPacket(peer, new ClientboundPlayerJoinedPacket { + SendPacket(peer, new ClientboundPlayerJoinedPacket + { Id = player.Id, Username = player.Username, Guid = player.Guid.ToByteArray(), @@ -517,7 +575,8 @@ private void OnServerboundPlayerPositionPacket(ServerboundPlayerPositionPacket p player.RawRotationY = packet.RotationY; } - ClientboundPlayerPositionPacket clientboundPacket = new() { + ClientboundPlayerPositionPacket clientboundPacket = new() + { Id = (byte)peer.Id, Position = packet.Position, MoveDir = packet.MoveDir, @@ -536,7 +595,8 @@ private void OnServerboundPlayerCarPacket(ServerboundPlayerCarPacket packet, Net if (TryGetServerPlayer(peer, out ServerPlayer player)) player.CarId = packet.CarId; - ClientboundPlayerCarPacket clientboundPacket = new() { + ClientboundPlayerCarPacket clientboundPacket = new() + { Id = (byte)peer.Id, CarId = packet.CarId }; @@ -546,7 +606,8 @@ private void OnServerboundPlayerCarPacket(ServerboundPlayerCarPacket packet, Net private void OnServerboundTimeAdvancePacket(ServerboundTimeAdvancePacket packet, NetPeer peer) { - SendPacketToAll(new ClientboundTimeAdvancePacket { + SendPacketToAll(new ClientboundTimeAdvancePacket + { amountOfTimeToSkipInSeconds = packet.amountOfTimeToSkipInSeconds }, DeliveryMethod.ReliableUnordered, peer); } @@ -721,6 +782,40 @@ private void OnServerboundLicensePurchaseRequestPacket(ServerboundLicensePurchas LicenseManager.Instance.AcquireGeneralLicense(generalLicense); } + private void OnServerboundJobTakeRequestPacket(ServerboundJobTakeRequestPacket packet, NetPeer peer) + { + NetworkedJob networkedJob; + + if (!NetworkedJob.Get(packet.netId, out networkedJob)) + { + Multiplayer.Log($"OnServerboundJobTakeRequestPacket netId Not Found: {packet.netId}"); + return; + } + + if (networkedJob.job.State != JobState.Available) { + + Multiplayer.Log($"OnServerboundJobTakeRequestPacket jobId: {networkedJob.job.ID}, DENIED"); + ServerPlayer player = ServerPlayers.First(x => x.Guid == networkedJob.takenBy); + //deny the request + SendPacket(peer, new ClientboundJobTakeResponsePacket { netId = packet.netId, granted = false, playerId = player.Id }, DeliveryMethod.ReliableOrdered); + } + else + { + //probably need to do more here + ServerPlayer player; + if (!TryGetServerPlayer(peer, out player)) + return; + + networkedJob.takenBy = player.Guid; + //networkedJob.job.State = JobState.InProgress; + + //todo: officially take the job + Multiplayer.Log($"OnServerboundJobTakeRequestPacket jobId: {networkedJob.job.ID}, GRANTED"); + SendPacket(peer, new ClientboundJobTakeResponsePacket { netId = packet.netId, granted = true, playerId = player.Id }, DeliveryMethod.ReliableOrdered); + + } + } + private void OnCommonChatPacket(CommonChatPacket packet, NetPeer peer) { ChatManager.ProcessMessage(packet.message,peer); diff --git a/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobCreatePacket.cs b/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobCreatePacket.cs new file mode 100644 index 0000000..4caa869 --- /dev/null +++ b/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobCreatePacket.cs @@ -0,0 +1,22 @@ +using Multiplayer.Components.Networking.Jobs; +using Multiplayer.Networking.Data; +using Multiplayer.Networking.Packets.Clientbound.Train; + +namespace Multiplayer.Networking.Packets.Clientbound.Jobs; + +public class ClientboundJobCreatePacket +{ + public ushort netId { get; set; } + public string stationId { get; set; } + public JobData job { get; set; } + + public static ClientboundJobCreatePacket FromNetworkedJob(NetworkedJob job) + { + return new ClientboundJobCreatePacket + { + netId = job.NetId, + stationId = job.stationID, + job = JobData.FromJob(job.job), + }; + } +} diff --git a/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobPacket.cs b/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobPacket.cs new file mode 100644 index 0000000..fc89a41 --- /dev/null +++ b/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobPacket.cs @@ -0,0 +1,11 @@ +using Multiplayer.Networking.Data; + +namespace Multiplayer.Networking.Packets.Clientbound.Jobs; + +public class ClientboundJobsPacket +{ + public string stationId { get; set; } + public ushort[] netIds { get; set; } + public JobData[] Jobs { get; set; } + +} diff --git a/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobTakeResponsePacket.cs b/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobTakeResponsePacket.cs new file mode 100644 index 0000000..53b0bd9 --- /dev/null +++ b/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobTakeResponsePacket.cs @@ -0,0 +1,12 @@ +using Multiplayer.Components.Networking.Jobs; +using Multiplayer.Networking.Data; +using Multiplayer.Networking.Packets.Clientbound.Train; + +namespace Multiplayer.Networking.Packets.Clientbound.Jobs; + +public class ClientboundJobTakeResponsePacket +{ + public ushort netId { get; set; } + public bool granted { get; set; } + public byte playerId { get; set; } +} diff --git a/Multiplayer/Networking/Packets/Serverbound/Jobs/ServerboundJobTakeRequestPacket.cs b/Multiplayer/Networking/Packets/Serverbound/Jobs/ServerboundJobTakeRequestPacket.cs new file mode 100644 index 0000000..895d5fe --- /dev/null +++ b/Multiplayer/Networking/Packets/Serverbound/Jobs/ServerboundJobTakeRequestPacket.cs @@ -0,0 +1,10 @@ +using Multiplayer.Components.Networking.Jobs; +using Multiplayer.Networking.Data; +using Multiplayer.Networking.Packets.Clientbound.Train; + +namespace Multiplayer.Networking.Packets.Clientbound.Jobs; + +public class ServerboundJobTakeRequestPacket +{ + public ushort netId { get; set; } +} diff --git a/Multiplayer/Patches/Jobs/JobOverviewUsePatch.cs b/Multiplayer/Patches/Jobs/JobOverviewUsePatch.cs new file mode 100644 index 0000000..2ee4ab5 --- /dev/null +++ b/Multiplayer/Patches/Jobs/JobOverviewUsePatch.cs @@ -0,0 +1,66 @@ +using DV; +using DV.Interaction; +using DV.Logic.Job; +using DV.ThingTypes; +using DV.Utils; +using HarmonyLib; +using Multiplayer.Components; +using Multiplayer.Components.Networking; +using Multiplayer.Components.Networking.Jobs; +using Multiplayer.Utils; +using System.Collections; +using Unity.Jobs; +using UnityEngine; +using static UnityEngine.GraphicsBuffer; + +namespace Multiplayer.Patches.Jobs; +//public void HandleUse(ItemUseTarget target) +[HarmonyPatch(typeof(JobOverviewUse), nameof(JobOverviewUse.HandleUse))] +public static class JobOverviewUse_HandleUse_Patch +{ + private static bool Prefix(JobOverviewUse __instance, ItemUseTarget target, ref JobOverview ___jobOverview) + { + JobValidator component = target.GetComponent(); + if (component == null) + return false; + + if (component.bookletPrinter.IsOnCooldown) + { + component.bookletPrinter.PlayErrorSound(); + return false; + } + + Job job = ___jobOverview.job; + + Multiplayer.Log($"JobOverviewUse_HandleUse_Patch jobId: {job.ID}"); + + NetworkedJob networkedJob; + + if (!NetworkedJob.TryGetFromJob(job, out networkedJob)) + { + Multiplayer.Log($"JobOverviewUse_HandleUse_Patch No netId found for jobId: {job.ID}"); + component.bookletPrinter.PlayErrorSound(); + return false; + } + + if(networkedJob.allowTake == true) { + Multiplayer.Log($"JobOverviewUse_HandleUse_Patch jobId: {job.ID}, Take allowed: {networkedJob.allowTake}"); + return true; + } + else if (networkedJob.allowTake == null || (networkedJob.allowTake == false && networkedJob.takenBy == null)) + { + Multiplayer.Log($"JobOverviewUse_HandleUse_Patch WaitForResponse returned for jobId: {job.ID}"); + networkedJob.jobValidator = component; + networkedJob.jobOverview = ___jobOverview; + NetworkLifecycle.Instance.Client.SendJobTakeRequest(networkedJob.NetId); + + return false; + + } + + component.bookletPrinter.PlayErrorSound(); + return false; + + } +} + diff --git a/Multiplayer/Patches/Jobs/StationControllerPatch.cs b/Multiplayer/Patches/Jobs/StationControllerPatch.cs new file mode 100644 index 0000000..f2fc105 --- /dev/null +++ b/Multiplayer/Patches/Jobs/StationControllerPatch.cs @@ -0,0 +1,13 @@ +using HarmonyLib; +using Multiplayer.Components.Networking.World; + +namespace Multiplayer.Patches.Jobs; + +[HarmonyPatch(typeof(StationController), nameof(StationController.Awake))] +public static class StationController_Awake_Patch +{ + public static void Postfix(StationController __instance) + { + __instance.gameObject.AddComponent(); + } +} diff --git a/Multiplayer/Patches/Jobs/StationJobGenerationRangePatch.cs b/Multiplayer/Patches/Jobs/StationJobGenerationRangePatch.cs new file mode 100644 index 0000000..ae82b0b --- /dev/null +++ b/Multiplayer/Patches/Jobs/StationJobGenerationRangePatch.cs @@ -0,0 +1,53 @@ +using HarmonyLib; +using Multiplayer.Components.Networking; +using Multiplayer.Networking.Data; +using UnityEngine; + +namespace Multiplayer.Patches.Jobs; + +[HarmonyPatch(typeof(StationJobGenerationRange), nameof(StationJobGenerationRange.PlayerSqrDistanceFromStationCenter), MethodType.Getter)] +public static class StationJobGenerationRange_PlayerSqrDistanceFromStationCenter_Patch +{ + private static bool Prefix(StationJobGenerationRange __instance, ref float __result) + { + if (!NetworkLifecycle.Instance.IsHost()) + return true; + + Vector3 anchor = __instance.stationCenterAnchor.position; + + __result = float.MaxValue; + + //Loop through all of the players and return the one thats closest to the anchor + foreach (ServerPlayer serverPlayer in NetworkLifecycle.Instance.Server.ServerPlayers) + { + float sqDist = (serverPlayer.WorldPosition - anchor).sqrMagnitude; + if (sqDist < __result) + __result = sqDist; + } + + return false; + } +} + +[HarmonyPatch(typeof(StationJobGenerationRange), nameof(StationJobGenerationRange.PlayerSqrDistanceFromStationOffice), MethodType.Getter)] +public static class StationJobGenerationRange_PlayerSqrDistanceFromStationOffice_Patch +{ + private static bool Prefix(StationJobGenerationRange __instance, ref float __result) + { + if (!NetworkLifecycle.Instance.IsHost()) + return true; + + Vector3 anchor = __instance.transform.position; + + __result = float.MaxValue; + //Loop through all of the players and return the one thats closest to the anchor + foreach (ServerPlayer serverPlayer in NetworkLifecycle.Instance.Server.ServerPlayers) + { + float sqDist = (serverPlayer.WorldPosition - anchor).sqrMagnitude; + if (sqDist < __result) + __result = sqDist; + } + + return false; + } +} diff --git a/Multiplayer/Patches/Jobs/StationPatch.cs b/Multiplayer/Patches/Jobs/StationPatch.cs new file mode 100644 index 0000000..a0f253d --- /dev/null +++ b/Multiplayer/Patches/Jobs/StationPatch.cs @@ -0,0 +1,34 @@ +using DV.Logic.Job; +using HarmonyLib; +using Multiplayer.Components; +using Multiplayer.Components.Networking; +using Multiplayer.Components.Networking.Jobs; +using Multiplayer.Components.Networking.Train; +using Multiplayer.Utils; + +namespace Multiplayer.Patches.Jobs; + +[HarmonyPatch(typeof(Station), nameof(Station.AddJobToStation))] +public static class Station_AddJobToStation_Patch +{ + private static bool Prefix(Station __instance, Job job) + { + if (!NetworkLifecycle.Instance.IsHost()) + return false; + + Multiplayer.Log($"Station_AddJobToStation_Patch adding NetworkJob for stationId: {__instance.ID}, jobId: {job.ID}"); + + StationController stationController; + if(!StationComponentLookup.Instance.StationControllerFromId(__instance.ID, out stationController)) + return false; + + NetworkedJob netJob = stationController.gameObject.AddComponent(); + if (netJob != null) + { + netJob.job=job; + netJob.stationID = __instance.ID; + + } + return true; + } +} diff --git a/Multiplayer/Patches/World/StationProceduralJobsControllerPatch.cs b/Multiplayer/Patches/Jobs/StationProceduralJobsControllerPatch.cs similarity index 90% rename from Multiplayer/Patches/World/StationProceduralJobsControllerPatch.cs rename to Multiplayer/Patches/Jobs/StationProceduralJobsControllerPatch.cs index 217630b..0d82e62 100644 --- a/Multiplayer/Patches/World/StationProceduralJobsControllerPatch.cs +++ b/Multiplayer/Patches/Jobs/StationProceduralJobsControllerPatch.cs @@ -1,7 +1,7 @@ using HarmonyLib; using Multiplayer.Components.Networking; -namespace Multiplayer.Patches.World; +namespace Multiplayer.Patches.Jobs; [HarmonyPatch(typeof(StationProceduralJobsController), nameof(StationProceduralJobsController.TryToGenerateJobs))] public static class StationProceduralJobsController_TryToGenerateJobs_Patch diff --git a/Multiplayer/Patches/MainMenu/LauncherControllerPatch.cs b/Multiplayer/Patches/MainMenu/LauncherControllerPatch.cs index 38122f5..17d4e4c 100644 --- a/Multiplayer/Patches/MainMenu/LauncherControllerPatch.cs +++ b/Multiplayer/Patches/MainMenu/LauncherControllerPatch.cs @@ -93,10 +93,7 @@ private static void SetData(LauncherController __instance, UIStartGameData start private static void HostAction() { - // Implement host action logic here - Debug.Log("Host button clicked."); - - + //Debug.Log("Host button clicked."); RightPaneController_OnEnable_Patch.uIMenuController.SwitchMenu(RightPaneController_OnEnable_Patch.hostMenuIndex); diff --git a/Multiplayer/Utils/Csv.cs b/Multiplayer/Utils/Csv.cs index b3fc68e..fcb12e5 100644 --- a/Multiplayer/Utils/Csv.cs +++ b/Multiplayer/Utils/Csv.cs @@ -17,7 +17,7 @@ public static ReadOnlyDictionary> Parse(strin { // Split the input data into lines string[] separators = new string[] { "\r\n", "\n" }; - string[] lines = data.Split(separators, StringSplitOptions.None); + string[] lines = data.Split(separators, StringSplitOptions.RemoveEmptyEntries); // Use an OrderedDictionary to preserve the insertion order of keys var columns = new OrderedDictionary(); @@ -31,7 +31,6 @@ public static ReadOnlyDictionary> Parse(strin } // Iterate through the remaining lines (rows) - for (int i = 1; i < lines.Length; i++) { string line = lines[i]; @@ -41,13 +40,6 @@ public static ReadOnlyDictionary> Parse(strin continue; string rowKey = values[0]; - - //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; - } // Add the row values to the appropriate column dictionaries for (int j = 0; j < values.Count && j < keys.Count; j++) @@ -75,7 +67,6 @@ private static List ParseLine(string line) List values = new(); StringBuilder builder = new(); - // Helper method to add the current value to the list and reset the builder void FinishValue() { values.Add(builder.ToString()); @@ -151,10 +142,11 @@ public static string Dump(ReadOnlyDictionary> result.Append(','); } } + result.Remove(result.Length - 1, 1); result.Append('\n'); } - + return result.ToString(); } } diff --git a/info.json b/info.json index 37a82f6..43accd0 100644 --- a/info.json +++ b/info.json @@ -1,6 +1,6 @@ { "Id": "Multiplayer", - "Version": "0.1.5.0", + "Version": "0.1.5.2", "DisplayName": "Multiplayer", "Author": "Insprill, Macka, Morm", "EntryMethod": "Multiplayer.Multiplayer.Load", From 36db54add7c15b4e49b482d1b23db968aac85de9 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 14 Jul 2024 12:57:30 +1000 Subject: [PATCH 040/188] Squashed commit of the following: commit 9d3fb9951246bc92cf1dfdb38bc341ab525acb1b Author: AMacro Date: Sun Jul 14 12:41:53 2024 +1000 Squashed commit of the following: commit 633bdc03e33ad60796eefc5aa7f995fdbfdfd15d Author: AMacro Date: Sun Jul 14 12:40:25 2024 +1000 Cleaved up excessive logging commit c30a2e497f494349443977291abd1a84adf77fd3 Author: AMacro Date: Sun Jul 14 12:14:41 2024 +1000 Back-merged beta to feature/sync-jobs commit 5db5133b5fa4a700fe75fe58c1c86c9f2b565ade Merge: c0d547e fa0fbfb Author: AMacro Date: Sun Jul 14 11:39:10 2024 +1000 Merge branch 'beta' into feature/sync-jobs commit c0d547e62654abd746bea16666ad4685a39f3cec Author: AMacro Date: Sun Jul 14 11:26:17 2024 +1000 Preparing to merge to beta commit 263cc55a9069fc656fe042e4bce1f52e3ed5f340 Author: AMacro Date: Sat May 18 15:29:02 2024 +1000 Fix car plate sync and added better job sync pt2 Added car plate syncing Changed time format into UTC for logging Jobs now sync in a batch when a player connects and then progressively sync as new items are added commit eff0205c25e7e3b1028fd3c05c148d15c4c37667 Author: AMacro Date: Sat May 18 15:27:24 2024 +1000 Fix car plate sync and added better job sync Added car plate syncing Changed time format into UTC for logging Jobs now sync in a batch when a player connects and then progressively sync as new items are added commit 8adb497661adc8e161983a0cdb7ef806e06fb1b5 Author: AMacro Date: Sat May 18 11:09:05 2024 +1000 Fixed infinite blank spawning (CargoType data Type) CaregoType is now held as a CargoType, rather than byte in TaskDataData CargoType is now serialised as int (byte is not wide enough to store all values of CargoType) commit 440b9172b20b4c7c5af61805b7b91b4ee16644e2 Author: AMacro Date: Sat May 18 10:56:38 2024 +1000 Fixed PlayerSqrDistanceFrom*() calculation Calcs now use player WorldPosition instead of RawPosition. This is inline with the game's internal calcs and gives the correct result. commit 220a04aad5915ff9a4b0a6cd690d0af86166834e Merge: c42f868 e1a3e97 Author: AMacro Date: Sat May 18 09:58:58 2024 +1000 Merge branch 'Join-Menu-Improvements' into feature/sync-jobs commit c42f86829bb202382a0e6d81d35464ef55c8bb3a Author: AMacro Date: Sat May 18 09:41:26 2024 +1000 Revert "Merge branch 'Join-Menu-Improvements' into feature/sync-jobs" This reverts commit c376c80d4071478aba46084a48325b398eea2bb2, reversing changes made to 8df7a754455b3eb3331247b8faf69d678d226a3a. commit e1a3e97cdb439599db5c63e5763112b9ac186c26 Author: AMacro Date: Sun May 12 18:46:23 2024 +1000 Reworked the saving of last direct connection details Separated server and client settings commit c376c80d4071478aba46084a48325b398eea2bb2 Merge: 8df7a75 6daa671 Author: AMacro Date: Sun May 12 11:32:55 2024 +1000 Merge branch 'Join-Menu-Improvements' into feature/sync-jobs commit 8df7a754455b3eb3331247b8faf69d678d226a3a Merge: e37dd3e 764bfc7 Author: AMacro Date: Sun May 12 11:32:45 2024 +1000 Merge branch 'Localisation-Parsing-Fix' into feature/sync-jobs commit 6daa671d082bbc6cd2b85119f51e23c32b9a3b3f Author: AMacro Date: Sun May 12 10:45:10 2024 +1000 Enhanced "join" interface Default remote IP can now be set through the settings Popup/prompt for IP, port and password now auto-fill from the defaults commit e37dd3e07bda9344a2856ef5cdd0fdf7d6dfd82c Author: ChaoticWagon Date: Mon Sep 18 18:00:07 2023 -0400 Job syncing almost works commit 1827ded9adbcc4d5de54a399a315174e49a40412 Author: AMacro Date: Sun Jul 14 12:16:28 2024 +1000 Minor update to CSV parsing commit fa0fbfb66ad991519faa818e6e1b03a39190acf8 Merge: c6f795f 0871556 Author: Macka Date: Sun Jul 14 11:04:31 2024 +1000 Merge pull request #8 from AMacro/Localisation-Parsing-Fix Merge Localisation-Parsing-Fix into beta branch commit 0871556de5a73744b16cec5a17836d55fead6f47 Merge: db77e3d c6f795f Author: Macka Date: Sun Jul 14 11:03:58 2024 +1000 Merge branch 'beta' into Localisation-Parsing-Fix commit db77e3dd943a23ee4f6b11ca8158324cb5d7538a Merge: 764bfc7 c43328b Author: Macka Date: Sun Jul 14 10:54:17 2024 +1000 Merge pull request #1 from N95JPL/CSV-Key-Fix Fix CSV.cs commit c6f795ff531e99d157c20d6842982ba37298e6cc Merge: 2b666d4 18333e4 Author: Macka Date: Sun Jul 14 10:25:12 2024 +1000 Merge pull request #7 from AMacro/feature/in-game-chat Merge feature/in-game-chat into beta branch commit 2b666d4d608dd9f6bccf805a07f43a292a8c08cf Merge: 3edb410 2458ed2 Author: Macka Date: Sun Jul 14 10:22:44 2024 +1000 Merge pull request #6 from AMacro/feature/updated-server-browser Merge feature/updated-server-browser into beta branch commit 18333e4ffd7fad2bf46c7b9af3fa787e2d1e660c Merge: 2458ed2 13184b8 Author: Macka Date: Sun Jul 14 10:10:38 2024 +1000 Merge pull request #4 from morm075/in-game-chat Tidy up repositories - this will be the main one from now on. commit 2458ed2b251f2863e9f68a6f48806a6fb6310f40 Merge: 0455990 00359ad Author: Macka Date: Sun Jul 14 10:06:59 2024 +1000 Merge pull request #3 from morm075/updated-server-browser Tidy up repositories - this will be the main one from now on. commit 00359ad38d94cdb5393dd058c1066544bfc750fa Author: AMacro Date: Sun Jul 14 09:42:07 2024 +1000 Bug fixes for lobby server redirects commit 99257833a0ceb948c8444140e6276594d557fb04 Author: AMacro Date: Sun Jul 14 00:01:33 2024 +1000 General tidy up and QoL for server browser commit 13184b83e32eadd65a1fe35fc7d8cbc95f19b3a7 Author: AMacro Date: Sat Jul 13 21:46:34 2024 +1000 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 commit 9ee085c9766adc69a5e721a2a019ca263a451073 Author: AMacro Date: Sat Jul 13 17:52:24 2024 +1000 Added sent message history commit 14e5aaec0815483b9d00d4c04aba2933c40a01a0 Author: AMacro Date: Sat Jul 13 16:40:48 2024 +1000 Added chat commands Added server messages Added whispers Added help (displays commands) commit 2d3d2df9d1ce42e72a0010bb5c8d99187f53f322 Author: AMacro Date: Fri Jul 12 22:18:55 2024 +1000 Bug fixes commit ea633a3b879eed03280681426d1c5adfc537191a Author: AMacro Date: Sun Jul 7 22:35:05 2024 +1000 Added hotbar blocking and aligned chat window with lower left corner commit 830cd1cb2024791e9887b005014d3d929ac1d581 Author: AMacro Date: Sun Jul 7 21:24:27 2024 +1000 Initial commit Create chat panel blocked input from player when shown. Still need to block number keys for toolbelt and implement network logic commit ab36de5ab0209174e030c2cac9fce167573c27f8 Author: AMacro Date: Sun Jul 7 09:40:32 2024 +1000 Reorganised UI components commit 252745db58f6a72aeb4afd2495a936fd07ee19d0 Author: AMacro Date: Sun Jul 7 09:37:26 2024 +1000 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 commit 30c598849a02c2ba601f5cd1a483c2bcf267e406 Author: AMacro Date: Sun Jul 7 09:31:45 2024 +1000 Fixed translation issue commit 86f8245f99f638fce848148f33135ca87d9852aa Author: AMacro Date: Sun Jul 7 09:26:18 2024 +1000 Updated translations for server browser details pane Added some CSV parsing logic to detect missing quotes in CSVs (flag too many columns), preventing crashes. commit a99179a2895b3b4736643f606fde666233c4fe1b Author: AMacro Date: Sun Jul 7 01:20:13 2024 +1000 Server details displayed in pane When selecting a server, its details are now shown in the adjacent pane commit 6e8df4677bf4d4b1b00e7084bf17a79f6d5ad87e Author: AMacro Date: Sat Jul 6 14:28:13 2024 +1000 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) commit eb3b948160311756d8cfa7faa4b44c3dafc8f173 Author: AMacro Date: Mon Jul 1 20:15:34 2024 +1000 Refactored server browser for consistency ServerBrowserPane is now responsible for cleanup tasks and building the UI, rather than the RightPaneControllerPatch commit e5051da7001c195cf0a3dbe3d32b8f9c9c87902e Author: AMacro Date: Mon Jul 1 19:05:32 2024 +1000 Updated default server to https commit 0fa44a6890255b8e51856e7a90924be8f4adcecb Author: AMacro Date: Sun Jun 30 21:45:06 2024 +1000 Minor UI fixes and version update commit 499dacfd3adcdd199a822341dd7798c0a9a8795c Merge: a7ae049 8329b63 Author: AMacro Date: Sun Jun 30 21:00:26 2024 +1000 Merge branch 'updated-server-browser' of https://github.com/morm075/dv-multiplayer into updated-server-browser commit a7ae049063b1f25be82516ae66c0983171e26937 Author: AMacro Date: Sun Jun 30 20:51:08 2024 +1000 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 commit 8329b6372dd1fbcd6e8a219d9c7777bb8b62ff11 Author: morm075 <124874578+morm075@users.noreply.github.com> Date: Sun Jun 30 20:15:56 2024 +0930 Updates to locale.csv Added translations to locale.csv, Translations to be verified. commit 94f344f0da46135fbe05064250218b686aab3401 Author: AMacro Date: Thu Jun 27 22:29:00 2024 +1000 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 commit 492938ed57775b4a02ccab61491ae12bda2325a1 Author: AMacro Date: Sun Jun 23 11:53:14 2024 +1000 Update Read Me.md commit 76f5aeeb314690bd5077b6da4a8eb0be56f03c20 Merge: c190545 4b2c6bb Author: morm075 <124874578+morm075@users.noreply.github.com> Date: Sun Jun 23 10:15:46 2024 +0930 Merge branch 'updated-server-browser' of https://github.com/morm075/dv-multiplayer into feature/server-browser-update commit 4b2c6bb503f6c3f1350e17ce5879bce96fd16370 Author: AMacro Date: Sun Jun 23 10:44:32 2024 +1000 Fixed SSL compilation issues commit c19054565decb32ec10a91720677db78555ca467 Author: morm075 <124874578+morm075@users.noreply.github.com> Date: Sat Jun 22 20:16:38 2024 +0930 another correction commit 0e373f4dfa8147406dca144cddbc9d7f9d9101a4 Merge: d236a90 c66ee37 Author: AMacro Date: Sat Jun 22 18:01:50 2024 +1000 Merge branch 'updated-server-browser' of https://github.com/morm075/dv-multiplayer into updated-server-browser commit d236a90b1ae1104d02a4293f36772c8bf6ccb60b Author: AMacro Date: Sat Jun 22 18:01:46 2024 +1000 PHP and Rust servers implemented commit c66ee37326e262c150ddb892f24600f458129c82 Author: morm075 <124874578+morm075@users.noreply.github.com> Date: Sat Jun 22 10:26:05 2024 +0930 minor correction commit 6e721e56390389bbe8bb3765154fd136fc31045b Author: AMacro Date: Thu Jun 20 21:16:32 2024 +1000 added button icons commit 81aa6baf0897326f5d1f45108ec1939b3a0d7138 Author: morm075 <124874578+morm075@users.noreply.github.com> Date: Tue Jun 18 19:54:47 2024 +0930 Minor adjustments and commenting commit bcf5735d160bea9a3d859b5ed9828b91b41a88aa Merge: a4c8453 0455990 Author: morm075 <124874578+morm075@users.noreply.github.com> Date: Sun Jun 16 21:48:14 2024 +0930 Merge pull request #1 from AMacro/updated-server-browser Minor fixes and improvements commit 0455990b15f736d0dbab5096d5fdcecfe4bf657e Merge: af9cd6d a4c8453 Author: AMacro Date: Sun Jun 16 22:17:45 2024 +1000 Merge branch 'updated-server-browser' into updated-server-browser commit af9cd6d884177d2990e2205008ed7a87b20d40aa Author: AMacro Date: Sun Jun 16 22:04:12 2024 +1000 Minor fixes and improvements Fixed main menu highlight bug added random server generation for testing fixed gridview element layout implemented a server data object commit a4c84533a38295bdb5af537926cfa57e36b98435 Author: morm075 <124874578+morm075@users.noreply.github.com> Date: Sun Jun 16 13:30:56 2024 +0930 Continuing to update server browser commit c43328b56d7795bd0fd55353cbbe2020f5174231 Merge: 44471ca 764bfc7 Author: N95JPL <37276225+N95JPL@users.noreply.github.com> Date: Sun May 26 21:05:35 2024 +0100 Merge branch 'Localisation-Parsing-Fix' into CSV-Key-Fix commit 44471ca0e8ac210162b8313ae39f1fad41409482 Author: N95JPL <37276225+N95JPL@users.noreply.github.com> Date: Sun May 26 21:03:46 2024 +0100 Fix CSV.cs Now ignores blank/whitespace keys commit a6ab70479e663e4caad495a8b25020dbba36b234 Merge: 3edb410 c691e32 Author: morm075 <124874578+morm075@users.noreply.github.com> Date: Sun May 26 12:04:08 2024 +0930 Merge branch 'feature/server-browser' into feature/server-browser-update commit c691e32b1c9466bdb69528b7aeade87d778be035 Author: morm075 <124874578+morm075@users.noreply.github.com> Date: Sat May 25 21:02:41 2024 +0930 refactoring und updating update to network game commit 764bfc70fadbd61e87fb4a926db4469c45f3abde Author: AMacro Date: Sun May 12 09:48:19 2024 +1000 Fixed minor issue with CSV parsing so that unix/windows line breaks don't matter. commit e23d522a4e966920651802ebc67ab636599bb466 Author: Pierce Thompson Date: Tue Aug 8 00:26:19 2023 -0400 Begin creating the server browser menu This is still very incomplete, and is just laying the groundwork for future progress. --- .gitignore | 2 + Lobby Servers/PHP Server/.htaccess | 11 + .../PHP Server/DatabaseInterface.php | 10 + Lobby Servers/PHP Server/FlatfileDatabase.php | 100 ++ Lobby Servers/PHP Server/MySQLDatabase.php | 78 + Lobby Servers/PHP Server/Read Me.md | 149 ++ Lobby Servers/PHP Server/config.php | 16 + Lobby Servers/PHP Server/index.php | 120 ++ Lobby Servers/PHP Server/install.php | 54 + Lobby Servers/RestAPI.md | 251 +++ Lobby Servers/Rust Server/Cargo.lock | 1540 +++++++++++++++++ Lobby Servers/Rust Server/Cargo.toml | 18 + Lobby Servers/Rust Server/Read Me.md | 56 + Lobby Servers/Rust Server/config.json | 7 + Lobby Servers/Rust Server/src/config.rs | 44 + Lobby Servers/Rust Server/src/handlers.rs | 175 ++ Lobby Servers/Rust Server/src/main.rs | 74 + 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 | 403 +++++ .../MainMenu/MainMenuThingsAndStuff.cs | 144 +- .../IServerBrowserGameDetails.cs | 33 + ...pupTextInputFieldControllerNoValidation.cs | 93 + .../ServerBrowserDummyElement.cs | 62 + .../ServerBrowser/ServerBrowserElement.cs | 85 + .../ServerBrowser/ServerBrowserGridView.cs | 38 + .../Components/MainMenu/ServerBrowserPane.cs | 662 +++++++ .../Networking/Jobs/NetworkedJob.cs | 342 ++++ .../Components/Networking/NetworkLifecycle.cs | 38 +- .../Train/NetworkTrainsetWatcher.cs | 1 + .../Networking/Train/NetworkedTrainCar.cs | 40 + .../Components/Networking/UI/ChatGUI.cs | 677 ++++++++ .../Networking/{ => UI}/NetworkStatsGui.cs | 2 +- .../Networking/{ => UI}/PlayerListGUI.cs | 2 +- .../Networking/World/NetworkedStation.cs | 120 ++ .../SaveGame/StartGameData_ServerSave.cs | 1 + .../Components/StationComponentLookup.cs | 50 + Multiplayer/Locale.cs | 297 ++-- Multiplayer/Multiplayer.cs | 6 +- Multiplayer/Multiplayer.csproj | 9 +- Multiplayer/Networking/Data/JobData.cs | 139 ++ .../Networking/Data/LobbyServerData.cs | 150 ++ .../Data/LobbyServerResponseData.cs | 23 + .../Networking/Data/LobbyServerUpdateData.cs | 36 + Multiplayer/Networking/Data/ModInfo.cs | 4 +- Multiplayer/Networking/Data/ServerPlayer.cs | 1 + Multiplayer/Networking/Data/TaskDataData.cs | 371 ++++ .../Managers/Client/NetworkClient.cs | 220 ++- .../Networking/Managers/NetworkManager.cs | 7 +- .../Networking/Managers/Server/ChatManager.cs | 197 +++ .../Managers/Server/LobbyServerManager.cs | 187 ++ .../Managers/Server/NetworkServer.cs | 230 ++- .../Jobs/ClientboundJobCreatePacket.cs | 22 + .../Clientbound/Jobs/ClientboundJobPacket.cs | 11 + .../Jobs/ClientboundJobTakeResponsePacket.cs | 12 + .../Packets/Common/CommonChatPacket.cs | 14 + .../Jobs/ServerboundJobTakeRequestPacket.cs | 10 + .../Patches/Jobs/JobOverviewUsePatch.cs | 66 + .../Patches/Jobs/StationControllerPatch.cs | 13 + .../Jobs/StationJobGenerationRangePatch.cs | 53 + Multiplayer/Patches/Jobs/StationPatch.cs | 34 + .../StationProceduralJobsControllerPatch.cs | 2 +- .../MainMenu/LauncherControllerPatch.cs | 101 ++ .../MainMenu/LocalizationManagerPatch.cs | 41 +- .../MainMenu/MainMenuControllerPatch.cs | 95 +- .../MainMenu/RightPaneControllerPatch.cs | 116 +- .../Patches/World/SaveGameManagerPatch.cs | 2 +- Multiplayer/Settings.cs | 20 + Multiplayer/Utils/Csv.cs | 199 ++- Multiplayer/Utils/DvExtensions.cs | 68 + 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 + info.json | 7 +- locale.csv | 93 +- 84 files changed, 8294 insertions(+), 473 deletions(-) create mode 100644 Lobby Servers/PHP Server/.htaccess 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/config.php create mode 100644 Lobby Servers/PHP Server/index.php create mode 100644 Lobby Servers/PHP Server/install.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/config.json 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/main.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 create mode 100644 Multiplayer/Components/MainMenu/ServerBrowser/IServerBrowserGameDetails.cs create mode 100644 Multiplayer/Components/MainMenu/ServerBrowser/PopupTextInputFieldControllerNoValidation.cs create mode 100644 Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserDummyElement.cs create mode 100644 Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserElement.cs create mode 100644 Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserGridView.cs create mode 100644 Multiplayer/Components/MainMenu/ServerBrowserPane.cs create mode 100644 Multiplayer/Components/Networking/Jobs/NetworkedJob.cs create mode 100644 Multiplayer/Components/Networking/UI/ChatGUI.cs rename Multiplayer/Components/Networking/{ => UI}/NetworkStatsGui.cs (98%) rename Multiplayer/Components/Networking/{ => UI}/PlayerListGUI.cs (97%) create mode 100644 Multiplayer/Components/Networking/World/NetworkedStation.cs create mode 100644 Multiplayer/Components/StationComponentLookup.cs create mode 100644 Multiplayer/Networking/Data/JobData.cs create mode 100644 Multiplayer/Networking/Data/LobbyServerData.cs create mode 100644 Multiplayer/Networking/Data/LobbyServerResponseData.cs create mode 100644 Multiplayer/Networking/Data/LobbyServerUpdateData.cs create mode 100644 Multiplayer/Networking/Data/TaskDataData.cs create mode 100644 Multiplayer/Networking/Managers/Server/ChatManager.cs create mode 100644 Multiplayer/Networking/Managers/Server/LobbyServerManager.cs create mode 100644 Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobCreatePacket.cs create mode 100644 Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobPacket.cs create mode 100644 Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobTakeResponsePacket.cs create mode 100644 Multiplayer/Networking/Packets/Common/CommonChatPacket.cs create mode 100644 Multiplayer/Networking/Packets/Serverbound/Jobs/ServerboundJobTakeRequestPacket.cs create mode 100644 Multiplayer/Patches/Jobs/JobOverviewUsePatch.cs create mode 100644 Multiplayer/Patches/Jobs/StationControllerPatch.cs create mode 100644 Multiplayer/Patches/Jobs/StationJobGenerationRangePatch.cs create mode 100644 Multiplayer/Patches/Jobs/StationPatch.cs rename Multiplayer/Patches/{World => Jobs}/StationProceduralJobsControllerPatch.cs (90%) create mode 100644 Multiplayer/Patches/MainMenu/LauncherControllerPatch.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 diff --git a/.gitignore b/.gitignore index 87860e1..d792194 100644 --- a/.gitignore +++ b/.gitignore @@ -306,3 +306,5 @@ MultiplayerAssets/ProjectSettings/* !MultiplayerAssets/ProjectSettings/ProjectVersion.txt # Packages !MultiplayerAssets/Packages +/Lobby Servers/Rust Server/target +*.pem diff --git a/Lobby Servers/PHP Server/.htaccess b/Lobby Servers/PHP Server/.htaccess new file mode 100644 index 0000000..c8f0917 --- /dev/null +++ b/Lobby Servers/PHP Server/.htaccess @@ -0,0 +1,11 @@ +# 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 +RewriteRule ^ index.php [QSA,L] \ No newline at end of file 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..9634991 --- /dev/null +++ b/Lobby Servers/PHP Server/FlatfileDatabase.php @@ -0,0 +1,100 @@ +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'], + "private_key" => $data['private_key'] + ]); + } + + 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..32a774e --- /dev/null +++ b/Lobby Servers/PHP Server/MySQLDatabase.php @@ -0,0 +1,78 @@ +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'], + "private_key" => $data['private_key'] + ]); + } + + 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..5bc4c50 --- /dev/null +++ b/Lobby Servers/PHP Server/Read Me.md @@ -0,0 +1,149 @@ +# 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 + +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: +```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 new file mode 100644 index 0000000..52073ea --- /dev/null +++ b/Lobby Servers/PHP Server/config.php @@ -0,0 +1,16 @@ + 'mysql', // Change to 'flatfile' to use flatfile database + 'host' => 'localhost', + 'dbname' => 'your_database', + 'username' => 'your_username', + '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 new file mode 100644 index 0000000..556e828 --- /dev/null +++ b/Lobby Servers/PHP Server/index.php @@ -0,0 +1,120 @@ + "Invalid server information"]); + } + + if (!isset($data['ip']) || !filter_var($data['ip'], FILTER_VALIDATE_IP)) { + $data['ip'] = $_SERVER['REMOTE_ADDR']; + } + + $data['game_server_id'] = uuid_create(); + $data['private_key'] = generate_private_key(); + + 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"]); + } + + $data['last_update'] = time(); + return $db->updateGameServer($data); +} + +function remove_game_server($db, $data) { + if (!validate_server_update($db, $data)) { + return json_encode(["error" => "Invalid game server ID or private key"]); + } + + return $db->removeGameServer($data); +} + +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']); + unset($server['last_update']); + } + return json_encode($servers); +} + +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; + } + 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 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) + ); +} + +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 new file mode 100644 index 0000000..4309b2c --- /dev/null +++ b/Lobby Servers/RestAPI.md @@ -0,0 +1,251 @@ +# 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 +- 2: Scenario + +### 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 (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. + - 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", + "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"` + +### Update Server + +- **URL:** `/update_game_server` +- **Method:** `POST` +- **Content-Type:** `application/json` +- **Request Body:** + ```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:** + - **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", + "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 + - **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", + "private_key": "6fca6e1499dab0358f79dc0b251b4e23" +} +``` + +### Update Game Server +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 +``` +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", + "private_key": "6fca6e1499dab0358f79dc0b251b4e23" +}' 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..f80e1d8 --- /dev/null +++ b/Lobby Servers/Rust Server/Cargo.lock @@ -0,0 +1,1540 @@ +# 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", + "openssl", + "rand", + "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..2e80b78 --- /dev/null +++ b/Lobby Servers/Rust Server/Cargo.toml @@ -0,0 +1,18 @@ +[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"] } +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 new file mode 100644 index 0000000..db84e87 --- /dev/null +++ b/Lobby Servers/Rust Server/Read Me.md @@ -0,0 +1,56 @@ +# 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/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` + +## 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 customise these parameters, create a `config.json` file in the project directory with the desired values. +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/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/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..76eec8d --- /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: 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: 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 new file mode 100644 index 0000000..286a442 --- /dev/null +++ b/Lobby Servers/Rust Server/src/main.rs @@ -0,0 +1,74 @@ +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 tokio::time::{interval, Duration}; + +#[tokio::main] +async fn main() -> std::io::Result<()> { + 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(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(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)())? + } else { + server_builder.bind(format!("0.0.0.0:{}", config.port))? + } + }; + + // Start the server + server.run().await +} \ 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..043c683 --- /dev/null +++ b/Multiplayer/Components/MainMenu/HostGamePane.cs @@ -0,0 +1,403 @@ +using System; +using System.Reflection; +using DV; +using DV.UI; +using DV.UI.PresetEditors; +using DV.UIFramework; +using DV.Localization; +using DV.Common; +using Multiplayer.Utils; +using TMPro; +using UnityEngine; +using UnityEngine.UI; +using UnityEngine.Events; +using Multiplayer.Networking.Data; +using Multiplayer.Components.Networking; +namespace Multiplayer.Components.MainMenu; + +public class HostGamePane : MonoBehaviour +{ + private const int MAX_SERVER_NAME_LEN = 25; + private const int MAX_PORT_LEN = 5; + private const int MAX_DETAILS_LEN = 500; + + private const int MIN_PORT = 1024; + private const int MAX_PORT = 49151; + private const int MIN_PLAYERS = 2; + private const int MAX_PLAYERS = 10; + + private const int DEFAULT_PORT = 7777; + + TMP_InputField serverName; + TMP_InputField password; + TMP_InputField port; + TMP_InputField details; + + Slider maxPlayers; + + Toggle gamePublic; + + ButtonDV startButton; + + GameObject ViewPort; + + public ISaveGame saveGame; + public UIStartGameData startGameData; + public AUserProfileProvider userProvider; + public AScenarioProvider scenarioProvider; + LauncherController lcInstance; + + public Action 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()"); + this.SetupListeners(true); + } + + // Disable listeners + 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) + { + Multiplayer.LogError("Divider not found!"); + return; + } + + GameObject cbPrefab = goMMC.FindChildByName("CheckboxFreeCam"); + if (cbPrefab == null) + { + Multiplayer.LogError("CheckboxFreeCam not found!"); + return; + } + + GameObject sliderPrefab = goMMC.FindChildByName("SliderLimitSession"); + if (sliderPrefab == null) + { + Multiplayer.LogError("SliderLimitSession not found!"); + return; + } + + GameObject inputPrefab = MainMenuThingsAndStuff.Instance.renamePopupPrefab.gameObject.FindChildByName("TextFieldTextIcon"); + if (inputPrefab == null) + { + Multiplayer.LogError("TextFieldTextIcon not found!"); + return; + } + + + lcInstance = goMMC.FindChildByName("PaneRight Launcher").GetComponent(); + if (lcInstance == null) + { + Multiplayer.LogError("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(); + + //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(); + 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; //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(); + + + 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) + { + if (on) + { + serverName.onValueChanged.RemoveAllListeners(); + serverName.onValueChanged.AddListener(new UnityAction(ValidateInputs)); + + port.onValueChanged.RemoveAllListeners(); + port.onValueChanged.AddListener(new UnityAction(ValidateInputs)); + } + else + { + this.serverName.onValueChanged.RemoveAllListeners(); + } + + } + + #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.ToggleInteractable(valid); + + //Multiplayer.Log($"HostPane 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); + } + + + 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 + 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); + //Multiplayer.Log($"OnRunClicked exists: {ContinueGameRequested != null}"); + ContinueGameRequested?.Invoke(lcInstance, null); + } + + + + #endregion + + +} diff --git a/Multiplayer/Components/MainMenu/MainMenuThingsAndStuff.cs b/Multiplayer/Components/MainMenu/MainMenuThingsAndStuff.cs index 02a6d6b..732a941 100644 --- a/Multiplayer/Components/MainMenu/MainMenuThingsAndStuff.cs +++ b/Multiplayer/Components/MainMenu/MainMenuThingsAndStuff.cs @@ -4,90 +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; + Multiplayer.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() - { - 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/ServerBrowser/IServerBrowserGameDetails.cs b/Multiplayer/Components/MainMenu/ServerBrowser/IServerBrowserGameDetails.cs new file mode 100644 index 0000000..28d4d38 --- /dev/null +++ b/Multiplayer/Components/MainMenu/ServerBrowser/IServerBrowserGameDetails.cs @@ -0,0 +1,33 @@ +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; +using Newtonsoft.Json; + +namespace Multiplayer.Components.MainMenu +{ + // + public interface IServerBrowserGameDetails : IDisposable + { + string id { get; set; } + string ip { get; set; } + int port { get; set; } + string Name { 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; } + string ServerDetails { get; set; } + int Ping { get; set; } + + } +} diff --git a/Multiplayer/Components/MainMenu/ServerBrowser/PopupTextInputFieldControllerNoValidation.cs b/Multiplayer/Components/MainMenu/ServerBrowser/PopupTextInputFieldControllerNoValidation.cs new file mode 100644 index 0000000..170caab --- /dev/null +++ b/Multiplayer/Components/MainMenu/ServerBrowser/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: + Multiplayer.LogError(string.Format("Unhandled action {0}", action)); + 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/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 new file mode 100644 index 0000000..f0ecf14 --- /dev/null +++ b/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserElement.cs @@ -0,0 +1,85 @@ +using DV.UIFramework; +using Multiplayer.Utils; +using System.ComponentModel; +using TMPro; +using UnityEngine; +using UnityEngine.UI; + +namespace Multiplayer.Components.MainMenu.ServerBrowser +{ + 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 = 124; // Adjusted width for the ping text + private const int PING_POS_X = 650; // X position for the ping text + + private void Awake() + { + // 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(); + + //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; + playerCount.transform.position = new Vector3(namePos.x + nameSize.x, namePos.y, namePos.z); + + // Adjust the size and position of the ping text + Vector2 rowSize = 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 change icon + icon.sprite = Multiplayer.AssetIndex.lockIcon; + } + + public override void SetData(IServerBrowserGameDetails data, AGridView _) + { + // 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(); + } + + private void UpdateView(object sender = null, PropertyChangedEventArgs e = null) + { + // 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/Components/MainMenu/ServerBrowser/ServerBrowserGridView.cs b/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserGridView.cs new file mode 100644 index 0000000..7f13fb3 --- /dev/null +++ b/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserGridView.cs @@ -0,0 +1,38 @@ +using System; +using DV.UI; +using DV.UIFramework; +using Multiplayer.Components.MainMenu.ServerBrowser; +using UnityEngine; +using UnityEngine.UI; + +namespace Multiplayer.Components.MainMenu +{ + [RequireComponent(typeof(ContentSizeFitter))] + [RequireComponent(typeof(VerticalLayoutGroup))] + // + public class ServerBrowserGridView : AGridView + { + + private void Awake() + { + Multiplayer.Log("serverBrowserGridview Awake"); + + //swap controller + this.viewElementPrefab.SetActive(false); + this.dummyElementPrefab = Instantiate(this.viewElementPrefab); + + GameObject.Destroy(this.viewElementPrefab.GetComponent()); + GameObject.Destroy(this.dummyElementPrefab.GetComponent()); + + this.viewElementPrefab.AddComponent(); + this.dummyElementPrefab.AddComponent(); + + this.viewElementPrefab.name = "prefabServerBrowserElement"; + this.dummyElementPrefab.name = "prefabServerBrowserDummyElement"; + + this.viewElementPrefab.SetActive(true); + this.dummyElementPrefab.SetActive(true); + + } + } +} diff --git a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs new file mode 100644 index 0000000..f74576a --- /dev/null +++ b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs @@ -0,0 +1,662 @@ +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; +using DV; +using Multiplayer.Components.Networking.UI; + + + +namespace Multiplayer.Components.MainMenu +{ + public class ServerBrowserPane : 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 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(); + private ServerBrowserGridView gridView; + private ScrollRect parentScroller; + private string serverIDOnRefresh; + private IServerBrowserGameDetails selectedServer; + + //Button variables + private ButtonDV buttonJoin; + 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; + 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; + private int 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()"); + + CleanUI(); + BuildUI(); + + 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.serverIDOnRefresh = ""; + + buttonDirectIP.ToggleInteractable(true); + buttonRefresh.ToggleInteractable(true); + } + + // Disable listeners + 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")); + + 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")); + + } + 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(); + + //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"; + + /* + * Setup server details + */ + + // 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 = "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); + 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; + } + + // Set up event listeners + buttonDirectIP = goDirectIP.GetComponent(); + buttonDirectIP.onClick.AddListener(DirectAction); + + buttonJoin = goJoin.GetComponent(); + buttonJoin.onClick.AddListener(JoinAction); + + buttonRefresh = goRefresh.GetComponent(); + buttonRefresh.onClick.AddListener(RefreshAction); + + //Lock out the join button until a server has been selected + buttonJoin.ToggleInteractable(false); + } + private void SetupServerBrowser() + { + GameObject GridviewGO = this.FindChildByName("Scroll View").FindChildByName("GRID VIEW"); + SaveLoadGridView slgv = GridviewGO.GetComponent(); + + GridviewGO.SetActive(false); + + gridView = GridviewGO.AddComponent(); + slgv.viewElementPrefab.SetActive(false); + gridView.viewElementPrefab = Instantiate(slgv.viewElementPrefab); + + + GameObject.Destroy(slgv); + + GridviewGO.SetActive(true); + } + private void SetupListeners(bool on) + { + if (on) + { + this.gridView.SelectedIndexChanged += this.IndexChanged; + } + else + { + this.gridView.SelectedIndexChanged -= this.IndexChanged; + } + + } + + #endregion + + #region UI callbacks + private void RefreshAction() + { + if (serverRefreshing) + return; + + + + if (selectedServer != null) + { + serverIDOnRefresh = selectedServer.id; + } + + serverRefreshing = true; + autoRefresh = true; + buttonJoin.ToggleInteractable(false); + buttonRefresh.ToggleInteractable(false); + + StartCoroutine(GetRequest($"{Multiplayer.Settings.LobbyServerAddress}/list_game_servers")); + + } + private void JoinAction() + { + if (selectedServer != null) + { + buttonDirectIP.ToggleInteractable(false); + buttonJoin.ToggleInteractable(false); + + if (selectedServer.HasPassword) + { + //not making a direct connection + direct = false; + ipAddress = selectedServer.ip; + portNumber = selectedServer.port; + + ShowPasswordPopup(); + + return; + } + + //No password, just connect + SingletonBehaviour.Instance.StartClient(selectedServer.ip, selectedServer.port, null, false); + } + } + + private void DirectAction() + { + //Debug.Log($"DirectAction()"); + buttonDirectIP.ToggleInteractable(false); + buttonJoin.ToggleInteractable(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]; + + UpdateDetailsPane(); + + //Check if we can connect to this server + + Multiplayer.Log($"Server: \"{selectedServer.GameVersion}\" \"{selectedServer.MultiplayerVersion}\""); + Multiplayer.Log($"Client: \"{BuildInfo.BUILD_VERSION_MAJOR.ToString()}\" \"{Multiplayer.ModEntry.Version.ToString()}\""); + Multiplayer.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 + { + buttonJoin.ToggleInteractable(false); + } + } + + #endregion + + private void UpdateDetailsPane() + { + string details=""; + + if (selectedServer != null) + { + //Multiplayer.Log("Prepping Data"); + serverName.text = selectedServer.Name; + + //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 += "" + 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; + + //Multiplayer.Log("Finished Prepping Data"); + detailsPane.text = details; + } + } + + private void ShowIpPopup() + { + Multiplayer.Log("In ShowIpPpopup"); + var popup = MainMenuThingsAndStuff.Instance.ShowRenamePopup(); + if (popup == null) + { + Multiplayer.LogError("Popup not found."); + return; + } + + popup.labelTMPro.text = Locale.SERVER_BROWSER__IP; + popup.GetComponentInChildren().text = Multiplayer.Settings.LastRemoteIP; + + popup.Closed += result => + { + if (result.closedBy == PopupClosedByAction.Abortion) + { + buttonDirectIP.ToggleInteractable(true); + IndexChanged(gridView); //re-enable the join button if a valid gridview item is selected + return; + } + + if (!IPv4Regex.IsMatch(result.data) && !IPv6Regex.IsMatch(result.data)) + { + ShowOkPopup(Locale.SERVER_BROWSER__IP_INVALID, ShowIpPopup); + } + else + { + ipAddress = result.data; + ShowPortPopup(); + } + }; + } + + 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}"; + popup.GetComponentInChildren().contentType = TMP_InputField.ContentType.IntegerNumber; + popup.GetComponentInChildren().characterLimit = MAX_PORT_LEN; + + popup.Closed += result => + { + if (result.closedBy == PopupClosedByAction.Abortion) + { + buttonDirectIP.ToggleInteractable(true); + return; + } + + if (!PortRegex.IsMatch(result.data)) + { + ShowOkPopup(Locale.SERVER_BROWSER__PORT_INVALID, ShowIpPopup); + } + else + { + portNumber = ushort.Parse(result.data); + ShowPasswordPopup(); + } + }; + + } + + private void ShowPasswordPopup() + { + var popup = MainMenuThingsAndStuff.Instance.ShowRenamePopup(); + if (popup == null) + { + Multiplayer.LogError("Popup not found."); + return; + } + + popup.labelTMPro.text = Locale.SERVER_BROWSER__PASSWORD; + + //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) + { + buttonDirectIP.ToggleInteractable(true); + return; + } + + if (direct) + { + //store params for later + Multiplayer.Settings.LastRemoteIP = ipAddress; + Multiplayer.Settings.LastRemotePort = portNumber; + Multiplayer.Settings.LastRemotePassword = result.data; + + } + + SingletonBehaviour.Instance.StartClient(ipAddress, portNumber, result.data, false); + + //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 + Multiplayer.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 + Multiplayer.LogError("Connection failed!"); + // ShowConnectionFailedPopup(); + } + + IEnumerator GetRequest(string uri) + { + 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) + { + Multiplayer.LogError(pages[page] + ": Error: " + webRequest.error); + } + else + { + Multiplayer.Log(pages[page] + ":\nReceived: " + webRequest.downloadHandler.text); + + LobbyServerData[] response; + + response = Newtonsoft.Json.JsonConvert.DeserializeObject(webRequest.downloadHandler.text); + + Multiplayer.Log($"Serverbrowser servers: {response.Length}"); + + foreach (LobbyServerData server in response) + { + Multiplayer.Log($"Server 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); + + //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; + timePassed = 0; + } + + 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) + { + foreach (var button in buttons) + { + button.SetActive(true); + } + } + + private void FillDummyServers() + { + gridView.showDummyElement = false; + gridViewModel.Clear(); + + + IServerBrowserGameDetails item = null; + + for (int i = 0; i < UnityEngine.Random.Range(1, 50); i++) + { + + 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); + 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); + } + + gridView.SetModel(gridViewModel); + } + } + + +} diff --git a/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs b/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs new file mode 100644 index 0000000..1980329 --- /dev/null +++ b/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs @@ -0,0 +1,342 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using DV.Logic.Job; +using DV.ThingTypes; +using DV.Utils; +using Multiplayer.Components.Networking.Player; +using Multiplayer.Components.Networking.World; +using Multiplayer.Networking.Data; +using Multiplayer.Utils; +using UnityEngine; +using static System.Collections.Specialized.BitVector32; + +namespace Multiplayer.Components.Networking.Jobs; + +public class NetworkedJob : IdMonoBehaviour +{ + #region Lookup Cache + + private static readonly Dictionary jobToNetworkedJob = new(); + private static readonly Dictionary jobIdToNetworkedJob = new(); + private static readonly Dictionary jobIdToJob = new(); + + public static bool Get(ushort netId, out NetworkedJob obj) + { + bool b = Get(netId, out IdMonoBehaviour rawObj); + obj = (NetworkedJob)rawObj; + return b; + } + + public static bool GetJob(ushort netId, out Job obj) + { + bool b = Get(netId, out NetworkedJob networkedJob); + obj = b ? networkedJob.job : null; + return b; + } + + + public static NetworkedJob GetFromJob(Job job) + { + return jobToNetworkedJob[job]; + } + + public static bool TryGetFromJob(Job job, out NetworkedJob networkedJob) + { + return jobToNetworkedJob.TryGetValue(job, out networkedJob); + } + + /*public static NetworkedJob AddJob(string stationID, Job job) + { + NetworkedJob netJob = new NetworkedJob(stationID, job); + + jobToNetworkedJob[job] = netJob; + jobIdToNetworkedJob[job.ID] = netJob; + jobIdToJob[job.ID] = job; + + Multiplayer.Log($"NetworkedJob Added with netId: {jobToNetworkedJob[job].NetId}, jobId: {job.ID}"); + return jobToNetworkedJob[job]; + }*/ + #endregion + + public Job job; + public JobOverview jobOverview; + public JobBooklet jobBooklet; + public string stationID; + public bool isJobNew = true; + public bool isJobDirty = false; + public bool isTaskDirty = false; + + public bool? allowTake = null; + public Guid takenBy; //GUID of player who took the job + public JobValidator jobValidator; + + //might be useful when a job is taken? + //public bool HasPlayers => PlayerManager.Car == Job || GetComponentInChildren() != null; + + #region Client + + private bool client_Initialized; + + #endregion + + protected override bool IsIdServerAuthoritative => true; + + protected override void Awake() + { + Multiplayer.Log("NetworkJob.Awake()"); + base.Awake(); + + + /* + job = GetComponent(); + jobToNetworkedJob[job] = this; + + + + if (NetworkLifecycle.Instance.IsHost()) + { + //do we need a job watcher - probably not, but maybe or maybe we need a task watcher + //NetworkTrainsetWatcher.Instance.CheckInstance(); // Ensure the NetworkTrainsetWatcher is initialized + } + else + { + //Networked task?? + + //Client_trainSpeedQueue = TrainCar.GetOrAddComponent(); + //Client_trainRigidbodyQueue = TrainCar.GetOrAddComponent(); + //StartCoroutine(Client_InitLater()); + } + */ + } + + private void Start() + { + //startup stuff + Multiplayer.Log("NetworkedJob.Start()"); + + jobToNetworkedJob[job] = this; + jobIdToNetworkedJob[job.ID] = this; + jobIdToJob[job.ID] = job; + + isJobNew = true; //Send new jobs on tick + + StationController station; + if (!StationComponentLookup.Instance.StationControllerFromId(stationID, out station)) + { + Multiplayer.LogWarning($"NetworkJob.Start() Could not get staion for stationId: {stationID}"); + return; + } + + if (!NetworkLifecycle.Instance.IsHost()) + { + //station.logicStation.AddJobToStation(job); + if (station.logicStation.availableJobs.Contains(job)) + { + Multiplayer.LogError("Trying to add the same job[" + job.ID + "] multiple times to station! Skipping, trying to recover."); + return; + } + + station.logicStation.availableJobs.Add(job); + job.JobTaken += this.OnJobTaken; + job.JobExpired += this.OnJobExpired; + //job.JobAddedToStation?.Invoke(); + SingletonBehaviour.Instance.StartCoroutine(NetworkedStation.UpdateCarPlates(job.tasks, job.ID)); + } + else + { + //setup even handlers + job.JobTaken += this.OnJobTaken; + job.JobExpired += this.OnJobExpired; + NetworkLifecycle.Instance.OnTick += Server_OnTick; + } + + Multiplayer.Log("NetworkedJob.Start() Started"); + //possibly capture tasks at this point for tracking?? + } + + private void OnDisable() + { + if (UnloadWatcher.isQuitting) + return; + + NetworkLifecycle.Instance.OnTick -= Common_OnTick; + NetworkLifecycle.Instance.OnTick -= Server_OnTick; + + if (UnloadWatcher.isUnloading) + return; + + job.JobTaken -= this.OnJobTaken; + + jobToNetworkedJob.Remove(job); + jobIdToNetworkedJob.Remove(job.ID); + jobIdToNetworkedJob.Remove(job.ID); + + //Clean up any actions we added + + if (NetworkLifecycle.Instance.IsHost()) + { + //actions relating only to host + } + + Destroy(this); + } + + /*public NetworkedJob(string stationID, Job job) + { + this.job = job; + this.stationID = stationID; + + //setup even handlers + //job.JobTaken += + + isJobNew = true; //Send new jobs on tick + + }*/ + + #region Server + + //wait for tasks? + + /* + public bool Server_ValidateClientTakeJob(ServerPlayer player, CommonTrainPortsPacket packet) + { + + return false; + } + */ + + /* + public bool Server_ValidateClientAbandonedJob(ServerPlayer player, CommonTrainPortsPacket packet) + { + + return false; + } + */ + + /* + public bool Server_ValidateClientCompleteJob(ServerPlayer player, CommonTrainPortsPacket packet) + { + + return false; + } + */ + + + private void Server_OnTick(uint tick) + { + if (UnloadWatcher.isUnloading) + return; + + Server_SendNewJob(); + //Server_SendJobStatus(); + //Server_SendTaskStatus(); + //Server_SendJobDestroy(); + + } + + private void Server_SendNewJob() + { + if (!isJobNew) + return; + + isJobNew = false; + NetworkLifecycle.Instance.Server.SendJobCreatePacket(this); + } + /* + private void Server_SendJobStatus() + { + if (!sendCouplers) + return; + sendCouplers = false; + + if (Job.frontCoupler.hoseAndCock.IsHoseConnected) + NetworkLifecycle.Instance.Client.SendHoseConnected(Job.frontCoupler, Job.frontCoupler.coupledTo, false); + + if (Job.rearCoupler.hoseAndCock.IsHoseConnected) + NetworkLifecycle.Instance.Client.SendHoseConnected(Job.rearCoupler, Job.rearCoupler.coupledTo, false); + + NetworkLifecycle.Instance.Client.SendCockState(NetId, Job.frontCoupler, Job.frontCoupler.IsCockOpen); + NetworkLifecycle.Instance.Client.SendCockState(NetId, Job.rearCoupler, Job.rearCoupler.IsCockOpen); + } + */ + + + #endregion + + #region Common + + private void Common_OnTick(uint tick) + { + if (UnloadWatcher.isUnloading) + return; + /* + Common_SendHandbrakePosition(); + Common_SendFuses(); + Common_SendPorts(); + */ + } + + public void OnJobTaken(Job jobTaken,bool _) + { + Multiplayer.Log($"JobTaken: {jobTaken.ID}"); + jobTaken.JobTaken -= this.OnJobTaken; + jobTaken.JobExpired -= this.OnJobExpired; + + /* + takenJob.JobCompleted += OnJobCompleted; + takenJob.JobAbandoned += OnJobAbandoned; + availableJobs.Remove(takenJob); + takenJobs.Add(takenJob); + */ + + isJobDirty = true; + /* + jobTaken.JobExpired -= this.OnJobExpired; + jobTaken.JobCompleted += this.OnJobCompleted; + jobTaken.JobAbandoned += this.OnJobAbandoned; + */ + } + + public void OnJobExpired(Job jobExpired) + { + Multiplayer.Log($"Job Expired: {job.ID}"); + jobExpired.JobTaken -= this.OnJobTaken; + jobExpired.JobExpired -= this.OnJobExpired; + //jobExpired.JobCompleted += this.OnJobCompleted; + //jobExpired.JobAbandoned += this.OnJobAbandoned; + + isJobDirty = true; + + } + + #endregion + + #region Client + + /* + public void Client_ReceiveJopStatus(in TrainsetMovementPart movementPart, uint tick) + { + if (!client_Initialized) + return; + if (Job.isEligibleForSleep) + Job.ForceOptimizationState(false); + + if (movementPart.IsRigidbodySnapshot) + { + Job.Derail(); + Job.stress.ResetTrainStress(); + Client_trainRigidbodyQueue.ReceiveSnapshot(movementPart.RigidbodySnapshot, tick); + } + else + { + Client_trainSpeedQueue.ReceiveSnapshot(movementPart.Speed, tick); + Job.stress.slowBuildUpStress = movementPart.SlowBuildUpStress; + client_bogie1Queue.ReceiveSnapshot(movementPart.Bogie1, tick); + client_bogie2Queue.ReceiveSnapshot(movementPart.Bogie2, tick); + } + } + */ + #endregion +} diff --git a/Multiplayer/Components/Networking/NetworkLifecycle.cs b/Multiplayer/Components/Networking/NetworkLifecycle.cs index 7c14288..e07dda8 100644 --- a/Multiplayer/Components/Networking/NetworkLifecycle.cs +++ b/Multiplayer/Components/Networking/NetworkLifecycle.cs @@ -6,8 +6,11 @@ using DV.Utils; using LiteNetLib; using LiteNetLib.Utils; +using Multiplayer.Components.Networking.UI; +using Multiplayer.Networking.Data; using Multiplayer.Networking.Listeners; using Multiplayer.Utils; +using Newtonsoft.Json; using UnityEngine; using UnityEngine.SceneManagement; @@ -19,6 +22,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 +43,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,25 +121,42 @@ 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; - 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 } @@ -206,4 +233,5 @@ public static void CreateLifecycle() gameObject.AddComponent(); DontDestroyOnLoad(gameObject); } + } diff --git a/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs b/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs index 249b47f..03ee184 100644 --- a/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs +++ b/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs @@ -73,6 +73,7 @@ private void Server_TickSet(Trainset set) TrainCar trainCar = set.cars[i]; if (!trainCar.TryNetworked(out NetworkedTrainCar _)) { + Multiplayer.LogDebug(() => $"TrainCar UNKNOWN is not networked! Is active? {trainCar.gameObject.activeInHierarchy}"); Multiplayer.LogDebug(() => $"TrainCar {trainCar.ID} is not networked! Is active? {trainCar.gameObject.activeInHierarchy}"); continue; } diff --git a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs index 436649c..c50a714 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs @@ -20,6 +20,8 @@ public class NetworkedTrainCar : IdMonoBehaviour #region Lookup Cache private static readonly Dictionary trainCarsToNetworkedTrainCars = new(); + private static readonly Dictionary trainCarIdToNetworkedTrainCars = new(); + private static readonly Dictionary trainCarIdToTrainCars = new(); private static readonly Dictionary hoseToCoupler = new(); public static bool Get(ushort netId, out NetworkedTrainCar obj) @@ -45,6 +47,14 @@ public static NetworkedTrainCar GetFromTrainCar(TrainCar trainCar) { return trainCarsToNetworkedTrainCars[trainCar]; } + public static bool GetFromTrainId(string carId, out NetworkedTrainCar networkedTrainCar) + { + return trainCarIdToNetworkedTrainCars.TryGetValue(carId, out networkedTrainCar); + } + public static bool GetTrainCarFromTrainId(string carId, out TrainCar trainCar) + { + return trainCarIdToTrainCars.TryGetValue(carId, out trainCar); + } public static bool TryGetFromTrainCar(TrainCar trainCar, out NetworkedTrainCar networkedTrainCar) { @@ -97,6 +107,8 @@ protected override void Awake() TrainCar = GetComponent(); trainCarsToNetworkedTrainCars[TrainCar] = this; + TrainCar.LogicCarInitialized += OnLogicCarInitialised; + bogie1 = TrainCar.Bogies[0]; bogie2 = TrainCar.Bogies[1]; @@ -157,7 +169,14 @@ private void OnDisable() NetworkLifecycle.Instance.OnTick -= Server_OnTick; if (UnloadWatcher.isUnloading) return; + trainCarsToNetworkedTrainCars.Remove(TrainCar); + if (TrainCar.logicCar != null) + { + trainCarIdToNetworkedTrainCars.Remove(TrainCar.ID); + trainCarIdToTrainCars.Remove(TrainCar.ID); + } + foreach (Coupler coupler in TrainCar.couplers) hoseToCoupler.Remove(coupler.hoseAndCock); brakeSystem.HandbrakePositionChanged -= Common_OnHandbrakePositionChanged; @@ -179,10 +198,27 @@ private void OnDisable() #region Server + private void OnLogicCarInitialised() + { + //Multiplayer.LogWarning("OnLogicCarInitialised"); + if (TrainCar.logicCar != null) + { + trainCarIdToNetworkedTrainCars[TrainCar.ID] = this; + trainCarIdToTrainCars[TrainCar.ID] = TrainCar; + + TrainCar.LogicCarInitialized -= OnLogicCarInitialised; + } + else + { + Multiplayer.LogWarning("OnLogicCarInitialised Car Not Initialised!"); + } + + } private IEnumerator Server_WaitForLogicCar() { while (TrainCar.logicCar == null) yield return null; + TrainCar.logicCar.CargoLoaded += Server_OnCargoLoaded; TrainCar.logicCar.CargoUnloaded += Server_OnCargoUnloaded; NetworkLifecycle.Instance.Server.SendSpawnTrainCar(this); @@ -334,6 +370,8 @@ public void Common_DirtyPorts(string[] portIds) { if (!simulationFlow.TryGetPort(portId, out Port _)) { + + Multiplayer.LogWarning($"Tried to dirty port {portId} on UNKNOWN but it doesn't exist!"); Multiplayer.LogWarning($"Tried to dirty port {portId} on {TrainCar.ID} but it doesn't exist!"); continue; } @@ -351,6 +389,7 @@ public void Common_DirtyFuses(string[] fuseIds) { if (!simulationFlow.TryGetFuse(fuseId, out Fuse _)) { + Multiplayer.LogWarning($"Tried to dirty port {fuseId} on UNKOWN but it doesn't exist!"); Multiplayer.LogWarning($"Tried to dirty port {fuseId} on {TrainCar.ID} but it doesn't exist!"); continue; } @@ -462,6 +501,7 @@ private IEnumerator Client_InitLater() yield return null; while ((client_bogie2Queue = bogie2.GetComponent()) == null) yield return null; + client_Initialized = true; } diff --git a/Multiplayer/Components/Networking/UI/ChatGUI.cs b/Multiplayer/Components/Networking/UI/ChatGUI.cs new file mode 100644 index 0000000..a5675d1 --- /dev/null +++ b/Multiplayer/Components/Networking/UI/ChatGUI.cs @@ -0,0 +1,677 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using DV; +using DV.UI; +using Multiplayer.Utils; +using TMPro; +using UnityEngine; +using UnityEngine.UI; +using System.Text.RegularExpressions; +using DV.Common; +using System.Collections; +using Multiplayer.Networking.Managers.Server; +using Multiplayer.Components.Networking.Player; +using static System.Net.Mime.MediaTypeNames; + + +namespace Multiplayer.Components.Networking.UI; + +//[RequireComponent(typeof(Canvas))] +//[RequireComponent(typeof(CanvasScaler))] +[RequireComponent(typeof(RectTransform))] +public class ChatGUI : MonoBehaviour +{ + 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 int SEND_MAX_HISTORY = 10; //How many previous messages to remember + + private GameObject messagePrefab; + + private List messageList = new List(); + private List sendHistory = new List(); + + private TMP_InputField chatInputIF; + private ScrollRect scrollRect; + private RectTransform chatPanel; + private CanvasGroup canvasGroup; + + private GameObject panelGO; + private GameObject textInputGO; + private GameObject scrollViewGO; + + private bool isOpen = false; + private bool showingMessage = false; + + private int sendHistoryIndex = -1; + private bool whispering = false; + private string lastRecipient; + + //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() + { + Multiplayer.Log("ChatGUI Awake() called"); + + SetupOverlay(); //sizes and positions panel + + 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) + { + Multiplayer.Log("Failed to find CustomFirstPersonController"); + return; + } + + hotbarController = GameObject.FindObjectOfType(); + if (hotbarController == null) + { + Multiplayer.Log("Failed to find HotbarController"); + return; + } + */ + + } + + private void OnEnable() + { + chatInputIF.onSubmit.AddListener(Submit); + chatInputIF.onValueChanged.AddListener(ChatInputChange); + + } + + private void OnDisable() + { + chatInputIF.onSubmit.RemoveAllListeners(); + chatInputIF.onValueChanged.RemoveAllListeners(); + } + + private void Update() + { + //Handle keypresses to open/close the chat window + if (!isOpen && Input.GetKeyDown(KeyCode.Return) && !AppUtil.Instance.IsPauseMenuOpen) + { + isOpen = true; //whole panel is open + showingMessage = false; //We don't want to time out + + ShowPanel(); + textInputGO.SetActive(isOpen); + + sendHistoryIndex = sendHistory.Count; + + if (whispering) + { + chatInputIF.text = "/w " + lastRecipient + ' '; + chatInputIF.caretPosition = chatInputIF.text.Length; + } + + BlockInput(true); + } + else if (isOpen) + { + //Check for closing window + if (Input.GetKeyDown(KeyCode.Escape) || Input.GetKeyDown(KeyCode.Return)) + { + isOpen = false; + if (!showingMessage) + { + textInputGO.SetActive(isOpen); + 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; + } + } + } + + //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 && !textInputGO.activeSelf) + { + timeOut += Time.deltaTime; + + if (timeOut >= MESSAGE_TIMEOUT) + { + showingMessage = false ; + //panelGO.SetActive(false); + HidePanel(); + } + } + + ////testTimeOut += Time.deltaTime; + //if (testTimeOut >= 60) + //{ + // testTimeOut = 0; + // ReceiveMessage("Morm: Test TimeOut"); + //} + } + + public void Submit(string text) + { + 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, out string recipient)) + { + whispering = true; + lastRecipient = recipient; + + if (localMessage == null || localMessage == string.Empty) + return; + + if (lastRecipient.Contains(" ")) + { + lastRecipient = '"' + lastRecipient + '"'; + } + + AddMessage("You (" + recipient + "): " + localMessage + ""); + } + else + { + whispering = false; + AddMessage("You: " + text + ""); + } + + //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; + showingMessage = true; + } + + chatInputIF.text = ""; + + textInputGO.SetActive(false); + BlockInput(false); + + 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("/") && message.Length > (ChatManager.COMMAND_WHISPER_SHORT.Length + 2)) + { + Multiplayer.Log("CheckForWhisper() starts with /"); + 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("\"")) + { + Multiplayer.Log("CheckForWhisper() starts with \""); + int endQuote = localMessage.Substring(1).IndexOf('"'); + Multiplayer.Log($"CheckForWhisper() starts with \" - indexOf, eQ: {endQuote}"); + if (endQuote <=1) + { + 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 + { + Multiplayer.Log("CheckForWhisper() no quote"); + */ + recipient = localMessage.Split(' ')[0]; + if (localMessage.Length > (recipient.Length + 2)) + { + localMessage = localMessage.Substring(recipient.Length + 1); + } + else + { + localMessage = ""; + } + //} + + return true; + } + + localMessage = message; + return false; + } + + public void ReceiveMessage(string message) + { + + if (message.Trim().Length > 0) + { + //add locally + AddMessage(message); + } + + timeOut = 0; + showingMessage = true; + + ShowPanel(); + //panelGO.SetActive(true); + } + + private void AddMessage(string text) + { + if (messageList.Count >= MESSAGE_MAX_HISTORY) + { + GameObject.Destroy(messageList[0]); + messageList.RemoveAt(0); + } + + 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 + 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); + + canvasGroup = panelGO.AddComponent(); // Add CanvasGroup for fade effect + } + + private void BuildUI() + { + GameObject scrollViewPrefab = null; + GameObject inputPrefab; + + //get prefabs + PopupNotificationReferences popup = GameObject.FindObjectOfType(); + SaveLoadController saveLoad = GameObject.FindObjectOfType(); + + if (popup == null) + { + Multiplayer.Log("Could not find PopupNotificationReferences"); + return; + } + else + { + inputPrefab = popup.popupTextInput.FindChildByName("TextFieldTextIcon"); + } + + if (saveLoad == null) + { + Multiplayer.Log("Could not find SaveLoadController, attempting to instanciate"); + AppUtil.Instance.PauseGame(); + + Multiplayer.Log("Paused"); + + saveLoad = FindObjectOfType().saveLoadController; + + if (saveLoad == null) + { + Multiplayer.Log("Failed to get SaveLoadController"); + } + else + { + Multiplayer.Log("Made a SaveLoadController!"); + scrollViewPrefab = saveLoad.FindChildByName("Scroll View"); + + if (scrollViewPrefab == null) + { + Multiplayer.Log("Could not find scrollViewPrefab"); + + } + else + { + scrollViewPrefab = Instantiate(scrollViewPrefab); + } + } + + AppUtil.Instance.UnpauseGame(); + } + else + { + scrollViewPrefab = saveLoad.FindChildByName("Scroll View"); + } + + + if (inputPrefab == null) + { + Multiplayer.Log("Could not find inputPrefab"); + return; + } + if (scrollViewPrefab == null) + { + Multiplayer.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(); + chatInputIF.onFocusSelectAll = false; + 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; + + + + //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 = 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(); + scrollBarRT.SetInsetAndSizeFromParentEdge(RectTransform.Edge.Top, 0, scrollViewRT.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!"; + } + + private void BlockInput(bool 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/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 { diff --git a/Multiplayer/Components/Networking/World/NetworkedStation.cs b/Multiplayer/Components/Networking/World/NetworkedStation.cs new file mode 100644 index 0000000..141dd5b --- /dev/null +++ b/Multiplayer/Components/Networking/World/NetworkedStation.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using DV.Logic.Job; +using Multiplayer.Components.Networking.Train; +using UnityEngine; +using static DV.Common.GameFeatureFlags; +using static DV.UI.ATutorialsMenuProvider; + +namespace Multiplayer.Components.Networking.World; + +public class NetworkedStation : MonoBehaviour +{ + private StationController stationController; + + private void Awake() + { + Multiplayer.Log("NetworkedStation.Awake()"); + + stationController = GetComponent(); + StartCoroutine(WaitForLogicStation()); + } + + private IEnumerator WaitForLogicStation() + { + while (stationController.logicStation == null) + yield return null; + + StationComponentLookup.Instance.RegisterStation(stationController); + + Multiplayer.Log("NetworkedStation.Awake() done"); + } + + public static IEnumerator UpdateCarPlates(List tasks, string jobId) + { + + List cars = new List(); + UpdateCarPlatesRecursive(tasks, jobId, ref cars); + + + if (cars != null) + { + Multiplayer.Log("NetworkedStation.UpdateCarPlates() Cars count: " + cars.Count); + + foreach (Car car in cars) + { + Multiplayer.Log("NetworkedStation.UpdateCarPlates() Car: " + car.ID); + + TrainCar trainCar = null; + int loopCtr = 0; + while (!NetworkedTrainCar.GetTrainCarFromTrainId(car.ID, out trainCar)) + { + loopCtr++; + if (loopCtr > 5000) + { + Multiplayer.Log("NetworkedStation.UpdateCarPlates() TimeOut"); + break; + } + + + yield return null; + } + + trainCar?.UpdateJobIdOnCarPlates(jobId); + } + } + } + private static void UpdateCarPlatesRecursive(List tasks, string jobId, ref List cars) + { + Multiplayer.Log("NetworkedStation.UpdateCarPlatesRecursive() Starting"); + + foreach (Task task in tasks) + { + if (task is WarehouseTask) + { + Multiplayer.Log("NetworkedStation.UpdateCarPlatesRecursive() WarehouseTask"); + cars = cars.Union(((WarehouseTask)task).cars).ToList(); + } + else if (task is TransportTask) + { + Multiplayer.Log("NetworkedStation.UpdateCarPlatesRecursive() TransportTask"); + cars = cars.Union(((TransportTask)task).cars).ToList(); + } + else if (task is SequentialTasks) + { + Multiplayer.Log("NetworkedStation.UpdateCarPlatesRecursive() SequentialTasks"); + List seqTask = new(); + + for (LinkedListNode node = ((SequentialTasks)task).tasks.First; node != null; node = node.Next) + { + Multiplayer.Log($"NetworkedStation.UpdateCarPlatesRecursive() SequentialTask Adding node"); + seqTask.Add(node.Value); + } + + Multiplayer.Log($"NetworkedStation.UpdateCarPlatesRecursive() SequentialTask Node Count:{seqTask.Count}"); + + Multiplayer.Log("NetworkedStation.UpdateCarPlatesRecursive() Calling UpdateCarPlates()"); + //drill down + UpdateCarPlatesRecursive(seqTask, jobId, ref cars); + Multiplayer.Log($"NetworkedStation.UpdateCarPlatesRecursive() SequentialTask RETURNED"); + } + else if (task is ParallelTasks) + { + //not implemented + Multiplayer.Log("NetworkedStation.UpdateCarPlatesRecursive() ParallelTasks"); + + Multiplayer.Log("NetworkedStation.UpdateCarPlatesRecursive() Calling UpdateCarPlates()"); + //drill down + UpdateCarPlatesRecursive(((ParallelTasks)task).tasks, jobId, ref cars); + } + else + { + throw new ArgumentException("NetworkedStation.UpdateCarPlatesRecursive() Unknown task type: " + task.GetType()); + } + } + + Multiplayer.Log("NetworkedStation.UpdateCarPlatesRecursive() Returning"); + } +} diff --git a/Multiplayer/Components/SaveGame/StartGameData_ServerSave.cs b/Multiplayer/Components/SaveGame/StartGameData_ServerSave.cs index c764c93..f9bf95c 100644 --- a/Multiplayer/Components/SaveGame/StartGameData_ServerSave.cs +++ b/Multiplayer/Components/SaveGame/StartGameData_ServerSave.cs @@ -93,3 +93,4 @@ public override bool ShouldCreateSaveGameAfterLoad() public override void MakeCurrent(){} } + diff --git a/Multiplayer/Components/StationComponentLookup.cs b/Multiplayer/Components/StationComponentLookup.cs new file mode 100644 index 0000000..3f30f62 --- /dev/null +++ b/Multiplayer/Components/StationComponentLookup.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using DV.Logic.Job; +using DV.Utils; +using JetBrains.Annotations; +using Multiplayer.Components.Networking.World; + +namespace Multiplayer.Components; + +public class StationComponentLookup : SingletonBehaviour +{ + private readonly Dictionary stationToNetworkedStationController = new(); + private readonly Dictionary stationIdToNetworkedStation = new(); + private readonly Dictionary stationIdToStationController = new(); + + public void RegisterStation(StationController stationController) + { + var networkedStation = stationController.GetComponent(); + stationToNetworkedStationController[stationController.logicStation] = networkedStation; + stationIdToNetworkedStation[stationController.logicStation.ID] = networkedStation; + stationIdToStationController[stationController.logicStation.ID] = stationController; + } + + public void UnregisterStation(StationController stationController) + { + stationToNetworkedStationController.Remove(stationController.logicStation); + stationIdToNetworkedStation.Remove(stationController.logicStation.ID); + stationIdToStationController.Remove(stationController.logicStation.ID); + } + + public bool NetworkedStationFromStation(Station station, out NetworkedStation networkedStation) + { + return stationToNetworkedStationController.TryGetValue(station, out networkedStation); + } + + public bool NetworkedStationFromId(string stationId, out NetworkedStation networkedStation) + { + return stationIdToNetworkedStation.TryGetValue(stationId, out networkedStation); + } + + public bool StationControllerFromId(string stationId, out StationController stationController) + { + return stationIdToStationController.TryGetValue(stationId, out stationController); + } + + [UsedImplicitly] + public new static string AllowAutoCreate() + { + return $"[{nameof(StationComponentLookup)}]"; + } +} diff --git a/Multiplayer/Locale.cs b/Multiplayer/Locale.cs index af4998e..dbfd637 100644 --- a/Multiplayer/Locale.cs +++ b/Multiplayer/Locale.cs @@ -5,152 +5,187 @@ using I2.Loc; using Multiplayer.Utils; -namespace Multiplayer; - -public static class Locale +namespace Multiplayer { - 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 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 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__DIRECT => Get(SERVER_BROWSER__DIRECT_KEY); - public const string SERVER_BROWSER__DIRECT_KEY = $"{PREFIX_SERVER_BROWSER}/direct"; - - 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"; - - #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"; - 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"; - - #endregion - - #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 - - #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 - - #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) + public static class Locale { - initializeAttempted = true; - string path = Path.Combine(localeDir, DEFAULT_LOCALE_FILE); - if (!File.Exists(path)) + 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 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"; + 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 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 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"; + 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 + 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"; + + 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 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"; + #endregion + + #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 + + #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 + + #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) { - 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..b54272e 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; @@ -130,7 +130,7 @@ public static void LogException(object msg, Exception e) private static void WriteLog(string msg) { - string str = $"[{DateTime.Now:HH:mm:ss.fff}] {msg}"; + string str = $"[{DateTime.Now.ToUniversalTime():HH:mm:ss.fff}] {msg}"; if (Settings.EnableLogFile) File.AppendAllLines(LOG_FILE, new[] { str }); ModEntry.Logger.Log(str); diff --git a/Multiplayer/Multiplayer.csproj b/Multiplayer/Multiplayer.csproj index 70448c4..a10601f 100644 --- a/Multiplayer/Multiplayer.csproj +++ b/Multiplayer/Multiplayer.csproj @@ -1,4 +1,4 @@ - + net48 latest @@ -77,9 +77,10 @@ - - - + + + + diff --git a/Multiplayer/Networking/Data/JobData.cs b/Multiplayer/Networking/Data/JobData.cs new file mode 100644 index 0000000..799199d --- /dev/null +++ b/Multiplayer/Networking/Data/JobData.cs @@ -0,0 +1,139 @@ +using System.Linq; +using DV.Logic.Job; +using LiteNetLib.Utils; +using Newtonsoft.Json; + +namespace Multiplayer.Networking.Data; + +public class JobData +{ + public byte JobType { get; set; } + public string ID { get; set; } + public TaskBeforeDataData[] Tasks { get; set; } + public StationsChainDataData ChainData { get; set; } + public int RequiredLicenses { get; set; } + public float StartTime { get; set; } + public float FinishTime { get; set; } + public float InitialWage { get; set; } + public byte State { get; set; } + public float TimeLimit { get; set; } + + public static JobData FromJob(Job job) + { + return new JobData + { + JobType = (byte)job.jobType, + ID = job.ID, + Tasks = job.tasks.Select(x => TaskBeforeDataData.FromTask(x)).ToArray(), + ChainData = StationsChainDataData.FromStationData(job.chainData), + RequiredLicenses = (int)job.requiredLicenses, + StartTime = job.startTime, + FinishTime = job.finishTime, + InitialWage = job.initialWage, + State = (byte)job.State, + TimeLimit = job.TimeLimit + }; + } + + public static void Serialize(NetDataWriter writer, JobData data) + { + writer.Put(data.JobType); + writer.Put(data.ID); + writer.Put((byte)data.Tasks.Length); + foreach (var taskBeforeDataData in data.Tasks) + TaskBeforeDataData.SerializeTask(taskBeforeDataData, writer); + StationsChainDataData.Serialize(writer, data.ChainData); + writer.Put(data.RequiredLicenses); + writer.Put(data.StartTime); + writer.Put(data.FinishTime); + writer.Put(data.InitialWage); + writer.Put(data.State); + writer.Put(data.TimeLimit); + Multiplayer.Log(JsonConvert.SerializeObject(data, Formatting.None)); + } + + public static JobData Deserialize(NetDataReader reader) + { + Multiplayer.Log("JobData.Deserialize()"); + var jobType = reader.GetByte(); + Multiplayer.Log("JobData.Deserialize() jobType: " + jobType); + var id = reader.GetString(); + Multiplayer.Log("JobData.Deserialize() id: " + id); + var tasksLength = reader.GetByte(); + Multiplayer.Log("JobData.Deserialize() tasksLength: " + tasksLength); + var tasks = new TaskBeforeDataData[tasksLength]; + for (int i = 0; i < tasksLength; i++) + tasks[i] = TaskBeforeDataData.DeserializeTask(reader); + //Multiplayer.Log("JobData.Deserialize() tasks: " + JsonConvert.SerializeObject(tasks, Formatting.None)); + var chainData = StationsChainDataData.Deserialize(reader); + //Multiplayer.Log("JobData.Deserialize() chainData: " + JsonConvert.SerializeObject(chainData, Formatting.Indented)); + var requiredLicenses = reader.GetInt(); + Multiplayer.Log("JobData.Deserialize() requiredLicenses: " + requiredLicenses); + var startTime = reader.GetFloat(); + Multiplayer.Log("JobData.Deserialize() startTime: " + startTime); + var finishTime = reader.GetFloat(); + Multiplayer.Log("JobData.Deserialize() finishTime: " + finishTime); + var initialWage = reader.GetFloat(); + Multiplayer.Log("JobData.Deserialize() initialWage: " + initialWage); + var state = reader.GetByte(); + Multiplayer.Log("JobData.Deserialize() state: " + state); + var timeLimit = reader.GetFloat(); + Multiplayer.Log(JsonConvert.SerializeObject(new JobData + { + JobType = jobType, + ID = id, + Tasks = tasks, + ChainData = chainData, + RequiredLicenses = requiredLicenses, + StartTime = startTime, + FinishTime = finishTime, + InitialWage = initialWage, + State = state, + TimeLimit = timeLimit + }, Formatting.None)); + + return new JobData + { + JobType = jobType, + ID = id, + Tasks = tasks, + ChainData = chainData, + RequiredLicenses = requiredLicenses, + StartTime = startTime, + FinishTime = finishTime, + InitialWage = initialWage, + State = state, + TimeLimit = timeLimit + }; + } +} + +public struct StationsChainDataData +{ + public string ChainOriginYardId { get; set; } + public string ChainDestinationYardId { get; set; } + + public static StationsChainDataData FromStationData(StationsChainData data) + { + return new StationsChainDataData + { + ChainOriginYardId = data.chainOriginYardId, + ChainDestinationYardId = data.chainDestinationYardId + }; + } + + public static void Serialize(NetDataWriter writer, StationsChainDataData data) + { + writer.Put(data.ChainOriginYardId); + writer.Put(data.ChainDestinationYardId); + } + + public static StationsChainDataData Deserialize(NetDataReader reader) + { + return new StationsChainDataData + { + ChainOriginYardId = reader.GetString(), + ChainDestinationYardId = reader.GetString() + }; + } +} 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/ModInfo.cs b/Multiplayer/Networking/Data/ModInfo.cs index 323884e..451bbdb 100644 --- a/Multiplayer/Networking/Data/ModInfo.cs +++ b/Multiplayer/Networking/Data/ModInfo.cs @@ -11,8 +11,8 @@ public readonly struct ModInfo { public readonly string Id; public readonly string Version; - - private ModInfo(string id, string version) + + public ModInfo(string id, string version) { Id = id; Version = version; 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/Data/TaskDataData.cs b/Multiplayer/Networking/Data/TaskDataData.cs new file mode 100644 index 0000000..eb1be23 --- /dev/null +++ b/Multiplayer/Networking/Data/TaskDataData.cs @@ -0,0 +1,371 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using DV.Logic.Job; +using DV.ThingTypes; +using HarmonyLib; +using LiteNetLib.Utils; +using Newtonsoft.Json; + +namespace Multiplayer.Networking.Data; + +public abstract class TaskBeforeDataData +{ + public byte State { get; set; } + public float TaskStartTime { get; set; } + public float TaskFinishTime { get; set; } + public bool IsLastTask { get; set; } + public float TimeLimit { get; set; } + public byte TaskType { get; set; } + + + public static TaskBeforeDataData FromTask(Task task) + { + TaskBeforeDataData taskData = task switch + { + WarehouseTask warehouseTask => WarehouseTaskData.FromWarehouseTask(warehouseTask), + TransportTask transportTask => TransportTaskData.FromTransportTask(transportTask), + SequentialTasks sequentialTasks => SequentialTasksData.FromSequentialTask(sequentialTasks), + ParallelTasks parallelTasks => ParallelTasksData.FromParallelTask(parallelTasks), + _ => throw new ArgumentException("Unknown task type: " + task.GetType()) + }; + + taskData.State = (byte)task.state; + taskData.TaskStartTime = task.taskStartTime; + taskData.TaskFinishTime = task.taskFinishTime; + taskData.IsLastTask = task.IsLastTask; + taskData.TimeLimit = task.TimeLimit; + taskData.TaskType = (byte)task.InstanceTaskType; + + return taskData; + } + + public static Task ToTask(object data) + { + if (data is WarehouseTaskData) + { + var task = (WarehouseTaskData)data; + return WarehouseTaskData.ToWarehouseTask(task); + } + + if (data is TransportTaskData) + { + var task = (TransportTaskData)data; + return TransportTaskData.ToTransportTask(task); + } + + if (data is SequentialTasksData) + { + var task = (SequentialTasksData)data; + List tasks = new List(); + + foreach (TaskBeforeDataData taskBeforeDataData in task.Tasks) + tasks.Add(ToTask(taskBeforeDataData)); + + + return new SequentialTasks(tasks); + } + + if (data is ParallelTasksData) + { + var task = (ParallelTasksData)data; + List tasks = new List(); + + foreach (TaskBeforeDataData taskBeforeDataData in task.Tasks) + tasks.Add(ToTask(taskBeforeDataData)); + + + return new ParallelTasks(tasks); + } + + throw new ArgumentException("Unknown task type: " + data.GetType()); + } + + public static void SerializeTask(object data, NetDataWriter writer) + { + if (data is WarehouseTaskData) + { + var task = (WarehouseTaskData)data; + WarehouseTaskData.Serialize(writer, task); + return; + } + + if (data is TransportTaskData) + { + var task = (TransportTaskData)data; + TransportTaskData.Serialize(writer, task); + return; + } + + if (data is SequentialTasksData) + { + var task = (SequentialTasksData)data; + + SequentialTasksData.Serialize(writer, task); + + return; + } + + if (data is ParallelTasksData) + { + var task = (ParallelTasksData)data; + + ParallelTasksData.Serialize(writer, task); + + + return; + } + + throw new ArgumentException("Unknown task type: " + data.GetType()); + } + + public static TaskBeforeDataData DeserializeTask(NetDataReader reader) + { + TaskType taskType = (TaskType)reader.GetByte(); + Multiplayer.Log("Task type: " + taskType + ""); + + return taskType switch + { + DV.Logic.Job.TaskType.Warehouse => WarehouseTaskData.Deserialize(reader), + DV.Logic.Job.TaskType.Transport => TransportTaskData.Deserialize(reader), + DV.Logic.Job.TaskType.Sequential => SequentialTasksData.Deserialize(reader), + DV.Logic.Job.TaskType.Parallel => ParallelTasksData.Deserialize(reader), + _ => throw new ArgumentException("Unknown task type: " + taskType) + }; + } + + public static void Serialize(NetDataWriter writer, TaskBeforeDataData data) + { + writer.Put(data.TaskType); + writer.Put(data.State); + writer.Put(data.TaskStartTime); + writer.Put(data.TaskFinishTime); + writer.Put(data.IsLastTask); + writer.Put(data.TimeLimit); + writer.Put(data.TaskType); + } + + public static void Deserialize(NetDataReader reader, TaskBeforeDataData data) + { + data.State = reader.GetByte(); + data.TaskStartTime = reader.GetFloat(); + data.TaskFinishTime = reader.GetFloat(); + data.IsLastTask = reader.GetBool(); + data.TimeLimit = reader.GetFloat(); + data.TaskType = reader.GetByte(); + } +} + +public class ParallelTasksData : TaskBeforeDataData +{ + public TaskBeforeDataData[] Tasks { get; set; } + + public static ParallelTasksData FromParallelTask(ParallelTasks task) + { + return new ParallelTasksData + { + Tasks = task.tasks.Select(x => FromTask(x)).ToArray() + }; + } + + public static void Serialize(NetDataWriter writer, ParallelTasksData data) + { + TaskBeforeDataData.Serialize(writer, data); + writer.Put((byte)data.Tasks.Length); + foreach (var taskBeforeDataData in data.Tasks) + SerializeTask(taskBeforeDataData, writer); + } + + public static ParallelTasksData Deserialize(NetDataReader reader) + { + var parallelTask = new ParallelTasksData(); + Deserialize(reader, parallelTask); + var tasksLength = reader.GetByte(); + var tasks = new TaskBeforeDataData[tasksLength]; + for (int i = 0; i < tasksLength; i++) + tasks[i] = DeserializeTask(reader); + parallelTask.Tasks = tasks; + return parallelTask; + } +} + +public class SequentialTasksData : TaskBeforeDataData +{ + public TaskBeforeDataData[] Tasks { get; set; } + + + public static SequentialTasksData FromSequentialTask(SequentialTasks task) + { + return new SequentialTasksData + { + Tasks = task.tasks.Select(x => FromTask(x)).ToArray(), + }; + } + + public static void Serialize(NetDataWriter writer, SequentialTasksData data) + { + TaskBeforeDataData.Serialize(writer, data); + writer.Put((byte)data.Tasks.Length); + foreach (var taskBeforeDataData in data.Tasks) + SerializeTask(taskBeforeDataData, writer); + } + + public static SequentialTasksData Deserialize(NetDataReader reader) + { + var sequentialTask = new SequentialTasksData(); + Deserialize(reader, sequentialTask); + var tasksLength = reader.GetByte(); + var tasks = new TaskBeforeDataData[tasksLength]; + for (int i = 0; i < tasksLength; i++) + tasks[i] = DeserializeTask(reader); + sequentialTask.Tasks = tasks; + return sequentialTask; + } +} + +public class WarehouseTaskData : TaskBeforeDataData +{ + public string[] Cars { get; set; } + public byte WarehouseTaskType { get; set; } + public string WarehouseMachine { get; set; } + public CargoType CargoType { get; set; } + public float CargoAmount { get; set; } + public bool ReadyForMachine { get; set; } + + public static WarehouseTaskData FromWarehouseTask(WarehouseTask task) + { + return new WarehouseTaskData + { + Cars = task.cars.Select(x => x.ID).ToArray(), + WarehouseTaskType = (byte)task.warehouseTaskType, + WarehouseMachine = task.warehouseMachine.ID, + CargoType = task.cargoType, + CargoAmount = task.cargoAmount, + ReadyForMachine = task.readyForMachine + }; + } + + public static WarehouseTask ToWarehouseTask(WarehouseTaskData data) + { + return new WarehouseTask( + CarSpawner.Instance.allCars.FindAll(x => data.Cars.Contains(x.ID)).Select(x => x.logicCar).ToList(), + (WarehouseTaskType)data.WarehouseTaskType, + JobSaveManager.Instance.GetWarehouseMachineWithId(data.WarehouseMachine), + (CargoType)data.CargoType, + data.CargoAmount + ); + } + + public static void Serialize(NetDataWriter writer, WarehouseTaskData data) + { + TaskBeforeDataData.Serialize(writer, data); + writer.PutArray(data.Cars); + writer.Put(data.WarehouseTaskType); + writer.Put(data.WarehouseMachine); + writer.Put((int)data.CargoType); + writer.Put(data.CargoAmount); + writer.Put(data.ReadyForMachine); + } + + public static WarehouseTaskData Deserialize(NetDataReader reader) + { + WarehouseTaskData data = new WarehouseTaskData(); + Deserialize(reader, data); + data.Cars = reader.GetStringArray(); + data.WarehouseTaskType = reader.GetByte(); + data.WarehouseMachine = reader.GetString(); + data.CargoType = (CargoType)reader.GetInt(); + data.CargoAmount = reader.GetFloat(); + data.ReadyForMachine = reader.GetBool(); + + return data; + } +} + +public class TransportTaskData : TaskBeforeDataData +{ + public string[] Cars { get; set; } + public string StartingTrack { get; set; } + public string DestinationTrack { get; set; } + public CargoType[] TransportedCargoPerCar { get; set; } + public bool CouplingRequiredAndNotDone { get; set; } + public bool AnyHandbrakeRequiredAndNotDone { get; set; } + + public static TransportTaskData FromTransportTask(TransportTask task) + { + Multiplayer.Log("Cars: " + task.cars.Select(x => x.ID).ToArray().Join()); + Multiplayer.Log("FromTransportTask.TransportedCargoPerCar: " + task.transportedCargoPerCar?.Select(x => (int)x).ToArray().Join() + "\r\n\t"+ task.transportedCargoPerCar?.ToArray().Join()); + + return new TransportTaskData + { + Cars = task.cars.Select(x => x.ID).ToArray(), + StartingTrack = task.startingTrack.ID.RailTrackGameObjectID, + DestinationTrack = task.destinationTrack.ID.RailTrackGameObjectID, + TransportedCargoPerCar = task.transportedCargoPerCar?.ToArray(), + CouplingRequiredAndNotDone = task.couplingRequiredAndNotDone, + AnyHandbrakeRequiredAndNotDone = task.anyHandbrakeRequiredAndNotDone + }; + } + + public static TransportTask ToTransportTask(TransportTaskData data) + { + return new TransportTask( + CarSpawner.Instance.allCars.FindAll(x => data.Cars.Contains(x.ID)).Select(x => x.logicCar).ToList(), + RailTrackRegistry.Instance.GetTrackWithName(data.DestinationTrack).logicTrack, + RailTrackRegistry.Instance.GetTrackWithName(data.StartingTrack).logicTrack, + data.TransportedCargoPerCar?.ToList() + ); + } + + public static void Serialize(NetDataWriter writer, TransportTaskData data) + { + TaskBeforeDataData.Serialize(writer, data); + writer.PutArray(data.Cars); + writer.Put(data.StartingTrack); + writer.Put(data.DestinationTrack); + + //transport cargo data exists? + writer.Put(data.TransportedCargoPerCar != null); + + //write data if it exists + if (data.TransportedCargoPerCar != null) + { + writer.PutArray(data.TransportedCargoPerCar?.Select(x => (int)x).ToArray()); + // transportedCargoPerCar?.Select(x => (int)x).ToArray() + Multiplayer.Log("Serialising cargo: " + (int)data.TransportedCargoPerCar[0]); + } + + writer.Put(data.CouplingRequiredAndNotDone); + writer.Put(data.AnyHandbrakeRequiredAndNotDone); + } + + public static TransportTaskData Deserialize(NetDataReader reader) + { + Multiplayer.Log("TransportTaskData.Deserialize"); + TransportTaskData data = new TransportTaskData(); + Multiplayer.Log("1"); + Deserialize(reader, data); + Multiplayer.Log("2"); + data.Cars = reader.GetStringArray(); + Multiplayer.Log("3"); + data.StartingTrack = reader.GetString(); + Multiplayer.Log("4"); + data.DestinationTrack = reader.GetString(); + Multiplayer.Log("5"); + + if (reader.GetBool()) + { + //transport data exists + data.TransportedCargoPerCar = reader.GetArray(sizeof(int))?.Select(x => (CargoType)x).ToArray(); + } + + Multiplayer.Log("TransportedCargoPerCar: " + data.TransportedCargoPerCar?.Select(x => (int)x).ToArray().Join() + "\r\n\t" + data.TransportedCargoPerCar?.ToArray().Join()); + Multiplayer.Log("6"); + data.CouplingRequiredAndNotDone = reader.GetBool(); + Multiplayer.Log("7"); + data.AnyHandbrakeRequiredAndNotDone = reader.GetBool(); + //Multiplayer.Log(JsonConvert.SerializeObject(data, Formatting.Indented)); + + return data; + } +} diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index b44d387..cfdb9b1 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -1,4 +1,7 @@ using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography; using System.Text; using DV; using DV.Damage; @@ -11,13 +14,18 @@ using DV.UIFramework; using DV.WeatherSystem; using LiteNetLib; +using Multiplayer.Components; using Multiplayer.Components.MainMenu; using Multiplayer.Components.Networking; +using Multiplayer.Components.Networking.Jobs; +using Multiplayer.Components.Networking.Player; using Multiplayer.Components.Networking.Train; +using Multiplayer.Components.Networking.UI; using Multiplayer.Components.Networking.World; using Multiplayer.Components.SaveGame; using Multiplayer.Networking.Data; using Multiplayer.Networking.Packets.Clientbound; +using Multiplayer.Networking.Packets.Clientbound.Jobs; using Multiplayer.Networking.Packets.Clientbound.SaveGame; using Multiplayer.Networking.Packets.Clientbound.Train; using Multiplayer.Networking.Packets.Clientbound.World; @@ -44,15 +52,19 @@ 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() { + ServerboundClientLoginPacket serverboundClientLoginPacket = new() + { Username = Multiplayer.Settings.Username, Guid = Multiplayer.Settings.GetGuid().ToByteArray(), Password = password, @@ -106,6 +118,10 @@ protected override void Subscribe() netPacketProcessor.SubscribeReusable(OnClientboundLicenseAcquiredPacket); netPacketProcessor.SubscribeReusable(OnClientboundGarageUnlockPacket); netPacketProcessor.SubscribeReusable(OnClientboundDebtStatusPacket); + netPacketProcessor.SubscribeReusable(OnClientboundJobsPacket); + netPacketProcessor.SubscribeReusable(OnClientboundJobCreatePacket); + netPacketProcessor.SubscribeReusable(OnClientboundJobTakeResponsePacket); + netPacketProcessor.SubscribeReusable(OnCommonChatPacket); } #region Net Events @@ -308,6 +324,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); + chatGUI = chat.GetComponent(); + } } private void OnClientboundTimeAdvancePacket(ClientboundTimeAdvancePacket packet) @@ -592,6 +618,120 @@ private void OnClientboundDebtStatusPacket(ClientboundDebtStatusPacket packet) { CareerManagerDebtControllerPatch.HasDebt = packet.HasDebt; } + private void OnCommonChatPacket(CommonChatPacket packet) + { + + chatGUI.ReceiveMessage(packet.message); + } + + private void OnClientboundJobCreatePacket(ClientboundJobCreatePacket packet) + { + if (NetworkLifecycle.Instance.IsHost()) + return; + + List tasks = new List(); + foreach (TaskBeforeDataData taskBeforeDataData in packet.job.Tasks) + tasks.Add(TaskBeforeDataData.ToTask(taskBeforeDataData)); + + StationsChainDataData chainData = packet.job.ChainData; + //packet.job.JobType + Job newJob = new Job( + tasks, + (JobType)packet.job.JobType, + packet.job.TimeLimit, + packet.job.InitialWage, + new StationsChainData(chainData.ChainOriginYardId, chainData.ChainDestinationYardId), + packet.job.ID, + (JobLicenses)packet.job.RequiredLicenses + ); + + //NetworkedJob netJob = NetworkedJob.AddJob(packet.stationId, newJob); + //netJob.NetId = packet.netId; + + //Find the station + StationController station; + if(!StationComponentLookup.Instance.StationControllerFromId(packet.stationId, out station)) + { + Multiplayer.LogWarning($"OnClientboundJobCreatePacket Could not get staion for stationId: {packet.stationId}"); + return; + } + + //create a new game object + NetworkedJob netJob = station.gameObject.AddComponent(); + if (netJob != null) + { + netJob.job = newJob; + netJob.stationID = packet.stationId; + netJob.NetId = packet.netId; + } + + } + private void OnClientboundJobsPacket(ClientboundJobsPacket packet) + { + if (NetworkLifecycle.Instance.IsHost()) + return; + + if (!StationComponentLookup.Instance.StationControllerFromId(packet.stationId, out StationController station)) + { + LogError("Received job packet but couldn't find station!"); + return; + } + + Multiplayer.Log($"Received job packet. Job count:{packet.Jobs.Count()}"); + + for (int i=0;i < packet.Jobs.Count(); i++) + { + JobData job = packet.Jobs[i]; + ushort netId = packet.netIds[i]; + + var tasks = new List(); + foreach (TaskBeforeDataData taskBeforeDataData in job.Tasks) + tasks.Add(TaskBeforeDataData.ToTask(taskBeforeDataData)); + + StationsChainDataData chainData = job.ChainData; + + Job newJob = new Job( + tasks, + (JobType)job.JobType, + job.TimeLimit, + job.InitialWage, + new StationsChainData(chainData.ChainOriginYardId, chainData.ChainDestinationYardId), + job.ID, + (JobLicenses)job.RequiredLicenses + ); + + Multiplayer.Log($"Attempting to add Job with ID {newJob.ID} to station.");//\r\nExisting jobs are: {station.logicStation.availableJobs.Select(x=>x.ID + "\r\n\t").ToArray().Join()}\r\nDoes the Job already exist in station? {station.logicStation.availableJobs.Where(x => x.ID == newJob.ID).Count() > 0}"); + + //create a new game object + NetworkedJob netJob = station.gameObject.AddComponent(); + if (netJob != null) + { + netJob.job = newJob; + netJob.stationID = packet.stationId; + netJob.NetId = netId; + } + } + } + + private void OnClientboundJobTakeResponsePacket(ClientboundJobTakeResponsePacket packet) + { + NetworkedJob networkedJob; + + if(!NetworkedJob.Get(packet.netId, out networkedJob)) + return; + + NetworkedPlayer player; + if (PlayerManager.TryGetPlayer(packet.playerId, out player)) + { + networkedJob.takenBy = player.Guid; + } + + Multiplayer.Log($"OnClientboundJobTakeResponsePacket jobId: {networkedJob.job.ID}, Status: {packet.granted}"); + networkedJob.allowTake = packet.granted; + networkedJob.jobValidator.ProcessJobOverview(networkedJob.jobOverview); + networkedJob.jobValidator = null; + networkedJob.jobOverview = null; + } #endregion @@ -615,7 +755,8 @@ private void SendReadyPacket() public void SendPlayerPosition(Vector3 position, Vector3 moveDir, float rotationY, bool isJumping, bool isOnCar, bool reliable) { - SendPacketToServer(new ServerboundPlayerPositionPacket { + SendPacketToServer(new ServerboundPlayerPositionPacket + { Position = position, MoveDir = new Vector2(moveDir.x, moveDir.z), RotationY = rotationY, @@ -625,21 +766,24 @@ public void SendPlayerPosition(Vector3 position, Vector3 moveDir, float rotation public void SendPlayerCar(ushort carId) { - SendPacketToServer(new ServerboundPlayerCarPacket { + SendPacketToServer(new ServerboundPlayerCarPacket + { CarId = carId }, DeliveryMethod.ReliableOrdered); } public void SendTimeAdvance(float amountOfTimeToSkipInSeconds) { - SendPacketToServer(new ServerboundTimeAdvancePacket { + SendPacketToServer(new ServerboundTimeAdvancePacket + { amountOfTimeToSkipInSeconds = amountOfTimeToSkipInSeconds }, DeliveryMethod.ReliableUnordered); } public void SendJunctionSwitched(ushort netId, byte selectedBranch, Junction.SwitchMode mode) { - SendPacketToServer(new CommonChangeJunctionPacket { + SendPacketToServer(new CommonChangeJunctionPacket + { NetId = netId, SelectedBranch = selectedBranch, Mode = (byte)mode @@ -648,7 +792,8 @@ public void SendJunctionSwitched(ushort netId, byte selectedBranch, Junction.Swi public void SendTurntableRotation(byte netId, float rotation) { - SendPacketToServer(new CommonRotateTurntablePacket { + SendPacketToServer(new CommonRotateTurntablePacket + { NetId = netId, rotation = rotation }, DeliveryMethod.ReliableOrdered); @@ -656,7 +801,8 @@ public void SendTurntableRotation(byte netId, float rotation) public void SendTrainCouple(Coupler coupler, Coupler otherCoupler, bool playAudio, bool viaChainInteraction) { - SendPacketToServer(new CommonTrainCouplePacket { + SendPacketToServer(new CommonTrainCouplePacket + { NetId = coupler.train.GetNetId(), IsFrontCoupler = coupler.isFrontCoupler, OtherNetId = otherCoupler.train.GetNetId(), @@ -668,7 +814,8 @@ public void SendTrainCouple(Coupler coupler, Coupler otherCoupler, bool playAudi public void SendTrainUncouple(Coupler coupler, bool playAudio, bool dueToBrokenCouple, bool viaChainInteraction) { - SendPacketToServer(new CommonTrainUncouplePacket { + SendPacketToServer(new CommonTrainUncouplePacket + { NetId = coupler.train.GetNetId(), IsFrontCoupler = coupler.isFrontCoupler, PlayAudio = playAudio, @@ -679,7 +826,8 @@ public void SendTrainUncouple(Coupler coupler, bool playAudio, bool dueToBrokenC public void SendHoseConnected(Coupler coupler, Coupler otherCoupler, bool playAudio) { - SendPacketToServer(new CommonHoseConnectedPacket { + SendPacketToServer(new CommonHoseConnectedPacket + { NetId = coupler.train.GetNetId(), IsFront = coupler.isFrontCoupler, OtherNetId = otherCoupler.train.GetNetId(), @@ -690,7 +838,8 @@ public void SendHoseConnected(Coupler coupler, Coupler otherCoupler, bool playAu public void SendHoseDisconnected(Coupler coupler, bool playAudio) { - SendPacketToServer(new CommonHoseDisconnectedPacket { + SendPacketToServer(new CommonHoseDisconnectedPacket + { NetId = coupler.train.GetNetId(), IsFront = coupler.isFrontCoupler, PlayAudio = playAudio @@ -699,7 +848,8 @@ public void SendHoseDisconnected(Coupler coupler, bool playAudio) public void SendMuConnected(MultipleUnitCable cable, MultipleUnitCable otherCable, bool playAudio) { - SendPacketToServer(new CommonMuConnectedPacket { + SendPacketToServer(new CommonMuConnectedPacket + { NetId = cable.muModule.train.GetNetId(), IsFront = cable.isFront, OtherNetId = otherCable.muModule.train.GetNetId(), @@ -710,7 +860,8 @@ public void SendMuConnected(MultipleUnitCable cable, MultipleUnitCable otherCabl public void SendMuDisconnected(ushort netId, MultipleUnitCable cable, bool playAudio) { - SendPacketToServer(new CommonMuDisconnectedPacket { + SendPacketToServer(new CommonMuDisconnectedPacket + { NetId = netId, IsFront = cable.isFront, PlayAudio = playAudio @@ -719,7 +870,8 @@ public void SendMuDisconnected(ushort netId, MultipleUnitCable cable, bool playA public void SendCockState(ushort netId, Coupler coupler, bool isOpen) { - SendPacketToServer(new CommonCockFiddlePacket { + SendPacketToServer(new CommonCockFiddlePacket + { NetId = netId, IsFront = coupler.isFrontCoupler, IsOpen = isOpen @@ -728,14 +880,16 @@ public void SendCockState(ushort netId, Coupler coupler, bool isOpen) public void SendBrakeCylinderReleased(ushort netId) { - SendPacketToServer(new CommonBrakeCylinderReleasePacket { + SendPacketToServer(new CommonBrakeCylinderReleasePacket + { NetId = netId }, DeliveryMethod.ReliableUnordered); } public void SendHandbrakePositionChanged(ushort netId, float position) { - SendPacketToServer(new CommonHandbrakePositionPacket { + SendPacketToServer(new CommonHandbrakePositionPacket + { NetId = netId, Position = position }, DeliveryMethod.ReliableOrdered); @@ -743,7 +897,8 @@ public void SendHandbrakePositionChanged(ushort netId, float position) public void SendPorts(ushort netId, string[] portIds, float[] portValues) { - SendPacketToServer(new CommonTrainPortsPacket { + SendPacketToServer(new CommonTrainPortsPacket + { NetId = netId, PortIds = portIds, PortValues = portValues @@ -752,7 +907,8 @@ public void SendPorts(ushort netId, string[] portIds, float[] portValues) public void SendFuses(ushort netId, string[] fuseIds, bool[] fuseValues) { - SendPacketToServer(new CommonTrainFusesPacket { + SendPacketToServer(new CommonTrainFusesPacket + { NetId = netId, FuseIds = fuseIds, FuseValues = fuseValues @@ -761,21 +917,24 @@ public void SendFuses(ushort netId, string[] fuseIds, bool[] fuseValues) public void SendTrainSyncRequest(ushort netId) { - SendPacketToServer(new ServerboundTrainSyncRequestPacket { + SendPacketToServer(new ServerboundTrainSyncRequestPacket + { NetId = netId }, DeliveryMethod.ReliableUnordered); } public void SendTrainDeleteRequest(ushort netId) { - SendPacketToServer(new ServerboundTrainDeleteRequestPacket { + SendPacketToServer(new ServerboundTrainDeleteRequestPacket + { NetId = netId }, DeliveryMethod.ReliableUnordered); } public void SendTrainRerailRequest(ushort netId, ushort trackId, Vector3 position, Vector3 forward) { - SendPacketToServer(new ServerboundTrainRerailRequestPacket { + SendPacketToServer(new ServerboundTrainRerailRequestPacket + { NetId = netId, TrackId = trackId, Position = position, @@ -785,11 +944,28 @@ public void SendTrainRerailRequest(ushort netId, ushort trackId, Vector3 positio public void SendLicensePurchaseRequest(string id, bool isJobLicense) { - SendPacketToServer(new ServerboundLicensePurchaseRequestPacket { + SendPacketToServer(new ServerboundLicensePurchaseRequestPacket + { Id = id, IsJobLicense = isJobLicense }, DeliveryMethod.ReliableUnordered); } + public void SendJobTakeRequest(ushort netId) + { + SendPacketToServer(new ServerboundJobTakeRequestPacket + { + netId = netId + }, DeliveryMethod.ReliableUnordered); + } + + + public void SendChat(string message) + { + SendPacketToServer(new CommonChatPacket + { + message = message + }, DeliveryMethod.ReliableUnordered); + } #endregion } diff --git a/Multiplayer/Networking/Managers/NetworkManager.cs b/Multiplayer/Networking/Managers/NetworkManager.cs index 93b5cd8..0a88130 100644 --- a/Multiplayer/Networking/Managers/NetworkManager.cs +++ b/Multiplayer/Networking/Managers/NetworkManager.cs @@ -22,7 +22,8 @@ public abstract class NetworkManager : INetEventListener protected NetworkManager(Settings settings) { - netManager = new NetManager(this) { + netManager = new NetManager(this) + { DisconnectTimeout = 10000 }; netPacketProcessor = new NetPacketProcessor(netManager); @@ -36,8 +37,10 @@ protected NetworkManager(Settings settings) private void RegisterNestedTypes() { netPacketProcessor.RegisterNestedType(BogieData.Serialize, BogieData.Deserialize); + netPacketProcessor.RegisterNestedType(JobData.Serialize, JobData.Deserialize); netPacketProcessor.RegisterNestedType(ModInfo.Serialize, ModInfo.Deserialize); netPacketProcessor.RegisterNestedType(RigidbodySnapshot.Serialize, RigidbodySnapshot.Deserialize); + netPacketProcessor.RegisterNestedType(StationsChainDataData.Serialize, StationsChainDataData.Deserialize); netPacketProcessor.RegisterNestedType(TrainsetMovementPart.Serialize, TrainsetMovementPart.Deserialize); netPacketProcessor.RegisterNestedType(TrainsetSpawnPart.Serialize, TrainsetSpawnPart.Deserialize); netPacketProcessor.RegisterNestedType(Vector2Serializer.Serialize, Vector2Serializer.Deserialize); @@ -64,7 +67,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/ChatManager.cs b/Multiplayer/Networking/Managers/Server/ChatManager.cs new file mode 100644 index 0000000..bee85ed --- /dev/null +++ b/Multiplayer/Networking/Managers/Server/ChatManager.cs @@ -0,0 +1,197 @@ +using LiteNetLib; +using Multiplayer.Components.Networking; +using System.Linq; +using Multiplayer.Networking.Data; +using System.Text.RegularExpressions; +using UnityEngine; + +namespace Multiplayer.Networking.Managers.Server; + +public static class ChatManager +{ + 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 (sender !=null && !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; + 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; + + } +} diff --git a/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs b/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs new file mode 100644 index 0000000..90f9ce8 --- /dev/null +++ b/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs @@ -0,0 +1,187 @@ +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; + +namespace Multiplayer.Networking.Managers.Server; +public class LobbyServerManager : MonoBehaviour +{ + //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() + { + server = NetworkLifecycle.Instance.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() + { + Multiplayer.Log($"LobbyServerManager OnDestroy()"); + sendUpdates = false; + StopAllCoroutines(); + StartCoroutine(RemoveFromLobbyServer($"{Multiplayer.Settings.LobbyServerAddress}/{ENDPOINT_REMOVE_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(UpdateLobbyServer($"{Multiplayer.Settings.LobbyServerAddress}/{ENDPOINT_UPDATE_SERVER}")); + } + } + } + + public void RemoveFromLobbyServer() + { + Multiplayer.Log($"RemoveFromLobbyServer OnDestroy()"); + sendUpdates = false; + StopAllCoroutines(); + StartCoroutine(RemoveFromLobbyServer($"{Multiplayer.Settings.LobbyServerAddress}/{ENDPOINT_REMOVE_SERVER}")); + } + + private IEnumerator RegisterWithLobbyServer(string uri) + { + JsonSerializerSettings jsonSettings = new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }; + string json = JsonConvert.SerializeObject(server.serverData, jsonSettings); + Multiplayer.LogDebug(()=>$"JsonRequest: {json}"); + + yield return SendWebRequest( + uri, + json, + webRequest => + { + LobbyServerResponseData response = JsonConvert.DeserializeObject(webRequest.downloadHandler.text); + if (response != null) + { + private_key = response.private_key; + server_id = response.game_server_id; + sendUpdates = true; + } + }, + webRequest => Multiplayer.LogError("Failed to register with lobby server") + ); + } + + private IEnumerator RemoveFromLobbyServer(string uri) + { + 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") + ); + } + + private IEnumerator UpdateLobbyServer(string uri) + { + 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( + 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)) + { + webRequest.redirectLimit = 0; + + webRequest.uploadHandler = new UploadHandlerRaw(System.Text.Encoding.UTF8.GetBytes(json)){contentType = "application/json"}; + webRequest.downloadHandler = new DownloadHandlerBuffer(); + + yield return webRequest.SendWebRequest(); + + //check for redirect + if (webRequest.responseCode >= 300 && webRequest.responseCode < 400) + { + 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 + { + 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 f5129b2..301ea27 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; @@ -14,8 +15,11 @@ using Multiplayer.Components.Networking; using Multiplayer.Components.Networking.Train; using Multiplayer.Components.Networking.World; +using Multiplayer.Components.Networking.Jobs; using Multiplayer.Networking.Data; +using Multiplayer.Networking.Managers.Server; using Multiplayer.Networking.Packets.Clientbound; +using Multiplayer.Networking.Packets.Clientbound.Jobs; using Multiplayer.Networking.Packets.Clientbound.SaveGame; using Multiplayer.Networking.Packets.Clientbound.Train; using Multiplayer.Networking.Packets.Clientbound.World; @@ -25,6 +29,7 @@ using Multiplayer.Utils; using UnityEngine; using UnityModManagerNet; +using Unity.Jobs; namespace Multiplayer.Networking.Listeners; @@ -36,6 +41,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 +56,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 +72,17 @@ public bool Start(int port) return netManager.Start(port); } + public override void Stop() + { + if (lobbyServerManager != null) + { + lobbyServerManager.RemoveFromLobbyServer(); + GameObject.Destroy(lobbyServerManager); + } + + base.Stop(); + } + protected override void Subscribe() { netPacketProcessor.SubscribeReusable(OnServerboundClientLoginPacket); @@ -83,10 +108,19 @@ protected override void Subscribe() netPacketProcessor.SubscribeReusable(OnCommonHandbrakePositionPacket); netPacketProcessor.SubscribeReusable(OnCommonTrainPortsPacket); netPacketProcessor.SubscribeReusable(OnCommonTrainFusesPacket); + + netPacketProcessor.SubscribeReusable(OnServerboundJobTakeRequestPacket); + netPacketProcessor.SubscribeReusable(OnCommonChatPacket); } 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) @@ -110,7 +144,8 @@ public bool TryGetNetPeer(byte id, out NetPeer peer) #region Net Events public override void OnPeerConnected(NetPeer peer) - { } + { + } public override void OnPeerDisconnected(NetPeer peer, DisconnectInfo disconnectInfo) { @@ -122,21 +157,24 @@ public override void OnPeerDisconnected(NetPeer peer, DisconnectInfo disconnectI serverPlayers.Remove(id); netPeers.Remove(id); - netManager.SendToAll(WritePacket(new ClientboundPlayerDisconnectPacket { + netManager.SendToAll(WritePacket(new ClientboundPlayerDisconnectPacket + { Id = id }), DeliveryMethod.ReliableUnordered); } public override void OnNetworkLatencyUpdate(NetPeer peer, int latency) { - ClientboundPingUpdatePacket clientboundPingUpdatePacket = new() { + ClientboundPingUpdatePacket clientboundPingUpdatePacket = new() + { Id = (byte)peer.Id, Ping = latency }; SendPacketToAll(clientboundPingUpdatePacket, DeliveryMethod.ReliableUnordered, peer); - SendPacket(peer, new ClientboundTickSyncPacket { + SendPacket(peer, new ClientboundTickSyncPacket + { ServerTick = NetworkLifecycle.Instance.Tick }, DeliveryMethod.ReliableUnordered); } @@ -180,7 +218,8 @@ public void SendSpawnTrainCar(NetworkedTrainCar networkedTrainCar) public void SendDestroyTrainCar(TrainCar trainCar) { - SendPacketToAll(new ClientboundDestroyTrainCarPacket { + SendPacketToAll(new ClientboundDestroyTrainCarPacket + { NetId = trainCar.GetNetId() }, DeliveryMethod.ReliableOrdered, selfPeer); } @@ -194,7 +233,8 @@ public void SendCargoState(TrainCar trainCar, ushort netId, bool isLoading, byte { Car logicCar = trainCar.logicCar; CargoType cargoType = isLoading ? logicCar.CurrentCargoTypeInCar : logicCar.LastUnloadedCargoType; - SendPacketToAll(new ClientboundCargoStatePacket { + SendPacketToAll(new ClientboundCargoStatePacket + { NetId = netId, IsLoading = isLoading, CargoType = (ushort)cargoType, @@ -206,7 +246,8 @@ public void SendCargoState(TrainCar trainCar, ushort netId, bool isLoading, byte public void SendCarHealthUpdate(ushort netId, float health) { - SendPacketToAll(new ClientboundCarHealthUpdatePacket { + SendPacketToAll(new ClientboundCarHealthUpdatePacket + { NetId = netId, Health = health }, DeliveryMethod.ReliableOrdered, selfPeer); @@ -214,7 +255,8 @@ public void SendCarHealthUpdate(ushort netId, float health) public void SendRerailTrainCar(ushort netId, ushort rerailTrack, Vector3 worldPos, Vector3 forward) { - SendPacketToAll(new ClientboundRerailTrainPacket { + SendPacketToAll(new ClientboundRerailTrainPacket + { NetId = netId, TrackId = rerailTrack, Position = worldPos, @@ -224,7 +266,8 @@ public void SendRerailTrainCar(ushort netId, ushort rerailTrack, Vector3 worldPo public void SendWindowsBroken(ushort netId, Vector3 forceDirection) { - SendPacketToAll(new ClientboundWindowsBrokenPacket { + SendPacketToAll(new ClientboundWindowsBrokenPacket + { NetId = netId, ForceDirection = forceDirection }, DeliveryMethod.ReliableUnordered, selfPeer); @@ -232,21 +275,24 @@ public void SendWindowsBroken(ushort netId, Vector3 forceDirection) public void SendWindowsRepaired(ushort netId) { - SendPacketToAll(new ClientboundWindowsBrokenPacket { + SendPacketToAll(new ClientboundWindowsBrokenPacket + { NetId = netId }, DeliveryMethod.ReliableUnordered, selfPeer); } public void SendMoney(float amount) { - SendPacketToAll(new ClientboundMoneyPacket { + SendPacketToAll(new ClientboundMoneyPacket + { Amount = amount }, DeliveryMethod.ReliableUnordered, selfPeer); } public void SendLicense(string id, bool isJobLicense) { - SendPacketToAll(new ClientboundLicenseAcquiredPacket { + SendPacketToAll(new ClientboundLicenseAcquiredPacket + { Id = id, IsJobLicense = isJobLicense }, DeliveryMethod.ReliableUnordered, selfPeer); @@ -254,25 +300,74 @@ public void SendLicense(string id, bool isJobLicense) public void SendGarage(string id) { - SendPacketToAll(new ClientboundGarageUnlockPacket { + SendPacketToAll(new ClientboundGarageUnlockPacket + { Id = id }, DeliveryMethod.ReliableUnordered, selfPeer); } public void SendDebtStatus(bool hasDebt) { - SendPacketToAll(new ClientboundDebtStatusPacket { + SendPacketToAll(new ClientboundDebtStatusPacket + { HasDebt = hasDebt }, DeliveryMethod.ReliableUnordered, selfPeer); } + public void SendJobCreatePacket(NetworkedJob job) + { + Multiplayer.Log("Sending JobCreatePacket with netId: " + job.NetId + ", Job ID: " + job.job.ID); + SendPacketToAll(ClientboundJobCreatePacket.FromNetworkedJob(job),DeliveryMethod.ReliableSequenced); + } + + 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 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 @@ -292,7 +387,8 @@ private void OnServerboundClientLoginPacket(ServerboundClientLoginPacket packet, if (Multiplayer.Settings.Password != packet.Password) { LogWarning("Denied login due to invalid password!"); - ClientboundServerDenyPacket denyPacket = new() { + ClientboundServerDenyPacket denyPacket = new() + { ReasonKey = Locale.DISCONN_REASON__INVALID_PASSWORD_KEY }; request.Reject(WritePacket(denyPacket)); @@ -302,7 +398,8 @@ private void OnServerboundClientLoginPacket(ServerboundClientLoginPacket packet, if (packet.BuildMajorVersion != BuildInfo.BUILD_VERSION_MAJOR) { LogWarning($"Denied login to incorrect game version! Got: {packet.BuildMajorVersion}, expected: {BuildInfo.BUILD_VERSION_MAJOR}"); - ClientboundServerDenyPacket denyPacket = new() { + ClientboundServerDenyPacket denyPacket = new() + { ReasonKey = Locale.DISCONN_REASON__GAME_VERSION_KEY, ReasonArgs = new[] { BuildInfo.BUILD_VERSION_MAJOR.ToString(), packet.BuildMajorVersion.ToString() } }; @@ -310,10 +407,11 @@ 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() { + ClientboundServerDenyPacket denyPacket = new() + { ReasonKey = Locale.DISCONN_REASON__FULL_SERVER_KEY }; request.Reject(WritePacket(denyPacket)); @@ -326,7 +424,8 @@ private void OnServerboundClientLoginPacket(ServerboundClientLoginPacket packet, ModInfo[] missing = serverMods.Except(clientMods).ToArray(); ModInfo[] extra = clientMods.Except(serverMods).ToArray(); LogWarning($"Denied login due to mod mismatch! {missing.Length} missing, {extra.Length} extra"); - ClientboundServerDenyPacket denyPacket = new() { + ClientboundServerDenyPacket denyPacket = new() + { ReasonKey = Locale.DISCONN_REASON__MODS_KEY, Missing = missing, Extra = extra @@ -337,9 +436,11 @@ private void OnServerboundClientLoginPacket(ServerboundClientLoginPacket packet, NetPeer peer = request.Accept(); - ServerPlayer serverPlayer = new() { + ServerPlayer serverPlayer = new() + { Id = (byte)peer.Id, - Username = packet.Username, + Username = overrideUsername, + OriginalUsername = packet.Username, Guid = guid }; @@ -381,13 +482,16 @@ private void OnServerboundClientReadyPacket(ServerboundClientReadyPacket packet, // Send the new player to all other players ServerPlayer serverPlayer = serverPlayers[peerId]; - ClientboundPlayerJoinedPacket clientboundPlayerJoinedPacket = new() { + ClientboundPlayerJoinedPacket clientboundPlayerJoinedPacket = new() + { Id = peerId, Username = serverPlayer.Username, Guid = serverPlayer.Guid.ToByteArray() }; 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 @@ -403,7 +507,8 @@ private void OnServerboundClientReadyPacket(ServerboundClientReadyPacket packet, SendPacket(peer, WeatherDriver.Instance.GetSaveData().ToObject(), DeliveryMethod.ReliableOrdered); // Send junctions and turntables - SendPacket(peer, new ClientboundRailwayStatePacket { + SendPacket(peer, new ClientboundRailwayStatePacket + { SelectedJunctionBranches = NetworkedJunction.IndexedJunctions.Select(j => (byte)j.Junction.selectedBranch).ToArray(), TurntableRotations = NetworkedTurntable.IndexedTurntables.Select(j => j.TurntableRailTrack.currentYRotation).ToArray() }, DeliveryMethod.ReliableOrdered); @@ -415,12 +520,38 @@ private void OnServerboundClientReadyPacket(ServerboundClientReadyPacket packet, SendPacket(peer, ClientboundSpawnTrainSetPacket.FromTrainSet(set), DeliveryMethod.ReliableOrdered); } + //send jobs - do we need a job manager/job IDs to make this easier? + foreach(StationController station in StationController.allStations) + { + List jobData = new List(); + List netIds = new List(); + + foreach(Job job in station.logicStation.availableJobs) + { + jobData.Add(JobData.FromJob(job)); + netIds.Add(NetworkedJob.GetFromJob(job).NetId); + } + + SendPacket(peer, + new ClientboundJobsPacket + { + stationId = station.logicStation.ID, + netIds = netIds.ToArray(), + Jobs = jobData.ToArray(), + }, + DeliveryMethod.ReliableOrdered + ); + + } + + // Send existing players foreach (ServerPlayer player in ServerPlayers) { if (player.Id == peer.Id) continue; - SendPacket(peer, new ClientboundPlayerJoinedPacket { + SendPacket(peer, new ClientboundPlayerJoinedPacket + { Id = player.Id, Username = player.Username, Guid = player.Guid.ToByteArray(), @@ -444,7 +575,8 @@ private void OnServerboundPlayerPositionPacket(ServerboundPlayerPositionPacket p player.RawRotationY = packet.RotationY; } - ClientboundPlayerPositionPacket clientboundPacket = new() { + ClientboundPlayerPositionPacket clientboundPacket = new() + { Id = (byte)peer.Id, Position = packet.Position, MoveDir = packet.MoveDir, @@ -463,7 +595,8 @@ private void OnServerboundPlayerCarPacket(ServerboundPlayerCarPacket packet, Net if (TryGetServerPlayer(peer, out ServerPlayer player)) player.CarId = packet.CarId; - ClientboundPlayerCarPacket clientboundPacket = new() { + ClientboundPlayerCarPacket clientboundPacket = new() + { Id = (byte)peer.Id, CarId = packet.CarId }; @@ -473,7 +606,8 @@ private void OnServerboundPlayerCarPacket(ServerboundPlayerCarPacket packet, Net private void OnServerboundTimeAdvancePacket(ServerboundTimeAdvancePacket packet, NetPeer peer) { - SendPacketToAll(new ClientboundTimeAdvancePacket { + SendPacketToAll(new ClientboundTimeAdvancePacket + { amountOfTimeToSkipInSeconds = packet.amountOfTimeToSkipInSeconds }, DeliveryMethod.ReliableUnordered, peer); } @@ -648,5 +782,43 @@ private void OnServerboundLicensePurchaseRequestPacket(ServerboundLicensePurchas LicenseManager.Instance.AcquireGeneralLicense(generalLicense); } + private void OnServerboundJobTakeRequestPacket(ServerboundJobTakeRequestPacket packet, NetPeer peer) + { + NetworkedJob networkedJob; + + if (!NetworkedJob.Get(packet.netId, out networkedJob)) + { + Multiplayer.Log($"OnServerboundJobTakeRequestPacket netId Not Found: {packet.netId}"); + return; + } + + if (networkedJob.job.State != JobState.Available) { + + Multiplayer.Log($"OnServerboundJobTakeRequestPacket jobId: {networkedJob.job.ID}, DENIED"); + ServerPlayer player = ServerPlayers.First(x => x.Guid == networkedJob.takenBy); + //deny the request + SendPacket(peer, new ClientboundJobTakeResponsePacket { netId = packet.netId, granted = false, playerId = player.Id }, DeliveryMethod.ReliableOrdered); + } + else + { + //probably need to do more here + ServerPlayer player; + if (!TryGetServerPlayer(peer, out player)) + return; + + networkedJob.takenBy = player.Guid; + //networkedJob.job.State = JobState.InProgress; + + //todo: officially take the job + Multiplayer.Log($"OnServerboundJobTakeRequestPacket jobId: {networkedJob.job.ID}, GRANTED"); + SendPacket(peer, new ClientboundJobTakeResponsePacket { netId = packet.netId, granted = true, playerId = player.Id }, DeliveryMethod.ReliableOrdered); + + } + } + + private void OnCommonChatPacket(CommonChatPacket packet, NetPeer peer) + { + ChatManager.ProcessMessage(packet.message,peer); + } #endregion } diff --git a/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobCreatePacket.cs b/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobCreatePacket.cs new file mode 100644 index 0000000..4caa869 --- /dev/null +++ b/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobCreatePacket.cs @@ -0,0 +1,22 @@ +using Multiplayer.Components.Networking.Jobs; +using Multiplayer.Networking.Data; +using Multiplayer.Networking.Packets.Clientbound.Train; + +namespace Multiplayer.Networking.Packets.Clientbound.Jobs; + +public class ClientboundJobCreatePacket +{ + public ushort netId { get; set; } + public string stationId { get; set; } + public JobData job { get; set; } + + public static ClientboundJobCreatePacket FromNetworkedJob(NetworkedJob job) + { + return new ClientboundJobCreatePacket + { + netId = job.NetId, + stationId = job.stationID, + job = JobData.FromJob(job.job), + }; + } +} diff --git a/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobPacket.cs b/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobPacket.cs new file mode 100644 index 0000000..fc89a41 --- /dev/null +++ b/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobPacket.cs @@ -0,0 +1,11 @@ +using Multiplayer.Networking.Data; + +namespace Multiplayer.Networking.Packets.Clientbound.Jobs; + +public class ClientboundJobsPacket +{ + public string stationId { get; set; } + public ushort[] netIds { get; set; } + public JobData[] Jobs { get; set; } + +} diff --git a/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobTakeResponsePacket.cs b/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobTakeResponsePacket.cs new file mode 100644 index 0000000..53b0bd9 --- /dev/null +++ b/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobTakeResponsePacket.cs @@ -0,0 +1,12 @@ +using Multiplayer.Components.Networking.Jobs; +using Multiplayer.Networking.Data; +using Multiplayer.Networking.Packets.Clientbound.Train; + +namespace Multiplayer.Networking.Packets.Clientbound.Jobs; + +public class ClientboundJobTakeResponsePacket +{ + public ushort netId { get; set; } + public bool granted { get; set; } + public byte playerId { get; set; } +} diff --git a/Multiplayer/Networking/Packets/Common/CommonChatPacket.cs b/Multiplayer/Networking/Packets/Common/CommonChatPacket.cs new file mode 100644 index 0000000..1c511ad --- /dev/null +++ b/Multiplayer/Networking/Packets/Common/CommonChatPacket.cs @@ -0,0 +1,14 @@ +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; } + +} diff --git a/Multiplayer/Networking/Packets/Serverbound/Jobs/ServerboundJobTakeRequestPacket.cs b/Multiplayer/Networking/Packets/Serverbound/Jobs/ServerboundJobTakeRequestPacket.cs new file mode 100644 index 0000000..895d5fe --- /dev/null +++ b/Multiplayer/Networking/Packets/Serverbound/Jobs/ServerboundJobTakeRequestPacket.cs @@ -0,0 +1,10 @@ +using Multiplayer.Components.Networking.Jobs; +using Multiplayer.Networking.Data; +using Multiplayer.Networking.Packets.Clientbound.Train; + +namespace Multiplayer.Networking.Packets.Clientbound.Jobs; + +public class ServerboundJobTakeRequestPacket +{ + public ushort netId { get; set; } +} diff --git a/Multiplayer/Patches/Jobs/JobOverviewUsePatch.cs b/Multiplayer/Patches/Jobs/JobOverviewUsePatch.cs new file mode 100644 index 0000000..2ee4ab5 --- /dev/null +++ b/Multiplayer/Patches/Jobs/JobOverviewUsePatch.cs @@ -0,0 +1,66 @@ +using DV; +using DV.Interaction; +using DV.Logic.Job; +using DV.ThingTypes; +using DV.Utils; +using HarmonyLib; +using Multiplayer.Components; +using Multiplayer.Components.Networking; +using Multiplayer.Components.Networking.Jobs; +using Multiplayer.Utils; +using System.Collections; +using Unity.Jobs; +using UnityEngine; +using static UnityEngine.GraphicsBuffer; + +namespace Multiplayer.Patches.Jobs; +//public void HandleUse(ItemUseTarget target) +[HarmonyPatch(typeof(JobOverviewUse), nameof(JobOverviewUse.HandleUse))] +public static class JobOverviewUse_HandleUse_Patch +{ + private static bool Prefix(JobOverviewUse __instance, ItemUseTarget target, ref JobOverview ___jobOverview) + { + JobValidator component = target.GetComponent(); + if (component == null) + return false; + + if (component.bookletPrinter.IsOnCooldown) + { + component.bookletPrinter.PlayErrorSound(); + return false; + } + + Job job = ___jobOverview.job; + + Multiplayer.Log($"JobOverviewUse_HandleUse_Patch jobId: {job.ID}"); + + NetworkedJob networkedJob; + + if (!NetworkedJob.TryGetFromJob(job, out networkedJob)) + { + Multiplayer.Log($"JobOverviewUse_HandleUse_Patch No netId found for jobId: {job.ID}"); + component.bookletPrinter.PlayErrorSound(); + return false; + } + + if(networkedJob.allowTake == true) { + Multiplayer.Log($"JobOverviewUse_HandleUse_Patch jobId: {job.ID}, Take allowed: {networkedJob.allowTake}"); + return true; + } + else if (networkedJob.allowTake == null || (networkedJob.allowTake == false && networkedJob.takenBy == null)) + { + Multiplayer.Log($"JobOverviewUse_HandleUse_Patch WaitForResponse returned for jobId: {job.ID}"); + networkedJob.jobValidator = component; + networkedJob.jobOverview = ___jobOverview; + NetworkLifecycle.Instance.Client.SendJobTakeRequest(networkedJob.NetId); + + return false; + + } + + component.bookletPrinter.PlayErrorSound(); + return false; + + } +} + diff --git a/Multiplayer/Patches/Jobs/StationControllerPatch.cs b/Multiplayer/Patches/Jobs/StationControllerPatch.cs new file mode 100644 index 0000000..f2fc105 --- /dev/null +++ b/Multiplayer/Patches/Jobs/StationControllerPatch.cs @@ -0,0 +1,13 @@ +using HarmonyLib; +using Multiplayer.Components.Networking.World; + +namespace Multiplayer.Patches.Jobs; + +[HarmonyPatch(typeof(StationController), nameof(StationController.Awake))] +public static class StationController_Awake_Patch +{ + public static void Postfix(StationController __instance) + { + __instance.gameObject.AddComponent(); + } +} diff --git a/Multiplayer/Patches/Jobs/StationJobGenerationRangePatch.cs b/Multiplayer/Patches/Jobs/StationJobGenerationRangePatch.cs new file mode 100644 index 0000000..ae82b0b --- /dev/null +++ b/Multiplayer/Patches/Jobs/StationJobGenerationRangePatch.cs @@ -0,0 +1,53 @@ +using HarmonyLib; +using Multiplayer.Components.Networking; +using Multiplayer.Networking.Data; +using UnityEngine; + +namespace Multiplayer.Patches.Jobs; + +[HarmonyPatch(typeof(StationJobGenerationRange), nameof(StationJobGenerationRange.PlayerSqrDistanceFromStationCenter), MethodType.Getter)] +public static class StationJobGenerationRange_PlayerSqrDistanceFromStationCenter_Patch +{ + private static bool Prefix(StationJobGenerationRange __instance, ref float __result) + { + if (!NetworkLifecycle.Instance.IsHost()) + return true; + + Vector3 anchor = __instance.stationCenterAnchor.position; + + __result = float.MaxValue; + + //Loop through all of the players and return the one thats closest to the anchor + foreach (ServerPlayer serverPlayer in NetworkLifecycle.Instance.Server.ServerPlayers) + { + float sqDist = (serverPlayer.WorldPosition - anchor).sqrMagnitude; + if (sqDist < __result) + __result = sqDist; + } + + return false; + } +} + +[HarmonyPatch(typeof(StationJobGenerationRange), nameof(StationJobGenerationRange.PlayerSqrDistanceFromStationOffice), MethodType.Getter)] +public static class StationJobGenerationRange_PlayerSqrDistanceFromStationOffice_Patch +{ + private static bool Prefix(StationJobGenerationRange __instance, ref float __result) + { + if (!NetworkLifecycle.Instance.IsHost()) + return true; + + Vector3 anchor = __instance.transform.position; + + __result = float.MaxValue; + //Loop through all of the players and return the one thats closest to the anchor + foreach (ServerPlayer serverPlayer in NetworkLifecycle.Instance.Server.ServerPlayers) + { + float sqDist = (serverPlayer.WorldPosition - anchor).sqrMagnitude; + if (sqDist < __result) + __result = sqDist; + } + + return false; + } +} diff --git a/Multiplayer/Patches/Jobs/StationPatch.cs b/Multiplayer/Patches/Jobs/StationPatch.cs new file mode 100644 index 0000000..a0f253d --- /dev/null +++ b/Multiplayer/Patches/Jobs/StationPatch.cs @@ -0,0 +1,34 @@ +using DV.Logic.Job; +using HarmonyLib; +using Multiplayer.Components; +using Multiplayer.Components.Networking; +using Multiplayer.Components.Networking.Jobs; +using Multiplayer.Components.Networking.Train; +using Multiplayer.Utils; + +namespace Multiplayer.Patches.Jobs; + +[HarmonyPatch(typeof(Station), nameof(Station.AddJobToStation))] +public static class Station_AddJobToStation_Patch +{ + private static bool Prefix(Station __instance, Job job) + { + if (!NetworkLifecycle.Instance.IsHost()) + return false; + + Multiplayer.Log($"Station_AddJobToStation_Patch adding NetworkJob for stationId: {__instance.ID}, jobId: {job.ID}"); + + StationController stationController; + if(!StationComponentLookup.Instance.StationControllerFromId(__instance.ID, out stationController)) + return false; + + NetworkedJob netJob = stationController.gameObject.AddComponent(); + if (netJob != null) + { + netJob.job=job; + netJob.stationID = __instance.ID; + + } + return true; + } +} diff --git a/Multiplayer/Patches/World/StationProceduralJobsControllerPatch.cs b/Multiplayer/Patches/Jobs/StationProceduralJobsControllerPatch.cs similarity index 90% rename from Multiplayer/Patches/World/StationProceduralJobsControllerPatch.cs rename to Multiplayer/Patches/Jobs/StationProceduralJobsControllerPatch.cs index 217630b..0d82e62 100644 --- a/Multiplayer/Patches/World/StationProceduralJobsControllerPatch.cs +++ b/Multiplayer/Patches/Jobs/StationProceduralJobsControllerPatch.cs @@ -1,7 +1,7 @@ using HarmonyLib; using Multiplayer.Components.Networking; -namespace Multiplayer.Patches.World; +namespace Multiplayer.Patches.Jobs; [HarmonyPatch(typeof(StationProceduralJobsController), nameof(StationProceduralJobsController.TryToGenerateJobs))] public static class StationProceduralJobsController_TryToGenerateJobs_Patch diff --git a/Multiplayer/Patches/MainMenu/LauncherControllerPatch.cs b/Multiplayer/Patches/MainMenu/LauncherControllerPatch.cs new file mode 100644 index 0000000..17d4e4c --- /dev/null +++ b/Multiplayer/Patches/MainMenu/LauncherControllerPatch.cs @@ -0,0 +1,101 @@ +using System; +using DV.Common; +using DV.UI; +using DV.UI.PresetEditors; +using DV.UIFramework; +using HarmonyLib; +using Multiplayer.Components.MainMenu; +using Multiplayer.Utils; +using UnityEngine; +using UnityEngine.UI; + + +namespace Multiplayer.Patches.MainMenu; + +[HarmonyPatch(typeof(LauncherController))] +public static class LauncherController_Patch +{ + private const int PADDING = 10; + + private static GameObject goHost; + private static LauncherController lcInstance; + + + + [HarmonyPostfix] + [HarmonyPatch(typeof(LauncherController), "OnEnable")] + private static void OnEnable(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); + + 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(); + + btnHost.onClick.AddListener(HostAction); + + goHost.SetActive(true); + + 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() + { + //Debug.Log("Host button clicked."); + + RightPaneController_OnEnable_Patch.uIMenuController.SwitchMenu(RightPaneController_OnEnable_Patch.hostMenuIndex); + + } +} diff --git a/Multiplayer/Patches/MainMenu/LocalizationManagerPatch.cs b/Multiplayer/Patches/MainMenu/LocalizationManagerPatch.cs index 0f799cb..7fd486f 100644 --- a/Multiplayer/Patches/MainMenu/LocalizationManagerPatch.cs +++ b/Multiplayer/Patches/MainMenu/LocalizationManagerPatch.cs @@ -1,20 +1,35 @@ 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; + /// + /// 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) + { + 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..3ece983 100644 --- a/Multiplayer/Patches/MainMenu/MainMenuControllerPatch.cs +++ b/Multiplayer/Patches/MainMenu/MainMenuControllerPatch.cs @@ -1,50 +1,81 @@ -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) + /// + /// Harmony patch for the Awake method of MainMenuController to add a Multiplayer button. + /// + [HarmonyPatch(typeof(MainMenuController), "Awake")] + public static class MainMenuController_Awake_Patch { - GameObject button = __instance.FindChildByName("ButtonSelectable Sessions"); - if (button == null) + public static GameObject multiplayerButton; + + /// + /// Prefix method to run before MainMenuController's Awake method. + /// + /// The instance of MainMenuController. + 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; + } - button.SetActive(false); - MultiplayerButton = Object.Instantiate(button, button.transform.parent); - button.SetActive(true); + // Deactivate the sessions button temporarily to duplicate it + sessionsButton.SetActive(false); + multiplayerButton = Object.Instantiate(sessionsButton, sessionsButton.transform.parent); + sessionsButton.SetActive(true); - MultiplayerButton.transform.SetSiblingIndex(button.transform.GetSiblingIndex() + 1); - MultiplayerButton.name = "ButtonSelectable Multiplayer"; + // Configure the new Multiplayer button + multiplayerButton.transform.SetSiblingIndex(sessionsButton.transform.GetSiblingIndex() + 1); + multiplayerButton.name = "ButtonSelectable Multiplayer"; - Localize localize = MultiplayerButton.GetComponentInChildren(); - localize.key = Locale.MAIN_MENU__JOIN_SERVER_KEY; + // Set the localization key for the new button + Localize localize = multiplayerButton.GetComponentInChildren(); + localize.key = Locale.MAIN_MENU__JOIN_SERVER_KEY; - // 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; + // Remove existing localization components to reset them + Object.Destroy(multiplayerButton.GetComponentInChildren()); + multiplayerButton.ResetTooltip(); - GameObject icon = MultiplayerButton.FindChildByName("icon"); - if (icon == null) - { - Multiplayer.LogError("Failed to find icon on Sessions button, destroying the Multiplayer button!"); - Object.Destroy(MultiplayerButton); - return; + // Set the icon for the new Multiplayer button + SetButtonIcon(multiplayerButton); } - icon.GetComponent().sprite = Multiplayer.AssetIndex.multiplayerIcon; + /// + /// 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; + //} + + /// + /// 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"); + 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 f393cc4..8b3c46e 100644 --- a/Multiplayer/Patches/MainMenu/RightPaneControllerPatch.cs +++ b/Multiplayer/Patches/MainMenu/RightPaneControllerPatch.cs @@ -1,68 +1,92 @@ -using DV.Localization; +using DV.Localization; using DV.UI; using DV.UIFramework; using HarmonyLib; using Multiplayer.Components.MainMenu; using Multiplayer.Utils; +using System.Reflection; using TMPro; using UnityEngine; -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) + public static int hostMenuIndex; + public static UIMenuController uIMenuController; + public static HostGamePane hgpInstance; + private static void Prefix(RightPaneController __instance) { - Multiplayer.LogError("Failed to find Launcher pane!"); - return; - } + uIMenuController = __instance.menuController; + // Check if the multiplayer pane already exists + if (__instance.HasChildWithName("PaneRight Multiplayer")) + return; - launcher.SetActive(false); - GameObject multiplayerPane = Object.Instantiate(launcher, launcher.transform.parent); - launcher.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); + multiplayerPane.name = "PaneRight Multiplayer"; - 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")); + // 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; - GameObject titleObj = multiplayerPane.FindChildByName("Title"); - if (titleObj == null) - { - Multiplayer.LogError("Failed to find title object!"); - return; - } + // Clean up unnecessary components and child objects + GameObject.Destroy(multiplayerPane.GetComponent()); + GameObject.Destroy(multiplayerPane.GetComponent()); + multiplayerPane.AddComponent(); - GameObject content = multiplayerPane.FindChildByName("text header"); - content.GetComponentInChildren().text = "Server browser not yet implemented."; + // Create and initialize MainMenuThingsAndStuff + 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; + }); - titleObj.GetComponentInChildren().key = Locale.SERVER_BROWSER__TITLE_KEY; - Object.Destroy(titleObj.GetComponentInChildren()); + // Activate the multiplayer button + MainMenuController_Awake_Patch.multiplayerButton.SetActive(true); + Multiplayer.LogError("At end!"); - 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; - }); - multiplayerPane.SetActive(true); - MainMenuController_Awake_Patch.MultiplayerButton.SetActive(true); + // Check if the host pane already exists + if (__instance.HasChildWithName("PaneRight Host")) + return; + + if (basePane == null) + { + Multiplayer.LogError("Failed to find Load/Save pane!"); + return; + } + + // 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"; + + GameObject.Destroy(hostPane.GetComponent()); + GameObject.Destroy(hostPane.GetComponent()); + hgpInstance = 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/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 b29e88a..2ca75d3 100644 --- a/Multiplayer/Settings.cs +++ b/Multiplayer/Settings.cs @@ -21,12 +21,32 @@ 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.")] 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 = "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 = ""; + [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("Last Server Connected to by IP")] diff --git a/Multiplayer/Utils/Csv.cs b/Multiplayer/Utils/Csv.cs index 560fb24..fcb12e5 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,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. + /// + public static ReadOnlyDictionary> Parse(string data) + { + // Split the input data into lines + string[] separators = new string[] { "\r\n", "\n" }; + string[] lines = data.Split(separators, StringSplitOptions.RemoveEmptyEntries); - // 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); - return new ReadOnlyDictionary>(columns.Cast() - .ToDictionary(entry => (string)entry.Key, entry => (Dictionary)entry.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 rowKey = values[0]; - void FinishLine() - { - values.Add(builder.ToString()); - builder.Clear(); + // 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]; + } + } + } + + // 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(); + } } } diff --git a/Multiplayer/Utils/DvExtensions.cs b/Multiplayer/Utils/DvExtensions.cs index 745ef94..5241d93 100644 --- a/Multiplayer/Utils/DvExtensions.cs +++ b/Multiplayer/Utils/DvExtensions.cs @@ -1,6 +1,14 @@ 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; +using System.Linq; + + namespace Multiplayer.Utils; @@ -36,4 +44,64 @@ public static NetworkedRailTrack Networked(this RailTrack railTrack) } #endregion + + #region UI + 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); + button.name = newButtonName; + + // Update localization and tooltip + if (button.GetComponentInChildren() != null) + { + button.GetComponentInChildren().key = localeKey; + 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 + if (icon != null) + { + SetButtonIcon(button, icon); + } + + // Enable button interaction + button.GetComponentInChildren().ToggleInteractable(true); + + return button; + } + + 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; + } + + 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/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/info.json b/info.json index b6f7b0e..43accd0 100644 --- a/info.json +++ b/info.json @@ -1,9 +1,10 @@ { "Id": "Multiplayer", - "Version": "0.1.0", + "Version": "0.1.5.2", "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" } diff --git a/locale.csv b/locale.csv index 6a08bcd..05fdbf5 100644 --- a/locale.csv +++ b/locale.csv @@ -1,37 +1,76 @@ 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.,Изберете игра за присъединяване,选择要加入的游戏,選擇要加入的遊戲,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,Ні +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!,Сервери не знайдено. Оновіть або почніть власний! ,,,,,,,,,,,,,,,,,,,,,,,,,,, -,"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,,,,,,,,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,,,,,,,,Navigateur de serveurs,Server Liste,,,Ricerca Server,,,,,,,,,,Buscar servidores,,, -sb/direct,Connect to IP button,Connect to IP,Свързване към IP,连接到IP,連接到IP,Připojit k IP,Opret forbindelse til IP,Verbinding maken met IP,Yhdistä IP-osoitteeseen,Connectez-vous à IP,Mit IP verbinden,आईपी से कनेक्ट करें,Csatlakozás az IP-hez,Connetti all'IP,IPに接続する,IP에 연결,Koble til IP,Połącz się z IP,Conecte-se ao IP,Conecte-se ao IP,Conectați-vă la IP,Подключиться к IP,Pripojiť k IP,Conectarse a IP,Anslut till IP,IP'ye Bağlan,Підключитися до IP -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,Домакин на играта,主机游戏,主機遊戲,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 24b6f5888f79d155e16f4a11fa1b64865a3f3d92 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 14 Jul 2024 13:42:26 +1000 Subject: [PATCH 041/188] Rectified conflicts Ready for merge --- .../Components/MainMenu/MultiplayerPane.cs | 178 ------------------ ...pupTextInputFieldControllerNoValidation.cs | 91 --------- .../Components/MainMenu/ServerBrowserPane.cs | 67 ++++++- Multiplayer/Settings.cs | 11 -- 4 files changed, 61 insertions(+), 286 deletions(-) delete mode 100644 Multiplayer/Components/MainMenu/MultiplayerPane.cs delete mode 100644 Multiplayer/Components/MainMenu/PopupTextInputFieldControllerNoValidation.cs diff --git a/Multiplayer/Components/MainMenu/MultiplayerPane.cs b/Multiplayer/Components/MainMenu/MultiplayerPane.cs deleted file mode 100644 index dcf588e..0000000 --- a/Multiplayer/Components/MainMenu/MultiplayerPane.cs +++ /dev/null @@ -1,178 +0,0 @@ -using System; -using System.Net; -using System.Text.RegularExpressions; -using DV.UIFramework; -using DV.Utils; -using Multiplayer.Components.Networking; -using UnityEngine; - -namespace Multiplayer.Components.MainMenu; - -public class MultiplayerPane : MonoBehaviour -{ - // @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 - - private bool why; - - private string address; - private ushort port; - - private void OnEnable() - { - if (!why) - { - why = true; - return; - } - - ShowIpPopup(); - } - - private void ShowIpPopup() - { - Popup popup = MainMenuThingsAndStuff.Instance.ShowRenamePopup(); - if (popup == null) - return; - - popup.labelTMPro.text = Locale.SERVER_BROWSER__IP; - - popup.Closed += result => - { - if (result.closedBy == PopupClosedByAction.Abortion) - { - MainMenuThingsAndStuff.Instance.SwitchToDefaultMenu(); - return; - } - - if (!IPv4.IsMatch(result.data) && !IPv6.IsMatch(result.data)) - { - - string inputUrl = result.data; - - if (!inputUrl.StartsWith("http://") && !inputUrl.StartsWith("https://")) - { - inputUrl = "http://" + inputUrl; - } - - bool isValidURL = Uri.TryCreate(inputUrl, UriKind.Absolute, out Uri uriResult) - && (uriResult.Scheme == Uri.UriSchemeHttp || uriResult.Scheme == Uri.UriSchemeHttps); - - - if (isValidURL) - { - string domainName = ExtractDomainName(result.data); - try - { - IPHostEntry hostEntry = Dns.GetHostEntry(domainName); - IPAddress[] addresses = hostEntry.AddressList; - - if (addresses.Length > 0) - { - string address2 = addresses[0].ToString(); - - address = address2; - Multiplayer.Log(address); - - ShowPortPopup(); - return; - } - } - catch (Exception ex) - { - Multiplayer.LogError($"An error occurred: {ex.Message}"); - } - } - - ShowOkPopup(Locale.SERVER_BROWSER__IP_INVALID, ShowIpPopup); - return; - } - - address = result.data; - - ShowPortPopup(); - }; - } - - static string ExtractDomainName(string input) - { - if (input.StartsWith("http://")) - { - input = input.Substring(7); - } - else if (input.StartsWith("https://")) - { - input = input.Substring(8); - } - - int portIndex = input.IndexOf(':'); - if (portIndex != -1) - { - input = input.Substring(0, portIndex); - } - - return input; - } - - private void ShowPortPopup() - { - Popup popup = MainMenuThingsAndStuff.Instance.ShowRenamePopup(); - if (popup == null) - return; - - popup.labelTMPro.text = Locale.SERVER_BROWSER__PORT; - - popup.Closed += result => - { - if (result.closedBy == PopupClosedByAction.Abortion) - { - MainMenuThingsAndStuff.Instance.SwitchToDefaultMenu(); - return; - } - - if (!PORT.IsMatch(result.data)) - { - ShowOkPopup(Locale.SERVER_BROWSER__PORT_INVALID, ShowPortPopup); - return; - } - - port = ushort.Parse(result.data); - - ShowPasswordPopup(); - }; - } - - private void ShowPasswordPopup() - { - Popup popup = MainMenuThingsAndStuff.Instance.ShowRenamePopup(); - if (popup == null) - return; - - popup.labelTMPro.text = Locale.SERVER_BROWSER__PASSWORD; - - popup.Closed += result => - { - if (result.closedBy == PopupClosedByAction.Abortion) - { - MainMenuThingsAndStuff.Instance.SwitchToDefaultMenu(); - return; - } - - SingletonBehaviour.Instance.StartClient(address, port, result.data); - }; - } - - private static void ShowOkPopup(string text, Action onClick) - { - Popup popup = MainMenuThingsAndStuff.Instance.ShowOkPopup(); - if (popup == null) - return; - - popup.labelTMPro.text = text; - popup.Closed += _ => { onClick(); }; - } -} diff --git a/Multiplayer/Components/MainMenu/PopupTextInputFieldControllerNoValidation.cs b/Multiplayer/Components/MainMenu/PopupTextInputFieldControllerNoValidation.cs deleted file mode 100644 index b3c4016..0000000 --- a/Multiplayer/Components/MainMenu/PopupTextInputFieldControllerNoValidation.cs +++ /dev/null @@ -1,91 +0,0 @@ -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; - } - } - - //patch us in as the new handler for the dialogue - typeof(Popup).GetField("handler", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(popup, this); - - } - - private void Start() - { - field.onValueChanged.AddListener(new UnityAction(OnInputValueChanged)); - OnInputValueChanged(field.text); - field.Select(); - field.ActivateInputField(); - - } - private void OnInputValueChanged(string value) - { - 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) - { - return true;// !string.IsNullOrWhiteSpace(value); - } - 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/ServerBrowserPane.cs b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs index f74576a..4d8570a 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs @@ -16,6 +16,7 @@ using Multiplayer.Networking.Data; using DV; using Multiplayer.Components.Networking.UI; +using System.Net; @@ -60,7 +61,7 @@ public class ServerBrowserPane : MonoBehaviour private const int REFRESH_MIN_TIME = 10; //Stop refresh spam //connection parameters - private string ipAddress; + private string address; private int portNumber; string password = null; bool direct = false; @@ -325,7 +326,7 @@ private void JoinAction() { //not making a direct connection direct = false; - ipAddress = selectedServer.ip; + address = selectedServer.ip; portNumber = selectedServer.port; ShowPasswordPopup(); @@ -413,7 +414,6 @@ private void UpdateDetailsPane() private void ShowIpPopup() { - Multiplayer.Log("In ShowIpPpopup"); var popup = MainMenuThingsAndStuff.Instance.ShowRenamePopup(); if (popup == null) { @@ -435,11 +435,46 @@ private void ShowIpPopup() if (!IPv4Regex.IsMatch(result.data) && !IPv6Regex.IsMatch(result.data)) { + string inputUrl = result.data; + + if (!inputUrl.StartsWith("http://") && !inputUrl.StartsWith("https://")) + { + inputUrl = "http://" + inputUrl; + } + + bool isValidURL = Uri.TryCreate(inputUrl, UriKind.Absolute, out Uri uriResult) + && (uriResult.Scheme == Uri.UriSchemeHttp || uriResult.Scheme == Uri.UriSchemeHttps); + + if (isValidURL) + { + string domainName = ExtractDomainName(result.data); + try + { + IPHostEntry hostEntry = Dns.GetHostEntry(domainName); + IPAddress[] addresses = hostEntry.AddressList; + + if (addresses.Length > 0) + { + string address2 = addresses[0].ToString(); + + address = address2; + Multiplayer.Log(address); + + ShowPortPopup(); + return; + } + } + catch (Exception ex) + { + Multiplayer.LogError($"An error occurred: {ex.Message}"); + } + } + ShowOkPopup(Locale.SERVER_BROWSER__IP_INVALID, ShowIpPopup); } else { - ipAddress = result.data; + address = result.data; ShowPortPopup(); } }; @@ -514,13 +549,13 @@ private void ShowPasswordPopup() if (direct) { //store params for later - Multiplayer.Settings.LastRemoteIP = ipAddress; + Multiplayer.Settings.LastRemoteIP = address; Multiplayer.Settings.LastRemotePort = portNumber; Multiplayer.Settings.LastRemotePassword = result.data; } - SingletonBehaviour.Instance.StartClient(ipAddress, portNumber, result.data, false); + SingletonBehaviour.Instance.StartClient(address, portNumber, result.data, false); //ShowConnectingPopup(); // Show a connecting message //SingletonBehaviour.Instance.ConnectionFailed += HandleConnectionFailed; @@ -656,6 +691,26 @@ private void FillDummyServers() gridView.SetModel(gridViewModel); } + + private string ExtractDomainName(string input) + { + if (input.StartsWith("http://")) + { + input = input.Substring(7); + } + else if (input.StartsWith("https://")) + { + input = input.Substring(8); + } + + int portIndex = input.IndexOf(':'); + if (portIndex != -1) + { + input = input.Substring(0, portIndex); + } + + return input; + } } diff --git a/Multiplayer/Settings.cs b/Multiplayer/Settings.cs index 2ca75d3..9b4ce99 100644 --- a/Multiplayer/Settings.cs +++ b/Multiplayer/Settings.cs @@ -46,18 +46,7 @@ public class Settings : UnityModManager.ModSettings, IDrawable 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("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.")] From 5f8e7305f43a067147fe8ab1f64c872d9d3f4050 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 14 Jul 2024 13:56:32 +1000 Subject: [PATCH 042/188] Add CSV parsing check and log --- Multiplayer/Utils/Csv.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Multiplayer/Utils/Csv.cs b/Multiplayer/Utils/Csv.cs index fcb12e5..13943da 100644 --- a/Multiplayer/Utils/Csv.cs +++ b/Multiplayer/Utils/Csv.cs @@ -40,6 +40,13 @@ public static ReadOnlyDictionary> Parse(strin continue; string rowKey = values[0]; + + //ensure we don't have too many columns + if (values.Count > columns.Count) + { + Multiplayer.LogWarning($"CSV Line {i + 1}: Found {values.Count} columns, expected {columns.Count}\r\n\t{line}"); + continue; + } // Add the row values to the appropriate column dictionaries for (int j = 0; j < values.Count && j < keys.Count; j++) From 13a265facb3cf869028db82c9e2deec268de7fb4 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 14 Jul 2024 14:24:50 +1000 Subject: [PATCH 043/188] Preparing for v0.16.0 --- Multiplayer/Multiplayer.csproj | 23 ++++++++++++++-- info.json | 18 ++++++------ package.ps1 | 28 ------------------- post-build.ps1 | 50 ++++++++++++++++++++++++++++++++++ 4 files changed, 81 insertions(+), 38 deletions(-) delete mode 100644 package.ps1 create mode 100644 post-build.ps1 diff --git a/Multiplayer/Multiplayer.csproj b/Multiplayer/Multiplayer.csproj index a10601f..d0aa83d 100644 --- a/Multiplayer/Multiplayer.csproj +++ b/Multiplayer/Multiplayer.csproj @@ -3,6 +3,8 @@ net48 latest Multiplayer + 0.1.6.0 + 0.1.6.0 @@ -83,12 +85,29 @@ - + + + + + + + + + + + + + + + + + + diff --git a/info.json b/info.json index 43accd0..06bd263 100644 --- a/info.json +++ b/info.json @@ -1,10 +1,12 @@ { - "Id": "Multiplayer", - "Version": "0.1.5.2", - "DisplayName": "Multiplayer", - "Author": "Insprill, Macka, Morm", - "EntryMethod": "Multiplayer.Multiplayer.Load", - "ManagerVersion": "0.27.3", - "LoadAfter": [ "RemoteDispatch" ], - "Repository": "https://www.andrewcraigmackenzie.com/unitymods/Releases.json" + "Id": "Multiplayer", + "Version": "0.1.6.0", + "DisplayName": "Multiplayer", + "Author": "Insprill, Macka, Morm", + "EntryMethod": "Multiplayer.Multiplayer.Load", + "ManagerVersion": "0.27.3", + "LoadAfter": [ + "RemoteDispatch" + ], + "Repository": "https://www.andrewcraigmackenzie.com/unitymods/Releases.json" } diff --git a/package.ps1 b/package.ps1 deleted file mode 100644 index 474e0e5..0000000 --- a/package.ps1 +++ /dev/null @@ -1,28 +0,0 @@ -param ( - [switch]$NoArchive, - [string]$OutputDirectory = $PSScriptRoot -) - -Set-Location "$PSScriptRoot" -$FilesToInclude = "build/*" - -$modInfo = Get-Content -Raw -Path "info.json" | ConvertFrom-Json -$modId = $modInfo.Id -$modVersion = $modInfo.Version - -$DistDir = "$OutputDirectory/dist" -if ($NoArchive) { - $ZipWorkDir = "$OutputDirectory" -} else { - $ZipWorkDir = "$DistDir/tmp" -} -$ZipOutDir = "$ZipWorkDir/$modId" - -New-Item "$ZipOutDir" -ItemType Directory -Force -Copy-Item -Force -Path $FilesToInclude -Destination "$ZipOutDir" - -if (!$NoArchive) -{ - $FILE_NAME = "$DistDir/${modId}_v$modVersion.zip" - Compress-Archive -Update -CompressionLevel Fastest -Path "$ZipOutDir/*" -DestinationPath "$FILE_NAME" -} diff --git a/post-build.ps1 b/post-build.ps1 new file mode 100644 index 0000000..ae93e0b --- /dev/null +++ b/post-build.ps1 @@ -0,0 +1,50 @@ +param +( + [switch]$NoArchive, + [string]$GameDir, + [string]$Target, + [string]$Ver +) + +Write-Host "Root: $PSScriptRoot" +Write-Host "No Archive: $NoArchive" +Write-Host "Target: $Target" +Write-Host "Game Dir: $GameDir" +Write-Host "Version: $Ver" + +$compress + +#Update the JSON +$json = Get-Content ($PSScriptRoot + '/info.json') -raw | ConvertFrom-Json +$json.Version = $Ver +$modId = $json.Id +$json | ConvertTo-Json -depth 32| set-content ($PSScriptRoot + '/info.json') + +#Copy files to Build Dir +Copy-Item ($PSScriptRoot + '/info.json') -Destination ("$PSScriptRoot/build/") +Copy-Item ($Target) -Destination ("$PSScriptRoot/build/") +Copy-Item ($PSScriptRoot + '/LICENSE') -Destination ("$PSScriptRoot/build/") + +#Copy files to Game Dir +if (!(Test-Path ($GameDir))) { + New-Item -ItemType Directory -Path $GameDir +} +Copy-Item ("$PSScriptRoot/build/*") -Destination ($GameDir) + + +#Files to be compressed if we make a zip +$compress = @{ + Path = ($PSScriptRoot + "/build/*") + CompressionLevel = "Fastest" + DestinationPath = ($PSScriptRoot + "/Releases/$modId $Ver.zip") +} + +#Are we building a release or debug? +if (!$NoArchive){ + if (!(Test-Path ($PSScriptRoot + "/releases"))) { + New-Item -ItemType Directory -Path ($PSScriptRoot + "/releases") + } + + Write-Host "Zip Path: " $compress.DestinationPath + Compress-Archive @compress -Force +} \ No newline at end of file From 97c10fe18b6ba5ee400603c8205e216879753272 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 14 Jul 2024 15:08:23 +1000 Subject: [PATCH 044/188] Fixed issue where PHP server does not return correct error code --- Lobby Servers/PHP Server/index.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Lobby Servers/PHP Server/index.php b/Lobby Servers/PHP Server/index.php index 556e828..1e70269 100644 --- a/Lobby Servers/PHP Server/index.php +++ b/Lobby Servers/PHP Server/index.php @@ -46,6 +46,7 @@ function add_game_server($db, $data) { if (!validate_server_info($data)) { + http_response_code(500); return json_encode(["error" => "Invalid server information"]); } @@ -61,6 +62,7 @@ function add_game_server($db, $data) { function update_game_server($db, $data) { if (!validate_server_update($db, $data)) { + http_response_code(500); return json_encode(["error" => "Invalid game server ID or private key"]); } @@ -70,6 +72,7 @@ function update_game_server($db, $data) { function remove_game_server($db, $data) { if (!validate_server_update($db, $data)) { + http_response_code(500); return json_encode(["error" => "Invalid game server ID or private key"]); } From 750958ebbdab07e9d963037bdbf020f5f9574200 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 14 Jul 2024 21:15:28 +1000 Subject: [PATCH 045/188] Fixed issues with servers that prioritise IPV6, disabled job sync code Game servers connecting to the lobby server will be requested to provide an IPv4 address also. The lobby server will now: return IPv6 addresses if the client makes an IPv6 connection and the game server has an IPv6 address. return IPv4 addresses if the client makes an IPv4 connection and the game server has an IPv4 address. Job sync code has been disabled to allow a working release to be made. Changed all uses of Networked() to TryNetworked() to fix null reference exceptions. --- Lobby Servers/PHP Server/FlatfileDatabase.php | 10 ++- Lobby Servers/PHP Server/MySQLDatabase.php | 8 +- Lobby Servers/PHP Server/index.php | 36 ++++++-- Lobby Servers/PHP Server/install.php | 3 +- Lobby Servers/RestAPI.md | 69 +++++++++------ Lobby Servers/Rust Server/src/handlers.rs | 85 +++++++++++++------ Lobby Servers/Rust Server/src/server.rs | 4 +- .../Components/MainMenu/ServerBrowserPane.cs | 1 - .../Train/NetworkTrainsetWatcher.cs | 9 +- .../Networking/Train/NetworkedTrainCar.cs | 8 +- .../Data/LobbyServerResponseData.cs | 5 +- .../Networking/Data/LobbyServerUpdateData.cs | 12 ++- .../Networking/Data/TrainsetSpawnPart.cs | 5 +- .../Managers/Client/NetworkClient.cs | 12 +-- .../Networking/Managers/NetworkManager.cs | 2 +- .../Managers/Server/LobbyServerManager.cs | 76 ++++++++++++++--- .../Managers/Server/NetworkServer.cs | 8 +- .../CommsRadio/CommsRadioCarDeleterPatch.cs | 6 +- .../CommsRadio/RerailControllerPatch.cs | 3 +- .../Patches/Jobs/JobOverviewUsePatch.cs | 3 +- .../Train/CargoModelControllerPatch.cs | 3 +- Multiplayer/Patches/Train/HoseAndCockPatch.cs | 6 +- .../Patches/Train/MultipleUnitCablePatch.cs | 4 +- Multiplayer/Settings.cs | 2 + Multiplayer/Utils/DvExtensions.cs | 14 +-- 25 files changed, 279 insertions(+), 115 deletions(-) diff --git a/Lobby Servers/PHP Server/FlatfileDatabase.php b/Lobby Servers/PHP Server/FlatfileDatabase.php index 9634991..5038f8c 100644 --- a/Lobby Servers/PHP Server/FlatfileDatabase.php +++ b/Lobby Servers/PHP Server/FlatfileDatabase.php @@ -28,7 +28,8 @@ public function addGameServer($data) { return json_encode([ "game_server_id" => $data['game_server_id'], - "private_key" => $data['private_key'] + "private_key" => $data['private_key'], + "ipv4_request" => !isset($data['ipv4']) ]); } @@ -41,6 +42,11 @@ public function updateGameServer($data) { $server['current_players'] = $data['current_players']; $server['time_passed'] = $data['time_passed']; $server['last_update'] = time(); // Update with current time + + if(isset($data['ipv4']) && filter_var($data['ipv4'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) && (!isset($server['ipv4']) || $server['ipv4'] === '')){ + $server['ipv4'] = $data['ipv4']; + } + $updated = true; break; } @@ -84,8 +90,6 @@ public function listGameServers() { return json_encode($active_servers); } - - public function getGameServer($game_server_id) { $servers = $this->readData(); foreach ($servers as $server) { diff --git a/Lobby Servers/PHP Server/MySQLDatabase.php b/Lobby Servers/PHP Server/MySQLDatabase.php index 32a774e..7810f4c 100644 --- a/Lobby Servers/PHP Server/MySQLDatabase.php +++ b/Lobby Servers/PHP Server/MySQLDatabase.php @@ -10,12 +10,13 @@ public function __construct($dbConfig) { } 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) + $stmt = $this->pdo->prepare("INSERT INTO game_servers (game_server_id, private_key, ipv4, ipv6, 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'], + ':ipv4' => isset($data['ipv4']) ? $data['ipv4'] : '', + ':ipv6' => isset($data['ipv6']) ? $data['ipv6'] : '', ':port' => $data['port'], ':server_name' => $data['server_name'], ':password_protected' => $data['password_protected'], @@ -32,7 +33,8 @@ public function addGameServer($data) { ]); return json_encode([ "game_server_id" => $data['game_server_id'], - "private_key" => $data['private_key'] + "private_key" => $data['private_key'], + "ipv4_request" => !isset($data['ipv4']) ]); } diff --git a/Lobby Servers/PHP Server/index.php b/Lobby Servers/PHP Server/index.php index 1e70269..a18569d 100644 --- a/Lobby Servers/PHP Server/index.php +++ b/Lobby Servers/PHP Server/index.php @@ -46,23 +46,24 @@ function add_game_server($db, $data) { if (!validate_server_info($data)) { - http_response_code(500); return json_encode(["error" => "Invalid server information"]); } - if (!isset($data['ip']) || !filter_var($data['ip'], FILTER_VALIDATE_IP)) { - $data['ip'] = $_SERVER['REMOTE_ADDR']; + if(filter_var($_SERVER['REMOTE_ADDR'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)){ + $data['ipv4'] = $_SERVER['REMOTE_ADDR']; + }elseif(filter_var($_SERVER['REMOTE_ADDR'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)){ + $data['ipv6'] = $_SERVER['REMOTE_ADDR']; } $data['game_server_id'] = uuid_create(); $data['private_key'] = generate_private_key(); - return $db->addGameServer($data); + + return $response = $db->addGameServer($data); } function update_game_server($db, $data) { if (!validate_server_update($db, $data)) { - http_response_code(500); return json_encode(["error" => "Invalid game server ID or private key"]); } @@ -72,7 +73,6 @@ function update_game_server($db, $data) { function remove_game_server($db, $data) { if (!validate_server_update($db, $data)) { - http_response_code(500); return json_encode(["error" => "Invalid game server ID or private key"]); } @@ -81,10 +81,34 @@ function remove_game_server($db, $data) { function list_game_servers($db) { $servers = json_decode($db->listGameServers(), true); + // Remove private keys from the servers before returning + // and select the correct protocol version for the requestor foreach ($servers as &$server) { unset($server['private_key']); unset($server['last_update']); + + if(filter_var($_SERVER['REMOTE_ADDR'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)){ + if(!isset($server['ipv4'])){ + $server['ip'] = ''; + }else{ + $server['ip'] = $server['ipv4']; + } + + unset($server['ipv4']); + unset($server['ipv6']); + + }elseif(filter_var($_SERVER['REMOTE_ADDR'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)){ + if(!isset($server['ipv6'])){ + $server['ip'] = $server['ipv4']; + }else{ + $server['ip'] = $server['ipv6']; + unset($server['ipv6']); + } + + unset($server['ipv4']); + unset($server['ipv6']); + } } return json_encode($servers); } diff --git a/Lobby Servers/PHP Server/install.php b/Lobby Servers/PHP Server/install.php index f383314..f323dcb 100644 --- a/Lobby Servers/PHP Server/install.php +++ b/Lobby Servers/PHP Server/install.php @@ -27,7 +27,8 @@ 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, + ipv4 VARCHAR(45) NOT NULL, + ipv6 VARCHAR(45) NOT NULL, port INT NOT NULL, server_name VARCHAR(100) NOT NULL, password_protected BOOLEAN NOT NULL, diff --git a/Lobby Servers/RestAPI.md b/Lobby Servers/RestAPI.md index 4309b2c..5045188 100644 --- a/Lobby Servers/RestAPI.md +++ b/Lobby Servers/RestAPI.md @@ -37,15 +37,14 @@ The difficulty field in the request body for adding a game server must be one of - **Request Body:** ```json { - "ip": "string", - "port": "integer", + "port": "integer", "server_name": "string", - "password_protected": "boolean", + "password_protected": "boolean", "game_mode": "integer", "difficulty": "integer", "time_passed": "string", - "current_players": "integer", - "max_players": "integer", + "current_players": "integer", + "max_players": "integer", "required_mods": "string", "game_version": "string", "multiplayer_version": "string", @@ -53,7 +52,6 @@ The difficulty field in the request body for adding a game server must be one of } ``` - **Fields:** - - 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. @@ -73,12 +71,14 @@ The difficulty field in the request body for adding a game server must be one of - **Content:** ```json { - "game_server_id": "string", - "private_key": "string" + "game_server_id": "string", + "private_key": "string", + "ipv4_request": "bool" } ``` - 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. + - ipv4_request (bool): A request to provide an IPV4 address. If `true`, the game server's IPV4 address should be provided via a call to the Update Server end point. - **Error:** - **Code:** 500 Internal Server Error - **Content:** `"Failed to add server"` @@ -91,17 +91,19 @@ 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", - "current_players": "integer", - "time_passed": "string" + "current_players": "integer", + "time_passed": "string", + "ipv4": "string" } ``` - - **Fields:** + - **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. + - ipv4 (optional string): The game server's public IPV4 address (if exists). Only provide if `ipv4_request` is `true`. - **Response:** - **Success:** - **Code:** 200 OK @@ -122,7 +124,7 @@ The difficulty field in the request body for adding a game server must be one of "private_key": "string" } ``` - - **Fields:** + - **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:** @@ -151,17 +153,36 @@ 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", "game_version": "string", "multiplayer_version": "string", - "server_info": "string" + "server_info": "string", + "game_server_id": "string" }, ... ] ``` + - **Fields:** + - ip (string): The IP address of the game server. + Note: if the server has both an IPV4 and IPV6, the returned IP will depend on the IP version of the requestor. + If the end point request is made using IPV4 and the game server does not have an IPV4 address, the server will return an empty string. + If the end point request is made using IPV6 and the game server does not have an IPV6 address, the server will return an IPV4 address. + - 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). + - game_server_id (string): The GUID assigned to the game server. - **Error:** - **Code:** 500 Internal Server Error - **Content:** `"Failed to retrieve servers"` @@ -172,12 +193,12 @@ The difficulty field in the request body for adding a game server must be one of Example request: ```bash curl -X POST -H "Content-Type: application/json" -d '{ - "ip": "127.0.0.1", - "port": 7777, + "ip": "127.0.0.1", + "port": 7777, "server_name": "My Derail Valley Server", - "password_protected": false, - "current_players": 1, - "max_players": 10, + "password_protected": false, + "current_players": 1, + "max_players": 10, "game_mode": 0, "difficulty": 0, "time_passed": "0d 10h 45m 12s", @@ -199,10 +220,10 @@ 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", - "current_players": 2, - "time_passed": "0d 10h 47m 12s" + "current_players": 2, + "time_passed": "0d 10h 47m 12s" }' http:///update_game_server ``` Example response: @@ -215,7 +236,7 @@ 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 ``` diff --git a/Lobby Servers/Rust Server/src/handlers.rs b/Lobby Servers/Rust Server/src/handlers.rs index 76eec8d..36961fd 100644 --- a/Lobby Servers/Rust Server/src/handlers.rs +++ b/Lobby Servers/Rust Server/src/handlers.rs @@ -7,7 +7,6 @@ use uuid::Uuid; #[derive(Deserialize)] pub struct AddServerRequest { - pub ip: Option, pub port: u16, pub server_name: String, pub password_protected: bool, @@ -25,20 +24,15 @@ pub struct AddServerRequest { 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 (ipv4, ipv6): (String, String) = match client_ip { + IpAddr::V4(ipv4) => (ipv4.to_string(), String::new()), // IPv4 case + IpAddr::V6(ipv6) => (String::new(), ipv6.to_string()), // IPv6 case }; let private_key = generate_private_key(); // Generate a private key let info = ServerInfo { - ip: ip.clone(), + ipv4: ipv4.clone(), + ipv6: ipv6.clone(), port: server_info.port, server_name: server_info.server_name.clone(), password_protected: server_info.password_protected, @@ -62,11 +56,13 @@ pub async fn add_server(data: web::Data, server_info: web::Json { servers.insert(key.clone(), info); log::info!("Server added: {}", key); - HttpResponse::Ok().json(AddServerResponse { game_server_id: key, private_key }) + HttpResponse::Ok().json(AddServerResponse { game_server_id: key, private_key, ipv4_request }) } Err(_) => { log::error!("Failed to add server: {}", key); @@ -81,6 +77,7 @@ pub struct UpdateServerRequest { pub private_key: String, pub current_players: u32, pub time_passed: String, + pub ipv4: Option, } pub async fn update_server(data: web::Data, server_info: web::Json) -> impl Responder { @@ -93,6 +90,18 @@ pub async fn update_server(data: web::Data, server_info: web::Json() { + if let IpAddr::V4(_) = ip { + info.ipv4 = ipv4_str.clone(); + } + } + } + } + updated = true; } } else { @@ -149,25 +158,45 @@ pub async fn remove_server(data: web::Data, server_info: web::Json) -> impl Responder { +pub async fn list_servers(data: web::Data, req: HttpRequest) -> impl Responder { + let client_ip = req.connection_info().realip_remote_addr().unwrap_or("unknown").to_string(); + + let ip_version = match client_ip { + IpAddr::V4(_) => "IPv4", + IpAddr::V6(_) => "IPv6", + }; + 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(), + let public_servers: Vec = servers.iter().map(|(id, info)| { + let ip = match ip_version { + "IPv4" => info.ipv4.clone(), + "IPv6" => if info.ipv6 != String::new() { + info.ipv6.clone() + } else { + info.ipv4.clone() + }, + _ => info.ipv4.clone(), // Default to IPv4 if something goes wrong + }; + + PublicServerInfo { + id: id.clone(), + ip: ip, + 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/server.rs b/Lobby Servers/Rust Server/src/server.rs index 3ffa009..a4c9abc 100644 --- a/Lobby Servers/Rust Server/src/server.rs +++ b/Lobby Servers/Rust Server/src/server.rs @@ -2,7 +2,8 @@ use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Clone)] pub struct ServerInfo { - pub ip: String, + pub ipv4: String, + pub ipv6: String, pub port: u16, pub server_name: String, pub password_protected: bool, @@ -43,6 +44,7 @@ pub struct PublicServerInfo { pub struct AddServerResponse { pub game_server_id: String, pub private_key: String, + pub ipv4_request: bool, } pub fn validate_server_info(info: &ServerInfo) -> Result<(), &'static str> { diff --git a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs index c453427..01cb8de 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs @@ -15,7 +15,6 @@ using System.Linq; using Multiplayer.Networking.Data; using DV; -using Multiplayer.Components.Networking.UI; using System.Net; namespace Multiplayer.Components.MainMenu diff --git a/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs b/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs index 03ee184..af7566f 100644 --- a/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs +++ b/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs @@ -71,14 +71,14 @@ private void Server_TickSet(Trainset set) for (int i = 0; i < set.cars.Count; i++) { TrainCar trainCar = set.cars[i]; - if (!trainCar.TryNetworked(out NetworkedTrainCar _)) + if (!trainCar.TryNetworked(out NetworkedTrainCar networkedTrainCar)) { Multiplayer.LogDebug(() => $"TrainCar UNKNOWN is not networked! Is active? {trainCar.gameObject.activeInHierarchy}"); Multiplayer.LogDebug(() => $"TrainCar {trainCar.ID} is not networked! Is active? {trainCar.gameObject.activeInHierarchy}"); continue; } - NetworkedTrainCar networkedTrainCar = trainCar.Networked(); + //NetworkedTrainCar networkedTrainCar = trainCar.Networked(); anyTracksDirty |= networkedTrainCar.BogieTracksDirty; if (trainCar.derailed) @@ -117,7 +117,10 @@ public void Client_HandleTrainsetPhysicsUpdate(ClientboundTrainsetPhysicsPacket } for (int i = 0; i < packet.TrainsetParts.Length; i++) - set.cars[i].Networked().Client_ReceiveTrainPhysicsUpdate(in packet.TrainsetParts[i], packet.Tick); + { + if(set.cars[i].TryNetworked(out NetworkedTrainCar networkedTrainCar)) + networkedTrainCar.Client_ReceiveTrainPhysicsUpdate(in packet.TrainsetParts[i], packet.Tick); + } } #endregion diff --git a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs index c50a714..9ec44bf 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs @@ -43,10 +43,10 @@ public static Coupler GetCoupler(HoseAndCock hoseAndCock) return hoseToCoupler[hoseAndCock]; } - public static NetworkedTrainCar GetFromTrainCar(TrainCar trainCar) - { - return trainCarsToNetworkedTrainCars[trainCar]; - } + //public static NetworkedTrainCar GetFromTrainCar(TrainCar trainCar) + //{ + // return trainCarsToNetworkedTrainCars[trainCar]; + //} public static bool GetFromTrainId(string carId, out NetworkedTrainCar networkedTrainCar) { return trainCarIdToNetworkedTrainCars.TryGetValue(carId, out networkedTrainCar); diff --git a/Multiplayer/Networking/Data/LobbyServerResponseData.cs b/Multiplayer/Networking/Data/LobbyServerResponseData.cs index 70d093b..5654149 100644 --- a/Multiplayer/Networking/Data/LobbyServerResponseData.cs +++ b/Multiplayer/Networking/Data/LobbyServerResponseData.cs @@ -13,11 +13,14 @@ public class LobbyServerResponseData public string game_server_id { get; set; } public string private_key { get; set; } + [JsonIgnore] + public bool? ipv4_request{ get; set; } - public LobbyServerResponseData(string game_server_id, string private_key) + public LobbyServerResponseData(string game_server_id, string private_key, bool? ipv4_request = null) { this.game_server_id = game_server_id; this.private_key = private_key; + this.ipv4_request = ipv4_request; } } } diff --git a/Multiplayer/Networking/Data/LobbyServerUpdateData.cs b/Multiplayer/Networking/Data/LobbyServerUpdateData.cs index f592f9a..c4c80a1 100644 --- a/Multiplayer/Networking/Data/LobbyServerUpdateData.cs +++ b/Multiplayer/Networking/Data/LobbyServerUpdateData.cs @@ -1,10 +1,5 @@ -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 { @@ -21,13 +16,16 @@ public class LobbyServerUpdateData [JsonProperty("current_players")] public int CurrentPlayers { get; set; } + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public string ipv4 { get; set; } - public LobbyServerUpdateData(string game_server_id, string private_key, string timePassed,int currentPlayers) + public LobbyServerUpdateData(string game_server_id, string private_key, string timePassed,int currentPlayers, string ipv4 = null) { this.game_server_id = game_server_id; this.private_key = private_key; this.TimePassed = timePassed; this.CurrentPlayers = currentPlayers; + this.ipv4 = ipv4; } diff --git a/Multiplayer/Networking/Data/TrainsetSpawnPart.cs b/Multiplayer/Networking/Data/TrainsetSpawnPart.cs index 5d6b6cd..d00e5bd 100644 --- a/Multiplayer/Networking/Data/TrainsetSpawnPart.cs +++ b/Multiplayer/Networking/Data/TrainsetSpawnPart.cs @@ -96,7 +96,10 @@ public static TrainsetSpawnPart[] FromTrainSet(Trainset trainset) { TrainsetSpawnPart[] parts = new TrainsetSpawnPart[trainset.cars.Count]; for (int i = 0; i < trainset.cars.Count; i++) - parts[i] = FromTrainCar(trainset.cars[i].Networked()); + { + if(trainset.cars[i].TryNetworked(out NetworkedTrainCar networkedTrainCar)) + parts[i] = FromTrainCar(networkedTrainCar); + } return parts; } } diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index d358386..6e94ca1 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -118,9 +118,9 @@ protected override void Subscribe() netPacketProcessor.SubscribeReusable(OnClientboundLicenseAcquiredPacket); netPacketProcessor.SubscribeReusable(OnClientboundGarageUnlockPacket); netPacketProcessor.SubscribeReusable(OnClientboundDebtStatusPacket); - netPacketProcessor.SubscribeReusable(OnClientboundJobsPacket); - netPacketProcessor.SubscribeReusable(OnClientboundJobCreatePacket); - netPacketProcessor.SubscribeReusable(OnClientboundJobTakeResponsePacket); + //netPacketProcessor.SubscribeReusable(OnClientboundJobsPacket); + //netPacketProcessor.SubscribeReusable(OnClientboundJobCreatePacket); + //netPacketProcessor.SubscribeReusable(OnClientboundJobTakeResponsePacket); netPacketProcessor.SubscribeReusable(OnCommonChatPacket); } @@ -624,6 +624,7 @@ private void OnCommonChatPacket(CommonChatPacket packet) chatGUI.ReceiveMessage(packet.message); } + /* Temp for stable release private void OnClientboundJobCreatePacket(ClientboundJobCreatePacket packet) { if (NetworkLifecycle.Instance.IsHost()) @@ -732,7 +733,7 @@ private void OnClientboundJobTakeResponsePacket(ClientboundJobTakeResponsePacket networkedJob.jobValidator = null; networkedJob.jobOverview = null; } - + */ #endregion #region Senders @@ -951,6 +952,7 @@ public void SendLicensePurchaseRequest(string id, bool isJobLicense) }, DeliveryMethod.ReliableUnordered); } + /* Temp for stable release public void SendJobTakeRequest(ushort netId) { SendPacketToServer(new ServerboundJobTakeRequestPacket @@ -958,7 +960,7 @@ public void SendJobTakeRequest(ushort netId) netId = netId }, DeliveryMethod.ReliableUnordered); } - +*/ public void SendChat(string message) { SendPacketToServer(new CommonChatPacket diff --git a/Multiplayer/Networking/Managers/NetworkManager.cs b/Multiplayer/Networking/Managers/NetworkManager.cs index 0a88130..f54520d 100644 --- a/Multiplayer/Networking/Managers/NetworkManager.cs +++ b/Multiplayer/Networking/Managers/NetworkManager.cs @@ -37,7 +37,7 @@ protected NetworkManager(Settings settings) private void RegisterNestedTypes() { netPacketProcessor.RegisterNestedType(BogieData.Serialize, BogieData.Deserialize); - netPacketProcessor.RegisterNestedType(JobData.Serialize, JobData.Deserialize); + /* Temp for stable releasenetPacketProcessor.RegisterNestedType(JobData.Serialize, JobData.Deserialize);*/ netPacketProcessor.RegisterNestedType(ModInfo.Serialize, ModInfo.Deserialize); netPacketProcessor.RegisterNestedType(RigidbodySnapshot.Serialize, RigidbodySnapshot.Deserialize); netPacketProcessor.RegisterNestedType(StationsChainDataData.Serialize, StationsChainDataData.Deserialize); diff --git a/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs b/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs index 90f9ce8..2a82877 100644 --- a/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs +++ b/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs @@ -7,6 +7,7 @@ using UnityEngine.Networking; using Multiplayer.Components.Networking; using DV.WeatherSystem; +using System.Text.RegularExpressions; namespace Multiplayer.Networking.Managers.Server; public class LobbyServerManager : MonoBehaviour @@ -16,6 +17,9 @@ public class LobbyServerManager : MonoBehaviour private const string ENDPOINT_UPDATE_SERVER = "update_game_server"; private const string ENDPOINT_REMOVE_SERVER = "remove_game_server"; + //RegEx + private readonly Regex IPv4Match = new Regex(@"\b(?:(?:2[0-5]{2}|1[0-9]{2}|[1-9]?[0-9])\.){3}(?:2[0-5]{2}|1[0-9]{2}|[1-9]?[0-9])\b"); + 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 @@ -23,8 +27,11 @@ public class LobbyServerManager : MonoBehaviour 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 string server_id { get; set; } + private string private_key { get; set; } + private string myIPv4 { get; set; } + private bool updateIPv4 { get; set; } = false; + private bool sendUpdates = false; private float timePassed = 0f; @@ -85,6 +92,14 @@ private IEnumerator RegisterWithLobbyServer(string uri) { private_key = response.private_key; server_id = response.game_server_id; + + //Check if we are using IPv6 to talk to the lobby server + if(response.ipv4_request == true) + { + //We are using IPv6, so now we will need to make a request to an IPv4 service, then update the lobby server with our IPv4 IP + StartCoroutine(GetIPv4($"{Multiplayer.Settings.Ipv4AddressCheck}")); + } + sendUpdates = true; } }, @@ -114,13 +129,21 @@ private IEnumerator UpdateLobbyServer(string uri) DateTime current = WeatherDriver.Instance.manager.DateTime; TimeSpan inGame = current - start; - string json = JsonConvert.SerializeObject(new LobbyServerUpdateData( - server_id, - private_key, - inGame.ToString("d\\d\\ hh\\h\\ mm\\m\\ ss\\s"), - server.serverData.CurrentPlayers), - jsonSettings - ); + LobbyServerUpdateData reqData = new LobbyServerUpdateData( + server_id, + private_key, + inGame.ToString("d\\d\\ hh\\h\\ mm\\m\\ ss\\s"), + server.serverData.CurrentPlayers + ); + + //do we need to provide our IPv4? + if(updateIPv4 && myIPv4 != null && myIPv4 != string.Empty) + { + reqData.ipv4 = myIPv4; + updateIPv4 = false; + } + + string json = JsonConvert.SerializeObject(reqData, jsonSettings); Multiplayer.LogDebug(() => $"UpdateLobbyServer JsonRequest: {json}"); yield return SendWebRequest( @@ -141,6 +164,36 @@ private IEnumerator UpdateLobbyServer(string uri) } ); } + + private IEnumerator GetIPv4(string uri) + { + + Multiplayer.Log("Preparing to get IPv4"); + + yield return SendWebRequest( + uri, + string.Empty, + webRequest => + { + Match match = IPv4Match.Match(webRequest.downloadHandler.text); + if (match != null) + { + Multiplayer.Log($"IPv4 address extracted: {match.Value}"); + myIPv4 = match.Value; + updateIPv4 = true; + StopAllCoroutines(); + StartCoroutine(UpdateLobbyServer($"{Multiplayer.Settings.LobbyServerAddress}/{ENDPOINT_UPDATE_SERVER}")); + } + else + { + Multiplayer.LogError($"Failed to find IPv4 address. Server will only be available via IPv6"); + } + + }, + webRequest => Multiplayer.LogError("Failed to remove from lobby server") + ); + } + private IEnumerator SendWebRequest(string uri, string json, Action onSuccess, Action onError, int depth=0) { if (depth > REDIRECT_MAX) @@ -153,7 +206,10 @@ private IEnumerator SendWebRequest(string uri, string json, Action 0) + { + webRequest.uploadHandler = new UploadHandlerRaw(System.Text.Encoding.UTF8.GetBytes(json)) { contentType = "application/json" }; + } webRequest.downloadHandler = new DownloadHandlerBuffer(); yield return webRequest.SendWebRequest(); diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index 2bee782..a689e3b 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Net; using DV; using DV.InventorySystem; using DV.Logic.Job; @@ -29,7 +28,6 @@ using Multiplayer.Utils; using UnityEngine; using UnityModManagerNet; -using Unity.Jobs; namespace Multiplayer.Networking.Listeners; @@ -519,6 +517,7 @@ private void OnServerboundClientReadyPacket(ServerboundClientReadyPacket packet, SendPacket(peer, ClientboundSpawnTrainSetPacket.FromTrainSet(set), DeliveryMethod.ReliableOrdered); } + /* Temp for stable release //send jobs - do we need a job manager/job IDs to make this easier? foreach(StationController station in StationController.allStations) { @@ -541,7 +540,7 @@ private void OnServerboundClientReadyPacket(ServerboundClientReadyPacket packet, DeliveryMethod.ReliableOrdered ); - } + }*/ // Send existing players @@ -781,8 +780,10 @@ private void OnServerboundLicensePurchaseRequestPacket(ServerboundLicensePurchas LicenseManager.Instance.AcquireGeneralLicense(generalLicense); } + private void OnServerboundJobTakeRequestPacket(ServerboundJobTakeRequestPacket packet, NetPeer peer) { + /* Temp for stable release NetworkedJob networkedJob; if (!NetworkedJob.Get(packet.netId, out networkedJob)) @@ -813,6 +814,7 @@ private void OnServerboundJobTakeRequestPacket(ServerboundJobTakeRequestPacket p SendPacket(peer, new ClientboundJobTakeResponsePacket { netId = packet.netId, granted = true, playerId = player.Id }, DeliveryMethod.ReliableOrdered); } + */ } private void OnCommonChatPacket(CommonChatPacket packet, NetPeer peer) diff --git a/Multiplayer/Patches/CommsRadio/CommsRadioCarDeleterPatch.cs b/Multiplayer/Patches/CommsRadio/CommsRadioCarDeleterPatch.cs index 0cd194a..f89ec5d 100644 --- a/Multiplayer/Patches/CommsRadio/CommsRadioCarDeleterPatch.cs +++ b/Multiplayer/Patches/CommsRadio/CommsRadioCarDeleterPatch.cs @@ -3,6 +3,7 @@ using DV.InventorySystem; using HarmonyLib; using Multiplayer.Components.Networking; +using Multiplayer.Components.Networking.Train; using Multiplayer.Utils; using UnityEngine; @@ -21,7 +22,8 @@ private static bool OnUse_Prefix(CommsRadioCarDeleter __instance) return true; if (Inventory.Instance.PlayerMoney < __instance.removePrice) return true; - if (__instance.carToDelete.Networked().HasPlayers) + + if (__instance.carToDelete.TryNetworked(out NetworkedTrainCar networkedTrainCar) && networkedTrainCar.HasPlayers) { CommsRadioController.PlayAudioFromRadio(__instance.cancelSound, __instance.transform); __instance.ClearFlags(); @@ -57,7 +59,7 @@ private static bool OnUpdate_Prefix(CommsRadioCarDeleter __instance) if (!Physics.Raycast(__instance.signalOrigin.position, __instance.signalOrigin.forward, out __instance.hit, CommsRadioCarDeleter.SIGNAL_RANGE, __instance.trainCarMask)) return true; TrainCar car = TrainCar.Resolve(__instance.hit.transform.root); - if (car != null && !car.Networked().HasPlayers) + if (car != null && car.TryNetworked(out NetworkedTrainCar networkedTrainCar) && !networkedTrainCar.HasPlayers) return true; __instance.PointToCar(null); return false; diff --git a/Multiplayer/Patches/CommsRadio/RerailControllerPatch.cs b/Multiplayer/Patches/CommsRadio/RerailControllerPatch.cs index 06c4ab7..7982127 100644 --- a/Multiplayer/Patches/CommsRadio/RerailControllerPatch.cs +++ b/Multiplayer/Patches/CommsRadio/RerailControllerPatch.cs @@ -3,6 +3,7 @@ using DV.InventorySystem; using HarmonyLib; using Multiplayer.Components.Networking; +using Multiplayer.Components.Networking.Train; using Multiplayer.Components.Networking.World; using Multiplayer.Utils; using UnityEngine; @@ -57,7 +58,7 @@ private static bool OnUpdate_Prefix(RerailController __instance) if (!Physics.Raycast(__instance.signalOrigin.position, __instance.signalOrigin.forward, out __instance.hit, RerailController.SIGNAL_RANGE, __instance.trainCarMask)) return true; TrainCar car = TrainCar.Resolve(__instance.hit.transform.root); - if (car != null && car.IsRerailAllowed && !car.Networked().HasPlayers) + if (car != null && car.IsRerailAllowed && car.TryNetworked(out NetworkedTrainCar networkedTrainCar) && !networkedTrainCar.HasPlayers) return true; __instance.PointToCar(null); return false; diff --git a/Multiplayer/Patches/Jobs/JobOverviewUsePatch.cs b/Multiplayer/Patches/Jobs/JobOverviewUsePatch.cs index 2ee4ab5..4687890 100644 --- a/Multiplayer/Patches/Jobs/JobOverviewUsePatch.cs +++ b/Multiplayer/Patches/Jobs/JobOverviewUsePatch.cs @@ -12,7 +12,7 @@ using Unity.Jobs; using UnityEngine; using static UnityEngine.GraphicsBuffer; - +/* Temp for stable release namespace Multiplayer.Patches.Jobs; //public void HandleUse(ItemUseTarget target) [HarmonyPatch(typeof(JobOverviewUse), nameof(JobOverviewUse.HandleUse))] @@ -64,3 +64,4 @@ private static bool Prefix(JobOverviewUse __instance, ItemUseTarget target, ref } } +*/ diff --git a/Multiplayer/Patches/Train/CargoModelControllerPatch.cs b/Multiplayer/Patches/Train/CargoModelControllerPatch.cs index 4e70550..70e2f5c 100644 --- a/Multiplayer/Patches/Train/CargoModelControllerPatch.cs +++ b/Multiplayer/Patches/Train/CargoModelControllerPatch.cs @@ -19,8 +19,9 @@ private static bool Prefix(CargoModelController __instance) private static IEnumerator AddCargoOnceInitialized(CargoModelController controller) { NetworkedTrainCar networkedTrainCar; - while ((networkedTrainCar = controller.trainCar.Networked()) == null) + while (!controller.trainCar.TryNetworked(out networkedTrainCar)) yield return null; + AddCargo(controller, networkedTrainCar); } diff --git a/Multiplayer/Patches/Train/HoseAndCockPatch.cs b/Multiplayer/Patches/Train/HoseAndCockPatch.cs index e2b68fb..bb1b70c 100644 --- a/Multiplayer/Patches/Train/HoseAndCockPatch.cs +++ b/Multiplayer/Patches/Train/HoseAndCockPatch.cs @@ -13,8 +13,12 @@ private static void Prefix(HoseAndCock __instance, bool open) { if (UnloadWatcher.isUnloading || NetworkLifecycle.Instance.IsProcessingPacket) return; + Coupler coupler = NetworkedTrainCar.GetCoupler(__instance); - NetworkedTrainCar networkedTrainCar = coupler.train.Networked(); + + if (coupler == null || !coupler.train.TryNetworked(out NetworkedTrainCar networkedTrainCar)) + return; + if (networkedTrainCar.IsDestroying) return; NetworkLifecycle.Instance.Client?.SendCockState(networkedTrainCar.NetId, coupler, open); diff --git a/Multiplayer/Patches/Train/MultipleUnitCablePatch.cs b/Multiplayer/Patches/Train/MultipleUnitCablePatch.cs index edb9dbf..90f752d 100644 --- a/Multiplayer/Patches/Train/MultipleUnitCablePatch.cs +++ b/Multiplayer/Patches/Train/MultipleUnitCablePatch.cs @@ -24,8 +24,8 @@ private static void Postfix(MultipleUnitCable __instance, bool playAudio) { if (NetworkLifecycle.Instance.IsProcessingPacket || UnloadWatcher.isUnloading) return; - NetworkedTrainCar networkedTrainCar = __instance.muModule.train.Networked(); - if (networkedTrainCar.IsDestroying) + + if (__instance.muModule.train.TryNetworked(out NetworkedTrainCar networkedTrainCar) && networkedTrainCar.IsDestroying) return; NetworkLifecycle.Instance.Client?.SendMuDisconnected(networkedTrainCar.NetId, __instance, playAudio); } diff --git a/Multiplayer/Settings.cs b/Multiplayer/Settings.cs index e2ffacb..e786b9f 100644 --- a/Multiplayer/Settings.cs +++ b/Multiplayer/Settings.cs @@ -38,6 +38,8 @@ public class Settings : UnityModManager.ModSettings, IDrawable [Header("Lobby Server")] [Draw("Lobby Server address", Tooltip = "Address of lobby server for finding multiplayer games")] public string LobbyServerAddress = "https://dv.mineit.space";//"http://localhost:8080"; + [Draw("IPv4 Check Address", Tooltip = "Do not modify unless the service is unavailable")] + public string Ipv4AddressCheck = "http://checkip.dyndns.org"; [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/Multiplayer/Utils/DvExtensions.cs b/Multiplayer/Utils/DvExtensions.cs index 5241d93..16121b5 100644 --- a/Multiplayer/Utils/DvExtensions.cs +++ b/Multiplayer/Utils/DvExtensions.cs @@ -18,16 +18,20 @@ public static class DvExtensions public static ushort GetNetId(this TrainCar car) { - ushort netId = car.Networked().NetId; + ushort netId = 0; + + if (car != null && car.TryNetworked(out NetworkedTrainCar networkedTrainCar)) + netId = networkedTrainCar.NetId; + if (netId == 0) throw new InvalidOperationException($"NetId for {car.carLivery.id} ({car.ID}) isn't initialized!"); return netId; } - public static NetworkedTrainCar Networked(this TrainCar trainCar) - { - return NetworkedTrainCar.GetFromTrainCar(trainCar); - } + //public static NetworkedTrainCar Networked(this TrainCar trainCar) + //{ + // return NetworkedTrainCar.GetFromTrainCar(trainCar); + //} public static bool TryNetworked(this TrainCar trainCar, out NetworkedTrainCar networkedTrainCar) { From 1ebb5f4ffd708a27af19de294e3d5491fd0d701a Mon Sep 17 00:00:00 2001 From: morm075 <124874578+morm075@users.noreply.github.com> Date: Sun, 14 Jul 2024 21:02:33 +0930 Subject: [PATCH 046/188] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 457cd00..bcda25e 100644 --- a/README.md +++ b/README.md @@ -101,8 +101,8 @@ See [LICENSE][license-url] for more information. [forks-url]: https://github.com/Insprill/dv-multiplayer/network/members [stars-shield]: https://img.shields.io/github/stars/Insprill/dv-multiplayer.svg?style=for-the-badge [stars-url]: https://github.com/Insprill/dv-multiplayer/stargazers -[issues-shield]: https://img.shields.io/github/issues/Insprill/dv-multiplayer.svg?style=for-the-badge -[issues-url]: https://github.com/Insprill/dv-multiplayer/issues +[issues-shield]: https://img.shields.io/github/issues/AMacro/dv-multiplayer.svg?style=for-the-badge +[issues-url]: https://github.com/AMacro/dv-multiplayer/issues [license-shield]: https://img.shields.io/github/license/Insprill/dv-multiplayer.svg?style=for-the-badge [license-url]: https://github.com/Insprill/dv-multiplayer/blob/master/LICENSE [altfuture-support-email-url]: mailto:support@altfuture.gg From d2f07aed61b3508bdc723a58bb9cfcc8e8b69c08 Mon Sep 17 00:00:00 2001 From: morm075 <124874578+morm075@users.noreply.github.com> Date: Sun, 14 Jul 2024 21:09:09 +0930 Subject: [PATCH 047/188] Update README.md --- README.md | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index bcda25e..1ce87c3 100644 --- a/README.md +++ b/README.md @@ -14,9 +14,9 @@ A Derail Valley mod that adds multiplayer.

- Report Bug + Report Bug · - Request Feature + Request Feature

@@ -46,6 +46,7 @@ Multiplayer is a Derail Valley mod that adds multiplayer to the game, allowing y It works by having one player host a game, and then other players can join that game. +Forked from https://github.com/Insprill/dv-multiplayer/ @@ -95,16 +96,16 @@ See [LICENSE][license-url] for more information. -[contributors-shield]: https://img.shields.io/github/contributors/Insprill/dv-multiplayer.svg?style=for-the-badge -[contributors-url]: https://github.com/Insprill/dv-multiplayer/graphs/contributors -[forks-shield]: https://img.shields.io/github/forks/Insprill/dv-multiplayer.svg?style=for-the-badge -[forks-url]: https://github.com/Insprill/dv-multiplayer/network/members -[stars-shield]: https://img.shields.io/github/stars/Insprill/dv-multiplayer.svg?style=for-the-badge -[stars-url]: https://github.com/Insprill/dv-multiplayer/stargazers +[contributors-shield]: https://img.shields.io/github/contributors/ACMacro/dv-multiplayer.svg?style=for-the-badge +[contributors-url]: https://github.com/ACMacro/dv-multiplayer/graphs/contributors +[forks-shield]: https://img.shields.io/github/forks/ACMacro/dv-multiplayer.svg?style=for-the-badge +[forks-url]: https://github.com/ACMacro/dv-multiplayer/network/members +[stars-shield]: https://img.shields.io/github/stars/ACMacro/dv-multiplayer.svg?style=for-the-badge +[stars-url]: https://github.com/ACMacro/dv-multiplayer/stargazers [issues-shield]: https://img.shields.io/github/issues/AMacro/dv-multiplayer.svg?style=for-the-badge [issues-url]: https://github.com/AMacro/dv-multiplayer/issues -[license-shield]: https://img.shields.io/github/license/Insprill/dv-multiplayer.svg?style=for-the-badge -[license-url]: https://github.com/Insprill/dv-multiplayer/blob/master/LICENSE +[license-shield]: https://img.shields.io/github/license/ACMacro/dv-multiplayer.svg?style=for-the-badge +[license-url]: https://github.com/ACMacro/dv-multiplayer/blob/master/LICENSE [altfuture-support-email-url]: mailto:support@altfuture.gg [contributing-quickstart-url]: https://docs.github.com/en/get-started/quickstart/contributing-to-projects [asset-studio-url]: https://github.com/Perfare/AssetStudio From 123cc2461fd9efad69e79317d67003c67739992f Mon Sep 17 00:00:00 2001 From: morm075 <124874578+morm075@users.noreply.github.com> Date: Sun, 14 Jul 2024 21:11:25 +0930 Subject: [PATCH 048/188] Update README.md --- README.md | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 1ce87c3..544928c 100644 --- a/README.md +++ b/README.md @@ -14,9 +14,9 @@ A Derail Valley mod that adds multiplayer.

- Report Bug + Report Bug · - Request Feature + Request Feature

@@ -46,7 +46,6 @@ Multiplayer is a Derail Valley mod that adds multiplayer to the game, allowing y It works by having one player host a game, and then other players can join that game. -Forked from https://github.com/Insprill/dv-multiplayer/ @@ -96,16 +95,16 @@ See [LICENSE][license-url] for more information. -[contributors-shield]: https://img.shields.io/github/contributors/ACMacro/dv-multiplayer.svg?style=for-the-badge -[contributors-url]: https://github.com/ACMacro/dv-multiplayer/graphs/contributors -[forks-shield]: https://img.shields.io/github/forks/ACMacro/dv-multiplayer.svg?style=for-the-badge -[forks-url]: https://github.com/ACMacro/dv-multiplayer/network/members -[stars-shield]: https://img.shields.io/github/stars/ACMacro/dv-multiplayer.svg?style=for-the-badge -[stars-url]: https://github.com/ACMacro/dv-multiplayer/stargazers +[contributors-shield]: https://img.shields.io/github/contributors/AMacro/dv-multiplayer.svg?style=for-the-badge +[contributors-url]: https://github.com/AMacro/dv-multiplayer/graphs/contributors +[forks-shield]: https://img.shields.io/github/forks/AMacro/dv-multiplayer.svg?style=for-the-badge +[forks-url]: https://github.com/AMacro/dv-multiplayer/network/members +[stars-shield]: https://img.shields.io/github/stars/AMacro/dv-multiplayer.svg?style=for-the-badge +[stars-url]: https://github.com/AMacro/dv-multiplayer/stargazers [issues-shield]: https://img.shields.io/github/issues/AMacro/dv-multiplayer.svg?style=for-the-badge [issues-url]: https://github.com/AMacro/dv-multiplayer/issues -[license-shield]: https://img.shields.io/github/license/ACMacro/dv-multiplayer.svg?style=for-the-badge -[license-url]: https://github.com/ACMacro/dv-multiplayer/blob/master/LICENSE +[license-shield]: https://img.shields.io/github/license/AMacro/dv-multiplayer.svg?style=for-the-badge +[license-url]: https://github.com/AMacro/dv-multiplayer/blob/master/LICENSE [altfuture-support-email-url]: mailto:support@altfuture.gg [contributing-quickstart-url]: https://docs.github.com/en/get-started/quickstart/contributing-to-projects [asset-studio-url]: https://github.com/Perfare/AssetStudio From 6ed3c82c9c459d43bdea80efdfb5f87ba96b1e7e Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 21 Jul 2024 11:34:41 +1000 Subject: [PATCH 049/188] Repair after data loss --- Lobby Servers/PHP Server/FlatfileDatabase.php | 5 -- Lobby Servers/PHP Server/index.php | 67 +++++++++++-------- .../ServerBrowserDummyElement.cs | 4 +- .../ServerBrowser/ServerBrowserElement.cs | 6 -- .../ServerBrowser/ServerBrowserGridView.cs | 5 +- .../Components/MainMenu/ServerBrowserPane.cs | 11 ++- .../Managers/Server/NetworkServer.cs | 10 +-- .../MainMenu/RightPaneControllerPatch.cs | 2 +- .../Patches/World/StationLocoSpawnerPatch.cs | 4 +- 9 files changed, 62 insertions(+), 52 deletions(-) diff --git a/Lobby Servers/PHP Server/FlatfileDatabase.php b/Lobby Servers/PHP Server/FlatfileDatabase.php index 5038f8c..c649d39 100644 --- a/Lobby Servers/PHP Server/FlatfileDatabase.php +++ b/Lobby Servers/PHP Server/FlatfileDatabase.php @@ -29,7 +29,6 @@ public function addGameServer($data) { return json_encode([ "game_server_id" => $data['game_server_id'], "private_key" => $data['private_key'], - "ipv4_request" => !isset($data['ipv4']) ]); } @@ -42,10 +41,6 @@ public function updateGameServer($data) { $server['current_players'] = $data['current_players']; $server['time_passed'] = $data['time_passed']; $server['last_update'] = time(); // Update with current time - - if(isset($data['ipv4']) && filter_var($data['ipv4'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) && (!isset($server['ipv4']) || $server['ipv4'] === '')){ - $server['ipv4'] = $data['ipv4']; - } $updated = true; break; diff --git a/Lobby Servers/PHP Server/index.php b/Lobby Servers/PHP Server/index.php index a18569d..7b00e06 100644 --- a/Lobby Servers/PHP Server/index.php +++ b/Lobby Servers/PHP Server/index.php @@ -1,5 +1,5 @@ "Invalid server information"]); } - if(filter_var($_SERVER['REMOTE_ADDR'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)){ - $data['ipv4'] = $_SERVER['REMOTE_ADDR']; - }elseif(filter_var($_SERVER['REMOTE_ADDR'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)){ - $data['ipv6'] = $_SERVER['REMOTE_ADDR']; - } - $data['game_server_id'] = uuid_create(); $data['private_key'] = generate_private_key(); - - return $response = $db->addGameServer($data); + return $db->addGameServer($data); } function update_game_server($db, $data) { if (!validate_server_update($db, $data)) { + http_response_code(500); return json_encode(["error" => "Invalid game server ID or private key"]); } @@ -88,35 +83,51 @@ function list_game_servers($db) { unset($server['private_key']); unset($server['last_update']); + if(!isset($server['ipv4'])){ + $server['ipv4'] = ''; + } + + if(!isset($server['ipv6'])){ + $server['ipv6'] = ''; + } + if(filter_var($_SERVER['REMOTE_ADDR'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)){ - if(!isset($server['ipv4'])){ - $server['ip'] = ''; - }else{ - $server['ip'] = $server['ipv4']; - } - - unset($server['ipv4']); + //Host made a request on IPv4, remove IPv6 address as we assume they don't support it. unset($server['ipv6']); - }elseif(filter_var($_SERVER['REMOTE_ADDR'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)){ - if(!isset($server['ipv6'])){ - $server['ip'] = $server['ipv4']; - }else{ - $server['ip'] = $server['ipv6']; - unset($server['ipv6']); - } - - unset($server['ipv4']); - unset($server['ipv6']); } } return json_encode($servers); } 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) { + + if(!isset($data['ipv4']) || !filter_var($data['ipv4'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)){ + $data['ipv4'] = ''; + }elseif(!isset($data['ipv6']) || !filter_var($data['ipv6'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)){ + $data['ipv6'] = ''; + } + + if ( + //make sure we have at lease one IP + $data['ipv4'] == '' && $data['ipv6'] == '' || + + //Make sure we have all required fields + !isset($data['server_name']) || + !isset($data['server_info']) || + !isset($data['current_players']) || + !isset($data['max_players']) || + + //Validate fields + strlen($data['server_name']) > 25 || + strlen($data['server_info']) > 500 || + $data['current_players'] > $data['max_players'] || + $data['max_players'] < 1 + ){ + return false; } + return true; } @@ -144,4 +155,4 @@ function generate_private_key() { return $private_key; } -?> +?> \ No newline at end of file diff --git a/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserDummyElement.cs b/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserDummyElement.cs index a566ef7..e36384a 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserDummyElement.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserDummyElement.cs @@ -44,9 +44,9 @@ private void Awake() loc.key = Locale.SERVER_BROWSER__NO_SERVERS_KEY ; loc.UpdateLocalization(); - this.GetOrAddComponent().enabled = true;//.enabledKey = Locale.SERVER_HOST_PASSWORD__TOOLTIP_KEY; + this.GetOrAddComponent().enabled = true; this.gameObject.ResetTooltip(); - //networkName.text = "No servers found. Refresh or start your own!"; + } public override void SetData(IServerBrowserGameDetails data, AGridView _) diff --git a/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserElement.cs b/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserElement.cs index f0ecf14..e1c122b 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserElement.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserElement.cs @@ -28,12 +28,6 @@ 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 7f13fb3..b674e23 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserGridView.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserGridView.cs @@ -15,12 +15,13 @@ public class ServerBrowserGridView : AGridView private void Awake() { - Multiplayer.Log("serverBrowserGridview Awake"); + Multiplayer.Log("serverBrowserGridview Awake()"); - //swap controller + //copy the copy this.viewElementPrefab.SetActive(false); this.dummyElementPrefab = Instantiate(this.viewElementPrefab); + //swap controllers GameObject.Destroy(this.viewElementPrefab.GetComponent()); GameObject.Destroy(this.dummyElementPrefab.GetComponent()); diff --git a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs index 01cb8de..2d4bcc2 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs @@ -262,17 +262,24 @@ private void BuildUI() private void SetupServerBrowser() { GameObject GridviewGO = this.FindChildByName("Scroll View").FindChildByName("GRID VIEW"); - SaveLoadGridView slgv = GridviewGO.GetComponent(); + //Disable before we make any changes GridviewGO.SetActive(false); + + //load our custom controller + SaveLoadGridView slgv = GridviewGO.GetComponent(); gridView = GridviewGO.AddComponent(); + + //grab the original prefab slgv.viewElementPrefab.SetActive(false); gridView.viewElementPrefab = Instantiate(slgv.viewElementPrefab); - + slgv.viewElementPrefab.SetActive(true); + //Remove original controller GameObject.Destroy(slgv); + //Don't forget to re-enable! GridviewGO.SetActive(true); } private void SetupListeners(bool on) diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index a689e3b..50df91d 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -311,11 +311,11 @@ public void SendDebtStatus(bool hasDebt) }, DeliveryMethod.ReliableUnordered, selfPeer); } - public void SendJobCreatePacket(NetworkedJob job) - { - Multiplayer.Log("Sending JobCreatePacket with netId: " + job.NetId + ", Job ID: " + job.job.ID); - SendPacketToAll(ClientboundJobCreatePacket.FromNetworkedJob(job),DeliveryMethod.ReliableSequenced); - } + //public void SendJobCreatePacket(NetworkedJob job) + //{ + // Multiplayer.Log("Sending JobCreatePacket with netId: " + job.NetId + ", Job ID: " + job.job.ID); + // SendPacketToAll(ClientboundJobCreatePacket.FromNetworkedJob(job),DeliveryMethod.ReliableSequenced); + //} public void SendChat(string message, NetPeer exclude = null) { diff --git a/Multiplayer/Patches/MainMenu/RightPaneControllerPatch.cs b/Multiplayer/Patches/MainMenu/RightPaneControllerPatch.cs index cd89b2c..2767569 100644 --- a/Multiplayer/Patches/MainMenu/RightPaneControllerPatch.cs +++ b/Multiplayer/Patches/MainMenu/RightPaneControllerPatch.cs @@ -59,7 +59,7 @@ private static void Prefix(RightPaneController __instance) // Activate the multiplayer button MainMenuController_Awake_Patch.multiplayerButton.SetActive(true); - Multiplayer.LogError("At end!"); + Multiplayer.Log("At end!"); // Check if the host pane already exists if (__instance.HasChildWithName("PaneRight Host")) diff --git a/Multiplayer/Patches/World/StationLocoSpawnerPatch.cs b/Multiplayer/Patches/World/StationLocoSpawnerPatch.cs index 3906a85..596f561 100644 --- a/Multiplayer/Patches/World/StationLocoSpawnerPatch.cs +++ b/Multiplayer/Patches/World/StationLocoSpawnerPatch.cs @@ -55,8 +55,10 @@ private static IEnumerator CheckShouldSpawn(StationLocoSpawner __instance) private static bool IsAnyoneWithinRange(StationLocoSpawner stationLocoSpawner, Vector3 targetPosition) { foreach (ServerPlayer serverPlayer in NetworkLifecycle.Instance.Server.ServerPlayers) - if ((serverPlayer.WorldPosition - targetPosition).sqrMagnitude < stationLocoSpawner.spawnLocoPlayerSqrDistanceFromTrack) + { + if (serverPlayer != null && (serverPlayer.WorldPosition - targetPosition).sqrMagnitude < stationLocoSpawner.spawnLocoPlayerSqrDistanceFromTrack) return true; + } return false; } From 32192969223a4cdf9c17afefd90a58fa8bb58969 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 21 Jul 2024 12:14:50 +1000 Subject: [PATCH 050/188] Forced connections on IPv4 Updated LobbyServerManager to provide IPv4 and IPv6 IP addresses to the lobby server Forced ServerBrowser to conenct on IPv4 until a connection sequence has been implemented, i.e.: - Try to connect on IPv6 - Try to connect on IPv4 - Try to Punch IPv6 - Try to Punch IPv4 - Fail to connect --- .../IServerBrowserGameDetails.cs | 3 +- .../Components/MainMenu/ServerBrowserPane.cs | 13 ++- .../Networking/Jobs/NetworkedJob.cs | 4 +- .../Networking/Data/LobbyServerData.cs | 3 +- .../Data/LobbyServerResponseData.cs | 3 - .../Networking/Data/LobbyServerUpdateData.cs | 4 - .../Managers/Server/LobbyServerManager.cs | 79 +++++++++++++------ 7 files changed, 71 insertions(+), 38 deletions(-) diff --git a/Multiplayer/Components/MainMenu/ServerBrowser/IServerBrowserGameDetails.cs b/Multiplayer/Components/MainMenu/ServerBrowser/IServerBrowserGameDetails.cs index 28d4d38..0c4622d 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowser/IServerBrowserGameDetails.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowser/IServerBrowserGameDetails.cs @@ -14,7 +14,8 @@ namespace Multiplayer.Components.MainMenu public interface IServerBrowserGameDetails : IDisposable { string id { get; set; } - string ip { get; set; } + string ipv6 { get; set; } + string ipv4 { get; set; } int port { get; set; } string Name { get; set; } bool HasPassword { get; set; } diff --git a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs index 2d4bcc2..3d1f2d9 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs @@ -325,12 +325,19 @@ private void JoinAction() buttonDirectIP.ToggleInteractable(false); buttonJoin.ToggleInteractable(false); + //TODO: Add logic to allow IPv6 addresses to be used + if (selectedServer.ipv4 != null && + selectedServer.ipv4 != string.Empty && + IPv4Regex.IsMatch(selectedServer.ipv4)) + { + address = selectedServer.ipv4; + } + if (selectedServer.HasPassword) { //not making a direct connection direct = false; - address = selectedServer.ip; portNumber = selectedServer.port; ShowPasswordPopup(); @@ -339,7 +346,7 @@ private void JoinAction() } //No password, just connect - SingletonBehaviour.Instance.StartClient(selectedServer.ip, selectedServer.port, null, false); + SingletonBehaviour.Instance.StartClient(address, selectedServer.port, null, false); } } @@ -608,7 +615,7 @@ IEnumerator GetRequest(string uri) foreach (LobbyServerData server in response) { - Multiplayer.Log($"Server name: {server.Name}\tIP: {server.ip}"); + Multiplayer.Log($"Server name: \"{server.Name}\", IPv4: {server.ipv4}, IPv6: {server.ipv6}, Port: {server.port}"); } if (response.Length == 0) diff --git a/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs b/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs index 1980329..3ea1e8e 100644 --- a/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs +++ b/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs @@ -229,13 +229,14 @@ private void Server_OnTick(uint tick) if (UnloadWatcher.isUnloading) return; - Server_SendNewJob(); + //Server_SendNewJob(); //Server_SendJobStatus(); //Server_SendTaskStatus(); //Server_SendJobDestroy(); } + /* private void Server_SendNewJob() { if (!isJobNew) @@ -244,6 +245,7 @@ private void Server_SendNewJob() isJobNew = false; NetworkLifecycle.Instance.Server.SendJobCreatePacket(this); } + */ /* private void Server_SendJobStatus() { diff --git a/Multiplayer/Networking/Data/LobbyServerData.cs b/Multiplayer/Networking/Data/LobbyServerData.cs index ffed4f0..7e9da1c 100644 --- a/Multiplayer/Networking/Data/LobbyServerData.cs +++ b/Multiplayer/Networking/Data/LobbyServerData.cs @@ -13,7 +13,8 @@ public class LobbyServerData : IServerBrowserGameDetails public string id { get; set; } - public string ip { get; set; } + public string ipv4 { get; set; } + public string ipv6 { get; set; } public int port { get; set; } [JsonProperty("server_name")] diff --git a/Multiplayer/Networking/Data/LobbyServerResponseData.cs b/Multiplayer/Networking/Data/LobbyServerResponseData.cs index 5654149..1a75af2 100644 --- a/Multiplayer/Networking/Data/LobbyServerResponseData.cs +++ b/Multiplayer/Networking/Data/LobbyServerResponseData.cs @@ -13,14 +13,11 @@ public class LobbyServerResponseData public string game_server_id { get; set; } public string private_key { get; set; } - [JsonIgnore] - public bool? ipv4_request{ get; set; } public LobbyServerResponseData(string game_server_id, string private_key, bool? ipv4_request = null) { this.game_server_id = game_server_id; this.private_key = private_key; - this.ipv4_request = ipv4_request; } } } diff --git a/Multiplayer/Networking/Data/LobbyServerUpdateData.cs b/Multiplayer/Networking/Data/LobbyServerUpdateData.cs index c4c80a1..f611cfd 100644 --- a/Multiplayer/Networking/Data/LobbyServerUpdateData.cs +++ b/Multiplayer/Networking/Data/LobbyServerUpdateData.cs @@ -16,16 +16,12 @@ public class LobbyServerUpdateData [JsonProperty("current_players")] public int CurrentPlayers { get; set; } - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - public string ipv4 { get; set; } - public LobbyServerUpdateData(string game_server_id, string private_key, string timePassed,int currentPlayers, string ipv4 = null) { this.game_server_id = game_server_id; this.private_key = private_key; this.TimePassed = timePassed; this.CurrentPlayers = currentPlayers; - this.ipv4 = ipv4; } diff --git a/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs b/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs index 2a82877..2c93f95 100644 --- a/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs +++ b/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs @@ -8,6 +8,8 @@ using Multiplayer.Components.Networking; using DV.WeatherSystem; using System.Text.RegularExpressions; +using System.Net.NetworkInformation; +using System.Net.Sockets; namespace Multiplayer.Networking.Managers.Server; public class LobbyServerManager : MonoBehaviour @@ -29,9 +31,10 @@ public class LobbyServerManager : MonoBehaviour private NetworkServer server; private string server_id { get; set; } private string private_key { get; set; } - private string myIPv4 { get; set; } - private bool updateIPv4 { get; set; } = false; - + + private bool initialised = false; + + private bool sendUpdates = false; private float timePassed = 0f; @@ -41,8 +44,21 @@ private void Awake() server = NetworkLifecycle.Instance.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 IEnumerator Start() + { + server.serverData.ipv6 = GetStaticIPv6Address(); + StartCoroutine(GetIPv4(Multiplayer.Settings.Ipv4AddressCheck)); + + yield return new WaitUntil(() => initialised); + + Multiplayer.Log("Public IPv4: " + server.serverData.ipv4); + Multiplayer.Log("Public IPv6: " + server.serverData.ipv6); + + Multiplayer.Log("Registering server at: " + Multiplayer.Settings.LobbyServerAddress + "/add_game_server"); + + StartCoroutine(RegisterWithLobbyServer(Multiplayer.Settings.LobbyServerAddress + "/add_game_server")); } private void OnDestroy() @@ -93,13 +109,6 @@ private IEnumerator RegisterWithLobbyServer(string uri) private_key = response.private_key; server_id = response.game_server_id; - //Check if we are using IPv6 to talk to the lobby server - if(response.ipv4_request == true) - { - //We are using IPv6, so now we will need to make a request to an IPv4 service, then update the lobby server with our IPv4 IP - StartCoroutine(GetIPv4($"{Multiplayer.Settings.Ipv4AddressCheck}")); - } - sendUpdates = true; } }, @@ -136,13 +145,6 @@ private IEnumerator UpdateLobbyServer(string uri) server.serverData.CurrentPlayers ); - //do we need to provide our IPv4? - if(updateIPv4 && myIPv4 != null && myIPv4 != string.Empty) - { - reqData.ipv4 = myIPv4; - updateIPv4 = false; - } - string json = JsonConvert.SerializeObject(reqData, jsonSettings); Multiplayer.LogDebug(() => $"UpdateLobbyServer JsonRequest: {json}"); @@ -168,7 +170,7 @@ private IEnumerator UpdateLobbyServer(string uri) private IEnumerator GetIPv4(string uri) { - Multiplayer.Log("Preparing to get IPv4"); + Multiplayer.Log("Preparing to get IPv4: " + uri); yield return SendWebRequest( uri, @@ -179,18 +181,21 @@ private IEnumerator GetIPv4(string uri) if (match != null) { Multiplayer.Log($"IPv4 address extracted: {match.Value}"); - myIPv4 = match.Value; - updateIPv4 = true; - StopAllCoroutines(); - StartCoroutine(UpdateLobbyServer($"{Multiplayer.Settings.LobbyServerAddress}/{ENDPOINT_UPDATE_SERVER}")); + server.serverData.ipv4 = match.Value; } else { Multiplayer.LogError($"Failed to find IPv4 address. Server will only be available via IPv6"); } + initialised = true; + }, - webRequest => Multiplayer.LogError("Failed to remove from lobby server") + webRequest => + { + Multiplayer.LogError("Failed to find IPv4 address. Server will only be available via IPv6"); + initialised = true; + } ); } @@ -240,4 +245,28 @@ private IEnumerator SendWebRequest(string uri, string json, Action Date: Sun, 21 Jul 2024 16:02:27 +1000 Subject: [PATCH 051/188] Self preservation when deleting cars --- Lobby Servers/RestAPI.md | 24 +++++++++---------- .../Components/Networking/UI/ChatGUI.cs | 2 +- .../Components/Networking/UI/PlayerListGUI.cs | 2 +- .../Managers/Client/NetworkClient.cs | 16 ++++++++++++- .../Patches/Mods/RemoteDispatchPatch.cs | 2 +- 5 files changed, 30 insertions(+), 16 deletions(-) diff --git a/Lobby Servers/RestAPI.md b/Lobby Servers/RestAPI.md index 5045188..cd9d574 100644 --- a/Lobby Servers/RestAPI.md +++ b/Lobby Servers/RestAPI.md @@ -37,6 +37,8 @@ The difficulty field in the request body for adding a game server must be one of - **Request Body:** ```json { + "ipv4": "string", + "ipv6": "string", "port": "integer", "server_name": "string", "password_protected": "boolean", @@ -52,7 +54,9 @@ The difficulty field in the request body for adding a game server must be one of } ``` - **Fields:** - - port (integer): The port number of the game server. + - ipv4 (optional string): The publically accessible IPv4 address of the game server - if this is not supplied, then the IPv6 address must be. + - ipv6 (optional string): The publically accessible IPv4 address of the game server - if this is not supplied, then the IPv4 address must be.. + - 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)). @@ -72,13 +76,11 @@ The difficulty field in the request body for adding a game server must be one of ```json { "game_server_id": "string", - "private_key": "string", - "ipv4_request": "bool" + "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. - - ipv4_request (bool): A request to provide an IPV4 address. If `true`, the game server's IPV4 address should be provided via a call to the Update Server end point. - **Error:** - **Code:** 500 Internal Server Error - **Content:** `"Failed to add server"` @@ -95,7 +97,6 @@ The difficulty field in the request body for adding a game server must be one of "private_key": "string", "current_players": "integer", "time_passed": "string", - "ipv4": "string" } ``` - **Fields:** @@ -103,7 +104,6 @@ The difficulty field in the request body for adding a game server must be one of - 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. - - ipv4 (optional string): The game server's public IPV4 address (if exists). Only provide if `ipv4_request` is `true`. - **Response:** - **Success:** - **Code:** 200 OK @@ -147,7 +147,8 @@ The difficulty field in the request body for adding a game server must be one of ```json [ { - "ip": "string", + "ipv4": "string", + "ipv6": "string", "port": "integer", "server_name": "string", "password_protected": "boolean", @@ -166,10 +167,8 @@ 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. - Note: if the server has both an IPV4 and IPV6, the returned IP will depend on the IP version of the requestor. - If the end point request is made using IPV4 and the game server does not have an IPV4 address, the server will return an empty string. - If the end point request is made using IPV6 and the game server does not have an IPV6 address, the server will return an IPV4 address. + - ipv4 (optional string): The IPv4 address of the game server, if known. + - ipv6 (optional string): The IPv6 address of the game server, if known and if the end point request is made using IPv6, i.e. IPv4 clients will not be provided with the ``ipv6`` field. - 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. @@ -193,7 +192,8 @@ The difficulty field in the request body for adding a game server must be one of Example request: ```bash curl -X POST -H "Content-Type: application/json" -d '{ - "ip": "127.0.0.1", + "ipv4": "127.0.0.1", + "ipv6": "::1", "port": 7777, "server_name": "My Derail Valley Server", "password_protected": false, diff --git a/Multiplayer/Components/Networking/UI/ChatGUI.cs b/Multiplayer/Components/Networking/UI/ChatGUI.cs index a5675d1..f8583c3 100644 --- a/Multiplayer/Components/Networking/UI/ChatGUI.cs +++ b/Multiplayer/Components/Networking/UI/ChatGUI.cs @@ -263,7 +263,7 @@ private void ChatInputChange(string message) if (localMessage == null || localMessage == string.Empty) { - string closestMatch = NetworkLifecycle.Instance.Client.PlayerManager.Players + string closestMatch = NetworkLifecycle.Instance.Client.ClientPlayerManager.Players .Where(player => player.Username.ToLower().StartsWith(recipient.ToLower())) .OrderBy(player => player.Username.Length) .ThenByDescending(player => player.Username) diff --git a/Multiplayer/Components/Networking/UI/PlayerListGUI.cs b/Multiplayer/Components/Networking/UI/PlayerListGUI.cs index 471d050..59ae043 100644 --- a/Multiplayer/Components/Networking/UI/PlayerListGUI.cs +++ b/Multiplayer/Components/Networking/UI/PlayerListGUI.cs @@ -38,7 +38,7 @@ private static IEnumerable GetPlayerList() if (!NetworkLifecycle.Instance.IsClientRunning) return new[] { "Not in game" }; - IReadOnlyCollection players = NetworkLifecycle.Instance.Client.PlayerManager.Players; + IReadOnlyCollection players = NetworkLifecycle.Instance.Client.ClientPlayerManager.Players; string[] playerList = new string[players.Count + 1]; int i = 0; foreach (NetworkedPlayer player in players) diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 6e94ca1..c862d64 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -46,7 +46,7 @@ public class NetworkClient : NetworkManager protected override string LogPrefix => "[Client]"; public NetPeer selfPeer { get; private set; } - public readonly ClientPlayerManager PlayerManager; + public readonly ClientPlayerManager ClientPlayerManager; // One way ping in milliseconds public int Ping { get; private set; } @@ -404,6 +404,20 @@ private void OnClientboundDestroyTrainCarPacket(ClientboundDestroyTrainCarPacket if (!NetworkedTrainCar.Get(packet.NetId, out NetworkedTrainCar networkedTrainCar)) return; + //Protect myself from getting deleted in race conditions + if (PlayerManager.Car == networkedTrainCar.TrainCar) + { + Multiplayer.LogWarning($"Server attempted to delete car I'm on: {PlayerManager.Car.ID}, net ID: {packet.NetId}"); + PlayerManager.SetCar(null); + } + + //Protect other players from getting deleted in race conditions - this should be a temporary fix, if another playe's game object is deleted we should just recreate it + NetworkedPlayer[] componentsInChildren = networkedTrainCar.GetComponentsInChildren(); + foreach (NetworkedPlayer networkedPlayer in componentsInChildren) + { + networkedPlayer.UpdateCar(0); + } + CarSpawner.Instance.DeleteCar(networkedTrainCar.TrainCar); } diff --git a/Multiplayer/Patches/Mods/RemoteDispatchPatch.cs b/Multiplayer/Patches/Mods/RemoteDispatchPatch.cs index 72da15c..98cb07a 100644 --- a/Multiplayer/Patches/Mods/RemoteDispatchPatch.cs +++ b/Multiplayer/Patches/Mods/RemoteDispatchPatch.cs @@ -45,7 +45,7 @@ private static void GetPlayerData_Postfix(ref JObject __result) if (!NetworkLifecycle.Instance.IsClientRunning) return; - foreach (NetworkedPlayer player in NetworkLifecycle.Instance.Client.PlayerManager.Players) + foreach (NetworkedPlayer player in NetworkLifecycle.Instance.Client.ClientPlayerManager.Players) { JObject data = new(); From 822c03f99eb4361cc0666b31bb77325b245ff191 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 27 Jul 2024 14:06:18 +1000 Subject: [PATCH 052/188] Continuing work on IPv6 and sync issues --- .../Components/MainMenu/ServerBrowserPane.cs | 8 +++++++- .../Networking/Player/NetworkedWorldMap.cs | 12 +++++------ .../Managers/Client/NetworkClient.cs | 16 +++++++-------- .../Managers/Server/LobbyServerManager.cs | 2 +- .../Managers/Server/NetworkServer.cs | 11 ++++++++++ .../Jobs/StationJobGenerationRangePatch.cs | 20 +++++++++++++++++++ 6 files changed, 53 insertions(+), 16 deletions(-) diff --git a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs index 3d1f2d9..6a41f7e 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs @@ -326,12 +326,18 @@ private void JoinAction() buttonJoin.ToggleInteractable(false); //TODO: Add logic to allow IPv6 addresses to be used - if (selectedServer.ipv4 != null && + if (selectedServer.ipv6 != null && + selectedServer.ipv6 != string.Empty && + IPv6Regex.IsMatch(selectedServer.ipv6)) + { + address = selectedServer.ipv6; + }else if (selectedServer.ipv4 != null && selectedServer.ipv4 != string.Empty && IPv4Regex.IsMatch(selectedServer.ipv4)) { address = selectedServer.ipv4; } + Multiplayer.Log($"Selected IP address is: {address}"); if (selectedServer.HasPassword) { diff --git a/Multiplayer/Components/Networking/Player/NetworkedWorldMap.cs b/Multiplayer/Components/Networking/Player/NetworkedWorldMap.cs index ccc044b..522ecfa 100644 --- a/Multiplayer/Components/Networking/Player/NetworkedWorldMap.cs +++ b/Multiplayer/Components/Networking/Player/NetworkedWorldMap.cs @@ -16,10 +16,10 @@ private void Awake() worldMap = GetComponent(); markersController = GetComponent(); textPrefab = worldMap.GetComponentInChildren().gameObject; - foreach (NetworkedPlayer networkedPlayer in NetworkLifecycle.Instance.Client.PlayerManager.Players) + foreach (NetworkedPlayer networkedPlayer in NetworkLifecycle.Instance.Client.ClientPlayerManager.Players) OnPlayerConnected(networkedPlayer.Id, networkedPlayer); - NetworkLifecycle.Instance.Client.PlayerManager.OnPlayerConnected += OnPlayerConnected; - NetworkLifecycle.Instance.Client.PlayerManager.OnPlayerDisconnected += OnPlayerDisconnected; + NetworkLifecycle.Instance.Client.ClientPlayerManager.OnPlayerConnected += OnPlayerConnected; + NetworkLifecycle.Instance.Client.ClientPlayerManager.OnPlayerDisconnected += OnPlayerDisconnected; NetworkLifecycle.Instance.OnTick += OnTick; } @@ -30,8 +30,8 @@ private void OnDestroy() NetworkLifecycle.Instance.OnTick -= OnTick; if (UnloadWatcher.isUnloading) return; - NetworkLifecycle.Instance.Client.PlayerManager.OnPlayerConnected -= OnPlayerConnected; - NetworkLifecycle.Instance.Client.PlayerManager.OnPlayerDisconnected -= OnPlayerDisconnected; + NetworkLifecycle.Instance.Client.ClientPlayerManager.OnPlayerConnected -= OnPlayerConnected; + NetworkLifecycle.Instance.Client.ClientPlayerManager.OnPlayerDisconnected -= OnPlayerDisconnected; } private void OnPlayerConnected(byte id, NetworkedPlayer player) @@ -83,7 +83,7 @@ public void UpdatePlayers() { foreach (KeyValuePair kvp in playerIndicators) { - if (!NetworkLifecycle.Instance.Client.PlayerManager.TryGetPlayer(kvp.Key, out NetworkedPlayer networkedPlayer)) + if (!NetworkLifecycle.Instance.Client.ClientPlayerManager.TryGetPlayer(kvp.Key, out NetworkedPlayer networkedPlayer)) { Multiplayer.LogWarning($"Player indicator for {kvp.Key} exists but {nameof(NetworkedPlayer)} does not!"); OnPlayerDisconnected(kvp.Key, null); diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index c862d64..83864c4 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -57,7 +57,7 @@ public class NetworkClient : NetworkManager public NetworkClient(Settings settings) : base(settings) { - PlayerManager = new ClientPlayerManager(); + ClientPlayerManager = new ClientPlayerManager(); } public void Start(string address, int port, string password, bool isSinglePlayer) @@ -217,30 +217,30 @@ private void OnClientboundServerDenyPacket(ClientboundServerDenyPacket packet) private void OnClientboundPlayerJoinedPacket(ClientboundPlayerJoinedPacket packet) { Guid guid = new(packet.Guid); - PlayerManager.AddPlayer(packet.Id, packet.Username, guid); - PlayerManager.UpdateCar(packet.Id, packet.TrainCar); - PlayerManager.UpdatePosition(packet.Id, packet.Position, Vector3.zero, packet.Rotation, false, packet.TrainCar != 0); + ClientPlayerManager.AddPlayer(packet.Id, packet.Username, guid); + ClientPlayerManager.UpdateCar(packet.Id, packet.TrainCar); + ClientPlayerManager.UpdatePosition(packet.Id, packet.Position, Vector3.zero, packet.Rotation, false, packet.TrainCar != 0); } private void OnClientboundPlayerDisconnectPacket(ClientboundPlayerDisconnectPacket packet) { Log($"Received player disconnect packet (Id: {packet.Id})"); - PlayerManager.RemovePlayer(packet.Id); + ClientPlayerManager.RemovePlayer(packet.Id); } private void OnClientboundPlayerPositionPacket(ClientboundPlayerPositionPacket packet) { - PlayerManager.UpdatePosition(packet.Id, packet.Position, packet.MoveDir, packet.RotationY, packet.IsJumping, packet.IsOnCar); + ClientPlayerManager.UpdatePosition(packet.Id, packet.Position, packet.MoveDir, packet.RotationY, packet.IsJumping, packet.IsOnCar); } private void OnClientboundPlayerCarPacket(ClientboundPlayerCarPacket packet) { - PlayerManager.UpdateCar(packet.Id, packet.CarId); + ClientPlayerManager.UpdateCar(packet.Id, packet.CarId); } private void OnClientboundPingUpdatePacket(ClientboundPingUpdatePacket packet) { - PlayerManager.UpdatePing(packet.Id, packet.Ping); + ClientPlayerManager.UpdatePing(packet.Id, packet.Ping); } private void OnClientboundTickSyncPacket(ClientboundTickSyncPacket packet) diff --git a/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs b/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs index 2c93f95..45c54ac 100644 --- a/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs +++ b/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs @@ -246,7 +246,7 @@ private IEnumerator SendWebRequest(string uri, string json, Action $"PlayerSqrDistanceFromStationCenter:\r\n\t" + + $"player: '{serverPlayer.Username}',\r\n\t\t" + + $"absPos: {serverPlayer.AbsoluteWorldPosition.ToString()},\r\n\t\t" + + $"rawPos: {serverPlayer.RawPosition.ToString()},\r\n\t\t" + + $"worldPos: {serverPlayer.WorldPosition.ToString()},\r\n\t" + + $"station name: '{__instance.name}',\r\n\t\t" + + $"anchor: {anchor.ToString()},\r\n\t" + + $"sqDist: {sqDist}"); + } + } + + frameCount++; + if (frameCount > 60) + { + frameCount = 0; + } return false; From 4a7e195902f0d0c0f2dfa696ebcfd6056ac95990 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 27 Jul 2024 21:45:50 +1000 Subject: [PATCH 053/188] Multiple bug fixes, sync fix for entering/exiting cars GetNetId() no longer throws an exception, instead returning 0. CustomFirstPersonControllerPatch.OnCarChanged() now sends the updated player position - fixes PlayerSqrDistanceFromStation*() issue due to race conditions around player position. --- .../Train/NetworkTrainsetWatcher.cs | 4 + Multiplayer/Multiplayer.cs | 2 + Multiplayer/Networking/Data/ServerPlayer.cs | 76 +++++++++++++++++-- .../Managers/Client/NetworkClient.cs | 68 ++++++++++++++--- .../Networking/Managers/Server/ChatManager.cs | 9 +++ .../Managers/Server/NetworkServer.cs | 13 ++++ .../Serverbound/ServerboundPlayerCarPacket.cs | 5 ++ .../CommsRadio/CommsRadioCarDeleterPatch.cs | 7 +- .../CommsRadio/RerailControllerPatch.cs | 13 +++- Multiplayer/Patches/Jobs/JobPatch.cs | 21 +++++ .../Jobs/StationJobGenerationRangePatch.cs | 53 ++++++++++++- .../CustomFirstPersonControllerPatch.cs | 17 ++++- .../Train/WindowsBreakingControllerPatch.cs | 16 +++- Multiplayer/Utils/DvExtensions.cs | 4 +- 14 files changed, 282 insertions(+), 26 deletions(-) create mode 100644 Multiplayer/Patches/Jobs/JobPatch.cs diff --git a/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs b/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs index af7566f..a1b9c7e 100644 --- a/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs +++ b/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs @@ -54,6 +54,10 @@ private void Server_TickSet(Trainset set) cachedSendPacket.NetId = set.firstCar.GetNetId(); + //car may not be initialised, missing a valid NetID + if (cachedSendPacket.NetId == 0) + return; + if (set.cars.Contains(null)) { Multiplayer.LogError($"Trainset {set.id} ({set.firstCar.GetNetId()} has a null car!"); diff --git a/Multiplayer/Multiplayer.cs b/Multiplayer/Multiplayer.cs index b54272e..d82edf5 100644 --- a/Multiplayer/Multiplayer.cs +++ b/Multiplayer/Multiplayer.cs @@ -22,6 +22,8 @@ public static class Multiplayer private static AssetBundle assetBundle; public static AssetIndex AssetIndex { get; private set; } + public static bool specLog = false; + [UsedImplicitly] private static bool Load(UnityModManager.ModEntry modEntry) { diff --git a/Multiplayer/Networking/Data/ServerPlayer.cs b/Multiplayer/Networking/Data/ServerPlayer.cs index 613f25e..a90618f 100644 --- a/Multiplayer/Networking/Data/ServerPlayer.cs +++ b/Multiplayer/Networking/Data/ServerPlayer.cs @@ -15,14 +15,78 @@ public class ServerPlayer public float RawRotationY { get; set; } public ushort CarId { get; set; } - public Vector3 AbsoluteWorldPosition => CarId == 0 || !NetworkedTrainCar.Get(CarId, out NetworkedTrainCar car) - ? RawPosition - : car.transform.TransformPoint(RawPosition) - WorldMover.currentMove; + private Vector3 _lastWorldPos = Vector3.zero; + private Vector3 _lastAbsoluteWorldPosition = Vector3.zero; - public Vector3 WorldPosition => CarId == 0 || !NetworkedTrainCar.Get(CarId, out NetworkedTrainCar car) - ? RawPosition + WorldMover.currentMove - : car.transform.TransformPoint(RawPosition); + public Vector3 AbsoluteWorldPosition + { + get + { + + Vector3 pos; + try + { + if (CarId == 0 || !NetworkedTrainCar.Get(CarId, out NetworkedTrainCar car)) + { + if (CarId != 0) + Multiplayer.LogDebug(() => $"AbsoluteWorldPosition() noID {Username}: CarId: {CarId}"); + + pos = RawPosition; + } + else + { + //Multiplayer.LogDebug(() => $"AbsoluteWorldPosition() hasID {Username}: CarId: {CarId}"); + pos = car.transform.TransformPoint(RawPosition) - WorldMover.currentMove; ; + } + + _lastAbsoluteWorldPosition = pos; + } + catch (Exception e) + { + Multiplayer.LogWarning($"AbsoluteWorldPosition() Exception {Username}"); + Multiplayer.LogWarning(e.Message); + Multiplayer.LogWarning(e.StackTrace); + pos = _lastAbsoluteWorldPosition; + } + + return pos; + + } + } + + public Vector3 WorldPosition { + get + { + Vector3 pos; + try + { + if (CarId == 0 || !NetworkedTrainCar.Get(CarId, out NetworkedTrainCar car)) + { + if(CarId != 0) + Multiplayer.LogDebug(() =>$"WorldPosition() noID {Username}: CarId: {CarId}"); + + pos = RawPosition + WorldMover.currentMove; + } + else + { + //Multiplayer.LogDebug(() => $"WorldPosition() hasID {Username}: CarId: {CarId}"); + pos = car.transform.TransformPoint(RawPosition); + } + + _lastWorldPos = pos; + } + catch (Exception e) + { + Multiplayer.LogWarning($"WorldPosition() Exception {Username}"); + Multiplayer.LogWarning(e.Message); + Multiplayer.LogWarning(e.StackTrace); + + pos = _lastWorldPos; + } + return pos; + } + } public float WorldRotationY => CarId == 0 || !NetworkedTrainCar.Get(CarId, out NetworkedTrainCar car) ? RawRotationY : (Quaternion.Euler(0, RawRotationY, 0) * car.transform.rotation).eulerAngles.y; diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 83864c4..c4c9bcd 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -779,11 +779,15 @@ public void SendPlayerPosition(Vector3 position, Vector3 moveDir, float rotation }, reliable ? DeliveryMethod.ReliableOrdered : DeliveryMethod.Sequenced); } - public void SendPlayerCar(ushort carId) + public void SendPlayerCar(ushort carId,Vector3 position, Vector3 moveDir, float rotationY, bool isJumping) { SendPacketToServer(new ServerboundPlayerCarPacket { - CarId = carId + CarId = carId, + Position = position, + MoveDir = moveDir, + RotationY=rotationY, + }, DeliveryMethod.ReliableOrdered); } @@ -816,11 +820,20 @@ public void SendTurntableRotation(byte netId, float rotation) public void SendTrainCouple(Coupler coupler, Coupler otherCoupler, bool playAudio, bool viaChainInteraction) { + ushort couplerNetId = coupler.train.GetNetId(); + ushort otherCouplerNetId = otherCoupler.train.GetNetId(); + + if (couplerNetId == 0 || otherCouplerNetId == 0) + { + Multiplayer.LogWarning($"SendTrainCouple failed. Coupler: {coupler.name} {couplerNetId}, OtherCoupler: {otherCoupler.name} {otherCouplerNetId}"); + return; + } + SendPacketToServer(new CommonTrainCouplePacket { - NetId = coupler.train.GetNetId(), + NetId = couplerNetId, //coupler.train.GetNetId(), IsFrontCoupler = coupler.isFrontCoupler, - OtherNetId = otherCoupler.train.GetNetId(), + OtherNetId = otherCouplerNetId, //otherCoupler.train.GetNetId(), OtherCarIsFrontCoupler = otherCoupler.isFrontCoupler, PlayAudio = playAudio, ViaChainInteraction = viaChainInteraction @@ -829,9 +842,17 @@ public void SendTrainCouple(Coupler coupler, Coupler otherCoupler, bool playAudi public void SendTrainUncouple(Coupler coupler, bool playAudio, bool dueToBrokenCouple, bool viaChainInteraction) { + ushort couplerNetId = coupler.train.GetNetId(); + + if (couplerNetId == 0) + { + Multiplayer.LogWarning($"SendTrainUncouple failed. Coupler: {coupler.name} {couplerNetId}"); + return; + } + SendPacketToServer(new CommonTrainUncouplePacket { - NetId = coupler.train.GetNetId(), + NetId = couplerNetId, IsFrontCoupler = coupler.isFrontCoupler, PlayAudio = playAudio, ViaChainInteraction = viaChainInteraction, @@ -841,11 +862,20 @@ public void SendTrainUncouple(Coupler coupler, bool playAudio, bool dueToBrokenC public void SendHoseConnected(Coupler coupler, Coupler otherCoupler, bool playAudio) { + ushort couplerNetId = coupler.train.GetNetId(); + ushort otherCouplerNetId = otherCoupler.train.GetNetId(); + + if (couplerNetId == 0 || otherCouplerNetId == 0) + { + Multiplayer.LogWarning($"SendHoseConnected failed. Coupler: {coupler.name} {couplerNetId}, OtherCoupler: {otherCoupler.name} {otherCouplerNetId}"); + return; + } + SendPacketToServer(new CommonHoseConnectedPacket { - NetId = coupler.train.GetNetId(), + NetId = couplerNetId, IsFront = coupler.isFrontCoupler, - OtherNetId = otherCoupler.train.GetNetId(), + OtherNetId = otherCouplerNetId, OtherIsFront = otherCoupler.isFrontCoupler, PlayAudio = playAudio }, DeliveryMethod.ReliableUnordered); @@ -853,9 +883,17 @@ public void SendHoseConnected(Coupler coupler, Coupler otherCoupler, bool playAu public void SendHoseDisconnected(Coupler coupler, bool playAudio) { + ushort couplerNetId = coupler.train.GetNetId(); + + if (couplerNetId == 0) + { + Multiplayer.LogWarning($"SendHoseDisconnected failed. Coupler: {coupler.name} {couplerNetId}"); + return; + } + SendPacketToServer(new CommonHoseDisconnectedPacket { - NetId = coupler.train.GetNetId(), + NetId = couplerNetId, IsFront = coupler.isFrontCoupler, PlayAudio = playAudio }, DeliveryMethod.ReliableUnordered); @@ -863,11 +901,20 @@ public void SendHoseDisconnected(Coupler coupler, bool playAudio) public void SendMuConnected(MultipleUnitCable cable, MultipleUnitCable otherCable, bool playAudio) { + ushort cableNetId = cable.muModule.train.GetNetId(); + ushort otherCableNetId = otherCable.muModule.train.GetNetId(); + + if (cableNetId == 0 || otherCableNetId == 0) + { + Multiplayer.LogWarning($"SendMuConnected failed. Cable: {cable.muModule.train.name} {cableNetId}, OtherCable: {otherCable.muModule.train.name} {otherCableNetId}"); + return; + } + SendPacketToServer(new CommonMuConnectedPacket { - NetId = cable.muModule.train.GetNetId(), + NetId = cableNetId, IsFront = cable.isFront, - OtherNetId = otherCable.muModule.train.GetNetId(), + OtherNetId = otherCableNetId, OtherIsFront = otherCable.isFront, PlayAudio = playAudio }, DeliveryMethod.ReliableUnordered); @@ -875,6 +922,7 @@ public void SendMuConnected(MultipleUnitCable cable, MultipleUnitCable otherCabl public void SendMuDisconnected(ushort netId, MultipleUnitCable cable, bool playAudio) { + SendPacketToServer(new CommonMuDisconnectedPacket { NetId = netId, diff --git a/Multiplayer/Networking/Managers/Server/ChatManager.cs b/Multiplayer/Networking/Managers/Server/ChatManager.cs index bee85ed..0e9b643 100644 --- a/Multiplayer/Networking/Managers/Server/ChatManager.cs +++ b/Multiplayer/Networking/Managers/Server/ChatManager.cs @@ -15,6 +15,8 @@ public static class ChatManager public const string COMMAND_WHISPER_SHORT = "w"; public const string COMMAND_HELP_SHORT = "?"; public const string COMMAND_HELP = "help"; + public const string COMMAND_LOG = "log"; + public const string COMMAND_LOG_SHORT = "l"; public const string MESSAGE_COLOUR_SERVER = "9CDCFE"; public const string MESSAGE_COLOUR_HELP = "00FF00"; @@ -58,6 +60,13 @@ public static void ProcessMessage(string message, NetPeer sender) HelpMessage(sender); break; + case COMMAND_LOG_SHORT: + Multiplayer.specLog = !Multiplayer.specLog; + break; + case COMMAND_LOG: + Multiplayer.specLog = !Multiplayer.specLog; + break; + //allow messages that are not commands to go through default: ChatMessage(message,player.Username, sender); diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index b665a59..185ceb6 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -226,6 +226,14 @@ public void SendSpawnTrainCar(NetworkedTrainCar networkedTrainCar) public void SendDestroyTrainCar(TrainCar trainCar) { + ushort netID = trainCar.GetNetId(); + + if (netID == 0) + { + Multiplayer.LogWarning($"SendDestroyTrainCar failed. TrainCar: {trainCar.name} {netID}"); + return; + } + SendPacketToAll(new ClientboundDestroyTrainCarPacket { NetId = trainCar.GetNetId() @@ -602,7 +610,12 @@ private void OnServerboundPlayerCarPacket(ServerboundPlayerCarPacket packet, Net return; if (TryGetServerPlayer(peer, out ServerPlayer player)) + { player.CarId = packet.CarId; + player.RawPosition = packet.Position; + player.RawRotationY = packet.RotationY; + + } ClientboundPlayerCarPacket clientboundPacket = new() { diff --git a/Multiplayer/Networking/Packets/Serverbound/ServerboundPlayerCarPacket.cs b/Multiplayer/Networking/Packets/Serverbound/ServerboundPlayerCarPacket.cs index 8ca39e9..2fd8ba7 100644 --- a/Multiplayer/Networking/Packets/Serverbound/ServerboundPlayerCarPacket.cs +++ b/Multiplayer/Networking/Packets/Serverbound/ServerboundPlayerCarPacket.cs @@ -1,6 +1,11 @@ +using UnityEngine; + namespace Multiplayer.Networking.Packets.Serverbound; public class ServerboundPlayerCarPacket { public ushort CarId { get; set; } + public Vector3 Position { get; set; } + public Vector2 MoveDir { get; set; } + public float RotationY { get; set; } } diff --git a/Multiplayer/Patches/CommsRadio/CommsRadioCarDeleterPatch.cs b/Multiplayer/Patches/CommsRadio/CommsRadioCarDeleterPatch.cs index f89ec5d..7d17bc3 100644 --- a/Multiplayer/Patches/CommsRadio/CommsRadioCarDeleterPatch.cs +++ b/Multiplayer/Patches/CommsRadio/CommsRadioCarDeleterPatch.cs @@ -23,14 +23,17 @@ private static bool OnUse_Prefix(CommsRadioCarDeleter __instance) if (Inventory.Instance.PlayerMoney < __instance.removePrice) return true; - if (__instance.carToDelete.TryNetworked(out NetworkedTrainCar networkedTrainCar) && networkedTrainCar.HasPlayers) + __instance.carToDelete.TryNetworked(out NetworkedTrainCar networkedTrainCar); + + if (networkedTrainCar == null || networkedTrainCar != null && (networkedTrainCar.HasPlayers || networkedTrainCar.NetId == 0)) { + Multiplayer.LogDebug(() => $"CommsRadioCarDeleter unable to delete car: {__instance.carToDelete.name}, hasPlayer: {networkedTrainCar?.HasPlayers}, netId {networkedTrainCar?.NetId} "); CommsRadioController.PlayAudioFromRadio(__instance.cancelSound, __instance.transform); __instance.ClearFlags(); return false; } - NetworkLifecycle.Instance.Client.SendTrainDeleteRequest(__instance.carToDelete.GetNetId()); + NetworkLifecycle.Instance.Client.SendTrainDeleteRequest(networkedTrainCar.NetId); CoroutineManager.Instance.StartCoroutine(PlaySoundsLater(__instance, __instance.carToDelete.transform.position, __instance.removePrice > 0)); __instance.ClearFlags(); diff --git a/Multiplayer/Patches/CommsRadio/RerailControllerPatch.cs b/Multiplayer/Patches/CommsRadio/RerailControllerPatch.cs index 7982127..f683c34 100644 --- a/Multiplayer/Patches/CommsRadio/RerailControllerPatch.cs +++ b/Multiplayer/Patches/CommsRadio/RerailControllerPatch.cs @@ -26,8 +26,19 @@ private static bool OnUse_Prefix(RerailController __instance) if (Inventory.Instance.PlayerMoney < __instance.rerailPrice) return true; + __instance.carToRerail.TryNetworked(out NetworkedTrainCar networkedTrainCar); + + if (networkedTrainCar == null || networkedTrainCar != null && networkedTrainCar.NetId == 0) + { + Multiplayer.LogDebug(() => $"RerailController unable to rerail car: {__instance.carToRerail.name}, netId {networkedTrainCar?.NetId} "); + //CommsRadioController.PlayAudioFromRadio(__instance.cancelSound, __instance.transform); + __instance.ClearFlags(); + return false; + } + + NetworkLifecycle.Instance.Client.SendTrainRerailRequest( - __instance.carToRerail.GetNetId(), + networkedTrainCar.NetId, NetworkedRailTrack.GetFromRailTrack(__instance.rerailTrack).NetId, __instance.rerailPointWorldAbsPosition, __instance.rerailPointWorldForward diff --git a/Multiplayer/Patches/Jobs/JobPatch.cs b/Multiplayer/Patches/Jobs/JobPatch.cs new file mode 100644 index 0000000..fed0f44 --- /dev/null +++ b/Multiplayer/Patches/Jobs/JobPatch.cs @@ -0,0 +1,21 @@ +using DV.Interaction; +using DV.Logic.Job; +using HarmonyLib; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +/* +namespace Multiplayer.Patches.Jobs; + +[HarmonyPatch(typeof(Job), nameof(Job.ExpireJob))] +public static class JobPatch +{ + private static bool Prefix(Job __instance) + { + Multiplayer.LogWarning($"Trying to expire {__instance.ID}\r\n"+ new System.Diagnostics.StackTrace()); + return false; + } +} +*/ diff --git a/Multiplayer/Patches/Jobs/StationJobGenerationRangePatch.cs b/Multiplayer/Patches/Jobs/StationJobGenerationRangePatch.cs index e2a8766..343979a 100644 --- a/Multiplayer/Patches/Jobs/StationJobGenerationRangePatch.cs +++ b/Multiplayer/Patches/Jobs/StationJobGenerationRangePatch.cs @@ -15,6 +15,7 @@ private static bool Prefix(StationJobGenerationRange __instance, ref float __res return true; Vector3 anchor = __instance.stationCenterAnchor.position; + Vector3 anchor2 = anchor - WorldMover.currentMove; __result = float.MaxValue; @@ -22,19 +23,31 @@ private static bool Prefix(StationJobGenerationRange __instance, ref float __res foreach (ServerPlayer serverPlayer in NetworkLifecycle.Instance.Server.ServerPlayers) { float sqDist = (serverPlayer.WorldPosition - anchor).sqrMagnitude; + //float sqDist2 = (serverPlayer.AbsoluteWorldPosition - anchor2).sqrMagnitude; + float sqDist3 = (PlayerManager.PlayerTransform.position - __instance.stationCenterAnchor.position).sqrMagnitude; + if (sqDist < __result) __result = sqDist; - if (frameCount == 60) + if (/*frameCount == 60 &&*/ Multiplayer.specLog && __instance.name == "StationFRS") { Multiplayer.LogDebug(() => $"PlayerSqrDistanceFromStationCenter:\r\n\t" + $"player: '{serverPlayer.Username}',\r\n\t\t" + - $"absPos: {serverPlayer.AbsoluteWorldPosition.ToString()},\r\n\t\t" + + //$"absPos: {serverPlayer.AbsoluteWorldPosition.ToString()},\r\n\t\t" + $"rawPos: {serverPlayer.RawPosition.ToString()},\r\n\t\t" + $"worldPos: {serverPlayer.WorldPosition.ToString()},\r\n\t" + $"station name: '{__instance.name}',\r\n\t\t" + - $"anchor: {anchor.ToString()},\r\n\t" + - $"sqDist: {sqDist}"); + $"anchor: {anchor.ToString()},\r\n\t\t" + + $"anchor2: {anchor - WorldMover.currentMove},\r\n\t\t" + + $"anchorTransform: {__instance.transform.TransformPoint(anchor)},\r\n\t\t" + + $"anchorTransform2: {__instance.transform.TransformPoint(anchor) - WorldMover.currentMove},\r\n\t\t" + + $"anchorInverseTransform: {__instance.transform.InverseTransformPoint(anchor)},\r\n\t\t" + + $"anchorInverseTransform2: {__instance.transform.InverseTransformPoint(anchor) - WorldMover.currentMove},\r\n\t" + + $"sqDist: {sqDist},\r\n\t" + + //$"sqDist2: {sqDist2},\r\n\t" + + $"sqDist3: {sqDist3},\r\n\t" + + $"sqDistTransform: {(serverPlayer.WorldPosition - __instance.transform.TransformPoint(anchor)).sqrMagnitude},\r\n\t" + + $"sqDistInverseTransform: {(serverPlayer.WorldPosition - __instance.transform.InverseTransformPoint(anchor)).sqrMagnitude}"); } } @@ -52,22 +65,54 @@ private static bool Prefix(StationJobGenerationRange __instance, ref float __res [HarmonyPatch(typeof(StationJobGenerationRange), nameof(StationJobGenerationRange.PlayerSqrDistanceFromStationOffice), MethodType.Getter)] public static class StationJobGenerationRange_PlayerSqrDistanceFromStationOffice_Patch { + private static int frameCount = 0; private static bool Prefix(StationJobGenerationRange __instance, ref float __result) { if (!NetworkLifecycle.Instance.IsHost()) return true; Vector3 anchor = __instance.transform.position; + Vector3 anchor2 = anchor - WorldMover.currentMove; __result = float.MaxValue; //Loop through all of the players and return the one thats closest to the anchor foreach (ServerPlayer serverPlayer in NetworkLifecycle.Instance.Server.ServerPlayers) { float sqDist = (serverPlayer.WorldPosition - anchor).sqrMagnitude; + //float sqDist2 = (serverPlayer.AbsoluteWorldPosition - anchor2).sqrMagnitude; + float sqDist3 = (PlayerManager.PlayerTransform.position - __instance.stationCenterAnchor.position).sqrMagnitude; + if (sqDist < __result) __result = sqDist; + + if (/*frameCount == 60 &&*/ Multiplayer.specLog && __instance.name == "StationFRS") + { + Multiplayer.LogDebug(() => $"PlayerSqrDistanceFromStationOffice:\r\n\t" + + $"player: '{serverPlayer.Username}',\r\n\t\t" + + //$"absPos: {serverPlayer.AbsoluteWorldPosition.ToString()},\r\n\t\t" + + $"rawPos: {serverPlayer.RawPosition.ToString()},\r\n\t\t" + + $"worldPos: {serverPlayer.WorldPosition.ToString()},\r\n\t" + + $"station name: '{__instance.name}',\r\n\t\t" + + $"anchor: {anchor.ToString()},\r\n\t\t" + + $"anchor2: {anchor - WorldMover.currentMove},\r\n\t\t" + + $"anchorTransform: {__instance.transform.TransformPoint(anchor)},\r\n\t\t" + + $"anchorTransform2: {__instance.transform.TransformPoint(anchor) - WorldMover.currentMove},\r\n\t\t" + + $"anchorInverseTransform: {__instance.transform.InverseTransformPoint(anchor)},\r\n\t\t" + + $"anchorInverseTransform2: {__instance.transform.InverseTransformPoint(anchor) - WorldMover.currentMove},\r\n\t" + + $"sqDist: {sqDist},\r\n\t" + + //$"sqDist2: {sqDist2},\r\n\t" + + $"sqDist3: {sqDist3},\r\n\t" + + $"sqDistTransform: {(serverPlayer.WorldPosition - __instance.transform.TransformPoint(anchor)).sqrMagnitude},\r\n\t" + + $"sqDistInverseTransform: {(serverPlayer.WorldPosition - __instance.transform.InverseTransformPoint(anchor)).sqrMagnitude}"); + } } + frameCount++; + if (frameCount > 60) + { + frameCount = 0; + + } return false; } } diff --git a/Multiplayer/Patches/Player/CustomFirstPersonControllerPatch.cs b/Multiplayer/Patches/Player/CustomFirstPersonControllerPatch.cs index c394305..1c83a62 100644 --- a/Multiplayer/Patches/Player/CustomFirstPersonControllerPatch.cs +++ b/Multiplayer/Patches/Player/CustomFirstPersonControllerPatch.cs @@ -39,8 +39,23 @@ private static void OnDestroy() private static void OnCarChanged(TrainCar trainCar) { + //Multiplayer.LogDebug(() => $"OnCarChanged isOnCar: {isOnCar}, car: {trainCar?.name}"); + isOnCar = trainCar != null; - NetworkLifecycle.Instance.Client.SendPlayerCar(!isOnCar ? (ushort)0 : trainCar.GetNetId()); + + //Multiplayer.LogDebug(() => $"OnCarChanged isOnCar: {isOnCar}, car: {trainCar?.name}"); + + Vector3 position = isOnCar ? PlayerManager.PlayerTransform.localPosition : PlayerManager.GetWorldAbsolutePlayerPosition(); + float rotationY = (isOnCar ? PlayerManager.PlayerTransform.localEulerAngles : PlayerManager.PlayerTransform.eulerAngles).y; + + //Multiplayer.LogDebug(() => $"OnCarChanged isOnCar: {isOnCar}, car: {trainCar?.name}, lastPosition: {lastPosition}, lastRotation: {lastRotationY}, position: {position}, rotation: {rotationY}"); + + lastPosition = position; + lastRotationY = rotationY; + + + + NetworkLifecycle.Instance.Client.SendPlayerCar(!isOnCar ? (ushort)0 : trainCar.GetNetId(), lastPosition, PlayerManager.PlayerTransform.InverseTransformDirection(fps.m_MoveDir), lastRotationY, isJumping); } private static void OnTick(uint tick) diff --git a/Multiplayer/Patches/Train/WindowsBreakingControllerPatch.cs b/Multiplayer/Patches/Train/WindowsBreakingControllerPatch.cs index 40949f2..96aa524 100644 --- a/Multiplayer/Patches/Train/WindowsBreakingControllerPatch.cs +++ b/Multiplayer/Patches/Train/WindowsBreakingControllerPatch.cs @@ -15,7 +15,21 @@ public static void BreakWindowsFromCollision_Postfix(WindowsBreakingController _ { if (!NetworkLifecycle.Instance.IsHost()) return; - ushort netId = TrainCar.Resolve(__instance.transform).GetNetId(); + + TrainCar car = TrainCar.Resolve(__instance.transform); + if (car == null) + { + Multiplayer.LogWarning($"BreakWindowsFromCollision failed, unable to resolve TrainCar"); + return; + } + + ushort netId = car.GetNetId(); + if(netId == 0) + { + Multiplayer.LogWarning($"BreakWindowsFromCollision failed, {car.name}"); + return; + } + NetworkLifecycle.Instance.Server.SendWindowsBroken(netId, forceDirection); } diff --git a/Multiplayer/Utils/DvExtensions.cs b/Multiplayer/Utils/DvExtensions.cs index 16121b5..72d52a5 100644 --- a/Multiplayer/Utils/DvExtensions.cs +++ b/Multiplayer/Utils/DvExtensions.cs @@ -7,6 +7,7 @@ using UnityEngine; using UnityEngine.UI; using System.Linq; +using System.Diagnostics; @@ -24,7 +25,8 @@ public static ushort GetNetId(this TrainCar car) netId = networkedTrainCar.NetId; if (netId == 0) - throw new InvalidOperationException($"NetId for {car.carLivery.id} ({car.ID}) isn't initialized!"); + Multiplayer.LogWarning($"NetId for {car.carLivery.id} ({car.ID}) isn't initialized!\r\n" + new System.Diagnostics.StackTrace()); + //throw new InvalidOperationException($"NetId for {car.carLivery.id} ({car.ID}) isn't initialized!"); return netId; } From 1e09a9f16fe8f23940c70c876496eab8b23c308c Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 27 Jul 2024 23:29:09 +1000 Subject: [PATCH 054/188] Optimised car destroy code, improvements to lag due to debug output Seems to be an issue with deletion of cars - client loses frame rate while cars are initialising or maybe deleted but sim packets still arriving - more investigation required. Disabled some debug prints to reduce client lag (related to above issue). --- .../Networking/Managers/Server/NetworkServer.cs | 10 +++++----- Multiplayer/Patches/Train/CarSpawnerPatch.cs | 2 +- .../Patches/Train/WindowsBreakingControllerPatch.cs | 11 ++++++++++- Multiplayer/Utils/DvExtensions.cs | 4 ++-- 4 files changed, 18 insertions(+), 9 deletions(-) diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index 185ceb6..a8aaf86 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -224,19 +224,19 @@ public void SendSpawnTrainCar(NetworkedTrainCar networkedTrainCar) SendPacketToAll(ClientboundSpawnTrainCarPacket.FromTrainCar(networkedTrainCar), DeliveryMethod.ReliableOrdered, selfPeer); } - public void SendDestroyTrainCar(TrainCar trainCar) + public void SendDestroyTrainCar(ushort netId) { - ushort netID = trainCar.GetNetId(); + //ushort netID = trainCar.GetNetId(); - if (netID == 0) + if (netId == 0) { - Multiplayer.LogWarning($"SendDestroyTrainCar failed. TrainCar: {trainCar.name} {netID}"); + Multiplayer.LogWarning($"SendDestroyTrainCar failed. netId {netId}"); return; } SendPacketToAll(new ClientboundDestroyTrainCarPacket { - NetId = trainCar.GetNetId() + NetId = netId, }, DeliveryMethod.ReliableOrdered, selfPeer); } diff --git a/Multiplayer/Patches/Train/CarSpawnerPatch.cs b/Multiplayer/Patches/Train/CarSpawnerPatch.cs index 06d2ae4..a163f7f 100644 --- a/Multiplayer/Patches/Train/CarSpawnerPatch.cs +++ b/Multiplayer/Patches/Train/CarSpawnerPatch.cs @@ -15,6 +15,6 @@ private static void Prefix(TrainCar trainCar) if (!trainCar.TryNetworked(out NetworkedTrainCar networkedTrainCar)) return; networkedTrainCar.IsDestroying = true; - NetworkLifecycle.Instance.Server?.SendDestroyTrainCar(trainCar); + NetworkLifecycle.Instance.Server?.SendDestroyTrainCar(networkedTrainCar.NetId); } } diff --git a/Multiplayer/Patches/Train/WindowsBreakingControllerPatch.cs b/Multiplayer/Patches/Train/WindowsBreakingControllerPatch.cs index 96aa524..f26aa57 100644 --- a/Multiplayer/Patches/Train/WindowsBreakingControllerPatch.cs +++ b/Multiplayer/Patches/Train/WindowsBreakingControllerPatch.cs @@ -39,7 +39,16 @@ public static void RepairWindows_Postfix(WindowsBreakingController __instance) { if (!NetworkLifecycle.Instance.IsHost()) return; - ushort netId = TrainCar.Resolve(__instance.transform).GetNetId(); + + TrainCar car = TrainCar.Resolve(__instance.transform); + ushort netId = car.GetNetId(); + + if (netId == 0) + { + Multiplayer.LogWarning($"RepairWindows failed, {car.name}"); + return; + } + NetworkLifecycle.Instance.Server.SendWindowsRepaired(netId); } } diff --git a/Multiplayer/Utils/DvExtensions.cs b/Multiplayer/Utils/DvExtensions.cs index 72d52a5..1254424 100644 --- a/Multiplayer/Utils/DvExtensions.cs +++ b/Multiplayer/Utils/DvExtensions.cs @@ -23,9 +23,9 @@ public static ushort GetNetId(this TrainCar car) if (car != null && car.TryNetworked(out NetworkedTrainCar networkedTrainCar)) netId = networkedTrainCar.NetId; - +/* if (netId == 0) - Multiplayer.LogWarning($"NetId for {car.carLivery.id} ({car.ID}) isn't initialized!\r\n" + new System.Diagnostics.StackTrace()); + Multiplayer.LogWarning($"NetId for {car.carLivery.id} ({car.ID}) isn't initialized!\r\n" + (Multiplayer.Settings.DebugLogging ? new System.Diagnostics.StackTrace() : ""));*/ //throw new InvalidOperationException($"NetId for {car.carLivery.id} ({car.ID}) isn't initialized!"); return netId; } From 64a899521fe3b6cff383095d1ec29764f2aac718 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 28 Jul 2024 17:28:41 +1000 Subject: [PATCH 055/188] Bug fixes and features Fixes: Windows breaking when a locomotive is repaired Features: Added player kick command Started work on NAT/firewall punching Enhanced server join process to add fallback from IPv6 to IPv6 Punched to IPv4 to IPv4 Punched to allow a smoother player experience. --- Lobby Servers/PHP Server/MySQLDatabase.php | 3 +- Lobby Servers/PHP Server/index.php | 2 +- .../MainMenu/MainMenuThingsAndStuff.cs | 9 + .../Components/MainMenu/ServerBrowserPane.cs | 300 +++++++++++++++--- .../Components/Networking/NetworkLifecycle.cs | 7 +- Multiplayer/Locale.cs | 2 +- .../Managers/Client/NetworkClient.cs | 50 ++- .../Networking/Managers/NetworkManager.cs | 6 +- .../Networking/Managers/Server/ChatManager.cs | 41 +++ .../Managers/Server/NetworkServer.cs | 24 +- .../ClientboundPlayerKickPacket.cs | 4 + .../Train/WindowsBreakingControllerPatch.cs | 5 +- 12 files changed, 386 insertions(+), 67 deletions(-) create mode 100644 Multiplayer/Networking/Packets/Clientbound/ClientboundPlayerKickPacket.cs diff --git a/Lobby Servers/PHP Server/MySQLDatabase.php b/Lobby Servers/PHP Server/MySQLDatabase.php index 7810f4c..c6caf42 100644 --- a/Lobby Servers/PHP Server/MySQLDatabase.php +++ b/Lobby Servers/PHP Server/MySQLDatabase.php @@ -33,8 +33,7 @@ public function addGameServer($data) { ]); return json_encode([ "game_server_id" => $data['game_server_id'], - "private_key" => $data['private_key'], - "ipv4_request" => !isset($data['ipv4']) + "private_key" => $data['private_key'] ]); } diff --git a/Lobby Servers/PHP Server/index.php b/Lobby Servers/PHP Server/index.php index 7b00e06..68751f1 100644 --- a/Lobby Servers/PHP Server/index.php +++ b/Lobby Servers/PHP Server/index.php @@ -109,7 +109,7 @@ function validate_server_info($data) { } if ( - //make sure we have at lease one IP + //make sure we have at least one IP $data['ipv4'] == '' && $data['ipv6'] == '' || //Make sure we have all required fields diff --git a/Multiplayer/Components/MainMenu/MainMenuThingsAndStuff.cs b/Multiplayer/Components/MainMenu/MainMenuThingsAndStuff.cs index 732a941..05b3487 100644 --- a/Multiplayer/Components/MainMenu/MainMenuThingsAndStuff.cs +++ b/Multiplayer/Components/MainMenu/MainMenuThingsAndStuff.cs @@ -93,6 +93,15 @@ private Popup ShowPopup(Popup popup) return null; } + public void ShowOkPopup(string text, Action onClick) + { + var popup = ShowOkPopup(); + if (popup == null) return; + + popup.labelTMPro.text = text; + popup.Closed += _ => onClick(); + } + /// A function to apply to the MainMenuPopupManager while the object is disabled public static void Create(Action func) { diff --git a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs index 6a41f7e..6cd490d 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs @@ -16,6 +16,8 @@ using Multiplayer.Networking.Data; using DV; using System.Net; +using LiteNetLib; +using LiteNetLib.Utils; namespace Multiplayer.Components.MainMenu { @@ -63,6 +65,21 @@ public class ServerBrowserPane : MonoBehaviour string password = null; bool direct = false; + private ConnectionState connectionState = ConnectionState.NotConnected; + private Popup connectingPopup; + private int attempt; + + private enum ConnectionState + { + NotConnected, + AttemptingIPv6, + AttemptingIPv6Punch, + AttemptingIPv4, + AttemptingIPv4Punch, + Failed, + Aborted + } + 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 @@ -325,20 +342,6 @@ private void JoinAction() buttonDirectIP.ToggleInteractable(false); buttonJoin.ToggleInteractable(false); - //TODO: Add logic to allow IPv6 addresses to be used - if (selectedServer.ipv6 != null && - selectedServer.ipv6 != string.Empty && - IPv6Regex.IsMatch(selectedServer.ipv6)) - { - address = selectedServer.ipv6; - }else if (selectedServer.ipv4 != null && - selectedServer.ipv4 != string.Empty && - IPv4Regex.IsMatch(selectedServer.ipv4)) - { - address = selectedServer.ipv4; - } - Multiplayer.Log($"Selected IP address is: {address}"); - if (selectedServer.HasPassword) { //not making a direct connection @@ -351,8 +354,8 @@ private void JoinAction() return; } - //No password, just connect - SingletonBehaviour.Instance.StartClient(address, selectedServer.port, null, false); + AttemptConnection(); + } } @@ -486,10 +489,19 @@ private void ShowIpPopup() } } - ShowOkPopup(Locale.SERVER_BROWSER__IP_INVALID, ShowIpPopup); + MainMenuThingsAndStuff.Instance.ShowOkPopup(Locale.SERVER_BROWSER__IP_INVALID, ShowIpPopup); } else { + if (IPv4Regex.IsMatch(result.data)) + { + connectionState = ConnectionState.AttemptingIPv4; + } + else + { + connectionState = ConnectionState.AttemptingIPv6; + } + address = result.data; ShowPortPopup(); } @@ -521,14 +533,14 @@ private void ShowPortPopup() if (!PortRegex.IsMatch(result.data)) { - ShowOkPopup(Locale.SERVER_BROWSER__PORT_INVALID, ShowIpPopup); + MainMenuThingsAndStuff.Instance.ShowOkPopup(Locale.SERVER_BROWSER__PORT_INVALID, ShowIpPopup); } else { portNumber = ushort.Parse(result.data); ShowPasswordPopup(); } - }; + }; } @@ -571,28 +583,240 @@ private void ShowPasswordPopup() } - SingletonBehaviour.Instance.StartClient(address, portNumber, result.data, false); + password = result.data; - //ShowConnectingPopup(); // Show a connecting message - //SingletonBehaviour.Instance.ConnectionFailed += HandleConnectionFailed; - //SingletonBehaviour.Instance.ConnectionEstablished += HandleConnectionEstablished; + AttemptConnection(); + //SingletonBehaviour.Instance.StartClient(address, portNumber, result.data, false, OnDisconnect); }; } - // Example of handling connection success - private void HandleConnectionEstablished() + public void ShowConnectingPopup() + { + var popup = MainMenuThingsAndStuff.Instance.ShowOkPopup(); + + if (popup == null) + { + Multiplayer.LogError("ShowConnectingPopup() Popup not found."); + return; + } + + connectingPopup = popup; + + Localize loc = popup.positiveButton.GetComponentInChildren(); + loc.key ="cancel"; + loc.UpdateLocalization(); + + + popup.labelTMPro.text = $"Connecting, please wait...\r\nAttempt: {attempt}"; //to be localised + + popup.Closed += _ => + { + connectionState = ConnectionState.Aborted; + }; + + } + + private void AttemptConnection() { - // Connection established, handle the UI or game state accordingly - Multiplayer.Log("Connection established!"); - // HideConnectingPopup(); // Hide the connecting message + + Multiplayer.Log($"AttemptConnection Direct: {direct}, Address: {address}"); + + attempt = 0; + ShowConnectingPopup(); + + + if (!direct) + { + if (selectedServer.ipv6 != null && selectedServer.ipv6 != string.Empty) + { + address = selectedServer.ipv6; + } + else + { + address = selectedServer.ipv4; + } + } + + Multiplayer.Log($"AttemptConnection address: {address}"); + + if (IPAddress.TryParse(address, out IPAddress IPaddress)) + { + Multiplayer.Log($"AttemptConnection tryParse: {IPaddress.AddressFamily}"); + + if (IPaddress.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork) + { + AttemptIPv4(); + } + else if(IPaddress.AddressFamily == System.Net.Sockets.AddressFamily.InterNetworkV6) + { + AttemptIPv6(); + } + + return; + } + + Multiplayer.LogError($"IP address invalid: {address}"); + + AttemptFail(); } - // Example of handling connection failure - private void HandleConnectionFailed() + private void AttemptIPv6() { - // Connection failed, show an error message or handle the failure scenario - Multiplayer.LogError("Connection failed!"); - // ShowConnectionFailedPopup(); + Multiplayer.Log($"AttemptIPv6() {address}"); + + if (connectionState == ConnectionState.Aborted) + return; + + attempt++; + if (connectingPopup != null) + connectingPopup.labelTMPro.text = $"Connecting, please wait...\r\nAttempt: {attempt}"; + + connectionState = ConnectionState.AttemptingIPv6; + SingletonBehaviour.Instance.StartClient(address, selectedServer.port, password, false, OnDisconnect); + + } + + private void AttemptIPv6Punch() + { + Multiplayer.Log($"AttemptIPv6Punch() {address}"); + + if (connectionState == ConnectionState.Aborted) + return; + + attempt++; + if(connectingPopup != null) + connectingPopup.labelTMPro.text = $"Connecting, please wait...\r\nAttempt: {attempt}"; + + //punching not implemented we'll just try again for now + connectionState = ConnectionState.AttemptingIPv6Punch; + SingletonBehaviour.Instance.StartClient(address, selectedServer.port, password, false, OnDisconnect); + + } + + private void AttemptIPv4() + { + Multiplayer.Log($"AttemptIPv4() {address}"); + + if (connectionState == ConnectionState.Aborted) + return; + + attempt++; + if (connectingPopup != null) + connectingPopup.labelTMPro.text = $"Connecting, please wait...\r\nAttempt: {attempt}"; + + if (!direct) + { + if(selectedServer.ipv4 == null || selectedServer.ipv4 == string.Empty) + { + AttemptFail(); + return; + } + + address = selectedServer.ipv4; + } + + Multiplayer.Log($"AttemptIPv4() {address}"); + + if (IPAddress.TryParse(address, out IPAddress IPaddress)) + { + if (IPaddress.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork) + { + connectionState = ConnectionState.AttemptingIPv4; + SingletonBehaviour.Instance.StartClient(address, selectedServer.port, password, false, OnDisconnect); + return; + } + } + + AttemptFail(); + } + + private void AttemptIPv4Punch() + { + Multiplayer.Log($"AttemptIPv4Punch() {address}"); + + if (connectionState == ConnectionState.Aborted) + return; + + attempt++; + if (connectingPopup != null) + connectingPopup.labelTMPro.text = $"Connecting, please wait...\r\nAttempt: {attempt}"; + + //punching not implemented we'll just try again for now + connectionState = ConnectionState.AttemptingIPv4Punch; + SingletonBehaviour.Instance.StartClient(address, selectedServer.port, password, false, OnDisconnect); + } + + private void AttemptFail() + { + connectionState = ConnectionState.Failed; + + if (connectingPopup != null) + { + connectingPopup.RequestClose(PopupClosedByAction.Abortion, null); + + string message = "Unable to reach host!"; //add translations + + MainMenuThingsAndStuff.Instance.ShowOkPopup(message, () => { }); + } + } + + + private void OnDisconnect(DisconnectReason reason, string message) + { + Multiplayer.LogError($"Connection failed! {reason}, \"{message}\""); + + switch (reason) + { + case DisconnectReason.UnknownHost: + if (message == null || message.Length == 0) + { + message = "Unknown Host"; //add translations + } + break; + case DisconnectReason.DisconnectPeerCalled: + if (message == null || message.Length == 0) + { + message = "Player kicked"; //add translations + } + break; + case DisconnectReason.ConnectionFailed: + + //Check our connectionState + switch (connectionState) + { + case ConnectionState.AttemptingIPv6: + AttemptIPv6Punch(); + return; + case ConnectionState.AttemptingIPv6Punch: + AttemptIPv4(); + return; + case ConnectionState.AttemptingIPv4: + AttemptIPv4Punch(); + return; + case ConnectionState.AttemptingIPv4Punch: + AttemptFail(); + return; + } + break; + + case DisconnectReason.ConnectionRejected: + if (message == null || message.Length == 0) + { + message = "Rejected!"; //add translations + } + break; + case DisconnectReason.RemoteConnectionClose: + if (message == null || message.Length == 0) + { + message = "Server shutdown"; //add translations + } + break; + } + + NetworkLifecycle.Instance.QueueMainMenuEvent(() => + { + MainMenuThingsAndStuff.Instance.ShowOkPopup(message, ()=>{ }); + }); } IEnumerator GetRequest(string uri) @@ -661,16 +885,6 @@ IEnumerator GetRequest(string uri) serverRefreshing = false; timePassed = 0; } - - 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) { foreach (var button in buttons) diff --git a/Multiplayer/Components/Networking/NetworkLifecycle.cs b/Multiplayer/Components/Networking/NetworkLifecycle.cs index e07dda8..2fdcfc8 100644 --- a/Multiplayer/Components/Networking/NetworkLifecycle.cs +++ b/Multiplayer/Components/Networking/NetworkLifecycle.cs @@ -147,16 +147,15 @@ public bool StartServer(IDifficulty difficulty) if (!server.Start(port)) return false; Server = server; - StartClient("localhost", port, Multiplayer.Settings.Password, isSinglePlayer); + StartClient("localhost", port, Multiplayer.Settings.Password, isSinglePlayer, null/* (DisconnectReason dr,string msg) =>{ }*/); return true; } - - public void StartClient(string address, int port, string password, bool isSinglePlayer) + public void StartClient(string address, int port, string password, bool isSinglePlayer, Action onDisconnect ) { if (Client != null) throw new InvalidOperationException("NetworkManager already exists!"); NetworkClient client = new(Multiplayer.Settings); - client.Start(address, port, password, isSinglePlayer); + client.Start(address, port, password, isSinglePlayer, onDisconnect); Client = client; OnSettingsUpdated(Multiplayer.Settings); // Show stats if enabled } diff --git a/Multiplayer/Locale.cs b/Multiplayer/Locale.cs index dbfd637..75f693a 100644 --- a/Multiplayer/Locale.cs +++ b/Multiplayer/Locale.cs @@ -140,7 +140,7 @@ public static void Load(string localeDir) } csv = Csv.Parse(File.ReadAllText(path)); - Multiplayer.LogDebug(() => $"Locale dump: {Csv.Dump(csv)}"); + //Multiplayer.LogDebug(() => $"Locale dump: {Csv.Dump(csv)}"); } public static string Get(string key, string overrideLanguage = null) diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index c4c9bcd..68a9d3c 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -1,7 +1,5 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Security.Cryptography; +using System.Net; using System.Text; using DV; using DV.Damage; @@ -11,10 +9,8 @@ using DV.ServicePenalty.UI; using DV.ThingTypes; using DV.UI; -using DV.UIFramework; using DV.WeatherSystem; using LiteNetLib; -using Multiplayer.Components; using Multiplayer.Components.MainMenu; using Multiplayer.Components.Networking; using Multiplayer.Components.Networking.Jobs; @@ -45,6 +41,8 @@ public class NetworkClient : NetworkManager { protected override string LogPrefix => "[Client]"; + private Action onDisconnect; + public NetPeer selfPeer { get; private set; } public readonly ClientPlayerManager ClientPlayerManager; @@ -60,8 +58,9 @@ public NetworkClient(Settings settings) : base(settings) ClientPlayerManager = new ClientPlayerManager(); } - public void Start(string address, int port, string password, bool isSinglePlayer) + public void Start(string address, int port, string password, bool isSinglePlayer, Action onDisconnect) { + this.onDisconnect = onDisconnect; netManager.Start(); ServerboundClientLoginPacket serverboundClientLoginPacket = new() { @@ -80,6 +79,7 @@ protected override void Subscribe() netPacketProcessor.SubscribeReusable(OnClientboundServerDenyPacket); netPacketProcessor.SubscribeReusable(OnClientboundPlayerJoinedPacket); netPacketProcessor.SubscribeReusable(OnClientboundPlayerDisconnectPacket); + netPacketProcessor.SubscribeReusable(OnClientboundPlayerKickPacket); netPacketProcessor.SubscribeReusable(OnClientboundPlayerPositionPacket); netPacketProcessor.SubscribeReusable(OnClientboundPlayerCarPacket); netPacketProcessor.SubscribeReusable(OnClientboundPingUpdatePacket); @@ -143,7 +143,7 @@ public override void OnPeerDisconnected(NetPeer peer, DisconnectInfo disconnectI if (MainMenuThingsAndStuff.Instance != null) { - MainMenuThingsAndStuff.Instance.SwitchToDefaultMenu(); + //MainMenuThingsAndStuff.Instance.SwitchToDefaultMenu(); NetworkLifecycle.Instance.TriggerMainMenuEventLater(); } else @@ -151,7 +151,7 @@ public override void OnPeerDisconnected(NetPeer peer, DisconnectInfo disconnectI MainMenu.GoBackToMainMenu(); } - string text = $"{disconnectInfo.Reason}"; + //string message = $"{disconnectInfo.Reason}"; switch (disconnectInfo.Reason) { @@ -160,17 +160,21 @@ public override void OnPeerDisconnected(NetPeer peer, DisconnectInfo disconnectI netPacketProcessor.ReadAllPackets(disconnectInfo.AdditionalData); return; case DisconnectReason.RemoteConnectionClose: - text = "The server shut down"; + netPacketProcessor.ReadAllPackets(disconnectInfo.AdditionalData); + //message = "The server shut down"; break; } + /* NetworkLifecycle.Instance.QueueMainMenuEvent(() => { Popup popup = MainMenuThingsAndStuff.Instance.ShowOkPopup(); if (popup == null) return; popup.labelTMPro.text = text; - }); + });*/ + + onDisconnect(disconnectInfo.Reason, null); } public override void OnNetworkLatencyUpdate(NetPeer peer, int latency) @@ -185,15 +189,29 @@ public override void OnConnectionRequest(ConnectionRequest request) #endregion + #region NAT Punch Events + public override void OnNatIntroductionRequest(IPEndPoint localEndPoint, IPEndPoint remoteEndPoint, string token) + { + //do some stuff here + } + public override void OnNatIntroductionSuccess(IPEndPoint targetEndPoint, NatAddressType type, string token) + { + //do other stuff here + } + #endregion + #region Listeners private void OnClientboundServerDenyPacket(ClientboundServerDenyPacket packet) { + + /* NetworkLifecycle.Instance.QueueMainMenuEvent(() => { Popup popup = MainMenuThingsAndStuff.Instance.ShowOkPopup(); if (popup == null) return; + */ string text = Locale.Get(packet.ReasonKey, packet.ReasonArgs); if (packet.Missing.Length != 0 || packet.Extra.Length != 0) @@ -210,8 +228,10 @@ private void OnClientboundServerDenyPacket(ClientboundServerDenyPacket packet) text += Locale.Get(Locale.DISCONN_REASON__MODS_EXTRA_KEY, placeholders: string.Join("\n - ", packet.Extra)); } - popup.labelTMPro.text = text; - }); + //popup.labelTMPro.text = text; + //}); + + onDisconnect(DisconnectReason.ConnectionRejected, text); } private void OnClientboundPlayerJoinedPacket(ClientboundPlayerJoinedPacket packet) @@ -228,6 +248,12 @@ private void OnClientboundPlayerDisconnectPacket(ClientboundPlayerDisconnectPack ClientPlayerManager.RemovePlayer(packet.Id); } + private void OnClientboundPlayerKickPacket(ClientboundPlayerKickPacket packet) + { + + string text = "You were kicked!"; //to be localised //Locale.Get(packet.ReasonKey, packet.ReasonArgs); + onDisconnect(DisconnectReason.ConnectionRejected, text); + } private void OnClientboundPlayerPositionPacket(ClientboundPlayerPositionPacket packet) { ClientPlayerManager.UpdatePosition(packet.Id, packet.Position, packet.MoveDir, packet.RotationY, packet.IsJumping, packet.IsOnCar); diff --git a/Multiplayer/Networking/Managers/NetworkManager.cs b/Multiplayer/Networking/Managers/NetworkManager.cs index f54520d..2d9c3eb 100644 --- a/Multiplayer/Networking/Managers/NetworkManager.cs +++ b/Multiplayer/Networking/Managers/NetworkManager.cs @@ -8,7 +8,7 @@ namespace Multiplayer.Networking.Listeners; -public abstract class NetworkManager : INetEventListener +public abstract class NetworkManager : INetEventListener, INatPunchListener { protected readonly NetPacketProcessor netPacketProcessor; protected readonly NetManager netManager; @@ -116,11 +116,15 @@ public void OnNetworkReceiveUnconnected(IPEndPoint remoteEndPoint, NetPacketRead // todo } + //Standard networking callbacks public abstract void OnPeerConnected(NetPeer peer); public abstract void OnPeerDisconnected(NetPeer peer, DisconnectInfo disconnectInfo); public abstract void OnNetworkLatencyUpdate(NetPeer peer, int latency); public abstract void OnConnectionRequest(ConnectionRequest request); + //NAT punching callbacks + public abstract void OnNatIntroductionRequest(IPEndPoint localEndPoint, IPEndPoint remoteEndPoint, string token); + public abstract void OnNatIntroductionSuccess(IPEndPoint targetEndPoint, NatAddressType type, string token); #endregion #region Logging diff --git a/Multiplayer/Networking/Managers/Server/ChatManager.cs b/Multiplayer/Networking/Managers/Server/ChatManager.cs index 0e9b643..1d3a7bb 100644 --- a/Multiplayer/Networking/Managers/Server/ChatManager.cs +++ b/Multiplayer/Networking/Managers/Server/ChatManager.cs @@ -17,6 +17,8 @@ public static class ChatManager public const string COMMAND_HELP = "help"; public const string COMMAND_LOG = "log"; public const string COMMAND_LOG_SHORT = "l"; + public const string COMMAND_KICK = "kick"; + //public const string COMMAND_KICK_SHORT = "kick"; public const string MESSAGE_COLOUR_SERVER = "9CDCFE"; public const string MESSAGE_COLOUR_HELP = "00FF00"; @@ -60,12 +62,18 @@ public static void ProcessMessage(string message, NetPeer sender) HelpMessage(sender); break; + case COMMAND_KICK: + KickMessage(message, COMMAND_KICK.Length, player.Username, sender); + break; + +#if DEBUG case COMMAND_LOG_SHORT: Multiplayer.specLog = !Multiplayer.specLog; break; case COMMAND_LOG: Multiplayer.specLog = !Multiplayer.specLog; break; +#endif //allow messages that are not commands to go through default: @@ -163,6 +171,39 @@ private static void WhisperMessage(string message, int commandLength, string sen NetworkLifecycle.Instance.Server.SendWhisper(message, recipient); } + public static void KickMessage(string message, int commandLength, string senderName, NetPeer sender) + { + NetPeer player; + string playerName; + + //If user is not the host, we should ignore - will require changes for dedicated server + if (sender != null && !NetworkLifecycle.Instance.IsHost(sender)) + return; + + //Remove the command "/server" or "/s" + if (commandLength > 0) + { + message = message.Substring(commandLength + 2); + } + + playerName = message.Split(' ')[0]; + + player = NetPeerFromName(playerName); + + if (player == null || NetworkLifecycle.Instance.IsHost(player)) + { + message = $"Unable to kick {playerName}"; + } + else + { + message = $"{playerName} was kicked"; + + NetworkLifecycle.Instance.Server.KickPlayer(player); + } + + NetworkLifecycle.Instance.Server.SendWhisper(message, sender); + } + private static void HelpMessage(NetPeer peer) { string message = $"Available commands:" + diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index a8aaf86..d3b2f7a 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -29,6 +29,7 @@ using UnityEngine; using UnityModManagerNet; using System.Net; +using static DV.UI.ATutorialsMenuProvider; namespace Multiplayer.Networking.Listeners; @@ -63,6 +64,12 @@ public NetworkServer(IDifficulty difficulty, Settings settings, bool isPublic, b Difficulty = difficulty; serverMods = ModInfo.FromModEntries(UnityModManager.modEntries); + + //Start our NAT punch server + if (Multiplayer.Settings.EnableNatPunch) + { + netManager.NatPunchModule.Init(this); + } } public bool Start(int port) @@ -194,6 +201,17 @@ public override void OnConnectionRequest(ConnectionRequest request) #endregion + #region NAT Punch Events + public override void OnNatIntroductionRequest(IPEndPoint localEndPoint, IPEndPoint remoteEndPoint, string token) + { + //do some stuff here + } + public override void OnNatIntroductionSuccess(IPEndPoint targetEndPoint, NatAddressType type, string token) + { + //do other stuff here + } + #endregion + #region Packet Senders private void SendPacketToAll(T packet, DeliveryMethod deliveryMethod) where T : class, new() @@ -214,6 +232,10 @@ public override void OnConnectionRequest(ConnectionRequest request) } } + public void KickPlayer(NetPeer peer) + { + peer.Disconnect(WritePacket(new ClientboundPlayerKickPacket())); + } public void SendGameParams(GameParams gameParams) { SendPacketToAll(ClientboundGameParamsPacket.FromGameParams(gameParams), DeliveryMethod.ReliableOrdered, selfPeer); @@ -291,7 +313,7 @@ public void SendWindowsBroken(ushort netId, Vector3 forceDirection) public void SendWindowsRepaired(ushort netId) { - SendPacketToAll(new ClientboundWindowsBrokenPacket + SendPacketToAll(new ClientboundWindowsRepairedPacket { NetId = netId }, DeliveryMethod.ReliableUnordered, selfPeer); diff --git a/Multiplayer/Networking/Packets/Clientbound/ClientboundPlayerKickPacket.cs b/Multiplayer/Networking/Packets/Clientbound/ClientboundPlayerKickPacket.cs new file mode 100644 index 0000000..c682efa --- /dev/null +++ b/Multiplayer/Networking/Packets/Clientbound/ClientboundPlayerKickPacket.cs @@ -0,0 +1,4 @@ +namespace Multiplayer.Networking.Packets.Clientbound; + +public class ClientboundPlayerKickPacket +{} diff --git a/Multiplayer/Patches/Train/WindowsBreakingControllerPatch.cs b/Multiplayer/Patches/Train/WindowsBreakingControllerPatch.cs index f26aa57..05162f3 100644 --- a/Multiplayer/Patches/Train/WindowsBreakingControllerPatch.cs +++ b/Multiplayer/Patches/Train/WindowsBreakingControllerPatch.cs @@ -43,12 +43,13 @@ public static void RepairWindows_Postfix(WindowsBreakingController __instance) TrainCar car = TrainCar.Resolve(__instance.transform); ushort netId = car.GetNetId(); - if (netId == 0) + if (car == null ||netId == 0) { - Multiplayer.LogWarning($"RepairWindows failed, {car.name}"); + Multiplayer.LogWarning($"RepairWindows_Postfix failed, {car?.name}"); return; } + Multiplayer.LogWarning($"RepairWindows_Postfix , {car.name}"); NetworkLifecycle.Instance.Server.SendWindowsRepaired(netId); } } From 298786fb00966e905004a35153aa7c2afb531fcd Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 28 Jul 2024 19:54:01 +1000 Subject: [PATCH 056/188] Ready for release --- .../Components/MainMenu/ServerBrowserPane.cs | 56 ++++++++++++------- Multiplayer/Multiplayer.csproj | 4 +- Multiplayer/Networking/Data/ModInfo.cs | 1 + .../Managers/Client/NetworkClient.cs | 23 +++++--- .../Managers/Server/NetworkServer.cs | 11 +++- info.json | 2 +- 6 files changed, 65 insertions(+), 32 deletions(-) diff --git a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs index 6cd490d..38f0857 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs @@ -387,11 +387,11 @@ private void IndexChanged(AGridView gridView) //Check if we can connect to this server Multiplayer.Log($"Server: \"{selectedServer.GameVersion}\" \"{selectedServer.MultiplayerVersion}\""); - Multiplayer.Log($"Client: \"{BuildInfo.BUILD_VERSION_MAJOR.ToString()}\" \"{Multiplayer.ModEntry.Version.ToString()}\""); - Multiplayer.Log($"Result: \"{selectedServer.GameVersion == BuildInfo.BUILD_VERSION_MAJOR.ToString()}\" \"{selectedServer.MultiplayerVersion == Multiplayer.ModEntry.Version.ToString()}\""); + Multiplayer.Log($"Client: \"{BuildInfo.BUILD_VERSION_MAJOR.ToString()}\" \"{Multiplayer.ModEntry.Version.ToString(3)}\""); + Multiplayer.Log($"Result: \"{selectedServer.GameVersion == BuildInfo.BUILD_VERSION_MAJOR.ToString()}\" \"{selectedServer.MultiplayerVersion == Multiplayer.ModEntry.Version.ToString(3)}\""); bool canConnect = selectedServer.GameVersion == BuildInfo.BUILD_VERSION_MAJOR.ToString() && - selectedServer.MultiplayerVersion == Multiplayer.ModEntry.Version.ToString(); + selectedServer.MultiplayerVersion == Multiplayer.ModEntry.Version.ToString(3); buttonJoin.ToggleInteractable(canConnect); } @@ -539,7 +539,7 @@ private void ShowPortPopup() { portNumber = ushort.Parse(result.data); ShowPasswordPopup(); - } + } }; } @@ -622,6 +622,7 @@ private void AttemptConnection() Multiplayer.Log($"AttemptConnection Direct: {direct}, Address: {address}"); attempt = 0; + connectionState = ConnectionState.NotConnected; ShowConnectingPopup(); @@ -671,8 +672,9 @@ private void AttemptIPv6() if (connectingPopup != null) connectingPopup.labelTMPro.text = $"Connecting, please wait...\r\nAttempt: {attempt}"; + Multiplayer.Log($"AttemptIPv6() starting attempt"); connectionState = ConnectionState.AttemptingIPv6; - SingletonBehaviour.Instance.StartClient(address, selectedServer.port, password, false, OnDisconnect); + SingletonBehaviour.Instance.StartClient(address, portNumber, password, false, OnDisconnect); } @@ -689,7 +691,7 @@ private void AttemptIPv6Punch() //punching not implemented we'll just try again for now connectionState = ConnectionState.AttemptingIPv6Punch; - SingletonBehaviour.Instance.StartClient(address, selectedServer.port, password, false, OnDisconnect); + SingletonBehaviour.Instance.StartClient(address, portNumber, password, false, OnDisconnect); } @@ -719,15 +721,20 @@ private void AttemptIPv4() if (IPAddress.TryParse(address, out IPAddress IPaddress)) { + Multiplayer.Log($"AttemptIPv4() TryParse passed"); if (IPaddress.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork) { + Multiplayer.Log($"AttemptIPv4() starting attempt"); connectionState = ConnectionState.AttemptingIPv4; - SingletonBehaviour.Instance.StartClient(address, selectedServer.port, password, false, OnDisconnect); + SingletonBehaviour.Instance.StartClient(address, portNumber, password, false, OnDisconnect); return; } } + Multiplayer.Log($"AttemptIPv4() TryParse failed"); AttemptFail(); + string message = "Host Unreachable"; + MainMenuThingsAndStuff.Instance.ShowOkPopup(message, () => { }); } private void AttemptIPv4Punch() @@ -743,7 +750,7 @@ private void AttemptIPv4Punch() //punching not implemented we'll just try again for now connectionState = ConnectionState.AttemptingIPv4Punch; - SingletonBehaviour.Instance.StartClient(address, selectedServer.port, password, false, OnDisconnect); + SingletonBehaviour.Instance.StartClient(address, portNumber, password, false, OnDisconnect); } private void AttemptFail() @@ -753,11 +760,13 @@ private void AttemptFail() if (connectingPopup != null) { connectingPopup.RequestClose(PopupClosedByAction.Abortion, null); + } - string message = "Unable to reach host!"; //add translations + if(this.gridView != null) + IndexChanged(this.gridView); - MainMenuThingsAndStuff.Instance.ShowOkPopup(message, () => { }); - } + if(buttonDirectIP != null) + buttonDirectIP.ToggleInteractable(true); } @@ -776,7 +785,7 @@ private void OnDisconnect(DisconnectReason reason, string message) case DisconnectReason.DisconnectPeerCalled: if (message == null || message.Length == 0) { - message = "Player kicked"; //add translations + message = "Player Kicked"; //add translations } break; case DisconnectReason.ConnectionFailed: @@ -795,11 +804,12 @@ private void OnDisconnect(DisconnectReason reason, string message) return; case ConnectionState.AttemptingIPv4Punch: AttemptFail(); - return; + message = "Host Unreachable"; + break; } break; - case DisconnectReason.ConnectionRejected: + case DisconnectReason.ConnectionRejected: if (message == null || message.Length == 0) { message = "Rejected!"; //add translations @@ -808,15 +818,23 @@ private void OnDisconnect(DisconnectReason reason, string message) case DisconnectReason.RemoteConnectionClose: if (message == null || message.Length == 0) { - message = "Server shutdown"; //add translations + message = "Server Shuttingdown"; //add translations } break; } + //Multiplayer.LogError($"OnDisconnect() Calling AF"); + AttemptFail(); + + //Multiplayer.LogError($"OnDisconnect() Queuing"); NetworkLifecycle.Instance.QueueMainMenuEvent(() => - { - MainMenuThingsAndStuff.Instance.ShowOkPopup(message, ()=>{ }); - }); + { + + //Multiplayer.LogError($"OnDisconnect() Adding PU"); + MainMenuThingsAndStuff.Instance.ShowOkPopup(message, ()=>{ }); + + //Multiplayer.LogError($"OnDisconnect() Done!"); + }); } IEnumerator GetRequest(string uri) @@ -912,7 +930,7 @@ private void FillDummyServers() 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"; + item.MultiplayerVersion = UnityEngine.Random.Range(1, 10) > 3 ? Multiplayer.ModEntry.Version.ToString(3) : "0.1.0"; gridViewModel.Add(item); } diff --git a/Multiplayer/Multiplayer.csproj b/Multiplayer/Multiplayer.csproj index d0aa83d..03f84b5 100644 --- a/Multiplayer/Multiplayer.csproj +++ b/Multiplayer/Multiplayer.csproj @@ -3,8 +3,8 @@ net48 latest Multiplayer - 0.1.6.0 - 0.1.6.0 + 0.1.7.0 + 0.1.7.0 diff --git a/Multiplayer/Networking/Data/ModInfo.cs b/Multiplayer/Networking/Data/ModInfo.cs index 451bbdb..31ccf1d 100644 --- a/Multiplayer/Networking/Data/ModInfo.cs +++ b/Multiplayer/Networking/Data/ModInfo.cs @@ -37,6 +37,7 @@ public static ModInfo Deserialize(NetDataReader reader) public static ModInfo[] FromModEntries(IEnumerable modEntries) { return modEntries + .Where(entry => entry.Enabled) //We only care if it's enabled .OrderBy(entry => entry.Info.Id) .Select(entry => new ModInfo(entry.Info.Id, entry.Info.Version)) .ToArray(); diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 68a9d3c..db45590 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -151,8 +151,18 @@ public override void OnPeerDisconnected(NetPeer peer, DisconnectInfo disconnectI MainMenu.GoBackToMainMenu(); } - //string message = $"{disconnectInfo.Reason}"; + + if( disconnectInfo.Reason == DisconnectReason.ConnectionRejected || + disconnectInfo.Reason == DisconnectReason.RemoteConnectionClose) + { + netPacketProcessor.ReadAllPackets(disconnectInfo.AdditionalData); + return; + } + + onDisconnect(disconnectInfo.Reason, null); + //string message = $"{disconnectInfo.Reason}"; + /* switch (disconnectInfo.Reason) { case DisconnectReason.DisconnectPeerCalled: @@ -161,9 +171,8 @@ public override void OnPeerDisconnected(NetPeer peer, DisconnectInfo disconnectI return; case DisconnectReason.RemoteConnectionClose: netPacketProcessor.ReadAllPackets(disconnectInfo.AdditionalData); - //message = "The server shut down"; - break; - } + return; + }*/ /* NetworkLifecycle.Instance.QueueMainMenuEvent(() => @@ -173,8 +182,6 @@ public override void OnPeerDisconnected(NetPeer peer, DisconnectInfo disconnectI return; popup.labelTMPro.text = text; });*/ - - onDisconnect(disconnectInfo.Reason, null); } public override void OnNetworkLatencyUpdate(NetPeer peer, int latency) @@ -228,9 +235,9 @@ private void OnClientboundServerDenyPacket(ClientboundServerDenyPacket packet) text += Locale.Get(Locale.DISCONN_REASON__MODS_EXTRA_KEY, placeholders: string.Join("\n - ", packet.Extra)); } - //popup.labelTMPro.text = text; + //popup.labelTMPro.text = text; //}); - + Log($"Received player deny packet: {text}"); onDisconnect(DisconnectReason.ConnectionRejected, text); } diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index d3b2f7a..b434c7a 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -30,6 +30,7 @@ using UnityModManagerNet; using System.Net; using static DV.UI.ATutorialsMenuProvider; +using HarmonyLib; namespace Multiplayer.Networking.Listeners; @@ -56,6 +57,9 @@ public class NetworkServer : NetworkManager public readonly IDifficulty Difficulty; private bool IsLoaded; + //we don't care if the client doesn't have these mods + private string[] modWhiteList = { "RuntimeUnityEditor" }; + public NetworkServer(IDifficulty difficulty, Settings settings, bool isPublic, bool isSinglePlayer, LobbyServerData serverData) : base(settings) { this.isPublic = isPublic; @@ -63,7 +67,9 @@ public NetworkServer(IDifficulty difficulty, Settings settings, bool isPublic, b this.serverData = serverData; Difficulty = difficulty; - serverMods = ModInfo.FromModEntries(UnityModManager.modEntries); + + serverMods = ModInfo.FromModEntries(UnityModManager.modEntries) + .Where(mod => !modWhiteList.Contains(mod.Id)).ToArray(); //Start our NAT punch server if (Multiplayer.Settings.EnableNatPunch) @@ -456,11 +462,12 @@ private void OnServerboundClientLoginPacket(ServerboundClientLoginPacket packet, return; } - ModInfo[] clientMods = packet.Mods; + ModInfo[] clientMods = packet.Mods.Where(mod => !modWhiteList.Contains(mod.Id)).ToArray(); if (!serverMods.SequenceEqual(clientMods)) { ModInfo[] missing = serverMods.Except(clientMods).ToArray(); ModInfo[] extra = clientMods.Except(serverMods).ToArray(); + LogWarning($"Denied login due to mod mismatch! {missing.Length} missing, {extra.Length} extra"); ClientboundServerDenyPacket denyPacket = new() { diff --git a/info.json b/info.json index 06bd263..e1294a0 100644 --- a/info.json +++ b/info.json @@ -1,6 +1,6 @@ { "Id": "Multiplayer", - "Version": "0.1.6.0", + "Version": "0.1.7.0", "DisplayName": "Multiplayer", "Author": "Insprill, Macka, Morm", "EntryMethod": "Multiplayer.Multiplayer.Load", From 2718a78ecb945b0aa8b190fe10b6f0937053fec0 Mon Sep 17 00:00:00 2001 From: AMacro Date: Fri, 2 Aug 2024 20:51:16 +1000 Subject: [PATCH 057/188] Fixed brake pressure sync issue --- .../Components/MainMenu/ServerBrowserPane.cs | 2 +- .../Networking/Train/NetworkedTrainCar.cs | 45 +++++++++++++++++ .../SaveGame/NetworkedSaveGameManager.cs | 1 + Multiplayer/Multiplayer.csproj | 4 +- .../Managers/Client/NetworkClient.cs | 49 ++++++++++++++----- .../Managers/Server/NetworkServer.cs | 14 ++++++ .../ClientboundBrakePressureUpdatePacket.cs | 10 ++++ info.json | 2 +- 8 files changed, 112 insertions(+), 15 deletions(-) create mode 100644 Multiplayer/Networking/Packets/Clientbound/Train/ClientboundBrakePressureUpdatePacket.cs diff --git a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs index 38f0857..d4f881c 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs @@ -697,7 +697,7 @@ private void AttemptIPv6Punch() private void AttemptIPv4() { - Multiplayer.Log($"AttemptIPv4() {address}"); + Multiplayer.Log($"AttemptIPv4() {address}, {connectionState}"); if (connectionState == ConnectionState.Aborted) return; diff --git a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs index 9ec44bf..eec4757 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs @@ -77,6 +77,7 @@ public static bool TryGetFromTrainCar(TrainCar trainCar, out NetworkedTrainCar n private Dictionary lastSentPortValues; private HashSet dirtyFuses; private bool handbrakeDirty; + private bool mainResPressureDirty; public bool BogieTracksDirty; public int Bogie1TrackDirection; public int Bogie2TrackDirection; @@ -150,6 +151,8 @@ private void Start() brakeSystem.HandbrakePositionChanged += Common_OnHandbrakePositionChanged; brakeSystem.BrakeCylinderReleased += Common_OnBrakeCylinderReleased; + + NetworkLifecycle.Instance.OnTick += Common_OnTick; if (NetworkLifecycle.Instance.IsHost()) { @@ -157,6 +160,9 @@ private void Start() bogie1.TrackChanged += Server_BogieTrackChanged; bogie2.TrackChanged += Server_BogieTrackChanged; TrainCar.CarDamage.CarEffectiveHealthStateUpdate += Server_CarHealthUpdate; + + brakeSystem.MainResPressureChanged += Server_MainResUpdate; + StartCoroutine(Server_WaitForLogicCar()); } } @@ -186,6 +192,9 @@ private void OnDisable() bogie1.TrackChanged -= Server_BogieTrackChanged; bogie2.TrackChanged -= Server_BogieTrackChanged; TrainCar.CarDamage.CarEffectiveHealthStateUpdate -= Server_CarHealthUpdate; + + brakeSystem.MainResPressureChanged -= Server_MainResUpdate; + if (TrainCar.logicCar != null) { TrainCar.logicCar.CargoLoaded -= Server_OnCargoLoaded; @@ -227,6 +236,7 @@ private IEnumerator Server_WaitForLogicCar() public void Server_DirtyAllState() { handbrakeDirty = true; + mainResPressureDirty = true; cargoDirty = true; cargoIsLoading = true; healthDirty = true; @@ -238,7 +248,12 @@ public void Server_DirtyAllState() { dirtyPorts.Add(portId); if (simulationFlow.TryGetPort(portId, out Port port)) + { lastSentPortValues[portId] = port.value; + + //Multiplayer.Log($"Server_DirtyAllState({TrainCar.ID}): {portId}({port.type}): {port.value}({port.valueType})"); + + } } foreach (string fuseId in simulationFlow.fullFuseIdToFuse.Keys) @@ -295,15 +310,29 @@ private void Server_CarHealthUpdate(float health) healthDirty = true; } + private void Server_MainResUpdate(float normalizedPressure, float pressure) + { + mainResPressureDirty = true; + } + private void Server_OnTick(uint tick) { if (UnloadWatcher.isUnloading) return; + Server_SendBrakePressures(); Server_SendCouplers(); Server_SendCargoState(); Server_SendHealthState(); } + private void Server_SendBrakePressures() + { + if (!mainResPressureDirty) + return; + mainResPressureDirty = false; + NetworkLifecycle.Instance.Server.SendBrakePressures(NetId, brakeSystem.mainReservoirPressure, brakeSystem.independentPipePressure, brakeSystem.brakePipePressure, brakeSystem.brakeCylinderPressure); + } + private void Server_SendCouplers() { if (!sendCouplers) @@ -527,5 +556,21 @@ public void Client_ReceiveTrainPhysicsUpdate(in TrainsetMovementPart movementPar } } + public void Client_ReceiveBrakePressureUpdate(float mainReservoirPressure, float independentPipePressure, float brakePipePressure, float brakeCylinderPressure) + { + if (brakeSystem == null) + return; + + if (!hasSimFlow) + return; + + brakeSystem.ForceIndependentPipePressure(independentPipePressure); + brakeSystem.ForceTargetIndBrakeCylinderPressure(brakeCylinderPressure); + brakeSystem.SetMainReservoirPressure(mainReservoirPressure); + + brakeSystem.brakePipePressure = brakePipePressure; + brakeSystem.brakeCylinderPressure = brakeCylinderPressure; + } + #endregion } diff --git a/Multiplayer/Components/SaveGame/NetworkedSaveGameManager.cs b/Multiplayer/Components/SaveGame/NetworkedSaveGameManager.cs index af0b5df..d143273 100644 --- a/Multiplayer/Components/SaveGame/NetworkedSaveGameManager.cs +++ b/Multiplayer/Components/SaveGame/NetworkedSaveGameManager.cs @@ -73,6 +73,7 @@ public void Server_UpdateInternalData(SaveGameData data) JObject playerData = new(); playerData.SetVector3(SaveGameKeys.Player_position, player.AbsoluteWorldPosition); playerData.SetFloat(SaveGameKeys.Player_rotation, player.WorldRotationY); + //store inventory see StorageSerializer.SaveStorage() players.SetJObject(player.Guid.ToString(), playerData); } diff --git a/Multiplayer/Multiplayer.csproj b/Multiplayer/Multiplayer.csproj index 03f84b5..8aedea6 100644 --- a/Multiplayer/Multiplayer.csproj +++ b/Multiplayer/Multiplayer.csproj @@ -3,8 +3,8 @@ net48 latest Multiplayer - 0.1.7.0 - 0.1.7.0 + 0.1.7.2 + 0.1.7.2 diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index db45590..06cfab4 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -1,6 +1,7 @@ using System; using System.Net; using System.Text; +using System.Collections.Generic; using DV; using DV.Damage; using DV.InventorySystem; @@ -11,6 +12,7 @@ using DV.UI; using DV.WeatherSystem; using LiteNetLib; +using Multiplayer.Components; using Multiplayer.Components.MainMenu; using Multiplayer.Components.Networking; using Multiplayer.Components.Networking.Jobs; @@ -34,6 +36,7 @@ using UnityEngine; using UnityModManagerNet; using Object = UnityEngine.Object; +using System.Linq; namespace Multiplayer.Networking.Listeners; @@ -109,6 +112,7 @@ protected override void Subscribe() netPacketProcessor.SubscribeReusable(OnCommonHandbrakePositionPacket); netPacketProcessor.SubscribeReusable(OnCommonSimFlowPacket); netPacketProcessor.SubscribeReusable(OnCommonTrainFusesPacket); + netPacketProcessor.SubscribeReusable(OnClientboundBrakePressureUpdatePacket); netPacketProcessor.SubscribeReusable(OnClientboundCargoStatePacket); netPacketProcessor.SubscribeReusable(OnClientboundCarHealthUpdatePacket); netPacketProcessor.SubscribeReusable(OnClientboundRerailTrainPacket); @@ -118,10 +122,10 @@ protected override void Subscribe() netPacketProcessor.SubscribeReusable(OnClientboundLicenseAcquiredPacket); netPacketProcessor.SubscribeReusable(OnClientboundGarageUnlockPacket); netPacketProcessor.SubscribeReusable(OnClientboundDebtStatusPacket); - //netPacketProcessor.SubscribeReusable(OnClientboundJobsPacket); - //netPacketProcessor.SubscribeReusable(OnClientboundJobCreatePacket); - //netPacketProcessor.SubscribeReusable(OnClientboundJobTakeResponsePacket); - netPacketProcessor.SubscribeReusable(OnCommonChatPacket); + netPacketProcessor.SubscribeReusable(OnClientboundJobsPacket); + netPacketProcessor.SubscribeReusable(OnClientboundJobCreatePacket); + netPacketProcessor.SubscribeReusable(OnClientboundJobTakeResponsePacket); + netPacketProcessor.SubscribeReusable(OnCommonChatPacket); } #region Net Events @@ -564,6 +568,17 @@ private void OnCommonTrainFusesPacket(CommonTrainFusesPacket packet) networkedTrainCar.Common_UpdateFuses(packet); } + private void OnClientboundBrakePressureUpdatePacket(ClientboundBrakePressureUpdatePacket packet) + { + if (!NetworkedTrainCar.Get(packet.NetId, out NetworkedTrainCar networkedTrainCar)) + return; + + + networkedTrainCar.Client_ReceiveBrakePressureUpdate(packet.MainReservoirPressure, packet.IndependentPipePressure, packet.BrakePipePressure, packet.BrakeCylinderPressure); + + //Multiplayer.LogDebug(() => $"Received Brake Pressures netId {packet.NetId}: {packet.MainReservoirPressure}, {packet.IndependentPipePressure}, {packet.BrakePipePressure}, {packet.BrakeCylinderPressure}"); + } + private void OnClientboundCargoStatePacket(ClientboundCargoStatePacket packet) { if (!NetworkedTrainCar.Get(packet.NetId, out NetworkedTrainCar networkedTrainCar)) @@ -671,9 +686,11 @@ private void OnCommonChatPacket(CommonChatPacket packet) chatGUI.ReceiveMessage(packet.message); } - /* Temp for stable release + private void OnClientboundJobCreatePacket(ClientboundJobCreatePacket packet) { + Multiplayer.Log($"Received job packet. Job ID:{packet.job.ID}"); + if (NetworkLifecycle.Instance.IsHost()) return; @@ -682,7 +699,7 @@ private void OnClientboundJobCreatePacket(ClientboundJobCreatePacket packet) tasks.Add(TaskBeforeDataData.ToTask(taskBeforeDataData)); StationsChainDataData chainData = packet.job.ChainData; - //packet.job.JobType + Job newJob = new Job( tasks, (JobType)packet.job.JobType, @@ -700,7 +717,7 @@ private void OnClientboundJobCreatePacket(ClientboundJobCreatePacket packet) StationController station; if(!StationComponentLookup.Instance.StationControllerFromId(packet.stationId, out station)) { - Multiplayer.LogWarning($"OnClientboundJobCreatePacket Could not get staion for stationId: {packet.stationId}"); + Multiplayer.LogWarning($"OnClientboundJobCreatePacket Could not get station for stationId: {packet.stationId}"); return; } @@ -716,6 +733,8 @@ private void OnClientboundJobCreatePacket(ClientboundJobCreatePacket packet) } private void OnClientboundJobsPacket(ClientboundJobsPacket packet) { + Multiplayer.Log($"Received job packet. Job count:{packet.Jobs.Count()}"); + if (NetworkLifecycle.Instance.IsHost()) return; @@ -725,8 +744,6 @@ private void OnClientboundJobsPacket(ClientboundJobsPacket packet) return; } - Multiplayer.Log($"Received job packet. Job count:{packet.Jobs.Count()}"); - for (int i=0;i < packet.Jobs.Count(); i++) { JobData job = packet.Jobs[i]; @@ -763,13 +780,15 @@ private void OnClientboundJobsPacket(ClientboundJobsPacket packet) private void OnClientboundJobTakeResponsePacket(ClientboundJobTakeResponsePacket packet) { + Multiplayer.Log($"OnClientboundJobTakeResponsePacket jobId: {packet.netId}, Status: {packet.granted}"); + NetworkedJob networkedJob; if(!NetworkedJob.Get(packet.netId, out networkedJob)) return; NetworkedPlayer player; - if (PlayerManager.TryGetPlayer(packet.playerId, out player)) + if (ClientPlayerManager.TryGetPlayer(packet.playerId, out player)) { networkedJob.takenBy = player.Guid; } @@ -780,7 +799,7 @@ private void OnClientboundJobTakeResponsePacket(ClientboundJobTakeResponsePacket networkedJob.jobValidator = null; networkedJob.jobOverview = null; } - */ + #endregion #region Senders @@ -999,6 +1018,14 @@ public void SendPorts(ushort netId, string[] portIds, float[] portValues) PortIds = portIds, PortValues = portValues }, DeliveryMethod.ReliableOrdered); + + string log=$"Sending ports netId: {netId}"; + for (int i = 0; i < portIds.Length; i++) { + log += $"\r\n\t{portIds[i]}: {portValues[i]}"; + } + + Multiplayer.LogDebug(() => log); + } public void SendFuses(ushort netId, string[] fuseIds, bool[] fuseValues) diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index b434c7a..502ef61 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -273,6 +273,20 @@ public void SendTrainsetPhysicsUpdate(ClientboundTrainsetPhysicsPacket packet, b SendPacketToAll(packet, reliable ? DeliveryMethod.ReliableOrdered : DeliveryMethod.Unreliable, selfPeer); } + public void SendBrakePressures(ushort netId, float mainReservoirPressure, float independentPipePressure, float brakePipePressure, float brakeCylinderPressure) + { + SendPacketToAll(new ClientboundBrakePressureUpdatePacket + { + NetId = netId, + MainReservoirPressure = mainReservoirPressure, + IndependentPipePressure = independentPipePressure, + BrakePipePressure = brakePipePressure, + BrakeCylinderPressure = brakeCylinderPressure + }, DeliveryMethod.ReliableOrdered, selfPeer); + + //Multiplayer.LogDebug(()=> $"Sending Brake Pressures netId {netId}: {mainReservoirPressure}, {independentPipePressure}, {brakePipePressure}, {brakeCylinderPressure}"); + } + public void SendCargoState(TrainCar trainCar, ushort netId, bool isLoading, byte cargoModelIndex) { Car logicCar = trainCar.logicCar; diff --git a/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundBrakePressureUpdatePacket.cs b/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundBrakePressureUpdatePacket.cs new file mode 100644 index 0000000..6fdee87 --- /dev/null +++ b/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundBrakePressureUpdatePacket.cs @@ -0,0 +1,10 @@ +namespace Multiplayer.Networking.Packets.Clientbound.Train; + +public class ClientboundBrakePressureUpdatePacket +{ + public ushort NetId { get; set; } + public float MainReservoirPressure { get; set; } + public float IndependentPipePressure { get; set; } + public float BrakePipePressure { get; set; } + public float BrakeCylinderPressure { get; set; } +} diff --git a/info.json b/info.json index e1294a0..c0e628f 100644 --- a/info.json +++ b/info.json @@ -1,6 +1,6 @@ { "Id": "Multiplayer", - "Version": "0.1.7.0", + "Version": "0.1.7.2", "DisplayName": "Multiplayer", "Author": "Insprill, Macka, Morm", "EntryMethod": "Multiplayer.Multiplayer.Load", From 0deb0fb98520fefe4a438f59c1c81a63413e06ec Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 3 Aug 2024 07:56:11 +1000 Subject: [PATCH 058/188] Added example Directory.Build.targets --- Directory.Build.targets.EXAMPLE | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 Directory.Build.targets.EXAMPLE diff --git a/Directory.Build.targets.EXAMPLE b/Directory.Build.targets.EXAMPLE new file mode 100644 index 0000000..29314c4 --- /dev/null +++ b/Directory.Build.targets.EXAMPLE @@ -0,0 +1,25 @@ + + + + C:\Program Files (x86)\Steam\steamapps\common\Derail Valley + C:\Program Files\Unity\Hub\Editor\2019.4.40f1\Editor + + $(DvInstallDir)\DerailValley_Data\Managed\; + $(DvInstallDir)\DerailValley_Data\Managed\UnityModManager\; + $(UnityInstallDir)\Data\Managed\ + + $(AssemblySearchPaths);$(ReferencePath); + C:\Program Files (x86)\Microsoft SDKs\ClickOnce\SignTool\ + 7cf2b8a98a09ffd407ada2e94f200af24a0e68bc + + \ No newline at end of file From fdf5b7dc35ffb43cdc253b467f84cc0caf970a8f Mon Sep 17 00:00:00 2001 From: Macka Date: Sat, 3 Aug 2024 08:28:15 +1000 Subject: [PATCH 059/188] Update README.md --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 544928c..cb32e0f 100644 --- a/README.md +++ b/README.md @@ -14,9 +14,11 @@ A Derail Valley mod that adds multiplayer.

- Report Bug + Report Bug · - Request Feature + Request Feature + · + Discord

@@ -46,6 +48,7 @@ Multiplayer is a Derail Valley mod that adds multiplayer to the game, allowing y It works by having one player host a game, and then other players can join that game. +This fork is a continuation of [Insprill's](https://github.com/Insprill/dv-multiplayer) amazing efforts. From 713e26ebfe4197fd1a4f0f641720b177c1ee30ad Mon Sep 17 00:00:00 2001 From: Macka Date: Sat, 3 Aug 2024 08:31:00 +1000 Subject: [PATCH 060/188] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cb32e0f..3524b75 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ · Request Feature · - Discord + Discord

From 08f5e02ad7585e4b683e99951751b114f7191d33 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 3 Aug 2024 14:25:33 +1000 Subject: [PATCH 061/188] Fixed bug where password was not cleared when attempting to join a server without password --- Multiplayer/Components/MainMenu/ServerBrowserPane.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs index d4f881c..60db854 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs @@ -354,6 +354,9 @@ private void JoinAction() return; } + //password not required + password = null; + AttemptConnection(); } From aa821aa256fd7dc186f844ec275cc106118d37bf Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 10 Aug 2024 14:58:33 +1000 Subject: [PATCH 062/188] Added versioning system to settings to allow upgrades on new version changes --- .../Components/MainMenu/HostGamePane.cs | 2 +- .../Components/MainMenu/ServerBrowserPane.cs | 34 ++++++++------ .../Networking/Player/NetworkedWorldMap.cs | 2 +- .../Components/Networking/TickedQueue.cs | 2 +- .../Train/NetworkTrainsetWatcher.cs | 3 ++ .../Networking/World/NetworkedTurntable.cs | 2 +- .../SaveGame/StartGameData_ServerSave.cs | 4 +- Multiplayer/Multiplayer.cs | 19 +++++++- Multiplayer/Multiplayer.csproj | 10 +---- Multiplayer/Networking/Data/JobData.cs | 10 ++--- .../Data/{TaskDataData.cs => TaskData.cs} | 40 ++++++++--------- .../Managers/Client/NetworkClient.cs | 8 ++-- .../Networking/Managers/NetworkManager.cs | 2 +- .../CustomFirstPersonControllerPatch.cs | 3 ++ Multiplayer/Settings.cs | 45 ++++++++++++++++++- info.json | 2 +- 16 files changed, 127 insertions(+), 61 deletions(-) rename Multiplayer/Networking/Data/{TaskDataData.cs => TaskData.cs} (90%) diff --git a/Multiplayer/Components/MainMenu/HostGamePane.cs b/Multiplayer/Components/MainMenu/HostGamePane.cs index eee6be8..3378207 100644 --- a/Multiplayer/Components/MainMenu/HostGamePane.cs +++ b/Multiplayer/Components/MainMenu/HostGamePane.cs @@ -352,7 +352,7 @@ private void StartClick() serverData.RequiredMods = ""; //FIX THIS - get the mods required serverData.GameVersion = BuildInfo.BUILD_VERSION_MAJOR.ToString(); - serverData.MultiplayerVersion = Multiplayer.ModEntry.Version.ToString(); + serverData.MultiplayerVersion = Multiplayer.Ver; serverData.ServerDetails = details.text.Trim(); diff --git a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs index 60db854..e2b002d 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs @@ -390,11 +390,11 @@ private void IndexChanged(AGridView gridView) //Check if we can connect to this server Multiplayer.Log($"Server: \"{selectedServer.GameVersion}\" \"{selectedServer.MultiplayerVersion}\""); - Multiplayer.Log($"Client: \"{BuildInfo.BUILD_VERSION_MAJOR.ToString()}\" \"{Multiplayer.ModEntry.Version.ToString(3)}\""); - Multiplayer.Log($"Result: \"{selectedServer.GameVersion == BuildInfo.BUILD_VERSION_MAJOR.ToString()}\" \"{selectedServer.MultiplayerVersion == Multiplayer.ModEntry.Version.ToString(3)}\""); + Multiplayer.Log($"Client: \"{BuildInfo.BUILD_VERSION_MAJOR.ToString()}\" \"{Multiplayer.Ver}\""); + Multiplayer.Log($"Result: \"{selectedServer.GameVersion == BuildInfo.BUILD_VERSION_MAJOR.ToString()}\" \"{selectedServer.MultiplayerVersion == Multiplayer.Ver}\""); bool canConnect = selectedServer.GameVersion == BuildInfo.BUILD_VERSION_MAJOR.ToString() && - selectedServer.MultiplayerVersion == Multiplayer.ModEntry.Version.ToString(3); + selectedServer.MultiplayerVersion == Multiplayer.Ver; buttonJoin.ToggleInteractable(canConnect); } @@ -425,7 +425,7 @@ private void UpdateDetailsPane() details += "" + Locale.SERVER_BROWSER__MODS_REQUIRED + ": " + (selectedServer.RequiredMods != null? Locale.SERVER_BROWSER__YES : Locale.SERVER_BROWSER__NO) + "
"; details += "
"; 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 += "" + Locale.SERVER_BROWSER__MOD_VERSION + ": " + (selectedServer.MultiplayerVersion != Multiplayer.Ver ? "" : "") + selectedServer.MultiplayerVersion + "
"; details += "
"; details += selectedServer.ServerDetails; @@ -628,7 +628,6 @@ private void AttemptConnection() connectionState = ConnectionState.NotConnected; ShowConnectingPopup(); - if (!direct) { if (selectedServer.ipv6 != null && selectedServer.ipv6 != string.Empty) @@ -782,13 +781,13 @@ private void OnDisconnect(DisconnectReason reason, string message) case DisconnectReason.UnknownHost: if (message == null || message.Length == 0) { - message = "Unknown Host"; //add translations + message = "Unknown Host"; //TODO: add translations } break; case DisconnectReason.DisconnectPeerCalled: if (message == null || message.Length == 0) { - message = "Player Kicked"; //add translations + message = "Player Kicked"; //TODO: add translations } break; case DisconnectReason.ConnectionFailed: @@ -797,17 +796,24 @@ private void OnDisconnect(DisconnectReason reason, string message) switch (connectionState) { case ConnectionState.AttemptingIPv6: - AttemptIPv6Punch(); + if (Multiplayer.Settings.EnableNatPunch) + AttemptIPv6Punch(); + else + AttemptIPv4(); return; case ConnectionState.AttemptingIPv6Punch: AttemptIPv4(); return; case ConnectionState.AttemptingIPv4: - AttemptIPv4Punch(); + if (Multiplayer.Settings.EnableNatPunch) + AttemptIPv4Punch(); + else + AttemptFail(); + message = "Host Unreachable"; //TODO: add translations return; case ConnectionState.AttemptingIPv4Punch: AttemptFail(); - message = "Host Unreachable"; + message = "Host Unreachable"; //TODO: add translations break; } break; @@ -815,13 +821,13 @@ private void OnDisconnect(DisconnectReason reason, string message) case DisconnectReason.ConnectionRejected: if (message == null || message.Length == 0) { - message = "Rejected!"; //add translations + message = "Rejected!"; //TODO: add translations } break; case DisconnectReason.RemoteConnectionClose: if (message == null || message.Length == 0) { - message = "Server Shuttingdown"; //add translations + message = "Server Shutting Down"; //TODO: add translations } break; } @@ -833,7 +839,7 @@ private void OnDisconnect(DisconnectReason reason, string message) NetworkLifecycle.Instance.QueueMainMenuEvent(() => { - //Multiplayer.LogError($"OnDisconnect() Adding PU"); + Multiplayer.LogError($"OnDisconnect() Adding PU"); MainMenuThingsAndStuff.Instance.ShowOkPopup(message, ()=>{ }); //Multiplayer.LogError($"OnDisconnect() Done!"); @@ -933,7 +939,7 @@ private void FillDummyServers() 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(3) : "0.1.0"; + item.MultiplayerVersion = UnityEngine.Random.Range(1, 10) > 3 ? Multiplayer.Ver : "0.1.0"; gridViewModel.Add(item); } diff --git a/Multiplayer/Components/Networking/Player/NetworkedWorldMap.cs b/Multiplayer/Components/Networking/Player/NetworkedWorldMap.cs index 522ecfa..3ff765e 100644 --- a/Multiplayer/Components/Networking/Player/NetworkedWorldMap.cs +++ b/Multiplayer/Components/Networking/Player/NetworkedWorldMap.cs @@ -74,7 +74,7 @@ private void OnPlayerDisconnected(byte id, NetworkedPlayer player) private void OnTick(uint obj) { - if (!worldMap.initialized) + if (!worldMap.initialized || UnloadWatcher.isUnloading) return; UpdatePlayers(); } diff --git a/Multiplayer/Components/Networking/TickedQueue.cs b/Multiplayer/Components/Networking/TickedQueue.cs index 31447e1..c2127a1 100644 --- a/Multiplayer/Components/Networking/TickedQueue.cs +++ b/Multiplayer/Components/Networking/TickedQueue.cs @@ -32,7 +32,7 @@ public void ReceiveSnapshot(T snapshot, uint tick) private void OnTick(uint tick) { - if (snapshots.Count == 0) + if (snapshots.Count == 0 || UnloadWatcher.isUnloading) return; while (snapshots.Count > 0) { diff --git a/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs b/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs index a1b9c7e..ad025e3 100644 --- a/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs +++ b/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs @@ -33,6 +33,9 @@ protected override void OnDestroy() private void Server_OnTick(uint tick) { + if (UnloadWatcher.isUnloading) + return; + cachedSendPacket.Tick = tick; foreach (Trainset set in Trainset.allSets) Server_TickSet(set); diff --git a/Multiplayer/Components/Networking/World/NetworkedTurntable.cs b/Multiplayer/Components/Networking/World/NetworkedTurntable.cs index ad2b0dd..ac5832d 100644 --- a/Multiplayer/Components/Networking/World/NetworkedTurntable.cs +++ b/Multiplayer/Components/Networking/World/NetworkedTurntable.cs @@ -31,7 +31,7 @@ protected override void OnDestroy() private void OnTick(uint tick) { - if (Mathf.Approximately(lastYRotation, TurntableRailTrack.targetYRotation)) + if (Mathf.Approximately(lastYRotation, TurntableRailTrack.targetYRotation) || UnloadWatcher.isUnloading) return; lastYRotation = TurntableRailTrack.targetYRotation; diff --git a/Multiplayer/Components/SaveGame/StartGameData_ServerSave.cs b/Multiplayer/Components/SaveGame/StartGameData_ServerSave.cs index f9bf95c..874fe8d 100644 --- a/Multiplayer/Components/SaveGame/StartGameData_ServerSave.cs +++ b/Multiplayer/Components/SaveGame/StartGameData_ServerSave.cs @@ -59,8 +59,8 @@ public override IEnumerator DoLoad(Transform playerContainer) if (saveGameData.GetString(SaveGameKeys.Game_mode) == "FreeRoam") LicenseManager.Instance.GrabAllGameModeSpecificUnlockables(SaveGameKeys.Game_mode); - else - StartingItemsController.Instance.AddStartingItems(saveGameData, true); + //else + StartingItemsController.Instance.AddStartingItems(saveGameData, true); // if (packet.Debt_existing_locos != null) // LocoDebtController.Instance.LoadExistingLocosDebtsSaveData(packet.Debt_existing_locos.Select(JObject.Parse).ToArray()); diff --git a/Multiplayer/Multiplayer.cs b/Multiplayer/Multiplayer.cs index d82edf5..7b6b64a 100644 --- a/Multiplayer/Multiplayer.cs +++ b/Multiplayer/Multiplayer.cs @@ -1,5 +1,7 @@ using System; using System.IO; +using System.Linq; +using System.Reflection; using HarmonyLib; using JetBrains.Annotations; using Multiplayer.Components.Networking; @@ -21,6 +23,19 @@ public static class Multiplayer private static AssetBundle assetBundle; public static AssetIndex AssetIndex { get; private set; } + public static string Ver { + get { + AssemblyInformationalVersionAttribute info = (AssemblyInformationalVersionAttribute)typeof(Multiplayer).Assembly. + GetCustomAttributes(typeof(AssemblyInformationalVersionAttribute), false) + .FirstOrDefault(); + + if (info == null || Settings.ForceJson) + return ModEntry.Info.Version; + + return info.InformationalVersion.Split('+')[0]; + } + } + public static bool specLog = false; @@ -28,7 +43,7 @@ public static class Multiplayer private static bool Load(UnityModManager.ModEntry modEntry) { ModEntry = modEntry; - Settings = Settings.Load(modEntry); + Settings = Settings.Load(modEntry);//Settings.Load(modEntry); ModEntry.OnGUI = Settings.Draw; ModEntry.OnSaveGUI = Settings.Save; @@ -40,6 +55,8 @@ private static bool Load(UnityModManager.ModEntry modEntry) Locale.Load(ModEntry.Path); + Log($"Multiplayer JSON Version: {ModEntry.Info.Version}, Internal Version: {Ver} "); + Log("Patching..."); harmony = new Harmony(ModEntry.Info.Id); harmony.PatchAll(); diff --git a/Multiplayer/Multiplayer.csproj b/Multiplayer/Multiplayer.csproj index 8aedea6..d909432 100644 --- a/Multiplayer/Multiplayer.csproj +++ b/Multiplayer/Multiplayer.csproj @@ -3,8 +3,7 @@ net48 latest Multiplayer - 0.1.7.2 - 0.1.7.2 + 0.1.8.0 @@ -90,7 +89,7 @@ - + @@ -101,11 +100,6 @@ - - diff --git a/Multiplayer/Networking/Data/JobData.cs b/Multiplayer/Networking/Data/JobData.cs index 799199d..bf14034 100644 --- a/Multiplayer/Networking/Data/JobData.cs +++ b/Multiplayer/Networking/Data/JobData.cs @@ -9,7 +9,7 @@ public class JobData { public byte JobType { get; set; } public string ID { get; set; } - public TaskBeforeDataData[] Tasks { get; set; } + public TaskData[] Tasks { get; set; } public StationsChainDataData ChainData { get; set; } public int RequiredLicenses { get; set; } public float StartTime { get; set; } @@ -24,7 +24,7 @@ public static JobData FromJob(Job job) { JobType = (byte)job.jobType, ID = job.ID, - Tasks = job.tasks.Select(x => TaskBeforeDataData.FromTask(x)).ToArray(), + Tasks = job.tasks.Select(x => TaskData.FromTask(x)).ToArray(), ChainData = StationsChainDataData.FromStationData(job.chainData), RequiredLicenses = (int)job.requiredLicenses, StartTime = job.startTime, @@ -41,7 +41,7 @@ public static void Serialize(NetDataWriter writer, JobData data) writer.Put(data.ID); writer.Put((byte)data.Tasks.Length); foreach (var taskBeforeDataData in data.Tasks) - TaskBeforeDataData.SerializeTask(taskBeforeDataData, writer); + TaskData.SerializeTask(taskBeforeDataData, writer); StationsChainDataData.Serialize(writer, data.ChainData); writer.Put(data.RequiredLicenses); writer.Put(data.StartTime); @@ -61,9 +61,9 @@ public static JobData Deserialize(NetDataReader reader) Multiplayer.Log("JobData.Deserialize() id: " + id); var tasksLength = reader.GetByte(); Multiplayer.Log("JobData.Deserialize() tasksLength: " + tasksLength); - var tasks = new TaskBeforeDataData[tasksLength]; + var tasks = new TaskData[tasksLength]; for (int i = 0; i < tasksLength; i++) - tasks[i] = TaskBeforeDataData.DeserializeTask(reader); + tasks[i] = TaskData.DeserializeTask(reader); //Multiplayer.Log("JobData.Deserialize() tasks: " + JsonConvert.SerializeObject(tasks, Formatting.None)); var chainData = StationsChainDataData.Deserialize(reader); //Multiplayer.Log("JobData.Deserialize() chainData: " + JsonConvert.SerializeObject(chainData, Formatting.Indented)); diff --git a/Multiplayer/Networking/Data/TaskDataData.cs b/Multiplayer/Networking/Data/TaskData.cs similarity index 90% rename from Multiplayer/Networking/Data/TaskDataData.cs rename to Multiplayer/Networking/Data/TaskData.cs index eb1be23..30e6e28 100644 --- a/Multiplayer/Networking/Data/TaskDataData.cs +++ b/Multiplayer/Networking/Data/TaskData.cs @@ -9,7 +9,7 @@ namespace Multiplayer.Networking.Data; -public abstract class TaskBeforeDataData +public abstract class TaskData { public byte State { get; set; } public float TaskStartTime { get; set; } @@ -19,9 +19,9 @@ public abstract class TaskBeforeDataData public byte TaskType { get; set; } - public static TaskBeforeDataData FromTask(Task task) + public static TaskData FromTask(Task task) { - TaskBeforeDataData taskData = task switch + TaskData taskData = task switch { WarehouseTask warehouseTask => WarehouseTaskData.FromWarehouseTask(warehouseTask), TransportTask transportTask => TransportTaskData.FromTransportTask(transportTask), @@ -59,7 +59,7 @@ public static Task ToTask(object data) var task = (SequentialTasksData)data; List tasks = new List(); - foreach (TaskBeforeDataData taskBeforeDataData in task.Tasks) + foreach (TaskData taskBeforeDataData in task.Tasks) tasks.Add(ToTask(taskBeforeDataData)); @@ -71,7 +71,7 @@ public static Task ToTask(object data) var task = (ParallelTasksData)data; List tasks = new List(); - foreach (TaskBeforeDataData taskBeforeDataData in task.Tasks) + foreach (TaskData taskBeforeDataData in task.Tasks) tasks.Add(ToTask(taskBeforeDataData)); @@ -119,7 +119,7 @@ public static void SerializeTask(object data, NetDataWriter writer) throw new ArgumentException("Unknown task type: " + data.GetType()); } - public static TaskBeforeDataData DeserializeTask(NetDataReader reader) + public static TaskData DeserializeTask(NetDataReader reader) { TaskType taskType = (TaskType)reader.GetByte(); Multiplayer.Log("Task type: " + taskType + ""); @@ -134,7 +134,7 @@ public static TaskBeforeDataData DeserializeTask(NetDataReader reader) }; } - public static void Serialize(NetDataWriter writer, TaskBeforeDataData data) + public static void Serialize(NetDataWriter writer, TaskData data) { writer.Put(data.TaskType); writer.Put(data.State); @@ -145,7 +145,7 @@ public static void Serialize(NetDataWriter writer, TaskBeforeDataData data) writer.Put(data.TaskType); } - public static void Deserialize(NetDataReader reader, TaskBeforeDataData data) + public static void Deserialize(NetDataReader reader, TaskData data) { data.State = reader.GetByte(); data.TaskStartTime = reader.GetFloat(); @@ -156,9 +156,9 @@ public static void Deserialize(NetDataReader reader, TaskBeforeDataData data) } } -public class ParallelTasksData : TaskBeforeDataData +public class ParallelTasksData : TaskData { - public TaskBeforeDataData[] Tasks { get; set; } + public TaskData[] Tasks { get; set; } public static ParallelTasksData FromParallelTask(ParallelTasks task) { @@ -170,7 +170,7 @@ public static ParallelTasksData FromParallelTask(ParallelTasks task) public static void Serialize(NetDataWriter writer, ParallelTasksData data) { - TaskBeforeDataData.Serialize(writer, data); + TaskData.Serialize(writer, data); writer.Put((byte)data.Tasks.Length); foreach (var taskBeforeDataData in data.Tasks) SerializeTask(taskBeforeDataData, writer); @@ -181,7 +181,7 @@ public static ParallelTasksData Deserialize(NetDataReader reader) var parallelTask = new ParallelTasksData(); Deserialize(reader, parallelTask); var tasksLength = reader.GetByte(); - var tasks = new TaskBeforeDataData[tasksLength]; + var tasks = new TaskData[tasksLength]; for (int i = 0; i < tasksLength; i++) tasks[i] = DeserializeTask(reader); parallelTask.Tasks = tasks; @@ -189,9 +189,9 @@ public static ParallelTasksData Deserialize(NetDataReader reader) } } -public class SequentialTasksData : TaskBeforeDataData +public class SequentialTasksData : TaskData { - public TaskBeforeDataData[] Tasks { get; set; } + public TaskData[] Tasks { get; set; } public static SequentialTasksData FromSequentialTask(SequentialTasks task) @@ -204,7 +204,7 @@ public static SequentialTasksData FromSequentialTask(SequentialTasks task) public static void Serialize(NetDataWriter writer, SequentialTasksData data) { - TaskBeforeDataData.Serialize(writer, data); + TaskData.Serialize(writer, data); writer.Put((byte)data.Tasks.Length); foreach (var taskBeforeDataData in data.Tasks) SerializeTask(taskBeforeDataData, writer); @@ -215,7 +215,7 @@ public static SequentialTasksData Deserialize(NetDataReader reader) var sequentialTask = new SequentialTasksData(); Deserialize(reader, sequentialTask); var tasksLength = reader.GetByte(); - var tasks = new TaskBeforeDataData[tasksLength]; + var tasks = new TaskData[tasksLength]; for (int i = 0; i < tasksLength; i++) tasks[i] = DeserializeTask(reader); sequentialTask.Tasks = tasks; @@ -223,7 +223,7 @@ public static SequentialTasksData Deserialize(NetDataReader reader) } } -public class WarehouseTaskData : TaskBeforeDataData +public class WarehouseTaskData : TaskData { public string[] Cars { get; set; } public byte WarehouseTaskType { get; set; } @@ -258,7 +258,7 @@ public static WarehouseTask ToWarehouseTask(WarehouseTaskData data) public static void Serialize(NetDataWriter writer, WarehouseTaskData data) { - TaskBeforeDataData.Serialize(writer, data); + TaskData.Serialize(writer, data); writer.PutArray(data.Cars); writer.Put(data.WarehouseTaskType); writer.Put(data.WarehouseMachine); @@ -282,7 +282,7 @@ public static WarehouseTaskData Deserialize(NetDataReader reader) } } -public class TransportTaskData : TaskBeforeDataData +public class TransportTaskData : TaskData { public string[] Cars { get; set; } public string StartingTrack { get; set; } @@ -319,7 +319,7 @@ public static TransportTask ToTransportTask(TransportTaskData data) public static void Serialize(NetDataWriter writer, TransportTaskData data) { - TaskBeforeDataData.Serialize(writer, data); + TaskData.Serialize(writer, data); writer.PutArray(data.Cars); writer.Put(data.StartingTrack); writer.Put(data.DestinationTrack); diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 06cfab4..9a3d397 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -695,8 +695,8 @@ private void OnClientboundJobCreatePacket(ClientboundJobCreatePacket packet) return; List tasks = new List(); - foreach (TaskBeforeDataData taskBeforeDataData in packet.job.Tasks) - tasks.Add(TaskBeforeDataData.ToTask(taskBeforeDataData)); + foreach (Data.TaskData taskBeforeDataData in packet.job.Tasks) + tasks.Add(Data.TaskData.ToTask(taskBeforeDataData)); StationsChainDataData chainData = packet.job.ChainData; @@ -750,8 +750,8 @@ private void OnClientboundJobsPacket(ClientboundJobsPacket packet) ushort netId = packet.netIds[i]; var tasks = new List(); - foreach (TaskBeforeDataData taskBeforeDataData in job.Tasks) - tasks.Add(TaskBeforeDataData.ToTask(taskBeforeDataData)); + foreach (Data.TaskData taskBeforeDataData in job.Tasks) + tasks.Add(Data.TaskData.ToTask(taskBeforeDataData)); StationsChainDataData chainData = job.ChainData; diff --git a/Multiplayer/Networking/Managers/NetworkManager.cs b/Multiplayer/Networking/Managers/NetworkManager.cs index 2d9c3eb..41dc518 100644 --- a/Multiplayer/Networking/Managers/NetworkManager.cs +++ b/Multiplayer/Networking/Managers/NetworkManager.cs @@ -37,7 +37,7 @@ protected NetworkManager(Settings settings) private void RegisterNestedTypes() { netPacketProcessor.RegisterNestedType(BogieData.Serialize, BogieData.Deserialize); - /* Temp for stable releasenetPacketProcessor.RegisterNestedType(JobData.Serialize, JobData.Deserialize);*/ + netPacketProcessor.RegisterNestedType(JobData.Serialize, JobData.Deserialize); netPacketProcessor.RegisterNestedType(ModInfo.Serialize, ModInfo.Deserialize); netPacketProcessor.RegisterNestedType(RigidbodySnapshot.Serialize, RigidbodySnapshot.Deserialize); netPacketProcessor.RegisterNestedType(StationsChainDataData.Serialize, StationsChainDataData.Deserialize); diff --git a/Multiplayer/Patches/Player/CustomFirstPersonControllerPatch.cs b/Multiplayer/Patches/Player/CustomFirstPersonControllerPatch.cs index 1c83a62..d6035bc 100644 --- a/Multiplayer/Patches/Player/CustomFirstPersonControllerPatch.cs +++ b/Multiplayer/Patches/Player/CustomFirstPersonControllerPatch.cs @@ -60,6 +60,9 @@ private static void OnCarChanged(TrainCar trainCar) private static void OnTick(uint tick) { + if(UnloadWatcher.isUnloading) + return; + Vector3 position = isOnCar ? PlayerManager.PlayerTransform.localPosition : PlayerManager.GetWorldAbsolutePlayerPosition(); float rotationY = (isOnCar ? PlayerManager.PlayerTransform.localEulerAngles : PlayerManager.PlayerTransform.eulerAngles).y; diff --git a/Multiplayer/Settings.cs b/Multiplayer/Settings.cs index e786b9f..24d812f 100644 --- a/Multiplayer/Settings.cs +++ b/Multiplayer/Settings.cs @@ -14,6 +14,8 @@ public class Settings : UnityModManager.ModSettings, IDrawable public static Action OnSettingsUpdated; + public int SettingsVer = 0; + [Header("Player")] [Draw("Username", Tooltip = "Your username in-game")] public string Username = "Player"; @@ -85,7 +87,7 @@ public class Settings : UnityModManager.ModSettings, IDrawable public int SimulationMinLatency = 30; [Draw("Maximum Latency (ms)", VisibleOn = "SimulateLatency|true")] public int SimulationMaxLatency = 100; - + public bool ForceJson = false; public void Draw(UnityModManager.ModEntry modEntry) { Settings self = this; @@ -118,4 +120,45 @@ public Guid GetGuid() Guid = guid.ToString(); return guid; } + + public static Settings Load(UnityModManager.ModEntry modEntry) + { + Settings data = Settings.Load(modEntry); + + MigrateSettings(ref data); + + data.SettingsVer = GetCurrentVersion(); + + data.Save(modEntry); + + return data; + } + + private static int GetCurrentVersion() + { + return 1; + } + + // Function to handle migrations based on the current version + private static void MigrateSettings(ref Settings data) + { + switch (data.SettingsVer) + { + case 0: + //We want to disable Punch until it's fully implemented + data.EnableNatPunch = false; + data.SettingsVer = 1; + + //Ensure http setting is upgraded to https if using the default lobby server + if(data.LobbyServerAddress == "http://dv.mineit.space") + data.LobbyServerAddress = new Settings().LobbyServerAddress; + + MigrateSettings(ref data); + break; + + default: + break; + } + + } } diff --git a/info.json b/info.json index c0e628f..1015e9d 100644 --- a/info.json +++ b/info.json @@ -1,6 +1,6 @@ { "Id": "Multiplayer", - "Version": "0.1.7.2", + "Version": "0.1.8.0", "DisplayName": "Multiplayer", "Author": "Insprill, Macka, Morm", "EntryMethod": "Multiplayer.Multiplayer.Load", From eedf22f7a298496102d74ac0f8557fbfb62b636f Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 10 Aug 2024 22:39:11 +1000 Subject: [PATCH 063/188] Fixed bug in RIgidBodySnapshot.Apply() Incorrect checking of flags when applying data to the client RigidBody Changed Rotation from Vector3 to Quaternion and added a Quaternion serialiser --- .../Networking/Data/RigidbodySnapshot.cs | 37 ++++++++++++++----- .../Serverbound/ServerboundAddCoalPacket.cs | 7 ++++ .../Serialization/QuaternionSerializer.cs | 20 ++++++++++ 3 files changed, 55 insertions(+), 9 deletions(-) create mode 100644 Multiplayer/Networking/Packets/Serverbound/ServerboundAddCoalPacket.cs create mode 100644 Multiplayer/Networking/Serialization/QuaternionSerializer.cs diff --git a/Multiplayer/Networking/Data/RigidbodySnapshot.cs b/Multiplayer/Networking/Data/RigidbodySnapshot.cs index d4161d8..06651f3 100644 --- a/Multiplayer/Networking/Data/RigidbodySnapshot.cs +++ b/Multiplayer/Networking/Data/RigidbodySnapshot.cs @@ -1,4 +1,4 @@ -using System; +using System; using LiteNetLib.Utils; using Multiplayer.Networking.Serialization; using UnityEngine; @@ -9,7 +9,7 @@ public class RigidbodySnapshot { public byte IncludedDataFlags { get; set; } public Vector3 Position { get; set; } - public Vector3 Rotation { get; set; } + public Quaternion Rotation { get; set; } public Vector3 Velocity { get; set; } public Vector3 AngularVelocity { get; set; } @@ -17,12 +17,16 @@ public static void Serialize(NetDataWriter writer, RigidbodySnapshot data) { writer.Put(data.IncludedDataFlags); IncludedData flags = (IncludedData)data.IncludedDataFlags; + if (flags.HasFlag(IncludedData.Position)) Vector3Serializer.Serialize(writer, data.Position); + if (flags.HasFlag(IncludedData.Rotation)) - Vector3Serializer.Serialize(writer, data.Rotation); + QuaternionSerializer.Serialize(writer, data.Rotation); + if (flags.HasFlag(IncludedData.Velocity)) Vector3Serializer.Serialize(writer, data.Velocity); + if (flags.HasFlag(IncludedData.AngularVelocity)) Vector3Serializer.Serialize(writer, data.AngularVelocity); } @@ -30,17 +34,23 @@ public static void Serialize(NetDataWriter writer, RigidbodySnapshot data) public static RigidbodySnapshot Deserialize(NetDataReader reader) { IncludedData IncludedDataFlags = (IncludedData)reader.GetByte(); + RigidbodySnapshot snapshot = new() { IncludedDataFlags = (byte)IncludedDataFlags }; + if (IncludedDataFlags.HasFlag(IncludedData.Position)) snapshot.Position = Vector3Serializer.Deserialize(reader); + if (IncludedDataFlags.HasFlag(IncludedData.Rotation)) - snapshot.Rotation = Vector3Serializer.Deserialize(reader); + snapshot.Rotation = QuaternionSerializer.Deserialize(reader); + if (IncludedDataFlags.HasFlag(IncludedData.Velocity)) snapshot.Velocity = Vector3Serializer.Deserialize(reader); + if (IncludedDataFlags.HasFlag(IncludedData.AngularVelocity)) snapshot.AngularVelocity = Vector3Serializer.Deserialize(reader); + return snapshot; } @@ -49,27 +59,36 @@ public static RigidbodySnapshot From(Rigidbody rb, IncludedData includedDataFlag RigidbodySnapshot snapshot = new() { IncludedDataFlags = (byte)includedDataFlags }; + if (includedDataFlags.HasFlag(IncludedData.Position)) snapshot.Position = rb.position - WorldMover.currentMove; + if (includedDataFlags.HasFlag(IncludedData.Rotation)) - snapshot.Rotation = rb.rotation.eulerAngles; + snapshot.Rotation = rb.rotation;//.eulerAngles; + if (includedDataFlags.HasFlag(IncludedData.Velocity)) snapshot.Velocity = rb.velocity; + if (includedDataFlags.HasFlag(IncludedData.AngularVelocity)) snapshot.AngularVelocity = rb.angularVelocity; + return snapshot; } public void Apply(Rigidbody rb) { IncludedData flags = (IncludedData)IncludedDataFlags; + if (flags.HasFlag(IncludedData.Position)) rb.MovePosition(Position + WorldMover.currentMove); - if (flags.HasFlag(IncludedData.Position)) - rb.MoveRotation(Quaternion.Euler(Rotation)); - if (flags.HasFlag(IncludedData.Position)) + + if (flags.HasFlag(IncludedData.Rotation)) + rb.MoveRotation(Rotation); + + if (flags.HasFlag(IncludedData.Velocity)) rb.velocity = Velocity; - if (flags.HasFlag(IncludedData.Position)) + + if (flags.HasFlag(IncludedData.AngularVelocity)) rb.angularVelocity = AngularVelocity; } diff --git a/Multiplayer/Networking/Packets/Serverbound/ServerboundAddCoalPacket.cs b/Multiplayer/Networking/Packets/Serverbound/ServerboundAddCoalPacket.cs new file mode 100644 index 0000000..582038d --- /dev/null +++ b/Multiplayer/Networking/Packets/Serverbound/ServerboundAddCoalPacket.cs @@ -0,0 +1,7 @@ +namespace Multiplayer.Networking.Packets.Serverbound; + +public class ServerboundAddCoalPacket +{ + public ushort NetId { get; set; } + public float CoalMassDelta { get; set; } +} diff --git a/Multiplayer/Networking/Serialization/QuaternionSerializer.cs b/Multiplayer/Networking/Serialization/QuaternionSerializer.cs new file mode 100644 index 0000000..cd95a4d --- /dev/null +++ b/Multiplayer/Networking/Serialization/QuaternionSerializer.cs @@ -0,0 +1,20 @@ +using LiteNetLib.Utils; +using UnityEngine; + +namespace Multiplayer.Networking.Serialization; + +public static class QuaternionSerializer +{ + public static void Serialize(NetDataWriter writer, Quaternion quat) + { + writer.Put(quat.x); + writer.Put(quat.y); + writer.Put(quat.z); + writer.Put(quat.w); + } + + public static Quaternion Deserialize(NetDataReader reader) + { + return new Quaternion(reader.GetFloat(), reader.GetFloat(), reader.GetFloat(), reader.GetFloat()); + } +} From 0cf6e43bcf3fee7adcc50f15d9767a2acd96fa6e Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 10 Aug 2024 22:45:55 +1000 Subject: [PATCH 064/188] Steam locomotive sync improvements Shoveling coal and igniting the firebox are 'one shot'/impulse functions and need a different sync methodology. Further review of all car sync might be required to lower network demand --- .../Networking/Train/NetworkedTrainCar.cs | 105 +++++++++++++++++- .../Managers/Client/NetworkClient.cs | 30 ++++- .../Managers/Server/NetworkServer.cs | 70 +++++++++++- .../Train/ClientboundFireboxStatePacket.cs | 9 ++ .../ServerboundFireboxIgnitePacket.cs | 12 ++ 5 files changed, 221 insertions(+), 5 deletions(-) create mode 100644 Multiplayer/Networking/Packets/Clientbound/Train/ClientboundFireboxStatePacket.cs create mode 100644 Multiplayer/Networking/Packets/Serverbound/ServerboundFireboxIgnitePacket.cs diff --git a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs index eec4757..bfcb44a 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs @@ -72,6 +72,7 @@ public static bool TryGetFromTrainCar(TrainCar trainCar, out NetworkedTrainCar n private bool hasSimFlow; private SimulationFlow simulationFlow; + public FireboxSimController firebox; private HashSet dirtyPorts; private Dictionary lastSentPortValues; @@ -86,6 +87,7 @@ public static bool TryGetFromTrainCar(TrainCar trainCar, out NetworkedTrainCar n public byte CargoModelIndex = byte.MaxValue; private bool healthDirty; private bool sendCouplers; + private bool fireboxDirty; public bool IsDestroying; @@ -147,8 +149,15 @@ private void Start() dirtyFuses = new HashSet(simulationFlow.fullFuseIdToFuse.Count); foreach (KeyValuePair kvp in simulationFlow.fullFuseIdToFuse) kvp.Value.StateUpdated += _ => { Common_OnFuseUpdated(kvp.Value); }; + + if (simController.firebox != null) + { + firebox = simController.firebox; + firebox.fireboxCoalControlPort.ValueUpdatedInternally += Client_OnAddCoal; //Player adding coal + firebox.fireboxIgnitionPort.ValueUpdatedInternally += Client_OnIgnite; //Player igniting firebox + } } - + brakeSystem.HandbrakePositionChanged += Common_OnHandbrakePositionChanged; brakeSystem.BrakeCylinderReleased += Common_OnBrakeCylinderReleased; @@ -163,6 +172,12 @@ private void Start() brakeSystem.MainResPressureChanged += Server_MainResUpdate; + if (firebox != null) + { + firebox.fireboxContentsPort.ValueUpdatedInternally += Common_OnFireboxUpdate; + firebox.fireOnPort.ValueUpdatedInternally += Common_OnFireboxUpdate; + } + StartCoroutine(Server_WaitForLogicCar()); } } @@ -185,8 +200,16 @@ private void OnDisable() foreach (Coupler coupler in TrainCar.couplers) hoseToCoupler.Remove(coupler.hoseAndCock); + + if (firebox != null) + { + firebox.fireboxCoalControlPort.ValueUpdatedInternally -= Client_OnAddCoal; //Player adding coal + firebox.fireboxIgnitionPort.ValueUpdatedInternally -= Client_OnIgnite; //Player igniting firebox + } + brakeSystem.HandbrakePositionChanged -= Common_OnHandbrakePositionChanged; brakeSystem.BrakeCylinderReleased -= Common_OnBrakeCylinderReleased; + if (NetworkLifecycle.Instance.IsHost()) { bogie1.TrackChanged -= Server_BogieTrackChanged; @@ -195,6 +218,12 @@ private void OnDisable() brakeSystem.MainResPressureChanged -= Server_MainResUpdate; + if (firebox != null) + { + firebox.fireboxContentsPort.ValueUpdatedInternally -= Common_OnFireboxUpdate; + firebox.fireOnPort.ValueUpdatedInternally -= Common_OnFireboxUpdate; + } + if (TrainCar.logicCar != null) { TrainCar.logicCar.CargoLoaded -= Server_OnCargoLoaded; @@ -242,6 +271,8 @@ public void Server_DirtyAllState() healthDirty = true; BogieTracksDirty = true; sendCouplers = true; + fireboxDirty = firebox != null; //only dirty if exists + if (!hasSimFlow) return; foreach (string portId in simulationFlow.fullPortIdToPort.Keys) @@ -271,6 +302,10 @@ public bool Server_ValidateClientSimFlowPacket(ServerPlayer player, CommonTrainP Common_DirtyPorts(packet.PortIds); return false; } + else + { + NetworkLifecycle.Instance.Server.LogWarning($"Player {player.Username} sent portId: {portId}, value type: {port.valueType}"); + } // Only allow the player to update ports on the car they are in/near if (player.CarId == packet.NetId) @@ -315,11 +350,18 @@ private void Server_MainResUpdate(float normalizedPressure, float pressure) mainResPressureDirty = true; } + private void Server_FireboxUpdate(float normalizedPressure, float pressure) + { + fireboxDirty = true; + } + private void Server_OnTick(uint tick) { if (UnloadWatcher.isUnloading) return; + Server_SendBrakePressures(); + Server_SendFireBoxState(); Server_SendCouplers(); Server_SendCargoState(); Server_SendHealthState(); @@ -333,6 +375,15 @@ private void Server_SendBrakePressures() NetworkLifecycle.Instance.Server.SendBrakePressures(NetId, brakeSystem.mainReservoirPressure, brakeSystem.independentPipePressure, brakeSystem.brakePipePressure, brakeSystem.brakeCylinderPressure); } + private void Server_SendFireBoxState() + { + if (!fireboxDirty || firebox == null) + return; + + fireboxDirty = false; + NetworkLifecycle.Instance.Server.SendFireboxState(NetId, firebox.fireboxContentsPort.value, firebox.IsFireOn); + } + private void Server_SendCouplers() { if (!sendCouplers) @@ -375,6 +426,7 @@ private void Common_OnTick(uint tick) { if (UnloadWatcher.isUnloading) return; + Common_SendHandbrakePosition(); Common_SendFuses(); Common_SendPorts(); @@ -386,6 +438,7 @@ private void Common_SendHandbrakePosition() return; if (!TrainCar.brakeSystem.hasHandbrake) return; + handbrakeDirty = false; NetworkLifecycle.Instance.Client.SendHandbrakePositionChanged(NetId, brakeSystem.handbrakePosition); } @@ -477,6 +530,14 @@ private void Common_OnBrakeCylinderReleased() NetworkLifecycle.Instance.Client.SendBrakeCylinderReleased(NetId); } + private void Common_OnFireboxUpdate(float _) + { + if (NetworkLifecycle.Instance.IsProcessingPacket) + return; + + fireboxDirty = true; + } + private void Common_OnPortUpdated(Port port) { if (UnloadWatcher.isUnloading || NetworkLifecycle.Instance.IsProcessingPacket) @@ -500,15 +561,23 @@ public void Common_UpdatePorts(CommonTrainPortsPacket packet) if (!hasSimFlow) return; + string log = $"CommonTrainPortsPacket({TrainCar.ID})"; for (int i = 0; i < packet.PortIds.Length; i++) { Port port = simulationFlow.fullPortIdToPort[packet.PortIds[i]]; float value = packet.PortValues[i]; + float before = port.value; + if (port.type == PortType.EXTERNAL_IN) port.ExternalValueUpdate(value); else port.Value = value; + + if (Multiplayer.Settings.DebugLogging) + log += $"\r\n\tPort name: {port.id}, value before: {before}, value after: {port.value}, value: {value}, port type: {port.type}"; } + + NetworkLifecycle.Instance.Client.LogDebug(() => log); } public void Common_UpdateFuses(CommonTrainFusesPacket packet) @@ -571,6 +640,40 @@ public void Client_ReceiveBrakePressureUpdate(float mainReservoirPressure, float brakeSystem.brakePipePressure = brakePipePressure; brakeSystem.brakeCylinderPressure = brakeCylinderPressure; } + private void Client_OnAddCoal(float coalMassDelta) + { + if (NetworkLifecycle.Instance.IsProcessingPacket) + return; + + if (coalMassDelta <= 0) + return; + + NetworkLifecycle.Instance.Client.LogDebug(() => $"Common_OnAddCoal({TrainCar.ID}): coalMassDelta: {coalMassDelta}"); + NetworkLifecycle.Instance.Client.SendAddCoal(NetId, coalMassDelta); + } + private void Client_OnIgnite(float ignition) + { + if (NetworkLifecycle.Instance.IsProcessingPacket) + return; + + if (ignition == 0f) + return; + + NetworkLifecycle.Instance.Client.LogDebug(() => $"Common_OnIgnite({TrainCar.ID})"); + NetworkLifecycle.Instance.Client.SendFireboxIgnition(NetId); + } + + public void Client_ReceiveFireboxStateUpdate(float fireboxContents, bool isOn) + { + if (firebox == null) + return; + + if (!hasSimFlow) + return; + + firebox.fireboxContentsPort.Value = fireboxContents; + firebox.fireOnPort.Value = isOn ? 1f : 0f; + } #endregion } diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 9a3d397..6e2f729 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -579,6 +579,17 @@ private void OnClientboundBrakePressureUpdatePacket(ClientboundBrakePressureUpda //Multiplayer.LogDebug(() => $"Received Brake Pressures netId {packet.NetId}: {packet.MainReservoirPressure}, {packet.IndependentPipePressure}, {packet.BrakePipePressure}, {packet.BrakeCylinderPressure}"); } + private void OnClientboundFireboxStatePacket(ClientboundFireboxStatePacket packet) + { + if (!NetworkedTrainCar.Get(packet.NetId, out NetworkedTrainCar networkedTrainCar)) + return; + + + networkedTrainCar.Client_ReceiveFireboxStateUpdate(packet.Contents, packet.IsOn); + + Multiplayer.LogDebug(() => $"Received Brake Pressures netId {packet.NetId}: {packet.Contents}, {packet.IsOn}"); + } + private void OnClientboundCargoStatePacket(ClientboundCargoStatePacket packet) { if (!NetworkedTrainCar.Get(packet.NetId, out NetworkedTrainCar networkedTrainCar)) @@ -1009,6 +1020,22 @@ public void SendHandbrakePositionChanged(ushort netId, float position) Position = position }, DeliveryMethod.ReliableOrdered); } + public void SendAddCoal(ushort netId, float coalMassDelta) + { + SendPacketToServer(new ServerboundAddCoalPacket + { + NetId = netId, + CoalMassDelta = coalMassDelta + }, DeliveryMethod.ReliableOrdered); + } + + public void SendFireboxIgnition(ushort netId) + { + SendPacketToServer(new ServerboundFireboxIgnitePacket + { + NetId = netId, + }, DeliveryMethod.ReliableOrdered); + } public void SendPorts(ushort netId, string[] portIds, float[] portValues) { @@ -1019,13 +1046,14 @@ public void SendPorts(ushort netId, string[] portIds, float[] portValues) PortValues = portValues }, DeliveryMethod.ReliableOrdered); + /* string log=$"Sending ports netId: {netId}"; for (int i = 0; i < portIds.Length; i++) { log += $"\r\n\t{portIds[i]}: {portValues[i]}"; } Multiplayer.LogDebug(() => log); - + */ } public void SendFuses(ushort netId, string[] fuseIds, bool[] fuseValues) diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index 502ef61..91ea230 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -128,6 +128,8 @@ protected override void Subscribe() netPacketProcessor.SubscribeReusable(OnCommonCockFiddlePacket); netPacketProcessor.SubscribeReusable(OnCommonBrakeCylinderReleasePacket); netPacketProcessor.SubscribeReusable(OnCommonHandbrakePositionPacket); + netPacketProcessor.SubscribeReusable(OnServerboundAddCoalPacket); + netPacketProcessor.SubscribeReusable(OnServerboundFireboxIgnitePacket); netPacketProcessor.SubscribeReusable(OnCommonTrainPortsPacket); netPacketProcessor.SubscribeReusable(OnCommonTrainFusesPacket); netPacketProcessor.SubscribeReusable(OnServerboundJobTakeRequestPacket); @@ -287,6 +289,18 @@ public void SendBrakePressures(ushort netId, float mainReservoirPressure, float //Multiplayer.LogDebug(()=> $"Sending Brake Pressures netId {netId}: {mainReservoirPressure}, {independentPipePressure}, {brakePipePressure}, {brakeCylinderPressure}"); } + public void SendFireboxState(ushort netId, float fireboxContents, bool fireboxOn) + { + SendPacketToAll(new ClientboundFireboxStatePacket + { + NetId = netId, + Contents = fireboxContents, + IsOn = fireboxOn + }, DeliveryMethod.ReliableOrdered, selfPeer); + + Multiplayer.LogDebug(() => $"Sending Firebox States netId {netId}: {fireboxContents}, {fireboxOn}"); + } + public void SendCargoState(TrainCar trainCar, ushort netId, bool isLoading, byte cargoModelIndex) { Car logicCar = trainCar.logicCar; @@ -579,7 +593,7 @@ private void OnServerboundClientReadyPacket(ServerboundClientReadyPacket packet, SendPacket(peer, ClientboundSpawnTrainSetPacket.FromTrainSet(set), DeliveryMethod.ReliableOrdered); } - /* Temp for stable release + /* //send jobs - do we need a job manager/job IDs to make this easier? foreach(StationController station in StationController.allStations) { @@ -732,15 +746,65 @@ private void OnCommonHandbrakePositionPacket(CommonHandbrakePositionPacket packe SendPacketToAll(packet, DeliveryMethod.ReliableOrdered, peer); } - private void OnCommonTrainPortsPacket(CommonTrainPortsPacket packet, NetPeer peer) + private void OnServerboundAddCoalPacket(ServerboundAddCoalPacket packet, NetPeer peer) { if (!TryGetServerPlayer(peer, out ServerPlayer player)) return; + if (!NetworkedTrainCar.Get(packet.NetId, out NetworkedTrainCar networkedTrainCar)) return; - if (!NetworkLifecycle.Instance.IsHost(peer) && !networkedTrainCar.Server_ValidateClientSimFlowPacket(player, packet)) + + //is value valid? + if (float.IsNaN(packet.CoalMassDelta)) return; + if (!NetworkLifecycle.Instance.IsHost(peer)) + { + float carLength = CarSpawner.Instance.carLiveryToCarLength[networkedTrainCar.TrainCar.carLivery]; + + //is player close enough to add coal? + if ((player.WorldPosition - networkedTrainCar.transform.position).sqrMagnitude <= carLength * carLength) + networkedTrainCar.firebox?.fireboxCoalControlPort.ExternalValueUpdate(packet.CoalMassDelta); + } + + } + + private void OnServerboundFireboxIgnitePacket(ServerboundFireboxIgnitePacket packet, NetPeer peer) + { + if (!TryGetServerPlayer(peer, out ServerPlayer player)) + return; + + if (!NetworkedTrainCar.Get(packet.NetId, out NetworkedTrainCar networkedTrainCar)) + return; + + if (!NetworkLifecycle.Instance.IsHost(peer)) + { + //is player close enough to ignite firebox? + float carLength = CarSpawner.Instance.carLiveryToCarLength[networkedTrainCar.TrainCar.carLivery]; + if ((player.WorldPosition - networkedTrainCar.transform.position).sqrMagnitude <= carLength * carLength) + networkedTrainCar.firebox?.Ignite(); + } + } + + private void OnCommonTrainPortsPacket(CommonTrainPortsPacket packet, NetPeer peer) + { + if (!TryGetServerPlayer(peer, out ServerPlayer player)) + return; + if (!NetworkedTrainCar.Get(packet.NetId, out NetworkedTrainCar networkedTrainCar)) + return; + + //if not the host && validation fails then ignore packet + if (!NetworkLifecycle.Instance.IsHost(peer)) + { + bool flag = networkedTrainCar.Server_ValidateClientSimFlowPacket(player, packet); + + LogDebug(() => $"OnCommonTrainPortsPacket from {player.Username}, Not host, valid: {flag}"); + if (!flag) + { + return; + } + } + SendPacketToAll(packet, DeliveryMethod.ReliableOrdered, peer); } diff --git a/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundFireboxStatePacket.cs b/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundFireboxStatePacket.cs new file mode 100644 index 0000000..1055146 --- /dev/null +++ b/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundFireboxStatePacket.cs @@ -0,0 +1,9 @@ +namespace Multiplayer.Networking.Packets.Clientbound.Train; + +public class ClientboundFireboxStatePacket +{ + public ushort NetId { get; set; } + public float Contents { get; set; } + + public bool IsOn { get; set; } +} diff --git a/Multiplayer/Networking/Packets/Serverbound/ServerboundFireboxIgnitePacket.cs b/Multiplayer/Networking/Packets/Serverbound/ServerboundFireboxIgnitePacket.cs new file mode 100644 index 0000000..9898aac --- /dev/null +++ b/Multiplayer/Networking/Packets/Serverbound/ServerboundFireboxIgnitePacket.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Multiplayer.Networking.Packets.Serverbound; + +public class ServerboundFireboxIgnitePacket +{ + public ushort NetId { get; set; } +} From ac30237473bd3c82244d6da5ac8b5b8e1424baaa Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 10 Aug 2024 22:46:14 +1000 Subject: [PATCH 065/188] Disabled chat in single player mode --- .../Networking/Managers/Client/NetworkClient.cs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 6e2f729..ae23bfa 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -363,13 +363,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) + if (!isSinglePlayer) { - // - GameObject chat = new GameObject("Chat GUI", typeof(ChatGUI)); - chat.transform.SetParent(common.transform, false); - chatGUI = chat.GetComponent(); + 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(); + } } } From 22c2ea2e10e945b6e6a3bc541b1ff6c2d83c097a Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 11 Aug 2024 19:25:25 +1000 Subject: [PATCH 066/188] Implemented potential fix for error accumulation and de-sync --- .../Train/NetworkTrainsetWatcher.cs | 158 +++++++++++++++--- .../Networking/Train/NetworkedTrainCar.cs | 20 ++- .../Networking/Data/TrainsetMovementPart.cs | 50 ++++-- 3 files changed, 190 insertions(+), 38 deletions(-) diff --git a/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs b/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs index ad025e3..da69b3a 100644 --- a/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs +++ b/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs @@ -11,6 +11,9 @@ public class NetworkTrainsetWatcher : SingletonBehaviour { private ClientboundTrainsetPhysicsPacket cachedSendPacket; + const float DESIRED_FULL_SYNC_INTERVAL = 2f; // in seconds + const int MAX_UNSYNC_TICKS = (int)(NetworkLifecycle.TICK_RATE * DESIRED_FULL_SYNC_INTERVAL); + protected override void Awake() { base.Awake(); @@ -43,35 +46,47 @@ private void Server_OnTick(uint tick) private void Server_TickSet(Trainset set) { - bool dirty = false; - foreach (TrainCar trainCar in set.cars) - { - if (trainCar.isStationary) - continue; - dirty = true; - break; - } + bool anyCarMoving = false; + bool maxTicksReached = false; - if (!dirty) + if (set == null) + { + Multiplayer.LogError($"Server_TickSet(): Received null set!"); return; + } cachedSendPacket.NetId = set.firstCar.GetNetId(); - //car may not be initialised, missing a valid NetID if (cachedSendPacket.NetId == 0) return; - if (set.cars.Contains(null)) + foreach (TrainCar trainCar in set.cars) { - Multiplayer.LogError($"Trainset {set.id} ({set.firstCar.GetNetId()} has a null car!"); - return; + if (trainCar == null || !trainCar.gameObject.activeSelf) + { + Multiplayer.LogError($"Trainset {set.id} ({set.firstCar?.GetNetId()} has a null or inactive ({trainCar?.gameObject.activeSelf}) car!"); + return; + } + + //If we can locate the networked car, we'll add to the ticks counter + if (NetworkedTrainCar.TryGetFromTrainCar(trainCar, out NetworkedTrainCar netTC)) + { + netTC.TicksSinceSync++; + maxTicksReached |= netTC.TicksSinceSync >= MAX_UNSYNC_TICKS; + } + + //Even if the car is stationary, if the max ticks has been exceeded we will still sync + if (!trainCar.isStationary) + anyCarMoving = true; + + //we can finish checking early if we have BOTH a dirty and a max ticks + if (anyCarMoving && maxTicksReached) + break; } - if (set.cars.Any(car => !car.gameObject.activeSelf)) - { - Multiplayer.LogError($"Trainset {set.id} ({set.firstCar.GetNetId()} has a non-active car!"); + //if any car is dirty or exceeded its max ticks we will re-sync the entire train + if (!anyCarMoving && !maxTicksReached) return; - } TrainsetMovementPart[] trainsetParts = new TrainsetMovementPart[set.cars.Count]; bool anyTracksDirty = false; @@ -80,23 +95,35 @@ private void Server_TickSet(Trainset set) TrainCar trainCar = set.cars[i]; if (!trainCar.TryNetworked(out NetworkedTrainCar networkedTrainCar)) { - Multiplayer.LogDebug(() => $"TrainCar UNKNOWN is not networked! Is active? {trainCar.gameObject.activeInHierarchy}"); Multiplayer.LogDebug(() => $"TrainCar {trainCar.ID} is not networked! Is active? {trainCar.gameObject.activeInHierarchy}"); continue; } - //NetworkedTrainCar networkedTrainCar = trainCar.Networked(); anyTracksDirty |= networkedTrainCar.BogieTracksDirty; if (trainCar.derailed) + { trainsetParts[i] = new TrainsetMovementPart(RigidbodySnapshot.From(trainCar.rb)); + } else + { + RigidbodySnapshot? snapshot = null; + + //Have we exceeded the max ticks? + if (maxTicksReached) + { + snapshot = RigidbodySnapshot.From(trainCar.rb); + networkedTrainCar.TicksSinceSync = 0; //reset this car's tick count + } + trainsetParts[i] = new TrainsetMovementPart( trainCar.GetForwardSpeed(), trainCar.stress.slowBuildUpStress, BogieData.FromBogie(trainCar.Bogies[0], networkedTrainCar.BogieTracksDirty, networkedTrainCar.Bogie1TrackDirection), - BogieData.FromBogie(trainCar.Bogies[1], networkedTrainCar.BogieTracksDirty, networkedTrainCar.Bogie2TrackDirection) + BogieData.FromBogie(trainCar.Bogies[1], networkedTrainCar.BogieTracksDirty, networkedTrainCar.Bogie2TrackDirection), + snapshot ); + } } cachedSendPacket.TrainsetParts = trainsetParts; @@ -138,3 +165,94 @@ public void Client_HandleTrainsetPhysicsUpdate(ClientboundTrainsetPhysicsPacket return $"[{nameof(NetworkTrainsetWatcher)}]"; } } + +/* Backup + * private void Server_TickSet(Trainset set) + { + bool dirty = false; + bool maxTicks = false; + + foreach (TrainCar trainCar in set.cars) + { + //If we can locate the networked car, we'll add to the ticks counter + if(NetworkedTrainCar.TryGetFromTrainCar(trainCar, out NetworkedTrainCar netTC)) + netTC.TicksSinceSync++; + + //if we've exceeded the max ticks since a full sync + if(netTC != null && netTC.TicksSinceSync >= MAX_UNSYNC_TICKS) + maxTicks = true; + + //Even if the car is stationary, if the max ticks has been exceeded we will still sync + if (trainCar.isStationary) + continue; + + dirty = true; + break; + } + + //if any car is dirty or exceeded its max ticks we will re-sync the entire train + if (!dirty && !maxTicks) + return; + + cachedSendPacket.NetId = set.firstCar.GetNetId(); + + //car may not be initialised, missing a valid NetID + if (cachedSendPacket.NetId == 0) + return; + + if (set.cars.Contains(null)) + { + Multiplayer.LogError($"Trainset {set.id} ({set.firstCar.GetNetId()} has a null car!"); + return; + } + + if (set.cars.Any(car => !car.gameObject.activeSelf)) + { + Multiplayer.LogError($"Trainset {set.id} ({set.firstCar.GetNetId()} has a non-active car!"); + return; + } + + TrainsetMovementPart[] trainsetParts = new TrainsetMovementPart[set.cars.Count]; + bool anyTracksDirty = false; + for (int i = 0; i < set.cars.Count; i++) + { + TrainCar trainCar = set.cars[i]; + if (!trainCar.TryNetworked(out NetworkedTrainCar networkedTrainCar)) + { + Multiplayer.LogDebug(() => $"TrainCar {trainCar?.ID ?? "UNKOWN"} is not networked! Is active? {trainCar.gameObject.activeInHierarchy}"); + continue; + } + + anyTracksDirty |= networkedTrainCar.BogieTracksDirty; + + if (trainCar.derailed) + { + trainsetParts[i] = new TrainsetMovementPart(RigidbodySnapshot.From(trainCar.rb)); + } + else + { + RigidbodySnapshot? snapshot = null; + + //Have we exceeded the max ticks? + if (maxTicks) + { + snapshot = RigidbodySnapshot.From(trainCar.rb); + networkedTrainCar.TicksSinceSync = 0; //reset this car's tick count + } + + trainsetParts[i] = new TrainsetMovementPart( + trainCar.GetForwardSpeed(), + trainCar.stress.slowBuildUpStress, + BogieData.FromBogie(trainCar.Bogies[0], networkedTrainCar.BogieTracksDirty, networkedTrainCar.Bogie1TrackDirection), + BogieData.FromBogie(trainCar.Bogies[1], networkedTrainCar.BogieTracksDirty, networkedTrainCar.Bogie2TrackDirection), + snapshot + ); + + + } + } + + cachedSendPacket.TrainsetParts = trainsetParts; + NetworkLifecycle.Instance.Server.SendTrainsetPhysicsUpdate(cachedSendPacket, anyTracksDirty); + } +*/ diff --git a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs index bfcb44a..4924147 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs @@ -98,6 +98,7 @@ public static bool TryGetFromTrainCar(TrainCar trainCar, out NetworkedTrainCar n public TickedQueue Client_trainRigidbodyQueue; private TickedQueue client_bogie1Queue; private TickedQueue client_bogie2Queue; + public uint TicksSinceSync = uint.MaxValue; #endregion @@ -607,16 +608,25 @@ public void Client_ReceiveTrainPhysicsUpdate(in TrainsetMovementPart movementPar { if (!client_Initialized) return; + if (TrainCar.isEligibleForSleep) TrainCar.ForceOptimizationState(false); - if (movementPart.IsRigidbodySnapshot) + + // Handle RigidBody snapshot (common for both derailed and normal states) + if (movementPart.MovementType.HasFlag(TrainsetMovementPart.TrainsetMovementType.RigidBody)) + Client_trainRigidbodyQueue.ReceiveSnapshot(movementPart.RigidbodySnapshot, tick); + + // Handle derailment (only rigid body) + if (movementPart.MovementType == TrainsetMovementPart.TrainsetMovementType.RigidBody) { TrainCar.Derail(); TrainCar.stress.ResetTrainStress(); - Client_trainRigidbodyQueue.ReceiveSnapshot(movementPart.RigidbodySnapshot, tick); + return; } - else + + // Handle normal movement (bogie data and optional rigid body data) + if (movementPart.MovementType.HasFlag(TrainsetMovementPart.TrainsetMovementType.Bogie)) { Client_trainSpeedQueue.ReceiveSnapshot(movementPart.Speed, tick); TrainCar.stress.slowBuildUpStress = movementPart.SlowBuildUpStress; @@ -648,7 +658,7 @@ private void Client_OnAddCoal(float coalMassDelta) if (coalMassDelta <= 0) return; - NetworkLifecycle.Instance.Client.LogDebug(() => $"Common_OnAddCoal({TrainCar.ID}): coalMassDelta: {coalMassDelta}"); + //NetworkLifecycle.Instance.Client.LogDebug(() => $"Common_OnAddCoal({TrainCar.ID}): coalMassDelta: {coalMassDelta}"); NetworkLifecycle.Instance.Client.SendAddCoal(NetId, coalMassDelta); } @@ -660,7 +670,7 @@ private void Client_OnIgnite(float ignition) if (ignition == 0f) return; - NetworkLifecycle.Instance.Client.LogDebug(() => $"Common_OnIgnite({TrainCar.ID})"); + //NetworkLifecycle.Instance.Client.LogDebug(() => $"Common_OnIgnite({TrainCar.ID})"); NetworkLifecycle.Instance.Client.SendFireboxIgnition(NetId); } diff --git a/Multiplayer/Networking/Data/TrainsetMovementPart.cs b/Multiplayer/Networking/Data/TrainsetMovementPart.cs index 62fade0..145847a 100644 --- a/Multiplayer/Networking/Data/TrainsetMovementPart.cs +++ b/Multiplayer/Networking/Data/TrainsetMovementPart.cs @@ -1,28 +1,38 @@ using LiteNetLib.Utils; +using System; namespace Multiplayer.Networking.Data; public readonly struct TrainsetMovementPart { - public readonly bool IsRigidbodySnapshot; + public readonly TrainsetMovementType MovementType; public readonly float Speed; public readonly float SlowBuildUpStress; public readonly BogieData Bogie1; public readonly BogieData Bogie2; public readonly RigidbodySnapshot RigidbodySnapshot; - public TrainsetMovementPart(float speed, float slowBuildUpStress, BogieData bogie1, BogieData bogie2) + [Flags] + public enum TrainsetMovementType : byte { - IsRigidbodySnapshot = false; + RigidBody = 1, + Bogie = 2, + All = byte.MaxValue + } + + public TrainsetMovementPart(float speed, float slowBuildUpStress, BogieData bogie1, BogieData bogie2, RigidbodySnapshot rigidbodySnapshot = null) + { + MovementType = rigidbodySnapshot != null ? TrainsetMovementType.All : TrainsetMovementType.Bogie; Speed = speed; SlowBuildUpStress = slowBuildUpStress; Bogie1 = bogie1; Bogie2 = bogie2; + RigidbodySnapshot = rigidbodySnapshot; } public TrainsetMovementPart(RigidbodySnapshot rigidbodySnapshot) { - IsRigidbodySnapshot = true; + MovementType = TrainsetMovementType.RigidBody; RigidbodySnapshot = rigidbodySnapshot; } @@ -30,13 +40,13 @@ public TrainsetMovementPart(RigidbodySnapshot rigidbodySnapshot) public static void Serialize(NetDataWriter writer, TrainsetMovementPart data) #pragma warning restore EPS05 { - writer.Put(data.IsRigidbodySnapshot); + writer.Put((byte)data.MovementType); - if (data.IsRigidbodySnapshot) - { + if (data.MovementType.HasFlag(TrainsetMovementType.RigidBody)) RigidbodySnapshot.Serialize(writer, data.RigidbodySnapshot); + + if (!data.MovementType.HasFlag(TrainsetMovementType.Bogie)) return; - } writer.Put(data.Speed); writer.Put(data.SlowBuildUpStress); @@ -46,14 +56,28 @@ public static void Serialize(NetDataWriter writer, TrainsetMovementPart data) public static TrainsetMovementPart Deserialize(NetDataReader reader) { - bool isRigidbodySnapshot = reader.GetBool(); - return isRigidbodySnapshot - ? new TrainsetMovementPart(RigidbodySnapshot.Deserialize(reader)) - : new TrainsetMovementPart( + TrainsetMovementType movementType = (TrainsetMovementType)reader.GetByte(); + RigidbodySnapshot snapshot = null; + + if (movementType.HasFlag(TrainsetMovementType.RigidBody)) + snapshot = RigidbodySnapshot.Deserialize(reader); + + if (movementType.HasFlag(TrainsetMovementType.Bogie)) + { + float speed = reader.GetFloat(); + float slowBuildUpStress = reader.GetFloat(); + BogieData bogie1 = BogieData.Deserialize(reader); + BogieData bogie2 = BogieData.Deserialize(reader); + + return new TrainsetMovementPart( reader.GetFloat(), reader.GetFloat(), BogieData.Deserialize(reader), - BogieData.Deserialize(reader) + BogieData.Deserialize(reader), + snapshot ); + } + + return new TrainsetMovementPart(snapshot); } } From ffe031329c303579facba04c6efdd3eb47b1fc36 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 11 Aug 2024 19:26:02 +1000 Subject: [PATCH 067/188] Cleanup of unused code / variables --- Multiplayer/Components/MainMenu/HostGamePane.cs | 2 +- Multiplayer/Components/MainMenu/ServerBrowserPane.cs | 2 +- Multiplayer/Components/Networking/NetworkLifecycle.cs | 2 -- Multiplayer/Patches/MainMenu/LauncherControllerPatch.cs | 2 +- 4 files changed, 3 insertions(+), 5 deletions(-) diff --git a/Multiplayer/Components/MainMenu/HostGamePane.cs b/Multiplayer/Components/MainMenu/HostGamePane.cs index 3378207..eb25935 100644 --- a/Multiplayer/Components/MainMenu/HostGamePane.cs +++ b/Multiplayer/Components/MainMenu/HostGamePane.cs @@ -39,7 +39,7 @@ public class HostGamePane : MonoBehaviour ButtonDV startButton; - GameObject ViewPort; + //GameObject ViewPort; public ISaveGame saveGame; public UIStartGameData startGameData; diff --git a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs index e2b002d..9ace5f1 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs @@ -50,7 +50,7 @@ public class ServerBrowserPane : MonoBehaviour //Misc GUI Elements private TextMeshProUGUI serverName; private TextMeshProUGUI detailsPane; - private ScrollRect serverInfo; + //private ScrollRect serverInfo; private bool serverRefreshing = false; diff --git a/Multiplayer/Components/Networking/NetworkLifecycle.cs b/Multiplayer/Components/Networking/NetworkLifecycle.cs index 2fdcfc8..b2e519f 100644 --- a/Multiplayer/Components/Networking/NetworkLifecycle.cs +++ b/Multiplayer/Components/Networking/NetworkLifecycle.cs @@ -43,8 +43,6 @@ 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. diff --git a/Multiplayer/Patches/MainMenu/LauncherControllerPatch.cs b/Multiplayer/Patches/MainMenu/LauncherControllerPatch.cs index 17d4e4c..49c3469 100644 --- a/Multiplayer/Patches/MainMenu/LauncherControllerPatch.cs +++ b/Multiplayer/Patches/MainMenu/LauncherControllerPatch.cs @@ -18,7 +18,7 @@ public static class LauncherController_Patch private const int PADDING = 10; private static GameObject goHost; - private static LauncherController lcInstance; + //private static LauncherController lcInstance; From b924d1aaee2f99793577f88706c964c728aac7c2 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 11 Aug 2024 19:26:34 +1000 Subject: [PATCH 068/188] Refactor and attempt reactivation of locomotive despawning --- .../Jobs/StationJobGenerationRangePatch.cs | 74 ++------------ .../Patches/Train/CarVisitCheckerPatch.cs | 38 +++++++- .../Train/UnusedTrainCarDeleterPatch.cs | 97 +++++++++++++++++++ Multiplayer/Utils/DvExtensions.cs | 35 +++++++ 4 files changed, 178 insertions(+), 66 deletions(-) create mode 100644 Multiplayer/Patches/Train/UnusedTrainCarDeleterPatch.cs diff --git a/Multiplayer/Patches/Jobs/StationJobGenerationRangePatch.cs b/Multiplayer/Patches/Jobs/StationJobGenerationRangePatch.cs index 343979a..86c625d 100644 --- a/Multiplayer/Patches/Jobs/StationJobGenerationRangePatch.cs +++ b/Multiplayer/Patches/Jobs/StationJobGenerationRangePatch.cs @@ -1,6 +1,7 @@ using HarmonyLib; using Multiplayer.Components.Networking; using Multiplayer.Networking.Data; +using Multiplayer.Utils; using UnityEngine; namespace Multiplayer.Patches.Jobs; @@ -8,55 +9,27 @@ namespace Multiplayer.Patches.Jobs; [HarmonyPatch(typeof(StationJobGenerationRange), nameof(StationJobGenerationRange.PlayerSqrDistanceFromStationCenter), MethodType.Getter)] public static class StationJobGenerationRange_PlayerSqrDistanceFromStationCenter_Patch { - private static int frameCount = 0; private static bool Prefix(StationJobGenerationRange __instance, ref float __result) { if (!NetworkLifecycle.Instance.IsHost()) return true; Vector3 anchor = __instance.stationCenterAnchor.position; - Vector3 anchor2 = anchor - WorldMover.currentMove; + __result = anchor.AnyPlayerSqrMag(); + + /* __result = float.MaxValue; //Loop through all of the players and return the one thats closest to the anchor foreach (ServerPlayer serverPlayer in NetworkLifecycle.Instance.Server.ServerPlayers) { float sqDist = (serverPlayer.WorldPosition - anchor).sqrMagnitude; - //float sqDist2 = (serverPlayer.AbsoluteWorldPosition - anchor2).sqrMagnitude; - float sqDist3 = (PlayerManager.PlayerTransform.position - __instance.stationCenterAnchor.position).sqrMagnitude; if (sqDist < __result) __result = sqDist; - - if (/*frameCount == 60 &&*/ Multiplayer.specLog && __instance.name == "StationFRS") - { - Multiplayer.LogDebug(() => $"PlayerSqrDistanceFromStationCenter:\r\n\t" + - $"player: '{serverPlayer.Username}',\r\n\t\t" + - //$"absPos: {serverPlayer.AbsoluteWorldPosition.ToString()},\r\n\t\t" + - $"rawPos: {serverPlayer.RawPosition.ToString()},\r\n\t\t" + - $"worldPos: {serverPlayer.WorldPosition.ToString()},\r\n\t" + - $"station name: '{__instance.name}',\r\n\t\t" + - $"anchor: {anchor.ToString()},\r\n\t\t" + - $"anchor2: {anchor - WorldMover.currentMove},\r\n\t\t" + - $"anchorTransform: {__instance.transform.TransformPoint(anchor)},\r\n\t\t" + - $"anchorTransform2: {__instance.transform.TransformPoint(anchor) - WorldMover.currentMove},\r\n\t\t" + - $"anchorInverseTransform: {__instance.transform.InverseTransformPoint(anchor)},\r\n\t\t" + - $"anchorInverseTransform2: {__instance.transform.InverseTransformPoint(anchor) - WorldMover.currentMove},\r\n\t" + - $"sqDist: {sqDist},\r\n\t" + - //$"sqDist2: {sqDist2},\r\n\t" + - $"sqDist3: {sqDist3},\r\n\t" + - $"sqDistTransform: {(serverPlayer.WorldPosition - __instance.transform.TransformPoint(anchor)).sqrMagnitude},\r\n\t" + - $"sqDistInverseTransform: {(serverPlayer.WorldPosition - __instance.transform.InverseTransformPoint(anchor)).sqrMagnitude}"); - } - } - - frameCount++; - if (frameCount > 60) - { - frameCount = 0; - } + */ return false; } @@ -65,54 +38,27 @@ private static bool Prefix(StationJobGenerationRange __instance, ref float __res [HarmonyPatch(typeof(StationJobGenerationRange), nameof(StationJobGenerationRange.PlayerSqrDistanceFromStationOffice), MethodType.Getter)] public static class StationJobGenerationRange_PlayerSqrDistanceFromStationOffice_Patch { - private static int frameCount = 0; private static bool Prefix(StationJobGenerationRange __instance, ref float __result) { if (!NetworkLifecycle.Instance.IsHost()) return true; Vector3 anchor = __instance.transform.position; - Vector3 anchor2 = anchor - WorldMover.currentMove; + __result = anchor.AnyPlayerSqrMag(); + + /* __result = float.MaxValue; + //Loop through all of the players and return the one thats closest to the anchor foreach (ServerPlayer serverPlayer in NetworkLifecycle.Instance.Server.ServerPlayers) { float sqDist = (serverPlayer.WorldPosition - anchor).sqrMagnitude; - //float sqDist2 = (serverPlayer.AbsoluteWorldPosition - anchor2).sqrMagnitude; - float sqDist3 = (PlayerManager.PlayerTransform.position - __instance.stationCenterAnchor.position).sqrMagnitude; if (sqDist < __result) __result = sqDist; - - if (/*frameCount == 60 &&*/ Multiplayer.specLog && __instance.name == "StationFRS") - { - Multiplayer.LogDebug(() => $"PlayerSqrDistanceFromStationOffice:\r\n\t" + - $"player: '{serverPlayer.Username}',\r\n\t\t" + - //$"absPos: {serverPlayer.AbsoluteWorldPosition.ToString()},\r\n\t\t" + - $"rawPos: {serverPlayer.RawPosition.ToString()},\r\n\t\t" + - $"worldPos: {serverPlayer.WorldPosition.ToString()},\r\n\t" + - $"station name: '{__instance.name}',\r\n\t\t" + - $"anchor: {anchor.ToString()},\r\n\t\t" + - $"anchor2: {anchor - WorldMover.currentMove},\r\n\t\t" + - $"anchorTransform: {__instance.transform.TransformPoint(anchor)},\r\n\t\t" + - $"anchorTransform2: {__instance.transform.TransformPoint(anchor) - WorldMover.currentMove},\r\n\t\t" + - $"anchorInverseTransform: {__instance.transform.InverseTransformPoint(anchor)},\r\n\t\t" + - $"anchorInverseTransform2: {__instance.transform.InverseTransformPoint(anchor) - WorldMover.currentMove},\r\n\t" + - $"sqDist: {sqDist},\r\n\t" + - //$"sqDist2: {sqDist2},\r\n\t" + - $"sqDist3: {sqDist3},\r\n\t" + - $"sqDistTransform: {(serverPlayer.WorldPosition - __instance.transform.TransformPoint(anchor)).sqrMagnitude},\r\n\t" + - $"sqDistInverseTransform: {(serverPlayer.WorldPosition - __instance.transform.InverseTransformPoint(anchor)).sqrMagnitude}"); - } - } - - frameCount++; - if (frameCount > 60) - { - frameCount = 0; - } + */ return false; } } diff --git a/Multiplayer/Patches/Train/CarVisitCheckerPatch.cs b/Multiplayer/Patches/Train/CarVisitCheckerPatch.cs index 7b9b49d..18c02bf 100644 --- a/Multiplayer/Patches/Train/CarVisitCheckerPatch.cs +++ b/Multiplayer/Patches/Train/CarVisitCheckerPatch.cs @@ -1,6 +1,8 @@ using DV; using HarmonyLib; using Multiplayer.Components.Networking; +using Multiplayer.Components.Networking.Train; +using Multiplayer.Networking.Data; namespace Multiplayer.Patches.World; @@ -9,10 +11,42 @@ public static class CarVisitCheckerPatch { [HarmonyPrefix] [HarmonyPatch(nameof(CarVisitChecker.IsRecentlyVisited), MethodType.Getter)] - private static bool IsRecentlyVisited_Prefix(ref bool __result) + private static bool IsRecentlyVisited_Prefix(CarVisitChecker __instance, ref bool __result) { if (NetworkLifecycle.Instance.IsHost() && NetworkLifecycle.Instance.Server.PlayerCount == 1) - return true; + return true; //playing in "vanilla mode" allow game code to run + + if (!NetworkLifecycle.Instance.IsHost()) + { + //if not the host, we want to keep the car from despawning + __instance.playerIsInCar = true; + __result = true; //Pretend there's a player in the car + return false; //don't run our vanilla game code + } + + //We are the host, check all players against this car + foreach (ServerPlayer player in NetworkLifecycle.Instance.Server.ServerPlayers) + { + if (NetworkedTrainCar.TryGetFromTrainCar(__instance.car, out NetworkedTrainCar netTC)) + { + if (player.CarId == netTC.NetId) + { + __instance.playerIsInCar = true; + __result = true; + return false; + } + } + else + { + //Car was not found, allow it to despawn + __instance.playerIsInCar = false; + __result = false; + return false; + } + } + + //no server players (this should only apply to a dedicated server), don't despawn + __instance.playerIsInCar = true; __result = true; return false; } diff --git a/Multiplayer/Patches/Train/UnusedTrainCarDeleterPatch.cs b/Multiplayer/Patches/Train/UnusedTrainCarDeleterPatch.cs new file mode 100644 index 0000000..8e2d6d7 --- /dev/null +++ b/Multiplayer/Patches/Train/UnusedTrainCarDeleterPatch.cs @@ -0,0 +1,97 @@ +using HarmonyLib; +using System; +using System.Collections.Generic; +using Multiplayer.Utils; +using System.Reflection.Emit; +using UnityEngine; +using static HarmonyLib.Code; +using Multiplayer.Networking.Data; +using DV.ThingTypes; +using DV.Logic.Job; +using DV.Utils; + + +namespace Multiplayer.Patches.Train; + +[HarmonyPatch(typeof(UnusedTrainCarDeleter))] +public static class UnusedTrainCarDeleterPatch +{ + private const int TARGET_LDARG_1 = 4; + private const int TARGET_SKIPS = 5; + public static TrainCar current; + + [HarmonyPatch("AreDeleteConditionsFulfilled")] + public static IEnumerable Transpiler(IEnumerable instructions) + { + int ldarg_1_Counter = 0; + int skipCtr = 0; + bool foundEntry = false; + bool complete = false; + + foreach (CodeInstruction instruction in instructions) + { + Multiplayer.LogDebug(() => $"Transpiling: {instruction.ToString()} - ldarg_1_Counter: {ldarg_1_Counter}, found: {foundEntry}, complete: {complete}, skip: {skipCtr}, len: {instruction.opcode.Size} + {instruction.operand}"); + if (instruction.opcode == OpCodes.Ldarg_1 && !foundEntry) + { + ldarg_1_Counter++; + foundEntry = ldarg_1_Counter == TARGET_LDARG_1; + } + else if (foundEntry && !complete) + { + if(instruction.opcode == OpCodes.Callvirt) + { + //allow IL_0083: callvirt and IL_0088: callvirt + yield return instruction; + continue; + } + + if (instruction.opcode == OpCodes.Call) + { + complete = true; + yield return CodeInstruction.Call(typeof(DvExtensions), "AnyPlayerSqrMag", [typeof(Vector3)], null); //inject our method + continue; + } + }else if (complete && skipCtr < TARGET_SKIPS) + { + //skip IL_0092: callvirt + //skip IL_0097: call + //skip IL_009C: stloc.s + //skip IL_009E: ldloca.s + //skip IL_00A0: call + + skipCtr++; + yield return new CodeInstruction(OpCodes.Nop); + continue; + } + + yield return instruction; + } + } + + [HarmonyPatch("AreDeleteConditionsFulfilled")] + [HarmonyPrefix] + public static void Prefix(UnusedTrainCarDeleter __instance, TrainCar trainCar) + { + string job=""; + + if (trainCar.IsLoco) + { + foreach (TrainCar car in trainCar.trainset.cars) + { + job += $"{car.ID} {SingletonBehaviour.Instance.GetJobOfCar(car, onlyActiveJobs: true)?.ID}, " ; + } + } + + Multiplayer.LogDebug(() => $"AreDeleteConditionsFulfilled_Prefix({trainCar?.ID}) Visit Checker: {trainCar?.visitChecker?.IsRecentlyVisited}, Livery: {CarTypes.IsAnyLocomotiveOrTender(trainCar?.carLivery)}, Player Spawned: {trainCar?.playerSpawnedCar} jobs: {job}"); + + current = trainCar; + } + + [HarmonyPatch("AreDeleteConditionsFulfilled")] + [HarmonyPostfix] + public static void Postfix(UnusedTrainCarDeleter __instance, TrainCar trainCar, bool __result) + { + Multiplayer.LogDebug(() => $"AreDeleteConditionsFulfilled_Postfix({trainCar?.ID}) = {__result}"); + } + +} diff --git a/Multiplayer/Utils/DvExtensions.cs b/Multiplayer/Utils/DvExtensions.cs index 1254424..d52216f 100644 --- a/Multiplayer/Utils/DvExtensions.cs +++ b/Multiplayer/Utils/DvExtensions.cs @@ -8,6 +8,10 @@ using UnityEngine.UI; using System.Linq; using System.Diagnostics; +using Multiplayer.Components.Networking; +using Multiplayer.Networking.Data; +using static Oculus.Avatar.CAPI; +using Multiplayer.Patches.Train; @@ -110,4 +114,35 @@ public static void ResetTooltip(this GameObject button) #endregion + #region Utils + + public static float AnyPlayerSqrMag(this GameObject item) + { + return AnyPlayerSqrMag(item.transform.position); + } + + public static float AnyPlayerSqrMag(this Vector3 anchor) + { + float result = float.MaxValue; + string origin = new StackTrace().GetFrame(1).GetMethod().Name; + + + + //Loop through all of the players and return the one thats closest to the anchor + foreach (ServerPlayer serverPlayer in NetworkLifecycle.Instance.Server.ServerPlayers) + { + float sqDist = (serverPlayer.WorldPosition - anchor).sqrMagnitude; + if(origin == "UnusedTrainCarDeleter.AreDeleteConditionsFulfilled_Patch0") + Multiplayer.LogDebug(() => $"AnyPlayerSqrMag(): car: {UnusedTrainCarDeleterPatch.current?.ID}, player: {serverPlayer.Username}, result: {sqDist}"); + + if (sqDist < result) + result = sqDist; + } + + if (origin == "UnusedTrainCarDeleter.AreDeleteConditionsFulfilled_Patch0") + Multiplayer.LogDebug(() => $"AnyPlayerSqrMag(): player: result: {result}"); + + return result; + } + #endregion } From 27f8cbf32c566bb7c4001534c7b24a1af7e86c43 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 11 Aug 2024 20:11:21 +1000 Subject: [PATCH 069/188] Continuing attempt to reactivate loco de-spawning Issue seems to be resolved, more testing required --- .../Patches/Train/CarVisitCheckerPatch.cs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/Multiplayer/Patches/Train/CarVisitCheckerPatch.cs b/Multiplayer/Patches/Train/CarVisitCheckerPatch.cs index 18c02bf..156ba56 100644 --- a/Multiplayer/Patches/Train/CarVisitCheckerPatch.cs +++ b/Multiplayer/Patches/Train/CarVisitCheckerPatch.cs @@ -23,6 +23,14 @@ private static bool IsRecentlyVisited_Prefix(CarVisitChecker __instance, ref boo __result = true; //Pretend there's a player in the car return false; //don't run our vanilla game code } + if (NetworkLifecycle.Instance.Server.ServerPlayers.Count == 0) + { + + //no server players (this should only apply to a dedicated server), don't despawn + __instance.playerIsInCar = true; + __result = true; + return false; + } //We are the host, check all players against this car foreach (ServerPlayer player in NetworkLifecycle.Instance.Server.ServerPlayers) @@ -45,12 +53,13 @@ private static bool IsRecentlyVisited_Prefix(CarVisitChecker __instance, ref boo } } - //no server players (this should only apply to a dedicated server), don't despawn - __instance.playerIsInCar = true; - __result = true; + //No one on the car + __instance.playerIsInCar = false; + __result = __instance.recentlyVisitedTimer.RemainingTime > 0f; return false; } + /* [HarmonyPrefix] [HarmonyPatch(nameof(CarVisitChecker.RecentlyVisitedRemainingTime), MethodType.Getter)] private static bool RecentlyVisitedRemainingTime_Prefix(ref float __result) @@ -60,4 +69,5 @@ private static bool RecentlyVisitedRemainingTime_Prefix(ref float __result) __result = CarVisitChecker.RECENTLY_VISITED_TIME_THRESHOLD; return false; } + */ } From 8d727fd3d4958d3b82944e826e47c8269e9ad7f1 Mon Sep 17 00:00:00 2001 From: AMacro Date: Fri, 16 Aug 2024 19:14:56 +1000 Subject: [PATCH 070/188] Revert "Implemented potential fix for error accumulation and de-sync" This reverts commit 22c2ea2e10e945b6e6a3bc541b1ff6c2d83c097a. --- .../Train/NetworkTrainsetWatcher.cs | 158 +++--------------- .../Networking/Train/NetworkedTrainCar.cs | 20 +-- .../Networking/Data/TrainsetMovementPart.cs | 50 ++---- 3 files changed, 38 insertions(+), 190 deletions(-) diff --git a/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs b/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs index da69b3a..ad025e3 100644 --- a/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs +++ b/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs @@ -11,9 +11,6 @@ public class NetworkTrainsetWatcher : SingletonBehaviour { private ClientboundTrainsetPhysicsPacket cachedSendPacket; - const float DESIRED_FULL_SYNC_INTERVAL = 2f; // in seconds - const int MAX_UNSYNC_TICKS = (int)(NetworkLifecycle.TICK_RATE * DESIRED_FULL_SYNC_INTERVAL); - protected override void Awake() { base.Awake(); @@ -46,47 +43,35 @@ private void Server_OnTick(uint tick) private void Server_TickSet(Trainset set) { - bool anyCarMoving = false; - bool maxTicksReached = false; - - if (set == null) + bool dirty = false; + foreach (TrainCar trainCar in set.cars) { - Multiplayer.LogError($"Server_TickSet(): Received null set!"); - return; + if (trainCar.isStationary) + continue; + dirty = true; + break; } + if (!dirty) + return; + cachedSendPacket.NetId = set.firstCar.GetNetId(); + //car may not be initialised, missing a valid NetID if (cachedSendPacket.NetId == 0) return; - foreach (TrainCar trainCar in set.cars) + if (set.cars.Contains(null)) { - if (trainCar == null || !trainCar.gameObject.activeSelf) - { - Multiplayer.LogError($"Trainset {set.id} ({set.firstCar?.GetNetId()} has a null or inactive ({trainCar?.gameObject.activeSelf}) car!"); - return; - } - - //If we can locate the networked car, we'll add to the ticks counter - if (NetworkedTrainCar.TryGetFromTrainCar(trainCar, out NetworkedTrainCar netTC)) - { - netTC.TicksSinceSync++; - maxTicksReached |= netTC.TicksSinceSync >= MAX_UNSYNC_TICKS; - } - - //Even if the car is stationary, if the max ticks has been exceeded we will still sync - if (!trainCar.isStationary) - anyCarMoving = true; - - //we can finish checking early if we have BOTH a dirty and a max ticks - if (anyCarMoving && maxTicksReached) - break; + Multiplayer.LogError($"Trainset {set.id} ({set.firstCar.GetNetId()} has a null car!"); + return; } - //if any car is dirty or exceeded its max ticks we will re-sync the entire train - if (!anyCarMoving && !maxTicksReached) + if (set.cars.Any(car => !car.gameObject.activeSelf)) + { + Multiplayer.LogError($"Trainset {set.id} ({set.firstCar.GetNetId()} has a non-active car!"); return; + } TrainsetMovementPart[] trainsetParts = new TrainsetMovementPart[set.cars.Count]; bool anyTracksDirty = false; @@ -95,35 +80,23 @@ private void Server_TickSet(Trainset set) TrainCar trainCar = set.cars[i]; if (!trainCar.TryNetworked(out NetworkedTrainCar networkedTrainCar)) { + Multiplayer.LogDebug(() => $"TrainCar UNKNOWN is not networked! Is active? {trainCar.gameObject.activeInHierarchy}"); Multiplayer.LogDebug(() => $"TrainCar {trainCar.ID} is not networked! Is active? {trainCar.gameObject.activeInHierarchy}"); continue; } + //NetworkedTrainCar networkedTrainCar = trainCar.Networked(); anyTracksDirty |= networkedTrainCar.BogieTracksDirty; if (trainCar.derailed) - { trainsetParts[i] = new TrainsetMovementPart(RigidbodySnapshot.From(trainCar.rb)); - } else - { - RigidbodySnapshot? snapshot = null; - - //Have we exceeded the max ticks? - if (maxTicksReached) - { - snapshot = RigidbodySnapshot.From(trainCar.rb); - networkedTrainCar.TicksSinceSync = 0; //reset this car's tick count - } - trainsetParts[i] = new TrainsetMovementPart( trainCar.GetForwardSpeed(), trainCar.stress.slowBuildUpStress, BogieData.FromBogie(trainCar.Bogies[0], networkedTrainCar.BogieTracksDirty, networkedTrainCar.Bogie1TrackDirection), - BogieData.FromBogie(trainCar.Bogies[1], networkedTrainCar.BogieTracksDirty, networkedTrainCar.Bogie2TrackDirection), - snapshot + BogieData.FromBogie(trainCar.Bogies[1], networkedTrainCar.BogieTracksDirty, networkedTrainCar.Bogie2TrackDirection) ); - } } cachedSendPacket.TrainsetParts = trainsetParts; @@ -165,94 +138,3 @@ public void Client_HandleTrainsetPhysicsUpdate(ClientboundTrainsetPhysicsPacket return $"[{nameof(NetworkTrainsetWatcher)}]"; } } - -/* Backup - * private void Server_TickSet(Trainset set) - { - bool dirty = false; - bool maxTicks = false; - - foreach (TrainCar trainCar in set.cars) - { - //If we can locate the networked car, we'll add to the ticks counter - if(NetworkedTrainCar.TryGetFromTrainCar(trainCar, out NetworkedTrainCar netTC)) - netTC.TicksSinceSync++; - - //if we've exceeded the max ticks since a full sync - if(netTC != null && netTC.TicksSinceSync >= MAX_UNSYNC_TICKS) - maxTicks = true; - - //Even if the car is stationary, if the max ticks has been exceeded we will still sync - if (trainCar.isStationary) - continue; - - dirty = true; - break; - } - - //if any car is dirty or exceeded its max ticks we will re-sync the entire train - if (!dirty && !maxTicks) - return; - - cachedSendPacket.NetId = set.firstCar.GetNetId(); - - //car may not be initialised, missing a valid NetID - if (cachedSendPacket.NetId == 0) - return; - - if (set.cars.Contains(null)) - { - Multiplayer.LogError($"Trainset {set.id} ({set.firstCar.GetNetId()} has a null car!"); - return; - } - - if (set.cars.Any(car => !car.gameObject.activeSelf)) - { - Multiplayer.LogError($"Trainset {set.id} ({set.firstCar.GetNetId()} has a non-active car!"); - return; - } - - TrainsetMovementPart[] trainsetParts = new TrainsetMovementPart[set.cars.Count]; - bool anyTracksDirty = false; - for (int i = 0; i < set.cars.Count; i++) - { - TrainCar trainCar = set.cars[i]; - if (!trainCar.TryNetworked(out NetworkedTrainCar networkedTrainCar)) - { - Multiplayer.LogDebug(() => $"TrainCar {trainCar?.ID ?? "UNKOWN"} is not networked! Is active? {trainCar.gameObject.activeInHierarchy}"); - continue; - } - - anyTracksDirty |= networkedTrainCar.BogieTracksDirty; - - if (trainCar.derailed) - { - trainsetParts[i] = new TrainsetMovementPart(RigidbodySnapshot.From(trainCar.rb)); - } - else - { - RigidbodySnapshot? snapshot = null; - - //Have we exceeded the max ticks? - if (maxTicks) - { - snapshot = RigidbodySnapshot.From(trainCar.rb); - networkedTrainCar.TicksSinceSync = 0; //reset this car's tick count - } - - trainsetParts[i] = new TrainsetMovementPart( - trainCar.GetForwardSpeed(), - trainCar.stress.slowBuildUpStress, - BogieData.FromBogie(trainCar.Bogies[0], networkedTrainCar.BogieTracksDirty, networkedTrainCar.Bogie1TrackDirection), - BogieData.FromBogie(trainCar.Bogies[1], networkedTrainCar.BogieTracksDirty, networkedTrainCar.Bogie2TrackDirection), - snapshot - ); - - - } - } - - cachedSendPacket.TrainsetParts = trainsetParts; - NetworkLifecycle.Instance.Server.SendTrainsetPhysicsUpdate(cachedSendPacket, anyTracksDirty); - } -*/ diff --git a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs index 4924147..bfcb44a 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs @@ -98,7 +98,6 @@ public static bool TryGetFromTrainCar(TrainCar trainCar, out NetworkedTrainCar n public TickedQueue Client_trainRigidbodyQueue; private TickedQueue client_bogie1Queue; private TickedQueue client_bogie2Queue; - public uint TicksSinceSync = uint.MaxValue; #endregion @@ -608,25 +607,16 @@ public void Client_ReceiveTrainPhysicsUpdate(in TrainsetMovementPart movementPar { if (!client_Initialized) return; - if (TrainCar.isEligibleForSleep) TrainCar.ForceOptimizationState(false); - - // Handle RigidBody snapshot (common for both derailed and normal states) - if (movementPart.MovementType.HasFlag(TrainsetMovementPart.TrainsetMovementType.RigidBody)) - Client_trainRigidbodyQueue.ReceiveSnapshot(movementPart.RigidbodySnapshot, tick); - - // Handle derailment (only rigid body) - if (movementPart.MovementType == TrainsetMovementPart.TrainsetMovementType.RigidBody) + if (movementPart.IsRigidbodySnapshot) { TrainCar.Derail(); TrainCar.stress.ResetTrainStress(); - return; + Client_trainRigidbodyQueue.ReceiveSnapshot(movementPart.RigidbodySnapshot, tick); } - - // Handle normal movement (bogie data and optional rigid body data) - if (movementPart.MovementType.HasFlag(TrainsetMovementPart.TrainsetMovementType.Bogie)) + else { Client_trainSpeedQueue.ReceiveSnapshot(movementPart.Speed, tick); TrainCar.stress.slowBuildUpStress = movementPart.SlowBuildUpStress; @@ -658,7 +648,7 @@ private void Client_OnAddCoal(float coalMassDelta) if (coalMassDelta <= 0) return; - //NetworkLifecycle.Instance.Client.LogDebug(() => $"Common_OnAddCoal({TrainCar.ID}): coalMassDelta: {coalMassDelta}"); + NetworkLifecycle.Instance.Client.LogDebug(() => $"Common_OnAddCoal({TrainCar.ID}): coalMassDelta: {coalMassDelta}"); NetworkLifecycle.Instance.Client.SendAddCoal(NetId, coalMassDelta); } @@ -670,7 +660,7 @@ private void Client_OnIgnite(float ignition) if (ignition == 0f) return; - //NetworkLifecycle.Instance.Client.LogDebug(() => $"Common_OnIgnite({TrainCar.ID})"); + NetworkLifecycle.Instance.Client.LogDebug(() => $"Common_OnIgnite({TrainCar.ID})"); NetworkLifecycle.Instance.Client.SendFireboxIgnition(NetId); } diff --git a/Multiplayer/Networking/Data/TrainsetMovementPart.cs b/Multiplayer/Networking/Data/TrainsetMovementPart.cs index 145847a..62fade0 100644 --- a/Multiplayer/Networking/Data/TrainsetMovementPart.cs +++ b/Multiplayer/Networking/Data/TrainsetMovementPart.cs @@ -1,38 +1,28 @@ using LiteNetLib.Utils; -using System; namespace Multiplayer.Networking.Data; public readonly struct TrainsetMovementPart { - public readonly TrainsetMovementType MovementType; + public readonly bool IsRigidbodySnapshot; public readonly float Speed; public readonly float SlowBuildUpStress; public readonly BogieData Bogie1; public readonly BogieData Bogie2; public readonly RigidbodySnapshot RigidbodySnapshot; - [Flags] - public enum TrainsetMovementType : byte + public TrainsetMovementPart(float speed, float slowBuildUpStress, BogieData bogie1, BogieData bogie2) { - RigidBody = 1, - Bogie = 2, - All = byte.MaxValue - } - - public TrainsetMovementPart(float speed, float slowBuildUpStress, BogieData bogie1, BogieData bogie2, RigidbodySnapshot rigidbodySnapshot = null) - { - MovementType = rigidbodySnapshot != null ? TrainsetMovementType.All : TrainsetMovementType.Bogie; + IsRigidbodySnapshot = false; Speed = speed; SlowBuildUpStress = slowBuildUpStress; Bogie1 = bogie1; Bogie2 = bogie2; - RigidbodySnapshot = rigidbodySnapshot; } public TrainsetMovementPart(RigidbodySnapshot rigidbodySnapshot) { - MovementType = TrainsetMovementType.RigidBody; + IsRigidbodySnapshot = true; RigidbodySnapshot = rigidbodySnapshot; } @@ -40,13 +30,13 @@ public TrainsetMovementPart(RigidbodySnapshot rigidbodySnapshot) public static void Serialize(NetDataWriter writer, TrainsetMovementPart data) #pragma warning restore EPS05 { - writer.Put((byte)data.MovementType); + writer.Put(data.IsRigidbodySnapshot); - if (data.MovementType.HasFlag(TrainsetMovementType.RigidBody)) + if (data.IsRigidbodySnapshot) + { RigidbodySnapshot.Serialize(writer, data.RigidbodySnapshot); - - if (!data.MovementType.HasFlag(TrainsetMovementType.Bogie)) return; + } writer.Put(data.Speed); writer.Put(data.SlowBuildUpStress); @@ -56,28 +46,14 @@ public static void Serialize(NetDataWriter writer, TrainsetMovementPart data) public static TrainsetMovementPart Deserialize(NetDataReader reader) { - TrainsetMovementType movementType = (TrainsetMovementType)reader.GetByte(); - RigidbodySnapshot snapshot = null; - - if (movementType.HasFlag(TrainsetMovementType.RigidBody)) - snapshot = RigidbodySnapshot.Deserialize(reader); - - if (movementType.HasFlag(TrainsetMovementType.Bogie)) - { - float speed = reader.GetFloat(); - float slowBuildUpStress = reader.GetFloat(); - BogieData bogie1 = BogieData.Deserialize(reader); - BogieData bogie2 = BogieData.Deserialize(reader); - - return new TrainsetMovementPart( + bool isRigidbodySnapshot = reader.GetBool(); + return isRigidbodySnapshot + ? new TrainsetMovementPart(RigidbodySnapshot.Deserialize(reader)) + : new TrainsetMovementPart( reader.GetFloat(), reader.GetFloat(), BogieData.Deserialize(reader), - BogieData.Deserialize(reader), - snapshot + BogieData.Deserialize(reader) ); - } - - return new TrainsetMovementPart(snapshot); } } From ff4245e0d855054a1acebf7d27738b9ba59baa73 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 17 Aug 2024 14:05:56 +1000 Subject: [PATCH 071/188] Cleanup for 0.1.8.0 release --- .../Networking/Jobs/NetworkedJob.cs | 42 +++++++++---------- .../Networking/World/NetworkedRigidbody.cs | 19 ++++++++- .../Networking/Data/RigidbodySnapshot.cs | 3 ++ .../Managers/Client/NetworkClient.cs | 2 +- .../Managers/Server/NetworkServer.cs | 2 +- .../Train/UnusedTrainCarDeleterPatch.cs | 9 ++-- Multiplayer/Utils/DvExtensions.cs | 9 ++-- 7 files changed, 52 insertions(+), 34 deletions(-) diff --git a/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs b/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs index 3ea1e8e..9433e9c 100644 --- a/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs +++ b/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs @@ -88,13 +88,13 @@ protected override void Awake() Multiplayer.Log("NetworkJob.Awake()"); base.Awake(); - /* job = GetComponent(); - jobToNetworkedJob[job] = this; - - + jobToNetworkedJob[job] = this; + jobIdToNetworkedJob[job.ID] = this; + jobIdToJob[job.ID] = job; + if (NetworkLifecycle.Instance.IsHost()) { //do we need a job watcher - probably not, but maybe or maybe we need a task watcher @@ -114,11 +114,7 @@ protected override void Awake() private void Start() { //startup stuff - Multiplayer.Log("NetworkedJob.Start()"); - - jobToNetworkedJob[job] = this; - jobIdToNetworkedJob[job.ID] = this; - jobIdToJob[job.ID] = job; + Multiplayer.Log($"NetworkedJob.Start({job.ID})"); isJobNew = true; //Send new jobs on tick @@ -132,24 +128,25 @@ private void Start() if (!NetworkLifecycle.Instance.IsHost()) { //station.logicStation.AddJobToStation(job); + if (station.logicStation.availableJobs.Contains(job)) { Multiplayer.LogError("Trying to add the same job[" + job.ID + "] multiple times to station! Skipping, trying to recover."); return; } - station.logicStation.availableJobs.Add(job); - job.JobTaken += this.OnJobTaken; - job.JobExpired += this.OnJobExpired; + //station.logicStation.availableJobs.Add(job); + //job.JobTaken += this.OnJobTaken; + //job.JobExpired += this.OnJobExpired; //job.JobAddedToStation?.Invoke(); - SingletonBehaviour.Instance.StartCoroutine(NetworkedStation.UpdateCarPlates(job.tasks, job.ID)); + CoroutineManager.Instance.StartCoroutine(NetworkedStation.UpdateCarPlates(job.tasks, job.ID)); } else { //setup even handlers - job.JobTaken += this.OnJobTaken; - job.JobExpired += this.OnJobExpired; - NetworkLifecycle.Instance.OnTick += Server_OnTick; + //job.JobTaken += this.OnJobTaken; + //job.JobExpired += this.OnJobExpired; + //NetworkLifecycle.Instance.OnTick += Server_OnTick; } Multiplayer.Log("NetworkedJob.Start() Started"); @@ -161,17 +158,18 @@ private void OnDisable() if (UnloadWatcher.isQuitting) return; - NetworkLifecycle.Instance.OnTick -= Common_OnTick; - NetworkLifecycle.Instance.OnTick -= Server_OnTick; + //NetworkLifecycle.Instance.OnTick -= Common_OnTick; + //NetworkLifecycle.Instance.OnTick -= Server_OnTick; if (UnloadWatcher.isUnloading) return; - job.JobTaken -= this.OnJobTaken; - jobToNetworkedJob.Remove(job); - jobIdToNetworkedJob.Remove(job.ID); - jobIdToNetworkedJob.Remove(job.ID); + //job.JobTaken -= this.OnJobTaken; + + //jobToNetworkedJob.Remove(job); + //jobIdToNetworkedJob.Remove(job.ID); + //jobIdToNetworkedJob.Remove(job.ID); //Clean up any actions we added diff --git a/Multiplayer/Components/Networking/World/NetworkedRigidbody.cs b/Multiplayer/Components/Networking/World/NetworkedRigidbody.cs index 8e1c499..ff62745 100644 --- a/Multiplayer/Components/Networking/World/NetworkedRigidbody.cs +++ b/Multiplayer/Components/Networking/World/NetworkedRigidbody.cs @@ -1,4 +1,5 @@ -using Multiplayer.Networking.Data; +using Multiplayer.Networking.Data; +using System; using UnityEngine; namespace Multiplayer.Components.Networking.World; @@ -21,6 +22,20 @@ protected override void OnEnable() protected override void Process(RigidbodySnapshot snapshot, uint snapshotTick) { - snapshot.Apply(rigidbody); + if (snapshot == null) + { + Multiplayer.LogError($"NetworkedRigidBody.Process() Snapshot NULL!"); + return; + } + + try + { + Multiplayer.LogDebug(()=>$"NetworkedRigidBody.Process() {snapshot.IncludedDataFlags}, {snapshot.Position.ToString() ?? "null"}, {snapshot.Rotation.ToString() ?? "null"}, {snapshot.Velocity.ToString() ?? "null"}, {snapshot.AngularVelocity.ToString() ?? "null"}"); + snapshot.Apply(rigidbody); + } + catch (Exception ex) + { + Multiplayer.LogError($"NetworkedRigidBody.Process() {ex.Message}\r\n {ex.StackTrace}"); + } } } diff --git a/Multiplayer/Networking/Data/RigidbodySnapshot.cs b/Multiplayer/Networking/Data/RigidbodySnapshot.cs index 06651f3..445207a 100644 --- a/Multiplayer/Networking/Data/RigidbodySnapshot.cs +++ b/Multiplayer/Networking/Data/RigidbodySnapshot.cs @@ -77,6 +77,9 @@ public static RigidbodySnapshot From(Rigidbody rb, IncludedData includedDataFlag public void Apply(Rigidbody rb) { + if (rb == null) + return; + IncludedData flags = (IncludedData)IncludedDataFlags; if (flags.HasFlag(IncludedData.Position)) diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index ae23bfa..38db294 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -590,7 +590,7 @@ private void OnClientboundFireboxStatePacket(ClientboundFireboxStatePacket packe networkedTrainCar.Client_ReceiveFireboxStateUpdate(packet.Contents, packet.IsOn); - Multiplayer.LogDebug(() => $"Received Brake Pressures netId {packet.NetId}: {packet.Contents}, {packet.IsOn}"); + //Multiplayer.LogDebug(() => $"Received Brake Pressures netId {packet.NetId}: {packet.Contents}, {packet.IsOn}"); } private void OnClientboundCargoStatePacket(ClientboundCargoStatePacket packet) diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index 91ea230..2ea4ef2 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -798,7 +798,7 @@ private void OnCommonTrainPortsPacket(CommonTrainPortsPacket packet, NetPeer pee { bool flag = networkedTrainCar.Server_ValidateClientSimFlowPacket(player, packet); - LogDebug(() => $"OnCommonTrainPortsPacket from {player.Username}, Not host, valid: {flag}"); + //LogDebug(() => $"OnCommonTrainPortsPacket from {player.Username}, Not host, valid: {flag}"); if (!flag) { return; diff --git a/Multiplayer/Patches/Train/UnusedTrainCarDeleterPatch.cs b/Multiplayer/Patches/Train/UnusedTrainCarDeleterPatch.cs index 8e2d6d7..e8d476c 100644 --- a/Multiplayer/Patches/Train/UnusedTrainCarDeleterPatch.cs +++ b/Multiplayer/Patches/Train/UnusedTrainCarDeleterPatch.cs @@ -30,7 +30,7 @@ public static IEnumerable Transpiler(IEnumerable $"Transpiling: {instruction.ToString()} - ldarg_1_Counter: {ldarg_1_Counter}, found: {foundEntry}, complete: {complete}, skip: {skipCtr}, len: {instruction.opcode.Size} + {instruction.operand}"); + //Multiplayer.LogDebug(() => $"Transpiling: {instruction.ToString()} - ldarg_1_Counter: {ldarg_1_Counter}, found: {foundEntry}, complete: {complete}, skip: {skipCtr}, len: {instruction.opcode.Size} + {instruction.operand}"); if (instruction.opcode == OpCodes.Ldarg_1 && !foundEntry) { ldarg_1_Counter++; @@ -68,6 +68,7 @@ public static IEnumerable Transpiler(IEnumerable $"AreDeleteConditionsFulfilled_Prefix({trainCar?.ID}) Visit Checker: {trainCar?.visitChecker?.IsRecentlyVisited}, Livery: {CarTypes.IsAnyLocomotiveOrTender(trainCar?.carLivery)}, Player Spawned: {trainCar?.playerSpawnedCar} jobs: {job}"); + //Multiplayer.LogDebug(() => $"AreDeleteConditionsFulfilled_Prefix({trainCar?.ID}) Visit Checker: {trainCar?.visitChecker?.IsRecentlyVisited}, Livery: {CarTypes.IsAnyLocomotiveOrTender(trainCar?.carLivery)}, Player Spawned: {trainCar?.playerSpawnedCar} jobs: {job}"); current = trainCar; } + [HarmonyPatch("AreDeleteConditionsFulfilled")] [HarmonyPostfix] public static void Postfix(UnusedTrainCarDeleter __instance, TrainCar trainCar, bool __result) { - Multiplayer.LogDebug(() => $"AreDeleteConditionsFulfilled_Postfix({trainCar?.ID}) = {__result}"); + //Multiplayer.LogDebug(() => $"AreDeleteConditionsFulfilled_Postfix({trainCar?.ID}) = {__result}"); } + */ } diff --git a/Multiplayer/Utils/DvExtensions.cs b/Multiplayer/Utils/DvExtensions.cs index d52216f..04b7e28 100644 --- a/Multiplayer/Utils/DvExtensions.cs +++ b/Multiplayer/Utils/DvExtensions.cs @@ -6,12 +6,9 @@ using Multiplayer.Components.Networking.World; using UnityEngine; using UnityEngine.UI; -using System.Linq; using System.Diagnostics; using Multiplayer.Components.Networking; using Multiplayer.Networking.Data; -using static Oculus.Avatar.CAPI; -using Multiplayer.Patches.Train; @@ -132,16 +129,18 @@ public static float AnyPlayerSqrMag(this Vector3 anchor) foreach (ServerPlayer serverPlayer in NetworkLifecycle.Instance.Server.ServerPlayers) { float sqDist = (serverPlayer.WorldPosition - anchor).sqrMagnitude; + /* if(origin == "UnusedTrainCarDeleter.AreDeleteConditionsFulfilled_Patch0") Multiplayer.LogDebug(() => $"AnyPlayerSqrMag(): car: {UnusedTrainCarDeleterPatch.current?.ID}, player: {serverPlayer.Username}, result: {sqDist}"); - + */ if (sqDist < result) result = sqDist; } + /* if (origin == "UnusedTrainCarDeleter.AreDeleteConditionsFulfilled_Patch0") Multiplayer.LogDebug(() => $"AnyPlayerSqrMag(): player: result: {result}"); - + */ return result; } #endregion From 5b85cf997fdb44f1ff079f2e4f37b53a76075b93 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 18 Aug 2024 16:46:41 +1000 Subject: [PATCH 072/188] implemented potential fix for error accumulation and de-sync Added periodic position and rotation sync --- .../Components/Networking/TickedQueue.cs | 5 ++ .../Train/NetworkTrainsetWatcher.cs | 84 +++++++++++++------ .../Networking/Train/NetworkedCarSpawner.cs | 2 +- .../Networking/Train/NetworkedTrainCar.cs | 37 +++++++- Multiplayer/Multiplayer.csproj | 2 +- .../Networking/Data/TrainsetMovementPart.cs | 83 ++++++++++++++---- .../Networking/Data/TrainsetSpawnPart.cs | 10 +-- info.json | 2 +- 8 files changed, 174 insertions(+), 51 deletions(-) diff --git a/Multiplayer/Components/Networking/TickedQueue.cs b/Multiplayer/Components/Networking/TickedQueue.cs index c2127a1..30aad3a 100644 --- a/Multiplayer/Components/Networking/TickedQueue.cs +++ b/Multiplayer/Components/Networking/TickedQueue.cs @@ -41,5 +41,10 @@ private void OnTick(uint tick) } } + public void Clear() + { + snapshots.Clear(); + } + protected abstract void Process(T snapshot, uint snapshotTick); } diff --git a/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs b/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs index ad025e3..b3705fd 100644 --- a/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs +++ b/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs @@ -1,5 +1,7 @@ +using System.Collections.Generic; using System.Linq; using DV.Utils; +using UnityEngine; using JetBrains.Annotations; using Multiplayer.Networking.Data; using Multiplayer.Networking.Packets.Clientbound.Train; @@ -11,6 +13,9 @@ public class NetworkTrainsetWatcher : SingletonBehaviour { private ClientboundTrainsetPhysicsPacket cachedSendPacket; + const float DESIRED_FULL_SYNC_INTERVAL = 2f; // in seconds + const int MAX_UNSYNC_TICKS = (int)(NetworkLifecycle.TICK_RATE * DESIRED_FULL_SYNC_INTERVAL); + protected override void Awake() { base.Awake(); @@ -43,62 +48,91 @@ private void Server_OnTick(uint tick) private void Server_TickSet(Trainset set) { - bool dirty = false; - foreach (TrainCar trainCar in set.cars) - { - if (trainCar.isStationary) - continue; - dirty = true; - break; - } + bool anyCarMoving = false; + bool maxTicksReached = false; + bool anyTracksDirty = false; - if (!dirty) + if (set == null) + { + Multiplayer.LogError($"Server_TickSet(): Received null set!"); return; + } cachedSendPacket.NetId = set.firstCar.GetNetId(); - //car may not be initialised, missing a valid NetID if (cachedSendPacket.NetId == 0) return; - if (set.cars.Contains(null)) + foreach (TrainCar trainCar in set.cars) { - Multiplayer.LogError($"Trainset {set.id} ({set.firstCar.GetNetId()} has a null car!"); - return; + if (trainCar == null || !trainCar.gameObject.activeSelf) + { + Multiplayer.LogError($"Trainset {set.id} ({set.firstCar?.GetNetId()} has a null or inactive ({trainCar?.gameObject.activeSelf}) car!"); + return; + } + + //If we can locate the networked car, we'll add to the ticks counter and check if any tracks are dirty + if (NetworkedTrainCar.TryGetFromTrainCar(trainCar, out NetworkedTrainCar netTC)) + { + maxTicksReached |= netTC.TicksSinceSync >= MAX_UNSYNC_TICKS; + anyTracksDirty |= netTC.BogieTracksDirty; + } + + //Even if the car is stationary, if the max ticks has been exceeded we will still sync + if (!trainCar.isStationary) + anyCarMoving = true; + + //we can finish checking early if we have BOTH a dirty and a max ticks + if (anyCarMoving && maxTicksReached) + break; } - if (set.cars.Any(car => !car.gameObject.activeSelf)) - { - Multiplayer.LogError($"Trainset {set.id} ({set.firstCar.GetNetId()} has a non-active car!"); + //if any car is dirty or exceeded its max ticks we will re-sync the entire train + if (!anyCarMoving && !maxTicksReached) return; - } TrainsetMovementPart[] trainsetParts = new TrainsetMovementPart[set.cars.Count]; - bool anyTracksDirty = false; + for (int i = 0; i < set.cars.Count; i++) { TrainCar trainCar = set.cars[i]; if (!trainCar.TryNetworked(out NetworkedTrainCar networkedTrainCar)) { - Multiplayer.LogDebug(() => $"TrainCar UNKNOWN is not networked! Is active? {trainCar.gameObject.activeInHierarchy}"); Multiplayer.LogDebug(() => $"TrainCar {trainCar.ID} is not networked! Is active? {trainCar.gameObject.activeInHierarchy}"); continue; } - //NetworkedTrainCar networkedTrainCar = trainCar.Networked(); - anyTracksDirty |= networkedTrainCar.BogieTracksDirty; - if (trainCar.derailed) + { trainsetParts[i] = new TrainsetMovementPart(RigidbodySnapshot.From(trainCar.rb)); + } else + { + Vector3? position = null; + Quaternion? rotation = null; + + //Have we exceeded the max ticks? + if (maxTicksReached) + { + //Multiplayer.Log($"Max Ticks Reached for TrainSet with cars {set.firstCar.ID}, {set.lastCar.ID}"); + + position = trainCar.transform.position - WorldMover.currentMove; + rotation = trainCar.transform.rotation; + networkedTrainCar.TicksSinceSync = 0; //reset this car's tick count + } + trainsetParts[i] = new TrainsetMovementPart( trainCar.GetForwardSpeed(), trainCar.stress.slowBuildUpStress, BogieData.FromBogie(trainCar.Bogies[0], networkedTrainCar.BogieTracksDirty, networkedTrainCar.Bogie1TrackDirection), - BogieData.FromBogie(trainCar.Bogies[1], networkedTrainCar.BogieTracksDirty, networkedTrainCar.Bogie2TrackDirection) + BogieData.FromBogie(trainCar.Bogies[1], networkedTrainCar.BogieTracksDirty, networkedTrainCar.Bogie2TrackDirection), + position, //only used in full sync + rotation //only used in full sync ); + } } + //Multiplayer.Log($"Server_TickSet({set.firstCar.ID}): SendTrainsetPhysicsUpdate, tick: {cachedSendPacket.Tick}"); cachedSendPacket.TrainsetParts = trainsetParts; NetworkLifecycle.Instance.Server.SendTrainsetPhysicsUpdate(cachedSendPacket, anyTracksDirty); } @@ -123,13 +157,15 @@ public void Client_HandleTrainsetPhysicsUpdate(ClientboundTrainsetPhysicsPacket return; } + //Multiplayer.Log($"Client_HandleTrainsetPhysicsUpdate({set.firstCar.ID}):, tick: {packet.Tick}"); + for (int i = 0; i < packet.TrainsetParts.Length; i++) { if(set.cars[i].TryNetworked(out NetworkedTrainCar networkedTrainCar)) networkedTrainCar.Client_ReceiveTrainPhysicsUpdate(in packet.TrainsetParts[i], packet.Tick); } } - + #endregion [UsedImplicitly] diff --git a/Multiplayer/Components/Networking/Train/NetworkedCarSpawner.cs b/Multiplayer/Components/Networking/Train/NetworkedCarSpawner.cs index 4268ceb..1fa9d44 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedCarSpawner.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedCarSpawner.cs @@ -53,7 +53,7 @@ public static TrainCar SpawnCar(TrainsetSpawnPart spawnPart, bool preventCouplin Transform trainTransform = trainCar.transform; trainTransform.position = spawnPart.Position + WorldMover.currentMove; - trainTransform.eulerAngles = spawnPart.Rotation; + trainTransform.rotation = spawnPart.Rotation; trainCar.playerSpawnedCar = spawnPart.PlayerSpawnedCar; trainCar.preventAutoCouple = true; diff --git a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs index bfcb44a..a1810d4 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs @@ -64,6 +64,7 @@ public static bool TryGetFromTrainCar(TrainCar trainCar, out NetworkedTrainCar n #endregion public TrainCar TrainCar; + public uint TicksSinceSync = uint.MaxValue; public bool HasPlayers => PlayerManager.Car == TrainCar || GetComponentInChildren() != null; private Bogie bogie1; @@ -96,8 +97,8 @@ public static bool TryGetFromTrainCar(TrainCar trainCar, out NetworkedTrainCar n private bool client_Initialized; public TickedQueue Client_trainSpeedQueue; public TickedQueue Client_trainRigidbodyQueue; - private TickedQueue client_bogie1Queue; - private TickedQueue client_bogie2Queue; + public TickedQueue client_bogie1Queue; + public TickedQueue client_bogie2Queue; #endregion @@ -365,6 +366,8 @@ private void Server_OnTick(uint tick) Server_SendCouplers(); Server_SendCargoState(); Server_SendHealthState(); + + TicksSinceSync++; //keep track of last full sync } private void Server_SendBrakePressures() @@ -610,18 +613,46 @@ public void Client_ReceiveTrainPhysicsUpdate(in TrainsetMovementPart movementPar if (TrainCar.isEligibleForSleep) TrainCar.ForceOptimizationState(false); - if (movementPart.IsRigidbodySnapshot) + if (movementPart.typeFlag == TrainsetMovementPart.MovementType.RigidBody) { + //Multiplayer.LogDebug(() => $"Client_ReceiveTrainPhysicsUpdate({TrainCar.ID}, {tick}): is RigidBody"); TrainCar.Derail(); TrainCar.stress.ResetTrainStress(); Client_trainRigidbodyQueue.ReceiveSnapshot(movementPart.RigidbodySnapshot, tick); } else { + //move the car to the correct position first - maybe? + if (movementPart.typeFlag.HasFlag(TrainsetMovementPart.MovementType.Sync)) + { + /* + float d1 = (TrainCar.transform.position - (movementPart.Position + WorldMover.currentMove)).sqrMagnitude; + Quaternion d2 = TrainCar.transform.rotation * Quaternion.Inverse(movementPart.Rotation); + + Multiplayer.LogDebug(()=> $"Client_ReceiveTrainPhysicsUpdate({TrainCar.ID}, {tick}): Sync, Queue counts: {Client_trainSpeedQueue.snapshots.Count}, {Client_trainRigidbodyQueue.snapshots.Count}, {client_bogie1Queue.snapshots.Count}, {client_bogie2Queue.snapshots.Count}, Deltas: {d1}, {d2}"); + */ + TrainCar.transform.position = movementPart.Position + WorldMover.currentMove; + TrainCar.transform.rotation = movementPart.Rotation; + + //clear the queues? + Client_trainSpeedQueue.Clear(); + Client_trainRigidbodyQueue.Clear(); + client_bogie1Queue.Clear(); + client_bogie2Queue.Clear(); + + TrainCar.stress.ResetTrainStress(); + }/* + else + { + Multiplayer.LogDebug(() => $"Client_ReceiveTrainPhysicsUpdate({TrainCar.ID}, {tick}): Physics"); + }*/ + Client_trainSpeedQueue.ReceiveSnapshot(movementPart.Speed, tick); TrainCar.stress.slowBuildUpStress = movementPart.SlowBuildUpStress; client_bogie1Queue.ReceiveSnapshot(movementPart.Bogie1, tick); client_bogie2Queue.ReceiveSnapshot(movementPart.Bogie2, tick); + + } } diff --git a/Multiplayer/Multiplayer.csproj b/Multiplayer/Multiplayer.csproj index d909432..740fd45 100644 --- a/Multiplayer/Multiplayer.csproj +++ b/Multiplayer/Multiplayer.csproj @@ -3,7 +3,7 @@ net48 latest Multiplayer - 0.1.8.0 + 0.1.8.1 diff --git a/Multiplayer/Networking/Data/TrainsetMovementPart.cs b/Multiplayer/Networking/Data/TrainsetMovementPart.cs index 62fade0..0c74da5 100644 --- a/Multiplayer/Networking/Data/TrainsetMovementPart.cs +++ b/Multiplayer/Networking/Data/TrainsetMovementPart.cs @@ -1,28 +1,54 @@ using LiteNetLib.Utils; - +using Multiplayer.Networking.Serialization; +using System; +using UnityEngine; namespace Multiplayer.Networking.Data; public readonly struct TrainsetMovementPart { - public readonly bool IsRigidbodySnapshot; + public readonly MovementType typeFlag; public readonly float Speed; public readonly float SlowBuildUpStress; + public readonly Vector3 Position; //Used in sync only + public readonly Quaternion Rotation; //Used in sync only public readonly BogieData Bogie1; public readonly BogieData Bogie2; public readonly RigidbodySnapshot RigidbodySnapshot; - public TrainsetMovementPart(float speed, float slowBuildUpStress, BogieData bogie1, BogieData bogie2) + [Flags] + public enum MovementType : byte + { + Physics = 1, + RigidBody = 2, + Sync = 4 + } + + public TrainsetMovementPart(float speed, float slowBuildUpStress, BogieData bogie1, BogieData bogie2, Vector3? position = null, Quaternion? rotation = null) { - IsRigidbodySnapshot = false; + typeFlag = MovementType.Physics; //no rigid body data + Speed = speed; SlowBuildUpStress = slowBuildUpStress; Bogie1 = bogie1; Bogie2 = bogie2; + + if(position != null && rotation != null) + { + //Multiplayer.LogDebug(()=>$"new TrainsetMovementPart() Sync"); + + typeFlag |= MovementType.Sync; //includes positional data + + Position = (Vector3)position; + Rotation = (Quaternion)rotation; + } } public TrainsetMovementPart(RigidbodySnapshot rigidbodySnapshot) { - IsRigidbodySnapshot = true; + typeFlag = MovementType.RigidBody; //rigid body data + + //Multiplayer.LogDebug(() => $"new TrainsetMovementPart() RigidBody"); + RigidbodySnapshot = rigidbodySnapshot; } @@ -30,9 +56,11 @@ public TrainsetMovementPart(RigidbodySnapshot rigidbodySnapshot) public static void Serialize(NetDataWriter writer, TrainsetMovementPart data) #pragma warning restore EPS05 { - writer.Put(data.IsRigidbodySnapshot); + writer.Put((byte)data.typeFlag); + + //Multiplayer.LogDebug(() => $"TrainsetMovementPart.Serialize() {data.typeFlag}"); - if (data.IsRigidbodySnapshot) + if (data.typeFlag == MovementType.RigidBody) { RigidbodySnapshot.Serialize(writer, data.RigidbodySnapshot); return; @@ -42,18 +70,41 @@ public static void Serialize(NetDataWriter writer, TrainsetMovementPart data) writer.Put(data.SlowBuildUpStress); BogieData.Serialize(writer, data.Bogie1); BogieData.Serialize(writer, data.Bogie2); + + if (data.typeFlag.HasFlag(MovementType.Sync)) //serialise positional data + { + Vector3Serializer.Serialize(writer, data.Position); + QuaternionSerializer.Serialize(writer, data.Rotation); + } } public static TrainsetMovementPart Deserialize(NetDataReader reader) { - bool isRigidbodySnapshot = reader.GetBool(); - return isRigidbodySnapshot - ? new TrainsetMovementPart(RigidbodySnapshot.Deserialize(reader)) - : new TrainsetMovementPart( - reader.GetFloat(), - reader.GetFloat(), - BogieData.Deserialize(reader), - BogieData.Deserialize(reader) - ); + MovementType dataType = (MovementType)reader.GetByte(); + + //Multiplayer.LogDebug(() => $"TrainsetMovementPart.Deserialize() {dataType}"); + + if (dataType == MovementType.RigidBody) + { + return new TrainsetMovementPart(RigidbodySnapshot.Deserialize(reader)); + } + else + { + float speed = reader.GetFloat(); + float slowBuildUpStress = reader.GetFloat(); + BogieData bd1 = BogieData.Deserialize(reader); + BogieData bd2 = BogieData.Deserialize(reader); + + Vector3? position = null; + Quaternion? rotation = null; + + if (dataType.HasFlag(MovementType.Sync)) + { + position = Vector3Serializer.Deserialize(reader); + rotation = QuaternionSerializer.Deserialize(reader); + } + + return new TrainsetMovementPart(speed, slowBuildUpStress, bd1, bd2, position, rotation); + } } } diff --git a/Multiplayer/Networking/Data/TrainsetSpawnPart.cs b/Multiplayer/Networking/Data/TrainsetSpawnPart.cs index d00e5bd..c967e7e 100644 --- a/Multiplayer/Networking/Data/TrainsetSpawnPart.cs +++ b/Multiplayer/Networking/Data/TrainsetSpawnPart.cs @@ -17,11 +17,11 @@ public readonly struct TrainsetSpawnPart public readonly bool IsRearCoupled; public readonly float Speed; public readonly Vector3 Position; - public readonly Vector3 Rotation; + public readonly Quaternion Rotation; public readonly BogieData Bogie1; public readonly BogieData Bogie2; - private TrainsetSpawnPart(ushort netId, string liveryId, string carId, string carGuid, bool playerSpawnedCar, bool isFrontCoupled, bool isRearCoupled, float speed, Vector3 position, Vector3 rotation, + private TrainsetSpawnPart(ushort netId, string liveryId, string carId, string carGuid, bool playerSpawnedCar, bool isFrontCoupled, bool isRearCoupled, float speed, Vector3 position, Quaternion rotation, BogieData bogie1, BogieData bogie2) { NetId = netId; @@ -49,7 +49,7 @@ public static void Serialize(NetDataWriter writer, TrainsetSpawnPart data) writer.Put(data.IsRearCoupled); writer.Put(data.Speed); Vector3Serializer.Serialize(writer, data.Position); - Vector3Serializer.Serialize(writer, data.Rotation); + QuaternionSerializer.Serialize(writer, data.Rotation); BogieData.Serialize(writer, data.Bogie1); BogieData.Serialize(writer, data.Bogie2); } @@ -66,7 +66,7 @@ public static TrainsetSpawnPart Deserialize(NetDataReader reader) reader.GetBool(), reader.GetFloat(), Vector3Serializer.Deserialize(reader), - Vector3Serializer.Deserialize(reader), + QuaternionSerializer.Deserialize(reader), BogieData.Deserialize(reader), BogieData.Deserialize(reader) ); @@ -86,7 +86,7 @@ public static TrainsetSpawnPart FromTrainCar(NetworkedTrainCar networkedTrainCar trainCar.rearCoupler.IsCoupled(), trainCar.GetForwardSpeed(), transform.position - WorldMover.currentMove, - transform.eulerAngles, + transform.rotation, BogieData.FromBogie(trainCar.Bogies[0], true, networkedTrainCar.Bogie1TrackDirection), BogieData.FromBogie(trainCar.Bogies[1], true, networkedTrainCar.Bogie2TrackDirection) ); diff --git a/info.json b/info.json index 1015e9d..c71ee5f 100644 --- a/info.json +++ b/info.json @@ -1,6 +1,6 @@ { "Id": "Multiplayer", - "Version": "0.1.8.0", + "Version": "0.1.8.1", "DisplayName": "Multiplayer", "Author": "Insprill, Macka, Morm", "EntryMethod": "Multiplayer.Multiplayer.Load", From 77f120aef7be9bbcf00a53083c35a6fdad76d6f0 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 24 Aug 2024 09:39:24 +1000 Subject: [PATCH 073/188] Fix for single player job expiry bug Should also help with player position sync --- .../Managers/Client/ClientPlayerManager.cs | 15 ++++--- .../Managers/Client/NetworkClient.cs | 44 ++++++++++--------- .../Managers/Server/NetworkServer.cs | 43 +++++++++--------- .../ClientboundPlayerJoinedPacket.cs | 2 +- .../ClientboundPlayerPositionPacket.cs | 1 + .../ServerboundPlayerPositionPacket.cs | 1 + .../Jobs/StationJobGenerationRangePatch.cs | 26 +---------- .../CustomFirstPersonControllerPatch.cs | 35 +++++++-------- Multiplayer/Utils/DvExtensions.cs | 4 +- 9 files changed, 76 insertions(+), 95 deletions(-) diff --git a/Multiplayer/Networking/Managers/Client/ClientPlayerManager.cs b/Multiplayer/Networking/Managers/Client/ClientPlayerManager.cs index 1911f9c..a3245c2 100644 --- a/Multiplayer/Networking/Managers/Client/ClientPlayerManager.cs +++ b/Multiplayer/Networking/Managers/Client/ClientPlayerManager.cs @@ -55,17 +55,18 @@ public void UpdatePing(byte id, int ping) player.SetPing(ping); } - public void UpdatePosition(byte id, Vector3 position, Vector3 moveDir, float rotation, bool isJumping, bool isOnCar) + public void UpdatePosition(byte id, Vector3 position, Vector3 moveDir, float rotation, bool isJumping, bool isOnCar, ushort carId) { if (!playerMap.TryGetValue(id, out NetworkedPlayer player)) return; + player.UpdateCar(carId); player.UpdatePosition(position, moveDir, rotation, isJumping, isOnCar); } - public void UpdateCar(byte playerId, ushort carId) - { - if (!playerMap.TryGetValue(playerId, out NetworkedPlayer player)) - return; - player.UpdateCar(carId); - } + //public void UpdateCar(byte playerId, ushort carId) + //{ + // if (!playerMap.TryGetValue(playerId, out NetworkedPlayer player)) + // return; + // player.UpdateCar(carId); + //} } diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 38db294..fdfada4 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -84,7 +84,7 @@ protected override void Subscribe() netPacketProcessor.SubscribeReusable(OnClientboundPlayerDisconnectPacket); netPacketProcessor.SubscribeReusable(OnClientboundPlayerKickPacket); netPacketProcessor.SubscribeReusable(OnClientboundPlayerPositionPacket); - netPacketProcessor.SubscribeReusable(OnClientboundPlayerCarPacket); + //netPacketProcessor.SubscribeReusable(OnClientboundPlayerCarPacket); netPacketProcessor.SubscribeReusable(OnClientboundPingUpdatePacket); netPacketProcessor.SubscribeReusable(OnClientboundTickSyncPacket); netPacketProcessor.SubscribeReusable(OnClientboundServerLoadingPacket); @@ -249,8 +249,8 @@ private void OnClientboundPlayerJoinedPacket(ClientboundPlayerJoinedPacket packe { Guid guid = new(packet.Guid); ClientPlayerManager.AddPlayer(packet.Id, packet.Username, guid); - ClientPlayerManager.UpdateCar(packet.Id, packet.TrainCar); - ClientPlayerManager.UpdatePosition(packet.Id, packet.Position, Vector3.zero, packet.Rotation, false, packet.TrainCar != 0); + //ClientPlayerManager.UpdateCar(packet.Id, packet.TrainCar); + ClientPlayerManager.UpdatePosition(packet.Id, packet.Position, Vector3.zero, packet.Rotation, false, packet.CarID != 0, packet.CarID); } private void OnClientboundPlayerDisconnectPacket(ClientboundPlayerDisconnectPacket packet) @@ -267,13 +267,13 @@ private void OnClientboundPlayerKickPacket(ClientboundPlayerKickPacket packet) } private void OnClientboundPlayerPositionPacket(ClientboundPlayerPositionPacket packet) { - ClientPlayerManager.UpdatePosition(packet.Id, packet.Position, packet.MoveDir, packet.RotationY, packet.IsJumping, packet.IsOnCar); + ClientPlayerManager.UpdatePosition(packet.Id, packet.Position, packet.MoveDir, packet.RotationY, packet.IsJumping, packet.IsOnCar, packet.CarID); } - private void OnClientboundPlayerCarPacket(ClientboundPlayerCarPacket packet) - { - ClientPlayerManager.UpdateCar(packet.Id, packet.CarId); - } + //private void OnClientboundPlayerCarPacket(ClientboundPlayerCarPacket packet) + //{ + // ClientPlayerManager.UpdateCar(packet.Id, packet.CarId); + //} private void OnClientboundPingUpdatePacket(ClientboundPingUpdatePacket packet) { @@ -834,28 +834,32 @@ private void SendReadyPacket() SendPacketToServer(new ServerboundClientReadyPacket(), DeliveryMethod.ReliableOrdered); } - public void SendPlayerPosition(Vector3 position, Vector3 moveDir, float rotationY, bool isJumping, bool isOnCar, bool reliable) + public void SendPlayerPosition(Vector3 position, Vector3 moveDir, float rotationY, ushort carId, bool isJumping, bool isOnCar, bool reliable) { + Multiplayer.LogDebug(() => $"SendPlayerPosition({position}, {moveDir}, {rotationY}, {carId}, {isJumping}, {isOnCar})"); + SendPacketToServer(new ServerboundPlayerPositionPacket { Position = position, MoveDir = new Vector2(moveDir.x, moveDir.z), RotationY = rotationY, - IsJumpingIsOnCar = (byte)((isJumping ? 1 : 0) | (isOnCar ? 2 : 0)) + IsJumpingIsOnCar = (byte)((isJumping ? 1 : 0) | (isOnCar ? 2 : 0)), + CarID = carId }, reliable ? DeliveryMethod.ReliableOrdered : DeliveryMethod.Sequenced); } - public void SendPlayerCar(ushort carId,Vector3 position, Vector3 moveDir, float rotationY, bool isJumping) - { - SendPacketToServer(new ServerboundPlayerCarPacket - { - CarId = carId, - Position = position, - MoveDir = moveDir, - RotationY=rotationY, + //public void SendPlayerCar(ushort carId,Vector3 position, Vector3 moveDir, float rotationY, bool isJumping) + //{ + // Multiplayer.LogDebug(() => $"SendPlayerCar({carId}, {position}, {moveDir}, {rotationY}, {isJumping})"); + // SendPacketToServer(new ServerboundPlayerCarPacket + // { + // CarId = carId, + // Position = position, + // MoveDir = moveDir, + // RotationY=rotationY, - }, DeliveryMethod.ReliableOrdered); - } + // }, DeliveryMethod.ReliableOrdered); + //} public void SendTimeAdvance(float amountOfTimeToSkipInSeconds) { diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index 2ea4ef2..433a642 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -111,7 +111,7 @@ protected override void Subscribe() netPacketProcessor.SubscribeReusable(OnServerboundClientReadyPacket); netPacketProcessor.SubscribeReusable(OnServerboundSaveGameDataRequestPacket); netPacketProcessor.SubscribeReusable(OnServerboundPlayerPositionPacket); - netPacketProcessor.SubscribeReusable(OnServerboundPlayerCarPacket); + //netPacketProcessor.SubscribeReusable(OnServerboundPlayerCarPacket); netPacketProcessor.SubscribeReusable(OnServerboundTimeAdvancePacket); netPacketProcessor.SubscribeReusable(OnServerboundTrainSyncRequestPacket); netPacketProcessor.SubscribeReusable(OnServerboundTrainDeleteRequestPacket); @@ -629,7 +629,7 @@ private void OnServerboundClientReadyPacket(ServerboundClientReadyPacket packet, Id = player.Id, Username = player.Username, Guid = player.Guid.ToByteArray(), - TrainCar = player.CarId, + CarID = player.CarId, Position = player.RawPosition, Rotation = player.RawRotationY }, DeliveryMethod.ReliableOrdered); @@ -645,8 +645,10 @@ private void OnServerboundPlayerPositionPacket(ServerboundPlayerPositionPacket p { if (TryGetServerPlayer(peer, out ServerPlayer player)) { + player.CarId = packet.CarID; player.RawPosition = packet.Position; player.RawRotationY = packet.RotationY; + } ClientboundPlayerPositionPacket clientboundPacket = new() @@ -655,33 +657,34 @@ private void OnServerboundPlayerPositionPacket(ServerboundPlayerPositionPacket p Position = packet.Position, MoveDir = packet.MoveDir, RotationY = packet.RotationY, - IsJumpingIsOnCar = packet.IsJumpingIsOnCar + IsJumpingIsOnCar = packet.IsJumpingIsOnCar, + CarID = packet.CarID }; SendPacketToAll(clientboundPacket, DeliveryMethod.Sequenced, peer); } - private void OnServerboundPlayerCarPacket(ServerboundPlayerCarPacket packet, NetPeer peer) - { - if (packet.CarId != 0 && !NetworkedTrainCar.Get(packet.CarId, out NetworkedTrainCar _)) - return; + //private void OnServerboundPlayerCarPacket(ServerboundPlayerCarPacket packet, NetPeer peer) + //{ + // if (packet.CarId != 0 && !NetworkedTrainCar.Get(packet.CarId, out NetworkedTrainCar _)) + // return; - if (TryGetServerPlayer(peer, out ServerPlayer player)) - { - player.CarId = packet.CarId; - player.RawPosition = packet.Position; - player.RawRotationY = packet.RotationY; + // if (TryGetServerPlayer(peer, out ServerPlayer player)) + // { + // player.CarId = packet.CarId; + // player.RawPosition = packet.Position; + // player.RawRotationY = packet.RotationY; - } + // } - ClientboundPlayerCarPacket clientboundPacket = new() - { - Id = (byte)peer.Id, - CarId = packet.CarId - }; + // ClientboundPlayerCarPacket clientboundPacket = new() + // { + // Id = (byte)peer.Id, + // CarId = packet.CarId + // }; - SendPacketToAll(clientboundPacket, DeliveryMethod.ReliableOrdered, peer); - } + // SendPacketToAll(clientboundPacket, DeliveryMethod.ReliableOrdered, peer); + //} private void OnServerboundTimeAdvancePacket(ServerboundTimeAdvancePacket packet, NetPeer peer) { diff --git a/Multiplayer/Networking/Packets/Clientbound/ClientboundPlayerJoinedPacket.cs b/Multiplayer/Networking/Packets/Clientbound/ClientboundPlayerJoinedPacket.cs index 409c3f5..d925e40 100644 --- a/Multiplayer/Networking/Packets/Clientbound/ClientboundPlayerJoinedPacket.cs +++ b/Multiplayer/Networking/Packets/Clientbound/ClientboundPlayerJoinedPacket.cs @@ -7,7 +7,7 @@ public class ClientboundPlayerJoinedPacket public byte Id { get; set; } public string Username { get; set; } public byte[] Guid { get; set; } - public ushort TrainCar { get; set; } + public ushort CarID { get; set; } public Vector3 Position { get; set; } public float Rotation { get; set; } } diff --git a/Multiplayer/Networking/Packets/Clientbound/ClientboundPlayerPositionPacket.cs b/Multiplayer/Networking/Packets/Clientbound/ClientboundPlayerPositionPacket.cs index bf27e5a..6abe79f 100644 --- a/Multiplayer/Networking/Packets/Clientbound/ClientboundPlayerPositionPacket.cs +++ b/Multiplayer/Networking/Packets/Clientbound/ClientboundPlayerPositionPacket.cs @@ -9,6 +9,7 @@ public class ClientboundPlayerPositionPacket public Vector2 MoveDir { get; set; } public float RotationY { get; set; } public byte IsJumpingIsOnCar { get; set; } + public ushort CarID { get; set; } public bool IsJumping => (IsJumpingIsOnCar & 1) != 0; public bool IsOnCar => (IsJumpingIsOnCar & 2) != 0; diff --git a/Multiplayer/Networking/Packets/Serverbound/ServerboundPlayerPositionPacket.cs b/Multiplayer/Networking/Packets/Serverbound/ServerboundPlayerPositionPacket.cs index b4f1f3c..c13d141 100644 --- a/Multiplayer/Networking/Packets/Serverbound/ServerboundPlayerPositionPacket.cs +++ b/Multiplayer/Networking/Packets/Serverbound/ServerboundPlayerPositionPacket.cs @@ -8,4 +8,5 @@ public class ServerboundPlayerPositionPacket public Vector2 MoveDir { get; set; } public float RotationY { get; set; } public byte IsJumpingIsOnCar { get; set; } + public ushort CarID { get; set; } } diff --git a/Multiplayer/Patches/Jobs/StationJobGenerationRangePatch.cs b/Multiplayer/Patches/Jobs/StationJobGenerationRangePatch.cs index 86c625d..e77279f 100644 --- a/Multiplayer/Patches/Jobs/StationJobGenerationRangePatch.cs +++ b/Multiplayer/Patches/Jobs/StationJobGenerationRangePatch.cs @@ -1,6 +1,5 @@ using HarmonyLib; using Multiplayer.Components.Networking; -using Multiplayer.Networking.Data; using Multiplayer.Utils; using UnityEngine; @@ -18,18 +17,7 @@ private static bool Prefix(StationJobGenerationRange __instance, ref float __res __result = anchor.AnyPlayerSqrMag(); - /* - __result = float.MaxValue; - - //Loop through all of the players and return the one thats closest to the anchor - foreach (ServerPlayer serverPlayer in NetworkLifecycle.Instance.Server.ServerPlayers) - { - float sqDist = (serverPlayer.WorldPosition - anchor).sqrMagnitude; - - if (sqDist < __result) - __result = sqDist; - } - */ + //Multiplayer.Log($"PlayerSqrDistanceFromStationCenter() {__result}"); return false; } @@ -47,18 +35,6 @@ private static bool Prefix(StationJobGenerationRange __instance, ref float __res __result = anchor.AnyPlayerSqrMag(); - /* - __result = float.MaxValue; - - //Loop through all of the players and return the one thats closest to the anchor - foreach (ServerPlayer serverPlayer in NetworkLifecycle.Instance.Server.ServerPlayers) - { - float sqDist = (serverPlayer.WorldPosition - anchor).sqrMagnitude; - - if (sqDist < __result) - __result = sqDist; - } - */ return false; } } diff --git a/Multiplayer/Patches/Player/CustomFirstPersonControllerPatch.cs b/Multiplayer/Patches/Player/CustomFirstPersonControllerPatch.cs index d6035bc..81f1e05 100644 --- a/Multiplayer/Patches/Player/CustomFirstPersonControllerPatch.cs +++ b/Multiplayer/Patches/Player/CustomFirstPersonControllerPatch.cs @@ -1,6 +1,7 @@ using HarmonyLib; using Multiplayer.Components.Networking; using Multiplayer.Utils; +using System; using UnityEngine; namespace Multiplayer.Patches.Player; @@ -8,6 +9,8 @@ namespace Multiplayer.Patches.Player; [HarmonyPatch(typeof(CustomFirstPersonController))] public static class CustomFirstPersonControllerPatch { + private const float ROTATION_THRESHOLD = 0.001f; + private static CustomFirstPersonController fps; private static Vector3 lastPosition; @@ -16,9 +19,10 @@ public static class CustomFirstPersonControllerPatch private static bool isJumping; private static bool isOnCar; + private static TrainCar car; - [HarmonyPostfix] [HarmonyPatch(nameof(CustomFirstPersonController.Awake))] + [HarmonyPostfix] private static void CharacterMovement(CustomFirstPersonController __instance) { fps = __instance; @@ -33,29 +37,15 @@ private static void OnDestroy() { if (UnloadWatcher.isQuitting) return; + NetworkLifecycle.Instance.OnTick -= OnTick; PlayerManager.CarChanged -= OnCarChanged; } private static void OnCarChanged(TrainCar trainCar) { - //Multiplayer.LogDebug(() => $"OnCarChanged isOnCar: {isOnCar}, car: {trainCar?.name}"); - isOnCar = trainCar != null; - - //Multiplayer.LogDebug(() => $"OnCarChanged isOnCar: {isOnCar}, car: {trainCar?.name}"); - - Vector3 position = isOnCar ? PlayerManager.PlayerTransform.localPosition : PlayerManager.GetWorldAbsolutePlayerPosition(); - float rotationY = (isOnCar ? PlayerManager.PlayerTransform.localEulerAngles : PlayerManager.PlayerTransform.eulerAngles).y; - - //Multiplayer.LogDebug(() => $"OnCarChanged isOnCar: {isOnCar}, car: {trainCar?.name}, lastPosition: {lastPosition}, lastRotation: {lastRotationY}, position: {position}, rotation: {rotationY}"); - - lastPosition = position; - lastRotationY = rotationY; - - - - NetworkLifecycle.Instance.Client.SendPlayerCar(!isOnCar ? (ushort)0 : trainCar.GetNetId(), lastPosition, PlayerManager.PlayerTransform.InverseTransformDirection(fps.m_MoveDir), lastRotationY, isJumping); + car = trainCar; } private static void OnTick(uint tick) @@ -66,17 +56,24 @@ private static void OnTick(uint tick) Vector3 position = isOnCar ? PlayerManager.PlayerTransform.localPosition : PlayerManager.GetWorldAbsolutePlayerPosition(); float rotationY = (isOnCar ? PlayerManager.PlayerTransform.localEulerAngles : PlayerManager.PlayerTransform.eulerAngles).y; - bool positionOrRotationChanged = lastPosition != position || !Mathf.Approximately(lastRotationY, rotationY); + //bool positionOrRotationChanged = lastPosition != position || !Mathf.Approximately(lastRotationY, rotationY); + + bool positionOrRotationChanged = Vector3.Distance(lastPosition, position) > 0 || Math.Abs(lastRotationY - rotationY) > ROTATION_THRESHOLD; + if (!positionOrRotationChanged && sentFinalPosition) return; lastPosition = position; lastRotationY = rotationY; sentFinalPosition = !positionOrRotationChanged; - NetworkLifecycle.Instance.Client.SendPlayerPosition(lastPosition, PlayerManager.PlayerTransform.InverseTransformDirection(fps.m_MoveDir), lastRotationY, isJumping, isOnCar, isJumping || sentFinalPosition); + + ushort carNetID = isOnCar ? car.GetNetId() : (ushort)0; + + NetworkLifecycle.Instance.Client.SendPlayerPosition(lastPosition, PlayerManager.PlayerTransform.InverseTransformDirection(fps.m_MoveDir), lastRotationY, carNetID, isJumping, isOnCar, isJumping || sentFinalPosition); isJumping = false; } + [HarmonyPostfix] [HarmonyPatch(nameof(CustomFirstPersonController.SetJumpParameters))] private static void SetJumpParameters() diff --git a/Multiplayer/Utils/DvExtensions.cs b/Multiplayer/Utils/DvExtensions.cs index 04b7e28..0ddc15a 100644 --- a/Multiplayer/Utils/DvExtensions.cs +++ b/Multiplayer/Utils/DvExtensions.cs @@ -121,9 +121,7 @@ public static float AnyPlayerSqrMag(this GameObject item) public static float AnyPlayerSqrMag(this Vector3 anchor) { float result = float.MaxValue; - string origin = new StackTrace().GetFrame(1).GetMethod().Name; - - + //string origin = new StackTrace().GetFrame(1).GetMethod().Name; //Loop through all of the players and return the one thats closest to the anchor foreach (ServerPlayer serverPlayer in NetworkLifecycle.Instance.Server.ServerPlayers) From f5f68cf9f3f24ea47920d5e767f939be46d02dd5 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 25 Aug 2024 19:21:25 +1000 Subject: [PATCH 074/188] Continuing Job Sync --- .../Networking/Jobs/NetworkedJob.cs | 95 +--- .../Networking/World/NetworkedStation.cs | 120 ----- .../World/NetworkedStationController.cs | 284 +++++++++++ .../Components/StationComponentLookup.cs | 3 +- Multiplayer/Networking/Data/JobData.cs | 102 ++-- Multiplayer/Networking/Data/TaskData.cs | 371 -------------- .../Networking/Data/TaskNetworkData.cs | 458 ++++++++++++++++++ .../Managers/Client/NetworkClient.cs | 146 ++---- .../Managers/Server/NetworkServer.cs | 53 +- .../Clientbound/ClientboundPlayerCarPacket.cs | 7 - .../Jobs/ClientboundJobCreatePacket.cs | 22 - .../Clientbound/Jobs/ClientboundJobPacket.cs | 11 - .../Jobs/ClientboundJobsCreatePacket.cs | 25 + ...lientboundStationControllerLookupPacket.cs | 37 ++ .../{ => Train}/ServerboundAddCoalPacket.cs | 2 +- .../ServerboundFireboxIgnitePacket.cs | 2 +- .../ServerboundTrainDeleteRequestPacket.cs | 2 +- .../ServerboundTrainRerailRequestPacket.cs | 2 +- .../ServerboundTrainSyncRequestPacket.cs | 2 +- Multiplayer/Patches/Jobs/JobPatch.cs | 22 +- .../Patches/Jobs/StationControllerPatch.cs | 2 +- Multiplayer/Patches/Jobs/StationPatch.cs | 13 +- 22 files changed, 984 insertions(+), 797 deletions(-) delete mode 100644 Multiplayer/Components/Networking/World/NetworkedStation.cs create mode 100644 Multiplayer/Components/Networking/World/NetworkedStationController.cs delete mode 100644 Multiplayer/Networking/Data/TaskData.cs create mode 100644 Multiplayer/Networking/Data/TaskNetworkData.cs delete mode 100644 Multiplayer/Networking/Packets/Clientbound/ClientboundPlayerCarPacket.cs delete mode 100644 Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobCreatePacket.cs delete mode 100644 Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobPacket.cs create mode 100644 Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobsCreatePacket.cs create mode 100644 Multiplayer/Networking/Packets/Clientbound/World/ClientboundStationControllerLookupPacket.cs rename Multiplayer/Networking/Packets/Serverbound/{ => Train}/ServerboundAddCoalPacket.cs (67%) rename Multiplayer/Networking/Packets/Serverbound/{ => Train}/ServerboundFireboxIgnitePacket.cs (77%) rename Multiplayer/Networking/Packets/Serverbound/{ => Train}/ServerboundTrainDeleteRequestPacket.cs (60%) rename Multiplayer/Networking/Packets/Serverbound/{ => Train}/ServerboundTrainRerailRequestPacket.cs (79%) rename Multiplayer/Networking/Packets/Serverbound/{ => Train}/ServerboundTrainSyncRequestPacket.cs (60%) diff --git a/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs b/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs index 9433e9c..ef0c864 100644 --- a/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs +++ b/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs @@ -3,14 +3,10 @@ using System.Collections.Generic; using System.Linq; using DV.Logic.Job; -using DV.ThingTypes; -using DV.Utils; -using Multiplayer.Components.Networking.Player; +using Multiplayer.Components.Networking.Train; using Multiplayer.Components.Networking.World; -using Multiplayer.Networking.Data; -using Multiplayer.Utils; using UnityEngine; -using static System.Collections.Specialized.BitVector32; + namespace Multiplayer.Components.Networking.Jobs; @@ -32,7 +28,7 @@ public static bool Get(ushort netId, out NetworkedJob obj) public static bool GetJob(ushort netId, out Job obj) { bool b = Get(netId, out NetworkedJob networkedJob); - obj = b ? networkedJob.job : null; + obj = b ? networkedJob.Job : null; return b; } @@ -46,25 +42,14 @@ public static bool TryGetFromJob(Job job, out NetworkedJob networkedJob) { return jobToNetworkedJob.TryGetValue(job, out networkedJob); } - - /*public static NetworkedJob AddJob(string stationID, Job job) - { - NetworkedJob netJob = new NetworkedJob(stationID, job); - - jobToNetworkedJob[job] = netJob; - jobIdToNetworkedJob[job.ID] = netJob; - jobIdToJob[job.ID] = job; - - Multiplayer.Log($"NetworkedJob Added with netId: {jobToNetworkedJob[job].NetId}, jobId: {job.ID}"); - return jobToNetworkedJob[job]; - }*/ #endregion - public Job job; - public JobOverview jobOverview; - public JobBooklet jobBooklet; - public string stationID; - public bool isJobNew = true; + public Job Job; + public JobOverview JobOverview; + public JobBooklet JobBooklet; + public Station Station; + +// public bool isJobNew = true; public bool isJobDirty = false; public bool isTaskDirty = false; @@ -83,63 +68,20 @@ public static bool TryGetFromJob(Job job, out NetworkedJob networkedJob) protected override bool IsIdServerAuthoritative => true; - protected override void Awake() - { - Multiplayer.Log("NetworkJob.Awake()"); - base.Awake(); - - /* - job = GetComponent(); - - jobToNetworkedJob[job] = this; - jobIdToNetworkedJob[job.ID] = this; - jobIdToJob[job.ID] = job; - - if (NetworkLifecycle.Instance.IsHost()) - { - //do we need a job watcher - probably not, but maybe or maybe we need a task watcher - //NetworkTrainsetWatcher.Instance.CheckInstance(); // Ensure the NetworkTrainsetWatcher is initialized - } - else - { - //Networked task?? - - //Client_trainSpeedQueue = TrainCar.GetOrAddComponent(); - //Client_trainRigidbodyQueue = TrainCar.GetOrAddComponent(); - //StartCoroutine(Client_InitLater()); - } - */ - } - private void Start() { //startup stuff - Multiplayer.Log($"NetworkedJob.Start({job.ID})"); + Multiplayer.Log($"NetworkedJob.Start({Job.ID})"); - isJobNew = true; //Send new jobs on tick + jobToNetworkedJob[Job] = this; + jobIdToNetworkedJob[Job.ID] = this; + jobIdToJob[Job.ID] = Job; - StationController station; - if (!StationComponentLookup.Instance.StationControllerFromId(stationID, out station)) - { - Multiplayer.LogWarning($"NetworkJob.Start() Could not get staion for stationId: {stationID}"); - return; - } + //isJobNew = true; //Send new jobs on tick if (!NetworkLifecycle.Instance.IsHost()) - { - //station.logicStation.AddJobToStation(job); - - if (station.logicStation.availableJobs.Contains(job)) - { - Multiplayer.LogError("Trying to add the same job[" + job.ID + "] multiple times to station! Skipping, trying to recover."); - return; - } - - //station.logicStation.availableJobs.Add(job); - //job.JobTaken += this.OnJobTaken; - //job.JobExpired += this.OnJobExpired; - //job.JobAddedToStation?.Invoke(); - CoroutineManager.Instance.StartCoroutine(NetworkedStation.UpdateCarPlates(job.tasks, job.ID)); + { + CoroutineManager.Instance.StartCoroutine(NetworkedStationController.UpdateCarPlates(Job.tasks, Job.ID)); } else { @@ -150,7 +92,6 @@ private void Start() } Multiplayer.Log("NetworkedJob.Start() Started"); - //possibly capture tasks at this point for tracking?? } private void OnDisable() @@ -221,7 +162,7 @@ public bool Server_ValidateClientCompleteJob(ServerPlayer player, CommonTrainPor } */ - + private void Server_OnTick(uint tick) { if (UnloadWatcher.isUnloading) @@ -301,7 +242,7 @@ public void OnJobTaken(Job jobTaken,bool _) public void OnJobExpired(Job jobExpired) { - Multiplayer.Log($"Job Expired: {job.ID}"); + Multiplayer.Log($"Job Expired: {Job.ID}"); jobExpired.JobTaken -= this.OnJobTaken; jobExpired.JobExpired -= this.OnJobExpired; //jobExpired.JobCompleted += this.OnJobCompleted; diff --git a/Multiplayer/Components/Networking/World/NetworkedStation.cs b/Multiplayer/Components/Networking/World/NetworkedStation.cs deleted file mode 100644 index 141dd5b..0000000 --- a/Multiplayer/Components/Networking/World/NetworkedStation.cs +++ /dev/null @@ -1,120 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using DV.Logic.Job; -using Multiplayer.Components.Networking.Train; -using UnityEngine; -using static DV.Common.GameFeatureFlags; -using static DV.UI.ATutorialsMenuProvider; - -namespace Multiplayer.Components.Networking.World; - -public class NetworkedStation : MonoBehaviour -{ - private StationController stationController; - - private void Awake() - { - Multiplayer.Log("NetworkedStation.Awake()"); - - stationController = GetComponent(); - StartCoroutine(WaitForLogicStation()); - } - - private IEnumerator WaitForLogicStation() - { - while (stationController.logicStation == null) - yield return null; - - StationComponentLookup.Instance.RegisterStation(stationController); - - Multiplayer.Log("NetworkedStation.Awake() done"); - } - - public static IEnumerator UpdateCarPlates(List tasks, string jobId) - { - - List cars = new List(); - UpdateCarPlatesRecursive(tasks, jobId, ref cars); - - - if (cars != null) - { - Multiplayer.Log("NetworkedStation.UpdateCarPlates() Cars count: " + cars.Count); - - foreach (Car car in cars) - { - Multiplayer.Log("NetworkedStation.UpdateCarPlates() Car: " + car.ID); - - TrainCar trainCar = null; - int loopCtr = 0; - while (!NetworkedTrainCar.GetTrainCarFromTrainId(car.ID, out trainCar)) - { - loopCtr++; - if (loopCtr > 5000) - { - Multiplayer.Log("NetworkedStation.UpdateCarPlates() TimeOut"); - break; - } - - - yield return null; - } - - trainCar?.UpdateJobIdOnCarPlates(jobId); - } - } - } - private static void UpdateCarPlatesRecursive(List tasks, string jobId, ref List cars) - { - Multiplayer.Log("NetworkedStation.UpdateCarPlatesRecursive() Starting"); - - foreach (Task task in tasks) - { - if (task is WarehouseTask) - { - Multiplayer.Log("NetworkedStation.UpdateCarPlatesRecursive() WarehouseTask"); - cars = cars.Union(((WarehouseTask)task).cars).ToList(); - } - else if (task is TransportTask) - { - Multiplayer.Log("NetworkedStation.UpdateCarPlatesRecursive() TransportTask"); - cars = cars.Union(((TransportTask)task).cars).ToList(); - } - else if (task is SequentialTasks) - { - Multiplayer.Log("NetworkedStation.UpdateCarPlatesRecursive() SequentialTasks"); - List seqTask = new(); - - for (LinkedListNode node = ((SequentialTasks)task).tasks.First; node != null; node = node.Next) - { - Multiplayer.Log($"NetworkedStation.UpdateCarPlatesRecursive() SequentialTask Adding node"); - seqTask.Add(node.Value); - } - - Multiplayer.Log($"NetworkedStation.UpdateCarPlatesRecursive() SequentialTask Node Count:{seqTask.Count}"); - - Multiplayer.Log("NetworkedStation.UpdateCarPlatesRecursive() Calling UpdateCarPlates()"); - //drill down - UpdateCarPlatesRecursive(seqTask, jobId, ref cars); - Multiplayer.Log($"NetworkedStation.UpdateCarPlatesRecursive() SequentialTask RETURNED"); - } - else if (task is ParallelTasks) - { - //not implemented - Multiplayer.Log("NetworkedStation.UpdateCarPlatesRecursive() ParallelTasks"); - - Multiplayer.Log("NetworkedStation.UpdateCarPlatesRecursive() Calling UpdateCarPlates()"); - //drill down - UpdateCarPlatesRecursive(((ParallelTasks)task).tasks, jobId, ref cars); - } - else - { - throw new ArgumentException("NetworkedStation.UpdateCarPlatesRecursive() Unknown task type: " + task.GetType()); - } - } - - Multiplayer.Log("NetworkedStation.UpdateCarPlatesRecursive() Returning"); - } -} diff --git a/Multiplayer/Components/Networking/World/NetworkedStationController.cs b/Multiplayer/Components/Networking/World/NetworkedStationController.cs new file mode 100644 index 0000000..01abd5a --- /dev/null +++ b/Multiplayer/Components/Networking/World/NetworkedStationController.cs @@ -0,0 +1,284 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using DV.Logic.Job; +using Multiplayer.Components.Networking.Jobs; +using Multiplayer.Components.Networking.Train; +using Multiplayer.Networking.Data; +using UnityEngine; + +namespace Multiplayer.Components.Networking.World; + +public class NetworkedStationController : IdMonoBehaviour +{ + #region Lookup Cache + private static readonly Dictionary stationControllerToNetworkedStationController = new(); + private static readonly Dictionary stationIdToNetworkedStationController = new(); + private static readonly Dictionary stationIdToStationController = new(); + private static readonly Dictionary stationToNetworkedStationController = new(); + + public static bool Get(ushort netId, out NetworkedStationController obj) + { + bool b = Get(netId, out IdMonoBehaviour rawObj); + obj = (NetworkedStationController)rawObj; + return b; + } + + public static DictionaryGetAll() + { + Dictionary result = new Dictionary(); + + foreach (var kvp in stationIdToNetworkedStationController ) + { + Multiplayer.Log($"GetAll() adding {kvp.Value.NetId}, {kvp.Key}"); + result.Add(kvp.Value.NetId, kvp.Key); + } + return result; + } + + public static bool GetStationController(ushort netId, out StationController obj) + { + bool b = Get(netId, out NetworkedStationController networkedStationController); + obj = b ? networkedStationController.StationController : null; + return b; + } + public static bool GetFromStationId(string stationId, out NetworkedStationController networkedStationController) + { + return stationIdToNetworkedStationController.TryGetValue(stationId, out networkedStationController); + } + + public static bool GetFromStation(Station station, out NetworkedStationController networkedStationController) + { + return stationToNetworkedStationController.TryGetValue(station, out networkedStationController); + } + public static bool GetStationControllerFromStationId(string stationId, out StationController stationController) + { + return stationIdToStationController.TryGetValue(stationId, out stationController); + } + + public static bool GetFromStationController(StationController stationController, out NetworkedStationController networkedStationController) + { + return stationControllerToNetworkedStationController.TryGetValue(stationController, out networkedStationController); + } + + public static void RegisterStationController(NetworkedStationController networkedStationController, StationController stationController) + { + string stationID = stationController.logicStation.ID; + + stationControllerToNetworkedStationController.Add(stationController,networkedStationController); + stationIdToNetworkedStationController.Add(stationID, networkedStationController); + stationIdToStationController.Add(stationID, stationController); + stationToNetworkedStationController.Add(stationController.logicStation, networkedStationController); +} + #endregion + + + protected override bool IsIdServerAuthoritative => true; + + private StationController StationController; + + public HashSet NetworkedJobs { get; } = new HashSet(); + private List NewJobs = new List(); + //public List JobOverviews; //for later use + + private void Awake() + { + base.Awake(); + StationController = GetComponent(); + StartCoroutine(WaitForLogicStation()); + } + + private void Start() + { + if (NetworkLifecycle.Instance.IsHost()) + { + NetworkLifecycle.Instance.OnTick += Server_OnTick; + } + } + + private IEnumerator WaitForLogicStation() + { + while (StationController.logicStation == null) + yield return null; + + NetworkedStationController.RegisterStationController(this, StationController); + Multiplayer.Log($"NetworkedStation.Awake({StationController.logicStation.ID})"); + } + + //Adding job on server + public void AddJob(Job job) + { + NetworkedJob networkedJob = new GameObject($"NetworkedJob {job.ID}").AddComponent(); + networkedJob.Job = job; + NetworkedJobs.Add(networkedJob); + NewJobs.Add(networkedJob); + } + + private void OnJobTaken(Job job, bool viaLoadGame) + { + + } + + private void OnJobAbandoned(Job job) + { + + } + + private void OnJobCompleted(Job job) + { + + } + + private void OnJobExpired(Job job) + { + + } + + private void Server_OnTick(uint tick) + { + if (NewJobs.Count > 0) + { + NetworkLifecycle.Instance.Server.SendJobsCreatePacket(NetId, NewJobs.ToArray()); + NewJobs.Clear(); + } + } + + #region Client + public void AddJobs(JobData[] jobs) + { + foreach (JobData jobData in jobs) + { + NetworkLifecycle.Instance.Client.Log($"AddJobs() {jobData.ID}, {jobData.NetID}"); + + // Convert TaskNetworkData to Task objects + List tasks = new List(); + foreach (TaskNetworkData taskData in jobData.Tasks) + { + tasks.Add(taskData.ToTask()); + } + + // Create StationsChainData from ChainData + StationsChainData chainData = new StationsChainData( + jobData.ChainData.ChainOriginYardId, + jobData.ChainData.ChainDestinationYardId + ); + + // Create a new local Job + Job newJob = new Job( + tasks, + jobData.JobType, + jobData.TimeLimit, + jobData.InitialWage, + chainData, + jobData.ID, + jobData.RequiredLicenses + ); + + // Set additional properties + newJob.startTime = jobData.StartTime; + newJob.finishTime = jobData.FinishTime; + newJob.State = jobData.State; + + // Create a new NetworkedJob + NetworkedJob networkedJob = new GameObject($"NetworkedJob {newJob.ID}").AddComponent(); + networkedJob.Job = newJob; + NetworkedJobs.Add(networkedJob); + + // Start coroutine to update car plates + StartCoroutine(UpdateCarPlates(tasks, newJob.ID)); + + // Log the addition of the new job + Multiplayer.Log($"AddJobs() {newJob.ID} to NetworkedStationController {StationController.logicStation.ID}"); + } + } + #endregion + + #region common functions + public static IEnumerator UpdateCarPlates(List tasks, string jobId) + { + + List cars = new List(); + UpdateCarPlatesRecursive(tasks, jobId, ref cars); + + + if (cars != null) + { + Multiplayer.Log("NetworkedStation.UpdateCarPlates() Cars count: " + cars.Count); + + foreach (Car car in cars) + { + Multiplayer.Log("NetworkedStation.UpdateCarPlates() Car: " + car.ID); + + TrainCar trainCar = null; + int loopCtr = 0; + while (!NetworkedTrainCar.GetTrainCarFromTrainId(car.ID, out trainCar)) + { + loopCtr++; + if (loopCtr > 5000) + { + Multiplayer.Log("NetworkedStation.UpdateCarPlates() TimeOut"); + break; + } + + + yield return null; + } + + trainCar?.UpdateJobIdOnCarPlates(jobId); + } + } + } + private static void UpdateCarPlatesRecursive(List tasks, string jobId, ref List cars) + { + Multiplayer.Log("NetworkedStation.UpdateCarPlatesRecursive() Starting"); + + foreach (Task task in tasks) + { + if (task is WarehouseTask) + { + Multiplayer.Log("NetworkedStation.UpdateCarPlatesRecursive() WarehouseTask"); + cars = cars.Union(((WarehouseTask)task).cars).ToList(); + } + else if (task is TransportTask) + { + Multiplayer.Log("NetworkedStation.UpdateCarPlatesRecursive() TransportTask"); + cars = cars.Union(((TransportTask)task).cars).ToList(); + } + else if (task is SequentialTasks) + { + Multiplayer.Log("NetworkedStation.UpdateCarPlatesRecursive() SequentialTasks"); + List seqTask = new(); + + for (LinkedListNode node = ((SequentialTasks)task).tasks.First; node != null; node = node.Next) + { + Multiplayer.Log($"NetworkedStation.UpdateCarPlatesRecursive() SequentialTask Adding node"); + seqTask.Add(node.Value); + } + + Multiplayer.Log($"NetworkedStation.UpdateCarPlatesRecursive() SequentialTask Node Count:{seqTask.Count}"); + + Multiplayer.Log("NetworkedStation.UpdateCarPlatesRecursive() Calling UpdateCarPlates()"); + //drill down + UpdateCarPlatesRecursive(seqTask, jobId, ref cars); + Multiplayer.Log($"NetworkedStation.UpdateCarPlatesRecursive() SequentialTask RETURNED"); + } + else if (task is ParallelTasks) + { + //not implemented + Multiplayer.Log("NetworkedStation.UpdateCarPlatesRecursive() ParallelTasks"); + + Multiplayer.Log("NetworkedStation.UpdateCarPlatesRecursive() Calling UpdateCarPlates()"); + //drill down + UpdateCarPlatesRecursive(((ParallelTasks)task).tasks, jobId, ref cars); + } + else + { + throw new ArgumentException("NetworkedStation.UpdateCarPlatesRecursive() Unknown task type: " + task.GetType()); + } + } + + Multiplayer.Log("NetworkedStation.UpdateCarPlatesRecursive() Returning"); + } + #endregion +} diff --git a/Multiplayer/Components/StationComponentLookup.cs b/Multiplayer/Components/StationComponentLookup.cs index 3f30f62..689552e 100644 --- a/Multiplayer/Components/StationComponentLookup.cs +++ b/Multiplayer/Components/StationComponentLookup.cs @@ -5,7 +5,7 @@ using Multiplayer.Components.Networking.World; namespace Multiplayer.Components; - +/* public class StationComponentLookup : SingletonBehaviour { private readonly Dictionary stationToNetworkedStationController = new(); @@ -48,3 +48,4 @@ public bool StationControllerFromId(string stationId, out StationController stat return $"[{nameof(StationComponentLookup)}]"; } } +*/ diff --git a/Multiplayer/Networking/Data/JobData.cs b/Multiplayer/Networking/Data/JobData.cs index bf14034..6703cbb 100644 --- a/Multiplayer/Networking/Data/JobData.cs +++ b/Multiplayer/Networking/Data/JobData.cs @@ -1,73 +1,109 @@ +using System.Collections.Generic; using System.Linq; using DV.Logic.Job; +using DV.ThingTypes; using LiteNetLib.Utils; using Newtonsoft.Json; +using static DV.UI.ATutorialsMenuProvider; namespace Multiplayer.Networking.Data; public class JobData { - public byte JobType { get; set; } + public ushort NetID { get; set; } + public JobType JobType { get; set; } //serialise as byte public string ID { get; set; } - public TaskData[] Tasks { get; set; } + public TaskNetworkData[] Tasks { get; set; } public StationsChainDataData ChainData { get; set; } - public int RequiredLicenses { get; set; } + public JobLicenses RequiredLicenses { get; set; } //serialise as int public float StartTime { get; set; } public float FinishTime { get; set; } public float InitialWage { get; set; } - public byte State { get; set; } + public JobState State { get; set; } //serialise as byte public float TimeLimit { get; set; } - public static JobData FromJob(Job job) + public static JobData FromJob(ushort netID, Job job) { return new JobData { - JobType = (byte)job.jobType, + NetID = netID, + JobType = job.jobType, ID = job.ID, - Tasks = job.tasks.Select(x => TaskData.FromTask(x)).ToArray(), + Tasks = TaskNetworkDataFactory.ConvertTasks(job.tasks), ChainData = StationsChainDataData.FromStationData(job.chainData), - RequiredLicenses = (int)job.requiredLicenses, + RequiredLicenses = job.requiredLicenses, StartTime = job.startTime, FinishTime = job.finishTime, InitialWage = job.initialWage, - State = (byte)job.State, + State = job.State, TimeLimit = job.TimeLimit }; } public static void Serialize(NetDataWriter writer, JobData data) { - writer.Put(data.JobType); + Multiplayer.Log($"JobData.Serialize({data.ID}) NetID {data.NetID}"); + writer.Put(data.NetID); + Multiplayer.Log($"JobData.Serialize({data.ID}) JobType {(byte)data.JobType}, {data.JobType}"); + writer.Put((byte)data.JobType); + Multiplayer.Log($"JobData.Serialize({data.ID}) JobID {data.ID}"); writer.Put(data.ID); + + Multiplayer.Log($"JobData.Serialize({data.ID}) task length {data.Tasks.Length}"); + //task data writer.Put((byte)data.Tasks.Length); - foreach (var taskBeforeDataData in data.Tasks) - TaskData.SerializeTask(taskBeforeDataData, writer); + foreach (var task in data.Tasks) + { + Multiplayer.Log($"JobData.Serialize({data.ID}) TaskType {(byte)task.TaskType}, {task.TaskType}"); + + writer.Put((byte)task.TaskType); + task.Serialize(writer); + } + + Multiplayer.Log($"JobData.Serialize({data.ID}) calling StationsChainDataData.Serialize()"); StationsChainDataData.Serialize(writer, data.ChainData); - writer.Put(data.RequiredLicenses); + + Multiplayer.Log($"JobData.Serialize({data.ID}) RequiredLicenses {data.RequiredLicenses}"); + writer.Put((int)data.RequiredLicenses); + Multiplayer.Log($"JobData.Serialize({data.ID}) StartTime {data.StartTime}"); writer.Put(data.StartTime); + Multiplayer.Log($"JobData.Serialize({data.ID}) FinishTime {data.FinishTime}"); writer.Put(data.FinishTime); + Multiplayer.Log($"JobData.Serialize({data.ID}) InitialWage {data.InitialWage}"); writer.Put(data.InitialWage); - writer.Put(data.State); + Multiplayer.Log($"JobData.Serialize({data.ID}) State {(byte)data.State}, {data.State}"); + writer.Put((byte)data.State); + Multiplayer.Log($"JobData.Serialize({data.ID}) TimeLimit {data.TimeLimit}"); writer.Put(data.TimeLimit); Multiplayer.Log(JsonConvert.SerializeObject(data, Formatting.None)); } public static JobData Deserialize(NetDataReader reader) { - Multiplayer.Log("JobData.Deserialize()"); - var jobType = reader.GetByte(); - Multiplayer.Log("JobData.Deserialize() jobType: " + jobType); + Multiplayer.LogDebug(() => $"JobData.Deserialize(): [{string.Join(", ", reader.RawData?.Select(id => id.ToString()))}]"); + var netID = reader.GetUShort(); + Multiplayer.Log($"JobData.Deserialize() netID {netID}"); + var jobType = (JobType)reader.GetByte(); + Multiplayer.Log($"JobData.Deserialize() jobType {jobType}"); var id = reader.GetString(); - Multiplayer.Log("JobData.Deserialize() id: " + id); + Multiplayer.Log($"JobData.Deserialize() id {id}"); + var tasksLength = reader.GetByte(); - Multiplayer.Log("JobData.Deserialize() tasksLength: " + tasksLength); - var tasks = new TaskData[tasksLength]; + Multiplayer.Log($"JobData.Deserialize() tasksLength {tasksLength}"); + + var tasks = new TaskNetworkData[tasksLength]; for (int i = 0; i < tasksLength; i++) - tasks[i] = TaskData.DeserializeTask(reader); - //Multiplayer.Log("JobData.Deserialize() tasks: " + JsonConvert.SerializeObject(tasks, Formatting.None)); + { + var taskType = (TaskType)reader.GetByte(); + Multiplayer.Log($"JobData.Deserialize() taskType {taskType}"); + tasks[i] = TaskNetworkData.CreateTaskNetworkDataFromType(taskType); + tasks[i].Deserialize(reader); + } + var chainData = StationsChainDataData.Deserialize(reader); - //Multiplayer.Log("JobData.Deserialize() chainData: " + JsonConvert.SerializeObject(chainData, Formatting.Indented)); - var requiredLicenses = reader.GetInt(); + Multiplayer.Log($"JobData.Deserialize() chainData {chainData.ChainOriginYardId}, {chainData.ChainDestinationYardId}"); + + var requiredLicenses = (JobLicenses)reader.GetInt(); Multiplayer.Log("JobData.Deserialize() requiredLicenses: " + requiredLicenses); var startTime = reader.GetFloat(); Multiplayer.Log("JobData.Deserialize() startTime: " + startTime); @@ -75,25 +111,14 @@ public static JobData Deserialize(NetDataReader reader) Multiplayer.Log("JobData.Deserialize() finishTime: " + finishTime); var initialWage = reader.GetFloat(); Multiplayer.Log("JobData.Deserialize() initialWage: " + initialWage); - var state = reader.GetByte(); + var state = (JobState)reader.GetByte(); Multiplayer.Log("JobData.Deserialize() state: " + state); var timeLimit = reader.GetFloat(); - Multiplayer.Log(JsonConvert.SerializeObject(new JobData - { - JobType = jobType, - ID = id, - Tasks = tasks, - ChainData = chainData, - RequiredLicenses = requiredLicenses, - StartTime = startTime, - FinishTime = finishTime, - InitialWage = initialWage, - State = state, - TimeLimit = timeLimit - }, Formatting.None)); + Multiplayer.Log("JobData.Deserialize() timeLimit: " + timeLimit); return new JobData { + NetID = netID, JobType = jobType, ID = id, Tasks = tasks, @@ -106,6 +131,7 @@ public static JobData Deserialize(NetDataReader reader) TimeLimit = timeLimit }; } + } public struct StationsChainDataData diff --git a/Multiplayer/Networking/Data/TaskData.cs b/Multiplayer/Networking/Data/TaskData.cs deleted file mode 100644 index 30e6e28..0000000 --- a/Multiplayer/Networking/Data/TaskData.cs +++ /dev/null @@ -1,371 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using DV.Logic.Job; -using DV.ThingTypes; -using HarmonyLib; -using LiteNetLib.Utils; -using Newtonsoft.Json; - -namespace Multiplayer.Networking.Data; - -public abstract class TaskData -{ - public byte State { get; set; } - public float TaskStartTime { get; set; } - public float TaskFinishTime { get; set; } - public bool IsLastTask { get; set; } - public float TimeLimit { get; set; } - public byte TaskType { get; set; } - - - public static TaskData FromTask(Task task) - { - TaskData taskData = task switch - { - WarehouseTask warehouseTask => WarehouseTaskData.FromWarehouseTask(warehouseTask), - TransportTask transportTask => TransportTaskData.FromTransportTask(transportTask), - SequentialTasks sequentialTasks => SequentialTasksData.FromSequentialTask(sequentialTasks), - ParallelTasks parallelTasks => ParallelTasksData.FromParallelTask(parallelTasks), - _ => throw new ArgumentException("Unknown task type: " + task.GetType()) - }; - - taskData.State = (byte)task.state; - taskData.TaskStartTime = task.taskStartTime; - taskData.TaskFinishTime = task.taskFinishTime; - taskData.IsLastTask = task.IsLastTask; - taskData.TimeLimit = task.TimeLimit; - taskData.TaskType = (byte)task.InstanceTaskType; - - return taskData; - } - - public static Task ToTask(object data) - { - if (data is WarehouseTaskData) - { - var task = (WarehouseTaskData)data; - return WarehouseTaskData.ToWarehouseTask(task); - } - - if (data is TransportTaskData) - { - var task = (TransportTaskData)data; - return TransportTaskData.ToTransportTask(task); - } - - if (data is SequentialTasksData) - { - var task = (SequentialTasksData)data; - List tasks = new List(); - - foreach (TaskData taskBeforeDataData in task.Tasks) - tasks.Add(ToTask(taskBeforeDataData)); - - - return new SequentialTasks(tasks); - } - - if (data is ParallelTasksData) - { - var task = (ParallelTasksData)data; - List tasks = new List(); - - foreach (TaskData taskBeforeDataData in task.Tasks) - tasks.Add(ToTask(taskBeforeDataData)); - - - return new ParallelTasks(tasks); - } - - throw new ArgumentException("Unknown task type: " + data.GetType()); - } - - public static void SerializeTask(object data, NetDataWriter writer) - { - if (data is WarehouseTaskData) - { - var task = (WarehouseTaskData)data; - WarehouseTaskData.Serialize(writer, task); - return; - } - - if (data is TransportTaskData) - { - var task = (TransportTaskData)data; - TransportTaskData.Serialize(writer, task); - return; - } - - if (data is SequentialTasksData) - { - var task = (SequentialTasksData)data; - - SequentialTasksData.Serialize(writer, task); - - return; - } - - if (data is ParallelTasksData) - { - var task = (ParallelTasksData)data; - - ParallelTasksData.Serialize(writer, task); - - - return; - } - - throw new ArgumentException("Unknown task type: " + data.GetType()); - } - - public static TaskData DeserializeTask(NetDataReader reader) - { - TaskType taskType = (TaskType)reader.GetByte(); - Multiplayer.Log("Task type: " + taskType + ""); - - return taskType switch - { - DV.Logic.Job.TaskType.Warehouse => WarehouseTaskData.Deserialize(reader), - DV.Logic.Job.TaskType.Transport => TransportTaskData.Deserialize(reader), - DV.Logic.Job.TaskType.Sequential => SequentialTasksData.Deserialize(reader), - DV.Logic.Job.TaskType.Parallel => ParallelTasksData.Deserialize(reader), - _ => throw new ArgumentException("Unknown task type: " + taskType) - }; - } - - public static void Serialize(NetDataWriter writer, TaskData data) - { - writer.Put(data.TaskType); - writer.Put(data.State); - writer.Put(data.TaskStartTime); - writer.Put(data.TaskFinishTime); - writer.Put(data.IsLastTask); - writer.Put(data.TimeLimit); - writer.Put(data.TaskType); - } - - public static void Deserialize(NetDataReader reader, TaskData data) - { - data.State = reader.GetByte(); - data.TaskStartTime = reader.GetFloat(); - data.TaskFinishTime = reader.GetFloat(); - data.IsLastTask = reader.GetBool(); - data.TimeLimit = reader.GetFloat(); - data.TaskType = reader.GetByte(); - } -} - -public class ParallelTasksData : TaskData -{ - public TaskData[] Tasks { get; set; } - - public static ParallelTasksData FromParallelTask(ParallelTasks task) - { - return new ParallelTasksData - { - Tasks = task.tasks.Select(x => FromTask(x)).ToArray() - }; - } - - public static void Serialize(NetDataWriter writer, ParallelTasksData data) - { - TaskData.Serialize(writer, data); - writer.Put((byte)data.Tasks.Length); - foreach (var taskBeforeDataData in data.Tasks) - SerializeTask(taskBeforeDataData, writer); - } - - public static ParallelTasksData Deserialize(NetDataReader reader) - { - var parallelTask = new ParallelTasksData(); - Deserialize(reader, parallelTask); - var tasksLength = reader.GetByte(); - var tasks = new TaskData[tasksLength]; - for (int i = 0; i < tasksLength; i++) - tasks[i] = DeserializeTask(reader); - parallelTask.Tasks = tasks; - return parallelTask; - } -} - -public class SequentialTasksData : TaskData -{ - public TaskData[] Tasks { get; set; } - - - public static SequentialTasksData FromSequentialTask(SequentialTasks task) - { - return new SequentialTasksData - { - Tasks = task.tasks.Select(x => FromTask(x)).ToArray(), - }; - } - - public static void Serialize(NetDataWriter writer, SequentialTasksData data) - { - TaskData.Serialize(writer, data); - writer.Put((byte)data.Tasks.Length); - foreach (var taskBeforeDataData in data.Tasks) - SerializeTask(taskBeforeDataData, writer); - } - - public static SequentialTasksData Deserialize(NetDataReader reader) - { - var sequentialTask = new SequentialTasksData(); - Deserialize(reader, sequentialTask); - var tasksLength = reader.GetByte(); - var tasks = new TaskData[tasksLength]; - for (int i = 0; i < tasksLength; i++) - tasks[i] = DeserializeTask(reader); - sequentialTask.Tasks = tasks; - return sequentialTask; - } -} - -public class WarehouseTaskData : TaskData -{ - public string[] Cars { get; set; } - public byte WarehouseTaskType { get; set; } - public string WarehouseMachine { get; set; } - public CargoType CargoType { get; set; } - public float CargoAmount { get; set; } - public bool ReadyForMachine { get; set; } - - public static WarehouseTaskData FromWarehouseTask(WarehouseTask task) - { - return new WarehouseTaskData - { - Cars = task.cars.Select(x => x.ID).ToArray(), - WarehouseTaskType = (byte)task.warehouseTaskType, - WarehouseMachine = task.warehouseMachine.ID, - CargoType = task.cargoType, - CargoAmount = task.cargoAmount, - ReadyForMachine = task.readyForMachine - }; - } - - public static WarehouseTask ToWarehouseTask(WarehouseTaskData data) - { - return new WarehouseTask( - CarSpawner.Instance.allCars.FindAll(x => data.Cars.Contains(x.ID)).Select(x => x.logicCar).ToList(), - (WarehouseTaskType)data.WarehouseTaskType, - JobSaveManager.Instance.GetWarehouseMachineWithId(data.WarehouseMachine), - (CargoType)data.CargoType, - data.CargoAmount - ); - } - - public static void Serialize(NetDataWriter writer, WarehouseTaskData data) - { - TaskData.Serialize(writer, data); - writer.PutArray(data.Cars); - writer.Put(data.WarehouseTaskType); - writer.Put(data.WarehouseMachine); - writer.Put((int)data.CargoType); - writer.Put(data.CargoAmount); - writer.Put(data.ReadyForMachine); - } - - public static WarehouseTaskData Deserialize(NetDataReader reader) - { - WarehouseTaskData data = new WarehouseTaskData(); - Deserialize(reader, data); - data.Cars = reader.GetStringArray(); - data.WarehouseTaskType = reader.GetByte(); - data.WarehouseMachine = reader.GetString(); - data.CargoType = (CargoType)reader.GetInt(); - data.CargoAmount = reader.GetFloat(); - data.ReadyForMachine = reader.GetBool(); - - return data; - } -} - -public class TransportTaskData : TaskData -{ - public string[] Cars { get; set; } - public string StartingTrack { get; set; } - public string DestinationTrack { get; set; } - public CargoType[] TransportedCargoPerCar { get; set; } - public bool CouplingRequiredAndNotDone { get; set; } - public bool AnyHandbrakeRequiredAndNotDone { get; set; } - - public static TransportTaskData FromTransportTask(TransportTask task) - { - Multiplayer.Log("Cars: " + task.cars.Select(x => x.ID).ToArray().Join()); - Multiplayer.Log("FromTransportTask.TransportedCargoPerCar: " + task.transportedCargoPerCar?.Select(x => (int)x).ToArray().Join() + "\r\n\t"+ task.transportedCargoPerCar?.ToArray().Join()); - - return new TransportTaskData - { - Cars = task.cars.Select(x => x.ID).ToArray(), - StartingTrack = task.startingTrack.ID.RailTrackGameObjectID, - DestinationTrack = task.destinationTrack.ID.RailTrackGameObjectID, - TransportedCargoPerCar = task.transportedCargoPerCar?.ToArray(), - CouplingRequiredAndNotDone = task.couplingRequiredAndNotDone, - AnyHandbrakeRequiredAndNotDone = task.anyHandbrakeRequiredAndNotDone - }; - } - - public static TransportTask ToTransportTask(TransportTaskData data) - { - return new TransportTask( - CarSpawner.Instance.allCars.FindAll(x => data.Cars.Contains(x.ID)).Select(x => x.logicCar).ToList(), - RailTrackRegistry.Instance.GetTrackWithName(data.DestinationTrack).logicTrack, - RailTrackRegistry.Instance.GetTrackWithName(data.StartingTrack).logicTrack, - data.TransportedCargoPerCar?.ToList() - ); - } - - public static void Serialize(NetDataWriter writer, TransportTaskData data) - { - TaskData.Serialize(writer, data); - writer.PutArray(data.Cars); - writer.Put(data.StartingTrack); - writer.Put(data.DestinationTrack); - - //transport cargo data exists? - writer.Put(data.TransportedCargoPerCar != null); - - //write data if it exists - if (data.TransportedCargoPerCar != null) - { - writer.PutArray(data.TransportedCargoPerCar?.Select(x => (int)x).ToArray()); - // transportedCargoPerCar?.Select(x => (int)x).ToArray() - Multiplayer.Log("Serialising cargo: " + (int)data.TransportedCargoPerCar[0]); - } - - writer.Put(data.CouplingRequiredAndNotDone); - writer.Put(data.AnyHandbrakeRequiredAndNotDone); - } - - public static TransportTaskData Deserialize(NetDataReader reader) - { - Multiplayer.Log("TransportTaskData.Deserialize"); - TransportTaskData data = new TransportTaskData(); - Multiplayer.Log("1"); - Deserialize(reader, data); - Multiplayer.Log("2"); - data.Cars = reader.GetStringArray(); - Multiplayer.Log("3"); - data.StartingTrack = reader.GetString(); - Multiplayer.Log("4"); - data.DestinationTrack = reader.GetString(); - Multiplayer.Log("5"); - - if (reader.GetBool()) - { - //transport data exists - data.TransportedCargoPerCar = reader.GetArray(sizeof(int))?.Select(x => (CargoType)x).ToArray(); - } - - Multiplayer.Log("TransportedCargoPerCar: " + data.TransportedCargoPerCar?.Select(x => (int)x).ToArray().Join() + "\r\n\t" + data.TransportedCargoPerCar?.ToArray().Join()); - Multiplayer.Log("6"); - data.CouplingRequiredAndNotDone = reader.GetBool(); - Multiplayer.Log("7"); - data.AnyHandbrakeRequiredAndNotDone = reader.GetBool(); - //Multiplayer.Log(JsonConvert.SerializeObject(data, Formatting.Indented)); - - return data; - } -} diff --git a/Multiplayer/Networking/Data/TaskNetworkData.cs b/Multiplayer/Networking/Data/TaskNetworkData.cs new file mode 100644 index 0000000..fbe90b1 --- /dev/null +++ b/Multiplayer/Networking/Data/TaskNetworkData.cs @@ -0,0 +1,458 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using DV.Logic.Job; +using DV.ThingTypes; +using LiteNetLib.Utils; +using Multiplayer.Components.Networking.Train; + + +namespace Multiplayer.Networking.Data; + +public abstract class TaskNetworkData +{ + public TaskState State { get; set; } + public float TaskStartTime { get; set; } + public float TaskFinishTime { get; set; } + public bool IsLastTask { get; set; } + public float TimeLimit { get; set; } + public TaskType TaskType { get; set; } + + public abstract void Serialize(NetDataWriter writer); + public abstract void Deserialize(NetDataReader reader); + public abstract Task ToTask(); + + public static TaskNetworkData CreateTaskNetworkDataFromType(TaskType taskType) + { + return taskType switch + { + TaskType.Warehouse => new WarehouseTaskData(), + TaskType.Transport => new TransportTaskData(), + TaskType.Sequential => new SequentialTasksData(), + TaskType.Parallel => new ParallelTasksData(), + _ => throw new ArgumentException($"Unknown task type: {taskType}") + }; + } + + public static TaskType GetTaskType(Task task) + { + return task switch + { + WarehouseTask => TaskType.Warehouse, + TransportTask => TaskType.Transport, + SequentialTasks => TaskType.Sequential, + ParallelTasks => TaskType.Parallel, + _ => throw new ArgumentException($"Unknown task type: {task.GetType()}") + }; + } +} +public abstract class TaskNetworkData : TaskNetworkData where T : TaskNetworkData +{ + public abstract T FromTask(Task task); + + protected void SerializeCommon(NetDataWriter writer) + { + Multiplayer.Log($"TaskNetworkData.SerializeCommon() State {(byte)State}, {State}"); + writer.Put((byte)State); + Multiplayer.Log($"TaskNetworkData.SerializeCommon() TaskStartTime {TaskStartTime}"); + writer.Put(TaskStartTime); + Multiplayer.Log($"TaskNetworkData.SerializeCommon() TaskFinishTime {TaskFinishTime}"); + writer.Put(TaskFinishTime); + Multiplayer.Log($"TaskNetworkData.SerializeCommon() IsLastTask {IsLastTask}"); + writer.Put(IsLastTask); + Multiplayer.Log($"TaskNetworkData.SerializeCommon() TimeLimit {TimeLimit}"); + writer.Put(TimeLimit); + Multiplayer.Log($"TaskNetworkData.SerializeCommon() TaskType {(byte)TaskType}, {TaskType}"); + writer.Put((byte)TaskType); + } + + protected void DeserializeCommon(NetDataReader reader) + { + State = (TaskState)reader.GetByte(); + Multiplayer.Log($"TaskNetworkData.DeserializeCommon() State {State}"); + TaskStartTime = reader.GetFloat(); + Multiplayer.Log($"TaskNetworkData.DeserializeCommon() TaskStartTime {TaskStartTime}"); + TaskFinishTime = reader.GetFloat(); + Multiplayer.Log($"TaskNetworkData.DeserializeCommon() TaskFinishTime {TaskFinishTime}"); + IsLastTask = reader.GetBool(); + Multiplayer.Log($"TaskNetworkData.DeserializeCommon() IsLastTask {IsLastTask}"); + TimeLimit = reader.GetFloat(); + Multiplayer.Log($"TaskNetworkData.DeserializeCommon() TimeLimit {TimeLimit}"); + TaskType = (TaskType)reader.GetByte(); + Multiplayer.Log($"TaskNetworkData.DeserializeCommon() TaskType {TaskType}"); + } +} + +public static class TaskNetworkDataFactory +{ + private static readonly Dictionary> TypeToTaskNetworkData = new(); + private static readonly Dictionary> EnumToTaskNetworkData = new(); + + + //Allow new task types to be registered - will help with mods such as passenger mod + public static void RegisterTaskType(TaskType taskType, Func converter) + where TGameTask : Task + { + TypeToTaskNetworkData[typeof(TGameTask)] = task => converter((TGameTask)task); + EnumToTaskNetworkData[taskType] = task => converter((TGameTask)task); + } + + public static TaskNetworkData ConvertTask(Task task) + { + Multiplayer.Log($"TaskNetworkDataFactory.ConvertTask: Processing task of type {task.GetType()}"); + if (TypeToTaskNetworkData.TryGetValue(task.GetType(), out var converter)) + { + return converter(task); + } + throw new ArgumentException($"Unknown task type: {task.GetType()}"); + } + + public static TaskNetworkData[] ConvertTasks(IEnumerable tasks) + { + return tasks.Select(ConvertTask).ToArray(); + } + + public static TaskNetworkData ConvertTask(TaskType type) + { + if (EnumToTaskNetworkData.TryGetValue(type, out var creator)) + { + return creator(null); // Passing null as we're just creating an empty instance + } + throw new ArgumentException($"Unknown task type: {type}"); + } + + // Register base task types + static TaskNetworkDataFactory() + { + RegisterTaskType(TaskType.Warehouse, task => new WarehouseTaskData().FromTask(task)); + RegisterTaskType(TaskType.Transport, task => new TransportTaskData().FromTask(task)); + RegisterTaskType(TaskType.Sequential, task => new SequentialTasksData().FromTask(task)); + RegisterTaskType(TaskType.Parallel, task => new ParallelTasksData().FromTask(task)); + } +} + + +public class WarehouseTaskData : TaskNetworkData +{ + public ushort[] CarNetIDs { get; set; } + public WarehouseTaskType WarehouseTaskType { get; set; } + public string WarehouseMachine { get; set; } + public CargoType CargoType { get; set; } + public float CargoAmount { get; set; } + public bool ReadyForMachine { get; set; } + + public override void Serialize(NetDataWriter writer) + { + SerializeCommon(writer); + writer.PutArray(CarNetIDs); + writer.Put((byte)WarehouseTaskType); + writer.Put(WarehouseMachine); + writer.Put((int)CargoType); + writer.Put(CargoAmount); + writer.Put(ReadyForMachine); + } + + public override void Deserialize(NetDataReader reader) + { + DeserializeCommon(reader); + CarNetIDs = reader.GetUShortArray(); + WarehouseTaskType = (WarehouseTaskType)reader.GetByte(); + WarehouseMachine = reader.GetString(); + CargoType = (CargoType)reader.GetInt(); + CargoAmount = reader.GetFloat(); + ReadyForMachine = reader.GetBool(); + } + + public override WarehouseTaskData FromTask(Task task) + { + if (task is not WarehouseTask warehouseTask) + throw new ArgumentException("Task is not a WarehouseTask"); + + CarNetIDs = warehouseTask.cars + .Select(car => NetworkedTrainCar.GetFromTrainId(car.ID, out var networkedTrainCar) + ? networkedTrainCar.NetId + : (ushort)0) + .ToArray(); + WarehouseTaskType = warehouseTask.warehouseTaskType; + WarehouseMachine = warehouseTask.warehouseMachine.ID; + CargoType = warehouseTask.cargoType; + CargoAmount = warehouseTask.cargoAmount; + ReadyForMachine = warehouseTask.readyForMachine; + + return this; + } + + public override Task ToTask() + { + + List cars = CarNetIDs + .Select(netId => NetworkedTrainCar.GetTrainCar(netId, out TrainCar trainCar) ? trainCar : null) + .Where(car => car != null) + .Select(car =>car.logicCar) + .ToList(); + + WarehouseTask newWareTask = new WarehouseTask( + cars, + WarehouseTaskType, + JobSaveManager.Instance.GetWarehouseMachineWithId(WarehouseMachine), + CargoType, + CargoAmount + ); + + newWareTask.readyForMachine = ReadyForMachine; + + return newWareTask; + } +} + +public class TransportTaskData : TaskNetworkData +{ + public ushort[] CarNetIDs { get; set; } + public string StartingTrack { get; set; } + public string DestinationTrack { get; set; } + public CargoType[] TransportedCargoPerCar { get; set; } + public bool CouplingRequiredAndNotDone { get; set; } + public bool AnyHandbrakeRequiredAndNotDone { get; set; } + + public override void Serialize(NetDataWriter writer) + { + SerializeCommon(writer); + Multiplayer.LogDebug(() => $"TransportTaskData.Serialize() CarNetIDs count: {CarNetIDs.Length}, Values: [{string.Join(", ", CarNetIDs?.Select(id => id.ToString()))}]"); + //Multiplayer.LogDebug(() => $"TransportTaskData.Serialize() raw before: [{string.Join(", ", writer.Data?.Select(id => id.ToString()))}]"); + + Multiplayer.Log($"TaskNetworkData.Serialize() CarNetIDs.Length {CarNetIDs.Length}"); + writer.PutArray(CarNetIDs); + + //Multiplayer.LogDebug(() => $"TransportTaskData.Serialize() raw after: [{string.Join(", ", writer.Data?.Select(id => id.ToString()))}]"); + + Multiplayer.Log($"TaskNetworkData.Serialize() StartingTrack {StartingTrack}"); + writer.Put(StartingTrack); + Multiplayer.Log($"TaskNetworkData.Serialize() DestinationTrack {DestinationTrack}"); + writer.Put(DestinationTrack); + + Multiplayer.Log($"TaskNetworkData.Serialize() TransportedCargoPerCar != null {TransportedCargoPerCar != null}"); + writer.Put(TransportedCargoPerCar != null); + + if (TransportedCargoPerCar != null) + { + Multiplayer.Log($"TaskNetworkData.Serialize() TransportedCargoPerCar.PutArray() length: {TransportedCargoPerCar.Length}"); + writer.PutArray(TransportedCargoPerCar.Select(x => (int)x).ToArray()); + } + + Multiplayer.Log($"TaskNetworkData.Serialize() CouplingRequiredAndNotDone {CouplingRequiredAndNotDone}"); + writer.Put(CouplingRequiredAndNotDone); + Multiplayer.Log($"TaskNetworkData.Serialize() AnyHandbrakeRequiredAndNotDone {AnyHandbrakeRequiredAndNotDone}"); + writer.Put(AnyHandbrakeRequiredAndNotDone); + } + + public override void Deserialize(NetDataReader reader) + { + DeserializeCommon(reader); + + + int idCount = reader.GetInt(); + Multiplayer.Log($"TaskNetworkData.Deserialize() CarNetIDs.Length {idCount}"); + CarNetIDs = new ushort[idCount]; + + //Multiplayer.LogDebug(() => $" {idCount} raw before: [{string.Join(", ", reader.RawData?.Select(id => id.ToString()))}]"); + + for (int i = 0; i < idCount; i++) + { + CarNetIDs[i] = reader.GetUShort(); + Multiplayer.Log($"TaskNetworkData.Deserialize() CarNetIDs[{i}] {CarNetIDs[i]}"); + } + + Multiplayer.LogDebug(() => $"TransportTaskData.Deserialize() CarNetIDs count: {CarNetIDs.Length}, Values: [{string.Join(", ", CarNetIDs?.Select(id => id.ToString()))}]"); + + StartingTrack = reader.GetString(); + Multiplayer.Log($"TaskNetworkData.Deserialize() StartingTrack {StartingTrack}"); + DestinationTrack = reader.GetString(); + Multiplayer.Log($"TaskNetworkData.Deserialize() DestinationTrack {DestinationTrack}"); + + if (reader.GetBool()) + { + Multiplayer.Log($"TaskNetworkData.Deserialize() TransportedCargoPerCar != null True"); + TransportedCargoPerCar = reader.GetIntArray().Select(x => (CargoType)x).ToArray(); + } + else + { + Multiplayer.Log($"TaskNetworkData.Deserialize() TransportedCargoPerCar != null False"); + } + CouplingRequiredAndNotDone = reader.GetBool(); + Multiplayer.Log($"TaskNetworkData.Deserialize() CouplingRequiredAndNotDone {CouplingRequiredAndNotDone}"); + AnyHandbrakeRequiredAndNotDone = reader.GetBool(); + Multiplayer.Log($"TaskNetworkData.Deserialize() AnyHandbrakeRequiredAndNotDone {AnyHandbrakeRequiredAndNotDone}"); + } + + public override TransportTaskData FromTask(Task task) + { + if (task is not TransportTask transportTask) + throw new ArgumentException("Task is not a TransportTask"); + + Multiplayer.LogDebug(() => $"TransportTaskData.FromTask() CarNetIDs count: {transportTask.cars.Count()}, Values: [{string.Join(", ", transportTask.cars.Select(car => car.ID))}]"); + CarNetIDs = transportTask.cars + .Select(car => NetworkedTrainCar.GetFromTrainId(car.ID, out var networkedTrainCar) + ? networkedTrainCar.NetId + : (ushort)0) + .ToArray(); + + Multiplayer.LogDebug(() => $"TransportTaskData.FromTask() after CarNetIDs count: {CarNetIDs.Length}, Values: [{string.Join(", ", CarNetIDs.Select(id => id.ToString()))}]"); + + StartingTrack = transportTask.startingTrack.ID.RailTrackGameObjectID; + DestinationTrack = transportTask.destinationTrack.ID.RailTrackGameObjectID; + TransportedCargoPerCar = transportTask.transportedCargoPerCar?.ToArray(); + CouplingRequiredAndNotDone = transportTask.couplingRequiredAndNotDone; + AnyHandbrakeRequiredAndNotDone = transportTask.anyHandbrakeRequiredAndNotDone; + + return this; + } + + public override Task ToTask() + { + Multiplayer.LogDebug(() => $"TransportTaskData.ToTask() CarNetIDs !null {CarNetIDs != null}, count: {CarNetIDs?.Length}"); + + List cars = CarNetIDs + .Select(netId => NetworkedTrainCar.GetTrainCar(netId, out TrainCar trainCar) ? trainCar.logicCar : null) + .Where(car => car != null) + .ToList(); + + return new TransportTask( + cars, + RailTrackRegistry.Instance.GetTrackWithName(DestinationTrack).logicTrack, + RailTrackRegistry.Instance.GetTrackWithName(StartingTrack).logicTrack, + TransportedCargoPerCar?.ToList() + ); + } +} + +public class SequentialTasksData : TaskNetworkData +{ + public TaskNetworkData[] Tasks { get; set; } + public byte CurrentTaskIndex { get; set; } + + public override void Serialize(NetDataWriter writer) + { + Multiplayer.Log($"SequentialTasksData.Serialize({writer != null})"); + + SerializeCommon(writer); + + Multiplayer.Log($"SequentialTasksData.Serialize() {Tasks.Length}"); + + writer.Put((byte)Tasks.Length); + foreach (var task in Tasks) + { + Multiplayer.Log($"SequentialTasksData.Serialize() {task.TaskType} {task.GetType()}"); + writer.Put((byte)task.TaskType); + task.Serialize(writer); + } + + writer.Put(CurrentTaskIndex); + } + + public override void Deserialize(NetDataReader reader) + { + DeserializeCommon(reader); + var tasksLength = reader.GetByte(); + Tasks = new TaskNetworkData[tasksLength]; + for (int i = 0; i < tasksLength; i++) + { + var taskType = (TaskType)reader.GetByte(); + Tasks[i] = TaskNetworkDataFactory.ConvertTask(taskType); + Tasks[i].Deserialize(reader); + } + + CurrentTaskIndex = reader.GetByte(); + + } + + public override SequentialTasksData FromTask(Task task) + { + if (task is not SequentialTasks sequentialTasks) + throw new ArgumentException("Task is not a SequentialTasks"); + + Multiplayer.Log($"SequentialTasksData.FromTask() {sequentialTasks.tasks.Count}"); + + Tasks = TaskNetworkDataFactory.ConvertTasks(sequentialTasks.tasks); + + bool found=false; + + CurrentTaskIndex = 0; + foreach(Task subTask in sequentialTasks.tasks) + { + if(subTask == sequentialTasks.currentTask.Value) + { + found = true; + break; + } + CurrentTaskIndex++; + } + + if (!found) + CurrentTaskIndex = byte.MaxValue; + + return this; + } + + public override Task ToTask() + { + List tasks = new List(); + + foreach (var task in Tasks) + { + Multiplayer.LogDebug(() => $"SequentialTask.ToTask() task not null: {task != null}"); + + tasks.Add(task.ToTask()); + } + + SequentialTasks newSeqTask = new SequentialTasks(Tasks.Select(t => t.ToTask()).ToList()); + + if(CurrentTaskIndex <= newSeqTask.tasks.Count()) + newSeqTask.currentTask = new LinkedListNode(newSeqTask.tasks.ToArray()[CurrentTaskIndex]); + + return newSeqTask; + } +} + +public class ParallelTasksData : TaskNetworkData +{ + public TaskNetworkData[] Tasks { get; set; } + + public override void Serialize(NetDataWriter writer) + { + SerializeCommon(writer); + writer.Put((byte)Tasks.Length); + foreach (var task in Tasks) + { + writer.Put((byte)task.TaskType); + task.Serialize(writer); + } + } + + public override void Deserialize(NetDataReader reader) + { + DeserializeCommon(reader); + var tasksLength = reader.GetByte(); + Tasks = new TaskNetworkData[tasksLength]; + for (int i = 0; i < tasksLength; i++) + { + var taskType = (TaskType)reader.GetByte(); + Tasks[i] = TaskNetworkDataFactory.ConvertTask(taskType); + Tasks[i].Deserialize(reader); + } + } + + public override ParallelTasksData FromTask(Task task) + { + if (task is not ParallelTasks parallelTasks) + throw new ArgumentException("Task is not a ParallelTasks"); + + Tasks = TaskNetworkDataFactory.ConvertTasks(parallelTasks.tasks); + + return this; + } + + public override Task ToTask() + { + return new ParallelTasks(Tasks.Select(t => t.ToTask()).ToList()); + } +} diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index fdfada4..8cc47cc 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -37,6 +37,8 @@ using UnityModManagerNet; using Object = UnityEngine.Object; using System.Linq; +using Multiplayer.Networking.Packets.Serverbound.Train; +using static Multiplayer.Networking.Packets.Clientbound.World.ClientBoundStationControllerLookupPacket; namespace Multiplayer.Networking.Listeners; @@ -95,6 +97,7 @@ protected override void Subscribe() netPacketProcessor.SubscribeReusable(OnClientboundRemoveLoadingScreen); netPacketProcessor.SubscribeReusable(OnClientboundTimeAdvancePacket); netPacketProcessor.SubscribeReusable(OnClientboundRailwayStatePacket); + netPacketProcessor.SubscribeReusable(OnClientBoundStationControllerLookupPacket); netPacketProcessor.SubscribeReusable(OnCommonChangeJunctionPacket); netPacketProcessor.SubscribeReusable(OnCommonRotateTurntablePacket); netPacketProcessor.SubscribeReusable(OnClientboundSpawnTrainCarPacket); @@ -122,9 +125,8 @@ protected override void Subscribe() netPacketProcessor.SubscribeReusable(OnClientboundLicenseAcquiredPacket); netPacketProcessor.SubscribeReusable(OnClientboundGarageUnlockPacket); netPacketProcessor.SubscribeReusable(OnClientboundDebtStatusPacket); - netPacketProcessor.SubscribeReusable(OnClientboundJobsPacket); - netPacketProcessor.SubscribeReusable(OnClientboundJobCreatePacket); - netPacketProcessor.SubscribeReusable(OnClientboundJobTakeResponsePacket); + netPacketProcessor.SubscribeReusable(OnClientboundJobsCreatePacket); + //netPacketProcessor.SubscribeReusable(OnClientboundJobTakeResponsePacket); netPacketProcessor.SubscribeReusable(OnCommonChatPacket); } @@ -381,7 +383,42 @@ private void OnClientboundTimeAdvancePacket(ClientboundTimeAdvancePacket packet) TimeAdvance.AdvanceTime(packet.amountOfTimeToSkipInSeconds); } - private void OnClientboundRailwayStatePacket(ClientboundRailwayStatePacket packet) + //Force stations to be mapped to same netId across all clients and server - probably should implement for junctions, etc. + private void OnClientBoundStationControllerLookupPacket(ClientBoundStationControllerLookupPacket packet) + { + + if (packet == null) + { + LogError("OnClientBoundStationControllerLookupPacket received null packet"); + return; + } + + if (packet.NetID == null || packet.StationID == null) + { + LogError($"OnClientBoundStationControllerLookupPacket received packet with null arrays: NetID is null: {packet.NetID == null}, StationID is null: {packet.StationID == null}"); + return; + } + + + for (int i = 0; i < packet.NetID.Length; i++) + { + if (!NetworkedStationController.GetFromStationId(packet.StationID[i], out NetworkedStationController netStationCont)) + { + LogError($"OnClientBoundStationControllerLookupPacket() could not find station: {packet.StationID[i]}"); + } + else if (packet.NetID[i] > 0) + { + netStationCont.NetId = packet.NetID[i]; + } + else + { + LogError($"OnClientBoundStationControllerLookupPacket() station: {packet.StationID[i]} mapped to NetID 0"); + } + } + } + + + private void OnClientboundRailwayStatePacket(ClientboundRailwayStatePacket packet) { for (int i = 0; i < packet.SelectedJunctionBranches.Length; i++) { @@ -701,97 +738,24 @@ private void OnCommonChatPacket(CommonChatPacket packet) } - private void OnClientboundJobCreatePacket(ClientboundJobCreatePacket packet) + private void OnClientboundJobsCreatePacket(ClientboundJobsCreatePacket packet) { - Multiplayer.Log($"Received job packet. Job ID:{packet.job.ID}"); + Multiplayer.Log($"OnClientboundJobCreatePacket() for station {packet.StationNetId}, containing {packet.Jobs.Length}"); if (NetworkLifecycle.Instance.IsHost()) return; - List tasks = new List(); - foreach (Data.TaskData taskBeforeDataData in packet.job.Tasks) - tasks.Add(Data.TaskData.ToTask(taskBeforeDataData)); - - StationsChainDataData chainData = packet.job.ChainData; - - Job newJob = new Job( - tasks, - (JobType)packet.job.JobType, - packet.job.TimeLimit, - packet.job.InitialWage, - new StationsChainData(chainData.ChainOriginYardId, chainData.ChainDestinationYardId), - packet.job.ID, - (JobLicenses)packet.job.RequiredLicenses - ); - - //NetworkedJob netJob = NetworkedJob.AddJob(packet.stationId, newJob); - //netJob.NetId = packet.netId; - - //Find the station - StationController station; - if(!StationComponentLookup.Instance.StationControllerFromId(packet.stationId, out station)) + if(!NetworkedStationController.Get(packet.StationNetId, out NetworkedStationController networkedStationController)) { - Multiplayer.LogWarning($"OnClientboundJobCreatePacket Could not get station for stationId: {packet.stationId}"); + LogError($"OnClientboundJobCreatePacket() {packet.StationNetId} does not exist!"); return; } - //create a new game object - NetworkedJob netJob = station.gameObject.AddComponent(); - if (netJob != null) - { - netJob.job = newJob; - netJob.stationID = packet.stationId; - netJob.NetId = packet.netId; - } - - } - private void OnClientboundJobsPacket(ClientboundJobsPacket packet) - { - Multiplayer.Log($"Received job packet. Job count:{packet.Jobs.Count()}"); - - if (NetworkLifecycle.Instance.IsHost()) - return; - - if (!StationComponentLookup.Instance.StationControllerFromId(packet.stationId, out StationController station)) - { - LogError("Received job packet but couldn't find station!"); - return; - } + networkedStationController.AddJobs(packet.Jobs); - for (int i=0;i < packet.Jobs.Count(); i++) - { - JobData job = packet.Jobs[i]; - ushort netId = packet.netIds[i]; - - var tasks = new List(); - foreach (Data.TaskData taskBeforeDataData in job.Tasks) - tasks.Add(Data.TaskData.ToTask(taskBeforeDataData)); - - StationsChainDataData chainData = job.ChainData; - - Job newJob = new Job( - tasks, - (JobType)job.JobType, - job.TimeLimit, - job.InitialWage, - new StationsChainData(chainData.ChainOriginYardId, chainData.ChainDestinationYardId), - job.ID, - (JobLicenses)job.RequiredLicenses - ); - - Multiplayer.Log($"Attempting to add Job with ID {newJob.ID} to station.");//\r\nExisting jobs are: {station.logicStation.availableJobs.Select(x=>x.ID + "\r\n\t").ToArray().Join()}\r\nDoes the Job already exist in station? {station.logicStation.availableJobs.Where(x => x.ID == newJob.ID).Count() > 0}"); - - //create a new game object - NetworkedJob netJob = station.gameObject.AddComponent(); - if (netJob != null) - { - netJob.job = newJob; - netJob.stationID = packet.stationId; - netJob.NetId = netId; - } - } } + /* private void OnClientboundJobTakeResponsePacket(ClientboundJobTakeResponsePacket packet) { Multiplayer.Log($"OnClientboundJobTakeResponsePacket jobId: {packet.netId}, Status: {packet.granted}"); @@ -813,6 +777,7 @@ private void OnClientboundJobTakeResponsePacket(ClientboundJobTakeResponsePacket networkedJob.jobValidator = null; networkedJob.jobOverview = null; } + */ #endregion @@ -836,7 +801,7 @@ private void SendReadyPacket() public void SendPlayerPosition(Vector3 position, Vector3 moveDir, float rotationY, ushort carId, bool isJumping, bool isOnCar, bool reliable) { - Multiplayer.LogDebug(() => $"SendPlayerPosition({position}, {moveDir}, {rotationY}, {carId}, {isJumping}, {isOnCar})"); + //Multiplayer.LogDebug(() => $"SendPlayerPosition({position}, {moveDir}, {rotationY}, {carId}, {isJumping}, {isOnCar})"); SendPacketToServer(new ServerboundPlayerPositionPacket { @@ -848,19 +813,6 @@ public void SendPlayerPosition(Vector3 position, Vector3 moveDir, float rotation }, reliable ? DeliveryMethod.ReliableOrdered : DeliveryMethod.Sequenced); } - //public void SendPlayerCar(ushort carId,Vector3 position, Vector3 moveDir, float rotationY, bool isJumping) - //{ - // Multiplayer.LogDebug(() => $"SendPlayerCar({carId}, {position}, {moveDir}, {rotationY}, {isJumping})"); - // SendPacketToServer(new ServerboundPlayerCarPacket - // { - // CarId = carId, - // Position = position, - // MoveDir = moveDir, - // RotationY=rotationY, - - // }, DeliveryMethod.ReliableOrdered); - //} - public void SendTimeAdvance(float amountOfTimeToSkipInSeconds) { SendPacketToServer(new ServerboundTimeAdvancePacket diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index 433a642..c93bf3a 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -29,8 +29,8 @@ using UnityEngine; using UnityModManagerNet; using System.Net; -using static DV.UI.ATutorialsMenuProvider; -using HarmonyLib; +using Multiplayer.Networking.Packets.Serverbound.Train; + namespace Multiplayer.Networking.Listeners; @@ -386,11 +386,11 @@ public void SendDebtStatus(bool hasDebt) }, DeliveryMethod.ReliableUnordered, selfPeer); } - //public void SendJobCreatePacket(NetworkedJob job) - //{ - // Multiplayer.Log("Sending JobCreatePacket with netId: " + job.NetId + ", Job ID: " + job.job.ID); - // SendPacketToAll(ClientboundJobCreatePacket.FromNetworkedJob(job),DeliveryMethod.ReliableSequenced); - //} + public void SendJobsCreatePacket(ushort stationID, NetworkedJob[] jobs) + { + Multiplayer.Log($"Sending JobCreatePacket with {jobs.Length} jobs"); + SendPacketToAll(ClientboundJobsCreatePacket.FromNetworkedJobs(stationID, jobs),DeliveryMethod.ReliableSequenced); + } public void SendChat(string message, NetPeer exclude = null) { @@ -593,30 +593,29 @@ private void OnServerboundClientReadyPacket(ServerboundClientReadyPacket packet, SendPacket(peer, ClientboundSpawnTrainSetPacket.FromTrainSet(set), DeliveryMethod.ReliableOrdered); } - /* - //send jobs - do we need a job manager/job IDs to make this easier? + // Sync Stations (match NetIDs with StationIDs) - we could do this the same as junctions but juntions may need to be upgraded to work this way - future planning for mod integration + SendPacket(peer, new ClientBoundStationControllerLookupPacket(NetworkedStationController.GetAll().ToArray()), DeliveryMethod.ReliableOrdered); + + //send jobs foreach(StationController station in StationController.allStations) { - List jobData = new List(); - List netIds = new List(); - - foreach(Job job in station.logicStation.availableJobs) + if(NetworkedStationController.GetFromStationController(station, out NetworkedStationController netStation)) { - jobData.Add(JobData.FromJob(job)); - netIds.Add(NetworkedJob.GetFromJob(job).NetId); + NetworkedJob[] jobs = netStation.NetworkedJobs.ToArray(); + for (int i = 0; i < jobs.Length; i++) + { + //NetworkedJob[] batch = new NetworkedJob[5]; + + //Array.Copy(jobs,i,batch,0,5); + + SendJobsCreatePacket(netStation.NetId, [jobs[i]]); + } } - - SendPacket(peer, - new ClientboundJobsPacket - { - stationId = station.logicStation.ID, - netIds = netIds.ToArray(), - Jobs = jobData.ToArray(), - }, - DeliveryMethod.ReliableOrdered - ); - - }*/ + else + { + Multiplayer.LogError($"Sending job packets... Failed to get NetworkedStation from station"); + } + } // Send existing players diff --git a/Multiplayer/Networking/Packets/Clientbound/ClientboundPlayerCarPacket.cs b/Multiplayer/Networking/Packets/Clientbound/ClientboundPlayerCarPacket.cs deleted file mode 100644 index 10c0c4c..0000000 --- a/Multiplayer/Networking/Packets/Clientbound/ClientboundPlayerCarPacket.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Multiplayer.Networking.Packets.Clientbound; - -public class ClientboundPlayerCarPacket -{ - public byte Id { get; set; } - public ushort CarId { get; set; } -} diff --git a/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobCreatePacket.cs b/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobCreatePacket.cs deleted file mode 100644 index 4caa869..0000000 --- a/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobCreatePacket.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Multiplayer.Components.Networking.Jobs; -using Multiplayer.Networking.Data; -using Multiplayer.Networking.Packets.Clientbound.Train; - -namespace Multiplayer.Networking.Packets.Clientbound.Jobs; - -public class ClientboundJobCreatePacket -{ - public ushort netId { get; set; } - public string stationId { get; set; } - public JobData job { get; set; } - - public static ClientboundJobCreatePacket FromNetworkedJob(NetworkedJob job) - { - return new ClientboundJobCreatePacket - { - netId = job.NetId, - stationId = job.stationID, - job = JobData.FromJob(job.job), - }; - } -} diff --git a/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobPacket.cs b/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobPacket.cs deleted file mode 100644 index fc89a41..0000000 --- a/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobPacket.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Multiplayer.Networking.Data; - -namespace Multiplayer.Networking.Packets.Clientbound.Jobs; - -public class ClientboundJobsPacket -{ - public string stationId { get; set; } - public ushort[] netIds { get; set; } - public JobData[] Jobs { get; set; } - -} diff --git a/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobsCreatePacket.cs b/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobsCreatePacket.cs new file mode 100644 index 0000000..c009714 --- /dev/null +++ b/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobsCreatePacket.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using Multiplayer.Components.Networking.Jobs; +using Multiplayer.Networking.Data; +namespace Multiplayer.Networking.Packets.Clientbound.Jobs; + +public class ClientboundJobsCreatePacket +{ + public ushort StationNetId { get; set; } + public JobData[] Jobs { get; set; } + + public static ClientboundJobsCreatePacket FromNetworkedJobs(ushort stationID, NetworkedJob[] jobs) + { + List jobData = new List(); + foreach (var job in jobs) + { + jobData.Add(JobData.FromJob(job.NetId, job.Job)); + } + + return new ClientboundJobsCreatePacket + { + StationNetId = stationID, + Jobs = jobData.ToArray() + }; + } +} diff --git a/Multiplayer/Networking/Packets/Clientbound/World/ClientboundStationControllerLookupPacket.cs b/Multiplayer/Networking/Packets/Clientbound/World/ClientboundStationControllerLookupPacket.cs new file mode 100644 index 0000000..e159634 --- /dev/null +++ b/Multiplayer/Networking/Packets/Clientbound/World/ClientboundStationControllerLookupPacket.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; + +namespace Multiplayer.Networking.Packets.Clientbound.World; + +public class ClientBoundStationControllerLookupPacket +{ + public ushort[] NetID { get; set; } + public string[] StationID { get; set; } + + public ClientBoundStationControllerLookupPacket() { } + + public ClientBoundStationControllerLookupPacket(ushort[] netID, string[] stationID) + { + if (netID == null) throw new ArgumentNullException(nameof(netID)); + if (stationID == null) throw new ArgumentNullException(nameof(stationID)); + if (netID.Length != stationID.Length) throw new ArgumentException("Arrays must have the same length"); + + NetID = netID; + StationID = stationID; + } + + public ClientBoundStationControllerLookupPacket(KeyValuePair[] NetIDtoStationID) + { + if (NetIDtoStationID == null) + throw new ArgumentNullException(nameof(NetIDtoStationID)); + + NetID = new ushort[NetIDtoStationID.Length]; + StationID = new string[NetIDtoStationID.Length]; + + for (int i = 0; i < NetIDtoStationID.Length; i++) + { + NetID[i] = NetIDtoStationID[i].Key; + StationID[i] = NetIDtoStationID[i].Value; + } + } +} diff --git a/Multiplayer/Networking/Packets/Serverbound/ServerboundAddCoalPacket.cs b/Multiplayer/Networking/Packets/Serverbound/Train/ServerboundAddCoalPacket.cs similarity index 67% rename from Multiplayer/Networking/Packets/Serverbound/ServerboundAddCoalPacket.cs rename to Multiplayer/Networking/Packets/Serverbound/Train/ServerboundAddCoalPacket.cs index 582038d..a3a1763 100644 --- a/Multiplayer/Networking/Packets/Serverbound/ServerboundAddCoalPacket.cs +++ b/Multiplayer/Networking/Packets/Serverbound/Train/ServerboundAddCoalPacket.cs @@ -1,4 +1,4 @@ -namespace Multiplayer.Networking.Packets.Serverbound; +namespace Multiplayer.Networking.Packets.Serverbound.Train; public class ServerboundAddCoalPacket { diff --git a/Multiplayer/Networking/Packets/Serverbound/ServerboundFireboxIgnitePacket.cs b/Multiplayer/Networking/Packets/Serverbound/Train/ServerboundFireboxIgnitePacket.cs similarity index 77% rename from Multiplayer/Networking/Packets/Serverbound/ServerboundFireboxIgnitePacket.cs rename to Multiplayer/Networking/Packets/Serverbound/Train/ServerboundFireboxIgnitePacket.cs index 9898aac..c9ededb 100644 --- a/Multiplayer/Networking/Packets/Serverbound/ServerboundFireboxIgnitePacket.cs +++ b/Multiplayer/Networking/Packets/Serverbound/Train/ServerboundFireboxIgnitePacket.cs @@ -4,7 +4,7 @@ using System.Text; using System.Threading.Tasks; -namespace Multiplayer.Networking.Packets.Serverbound; +namespace Multiplayer.Networking.Packets.Serverbound.Train; public class ServerboundFireboxIgnitePacket { diff --git a/Multiplayer/Networking/Packets/Serverbound/ServerboundTrainDeleteRequestPacket.cs b/Multiplayer/Networking/Packets/Serverbound/Train/ServerboundTrainDeleteRequestPacket.cs similarity index 60% rename from Multiplayer/Networking/Packets/Serverbound/ServerboundTrainDeleteRequestPacket.cs rename to Multiplayer/Networking/Packets/Serverbound/Train/ServerboundTrainDeleteRequestPacket.cs index bcf2023..0de0e6e 100644 --- a/Multiplayer/Networking/Packets/Serverbound/ServerboundTrainDeleteRequestPacket.cs +++ b/Multiplayer/Networking/Packets/Serverbound/Train/ServerboundTrainDeleteRequestPacket.cs @@ -1,4 +1,4 @@ -namespace Multiplayer.Networking.Packets.Serverbound; +namespace Multiplayer.Networking.Packets.Serverbound.Train; public class ServerboundTrainDeleteRequestPacket { diff --git a/Multiplayer/Networking/Packets/Serverbound/ServerboundTrainRerailRequestPacket.cs b/Multiplayer/Networking/Packets/Serverbound/Train/ServerboundTrainRerailRequestPacket.cs similarity index 79% rename from Multiplayer/Networking/Packets/Serverbound/ServerboundTrainRerailRequestPacket.cs rename to Multiplayer/Networking/Packets/Serverbound/Train/ServerboundTrainRerailRequestPacket.cs index 8b5da12..62aefad 100644 --- a/Multiplayer/Networking/Packets/Serverbound/ServerboundTrainRerailRequestPacket.cs +++ b/Multiplayer/Networking/Packets/Serverbound/Train/ServerboundTrainRerailRequestPacket.cs @@ -1,6 +1,6 @@ using UnityEngine; -namespace Multiplayer.Networking.Packets.Serverbound; +namespace Multiplayer.Networking.Packets.Serverbound.Train; public class ServerboundTrainRerailRequestPacket { diff --git a/Multiplayer/Networking/Packets/Serverbound/ServerboundTrainSyncRequestPacket.cs b/Multiplayer/Networking/Packets/Serverbound/Train/ServerboundTrainSyncRequestPacket.cs similarity index 60% rename from Multiplayer/Networking/Packets/Serverbound/ServerboundTrainSyncRequestPacket.cs rename to Multiplayer/Networking/Packets/Serverbound/Train/ServerboundTrainSyncRequestPacket.cs index be4950e..91ec9dc 100644 --- a/Multiplayer/Networking/Packets/Serverbound/ServerboundTrainSyncRequestPacket.cs +++ b/Multiplayer/Networking/Packets/Serverbound/Train/ServerboundTrainSyncRequestPacket.cs @@ -1,4 +1,4 @@ -namespace Multiplayer.Networking.Packets.Serverbound; +namespace Multiplayer.Networking.Packets.Serverbound.Train; public class ServerboundTrainSyncRequestPacket { diff --git a/Multiplayer/Patches/Jobs/JobPatch.cs b/Multiplayer/Patches/Jobs/JobPatch.cs index fed0f44..be3f6a4 100644 --- a/Multiplayer/Patches/Jobs/JobPatch.cs +++ b/Multiplayer/Patches/Jobs/JobPatch.cs @@ -6,16 +6,16 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -/* + namespace Multiplayer.Patches.Jobs; -[HarmonyPatch(typeof(Job), nameof(Job.ExpireJob))] -public static class JobPatch -{ - private static bool Prefix(Job __instance) - { - Multiplayer.LogWarning($"Trying to expire {__instance.ID}\r\n"+ new System.Diagnostics.StackTrace()); - return false; - } -} -*/ +//[HarmonyPatch(typeof(Job), nameof(Job.ExpireJob))] +//public static class JobPatch +//{ +// private static bool Prefix(Job __instance) +// { +// Multiplayer.LogWarning($"Trying to expire {__instance.ID}\r\n"+ new System.Diagnostics.StackTrace()); +// return false; +// } +//} + diff --git a/Multiplayer/Patches/Jobs/StationControllerPatch.cs b/Multiplayer/Patches/Jobs/StationControllerPatch.cs index f2fc105..c66aa29 100644 --- a/Multiplayer/Patches/Jobs/StationControllerPatch.cs +++ b/Multiplayer/Patches/Jobs/StationControllerPatch.cs @@ -8,6 +8,6 @@ public static class StationController_Awake_Patch { public static void Postfix(StationController __instance) { - __instance.gameObject.AddComponent(); + __instance.gameObject.AddComponent(); } } diff --git a/Multiplayer/Patches/Jobs/StationPatch.cs b/Multiplayer/Patches/Jobs/StationPatch.cs index a0f253d..d4ceec2 100644 --- a/Multiplayer/Patches/Jobs/StationPatch.cs +++ b/Multiplayer/Patches/Jobs/StationPatch.cs @@ -4,6 +4,7 @@ using Multiplayer.Components.Networking; using Multiplayer.Components.Networking.Jobs; using Multiplayer.Components.Networking.Train; +using Multiplayer.Components.Networking.World; using Multiplayer.Utils; namespace Multiplayer.Patches.Jobs; @@ -16,19 +17,13 @@ private static bool Prefix(Station __instance, Job job) if (!NetworkLifecycle.Instance.IsHost()) return false; - Multiplayer.Log($"Station_AddJobToStation_Patch adding NetworkJob for stationId: {__instance.ID}, jobId: {job.ID}"); + Multiplayer.Log($"Station.AddJobToStation() adding NetworkJob for stationId: {__instance.ID}, jobId: {job.ID}"); - StationController stationController; - if(!StationComponentLookup.Instance.StationControllerFromId(__instance.ID, out stationController)) + if(!NetworkedStationController.GetFromStationId(__instance.ID, out NetworkedStationController netStationController)) return false; - NetworkedJob netJob = stationController.gameObject.AddComponent(); - if (netJob != null) - { - netJob.job=job; - netJob.stationID = __instance.ID; + netStationController.AddJob(job); - } return true; } } From 3a235a828934650651d173bb611bcf5270321dc8 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 31 Aug 2024 08:10:07 +1000 Subject: [PATCH 075/188] Update server browser pane to remove dummy servers --- .../Components/MainMenu/ServerBrowserPane.cs | 49 +++++++++---------- Multiplayer/Multiplayer.csproj | 2 +- info.json | 2 +- 3 files changed, 26 insertions(+), 27 deletions(-) diff --git a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs index 9ace5f1..861221b 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs @@ -80,7 +80,7 @@ private enum ConnectionState Aborted } - 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 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 @@ -91,7 +91,8 @@ private void Awake() BuildUI(); SetupServerBrowser(); - FillDummyServers(); + //FillDummyServers(); + RefreshAction(); } @@ -238,7 +239,7 @@ private void BuildUI() detailsPane = textGO.GetComponent(); detailsPane.textWrappingMode = TextWrappingModes.Normal; detailsPane.fontSize = 18; - 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."; + detailsPane.text = "Welcome to Derail Valley Multiplayer Mod!

The server list refreshes automatically every 30 seconds, but you can refresh manually once every 10 seconds."; // Adjust text RectTransform to fit content RectTransform textRT = textGO.GetComponent(); @@ -318,9 +319,7 @@ private void SetupListeners(bool on) private void RefreshAction() { if (serverRefreshing) - return; - - + return; if (selectedServer != null) { @@ -920,32 +919,32 @@ private void SetButtonsActive(params GameObject[] buttons) } } - private void FillDummyServers() - { - gridView.showDummyElement = false; - gridViewModel.Clear(); + //private void FillDummyServers() + //{ + // gridView.showDummyElement = false; + // gridViewModel.Clear(); - IServerBrowserGameDetails item = null; + // 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 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); - item.Ping = UnityEngine.Random.Range(5, 1500); - item.HasPassword = UnityEngine.Random.Range(0, 10) > 5; + // 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); + // 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.Ver : "0.1.0"; + // item.GameVersion = UnityEngine.Random.Range(1, 10) > 3 ? BuildInfo.BUILD_VERSION_MAJOR.ToString() : "97"; + // item.MultiplayerVersion = UnityEngine.Random.Range(1, 10) > 3 ? Multiplayer.Ver : "0.1.0"; - gridViewModel.Add(item); - } + // gridViewModel.Add(item); + // } - gridView.SetModel(gridViewModel); - } + // gridView.SetModel(gridViewModel); + //} private string ExtractDomainName(string input) { diff --git a/Multiplayer/Multiplayer.csproj b/Multiplayer/Multiplayer.csproj index 740fd45..3ff1b28 100644 --- a/Multiplayer/Multiplayer.csproj +++ b/Multiplayer/Multiplayer.csproj @@ -3,7 +3,7 @@ net48 latest Multiplayer - 0.1.8.1 + 0.1.8.2 diff --git a/info.json b/info.json index c71ee5f..722a2bc 100644 --- a/info.json +++ b/info.json @@ -1,6 +1,6 @@ { "Id": "Multiplayer", - "Version": "0.1.8.1", + "Version": "0.1.8.2", "DisplayName": "Multiplayer", "Author": "Insprill, Macka, Morm", "EntryMethod": "Multiplayer.Multiplayer.Load", From 85c614a5e7ee05ad91b6e6cd8e1d24516161a7e4 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 31 Aug 2024 11:43:39 +1000 Subject: [PATCH 076/188] Fix for joining servers with no password --- .../Components/MainMenu/ServerBrowserPane.cs | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs index 861221b..b4165d8 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs @@ -341,21 +341,17 @@ private void JoinAction() buttonDirectIP.ToggleInteractable(false); buttonJoin.ToggleInteractable(false); + //not making a direct connection + direct = false; + portNumber = selectedServer.port; + password = null; //clear the password + if (selectedServer.HasPassword) { - //not making a direct connection - direct = false; - - portNumber = selectedServer.port; - ShowPasswordPopup(); - return; } - //password not required - password = null; - AttemptConnection(); } @@ -369,6 +365,7 @@ private void DirectAction() //making a direct connection direct = true; + password = null; ShowIpPopup(); } From 310fcfc75f6f4250a66c01754008a8814866d51b Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 31 Aug 2024 11:51:00 +1000 Subject: [PATCH 077/188] Allowed multiple lines to be entered in details text box --- Multiplayer/Components/MainMenu/HostGamePane.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Multiplayer/Components/MainMenu/HostGamePane.cs b/Multiplayer/Components/MainMenu/HostGamePane.cs index eb25935..daad67a 100644 --- a/Multiplayer/Components/MainMenu/HostGamePane.cs +++ b/Multiplayer/Components/MainMenu/HostGamePane.cs @@ -223,7 +223,7 @@ private void BuildUI() 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.lineType = TMP_InputField.LineType.MultiLineNewline; details.FindChildByName("text [noloc]").GetComponent().alignment = TextAlignmentOptions.TopLeft; details.placeholder.GetComponent().text = Locale.SERVER_HOST_DETAILS; From a673023ea7d41a9716f529a1cf2ee0967e1719b8 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 31 Aug 2024 12:04:28 +1000 Subject: [PATCH 078/188] Fixed serialisation and deserialisation of jobs --- .../World/NetworkedStationController.cs | 25 +++- Multiplayer/Networking/Data/JobData.cs | 111 +++++++------- .../Networking/Data/TaskNetworkData.cs | 140 ++++++++---------- .../Managers/Client/NetworkClient.cs | 28 ++-- .../Networking/Managers/NetworkManager.cs | 2 +- .../Managers/Server/NetworkServer.cs | 3 +- 6 files changed, 154 insertions(+), 155 deletions(-) diff --git a/Multiplayer/Components/Networking/World/NetworkedStationController.cs b/Multiplayer/Components/Networking/World/NetworkedStationController.cs index 01abd5a..a8c609d 100644 --- a/Multiplayer/Components/Networking/World/NetworkedStationController.cs +++ b/Multiplayer/Components/Networking/World/NetworkedStationController.cs @@ -147,23 +147,38 @@ private void Server_OnTick(uint tick) #region Client public void AddJobs(JobData[] jobs) { + //NetworkLifecycle.Instance.Client.Log($"AddJobs() jobs[] exists: {jobs != null}, job count: {jobs?.Count()}"); + + //NetworkLifecycle.Instance.Client.Log($"AddJobs() preloop"); foreach (JobData jobData in jobs) { - NetworkLifecycle.Instance.Client.Log($"AddJobs() {jobData.ID}, {jobData.NetID}"); + //NetworkLifecycle.Instance.Client.Log($"AddJobs() inloop"); + + //NetworkLifecycle.Instance.Client.Log($"AddJobs() ID: {jobData?.ID ?? ""}, netID: {jobData?.NetID}, task count: {jobData?.Tasks?.Count()}"); // Convert TaskNetworkData to Task objects List tasks = new List(); foreach (TaskNetworkData taskData in jobData.Tasks) { + if (NetworkLifecycle.Instance.IsHost()) + { + Task test = taskData.ToTask(); + continue; + } + + //NetworkLifecycle.Instance.Client.Log($"AddJobs() ID: {jobData?.ID}, task type: {taskData.TaskType}"); tasks.Add(taskData.ToTask()); } + //NetworkLifecycle.Instance.Client.Log($"AddJobs() ID: {jobData?.ID}, netID: {jobData?.NetID}, StationsChainData"); // Create StationsChainData from ChainData StationsChainData chainData = new StationsChainData( jobData.ChainData.ChainOriginYardId, jobData.ChainData.ChainDestinationYardId ); + + //NetworkLifecycle.Instance.Client.Log($"AddJobs() ID: {jobData?.ID}, netID: {jobData?.NetID}, newJob"); // Create a new local Job Job newJob = new Job( tasks, @@ -175,21 +190,27 @@ public void AddJobs(JobData[] jobs) jobData.RequiredLicenses ); + //NetworkLifecycle.Instance.Client.Log($"AddJobs() ID: {jobData?.ID}, netID: {jobData?.NetID}, properties"); // Set additional properties newJob.startTime = jobData.StartTime; newJob.finishTime = jobData.FinishTime; newJob.State = jobData.State; + //NetworkLifecycle.Instance.Client.Log($"AddJobs() ID: {jobData?.ID}, netID: {jobData?.NetID}, netjob"); + // Create a new NetworkedJob NetworkedJob networkedJob = new GameObject($"NetworkedJob {newJob.ID}").AddComponent(); networkedJob.Job = newJob; + + //NetworkLifecycle.Instance.Client.Log($"AddJobs() ID: {jobData?.ID}, netID: {jobData?.NetID}, NetJob Add"); NetworkedJobs.Add(networkedJob); + //NetworkLifecycle.Instance.Client.Log($"AddJobs() ID: {jobData?.ID}, netID: {jobData?.NetID}, CarPlates"); // Start coroutine to update car plates StartCoroutine(UpdateCarPlates(tasks, newJob.ID)); // Log the addition of the new job - Multiplayer.Log($"AddJobs() {newJob.ID} to NetworkedStationController {StationController.logicStation.ID}"); + NetworkLifecycle.Instance.Client.Log($"AddJobs() {newJob?.ID} to NetworkedStationController {StationController?.logicStation?.ID}"); } } #endregion diff --git a/Multiplayer/Networking/Data/JobData.cs b/Multiplayer/Networking/Data/JobData.cs index 6703cbb..39132c2 100644 --- a/Multiplayer/Networking/Data/JobData.cs +++ b/Multiplayer/Networking/Data/JobData.cs @@ -1,10 +1,7 @@ -using System.Collections.Generic; -using System.Linq; using DV.Logic.Job; using DV.ThingTypes; using LiteNetLib.Utils; -using Newtonsoft.Json; -using static DV.UI.ATutorialsMenuProvider; +using Multiplayer.Components.Networking; namespace Multiplayer.Networking.Data; @@ -14,7 +11,7 @@ public class JobData public JobType JobType { get; set; } //serialise as byte public string ID { get; set; } public TaskNetworkData[] Tasks { get; set; } - public StationsChainDataData ChainData { get; set; } + public StationsChainNetworkData ChainData { get; set; } public JobLicenses RequiredLicenses { get; set; } //serialise as int public float StartTime { get; set; } public float FinishTime { get; set; } @@ -30,7 +27,7 @@ public static JobData FromJob(ushort netID, Job job) JobType = job.jobType, ID = job.ID, Tasks = TaskNetworkDataFactory.ConvertTasks(job.tasks), - ChainData = StationsChainDataData.FromStationData(job.chainData), + ChainData = StationsChainNetworkData.FromStationData(job.chainData), RequiredLicenses = job.requiredLicenses, StartTime = job.startTime, FinishTime = job.finishTime, @@ -42,79 +39,81 @@ public static JobData FromJob(ushort netID, Job job) public static void Serialize(NetDataWriter writer, JobData data) { - Multiplayer.Log($"JobData.Serialize({data.ID}) NetID {data.NetID}"); + NetworkLifecycle.Instance.Server.Log($"JobData.Serialize({data.ID}) NetID {data.NetID}"); writer.Put(data.NetID); - Multiplayer.Log($"JobData.Serialize({data.ID}) JobType {(byte)data.JobType}, {data.JobType}"); + //Multiplayer.Log($"JobData.Serialize({data.ID}) JobType {(byte)data.JobType}, {data.JobType}"); writer.Put((byte)data.JobType); - Multiplayer.Log($"JobData.Serialize({data.ID}) JobID {data.ID}"); + //Multiplayer.Log($"JobData.Serialize({data.ID}) JobID {data.ID}"); writer.Put(data.ID); - Multiplayer.Log($"JobData.Serialize({data.ID}) task length {data.Tasks.Length}"); + //Multiplayer.Log($"JobData.Serialize({data.ID}) task length {data.Tasks.Length}"); //task data writer.Put((byte)data.Tasks.Length); foreach (var task in data.Tasks) { - Multiplayer.Log($"JobData.Serialize({data.ID}) TaskType {(byte)task.TaskType}, {task.TaskType}"); + //Multiplayer.Log($"JobData.Serialize({data.ID}) TaskType {(byte)task.TaskType}, {task.TaskType}"); writer.Put((byte)task.TaskType); task.Serialize(writer); } - Multiplayer.Log($"JobData.Serialize({data.ID}) calling StationsChainDataData.Serialize()"); - StationsChainDataData.Serialize(writer, data.ChainData); + //Multiplayer.Log($"JobData.Serialize({data.ID}) calling StationsChainDataData.Serialize()"); + StationsChainNetworkData.Serialize(writer, data.ChainData); - Multiplayer.Log($"JobData.Serialize({data.ID}) RequiredLicenses {data.RequiredLicenses}"); + //Multiplayer.Log($"JobData.Serialize({data.ID}) RequiredLicenses {data.RequiredLicenses}"); writer.Put((int)data.RequiredLicenses); - Multiplayer.Log($"JobData.Serialize({data.ID}) StartTime {data.StartTime}"); + //Multiplayer.Log($"JobData.Serialize({data.ID}) StartTime {data.StartTime}"); writer.Put(data.StartTime); - Multiplayer.Log($"JobData.Serialize({data.ID}) FinishTime {data.FinishTime}"); + //Multiplayer.Log($"JobData.Serialize({data.ID}) FinishTime {data.FinishTime}"); writer.Put(data.FinishTime); - Multiplayer.Log($"JobData.Serialize({data.ID}) InitialWage {data.InitialWage}"); + //Multiplayer.Log($"JobData.Serialize({data.ID}) InitialWage {data.InitialWage}"); writer.Put(data.InitialWage); - Multiplayer.Log($"JobData.Serialize({data.ID}) State {(byte)data.State}, {data.State}"); + //Multiplayer.Log($"JobData.Serialize({data.ID}) State {(byte)data.State}, {data.State}"); writer.Put((byte)data.State); - Multiplayer.Log($"JobData.Serialize({data.ID}) TimeLimit {data.TimeLimit}"); + //Multiplayer.Log($"JobData.Serialize({data.ID}) TimeLimit {data.TimeLimit}"); writer.Put(data.TimeLimit); - Multiplayer.Log(JsonConvert.SerializeObject(data, Formatting.None)); + //Multiplayer.Log(JsonConvert.SerializeObject(data, Formatting.None)); } public static JobData Deserialize(NetDataReader reader) { - Multiplayer.LogDebug(() => $"JobData.Deserialize(): [{string.Join(", ", reader.RawData?.Select(id => id.ToString()))}]"); - var netID = reader.GetUShort(); - Multiplayer.Log($"JobData.Deserialize() netID {netID}"); - var jobType = (JobType)reader.GetByte(); - Multiplayer.Log($"JobData.Deserialize() jobType {jobType}"); - var id = reader.GetString(); - Multiplayer.Log($"JobData.Deserialize() id {id}"); - - var tasksLength = reader.GetByte(); - Multiplayer.Log($"JobData.Deserialize() tasksLength {tasksLength}"); - - var tasks = new TaskNetworkData[tasksLength]; + //Multiplayer.LogDebug(() => $"JobData.Deserialize(): [{string.Join(", ", reader.RawData?.Select(id => id.ToString()))}]"); + ushort netID = reader.GetUShort(); + //Multiplayer.Log($"JobData.Deserialize() netID {netID}"); + JobType jobType = (JobType)reader.GetByte(); + //Multiplayer.Log($"JobData.Deserialize() jobType {jobType}"); + string id = reader.GetString(); + //Multiplayer.Log($"JobData.Deserialize() id {id}"); + + byte tasksLength = reader.GetByte(); + //Multiplayer.Log($"JobData.Deserialize() tasksLength {tasksLength}"); + + TaskNetworkData[] tasks = new TaskNetworkData[tasksLength]; for (int i = 0; i < tasksLength; i++) { - var taskType = (TaskType)reader.GetByte(); - Multiplayer.Log($"JobData.Deserialize() taskType {taskType}"); - tasks[i] = TaskNetworkData.CreateTaskNetworkDataFromType(taskType); + TaskType taskType = (TaskType)reader.GetByte(); + //Multiplayer.Log($"JobData.Deserialize() taskType {taskType}"); + tasks[i] = TaskNetworkDataFactory.ConvertTask(taskType); + //Multiplayer.Log($"JobData.Deserialize() TaskNetworkData not null: {tasks[i] != null}, {tasks[i].GetType().FullName}"); tasks[i].Deserialize(reader); + //Multiplayer.Log($"JobData.Deserialize() TaskNetworkData Deserialised"); } - var chainData = StationsChainDataData.Deserialize(reader); - Multiplayer.Log($"JobData.Deserialize() chainData {chainData.ChainOriginYardId}, {chainData.ChainDestinationYardId}"); - - var requiredLicenses = (JobLicenses)reader.GetInt(); - Multiplayer.Log("JobData.Deserialize() requiredLicenses: " + requiredLicenses); - var startTime = reader.GetFloat(); - Multiplayer.Log("JobData.Deserialize() startTime: " + startTime); - var finishTime = reader.GetFloat(); - Multiplayer.Log("JobData.Deserialize() finishTime: " + finishTime); - var initialWage = reader.GetFloat(); - Multiplayer.Log("JobData.Deserialize() initialWage: " + initialWage); - var state = (JobState)reader.GetByte(); - Multiplayer.Log("JobData.Deserialize() state: " + state); - var timeLimit = reader.GetFloat(); - Multiplayer.Log("JobData.Deserialize() timeLimit: " + timeLimit); + StationsChainNetworkData chainData = StationsChainNetworkData.Deserialize(reader); + //Multiplayer.Log($"JobData.Deserialize() chainData {chainData.ChainOriginYardId}, {chainData.ChainDestinationYardId}"); + + JobLicenses requiredLicenses = (JobLicenses)reader.GetInt(); + //Multiplayer.Log("JobData.Deserialize() requiredLicenses: " + requiredLicenses); + float startTime = reader.GetFloat(); + //Multiplayer.Log("JobData.Deserialize() startTime: " + startTime); + float finishTime = reader.GetFloat(); + //Multiplayer.Log("JobData.Deserialize() finishTime: " + finishTime); + float initialWage = reader.GetFloat(); + //Multiplayer.Log("JobData.Deserialize() initialWage: " + initialWage); + JobState state = (JobState)reader.GetByte(); + //Multiplayer.Log("JobData.Deserialize() state: " + state); + float timeLimit = reader.GetFloat(); + //Multiplayer.Log("JobData.Deserialize() timeLimit: " + timeLimit); return new JobData { @@ -134,29 +133,29 @@ public static JobData Deserialize(NetDataReader reader) } -public struct StationsChainDataData +public struct StationsChainNetworkData { public string ChainOriginYardId { get; set; } public string ChainDestinationYardId { get; set; } - public static StationsChainDataData FromStationData(StationsChainData data) + public static StationsChainNetworkData FromStationData(StationsChainData data) { - return new StationsChainDataData + return new StationsChainNetworkData { ChainOriginYardId = data.chainOriginYardId, ChainDestinationYardId = data.chainDestinationYardId }; } - public static void Serialize(NetDataWriter writer, StationsChainDataData data) + public static void Serialize(NetDataWriter writer, StationsChainNetworkData data) { writer.Put(data.ChainOriginYardId); writer.Put(data.ChainDestinationYardId); } - public static StationsChainDataData Deserialize(NetDataReader reader) + public static StationsChainNetworkData Deserialize(NetDataReader reader) { - return new StationsChainDataData + return new StationsChainNetworkData { ChainOriginYardId = reader.GetString(), ChainDestinationYardId = reader.GetString() diff --git a/Multiplayer/Networking/Data/TaskNetworkData.cs b/Multiplayer/Networking/Data/TaskNetworkData.cs index fbe90b1..7209948 100644 --- a/Multiplayer/Networking/Data/TaskNetworkData.cs +++ b/Multiplayer/Networking/Data/TaskNetworkData.cs @@ -9,6 +9,7 @@ namespace Multiplayer.Networking.Data; +#region TaskData Base Class public abstract class TaskNetworkData { public TaskState State { get; set; } @@ -21,30 +22,6 @@ public abstract class TaskNetworkData public abstract void Serialize(NetDataWriter writer); public abstract void Deserialize(NetDataReader reader); public abstract Task ToTask(); - - public static TaskNetworkData CreateTaskNetworkDataFromType(TaskType taskType) - { - return taskType switch - { - TaskType.Warehouse => new WarehouseTaskData(), - TaskType.Transport => new TransportTaskData(), - TaskType.Sequential => new SequentialTasksData(), - TaskType.Parallel => new ParallelTasksData(), - _ => throw new ArgumentException($"Unknown task type: {taskType}") - }; - } - - public static TaskType GetTaskType(Task task) - { - return task switch - { - WarehouseTask => TaskType.Warehouse, - TransportTask => TaskType.Transport, - SequentialTasks => TaskType.Sequential, - ParallelTasks => TaskType.Parallel, - _ => throw new ArgumentException($"Unknown task type: {task.GetType()}") - }; - } } public abstract class TaskNetworkData : TaskNetworkData where T : TaskNetworkData { @@ -69,37 +46,38 @@ protected void SerializeCommon(NetDataWriter writer) protected void DeserializeCommon(NetDataReader reader) { State = (TaskState)reader.GetByte(); - Multiplayer.Log($"TaskNetworkData.DeserializeCommon() State {State}"); + //Multiplayer.Log($"TaskNetworkData.DeserializeCommon() State {State}"); TaskStartTime = reader.GetFloat(); - Multiplayer.Log($"TaskNetworkData.DeserializeCommon() TaskStartTime {TaskStartTime}"); + //Multiplayer.Log($"TaskNetworkData.DeserializeCommon() TaskStartTime {TaskStartTime}"); TaskFinishTime = reader.GetFloat(); - Multiplayer.Log($"TaskNetworkData.DeserializeCommon() TaskFinishTime {TaskFinishTime}"); + //Multiplayer.Log($"TaskNetworkData.DeserializeCommon() TaskFinishTime {TaskFinishTime}"); IsLastTask = reader.GetBool(); - Multiplayer.Log($"TaskNetworkData.DeserializeCommon() IsLastTask {IsLastTask}"); + //Multiplayer.Log($"TaskNetworkData.DeserializeCommon() IsLastTask {IsLastTask}"); TimeLimit = reader.GetFloat(); - Multiplayer.Log($"TaskNetworkData.DeserializeCommon() TimeLimit {TimeLimit}"); + //Multiplayer.Log($"TaskNetworkData.DeserializeCommon() TimeLimit {TimeLimit}"); TaskType = (TaskType)reader.GetByte(); - Multiplayer.Log($"TaskNetworkData.DeserializeCommon() TaskType {TaskType}"); + //Multiplayer.Log($"TaskNetworkData.DeserializeCommon() TaskType {TaskType}"); } } +#endregion + +#region Extension of TaskTypes public static class TaskNetworkDataFactory { private static readonly Dictionary> TypeToTaskNetworkData = new(); - private static readonly Dictionary> EnumToTaskNetworkData = new(); + private static readonly Dictionary> EnumToEmptyTaskNetworkData = new(); - - //Allow new task types to be registered - will help with mods such as passenger mod - public static void RegisterTaskType(TaskType taskType, Func converter) + public static void RegisterTaskType(TaskType taskType, Func converter, Func emptyCreator) where TGameTask : Task { TypeToTaskNetworkData[typeof(TGameTask)] = task => converter((TGameTask)task); - EnumToTaskNetworkData[taskType] = task => converter((TGameTask)task); + EnumToEmptyTaskNetworkData[taskType] = emptyCreator; } public static TaskNetworkData ConvertTask(Task task) { - Multiplayer.Log($"TaskNetworkDataFactory.ConvertTask: Processing task of type {task.GetType()}"); + Multiplayer.LogDebug(()=>$"TaskNetworkDataFactory.ConvertTask: Processing task of type {task.GetType()}"); if (TypeToTaskNetworkData.TryGetValue(task.GetType(), out var converter)) { return converter(task); @@ -114,9 +92,9 @@ public static TaskNetworkData[] ConvertTasks(IEnumerable tasks) public static TaskNetworkData ConvertTask(TaskType type) { - if (EnumToTaskNetworkData.TryGetValue(type, out var creator)) + if (EnumToEmptyTaskNetworkData.TryGetValue(type, out var creator)) { - return creator(null); // Passing null as we're just creating an empty instance + return creator(type); } throw new ArgumentException($"Unknown task type: {type}"); } @@ -124,13 +102,29 @@ public static TaskNetworkData ConvertTask(TaskType type) // Register base task types static TaskNetworkDataFactory() { - RegisterTaskType(TaskType.Warehouse, task => new WarehouseTaskData().FromTask(task)); - RegisterTaskType(TaskType.Transport, task => new TransportTaskData().FromTask(task)); - RegisterTaskType(TaskType.Sequential, task => new SequentialTasksData().FromTask(task)); - RegisterTaskType(TaskType.Parallel, task => new ParallelTasksData().FromTask(task)); + RegisterTaskType( + TaskType.Warehouse, + task => new WarehouseTaskData { TaskType = TaskType.Warehouse }.FromTask(task), + type => new WarehouseTaskData { TaskType = type } + ); + RegisterTaskType( + TaskType.Transport, + task => new TransportTaskData { TaskType = TaskType.Transport }.FromTask(task), + type => new TransportTaskData { TaskType = type } + ); + RegisterTaskType( + TaskType.Sequential, + task => new SequentialTasksData { TaskType = TaskType.Sequential }.FromTask(task), + type => new SequentialTasksData { TaskType = type } + ); + RegisterTaskType( + TaskType.Parallel, + task => new ParallelTasksData { TaskType = TaskType.Parallel }.FromTask(task), + type => new ParallelTasksData { TaskType = type } + ); } } - +#endregion public class WarehouseTaskData : TaskNetworkData { @@ -217,31 +211,28 @@ public class TransportTaskData : TaskNetworkData public override void Serialize(NetDataWriter writer) { SerializeCommon(writer); - Multiplayer.LogDebug(() => $"TransportTaskData.Serialize() CarNetIDs count: {CarNetIDs.Length}, Values: [{string.Join(", ", CarNetIDs?.Select(id => id.ToString()))}]"); - //Multiplayer.LogDebug(() => $"TransportTaskData.Serialize() raw before: [{string.Join(", ", writer.Data?.Select(id => id.ToString()))}]"); - - Multiplayer.Log($"TaskNetworkData.Serialize() CarNetIDs.Length {CarNetIDs.Length}"); + //Multiplayer.LogDebug(() => $"TransportTaskData.Serialize() CarNetIDs count: {CarNetIDs.Length}, Values: [{string.Join(", ", CarNetIDs?.Select(id => id.ToString()))}]"); writer.PutArray(CarNetIDs); //Multiplayer.LogDebug(() => $"TransportTaskData.Serialize() raw after: [{string.Join(", ", writer.Data?.Select(id => id.ToString()))}]"); - Multiplayer.Log($"TaskNetworkData.Serialize() StartingTrack {StartingTrack}"); + //Multiplayer.Log($"TaskNetworkData.Serialize() StartingTrack {StartingTrack}"); writer.Put(StartingTrack); - Multiplayer.Log($"TaskNetworkData.Serialize() DestinationTrack {DestinationTrack}"); + //Multiplayer.Log($"TaskNetworkData.Serialize() DestinationTrack {DestinationTrack}"); writer.Put(DestinationTrack); - Multiplayer.Log($"TaskNetworkData.Serialize() TransportedCargoPerCar != null {TransportedCargoPerCar != null}"); + //Multiplayer.Log($"TaskNetworkData.Serialize() TransportedCargoPerCar != null {TransportedCargoPerCar != null}"); writer.Put(TransportedCargoPerCar != null); if (TransportedCargoPerCar != null) { - Multiplayer.Log($"TaskNetworkData.Serialize() TransportedCargoPerCar.PutArray() length: {TransportedCargoPerCar.Length}"); + //Multiplayer.Log($"TaskNetworkData.Serialize() TransportedCargoPerCar.PutArray() length: {TransportedCargoPerCar.Length}"); writer.PutArray(TransportedCargoPerCar.Select(x => (int)x).ToArray()); } - Multiplayer.Log($"TaskNetworkData.Serialize() CouplingRequiredAndNotDone {CouplingRequiredAndNotDone}"); + //Multiplayer.Log($"TaskNetworkData.Serialize() CouplingRequiredAndNotDone {CouplingRequiredAndNotDone}"); writer.Put(CouplingRequiredAndNotDone); - Multiplayer.Log($"TaskNetworkData.Serialize() AnyHandbrakeRequiredAndNotDone {AnyHandbrakeRequiredAndNotDone}"); + //Multiplayer.Log($"TaskNetworkData.Serialize() AnyHandbrakeRequiredAndNotDone {AnyHandbrakeRequiredAndNotDone}"); writer.Put(AnyHandbrakeRequiredAndNotDone); } @@ -249,39 +240,28 @@ public override void Deserialize(NetDataReader reader) { DeserializeCommon(reader); + CarNetIDs = reader.GetUShortArray(); - int idCount = reader.GetInt(); - Multiplayer.Log($"TaskNetworkData.Deserialize() CarNetIDs.Length {idCount}"); - CarNetIDs = new ushort[idCount]; - - //Multiplayer.LogDebug(() => $" {idCount} raw before: [{string.Join(", ", reader.RawData?.Select(id => id.ToString()))}]"); - - for (int i = 0; i < idCount; i++) - { - CarNetIDs[i] = reader.GetUShort(); - Multiplayer.Log($"TaskNetworkData.Deserialize() CarNetIDs[{i}] {CarNetIDs[i]}"); - } - - Multiplayer.LogDebug(() => $"TransportTaskData.Deserialize() CarNetIDs count: {CarNetIDs.Length}, Values: [{string.Join(", ", CarNetIDs?.Select(id => id.ToString()))}]"); + //Multiplayer.LogDebug(() => $"TransportTaskData.Deserialize() CarNetIDs count: {CarNetIDs.Length}, Values: [{string.Join(", ", CarNetIDs?.Select(id => id.ToString()))}]"); StartingTrack = reader.GetString(); - Multiplayer.Log($"TaskNetworkData.Deserialize() StartingTrack {StartingTrack}"); + //Multiplayer.Log($"TaskNetworkData.Deserialize() StartingTrack {StartingTrack}"); DestinationTrack = reader.GetString(); - Multiplayer.Log($"TaskNetworkData.Deserialize() DestinationTrack {DestinationTrack}"); + //Multiplayer.Log($"TaskNetworkData.Deserialize() DestinationTrack {DestinationTrack}"); if (reader.GetBool()) { - Multiplayer.Log($"TaskNetworkData.Deserialize() TransportedCargoPerCar != null True"); + //Multiplayer.Log($"TaskNetworkData.Deserialize() TransportedCargoPerCar != null True"); TransportedCargoPerCar = reader.GetIntArray().Select(x => (CargoType)x).ToArray(); } else { - Multiplayer.Log($"TaskNetworkData.Deserialize() TransportedCargoPerCar != null False"); + Multiplayer.LogWarning($"TaskNetworkData.Deserialize() TransportedCargoPerCar != null False"); } CouplingRequiredAndNotDone = reader.GetBool(); - Multiplayer.Log($"TaskNetworkData.Deserialize() CouplingRequiredAndNotDone {CouplingRequiredAndNotDone}"); + //Multiplayer.Log($"TaskNetworkData.Deserialize() CouplingRequiredAndNotDone {CouplingRequiredAndNotDone}"); AnyHandbrakeRequiredAndNotDone = reader.GetBool(); - Multiplayer.Log($"TaskNetworkData.Deserialize() AnyHandbrakeRequiredAndNotDone {AnyHandbrakeRequiredAndNotDone}"); + //Multiplayer.Log($"TaskNetworkData.Deserialize() AnyHandbrakeRequiredAndNotDone {AnyHandbrakeRequiredAndNotDone}"); } public override TransportTaskData FromTask(Task task) @@ -289,14 +269,14 @@ public override TransportTaskData FromTask(Task task) if (task is not TransportTask transportTask) throw new ArgumentException("Task is not a TransportTask"); - Multiplayer.LogDebug(() => $"TransportTaskData.FromTask() CarNetIDs count: {transportTask.cars.Count()}, Values: [{string.Join(", ", transportTask.cars.Select(car => car.ID))}]"); + //Multiplayer.LogDebug(() => $"TransportTaskData.FromTask() CarNetIDs count: {transportTask.cars.Count()}, Values: [{string.Join(", ", transportTask.cars.Select(car => car.ID))}]"); CarNetIDs = transportTask.cars .Select(car => NetworkedTrainCar.GetFromTrainId(car.ID, out var networkedTrainCar) ? networkedTrainCar.NetId : (ushort)0) .ToArray(); - Multiplayer.LogDebug(() => $"TransportTaskData.FromTask() after CarNetIDs count: {CarNetIDs.Length}, Values: [{string.Join(", ", CarNetIDs.Select(id => id.ToString()))}]"); + //Multiplayer.LogDebug(() => $"TransportTaskData.FromTask() after CarNetIDs count: {CarNetIDs.Length}, Values: [{string.Join(", ", CarNetIDs.Select(id => id.ToString()))}]"); StartingTrack = transportTask.startingTrack.ID.RailTrackGameObjectID; DestinationTrack = transportTask.destinationTrack.ID.RailTrackGameObjectID; @@ -309,7 +289,7 @@ public override TransportTaskData FromTask(Task task) public override Task ToTask() { - Multiplayer.LogDebug(() => $"TransportTaskData.ToTask() CarNetIDs !null {CarNetIDs != null}, count: {CarNetIDs?.Length}"); + //Multiplayer.LogDebug(() => $"TransportTaskData.ToTask() CarNetIDs !null {CarNetIDs != null}, count: {CarNetIDs?.Length}"); List cars = CarNetIDs .Select(netId => NetworkedTrainCar.GetTrainCar(netId, out TrainCar trainCar) ? trainCar.logicCar : null) @@ -332,16 +312,16 @@ public class SequentialTasksData : TaskNetworkData public override void Serialize(NetDataWriter writer) { - Multiplayer.Log($"SequentialTasksData.Serialize({writer != null})"); + //Multiplayer.Log($"SequentialTasksData.Serialize({writer != null})"); SerializeCommon(writer); - Multiplayer.Log($"SequentialTasksData.Serialize() {Tasks.Length}"); + //Multiplayer.Log($"SequentialTasksData.Serialize() {Tasks.Length}"); writer.Put((byte)Tasks.Length); foreach (var task in Tasks) { - Multiplayer.Log($"SequentialTasksData.Serialize() {task.TaskType} {task.GetType()}"); + //Multiplayer.Log($"SequentialTasksData.Serialize() {task.TaskType} {task.GetType()}"); writer.Put((byte)task.TaskType); task.Serialize(writer); } @@ -370,7 +350,7 @@ public override SequentialTasksData FromTask(Task task) if (task is not SequentialTasks sequentialTasks) throw new ArgumentException("Task is not a SequentialTasks"); - Multiplayer.Log($"SequentialTasksData.FromTask() {sequentialTasks.tasks.Count}"); + //Multiplayer.Log($"SequentialTasksData.FromTask() {sequentialTasks.tasks.Count}"); Tasks = TaskNetworkDataFactory.ConvertTasks(sequentialTasks.tasks); @@ -399,7 +379,7 @@ public override Task ToTask() foreach (var task in Tasks) { - Multiplayer.LogDebug(() => $"SequentialTask.ToTask() task not null: {task != null}"); + //Multiplayer.LogDebug(() => $"SequentialTask.ToTask() task not null: {task != null}"); tasks.Add(task.ToTask()); } diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 8cc47cc..d710c07 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -484,7 +484,7 @@ private void OnClientboundDestroyTrainCarPacket(ClientboundDestroyTrainCarPacket //Protect myself from getting deleted in race conditions if (PlayerManager.Car == networkedTrainCar.TrainCar) { - Multiplayer.LogWarning($"Server attempted to delete car I'm on: {PlayerManager.Car.ID}, net ID: {packet.NetId}"); + LogWarning($"Server attempted to delete car I'm on: {PlayerManager.Car.ID}, net ID: {packet.NetId}"); PlayerManager.SetCar(null); } @@ -616,7 +616,7 @@ private void OnClientboundBrakePressureUpdatePacket(ClientboundBrakePressureUpda networkedTrainCar.Client_ReceiveBrakePressureUpdate(packet.MainReservoirPressure, packet.IndependentPipePressure, packet.BrakePipePressure, packet.BrakeCylinderPressure); - //Multiplayer.LogDebug(() => $"Received Brake Pressures netId {packet.NetId}: {packet.MainReservoirPressure}, {packet.IndependentPipePressure}, {packet.BrakePipePressure}, {packet.BrakeCylinderPressure}"); + //LogDebug(() => $"Received Brake Pressures netId {packet.NetId}: {packet.MainReservoirPressure}, {packet.IndependentPipePressure}, {packet.BrakePipePressure}, {packet.BrakeCylinderPressure}"); } private void OnClientboundFireboxStatePacket(ClientboundFireboxStatePacket packet) @@ -627,7 +627,7 @@ private void OnClientboundFireboxStatePacket(ClientboundFireboxStatePacket packe networkedTrainCar.Client_ReceiveFireboxStateUpdate(packet.Contents, packet.IsOn); - //Multiplayer.LogDebug(() => $"Received Brake Pressures netId {packet.NetId}: {packet.Contents}, {packet.IsOn}"); + //LogDebug(() => $"Received Brake Pressures netId {packet.NetId}: {packet.Contents}, {packet.IsOn}"); } private void OnClientboundCargoStatePacket(ClientboundCargoStatePacket packet) @@ -740,14 +740,14 @@ private void OnCommonChatPacket(CommonChatPacket packet) private void OnClientboundJobsCreatePacket(ClientboundJobsCreatePacket packet) { - Multiplayer.Log($"OnClientboundJobCreatePacket() for station {packet.StationNetId}, containing {packet.Jobs.Length}"); + Log($"OnClientboundJobsCreatePacket() for station {packet.StationNetId}, containing {packet.Jobs.Length}"); if (NetworkLifecycle.Instance.IsHost()) return; if(!NetworkedStationController.Get(packet.StationNetId, out NetworkedStationController networkedStationController)) { - LogError($"OnClientboundJobCreatePacket() {packet.StationNetId} does not exist!"); + LogError($"OnClientboundJobsCreatePacket() {packet.StationNetId} does not exist!"); return; } @@ -758,7 +758,7 @@ private void OnClientboundJobsCreatePacket(ClientboundJobsCreatePacket packet) /* private void OnClientboundJobTakeResponsePacket(ClientboundJobTakeResponsePacket packet) { - Multiplayer.Log($"OnClientboundJobTakeResponsePacket jobId: {packet.netId}, Status: {packet.granted}"); + Log($"OnClientboundJobTakeResponsePacket jobId: {packet.netId}, Status: {packet.granted}"); NetworkedJob networkedJob; @@ -771,7 +771,7 @@ private void OnClientboundJobTakeResponsePacket(ClientboundJobTakeResponsePacket networkedJob.takenBy = player.Guid; } - Multiplayer.Log($"OnClientboundJobTakeResponsePacket jobId: {networkedJob.job.ID}, Status: {packet.granted}"); + Log($"OnClientboundJobTakeResponsePacket jobId: {networkedJob.job.ID}, Status: {packet.granted}"); networkedJob.allowTake = packet.granted; networkedJob.jobValidator.ProcessJobOverview(networkedJob.jobOverview); networkedJob.jobValidator = null; @@ -801,7 +801,7 @@ private void SendReadyPacket() public void SendPlayerPosition(Vector3 position, Vector3 moveDir, float rotationY, ushort carId, bool isJumping, bool isOnCar, bool reliable) { - //Multiplayer.LogDebug(() => $"SendPlayerPosition({position}, {moveDir}, {rotationY}, {carId}, {isJumping}, {isOnCar})"); + //LogDebug(() => $"SendPlayerPosition({position}, {moveDir}, {rotationY}, {carId}, {isJumping}, {isOnCar})"); SendPacketToServer(new ServerboundPlayerPositionPacket { @@ -847,7 +847,7 @@ public void SendTrainCouple(Coupler coupler, Coupler otherCoupler, bool playAudi if (couplerNetId == 0 || otherCouplerNetId == 0) { - Multiplayer.LogWarning($"SendTrainCouple failed. Coupler: {coupler.name} {couplerNetId}, OtherCoupler: {otherCoupler.name} {otherCouplerNetId}"); + LogWarning($"SendTrainCouple failed. Coupler: {coupler.name} {couplerNetId}, OtherCoupler: {otherCoupler.name} {otherCouplerNetId}"); return; } @@ -868,7 +868,7 @@ public void SendTrainUncouple(Coupler coupler, bool playAudio, bool dueToBrokenC if (couplerNetId == 0) { - Multiplayer.LogWarning($"SendTrainUncouple failed. Coupler: {coupler.name} {couplerNetId}"); + LogWarning($"SendTrainUncouple failed. Coupler: {coupler.name} {couplerNetId}"); return; } @@ -889,7 +889,7 @@ public void SendHoseConnected(Coupler coupler, Coupler otherCoupler, bool playAu if (couplerNetId == 0 || otherCouplerNetId == 0) { - Multiplayer.LogWarning($"SendHoseConnected failed. Coupler: {coupler.name} {couplerNetId}, OtherCoupler: {otherCoupler.name} {otherCouplerNetId}"); + LogWarning($"SendHoseConnected failed. Coupler: {coupler.name} {couplerNetId}, OtherCoupler: {otherCoupler.name} {otherCouplerNetId}"); return; } @@ -909,7 +909,7 @@ public void SendHoseDisconnected(Coupler coupler, bool playAudio) if (couplerNetId == 0) { - Multiplayer.LogWarning($"SendHoseDisconnected failed. Coupler: {coupler.name} {couplerNetId}"); + LogWarning($"SendHoseDisconnected failed. Coupler: {coupler.name} {couplerNetId}"); return; } @@ -928,7 +928,7 @@ public void SendMuConnected(MultipleUnitCable cable, MultipleUnitCable otherCabl if (cableNetId == 0 || otherCableNetId == 0) { - Multiplayer.LogWarning($"SendMuConnected failed. Cable: {cable.muModule.train.name} {cableNetId}, OtherCable: {otherCable.muModule.train.name} {otherCableNetId}"); + LogWarning($"SendMuConnected failed. Cable: {cable.muModule.train.name} {cableNetId}, OtherCable: {otherCable.muModule.train.name} {otherCableNetId}"); return; } @@ -1011,7 +1011,7 @@ public void SendPorts(ushort netId, string[] portIds, float[] portValues) log += $"\r\n\t{portIds[i]}: {portValues[i]}"; } - Multiplayer.LogDebug(() => log); + LogDebug(() => log); */ } diff --git a/Multiplayer/Networking/Managers/NetworkManager.cs b/Multiplayer/Networking/Managers/NetworkManager.cs index 41dc518..6b81e25 100644 --- a/Multiplayer/Networking/Managers/NetworkManager.cs +++ b/Multiplayer/Networking/Managers/NetworkManager.cs @@ -40,7 +40,7 @@ private void RegisterNestedTypes() netPacketProcessor.RegisterNestedType(JobData.Serialize, JobData.Deserialize); netPacketProcessor.RegisterNestedType(ModInfo.Serialize, ModInfo.Deserialize); netPacketProcessor.RegisterNestedType(RigidbodySnapshot.Serialize, RigidbodySnapshot.Deserialize); - netPacketProcessor.RegisterNestedType(StationsChainDataData.Serialize, StationsChainDataData.Deserialize); + netPacketProcessor.RegisterNestedType(StationsChainNetworkData.Serialize, StationsChainNetworkData.Deserialize); netPacketProcessor.RegisterNestedType(TrainsetMovementPart.Serialize, TrainsetMovementPart.Deserialize); netPacketProcessor.RegisterNestedType(TrainsetSpawnPart.Serialize, TrainsetSpawnPart.Deserialize); netPacketProcessor.RegisterNestedType(Vector2Serializer.Serialize, Vector2Serializer.Deserialize); diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index c93bf3a..9af7215 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -111,7 +111,6 @@ protected override void Subscribe() netPacketProcessor.SubscribeReusable(OnServerboundClientReadyPacket); netPacketProcessor.SubscribeReusable(OnServerboundSaveGameDataRequestPacket); netPacketProcessor.SubscribeReusable(OnServerboundPlayerPositionPacket); - //netPacketProcessor.SubscribeReusable(OnServerboundPlayerCarPacket); netPacketProcessor.SubscribeReusable(OnServerboundTimeAdvancePacket); netPacketProcessor.SubscribeReusable(OnServerboundTrainSyncRequestPacket); netPacketProcessor.SubscribeReusable(OnServerboundTrainDeleteRequestPacket); @@ -388,7 +387,7 @@ public void SendDebtStatus(bool hasDebt) public void SendJobsCreatePacket(ushort stationID, NetworkedJob[] jobs) { - Multiplayer.Log($"Sending JobCreatePacket with {jobs.Length} jobs"); + Multiplayer.Log($"Sending JobsCreatePacket with {jobs.Count()} jobs"); SendPacketToAll(ClientboundJobsCreatePacket.FromNetworkedJobs(stationID, jobs),DeliveryMethod.ReliableSequenced); } From 0b2fdc7e27911129733f9aca6e0285a8dab7b247 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 31 Aug 2024 22:08:01 +1000 Subject: [PATCH 079/188] Continuing job sync code --- .../Networking/Jobs/NetworkedJob.cs | 183 +----------------- .../World/NetworkedStationController.cs | 40 +++- Multiplayer/Multiplayer.cs | 20 ++ Multiplayer/Networking/Data/JobData.cs | 21 +- .../Managers/Server/NetworkServer.cs | 32 +-- .../Jobs/ClientboundJobsCreatePacket.cs | 9 +- .../Patches/Jobs/JobOverviewUsePatch.cs | 24 ++- Multiplayer/Patches/Jobs/StationPatch.cs | 18 +- 8 files changed, 113 insertions(+), 234 deletions(-) diff --git a/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs b/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs index ef0c864..b74a935 100644 --- a/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs +++ b/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs @@ -43,30 +43,23 @@ public static bool TryGetFromJob(Job job, out NetworkedJob networkedJob) return jobToNetworkedJob.TryGetValue(job, out networkedJob); } #endregion + protected override bool IsIdServerAuthoritative => true; public Job Job; public JobOverview JobOverview; public JobBooklet JobBooklet; - public Station Station; - -// public bool isJobNew = true; - public bool isJobDirty = false; - public bool isTaskDirty = false; + public NetworkedStationController Station; public bool? allowTake = null; - public Guid takenBy; //GUID of player who took the job + public Guid OwnedBy = Guid.Empty; //GUID of player who took the job public JobValidator jobValidator; - //might be useful when a job is taken? - //public bool HasPlayers => PlayerManager.Car == Job || GetComponentInChildren() != null; - #region Client - private bool client_Initialized; + #endregion - protected override bool IsIdServerAuthoritative => true; private void Start() { @@ -76,21 +69,7 @@ private void Start() jobToNetworkedJob[Job] = this; jobIdToNetworkedJob[Job.ID] = this; jobIdToJob[Job.ID] = Job; - - //isJobNew = true; //Send new jobs on tick - - if (!NetworkLifecycle.Instance.IsHost()) - { - CoroutineManager.Instance.StartCoroutine(NetworkedStationController.UpdateCarPlates(Job.tasks, Job.ID)); - } - else - { - //setup even handlers - //job.JobTaken += this.OnJobTaken; - //job.JobExpired += this.OnJobExpired; - //NetworkLifecycle.Instance.OnTick += Server_OnTick; - } - + Multiplayer.Log("NetworkedJob.Start() Started"); } @@ -99,110 +78,25 @@ private void OnDisable() if (UnloadWatcher.isQuitting) return; - //NetworkLifecycle.Instance.OnTick -= Common_OnTick; - //NetworkLifecycle.Instance.OnTick -= Server_OnTick; if (UnloadWatcher.isUnloading) return; - - //job.JobTaken -= this.OnJobTaken; - - //jobToNetworkedJob.Remove(job); - //jobIdToNetworkedJob.Remove(job.ID); - //jobIdToNetworkedJob.Remove(job.ID); - - //Clean up any actions we added - - if (NetworkLifecycle.Instance.IsHost()) - { - //actions relating only to host - } + jobToNetworkedJob.Remove(Job); + jobIdToNetworkedJob.Remove(Job.ID); + jobIdToNetworkedJob.Remove(Job.ID); Destroy(this); } - /*public NetworkedJob(string stationID, Job job) - { - this.job = job; - this.stationID = stationID; - - //setup even handlers - //job.JobTaken += - - isJobNew = true; //Send new jobs on tick - - }*/ - #region Server - //wait for tasks? - - /* - public bool Server_ValidateClientTakeJob(ServerPlayer player, CommonTrainPortsPacket packet) - { - - return false; - } - */ - - /* - public bool Server_ValidateClientAbandonedJob(ServerPlayer player, CommonTrainPortsPacket packet) - { - - return false; - } - */ - - /* - public bool Server_ValidateClientCompleteJob(ServerPlayer player, CommonTrainPortsPacket packet) - { - - return false; - } - */ - - private void Server_OnTick(uint tick) { if (UnloadWatcher.isUnloading) return; - //Server_SendNewJob(); - //Server_SendJobStatus(); - //Server_SendTaskStatus(); - //Server_SendJobDestroy(); - - } - - /* - private void Server_SendNewJob() - { - if (!isJobNew) - return; - - isJobNew = false; - NetworkLifecycle.Instance.Server.SendJobCreatePacket(this); } - */ - /* - private void Server_SendJobStatus() - { - if (!sendCouplers) - return; - sendCouplers = false; - - if (Job.frontCoupler.hoseAndCock.IsHoseConnected) - NetworkLifecycle.Instance.Client.SendHoseConnected(Job.frontCoupler, Job.frontCoupler.coupledTo, false); - - if (Job.rearCoupler.hoseAndCock.IsHoseConnected) - NetworkLifecycle.Instance.Client.SendHoseConnected(Job.rearCoupler, Job.rearCoupler.coupledTo, false); - - NetworkLifecycle.Instance.Client.SendCockState(NetId, Job.frontCoupler, Job.frontCoupler.IsCockOpen); - NetworkLifecycle.Instance.Client.SendCockState(NetId, Job.rearCoupler, Job.rearCoupler.IsCockOpen); - } - */ - #endregion @@ -212,72 +106,11 @@ private void Common_OnTick(uint tick) { if (UnloadWatcher.isUnloading) return; - /* - Common_SendHandbrakePosition(); - Common_SendFuses(); - Common_SendPorts(); - */ - } - - public void OnJobTaken(Job jobTaken,bool _) - { - Multiplayer.Log($"JobTaken: {jobTaken.ID}"); - jobTaken.JobTaken -= this.OnJobTaken; - jobTaken.JobExpired -= this.OnJobExpired; - - /* - takenJob.JobCompleted += OnJobCompleted; - takenJob.JobAbandoned += OnJobAbandoned; - availableJobs.Remove(takenJob); - takenJobs.Add(takenJob); - */ - - isJobDirty = true; - /* - jobTaken.JobExpired -= this.OnJobExpired; - jobTaken.JobCompleted += this.OnJobCompleted; - jobTaken.JobAbandoned += this.OnJobAbandoned; - */ - } - - public void OnJobExpired(Job jobExpired) - { - Multiplayer.Log($"Job Expired: {Job.ID}"); - jobExpired.JobTaken -= this.OnJobTaken; - jobExpired.JobExpired -= this.OnJobExpired; - //jobExpired.JobCompleted += this.OnJobCompleted; - //jobExpired.JobAbandoned += this.OnJobAbandoned; - - isJobDirty = true; - } #endregion #region Client - /* - public void Client_ReceiveJopStatus(in TrainsetMovementPart movementPart, uint tick) - { - if (!client_Initialized) - return; - if (Job.isEligibleForSleep) - Job.ForceOptimizationState(false); - - if (movementPart.IsRigidbodySnapshot) - { - Job.Derail(); - Job.stress.ResetTrainStress(); - Client_trainRigidbodyQueue.ReceiveSnapshot(movementPart.RigidbodySnapshot, tick); - } - else - { - Client_trainSpeedQueue.ReceiveSnapshot(movementPart.Speed, tick); - Job.stress.slowBuildUpStress = movementPart.SlowBuildUpStress; - client_bogie1Queue.ReceiveSnapshot(movementPart.Bogie1, tick); - client_bogie2Queue.ReceiveSnapshot(movementPart.Bogie2, tick); - } - } - */ #endregion } diff --git a/Multiplayer/Components/Networking/World/NetworkedStationController.cs b/Multiplayer/Components/Networking/World/NetworkedStationController.cs index a8c609d..55abe5b 100644 --- a/Multiplayer/Components/Networking/World/NetworkedStationController.cs +++ b/Multiplayer/Components/Networking/World/NetworkedStationController.cs @@ -80,6 +80,7 @@ public static void RegisterStationController(NetworkedStationController networke public HashSet NetworkedJobs { get; } = new HashSet(); private List NewJobs = new List(); + private List DirtyJobs = new List(); //public List JobOverviews; //for later use private void Awake() @@ -113,6 +114,12 @@ public void AddJob(Job job) networkedJob.Job = job; NetworkedJobs.Add(networkedJob); NewJobs.Add(networkedJob); + + //Setup handlers + job.JobTaken += OnJobTaken; + job.JobAbandoned += OnJobAbandoned; + job.JobCompleted += OnJobCompleted; + job.JobExpired += OnJobExpired; } private void OnJobTaken(Job job, bool viaLoadGame) @@ -137,13 +144,21 @@ private void OnJobExpired(Job job) private void Server_OnTick(uint tick) { + //Send new jobs if (NewJobs.Count > 0) { NetworkLifecycle.Instance.Server.SendJobsCreatePacket(NetId, NewJobs.ToArray()); NewJobs.Clear(); } + + //Send jobs with a changed status + if (DirtyJobs.Count > 0) + { + //todo send packet with updates + } } + #region Client public void AddJobs(JobData[] jobs) { @@ -201,6 +216,8 @@ public void AddJobs(JobData[] jobs) // Create a new NetworkedJob NetworkedJob networkedJob = new GameObject($"NetworkedJob {newJob.ID}").AddComponent(); networkedJob.Job = newJob; + networkedJob.Station = this; + networkedJob.OwnedBy = jobData.OwnedBy; //NetworkLifecycle.Instance.Client.Log($"AddJobs() ID: {jobData?.ID}, netID: {jobData?.NetID}, NetJob Add"); NetworkedJobs.Add(networkedJob); @@ -209,13 +226,27 @@ public void AddJobs(JobData[] jobs) // Start coroutine to update car plates StartCoroutine(UpdateCarPlates(tasks, newJob.ID)); + //If the job is not owned by anyone, we can add it to the station + //if(networkedJob.OwnedBy == Guid.Empty) + StationController.logicStation.AddJobToStation(newJob); + + + //start coroutine for generating overviews and booklets + //StartCoroutine(CreatePaperWork()); + // Log the addition of the new job NetworkLifecycle.Instance.Client.Log($"AddJobs() {newJob?.ID} to NetworkedStationController {StationController?.logicStation?.ID}"); } + + //allow booklets to be created + StationController.attemptJobOverviewGeneration = true; + } + + public void UpdateJob() + { + } - #endregion - #region common functions public static IEnumerator UpdateCarPlates(List tasks, string jobId) { @@ -301,5 +332,10 @@ private static void UpdateCarPlatesRecursive(List tasks, stri Multiplayer.Log("NetworkedStation.UpdateCarPlatesRecursive() Returning"); } + + public IEnumerator CreatePaperWork() + { + yield return null; + } #endregion } diff --git a/Multiplayer/Multiplayer.cs b/Multiplayer/Multiplayer.cs index 7b6b64a..f06022c 100644 --- a/Multiplayer/Multiplayer.cs +++ b/Multiplayer/Multiplayer.cs @@ -2,8 +2,10 @@ using System.IO; using System.Linq; using System.Reflection; +using DV.UI; using HarmonyLib; using JetBrains.Annotations; +using Multiplayer.Components.MainMenu; using Multiplayer.Components.Networking; using Multiplayer.Editor; using Multiplayer.Patches.Mods; @@ -118,6 +120,24 @@ public static bool LoadAssets() return true; } + //private static void LateUpdate(UnityModManager.ModEntry modEntry, float deltaTime) + //{ + // if (ModEntry.NewestVersion != null && ModEntry.NewestVersion.ToString() != "") + // { + // Log($"Multiplayer Latest Version: {ModEntry.NewestVersion}"); + + // ModEntry.OnLateUpdate -= Multiplayer.LateUpdate; + + // if (ModEntry.NewestVersion > ModEntry.Version) + // { + // if (MainMenuThingsAndStuff.Instance != null) + // { + + // } + // } + // } + //} + #region Logging public static void LogDebug(Func resolver) diff --git a/Multiplayer/Networking/Data/JobData.cs b/Multiplayer/Networking/Data/JobData.cs index 39132c2..1418790 100644 --- a/Multiplayer/Networking/Data/JobData.cs +++ b/Multiplayer/Networking/Data/JobData.cs @@ -2,6 +2,8 @@ using DV.ThingTypes; using LiteNetLib.Utils; using Multiplayer.Components.Networking; +using Multiplayer.Components.Networking.Jobs; +using System; namespace Multiplayer.Networking.Data; @@ -18,12 +20,15 @@ public class JobData public float InitialWage { get; set; } public JobState State { get; set; } //serialise as byte public float TimeLimit { get; set; } + public Guid OwnedBy { get; set; } - public static JobData FromJob(ushort netID, Job job) + public static JobData FromJob(NetworkedJob networkedJob) { + Job job = networkedJob.Job; + return new JobData { - NetID = netID, + NetID = networkedJob.NetId, JobType = job.jobType, ID = job.ID, Tasks = TaskNetworkDataFactory.ConvertTasks(job.tasks), @@ -33,7 +38,8 @@ public static JobData FromJob(ushort netID, Job job) FinishTime = job.finishTime, InitialWage = job.initialWage, State = job.State, - TimeLimit = job.TimeLimit + TimeLimit = job.TimeLimit, + OwnedBy = networkedJob.OwnedBy }; } @@ -73,6 +79,10 @@ public static void Serialize(NetDataWriter writer, JobData data) //Multiplayer.Log($"JobData.Serialize({data.ID}) TimeLimit {data.TimeLimit}"); writer.Put(data.TimeLimit); //Multiplayer.Log(JsonConvert.SerializeObject(data, Formatting.None)); + + //Take on the GUID of the player + //if(data.State != JobState.Available) + // writer.Put(data.OwnedBy.ToByteArray()); } public static JobData Deserialize(NetDataReader reader) @@ -115,6 +125,8 @@ public static JobData Deserialize(NetDataReader reader) float timeLimit = reader.GetFloat(); //Multiplayer.Log("JobData.Deserialize() timeLimit: " + timeLimit); + //Guid ownedBy = (state != JobState.Available)? new(reader.GetBytesWithLength()) : Guid.Empty; + return new JobData { NetID = netID, @@ -127,7 +139,8 @@ public static JobData Deserialize(NetDataReader reader) FinishTime = finishTime, InitialWage = initialWage, State = state, - TimeLimit = timeLimit + TimeLimit = timeLimit, + //OwnedBy = ownedBy, }; } diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index 9af7215..06e0fa9 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -385,10 +385,10 @@ public void SendDebtStatus(bool hasDebt) }, DeliveryMethod.ReliableUnordered, selfPeer); } - public void SendJobsCreatePacket(ushort stationID, NetworkedJob[] jobs) + public void SendJobsCreatePacket(ushort stationID, NetworkedJob[] jobs, DeliveryMethod method = DeliveryMethod.ReliableSequenced ) { Multiplayer.Log($"Sending JobsCreatePacket with {jobs.Count()} jobs"); - SendPacketToAll(ClientboundJobsCreatePacket.FromNetworkedJobs(stationID, jobs),DeliveryMethod.ReliableSequenced); + SendPacketToAll(ClientboundJobsCreatePacket.FromNetworkedJobs(stationID, jobs), method); } public void SendChat(string message, NetPeer exclude = null) @@ -603,11 +603,7 @@ private void OnServerboundClientReadyPacket(ServerboundClientReadyPacket packet, NetworkedJob[] jobs = netStation.NetworkedJobs.ToArray(); for (int i = 0; i < jobs.Length; i++) { - //NetworkedJob[] batch = new NetworkedJob[5]; - - //Array.Copy(jobs,i,batch,0,5); - - SendJobsCreatePacket(netStation.NetId, [jobs[i]]); + SendJobsCreatePacket(netStation.NetId, [jobs[i]], DeliveryMethod.ReliableOrdered); } } else @@ -662,28 +658,6 @@ private void OnServerboundPlayerPositionPacket(ServerboundPlayerPositionPacket p SendPacketToAll(clientboundPacket, DeliveryMethod.Sequenced, peer); } - //private void OnServerboundPlayerCarPacket(ServerboundPlayerCarPacket packet, NetPeer peer) - //{ - // if (packet.CarId != 0 && !NetworkedTrainCar.Get(packet.CarId, out NetworkedTrainCar _)) - // return; - - // if (TryGetServerPlayer(peer, out ServerPlayer player)) - // { - // player.CarId = packet.CarId; - // player.RawPosition = packet.Position; - // player.RawRotationY = packet.RotationY; - - // } - - // ClientboundPlayerCarPacket clientboundPacket = new() - // { - // Id = (byte)peer.Id, - // CarId = packet.CarId - // }; - - // SendPacketToAll(clientboundPacket, DeliveryMethod.ReliableOrdered, peer); - //} - private void OnServerboundTimeAdvancePacket(ServerboundTimeAdvancePacket packet, NetPeer peer) { SendPacketToAll(new ClientboundTimeAdvancePacket diff --git a/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobsCreatePacket.cs b/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobsCreatePacket.cs index c009714..b69d632 100644 --- a/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobsCreatePacket.cs +++ b/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobsCreatePacket.cs @@ -3,11 +3,10 @@ using Multiplayer.Networking.Data; namespace Multiplayer.Networking.Packets.Clientbound.Jobs; -public class ClientboundJobsCreatePacket +public class ClientboundJobUpdatePacket { - public ushort StationNetId { get; set; } - public JobData[] Jobs { get; set; } - + public ushort NetId { get; set; } + /* public static ClientboundJobsCreatePacket FromNetworkedJobs(ushort stationID, NetworkedJob[] jobs) { List jobData = new List(); @@ -21,5 +20,5 @@ public static ClientboundJobsCreatePacket FromNetworkedJobs(ushort stationID, Ne StationNetId = stationID, Jobs = jobData.ToArray() }; - } + }*/ } diff --git a/Multiplayer/Patches/Jobs/JobOverviewUsePatch.cs b/Multiplayer/Patches/Jobs/JobOverviewUsePatch.cs index 4687890..6f1c4e0 100644 --- a/Multiplayer/Patches/Jobs/JobOverviewUsePatch.cs +++ b/Multiplayer/Patches/Jobs/JobOverviewUsePatch.cs @@ -12,14 +12,24 @@ using Unity.Jobs; using UnityEngine; using static UnityEngine.GraphicsBuffer; -/* Temp for stable release + namespace Multiplayer.Patches.Jobs; -//public void HandleUse(ItemUseTarget target) -[HarmonyPatch(typeof(JobOverviewUse), nameof(JobOverviewUse.HandleUse))] -public static class JobOverviewUse_HandleUse_Patch + +[HarmonyPatch(typeof(JobValidator))] +public static class JobValidator_Patch { - private static bool Prefix(JobOverviewUse __instance, ItemUseTarget target, ref JobOverview ___jobOverview) + [HarmonyPatch(nameof(JobValidator.ProcessJobOverview))] + private static bool Prefix(JobValidator __instance, JobOverview jobOverview) { + if (!NetworkLifecycle.Instance.IsHost()) + { + __instance.bookletPrinter.PlayErrorSound(); + return false; + } + + return true; + + /* JobValidator component = target.GetComponent(); if (component == null) return false; @@ -60,8 +70,6 @@ private static bool Prefix(JobOverviewUse __instance, ItemUseTarget target, ref component.bookletPrinter.PlayErrorSound(); return false; - + */ } } - -*/ diff --git a/Multiplayer/Patches/Jobs/StationPatch.cs b/Multiplayer/Patches/Jobs/StationPatch.cs index d4ceec2..add95b2 100644 --- a/Multiplayer/Patches/Jobs/StationPatch.cs +++ b/Multiplayer/Patches/Jobs/StationPatch.cs @@ -1,11 +1,7 @@ using DV.Logic.Job; using HarmonyLib; -using Multiplayer.Components; using Multiplayer.Components.Networking; -using Multiplayer.Components.Networking.Jobs; -using Multiplayer.Components.Networking.Train; using Multiplayer.Components.Networking.World; -using Multiplayer.Utils; namespace Multiplayer.Patches.Jobs; @@ -14,15 +10,15 @@ public static class Station_AddJobToStation_Patch { private static bool Prefix(Station __instance, Job job) { - if (!NetworkLifecycle.Instance.IsHost()) - return false; - Multiplayer.Log($"Station.AddJobToStation() adding NetworkJob for stationId: {__instance.ID}, jobId: {job.ID}"); + + if (NetworkLifecycle.Instance.IsHost()) + { + if(!NetworkedStationController.GetFromStationId(__instance.ID, out NetworkedStationController netStationController)) + return false; - if(!NetworkedStationController.GetFromStationId(__instance.ID, out NetworkedStationController netStationController)) - return false; - - netStationController.AddJob(job); + netStationController.AddJob(job); + } return true; } From bc807c50176792564c65a434b5ab8beeb5f12562 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 1 Sep 2024 12:44:33 +1000 Subject: [PATCH 080/188] Fix for early game rerails costing >$0 added check for early game, uses vanilla test, will need to be extended later for company mode or individual license modes. --- .../Networking/Managers/Server/NetworkServer.cs | 7 ++++++- .../Patches/CommsRadio/RerailControllerPatch.cs | 10 ++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index 06e0fa9..9d258f4 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -46,6 +46,7 @@ public class NetworkServer : NetworkManager public bool isPublic; public bool isSinglePlayer; public LobbyServerData serverData; + public RerailController rerailController; public IReadOnlyCollection ServerPlayers => serverPlayers.Values; public int PlayerCount => netManager.ConnectedPeersCount; @@ -840,7 +841,11 @@ private void OnServerboundTrainRerailRequestPacket(ServerboundTrainRerailRequest TrainCar trainCar = networkedTrainCar.TrainCar; Vector3 position = packet.Position + WorldMover.currentMove; - float cost = RerailController.CalculatePrice((networkedTrainCar.transform.position - position).magnitude, trainCar.carType, Globals.G.GameParams.RerailMaxPrice); + + //Check if player is a Newbie (currently shared with all players) + float cost = (TutorialHelper.InRestrictedMode || (rerailController != null && rerailController.isPlayerNewbie)) ? 0f : + RerailController.CalculatePrice((networkedTrainCar.transform.position - position).magnitude, trainCar.carType, Globals.G.GameParams.RerailMaxPrice); + if (!Inventory.Instance.RemoveMoney(cost)) { LogWarning($"{player.Username} tried to rerail a train without enough money to do so!"); diff --git a/Multiplayer/Patches/CommsRadio/RerailControllerPatch.cs b/Multiplayer/Patches/CommsRadio/RerailControllerPatch.cs index f683c34..00403e3 100644 --- a/Multiplayer/Patches/CommsRadio/RerailControllerPatch.cs +++ b/Multiplayer/Patches/CommsRadio/RerailControllerPatch.cs @@ -13,6 +13,16 @@ namespace Multiplayer.Patches.CommsRadio; [HarmonyPatch(typeof(RerailController))] public static class RerailControllerPatch { + [HarmonyPostfix] + [HarmonyPatch(nameof(RerailController.Awake))] + private static void OnAwake_Prefix(RerailController __instance) + { + if (!NetworkLifecycle.Instance.IsHost()) + return; + + NetworkLifecycle.Instance.Server.rerailController = __instance; + } + [HarmonyPrefix] [HarmonyPatch(nameof(RerailController.OnUse))] private static bool OnUse_Prefix(RerailController __instance) From de7d2520a948eebebac71d06c3660b67dc88c923 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 1 Sep 2024 12:45:50 +1000 Subject: [PATCH 081/188] Remove redundant code Related to commit 77f120aef7be9bbcf00a53083c35a6fdad76d6f0 --- .../Packets/Serverbound/ServerboundPlayerCarPacket.cs | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 Multiplayer/Networking/Packets/Serverbound/ServerboundPlayerCarPacket.cs diff --git a/Multiplayer/Networking/Packets/Serverbound/ServerboundPlayerCarPacket.cs b/Multiplayer/Networking/Packets/Serverbound/ServerboundPlayerCarPacket.cs deleted file mode 100644 index 2fd8ba7..0000000 --- a/Multiplayer/Networking/Packets/Serverbound/ServerboundPlayerCarPacket.cs +++ /dev/null @@ -1,11 +0,0 @@ -using UnityEngine; - -namespace Multiplayer.Networking.Packets.Serverbound; - -public class ServerboundPlayerCarPacket -{ - public ushort CarId { get; set; } - public Vector3 Position { get; set; } - public Vector2 MoveDir { get; set; } - public float RotationY { get; set; } -} From 3256da24ef4480912421e79db33b2caad8e98f8d Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 1 Sep 2024 18:18:59 +1000 Subject: [PATCH 082/188] Added framework for clients to take and complete jobs --- .../Networking/Jobs/NetworkedJob.cs | 10 +- .../Networking/Player/NetworkedPlayer.cs | 2 +- .../World/NetworkedStationController.cs | 19 +- Multiplayer/Multiplayer.csproj | 2 +- .../Managers/Client/ClientPlayerManager.cs | 4 +- .../Managers/Client/NetworkClient.cs | 50 ++--- .../Managers/Server/NetworkServer.cs | 109 ++++++++--- .../ClientboundPlayerJoinedPacket.cs | 2 +- .../Jobs/ClientboundJobTakeResponsePacket.cs | 12 -- .../ClientboundJobValidateResponsePacket.cs | 8 + .../Jobs/ClientboundJobsCreatePacket.cs | 19 +- .../Jobs/ClientboundJobsUpdatePacket.cs | 69 +++++++ .../Jobs/ServerboundJobTakeRequestPacket.cs | 10 - .../ServerboundJobValidateRequestPacket.cs | 13 ++ Multiplayer/Patches/Jobs/JobBookletPatch.cs | 33 ++++ Multiplayer/Patches/Jobs/JobOverviewPatch.cs | 45 +++++ .../Patches/Jobs/JobOverviewUsePatch.cs | 75 -------- Multiplayer/Patches/Jobs/JobValidatorPatch.cs | 171 ++++++++++++++++++ info.json | 2 +- 19 files changed, 478 insertions(+), 177 deletions(-) delete mode 100644 Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobTakeResponsePacket.cs create mode 100644 Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobValidateResponsePacket.cs create mode 100644 Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobsUpdatePacket.cs delete mode 100644 Multiplayer/Networking/Packets/Serverbound/Jobs/ServerboundJobTakeRequestPacket.cs create mode 100644 Multiplayer/Networking/Packets/Serverbound/Jobs/ServerboundJobValidateRequestPacket.cs create mode 100644 Multiplayer/Patches/Jobs/JobBookletPatch.cs create mode 100644 Multiplayer/Patches/Jobs/JobOverviewPatch.cs delete mode 100644 Multiplayer/Patches/Jobs/JobOverviewUsePatch.cs create mode 100644 Multiplayer/Patches/Jobs/JobValidatorPatch.cs diff --git a/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs b/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs index b74a935..381054d 100644 --- a/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs +++ b/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs @@ -50,9 +50,13 @@ public static bool TryGetFromJob(Job job, out NetworkedJob networkedJob) public JobBooklet JobBooklet; public NetworkedStationController Station; - public bool? allowTake = null; - public Guid OwnedBy = Guid.Empty; //GUID of player who took the job - public JobValidator jobValidator; + public Guid OwnedBy = Guid.Empty; //GUID of player who took the job (sever only) + public int playerID; //ID of player who took the job (client & server) + + public JobValidator jobValidator; //Job validator to print the booklet/job validation at (client only) + public bool ValidatorRequestSent = false; + public bool ValidatorResponseReceived = false; + public bool ValidationAccepted = false; #region Client diff --git a/Multiplayer/Components/Networking/Player/NetworkedPlayer.cs b/Multiplayer/Components/Networking/Player/NetworkedPlayer.cs index fec0ea6..59519c5 100644 --- a/Multiplayer/Components/Networking/Player/NetworkedPlayer.cs +++ b/Multiplayer/Components/Networking/Player/NetworkedPlayer.cs @@ -10,7 +10,7 @@ public class NetworkedPlayer : MonoBehaviour private const float LERP_SPEED = 5.0f; public byte Id; - public Guid Guid; + //public Guid Guid; private AnimationHandler animationHandler; private NameTag nameTag; diff --git a/Multiplayer/Components/Networking/World/NetworkedStationController.cs b/Multiplayer/Components/Networking/World/NetworkedStationController.cs index 55abe5b..691eb14 100644 --- a/Multiplayer/Components/Networking/World/NetworkedStationController.cs +++ b/Multiplayer/Components/Networking/World/NetworkedStationController.cs @@ -17,6 +17,7 @@ public class NetworkedStationController : IdMonoBehaviour stationIdToNetworkedStationController = new(); private static readonly Dictionary stationIdToStationController = new(); private static readonly Dictionary stationToNetworkedStationController = new(); + private static readonly Dictionary jobValidatorToNetworkedStation = new(); public static bool Get(ushort netId, out NetworkedStationController obj) { @@ -62,6 +63,11 @@ public static bool GetFromStationController(StationController stationController, return stationControllerToNetworkedStationController.TryGetValue(stationController, out networkedStationController); } + public static bool GetFromJobValidator(JobValidator jobValidator, out NetworkedStationController networkedStationController) + { + return jobValidatorToNetworkedStation.TryGetValue(jobValidator, out networkedStationController); + } + public static void RegisterStationController(NetworkedStationController networkedStationController, StationController stationController) { string stationID = stationController.logicStation.ID; @@ -70,7 +76,16 @@ public static void RegisterStationController(NetworkedStationController networke stationIdToNetworkedStationController.Add(stationID, networkedStationController); stationIdToStationController.Add(stationID, stationController); stationToNetworkedStationController.Add(stationController.logicStation, networkedStationController); -} + } + + public static void RegisterJobValidator(JobValidator jobValidator, NetworkedStationController stationController) + { + if (jobValidator == null || stationController == null) + return; + + stationController.JobValidator = jobValidator; + jobValidatorToNetworkedStation[jobValidator] = stationController; + } #endregion @@ -78,6 +93,8 @@ public static void RegisterStationController(NetworkedStationController networke private StationController StationController; + public JobValidator JobValidator; + public HashSet NetworkedJobs { get; } = new HashSet(); private List NewJobs = new List(); private List DirtyJobs = new List(); diff --git a/Multiplayer/Multiplayer.csproj b/Multiplayer/Multiplayer.csproj index 3ff1b28..2af864a 100644 --- a/Multiplayer/Multiplayer.csproj +++ b/Multiplayer/Multiplayer.csproj @@ -3,7 +3,7 @@ net48 latest Multiplayer - 0.1.8.2 + 0.1.8.3 diff --git a/Multiplayer/Networking/Managers/Client/ClientPlayerManager.cs b/Multiplayer/Networking/Managers/Client/ClientPlayerManager.cs index a3245c2..caf86cd 100644 --- a/Multiplayer/Networking/Managers/Client/ClientPlayerManager.cs +++ b/Multiplayer/Networking/Managers/Client/ClientPlayerManager.cs @@ -27,14 +27,14 @@ public bool TryGetPlayer(byte id, out NetworkedPlayer player) return playerMap.TryGetValue(id, out player); } - public void AddPlayer(byte id, string username, Guid guid) + public void AddPlayer(byte id, string username) { GameObject go = Object.Instantiate(playerPrefab, WorldMover.Instance.originShiftParent); go.layer = LayerMask.NameToLayer(Layers.Player); NetworkedPlayer networkedPlayer = go.AddComponent(); networkedPlayer.Id = id; networkedPlayer.Username = username; - networkedPlayer.Guid = guid; + //networkedPlayer.Guid = guid; playerMap.Add(id, networkedPlayer); OnPlayerConnected?.Invoke(id, networkedPlayer); } diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index d710c07..345b8b3 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -86,7 +86,6 @@ protected override void Subscribe() netPacketProcessor.SubscribeReusable(OnClientboundPlayerDisconnectPacket); netPacketProcessor.SubscribeReusable(OnClientboundPlayerKickPacket); netPacketProcessor.SubscribeReusable(OnClientboundPlayerPositionPacket); - //netPacketProcessor.SubscribeReusable(OnClientboundPlayerCarPacket); netPacketProcessor.SubscribeReusable(OnClientboundPingUpdatePacket); netPacketProcessor.SubscribeReusable(OnClientboundTickSyncPacket); netPacketProcessor.SubscribeReusable(OnClientboundServerLoadingPacket); @@ -126,7 +125,7 @@ protected override void Subscribe() netPacketProcessor.SubscribeReusable(OnClientboundGarageUnlockPacket); netPacketProcessor.SubscribeReusable(OnClientboundDebtStatusPacket); netPacketProcessor.SubscribeReusable(OnClientboundJobsCreatePacket); - //netPacketProcessor.SubscribeReusable(OnClientboundJobTakeResponsePacket); + netPacketProcessor.SubscribeReusable(OnClientboundJobValidateResponsePacket); netPacketProcessor.SubscribeReusable(OnCommonChatPacket); } @@ -249,9 +248,9 @@ private void OnClientboundServerDenyPacket(ClientboundServerDenyPacket packet) private void OnClientboundPlayerJoinedPacket(ClientboundPlayerJoinedPacket packet) { - Guid guid = new(packet.Guid); - ClientPlayerManager.AddPlayer(packet.Id, packet.Username, guid); - //ClientPlayerManager.UpdateCar(packet.Id, packet.TrainCar); + //Guid guid = new(packet.Guid); + ClientPlayerManager.AddPlayer(packet.Id, packet.Username); + ClientPlayerManager.UpdatePosition(packet.Id, packet.Position, Vector3.zero, packet.Rotation, false, packet.CarID != 0, packet.CarID); } @@ -272,11 +271,6 @@ private void OnClientboundPlayerPositionPacket(ClientboundPlayerPositionPacket p ClientPlayerManager.UpdatePosition(packet.Id, packet.Position, packet.MoveDir, packet.RotationY, packet.IsJumping, packet.IsOnCar, packet.CarID); } - //private void OnClientboundPlayerCarPacket(ClientboundPlayerCarPacket packet) - //{ - // ClientPlayerManager.UpdateCar(packet.Id, packet.CarId); - //} - private void OnClientboundPingUpdatePacket(ClientboundPingUpdatePacket packet) { ClientPlayerManager.UpdatePing(packet.Id, packet.Ping); @@ -733,7 +727,6 @@ private void OnClientboundDebtStatusPacket(ClientboundDebtStatusPacket packet) } private void OnCommonChatPacket(CommonChatPacket packet) { - chatGUI.ReceiveMessage(packet.message); } @@ -755,29 +748,17 @@ private void OnClientboundJobsCreatePacket(ClientboundJobsCreatePacket packet) } - /* - private void OnClientboundJobTakeResponsePacket(ClientboundJobTakeResponsePacket packet) + + private void OnClientboundJobValidateResponsePacket(ClientboundJobValidateResponsePacket packet) { - Log($"OnClientboundJobTakeResponsePacket jobId: {packet.netId}, Status: {packet.granted}"); - - NetworkedJob networkedJob; + Log($"OnClientboundJobValidateResponsePacket() JobNetId: {packet.JobNetId}, Status: {packet.Accepted}"); - if(!NetworkedJob.Get(packet.netId, out networkedJob)) + if(!NetworkedJob.Get(packet.JobNetId, out NetworkedJob networkedJob)) return; - NetworkedPlayer player; - if (ClientPlayerManager.TryGetPlayer(packet.playerId, out player)) - { - networkedJob.takenBy = player.Guid; - } - - Log($"OnClientboundJobTakeResponsePacket jobId: {networkedJob.job.ID}, Status: {packet.granted}"); - networkedJob.allowTake = packet.granted; - networkedJob.jobValidator.ProcessJobOverview(networkedJob.jobOverview); - networkedJob.jobValidator = null; - networkedJob.jobOverview = null; + networkedJob.ValidatorResponseReceived = true; + networkedJob.ValidationAccepted = packet.Accepted; } - */ #endregion @@ -1061,15 +1042,16 @@ public void SendLicensePurchaseRequest(string id, bool isJobLicense) }, DeliveryMethod.ReliableUnordered); } - /* Temp for stable release - public void SendJobTakeRequest(ushort netId) + public void SendJobValidateRequest(ushort jobNetId, ushort stationNetId, ValidationType type) { - SendPacketToServer(new ServerboundJobTakeRequestPacket + SendPacketToServer(new ServerboundJobValidateRequestPacket { - netId = netId + JobNetId = jobNetId, + StationNetId = stationNetId, + validationType = type }, DeliveryMethod.ReliableUnordered); } -*/ + public void SendChat(string message) { SendPacketToServer(new CommonChatPacket diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index 9d258f4..b234195 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -132,7 +132,7 @@ protected override void Subscribe() netPacketProcessor.SubscribeReusable(OnServerboundFireboxIgnitePacket); netPacketProcessor.SubscribeReusable(OnCommonTrainPortsPacket); netPacketProcessor.SubscribeReusable(OnCommonTrainFusesPacket); - netPacketProcessor.SubscribeReusable(OnServerboundJobTakeRequestPacket); + netPacketProcessor.SubscribeReusable(OnServerboundJobValidateRequestPacket); netPacketProcessor.SubscribeReusable(OnCommonChatPacket); } @@ -386,10 +386,22 @@ public void SendDebtStatus(bool hasDebt) }, DeliveryMethod.ReliableUnordered, selfPeer); } - public void SendJobsCreatePacket(ushort stationID, NetworkedJob[] jobs, DeliveryMethod method = DeliveryMethod.ReliableSequenced ) + public void SendJobsCreatePacket(ushort stationNetId, NetworkedJob[] jobs, DeliveryMethod method = DeliveryMethod.ReliableSequenced ) { Multiplayer.Log($"Sending JobsCreatePacket with {jobs.Count()} jobs"); - SendPacketToAll(ClientboundJobsCreatePacket.FromNetworkedJobs(stationID, jobs), method); + SendPacketToAll(ClientboundJobsCreatePacket.FromNetworkedJobs(stationNetId, jobs), method); + } + + public void SendJobsUpdatePacket(JobUpdateStruct[] jobs, NetPeer peer = null) + { + if (peer != null) + { + SendPacketToAll(new ClientboundJobsUpdatePacket { JobUpdates = jobs }, DeliveryMethod.ReliableUnordered); + } + else + { + SendPacket(peer, new ClientboundJobsUpdatePacket { JobUpdates = jobs }, DeliveryMethod.ReliableUnordered); + } } public void SendChat(string message, NetPeer exclude = null) @@ -559,7 +571,7 @@ private void OnServerboundClientReadyPacket(ServerboundClientReadyPacket packet, { Id = peerId, Username = serverPlayer.Username, - Guid = serverPlayer.Guid.ToByteArray() + //Guid = serverPlayer.Guid.ToByteArray() }; SendPacketToAll(clientboundPlayerJoinedPacket, DeliveryMethod.ReliableOrdered, peer); @@ -623,7 +635,7 @@ private void OnServerboundClientReadyPacket(ServerboundClientReadyPacket packet, { Id = player.Id, Username = player.Username, - Guid = player.Guid.ToByteArray(), + //Guid = player.Guid.ToByteArray(), CarID = player.CarId, Position = player.RawPosition, Rotation = player.RawRotationY @@ -892,40 +904,83 @@ private void OnServerboundLicensePurchaseRequestPacket(ServerboundLicensePurchas } - private void OnServerboundJobTakeRequestPacket(ServerboundJobTakeRequestPacket packet, NetPeer peer) + private void OnServerboundJobValidateRequestPacket(ServerboundJobValidateRequestPacket packet, NetPeer peer) { - /* Temp for stable release - NetworkedJob networkedJob; - if (!NetworkedJob.Get(packet.netId, out networkedJob)) + if (!NetworkedJob.Get(packet.JobNetId, out NetworkedJob networkedJob)) { - Multiplayer.Log($"OnServerboundJobTakeRequestPacket netId Not Found: {packet.netId}"); + LogWarning($"OnServerboundJobValidateRequestPacket() NetworkedJob not found: {packet.JobNetId}"); + + JobUpdateStruct invalidJob = new JobUpdateStruct(); + invalidJob.JobNetID = packet.JobNetId; + invalidJob.Invalid = true; + + SendJobsUpdatePacket([invalidJob],peer); return; } - if (networkedJob.job.State != JobState.Available) { - - Multiplayer.Log($"OnServerboundJobTakeRequestPacket jobId: {networkedJob.job.ID}, DENIED"); - ServerPlayer player = ServerPlayers.First(x => x.Guid == networkedJob.takenBy); - //deny the request - SendPacket(peer, new ClientboundJobTakeResponsePacket { netId = packet.netId, granted = false, playerId = player.Id }, DeliveryMethod.ReliableOrdered); + if (TryGetServerPlayer(peer,out ServerPlayer player)) + { + LogWarning($"OnServerboundJobValidateRequestPacket() ServerPlayer not found: {peer.Id}"); + return; } - else + + //Find the station and validator + if (!NetworkedStationController.Get(packet.StationNetId, out NetworkedStationController networkedStationController) || networkedStationController.JobValidator == null) { - //probably need to do more here - ServerPlayer player; - if (!TryGetServerPlayer(peer, out player)) - return; + LogWarning($"OnServerboundJobValidateRequestPacket() JobValidator not found. StationNetId: {packet.StationNetId}, StationController found: {networkedStationController != null}, JobValidator found: {networkedStationController?.JobValidator != null}"); + return; + } - networkedJob.takenBy = player.Guid; - //networkedJob.job.State = JobState.InProgress; + ClientboundJobValidateResponsePacket responsePacket = new ClientboundJobValidateResponsePacket { JobNetId = packet.JobNetId, Accepted = false}; - //todo: officially take the job - Multiplayer.Log($"OnServerboundJobTakeRequestPacket jobId: {networkedJob.job.ID}, GRANTED"); - SendPacket(peer, new ClientboundJobTakeResponsePacket { netId = packet.netId, granted = true, playerId = player.Id }, DeliveryMethod.ReliableOrdered); + switch (packet.validationType) + { + case ValidationType.JobOverview: + if (networkedJob.Job.State != JobState.Available) + { + Log($"OnServerboundJobValidateRequestPacket({networkedJob.Job?.ID}) JobState: {networkedJob.Job.State}, DENIED"); + } + else if(networkedJob.JobOverview == null) + { + Log($"OnServerboundJobValidateRequestPacket({networkedJob.Job?.ID}) JobOverview does not exist, DENIED"); + } + else + { + networkedStationController.JobValidator.ProcessJobOverview(networkedJob.JobOverview); + if(networkedJob.JobBooklet != null) + { + Log($"OnServerboundJobValidateRequestPacket({networkedJob.Job?.ID}) JobState: {networkedJob.Job.State}, ACCEPTED"); + responsePacket.Accepted = true; + networkedJob.OwnedBy = player.Guid; + networkedJob.playerID = peer.Id; + + } + else + { + Log($"OnServerboundJobValidateRequestPacket({networkedJob.Job?.ID}) Failed to generate booklet, DENIED"); + } + } + break; + case ValidationType.JobBooklet: + if (networkedJob.Job.State != JobState.InProgress) + { + Log($"OnServerboundJobValidateRequestPacket({networkedJob.Job?.ID}) JobState: {networkedJob.Job.State}, DENIED"); + } + else if (networkedJob.JobBooklet == null) + { + Log($"OnServerboundJobValidateRequestPacket({networkedJob.Job?.ID}) JobBooklet does not exist, DENIED"); + } + else + { + networkedStationController.JobValidator.ValidateJob(networkedJob.JobBooklet); + responsePacket.Accepted = true; + } + break; } - */ + + SendPacket(peer, responsePacket, DeliveryMethod.ReliableOrdered); } private void OnCommonChatPacket(CommonChatPacket packet, NetPeer peer) diff --git a/Multiplayer/Networking/Packets/Clientbound/ClientboundPlayerJoinedPacket.cs b/Multiplayer/Networking/Packets/Clientbound/ClientboundPlayerJoinedPacket.cs index d925e40..befc8c3 100644 --- a/Multiplayer/Networking/Packets/Clientbound/ClientboundPlayerJoinedPacket.cs +++ b/Multiplayer/Networking/Packets/Clientbound/ClientboundPlayerJoinedPacket.cs @@ -6,7 +6,7 @@ public class ClientboundPlayerJoinedPacket { public byte Id { get; set; } public string Username { get; set; } - public byte[] Guid { get; set; } + //public byte[] Guid { get; set; } public ushort CarID { get; set; } public Vector3 Position { get; set; } public float Rotation { get; set; } diff --git a/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobTakeResponsePacket.cs b/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobTakeResponsePacket.cs deleted file mode 100644 index 53b0bd9..0000000 --- a/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobTakeResponsePacket.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Multiplayer.Components.Networking.Jobs; -using Multiplayer.Networking.Data; -using Multiplayer.Networking.Packets.Clientbound.Train; - -namespace Multiplayer.Networking.Packets.Clientbound.Jobs; - -public class ClientboundJobTakeResponsePacket -{ - public ushort netId { get; set; } - public bool granted { get; set; } - public byte playerId { get; set; } -} diff --git a/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobValidateResponsePacket.cs b/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobValidateResponsePacket.cs new file mode 100644 index 0000000..54a7e09 --- /dev/null +++ b/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobValidateResponsePacket.cs @@ -0,0 +1,8 @@ + +namespace Multiplayer.Networking.Packets.Clientbound.Jobs; + +public class ClientboundJobValidateResponsePacket +{ + public ushort JobNetId { get; set; } + public bool Accepted { get; set; } +} diff --git a/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobsCreatePacket.cs b/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobsCreatePacket.cs index b69d632..61d3c3a 100644 --- a/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobsCreatePacket.cs +++ b/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobsCreatePacket.cs @@ -3,22 +3,23 @@ using Multiplayer.Networking.Data; namespace Multiplayer.Networking.Packets.Clientbound.Jobs; -public class ClientboundJobUpdatePacket +public class ClientboundJobsCreatePacket { - public ushort NetId { get; set; } - /* + public ushort StationNetId { get; set; } + public JobData[] Jobs { get; set; } + public static ClientboundJobsCreatePacket FromNetworkedJobs(ushort stationID, NetworkedJob[] jobs) { List jobData = new List(); foreach (var job in jobs) { - jobData.Add(JobData.FromJob(job.NetId, job.Job)); + jobData.Add(JobData.FromJob(job)); } return new ClientboundJobsCreatePacket - { - StationNetId = stationID, - Jobs = jobData.ToArray() - }; - }*/ + { + StationNetId = stationID, + Jobs = jobData.ToArray() + }; + } } diff --git a/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobsUpdatePacket.cs b/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobsUpdatePacket.cs new file mode 100644 index 0000000..886e914 --- /dev/null +++ b/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobsUpdatePacket.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using DV.ThingTypes; +using LiteNetLib.Utils; +using Multiplayer.Components.Networking.Jobs; +using Multiplayer.Networking.Data; +namespace Multiplayer.Networking.Packets.Clientbound.Jobs; + +public struct JobUpdateStruct : INetSerializable +{ + public ushort JobNetID; + public bool Invalid; + public JobState JobState; + public float StartTime; + public float FinishTime; + public Guid OwnedBy; + + public void Serialize(NetDataWriter writer) + { + writer.Put(JobNetID); + writer.Put(Invalid); + + //Invalid jobs will be deleted / deregistered + if (Invalid) + return; + + writer.Put((byte)JobState); + writer.Put(StartTime); + writer.Put(FinishTime); + + if(JobState == JobState.InProgress) + writer.Put(OwnedBy.ToByteArray()); + } + + public void Deserialize(NetDataReader reader) + { + JobNetID = reader.GetUShort(); + Invalid = reader.GetBool(); + + if (Invalid) + return; + + JobState = (JobState) reader.GetByte(); + StartTime = reader.GetFloat(); + FinishTime = reader.GetFloat(); + OwnedBy = (JobState == JobState.InProgress) ? new(reader.GetBytesWithLength()) : Guid.Empty; + } +} +public class ClientboundJobsUpdatePacket +{ + public JobUpdateStruct[] JobUpdates { get; set; } + + /* + public static ClientboundJobsUpdatePacket FromNetworkedJobs(ushort stationID, NetworkedJob[] jobs) + { + List jobData = new List(); + foreach (var job in jobs) + { + jobData.Add(JobData.FromJob(job)); + } + + return new ClientboundJobsCreatePacket + { + StationNetId = stationID, + Jobs = jobData.ToArray() + }; + } + */ +} diff --git a/Multiplayer/Networking/Packets/Serverbound/Jobs/ServerboundJobTakeRequestPacket.cs b/Multiplayer/Networking/Packets/Serverbound/Jobs/ServerboundJobTakeRequestPacket.cs deleted file mode 100644 index 895d5fe..0000000 --- a/Multiplayer/Networking/Packets/Serverbound/Jobs/ServerboundJobTakeRequestPacket.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Multiplayer.Components.Networking.Jobs; -using Multiplayer.Networking.Data; -using Multiplayer.Networking.Packets.Clientbound.Train; - -namespace Multiplayer.Networking.Packets.Clientbound.Jobs; - -public class ServerboundJobTakeRequestPacket -{ - public ushort netId { get; set; } -} diff --git a/Multiplayer/Networking/Packets/Serverbound/Jobs/ServerboundJobValidateRequestPacket.cs b/Multiplayer/Networking/Packets/Serverbound/Jobs/ServerboundJobValidateRequestPacket.cs new file mode 100644 index 0000000..2119097 --- /dev/null +++ b/Multiplayer/Networking/Packets/Serverbound/Jobs/ServerboundJobValidateRequestPacket.cs @@ -0,0 +1,13 @@ +namespace Multiplayer.Networking.Packets.Clientbound.Jobs; + +public enum ValidationType : byte +{ + JobOverview, + JobBooklet +} +public class ServerboundJobValidateRequestPacket +{ + public ushort JobNetId { get; set; } + public ushort StationNetId { get; set; } + public ValidationType validationType { get; set; } +} diff --git a/Multiplayer/Patches/Jobs/JobBookletPatch.cs b/Multiplayer/Patches/Jobs/JobBookletPatch.cs new file mode 100644 index 0000000..c297dc3 --- /dev/null +++ b/Multiplayer/Patches/Jobs/JobBookletPatch.cs @@ -0,0 +1,33 @@ +using HarmonyLib; +using Multiplayer.Components.Networking.Jobs; + + +namespace Multiplayer.Patches.Jobs; + +[HarmonyPatch(typeof(JobBooklet))] +public static class JobBooklet_Patch +{ + [HarmonyPatch(nameof(JobBooklet.Awake))] + [HarmonyPostfix] + private static void Awake(JobBooklet __instance) + { + if(!NetworkedJob.TryGetFromJob(__instance.job, out NetworkedJob networkedJob)) + { + Multiplayer.LogError($"JobBooklet.Awake() NetworkedJob not found for Job ID: {__instance.job?.ID}"); + return; + } + + networkedJob.JobBooklet = __instance; + } + + + [HarmonyPatch(nameof(JobBooklet.DestroyJobBooklet))] + [HarmonyPrefix] + private static void DestroyJobBooklet(JobBooklet __instance) + { + if (!NetworkedJob.TryGetFromJob(__instance.job, out NetworkedJob networkedJob)) + Multiplayer.LogError($"JobBooklet.DestroyJobBooklet() NetworkedJob not found for Job ID: {__instance.job?.ID}"); + else + networkedJob.JobBooklet = null; + } +} diff --git a/Multiplayer/Patches/Jobs/JobOverviewPatch.cs b/Multiplayer/Patches/Jobs/JobOverviewPatch.cs new file mode 100644 index 0000000..3e2617a --- /dev/null +++ b/Multiplayer/Patches/Jobs/JobOverviewPatch.cs @@ -0,0 +1,45 @@ +using DV; +using DV.Interaction; +using DV.Logic.Job; +using DV.ThingTypes; +using DV.Utils; +using HarmonyLib; +using Multiplayer.Components; +using Multiplayer.Components.Networking; +using Multiplayer.Components.Networking.Jobs; +using Multiplayer.Utils; +using System.Collections; +using Unity.Jobs; +using UnityEngine; +using static UnityEngine.GraphicsBuffer; + +namespace Multiplayer.Patches.Jobs; + +[HarmonyPatch(typeof(JobOverview))] +public static class JobOverview_Patch +{ + [HarmonyPatch(nameof(JobOverview.Start))] + [HarmonyPostfix] + private static void Start(JobOverview __instance) + { + if(!NetworkedJob.TryGetFromJob(__instance.job, out NetworkedJob networkedJob)) + { + Multiplayer.LogError($"JobOverview.Start() NetworkedJob not found for Job ID: {__instance.job?.ID}"); + __instance.DestroyJobOverview(); + return; + } + + networkedJob.JobOverview = __instance; + } + + + [HarmonyPatch(nameof(JobOverview.DestroyJobOverview))] + [HarmonyPrefix] + private static void DestroyJobOverview(JobOverview __instance) + { + if (!NetworkedJob.TryGetFromJob(__instance.job, out NetworkedJob networkedJob)) + Multiplayer.LogError($"JobOverview.DestroyJobOverview() NetworkedJob not found for Job ID: {__instance.job}"); + else + networkedJob.JobOverview = null; + } +} diff --git a/Multiplayer/Patches/Jobs/JobOverviewUsePatch.cs b/Multiplayer/Patches/Jobs/JobOverviewUsePatch.cs deleted file mode 100644 index 6f1c4e0..0000000 --- a/Multiplayer/Patches/Jobs/JobOverviewUsePatch.cs +++ /dev/null @@ -1,75 +0,0 @@ -using DV; -using DV.Interaction; -using DV.Logic.Job; -using DV.ThingTypes; -using DV.Utils; -using HarmonyLib; -using Multiplayer.Components; -using Multiplayer.Components.Networking; -using Multiplayer.Components.Networking.Jobs; -using Multiplayer.Utils; -using System.Collections; -using Unity.Jobs; -using UnityEngine; -using static UnityEngine.GraphicsBuffer; - -namespace Multiplayer.Patches.Jobs; - -[HarmonyPatch(typeof(JobValidator))] -public static class JobValidator_Patch -{ - [HarmonyPatch(nameof(JobValidator.ProcessJobOverview))] - private static bool Prefix(JobValidator __instance, JobOverview jobOverview) - { - if (!NetworkLifecycle.Instance.IsHost()) - { - __instance.bookletPrinter.PlayErrorSound(); - return false; - } - - return true; - - /* - JobValidator component = target.GetComponent(); - if (component == null) - return false; - - if (component.bookletPrinter.IsOnCooldown) - { - component.bookletPrinter.PlayErrorSound(); - return false; - } - - Job job = ___jobOverview.job; - - Multiplayer.Log($"JobOverviewUse_HandleUse_Patch jobId: {job.ID}"); - - NetworkedJob networkedJob; - - if (!NetworkedJob.TryGetFromJob(job, out networkedJob)) - { - Multiplayer.Log($"JobOverviewUse_HandleUse_Patch No netId found for jobId: {job.ID}"); - component.bookletPrinter.PlayErrorSound(); - return false; - } - - if(networkedJob.allowTake == true) { - Multiplayer.Log($"JobOverviewUse_HandleUse_Patch jobId: {job.ID}, Take allowed: {networkedJob.allowTake}"); - return true; - } - else if (networkedJob.allowTake == null || (networkedJob.allowTake == false && networkedJob.takenBy == null)) - { - Multiplayer.Log($"JobOverviewUse_HandleUse_Patch WaitForResponse returned for jobId: {job.ID}"); - networkedJob.jobValidator = component; - networkedJob.jobOverview = ___jobOverview; - NetworkLifecycle.Instance.Client.SendJobTakeRequest(networkedJob.NetId); - - return false; - - } - - component.bookletPrinter.PlayErrorSound(); - return false; - */ - } -} diff --git a/Multiplayer/Patches/Jobs/JobValidatorPatch.cs b/Multiplayer/Patches/Jobs/JobValidatorPatch.cs new file mode 100644 index 0000000..5e77d6c --- /dev/null +++ b/Multiplayer/Patches/Jobs/JobValidatorPatch.cs @@ -0,0 +1,171 @@ +using System; +using System.Collections; +using System.Linq; +using DV; +using DV.ThingTypes; +using HarmonyLib; +using Multiplayer.Components.Networking; +using Multiplayer.Components.Networking.Jobs; +using Multiplayer.Components.Networking.World; +using Multiplayer.Networking.Packets.Clientbound.Jobs; + +using UnityEngine; + +namespace Multiplayer.Patches.Jobs; + +[HarmonyPatch(typeof(JobValidator))] +public static class JobValidator_Patch +{ + [HarmonyPatch(nameof(JobValidator.Start))] + [HarmonyPostfix] + private static void Start_Postfix(JobValidator __instance) + { + + string stationName = __instance.transform.parent.name ?? ""; + + if (string.IsNullOrEmpty(stationName)) + { + Multiplayer.LogError($"JobValidator.Start() Can not find parent's name"); + return; + } + + stationName += "_office_anchor"; + + StationController[] stations = StationController.allStations.Where(s => s.transform.parent.name.Equals(stationName,StringComparison.OrdinalIgnoreCase)).ToArray(); + + if (stations.Length == 1) + { + if(!NetworkedStationController.GetFromStationController(stations.First(), out NetworkedStationController networkedStationController)) + Multiplayer.LogError($"JobValidator.Start() Could not find NetworkedStation for validator: {stationName}"); + else + NetworkedStationController.RegisterJobValidator(__instance, networkedStationController); + } + else + { + Multiplayer.LogError($"JobValidator.Start() Found {stations.Length} stations for {stationName}"); + } + } + + [HarmonyPatch(nameof(JobValidator.ProcessJobOverview))] + [HarmonyPrefix] + private static bool ProcessJobOverview_Prefix(JobValidator __instance, JobOverview jobOverview) + { + if (NetworkLifecycle.Instance.IsHost()) + return true; + + if(__instance.bookletPrinter.IsOnCooldown) + { + __instance.bookletPrinter.PlayErrorSound(); + return false; + } + + if(!NetworkedJob.TryGetFromJob(jobOverview.job, out NetworkedJob networkedJob) || jobOverview.job.State != JobState.Available) + { + NetworkLifecycle.Instance.Client.LogWarning($"ProcessJobOverview_Prefix({jobOverview?.job?.ID}) NetworkedJob found: {networkedJob != null}, Job state: {jobOverview?.job?.State}"); + __instance.bookletPrinter.PlayErrorSound(); + jobOverview.DestroyJobOverview(); + return false; + } + + if(networkedJob.ValidatorRequestSent) + { + if (networkedJob.ValidatorResponseReceived && networkedJob.ValidationAccepted) + return true; + } + else + { + if(NetworkedStationController.GetFromJobValidator(__instance, out NetworkedStationController networkedStation)) + { + //Set initial job state parameters + networkedJob.ValidatorRequestSent = true; + networkedJob.ValidatorResponseReceived = false; + networkedJob.ValidationAccepted = false; + + NetworkLifecycle.Instance.Client.SendJobValidateRequest(networkedJob.NetId, networkedStation.NetId, ValidationType.JobOverview); + CoroutineManager.Instance.StartCoroutine(AwaitResponse(__instance, networkedJob, ValidationType.JobOverview)); + } + else + { + NetworkLifecycle.Instance.Client.LogError($"ProcessJobOverview_Prefix({jobOverview?.job?.ID}) Failed to find NetworkedStation"); + __instance.bookletPrinter.PlayErrorSound(); + } + } + + return false; + } + + [HarmonyPatch(nameof(JobValidator.ValidateJob))] + [HarmonyPrefix] + private static bool ValidateJob_Prefix(JobValidator __instance, JobBooklet jobBooklet) + { + if (NetworkLifecycle.Instance.IsHost()) + return true; + + if (__instance.bookletPrinter.IsOnCooldown) + { + __instance.bookletPrinter.PlayErrorSound(); + return false; + } + + if (!NetworkedJob.TryGetFromJob(jobBooklet.job, out NetworkedJob networkedJob) || jobBooklet.job.State != JobState.InProgress) + { + NetworkLifecycle.Instance.Client.LogWarning($"ValidateJob({jobBooklet?.job?.ID}) NetworkedJob found: {networkedJob != null}, Job state: {jobBooklet?.job?.State}"); + __instance.bookletPrinter.PlayErrorSound(); + jobBooklet.DestroyJobBooklet(); + return false; + } + + if (networkedJob.ValidatorRequestSent) + { + if (networkedJob.ValidatorResponseReceived && networkedJob.ValidationAccepted) + return true; + } + else + { + //find the current station we're at + if (NetworkedStationController.GetFromJobValidator(__instance, out NetworkedStationController networkedStation)) + { + //Set initial job state parameters + networkedJob.ValidatorRequestSent = true; + networkedJob.ValidatorResponseReceived = false; + networkedJob.ValidationAccepted = false; + + NetworkLifecycle.Instance.Client.SendJobValidateRequest(networkedJob.NetId, networkedStation.NetId, ValidationType.JobBooklet); + CoroutineManager.Instance.StartCoroutine(AwaitResponse(__instance, networkedJob, ValidationType.JobBooklet)); + } + else + { + NetworkLifecycle.Instance.Client.LogError($"ValidateJob({jobBooklet?.job?.ID}) Failed to find NetworkedStation"); + __instance.bookletPrinter.PlayErrorSound(); + } + } + + return false; + } + + private static IEnumerator AwaitResponse(JobValidator validator, NetworkedJob networkedJob, ValidationType type) + { + yield return new WaitForSecondsRealtime(NetworkLifecycle.Instance.Client.Ping * 2); + + NetworkLifecycle.Instance.Client.Log($"JobValidator_Patch.AwaitResponse() ResponseReceived: {networkedJob?.ValidatorResponseReceived}, Accepted: {networkedJob?.ValidationAccepted}"); + + if (networkedJob == null || (!networkedJob.ValidatorResponseReceived || !networkedJob.ValidationAccepted)) + { + validator.bookletPrinter.PlayErrorSound(); + yield break; + } + + switch (type) + { + case ValidationType.JobOverview: + validator.ProcessJobOverview(networkedJob.JobOverview); + break; + + case ValidationType.JobBooklet: + validator.ValidateJob(networkedJob.JobBooklet); + break; + } + + + } +} diff --git a/info.json b/info.json index 722a2bc..b687330 100644 --- a/info.json +++ b/info.json @@ -1,6 +1,6 @@ { "Id": "Multiplayer", - "Version": "0.1.8.2", + "Version": "0.1.8.3", "DisplayName": "Multiplayer", "Author": "Insprill, Macka, Morm", "EntryMethod": "Multiplayer.Multiplayer.Load", From 84161bd6f557f93dab19f29ff61bc6603b205cfd Mon Sep 17 00:00:00 2001 From: AMacro Date: Fri, 6 Sep 2024 17:17:34 +1000 Subject: [PATCH 083/188] Start work on LAN discovery and server browser ping --- .../Components/MainMenu/ServerBrowserPane.cs | 28 +++ .../Networking/Train/NetworkedTrainCar.cs | 4 +- .../Networking/Data/LobbyServerData.cs | 2 +- .../Managers/Client/NetworkClient.cs | 3 - .../Managers/Client/ServerBrowserClient.cs | 196 ++++++++++++++++++ .../Networking/Managers/NetworkManager.cs | 25 ++- .../Managers/Server/NetworkServer.cs | 10 + .../Unconnected/UnconnectedPingPacket.cs | 13 ++ 8 files changed, 274 insertions(+), 7 deletions(-) create mode 100644 Multiplayer/Networking/Managers/Client/ServerBrowserClient.cs create mode 100644 Multiplayer/Networking/Packets/Unconnected/UnconnectedPingPacket.cs diff --git a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs index b4165d8..ace13c4 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs @@ -18,6 +18,9 @@ using System.Net; using LiteNetLib; using LiteNetLib.Utils; +using Multiplayer.Networking.Listeners; +using System.Collections.Generic; +using System.Timers; namespace Multiplayer.Components.MainMenu { @@ -59,6 +62,8 @@ public class ServerBrowserPane : MonoBehaviour private const int AUTO_REFRESH_TIME = 30; //how often to refresh in auto private const int REFRESH_MIN_TIME = 10; //Stop refresh spam + private ServerBrowserClient serverBrowserClient; + //connection parameters private string address; private int portNumber; @@ -94,6 +99,10 @@ private void Awake() //FillDummyServers(); RefreshAction(); + //Start Server + serverBrowserClient = new ServerBrowserClient(Multiplayer.Settings); + serverBrowserClient.OnPing += this.OnPing; + serverBrowserClient.Start(); } private void OnEnable() @@ -118,6 +127,12 @@ private void OnDisable() this.SetupListeners(false); } + private void OnDestroy() + { + serverBrowserClient.OnPing -= this.OnPing; + serverBrowserClient.Stop(); + } + private void Update() { @@ -962,5 +977,18 @@ private string ExtractDomainName(string input) return input; } + + private void OnPing(string serverId, int ping, bool isIPv4, bool isIPv6) + { + Multiplayer.Log($"ServerBrowser.OnPing({serverId}, {ping} ms, IPv4 {isIPv4}, IPv6 {isIPv6} )"); + } + + private void SendPing() + { + if (selectedServer != null) + { + serverBrowserClient.SendUnconnectedPingPacket(selectedServer.id, selectedServer.ipv4, selectedServer.ipv6, selectedServer.port); + } + } } } diff --git a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs index a1810d4..d913aaf 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs @@ -576,8 +576,10 @@ public void Common_UpdatePorts(CommonTrainPortsPacket packet) else port.Value = value; + /* if (Multiplayer.Settings.DebugLogging) - log += $"\r\n\tPort name: {port.id}, value before: {before}, value after: {port.value}, value: {value}, port type: {port.type}"; + log += $"\r\n\tPort name: {port.id}, value before: {before}, value after: {port.value}, value: {value}, port type: {port.type}";) + */ } NetworkLifecycle.Instance.Client.LogDebug(() => log); diff --git a/Multiplayer/Networking/Data/LobbyServerData.cs b/Multiplayer/Networking/Data/LobbyServerData.cs index 7e9da1c..1d4f8e5 100644 --- a/Multiplayer/Networking/Data/LobbyServerData.cs +++ b/Multiplayer/Networking/Data/LobbyServerData.cs @@ -10,7 +10,7 @@ namespace Multiplayer.Networking.Data { public class LobbyServerData : IServerBrowserGameDetails { - + [JsonProperty("game_server_id")] public string id { get; set; } public string ipv4 { get; set; } diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 345b8b3..cfbfcba 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -12,7 +12,6 @@ using DV.UI; using DV.WeatherSystem; using LiteNetLib; -using Multiplayer.Components; using Multiplayer.Components.MainMenu; using Multiplayer.Components.Networking; using Multiplayer.Components.Networking.Jobs; @@ -36,9 +35,7 @@ using UnityEngine; using UnityModManagerNet; using Object = UnityEngine.Object; -using System.Linq; using Multiplayer.Networking.Packets.Serverbound.Train; -using static Multiplayer.Networking.Packets.Clientbound.World.ClientBoundStationControllerLookupPacket; namespace Multiplayer.Networking.Listeners; diff --git a/Multiplayer/Networking/Managers/Client/ServerBrowserClient.cs b/Multiplayer/Networking/Managers/Client/ServerBrowserClient.cs new file mode 100644 index 0000000..fe157f5 --- /dev/null +++ b/Multiplayer/Networking/Managers/Client/ServerBrowserClient.cs @@ -0,0 +1,196 @@ +using System; +using System.Net; +using System.Text; +using System.Collections.Generic; +using LiteNetLib; +using Multiplayer.Networking.Packets.Unconnected; +using Newtonsoft.Json.Linq; +using System.Threading.Tasks; +using System.Diagnostics; +using System.Linq; + + +namespace Multiplayer.Networking.Listeners; + +public class ServerBrowserClient : NetworkManager, IDisposable +{ + protected override string LogPrefix => "[SBClient]"; + private class PingInfo + { + public Stopwatch Stopwatch { get; } = new Stopwatch(); + public DateTime StartTime { get; private set; } + public bool IPv4Received { get; set; } + public bool IPv6Received { get; set; } + public bool IPv4Sent { get; set; } + public bool IPv6Sent { get; set; } + + public void Start() + { + StartTime = DateTime.Now; + Stopwatch.Start(); + } + } + + private Dictionary pingInfos = new Dictionary(); + public Action OnPing; // serverId, pingTime, isIPv4, isIPv6 + + private const int PingTimeoutMs = 5000; // 5 seconds timeout + + public ServerBrowserClient(Settings settings) : base(settings) + { + } + + public void Start() + { + Log($"ServerBrowserClient.Start()"); + netManager.Start(); + } + public override void Stop() + { + base.Stop(); + Dispose(); + } + + public void Dispose() + { + foreach (var pingInfo in pingInfos.Values) + { + pingInfo.Stopwatch.Stop(); + } + pingInfos.Clear(); + } + private async Task CleanupTimedOutPings() + { + while (true) + { + await Task.Delay(PingTimeoutMs * 2); + var now = DateTime.Now; + var timedOutServers = pingInfos + .Where(kvp => (now - kvp.Value.StartTime).TotalMilliseconds > PingTimeoutMs) + .Select(kvp => kvp.Key) + .ToList(); + + foreach (var serverId in timedOutServers) + { + pingInfos.Remove(serverId); + Log($"Cleaned up timed out ping for {serverId}"); + } + } + } + + protected override void Subscribe() + { + netPacketProcessor.SubscribeReusable(OnUnconnectedPingPacket); + } + + #region Net Events + + public override void OnPeerConnected(NetPeer peer) + { + } + + public override void OnPeerDisconnected(NetPeer peer, DisconnectInfo disconnectInfo) + { + } + + public override void OnNetworkLatencyUpdate(NetPeer peer, int latency) + { + } + + public override void OnConnectionRequest(ConnectionRequest request) + { + } + + #endregion + + #region Listeners + + private void OnUnconnectedPingPacket(UnconnectedPingPacket packet, IPEndPoint endPoint) + { + string serverId = new Guid(packet.ServerID).ToString(); + Log($"OnUnconnectedPingPacket({serverId ?? ""}, {endPoint?.Address})"); + + if (pingInfos.TryGetValue(serverId, out PingInfo pingInfo)) + { + pingInfo.Stopwatch.Stop(); + int pingTime = (int)pingInfo.Stopwatch.ElapsedMilliseconds; + + bool isIPv4 = endPoint.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork; + if (isIPv4) + pingInfo.IPv4Received = true; + else + pingInfo.IPv6Received = true; + + OnPing?.Invoke(serverId, pingTime, pingInfo.IPv4Received, pingInfo.IPv6Received); + + Log($"Ping received for {serverId}: {pingTime}ms, IPv4: {pingInfo.IPv4Received}, IPv6: {pingInfo.IPv6Received}"); + + if (pingInfo.IPv4Received && pingInfo.IPv6Received) + { + pingInfos.Remove(serverId); + } + } + } + + #endregion + + #region Senders + public async Task SendUnconnectedPingPacket(string serverId, string ipv4, string ipv6, int port) + { + if (!Guid.TryParse(serverId, out Guid server)) + { + LogError($"SendUnconnectedPingPacket({serverId}) failed to parse GUID"); + return; + } + + PingInfo pingInfo = new PingInfo(); + pingInfos[serverId] = pingInfo; + + Log($"Sending ping to {serverId} at IPv4: {ipv4}, IPv6: {ipv6}, Port: {port}"); + pingInfo.Start(); + + var packet = new UnconnectedPingPacket { ServerID = server.ToByteArray() }; + + // Send to IPv4 if provided + if (!string.IsNullOrEmpty(ipv4)) + { + SendUnconnnectedPacket(packet, ipv4, port); + pingInfo.IPv4Sent = true; + } + + // Send to IPv6 if provided + if (!string.IsNullOrEmpty(ipv6)) + { + SendUnconnnectedPacket(packet, ipv6, port); + pingInfo.IPv6Sent = true; + } + + // Start a timeout task + _ = StartTimeoutTask(serverId); + } + + private async Task StartTimeoutTask(string serverId) + { + await Task.Delay(PingTimeoutMs); + if (pingInfos.TryGetValue(serverId, out PingInfo pingInfo)) + { + pingInfo.Stopwatch.Stop(); + OnPing?.Invoke(serverId, -1, pingInfo.IPv4Received, pingInfo.IPv6Received); + pingInfos.Remove(serverId); + Log($"Ping timeout for {serverId}"); + } + } + + #endregion + + #region NAT Punch Events + public override void OnNatIntroductionRequest(IPEndPoint localEndPoint, IPEndPoint remoteEndPoint, string token) + { + //do some stuff here + } + public override void OnNatIntroductionSuccess(IPEndPoint targetEndPoint, NatAddressType type, string token) + { + //do other stuff here + } + #endregion +} diff --git a/Multiplayer/Networking/Managers/NetworkManager.cs b/Multiplayer/Networking/Managers/NetworkManager.cs index 6b81e25..283d805 100644 --- a/Multiplayer/Networking/Managers/NetworkManager.cs +++ b/Multiplayer/Networking/Managers/NetworkManager.cs @@ -24,7 +24,10 @@ protected NetworkManager(Settings settings) { netManager = new NetManager(this) { - DisconnectTimeout = 10000 + DisconnectTimeout = 10000, + UnconnectedMessagesEnabled = true, + BroadcastReceiveEnabled = true, + }; netPacketProcessor = new NetPacketProcessor(netManager); RegisterNestedTypes(); @@ -84,6 +87,11 @@ public virtual void Stop() { peer?.Send(WritePacket(packet), deliveryMethod); } + + protected void SendUnconnnectedPacket(T packet, string ipAddress, int port) where T : class, new() + { + netManager.SendUnconnectedMessage(WritePacket(packet), ipAddress, port); + } protected abstract void Subscribe(); @@ -113,7 +121,20 @@ public void OnNetworkError(IPEndPoint endPoint, SocketError socketError) public void OnNetworkReceiveUnconnected(IPEndPoint remoteEndPoint, NetPacketReader reader, UnconnectedMessageType messageType) { - // todo + Multiplayer.Log($"OnNetworkReceiveUnconnected({remoteEndPoint}, {messageType})"); + try + { + IsProcessingPacket = true; + netPacketProcessor.ReadAllPackets(reader, remoteEndPoint); + } + catch (ParseException e) + { + Multiplayer.LogWarning($"Failed to parse packet: {e.Message}"); + } + finally + { + IsProcessingPacket = false; + } } //Standard networking callbacks diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index b234195..9d50dba 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -30,6 +30,7 @@ using UnityModManagerNet; using System.Net; using Multiplayer.Networking.Packets.Serverbound.Train; +using Multiplayer.Networking.Packets.Unconnected; namespace Multiplayer.Networking.Listeners; @@ -134,6 +135,7 @@ protected override void Subscribe() netPacketProcessor.SubscribeReusable(OnCommonTrainFusesPacket); netPacketProcessor.SubscribeReusable(OnServerboundJobValidateRequestPacket); netPacketProcessor.SubscribeReusable(OnCommonChatPacket); + netPacketProcessor.SubscribeReusable(OnUnconnectedPingPacket); } private void OnLoaded() @@ -988,4 +990,12 @@ private void OnCommonChatPacket(CommonChatPacket packet, NetPeer peer) ChatManager.ProcessMessage(packet.message,peer); } #endregion + + #region Unconnected Packet Handling + private void OnUnconnectedPingPacket(UnconnectedPingPacket packet, IPEndPoint endPoint) + { + Multiplayer.Log($"OnUnconnectedPingPacket({endPoint.Address})"); + SendUnconnnectedPacket(packet, endPoint.Address.ToString(),endPoint.Port); + } + #endregion } diff --git a/Multiplayer/Networking/Packets/Unconnected/UnconnectedPingPacket.cs b/Multiplayer/Networking/Packets/Unconnected/UnconnectedPingPacket.cs new file mode 100644 index 0000000..0722dd4 --- /dev/null +++ b/Multiplayer/Networking/Packets/Unconnected/UnconnectedPingPacket.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Multiplayer.Networking.Packets.Unconnected +{ + public class UnconnectedPingPacket + { + public byte[] ServerID { get; set; } + } +} From 00e39aba7db4de2a03624ed1cef6afa9fc65c386 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 7 Sep 2024 20:28:55 +1000 Subject: [PATCH 084/188] Fixed MU cable connection bug More work required as MU causes fighting between locos --- .../Networking/Train/NetworkedTrainCar.cs | 19 +++++++++++++++---- .../Managers/Client/NetworkClient.cs | 2 +- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs index d913aaf..e82b108 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs @@ -43,10 +43,6 @@ public static Coupler GetCoupler(HoseAndCock hoseAndCock) return hoseToCoupler[hoseAndCock]; } - //public static NetworkedTrainCar GetFromTrainCar(TrainCar trainCar) - //{ - // return trainCarsToNetworkedTrainCars[trainCar]; - //} public static bool GetFromTrainId(string carId, out NetworkedTrainCar networkedTrainCar) { return trainCarIdToNetworkedTrainCars.TryGetValue(carId, out networkedTrainCar); @@ -88,6 +84,7 @@ public static bool TryGetFromTrainCar(TrainCar trainCar, out NetworkedTrainCar n public byte CargoModelIndex = byte.MaxValue; private bool healthDirty; private bool sendCouplers; + private bool sendCables; private bool fireboxDirty; public bool IsDestroying; @@ -272,6 +269,7 @@ public void Server_DirtyAllState() healthDirty = true; BogieTracksDirty = true; sendCouplers = true; + sendCables = true; fireboxDirty = firebox != null; //only dirty if exists if (!hasSimFlow) @@ -364,6 +362,7 @@ private void Server_OnTick(uint tick) Server_SendBrakePressures(); Server_SendFireBoxState(); Server_SendCouplers(); + Server_SendCables(); Server_SendCargoState(); Server_SendHealthState(); @@ -402,6 +401,18 @@ private void Server_SendCouplers() NetworkLifecycle.Instance.Client.SendCockState(NetId, TrainCar.frontCoupler, TrainCar.frontCoupler.IsCockOpen); NetworkLifecycle.Instance.Client.SendCockState(NetId, TrainCar.rearCoupler, TrainCar.rearCoupler.IsCockOpen); } + private void Server_SendCables() + { + if (!sendCables) + return; + sendCables = false; + + if (TrainCar.muModule.frontCable.IsConnected) + NetworkLifecycle.Instance.Client.SendMuConnected(TrainCar.muModule.frontCable, TrainCar.muModule.frontCable.connectedTo, false); + + if (TrainCar.muModule.rearCable.IsConnected) + NetworkLifecycle.Instance.Client.SendMuConnected(TrainCar.muModule.rearCable, TrainCar.muModule.rearCable.connectedTo, false); + } private void Server_SendCargoState() { diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index cfbfcba..eefb020 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -542,7 +542,7 @@ private void OnCommonMuConnectedPacket(CommonMuConnectedPacket packet) return; MultipleUnitCable cable = packet.IsFront ? trainCar.muModule.frontCable : trainCar.muModule.rearCable; - MultipleUnitCable otherCable = packet.IsFront ? otherTrainCar.muModule.frontCable : otherTrainCar.muModule.rearCable; + MultipleUnitCable otherCable = packet.OtherIsFront ? otherTrainCar.muModule.frontCable : otherTrainCar.muModule.rearCable; cable.Connect(otherCable, packet.PlayAudio); } From 7d637c3c96d7a1f33ebbaf39e7cfec693d345efd Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 8 Sep 2024 11:23:18 +1000 Subject: [PATCH 085/188] Fixed issues with obtaining external IPv4 address Changed service from `http://checkip.dyndns.org` to `https://api.ipify.org/` Fixed typo in regex Changed the IPv4 lookup to use GET rather than POST --- .../Managers/Server/LobbyServerManager.cs | 48 +++++++++++++++++-- Multiplayer/Settings.cs | 8 +++- 2 files changed, 50 insertions(+), 6 deletions(-) diff --git a/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs b/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs index 45c54ac..b65be1a 100644 --- a/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs +++ b/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs @@ -20,7 +20,7 @@ public class LobbyServerManager : MonoBehaviour private const string ENDPOINT_REMOVE_SERVER = "remove_game_server"; //RegEx - private readonly Regex IPv4Match = new Regex(@"\b(?:(?:2[0-5]{2}|1[0-9]{2}|[1-9]?[0-9])\.){3}(?:2[0-5]{2}|1[0-9]{2}|[1-9]?[0-9])\b"); + private readonly Regex IPv4Match = 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 const int REDIRECT_MAX = 5; @@ -50,7 +50,6 @@ private IEnumerator Start() { server.serverData.ipv6 = GetStaticIPv6Address(); StartCoroutine(GetIPv4(Multiplayer.Settings.Ipv4AddressCheck)); - yield return new WaitUntil(() => initialised); Multiplayer.Log("Public IPv4: " + server.serverData.ipv4); @@ -172,9 +171,8 @@ private IEnumerator GetIPv4(string uri) Multiplayer.Log("Preparing to get IPv4: " + uri); - yield return SendWebRequest( + yield return SendWebRequestGET( uri, - string.Empty, webRequest => { Match match = IPv4Match.Match(webRequest.downloadHandler.text); @@ -246,6 +244,48 @@ 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.Get(uri)) + { + webRequest.redirectLimit = 0; + webRequest.downloadHandler = new DownloadHandlerBuffer(); + + yield return webRequest.SendWebRequest(); + + //check for redirect + if (webRequest.responseCode >= 300 && webRequest.responseCode < 400) + { + 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 SendWebRequestGET(redirectUrl, onSuccess, onError, ++depth); + } + } + else + { + 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); + } + } + } + } + public static string GetStaticIPv6Address() { foreach (NetworkInterface networkInterface in NetworkInterface.GetAllNetworkInterfaces()) diff --git a/Multiplayer/Settings.cs b/Multiplayer/Settings.cs index 24d812f..f88c7d5 100644 --- a/Multiplayer/Settings.cs +++ b/Multiplayer/Settings.cs @@ -14,7 +14,7 @@ public class Settings : UnityModManager.ModSettings, IDrawable public static Action OnSettingsUpdated; - public int SettingsVer = 0; + public int SettingsVer = 1; [Header("Player")] [Draw("Username", Tooltip = "Your username in-game")] @@ -41,7 +41,7 @@ public class Settings : UnityModManager.ModSettings, IDrawable [Draw("Lobby Server address", Tooltip = "Address of lobby server for finding multiplayer games")] public string LobbyServerAddress = "https://dv.mineit.space";//"http://localhost:8080"; [Draw("IPv4 Check Address", Tooltip = "Do not modify unless the service is unavailable")] - public string Ipv4AddressCheck = "http://checkip.dyndns.org"; + public string Ipv4AddressCheck = "https://api.ipify.org/"; [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 = ""; @@ -155,6 +155,10 @@ private static void MigrateSettings(ref Settings data) MigrateSettings(ref data); break; + case 1: + if (data.Ipv4AddressCheck == "http://checkip.dyndns.org") + data.Ipv4AddressCheck = new Settings().Ipv4AddressCheck; + break; default: break; From ff4beccd5216256f1e97c9e50fa500c09c86a439 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 8 Sep 2024 11:29:03 +1000 Subject: [PATCH 086/188] fix for null reference on TrainCars without MU capabilities --- Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs index e82b108..2a26aad 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs @@ -407,6 +407,9 @@ private void Server_SendCables() return; sendCables = false; + if(TrainCar.muModule == null) + return; + if (TrainCar.muModule.frontCable.IsConnected) NetworkLifecycle.Instance.Client.SendMuConnected(TrainCar.muModule.frontCable, TrainCar.muModule.frontCable.connectedTo, false); From 7a1524fd5686dc62c3ecb27dfb7379460b6bbfe5 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 8 Sep 2024 11:30:39 +1000 Subject: [PATCH 087/188] Fixed StationController to JobValidator mapping --- .../World/NetworkedStationController.cs | 102 ++++++++++++------ .../Components/StationComponentLookup.cs | 51 --------- 2 files changed, 69 insertions(+), 84 deletions(-) delete mode 100644 Multiplayer/Components/StationComponentLookup.cs diff --git a/Multiplayer/Components/Networking/World/NetworkedStationController.cs b/Multiplayer/Components/Networking/World/NetworkedStationController.cs index 691eb14..638440b 100644 --- a/Multiplayer/Components/Networking/World/NetworkedStationController.cs +++ b/Multiplayer/Components/Networking/World/NetworkedStationController.cs @@ -18,6 +18,7 @@ public class NetworkedStationController : IdMonoBehaviour stationIdToStationController = new(); private static readonly Dictionary stationToNetworkedStationController = new(); private static readonly Dictionary jobValidatorToNetworkedStation = new(); + private static readonly List jobValidators = new List(); public static bool Get(ushort netId, out NetworkedStationController obj) { @@ -78,11 +79,16 @@ public static void RegisterStationController(NetworkedStationController networke stationToNetworkedStationController.Add(stationController.logicStation, networkedStationController); } - public static void RegisterJobValidator(JobValidator jobValidator, NetworkedStationController stationController) + public static void QueueJobValidator(JobValidator jobValidator) { - if (jobValidator == null || stationController == null) - return; + Multiplayer.Log($"QueueJobValidator() {jobValidator.transform.parent.name}"); + + jobValidators.Add(jobValidator); + } + private static void RegisterJobValidator(JobValidator jobValidator, NetworkedStationController stationController) + { + Multiplayer.Log($"RegisterJobValidator() {jobValidator.transform.parent.name}, {stationController.name}"); stationController.JobValidator = jobValidator; jobValidatorToNetworkedStation[jobValidator] = stationController; } @@ -98,7 +104,6 @@ public static void RegisterJobValidator(JobValidator jobValidator, NetworkedStat public HashSet NetworkedJobs { get; } = new HashSet(); private List NewJobs = new List(); private List DirtyJobs = new List(); - //public List JobOverviews; //for later use private void Awake() { @@ -122,6 +127,20 @@ private IEnumerator WaitForLogicStation() NetworkedStationController.RegisterStationController(this, StationController); Multiplayer.Log($"NetworkedStation.Awake({StationController.logicStation.ID})"); + + foreach (JobValidator validator in jobValidators) + { + string stationName = validator.transform.parent.name ?? ""; + stationName += "_office_anchor"; + + if(this.transform.parent.name.Equals(stationName, StringComparison.OrdinalIgnoreCase)) + { + JobValidator = validator; + RegisterJobValidator(validator, this); + jobValidators.Remove(validator); + break; + } + } } //Adding job on server @@ -182,7 +201,7 @@ public void AddJobs(JobData[] jobs) //NetworkLifecycle.Instance.Client.Log($"AddJobs() jobs[] exists: {jobs != null}, job count: {jobs?.Count()}"); //NetworkLifecycle.Instance.Client.Log($"AddJobs() preloop"); - foreach (JobData jobData in jobs) + foreach (JobData job in jobs) { //NetworkLifecycle.Instance.Client.Log($"AddJobs() inloop"); @@ -190,7 +209,7 @@ public void AddJobs(JobData[] jobs) // Convert TaskNetworkData to Task objects List tasks = new List(); - foreach (TaskNetworkData taskData in jobData.Tasks) + foreach (TaskNetworkData taskData in job.Tasks) { if (NetworkLifecycle.Instance.IsHost()) { @@ -205,8 +224,8 @@ public void AddJobs(JobData[] jobs) //NetworkLifecycle.Instance.Client.Log($"AddJobs() ID: {jobData?.ID}, netID: {jobData?.NetID}, StationsChainData"); // Create StationsChainData from ChainData StationsChainData chainData = new StationsChainData( - jobData.ChainData.ChainOriginYardId, - jobData.ChainData.ChainDestinationYardId + job.ChainData.ChainOriginYardId, + job.ChainData.ChainDestinationYardId ); @@ -214,27 +233,28 @@ public void AddJobs(JobData[] jobs) // Create a new local Job Job newJob = new Job( tasks, - jobData.JobType, - jobData.TimeLimit, - jobData.InitialWage, + job.JobType, + job.TimeLimit, + job.InitialWage, chainData, - jobData.ID, - jobData.RequiredLicenses + job.ID, + job.RequiredLicenses ); //NetworkLifecycle.Instance.Client.Log($"AddJobs() ID: {jobData?.ID}, netID: {jobData?.NetID}, properties"); // Set additional properties - newJob.startTime = jobData.StartTime; - newJob.finishTime = jobData.FinishTime; - newJob.State = jobData.State; + newJob.startTime = job.StartTime; + newJob.finishTime = job.FinishTime; + newJob.State = job.State; //NetworkLifecycle.Instance.Client.Log($"AddJobs() ID: {jobData?.ID}, netID: {jobData?.NetID}, netjob"); // Create a new NetworkedJob NetworkedJob networkedJob = new GameObject($"NetworkedJob {newJob.ID}").AddComponent(); + networkedJob.NetId = job.NetID; networkedJob.Job = newJob; networkedJob.Station = this; - networkedJob.OwnedBy = jobData.OwnedBy; + networkedJob.playerID = job.PlayerId; //NetworkLifecycle.Instance.Client.Log($"AddJobs() ID: {jobData?.ID}, netID: {jobData?.NetID}, NetJob Add"); NetworkedJobs.Add(networkedJob); @@ -273,11 +293,11 @@ public static IEnumerator UpdateCarPlates(List tasks, string if (cars != null) { - Multiplayer.Log("NetworkedStation.UpdateCarPlates() Cars count: " + cars.Count); + //Multiplayer.Log("NetworkedStation.UpdateCarPlates() Cars count: " + cars.Count); foreach (Car car in cars) { - Multiplayer.Log("NetworkedStation.UpdateCarPlates() Car: " + car.ID); + //Multiplayer.Log("NetworkedStation.UpdateCarPlates() Car: " + car.ID); TrainCar trainCar = null; int loopCtr = 0; @@ -286,7 +306,7 @@ public static IEnumerator UpdateCarPlates(List tasks, string loopCtr++; if (loopCtr > 5000) { - Multiplayer.Log("NetworkedStation.UpdateCarPlates() TimeOut"); + //Multiplayer.Log("NetworkedStation.UpdateCarPlates() TimeOut"); break; } @@ -300,44 +320,44 @@ public static IEnumerator UpdateCarPlates(List tasks, string } private static void UpdateCarPlatesRecursive(List tasks, string jobId, ref List cars) { - Multiplayer.Log("NetworkedStation.UpdateCarPlatesRecursive() Starting"); + //Multiplayer.Log("NetworkedStation.UpdateCarPlatesRecursive() Starting"); foreach (Task task in tasks) { if (task is WarehouseTask) { - Multiplayer.Log("NetworkedStation.UpdateCarPlatesRecursive() WarehouseTask"); + //Multiplayer.Log("NetworkedStation.UpdateCarPlatesRecursive() WarehouseTask"); cars = cars.Union(((WarehouseTask)task).cars).ToList(); } else if (task is TransportTask) { - Multiplayer.Log("NetworkedStation.UpdateCarPlatesRecursive() TransportTask"); + //Multiplayer.Log("NetworkedStation.UpdateCarPlatesRecursive() TransportTask"); cars = cars.Union(((TransportTask)task).cars).ToList(); } else if (task is SequentialTasks) { - Multiplayer.Log("NetworkedStation.UpdateCarPlatesRecursive() SequentialTasks"); + //Multiplayer.Log("NetworkedStation.UpdateCarPlatesRecursive() SequentialTasks"); List seqTask = new(); for (LinkedListNode node = ((SequentialTasks)task).tasks.First; node != null; node = node.Next) { - Multiplayer.Log($"NetworkedStation.UpdateCarPlatesRecursive() SequentialTask Adding node"); + //Multiplayer.Log($"NetworkedStation.UpdateCarPlatesRecursive() SequentialTask Adding node"); seqTask.Add(node.Value); } - Multiplayer.Log($"NetworkedStation.UpdateCarPlatesRecursive() SequentialTask Node Count:{seqTask.Count}"); + //Multiplayer.Log($"NetworkedStation.UpdateCarPlatesRecursive() SequentialTask Node Count:{seqTask.Count}"); - Multiplayer.Log("NetworkedStation.UpdateCarPlatesRecursive() Calling UpdateCarPlates()"); + //Multiplayer.Log("NetworkedStation.UpdateCarPlatesRecursive() Calling UpdateCarPlates()"); //drill down UpdateCarPlatesRecursive(seqTask, jobId, ref cars); - Multiplayer.Log($"NetworkedStation.UpdateCarPlatesRecursive() SequentialTask RETURNED"); + //Multiplayer.Log($"NetworkedStation.UpdateCarPlatesRecursive() SequentialTask RETURNED"); } else if (task is ParallelTasks) { //not implemented - Multiplayer.Log("NetworkedStation.UpdateCarPlatesRecursive() ParallelTasks"); + //Multiplayer.Log("NetworkedStation.UpdateCarPlatesRecursive() ParallelTasks"); - Multiplayer.Log("NetworkedStation.UpdateCarPlatesRecursive() Calling UpdateCarPlates()"); + //Multiplayer.Log("NetworkedStation.UpdateCarPlatesRecursive() Calling UpdateCarPlates()"); //drill down UpdateCarPlatesRecursive(((ParallelTasks)task).tasks, jobId, ref cars); } @@ -347,12 +367,28 @@ private static void UpdateCarPlatesRecursive(List tasks, stri } } - Multiplayer.Log("NetworkedStation.UpdateCarPlatesRecursive() Returning"); + //Multiplayer.Log("NetworkedStation.UpdateCarPlatesRecursive() Returning"); } - public IEnumerator CreatePaperWork() + private void OnDisable() { - yield return null; + + if (UnloadWatcher.isQuitting) + return; + + NetworkLifecycle.Instance.OnTick -= Server_OnTick; + + string stationId = StationController.logicStation.ID; + + stationControllerToNetworkedStationController.Remove(StationController); + stationIdToNetworkedStationController.Remove(stationId); + stationIdToStationController.Remove(stationId); + stationToNetworkedStationController.Remove(StationController.logicStation); + jobValidatorToNetworkedStation.Remove(JobValidator); + jobValidators.Remove(this.JobValidator); + + Destroy(this); + } #endregion } diff --git a/Multiplayer/Components/StationComponentLookup.cs b/Multiplayer/Components/StationComponentLookup.cs deleted file mode 100644 index 689552e..0000000 --- a/Multiplayer/Components/StationComponentLookup.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System.Collections.Generic; -using DV.Logic.Job; -using DV.Utils; -using JetBrains.Annotations; -using Multiplayer.Components.Networking.World; - -namespace Multiplayer.Components; -/* -public class StationComponentLookup : SingletonBehaviour -{ - private readonly Dictionary stationToNetworkedStationController = new(); - private readonly Dictionary stationIdToNetworkedStation = new(); - private readonly Dictionary stationIdToStationController = new(); - - public void RegisterStation(StationController stationController) - { - var networkedStation = stationController.GetComponent(); - stationToNetworkedStationController[stationController.logicStation] = networkedStation; - stationIdToNetworkedStation[stationController.logicStation.ID] = networkedStation; - stationIdToStationController[stationController.logicStation.ID] = stationController; - } - - public void UnregisterStation(StationController stationController) - { - stationToNetworkedStationController.Remove(stationController.logicStation); - stationIdToNetworkedStation.Remove(stationController.logicStation.ID); - stationIdToStationController.Remove(stationController.logicStation.ID); - } - - public bool NetworkedStationFromStation(Station station, out NetworkedStation networkedStation) - { - return stationToNetworkedStationController.TryGetValue(station, out networkedStation); - } - - public bool NetworkedStationFromId(string stationId, out NetworkedStation networkedStation) - { - return stationIdToNetworkedStation.TryGetValue(stationId, out networkedStation); - } - - public bool StationControllerFromId(string stationId, out StationController stationController) - { - return stationIdToStationController.TryGetValue(stationId, out stationController); - } - - [UsedImplicitly] - public new static string AllowAutoCreate() - { - return $"[{nameof(StationComponentLookup)}]"; - } -} -*/ From 1cee17db343eae92c7eeb801e1f195a5a8f34a02 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 8 Sep 2024 11:31:33 +1000 Subject: [PATCH 088/188] Started work on item sync --- .../Networking/World/NetworkedItem.cs | 198 ++++++++++++++++++ Multiplayer/Patches/World/ItemBasePatch.cs | 19 ++ 2 files changed, 217 insertions(+) create mode 100644 Multiplayer/Components/Networking/World/NetworkedItem.cs create mode 100644 Multiplayer/Patches/World/ItemBasePatch.cs diff --git a/Multiplayer/Components/Networking/World/NetworkedItem.cs b/Multiplayer/Components/Networking/World/NetworkedItem.cs new file mode 100644 index 0000000..62f507e --- /dev/null +++ b/Multiplayer/Components/Networking/World/NetworkedItem.cs @@ -0,0 +1,198 @@ +using DV.CabControls; +using DV.InventorySystem; +using DV.Simulation.Brake; +using Multiplayer.Components.Networking.Jobs; +using Multiplayer.Components.Networking.Train; +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using UnityEngine; + +namespace Multiplayer.Components.Networking.World; + +public class NetworkedItem : IdMonoBehaviour +{ + #region Lookup Cache + + private static readonly Dictionary itemBaseToNetworkedItem = new(); + + public static bool Get(ushort netId, out NetworkedItem obj) + { + bool b = Get(netId, out IdMonoBehaviour rawObj); + obj = (NetworkedItem)rawObj; + return b; + } + + public static bool GetItem(ushort netId, out ItemBase obj) + { + bool b = Get(netId, out NetworkedItem networkedItem); + obj = b ? networkedItem.Item : null; + return b; + } + #endregion + + public ItemBase Item { get; set; } + public Guid Owner { get; set; } + + protected override bool IsIdServerAuthoritative => true; + + protected override void Awake() + { + base.Awake(); + + Multiplayer.LogDebug(()=>$"NetworkedItem.Awake() {name}"); + + if (!TryGetComponent(out ItemBase item)) + { + Multiplayer.LogError($"Unable to find ItemBase for {name}"); + return; + } + + Item = item; + itemBaseToNetworkedItem[Item] = this; + SetupItem(); + } + + private void Start() + { + + } + + private void SetupItem() + { + //Let's get the item type and take an appropriate action + string itemType = Item?.InventorySpecs?.itemPrefabName; + + Multiplayer.LogDebug(() => $"NetworkedItem.SetupItem() {name}, {itemType}"); + + switch (itemType) + { + //Job related items + case "JobOverview": + SetupJobOverview(); + break; + + case "JobBooklet": + //SetupJobBooklet(); + break; + + case "JobMissingLicenseReport": + SetupJobMissingLicenseReport(); + break; + + case "JobDebtWarningReport": + SetupJobDebtWarningReport(); + break; + + //Loco related items + case "lighter": + break; + + case "Shovel": + break; + + //Other interactables + case "Lantern": + break; + + //Non interactables + default: + break; + } + + Item.Grabbed += OnGrabbed; + Item.Ungrabbed += OnUngrabbed; + Item.ItemInventoryStateChanged += OnItemInventoryStateChanged; + } + + private void OnUngrabbed(ControlImplBase obj) + { + Multiplayer.LogDebug(() => $"OnUngrabbed() {name}"); + } + + private void OnGrabbed(ControlImplBase obj) + { + Multiplayer.LogDebug(() => $"OnGrabbed() {name}"); + } + + private void OnItemInventoryStateChanged(ItemBase itemBase, InventoryActionType actionType, InventoryItemState itemState) + { + Multiplayer.LogDebug(() => $"OnItemInventoryStateChanged() {name}, InventoryActionType: {actionType}, InventoryItemState: {itemState}"); + } + + private void SetupJobOverview() + { + if(!TryGetComponent(out JobOverview jobOverview)) + { + Multiplayer.LogError($"SetupJobOverview() Could not find JobOverview"); + return; + } + + if (!NetworkedJob.TryGetFromJob(jobOverview.job, out NetworkedJob networkedJob)) + { + Multiplayer.LogError($"SetupJobOverview() NetworkedJob not found for Job ID: {jobOverview?.job?.ID}"); + jobOverview.DestroyJobOverview(); + return; + } + + networkedJob.JobOverview = jobOverview; + networkedJob.ValidationItem = this; + } + + //private IEnumerator SetupJobBooklet() + //{ + // if (!TryGetComponent(out JobBooklet jobBooklet)) + // { + // Multiplayer.LogError($"SetupJobBooklet() Could not find JobBooklet"); + // yield break; + // } + + // while (jobBooklet.job == null) + // yield return new WaitForEndOfFrame(); + + // if (!NetworkedJob.TryGetFromJob(jobBooklet.job, out NetworkedJob networkedJob)) + // { + // Multiplayer.LogError($"SetupJobOverview() NetworkedJob not found for Job ID: {jobBooklet?.job?.ID}"); + // jobBooklet.DestroyJobBooklet(); + // } + + // networkedJob.JobBooklet = jobBooklet; + // networkedJob.ValidationItem = this; + //} + + private void SetupJobMissingLicenseReport() + { + if (!TryGetComponent(out JobMissingLicenseReport report)) + { + Multiplayer.LogError($"SetupJobLicenseReport() Could not find JobMissingLicenseReport"); + return; + } + + if (!NetworkedJob.TryGetFromJobId(report.jobId, out NetworkedJob networkedJob)) + { + Multiplayer.LogError($"SetupJobLicenseReport() NetworkedJob not found for Job ID: {report?.jobId}"); + return; + } + + networkedJob.ValidationItem = this; + } + private void SetupJobDebtWarningReport() + { + if (!TryGetComponent(out JobMissingLicenseReport report)) + { + Multiplayer.LogError($"SetupJobDebtWarningReport() Could not find SetupJobDebtWarningReport"); + return; + } + + if (!NetworkedJob.TryGetFromJobId(report.jobId, out NetworkedJob networkedJob)) + { + Multiplayer.LogError($"SetupJobDebtWarningReport() NetworkedJob not found for Job ID: {report?.jobId}"); + return; + } + + networkedJob.ValidationItem = this; + } + +} diff --git a/Multiplayer/Patches/World/ItemBasePatch.cs b/Multiplayer/Patches/World/ItemBasePatch.cs new file mode 100644 index 0000000..e3f6f2d --- /dev/null +++ b/Multiplayer/Patches/World/ItemBasePatch.cs @@ -0,0 +1,19 @@ +using DV.CabControls; +using HarmonyLib; +using Multiplayer.Components.Networking.World; +using Multiplayer.Utils; + +namespace Multiplayer.Patches.World; + +[HarmonyPatch(typeof(ItemBase))] +public static class ItemBase_Patch +{ + [HarmonyPatch(nameof(ItemBase.Awake))] + [HarmonyPostfix] + private static void Awake(ItemBase __instance) + { + Multiplayer.Log($"ItemBase.Awake() ItemSpec: {__instance?.InventorySpecs?.itemPrefabName}"); + __instance.GetOrAddComponent(); + return; + } +} From 8e8e3335ed946099e61ca144432e9b2476f8fb1b Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 8 Sep 2024 11:33:18 +1000 Subject: [PATCH 089/188] Continued work on syncing jobs - framework for job validation --- .../Networking/Jobs/NetworkedJob.cs | 24 ++-- Multiplayer/Networking/Data/JobData.cs | 11 +- .../Managers/Client/NetworkClient.cs | 26 +++- .../Networking/Managers/NetworkManager.cs | 2 + .../Managers/Server/NetworkServer.cs | 14 ++- .../ClientboundJobValidateResponsePacket.cs | 1 + .../Jobs/ClientboundJobsUpdatePacket.cs | 48 ++++---- Multiplayer/Patches/Jobs/JobBookletPatch.cs | 12 +- Multiplayer/Patches/Jobs/JobOverviewPatch.cs | 2 +- Multiplayer/Patches/Jobs/JobValidatorPatch.cs | 113 +++++------------- 10 files changed, 123 insertions(+), 130 deletions(-) diff --git a/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs b/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs index 381054d..02494e9 100644 --- a/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs +++ b/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs @@ -3,8 +3,8 @@ using System.Collections.Generic; using System.Linq; using DV.Logic.Job; -using Multiplayer.Components.Networking.Train; using Multiplayer.Components.Networking.World; +using Multiplayer.Networking.Packets.Clientbound.Jobs; using UnityEngine; @@ -33,15 +33,21 @@ public static bool GetJob(ushort netId, out Job obj) } - public static NetworkedJob GetFromJob(Job job) - { - return jobToNetworkedJob[job]; - } + //public static NetworkedJob GetFromJob(Job job) + //{ + // return jobToNetworkedJob[job]; + //} public static bool TryGetFromJob(Job job, out NetworkedJob networkedJob) { return jobToNetworkedJob.TryGetValue(job, out networkedJob); } + + public static bool TryGetFromJobId(string jobId, out NetworkedJob networkedJob) + { + return jobIdToNetworkedJob.TryGetValue(jobId, out networkedJob); + } + #endregion protected override bool IsIdServerAuthoritative => true; @@ -53,14 +59,16 @@ public static bool TryGetFromJob(Job job, out NetworkedJob networkedJob) public Guid OwnedBy = Guid.Empty; //GUID of player who took the job (sever only) public int playerID; //ID of player who took the job (client & server) - public JobValidator jobValidator; //Job validator to print the booklet/job validation at (client only) + public JobValidator JobValidator; //Job validator to print the booklet/job validation at (client only) public bool ValidatorRequestSent = false; public bool ValidatorResponseReceived = false; public bool ValidationAccepted = false; - + public ValidationType ValidationType; + public NetworkedItem ValidationItem; + #region Client - + #endregion diff --git a/Multiplayer/Networking/Data/JobData.cs b/Multiplayer/Networking/Data/JobData.cs index 1418790..ac5e5af 100644 --- a/Multiplayer/Networking/Data/JobData.cs +++ b/Multiplayer/Networking/Data/JobData.cs @@ -20,7 +20,7 @@ public class JobData public float InitialWage { get; set; } public JobState State { get; set; } //serialise as byte public float TimeLimit { get; set; } - public Guid OwnedBy { get; set; } + public int PlayerId { get; set; } public static JobData FromJob(NetworkedJob networkedJob) { @@ -39,7 +39,7 @@ public static JobData FromJob(NetworkedJob networkedJob) InitialWage = job.initialWage, State = job.State, TimeLimit = job.TimeLimit, - OwnedBy = networkedJob.OwnedBy + PlayerId = networkedJob.playerID }; } @@ -83,6 +83,8 @@ public static void Serialize(NetDataWriter writer, JobData data) //Take on the GUID of the player //if(data.State != JobState.Available) // writer.Put(data.OwnedBy.ToByteArray()); + + writer.Put(data.PlayerId); } public static JobData Deserialize(NetDataReader reader) @@ -125,7 +127,8 @@ public static JobData Deserialize(NetDataReader reader) float timeLimit = reader.GetFloat(); //Multiplayer.Log("JobData.Deserialize() timeLimit: " + timeLimit); - //Guid ownedBy = (state != JobState.Available)? new(reader.GetBytesWithLength()) : Guid.Empty; + //int playerId = (state != JobState.Available)? new(reader.GetBytesWithLength()) : Guid.Empty; + int playerId = reader.GetInt(); return new JobData { @@ -140,7 +143,7 @@ public static JobData Deserialize(NetDataReader reader) InitialWage = initialWage, State = state, TimeLimit = timeLimit, - //OwnedBy = ownedBy, + PlayerId = playerId, }; } diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index eefb020..e1a5cd5 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -755,6 +755,22 @@ private void OnClientboundJobValidateResponsePacket(ClientboundJobValidateRespon networkedJob.ValidatorResponseReceived = true; networkedJob.ValidationAccepted = packet.Accepted; + + switch (networkedJob.ValidationType) + { + case ValidationType.JobOverview: + networkedJob.JobValidator.ProcessJobOverview(networkedJob.JobOverview); + break; + + case ValidationType.JobBooklet: + networkedJob.JobValidator.ValidateJob(networkedJob.JobBooklet); + break; + } + + if(networkedJob.ValidationItem != null) + networkedJob.ValidationItem.NetId = packet.ItemNetID; + else + LogError($"OnClientboundJobValidateResponsePacket() {packet.JobNetId}, ValidationItem not found!"); } #endregion @@ -1039,14 +1055,16 @@ public void SendLicensePurchaseRequest(string id, bool isJobLicense) }, DeliveryMethod.ReliableUnordered); } - public void SendJobValidateRequest(ushort jobNetId, ushort stationNetId, ValidationType type) + public void SendJobValidateRequest(NetworkedJob job, NetworkedStationController station) { + /* disabled for stable release SendPacketToServer(new ServerboundJobValidateRequestPacket { - JobNetId = jobNetId, - StationNetId = stationNetId, - validationType = type + JobNetId = job.NetId, + StationNetId = station.NetId, + validationType = job.ValidationType }, DeliveryMethod.ReliableUnordered); + */ } public void SendChat(string message) diff --git a/Multiplayer/Networking/Managers/NetworkManager.cs b/Multiplayer/Networking/Managers/NetworkManager.cs index 283d805..2e30de6 100644 --- a/Multiplayer/Networking/Managers/NetworkManager.cs +++ b/Multiplayer/Networking/Managers/NetworkManager.cs @@ -4,6 +4,7 @@ using LiteNetLib; using LiteNetLib.Utils; using Multiplayer.Networking.Data; +using Multiplayer.Networking.Packets.Clientbound.Jobs; using Multiplayer.Networking.Serialization; namespace Multiplayer.Networking.Listeners; @@ -40,6 +41,7 @@ protected NetworkManager(Settings settings) private void RegisterNestedTypes() { netPacketProcessor.RegisterNestedType(BogieData.Serialize, BogieData.Deserialize); + netPacketProcessor.RegisterNestedType(JobUpdateStruct.Serialize, JobUpdateStruct.Deserialize); netPacketProcessor.RegisterNestedType(JobData.Serialize, JobData.Deserialize); netPacketProcessor.RegisterNestedType(ModInfo.Serialize, ModInfo.Deserialize); netPacketProcessor.RegisterNestedType(RigidbodySnapshot.Serialize, RigidbodySnapshot.Deserialize); diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index 9d50dba..936df45 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -908,6 +908,7 @@ private void OnServerboundLicensePurchaseRequestPacket(ServerboundLicensePurchas private void OnServerboundJobValidateRequestPacket(ServerboundJobValidateRequestPacket packet, NetPeer peer) { + NetworkedItem item; if (!NetworkedJob.Get(packet.JobNetId, out NetworkedJob networkedJob)) { @@ -921,7 +922,7 @@ private void OnServerboundJobValidateRequestPacket(ServerboundJobValidateRequest return; } - if (TryGetServerPlayer(peer,out ServerPlayer player)) + if (!TryGetServerPlayer(peer, out ServerPlayer player)) { LogWarning($"OnServerboundJobValidateRequestPacket() ServerPlayer not found: {peer.Id}"); return; @@ -952,11 +953,18 @@ private void OnServerboundJobValidateRequestPacket(ServerboundJobValidateRequest networkedStationController.JobValidator.ProcessJobOverview(networkedJob.JobOverview); if(networkedJob.JobBooklet != null) { - Log($"OnServerboundJobValidateRequestPacket({networkedJob.Job?.ID}) JobState: {networkedJob.Job.State}, ACCEPTED"); + if(!networkedJob.JobBooklet.TryGetComponent(out item)) + { + LogError($"OnServerboundJobValidateRequestPacket({networkedJob.Job?.ID}) JobState: {networkedJob.Job.State}, Could not get NetworkedItem"); + return; + } + + responsePacket.ItemNetID = item.NetId; responsePacket.Accepted = true; + networkedJob.OwnedBy = player.Guid; networkedJob.playerID = peer.Id; - + Log($"OnServerboundJobValidateRequestPacket({networkedJob.Job?.ID}) JobState: {networkedJob.Job.State}, ACCEPTED"); } else { diff --git a/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobValidateResponsePacket.cs b/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobValidateResponsePacket.cs index 54a7e09..cd35fd7 100644 --- a/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobValidateResponsePacket.cs +++ b/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobValidateResponsePacket.cs @@ -5,4 +5,5 @@ public class ClientboundJobValidateResponsePacket { public ushort JobNetId { get; set; } public bool Accepted { get; set; } + public ushort ItemNetID { get; set; } } diff --git a/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobsUpdatePacket.cs b/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobsUpdatePacket.cs index 886e914..cea049a 100644 --- a/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobsUpdatePacket.cs +++ b/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobsUpdatePacket.cs @@ -1,49 +1,49 @@ -using System; -using System.Collections.Generic; using DV.ThingTypes; using LiteNetLib.Utils; -using Multiplayer.Components.Networking.Jobs; -using Multiplayer.Networking.Data; + namespace Multiplayer.Networking.Packets.Clientbound.Jobs; -public struct JobUpdateStruct : INetSerializable +public struct JobUpdateStruct { public ushort JobNetID; public bool Invalid; public JobState JobState; public float StartTime; public float FinishTime; - public Guid OwnedBy; + public ushort OwnedBy; - public void Serialize(NetDataWriter writer) + public static void Serialize(NetDataWriter writer, JobUpdateStruct data) { - writer.Put(JobNetID); - writer.Put(Invalid); + writer.Put(data.JobNetID); + writer.Put(data.Invalid); //Invalid jobs will be deleted / deregistered - if (Invalid) + if (data.Invalid) return; - writer.Put((byte)JobState); - writer.Put(StartTime); - writer.Put(FinishTime); + writer.Put((byte)data.JobState); + writer.Put(data.StartTime); + writer.Put(data.FinishTime); - if(JobState == JobState.InProgress) - writer.Put(OwnedBy.ToByteArray()); + writer.Put(data.OwnedBy); } - public void Deserialize(NetDataReader reader) + public static JobUpdateStruct Deserialize(NetDataReader reader) { - JobNetID = reader.GetUShort(); - Invalid = reader.GetBool(); + JobUpdateStruct deserialised = new JobUpdateStruct(); - if (Invalid) - return; + deserialised.JobNetID = reader.GetUShort(); + deserialised.Invalid = reader.GetBool(); + + if (deserialised.Invalid) + return deserialised; + + deserialised.JobState = (JobState) reader.GetByte(); + deserialised.StartTime = reader.GetFloat(); + deserialised.FinishTime = reader.GetFloat(); + deserialised.OwnedBy = reader.GetUShort(); - JobState = (JobState) reader.GetByte(); - StartTime = reader.GetFloat(); - FinishTime = reader.GetFloat(); - OwnedBy = (JobState == JobState.InProgress) ? new(reader.GetBytesWithLength()) : Guid.Empty; + return deserialised; } } public class ClientboundJobsUpdatePacket diff --git a/Multiplayer/Patches/Jobs/JobBookletPatch.cs b/Multiplayer/Patches/Jobs/JobBookletPatch.cs index c297dc3..ba0fd98 100644 --- a/Multiplayer/Patches/Jobs/JobBookletPatch.cs +++ b/Multiplayer/Patches/Jobs/JobBookletPatch.cs @@ -1,5 +1,7 @@ +using DV.Logic.Job; using HarmonyLib; using Multiplayer.Components.Networking.Jobs; +using Multiplayer.Components.Networking.World; namespace Multiplayer.Patches.Jobs; @@ -7,17 +9,19 @@ namespace Multiplayer.Patches.Jobs; [HarmonyPatch(typeof(JobBooklet))] public static class JobBooklet_Patch { - [HarmonyPatch(nameof(JobBooklet.Awake))] + [HarmonyPatch(nameof(JobBooklet.AssignJob))] [HarmonyPostfix] - private static void Awake(JobBooklet __instance) + private static void AssignJob(JobBooklet __instance, Job jobToAssign) { - if(!NetworkedJob.TryGetFromJob(__instance.job, out NetworkedJob networkedJob)) + if (!NetworkedJob.TryGetFromJob(__instance.job, out NetworkedJob networkedJob)) { - Multiplayer.LogError($"JobBooklet.Awake() NetworkedJob not found for Job ID: {__instance.job?.ID}"); + Multiplayer.LogError($"JobBooklet.AssignJob() NetworkedJob not found for Job ID: {__instance.job?.ID}"); return; } networkedJob.JobBooklet = __instance; + if(networkedJob.TryGetComponent(out NetworkedItem netItem)) + networkedJob.ValidationItem = netItem; } diff --git a/Multiplayer/Patches/Jobs/JobOverviewPatch.cs b/Multiplayer/Patches/Jobs/JobOverviewPatch.cs index 3e2617a..dd70720 100644 --- a/Multiplayer/Patches/Jobs/JobOverviewPatch.cs +++ b/Multiplayer/Patches/Jobs/JobOverviewPatch.cs @@ -22,7 +22,7 @@ public static class JobOverview_Patch [HarmonyPostfix] private static void Start(JobOverview __instance) { - if(!NetworkedJob.TryGetFromJob(__instance.job, out NetworkedJob networkedJob)) + if (!NetworkedJob.TryGetFromJob(__instance.job, out NetworkedJob networkedJob)) { Multiplayer.LogError($"JobOverview.Start() NetworkedJob not found for Job ID: {__instance.job?.ID}"); __instance.DestroyJobOverview(); diff --git a/Multiplayer/Patches/Jobs/JobValidatorPatch.cs b/Multiplayer/Patches/Jobs/JobValidatorPatch.cs index 5e77d6c..2d85ff1 100644 --- a/Multiplayer/Patches/Jobs/JobValidatorPatch.cs +++ b/Multiplayer/Patches/Jobs/JobValidatorPatch.cs @@ -18,34 +18,13 @@ public static class JobValidator_Patch { [HarmonyPatch(nameof(JobValidator.Start))] [HarmonyPostfix] - private static void Start_Postfix(JobValidator __instance) + private static void Start(JobValidator __instance) { - - string stationName = __instance.transform.parent.name ?? ""; - - if (string.IsNullOrEmpty(stationName)) - { - Multiplayer.LogError($"JobValidator.Start() Can not find parent's name"); - return; - } - - stationName += "_office_anchor"; - - StationController[] stations = StationController.allStations.Where(s => s.transform.parent.name.Equals(stationName,StringComparison.OrdinalIgnoreCase)).ToArray(); - - if (stations.Length == 1) - { - if(!NetworkedStationController.GetFromStationController(stations.First(), out NetworkedStationController networkedStationController)) - Multiplayer.LogError($"JobValidator.Start() Could not find NetworkedStation for validator: {stationName}"); - else - NetworkedStationController.RegisterJobValidator(__instance, networkedStationController); - } - else - { - Multiplayer.LogError($"JobValidator.Start() Found {stations.Length} stations for {stationName}"); - } + Multiplayer.Log($"JobValidator Awake!"); + NetworkedStationController.QueueJobValidator(__instance); } + [HarmonyPatch(nameof(JobValidator.ProcessJobOverview))] [HarmonyPrefix] private static bool ProcessJobOverview_Prefix(JobValidator __instance, JobOverview jobOverview) @@ -67,33 +46,15 @@ private static bool ProcessJobOverview_Prefix(JobValidator __instance, JobOvervi return false; } - if(networkedJob.ValidatorRequestSent) - { - if (networkedJob.ValidatorResponseReceived && networkedJob.ValidationAccepted) - return true; - } + if (networkedJob.ValidatorRequestSent) + return (networkedJob.ValidatorResponseReceived && networkedJob.ValidationAccepted); else - { - if(NetworkedStationController.GetFromJobValidator(__instance, out NetworkedStationController networkedStation)) - { - //Set initial job state parameters - networkedJob.ValidatorRequestSent = true; - networkedJob.ValidatorResponseReceived = false; - networkedJob.ValidationAccepted = false; - - NetworkLifecycle.Instance.Client.SendJobValidateRequest(networkedJob.NetId, networkedStation.NetId, ValidationType.JobOverview); - CoroutineManager.Instance.StartCoroutine(AwaitResponse(__instance, networkedJob, ValidationType.JobOverview)); - } - else - { - NetworkLifecycle.Instance.Client.LogError($"ProcessJobOverview_Prefix({jobOverview?.job?.ID}) Failed to find NetworkedStation"); - __instance.bookletPrinter.PlayErrorSound(); - } - } + SendValidationRequest(__instance, networkedJob, ValidationType.JobOverview); return false; } + [HarmonyPatch(nameof(JobValidator.ValidateJob))] [HarmonyPrefix] private static bool ValidateJob_Prefix(JobValidator __instance, JobBooklet jobBooklet) @@ -116,34 +77,35 @@ private static bool ValidateJob_Prefix(JobValidator __instance, JobBooklet jobBo } if (networkedJob.ValidatorRequestSent) + return (networkedJob.ValidatorResponseReceived && networkedJob.ValidationAccepted); + else + SendValidationRequest(__instance, networkedJob, ValidationType.JobBooklet); + + return false; + } + + private static void SendValidationRequest(JobValidator validator,NetworkedJob netJob, ValidationType type) + { + //find the current station we're at + if (NetworkedStationController.GetFromJobValidator(validator, out NetworkedStationController networkedStation)) { - if (networkedJob.ValidatorResponseReceived && networkedJob.ValidationAccepted) - return true; + //Set initial job state parameters + netJob.ValidatorRequestSent = true; + netJob.ValidatorResponseReceived = false; + netJob.ValidationAccepted = false; + netJob.JobValidator = validator; + netJob.ValidationType = type; + + NetworkLifecycle.Instance.Client.SendJobValidateRequest(netJob, networkedStation); + CoroutineManager.Instance.StartCoroutine(AwaitResponse(validator, netJob)); } else { - //find the current station we're at - if (NetworkedStationController.GetFromJobValidator(__instance, out NetworkedStationController networkedStation)) - { - //Set initial job state parameters - networkedJob.ValidatorRequestSent = true; - networkedJob.ValidatorResponseReceived = false; - networkedJob.ValidationAccepted = false; - - NetworkLifecycle.Instance.Client.SendJobValidateRequest(networkedJob.NetId, networkedStation.NetId, ValidationType.JobBooklet); - CoroutineManager.Instance.StartCoroutine(AwaitResponse(__instance, networkedJob, ValidationType.JobBooklet)); - } - else - { - NetworkLifecycle.Instance.Client.LogError($"ValidateJob({jobBooklet?.job?.ID}) Failed to find NetworkedStation"); - __instance.bookletPrinter.PlayErrorSound(); - } + NetworkLifecycle.Instance.Client.LogError($"SendValidation({netJob?.Job?.ID}, {type}) Failed to find NetworkedStation"); + validator.bookletPrinter.PlayErrorSound(); } - - return false; } - - private static IEnumerator AwaitResponse(JobValidator validator, NetworkedJob networkedJob, ValidationType type) + private static IEnumerator AwaitResponse(JobValidator validator, NetworkedJob networkedJob) { yield return new WaitForSecondsRealtime(NetworkLifecycle.Instance.Client.Ping * 2); @@ -154,18 +116,5 @@ private static IEnumerator AwaitResponse(JobValidator validator, NetworkedJob ne validator.bookletPrinter.PlayErrorSound(); yield break; } - - switch (type) - { - case ValidationType.JobOverview: - validator.ProcessJobOverview(networkedJob.JobOverview); - break; - - case ValidationType.JobBooklet: - validator.ValidateJob(networkedJob.JobBooklet); - break; - } - - } } From b6e6658d82033dcd81e391baaedd8bcf2ce2340f Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 8 Sep 2024 11:34:06 +1000 Subject: [PATCH 090/188] Added message box for out of date Multiplayer mod --- Multiplayer/Multiplayer.cs | 55 +++++++++++++++++++++++++++----------- 1 file changed, 40 insertions(+), 15 deletions(-) diff --git a/Multiplayer/Multiplayer.cs b/Multiplayer/Multiplayer.cs index f06022c..04343e0 100644 --- a/Multiplayer/Multiplayer.cs +++ b/Multiplayer/Multiplayer.cs @@ -2,7 +2,7 @@ using System.IO; using System.Linq; using System.Reflection; -using DV.UI; +using DV.UIFramework; using HarmonyLib; using JetBrains.Annotations; using Multiplayer.Components.MainMenu; @@ -48,6 +48,7 @@ private static bool Load(UnityModManager.ModEntry modEntry) Settings = Settings.Load(modEntry);//Settings.Load(modEntry); ModEntry.OnGUI = Settings.Draw; ModEntry.OnSaveGUI = Settings.Save; + ModEntry.OnLateUpdate = LateUpdate; Harmony harmony = null; @@ -120,23 +121,47 @@ public static bool LoadAssets() return true; } - //private static void LateUpdate(UnityModManager.ModEntry modEntry, float deltaTime) - //{ - // if (ModEntry.NewestVersion != null && ModEntry.NewestVersion.ToString() != "") - // { - // Log($"Multiplayer Latest Version: {ModEntry.NewestVersion}"); + private static void LateUpdate(UnityModManager.ModEntry modEntry, float deltaTime) + { + if (ModEntry.NewestVersion != null && ModEntry.NewestVersion.ToString() != "") + { + Log($"Multiplayer Latest Version: {ModEntry.NewestVersion}"); + + ModEntry.OnLateUpdate -= Multiplayer.LateUpdate; + + if (ModEntry.NewestVersion > ModEntry.Version) + { + if (MainMenuThingsAndStuff.Instance != null) + { + Popup update = MainMenuThingsAndStuff.Instance.ShowOkPopup(); + + if (update == null) + return; + + update.labelTMPro.text = "Multiplayer Mod Update Available!\r\n\r\n"+ + $"Latest version:\t\t{ModEntry.NewestVersion}\r\n" + + $"Installed version:\t\t{ModEntry.Version}\r\n\r\n" + + "Run Unity Mod Manager Installer to apply the update."; - // ModEntry.OnLateUpdate -= Multiplayer.LateUpdate; + Vector3 currPos = update.labelTMPro.transform.localPosition; + Vector2 size = update.labelTMPro.rectTransform.sizeDelta; - // if (ModEntry.NewestVersion > ModEntry.Version) - // { - // if (MainMenuThingsAndStuff.Instance != null) - // { + float delta = size.y - update.labelTMPro.preferredHeight; + currPos.y -= delta *2 ; + size.y = update.labelTMPro.preferredHeight; - // } - // } - // } - //} + update.labelTMPro.transform.localPosition = currPos; + update.labelTMPro.rectTransform.sizeDelta = size; + + currPos = update.positiveButton.transform.localPosition; + currPos.y += delta * 2; + update.positiveButton.transform.localPosition = currPos; + + + } + } + } + } #region Logging From 53e21807f373a8e4689a3492633b5e051c743863 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 8 Sep 2024 11:50:15 +1000 Subject: [PATCH 091/188] Fixed RPC timeouts to work in milliseconds rather than seconds --- Multiplayer/Patches/CommsRadio/CommsRadioCarDeleterPatch.cs | 2 +- Multiplayer/Patches/CommsRadio/RerailControllerPatch.cs | 2 +- Multiplayer/Patches/Jobs/JobValidatorPatch.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Multiplayer/Patches/CommsRadio/CommsRadioCarDeleterPatch.cs b/Multiplayer/Patches/CommsRadio/CommsRadioCarDeleterPatch.cs index 7d17bc3..731595b 100644 --- a/Multiplayer/Patches/CommsRadio/CommsRadioCarDeleterPatch.cs +++ b/Multiplayer/Patches/CommsRadio/CommsRadioCarDeleterPatch.cs @@ -42,7 +42,7 @@ private static bool OnUse_Prefix(CommsRadioCarDeleter __instance) private static IEnumerator PlaySoundsLater(CommsRadioCarDeleter __instance, Vector3 trainPosition, bool playMoneyRemovedSound = true) { - yield return new WaitForSecondsRealtime(NetworkLifecycle.Instance.Client.Ping * 2); + yield return new WaitForSecondsRealtime((NetworkLifecycle.Instance.Client.Ping * 3f)/1000); if (playMoneyRemovedSound && __instance.moneyRemovedSound != null) __instance.moneyRemovedSound.Play2D(); // The TrainCar may already be deleted when we're done waiting, so we play the sound manually. diff --git a/Multiplayer/Patches/CommsRadio/RerailControllerPatch.cs b/Multiplayer/Patches/CommsRadio/RerailControllerPatch.cs index 00403e3..3534dab 100644 --- a/Multiplayer/Patches/CommsRadio/RerailControllerPatch.cs +++ b/Multiplayer/Patches/CommsRadio/RerailControllerPatch.cs @@ -61,7 +61,7 @@ private static bool OnUse_Prefix(RerailController __instance) private static IEnumerator PlayerSoundsLater(RerailController __instance) { - yield return new WaitForSecondsRealtime(NetworkLifecycle.Instance.Client.Ping * 2); + yield return new WaitForSecondsRealtime((NetworkLifecycle.Instance.Client.Ping * 3f)/1000); if (__instance.moneyRemovedSound != null) __instance.moneyRemovedSound.Play2D(); CommsRadioController.PlayAudioFromCar(__instance.rerailingSound, __instance.carToRerail); diff --git a/Multiplayer/Patches/Jobs/JobValidatorPatch.cs b/Multiplayer/Patches/Jobs/JobValidatorPatch.cs index 2d85ff1..75ea7af 100644 --- a/Multiplayer/Patches/Jobs/JobValidatorPatch.cs +++ b/Multiplayer/Patches/Jobs/JobValidatorPatch.cs @@ -107,7 +107,7 @@ private static void SendValidationRequest(JobValidator validator,NetworkedJob ne } private static IEnumerator AwaitResponse(JobValidator validator, NetworkedJob networkedJob) { - yield return new WaitForSecondsRealtime(NetworkLifecycle.Instance.Client.Ping * 2); + yield return new WaitForSecondsRealtime((NetworkLifecycle.Instance.Client.Ping * 3f)/1000); NetworkLifecycle.Instance.Client.Log($"JobValidator_Patch.AwaitResponse() ResponseReceived: {networkedJob?.ValidatorResponseReceived}, Accepted: {networkedJob?.ValidationAccepted}"); From 7de37688a2724653478fa8d0b7cc58a1f96c46e8 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 8 Sep 2024 12:15:41 +1000 Subject: [PATCH 092/188] Fixed error log, ready for release --- Multiplayer/Patches/Train/WindowsBreakingControllerPatch.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Multiplayer/Patches/Train/WindowsBreakingControllerPatch.cs b/Multiplayer/Patches/Train/WindowsBreakingControllerPatch.cs index 05162f3..3f32e50 100644 --- a/Multiplayer/Patches/Train/WindowsBreakingControllerPatch.cs +++ b/Multiplayer/Patches/Train/WindowsBreakingControllerPatch.cs @@ -49,7 +49,7 @@ public static void RepairWindows_Postfix(WindowsBreakingController __instance) return; } - Multiplayer.LogWarning($"RepairWindows_Postfix , {car.name}"); + Multiplayer.LogDebug(()=>$"RepairWindows_Postfix, {car.name}"); NetworkLifecycle.Instance.Server.SendWindowsRepaired(netId); } } From 6ae42d0a52287c2c90e57755c6d8409152299c38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Drobni=C4=8D?= Date: Tue, 10 Sep 2024 10:21:24 +0200 Subject: [PATCH 093/188] Fix project board link in the README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3524b75..570dd2a 100644 --- a/README.md +++ b/README.md @@ -112,4 +112,4 @@ See [LICENSE][license-url] for more information. [contributing-quickstart-url]: https://docs.github.com/en/get-started/quickstart/contributing-to-projects [asset-studio-url]: https://github.com/Perfare/AssetStudio [mapify-building-docs]: https://dv-mapify.readthedocs.io/en/latest/contributing/building/ -[project-board-url]: https://github.com/users/Insprill/projects/8 +[project-board-url]: https://github.com/users/AMacro/projects/2 From 5c4c9ce163e71c82407f0ba6459386d172ddb717 Mon Sep 17 00:00:00 2001 From: Macka Date: Fri, 13 Sep 2024 17:10:40 +1000 Subject: [PATCH 094/188] Update README.md --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 570dd2a..502fd3d 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,11 @@ +# Important! +At present, assume all other mods are incompatible! +Some mods may work, but many do cause issues and break multiplayer capabilities. +Our primary focus is to have the vanilla game working in multiplayer; once this is achieved we will then work on compatibility with other mods. From e82949ae981381767633b2dfa95e805fc287d43f Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 15 Sep 2024 17:38:09 +1000 Subject: [PATCH 095/188] Completed ping functionality for Server Browser Future work, maybe port punch for pinging? --- .../ServerBrowser/ServerBrowserElement.cs | 24 +- .../ServerBrowser/ServerBrowserGridView.cs | 7 +- .../Components/MainMenu/ServerBrowserPane.cs | 233 ++++++++++++++---- .../Managers/Client/ServerBrowserClient.cs | 41 +-- .../Networking/Managers/NetworkManager.cs | 3 +- .../Managers/Server/NetworkServer.cs | 2 +- .../Unconnected/UnconnectedPingPacket.cs | 13 +- 7 files changed, 239 insertions(+), 84 deletions(-) diff --git a/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserElement.cs b/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserElement.cs index e1c122b..6413243 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserElement.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserElement.cs @@ -1,6 +1,5 @@ using DV.UIFramework; using Multiplayer.Utils; -using System.ComponentModel; using TMPro; using UnityEngine; using UnityEngine.UI; @@ -19,7 +18,7 @@ public class ServerBrowserElement : AViewElement 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 - private void Awake() + protected override void Awake() { // Find and assign TextMeshProUGUI components for displaying server details networkName = this.FindChildByName("name [noloc]").GetComponent(); @@ -62,12 +61,12 @@ public override void SetData(IServerBrowserGameDetails data, AGridView{(data.Ping < 0 ? "?" : data.Ping)} ms"; // Hide the icon if the server does not have a password if (!data.HasPassword) @@ -75,5 +74,22 @@ private void UpdateView(object sender = null, PropertyChangedEventArgs e = null) goIcon.SetActive(false); } } + + private string GetColourForPing(int ping) + { + switch (ping) + { + case -1: + return "#808080"; // Mid-range gray for unknown + case < 60: + return "#00ff00"; // Bright green for excellent ping + case < 100: + return "#ffa500"; // Orange for good ping + case < 150: + return "#ff4500"; // OrangeRed for high ping + default: + return "#ff0000"; // Red for don't even bother + } + } } } diff --git a/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserGridView.cs b/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserGridView.cs index b674e23..3a861b3 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserGridView.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserGridView.cs @@ -13,7 +13,7 @@ namespace Multiplayer.Components.MainMenu public class ServerBrowserGridView : AGridView { - private void Awake() + protected override void Awake() { Multiplayer.Log("serverBrowserGridview Awake()"); @@ -35,5 +35,10 @@ private void Awake() this.dummyElementPrefab.SetActive(true); } + + public ServerBrowserElement GetElementAt(int index) + { + return transform.GetChild(index + indexOffset).GetComponent(); + } } } diff --git a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs index ace13c4..9eb782b 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs @@ -17,15 +17,46 @@ using DV; using System.Net; using LiteNetLib; -using LiteNetLib.Utils; using Multiplayer.Networking.Listeners; using System.Collections.Generic; -using System.Timers; namespace Multiplayer.Components.MainMenu { public class ServerBrowserPane : MonoBehaviour { + private class PingRecord + { + public int ping1; + public int ping2; + int received; + + public PingRecord() + { + ping1 = -1; + ping2 = -1; + } + + public int Avg() + { + Multiplayer.Log($"Avg() {ping1}, {ping2}"); + + if (received >= 2 && ping1 >-1 && ping2 > -1) + return (ping1 + ping2) / 2; + else + return Math.Max(ping1, ping2); + } + + public void AddPing(int ping) + { + Multiplayer.Log($"AddPing() ping1 {ping1}, ping2 {ping2}, new {ping}, {received}"); + ping1 = ping2; + ping2 = ping; + + if(received < 2) + received++; + } + } + // Regular expressions for IP and port validation // @formatter:off // Patterns from https://ihateregex.io/ @@ -45,6 +76,15 @@ public class ServerBrowserPane : MonoBehaviour private string serverIDOnRefresh; private IServerBrowserGameDetails selectedServer; + //ping tracking + private List serversToPing = new List(); + private Dictionary serverPings = new Dictionary(); + + private float pingTimer = 0f; + private const float PING_INTERVAL = 30f; // base interval to refresh all pings + private const float PING_BATCH_INTERVAL = 0.5f; //gap bwetween ping batches + private const int SERVERS_PER_BATCH = 10; + //Button variables private ButtonDV buttonJoin; private ButtonDV buttonRefresh; @@ -98,11 +138,6 @@ private void Awake() SetupServerBrowser(); //FillDummyServers(); RefreshAction(); - - //Start Server - serverBrowserClient = new ServerBrowserClient(Multiplayer.Settings); - serverBrowserClient.OnPing += this.OnPing; - serverBrowserClient.Start(); } private void OnEnable() @@ -119,12 +154,24 @@ private void OnEnable() buttonDirectIP.ToggleInteractable(true); buttonRefresh.ToggleInteractable(true); + + //Start the server browser network client + serverBrowserClient = new ServerBrowserClient(Multiplayer.Settings); + serverBrowserClient.OnPing += this.OnPing; + serverBrowserClient.Start(); } // Disable listeners private void OnDisable() { this.SetupListeners(false); + + if (serverBrowserClient != null) + { + serverBrowserClient.OnPing -= this.OnPing; + serverBrowserClient.Stop(); + serverBrowserClient = null; + } } private void OnDestroy() @@ -135,7 +182,11 @@ private void OnDestroy() private void Update() { - + //Poll for any LAN discovery or ping packets + if (serverBrowserClient != null) + serverBrowserClient.PollEvents(); + + //Handle server refresh interval timePassed += Time.deltaTime; if (autoRefresh && !serverRefreshing) @@ -149,6 +200,15 @@ private void Update() buttonRefresh.ToggleInteractable(true); } } + + //Handle pinging servers + pingTimer += Time.deltaTime; + + if (pingTimer >= (serversToPing.Count > 0 ? PING_BATCH_INTERVAL : GetPingInterval())) + { + PingNextBatch(); + pingTimer = 0f; + } } private void CleanUI() @@ -325,9 +385,7 @@ private void SetupListeners(bool on) { this.gridView.SelectedIndexChanged -= this.IndexChanged; } - } - #endregion #region UI callbacks @@ -415,6 +473,19 @@ private void IndexChanged(AGridView gridView) } } + private void UpdateElement(IServerBrowserGameDetails element) + { + int index = gridViewModel.IndexOf(element); + + if (index >= 0) + { + var viewElement = gridView.GetElementAt(index); + if (viewElement != null) + { + viewElement.UpdateView(); + } + } + } #endregion private void UpdateDetailsPane() @@ -629,7 +700,6 @@ public void ShowConnectingPopup() }; } - private void AttemptConnection() { @@ -690,7 +760,6 @@ private void AttemptIPv6() SingletonBehaviour.Instance.StartClient(address, portNumber, password, false, OnDisconnect); } - private void AttemptIPv6Punch() { Multiplayer.Log($"AttemptIPv6Punch() {address}"); @@ -707,7 +776,6 @@ private void AttemptIPv6Punch() SingletonBehaviour.Instance.StartClient(address, portNumber, password, false, OnDisconnect); } - private void AttemptIPv4() { Multiplayer.Log($"AttemptIPv4() {address}, {connectionState}"); @@ -782,7 +850,6 @@ private void AttemptFail() buttonDirectIP.ToggleInteractable(true); } - private void OnDisconnect(DisconnectReason reason, string message) { Multiplayer.LogError($"Connection failed! {reason}, \"{message}\""); @@ -895,9 +962,43 @@ IEnumerator GetRequest(string uri) { gridView.showDummyElement = false; } - gridViewModel.Clear(); + + bool startPing = gridViewModel.Count == 0; + + + //Get Server update lists + List serversClosed = gridViewModel.Where(element => !response.Any(resp => resp.id == element.id)).ToList(); + List<(IServerBrowserGameDetails, LobbyServerData)> serversUpdate = gridViewModel.Join( + response, + element => element.id, + resp => resp.id, + (element, resp) => (element, resp) + ).ToList(); + LobbyServerData[] serversNew = response.Where(element => !gridViewModel.Any(resp => resp.id == element.id)).ToArray(); + + Multiplayer.Log($"servers closed: {serversClosed.Count()}, servers new: {serversNew.Count()}, servers update: {serversUpdate.Count()}"); + + //Remove expired + foreach(IServerBrowserGameDetails server in serversClosed) + { + if(serverPings.ContainsKey(server.id)) + serverPings.Remove(server.id); + + gridViewModel.Remove(server); + } + + //Add new servers + gridViewModel.AddRange(serversNew); + + //Update existing servers + foreach((IServerBrowserGameDetails, LobbyServerData) server in serversUpdate) + { + server.Item1.TimePassed = server.Item2.TimePassed; + server.Item1.CurrentPlayers = server.Item2.CurrentPlayers; + } + + //Update the gridview rendering gridView.SetModel(gridViewModel); - gridViewModel.AddRange(response); //if we have a server selected, we need to re-select it after refresh if (serverIDOnRefresh != null) @@ -912,12 +1013,14 @@ IEnumerator GetRequest(string uri) this.parentScroller.verticalNormalizedPosition = 1f - (float)selID / (float)gridView.Model.Count; } } - serverIDOnRefresh = null; } - + //trigger ping to start + if (startPing) + PingNextBatch(); } + } serverRefreshing = false; @@ -931,33 +1034,6 @@ private void SetButtonsActive(params GameObject[] buttons) } } - //private void FillDummyServers() - //{ - // gridView.showDummyElement = false; - // gridViewModel.Clear(); - - - // IServerBrowserGameDetails item = null; - - // for (int i = 0; i < UnityEngine.Random.Range(1, 50); i++) - // { - - // 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); - // 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.Ver : "0.1.0"; - - // gridViewModel.Add(item); - // } - - // gridView.SetModel(gridViewModel); - //} - private string ExtractDomainName(string input) { if (input.StartsWith("http://")) @@ -978,17 +1054,74 @@ private string ExtractDomainName(string input) return input; } - private void OnPing(string serverId, int ping, bool isIPv4, bool isIPv6) + #region Network Utils + private void OnPing(string serverId, int ping, bool isIPv4) + { + Multiplayer.Log($"OnPing() Ping: {ping}, {(isIPv4?"IPv4" : "IPv6")}"); + + if (!serverPings.ContainsKey(serverId)) + serverPings[serverId] = (new PingRecord(), new PingRecord()); + + if (isIPv4) + serverPings[serverId].IPv4Ping.AddPing(ping); + else + serverPings[serverId].IPv6Ping.AddPing(ping); + + var server = gridViewModel.FirstOrDefault(s => s.id == serverId); + if (server != null) + { + server.Ping = GetBestPing(serverPings[serverId].IPv4Ping.Avg(), serverPings[serverId].IPv6Ping.Avg()); + UpdateElement(server); + } + } + private void SendPing(IServerBrowserGameDetails server) + { + serverBrowserClient.SendUnconnectedPingPacket(server.id, server.ipv4, server.ipv6, server.port); + } + + private float GetPingInterval() { - Multiplayer.Log($"ServerBrowser.OnPing({serverId}, {ping} ms, IPv4 {isIPv4}, IPv6 {isIPv6} )"); + int serverCount = gridViewModel.Count; + if (serverCount < 10) return PING_INTERVAL; + if (serverCount < 50) return PING_INTERVAL * 2; + if (serverCount < 100) return PING_INTERVAL * 4; + return PING_INTERVAL * 10; } - private void SendPing() + private void PingNextBatch() { - if (selectedServer != null) + if (serversToPing.Count == 0) + { + serversToPing.AddRange(gridViewModel); + } + + var batch = serversToPing.Take(SERVERS_PER_BATCH).ToList(); + foreach (var server in batch) + { + SendPing(server); + } + serversToPing.RemoveRange(0, batch.Count); + + if (serversToPing.Count == 0) + pingTimer = 0; //Get ready to start from the beginning + } + + private int GetBestPing(int ipv4Ping, int ipv6Ping) + { + if (ipv4Ping > -1 && ipv6Ping > -1) + { + return Math.Min(ipv4Ping, ipv6Ping); + } + else if (ipv4Ping > -1) { - serverBrowserClient.SendUnconnectedPingPacket(selectedServer.id, selectedServer.ipv4, selectedServer.ipv6, selectedServer.port); + return ipv4Ping; } + else if (ipv6Ping > -1) + { + return ipv6Ping; + } + return -1; // No ping available } + #endregion } } diff --git a/Multiplayer/Networking/Managers/Client/ServerBrowserClient.cs b/Multiplayer/Networking/Managers/Client/ServerBrowserClient.cs index fe157f5..639ff40 100644 --- a/Multiplayer/Networking/Managers/Client/ServerBrowserClient.cs +++ b/Multiplayer/Networking/Managers/Client/ServerBrowserClient.cs @@ -32,7 +32,7 @@ public void Start() } private Dictionary pingInfos = new Dictionary(); - public Action OnPing; // serverId, pingTime, isIPv4, isIPv6 + public Action OnPing; // serverId, pingTime, isIPv4 private const int PingTimeoutMs = 5000; // 5 seconds timeout @@ -42,7 +42,6 @@ public ServerBrowserClient(Settings settings) : base(settings) public void Start() { - Log($"ServerBrowserClient.Start()"); netManager.Start(); } public override void Stop() @@ -73,7 +72,7 @@ private async Task CleanupTimedOutPings() foreach (var serverId in timedOutServers) { pingInfos.Remove(serverId); - Log($"Cleaned up timed out ping for {serverId}"); + LogDebug(() => $"Cleaned up timed out ping for {serverId}"); } } } @@ -108,26 +107,27 @@ public override void OnConnectionRequest(ConnectionRequest request) private void OnUnconnectedPingPacket(UnconnectedPingPacket packet, IPEndPoint endPoint) { string serverId = new Guid(packet.ServerID).ToString(); - Log($"OnUnconnectedPingPacket({serverId ?? ""}, {endPoint?.Address})"); + //Log($"OnUnconnectedPingPacket({serverId ?? ""}, {endPoint?.Address})"); if (pingInfos.TryGetValue(serverId, out PingInfo pingInfo)) { - pingInfo.Stopwatch.Stop(); int pingTime = (int)pingInfo.Stopwatch.ElapsedMilliseconds; bool isIPv4 = endPoint.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork; + if (isIPv4) pingInfo.IPv4Received = true; else pingInfo.IPv6Received = true; - OnPing?.Invoke(serverId, pingTime, pingInfo.IPv4Received, pingInfo.IPv6Received); - - Log($"Ping received for {serverId}: {pingTime}ms, IPv4: {pingInfo.IPv4Received}, IPv6: {pingInfo.IPv6Received}"); + OnPing?.Invoke(serverId, pingTime, isIPv4); - if (pingInfo.IPv4Received && pingInfo.IPv6Received) + LogDebug(()=>$"OnUnconnectedPingPacket() serverId {serverId}, IPv4 ({pingInfo.IPv4Sent}, {pingInfo.IPv4Received}), IPv6 ({pingInfo.IPv6Sent}, {pingInfo.IPv6Received})"); + if ((!pingInfo.IPv4Sent || pingInfo.IPv4Received) && (!pingInfo.IPv6Sent || pingInfo.IPv6Received)) { + pingInfo.Stopwatch.Stop(); pingInfos.Remove(serverId); + LogDebug(()=>$"OnUnconnectedPingPacket() removed {serverId}"); } } } @@ -135,7 +135,7 @@ private void OnUnconnectedPingPacket(UnconnectedPingPacket packet, IPEndPoint en #endregion #region Senders - public async Task SendUnconnectedPingPacket(string serverId, string ipv4, string ipv6, int port) + public void SendUnconnectedPingPacket(string serverId, string ipv4, string ipv6, int port) { if (!Guid.TryParse(serverId, out Guid server)) { @@ -146,22 +146,22 @@ public async Task SendUnconnectedPingPacket(string serverId, string ipv4, string PingInfo pingInfo = new PingInfo(); pingInfos[serverId] = pingInfo; - Log($"Sending ping to {serverId} at IPv4: {ipv4}, IPv6: {ipv6}, Port: {port}"); - pingInfo.Start(); - + LogDebug(()=>$"Sending ping to {serverId} at IPv4: {ipv4}, IPv6: {ipv6}, Port: {port}"); var packet = new UnconnectedPingPacket { ServerID = server.ToByteArray() }; + pingInfo.Start(); + // Send to IPv4 if provided if (!string.IsNullOrEmpty(ipv4)) { - SendUnconnnectedPacket(packet, ipv4, port); + SendUnconnectedPacket(packet, ipv4, port); pingInfo.IPv4Sent = true; } // Send to IPv6 if provided if (!string.IsNullOrEmpty(ipv6)) { - SendUnconnnectedPacket(packet, ipv6, port); + SendUnconnectedPacket(packet, ipv6, port); pingInfo.IPv6Sent = true; } @@ -175,9 +175,16 @@ private async Task StartTimeoutTask(string serverId) if (pingInfos.TryGetValue(serverId, out PingInfo pingInfo)) { pingInfo.Stopwatch.Stop(); - OnPing?.Invoke(serverId, -1, pingInfo.IPv4Received, pingInfo.IPv6Received); + LogDebug(() => $"Ping timeout for {serverId}, elapsed: {pingInfo.Stopwatch.ElapsedMilliseconds}, IPv4: ({pingInfo.IPv4Sent}, {pingInfo.IPv4Received}), IPv6: ({pingInfo.IPv6Sent}, {pingInfo.IPv6Received}) "); + + if(!pingInfo.IPv4Received && pingInfo.IPv4Sent) + OnPing?.Invoke(serverId, -1, true); + + if (!pingInfo.IPv6Received && pingInfo.IPv6Sent) + OnPing?.Invoke(serverId, -1, false); + + pingInfos.Remove(serverId); - Log($"Ping timeout for {serverId}"); } } diff --git a/Multiplayer/Networking/Managers/NetworkManager.cs b/Multiplayer/Networking/Managers/NetworkManager.cs index 2e30de6..fb115a8 100644 --- a/Multiplayer/Networking/Managers/NetworkManager.cs +++ b/Multiplayer/Networking/Managers/NetworkManager.cs @@ -90,7 +90,7 @@ public virtual void Stop() peer?.Send(WritePacket(packet), deliveryMethod); } - protected void SendUnconnnectedPacket(T packet, string ipAddress, int port) where T : class, new() + protected void SendUnconnectedPacket(T packet, string ipAddress, int port) where T : class, new() { netManager.SendUnconnectedMessage(WritePacket(packet), ipAddress, port); } @@ -101,6 +101,7 @@ public virtual void Stop() public void OnNetworkReceive(NetPeer peer, NetPacketReader reader, byte channelNumber, DeliveryMethod deliveryMethod) { + Log($"NetworkManager.OnNetworkReceive()"); try { IsProcessingPacket = true; diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index 936df45..c8cedfd 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -1003,7 +1003,7 @@ private void OnCommonChatPacket(CommonChatPacket packet, NetPeer peer) private void OnUnconnectedPingPacket(UnconnectedPingPacket packet, IPEndPoint endPoint) { Multiplayer.Log($"OnUnconnectedPingPacket({endPoint.Address})"); - SendUnconnnectedPacket(packet, endPoint.Address.ToString(),endPoint.Port); + SendUnconnectedPacket(packet, endPoint.Address.ToString(),endPoint.Port); } #endregion } diff --git a/Multiplayer/Networking/Packets/Unconnected/UnconnectedPingPacket.cs b/Multiplayer/Networking/Packets/Unconnected/UnconnectedPingPacket.cs index 0722dd4..5ee2159 100644 --- a/Multiplayer/Networking/Packets/Unconnected/UnconnectedPingPacket.cs +++ b/Multiplayer/Networking/Packets/Unconnected/UnconnectedPingPacket.cs @@ -1,13 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +namespace Multiplayer.Networking.Packets.Unconnected; -namespace Multiplayer.Networking.Packets.Unconnected +public class UnconnectedPingPacket { - public class UnconnectedPingPacket - { - public byte[] ServerID { get; set; } - } + public byte[] ServerID { get; set; } } From 503e4a0cd2a278932a9af19aa8769652aae6472c Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 15 Sep 2024 17:38:40 +1000 Subject: [PATCH 096/188] General clean up and warnings fixes --- .../MainMenu/ServerBrowser/ServerBrowserDummyElement.cs | 9 +-------- .../Networking/World/NetworkedStationController.cs | 2 +- Multiplayer/Networking/Data/LobbyServerResponseData.cs | 8 -------- Multiplayer/Networking/Data/LobbyServerUpdateData.cs | 1 - 4 files changed, 2 insertions(+), 18 deletions(-) diff --git a/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserDummyElement.cs b/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserDummyElement.cs index e36384a..bf56f5f 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserDummyElement.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserDummyElement.cs @@ -12,15 +12,8 @@ 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() + protected override void Awake() { // Find and assign TextMeshProUGUI components for displaying server details GameObject networkNameGO = this.FindChildByName("name [noloc]"); diff --git a/Multiplayer/Components/Networking/World/NetworkedStationController.cs b/Multiplayer/Components/Networking/World/NetworkedStationController.cs index 638440b..f6be4e5 100644 --- a/Multiplayer/Components/Networking/World/NetworkedStationController.cs +++ b/Multiplayer/Components/Networking/World/NetworkedStationController.cs @@ -105,7 +105,7 @@ private static void RegisterJobValidator(JobValidator jobValidator, NetworkedSta private List NewJobs = new List(); private List DirtyJobs = new List(); - private void Awake() + protected override void Awake() { base.Awake(); StationController = GetComponent(); diff --git a/Multiplayer/Networking/Data/LobbyServerResponseData.cs b/Multiplayer/Networking/Data/LobbyServerResponseData.cs index 1a75af2..4990298 100644 --- a/Multiplayer/Networking/Data/LobbyServerResponseData.cs +++ b/Multiplayer/Networking/Data/LobbyServerResponseData.cs @@ -1,11 +1,3 @@ -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 diff --git a/Multiplayer/Networking/Data/LobbyServerUpdateData.cs b/Multiplayer/Networking/Data/LobbyServerUpdateData.cs index f611cfd..3e632ac 100644 --- a/Multiplayer/Networking/Data/LobbyServerUpdateData.cs +++ b/Multiplayer/Networking/Data/LobbyServerUpdateData.cs @@ -1,6 +1,5 @@ using Newtonsoft.Json; - namespace Multiplayer.Networking.Data { public class LobbyServerUpdateData From 38eacc4720fca1ca1d31b0eba553ddf68510dbc4 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 21 Sep 2024 12:08:09 +1000 Subject: [PATCH 097/188] Added LAN discovery --- .../Components/MainMenu/HostGamePane.cs | 5 +- .../IServerBrowserGameDetails.cs | 6 +- .../ServerBrowser/ServerBrowserElement.cs | 38 ++- .../Components/MainMenu/ServerBrowserPane.cs | 256 ++++++++++++------ .../Components/Networking/NetworkLifecycle.cs | 3 +- Multiplayer/Multiplayer.csproj | 2 +- .../Networking/Data/LobbyServerData.cs | 39 ++- .../Managers/Client/ServerBrowserClient.cs | 73 +++-- .../Networking/Managers/NetworkManager.cs | 4 +- .../Managers/Server/LobbyServerManager.cs | 152 ++++++++++- .../Managers/Server/NetworkServer.cs | 6 +- .../Unconnected/UnconnectedDiscoveryPacket.cs | 15 + MultiplayerAssets/Assets/AssetIndex.asset | 1 + .../Assets/Scripts/Multiplayer/AssetIndex.cs | 1 + .../Scripts/Multiplayer/AssetIndex.cs.meta | 16 +- .../Assets/Textures/LAN_icon.png | Bin 0 -> 7131 bytes .../Assets/Textures/LAN_icon.png.meta | 104 +++++++ .../Assets/Textures/Refresh.png.meta | 4 +- .../Assets/Textures/lock_icon.png.meta | 4 +- .../Assets/Textures/multiplayer_icon.png.meta | 4 +- info.json | 2 +- 21 files changed, 583 insertions(+), 152 deletions(-) create mode 100644 Multiplayer/Networking/Packets/Unconnected/UnconnectedDiscoveryPacket.cs create mode 100644 MultiplayerAssets/Assets/Textures/LAN_icon.png create mode 100644 MultiplayerAssets/Assets/Textures/LAN_icon.png.meta diff --git a/Multiplayer/Components/MainMenu/HostGamePane.cs b/Multiplayer/Components/MainMenu/HostGamePane.cs index daad67a..63ab038 100644 --- a/Multiplayer/Components/MainMenu/HostGamePane.cs +++ b/Multiplayer/Components/MainMenu/HostGamePane.cs @@ -342,6 +342,7 @@ private void StartClick() serverData.port = (port.text == "") ? Multiplayer.Settings.Port : int.Parse(port.text); ; serverData.Name = serverName.text.Trim(); serverData.HasPassword = password.text != ""; + serverData.isPublic = gamePublic.isOn; serverData.GameMode = 0; //replaced with details from save / new game serverData.Difficulty = 0; //replaced with details from save / new game @@ -375,7 +376,7 @@ private void StartClick() Multiplayer.Settings.ServerName = serverData.Name; Multiplayer.Settings.Password = password.text; - Multiplayer.Settings.PublicGame = gamePublic.isOn; + Multiplayer.Settings.PublicGame = serverData.isPublic; Multiplayer.Settings.Port = serverData.port; Multiplayer.Settings.MaxPlayers = serverData.MaxPlayers; Multiplayer.Settings.Details = serverData.ServerDetails; @@ -383,8 +384,6 @@ private void StartClick() //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; diff --git a/Multiplayer/Components/MainMenu/ServerBrowser/IServerBrowserGameDetails.cs b/Multiplayer/Components/MainMenu/ServerBrowser/IServerBrowserGameDetails.cs index 0c4622d..c6d0c6b 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowser/IServerBrowserGameDetails.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowser/IServerBrowserGameDetails.cs @@ -16,6 +16,7 @@ public interface IServerBrowserGameDetails : IDisposable string id { get; set; } string ipv6 { get; set; } string ipv4 { get; set; } + string LocalIPv4 { get; set; } int port { get; set; } string Name { get; set; } bool HasPassword { get; set; } @@ -28,7 +29,8 @@ public interface IServerBrowserGameDetails : IDisposable string GameVersion { get; set; } string MultiplayerVersion { get; set; } string ServerDetails { get; set; } - int Ping { get; set; } - + int Ping {get; set; } + bool isPublic { get; set; } + int LastSeen { get; set; } } } diff --git a/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserElement.cs b/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserElement.cs index 6413243..09e153a 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserElement.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserElement.cs @@ -1,5 +1,6 @@ using DV.UIFramework; using Multiplayer.Utils; +using System; using TMPro; using UnityEngine; using UnityEngine.UI; @@ -11,8 +12,10 @@ public class ServerBrowserElement : AViewElement private TextMeshProUGUI networkName; private TextMeshProUGUI playerCount; private TextMeshProUGUI ping; - private GameObject goIcon; - private Image icon; + private GameObject goIconPassword; + private Image iconPassword; + private GameObject goIconLAN; + private Image iconLAN; private IServerBrowserGameDetails data; private const int PING_WIDTH = 124; // Adjusted width for the ping text @@ -24,8 +27,8 @@ protected override void Awake() 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(); + goIconPassword = this.FindChildByName("autosave icon"); + iconPassword = goIconPassword.GetComponent(); // Fix alignment of the player count text relative to the network name text Vector3 namePos = networkName.transform.position; @@ -41,8 +44,25 @@ protected override void Awake() ping.transform.position = new Vector3(PING_POS_X, pingPos.y, pingPos.z); ping.alignment = TextAlignmentOptions.Right; - // Set change icon - icon.sprite = Multiplayer.AssetIndex.lockIcon; + // Set password icon + iconPassword.sprite = Multiplayer.AssetIndex.lockIcon; + + // Set LAN icon + try + { + goIconLAN = this.FindChildByName("LAN Icon"); + } + catch (Exception e) + { + goIconLAN = Instantiate(goIconPassword, goIconPassword.transform.parent); + goIconLAN.name = "LAN Icon"; + Vector3 LANpos = goIconLAN.transform.localPosition; + Vector3 LANSize = goIconLAN.GetComponent().sizeDelta; + LANpos.x += (PING_POS_X - LANpos.x - LANSize.x) / 2; + goIconLAN.transform.localPosition = LANpos; + iconLAN = goIconLAN.GetComponent(); + iconLAN.sprite = Multiplayer.AssetIndex.lanIcon; + } } public override void SetData(IServerBrowserGameDetails data, AGridView _) @@ -69,10 +89,8 @@ public void UpdateView() ping.text = $"{(data.Ping < 0 ? "?" : data.Ping)} ms"; // Hide the icon if the server does not have a password - if (!data.HasPassword) - { - goIcon.SetActive(false); - } + goIconPassword.SetActive(data.HasPassword); + goIconLAN.SetActive(!string.IsNullOrEmpty(data.LocalIPv4)); } private string GetColourForPing(int ping) diff --git a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs index 9eb782b..fb101b2 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs @@ -26,8 +26,8 @@ public class ServerBrowserPane : MonoBehaviour { private class PingRecord { - public int ping1; - public int ping2; + int ping1; + int ping2; int received; public PingRecord() @@ -38,8 +38,6 @@ public PingRecord() public int Avg() { - Multiplayer.Log($"Avg() {ping1}, {ping2}"); - if (received >= 2 && ping1 >-1 && ping2 > -1) return (ping1 + ping2) / 2; else @@ -48,7 +46,7 @@ public int Avg() public void AddPing(int ping) { - Multiplayer.Log($"AddPing() ping1 {ping1}, ping2 {ping2}, new {ping}, {received}"); + //Multiplayer.Log($"AddPing() ping1 {ping1}, ping2 {ping2}, new {ping}, {received}"); ping1 = ping2; ping2 = ping; @@ -57,6 +55,17 @@ public void AddPing(int ping) } } + private enum ConnectionState + { + NotConnected, + AttemptingIPv6, + AttemptingIPv6Punch, + AttemptingIPv4, + AttemptingIPv4Punch, + Failed, + Aborted + } + // Regular expressions for IP and port validation // @formatter:off // Patterns from https://ihateregex.io/ @@ -81,10 +90,17 @@ public void AddPing(int ping) private Dictionary serverPings = new Dictionary(); private float pingTimer = 0f; - private const float PING_INTERVAL = 30f; // base interval to refresh all pings + private const float PING_INTERVAL = 2f; // base interval to refresh all pings private const float PING_BATCH_INTERVAL = 0.5f; //gap bwetween ping batches private const int SERVERS_PER_BATCH = 10; + //LAN tracking + private List localServers = new List(); + private const int LAN_TIMEOUT = 60; //How long to hold a LAN server without a response + private const int DISCOVERY_TIMEOUT = 2; //how long to wait for servers to respond + private bool localRefreshComplete; + private float discoveryTimer = 0f; + //Button variables private ButtonDV buttonJoin; private ButtonDV buttonRefresh; @@ -93,14 +109,14 @@ public void AddPing(int ping) //Misc GUI Elements private TextMeshProUGUI serverName; private TextMeshProUGUI detailsPane; - //private ScrollRect serverInfo; - + //Remote server tracking + private List remoteServers = new List(); 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 + private bool remoteRefreshComplete; private ServerBrowserClient serverBrowserClient; @@ -114,18 +130,7 @@ public void AddPing(int ping) private Popup connectingPopup; private int attempt; - private enum ConnectionState - { - NotConnected, - AttemptingIPv6, - AttemptingIPv6Punch, - AttemptingIPv4, - AttemptingIPv4Punch, - Failed, - Aborted - } - //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 @@ -136,7 +141,7 @@ private void Awake() BuildUI(); SetupServerBrowser(); - //FillDummyServers(); + RefreshGridView(); RefreshAction(); } @@ -158,6 +163,7 @@ private void OnEnable() //Start the server browser network client serverBrowserClient = new ServerBrowserClient(Multiplayer.Settings); serverBrowserClient.OnPing += this.OnPing; + serverBrowserClient.OnDiscovery += this.OnDiscovery; serverBrowserClient.Start(); } @@ -176,6 +182,9 @@ private void OnDisable() private void OnDestroy() { + if (serverBrowserClient == null) + return; + serverBrowserClient.OnPing -= this.OnPing; serverBrowserClient.Stop(); } @@ -188,8 +197,9 @@ private void Update() //Handle server refresh interval timePassed += Time.deltaTime; + discoveryTimer += Time.deltaTime; - if (autoRefresh && !serverRefreshing) + if (!serverRefreshing) { if (timePassed >= AUTO_REFRESH_TIME) { @@ -200,6 +210,22 @@ private void Update() buttonRefresh.ToggleInteractable(true); } } + else if(localRefreshComplete && remoteRefreshComplete) + { + ExpireLocalServers(); //remove any that have not been seen in a while + RefreshGridView(); + + localRefreshComplete = false; + remoteRefreshComplete = false; + serverRefreshing = false; + timePassed = 0; + } + else + { + if (discoveryTimer >= DISCOVERY_TIMEOUT) + localRefreshComplete = true; + } + //Handle pinging servers pingTimer += Time.deltaTime; @@ -374,6 +400,8 @@ private void SetupServerBrowser() //Don't forget to re-enable! GridviewGO.SetActive(true); + + gridView.showDummyElement = true; } private void SetupListeners(bool on) { @@ -395,17 +423,19 @@ private void RefreshAction() return; if (selectedServer != null) - { serverIDOnRefresh = selectedServer.id; - } + + remoteServers.Clear(); serverRefreshing = true; - autoRefresh = true; buttonJoin.ToggleInteractable(false); buttonRefresh.ToggleInteractable(false); StartCoroutine(GetRequest($"{Multiplayer.Settings.LobbyServerAddress}/list_game_servers")); + //Send a message to find local peers + discoveryTimer = 0f; + serverBrowserClient?.SendDiscoveryRequest(); } private void JoinAction() { @@ -665,13 +695,11 @@ private void ShowPasswordPopup() Multiplayer.Settings.LastRemoteIP = address; Multiplayer.Settings.LastRemotePort = portNumber; Multiplayer.Settings.LastRemotePassword = result.data; - } password = result.data; AttemptConnection(); - //SingletonBehaviour.Instance.StartClient(address, portNumber, result.data, false, OnDisconnect); }; } @@ -953,78 +981,92 @@ IEnumerator GetRequest(string uri) Multiplayer.Log($"Server name: \"{server.Name}\", IPv4: {server.ipv4}, IPv6: {server.ipv6}, Port: {server.port}"); } - if (response.Length == 0) - { - gridView.showDummyElement = true; - buttonJoin.ToggleInteractable(false); - } - else - { - gridView.showDummyElement = false; - } + remoteServers.AddRange(response); - bool startPing = gridViewModel.Count == 0; + } + remoteRefreshComplete = true; + } + } - //Get Server update lists - List serversClosed = gridViewModel.Where(element => !response.Any(resp => resp.id == element.id)).ToList(); - List<(IServerBrowserGameDetails, LobbyServerData)> serversUpdate = gridViewModel.Join( - response, - element => element.id, - resp => resp.id, - (element, resp) => (element, resp) - ).ToList(); - LobbyServerData[] serversNew = response.Where(element => !gridViewModel.Any(resp => resp.id == element.id)).ToArray(); + private void RefreshGridView() + { - Multiplayer.Log($"servers closed: {serversClosed.Count()}, servers new: {serversNew.Count()}, servers update: {serversUpdate.Count()}"); + bool startPing = gridViewModel.Count == 0; - //Remove expired - foreach(IServerBrowserGameDetails server in serversClosed) - { - if(serverPings.ContainsKey(server.id)) - serverPings.Remove(server.id); + var allServers = new List(); + allServers.AddRange(localServers); + allServers.AddRange(remoteServers.Where(r => !localServers.Any(l => l.id == r.id))); - gridViewModel.Remove(server); - } + // Get all active IDs + List activeIDs = allServers.Select(s => s.id).Distinct().ToList(); - //Add new servers - gridViewModel.AddRange(serversNew); + Multiplayer.Log($"RefreshGridView() Active servers: {activeIDs.Count}\r\n{string.Join("\r\n", activeIDs)}"); - //Update existing servers - foreach((IServerBrowserGameDetails, LobbyServerData) server in serversUpdate) - { - server.Item1.TimePassed = server.Item2.TimePassed; - server.Item1.CurrentPlayers = server.Item2.CurrentPlayers; - } + // Find servers to remove + List removeList = gridViewModel.Where(gv => !activeIDs.Contains(gv.id)).ToList(); + Multiplayer.Log($"RefreshGridView() Remove List: {removeList.Count}\r\n{string.Join("\r\n", removeList.Select(l => l.id))}"); - //Update the gridview rendering - gridView.SetModel(gridViewModel); + // Remove expired servers + foreach (var remove in removeList) + { + Multiplayer.Log($"RefreshGridView() Removing: {remove.id}"); + if (serverPings.ContainsKey(remove.id)) + serverPings.Remove(remove.id); + gridViewModel.Remove(remove); + } - //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); + // Update existing servers and add new ones + foreach (var server in allServers) + { + var existingServer = gridViewModel.FirstOrDefault(gv => gv.id == server.id); + if (existingServer != null) + { + // Update existing server + existingServer.TimePassed = server.TimePassed; + existingServer.CurrentPlayers = server.CurrentPlayers; + existingServer.LocalIPv4 = server.LocalIPv4; + existingServer.LastSeen = server.LastSeen; + } + else + { + // Add new server + gridViewModel.Add(server); + } + } - if (this.parentScroller) - { - this.parentScroller.verticalNormalizedPosition = 1f - (float)selID / (float)gridView.Model.Count; - } - } - serverIDOnRefresh = null; - } + if (gridViewModel.Count() == 0) + { + gridView.showDummyElement = true; + buttonJoin.ToggleInteractable(false); + } + else + { + gridView.showDummyElement = false; + } - //trigger ping to start - if (startPing) - PingNextBatch(); - } + //Update the gridview rendering + gridView.SetModel(gridViewModel); + + //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; - timePassed = 0; + //trigger ping to start + if (startPing && gridViewModel.Count() > 0) + PingNextBatch(); } private void SetButtonsActive(params GameObject[] buttons) { @@ -1057,7 +1099,7 @@ private string ExtractDomainName(string input) #region Network Utils private void OnPing(string serverId, int ping, bool isIPv4) { - Multiplayer.Log($"OnPing() Ping: {ping}, {(isIPv4?"IPv4" : "IPv6")}"); + //Multiplayer.Log($"OnPing() Ping: {ping}, {(isIPv4?"IPv4" : "IPv6")}"); if (!serverPings.ContainsKey(serverId)) serverPings[serverId] = (new PingRecord(), new PingRecord()); @@ -1076,7 +1118,12 @@ private void OnPing(string serverId, int ping, bool isIPv4) } private void SendPing(IServerBrowserGameDetails server) { - serverBrowserClient.SendUnconnectedPingPacket(server.id, server.ipv4, server.ipv6, server.port); + string ipv4 = server.ipv4; + + if(!string.IsNullOrEmpty(server.LocalIPv4)) + ipv4 = server.LocalIPv4; + + serverBrowserClient.SendUnconnectedPingPacket(server.id, ipv4, server.ipv6, server.port); } private float GetPingInterval() @@ -1122,6 +1169,43 @@ private int GetBestPing(int ipv4Ping, int ipv6Ping) } return -1; // No ping available } + + private void OnDiscovery(IPEndPoint endpoint, LobbyServerData data) + { + //Multiplayer.Log($"OnDiscovery({endpoint}) ID: {data.id}, Name: {data.Name}"); + + IServerBrowserGameDetails existing = localServers.FirstOrDefault(element => element.id == data.id); + if (existing != default(IServerBrowserGameDetails)) + { + localServers.Remove(existing); + } + + data.LastSeen = (int)Time.time; + localServers.Add(data); + + existing = gridViewModel.FirstOrDefault(element => element.id == data.id); + if (existing != default(IServerBrowserGameDetails)) + { + existing.LastSeen = (int)Time.time; + existing.LocalIPv4 = data.LocalIPv4; + } + + data.LastSeen = (int)Time.time; + localServers.Add(data); + } + + private void ExpireLocalServers() + { + List timedOut = localServers.Where(s => (s.LastSeen + LAN_TIMEOUT) < Time.time ).ToList(); + + foreach (IServerBrowserGameDetails expired in timedOut) + { + if (serverPings.ContainsKey(expired.id)) + serverPings.Remove(expired.id); + + localServers.Remove(expired); + } + } #endregion } } diff --git a/Multiplayer/Components/Networking/NetworkLifecycle.cs b/Multiplayer/Components/Networking/NetworkLifecycle.cs index b2e519f..64eccac 100644 --- a/Multiplayer/Components/Networking/NetworkLifecycle.cs +++ b/Multiplayer/Components/Networking/NetworkLifecycle.cs @@ -135,10 +135,9 @@ public bool StartServer(IDifficulty difficulty) } Multiplayer.Log($"Starting server on port {port}"); - NetworkServer server = new(difficulty, Multiplayer.Settings, isPublicGame, isSinglePlayer, serverData); + NetworkServer server = new(difficulty, Multiplayer.Settings, isSinglePlayer, serverData); //reset for next game - isPublicGame = false; isSinglePlayer = true; serverData = null; diff --git a/Multiplayer/Multiplayer.csproj b/Multiplayer/Multiplayer.csproj index 2af864a..f16678c 100644 --- a/Multiplayer/Multiplayer.csproj +++ b/Multiplayer/Multiplayer.csproj @@ -3,7 +3,7 @@ net48 latest Multiplayer - 0.1.8.3 + 0.1.8.4 diff --git a/Multiplayer/Networking/Data/LobbyServerData.cs b/Multiplayer/Networking/Data/LobbyServerData.cs index 1d4f8e5..2323a88 100644 --- a/Multiplayer/Networking/Data/LobbyServerData.cs +++ b/Multiplayer/Networking/Data/LobbyServerData.cs @@ -1,10 +1,9 @@ +using LiteNetLib.Utils; using Multiplayer.Components.MainMenu; using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using Newtonsoft.Json.Linq; +using System.Reflection; +using UnityEngine.Profiling; namespace Multiplayer.Networking.Data { @@ -17,6 +16,9 @@ public class LobbyServerData : IServerBrowserGameDetails public string ipv6 { get; set; } public int port { get; set; } + [JsonIgnore] + public string LocalIPv4 { get; set; } + [JsonProperty("server_name")] public string Name { get; set; } @@ -61,7 +63,11 @@ public class LobbyServerData : IServerBrowserGameDetails public string ServerDetails { get; set; } [JsonIgnore] - public int Ping { get; set; } + public int Ping { get; set; } = -1; + [JsonIgnore] + public bool isPublic { get; set; } + [JsonIgnore] + public int LastSeen { get; set; } = int.MaxValue; public void Dispose() { } @@ -147,5 +153,26 @@ public static string GetGameModeFromInt(int difficulty) return diff; } + public static void Serialize(NetDataWriter writer, LobbyServerData data) + { + //Multiplayer.Log($"LobbyServerData.Serialize() {writer != null }, {data != null} "); + + //have we got data? + writer.Put(data != null); + + if (data != null) + writer.Put(new NetSerializer().Serialize(data)); + + //Multiplayer.Log($"LobbyServerData.Serialize() {writer != null}, {data != null} POST"); + + } + + public static LobbyServerData Deserialize(NetDataReader reader) + { + if(reader.GetBool()) + return new NetSerializer().Deserialize(reader); + else + return null; + } } } diff --git a/Multiplayer/Networking/Managers/Client/ServerBrowserClient.cs b/Multiplayer/Networking/Managers/Client/ServerBrowserClient.cs index 639ff40..33e027e 100644 --- a/Multiplayer/Networking/Managers/Client/ServerBrowserClient.cs +++ b/Multiplayer/Networking/Managers/Client/ServerBrowserClient.cs @@ -1,13 +1,13 @@ using System; using System.Net; -using System.Text; using System.Collections.Generic; using LiteNetLib; using Multiplayer.Networking.Packets.Unconnected; -using Newtonsoft.Json.Linq; using System.Threading.Tasks; using System.Diagnostics; using System.Linq; +using Multiplayer.Networking.Managers.Server; +using Multiplayer.Networking.Data; namespace Multiplayer.Networking.Listeners; @@ -33,6 +33,9 @@ public void Start() private Dictionary pingInfos = new Dictionary(); public Action OnPing; // serverId, pingTime, isIPv4 + public Action OnDiscovery; // endPoint, serverId, serverData + + private int[] discoveryPorts = { 8888, 8889, 8890 }; private const int PingTimeoutMs = 5000; // 5 seconds timeout @@ -43,6 +46,8 @@ public ServerBrowserClient(Settings settings) : base(settings) public void Start() { netManager.Start(); + netManager.UseNativeSockets = true; + netManager.UpdateTime = 0; } public override void Stop() { @@ -77,9 +82,30 @@ private async Task CleanupTimedOutPings() } } + private async Task StartTimeoutTask(string serverId) + { + await Task.Delay(PingTimeoutMs); + if (pingInfos.TryGetValue(serverId, out PingInfo pingInfo)) + { + pingInfo.Stopwatch.Stop(); + LogDebug(() => $"Ping timeout for {serverId}, elapsed: {pingInfo.Stopwatch.ElapsedMilliseconds}, IPv4: ({pingInfo.IPv4Sent}, {pingInfo.IPv4Received}), IPv6: ({pingInfo.IPv6Sent}, {pingInfo.IPv6Received}) "); + + if (!pingInfo.IPv4Received && pingInfo.IPv4Sent) + OnPing?.Invoke(serverId, -1, true); + + if (!pingInfo.IPv6Received && pingInfo.IPv6Sent) + OnPing?.Invoke(serverId, -1, false); + + + pingInfos.Remove(serverId); + } + } + protected override void Subscribe() { + netPacketProcessor.RegisterNestedType(LobbyServerData.Serialize, LobbyServerData.Deserialize); netPacketProcessor.SubscribeReusable(OnUnconnectedPingPacket); + netPacketProcessor.SubscribeReusable(OnUnconnectedDiscoveryPacket); } #region Net Events @@ -122,16 +148,29 @@ private void OnUnconnectedPingPacket(UnconnectedPingPacket packet, IPEndPoint en OnPing?.Invoke(serverId, pingTime, isIPv4); - LogDebug(()=>$"OnUnconnectedPingPacket() serverId {serverId}, IPv4 ({pingInfo.IPv4Sent}, {pingInfo.IPv4Received}), IPv6 ({pingInfo.IPv6Sent}, {pingInfo.IPv6Received})"); + //LogDebug(()=>$"OnUnconnectedPingPacket() serverId {serverId}, IPv4 ({pingInfo.IPv4Sent}, {pingInfo.IPv4Received}), IPv6 ({pingInfo.IPv6Sent}, {pingInfo.IPv6Received})"); if ((!pingInfo.IPv4Sent || pingInfo.IPv4Received) && (!pingInfo.IPv6Sent || pingInfo.IPv6Received)) { pingInfo.Stopwatch.Stop(); pingInfos.Remove(serverId); - LogDebug(()=>$"OnUnconnectedPingPacket() removed {serverId}"); + //LogDebug(()=>$"OnUnconnectedPingPacket() removed {serverId}"); } } } + private void OnUnconnectedDiscoveryPacket(UnconnectedDiscoveryPacket packet, IPEndPoint endPoint) + { + //Log($"OnUnconnectedDiscoveryPacket({packet.PacketType}, {endPoint?.Address})"); + + switch (packet.PacketType) + { + case DiscoveryPacketType.Response: + //Log($"OnUnconnectedDiscoveryPacket({packet.PacketType}, {endPoint?.Address}) id: {packet.data.id}"); + OnDiscovery?.Invoke(endPoint,packet.data); + break; + } + } + #endregion #region Senders @@ -139,7 +178,7 @@ public void SendUnconnectedPingPacket(string serverId, string ipv4, string ipv6, { if (!Guid.TryParse(serverId, out Guid server)) { - LogError($"SendUnconnectedPingPacket({serverId}) failed to parse GUID"); + //LogError($"SendUnconnectedPingPacket({serverId}) failed to parse GUID"); return; } @@ -169,22 +208,18 @@ public void SendUnconnectedPingPacket(string serverId, string ipv4, string ipv6, _ = StartTimeoutTask(serverId); } - private async Task StartTimeoutTask(string serverId) + public void SendDiscoveryRequest() { - await Task.Delay(PingTimeoutMs); - if (pingInfos.TryGetValue(serverId, out PingInfo pingInfo)) + foreach (int port in discoveryPorts) { - pingInfo.Stopwatch.Stop(); - LogDebug(() => $"Ping timeout for {serverId}, elapsed: {pingInfo.Stopwatch.ElapsedMilliseconds}, IPv4: ({pingInfo.IPv4Sent}, {pingInfo.IPv4Received}), IPv6: ({pingInfo.IPv6Sent}, {pingInfo.IPv6Received}) "); - - if(!pingInfo.IPv4Received && pingInfo.IPv4Sent) - OnPing?.Invoke(serverId, -1, true); - - if (!pingInfo.IPv6Received && pingInfo.IPv6Sent) - OnPing?.Invoke(serverId, -1, false); - - - pingInfos.Remove(serverId); + try + { + netManager.SendBroadcast(WritePacket(new UnconnectedDiscoveryPacket()), port); + } + catch (Exception ex) + { + Multiplayer.Log($"SendDiscoveryRequest() Broadcast error: {ex.Message}\r\n{ex.StackTrace}"); + } } } diff --git a/Multiplayer/Networking/Managers/NetworkManager.cs b/Multiplayer/Networking/Managers/NetworkManager.cs index fb115a8..b1d4aee 100644 --- a/Multiplayer/Networking/Managers/NetworkManager.cs +++ b/Multiplayer/Networking/Managers/NetworkManager.cs @@ -101,7 +101,7 @@ public virtual void Stop() public void OnNetworkReceive(NetPeer peer, NetPacketReader reader, byte channelNumber, DeliveryMethod deliveryMethod) { - Log($"NetworkManager.OnNetworkReceive()"); + //Log($"NetworkManager.OnNetworkReceive()"); try { IsProcessingPacket = true; @@ -124,7 +124,7 @@ public void OnNetworkError(IPEndPoint endPoint, SocketError socketError) public void OnNetworkReceiveUnconnected(IPEndPoint remoteEndPoint, NetPacketReader reader, UnconnectedMessageType messageType) { - Multiplayer.Log($"OnNetworkReceiveUnconnected({remoteEndPoint}, {messageType})"); + //Multiplayer.Log($"OnNetworkReceiveUnconnected({remoteEndPoint}, {messageType})"); try { IsProcessingPacket = true; diff --git a/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs b/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs index b65be1a..71a7beb 100644 --- a/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs +++ b/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs @@ -10,6 +10,11 @@ using System.Text.RegularExpressions; using System.Net.NetworkInformation; using System.Net.Sockets; +using LiteNetLib; +using LiteNetLib.Utils; +using Multiplayer.Networking.Packets.Unconnected; +using System.Net; +using LocoSim.Implementations; namespace Multiplayer.Networking.Managers.Server; public class LobbyServerManager : MonoBehaviour @@ -33,12 +38,17 @@ public class LobbyServerManager : MonoBehaviour private string private_key { get; set; } private bool initialised = false; - - - private bool sendUpdates = false; private float timePassed = 0f; + //LAN discovery + private NetManager discoveryManager; + private NetPacketProcessor packetProcessor; + private EventBasedNetListener discoveryListener; + private NetDataWriter cachedWriter = new(); + public static int[] discoveryPorts = { 8888, 8889, 8890 }; + + #region MonoBehavior private void Awake() { server = NetworkLifecycle.Instance.Server; @@ -49,15 +59,38 @@ private void Awake() private IEnumerator Start() { server.serverData.ipv6 = GetStaticIPv6Address(); + server.serverData.LocalIPv4 = GetLocalIPv4Address(); + StartCoroutine(GetIPv4(Multiplayer.Settings.Ipv4AddressCheck)); - yield return new WaitUntil(() => initialised); - Multiplayer.Log("Public IPv4: " + server.serverData.ipv4); - Multiplayer.Log("Public IPv6: " + server.serverData.ipv6); + while(!initialised) + yield return null; + + server.Log("Public IPv4: " + server.serverData.ipv4); + server.Log("Public IPv6: " + server.serverData.ipv6); + server.Log("Private IPv4: " + server.serverData.LocalIPv4); + + if (server.serverData.isPublic) + { + Multiplayer.Log($"Registering server at: {Multiplayer.Settings.LobbyServerAddress}/{ENDPOINT_ADD_SERVER}"); + StartCoroutine(RegisterWithLobbyServer($"{Multiplayer.Settings.LobbyServerAddress}/{ENDPOINT_ADD_SERVER}")); + + //allow the server some time to register (should take less than a second) + float timeout = 5f; + while (server_id == null || server_id == string.Empty || (timeout -= Time.deltaTime) <= 0) + yield return null; + + + } + + if(server_id == null || server_id == string.Empty) + { + server_id = $"LAN-{Guid.NewGuid()}"; + } - Multiplayer.Log("Registering server at: " + Multiplayer.Settings.LobbyServerAddress + "/add_game_server"); + server.serverData.id = server_id; - StartCoroutine(RegisterWithLobbyServer(Multiplayer.Settings.LobbyServerAddress + "/add_game_server")); + StartDiscoveryServer(); } private void OnDestroy() @@ -66,6 +99,8 @@ private void OnDestroy() sendUpdates = false; StopAllCoroutines(); StartCoroutine(RemoveFromLobbyServer($"{Multiplayer.Settings.LobbyServerAddress}/{ENDPOINT_REMOVE_SERVER}")); + + discoveryManager?.Stop(); } private void Update() @@ -80,9 +115,18 @@ private void Update() server.serverData.CurrentPlayers = server.PlayerCount; StartCoroutine(UpdateLobbyServer($"{Multiplayer.Settings.LobbyServerAddress}/{ENDPOINT_UPDATE_SERVER}")); } + }else if (!server.serverData.isPublic || !sendUpdates) + { + server.serverData.CurrentPlayers = server.PlayerCount; } + + //Keep LAN discovery running + discoveryManager?.PollEvents(); } + #endregion + + #region Lobby Server public void RemoveFromLobbyServer() { Multiplayer.Log($"RemoveFromLobbyServer OnDestroy()"); @@ -285,7 +329,7 @@ private IEnumerator SendWebRequestGET(string uri, Action onSucc } } } - + #endregion public static string GetStaticIPv6Address() { foreach (NetworkInterface networkInterface in NetworkInterface.GetAllNetworkInterfaces()) @@ -309,4 +353,94 @@ public static string GetStaticIPv6Address() } return null; } + + public static string GetLocalIPv4Address() + { + foreach (NetworkInterface networkInterface in NetworkInterface.GetAllNetworkInterfaces()) + { + bool flag = !networkInterface.Supports(NetworkInterfaceComponent.IPv4) || networkInterface.OperationalStatus != OperationalStatus.Up || networkInterface.NetworkInterfaceType == NetworkInterfaceType.Loopback; + if (!flag) + { + IPInterfaceProperties properties = networkInterface.GetIPProperties(); + if (properties.GatewayAddresses.Count == 0) + continue; + + foreach (UnicastIPAddressInformation unicastIPAddressInformation in properties.UnicastAddresses) + { + bool flag2 = unicastIPAddressInformation.Address.AddressFamily == AddressFamily.InterNetwork; + if (flag2) + { + return unicastIPAddressInformation.Address.ToString(); + } + } + } + } + return null; + } + + #region LAN Discovery + public void StartDiscoveryServer() + { + server.Log($"StartDiscoveryServer()"); + discoveryListener = new EventBasedNetListener(); + discoveryManager = new NetManager(discoveryListener) + { + UnconnectedMessagesEnabled = true, + BroadcastReceiveEnabled = true, + }; + packetProcessor = new NetPacketProcessor(discoveryManager); + + discoveryListener.NetworkReceiveUnconnectedEvent += OnNetworkReceiveUnconnected; + + packetProcessor.RegisterNestedType(LobbyServerData.Serialize, LobbyServerData.Deserialize); + packetProcessor.SubscribeReusable(OnUnconnectedDiscoveryPacket); + + foreach (int port in discoveryPorts) + { + if (discoveryManager.Start(port)) + server.LogDebug(()=>$"Discovery server started on port {port}"); + else + server.LogError($"Failed to start discovery server on port {port}"); + } + } + protected NetDataWriter WritePacket(T packet) where T : class, new() + { + cachedWriter.Reset(); + packetProcessor.Write(cachedWriter, packet); + return cachedWriter; + } + protected void SendUnconnectedPacket(T packet, string ipAddress, int port) where T : class, new() + { + discoveryManager.SendUnconnectedMessage(WritePacket(packet), ipAddress, port); + } + public void OnNetworkReceiveUnconnected(IPEndPoint remoteEndPoint, NetPacketReader reader, UnconnectedMessageType messageType) + { + //server.Log($"LobbyServerManager.OnNetworkReceiveUnconnected({remoteEndPoint}, {messageType})"); + try + { + packetProcessor.ReadAllPackets(reader, remoteEndPoint); + } + catch (ParseException e) + { + server.LogWarning($"LobbyServerManager.OnNetworkReceiveUnconnected() Failed to parse packet: {e.Message}"); + } + } + + private void OnUnconnectedDiscoveryPacket(UnconnectedDiscoveryPacket packet, IPEndPoint endPoint) + { + //server.LogDebug(()=>$"OnUnconnectedDiscoveryPacket({packet.PacketType}, {endPoint.Address},{endPoint.Port})"); + + switch (packet.PacketType) + { + case DiscoveryPacketType.Discovery: + packet.PacketType = DiscoveryPacketType.Response; + packet.data = server.serverData; + break; + default: + return; + } + + SendUnconnectedPacket(packet, endPoint.Address.ToString(), endPoint.Port); + } + #endregion } diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index c8cedfd..2cbb40b 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -44,7 +44,6 @@ public class NetworkServer : NetworkManager private readonly Dictionary netPeers = new(); private LobbyServerManager lobbyServerManager; - public bool isPublic; public bool isSinglePlayer; public LobbyServerData serverData; public RerailController rerailController; @@ -62,9 +61,8 @@ public class NetworkServer : NetworkManager //we don't care if the client doesn't have these mods private string[] modWhiteList = { "RuntimeUnityEditor" }; - public NetworkServer(IDifficulty difficulty, Settings settings, bool isPublic, bool isSinglePlayer, LobbyServerData serverData) : base(settings) + public NetworkServer(IDifficulty difficulty, Settings settings, bool isSinglePlayer, LobbyServerData serverData) : base(settings) { - this.isPublic = isPublic; this.isSinglePlayer = isSinglePlayer; this.serverData = serverData; @@ -141,7 +139,7 @@ protected override void Subscribe() private void OnLoaded() { //Debug.Log($"Server loaded, isSinglePlayer: {isSinglePlayer} isPublic: {isPublic}"); - if (!isSinglePlayer && isPublic) + if (!isSinglePlayer) { lobbyServerManager = NetworkLifecycle.Instance.GetOrAddComponent(); } diff --git a/Multiplayer/Networking/Packets/Unconnected/UnconnectedDiscoveryPacket.cs b/Multiplayer/Networking/Packets/Unconnected/UnconnectedDiscoveryPacket.cs new file mode 100644 index 0000000..94f8238 --- /dev/null +++ b/Multiplayer/Networking/Packets/Unconnected/UnconnectedDiscoveryPacket.cs @@ -0,0 +1,15 @@ +using LiteNetLib.Utils; +using Multiplayer.Networking.Data; + +namespace Multiplayer.Networking.Packets.Unconnected; + +public enum DiscoveryPacketType : byte +{ + Discovery = 1, + Response = 2, +} +public class UnconnectedDiscoveryPacket +{ + public DiscoveryPacketType PacketType { get; set; } = DiscoveryPacketType.Discovery; + public LobbyServerData data { get; set; } +} diff --git a/MultiplayerAssets/Assets/AssetIndex.asset b/MultiplayerAssets/Assets/AssetIndex.asset index 735f514..48bf760 100644 --- a/MultiplayerAssets/Assets/AssetIndex.asset +++ b/MultiplayerAssets/Assets/AssetIndex.asset @@ -18,3 +18,4 @@ MonoBehaviour: lockIcon: {fileID: 21300000, guid: b8a707a2b12db584fad32aed46912dd0, type: 3} refreshIcon: {fileID: 21300000, guid: 7c3f2166549e6e144ae26c8d527d59b0, type: 3} connectIcon: {fileID: 21300000, guid: dad0fda7f8df3cd41a278a839fe12d23, type: 3} + lanIcon: {fileID: 21300000, guid: 8386cff9a47c8a2409ad12ae6ae2233e, type: 3} diff --git a/MultiplayerAssets/Assets/Scripts/Multiplayer/AssetIndex.cs b/MultiplayerAssets/Assets/Scripts/Multiplayer/AssetIndex.cs index 2a89138..7f74bf2 100644 --- a/MultiplayerAssets/Assets/Scripts/Multiplayer/AssetIndex.cs +++ b/MultiplayerAssets/Assets/Scripts/Multiplayer/AssetIndex.cs @@ -13,5 +13,6 @@ public class AssetIndex : ScriptableObject public Sprite lockIcon; public Sprite refreshIcon; public Sprite connectIcon; + public Sprite lanIcon; } } diff --git a/MultiplayerAssets/Assets/Scripts/Multiplayer/AssetIndex.cs.meta b/MultiplayerAssets/Assets/Scripts/Multiplayer/AssetIndex.cs.meta index f58bced..7e50507 100644 --- a/MultiplayerAssets/Assets/Scripts/Multiplayer/AssetIndex.cs.meta +++ b/MultiplayerAssets/Assets/Scripts/Multiplayer/AssetIndex.cs.meta @@ -1,3 +1,17 @@ fileFormatVersion: 2 guid: 6ab658f490174d2e96148e7e6e27ad3a -timeCreated: 1689643659 \ No newline at end of file +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: + - playerPrefab: {instanceID: 0} + - 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} + - lanIcon: {fileID: 21300000, guid: 8386cff9a47c8a2409ad12ae6ae2233e, type: 3} + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Textures/LAN_icon.png b/MultiplayerAssets/Assets/Textures/LAN_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..142abee22b6582fa95cac46d043301a303dc57b5 GIT binary patch literal 7131 zcmd5=c{tQ-ynkm*%p7EyqKx%)vS#1L5)z|iNjk{ZAlW8F%055kpapdz+n^{$Awt>B zIgZ8_(v%@v*$Sn`*4XC$=seH8|J-~3yU(5HnP=wx{l4G#{Vt#H_Fg|}W5NH6*e?J8 z{^Q5YP62>~{~`gJ8~#}eXRN_Lh|p6Orl7n>{4=~j`5du60zhRd?}o=Nc#R1@b|w^n z-G6O=5w&*$z5sv=K5lm8bh!JMkpQapz!{!zjhnLW->&XeO)8WB)xoaX{Uph}?ZiWe z5>FTPp;$GYgCz<>#DJV3VddoG)}A*Ad@BXj{y2UIMTYZ@4EeW1s-`8!j_i6}VE&hY zX>^`mK=x+dDrI#(W~JL%vntj&93N$ScWGW*vv+NwX{Ng#0gLs2{2_iXPs=xvlO%6^ zS51-X)sMK`dcd*Vm3=IN50LC^w(M5Tl4B1lav?y4x!!Aw!)aAp3PCBtNI(n-xtbcO z<+C2T;g==|fXf|h6k}}W5IHW@Z1Yl@GynnV*odq?W67|jMuG_%Ks6m0!&%2rotB4< z4m<1i>jtw|LPd=Gr3e^!|4Vuv7Ms8KjAAwx-eFcYQU(C$E{vXH(4>9W`vREU_n_QI>gr^v>)=^4c(;@GHqCbI-00ET;&gIK zh#LTm+t@(HS5Kp|5!^u(pt*(=Bhn%bhBqprZ(D1|)QK#4rI zDqTp59IEiBGZJ5ZNwp|!O#=|f2XB&T<9-cdybaL?!Bs8O(X^%-}`jGThV@Au>fcF~YOP(Vxp`{Aj1 z%=?N5QVXf|9dd}idjO=TUpH=9;FM6^$N0%wv;21Dybu8es8p-17iJ?mBdnzmBno7n$=VpGfc4@Ch7}5e zFf>rRrtx{kI%Z3ER<6KE`NqvnTSl)1?49$nfX4SLFx+Px%bMJual7l67I!qnHZ0jd z$(|bBBzJUZswlL+nd@{yYJS52E2@=KP|8|@2Nyl^&}`k|XkkHmk560bL3cUUgMA#1 zeCnVe0?3|}-6~U=elyf!zIlpK^prQr>(l=46SlT_%AJ~C!(%RzRWi*;=s+ZNx}2xKgWb8_l+r_Xj&*x`%pt@%G0PP7dftm1g`h34z)-v$Fy?a zlXpB5ohS&_;Z4Q$vdpI1V#(O$^j5;XLkk6~al<}WfvXjOG}ADXE7Zom`8T1sC%2!d zOdB=0p`kgLDNWf$`lS0jbx)=CAWgviqk|iTFWaJcO4lh4-H5TI*Zhc>iJK_bpB>H%g$CBo<|`fr9s8< zM{zT&4eZL6wrJ*n4`E7uQCJ2l!vZFyzxFI{xxj-AkJE~B$OY+R9nRf}4$X&VukAP6g?MKPh}xEguV6%R8}|*#S$GS^lnBzq zFrd`buVRp-Jl)#AagH$;58e>~@w=s0OjKH1w9{j$q{t_Nv}`muA0+Q?jZTG$-P zM0YJOQH^`O90ZB{C?Haz)dw4{xBWr|(l({jYsEnZdV~c|lzu!}rqk1?^!L#$mW>Q_ z5Pp`{uO^kUcE^LObYLe6oD>m|dAscX^s&_zT}4r1oN_pU(XhxAK{<&bplM4lanVKg z)nRTNi$OJf9wdZx5=KBpmPe~I1rFCL&+dj#5(K_QTBbMIeRoC8Mxt!cKNeWn$AW;W zl5=(&jux;TAL!~5nkH@ue!F6~U>Jd3!~*JLttMDz)mIn#9u%9$NMHN554D|V$#%Ud ziBCIc)N~P$zhz;RA8dv31e}qw;E=fYy7??qfbvDpF34yvl9de=n@!nfQUHzgEXy1Y!&LK_ zR~16|UR|>$)!r<1v>EzGFVz*TWGn<7$AInvt%_~4c3+_VVWg_qnNyXznw<6ucId0;>vl) zec?ClvU|PNr7|5x<>%ZY0+WwOo0`~c?MJd)ubsdGytAK`$YNYX^zqYqE~-NqM8rj6 z0<_@_RLsG+{3DqrL)e{aPaDzSyuHxJgpeUF?$T%;EU2ySPm0x^E%M+}dm%~~iBUvN zYU{F#ud3{xltzM#Q(}FEc4K-Z;NOAsZ~6jDG>6nc)#7hAVSxB=IX0)==T{$4RdG7V znyKU($*$0gd`KbWmO|`g7$Q*YA9oa3aS*dsnZ7IfTg<6IuIvsRX{zqWpSgm!MR}&R z5e|DMO_3l;d#W-=2TdKFqI347yZ z4)>HJS1>;U3iZ~!-np>OQG|7?AVqMMuTJV2aL1t$u=K7-;G@WY&b51{Dg;*%4u|N> zyqpMDabTB|-R%H0a4pnIakMzo&GSwnsC${Ej(UiOZFWD0`#S~+92NK{bPHJq=ENoz zx7%v|F9^1RiM#0dM-vpVId?Y+K6DJbtlUZ(a{^w>V=_Le>O?U5!kmPacx1xWAVNT!iL+o0QTaxJ>UrkGCyeU<&_ zuc@@z&9se`LodyB*bVXqj$jaMj7hBUc+n9F zL7;DE^KHN!HUu-BjJPxth)(JZK(dB_&b%eNBOx8Tex;!KG7|)sH5$`+y;z*~jyDgk zOd^RK$@in=XMZRQ0i9B-jourPXBa*+I?&t?I+dqDa{o1xr8~CQ>73luK@kM8uyZnB z>O)<=aWJ(}m$nIqW5r=4jeDmGUo<9R4GN7eUX-4b1EsE68)Owl zPM|2!Mu0G)j3QkF6`pYOqB`4<$PX5yq8JjUv^5M+`dyY|oV3RB;4(Q(-oG3zLmWXt z>4SAlRZe#=e3HY~LB#>#U^kj2OnXX}z`U1g-lWsn7RUQ5&~n&eJ{&GRD=9P>P3EP!OSWGDuzOz6IP99!LA0094Y9XUm3Wi*3gRAhG7lz!kWP_9cOnc3B(B z(_G);`4FoZ#4-k?r)?_$1|spZ+NLIMNRwUOEOG$j4Qyk|3f=An8nKK9nnUbnj!$78 zo6%`ExqM_nH>`umBBHj@)#1moDJ>cWU572= z_*dl13Ix=(wn@zwLQcy7t8+8!TaCpW`w$$kh=LnIQ`!;=!u%9w@y845*&88lw0P8Z zi^oZ!it^z=W><_|kRY;128chf+KLc;vLK-Y)Ly(WYdVF-=Yinku;4xnN0^d2)nuo- z2bppJi2K4;a5Rtc59GREPrel?-5G|+_zj5v6w)ars_?5Qa~t zZxZ;y(ns5Zz^{iFBpU3o zz1tlmpoGiMibnAw+_s;=8bgbQ!eLRiGQ>9sBJ3vDR!(Rr4p1h73iIBaKr=}|{w^E` z0RIoYz#Uh^zR9J#AHbQxk!=j;caRt%J)5VT2))|%?`~e3ml;aW#gIB!aJhMVNQveH z^hh$p+Jz61gZ`-=2>s!EdQ>OYeqdLr5iIRCCcBm4sl^3@c(Sc!i|*`8Sk&&h57Yq? z`=bf?&kxa0=ZeyJO}JrNB8XRdVEFK=TzWn*u|NUv={8IzAq?OS7QQ98bPND!HwJ({ z8F=E>{|x|>#Qz^eZVJXF^WRTOnua-=Muf}K<&<47xoRk18%g5s5_i@*N!PF!0<#*- zWZ!IYFaG3y^_~YPsGjfgf$Rf`OjEB*-JwE=`zkQ``S#_dK6EY@MHVo;D{!wjm~W#d zLXB?!q!yFR9vP(IHbZQ7UQVt(>iE(d(JwkfaN}+eN-k3sdw-Fl8WWti^SW>C{sZgY zx+=GxjGd#H&;8CVhVH(Qx>&S*)O3xtm{Uy8qc6c*JLJ{R%_sbO^=e{!3U|og>i9Z! zO$`{Yr7U&v@eX^njTmDMKk%-V{d>k=foAs3`87JlB`GyEx;`_* ztg_90F1T%lswD$_E!QP&6D2PvHZCbAV=t6`*f%5$zamucK5^lgsDv@wxOe$UDraC| zeL*#Bm>`-gb>Yxv?VuP;!(B}}5n$#cp*m4?aPu+SAa`}W)Wgazz>nVkZvQ+%D}Tz_ z601MAufsav^PQQ^@)P^VUZ0t;!ksYCdEP_RE%S2(-(^Te_)a(_E)O1k;{$6*`g$K8vcj3~u8qh@on2KE z^a$@y!X)M4nK-|ZPx#jbAKtI}bb07C88}8Y&q%H$V5eI#YVSCDM6*EU%6mDrW?$$)@_&Z}T$0ykWl`EpF0;5hNS-*8fn$y0+A;&mvRez4Q9z>W0)T zhc7XM24@i!5jEMJpRXTL&9P1Sf*;oV>KA_MNZABtS{79JX=jIJksFmtNy9Z0eFMWh z@Nfl>XJ=|;Ijq}+5Z#3YNXrmWegZ%zG+rN~XD+{VJLNUTKxeASAt3AQD!rM>KRfM3 ze!H?>8(_2Y=O$`K2%fSh;yvBZl&h~Nulo-B(B7f|^UYVYk8j4N3^x5}-@p&lHgElh z9Fuo`t8bv;L_a1#gW$v7J!*cX6kD!wXC%l8BNgh}k-8W%wC{fnVF0PWGTA+Jtf<5k znz#m!Y0QUSVw*pDrG7lE6sEh227fXZI5BlE1 zaaSZ7xx;kLd$%@@>XlgnQ*Fb{Hl^mD^Pg*{_aRMoBJIzkWtDpRJ$ms!&+s}Y<>H@| z5~1?z|A%PZ40r5pSF8}tz6o!4ThWCdK-yI@d$SMOi4!OI1!?8M!C-+a&X@TZK> zg6*)99Ze_$C ztF@kCbUxAtW+{FCz@gQ7{ktcHX;C&2kKIl|{+rW*4lafSf35JDcad-LM&VasAKZ0* zp@;ijfKEszEQq`0i_v%421^0Fi4OI2o{geh>1j!U_s`EqyS4K2x8BX_uE{4VQKXZ* zM>aA?DOhp@Gda4Wp;d3@+!t4uRJ~NL@+tW8pw%_mk3xyeDSY`s{OpyKqhfhh-4BsI zp`KbAj#XZfp;po>oVOwP%ZU-?$KThgzIR>Etzx4zVNH? z_1ombd!6<@6$Zu`A2i(6un5qjKGo26@mJ0KFG1o@F3uNyvI2AwhQ%&4jtnh~EyeLX5xms2#%dXQ(h;^Rw=|3k4T7_ZlBh~A# zLk*j!DV3`4E6LBNN;m1z+jT=24mGGg8om(!AIsy$EAInexlF9-TQM~g>0-k?J5?s3 z)74+cj;?Mm{Ie)gto#&`-&}m9{%@H%NOz8`x^yd4{%28|Zuc&&Jfi<@FyklO?I#6Y zd~f|1xDV1ra0OoZi$9}F>qvQVBH@$xxYoby^fQqpsAm@e4xXCW z9?P{Xe{Xrwfhal%`#vSNwsWJd{z^;n*_Ay{kD#H?@U3d00-QaAv8?5Xtff=&ATt2~ zO}+C4ii?v)vK%*hvyKc%kpr~9Nf95VrcQJZWOd5k*FXS~hZz|4F?Bfgn;Q3zi%?bjdtf;kb-Qo1&k@qw8 z&~RR-{Wy9ntw_k4Z`^;Yu}UTXe{YNQGsscf4#3*15r)x8Ln@jb1~w(f1QCuG>bL&MTE@=|0k4+9d}n)U~sz1b%qhqa`pXnSTH$ zT?Ei6Xp29DqDB7Ors>?DGPszJr0M^=b_m2Q&|-c8a|Zy;l5HP7zVE+n?3@(H5j^5Y z0hI3YcYotOo`hu~Ky2%7pm}59QF3AcD38TOtqRQj+Xpg}L>B&OT2EXS2E?QOHtn+H z9dU|Y{zGCVF2h%@ULk=!82xnXy9%5$ca2Gq!F8>I>l(#|jhBP=T$+#EOmXzbui&TD zj1UmN*!m}Y& Date: Sat, 21 Sep 2024 12:14:26 +1000 Subject: [PATCH 098/188] Removed extra logging from Ping and LAN discovery --- Multiplayer/Components/MainMenu/ServerBrowserPane.cs | 6 +++--- .../Networking/Managers/Client/ServerBrowserClient.cs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs index fb101b2..ba037c2 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs @@ -1001,16 +1001,16 @@ private void RefreshGridView() // Get all active IDs List activeIDs = allServers.Select(s => s.id).Distinct().ToList(); - Multiplayer.Log($"RefreshGridView() Active servers: {activeIDs.Count}\r\n{string.Join("\r\n", activeIDs)}"); + //Multiplayer.Log($"RefreshGridView() Active servers: {activeIDs.Count}\r\n{string.Join("\r\n", activeIDs)}"); // Find servers to remove List removeList = gridViewModel.Where(gv => !activeIDs.Contains(gv.id)).ToList(); - Multiplayer.Log($"RefreshGridView() Remove List: {removeList.Count}\r\n{string.Join("\r\n", removeList.Select(l => l.id))}"); + //Multiplayer.Log($"RefreshGridView() Remove List: {removeList.Count}\r\n{string.Join("\r\n", removeList.Select(l => l.id))}"); // Remove expired servers foreach (var remove in removeList) { - Multiplayer.Log($"RefreshGridView() Removing: {remove.id}"); + //Multiplayer.Log($"RefreshGridView() Removing: {remove.id}"); if (serverPings.ContainsKey(remove.id)) serverPings.Remove(remove.id); gridViewModel.Remove(remove); diff --git a/Multiplayer/Networking/Managers/Client/ServerBrowserClient.cs b/Multiplayer/Networking/Managers/Client/ServerBrowserClient.cs index 33e027e..d1c7f23 100644 --- a/Multiplayer/Networking/Managers/Client/ServerBrowserClient.cs +++ b/Multiplayer/Networking/Managers/Client/ServerBrowserClient.cs @@ -88,7 +88,7 @@ private async Task StartTimeoutTask(string serverId) if (pingInfos.TryGetValue(serverId, out PingInfo pingInfo)) { pingInfo.Stopwatch.Stop(); - LogDebug(() => $"Ping timeout for {serverId}, elapsed: {pingInfo.Stopwatch.ElapsedMilliseconds}, IPv4: ({pingInfo.IPv4Sent}, {pingInfo.IPv4Received}), IPv6: ({pingInfo.IPv6Sent}, {pingInfo.IPv6Received}) "); + //LogDebug(() => $"Ping timeout for {serverId}, elapsed: {pingInfo.Stopwatch.ElapsedMilliseconds}, IPv4: ({pingInfo.IPv4Sent}, {pingInfo.IPv4Received}), IPv6: ({pingInfo.IPv6Sent}, {pingInfo.IPv6Received}) "); if (!pingInfo.IPv4Received && pingInfo.IPv4Sent) OnPing?.Invoke(serverId, -1, true); @@ -185,7 +185,7 @@ public void SendUnconnectedPingPacket(string serverId, string ipv4, string ipv6, PingInfo pingInfo = new PingInfo(); pingInfos[serverId] = pingInfo; - LogDebug(()=>$"Sending ping to {serverId} at IPv4: {ipv4}, IPv6: {ipv6}, Port: {port}"); + //LogDebug(()=>$"Sending ping to {serverId} at IPv4: {ipv4}, IPv6: {ipv6}, Port: {port}"); var packet = new UnconnectedPingPacket { ServerID = server.ToByteArray() }; pingInfo.Start(); From d23098cef2eda5f9b28e0200a49ecb95f7fe4135 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 22 Sep 2024 10:36:44 +1000 Subject: [PATCH 099/188] Minor UI changes --- Multiplayer/Components/MainMenu/HostGamePane.cs | 7 ++++++- Multiplayer/Components/MainMenu/ServerBrowserPane.cs | 9 +++++++++ Multiplayer/Multiplayer.cs | 6 ++++++ .../Networking/Managers/Client/ServerBrowserClient.cs | 2 +- 4 files changed, 22 insertions(+), 2 deletions(-) diff --git a/Multiplayer/Components/MainMenu/HostGamePane.cs b/Multiplayer/Components/MainMenu/HostGamePane.cs index 63ab038..f2f8a05 100644 --- a/Multiplayer/Components/MainMenu/HostGamePane.cs +++ b/Multiplayer/Components/MainMenu/HostGamePane.cs @@ -32,6 +32,7 @@ public class HostGamePane : MonoBehaviour TMP_InputField password; TMP_InputField port; TMP_InputField details; + TextMeshProUGUI serverDetails; Slider maxPlayers; @@ -148,7 +149,11 @@ private void BuildUI() GameObject serverWindowGO = this.FindChildByName("Save Description"); GameObject serverDetailsGO = serverWindowGO.FindChildByName("text list [noloc]"); serverWindowGO.name = "Host Details"; - serverDetailsGO.GetComponent().text = ""; + serverDetails = serverDetailsGO.GetComponent(); + serverDetails.textWrappingMode = TextWrappingModes.Normal; + serverDetails.text = "Please note: Use of other mods is currently not supported and may cause unexpected behaviour.
" + + "It is recommended that other mods are disabled and the game restarted prior to playing in multiplayer.

" + + "We hope to make your favourite mods work with multiplayer in the future."; //Find scrolling viewport diff --git a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs index ba037c2..2dbbf28 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs @@ -142,6 +142,8 @@ private void Awake() SetupServerBrowser(); RefreshGridView(); + + buttonRefresh.ToggleInteractable(true); RefreshAction(); } @@ -936,6 +938,13 @@ private void OnDisconnect(DisconnectReason reason, string message) message = "Server Shutting Down"; //TODO: add translations } break; + + case DisconnectReason.Timeout: + if (message == null || message.Length == 0) + { + message = "Server Timed out"; //TODO: add translations + } + break; } //Multiplayer.LogError($"OnDisconnect() Calling AF"); diff --git a/Multiplayer/Multiplayer.cs b/Multiplayer/Multiplayer.cs index 04343e0..bead7dd 100644 --- a/Multiplayer/Multiplayer.cs +++ b/Multiplayer/Multiplayer.cs @@ -72,6 +72,12 @@ private static bool Load(UnityModManager.ModEntry modEntry) RemoteDispatchPatch.Patch(harmony, remoteDispatch.Assembly); } + //if (passengerJobs?.Enabled == true) + //{ + // Log("Found PassengerJobs, initialising..."); + // PassengerJobsMod.Init(); + //} + if (!LoadAssets()) return false; diff --git a/Multiplayer/Networking/Managers/Client/ServerBrowserClient.cs b/Multiplayer/Networking/Managers/Client/ServerBrowserClient.cs index d1c7f23..b9c5d69 100644 --- a/Multiplayer/Networking/Managers/Client/ServerBrowserClient.cs +++ b/Multiplayer/Networking/Managers/Client/ServerBrowserClient.cs @@ -137,7 +137,7 @@ private void OnUnconnectedPingPacket(UnconnectedPingPacket packet, IPEndPoint en if (pingInfos.TryGetValue(serverId, out PingInfo pingInfo)) { - int pingTime = (int)pingInfo.Stopwatch.ElapsedMilliseconds; + int pingTime = (int)pingInfo.Stopwatch.ElapsedMilliseconds / 2; //game reports one-way ping, so we should do the same in the server browser bool isIPv4 = endPoint.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork; From 15361786dcaa0087846de63f6832a35b2d4354f3 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 22 Sep 2024 10:38:08 +1000 Subject: [PATCH 100/188] Update HostGamePane.cs --- Multiplayer/Components/MainMenu/HostGamePane.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Multiplayer/Components/MainMenu/HostGamePane.cs b/Multiplayer/Components/MainMenu/HostGamePane.cs index f2f8a05..5fff99d 100644 --- a/Multiplayer/Components/MainMenu/HostGamePane.cs +++ b/Multiplayer/Components/MainMenu/HostGamePane.cs @@ -151,8 +151,8 @@ private void BuildUI() serverWindowGO.name = "Host Details"; serverDetails = serverDetailsGO.GetComponent(); serverDetails.textWrappingMode = TextWrappingModes.Normal; - serverDetails.text = "Please note: Use of other mods is currently not supported and may cause unexpected behaviour.
" + - "It is recommended that other mods are disabled and the game restarted prior to playing in multiplayer.

" + + serverDetails.text = "Please note:
Use of other mods is currently not supported and may cause unexpected behaviour.

" + + "It is recommended that other mods are disabled and Derail Valley restarted prior to playing in multiplayer.

" + "We hope to make your favourite mods work with multiplayer in the future."; From 2cef3efc5441004a5bf4f2669731a915fb05ba28 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 22 Sep 2024 21:32:29 +1000 Subject: [PATCH 101/188] Continuing job and item sync --- .../Networking/Jobs/NetworkedJob.cs | 13 +- .../Networking/World/NetworkedItem.cs | 38 +-- .../World/NetworkedStationController.cs | 224 +++++++++++++++--- .../Networking/Data/ItemPositionData.cs | 38 +++ Multiplayer/Networking/Data/JobData.cs | 191 +++++++++------ Multiplayer/Networking/Data/JobUpdateData.cs | 48 ++++ .../Networking/Data/JobValidationData.cs | 8 + .../Networking/Data/TaskNetworkData.cs | 6 + .../Managers/Client/NetworkClient.cs | 40 ++-- .../Networking/Managers/NetworkManager.cs | 3 +- .../Managers/Server/NetworkServer.cs | 84 ++----- .../ClientboundJobValidateResponsePacket.cs | 3 +- .../Jobs/ClientboundJobsCreatePacket.cs | 9 +- .../Jobs/ClientboundJobsUpdatePacket.cs | 87 +++---- .../ServerboundJobValidateRequestPacket.cs | 6 +- .../Patches/Jobs/BookletCreatorJobPatch.cs | 44 ++++ Multiplayer/Patches/Jobs/JobBookletPatch.cs | 26 +- Multiplayer/Patches/Jobs/JobValidatorPatch.cs | 30 ++- .../Patches/Jobs/StationControllerPatch.cs | 18 +- Multiplayer/Utils/PacketCompression.cs | 30 +++ 20 files changed, 629 insertions(+), 317 deletions(-) create mode 100644 Multiplayer/Networking/Data/ItemPositionData.cs create mode 100644 Multiplayer/Networking/Data/JobUpdateData.cs create mode 100644 Multiplayer/Networking/Data/JobValidationData.cs create mode 100644 Multiplayer/Patches/Jobs/BookletCreatorJobPatch.cs create mode 100644 Multiplayer/Utils/PacketCompression.cs diff --git a/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs b/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs index 02494e9..24a0af9 100644 --- a/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs +++ b/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs @@ -4,7 +4,7 @@ using System.Linq; using DV.Logic.Job; using Multiplayer.Components.Networking.World; -using Multiplayer.Networking.Packets.Clientbound.Jobs; +using Multiplayer.Networking.Data; using UnityEngine; @@ -51,9 +51,13 @@ public static bool TryGetFromJobId(string jobId, out NetworkedJob networkedJob) #endregion protected override bool IsIdServerAuthoritative => true; + public Action OverviewGenerated; + public Job Job; public JobOverview JobOverview; public JobBooklet JobBooklet; + public JobReport JobReport; + public JobExpiredReport JobExpiredReport; public NetworkedStationController Station; public Guid OwnedBy = Guid.Empty; //GUID of player who took the job (sever only) @@ -103,6 +107,13 @@ private void OnDisable() #region Server + public void JobOverviewGenerated(JobOverview jobOverview) + { + JobOverview = jobOverview; + + OverviewGenerated?.Invoke(this); + } + private void Server_OnTick(uint tick) { if (UnloadWatcher.isUnloading) diff --git a/Multiplayer/Components/Networking/World/NetworkedItem.cs b/Multiplayer/Components/Networking/World/NetworkedItem.cs index 62f507e..9d27f42 100644 --- a/Multiplayer/Components/Networking/World/NetworkedItem.cs +++ b/Multiplayer/Components/Networking/World/NetworkedItem.cs @@ -71,7 +71,7 @@ private void SetupItem() { //Job related items case "JobOverview": - SetupJobOverview(); + //SetupJobOverview(); break; case "JobBooklet": @@ -79,11 +79,11 @@ private void SetupItem() break; case "JobMissingLicenseReport": - SetupJobMissingLicenseReport(); + //SetupJobMissingLicenseReport(); break; case "JobDebtWarningReport": - SetupJobDebtWarningReport(); + //SetupJobDebtWarningReport(); break; //Loco related items @@ -122,24 +122,24 @@ private void OnItemInventoryStateChanged(ItemBase itemBase, InventoryActionType Multiplayer.LogDebug(() => $"OnItemInventoryStateChanged() {name}, InventoryActionType: {actionType}, InventoryItemState: {itemState}"); } - private void SetupJobOverview() - { - if(!TryGetComponent(out JobOverview jobOverview)) - { - Multiplayer.LogError($"SetupJobOverview() Could not find JobOverview"); - return; - } + //private void SetupJobOverview() + //{ + // if(!TryGetComponent(out JobOverview jobOverview)) + // { + // Multiplayer.LogError($"SetupJobOverview() Could not find JobOverview"); + // return; + // } - if (!NetworkedJob.TryGetFromJob(jobOverview.job, out NetworkedJob networkedJob)) - { - Multiplayer.LogError($"SetupJobOverview() NetworkedJob not found for Job ID: {jobOverview?.job?.ID}"); - jobOverview.DestroyJobOverview(); - return; - } + // if (!NetworkedJob.TryGetFromJob(jobOverview.job, out NetworkedJob networkedJob)) + // { + // Multiplayer.LogError($"SetupJobOverview() NetworkedJob not found for Job ID: {jobOverview?.job?.ID}"); + // jobOverview.DestroyJobOverview(); + // return; + // } - networkedJob.JobOverview = jobOverview; - networkedJob.ValidationItem = this; - } + // networkedJob.JobOverview = jobOverview; + // networkedJob.ValidationItem = this; + //} //private IEnumerator SetupJobBooklet() //{ diff --git a/Multiplayer/Components/Networking/World/NetworkedStationController.cs b/Multiplayer/Components/Networking/World/NetworkedStationController.cs index f6be4e5..caf4d34 100644 --- a/Multiplayer/Components/Networking/World/NetworkedStationController.cs +++ b/Multiplayer/Components/Networking/World/NetworkedStationController.cs @@ -2,11 +2,18 @@ using System.Collections; using System.Collections.Generic; using System.Linq; +using DV.Booklets; using DV.Logic.Job; +using DV.ServicePenalty; +using DV.Utils; using Multiplayer.Components.Networking.Jobs; using Multiplayer.Components.Networking.Train; using Multiplayer.Networking.Data; +using Multiplayer.Utils; +using Unity.Jobs; using UnityEngine; +using UnityEngine.Assertions.Must; + namespace Multiplayer.Components.Networking.World; @@ -66,6 +73,12 @@ public static bool GetFromStationController(StationController stationController, public static bool GetFromJobValidator(JobValidator jobValidator, out NetworkedStationController networkedStationController) { + if (jobValidator == null) + { + networkedStationController = null; + return false; + } + return jobValidatorToNetworkedStation.TryGetValue(jobValidator, out networkedStationController); } @@ -97,7 +110,7 @@ private static void RegisterJobValidator(JobValidator jobValidator, NetworkedSta protected override bool IsIdServerAuthoritative => true; - private StationController StationController; + public StationController StationController; public JobValidator JobValidator; @@ -105,6 +118,12 @@ private static void RegisterJobValidator(JobValidator jobValidator, NetworkedSta private List NewJobs = new List(); private List DirtyJobs = new List(); + private List availableJobs; + private List takenJobs; + private List abandonedJobs; + private List completedJobs; + + protected override void Awake() { base.Awake(); @@ -125,7 +144,13 @@ private IEnumerator WaitForLogicStation() while (StationController.logicStation == null) yield return null; - NetworkedStationController.RegisterStationController(this, StationController); + RegisterStationController(this, StationController); + + availableJobs = StationController.logicStation.availableJobs; + takenJobs = StationController.logicStation.takenJobs; + abandonedJobs = StationController.logicStation.abandonedJobs; + completedJobs = StationController.logicStation.completedJobs; + Multiplayer.Log($"NetworkedStation.Awake({StationController.logicStation.ID})"); foreach (JobValidator validator in jobValidators) @@ -156,26 +181,43 @@ public void AddJob(Job job) job.JobAbandoned += OnJobAbandoned; job.JobCompleted += OnJobCompleted; job.JobExpired += OnJobExpired; + + networkedJob.OverviewGenerated += OnOverviewGeneration; + } + + public void OnOverviewGeneration(NetworkedJob job) + { + if(!DirtyJobs.Contains(job)) + DirtyJobs.Add(job); + } private void OnJobTaken(Job job, bool viaLoadGame) { + if (viaLoadGame) + return; + Multiplayer.Log($"NetworkedStationController.OnJobTaken({job.ID})"); + if(NetworkedJob.TryGetFromJob(job, out NetworkedJob networkedJob)) + DirtyJobs.Add(networkedJob); } private void OnJobAbandoned(Job job) { - + if (NetworkedJob.TryGetFromJob(job, out NetworkedJob networkedJob)) + DirtyJobs.Add(networkedJob); } private void OnJobCompleted(Job job) { - + if (NetworkedJob.TryGetFromJob(job, out NetworkedJob networkedJob)) + DirtyJobs.Add(networkedJob); } private void OnJobExpired(Job job) { - + if (NetworkedJob.TryGetFromJob(job, out NetworkedJob networkedJob)) + DirtyJobs.Add(networkedJob); } private void Server_OnTick(uint tick) @@ -183,7 +225,7 @@ private void Server_OnTick(uint tick) //Send new jobs if (NewJobs.Count > 0) { - NetworkLifecycle.Instance.Server.SendJobsCreatePacket(NetId, NewJobs.ToArray()); + NetworkLifecycle.Instance.Server.SendJobsCreatePacket(this, NewJobs.ToArray()); NewJobs.Clear(); } @@ -191,6 +233,8 @@ private void Server_OnTick(uint tick) if (DirtyJobs.Count > 0) { //todo send packet with updates + NetworkLifecycle.Instance.Server.SendJobsUpdatePacket(NetId, DirtyJobs.ToArray()); + DirtyJobs.Clear(); } } @@ -198,14 +242,12 @@ private void Server_OnTick(uint tick) #region Client public void AddJobs(JobData[] jobs) { - //NetworkLifecycle.Instance.Client.Log($"AddJobs() jobs[] exists: {jobs != null}, job count: {jobs?.Count()}"); + NetworkLifecycle.Instance.Client.Log($"AddJobs() jobs[] exists: {jobs != null}, job count: {jobs?.Count()}"); - //NetworkLifecycle.Instance.Client.Log($"AddJobs() preloop"); foreach (JobData job in jobs) { - //NetworkLifecycle.Instance.Client.Log($"AddJobs() inloop"); - - //NetworkLifecycle.Instance.Client.Log($"AddJobs() ID: {jobData?.ID ?? ""}, netID: {jobData?.NetID}, task count: {jobData?.Tasks?.Count()}"); + NetworkLifecycle.Instance.Client.Log($"AddJobs() inloop"); + NetworkLifecycle.Instance.Client.Log($"AddJobs() ID: {job?.ID ?? ""}, netID: {job?.NetID}, task count: {job?.Tasks?.Count()}"); // Convert TaskNetworkData to Task objects List tasks = new List(); @@ -217,11 +259,11 @@ public void AddJobs(JobData[] jobs) continue; } - //NetworkLifecycle.Instance.Client.Log($"AddJobs() ID: {jobData?.ID}, task type: {taskData.TaskType}"); + NetworkLifecycle.Instance.Client.Log($"AddJobs() ID: {job?.ID}, task type: {taskData.TaskType}"); tasks.Add(taskData.ToTask()); } - //NetworkLifecycle.Instance.Client.Log($"AddJobs() ID: {jobData?.ID}, netID: {jobData?.NetID}, StationsChainData"); + NetworkLifecycle.Instance.Client.Log($"AddJobs() ID: {job?.ID}, netID: {job?.NetID}, StationsChainData"); // Create StationsChainData from ChainData StationsChainData chainData = new StationsChainData( job.ChainData.ChainOriginYardId, @@ -229,7 +271,7 @@ public void AddJobs(JobData[] jobs) ); - //NetworkLifecycle.Instance.Client.Log($"AddJobs() ID: {jobData?.ID}, netID: {jobData?.NetID}, newJob"); + NetworkLifecycle.Instance.Client.Log($"AddJobs() ID: {job?.ID}, netID: {job?.NetID}, newJob"); // Create a new local Job Job newJob = new Job( tasks, @@ -241,35 +283,40 @@ public void AddJobs(JobData[] jobs) job.RequiredLicenses ); - //NetworkLifecycle.Instance.Client.Log($"AddJobs() ID: {jobData?.ID}, netID: {jobData?.NetID}, properties"); + NetworkLifecycle.Instance.Client.Log($"AddJobs() ID: {job?.ID}, netID: {job?.NetID}, properties"); // Set additional properties newJob.startTime = job.StartTime; newJob.finishTime = job.FinishTime; newJob.State = job.State; - //NetworkLifecycle.Instance.Client.Log($"AddJobs() ID: {jobData?.ID}, netID: {jobData?.NetID}, netjob"); + NetworkLifecycle.Instance.Client.Log($"AddJobs() ID: {job?.ID}, netID: {job?.NetID}, netjob"); // Create a new NetworkedJob NetworkedJob networkedJob = new GameObject($"NetworkedJob {newJob.ID}").AddComponent(); networkedJob.NetId = job.NetID; networkedJob.Job = newJob; networkedJob.Station = this; - networkedJob.playerID = job.PlayerId; + //networkedJob.playerID = job.PlayerId; - //NetworkLifecycle.Instance.Client.Log($"AddJobs() ID: {jobData?.ID}, netID: {jobData?.NetID}, NetJob Add"); + NetworkLifecycle.Instance.Client.Log($"AddJobs() ID: {job?.ID}, netID: {job?.NetID}, NetJob Add"); NetworkedJobs.Add(networkedJob); - //NetworkLifecycle.Instance.Client.Log($"AddJobs() ID: {jobData?.ID}, netID: {jobData?.NetID}, CarPlates"); + NetworkLifecycle.Instance.Client.Log($"AddJobs() ID: {job?.ID}, netID: {job?.NetID}, CarPlates"); // Start coroutine to update car plates StartCoroutine(UpdateCarPlates(tasks, newJob.ID)); //If the job is not owned by anyone, we can add it to the station //if(networkedJob.OwnedBy == Guid.Empty) + NetworkLifecycle.Instance.Client.Log($"AddJobs() ID: {job?.ID}, netID: {job?.NetID}, AddJobToStation()"); StationController.logicStation.AddJobToStation(newJob); - + StationController.processedNewJobs.Add(newJob); + + NetworkLifecycle.Instance.Client.Log($"AddJobs() ID: {job?.ID}, netID: {job?.NetID}, job state {job.State}, itemNetId {job.ItemNetID}"); //start coroutine for generating overviews and booklets - //StartCoroutine(CreatePaperWork()); + NetworkLifecycle.Instance.Client.Log($"AddJobs() {newJob?.ID} Generating Overview {(newJob.State == DV.ThingTypes.JobState.Available && job.ItemNetID != 0)}"); + if (newJob.State == DV.ThingTypes.JobState.Available && job.ItemNetID != 0) + GenerateOverview(networkedJob, job.ItemNetID, job.ItemPosition); // Log the addition of the new job NetworkLifecycle.Instance.Client.Log($"AddJobs() {newJob?.ID} to NetworkedStationController {StationController?.logicStation?.ID}"); @@ -279,9 +326,125 @@ public void AddJobs(JobData[] jobs) StationController.attemptJobOverviewGeneration = true; } - public void UpdateJob() + public void UpdateJobs(JobUpdateStruct[] jobs) + { + foreach (JobUpdateStruct job in jobs) + { + if (!NetworkedJob.Get(job.JobNetID, out NetworkedJob netJob)) + continue; + + JobValidator validator = null; + if(job.ItemNetID != 0 && job.ValidationStationId != 0) + if (Get(job.ValidationStationId, out var netStation)) + validator = netStation.JobValidator; + + Multiplayer.Log($"NetworkedStation.UpdateJobs() jobNetId: {job.JobNetID}, Validator found: {validator != null}"); + + //state change updates + if (netJob.Job.State != job.JobState) + { + netJob.Job.State = job.JobState; + bool printed = false; + + switch (netJob.Job.State) + { + case DV.ThingTypes.JobState.InProgress: + availableJobs.Remove(netJob.Job); + takenJobs.Add(netJob.Job); + + netJob.JobBooklet = BookletCreator.CreateJobBooklet(netJob.Job, validator.bookletPrinter.spawnAnchor.position, validator.bookletPrinter.spawnAnchor.rotation, WorldMover.OriginShiftParent, true); + + netJob.ValidationItem.NetId = job.ItemNetID; + printed = true; + netJob.JobOverview.DestroyJobOverview(); + break; + case DV.ThingTypes.JobState.Completed: + takenJobs.Remove(netJob.Job); + completedJobs.Add(netJob.Job); + + DisplayableDebt displayableDebt = SingletonBehaviour.Instance.LastStagedJobDebt; + netJob.JobReport = BookletCreator.CreateJobReport(netJob.Job, displayableDebt, validator.bookletPrinter.spawnAnchor.position, validator.bookletPrinter.spawnAnchor.rotation, WorldMover.OriginShiftParent); + + netJob.ValidationItem.NetId = job.ItemNetID; + printed = true; + netJob.JobBooklet.DestroyJobBooklet(); + break; + case DV.ThingTypes.JobState.Abandoned: + takenJobs.Remove(netJob.Job); + abandonedJobs.Add(netJob.Job); + + //netJob.JobExpiredReport = BookletCreator.CreateJobExpiredReport(netJob.Job, validator.bookletPrinter.spawnAnchor.position, validator.bookletPrinter.spawnAnchor.rotation, WorldMover.OriginShiftParent); + //netJob.ValidationItem.NetId = job.ItemNetID; + //printed = true; + + break; + case DV.ThingTypes.JobState.Expired: + if(availableJobs.Contains(netJob.Job)) + availableJobs.Remove(netJob.Job); + + //netJob.JobExpiredReport = BookletCreator.CreateJobExpiredReport(netJob.Job, validator.bookletPrinter.spawnAnchor.position, validator.bookletPrinter.spawnAnchor.rotation, WorldMover.OriginShiftParent); + //netJob.ValidationItem.NetId = job.ItemNetID; + //printed = true; + + break; + default: + NetworkLifecycle.Instance.Client.LogError($"NetworkedStation.UpdateJobs() Unrecognised Job State for JobId: {job.JobNetID}, {netJob.Job.ID}"); + break; + } + + if (printed) + { + Multiplayer.Log($"NetworkedStation.UpdateJobs() jobNetId: {job.JobNetID}, Playing sounds"); + netJob.ValidatorResponseReceived = true; + netJob.ValidationAccepted = true; + validator.jobValidatedSound.Play(validator.bookletPrinter.spawnAnchor.position, 1f, 1f, 0f, 1f, 500f, default(AudioSourceCurves), null, validator.transform, false, 0f, null); + validator.bookletPrinter.Print(false); + } + } + + //job overview generation update + if(job.JobState == DV.ThingTypes.JobState.Available && job.ItemNetID !=0) + { + + if (netJob.JobOverview == null) + { + //create overview + Multiplayer.LogDebug(()=>$"NetworkedStation.UpdateJobs() Creating JobOverview"); + if (job.JobState == DV.ThingTypes.JobState.Available && job.ItemNetID != 0) + GenerateOverview(netJob, job.ItemNetID, job.ItemPositionData); + } + else + { + Multiplayer.LogDebug(() => $"NetworkedStation.UpdateJobs() Setting JobOverview"); + netJob.ValidationItem.NetId = job.ItemNetID; + } + } + + //generic update + netJob.Job.startTime = job.StartTime; + netJob.Job.finishTime = job.FinishTime; + } + } + + public void RemoveJob(NetworkedJob job) { + if (availableJobs.Contains(job.Job)) + availableJobs.Remove(job.Job); + + if (takenJobs.Contains(job.Job)) + takenJobs.Remove(job.Job); + + if (completedJobs.Contains(job.Job)) + completedJobs.Remove(job.Job); + if (abandonedJobs.Contains(job.Job)) + abandonedJobs.Remove(job.Job); + + job.JobOverview?.DestroyJobOverview(); + job.JobBooklet?.DestroyJobBooklet(); + + NetworkedJobs.Remove(job); + GameObject.Destroy(job); } public static IEnumerator UpdateCarPlates(List tasks, string jobId) @@ -344,20 +507,12 @@ private static void UpdateCarPlatesRecursive(List tasks, stri //Multiplayer.Log($"NetworkedStation.UpdateCarPlatesRecursive() SequentialTask Adding node"); seqTask.Add(node.Value); } - - //Multiplayer.Log($"NetworkedStation.UpdateCarPlatesRecursive() SequentialTask Node Count:{seqTask.Count}"); - - //Multiplayer.Log("NetworkedStation.UpdateCarPlatesRecursive() Calling UpdateCarPlates()"); //drill down UpdateCarPlatesRecursive(seqTask, jobId, ref cars); //Multiplayer.Log($"NetworkedStation.UpdateCarPlatesRecursive() SequentialTask RETURNED"); } else if (task is ParallelTasks) { - //not implemented - //Multiplayer.Log("NetworkedStation.UpdateCarPlatesRecursive() ParallelTasks"); - - //Multiplayer.Log("NetworkedStation.UpdateCarPlatesRecursive() Calling UpdateCarPlates()"); //drill down UpdateCarPlatesRecursive(((ParallelTasks)task).tasks, jobId, ref cars); } @@ -370,6 +525,15 @@ private static void UpdateCarPlatesRecursive(List tasks, stri //Multiplayer.Log("NetworkedStation.UpdateCarPlatesRecursive() Returning"); } + private void GenerateOverview(NetworkedJob networkedJob, ushort itemNetId, ItemPositionData posData) + { + networkedJob.JobOverview = BookletCreator_JobOverview.Create(networkedJob.Job, posData.Position + WorldMover.currentMove, posData.Rotation); + NetworkedItem netItem = networkedJob.JobOverview.GetOrAddComponent(); + netItem.NetId = itemNetId; + networkedJob.ValidationItem = netItem; + StationController.spawnedJobOverviews.Add(networkedJob.JobOverview); + } + private void OnDisable() { diff --git a/Multiplayer/Networking/Data/ItemPositionData.cs b/Multiplayer/Networking/Data/ItemPositionData.cs new file mode 100644 index 0000000..9006c33 --- /dev/null +++ b/Multiplayer/Networking/Data/ItemPositionData.cs @@ -0,0 +1,38 @@ +using LiteNetLib.Utils; +using Multiplayer.Components.Networking.World; +using Multiplayer.Networking.Serialization; +using UnityEngine; + +namespace Multiplayer.Networking.Data; + +public struct ItemPositionData +{ + public Vector3 Position; + public Quaternion Rotation; + //public bool held; + + public static ItemPositionData FromItem(NetworkedItem item) + { + return new ItemPositionData + { + Position = item.Item.transform.position - WorldMover.currentMove, + Rotation = item.Item.transform.rotation, + //held = item.Item. //Todo: track if item is held by a player + }; + } + + public static void Serialize(NetDataWriter writer, ItemPositionData data) + { + Vector3Serializer.Serialize(writer, data.Position); + QuaternionSerializer.Serialize(writer, data.Rotation); + } + + public static ItemPositionData Deserialize(NetDataReader reader) + { + return new ItemPositionData + { + Position = Vector3Serializer.Deserialize(reader), + Rotation = QuaternionSerializer.Deserialize(reader), + }; + } +} diff --git a/Multiplayer/Networking/Data/JobData.cs b/Multiplayer/Networking/Data/JobData.cs index ac5e5af..6b178b2 100644 --- a/Multiplayer/Networking/Data/JobData.cs +++ b/Multiplayer/Networking/Data/JobData.cs @@ -3,7 +3,10 @@ using LiteNetLib.Utils; using Multiplayer.Components.Networking; using Multiplayer.Components.Networking.Jobs; +using Multiplayer.Components.Networking.World; using System; +using System.IO; +using System.Linq; namespace Multiplayer.Networking.Data; @@ -20,11 +23,32 @@ public class JobData public float InitialWage { get; set; } public JobState State { get; set; } //serialise as byte public float TimeLimit { get; set; } - public int PlayerId { get; set; } + public ushort ItemNetID { get; set; } + public ItemPositionData ItemPosition { get; set; } - public static JobData FromJob(NetworkedJob networkedJob) + public static JobData FromJob(NetworkedStationController netStation, NetworkedJob networkedJob) { Job job = networkedJob.Job; + ushort itemNetId = 0; + ItemPositionData itemPos = new ItemPositionData(); + + if (networkedJob.Job.State == JobState.Available) + { + JobOverview jobOverview = netStation.StationController.spawnedJobOverviews.Where(jo => jo.job == job).FirstOrDefault(); + if (jobOverview != default(JobOverview)) + { + NetworkedItem netItem = jobOverview.GetComponent(); + if (netItem != null) + { + itemNetId = netItem.NetId; + itemPos = ItemPositionData.FromItem(netItem); + } + } + }else if(job.State == JobState.InProgress || job.State == JobState.Completed) + { + itemNetId = networkedJob.ValidationItem.NetId; + itemPos = ItemPositionData.FromItem(networkedJob.ValidationItem); + } return new JobData { @@ -39,112 +63,121 @@ public static JobData FromJob(NetworkedJob networkedJob) InitialWage = job.initialWage, State = job.State, TimeLimit = job.TimeLimit, - PlayerId = networkedJob.playerID + ItemNetID = itemNetId, + ItemPosition = itemPos, }; } public static void Serialize(NetDataWriter writer, JobData data) { NetworkLifecycle.Instance.Server.Log($"JobData.Serialize({data.ID}) NetID {data.NetID}"); + writer.Put(data.NetID); - //Multiplayer.Log($"JobData.Serialize({data.ID}) JobType {(byte)data.JobType}, {data.JobType}"); writer.Put((byte)data.JobType); - //Multiplayer.Log($"JobData.Serialize({data.ID}) JobID {data.ID}"); writer.Put(data.ID); - //Multiplayer.Log($"JobData.Serialize({data.ID}) task length {data.Tasks.Length}"); - //task data - writer.Put((byte)data.Tasks.Length); - foreach (var task in data.Tasks) + //task data - add compression + using (MemoryStream ms = new MemoryStream()) + using (BinaryWriter bw = new BinaryWriter(ms)) { - //Multiplayer.Log($"JobData.Serialize({data.ID}) TaskType {(byte)task.TaskType}, {task.TaskType}"); + bw.Write((byte)data.Tasks.Length); + foreach (var task in data.Tasks) + { + NetDataWriter taskSerialiser = new NetDataWriter(); + + bw.Write((byte)task.TaskType); + task.Serialize(taskSerialiser); + + bw.Write(taskSerialiser.Data.Length); + bw.Write(taskSerialiser.Data); + } + + byte[] compressedData = PacketCompression.Compress(ms.ToArray()); - writer.Put((byte)task.TaskType); - task.Serialize(writer); + Multiplayer.Log($"JobData.Serialize() Uncompressed: {ms.Length} Compressed: {compressedData.Length}"); + writer.PutBytesWithLength(compressedData); } - //Multiplayer.Log($"JobData.Serialize({data.ID}) calling StationsChainDataData.Serialize()"); StationsChainNetworkData.Serialize(writer, data.ChainData); - //Multiplayer.Log($"JobData.Serialize({data.ID}) RequiredLicenses {data.RequiredLicenses}"); writer.Put((int)data.RequiredLicenses); - //Multiplayer.Log($"JobData.Serialize({data.ID}) StartTime {data.StartTime}"); writer.Put(data.StartTime); - //Multiplayer.Log($"JobData.Serialize({data.ID}) FinishTime {data.FinishTime}"); writer.Put(data.FinishTime); - //Multiplayer.Log($"JobData.Serialize({data.ID}) InitialWage {data.InitialWage}"); writer.Put(data.InitialWage); - //Multiplayer.Log($"JobData.Serialize({data.ID}) State {(byte)data.State}, {data.State}"); writer.Put((byte)data.State); - //Multiplayer.Log($"JobData.Serialize({data.ID}) TimeLimit {data.TimeLimit}"); writer.Put(data.TimeLimit); - //Multiplayer.Log(JsonConvert.SerializeObject(data, Formatting.None)); - - //Take on the GUID of the player - //if(data.State != JobState.Available) - // writer.Put(data.OwnedBy.ToByteArray()); - - writer.Put(data.PlayerId); + writer.Put(data.ItemNetID); + ItemPositionData.Serialize(writer, data.ItemPosition); } public static JobData Deserialize(NetDataReader reader) { - //Multiplayer.LogDebug(() => $"JobData.Deserialize(): [{string.Join(", ", reader.RawData?.Select(id => id.ToString()))}]"); - ushort netID = reader.GetUShort(); - //Multiplayer.Log($"JobData.Deserialize() netID {netID}"); - JobType jobType = (JobType)reader.GetByte(); - //Multiplayer.Log($"JobData.Deserialize() jobType {jobType}"); - string id = reader.GetString(); - //Multiplayer.Log($"JobData.Deserialize() id {id}"); - - byte tasksLength = reader.GetByte(); - //Multiplayer.Log($"JobData.Deserialize() tasksLength {tasksLength}"); - - TaskNetworkData[] tasks = new TaskNetworkData[tasksLength]; - for (int i = 0; i < tasksLength; i++) + try { - TaskType taskType = (TaskType)reader.GetByte(); - //Multiplayer.Log($"JobData.Deserialize() taskType {taskType}"); - tasks[i] = TaskNetworkDataFactory.ConvertTask(taskType); - //Multiplayer.Log($"JobData.Deserialize() TaskNetworkData not null: {tasks[i] != null}, {tasks[i].GetType().FullName}"); - tasks[i].Deserialize(reader); - //Multiplayer.Log($"JobData.Deserialize() TaskNetworkData Deserialised"); - } - - StationsChainNetworkData chainData = StationsChainNetworkData.Deserialize(reader); - //Multiplayer.Log($"JobData.Deserialize() chainData {chainData.ChainOriginYardId}, {chainData.ChainDestinationYardId}"); - - JobLicenses requiredLicenses = (JobLicenses)reader.GetInt(); - //Multiplayer.Log("JobData.Deserialize() requiredLicenses: " + requiredLicenses); - float startTime = reader.GetFloat(); - //Multiplayer.Log("JobData.Deserialize() startTime: " + startTime); - float finishTime = reader.GetFloat(); - //Multiplayer.Log("JobData.Deserialize() finishTime: " + finishTime); - float initialWage = reader.GetFloat(); - //Multiplayer.Log("JobData.Deserialize() initialWage: " + initialWage); - JobState state = (JobState)reader.GetByte(); - //Multiplayer.Log("JobData.Deserialize() state: " + state); - float timeLimit = reader.GetFloat(); - //Multiplayer.Log("JobData.Deserialize() timeLimit: " + timeLimit); - - //int playerId = (state != JobState.Available)? new(reader.GetBytesWithLength()) : Guid.Empty; - int playerId = reader.GetInt(); - return new JobData + ushort netID = reader.GetUShort(); + JobType jobType = (JobType)reader.GetByte(); + string id = reader.GetString(); + + //Decompress task data + byte[] compressedData = reader.GetBytesWithLength(); + byte[] decompressedData = PacketCompression.Decompress(compressedData); + + Multiplayer.Log($"JobData.Deserialize() Compressed: {compressedData.Length} Decompressed: {decompressedData.Length}"); + + TaskNetworkData[] tasks; + + using (MemoryStream ms = new MemoryStream(decompressedData)) + using (BinaryReader br = new BinaryReader(ms)) + { + byte tasksLength = br.ReadByte(); + tasks = new TaskNetworkData[tasksLength]; + + for (int i = 0; i < tasksLength; i++) + { + TaskType taskType = (TaskType)br.ReadByte(); + + int taskLength = br.ReadInt32(); + NetDataReader taskReader = new NetDataReader(br.ReadBytes(taskLength)); + + tasks[i] = TaskNetworkDataFactory.ConvertTask(taskType); + tasks[i].Deserialize(taskReader); + } + } + + StationsChainNetworkData chainData = StationsChainNetworkData.Deserialize(reader); + + JobLicenses requiredLicenses = (JobLicenses)reader.GetInt(); + float startTime = reader.GetFloat(); + float finishTime = reader.GetFloat(); + float initialWage = reader.GetFloat(); + JobState state = (JobState)reader.GetByte(); + float timeLimit = reader.GetFloat(); + ushort itemNetId = reader.GetUShort(); + ItemPositionData itemPositionData = ItemPositionData.Deserialize(reader); + + return new JobData + { + NetID = netID, + JobType = jobType, + ID = id, + Tasks = tasks, + ChainData = chainData, + RequiredLicenses = requiredLicenses, + StartTime = startTime, + FinishTime = finishTime, + InitialWage = initialWage, + State = state, + TimeLimit = timeLimit, + ItemNetID = itemNetId, + ItemPosition = itemPositionData + }; + } + catch (Exception ex) { - NetID = netID, - JobType = jobType, - ID = id, - Tasks = tasks, - ChainData = chainData, - RequiredLicenses = requiredLicenses, - StartTime = startTime, - FinishTime = finishTime, - InitialWage = initialWage, - State = state, - TimeLimit = timeLimit, - PlayerId = playerId, - }; + Multiplayer.Log($"JobData.Deserialize() Failed! {ex.Message}\r\n{ex.StackTrace}"); + return null; + } } } diff --git a/Multiplayer/Networking/Data/JobUpdateData.cs b/Multiplayer/Networking/Data/JobUpdateData.cs new file mode 100644 index 0000000..c71c56d --- /dev/null +++ b/Multiplayer/Networking/Data/JobUpdateData.cs @@ -0,0 +1,48 @@ +using DV.ThingTypes; +using LiteNetLib.Utils; +namespace Multiplayer.Networking.Data; + +public struct JobUpdateStruct : INetSerializable +{ + public ushort JobNetID; + public bool Invalid; + public JobState JobState; + public float StartTime; + public float FinishTime; + public ushort ItemNetID; + public ushort ValidationStationId; + public ItemPositionData ItemPositionData; + + public readonly void Serialize(NetDataWriter writer) + { + writer.Put(JobNetID); + writer.Put(Invalid); + + //Invalid jobs will be deleted / deregistered + if (Invalid) + return; + + writer.Put((byte)JobState); + writer.Put(StartTime); + writer.Put(FinishTime); + writer.Put(ItemNetID); + writer.Put(ValidationStationId); + ItemPositionData.Serialize(writer,ItemPositionData); + } + + public void Deserialize(NetDataReader reader) + { + JobNetID = reader.GetUShort(); + Invalid = reader.GetBool(); + + if (Invalid) + return; + + JobState = (JobState)reader.GetByte(); + StartTime = reader.GetFloat(); + FinishTime = reader.GetFloat(); + ItemNetID = reader.GetUShort(); + ValidationStationId = reader.GetUShort(); + ItemPositionData = ItemPositionData.Deserialize(reader); + } +} diff --git a/Multiplayer/Networking/Data/JobValidationData.cs b/Multiplayer/Networking/Data/JobValidationData.cs new file mode 100644 index 0000000..f2b59ec --- /dev/null +++ b/Multiplayer/Networking/Data/JobValidationData.cs @@ -0,0 +1,8 @@ + +namespace Multiplayer.Networking.Data; + +public enum ValidationType : byte +{ + JobOverview, + JobBooklet +} diff --git a/Multiplayer/Networking/Data/TaskNetworkData.cs b/Multiplayer/Networking/Data/TaskNetworkData.cs index 7209948..3fffaa4 100644 --- a/Multiplayer/Networking/Data/TaskNetworkData.cs +++ b/Multiplayer/Networking/Data/TaskNetworkData.cs @@ -30,16 +30,22 @@ public abstract class TaskNetworkData : TaskNetworkData where T : TaskNetwork protected void SerializeCommon(NetDataWriter writer) { Multiplayer.Log($"TaskNetworkData.SerializeCommon() State {(byte)State}, {State}"); + //Multiplayer.Log($"TaskNetworkData.SerializeCommon() State {(byte)State}, {State}"); writer.Put((byte)State); Multiplayer.Log($"TaskNetworkData.SerializeCommon() TaskStartTime {TaskStartTime}"); + //Multiplayer.Log($"TaskNetworkData.SerializeCommon() TaskStartTime {TaskStartTime}"); writer.Put(TaskStartTime); Multiplayer.Log($"TaskNetworkData.SerializeCommon() TaskFinishTime {TaskFinishTime}"); + //Multiplayer.Log($"TaskNetworkData.SerializeCommon() TaskFinishTime {TaskFinishTime}"); writer.Put(TaskFinishTime); Multiplayer.Log($"TaskNetworkData.SerializeCommon() IsLastTask {IsLastTask}"); + //Multiplayer.Log($"TaskNetworkData.SerializeCommon() IsLastTask {IsLastTask}"); writer.Put(IsLastTask); Multiplayer.Log($"TaskNetworkData.SerializeCommon() TimeLimit {TimeLimit}"); + //Multiplayer.Log($"TaskNetworkData.SerializeCommon() TimeLimit {TimeLimit}"); writer.Put(TimeLimit); Multiplayer.Log($"TaskNetworkData.SerializeCommon() TaskType {(byte)TaskType}, {TaskType}"); + //Multiplayer.Log($"TaskNetworkData.SerializeCommon() TaskType {(byte)TaskType}, {TaskType}"); writer.Put((byte)TaskType); } diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index e1a5cd5..c400ed2 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -121,6 +121,7 @@ protected override void Subscribe() netPacketProcessor.SubscribeReusable(OnClientboundLicenseAcquiredPacket); netPacketProcessor.SubscribeReusable(OnClientboundGarageUnlockPacket); netPacketProcessor.SubscribeReusable(OnClientboundDebtStatusPacket); + netPacketProcessor.SubscribeReusable(OnClientboundJobsUpdatePacket); netPacketProcessor.SubscribeReusable(OnClientboundJobsCreatePacket); netPacketProcessor.SubscribeReusable(OnClientboundJobValidateResponsePacket); netPacketProcessor.SubscribeReusable(OnCommonChatPacket); @@ -744,33 +745,32 @@ private void OnClientboundJobsCreatePacket(ClientboundJobsCreatePacket packet) networkedStationController.AddJobs(packet.Jobs); } + + private void OnClientboundJobsUpdatePacket(ClientboundJobsUpdatePacket packet) + { + Log($"OnClientboundJobsUpdatePacket() for station {packet.StationNetId}, containing {packet.JobUpdates.Length}"); + + if (NetworkLifecycle.Instance.IsHost()) + return; + + if (!NetworkedStationController.Get(packet.StationNetId, out NetworkedStationController networkedStationController)) + { + LogError($"OnClientboundJobsUpdatePacket() {packet.StationNetId} does not exist!"); + return; + } + + networkedStationController.UpdateJobs(packet.JobUpdates); + } private void OnClientboundJobValidateResponsePacket(ClientboundJobValidateResponsePacket packet) { - Log($"OnClientboundJobValidateResponsePacket() JobNetId: {packet.JobNetId}, Status: {packet.Accepted}"); + Log($"OnClientboundJobValidateResponsePacket() JobNetId: {packet.JobNetId}, Status: {packet.Invalid}"); if(!NetworkedJob.Get(packet.JobNetId, out NetworkedJob networkedJob)) return; - networkedJob.ValidatorResponseReceived = true; - networkedJob.ValidationAccepted = packet.Accepted; - - switch (networkedJob.ValidationType) - { - case ValidationType.JobOverview: - networkedJob.JobValidator.ProcessJobOverview(networkedJob.JobOverview); - break; - - case ValidationType.JobBooklet: - networkedJob.JobValidator.ValidateJob(networkedJob.JobBooklet); - break; - } - - if(networkedJob.ValidationItem != null) - networkedJob.ValidationItem.NetId = packet.ItemNetID; - else - LogError($"OnClientboundJobValidateResponsePacket() {packet.JobNetId}, ValidationItem not found!"); + GameObject.Destroy(networkedJob.gameObject); } #endregion @@ -1057,14 +1057,12 @@ public void SendLicensePurchaseRequest(string id, bool isJobLicense) public void SendJobValidateRequest(NetworkedJob job, NetworkedStationController station) { - /* disabled for stable release SendPacketToServer(new ServerboundJobValidateRequestPacket { JobNetId = job.NetId, StationNetId = station.NetId, validationType = job.ValidationType }, DeliveryMethod.ReliableUnordered); - */ } public void SendChat(string message) diff --git a/Multiplayer/Networking/Managers/NetworkManager.cs b/Multiplayer/Networking/Managers/NetworkManager.cs index b1d4aee..6bdcbb9 100644 --- a/Multiplayer/Networking/Managers/NetworkManager.cs +++ b/Multiplayer/Networking/Managers/NetworkManager.cs @@ -4,7 +4,6 @@ using LiteNetLib; using LiteNetLib.Utils; using Multiplayer.Networking.Data; -using Multiplayer.Networking.Packets.Clientbound.Jobs; using Multiplayer.Networking.Serialization; namespace Multiplayer.Networking.Listeners; @@ -41,7 +40,7 @@ protected NetworkManager(Settings settings) private void RegisterNestedTypes() { netPacketProcessor.RegisterNestedType(BogieData.Serialize, BogieData.Deserialize); - netPacketProcessor.RegisterNestedType(JobUpdateStruct.Serialize, JobUpdateStruct.Deserialize); + netPacketProcessor.RegisterNestedType(); netPacketProcessor.RegisterNestedType(JobData.Serialize, JobData.Deserialize); netPacketProcessor.RegisterNestedType(ModInfo.Serialize, ModInfo.Deserialize); netPacketProcessor.RegisterNestedType(RigidbodySnapshot.Serialize, RigidbodySnapshot.Deserialize); diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index 2cbb40b..e68fcef 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -32,7 +32,6 @@ using Multiplayer.Networking.Packets.Serverbound.Train; using Multiplayer.Networking.Packets.Unconnected; - namespace Multiplayer.Networking.Listeners; public class NetworkServer : NetworkManager @@ -59,7 +58,7 @@ public class NetworkServer : NetworkManager private bool IsLoaded; //we don't care if the client doesn't have these mods - private string[] modWhiteList = { "RuntimeUnityEditor" }; + private string[] modWhiteList = { "RuntimeUnityEditor", "BookletOrganizer" }; public NetworkServer(IDifficulty difficulty, Settings settings, bool isSinglePlayer, LobbyServerData serverData) : base(settings) { @@ -386,22 +385,16 @@ public void SendDebtStatus(bool hasDebt) }, DeliveryMethod.ReliableUnordered, selfPeer); } - public void SendJobsCreatePacket(ushort stationNetId, NetworkedJob[] jobs, DeliveryMethod method = DeliveryMethod.ReliableSequenced ) + public void SendJobsCreatePacket(NetworkedStationController networkedStation, NetworkedJob[] jobs, DeliveryMethod method = DeliveryMethod.ReliableSequenced ) { - Multiplayer.Log($"Sending JobsCreatePacket with {jobs.Count()} jobs"); - SendPacketToAll(ClientboundJobsCreatePacket.FromNetworkedJobs(stationNetId, jobs), method); + Multiplayer.Log($"Sending JobsCreatePacket for stationNetId {networkedStation.NetId} with {jobs.Count()} jobs"); + SendPacketToAll(ClientboundJobsCreatePacket.FromNetworkedJobs(networkedStation, jobs), method); } - public void SendJobsUpdatePacket(JobUpdateStruct[] jobs, NetPeer peer = null) + public void SendJobsUpdatePacket(ushort stationNetId, NetworkedJob[] jobs, NetPeer peer = null) { - if (peer != null) - { - SendPacketToAll(new ClientboundJobsUpdatePacket { JobUpdates = jobs }, DeliveryMethod.ReliableUnordered); - } - else - { - SendPacket(peer, new ClientboundJobsUpdatePacket { JobUpdates = jobs }, DeliveryMethod.ReliableUnordered); - } + Multiplayer.Log($"Sending JobsUpdatePacket for stationNetId {stationNetId} with {jobs.Count()} jobs"); + SendPacketToAll(ClientboundJobsUpdatePacket.FromNetworkedJobs(stationNetId, jobs), DeliveryMethod.ReliableUnordered); } public void SendChat(string message, NetPeer exclude = null) @@ -616,7 +609,7 @@ private void OnServerboundClientReadyPacket(ServerboundClientReadyPacket packet, NetworkedJob[] jobs = netStation.NetworkedJobs.ToArray(); for (int i = 0; i < jobs.Length; i++) { - SendJobsCreatePacket(netStation.NetId, [jobs[i]], DeliveryMethod.ReliableOrdered); + SendJobsCreatePacket(netStation, [jobs[i]], DeliveryMethod.ReliableOrdered); } } else @@ -906,17 +899,13 @@ private void OnServerboundLicensePurchaseRequestPacket(ServerboundLicensePurchas private void OnServerboundJobValidateRequestPacket(ServerboundJobValidateRequestPacket packet, NetPeer peer) { - NetworkedItem item; + LogWarning($"OnServerboundJobValidateRequestPacket(): {packet.JobNetId}"); if (!NetworkedJob.Get(packet.JobNetId, out NetworkedJob networkedJob)) { LogWarning($"OnServerboundJobValidateRequestPacket() NetworkedJob not found: {packet.JobNetId}"); - JobUpdateStruct invalidJob = new JobUpdateStruct(); - invalidJob.JobNetID = packet.JobNetId; - invalidJob.Invalid = true; - - SendJobsUpdatePacket([invalidJob],peer); + SendPacket(peer, new ClientboundJobValidateResponsePacket { JobNetId = packet.JobNetId, Invalid = true }, DeliveryMethod.ReliableUnordered); return; } @@ -933,62 +922,17 @@ private void OnServerboundJobValidateRequestPacket(ServerboundJobValidateRequest return; } - ClientboundJobValidateResponsePacket responsePacket = new ClientboundJobValidateResponsePacket { JobNetId = packet.JobNetId, Accepted = false}; - + LogDebug(() => $"OnServerboundJobValidateRequestPacket() Validating {packet.JobNetId}, Validation Type: {packet.validationType} overview: {networkedJob.JobOverview!=null}, booklet: {networkedJob.JobBooklet !=null}"); switch (packet.validationType) { case ValidationType.JobOverview: - if (networkedJob.Job.State != JobState.Available) - { - Log($"OnServerboundJobValidateRequestPacket({networkedJob.Job?.ID}) JobState: {networkedJob.Job.State}, DENIED"); - } - else if(networkedJob.JobOverview == null) - { - Log($"OnServerboundJobValidateRequestPacket({networkedJob.Job?.ID}) JobOverview does not exist, DENIED"); - } - else - { - networkedStationController.JobValidator.ProcessJobOverview(networkedJob.JobOverview); - if(networkedJob.JobBooklet != null) - { - if(!networkedJob.JobBooklet.TryGetComponent(out item)) - { - LogError($"OnServerboundJobValidateRequestPacket({networkedJob.Job?.ID}) JobState: {networkedJob.Job.State}, Could not get NetworkedItem"); - return; - } - - responsePacket.ItemNetID = item.NetId; - responsePacket.Accepted = true; - - networkedJob.OwnedBy = player.Guid; - networkedJob.playerID = peer.Id; - Log($"OnServerboundJobValidateRequestPacket({networkedJob.Job?.ID}) JobState: {networkedJob.Job.State}, ACCEPTED"); - } - else - { - Log($"OnServerboundJobValidateRequestPacket({networkedJob.Job?.ID}) Failed to generate booklet, DENIED"); - } - } + networkedStationController.JobValidator.ProcessJobOverview(networkedJob.JobOverview); break; case ValidationType.JobBooklet: - if (networkedJob.Job.State != JobState.InProgress) - { - Log($"OnServerboundJobValidateRequestPacket({networkedJob.Job?.ID}) JobState: {networkedJob.Job.State}, DENIED"); - } - else if (networkedJob.JobBooklet == null) - { - Log($"OnServerboundJobValidateRequestPacket({networkedJob.Job?.ID}) JobBooklet does not exist, DENIED"); - } - else - { - networkedStationController.JobValidator.ValidateJob(networkedJob.JobBooklet); - responsePacket.Accepted = true; - } - break; + networkedStationController.JobValidator.ValidateJob(networkedJob.JobBooklet); + break; } - - SendPacket(peer, responsePacket, DeliveryMethod.ReliableOrdered); } private void OnCommonChatPacket(CommonChatPacket packet, NetPeer peer) diff --git a/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobValidateResponsePacket.cs b/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobValidateResponsePacket.cs index cd35fd7..e489af2 100644 --- a/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobValidateResponsePacket.cs +++ b/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobValidateResponsePacket.cs @@ -4,6 +4,5 @@ namespace Multiplayer.Networking.Packets.Clientbound.Jobs; public class ClientboundJobValidateResponsePacket { public ushort JobNetId { get; set; } - public bool Accepted { get; set; } - public ushort ItemNetID { get; set; } + public bool Invalid { get; set; } } diff --git a/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobsCreatePacket.cs b/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobsCreatePacket.cs index 61d3c3a..4b5d536 100644 --- a/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobsCreatePacket.cs +++ b/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobsCreatePacket.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using Multiplayer.Components.Networking.Jobs; +using Multiplayer.Components.Networking.World; using Multiplayer.Networking.Data; namespace Multiplayer.Networking.Packets.Clientbound.Jobs; @@ -8,17 +9,19 @@ public class ClientboundJobsCreatePacket public ushort StationNetId { get; set; } public JobData[] Jobs { get; set; } - public static ClientboundJobsCreatePacket FromNetworkedJobs(ushort stationID, NetworkedJob[] jobs) + public static ClientboundJobsCreatePacket FromNetworkedJobs(NetworkedStationController netStation, NetworkedJob[] jobs) { List jobData = new List(); foreach (var job in jobs) { - jobData.Add(JobData.FromJob(job)); + JobData jd = JobData.FromJob(netStation, job); + Multiplayer.Log($"JobData: jobNetId: {jd.NetID}, jobId: {jd.ID}, itemNetId {jd.ItemNetID}"); + jobData.Add(jd); } return new ClientboundJobsCreatePacket { - StationNetId = stationID, + StationNetId = netStation.NetId, Jobs = jobData.ToArray() }; } diff --git a/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobsUpdatePacket.cs b/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobsUpdatePacket.cs index cea049a..7201275 100644 --- a/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobsUpdatePacket.cs +++ b/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobsUpdatePacket.cs @@ -1,69 +1,44 @@ -using DV.ThingTypes; -using LiteNetLib.Utils; +using Multiplayer.Networking.Data; +using Multiplayer.Components.Networking.Jobs; +using Multiplayer.Components.Networking.World; +using System.Collections.Generic; namespace Multiplayer.Networking.Packets.Clientbound.Jobs; - -public struct JobUpdateStruct -{ - public ushort JobNetID; - public bool Invalid; - public JobState JobState; - public float StartTime; - public float FinishTime; - public ushort OwnedBy; - - public static void Serialize(NetDataWriter writer, JobUpdateStruct data) - { - writer.Put(data.JobNetID); - writer.Put(data.Invalid); - - //Invalid jobs will be deleted / deregistered - if (data.Invalid) - return; - - writer.Put((byte)data.JobState); - writer.Put(data.StartTime); - writer.Put(data.FinishTime); - - writer.Put(data.OwnedBy); - } - - public static JobUpdateStruct Deserialize(NetDataReader reader) - { - JobUpdateStruct deserialised = new JobUpdateStruct(); - - deserialised.JobNetID = reader.GetUShort(); - deserialised.Invalid = reader.GetBool(); - - if (deserialised.Invalid) - return deserialised; - - deserialised.JobState = (JobState) reader.GetByte(); - deserialised.StartTime = reader.GetFloat(); - deserialised.FinishTime = reader.GetFloat(); - deserialised.OwnedBy = reader.GetUShort(); - - return deserialised; - } -} public class ClientboundJobsUpdatePacket { + public ushort StationNetId { get; set; } public JobUpdateStruct[] JobUpdates { get; set; } - /* - public static ClientboundJobsUpdatePacket FromNetworkedJobs(ushort stationID, NetworkedJob[] jobs) + + public static ClientboundJobsUpdatePacket FromNetworkedJobs(ushort stationNetID, NetworkedJob[] jobs) { - List jobData = new List(); + Multiplayer.Log($"ClientboundJobsUpdatePacket.FromNetworkedJobs({stationNetID}, {jobs.Length})"); + + List jobData = new List(); foreach (var job in jobs) { - jobData.Add(JobData.FromJob(job)); + ushort validationNetId = 0; + + if (NetworkedStationController.GetFromJobValidator(job.JobValidator, out NetworkedStationController netValidationStation)) + validationNetId = netValidationStation.NetId; + + JobUpdateStruct data = new JobUpdateStruct + { + JobNetID = job.NetId, + JobState = job.Job.State, + StartTime = job.Job.startTime, + FinishTime = job.Job.finishTime, + ItemNetID = job.ValidationItem.NetId, + ValidationStationId = validationNetId + }; + + jobData.Add(data); } - return new ClientboundJobsCreatePacket - { - StationNetId = stationID, - Jobs = jobData.ToArray() - }; + return new ClientboundJobsUpdatePacket + { + StationNetId = stationNetID, + JobUpdates = jobData.ToArray() + }; } - */ } diff --git a/Multiplayer/Networking/Packets/Serverbound/Jobs/ServerboundJobValidateRequestPacket.cs b/Multiplayer/Networking/Packets/Serverbound/Jobs/ServerboundJobValidateRequestPacket.cs index 2119097..8e51f85 100644 --- a/Multiplayer/Networking/Packets/Serverbound/Jobs/ServerboundJobValidateRequestPacket.cs +++ b/Multiplayer/Networking/Packets/Serverbound/Jobs/ServerboundJobValidateRequestPacket.cs @@ -1,10 +1,6 @@ +using Multiplayer.Networking.Data; namespace Multiplayer.Networking.Packets.Clientbound.Jobs; -public enum ValidationType : byte -{ - JobOverview, - JobBooklet -} public class ServerboundJobValidateRequestPacket { public ushort JobNetId { get; set; } diff --git a/Multiplayer/Patches/Jobs/BookletCreatorJobPatch.cs b/Multiplayer/Patches/Jobs/BookletCreatorJobPatch.cs new file mode 100644 index 0000000..81ff351 --- /dev/null +++ b/Multiplayer/Patches/Jobs/BookletCreatorJobPatch.cs @@ -0,0 +1,44 @@ +using DV.Booklets; +using DV.Logic.Job; +using HarmonyLib; +using Multiplayer.Components.Networking.Jobs; +using Multiplayer.Components.Networking.World; +using Multiplayer.Utils; +using UnityEngine; + + +namespace Multiplayer.Patches.Jobs; + +[HarmonyPatch(typeof(BookletCreator))] +public static class BookletCreatorJob_Patch +{ + [HarmonyPatch(nameof(BookletCreator.CreateJobOverview))] + [HarmonyPostfix] + private static void CreateJobOverview(JobOverview __result, Job job) + { + if (!NetworkedJob.TryGetFromJob(job, out NetworkedJob networkedJob)) + { + Multiplayer.LogError($"BookletCreatorJob_Patch.CreateJobOverview() NetworkedJob not found for Job ID: {job.ID}"); + } + else + { + networkedJob.JobOverview = __result; + networkedJob.ValidationItem = __result.GetOrAddComponent(); + } + } + + [HarmonyPatch(nameof(BookletCreator.CreateJobBooklet))] + [HarmonyPostfix] + private static void CreateJobBooklet(JobBooklet __result, Job job) + { + if (!NetworkedJob.TryGetFromJob(job, out NetworkedJob networkedJob)) + { + Multiplayer.LogError($"BookletCreatorJob_Patch.CreateJobBooklet() NetworkedJob not found for Job ID: {job.ID}"); + } + else + { + networkedJob.JobBooklet = __result; + networkedJob.ValidationItem = __result.GetOrAddComponent(); + } + } +} diff --git a/Multiplayer/Patches/Jobs/JobBookletPatch.cs b/Multiplayer/Patches/Jobs/JobBookletPatch.cs index ba0fd98..716af65 100644 --- a/Multiplayer/Patches/Jobs/JobBookletPatch.cs +++ b/Multiplayer/Patches/Jobs/JobBookletPatch.cs @@ -9,20 +9,20 @@ namespace Multiplayer.Patches.Jobs; [HarmonyPatch(typeof(JobBooklet))] public static class JobBooklet_Patch { - [HarmonyPatch(nameof(JobBooklet.AssignJob))] - [HarmonyPostfix] - private static void AssignJob(JobBooklet __instance, Job jobToAssign) - { - if (!NetworkedJob.TryGetFromJob(__instance.job, out NetworkedJob networkedJob)) - { - Multiplayer.LogError($"JobBooklet.AssignJob() NetworkedJob not found for Job ID: {__instance.job?.ID}"); - return; - } + //[HarmonyPatch(nameof(JobBooklet.AssignJob))] + //[HarmonyPostfix] + //private static void AssignJob(JobBooklet __instance, Job jobToAssign) + //{ + // if (!NetworkedJob.TryGetFromJob(__instance.job, out NetworkedJob networkedJob)) + // { + // Multiplayer.LogError($"JobBooklet.AssignJob() NetworkedJob not found for Job ID: {__instance.job?.ID}"); + // return; + // } - networkedJob.JobBooklet = __instance; - if(networkedJob.TryGetComponent(out NetworkedItem netItem)) - networkedJob.ValidationItem = netItem; - } + // networkedJob.JobBooklet = __instance; + // if(networkedJob.TryGetComponent(out NetworkedItem netItem)) + // networkedJob.ValidationItem = netItem; + //} [HarmonyPatch(nameof(JobBooklet.DestroyJobBooklet))] diff --git a/Multiplayer/Patches/Jobs/JobValidatorPatch.cs b/Multiplayer/Patches/Jobs/JobValidatorPatch.cs index 75ea7af..0b102c1 100644 --- a/Multiplayer/Patches/Jobs/JobValidatorPatch.cs +++ b/Multiplayer/Patches/Jobs/JobValidatorPatch.cs @@ -1,14 +1,10 @@ -using System; using System.Collections; -using System.Linq; -using DV; using DV.ThingTypes; using HarmonyLib; using Multiplayer.Components.Networking; using Multiplayer.Components.Networking.Jobs; using Multiplayer.Components.Networking.World; -using Multiplayer.Networking.Packets.Clientbound.Jobs; - +using Multiplayer.Networking.Data; using UnityEngine; namespace Multiplayer.Patches.Jobs; @@ -29,8 +25,6 @@ private static void Start(JobValidator __instance) [HarmonyPrefix] private static bool ProcessJobOverview_Prefix(JobValidator __instance, JobOverview jobOverview) { - if (NetworkLifecycle.Instance.IsHost()) - return true; if(__instance.bookletPrinter.IsOnCooldown) { @@ -46,9 +40,16 @@ private static bool ProcessJobOverview_Prefix(JobValidator __instance, JobOvervi return false; } - if (networkedJob.ValidatorRequestSent) - return (networkedJob.ValidatorResponseReceived && networkedJob.ValidationAccepted); - else + if (NetworkLifecycle.Instance.IsHost()) + { + Multiplayer.Log($"ProcessJobOverview_Prefix({jobOverview?.job?.ID}) IsHost"); + networkedJob.JobValidator = __instance; + return true; + } + + if (!networkedJob.ValidatorRequestSent) + // return (networkedJob.ValidatorResponseReceived && networkedJob.ValidationAccepted); + //else SendValidationRequest(__instance, networkedJob, ValidationType.JobOverview); return false; @@ -59,9 +60,6 @@ private static bool ProcessJobOverview_Prefix(JobValidator __instance, JobOvervi [HarmonyPrefix] private static bool ValidateJob_Prefix(JobValidator __instance, JobBooklet jobBooklet) { - if (NetworkLifecycle.Instance.IsHost()) - return true; - if (__instance.bookletPrinter.IsOnCooldown) { __instance.bookletPrinter.PlayErrorSound(); @@ -76,6 +74,12 @@ private static bool ValidateJob_Prefix(JobValidator __instance, JobBooklet jobBo return false; } + if (NetworkLifecycle.Instance.IsHost()) + { + networkedJob.JobValidator = __instance; + return true; + } + if (networkedJob.ValidatorRequestSent) return (networkedJob.ValidatorResponseReceived && networkedJob.ValidationAccepted); else diff --git a/Multiplayer/Patches/Jobs/StationControllerPatch.cs b/Multiplayer/Patches/Jobs/StationControllerPatch.cs index c66aa29..dc89c2d 100644 --- a/Multiplayer/Patches/Jobs/StationControllerPatch.cs +++ b/Multiplayer/Patches/Jobs/StationControllerPatch.cs @@ -1,13 +1,25 @@ using HarmonyLib; +using Multiplayer.Components.Networking; using Multiplayer.Components.Networking.World; namespace Multiplayer.Patches.Jobs; -[HarmonyPatch(typeof(StationController), nameof(StationController.Awake))] -public static class StationController_Awake_Patch +[HarmonyPatch(typeof(StationController))] +public static class StationController_Patch { - public static void Postfix(StationController __instance) + [HarmonyPatch(nameof(StationController.Awake))] + [HarmonyPostfix] + public static void Awake(StationController __instance) { __instance.gameObject.AddComponent(); } + + [HarmonyPatch(nameof(StationController.ExpireAllAvailableJobsInStation))] + [HarmonyPrefix] + public static bool ExpireAllAvailableJobsInStation(StationController __instance) + { + return NetworkLifecycle.Instance.IsHost(); + } + + } diff --git a/Multiplayer/Utils/PacketCompression.cs b/Multiplayer/Utils/PacketCompression.cs new file mode 100644 index 0000000..39baeef --- /dev/null +++ b/Multiplayer/Utils/PacketCompression.cs @@ -0,0 +1,30 @@ +using UnityEngine; +using System.IO; +using System.IO.Compression; +using System.Text; + +public static class PacketCompression +{ + public static byte[] Compress(byte[] data) + { + using (var outputStream = new MemoryStream()) + { + using (var gzipStream = new GZipStream(outputStream, CompressionMode.Compress)) + { + gzipStream.Write(data, 0, data.Length); + } + return outputStream.ToArray(); + } + } + + public static byte[] Decompress(byte[] compressedData) + { + using (var inputStream = new MemoryStream(compressedData)) + using (var gzipStream = new GZipStream(inputStream, CompressionMode.Decompress)) + using (var outputStream = new MemoryStream()) + { + gzipStream.CopyTo(outputStream); + return outputStream.ToArray(); + } + } +} From d95229dd137eb78c3eec29c66c66ecb291fab901 Mon Sep 17 00:00:00 2001 From: AMacro Date: Tue, 24 Sep 2024 19:59:08 +1000 Subject: [PATCH 102/188] Continuation of item sync --- .../Networking/Jobs/NetworkedJob.cs | 217 +++++++--- .../Networking/World/NetworkedItem.cs | 328 +++++++++----- .../Networking/World/NetworkedItemManager.cs | 66 +++ .../World/NetworkedStationController.cs | 409 +++++++----------- .../Networking/Data/ItemPositionData.cs | 3 +- Multiplayer/Networking/Data/ItemUpdateData.cs | 152 +++++++ Multiplayer/Networking/Data/JobData.cs | 32 +- Multiplayer/Networking/Data/TrackedValue.cs | 57 +++ .../Managers/Client/NetworkClient.cs | 38 +- .../Managers/Server/NetworkServer.cs | 45 +- .../Jobs/ClientboundJobsUpdatePacket.cs | 27 +- .../Packets/Common/CommonItemChangePacket.cs | 118 +++++ .../Patches/Jobs/BookletCreatorJobPatch.cs | 17 +- Multiplayer/Patches/Jobs/JobOverviewPatch.cs | 24 +- .../World/{ => Items}/ItemBasePatch.cs | 10 +- .../Patches/World/Items/LanternPatch.cs | 40 ++ .../Patches/World/Items/LighterPatch.cs | 44 ++ 17 files changed, 1183 insertions(+), 444 deletions(-) create mode 100644 Multiplayer/Components/Networking/World/NetworkedItemManager.cs create mode 100644 Multiplayer/Networking/Data/ItemUpdateData.cs create mode 100644 Multiplayer/Networking/Data/TrackedValue.cs create mode 100644 Multiplayer/Networking/Packets/Common/CommonItemChangePacket.cs rename Multiplayer/Patches/World/{ => Items}/ItemBasePatch.cs (57%) create mode 100644 Multiplayer/Patches/World/Items/LanternPatch.cs create mode 100644 Multiplayer/Patches/World/Items/LighterPatch.cs diff --git a/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs b/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs index 24a0af9..663d4a9 100644 --- a/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs +++ b/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs @@ -1,10 +1,12 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; using DV.Logic.Job; using Multiplayer.Components.Networking.World; using Multiplayer.Networking.Data; +using Newtonsoft.Json.Linq; using UnityEngine; @@ -32,12 +34,6 @@ public static bool GetJob(ushort netId, out Job obj) return b; } - - //public static NetworkedJob GetFromJob(Job job) - //{ - // return jobToNetworkedJob[job]; - //} - public static bool TryGetFromJob(Job job, out NetworkedJob networkedJob) { return jobToNetworkedJob.TryGetValue(job, out networkedJob); @@ -50,90 +46,207 @@ public static bool TryGetFromJobId(string jobId, out NetworkedJob networkedJob) #endregion protected override bool IsIdServerAuthoritative => true; + public enum DirtyCause + { + JobOverview, + JobBooklet, + JobReport, + JobState + } - public Action OverviewGenerated; + public Job Job { get; private set; } + public NetworkedStationController Station { get; private set; } - public Job Job; - public JobOverview JobOverview; - public JobBooklet JobBooklet; - public JobReport JobReport; - public JobExpiredReport JobExpiredReport; - public NetworkedStationController Station; + private NetworkedItem _jobOverview; + public NetworkedItem JobOverview + { + get => _jobOverview; + set + { + if (value != null && value.GetTrackedItem() == null) + return; + + _jobOverview = value; + + if (value != null) + { + Cause = DirtyCause.JobOverview; + OnJobDirty?.Invoke(this); + } + } + } - public Guid OwnedBy = Guid.Empty; //GUID of player who took the job (sever only) - public int playerID; //ID of player who took the job (client & server) + private NetworkedItem _jobBooklet; + public NetworkedItem JobBooklet + { + get => _jobBooklet; + set + { + if (value != null && value.GetTrackedItem() == null) + return; + + _jobBooklet = value; + if (value != null) + { + Cause = DirtyCause.JobBooklet; + OnJobDirty?.Invoke(this); + } + } + } + private NetworkedItem _jobReport; + public NetworkedItem JobReport + { + get => _jobReport; + set + { + if (value != null && value.GetTrackedItem() == null) + return; + + _jobReport = value; + if (value != null) + { + Cause = DirtyCause.JobReport; + OnJobDirty?.Invoke(this); + } + } + } - public JobValidator JobValidator; //Job validator to print the booklet/job validation at (client only) - public bool ValidatorRequestSent = false; - public bool ValidatorResponseReceived = false; - public bool ValidationAccepted = false; - public ValidationType ValidationType; - public NetworkedItem ValidationItem; + private List JobReports = new List(); - #region Client + public Guid OwnedBy { get; set; } = Guid.Empty; + public JobValidator JobValidator { get; set; } + public bool ValidatorRequestSent { get; set; } = false; + public bool ValidatorResponseReceived { get; set; } = false; + public bool ValidationAccepted { get; set; } = false; + public ValidationType ValidationType { get; set; } + public DirtyCause Cause { get; private set; } - #endregion + public Action OnJobDirty; + protected override void Awake() + { + base.Awake(); + } private void Start() { - //startup stuff - Multiplayer.Log($"NetworkedJob.Start({Job.ID})"); + if (Job != null) + { + AddToCache(); + } + else + { + Multiplayer.LogError($"NetworkedJob Start(): Job is null for {gameObject.name}"); + } + } + + public void Initialize(Job job, NetworkedStationController station) + { + Job = job; + Station = station; + + // Setup handlers + job.JobTaken += OnJobTaken; + job.JobAbandoned += OnJobAbandoned; + job.JobCompleted += OnJobCompleted; + job.JobExpired += OnJobExpired; + + // If this is called after Start(), we need to add to cache here + if (gameObject.activeInHierarchy) + { + AddToCache(); + } + } + private void AddToCache() + { jobToNetworkedJob[Job] = this; jobIdToNetworkedJob[Job.ID] = this; jobIdToJob[Job.ID] = Job; - - Multiplayer.Log("NetworkedJob.Start() Started"); + Multiplayer.Log($"NetworkedJob added to cache: {Job.ID}"); } - private void OnDisable() + private void OnJobTaken(Job job, bool viaLoadGame) { - if (UnloadWatcher.isQuitting) + if (viaLoadGame) return; - - if (UnloadWatcher.isUnloading) - return; - - jobToNetworkedJob.Remove(Job); - jobIdToNetworkedJob.Remove(Job.ID); - jobIdToNetworkedJob.Remove(Job.ID); - - Destroy(this); + Cause = DirtyCause.JobState; + OnJobDirty?.Invoke(this); } - #region Server + private void OnJobAbandoned(Job job) + { + Cause = DirtyCause.JobState; + OnJobDirty?.Invoke(this); + } - public void JobOverviewGenerated(JobOverview jobOverview) + private void OnJobCompleted(Job job) { - JobOverview = jobOverview; + Cause = DirtyCause.JobState; + OnJobDirty?.Invoke(this); + } - OverviewGenerated?.Invoke(this); + private void OnJobExpired(Job job) + { + Cause = DirtyCause.JobState; + OnJobDirty?.Invoke(this); } - private void Server_OnTick(uint tick) + public void AddReport(NetworkedItem item) { - if (UnloadWatcher.isUnloading) + if (item == null || !item.UsefulItem) + { + Multiplayer.LogError($"Attempted to add a null or uninitialised report: JobId: {Job?.ID}, JobNetID: {NetId}"); return; + } + + Type reportType = item.TrackedItemType; + if( reportType == typeof(JobReport) || + reportType == typeof(JobExpiredReport) || + reportType == typeof(JobMissingLicenseReport) /*|| + reportType == typeof(Debtre) ||*/ + ) + { + JobReports.Add(item); + Cause = DirtyCause.JobReport; + OnJobDirty?.Invoke(this); + } + } + + public void RemoveReport(NetworkedItem item) + { } - #endregion + public void ClearReports() + { + foreach (var report in JobReports) + { + Destroy(report.gameObject); + } - #region Common + JobReports.Clear(); + } - private void Common_OnTick(uint tick) + private void OnDisable() { - if (UnloadWatcher.isUnloading) + if (UnloadWatcher.isQuitting || UnloadWatcher.isUnloading) return; - } - #endregion + // Remove from lookup caches + jobToNetworkedJob.Remove(Job); + jobIdToNetworkedJob.Remove(Job.ID); + jobIdToJob.Remove(Job.ID); - #region Client + // Unsubscribe from events + Job.JobTaken -= OnJobTaken; + Job.JobAbandoned -= OnJobAbandoned; + Job.JobCompleted -= OnJobCompleted; + Job.JobExpired -= OnJobExpired; - #endregion + Destroy(this); + } } diff --git a/Multiplayer/Components/Networking/World/NetworkedItem.cs b/Multiplayer/Components/Networking/World/NetworkedItem.cs index 9d27f42..cec7f08 100644 --- a/Multiplayer/Components/Networking/World/NetworkedItem.cs +++ b/Multiplayer/Components/Networking/World/NetworkedItem.cs @@ -1,13 +1,12 @@ using DV.CabControls; +using DV.CabControls.Spec; using DV.InventorySystem; -using DV.Simulation.Brake; -using Multiplayer.Components.Networking.Jobs; using Multiplayer.Components.Networking.Train; +using Multiplayer.Networking.Data; using System; -using System.Collections; using System.Collections.Generic; -using System.ComponentModel; using System.Linq; +using System.Text; using UnityEngine; namespace Multiplayer.Components.Networking.World; @@ -15,7 +14,6 @@ namespace Multiplayer.Components.Networking.World; public class NetworkedItem : IdMonoBehaviour { #region Lookup Cache - private static readonly Dictionary itemBaseToNetworkedItem = new(); public static bool Get(ushort netId, out NetworkedItem obj) @@ -31,168 +29,286 @@ public static bool GetItem(ushort netId, out ItemBase obj) obj = b ? networkedItem.Item : null; return b; } + + public static bool TryGetNetworkedItem(ItemBase item, out NetworkedItem networkedItem) + { + return itemBaseToNetworkedItem.TryGetValue(item, out networkedItem); + } #endregion - public ItemBase Item { get; set; } - public Guid Owner { get; set; } + private const float PositionThreshold = 0.01f; + private const float RotationThreshold = 0.1f; + + public ItemBase Item { get; private set; } + private Component trackedItem; + private List trackedValues = new List(); + public bool UsefulItem { get; private set; } = false; + public Type TrackedItemType { get; private set; } + + //Track dirty states + private bool CreatedDirty = true; //if set, we created this item dirty and have not sent an update + + private bool ItemGrabbed = false; //Current state of item grabbed + private bool GrabbedDirty = false; //Current state is dirty + + private bool ItemDropped = false; //Current state of item dropped + private bool DroppedDirty = false; //Current state is dirty + + private Vector3 lastPosition; + private Quaternion lastRotation; + private ItemPositionData ItemPosition; + private bool PositionDirty = false; + protected override bool IsIdServerAuthoritative => true; protected override void Awake() { base.Awake(); + Multiplayer.LogDebug(() => $"NetworkedItem.Awake() {name}"); - Multiplayer.LogDebug(()=>$"NetworkedItem.Awake() {name}"); + Register(); + } - if (!TryGetComponent(out ItemBase item)) - { - Multiplayer.LogError($"Unable to find ItemBase for {name}"); + protected void Start() + { + if (!CreatedDirty) return; + + if (StorageController.Instance.IsInStorageWorld(Item)) + { + ItemDropped = true; } - - Item = item; - itemBaseToNetworkedItem[Item] = this; - SetupItem(); } - private void Start() + public T GetTrackedItem() where T : Component { - + return UsefulItem ? trackedItem as T : null; } - private void SetupItem() + public void Initialize(T item, ushort netId = 0, bool createDirty = true) where T : Component { - //Let's get the item type and take an appropriate action - string itemType = Item?.InventorySpecs?.itemPrefabName; - - Multiplayer.LogDebug(() => $"NetworkedItem.SetupItem() {name}, {itemType}"); - - switch (itemType) - { - //Job related items - case "JobOverview": - //SetupJobOverview(); - break; + if(netId != 0) + NetId = netId; - case "JobBooklet": - //SetupJobBooklet(); - break; + trackedItem = item; + TrackedItemType = typeof(T); + UsefulItem = true; - case "JobMissingLicenseReport": - //SetupJobMissingLicenseReport(); - break; + CreatedDirty = createDirty; - case "JobDebtWarningReport": - //SetupJobDebtWarningReport(); - break; + if(Item == null) + Register(); - //Loco related items - case "lighter": - break; - - case "Shovel": - break; - - //Other interactables - case "Lantern": - break; + } - //Non interactables - default: - break; + private bool Register() + { + if (!TryGetComponent(out ItemBase itemBase)) + { + Multiplayer.LogError($"Unable to find ItemBase for {name}"); + return false; } + Item = itemBase; + itemBaseToNetworkedItem[Item] = this; + Item.Grabbed += OnGrabbed; Item.Ungrabbed += OnUngrabbed; Item.ItemInventoryStateChanged += OnItemInventoryStateChanged; + + lastPosition = Item.transform.position - WorldMover.currentMove; + lastRotation = Item.transform.rotation; + + return true; } private void OnUngrabbed(ControlImplBase obj) { Multiplayer.LogDebug(() => $"OnUngrabbed() {name}"); + GrabbedDirty = ItemGrabbed; + ItemGrabbed = false; + } private void OnGrabbed(ControlImplBase obj) { Multiplayer.LogDebug(() => $"OnGrabbed() {name}"); + GrabbedDirty = !ItemGrabbed; + ItemGrabbed = true; } private void OnItemInventoryStateChanged(ItemBase itemBase, InventoryActionType actionType, InventoryItemState itemState) { Multiplayer.LogDebug(() => $"OnItemInventoryStateChanged() {name}, InventoryActionType: {actionType}, InventoryItemState: {itemState}"); + if (actionType == InventoryActionType.Purge) + { + DroppedDirty = !ItemDropped; + ItemDropped = true; + } + } + + #region Item Value Tracking + public void RegisterTrackedValue(string key, Func valueGetter, Action valueSetter) + { + trackedValues.Add(new TrackedValue(key, valueGetter, valueSetter)); } - //private void SetupJobOverview() - //{ - // if(!TryGetComponent(out JobOverview jobOverview)) - // { - // Multiplayer.LogError($"SetupJobOverview() Could not find JobOverview"); - // return; - // } - - // if (!NetworkedJob.TryGetFromJob(jobOverview.job, out NetworkedJob networkedJob)) - // { - // Multiplayer.LogError($"SetupJobOverview() NetworkedJob not found for Job ID: {jobOverview?.job?.ID}"); - // jobOverview.DestroyJobOverview(); - // return; - // } - - // networkedJob.JobOverview = jobOverview; - // networkedJob.ValidationItem = this; - //} - - //private IEnumerator SetupJobBooklet() - //{ - // if (!TryGetComponent(out JobBooklet jobBooklet)) - // { - // Multiplayer.LogError($"SetupJobBooklet() Could not find JobBooklet"); - // yield break; - // } - - // while (jobBooklet.job == null) - // yield return new WaitForEndOfFrame(); - - // if (!NetworkedJob.TryGetFromJob(jobBooklet.job, out NetworkedJob networkedJob)) - // { - // Multiplayer.LogError($"SetupJobOverview() NetworkedJob not found for Job ID: {jobBooklet?.job?.ID}"); - // jobBooklet.DestroyJobBooklet(); - // } - - // networkedJob.JobBooklet = jobBooklet; - // networkedJob.ValidationItem = this; - //} - - private void SetupJobMissingLicenseReport() - { - if (!TryGetComponent(out JobMissingLicenseReport report)) + private bool HasDirtyValues() + { + return trackedValues.Any(tv => ((dynamic)tv).IsDirty); + } + + private Dictionary GetDirtyStateData() + { + var dirtyData = new Dictionary(); + foreach (var trackedValue in trackedValues) { - Multiplayer.LogError($"SetupJobLicenseReport() Could not find JobMissingLicenseReport"); - return; + if (((dynamic)trackedValue).IsDirty) + { + dirtyData[((dynamic)trackedValue).Key] = ((dynamic)trackedValue).GetValueAsObject(); + } } + return dirtyData; + } - if (!NetworkedJob.TryGetFromJobId(report.jobId, out NetworkedJob networkedJob)) + private void MarkValuesClean() + { + foreach (var trackedValue in trackedValues) { - Multiplayer.LogError($"SetupJobLicenseReport() NetworkedJob not found for Job ID: {report?.jobId}"); + ((dynamic)trackedValue).MarkClean(); + } + } + + private void CheckPositionChange() + { + Vector3 currentPosition = transform.position - WorldMover.currentMove; + Quaternion currentRotation = transform.rotation; + + bool positionChanged = Vector3.Distance(currentPosition, lastPosition) > PositionThreshold; + bool rotationChanged = Quaternion.Angle(currentRotation, lastRotation) > RotationThreshold; + + if (positionChanged || rotationChanged) + { + ItemPosition = new ItemPositionData + { + Position = currentPosition, + Rotation = currentRotation + }; + lastPosition = currentPosition; + lastRotation = currentRotation; + PositionDirty = true; + } + } + + private void Update() + { + ItemUpdateData snapshot; + ItemUpdateData.ItemUpdateType updateType = ItemUpdateData.ItemUpdateType.None; + + if (Item == null && Register() ==false) return; + + CheckPositionChange(); + + if (!CreatedDirty) + { + if(PositionDirty) + updateType |= ItemUpdateData.ItemUpdateType.Position; + if(DroppedDirty) + updateType |= ItemUpdateData.ItemUpdateType.ItemDropped; + if(GrabbedDirty) + updateType |= ItemUpdateData.ItemUpdateType.ItemEquipped; + if (HasDirtyValues()) + { + Multiplayer.LogDebug(GetDirtyValuesDebugString); + updateType |= ItemUpdateData.ItemUpdateType.ObjectState; + } + } + else + { + updateType = ItemUpdateData.ItemUpdateType.Create; } - networkedJob.ValidationItem = this; + snapshot = CreateUpdateData(updateType); + NetworkedItemManager.Instance.AddDirtyItemSnapshot(snapshot); + + CreatedDirty = false; + GrabbedDirty = false; + DroppedDirty = false; + PositionDirty = false; + + MarkValuesClean(); } - private void SetupJobDebtWarningReport() + + /* + private void SendStateUpdate() + { + var updateData = CreateUpdateData(ItemUpdateData.ItemUpdateType.State); + updateData.StateData = GetDirtyStateData(); + SendItemUpdate(updateData); + MarkValuesClean(); + } + */ + #endregion + + public ItemUpdateData CreateUpdateData(ItemUpdateData.ItemUpdateType updateType) { - if (!TryGetComponent(out JobMissingLicenseReport report)) + + var updateData = new ItemUpdateData { - Multiplayer.LogError($"SetupJobDebtWarningReport() Could not find SetupJobDebtWarningReport"); + UpdateType = updateType, + ItemNetId = NetId, + PrefabName = Item.name, + PositionData = ItemPosition, + Equipped = ItemGrabbed, + Dropped = ItemDropped, + States = GetDirtyStateData(), + }; + + return updateData; + } + + + protected override void OnDestroy() + { + if (UnloadWatcher.isQuitting || UnloadWatcher.isUnloading) return; + + if (NetworkLifecycle.Instance.IsHost()) + { + NetworkedItemManager.Instance.AddDirtyItemSnapshot(CreateUpdateData(ItemUpdateData.ItemUpdateType.Destroy)); + } + else + { + Multiplayer.LogWarning($"NetworkedItem.OnDestroy({name}, {NetId})\r\n{new System.Diagnostics.StackTrace()}"); } - if (!NetworkedJob.TryGetFromJobId(report.jobId, out NetworkedJob networkedJob)) + base.OnDestroy(); + if (Item != null) { - Multiplayer.LogError($"SetupJobDebtWarningReport() NetworkedJob not found for Job ID: {report?.jobId}"); - return; + Item.Grabbed -= OnGrabbed; + Item.Ungrabbed -= OnUngrabbed; + Item.ItemInventoryStateChanged -= OnItemInventoryStateChanged; + itemBaseToNetworkedItem.Remove(Item); } - networkedJob.ValidationItem = this; } + public string GetDirtyValuesDebugString() + { + var dirtyValues = trackedValues.Where(tv => ((dynamic)tv).IsDirty).ToList(); + if (dirtyValues.Count == 0) + { + return "No dirty values"; + } + + StringBuilder sb = new StringBuilder(); + sb.AppendLine($"Dirty values for NetworkedItem {name}, NetId {NetId}:"); + foreach (var value in dirtyValues) + { + sb.AppendLine(((dynamic)value).GetDebugString()); + } + return sb.ToString(); + } } diff --git a/Multiplayer/Components/Networking/World/NetworkedItemManager.cs b/Multiplayer/Components/Networking/World/NetworkedItemManager.cs new file mode 100644 index 0000000..30736ed --- /dev/null +++ b/Multiplayer/Components/Networking/World/NetworkedItemManager.cs @@ -0,0 +1,66 @@ +using System.Collections.Generic; +using System.Linq; +using DV.Utils; +using UnityEngine; +using JetBrains.Annotations; +using Multiplayer.Networking.Data; +using Multiplayer.Networking.Packets.Clientbound.Train; +using Multiplayer.Utils; +using Multiplayer.Components.Networking.World; + +namespace Multiplayer.Components.Networking.Train; + +public class NetworkedItemManager : SingletonBehaviour +{ + private List DirtyItems = new List(); + + protected override void Awake() + { + base.Awake(); + if (!NetworkLifecycle.Instance.IsHost()) + return; + + NetworkLifecycle.Instance.OnTick += Common_OnTick; + } + + protected override void OnDestroy() + { + base.OnDestroy(); + if (UnloadWatcher.isQuitting) + return; + + NetworkLifecycle.Instance.OnTick -= Common_OnTick; + } + + public void AddDirtyItemSnapshot(ItemUpdateData item) + { + if(! DirtyItems.Contains(item)) + DirtyItems.Add(item); + } + + #region Common + + private void Common_OnTick(uint tick) + { + if(DirtyItems.Count == 0) + return; + + if(NetworkLifecycle.Instance.IsHost()) + { + NetworkLifecycle.Instance.Server.SendItemsChangePacket(DirtyItems); + } + else + { + NetworkLifecycle.Instance.Client.SendItemsChangePacket(DirtyItems); + } + + DirtyItems.Clear(); + } + #endregion + + [UsedImplicitly] + public new static string AllowAutoCreate() + { + return $"[{nameof(NetworkedItemManager)}]"; + } +} diff --git a/Multiplayer/Components/Networking/World/NetworkedStationController.cs b/Multiplayer/Components/Networking/World/NetworkedStationController.cs index caf4d34..4519e64 100644 --- a/Multiplayer/Components/Networking/World/NetworkedStationController.cs +++ b/Multiplayer/Components/Networking/World/NetworkedStationController.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using System.Linq; using DV.Booklets; +using DV.CabControls; +using DV.CabControls.Spec; using DV.Logic.Job; using DV.ServicePenalty; using DV.Utils; @@ -10,10 +12,7 @@ using Multiplayer.Components.Networking.Train; using Multiplayer.Networking.Data; using Multiplayer.Utils; -using Unity.Jobs; using UnityEngine; -using UnityEngine.Assertions.Must; - namespace Multiplayer.Components.Networking.World; @@ -168,56 +167,24 @@ private IEnumerator WaitForLogicStation() } } - //Adding job on server + #region Server + //Adding job public void AddJob(Job job) { NetworkedJob networkedJob = new GameObject($"NetworkedJob {job.ID}").AddComponent(); - networkedJob.Job = job; + networkedJob.Initialize(job, this); NetworkedJobs.Add(networkedJob); + NewJobs.Add(networkedJob); //Setup handlers - job.JobTaken += OnJobTaken; - job.JobAbandoned += OnJobAbandoned; - job.JobCompleted += OnJobCompleted; - job.JobExpired += OnJobExpired; - - networkedJob.OverviewGenerated += OnOverviewGeneration; + networkedJob.OnJobDirty += OnJobDirty; } - public void OnOverviewGeneration(NetworkedJob job) + private void OnJobDirty(NetworkedJob job) { - if(!DirtyJobs.Contains(job)) + if (!DirtyJobs.Contains(job)) DirtyJobs.Add(job); - - } - - private void OnJobTaken(Job job, bool viaLoadGame) - { - if (viaLoadGame) - return; - - Multiplayer.Log($"NetworkedStationController.OnJobTaken({job.ID})"); - if(NetworkedJob.TryGetFromJob(job, out NetworkedJob networkedJob)) - DirtyJobs.Add(networkedJob); - } - - private void OnJobAbandoned(Job job) - { - if (NetworkedJob.TryGetFromJob(job, out NetworkedJob networkedJob)) - DirtyJobs.Add(networkedJob); - } - - private void OnJobCompleted(Job job) - { - if (NetworkedJob.TryGetFromJob(job, out NetworkedJob networkedJob)) - DirtyJobs.Add(networkedJob); - } - - private void OnJobExpired(Job job) - { - if (NetworkedJob.TryGetFromJob(job, out NetworkedJob networkedJob)) - DirtyJobs.Add(networkedJob); } private void Server_OnTick(uint tick) @@ -237,93 +204,55 @@ private void Server_OnTick(uint tick) DirtyJobs.Clear(); } } - + #endregion Server #region Client public void AddJobs(JobData[] jobs) { - NetworkLifecycle.Instance.Client.Log($"AddJobs() jobs[] exists: {jobs != null}, job count: {jobs?.Count()}"); - - foreach (JobData job in jobs) + foreach (JobData jobData in jobs) { - NetworkLifecycle.Instance.Client.Log($"AddJobs() inloop"); - NetworkLifecycle.Instance.Client.Log($"AddJobs() ID: {job?.ID ?? ""}, netID: {job?.NetID}, task count: {job?.Tasks?.Count()}"); + Job newJob = CreateJobFromJobData(jobData); + NetworkedJob networkedJob = CreateNetworkedJob(newJob, jobData.NetID); - // Convert TaskNetworkData to Task objects - List tasks = new List(); - foreach (TaskNetworkData taskData in job.Tasks) + NetworkedJobs.Add(networkedJob); + + if (networkedJob.Job.State == DV.ThingTypes.JobState.Available) { - if (NetworkLifecycle.Instance.IsHost()) + StationController.logicStation.AddJobToStation(newJob); + StationController.processedNewJobs.Add(newJob); + + if (jobData.ItemNetID != 0) { - Task test = taskData.ToTask(); - continue; + GenerateOverview(networkedJob, jobData.ItemNetID, jobData.ItemPosition); } - - NetworkLifecycle.Instance.Client.Log($"AddJobs() ID: {job?.ID}, task type: {taskData.TaskType}"); - tasks.Add(taskData.ToTask()); } - NetworkLifecycle.Instance.Client.Log($"AddJobs() ID: {job?.ID}, netID: {job?.NetID}, StationsChainData"); - // Create StationsChainData from ChainData - StationsChainData chainData = new StationsChainData( - job.ChainData.ChainOriginYardId, - job.ChainData.ChainDestinationYardId - ); - - - NetworkLifecycle.Instance.Client.Log($"AddJobs() ID: {job?.ID}, netID: {job?.NetID}, newJob"); - // Create a new local Job - Job newJob = new Job( - tasks, - job.JobType, - job.TimeLimit, - job.InitialWage, - chainData, - job.ID, - job.RequiredLicenses - ); - - NetworkLifecycle.Instance.Client.Log($"AddJobs() ID: {job?.ID}, netID: {job?.NetID}, properties"); - // Set additional properties - newJob.startTime = job.StartTime; - newJob.finishTime = job.FinishTime; - newJob.State = job.State; - - NetworkLifecycle.Instance.Client.Log($"AddJobs() ID: {job?.ID}, netID: {job?.NetID}, netjob"); - - // Create a new NetworkedJob - NetworkedJob networkedJob = new GameObject($"NetworkedJob {newJob.ID}").AddComponent(); - networkedJob.NetId = job.NetID; - networkedJob.Job = newJob; - networkedJob.Station = this; - //networkedJob.playerID = job.PlayerId; - - NetworkLifecycle.Instance.Client.Log($"AddJobs() ID: {job?.ID}, netID: {job?.NetID}, NetJob Add"); - NetworkedJobs.Add(networkedJob); - - NetworkLifecycle.Instance.Client.Log($"AddJobs() ID: {job?.ID}, netID: {job?.NetID}, CarPlates"); - // Start coroutine to update car plates - StartCoroutine(UpdateCarPlates(tasks, newJob.ID)); + StartCoroutine(UpdateCarPlates(newJob.tasks, newJob.ID)); - //If the job is not owned by anyone, we can add it to the station - //if(networkedJob.OwnedBy == Guid.Empty) - NetworkLifecycle.Instance.Client.Log($"AddJobs() ID: {job?.ID}, netID: {job?.NetID}, AddJobToStation()"); - StationController.logicStation.AddJobToStation(newJob); - StationController.processedNewJobs.Add(newJob); + Multiplayer.Log($"Added NetworkedJob {newJob.ID} to NetworkedStationController {StationController.logicStation.ID}"); + } + } - NetworkLifecycle.Instance.Client.Log($"AddJobs() ID: {job?.ID}, netID: {job?.NetID}, job state {job.State}, itemNetId {job.ItemNetID}"); + private Job CreateJobFromJobData(JobData jobData) + { + List tasks = jobData.Tasks.Select(taskData => taskData.ToTask()).ToList(); + StationsChainData chainData = new StationsChainData(jobData.ChainData.ChainOriginYardId, jobData.ChainData.ChainDestinationYardId); - //start coroutine for generating overviews and booklets - NetworkLifecycle.Instance.Client.Log($"AddJobs() {newJob?.ID} Generating Overview {(newJob.State == DV.ThingTypes.JobState.Available && job.ItemNetID != 0)}"); - if (newJob.State == DV.ThingTypes.JobState.Available && job.ItemNetID != 0) - GenerateOverview(networkedJob, job.ItemNetID, job.ItemPosition); + Job newJob = new Job(tasks, jobData.JobType, jobData.TimeLimit, jobData.InitialWage, chainData, jobData.ID, jobData.RequiredLicenses); + newJob.startTime = jobData.StartTime; + newJob.finishTime = jobData.FinishTime; + newJob.State = jobData.State; - // Log the addition of the new job - NetworkLifecycle.Instance.Client.Log($"AddJobs() {newJob?.ID} to NetworkedStationController {StationController?.logicStation?.ID}"); - } + return newJob; + } - //allow booklets to be created - StationController.attemptJobOverviewGeneration = true; + private NetworkedJob CreateNetworkedJob(Job job, ushort netId) + { + NetworkedJob networkedJob = new GameObject($"NetworkedJob {job.ID}").AddComponent(); + networkedJob.NetId = netId; + networkedJob.Initialize(job, this); + networkedJob.OnJobDirty += OnJobDirty; + return networkedJob; } public void UpdateJobs(JobUpdateStruct[] jobs) @@ -333,99 +262,115 @@ public void UpdateJobs(JobUpdateStruct[] jobs) if (!NetworkedJob.Get(job.JobNetID, out NetworkedJob netJob)) continue; - JobValidator validator = null; - if(job.ItemNetID != 0 && job.ValidationStationId != 0) - if (Get(job.ValidationStationId, out var netStation)) - validator = netStation.JobValidator; + UpdateJobState(netJob, job); + UpdateJobOverview(netJob, job); - Multiplayer.Log($"NetworkedStation.UpdateJobs() jobNetId: {job.JobNetID}, Validator found: {validator != null}"); + netJob.Job.startTime = job.StartTime; + netJob.Job.finishTime = job.FinishTime; + } + } - //state change updates - if (netJob.Job.State != job.JobState) - { - netJob.Job.State = job.JobState; - bool printed = false; + private void UpdateJobState(NetworkedJob netJob, JobUpdateStruct job) + { + if (netJob.Job.State != job.JobState) + { + netJob.Job.State = job.JobState; + HandleJobStateChange(netJob, job); + } + } - switch (netJob.Job.State) - { - case DV.ThingTypes.JobState.InProgress: - availableJobs.Remove(netJob.Job); - takenJobs.Add(netJob.Job); - - netJob.JobBooklet = BookletCreator.CreateJobBooklet(netJob.Job, validator.bookletPrinter.spawnAnchor.position, validator.bookletPrinter.spawnAnchor.rotation, WorldMover.OriginShiftParent, true); - - netJob.ValidationItem.NetId = job.ItemNetID; - printed = true; - netJob.JobOverview.DestroyJobOverview(); - break; - case DV.ThingTypes.JobState.Completed: - takenJobs.Remove(netJob.Job); - completedJobs.Add(netJob.Job); - - DisplayableDebt displayableDebt = SingletonBehaviour.Instance.LastStagedJobDebt; - netJob.JobReport = BookletCreator.CreateJobReport(netJob.Job, displayableDebt, validator.bookletPrinter.spawnAnchor.position, validator.bookletPrinter.spawnAnchor.rotation, WorldMover.OriginShiftParent); - - netJob.ValidationItem.NetId = job.ItemNetID; - printed = true; - netJob.JobBooklet.DestroyJobBooklet(); - break; - case DV.ThingTypes.JobState.Abandoned: - takenJobs.Remove(netJob.Job); - abandonedJobs.Add(netJob.Job); - - //netJob.JobExpiredReport = BookletCreator.CreateJobExpiredReport(netJob.Job, validator.bookletPrinter.spawnAnchor.position, validator.bookletPrinter.spawnAnchor.rotation, WorldMover.OriginShiftParent); - //netJob.ValidationItem.NetId = job.ItemNetID; - //printed = true; - - break; - case DV.ThingTypes.JobState.Expired: - if(availableJobs.Contains(netJob.Job)) - availableJobs.Remove(netJob.Job); - - //netJob.JobExpiredReport = BookletCreator.CreateJobExpiredReport(netJob.Job, validator.bookletPrinter.spawnAnchor.position, validator.bookletPrinter.spawnAnchor.rotation, WorldMover.OriginShiftParent); - //netJob.ValidationItem.NetId = job.ItemNetID; - //printed = true; - - break; - default: - NetworkLifecycle.Instance.Client.LogError($"NetworkedStation.UpdateJobs() Unrecognised Job State for JobId: {job.JobNetID}, {netJob.Job.ID}"); - break; - } + private void UpdateJobOverview(NetworkedJob netJob, JobUpdateStruct job) + { + Multiplayer.Log($"UpdateJobOverview({netJob.Job.ID}) State: {job.JobState}, ItemNetId: {job.ItemNetID}"); + if (job.JobState == DV.ThingTypes.JobState.Available && job.ItemNetID != 0) + { + if (netJob.JobOverview == null) + GenerateOverview(netJob, job.ItemNetID, job.ItemPositionData); + /* + else + netJob.JobOverview.NetId = job.ItemNetID; + */ + } + } - if (printed) - { - Multiplayer.Log($"NetworkedStation.UpdateJobs() jobNetId: {job.JobNetID}, Playing sounds"); - netJob.ValidatorResponseReceived = true; - netJob.ValidationAccepted = true; - validator.jobValidatedSound.Play(validator.bookletPrinter.spawnAnchor.position, 1f, 1f, 0f, 1f, 500f, default(AudioSourceCurves), null, validator.transform, false, 0f, null); - validator.bookletPrinter.Print(false); - } - } + private void HandleJobStateChange(NetworkedJob netJob, JobUpdateStruct job) + { + JobValidator validator = null; + NetworkedItem netItem; - //job overview generation update - if(job.JobState == DV.ThingTypes.JobState.Available && job.ItemNetID !=0) - { - - if (netJob.JobOverview == null) - { - //create overview - Multiplayer.LogDebug(()=>$"NetworkedStation.UpdateJobs() Creating JobOverview"); - if (job.JobState == DV.ThingTypes.JobState.Available && job.ItemNetID != 0) - GenerateOverview(netJob, job.ItemNetID, job.ItemPositionData); - } - else - { - Multiplayer.LogDebug(() => $"NetworkedStation.UpdateJobs() Setting JobOverview"); - netJob.ValidationItem.NetId = job.ItemNetID; - } - } + if (job.ItemNetID != 0 && job.ValidationStationId != 0) + if (Get(job.ValidationStationId, out var netStation)) + validator = netStation.JobValidator; - //generic update - netJob.Job.startTime = job.StartTime; - netJob.Job.finishTime = job.FinishTime; + if ((netJob.Job.State == DV.ThingTypes.JobState.InProgress || + netJob.Job.State == DV.ThingTypes.JobState.Completed) && + validator == null) + { + NetworkLifecycle.Instance.Client.LogError($"NetworkedStation.UpdateJobs() jobNetId: {job.JobNetID}, Validator required and not found!"); + return; } - } + bool printed = false; + switch (netJob.Job.State) + { + case DV.ThingTypes.JobState.InProgress: + availableJobs.Remove(netJob.Job); + takenJobs.Add(netJob.Job); + + JobBooklet jobBooklet = BookletCreator.CreateJobBooklet(netJob.Job, validator.bookletPrinter.spawnAnchor.position, validator.bookletPrinter.spawnAnchor.rotation, WorldMover.OriginShiftParent, true); + + netItem = jobBooklet.GetOrAddComponent(); + netItem.Initialize(jobBooklet, job.ItemNetID, false); + netJob.JobBooklet = netItem; + printed = true; + + netJob.JobOverview?.GetTrackedItem()?.DestroyJobOverview(); + + break; + + case DV.ThingTypes.JobState.Completed: + takenJobs.Remove(netJob.Job); + completedJobs.Add(netJob.Job); + + DisplayableDebt displayableDebt = SingletonBehaviour.Instance.LastStagedJobDebt; + JobReport jobReport = BookletCreator.CreateJobReport(netJob.Job, displayableDebt, validator.bookletPrinter.spawnAnchor.position, validator.bookletPrinter.spawnAnchor.rotation, WorldMover.OriginShiftParent); + + netItem = jobReport.GetOrAddComponent(); + netItem.Initialize(jobReport, job.ItemNetID, false); + netJob.AddReport(netItem); + printed = true; + + netJob.JobBooklet?.GetTrackedItem()?.DestroyJobBooklet(); + + break; + + case DV.ThingTypes.JobState.Abandoned: + takenJobs.Remove(netJob.Job); + abandonedJobs.Add(netJob.Job); + break; + + case DV.ThingTypes.JobState.Expired: + //if (availableJobs.Contains(netJob.Job)) + // availableJobs.Remove(netJob.Job); + + netJob.Job.ExpireJob(); + StationController.ClearAvailableJobOverviewGOs(); //todo: better logic when players can hold items + break; + + default: + NetworkLifecycle.Instance.Client.LogError($"NetworkedStation.UpdateJobs() Unrecognised Job State for JobId: {job.JobNetID}, {netJob.Job.ID}"); + break; + } + + if (printed && validator != null) + { + Multiplayer.Log($"NetworkedStation.UpdateJobs() jobNetId: {job.JobNetID}, Playing sounds"); + netJob.ValidatorResponseReceived = true; + netJob.ValidationAccepted = true; + validator.jobValidatedSound.Play(validator.bookletPrinter.spawnAnchor.position, 1f, 1f, 0f, 1f, 500f, default(AudioSourceCurves), null, validator.transform, false, 0f, null); + validator.bookletPrinter.Print(false); + } + } public void RemoveJob(NetworkedJob job) { if (availableJobs.Contains(job.Job)) @@ -440,8 +385,10 @@ public void RemoveJob(NetworkedJob job) if (abandonedJobs.Contains(job.Job)) abandonedJobs.Remove(job.Job); - job.JobOverview?.DestroyJobOverview(); - job.JobBooklet?.DestroyJobBooklet(); + job.JobOverview?.GetTrackedItem()?.DestroyJobOverview(); + job.JobBooklet?.GetTrackedItem()?.DestroyJobBooklet(); + + job.ClearReports(); NetworkedJobs.Remove(job); GameObject.Destroy(job); @@ -454,84 +401,64 @@ public static IEnumerator UpdateCarPlates(List tasks, string UpdateCarPlatesRecursive(tasks, jobId, ref cars); - if (cars != null) + if (cars == null) + yield break; + + foreach (Car car in cars) { - //Multiplayer.Log("NetworkedStation.UpdateCarPlates() Cars count: " + cars.Count); - foreach (Car car in cars) + TrainCar trainCar = null; + int loopCtr = 0; + while (!NetworkedTrainCar.GetTrainCarFromTrainId(car.ID, out trainCar)) { - //Multiplayer.Log("NetworkedStation.UpdateCarPlates() Car: " + car.ID); - - TrainCar trainCar = null; - int loopCtr = 0; - while (!NetworkedTrainCar.GetTrainCarFromTrainId(car.ID, out trainCar)) + loopCtr++; + if (loopCtr > 5000) { - loopCtr++; - if (loopCtr > 5000) - { - //Multiplayer.Log("NetworkedStation.UpdateCarPlates() TimeOut"); - break; - } - - - yield return null; - } + break; + } - trainCar?.UpdateJobIdOnCarPlates(jobId); + yield return null; } + + trainCar?.UpdateJobIdOnCarPlates(jobId); } } private static void UpdateCarPlatesRecursive(List tasks, string jobId, ref List cars) { - //Multiplayer.Log("NetworkedStation.UpdateCarPlatesRecursive() Starting"); foreach (Task task in tasks) { if (task is WarehouseTask) - { - //Multiplayer.Log("NetworkedStation.UpdateCarPlatesRecursive() WarehouseTask"); cars = cars.Union(((WarehouseTask)task).cars).ToList(); - } else if (task is TransportTask) - { - //Multiplayer.Log("NetworkedStation.UpdateCarPlatesRecursive() TransportTask"); cars = cars.Union(((TransportTask)task).cars).ToList(); - } else if (task is SequentialTasks) { - //Multiplayer.Log("NetworkedStation.UpdateCarPlatesRecursive() SequentialTasks"); List seqTask = new(); for (LinkedListNode node = ((SequentialTasks)task).tasks.First; node != null; node = node.Next) { - //Multiplayer.Log($"NetworkedStation.UpdateCarPlatesRecursive() SequentialTask Adding node"); seqTask.Add(node.Value); } //drill down UpdateCarPlatesRecursive(seqTask, jobId, ref cars); - //Multiplayer.Log($"NetworkedStation.UpdateCarPlatesRecursive() SequentialTask RETURNED"); } else if (task is ParallelTasks) - { - //drill down UpdateCarPlatesRecursive(((ParallelTasks)task).tasks, jobId, ref cars); - } else - { throw new ArgumentException("NetworkedStation.UpdateCarPlatesRecursive() Unknown task type: " + task.GetType()); - } } - - //Multiplayer.Log("NetworkedStation.UpdateCarPlatesRecursive() Returning"); } private void GenerateOverview(NetworkedJob networkedJob, ushort itemNetId, ItemPositionData posData) { - networkedJob.JobOverview = BookletCreator_JobOverview.Create(networkedJob.Job, posData.Position + WorldMover.currentMove, posData.Rotation); - NetworkedItem netItem = networkedJob.JobOverview.GetOrAddComponent(); - netItem.NetId = itemNetId; - networkedJob.ValidationItem = netItem; - StationController.spawnedJobOverviews.Add(networkedJob.JobOverview); + Multiplayer.Log($"GenerateOverview({networkedJob.Job.ID}) Position: {posData.Position}, Less currentMove: {posData.Position + WorldMover.currentMove} "); + JobOverview jobOverview = BookletCreator_JobOverview.Create(networkedJob.Job, posData.Position + WorldMover.currentMove, posData.Rotation,WorldMover.OriginShiftParent); + + NetworkedItem netItem = jobOverview.GetOrAddComponent(); + netItem.Initialize(jobOverview, itemNetId, false); + networkedJob.JobOverview = netItem; + StationController.spawnedJobOverviews.Add(jobOverview); } private void OnDisable() diff --git a/Multiplayer/Networking/Data/ItemPositionData.cs b/Multiplayer/Networking/Data/ItemPositionData.cs index 9006c33..e8d34e7 100644 --- a/Multiplayer/Networking/Data/ItemPositionData.cs +++ b/Multiplayer/Networking/Data/ItemPositionData.cs @@ -9,15 +9,14 @@ public struct ItemPositionData { public Vector3 Position; public Quaternion Rotation; - //public bool held; public static ItemPositionData FromItem(NetworkedItem item) { + //Multiplayer.Log($"ItemPositionData.FromItem() Position: {item.Item.transform.position}, Less currentMove: {item.Item.transform.position - WorldMover.currentMove } "); return new ItemPositionData { Position = item.Item.transform.position - WorldMover.currentMove, Rotation = item.Item.transform.rotation, - //held = item.Item. //Todo: track if item is held by a player }; } diff --git a/Multiplayer/Networking/Data/ItemUpdateData.cs b/Multiplayer/Networking/Data/ItemUpdateData.cs new file mode 100644 index 0000000..7f53093 --- /dev/null +++ b/Multiplayer/Networking/Data/ItemUpdateData.cs @@ -0,0 +1,152 @@ +using LiteNetLib.Utils; +using Multiplayer.Components.Networking.World; +using System; +using System.Collections.Generic; + +namespace Multiplayer.Networking.Data; + +public class ItemUpdateData +{ + [Flags] + public enum ItemUpdateType : byte + { + None = 0, + Create = 1, + Destroy = 2, + Position = 4, + ItemDropped = 8, + ItemEquipped = 16, + ObjectState = 32, + } + + public ItemUpdateType UpdateType { get; set; } + public ushort ItemNetId { get; set; } + public string PrefabName { get; set; } + public ItemPositionData PositionData { get; set; } + + public bool Dropped { get; set; } + public bool Equipped { get; set; } + public ushort Player { get; set; } + public Dictionary States { get; set; } + + public void Serialize(NetDataWriter writer) + { + writer.Put((byte)UpdateType); + writer.Put(ItemNetId); + + if(UpdateType == ItemUpdateType.Destroy) + return; + + if (UpdateType.HasFlag(ItemUpdateType.Create)) + writer.Put(PrefabName); + + if (UpdateType.HasFlag(ItemUpdateType.Position) || UpdateType.HasFlag(ItemUpdateType.ItemDropped) || UpdateType.HasFlag(ItemUpdateType.Create)) + ItemPositionData.Serialize(writer, PositionData); + + if (UpdateType.HasFlag(ItemUpdateType.ItemDropped) || UpdateType.HasFlag(ItemUpdateType.Create)) + writer.Put(Dropped); + + if (UpdateType.HasFlag(ItemUpdateType.ItemEquipped) || UpdateType.HasFlag(ItemUpdateType.Create)) + writer.Put(Equipped); + + if (UpdateType.HasFlag(ItemUpdateType.ItemDropped) || UpdateType.HasFlag(ItemUpdateType.ItemEquipped) || UpdateType.HasFlag(ItemUpdateType.Create)) + writer.Put(Player); + + if (UpdateType.HasFlag(ItemUpdateType.ObjectState) || UpdateType.HasFlag(ItemUpdateType.Create)) + { + if (States == null) + writer.Put(0); + else + { + writer.Put(States.Count); + foreach(var state in States) + { + writer.Put(state.Key); + SerializeTrackedValue(writer, state.Value); + } + } + } + } + + public void Deserialize(NetDataReader reader) + { + UpdateType = (ItemUpdateType)reader.GetByte(); + ItemNetId = reader.GetUShort(); + + if (UpdateType == ItemUpdateType.Destroy) + return; + + if (UpdateType == ItemUpdateType.Create) + PrefabName = reader.GetString(); + + if (UpdateType.HasFlag(ItemUpdateType.Position) || UpdateType.HasFlag(ItemUpdateType.ItemDropped) || UpdateType.HasFlag(ItemUpdateType.Create)) + { + PositionData = ItemPositionData.Deserialize(reader); + } + + if (UpdateType.HasFlag(ItemUpdateType.ItemDropped) || UpdateType.HasFlag(ItemUpdateType.Create)) + Dropped = reader.GetBool(); + + if (UpdateType.HasFlag(ItemUpdateType.ItemEquipped) || UpdateType.HasFlag(ItemUpdateType.Create)) + Equipped = reader.GetBool(); + + if (UpdateType.HasFlag(ItemUpdateType.ItemDropped) || UpdateType.HasFlag(ItemUpdateType.ItemEquipped) || UpdateType.HasFlag(ItemUpdateType.Create)) + Player = reader.GetUShort(); + + if (UpdateType.HasFlag(ItemUpdateType.ObjectState) || UpdateType.HasFlag(ItemUpdateType.Create)) + { + States = new Dictionary(); + + int stateCount = reader.GetInt(); + for (int i = 0; i < stateCount; i++) + { + string key = reader.GetString(); + object value = DeserializeTrackedValue(reader); + States[key] = value; + } + } + } + + private void SerializeTrackedValue(NetDataWriter writer, object value) + { + if (value is bool boolValue) + { + writer.Put((byte)0); + writer.Put(boolValue); + } + else if (value is int intValue) + { + writer.Put((byte)1); + writer.Put(intValue); + } + else if (value is float floatValue) + { + writer.Put((byte)2); + writer.Put(floatValue); + } + else if (value is string stringValue) + { + writer.Put((byte)3); + writer.Put(stringValue); + } + else + { + throw new NotSupportedException($"ItemUpdateData.SerializeTrackedValue({ItemNetId}, {PrefabName??""}) Unsupported type for serialization: {value.GetType()}"); + } + } + + private object DeserializeTrackedValue(NetDataReader reader) + { + byte typeCode = reader.GetByte(); + switch (typeCode) + { + case 0: return reader.GetBool(); + case 1: return reader.GetInt(); + case 2: return reader.GetFloat(); + case 3: return reader.GetString(); + + default: + throw new NotSupportedException($"ItemUpdateData.DeserializeTrackedValue({ItemNetId}, {PrefabName ?? ""}) Unsupported type code for deserialization: {typeCode}"); + } + } +} diff --git a/Multiplayer/Networking/Data/JobData.cs b/Multiplayer/Networking/Data/JobData.cs index 6b178b2..2776a6a 100644 --- a/Multiplayer/Networking/Data/JobData.cs +++ b/Multiplayer/Networking/Data/JobData.cs @@ -29,25 +29,35 @@ public class JobData public static JobData FromJob(NetworkedStationController netStation, NetworkedJob networkedJob) { Job job = networkedJob.Job; + ushort itemNetId = 0; ItemPositionData itemPos = new ItemPositionData(); + Multiplayer.Log($"JobData.FromJob({netStation.name}, {job.ID}, {networkedJob.Job.State})"); + if (networkedJob.Job.State == JobState.Available) { - JobOverview jobOverview = netStation.StationController.spawnedJobOverviews.Where(jo => jo.job == job).FirstOrDefault(); - if (jobOverview != default(JobOverview)) + if (networkedJob.JobOverview != null) { - NetworkedItem netItem = jobOverview.GetComponent(); - if (netItem != null) - { - itemNetId = netItem.NetId; - itemPos = ItemPositionData.FromItem(netItem); - } + itemNetId = networkedJob.JobOverview.NetId; + itemPos = ItemPositionData.FromItem(networkedJob.JobOverview); } - }else if(job.State == JobState.InProgress || job.State == JobState.Completed) + } + else if (job.State == JobState.InProgress) { - itemNetId = networkedJob.ValidationItem.NetId; - itemPos = ItemPositionData.FromItem(networkedJob.ValidationItem); + if (networkedJob.JobBooklet != null) + { + itemNetId = networkedJob.JobBooklet.NetId; + itemPos = ItemPositionData.FromItem(networkedJob.JobBooklet); + } + } + else if(job.State == JobState.Completed) + { + if (networkedJob.JobReport != null) + { + itemNetId = networkedJob.JobReport.NetId; + itemPos = ItemPositionData.FromItem(networkedJob.JobReport); + } } return new JobData diff --git a/Multiplayer/Networking/Data/TrackedValue.cs b/Multiplayer/Networking/Data/TrackedValue.cs new file mode 100644 index 0000000..d9f8485 --- /dev/null +++ b/Multiplayer/Networking/Data/TrackedValue.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; + +namespace Multiplayer.Networking.Data; + +public class TrackedValue +{ + private T lastSentValue; + private Func valueGetter; + private Action valueSetter; + public string Key { get; } + + public TrackedValue(string key, Func valueGetter, Action valueSetter) + { + Key = key; + this.valueGetter = valueGetter; + this.valueSetter = valueSetter; + lastSentValue = valueGetter(); + } + + public bool IsDirty => !EqualityComparer.Default.Equals(CurrentValue, lastSentValue); + + public T CurrentValue + { + get => valueGetter(); + set + { + valueSetter(value); + lastSentValue = value; + } + } + + public void MarkClean() + { + lastSentValue = CurrentValue; + } + + public object GetValueAsObject() => CurrentValue; + + public void SetValueFromObject(object value) + { + if (value is T typedValue) + { + CurrentValue = typedValue; + } + else + { + throw new ArgumentException($"Value type mismatch. Expected {typeof(T)}, got {value.GetType()}"); + } + } + + public string GetDebugString() + { + return $"{Key}: {lastSentValue} -> {CurrentValue}"; + } + +} diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index c400ed2..5eac496 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -36,6 +36,7 @@ using UnityModManagerNet; using Object = UnityEngine.Object; using Multiplayer.Networking.Packets.Serverbound.Train; +using System.Linq; namespace Multiplayer.Networking.Listeners; @@ -124,7 +125,8 @@ protected override void Subscribe() netPacketProcessor.SubscribeReusable(OnClientboundJobsUpdatePacket); netPacketProcessor.SubscribeReusable(OnClientboundJobsCreatePacket); netPacketProcessor.SubscribeReusable(OnClientboundJobValidateResponsePacket); - netPacketProcessor.SubscribeReusable(OnCommonChatPacket); + netPacketProcessor.SubscribeReusable(OnCommonChatPacket); + netPacketProcessor.SubscribeReusable(OnCommonItemChangePacket); } #region Net Events @@ -772,7 +774,32 @@ private void OnClientboundJobValidateResponsePacket(ClientboundJobValidateRespon GameObject.Destroy(networkedJob.gameObject); } - + + private void OnCommonItemChangePacket(CommonItemChangePacket packet, NetPeer peer) + { + Multiplayer.LogDebug(() => $"OnCommonItemChangePacket({packet.Items.Count}, {peer.Id})"); + + string debug = ""; + + foreach (var item in packet.Items) + { + debug += "UpdateType: {" + item.UpdateType + "}"; + debug += "itemNetId: " + item.ItemNetId; + debug += "PrefabName: " + item.PrefabName; + debug += "Equipped: " + item.Equipped; + debug += "Dropped: " + item.Dropped; + debug += "Position: " + item.PositionData.Position; + debug += "Rotation: " + item.PositionData.Rotation; + + debug += "States:"; + + foreach (var state in item.States) + debug += "\r\n\t" + state.Key + ": " + state.Value; + } + + Multiplayer.LogDebug(() => debug); + } + #endregion #region Senders @@ -1073,5 +1100,12 @@ public void SendChat(string message) }, DeliveryMethod.ReliableUnordered); } + public void SendItemsChangePacket(List items, NetPeer peer = null) + { + Multiplayer.Log($"Sending SendItemsChangePacket with {items.Count()} items"); + SendPacketToServer(new CommonItemChangePacket { Items = items }, + DeliveryMethod.ReliableUnordered); + } + #endregion } diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index e68fcef..cd965f7 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -133,6 +133,7 @@ protected override void Subscribe() netPacketProcessor.SubscribeReusable(OnServerboundJobValidateRequestPacket); netPacketProcessor.SubscribeReusable(OnCommonChatPacket); netPacketProcessor.SubscribeReusable(OnUnconnectedPingPacket); + netPacketProcessor.SubscribeReusable(OnCommonItemChangePacket); } private void OnLoaded() @@ -388,13 +389,20 @@ public void SendDebtStatus(bool hasDebt) public void SendJobsCreatePacket(NetworkedStationController networkedStation, NetworkedJob[] jobs, DeliveryMethod method = DeliveryMethod.ReliableSequenced ) { Multiplayer.Log($"Sending JobsCreatePacket for stationNetId {networkedStation.NetId} with {jobs.Count()} jobs"); - SendPacketToAll(ClientboundJobsCreatePacket.FromNetworkedJobs(networkedStation, jobs), method); + SendPacketToAll(ClientboundJobsCreatePacket.FromNetworkedJobs(networkedStation, jobs), method, selfPeer); } public void SendJobsUpdatePacket(ushort stationNetId, NetworkedJob[] jobs, NetPeer peer = null) { Multiplayer.Log($"Sending JobsUpdatePacket for stationNetId {stationNetId} with {jobs.Count()} jobs"); - SendPacketToAll(ClientboundJobsUpdatePacket.FromNetworkedJobs(stationNetId, jobs), DeliveryMethod.ReliableUnordered); + SendPacketToAll(ClientboundJobsUpdatePacket.FromNetworkedJobs(stationNetId, jobs), DeliveryMethod.ReliableUnordered,selfPeer); + } + + public void SendItemsChangePacket(List items, NetPeer peer = null) + { + Multiplayer.Log($"Sending SendItemsChangePacket with {items.Count()} items"); + SendPacketToAll(new CommonItemChangePacket { Items = items }, + DeliveryMethod.ReliableUnordered, selfPeer); } public void SendChat(string message, NetPeer exclude = null) @@ -899,7 +907,7 @@ private void OnServerboundLicensePurchaseRequestPacket(ServerboundLicensePurchas private void OnServerboundJobValidateRequestPacket(ServerboundJobValidateRequestPacket packet, NetPeer peer) { - LogWarning($"OnServerboundJobValidateRequestPacket(): {packet.JobNetId}"); + Log($"OnServerboundJobValidateRequestPacket(): {packet.JobNetId}"); if (!NetworkedJob.Get(packet.JobNetId, out NetworkedJob networkedJob)) { @@ -926,13 +934,15 @@ private void OnServerboundJobValidateRequestPacket(ServerboundJobValidateRequest switch (packet.validationType) { case ValidationType.JobOverview: - networkedStationController.JobValidator.ProcessJobOverview(networkedJob.JobOverview); + networkedStationController.JobValidator.ProcessJobOverview(networkedJob.JobOverview.GetTrackedItem()); break; case ValidationType.JobBooklet: - networkedStationController.JobValidator.ValidateJob(networkedJob.JobBooklet); + networkedStationController.JobValidator.ValidateJob(networkedJob.JobBooklet.GetTrackedItem()); break; } + + //SendPacket(peer, new ClientboundJobValidateResponsePacket { JobNetId = packet.JobNetId, Invalid = false }, DeliveryMethod.ReliableUnordered); } private void OnCommonChatPacket(CommonChatPacket packet, NetPeer peer) @@ -947,5 +957,30 @@ private void OnUnconnectedPingPacket(UnconnectedPingPacket packet, IPEndPoint en Multiplayer.Log($"OnUnconnectedPingPacket({endPoint.Address})"); SendUnconnectedPacket(packet, endPoint.Address.ToString(),endPoint.Port); } + + private void OnCommonItemChangePacket(CommonItemChangePacket packet, NetPeer peer) + { + Multiplayer.LogDebug(()=>$"OnCommonItemChangePacket({packet.Items.Count}, {peer.Id})"); + + string debug = ""; + + foreach(var item in packet.Items) + { + debug += "UpdateType: {" + item.UpdateType + "}"; + debug += "itemNetId: " + item.ItemNetId; + debug += "PrefabName: " + item.PrefabName; + debug += "Equipped: " + item.Equipped; + debug += "Dropped: " + item.Dropped; + debug += "Position: " + item.PositionData.Position; + debug += "Rotation: " + item.PositionData.Rotation; + + debug += "States:"; + + foreach(var state in item.States) + debug += "\r\n\t" + state.Key + ": " + state.Value; + } + + Multiplayer.LogDebug(()=> debug); + } #endregion } diff --git a/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobsUpdatePacket.cs b/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobsUpdatePacket.cs index 7201275..b6f8bbe 100644 --- a/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobsUpdatePacket.cs +++ b/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobsUpdatePacket.cs @@ -17,10 +17,28 @@ public static ClientboundJobsUpdatePacket FromNetworkedJobs(ushort stationNetID, List jobData = new List(); foreach (var job in jobs) { - ushort validationNetId = 0; + ushort validationStationNetId = 0; + ushort validationItemNetId = 0; + ItemPositionData itemPositionData = new ItemPositionData(); if (NetworkedStationController.GetFromJobValidator(job.JobValidator, out NetworkedStationController netValidationStation)) - validationNetId = netValidationStation.NetId; + validationStationNetId = netValidationStation.NetId; + + switch (job.Cause) + { + case NetworkedJob.DirtyCause.JobOverview: + validationItemNetId = job.JobOverview.NetId; + itemPositionData = ItemPositionData.FromItem(job.JobOverview); + break; + case NetworkedJob.DirtyCause.JobBooklet: + validationItemNetId = job.JobBooklet.NetId; + itemPositionData = ItemPositionData.FromItem(job.JobBooklet); + break; + case NetworkedJob.DirtyCause.JobReport: + validationItemNetId = job.JobReport.NetId; + itemPositionData = ItemPositionData.FromItem(job.JobReport); + break; + } JobUpdateStruct data = new JobUpdateStruct { @@ -28,8 +46,9 @@ public static ClientboundJobsUpdatePacket FromNetworkedJobs(ushort stationNetID, JobState = job.Job.State, StartTime = job.Job.startTime, FinishTime = job.Job.finishTime, - ItemNetID = job.ValidationItem.NetId, - ValidationStationId = validationNetId + ValidationStationId = validationStationNetId, + ItemNetID = validationItemNetId, + ItemPositionData = itemPositionData }; jobData.Add(data); diff --git a/Multiplayer/Networking/Packets/Common/CommonItemChangePacket.cs b/Multiplayer/Networking/Packets/Common/CommonItemChangePacket.cs new file mode 100644 index 0000000..1a5f407 --- /dev/null +++ b/Multiplayer/Networking/Packets/Common/CommonItemChangePacket.cs @@ -0,0 +1,118 @@ +using LiteNetLib.Utils; +using Multiplayer.Networking.Data; +using System.IO; +using DV.Logic.Job; +using System.Collections.Generic; +using System.Linq; +using System; + + +namespace Multiplayer.Networking.Packets.Common; + +public class CommonItemChangePacket : INetSerializable +{ + private const int COMPRESS_AFTER_COUNT = 50; + + public List Items = new List(); + + public void Deserialize(NetDataReader reader) + { + Multiplayer.Log("CommonItemChangePacket.Deserialize()"); + try + { + bool compressed = reader.GetBool(); + if (compressed) + { + DeserializeCompressed(reader); + } + else + { + DeserializeRaw(reader); + } + } + catch (Exception ex) + { + Multiplayer.LogError($"Error in CommonItemChangePacket.Deserialize: {ex.Message}"); + } + } + + private void DeserializeCompressed(NetDataReader reader) + { + Multiplayer.Log("CommonItemChangePacket.DeserializeCompressed()"); + byte[] compressedData = reader.GetBytesWithLength(); + byte[] decompressedData = PacketCompression.Decompress(compressedData); + Multiplayer.Log($"Compressed: {compressedData.Length} Decompressed: {decompressedData.Length}"); + + NetDataReader decompressedReader = new NetDataReader(decompressedData); + int itemCount = decompressedReader.GetInt(); + Items.Capacity = itemCount; + for (int i = 0; i < itemCount; i++) + { + var item = new ItemUpdateData(); + item.Deserialize(decompressedReader); + Items.Add(item); + } + } + + private void DeserializeRaw(NetDataReader reader) + { + Multiplayer.Log("CommonItemChangePacket.DeserializeRaw()"); + int itemCount = reader.GetInt(); + Items.Capacity = itemCount; + for (int i = 0; i < itemCount; i++) + { + var item = new ItemUpdateData(); + item.Deserialize(reader); + Items.Add(item); + } + } + + public void Serialize(NetDataWriter writer) + { + Multiplayer.Log("CommonItemChangePacket.Serialize()"); + try + { + if (Items.Count > COMPRESS_AFTER_COUNT) + { + SerializeCompressed(writer); + } + else + { + SerializeRaw(writer); + } + } + catch (Exception ex) + { + Multiplayer.LogError($"CommonItemChangePacket.Serialize: {ex.Message}\r\n{ex.StackTrace}"); + } + } + + private void SerializeCompressed(NetDataWriter writer) + { + Multiplayer.Log($"CommonItemChangePacket.Serialize() Compressing. Item Count: {Items.Count}"); + writer.Put(true); // compressed data stream + + NetDataWriter dataWriter = new NetDataWriter(); + + dataWriter.Put(Items.Count); + foreach (var item in Items) + { + item.Serialize(dataWriter); + } + + byte[] compressedData = PacketCompression.Compress(dataWriter.Data); + Multiplayer.Log($"Uncompressed: {dataWriter.Length} Compressed: {compressedData.Length}"); + writer.PutBytesWithLength(compressedData); + } + + private void SerializeRaw(NetDataWriter writer) + { + Multiplayer.Log($"CommonItemChangePacket.Serialize() Raw. Item Count: {Items.Count}"); + writer.Put(false); // uncompressed data stream + writer.Put(Items.Count); + foreach (var item in Items) + { + item.Serialize(writer); + } + } +} diff --git a/Multiplayer/Patches/Jobs/BookletCreatorJobPatch.cs b/Multiplayer/Patches/Jobs/BookletCreatorJobPatch.cs index 81ff351..963aa2b 100644 --- a/Multiplayer/Patches/Jobs/BookletCreatorJobPatch.cs +++ b/Multiplayer/Patches/Jobs/BookletCreatorJobPatch.cs @@ -1,6 +1,7 @@ using DV.Booklets; using DV.Logic.Job; using HarmonyLib; +using Multiplayer.Components.Networking; using Multiplayer.Components.Networking.Jobs; using Multiplayer.Components.Networking.World; using Multiplayer.Utils; @@ -16,14 +17,18 @@ public static class BookletCreatorJob_Patch [HarmonyPostfix] private static void CreateJobOverview(JobOverview __result, Job job) { + if (!NetworkLifecycle.Instance.IsHost()) + return; + if (!NetworkedJob.TryGetFromJob(job, out NetworkedJob networkedJob)) { Multiplayer.LogError($"BookletCreatorJob_Patch.CreateJobOverview() NetworkedJob not found for Job ID: {job.ID}"); } else { - networkedJob.JobOverview = __result; - networkedJob.ValidationItem = __result.GetOrAddComponent(); + NetworkedItem netItem = __result.GetOrAddComponent(); + netItem.Initialize(__result, 0, false); + networkedJob.JobOverview = netItem; } } @@ -31,14 +36,18 @@ private static void CreateJobOverview(JobOverview __result, Job job) [HarmonyPostfix] private static void CreateJobBooklet(JobBooklet __result, Job job) { + if (!NetworkLifecycle.Instance.IsHost()) + return; + if (!NetworkedJob.TryGetFromJob(job, out NetworkedJob networkedJob)) { Multiplayer.LogError($"BookletCreatorJob_Patch.CreateJobBooklet() NetworkedJob not found for Job ID: {job.ID}"); } else { - networkedJob.JobBooklet = __result; - networkedJob.ValidationItem = __result.GetOrAddComponent(); + NetworkedItem netItem = __result.GetOrAddComponent(); + netItem.Initialize(__result, 0, false); + networkedJob.JobBooklet = netItem; } } } diff --git a/Multiplayer/Patches/Jobs/JobOverviewPatch.cs b/Multiplayer/Patches/Jobs/JobOverviewPatch.cs index dd70720..c5ba156 100644 --- a/Multiplayer/Patches/Jobs/JobOverviewPatch.cs +++ b/Multiplayer/Patches/Jobs/JobOverviewPatch.cs @@ -18,19 +18,19 @@ namespace Multiplayer.Patches.Jobs; [HarmonyPatch(typeof(JobOverview))] public static class JobOverview_Patch { - [HarmonyPatch(nameof(JobOverview.Start))] - [HarmonyPostfix] - private static void Start(JobOverview __instance) - { - if (!NetworkedJob.TryGetFromJob(__instance.job, out NetworkedJob networkedJob)) - { - Multiplayer.LogError($"JobOverview.Start() NetworkedJob not found for Job ID: {__instance.job?.ID}"); - __instance.DestroyJobOverview(); - return; - } + //[HarmonyPatch(nameof(JobOverview.Start))] + //[HarmonyPostfix] + //private static void Start(JobOverview __instance) + //{ + // if (!NetworkedJob.TryGetFromJob(__instance.job, out NetworkedJob networkedJob)) + // { + // Multiplayer.LogError($"JobOverview.Start() NetworkedJob not found for Job ID: {__instance.job?.ID}"); + // __instance.DestroyJobOverview(); + // return; + // } - networkedJob.JobOverview = __instance; - } + // networkedJob.JobOverview = __instance; + //} [HarmonyPatch(nameof(JobOverview.DestroyJobOverview))] diff --git a/Multiplayer/Patches/World/ItemBasePatch.cs b/Multiplayer/Patches/World/Items/ItemBasePatch.cs similarity index 57% rename from Multiplayer/Patches/World/ItemBasePatch.cs rename to Multiplayer/Patches/World/Items/ItemBasePatch.cs index e3f6f2d..d57466e 100644 --- a/Multiplayer/Patches/World/ItemBasePatch.cs +++ b/Multiplayer/Patches/World/Items/ItemBasePatch.cs @@ -3,7 +3,7 @@ using Multiplayer.Components.Networking.World; using Multiplayer.Utils; -namespace Multiplayer.Patches.World; +namespace Multiplayer.Patches.World.Items; [HarmonyPatch(typeof(ItemBase))] public static class ItemBase_Patch @@ -12,8 +12,8 @@ public static class ItemBase_Patch [HarmonyPostfix] private static void Awake(ItemBase __instance) { - Multiplayer.Log($"ItemBase.Awake() ItemSpec: {__instance?.InventorySpecs?.itemPrefabName}"); - __instance.GetOrAddComponent(); - return; - } + //Multiplayer.Log($"ItemBase.Awake() ItemSpec: {__instance?.InventorySpecs?.itemPrefabName}"); + __instance.GetOrAddComponent(); + return; + } } diff --git a/Multiplayer/Patches/World/Items/LanternPatch.cs b/Multiplayer/Patches/World/Items/LanternPatch.cs new file mode 100644 index 0000000..4020f23 --- /dev/null +++ b/Multiplayer/Patches/World/Items/LanternPatch.cs @@ -0,0 +1,40 @@ +using HarmonyLib; +using Multiplayer.Components.Networking.World; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Multiplayer.Patches.World.Items; + +[HarmonyPatch(typeof(Lantern), "Awake")] +public static class LanternAwakePatch +{ + static void Postfix(Lantern __instance) + { + var networkedItem = __instance.gameObject.AddComponent(); + networkedItem.Initialize(__instance); + + // Register the values you want to track with both getters and setters + networkedItem.RegisterTrackedValue( + "wickSize", + () => __instance.wickSize, + value => { + __instance.UpdateWickRelatedLogic(value); + } + ); + + networkedItem.RegisterTrackedValue( + "Ignited", + () => __instance.igniter.enabled, + value => + { + if (value) + __instance.OnFlameIgnited(); + else + __instance.OnFlameExtinguished(); + } + ); + } +} diff --git a/Multiplayer/Patches/World/Items/LighterPatch.cs b/Multiplayer/Patches/World/Items/LighterPatch.cs new file mode 100644 index 0000000..13cc7f7 --- /dev/null +++ b/Multiplayer/Patches/World/Items/LighterPatch.cs @@ -0,0 +1,44 @@ +using HarmonyLib; +using Multiplayer.Components.Networking.World; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Multiplayer.Patches.World.Items; + +[HarmonyPatch(typeof(Lighter), "Awake")] +public static class LighterAwakePatch +{ + static void Postfix(Lighter __instance) + { + var networkedItem = __instance.gameObject.AddComponent(); + networkedItem.Initialize(__instance); + + // Register the values you want to track with both getters and setters + networkedItem.RegisterTrackedValue( + "isOpen", + () => __instance.isOpen, + value => + { + if (value) + __instance.OpenLid(); + else + __instance.CloseLid(); + } + ); + + networkedItem.RegisterTrackedValue( + "Ignited", + () => __instance.igniter.enabled, + value => + { + if (value) + __instance.LightFire(true, true); + else + __instance.OnFlameExtinguished(); + } + ); + } +} From 42573a01c36b302055882310328a238f20c38eff Mon Sep 17 00:00:00 2001 From: AMacro Date: Tue, 24 Sep 2024 19:59:29 +1000 Subject: [PATCH 103/188] fixed warning --- .../MainMenu/ServerBrowser/ServerBrowserElement.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserElement.cs b/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserElement.cs index 09e153a..6222a89 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserElement.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserElement.cs @@ -48,12 +48,12 @@ protected override void Awake() iconPassword.sprite = Multiplayer.AssetIndex.lockIcon; // Set LAN icon - try + if(this.HasChildWithName("LAN Icon")) { goIconLAN = this.FindChildByName("LAN Icon"); } - catch (Exception e) - { + else + { goIconLAN = Instantiate(goIconPassword, goIconPassword.transform.parent); goIconLAN.name = "LAN Icon"; Vector3 LANpos = goIconLAN.transform.localPosition; @@ -63,6 +63,7 @@ protected override void Awake() iconLAN = goIconLAN.GetComponent(); iconLAN.sprite = Multiplayer.AssetIndex.lanIcon; } + } public override void SetData(IServerBrowserGameDetails data, AGridView _) From 79f614066fd7675f404a5de9307e0beefd9d7bff Mon Sep 17 00:00:00 2001 From: AMacro Date: Mon, 7 Oct 2024 08:00:01 +1000 Subject: [PATCH 104/188] Continuation of item sync --- .../Networking/Train/NetworkedTrainCar.cs | 4 +- .../Networking/World/NetworkedItem.cs | 74 ++++++++---- .../Networking/World/NetworkedItemManager.cs | 107 +++++++++++++++++- .../SaveGame/StartGameData_ServerSave.cs | 9 ++ .../Managers/Client/NetworkClient.cs | 4 +- .../Networking/Managers/NetworkManager.cs | 10 +- .../Managers/Server/NetworkServer.cs | 14 ++- .../Packets/Common/CommonItemChangePacket.cs | 15 ++- 8 files changed, 198 insertions(+), 39 deletions(-) diff --git a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs index 2a26aad..b94b9e7 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs @@ -578,7 +578,7 @@ public void Common_UpdatePorts(CommonTrainPortsPacket packet) if (!hasSimFlow) return; - string log = $"CommonTrainPortsPacket({TrainCar.ID})"; + //string log = $"CommonTrainPortsPacket({TrainCar.ID})"; for (int i = 0; i < packet.PortIds.Length; i++) { Port port = simulationFlow.fullPortIdToPort[packet.PortIds[i]]; @@ -596,7 +596,7 @@ public void Common_UpdatePorts(CommonTrainPortsPacket packet) */ } - NetworkLifecycle.Instance.Client.LogDebug(() => log); + //NetworkLifecycle.Instance.Client.LogDebug(() => log); } public void Common_UpdateFuses(CommonTrainFusesPacket packet) diff --git a/Multiplayer/Components/Networking/World/NetworkedItem.cs b/Multiplayer/Components/Networking/World/NetworkedItem.cs index cec7f08..39787e5 100644 --- a/Multiplayer/Components/Networking/World/NetworkedItem.cs +++ b/Multiplayer/Components/Networking/World/NetworkedItem.cs @@ -16,6 +16,10 @@ public class NetworkedItem : IdMonoBehaviour #region Lookup Cache private static readonly Dictionary itemBaseToNetworkedItem = new(); + public static List GetAll() + { + return itemBaseToNetworkedItem.Values.ToList(); + } public static bool Get(ushort netId, out NetworkedItem obj) { bool b = Get(netId, out IdMonoBehaviour rawObj); @@ -44,6 +48,7 @@ public static bool TryGetNetworkedItem(ItemBase item, out NetworkedItem networke private List trackedValues = new List(); public bool UsefulItem { get; private set; } = false; public Type TrackedItemType { get; private set; } + public bool BlockSync { get; set; } = false; //Track dirty states private bool CreatedDirty = true; //if set, we created this item dirty and have not sent an update @@ -66,6 +71,7 @@ protected override void Awake() { base.Awake(); Multiplayer.LogDebug(() => $"NetworkedItem.Awake() {name}"); + NetworkedItemManager.Instance.CheckInstance(); //Ensure the NetworkedItemManager is initialised Register(); } @@ -104,23 +110,32 @@ public void Initialize(T item, ushort netId = 0, bool createDirty = true) whe private bool Register() { - if (!TryGetComponent(out ItemBase itemBase)) + try { - Multiplayer.LogError($"Unable to find ItemBase for {name}"); - return false; - } - Item = itemBase; - itemBaseToNetworkedItem[Item] = this; + if (!TryGetComponent(out ItemBase itemBase)) + { + Multiplayer.LogError($"Unable to find ItemBase for {name}"); + return false; + } + + Item = itemBase; + itemBaseToNetworkedItem[Item] = this; - Item.Grabbed += OnGrabbed; - Item.Ungrabbed += OnUngrabbed; - Item.ItemInventoryStateChanged += OnItemInventoryStateChanged; + Item.Grabbed += OnGrabbed; + Item.Ungrabbed += OnUngrabbed; + Item.ItemInventoryStateChanged += OnItemInventoryStateChanged; - lastPosition = Item.transform.position - WorldMover.currentMove; - lastRotation = Item.transform.rotation; + lastPosition = Item.transform.position - WorldMover.currentMove; + lastRotation = Item.transform.rotation; - return true; + return true; + } + catch (Exception ex) + { + Multiplayer.LogError($"Unable to find ItemBase for {name}\r\n{ex.Message}"); + return false; + } } private void OnUngrabbed(ControlImplBase obj) @@ -188,7 +203,8 @@ private void CheckPositionChange() bool positionChanged = Vector3.Distance(currentPosition, lastPosition) > PositionThreshold; bool rotationChanged = Quaternion.Angle(currentRotation, lastRotation) > RotationThreshold; - if (positionChanged || rotationChanged) + //We don't care about position and rotation if the player is holding it, as it will move relative to the player + if ((positionChanged || rotationChanged) && !ItemGrabbed) { ItemPosition = new ItemPositionData { @@ -201,13 +217,13 @@ private void CheckPositionChange() } } - private void Update() + public ItemUpdateData GetSnapshot() { ItemUpdateData snapshot; ItemUpdateData.ItemUpdateType updateType = ItemUpdateData.ItemUpdateType.None; - if (Item == null && Register() ==false) - return; + if (Item == null && Register() == false) + return null; CheckPositionChange(); @@ -230,8 +246,11 @@ private void Update() updateType = ItemUpdateData.ItemUpdateType.Create; } + //no changes this snapshot + if (updateType == ItemUpdateData.ItemUpdateType.None) + return null; + snapshot = CreateUpdateData(updateType); - NetworkedItemManager.Instance.AddDirtyItemSnapshot(snapshot); CreatedDirty = false; GrabbedDirty = false; @@ -239,21 +258,22 @@ private void Update() PositionDirty = false; MarkValuesClean(); + + return snapshot; } - /* - private void SendStateUpdate() + public void ReceiveSnapshot(ItemUpdateData snapshot) { - var updateData = CreateUpdateData(ItemUpdateData.ItemUpdateType.State); - updateData.StateData = GetDirtyStateData(); - SendItemUpdate(updateData); - MarkValuesClean(); + if(snapshot == null || snapshot.UpdateType == ItemUpdateData.ItemUpdateType.None) + return; + + if(snapshot.UpdateType == ItemUpdateData.ItemUpdateType.Create) return; } - */ #endregion public ItemUpdateData CreateUpdateData(ItemUpdateData.ItemUpdateType updateType) { + Multiplayer.LogDebug(() => $"NetworkedItem.CreateUpdateData({updateType}) NetId: {NetId}, name: {name}"); var updateData = new ItemUpdateData { @@ -279,10 +299,14 @@ protected override void OnDestroy() { NetworkedItemManager.Instance.AddDirtyItemSnapshot(CreateUpdateData(ItemUpdateData.ItemUpdateType.Destroy)); } - else + else if(!BlockSync) { Multiplayer.LogWarning($"NetworkedItem.OnDestroy({name}, {NetId})\r\n{new System.Diagnostics.StackTrace()}"); } + else + { + Multiplayer.LogDebug(()=>$"NetworkedItem.OnDestroy({name}, {NetId}) Sync blocked"); + } base.OnDestroy(); if (Item != null) diff --git a/Multiplayer/Components/Networking/World/NetworkedItemManager.cs b/Multiplayer/Components/Networking/World/NetworkedItemManager.cs index 30736ed..a6d556d 100644 --- a/Multiplayer/Components/Networking/World/NetworkedItemManager.cs +++ b/Multiplayer/Components/Networking/World/NetworkedItemManager.cs @@ -13,6 +13,7 @@ namespace Multiplayer.Components.Networking.Train; public class NetworkedItemManager : SingletonBehaviour { private List DirtyItems = new List(); + private List ReceivedSnapshots = new List(); protected override void Awake() { @@ -38,14 +39,87 @@ public void AddDirtyItemSnapshot(ItemUpdateData item) DirtyItems.Add(item); } + public void ReceiveSnapshots(List snapshots) + { + if (snapshots == null) + return; + + ReceivedSnapshots.AddRange(snapshots); + } + #region Common private void Common_OnTick(uint tick) { - if(DirtyItems.Count == 0) + //Process received Snapshots + ProcessReceived(); + + ProcessChanged(); + } + + private void ProcessReceived() + { + while(ReceivedSnapshots.Count > 0) + { + ItemUpdateData snapshot = ReceivedSnapshots.First(); + + //process + if (snapshot != null && snapshot.UpdateType != ItemUpdateData.ItemUpdateType.None) + { + //try to find an existing item + NetworkedItem.Get(snapshot.ItemNetId, out NetworkedItem netItem); + + if (NetworkLifecycle.Instance.IsHost()) + { + if (snapshot.UpdateType == ItemUpdateData.ItemUpdateType.Create) + { + Multiplayer.LogError($"NetworkedItemManager.ProcessReceived() Host received Create snapshot! ItemNetId: {snapshot.ItemNetId}, prefabName: {snapshot.PrefabName}"); + } + else + { + //we should validate if the player can perform this action... TODO later + if (netItem != null) + netItem.ReceiveSnapshot(snapshot); + else + Multiplayer.LogError($"NetworkedItemManager.ProcessReceived() NetworkedItem not found! Update Type: {snapshot?.UpdateType}, ItemNetId: {snapshot?.ItemNetId}, prefabName: {snapshot?.PrefabName}"); + } + } + else + { + if (snapshot.UpdateType == ItemUpdateData.ItemUpdateType.Create) + { + CreateItem(snapshot); + } + else + { + netItem.ReceiveSnapshot(snapshot); + } + } + } + else + { + Multiplayer.LogError($"NetworkedItemManager.ProcessReceived() Invalid Update Type: {snapshot?.UpdateType}, ItemNetId: {snapshot?.ItemNetId}, prefabName: {snapshot?.PrefabName}"); + } + + ReceivedSnapshots.Remove(snapshot); + } + } + + private void ProcessChanged() + { + //Process all items for updates + foreach (var item in NetworkedItem.GetAll()) + { + ItemUpdateData snapshot = item.GetSnapshot(); + + if (snapshot != null) + DirtyItems.Add(snapshot); + } + + if (DirtyItems.Count == 0) return; - if(NetworkLifecycle.Instance.IsHost()) + if (NetworkLifecycle.Instance.IsHost()) { NetworkLifecycle.Instance.Server.SendItemsChangePacket(DirtyItems); } @@ -56,6 +130,35 @@ private void Common_OnTick(uint tick) DirtyItems.Clear(); } + + private void CreateItem(ItemUpdateData snapshot) + { + if(snapshot == null || snapshot.ItemNetId == 0) + { + Multiplayer.LogError($"NetworkedItemManager.CreateItem() Invalid snapshot! ItemNetId: {snapshot?.ItemNetId}, prefabName: {snapshot?.PrefabName}"); + return; + } + + GameObject prefabObj = Resources.Load(snapshot.PrefabName) as GameObject; + + if (prefabObj == null) + { + Multiplayer.LogError($"NetworkedItemManager.CreateItem() Unable to load prefab for ItemNetId: {snapshot.ItemNetId}, prefabName: {snapshot.PrefabName}"); + return; + } + + //create a new item + GameObject gameObject = UnityEngine.Object.Instantiate(prefabObj, snapshot.PositionData.Position, snapshot.PositionData.Rotation); + + InventoryItemSpec component = gameObject.GetComponent(); + if (component != null) + component.BelongsToPlayer = true; + + NetworkedItem newItem = gameObject.AddComponent(); + newItem.NetId = snapshot.ItemNetId; + newItem.ReceiveSnapshot(snapshot); + } + #endregion [UsedImplicitly] diff --git a/Multiplayer/Components/SaveGame/StartGameData_ServerSave.cs b/Multiplayer/Components/SaveGame/StartGameData_ServerSave.cs index 874fe8d..6492881 100644 --- a/Multiplayer/Components/SaveGame/StartGameData_ServerSave.cs +++ b/Multiplayer/Components/SaveGame/StartGameData_ServerSave.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using DV.UserManagement; +using Multiplayer.Components.Networking.World; using Multiplayer.Networking.Packets.Clientbound; using Multiplayer.Patches.SaveGame; using Newtonsoft.Json.Linq; @@ -51,6 +52,14 @@ public override SaveGameData GetSaveGameData() public override IEnumerator DoLoad(Transform playerContainer) { + // clear spawned world items + foreach (var item in NetworkedItem.GetAll()) + { + item.BlockSync = true; + Destroy(item); + } + + Transform playerTransform = playerContainer.transform; playerTransform.position = PlayerManager.IsPlayerPositionValid(packet.Position) ? packet.Position : LevelInfo.Instance.defaultSpawnPosition; playerTransform.eulerAngles = new Vector3(0, packet.Rotation, 0); diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 5eac496..290b466 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -126,7 +126,7 @@ protected override void Subscribe() netPacketProcessor.SubscribeReusable(OnClientboundJobsCreatePacket); netPacketProcessor.SubscribeReusable(OnClientboundJobValidateResponsePacket); netPacketProcessor.SubscribeReusable(OnCommonChatPacket); - netPacketProcessor.SubscribeReusable(OnCommonItemChangePacket); + netPacketProcessor.SubscribeNetSerializable(OnCommonItemChangePacket); } #region Net Events @@ -212,7 +212,7 @@ public override void OnNatIntroductionSuccess(IPEndPoint targetEndPoint, NatAddr } #endregion - #region Listeners + #region Listeners private void OnClientboundServerDenyPacket(ClientboundServerDenyPacket packet) { diff --git a/Multiplayer/Networking/Managers/NetworkManager.cs b/Multiplayer/Networking/Managers/NetworkManager.cs index 6bdcbb9..eed731f 100644 --- a/Multiplayer/Networking/Managers/NetworkManager.cs +++ b/Multiplayer/Networking/Managers/NetworkManager.cs @@ -4,6 +4,7 @@ using LiteNetLib; using LiteNetLib.Utils; using Multiplayer.Networking.Data; +using Multiplayer.Networking.Packets.Common; using Multiplayer.Networking.Serialization; namespace Multiplayer.Networking.Listeners; @@ -84,6 +85,13 @@ public virtual void Stop() return cachedWriter; } + protected NetDataWriter WriteNetSerializablePacket(T packet) where T : INetSerializable, new() + { + cachedWriter.Reset(); + netPacketProcessor.WriteNetSerializable(cachedWriter, ref packet); + return cachedWriter; + } + protected void SendPacket(NetPeer peer, T packet, DeliveryMethod deliveryMethod) where T : class, new() { peer?.Send(WritePacket(packet), deliveryMethod); @@ -94,7 +102,7 @@ public virtual void Stop() netManager.SendUnconnectedMessage(WritePacket(packet), ipAddress, port); } - protected abstract void Subscribe(); + protected abstract void Subscribe(); #region Net Events diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index cd965f7..559afea 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -133,7 +133,7 @@ protected override void Subscribe() netPacketProcessor.SubscribeReusable(OnServerboundJobValidateRequestPacket); netPacketProcessor.SubscribeReusable(OnCommonChatPacket); netPacketProcessor.SubscribeReusable(OnUnconnectedPingPacket); - netPacketProcessor.SubscribeReusable(OnCommonItemChangePacket); + netPacketProcessor.SubscribeNetSerializable(OnCommonItemChangePacket); } private void OnLoaded() @@ -239,6 +239,16 @@ public override void OnNatIntroductionSuccess(IPEndPoint targetEndPoint, NatAddr kvp.Value.Send(writer, deliveryMethod); } } + private void SendNetSerializablePacketToAll(T packet, DeliveryMethod deliveryMethod, NetPeer excludePeer) where T : INetSerializable, new() + { + NetDataWriter writer = WriteNetSerializablePacket(packet); + foreach (KeyValuePair kvp in netPeers) + { + if (kvp.Key == excludePeer.Id) + continue; + kvp.Value.Send(writer, deliveryMethod); + } + } public void KickPlayer(NetPeer peer) { @@ -401,7 +411,7 @@ public void SendJobsUpdatePacket(ushort stationNetId, NetworkedJob[] jobs, NetPe public void SendItemsChangePacket(List items, NetPeer peer = null) { Multiplayer.Log($"Sending SendItemsChangePacket with {items.Count()} items"); - SendPacketToAll(new CommonItemChangePacket { Items = items }, + SendNetSerializablePacketToAll(new CommonItemChangePacket { Items = items }, DeliveryMethod.ReliableUnordered, selfPeer); } diff --git a/Multiplayer/Networking/Packets/Common/CommonItemChangePacket.cs b/Multiplayer/Networking/Packets/Common/CommonItemChangePacket.cs index 1a5f407..a60111c 100644 --- a/Multiplayer/Networking/Packets/Common/CommonItemChangePacket.cs +++ b/Multiplayer/Networking/Packets/Common/CommonItemChangePacket.cs @@ -38,14 +38,17 @@ public void Deserialize(NetDataReader reader) private void DeserializeCompressed(NetDataReader reader) { - Multiplayer.Log("CommonItemChangePacket.DeserializeCompressed()"); + int itemCount = reader.GetInt(); byte[] compressedData = reader.GetBytesWithLength(); + Multiplayer.Log($"CommonItemChangePacket.DeserializeCompressed() itemCount {itemCount} length: {compressedData.Length}"); + byte[] decompressedData = PacketCompression.Decompress(compressedData); - Multiplayer.Log($"Compressed: {compressedData.Length} Decompressed: {decompressedData.Length}"); + Multiplayer.Log($"CommonItemChangePacket.DeserializeCompressed() Compressed: {compressedData.Length} Decompressed: {decompressedData.Length}"); NetDataReader decompressedReader = new NetDataReader(decompressedData); - int itemCount = decompressedReader.GetInt(); + Items.Capacity = itemCount; + for (int i = 0; i < itemCount; i++) { var item = new ItemUpdateData(); @@ -56,9 +59,11 @@ private void DeserializeCompressed(NetDataReader reader) private void DeserializeRaw(NetDataReader reader) { - Multiplayer.Log("CommonItemChangePacket.DeserializeRaw()"); int itemCount = reader.GetInt(); + Multiplayer.Log($"CommonItemChangePacket.DeserializeRaw() itemCount: {itemCount}"); + Items.Capacity = itemCount; + for (int i = 0; i < itemCount; i++) { var item = new ItemUpdateData(); @@ -91,10 +96,10 @@ private void SerializeCompressed(NetDataWriter writer) { Multiplayer.Log($"CommonItemChangePacket.Serialize() Compressing. Item Count: {Items.Count}"); writer.Put(true); // compressed data stream + writer.Put(Items.Count); NetDataWriter dataWriter = new NetDataWriter(); - dataWriter.Put(Items.Count); foreach (var item in Items) { item.Serialize(dataWriter); From dbc9b785003b9331a0d3c419f7548f9ecde0ffa3 Mon Sep 17 00:00:00 2001 From: AMacro Date: Tue, 8 Oct 2024 15:07:52 +1000 Subject: [PATCH 105/188] Enhanced Hosting pane --- .../Components/MainMenu/HostGamePane.cs | 13 +- .../Components/Util/HyperlinkHandler.cs | 130 ++++++++++++++++++ 2 files changed, 141 insertions(+), 2 deletions(-) create mode 100644 Multiplayer/Components/Util/HyperlinkHandler.cs diff --git a/Multiplayer/Components/MainMenu/HostGamePane.cs b/Multiplayer/Components/MainMenu/HostGamePane.cs index 5fff99d..852b739 100644 --- a/Multiplayer/Components/MainMenu/HostGamePane.cs +++ b/Multiplayer/Components/MainMenu/HostGamePane.cs @@ -13,6 +13,7 @@ using UnityEngine.Events; using Multiplayer.Networking.Data; using Multiplayer.Components.Networking; +using Multiplayer.Components.Util; namespace Multiplayer.Components.MainMenu; public class HostGamePane : MonoBehaviour @@ -148,12 +149,20 @@ private void BuildUI() //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]"); + HyperlinkHandler hyperLinks = serverDetailsGO.GetOrAddComponent(); + + hyperLinks.linkColor = new Color(0.302f, 0.651f, 1f); // #4DA6FF + hyperLinks.linkHoverColor = new Color(0.498f, 0.749f, 1f); // #7FBFFF + serverWindowGO.name = "Host Details"; serverDetails = serverDetailsGO.GetComponent(); serverDetails.textWrappingMode = TextWrappingModes.Normal; - serverDetails.text = "Please note:
Use of other mods is currently not supported and may cause unexpected behaviour.

" + + serverDetails.text = "First time hosts, please see the Hosting section of our Wiki.


" + + + "Using other mods may cause unexpected behaviour including de-syncs. See Mod Compatibility for more info.

" + "It is recommended that other mods are disabled and Derail Valley restarted prior to playing in multiplayer.

" + - "We hope to make your favourite mods work with multiplayer in the future."; + + "We hope to have your favourite mods compatible with multiplayer in the future."; //Find scrolling viewport diff --git a/Multiplayer/Components/Util/HyperlinkHandler.cs b/Multiplayer/Components/Util/HyperlinkHandler.cs new file mode 100644 index 0000000..91cc380 --- /dev/null +++ b/Multiplayer/Components/Util/HyperlinkHandler.cs @@ -0,0 +1,130 @@ +using UnityEngine; +using TMPro; +using UnityEngine.EventSystems; +using System.Text.RegularExpressions; + +namespace Multiplayer.Components.Util +{ + public class HyperlinkHandler : MonoBehaviour, IPointerClickHandler + { + public static readonly Color DEFAULT_COLOR = Color.blue; + public static readonly Color DEFAULT_HOVER_COLOR = new Color(0x00, 0x59, 0xFF, 0xFF); + + public Color linkColor = DEFAULT_COLOR; + public Color linkHoverColor = DEFAULT_HOVER_COLOR; + + public TextMeshProUGUI textComponent; + private Canvas canvas; + private Camera canvasCamera; + + private int hoveredLinkIndex = -1; + private bool underlineLinks = true; + + void Start() + { + if (textComponent == null) + { + textComponent = GetComponent(); + } + + canvas = GetComponentInParent(); + if (canvas != null) + { + canvasCamera = canvas.renderMode == RenderMode.ScreenSpaceOverlay ? null : canvas.worldCamera; + } + + ApplyLinkStyling(); + } + + void Update() + { + int linkIndex = TMP_TextUtilities.FindIntersectingLink(textComponent, Input.mousePosition, canvasCamera); + + if (linkIndex != -1 && linkIndex != hoveredLinkIndex) + { + // Mouse is over a new link + if (hoveredLinkIndex != -1) + { + // Remove hover style from the previously hovered link + RemoveHoverStyle(hoveredLinkIndex); + } + ApplyHoverStyle(linkIndex); + hoveredLinkIndex = linkIndex; + } + else if (linkIndex == -1 && hoveredLinkIndex != -1) + { + // Mouse is no longer over any link + RemoveHoverStyle(hoveredLinkIndex); + hoveredLinkIndex = -1; + } + } + + public void OnPointerClick(PointerEventData eventData) + { + int linkIndex = TMP_TextUtilities.FindIntersectingLink(textComponent, Input.mousePosition, canvasCamera); + + if (linkIndex != -1) + { + TMP_LinkInfo linkInfo = textComponent.textInfo.linkInfo[linkIndex]; + string url = linkInfo.GetLinkID(); + Application.OpenURL(url); + } + } + + private void ApplyLinkStyling() + { + string text = textComponent.text; + string pattern = @"(.*?)<\/link>"; + string replacement = underlineLinks + ? $"$2" + : $"$2"; + + text = Regex.Replace(text, pattern, replacement); + textComponent.text = text; + } + + private void ApplyHoverStyle(int linkIndex) + { + TMP_LinkInfo linkInfo = textComponent.textInfo.linkInfo[linkIndex]; + SetLinkColor(linkInfo, linkHoverColor); + } + + private void RemoveHoverStyle(int linkIndex) + { + TMP_LinkInfo linkInfo = textComponent.textInfo.linkInfo[linkIndex]; + SetLinkColor(linkInfo, linkColor); + } + + private void SetLinkColor(TMP_LinkInfo linkInfo, Color32 color) + { + var meshInfo = textComponent.textInfo.meshInfo[0]; + + for (int i = 0; i < linkInfo.linkTextLength; i++) + { + int characterIndex = linkInfo.linkTextfirstCharacterIndex + i; + + // Check if the character is within bounds and is visible + if (characterIndex >= textComponent.textInfo.characterCount || + !textComponent.textInfo.characterInfo[characterIndex].isVisible) + continue; + + int materialIndex = textComponent.textInfo.characterInfo[characterIndex].materialReferenceIndex; + int vertexIndex = textComponent.textInfo.characterInfo[characterIndex].vertexIndex; + + // Ensure we're using the correct mesh info + meshInfo = textComponent.textInfo.meshInfo[materialIndex]; + + meshInfo.colors32[vertexIndex] = color; + meshInfo.colors32[vertexIndex + 1] = color; + meshInfo.colors32[vertexIndex + 2] = color; + meshInfo.colors32[vertexIndex + 3] = color; + } + + // Mark the vertex data as dirty for all used materials + for (int i = 0; i < textComponent.textInfo.materialCount; i++) + { + textComponent.UpdateVertexData(TMP_VertexDataUpdateFlags.Colors32); + } + } + } +} From 903890688c94689bc10265f6a3a91e58be336169 Mon Sep 17 00:00:00 2001 From: AMacro Date: Tue, 8 Oct 2024 15:11:30 +1000 Subject: [PATCH 106/188] Fixed issues with deserialisation --- .../Networking/World/NetworkedItemManager.cs | 2 -- .../Managers/Client/NetworkClient.cs | 29 ++++++++++-------- .../Managers/Server/NetworkServer.cs | 30 ++++++++++++------- .../Packets/Common/CommonItemChangePacket.cs | 19 +++++++++--- 4 files changed, 52 insertions(+), 28 deletions(-) diff --git a/Multiplayer/Components/Networking/World/NetworkedItemManager.cs b/Multiplayer/Components/Networking/World/NetworkedItemManager.cs index a6d556d..0aa0e4d 100644 --- a/Multiplayer/Components/Networking/World/NetworkedItemManager.cs +++ b/Multiplayer/Components/Networking/World/NetworkedItemManager.cs @@ -4,8 +4,6 @@ using UnityEngine; using JetBrains.Annotations; using Multiplayer.Networking.Data; -using Multiplayer.Networking.Packets.Clientbound.Train; -using Multiplayer.Utils; using Multiplayer.Components.Networking.World; namespace Multiplayer.Components.Networking.Train; diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 290b466..cb3ab17 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -777,27 +777,32 @@ private void OnClientboundJobValidateResponsePacket(ClientboundJobValidateRespon private void OnCommonItemChangePacket(CommonItemChangePacket packet, NetPeer peer) { - Multiplayer.LogDebug(() => $"OnCommonItemChangePacket({packet.Items.Count}, {peer.Id})"); + LogDebug(() => $"OnCommonItemChangePacket({packet?.Items?.Count}, {peer.Id})"); string debug = ""; - foreach (var item in packet.Items) + foreach (var item in packet?.Items) { - debug += "UpdateType: {" + item.UpdateType + "}"; - debug += "itemNetId: " + item.ItemNetId; - debug += "PrefabName: " + item.PrefabName; - debug += "Equipped: " + item.Equipped; - debug += "Dropped: " + item.Dropped; - debug += "Position: " + item.PositionData.Position; - debug += "Rotation: " + item.PositionData.Rotation; - + LogDebug(() => $"OnCommonItemChangePacket({packet?.Items?.Count}, {peer.Id}) in loop"); + debug += "UpdateType: " + item?.UpdateType + "\r\n"; + debug += "itemNetId: " + item?.ItemNetId + "\r\n"; + debug += "PrefabName: " + item?.PrefabName + "\r\n"; + debug += "Equipped: " + item?.Equipped + "\r\n"; + debug += "Dropped: " + item?.Dropped + "\r\n"; + debug += "Position: " + item?.PositionData.Position + "\r\n"; + debug += "Rotation: " + item?.PositionData.Rotation + "\r\n"; + + LogDebug(() => $"OnCommonItemChangePacket({packet?.Items?.Count}, {peer.Id}) prep states"); debug += "States:"; - foreach (var state in item.States) - debug += "\r\n\t" + state.Key + ": " + state.Value; + if (item.States != null) + foreach (var state in item?.States) + debug += "\r\n\t" + state.Key + ": " + state.Value; } Multiplayer.LogDebug(() => debug); + + NetworkedItemManager.Instance.ReceiveSnapshots(packet.Items); } #endregion diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index 559afea..aef49e1 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -239,6 +239,13 @@ public override void OnNatIntroductionSuccess(IPEndPoint targetEndPoint, NatAddr kvp.Value.Send(writer, deliveryMethod); } } + private void SendNetSerializablePacketToAll(T packet, DeliveryMethod deliveryMethod) where T : INetSerializable, new() + { + NetDataWriter writer = WriteNetSerializablePacket(packet); + foreach (KeyValuePair kvp in netPeers) + kvp.Value.Send(writer, deliveryMethod); + } + private void SendNetSerializablePacketToAll(T packet, DeliveryMethod deliveryMethod, NetPeer excludePeer) where T : INetSerializable, new() { NetDataWriter writer = WriteNetSerializablePacket(packet); @@ -413,6 +420,9 @@ public void SendItemsChangePacket(List items, NetPeer peer = nul Multiplayer.Log($"Sending SendItemsChangePacket with {items.Count()} items"); SendNetSerializablePacketToAll(new CommonItemChangePacket { Items = items }, DeliveryMethod.ReliableUnordered, selfPeer); + + //SendNetSerializablePacketToAll(new CommonItemChangePacket { Items = items }, + // DeliveryMethod.ReliableUnordered); } public void SendChat(string message, NetPeer exclude = null) @@ -970,23 +980,23 @@ private void OnUnconnectedPingPacket(UnconnectedPingPacket packet, IPEndPoint en private void OnCommonItemChangePacket(CommonItemChangePacket packet, NetPeer peer) { - Multiplayer.LogDebug(()=>$"OnCommonItemChangePacket({packet.Items.Count}, {peer.Id})"); + LogDebug(()=>$"OnCommonItemChangePacket({packet?.Items?.Count}, {peer.Id})"); string debug = ""; - foreach(var item in packet.Items) + foreach(var item in packet?.Items) { - debug += "UpdateType: {" + item.UpdateType + "}"; - debug += "itemNetId: " + item.ItemNetId; - debug += "PrefabName: " + item.PrefabName; - debug += "Equipped: " + item.Equipped; - debug += "Dropped: " + item.Dropped; - debug += "Position: " + item.PositionData.Position; - debug += "Rotation: " + item.PositionData.Rotation; + debug += "UpdateType: {" + item?.UpdateType + "}"; + debug += "itemNetId: " + item?.ItemNetId; + debug += "PrefabName: " + item?.PrefabName; + debug += "Equipped: " + item?.Equipped; + debug += "Dropped: " + item?.Dropped; + debug += "Position: " + item?.PositionData.Position; + debug += "Rotation: " + item?.PositionData.Rotation; debug += "States:"; - foreach(var state in item.States) + foreach(var state in item?.States) debug += "\r\n\t" + state.Key + ": " + state.Value; } diff --git a/Multiplayer/Networking/Packets/Common/CommonItemChangePacket.cs b/Multiplayer/Networking/Packets/Common/CommonItemChangePacket.cs index a60111c..a6a8cf2 100644 --- a/Multiplayer/Networking/Packets/Common/CommonItemChangePacket.cs +++ b/Multiplayer/Networking/Packets/Common/CommonItemChangePacket.cs @@ -18,6 +18,7 @@ public class CommonItemChangePacket : INetSerializable public void Deserialize(NetDataReader reader) { Multiplayer.Log("CommonItemChangePacket.Deserialize()"); + //Multiplayer.LogDebug(() => $"CommonItemChangePacket.Deserialize()\r\nBytes: {BitConverter.ToString(reader.RawData).Replace("-", " ")}"); try { bool compressed = reader.GetBool(); @@ -29,6 +30,8 @@ public void Deserialize(NetDataReader reader) { DeserializeRaw(reader); } + + } catch (Exception ex) { @@ -40,14 +43,14 @@ private void DeserializeCompressed(NetDataReader reader) { int itemCount = reader.GetInt(); byte[] compressedData = reader.GetBytesWithLength(); - Multiplayer.Log($"CommonItemChangePacket.DeserializeCompressed() itemCount {itemCount} length: {compressedData.Length}"); + //Multiplayer.Log($"CommonItemChangePacket.DeserializeCompressed() itemCount {itemCount} length: {compressedData.Length}"); byte[] decompressedData = PacketCompression.Decompress(compressedData); - Multiplayer.Log($"CommonItemChangePacket.DeserializeCompressed() Compressed: {compressedData.Length} Decompressed: {decompressedData.Length}"); + //Multiplayer.Log($"CommonItemChangePacket.DeserializeCompressed() Compressed: {compressedData.Length} Decompressed: {decompressedData.Length}"); NetDataReader decompressedReader = new NetDataReader(decompressedData); - Items.Capacity = itemCount; + //Items.Capacity = itemCount; for (int i = 0; i < itemCount; i++) { @@ -62,12 +65,16 @@ private void DeserializeRaw(NetDataReader reader) int itemCount = reader.GetInt(); Multiplayer.Log($"CommonItemChangePacket.DeserializeRaw() itemCount: {itemCount}"); - Items.Capacity = itemCount; + //Items.Capacity = itemCount; + //Multiplayer.Log($"CommonItemChangePacket.DeserializeRaw() itemCount: {itemCount}, pre-loop"); for (int i = 0; i < itemCount; i++) { + //Multiplayer.Log($"CommonItemChangePacket.DeserializeRaw() itemCount: {itemCount}, new ItemUpdateData()"); var item = new ItemUpdateData(); + //Multiplayer.Log($"CommonItemChangePacket.DeserializeRaw() itemCount: {itemCount}, item.Deserialize()"); item.Deserialize(reader); + //Multiplayer.Log($"CommonItemChangePacket.DeserializeRaw() itemCount: {itemCount}, Items.Add()"); Items.Add(item); } } @@ -75,6 +82,8 @@ private void DeserializeRaw(NetDataReader reader) public void Serialize(NetDataWriter writer) { Multiplayer.Log("CommonItemChangePacket.Serialize()"); + //Multiplayer.LogDebug(() => $"CommonItemChangePacket.Serialize() Data Before\r\nBytes: {BitConverter.ToString(writer.CopyData()).Replace("-", " ")}"); + try { if (Items.Count > COMPRESS_AFTER_COUNT) @@ -85,6 +94,8 @@ public void Serialize(NetDataWriter writer) { SerializeRaw(writer); } + + //Multiplayer.LogDebug(() => $"CommonItemChangePacket.Serialize() Data After\r\nBytes: {BitConverter.ToString(writer.CopyData()).Replace("-", " ")}"); } catch (Exception ex) { From 73bbaebbd8127047bf87ecc157fdd13c94c18069 Mon Sep 17 00:00:00 2001 From: AMacro Date: Tue, 8 Oct 2024 20:36:21 +1000 Subject: [PATCH 107/188] Item Sync continued --- .../Networking/World/NetworkedItem.cs | 61 +++++++++++++++-- .../Networking/World/NetworkedItemManager.cs | 66 ++++++++++++------- .../SaveGame/StartGameData_ServerSave.cs | 41 ++++++++++-- .../Managers/Client/NetworkClient.cs | 16 +++-- .../Networking/Managers/NetworkManager.cs | 8 ++- .../Managers/Server/NetworkServer.cs | 12 ++++ .../Packets/Common/CommonItemChangePacket.cs | 13 ++-- 7 files changed, 166 insertions(+), 51 deletions(-) diff --git a/Multiplayer/Components/Networking/World/NetworkedItem.cs b/Multiplayer/Components/Networking/World/NetworkedItem.cs index 39787e5..16cc303 100644 --- a/Multiplayer/Components/Networking/World/NetworkedItem.cs +++ b/Multiplayer/Components/Networking/World/NetworkedItem.cs @@ -41,7 +41,7 @@ public static bool TryGetNetworkedItem(ItemBase item, out NetworkedItem networke #endregion private const float PositionThreshold = 0.01f; - private const float RotationThreshold = 0.1f; + private const float RotationThreshold = 0.01f; public ItemBase Item { get; private set; } private Component trackedItem; @@ -141,7 +141,7 @@ private bool Register() private void OnUngrabbed(ControlImplBase obj) { Multiplayer.LogDebug(() => $"OnUngrabbed() {name}"); - GrabbedDirty = ItemGrabbed; + GrabbedDirty = ItemGrabbed == true; ItemGrabbed = false; } @@ -149,7 +149,7 @@ private void OnUngrabbed(ControlImplBase obj) private void OnGrabbed(ControlImplBase obj) { Multiplayer.LogDebug(() => $"OnGrabbed() {name}"); - GrabbedDirty = !ItemGrabbed; + GrabbedDirty = ItemGrabbed == false; ItemGrabbed = true; } @@ -158,7 +158,7 @@ private void OnItemInventoryStateChanged(ItemBase itemBase, InventoryActionType Multiplayer.LogDebug(() => $"OnItemInventoryStateChanged() {name}, InventoryActionType: {actionType}, InventoryItemState: {itemState}"); if (actionType == InventoryActionType.Purge) { - DroppedDirty = !ItemDropped; + DroppedDirty = true; ItemDropped = true; } } @@ -267,7 +267,58 @@ public void ReceiveSnapshot(ItemUpdateData snapshot) if(snapshot == null || snapshot.UpdateType == ItemUpdateData.ItemUpdateType.None) return; - if(snapshot.UpdateType == ItemUpdateData.ItemUpdateType.Create) return; + if (snapshot.UpdateType.HasFlag(ItemUpdateData.ItemUpdateType.ItemEquipped)) + { + //do something when a player equips/unequips an item + Multiplayer.Log($"NetworkedItem.ReceiveSnapshot() netID: {snapshot.ItemNetId}, Equipped: {snapshot.Equipped}, Player ID: {snapshot.Player}"); + } + else if (snapshot.UpdateType.HasFlag(ItemUpdateData.ItemUpdateType.ItemDropped)) + { + //do something when a player drops/picks up an item + Multiplayer.Log($"NetworkedItem.ReceiveSnapshot() netID: {snapshot.ItemNetId}, Dropped: {snapshot.Dropped}, Player ID: {snapshot.Player}"); + Item.gameObject.SetActive(snapshot.Dropped); + } + else if (snapshot.UpdateType.HasFlag(ItemUpdateData.ItemUpdateType.Position)) + { + //update all values + transform.position = snapshot.PositionData.Position + WorldMover.currentMove; + transform.rotation = snapshot.PositionData.Rotation; + } + else if (snapshot.UpdateType == ItemUpdateData.ItemUpdateType.ObjectState) + { + Multiplayer.Log($"NetworkedItem.ReceiveSnapshot() netID: {snapshot.ItemNetId}, States: {snapshot?.States?.Count}"); + Multiplayer.Log($"NetworkedItem.ReceiveSnapshot() netID: {snapshot.ItemNetId}, States: {snapshot?.States?.Count}"); + + foreach (var state in snapshot.States) + { + var trackedValue = trackedValues.Find(tv => ((dynamic)tv).Key == state.Key); + if (trackedValue != null) + { + try + { + ((dynamic)trackedValue).SetValueFromObject(state.Value); + Multiplayer.LogDebug(() => $"Updated tracked value: {state.Key}"); + } + catch (Exception ex) + { + Multiplayer.LogError($"Error updating tracked value {state.Key}: {ex.Message}"); + } + } + else + { + Multiplayer.LogWarning($"Tracked value not found: {state.Key}"); + } + } + } + + //mark values as clean + CreatedDirty = false; + GrabbedDirty = false; + DroppedDirty = false; + PositionDirty = false; + + MarkValuesClean(); + return; } #endregion diff --git a/Multiplayer/Components/Networking/World/NetworkedItemManager.cs b/Multiplayer/Components/Networking/World/NetworkedItemManager.cs index 0aa0e4d..6ce4bdb 100644 --- a/Multiplayer/Components/Networking/World/NetworkedItemManager.cs +++ b/Multiplayer/Components/Networking/World/NetworkedItemManager.cs @@ -5,6 +5,7 @@ using JetBrains.Annotations; using Multiplayer.Networking.Data; using Multiplayer.Components.Networking.World; +using System; namespace Multiplayer.Components.Networking.Train; @@ -18,7 +19,10 @@ protected override void Awake() base.Awake(); if (!NetworkLifecycle.Instance.IsHost()) return; + } + protected void Start() + { NetworkLifecycle.Instance.OnTick += Common_OnTick; } @@ -43,6 +47,7 @@ public void ReceiveSnapshots(List snapshots) return; ReceivedSnapshots.AddRange(snapshots); + Multiplayer.LogDebug(() => $"ReceiveSnapshots: {ReceivedSnapshots.Count}"); } #region Common @@ -52,51 +57,62 @@ private void Common_OnTick(uint tick) //Process received Snapshots ProcessReceived(); - ProcessChanged(); + if (NetworkLifecycle.Instance.IsHost()) + ProcessChanged(); } private void ProcessReceived() { - while(ReceivedSnapshots.Count > 0) + //Multiplayer.LogDebug(() => $"ProcessReceived: {ReceivedSnapshots.Count}"); + while (ReceivedSnapshots.Count > 0) { - ItemUpdateData snapshot = ReceivedSnapshots.First(); - - //process - if (snapshot != null && snapshot.UpdateType != ItemUpdateData.ItemUpdateType.None) + + ItemUpdateData snapshot = ReceivedSnapshots.First(); + try { - //try to find an existing item - NetworkedItem.Get(snapshot.ItemNetId, out NetworkedItem netItem); + Multiplayer.LogDebug(() => $"ProcessReceived: {snapshot.UpdateType}"); - if (NetworkLifecycle.Instance.IsHost()) + //process + if (snapshot != null && snapshot.UpdateType != ItemUpdateData.ItemUpdateType.None) { - if (snapshot.UpdateType == ItemUpdateData.ItemUpdateType.Create) + //try to find an existing item + NetworkedItem.Get(snapshot.ItemNetId, out NetworkedItem netItem); + + if (NetworkLifecycle.Instance.IsHost()) { - Multiplayer.LogError($"NetworkedItemManager.ProcessReceived() Host received Create snapshot! ItemNetId: {snapshot.ItemNetId}, prefabName: {snapshot.PrefabName}"); + if (snapshot.UpdateType == ItemUpdateData.ItemUpdateType.Create) + { + Multiplayer.LogError($"NetworkedItemManager.ProcessReceived() Host received Create snapshot! ItemNetId: {snapshot.ItemNetId}, prefabName: {snapshot.PrefabName}"); + } + else + { + //we should validate if the player can perform this action... TODO later + if (netItem != null) + netItem.ReceiveSnapshot(snapshot); + else + Multiplayer.LogError($"NetworkedItemManager.ProcessReceived() NetworkedItem not found! Update Type: {snapshot?.UpdateType}, ItemNetId: {snapshot?.ItemNetId}, prefabName: {snapshot?.PrefabName}"); + } } else { - //we should validate if the player can perform this action... TODO later - if (netItem != null) - netItem.ReceiveSnapshot(snapshot); + if (snapshot.UpdateType == ItemUpdateData.ItemUpdateType.Create) + { + CreateItem(snapshot); + } else - Multiplayer.LogError($"NetworkedItemManager.ProcessReceived() NetworkedItem not found! Update Type: {snapshot?.UpdateType}, ItemNetId: {snapshot?.ItemNetId}, prefabName: {snapshot?.PrefabName}"); + { + netItem.ReceiveSnapshot(snapshot); + } } } else { - if (snapshot.UpdateType == ItemUpdateData.ItemUpdateType.Create) - { - CreateItem(snapshot); - } - else - { - netItem.ReceiveSnapshot(snapshot); - } + Multiplayer.LogError($"NetworkedItemManager.ProcessReceived() Invalid Update Type: {snapshot?.UpdateType}, ItemNetId: {snapshot?.ItemNetId}, prefabName: {snapshot?.PrefabName}"); } } - else + catch (Exception ex) { - Multiplayer.LogError($"NetworkedItemManager.ProcessReceived() Invalid Update Type: {snapshot?.UpdateType}, ItemNetId: {snapshot?.ItemNetId}, prefabName: {snapshot?.PrefabName}"); + Multiplayer.LogError($"NetworkedItemManager.ProcessReceived() Error! {ex.Message}\r\n{ex.StackTrace}"); } ReceivedSnapshots.Remove(snapshot); diff --git a/Multiplayer/Components/SaveGame/StartGameData_ServerSave.cs b/Multiplayer/Components/SaveGame/StartGameData_ServerSave.cs index 6492881..52b7c2d 100644 --- a/Multiplayer/Components/SaveGame/StartGameData_ServerSave.cs +++ b/Multiplayer/Components/SaveGame/StartGameData_ServerSave.cs @@ -1,6 +1,12 @@ using System; using System.Collections; +using System.ComponentModel; +using System.Linq; +using DV; +using DV.CabControls; using DV.UserManagement; +using DV.Utils; +using Multiplayer.Components.Networking; using Multiplayer.Components.Networking.World; using Multiplayer.Networking.Packets.Clientbound; using Multiplayer.Patches.SaveGame; @@ -52,13 +58,6 @@ public override SaveGameData GetSaveGameData() public override IEnumerator DoLoad(Transform playerContainer) { - // clear spawned world items - foreach (var item in NetworkedItem.GetAll()) - { - item.BlockSync = true; - Destroy(item); - } - Transform playerTransform = playerContainer.transform; playerTransform.position = PlayerManager.IsPlayerPositionValid(packet.Position) ? packet.Position : LevelInfo.Instance.defaultSpawnPosition; @@ -86,6 +85,34 @@ public override IEnumerator DoLoad(Transform playerContainer) // if (!string.IsNullOrEmpty(packet.Debt_insurance)) // CareerManagerDebtController.Instance.feeQuota.LoadSaveData(JObject.Parse(packet.Debt_insurance)); + // clear spawned world items + var items = NetworkedItem.GetAll().ToList(); + foreach (var item in items) + { + try + { + if (item.Item != null && !item.Item.IsEssential() && !item.Item.IsGrabbed()) + { + NetworkLifecycle.Instance.Client.LogDebug(() => $"Clearing Spawned Item: {item?.TrackedItemType?.FullName}"); + item.BlockSync = true; + + RespawnOnDrop respawn = item.Item.GetComponent(); + respawn.respawnOnDropThroughFloor = false; + item.Item.itemDisabler.ToggleInDumpster(true); + + if (SingletonBehaviour.Instance.StorageWorld.ContainsItem(item.Item)) + { + SingletonBehaviour.Instance.RemoveItemFromWorldStorage(item.Item); + } + //Destroy(item.gameObject); + } + } + catch (Exception ex) + { + NetworkLifecycle.Instance.Client.LogDebug(() => $"Error Clearing Spawned Item: {ex.Message}"); + } + } + carsAndJobsLoadingFinished = true; yield break; } diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index cb3ab17..da77553 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -126,7 +126,7 @@ protected override void Subscribe() netPacketProcessor.SubscribeReusable(OnClientboundJobsCreatePacket); netPacketProcessor.SubscribeReusable(OnClientboundJobValidateResponsePacket); netPacketProcessor.SubscribeReusable(OnCommonChatPacket); - netPacketProcessor.SubscribeNetSerializable(OnCommonItemChangePacket); + netPacketProcessor.SubscribeNetSerializable(OnCommonItemChangePacket); } #region Net Events @@ -324,6 +324,8 @@ private void OnClientboundSaveGameDataPacket(ClientboundSaveGameDataPacket packe WorldStreamingInit.LoadingFinished += SendReadyPacket; TrainStress.globalIgnoreStressCalculation = true; + + NetworkedItemManager.Instance.CheckInstance(); } private void OnClientboundBeginWorldSyncPacket(ClientboundBeginWorldSyncPacket packet) @@ -775,15 +777,15 @@ private void OnClientboundJobValidateResponsePacket(ClientboundJobValidateRespon GameObject.Destroy(networkedJob.gameObject); } - private void OnCommonItemChangePacket(CommonItemChangePacket packet, NetPeer peer) + private void OnCommonItemChangePacket(CommonItemChangePacket packet) { - LogDebug(() => $"OnCommonItemChangePacket({packet?.Items?.Count}, {peer.Id})"); + LogDebug(() => $"OnCommonItemChangePacket({packet?.Items?.Count})"); string debug = ""; foreach (var item in packet?.Items) { - LogDebug(() => $"OnCommonItemChangePacket({packet?.Items?.Count}, {peer.Id}) in loop"); + //LogDebug(() => $"OnCommonItemChangePacket({packet?.Items?.Count}, {peer.Id}) in loop"); debug += "UpdateType: " + item?.UpdateType + "\r\n"; debug += "itemNetId: " + item?.ItemNetId + "\r\n"; debug += "PrefabName: " + item?.PrefabName + "\r\n"; @@ -792,7 +794,7 @@ private void OnCommonItemChangePacket(CommonItemChangePacket packet, NetPeer pee debug += "Position: " + item?.PositionData.Position + "\r\n"; debug += "Rotation: " + item?.PositionData.Rotation + "\r\n"; - LogDebug(() => $"OnCommonItemChangePacket({packet?.Items?.Count}, {peer.Id}) prep states"); + //LogDebug(() => $"OnCommonItemChangePacket({packet?.Items?.Count}, {peer.Id}) prep states"); debug += "States:"; if (item.States != null) @@ -1108,8 +1110,8 @@ public void SendChat(string message) public void SendItemsChangePacket(List items, NetPeer peer = null) { Multiplayer.Log($"Sending SendItemsChangePacket with {items.Count()} items"); - SendPacketToServer(new CommonItemChangePacket { Items = items }, - DeliveryMethod.ReliableUnordered); + //SendPacketToServer(new CommonItemChangePacket { Items = items }, + // DeliveryMethod.ReliableUnordered); } #endregion diff --git a/Multiplayer/Networking/Managers/NetworkManager.cs b/Multiplayer/Networking/Managers/NetworkManager.cs index eed731f..0848124 100644 --- a/Multiplayer/Networking/Managers/NetworkManager.cs +++ b/Multiplayer/Networking/Managers/NetworkManager.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Net; using System.Net.Sockets; using LiteNetLib; @@ -96,7 +97,12 @@ public virtual void Stop() { peer?.Send(WritePacket(packet), deliveryMethod); } - + + protected void SendNetSerializablePacket(NetPeer peer, T packet, DeliveryMethod deliveryMethod) where T : INetSerializable, new() + { + peer?.Send(WriteNetSerializablePacket(packet), deliveryMethod); + } + protected void SendUnconnectedPacket(T packet, string ipAddress, int port) where T : class, new() { netManager.SendUnconnectedMessage(WritePacket(packet), ipAddress, port); diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index aef49e1..8be99ad 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -31,6 +31,7 @@ using System.Net; using Multiplayer.Networking.Packets.Serverbound.Train; using Multiplayer.Networking.Packets.Unconnected; +using DV.CabControls.Spec; namespace Multiplayer.Networking.Listeners; @@ -418,6 +419,7 @@ public void SendJobsUpdatePacket(ushort stationNetId, NetworkedJob[] jobs, NetPe public void SendItemsChangePacket(List items, NetPeer peer = null) { Multiplayer.Log($"Sending SendItemsChangePacket with {items.Count()} items"); + SendNetSerializablePacketToAll(new CommonItemChangePacket { Items = items }, DeliveryMethod.ReliableUnordered, selfPeer); @@ -646,6 +648,16 @@ private void OnServerboundClientReadyPacket(ServerboundClientReadyPacket packet, } } + //Send Item Sync + + List snapshots = new List(); + foreach (var item in NetworkedItem.GetAll()) + { + snapshots.Add(item.CreateUpdateData(ItemUpdateData.ItemUpdateType.Create)); + } + + LogDebug(() => $"Sending sync ItemUpdateData {snapshots.Count} items"); + SendNetSerializablePacket(peer, new CommonItemChangePacket { Items = snapshots }, DeliveryMethod.ReliableOrdered); // Send existing players foreach (ServerPlayer player in ServerPlayers) diff --git a/Multiplayer/Networking/Packets/Common/CommonItemChangePacket.cs b/Multiplayer/Networking/Packets/Common/CommonItemChangePacket.cs index a6a8cf2..2b4fa37 100644 --- a/Multiplayer/Networking/Packets/Common/CommonItemChangePacket.cs +++ b/Multiplayer/Networking/Packets/Common/CommonItemChangePacket.cs @@ -1,12 +1,8 @@ using LiteNetLib.Utils; using Multiplayer.Networking.Data; -using System.IO; -using DV.Logic.Job; using System.Collections.Generic; -using System.Linq; using System; - namespace Multiplayer.Networking.Packets.Common; public class CommonItemChangePacket : INetSerializable @@ -17,8 +13,13 @@ public class CommonItemChangePacket : INetSerializable public void Deserialize(NetDataReader reader) { + + Items.Clear(); + Multiplayer.Log("CommonItemChangePacket.Deserialize()"); + //Multiplayer.LogDebug(() => $"CommonItemChangePacket.Deserialize()\r\nBytes: {BitConverter.ToString(reader.RawData).Replace("-", " ")}"); + Multiplayer.Log($"CommonItemChangePacket.Deserialize() Pre-itemCount {Items?.Count} "); try { bool compressed = reader.GetBool(); @@ -31,7 +32,7 @@ public void Deserialize(NetDataReader reader) DeserializeRaw(reader); } - + Multiplayer.Log($"CommonItemChangePacket.Deserialize() post-itemCount {Items?.Count} "); } catch (Exception ex) { @@ -43,7 +44,7 @@ private void DeserializeCompressed(NetDataReader reader) { int itemCount = reader.GetInt(); byte[] compressedData = reader.GetBytesWithLength(); - //Multiplayer.Log($"CommonItemChangePacket.DeserializeCompressed() itemCount {itemCount} length: {compressedData.Length}"); + Multiplayer.Log($"CommonItemChangePacket.DeserializeCompressed() itemCount {itemCount} length: {compressedData.Length}"); byte[] decompressedData = PacketCompression.Decompress(compressedData); //Multiplayer.Log($"CommonItemChangePacket.DeserializeCompressed() Compressed: {compressedData.Length} Decompressed: {decompressedData.Length}"); From bb02a7d8c63e96ed4fded6a3357fed9bff2b360f Mon Sep 17 00:00:00 2001 From: AMacro Date: Mon, 21 Oct 2024 14:03:20 +1000 Subject: [PATCH 108/188] Allow servers with names up to 25 chars (minor bug) --- Multiplayer/Components/MainMenu/HostGamePane.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Multiplayer/Components/MainMenu/HostGamePane.cs b/Multiplayer/Components/MainMenu/HostGamePane.cs index 852b739..e097302 100644 --- a/Multiplayer/Components/MainMenu/HostGamePane.cs +++ b/Multiplayer/Components/MainMenu/HostGamePane.cs @@ -329,7 +329,7 @@ private void ValidateInputs(string text) bool valid = true; int portNum=0; - if (serverName.text.Trim() == "" || serverName.text.Length >= MAX_SERVER_NAME_LEN) + if (serverName.text.Trim() == "" || serverName.text.Length > MAX_SERVER_NAME_LEN) valid = false; if (port.text != "") From 0809fd27176c83745993f1b15a58853b865b69da Mon Sep 17 00:00:00 2001 From: AMacro Date: Mon, 21 Oct 2024 21:19:12 +1000 Subject: [PATCH 109/188] Increased initial update speed --- Multiplayer/Components/MainMenu/ServerBrowserPane.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs index 2dbbf28..8ce2022 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs @@ -97,7 +97,7 @@ private enum ConnectionState //LAN tracking private List localServers = new List(); private const int LAN_TIMEOUT = 60; //How long to hold a LAN server without a response - private const int DISCOVERY_TIMEOUT = 2; //how long to wait for servers to respond + private const int DISCOVERY_TIMEOUT = 1; //how long to wait for servers to respond private bool localRefreshComplete; private float discoveryTimer = 0f; @@ -142,9 +142,6 @@ private void Awake() SetupServerBrowser(); RefreshGridView(); - - buttonRefresh.ToggleInteractable(true); - RefreshAction(); } private void OnEnable() @@ -167,6 +164,8 @@ private void OnEnable() serverBrowserClient.OnPing += this.OnPing; serverBrowserClient.OnDiscovery += this.OnDiscovery; serverBrowserClient.Start(); + + RefreshAction(); } // Disable listeners From 3b19f2dadbe9a7cc179eb35ac99eb2d091629d8b Mon Sep 17 00:00:00 2001 From: AMacro Date: Mon, 21 Oct 2024 21:20:29 +1000 Subject: [PATCH 110/188] Refactoring and improving item sync --- .../Networking/World/NetworkedItem.cs | 29 ++- .../Networking/World/NetworkedItemManager.cs | 236 ++++++++++++++---- .../SaveGame/StartGameData_ServerSave.cs | 28 --- .../Managers/Client/NetworkClient.cs | 58 +++-- .../Managers/Server/NetworkServer.cs | 6 +- .../Packets/Common/CommonItemChangePacket.cs | 25 +- .../Patches/World/Items/LanternPatch.cs | 3 +- .../Patches/World/Items/LighterPatch.cs | 3 +- 8 files changed, 258 insertions(+), 130 deletions(-) diff --git a/Multiplayer/Components/Networking/World/NetworkedItem.cs b/Multiplayer/Components/Networking/World/NetworkedItem.cs index 16cc303..ca6cf6b 100644 --- a/Multiplayer/Components/Networking/World/NetworkedItem.cs +++ b/Multiplayer/Components/Networking/World/NetworkedItem.cs @@ -267,27 +267,32 @@ public void ReceiveSnapshot(ItemUpdateData snapshot) if(snapshot == null || snapshot.UpdateType == ItemUpdateData.ItemUpdateType.None) return; + //Multiplayer.LogDebug(()=>$"NetworkedItem.ReceiveSnapshot() netID: {snapshot.ItemNetId}, {snapshot.UpdateType}"); + if (snapshot.UpdateType.HasFlag(ItemUpdateData.ItemUpdateType.ItemEquipped)) { //do something when a player equips/unequips an item Multiplayer.Log($"NetworkedItem.ReceiveSnapshot() netID: {snapshot.ItemNetId}, Equipped: {snapshot.Equipped}, Player ID: {snapshot.Player}"); + } - else if (snapshot.UpdateType.HasFlag(ItemUpdateData.ItemUpdateType.ItemDropped)) + + if (snapshot.UpdateType.HasFlag(ItemUpdateData.ItemUpdateType.ItemDropped)) { //do something when a player drops/picks up an item Multiplayer.Log($"NetworkedItem.ReceiveSnapshot() netID: {snapshot.ItemNetId}, Dropped: {snapshot.Dropped}, Player ID: {snapshot.Player}"); Item.gameObject.SetActive(snapshot.Dropped); } - else if (snapshot.UpdateType.HasFlag(ItemUpdateData.ItemUpdateType.Position)) + + if (snapshot.UpdateType.HasFlag(ItemUpdateData.ItemUpdateType.Position) || snapshot.UpdateType.HasFlag(ItemUpdateData.ItemUpdateType.Create)) { //update all values transform.position = snapshot.PositionData.Position + WorldMover.currentMove; transform.rotation = snapshot.PositionData.Rotation; } - else if (snapshot.UpdateType == ItemUpdateData.ItemUpdateType.ObjectState) + + if (snapshot.UpdateType == ItemUpdateData.ItemUpdateType.ObjectState || snapshot.UpdateType.HasFlag(ItemUpdateData.ItemUpdateType.Create)) { - Multiplayer.Log($"NetworkedItem.ReceiveSnapshot() netID: {snapshot.ItemNetId}, States: {snapshot?.States?.Count}"); - Multiplayer.Log($"NetworkedItem.ReceiveSnapshot() netID: {snapshot.ItemNetId}, States: {snapshot?.States?.Count}"); + //Multiplayer.Log($"NetworkedItem.ReceiveSnapshot() netID: {snapshot.ItemNetId}, States: {snapshot?.States?.Count}"); foreach (var state in snapshot.States) { @@ -350,16 +355,16 @@ protected override void OnDestroy() { NetworkedItemManager.Instance.AddDirtyItemSnapshot(CreateUpdateData(ItemUpdateData.ItemUpdateType.Destroy)); } + /* else if(!BlockSync) { - Multiplayer.LogWarning($"NetworkedItem.OnDestroy({name}, {NetId})\r\n{new System.Diagnostics.StackTrace()}"); + Multiplayer.LogWarning($"NetworkedItem.OnDestroy({name}, {NetId})");/*\r\n{new System.Diagnostics.StackTrace()} } else { - Multiplayer.LogDebug(()=>$"NetworkedItem.OnDestroy({name}, {NetId}) Sync blocked"); - } + Multiplayer.LogDebug(()=>$"NetworkedItem.OnDestroy({name}, {NetId})");/*\r\n{new System.Diagnostics.StackTrace()} + }*/ - base.OnDestroy(); if (Item != null) { Item.Grabbed -= OnGrabbed; @@ -367,6 +372,12 @@ protected override void OnDestroy() Item.ItemInventoryStateChanged -= OnItemInventoryStateChanged; itemBaseToNetworkedItem.Remove(Item); } + else + { + Multiplayer.LogWarning($"NetworkedItem.OnDestroy({name}, {NetId}) Item is null!"); + } + + base.OnDestroy(); } diff --git a/Multiplayer/Components/Networking/World/NetworkedItemManager.cs b/Multiplayer/Components/Networking/World/NetworkedItemManager.cs index 6ce4bdb..7ab1037 100644 --- a/Multiplayer/Components/Networking/World/NetworkedItemManager.cs +++ b/Multiplayer/Components/Networking/World/NetworkedItemManager.cs @@ -6,19 +6,23 @@ using Multiplayer.Networking.Data; using Multiplayer.Components.Networking.World; using System; +using Multiplayer.Utils; +using DV.CabControls.Spec; namespace Multiplayer.Components.Networking.Train; public class NetworkedItemManager : SingletonBehaviour { private List DirtyItems = new List(); - private List ReceivedSnapshots = new List(); + private Queue ReceivedSnapshots = new Queue(); + private Dictionary> CachedItems = new Dictionary>(); - protected override void Awake() +protected override void Awake() { base.Awake(); if (!NetworkLifecycle.Instance.IsHost()) return; + } protected void Start() @@ -46,7 +50,11 @@ public void ReceiveSnapshots(List snapshots) if (snapshots == null) return; - ReceivedSnapshots.AddRange(snapshots); + foreach (var snapshot in snapshots) + { + ReceivedSnapshots.Enqueue(snapshot); + } + Multiplayer.LogDebug(() => $"ReceiveSnapshots: {ReceivedSnapshots.Count}"); } @@ -63,59 +71,32 @@ private void Common_OnTick(uint tick) private void ProcessReceived() { - //Multiplayer.LogDebug(() => $"ProcessReceived: {ReceivedSnapshots.Count}"); while (ReceivedSnapshots.Count > 0) { - - ItemUpdateData snapshot = ReceivedSnapshots.First(); + ItemUpdateData snapshot = ReceivedSnapshots.Dequeue(); try { - Multiplayer.LogDebug(() => $"ProcessReceived: {snapshot.UpdateType}"); + //Multiplayer.LogDebug(() => $"ProcessReceived: {snapshot.UpdateType}"); - //process - if (snapshot != null && snapshot.UpdateType != ItemUpdateData.ItemUpdateType.None) + if (snapshot == null || snapshot.UpdateType == ItemUpdateData.ItemUpdateType.None) { - //try to find an existing item - NetworkedItem.Get(snapshot.ItemNetId, out NetworkedItem netItem); - - if (NetworkLifecycle.Instance.IsHost()) - { - if (snapshot.UpdateType == ItemUpdateData.ItemUpdateType.Create) - { - Multiplayer.LogError($"NetworkedItemManager.ProcessReceived() Host received Create snapshot! ItemNetId: {snapshot.ItemNetId}, prefabName: {snapshot.PrefabName}"); - } - else - { - //we should validate if the player can perform this action... TODO later - if (netItem != null) - netItem.ReceiveSnapshot(snapshot); - else - Multiplayer.LogError($"NetworkedItemManager.ProcessReceived() NetworkedItem not found! Update Type: {snapshot?.UpdateType}, ItemNetId: {snapshot?.ItemNetId}, prefabName: {snapshot?.PrefabName}"); - } - } - else - { - if (snapshot.UpdateType == ItemUpdateData.ItemUpdateType.Create) - { - CreateItem(snapshot); - } - else - { - netItem.ReceiveSnapshot(snapshot); - } - } + Multiplayer.LogError($"NetworkedItemManager.ProcessReceived() Invalid Update Type: {snapshot?.UpdateType}, ItemNetId: {snapshot?.ItemNetId}, prefabName: {snapshot?.PrefabName}"); + continue; + } + + if (NetworkLifecycle.Instance.IsHost()) + { + ProcessReceivedAsHost(snapshot); } else { - Multiplayer.LogError($"NetworkedItemManager.ProcessReceived() Invalid Update Type: {snapshot?.UpdateType}, ItemNetId: {snapshot?.ItemNetId}, prefabName: {snapshot?.PrefabName}"); + ProcessReceivedAsClient(snapshot); } } catch (Exception ex) { Multiplayer.LogError($"NetworkedItemManager.ProcessReceived() Error! {ex.Message}\r\n{ex.StackTrace}"); } - - ReceivedSnapshots.Remove(snapshot); } } @@ -145,6 +126,61 @@ private void ProcessChanged() DirtyItems.Clear(); } + #endregion + + #region Server + + private void ProcessReceivedAsHost(ItemUpdateData snapshot) + { + if (snapshot.UpdateType == ItemUpdateData.ItemUpdateType.Create) + { + Multiplayer.LogError($"NetworkedItemManager.ProcessReceived() Host received Create snapshot! ItemNetId: {snapshot.ItemNetId}, prefabName: {snapshot.PrefabName}"); + return; + } + + if (NetworkedItem.Get(snapshot.ItemNetId, out NetworkedItem netItem)) + { + if (ValidatePlayerAction(snapshot)) //Ensure the player can do this + { + netItem.ReceiveSnapshot(snapshot); + } + else + { + Multiplayer.LogWarning($"NetworkedItemManager.ProcessReceived() Player action validation failed for ItemNetId: {snapshot.ItemNetId}"); + } + } + else + { + Multiplayer.LogError($"NetworkedItemManager.ProcessReceived() NetworkedItem not found! Update Type: {snapshot.UpdateType}, ItemNetId: {snapshot.ItemNetId}, prefabName: {snapshot.PrefabName}"); + } + } + + private bool ValidatePlayerAction(ItemUpdateData snapshot) + { + return true; // Placeholder + } + + #endregion + + #region Client + private void ProcessReceivedAsClient(ItemUpdateData snapshot) + { + if (snapshot.UpdateType == ItemUpdateData.ItemUpdateType.Create) + { + CreateItem(snapshot); + } + else if (NetworkedItem.Get(snapshot.ItemNetId, out NetworkedItem netItem)) + { + netItem.ReceiveSnapshot(snapshot); + } + else + { + Multiplayer.LogError($"NetworkedItemManager.ProcessReceived() NetworkedItem not found on client! Update Type: {snapshot.UpdateType}, ItemNetId: {snapshot.ItemNetId}, prefabName: {snapshot.PrefabName}"); + } + } + #endregion + + #region Item Cache And Management private void CreateItem(ItemUpdateData snapshot) { if(snapshot == null || snapshot.ItemNetId == 0) @@ -153,28 +189,124 @@ private void CreateItem(ItemUpdateData snapshot) return; } - GameObject prefabObj = Resources.Load(snapshot.PrefabName) as GameObject; + NetworkedItem newItem = GetFromCache(snapshot.PrefabName); - if (prefabObj == null) + if(newItem == null) { - Multiplayer.LogError($"NetworkedItemManager.CreateItem() Unable to load prefab for ItemNetId: {snapshot.ItemNetId}, prefabName: {snapshot.PrefabName}"); - return; + GameObject prefabObj = Resources.Load(snapshot.PrefabName) as GameObject; + + if (prefabObj == null) + { + Multiplayer.LogError($"NetworkedItemManager.CreateItem() Unable to load prefab for ItemNetId: {snapshot.ItemNetId}, prefabName: {snapshot.PrefabName}"); + return; + } + + //create a new item + GameObject gameObject = Instantiate(prefabObj, snapshot.PositionData.Position, snapshot.PositionData.Rotation); + + //Make sure we have a NetworkedItem + newItem = gameObject.GetOrAddComponent(); } - //create a new item - GameObject gameObject = UnityEngine.Object.Instantiate(prefabObj, snapshot.PositionData.Position, snapshot.PositionData.Rotation); + newItem.gameObject.SetActive(true); + + //InventoryItemSpec component = newItem.GetComponent(); + //if (newItem.Item.InventorySpecs != null) + // newItem.Item.InventorySpecs.BelongsToPlayer = false; + + //SingletonBehaviour.Instance.AddItemToWorldStorage(newItem.Item); - InventoryItemSpec component = gameObject.GetComponent(); - if (component != null) - component.BelongsToPlayer = true; - - NetworkedItem newItem = gameObject.AddComponent(); newItem.NetId = snapshot.ItemNetId; newItem.ReceiveSnapshot(snapshot); } + public void CacheWorldItems() + { + if (NetworkLifecycle.Instance.IsHost()) + return; + + NetworkLifecycle.Instance.Client.LogDebug(() => $"CacheWorldItems()"); + + // Remove all spawned world items and place them into a cache for later use + var items = NetworkedItem.GetAll().ToList(); + foreach (var item in items) + { + try + { + if (item.Item != null && !item.Item.IsEssential() && !item.Item.IsGrabbed() && !StorageController.Instance.StorageInventory.ContainsItem(item.Item)) + { + SendToCache(item); + } + else + { + NetworkLifecycle.Instance.Client.LogDebug(() => $"CacheWorldItems() Not caching: {item.Item.InventorySpecs.previewPrefab} is in Inventory: {StorageController.Instance.StorageInventory.ContainsItem(item.Item)}"); + } + } + catch (Exception ex) + { + NetworkLifecycle.Instance.Client.LogDebug(() => $"Error Caching Spawned Item: {ex.Message}"); + } + } + } + + private NetworkedItem GetFromCache(string prefabName) + { + if (CachedItems.TryGetValue(prefabName, out var items) && items.Count > 0) + { + //NetworkLifecycle.Instance.Client.LogDebug(() => $"GetFromCache({prefabName}) Cache Hit"); + var cachedItem = items[items.Count - 1]; + items.RemoveAt(items.Count - 1); + return cachedItem; + } + + //NetworkLifecycle.Instance.Client.LogDebug(() => $"GetFromCache({prefabName}) Cache Miss!"); + return null; + } + + private void SendToCache(NetworkedItem netItem) + { + string prefabName = netItem?.Item?.InventorySpecs?.itemPrefabName; + + NetworkLifecycle.Instance.Client.LogDebug(() => $"Caching Spawned Item: {prefabName ?? ""}"); + + netItem.BlockSync = true; + + netItem.gameObject.SetActive(false); + RespawnOnDrop respawn = netItem.Item.GetComponent(); + + Destroy(respawn); + + + NetworkLifecycle.Instance.Client.LogDebug(() => $"Caching Spawned Item: {prefabName ?? ""}: checkWhileDisabled {respawn.checkWhileDisabled}, ignoreDistanceFromSpawnPosition {respawn.ignoreDistanceFromSpawnPosition}, respawnOnDropThroughFloor {respawn.respawnOnDropThroughFloor}"); + + + //respawn.checkWhileDisabled = false; + //respawn.ignoreDistanceFromSpawnPosition = true; + //respawn.respawnOnDropThroughFloor = false; + //netItem.Item.itemDisabler.ToggleInDumpster(false); + + if (SingletonBehaviour.Instance.StorageWorld.ContainsItem(netItem.Item)) + { + SingletonBehaviour.Instance.RemoveItemFromWorldStorage(netItem.Item); + } + + netItem.Item.InventorySpecs.BelongsToPlayer = false; + netItem.NetId = 0; + + + + if (!CachedItems.ContainsKey(prefabName)) + { + CachedItems[prefabName] = new List(); + } + CachedItems[prefabName].Add(netItem); + } + #endregion + + + [UsedImplicitly] public new static string AllowAutoCreate() { diff --git a/Multiplayer/Components/SaveGame/StartGameData_ServerSave.cs b/Multiplayer/Components/SaveGame/StartGameData_ServerSave.cs index 52b7c2d..9ba5514 100644 --- a/Multiplayer/Components/SaveGame/StartGameData_ServerSave.cs +++ b/Multiplayer/Components/SaveGame/StartGameData_ServerSave.cs @@ -85,34 +85,6 @@ public override IEnumerator DoLoad(Transform playerContainer) // if (!string.IsNullOrEmpty(packet.Debt_insurance)) // CareerManagerDebtController.Instance.feeQuota.LoadSaveData(JObject.Parse(packet.Debt_insurance)); - // clear spawned world items - var items = NetworkedItem.GetAll().ToList(); - foreach (var item in items) - { - try - { - if (item.Item != null && !item.Item.IsEssential() && !item.Item.IsGrabbed()) - { - NetworkLifecycle.Instance.Client.LogDebug(() => $"Clearing Spawned Item: {item?.TrackedItemType?.FullName}"); - item.BlockSync = true; - - RespawnOnDrop respawn = item.Item.GetComponent(); - respawn.respawnOnDropThroughFloor = false; - item.Item.itemDisabler.ToggleInDumpster(true); - - if (SingletonBehaviour.Instance.StorageWorld.ContainsItem(item.Item)) - { - SingletonBehaviour.Instance.RemoveItemFromWorldStorage(item.Item); - } - //Destroy(item.gameObject); - } - } - catch (Exception ex) - { - NetworkLifecycle.Instance.Client.LogDebug(() => $"Error Clearing Spawned Item: {ex.Message}"); - } - } - carsAndJobsLoadingFinished = true; yield break; } diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index da77553..7120700 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -321,11 +321,19 @@ private void OnClientboundSaveGameDataPacket(ClientboundSaveGameDataPacket packe Object.DontDestroyOnLoad(go); SceneSwitcher.SwitchToScene(DVScenes.Game); - WorldStreamingInit.LoadingFinished += SendReadyPacket; + WorldStreamingInit.LoadingFinished += () => + { + LogDebug(() => $"WorldStreamingInit.LoadingFinished()"); + NetworkedItemManager.Instance.CheckInstance(); + LogDebug(() => $"WorldStreamingInit.LoadingFinished() CacheWorldItems()"); + NetworkedItemManager.Instance.CacheWorldItems(); + LogDebug(() => $"WorldStreamingInit.LoadingFinished() SendReadyPacket()"); + SendReadyPacket(); + }; + TrainStress.globalIgnoreStressCalculation = true; - NetworkedItemManager.Instance.CheckInstance(); } private void OnClientboundBeginWorldSyncPacket(ClientboundBeginWorldSyncPacket packet) @@ -781,28 +789,34 @@ private void OnCommonItemChangePacket(CommonItemChangePacket packet) { LogDebug(() => $"OnCommonItemChangePacket({packet?.Items?.Count})"); - string debug = ""; + /* + Multiplayer.LogDebug(() => + { + string debug = ""; - foreach (var item in packet?.Items) - { - //LogDebug(() => $"OnCommonItemChangePacket({packet?.Items?.Count}, {peer.Id}) in loop"); - debug += "UpdateType: " + item?.UpdateType + "\r\n"; - debug += "itemNetId: " + item?.ItemNetId + "\r\n"; - debug += "PrefabName: " + item?.PrefabName + "\r\n"; - debug += "Equipped: " + item?.Equipped + "\r\n"; - debug += "Dropped: " + item?.Dropped + "\r\n"; - debug += "Position: " + item?.PositionData.Position + "\r\n"; - debug += "Rotation: " + item?.PositionData.Rotation + "\r\n"; - - //LogDebug(() => $"OnCommonItemChangePacket({packet?.Items?.Count}, {peer.Id}) prep states"); - debug += "States:"; - - if (item.States != null) - foreach (var state in item?.States) - debug += "\r\n\t" + state.Key + ": " + state.Value; - } + foreach (var item in packet?.Items) + { + //LogDebug(() => $"OnCommonItemChangePacket({packet?.Items?.Count}, {peer.Id}) in loop"); + debug += "UpdateType: " + item?.UpdateType + "\r\n"; + debug += "itemNetId: " + item?.ItemNetId + "\r\n"; + debug += "PrefabName: " + item?.PrefabName + "\r\n"; + debug += "Equipped: " + item?.Equipped + "\r\n"; + debug += "Dropped: " + item?.Dropped + "\r\n"; + debug += "Position: " + item?.PositionData.Position + "\r\n"; + debug += "Rotation: " + item?.PositionData.Rotation + "\r\n"; + + //LogDebug(() => $"OnCommonItemChangePacket({packet?.Items?.Count}, {peer.Id}) prep states"); + debug += "States:"; + + if (item.States != null) + foreach (var state in item?.States) + debug += "\r\n\t" + state.Key + ": " + state.Value; + } - Multiplayer.LogDebug(() => debug); + return debug; + } + ); + */ NetworkedItemManager.Instance.ReceiveSnapshots(packet.Items); } diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index 8be99ad..d5e6d9f 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -653,7 +653,11 @@ private void OnServerboundClientReadyPacket(ServerboundClientReadyPacket packet, List snapshots = new List(); foreach (var item in NetworkedItem.GetAll()) { - snapshots.Add(item.CreateUpdateData(ItemUpdateData.ItemUpdateType.Create)); + //only send items that are close to the player + float sqDist = (serverPlayer.WorldPosition - item.transform.position).sqrMagnitude; + + if (sqDist < 1000f ) + snapshots.Add(item.CreateUpdateData(ItemUpdateData.ItemUpdateType.Create)); } LogDebug(() => $"Sending sync ItemUpdateData {snapshots.Count} items"); diff --git a/Multiplayer/Networking/Packets/Common/CommonItemChangePacket.cs b/Multiplayer/Networking/Packets/Common/CommonItemChangePacket.cs index 2b4fa37..0445a11 100644 --- a/Multiplayer/Networking/Packets/Common/CommonItemChangePacket.cs +++ b/Multiplayer/Networking/Packets/Common/CommonItemChangePacket.cs @@ -16,10 +16,9 @@ public void Deserialize(NetDataReader reader) Items.Clear(); - Multiplayer.Log("CommonItemChangePacket.Deserialize()"); - + //Multiplayer.LogDebug(()=>"CommonItemChangePacket.Deserialize()"); //Multiplayer.LogDebug(() => $"CommonItemChangePacket.Deserialize()\r\nBytes: {BitConverter.ToString(reader.RawData).Replace("-", " ")}"); - Multiplayer.Log($"CommonItemChangePacket.Deserialize() Pre-itemCount {Items?.Count} "); + try { bool compressed = reader.GetBool(); @@ -32,7 +31,7 @@ public void Deserialize(NetDataReader reader) DeserializeRaw(reader); } - Multiplayer.Log($"CommonItemChangePacket.Deserialize() post-itemCount {Items?.Count} "); + //Multiplayer.LogDebug(() => $"CommonItemChangePacket.Deserialize() post-itemCount {Items?.Count} "); } catch (Exception ex) { @@ -44,7 +43,7 @@ private void DeserializeCompressed(NetDataReader reader) { int itemCount = reader.GetInt(); byte[] compressedData = reader.GetBytesWithLength(); - Multiplayer.Log($"CommonItemChangePacket.DeserializeCompressed() itemCount {itemCount} length: {compressedData.Length}"); + //Multiplayer.LogDebug(() => $"CommonItemChangePacket.DeserializeCompressed() itemCount {itemCount} length: {compressedData.Length}"); byte[] decompressedData = PacketCompression.Decompress(compressedData); //Multiplayer.Log($"CommonItemChangePacket.DeserializeCompressed() Compressed: {compressedData.Length} Decompressed: {decompressedData.Length}"); @@ -64,25 +63,19 @@ private void DeserializeCompressed(NetDataReader reader) private void DeserializeRaw(NetDataReader reader) { int itemCount = reader.GetInt(); - Multiplayer.Log($"CommonItemChangePacket.DeserializeRaw() itemCount: {itemCount}"); - - //Items.Capacity = itemCount; + //Multiplayer.LogDebug(() => $"CommonItemChangePacket.DeserializeRaw() itemCount: {itemCount}"); - //Multiplayer.Log($"CommonItemChangePacket.DeserializeRaw() itemCount: {itemCount}, pre-loop"); for (int i = 0; i < itemCount; i++) { - //Multiplayer.Log($"CommonItemChangePacket.DeserializeRaw() itemCount: {itemCount}, new ItemUpdateData()"); var item = new ItemUpdateData(); - //Multiplayer.Log($"CommonItemChangePacket.DeserializeRaw() itemCount: {itemCount}, item.Deserialize()"); item.Deserialize(reader); - //Multiplayer.Log($"CommonItemChangePacket.DeserializeRaw() itemCount: {itemCount}, Items.Add()"); Items.Add(item); } } public void Serialize(NetDataWriter writer) { - Multiplayer.Log("CommonItemChangePacket.Serialize()"); + //Multiplayer.LogDebug(() => "CommonItemChangePacket.Serialize()"); //Multiplayer.LogDebug(() => $"CommonItemChangePacket.Serialize() Data Before\r\nBytes: {BitConverter.ToString(writer.CopyData()).Replace("-", " ")}"); try @@ -106,7 +99,7 @@ public void Serialize(NetDataWriter writer) private void SerializeCompressed(NetDataWriter writer) { - Multiplayer.Log($"CommonItemChangePacket.Serialize() Compressing. Item Count: {Items.Count}"); + //Multiplayer.LogDebug(() => $"CommonItemChangePacket.Serialize() Compressing. Item Count: {Items.Count}"); writer.Put(true); // compressed data stream writer.Put(Items.Count); @@ -118,13 +111,13 @@ private void SerializeCompressed(NetDataWriter writer) } byte[] compressedData = PacketCompression.Compress(dataWriter.Data); - Multiplayer.Log($"Uncompressed: {dataWriter.Length} Compressed: {compressedData.Length}"); + //Multiplayer.LogDebug(() => $"Uncompressed: {dataWriter.Length} Compressed: {compressedData.Length}"); writer.PutBytesWithLength(compressedData); } private void SerializeRaw(NetDataWriter writer) { - Multiplayer.Log($"CommonItemChangePacket.Serialize() Raw. Item Count: {Items.Count}"); + //Multiplayer.LogDebug(() => $"CommonItemChangePacket.Serialize() Raw. Item Count: {Items.Count}"); writer.Put(false); // uncompressed data stream writer.Put(Items.Count); foreach (var item in Items) diff --git a/Multiplayer/Patches/World/Items/LanternPatch.cs b/Multiplayer/Patches/World/Items/LanternPatch.cs index 4020f23..005fade 100644 --- a/Multiplayer/Patches/World/Items/LanternPatch.cs +++ b/Multiplayer/Patches/World/Items/LanternPatch.cs @@ -1,5 +1,6 @@ using HarmonyLib; using Multiplayer.Components.Networking.World; +using Multiplayer.Utils; using System; using System.Collections.Generic; using System.Linq; @@ -13,7 +14,7 @@ public static class LanternAwakePatch { static void Postfix(Lantern __instance) { - var networkedItem = __instance.gameObject.AddComponent(); + var networkedItem = __instance.gameObject.GetOrAddComponent(); networkedItem.Initialize(__instance); // Register the values you want to track with both getters and setters diff --git a/Multiplayer/Patches/World/Items/LighterPatch.cs b/Multiplayer/Patches/World/Items/LighterPatch.cs index 13cc7f7..7e995cd 100644 --- a/Multiplayer/Patches/World/Items/LighterPatch.cs +++ b/Multiplayer/Patches/World/Items/LighterPatch.cs @@ -1,5 +1,6 @@ using HarmonyLib; using Multiplayer.Components.Networking.World; +using Multiplayer.Utils; using System; using System.Collections.Generic; using System.Linq; @@ -13,7 +14,7 @@ public static class LighterAwakePatch { static void Postfix(Lighter __instance) { - var networkedItem = __instance.gameObject.AddComponent(); + var networkedItem = __instance.gameObject.GetOrAddComponent(); networkedItem.Initialize(__instance); // Register the values you want to track with both getters and setters From 352b35753960776c0bb22570e301df14b91ddb86 Mon Sep 17 00:00:00 2001 From: AMacro Date: Tue, 22 Oct 2024 16:09:35 +1000 Subject: [PATCH 111/188] Begin implementation of item sync for distance from player --- .../Networking/World/NetworkedItem.cs | 46 +++- .../Networking/World/NetworkedItemManager.cs | 225 ++++++++++++++---- Multiplayer/Networking/Data/ItemUpdateData.cs | 1 + Multiplayer/Networking/Data/ServerPlayer.cs | 6 + .../Managers/Client/NetworkClient.cs | 11 +- .../Managers/Server/NetworkServer.cs | 41 ++-- 6 files changed, 252 insertions(+), 78 deletions(-) diff --git a/Multiplayer/Components/Networking/World/NetworkedItem.cs b/Multiplayer/Components/Networking/World/NetworkedItem.cs index ca6cf6b..4fcf086 100644 --- a/Multiplayer/Components/Networking/World/NetworkedItem.cs +++ b/Multiplayer/Components/Networking/World/NetworkedItem.cs @@ -49,6 +49,7 @@ public static bool TryGetNetworkedItem(ItemBase item, out NetworkedItem networke public bool UsefulItem { get; private set; } = false; public Type TrackedItemType { get; private set; } public bool BlockSync { get; set; } = false; + public uint LastDirtyTick { get; private set; } //Track dirty states private bool CreatedDirty = true; //if set, we created this item dirty and have not sent an update @@ -64,6 +65,24 @@ public static bool TryGetNetworkedItem(ItemBase item, out NetworkedItem networke private ItemPositionData ItemPosition; private bool PositionDirty = false; + //Handle ownership + public ushort OwnerId { get; private set; } = 0; // 0 means no owner + + //public void SetOwner(ushort playerId) + //{ + // if (OwnerId != playerId) + // { + // if (OwnerId != 0) + // { + // NetworkedItemManager.Instance.RemoveItemFromPlayerInventory(this); + // } + // OwnerId = playerId; + // if (playerId != 0) + // { + // NetworkedItemManager.Instance.AddItemToPlayerInventory(playerId, this); + // } + // } + //} protected override bool IsIdServerAuthoritative => true; @@ -80,11 +99,15 @@ protected void Start() { if (!CreatedDirty) return; - - if (StorageController.Instance.IsInStorageWorld(Item)) - { - ItemDropped = true; - } + + + ItemGrabbed = Item.IsGrabbed(); + ItemDropped = Item.transform.parent == WorldMover.OriginShiftParent; + + //if (StorageController.Instance.IsInStorageWorld(Item) ) + //{ + // ItemDropped = true; + //} } public T GetTrackedItem() where T : Component @@ -161,6 +184,7 @@ private void OnItemInventoryStateChanged(ItemBase itemBase, InventoryActionType DroppedDirty = true; ItemDropped = true; } + } #region Item Value Tracking @@ -250,6 +274,7 @@ public ItemUpdateData GetSnapshot() if (updateType == ItemUpdateData.ItemUpdateType.None) return null; + LastDirtyTick = NetworkLifecycle.Instance.Tick; snapshot = CreateUpdateData(updateType); CreatedDirty = false; @@ -269,18 +294,19 @@ public void ReceiveSnapshot(ItemUpdateData snapshot) //Multiplayer.LogDebug(()=>$"NetworkedItem.ReceiveSnapshot() netID: {snapshot.ItemNetId}, {snapshot.UpdateType}"); - if (snapshot.UpdateType.HasFlag(ItemUpdateData.ItemUpdateType.ItemEquipped)) + if (snapshot.UpdateType.HasFlag(ItemUpdateData.ItemUpdateType.ItemEquipped) || snapshot.UpdateType.HasFlag(ItemUpdateData.ItemUpdateType.Create)) { //do something when a player equips/unequips an item Multiplayer.Log($"NetworkedItem.ReceiveSnapshot() netID: {snapshot.ItemNetId}, Equipped: {snapshot.Equipped}, Player ID: {snapshot.Player}"); - + //OwnerId = snapshot.Player; + //if(OwnerId != NetworkLifecycle.Instance.Client.selfPeer.RemoteId) } - if (snapshot.UpdateType.HasFlag(ItemUpdateData.ItemUpdateType.ItemDropped)) + if (snapshot.UpdateType.HasFlag(ItemUpdateData.ItemUpdateType.ItemDropped) || snapshot.UpdateType.HasFlag(ItemUpdateData.ItemUpdateType.Create)) { //do something when a player drops/picks up an item Multiplayer.Log($"NetworkedItem.ReceiveSnapshot() netID: {snapshot.ItemNetId}, Dropped: {snapshot.Dropped}, Player ID: {snapshot.Player}"); - Item.gameObject.SetActive(snapshot.Dropped); + //Item.gameObject.SetActive(snapshot.Dropped); } if (snapshot.UpdateType.HasFlag(ItemUpdateData.ItemUpdateType.Position) || snapshot.UpdateType.HasFlag(ItemUpdateData.ItemUpdateType.Create)) @@ -353,7 +379,7 @@ protected override void OnDestroy() if (NetworkLifecycle.Instance.IsHost()) { - NetworkedItemManager.Instance.AddDirtyItemSnapshot(CreateUpdateData(ItemUpdateData.ItemUpdateType.Destroy)); + NetworkedItemManager.Instance.AddDirtyItemSnapshot(this, CreateUpdateData(ItemUpdateData.ItemUpdateType.Destroy)); } /* else if(!BlockSync) diff --git a/Multiplayer/Components/Networking/World/NetworkedItemManager.cs b/Multiplayer/Components/Networking/World/NetworkedItemManager.cs index 7ab1037..4d97b2f 100644 --- a/Multiplayer/Components/Networking/World/NetworkedItemManager.cs +++ b/Multiplayer/Components/Networking/World/NetworkedItemManager.cs @@ -7,27 +7,45 @@ using Multiplayer.Components.Networking.World; using System; using Multiplayer.Utils; +using DV; using DV.CabControls.Spec; namespace Multiplayer.Components.Networking.Train; public class NetworkedItemManager : SingletonBehaviour { - private List DirtyItems = new List(); + public const float MAX_DISTANCE_TO_ITEM = 100f; + public const float MAX_DISTANCE_TO_ITEM_SQR = MAX_DISTANCE_TO_ITEM * MAX_DISTANCE_TO_ITEM; + public const float NEARBY_REMOVAL_DELAY = 3f; // 3 seconds delay + + private List DestroyedItems = new List(); private Queue ReceivedSnapshots = new Queue(); private Dictionary> CachedItems = new Dictionary>(); + private Dictionary ItemPrefabs = new Dictionary(); + + //private Dictionary playerInventories = new Dictionary(); + //private Dictionary itemToPlayerMap = new Dictionary(); + -protected override void Awake() + protected override void Awake() { base.Awake(); if (!NetworkLifecycle.Instance.IsHost()) return; + NetworkLifecycle.Instance.Server.PlayerDisconnect += PlayerDisconnected; + } + + private void PlayerDisconnected(uint netID) + { + throw new NotImplementedException(); } protected void Start() { NetworkLifecycle.Instance.OnTick += Common_OnTick; + + BuildPrefabLookup(); } protected override void OnDestroy() @@ -39,10 +57,18 @@ protected override void OnDestroy() NetworkLifecycle.Instance.OnTick -= Common_OnTick; } - public void AddDirtyItemSnapshot(ItemUpdateData item) + public void AddDirtyItemSnapshot(NetworkedItem netItem, ItemUpdateData snapshot) { - if(! DirtyItems.Contains(item)) - DirtyItems.Add(item); + DestroyedItems.Add(snapshot); + + foreach(var player in NetworkLifecycle.Instance.Server.ServerPlayers) + { + if(player.KnownItems.ContainsKey(netItem)) + player.KnownItems.Remove(netItem); + + if(player.NearbyItems.ContainsKey(netItem)) + player.NearbyItems.Remove(netItem); + } } public void ReceiveSnapshots(List snapshots) @@ -62,11 +88,17 @@ public void ReceiveSnapshots(List snapshots) private void Common_OnTick(uint tick) { - //Process received Snapshots ProcessReceived(); if (NetworkLifecycle.Instance.IsHost()) - ProcessChanged(); + { + UpdatePlayerItemLists(); + ProcessChanged(tick); + } + else + { + ProcessClientChanges(tick); + } } private void ProcessReceived() @@ -100,41 +132,118 @@ private void ProcessReceived() } } - private void ProcessChanged() + #endregion + + #region Server + + private void UpdatePlayerItemLists() { - //Process all items for updates - foreach (var item in NetworkedItem.GetAll()) + float currentTime = Time.time; + + List allItems = NetworkedItem.GetAll(); + + foreach (var player in NetworkLifecycle.Instance.Server.ServerPlayers) { - ItemUpdateData snapshot = item.GetSnapshot(); + if (!player.IsLoaded) + continue; + foreach (var item in allItems) + { + float sqrDistance = (player.WorldPosition - item.transform.position).sqrMagnitude; - if (snapshot != null) - DirtyItems.Add(snapshot); + if (sqrDistance <= MAX_DISTANCE_TO_ITEM_SQR) + { + //NetworkLifecycle.Instance.Server.LogDebug(() => $"UpdatePlayerItemLists() Adding for player: {player.Username}, Nearby Item: {item.NetId}, {item.name}"); + player.NearbyItems[item] = currentTime; + } + } + + // Remove items that are no longer nearby + foreach (var kvp in player.NearbyItems) + { + if (currentTime - kvp.Value > NEARBY_REMOVAL_DELAY) + { + //NetworkLifecycle.Instance.Server.LogDebug(() => $"UpdatePlayerItemLists() Removing for player: {player.Username}, Nearby Item: {kvp.Key.NetId}, {kvp.Key.name}"); + player.NearbyItems.Remove(kvp.Key); + } + } } + } - if (DirtyItems.Count == 0) - return; + private void ProcessChanged(uint tick) + { + List dirtyItems = new List(); + float timeStamp = Time.time; - if (NetworkLifecycle.Instance.IsHost()) + foreach (var item in NetworkedItem.GetAll()) { - NetworkLifecycle.Instance.Server.SendItemsChangePacket(DirtyItems); + ItemUpdateData snapshot = item.GetSnapshot(); + if (snapshot != null) + dirtyItems.Add(snapshot); } - else + + //NetworkLifecycle.Instance.Server.LogDebug(() => $"ProcessChanged({tick}) DirtyItems: {dirtyItems.Count}"); + + foreach (var player in NetworkLifecycle.Instance.Server.ServerPlayers) { - NetworkLifecycle.Instance.Client.SendItemsChangePacket(DirtyItems); - } + if (!player.IsLoaded) + continue; - DirtyItems.Clear(); - } + List playerUpdates = new List(); - #endregion + // Process nearby items + foreach (var nearbyItem in player.NearbyItems.Keys) + { + if (!player.KnownItems.ContainsKey(nearbyItem)) + { + // This is a new item for the player + //NetworkLifecycle.Instance.Server.LogDebug(() => $"ProcessChanged({tick}) New item for: {player.Username}, itemNetID{nearbyItem.NetId}"); + ItemUpdateData snapshot = nearbyItem.CreateUpdateData(ItemUpdateData.ItemUpdateType.Create); + playerUpdates.Add(snapshot); + player.KnownItems[nearbyItem] = tick; + } + else + { + // Check if this item is in the dirty items list + var dirtyUpdate = dirtyItems.FirstOrDefault(di => di.ItemNetId == nearbyItem.NetId); + + //NetworkLifecycle.Instance.Server.LogDebug(() => $"ProcessChanged({tick}) Item exists for: {player.Username}, {dirtyUpdate != null}"); + + if (dirtyUpdate == null) + { + //NetworkLifecycle.Instance.Server.LogDebug(() => $"ProcessChanged({tick}) Item exists for: {player.Username}, LastDirtyTick: {player.KnownItems[nearbyItem] < nearbyItem.LastDirtyTick}"); + if (player.KnownItems[nearbyItem] < nearbyItem.LastDirtyTick) + { + dirtyUpdate = nearbyItem.CreateUpdateData(ItemUpdateData.ItemUpdateType.FullSync); + } + } + + if (dirtyUpdate != null) + { + playerUpdates.Add(dirtyUpdate); + player.KnownItems[nearbyItem] = tick; + } + } + } - #region Server + //NetworkLifecycle.Instance.Server.LogDebug(() => $"ProcessChanged({tick}) Adding {DestroyedItems.Count()} DestroyedItems for: {player.Username}"); + + playerUpdates.AddRange(DestroyedItems); + + if (playerUpdates.Count > 0) + { + //NetworkLifecycle.Instance.Server.LogDebug(() => $"ProcessChanged({tick}) Sending {playerUpdates.Count()} to player: {player.Username}"); + NetworkLifecycle.Instance.Server.SendItemsChangePacket(playerUpdates, player); + } + } + + DestroyedItems.Clear(); + } private void ProcessReceivedAsHost(ItemUpdateData snapshot) { if (snapshot.UpdateType == ItemUpdateData.ItemUpdateType.Create) { - Multiplayer.LogError($"NetworkedItemManager.ProcessReceived() Host received Create snapshot! ItemNetId: {snapshot.ItemNetId}, prefabName: {snapshot.PrefabName}"); + NetworkLifecycle.Instance.Server.LogError($"NetworkedItemManager.ProcessReceivedAsHost() Host received Create snapshot! ItemNetId: {snapshot.ItemNetId}, prefabName: {snapshot.PrefabName}"); return; } @@ -142,16 +251,17 @@ private void ProcessReceivedAsHost(ItemUpdateData snapshot) { if (ValidatePlayerAction(snapshot)) //Ensure the player can do this { + NetworkLifecycle.Instance.Server.LogWarning($"NetworkedItemManager.ProcessReceivedAsHost() ItemNetId: {snapshot.ItemNetId}, snapshot type: {snapshot.UpdateType}"); netItem.ReceiveSnapshot(snapshot); } else { - Multiplayer.LogWarning($"NetworkedItemManager.ProcessReceived() Player action validation failed for ItemNetId: {snapshot.ItemNetId}"); + NetworkLifecycle.Instance.Server.LogWarning($"NetworkedItemManager.ProcessReceivedAsHost() Player action validation failed for ItemNetId: {snapshot.ItemNetId}"); } } else { - Multiplayer.LogError($"NetworkedItemManager.ProcessReceived() NetworkedItem not found! Update Type: {snapshot.UpdateType}, ItemNetId: {snapshot.ItemNetId}, prefabName: {snapshot.PrefabName}"); + NetworkLifecycle.Instance.Server.LogError($"NetworkedItemManager.ProcessReceivedAsHost() NetworkedItem not found! Update Type: {snapshot.UpdateType}, ItemNetId: {snapshot.ItemNetId}, prefabName: {snapshot.PrefabName}"); } } @@ -163,8 +273,29 @@ private bool ValidatePlayerAction(ItemUpdateData snapshot) #endregion #region Client + + private void ProcessClientChanges(uint tick) + { + List changedItems = new List(); + + foreach (var item in NetworkedItem.GetAll()) + { + ItemUpdateData snapshot = item.GetSnapshot(); + if (snapshot != null) + { + changedItems.Add(snapshot); + } + } + + if (changedItems.Count > 0) + { + NetworkLifecycle.Instance.Client.SendItemsChangePacket(changedItems); + } + } + private void ProcessReceivedAsClient(ItemUpdateData snapshot) { + NetworkLifecycle.Instance.Client.LogDebug(() => $"NetworkedItemManager.ProcessReceivedAsClient() Update Type: {snapshot?.UpdateType}, ItemNetId: {snapshot?.ItemNetId}, prefabName: {snapshot?.PrefabName}"); if (snapshot.UpdateType == ItemUpdateData.ItemUpdateType.Create) { CreateItem(snapshot); @@ -175,7 +306,7 @@ private void ProcessReceivedAsClient(ItemUpdateData snapshot) } else { - Multiplayer.LogError($"NetworkedItemManager.ProcessReceived() NetworkedItem not found on client! Update Type: {snapshot.UpdateType}, ItemNetId: {snapshot.ItemNetId}, prefabName: {snapshot.PrefabName}"); + NetworkLifecycle.Instance.Client.LogError($"NetworkedItemManager.ProcessReceivedAsClient() NetworkedItem not found on client! Update Type: {snapshot.UpdateType}, ItemNetId: {snapshot.ItemNetId}, prefabName: {snapshot.PrefabName}"); } } #endregion @@ -193,16 +324,16 @@ private void CreateItem(ItemUpdateData snapshot) if(newItem == null) { - GameObject prefabObj = Resources.Load(snapshot.PrefabName) as GameObject; - - if (prefabObj == null) + //GameObject prefabObj = Resources.Load(snapshot.PrefabName) as GameObject; + + if (!ItemPrefabs.TryGetValue(snapshot.PrefabName, out InventoryItemSpec spec)) { Multiplayer.LogError($"NetworkedItemManager.CreateItem() Unable to load prefab for ItemNetId: {snapshot.ItemNetId}, prefabName: {snapshot.PrefabName}"); return; } //create a new item - GameObject gameObject = Instantiate(prefabObj, snapshot.PositionData.Position, snapshot.PositionData.Rotation); + GameObject gameObject = Instantiate(spec.gameObject, snapshot.PositionData.Position + WorldMover.currentMove, snapshot.PositionData.Rotation); //Make sure we have a NetworkedItem newItem = gameObject.GetOrAddComponent(); @@ -210,23 +341,27 @@ private void CreateItem(ItemUpdateData snapshot) newItem.gameObject.SetActive(true); - //InventoryItemSpec component = newItem.GetComponent(); - //if (newItem.Item.InventorySpecs != null) - // newItem.Item.InventorySpecs.BelongsToPlayer = false; - - //SingletonBehaviour.Instance.AddItemToWorldStorage(newItem.Item); - newItem.NetId = snapshot.ItemNetId; newItem.ReceiveSnapshot(snapshot); } + private void BuildPrefabLookup() + { + NetworkLifecycle.Instance.Client.LogDebug(() => $"BuildPrefabLookup()"); + + foreach (var item in Globals.G.Items.items) + { + if (!ItemPrefabs.ContainsKey(item.ItemPrefabName)) + { + ItemPrefabs[item.itemPrefabName] = item; + } + } + } public void CacheWorldItems() { if (NetworkLifecycle.Instance.IsHost()) return; - NetworkLifecycle.Instance.Client.LogDebug(() => $"CacheWorldItems()"); - // Remove all spawned world items and place them into a cache for later use var items = NetworkedItem.GetAll().ToList(); foreach (var item in items) @@ -253,13 +388,12 @@ private NetworkedItem GetFromCache(string prefabName) { if (CachedItems.TryGetValue(prefabName, out var items) && items.Count > 0) { - //NetworkLifecycle.Instance.Client.LogDebug(() => $"GetFromCache({prefabName}) Cache Hit"); + var cachedItem = items[items.Count - 1]; items.RemoveAt(items.Count - 1); return cachedItem; } - //NetworkLifecycle.Instance.Client.LogDebug(() => $"GetFromCache({prefabName}) Cache Miss!"); return null; } @@ -275,15 +409,12 @@ private void SendToCache(NetworkedItem netItem) RespawnOnDrop respawn = netItem.Item.GetComponent(); Destroy(respawn); - - - NetworkLifecycle.Instance.Client.LogDebug(() => $"Caching Spawned Item: {prefabName ?? ""}: checkWhileDisabled {respawn.checkWhileDisabled}, ignoreDistanceFromSpawnPosition {respawn.ignoreDistanceFromSpawnPosition}, respawnOnDropThroughFloor {respawn.respawnOnDropThroughFloor}"); + //NetworkLifecycle.Instance.Client.LogDebug(() => $"Caching Spawned Item: {prefabName ?? ""}: checkWhileDisabled {respawn.checkWhileDisabled}, ignoreDistanceFromSpawnPosition {respawn.ignoreDistanceFromSpawnPosition}, respawnOnDropThroughFloor {respawn.respawnOnDropThroughFloor}"); //respawn.checkWhileDisabled = false; //respawn.ignoreDistanceFromSpawnPosition = true; //respawn.respawnOnDropThroughFloor = false; - //netItem.Item.itemDisabler.ToggleInDumpster(false); if (SingletonBehaviour.Instance.StorageWorld.ContainsItem(netItem.Item)) { @@ -305,8 +436,6 @@ private void SendToCache(NetworkedItem netItem) #endregion - - [UsedImplicitly] public new static string AllowAutoCreate() { diff --git a/Multiplayer/Networking/Data/ItemUpdateData.cs b/Multiplayer/Networking/Data/ItemUpdateData.cs index 7f53093..32768a3 100644 --- a/Multiplayer/Networking/Data/ItemUpdateData.cs +++ b/Multiplayer/Networking/Data/ItemUpdateData.cs @@ -17,6 +17,7 @@ public enum ItemUpdateType : byte ItemDropped = 8, ItemEquipped = 16, ObjectState = 32, + FullSync = Position | ItemDropped | ItemEquipped | ObjectState, } public ItemUpdateType UpdateType { get; set; } diff --git a/Multiplayer/Networking/Data/ServerPlayer.cs b/Multiplayer/Networking/Data/ServerPlayer.cs index a90618f..d00541c 100644 --- a/Multiplayer/Networking/Data/ServerPlayer.cs +++ b/Multiplayer/Networking/Data/ServerPlayer.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Generic; using Multiplayer.Components.Networking.Train; +using Multiplayer.Components.Networking.World; using UnityEngine; namespace Multiplayer.Networking.Data; @@ -15,6 +17,10 @@ public class ServerPlayer public float RawRotationY { get; set; } public ushort CarId { get; set; } + public Dictionary KnownItems { get; private set; } = new Dictionary(); //NetworkedItem, last updated tick + public Dictionary NearbyItems { get; private set; } = new Dictionary(); //NetworkedItem, time since near the item + public StorageBase Storage { get; set; } = new StorageBase(); + private Vector3 _lastWorldPos = Vector3.zero; private Vector3 _lastAbsoluteWorldPosition = Vector3.zero; diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 7120700..692b130 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -37,6 +37,7 @@ using Object = UnityEngine.Object; using Multiplayer.Networking.Packets.Serverbound.Train; using System.Linq; +using LiteNetLib.Utils; namespace Multiplayer.Networking.Listeners; @@ -830,6 +831,11 @@ private void OnCommonItemChangePacket(CommonItemChangePacket packet) SendPacket(serverPeer, packet, deliveryMethod); } + private void SendNetSerializablePacketToServer(T packet, DeliveryMethod deliveryMethod) where T : INetSerializable, new() + { + SendNetSerializablePacket(serverPeer, packet, deliveryMethod); + } + public void SendSaveGameDataRequest() { SendPacketToServer(new ServerboundSaveGameDataRequestPacket(), DeliveryMethod.ReliableOrdered); @@ -1121,11 +1127,14 @@ public void SendChat(string message) }, DeliveryMethod.ReliableUnordered); } - public void SendItemsChangePacket(List items, NetPeer peer = null) + public void SendItemsChangePacket(List items) { Multiplayer.Log($"Sending SendItemsChangePacket with {items.Count()} items"); //SendPacketToServer(new CommonItemChangePacket { Items = items }, // DeliveryMethod.ReliableUnordered); + + SendNetSerializablePacketToServer(new CommonItemChangePacket { Items = items }, + DeliveryMethod.ReliableOrdered); } #endregion diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index d5e6d9f..1c175cf 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -37,6 +37,7 @@ namespace Multiplayer.Networking.Listeners; public class NetworkServer : NetworkManager { + public Action PlayerDisconnect; protected override string LogPrefix => "[Server]"; private readonly Queue joinQueue = new(); @@ -185,6 +186,8 @@ public override void OnPeerDisconnected(NetPeer peer, DisconnectInfo disconnectI { Id = id }), DeliveryMethod.ReliableUnordered); + + PlayerDisconnect?.Invoke(id); } public override void OnNetworkLatencyUpdate(NetPeer peer, int latency) @@ -416,15 +419,15 @@ public void SendJobsUpdatePacket(ushort stationNetId, NetworkedJob[] jobs, NetPe SendPacketToAll(ClientboundJobsUpdatePacket.FromNetworkedJobs(stationNetId, jobs), DeliveryMethod.ReliableUnordered,selfPeer); } - public void SendItemsChangePacket(List items, NetPeer peer = null) + public void SendItemsChangePacket(List items, ServerPlayer player) { - Multiplayer.Log($"Sending SendItemsChangePacket with {items.Count()} items"); - - SendNetSerializablePacketToAll(new CommonItemChangePacket { Items = items }, - DeliveryMethod.ReliableUnordered, selfPeer); + Multiplayer.Log($"Sending SendItemsChangePacket with {items.Count()} items to {player.Username}"); - //SendNetSerializablePacketToAll(new CommonItemChangePacket { Items = items }, - // DeliveryMethod.ReliableUnordered); + if(TryGetNetPeer(player.Id, out NetPeer peer) && peer != selfPeer) + { + SendNetSerializablePacket(peer, new CommonItemChangePacket { Items = items }, + DeliveryMethod.ReliableUnordered); + } } public void SendChat(string message, NetPeer exclude = null) @@ -650,18 +653,18 @@ private void OnServerboundClientReadyPacket(ServerboundClientReadyPacket packet, //Send Item Sync - List snapshots = new List(); - foreach (var item in NetworkedItem.GetAll()) - { - //only send items that are close to the player - float sqDist = (serverPlayer.WorldPosition - item.transform.position).sqrMagnitude; - - if (sqDist < 1000f ) - snapshots.Add(item.CreateUpdateData(ItemUpdateData.ItemUpdateType.Create)); - } - - LogDebug(() => $"Sending sync ItemUpdateData {snapshots.Count} items"); - SendNetSerializablePacket(peer, new CommonItemChangePacket { Items = snapshots }, DeliveryMethod.ReliableOrdered); + //List snapshots = new List(); + //foreach (var item in NetworkedItem.GetAll()) + //{ + // //only send items that are close to the player + // float sqDist = (serverPlayer.WorldPosition - item.transform.position).sqrMagnitude; + + // if (sqDist < 1000f ) + // snapshots.Add(item.CreateUpdateData(ItemUpdateData.ItemUpdateType.Create)); + //} + + //LogDebug(() => $"Sending sync ItemUpdateData {snapshots.Count} items"); + //SendNetSerializablePacket(peer, new CommonItemChangePacket { Items = snapshots }, DeliveryMethod.ReliableOrdered); // Send existing players foreach (ServerPlayer player in ServerPlayers) From cbbf773eaea440e84a58cea2d0c0f15982a25789 Mon Sep 17 00:00:00 2001 From: AMacro Date: Thu, 14 Nov 2024 11:53:23 +1000 Subject: [PATCH 112/188] Continuation of item sync Fixed issues with Lanterns Fixed changes during enumeration Fixed server side debug logging causing exceptions --- .../Networking/World/NetworkedItem.cs | 9 +--- .../Networking/World/NetworkedItemManager.cs | 16 +++++-- Multiplayer/Multiplayer.cs | 1 + .../Networking/Data/TaskNetworkData.cs | 7 --- .../Managers/Client/NetworkClient.cs | 6 +-- .../Managers/Server/NetworkServer.cs | 38 ++++++++------- .../Patches/World/Items/LanternPatch.cs | 2 +- .../Patches/World/Items/LighterPatch.cs | 48 +++++++++++-------- 8 files changed, 71 insertions(+), 56 deletions(-) diff --git a/Multiplayer/Components/Networking/World/NetworkedItem.cs b/Multiplayer/Components/Networking/World/NetworkedItem.cs index 4fcf086..9d8b773 100644 --- a/Multiplayer/Components/Networking/World/NetworkedItem.cs +++ b/Multiplayer/Components/Networking/World/NetworkedItem.cs @@ -40,8 +40,8 @@ public static bool TryGetNetworkedItem(ItemBase item, out NetworkedItem networke } #endregion - private const float PositionThreshold = 0.01f; - private const float RotationThreshold = 0.01f; + private const float PositionThreshold = 0.1f; + private const float RotationThreshold = 0.1f; public ItemBase Item { get; private set; } private Component trackedItem; @@ -103,11 +103,6 @@ protected void Start() ItemGrabbed = Item.IsGrabbed(); ItemDropped = Item.transform.parent == WorldMover.OriginShiftParent; - - //if (StorageController.Instance.IsInStorageWorld(Item) ) - //{ - // ItemDropped = true; - //} } public T GetTrackedItem() where T : Component diff --git a/Multiplayer/Components/Networking/World/NetworkedItemManager.cs b/Multiplayer/Components/Networking/World/NetworkedItemManager.cs index 4d97b2f..17f885d 100644 --- a/Multiplayer/Components/Networking/World/NetworkedItemManager.cs +++ b/Multiplayer/Components/Networking/World/NetworkedItemManager.cs @@ -23,6 +23,8 @@ public class NetworkedItemManager : SingletonBehaviour private Dictionary> CachedItems = new Dictionary>(); private Dictionary ItemPrefabs = new Dictionary(); + private bool ClientInitialised = false; + //private Dictionary playerInventories = new Dictionary(); //private Dictionary itemToPlayerMap = new Dictionary(); @@ -146,23 +148,26 @@ private void UpdatePlayerItemLists() { if (!player.IsLoaded) continue; + foreach (var item in allItems) { float sqrDistance = (player.WorldPosition - item.transform.position).sqrMagnitude; if (sqrDistance <= MAX_DISTANCE_TO_ITEM_SQR) { - //NetworkLifecycle.Instance.Server.LogDebug(() => $"UpdatePlayerItemLists() Adding for player: {player.Username}, Nearby Item: {item.NetId}, {item.name}"); + NetworkLifecycle.Instance.Server.LogDebug(() => $"UpdatePlayerItemLists() Adding for player: {player?.Username}, Nearby Item: {item?.NetId}, {item?.name}"); player.NearbyItems[item] = currentTime; } } // Remove items that are no longer nearby - foreach (var kvp in player.NearbyItems) + for (int i = 0; i < player.NearbyItems.Count; i++) { + var kvp = player.NearbyItems.ElementAt(i); + if (currentTime - kvp.Value > NEARBY_REMOVAL_DELAY) { - //NetworkLifecycle.Instance.Server.LogDebug(() => $"UpdatePlayerItemLists() Removing for player: {player.Username}, Nearby Item: {kvp.Key.NetId}, {kvp.Key.name}"); + NetworkLifecycle.Instance.Server.LogDebug(() => $"UpdatePlayerItemLists() Removing for player: {player?.Username}, Nearby Item: {kvp.Key?.NetId}, {kvp.Key?.name}"); player.NearbyItems.Remove(kvp.Key); } } @@ -278,6 +283,9 @@ private void ProcessClientChanges(uint tick) { List changedItems = new List(); + if(!ClientInitialised) + return; + foreach (var item in NetworkedItem.GetAll()) { ItemUpdateData snapshot = item.GetSnapshot(); @@ -382,6 +390,8 @@ public void CacheWorldItems() NetworkLifecycle.Instance.Client.LogDebug(() => $"Error Caching Spawned Item: {ex.Message}"); } } + + ClientInitialised = true; } private NetworkedItem GetFromCache(string prefabName) diff --git a/Multiplayer/Multiplayer.cs b/Multiplayer/Multiplayer.cs index bead7dd..e613abc 100644 --- a/Multiplayer/Multiplayer.cs +++ b/Multiplayer/Multiplayer.cs @@ -72,6 +72,7 @@ private static bool Load(UnityModManager.ModEntry modEntry) RemoteDispatchPatch.Patch(harmony, remoteDispatch.Assembly); } + //UnityModManager.ModEntry passengerJobs = UnityModManager.FindMod("PassengerJobs"); //if (passengerJobs?.Enabled == true) //{ // Log("Found PassengerJobs, initialising..."); diff --git a/Multiplayer/Networking/Data/TaskNetworkData.cs b/Multiplayer/Networking/Data/TaskNetworkData.cs index 3fffaa4..6d5f207 100644 --- a/Multiplayer/Networking/Data/TaskNetworkData.cs +++ b/Multiplayer/Networking/Data/TaskNetworkData.cs @@ -6,7 +6,6 @@ using LiteNetLib.Utils; using Multiplayer.Components.Networking.Train; - namespace Multiplayer.Networking.Data; #region TaskData Base Class @@ -29,22 +28,16 @@ public abstract class TaskNetworkData : TaskNetworkData where T : TaskNetwork protected void SerializeCommon(NetDataWriter writer) { - Multiplayer.Log($"TaskNetworkData.SerializeCommon() State {(byte)State}, {State}"); //Multiplayer.Log($"TaskNetworkData.SerializeCommon() State {(byte)State}, {State}"); writer.Put((byte)State); - Multiplayer.Log($"TaskNetworkData.SerializeCommon() TaskStartTime {TaskStartTime}"); //Multiplayer.Log($"TaskNetworkData.SerializeCommon() TaskStartTime {TaskStartTime}"); writer.Put(TaskStartTime); - Multiplayer.Log($"TaskNetworkData.SerializeCommon() TaskFinishTime {TaskFinishTime}"); //Multiplayer.Log($"TaskNetworkData.SerializeCommon() TaskFinishTime {TaskFinishTime}"); writer.Put(TaskFinishTime); - Multiplayer.Log($"TaskNetworkData.SerializeCommon() IsLastTask {IsLastTask}"); //Multiplayer.Log($"TaskNetworkData.SerializeCommon() IsLastTask {IsLastTask}"); writer.Put(IsLastTask); - Multiplayer.Log($"TaskNetworkData.SerializeCommon() TimeLimit {TimeLimit}"); //Multiplayer.Log($"TaskNetworkData.SerializeCommon() TimeLimit {TimeLimit}"); writer.Put(TimeLimit); - Multiplayer.Log($"TaskNetworkData.SerializeCommon() TaskType {(byte)TaskType}, {TaskType}"); //Multiplayer.Log($"TaskNetworkData.SerializeCommon() TaskType {(byte)TaskType}, {TaskType}"); writer.Put((byte)TaskType); } diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 692b130..b5ab55d 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -324,11 +324,11 @@ private void OnClientboundSaveGameDataPacket(ClientboundSaveGameDataPacket packe SceneSwitcher.SwitchToScene(DVScenes.Game); WorldStreamingInit.LoadingFinished += () => { - LogDebug(() => $"WorldStreamingInit.LoadingFinished()"); + Log($"WorldStreamingInit.LoadingFinished()"); NetworkedItemManager.Instance.CheckInstance(); - LogDebug(() => $"WorldStreamingInit.LoadingFinished() CacheWorldItems()"); + Log($"WorldStreamingInit.LoadingFinished() CacheWorldItems()"); NetworkedItemManager.Instance.CacheWorldItems(); - LogDebug(() => $"WorldStreamingInit.LoadingFinished() SendReadyPacket()"); + Log($"WorldStreamingInit.LoadingFinished() SendReadyPacket()"); SendReadyPacket(); }; diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index 1c175cf..0b502c1 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -1001,25 +1001,31 @@ private void OnCommonItemChangePacket(CommonItemChangePacket packet, NetPeer pee { LogDebug(()=>$"OnCommonItemChangePacket({packet?.Items?.Count}, {peer.Id})"); - string debug = ""; - - foreach(var item in packet?.Items) + Multiplayer.LogDebug(() => { - debug += "UpdateType: {" + item?.UpdateType + "}"; - debug += "itemNetId: " + item?.ItemNetId; - debug += "PrefabName: " + item?.PrefabName; - debug += "Equipped: " + item?.Equipped; - debug += "Dropped: " + item?.Dropped; - debug += "Position: " + item?.PositionData.Position; - debug += "Rotation: " + item?.PositionData.Rotation; - - debug += "States:"; - - foreach(var state in item?.States) - debug += "\r\n\t" + state.Key + ": " + state.Value; + string debug = ""; + + foreach (var item in packet?.Items) + { + debug += "UpdateType: " + item?.UpdateType + "\r\n"; + debug += "itemNetId: " + item?.ItemNetId + "\r\n"; + debug += "PrefabName: " + item?.PrefabName + "\r\n"; + debug += "Equipped: " + item?.Equipped + "\r\n"; + debug += "Dropped: " + item?.Dropped + "\r\n"; + debug += "Position: " + item?.PositionData.Position + "\r\n"; + debug += "Rotation: " + item?.PositionData.Rotation + "\r\n"; + + debug += "States:"; + + if (item.States != null) + foreach (var state in item?.States) + debug += "\r\n\t" + state.Key + ": " + state.Value; + } + + return debug; } - Multiplayer.LogDebug(()=> debug); +); } #endregion } diff --git a/Multiplayer/Patches/World/Items/LanternPatch.cs b/Multiplayer/Patches/World/Items/LanternPatch.cs index 005fade..e140123 100644 --- a/Multiplayer/Patches/World/Items/LanternPatch.cs +++ b/Multiplayer/Patches/World/Items/LanternPatch.cs @@ -32,7 +32,7 @@ static void Postfix(Lantern __instance) value => { if (value) - __instance.OnFlameIgnited(); + __instance.Ignite(1); else __instance.OnFlameExtinguished(); } diff --git a/Multiplayer/Patches/World/Items/LighterPatch.cs b/Multiplayer/Patches/World/Items/LighterPatch.cs index 7e995cd..4e7dbbc 100644 --- a/Multiplayer/Patches/World/Items/LighterPatch.cs +++ b/Multiplayer/Patches/World/Items/LighterPatch.cs @@ -2,6 +2,7 @@ using Multiplayer.Components.Networking.World; using Multiplayer.Utils; using System; +using System.Collections; using System.Collections.Generic; using System.Linq; using System.Text; @@ -9,37 +10,46 @@ namespace Multiplayer.Patches.World.Items; -[HarmonyPatch(typeof(Lighter), "Awake")] -public static class LighterAwakePatch +[HarmonyPatch(typeof(Lighter), "Start")] +public static class LighterPatch { static void Postfix(Lighter __instance) { var networkedItem = __instance.gameObject.GetOrAddComponent(); - networkedItem.Initialize(__instance); + + __instance.StartCoroutine(Init(networkedItem, __instance)); + } + + private static IEnumerator Init(NetworkedItem netItem, Lighter lighter) + { + while (!lighter.initialized) + yield return null; + + netItem.Initialize(lighter); // Register the values you want to track with both getters and setters - networkedItem.RegisterTrackedValue( + netItem.RegisterTrackedValue( "isOpen", - () => __instance.isOpen, + () => lighter.isOpen, value => - { - if (value) - __instance.OpenLid(); - else - __instance.CloseLid(); - } + { + if (value) + lighter.OpenLid(); + else + lighter.CloseLid(); + } ); - networkedItem.RegisterTrackedValue( + netItem.RegisterTrackedValue( "Ignited", - () => __instance.igniter.enabled, + () => lighter.igniter.enabled, value => - { - if (value) - __instance.LightFire(true, true); - else - __instance.OnFlameExtinguished(); - } + { + if (value) + lighter.LightFire(true, true); + else + lighter.OnFlameExtinguished(); + } ); } } From 32995672096ed039ed2f49c658e187df394a7106 Mon Sep 17 00:00:00 2001 From: AMacro Date: Thu, 14 Nov 2024 21:50:23 +1000 Subject: [PATCH 113/188] Reworked item sync Improved tracking of item states and ability to throw an item. Rigid body physics needs more work. --- .../Networking/World/NetworkedItem.cs | 265 +++++++++++------- .../Networking/World/NetworkedItemManager.cs | 7 +- Multiplayer/Networking/Data/ItemUpdateData.cs | 99 ++++--- .../Managers/Server/NetworkServer.cs | 11 +- .../Patches/World/Items/AGrabHandlerPatch.cs | 45 +++ 5 files changed, 279 insertions(+), 148 deletions(-) create mode 100644 Multiplayer/Patches/World/Items/AGrabHandlerPatch.cs diff --git a/Multiplayer/Components/Networking/World/NetworkedItem.cs b/Multiplayer/Components/Networking/World/NetworkedItem.cs index 9d8b773..07da285 100644 --- a/Multiplayer/Components/Networking/World/NetworkedItem.cs +++ b/Multiplayer/Components/Networking/World/NetworkedItem.cs @@ -1,6 +1,7 @@ using DV.CabControls; -using DV.CabControls.Spec; +using DV.Interaction; using DV.InventorySystem; +using DV.Items; using Multiplayer.Components.Networking.Train; using Multiplayer.Networking.Data; using System; @@ -11,6 +12,15 @@ namespace Multiplayer.Components.Networking.World; +public enum ItemState : byte +{ + Dropped, //belongs to the world + Thrown, //was thrown by player + InHand, //held by player + InInventory, //in player's inventory + Attached //attached to another object (e.g. EOT Lanterns) +} + public class NetworkedItem : IdMonoBehaviour { #region Lookup Cache @@ -44,26 +54,26 @@ public static bool TryGetNetworkedItem(ItemBase item, out NetworkedItem networke private const float RotationThreshold = 0.1f; public ItemBase Item { get; private set; } + private GrabHandlerItem GrabHandler; + private SnappableItem SnappableItem; private Component trackedItem; private List trackedValues = new List(); public bool UsefulItem { get; private set; } = false; public Type TrackedItemType { get; private set; } public bool BlockSync { get; set; } = false; public uint LastDirtyTick { get; private set; } + private bool Initialised; //Track dirty states private bool CreatedDirty = true; //if set, we created this item dirty and have not sent an update - private bool ItemGrabbed = false; //Current state of item grabbed - private bool GrabbedDirty = false; //Current state is dirty - - private bool ItemDropped = false; //Current state of item dropped - private bool DroppedDirty = false; //Current state is dirty + private ItemState lastState; + private bool stateDirty; + private bool wasThrown; - private Vector3 lastPosition; - private Quaternion lastRotation; - private ItemPositionData ItemPosition; - private bool PositionDirty = false; + private Vector3 thrownPosition; + private Quaternion thrownRotation; + private Vector3 throwDirection; //Handle ownership public ushort OwnerId { get; private set; } = 0; // 0 means no owner @@ -99,10 +109,6 @@ protected void Start() { if (!CreatedDirty) return; - - - ItemGrabbed = Item.IsGrabbed(); - ItemDropped = Item.transform.parent == WorldMover.OriginShiftParent; } public T GetTrackedItem() where T : Component @@ -128,6 +134,9 @@ public void Initialize(T item, ushort netId = 0, bool createDirty = true) whe private bool Register() { + if (Initialised) + return false; + try { @@ -142,11 +151,17 @@ private bool Register() Item.Grabbed += OnGrabbed; Item.Ungrabbed += OnUngrabbed; - Item.ItemInventoryStateChanged += OnItemInventoryStateChanged; - lastPosition = Item.transform.position - WorldMover.currentMove; - lastRotation = Item.transform.rotation; + TryGetComponent(out GrabHandler); + TryGetComponent(out SnappableItem); + + //Item.ItemInventoryStateChanged += OnItemInventoryStateChanged; + + lastState = GetItemState(); + stateDirty = false; + + Initialised = true; return true; } catch (Exception ex) @@ -158,30 +173,28 @@ private bool Register() private void OnUngrabbed(ControlImplBase obj) { - Multiplayer.LogDebug(() => $"OnUngrabbed() {name}"); - GrabbedDirty = ItemGrabbed == true; - ItemGrabbed = false; - + Multiplayer.LogDebug(() => $"OnUngrabbed() NetID: {NetId}, {name}"); + stateDirty = true; } private void OnGrabbed(ControlImplBase obj) { - Multiplayer.LogDebug(() => $"OnGrabbed() {name}"); - GrabbedDirty = ItemGrabbed == false; - ItemGrabbed = true; + Multiplayer.LogDebug(() => $"OnGrabbed() NetID: {NetId}, {name}"); + stateDirty = true; } - private void OnItemInventoryStateChanged(ItemBase itemBase, InventoryActionType actionType, InventoryItemState itemState) + public void OnThrow(Vector3 direction) { - Multiplayer.LogDebug(() => $"OnItemInventoryStateChanged() {name}, InventoryActionType: {actionType}, InventoryItemState: {itemState}"); - if (actionType == InventoryActionType.Purge) - { - DroppedDirty = true; - ItemDropped = true; - } + Multiplayer.LogDebug(() => $"OnThrow() netId: {NetId}, Name: {name}, Direction: {direction}"); + throwDirection = direction; + thrownPosition = Item.transform.position - WorldMover.currentMove; + thrownRotation = Item.transform.rotation; + wasThrown = true; + stateDirty = true; } + #region Item Value Tracking public void RegisterTrackedValue(string key, Func valueGetter, Action valueSetter) { @@ -214,27 +227,7 @@ private void MarkValuesClean() } } - private void CheckPositionChange() - { - Vector3 currentPosition = transform.position - WorldMover.currentMove; - Quaternion currentRotation = transform.rotation; - - bool positionChanged = Vector3.Distance(currentPosition, lastPosition) > PositionThreshold; - bool rotationChanged = Quaternion.Angle(currentRotation, lastRotation) > RotationThreshold; - - //We don't care about position and rotation if the player is holding it, as it will move relative to the player - if ((positionChanged || rotationChanged) && !ItemGrabbed) - { - ItemPosition = new ItemPositionData - { - Position = currentPosition, - Rotation = currentRotation - }; - lastPosition = currentPosition; - lastRotation = currentRotation; - PositionDirty = true; - } - } + #endregion public ItemUpdateData GetSnapshot() { @@ -244,16 +237,16 @@ public ItemUpdateData GetSnapshot() if (Item == null && Register() == false) return null; - CheckPositionChange(); + if (!stateDirty) + return null; + + ItemState currentState = GetItemState(); if (!CreatedDirty) { - if(PositionDirty) - updateType |= ItemUpdateData.ItemUpdateType.Position; - if(DroppedDirty) - updateType |= ItemUpdateData.ItemUpdateType.ItemDropped; - if(GrabbedDirty) - updateType |= ItemUpdateData.ItemUpdateType.ItemEquipped; + if(lastState != currentState) + updateType |= ItemUpdateData.ItemUpdateType.ItemState; + if (HasDirtyValues()) { Multiplayer.LogDebug(GetDirtyValuesDebugString); @@ -269,13 +262,13 @@ public ItemUpdateData GetSnapshot() if (updateType == ItemUpdateData.ItemUpdateType.None) return null; + lastState = currentState; LastDirtyTick = NetworkLifecycle.Instance.Tick; snapshot = CreateUpdateData(updateType); CreatedDirty = false; - GrabbedDirty = false; - DroppedDirty = false; - PositionDirty = false; + stateDirty = false; + wasThrown = false; MarkValuesClean(); @@ -287,85 +280,149 @@ public void ReceiveSnapshot(ItemUpdateData snapshot) if(snapshot == null || snapshot.UpdateType == ItemUpdateData.ItemUpdateType.None) return; - //Multiplayer.LogDebug(()=>$"NetworkedItem.ReceiveSnapshot() netID: {snapshot.ItemNetId}, {snapshot.UpdateType}"); - - if (snapshot.UpdateType.HasFlag(ItemUpdateData.ItemUpdateType.ItemEquipped) || snapshot.UpdateType.HasFlag(ItemUpdateData.ItemUpdateType.Create)) + if (snapshot.UpdateType.HasFlag(ItemUpdateData.ItemUpdateType.ItemState) || snapshot.UpdateType.HasFlag(ItemUpdateData.ItemUpdateType.FullSync) || snapshot.UpdateType.HasFlag(ItemUpdateData.ItemUpdateType.Create)) { - //do something when a player equips/unequips an item - Multiplayer.Log($"NetworkedItem.ReceiveSnapshot() netID: {snapshot.ItemNetId}, Equipped: {snapshot.Equipped}, Player ID: {snapshot.Player}"); - //OwnerId = snapshot.Player; - //if(OwnerId != NetworkLifecycle.Instance.Client.selfPeer.RemoteId) - } + Multiplayer.Log($"NetworkedItem.ReceiveSnapshot() netID: {snapshot?.ItemNetId}, ItemUpdateType {snapshot?.UpdateType}, ItemState {snapshot?.ItemState}"); - if (snapshot.UpdateType.HasFlag(ItemUpdateData.ItemUpdateType.ItemDropped) || snapshot.UpdateType.HasFlag(ItemUpdateData.ItemUpdateType.Create)) - { - //do something when a player drops/picks up an item - Multiplayer.Log($"NetworkedItem.ReceiveSnapshot() netID: {snapshot.ItemNetId}, Dropped: {snapshot.Dropped}, Player ID: {snapshot.Player}"); - //Item.gameObject.SetActive(snapshot.Dropped); - } + switch (snapshot.ItemState) + { + case ItemState.Dropped: + this.gameObject.SetActive(true); + transform.position = snapshot.ItemPosition + WorldMover.currentMove; + transform.rotation = snapshot.ItemRotation; + OwnerId = 0; + break; + + case ItemState.Thrown: + this.gameObject.SetActive(true); + transform.position = snapshot.ItemPosition + WorldMover.currentMove; + transform.rotation = snapshot.ItemRotation; + OwnerId = 0; + + GrabHandler?.Throw(throwDirection); + break; + + case ItemState.InHand: + this.gameObject.SetActive(false); + break; + + case ItemState.InInventory: + this.gameObject.SetActive(false); + break; + + case ItemState.Attached: + this.gameObject.SetActive(true); + break; + + default: + throw new Exception($"Item state not implemented: {snapshot.ItemState}"); + + } - if (snapshot.UpdateType.HasFlag(ItemUpdateData.ItemUpdateType.Position) || snapshot.UpdateType.HasFlag(ItemUpdateData.ItemUpdateType.Create)) - { - //update all values - transform.position = snapshot.PositionData.Position + WorldMover.currentMove; - transform.rotation = snapshot.PositionData.Rotation; } - if (snapshot.UpdateType == ItemUpdateData.ItemUpdateType.ObjectState || snapshot.UpdateType.HasFlag(ItemUpdateData.ItemUpdateType.Create)) + Multiplayer.Log($"NetworkedItem.ReceiveSnapshot() netID: {snapshot?.ItemNetId}, ItemUpdateType {snapshot?.UpdateType} About to process states"); + + if (snapshot.UpdateType.HasFlag(ItemUpdateData.ItemUpdateType.ObjectState) || snapshot.UpdateType.HasFlag(ItemUpdateData.ItemUpdateType.FullSync) || snapshot.UpdateType.HasFlag(ItemUpdateData.ItemUpdateType.Create)) { //Multiplayer.Log($"NetworkedItem.ReceiveSnapshot() netID: {snapshot.ItemNetId}, States: {snapshot?.States?.Count}"); - foreach (var state in snapshot.States) + if (snapshot.States != null) { - var trackedValue = trackedValues.Find(tv => ((dynamic)tv).Key == state.Key); - if (trackedValue != null) + foreach (var state in snapshot.States) { - try + var trackedValue = trackedValues.Find(tv => ((dynamic)tv).Key == state.Key); + if (trackedValue != null) { - ((dynamic)trackedValue).SetValueFromObject(state.Value); - Multiplayer.LogDebug(() => $"Updated tracked value: {state.Key}"); + try + { + ((dynamic)trackedValue).SetValueFromObject(state.Value); + Multiplayer.LogDebug(() => $"Updated tracked value: {state.Key}"); + } + catch (Exception ex) + { + Multiplayer.LogError($"Error updating tracked value {state.Key}: {ex.Message}"); + } } - catch (Exception ex) + else { - Multiplayer.LogError($"Error updating tracked value {state.Key}: {ex.Message}"); + Multiplayer.LogWarning($"Tracked value not found: {state.Key}"); } } - else - { - Multiplayer.LogWarning($"Tracked value not found: {state.Key}"); - } } } + Multiplayer.Log($"NetworkedItem.ReceiveSnapshot() netID: {snapshot?.ItemNetId}, ItemUpdateType {snapshot?.UpdateType} states processed"); + //mark values as clean CreatedDirty = false; - GrabbedDirty = false; - DroppedDirty = false; - PositionDirty = false; + stateDirty = false; MarkValuesClean(); return; } - #endregion public ItemUpdateData CreateUpdateData(ItemUpdateData.ItemUpdateType updateType) { Multiplayer.LogDebug(() => $"NetworkedItem.CreateUpdateData({updateType}) NetId: {NetId}, name: {name}"); - + + Vector3 position; + Quaternion rotation; + + if (wasThrown) + { + position = thrownPosition; + rotation = thrownRotation; + } + else + { + position = transform.position - WorldMover.currentMove; + rotation = transform.rotation; + } + var updateData = new ItemUpdateData { UpdateType = updateType, ItemNetId = NetId, - PrefabName = Item.name, - PositionData = ItemPosition, - Equipped = ItemGrabbed, - Dropped = ItemDropped, + PrefabName = Item.InventorySpecs.ItemPrefabName, + ItemState = lastState, + ItemPosition = position, + ItemRotation = rotation, + ThrowDirection = throwDirection, States = GetDirtyStateData(), }; return updateData; } + private ItemState GetItemState() + { + Multiplayer.LogDebug(() => $"GetItemState() NetId: {NetId}, {name}, Parent: {Item.transform.parent} WorldMover: {WorldMover.OriginShiftParent}, wasThrown: {wasThrown}, isGrabbed: {Item.IsGrabbed()} Inventory.Contains(): {Inventory.Instance.Contains(this.gameObject, false)} Storage.Contains: {StorageController.Instance.StorageInventory.ContainsItem(Item)}"); + + + if (Item.transform.parent == WorldMover.OriginShiftParent) + return ItemState.Dropped; + + if (wasThrown) + return ItemState.Thrown; + + if (Item.IsGrabbed()) + return ItemState.InHand; + + if (Inventory.Instance.Contains(this.gameObject, false)) + return ItemState.InInventory; + + if(SnappableItem != null && SnappableItem.IsSnapped) + { + + Multiplayer.LogDebug(() => $"GetItemState() NetId: {NetId}, {name}, snapped! {this.transform.parent}"); + return ItemState.Attached; + } + + //we need a condition to check if it's attached to something else + return ItemState.Dropped; + + } protected override void OnDestroy() { @@ -390,7 +447,7 @@ protected override void OnDestroy() { Item.Grabbed -= OnGrabbed; Item.Ungrabbed -= OnUngrabbed; - Item.ItemInventoryStateChanged -= OnItemInventoryStateChanged; + //Item.ItemInventoryStateChanged -= OnItemInventoryStateChanged; itemBaseToNetworkedItem.Remove(Item); } else diff --git a/Multiplayer/Components/Networking/World/NetworkedItemManager.cs b/Multiplayer/Components/Networking/World/NetworkedItemManager.cs index 17f885d..1e56e6d 100644 --- a/Multiplayer/Components/Networking/World/NetworkedItemManager.cs +++ b/Multiplayer/Components/Networking/World/NetworkedItemManager.cs @@ -8,7 +8,6 @@ using System; using Multiplayer.Utils; using DV; -using DV.CabControls.Spec; namespace Multiplayer.Components.Networking.Train; @@ -155,7 +154,7 @@ private void UpdatePlayerItemLists() if (sqrDistance <= MAX_DISTANCE_TO_ITEM_SQR) { - NetworkLifecycle.Instance.Server.LogDebug(() => $"UpdatePlayerItemLists() Adding for player: {player?.Username}, Nearby Item: {item?.NetId}, {item?.name}"); + //NetworkLifecycle.Instance.Server.LogDebug(() => $"UpdatePlayerItemLists() Adding for player: {player?.Username}, Nearby Item: {item?.NetId}, {item?.name}"); player.NearbyItems[item] = currentTime; } } @@ -167,7 +166,7 @@ private void UpdatePlayerItemLists() if (currentTime - kvp.Value > NEARBY_REMOVAL_DELAY) { - NetworkLifecycle.Instance.Server.LogDebug(() => $"UpdatePlayerItemLists() Removing for player: {player?.Username}, Nearby Item: {kvp.Key?.NetId}, {kvp.Key?.name}"); + //NetworkLifecycle.Instance.Server.LogDebug(() => $"UpdatePlayerItemLists() Removing for player: {player?.Username}, Nearby Item: {kvp.Key?.NetId}, {kvp.Key?.name}"); player.NearbyItems.Remove(kvp.Key); } } @@ -341,7 +340,7 @@ private void CreateItem(ItemUpdateData snapshot) } //create a new item - GameObject gameObject = Instantiate(spec.gameObject, snapshot.PositionData.Position + WorldMover.currentMove, snapshot.PositionData.Rotation); + GameObject gameObject = Instantiate(spec.gameObject, snapshot.ItemPosition + WorldMover.currentMove, snapshot.ItemRotation); //Make sure we have a NetworkedItem newItem = gameObject.GetOrAddComponent(); diff --git a/Multiplayer/Networking/Data/ItemUpdateData.cs b/Multiplayer/Networking/Data/ItemUpdateData.cs index 32768a3..d0e8280 100644 --- a/Multiplayer/Networking/Data/ItemUpdateData.cs +++ b/Multiplayer/Networking/Data/ItemUpdateData.cs @@ -1,7 +1,9 @@ using LiteNetLib.Utils; using Multiplayer.Components.Networking.World; +using Multiplayer.Networking.Serialization; using System; using System.Collections.Generic; +using UnityEngine; namespace Multiplayer.Networking.Data; @@ -13,21 +15,21 @@ public enum ItemUpdateType : byte None = 0, Create = 1, Destroy = 2, - Position = 4, - ItemDropped = 8, - ItemEquipped = 16, - ObjectState = 32, - FullSync = Position | ItemDropped | ItemEquipped | ObjectState, + ItemState = 4, + ObjectState = 8, + FullSync = 16, } public ItemUpdateType UpdateType { get; set; } public ushort ItemNetId { get; set; } public string PrefabName { get; set; } - public ItemPositionData PositionData { get; set; } - - public bool Dropped { get; set; } - public bool Equipped { get; set; } + public ItemState ItemState { get; set; } + public Vector3 ItemPosition { get; set; } + public Quaternion ItemRotation { get; set; } + public Vector3 ThrowDirection { get; set; } public ushort Player { get; set; } + public ushort CarNetId { get; set; } + public bool AttachedFront { get; set; } public Dictionary States { get; set; } public void Serialize(NetDataWriter writer) @@ -38,20 +40,32 @@ public void Serialize(NetDataWriter writer) if(UpdateType == ItemUpdateType.Destroy) return; + if(UpdateType.HasFlag(ItemUpdateType.ItemState) || UpdateType.HasFlag(ItemUpdateType.Create) || UpdateType.HasFlag(ItemUpdateType.FullSync)) + writer.Put((byte)ItemState); + if (UpdateType.HasFlag(ItemUpdateType.Create)) writer.Put(PrefabName); - if (UpdateType.HasFlag(ItemUpdateType.Position) || UpdateType.HasFlag(ItemUpdateType.ItemDropped) || UpdateType.HasFlag(ItemUpdateType.Create)) - ItemPositionData.Serialize(writer, PositionData); - - if (UpdateType.HasFlag(ItemUpdateType.ItemDropped) || UpdateType.HasFlag(ItemUpdateType.Create)) - writer.Put(Dropped); + if (UpdateType.HasFlag(ItemUpdateType.Create) || UpdateType.HasFlag(ItemUpdateType.FullSync) || (UpdateType.HasFlag(ItemUpdateType.ItemState) && ItemState == ItemState.Dropped)) + { + Vector3Serializer.Serialize(writer, ItemPosition); + QuaternionSerializer.Serialize(writer, ItemRotation); - if (UpdateType.HasFlag(ItemUpdateType.ItemEquipped) || UpdateType.HasFlag(ItemUpdateType.Create)) - writer.Put(Equipped); + if(ItemState == ItemState.InInventory || ItemState == ItemState.InHand) + { + writer.Put(Player); + } + else if(ItemState == ItemState.Attached) + { + writer.Put(CarNetId); + writer.Put(AttachedFront); + } + } - if (UpdateType.HasFlag(ItemUpdateType.ItemDropped) || UpdateType.HasFlag(ItemUpdateType.ItemEquipped) || UpdateType.HasFlag(ItemUpdateType.Create)) - writer.Put(Player); + if (UpdateType.HasFlag(ItemUpdateType.ItemState) && ItemState == ItemState.Thrown) + { + Vector3Serializer.Serialize(writer, ThrowDirection); + } if (UpdateType.HasFlag(ItemUpdateType.ObjectState) || UpdateType.HasFlag(ItemUpdateType.Create)) { @@ -60,7 +74,7 @@ public void Serialize(NetDataWriter writer) else { writer.Put(States.Count); - foreach(var state in States) + foreach (var state in States) { writer.Put(state.Key); SerializeTrackedValue(writer, state.Value); @@ -77,33 +91,46 @@ public void Deserialize(NetDataReader reader) if (UpdateType == ItemUpdateType.Destroy) return; - if (UpdateType == ItemUpdateType.Create) + if (UpdateType.HasFlag(ItemUpdateType.ItemState) || UpdateType.HasFlag(ItemUpdateType.Create) || UpdateType.HasFlag(ItemUpdateType.FullSync)) + ItemState = (ItemState)reader.GetByte(); + + if (UpdateType.HasFlag(ItemUpdateType.Create)) PrefabName = reader.GetString(); - if (UpdateType.HasFlag(ItemUpdateType.Position) || UpdateType.HasFlag(ItemUpdateType.ItemDropped) || UpdateType.HasFlag(ItemUpdateType.Create)) + if (UpdateType.HasFlag(ItemUpdateType.Create) || UpdateType.HasFlag(ItemUpdateType.FullSync) || + (UpdateType.HasFlag(ItemUpdateType.ItemState) && ItemState == ItemState.Dropped)) { - PositionData = ItemPositionData.Deserialize(reader); - } - - if (UpdateType.HasFlag(ItemUpdateType.ItemDropped) || UpdateType.HasFlag(ItemUpdateType.Create)) - Dropped = reader.GetBool(); + ItemPosition = Vector3Serializer.Deserialize(reader); + ItemRotation = QuaternionSerializer.Deserialize(reader); - if (UpdateType.HasFlag(ItemUpdateType.ItemEquipped) || UpdateType.HasFlag(ItemUpdateType.Create)) - Equipped = reader.GetBool(); + if (ItemState == ItemState.InInventory || ItemState == ItemState.InHand) + { + Player = reader.GetUShort(); + } + else if (ItemState == ItemState.Attached) + { + CarNetId = reader.GetUShort(); + AttachedFront = reader.GetBool(); + } + } - if (UpdateType.HasFlag(ItemUpdateType.ItemDropped) || UpdateType.HasFlag(ItemUpdateType.ItemEquipped) || UpdateType.HasFlag(ItemUpdateType.Create)) - Player = reader.GetUShort(); + if (UpdateType.HasFlag(ItemUpdateType.ItemState) && ItemState == ItemState.Thrown) + { + ThrowDirection = Vector3Serializer.Deserialize(reader); + } if (UpdateType.HasFlag(ItemUpdateType.ObjectState) || UpdateType.HasFlag(ItemUpdateType.Create)) { - States = new Dictionary(); - int stateCount = reader.GetInt(); - for (int i = 0; i < stateCount; i++) + if (stateCount > 0) { - string key = reader.GetString(); - object value = DeserializeTrackedValue(reader); - States[key] = value; + States = new Dictionary(); + for (int i = 0; i < stateCount; i++) + { + string key = reader.GetString(); + object value = DeserializeTrackedValue(reader); + States[key] = value; + } } } } diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index 0b502c1..1f10f9e 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -1010,10 +1010,13 @@ private void OnCommonItemChangePacket(CommonItemChangePacket packet, NetPeer pee debug += "UpdateType: " + item?.UpdateType + "\r\n"; debug += "itemNetId: " + item?.ItemNetId + "\r\n"; debug += "PrefabName: " + item?.PrefabName + "\r\n"; - debug += "Equipped: " + item?.Equipped + "\r\n"; - debug += "Dropped: " + item?.Dropped + "\r\n"; - debug += "Position: " + item?.PositionData.Position + "\r\n"; - debug += "Rotation: " + item?.PositionData.Rotation + "\r\n"; + debug += "Equipped: " + item?.ItemState + "\r\n"; + debug += "Position: " + item?.ItemPosition + "\r\n"; + debug += "Rotation: " + item?.ItemRotation + "\r\n"; + debug += "ThrowDirection: " + item?.ThrowDirection + "\r\n"; + debug += "Player: " + item?.Player + "\r\n"; + debug += "CarNetId: " + item?.CarNetId + "\r\n"; + debug += "AttachedFront: " + item?.AttachedFront + "\r\n"; debug += "States:"; diff --git a/Multiplayer/Patches/World/Items/AGrabHandlerPatch.cs b/Multiplayer/Patches/World/Items/AGrabHandlerPatch.cs new file mode 100644 index 0000000..30f6489 --- /dev/null +++ b/Multiplayer/Patches/World/Items/AGrabHandlerPatch.cs @@ -0,0 +1,45 @@ +using DV.Interaction; +using HarmonyLib; +using Multiplayer.Components.Networking.World; +using UnityEngine; + +namespace Multiplayer.Patches.World.Items; + +[HarmonyPatch(typeof(AGrabHandler))] +public static class AGrabHandler_Patch +{ + [HarmonyPatch(nameof(AGrabHandler.Throw))] + [HarmonyPrefix] + private static void Throw(AGrabHandler __instance, Vector3 direction) + { + __instance.TryGetComponent(out NetworkedItem netItem); + + if (netItem != null) + { + netItem.OnThrow(direction); + } + + } + + + /** + * Patch below methods to get attach/detach events + */ + + //public void AttachToAttachPoint(Transform attachPoint, bool positionStays) + //{ + // this.TogglePhysics(false); + // base.transform.SetParent(attachPoint, positionStays); + //} + + //// Token: 0x060000A7 RID: 167 RVA: 0x000042EC File Offset: 0x000024EC + //public override void EndInteraction() + //{ + // base.transform.parent = null; + // base.EndInteraction(); + // this.TogglePhysics(true); + //} + + + +} From 82e7eb5999f78d81205e3bb0ad3d063a2d780579 Mon Sep 17 00:00:00 2001 From: AMacro Date: Tue, 19 Nov 2024 15:36:15 +1000 Subject: [PATCH 114/188] Fixed issues with item sync and added more "smart" objects to the syncable list Reworked: - Snappable items - Item throwing code - Application of tracked value changes - Lighter setup and value tracking/application Resolved TrackedValue race conditions --- Multiplayer/Components/IdMonoBehaviour.cs | 9 + .../Networking/World/NetworkedItem.cs | 295 +++++++++++++----- .../Networking/World/NetworkedItemManager.cs | 53 +++- Multiplayer/Multiplayer.csproj | 1 + Multiplayer/Networking/Data/ItemUpdateData.cs | 74 +++-- .../Managers/Client/NetworkClient.cs | 51 +-- .../Patches/World/Items/FlashlightPatch.cs | 88 ++++++ ...GrabHandlerPatch.cs => GrabHandlerItem.cs} | 8 +- .../Patches/World/Items/ItemBasePatch.cs | 4 +- .../Patches/World/Items/LanternPatch.cs | 7 +- .../Patches/World/Items/LighterPatch.cs | 63 ++-- .../Patches/World/Items/ShovelPatch.cs | 55 ++++ Multiplayer/Utils/UnityExtensions.cs | 20 +- 13 files changed, 555 insertions(+), 173 deletions(-) create mode 100644 Multiplayer/Patches/World/Items/FlashlightPatch.cs rename Multiplayer/Patches/World/Items/{AGrabHandlerPatch.cs => GrabHandlerItem.cs} (80%) create mode 100644 Multiplayer/Patches/World/Items/ShovelPatch.cs diff --git a/Multiplayer/Components/IdMonoBehaviour.cs b/Multiplayer/Components/IdMonoBehaviour.cs index f8fa3c6..a233557 100644 --- a/Multiplayer/Components/IdMonoBehaviour.cs +++ b/Multiplayer/Components/IdMonoBehaviour.cs @@ -36,6 +36,15 @@ protected static bool Get(T netId, out IdMonoBehaviour obj) return false; } + protected static bool TryGet(T netId, out IdMonoBehaviour obj) + { + if (indexToObject.TryGetValue(netId, out obj)) + return true; + + obj = null; + return false; + } + protected virtual void Awake() { if (IsIdServerAuthoritative && !NetworkLifecycle.Instance.IsHost()) diff --git a/Multiplayer/Components/Networking/World/NetworkedItem.cs b/Multiplayer/Components/Networking/World/NetworkedItem.cs index 07da285..00fa9d8 100644 --- a/Multiplayer/Components/Networking/World/NetworkedItem.cs +++ b/Multiplayer/Components/Networking/World/NetworkedItem.cs @@ -4,7 +4,9 @@ using DV.Items; using Multiplayer.Components.Networking.Train; using Multiplayer.Networking.Data; +using Multiplayer.Utils; using System; +using System.Collections; using System.Collections.Generic; using System.Linq; using System.Text; @@ -37,6 +39,13 @@ public static bool Get(ushort netId, out NetworkedItem obj) return b; } + public static bool TryGet(ushort netId, out NetworkedItem obj) + { + bool b = TryGet(netId, out IdMonoBehaviour rawObj); + obj = (NetworkedItem)rawObj; + return b; + } + public static bool GetItem(ushort netId, out ItemBase obj) { bool b = Get(netId, out NetworkedItem networkedItem); @@ -54,19 +63,20 @@ public static bool TryGetNetworkedItem(ItemBase item, out NetworkedItem networke private const float RotationThreshold = 0.1f; public ItemBase Item { get; private set; } - private GrabHandlerItem GrabHandler; - private SnappableItem SnappableItem; + private GrabHandlerItem grabHandler; + private SnappableOnCoupler snappableOnCoupler; private Component trackedItem; private List trackedValues = new List(); public bool UsefulItem { get; private set; } = false; public Type TrackedItemType { get; private set; } public bool BlockSync { get; set; } = false; public uint LastDirtyTick { get; private set; } - private bool Initialised; + private bool initialised; + private bool registrationComplete = false; + private Queue pendingSnapshots = new Queue(); //Track dirty states - private bool CreatedDirty = true; //if set, we created this item dirty and have not sent an update - + private bool createdDirty = true; //if set, we created this item dirty and have not sent an update private ItemState lastState; private bool stateDirty; private bool wasThrown; @@ -107,7 +117,7 @@ protected override void Awake() protected void Start() { - if (!CreatedDirty) + if (!createdDirty) return; } @@ -125,7 +135,7 @@ public void Initialize(T item, ushort netId = 0, bool createDirty = true) whe TrackedItemType = typeof(T); UsefulItem = true; - CreatedDirty = createDirty; + createdDirty = createDirty; if(Item == null) Register(); @@ -134,7 +144,7 @@ public void Initialize(T item, ushort netId = 0, bool createDirty = true) whe private bool Register() { - if (Initialised) + if (initialised) return false; try @@ -152,16 +162,17 @@ private bool Register() Item.Grabbed += OnGrabbed; Item.Ungrabbed += OnUngrabbed; - TryGetComponent(out GrabHandler); - TryGetComponent(out SnappableItem); - + //Find special interaction components + TryGetComponent(out grabHandler); + TryGetComponent(out snappableOnCoupler); + //Item.ItemInventoryStateChanged += OnItemInventoryStateChanged; lastState = GetItemState(); stateDirty = false; - Initialised = true; + initialised = true; return true; } catch (Exception ex) @@ -185,11 +196,19 @@ private void OnGrabbed(ControlImplBase obj) public void OnThrow(Vector3 direction) { - Multiplayer.LogDebug(() => $"OnThrow() netId: {NetId}, Name: {name}, Direction: {direction}"); + //block a received throw from + if(wasThrown) + { + wasThrown = false; + return; + } + throwDirection = direction; thrownPosition = Item.transform.position - WorldMover.currentMove; thrownRotation = Item.transform.rotation; + Multiplayer.LogDebug(() => $"OnThrow() netId: {NetId}, Name: {name}, Raw Position: {Item.transform.position}, Position: {thrownPosition}, Rotation: {thrownRotation}, Direction: {throwDirection}"); + wasThrown = true; stateDirty = true; } @@ -198,9 +217,24 @@ public void OnThrow(Vector3 direction) #region Item Value Tracking public void RegisterTrackedValue(string key, Func valueGetter, Action valueSetter) { + Multiplayer.LogDebug(() => $"NetworkedItem.RegisterTrackedValue(\"{key}\", {valueGetter != null}, {valueSetter != null}) itemNetId {NetId}, item name: {name}"); trackedValues.Add(new TrackedValue(key, valueGetter, valueSetter)); } + public void FinaliseTrackedValues() + { + Multiplayer.LogDebug(() => $"NetworkedItem.FinaliseTrackedValues() itemNetId: {NetId}, item name: {name}"); + + while (pendingSnapshots.Count > 0) + { + Multiplayer.LogDebug(() => $"NetworkedItem.FinaliseTrackedValues() itemNetId: {NetId}, item name: {name}. Dequeuing"); + ApplySnapshot(pendingSnapshots.Dequeue()); + } + + registrationComplete = true; + + } + private bool HasDirtyValues() { return trackedValues.Any(tv => ((dynamic)tv).IsDirty); @@ -218,6 +252,15 @@ private Dictionary GetDirtyStateData() } return dirtyData; } + private Dictionary GetAllStateData() + { + var data = new Dictionary(); + foreach (var trackedValue in trackedValues) + { + data[((dynamic)trackedValue).Key] = ((dynamic)trackedValue).GetValueAsObject(); + } + return data; + } private void MarkValuesClean() { @@ -234,20 +277,22 @@ public ItemUpdateData GetSnapshot() ItemUpdateData snapshot; ItemUpdateData.ItemUpdateType updateType = ItemUpdateData.ItemUpdateType.None; + bool hasDirtyVals = HasDirtyValues(); + if (Item == null && Register() == false) return null; - if (!stateDirty) + if (!stateDirty && !hasDirtyVals) return null; ItemState currentState = GetItemState(); - if (!CreatedDirty) + if (!createdDirty) { if(lastState != currentState) updateType |= ItemUpdateData.ItemUpdateType.ItemState; - if (HasDirtyValues()) + if (hasDirtyVals) { Multiplayer.LogDebug(GetDirtyValuesDebugString); updateType |= ItemUpdateData.ItemUpdateType.ObjectState; @@ -266,7 +311,7 @@ public ItemUpdateData GetSnapshot() LastDirtyTick = NetworkLifecycle.Instance.Tick; snapshot = CreateUpdateData(updateType); - CreatedDirty = false; + createdDirty = false; stateDirty = false; wasThrown = false; @@ -280,82 +325,60 @@ public void ReceiveSnapshot(ItemUpdateData snapshot) if(snapshot == null || snapshot.UpdateType == ItemUpdateData.ItemUpdateType.None) return; + if (!registrationComplete) + { + Multiplayer.Log($"NetworkedItem.ReceiveSnapshot() netId: {snapshot?.ItemNetId}, ItemUpdateType: {snapshot?.UpdateType}. Queuing"); + pendingSnapshots.Enqueue(snapshot); + return; + } + + ApplySnapshot(snapshot); + } + + private void ApplySnapshot(ItemUpdateData snapshot) + { if (snapshot.UpdateType.HasFlag(ItemUpdateData.ItemUpdateType.ItemState) || snapshot.UpdateType.HasFlag(ItemUpdateData.ItemUpdateType.FullSync) || snapshot.UpdateType.HasFlag(ItemUpdateData.ItemUpdateType.Create)) { - Multiplayer.Log($"NetworkedItem.ReceiveSnapshot() netID: {snapshot?.ItemNetId}, ItemUpdateType {snapshot?.UpdateType}, ItemState {snapshot?.ItemState}"); + Multiplayer.Log($"NetworkedItem.ApplySnapshot() netId: {snapshot?.ItemNetId}, ItemUpdateType: {snapshot?.UpdateType}, ItemState: {snapshot?.ItemState}, Active state: {gameObject.activeInHierarchy}"); switch (snapshot.ItemState) { case ItemState.Dropped: - this.gameObject.SetActive(true); - transform.position = snapshot.ItemPosition + WorldMover.currentMove; - transform.rotation = snapshot.ItemRotation; - OwnerId = 0; - break; - case ItemState.Thrown: - this.gameObject.SetActive(true); - transform.position = snapshot.ItemPosition + WorldMover.currentMove; - transform.rotation = snapshot.ItemRotation; - OwnerId = 0; - - GrabHandler?.Throw(throwDirection); + HandleDroppedOrThrownState(snapshot); break; case ItemState.InHand: - this.gameObject.SetActive(false); - break; - case ItemState.InInventory: - this.gameObject.SetActive(false); + HandleInventoryorHandState(snapshot); break; case ItemState.Attached: - this.gameObject.SetActive(true); + HandleAttachedState(snapshot); break; default: - throw new Exception($"Item state not implemented: {snapshot.ItemState}"); + throw new Exception($"NetworkedItem.ApplySnapshot() Item state not implemented: {snapshot?.ItemState}"); } - } - Multiplayer.Log($"NetworkedItem.ReceiveSnapshot() netID: {snapshot?.ItemNetId}, ItemUpdateType {snapshot?.UpdateType} About to process states"); + Multiplayer.Log($"NetworkedItem.ApplySnapshot() netID: {snapshot?.ItemNetId}, ItemUpdateType {snapshot?.UpdateType} About to process states"); - if (snapshot.UpdateType.HasFlag(ItemUpdateData.ItemUpdateType.ObjectState) || snapshot.UpdateType.HasFlag(ItemUpdateData.ItemUpdateType.FullSync) || snapshot.UpdateType.HasFlag(ItemUpdateData.ItemUpdateType.Create)) + if (snapshot.UpdateType.HasFlag(ItemUpdateData.ItemUpdateType.Create) || snapshot.UpdateType.HasFlag(ItemUpdateData.ItemUpdateType.ObjectState)) { - //Multiplayer.Log($"NetworkedItem.ReceiveSnapshot() netID: {snapshot.ItemNetId}, States: {snapshot?.States?.Count}"); + Multiplayer.Log($"NetworkedItem.ApplySnapshot() netID: {snapshot?.ItemNetId}, States: {snapshot?.States?.Count}"); - if (snapshot.States != null) + if (trackedItem != null && snapshot.States != null) { - foreach (var state in snapshot.States) - { - var trackedValue = trackedValues.Find(tv => ((dynamic)tv).Key == state.Key); - if (trackedValue != null) - { - try - { - ((dynamic)trackedValue).SetValueFromObject(state.Value); - Multiplayer.LogDebug(() => $"Updated tracked value: {state.Key}"); - } - catch (Exception ex) - { - Multiplayer.LogError($"Error updating tracked value {state.Key}: {ex.Message}"); - } - } - else - { - Multiplayer.LogWarning($"Tracked value not found: {state.Key}"); - } - } + ApplyTrackedValues(snapshot.States); } } - Multiplayer.Log($"NetworkedItem.ReceiveSnapshot() netID: {snapshot?.ItemNetId}, ItemUpdateType {snapshot?.UpdateType} states processed"); + Multiplayer.Log($"NetworkedItem.ApplySnapshot() netID: {snapshot?.ItemNetId}, ItemUpdateType {snapshot?.UpdateType} states processed"); //mark values as clean - CreatedDirty = false; + createdDirty = false; stateDirty = false; MarkValuesClean(); @@ -368,6 +391,9 @@ public ItemUpdateData CreateUpdateData(ItemUpdateData.ItemUpdateType updateType) Vector3 position; Quaternion rotation; + Dictionary states; + ushort carId =0; + bool frontCoupler = true; if (wasThrown) { @@ -380,6 +406,26 @@ public ItemUpdateData CreateUpdateData(ItemUpdateData.ItemUpdateType updateType) rotation = transform.rotation; } + if (updateType.HasFlag(ItemUpdateData.ItemUpdateType.Create) || updateType.HasFlag(ItemUpdateData.ItemUpdateType.FullSync)) + { + states = GetAllStateData(); + } + else + { + states = GetDirtyStateData(); + } + + if(lastState == ItemState.Attached) + { + ItemSnapPointCoupler itemSnapPointCoupler = snappableOnCoupler.SnappedTo as ItemSnapPointCoupler; + + if (itemSnapPointCoupler != null) + { + carId = itemSnapPointCoupler.Car.GetNetId(); + frontCoupler = itemSnapPointCoupler.IsFront; + } + } + var updateData = new ItemUpdateData { UpdateType = updateType, @@ -389,7 +435,9 @@ public ItemUpdateData CreateUpdateData(ItemUpdateData.ItemUpdateType updateType) ItemPosition = position, ItemRotation = rotation, ThrowDirection = throwDirection, - States = GetDirtyStateData(), + CarNetId = carId, + AttachedFront = frontCoupler, + States = states, }; return updateData; @@ -400,11 +448,17 @@ private ItemState GetItemState() Multiplayer.LogDebug(() => $"GetItemState() NetId: {NetId}, {name}, Parent: {Item.transform.parent} WorldMover: {WorldMover.OriginShiftParent}, wasThrown: {wasThrown}, isGrabbed: {Item.IsGrabbed()} Inventory.Contains(): {Inventory.Instance.Contains(this.gameObject, false)} Storage.Contains: {StorageController.Instance.StorageInventory.ContainsItem(Item)}"); - if (Item.transform.parent == WorldMover.OriginShiftParent) + if (Item.transform.parent == WorldMover.OriginShiftParent && !wasThrown) + { + Multiplayer.LogDebug(() => $"GetItemState() NetId: {NetId}, {name}, Parent: {Item.transform.parent} WorldMover: {WorldMover.OriginShiftParent}, wasThrown: {wasThrown}"); return ItemState.Dropped; + } if (wasThrown) + { + Multiplayer.LogDebug(() => $"GetItemState() NetId: {NetId}, {name}, Parent: {Item.transform.parent} WorldMover: {WorldMover.OriginShiftParent}, wasThrown: {wasThrown}"); return ItemState.Thrown; + } if (Item.IsGrabbed()) return ItemState.InHand; @@ -412,9 +466,8 @@ private ItemState GetItemState() if (Inventory.Instance.Contains(this.gameObject, false)) return ItemState.InInventory; - if(SnappableItem != null && SnappableItem.IsSnapped) + if(snappableOnCoupler != null && snappableOnCoupler.IsSnapped) { - Multiplayer.LogDebug(() => $"GetItemState() NetId: {NetId}, {name}, snapped! {this.transform.parent}"); return ItemState.Attached; } @@ -424,6 +477,103 @@ private ItemState GetItemState() } + private void ApplyTrackedValues(Dictionary newValues) + { + Multiplayer.LogDebug(() => $"NetworkedItem.ApplyTrackedValues() itemNetId: {NetId}, item name: {name}. Null checks"); + + if (newValues == null || newValues.Count == 0) + return; //yield break; + + //int i = 0; + //while (!registrationComplete) + //{ + // Multiplayer.LogDebug(() => $"NetworkedItem.ApplyTrackedValues() itemNetId: {NetId}, item name: {name}. Registration checks: {i}"); + // i++; + // //yield return null; + //} + + Multiplayer.LogDebug(() => $"NetworkedItem.ApplyTrackedValues() itemNetId: {NetId}, item name: {name}. Registration complete: {registrationComplete}"); + + foreach (var newValue in newValues) + { + var trackedValue = trackedValues.Find(tv => ((dynamic)tv).Key == newValue.Key); + if (trackedValue != null) + { + try + { + ((dynamic)trackedValue).SetValueFromObject(newValue.Value); + Multiplayer.LogDebug(() => $"Updated tracked value: {newValue.Key}, value: {newValue.Value} "); + } + catch (Exception ex) + { + Multiplayer.LogError($"Error updating tracked value {newValue.Key}: {ex.Message}"); + } + } + else + { + Multiplayer.LogWarning($"Tracked value not found: {newValue.Key}\r\n {String.Join(", ", trackedValues.Select(val => ((dynamic)val).Key))}"); + } + } + } + + #region Item State Update Handlers + + private void HandleDroppedOrThrownState(ItemUpdateData snapshot) + { + gameObject.SetActive(true); + transform.position = snapshot.ItemPosition + WorldMover.currentMove; + transform.rotation = snapshot.ItemRotation; + OwnerId = 0; + + if (snapshot.ItemState == ItemState.Thrown) + { + Multiplayer.LogDebug(()=>$"NetworkedItem.HandleDroppedOrThrownState() ItemNetId: {snapshot?.ItemNetId} Thrown. Position: {transform.position}, Direction: {snapshot?.ThrowDirection}"); + + wasThrown = true; + grabHandler?.Throw(snapshot.ThrowDirection); + } + else + { + Multiplayer.LogDebug(() => $"NetworkedItem.HandleDroppedOrThrownState() ItemNetId: {snapshot?.ItemNetId} Dropped. Position: {transform.position}"); + } + } + + private void HandleAttachedState(ItemUpdateData snapshot) + { + gameObject.SetActive(true); + Multiplayer.LogDebug(() => $"NetworkedItem.HandleAttachedState() ItemNetId: {snapshot?.ItemNetId} attempting attachment to car {snapshot.CarNetId}, at the front {snapshot.AttachedFront}"); + + if (!NetworkedTrainCar.GetTrainCar(snapshot.CarNetId, out TrainCar trainCar)) + { + Multiplayer.LogWarning($"NetworkedItem.HandleAttachedState() CarNetId: {snapshot?.CarNetId} not found for ItemNetId: {snapshot?.ItemNetId}"); + return; + } + + //Try to find the coupler snap point for the car and correct end + var snapPoint = trainCar?.physicsLod?.GetCouplerSnapPoints() + .FirstOrDefault(sp => sp.IsFront == snapshot.AttachedFront); + + if (snapPoint == null) + { + Multiplayer.LogWarning($"No valid snap point found for car {snapshot.CarNetId}"); + return; + } + + //Attempt attachment to car + Item.ItemRigidbody.isKinematic = false; + if (!snapPoint.SnapItem(Item, false)) + { + Multiplayer.LogWarning($"Attachment failed for item {snapshot?.ItemNetId} to car {snapshot.CarNetId}"); + } + } + + private void HandleInventoryorHandState(ItemUpdateData snapshot) + { + //todo add to player model's hand + this.gameObject.SetActive(false); + } + #endregion + protected override void OnDestroy() { if (UnloadWatcher.isQuitting || UnloadWatcher.isUnloading) @@ -433,15 +583,6 @@ protected override void OnDestroy() { NetworkedItemManager.Instance.AddDirtyItemSnapshot(this, CreateUpdateData(ItemUpdateData.ItemUpdateType.Destroy)); } - /* - else if(!BlockSync) - { - Multiplayer.LogWarning($"NetworkedItem.OnDestroy({name}, {NetId})");/*\r\n{new System.Diagnostics.StackTrace()} - } - else - { - Multiplayer.LogDebug(()=>$"NetworkedItem.OnDestroy({name}, {NetId})");/*\r\n{new System.Diagnostics.StackTrace()} - }*/ if (Item != null) { @@ -468,7 +609,7 @@ public string GetDirtyValuesDebugString() } StringBuilder sb = new StringBuilder(); - sb.AppendLine($"Dirty values for NetworkedItem {name}, NetId {NetId}:"); + sb.AppendLine($"Dirty values for NetworkedItem: {name}, NetId: {NetId}:"); foreach (var value in dirtyValues) { sb.AppendLine(((dynamic)value).GetDebugString()); diff --git a/Multiplayer/Components/Networking/World/NetworkedItemManager.cs b/Multiplayer/Components/Networking/World/NetworkedItemManager.cs index 1e56e6d..1817f96 100644 --- a/Multiplayer/Components/Networking/World/NetworkedItemManager.cs +++ b/Multiplayer/Components/Networking/World/NetworkedItemManager.cs @@ -8,6 +8,9 @@ using System; using Multiplayer.Utils; using DV; +using DV.CabControls.Spec; +using System.ComponentModel; +using Cysharp.Threading.Tasks.Triggers; namespace Multiplayer.Components.Networking.Train; @@ -201,9 +204,13 @@ private void ProcessChanged(uint tick) { // This is a new item for the player //NetworkLifecycle.Instance.Server.LogDebug(() => $"ProcessChanged({tick}) New item for: {player.Username}, itemNetID{nearbyItem.NetId}"); + ItemUpdateData snapshot = nearbyItem.CreateUpdateData(ItemUpdateData.ItemUpdateType.Create); - playerUpdates.Add(snapshot); player.KnownItems[nearbyItem] = tick; + + //prevent propagation of creates for special items + if(!DoNotCreateItem(nearbyItem.GetType())) + playerUpdates.Add(snapshot); } else { @@ -223,6 +230,7 @@ private void ProcessChanged(uint tick) if (dirtyUpdate != null) { + Multiplayer.LogDebug(() => $"ProcessChanged({tick}) Update Type: {dirtyUpdate.UpdateType}, Item State: {dirtyUpdate.ItemState}"); playerUpdates.Add(dirtyUpdate); player.KnownItems[nearbyItem] = tick; } @@ -251,7 +259,7 @@ private void ProcessReceivedAsHost(ItemUpdateData snapshot) return; } - if (NetworkedItem.Get(snapshot.ItemNetId, out NetworkedItem netItem)) + if (NetworkedItem.TryGet(snapshot.ItemNetId, out NetworkedItem netItem)) { if (ValidatePlayerAction(snapshot)) //Ensure the player can do this { @@ -302,12 +310,22 @@ private void ProcessClientChanges(uint tick) private void ProcessReceivedAsClient(ItemUpdateData snapshot) { + NetworkedItem.TryGet(snapshot.ItemNetId, out NetworkedItem netItem); + NetworkLifecycle.Instance.Client.LogDebug(() => $"NetworkedItemManager.ProcessReceivedAsClient() Update Type: {snapshot?.UpdateType}, ItemNetId: {snapshot?.ItemNetId}, prefabName: {snapshot?.PrefabName}"); if (snapshot.UpdateType == ItemUpdateData.ItemUpdateType.Create) { + //if the item already exists we need to remove it + if (netItem != null) + SendToCache(netItem); + CreateItem(snapshot); } - else if (NetworkedItem.Get(snapshot.ItemNetId, out NetworkedItem netItem)) + else if (snapshot.UpdateType == ItemUpdateData.ItemUpdateType.Destroy) + { + SendToCache(netItem); + } + else if (netItem != null) { netItem.ReceiveSnapshot(snapshot); } @@ -347,8 +365,8 @@ private void CreateItem(ItemUpdateData snapshot) } newItem.gameObject.SetActive(true); - newItem.NetId = snapshot.ItemNetId; + newItem.ReceiveSnapshot(snapshot); } @@ -430,11 +448,19 @@ private void SendToCache(NetworkedItem netItem) SingletonBehaviour.Instance.RemoveItemFromWorldStorage(netItem.Item); } + if (SingletonBehaviour.Instance.StorageInventory.ContainsItem(netItem.Item)) + { + SingletonBehaviour.Instance.RemoveItemFromStorageItemList(netItem.Item); + } + + if (SingletonBehaviour.Instance.StorageLostAndFound.ContainsItem(netItem.Item)) + { + SingletonBehaviour.Instance.RemoveItemFromStorageItemList(netItem.Item); + } + netItem.Item.InventorySpecs.BelongsToPlayer = false; netItem.NetId = 0; - - if (!CachedItems.ContainsKey(prefabName)) { CachedItems[prefabName] = new List(); @@ -444,6 +470,21 @@ private void SendToCache(NetworkedItem netItem) #endregion + public bool DoNotCreateItem(Type itemType) + { + if ( + itemType == typeof(JobOverview) || + itemType == typeof(JobBooklet) || + itemType == typeof(JobReport) || + itemType == typeof(JobExpiredReport) || + itemType == typeof(JobMissingLicenseReport) + ) + { + return true; + } + + return false; + } [UsedImplicitly] public new static string AllowAutoCreate() diff --git a/Multiplayer/Multiplayer.csproj b/Multiplayer/Multiplayer.csproj index f16678c..d80d6a9 100644 --- a/Multiplayer/Multiplayer.csproj +++ b/Multiplayer/Multiplayer.csproj @@ -73,6 +73,7 @@ + diff --git a/Multiplayer/Networking/Data/ItemUpdateData.cs b/Multiplayer/Networking/Data/ItemUpdateData.cs index d0e8280..85bf29a 100644 --- a/Multiplayer/Networking/Data/ItemUpdateData.cs +++ b/Multiplayer/Networking/Data/ItemUpdateData.cs @@ -16,8 +16,9 @@ public enum ItemUpdateType : byte Create = 1, Destroy = 2, ItemState = 4, - ObjectState = 8, - FullSync = 16, + ItemPosition = 8, + ObjectState = 16, + FullSync = ItemState | ItemPosition | ObjectState, } public ItemUpdateType UpdateType { get; set; } @@ -37,37 +38,36 @@ public void Serialize(NetDataWriter writer) writer.Put((byte)UpdateType); writer.Put(ItemNetId); - if(UpdateType == ItemUpdateType.Destroy) + if (UpdateType == ItemUpdateType.Destroy) return; - if(UpdateType.HasFlag(ItemUpdateType.ItemState) || UpdateType.HasFlag(ItemUpdateType.Create) || UpdateType.HasFlag(ItemUpdateType.FullSync)) - writer.Put((byte)ItemState); + writer.Put((byte)ItemState); if (UpdateType.HasFlag(ItemUpdateType.Create)) writer.Put(PrefabName); - if (UpdateType.HasFlag(ItemUpdateType.Create) || UpdateType.HasFlag(ItemUpdateType.FullSync) || (UpdateType.HasFlag(ItemUpdateType.ItemState) && ItemState == ItemState.Dropped)) + if (UpdateType.HasFlag(ItemUpdateType.Create) || UpdateType.HasFlag(ItemUpdateType.ItemState)) { - Vector3Serializer.Serialize(writer, ItemPosition); - QuaternionSerializer.Serialize(writer, ItemRotation); + if (ItemState == ItemState.Dropped || ItemState == ItemState.Thrown) // || UpdateType.HasFlag(ItemUpdateType.ItemPosition) + { + Vector3Serializer.Serialize(writer, ItemPosition); + QuaternionSerializer.Serialize(writer, ItemRotation); - if(ItemState == ItemState.InInventory || ItemState == ItemState.InHand) + if (ItemState == ItemState.Thrown) + Vector3Serializer.Serialize(writer, ThrowDirection); + } + else if (ItemState == ItemState.InInventory || ItemState == ItemState.InHand) { writer.Put(Player); } - else if(ItemState == ItemState.Attached) + else if (ItemState == ItemState.Attached) { writer.Put(CarNetId); writer.Put(AttachedFront); } } - if (UpdateType.HasFlag(ItemUpdateType.ItemState) && ItemState == ItemState.Thrown) - { - Vector3Serializer.Serialize(writer, ThrowDirection); - } - - if (UpdateType.HasFlag(ItemUpdateType.ObjectState) || UpdateType.HasFlag(ItemUpdateType.Create)) + if (UpdateType.HasFlag(ItemUpdateType.Create) || UpdateType.HasFlag(ItemUpdateType.ObjectState)) { if (States == null) writer.Put(0); @@ -91,19 +91,26 @@ public void Deserialize(NetDataReader reader) if (UpdateType == ItemUpdateType.Destroy) return; - if (UpdateType.HasFlag(ItemUpdateType.ItemState) || UpdateType.HasFlag(ItemUpdateType.Create) || UpdateType.HasFlag(ItemUpdateType.FullSync)) - ItemState = (ItemState)reader.GetByte(); + ItemState = (ItemState)reader.GetByte(); if (UpdateType.HasFlag(ItemUpdateType.Create)) PrefabName = reader.GetString(); - if (UpdateType.HasFlag(ItemUpdateType.Create) || UpdateType.HasFlag(ItemUpdateType.FullSync) || - (UpdateType.HasFlag(ItemUpdateType.ItemState) && ItemState == ItemState.Dropped)) + if (UpdateType.HasFlag(ItemUpdateType.Create) || UpdateType.HasFlag(ItemUpdateType.ItemState)) { - ItemPosition = Vector3Serializer.Deserialize(reader); - ItemRotation = QuaternionSerializer.Deserialize(reader); + if (ItemState == ItemState.Dropped || ItemState == ItemState.Thrown) // || UpdateType.HasFlag(ItemUpdateType.ItemPosition) + { + ItemPosition = Vector3Serializer.Deserialize(reader); + ItemRotation = QuaternionSerializer.Deserialize(reader); - if (ItemState == ItemState.InInventory || ItemState == ItemState.InHand) + if (ItemState == ItemState.Thrown) + { + Multiplayer.LogDebug(() => $"ItemUpdateData.Deserialize() Item Thrown before: {ThrowDirection}"); + ThrowDirection = Vector3Serializer.Deserialize(reader); + Multiplayer.LogDebug(() => $"ItemUpdateData.Deserialize() Item Thrown after: {ThrowDirection}"); + } + } + else if (ItemState == ItemState.InInventory || ItemState == ItemState.InHand) { Player = reader.GetUShort(); } @@ -114,12 +121,7 @@ public void Deserialize(NetDataReader reader) } } - if (UpdateType.HasFlag(ItemUpdateType.ItemState) && ItemState == ItemState.Thrown) - { - ThrowDirection = Vector3Serializer.Deserialize(reader); - } - - if (UpdateType.HasFlag(ItemUpdateType.ObjectState) || UpdateType.HasFlag(ItemUpdateType.Create)) + if (UpdateType.HasFlag(ItemUpdateType.Create) || UpdateType.HasFlag(ItemUpdateType.ObjectState)) { int stateCount = reader.GetInt(); if (stateCount > 0) @@ -147,14 +149,19 @@ private void SerializeTrackedValue(NetDataWriter writer, object value) writer.Put((byte)1); writer.Put(intValue); } - else if (value is float floatValue) + else if (value is uint uintValue) { writer.Put((byte)2); + writer.Put(uintValue); + } + else if (value is float floatValue) + { + writer.Put((byte)3); writer.Put(floatValue); } else if (value is string stringValue) { - writer.Put((byte)3); + writer.Put((byte)4); writer.Put(stringValue); } else @@ -170,8 +177,9 @@ private object DeserializeTrackedValue(NetDataReader reader) { case 0: return reader.GetBool(); case 1: return reader.GetInt(); - case 2: return reader.GetFloat(); - case 3: return reader.GetString(); + case 2: return reader.GetUInt(); + case 3: return reader.GetFloat(); + case 4: return reader.GetString(); default: throw new NotSupportedException($"ItemUpdateData.DeserializeTrackedValue({ItemNetId}, {PrefabName ?? ""}) Unsupported type code for deserialization: {typeCode}"); diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index b5ab55d..1893cca 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -790,34 +790,37 @@ private void OnCommonItemChangePacket(CommonItemChangePacket packet) { LogDebug(() => $"OnCommonItemChangePacket({packet?.Items?.Count})"); - /* + Multiplayer.LogDebug(() => + { + string debug = ""; + + foreach (var item in packet?.Items) { - string debug = ""; + debug += "UpdateType: " + item?.UpdateType + "\r\n"; + debug += "itemNetId: " + item?.ItemNetId + "\r\n"; + debug += "PrefabName: " + item?.PrefabName + "\r\n"; + debug += "Equipped: " + item?.ItemState + "\r\n"; + debug += "Position: " + item?.ItemPosition + "\r\n"; + debug += "Rotation: " + item?.ItemRotation + "\r\n"; + debug += "ThrowDirection: " + item?.ThrowDirection + "\r\n"; + debug += "Player: " + item?.Player + "\r\n"; + debug += "CarNetId: " + item?.CarNetId + "\r\n"; + debug += "AttachedFront: " + item?.AttachedFront + "\r\n"; + + debug += $"States: {item?.States?.Count}\r\n"; + + if (item.States != null) + foreach (var state in item?.States) + debug += "\t" + state.Key + ": " + state.Value + "\r\n"; + else + debug += "\r\n"; + } + + return debug; + }); - foreach (var item in packet?.Items) - { - //LogDebug(() => $"OnCommonItemChangePacket({packet?.Items?.Count}, {peer.Id}) in loop"); - debug += "UpdateType: " + item?.UpdateType + "\r\n"; - debug += "itemNetId: " + item?.ItemNetId + "\r\n"; - debug += "PrefabName: " + item?.PrefabName + "\r\n"; - debug += "Equipped: " + item?.Equipped + "\r\n"; - debug += "Dropped: " + item?.Dropped + "\r\n"; - debug += "Position: " + item?.PositionData.Position + "\r\n"; - debug += "Rotation: " + item?.PositionData.Rotation + "\r\n"; - - //LogDebug(() => $"OnCommonItemChangePacket({packet?.Items?.Count}, {peer.Id}) prep states"); - debug += "States:"; - - if (item.States != null) - foreach (var state in item?.States) - debug += "\r\n\t" + state.Key + ": " + state.Value; - } - return debug; - } - ); - */ NetworkedItemManager.Instance.ReceiveSnapshots(packet.Items); } diff --git a/Multiplayer/Patches/World/Items/FlashlightPatch.cs b/Multiplayer/Patches/World/Items/FlashlightPatch.cs new file mode 100644 index 0000000..24b752f --- /dev/null +++ b/Multiplayer/Patches/World/Items/FlashlightPatch.cs @@ -0,0 +1,88 @@ +using HarmonyLib; +using Multiplayer.Components.Networking.World; +using Multiplayer.Utils; +using UnityEngine; + +namespace Multiplayer.Patches.World.Items; + +[HarmonyPatch(typeof(FlashlightItem))] +public static class FlashlightItemPatch +{ + [HarmonyPatch(nameof(FlashlightItem.Start))] + static void Postfix(FlashlightItem __instance) + { + var networkedItem = __instance.gameObject.GetOrAddComponent(); + networkedItem.Initialize(__instance); + + // Register the values you want to track with both getters and setters + networkedItem.RegisterTrackedValue( + "originalLightIntensity ", + () => __instance.originalLightIntensity, + value => __instance.originalLightIntensity = value + ); + + networkedItem.RegisterTrackedValue( + "originalLightIntensity ", + () => __instance.originalLightIntensity, + value => __instance.originalLightIntensity = value + ); + + networkedItem.RegisterTrackedValue( + "intensity", + () => __instance.originalLightIntensity, + value => __instance.spotlight.intensity = value + ); + + networkedItem.RegisterTrackedValue( + "originalBeamColour", + () => __instance.originalBeamColor.ColorToUInt32(), + value =>__instance.originalBeamColor = value.UInt32ToColor() + ); + + networkedItem.RegisterTrackedValue( + "beamColour", + () => __instance.beamController.GetBeamColor().ColorToUInt32(), + value => + { + Color colour = value.UInt32ToColor(); + __instance.beamController.SetBeamColor(colour); + __instance.spotlight.color = colour; + } + ); + + networkedItem.RegisterTrackedValue( + "batteryCurrentPower", + () => __instance.battery.currentPower, + value => + { + __instance.battery.currentPower = value; //set the value + __instance.battery.UpdatePower(0f); //process a delta of 0 to force an update + } + ); + + networkedItem.RegisterTrackedValue( + "buttonState", + () => (__instance.button.Value > 0f), + value => + { + if (value) + __instance.button.SetValue(1f); + else + __instance.button.SetValue(0f); + + __instance.ToggleFlashlight(value); + } + ); + + //This may not be required testing needed + /* + networkedItem.RegisterTrackedValue( + "batteryDepleted", + () => __instance.battery.Depleted, + value =>__instance.battery.Depleted = value + ); + */ + + networkedItem.FinaliseTrackedValues(); + } +} diff --git a/Multiplayer/Patches/World/Items/AGrabHandlerPatch.cs b/Multiplayer/Patches/World/Items/GrabHandlerItem.cs similarity index 80% rename from Multiplayer/Patches/World/Items/AGrabHandlerPatch.cs rename to Multiplayer/Patches/World/Items/GrabHandlerItem.cs index 30f6489..333e5c7 100644 --- a/Multiplayer/Patches/World/Items/AGrabHandlerPatch.cs +++ b/Multiplayer/Patches/World/Items/GrabHandlerItem.cs @@ -5,12 +5,12 @@ namespace Multiplayer.Patches.World.Items; -[HarmonyPatch(typeof(AGrabHandler))] -public static class AGrabHandler_Patch +[HarmonyPatch(typeof(GrabHandlerItem))] +public static class GrabHandlerItem_Patch { - [HarmonyPatch(nameof(AGrabHandler.Throw))] + [HarmonyPatch(nameof(GrabHandlerItem.Throw))] [HarmonyPrefix] - private static void Throw(AGrabHandler __instance, Vector3 direction) + private static void Throw(GrabHandlerItem __instance, Vector3 direction) { __instance.TryGetComponent(out NetworkedItem netItem); diff --git a/Multiplayer/Patches/World/Items/ItemBasePatch.cs b/Multiplayer/Patches/World/Items/ItemBasePatch.cs index d57466e..9c899ec 100644 --- a/Multiplayer/Patches/World/Items/ItemBasePatch.cs +++ b/Multiplayer/Patches/World/Items/ItemBasePatch.cs @@ -13,7 +13,9 @@ public static class ItemBase_Patch private static void Awake(ItemBase __instance) { //Multiplayer.Log($"ItemBase.Awake() ItemSpec: {__instance?.InventorySpecs?.itemPrefabName}"); - __instance.GetOrAddComponent(); + var networkedItem = __instance.GetOrAddComponent(); + + //networkedItem.FinaliseTrackedValues(); return; } } diff --git a/Multiplayer/Patches/World/Items/LanternPatch.cs b/Multiplayer/Patches/World/Items/LanternPatch.cs index e140123..e61a541 100644 --- a/Multiplayer/Patches/World/Items/LanternPatch.cs +++ b/Multiplayer/Patches/World/Items/LanternPatch.cs @@ -1,11 +1,6 @@ using HarmonyLib; using Multiplayer.Components.Networking.World; using Multiplayer.Utils; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Multiplayer.Patches.World.Items; @@ -37,5 +32,7 @@ static void Postfix(Lantern __instance) __instance.OnFlameExtinguished(); } ); + + networkedItem.FinaliseTrackedValues(); } } diff --git a/Multiplayer/Patches/World/Items/LighterPatch.cs b/Multiplayer/Patches/World/Items/LighterPatch.cs index 4e7dbbc..0044dd3 100644 --- a/Multiplayer/Patches/World/Items/LighterPatch.cs +++ b/Multiplayer/Patches/World/Items/LighterPatch.cs @@ -4,28 +4,21 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using UnityEngine; namespace Multiplayer.Patches.World.Items; -[HarmonyPatch(typeof(Lighter), "Start")] +[HarmonyPatch(typeof(Lighter))] public static class LighterPatch { - static void Postfix(Lighter __instance) + [HarmonyPatch(nameof(Lighter.Start))] + [HarmonyPostfix] + static void Start(Lighter __instance) { - var networkedItem = __instance.gameObject.GetOrAddComponent(); + var netItem = __instance.gameObject.GetOrAddComponent(); + netItem.Initialize(__instance); - __instance.StartCoroutine(Init(networkedItem, __instance)); - } - - private static IEnumerator Init(NetworkedItem netItem, Lighter lighter) - { - while (!lighter.initialized) - yield return null; - - netItem.Initialize(lighter); + Lighter lighter = __instance; // Register the values you want to track with both getters and setters netItem.RegisterTrackedValue( @@ -33,23 +26,49 @@ private static IEnumerator Init(NetworkedItem netItem, Lighter lighter) () => lighter.isOpen, value => { - if (value) - lighter.OpenLid(); + bool active = lighter.gameObject.activeInHierarchy; + if (active) + { + if (value) + lighter.OpenLid(active); + else + lighter.CloseLid(!active); + } else - lighter.CloseLid(); + { + lighter.isOpen = value; + } } ); netItem.RegisterTrackedValue( "Ignited", - () => lighter.igniter.enabled, + () => lighter.IsFireOn(), value => { - if (value) - lighter.LightFire(true, true); + bool active = lighter.gameObject.activeInHierarchy; + if (active) + if (value) + lighter.LightFire(true, true); + else + lighter.flame.UpdateFlameIntensity(0f, true); else - lighter.OnFlameExtinguished(); + if (value && lighter.isOpen) + lighter.flame.UpdateFlameIntensity(1f, true); } ); + + netItem.FinaliseTrackedValues(); + } + + [HarmonyPatch(nameof(Lighter.OnEnable))] + [HarmonyPostfix] + + static void OnEnable(Lighter __instance) + { + if (__instance.isOpen) + { + __instance.lighterAnimator.Play("lighter_case_top_open", 0); + } } } diff --git a/Multiplayer/Patches/World/Items/ShovelPatch.cs b/Multiplayer/Patches/World/Items/ShovelPatch.cs new file mode 100644 index 0000000..5234c99 --- /dev/null +++ b/Multiplayer/Patches/World/Items/ShovelPatch.cs @@ -0,0 +1,55 @@ +using DV.CabControls; +using HarmonyLib; +using Multiplayer.Components.Networking.World; +using Multiplayer.Utils; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using UnityEngine; + +namespace Multiplayer.Patches.World.Items; + +[HarmonyPatch(typeof(Shovel))] +public static class ShovelPatch +{ + [HarmonyPatch(nameof(Shovel.Start))] + [HarmonyPostfix] + static void Start(Shovel __instance) + { + var netItem = __instance.gameObject.GetOrAddComponent(); + + netItem.Initialize(__instance); + + ShovelNonPhysicalCoal shovelNonPhysicalCoal = __instance.GetComponent(); + if( shovelNonPhysicalCoal == null) + { + Multiplayer.LogWarning($"Shovel.Start() netId: {netItem.NetId} Failed to find ShovelNonPhysicalCoal"); + return; + } + + // Register the values you want to track with both getters and setters + netItem.RegisterTrackedValue( + "coalMassCapacity", + () => shovelNonPhysicalCoal.coalMassCapacity, + value => + { + shovelNonPhysicalCoal.coalMassCapacity = value; + } + ); + + netItem.RegisterTrackedValue( + "coalMassLoaded", + () => shovelNonPhysicalCoal.coalMassLoaded, + value => + { + shovelNonPhysicalCoal.coalMassLoaded = value; + } + ); + + netItem.FinaliseTrackedValues(); + } + +} diff --git a/Multiplayer/Utils/UnityExtensions.cs b/Multiplayer/Utils/UnityExtensions.cs index ed75e18..c587612 100644 --- a/Multiplayer/Utils/UnityExtensions.cs +++ b/Multiplayer/Utils/UnityExtensions.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using JetBrains.Annotations; @@ -105,4 +105,22 @@ public static T GetOrAddComponent(this Component component) where T : Compone { return component.gameObject.GetOrAddComponent(); } + + public static uint ColorToUInt32(this Color color) + { + uint r = (uint)(color.r * 255); + uint g = (uint)(color.g * 255); + uint b = (uint)(color.b * 255); + uint a = (uint)(color.a * 255); + return (a << 24) | (r << 16) | (g << 8) | b; + } + + public static Color UInt32ToColor(this uint packed) + { + float a = ((packed >> 24) & 0xFF) / 255f; + float r = ((packed >> 16) & 0xFF) / 255f; + float g = ((packed >> 8) & 0xFF) / 255f; + float b = (packed & 0xFF) / 255f; + return new Color(r, g, b, a); + } } From dd44f8841bf77a1861c2d133f8fb2283817bb687 Mon Sep 17 00:00:00 2001 From: AMacro Date: Thu, 21 Nov 2024 12:22:58 +1000 Subject: [PATCH 115/188] Remove excessive logging for jobs --- Multiplayer/Components/Networking/Jobs/NetworkedJob.cs | 2 +- Multiplayer/Networking/Data/JobData.cs | 8 ++++---- Multiplayer/Networking/Data/TaskNetworkData.cs | 1 + 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs b/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs index 663d4a9..bffad9c 100644 --- a/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs +++ b/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs @@ -165,7 +165,7 @@ private void AddToCache() jobToNetworkedJob[Job] = this; jobIdToNetworkedJob[Job.ID] = this; jobIdToJob[Job.ID] = Job; - Multiplayer.Log($"NetworkedJob added to cache: {Job.ID}"); + //Multiplayer.Log($"NetworkedJob added to cache: {Job.ID}"); } private void OnJobTaken(Job job, bool viaLoadGame) diff --git a/Multiplayer/Networking/Data/JobData.cs b/Multiplayer/Networking/Data/JobData.cs index 2776a6a..638a9df 100644 --- a/Multiplayer/Networking/Data/JobData.cs +++ b/Multiplayer/Networking/Data/JobData.cs @@ -33,7 +33,7 @@ public static JobData FromJob(NetworkedStationController netStation, NetworkedJo ushort itemNetId = 0; ItemPositionData itemPos = new ItemPositionData(); - Multiplayer.Log($"JobData.FromJob({netStation.name}, {job.ID}, {networkedJob.Job.State})"); + //Multiplayer.Log($"JobData.FromJob({netStation.name}, {job.ID}, {networkedJob.Job.State})"); if (networkedJob.Job.State == JobState.Available) { @@ -80,7 +80,7 @@ public static JobData FromJob(NetworkedStationController netStation, NetworkedJo public static void Serialize(NetDataWriter writer, JobData data) { - NetworkLifecycle.Instance.Server.Log($"JobData.Serialize({data.ID}) NetID {data.NetID}"); + //NetworkLifecycle.Instance.Server.Log($"JobData.Serialize({data.ID}) NetID {data.NetID}"); writer.Put(data.NetID); writer.Put((byte)data.JobType); @@ -104,7 +104,7 @@ public static void Serialize(NetDataWriter writer, JobData data) byte[] compressedData = PacketCompression.Compress(ms.ToArray()); - Multiplayer.Log($"JobData.Serialize() Uncompressed: {ms.Length} Compressed: {compressedData.Length}"); + // Multiplayer.Log($"JobData.Serialize() Uncompressed: {ms.Length} Compressed: {compressedData.Length}"); writer.PutBytesWithLength(compressedData); } @@ -133,7 +133,7 @@ public static JobData Deserialize(NetDataReader reader) byte[] compressedData = reader.GetBytesWithLength(); byte[] decompressedData = PacketCompression.Decompress(compressedData); - Multiplayer.Log($"JobData.Deserialize() Compressed: {compressedData.Length} Decompressed: {decompressedData.Length}"); + //Multiplayer.Log($"JobData.Deserialize() Compressed: {compressedData.Length} Decompressed: {decompressedData.Length}"); TaskNetworkData[] tasks; diff --git a/Multiplayer/Networking/Data/TaskNetworkData.cs b/Multiplayer/Networking/Data/TaskNetworkData.cs index 6d5f207..eaa760f 100644 --- a/Multiplayer/Networking/Data/TaskNetworkData.cs +++ b/Multiplayer/Networking/Data/TaskNetworkData.cs @@ -77,6 +77,7 @@ public static void RegisterTaskType(TaskType taskType, Func$"TaskNetworkDataFactory.ConvertTask: Processing task of type {task.GetType()}"); + //Multiplayer.LogDebug(()=>$"TaskNetworkDataFactory.ConvertTask: Processing task of type {task.GetType()}"); if (TypeToTaskNetworkData.TryGetValue(task.GetType(), out var converter)) { return converter(task); From 08f469a3830012d456966133152f0c2ffbaf57ac Mon Sep 17 00:00:00 2001 From: AMacro Date: Thu, 21 Nov 2024 12:33:48 +1000 Subject: [PATCH 116/188] Add authority to tracked values (Server update vs Common update) --- .../Networking/World/NetworkedItem.cs | 92 +++++++++++-------- Multiplayer/Networking/Data/TrackedValue.cs | 18 +++- .../Managers/Client/NetworkClient.cs | 4 +- .../Managers/Server/NetworkServer.cs | 11 +++ .../Patches/World/Items/FlashlightPatch.cs | 49 +++++----- .../Patches/World/Items/LighterPatch.cs | 21 ++++- 6 files changed, 123 insertions(+), 72 deletions(-) diff --git a/Multiplayer/Components/Networking/World/NetworkedItem.cs b/Multiplayer/Components/Networking/World/NetworkedItem.cs index 00fa9d8..9929de8 100644 --- a/Multiplayer/Components/Networking/World/NetworkedItem.cs +++ b/Multiplayer/Components/Networking/World/NetworkedItem.cs @@ -117,8 +117,12 @@ protected override void Awake() protected void Start() { - if (!createdDirty) - return; + if (!initialised) + Register(); + + // Mark registration as complete for items that don't need tracked values + if (!registrationComplete && !UsefulItem) + registrationComplete = true; } public T GetTrackedItem() where T : Component @@ -128,7 +132,9 @@ public T GetTrackedItem() where T : Component public void Initialize(T item, ushort netId = 0, bool createDirty = true) where T : Component { - if(netId != 0) + Multiplayer.LogDebug(() => $"NetworkedItem.Initialize<{typeof(T)}>(netId: {netId}, name: {name}, createDirty: {createdDirty})"); + + if (netId != 0) NetId = netId; trackedItem = item; @@ -152,7 +158,7 @@ private bool Register() if (!TryGetComponent(out ItemBase itemBase)) { - Multiplayer.LogError($"Unable to find ItemBase for {name}"); + Multiplayer.LogError($"NetworkedItem.Register() Unable to find ItemBase for {name}"); return false; } @@ -165,9 +171,6 @@ private bool Register() //Find special interaction components TryGetComponent(out grabHandler); TryGetComponent(out snappableOnCoupler); - - - //Item.ItemInventoryStateChanged += OnItemInventoryStateChanged; lastState = GetItemState(); stateDirty = false; @@ -177,20 +180,20 @@ private bool Register() } catch (Exception ex) { - Multiplayer.LogError($"Unable to find ItemBase for {name}\r\n{ex.Message}"); + Multiplayer.LogError($"NetworkedItem.Register() Unable to find ItemBase for {name}\r\n{ex.Message}"); return false; } } private void OnUngrabbed(ControlImplBase obj) { - Multiplayer.LogDebug(() => $"OnUngrabbed() NetID: {NetId}, {name}"); + Multiplayer.LogDebug(() => $"NetworkedItem.OnUngrabbed() NetID: {NetId}, {name}"); stateDirty = true; } private void OnGrabbed(ControlImplBase obj) { - Multiplayer.LogDebug(() => $"OnGrabbed() NetID: {NetId}, {name}"); + Multiplayer.LogDebug(() => $"NetworkedItem.OnGrabbed() NetID: {NetId}, {name}"); stateDirty = true; } @@ -207,7 +210,7 @@ public void OnThrow(Vector3 direction) thrownPosition = Item.transform.position - WorldMover.currentMove; thrownRotation = Item.transform.rotation; - Multiplayer.LogDebug(() => $"OnThrow() netId: {NetId}, Name: {name}, Raw Position: {Item.transform.position}, Position: {thrownPosition}, Rotation: {thrownRotation}, Direction: {throwDirection}"); + Multiplayer.LogDebug(() => $"NetworkedItem.OnThrow() netId: {NetId}, Name: {name}, Raw Position: {Item.transform.position}, Position: {thrownPosition}, Rotation: {thrownRotation}, Direction: {throwDirection}"); wasThrown = true; stateDirty = true; @@ -215,10 +218,10 @@ public void OnThrow(Vector3 direction) #region Item Value Tracking - public void RegisterTrackedValue(string key, Func valueGetter, Action valueSetter) + public void RegisterTrackedValue(string key, Func valueGetter, Action valueSetter, Func thresholdComparer = null, bool serverAuthoritative = false) { - Multiplayer.LogDebug(() => $"NetworkedItem.RegisterTrackedValue(\"{key}\", {valueGetter != null}, {valueSetter != null}) itemNetId {NetId}, item name: {name}"); - trackedValues.Add(new TrackedValue(key, valueGetter, valueSetter)); + Multiplayer.LogDebug(() => $"NetworkedItem.RegisterTrackedValue(\"{key}\", {valueGetter != null}, {valueSetter != null}, {thresholdComparer != null}, {serverAuthoritative}) itemNetId {NetId}, item name: {name}"); + trackedValues.Add(new TrackedValue(key, valueGetter, valueSetter, thresholdComparer, serverAuthoritative)); } public void FinaliseTrackedValues() @@ -237,7 +240,11 @@ public void FinaliseTrackedValues() private bool HasDirtyValues() { - return trackedValues.Any(tv => ((dynamic)tv).IsDirty); + //clients should only send values that are not server authoritative + if(!NetworkLifecycle.Instance.IsHost()) + return trackedValues.Any(tv => ((dynamic)tv).IsDirty && !((dynamic)tv).ServerAuthoritative); + else + return trackedValues.Any(tv => ((dynamic)tv).IsDirty); } private Dictionary GetDirtyStateData() @@ -350,7 +357,7 @@ private void ApplySnapshot(ItemUpdateData snapshot) case ItemState.InHand: case ItemState.InInventory: - HandleInventoryorHandState(snapshot); + HandleInventoryOrHandState(snapshot); break; case ItemState.Attached: @@ -479,34 +486,34 @@ private ItemState GetItemState() private void ApplyTrackedValues(Dictionary newValues) { - Multiplayer.LogDebug(() => $"NetworkedItem.ApplyTrackedValues() itemNetId: {NetId}, item name: {name}. Null checks"); + Multiplayer.LogDebug(() => $"NetworkedItem.ApplyTrackedValues() itemNetId: {NetId}, item name: {name}. Null checks"); if (newValues == null || newValues.Count == 0) - return; //yield break; + return; - //int i = 0; - //while (!registrationComplete) - //{ - // Multiplayer.LogDebug(() => $"NetworkedItem.ApplyTrackedValues() itemNetId: {NetId}, item name: {name}. Registration checks: {i}"); - // i++; - // //yield return null; - //} - Multiplayer.LogDebug(() => $"NetworkedItem.ApplyTrackedValues() itemNetId: {NetId}, item name: {name}. Registration complete: {registrationComplete}"); + Multiplayer.LogDebug(() => $"NetworkedItem.ApplyTrackedValues() itemNetId: {NetId}, item name: {name}. Registration complete: {registrationComplete}"); foreach (var newValue in newValues) { var trackedValue = trackedValues.Find(tv => ((dynamic)tv).Key == newValue.Key); if (trackedValue != null) { - try + if (!NetworkLifecycle.Instance.IsHost() || !((dynamic)trackedValue).ServerAuthoritative) { - ((dynamic)trackedValue).SetValueFromObject(newValue.Value); - Multiplayer.LogDebug(() => $"Updated tracked value: {newValue.Key}, value: {newValue.Value} "); + try + { + ((dynamic)trackedValue).SetValueFromObject(newValue.Value); + Multiplayer.LogDebug(() => $"NetworkedItem.ApplyTrackedValues() itemNetId: {NetId}, item name: {name}, Updated tracked value: {newValue.Key}, value: {newValue.Value} "); + } + catch (Exception ex) + { + Multiplayer.LogError($"NetworkedItem.ApplyTrackedValues() itemNetId: {NetId}, item name: {name}. Error updating tracked value {newValue.Key}: {ex.Message}"); + } } - catch (Exception ex) + else { - Multiplayer.LogError($"Error updating tracked value {newValue.Key}: {ex.Message}"); + Multiplayer.LogWarning(() => $"NetworkedItem.ApplyTrackedValues() itemNetId: {NetId}, item name: {name}. Skipped server-authoritative value update from client: {newValue.Key}"); } } else @@ -520,11 +527,19 @@ private void ApplyTrackedValues(Dictionary newValues) private void HandleDroppedOrThrownState(ItemUpdateData snapshot) { + //resolve attachment + if (Item.IsSnapped) + { + Item.SnappableItem.SnappedTo.UnsnapItem(false); + } + + //activate and relocate item gameObject.SetActive(true); transform.position = snapshot.ItemPosition + WorldMover.currentMove; transform.rotation = snapshot.ItemRotation; OwnerId = 0; + //handle throwing of the item if (snapshot.ItemState == ItemState.Thrown) { Multiplayer.LogDebug(()=>$"NetworkedItem.HandleDroppedOrThrownState() ItemNetId: {snapshot?.ItemNetId} Thrown. Position: {transform.position}, Direction: {snapshot?.ThrowDirection}"); @@ -540,6 +555,7 @@ private void HandleDroppedOrThrownState(ItemUpdateData snapshot) private void HandleAttachedState(ItemUpdateData snapshot) { + //handle attaching the item gameObject.SetActive(true); Multiplayer.LogDebug(() => $"NetworkedItem.HandleAttachedState() ItemNetId: {snapshot?.ItemNetId} attempting attachment to car {snapshot.CarNetId}, at the front {snapshot.AttachedFront}"); @@ -549,13 +565,13 @@ private void HandleAttachedState(ItemUpdateData snapshot) return; } - //Try to find the coupler snap point for the car and correct end + //Try to find the coupler snap point for the car and correct end to snap to var snapPoint = trainCar?.physicsLod?.GetCouplerSnapPoints() .FirstOrDefault(sp => sp.IsFront == snapshot.AttachedFront); if (snapPoint == null) { - Multiplayer.LogWarning($"No valid snap point found for car {snapshot.CarNetId}"); + Multiplayer.LogWarning($"NetworkedItem.HandleAttachedState() ItemNetId: {snapshot?.ItemNetId}. No valid snap point found for car {snapshot.CarNetId}"); return; } @@ -563,12 +579,17 @@ private void HandleAttachedState(ItemUpdateData snapshot) Item.ItemRigidbody.isKinematic = false; if (!snapPoint.SnapItem(Item, false)) { - Multiplayer.LogWarning($"Attachment failed for item {snapshot?.ItemNetId} to car {snapshot.CarNetId}"); + Multiplayer.LogWarning($"NetworkedItem.HandleAttachedState() Attachment failed for item {snapshot?.ItemNetId} to car {snapshot.CarNetId}"); } } - private void HandleInventoryorHandState(ItemUpdateData snapshot) + private void HandleInventoryOrHandState(ItemUpdateData snapshot) { + if (Item.IsSnapped) + { + Item.SnappableItem.SnappedTo.UnsnapItem(false); + } + //todo add to player model's hand this.gameObject.SetActive(false); } @@ -588,7 +609,6 @@ protected override void OnDestroy() { Item.Grabbed -= OnGrabbed; Item.Ungrabbed -= OnUngrabbed; - //Item.ItemInventoryStateChanged -= OnItemInventoryStateChanged; itemBaseToNetworkedItem.Remove(Item); } else diff --git a/Multiplayer/Networking/Data/TrackedValue.cs b/Multiplayer/Networking/Data/TrackedValue.cs index d9f8485..02db9d3 100644 --- a/Multiplayer/Networking/Data/TrackedValue.cs +++ b/Multiplayer/Networking/Data/TrackedValue.cs @@ -1,3 +1,4 @@ +using Multiplayer.Components.Networking; using System; using System.Collections.Generic; @@ -8,17 +9,25 @@ public class TrackedValue private T lastSentValue; private Func valueGetter; private Action valueSetter; + private Func thresholdComparer; + private bool serverAuthoritative; public string Key { get; } - public TrackedValue(string key, Func valueGetter, Action valueSetter) + public TrackedValue(string key, Func valueGetter, Action valueSetter, Func thresholdComparer = null, bool serverAuthoritative = false) { Key = key; this.valueGetter = valueGetter; this.valueSetter = valueSetter; + + this.thresholdComparer = thresholdComparer ?? DefaultComparer; + this.serverAuthoritative = serverAuthoritative; + lastSentValue = valueGetter(); } - public bool IsDirty => !EqualityComparer.Default.Equals(CurrentValue, lastSentValue); + public bool IsDirty => thresholdComparer(CurrentValue, lastSentValue); + + public bool ServerAuthoritative => serverAuthoritative; public T CurrentValue { @@ -49,6 +58,11 @@ public void SetValueFromObject(object value) } } + private bool DefaultComparer(T current, T last) + { + return !current.Equals(last); + } + public string GetDebugString() { return $"{Key}: {lastSentValue} -> {CurrentValue}"; diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 1893cca..5e22967 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -820,9 +820,7 @@ private void OnCommonItemChangePacket(CommonItemChangePacket packet) return debug; }); - - - NetworkedItemManager.Instance.ReceiveSnapshots(packet.Items); + NetworkedItemManager.Instance.ReceiveSnapshots(packet.Items, null); } #endregion diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index 1f10f9e..4ae7e57 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -160,6 +160,10 @@ public bool TryGetServerPlayer(NetPeer peer, out ServerPlayer player) { return serverPlayers.TryGetValue((byte)peer.Id, out player); } + public bool TryGetServerPlayer(byte id, out ServerPlayer player) + { + return serverPlayers.TryGetValue(id, out player); + } public bool TryGetNetPeer(byte id, out NetPeer peer) { @@ -1000,6 +1004,10 @@ private void OnUnconnectedPingPacket(UnconnectedPingPacket packet, IPEndPoint en private void OnCommonItemChangePacket(CommonItemChangePacket packet, NetPeer peer) { LogDebug(()=>$"OnCommonItemChangePacket({packet?.Items?.Count}, {peer.Id})"); + if(!TryGetServerPlayer(peer, out var player)) + return; + + LogDebug(()=>$"OnCommonItemChangePacket({packet?.Items?.Count}, {peer.Id} (\"{player.Username}\"))"); Multiplayer.LogDebug(() => { @@ -1029,6 +1037,9 @@ private void OnCommonItemChangePacket(CommonItemChangePacket packet, NetPeer pee } ); + ); + + NetworkedItemManager.Instance.ReceiveSnapshots(packet.Items, player); } #endregion } diff --git a/Multiplayer/Patches/World/Items/FlashlightPatch.cs b/Multiplayer/Patches/World/Items/FlashlightPatch.cs index 24b752f..3a5f713 100644 --- a/Multiplayer/Patches/World/Items/FlashlightPatch.cs +++ b/Multiplayer/Patches/World/Items/FlashlightPatch.cs @@ -1,6 +1,7 @@ using HarmonyLib; using Multiplayer.Components.Networking.World; using Multiplayer.Utils; +using System; using UnityEngine; namespace Multiplayer.Patches.World.Items; @@ -16,27 +17,25 @@ static void Postfix(FlashlightItem __instance) // Register the values you want to track with both getters and setters networkedItem.RegisterTrackedValue( - "originalLightIntensity ", + "originalLightIntensity", () => __instance.originalLightIntensity, - value => __instance.originalLightIntensity = value + value => __instance.originalLightIntensity = value, + serverAuthoritative: true //This parameter is driven by the server: true ); - networkedItem.RegisterTrackedValue( - "originalLightIntensity ", - () => __instance.originalLightIntensity, - value => __instance.originalLightIntensity = value - ); - - networkedItem.RegisterTrackedValue( - "intensity", - () => __instance.originalLightIntensity, - value => __instance.spotlight.intensity = value - ); + //probably not needed as flicker can be handled locally + //networkedItem.RegisterTrackedValue( + // "intensity", + // () => __instance.spotlight.intensity, + // value => __instance.spotlight.intensity = value, + // serverAuthoritative: true //This parameter is driven by the server: true + // ); networkedItem.RegisterTrackedValue( "originalBeamColour", () => __instance.originalBeamColor.ColorToUInt32(), - value =>__instance.originalBeamColor = value.UInt32ToColor() + value =>__instance.originalBeamColor = value.UInt32ToColor(), + serverAuthoritative: true //This parameter is driven by the server: true ); networkedItem.RegisterTrackedValue( @@ -47,17 +46,20 @@ static void Postfix(FlashlightItem __instance) Color colour = value.UInt32ToColor(); __instance.beamController.SetBeamColor(colour); __instance.spotlight.color = colour; - } + }, + serverAuthoritative: true //This parameter is driven by the server: true ); networkedItem.RegisterTrackedValue( - "batteryCurrentPower", + "batteryPower", () => __instance.battery.currentPower, value => { - __instance.battery.currentPower = value; //set the value - __instance.battery.UpdatePower(0f); //process a delta of 0 to force an update - } + __instance.battery.currentPower = value; //set the value + __instance.battery.UpdatePower(0f); //process a delta of 0 to force an update + }, + (current, last) => Math.Abs(current - last) >= 1.0f, //Don't communicate updates for changes less than 1f + true //This parameter is driven by the server: true ); networkedItem.RegisterTrackedValue( @@ -74,15 +76,6 @@ static void Postfix(FlashlightItem __instance) } ); - //This may not be required testing needed - /* - networkedItem.RegisterTrackedValue( - "batteryDepleted", - () => __instance.battery.Depleted, - value =>__instance.battery.Depleted = value - ); - */ - networkedItem.FinaliseTrackedValues(); } } diff --git a/Multiplayer/Patches/World/Items/LighterPatch.cs b/Multiplayer/Patches/World/Items/LighterPatch.cs index 0044dd3..ba23a46 100644 --- a/Multiplayer/Patches/World/Items/LighterPatch.cs +++ b/Multiplayer/Patches/World/Items/LighterPatch.cs @@ -30,13 +30,15 @@ static void Start(Lighter __instance) if (active) { if (value) - lighter.OpenLid(active); + lighter.OpenLid(); else - lighter.CloseLid(!active); + lighter.CloseLid(); } else { lighter.isOpen = value; + if (!value) + lighter.CloseLid(true); } } ); @@ -48,13 +50,26 @@ static void Start(Lighter __instance) { bool active = lighter.gameObject.activeInHierarchy; if (active) + { if (value) lighter.LightFire(true, true); else lighter.flame.UpdateFlameIntensity(0f, true); + } else + { if (value && lighter.isOpen) - lighter.flame.UpdateFlameIntensity(1f, true); + { + lighter.flame.UpdateFlameIntensity(1f, true); + lighter.OnFlameIgnited(); + + } + else + { + lighter.flame.UpdateFlameIntensity(0f, true); + lighter.OnFlameExtinguished(); + } + } } ); From c9a57425ecb61b858e4b73856d0aa4e8b8a0d3ea Mon Sep 17 00:00:00 2001 From: AMacro Date: Fri, 22 Nov 2024 10:34:15 +1000 Subject: [PATCH 117/188] Begin implementation of item update validation and ownership --- .../Networking/World/NetworkedItem.cs | 19 +++- .../Networking/World/NetworkedItemManager.cs | 104 ++++++++++++++---- Multiplayer/Multiplayer.csproj | 2 +- Multiplayer/Networking/Data/ItemUpdateData.cs | 4 +- Multiplayer/Networking/Data/ServerPlayer.cs | 44 ++++++++ .../Managers/Server/NetworkServer.cs | 18 --- .../Patches/World/Items/RespawnOnDropPatch.cs | 87 +++++++++++++++ .../Patches/World/StorageControllerPatch.cs | 82 ++++++++++++++ info.json | 2 +- 9 files changed, 318 insertions(+), 44 deletions(-) create mode 100644 Multiplayer/Patches/World/Items/RespawnOnDropPatch.cs create mode 100644 Multiplayer/Patches/World/StorageControllerPatch.cs diff --git a/Multiplayer/Components/Networking/World/NetworkedItem.cs b/Multiplayer/Components/Networking/World/NetworkedItem.cs index 9929de8..7676dd4 100644 --- a/Multiplayer/Components/Networking/World/NetworkedItem.cs +++ b/Multiplayer/Components/Networking/World/NetworkedItem.cs @@ -69,7 +69,6 @@ public static bool TryGetNetworkedItem(ItemBase item, out NetworkedItem networke private List trackedValues = new List(); public bool UsefulItem { get; private set; } = false; public Type TrackedItemType { get; private set; } - public bool BlockSync { get; set; } = false; public uint LastDirtyTick { get; private set; } private bool initialised; private bool registrationComplete = false; @@ -86,7 +85,7 @@ public static bool TryGetNetworkedItem(ItemBase item, out NetworkedItem networke private Vector3 throwDirection; //Handle ownership - public ushort OwnerId { get; private set; } = 0; // 0 means no owner + public sbyte OwnerId { get; private set; } = -1; // 0 means no owner //public void SetOwner(ushort playerId) //{ @@ -479,7 +478,7 @@ private ItemState GetItemState() return ItemState.Attached; } - //we need a condition to check if it's attached to something else + //do we need a condition to check if it's attached to something else (last attach vs current attach)? return ItemState.Dropped; } @@ -533,6 +532,11 @@ private void HandleDroppedOrThrownState(ItemUpdateData snapshot) Item.SnappableItem.SnappedTo.UnsnapItem(false); } + //resolve ownership + if (NetworkLifecycle.Instance.IsHost()) + if (NetworkLifecycle.Instance.Server.TryGetServerPlayer(snapshot.Player, out ServerPlayer player) && player.OwnsItem(NetId)) + player.RemoveOwnedItem(NetId); + //activate and relocate item gameObject.SetActive(true); transform.position = snapshot.ItemPosition + WorldMover.currentMove; @@ -555,6 +559,11 @@ private void HandleDroppedOrThrownState(ItemUpdateData snapshot) private void HandleAttachedState(ItemUpdateData snapshot) { + //resovle ownership + if (NetworkLifecycle.Instance.IsHost()) + if (NetworkLifecycle.Instance.Server.TryGetServerPlayer(snapshot.Player, out ServerPlayer player) && player.OwnsItem(NetId)) + player.RemoveOwnedItem(NetId); + //handle attaching the item gameObject.SetActive(true); Multiplayer.LogDebug(() => $"NetworkedItem.HandleAttachedState() ItemNetId: {snapshot?.ItemNetId} attempting attachment to car {snapshot.CarNetId}, at the front {snapshot.AttachedFront}"); @@ -590,6 +599,10 @@ private void HandleInventoryOrHandState(ItemUpdateData snapshot) Item.SnappableItem.SnappedTo.UnsnapItem(false); } + if (NetworkLifecycle.Instance.IsHost()) + if(NetworkLifecycle.Instance.Server.TryGetServerPlayer(snapshot.Player, out ServerPlayer player) && !player.OwnsItem(NetId)) + player.AddOwnedItem(NetId); + //todo add to player model's hand this.gameObject.SetActive(false); } diff --git a/Multiplayer/Components/Networking/World/NetworkedItemManager.cs b/Multiplayer/Components/Networking/World/NetworkedItemManager.cs index 1817f96..4086fe9 100644 --- a/Multiplayer/Components/Networking/World/NetworkedItemManager.cs +++ b/Multiplayer/Components/Networking/World/NetworkedItemManager.cs @@ -8,29 +8,47 @@ using System; using Multiplayer.Utils; using DV; -using DV.CabControls.Spec; -using System.ComponentModel; -using Cysharp.Threading.Tasks.Triggers; +using DV.Interaction; namespace Multiplayer.Components.Networking.Train; public class NetworkedItemManager : SingletonBehaviour { + /* + * Server + */ + + //Culling distance for items public const float MAX_DISTANCE_TO_ITEM = 100f; public const float MAX_DISTANCE_TO_ITEM_SQR = MAX_DISTANCE_TO_ITEM * MAX_DISTANCE_TO_ITEM; public const float NEARBY_REMOVAL_DELAY = 3f; // 3 seconds delay + public const float REACH_DISTANCE_BUFFER = 0.5f; + public float MAX_REACH_DISTANCE = 4f + REACH_DISTANCE_BUFFER; //from the game, but we should try to look up the value + //caches for item snapshots private List DestroyedItems = new List(); - private Queue ReceivedSnapshots = new Queue(); - private Dictionary> CachedItems = new Dictionary>(); - private Dictionary ItemPrefabs = new Dictionary(); - - private bool ClientInitialised = false; + //Item ownership //private Dictionary playerInventories = new Dictionary(); //private Dictionary itemToPlayerMap = new Dictionary(); + /* + * Client + */ + + //cache for client-sided items & spawns + private Dictionary> CachedItems = new Dictionary>(); //Client cached items + private Dictionary ItemPrefabs = new Dictionary(); //Item prefabs + private bool ClientInitialised = false; + + + /* + * Common + */ + private Queue> ReceivedSnapshots = new Queue>(); + + protected override void Awake() { base.Awake(); @@ -38,6 +56,15 @@ protected override void Awake() return; NetworkLifecycle.Instance.Server.PlayerDisconnect += PlayerDisconnected; + + try + { + MAX_REACH_DISTANCE = GrabberRaycasterDV.SPHERE_CAST_MAX_DIST + REACH_DISTANCE_BUFFER; + } + catch (Exception ex) + { + NetworkLifecycle.Instance.Server.LogWarning($"NatworkedItemManager.Awake() Failed to find GrabberRaycasterDV\r\n{ex.Message}"); + } } private void PlayerDisconnected(uint netID) @@ -75,17 +102,17 @@ public void AddDirtyItemSnapshot(NetworkedItem netItem, ItemUpdateData snapshot) } } - public void ReceiveSnapshots(List snapshots) + public void ReceiveSnapshots(List snapshots, ServerPlayer sender) { if (snapshots == null) return; foreach (var snapshot in snapshots) { - ReceivedSnapshots.Enqueue(snapshot); + ReceivedSnapshots.Enqueue(new (snapshot, sender)); } - Multiplayer.LogDebug(() => $"ReceiveSnapshots: {ReceivedSnapshots.Count}"); + Multiplayer.LogDebug(() => $"NetworkItemManager.ReceiveSnapshots() count: {ReceivedSnapshots.Count}, from: "); } #region Common @@ -109,7 +136,8 @@ private void ProcessReceived() { while (ReceivedSnapshots.Count > 0) { - ItemUpdateData snapshot = ReceivedSnapshots.Dequeue(); + var snapshotInfo = ReceivedSnapshots.Dequeue(); + ItemUpdateData snapshot = snapshotInfo.Item1; try { //Multiplayer.LogDebug(() => $"ProcessReceived: {snapshot.UpdateType}"); @@ -122,7 +150,7 @@ private void ProcessReceived() if (NetworkLifecycle.Instance.IsHost()) { - ProcessReceivedAsHost(snapshot); + ProcessReceivedAsHost(snapshot, snapshotInfo.Item2); } else { @@ -251,7 +279,7 @@ private void ProcessChanged(uint tick) DestroyedItems.Clear(); } - private void ProcessReceivedAsHost(ItemUpdateData snapshot) + private void ProcessReceivedAsHost(ItemUpdateData snapshot, ServerPlayer player) { if (snapshot.UpdateType == ItemUpdateData.ItemUpdateType.Create) { @@ -261,7 +289,7 @@ private void ProcessReceivedAsHost(ItemUpdateData snapshot) if (NetworkedItem.TryGet(snapshot.ItemNetId, out NetworkedItem netItem)) { - if (ValidatePlayerAction(snapshot)) //Ensure the player can do this + if (ValidatePlayerAction(snapshot, player)) //Ensure the player can do this { NetworkLifecycle.Instance.Server.LogWarning($"NetworkedItemManager.ProcessReceivedAsHost() ItemNetId: {snapshot.ItemNetId}, snapshot type: {snapshot.UpdateType}"); netItem.ReceiveSnapshot(snapshot); @@ -277,11 +305,51 @@ private void ProcessReceivedAsHost(ItemUpdateData snapshot) } } - private bool ValidatePlayerAction(ItemUpdateData snapshot) + private bool ValidatePlayerAction(ItemUpdateData snapshot, ServerPlayer player) { - return true; // Placeholder + return true; + // Must have valid item + if (!NetworkedItem.TryGet(snapshot.ItemNetId, out NetworkedItem networkedItem)) + return false; + + Multiplayer.LogDebug(() => $"ValidatePlayerAction() ItemId: {snapshot.ItemNetId}, name: {networkedItem.name} Update Type: {snapshot.UpdateType}, Item State: {snapshot.ItemState}, Player: {player.Username}"); + + switch (snapshot.ItemState) + { + case ItemState.InHand: + case ItemState.InInventory: + // Check if someone else owns it + GetItemOwner(snapshot.ItemNetId, out ServerPlayer currentOwner); + Multiplayer.LogDebug(() => $"ValidatePlayerAction() ItemId: {snapshot.ItemNetId}, name: {networkedItem.name} Update Type: {snapshot.UpdateType}, Item State: {snapshot.ItemState}, Player: {player?.Username}, Current Owner: {currentOwner?.Username}"); + + if (currentOwner != null && currentOwner != player) + return false; + + // Check pickup distance + float distance = Vector3.Distance(player.WorldPosition, networkedItem.transform.position); + if (distance > MAX_REACH_DISTANCE) + return false; + + Multiplayer.LogDebug(() => $"ValidatePlayerAction() ItemId: {snapshot.ItemNetId}, name: {networkedItem.name} Update Type: {snapshot.UpdateType}, Item State: {snapshot.ItemState}, Player: {player.Username}, Distance check: {distance}"); + break; + + case ItemState.Dropped: + case ItemState.Thrown: + case ItemState.Attached: //needs additional checks for distance to coupler + // Only owner can drop/throw + if (!player.OwnsItem(snapshot.ItemNetId)) + return false; + break; + } + + return true; } + private bool GetItemOwner(ushort itemNetId, out ServerPlayer owner) + { + owner = NetworkLifecycle.Instance.Server.ServerPlayers.FirstOrDefault(p => p.OwnsItem(itemNetId)); + return owner != null; + } #endregion #region Client @@ -430,8 +498,6 @@ private void SendToCache(NetworkedItem netItem) NetworkLifecycle.Instance.Client.LogDebug(() => $"Caching Spawned Item: {prefabName ?? ""}"); - netItem.BlockSync = true; - netItem.gameObject.SetActive(false); RespawnOnDrop respawn = netItem.Item.GetComponent(); diff --git a/Multiplayer/Multiplayer.csproj b/Multiplayer/Multiplayer.csproj index d80d6a9..8c4c166 100644 --- a/Multiplayer/Multiplayer.csproj +++ b/Multiplayer/Multiplayer.csproj @@ -3,7 +3,7 @@ net48 latest Multiplayer - 0.1.8.4 + 0.1.8.5 diff --git a/Multiplayer/Networking/Data/ItemUpdateData.cs b/Multiplayer/Networking/Data/ItemUpdateData.cs index 85bf29a..3c8e8c4 100644 --- a/Multiplayer/Networking/Data/ItemUpdateData.cs +++ b/Multiplayer/Networking/Data/ItemUpdateData.cs @@ -28,7 +28,7 @@ public enum ItemUpdateType : byte public Vector3 ItemPosition { get; set; } public Quaternion ItemRotation { get; set; } public Vector3 ThrowDirection { get; set; } - public ushort Player { get; set; } + public byte Player { get; set; } public ushort CarNetId { get; set; } public bool AttachedFront { get; set; } public Dictionary States { get; set; } @@ -112,7 +112,7 @@ public void Deserialize(NetDataReader reader) } else if (ItemState == ItemState.InInventory || ItemState == ItemState.InHand) { - Player = reader.GetUShort(); + Player = reader.GetByte(); } else if (ItemState == ItemState.Attached) { diff --git a/Multiplayer/Networking/Data/ServerPlayer.cs b/Multiplayer/Networking/Data/ServerPlayer.cs index d00541c..72c4d7e 100644 --- a/Multiplayer/Networking/Data/ServerPlayer.cs +++ b/Multiplayer/Networking/Data/ServerPlayer.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Multiplayer.Components.Networking; using Multiplayer.Components.Networking.Train; using Multiplayer.Components.Networking.World; using UnityEngine; @@ -19,11 +20,13 @@ public class ServerPlayer public Dictionary KnownItems { get; private set; } = new Dictionary(); //NetworkedItem, last updated tick public Dictionary NearbyItems { get; private set; } = new Dictionary(); //NetworkedItem, time since near the item + public HashSet OwnedItems { get; private set; } = new HashSet(); public StorageBase Storage { get; set; } = new StorageBase(); private Vector3 _lastWorldPos = Vector3.zero; private Vector3 _lastAbsoluteWorldPosition = Vector3.zero; + #region Positioning public Vector3 AbsoluteWorldPosition { get @@ -96,6 +99,47 @@ public Vector3 WorldPosition { public float WorldRotationY => CarId == 0 || !NetworkedTrainCar.Get(CarId, out NetworkedTrainCar car) ? RawRotationY : (Quaternion.Euler(0, RawRotationY, 0) * car.transform.rotation).eulerAngles.y; + #endregion + + #region Item Ownership + public bool OwnsItem(ushort itemNetId) => OwnedItems.Contains(itemNetId); + + public void AddOwnedItem(ushort itemNetId) + { + OwnedItems.Add(itemNetId); + NetworkLifecycle.Instance.Server.LogDebug(() => $"Player {Username} now owns item {itemNetId}"); + } + + public void AddOwnedItems(IEnumerable itemNetIds) + { + OwnedItems.UnionWith(itemNetIds); + NetworkLifecycle.Instance.Server.LogDebug(() => $"Player {Username} batch added items: {string.Join(", ", itemNetIds)}"); + } + + public void RemoveOwnedItem(ushort itemNetId) + { + if (OwnedItems.Remove(itemNetId)) + { + NetworkLifecycle.Instance.Server.LogDebug(() => $"Player {Username} no longer owns item {itemNetId}"); + } + } + + public void ClearOwnedItems() + { + OwnedItems.Clear(); + NetworkLifecycle.Instance.Server.LogDebug(() => $"Cleared all owned items for player {Username}"); + } + + public bool TryGetOwnedItem(ushort itemNetId, out NetworkedItem item) + { + if (OwnedItems.Contains(itemNetId) && NetworkedItem.TryGet(itemNetId, out item)) + { + return true; + } + item = null; + return false; + } + #endregion public override string ToString() { diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index 4ae7e57..8d5e97c 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -31,7 +31,6 @@ using System.Net; using Multiplayer.Networking.Packets.Serverbound.Train; using Multiplayer.Networking.Packets.Unconnected; -using DV.CabControls.Spec; namespace Multiplayer.Networking.Listeners; @@ -655,21 +654,6 @@ private void OnServerboundClientReadyPacket(ServerboundClientReadyPacket packet, } } - //Send Item Sync - - //List snapshots = new List(); - //foreach (var item in NetworkedItem.GetAll()) - //{ - // //only send items that are close to the player - // float sqDist = (serverPlayer.WorldPosition - item.transform.position).sqrMagnitude; - - // if (sqDist < 1000f ) - // snapshots.Add(item.CreateUpdateData(ItemUpdateData.ItemUpdateType.Create)); - //} - - //LogDebug(() => $"Sending sync ItemUpdateData {snapshots.Count} items"); - //SendNetSerializablePacket(peer, new CommonItemChangePacket { Items = snapshots }, DeliveryMethod.ReliableOrdered); - // Send existing players foreach (ServerPlayer player in ServerPlayers) { @@ -1003,7 +987,6 @@ private void OnUnconnectedPingPacket(UnconnectedPingPacket packet, IPEndPoint en private void OnCommonItemChangePacket(CommonItemChangePacket packet, NetPeer peer) { - LogDebug(()=>$"OnCommonItemChangePacket({packet?.Items?.Count}, {peer.Id})"); if(!TryGetServerPlayer(peer, out var player)) return; @@ -1036,7 +1019,6 @@ private void OnCommonItemChangePacket(CommonItemChangePacket packet, NetPeer pee return debug; } -); ); NetworkedItemManager.Instance.ReceiveSnapshots(packet.Items, player); diff --git a/Multiplayer/Patches/World/Items/RespawnOnDropPatch.cs b/Multiplayer/Patches/World/Items/RespawnOnDropPatch.cs new file mode 100644 index 0000000..737c6c1 --- /dev/null +++ b/Multiplayer/Patches/World/Items/RespawnOnDropPatch.cs @@ -0,0 +1,87 @@ +using HarmonyLib; +using System.Collections.Generic; +using System.Linq; +using System.Reflection.Emit; +using System.Text; +using UnityEngine; + +namespace Multiplayer.Patches.World.Items; + +[HarmonyPatch(typeof(RespawnOnDrop))] +[HarmonyPatch("RespawnOrDestroy")] +[HarmonyPatch(MethodType.Enumerator)] +class RespawnOnDropPatch +{ + static IEnumerable Transpiler(IEnumerable instructions) + { + var codes = new List(instructions); + + return codes; //disable pactch temporarily + + StringBuilder sb = new StringBuilder(); + sb.AppendLine("Pre-patch:"); + foreach (var code in codes) + { + sb.AppendLine(code.ToString()); + } + + Debug.Log(sb.ToString()); + + // Find base.gameObject.SetActive(false) + // ldloc.1 NULL[Label10] //this is the 'base' loading to the stack + // call UnityEngine.GameObject UnityEngine.Component::get_gameObject() //call to retrieve the gameObject + // ldc.i4.0 NULL //load a 'false' onto the stack + // callvirt System.Void UnityEngine.GameObject::SetActive(System.Boolean value) //call to SetActive() + + int startIndex = -1; + for (int i = 0; i < codes.Count - 1; i++) + { + if (codes[i].opcode == OpCodes.Ldloc_1 && + codes[i + 1].Calls(AccessTools.Method(typeof(Component), "get_gameObject")) && + codes[i + 2].opcode == OpCodes.Ldc_I4_0 && + codes[i + 3].Calls(AccessTools.Method(typeof(GameObject), "SetActive"))) + { + startIndex = i; + break; + } + } + + if (startIndex < 0) + { + Multiplayer.LogError(() => $"RespawnOnDrop.RespawnOrDestroy() transpiler failed - start index not found!"); + return codes.AsEnumerable(); + } + + // Find SingletonBehaviour.Instance.AddItemToLostAndFound(this.item); + int endIndex = codes.FindIndex(startIndex, x => + x.Calls(AccessTools.Method(typeof(StorageController), "AddItemToLostAndFound"))); + + + if (endIndex < 0) + { + Multiplayer.LogError(() => $"RespawnOnDrop.RespawnOrDestroy() transpiler failed - end index not found!"); + return codes.AsEnumerable(); + } + + + // replace 'else' branch with NOPs rather than trying to patch labels + for (int i = startIndex; i <= endIndex; i++) + { + var newNop = new CodeInstruction(OpCodes.Nop); + newNop.labels.AddRange(codes[i].labels); // Maintain any labels on the original instruction + codes[i] = newNop; + } + + sb = new StringBuilder(); + sb.AppendLine("Post-patch:"); + foreach (var code in codes) + { + sb.AppendLine(code.ToString()); + } + + Debug.Log(sb.ToString()); + + return codes.AsEnumerable(); + } +} + diff --git a/Multiplayer/Patches/World/StorageControllerPatch.cs b/Multiplayer/Patches/World/StorageControllerPatch.cs new file mode 100644 index 0000000..f0d5fa9 --- /dev/null +++ b/Multiplayer/Patches/World/StorageControllerPatch.cs @@ -0,0 +1,82 @@ +using DV.CabControls; +using HarmonyLib; +using Multiplayer.Components.Networking.World; +using Multiplayer.Utils; +using System; +using UnityEngine; + +namespace Multiplayer.Patches.World.Items; + +[HarmonyPatch(typeof(StorageController))] +public static class StorageControllerPatch +{ + [HarmonyPatch(nameof(StorageController.AddItemToLostAndFound))] + [HarmonyPrefix] + static void AddItemToLostAndFound(StorageController __instance, ItemBase item) + { + + Multiplayer.LogDebug(() => + { + NetworkedItem.TryGetNetworkedItem(item, out NetworkedItem netItem); + return $"StorageController.AddItemToLostAndFound({item.name}) netId: {netItem?.NetId}\r\n{new System.Diagnostics.StackTrace()}"; + }); + } + + [HarmonyPatch(nameof(StorageController.RemoveItemFromLostAndFound))] + [HarmonyPrefix] + static void RemoveItemFromLostAndFound(StorageController __instance, ItemBase item) + { + + Multiplayer.LogDebug(() => + { + NetworkedItem.TryGetNetworkedItem(item, out NetworkedItem netItem); + return $"StorageController.RemoveItemFromLostAndFound({item.name}) netId: {netItem?.NetId}\r\n{new System.Diagnostics.StackTrace()}"; + }); + } + + [HarmonyPatch(nameof(StorageController.RequestLostAndFoundItemActivation))] + [HarmonyPrefix] + static void RequestLostAndFoundItemActivation(StorageController __instance) + { + + Multiplayer.LogDebug(() => + { + return $"StorageController.RequestLostAndFoundItemActivation()\r\n{new System.Diagnostics.StackTrace()}"; + }); + } + + [HarmonyPatch(nameof(StorageController.MoveItemsFromWorldToLostAndFound))] + [HarmonyPrefix] + static void MoveItemsFromWorldToLostAndFound(StorageController __instance, bool ignoreItemsWithRespawnParents) + { + + Multiplayer.LogDebug(() => + { + return $"StorageController.MoveItemsFromWorldToLostAndFound({ignoreItemsWithRespawnParents})\r\n{new System.Diagnostics.StackTrace()}"; + }); + } + + [HarmonyPatch(nameof(StorageController.ForceSummonAllWorldItemsToLostAndFound))] + [HarmonyPrefix] + static void ForceSummonAllWorldItemsToLostAndFound(StorageController __instance) + { + + Multiplayer.LogDebug(() => + { + return $"StorageController.ForceSummonAllWorldItemsToLostAndFound()\r\n{new System.Diagnostics.StackTrace()}"; + }); + } + + [HarmonyPatch(nameof(StorageController.RequestItemActivation))] + [HarmonyPrefix] + static void RequestItemActivation(StorageController __instance) + { + + Multiplayer.LogDebug(() => + { + return $"StorageController.RequestItemActivation()\r\n{new System.Diagnostics.StackTrace()}"; + }); + } + + +} diff --git a/info.json b/info.json index 9539e68..f72e9b9 100644 --- a/info.json +++ b/info.json @@ -1,6 +1,6 @@ { "Id": "Multiplayer", - "Version": "0.1.8.4", + "Version": "0.1.8.5", "DisplayName": "Multiplayer", "Author": "Insprill, Macka, Morm", "EntryMethod": "Multiplayer.Multiplayer.Load", From 7f38fde3f548b6a09f249bd34762bcaa702e673c Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 24 Nov 2024 17:31:20 +1000 Subject: [PATCH 118/188] Localisation updates --- Multiplayer/Locale.cs | 13 ++++++++ locale.csv | 74 +++++++++++++++++++++++++++++-------------- 2 files changed, 63 insertions(+), 24 deletions(-) diff --git a/Multiplayer/Locale.cs b/Multiplayer/Locale.cs index 75f693a..6869652 100644 --- a/Multiplayer/Locale.cs +++ b/Multiplayer/Locale.cs @@ -21,6 +21,8 @@ public static class Locale 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_CHAT_INFO = $"{PREFIX}chat"; + private const string PREFIX_PAUSE_MENU = $"{PREFIX}pm"; #region Main Menu public static string MAIN_MENU__JOIN_SERVER => Get(MAIN_MENU__JOIN_SERVER_KEY); @@ -126,6 +128,17 @@ public static class Locale private const string LOADING_INFO__SYNC_WORLD_STATE_KEY = $"{PREFIX_LOADING_INFO}/sync_world_state"; #endregion + #region Chat + #endregion + + #region Pause Menu + public static string PAUSE_MENU_DISCONNECT => Get(PAUSE_MENU_DISCONNECT_KEY); + public const string PAUSE_MENU_DISCONNECT_KEY = $"{PREFIX_PAUSE_MENU}/disconnect_msg"; + + public static string PAUSE_MENU_QUIT => Get(PAUSE_MENU_QUIT_KEY); + public const string PAUSE_MENU_QUIT_KEY = $"{PREFIX_PAUSE_MENU}/quit_msg"; + #endregion + private static bool initializeAttempted; private static ReadOnlyDictionary> csv; diff --git a/locale.csv b/locale.csv index 05fdbf5..0f13141 100644 --- a/locale.csv +++ b/locale.csv @@ -6,64 +6,77 @@ Key,Description,English,Bulgarian,Chinese (Simplified),Chinese (Traditional),Cze ,,,,,,,,,,,,,,,,,,,,,,,,,,,, ,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,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/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,,,,,,,,,,,,Felhasználatlan,,,,,,,,,,,,,, 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/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,,,,,,,,,,,,Felhasználatlan,,,,,,,,,,,,,, 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,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,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/players,Player count in details text,Players,Играчите,玩家,玩家,Hráči,Spillere,Spelers,Pelaajat,Joueurs,Spieler,खिलाड़ी,Játékosok,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,पासवर्ड,Jelszó,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,Ні -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!,Сервери не знайдено. Оновіть або почніть власний! +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!,Сервери не знайдено. Оновіть або почніть власний! +sb/info/title,Title for server browser info,Server Browser Info,,服务器浏览器介绍,,,,,,,,,,,,,,,,,,,,,,, +sb/info/content,Content for server browser info,"Welcome to Derail Valley Multiplayer Mod!\n\nThe server list refreshes automatically every {0} seconds, but you can refresh manually once every {1} seconds.",,欢迎来到脱轨山谷的联机模式!\n\n服务器列表会在每{0}秒刷新,但是你可以手动让它在每{1}秒刷新,,,,,,,,,,,,,,,,,,,,,,, +sb/connecting,Connecting dialogue,"Connecting, please wait...\nAttempt: {0}",,正在连接中,请稍候片刻\n尝试次数: {0},,,,,,,,,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,, ,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/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,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/public,Public checkbox label,Public Game,Публична игра,公共游戏,公開遊戲,Veřejná hra,Offentligt spil,Openbaar spel,Julkinen peli,Jeu public,Öffentliches Spiel,,Nyilvános Játék,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/public__tooltip_disabled,Unused,,,,,,,,,,,,,,,,,,,,,,,,,, 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/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/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/max_players__tooltip_disabled,Unused,,,,,,,,,,,,,,,,,,,,,,,,,, +host/start,Maximum players slider label,Start,Започнете,开始,開始,Start,Start,Begin,alkaa,Commencer,Start,शुरू,Indít!,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.,सर्वर प्रारंभ करें.,Szerver Indul!,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/instructions/first,Instructions for the host 1,"First time hosts, please see the {0}Hosting{1} section of our Wiki.",,"第一次主持游戏的话, 请看我们wiki的{0}Hosting{1} 模块",,,,,,,,,,,,,,,,,,,,,,, +host/instructions/mod_warning,Instructions for the host 2,Using other mods may cause unexpected behaviour including de-syncs. See {0}Mod Compatibility{1} for more info.,,同时使用其他模组可能会导致游戏出错,比如物品不同步, 看 {0}Mod Compatibility{1} 模块来获取更多信息,,,,,,,,,,,,,,,,,,,,,,, +host/instructions/recommend,Instructions for the host 3,It is recommended that other mods are disabled and Derail Valley restarted prior to playing in multiplayer.,,推荐你卸载其他模组并重启游戏后,再进行联机,,,,,,,,,,,,,,,,,,,,,,, +host/instructions/signoff,Instructions for the host 4,We hope to have your favourite mods compatible with multiplayer in the future.,,我们希望未来能让你装联机模组的同时也能玩其他模组,,,,,,,,,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,, ,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! - -",Nepotrivire mod!,Несоответствие модов!,Nezhoda modov!,"Falta el cliente, o tiene modificaciones adicionales.",Mod-felmatchning!,Mod uyumsuzluğu!,Невідповідність модів! +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/disconnect/unreachable,Host Unreachable error message,Host Unreachable,,无法找到房主,,,,,,,,,,,,,,,,,,,,,,, +dr/disconnect/unknown,Unknown Host error message,Unknown Host,,房主未知,,,,,,,,,,,,,,,,,,,,,,, +dr/disconnect/kicked,Player Kicked error message,Player Kicked,,玩家已被踢出,,,,,,,,,,,,,,,,,,,,,,, +dr/disconnect/rejected,Rejected! error message,Rejected!,,你已被拒绝加入服务器!,,,,,,,,,,,,,,,,,,,,,,, +dr/disconnect/shutdown,Server Shutting Down error message,Server Shutting Down,,服务器已经关闭,,,,,,,,,,,,,,,,,,,,,,, +dr/disconnect/timeout,Server Timed out,Server Timed out,,服务器连接超时,,,,,,,,,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,, ,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!,Тільки господар може керувати оплатою! @@ -74,3 +87,16 @@ plist/title,The title of the player list.,Online Players,Онлайн играч ,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,Синхронізація стану світу +,,,,,,,,,,,,,,,,,,,,,,,,,,, +,Chat,,,,,,,,,,,,,,,,,,,,,,,,,, +chat/placeholder,Chat input placeholder,Type a message and press Enter!,,在此输入文字,按回车发送,,,,,,,,,,,,,,,,,,,,,,, +chat/help/available,Chat help info available commands,Available commands:,,可用命令:,,,,,,,,,,,,,,,,,,,,,,, +chat/help/servermsg,Chat help send message as server,Send a message as the server (host only),,以服务器的身份发消息(仅限房主),,,,,,,,,,,,,,,,,,,,,,, +chat/help/whispermsg,Chat help whisper to a player,Whisper to a player,,向一位玩家说悄悄话,,,,,,,,,,,,,,,,,,,,,,, +chat/help/help,Chat help show help,Display this help message,,展示此帮助信息,,,,,,,,,,,,,,,,,,,,,,, +chat/help/msg,Chat help parameter e.g. /s ,message,,信息,,,,,,,,,,,,,,,,,,,,,,, +chat/help/playername,Chat help parameter e.g. /w ,player name,,玩家名字,,,,,,,,,,,,,,,,,,,,,,, +,,,,,,,,,,,,,,,,,,,,,,,,,,, +,Pause Menu,,,,,,,,,,,,,,,,,,,,,,,,,, +pm/disconnect_msg,Message when disconnecting from server (back to main menu),Disconnect and return to main menu?,,确定要断开连接并退回到主界面吗?,,,,,,,,,,,,,,,,,,,,,,, +pm/quit_msg,Message when disconnecting from server (quit game),Disconnect and quit?,,确定要断开连接并直接退出吗?,,,,,,,,,,,,,,,,,,,,,,, From b82d4777afce994fea7b5445f2e27c1d7fcc5dff Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 24 Nov 2024 18:19:36 +1000 Subject: [PATCH 119/188] Compatibility with B99 --- .../Components/MainMenu/HostGamePane.cs | 5 +- .../Networking/Player/NetworkedWorldMap.cs | 21 ++-- .../Networking/Train/NetworkedTrainCar.cs | 28 ++++- .../Networking/World/NetworkedItem.cs | 8 +- .../Networking/World/NetworkedItemManager.cs | 6 +- .../Networking/World/NetworkedJunction.cs | 7 +- .../SaveGame/StartGameData_ServerSave.cs | 2 + Multiplayer/Multiplayer.csproj | 4 +- .../Networking/Data/TaskNetworkData.cs | 1 - .../Managers/Client/NetworkClient.cs | 10 +- .../Managers/Server/NetworkServer.cs | 6 +- .../Common/Train/CommonTrainCouplePacket.cs | 2 + .../Common/Train/CommonTrainUncouplePacket.cs | 2 + .../PauseMenu/PauseMenuControllerPatch.cs | 118 ++++++++++++++++++ .../CustomFirstPersonControllerPatch.cs | 2 +- .../Player/MapMarkersControllerPatch.cs | 13 ++ Multiplayer/Patches/Player/WorldMapPatch.cs | 13 -- Multiplayer/Patches/Train/HoseAndCockPatch.cs | 6 +- Multiplayer/Utils/DvExtensions.cs | 9 ++ 19 files changed, 213 insertions(+), 50 deletions(-) create mode 100644 Multiplayer/Patches/PauseMenu/PauseMenuControllerPatch.cs create mode 100644 Multiplayer/Patches/Player/MapMarkersControllerPatch.cs delete mode 100644 Multiplayer/Patches/Player/WorldMapPatch.cs diff --git a/Multiplayer/Components/MainMenu/HostGamePane.cs b/Multiplayer/Components/MainMenu/HostGamePane.cs index e097302..27a179a 100644 --- a/Multiplayer/Components/MainMenu/HostGamePane.cs +++ b/Multiplayer/Components/MainMenu/HostGamePane.cs @@ -35,7 +35,7 @@ public class HostGamePane : MonoBehaviour TMP_InputField details; TextMeshProUGUI serverDetails; - Slider maxPlayers; + SliderDV maxPlayers; Toggle gamePublic; @@ -116,7 +116,7 @@ private void BuildUI() return; } - GameObject sliderPrefab = goMMC.FindChildByName("SliderLimitSession"); + GameObject sliderPrefab = goMMC.FindChildByName("Field Of View").gameObject; if (sliderPrefab == null) { Multiplayer.LogError("SliderLimitSession not found!"); @@ -253,6 +253,7 @@ private void BuildUI() go.ResetTooltip(); go.FindChildByName("[text label]").GetComponent().UpdateLocalization(); maxPlayers = go.GetComponent(); + maxPlayers.stepIncrement = 1; maxPlayers.minValue = MIN_PLAYERS; maxPlayers.maxValue = MAX_PLAYERS; maxPlayers.value = Mathf.Clamp(Multiplayer.Settings.MaxPlayers,MIN_PLAYERS,MAX_PLAYERS); diff --git a/Multiplayer/Components/Networking/Player/NetworkedWorldMap.cs b/Multiplayer/Components/Networking/Player/NetworkedWorldMap.cs index 3ff765e..144503a 100644 --- a/Multiplayer/Components/Networking/Player/NetworkedWorldMap.cs +++ b/Multiplayer/Components/Networking/Player/NetworkedWorldMap.cs @@ -1,21 +1,20 @@ +using DV; using System.Collections.Generic; using TMPro; using UnityEngine; namespace Multiplayer.Components.Networking.Player; -public class NetworkedWorldMap : MonoBehaviour +public class NetworkedMapMarkersController : MonoBehaviour { - private WorldMap worldMap; private MapMarkersController markersController; private GameObject textPrefab; private readonly Dictionary playerIndicators = new(); private void Awake() { - worldMap = GetComponent(); markersController = GetComponent(); - textPrefab = worldMap.GetComponentInChildren().gameObject; + textPrefab = markersController.GetComponentInChildren().gameObject; foreach (NetworkedPlayer networkedPlayer in NetworkLifecycle.Instance.Client.ClientPlayerManager.Players) OnPlayerConnected(networkedPlayer.Id, networkedPlayer); NetworkLifecycle.Instance.Client.ClientPlayerManager.OnPlayerConnected += OnPlayerConnected; @@ -36,16 +35,16 @@ private void OnDestroy() private void OnPlayerConnected(byte id, NetworkedPlayer player) { - Transform root = new GameObject($"{player.Username}'s Indicator") { + Transform root = new GameObject($"MapMarkerPlayer({player.Username})") { transform = { - parent = worldMap.playerIndicator.parent, + parent = this.transform, localPosition = Vector3.zero, localEulerAngles = Vector3.zero } }.transform; WorldMapIndicatorRefs refs = root.gameObject.AddComponent(); - GameObject indicator = Instantiate(worldMap.playerIndicator.gameObject, root); + GameObject indicator = Instantiate(markersController.playerMarkerPrefab.gameObject, root); indicator.transform.localPosition = Vector3.zero; refs.indicator = indicator.transform; @@ -54,6 +53,8 @@ private void OnPlayerConnected(byte id, NetworkedPlayer player) textGo.transform.localEulerAngles = new Vector3(90f, 0, 0); refs.text = textGo.GetComponent(); TMP_Text text = textGo.GetComponent(); + + text.name = "Player Name"; text.text = player.Username; text.alignment = TextAlignmentOptions.Center; text.fontSize /= 1.25f; @@ -74,7 +75,7 @@ private void OnPlayerDisconnected(byte id, NetworkedPlayer player) private void OnTick(uint obj) { - if (!worldMap.initialized || UnloadWatcher.isUnloading) + if (markersController == null || UnloadWatcher.isUnloading) return; UpdatePlayers(); } @@ -92,7 +93,7 @@ public void UpdatePlayers() WorldMapIndicatorRefs refs = kvp.Value; - bool active = worldMap.gameParams.PlayerMarkerDisplayed; + bool active = Globals.G.gameParams.PlayerMarkerDisplayed; if (refs.gameObject.activeSelf != active) refs.gameObject.SetActive(active); if (!active) @@ -104,7 +105,7 @@ public void UpdatePlayers() if (normalized != Vector3.zero) refs.indicator.localRotation = Quaternion.LookRotation(normalized); - Vector3 position = markersController.GetMapPosition(playerTransform.position - WorldMover.currentMove, worldMap.triggerExtentsXZ); + Vector3 position = markersController.GetMapPosition(playerTransform.position - WorldMover.currentMove, true); refs.indicator.localPosition = position; refs.text.localPosition = position with { y = position.y + 0.025f }; } diff --git a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs index b94b9e7..247b3a7 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs @@ -42,6 +42,10 @@ public static Coupler GetCoupler(HoseAndCock hoseAndCock) { return hoseToCoupler[hoseAndCock]; } + public static bool TryGetCoupler(HoseAndCock hoseAndCock, out Coupler coupler) + { + return hoseToCoupler.TryGetValue(hoseAndCock, out coupler); + } public static bool GetFromTrainId(string carId, out NetworkedTrainCar networkedTrainCar) { @@ -184,6 +188,7 @@ private void OnDisable() { if (UnloadWatcher.isQuitting) return; + NetworkLifecycle.Instance.OnTick -= Common_OnTick; NetworkLifecycle.Instance.OnTick -= Server_OnTick; if (UnloadWatcher.isUnloading) @@ -205,16 +210,21 @@ private void OnDisable() firebox.fireboxIgnitionPort.ValueUpdatedInternally -= Client_OnIgnite; //Player igniting firebox } - brakeSystem.HandbrakePositionChanged -= Common_OnHandbrakePositionChanged; - brakeSystem.BrakeCylinderReleased -= Common_OnBrakeCylinderReleased; + if (brakeSystem != null) + { + brakeSystem.HandbrakePositionChanged -= Common_OnHandbrakePositionChanged; + brakeSystem.BrakeCylinderReleased -= Common_OnBrakeCylinderReleased; + } if (NetworkLifecycle.Instance.IsHost()) { bogie1.TrackChanged -= Server_BogieTrackChanged; bogie2.TrackChanged -= Server_BogieTrackChanged; + TrainCar.CarDamage.CarEffectiveHealthStateUpdate -= Server_CarHealthUpdate; - brakeSystem.MainResPressureChanged -= Server_MainResUpdate; + if(brakeSystem != null) + brakeSystem.MainResPressureChanged -= Server_MainResUpdate; if (firebox != null) { @@ -374,7 +384,7 @@ private void Server_SendBrakePressures() if (!mainResPressureDirty) return; mainResPressureDirty = false; - NetworkLifecycle.Instance.Server.SendBrakePressures(NetId, brakeSystem.mainReservoirPressure, brakeSystem.independentPipePressure, brakeSystem.brakePipePressure, brakeSystem.brakeCylinderPressure); + //B99 review need / mod NetworkLifecycle.Instance.Server.SendBrakePressures(NetId, brakeSystem.mainReservoirPressure, brakeSystem.independentPipePressure, brakeSystem.brakePipePressure, brakeSystem.brakeCylinderPressure); } private void Server_SendFireBoxState() @@ -392,6 +402,12 @@ private void Server_SendCouplers() return; sendCouplers = false; + if(TrainCar.frontCoupler.IsCoupled()) + NetworkLifecycle.Instance.Client.SendTrainCouple(TrainCar.frontCoupler,TrainCar.frontCoupler.coupledTo,false, false); + + if(TrainCar.rearCoupler.IsCoupled()) + NetworkLifecycle.Instance.Client.SendTrainCouple(TrainCar.rearCoupler,TrainCar.rearCoupler.coupledTo,false, false); + if (TrainCar.frontCoupler.hoseAndCock.IsHoseConnected) NetworkLifecycle.Instance.Client.SendHoseConnected(TrainCar.frontCoupler, TrainCar.frontCoupler.coupledTo, false); @@ -680,8 +696,8 @@ public void Client_ReceiveBrakePressureUpdate(float mainReservoirPressure, float if (!hasSimFlow) return; - brakeSystem.ForceIndependentPipePressure(independentPipePressure); - brakeSystem.ForceTargetIndBrakeCylinderPressure(brakeCylinderPressure); + //B99 review need / mod brakeSystem.ForceIndependentPipePressure(independentPipePressure); + //B99 review need / mod brakeSystem.ForceTargetIndBrakeCylinderPressure(brakeCylinderPressure); brakeSystem.SetMainReservoirPressure(mainReservoirPressure); brakeSystem.brakePipePressure = brakePipePressure; diff --git a/Multiplayer/Components/Networking/World/NetworkedItem.cs b/Multiplayer/Components/Networking/World/NetworkedItem.cs index 7676dd4..36b372f 100644 --- a/Multiplayer/Components/Networking/World/NetworkedItem.cs +++ b/Multiplayer/Components/Networking/World/NetworkedItem.cs @@ -64,7 +64,7 @@ public static bool TryGetNetworkedItem(ItemBase item, out NetworkedItem networke public ItemBase Item { get; private set; } private GrabHandlerItem grabHandler; - private SnappableOnCoupler snappableOnCoupler; + private SnappableItem snappableItem; private Component trackedItem; private List trackedValues = new List(); public bool UsefulItem { get; private set; } = false; @@ -169,7 +169,7 @@ private bool Register() //Find special interaction components TryGetComponent(out grabHandler); - TryGetComponent(out snappableOnCoupler); + TryGetComponent(out snappableItem); lastState = GetItemState(); stateDirty = false; @@ -423,7 +423,7 @@ public ItemUpdateData CreateUpdateData(ItemUpdateData.ItemUpdateType updateType) if(lastState == ItemState.Attached) { - ItemSnapPointCoupler itemSnapPointCoupler = snappableOnCoupler.SnappedTo as ItemSnapPointCoupler; + ItemSnapPointCoupler itemSnapPointCoupler = snappableItem.SnappedTo as ItemSnapPointCoupler; if (itemSnapPointCoupler != null) { @@ -472,7 +472,7 @@ private ItemState GetItemState() if (Inventory.Instance.Contains(this.gameObject, false)) return ItemState.InInventory; - if(snappableOnCoupler != null && snappableOnCoupler.IsSnapped) + if(snappableItem != null && snappableItem.IsSnapped) { Multiplayer.LogDebug(() => $"GetItemState() NetId: {NetId}, {name}, snapped! {this.transform.parent}"); return ItemState.Attached; diff --git a/Multiplayer/Components/Networking/World/NetworkedItemManager.cs b/Multiplayer/Components/Networking/World/NetworkedItemManager.cs index 4086fe9..c2938e5 100644 --- a/Multiplayer/Components/Networking/World/NetworkedItemManager.cs +++ b/Multiplayer/Components/Networking/World/NetworkedItemManager.cs @@ -55,11 +55,11 @@ protected override void Awake() if (!NetworkLifecycle.Instance.IsHost()) return; - NetworkLifecycle.Instance.Server.PlayerDisconnect += PlayerDisconnected; + //B99 temporary patch NetworkLifecycle.Instance.Server.PlayerDisconnect += PlayerDisconnected; try { - MAX_REACH_DISTANCE = GrabberRaycasterDV.SPHERE_CAST_MAX_DIST + REACH_DISTANCE_BUFFER; + MAX_REACH_DISTANCE = GrabberRaycasterDV.RAYCAST_MAX_DIST + REACH_DISTANCE_BUFFER; } catch (Exception ex) { @@ -452,6 +452,8 @@ private void BuildPrefabLookup() } public void CacheWorldItems() { + //B99 temporary patch + return; if (NetworkLifecycle.Instance.IsHost()) return; diff --git a/Multiplayer/Components/Networking/World/NetworkedJunction.cs b/Multiplayer/Components/Networking/World/NetworkedJunction.cs index c4b965f..cafbf66 100644 --- a/Multiplayer/Components/Networking/World/NetworkedJunction.cs +++ b/Multiplayer/Components/Networking/World/NetworkedJunction.cs @@ -28,8 +28,11 @@ private void Junction_Switched(Junction.SwitchMode switchMode, int branch) public void Switch(byte mode, byte selectedBranch) { - Junction.selectedBranch = selectedBranch - 1; // Junction#Switch increments this before processing - Junction.Switch((Junction.SwitchMode)mode); + //Junction.selectedBranch = (byte)(selectedBranch - 1); // Junction#Switch increments this before processing + //Junction.Switch((Junction.SwitchMode)mode); + + //B99 + Junction.Switch((Junction.SwitchMode)mode, selectedBranch); } public static bool Get(ushort netId, out NetworkedJunction obj) diff --git a/Multiplayer/Components/SaveGame/StartGameData_ServerSave.cs b/Multiplayer/Components/SaveGame/StartGameData_ServerSave.cs index 9ba5514..326e564 100644 --- a/Multiplayer/Components/SaveGame/StartGameData_ServerSave.cs +++ b/Multiplayer/Components/SaveGame/StartGameData_ServerSave.cs @@ -21,6 +21,8 @@ public class StartGameData_ServerSave : AStartGameData private ClientboundSaveGameDataPacket packet; + public override bool IsStartingNewSession => false; + public void SetFromPacket(ClientboundSaveGameDataPacket packet) { this.packet = packet.Clone(); diff --git a/Multiplayer/Multiplayer.csproj b/Multiplayer/Multiplayer.csproj index 8c4c166..6536c0b 100644 --- a/Multiplayer/Multiplayer.csproj +++ b/Multiplayer/Multiplayer.csproj @@ -3,7 +3,7 @@ net48 latest Multiplayer - 0.1.8.5 + 0.1.9.0 @@ -12,6 +12,7 @@ + @@ -65,6 +66,7 @@ ../build/UnityChan.dll + diff --git a/Multiplayer/Networking/Data/TaskNetworkData.cs b/Multiplayer/Networking/Data/TaskNetworkData.cs index eaa760f..5d1edaa 100644 --- a/Multiplayer/Networking/Data/TaskNetworkData.cs +++ b/Multiplayer/Networking/Data/TaskNetworkData.cs @@ -76,7 +76,6 @@ public static void RegisterTaskType(TaskType taskType, Func$"TaskNetworkDataFactory.ConvertTask: Processing task of type {task.GetType()}"); //Multiplayer.LogDebug(()=>$"TaskNetworkDataFactory.ConvertTask: Processing task of type {task.GetType()}"); if (TypeToTaskNetworkData.TryGetValue(task.GetType(), out var converter)) { diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 5e22967..84aa28e 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -516,7 +516,7 @@ private void OnCommonTrainCouplePacket(CommonTrainCouplePacket packet) Coupler coupler = packet.IsFrontCoupler ? trainCar.frontCoupler : trainCar.rearCoupler; Coupler otherCoupler = packet.OtherCarIsFrontCoupler ? otherTrainCar.frontCoupler : otherTrainCar.rearCoupler; - coupler.CoupleTo(otherCoupler, packet.PlayAudio, packet.ViaChainInteraction); + coupler.CoupleTo(otherCoupler, packet.PlayAudio, false/*B99 packet.ViaChainInteraction*/); } private void OnCommonTrainUncouplePacket(CommonTrainUncouplePacket packet) @@ -526,7 +526,7 @@ private void OnCommonTrainUncouplePacket(CommonTrainUncouplePacket packet) Coupler coupler = packet.IsFrontCoupler ? trainCar.frontCoupler : trainCar.rearCoupler; - coupler.Uncouple(packet.PlayAudio, false, packet.DueToBrokenCouple, packet.ViaChainInteraction); + coupler.Uncouple(packet.PlayAudio, false, packet.DueToBrokenCouple, false/*B99 packet.ViaChainInteraction*/); } private void OnCommonHoseConnectedPacket(CommonHoseConnectedPacket packet) @@ -723,7 +723,7 @@ private void OnClientboundLicenseAcquiredPacket(ClientboundLicenseAcquiredPacket LicenseManager.Instance.AcquireGeneralLicense(Globals.G.Types.generalLicenses.Find(l => l.id == packet.Id)); foreach (CareerManagerLicensesScreen screen in Object.FindObjectsOfType()) - screen.PopulateLicensesTextsFromIndex(screen.indexOfFirstDisplayedLicense); + screen.PopulateTextsFromIndex(screen.IndexOfFirstDisplayedEntry); //B99 } private void OnClientboundGarageUnlockPacket(ClientboundGarageUnlockPacket packet) @@ -820,7 +820,7 @@ private void OnCommonItemChangePacket(CommonItemChangePacket packet) return debug; }); - NetworkedItemManager.Instance.ReceiveSnapshots(packet.Items, null); + //NetworkedItemManager.Instance.ReceiveSnapshots(packet.Items, null); } #endregion @@ -904,7 +904,9 @@ public void SendTrainCouple(Coupler coupler, Coupler otherCoupler, bool playAudi { NetId = couplerNetId, //coupler.train.GetNetId(), IsFrontCoupler = coupler.isFrontCoupler, + State = (byte)coupler.state, OtherNetId = otherCouplerNetId, //otherCoupler.train.GetNetId(), + OtherState = (byte)otherCoupler.state, OtherCarIsFrontCoupler = otherCoupler.isFrontCoupler, PlayAudio = playAudio, ViaChainInteraction = viaChainInteraction diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index 8d5e97c..6511723 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -977,11 +977,11 @@ private void OnCommonChatPacket(CommonChatPacket packet, NetPeer peer) ChatManager.ProcessMessage(packet.message,peer); } #endregion - + #region Unconnected Packet Handling private void OnUnconnectedPingPacket(UnconnectedPingPacket packet, IPEndPoint endPoint) { - Multiplayer.Log($"OnUnconnectedPingPacket({endPoint.Address})"); + //Multiplayer.Log($"OnUnconnectedPingPacket({endPoint.Address})"); SendUnconnectedPacket(packet, endPoint.Address.ToString(),endPoint.Port); } @@ -1021,7 +1021,7 @@ private void OnCommonItemChangePacket(CommonItemChangePacket packet, NetPeer pee ); - NetworkedItemManager.Instance.ReceiveSnapshots(packet.Items, player); + //NetworkedItemManager.Instance.ReceiveSnapshots(packet.Items, player); } #endregion } diff --git a/Multiplayer/Networking/Packets/Common/Train/CommonTrainCouplePacket.cs b/Multiplayer/Networking/Packets/Common/Train/CommonTrainCouplePacket.cs index 3600589..ce04a4c 100644 --- a/Multiplayer/Networking/Packets/Common/Train/CommonTrainCouplePacket.cs +++ b/Multiplayer/Networking/Packets/Common/Train/CommonTrainCouplePacket.cs @@ -4,7 +4,9 @@ public class CommonTrainCouplePacket { public ushort NetId { get; set; } public bool IsFrontCoupler { get; set; } + public byte State { get; set; } public ushort OtherNetId { get; set; } + public byte OtherState { get; set; } public bool OtherCarIsFrontCoupler { get; set; } public bool PlayAudio { get; set; } public bool ViaChainInteraction { get; set; } diff --git a/Multiplayer/Networking/Packets/Common/Train/CommonTrainUncouplePacket.cs b/Multiplayer/Networking/Packets/Common/Train/CommonTrainUncouplePacket.cs index 590c7b0..de54339 100644 --- a/Multiplayer/Networking/Packets/Common/Train/CommonTrainUncouplePacket.cs +++ b/Multiplayer/Networking/Packets/Common/Train/CommonTrainUncouplePacket.cs @@ -4,6 +4,8 @@ public class CommonTrainUncouplePacket { public ushort NetId { get; set; } public bool IsFrontCoupler { get; set; } + public byte State { get; set; } + public byte OtherState { get; set; } public bool PlayAudio { get; set; } public bool ViaChainInteraction { get; set; } public bool DueToBrokenCouple { get; set; } diff --git a/Multiplayer/Patches/PauseMenu/PauseMenuControllerPatch.cs b/Multiplayer/Patches/PauseMenu/PauseMenuControllerPatch.cs new file mode 100644 index 0000000..3552aff --- /dev/null +++ b/Multiplayer/Patches/PauseMenu/PauseMenuControllerPatch.cs @@ -0,0 +1,118 @@ +using DV.Localization; +using DV.UI; +using DV.UIFramework; +using HarmonyLib; +using Multiplayer.Components.MainMenu; +using Multiplayer.Components.Networking; +using Multiplayer.Utils; +using System; +using System.Reflection; +using UnityEngine; +using UnityEngine.UI; + +namespace Multiplayer.Patches.PauseMenu; + + + +[HarmonyPatch(typeof(PauseMenuController))] +public static class PauseMenuController_Patch +{ + private static readonly PopupLocalizationKeys popupQuitLocalizationKeys = new PopupLocalizationKeys + { + positiveKey = "yes", + negativeKey = "no", + labelKey = Locale.PAUSE_MENU_QUIT_KEY + }; + private static readonly PopupLocalizationKeys popupDisconnectLocalizationKeys = new PopupLocalizationKeys + { + positiveKey = "yes", + negativeKey = "no", + labelKey = Locale.PAUSE_MENU_DISCONNECT_KEY + }; + + + [HarmonyPatch(nameof(PauseMenuController.Start))] + [HarmonyPostfix] + private static void Start(PauseMenuController __instance) + { + if(NetworkLifecycle.Instance.IsHost()) + return; + + __instance.loadSaveButton.gameObject.SetActive(false); + __instance.tutorialsButton.gameObject.SetActive(false); + } + + [HarmonyPatch(nameof(PauseMenuController.OnExitLevelClicked))] + [HarmonyPrefix] + private static bool OnExitLevelClicked(PauseMenuController __instance) + { + if(NetworkLifecycle.Instance.IsHost()) + return true; + + + if (!__instance.popupManager.CanShowPopup()) + { + Multiplayer.LogWarning("PauseMenuController.OnExitLevelClicked() PopupManager can't show popups at this moment"); + return false; + } + Popup popupPrefab = __instance.yesNoPopupPrefab; + PopupLocalizationKeys locKeys = popupDisconnectLocalizationKeys; + + __instance.popupManager.ShowPopup(popupPrefab, locKeys).Closed += (PopupResult result) => + { + //Negative = 'No', so we're aborting the disconnect + if (result.closedBy == PopupClosedByAction.Negative) + return; + + //Negative = 'No', so we're aborting the disconnect + if (result.closedBy == PopupClosedByAction.Negative) + return; + + FieldInfo eventField = __instance.GetType().GetField("ExitLevelRequested", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + if (eventField != null) + { + Delegate eventDelegate = (Delegate)eventField.GetValue(__instance); + if (eventDelegate != null) + eventDelegate.DynamicInvoke(); + } + }; + + return false; + } + + [HarmonyPatch("OnQuitClicked")] + [HarmonyPrefix] + private static bool OnQuitClicked(PauseMenuController __instance) + { + if(NetworkLifecycle.Instance.IsHost()) + return true; + + + if (!__instance.popupManager.CanShowPopup()) + { + Multiplayer.LogWarning("PauseMenuController.OnQuitClicked() PopupManager can't show popups at this moment"); + return false; + } + Popup popupPrefab = __instance.yesNoPopupPrefab; + PopupLocalizationKeys locKeys = popupDisconnectLocalizationKeys; + + __instance.popupManager.ShowPopup(popupPrefab, locKeys).Closed += (PopupResult result) => + { + //Negative = 'No', so we're aborting the disconnect + if (result.closedBy == PopupClosedByAction.Negative) + return; + + FieldInfo eventField = __instance.GetType().GetField("QuitGameRequested", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + if (eventField != null) + { + Delegate eventDelegate = (Delegate)eventField.GetValue(__instance); + if (eventDelegate != null) + eventDelegate.DynamicInvoke(); + } + }; + + return false; + } + + +} diff --git a/Multiplayer/Patches/Player/CustomFirstPersonControllerPatch.cs b/Multiplayer/Patches/Player/CustomFirstPersonControllerPatch.cs index 81f1e05..8dbd78a 100644 --- a/Multiplayer/Patches/Player/CustomFirstPersonControllerPatch.cs +++ b/Multiplayer/Patches/Player/CustomFirstPersonControllerPatch.cs @@ -53,7 +53,7 @@ private static void OnTick(uint tick) if(UnloadWatcher.isUnloading) return; - Vector3 position = isOnCar ? PlayerManager.PlayerTransform.localPosition : PlayerManager.GetWorldAbsolutePlayerPosition(); + Vector3 position = isOnCar ? PlayerManager.PlayerTransform.localPosition : PlayerManager.PlayerTransform.GetWorldAbsolutePosition(); float rotationY = (isOnCar ? PlayerManager.PlayerTransform.localEulerAngles : PlayerManager.PlayerTransform.eulerAngles).y; //bool positionOrRotationChanged = lastPosition != position || !Mathf.Approximately(lastRotationY, rotationY); diff --git a/Multiplayer/Patches/Player/MapMarkersControllerPatch.cs b/Multiplayer/Patches/Player/MapMarkersControllerPatch.cs new file mode 100644 index 0000000..ffa0f09 --- /dev/null +++ b/Multiplayer/Patches/Player/MapMarkersControllerPatch.cs @@ -0,0 +1,13 @@ +using HarmonyLib; +using Multiplayer.Components.Networking.Player; + +namespace Multiplayer.Patches.World; + +[HarmonyPatch(typeof(MapMarkersController), nameof(MapMarkersController.Awake))] +public static class MapMarkersController_Awake_Patch +{ + private static void Postfix(MapMarkersController __instance) + { + __instance.gameObject.AddComponent(); + } +} diff --git a/Multiplayer/Patches/Player/WorldMapPatch.cs b/Multiplayer/Patches/Player/WorldMapPatch.cs deleted file mode 100644 index ebcd469..0000000 --- a/Multiplayer/Patches/Player/WorldMapPatch.cs +++ /dev/null @@ -1,13 +0,0 @@ -using HarmonyLib; -using Multiplayer.Components.Networking.Player; - -namespace Multiplayer.Patches.World; - -[HarmonyPatch(typeof(WorldMap), nameof(WorldMap.Awake))] -public static class WorldMap_Awake_Patch -{ - private static void Postfix(WorldMap __instance) - { - __instance.gameObject.AddComponent(); - } -} diff --git a/Multiplayer/Patches/Train/HoseAndCockPatch.cs b/Multiplayer/Patches/Train/HoseAndCockPatch.cs index bb1b70c..03dc233 100644 --- a/Multiplayer/Patches/Train/HoseAndCockPatch.cs +++ b/Multiplayer/Patches/Train/HoseAndCockPatch.cs @@ -14,13 +14,17 @@ private static void Prefix(HoseAndCock __instance, bool open) if (UnloadWatcher.isUnloading || NetworkLifecycle.Instance.IsProcessingPacket) return; - Coupler coupler = NetworkedTrainCar.GetCoupler(__instance); + if(!NetworkedTrainCar.TryGetCoupler(__instance, out Coupler coupler)) + { + Multiplayer.LogError($"HoseAndCock.SetCock() Coupler not found! - Cars may be getting destroyed on load?"); + } if (coupler == null || !coupler.train.TryNetworked(out NetworkedTrainCar networkedTrainCar)) return; if (networkedTrainCar.IsDestroying) return; + NetworkLifecycle.Instance.Client?.SendCockState(networkedTrainCar.NetId, coupler, open); } } diff --git a/Multiplayer/Utils/DvExtensions.cs b/Multiplayer/Utils/DvExtensions.cs index 0ddc15a..e25ef67 100644 --- a/Multiplayer/Utils/DvExtensions.cs +++ b/Multiplayer/Utils/DvExtensions.cs @@ -141,5 +141,14 @@ public static float AnyPlayerSqrMag(this Vector3 anchor) */ return result; } + + public static Vector3 GetWorldAbsolutePosition(this GameObject go) + { + return go.transform.GetWorldAbsolutePosition(); + } + public static Vector3 GetWorldAbsolutePosition(this Transform transform) + { + return transform.position - WorldMover.currentMove; + } #endregion } From 43c43723e35daf513e546c7541cb12b23f84025d Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 24 Nov 2024 19:57:34 +1000 Subject: [PATCH 120/188] Minor localisation updates --- Multiplayer/Components/MainMenu/HostGamePane.cs | 8 ++++++-- .../Components/MainMenu/ServerBrowserPane.cs | 4 ++-- Multiplayer/Locale.cs | 14 ++++++++++++++ .../Patches/World/StorageControllerPatch.cs | 4 ++-- info.json | 2 +- 5 files changed, 25 insertions(+), 7 deletions(-) diff --git a/Multiplayer/Components/MainMenu/HostGamePane.cs b/Multiplayer/Components/MainMenu/HostGamePane.cs index 27a179a..f81788f 100644 --- a/Multiplayer/Components/MainMenu/HostGamePane.cs +++ b/Multiplayer/Components/MainMenu/HostGamePane.cs @@ -157,12 +157,16 @@ private void BuildUI() serverWindowGO.name = "Host Details"; serverDetails = serverDetailsGO.GetComponent(); serverDetails.textWrappingMode = TextWrappingModes.Normal; - serverDetails.text = "First time hosts, please see the Hosting section of our Wiki.


" + + serverDetails.text = Locale.Get(Locale.SERVER_HOST__INSTRUCTIONS_FIRST_KEY, ["", ""]) + "


" + + Locale.Get(Locale.SERVER_HOST__MOD_WARNING_KEY, ["", ""]) + "

" + + Locale.SERVER_HOST__RECOMMEND + "

" + + Locale.SERVER_HOST__SIGNOFF; + /*"First time hosts, please see the Hosting section of our Wiki.


" + "Using other mods may cause unexpected behaviour including de-syncs. See Mod Compatibility for more info.

" + "It is recommended that other mods are disabled and Derail Valley restarted prior to playing in multiplayer.

" + - "We hope to have your favourite mods compatible with multiplayer in the future."; + "We hope to have your favourite mods compatible with multiplayer in the future.";*/ //Find scrolling viewport diff --git a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs index 8ce2022..09a9e88 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs @@ -287,7 +287,7 @@ private void BuildUI() serverName.alignment = TextAlignmentOptions.Center; serverName.textWrappingMode = TextWrappingModes.Normal; serverName.fontSize = 22; - serverName.text = "Server Browser Info"; + serverName.text = Locale.SERVER_BROWSER__INFO_TITLE;// "Server Browser Info"; /* * Setup server details @@ -341,7 +341,7 @@ private void BuildUI() detailsPane = textGO.GetComponent(); detailsPane.textWrappingMode = TextWrappingModes.Normal; detailsPane.fontSize = 18; - detailsPane.text = "Welcome to Derail Valley Multiplayer Mod!

The server list refreshes automatically every 30 seconds, but you can refresh manually once every 10 seconds."; + detailsPane.text = Locale.Get(Locale.SERVER_BROWSER__INFO_CONTENT_KEY, [AUTO_REFRESH_TIME, REFRESH_MIN_TIME]);// "Welcome to Derail Valley Multiplayer Mod!

The server list refreshes automatically every 30 seconds, but you can refresh manually once every 10 seconds."; // Adjust text RectTransform to fit content RectTransform textRT = textGO.GetComponent(); diff --git a/Multiplayer/Locale.cs b/Multiplayer/Locale.cs index 6869652..fa9326e 100644 --- a/Multiplayer/Locale.cs +++ b/Multiplayer/Locale.cs @@ -66,6 +66,10 @@ public static class Locale 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"; + public static string SERVER_BROWSER__INFO_TITLE => Get(SERVER_BROWSER__INFO_TITLE_KEY); + public const string SERVER_BROWSER__INFO_TITLE_KEY = $"{PREFIX_SERVER_BROWSER}/info/title"; + public static string SERVER_BROWSER__INFO_CONTENT => Get(SERVER_BROWSER__INFO_CONTENT_KEY); + public const string SERVER_BROWSER__INFO_CONTENT_KEY = $"{PREFIX_SERVER_BROWSER}/info/content"; #endregion #region Server Host @@ -84,6 +88,16 @@ public static class Locale public static string SERVER_HOST_START => Get(SERVER_HOST_START_KEY); public const string SERVER_HOST_START_KEY = $"{PREFIX_SERVER_HOST}/start"; + public static string SERVER_HOST__INSTRUCTIONS_FIRST => Get(SERVER_HOST__INSTRUCTIONS_FIRST_KEY); + public const string SERVER_HOST__INSTRUCTIONS_FIRST_KEY = $"{PREFIX_SERVER_HOST}/instructions/first"; + public static string SERVER_HOST__MOD_WARNING => Get(SERVER_HOST__MOD_WARNING_KEY); + + public const string SERVER_HOST__MOD_WARNING_KEY = $"{PREFIX_SERVER_HOST}/instructions/mod_warning"; + public static string SERVER_HOST__RECOMMEND => Get(SERVER_HOST__RECOMMEND_KEY); + public const string SERVER_HOST__RECOMMEND_KEY = $"{PREFIX_SERVER_HOST}/instructions/recommend"; + public static string SERVER_HOST__SIGNOFF => Get(SERVER_HOST__SIGNOFF_KEY); + public const string SERVER_HOST__SIGNOFF_KEY = $"{PREFIX_SERVER_HOST}/instructions/signoff"; + #endregion diff --git a/Multiplayer/Patches/World/StorageControllerPatch.cs b/Multiplayer/Patches/World/StorageControllerPatch.cs index f0d5fa9..df1e981 100644 --- a/Multiplayer/Patches/World/StorageControllerPatch.cs +++ b/Multiplayer/Patches/World/StorageControllerPatch.cs @@ -6,7 +6,7 @@ using UnityEngine; namespace Multiplayer.Patches.World.Items; - +/* [HarmonyPatch(typeof(StorageController))] public static class StorageControllerPatch { @@ -78,5 +78,5 @@ static void RequestItemActivation(StorageController __instance) }); } - } +*/ diff --git a/info.json b/info.json index f72e9b9..b744923 100644 --- a/info.json +++ b/info.json @@ -1,6 +1,6 @@ { "Id": "Multiplayer", - "Version": "0.1.8.5", + "Version": "0.1.9.0", "DisplayName": "Multiplayer", "Author": "Insprill, Macka, Morm", "EntryMethod": "Multiplayer.Multiplayer.Load", From b00f47a615215cf665d5dc7468f060b9bd9141af Mon Sep 17 00:00:00 2001 From: AMacro Date: Fri, 29 Nov 2024 13:08:31 +1000 Subject: [PATCH 121/188] Updates to localisation --- .../Components/Networking/UI/ChatGUI.cs | 2 +- Multiplayer/Locale.cs | 19 ++++++++ Multiplayer/Multiplayer.csproj | 2 +- .../Networking/Managers/Server/ChatManager.cs | 21 ++++++++- README.md | 15 ++++++ info.json | 2 +- locale.csv | 46 +++++++++---------- 7 files changed, 79 insertions(+), 28 deletions(-) diff --git a/Multiplayer/Components/Networking/UI/ChatGUI.cs b/Multiplayer/Components/Networking/UI/ChatGUI.cs index f8583c3..721eff1 100644 --- a/Multiplayer/Components/Networking/UI/ChatGUI.cs +++ b/Multiplayer/Components/Networking/UI/ChatGUI.cs @@ -564,7 +564,7 @@ private void BuildUI() //Setup placeholder chatInputIF.placeholder.GetComponent().richText = false; - chatInputIF.placeholder.GetComponent().text = "Type a message and press Enter!"; + chatInputIF.placeholder.GetComponent().text = Locale.CHAT_PLACEHOLDER;// "Type a message and press Enter!"; //translate //Setup input renderer TMP_Text chatInputRenderer = textInputGO.FindChildByName("text [noloc]").GetComponent(); chatInputRenderer.fontSize = 18; diff --git a/Multiplayer/Locale.cs b/Multiplayer/Locale.cs index fa9326e..7a99dd0 100644 --- a/Multiplayer/Locale.cs +++ b/Multiplayer/Locale.cs @@ -2,8 +2,13 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.IO; +using dnlib.DotNet; +using DV.Rain; +using Humanizer; +using System.Xml.Linq; using I2.Loc; using Multiplayer.Utils; +using static VLB.Consts; namespace Multiplayer { @@ -143,6 +148,20 @@ public static class Locale #endregion #region Chat + public static string CHAT_PLACEHOLDER => Get(CHAT_PLACEHOLDER_KEY); + public const string CHAT_PLACEHOLDER_KEY = $"{PREFIX_CHAT_INFO}/placeholder"; + public static string CHAT_HELP_AVAILABLE => Get(CHAT_HELP_AVAILABLE_KEY); + public const string CHAT_HELP_AVAILABLE_KEY = $"{PREFIX_CHAT_INFO}/help/available"; + public static string CHAT_HELP_SERVER_MSG => Get(CHAT_HELP_SERVER_MSG_KEY); + public const string CHAT_HELP_SERVER_MSG_KEY = $"{PREFIX_CHAT_INFO}/help/servermsg"; + public static string CHAT_HELP_WHISPER_MSG => Get(CHAT_HELP_WHISPER_MSG_KEY); + public const string CHAT_HELP_WHISPER_MSG_KEY = $"{PREFIX_CHAT_INFO}/help/whispermsg"; + public static string CHAT_HELP_HELP => Get(CHAT_HELP_HELP_KEY); + public const string CHAT_HELP_HELP_KEY = $"{PREFIX_CHAT_INFO}/help/help"; + public static string CHAT_HELP_MSG => Get(CHAT_HELP_MSG_KEY); + public const string CHAT_HELP_MSG_KEY = $"{PREFIX_CHAT_INFO}/help/msg"; + public static string CHAT_HELP_PLAYER_NAME => Get(CHAT_HELP_PLAYER_NAME_KEY); + public const string CHAT_HELP_PLAYER_NAME_KEY = $"{PREFIX_CHAT_INFO}/help/playername"; #endregion #region Pause Menu diff --git a/Multiplayer/Multiplayer.csproj b/Multiplayer/Multiplayer.csproj index 6536c0b..af1909e 100644 --- a/Multiplayer/Multiplayer.csproj +++ b/Multiplayer/Multiplayer.csproj @@ -3,7 +3,7 @@ net48 latest Multiplayer - 0.1.9.0 + 0.1.9.1 diff --git a/Multiplayer/Networking/Managers/Server/ChatManager.cs b/Multiplayer/Networking/Managers/Server/ChatManager.cs index 1d3a7bb..8d6a05e 100644 --- a/Multiplayer/Networking/Managers/Server/ChatManager.cs +++ b/Multiplayer/Networking/Managers/Server/ChatManager.cs @@ -206,7 +206,24 @@ public static void KickMessage(string message, int commandLength, string senderN private static void HelpMessage(NetPeer peer) { - string message = $"Available commands:" + + string message = $"{Locale.CHAT_HELP_AVAILABLE}" + + + $"\r\n\r\n\t{Locale.CHAT_HELP_SERVER_MSG}" + + $"\r\n\t\t/server <{Locale.CHAT_HELP_MSG}>" + + $"\r\n\t\t/s <{Locale.CHAT_HELP_MSG}>" + + + $"\r\n\r\n\t{Locale.CHAT_HELP_WHISPER_MSG}" + + $"\r\n\t\t/whisper <{Locale.CHAT_HELP_PLAYER_NAME}> <{Locale.CHAT_HELP_MSG}>" + + $"\r\n\t\t/w <{Locale.CHAT_HELP_PLAYER_NAME}> <{Locale.CHAT_HELP_MSG}>" + + + $"\r\n\r\n\t{Locale.CHAT_HELP_HELP}" + + "\r\n\t\t/help" + + "\r\n\t\t/?" + + + ""; + + /* + * $"Available commands:" + "\r\n\r\n\tSend a message as the server (host only)" + "\r\n\t\t/server " + @@ -221,7 +238,7 @@ private static void HelpMessage(NetPeer peer) "\r\n\t\t/?" + ""; - + */ NetworkLifecycle.Instance.Server.SendWhisper(message, peer); } diff --git a/README.md b/README.md index 502fd3d..c0bc694 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@
  • Roadmap
  • Building
  • Contributing
  • +
  • Translations
  • License
  • @@ -89,6 +90,20 @@ If you're new to contributing to open-source projects, you can follow [this][con + + +## Translations + +Special thanks to those who have assisted with translations - Apologies if I've missed you, drop me a line and I'll update this section. +If you'd like to help with translations, please create a pull request or send a message on our [Discord channel](https://discord.com/channels/332511223536943105/1234574186161377363). +| **Translator** | **Language** | +| :------------ | :------------ +| Ádi | Hungarian | +| My Name Is BorING | Chinese (Simplified) | + + + + ## License diff --git a/info.json b/info.json index b744923..fe68cfe 100644 --- a/info.json +++ b/info.json @@ -1,6 +1,6 @@ { "Id": "Multiplayer", - "Version": "0.1.9.0", + "Version": "0.1.9.1", "DisplayName": "Multiplayer", "Author": "Insprill, Macka, Morm", "EntryMethod": "Multiplayer.Multiplayer.Load", diff --git a/locale.csv b/locale.csv index 0f13141..cd4a267 100644 --- a/locale.csv +++ b/locale.csv @@ -38,9 +38,9 @@ sb/no,Response 'no' for details text,No,Не,否,否,Ne,Nej,Nee,Ei,Non,Nein,न 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!,Сервери не знайдено. Оновіть або почніть власний! -sb/info/title,Title for server browser info,Server Browser Info,,服务器浏览器介绍,,,,,,,,,,,,,,,,,,,,,,, -sb/info/content,Content for server browser info,"Welcome to Derail Valley Multiplayer Mod!\n\nThe server list refreshes automatically every {0} seconds, but you can refresh manually once every {1} seconds.",,欢迎来到脱轨山谷的联机模式!\n\n服务器列表会在每{0}秒刷新,但是你可以手动让它在每{1}秒刷新,,,,,,,,,,,,,,,,,,,,,,, -sb/connecting,Connecting dialogue,"Connecting, please wait...\nAttempt: {0}",,正在连接中,请稍候片刻\n尝试次数: {0},,,,,,,,,,,,,,,,,,,,,,, +sb/info/title,Title for server browser info,Server Browser Info,,服务器浏览器介绍,,,,,,,,,Szerverböngésző információ,,,,,,,,,,,,,, +sb/info/content,Content for server browser info,"Welcome to Derail Valley Multiplayer Mod!\n\nThe server list refreshes automatically every {0} seconds, but you can refresh manually once every {1} seconds.",,"欢迎来到脱轨山谷的联机模式!\n\n服务器列表会在每{0}秒刷新,但是你可以手动让它在每{1}秒刷新",,,,,,,,,"Üdvözli a Derail Valley Multiplayer Mod!\n\nA szerverlista automatikusan frissül {0} másodpercenként, de manuálisan is frissíthetsz minden {1} másodpercet.",,,,,,,,,,,,,, +sb/connecting,Connecting dialogue,"Connecting, please wait...\nAttempt: {0}",,"正在连接中,请稍候片刻\n尝试次数: {0}",,,,,,,,,"Csatlakozás, kérjük, várjon...\nKísérlet: {0}",,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,, ,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,Ведуча гра @@ -59,24 +59,24 @@ host/max_players__tooltip_disabled,Unused,,,,,,,,,,,,,,,,,,,,,,,,,, host/start,Maximum players slider label,Start,Започнете,开始,開始,Start,Start,Begin,alkaa,Commencer,Start,शुरू,Indít!,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.,सर्वर प्रारंभ करें.,Szerver Indul!,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/instructions/first,Instructions for the host 1,"First time hosts, please see the {0}Hosting{1} section of our Wiki.",,"第一次主持游戏的话, 请看我们wiki的{0}Hosting{1} 模块",,,,,,,,,,,,,,,,,,,,,,, -host/instructions/mod_warning,Instructions for the host 2,Using other mods may cause unexpected behaviour including de-syncs. See {0}Mod Compatibility{1} for more info.,,同时使用其他模组可能会导致游戏出错,比如物品不同步, 看 {0}Mod Compatibility{1} 模块来获取更多信息,,,,,,,,,,,,,,,,,,,,,,, -host/instructions/recommend,Instructions for the host 3,It is recommended that other mods are disabled and Derail Valley restarted prior to playing in multiplayer.,,推荐你卸载其他模组并重启游戏后,再进行联机,,,,,,,,,,,,,,,,,,,,,,, -host/instructions/signoff,Instructions for the host 4,We hope to have your favourite mods compatible with multiplayer in the future.,,我们希望未来能让你装联机模组的同时也能玩其他模组,,,,,,,,,,,,,,,,,,,,,,, +host/instructions/first,Instructions for the host 1,"First time hosts, please see the {0}Hosting{1} section of our Wiki.",,"第一次主持游戏的话, 请看我们wiki的{0}Hosting{1} 模块",,,,,,,,,"Az első házigazdák, kérjük, tekintse meg Wikink {0}Hosting{1} részét.",,,,,,,,,,,,,, +host/instructions/mod_warning,Instructions for the host 2,Using other mods may cause unexpected behaviour including de-syncs. See {0}Mod Compatibility{1} for more info.,,"同时使用其他模组可能会导致游戏出错,比如物品不同步, 看 {0}Mod Compatibility{1} 模块来获取更多信息",,,,,,,,,"Más modok használata váratlan viselkedést okozhat, beleértve a szinkronizálást. További információért lásd a {0}Modkompatibilitást{1}.",,,,,,,,,,,,,, +host/instructions/recommend,Instructions for the host 3,It is recommended that other mods are disabled and Derail Valley restarted prior to playing in multiplayer.,,"推荐你卸载其他模组并重启游戏后,再进行联机",,,,,,,,,"Javasoljuk, hogy tiltsa le a többi modot, és indítsa újra a Derail Valleyt, mielőtt többjátékos módban játszana.",,,,,,,,,,,,,, +host/instructions/signoff,Instructions for the host 4,We hope to have your favourite mods compatible with multiplayer in the future.,,我们希望未来能让你装联机模组的同时也能玩其他模组,,,,,,,,,"Reméljük, hogy kedvenc modjai a jövőben kompatibilisek lesznek a többjátékos játékkal.",,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,, ,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/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/disconnect/unreachable,Host Unreachable error message,Host Unreachable,,无法找到房主,,,,,,,,,,,,,,,,,,,,,,, -dr/disconnect/unknown,Unknown Host error message,Unknown Host,,房主未知,,,,,,,,,,,,,,,,,,,,,,, -dr/disconnect/kicked,Player Kicked error message,Player Kicked,,玩家已被踢出,,,,,,,,,,,,,,,,,,,,,,, -dr/disconnect/rejected,Rejected! error message,Rejected!,,你已被拒绝加入服务器!,,,,,,,,,,,,,,,,,,,,,,, -dr/disconnect/shutdown,Server Shutting Down error message,Server Shutting Down,,服务器已经关闭,,,,,,,,,,,,,,,,,,,,,,, -dr/disconnect/timeout,Server Timed out,Server Timed out,,服务器连接超时,,,,,,,,,,,,,,,,,,,,,,, +dr/disconnect/unreachable,Host Unreachable error message,Host Unreachable,,无法找到房主,,,,,,,,,A házigazda elérhetetlen,,,,,,,,,,,,,, +dr/disconnect/unknown,Unknown Host error message,Unknown Host,,房主未知,,,,,,,,,Ismeretlen gazda,,,,,,,,,,,,,, +dr/disconnect/kicked,Player Kicked error message,Player Kicked,,玩家已被踢出,,,,,,,,,Játékos kirúgva,,,,,,,,,,,,,, +dr/disconnect/rejected,Rejected! error message,Rejected!,,你已被拒绝加入服务器!,,,,,,,,,Elutasítva!,,,,,,,,,,,,,, +dr/disconnect/shutdown,Server Shutting Down error message,Server Shutting Down,,服务器已经关闭,,,,,,,,,Szerver leállás,,,,,,,,,,,,,, +dr/disconnect/timeout,Server Timed out,Server Timed out,,服务器连接超时,,,,,,,,,Szerver időtúllépés,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,, ,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!,Тільки господар може керувати оплатою! @@ -89,14 +89,14 @@ linfo/wait_for_server,Text shown in the loading screen.,Waiting for server to lo 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,Синхронізація стану світу ,,,,,,,,,,,,,,,,,,,,,,,,,,, ,Chat,,,,,,,,,,,,,,,,,,,,,,,,,, -chat/placeholder,Chat input placeholder,Type a message and press Enter!,,在此输入文字,按回车发送,,,,,,,,,,,,,,,,,,,,,,, -chat/help/available,Chat help info available commands,Available commands:,,可用命令:,,,,,,,,,,,,,,,,,,,,,,, -chat/help/servermsg,Chat help send message as server,Send a message as the server (host only),,以服务器的身份发消息(仅限房主),,,,,,,,,,,,,,,,,,,,,,, -chat/help/whispermsg,Chat help whisper to a player,Whisper to a player,,向一位玩家说悄悄话,,,,,,,,,,,,,,,,,,,,,,, -chat/help/help,Chat help show help,Display this help message,,展示此帮助信息,,,,,,,,,,,,,,,,,,,,,,, -chat/help/msg,Chat help parameter e.g. /s ,message,,信息,,,,,,,,,,,,,,,,,,,,,,, -chat/help/playername,Chat help parameter e.g. /w ,player name,,玩家名字,,,,,,,,,,,,,,,,,,,,,,, +chat/placeholder,Chat input placeholder,Type a message and press Enter!,,"在此输入文字,按回车发送",,,,,,,,,Írjon be egy üzenetet és nyomja meg az Entert!,,,,,,,,,,,,,, +chat/help/available,Chat help info available commands,Available commands:,,可用命令:,,,,,,,,,Elérhető parancsok:,,,,,,,,,,,,,, +chat/help/servermsg,Chat help send message as server,Send a message as the server (host only),,以服务器的身份发消息(仅限房主),,,,,,,,,Üzenet küldése szerverként (csak gazdagép),,,,,,,,,,,,,, +chat/help/whispermsg,Chat help whisper to a player,Whisper to a player,,向一位玩家说悄悄话,,,,,,,,,Suttogj egy játékosnak,,,,,,,,,,,,,, +chat/help/help,Chat help show help,Display this help message,,展示此帮助信息,,,,,,,,,Jelenítse meg ezt a súgóüzenetet,,,,,,,,,,,,,, +chat/help/msg,Chat help parameter e.g. /s ,message,,信息,,,,,,,,,Üzenet,,,,,,,,,,,,,, +chat/help/playername,Chat help parameter e.g. /w ,player name,,玩家名字,,,,,,,,,Játékos neve,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,, ,Pause Menu,,,,,,,,,,,,,,,,,,,,,,,,,, -pm/disconnect_msg,Message when disconnecting from server (back to main menu),Disconnect and return to main menu?,,确定要断开连接并退回到主界面吗?,,,,,,,,,,,,,,,,,,,,,,, -pm/quit_msg,Message when disconnecting from server (quit game),Disconnect and quit?,,确定要断开连接并直接退出吗?,,,,,,,,,,,,,,,,,,,,,,, +pm/disconnect_msg,Message when disconnecting from server (back to main menu),Disconnect and return to main menu?,,确定要断开连接并退回到主界面吗?,,,,,,,,,Leválasztás és visszatérés a főmenübe?,,,,,,,,,,,,,, +pm/quit_msg,Message when disconnecting from server (quit game),Disconnect and quit?,,确定要断开连接并直接退出吗?,,,,,,,,,Lekapcsolja és kilép?,,,,,,,,,,,,,, From 8c2237940b4e76754a77185d015d5dfc9522de51 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 30 Nov 2024 16:38:53 +1000 Subject: [PATCH 122/188] Fix for 'LocalFruits' Error --- Multiplayer/Multiplayer.csproj | 2 +- Multiplayer/Patches/Jobs/JobBookletPatch.cs | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Multiplayer/Multiplayer.csproj b/Multiplayer/Multiplayer.csproj index af1909e..58b021d 100644 --- a/Multiplayer/Multiplayer.csproj +++ b/Multiplayer/Multiplayer.csproj @@ -106,5 +106,5 @@ - + diff --git a/Multiplayer/Patches/Jobs/JobBookletPatch.cs b/Multiplayer/Patches/Jobs/JobBookletPatch.cs index 716af65..6dc12f1 100644 --- a/Multiplayer/Patches/Jobs/JobBookletPatch.cs +++ b/Multiplayer/Patches/Jobs/JobBookletPatch.cs @@ -29,8 +29,11 @@ public static class JobBooklet_Patch [HarmonyPrefix] private static void DestroyJobBooklet(JobBooklet __instance) { - if (!NetworkedJob.TryGetFromJob(__instance.job, out NetworkedJob networkedJob)) - Multiplayer.LogError($"JobBooklet.DestroyJobBooklet() NetworkedJob not found for Job ID: {__instance.job?.ID}"); + if (__instance == null || __instance.job == null) + return; + + if (!NetworkedJob.TryGetFromJob(__instance?.job, out NetworkedJob networkedJob)) + Multiplayer.LogError($"JobBooklet.DestroyJobBooklet() NetworkedJob not found for Job ID: {__instance?.job?.ID}"); else networkedJob.JobBooklet = null; } From f1338453b584190d5f8e108866c13ccc4fe8df79 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 1 Dec 2024 18:11:56 +1000 Subject: [PATCH 123/188] Fixes for common post-B99 bugs Lantern delay registering tracked values and try-catch TrainsOptimizer - not sure why this is occasionally wigging out, but better data collection to track down the issue Creation of a dummy GameSession for clients - this is because a new user who has never started a single player game will not have a session and will cause CommsRadio and CareerManager to break. It's suspected (but not yet proven) that this may be the cause of license sync issues as well, as there are interactions between the session and LicenseManager --- .../Components/SaveGame/Client_GameSession.cs | 97 +++++++++++++++++++ .../SaveGame/StartGameData_ServerSave.cs | 19 ++++ Multiplayer/Multiplayer.csproj | 2 +- .../Managers/Client/NetworkClient.cs | 28 +++++- .../Patches/Train/TrainsOptimizerPatch.cs | 50 ++++++++++ .../Patches/World/Items/LanternPatch.cs | 76 ++++++++++----- info.json | 2 +- 7 files changed, 249 insertions(+), 25 deletions(-) create mode 100644 Multiplayer/Components/SaveGame/Client_GameSession.cs create mode 100644 Multiplayer/Patches/Train/TrainsOptimizerPatch.cs diff --git a/Multiplayer/Components/SaveGame/Client_GameSession.cs b/Multiplayer/Components/SaveGame/Client_GameSession.cs new file mode 100644 index 0000000..7b8a3f8 --- /dev/null +++ b/Multiplayer/Components/SaveGame/Client_GameSession.cs @@ -0,0 +1,97 @@ +using DV.Common; +using DV.JObjectExtstensions; +using DV.Scenarios.Common; +using DV.UserManagement; +using DV.UserManagement.Data; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using UnityEngine; + +namespace Multiplayer.Components.SaveGame; + +public class Client_GameSession : IGameSession, IThing, IDisposable +{ + private string _gameMode; + private JObject _gameData = new JObject(); + public static void SetCurrent(IGameSession session) + { + try + { + PropertyInfo currentSession = typeof(User).GetProperty("CurrentSession"); + currentSession?.SetValue(UserManager.Instance.CurrentUser, session); + } + catch (Exception ex) + { + Multiplayer.Log($"Client_GameSession.SetCurrent() failed: \r\n{ex.ToString()}"); + } + } + public Client_GameSession(string GameMode, IDifficulty difficulty) + { + _gameMode = GameMode; + _gameData.SetBool("Difficulty_picked", true); + Saves = new ReadOnlyObservableCollection(new ObservableCollection()); + + this.SetDifficulty(difficulty); + } + + string IGameSession.GameMode => _gameMode; + + string IGameSession.World => null; + + int IGameSession.SessionID => int.MaxValue; + + JObject IGameSession.GameData => _gameData; + + IUserProfile IGameSession.Owner => null; + + string IGameSession.BasePath => null; + + public ReadOnlyObservableCollection Saves { get; private set; } + + ISaveGame IGameSession.LatestSave => null; + + string IThing.Name { get => "Multiplayer Session"; set => throw new NotImplementedException(); } + + int IThing.DataVersion => 1; //might need to extract this from the Vanilla GameSession + + public void Save() + { + //do nothing + } + + void IGameSession.DeleteSaveGame(ISaveGame save) + { + //do nothing + } + + void IDisposable.Dispose() + { + //do nothing + } + + int IGameSession.GetSavesCountByType(SaveType type) + { + return 0; + } + + void IGameSession.MakeCurrent() + { + //do nothing + } + + ISaveGame IGameSession.SaveGame(SaveType type, JObject data, Texture2D thumbnail, List<(int Type, byte[] Data)> customChunks, ISaveGame overwrite) + { + return null; + } + + int IGameSession.TrimSaves(SaveType type, int maxCount, ISaveGame excluded) + { + return 0; + } +} diff --git a/Multiplayer/Components/SaveGame/StartGameData_ServerSave.cs b/Multiplayer/Components/SaveGame/StartGameData_ServerSave.cs index 326e564..2f14c15 100644 --- a/Multiplayer/Components/SaveGame/StartGameData_ServerSave.cs +++ b/Multiplayer/Components/SaveGame/StartGameData_ServerSave.cs @@ -4,6 +4,7 @@ using System.Linq; using DV; using DV.CabControls; +using DV.Common; using DV.UserManagement; using DV.Utils; using Multiplayer.Components.Networking; @@ -44,6 +45,24 @@ public void SetFromPacket(ClientboundSaveGameDataPacket packet) saveGameData.SetBool(SaveGameKeys.Damage_Popup_Shown, true); CareerManagerDebtControllerPatch.HasDebt = packet.HasDebt; + + Multiplayer.LogDebug(() => + { + string unlockedGen = string.Join(", ", UnlockablesManager.Instance.UnlockedGeneralLicenses); + string packetGen = string.Join(", ", packet.AcquiredGeneralLicenses); + + string unlockedJob = string.Join(", ", UnlockablesManager.Instance.UnlockedJobLicenses); + string packetJob = string.Join(", ", packet.AcquiredJobLicenses); + + return $"StartGameData_ServerSave.SetFromPacket() UnlockedGen: {{{unlockedGen}}}, PacketGen: {{{packetGen}}}, UnlockedJob: {{{unlockedJob}}}, PacketJob: {{{packetJob}}}"; + }); + + + //For clients we need to have a session - new users may not have a session and this may also be causing problems with licenses syncing + if (NetworkLifecycle.Instance.IsHost()) + return; + + Client_GameSession.SetCurrent(new Client_GameSession(packet.GameMode, DifficultyToUse)); } public override void Initialize() diff --git a/Multiplayer/Multiplayer.csproj b/Multiplayer/Multiplayer.csproj index 58b021d..150d9e8 100644 --- a/Multiplayer/Multiplayer.csproj +++ b/Multiplayer/Multiplayer.csproj @@ -3,7 +3,7 @@ net48 latest Multiplayer - 0.1.9.1 + 0.1.9.2 diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 84aa28e..49f300a 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -38,6 +38,8 @@ using Multiplayer.Networking.Packets.Serverbound.Train; using System.Linq; using LiteNetLib.Utils; +using DV.UserManagement; +using DV.Common; namespace Multiplayer.Networking.Listeners; @@ -57,6 +59,9 @@ public class NetworkClient : NetworkManager private ChatGUI chatGUI; public bool isSinglePlayer; + private bool isAlsoHost; + IGameSession originalSession; + public NetworkClient(Settings settings) : base(settings) { ClientPlayerManager = new ClientPlayerManager(); @@ -76,6 +81,21 @@ public void Start(string address, int port, string password, bool isSinglePlayer }; netPacketProcessor.Write(cachedWriter, serverboundClientLoginPacket); selfPeer = netManager.Connect(address, port, cachedWriter); + + isAlsoHost = NetworkLifecycle.Instance.IsServerRunning; + } + + public override void Stop() + { + if (!isAlsoHost) + { + LogDebug(() => $"NetworkClient.Stop() destroying session..."); + IGameSession session = UserManager.Instance.CurrentUser.CurrentSession; + Client_GameSession.SetCurrent(originalSession); + session?.Dispose(); + } + + base.Stop(); } protected override void Subscribe() @@ -318,7 +338,14 @@ private void OnClientboundSaveGameDataPacket(ClientboundSaveGameDataPacket packe AStartGameData.DestroyAllInstances(); GameObject go = new("Server Start Game Data"); + + //backup the session + originalSession = UserManager.Instance.CurrentUser.CurrentSession; + + //create a new save and load it go.AddComponent().SetFromPacket(packet); + + //ensure save is not destroyed on scene switch Object.DontDestroyOnLoad(go); SceneSwitcher.SwitchToScene(DVScenes.Game); @@ -331,7 +358,6 @@ private void OnClientboundSaveGameDataPacket(ClientboundSaveGameDataPacket packe Log($"WorldStreamingInit.LoadingFinished() SendReadyPacket()"); SendReadyPacket(); }; - TrainStress.globalIgnoreStressCalculation = true; diff --git a/Multiplayer/Patches/Train/TrainsOptimizerPatch.cs b/Multiplayer/Patches/Train/TrainsOptimizerPatch.cs new file mode 100644 index 0000000..2c9e0ff --- /dev/null +++ b/Multiplayer/Patches/Train/TrainsOptimizerPatch.cs @@ -0,0 +1,50 @@ +using HarmonyLib; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using DV.Logic.Job; +using DV.Utils; + +namespace Multiplayer.Patches.Train; +[HarmonyPatch(typeof(TrainsOptimizer))] +public static class TrainsOptimizerPatch +{ + [HarmonyPatch(nameof(TrainsOptimizer.ForceOptimizationStateOnCars))] + [HarmonyFinalizer] + public static void ForceOptimizationStateOnCars(TrainsOptimizer __instance, Exception __exception, HashSet carsToProcess, bool forceSleep, bool forceStateOnCloseStationaryCars) + { + if (__exception == null) + return; + + Multiplayer.LogError(() => + { + Dictionary logicCarToTrainCar = SingletonBehaviour.Instance.logicCarToTrainCar; + + if (carsToProcess == null) + return $"TrainsOptimizer.ForceOptimizationStateOnCars() carstToProcess is null!"; + + StringBuilder sb = new StringBuilder(); + sb.Append($"TrainsOptimizer.ForceOptimizationStateOnCars() iterating over {carsToProcess?.Count} cars:\r\n"); + + int i=0 ; + foreach (Car car in carsToProcess) + { + if (car == null) + sb.AppendLine($"\tCar {i} is null!"); + else + { + bool result = logicCarToTrainCar.TryGetValue(car, out TrainCar trainCar); + + sb.AppendLine($"\tCar {i} id {car.ID} found TrainCar: {result}, TC ID: {trainCar?.ID}"); + } + } + + i++; + + return sb.ToString(); + } + ); + } +} + diff --git a/Multiplayer/Patches/World/Items/LanternPatch.cs b/Multiplayer/Patches/World/Items/LanternPatch.cs index e61a541..324866d 100644 --- a/Multiplayer/Patches/World/Items/LanternPatch.cs +++ b/Multiplayer/Patches/World/Items/LanternPatch.cs @@ -1,38 +1,70 @@ using HarmonyLib; using Multiplayer.Components.Networking.World; using Multiplayer.Utils; +using System; +using System.Diagnostics; namespace Multiplayer.Patches.World.Items; -[HarmonyPatch(typeof(Lantern), "Awake")] -public static class LanternAwakePatch +[HarmonyPatch(typeof(Lantern))] +public static class LanternPatch { - static void Postfix(Lantern __instance) + [HarmonyPatch(nameof(Lantern.Awake))] + [HarmonyPostfix] + static void Awake(Lantern __instance) { - var networkedItem = __instance.gameObject.GetOrAddComponent(); + var networkedItem = __instance?.gameObject?.GetOrAddComponent(); + if (networkedItem == null) + { + Multiplayer.LogError($"LanternAwakePatch.Awake() networkedItem returned null!"); + return; + } + networkedItem.Initialize(__instance); + } - // Register the values you want to track with both getters and setters - networkedItem.RegisterTrackedValue( - "wickSize", - () => __instance.wickSize, - value => { - __instance.UpdateWickRelatedLogic(value); - } - ); + [HarmonyPatch(nameof(Lantern.Initialize))] + [HarmonyPostfix] + static void Initialize(Lantern __instance) + { + + var networkedItem = __instance?.gameObject?.GetOrAddComponent(); - networkedItem.RegisterTrackedValue( - "Ignited", - () => __instance.igniter.enabled, - value => + if(networkedItem == null) + { + Multiplayer.LogError($"Lantern.Initialize() networkedItem Not Found!"); + return; + } + + try + { + // Register the values you want to track with both getters and setters + networkedItem.RegisterTrackedValue( + "wickSize", + () => __instance.wickSize, + value => { - if (value) - __instance.Ignite(1); - else - __instance.OnFlameExtinguished(); + __instance.UpdateWickRelatedLogic(value); } - ); + ); + + networkedItem.RegisterTrackedValue( + "Ignited", + () => __instance.igniter.enabled, + value => + { + if (value) + __instance.Ignite(1); + else + __instance.OnFlameExtinguished(); + } + ); + + networkedItem.FinaliseTrackedValues(); - networkedItem.FinaliseTrackedValues(); + }catch(Exception ex) + { + Multiplayer.LogError($"Lantern.Initialize() {ex.Message}\r\n{ex.StackTrace}"); + } } } diff --git a/info.json b/info.json index fe68cfe..1da5d1e 100644 --- a/info.json +++ b/info.json @@ -1,6 +1,6 @@ { "Id": "Multiplayer", - "Version": "0.1.9.1", + "Version": "0.1.9.2", "DisplayName": "Multiplayer", "Author": "Insprill, Macka, Morm", "EntryMethod": "Multiplayer.Multiplayer.Load", From 8e0c67a3504ece82d15848deee0d08cf2be2c9e0 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 1 Dec 2024 18:25:55 +1000 Subject: [PATCH 124/188] Fix ping issues for private LAN games --- .../Components/MainMenu/HostGamePane.cs | 14 ++++- .../IServerBrowserGameDetails.cs | 1 + .../ServerBrowser/ServerBrowserElement.cs | 42 +++++++++---- .../Components/MainMenu/ServerBrowserPane.cs | 61 +++++++++++++------ .../Networking/Data/LobbyServerData.cs | 2 + .../Managers/Client/ServerBrowserClient.cs | 4 +- .../Managers/Server/LobbyServerManager.cs | 8 ++- .../Managers/Server/NetworkServer.cs | 7 ++- 8 files changed, 99 insertions(+), 40 deletions(-) diff --git a/Multiplayer/Components/MainMenu/HostGamePane.cs b/Multiplayer/Components/MainMenu/HostGamePane.cs index f81788f..02972aa 100644 --- a/Multiplayer/Components/MainMenu/HostGamePane.cs +++ b/Multiplayer/Components/MainMenu/HostGamePane.cs @@ -14,6 +14,9 @@ using Multiplayer.Networking.Data; using Multiplayer.Components.Networking; using Multiplayer.Components.Util; +using Multiplayer.Networking.Listeners; +using UnityModManagerNet; +using System.Linq; namespace Multiplayer.Components.MainMenu; public class HostGamePane : MonoBehaviour @@ -370,7 +373,16 @@ private void StartClick() serverData.CurrentPlayers = 0; serverData.MaxPlayers = (int)maxPlayers.value; - serverData.RequiredMods = ""; //FIX THIS - get the mods required + ModInfo[] serverMods = ModInfo.FromModEntries(UnityModManager.modEntries) + .Where(mod => !NetworkServer.modWhiteList.Contains(mod.Id) && mod.Id != Multiplayer.ModEntry.Info.Id).ToArray(); + + string requiredMods = ""; + if( serverMods.Length > 0) + { + requiredMods = string.Join(", ", serverMods.Select(mod => $"{{{mod.Id}, {mod.Version}}}")); + } + + serverData.RequiredMods = requiredMods; //FIX THIS - get the mods required serverData.GameVersion = BuildInfo.BUILD_VERSION_MAJOR.ToString(); serverData.MultiplayerVersion = Multiplayer.Ver; diff --git a/Multiplayer/Components/MainMenu/ServerBrowser/IServerBrowserGameDetails.cs b/Multiplayer/Components/MainMenu/ServerBrowser/IServerBrowserGameDetails.cs index c6d0c6b..997e367 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowser/IServerBrowserGameDetails.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowser/IServerBrowserGameDetails.cs @@ -17,6 +17,7 @@ public interface IServerBrowserGameDetails : IDisposable string ipv6 { get; set; } string ipv4 { get; set; } string LocalIPv4 { get; set; } + string LocalIPv6 { get; set; } int port { get; set; } string Name { get; set; } bool HasPassword { get; set; } diff --git a/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserElement.cs b/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserElement.cs index 6222a89..1f0cb5f 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserElement.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserElement.cs @@ -21,6 +21,17 @@ public class ServerBrowserElement : AViewElement 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 + private const string PING_COLOR_UNKNOWN = "#808080"; + private const string PING_COLOR_EXCELLENT = "#00ff00"; + private const string PING_COLOR_GOOD = "#ffa500"; + private const string PING_COLOR_HIGH = "#ff4500"; + private const string PING_COLOR_POOR = "#ff0000"; + + private const int PING_THRESHOLD_NONE = -1; + private const int PING_THRESHOLD_EXCELLENT = 60; + private const int PING_THRESHOLD_GOOD = 100; + private const int PING_THRESHOLD_HIGH = 150; + protected override void Awake() { // Find and assign TextMeshProUGUI components for displaying server details @@ -84,30 +95,37 @@ public override void SetData(IServerBrowserGameDetails data, AGridView{(data.Ping < 0 ? "?" : data.Ping)} ms"; + + if (data.MultiplayerVersion == Multiplayer.Ver) + ping.text = $"{(data.Ping < 0 ? "?" : data.Ping)} ms"; + else + ping.text = $"N/A"; // Hide the icon if the server does not have a password - goIconPassword.SetActive(data.HasPassword); - goIconLAN.SetActive(!string.IsNullOrEmpty(data.LocalIPv4)); + goIconPassword.SetActive(data.HasPassword); + + bool isLan = !string.IsNullOrEmpty(data.LocalIPv4) || !string.IsNullOrEmpty(data.LocalIPv6); + goIconLAN.SetActive(isLan); } private string GetColourForPing(int ping) { switch (ping) { - case -1: - return "#808080"; // Mid-range gray for unknown - case < 60: - return "#00ff00"; // Bright green for excellent ping - case < 100: - return "#ffa500"; // Orange for good ping - case < 150: - return "#ff4500"; // OrangeRed for high ping + case PING_THRESHOLD_NONE: + return PING_COLOR_UNKNOWN; + case < PING_THRESHOLD_EXCELLENT: + return PING_COLOR_EXCELLENT; + case < PING_THRESHOLD_GOOD: + return PING_COLOR_GOOD; + case < PING_THRESHOLD_HIGH: + return PING_COLOR_HIGH; default: - return "#ff0000"; // Red for don't even bother + return PING_COLOR_POOR; } } } diff --git a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs index 09a9e88..05023b8 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs @@ -535,7 +535,7 @@ private void UpdateDetailsPane() 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 += "" + Locale.SERVER_BROWSER__MODS_REQUIRED + ": " + (string.IsNullOrEmpty(selectedServer.RequiredMods) ? Locale.SERVER_BROWSER__NO : Locale.SERVER_BROWSER__YES) + "
    "; details += "
    "; details += "" + Locale.SERVER_BROWSER__GAME_VERSION + ": " + (selectedServer.GameVersion != BuildInfo.BUILD_VERSION_MAJOR.ToString() ? "" : "") + selectedServer.GameVersion + "
    "; details += "" + Locale.SERVER_BROWSER__MOD_VERSION + ": " + (selectedServer.MultiplayerVersion != Multiplayer.Ver ? "" : "") + selectedServer.MultiplayerVersion + "
    "; @@ -1126,12 +1126,16 @@ private void OnPing(string serverId, int ping, bool isIPv4) } private void SendPing(IServerBrowserGameDetails server) { - string ipv4 = server.ipv4; + //Ensure we are using the same MP mod version, don't ping other versions + Multiplayer.LogDebug(()=>$"SendPing: {server.Name}, {server.MultiplayerVersion}, {Multiplayer.Ver}"); + if (server.MultiplayerVersion != Multiplayer.Ver) + return; - if(!string.IsNullOrEmpty(server.LocalIPv4)) - ipv4 = server.LocalIPv4; - - serverBrowserClient.SendUnconnectedPingPacket(server.id, ipv4, server.ipv6, server.port); + // For LAN servers, prioritize the local IP addresses + string ipv4 = server.LocalIPv4 ?? server.ipv4; + string ipv6 = server.LocalIPv6 ?? server.ipv6; + + serverBrowserClient.SendUnconnectedPingPacket(server.id, ipv4, ipv6, server.port); } private float GetPingInterval() @@ -1180,26 +1184,43 @@ private int GetBestPing(int ipv4Ping, int ipv6Ping) private void OnDiscovery(IPEndPoint endpoint, LobbyServerData data) { - //Multiplayer.Log($"OnDiscovery({endpoint}) ID: {data.id}, Name: {data.Name}"); + if (data == null || endpoint == null) + return; - IServerBrowserGameDetails existing = localServers.FirstOrDefault(element => element.id == data.id); - if (existing != default(IServerBrowserGameDetails)) + Multiplayer.Log($"Discovery - Endpoint: {endpoint}, EP Family: {endpoint.AddressFamily}, LocalIPv4: {data?.LocalIPv4}, LocalIPv6: {data?.LocalIPv6}"); + + // Set local IP based on endpoint address type first + if (endpoint.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork) { - localServers.Remove(existing); + data.LocalIPv4 = endpoint.Address.ToString(); + Multiplayer.Log($"Setting LocalIPv4 to {data.LocalIPv4}"); } - - data.LastSeen = (int)Time.time; - localServers.Add(data); - - existing = gridViewModel.FirstOrDefault(element => element.id == data.id); - if (existing != default(IServerBrowserGameDetails)) + else if (endpoint.AddressFamily == System.Net.Sockets.AddressFamily.InterNetworkV6) { - existing.LastSeen = (int)Time.time; - existing.LocalIPv4 = data.LocalIPv4; + data.LocalIPv6 = endpoint.Address.ToString(); + Multiplayer.Log($"Setting LocalIPv6 to {data.LocalIPv6}"); } - data.LastSeen = (int)Time.time; - localServers.Add(data); + // Then handle server list management + if (!string.IsNullOrEmpty(data.id)) + { + IServerBrowserGameDetails existing = localServers.FirstOrDefault(element => element.id == data.id); + if (existing != default(IServerBrowserGameDetails)) + { + localServers.Remove(existing); + } + + data.LastSeen = (int)Time.time; + localServers.Add(data); + + existing = gridViewModel.FirstOrDefault(element => element.id == data.id); + if (existing != default(IServerBrowserGameDetails)) + { + existing.LastSeen = (int)Time.time; + existing.LocalIPv4 = data.LocalIPv4; + existing.LocalIPv6 = data.LocalIPv6; + } + } } private void ExpireLocalServers() diff --git a/Multiplayer/Networking/Data/LobbyServerData.cs b/Multiplayer/Networking/Data/LobbyServerData.cs index 2323a88..91484b4 100644 --- a/Multiplayer/Networking/Data/LobbyServerData.cs +++ b/Multiplayer/Networking/Data/LobbyServerData.cs @@ -18,6 +18,8 @@ public class LobbyServerData : IServerBrowserGameDetails [JsonIgnore] public string LocalIPv4 { get; set; } + [JsonIgnore] + public string LocalIPv6 { get; set; } [JsonProperty("server_name")] public string Name { get; set; } diff --git a/Multiplayer/Networking/Managers/Client/ServerBrowserClient.cs b/Multiplayer/Networking/Managers/Client/ServerBrowserClient.cs index b9c5d69..5d89743 100644 --- a/Multiplayer/Networking/Managers/Client/ServerBrowserClient.cs +++ b/Multiplayer/Networking/Managers/Client/ServerBrowserClient.cs @@ -45,8 +45,10 @@ public ServerBrowserClient(Settings settings) : base(settings) public void Start() { - netManager.Start(); netManager.UseNativeSockets = true; + netManager.IPv6Enabled = true; + netManager.Start(); + netManager.UpdateTime = 0; } public override void Stop() diff --git a/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs b/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs index 71a7beb..2fb92ee 100644 --- a/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs +++ b/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs @@ -85,7 +85,7 @@ private IEnumerator Start() if(server_id == null || server_id == string.Empty) { - server_id = $"LAN-{Guid.NewGuid()}"; + server_id = Guid.NewGuid().ToString(); } server.serverData.id = server_id; @@ -385,8 +385,10 @@ public void StartDiscoveryServer() discoveryListener = new EventBasedNetListener(); discoveryManager = new NetManager(discoveryListener) { + IPv6Enabled = true, UnconnectedMessagesEnabled = true, BroadcastReceiveEnabled = true, + }; packetProcessor = new NetPacketProcessor(discoveryManager); @@ -397,8 +399,8 @@ public void StartDiscoveryServer() foreach (int port in discoveryPorts) { - if (discoveryManager.Start(port)) - server.LogDebug(()=>$"Discovery server started on port {port}"); + if (discoveryManager.Start(IPAddress.Any, IPAddress.IPv6Any, port)) + server.LogDebug(()=>$"Discovery server started on port {port}"); else server.LogError($"Failed to start discovery server on port {port}"); } diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index 6511723..aea12eb 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -59,7 +59,7 @@ public class NetworkServer : NetworkManager private bool IsLoaded; //we don't care if the client doesn't have these mods - private string[] modWhiteList = { "RuntimeUnityEditor", "BookletOrganizer" }; + public static string[] modWhiteList = { "RuntimeUnityEditor", "BookletOrganizer" }; public NetworkServer(IDifficulty difficulty, Settings settings, bool isSinglePlayer, LobbyServerData serverData) : base(settings) { @@ -87,7 +87,8 @@ public bool Start(int port) { Multiplayer.Log($"Starting server, will listen to IPv6: {ipv6Address.ToString()}"); //start the connection, IPv4 messages can come from anywhere, IPv6 messages need to specifically come from the static IPv6 - return netManager.Start(IPAddress.Any, ipv6Address,port); + //return netManager.Start(IPAddress.Any, ipv6Address,port); + return netManager.Start(IPAddress.Any, IPAddress.IPv6Any,port); } //we're not running IPv6, start as normal @@ -982,7 +983,7 @@ private void OnCommonChatPacket(CommonChatPacket packet, NetPeer peer) private void OnUnconnectedPingPacket(UnconnectedPingPacket packet, IPEndPoint endPoint) { //Multiplayer.Log($"OnUnconnectedPingPacket({endPoint.Address})"); - SendUnconnectedPacket(packet, endPoint.Address.ToString(),endPoint.Port); + SendUnconnectedPacket(packet, endPoint.Address.ToString(), endPoint.Port); } private void OnCommonItemChangePacket(CommonItemChangePacket packet, NetPeer peer) From 9d1be936ee95dba962f934b55cc114b94b4b2368 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 1 Dec 2024 18:26:18 +1000 Subject: [PATCH 125/188] Fix Garage Unlock --- .../SaveGame/NetworkedSaveGameManager.cs | 1 + .../Patches/Train/GarageSpawnerPatch.cs | 20 +++++++++++++++++++ 2 files changed, 21 insertions(+) create mode 100644 Multiplayer/Patches/Train/GarageSpawnerPatch.cs diff --git a/Multiplayer/Components/SaveGame/NetworkedSaveGameManager.cs b/Multiplayer/Components/SaveGame/NetworkedSaveGameManager.cs index d143273..974a991 100644 --- a/Multiplayer/Components/SaveGame/NetworkedSaveGameManager.cs +++ b/Multiplayer/Components/SaveGame/NetworkedSaveGameManager.cs @@ -24,6 +24,7 @@ protected override void Awake() Inventory.Instance.MoneyChanged += Server_OnMoneyChanged; LicenseManager.Instance.LicenseAcquired += Server_OnLicenseAcquired; LicenseManager.Instance.JobLicenseAcquired += Server_OnJobLicenseAcquired; + LicenseManager.Instance.GarageUnlocked += Server_OnGarageUnlocked; } protected override void OnDestroy() diff --git a/Multiplayer/Patches/Train/GarageSpawnerPatch.cs b/Multiplayer/Patches/Train/GarageSpawnerPatch.cs new file mode 100644 index 0000000..ee87e61 --- /dev/null +++ b/Multiplayer/Patches/Train/GarageSpawnerPatch.cs @@ -0,0 +1,20 @@ +using HarmonyLib; +using Multiplayer.Components.Networking; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Multiplayer.Patches.Train; + +[HarmonyPatch(typeof(GarageCarSpawner))] +public static class GarageSpawnerPatch +{ + [HarmonyPatch(nameof(GarageCarSpawner.AllowSpawning))] + [HarmonyPrefix] + private static bool AllowSpawning(GarageCarSpawner __instance) + { + //we don't want the client to also spawn + return NetworkLifecycle.Instance.IsHost(); + } +} From 4bfaf4db2fd26009dbf15786cd28a50fb9b14155 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 1 Dec 2024 21:38:56 +1000 Subject: [PATCH 126/188] Additional logging to find source of sync issues --- .../Networking/Data/TrainsetSpawnPart.cs | 18 ++++++++- .../Managers/Server/NetworkServer.cs | 17 +++++++- Multiplayer/Patches/Train/CouplerPatch.cs | 7 ++++ Multiplayer/Patches/Train/HoseAndCockPatch.cs | 3 +- .../Patches/Train/TrainsOptimizerPatch.cs | 39 ++++++++++--------- 5 files changed, 61 insertions(+), 23 deletions(-) diff --git a/Multiplayer/Networking/Data/TrainsetSpawnPart.cs b/Multiplayer/Networking/Data/TrainsetSpawnPart.cs index c967e7e..fc2c6d9 100644 --- a/Multiplayer/Networking/Data/TrainsetSpawnPart.cs +++ b/Multiplayer/Networking/Data/TrainsetSpawnPart.cs @@ -1,4 +1,5 @@ using LiteNetLib.Utils; +using Multiplayer.Components.Networking; using Multiplayer.Components.Networking.Train; using Multiplayer.Networking.Serialization; using Multiplayer.Utils; @@ -76,6 +77,12 @@ public static TrainsetSpawnPart FromTrainCar(NetworkedTrainCar networkedTrainCar { TrainCar trainCar = networkedTrainCar.TrainCar; Transform transform = networkedTrainCar.transform; + + NetworkLifecycle.Instance.Server.LogDebug(() => + { + return $"TrainsetSpawnPart.FromTrainCar({networkedTrainCar?.NetId}) TrainCarID: {trainCar?.ID}, Livery: {trainCar?.carLivery}, LiveryID: {trainCar?.carLivery?.id}"; + }); + return new TrainsetSpawnPart( networkedTrainCar.NetId, trainCar.carLivery.id, @@ -97,8 +104,15 @@ public static TrainsetSpawnPart[] FromTrainSet(Trainset trainset) TrainsetSpawnPart[] parts = new TrainsetSpawnPart[trainset.cars.Count]; for (int i = 0; i < trainset.cars.Count; i++) { - if(trainset.cars[i].TryNetworked(out NetworkedTrainCar networkedTrainCar)) - parts[i] = FromTrainCar(networkedTrainCar); + NetworkedTrainCar networkedTrainCar; + + if (!trainset.cars[i].TryNetworked(out networkedTrainCar)) + { + NetworkLifecycle.Instance.Server.LogWarning($"TrainsetSpawnPart.FromTrainSet({trainset?.id}) Failed to find NetworkedTrainCar for: {trainset?.cars[i]?.ID}"); + networkedTrainCar = trainset.cars[i].GetOrAddComponent(); + } + + parts[i] = FromTrainCar(networkedTrainCar); } return parts; } diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index aea12eb..3cc94d7 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -31,6 +31,7 @@ using System.Net; using Multiplayer.Networking.Packets.Serverbound.Train; using Multiplayer.Networking.Packets.Unconnected; +using System.Text; namespace Multiplayer.Networking.Listeners; @@ -631,7 +632,21 @@ private void OnServerboundClientReadyPacket(ServerboundClientReadyPacket packet, // Send trains foreach (Trainset set in Trainset.allSets) { - LogDebug(() => $"Sending trainset {set.firstCar.GetNetId()} with {set.cars.Count} cars"); + LogDebug(() => + { + StringBuilder sb = new StringBuilder(); + + sb.Append($"Sending trainset {set?.firstCar?.GetNetId()} with {set?.cars?.Count} cars"); + + TrainCar[] noNetId = set?.cars?.Where(car => car.GetNetId() == 0).ToArray(); + + if (noNetId.Length > 0) + sb.AppendLine($"Erroneous cars!: {string.Join(", ", noNetId.Select(car=> $"{{{car?.ID}, {car?.CarGUID}, {car.logicCar != null}}}"))}"); + + return sb.ToString(); + + }); + SendPacket(peer, ClientboundSpawnTrainSetPacket.FromTrainSet(set), DeliveryMethod.ReliableOrdered); } diff --git a/Multiplayer/Patches/Train/CouplerPatch.cs b/Multiplayer/Patches/Train/CouplerPatch.cs index ffe7b93..c036ff0 100644 --- a/Multiplayer/Patches/Train/CouplerPatch.cs +++ b/Multiplayer/Patches/Train/CouplerPatch.cs @@ -12,6 +12,13 @@ private static void Postfix(Coupler __instance, Coupler other, bool playAudio, b { if (UnloadWatcher.isUnloading || NetworkLifecycle.Instance.IsProcessingPacket) return; + + if(__instance == null || other == null) + { + Multiplayer.LogError($"Coupler_CoupleTo_Patch({__instance?.train?.ID}, {other?.train?.ID}, {playAudio}, {viaChainInteraction})\r\n{new System.Diagnostics.StackTrace()}"); + return; + } + NetworkLifecycle.Instance.Client?.SendTrainCouple(__instance, other, playAudio, viaChainInteraction); } } diff --git a/Multiplayer/Patches/Train/HoseAndCockPatch.cs b/Multiplayer/Patches/Train/HoseAndCockPatch.cs index 03dc233..8b0f67d 100644 --- a/Multiplayer/Patches/Train/HoseAndCockPatch.cs +++ b/Multiplayer/Patches/Train/HoseAndCockPatch.cs @@ -16,7 +16,8 @@ private static void Prefix(HoseAndCock __instance, bool open) if(!NetworkedTrainCar.TryGetCoupler(__instance, out Coupler coupler)) { - Multiplayer.LogError($"HoseAndCock.SetCock() Coupler not found! - Cars may be getting destroyed on load?"); + TrainCar me = TrainCar.Resolve(__instance?.parentSystem?.gameObject); + Multiplayer.LogError($"HoseAndCock.SetCock() Coupler not found! - Cars may be getting destroyed on load? TrainCar ID: {me?.ID}"); } if (coupler == null || !coupler.train.TryNetworked(out NetworkedTrainCar networkedTrainCar)) diff --git a/Multiplayer/Patches/Train/TrainsOptimizerPatch.cs b/Multiplayer/Patches/Train/TrainsOptimizerPatch.cs index 2c9e0ff..4338a00 100644 --- a/Multiplayer/Patches/Train/TrainsOptimizerPatch.cs +++ b/Multiplayer/Patches/Train/TrainsOptimizerPatch.cs @@ -17,33 +17,34 @@ public static void ForceOptimizationStateOnCars(TrainsOptimizer __instance, Exce if (__exception == null) return; - Multiplayer.LogError(() => - { - Dictionary logicCarToTrainCar = SingletonBehaviour.Instance.logicCarToTrainCar; + Multiplayer.LogDebug(() => + { + Dictionary logicCarToTrainCar = SingletonBehaviour.Instance.logicCarToTrainCar; - if (carsToProcess == null) - return $"TrainsOptimizer.ForceOptimizationStateOnCars() carstToProcess is null!"; + if (carsToProcess == null) + return $"TrainsOptimizer.ForceOptimizationStateOnCars() carsToProcess is null!"; - StringBuilder sb = new StringBuilder(); - sb.Append($"TrainsOptimizer.ForceOptimizationStateOnCars() iterating over {carsToProcess?.Count} cars:\r\n"); + StringBuilder sb = new StringBuilder(); + sb.Append($"TrainsOptimizer.ForceOptimizationStateOnCars() iterating over {carsToProcess?.Count} cars:\r\n"); - int i=0 ; - foreach (Car car in carsToProcess) - { - if (car == null) - sb.AppendLine($"\tCar {i} is null!"); - else + int i=0 ; + foreach (Car car in carsToProcess) { - bool result = logicCarToTrainCar.TryGetValue(car, out TrainCar trainCar); + if (car == null) + sb.AppendLine($"\tCar {i} is null!"); + else + { + bool result = logicCarToTrainCar.TryGetValue(car, out TrainCar trainCar); + + sb.AppendLine($"\tCar {i} id {car?.ID} found TrainCar: {result}, TC ID: {trainCar?.ID}"); + } - sb.AppendLine($"\tCar {i} id {car.ID} found TrainCar: {result}, TC ID: {trainCar?.ID}"); + i++; } - } - i++; - return sb.ToString(); - } + return sb.ToString(); + } ); } } From adb43ca84e4d64c0208decdf3efc1ee64272affa Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 1 Dec 2024 21:39:23 +1000 Subject: [PATCH 127/188] Minor fixes --- .../Components/Networking/World/NetworkedItemManager.cs | 2 +- Multiplayer/Networking/Managers/Client/NetworkClient.cs | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/Multiplayer/Components/Networking/World/NetworkedItemManager.cs b/Multiplayer/Components/Networking/World/NetworkedItemManager.cs index c2938e5..3e938ca 100644 --- a/Multiplayer/Components/Networking/World/NetworkedItemManager.cs +++ b/Multiplayer/Components/Networking/World/NetworkedItemManager.cs @@ -10,7 +10,7 @@ using DV; using DV.Interaction; -namespace Multiplayer.Components.Networking.Train; +namespace Multiplayer.Components.Networking.World; public class NetworkedItemManager : SingletonBehaviour { diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 49f300a..7c7d77f 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -83,11 +83,12 @@ public void Start(string address, int port, string password, bool isSinglePlayer selfPeer = netManager.Connect(address, port, cachedWriter); isAlsoHost = NetworkLifecycle.Instance.IsServerRunning; + originalSession = UserManager.Instance.CurrentUser.CurrentSession; } public override void Stop() { - if (!isAlsoHost) + if (!isAlsoHost && originalSession != null) { LogDebug(() => $"NetworkClient.Stop() destroying session..."); IGameSession session = UserManager.Instance.CurrentUser.CurrentSession; @@ -339,9 +340,6 @@ private void OnClientboundSaveGameDataPacket(ClientboundSaveGameDataPacket packe GameObject go = new("Server Start Game Data"); - //backup the session - originalSession = UserManager.Instance.CurrentUser.CurrentSession; - //create a new save and load it go.AddComponent().SetFromPacket(packet); From ff9cbef36b3215e4f911725397990d933afc1e62 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 14 Dec 2024 16:17:17 +1000 Subject: [PATCH 128/188] Fix lobby server start-up logging --- .../Managers/Server/LobbyServerManager.cs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs b/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs index 2fb92ee..634d056 100644 --- a/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs +++ b/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs @@ -15,6 +15,7 @@ using Multiplayer.Networking.Packets.Unconnected; using System.Net; using LocoSim.Implementations; +using System.Linq; namespace Multiplayer.Networking.Managers.Server; public class LobbyServerManager : MonoBehaviour @@ -397,13 +398,14 @@ public void StartDiscoveryServer() packetProcessor.RegisterNestedType(LobbyServerData.Serialize, LobbyServerData.Deserialize); packetProcessor.SubscribeReusable(OnUnconnectedDiscoveryPacket); - foreach (int port in discoveryPorts) - { - if (discoveryManager.Start(IPAddress.Any, IPAddress.IPv6Any, port)) - server.LogDebug(()=>$"Discovery server started on port {port}"); - else - server.LogError($"Failed to start discovery server on port {port}"); - } + //start listening for discovery packets + int successPort = discoveryPorts.FirstOrDefault(port => + discoveryManager.Start(IPAddress.Any, IPAddress.IPv6Any, port)); + + if (successPort != 0) + server.Log($"Discovery server started on port {successPort}"); + else + server.LogError("Failed to start discovery server on any port"); } protected NetDataWriter WritePacket(T packet) where T : class, new() { From d6ce99149c8b708d9941d9acf9d20440c722b0b6 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 14 Dec 2024 16:21:19 +1000 Subject: [PATCH 129/188] Refactor StationLocoSpawner Use utility method for checking if anyone is in range, rather than a duplicate method --- .../Patches/World/StationLocoSpawnerPatch.cs | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/Multiplayer/Patches/World/StationLocoSpawnerPatch.cs b/Multiplayer/Patches/World/StationLocoSpawnerPatch.cs index 596f561..3bf5ff3 100644 --- a/Multiplayer/Patches/World/StationLocoSpawnerPatch.cs +++ b/Multiplayer/Patches/World/StationLocoSpawnerPatch.cs @@ -5,7 +5,7 @@ using DV.Utils; using HarmonyLib; using Multiplayer.Components.Networking; -using Multiplayer.Networking.Data; +using Multiplayer.Utils; using UnityEngine; namespace Multiplayer.Patches.World; @@ -37,7 +37,8 @@ private static IEnumerator CheckShouldSpawn(StationLocoSpawner __instance) { yield return CHECK_DELAY; - bool anyoneWithinRange = IsAnyoneWithinRange(__instance, __instance.spawnTrackMiddleAnchor.transform.position); + + bool anyoneWithinRange = __instance.spawnTrackMiddleAnchor.transform.position.AnyPlayerSqrMag() < __instance.spawnLocoPlayerSqrDistanceFromTrack; switch (__instance.playerEnteredLocoSpawnRange) { @@ -52,16 +53,6 @@ private static IEnumerator CheckShouldSpawn(StationLocoSpawner __instance) } } - private static bool IsAnyoneWithinRange(StationLocoSpawner stationLocoSpawner, Vector3 targetPosition) - { - foreach (ServerPlayer serverPlayer in NetworkLifecycle.Instance.Server.ServerPlayers) - { - if (serverPlayer != null && (serverPlayer.WorldPosition - targetPosition).sqrMagnitude < stationLocoSpawner.spawnLocoPlayerSqrDistanceFromTrack) - return true; - } - return false; - } - private static void SpawnLocomotives(StationLocoSpawner stationLocoSpawner) { List carsFullyOnTrack = stationLocoSpawner.locoSpawnTrack.logicTrack.GetCarsFullyOnTrack(); From bedd1dff3f039908af2236883d9224b8f0e4e58d Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 14 Dec 2024 16:55:13 +1000 Subject: [PATCH 130/188] Fixed issue `Undefined packet 34 in NetDataReader` --- Multiplayer/Networking/Managers/Client/NetworkClient.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 7c7d77f..a7980ee 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -135,6 +135,7 @@ protected override void Subscribe() netPacketProcessor.SubscribeReusable(OnCommonSimFlowPacket); netPacketProcessor.SubscribeReusable(OnCommonTrainFusesPacket); netPacketProcessor.SubscribeReusable(OnClientboundBrakePressureUpdatePacket); + netPacketProcessor.SubscribeReusable(OnClientboundFireboxStatePacket); netPacketProcessor.SubscribeReusable(OnClientboundCargoStatePacket); netPacketProcessor.SubscribeReusable(OnClientboundCarHealthUpdatePacket); netPacketProcessor.SubscribeReusable(OnClientboundRerailTrainPacket); From 225c2ad6fc4436d39f356403bf67c28e39d4062a Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 21 Dec 2024 22:00:52 +1000 Subject: [PATCH 131/188] Clean up logging --- .../ServerBrowser/ServerBrowserGridView.cs | 2 +- .../Components/MainMenu/ServerBrowserPane.cs | 1 + .../Components/Networking/UI/ChatGUI.cs | 8 +-- .../Networking/World/NetworkedItem.cs | 18 ++--- .../World/NetworkedStationController.cs | 8 +-- .../Networking/Data/TaskNetworkData.cs | 8 +-- .../Managers/Client/NetworkClient.cs | 69 ++++++++++--------- .../Managers/Server/NetworkServer.cs | 59 ++++++++-------- .../ClientboundSaveGameDataPacket.cs | 11 +++ .../Train/ClientboundFireboxStatePacket.cs | 3 +- .../MainMenu/LauncherControllerPatch.cs | 4 +- .../MainMenu/RightPaneControllerPatch.cs | 2 +- Multiplayer/Patches/Train/HoseAndCockPatch.cs | 4 +- 13 files changed, 106 insertions(+), 91 deletions(-) diff --git a/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserGridView.cs b/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserGridView.cs index 3a861b3..ea52694 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserGridView.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserGridView.cs @@ -15,7 +15,7 @@ public class ServerBrowserGridView : AGridView protected override void Awake() { - Multiplayer.Log("serverBrowserGridview Awake()"); + //Multiplayer.Log("serverBrowserGridview Awake()"); //copy the copy this.viewElementPrefab.SetActive(false); diff --git a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs index 05023b8..56ac125 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs @@ -215,6 +215,7 @@ private void Update() { ExpireLocalServers(); //remove any that have not been seen in a while RefreshGridView(); + IndexChanged(gridView); //Revalidate any selected servers localRefreshComplete = false; remoteRefreshComplete = false; diff --git a/Multiplayer/Components/Networking/UI/ChatGUI.cs b/Multiplayer/Components/Networking/UI/ChatGUI.cs index 721eff1..27b1db5 100644 --- a/Multiplayer/Components/Networking/UI/ChatGUI.cs +++ b/Multiplayer/Components/Networking/UI/ChatGUI.cs @@ -486,7 +486,7 @@ private void BuildUI() if (saveLoad == null) { - Multiplayer.Log("Could not find SaveLoadController, attempting to instanciate"); + //Multiplayer.Log("Could not find SaveLoadController, attempting to instantiate"); AppUtil.Instance.PauseGame(); Multiplayer.Log("Paused"); @@ -495,16 +495,16 @@ private void BuildUI() if (saveLoad == null) { - Multiplayer.Log("Failed to get SaveLoadController"); + Multiplayer.LogError("Failed to get SaveLoadController"); } else { - Multiplayer.Log("Made a SaveLoadController!"); + //Multiplayer.Log("Made a SaveLoadController!"); scrollViewPrefab = saveLoad.FindChildByName("Scroll View"); if (scrollViewPrefab == null) { - Multiplayer.Log("Could not find scrollViewPrefab"); + Multiplayer.LogError("Could not find scrollViewPrefab"); } else diff --git a/Multiplayer/Components/Networking/World/NetworkedItem.cs b/Multiplayer/Components/Networking/World/NetworkedItem.cs index 36b372f..24f9be3 100644 --- a/Multiplayer/Components/Networking/World/NetworkedItem.cs +++ b/Multiplayer/Components/Networking/World/NetworkedItem.cs @@ -108,7 +108,7 @@ public static bool TryGetNetworkedItem(ItemBase item, out NetworkedItem networke protected override void Awake() { base.Awake(); - Multiplayer.LogDebug(() => $"NetworkedItem.Awake() {name}"); + //Multiplayer.LogDebug(() => $"NetworkedItem.Awake() {name}"); NetworkedItemManager.Instance.CheckInstance(); //Ensure the NetworkedItemManager is initialised Register(); @@ -131,7 +131,7 @@ public T GetTrackedItem() where T : Component public void Initialize(T item, ushort netId = 0, bool createDirty = true) where T : Component { - Multiplayer.LogDebug(() => $"NetworkedItem.Initialize<{typeof(T)}>(netId: {netId}, name: {name}, createDirty: {createdDirty})"); + //Multiplayer.LogDebug(() => $"NetworkedItem.Initialize<{typeof(T)}>(netId: {netId}, name: {name}, createDirty: {createdDirty})"); if (netId != 0) NetId = netId; @@ -186,13 +186,13 @@ private bool Register() private void OnUngrabbed(ControlImplBase obj) { - Multiplayer.LogDebug(() => $"NetworkedItem.OnUngrabbed() NetID: {NetId}, {name}"); + //Multiplayer.LogDebug(() => $"NetworkedItem.OnUngrabbed() NetID: {NetId}, {name}"); stateDirty = true; } private void OnGrabbed(ControlImplBase obj) { - Multiplayer.LogDebug(() => $"NetworkedItem.OnGrabbed() NetID: {NetId}, {name}"); + //Multiplayer.LogDebug(() => $"NetworkedItem.OnGrabbed() NetID: {NetId}, {name}"); stateDirty = true; } @@ -209,7 +209,7 @@ public void OnThrow(Vector3 direction) thrownPosition = Item.transform.position - WorldMover.currentMove; thrownRotation = Item.transform.rotation; - Multiplayer.LogDebug(() => $"NetworkedItem.OnThrow() netId: {NetId}, Name: {name}, Raw Position: {Item.transform.position}, Position: {thrownPosition}, Rotation: {thrownRotation}, Direction: {throwDirection}"); + //Multiplayer.LogDebug(() => $"NetworkedItem.OnThrow() netId: {NetId}, Name: {name}, Raw Position: {Item.transform.position}, Position: {thrownPosition}, Rotation: {thrownRotation}, Direction: {throwDirection}"); wasThrown = true; stateDirty = true; @@ -219,13 +219,13 @@ public void OnThrow(Vector3 direction) #region Item Value Tracking public void RegisterTrackedValue(string key, Func valueGetter, Action valueSetter, Func thresholdComparer = null, bool serverAuthoritative = false) { - Multiplayer.LogDebug(() => $"NetworkedItem.RegisterTrackedValue(\"{key}\", {valueGetter != null}, {valueSetter != null}, {thresholdComparer != null}, {serverAuthoritative}) itemNetId {NetId}, item name: {name}"); + //Multiplayer.LogDebug(() => $"NetworkedItem.RegisterTrackedValue(\"{key}\", {valueGetter != null}, {valueSetter != null}, {thresholdComparer != null}, {serverAuthoritative}) itemNetId {NetId}, item name: {name}"); trackedValues.Add(new TrackedValue(key, valueGetter, valueSetter, thresholdComparer, serverAuthoritative)); } public void FinaliseTrackedValues() { - Multiplayer.LogDebug(() => $"NetworkedItem.FinaliseTrackedValues() itemNetId: {NetId}, item name: {name}"); + //Multiplayer.LogDebug(() => $"NetworkedItem.FinaliseTrackedValues() itemNetId: {NetId}, item name: {name}"); while (pendingSnapshots.Count > 0) { @@ -393,7 +393,7 @@ private void ApplySnapshot(ItemUpdateData snapshot) public ItemUpdateData CreateUpdateData(ItemUpdateData.ItemUpdateType updateType) { - Multiplayer.LogDebug(() => $"NetworkedItem.CreateUpdateData({updateType}) NetId: {NetId}, name: {name}"); + //Multiplayer.LogDebug(() => $"NetworkedItem.CreateUpdateData({updateType}) NetId: {NetId}, name: {name}"); Vector3 position; Quaternion rotation; @@ -451,7 +451,7 @@ public ItemUpdateData CreateUpdateData(ItemUpdateData.ItemUpdateType updateType) private ItemState GetItemState() { - Multiplayer.LogDebug(() => $"GetItemState() NetId: {NetId}, {name}, Parent: {Item.transform.parent} WorldMover: {WorldMover.OriginShiftParent}, wasThrown: {wasThrown}, isGrabbed: {Item.IsGrabbed()} Inventory.Contains(): {Inventory.Instance.Contains(this.gameObject, false)} Storage.Contains: {StorageController.Instance.StorageInventory.ContainsItem(Item)}"); + //Multiplayer.LogDebug(() => $"GetItemState() NetId: {NetId}, {name}, Parent: {Item.transform.parent} WorldMover: {WorldMover.OriginShiftParent}, wasThrown: {wasThrown}, isGrabbed: {Item.IsGrabbed()} Inventory.Contains(): {Inventory.Instance.Contains(this.gameObject, false)} Storage.Contains: {StorageController.Instance.StorageInventory.ContainsItem(Item)}"); if (Item.transform.parent == WorldMover.OriginShiftParent && !wasThrown) diff --git a/Multiplayer/Components/Networking/World/NetworkedStationController.cs b/Multiplayer/Components/Networking/World/NetworkedStationController.cs index 4519e64..6ca7547 100644 --- a/Multiplayer/Components/Networking/World/NetworkedStationController.cs +++ b/Multiplayer/Components/Networking/World/NetworkedStationController.cs @@ -93,14 +93,14 @@ public static void RegisterStationController(NetworkedStationController networke public static void QueueJobValidator(JobValidator jobValidator) { - Multiplayer.Log($"QueueJobValidator() {jobValidator.transform.parent.name}"); + //Multiplayer.Log($"QueueJobValidator() {jobValidator.transform.parent.name}"); jobValidators.Add(jobValidator); } private static void RegisterJobValidator(JobValidator jobValidator, NetworkedStationController stationController) { - Multiplayer.Log($"RegisterJobValidator() {jobValidator.transform.parent.name}, {stationController.name}"); + //Multiplayer.Log($"RegisterJobValidator() {jobValidator.transform.parent.name}, {stationController.name}"); stationController.JobValidator = jobValidator; jobValidatorToNetworkedStation[jobValidator] = stationController; } @@ -150,7 +150,7 @@ private IEnumerator WaitForLogicStation() abandonedJobs = StationController.logicStation.abandonedJobs; completedJobs = StationController.logicStation.completedJobs; - Multiplayer.Log($"NetworkedStation.Awake({StationController.logicStation.ID})"); + //Multiplayer.Log($"NetworkedStation.Awake({StationController.logicStation.ID})"); foreach (JobValidator validator in jobValidators) { @@ -452,7 +452,7 @@ private static void UpdateCarPlatesRecursive(List tasks, stri private void GenerateOverview(NetworkedJob networkedJob, ushort itemNetId, ItemPositionData posData) { - Multiplayer.Log($"GenerateOverview({networkedJob.Job.ID}) Position: {posData.Position}, Less currentMove: {posData.Position + WorldMover.currentMove} "); + Multiplayer.Log($"GenerateOverview({networkedJob.Job.ID}, {itemNetId}) Position: {posData.Position}, Less currentMove: {posData.Position + WorldMover.currentMove} "); JobOverview jobOverview = BookletCreator_JobOverview.Create(networkedJob.Job, posData.Position + WorldMover.currentMove, posData.Rotation,WorldMover.OriginShiftParent); NetworkedItem netItem = jobOverview.GetOrAddComponent(); diff --git a/Multiplayer/Networking/Data/TaskNetworkData.cs b/Multiplayer/Networking/Data/TaskNetworkData.cs index 5d1edaa..bd74e6e 100644 --- a/Multiplayer/Networking/Data/TaskNetworkData.cs +++ b/Multiplayer/Networking/Data/TaskNetworkData.cs @@ -253,10 +253,10 @@ public override void Deserialize(NetDataReader reader) //Multiplayer.Log($"TaskNetworkData.Deserialize() TransportedCargoPerCar != null True"); TransportedCargoPerCar = reader.GetIntArray().Select(x => (CargoType)x).ToArray(); } - else - { - Multiplayer.LogWarning($"TaskNetworkData.Deserialize() TransportedCargoPerCar != null False"); - } + //else + //{ + // Multiplayer.LogWarning($"TaskNetworkData.Deserialize() TransportedCargoPerCar != null False"); + //} CouplingRequiredAndNotDone = reader.GetBool(); //Multiplayer.Log($"TaskNetworkData.Deserialize() CouplingRequiredAndNotDone {CouplingRequiredAndNotDone}"); AnyHandbrakeRequiredAndNotDone = reader.GetBool(); diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index a7980ee..011dfae 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -492,13 +492,16 @@ private void OnClientboundSpawnTrainCarPacket(ClientboundSpawnTrainCarPacket pac private void OnClientboundSpawnTrainSetPacket(ClientboundSpawnTrainSetPacket packet) { - LogDebug(() => + LogDebug(() => $"Spawning trainset consisting of {string.Join(", ", packet.SpawnParts.Select(p => $"{p.CarId} ({p.LiveryId}) with netId: {p.NetId}"))}"); + + foreach (var part in packet.SpawnParts) { - StringBuilder sb = new("Spawning trainset consisting of "); - foreach (TrainsetSpawnPart spawnPart in packet.SpawnParts) - sb.Append($"{spawnPart.CarId} ({spawnPart.LiveryId}) with net ID {spawnPart.NetId}, "); - return sb.ToString(); - }); + if(NetworkedTrainCar.GetTrainCarFromTrainId(part.CarId, out TrainCar car)) + { + LogError($"ClientboundSpawnTrainSetPacket() Tried to spawn trainset with carId: {part.CarId}, but car already exists!"); + return; + } + } NetworkedCarSpawner.SpawnCars(packet.SpawnParts); @@ -813,37 +816,37 @@ private void OnClientboundJobValidateResponsePacket(ClientboundJobValidateRespon private void OnCommonItemChangePacket(CommonItemChangePacket packet) { - LogDebug(() => $"OnCommonItemChangePacket({packet?.Items?.Count})"); + //LogDebug(() => $"OnCommonItemChangePacket({packet?.Items?.Count})"); - Multiplayer.LogDebug(() => - { - string debug = ""; + //Multiplayer.LogDebug(() => + //{ + // string debug = ""; - foreach (var item in packet?.Items) - { - debug += "UpdateType: " + item?.UpdateType + "\r\n"; - debug += "itemNetId: " + item?.ItemNetId + "\r\n"; - debug += "PrefabName: " + item?.PrefabName + "\r\n"; - debug += "Equipped: " + item?.ItemState + "\r\n"; - debug += "Position: " + item?.ItemPosition + "\r\n"; - debug += "Rotation: " + item?.ItemRotation + "\r\n"; - debug += "ThrowDirection: " + item?.ThrowDirection + "\r\n"; - debug += "Player: " + item?.Player + "\r\n"; - debug += "CarNetId: " + item?.CarNetId + "\r\n"; - debug += "AttachedFront: " + item?.AttachedFront + "\r\n"; - - debug += $"States: {item?.States?.Count}\r\n"; - - if (item.States != null) - foreach (var state in item?.States) - debug += "\t" + state.Key + ": " + state.Value + "\r\n"; - else - debug += "\r\n"; - } + // foreach (var item in packet?.Items) + // { + // debug += "UpdateType: " + item?.UpdateType + "\r\n"; + // debug += "itemNetId: " + item?.ItemNetId + "\r\n"; + // debug += "PrefabName: " + item?.PrefabName + "\r\n"; + // debug += "Equipped: " + item?.ItemState + "\r\n"; + // debug += "Position: " + item?.ItemPosition + "\r\n"; + // debug += "Rotation: " + item?.ItemRotation + "\r\n"; + // debug += "ThrowDirection: " + item?.ThrowDirection + "\r\n"; + // debug += "Player: " + item?.Player + "\r\n"; + // debug += "CarNetId: " + item?.CarNetId + "\r\n"; + // debug += "AttachedFront: " + item?.AttachedFront + "\r\n"; + + // debug += $"States: {item?.States?.Count}\r\n"; - return debug; - }); + // if (item.States != null) + // foreach (var state in item?.States) + // debug += "\t" + state.Key + ": " + state.Value + "\r\n"; + // else + // debug += "\r\n"; + // } + + // return debug; + //}); //NetworkedItemManager.Instance.ReceiveSnapshots(packet.Items, null); } diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index 3cc94d7..e219e14 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -666,7 +666,7 @@ private void OnServerboundClientReadyPacket(ServerboundClientReadyPacket packet, } else { - Multiplayer.LogError($"Sending job packets... Failed to get NetworkedStation from station"); + LogError($"Sending job packets... Failed to get NetworkedStation from station"); } } @@ -687,6 +687,7 @@ private void OnServerboundClientReadyPacket(ServerboundClientReadyPacket packet, } // All data has been sent, allow the client to load into the world. + Log($"Sending Remove Loading Screen to {serverPlayer.Username}"); SendPacket(peer, new ClientboundRemoveLoadingScreenPacket(), DeliveryMethod.ReliableOrdered); serverPlayer.IsLoaded = true; @@ -1003,39 +1004,39 @@ private void OnUnconnectedPingPacket(UnconnectedPingPacket packet, IPEndPoint en private void OnCommonItemChangePacket(CommonItemChangePacket packet, NetPeer peer) { - if(!TryGetServerPlayer(peer, out var player)) - return; + //if(!TryGetServerPlayer(peer, out var player)) + // return; - LogDebug(()=>$"OnCommonItemChangePacket({packet?.Items?.Count}, {peer.Id} (\"{player.Username}\"))"); + //LogDebug(()=>$"OnCommonItemChangePacket({packet?.Items?.Count}, {peer.Id} (\"{player.Username}\"))"); - Multiplayer.LogDebug(() => - { - string debug = ""; + //Multiplayer.LogDebug(() => + //{ + // string debug = ""; - foreach (var item in packet?.Items) - { - debug += "UpdateType: " + item?.UpdateType + "\r\n"; - debug += "itemNetId: " + item?.ItemNetId + "\r\n"; - debug += "PrefabName: " + item?.PrefabName + "\r\n"; - debug += "Equipped: " + item?.ItemState + "\r\n"; - debug += "Position: " + item?.ItemPosition + "\r\n"; - debug += "Rotation: " + item?.ItemRotation + "\r\n"; - debug += "ThrowDirection: " + item?.ThrowDirection + "\r\n"; - debug += "Player: " + item?.Player + "\r\n"; - debug += "CarNetId: " + item?.CarNetId + "\r\n"; - debug += "AttachedFront: " + item?.AttachedFront + "\r\n"; - - debug += "States:"; - - if (item.States != null) - foreach (var state in item?.States) - debug += "\r\n\t" + state.Key + ": " + state.Value; - } + // foreach (var item in packet?.Items) + // { + // debug += "UpdateType: " + item?.UpdateType + "\r\n"; + // debug += "itemNetId: " + item?.ItemNetId + "\r\n"; + // debug += "PrefabName: " + item?.PrefabName + "\r\n"; + // debug += "Equipped: " + item?.ItemState + "\r\n"; + // debug += "Position: " + item?.ItemPosition + "\r\n"; + // debug += "Rotation: " + item?.ItemRotation + "\r\n"; + // debug += "ThrowDirection: " + item?.ThrowDirection + "\r\n"; + // debug += "Player: " + item?.Player + "\r\n"; + // debug += "CarNetId: " + item?.CarNetId + "\r\n"; + // debug += "AttachedFront: " + item?.AttachedFront + "\r\n"; - return debug; - } + // debug += "States:"; + + // if (item.States != null) + // foreach (var state in item?.States) + // debug += "\r\n\t" + state.Key + ": " + state.Value; + // } + + // return debug; + //} - ); + //); //NetworkedItemManager.Instance.ReceiveSnapshots(packet.Items, player); } diff --git a/Multiplayer/Networking/Packets/Clientbound/ClientboundSaveGameDataPacket.cs b/Multiplayer/Networking/Packets/Clientbound/ClientboundSaveGameDataPacket.cs index 6ecf96d..551bb1d 100644 --- a/Multiplayer/Networking/Packets/Clientbound/ClientboundSaveGameDataPacket.cs +++ b/Multiplayer/Networking/Packets/Clientbound/ClientboundSaveGameDataPacket.cs @@ -43,6 +43,17 @@ public static ClientboundSaveGameDataPacket CreatePacket(ServerPlayer player) JObject playerData = NetworkedSaveGameManager.Instance.Server_GetPlayerData(data, player.Guid); + Multiplayer.LogDebug(() => + { + string unlockedGen = string.Join(", ", UnlockablesManager.Instance.UnlockedGeneralLicenses); + string packetGen = string.Join(", ", data.GetStringArray(SaveGameKeys.Licenses_General)); + + string unlockedJob = string.Join(", ", UnlockablesManager.Instance.UnlockedJobLicenses); + string packetJob = string.Join(", ", data.GetStringArray(SaveGameKeys.Licenses_Jobs)); + + return $"ClientboundSaveGameDataPacket.CreatePacket() UnlockedGen: {{{unlockedGen}}}, PacketGen: {{{packetGen}}}, UnlockedJob: {{{unlockedJob}}}, PacketJob: {{{packetJob}}}"; + }); + return new ClientboundSaveGameDataPacket { GameMode = data.GetString(SaveGameKeys.Game_mode), SerializedDifficulty = difficulty.ToString(Formatting.None), diff --git a/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundFireboxStatePacket.cs b/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundFireboxStatePacket.cs index 1055146..bbf0250 100644 --- a/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundFireboxStatePacket.cs +++ b/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundFireboxStatePacket.cs @@ -4,6 +4,5 @@ public class ClientboundFireboxStatePacket { public ushort NetId { get; set; } public float Contents { get; set; } - - public bool IsOn { get; set; } + public bool IsOn { get; set; } } diff --git a/Multiplayer/Patches/MainMenu/LauncherControllerPatch.cs b/Multiplayer/Patches/MainMenu/LauncherControllerPatch.cs index 49c3469..98a67b1 100644 --- a/Multiplayer/Patches/MainMenu/LauncherControllerPatch.cs +++ b/Multiplayer/Patches/MainMenu/LauncherControllerPatch.cs @@ -27,7 +27,7 @@ public static class LauncherController_Patch private static void OnEnable(LauncherController __instance) { - Multiplayer.Log("LauncherController_Patch()"); + //Multiplayer.Log("LauncherController_Patch()"); if (goHost != null) return; @@ -60,7 +60,7 @@ private static void OnEnable(LauncherController __instance) goHost.SetActive(true); - Multiplayer.Log("LauncherController_Patch() complete"); + //Multiplayer.Log("LauncherController_Patch() complete"); } } diff --git a/Multiplayer/Patches/MainMenu/RightPaneControllerPatch.cs b/Multiplayer/Patches/MainMenu/RightPaneControllerPatch.cs index 2767569..99b491e 100644 --- a/Multiplayer/Patches/MainMenu/RightPaneControllerPatch.cs +++ b/Multiplayer/Patches/MainMenu/RightPaneControllerPatch.cs @@ -59,7 +59,7 @@ private static void Prefix(RightPaneController __instance) // Activate the multiplayer button MainMenuController_Awake_Patch.multiplayerButton.SetActive(true); - Multiplayer.Log("At end!"); + //Multiplayer.Log("At end!"); // Check if the host pane already exists if (__instance.HasChildWithName("PaneRight Host")) diff --git a/Multiplayer/Patches/Train/HoseAndCockPatch.cs b/Multiplayer/Patches/Train/HoseAndCockPatch.cs index 8b0f67d..ae4e70f 100644 --- a/Multiplayer/Patches/Train/HoseAndCockPatch.cs +++ b/Multiplayer/Patches/Train/HoseAndCockPatch.cs @@ -16,8 +16,8 @@ private static void Prefix(HoseAndCock __instance, bool open) if(!NetworkedTrainCar.TryGetCoupler(__instance, out Coupler coupler)) { - TrainCar me = TrainCar.Resolve(__instance?.parentSystem?.gameObject); - Multiplayer.LogError($"HoseAndCock.SetCock() Coupler not found! - Cars may be getting destroyed on load? TrainCar ID: {me?.ID}"); + //TrainCar me = TrainCar.Resolve(__instance?.parentSystem?.gameObject); + //Multiplayer.LogError($"HoseAndCock.SetCock() Coupler not found! - Cars may be getting destroyed on load? TrainCar ID: {me?.ID}"); } if (coupler == null || !coupler.train.TryNetworked(out NetworkedTrainCar networkedTrainCar)) From dd518705ba7d84c917055bf01ada2ca6b1649243 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 21 Dec 2024 22:15:05 +1000 Subject: [PATCH 132/188] Refactor Networking components --- .../Train/NetworkTrainsetWatcher.cs | 3 +- .../Networking/Train/NetworkedBogie.cs | 2 +- .../Networking/Train/NetworkedCarSpawner.cs | 2 +- .../Networking/Train/NetworkedRigidbody.cs | 60 ++++++++++++++ .../Networking/Train/NetworkedTrainCar.cs | 4 +- .../Networking/World/NetworkedItemManager.cs | 6 ++ .../Networking/World/NetworkedRigidbody.cs | 41 ---------- Multiplayer/Networking/Data/BogieData.cs | 54 ------------- .../Networking/Data/Train/BogieData.cs | 81 +++++++++++++++++++ .../Data/{ => Train}/RigidbodySnapshot.cs | 8 +- .../Data/{ => Train}/TrainsetMovementPart.cs | 64 ++++++++------- .../Managers/Client/NetworkClient.cs | 1 + .../Networking/Managers/NetworkManager.cs | 3 +- .../Train/ClientboundSpawnTrainCarPacket.cs | 2 +- .../Train/ClientboundSpawnTrainSetPacket.cs | 2 +- .../Train/ClientboundTrainsetPhysicsPacket.cs | 2 +- 16 files changed, 196 insertions(+), 139 deletions(-) create mode 100644 Multiplayer/Components/Networking/Train/NetworkedRigidbody.cs delete mode 100644 Multiplayer/Components/Networking/World/NetworkedRigidbody.cs delete mode 100644 Multiplayer/Networking/Data/BogieData.cs create mode 100644 Multiplayer/Networking/Data/Train/BogieData.cs rename Multiplayer/Networking/Data/{ => Train}/RigidbodySnapshot.cs (95%) rename Multiplayer/Networking/Data/{ => Train}/TrainsetMovementPart.cs (62%) diff --git a/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs b/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs index b3705fd..32c9f68 100644 --- a/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs +++ b/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs @@ -1,11 +1,10 @@ -using System.Collections.Generic; using System.Linq; using DV.Utils; using UnityEngine; using JetBrains.Annotations; -using Multiplayer.Networking.Data; using Multiplayer.Networking.Packets.Clientbound.Train; using Multiplayer.Utils; +using Multiplayer.Networking.Data.Train; namespace Multiplayer.Components.Networking.Train; diff --git a/Multiplayer/Components/Networking/Train/NetworkedBogie.cs b/Multiplayer/Components/Networking/Train/NetworkedBogie.cs index 6da72fd..943cd21 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedBogie.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedBogie.cs @@ -1,5 +1,5 @@ using Multiplayer.Components.Networking.World; -using Multiplayer.Networking.Data; +using Multiplayer.Networking.Data.Train; using UnityEngine; namespace Multiplayer.Components.Networking.Train; diff --git a/Multiplayer/Components/Networking/Train/NetworkedCarSpawner.cs b/Multiplayer/Components/Networking/Train/NetworkedCarSpawner.cs index 1fa9d44..b52e046 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedCarSpawner.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedCarSpawner.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using DV.ThingTypes; using Multiplayer.Components.Networking.World; -using Multiplayer.Networking.Data; +using Multiplayer.Networking.Data.Train; using Multiplayer.Utils; using UnityEngine; diff --git a/Multiplayer/Components/Networking/Train/NetworkedRigidbody.cs b/Multiplayer/Components/Networking/Train/NetworkedRigidbody.cs new file mode 100644 index 0000000..f0bd46e --- /dev/null +++ b/Multiplayer/Components/Networking/Train/NetworkedRigidbody.cs @@ -0,0 +1,60 @@ +using Multiplayer.Networking.Data.Train; +using System; +using System.Collections; +using UnityEngine; + +namespace Multiplayer.Components.Networking.Train; + +public class NetworkedRigidbody : TickedQueue +{ + private const int MAX_FRAMES = 60; + private Rigidbody rigidbody; + + protected override void OnEnable() + { + StartCoroutine(WaitForRB()); + } + + protected IEnumerator WaitForRB() + { + int counter = 0; + + while (rigidbody == null && counter < MAX_FRAMES) + { + rigidbody = GetComponent(); + if (rigidbody == null) + { + counter++; + yield return new WaitForEndOfFrame(); + } + } + + base.OnEnable(); + + if (rigidbody == null) + { + gameObject.TryGetComponent(out TrainCar car); + + Multiplayer.LogError($"{gameObject.name} ({car?.ID}): {nameof(NetworkedBogie)} requires a {nameof(Bogie)} component on the same GameObject! Waited {counter} iterations"); + } + } + + protected override void Process(RigidbodySnapshot snapshot, uint snapshotTick) + { + if (snapshot == null) + { + Multiplayer.LogError($"NetworkedRigidBody.Process() Snapshot NULL!"); + return; + } + + try + { + Multiplayer.LogDebug(() => $"NetworkedRigidBody.Process() {snapshot.IncludedDataFlags}, {snapshot.Position.ToString() ?? "null"}, {snapshot.Rotation.ToString() ?? "null"}, {snapshot.Velocity.ToString() ?? "null"}, {snapshot.AngularVelocity.ToString() ?? "null"}"); + snapshot.Apply(rigidbody); + } + catch (Exception ex) + { + Multiplayer.LogError($"NetworkedRigidBody.Process() {ex.Message}\r\n {ex.StackTrace}"); + } + } +} diff --git a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs index 247b3a7..9807828 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs @@ -7,8 +7,8 @@ using LocoSim.Definitions; using LocoSim.Implementations; using Multiplayer.Components.Networking.Player; -using Multiplayer.Components.Networking.World; using Multiplayer.Networking.Data; +using Multiplayer.Networking.Data.Train; using Multiplayer.Networking.Packets.Common.Train; using Multiplayer.Utils; using UnityEngine; @@ -655,7 +655,7 @@ public void Client_ReceiveTrainPhysicsUpdate(in TrainsetMovementPart movementPar else { //move the car to the correct position first - maybe? - if (movementPart.typeFlag.HasFlag(TrainsetMovementPart.MovementType.Sync)) + if (movementPart.typeFlag.HasFlag(TrainsetMovementPart.MovementType.Position)) { /* float d1 = (TrainCar.transform.position - (movementPart.Position + WorldMover.currentMove)).sqrMagnitude; diff --git a/Multiplayer/Components/Networking/World/NetworkedItemManager.cs b/Multiplayer/Components/Networking/World/NetworkedItemManager.cs index 3e938ca..6e22d15 100644 --- a/Multiplayer/Components/Networking/World/NetworkedItemManager.cs +++ b/Multiplayer/Components/Networking/World/NetworkedItemManager.cs @@ -181,6 +181,12 @@ private void UpdatePlayerItemLists() foreach (var item in allItems) { + if (item == null) + { + NetworkLifecycle.Instance.Server.LogDebug(() => $"UpdatePlayerItemLists() Null item found in allItems!"); + continue; + } + float sqrDistance = (player.WorldPosition - item.transform.position).sqrMagnitude; if (sqrDistance <= MAX_DISTANCE_TO_ITEM_SQR) diff --git a/Multiplayer/Components/Networking/World/NetworkedRigidbody.cs b/Multiplayer/Components/Networking/World/NetworkedRigidbody.cs deleted file mode 100644 index ff62745..0000000 --- a/Multiplayer/Components/Networking/World/NetworkedRigidbody.cs +++ /dev/null @@ -1,41 +0,0 @@ -using Multiplayer.Networking.Data; -using System; -using UnityEngine; - -namespace Multiplayer.Components.Networking.World; - -public class NetworkedRigidbody : TickedQueue -{ - private Rigidbody rigidbody; - - protected override void OnEnable() - { - rigidbody = GetComponent(); - if (rigidbody == null) - { - Multiplayer.LogError($"{gameObject.name}: {nameof(NetworkedRigidbody)} requires a {nameof(Rigidbody)} component on the same GameObject!"); - return; - } - - base.OnEnable(); - } - - protected override void Process(RigidbodySnapshot snapshot, uint snapshotTick) - { - if (snapshot == null) - { - Multiplayer.LogError($"NetworkedRigidBody.Process() Snapshot NULL!"); - return; - } - - try - { - Multiplayer.LogDebug(()=>$"NetworkedRigidBody.Process() {snapshot.IncludedDataFlags}, {snapshot.Position.ToString() ?? "null"}, {snapshot.Rotation.ToString() ?? "null"}, {snapshot.Velocity.ToString() ?? "null"}, {snapshot.AngularVelocity.ToString() ?? "null"}"); - snapshot.Apply(rigidbody); - } - catch (Exception ex) - { - Multiplayer.LogError($"NetworkedRigidBody.Process() {ex.Message}\r\n {ex.StackTrace}"); - } - } -} diff --git a/Multiplayer/Networking/Data/BogieData.cs b/Multiplayer/Networking/Data/BogieData.cs deleted file mode 100644 index 33f6851..0000000 --- a/Multiplayer/Networking/Data/BogieData.cs +++ /dev/null @@ -1,54 +0,0 @@ -using LiteNetLib.Utils; -using Multiplayer.Utils; - -namespace Multiplayer.Networking.Data; - -public readonly struct BogieData -{ - private readonly byte PackedBools; - public readonly double PositionAlongTrack; - public readonly ushort TrackNetId; - public readonly int TrackDirection; - - public bool IncludesTrackData => (PackedBools & 1) != 0; - public bool HasDerailed => (PackedBools & 2) != 0; - - private BogieData(byte packedBools, double positionAlongTrack, ushort trackNetId, int trackDirection) - { - PackedBools = packedBools; - PositionAlongTrack = positionAlongTrack; - TrackNetId = trackNetId; - TrackDirection = trackDirection; - } - - public static BogieData FromBogie(Bogie bogie, bool includeTrack, int trackDirection) - { - bool includesTrackData = includeTrack && !bogie.HasDerailed && bogie.track; - return new BogieData( - (byte)((includesTrackData ? 1 : 0) | (bogie.HasDerailed ? 2 : 0)), - bogie.traveller?.Span ?? -1.0, - includesTrackData ? bogie.track.Networked().NetId : (ushort)0, - trackDirection - ); - } - - public static void Serialize(NetDataWriter writer, BogieData data) - { - writer.Put(data.PackedBools); - if (!data.HasDerailed) writer.Put(data.PositionAlongTrack); - if (!data.IncludesTrackData) return; - writer.Put(data.TrackNetId); - writer.Put(data.TrackDirection); - } - - public static BogieData Deserialize(NetDataReader reader) - { - byte packedBools = reader.GetByte(); - bool includesTrackData = (packedBools & 1) != 0; - bool hasDerailed = (packedBools & 2) != 0; - double positionAlongTrack = !hasDerailed ? reader.GetDouble() : -1.0; - ushort trackNetId = includesTrackData ? reader.GetUShort() : (ushort)0; - int trackDirection = includesTrackData ? reader.GetInt() : 0; - return new BogieData(packedBools, positionAlongTrack, trackNetId, trackDirection); - } -} diff --git a/Multiplayer/Networking/Data/Train/BogieData.cs b/Multiplayer/Networking/Data/Train/BogieData.cs new file mode 100644 index 0000000..a4670ae --- /dev/null +++ b/Multiplayer/Networking/Data/Train/BogieData.cs @@ -0,0 +1,81 @@ +using LiteNetLib.Utils; +using Multiplayer.Utils; +using System; + +namespace Multiplayer.Networking.Data.Train; + +[Flags] +public enum BogieFlags : byte +{ + None = 0, + IncludesTrackData = 1, + HasDerailed = 2 +} +public readonly struct BogieData +{ + private readonly BogieFlags DataFlags; + public readonly double PositionAlongTrack; + public readonly ushort TrackNetId; + public readonly int TrackDirection; + + public bool IncludesTrackData => DataFlags.HasFlag(BogieFlags.IncludesTrackData); + public bool HasDerailed => DataFlags.HasFlag(BogieFlags.HasDerailed); + + private BogieData(BogieFlags flags, double positionAlongTrack, ushort trackNetId, int trackDirection) + { + // Prevent invalid state combinations + if (flags.HasFlag(BogieFlags.HasDerailed)) + flags &= ~BogieFlags.IncludesTrackData; // Clear track data flag if derailed + + DataFlags = flags; + PositionAlongTrack = positionAlongTrack; + TrackNetId = trackNetId; + TrackDirection = trackDirection; + } + + public static BogieData FromBogie(Bogie bogie, bool includeTrack) + { + bool includesTrackData = includeTrack && !bogie.HasDerailed && bogie.track; + + BogieFlags flags = BogieFlags.None; + if (includesTrackData) flags |= BogieFlags.IncludesTrackData; + if (bogie.HasDerailed) flags |= BogieFlags.HasDerailed; + + return new BogieData( + flags, + bogie.traveller?.Span ?? -1.0, + includesTrackData ? bogie.track.Networked().NetId : (ushort)0, + bogie.trackDirection + ); + } + + public static void Serialize(NetDataWriter writer, BogieData data) + { + writer.Put((byte)data.DataFlags); + if (!data.HasDerailed) writer.Put(data.PositionAlongTrack); + if (!data.IncludesTrackData) return; + writer.Put(data.TrackNetId); + writer.Put(data.TrackDirection); + } + + public static BogieData Deserialize(NetDataReader reader) + { + BogieFlags flags = (BogieFlags)reader.GetByte(); + + // Read position if not derailed + double positionAlongTrack = !flags.HasFlag(BogieFlags.HasDerailed) + ? reader.GetDouble() + : -1.0; + + // Read track data if included + ushort trackNetId = 0; + int trackDirection = 0; + if (flags.HasFlag(BogieFlags.IncludesTrackData)) + { + trackNetId = reader.GetUShort(); + trackDirection = reader.GetInt(); + } + + return new BogieData(flags, positionAlongTrack, trackNetId, trackDirection); + } +} diff --git a/Multiplayer/Networking/Data/RigidbodySnapshot.cs b/Multiplayer/Networking/Data/Train/RigidbodySnapshot.cs similarity index 95% rename from Multiplayer/Networking/Data/RigidbodySnapshot.cs rename to Multiplayer/Networking/Data/Train/RigidbodySnapshot.cs index 445207a..00af0e7 100644 --- a/Multiplayer/Networking/Data/RigidbodySnapshot.cs +++ b/Multiplayer/Networking/Data/Train/RigidbodySnapshot.cs @@ -3,7 +3,7 @@ using Multiplayer.Networking.Serialization; using UnityEngine; -namespace Multiplayer.Networking.Data; +namespace Multiplayer.Networking.Data.Train; public class RigidbodySnapshot { @@ -35,7 +35,8 @@ public static RigidbodySnapshot Deserialize(NetDataReader reader) { IncludedData IncludedDataFlags = (IncludedData)reader.GetByte(); - RigidbodySnapshot snapshot = new() { + RigidbodySnapshot snapshot = new() + { IncludedDataFlags = (byte)IncludedDataFlags }; @@ -56,7 +57,8 @@ public static RigidbodySnapshot Deserialize(NetDataReader reader) public static RigidbodySnapshot From(Rigidbody rb, IncludedData includedDataFlags = IncludedData.All) { - RigidbodySnapshot snapshot = new() { + RigidbodySnapshot snapshot = new() + { IncludedDataFlags = (byte)includedDataFlags }; diff --git a/Multiplayer/Networking/Data/TrainsetMovementPart.cs b/Multiplayer/Networking/Data/Train/TrainsetMovementPart.cs similarity index 62% rename from Multiplayer/Networking/Data/TrainsetMovementPart.cs rename to Multiplayer/Networking/Data/Train/TrainsetMovementPart.cs index 0c74da5..e6bb90c 100644 --- a/Multiplayer/Networking/Data/TrainsetMovementPart.cs +++ b/Multiplayer/Networking/Data/Train/TrainsetMovementPart.cs @@ -2,7 +2,7 @@ using Multiplayer.Networking.Serialization; using System; using UnityEngine; -namespace Multiplayer.Networking.Data; +namespace Multiplayer.Networking.Data.Train; public readonly struct TrainsetMovementPart { @@ -20,7 +20,7 @@ public enum MovementType : byte { Physics = 1, RigidBody = 2, - Sync = 4 + Position = 4 } public TrainsetMovementPart(float speed, float slowBuildUpStress, BogieData bogie1, BogieData bogie2, Vector3? position = null, Quaternion? rotation = null) @@ -32,11 +32,11 @@ public TrainsetMovementPart(float speed, float slowBuildUpStress, BogieData bogi Bogie1 = bogie1; Bogie2 = bogie2; - if(position != null && rotation != null) + if (position != null && rotation != null) { //Multiplayer.LogDebug(()=>$"new TrainsetMovementPart() Sync"); - typeFlag |= MovementType.Sync; //includes positional data + typeFlag |= MovementType.Position; //includes positional data Position = (Vector3)position; Rotation = (Quaternion)rotation; @@ -52,26 +52,27 @@ public TrainsetMovementPart(RigidbodySnapshot rigidbodySnapshot) RigidbodySnapshot = rigidbodySnapshot; } -#pragma warning disable EPS05 public static void Serialize(NetDataWriter writer, TrainsetMovementPart data) -#pragma warning restore EPS05 { writer.Put((byte)data.typeFlag); //Multiplayer.LogDebug(() => $"TrainsetMovementPart.Serialize() {data.typeFlag}"); - if (data.typeFlag == MovementType.RigidBody) + if (data.typeFlag.HasFlag(MovementType.RigidBody)) { RigidbodySnapshot.Serialize(writer, data.RigidbodySnapshot); return; } - writer.Put(data.Speed); - writer.Put(data.SlowBuildUpStress); - BogieData.Serialize(writer, data.Bogie1); - BogieData.Serialize(writer, data.Bogie2); + if (data.typeFlag.HasFlag(MovementType.Physics)) + { + writer.Put(data.Speed); + writer.Put(data.SlowBuildUpStress); + BogieData.Serialize(writer, data.Bogie1); + BogieData.Serialize(writer, data.Bogie2); + } - if (data.typeFlag.HasFlag(MovementType.Sync)) //serialise positional data + if (data.typeFlag.HasFlag(MovementType.Position)) { Vector3Serializer.Serialize(writer, data.Position); QuaternionSerializer.Serialize(writer, data.Rotation); @@ -80,31 +81,34 @@ public static void Serialize(NetDataWriter writer, TrainsetMovementPart data) public static TrainsetMovementPart Deserialize(NetDataReader reader) { - MovementType dataType = (MovementType)reader.GetByte(); + float speed = 0; + float slowBuildUpStress = 0; + Vector3? position = null; + Quaternion? rotation = null; + BogieData bd1 = default; + BogieData bd2 = default; - //Multiplayer.LogDebug(() => $"TrainsetMovementPart.Deserialize() {dataType}"); + MovementType dataType = (MovementType)reader.GetByte(); - if (dataType == MovementType.RigidBody) + if (dataType.HasFlag(MovementType.RigidBody)) { return new TrainsetMovementPart(RigidbodySnapshot.Deserialize(reader)); } - else - { - float speed = reader.GetFloat(); - float slowBuildUpStress = reader.GetFloat(); - BogieData bd1 = BogieData.Deserialize(reader); - BogieData bd2 = BogieData.Deserialize(reader); - Vector3? position = null; - Quaternion? rotation = null; - - if (dataType.HasFlag(MovementType.Sync)) - { - position = Vector3Serializer.Deserialize(reader); - rotation = QuaternionSerializer.Deserialize(reader); - } + if (dataType.HasFlag(MovementType.Physics)) + { + speed = reader.GetFloat(); + slowBuildUpStress = reader.GetFloat(); + bd1 = BogieData.Deserialize(reader); + bd2 = BogieData.Deserialize(reader); + } - return new TrainsetMovementPart(speed, slowBuildUpStress, bd1, bd2, position, rotation); + if (dataType.HasFlag(MovementType.Position)) + { + position = Vector3Serializer.Deserialize(reader); + rotation = QuaternionSerializer.Deserialize(reader); } + + return new TrainsetMovementPart(speed, slowBuildUpStress, bd1, bd2, position, rotation); } } diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 011dfae..d8ff87c 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -40,6 +40,7 @@ using LiteNetLib.Utils; using DV.UserManagement; using DV.Common; +using Multiplayer.Networking.Data.Train; namespace Multiplayer.Networking.Listeners; diff --git a/Multiplayer/Networking/Managers/NetworkManager.cs b/Multiplayer/Networking/Managers/NetworkManager.cs index 0848124..2db9e8e 100644 --- a/Multiplayer/Networking/Managers/NetworkManager.cs +++ b/Multiplayer/Networking/Managers/NetworkManager.cs @@ -1,11 +1,10 @@ using System; -using System.Collections.Generic; using System.Net; using System.Net.Sockets; using LiteNetLib; using LiteNetLib.Utils; using Multiplayer.Networking.Data; -using Multiplayer.Networking.Packets.Common; +using Multiplayer.Networking.Data.Train; using Multiplayer.Networking.Serialization; namespace Multiplayer.Networking.Listeners; diff --git a/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundSpawnTrainCarPacket.cs b/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundSpawnTrainCarPacket.cs index 122de31..0d69e5f 100644 --- a/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundSpawnTrainCarPacket.cs +++ b/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundSpawnTrainCarPacket.cs @@ -1,5 +1,5 @@ using Multiplayer.Components.Networking.Train; -using Multiplayer.Networking.Data; +using Multiplayer.Networking.Data.Train; namespace Multiplayer.Networking.Packets.Clientbound.Train; diff --git a/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundSpawnTrainSetPacket.cs b/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundSpawnTrainSetPacket.cs index e81d535..1d38dc2 100644 --- a/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundSpawnTrainSetPacket.cs +++ b/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundSpawnTrainSetPacket.cs @@ -1,4 +1,4 @@ -using Multiplayer.Networking.Data; +using Multiplayer.Networking.Data.Train; namespace Multiplayer.Networking.Packets.Clientbound.Train; diff --git a/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundTrainsetPhysicsPacket.cs b/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundTrainsetPhysicsPacket.cs index aee1b0f..dd3f41d 100644 --- a/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundTrainsetPhysicsPacket.cs +++ b/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundTrainsetPhysicsPacket.cs @@ -1,4 +1,4 @@ -using Multiplayer.Networking.Data; +using Multiplayer.Networking.Data.Train; namespace Multiplayer.Networking.Packets.Clientbound.Train; From e2645f026abc0e1c16441a825030a34d5e5df8c1 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 22 Dec 2024 11:40:21 +1000 Subject: [PATCH 133/188] Fixed issue with overloading cargo on cars This issue causes sync problems and car explosions --- .../Networking/Train/NetworkedTrainCar.cs | 2 +- .../Managers/Client/NetworkClient.cs | 46 ++++++++++++++++++- 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs index 9807828..b0e3065 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs @@ -95,7 +95,7 @@ public static bool TryGetFromTrainCar(TrainCar trainCar, out NetworkedTrainCar n #region Client - private bool client_Initialized; + public bool Client_Initialized {get; private set;} public TickedQueue Client_trainSpeedQueue; public TickedQueue Client_trainRigidbodyQueue; public TickedQueue client_bogie1Queue; diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index d8ff87c..2be965e 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -672,17 +672,59 @@ private void OnClientboundCargoStatePacket(ClientboundCargoStatePacket packet) networkedTrainCar.CargoModelIndex = packet.CargoModelIndex; Car logicCar = networkedTrainCar.TrainCar.logicCar; + if (logicCar == null) + { + Multiplayer.LogWarning($"OnClientboundCargoStatePacket() Failed to find logic car for [{networkedTrainCar.TrainCar.ID}, {packet.NetId}] is initialised: {networkedTrainCar.Client_Initialized}"); + return; + } + if (packet.CargoType == (ushort)CargoType.None && logicCar.CurrentCargoTypeInCar == CargoType.None) return; + //packet.CargoAmount is the total amount, not the amount to load/unload float cargoAmount = Mathf.Clamp(packet.CargoAmount, 0, logicCar.capacity); // todo: cache warehouse machine WarehouseMachine warehouse = string.IsNullOrEmpty(packet.WarehouseMachineId) ? null : JobSaveManager.Instance.GetWarehouseMachineWithId(packet.WarehouseMachineId); if (packet.IsLoading) - logicCar.LoadCargo(cargoAmount, (CargoType)packet.CargoType, warehouse); + { + //Check correct cargo is loaded and the amount is correct + if (logicCar.LoadedCargoAmount == cargoAmount && logicCar.CurrentCargoTypeInCar == (CargoType)packet.CargoType) + return; + + //We need either no cargo or the same cargo - if it's different, we need to remove it first + if (logicCar.CurrentCargoTypeInCar != CargoType.None && logicCar.CurrentCargoTypeInCar != (CargoType)packet.CargoType) + logicCar.DumpCargo(); + + //We have the correct cargo, but not the right amount, calculate the delta + if (logicCar.CurrentCargoTypeInCar == (CargoType)packet.CargoType) + cargoAmount = cargoAmount - logicCar.LoadedCargoAmount; + + if(cargoAmount > 0) + logicCar.LoadCargo(cargoAmount, (CargoType)packet.CargoType, warehouse); + } else - logicCar.UnloadCargo(cargoAmount, (CargoType)packet.CargoType, warehouse); + { + //Check correct cargo is loaded and the amount is correct + if (logicCar.LoadedCargoAmount == cargoAmount && logicCar.CurrentCargoTypeInCar == (CargoType)packet.CargoType) + return; + + //If there is different cargo we need to remove it, then load the appropriate amount + if (logicCar.CurrentCargoTypeInCar == CargoType.None || logicCar.CurrentCargoTypeInCar != (CargoType)packet.CargoType) + { + //avoid triggering the load event by backdooring it + logicCar.LastUnloadedCargoType = logicCar.CurrentCargoTypeInCar; + logicCar.CurrentCargoTypeInCar = (CargoType)packet.CargoType; + logicCar.LoadedCargoAmount = cargoAmount; + } + + //We have the correct cargo, calculate the delta + if (logicCar.CurrentCargoTypeInCar == (CargoType)packet.CargoType) + cargoAmount = logicCar.LoadedCargoAmount - cargoAmount; + + if (cargoAmount > 0) + logicCar.UnloadCargo(cargoAmount, (CargoType)packet.CargoType, warehouse); + } } private void OnClientboundCarHealthUpdatePacket(ClientboundCarHealthUpdatePacket packet) From 2dea876ffa052dec9074bed99b522902ce06a17a Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 22 Dec 2024 11:43:02 +1000 Subject: [PATCH 134/188] Added initialisation delay to NetworkedBogie Coroutine added to ensure the bogie has instantiated as sometimes it's not fully loaded and can't be found --- .../Networking/Train/NetworkedBogie.cs | 42 +++++++++++++++---- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/Multiplayer/Components/Networking/Train/NetworkedBogie.cs b/Multiplayer/Components/Networking/Train/NetworkedBogie.cs index 943cd21..c316948 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedBogie.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedBogie.cs @@ -1,23 +1,40 @@ using Multiplayer.Components.Networking.World; using Multiplayer.Networking.Data.Train; +using System.Collections; using UnityEngine; namespace Multiplayer.Components.Networking.Train; public class NetworkedBogie : TickedQueue { + private const int MAX_FRAMES = 60; private Bogie bogie; protected override void OnEnable() { - bogie = GetComponent(); - if (bogie == null) + StartCoroutine(WaitForBogie()); + } + + protected IEnumerator WaitForBogie() + { + int counter = 0; + + while (bogie == null && counter < MAX_FRAMES) { - Multiplayer.LogError($"{gameObject.name}: {nameof(NetworkedBogie)} requires a {nameof(Bogie)} component on the same GameObject!"); - return; + bogie = GetComponent(); + if (bogie == null) + { + counter++; + yield return new WaitForEndOfFrame(); + } } base.OnEnable(); + + if (bogie == null) + { + Multiplayer.LogError($"{gameObject.name} ({bogie?.Car?.ID}): {nameof(NetworkedBogie)} requires a {nameof(Bogie)} component on the same GameObject! Waited {counter} iterations"); + } } protected override void Process(BogieData snapshot, uint snapshotTick) @@ -25,7 +42,7 @@ protected override void Process(BogieData snapshot, uint snapshotTick) if (bogie.HasDerailed) return; - if (snapshot.HasDerailed || !bogie.track) + if (snapshot.HasDerailed) { bogie.Derail(); return; @@ -33,12 +50,21 @@ protected override void Process(BogieData snapshot, uint snapshotTick) if (snapshot.IncludesTrackData) { - if (NetworkedRailTrack.Get(snapshot.TrackNetId, out NetworkedRailTrack track)) - bogie.SetTrack(track.RailTrack, snapshot.PositionAlongTrack, snapshot.TrackDirection); + if (!NetworkedRailTrack.Get(snapshot.TrackNetId, out NetworkedRailTrack track)) + { + Multiplayer.LogWarning($"NetworkedBogie.Process() Failed to find track {snapshot.TrackNetId} for bogie: {bogie.Car.ID}"); + return; + } + + bogie.SetTrack(track.RailTrack, snapshot.PositionAlongTrack, snapshot.TrackDirection); + } else { - bogie.traveller.MoveToSpan(snapshot.PositionAlongTrack); + if(bogie.track) + bogie.traveller.MoveToSpan(snapshot.PositionAlongTrack); + else + Multiplayer.LogWarning($"NetworkedBogie.Process() No track for current bogie for bogie: {bogie?.Car?.ID}, unable to move position!"); } int physicsSteps = Mathf.FloorToInt((NetworkLifecycle.Instance.Tick - (float)snapshotTick) / NetworkLifecycle.TICK_RATE / Time.fixedDeltaTime) + 1; From 9891cf9b842a57a6de9210471f0963f597ec643a Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 22 Dec 2024 12:09:37 +1000 Subject: [PATCH 135/188] Re-wrote car spawning method and data structures * Removed redundant code and replaced with calls to standard game methods where possible. * Used same logic as game where game methods didn't quite suit e.g. `Coupler.InitFromSave()` uses `TryCouple()`, but we want an "exact" couple using `CoupleTo()` * Added initial state data for brakes, couplings and hoses as the coupling and hose data packets were not reliable at forming couplings and will be migrated to action/RPC based methods (due to B99 changes) --- .../Train/NetworkTrainsetWatcher.cs | 31 +- .../Networking/Train/NetworkedCarSpawner.cs | 195 +++++--- .../Networking/Train/NetworkedTrainCar.cs | 26 +- .../Data/Train/TrainsetSpawnPart.cs | 439 ++++++++++++++++++ .../Networking/Data/TrainsetSpawnPart.cs | 119 ----- .../Managers/Client/NetworkClient.cs | 2 +- .../Managers/Server/NetworkServer.cs | 53 ++- .../Train/ClientboundSpawnTrainSetPacket.cs | 16 +- .../Train/ClientboundTrainsetPhysicsPacket.cs | 3 +- Multiplayer/Patches/Train/BogiePatch.cs | 14 - Multiplayer/Patches/Train/CarSpawnerPatch.cs | 28 +- 11 files changed, 688 insertions(+), 238 deletions(-) create mode 100644 Multiplayer/Networking/Data/Train/TrainsetSpawnPart.cs delete mode 100644 Multiplayer/Networking/Data/TrainsetSpawnPart.cs diff --git a/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs b/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs index 32c9f68..08c0b29 100644 --- a/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs +++ b/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs @@ -42,10 +42,9 @@ private void Server_OnTick(uint tick) cachedSendPacket.Tick = tick; foreach (Trainset set in Trainset.allSets) - Server_TickSet(set); + Server_TickSet(set, tick); } - - private void Server_TickSet(Trainset set) + private void Server_TickSet(Trainset set, uint tick) { bool anyCarMoving = false; bool maxTicksReached = false; @@ -57,9 +56,10 @@ private void Server_TickSet(Trainset set) return; } - cachedSendPacket.NetId = set.firstCar.GetNetId(); + cachedSendPacket.FirstNetId = set.firstCar.GetNetId(); + cachedSendPacket.LastNetId = set.lastCar.GetNetId(); //car may not be initialised, missing a valid NetID - if (cachedSendPacket.NetId == 0) + if (cachedSendPacket.FirstNetId == 0 || cachedSendPacket.LastNetId == 0) return; foreach (TrainCar trainCar in set.cars) @@ -73,7 +73,7 @@ private void Server_TickSet(Trainset set) //If we can locate the networked car, we'll add to the ticks counter and check if any tracks are dirty if (NetworkedTrainCar.TryGetFromTrainCar(trainCar, out NetworkedTrainCar netTC)) { - maxTicksReached |= netTC.TicksSinceSync >= MAX_UNSYNC_TICKS; + maxTicksReached |= netTC.TicksSinceSync >= MAX_UNSYNC_TICKS; anyTracksDirty |= netTC.BogieTracksDirty; } @@ -123,8 +123,8 @@ private void Server_TickSet(Trainset set) trainsetParts[i] = new TrainsetMovementPart( trainCar.GetForwardSpeed(), trainCar.stress.slowBuildUpStress, - BogieData.FromBogie(trainCar.Bogies[0], networkedTrainCar.BogieTracksDirty, networkedTrainCar.Bogie1TrackDirection), - BogieData.FromBogie(trainCar.Bogies[1], networkedTrainCar.BogieTracksDirty, networkedTrainCar.Bogie2TrackDirection), + BogieData.FromBogie(trainCar.Bogies[0], networkedTrainCar.BogieTracksDirty), + BogieData.FromBogie(trainCar.Bogies[1], networkedTrainCar.BogieTracksDirty), position, //only used in full sync rotation //only used in full sync ); @@ -142,20 +142,27 @@ private void Server_TickSet(Trainset set) public void Client_HandleTrainsetPhysicsUpdate(ClientboundTrainsetPhysicsPacket packet) { - Trainset set = Trainset.allSets.Find(set => set.firstCar.GetNetId() == packet.NetId || set.lastCar.GetNetId() == packet.NetId); + Trainset set = Trainset.allSets.Find(set => set.firstCar.GetNetId() == packet.FirstNetId || set.lastCar.GetNetId() == packet.FirstNetId || + set.firstCar.GetNetId() == packet.LastNetId || set.lastCar.GetNetId() == packet.LastNetId); + if (set == null) { - Multiplayer.LogDebug(() => $"Received {nameof(ClientboundTrainsetPhysicsPacket)} for unknown trainset with netId {packet.NetId}"); + Multiplayer.LogWarning($"Received {nameof(ClientboundTrainsetPhysicsPacket)} for unknown trainset with FirstNetId: {packet.FirstNetId} and LastNetId: {packet.LastNetId}"); return; } if (set.cars.Count != packet.TrainsetParts.Length) { - Multiplayer.LogDebug(() => - $"Received {nameof(ClientboundTrainsetPhysicsPacket)} for trainset with netId {packet.NetId} with {packet.TrainsetParts.Length} parts, but trainset has {set.cars.Count} parts"); + //log the discrepancies + Multiplayer.LogWarning( + $"Received {nameof(ClientboundTrainsetPhysicsPacket)} for trainset with FirstNetId: {packet.FirstNetId} and LastNetId: {packet.LastNetId} with {packet.TrainsetParts.Length} parts, but trainset has {set.cars.Count} parts"); return; } + //Check direction of trainset vs packet + if(set.firstCar.GetNetId() == packet.LastNetId) + packet.TrainsetParts = packet.TrainsetParts.Reverse().ToArray(); + //Multiplayer.Log($"Client_HandleTrainsetPhysicsUpdate({set.firstCar.ID}):, tick: {packet.Tick}"); for (int i = 0; i < packet.TrainsetParts.Length; i++) diff --git a/Multiplayer/Components/Networking/Train/NetworkedCarSpawner.cs b/Multiplayer/Components/Networking/Train/NetworkedCarSpawner.cs index b52e046..6cb0947 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedCarSpawner.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedCarSpawner.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System.Collections; +using DV.Simulation.Brake; using DV.ThingTypes; using Multiplayer.Components.Networking.World; using Multiplayer.Networking.Data.Train; @@ -9,16 +10,29 @@ namespace Multiplayer.Components.Networking.Train; public static class NetworkedCarSpawner { - public static void SpawnCars(TrainsetSpawnPart[] parts) + //static Coroutine ignoreStress; + public static void SpawnCars(TrainsetSpawnPart[] parts, bool autoCouple) { - TrainCar[] cars = new TrainCar[parts.Length]; + NetworkedTrainCar[] cars = new NetworkedTrainCar[parts.Length]; + + //spawn the cars for (int i = 0; i < parts.Length; i++) cars[i] = SpawnCar(parts[i], true); + + //Set brake params + for (int i = 0; i < cars.Length; i++) + SetBrakeParams(parts[i], cars[i].TrainCar); + + //couple them if marked as coupled + for (int i = 0; i < cars.Length; i++) + Couple(parts[i], cars[i].TrainCar, autoCouple); + + //update speed queue data for (int i = 0; i < cars.Length; i++) - AutoCouple(parts[i], cars[i]); + cars[i].Client_trainSpeedQueue.ReceiveSnapshot(parts[i].Speed, NetworkLifecycle.Instance.Tick); } - public static TrainCar SpawnCar(TrainsetSpawnPart spawnPart, bool preventCoupling = false) + public static NetworkedTrainCar SpawnCar(TrainsetSpawnPart spawnPart, bool preventCoupling = false) { if (!NetworkedRailTrack.Get(spawnPart.Bogie1.TrackNetId, out NetworkedRailTrack bogie1Track) && spawnPart.Bogie1.TrackNetId != 0) { @@ -38,24 +52,25 @@ public static TrainCar SpawnCar(TrainsetSpawnPart spawnPart, bool preventCouplin return null; } - (TrainCar trainCar, bool isPooled) = GetFromPool(livery); + //TrainCar trainCar = CarSpawner.Instance.BaseSpawn(livery.prefab, spawnPart.PlayerSpawnedCar, false); //todo: do we need to set the unique flag ever on a client? + TrainCar trainCar = (CarSpawner.Instance.useCarPooling ? CarSpawner.Instance.GetFromPool(livery.prefab) : UnityEngine.Object.Instantiate(livery.prefab)).GetComponentInChildren(); + //Multiplayer.LogDebug(() => $"SpawnCar({spawnPart.CarId}) activePrefab: {livery.prefab.activeSelf} activeInstance: {trainCar.gameObject.activeSelf}"); + trainCar.playerSpawnedCar = spawnPart.PlayerSpawnedCar; + trainCar.uniqueCar = false; + trainCar.InitializeExistingLogicCar(spawnPart.CarId, spawnPart.CarGuid); + //Add networked components NetworkedTrainCar networkedTrainCar = trainCar.gameObject.GetOrAddComponent(); networkedTrainCar.NetId = spawnPart.NetId; - trainCar.gameObject.GetOrAddComponent(); - - trainCar.gameObject.SetActive(true); - - if (isPooled) - trainCar.AwakeForPooledCar(); - - trainCar.InitializeExistingLogicCar(spawnPart.CarId, spawnPart.CarGuid); + //Setup positions and bogies Transform trainTransform = trainCar.transform; trainTransform.position = spawnPart.Position + WorldMover.currentMove; trainTransform.rotation = spawnPart.Rotation; - trainCar.playerSpawnedCar = spawnPart.PlayerSpawnedCar; - trainCar.preventAutoCouple = true; + + //Multiplayer.LogDebug(() => $"SpawnCar({spawnPart.CarId}) Bogie1 derailed: {spawnPart.Bogie1.HasDerailed}, Rail Track: {bogie1Track?.RailTrack?.name}, Position along track: {spawnPart.Bogie1.PositionAlongTrack}, Track direction: {spawnPart.Bogie1.TrackDirection}, " + + // $"Bogie2 derailed: {spawnPart.Bogie2.HasDerailed}, Rail Track: {bogie2Track?.RailTrack?.name}, Position along track: {spawnPart.Bogie2.PositionAlongTrack}, Track direction: {spawnPart.Bogie2.TrackDirection}" + //); if (!spawnPart.Bogie1.HasDerailed) trainCar.Bogies[0].SetTrack(bogie1Track.RailTrack, spawnPart.Bogie1.PositionAlongTrack, spawnPart.Bogie1.TrackDirection); @@ -67,62 +82,134 @@ public static TrainCar SpawnCar(TrainsetSpawnPart spawnPart, bool preventCouplin else trainCar.Bogies[1].SetDerailedOnLoadFlag(true); + trainCar.TryAddFastTravelDestination(); + CarSpawner.Instance.FireCarSpawned(trainCar); - networkedTrainCar.Client_trainSpeedQueue.ReceiveSnapshot(spawnPart.Speed, NetworkLifecycle.Instance.Tick); + return networkedTrainCar; + } + + private static void Couple(in TrainsetSpawnPart spawnPart, TrainCar trainCar, bool autoCouple) + { + if (autoCouple) + { + trainCar.frontCoupler.preventAutoCouple = spawnPart.PreventFrontAutoCouple; + trainCar.rearCoupler.preventAutoCouple = spawnPart.PreventRearAutoCouple; - if (!preventCoupling) - AutoCouple(spawnPart, trainCar); + trainCar.frontCoupler.AttemptAutoCouple(); + trainCar.rearCoupler.AttemptAutoCouple(); + + return; + } - return trainCar; + //Handle coupling at front of car + HandleCoupling( + spawnPart.IsFrontCoupled, + spawnPart.FrontHoseConnected, + spawnPart.FrontConnectionNetId, + spawnPart.FrontConnectionToFront, + spawnPart.FrontState, + spawnPart.FrontCockOpen, + trainCar.frontCoupler + ); + + //Handle coupling at rear of car + HandleCoupling( + spawnPart.IsRearCoupled, + spawnPart.RearHoseConnected, + spawnPart.RearConnectionNetId, + spawnPart.RearConnectionToFront, + spawnPart.RearState, + spawnPart.RearCockOpen, + trainCar.rearCoupler + ); } - private static void AutoCouple(TrainsetSpawnPart spawnPart, TrainCar trainCar) + private static void HandleCoupling( + bool isCoupled, + bool isHoseConnected, + ushort connectionNetId, + bool connectionToFront, + ChainCouplerInteraction.State couplingState, + bool cockOpen, + Coupler currentCoupler) { - if (spawnPart.IsFrontCoupled) trainCar.frontCoupler.TryCouple(false, true); - if (spawnPart.IsRearCoupled) trainCar.rearCoupler.TryCouple(false, true); + if (!isCoupled && !isHoseConnected) + return; + + if (!NetworkedTrainCar.GetTrainCar(connectionNetId, out TrainCar otherCar)) + { + Multiplayer.LogWarning($"AutoCouple([{currentCoupler?.train?.GetNetId()}, {currentCoupler?.train?.ID}]) did not find car at {(currentCoupler.isFrontCoupler ? "Front" : "Rear")} car with netId: {connectionNetId}"); + return; + } + + var otherCoupler = connectionToFront ? otherCar.frontCoupler : otherCar.rearCoupler; + + if (isCoupled) + { + //NetworkLifecycle.Instance.Client.LogDebug(() => $"AutoCouple() Coupling {(currentCoupler.isFrontCoupler? "Front" : "Rear")}: {currentCoupler?.train?.ID}, to {otherCar?.ID}, at: {(connectionToFront ? "Front" : "Rear")}"); + SetCouplingState(currentCoupler, otherCoupler, couplingState); + } + + if (isHoseConnected) + { + CarsSaveManager.RestoreHoseAndCock(currentCoupler, isHoseConnected, cockOpen); + } } - private static (TrainCar, bool) GetFromPool(TrainCarLivery livery) + public static void SetCouplingState(Coupler coupler, Coupler otherCoupler, ChainCouplerInteraction.State targetState) { - if (!CarSpawner.Instance.useCarPooling || !CarSpawner.Instance.carLiveryToTrainCarPool.TryGetValue(livery, out List trainCarList)) - return Instantiate(livery); + //Multiplayer.LogDebug(() => $"SetCouplingState({coupler.train.ID}, {otherCoupler.train.ID}, {targetState}) Coupled: {coupler.IsCoupled()}"); - int count = trainCarList.Count; - if (count <= 0) - return Instantiate(livery); + if (coupler.IsCoupled() && targetState == ChainCouplerInteraction.State.Attached_Tight) + { + //Multiplayer.LogDebug(() => $"SetCouplingState({coupler.train.ID}, {otherCoupler.train.ID}, {targetState}) Coupled, attaching tight"); + coupler.state = ChainCouplerInteraction.State.Parked; + return; + } - int index = count - 1; - TrainCar trainCar = trainCarList[index]; - trainCarList.RemoveAt(index); - CarSpawner.Instance.trainCarPoolHashSet.Remove(trainCar); + coupler.state = targetState; + if (coupler.state == ChainCouplerInteraction.State.Attached_Tight) + { + //Multiplayer.LogDebug(() => $"SetCouplingState({coupler.train.ID}, {otherCoupler.train.ID}, {targetState}) Not coupled, attaching tight"); + coupler.CoupleTo(otherCoupler, false); + coupler.SetChainTight(true); + } + else if (coupler.state == ChainCouplerInteraction.State.Attached_Loose) + { + //Multiplayer.LogDebug(() => $"SetCouplingState({coupler.train.ID}, {otherCoupler.train.ID}, {targetState}) Unknown coupled, attaching loose"); + coupler.CoupleTo(otherCoupler, false); + coupler.SetChainTight(false); + } - if (trainCar != null) + if (!coupler.IsCoupled()) { - Transform trainCarTransform = trainCar.transform; - trainCarTransform.SetParent(null); - trainCarTransform.localScale = Vector3.one; - trainCar.gameObject.SetActive(false); // Enabled after NetworkedTrainCar has been added - - Transform interiorTransform = trainCar.interior.transform; - interiorTransform.SetParent(null); - interiorTransform.localScale = Vector3.one; - - trainCar.interior.gameObject.SetActive(true); - trainCar.rb.isKinematic = false; - return (trainCar, true); + //Multiplayer.LogDebug(() => $"SetCouplingState({coupler.train.ID}, {otherCoupler.train.ID}, {targetState}) Failed to couple, activating buffer collider"); + coupler.fakeBuffersCollider.enabled = true; } - Multiplayer.LogError($"Failed to get {livery.id} from pool!"); - return Instantiate(livery); } - private static (TrainCar, bool) Instantiate(TrainCarLivery livery) + private static void SetBrakeParams(TrainsetSpawnPart spawnPart, TrainCar trainCar) { - bool wasActive = livery.prefab.activeSelf; - livery.prefab.SetActive(false); - (TrainCar, bool) result = (Object.Instantiate(livery.prefab).GetComponent(), false); - livery.prefab.SetActive(wasActive); - return result; + BrakeSystem bs = trainCar.brakeSystem; + + if (bs == null) + { + Multiplayer.LogWarning($"NetworkedCarSpawner.SetBrakeParams() Brake system is null! netId: {spawnPart.NetId}, trainCar: {spawnPart.CarId}"); + return; + } + + if(bs.hasHandbrake) + bs.SetHandbrakePosition(spawnPart.HandBrakePosition); + if(bs.hasTrainBrake) + bs.trainBrakePosition = spawnPart.TrainBrakePosition; + + bs.SetBrakePipePressure(spawnPart.BrakePipePressure); + bs.SetAuxReservoirPressure(spawnPart.AuxResPressure); + bs.SetMainReservoirPressure(spawnPart.MainResPressure); + bs.SetControlReservoirPressure(spawnPart.ControlResPressure); + bs.ForceCylinderPressure(spawnPart.BrakeCylPressure); + } } diff --git a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs index b0e3065..d270188 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs @@ -81,8 +81,6 @@ public static bool TryGetFromTrainCar(TrainCar trainCar, out NetworkedTrainCar n private bool handbrakeDirty; private bool mainResPressureDirty; public bool BogieTracksDirty; - public int Bogie1TrackDirection; - public int Bogie2TrackDirection; private bool cargoDirty; private bool cargoIsLoading; public byte CargoModelIndex = byte.MaxValue; @@ -267,7 +265,8 @@ private IEnumerator Server_WaitForLogicCar() TrainCar.logicCar.CargoLoaded += Server_OnCargoLoaded; TrainCar.logicCar.CargoUnloaded += Server_OnCargoUnloaded; - NetworkLifecycle.Instance.Server.SendSpawnTrainCar(this); + + Server_DirtyAllState(); } public void Server_DirtyAllState() @@ -371,7 +370,7 @@ private void Server_OnTick(uint tick) Server_SendBrakePressures(); Server_SendFireBoxState(); - Server_SendCouplers(); + //Server_SendCouplers(); Server_SendCables(); Server_SendCargoState(); Server_SendHealthState(); @@ -383,6 +382,7 @@ private void Server_SendBrakePressures() { if (!mainResPressureDirty) return; + mainResPressureDirty = false; //B99 review need / mod NetworkLifecycle.Instance.Server.SendBrakePressures(NetId, brakeSystem.mainReservoirPressure, brakeSystem.independentPipePressure, brakeSystem.brakePipePressure, brakeSystem.brakeCylinderPressure); } @@ -400,6 +400,7 @@ private void Server_SendCouplers() { if (!sendCouplers) return; + sendCouplers = false; if(TrainCar.frontCoupler.IsCoupled()) @@ -635,13 +636,14 @@ private IEnumerator Client_InitLater() while ((client_bogie2Queue = bogie2.GetComponent()) == null) yield return null; - client_Initialized = true; + Client_Initialized = true; } public void Client_ReceiveTrainPhysicsUpdate(in TrainsetMovementPart movementPart, uint tick) { - if (!client_Initialized) + if (!Client_Initialized) return; + if (TrainCar.isEligibleForSleep) TrainCar.ForceOptimizationState(false); @@ -657,12 +659,6 @@ public void Client_ReceiveTrainPhysicsUpdate(in TrainsetMovementPart movementPar //move the car to the correct position first - maybe? if (movementPart.typeFlag.HasFlag(TrainsetMovementPart.MovementType.Position)) { - /* - float d1 = (TrainCar.transform.position - (movementPart.Position + WorldMover.currentMove)).sqrMagnitude; - Quaternion d2 = TrainCar.transform.rotation * Quaternion.Inverse(movementPart.Rotation); - - Multiplayer.LogDebug(()=> $"Client_ReceiveTrainPhysicsUpdate({TrainCar.ID}, {tick}): Sync, Queue counts: {Client_trainSpeedQueue.snapshots.Count}, {Client_trainRigidbodyQueue.snapshots.Count}, {client_bogie1Queue.snapshots.Count}, {client_bogie2Queue.snapshots.Count}, Deltas: {d1}, {d2}"); - */ TrainCar.transform.position = movementPart.Position + WorldMover.currentMove; TrainCar.transform.rotation = movementPart.Rotation; @@ -673,11 +669,7 @@ public void Client_ReceiveTrainPhysicsUpdate(in TrainsetMovementPart movementPar client_bogie2Queue.Clear(); TrainCar.stress.ResetTrainStress(); - }/* - else - { - Multiplayer.LogDebug(() => $"Client_ReceiveTrainPhysicsUpdate({TrainCar.ID}, {tick}): Physics"); - }*/ + } Client_trainSpeedQueue.ReceiveSnapshot(movementPart.Speed, tick); TrainCar.stress.slowBuildUpStress = movementPart.SlowBuildUpStress; diff --git a/Multiplayer/Networking/Data/Train/TrainsetSpawnPart.cs b/Multiplayer/Networking/Data/Train/TrainsetSpawnPart.cs new file mode 100644 index 0000000..008a555 --- /dev/null +++ b/Multiplayer/Networking/Data/Train/TrainsetSpawnPart.cs @@ -0,0 +1,439 @@ +using DV.ThingTypes; +using LiteNetLib.Utils; +using Multiplayer.Components.Networking; +using Multiplayer.Components.Networking.Train; +using Multiplayer.Networking.Serialization; +using Multiplayer.Utils; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using UnityEngine; + +namespace Multiplayer.Networking.Data.Train; + +public readonly struct TrainsetSpawnPart +{ + private static readonly byte[] EMPTY_GUID = new Guid().ToByteArray(); // Empty GUID as bytes + + public readonly ushort NetId; + + //car details + public readonly string LiveryId; + public readonly string CarId; + public readonly string CarGuid; + + //Cargo details + public readonly CargoType CargoType; + public readonly float LoadedAmount; + + //customisation details + public readonly bool PlayerSpawnedCar; + + //coupling details + public readonly ushort FrontConnectionNetId; //if we are coupled or hosed this will be the netId of the other car + public readonly bool FrontConnectionToFront; //if we are coupled or hosed this will be 'true' if connected to the front other car + public readonly bool IsFrontCoupled; + public readonly ChainCouplerInteraction.State FrontState; + public readonly bool FrontHoseConnected; + public readonly bool PreventFrontAutoCouple; + + public readonly ushort RearConnectionNetId; //if we are coupled or hosed this will be the netId of the other car + public readonly bool RearConnectionToFront; //if we are coupled or hosed this will be 'true' if connected to the front other car + public readonly bool IsRearCoupled; + public readonly ChainCouplerInteraction.State RearState; + public readonly bool RearHoseConnected; + public readonly bool PreventRearAutoCouple; + + //positional details + public readonly float Speed; + public readonly Vector3 Position; + public readonly Quaternion Rotation; + + //bogie details + public readonly BogieData Bogie1; + public readonly BogieData Bogie2; + + //brake initial states + public readonly bool HasHandbrake; + public readonly bool HasTrainbrake; + public readonly float HandBrakePosition; + public readonly float TrainBrakePosition; + public readonly float BrakePipePressure; + public readonly float AuxResPressure; + public readonly float MainResPressure; + public readonly float ControlResPressure; + public readonly float BrakeCylPressure; + + public readonly bool FrontCockOpen; + public readonly bool RearCockOpen; + + private TrainsetSpawnPart(ushort netId, string liveryId, string carId, string carGuid, bool playerSpawnedCar, + bool isFrontCoupled, ChainCouplerInteraction.State frontState, ushort frontConnectionNetId, bool frontConnectedToFront, bool preventFrontAutoCouple, + bool isRearCoupled, ChainCouplerInteraction.State rearState, ushort rearConnectionNetId, bool rearConnectedToFront, bool preventRearAutoCouple, + float speed, Vector3 position, Quaternion rotation, + BogieData bogie1, BogieData bogie2, + float? handBrakePos, float? trainBrakePos, float brakePipePress, float auxResPress, float mainResPress, float controlResPress, float brakeCylPress, + bool frontHoseConnected, + bool rearHoseConnected, + bool frontCockOpen, bool rearCockOpen) + { + NetId = netId; + + LiveryId = liveryId; + CarId = carId; + CarGuid = carGuid; + + PlayerSpawnedCar = playerSpawnedCar; + + IsFrontCoupled = isFrontCoupled; + FrontState = frontState; + FrontConnectionNetId = frontConnectionNetId; + FrontConnectionToFront = frontConnectedToFront; + FrontHoseConnected = frontHoseConnected; + PreventFrontAutoCouple = preventFrontAutoCouple; + + IsRearCoupled = isRearCoupled; + RearState = rearState; + RearConnectionNetId = rearConnectionNetId; + RearConnectionToFront = rearConnectedToFront; + RearHoseConnected = rearHoseConnected; + PreventRearAutoCouple = preventRearAutoCouple; + + + Speed = speed; + Position = position; + Rotation = rotation; + + Bogie1 = bogie1; + Bogie2 = bogie2; + + HasHandbrake = handBrakePos != null; + HasTrainbrake = trainBrakePos != null; + + if (HasHandbrake) + HandBrakePosition = (float)handBrakePos; + + if (HasTrainbrake) + TrainBrakePosition = (float)trainBrakePos; + + BrakePipePressure = brakePipePress; + AuxResPressure = auxResPress; + MainResPressure = mainResPress; + ControlResPressure = controlResPress; + BrakeCylPressure = brakeCylPress; + + FrontCockOpen = frontCockOpen; + RearCockOpen = rearCockOpen; + } + + public static void Serialize(NetDataWriter writer, TrainsetSpawnPart data) + { + writer.Put(data.NetId); + + writer.Put(data.LiveryId); + writer.Put(data.CarId); + + //encode our Guid to save 50% bytes in the packet size + if (Guid.TryParse(data.CarGuid, out Guid guid)) + writer.PutBytesWithLength(guid.ToByteArray()); + else + { + Multiplayer.LogError($"TrainsetSpawnPart.TrainsetSpawnPart() failed to parse carGuid: {data.CarGuid}"); + writer.PutBytesWithLength(EMPTY_GUID); + } + + writer.Put(data.PlayerSpawnedCar); + + writer.Put(data.IsFrontCoupled); + writer.Put(data.FrontHoseConnected); + writer.Put((byte)data.FrontState); + if (data.IsFrontCoupled || data.FrontHoseConnected) + { + writer.Put(data.FrontConnectionNetId); + writer.Put(data.FrontConnectionToFront); + } + writer.Put(data.PreventFrontAutoCouple); + + writer.Put(data.IsRearCoupled); + writer.Put(data.RearHoseConnected); + writer.Put((byte)data.RearState); + if (data.IsRearCoupled || data.RearHoseConnected) + { + writer.Put(data.RearConnectionNetId); + writer.Put(data.RearConnectionToFront); + } + writer.Put(data.PreventRearAutoCouple); + + writer.Put(data.Speed); + Vector3Serializer.Serialize(writer, data.Position); + QuaternionSerializer.Serialize(writer, data.Rotation); + + BogieData.Serialize(writer, data.Bogie1); + BogieData.Serialize(writer, data.Bogie2); + + writer.Put(data.HasHandbrake); + if (data.HasHandbrake) + writer.Put(data.HandBrakePosition); + + writer.Put(data.HasTrainbrake); + if (data.HasTrainbrake) + writer.Put(data.TrainBrakePosition); + + writer.Put(data.BrakePipePressure); + writer.Put(data.AuxResPressure); + writer.Put(data.MainResPressure); + writer.Put(data.ControlResPressure); + writer.Put(data.BrakeCylPressure); + + writer.Put(data.FrontCockOpen); + writer.Put(data.RearCockOpen); + } + + public static TrainsetSpawnPart Deserialize(NetDataReader reader) + { + ushort netId = reader.GetUShort(); //NetId + + string liveryId = reader.GetString(); //LiveryId + string carId = reader.GetString(); //CarId + byte[] guidBytes = reader.GetBytesWithLength(); //GuiId + + string carGuid = new Guid(guidBytes).ToString(); //decode GuiId + + bool playerSpawnedCar = reader.GetBool(); //PlayerSpawnedCar + + bool isFrontCoupled = reader.GetBool(); //IsFrontCoupled + bool isFrontHoseConnected = reader.GetBool(); //IsFrontHose + ChainCouplerInteraction.State frontState = (ChainCouplerInteraction.State)reader.GetByte(); + + ushort frontConnectedToNetId = 0; + bool frontConnectedToFront = false; + if (isFrontCoupled || isFrontHoseConnected) + { + frontConnectedToNetId = reader.GetUShort(); + frontConnectedToFront = reader.GetBool(); + } + bool preventFrontAutoCouple = reader.GetBool(); + + bool isRearCoupled = reader.GetBool(); //IsRearCoupled + bool isRearHoseConnected = reader.GetBool(); //IsRearHose + ChainCouplerInteraction.State rearState = (ChainCouplerInteraction.State)reader.GetByte(); + ushort rearConnectedToNetId = 0; + bool rearConnectedToFront = false; + if (isRearCoupled || isRearHoseConnected) + { + rearConnectedToNetId = reader.GetUShort(); + rearConnectedToFront = reader.GetBool(); + } + bool preventRearAutoCouple = reader.GetBool(); + + return new TrainsetSpawnPart( + netId, + + liveryId, + carId, + carGuid, + + playerSpawnedCar, + + isFrontCoupled, + frontState, + frontConnectedToNetId, + frontConnectedToFront, + preventFrontAutoCouple, + + isRearCoupled, + rearState, + rearConnectedToNetId, + rearConnectedToFront, + preventRearAutoCouple, + + reader.GetFloat(), //Speed + Vector3Serializer.Deserialize(reader), //Position + QuaternionSerializer.Deserialize(reader), //Rotation + + BogieData.Deserialize(reader), //Bogie 1 + BogieData.Deserialize(reader), //Bogie 2 + + reader.GetBool() ? reader.GetFloat() : null, //HandbrakePos + reader.GetBool() ? reader.GetFloat() : null, //TrainBrakePos + reader.GetFloat(), //BrakePipePressure + reader.GetFloat(), //AuxResPressure + reader.GetFloat(), //MainResPressure + reader.GetFloat(), //ControlResPressure + reader.GetFloat(), //BrakeCylPressure + + isFrontHoseConnected, //FrontHoseConnected + isRearHoseConnected, //RearHoseConnected + + reader.GetBool(), //FrontCockOpen + reader.GetBool() //RearCockOpen + ); + } + + public static TrainsetSpawnPart FromTrainCar(NetworkedTrainCar networkedTrainCar) + { + TrainCar trainCar = networkedTrainCar.TrainCar; + Transform transform = networkedTrainCar.transform; + + ushort frontConnectedTo = 0; + bool frontConnectedToFront = false; + ChainCouplerInteraction.State frontCouplerState = ChainCouplerInteraction.State.Parked; + + ushort rearConnectedTo = 0; + bool rearConnectedToFront = false; + ChainCouplerInteraction.State rearCouplerState = ChainCouplerInteraction.State.Parked; + + bool frontCouplerIsCoupled = false; + bool preventFrontAutoCouple = false; + bool rearCouplerIsCoupled = false; + bool preventRearAutoCouple = false; + + bool frontHoseConnected = false; + bool rearHoseConnected = false; + + bool frontCockOpen = false; + bool rearCockOpen = false; + + + NetworkLifecycle.Instance.Server.LogDebug(() => + { + return $"TrainsetSpawnPart.FromTrainCar({networkedTrainCar?.NetId}) TrainCarID: {trainCar?.ID}, LiveryID: {trainCar?.carLivery?.id}, " + + $"Front[Coupled:{trainCar?.frontCoupler?.IsCoupled()}, State:{trainCar?.frontCoupler?.state}, Hose:{trainCar?.frontCoupler?.hoseAndCock?.IsHoseConnected}, Cock:{trainCar?.frontCoupler?.IsCockOpen}], " + + $"Rear[Coupled:{trainCar?.rearCoupler?.IsCoupled()}, State:{trainCar?.rearCoupler?.state}, Hose:{trainCar?.rearCoupler?.hoseAndCock?.IsHoseConnected}, Cock:{trainCar?.rearCoupler?.IsCockOpen}]"; + }); + + + if (trainCar.frontCoupler.IsCoupled()) + { + Multiplayer.LogDebug(() => $"FromTrainCar([{networkedTrainCar?.NetId},{networkedTrainCar?.TrainCar?.ID}]) front is coupled to netID: {trainCar?.frontCoupler?.coupledTo?.train?.GetNetId()}"); + frontConnectedTo = trainCar.frontCoupler.coupledTo.train.GetNetId(); + frontConnectedToFront = trainCar.frontCoupler.coupledTo.isFrontCoupler; + } + else if (trainCar.frontCoupler.hoseAndCock.IsHoseConnected) + { + Multiplayer.LogDebug(() => $"FromTrainCar([{networkedTrainCar?.NetId},{networkedTrainCar?.TrainCar?.ID}]) front hose connected to netID: {trainCar?.frontCoupler?.coupledTo?.train?.GetNetId()}"); + frontConnectedTo = trainCar.frontCoupler.GetAirHoseConnectedTo().train.GetNetId(); + frontConnectedToFront = trainCar.frontCoupler.GetAirHoseConnectedTo().isFrontCoupler; + } + + if (trainCar.rearCoupler.IsCoupled()) + { + Multiplayer.LogDebug(() => $"FromTrainCar([{networkedTrainCar?.NetId},{networkedTrainCar?.TrainCar?.ID}]) rear is coupled to netID: {trainCar?.rearCoupler?.coupledTo?.train?.GetNetId()}"); + rearConnectedTo = trainCar.rearCoupler.coupledTo.train.GetNetId(); + rearConnectedToFront = trainCar.rearCoupler.coupledTo.isFrontCoupler; + } + else if (trainCar.rearCoupler.hoseAndCock.IsHoseConnected) + { + Multiplayer.LogDebug(() => $"FromTrainCar([{networkedTrainCar?.NetId},{networkedTrainCar?.TrainCar?.ID}]) rear hose connected to netID: {trainCar?.rearCoupler?.coupledTo?.train?.GetNetId()}"); + rearConnectedTo = trainCar.rearCoupler.GetAirHoseConnectedTo().train.GetNetId(); + rearConnectedToFront = trainCar.rearCoupler.GetAirHoseConnectedTo().isFrontCoupler; + } + + frontCouplerIsCoupled = trainCar.frontCoupler.IsCoupled(); + preventFrontAutoCouple = trainCar.frontCoupler.preventAutoCouple; + rearCouplerIsCoupled = trainCar.rearCoupler.IsCoupled(); + preventRearAutoCouple = trainCar.rearCoupler.preventAutoCouple; + + frontCouplerState = trainCar.frontCoupler.state; + rearCouplerState = trainCar.rearCoupler.state; + + frontHoseConnected = trainCar.frontCoupler.hoseAndCock.IsHoseConnected; + rearHoseConnected = trainCar.rearCoupler.hoseAndCock.IsHoseConnected; + + frontCockOpen = trainCar.frontCoupler.IsCockOpen; + rearCockOpen = trainCar.rearCoupler.IsCockOpen; + + return new TrainsetSpawnPart( + networkedTrainCar.NetId, + + trainCar.carLivery.id, + trainCar.ID, + trainCar.CarGUID, + + trainCar.playerSpawnedCar, + + frontCouplerIsCoupled, + frontCouplerState, + frontConnectedTo, + frontConnectedToFront, + preventFrontAutoCouple, + + rearCouplerIsCoupled, + rearCouplerState, + rearConnectedTo, + rearConnectedToFront, + preventRearAutoCouple, + + trainCar.GetForwardSpeed(), + transform.position - WorldMover.currentMove, + transform.rotation, + + BogieData.FromBogie(trainCar.Bogies[0], true), + BogieData.FromBogie(trainCar.Bogies[1], true), + + trainCar.brakeSystem.hasHandbrake ? trainCar.brakeSystem.handbrakePosition : null, + trainCar.brakeSystem.hasTrainBrake ? trainCar.brakeSystem.trainBrakePosition : null, + trainCar.brakeSystem.brakePipePressure, + trainCar.brakeSystem.auxReservoirPressure, + trainCar.brakeSystem.mainReservoirPressure, + trainCar.brakeSystem.controlReservoirPressure, + trainCar.brakeSystem.brakeCylinderPressure, + + frontHoseConnected, + rearHoseConnected, + frontCockOpen, + rearCockOpen + ); + } + + //public static TrainsetSpawnPart[] FromTrainSet(Trainset trainset) + //{ + // if (trainset == null) + // { + // NetworkLifecycle.Instance.Server.LogWarning("TrainsetSpawnPart.FromTrainSet() trainset is null!"); + // return null; + // } + + // TrainsetSpawnPart[] parts = new TrainsetSpawnPart[trainset.cars.Count]; + // for (int i = 0; i < trainset.cars.Count; i++) + // { + // NetworkedTrainCar networkedTrainCar; + + // if (!trainset.cars[i].TryNetworked(out networkedTrainCar)) + // { + // NetworkLifecycle.Instance.Server.LogWarning($"TrainsetSpawnPart.FromTrainSet({trainset?.id}) Failed to find NetworkedTrainCar for: {trainset?.cars[i]?.ID}"); + // networkedTrainCar = trainset.cars[i].GetOrAddComponent(); + // } + + // parts[i] = FromTrainCar(networkedTrainCar); + // } + // return parts; + //} + + public static TrainsetSpawnPart[] FromTrainSet(List trainset/*, bool resolveCoupling = false*/) + { + if (trainset == null) + { + NetworkLifecycle.Instance.Server.LogWarning("TrainsetSpawnPart.FromTrainSet() trainset list is null!"); + return null; + } + + TrainsetSpawnPart[] parts = new TrainsetSpawnPart[trainset.Count]; + for (int i = 0; i < trainset.Count; i++) + { + NetworkedTrainCar networkedTrainCar; + + if (!trainset[i].TryNetworked(out networkedTrainCar)) + { + NetworkLifecycle.Instance.Server.LogWarning($"TrainsetSpawnPart.FromTrainSet() Failed to find NetworkedTrainCar for: {trainset[i]?.ID}"); + networkedTrainCar = trainset[i].GetOrAddComponent(); + } + + parts[i] = FromTrainCar(networkedTrainCar); + } + + return parts; + } + +} diff --git a/Multiplayer/Networking/Data/TrainsetSpawnPart.cs b/Multiplayer/Networking/Data/TrainsetSpawnPart.cs deleted file mode 100644 index fc2c6d9..0000000 --- a/Multiplayer/Networking/Data/TrainsetSpawnPart.cs +++ /dev/null @@ -1,119 +0,0 @@ -using LiteNetLib.Utils; -using Multiplayer.Components.Networking; -using Multiplayer.Components.Networking.Train; -using Multiplayer.Networking.Serialization; -using Multiplayer.Utils; -using UnityEngine; - -namespace Multiplayer.Networking.Data; - -public readonly struct TrainsetSpawnPart -{ - public readonly ushort NetId; - public readonly string LiveryId; - public readonly string CarId; - public readonly string CarGuid; - public readonly bool PlayerSpawnedCar; - public readonly bool IsFrontCoupled; - public readonly bool IsRearCoupled; - public readonly float Speed; - public readonly Vector3 Position; - public readonly Quaternion Rotation; - public readonly BogieData Bogie1; - public readonly BogieData Bogie2; - - private TrainsetSpawnPart(ushort netId, string liveryId, string carId, string carGuid, bool playerSpawnedCar, bool isFrontCoupled, bool isRearCoupled, float speed, Vector3 position, Quaternion rotation, - BogieData bogie1, BogieData bogie2) - { - NetId = netId; - LiveryId = liveryId; - CarId = carId; - CarGuid = carGuid; - PlayerSpawnedCar = playerSpawnedCar; - IsFrontCoupled = isFrontCoupled; - IsRearCoupled = isRearCoupled; - Speed = speed; - Position = position; - Rotation = rotation; - Bogie1 = bogie1; - Bogie2 = bogie2; - } - - public static void Serialize(NetDataWriter writer, TrainsetSpawnPart data) - { - writer.Put(data.NetId); - writer.Put(data.LiveryId); - writer.Put(data.CarId); - writer.Put(data.CarGuid); - writer.Put(data.PlayerSpawnedCar); - writer.Put(data.IsFrontCoupled); - writer.Put(data.IsRearCoupled); - writer.Put(data.Speed); - Vector3Serializer.Serialize(writer, data.Position); - QuaternionSerializer.Serialize(writer, data.Rotation); - BogieData.Serialize(writer, data.Bogie1); - BogieData.Serialize(writer, data.Bogie2); - } - - public static TrainsetSpawnPart Deserialize(NetDataReader reader) - { - return new TrainsetSpawnPart( - reader.GetUShort(), - reader.GetString(), - reader.GetString(), - reader.GetString(), - reader.GetBool(), - reader.GetBool(), - reader.GetBool(), - reader.GetFloat(), - Vector3Serializer.Deserialize(reader), - QuaternionSerializer.Deserialize(reader), - BogieData.Deserialize(reader), - BogieData.Deserialize(reader) - ); - } - - public static TrainsetSpawnPart FromTrainCar(NetworkedTrainCar networkedTrainCar) - { - TrainCar trainCar = networkedTrainCar.TrainCar; - Transform transform = networkedTrainCar.transform; - - NetworkLifecycle.Instance.Server.LogDebug(() => - { - return $"TrainsetSpawnPart.FromTrainCar({networkedTrainCar?.NetId}) TrainCarID: {trainCar?.ID}, Livery: {trainCar?.carLivery}, LiveryID: {trainCar?.carLivery?.id}"; - }); - - return new TrainsetSpawnPart( - networkedTrainCar.NetId, - trainCar.carLivery.id, - trainCar.ID, - trainCar.CarGUID, - trainCar.playerSpawnedCar, - trainCar.frontCoupler.IsCoupled(), - trainCar.rearCoupler.IsCoupled(), - trainCar.GetForwardSpeed(), - transform.position - WorldMover.currentMove, - transform.rotation, - BogieData.FromBogie(trainCar.Bogies[0], true, networkedTrainCar.Bogie1TrackDirection), - BogieData.FromBogie(trainCar.Bogies[1], true, networkedTrainCar.Bogie2TrackDirection) - ); - } - - public static TrainsetSpawnPart[] FromTrainSet(Trainset trainset) - { - TrainsetSpawnPart[] parts = new TrainsetSpawnPart[trainset.cars.Count]; - for (int i = 0; i < trainset.cars.Count; i++) - { - NetworkedTrainCar networkedTrainCar; - - if (!trainset.cars[i].TryNetworked(out networkedTrainCar)) - { - NetworkLifecycle.Instance.Server.LogWarning($"TrainsetSpawnPart.FromTrainSet({trainset?.id}) Failed to find NetworkedTrainCar for: {trainset?.cars[i]?.ID}"); - networkedTrainCar = trainset.cars[i].GetOrAddComponent(); - } - - parts[i] = FromTrainCar(networkedTrainCar); - } - return parts; - } -} diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 2be965e..44b55ac 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -504,7 +504,7 @@ private void OnClientboundSpawnTrainSetPacket(ClientboundSpawnTrainSetPacket pac } } - NetworkedCarSpawner.SpawnCars(packet.SpawnParts); + NetworkedCarSpawner.SpawnCars(packet.SpawnParts, packet.AutoCouple); foreach (TrainsetSpawnPart spawnPart in packet.SpawnParts) SendTrainSyncRequest(spawnPart.NetId); diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index e219e14..89b7f33 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -275,6 +275,37 @@ public void SendGameParams(GameParams gameParams) SendPacketToAll(ClientboundGameParamsPacket.FromGameParams(gameParams), DeliveryMethod.ReliableOrdered, selfPeer); } + public void SendSpawnTrainset(List set, bool autoCouple, bool sendToAll, NetPeer sendTo = null) + { + + LogDebug(() => + { + StringBuilder sb = new StringBuilder(); + + sb.Append($"SendSpawnTrainSet() Sending trainset {set?.FirstOrDefault()?.GetNetId()} with {set?.Count} cars"); + + TrainCar[] noNetId = set?.Where(car => car.GetNetId() == 0).ToArray(); + + if (noNetId.Length > 0) + sb.AppendLine($"Erroneous cars!: {string.Join(", ", noNetId.Select(car => $"{{{car?.ID}, {car?.CarGUID}, {car.logicCar != null}}}"))}"); + + return sb.ToString(); + + }); + + var packet = ClientboundSpawnTrainSetPacket.FromTrainSet(set, autoCouple); + + if (!sendToAll) + { + if (sendTo == null) + LogError($"SendSpawnTrainSet() Trying to send to null peer!"); + else + SendPacket(sendTo, packet, DeliveryMethod.ReliableOrdered); + } + else + SendPacketToAll(packet, DeliveryMethod.ReliableOrdered, selfPeer); + } + public void SendSpawnTrainCar(NetworkedTrainCar networkedTrainCar) { SendPacketToAll(ClientboundSpawnTrainCarPacket.FromTrainCar(networkedTrainCar), DeliveryMethod.ReliableOrdered, selfPeer); @@ -632,22 +663,14 @@ private void OnServerboundClientReadyPacket(ServerboundClientReadyPacket packet, // Send trains foreach (Trainset set in Trainset.allSets) { - LogDebug(() => + try { - StringBuilder sb = new StringBuilder(); - - sb.Append($"Sending trainset {set?.firstCar?.GetNetId()} with {set?.cars?.Count} cars"); - - TrainCar[] noNetId = set?.cars?.Where(car => car.GetNetId() == 0).ToArray(); - - if (noNetId.Length > 0) - sb.AppendLine($"Erroneous cars!: {string.Join(", ", noNetId.Select(car=> $"{{{car?.ID}, {car?.CarGUID}, {car.logicCar != null}}}"))}"); - - return sb.ToString(); - - }); - - SendPacket(peer, ClientboundSpawnTrainSetPacket.FromTrainSet(set), DeliveryMethod.ReliableOrdered); + SendSpawnTrainset(set.cars, false, false, peer); + } + catch (Exception e) + { + LogWarning($"Exception when trying to send train set spawn data for [{set?.firstCar?.ID}, {set?.firstCar?.GetNetId()}]\r\n{e.Message}\r\n{e.StackTrace}"); + } } // Sync Stations (match NetIDs with StationIDs) - we could do this the same as junctions but juntions may need to be upgraded to work this way - future planning for mod integration diff --git a/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundSpawnTrainSetPacket.cs b/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundSpawnTrainSetPacket.cs index 1d38dc2..6d9d396 100644 --- a/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundSpawnTrainSetPacket.cs +++ b/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundSpawnTrainSetPacket.cs @@ -1,15 +1,27 @@ using Multiplayer.Networking.Data.Train; +using System.Collections.Generic; namespace Multiplayer.Networking.Packets.Clientbound.Train; public class ClientboundSpawnTrainSetPacket { public TrainsetSpawnPart[] SpawnParts { get; set; } + public bool AutoCouple { get; set; } - public static ClientboundSpawnTrainSetPacket FromTrainSet(Trainset trainset) + //public static ClientboundSpawnTrainSetPacket FromTrainSet(Trainset trainset, bool autoCouple) + //{ + // return new ClientboundSpawnTrainSetPacket { + // SpawnParts = TrainsetSpawnPart.FromTrainSet(trainset), + // AutoCouple = autoCouple + // }; + //} + + public static ClientboundSpawnTrainSetPacket FromTrainSet(List trainset, bool autoCouple) { return new ClientboundSpawnTrainSetPacket { - SpawnParts = TrainsetSpawnPart.FromTrainSet(trainset) + SpawnParts = TrainsetSpawnPart.FromTrainSet(trainset), + AutoCouple = autoCouple + }; } } diff --git a/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundTrainsetPhysicsPacket.cs b/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundTrainsetPhysicsPacket.cs index dd3f41d..05903fa 100644 --- a/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundTrainsetPhysicsPacket.cs +++ b/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundTrainsetPhysicsPacket.cs @@ -4,7 +4,8 @@ namespace Multiplayer.Networking.Packets.Clientbound.Train; public class ClientboundTrainsetPhysicsPacket { - public int NetId { get; set; } + public int FirstNetId { get; set; } + public int LastNetId { get; set; } public uint Tick { get; set; } public TrainsetMovementPart[] TrainsetParts { get; set; } } diff --git a/Multiplayer/Patches/Train/BogiePatch.cs b/Multiplayer/Patches/Train/BogiePatch.cs index 89d806c..71b72ae 100644 --- a/Multiplayer/Patches/Train/BogiePatch.cs +++ b/Multiplayer/Patches/Train/BogiePatch.cs @@ -23,17 +23,3 @@ private static bool Prefix() return NetworkLifecycle.Instance.IsHost(); } } - -[HarmonyPatch(typeof(Bogie), nameof(Bogie.SetTrack))] -public static class Bogie_SetTrack_Patch -{ - private static void Prefix(Bogie __instance, int newTrackDirection) - { - if (!__instance.Car.TryNetworked(out NetworkedTrainCar networkedTrainCar)) - return; // When the car first gets spawned in by CarSpawner#SpawnExistingCar, this method gets called before the NetworkedTrainCar component is added to the car. - if (__instance.Car.Bogies[0] == __instance) - networkedTrainCar.Bogie1TrackDirection = newTrackDirection; - else if (__instance.Car.Bogies[1] == __instance) - networkedTrainCar.Bogie2TrackDirection = newTrackDirection; - } -} diff --git a/Multiplayer/Patches/Train/CarSpawnerPatch.cs b/Multiplayer/Patches/Train/CarSpawnerPatch.cs index a163f7f..de5bad2 100644 --- a/Multiplayer/Patches/Train/CarSpawnerPatch.cs +++ b/Multiplayer/Patches/Train/CarSpawnerPatch.cs @@ -2,13 +2,16 @@ using Multiplayer.Components.Networking; using Multiplayer.Components.Networking.Train; using Multiplayer.Utils; +using System.Collections.Generic; namespace Multiplayer.Patches.World; -[HarmonyPatch(typeof(CarSpawner), nameof(CarSpawner.PrepareTrainCarForDeleting))] -public static class CarSpawner_PrepareTrainCarForDeleting_Patch +[HarmonyPatch(typeof(CarSpawner))] +public static class CarSpawner_Patch { - private static void Prefix(TrainCar trainCar) + [HarmonyPatch(nameof(CarSpawner.PrepareTrainCarForDeleting))] + [HarmonyPrefix] + private static void PrepareTrainCarForDeleting(TrainCar trainCar) { if (UnloadWatcher.isUnloading) return; @@ -17,4 +20,23 @@ private static void Prefix(TrainCar trainCar) networkedTrainCar.IsDestroying = true; NetworkLifecycle.Instance.Server?.SendDestroyTrainCar(networkedTrainCar.NetId); } + + [HarmonyPatch(nameof(CarSpawner.SpawnCars))] + [HarmonyPostfix] + private static void SpawnCars(List __result) + { + if (UnloadWatcher.isUnloading) + return; + + if (!NetworkLifecycle.Instance.IsHost()) + return; + + if (__result == null || __result.Count == 0) + return; + + //Coupling is delayed by AutoCouple(), so a true trainset for the entire consist doesn't exist yet + Multiplayer.LogDebug(() => $"SpawnCars() {__result?.Count} cars spawned, adding to queue"); + NetworkLifecycle.Instance.Server.SendSpawnTrainset(__result, true,true); + + } } From b778c033ea5ec007184976d3424fddef8915b6c7 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 22 Dec 2024 12:36:19 +1000 Subject: [PATCH 136/188] Fix item bugs Ensure item is initialised (not null) when calling GetAll() Temp disable LanternPatch until item sync work resumes as it sometimes causes issues. --- Multiplayer/Components/Networking/World/NetworkedItem.cs | 2 +- Multiplayer/Patches/World/Items/LanternPatch.cs | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Multiplayer/Components/Networking/World/NetworkedItem.cs b/Multiplayer/Components/Networking/World/NetworkedItem.cs index 24f9be3..295e745 100644 --- a/Multiplayer/Components/Networking/World/NetworkedItem.cs +++ b/Multiplayer/Components/Networking/World/NetworkedItem.cs @@ -30,7 +30,7 @@ public class NetworkedItem : IdMonoBehaviour public static List GetAll() { - return itemBaseToNetworkedItem.Values.ToList(); + return itemBaseToNetworkedItem.Values.Where(val => val.Item != null).ToList(); } public static bool Get(ushort netId, out NetworkedItem obj) { diff --git a/Multiplayer/Patches/World/Items/LanternPatch.cs b/Multiplayer/Patches/World/Items/LanternPatch.cs index 324866d..01f9764 100644 --- a/Multiplayer/Patches/World/Items/LanternPatch.cs +++ b/Multiplayer/Patches/World/Items/LanternPatch.cs @@ -6,6 +6,7 @@ namespace Multiplayer.Patches.World.Items; +/* [HarmonyPatch(typeof(Lantern))] public static class LanternPatch { @@ -68,3 +69,4 @@ static void Initialize(Lantern __instance) } } } +*/ From d7f5d4ce39d4074dcc7962bb2cdd2125d06e865c Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 22 Dec 2024 15:15:50 +1000 Subject: [PATCH 137/188] Refactored SpawnPart data for easier maintenance --- .../Networking/Train/NetworkedCarSpawner.cs | 70 ++-- .../Networking/Data/Train/BrakeSystemData.cs | 89 +++++ .../Networking/Data/Train/CouplingData.cs | 77 ++++ .../Data/Train/TrainsetSpawnPart.cs | 377 +++--------------- 4 files changed, 243 insertions(+), 370 deletions(-) create mode 100644 Multiplayer/Networking/Data/Train/BrakeSystemData.cs create mode 100644 Multiplayer/Networking/Data/Train/CouplingData.cs diff --git a/Multiplayer/Components/Networking/Train/NetworkedCarSpawner.cs b/Multiplayer/Components/Networking/Train/NetworkedCarSpawner.cs index 6cb0947..103e29b 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedCarSpawner.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedCarSpawner.cs @@ -10,7 +10,6 @@ namespace Multiplayer.Components.Networking.Train; public static class NetworkedCarSpawner { - //static Coroutine ignoreStress; public static void SpawnCars(TrainsetSpawnPart[] parts, bool autoCouple) { NetworkedTrainCar[] cars = new NetworkedTrainCar[parts.Length]; @@ -21,7 +20,7 @@ public static void SpawnCars(TrainsetSpawnPart[] parts, bool autoCouple) //Set brake params for (int i = 0; i < cars.Length; i++) - SetBrakeParams(parts[i], cars[i].TrainCar); + SetBrakeParams(parts[i].BrakeData, cars[i].TrainCar); //couple them if marked as coupled for (int i = 0; i < cars.Length; i++) @@ -93,8 +92,8 @@ private static void Couple(in TrainsetSpawnPart spawnPart, TrainCar trainCar, bo { if (autoCouple) { - trainCar.frontCoupler.preventAutoCouple = spawnPart.PreventFrontAutoCouple; - trainCar.rearCoupler.preventAutoCouple = spawnPart.PreventRearAutoCouple; + trainCar.frontCoupler.preventAutoCouple = spawnPart.FrontCoupling.PreventAutoCouple; + trainCar.rearCoupler.preventAutoCouple = spawnPart.RearCoupling.PreventAutoCouple; trainCar.frontCoupler.AttemptAutoCouple(); trainCar.rearCoupler.AttemptAutoCouple(); @@ -103,57 +102,34 @@ private static void Couple(in TrainsetSpawnPart spawnPart, TrainCar trainCar, bo } //Handle coupling at front of car - HandleCoupling( - spawnPart.IsFrontCoupled, - spawnPart.FrontHoseConnected, - spawnPart.FrontConnectionNetId, - spawnPart.FrontConnectionToFront, - spawnPart.FrontState, - spawnPart.FrontCockOpen, - trainCar.frontCoupler - ); + HandleCoupling(spawnPart.FrontCoupling, trainCar.frontCoupler); //Handle coupling at rear of car - HandleCoupling( - spawnPart.IsRearCoupled, - spawnPart.RearHoseConnected, - spawnPart.RearConnectionNetId, - spawnPart.RearConnectionToFront, - spawnPart.RearState, - spawnPart.RearCockOpen, - trainCar.rearCoupler - ); + HandleCoupling(spawnPart.RearCoupling, trainCar.rearCoupler); } - private static void HandleCoupling( - bool isCoupled, - bool isHoseConnected, - ushort connectionNetId, - bool connectionToFront, - ChainCouplerInteraction.State couplingState, - bool cockOpen, - Coupler currentCoupler) + private static void HandleCoupling(CouplingData couplingData, Coupler currentCoupler) { - if (!isCoupled && !isHoseConnected) + if (!couplingData.IsCoupled && !couplingData.HoseConnected) return; - if (!NetworkedTrainCar.GetTrainCar(connectionNetId, out TrainCar otherCar)) + if (!NetworkedTrainCar.GetTrainCar(couplingData.ConnectionNetId, out TrainCar otherCar)) { - Multiplayer.LogWarning($"AutoCouple([{currentCoupler?.train?.GetNetId()}, {currentCoupler?.train?.ID}]) did not find car at {(currentCoupler.isFrontCoupler ? "Front" : "Rear")} car with netId: {connectionNetId}"); + Multiplayer.LogWarning($"AutoCouple([{currentCoupler?.train?.GetNetId()}, {currentCoupler?.train?.ID}]) did not find car at {(currentCoupler.isFrontCoupler ? "Front" : "Rear")} car with netId: {couplingData.ConnectionNetId}"); return; } - var otherCoupler = connectionToFront ? otherCar.frontCoupler : otherCar.rearCoupler; + var otherCoupler = couplingData.ConnectionToFront ? otherCar.frontCoupler : otherCar.rearCoupler; - if (isCoupled) + if (couplingData.IsCoupled) { //NetworkLifecycle.Instance.Client.LogDebug(() => $"AutoCouple() Coupling {(currentCoupler.isFrontCoupler? "Front" : "Rear")}: {currentCoupler?.train?.ID}, to {otherCar?.ID}, at: {(connectionToFront ? "Front" : "Rear")}"); - SetCouplingState(currentCoupler, otherCoupler, couplingState); + SetCouplingState(currentCoupler, otherCoupler, couplingData.State); } - if (isHoseConnected) + if (couplingData.HoseConnected) { - CarsSaveManager.RestoreHoseAndCock(currentCoupler, isHoseConnected, cockOpen); + CarsSaveManager.RestoreHoseAndCock(currentCoupler, couplingData.HoseConnected, couplingData.CockOpen); } } @@ -190,26 +166,26 @@ public static void SetCouplingState(Coupler coupler, Coupler otherCoupler, Chain } - private static void SetBrakeParams(TrainsetSpawnPart spawnPart, TrainCar trainCar) + private static void SetBrakeParams(BrakeSystemData brakeSystemData, TrainCar trainCar) { BrakeSystem bs = trainCar.brakeSystem; if (bs == null) { - Multiplayer.LogWarning($"NetworkedCarSpawner.SetBrakeParams() Brake system is null! netId: {spawnPart.NetId}, trainCar: {spawnPart.CarId}"); + Multiplayer.LogWarning($"NetworkedCarSpawner.SetBrakeParams() Brake system is null! netId: {trainCar?.GetNetId()}, trainCar: {trainCar?.ID}"); return; } if(bs.hasHandbrake) - bs.SetHandbrakePosition(spawnPart.HandBrakePosition); + bs.SetHandbrakePosition(brakeSystemData.HandBrakePosition); if(bs.hasTrainBrake) - bs.trainBrakePosition = spawnPart.TrainBrakePosition; + bs.trainBrakePosition = brakeSystemData.TrainBrakePosition; - bs.SetBrakePipePressure(spawnPart.BrakePipePressure); - bs.SetAuxReservoirPressure(spawnPart.AuxResPressure); - bs.SetMainReservoirPressure(spawnPart.MainResPressure); - bs.SetControlReservoirPressure(spawnPart.ControlResPressure); - bs.ForceCylinderPressure(spawnPart.BrakeCylPressure); + bs.SetBrakePipePressure(brakeSystemData.BrakePipePressure); + bs.SetAuxReservoirPressure(brakeSystemData.AuxResPressure); + bs.SetMainReservoirPressure(brakeSystemData.MainResPressure); + bs.SetControlReservoirPressure(brakeSystemData.ControlResPressure); + bs.ForceCylinderPressure(brakeSystemData.BrakeCylPressure); } } diff --git a/Multiplayer/Networking/Data/Train/BrakeSystemData.cs b/Multiplayer/Networking/Data/Train/BrakeSystemData.cs new file mode 100644 index 0000000..2fef7bb --- /dev/null +++ b/Multiplayer/Networking/Data/Train/BrakeSystemData.cs @@ -0,0 +1,89 @@ +using DV.Simulation.Brake; +using LiteNetLib.Utils; + +namespace Multiplayer.Networking.Data.Train; + +public readonly struct BrakeSystemData +{ + public readonly bool HasHandbrake; + public readonly bool HasTrainbrake; + public readonly float HandBrakePosition; + public readonly float TrainBrakePosition; + public readonly float BrakePipePressure; + public readonly float AuxResPressure; + public readonly float MainResPressure; + public readonly float ControlResPressure; + public readonly float BrakeCylPressure; + + public BrakeSystemData( + bool hasHandbrake, bool hasTrainbrake, + float handBrakePosition, float trainBrakePosition, + float brakePipePressure, float auxResPressure, + float mainResPressure, float controlResPressure, + float brakeCylPressure) + { + HasHandbrake = hasHandbrake; + HasTrainbrake = hasTrainbrake; + HandBrakePosition = handBrakePosition; + TrainBrakePosition = trainBrakePosition; + BrakePipePressure = brakePipePressure; + AuxResPressure = auxResPressure; + MainResPressure = mainResPressure; + ControlResPressure = controlResPressure; + BrakeCylPressure = brakeCylPressure; + } + + public static void Serialize(NetDataWriter writer, BrakeSystemData data) + { + writer.Put(data.HasHandbrake); + if (data.HasHandbrake) + writer.Put(data.HandBrakePosition); + + writer.Put(data.HasTrainbrake); + if (data.HasTrainbrake) + writer.Put(data.TrainBrakePosition); + + writer.Put(data.BrakePipePressure); + writer.Put(data.AuxResPressure); + writer.Put(data.MainResPressure); + writer.Put(data.ControlResPressure); + writer.Put(data.BrakeCylPressure); + } + + public static BrakeSystemData Deserialize(NetDataReader reader) + { + bool hasHandbrake = reader.GetBool(); + float handBrakePosition = hasHandbrake ? reader.GetFloat() : 0f; + + bool hasTrainbrake = reader.GetBool(); + float trainBrakePosition = hasTrainbrake ? reader.GetFloat() : 0f; + + return new BrakeSystemData( + hasHandbrake, + hasTrainbrake, + handBrakePosition, + trainBrakePosition, + reader.GetFloat(), // BrakePipePressure + reader.GetFloat(), // AuxResPressure + reader.GetFloat(), // MainResPressure + reader.GetFloat(), // ControlResPressure + reader.GetFloat() // BrakeCylPressure + ); + } + + public static BrakeSystemData From(BrakeSystem brakeSystem) + { + return new BrakeSystemData( + hasHandbrake: brakeSystem.hasHandbrake, + hasTrainbrake: brakeSystem.hasTrainBrake, + handBrakePosition: brakeSystem.handbrakePosition, + trainBrakePosition: brakeSystem.trainBrakePosition, + brakePipePressure: brakeSystem.brakePipePressure, + auxResPressure: brakeSystem.auxReservoirPressure, + mainResPressure: brakeSystem.mainReservoirPressure, + controlResPressure: brakeSystem.controlReservoirPressure, + brakeCylPressure: brakeSystem.brakeCylinderPressure + ); + } + +} diff --git a/Multiplayer/Networking/Data/Train/CouplingData.cs b/Multiplayer/Networking/Data/Train/CouplingData.cs new file mode 100644 index 0000000..b4469c3 --- /dev/null +++ b/Multiplayer/Networking/Data/Train/CouplingData.cs @@ -0,0 +1,77 @@ +using LiteNetLib.Utils; +using Multiplayer.Utils; + +namespace Multiplayer.Networking.Data.Train; + +public readonly struct CouplingData +{ + public readonly bool IsCoupled; + public readonly ChainCouplerInteraction.State State; + public readonly ushort ConnectionNetId; + public readonly bool ConnectionToFront; + public readonly bool HoseConnected; + public readonly bool PreventAutoCouple; + public readonly bool CockOpen; + + public CouplingData(bool isCoupled, bool hoseConnected, ChainCouplerInteraction.State state, + ushort connectionNetId, bool connectionToFront, bool preventAutoCouple, bool cockOpen) + { + IsCoupled = isCoupled; + State = state; + ConnectionNetId = connectionNetId; + ConnectionToFront = connectionToFront; + HoseConnected = hoseConnected; + PreventAutoCouple = preventAutoCouple; + CockOpen = cockOpen; + } + + public static void Serialize(NetDataWriter writer, CouplingData data) + { + writer.Put(data.IsCoupled); + writer.Put(data.HoseConnected); + writer.Put((byte)data.State); + + if (data.IsCoupled || data.HoseConnected) + { + writer.Put(data.ConnectionNetId); + writer.Put(data.ConnectionToFront); + } + + writer.Put(data.PreventAutoCouple); + writer.Put(data.CockOpen); + } + + public static CouplingData Deserialize(NetDataReader reader) + { + bool isCoupled = reader.GetBool(); + bool hoseConnected = reader.GetBool(); + var state = (ChainCouplerInteraction.State)reader.GetByte(); + + ushort connectionNetId = 0; + bool connectionToFront = false; + + if (isCoupled || hoseConnected) + { + connectionNetId = reader.GetUShort(); + connectionToFront = reader.GetBool(); + } + + bool preventAutoCouple = reader.GetBool(); + bool cockOpen = reader.GetBool(); + + return new CouplingData(isCoupled, hoseConnected, state, connectionNetId, + connectionToFront, preventAutoCouple, cockOpen); + } + public static CouplingData From(Coupler coupler) + { + return new CouplingData( + isCoupled: coupler.IsCoupled(), + hoseConnected: coupler.hoseAndCock.IsHoseConnected, + state: coupler.state, + connectionNetId: coupler.IsCoupled() ? coupler.coupledTo.train.GetNetId() : (ushort)0, + connectionToFront: coupler.IsCoupled() ? coupler.coupledTo.isFrontCoupler : false, + preventAutoCouple: coupler.preventAutoCouple, + cockOpen: coupler.IsCockOpen + ); + } +} diff --git a/Multiplayer/Networking/Data/Train/TrainsetSpawnPart.cs b/Multiplayer/Networking/Data/Train/TrainsetSpawnPart.cs index 008a555..a8b5761 100644 --- a/Multiplayer/Networking/Data/Train/TrainsetSpawnPart.cs +++ b/Multiplayer/Networking/Data/Train/TrainsetSpawnPart.cs @@ -1,3 +1,4 @@ +using DV.Customization.Paint; using DV.ThingTypes; using LiteNetLib.Utils; using Multiplayer.Components.Networking; @@ -18,152 +19,71 @@ public readonly struct TrainsetSpawnPart public readonly ushort NetId; - //car details + // Car details public readonly string LiveryId; public readonly string CarId; public readonly string CarGuid; - //Cargo details - public readonly CargoType CargoType; - public readonly float LoadedAmount; - - //customisation details + // Customisation details public readonly bool PlayerSpawnedCar; + public readonly TrainCarPaint PaintExterior; + public readonly TrainCarPaint PaintInterior; - //coupling details - public readonly ushort FrontConnectionNetId; //if we are coupled or hosed this will be the netId of the other car - public readonly bool FrontConnectionToFront; //if we are coupled or hosed this will be 'true' if connected to the front other car - public readonly bool IsFrontCoupled; - public readonly ChainCouplerInteraction.State FrontState; - public readonly bool FrontHoseConnected; - public readonly bool PreventFrontAutoCouple; - - public readonly ushort RearConnectionNetId; //if we are coupled or hosed this will be the netId of the other car - public readonly bool RearConnectionToFront; //if we are coupled or hosed this will be 'true' if connected to the front other car - public readonly bool IsRearCoupled; - public readonly ChainCouplerInteraction.State RearState; - public readonly bool RearHoseConnected; - public readonly bool PreventRearAutoCouple; + // Coupling data + public readonly CouplingData FrontCoupling; + public readonly CouplingData RearCoupling; - //positional details + // Positional details public readonly float Speed; public readonly Vector3 Position; public readonly Quaternion Rotation; - //bogie details + // Bogie data public readonly BogieData Bogie1; public readonly BogieData Bogie2; - //brake initial states - public readonly bool HasHandbrake; - public readonly bool HasTrainbrake; - public readonly float HandBrakePosition; - public readonly float TrainBrakePosition; - public readonly float BrakePipePressure; - public readonly float AuxResPressure; - public readonly float MainResPressure; - public readonly float ControlResPressure; - public readonly float BrakeCylPressure; - - public readonly bool FrontCockOpen; - public readonly bool RearCockOpen; + // Brake initial states + public readonly BrakeSystemData BrakeData; - private TrainsetSpawnPart(ushort netId, string liveryId, string carId, string carGuid, bool playerSpawnedCar, - bool isFrontCoupled, ChainCouplerInteraction.State frontState, ushort frontConnectionNetId, bool frontConnectedToFront, bool preventFrontAutoCouple, - bool isRearCoupled, ChainCouplerInteraction.State rearState, ushort rearConnectionNetId, bool rearConnectedToFront, bool preventRearAutoCouple, - float speed, Vector3 position, Quaternion rotation, - BogieData bogie1, BogieData bogie2, - float? handBrakePos, float? trainBrakePos, float brakePipePress, float auxResPress, float mainResPress, float controlResPress, float brakeCylPress, - bool frontHoseConnected, - bool rearHoseConnected, - bool frontCockOpen, bool rearCockOpen) + public TrainsetSpawnPart( + ushort netId, string liveryId, string carId, string carGuid, bool playerSpawnedCar, + CouplingData frontCoupling, CouplingData rearCoupling, + float speed, Vector3 position, Quaternion rotation, + BogieData bogie1, BogieData bogie2, BrakeSystemData brakeData) { NetId = netId; - LiveryId = liveryId; CarId = carId; CarGuid = carGuid; - PlayerSpawnedCar = playerSpawnedCar; - - IsFrontCoupled = isFrontCoupled; - FrontState = frontState; - FrontConnectionNetId = frontConnectionNetId; - FrontConnectionToFront = frontConnectedToFront; - FrontHoseConnected = frontHoseConnected; - PreventFrontAutoCouple = preventFrontAutoCouple; - - IsRearCoupled = isRearCoupled; - RearState = rearState; - RearConnectionNetId = rearConnectionNetId; - RearConnectionToFront = rearConnectedToFront; - RearHoseConnected = rearHoseConnected; - PreventRearAutoCouple = preventRearAutoCouple; - - + FrontCoupling = frontCoupling; + RearCoupling = rearCoupling; Speed = speed; Position = position; Rotation = rotation; - Bogie1 = bogie1; Bogie2 = bogie2; - - HasHandbrake = handBrakePos != null; - HasTrainbrake = trainBrakePos != null; - - if (HasHandbrake) - HandBrakePosition = (float)handBrakePos; - - if (HasTrainbrake) - TrainBrakePosition = (float)trainBrakePos; - - BrakePipePressure = brakePipePress; - AuxResPressure = auxResPress; - MainResPressure = mainResPress; - ControlResPressure = controlResPress; - BrakeCylPressure = brakeCylPress; - - FrontCockOpen = frontCockOpen; - RearCockOpen = rearCockOpen; + BrakeData = brakeData; } public static void Serialize(NetDataWriter writer, TrainsetSpawnPart data) { writer.Put(data.NetId); - writer.Put(data.LiveryId); writer.Put(data.CarId); - //encode our Guid to save 50% bytes in the packet size if (Guid.TryParse(data.CarGuid, out Guid guid)) writer.PutBytesWithLength(guid.ToByteArray()); else { - Multiplayer.LogError($"TrainsetSpawnPart.TrainsetSpawnPart() failed to parse carGuid: {data.CarGuid}"); + Multiplayer.LogError($"TrainsetSpawnPart.Serialize() failed to parse carGuid: {data.CarGuid}"); writer.PutBytesWithLength(EMPTY_GUID); } writer.Put(data.PlayerSpawnedCar); - writer.Put(data.IsFrontCoupled); - writer.Put(data.FrontHoseConnected); - writer.Put((byte)data.FrontState); - if (data.IsFrontCoupled || data.FrontHoseConnected) - { - writer.Put(data.FrontConnectionNetId); - writer.Put(data.FrontConnectionToFront); - } - writer.Put(data.PreventFrontAutoCouple); - - writer.Put(data.IsRearCoupled); - writer.Put(data.RearHoseConnected); - writer.Put((byte)data.RearState); - if (data.IsRearCoupled || data.RearHoseConnected) - { - writer.Put(data.RearConnectionNetId); - writer.Put(data.RearConnectionToFront); - } - writer.Put(data.PreventRearAutoCouple); + CouplingData.Serialize(writer, data.FrontCoupling); + CouplingData.Serialize(writer, data.RearCoupling); writer.Put(data.Speed); Vector3Serializer.Serialize(writer, data.Position); @@ -171,104 +91,33 @@ public static void Serialize(NetDataWriter writer, TrainsetSpawnPart data) BogieData.Serialize(writer, data.Bogie1); BogieData.Serialize(writer, data.Bogie2); - - writer.Put(data.HasHandbrake); - if (data.HasHandbrake) - writer.Put(data.HandBrakePosition); - - writer.Put(data.HasTrainbrake); - if (data.HasTrainbrake) - writer.Put(data.TrainBrakePosition); - - writer.Put(data.BrakePipePressure); - writer.Put(data.AuxResPressure); - writer.Put(data.MainResPressure); - writer.Put(data.ControlResPressure); - writer.Put(data.BrakeCylPressure); - - writer.Put(data.FrontCockOpen); - writer.Put(data.RearCockOpen); + BrakeSystemData.Serialize(writer, data.BrakeData); } public static TrainsetSpawnPart Deserialize(NetDataReader reader) { - ushort netId = reader.GetUShort(); //NetId + ushort netId = reader.GetUShort(); + string liveryId = reader.GetString(); + string carId = reader.GetString(); + string carGuid = new Guid(reader.GetBytesWithLength()).ToString(); + bool playerSpawnedCar = reader.GetBool(); - string liveryId = reader.GetString(); //LiveryId - string carId = reader.GetString(); //CarId - byte[] guidBytes = reader.GetBytesWithLength(); //GuiId + var frontCoupling = CouplingData.Deserialize(reader); + var rearCoupling = CouplingData.Deserialize(reader); - string carGuid = new Guid(guidBytes).ToString(); //decode GuiId + float speed = reader.GetFloat(); + Vector3 position = Vector3Serializer.Deserialize(reader); + Quaternion rotation = QuaternionSerializer.Deserialize(reader); - bool playerSpawnedCar = reader.GetBool(); //PlayerSpawnedCar - - bool isFrontCoupled = reader.GetBool(); //IsFrontCoupled - bool isFrontHoseConnected = reader.GetBool(); //IsFrontHose - ChainCouplerInteraction.State frontState = (ChainCouplerInteraction.State)reader.GetByte(); - - ushort frontConnectedToNetId = 0; - bool frontConnectedToFront = false; - if (isFrontCoupled || isFrontHoseConnected) - { - frontConnectedToNetId = reader.GetUShort(); - frontConnectedToFront = reader.GetBool(); - } - bool preventFrontAutoCouple = reader.GetBool(); - - bool isRearCoupled = reader.GetBool(); //IsRearCoupled - bool isRearHoseConnected = reader.GetBool(); //IsRearHose - ChainCouplerInteraction.State rearState = (ChainCouplerInteraction.State)reader.GetByte(); - ushort rearConnectedToNetId = 0; - bool rearConnectedToFront = false; - if (isRearCoupled || isRearHoseConnected) - { - rearConnectedToNetId = reader.GetUShort(); - rearConnectedToFront = reader.GetBool(); - } - bool preventRearAutoCouple = reader.GetBool(); + var bogie1 = BogieData.Deserialize(reader); + var bogie2 = BogieData.Deserialize(reader); + var brakeSet = BrakeSystemData.Deserialize(reader); return new TrainsetSpawnPart( - netId, - - liveryId, - carId, - carGuid, - - playerSpawnedCar, - - isFrontCoupled, - frontState, - frontConnectedToNetId, - frontConnectedToFront, - preventFrontAutoCouple, - - isRearCoupled, - rearState, - rearConnectedToNetId, - rearConnectedToFront, - preventRearAutoCouple, - - reader.GetFloat(), //Speed - Vector3Serializer.Deserialize(reader), //Position - QuaternionSerializer.Deserialize(reader), //Rotation - - BogieData.Deserialize(reader), //Bogie 1 - BogieData.Deserialize(reader), //Bogie 2 - - reader.GetBool() ? reader.GetFloat() : null, //HandbrakePos - reader.GetBool() ? reader.GetFloat() : null, //TrainBrakePos - reader.GetFloat(), //BrakePipePressure - reader.GetFloat(), //AuxResPressure - reader.GetFloat(), //MainResPressure - reader.GetFloat(), //ControlResPressure - reader.GetFloat(), //BrakeCylPressure - - isFrontHoseConnected, //FrontHoseConnected - isRearHoseConnected, //RearHoseConnected - - reader.GetBool(), //FrontCockOpen - reader.GetBool() //RearCockOpen - ); + netId, liveryId, carId, carGuid, playerSpawnedCar, + frontCoupling, rearCoupling, + speed, position, rotation, + bogie1, bogie2, brakeSet); } public static TrainsetSpawnPart FromTrainCar(NetworkedTrainCar networkedTrainCar) @@ -276,141 +125,23 @@ public static TrainsetSpawnPart FromTrainCar(NetworkedTrainCar networkedTrainCar TrainCar trainCar = networkedTrainCar.TrainCar; Transform transform = networkedTrainCar.transform; - ushort frontConnectedTo = 0; - bool frontConnectedToFront = false; - ChainCouplerInteraction.State frontCouplerState = ChainCouplerInteraction.State.Parked; - - ushort rearConnectedTo = 0; - bool rearConnectedToFront = false; - ChainCouplerInteraction.State rearCouplerState = ChainCouplerInteraction.State.Parked; - - bool frontCouplerIsCoupled = false; - bool preventFrontAutoCouple = false; - bool rearCouplerIsCoupled = false; - bool preventRearAutoCouple = false; - - bool frontHoseConnected = false; - bool rearHoseConnected = false; - - bool frontCockOpen = false; - bool rearCockOpen = false; - - - NetworkLifecycle.Instance.Server.LogDebug(() => - { - return $"TrainsetSpawnPart.FromTrainCar({networkedTrainCar?.NetId}) TrainCarID: {trainCar?.ID}, LiveryID: {trainCar?.carLivery?.id}, " + - $"Front[Coupled:{trainCar?.frontCoupler?.IsCoupled()}, State:{trainCar?.frontCoupler?.state}, Hose:{trainCar?.frontCoupler?.hoseAndCock?.IsHoseConnected}, Cock:{trainCar?.frontCoupler?.IsCockOpen}], " + - $"Rear[Coupled:{trainCar?.rearCoupler?.IsCoupled()}, State:{trainCar?.rearCoupler?.state}, Hose:{trainCar?.rearCoupler?.hoseAndCock?.IsHoseConnected}, Cock:{trainCar?.rearCoupler?.IsCockOpen}]"; - }); - - - if (trainCar.frontCoupler.IsCoupled()) - { - Multiplayer.LogDebug(() => $"FromTrainCar([{networkedTrainCar?.NetId},{networkedTrainCar?.TrainCar?.ID}]) front is coupled to netID: {trainCar?.frontCoupler?.coupledTo?.train?.GetNetId()}"); - frontConnectedTo = trainCar.frontCoupler.coupledTo.train.GetNetId(); - frontConnectedToFront = trainCar.frontCoupler.coupledTo.isFrontCoupler; - } - else if (trainCar.frontCoupler.hoseAndCock.IsHoseConnected) - { - Multiplayer.LogDebug(() => $"FromTrainCar([{networkedTrainCar?.NetId},{networkedTrainCar?.TrainCar?.ID}]) front hose connected to netID: {trainCar?.frontCoupler?.coupledTo?.train?.GetNetId()}"); - frontConnectedTo = trainCar.frontCoupler.GetAirHoseConnectedTo().train.GetNetId(); - frontConnectedToFront = trainCar.frontCoupler.GetAirHoseConnectedTo().isFrontCoupler; - } - - if (trainCar.rearCoupler.IsCoupled()) - { - Multiplayer.LogDebug(() => $"FromTrainCar([{networkedTrainCar?.NetId},{networkedTrainCar?.TrainCar?.ID}]) rear is coupled to netID: {trainCar?.rearCoupler?.coupledTo?.train?.GetNetId()}"); - rearConnectedTo = trainCar.rearCoupler.coupledTo.train.GetNetId(); - rearConnectedToFront = trainCar.rearCoupler.coupledTo.isFrontCoupler; - } - else if (trainCar.rearCoupler.hoseAndCock.IsHoseConnected) - { - Multiplayer.LogDebug(() => $"FromTrainCar([{networkedTrainCar?.NetId},{networkedTrainCar?.TrainCar?.ID}]) rear hose connected to netID: {trainCar?.rearCoupler?.coupledTo?.train?.GetNetId()}"); - rearConnectedTo = trainCar.rearCoupler.GetAirHoseConnectedTo().train.GetNetId(); - rearConnectedToFront = trainCar.rearCoupler.GetAirHoseConnectedTo().isFrontCoupler; - } - - frontCouplerIsCoupled = trainCar.frontCoupler.IsCoupled(); - preventFrontAutoCouple = trainCar.frontCoupler.preventAutoCouple; - rearCouplerIsCoupled = trainCar.rearCoupler.IsCoupled(); - preventRearAutoCouple = trainCar.rearCoupler.preventAutoCouple; - - frontCouplerState = trainCar.frontCoupler.state; - rearCouplerState = trainCar.rearCoupler.state; - - frontHoseConnected = trainCar.frontCoupler.hoseAndCock.IsHoseConnected; - rearHoseConnected = trainCar.rearCoupler.hoseAndCock.IsHoseConnected; - - frontCockOpen = trainCar.frontCoupler.IsCockOpen; - rearCockOpen = trainCar.rearCoupler.IsCockOpen; - return new TrainsetSpawnPart( - networkedTrainCar.NetId, - - trainCar.carLivery.id, - trainCar.ID, - trainCar.CarGUID, - - trainCar.playerSpawnedCar, - - frontCouplerIsCoupled, - frontCouplerState, - frontConnectedTo, - frontConnectedToFront, - preventFrontAutoCouple, - - rearCouplerIsCoupled, - rearCouplerState, - rearConnectedTo, - rearConnectedToFront, - preventRearAutoCouple, - - trainCar.GetForwardSpeed(), - transform.position - WorldMover.currentMove, - transform.rotation, - - BogieData.FromBogie(trainCar.Bogies[0], true), - BogieData.FromBogie(trainCar.Bogies[1], true), - - trainCar.brakeSystem.hasHandbrake ? trainCar.brakeSystem.handbrakePosition : null, - trainCar.brakeSystem.hasTrainBrake ? trainCar.brakeSystem.trainBrakePosition : null, - trainCar.brakeSystem.brakePipePressure, - trainCar.brakeSystem.auxReservoirPressure, - trainCar.brakeSystem.mainReservoirPressure, - trainCar.brakeSystem.controlReservoirPressure, - trainCar.brakeSystem.brakeCylinderPressure, - - frontHoseConnected, - rearHoseConnected, - frontCockOpen, - rearCockOpen + netId: networkedTrainCar.NetId, + liveryId: trainCar.carLivery.id, + carId: trainCar.ID, + carGuid: trainCar.CarGUID, + playerSpawnedCar: trainCar.playerSpawnedCar, + frontCoupling: CouplingData.From(trainCar.frontCoupler), + rearCoupling: CouplingData.From(trainCar.rearCoupler), + speed: trainCar.GetForwardSpeed(), + position: transform.position - WorldMover.currentMove, + rotation: transform.rotation, + bogie1: BogieData.FromBogie(trainCar.Bogies[0], true), + bogie2: BogieData.FromBogie(trainCar.Bogies[1], true), + brakeData: BrakeSystemData.From(trainCar.brakeSystem) ); } - //public static TrainsetSpawnPart[] FromTrainSet(Trainset trainset) - //{ - // if (trainset == null) - // { - // NetworkLifecycle.Instance.Server.LogWarning("TrainsetSpawnPart.FromTrainSet() trainset is null!"); - // return null; - // } - - // TrainsetSpawnPart[] parts = new TrainsetSpawnPart[trainset.cars.Count]; - // for (int i = 0; i < trainset.cars.Count; i++) - // { - // NetworkedTrainCar networkedTrainCar; - - // if (!trainset.cars[i].TryNetworked(out networkedTrainCar)) - // { - // NetworkLifecycle.Instance.Server.LogWarning($"TrainsetSpawnPart.FromTrainSet({trainset?.id}) Failed to find NetworkedTrainCar for: {trainset?.cars[i]?.ID}"); - // networkedTrainCar = trainset.cars[i].GetOrAddComponent(); - // } - - // parts[i] = FromTrainCar(networkedTrainCar); - // } - // return parts; - //} - public static TrainsetSpawnPart[] FromTrainSet(List trainset/*, bool resolveCoupling = false*/) { if (trainset == null) From 838543415e1bf073d0ae724eea0f28997ad0f278 Mon Sep 17 00:00:00 2001 From: AMacro Date: Fri, 27 Dec 2024 23:29:37 +1000 Subject: [PATCH 138/188] Rework of coupler interaction messages Interaction is now synchronised via an interaction message, allowing tight, loose and parked states to be sync'd More work may be required to harden against de-syncs (e.g. 2 players interacting with the same coupler at once). HUD interaction with the chain is now also sync'd this way, and remote control chain interaction will need to be patched as well. --- .../Networking/Train/NetworkedTrainCar.cs | 250 ++++++++++++++++++ Multiplayer/Multiplayer.cs | 3 +- Multiplayer/Multiplayer.csproj | 4 +- .../Data/Train/CouplerInteractionType.cs | 24 ++ .../Managers/Client/NetworkClient.cs | 83 +++++- .../Managers/Server/NetworkServer.cs | 8 +- .../Train/CommonCouplerInteractionPacket.cs | 13 + .../Train/CouplerChainInteractionPatch.cs | 36 +++ .../Patches/Train/CouplerInterfacerPatch.cs | 88 ++++++ Multiplayer/Patches/Train/CouplerPatch.cs | 54 +--- Multiplayer/Patches/Train/HoseAndCockPatch.cs | 8 +- info.json | 2 +- 12 files changed, 508 insertions(+), 65 deletions(-) create mode 100644 Multiplayer/Networking/Data/Train/CouplerInteractionType.cs create mode 100644 Multiplayer/Networking/Packets/Common/Train/CommonCouplerInteractionPacket.cs create mode 100644 Multiplayer/Patches/Train/CouplerChainInteractionPatch.cs create mode 100644 Multiplayer/Patches/Train/CouplerInterfacerPatch.cs diff --git a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs index d270188..d738391 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs @@ -1,3 +1,4 @@ +using System; using System.Collections; using System.Collections.Generic; using System.Linq; @@ -63,6 +64,8 @@ public static bool TryGetFromTrainCar(TrainCar trainCar, out NetworkedTrainCar n #endregion + private const int MAX_COUPLER_ITERATIONS = 10; + public TrainCar TrainCar; public uint TicksSinceSync = uint.MaxValue; public bool HasPlayers => PlayerManager.Car == TrainCar || GetComponentInChildren() != null; @@ -99,6 +102,7 @@ public static bool TryGetFromTrainCar(TrainCar trainCar, out NetworkedTrainCar n public TickedQueue client_bogie1Queue; public TickedQueue client_bogie2Queue; + private Coupler couplerInteraction; #endregion protected override bool IsIdServerAuthoritative => true; @@ -132,8 +136,21 @@ private void Start() brakeSystem = TrainCar.brakeSystem; foreach (Coupler coupler in TrainCar.couplers) + { hoseToCoupler[coupler.hoseAndCock] = coupler; + Multiplayer.LogDebug(() => $"TrainCar.Start() [{TrainCar?.ID}, {NetId}], Coupler exists: {coupler != null}, ChainScript exists: {coupler.ChainScript != null}"); + try + { + + coupler.ChainScript.StateChanged += (state) => { Client_CouplerStateChange(state, coupler); }; + } + catch (Exception ex) + { + Multiplayer.LogError($"Error subscribing to coupler state changes [{TrainCar?.ID}, {NetId}]\r\n{ex.Message}\r\n{ex.StackTrace}"); + } + } + SimController simController = GetComponent(); if (simController != null) { @@ -625,6 +642,178 @@ public void Common_UpdateFuses(CommonTrainFusesPacket packet) simulationFlow.fullFuseIdToFuse[packet.FuseIds[i]].ChangeState(packet.FuseValues[i]); } + public void Common_ReceiveCouplerInteraction(CommonCouplerInteractionPacket packet) + { + + Coupler coupler = packet.IsFrontCoupler ? TrainCar.frontCoupler : TrainCar.rearCoupler; + + if (coupler == null) + { + Multiplayer.LogWarning($"Common_ReceiveCouplerInteraction() did not find coupler for [{TrainCar?.ID}, {NetId}], coupler is front: {packet.IsFrontCoupler}"); + return; + } + + CouplerInteractionType flags = (CouplerInteractionType)packet.Flags; + + Multiplayer.LogDebug(() => $"Common_ReceiveCouplerInteraction() [{TrainCar?.ID}, {NetId}], coupler is front: {packet.IsFrontCoupler}, flags: {flags}, otherCouplerNetId: {packet.OtherNetId}"); + + if (flags.HasFlag(CouplerInteractionType.CouplerCouple) && packet.OtherNetId != 0) + { + Multiplayer.LogDebug(() => $"1 Common_ReceiveCouplerInteraction() [{TrainCar?.ID}, {NetId}], coupler is front: {packet.IsFrontCoupler}, flags: {flags} "); + if (GetTrainCar(packet.OtherNetId, out TrainCar otherCar)) + { + Multiplayer.LogDebug(() => $"2 Common_ReceiveCouplerInteraction() [{TrainCar?.ID}, {NetId}], coupler is front: {packet.IsFrontCoupler}, flags: {flags}"); + Coupler otherCoupler = packet.IsFrontOtherCoupler ? otherCar.frontCoupler : otherCar.rearCoupler; + + StartCoroutine(LooseAttachCoupler(coupler, otherCoupler)); + } + } + + if (flags.HasFlag(CouplerInteractionType.CouplerPark)) + { + Multiplayer.LogDebug(() => $"3 Common_ReceiveCouplerInteraction() [{TrainCar?.ID}, {NetId}], coupler is front: {packet.IsFrontCoupler}, flags: {flags}, current state: {coupler.state}, Chain state:{coupler.ChainScript.state}, isCoupled: {coupler.IsCoupled()}"); + + if (coupler.ChainScript.state != ChainCouplerInteraction.State.Attached_Tight) + StartCoroutine(ParkCoupler(coupler)); + else + Multiplayer.LogWarning(() => $"Received Park interaction for [{TrainCar?.ID}, {NetId}], coupler is front: {packet.IsFrontCoupler}, but coupler is in the wrong state: {coupler.state}, Chain state:{coupler.ChainScript.state}, isCoupled: {coupler.IsCoupled()}"); + + Multiplayer.LogDebug(() => $"4 Common_ReceiveCouplerInteraction() [{TrainCar?.ID}, {NetId}], coupler is front: {packet.IsFrontCoupler}, flags: {flags} restorestate: {coupler.state}, current state: {coupler.state}, Chain state:{coupler.ChainScript.state}, isCoupled: {coupler.IsCoupled()}"); + } + + if (flags.HasFlag(CouplerInteractionType.CouplerDrop)) + { + Multiplayer.LogDebug(() => $"5 Common_ReceiveCouplerInteraction() [{TrainCar?.ID}, {NetId}], coupler is front: {packet.IsFrontCoupler}, flags: {flags} restorestate: {coupler.state}, current state: {coupler.state}, Chain state:{coupler.ChainScript.state}, isCoupled: {coupler.IsCoupled()}"); + + if (coupler.ChainScript.state != ChainCouplerInteraction.State.Attached_Tight) + { + Multiplayer.LogDebug(() => $"5A Common_ReceiveCouplerInteraction() [{TrainCar?.ID}, {NetId}], coupler is front: {packet.IsFrontCoupler}, flags: {flags} restorestate: {coupler.state}, current state: {coupler.state}, Chain state:{coupler.ChainScript.state}, isCoupled: {coupler.IsCoupled()}"); + StartCoroutine(DangleCoupler(coupler)); + } + else + Multiplayer.LogWarning(() => $"Received Dangle interaction for [{TrainCar?.ID}, {NetId}], coupler is front: {packet.IsFrontCoupler}, but coupler is in the wrong state: {coupler.state}, Chain state:{coupler.ChainScript.state}, isCoupled: {coupler.IsCoupled()}"); + } + + if (flags.HasFlag(CouplerInteractionType.CouplerLoosen)) + { + Multiplayer.LogDebug(() => $"6 Common_ReceiveCouplerInteraction() [{TrainCar?.ID}, {NetId}], flags: {flags} current state: {coupler.ChainScript.state}"); + if (coupler.ChainScript.state == ChainCouplerInteraction.State.Attached_Tight) + { + Multiplayer.LogDebug(() => $"7 Common_ReceiveCouplerInteraction() [{TrainCar?.ID}, {NetId}], coupler is front: {packet.IsFrontCoupler}, flags: {flags}"); + coupler.ChainScript.fsm.Fire(ChainCouplerInteraction.Trigger.Screw_Used); + } + } + + if (flags.HasFlag(CouplerInteractionType.CouplerTighten)) + { + Multiplayer.LogDebug(() => $"8 Common_ReceiveCouplerInteraction() [{TrainCar?.ID}, {NetId}], flags: {flags} current state: {coupler.ChainScript.state}"); + if (coupler.ChainScript.state == ChainCouplerInteraction.State.Attached_Loose) + { + Multiplayer.LogDebug(() => $"9 Common_ReceiveCouplerInteraction() [{TrainCar?.ID}, {NetId}], coupler is front: {packet.IsFrontCoupler}, flags: {flags}"); + coupler.ChainScript.fsm.Fire(ChainCouplerInteraction.Trigger.Screw_Used); + } + } + } + + private IEnumerator LooseAttachCoupler(Coupler coupler, Coupler otherCoupler) + { + if (coupler == null || coupler.ChainScript == null || + otherCoupler == null || otherCoupler.ChainScript == null || + otherCoupler.ChainScript.ownAttachPoint == null) + { + Multiplayer.LogDebug(() => $"LooseAttachCoupler() [{TrainCar?.ID}], Null reference! Coupler: {coupler != null}, chainscript: {coupler?.ChainScript != null}, other coupler: {otherCoupler != null}, other chainscript: {otherCoupler?.ChainScript != null}, other attach point: {otherCoupler?.ChainScript?.ownAttachPoint}"); + yield break; + } + ChainCouplerInteraction ccInteraction = coupler.ChainScript; + + //Simulate player pickup + coupler.ChainScript.fsm.Fire(ChainCouplerInteraction.Trigger.Picked_Up_By_Player); + + //Set the knob position to the other coupler's hook + Vector3 targetHookPos = otherCoupler.ChainScript.ownAttachPoint.transform.position; + coupler.ChainScript.knob.transform.position = targetHookPos; + + //allow the follower and IK solver to update + coupler.ChainScript.Update_Being_Dragged(); + + //we need to allow the IK solver to calculate the chain ring anchor's position, over a number of iterations + int x = 0; + float distance = float.MaxValue; + //game checks for Vector3.Distance(this.chainRingAnchor.position, this.closestAttachPoint.transform.position) < attachDistanceThreshold; + while (distance >= ChainCouplerInteraction.attachDistanceThreshold && x < MAX_COUPLER_ITERATIONS) + { + distance = Vector3.Distance(ccInteraction.chainRingAnchor.position, targetHookPos); + + x++; + yield return new WaitForSeconds(ccInteraction.ROTATION_SMOOTH_DURATION); + } + + //Drop the chain + coupler.ChainScript.fsm.Fire(ChainCouplerInteraction.Trigger.Dropped_By_Player); + } + + private IEnumerator ParkCoupler(Coupler coupler) + { + ChainCouplerInteraction ccInteraction = coupler.ChainScript; + + //Simulate player pickup + coupler.ChainScript.fsm.Fire(ChainCouplerInteraction.Trigger.Picked_Up_By_Player); + + //Set the knob position + Vector3 parkPos = coupler.ChainScript.parkedAnchor.position; + + coupler.ChainScript.knob.transform.position = parkPos; + + //allow the follower and IK solver to update + coupler.ChainScript.Update_Being_Dragged(); + + //we need to allow the IK solver to calculate the chain ring anchor's position, over a number of iterations + int x = 0; + float distance = float.MaxValue; + //game checks for Vector3.Distance(this.chainRingAnchor.position, this.parkedAnchor.position) < parkDistanceThreshold; + //need to make sure we are closer than the threshold before dropping + while (distance > ChainCouplerInteraction.parkDistanceThreshold && x < MAX_COUPLER_ITERATIONS) + { + distance = Vector3.Distance(ccInteraction.chainRingAnchor.position, ccInteraction.parkedAnchor.position); + + x++; + yield return new WaitForSeconds(ccInteraction.ROTATION_SMOOTH_DURATION); + } + + //Drop the chain + coupler.ChainScript.fsm.Fire(ChainCouplerInteraction.Trigger.Dropped_By_Player); + } + private IEnumerator DangleCoupler(Coupler coupler) + { + ChainCouplerInteraction ccInteraction = coupler.ChainScript; + + //Simulate player pickup + coupler.ChainScript.fsm.Fire(ChainCouplerInteraction.Trigger.Picked_Up_By_Player); + + Vector3 parkPos = coupler.ChainScript.parkedAnchor.position; + + //Set the knob position + coupler.ChainScript.knob.transform.position = parkPos + Vector3.down; //ensure we are not near the park anchor or other car's anchor + + //allow the follower and IK solver to update + coupler.ChainScript.Update_Being_Dragged(); + + //we need to allow the IK solver to calculate the chain ring anchor's position, over a number of iterations + int x = 0; + float distance = float.MinValue; + //game checks for Vector3.Distance(this.chainRingAnchor.position, this.parkedAnchor.position) < parkDistanceThreshold; + //to determine if it should be parked or dangled, need to make sure we are at least at the threshold before dropping + while (distance <= ChainCouplerInteraction.parkDistanceThreshold && x < MAX_COUPLER_ITERATIONS) + { + distance = Vector3.Distance(ccInteraction.chainRingAnchor.position, ccInteraction.parkedAnchor.position); + + x++; + yield return new WaitForSeconds(ccInteraction.ROTATION_SMOOTH_DURATION); + } + + //Drop the chain + coupler.ChainScript.fsm.Fire(ChainCouplerInteraction.Trigger.Dropped_By_Player); + } #endregion #region Client @@ -730,5 +919,66 @@ public void Client_ReceiveFireboxStateUpdate(float fireboxContents, bool isOn) firebox.fireboxContentsPort.Value = fireboxContents; firebox.fireOnPort.Value = isOn ? 1f : 0f; } + + public void Client_CouplerStateChange(ChainCouplerInteraction.State state, Coupler coupler) + { + Multiplayer.LogDebug(() => $"1 Client_CouplerStateChange({state}) trainCar: [{TrainCar?.ID}, {NetId}], coupler is front: {coupler?.isFrontCoupler}"); + + //if we are processing a packet, then these state changes are likely triggered by a received update, not player interaction + //in future, maybe patch OnGrab() or add logic to add/remove action subscriptions + if (NetworkLifecycle.Instance.IsProcessingPacket) + return; + + CouplerInteractionType interactionFlags = CouplerInteractionType.NoAction; + Coupler otherCoupler = null; + + switch (state) + { + case ChainCouplerInteraction.State.Being_Dragged: + couplerInteraction = coupler; + Multiplayer.LogDebug(() => $"3 Client_CouplerStateChange({state}) trainCar: [{TrainCar?.ID}, {NetId}]"); + break; + + case ChainCouplerInteraction.State.Attached_Loose: + if (couplerInteraction != null) + { + //couldn't find an appropriate constant in the game code, other than the default value + //at B99.3 this distance is 1.5f for both default and constant/magic number + otherCoupler = coupler.GetFirstCouplerInRange(); + Multiplayer.LogDebug(() => $"4 Client_CouplerStateChange({state}) trainCar: [{TrainCar?.ID}, {NetId}] coupledTo: {coupler?.coupledTo?.train?.ID}, first Coupler: {otherCoupler?.train?.ID}"); + interactionFlags = CouplerInteractionType.CouplerCouple; + } + break; + + case ChainCouplerInteraction.State.Parked: + if (couplerInteraction != null) + { + Multiplayer.LogDebug(() => $"6 Client_CouplerStateChange({state}) trainCar: [{TrainCar?.ID}, {NetId}]"); + interactionFlags = CouplerInteractionType.CouplerPark; + } + break; + + case ChainCouplerInteraction.State.Dangling: + if (couplerInteraction != null) + { + Multiplayer.LogDebug(() => $"7 Client_CouplerStateChange({state}) trainCar: [{TrainCar?.ID}, {NetId}]"); + interactionFlags = CouplerInteractionType.CouplerDrop; + } + break; + + default: + //nothing to do + break; + } + + if (interactionFlags != CouplerInteractionType.NoAction) + { + Multiplayer.LogDebug(() => $"8 Client_CouplerStateChange({state}) trainCar: [{TrainCar?.ID}, {NetId}], coupler is front: {coupler?.isFrontCoupler}, Sending: {interactionFlags}"); + couplerInteraction = null; + NetworkLifecycle.Instance.Client.SendCouplerInteraction(interactionFlags, coupler, otherCoupler); + return; + } + Multiplayer.LogDebug(() => $"9 Client_CouplerStateChange({state}) trainCar: [{TrainCar?.ID}, {NetId}]"); + } #endregion } diff --git a/Multiplayer/Multiplayer.cs b/Multiplayer/Multiplayer.cs index e613abc..2568cd6 100644 --- a/Multiplayer/Multiplayer.cs +++ b/Multiplayer/Multiplayer.cs @@ -2,6 +2,7 @@ using System.IO; using System.Linq; using System.Reflection; +using DV; using DV.UIFramework; using HarmonyLib; using JetBrains.Annotations; @@ -58,7 +59,7 @@ private static bool Load(UnityModManager.ModEntry modEntry) Locale.Load(ModEntry.Path); - Log($"Multiplayer JSON Version: {ModEntry.Info.Version}, Internal Version: {Ver} "); + Log($"Multiplayer JSON Version: {ModEntry.Info.Version}, Internal Version: {Ver}\r\nGame version: {BuildInfo.BUILD_VERSION_MAJOR.ToString()}.{BuildInfo.BUILDBOT_INFO.ToString()}"); Log("Patching..."); harmony = new Harmony(ModEntry.Info.Id); diff --git a/Multiplayer/Multiplayer.csproj b/Multiplayer/Multiplayer.csproj index 150d9e8..baaca99 100644 --- a/Multiplayer/Multiplayer.csproj +++ b/Multiplayer/Multiplayer.csproj @@ -3,7 +3,7 @@ net48 latest Multiplayer - 0.1.9.2 + 0.1.9.5 @@ -41,6 +41,8 @@ + + diff --git a/Multiplayer/Networking/Data/Train/CouplerInteractionType.cs b/Multiplayer/Networking/Data/Train/CouplerInteractionType.cs new file mode 100644 index 0000000..fe1385a --- /dev/null +++ b/Multiplayer/Networking/Data/Train/CouplerInteractionType.cs @@ -0,0 +1,24 @@ +using System; + +namespace Multiplayer.Networking.Data.Train; + +[Flags] +public enum CouplerInteractionType : ushort +{ + NoAction = 0, + + CouplerCouple = 1, + CouplerPark = 2, + CouplerDrop = 4, + CouplerTighten = 8, + CouplerLoosen = 16, + + HoseConnect = 32, + HoseDisconnect = 64, + + CockOpen = 128, + CockClose = 256, + + CoupleViaUI = 512, + UncoupleViaUI = 1024, +} diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 44b55ac..f3bf0cb 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -124,6 +124,7 @@ protected override void Subscribe() netPacketProcessor.SubscribeReusable(OnClientboundSpawnTrainSetPacket); netPacketProcessor.SubscribeReusable(OnClientboundDestroyTrainCarPacket); netPacketProcessor.SubscribeReusable(OnClientboundTrainPhysicsPacket); + netPacketProcessor.SubscribeReusable(OnCommonCouplerInteractionPacket); netPacketProcessor.SubscribeReusable(OnCommonTrainCouplePacket); netPacketProcessor.SubscribeReusable(OnCommonTrainUncouplePacket); netPacketProcessor.SubscribeReusable(OnCommonHoseConnectedPacket); @@ -537,31 +538,62 @@ public void OnClientboundTrainPhysicsPacket(ClientboundTrainsetPhysicsPacket pac NetworkTrainsetWatcher.Instance.Client_HandleTrainsetPhysicsUpdate(packet); } - private void OnCommonTrainCouplePacket(CommonTrainCouplePacket packet) + private void OnCommonCouplerInteractionPacket(CommonCouplerInteractionPacket packet) { - if (!NetworkedTrainCar.GetTrainCar(packet.NetId, out TrainCar trainCar) || !NetworkedTrainCar.GetTrainCar(packet.OtherNetId, out TrainCar otherTrainCar)) + if (!NetworkedTrainCar.Get(packet.NetId, out var netTrainCar)) + { + LogError($"OnCommonCouplerInteractionPacket netId: {packet.NetId}, TrainCar not found!"); return; + } + + netTrainCar.Common_ReceiveCouplerInteraction(packet); + } + private void OnCommonTrainCouplePacket(CommonTrainCouplePacket packet) + { + // TrainCar trainCar = null; + // TrainCar otherTrainCar = null; + + // if (!NetworkedTrainCar.GetTrainCar(packet.NetId, out trainCar) || !NetworkedTrainCar.GetTrainCar(packet.OtherNetId, out otherTrainCar)) + // { + // LogDebug(() => $"OnCommonTrainCouplePacket() netId: {packet.NetId}, trainCar found?: {trainCar != null}, otherNetId: {packet.OtherNetId}, otherTrainCar found?: {otherTrainCar != null}"); + // return; + // } + + // LogDebug(() => $"OnCommonTrainCouplePacket() netId: {packet.NetId}, trainCar: {trainCar.ID}, otherNetId: {packet.OtherNetId}, otherTrainCar: {otherTrainCar.ID}"); - Coupler coupler = packet.IsFrontCoupler ? trainCar.frontCoupler : trainCar.rearCoupler; - Coupler otherCoupler = packet.OtherCarIsFrontCoupler ? otherTrainCar.frontCoupler : otherTrainCar.rearCoupler; + // Coupler coupler = packet.IsFrontCoupler ? trainCar.frontCoupler : trainCar.rearCoupler; + // Coupler otherCoupler = packet.OtherCarIsFrontCoupler ? otherTrainCar.frontCoupler : otherTrainCar.rearCoupler; - coupler.CoupleTo(otherCoupler, packet.PlayAudio, false/*B99 packet.ViaChainInteraction*/); + // if (coupler.CoupleTo(otherCoupler, packet.PlayAudio, false/*B99 packet.ViaChainInteraction*/) == null) + // LogDebug(() => $"OnCommonTrainCouplePacket() netId: {packet.NetId}, trainCar: {trainCar.ID}, otherNetId: {packet.OtherNetId}, otherTrainCar: {otherTrainCar.ID} Failed to couple!"); } private void OnCommonTrainUncouplePacket(CommonTrainUncouplePacket packet) { - if (!NetworkedTrainCar.GetTrainCar(packet.NetId, out TrainCar trainCar)) - return; + //if (!NetworkedTrainCar.GetTrainCar(packet.NetId, out TrainCar trainCar)) + //{ + // LogDebug(() => $"OnCommonTrainUncouplePacket() netId: {packet.NetId}, trainCar found?: {trainCar != null}"); + // return; + //} - Coupler coupler = packet.IsFrontCoupler ? trainCar.frontCoupler : trainCar.rearCoupler; + //LogDebug(() => $"OnCommonTrainUncouplePacket() netId: {packet.NetId}, trainCar: {trainCar.ID}, isFront: {packet.IsFrontCoupler}, playAudio: {packet.PlayAudio}, DueToBrokenCouple: {packet.DueToBrokenCouple}, viaChainInteraction: {packet.ViaChainInteraction}"); - coupler.Uncouple(packet.PlayAudio, false, packet.DueToBrokenCouple, false/*B99 packet.ViaChainInteraction*/); + //Coupler coupler = packet.IsFrontCoupler ? trainCar.frontCoupler : trainCar.rearCoupler; + //coupler.Uncouple(packet.PlayAudio, false, packet.DueToBrokenCouple, false/*B99 packet.ViaChainInteraction*/); } private void OnCommonHoseConnectedPacket(CommonHoseConnectedPacket packet) { - if (!NetworkedTrainCar.GetTrainCar(packet.NetId, out TrainCar trainCar) || !NetworkedTrainCar.GetTrainCar(packet.OtherNetId, out TrainCar otherTrainCar)) + TrainCar trainCar = null; + TrainCar otherTrainCar = null; + + if (!NetworkedTrainCar.GetTrainCar(packet.NetId, out trainCar) || !NetworkedTrainCar.GetTrainCar(packet.OtherNetId, out otherTrainCar)) + { + LogDebug(() => $"OnCommonHoseConnectedPacket() netId: {packet.NetId}, trainCar found?: {trainCar != null}, otherNetId: {packet.OtherNetId}, otherTrainCar found?: {otherTrainCar != null}"); return; + } + + LogDebug(() => $"OnCommonHoseConnectedPacket() netId: {packet.NetId}, trainCar: {trainCar.ID}, isFront: {packet.IsFront}, playAudio: {packet.PlayAudio}"); Coupler coupler = packet.IsFront ? trainCar.frontCoupler : trainCar.rearCoupler; Coupler otherCoupler = packet.OtherIsFront ? otherTrainCar.frontCoupler : otherTrainCar.rearCoupler; @@ -572,7 +604,12 @@ private void OnCommonHoseConnectedPacket(CommonHoseConnectedPacket packet) private void OnCommonHoseDisconnectedPacket(CommonHoseDisconnectedPacket packet) { if (!NetworkedTrainCar.GetTrainCar(packet.NetId, out TrainCar trainCar)) + { + LogDebug(() => $"OnCommonHoseDisconnectedPacket() netId: {packet.NetId}, trainCar found?: {trainCar != null}"); return; + } + + LogDebug(() => $"OnCommonHoseDisconnectedPacket() netId: {packet.NetId}, trainCar: {trainCar.ID}, isFront: {packet.IsFront}, playAudio: {packet.PlayAudio}"); Coupler coupler = packet.IsFront ? trainCar.frontCoupler : trainCar.rearCoupler; @@ -648,7 +685,7 @@ private void OnClientboundBrakePressureUpdatePacket(ClientboundBrakePressureUpda return; - networkedTrainCar.Client_ReceiveBrakePressureUpdate(packet.MainReservoirPressure, packet.IndependentPipePressure, packet.BrakePipePressure, packet.BrakeCylinderPressure); + networkedTrainCar.Client_ReceiveBrakePressureUpdate(packet.MainReservoirPressure, packet.BrakePipePressure, packet.BrakeCylinderPressure); //LogDebug(() => $"Received Brake Pressures netId {packet.NetId}: {packet.MainReservoirPressure}, {packet.IndependentPipePressure}, {packet.BrakePipePressure}, {packet.BrakeCylinderPressure}"); } @@ -660,8 +697,6 @@ private void OnClientboundFireboxStatePacket(ClientboundFireboxStatePacket packe networkedTrainCar.Client_ReceiveFireboxStateUpdate(packet.Contents, packet.IsOn); - - //LogDebug(() => $"Received Brake Pressures netId {packet.NetId}: {packet.Contents}, {packet.IsOn}"); } private void OnClientboundCargoStatePacket(ClientboundCargoStatePacket packet) @@ -960,6 +995,28 @@ public void SendTurntableRotation(byte netId, float rotation) }, DeliveryMethod.ReliableOrdered); } + public void SendCouplerInteraction(CouplerInteractionType flags, Coupler coupler, Coupler otherCoupler = null) + { + ushort couplerNetId = coupler?.train?.GetNetId() ?? 0; + ushort otherCouplerNetId = otherCoupler?.train?.GetNetId() ?? 0; + + if (couplerNetId == 0) + { + LogWarning($"SendCouplerInteraction failed. Coupler: {coupler.name} {couplerNetId}"); + return; + } + + Log($"Sending coupler interaction {flags} for {coupler?.train?.ID}"); + SendPacketToServer(new CommonCouplerInteractionPacket + { + NetId = couplerNetId, + OtherNetId = otherCouplerNetId, + IsFrontCoupler = coupler.isFrontCoupler, + Flags = (ushort)flags, + }, DeliveryMethod.ReliableUnordered); + } + + public void SendTrainCouple(Coupler coupler, Coupler otherCoupler, bool playAudio, bool viaChainInteraction) { ushort couplerNetId = coupler.train.GetNetId(); diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index 89b7f33..fa5ff4a 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -120,6 +120,7 @@ protected override void Subscribe() netPacketProcessor.SubscribeReusable(OnServerboundLicensePurchaseRequestPacket); netPacketProcessor.SubscribeReusable(OnCommonChangeJunctionPacket); netPacketProcessor.SubscribeReusable(OnCommonRotateTurntablePacket); + netPacketProcessor.SubscribeReusable(OnCommonCouplerInteractionPacket); netPacketProcessor.SubscribeReusable(OnCommonTrainCouplePacket); netPacketProcessor.SubscribeReusable(OnCommonTrainUncouplePacket); netPacketProcessor.SubscribeReusable(OnCommonHoseConnectedPacket); @@ -757,6 +758,11 @@ private void OnCommonRotateTurntablePacket(CommonRotateTurntablePacket packet, N SendPacketToAll(packet, DeliveryMethod.ReliableOrdered, peer); } + private void OnCommonCouplerInteractionPacket(CommonCouplerInteractionPacket packet, NetPeer peer) + { + //todo: add validation that to ensure the client is near the coupler - this packet may also be used for remote operations and may need to factor that in in the future + SendPacketToAll(packet, DeliveryMethod.ReliableUnordered, peer); + } private void OnCommonTrainCouplePacket(CommonTrainCouplePacket packet, NetPeer peer) { SendPacketToAll(packet, DeliveryMethod.ReliableUnordered, peer); @@ -934,7 +940,7 @@ private void OnServerboundTrainRerailRequestPacket(ServerboundTrainRerailRequest trainCar.Rerail(networkedRailTrack.RailTrack, position, packet.Forward); } - + private void OnServerboundLicensePurchaseRequestPacket(ServerboundLicensePurchaseRequestPacket packet, NetPeer peer) { if (!TryGetServerPlayer(peer, out ServerPlayer player)) diff --git a/Multiplayer/Networking/Packets/Common/Train/CommonCouplerInteractionPacket.cs b/Multiplayer/Networking/Packets/Common/Train/CommonCouplerInteractionPacket.cs new file mode 100644 index 0000000..8766f28 --- /dev/null +++ b/Multiplayer/Networking/Packets/Common/Train/CommonCouplerInteractionPacket.cs @@ -0,0 +1,13 @@ +using System; + +namespace Multiplayer.Networking.Packets.Common.Train; + + +public class CommonCouplerInteractionPacket +{ + public ushort NetId { get; set; } + public ushort OtherNetId { get; set; } + public bool IsFrontCoupler { get; set; } + public bool IsFrontOtherCoupler { get; set; } + public ushort Flags { get; set; } +} diff --git a/Multiplayer/Patches/Train/CouplerChainInteractionPatch.cs b/Multiplayer/Patches/Train/CouplerChainInteractionPatch.cs new file mode 100644 index 0000000..5a4d50e --- /dev/null +++ b/Multiplayer/Patches/Train/CouplerChainInteractionPatch.cs @@ -0,0 +1,36 @@ +using DV.CabControls; +using HarmonyLib; +using Multiplayer.Components.Networking; +using Multiplayer.Networking.Data.Train; +using UnityEngine; + +namespace Multiplayer.Patches.World; + + +[HarmonyPatch(typeof(ChainCouplerInteraction))] +public static class ChainCouplerInteractionPatch +{ + [HarmonyPatch(nameof(ChainCouplerInteraction.OnScrewButtonUsed))] + [HarmonyPostfix] + private static void OnScrewButtonUsed(ChainCouplerInteraction __instance) + { + + Multiplayer.LogDebug(() => $"OnScrewButtonUsed({__instance?.couplerAdapter?.coupler?.train?.ID}) state: {__instance.state}"); + + CouplerInteractionType flag = default; + if (__instance.state == ChainCouplerInteraction.State.Attached_Tightening_Couple || __instance.state == ChainCouplerInteraction.State.Attached_Tight) + flag = CouplerInteractionType.CouplerTighten; + else if (__instance.state == ChainCouplerInteraction.State.Attached_Loosening_Uncouple || __instance.state == ChainCouplerInteraction.State.Attached_Loose) + flag = CouplerInteractionType.CouplerLoosen; + else + Multiplayer.LogDebug(() => + { + TrainCar car = __instance?.couplerAdapter?.coupler?.train; + return $"OnScrewButtonUsed({car?.ID})\r\n{new System.Diagnostics.StackTrace()}"; + }); + + if (flag != CouplerInteractionType.NoAction) + NetworkLifecycle.Instance.Client.SendCouplerInteraction(flag, __instance?.couplerAdapter?.coupler); + } + +} diff --git a/Multiplayer/Patches/Train/CouplerInterfacerPatch.cs b/Multiplayer/Patches/Train/CouplerInterfacerPatch.cs new file mode 100644 index 0000000..eca761b --- /dev/null +++ b/Multiplayer/Patches/Train/CouplerInterfacerPatch.cs @@ -0,0 +1,88 @@ +using DV.HUD; +using HarmonyLib; +using Multiplayer.Components.Networking; +using Multiplayer.Networking.Data.Train; +using Newtonsoft.Json.Linq; +using System; + + +namespace Multiplayer.Patches.Train; + +[HarmonyPatch(typeof(CouplerInterfacer))] +public static class CouplerInterfacerPatch +{ + static Action frontCouplerDelegate; + static Action rearCouplerDelegate; + + + [HarmonyPatch(nameof(CouplerInterfacer.SetupListeners))] + [HarmonyPrefix] + private static void SetupListeners(CouplerInterfacer __instance, bool on) + { + Multiplayer.LogDebug(() => $"CouplerInterfacer.SetupListeners({__instance?.train?.ID}, {on})"); + if (on) + { + if(frontCouplerDelegate != null) + { + Multiplayer.LogDebug(() => $"CouplerInterfacer.SetupListeners({__instance?.train?.ID}, {on}) not null!"); + return; + } + + frontCouplerDelegate += (float value)=>SendCouple(__instance, value, true); + rearCouplerDelegate += (float value)=>SendCouple(__instance, value, false); + + __instance.manager.CouplerMenu.coupleF.controlModule.ValueChanged += frontCouplerDelegate; + __instance.manager.CouplerMenu.chainF.controlModule.ValueChanged += frontCouplerDelegate; + + __instance.manager.CouplerMenu.coupleR.controlModule.ValueChanged += rearCouplerDelegate; + __instance.manager.CouplerMenu.chainR.controlModule.ValueChanged += rearCouplerDelegate; + } + else + { + if (frontCouplerDelegate != null) + { + __instance.manager.CouplerMenu.coupleF.controlModule.ValueChanged -= frontCouplerDelegate; + __instance.manager.CouplerMenu.chainF.controlModule.ValueChanged -= frontCouplerDelegate; + + frontCouplerDelegate = null; + } + + if (rearCouplerDelegate != null) + { + __instance.manager.CouplerMenu.coupleR.controlModule.ValueChanged -= rearCouplerDelegate; + __instance.manager.CouplerMenu.chainR.controlModule.ValueChanged -= rearCouplerDelegate; + + rearCouplerDelegate = null; + } + } + } + + private static void SendCouple(CouplerInterfacer couplerInterfacer, float value, bool front) + { + Multiplayer.LogDebug(() => $"CouplerInterfacer.SendCouple({couplerInterfacer?.train?.ID}, {value}, {front})"); + + if (value <= 0.5f) + return; + + Coupler coupler = couplerInterfacer.GetCoupler(front); + Coupler otherCoupler = null; + CouplerInteractionType interaction = CouplerInteractionType.UncoupleViaUI; + + Multiplayer.LogDebug(() => $"CouplerInterfacer.SendCouple({couplerInterfacer?.train?.ID}, {value}, {front}) coupler: {coupler?.train?.ID}, action: {interaction}"); + + if (coupler == null) + return; + + if (!coupler.IsCoupled()) + { + interaction = CouplerInteractionType.CoupleViaUI; + otherCoupler = coupler.GetFirstCouplerInRange(); + + Multiplayer.LogDebug(() => $"CouplerInterfacer.SendCouple({couplerInterfacer?.train?.ID}, {value}, {front}) coupler: {coupler?.train?.ID}, otherCoupler: {otherCoupler?.train?.ID}, action: {interaction}"); + if (otherCoupler == null) + return; + } + + NetworkLifecycle.Instance.Client.SendCouplerInteraction(interaction, coupler, otherCoupler); + } +} diff --git a/Multiplayer/Patches/Train/CouplerPatch.cs b/Multiplayer/Patches/Train/CouplerPatch.cs index c036ff0..b70a8d2 100644 --- a/Multiplayer/Patches/Train/CouplerPatch.cs +++ b/Multiplayer/Patches/Train/CouplerPatch.cs @@ -1,59 +1,31 @@ using HarmonyLib; using Multiplayer.Components.Networking; -using Multiplayer.Components.Networking.Train; -using Multiplayer.Utils; -namespace Multiplayer.Patches.World; +namespace Multiplayer.Patches.Train; -[HarmonyPatch(typeof(Coupler), nameof(Coupler.CoupleTo))] -public static class Coupler_CoupleTo_Patch -{ - private static void Postfix(Coupler __instance, Coupler other, bool playAudio, bool viaChainInteraction) - { - if (UnloadWatcher.isUnloading || NetworkLifecycle.Instance.IsProcessingPacket) - return; - - if(__instance == null || other == null) - { - Multiplayer.LogError($"Coupler_CoupleTo_Patch({__instance?.train?.ID}, {other?.train?.ID}, {playAudio}, {viaChainInteraction})\r\n{new System.Diagnostics.StackTrace()}"); - return; - } - - NetworkLifecycle.Instance.Client?.SendTrainCouple(__instance, other, playAudio, viaChainInteraction); - } -} -[HarmonyPatch(typeof(Coupler), nameof(Coupler.Uncouple))] -public static class Coupler_Uncouple_Patch +[HarmonyPatch(typeof(Coupler))] +public static class CouplerPatch { - private static void Postfix(Coupler __instance, bool playAudio, bool calledOnOtherCoupler, bool dueToBrokenCouple, bool viaChainInteraction) + [HarmonyPatch(nameof(Coupler.ConnectAirHose))] + [HarmonyPostfix] + private static void ConnectAirHose(Coupler __instance, Coupler other, bool playAudio) { - if (UnloadWatcher.isUnloading || NetworkLifecycle.Instance.IsProcessingPacket || calledOnOtherCoupler) - return; - if (!__instance.train.TryNetworked(out NetworkedTrainCar networkedTrainCar) || networkedTrainCar.IsDestroying) - return; - NetworkLifecycle.Instance.Client?.SendTrainUncouple(__instance, playAudio, dueToBrokenCouple, viaChainInteraction); - } -} + //Multiplayer.LogDebug(() => $"ConnectAirHose([{__instance?.train?.ID}, isFront: {__instance?.isFrontCoupler}])\r\n{new System.Diagnostics.StackTrace()}"); -[HarmonyPatch(typeof(Coupler), nameof(Coupler.ConnectAirHose))] -public static class Coupler_ConnectAirHose_Patch -{ - private static void Postfix(Coupler __instance, Coupler other, bool playAudio) - { if (UnloadWatcher.isUnloading || NetworkLifecycle.Instance.IsProcessingPacket) return; NetworkLifecycle.Instance.Client?.SendHoseConnected(__instance, other, playAudio); } -} -[HarmonyPatch(typeof(Coupler), nameof(Coupler.DisconnectAirHose))] -public static class Coupler_DisconnectAirHose_Patch -{ - private static void Postfix(Coupler __instance, bool playAudio) + [HarmonyPatch(nameof(Coupler.DisconnectAirHose))] + [HarmonyPostfix] + private static void DisconnectAirHose(Coupler __instance, bool playAudio) { + //Multiplayer.LogDebug(() => $"DisconnectAirHose([{__instance?.train?.ID}, isFront: {__instance?.isFrontCoupler}])\r\n{new System.Diagnostics.StackTrace()}"); if (UnloadWatcher.isUnloading || NetworkLifecycle.Instance.IsProcessingPacket) return; - NetworkLifecycle.Instance.Client?.SendHoseDisconnected(__instance, playAudio); + NetworkLifecycle.Instance.Client?.SendHoseDisconnected(__instance, playAudio); } + } diff --git a/Multiplayer/Patches/Train/HoseAndCockPatch.cs b/Multiplayer/Patches/Train/HoseAndCockPatch.cs index ae4e70f..6072e96 100644 --- a/Multiplayer/Patches/Train/HoseAndCockPatch.cs +++ b/Multiplayer/Patches/Train/HoseAndCockPatch.cs @@ -14,13 +14,7 @@ private static void Prefix(HoseAndCock __instance, bool open) if (UnloadWatcher.isUnloading || NetworkLifecycle.Instance.IsProcessingPacket) return; - if(!NetworkedTrainCar.TryGetCoupler(__instance, out Coupler coupler)) - { - //TrainCar me = TrainCar.Resolve(__instance?.parentSystem?.gameObject); - //Multiplayer.LogError($"HoseAndCock.SetCock() Coupler not found! - Cars may be getting destroyed on load? TrainCar ID: {me?.ID}"); - } - - if (coupler == null || !coupler.train.TryNetworked(out NetworkedTrainCar networkedTrainCar)) + if (!NetworkedTrainCar.TryGetCoupler(__instance, out Coupler coupler) || !coupler.train.TryNetworked(out NetworkedTrainCar networkedTrainCar)) return; if (networkedTrainCar.IsDestroying) diff --git a/info.json b/info.json index 1da5d1e..7854455 100644 --- a/info.json +++ b/info.json @@ -1,6 +1,6 @@ { "Id": "Multiplayer", - "Version": "0.1.9.2", + "Version": "0.1.9.5", "DisplayName": "Multiplayer", "Author": "Insprill, Macka, Morm", "EntryMethod": "Multiplayer.Multiplayer.Load", From 7ca6943f6c1b9537757ac7f9f40e129ba58852b1 Mon Sep 17 00:00:00 2001 From: AMacro Date: Fri, 27 Dec 2024 23:32:32 +1000 Subject: [PATCH 139/188] Minor clean-up of LAN discovery packets Removed the enum and replaced with bool - cleaner packet registration by removing an unnecessary enum --- .../Networking/Managers/Client/ServerBrowserClient.cs | 8 +++----- .../Networking/Managers/Server/LobbyServerManager.cs | 10 +++------- .../Packets/Unconnected/UnconnectedDiscoveryPacket.cs | 9 ++------- 3 files changed, 8 insertions(+), 19 deletions(-) diff --git a/Multiplayer/Networking/Managers/Client/ServerBrowserClient.cs b/Multiplayer/Networking/Managers/Client/ServerBrowserClient.cs index 5d89743..35c1d12 100644 --- a/Multiplayer/Networking/Managers/Client/ServerBrowserClient.cs +++ b/Multiplayer/Networking/Managers/Client/ServerBrowserClient.cs @@ -164,12 +164,10 @@ private void OnUnconnectedDiscoveryPacket(UnconnectedDiscoveryPacket packet, IPE { //Log($"OnUnconnectedDiscoveryPacket({packet.PacketType}, {endPoint?.Address})"); - switch (packet.PacketType) + if (packet.IsResponse) { - case DiscoveryPacketType.Response: - //Log($"OnUnconnectedDiscoveryPacket({packet.PacketType}, {endPoint?.Address}) id: {packet.data.id}"); - OnDiscovery?.Invoke(endPoint,packet.data); - break; + //Log($"OnUnconnectedDiscoveryPacket({packet.PacketType}, {endPoint?.Address}) id: {packet.data.id}"); + OnDiscovery?.Invoke(endPoint,packet.Data); } } diff --git a/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs b/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs index 634d056..3c74e66 100644 --- a/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs +++ b/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs @@ -434,14 +434,10 @@ private void OnUnconnectedDiscoveryPacket(UnconnectedDiscoveryPacket packet, IPE { //server.LogDebug(()=>$"OnUnconnectedDiscoveryPacket({packet.PacketType}, {endPoint.Address},{endPoint.Port})"); - switch (packet.PacketType) + if (!packet.IsResponse) { - case DiscoveryPacketType.Discovery: - packet.PacketType = DiscoveryPacketType.Response; - packet.data = server.serverData; - break; - default: - return; + packet.IsResponse = true; + packet.Data = server.serverData; } SendUnconnectedPacket(packet, endPoint.Address.ToString(), endPoint.Port); diff --git a/Multiplayer/Networking/Packets/Unconnected/UnconnectedDiscoveryPacket.cs b/Multiplayer/Networking/Packets/Unconnected/UnconnectedDiscoveryPacket.cs index 94f8238..2c01c51 100644 --- a/Multiplayer/Networking/Packets/Unconnected/UnconnectedDiscoveryPacket.cs +++ b/Multiplayer/Networking/Packets/Unconnected/UnconnectedDiscoveryPacket.cs @@ -3,13 +3,8 @@ namespace Multiplayer.Networking.Packets.Unconnected; -public enum DiscoveryPacketType : byte -{ - Discovery = 1, - Response = 2, -} public class UnconnectedDiscoveryPacket { - public DiscoveryPacketType PacketType { get; set; } = DiscoveryPacketType.Discovery; - public LobbyServerData data { get; set; } + public bool IsResponse { get; set; } = false; + public LobbyServerData Data { get; set; } } From 3274b8b4a603e77d0ba5b14bf565158572e66b7c Mon Sep 17 00:00:00 2001 From: AMacro Date: Thu, 2 Jan 2025 09:28:22 +1030 Subject: [PATCH 140/188] Remove reliance on RegEx checks Use inbuilt IPAddress class TryParse() instead of Regex --- .../Components/MainMenu/ServerBrowserPane.cs | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs index 56ac125..11f1ab5 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs @@ -66,14 +66,6 @@ private enum ConnectionState Aborted } - // 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 const int MAX_PORT_LEN = 5; private const int MIN_PORT = 1024; private const int MAX_PORT = 49151; @@ -569,7 +561,7 @@ private void ShowIpPopup() return; } - if (!IPv4Regex.IsMatch(result.data) && !IPv6Regex.IsMatch(result.data)) + if (!IPAddress.TryParse(result.data, out IPAddress parsedAddress)) { string inputUrl = result.data; @@ -610,8 +602,7 @@ private void ShowIpPopup() } else { - if (IPv4Regex.IsMatch(result.data)) - { + if (parsedAddress.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork) connectionState = ConnectionState.AttemptingIPv4; } else @@ -648,7 +639,7 @@ private void ShowPortPopup() return; } - if (!PortRegex.IsMatch(result.data)) + if (!int.TryParse(result.data, out portNumber) || portNumber < MIN_PORT || portNumber > MAX_PORT) { MainMenuThingsAndStuff.Instance.ShowOkPopup(Locale.SERVER_BROWSER__PORT_INVALID, ShowIpPopup); } @@ -656,7 +647,8 @@ private void ShowPortPopup() { portNumber = ushort.Parse(result.data); ShowPasswordPopup(); - } + } + }; } From cea09707baa40a343767bb17f38e81663745675c Mon Sep 17 00:00:00 2001 From: AMacro Date: Thu, 2 Jan 2025 11:45:57 +1030 Subject: [PATCH 141/188] Add support for Steam player name --- .../Components/Networking/UI/PlayerListGUI.cs | 2 +- .../Managers/Client/NetworkClient.cs | 5 +-- .../Items/RemoteControllerModulePatch.cs | 34 +++++++++++++++++++ Multiplayer/Settings.cs | 25 +++++++++++++- 4 files changed, 62 insertions(+), 4 deletions(-) create mode 100644 Multiplayer/Patches/World/Items/RemoteControllerModulePatch.cs diff --git a/Multiplayer/Components/Networking/UI/PlayerListGUI.cs b/Multiplayer/Components/Networking/UI/PlayerListGUI.cs index 59ae043..395e8ae 100644 --- a/Multiplayer/Components/Networking/UI/PlayerListGUI.cs +++ b/Multiplayer/Components/Networking/UI/PlayerListGUI.cs @@ -48,7 +48,7 @@ private static IEnumerable GetPlayerList() } // The Player of the Client is not in the PlayerManager, so we need to add it separately - playerList[playerList.Length - 1] = $"{Multiplayer.Settings.Username} ({NetworkLifecycle.Instance.Client.Ping.ToString()}ms)"; + playerList[playerList.Length - 1] = $"{Multiplayer.Settings.GetUserName()} ({NetworkLifecycle.Instance.Client.Ping}ms)"; return playerList; } } diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index f3bf0cb..3b07476 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -68,13 +68,14 @@ public NetworkClient(Settings settings) : base(settings) ClientPlayerManager = new ClientPlayerManager(); } - public void Start(string address, int port, string password, bool isSinglePlayer, Action onDisconnect) + public void Start(string address, int port, string password, bool isSinglePlayer, Action onDisconnect) { this.onDisconnect = onDisconnect; netManager.Start(); + ServerboundClientLoginPacket serverboundClientLoginPacket = new() { - Username = Multiplayer.Settings.Username, + Username = Multiplayer.Settings.GetUserName(), Guid = Multiplayer.Settings.GetGuid().ToByteArray(), Password = password, BuildMajorVersion = (ushort)BuildInfo.BUILD_VERSION_MAJOR, diff --git a/Multiplayer/Patches/World/Items/RemoteControllerModulePatch.cs b/Multiplayer/Patches/World/Items/RemoteControllerModulePatch.cs new file mode 100644 index 0000000..705192c --- /dev/null +++ b/Multiplayer/Patches/World/Items/RemoteControllerModulePatch.cs @@ -0,0 +1,34 @@ +using DV.RemoteControls; +using HarmonyLib; +using Multiplayer.Components.Networking; +using Multiplayer.Networking.Data.Train; +using Multiplayer.Utils; +using System; +using UnityEngine; + + +namespace Multiplayer.Patches.World.Items; + +[HarmonyPatch(typeof(RemoteControllerModule))] +public static class RemoteControllerModulePatch +{ + [HarmonyPatch(nameof(RemoteControllerModule.RemoteControllerCouple))] + [HarmonyPostfix] + static void RemoteControllerCouple(RemoteControllerModule __instance) + { + NetworkLifecycle.Instance.Client.SendCouplerInteraction(CouplerInteractionType.CoupleViaRemote, __instance.car.frontCoupler); + } + + [HarmonyPatch(nameof(RemoteControllerModule.Uncouple))] + [HarmonyPostfix] + static void Uncouple(RemoteControllerModule __instance, int selectedCoupler) + { + TrainCar startCar = __instance.car; + Coupler nthCouplerFrom = CouplerLogic.GetNthCouplerFrom((selectedCoupler > 0) ? startCar.frontCoupler : startCar.rearCoupler, Mathf.Abs(selectedCoupler) - 1); + + if (nthCouplerFrom != null) + { + NetworkLifecycle.Instance.Client.SendCouplerInteraction(CouplerInteractionType.UncoupleViaRemote, nthCouplerFrom); + } + } +} diff --git a/Multiplayer/Settings.cs b/Multiplayer/Settings.cs index f88c7d5..8c35e65 100644 --- a/Multiplayer/Settings.cs +++ b/Multiplayer/Settings.cs @@ -1,5 +1,6 @@ using System; using Humanizer; +using Steamworks; using UnityEngine; using UnityModManagerNet; using Console = DV.Console; @@ -17,7 +18,10 @@ public class Settings : UnityModManager.ModSettings, IDrawable public int SettingsVer = 1; [Header("Player")] - [Draw("Username", Tooltip = "Your username in-game")] + [Draw("Use Steam Name", Tooltip = "Use your Steam name as your username in-game")] + public bool UseSteamName = true; + public string LastSteamName = string.Empty; + [Draw("Username", Tooltip = "Your username in-game", VisibleOn = "UseSteamName|false")] public string Username = "Player"; public string Guid = System.Guid.NewGuid().ToString(); @@ -98,6 +102,7 @@ public void Draw(UnityModManager.ModEntry modEntry) public override void Save(UnityModManager.ModEntry modEntry) { + LastSteamName = LastSteamName.Trim().Truncate(MAX_USERNAME_LENGTH); Username = Username.Trim().Truncate(MAX_USERNAME_LENGTH); Port = Mathf.Clamp(Port, 1024, 49151); MaxPlayers = Mathf.Clamp(MaxPlayers, 1, byte.MaxValue); @@ -121,6 +126,24 @@ public Guid GetGuid() return guid; } + public string GetUserName() + { + string username = Username; + + if (Multiplayer.Settings.UseSteamName) + { + if (DVSteamworks.Success) + { + Multiplayer.Settings.LastSteamName = SteamClient.Name; + } + + if (Multiplayer.Settings.LastSteamName != string.Empty) + username = Multiplayer.Settings.LastSteamName; + } + + return username; + } + public static Settings Load(UnityModManager.ModEntry modEntry) { Settings data = Settings.Load(modEntry); From 9c08cda64be5d4692e1d5a62d4c534318ada489f Mon Sep 17 00:00:00 2001 From: AMacro Date: Thu, 2 Jan 2025 11:46:20 +1030 Subject: [PATCH 142/188] Update default settings --- Multiplayer/Settings.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Multiplayer/Settings.cs b/Multiplayer/Settings.cs index 8c35e65..098cce6 100644 --- a/Multiplayer/Settings.cs +++ b/Multiplayer/Settings.cs @@ -15,7 +15,7 @@ public class Settings : UnityModManager.ModSettings, IDrawable public static Action OnSettingsUpdated; - public int SettingsVer = 1; + public int SettingsVer = 2; [Header("Player")] [Draw("Use Steam Name", Tooltip = "Use your Steam name as your username in-game")] @@ -159,7 +159,7 @@ public static Settings Load(UnityModManager.ModEntry modEntry) private static int GetCurrentVersion() { - return 1; + return 2; } // Function to handle migrations based on the current version @@ -178,11 +178,14 @@ private static void MigrateSettings(ref Settings data) MigrateSettings(ref data); break; - case 1: + case 1: if (data.Ipv4AddressCheck == "http://checkip.dyndns.org") data.Ipv4AddressCheck = new Settings().Ipv4AddressCheck; - break; + data.ShowAdvancedSettings = true; + data.DebugLogging = true; + data.ShowPingInNameTags = true; + break; default: break; } From ab238ccdeb9e84b6da9d11f51bfcf4d5007a9ada Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maxime=20Chourr=C3=A9?= Date: Thu, 2 Jan 2025 21:45:15 +0100 Subject: [PATCH 143/188] Edited french locale --- locale.csv | 102 ++++++++++++++++++++++++++--------------------------- 1 file changed, 51 insertions(+), 51 deletions(-) diff --git a/locale.csv b/locale.csv index cd4a267..1032073 100644 --- a/locale.csv +++ b/locale.csv @@ -1,33 +1,33 @@ 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,,,,,,,,,,,,,,,,,,,,,,,,,,, +,,,,,,,,,,,,,,,,,,,,,,,,,,, +,"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,,,,,,,,,,,,,,,,,,,,,,,,,,, +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,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,Se connecter à une 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,,,,,,,,,,,,Felhasználatlan,,,,,,,,,,,,,, -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,Host Game,Host Game,Домакин на играта,主机游戏,主機遊戲,Hostitelská hra,Værtsspil,Gastheerspel,Isäntäpeli,Héberger une partie,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.,Héberger 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,,,,,,,,,,,,Felhasználatlan,,,,,,,,,,,,,, -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,Join Game,Join Game,Присъединете се към играта,加入游戏,加入遊戲,Připojte se ke hře,Deltag i spil,Speel mee,Liity peliin,Rejoindre 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.,Rejoindre 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/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.,Actualise 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,Entrez 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,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),Entrez 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/password,Password popup.,Enter Password,Въведете паролата,输入密码,輸入密碼,Zadejte heslo,Indtast adgangskode,Voer wachtwoord in,Kirjoita salasana,Entrez 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,खिलाड़ी,Játékosok,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,पासवर्ड,Jelszó,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,Потрібні модифікації @@ -38,45 +38,45 @@ sb/no,Response 'no' for details text,No,Не,否,否,Ne,Nej,Nee,Ei,Non,Nein,न 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!,Сервери не знайдено. Оновіть або почніть власний! -sb/info/title,Title for server browser info,Server Browser Info,,服务器浏览器介绍,,,,,,,,,Szerverböngésző információ,,,,,,,,,,,,,, -sb/info/content,Content for server browser info,"Welcome to Derail Valley Multiplayer Mod!\n\nThe server list refreshes automatically every {0} seconds, but you can refresh manually once every {1} seconds.",,"欢迎来到脱轨山谷的联机模式!\n\n服务器列表会在每{0}秒刷新,但是你可以手动让它在每{1}秒刷新",,,,,,,,,"Üdvözli a Derail Valley Multiplayer Mod!\n\nA szerverlista automatikusan frissül {0} másodpercenként, de manuálisan is frissíthetsz minden {1} másodpercet.",,,,,,,,,,,,,, -sb/connecting,Connecting dialogue,"Connecting, please wait...\nAttempt: {0}",,"正在连接中,请稍候片刻\n尝试次数: {0}",,,,,,,,,"Csatlakozás, kérjük, várjon...\nKísérlet: {0}",,,,,,,,,,,,,, +sb/info/title,Title for server browser info,Server Browser Info,,服务器浏览器介绍,,,,,,Informations du navigateur de serveurs,,,Szerverböngésző információ,,,,,,,,,,,,,, +sb/info/content,Content for server browser info,"Welcome to Derail Valley Multiplayer Mod!\n\nThe server list refreshes automatically every {0} seconds, but you can refresh manually once every {1} seconds.",,欢迎来到脱轨山谷的联机模式!\n\n服务器列表会在每{0}秒刷新,但是你可以手动让它在每{1}秒刷新,,,,,,"Bienvenue dans le mod multijoueur de Derail Valley !\n\nLa liste des serveurs est mise à jour automatiquement toutes les {0} secondes, mais vous pouvez la rafraîchir manuellement toutes les {1} secondes.",,,"Üdvözli a Derail Valley Multiplayer Mod!\n\nA szerverlista automatikusan frissül {0} másodpercenként, de manuálisan is frissíthetsz minden {1} másodpercet.",,,,,,,,,,,,,, +sb/connecting,Connecting dialogue,"Connecting, please wait...\nAttempt: {0}",,正在连接中,请稍候片刻\n尝试次数: {0},,,,,,"Connexion, merci de patienter...\nEssai : {0}",,,"Csatlakozás, kérjük, várjon...\nKísérlet: {0}",,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,, ,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/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. Laissez 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,,Nyilvános Játék,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/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.,Lister 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/public__tooltip_disabled,Unused,,,,,,,,,,,,,,,,,,,,,,,,,, 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/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 de serveurs.,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ä,Joueurs maximum,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 la partie.,"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/max_players__tooltip_disabled,Unused,,,,,,,,,,,,,,,,,,,,,,,,,, host/start,Maximum players slider label,Start,Започнете,开始,開始,Start,Start,Begin,alkaa,Commencer,Start,शुरू,Indít!,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.,सर्वर प्रारंभ करें.,Szerver Indul!,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,Maximum players slider tooltip,Start the server.,Стартирайте сървъра.,启动服务器,啟動伺服器。,Spusťte server.,Start serveren.,Start de server.,Käynnistä palvelin.,Démarre le serveur.,Starten Sie den Server.,सर्वर प्रारंभ करें.,Szerver Indul!,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/instructions/first,Instructions for the host 1,"First time hosts, please see the {0}Hosting{1} section of our Wiki.",,"第一次主持游戏的话, 请看我们wiki的{0}Hosting{1} 模块",,,,,,,,,"Az első házigazdák, kérjük, tekintse meg Wikink {0}Hosting{1} részét.",,,,,,,,,,,,,, -host/instructions/mod_warning,Instructions for the host 2,Using other mods may cause unexpected behaviour including de-syncs. See {0}Mod Compatibility{1} for more info.,,"同时使用其他模组可能会导致游戏出错,比如物品不同步, 看 {0}Mod Compatibility{1} 模块来获取更多信息",,,,,,,,,"Más modok használata váratlan viselkedést okozhat, beleértve a szinkronizálást. További információért lásd a {0}Modkompatibilitást{1}.",,,,,,,,,,,,,, -host/instructions/recommend,Instructions for the host 3,It is recommended that other mods are disabled and Derail Valley restarted prior to playing in multiplayer.,,"推荐你卸载其他模组并重启游戏后,再进行联机",,,,,,,,,"Javasoljuk, hogy tiltsa le a többi modot, és indítsa újra a Derail Valleyt, mielőtt többjátékos módban játszana.",,,,,,,,,,,,,, -host/instructions/signoff,Instructions for the host 4,We hope to have your favourite mods compatible with multiplayer in the future.,,我们希望未来能让你装联机模组的同时也能玩其他模组,,,,,,,,,"Reméljük, hogy kedvenc modjai a jövőben kompatibilisek lesznek a többjátékos játékkal.",,,,,,,,,,,,,, +host/instructions/first,Instructions for the host 1,"First time hosts, please see the {0}Hosting{1} section of our Wiki.",,"第一次主持游戏的话, 请看我们wiki的{0}Hosting{1} 模块",,,,,,"La première fois que vous hébergez, merci de consulter la section {0}Hébergement{1} sur notre Wiki.",,,"Az első házigazdák, kérjük, tekintse meg Wikink {0}Hosting{1} részét.",,,,,,,,,,,,,, +host/instructions/mod_warning,Instructions for the host 2,Using other mods may cause unexpected behaviour including de-syncs. See {0}Mod Compatibility{1} for more info.,,同时使用其他模组可能会导致游戏出错,比如物品不同步, 看 {0}Mod Compatibility{1} 模块来获取更多信息,,,,,,"L’utilisation d’autres mods peut causer un comportement inattendu, y-compris des désynchronisation. Consultez les {0}Mods compatibles{1} pour plus d’information.",,,"Más modok használata váratlan viselkedést okozhat, beleértve a szinkronizálást. További információért lásd a {0}Modkompatibilitást{1}.",,,,,,,,,,,,,, +host/instructions/recommend,Instructions for the host 3,It is recommended that other mods are disabled and Derail Valley restarted prior to playing in multiplayer.,,推荐你卸载其他模组并重启游戏后,再进行联机,,,,,,Il est recommandé de désactiver les autres mods et de redémarrer Derail Valley avant de joueur en multijoueur.,,,"Javasoljuk, hogy tiltsa le a többi modot, és indítsa újra a Derail Valleyt, mielőtt többjátékos módban játszana.",,,,,,,,,,,,,, +host/instructions/signoff,Instructions for the host 4,We hope to have your favourite mods compatible with multiplayer in the future.,,我们希望未来能让你装联机模组的同时也能玩其他模组,,,,,,Nous espérons avoir vos mods favoris compatibles avec le multijoueur dans le futur.,,,"Reméljük, hogy kedvenc modjai a jövőben kompatibilisek lesznek a többjátékos játékkal.",,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,, ,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/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/disconnect/unreachable,Host Unreachable error message,Host Unreachable,,无法找到房主,,,,,,,,,A házigazda elérhetetlen,,,,,,,,,,,,,, -dr/disconnect/unknown,Unknown Host error message,Unknown Host,,房主未知,,,,,,,,,Ismeretlen gazda,,,,,,,,,,,,,, -dr/disconnect/kicked,Player Kicked error message,Player Kicked,,玩家已被踢出,,,,,,,,,Játékos kirúgva,,,,,,,,,,,,,, -dr/disconnect/rejected,Rejected! error message,Rejected!,,你已被拒绝加入服务器!,,,,,,,,,Elutasítva!,,,,,,,,,,,,,, -dr/disconnect/shutdown,Server Shutting Down error message,Server Shutting Down,,服务器已经关闭,,,,,,,,,Szerver leállás,,,,,,,,,,,,,, -dr/disconnect/timeout,Server Timed out,Server Timed out,,服务器连接超时,,,,,,,,,Szerver időtúllépés,,,,,,,,,,,,,, +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 en trop :\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/disconnect/unreachable,Host Unreachable error message,Host Unreachable,,无法找到房主,,,,,,Hôte injoignable,,,A házigazda elérhetetlen,,,,,,,,,,,,,, +dr/disconnect/unknown,Unknown Host error message,Unknown Host,,房主未知,,,,,,Hôte inconnu,,,Ismeretlen gazda,,,,,,,,,,,,,, +dr/disconnect/kicked,Player Kicked error message,Player Kicked,,玩家已被踢出,,,,,,Joueur éjecté,,,Játékos kirúgva,,,,,,,,,,,,,, +dr/disconnect/rejected,Rejected! error message,Rejected!,,你已被拒绝加入服务器!,,,,,,Rejeté !,,,Elutasítva!,,,,,,,,,,,,,, +dr/disconnect/shutdown,Server Shutting Down error message,Server Shutting Down,,服务器已经关闭,,,,,,Arrêt du serveur,,,Szerver leállás,,,,,,,,,,,,,, +dr/disconnect/timeout,Server Timed out,Server Timed out,,服务器连接超时,,,,,,Le serveur n’a pas répondu à temps,,,Szerver időtúllépés,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,, ,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!,Тільки господар може керувати оплатою! @@ -89,14 +89,14 @@ linfo/wait_for_server,Text shown in the loading screen.,Waiting for server to lo 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,Синхронізація стану світу ,,,,,,,,,,,,,,,,,,,,,,,,,,, ,Chat,,,,,,,,,,,,,,,,,,,,,,,,,, -chat/placeholder,Chat input placeholder,Type a message and press Enter!,,"在此输入文字,按回车发送",,,,,,,,,Írjon be egy üzenetet és nyomja meg az Entert!,,,,,,,,,,,,,, -chat/help/available,Chat help info available commands,Available commands:,,可用命令:,,,,,,,,,Elérhető parancsok:,,,,,,,,,,,,,, -chat/help/servermsg,Chat help send message as server,Send a message as the server (host only),,以服务器的身份发消息(仅限房主),,,,,,,,,Üzenet küldése szerverként (csak gazdagép),,,,,,,,,,,,,, -chat/help/whispermsg,Chat help whisper to a player,Whisper to a player,,向一位玩家说悄悄话,,,,,,,,,Suttogj egy játékosnak,,,,,,,,,,,,,, -chat/help/help,Chat help show help,Display this help message,,展示此帮助信息,,,,,,,,,Jelenítse meg ezt a súgóüzenetet,,,,,,,,,,,,,, -chat/help/msg,Chat help parameter e.g. /s ,message,,信息,,,,,,,,,Üzenet,,,,,,,,,,,,,, -chat/help/playername,Chat help parameter e.g. /w ,player name,,玩家名字,,,,,,,,,Játékos neve,,,,,,,,,,,,,, +chat/placeholder,Chat input placeholder,Type a message and press Enter!,,在此输入文字,按回车发送,,,,,,Tapez un message et appuyez sur Entrée !,,,Írjon be egy üzenetet és nyomja meg az Entert!,,,,,,,,,,,,,, +chat/help/available,Chat help info available commands,Available commands:,,可用命令:,,,,,,Commandes disponibles :,,,Elérhető parancsok:,,,,,,,,,,,,,, +chat/help/servermsg,Chat help send message as server,Send a message as the server (host only),,以服务器的身份发消息(仅限房主),,,,,,Envoyer un message au nom du serveur (hôte uniquement),,,Üzenet küldése szerverként (csak gazdagép),,,,,,,,,,,,,, +chat/help/whispermsg,Chat help whisper to a player,Whisper to a player,,向一位玩家说悄悄话,,,,,,Chuchoter à un joueur,,,Suttogj egy játékosnak,,,,,,,,,,,,,, +chat/help/help,Chat help show help,Display this help message,,展示此帮助信息,,,,,,Afficher ce message d’aide,,,Jelenítse meg ezt a súgóüzenetet,,,,,,,,,,,,,, +chat/help/msg,Chat help parameter e.g. /s ,message,,信息,,,,,,message,,,Üzenet,,,,,,,,,,,,,, +chat/help/playername,Chat help parameter e.g. /w ,player name,,玩家名字,,,,,,nom du joueur,,,Játékos neve,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,, ,Pause Menu,,,,,,,,,,,,,,,,,,,,,,,,,, -pm/disconnect_msg,Message when disconnecting from server (back to main menu),Disconnect and return to main menu?,,确定要断开连接并退回到主界面吗?,,,,,,,,,Leválasztás és visszatérés a főmenübe?,,,,,,,,,,,,,, -pm/quit_msg,Message when disconnecting from server (quit game),Disconnect and quit?,,确定要断开连接并直接退出吗?,,,,,,,,,Lekapcsolja és kilép?,,,,,,,,,,,,,, +pm/disconnect_msg,Message when disconnecting from server (back to main menu),Disconnect and return to main menu?,,确定要断开连接并退回到主界面吗?,,,,,,Se déconnecter et revenir au menu principal ?,,,Leválasztás és visszatérés a főmenübe?,,,,,,,,,,,,,, +pm/quit_msg,Message when disconnecting from server (quit game),Disconnect and quit?,,确定要断开连接并直接退出吗?,,,,,,Se déconnecter et quitter ?,,,Lekapcsolja és kilép?,,,,,,,,,,,,,, From edee3dbc46a1c421b270d4d44f1ee4e05301ebfa Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 4 Jan 2025 00:30:21 +1030 Subject: [PATCH 144/188] Intellisense Message fixes Implemented fixes for Intellisense messages / warnings --- Multiplayer/Components/IdMonoBehaviour.cs | 2 +- .../Components/MainMenu/HostGamePane.cs | 117 +++++++-------- .../Components/MainMenu/ServerBrowserPane.cs | 105 ++++++------- .../Components/Networking/NetworkLifecycle.cs | 22 +-- .../Networking/Train/NetworkedTrainCar.cs | 16 +- Multiplayer/Networking/Data/JobData.cs | 7 +- .../Networking/Data/TaskNetworkData.cs | 4 +- .../Data/Train/TrainsetMovementPart.cs | 2 + .../Managers/Client/ClientPlayerManager.cs | 2 +- .../Managers/Client/NetworkClient.cs | 57 ++++--- .../Managers/Client/ServerBrowserClient.cs | 11 +- .../Networking/Managers/NetworkManager.cs | 8 +- .../Managers/Server/LobbyServerManager.cs | 139 +++++++++--------- .../Managers/Server/NetworkServer.cs | 90 ++++++------ .../Jobs/ClientboundJobsCreatePacket.cs | 2 +- Multiplayer/Patches/Train/BogiePatch.cs | 2 +- Multiplayer/Patches/Train/CarSpawnerPatch.cs | 9 +- .../Train/CouplerChainInteractionPatch.cs | 7 +- Multiplayer/Patches/Train/HoseAndCockPatch.cs | 2 +- 19 files changed, 294 insertions(+), 310 deletions(-) diff --git a/Multiplayer/Components/IdMonoBehaviour.cs b/Multiplayer/Components/IdMonoBehaviour.cs index a233557..9c71626 100644 --- a/Multiplayer/Components/IdMonoBehaviour.cs +++ b/Multiplayer/Components/IdMonoBehaviour.cs @@ -9,7 +9,7 @@ namespace Multiplayer.Components; public abstract class IdMonoBehaviour : MonoBehaviour where T : struct where I : MonoBehaviour { private static readonly IdPool idPool = new(); - private static readonly Dictionary> indexToObject = new(); + private static readonly Dictionary> indexToObject = []; private T _netId; diff --git a/Multiplayer/Components/MainMenu/HostGamePane.cs b/Multiplayer/Components/MainMenu/HostGamePane.cs index 02972aa..996f35c 100644 --- a/Multiplayer/Components/MainMenu/HostGamePane.cs +++ b/Multiplayer/Components/MainMenu/HostGamePane.cs @@ -14,9 +14,9 @@ using Multiplayer.Networking.Data; using Multiplayer.Components.Networking; using Multiplayer.Components.Util; -using Multiplayer.Networking.Listeners; using UnityModManagerNet; using System.Linq; +using Multiplayer.Networking.Managers.Server; namespace Multiplayer.Components.MainMenu; public class HostGamePane : MonoBehaviour @@ -39,13 +39,9 @@ public class HostGamePane : MonoBehaviour TextMeshProUGUI serverDetails; SliderDV maxPlayers; - Toggle gamePublic; - ButtonDV startButton; - //GameObject ViewPort; - public ISaveGame saveGame; public UIStartGameData startGameData; public AUserProfileProvider userProvider; @@ -55,7 +51,7 @@ public class HostGamePane : MonoBehaviour public Action continueCareerRequested; #region setup - private void Awake() + public void Awake() { Multiplayer.Log("HostGamePane Awake()"); @@ -64,20 +60,20 @@ private void Awake() ValidateInputs(null); } - private void Start() + public void Start() { - Multiplayer.Log("HostGamePane Start()"); + Multiplayer.Log("HostGamePane Started"); } - private void OnEnable() + public void OnEnable() { //Multiplayer.Log("HostGamePane OnEnable()"); this.SetupListeners(true); } // Disable listeners - private void OnDisable() + public void OnDisable() { this.SetupListeners(false); } @@ -178,7 +174,7 @@ private void BuildUI() scrollerRT.sizeDelta = new Vector2(scrollerRT.sizeDelta.x, 504); // Create the content object - GameObject controls = new GameObject("Controls"); + GameObject controls = new("Controls"); controls.SetLayersRecursive(Layers.UI); controls.transform.SetParent(scroller.viewport.transform, false); @@ -288,7 +284,7 @@ private void BuildUI() private GameObject NewContentGroup(GameObject parent, Vector2 sizeDelta, int cellMaxHeight = 53) { // Create a content group - GameObject contentGroup = new GameObject("ContentGroup"); + GameObject contentGroup = new("ContentGroup"); contentGroup.SetLayersRecursive(Layers.UI); RectTransform groupRect = contentGroup.AddComponent(); contentGroup.transform.SetParent(parent.transform, false); @@ -335,7 +331,7 @@ private void SetupListeners(bool on) private void ValidateInputs(string text) { bool valid = true; - int portNum=0; + int portNum; if (serverName.text.Trim() == "" || serverName.text.Length > MAX_SERVER_NAME_LEN) valid = false; @@ -359,73 +355,74 @@ private void ValidateInputs(string text) 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.isPublic = gamePublic.isOn; + using (LobbyServerData serverData = new()) + { + serverData.port = (port.text == "") ? Multiplayer.Settings.Port : int.Parse(port.text); ; + serverData.Name = serverName.text.Trim(); + serverData.HasPassword = password.text != ""; + serverData.isPublic = gamePublic.isOn; - 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.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.CurrentPlayers = 0; + serverData.MaxPlayers = (int)maxPlayers.value; - ModInfo[] serverMods = ModInfo.FromModEntries(UnityModManager.modEntries) - .Where(mod => !NetworkServer.modWhiteList.Contains(mod.Id) && mod.Id != Multiplayer.ModEntry.Info.Id).ToArray(); + ModInfo[] serverMods = ModInfo.FromModEntries(UnityModManager.modEntries) + .Where(mod => !NetworkServer.modWhiteList.Contains(mod.Id) && mod.Id != Multiplayer.ModEntry.Info.Id).ToArray(); - string requiredMods = ""; - if( serverMods.Length > 0) - { - requiredMods = string.Join(", ", serverMods.Select(mod => $"{{{mod.Id}, {mod.Version}}}")); - } + string requiredMods = ""; + if (serverMods.Length > 0) + { + requiredMods = string.Join(", ", serverMods.Select(mod => $"{{{mod.Id}, {mod.Version}}}")); + } - serverData.RequiredMods = requiredMods; //FIX THIS - get the mods required - serverData.GameVersion = BuildInfo.BUILD_VERSION_MAJOR.ToString(); - serverData.MultiplayerVersion = Multiplayer.Ver; + serverData.RequiredMods = requiredMods; //FIX THIS - get the mods required + serverData.GameVersion = BuildInfo.BUILD_VERSION_MAJOR.ToString(); + serverData.MultiplayerVersion = Multiplayer.Ver; - serverData.ServerDetails = details.text.Trim(); + serverData.ServerDetails = details.text.Trim(); - if (saveGame != null) - { - ISaveGameplayInfo saveGameplayInfo = this.userProvider.GetSaveGameplayInfo(this.saveGame); - if (!saveGameplayInfo.IsCorrupt) + if (saveGame != null) { - 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); + 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); } - } - else if(startGameData != null) - { - serverData.Difficulty = LobbyServerData.GetDifficultyFromString(this.startGameData.difficulty.Name); - serverData.GameMode = LobbyServerData.GetGameModeFromString(startGameData.session.GameMode); - } - Multiplayer.Settings.ServerName = serverData.Name; - Multiplayer.Settings.Password = password.text; - Multiplayer.Settings.PublicGame = serverData.isPublic; - Multiplayer.Settings.Port = serverData.port; - Multiplayer.Settings.MaxPlayers = serverData.MaxPlayers; - Multiplayer.Settings.Details = serverData.ServerDetails; + Multiplayer.Settings.ServerName = serverData.Name; + Multiplayer.Settings.Password = password.text; + Multiplayer.Settings.PublicGame = serverData.isPublic; + 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; + //Pass the server data to the NetworkLifecycle manager + NetworkLifecycle.Instance.serverData = serverData; + } //Mark it as a real multiplayer game - NetworkLifecycle.Instance.isSinglePlayer = false; + NetworkLifecycle.Instance.IsSinglePlayer = false; - var ContinueGameRequested = lcInstance.GetType().GetMethod("OnRunClicked", BindingFlags.NonPublic | BindingFlags.Instance); - + var ContinueGameRequested = lcInstance.GetType().GetMethod("OnRunClicked", BindingFlags.NonPublic | BindingFlags.Instance); + //Multiplayer.Log($"OnRunClicked exists: {ContinueGameRequested != null}"); ContinueGameRequested?.Invoke(lcInstance, null); } - + #endregion diff --git a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs index 11f1ab5..331388a 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs @@ -17,8 +17,9 @@ using DV; using System.Net; using LiteNetLib; -using Multiplayer.Networking.Listeners; using System.Collections.Generic; +using Multiplayer.Networking.Managers.Client; +using JetBrains.Annotations; namespace Multiplayer.Components.MainMenu { @@ -71,15 +72,15 @@ private enum ConnectionState private const int MAX_PORT = 49151; //Gridview variables - private ObservableCollectionExt gridViewModel = new ObservableCollectionExt(); + private readonly ObservableCollectionExt gridViewModel = []; private ServerBrowserGridView gridView; private ScrollRect parentScroller; private string serverIDOnRefresh; private IServerBrowserGameDetails selectedServer; //ping tracking - private List serversToPing = new List(); - private Dictionary serverPings = new Dictionary(); + private readonly List serversToPing = []; + private readonly Dictionary serverPings = []; private float pingTimer = 0f; private const float PING_INTERVAL = 2f; // base interval to refresh all pings @@ -87,7 +88,7 @@ private enum ConnectionState private const int SERVERS_PER_BATCH = 10; //LAN tracking - private List localServers = new List(); + private readonly List localServers = []; private const int LAN_TIMEOUT = 60; //How long to hold a LAN server without a response private const int DISCOVERY_TIMEOUT = 1; //how long to wait for servers to respond private bool localRefreshComplete; @@ -103,7 +104,7 @@ private enum ConnectionState private TextMeshProUGUI detailsPane; //Remote server tracking - private List remoteServers = new List(); + private readonly List remoteServers = []; private bool serverRefreshing = false; private float timePassed = 0f; //time since last refresh private const int AUTO_REFRESH_TIME = 30; //how often to refresh in auto @@ -126,7 +127,7 @@ private enum ConnectionState #region setup - private void Awake() + public void Awake() { //Multiplayer.Log("MultiplayerPane Awake()"); CleanUI(); @@ -136,7 +137,7 @@ private void Awake() RefreshGridView(); } - private void OnEnable() + public void OnEnable() { //Multiplayer.Log("MultiplayerPane OnEnable()"); if (!this.parentScroller) @@ -161,7 +162,7 @@ private void OnEnable() } // Disable listeners - private void OnDisable() + public void OnDisable() { this.SetupListeners(false); @@ -173,7 +174,7 @@ private void OnDisable() } } - private void OnDestroy() + public void OnDestroy() { if (serverBrowserClient == null) return; @@ -182,11 +183,10 @@ private void OnDestroy() serverBrowserClient.Stop(); } - private void Update() + public void Update() { //Poll for any LAN discovery or ping packets - if (serverBrowserClient != null) - serverBrowserClient.PollEvents(); + serverBrowserClient?.PollEvents(); //Handle server refresh interval timePassed += Time.deltaTime; @@ -307,7 +307,7 @@ private void BuildUI() // Create Content GameObject.Destroy(serverScroll.FindChildByName("GRID VIEW").gameObject); - GameObject content = new GameObject("Content", typeof(RectTransform), typeof(ContentSizeFitter), typeof(VerticalLayoutGroup)); + GameObject content = new("Content", typeof(RectTransform), typeof(ContentSizeFitter), typeof(VerticalLayoutGroup)); content.transform.SetParent(viewport.transform, false); ContentSizeFitter contentSF = content.GetComponent(); contentSF.verticalFit = ContentSizeFitter.FitMode.PreferredSize; @@ -323,12 +323,12 @@ private void BuildUI() scrollRect.content = contentRT; // Create TextMeshProUGUI object - GameObject textContainerGO = new GameObject("Details Container", typeof(HorizontalLayoutGroup)); + GameObject textContainerGO = new ("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)); + GameObject textGO = new("Details Text", typeof(TextMeshProUGUI)); textGO.transform.SetParent(textContainerGO.transform, false); HorizontalLayoutGroup textHLG = textGO.GetComponent(); detailsPane = textGO.GetComponent(); @@ -483,7 +483,7 @@ private void IndexChanged(AGridView gridView) //Check if we can connect to this server Multiplayer.Log($"Server: \"{selectedServer.GameVersion}\" \"{selectedServer.MultiplayerVersion}\""); - Multiplayer.Log($"Client: \"{BuildInfo.BUILD_VERSION_MAJOR.ToString()}\" \"{Multiplayer.Ver}\""); + Multiplayer.Log($"Client: \"{BuildInfo.BUILD_VERSION_MAJOR}\" \"{Multiplayer.Ver}\""); Multiplayer.Log($"Result: \"{selectedServer.GameVersion == BuildInfo.BUILD_VERSION_MAJOR.ToString()}\" \"{selectedServer.MultiplayerVersion == Multiplayer.Ver}\""); bool canConnect = selectedServer.GameVersion == BuildInfo.BUILD_VERSION_MAJOR.ToString() && @@ -504,17 +504,14 @@ private void UpdateElement(IServerBrowserGameDetails element) if (index >= 0) { var viewElement = gridView.GetElementAt(index); - if (viewElement != null) - { - viewElement.UpdateView(); - } + viewElement?.UpdateView(); } } #endregion private void UpdateDetailsPane() { - string details=""; + string details; if (selectedServer != null) { @@ -523,9 +520,9 @@ private void UpdateDetailsPane() //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 = "" + LocalizationAPI.L("launcher/game_mode", []) + " " + LobbyServerData.GetGameModeFromInt(selectedServer.GameMode) + "
    "; + details += "" + LocalizationAPI.L("launcher/difficulty", []) + " " + LobbyServerData.GetDifficultyFromInt(selectedServer.Difficulty) + "
    "; + details += "" + LocalizationAPI.L("launcher/in_game_time_passed", []) + " " + 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 + ": " + (string.IsNullOrEmpty(selectedServer.RequiredMods) ? Locale.SERVER_BROWSER__NO : Locale.SERVER_BROWSER__YES) + "
    "; @@ -604,11 +601,8 @@ private void ShowIpPopup() { if (parsedAddress.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork) connectionState = ConnectionState.AttemptingIPv4; - } else - { connectionState = ConnectionState.AttemptingIPv6; - } address = result.data; ShowPortPopup(); @@ -645,7 +639,6 @@ private void ShowPortPopup() } else { - portNumber = ushort.Parse(result.data); ShowPasswordPopup(); } @@ -860,16 +853,12 @@ private void AttemptFail() { connectionState = ConnectionState.Failed; - if (connectingPopup != null) - { - connectingPopup.RequestClose(PopupClosedByAction.Abortion, null); - } + connectingPopup?.RequestClose(PopupClosedByAction.Abortion, null); if(this.gridView != null) IndexChanged(this.gridView); - if(buttonDirectIP != null) - buttonDirectIP.ToggleInteractable(true); + buttonDirectIP?.ToggleInteractable(true); } private void OnDisconnect(DisconnectReason reason, string message) @@ -955,39 +944,37 @@ private void OnDisconnect(DisconnectReason reason, string message) IEnumerator GetRequest(string uri) { - using (UnityWebRequest webRequest = UnityWebRequest.Get(uri)) - { - // Request and wait for the desired page. - yield return webRequest.SendWebRequest(); + 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; + string[] pages = uri.Split('/'); + int page = pages.Length - 1; - if (webRequest.isNetworkError) - { - Multiplayer.LogError(pages[page] + ": Error: " + webRequest.error); - } - else - { - Multiplayer.Log(pages[page] + ":\nReceived: " + webRequest.downloadHandler.text); - - LobbyServerData[] response; - - response = Newtonsoft.Json.JsonConvert.DeserializeObject(webRequest.downloadHandler.text); + if (webRequest.isNetworkError) + { + Multiplayer.LogError(pages[page] + ": Error: " + webRequest.error); + } + else + { + Multiplayer.Log(pages[page] + ":\nReceived: " + webRequest.downloadHandler.text); - Multiplayer.Log($"Serverbrowser servers: {response.Length}"); + LobbyServerData[] response; - foreach (LobbyServerData server in response) - { - Multiplayer.Log($"Server name: \"{server.Name}\", IPv4: {server.ipv4}, IPv6: {server.ipv6}, Port: {server.port}"); - } + response = Newtonsoft.Json.JsonConvert.DeserializeObject(webRequest.downloadHandler.text); - remoteServers.AddRange(response); + Multiplayer.Log($"Serverbrowser servers: {response.Length}"); + foreach (LobbyServerData server in response) + { + Multiplayer.Log($"Server name: \"{server.Name}\", IPv4: {server.ipv4}, IPv6: {server.ipv6}, Port: {server.port}"); } - remoteRefreshComplete = true; + remoteServers.AddRange(response); + } + + remoteRefreshComplete = true; } private void RefreshGridView() diff --git a/Multiplayer/Components/Networking/NetworkLifecycle.cs b/Multiplayer/Components/Networking/NetworkLifecycle.cs index 64eccac..3bb6597 100644 --- a/Multiplayer/Components/Networking/NetworkLifecycle.cs +++ b/Multiplayer/Components/Networking/NetworkLifecycle.cs @@ -8,7 +8,9 @@ using LiteNetLib.Utils; using Multiplayer.Components.Networking.UI; using Multiplayer.Networking.Data; -using Multiplayer.Networking.Listeners; +using Multiplayer.Networking.Managers; +using Multiplayer.Networking.Managers.Client; +using Multiplayer.Networking.Managers.Server; using Multiplayer.Utils; using Newtonsoft.Json; using UnityEngine; @@ -23,8 +25,8 @@ public class NetworkLifecycle : SingletonBehaviour 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 bool IsPublicGame { get; set; } = false; + public bool IsSinglePlayer { get; set; } = true; public NetworkServer Server { get; private set; } @@ -49,7 +51,7 @@ public class NetworkLifecycle : SingletonBehaviour /// public bool IsHost(NetPeer peer) { - return Server?.IsRunning == true && Client?.IsRunning == true && Client?.selfPeer?.Id == peer?.Id; + return Server?.IsRunning == true && Client?.IsRunning == true && Client?.SelfPeer?.Id == peer?.Id; } /// @@ -58,7 +60,7 @@ public bool IsHost(NetPeer peer) /// public bool IsHost() { - return IsHost(Client?.selfPeer); + return IsHost(Client?.SelfPeer); } private readonly Queue mainMenuLoadedQueue = new(); @@ -126,7 +128,7 @@ public bool StartServer(IDifficulty difficulty) if (Server != null) throw new InvalidOperationException("NetworkManager already exists!"); - if (!isSinglePlayer) + if (!IsSinglePlayer) { if(serverData != null) { @@ -135,16 +137,16 @@ public bool StartServer(IDifficulty difficulty) } Multiplayer.Log($"Starting server on port {port}"); - NetworkServer server = new(difficulty, Multiplayer.Settings, isSinglePlayer, serverData); + NetworkServer server = new(difficulty, Multiplayer.Settings, IsSinglePlayer, serverData); //reset for next game - isSinglePlayer = true; + IsSinglePlayer = true; serverData = null; if (!server.Start(port)) return false; Server = server; - StartClient("localhost", port, Multiplayer.Settings.Password, isSinglePlayer, null/* (DisconnectReason dr,string msg) =>{ }*/); + StartClient("localhost", port, Multiplayer.Settings.Password, IsSinglePlayer, null/* (DisconnectReason dr,string msg) =>{ }*/); return true; } public void StartClient(string address, int port, string password, bool isSinglePlayer, Action onDisconnect ) @@ -209,7 +211,7 @@ private void TickManager(NetworkManager manager) public void Stop() { - if (Stats != null) Stats.Hide(); + Stats?.Hide(); Server?.Stop(); Client?.Stop(); Server = null; diff --git a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs index d738391..76160dc 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs @@ -2,9 +2,11 @@ using System.Collections; using System.Collections.Generic; using System.Linq; +using DV.MultipleUnit; using DV.Simulation.Brake; using DV.Simulation.Cars; using DV.ThingTypes; +using JetBrains.Annotations; using LocoSim.Definitions; using LocoSim.Implementations; using Multiplayer.Components.Networking.Player; @@ -20,10 +22,10 @@ public class NetworkedTrainCar : IdMonoBehaviour { #region Lookup Cache - private static readonly Dictionary trainCarsToNetworkedTrainCars = new(); - private static readonly Dictionary trainCarIdToNetworkedTrainCars = new(); - private static readonly Dictionary trainCarIdToTrainCars = new(); - private static readonly Dictionary hoseToCoupler = new(); + private static readonly Dictionary trainCarsToNetworkedTrainCars = []; + private static readonly Dictionary trainCarIdToNetworkedTrainCars = []; + private static readonly Dictionary trainCarIdToTrainCars = []; + private static readonly Dictionary hoseToCoupler = []; public static bool Get(ushort netId, out NetworkedTrainCar obj) { @@ -131,7 +133,8 @@ protected override void Awake() } } - private void Start() + [UsedImplicitly] + public void Start() { brakeSystem = TrainCar.brakeSystem; @@ -198,8 +201,7 @@ private void Start() StartCoroutine(Server_WaitForLogicCar()); } } - - private void OnDisable() + public void OnDisable() { if (UnloadWatcher.isQuitting) return; diff --git a/Multiplayer/Networking/Data/JobData.cs b/Multiplayer/Networking/Data/JobData.cs index 638a9df..0d4cc6d 100644 --- a/Multiplayer/Networking/Data/JobData.cs +++ b/Multiplayer/Networking/Data/JobData.cs @@ -5,6 +5,7 @@ using Multiplayer.Components.Networking.Jobs; using Multiplayer.Components.Networking.World; using System; +using System.Collections.Generic; using System.IO; using System.Linq; @@ -31,7 +32,7 @@ public static JobData FromJob(NetworkedStationController netStation, NetworkedJo Job job = networkedJob.Job; ushort itemNetId = 0; - ItemPositionData itemPos = new ItemPositionData(); + ItemPositionData itemPos = new(); //Multiplayer.Log($"JobData.FromJob({netStation.name}, {job.ID}, {networkedJob.Job.State})"); @@ -87,8 +88,8 @@ public static void Serialize(NetDataWriter writer, JobData data) writer.Put(data.ID); //task data - add compression - using (MemoryStream ms = new MemoryStream()) - using (BinaryWriter bw = new BinaryWriter(ms)) + using (MemoryStream ms = new()) + using (BinaryWriter bw = new(ms)) { bw.Write((byte)data.Tasks.Length); foreach (var task in data.Tasks) diff --git a/Multiplayer/Networking/Data/TaskNetworkData.cs b/Multiplayer/Networking/Data/TaskNetworkData.cs index bd74e6e..123dc8f 100644 --- a/Multiplayer/Networking/Data/TaskNetworkData.cs +++ b/Multiplayer/Networking/Data/TaskNetworkData.cs @@ -64,8 +64,8 @@ protected void DeserializeCommon(NetDataReader reader) #region Extension of TaskTypes public static class TaskNetworkDataFactory { - private static readonly Dictionary> TypeToTaskNetworkData = new(); - private static readonly Dictionary> EnumToEmptyTaskNetworkData = new(); + private static readonly Dictionary> TypeToTaskNetworkData = []; + private static readonly Dictionary> EnumToEmptyTaskNetworkData = []; public static void RegisterTaskType(TaskType taskType, Func converter, Func emptyCreator) where TGameTask : Task diff --git a/Multiplayer/Networking/Data/Train/TrainsetMovementPart.cs b/Multiplayer/Networking/Data/Train/TrainsetMovementPart.cs index e6bb90c..84b5eef 100644 --- a/Multiplayer/Networking/Data/Train/TrainsetMovementPart.cs +++ b/Multiplayer/Networking/Data/Train/TrainsetMovementPart.cs @@ -52,7 +52,9 @@ public TrainsetMovementPart(RigidbodySnapshot rigidbodySnapshot) RigidbodySnapshot = rigidbodySnapshot; } +#pragma warning disable EPS05 // Use in-modifier for a readonly struct public static void Serialize(NetDataWriter writer, TrainsetMovementPart data) +#pragma warning restore EPS05 // Use in-modifier for a readonly struct { writer.Put((byte)data.typeFlag); diff --git a/Multiplayer/Networking/Managers/Client/ClientPlayerManager.cs b/Multiplayer/Networking/Managers/Client/ClientPlayerManager.cs index caf86cd..eab4bd2 100644 --- a/Multiplayer/Networking/Managers/Client/ClientPlayerManager.cs +++ b/Multiplayer/Networking/Managers/Client/ClientPlayerManager.cs @@ -5,7 +5,7 @@ using UnityEngine; using Object = UnityEngine.Object; -namespace Multiplayer.Networking.Listeners; +namespace Multiplayer.Networking.Managers.Client; public class ClientPlayerManager { diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 3b07476..81e5840 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -29,6 +29,7 @@ using Multiplayer.Networking.Packets.Common; using Multiplayer.Networking.Packets.Common.Train; using Multiplayer.Networking.Packets.Serverbound; +using Multiplayer.Networking.Data.Train; using Multiplayer.Patches.SaveGame; using Multiplayer.Utils; using Newtonsoft.Json.Linq; @@ -40,17 +41,16 @@ using LiteNetLib.Utils; using DV.UserManagement; using DV.Common; -using Multiplayer.Networking.Data.Train; -namespace Multiplayer.Networking.Listeners; +namespace Multiplayer.Networking.Managers.Client; public class NetworkClient : NetworkManager { protected override string LogPrefix => "[Client]"; - private Action onDisconnect; + private Action onDisconnect; - public NetPeer selfPeer { get; private set; } + public NetPeer SelfPeer { get; private set; } public readonly ClientPlayerManager ClientPlayerManager; // One way ping in milliseconds @@ -82,7 +82,7 @@ public void Start(string address, int port, string password, bool isSinglePlayer Mods = ModInfo.FromModEntries(UnityModManager.modEntries) }; netPacketProcessor.Write(cachedWriter, serverboundClientLoginPacket); - selfPeer = netManager.Connect(address, port, cachedWriter); + SelfPeer = netManager.Connect(address, port, cachedWriter); isAlsoHost = NetworkLifecycle.Instance.IsServerRunning; originalSession = UserManager.Instance.CurrentUser.CurrentSession; @@ -182,8 +182,8 @@ public override void OnPeerDisconnected(NetPeer peer, DisconnectInfo disconnectI MainMenu.GoBackToMainMenu(); } - - if( disconnectInfo.Reason == DisconnectReason.ConnectionRejected || + + if (disconnectInfo.Reason == DisconnectReason.ConnectionRejected || disconnectInfo.Reason == DisconnectReason.RemoteConnectionClose) { netPacketProcessor.ReadAllPackets(disconnectInfo.AdditionalData); @@ -242,7 +242,7 @@ public override void OnNatIntroductionSuccess(IPEndPoint targetEndPoint, NatAddr private void OnClientboundServerDenyPacket(ClientboundServerDenyPacket packet) { - + /* NetworkLifecycle.Instance.QueueMainMenuEvent(() => { @@ -250,22 +250,22 @@ private void OnClientboundServerDenyPacket(ClientboundServerDenyPacket packet) if (popup == null) return; */ - string text = Locale.Get(packet.ReasonKey, packet.ReasonArgs); + string text = Locale.Get(packet.ReasonKey, packet.ReasonArgs); - if (packet.Missing.Length != 0 || packet.Extra.Length != 0) + if (packet.Missing.Length != 0 || packet.Extra.Length != 0) + { + text += "\n\n"; + if (packet.Missing.Length != 0) { - text += "\n\n"; - if (packet.Missing.Length != 0) - { - text += Locale.Get(Locale.DISCONN_REASON__MODS_MISSING_KEY, placeholders: string.Join("\n - ", packet.Missing)); - if (packet.Extra.Length != 0) - text += "\n"; - } - + text += Locale.Get(Locale.DISCONN_REASON__MODS_MISSING_KEY, placeholders: string.Join("\n - ", packet.Missing)); if (packet.Extra.Length != 0) - text += Locale.Get(Locale.DISCONN_REASON__MODS_EXTRA_KEY, placeholders: string.Join("\n - ", packet.Extra)); + text += "\n"; } + if (packet.Extra.Length != 0) + text += Locale.Get(Locale.DISCONN_REASON__MODS_EXTRA_KEY, placeholders: string.Join("\n - ", packet.Extra)); + } + //popup.labelTMPro.text = text; //}); Log($"Received player deny packet: {text}"); @@ -404,7 +404,7 @@ private void OnClientboundRemoveLoadingScreen(ClientboundRemoveLoadingScreenPack if (common != null) { // - GameObject chat = new GameObject("Chat GUI", typeof(ChatGUI)); + GameObject chat = new("Chat GUI", typeof(ChatGUI)); chat.transform.SetParent(common.transform, false); chatGUI = chat.GetComponent(); } @@ -451,7 +451,7 @@ private void OnClientBoundStationControllerLookupPacket(ClientBoundStationContro } - private void OnClientboundRailwayStatePacket(ClientboundRailwayStatePacket packet) + private void OnClientboundRailwayStatePacket(ClientboundRailwayStatePacket packet) { for (int i = 0; i < packet.SelectedJunctionBranches.Length; i++) { @@ -499,10 +499,10 @@ private void OnClientboundSpawnTrainSetPacket(ClientboundSpawnTrainSetPacket pac foreach (var part in packet.SpawnParts) { - if(NetworkedTrainCar.GetTrainCarFromTrainId(part.CarId, out TrainCar car)) + if (NetworkedTrainCar.GetTrainCarFromTrainId(part.CarId, out TrainCar car)) { LogError($"ClientboundSpawnTrainSetPacket() Tried to spawn trainset with carId: {part.CarId}, but car already exists!"); - return; + return; } } @@ -585,10 +585,9 @@ private void OnCommonTrainUncouplePacket(CommonTrainUncouplePacket packet) private void OnCommonHoseConnectedPacket(CommonHoseConnectedPacket packet) { - TrainCar trainCar = null; TrainCar otherTrainCar = null; - if (!NetworkedTrainCar.GetTrainCar(packet.NetId, out trainCar) || !NetworkedTrainCar.GetTrainCar(packet.OtherNetId, out otherTrainCar)) + if (!NetworkedTrainCar.GetTrainCar(packet.NetId, out TrainCar trainCar) || !NetworkedTrainCar.GetTrainCar(packet.OtherNetId, out otherTrainCar)) { LogDebug(() => $"OnCommonHoseConnectedPacket() netId: {packet.NetId}, trainCar found?: {trainCar != null}, otherNetId: {packet.OtherNetId}, otherTrainCar found?: {otherTrainCar != null}"); return; @@ -856,7 +855,7 @@ private void OnClientboundJobsCreatePacket(ClientboundJobsCreatePacket packet) if (NetworkLifecycle.Instance.IsHost()) return; - if(!NetworkedStationController.Get(packet.StationNetId, out NetworkedStationController networkedStationController)) + if (!NetworkedStationController.Get(packet.StationNetId, out NetworkedStationController networkedStationController)) { LogError($"OnClientboundJobsCreatePacket() {packet.StationNetId} does not exist!"); return; @@ -882,15 +881,15 @@ private void OnClientboundJobsUpdatePacket(ClientboundJobsUpdatePacket packet) networkedStationController.UpdateJobs(packet.JobUpdates); } - + private void OnClientboundJobValidateResponsePacket(ClientboundJobValidateResponsePacket packet) { Log($"OnClientboundJobValidateResponsePacket() JobNetId: {packet.JobNetId}, Status: {packet.Invalid}"); - if(!NetworkedJob.Get(packet.JobNetId, out NetworkedJob networkedJob)) + if (!NetworkedJob.Get(packet.JobNetId, out NetworkedJob networkedJob)) return; - GameObject.Destroy(networkedJob.gameObject); + Object.Destroy(networkedJob.gameObject); } private void OnCommonItemChangePacket(CommonItemChangePacket packet) diff --git a/Multiplayer/Networking/Managers/Client/ServerBrowserClient.cs b/Multiplayer/Networking/Managers/Client/ServerBrowserClient.cs index 35c1d12..ed6f618 100644 --- a/Multiplayer/Networking/Managers/Client/ServerBrowserClient.cs +++ b/Multiplayer/Networking/Managers/Client/ServerBrowserClient.cs @@ -6,11 +6,10 @@ using System.Threading.Tasks; using System.Diagnostics; using System.Linq; -using Multiplayer.Networking.Managers.Server; using Multiplayer.Networking.Data; -namespace Multiplayer.Networking.Listeners; +namespace Multiplayer.Networking.Managers.Client; public class ServerBrowserClient : NetworkManager, IDisposable { @@ -31,11 +30,11 @@ public void Start() } } - private Dictionary pingInfos = new Dictionary(); + private readonly Dictionary pingInfos = []; public Action OnPing; // serverId, pingTime, isIPv4 public Action OnDiscovery; // endPoint, serverId, serverData - private int[] discoveryPorts = { 8888, 8889, 8890 }; + private readonly int[] discoveryPorts = [8888, 8889, 8890]; private const int PingTimeoutMs = 5000; // 5 seconds timeout @@ -167,7 +166,7 @@ private void OnUnconnectedDiscoveryPacket(UnconnectedDiscoveryPacket packet, IPE if (packet.IsResponse) { //Log($"OnUnconnectedDiscoveryPacket({packet.PacketType}, {endPoint?.Address}) id: {packet.data.id}"); - OnDiscovery?.Invoke(endPoint,packet.Data); + OnDiscovery?.Invoke(endPoint, packet.Data); } } @@ -182,7 +181,7 @@ public void SendUnconnectedPingPacket(string serverId, string ipv4, string ipv6, return; } - PingInfo pingInfo = new PingInfo(); + PingInfo pingInfo = new(); pingInfos[serverId] = pingInfo; //LogDebug(()=>$"Sending ping to {serverId} at IPv4: {ipv4}, IPv6: {ipv6}, Port: {port}"); diff --git a/Multiplayer/Networking/Managers/NetworkManager.cs b/Multiplayer/Networking/Managers/NetworkManager.cs index 2db9e8e..a7c5163 100644 --- a/Multiplayer/Networking/Managers/NetworkManager.cs +++ b/Multiplayer/Networking/Managers/NetworkManager.cs @@ -7,7 +7,7 @@ using Multiplayer.Networking.Data.Train; using Multiplayer.Networking.Serialization; -namespace Multiplayer.Networking.Listeners; +namespace Multiplayer.Networking.Managers; public abstract class NetworkManager : INetEventListener, INatPunchListener { @@ -88,7 +88,7 @@ public virtual void Stop() protected NetDataWriter WriteNetSerializablePacket(T packet) where T : INetSerializable, new() { cachedWriter.Reset(); - netPacketProcessor.WriteNetSerializable(cachedWriter, ref packet); + netPacketProcessor.WriteNetSerializable(cachedWriter, ref packet); return cachedWriter; } @@ -107,7 +107,7 @@ public virtual void Stop() netManager.SendUnconnectedMessage(WritePacket(packet), ipAddress, port); } - protected abstract void Subscribe(); + protected abstract void Subscribe(); #region Net Events @@ -142,7 +142,7 @@ public void OnNetworkReceiveUnconnected(IPEndPoint remoteEndPoint, NetPacketRead IsProcessingPacket = true; netPacketProcessor.ReadAllPackets(reader, remoteEndPoint); } - catch (ParseException e) + catch (ParseException e) { Multiplayer.LogWarning($"Failed to parse packet: {e.Message}"); } diff --git a/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs b/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs index 3c74e66..b29397f 100644 --- a/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs +++ b/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs @@ -1,6 +1,5 @@ using System; using Multiplayer.Networking.Data; -using Multiplayer.Networking.Listeners; using Newtonsoft.Json; using System.Collections; using UnityEngine; @@ -14,8 +13,8 @@ using LiteNetLib.Utils; using Multiplayer.Networking.Packets.Unconnected; using System.Net; -using LocoSim.Implementations; using System.Linq; +using JetBrains.Annotations; namespace Multiplayer.Networking.Managers.Server; public class LobbyServerManager : MonoBehaviour @@ -26,7 +25,7 @@ public class LobbyServerManager : MonoBehaviour private const string ENDPOINT_REMOVE_SERVER = "remove_game_server"; //RegEx - private readonly Regex IPv4Match = 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 readonly Regex IPv4Match = 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 const int REDIRECT_MAX = 5; @@ -35,8 +34,8 @@ public class LobbyServerManager : MonoBehaviour private const int PLAYER_CHANGE_TIME = 5; //Update server early if the number of players has changed in this time frame private NetworkServer server; - private string server_id { get; set; } - private string private_key { get; set; } + private string server_id; + private string private_key; private bool initialised = false; private bool sendUpdates = false; @@ -46,18 +45,18 @@ public class LobbyServerManager : MonoBehaviour private NetManager discoveryManager; private NetPacketProcessor packetProcessor; private EventBasedNetListener discoveryListener; - private NetDataWriter cachedWriter = new(); - public static int[] discoveryPorts = { 8888, 8889, 8890 }; + private readonly NetDataWriter cachedWriter = new(); + public static int[] discoveryPorts = [8888, 8889, 8890]; - #region MonoBehavior - private void Awake() + #region + public void Awake() { server = NetworkLifecycle.Instance.Server; Multiplayer.Log($"LobbyServerManager New({server != null})"); } - private IEnumerator Start() + public IEnumerator Start() { server.serverData.ipv6 = GetStaticIPv6Address(); server.serverData.LocalIPv4 = GetLocalIPv4Address(); @@ -94,7 +93,7 @@ private IEnumerator Start() StartDiscoveryServer(); } - private void OnDestroy() + public void OnDestroy() { Multiplayer.Log($"LobbyServerManager OnDestroy()"); sendUpdates = false; @@ -104,7 +103,7 @@ private void OnDestroy() discoveryManager?.Stop(); } - private void Update() + public void Update() { if (sendUpdates) { @@ -138,7 +137,7 @@ public void RemoveFromLobbyServer() private IEnumerator RegisterWithLobbyServer(string uri) { - JsonSerializerSettings jsonSettings = new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }; + JsonSerializerSettings jsonSettings = new() { NullValueHandling = NullValueHandling.Ignore }; string json = JsonConvert.SerializeObject(server.serverData, jsonSettings); Multiplayer.LogDebug(()=>$"JsonRequest: {json}"); @@ -162,7 +161,7 @@ private IEnumerator RegisterWithLobbyServer(string uri) private IEnumerator RemoveFromLobbyServer(string uri) { - JsonSerializerSettings jsonSettings = new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }; + JsonSerializerSettings jsonSettings = new() { NullValueHandling = NullValueHandling.Ignore }; string json = JsonConvert.SerializeObject(new LobbyServerResponseData(server_id, private_key), jsonSettings); Multiplayer.LogDebug(() => $"JsonRequest: {json}"); @@ -176,18 +175,18 @@ private IEnumerator RemoveFromLobbyServer(string uri) private IEnumerator UpdateLobbyServer(string uri) { - JsonSerializerSettings jsonSettings = new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }; + JsonSerializerSettings jsonSettings = new() { NullValueHandling = NullValueHandling.Ignore }; DateTime start = AStartGameData.BaseTimeAndDate; DateTime current = WeatherDriver.Instance.manager.DateTime; TimeSpan inGame = current - start; - LobbyServerUpdateData reqData = new LobbyServerUpdateData( - server_id, - private_key, - inGame.ToString("d\\d\\ hh\\h\\ mm\\m\\ ss\\s"), - server.serverData.CurrentPlayers - ); + LobbyServerUpdateData reqData = new( + server_id, + private_key, + inGame.ToString("d\\d\\ hh\\h\\ mm\\m\\ ss\\s"), + server.serverData.CurrentPlayers + ); string json = JsonConvert.SerializeObject(reqData, jsonSettings); Multiplayer.LogDebug(() => $"UpdateLobbyServer JsonRequest: {json}"); @@ -250,41 +249,39 @@ private IEnumerator SendWebRequest(string uri, string json, Action 0) { - webRequest.redirectLimit = 0; + webRequest.uploadHandler = new UploadHandlerRaw(System.Text.Encoding.UTF8.GetBytes(json)) { contentType = "application/json" }; + } + webRequest.downloadHandler = new DownloadHandlerBuffer(); - if (json != null && json.Length > 0) - { - webRequest.uploadHandler = new UploadHandlerRaw(System.Text.Encoding.UTF8.GetBytes(json)) { contentType = "application/json" }; - } - webRequest.downloadHandler = new DownloadHandlerBuffer(); + yield return webRequest.SendWebRequest(); - yield return webRequest.SendWebRequest(); + //check for redirect + if (webRequest.responseCode >= 300 && webRequest.responseCode < 400) + { + string redirectUrl = webRequest.GetResponseHeader("Location"); + Multiplayer.LogWarning($"Lobby Server redirected, check address is up to date: '{redirectUrl}'"); - //check for redirect - if (webRequest.responseCode >= 300 && webRequest.responseCode < 400) + if (redirectUrl != null && redirectUrl.StartsWith("https://") && redirectUrl.Replace("https://", "http://") == uri) { - 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); - } + yield return SendWebRequest(redirectUrl, json, onSuccess, onError, ++depth); + } + } + else + { + if (webRequest.isNetworkError || webRequest.isHttpError) + { + Multiplayer.LogError($"Error: {webRequest.error}\r\n{webRequest.downloadHandler.text}"); + onError?.Invoke(webRequest); } else { - 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); - } + Multiplayer.Log($"Received: {webRequest.downloadHandler.text}"); + onSuccess?.Invoke(webRequest); } } } @@ -297,36 +294,34 @@ private IEnumerator SendWebRequestGET(string uri, Action onSucc yield break; } - using (UnityWebRequest webRequest = UnityWebRequest.Get(uri)) - { - webRequest.redirectLimit = 0; - webRequest.downloadHandler = new DownloadHandlerBuffer(); + using UnityWebRequest webRequest = UnityWebRequest.Get(uri); + webRequest.redirectLimit = 0; + webRequest.downloadHandler = new DownloadHandlerBuffer(); - yield return webRequest.SendWebRequest(); + yield return webRequest.SendWebRequest(); - //check for redirect - if (webRequest.responseCode >= 300 && webRequest.responseCode < 400) - { - string redirectUrl = webRequest.GetResponseHeader("Location"); - Multiplayer.LogWarning($"Lobby Server redirected, check address is up to date: '{redirectUrl}'"); + //check for redirect + if (webRequest.responseCode >= 300 && webRequest.responseCode < 400) + { + 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 SendWebRequestGET(redirectUrl, onSuccess, onError, ++depth); - } + if (redirectUrl != null && redirectUrl.StartsWith("https://") && redirectUrl.Replace("https://", "http://") == uri) + { + yield return SendWebRequestGET(redirectUrl, onSuccess, onError, ++depth); + } + } + else + { + if (webRequest.isNetworkError || webRequest.isHttpError) + { + Multiplayer.LogError($"Error: {webRequest.error}\r\n{webRequest.downloadHandler.text}"); + onError?.Invoke(webRequest); } else { - 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); - } + 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 fa5ff4a..544831f 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -16,7 +16,6 @@ using Multiplayer.Components.Networking.World; using Multiplayer.Components.Networking.Jobs; using Multiplayer.Networking.Data; -using Multiplayer.Networking.Managers.Server; using Multiplayer.Networking.Packets.Clientbound; using Multiplayer.Networking.Packets.Clientbound.Jobs; using Multiplayer.Networking.Packets.Clientbound.SaveGame; @@ -33,7 +32,7 @@ using Multiplayer.Networking.Packets.Unconnected; using System.Text; -namespace Multiplayer.Networking.Listeners; +namespace Multiplayer.Networking.Managers.Server; public class NetworkServer : NetworkManager { @@ -41,8 +40,8 @@ public class NetworkServer : NetworkManager protected override string LogPrefix => "[Server]"; private readonly Queue joinQueue = new(); - private readonly Dictionary serverPlayers = new(); - private readonly Dictionary netPeers = new(); + private readonly Dictionary serverPlayers = []; + private readonly Dictionary netPeers = []; private LobbyServerManager lobbyServerManager; public bool isSinglePlayer; @@ -52,15 +51,15 @@ public class NetworkServer : NetworkManager public IReadOnlyCollection ServerPlayers => serverPlayers.Values; public int PlayerCount => netManager.ConnectedPeersCount; - private static NetPeer selfPeer => NetworkLifecycle.Instance.Client?.selfPeer; - public static byte SelfId => (byte)selfPeer.Id; + private static NetPeer SelfPeer => NetworkLifecycle.Instance.Client?.SelfPeer; + public static byte SelfId => (byte)SelfPeer.Id; private readonly ModInfo[] serverMods; public readonly IDifficulty Difficulty; private bool IsLoaded; //we don't care if the client doesn't have these mods - public static string[] modWhiteList = { "RuntimeUnityEditor", "BookletOrganizer" }; + public static string[] modWhiteList = ["RuntimeUnityEditor", "BookletOrganizer"]; public NetworkServer(IDifficulty difficulty, Settings settings, bool isSinglePlayer, LobbyServerData serverData) : base(settings) { @@ -84,12 +83,12 @@ public bool Start(int port) WorldStreamingInit.LoadingFinished += OnLoaded; //Try to get our static IPv6 Address we will need this for IPv6 NAT punching to be reliable - if(IPAddress.TryParse(LobbyServerManager.GetStaticIPv6Address(), out IPAddress ipv6Address)) + if (IPAddress.TryParse(LobbyServerManager.GetStaticIPv6Address(), out IPAddress ipv6Address)) { - Multiplayer.Log($"Starting server, will listen to IPv6: {ipv6Address.ToString()}"); + Multiplayer.Log($"Starting server, will listen to IPv6: {ipv6Address}"); //start the connection, IPv4 messages can come from anywhere, IPv6 messages need to specifically come from the static IPv6 //return netManager.Start(IPAddress.Any, ipv6Address,port); - return netManager.Start(IPAddress.Any, IPAddress.IPv6Any,port); + return netManager.Start(IPAddress.Any, IPAddress.IPv6Any, port); } //we're not running IPv6, start as normal @@ -101,7 +100,7 @@ public override void Stop() if (lobbyServerManager != null) { lobbyServerManager.RemoveFromLobbyServer(); - GameObject.Destroy(lobbyServerManager); + UnityEngine.Object.Destroy(lobbyServerManager); } base.Stop(); @@ -273,7 +272,7 @@ public void KickPlayer(NetPeer peer) } public void SendGameParams(GameParams gameParams) { - SendPacketToAll(ClientboundGameParamsPacket.FromGameParams(gameParams), DeliveryMethod.ReliableOrdered, selfPeer); + SendPacketToAll(ClientboundGameParamsPacket.FromGameParams(gameParams), DeliveryMethod.ReliableOrdered, SelfPeer); } public void SendSpawnTrainset(List set, bool autoCouple, bool sendToAll, NetPeer sendTo = null) @@ -281,7 +280,7 @@ public void SendSpawnTrainset(List set, bool autoCouple, bool sendToAl LogDebug(() => { - StringBuilder sb = new StringBuilder(); + StringBuilder sb = new(); sb.Append($"SendSpawnTrainSet() Sending trainset {set?.FirstOrDefault()?.GetNetId()} with {set?.Count} cars"); @@ -304,17 +303,18 @@ public void SendSpawnTrainset(List set, bool autoCouple, bool sendToAl SendPacket(sendTo, packet, DeliveryMethod.ReliableOrdered); } else - SendPacketToAll(packet, DeliveryMethod.ReliableOrdered, selfPeer); + SendPacketToAll(packet, DeliveryMethod.ReliableOrdered, SelfPeer); } public void SendSpawnTrainCar(NetworkedTrainCar networkedTrainCar) { - SendPacketToAll(ClientboundSpawnTrainCarPacket.FromTrainCar(networkedTrainCar), DeliveryMethod.ReliableOrdered, selfPeer); + SendPacketToAll(ClientboundSpawnTrainCarPacket.FromTrainCar(networkedTrainCar), DeliveryMethod.ReliableOrdered, SelfPeer); } public void SendDestroyTrainCar(ushort netId) { //ushort netID = trainCar.GetNetId(); + LogDebug(() => $"SendDestroyTrainCar({netId})"); if (netId == 0) { @@ -325,12 +325,12 @@ public void SendDestroyTrainCar(ushort netId) SendPacketToAll(new ClientboundDestroyTrainCarPacket { NetId = netId, - }, DeliveryMethod.ReliableOrdered, selfPeer); + }, DeliveryMethod.ReliableOrdered, SelfPeer); } public void SendTrainsetPhysicsUpdate(ClientboundTrainsetPhysicsPacket packet, bool reliable) { - SendPacketToAll(packet, reliable ? DeliveryMethod.ReliableOrdered : DeliveryMethod.Unreliable, selfPeer); + SendPacketToAll(packet, reliable ? DeliveryMethod.ReliableOrdered : DeliveryMethod.Unreliable, SelfPeer); } public void SendBrakePressures(ushort netId, float mainReservoirPressure, float independentPipePressure, float brakePipePressure, float brakeCylinderPressure) @@ -342,7 +342,7 @@ public void SendBrakePressures(ushort netId, float mainReservoirPressure, float IndependentPipePressure = independentPipePressure, BrakePipePressure = brakePipePressure, BrakeCylinderPressure = brakeCylinderPressure - }, DeliveryMethod.ReliableOrdered, selfPeer); + }, DeliveryMethod.ReliableOrdered, SelfPeer); //Multiplayer.LogDebug(()=> $"Sending Brake Pressures netId {netId}: {mainReservoirPressure}, {independentPipePressure}, {brakePipePressure}, {brakeCylinderPressure}"); } @@ -354,7 +354,7 @@ public void SendFireboxState(ushort netId, float fireboxContents, bool fireboxOn NetId = netId, Contents = fireboxContents, IsOn = fireboxOn - }, DeliveryMethod.ReliableOrdered, selfPeer); + }, DeliveryMethod.ReliableOrdered, SelfPeer); Multiplayer.LogDebug(() => $"Sending Firebox States netId {netId}: {fireboxContents}, {fireboxOn}"); } @@ -371,7 +371,7 @@ public void SendCargoState(TrainCar trainCar, ushort netId, bool isLoading, byte CargoAmount = logicCar.LoadedCargoAmount, CargoModelIndex = cargoModelIndex, WarehouseMachineId = logicCar.CargoOriginWarehouse?.ID - }, DeliveryMethod.ReliableOrdered, selfPeer); + }, DeliveryMethod.ReliableOrdered, SelfPeer); } public void SendCarHealthUpdate(ushort netId, float health) @@ -380,7 +380,7 @@ public void SendCarHealthUpdate(ushort netId, float health) { NetId = netId, Health = health - }, DeliveryMethod.ReliableOrdered, selfPeer); + }, DeliveryMethod.ReliableOrdered, SelfPeer); } public void SendRerailTrainCar(ushort netId, ushort rerailTrack, Vector3 worldPos, Vector3 forward) @@ -391,7 +391,7 @@ public void SendRerailTrainCar(ushort netId, ushort rerailTrack, Vector3 worldPo TrackId = rerailTrack, Position = worldPos, Forward = forward - }, DeliveryMethod.ReliableOrdered, selfPeer); + }, DeliveryMethod.ReliableOrdered, SelfPeer); } public void SendWindowsBroken(ushort netId, Vector3 forceDirection) @@ -400,7 +400,7 @@ public void SendWindowsBroken(ushort netId, Vector3 forceDirection) { NetId = netId, ForceDirection = forceDirection - }, DeliveryMethod.ReliableUnordered, selfPeer); + }, DeliveryMethod.ReliableUnordered, SelfPeer); } public void SendWindowsRepaired(ushort netId) @@ -408,7 +408,7 @@ public void SendWindowsRepaired(ushort netId) SendPacketToAll(new ClientboundWindowsRepairedPacket { NetId = netId - }, DeliveryMethod.ReliableUnordered, selfPeer); + }, DeliveryMethod.ReliableUnordered, SelfPeer); } public void SendMoney(float amount) @@ -416,7 +416,7 @@ public void SendMoney(float amount) SendPacketToAll(new ClientboundMoneyPacket { Amount = amount - }, DeliveryMethod.ReliableUnordered, selfPeer); + }, DeliveryMethod.ReliableUnordered, SelfPeer); } public void SendLicense(string id, bool isJobLicense) @@ -425,7 +425,7 @@ public void SendLicense(string id, bool isJobLicense) { Id = id, IsJobLicense = isJobLicense - }, DeliveryMethod.ReliableUnordered, selfPeer); + }, DeliveryMethod.ReliableUnordered, SelfPeer); } public void SendGarage(string id) @@ -433,7 +433,7 @@ public void SendGarage(string id) SendPacketToAll(new ClientboundGarageUnlockPacket { Id = id - }, DeliveryMethod.ReliableUnordered, selfPeer); + }, DeliveryMethod.ReliableUnordered, SelfPeer); } public void SendDebtStatus(bool hasDebt) @@ -441,26 +441,26 @@ public void SendDebtStatus(bool hasDebt) SendPacketToAll(new ClientboundDebtStatusPacket { HasDebt = hasDebt - }, DeliveryMethod.ReliableUnordered, selfPeer); + }, DeliveryMethod.ReliableUnordered, SelfPeer); } - public void SendJobsCreatePacket(NetworkedStationController networkedStation, NetworkedJob[] jobs, DeliveryMethod method = DeliveryMethod.ReliableSequenced ) + public void SendJobsCreatePacket(NetworkedStationController networkedStation, NetworkedJob[] jobs, DeliveryMethod method = DeliveryMethod.ReliableSequenced) { Multiplayer.Log($"Sending JobsCreatePacket for stationNetId {networkedStation.NetId} with {jobs.Count()} jobs"); - SendPacketToAll(ClientboundJobsCreatePacket.FromNetworkedJobs(networkedStation, jobs), method, selfPeer); + SendPacketToAll(ClientboundJobsCreatePacket.FromNetworkedJobs(networkedStation, jobs), method, SelfPeer); } public void SendJobsUpdatePacket(ushort stationNetId, NetworkedJob[] jobs, NetPeer peer = null) { Multiplayer.Log($"Sending JobsUpdatePacket for stationNetId {stationNetId} with {jobs.Count()} jobs"); - SendPacketToAll(ClientboundJobsUpdatePacket.FromNetworkedJobs(stationNetId, jobs), DeliveryMethod.ReliableUnordered,selfPeer); + SendPacketToAll(ClientboundJobsUpdatePacket.FromNetworkedJobs(stationNetId, jobs), DeliveryMethod.ReliableUnordered, SelfPeer); } public void SendItemsChangePacket(List items, ServerPlayer player) { Multiplayer.Log($"Sending SendItemsChangePacket with {items.Count()} items to {player.Username}"); - if(TryGetNetPeer(player.Id, out NetPeer peer) && peer != selfPeer) + if (TryGetNetPeer(player.Id, out NetPeer peer) && peer != SelfPeer) { SendNetSerializablePacket(peer, new CommonItemChangePacket { Items = items }, DeliveryMethod.ReliableUnordered); @@ -488,7 +488,7 @@ public void SendChat(string message, NetPeer exclude = null) public void SendWhisper(string message, NetPeer recipient) { - if(message != null || recipient != null) + if (message != null || recipient != null) { NetworkLifecycle.Instance.Server.SendPacket(recipient, new CommonChatPacket { @@ -529,7 +529,7 @@ private void OnServerboundClientLoginPacket(ServerboundClientLoginPacket packet, return; } - Log($"Processing login packet for {packet.Username} ({guid.ToString()}){(Multiplayer.Settings.LogIps ? $" at {request.RemoteEndPoint.Address}" : "")}"); + Log($"Processing login packet for {packet.Username} ({guid}){(Multiplayer.Settings.LogIps ? $" at {request.RemoteEndPoint.Address}" : "")}"); if (Multiplayer.Settings.Password != packet.Password) { @@ -657,7 +657,7 @@ private void OnServerboundClientReadyPacket(ServerboundClientReadyPacket packet, // Send junctions and turntables SendPacket(peer, new ClientboundRailwayStatePacket { - SelectedJunctionBranches = NetworkedJunction.IndexedJunctions.Select(j => (byte)j.Junction.selectedBranch).ToArray(), + SelectedJunctionBranches = NetworkedJunction.IndexedJunctions.Select(j => j.Junction.selectedBranch).ToArray(), TurntableRotations = NetworkedTurntable.IndexedTurntables.Select(j => j.TurntableRailTrack.currentYRotation).ToArray() }, DeliveryMethod.ReliableOrdered); @@ -678,9 +678,9 @@ private void OnServerboundClientReadyPacket(ServerboundClientReadyPacket packet, SendPacket(peer, new ClientBoundStationControllerLookupPacket(NetworkedStationController.GetAll().ToArray()), DeliveryMethod.ReliableOrdered); //send jobs - foreach(StationController station in StationController.allStations) + foreach (StationController station in StationController.allStations) { - if(NetworkedStationController.GetFromStationController(station, out NetworkedStationController netStation)) + if (NetworkedStationController.GetFromStationController(station, out NetworkedStationController netStation)) { NetworkedJob[] jobs = netStation.NetworkedJobs.ToArray(); for (int i = 0; i < jobs.Length; i++) @@ -826,9 +826,9 @@ private void OnServerboundAddCoalPacket(ServerboundAddCoalPacket packet, NetPeer //is player close enough to add coal? if ((player.WorldPosition - networkedTrainCar.transform.position).sqrMagnitude <= carLength * carLength) - networkedTrainCar.firebox?.fireboxCoalControlPort.ExternalValueUpdate(packet.CoalMassDelta); + networkedTrainCar.firebox?.fireboxCoalControlPort.ExternalValueUpdate(packet.CoalMassDelta); } - + } private void OnServerboundFireboxIgnitePacket(ServerboundFireboxIgnitePacket packet, NetPeer peer) @@ -929,7 +929,7 @@ private void OnServerboundTrainRerailRequestPacket(ServerboundTrainRerailRequest Vector3 position = packet.Position + WorldMover.currentMove; //Check if player is a Newbie (currently shared with all players) - float cost = (TutorialHelper.InRestrictedMode || (rerailController != null && rerailController.isPlayerNewbie)) ? 0f : + float cost = TutorialHelper.InRestrictedMode || rerailController != null && rerailController.isPlayerNewbie ? 0f : RerailController.CalculatePrice((networkedTrainCar.transform.position - position).magnitude, trainCar.carType, Globals.G.GameParams.RerailMaxPrice); if (!Inventory.Instance.RemoveMoney(cost)) @@ -940,7 +940,7 @@ private void OnServerboundTrainRerailRequestPacket(ServerboundTrainRerailRequest trainCar.Rerail(networkedRailTrack.RailTrack, position, packet.Forward); } - + private void OnServerboundLicensePurchaseRequestPacket(ServerboundLicensePurchaseRequestPacket packet, NetPeer peer) { if (!TryGetServerPlayer(peer, out ServerPlayer player)) @@ -1003,7 +1003,7 @@ private void OnServerboundJobValidateRequestPacket(ServerboundJobValidateRequest return; } - LogDebug(() => $"OnServerboundJobValidateRequestPacket() Validating {packet.JobNetId}, Validation Type: {packet.validationType} overview: {networkedJob.JobOverview!=null}, booklet: {networkedJob.JobBooklet !=null}"); + LogDebug(() => $"OnServerboundJobValidateRequestPacket() Validating {packet.JobNetId}, Validation Type: {packet.validationType} overview: {networkedJob.JobOverview != null}, booklet: {networkedJob.JobBooklet != null}"); switch (packet.validationType) { case ValidationType.JobOverview: @@ -1020,10 +1020,10 @@ private void OnServerboundJobValidateRequestPacket(ServerboundJobValidateRequest private void OnCommonChatPacket(CommonChatPacket packet, NetPeer peer) { - ChatManager.ProcessMessage(packet.message,peer); + ChatManager.ProcessMessage(packet.message, peer); } #endregion - + #region Unconnected Packet Handling private void OnUnconnectedPingPacket(UnconnectedPingPacket packet, IPEndPoint endPoint) { @@ -1066,7 +1066,7 @@ private void OnCommonItemChangePacket(CommonItemChangePacket packet, NetPeer pee //} //); - + //NetworkedItemManager.Instance.ReceiveSnapshots(packet.Items, player); } #endregion diff --git a/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobsCreatePacket.cs b/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobsCreatePacket.cs index 4b5d536..bd51543 100644 --- a/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobsCreatePacket.cs +++ b/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobsCreatePacket.cs @@ -11,7 +11,7 @@ public class ClientboundJobsCreatePacket public static ClientboundJobsCreatePacket FromNetworkedJobs(NetworkedStationController netStation, NetworkedJob[] jobs) { - List jobData = new List(); + List jobData = []; foreach (var job in jobs) { JobData jd = JobData.FromJob(netStation, job); diff --git a/Multiplayer/Patches/Train/BogiePatch.cs b/Multiplayer/Patches/Train/BogiePatch.cs index 71b72ae..0b407fa 100644 --- a/Multiplayer/Patches/Train/BogiePatch.cs +++ b/Multiplayer/Patches/Train/BogiePatch.cs @@ -3,7 +3,7 @@ using Multiplayer.Components.Networking.Train; using Multiplayer.Utils; -namespace Multiplayer.Patches.World; +namespace Multiplayer.Patches.Train; [HarmonyPatch(typeof(Bogie), nameof(Bogie.SetupPhysics))] public static class Bogie_SetupPhysics_Patch diff --git a/Multiplayer/Patches/Train/CarSpawnerPatch.cs b/Multiplayer/Patches/Train/CarSpawnerPatch.cs index de5bad2..3b00185 100644 --- a/Multiplayer/Patches/Train/CarSpawnerPatch.cs +++ b/Multiplayer/Patches/Train/CarSpawnerPatch.cs @@ -4,7 +4,7 @@ using Multiplayer.Utils; using System.Collections.Generic; -namespace Multiplayer.Patches.World; +namespace Multiplayer.Patches.Train; [HarmonyPatch(typeof(CarSpawner))] public static class CarSpawner_Patch @@ -15,9 +15,12 @@ private static void PrepareTrainCarForDeleting(TrainCar trainCar) { if (UnloadWatcher.isUnloading) return; - if (!trainCar.TryNetworked(out NetworkedTrainCar networkedTrainCar)) + + if (trainCar == null || !trainCar.TryNetworked(out NetworkedTrainCar networkedTrainCar)) return; + networkedTrainCar.IsDestroying = true; + NetworkLifecycle.Instance.Server?.SendDestroyTrainCar(networkedTrainCar.NetId); } @@ -36,7 +39,7 @@ private static void SpawnCars(List __result) //Coupling is delayed by AutoCouple(), so a true trainset for the entire consist doesn't exist yet Multiplayer.LogDebug(() => $"SpawnCars() {__result?.Count} cars spawned, adding to queue"); - NetworkLifecycle.Instance.Server.SendSpawnTrainset(__result, true,true); + NetworkLifecycle.Instance.Server.SendSpawnTrainset(__result, true, true); } } diff --git a/Multiplayer/Patches/Train/CouplerChainInteractionPatch.cs b/Multiplayer/Patches/Train/CouplerChainInteractionPatch.cs index 5a4d50e..2ee1d5a 100644 --- a/Multiplayer/Patches/Train/CouplerChainInteractionPatch.cs +++ b/Multiplayer/Patches/Train/CouplerChainInteractionPatch.cs @@ -1,11 +1,8 @@ -using DV.CabControls; using HarmonyLib; using Multiplayer.Components.Networking; using Multiplayer.Networking.Data.Train; -using UnityEngine; - -namespace Multiplayer.Patches.World; +namespace Multiplayer.Patches.Train; [HarmonyPatch(typeof(ChainCouplerInteraction))] public static class ChainCouplerInteractionPatch @@ -14,7 +11,7 @@ public static class ChainCouplerInteractionPatch [HarmonyPostfix] private static void OnScrewButtonUsed(ChainCouplerInteraction __instance) { - + Multiplayer.LogDebug(() => $"OnScrewButtonUsed({__instance?.couplerAdapter?.coupler?.train?.ID}) state: {__instance.state}"); CouplerInteractionType flag = default; diff --git a/Multiplayer/Patches/Train/HoseAndCockPatch.cs b/Multiplayer/Patches/Train/HoseAndCockPatch.cs index 6072e96..62d33fe 100644 --- a/Multiplayer/Patches/Train/HoseAndCockPatch.cs +++ b/Multiplayer/Patches/Train/HoseAndCockPatch.cs @@ -4,7 +4,7 @@ using Multiplayer.Components.Networking.Train; using Multiplayer.Utils; -namespace Multiplayer.Patches.World; +namespace Multiplayer.Patches.Train; [HarmonyPatch(typeof(HoseAndCock), nameof(HoseAndCock.SetCock))] public static class HoseAndCock_SetCock_Patch From 829aacb4e438966aff03cd1f5be2032402686ad1 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 4 Jan 2025 00:30:57 +1030 Subject: [PATCH 145/188] Fix for dictionaries caching deleted cards --- .../Networking/Train/NetworkedTrainCar.cs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs index 76160dc..646c39f 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs @@ -212,11 +212,15 @@ public void OnDisable() return; trainCarsToNetworkedTrainCars.Remove(TrainCar); - if (TrainCar.logicCar != null) - { - trainCarIdToNetworkedTrainCars.Remove(TrainCar.ID); - trainCarIdToTrainCars.Remove(TrainCar.ID); - } + + string id = ""; + if (TrainCar.logicCar == null) + id = trainCarIdToNetworkedTrainCars.FirstOrDefault(x => x.Value == this).Key; + else + id = TrainCar.ID; + + trainCarIdToNetworkedTrainCars.Remove(id); + trainCarIdToTrainCars.Remove(id); foreach (Coupler coupler in TrainCar.couplers) hoseToCoupler.Remove(coupler.hoseAndCock); From e5272c798155351193432e64dcf6d363e329ef95 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 4 Jan 2025 11:45:26 +1030 Subject: [PATCH 146/188] Intellisense Message fixes Implemented fixes for Intellisense messages / warnings --- .../World/NetworkedStationController.cs | 22 +++++++++---------- .../SaveGame/NetworkedSaveGameManager.cs | 8 +++---- .../Networking/Data/TaskNetworkData.cs | 2 +- .../Managers/Client/NetworkClient.cs | 1 + 4 files changed, 17 insertions(+), 16 deletions(-) diff --git a/Multiplayer/Components/Networking/World/NetworkedStationController.cs b/Multiplayer/Components/Networking/World/NetworkedStationController.cs index 6ca7547..43d660f 100644 --- a/Multiplayer/Components/Networking/World/NetworkedStationController.cs +++ b/Multiplayer/Components/Networking/World/NetworkedStationController.cs @@ -19,12 +19,12 @@ namespace Multiplayer.Components.Networking.World; public class NetworkedStationController : IdMonoBehaviour { #region Lookup Cache - private static readonly Dictionary stationControllerToNetworkedStationController = new(); - private static readonly Dictionary stationIdToNetworkedStationController = new(); - private static readonly Dictionary stationIdToStationController = new(); - private static readonly Dictionary stationToNetworkedStationController = new(); - private static readonly Dictionary jobValidatorToNetworkedStation = new(); - private static readonly List jobValidators = new List(); + private static readonly Dictionary stationControllerToNetworkedStationController = []; + private static readonly Dictionary stationIdToNetworkedStationController = []; + private static readonly Dictionary stationIdToStationController = []; + private static readonly Dictionary stationToNetworkedStationController = []; + private static readonly Dictionary jobValidatorToNetworkedStation = []; + private static readonly List jobValidators = []; public static bool Get(ushort netId, out NetworkedStationController obj) { @@ -35,7 +35,7 @@ public static bool Get(ushort netId, out NetworkedStationController obj) public static DictionaryGetAll() { - Dictionary result = new Dictionary(); + Dictionary result = []; foreach (var kvp in stationIdToNetworkedStationController ) { @@ -113,9 +113,9 @@ private static void RegisterJobValidator(JobValidator jobValidator, NetworkedSta public JobValidator JobValidator; - public HashSet NetworkedJobs { get; } = new HashSet(); - private List NewJobs = new List(); - private List DirtyJobs = new List(); + public HashSet NetworkedJobs { get; } = []; + private readonly List NewJobs = []; + private readonly List DirtyJobs = []; private List availableJobs; private List takenJobs; @@ -130,7 +130,7 @@ protected override void Awake() StartCoroutine(WaitForLogicStation()); } - private void Start() + protected void Start() { if (NetworkLifecycle.Instance.IsHost()) { diff --git a/Multiplayer/Components/SaveGame/NetworkedSaveGameManager.cs b/Multiplayer/Components/SaveGame/NetworkedSaveGameManager.cs index 974a991..5ee3a8f 100644 --- a/Multiplayer/Components/SaveGame/NetworkedSaveGameManager.cs +++ b/Multiplayer/Components/SaveGame/NetworkedSaveGameManager.cs @@ -6,7 +6,7 @@ using JetBrains.Annotations; using Multiplayer.Components.Networking; using Multiplayer.Networking.Data; -using Multiplayer.Networking.Listeners; +using Multiplayer.Networking.Managers.Server; using Newtonsoft.Json.Linq; namespace Multiplayer.Components.SaveGame; @@ -64,14 +64,14 @@ private static void Server_OnGarageUnlocked(GarageType_v2 garage) public void Server_UpdateInternalData(SaveGameData data) { - JObject root = data.GetJObject(ROOT_KEY) ?? new JObject(); - JObject players = root.GetJObject(PLAYERS_KEY) ?? new JObject(); + JObject root = data.GetJObject(ROOT_KEY) ?? []; + JObject players = root.GetJObject(PLAYERS_KEY) ?? []; foreach (ServerPlayer player in NetworkLifecycle.Instance.Server.ServerPlayers) { if (player.Id == NetworkServer.SelfId || !player.IsLoaded) continue; - JObject playerData = new(); + JObject playerData = []; playerData.SetVector3(SaveGameKeys.Player_position, player.AbsoluteWorldPosition); playerData.SetFloat(SaveGameKeys.Player_rotation, player.WorldRotationY); //store inventory see StorageSerializer.SaveStorage() diff --git a/Multiplayer/Networking/Data/TaskNetworkData.cs b/Multiplayer/Networking/Data/TaskNetworkData.cs index 123dc8f..1109f99 100644 --- a/Multiplayer/Networking/Data/TaskNetworkData.cs +++ b/Multiplayer/Networking/Data/TaskNetworkData.cs @@ -374,7 +374,7 @@ public override SequentialTasksData FromTask(Task task) public override Task ToTask() { - List tasks = new List(); + List tasks = []; foreach (var task in Tasks) { diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 81e5840..b05b55a 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -736,6 +736,7 @@ private void OnClientboundCargoStatePacket(ClientboundCargoStatePacket packet) cargoAmount = cargoAmount - logicCar.LoadedCargoAmount; if(cargoAmount > 0) + if (cargoAmount > 0) logicCar.LoadCargo(cargoAmount, (CargoType)packet.CargoType, warehouse); } else From deebcdcb44c4d151910ada64846ffe0812aebe57 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 4 Jan 2025 11:54:00 +1030 Subject: [PATCH 147/188] Update brake pressure synchronisation Updates for B99 compatibility --- .../Components/Networking/Train/NetworkedTrainCar.cs | 8 ++++++-- Multiplayer/Networking/Managers/Server/NetworkServer.cs | 2 +- .../Train/ClientboundBrakePressureUpdatePacket.cs | 1 - 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs index 646c39f..51edeab 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs @@ -407,7 +407,7 @@ private void Server_SendBrakePressures() return; mainResPressureDirty = false; - //B99 review need / mod NetworkLifecycle.Instance.Server.SendBrakePressures(NetId, brakeSystem.mainReservoirPressure, brakeSystem.independentPipePressure, brakeSystem.brakePipePressure, brakeSystem.brakeCylinderPressure); + NetworkLifecycle.Instance.Server.SendBrakePressures(NetId, brakeSystem.mainReservoirPressure, brakeSystem.brakePipePressure, brakeSystem.brakeCylinderPressure); } private void Server_SendFireBoxState() @@ -875,7 +875,7 @@ public void Client_ReceiveTrainPhysicsUpdate(in TrainsetMovementPart movementPar } } - public void Client_ReceiveBrakePressureUpdate(float mainReservoirPressure, float independentPipePressure, float brakePipePressure, float brakeCylinderPressure) + public void Client_ReceiveBrakePressureUpdate(float mainReservoirPressure, float brakePipePressure, float brakeCylinderPressure) { if (brakeSystem == null) return; @@ -887,9 +887,13 @@ public void Client_ReceiveBrakePressureUpdate(float mainReservoirPressure, float //B99 review need / mod brakeSystem.ForceTargetIndBrakeCylinderPressure(brakeCylinderPressure); brakeSystem.SetMainReservoirPressure(mainReservoirPressure); + //brakeSystem.SetBrakePipePressure(brakePipePressure); brakeSystem.brakePipePressure = brakePipePressure; + brakeSystem.brakeset.pipePressure = brakePipePressure; + brakeSystem.brakeCylinderPressure = brakeCylinderPressure; } + private void Client_OnAddCoal(float coalMassDelta) { if (NetworkLifecycle.Instance.IsProcessingPacket) diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index 544831f..6780858 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -333,7 +333,7 @@ public void SendTrainsetPhysicsUpdate(ClientboundTrainsetPhysicsPacket packet, b SendPacketToAll(packet, reliable ? DeliveryMethod.ReliableOrdered : DeliveryMethod.Unreliable, SelfPeer); } - public void SendBrakePressures(ushort netId, float mainReservoirPressure, float independentPipePressure, float brakePipePressure, float brakeCylinderPressure) + public void SendBrakePressures(ushort netId, float mainReservoirPressure, float brakePipePressure, float brakeCylinderPressure) { SendPacketToAll(new ClientboundBrakePressureUpdatePacket { diff --git a/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundBrakePressureUpdatePacket.cs b/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundBrakePressureUpdatePacket.cs index 6fdee87..5516356 100644 --- a/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundBrakePressureUpdatePacket.cs +++ b/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundBrakePressureUpdatePacket.cs @@ -4,7 +4,6 @@ public class ClientboundBrakePressureUpdatePacket { public ushort NetId { get; set; } public float MainReservoirPressure { get; set; } - public float IndependentPipePressure { get; set; } public float BrakePipePressure { get; set; } public float BrakeCylinderPressure { get; set; } } From 1cb5945567cccecfde9f575898acd823c1440939 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 4 Jan 2025 11:57:28 +1030 Subject: [PATCH 148/188] Improve logging of NetworkedTrainCar Sim data --- .../Networking/Train/NetworkedTrainCar.cs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs index 51edeab..31041f6 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs @@ -327,15 +327,18 @@ public bool Server_ValidateClientSimFlowPacket(ServerPlayer player, CommonTrainP // Only allow control ports to be updated by clients if (hasSimFlow) foreach (string portId in packet.PortIds) - if (simulationFlow.TryGetPort(portId, out Port port) && port.valueType != PortValueType.CONTROL) + if (simulationFlow.TryGetPort(portId, out Port port)) { - NetworkLifecycle.Instance.Server.LogWarning($"Player {player.Username} tried to send a non-control port!"); - Common_DirtyPorts(packet.PortIds); - return false; + if (port.valueType != PortValueType.CONTROL) + { + NetworkLifecycle.Instance.Server.LogWarning($"Player {player.Username} tried to send a non-control port! ({portId} on [{TrainCar?.ID}, {NetId}])"); + Common_DirtyPorts(packet.PortIds); + return false; + } } else { - NetworkLifecycle.Instance.Server.LogWarning($"Player {player.Username} sent portId: {portId}, value type: {port.valueType}"); + NetworkLifecycle.Instance.Server.LogWarning($"Player {player.Username} sent portId: {portId}, value type: {port.valueType}, but the port was not found"); } // Only allow the player to update ports on the car they are in/near @@ -623,7 +626,7 @@ public void Common_UpdatePorts(CommonTrainPortsPacket packet) { Port port = simulationFlow.fullPortIdToPort[packet.PortIds[i]]; float value = packet.PortValues[i]; - float before = port.value; + // before = port.value; if (port.type == PortType.EXTERNAL_IN) port.ExternalValueUpdate(value); From 5dba90721f65e39e15b0bab80a8507f99c6db732 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 4 Jan 2025 12:01:42 +1030 Subject: [PATCH 149/188] Complete coupler sync update. Completion of coupler interaction improvements and start of work on hose interaction improvements --- .../Networking/Train/NetworkedTrainCar.cs | 58 ++++++++++++++++--- .../Data/Train/CouplerInteractionType.cs | 3 + .../Managers/Client/NetworkClient.cs | 55 ++++++++++++------ .../Patches/Train/CouplerInterfacerPatch.cs | 3 +- .../Items/RemoteControllerModulePatch.cs | 12 +++- 5 files changed, 102 insertions(+), 29 deletions(-) diff --git a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs index 31041f6..71e8c47 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs @@ -653,8 +653,9 @@ public void Common_UpdateFuses(CommonTrainFusesPacket packet) public void Common_ReceiveCouplerInteraction(CommonCouplerInteractionPacket packet) { - - Coupler coupler = packet.IsFrontCoupler ? TrainCar.frontCoupler : TrainCar.rearCoupler; + Coupler coupler = packet.IsFrontCoupler ? TrainCar?.frontCoupler : TrainCar?.rearCoupler; + TrainCar otherCar = null; + Coupler otherCoupler = null; if (coupler == null) { @@ -664,16 +665,20 @@ public void Common_ReceiveCouplerInteraction(CommonCouplerInteractionPacket pack CouplerInteractionType flags = (CouplerInteractionType)packet.Flags; + if (packet.OtherNetId != 0) + { + if (GetTrainCar(packet.OtherNetId, out otherCar)) + otherCoupler = packet.IsFrontOtherCoupler ? otherCar?.frontCoupler : otherCar?.rearCoupler; + } + Multiplayer.LogDebug(() => $"Common_ReceiveCouplerInteraction() [{TrainCar?.ID}, {NetId}], coupler is front: {packet.IsFrontCoupler}, flags: {flags}, otherCouplerNetId: {packet.OtherNetId}"); if (flags.HasFlag(CouplerInteractionType.CouplerCouple) && packet.OtherNetId != 0) { Multiplayer.LogDebug(() => $"1 Common_ReceiveCouplerInteraction() [{TrainCar?.ID}, {NetId}], coupler is front: {packet.IsFrontCoupler}, flags: {flags} "); - if (GetTrainCar(packet.OtherNetId, out TrainCar otherCar)) + if (otherCar != null) { Multiplayer.LogDebug(() => $"2 Common_ReceiveCouplerInteraction() [{TrainCar?.ID}, {NetId}], coupler is front: {packet.IsFrontCoupler}, flags: {flags}"); - Coupler otherCoupler = packet.IsFrontOtherCoupler ? otherCar.frontCoupler : otherCar.rearCoupler; - StartCoroutine(LooseAttachCoupler(coupler, otherCoupler)); } } @@ -722,6 +727,43 @@ public void Common_ReceiveCouplerInteraction(CommonCouplerInteractionPacket pack coupler.ChainScript.fsm.Fire(ChainCouplerInteraction.Trigger.Screw_Used); } } + + if (flags.HasFlag(CouplerInteractionType.CoupleViaUI)) + { + Multiplayer.LogDebug(() => $"10 Common_ReceiveCouplerInteraction() [{TrainCar?.ID}, {NetId}], coupler is front: {packet.IsFrontCoupler}, flags: {flags}, other coupler: {otherCoupler != null}"); + if(otherCoupler != null) + { + Multiplayer.LogDebug(() => $"10A Common_ReceiveCouplerInteraction() [{TrainCar?.ID}, {NetId}], coupler state: {coupler.state}, other coupler state: {otherCoupler.state}, coupler coupledTo: {coupler?.coupledTo?.train?.ID}, other coupledTo: {otherCoupler?.coupledTo?.train?.ID}"); + var car = coupler.CoupleTo(otherCoupler, true); + Multiplayer.LogDebug(() => $"10B Common_ReceiveCouplerInteraction() [{TrainCar?.ID}, {NetId}], result: {car != null}"); + //todo: rework hose and MU interactions + } + } + + if (flags.HasFlag(CouplerInteractionType.UncoupleViaUI)) + { + Multiplayer.LogDebug(() => $"11 Common_ReceiveCouplerInteraction() [{TrainCar?.ID}, {NetId}], coupler is front: {packet.IsFrontCoupler}, flags: {flags}"); + CouplerLogic.Uncouple(coupler); + //todo: rework hose and MU interactions + } + + if (flags.HasFlag(CouplerInteractionType.CoupleViaRemote)) + { + Multiplayer.LogDebug(() => $"12 Common_ReceiveCouplerInteraction() [{TrainCar?.ID}, {NetId}], coupler is front: {packet.IsFrontCoupler}, flags: {flags}, other coupler: {otherCoupler != null}"); + + if (TryGetComponent(out var couplingHandler)) + couplingHandler.Couple(); + } + + if (flags.HasFlag(CouplerInteractionType.UncoupleViaRemote)) + { + Multiplayer.LogDebug(() => $"13 Common_ReceiveCouplerInteraction() [{TrainCar?.ID}, {NetId}], coupler is front: {packet.IsFrontCoupler}, flags: {flags}"); + if (coupler != null) + { + coupler.Uncouple(true, false, false, false); + MultipleUnitModule.DisconnectCablesIfMultipleUnitSupported(coupler.train, coupler.isFrontCoupler, !coupler.isFrontCoupler); + } + } } private IEnumerator LooseAttachCoupler(Coupler coupler, Coupler otherCoupler) @@ -745,7 +787,7 @@ private IEnumerator LooseAttachCoupler(Coupler coupler, Coupler otherCoupler) //allow the follower and IK solver to update coupler.ChainScript.Update_Being_Dragged(); - //we need to allow the IK solver to calculate the chain ring anchor's position, over a number of iterations + //we need to allow the IK solver to calculate the chain ring anchor's position over a number of iterations int x = 0; float distance = float.MaxValue; //game checks for Vector3.Distance(this.chainRingAnchor.position, this.closestAttachPoint.transform.position) < attachDistanceThreshold; @@ -776,7 +818,7 @@ private IEnumerator ParkCoupler(Coupler coupler) //allow the follower and IK solver to update coupler.ChainScript.Update_Being_Dragged(); - //we need to allow the IK solver to calculate the chain ring anchor's position, over a number of iterations + //we need to allow the IK solver to calculate the chain ring anchor's position over a number of iterations int x = 0; float distance = float.MaxValue; //game checks for Vector3.Distance(this.chainRingAnchor.position, this.parkedAnchor.position) < parkDistanceThreshold; @@ -807,7 +849,7 @@ private IEnumerator DangleCoupler(Coupler coupler) //allow the follower and IK solver to update coupler.ChainScript.Update_Being_Dragged(); - //we need to allow the IK solver to calculate the chain ring anchor's position, over a number of iterations + //we need to allow the IK solver to calculate the chain ring anchor's position over a number of iterations int x = 0; float distance = float.MinValue; //game checks for Vector3.Distance(this.chainRingAnchor.position, this.parkedAnchor.position) < parkDistanceThreshold; diff --git a/Multiplayer/Networking/Data/Train/CouplerInteractionType.cs b/Multiplayer/Networking/Data/Train/CouplerInteractionType.cs index fe1385a..0944e72 100644 --- a/Multiplayer/Networking/Data/Train/CouplerInteractionType.cs +++ b/Multiplayer/Networking/Data/Train/CouplerInteractionType.cs @@ -21,4 +21,7 @@ public enum CouplerInteractionType : ushort CoupleViaUI = 512, UncoupleViaUI = 1024, + + CoupleViaRemote = 2048, + UncoupleViaRemote = 4096, } diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index b05b55a..5e97c78 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -551,22 +551,22 @@ private void OnCommonCouplerInteractionPacket(CommonCouplerInteractionPacket pac } private void OnCommonTrainCouplePacket(CommonTrainCouplePacket packet) { - // TrainCar trainCar = null; - // TrainCar otherTrainCar = null; + // TrainCar trainCar = null; + // TrainCar otherTrainCar = null; - // if (!NetworkedTrainCar.GetTrainCar(packet.NetId, out trainCar) || !NetworkedTrainCar.GetTrainCar(packet.OtherNetId, out otherTrainCar)) - // { - // LogDebug(() => $"OnCommonTrainCouplePacket() netId: {packet.NetId}, trainCar found?: {trainCar != null}, otherNetId: {packet.OtherNetId}, otherTrainCar found?: {otherTrainCar != null}"); - // return; - // } + // if (!NetworkedTrainCar.GetTrainCar(packet.NetId, out trainCar) || !NetworkedTrainCar.GetTrainCar(packet.OtherNetId, out otherTrainCar)) + // { + // LogDebug(() => $"OnCommonTrainCouplePacket() netId: {packet.NetId}, trainCar found?: {trainCar != null}, otherNetId: {packet.OtherNetId}, otherTrainCar found?: {otherTrainCar != null}"); + // return; + // } - // LogDebug(() => $"OnCommonTrainCouplePacket() netId: {packet.NetId}, trainCar: {trainCar.ID}, otherNetId: {packet.OtherNetId}, otherTrainCar: {otherTrainCar.ID}"); + // LogDebug(() => $"OnCommonTrainCouplePacket() netId: {packet.NetId}, trainCar: {trainCar.ID}, otherNetId: {packet.OtherNetId}, otherTrainCar: {otherTrainCar.ID}"); - // Coupler coupler = packet.IsFrontCoupler ? trainCar.frontCoupler : trainCar.rearCoupler; - // Coupler otherCoupler = packet.OtherCarIsFrontCoupler ? otherTrainCar.frontCoupler : otherTrainCar.rearCoupler; + // Coupler coupler = packet.IsFrontCoupler ? trainCar.frontCoupler : trainCar.rearCoupler; + // Coupler otherCoupler = packet.OtherCarIsFrontCoupler ? otherTrainCar.frontCoupler : otherTrainCar.rearCoupler; - // if (coupler.CoupleTo(otherCoupler, packet.PlayAudio, false/*B99 packet.ViaChainInteraction*/) == null) - // LogDebug(() => $"OnCommonTrainCouplePacket() netId: {packet.NetId}, trainCar: {trainCar.ID}, otherNetId: {packet.OtherNetId}, otherTrainCar: {otherTrainCar.ID} Failed to couple!"); + // if (coupler.CoupleTo(otherCoupler, packet.PlayAudio, false/*B99 packet.ViaChainInteraction*/) == null) + // LogDebug(() => $"OnCommonTrainCouplePacket() netId: {packet.NetId}, trainCar: {trainCar.ID}, otherNetId: {packet.OtherNetId}, otherTrainCar: {otherTrainCar.ID} Failed to couple!"); } private void OnCommonTrainUncouplePacket(CommonTrainUncouplePacket packet) @@ -598,16 +598,31 @@ private void OnCommonHoseConnectedPacket(CommonHoseConnectedPacket packet) Coupler coupler = packet.IsFront ? trainCar.frontCoupler : trainCar.rearCoupler; Coupler otherCoupler = packet.OtherIsFront ? otherTrainCar.frontCoupler : otherTrainCar.rearCoupler; - coupler.ConnectAirHose(otherCoupler, packet.PlayAudio); + if (coupler == null || otherCoupler == null || coupler.hoseAndCock.IsHoseConnected || otherCoupler.hoseAndCock.IsHoseConnected) + { + Coupler connectedTo = null; + Coupler otherConnectedTo = null; + + if(coupler?.hoseAndCock?.connectedTo != null) + NetworkedTrainCar.TryGetCoupler(coupler.hoseAndCock.connectedTo, out connectedTo); + if(otherCoupler?.hoseAndCock?.connectedTo != null) + NetworkedTrainCar.TryGetCoupler(otherCoupler.hoseAndCock.connectedTo, out otherConnectedTo); + + LogWarning($"OnCommonHoseConnectedPacket() netId: {packet.NetId}, trainCar: {trainCar?.ID}, isFront: {packet.IsFront}, IsHoseConnected: {coupler?.hoseAndCock?.IsHoseConnected}, connectedTo: {connectedTo?.train?.ID}," + + $" other trainCar: {otherTrainCar?.ID}, other isFront: {otherCoupler?.isFrontCoupler}, other IsHoseConnected: {otherCoupler?.hoseAndCock?.IsHoseConnected}, other connectedTo: {otherConnectedTo?.train?.ID}"); + } + else + { + coupler.ConnectAirHose(otherCoupler, packet.PlayAudio); + } } private void OnCommonHoseDisconnectedPacket(CommonHoseDisconnectedPacket packet) { - if (!NetworkedTrainCar.GetTrainCar(packet.NetId, out TrainCar trainCar)) - { - LogDebug(() => $"OnCommonHoseDisconnectedPacket() netId: {packet.NetId}, trainCar found?: {trainCar != null}"); + if (!NetworkedTrainCar.Get(packet.NetId, out NetworkedTrainCar netTrainCar) || netTrainCar.IsDestroying) return; - } + + TrainCar trainCar = netTrainCar.TrainCar; LogDebug(() => $"OnCommonHoseDisconnectedPacket() netId: {packet.NetId}, trainCar: {trainCar.ID}, isFront: {packet.IsFront}, playAudio: {packet.PlayAudio}"); @@ -1000,6 +1015,7 @@ public void SendCouplerInteraction(CouplerInteractionType flags, Coupler coupler { ushort couplerNetId = coupler?.train?.GetNetId() ?? 0; ushort otherCouplerNetId = otherCoupler?.train?.GetNetId() ?? 0; + bool otherCouplerIsFront = otherCoupler?.isFrontCoupler ?? false; if (couplerNetId == 0) { @@ -1008,11 +1024,14 @@ public void SendCouplerInteraction(CouplerInteractionType flags, Coupler coupler } Log($"Sending coupler interaction {flags} for {coupler?.train?.ID}"); + LogDebug(() => $"SendCouplerInteraction({flags}, {coupler?.train?.ID}, {otherCoupler?.train?.ID}) coupler isFront: {coupler?.isFrontCoupler}, otherCoupler isFront: {otherCoupler?.isFrontCoupler}"); + SendPacketToServer(new CommonCouplerInteractionPacket { NetId = couplerNetId, - OtherNetId = otherCouplerNetId, IsFrontCoupler = coupler.isFrontCoupler, + OtherNetId = otherCouplerNetId, + IsFrontOtherCoupler = otherCouplerIsFront, Flags = (ushort)flags, }, DeliveryMethod.ReliableUnordered); } diff --git a/Multiplayer/Patches/Train/CouplerInterfacerPatch.cs b/Multiplayer/Patches/Train/CouplerInterfacerPatch.cs index eca761b..8dc27c8 100644 --- a/Multiplayer/Patches/Train/CouplerInterfacerPatch.cs +++ b/Multiplayer/Patches/Train/CouplerInterfacerPatch.cs @@ -2,7 +2,6 @@ using HarmonyLib; using Multiplayer.Components.Networking; using Multiplayer.Networking.Data.Train; -using Newtonsoft.Json.Linq; using System; @@ -78,7 +77,7 @@ private static void SendCouple(CouplerInterfacer couplerInterfacer, float value, interaction = CouplerInteractionType.CoupleViaUI; otherCoupler = coupler.GetFirstCouplerInRange(); - Multiplayer.LogDebug(() => $"CouplerInterfacer.SendCouple({couplerInterfacer?.train?.ID}, {value}, {front}) coupler: {coupler?.train?.ID}, otherCoupler: {otherCoupler?.train?.ID}, action: {interaction}"); + Multiplayer.LogDebug(() => $"CouplerInterfacer.SendCouple({couplerInterfacer?.train?.ID}, {value}, {front}) coupler: {coupler?.train?.ID}, coupler is front: {coupler?.isFrontCoupler}, otherCoupler: {otherCoupler?.train?.ID}, otherCoupler is front: {otherCoupler?.isFrontCoupler}, action: {interaction}"); if (otherCoupler == null) return; } diff --git a/Multiplayer/Patches/World/Items/RemoteControllerModulePatch.cs b/Multiplayer/Patches/World/Items/RemoteControllerModulePatch.cs index 705192c..41a4375 100644 --- a/Multiplayer/Patches/World/Items/RemoteControllerModulePatch.cs +++ b/Multiplayer/Patches/World/Items/RemoteControllerModulePatch.cs @@ -20,12 +20,22 @@ static void RemoteControllerCouple(RemoteControllerModule __instance) } [HarmonyPatch(nameof(RemoteControllerModule.Uncouple))] - [HarmonyPostfix] + [HarmonyPrefix] static void Uncouple(RemoteControllerModule __instance, int selectedCoupler) { + Multiplayer.LogDebug(() => $"RemoteControllerModule.Uncouple({selectedCoupler})"); + TrainCar startCar = __instance.car; + + if (startCar == null) + { + Multiplayer.LogWarning($"Trying to Uncouple from Remote with no paired loco"); + return; + } + Coupler nthCouplerFrom = CouplerLogic.GetNthCouplerFrom((selectedCoupler > 0) ? startCar.frontCoupler : startCar.rearCoupler, Mathf.Abs(selectedCoupler) - 1); + Multiplayer.LogDebug(() => $"RemoteControllerModule.Uncouple({startCar?.ID}, {selectedCoupler}) nthCouplerFrom: [{nthCouplerFrom?.train?.ID}, {nthCouplerFrom?.train?.GetNetId()}]"); if (nthCouplerFrom != null) { NetworkLifecycle.Instance.Client.SendCouplerInteraction(CouplerInteractionType.UncoupleViaRemote, nthCouplerFrom); From 5f841d5427fc107f1a5bc94c7ace5996be7750d9 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 4 Jan 2025 12:05:29 +1030 Subject: [PATCH 150/188] General tidy up of code and logging --- .../World/NetworkedStationController.cs | 4 +--- .../Networking/Managers/Client/NetworkClient.cs | 4 +++- .../Networking/Managers/Server/NetworkServer.cs | 16 ++++++++-------- Multiplayer/Patches/Jobs/JobValidatorPatch.cs | 2 +- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Multiplayer/Components/Networking/World/NetworkedStationController.cs b/Multiplayer/Components/Networking/World/NetworkedStationController.cs index 43d660f..0534c6d 100644 --- a/Multiplayer/Components/Networking/World/NetworkedStationController.cs +++ b/Multiplayer/Components/Networking/World/NetworkedStationController.cs @@ -3,8 +3,6 @@ using System.Collections.Generic; using System.Linq; using DV.Booklets; -using DV.CabControls; -using DV.CabControls.Spec; using DV.Logic.Job; using DV.ServicePenalty; using DV.Utils; @@ -39,7 +37,7 @@ public static DictionaryGetAll() foreach (var kvp in stationIdToNetworkedStationController ) { - Multiplayer.Log($"GetAll() adding {kvp.Value.NetId}, {kvp.Key}"); + //Multiplayer.Log($"GetAll() adding {kvp.Value.NetId}, {kvp.Key}"); result.Add(kvp.Value.NetId, kvp.Key); } return result; diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 5e97c78..8972286 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -877,7 +877,7 @@ private void OnClientboundJobsCreatePacket(ClientboundJobsCreatePacket packet) return; } - networkedStationController.AddJobs(packet.Jobs); + Log($"Received {packet.Jobs.Length} jobs for station {networkedStationController.StationController.logicStation.ID}"); } @@ -894,6 +894,8 @@ private void OnClientboundJobsUpdatePacket(ClientboundJobsUpdatePacket packet) return; } + Log($"Received {packet.JobUpdates.Length} job updates for station {networkedStationController.StationController.logicStation.ID}"); + networkedStationController.UpdateJobs(packet.JobUpdates); } diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index 6780858..0ba7779 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -82,14 +82,14 @@ public bool Start(int port) { WorldStreamingInit.LoadingFinished += OnLoaded; + Multiplayer.Log($"Starting server..."); //Try to get our static IPv6 Address we will need this for IPv6 NAT punching to be reliable - if (IPAddress.TryParse(LobbyServerManager.GetStaticIPv6Address(), out IPAddress ipv6Address)) - { - Multiplayer.Log($"Starting server, will listen to IPv6: {ipv6Address}"); - //start the connection, IPv4 messages can come from anywhere, IPv6 messages need to specifically come from the static IPv6 - //return netManager.Start(IPAddress.Any, ipv6Address,port); - return netManager.Start(IPAddress.Any, IPAddress.IPv6Any, port); - } + //if (IPAddress.TryParse(LobbyServerManager.GetStaticIPv6Address(), out IPAddress ipv6Address)) + //{ + // //start the connection, IPv4 messages can come from anywhere, IPv6 messages need to specifically come from the static IPv6 + // //return netManager.Start(IPAddress.Any, ipv6Address,port); + // return netManager.Start(IPAddress.Any, IPAddress.IPv6Any, port); + //} //we're not running IPv6, start as normal return netManager.Start(port); @@ -548,7 +548,7 @@ private void OnServerboundClientLoginPacket(ServerboundClientLoginPacket packet, ClientboundServerDenyPacket denyPacket = new() { ReasonKey = Locale.DISCONN_REASON__GAME_VERSION_KEY, - ReasonArgs = new[] { BuildInfo.BUILD_VERSION_MAJOR.ToString(), packet.BuildMajorVersion.ToString() } + ReasonArgs = [BuildInfo.BUILD_VERSION_MAJOR.ToString(), packet.BuildMajorVersion.ToString()] }; request.Reject(WritePacket(denyPacket)); return; diff --git a/Multiplayer/Patches/Jobs/JobValidatorPatch.cs b/Multiplayer/Patches/Jobs/JobValidatorPatch.cs index 0b102c1..8fa9401 100644 --- a/Multiplayer/Patches/Jobs/JobValidatorPatch.cs +++ b/Multiplayer/Patches/Jobs/JobValidatorPatch.cs @@ -16,7 +16,7 @@ public static class JobValidator_Patch [HarmonyPostfix] private static void Start(JobValidator __instance) { - Multiplayer.Log($"JobValidator Awake!"); + //Multiplayer.Log($"JobValidator Awake!"); NetworkedStationController.QueueJobValidator(__instance); } From dd576f2bbab4b77f27494770fce10b7085239409 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 4 Jan 2025 12:11:19 +1030 Subject: [PATCH 151/188] Fix issues with job sync and overview spawns --- .../World/NetworkedStationController.cs | 202 +++++++++++------- Multiplayer/Networking/Data/JobData.cs | 13 ++ .../Networking/Data/TaskNetworkData.cs | 37 ++++ .../Managers/Client/NetworkClient.cs | 7 +- 4 files changed, 174 insertions(+), 85 deletions(-) diff --git a/Multiplayer/Components/Networking/World/NetworkedStationController.cs b/Multiplayer/Components/Networking/World/NetworkedStationController.cs index 0534c6d..86556f2 100644 --- a/Multiplayer/Components/Networking/World/NetworkedStationController.cs +++ b/Multiplayer/Components/Networking/World/NetworkedStationController.cs @@ -104,6 +104,7 @@ private static void RegisterJobValidator(JobValidator jobValidator, NetworkedSta } #endregion + const int MAX_FRAMES = 120; protected override bool IsIdServerAuthoritative => true; @@ -136,6 +137,26 @@ protected void Start() } } + protected void OnDisable() + { + + if (UnloadWatcher.isQuitting) + return; + + NetworkLifecycle.Instance.OnTick -= Server_OnTick; + + string stationId = StationController.logicStation.ID; + + stationControllerToNetworkedStationController.Remove(StationController); + stationIdToNetworkedStationController.Remove(stationId); + stationIdToStationController.Remove(stationId); + stationToNetworkedStationController.Remove(StationController.logicStation); + jobValidatorToNetworkedStation.Remove(JobValidator); + jobValidators.Remove(this.JobValidator); + + Destroy(this); + } + private IEnumerator WaitForLogicStation() { while (StationController.logicStation == null) @@ -207,43 +228,103 @@ private void Server_OnTick(uint tick) #region Client public void AddJobs(JobData[] jobs) { + foreach (JobData jobData in jobs) { - Job newJob = CreateJobFromJobData(jobData); - NetworkedJob networkedJob = CreateNetworkedJob(newJob, jobData.NetID); + //Cars may still be loading, we shouldn't spawn the job until they are ready + if (CheckCarsLoaded(jobData)) + { + Multiplayer.LogDebug(() => $"AddJobs() calling AddJob({jobData.ID})"); + AddJob(jobData); + } + else + { + Multiplayer.LogDebug(() => $"AddJobs() Delaying({jobData.ID})"); + StartCoroutine(DelayCreateJob(jobData)); + } + } + } - NetworkedJobs.Add(networkedJob); + private void AddJob(JobData jobData) + { + Job newJob = CreateJobFromJobData(jobData); - if (networkedJob.Job.State == DV.ThingTypes.JobState.Available) - { - StationController.logicStation.AddJobToStation(newJob); - StationController.processedNewJobs.Add(newJob); + NetworkedJob networkedJob = CreateNetworkedJob(newJob, jobData.NetID); - if (jobData.ItemNetID != 0) - { - GenerateOverview(networkedJob, jobData.ItemNetID, jobData.ItemPosition); - } - } + NetworkedJobs.Add(networkedJob); - StartCoroutine(UpdateCarPlates(newJob.tasks, newJob.ID)); + if (networkedJob.Job.State == DV.ThingTypes.JobState.Available) + { + StationController.logicStation.AddJobToStation(newJob); + StationController.processedNewJobs.Add(newJob); - Multiplayer.Log($"Added NetworkedJob {newJob.ID} to NetworkedStationController {StationController.logicStation.ID}"); + if (jobData.ItemNetID != 0) + { + GenerateOverview(networkedJob, jobData.ItemNetID, jobData.ItemPosition); + } } + + Multiplayer.LogDebug(() => $"AddJob({jobData.ID}) Starting plate update {newJob.ID} count: {jobData.GetCars().Count}"); + StartCoroutine(UpdateCarPlates(jobData.GetCars(), newJob.ID)); + + Multiplayer.Log($"Added NetworkedJob {newJob.ID} to NetworkedStationController {StationController.logicStation.ID}"); } private Job CreateJobFromJobData(JobData jobData) { + List tasks = jobData.Tasks.Select(taskData => taskData.ToTask()).ToList(); - StationsChainData chainData = new StationsChainData(jobData.ChainData.ChainOriginYardId, jobData.ChainData.ChainDestinationYardId); + StationsChainData chainData = new(jobData.ChainData.ChainOriginYardId, jobData.ChainData.ChainDestinationYardId); - Job newJob = new Job(tasks, jobData.JobType, jobData.TimeLimit, jobData.InitialWage, chainData, jobData.ID, jobData.RequiredLicenses); - newJob.startTime = jobData.StartTime; - newJob.finishTime = jobData.FinishTime; - newJob.State = jobData.State; + Job newJob = new(tasks, jobData.JobType, jobData.TimeLimit, jobData.InitialWage, chainData, jobData.ID, jobData.RequiredLicenses) + { + startTime = jobData.StartTime, + finishTime = jobData.FinishTime, + State = jobData.State + }; return newJob; } + private IEnumerator DelayCreateJob(JobData jobData) + { + int frameCounter = 0; + + Multiplayer.LogDebug(()=>$"DelayCreateJob({jobData.NetID}) job type: {jobData.JobType}"); + + yield return new WaitForEndOfFrame(); + + while (frameCounter < MAX_FRAMES) + { + if (CheckCarsLoaded(jobData)) + { + Multiplayer.LogDebug(() => $"DelayCreateJob({jobData.NetID}) job type: {jobData.JobType}. Successfully created cars!"); + AddJob(jobData); + yield break; + } + + frameCounter++; + yield return new WaitForEndOfFrame(); + } + + Multiplayer.LogWarning($"Timeout waiting for cars to load for job {jobData.NetID}"); + } + + private bool CheckCarsLoaded(JobData jobData) + { + //extract all cars from the job and verify they have been initialised + foreach (var carNetId in jobData.GetCars()) + { + if (!NetworkedTrainCar.Get(carNetId, out NetworkedTrainCar car) || !car.Client_Initialized) + { + //car not spawned or not yet initialised + return false; + } + } + + return true; + } + private NetworkedJob CreateNetworkedJob(Job job, ushort netId) { NetworkedJob networkedJob = new GameObject($"NetworkedJob {job.ID}").AddComponent(); @@ -291,7 +372,7 @@ private void UpdateJobOverview(NetworkedJob netJob, JobUpdateStruct job) } } - private void HandleJobStateChange(NetworkedJob netJob, JobUpdateStruct job) + private void HandleJobStateChange(NetworkedJob netJob, JobUpdateStruct job) { JobValidator validator = null; NetworkedItem netItem; @@ -365,7 +446,7 @@ private void HandleJobStateChange(NetworkedJob netJob, JobUpdateStruct job) Multiplayer.Log($"NetworkedStation.UpdateJobs() jobNetId: {job.JobNetID}, Playing sounds"); netJob.ValidatorResponseReceived = true; netJob.ValidationAccepted = true; - validator.jobValidatedSound.Play(validator.bookletPrinter.spawnAnchor.position, 1f, 1f, 0f, 1f, 500f, default(AudioSourceCurves), null, validator.transform, false, 0f, null); + validator.jobValidatedSound.Play(validator.bookletPrinter.spawnAnchor.position, 1f, 1f, 0f, 1f, 500f, default, null, validator.transform, false, 0f, null); validator.bookletPrinter.Print(false); } } @@ -392,59 +473,41 @@ public void RemoveJob(NetworkedJob job) GameObject.Destroy(job); } - public static IEnumerator UpdateCarPlates(List tasks, string jobId) + public static IEnumerator UpdateCarPlates(List carNetIds, string jobId) { - List cars = new List(); - UpdateCarPlatesRecursive(tasks, jobId, ref cars); - + Multiplayer.LogDebug(() => $"UpdateCarPlates({jobId}) carNetIds: {carNetIds?.Count}"); - if (cars == null) + if (carNetIds == null || string.IsNullOrEmpty(jobId)) yield break; - foreach (Car car in cars) + foreach (ushort carNetId in carNetIds) { - + int frameCounter = 0; TrainCar trainCar = null; - int loopCtr = 0; - while (!NetworkedTrainCar.GetTrainCarFromTrainId(car.ID, out trainCar)) + + while (frameCounter < MAX_FRAMES) { - loopCtr++; - if (loopCtr > 5000) + + if (NetworkedTrainCar.GetTrainCar(carNetId, out trainCar) && + trainCar != null && + trainCar.trainPlatesCtrl?.trainCarPlates != null && + trainCar.trainPlatesCtrl.trainCarPlates.Count > 0) { + Multiplayer.LogDebug(() => $"UpdateCarPlates({jobId}) car: {carNetId}, frameCount: {frameCounter}. Calling Update"); + trainCar.UpdateJobIdOnCarPlates(jobId); break; - } + } - yield return null; + Multiplayer.LogDebug(() => $"UpdateCarPlates({jobId}) car: {carNetId}, frameCount: {frameCounter}. Incrementing frames"); + frameCounter++; + yield return new WaitForEndOfFrame(); } - trainCar?.UpdateJobIdOnCarPlates(jobId); - } - } - private static void UpdateCarPlatesRecursive(List tasks, string jobId, ref List cars) - { - - foreach (Task task in tasks) - { - if (task is WarehouseTask) - cars = cars.Union(((WarehouseTask)task).cars).ToList(); - else if (task is TransportTask) - cars = cars.Union(((TransportTask)task).cars).ToList(); - else if (task is SequentialTasks) + if (frameCounter >= MAX_FRAMES) { - List seqTask = new(); - - for (LinkedListNode node = ((SequentialTasks)task).tasks.First; node != null; node = node.Next) - { - seqTask.Add(node.Value); - } - //drill down - UpdateCarPlatesRecursive(seqTask, jobId, ref cars); + Multiplayer.LogError($"Failed to update plates for car [{trainCar?.ID}, {carNetId}] (Job: {jobId}) after {frameCounter} frames"); } - else if (task is ParallelTasks) - UpdateCarPlatesRecursive(((ParallelTasks)task).tasks, jobId, ref cars); - else - throw new ArgumentException("NetworkedStation.UpdateCarPlatesRecursive() Unknown task type: " + task.GetType()); } } @@ -458,26 +521,5 @@ private void GenerateOverview(NetworkedJob networkedJob, ushort itemNetId, ItemP networkedJob.JobOverview = netItem; StationController.spawnedJobOverviews.Add(jobOverview); } - - private void OnDisable() - { - - if (UnloadWatcher.isQuitting) - return; - - NetworkLifecycle.Instance.OnTick -= Server_OnTick; - - string stationId = StationController.logicStation.ID; - - stationControllerToNetworkedStationController.Remove(StationController); - stationIdToNetworkedStationController.Remove(stationId); - stationIdToStationController.Remove(stationId); - stationToNetworkedStationController.Remove(StationController.logicStation); - jobValidatorToNetworkedStation.Remove(JobValidator); - jobValidators.Remove(this.JobValidator); - - Destroy(this); - - } #endregion } diff --git a/Multiplayer/Networking/Data/JobData.cs b/Multiplayer/Networking/Data/JobData.cs index 0d4cc6d..1a5b676 100644 --- a/Multiplayer/Networking/Data/JobData.cs +++ b/Multiplayer/Networking/Data/JobData.cs @@ -191,6 +191,19 @@ public static JobData Deserialize(NetDataReader reader) } } + public List GetCars() + { + List result = []; + + foreach (var task in Tasks) + { + var cars = task.GetCars(); + result.AddRange(cars); + } + + return result; + } + } public struct StationsChainNetworkData diff --git a/Multiplayer/Networking/Data/TaskNetworkData.cs b/Multiplayer/Networking/Data/TaskNetworkData.cs index 1109f99..37caf8c 100644 --- a/Multiplayer/Networking/Data/TaskNetworkData.cs +++ b/Multiplayer/Networking/Data/TaskNetworkData.cs @@ -21,6 +21,7 @@ public abstract class TaskNetworkData public abstract void Serialize(NetDataWriter writer); public abstract void Deserialize(NetDataReader reader); public abstract Task ToTask(); + public abstract List GetCars(); } public abstract class TaskNetworkData : TaskNetworkData where T : TaskNetworkData { @@ -196,6 +197,11 @@ public override Task ToTask() return newWareTask; } + + public override List GetCars() + { + return CarNetIDs.ToList(); + } } public class TransportTaskData : TaskNetworkData @@ -302,6 +308,11 @@ public override Task ToTask() TransportedCargoPerCar?.ToList() ); } + + public override List GetCars() + { + return CarNetIDs.ToList(); + } } public class SequentialTasksData : TaskNetworkData @@ -390,6 +401,19 @@ public override Task ToTask() return newSeqTask; } + + public override List GetCars() + { + List result = []; + + foreach (var task in Tasks) + { + var cars = task.GetCars(); + result.AddRange(cars); + } + + return result; + } } public class ParallelTasksData : TaskNetworkData @@ -434,4 +458,17 @@ public override Task ToTask() { return new ParallelTasks(Tasks.Select(t => t.ToTask()).ToList()); } + + public override List GetCars() + { + List result = []; + + foreach(var task in Tasks) + { + var cars = task.GetCars(); + result.AddRange(cars); + } + + return result; + } } diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 8972286..855c2dd 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -866,8 +866,6 @@ private void OnCommonChatPacket(CommonChatPacket packet) private void OnClientboundJobsCreatePacket(ClientboundJobsCreatePacket packet) { - Log($"OnClientboundJobsCreatePacket() for station {packet.StationNetId}, containing {packet.Jobs.Length}"); - if (NetworkLifecycle.Instance.IsHost()) return; @@ -879,12 +877,11 @@ private void OnClientboundJobsCreatePacket(ClientboundJobsCreatePacket packet) Log($"Received {packet.Jobs.Length} jobs for station {networkedStationController.StationController.logicStation.ID}"); + networkedStationController.AddJobs(packet.Jobs); } - + private void OnClientboundJobsUpdatePacket(ClientboundJobsUpdatePacket packet) { - Log($"OnClientboundJobsUpdatePacket() for station {packet.StationNetId}, containing {packet.JobUpdates.Length}"); - if (NetworkLifecycle.Instance.IsHost()) return; From 537b4a0a9263f13e21593112f71ee8e331d25b68 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 4 Jan 2025 12:12:19 +1030 Subject: [PATCH 152/188] Fix issues with cargo sync --- Multiplayer/Networking/Managers/Client/NetworkClient.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 855c2dd..7c0e4d8 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -748,9 +748,8 @@ private void OnClientboundCargoStatePacket(ClientboundCargoStatePacket packet) //We have the correct cargo, but not the right amount, calculate the delta if (logicCar.CurrentCargoTypeInCar == (CargoType)packet.CargoType) - cargoAmount = cargoAmount - logicCar.LoadedCargoAmount; + cargoAmount -= logicCar.LoadedCargoAmount; - if(cargoAmount > 0) if (cargoAmount > 0) logicCar.LoadCargo(cargoAmount, (CargoType)packet.CargoType, warehouse); } From 4bd5a169a6ba565e9fbb4ad58ede23d790a8dbd2 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 4 Jan 2025 12:13:30 +1030 Subject: [PATCH 153/188] Fix issues with deletion of cars on the client --- .../Managers/Client/NetworkClient.cs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 7c0e4d8..b4fe006 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -520,18 +520,26 @@ private void OnClientboundDestroyTrainCarPacket(ClientboundDestroyTrainCarPacket //Protect myself from getting deleted in race conditions if (PlayerManager.Car == networkedTrainCar.TrainCar) { - LogWarning($"Server attempted to delete car I'm on: {PlayerManager.Car.ID}, net ID: {packet.NetId}"); + LogWarning($"Server attempted to delete car I'm on: {PlayerManager.Car?.ID}, net ID: {packet?.NetId}"); PlayerManager.SetCar(null); } //Protect other players from getting deleted in race conditions - this should be a temporary fix, if another playe's game object is deleted we should just recreate it - NetworkedPlayer[] componentsInChildren = networkedTrainCar.GetComponentsInChildren(); - foreach (NetworkedPlayer networkedPlayer in componentsInChildren) + if(networkedTrainCar == null || networkedTrainCar.gameObject == null || networkedTrainCar.TrainCar == null) { - networkedPlayer.UpdateCar(0); + LogDebug(() => $"OnClientboundDestroyTrainCarPacket({packet?.NetId}) networkedTrainCar: {networkedTrainCar != null}, go: {networkedTrainCar?.gameObject != null}, trainCar: {networkedTrainCar?.TrainCar != null}"); } + else + { + NetworkedPlayer[] componentsInChildren = networkedTrainCar?.GetComponentsInChildren() ?? []; + + foreach (NetworkedPlayer networkedPlayer in componentsInChildren) + { + networkedPlayer.UpdateCar(0); + } - CarSpawner.Instance.DeleteCar(networkedTrainCar.TrainCar); + CarSpawner.Instance.DeleteCar(networkedTrainCar.TrainCar); + } } public void OnClientboundTrainPhysicsPacket(ClientboundTrainsetPhysicsPacket packet) From 4ece2c6a39098050f2489b0374a51b8b924f6455 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 4 Jan 2025 12:15:06 +1030 Subject: [PATCH 154/188] Temporary patch for restoration / demo locos --- .../Train/LocoRestorationControllerPatch.cs | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 Multiplayer/Patches/Train/LocoRestorationControllerPatch.cs diff --git a/Multiplayer/Patches/Train/LocoRestorationControllerPatch.cs b/Multiplayer/Patches/Train/LocoRestorationControllerPatch.cs new file mode 100644 index 0000000..f0243e0 --- /dev/null +++ b/Multiplayer/Patches/Train/LocoRestorationControllerPatch.cs @@ -0,0 +1,28 @@ +using DV.LocoRestoration; +using HarmonyLib; +using Multiplayer.Components.Networking; + +namespace Multiplayer.Patches.Train; +[HarmonyPatch(typeof(LocoRestorationController))] +public static class LocoRestorationControllerPatch +{ + [HarmonyPatch(nameof(LocoRestorationController.Start))] + [HarmonyPrefix] + private static bool Start(LocoRestorationController __instance) + { + if(NetworkLifecycle.Instance.IsHost()) + return true; + + //TrainCar loco = __instance.loco; + //TrainCar second = __instance.secondCar; + + Multiplayer.LogDebug(() => $"LocoRestorationController.Start()"); + + UnityEngine.Object.Destroy(__instance); + + //CarSpawner.Instance.DeleteCar(loco); + //if(second != null) + // CarSpawner.Instance.DeleteCar(second); + return false; + } +} From 4607b60bc0db97d0b4f35f5ac6c7be19d6ac93e3 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 4 Jan 2025 12:15:33 +1030 Subject: [PATCH 155/188] Begin work on loco restoration sync --- .../Networking/Train/PaintThemeLookup.cs | 86 +++++++++++++++++++ .../Managers/Server/NetworkServer.cs | 3 + 2 files changed, 89 insertions(+) create mode 100644 Multiplayer/Components/Networking/Train/PaintThemeLookup.cs diff --git a/Multiplayer/Components/Networking/Train/PaintThemeLookup.cs b/Multiplayer/Components/Networking/Train/PaintThemeLookup.cs new file mode 100644 index 0000000..994adec --- /dev/null +++ b/Multiplayer/Components/Networking/Train/PaintThemeLookup.cs @@ -0,0 +1,86 @@ +using DV.Customization.Paint; +using DV.Utils; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; +using JetBrains.Annotations; + + +namespace Multiplayer.Components.Networking.Train; + +public class PaintThemeLookup : SingletonBehaviour +{ + private readonly Dictionary themeIndices = []; + private string[] themeNames; + + protected override void Awake() + { + base.Awake(); + themeNames = Resources.LoadAll("").Where(x => x is PaintTheme) + .Select(x => x.name.ToLower()) + .ToArray(); + + for (int i = 0; i < themeNames.Length; i++) + { + themeIndices.Add(themeNames[i], i); + } + + Multiplayer.LogDebug(() => + { + return $"Registered Paint Themes:\r\n{string.Join("\r\n", themeNames.Select((name, index) => $"{index}: {name}"))}"; + }); + } + + public string GetThemeName(int index) + { + return (index >= 0 && index < themeNames.Length) ? themeNames[index] : null; + } + + public int GetThemeIndex(string themeName) + { + return themeIndices.TryGetValue(themeName.ToLower(), out int index) ? index : -1; + } + + /* + * Allow other mods to register custom themes + + public void RegisterTheme(string themeName) + { + themeName = themeName.ToLower(); + if (!themeIndices.ContainsKey(themeName)) + { + // Add to array + Array.Resize(ref themeNames, themeNames.Length + 1); + int newIndex = themeNames.Length - 1; + themeNames[newIndex] = themeName; + + // Add to dictionary + themeIndices.Add(themeName, newIndex); + } + } + + public void UnregisterTheme(string themeName) + { + themeName = themeName.ToLower(); + if (themeIndices.TryGetValue(themeName, out int index)) + { + // Remove from dictionary + themeIndices.Remove(themeName); + + // Remove from array and shift remaining elements + for (int i = index; i < themeNames.Length - 1; i++) + { + themeNames[i] = themeNames[i + 1]; + themeIndices[themeNames[i]] = i; // Update indices + } + Array.Resize(ref themeNames, themeNames.Length - 1); + } + } + */ + + [UsedImplicitly] + public new static string AllowAutoCreate() + { + return $"[{nameof(PaintThemeLookup)}]"; + } +} diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index 0ba7779..63f80bf 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -80,6 +80,9 @@ public NetworkServer(IDifficulty difficulty, Settings settings, bool isSinglePla public bool Start(int port) { + //setup paint theme lookup cache + PaintThemeLookup.Instance.CheckInstance(); + WorldStreamingInit.LoadingFinished += OnLoaded; Multiplayer.Log($"Starting server..."); From f78eb89a1c14d441be1a89966ac000cfbba6b1e2 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 4 Jan 2025 12:16:05 +1030 Subject: [PATCH 156/188] Fix for Main Menu braking on clients when exiting a game --- Multiplayer/Networking/Managers/Client/NetworkClient.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index b4fe006..7f69d12 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -86,16 +86,18 @@ public void Start(string address, int port, string password, bool isSinglePlayer isAlsoHost = NetworkLifecycle.Instance.IsServerRunning; originalSession = UserManager.Instance.CurrentUser.CurrentSession; + + LogDebug(() => $"NetworkClient.Start() isAlsoHost: {isAlsoHost}, Original session is Null: {originalSession == null}"); } public override void Stop() { if (!isAlsoHost && originalSession != null) { - LogDebug(() => $"NetworkClient.Stop() destroying session..."); - IGameSession session = UserManager.Instance.CurrentUser.CurrentSession; + LogDebug(() => $"NetworkClient.Stop() destroying session... Original session is Null: {originalSession == null}"); + //IGameSession session = UserManager.Instance.CurrentUser.CurrentSession; Client_GameSession.SetCurrent(originalSession); - session?.Dispose(); + //session?.Dispose(); } base.Stop(); From 206c839709fe225bf077e779983faee3783d63b8 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 4 Jan 2025 12:16:45 +1030 Subject: [PATCH 157/188] Brake sync fix --- Multiplayer/Networking/Managers/Server/NetworkServer.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index 63f80bf..9e1c40f 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -342,7 +342,6 @@ public void SendBrakePressures(ushort netId, float mainReservoirPressure, float { NetId = netId, MainReservoirPressure = mainReservoirPressure, - IndependentPipePressure = independentPipePressure, BrakePipePressure = brakePipePressure, BrakeCylinderPressure = brakeCylinderPressure }, DeliveryMethod.ReliableOrdered, SelfPeer); From 40e7a53300c184ba5f20db8b4b7866023e9474b7 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 4 Jan 2025 12:21:29 +1030 Subject: [PATCH 158/188] Minor change to logging --- Multiplayer/Multiplayer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Multiplayer/Multiplayer.cs b/Multiplayer/Multiplayer.cs index 2568cd6..2b17335 100644 --- a/Multiplayer/Multiplayer.cs +++ b/Multiplayer/Multiplayer.cs @@ -59,7 +59,7 @@ private static bool Load(UnityModManager.ModEntry modEntry) Locale.Load(ModEntry.Path); - Log($"Multiplayer JSON Version: {ModEntry.Info.Version}, Internal Version: {Ver}\r\nGame version: {BuildInfo.BUILD_VERSION_MAJOR.ToString()}.{BuildInfo.BUILDBOT_INFO.ToString()}"); + Log($"Multiplayer JSON Version: {ModEntry.Info.Version}, Internal Version: {Ver}\r\nGame version: {BuildInfo.BUILD_VERSION_MAJOR.ToString()}\r\nBuilbot version: {BuildInfo.BUILDBOT_INFO.ToString()}"); Log("Patching..."); harmony = new Harmony(ModEntry.Info.Id); From f3012761d7924273f729d52ea0b9c62da124bd19 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 4 Jan 2025 23:03:29 +1000 Subject: [PATCH 159/188] Increase time out of connection attempt to match disconnect time out ReconnectDelay * MaxConnectAttempts = 1000 * 10 == DisconnectTimeout --- Multiplayer/Networking/Managers/NetworkManager.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Multiplayer/Networking/Managers/NetworkManager.cs b/Multiplayer/Networking/Managers/NetworkManager.cs index a7c5163..ea147f5 100644 --- a/Multiplayer/Networking/Managers/NetworkManager.cs +++ b/Multiplayer/Networking/Managers/NetworkManager.cs @@ -26,6 +26,7 @@ protected NetworkManager(Settings settings) netManager = new NetManager(this) { DisconnectTimeout = 10000, + ReconnectDelay = 1000, UnconnectedMessagesEnabled = true, BroadcastReceiveEnabled = true, From 83445a12495cc71eca3bfa02d0b3296fbe05479d Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 5 Jan 2025 13:05:11 +1000 Subject: [PATCH 160/188] Fix for dictionaries not clearing on return to main menu --- .../Networking/Train/NetworkedTrainCar.cs | 21 ++++++++----------- Multiplayer/Multiplayer.csproj | 2 +- info.json | 2 +- 3 files changed, 11 insertions(+), 14 deletions(-) diff --git a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs index 71e8c47..b56b72c 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs @@ -68,6 +68,7 @@ public static bool TryGetFromTrainCar(TrainCar trainCar, out NetworkedTrainCar n private const int MAX_COUPLER_ITERATIONS = 10; + private string currentID; public TrainCar TrainCar; public uint TicksSinceSync = uint.MaxValue; public bool HasPlayers => PlayerManager.Car == TrainCar || GetComponentInChildren() != null; @@ -208,19 +209,13 @@ public void OnDisable() NetworkLifecycle.Instance.OnTick -= Common_OnTick; NetworkLifecycle.Instance.OnTick -= Server_OnTick; - if (UnloadWatcher.isUnloading) - return; + //if (UnloadWatcher.isUnloading) + // return; trainCarsToNetworkedTrainCars.Remove(TrainCar); - string id = ""; - if (TrainCar.logicCar == null) - id = trainCarIdToNetworkedTrainCars.FirstOrDefault(x => x.Value == this).Key; - else - id = TrainCar.ID; - - trainCarIdToNetworkedTrainCars.Remove(id); - trainCarIdToTrainCars.Remove(id); + trainCarIdToNetworkedTrainCars.Remove(currentID); + trainCarIdToTrainCars.Remove(currentID); foreach (Coupler coupler in TrainCar.couplers) hoseToCoupler.Remove(coupler.hoseAndCock); @@ -260,6 +255,7 @@ public void OnDisable() } } + currentID = string.Empty; Destroy(this); } @@ -270,8 +266,9 @@ private void OnLogicCarInitialised() //Multiplayer.LogWarning("OnLogicCarInitialised"); if (TrainCar.logicCar != null) { - trainCarIdToNetworkedTrainCars[TrainCar.ID] = this; - trainCarIdToTrainCars[TrainCar.ID] = TrainCar; + currentID = TrainCar.ID; + trainCarIdToNetworkedTrainCars[currentID] = this; + trainCarIdToTrainCars[currentID] = TrainCar; TrainCar.LogicCarInitialized -= OnLogicCarInitialised; } diff --git a/Multiplayer/Multiplayer.csproj b/Multiplayer/Multiplayer.csproj index baaca99..d11332a 100644 --- a/Multiplayer/Multiplayer.csproj +++ b/Multiplayer/Multiplayer.csproj @@ -3,7 +3,7 @@ net48 latest Multiplayer - 0.1.9.5 + 0.1.9.6 diff --git a/info.json b/info.json index 7854455..53b0b64 100644 --- a/info.json +++ b/info.json @@ -1,6 +1,6 @@ { "Id": "Multiplayer", - "Version": "0.1.9.5", + "Version": "0.1.9.6", "DisplayName": "Multiplayer", "Author": "Insprill, Macka, Morm", "EntryMethod": "Multiplayer.Multiplayer.Load", From 41a9b363cac35e9db87e7005ad87bb3c06ded4c2 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 11 Jan 2025 22:34:48 +1000 Subject: [PATCH 161/188] Add in paint theme and basic restoration sync --- .../Networking/Train/NetworkedCarSpawner.cs | 39 ++++++++++ .../Networking/Train/PaintThemeLookup.cs | 30 ++++++-- .../Data/Train/TrainsetSpawnPart.cs | 75 ++++++++++++++----- 3 files changed, 121 insertions(+), 23 deletions(-) diff --git a/Multiplayer/Components/Networking/Train/NetworkedCarSpawner.cs b/Multiplayer/Components/Networking/Train/NetworkedCarSpawner.cs index 103e29b..6a6261f 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedCarSpawner.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedCarSpawner.cs @@ -1,4 +1,5 @@ using System.Collections; +using DV.LocoRestoration; using DV.Simulation.Brake; using DV.ThingTypes; using Multiplayer.Components.Networking.World; @@ -58,6 +59,25 @@ public static NetworkedTrainCar SpawnCar(TrainsetSpawnPart spawnPart, bool preve trainCar.uniqueCar = false; trainCar.InitializeExistingLogicCar(spawnPart.CarId, spawnPart.CarGuid); + //Restoration vehicle hack + //todo: make it work properly + if (spawnPart.IsRestorationLoco) + switch(spawnPart.RestorationState) + { + case LocoRestorationController.RestorationState.S0_Initialized: + case LocoRestorationController.RestorationState.S1_UnlockedRestorationLicense: + case LocoRestorationController.RestorationState.S2_LocoUnblocked: + BlockLoco(trainCar); + + break; + } + + if (trainCar.PaintExterior != null && spawnPart.PaintExterior != null) + trainCar.PaintExterior.currentTheme = spawnPart.PaintExterior; + + if (trainCar.PaintInterior != null && spawnPart.PaintInterior != null) + trainCar.PaintInterior.currentTheme = spawnPart.PaintInterior; + //Add networked components NetworkedTrainCar networkedTrainCar = trainCar.gameObject.GetOrAddComponent(); networkedTrainCar.NetId = spawnPart.NetId; @@ -188,4 +208,23 @@ private static void SetBrakeParams(BrakeSystemData brakeSystemData, TrainCar tra bs.ForceCylinderPressure(brakeSystemData.BrakeCylPressure); } + + private static void BlockLoco(TrainCar trainCar) + { + trainCar.blockInteriorLoading = true; + trainCar.preventFastTravelWithCar = true; + trainCar.preventFastTravelDestination = true; + + if (trainCar.FastTravelDestination != null) + { + trainCar.FastTravelDestination.showOnMap = false; + trainCar.FastTravelDestination.RefreshMarkerVisibility(); + } + + trainCar.preventDebtDisplay = true; + trainCar.preventRerail = true; + trainCar.preventDelete = true; + trainCar.preventService = true; + trainCar.preventCouple = true; + } } diff --git a/Multiplayer/Components/Networking/Train/PaintThemeLookup.cs b/Multiplayer/Components/Networking/Train/PaintThemeLookup.cs index 994adec..1e8dd87 100644 --- a/Multiplayer/Components/Networking/Train/PaintThemeLookup.cs +++ b/Multiplayer/Components/Networking/Train/PaintThemeLookup.cs @@ -10,7 +10,7 @@ namespace Multiplayer.Components.Networking.Train; public class PaintThemeLookup : SingletonBehaviour { - private readonly Dictionary themeIndices = []; + private readonly Dictionary themeIndices = []; private string[] themeNames; protected override void Awake() @@ -20,7 +20,7 @@ protected override void Awake() .Select(x => x.name.ToLower()) .ToArray(); - for (int i = 0; i < themeNames.Length; i++) + for (sbyte i = 0; i < themeNames.Length; i++) { themeIndices.Add(themeNames[i], i); } @@ -31,14 +31,34 @@ protected override void Awake() }); } - public string GetThemeName(int index) + public PaintTheme GetPaintTheme(sbyte index) + { + PaintTheme theme = null; + + var themeName = GetThemeName(index); + + if (themeName != null) + PaintTheme.TryLoad(GetThemeName(index), out theme); + + return theme; + } + + public string GetThemeName(sbyte index) { return (index >= 0 && index < themeNames.Length) ? themeNames[index] : null; } - public int GetThemeIndex(string themeName) + public sbyte GetThemeIndex(PaintTheme theme) + { + if(theme == null) + return -1; + + return GetThemeIndex(theme.assetName); + } + + public sbyte GetThemeIndex(string themeName) { - return themeIndices.TryGetValue(themeName.ToLower(), out int index) ? index : -1; + return themeIndices.TryGetValue(themeName.ToLower(), out sbyte index) ? index : (sbyte)-1; } /* diff --git a/Multiplayer/Networking/Data/Train/TrainsetSpawnPart.cs b/Multiplayer/Networking/Data/Train/TrainsetSpawnPart.cs index a8b5761..afd580d 100644 --- a/Multiplayer/Networking/Data/Train/TrainsetSpawnPart.cs +++ b/Multiplayer/Networking/Data/Train/TrainsetSpawnPart.cs @@ -1,5 +1,5 @@ using DV.Customization.Paint; -using DV.ThingTypes; +using DV.LocoRestoration; using LiteNetLib.Utils; using Multiplayer.Components.Networking; using Multiplayer.Components.Networking.Train; @@ -7,8 +7,6 @@ using Multiplayer.Utils; using System; using System.Collections.Generic; -using System.Linq; -using System.Text; using UnityEngine; namespace Multiplayer.Networking.Data.Train; @@ -26,8 +24,10 @@ public readonly struct TrainsetSpawnPart // Customisation details public readonly bool PlayerSpawnedCar; - public readonly TrainCarPaint PaintExterior; - public readonly TrainCarPaint PaintInterior; + public readonly bool IsRestorationLoco; + public readonly LocoRestorationController.RestorationState RestorationState; + public readonly PaintTheme PaintExterior; + public readonly PaintTheme PaintInterior; // Coupling data public readonly CouplingData FrontCoupling; @@ -46,7 +46,7 @@ public readonly struct TrainsetSpawnPart public readonly BrakeSystemData BrakeData; public TrainsetSpawnPart( - ushort netId, string liveryId, string carId, string carGuid, bool playerSpawnedCar, + ushort netId, string liveryId, string carId, string carGuid, bool playerSpawnedCar, bool isRestoration, LocoRestorationController.RestorationState restorationState, PaintTheme paintExterior, PaintTheme paintInterior, CouplingData frontCoupling, CouplingData rearCoupling, float speed, Vector3 position, Quaternion rotation, BogieData bogie1, BogieData bogie2, BrakeSystemData brakeData) @@ -55,9 +55,17 @@ public TrainsetSpawnPart( LiveryId = liveryId; CarId = carId; CarGuid = carGuid; + PlayerSpawnedCar = playerSpawnedCar; + IsRestorationLoco = isRestoration; + RestorationState = restorationState; + + PaintExterior = paintExterior; + PaintInterior = paintInterior; + FrontCoupling = frontCoupling; RearCoupling = rearCoupling; + Speed = speed; Position = position; Rotation = rotation; @@ -81,6 +89,14 @@ public static void Serialize(NetDataWriter writer, TrainsetSpawnPart data) } writer.Put(data.PlayerSpawnedCar); + writer.Put(data.IsRestorationLoco); + + if(data.IsRestorationLoco) + writer.Put((byte) data.RestorationState); + + writer.Put(PaintThemeLookup.Instance.GetThemeIndex(data.PaintExterior)); + writer.Put(PaintThemeLookup.Instance.GetThemeIndex(data.PaintInterior)); + CouplingData.Serialize(writer, data.FrontCoupling); CouplingData.Serialize(writer, data.RearCoupling); @@ -102,6 +118,18 @@ public static TrainsetSpawnPart Deserialize(NetDataReader reader) string carGuid = new Guid(reader.GetBytesWithLength()).ToString(); bool playerSpawnedCar = reader.GetBool(); + bool isRestoration = reader.GetBool(); + LocoRestorationController.RestorationState restorationState = default; + if (isRestoration) + restorationState = (LocoRestorationController.RestorationState)reader.GetByte(); + + sbyte extThemeIndex = reader.GetSByte(); + sbyte intThemeIndex = reader.GetSByte(); + + + PaintTheme exteriorPaint = PaintThemeLookup.Instance.GetPaintTheme(extThemeIndex); + PaintTheme interiorPaint = PaintThemeLookup.Instance.GetPaintTheme(intThemeIndex); + var frontCoupling = CouplingData.Deserialize(reader); var rearCoupling = CouplingData.Deserialize(reader); @@ -114,7 +142,7 @@ public static TrainsetSpawnPart Deserialize(NetDataReader reader) var brakeSet = BrakeSystemData.Deserialize(reader); return new TrainsetSpawnPart( - netId, liveryId, carId, carGuid, playerSpawnedCar, + netId, liveryId, carId, carGuid, playerSpawnedCar, isRestoration, restorationState, exteriorPaint, interiorPaint, frontCoupling, rearCoupling, speed, position, rotation, bogie1, bogie2, brakeSet); @@ -125,20 +153,31 @@ public static TrainsetSpawnPart FromTrainCar(NetworkedTrainCar networkedTrainCar TrainCar trainCar = networkedTrainCar.TrainCar; Transform transform = networkedTrainCar.transform; + + LocoRestorationController restorationController = LocoRestorationController.GetForTrainCar(trainCar); + var restorationState = restorationController?.State ?? default; + return new TrainsetSpawnPart( - netId: networkedTrainCar.NetId, - liveryId: trainCar.carLivery.id, - carId: trainCar.ID, - carGuid: trainCar.CarGUID, - playerSpawnedCar: trainCar.playerSpawnedCar, + networkedTrainCar.NetId, + trainCar.carLivery.id, + trainCar.ID, + trainCar.CarGUID, + + trainCar.playerSpawnedCar, + restorationController != null, + restorationState, + + trainCar?.PaintExterior?.currentTheme, + trainCar?.PaintInterior?.currentTheme, + frontCoupling: CouplingData.From(trainCar.frontCoupler), rearCoupling: CouplingData.From(trainCar.rearCoupler), - speed: trainCar.GetForwardSpeed(), - position: transform.position - WorldMover.currentMove, - rotation: transform.rotation, - bogie1: BogieData.FromBogie(trainCar.Bogies[0], true), - bogie2: BogieData.FromBogie(trainCar.Bogies[1], true), - brakeData: BrakeSystemData.From(trainCar.brakeSystem) + trainCar.GetForwardSpeed(), + transform.position - WorldMover.currentMove, + transform.rotation, + BogieData.FromBogie(trainCar.Bogies[0], true), + BogieData.FromBogie(trainCar.Bogies[1], true), + BrakeSystemData.From(trainCar.brakeSystem) ); } From 3874c0578793aca9119e3f148f1b53ef7c9a3eb4 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 11 Jan 2025 22:37:53 +1000 Subject: [PATCH 162/188] Update job registration and plate update process --- .../Networking/Jobs/NetworkedJob.cs | 2 ++ .../World/NetworkedStationController.cs | 25 ++++++++++--- .../Managers/Client/NetworkClient.cs | 1 + .../Managers/Server/NetworkServer.cs | 26 +++++++++----- Multiplayer/Patches/Jobs/JobValidatorPatch.cs | 36 ++++++++++++------- 5 files changed, 66 insertions(+), 24 deletions(-) diff --git a/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs b/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs index bffad9c..2f3232d 100644 --- a/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs +++ b/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs @@ -125,6 +125,8 @@ public NetworkedItem JobReport public Action OnJobDirty; + public List JobCars = []; + protected override void Awake() { base.Awake(); diff --git a/Multiplayer/Components/Networking/World/NetworkedStationController.cs b/Multiplayer/Components/Networking/World/NetworkedStationController.cs index 86556f2..bed6a24 100644 --- a/Multiplayer/Components/Networking/World/NetworkedStationController.cs +++ b/Multiplayer/Components/Networking/World/NetworkedStationController.cs @@ -248,8 +248,9 @@ public void AddJobs(JobData[] jobs) private void AddJob(JobData jobData) { Job newJob = CreateJobFromJobData(jobData); + var carNetIds = jobData.GetCars(); - NetworkedJob networkedJob = CreateNetworkedJob(newJob, jobData.NetID); + NetworkedJob networkedJob = CreateNetworkedJob(newJob, jobData.NetID, carNetIds); NetworkedJobs.Add(networkedJob); @@ -263,9 +264,20 @@ private void AddJob(JobData jobData) GenerateOverview(networkedJob, jobData.ItemNetID, jobData.ItemPosition); } } + else if (networkedJob.Job.State == DV.ThingTypes.JobState.InProgress) + { + takenJobs.Add(newJob); + } + else + { + //we don't need to update anything, so we'll return + //Maybe item sync will require knowledge of the job for expired/failed/completed reports, but we currently only sync these for connected players + return; + } + Multiplayer.LogDebug(() => $"AddJob({jobData.ID}) Starting plate update {newJob.ID} count: {jobData.GetCars().Count}"); - StartCoroutine(UpdateCarPlates(jobData.GetCars(), newJob.ID)); + StartCoroutine(UpdateCarPlates(carNetIds, newJob.ID)); Multiplayer.Log($"Added NetworkedJob {newJob.ID} to NetworkedStationController {StationController.logicStation.ID}"); } @@ -325,12 +337,13 @@ private bool CheckCarsLoaded(JobData jobData) return true; } - private NetworkedJob CreateNetworkedJob(Job job, ushort netId) + private NetworkedJob CreateNetworkedJob(Job job, ushort netId, List carNetIds) { NetworkedJob networkedJob = new GameObject($"NetworkedJob {job.ID}").AddComponent(); networkedJob.NetId = netId; networkedJob.Initialize(job, this); networkedJob.OnJobDirty += OnJobDirty; + networkedJob.JobCars = carNetIds; return networkedJob; } @@ -376,6 +389,7 @@ private void HandleJobStateChange(NetworkedJob netJob, JobUpdateStruct job) { JobValidator validator = null; NetworkedItem netItem; + NetworkLifecycle.Instance.Client.LogDebug(()=> $"NetworkedStation.HandleJobStateChange() {job.JobNetID}, {job.ValidationStationId}"); if (job.ItemNetID != 0 && job.ValidationStationId != 0) if (Get(job.ValidationStationId, out var netStation)) @@ -385,7 +399,7 @@ private void HandleJobStateChange(NetworkedJob netJob, JobUpdateStruct job) netJob.Job.State == DV.ThingTypes.JobState.Completed) && validator == null) { - NetworkLifecycle.Instance.Client.LogError($"NetworkedStation.UpdateJobs() jobNetId: {job.JobNetID}, Validator required and not found!"); + NetworkLifecycle.Instance.Client.LogError($"NetworkedStation.HandleJobStateChange() jobNetId: {job.JobNetID}, Validator required and not found!"); return; } @@ -419,6 +433,7 @@ private void HandleJobStateChange(NetworkedJob netJob, JobUpdateStruct job) netJob.AddReport(netItem); printed = true; + StartCoroutine(UpdateCarPlates(netJob.JobCars, string.Empty)); netJob.JobBooklet?.GetTrackedItem()?.DestroyJobBooklet(); break; @@ -426,6 +441,7 @@ private void HandleJobStateChange(NetworkedJob netJob, JobUpdateStruct job) case DV.ThingTypes.JobState.Abandoned: takenJobs.Remove(netJob.Job); abandonedJobs.Add(netJob.Job); + StartCoroutine(UpdateCarPlates(netJob.JobCars, string.Empty)); break; case DV.ThingTypes.JobState.Expired: @@ -434,6 +450,7 @@ private void HandleJobStateChange(NetworkedJob netJob, JobUpdateStruct job) netJob.Job.ExpireJob(); StationController.ClearAvailableJobOverviewGOs(); //todo: better logic when players can hold items + StartCoroutine(UpdateCarPlates(netJob.JobCars, string.Empty)); break; default: diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 7f69d12..f7acc2a 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -540,6 +540,7 @@ private void OnClientboundDestroyTrainCarPacket(ClientboundDestroyTrainCarPacket networkedPlayer.UpdateCar(0); } + networkedTrainCar.TrainCar.UpdateJobIdOnCarPlates(string.Empty); CarSpawner.Instance.DeleteCar(networkedTrainCar.TrainCar); } } diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index 9e1c40f..c48f02f 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -446,16 +446,22 @@ public void SendDebtStatus(bool hasDebt) }, DeliveryMethod.ReliableUnordered, SelfPeer); } - public void SendJobsCreatePacket(NetworkedStationController networkedStation, NetworkedJob[] jobs, DeliveryMethod method = DeliveryMethod.ReliableSequenced) + public void SendJobsCreatePacket(NetworkedStationController networkedStation, NetworkedJob[] jobs, NetPeer peer = null) { Multiplayer.Log($"Sending JobsCreatePacket for stationNetId {networkedStation.NetId} with {jobs.Count()} jobs"); - SendPacketToAll(ClientboundJobsCreatePacket.FromNetworkedJobs(networkedStation, jobs), method, SelfPeer); + + var packet = ClientboundJobsCreatePacket.FromNetworkedJobs(networkedStation, jobs); + + if (peer ==null) + SendPacketToAll(packet, DeliveryMethod.ReliableOrdered, SelfPeer); + else + SendPacket(peer, packet, DeliveryMethod.ReliableOrdered); } - public void SendJobsUpdatePacket(ushort stationNetId, NetworkedJob[] jobs, NetPeer peer = null) + public void SendJobsUpdatePacket(ushort stationNetId, NetworkedJob[] jobs) { Multiplayer.Log($"Sending JobsUpdatePacket for stationNetId {stationNetId} with {jobs.Count()} jobs"); - SendPacketToAll(ClientboundJobsUpdatePacket.FromNetworkedJobs(stationNetId, jobs), DeliveryMethod.ReliableUnordered, SelfPeer); + SendPacketToAll(ClientboundJobsUpdatePacket.FromNetworkedJobs(stationNetId, jobs), DeliveryMethod.ReliableOrdered, SelfPeer); } public void SendItemsChangePacket(List items, ServerPlayer player) @@ -465,7 +471,7 @@ public void SendItemsChangePacket(List items, ServerPlayer playe if (TryGetNetPeer(player.Id, out NetPeer peer) && peer != SelfPeer) { SendNetSerializablePacket(peer, new CommonItemChangePacket { Items = items }, - DeliveryMethod.ReliableUnordered); + DeliveryMethod.ReliableOrdered); } } @@ -684,10 +690,14 @@ private void OnServerboundClientReadyPacket(ServerboundClientReadyPacket packet, { if (NetworkedStationController.GetFromStationController(station, out NetworkedStationController netStation)) { - NetworkedJob[] jobs = netStation.NetworkedJobs.ToArray(); + //only send active jobs (available or in progress) - new clients don't need to know about old jobs + NetworkedJob[] jobs = netStation.NetworkedJobs + .Where(j => j.Job.State == JobState.Available || j.Job.State == JobState.InProgress) + .ToArray(); + for (int i = 0; i < jobs.Length; i++) { - SendJobsCreatePacket(netStation, [jobs[i]], DeliveryMethod.ReliableOrdered); + SendJobsCreatePacket(netStation, [jobs[i]]); } } else @@ -988,7 +998,7 @@ private void OnServerboundJobValidateRequestPacket(ServerboundJobValidateRequest { LogWarning($"OnServerboundJobValidateRequestPacket() NetworkedJob not found: {packet.JobNetId}"); - SendPacket(peer, new ClientboundJobValidateResponsePacket { JobNetId = packet.JobNetId, Invalid = true }, DeliveryMethod.ReliableUnordered); + SendPacket(peer, new ClientboundJobValidateResponsePacket { JobNetId = packet.JobNetId, Invalid = true }, DeliveryMethod.ReliableOrdered); return; } diff --git a/Multiplayer/Patches/Jobs/JobValidatorPatch.cs b/Multiplayer/Patches/Jobs/JobValidatorPatch.cs index 8fa9401..35d5243 100644 --- a/Multiplayer/Patches/Jobs/JobValidatorPatch.cs +++ b/Multiplayer/Patches/Jobs/JobValidatorPatch.cs @@ -23,7 +23,7 @@ private static void Start(JobValidator __instance) [HarmonyPatch(nameof(JobValidator.ProcessJobOverview))] [HarmonyPrefix] - private static bool ProcessJobOverview_Prefix(JobValidator __instance, JobOverview jobOverview) + private static bool ProcessJobOverview(JobValidator __instance, JobOverview jobOverview) { if(__instance.bookletPrinter.IsOnCooldown) @@ -34,7 +34,7 @@ private static bool ProcessJobOverview_Prefix(JobValidator __instance, JobOvervi if(!NetworkedJob.TryGetFromJob(jobOverview.job, out NetworkedJob networkedJob) || jobOverview.job.State != JobState.Available) { - NetworkLifecycle.Instance.Client.LogWarning($"ProcessJobOverview_Prefix({jobOverview?.job?.ID}) NetworkedJob found: {networkedJob != null}, Job state: {jobOverview?.job?.State}"); + NetworkLifecycle.Instance.Client.LogWarning($"Processing JobOverview {jobOverview?.job?.ID} {(networkedJob == null ? "NetworkedJob not found!, " : "")}Job state: {jobOverview?.job?.State}"); __instance.bookletPrinter.PlayErrorSound(); jobOverview.DestroyJobOverview(); return false; @@ -42,14 +42,12 @@ private static bool ProcessJobOverview_Prefix(JobValidator __instance, JobOvervi if (NetworkLifecycle.Instance.IsHost()) { - Multiplayer.Log($"ProcessJobOverview_Prefix({jobOverview?.job?.ID}) IsHost"); + NetworkLifecycle.Instance.Server.Log($"Processing JobOverview {jobOverview?.job?.ID}"); networkedJob.JobValidator = __instance; return true; } if (!networkedJob.ValidatorRequestSent) - // return (networkedJob.ValidatorResponseReceived && networkedJob.ValidationAccepted); - //else SendValidationRequest(__instance, networkedJob, ValidationType.JobOverview); return false; @@ -68,7 +66,7 @@ private static bool ValidateJob_Prefix(JobValidator __instance, JobBooklet jobBo if (!NetworkedJob.TryGetFromJob(jobBooklet.job, out NetworkedJob networkedJob) || jobBooklet.job.State != JobState.InProgress) { - NetworkLifecycle.Instance.Client.LogWarning($"ValidateJob({jobBooklet?.job?.ID}) NetworkedJob found: {networkedJob != null}, Job state: {jobBooklet?.job?.State}"); + NetworkLifecycle.Instance.Client.LogWarning($"Validating Job {jobBooklet?.job?.ID} {(networkedJob == null ? "NetworkedJob not found!, " : "")}Job state: {jobBooklet?.job?.State}"); __instance.bookletPrinter.PlayErrorSound(); jobBooklet.DestroyJobBooklet(); return false; @@ -76,13 +74,12 @@ private static bool ValidateJob_Prefix(JobValidator __instance, JobBooklet jobBo if (NetworkLifecycle.Instance.IsHost()) { + NetworkLifecycle.Instance.Server.Log($"Validating Job {jobBooklet?.job?.ID}"); networkedJob.JobValidator = __instance; return true; } - if (networkedJob.ValidatorRequestSent) - return (networkedJob.ValidatorResponseReceived && networkedJob.ValidationAccepted); - else + if (!networkedJob.ValidatorRequestSent) SendValidationRequest(__instance, networkedJob, ValidationType.JobBooklet); return false; @@ -105,7 +102,7 @@ private static void SendValidationRequest(JobValidator validator,NetworkedJob ne } else { - NetworkLifecycle.Instance.Client.LogError($"SendValidation({netJob?.Job?.ID}, {type}) Failed to find NetworkedStation"); + NetworkLifecycle.Instance.Client.LogError($"Failed to validate {type} for {netJob?.Job?.ID}. NetworkedStation not found!"); validator.bookletPrinter.PlayErrorSound(); } } @@ -113,12 +110,27 @@ private static IEnumerator AwaitResponse(JobValidator validator, NetworkedJob ne { yield return new WaitForSecondsRealtime((NetworkLifecycle.Instance.Client.Ping * 3f)/1000); - NetworkLifecycle.Instance.Client.Log($"JobValidator_Patch.AwaitResponse() ResponseReceived: {networkedJob?.ValidatorResponseReceived}, Accepted: {networkedJob?.ValidationAccepted}"); + bool received = networkedJob.ValidatorResponseReceived; + bool accepted = networkedJob.ValidationAccepted; + + var receivedStr = received ? "received" : "timed out"; + var acceptedStr = accepted ? " Accepted" : " Rejected"; - if (networkedJob == null || (!networkedJob.ValidatorResponseReceived || !networkedJob.ValidationAccepted)) + NetworkLifecycle.Instance.Client.Log($"Job Validation Response {receivedStr} for {networkedJob?.Job?.ID}.{acceptedStr}"); + + if (networkedJob == null) { validator.bookletPrinter.PlayErrorSound(); yield break; } + + if(!received || !accepted) + { + validator.bookletPrinter.PlayErrorSound(); + } + + networkedJob.ValidatorResponseReceived = false; + networkedJob.ValidationAccepted = false; + } } From 32fb21034afe7861b0e6d606e3956f7dfe27e9cc Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 11 Jan 2025 22:50:49 +1000 Subject: [PATCH 163/188] Coupler interaction ready for testing Future work: Track chain / hose positions and send across network Implement fix when a player disconnects while dragging a chain / hose --- .../Networking/Train/NetworkedCarSpawner.cs | 33 +-- .../Networking/Train/NetworkedTrainCar.cs | 199 +++++++++++++++++- .../Data/Train/CouplerInteractionType.cs | 27 +-- .../Networking/Data/Train/CouplingData.cs | 2 +- .../Managers/Client/NetworkClient.cs | 4 +- .../Managers/Server/NetworkServer.cs | 31 ++- .../Train/CouplerChainInteractionPatch.cs | 6 +- .../Patches/Train/CouplerInterfacerPatch.cs | 4 +- .../Items/RemoteControllerModulePatch.cs | 4 +- 9 files changed, 263 insertions(+), 47 deletions(-) diff --git a/Multiplayer/Components/Networking/Train/NetworkedCarSpawner.cs b/Multiplayer/Components/Networking/Train/NetworkedCarSpawner.cs index 6a6261f..4625a3d 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedCarSpawner.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedCarSpawner.cs @@ -110,6 +110,9 @@ public static NetworkedTrainCar SpawnCar(TrainsetSpawnPart spawnPart, bool preve private static void Couple(in TrainsetSpawnPart spawnPart, TrainCar trainCar, bool autoCouple) { + TrainsetSpawnPart sp = spawnPart; + Multiplayer.LogDebug(() =>$"Couple([{sp.CarId}, {sp.NetId}], trainCar, {autoCouple})"); + if (autoCouple) { trainCar.frontCoupler.preventAutoCouple = spawnPart.FrontCoupling.PreventAutoCouple; @@ -130,27 +133,27 @@ private static void Couple(in TrainsetSpawnPart spawnPart, TrainCar trainCar, bo private static void HandleCoupling(CouplingData couplingData, Coupler currentCoupler) { - if (!couplingData.IsCoupled && !couplingData.HoseConnected) - return; - if (!NetworkedTrainCar.GetTrainCar(couplingData.ConnectionNetId, out TrainCar otherCar)) - { - Multiplayer.LogWarning($"AutoCouple([{currentCoupler?.train?.GetNetId()}, {currentCoupler?.train?.ID}]) did not find car at {(currentCoupler.isFrontCoupler ? "Front" : "Rear")} car with netId: {couplingData.ConnectionNetId}"); - return; - } - - var otherCoupler = couplingData.ConnectionToFront ? otherCar.frontCoupler : otherCar.rearCoupler; + CouplingData cd = couplingData; + TrainCar tc = currentCoupler.train; + var net = tc.GetNetId(); + + Multiplayer.LogDebug(() => $"HandleCoupling([{tc?.ID}, {net}]) couplingData: is front: {currentCoupler.isFrontCoupler}, {couplingData.HoseConnected}, {couplingData.CockOpen}"); if (couplingData.IsCoupled) { - //NetworkLifecycle.Instance.Client.LogDebug(() => $"AutoCouple() Coupling {(currentCoupler.isFrontCoupler? "Front" : "Rear")}: {currentCoupler?.train?.ID}, to {otherCar?.ID}, at: {(connectionToFront ? "Front" : "Rear")}"); - SetCouplingState(currentCoupler, otherCoupler, couplingData.State); + if (!NetworkedTrainCar.GetTrainCar(couplingData.ConnectionNetId, out TrainCar otherCar)) + { + Multiplayer.LogWarning($"HandleCoupling([{currentCoupler?.train?.ID}, {currentCoupler?.train?.GetNetId()}]) did not find car at {(currentCoupler.isFrontCoupler ? "Front" : "Rear")} car with netId: {couplingData.ConnectionNetId}"); + } + else + { + var otherCoupler = couplingData.ConnectionToFront ? otherCar.frontCoupler : otherCar.rearCoupler; + SetCouplingState(currentCoupler, otherCoupler, couplingData.State); + } } - if (couplingData.HoseConnected) - { - CarsSaveManager.RestoreHoseAndCock(currentCoupler, couplingData.HoseConnected, couplingData.CockOpen); - } + CarsSaveManager.RestoreHoseAndCock(currentCoupler, couplingData.HoseConnected, couplingData.CockOpen); } public static void SetCouplingState(Coupler coupler, Coupler otherCoupler, ChainCouplerInteraction.State targetState) diff --git a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs index b56b72c..94ff7d4 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs @@ -7,6 +7,7 @@ using DV.Simulation.Cars; using DV.ThingTypes; using JetBrains.Annotations; +using LiteNetLib; using LocoSim.Definitions; using LocoSim.Implementations; using Multiplayer.Components.Networking.Player; @@ -97,6 +98,12 @@ public static bool TryGetFromTrainCar(TrainCar trainCar, out NetworkedTrainCar n public bool IsDestroying; + //Coupler interaction + private bool frontInteracting = false; + private bool rearInteracting = false; + + private int frontInteractionPeer; + private int rearInteractionPeer; #region Client public bool Client_Initialized {get; private set;} @@ -105,7 +112,10 @@ public static bool TryGetFromTrainCar(TrainCar trainCar, out NetworkedTrainCar n public TickedQueue client_bogie1Queue; public TickedQueue client_bogie2Queue; + private Coupler couplerInteraction; + private ChainCouplerInteraction.State originalState; + private Coupler originalCoupledTo; #endregion protected override bool IsIdServerAuthoritative => true; @@ -187,6 +197,8 @@ public void Start() if (NetworkLifecycle.Instance.IsHost()) { NetworkLifecycle.Instance.OnTick += Server_OnTick; + NetworkLifecycle.Instance.Server.PlayerDisconnect += Server_OnPlayerDisconnect; + bogie1.TrackChanged += Server_BogieTrackChanged; bogie2.TrackChanged += Server_BogieTrackChanged; TrainCar.CarDamage.CarEffectiveHealthStateUpdate += Server_CarHealthUpdate; @@ -475,6 +487,67 @@ private void Server_SendHealthState() NetworkLifecycle.Instance.Server.SendCarHealthUpdate(NetId, TrainCar.CarDamage.currentHealth); } + public bool Server_ValidateCouplerInteraction(CommonCouplerInteractionPacket packet, NetPeer peer) + { + Multiplayer.LogDebug(() => + $"Server_ValidateCouplerInteraction([{(CouplerInteractionType)packet.Flags}, {CurrentID}, {packet.NetId}], {peer.Id}) " + + $"isFront: {packet.IsFrontCoupler}, frontInteracting: {frontInteracting}, frontInteractionPeer: {frontInteractionPeer}, " + + $"rearInteracting: {rearInteracting}, rearInteractionPeer: {rearInteractionPeer}" + ); + //Ensure no one else is interacting + if (packet.IsFrontCoupler && frontInteracting && peer.Id != frontInteractionPeer || + packet.IsFrontCoupler == false && rearInteracting && peer.Id != rearInteractionPeer) + { + Multiplayer.LogDebug(() => $"Server_ValidateCouplerInteraction([{packet.Flags}, {CurrentID}, {packet.NetId}], {peer.Id}) Failed to validate!"); + return false; + } + + Multiplayer.LogDebug(() => $"Server_ValidateCouplerInteraction([{packet.Flags}, {CurrentID}, {packet.NetId}], {peer.Id}) No one interacting"); + + if (((CouplerInteractionType)packet.Flags).HasFlag(CouplerInteractionType.Start)) + { + if (packet.IsFrontCoupler) + { + frontInteracting = true; + frontInteractionPeer = peer.Id; + } + else + { + rearInteracting = true; + rearInteractionPeer = peer.Id; + } + } + else + { + if (packet.IsFrontCoupler) + frontInteracting = false; + else + rearInteracting = false; + } + + //todo: Additional checks for player location/proximity + + Multiplayer.LogDebug(() => $"Server_ValidateCouplerInteraction([{packet.Flags}, {CurrentID}, {packet.NetId}], {peer.Id}) Validation passed!"); + return true; + } + + private void Server_OnPlayerDisconnect(uint id) + { + //todo: resove player disconnection during chain interaction + if (frontInteractionPeer == id || rearInteractionPeer == id) + { + Multiplayer.LogWarning($"Server_OnPlayerDisconnect() Coupler interaction in unknown state [{CurrentID}, {NetId}] isFront: {frontInteractionPeer == id}"); + if (frontInteractionPeer == id) + { + frontInteracting = false ; + //NetworkLifecycle.Instance.Client.SendCouplerInteraction(cou, coupler, otherCoupler); + } + else + { + rearInteracting = false; + } + } + } #endregion #region Common @@ -653,7 +726,7 @@ public void Common_ReceiveCouplerInteraction(CommonCouplerInteractionPacket pack Coupler coupler = packet.IsFrontCoupler ? TrainCar?.frontCoupler : TrainCar?.rearCoupler; TrainCar otherCar = null; Coupler otherCoupler = null; - + if (coupler == null) { Multiplayer.LogWarning($"Common_ReceiveCouplerInteraction() did not find coupler for [{TrainCar?.ID}, {NetId}], coupler is front: {packet.IsFrontCoupler}"); @@ -670,6 +743,64 @@ public void Common_ReceiveCouplerInteraction(CommonCouplerInteractionPacket pack Multiplayer.LogDebug(() => $"Common_ReceiveCouplerInteraction() [{TrainCar?.ID}, {NetId}], coupler is front: {packet.IsFrontCoupler}, flags: {flags}, otherCouplerNetId: {packet.OtherNetId}"); + if (flags == CouplerInteractionType.NoAction) + { + Multiplayer.LogDebug(() => $"Common_ReceiveCouplerInteraction() Interaction rejected! [{CurrentID}, {NetId}]"); + //our interaction was denied + coupler.ChainScript?.knobGizmo?.ForceEndInteraction(); + couplerInteraction = null; + + if (coupler.ChainScript.state == originalState) + return; + + switch (originalState) + { + case ChainCouplerInteraction.State.Parked: + StartCoroutine(ParkCoupler(coupler)); + break; + case ChainCouplerInteraction.State.Dangling: + if (coupler.ChainScript.state == ChainCouplerInteraction.State.Attached_Tight) + coupler.ChainScript.fsm.Fire(ChainCouplerInteraction.Trigger.Screw_Used); + + StartCoroutine(DangleCoupler(coupler)); + break; + case ChainCouplerInteraction.State.Attached_Loose: + if(coupler.ChainScript.state == ChainCouplerInteraction.State.Attached_Tight) + coupler.ChainScript.fsm.Fire(ChainCouplerInteraction.Trigger.Screw_Used); + else + StartCoroutine(LooseAttachCoupler(coupler, originalCoupledTo)); + break; + case ChainCouplerInteraction.State.Attached_Tight: + if (coupler.ChainScript.state != ChainCouplerInteraction.State.Attached_Loose) + StartCoroutine(LooseAttachCoupler(coupler, originalCoupledTo)); + + coupler.ChainScript.fsm.Fire(ChainCouplerInteraction.Trigger.Screw_Used); + break; + default: + Multiplayer.LogDebug(() => $"Common_ReceiveCouplerInteraction() Unable to return to last state! {originalState}"); + break; + } + return; + } + if (flags == CouplerInteractionType.Start && coupler != couplerInteraction) + { + Multiplayer.LogDebug(() => $"Common_ReceiveCouplerInteraction() Interaction started [{CurrentID}, {NetId}] isFront: {coupler.isFrontCoupler}"); + //We've received a start signal for a coupler we aren't interacting with + //Another player must be interacting, so let's block us from tampering with it + if (coupler?.ChainScript?.knobGizmo) + coupler.ChainScript.knobGizmo.InteractionAllowed = false; + if(coupler?.ChainScript?.screwButtonBase) + coupler.ChainScript.screwButtonBase.InteractionAllowed = false; + + return; + } + + if (coupler.ChainScript.state == ChainCouplerInteraction.State.Being_Dragged) + { + Multiplayer.LogDebug(() => $"Common_ReceiveCouplerInteraction() [{TrainCar?.ID}, {NetId}], coupler is front: {packet.IsFrontCoupler}, flags: {flags}, otherCouplerNetId: {packet.OtherNetId} Being Dragged!"); + coupler.ChainScript?.knobGizmo?.ForceEndInteraction(); + } + if (flags.HasFlag(CouplerInteractionType.CouplerCouple) && packet.OtherNetId != 0) { Multiplayer.LogDebug(() => $"1 Common_ReceiveCouplerInteraction() [{TrainCar?.ID}, {NetId}], coupler is front: {packet.IsFrontCoupler}, flags: {flags} "); @@ -697,10 +828,7 @@ public void Common_ReceiveCouplerInteraction(CommonCouplerInteractionPacket pack Multiplayer.LogDebug(() => $"5 Common_ReceiveCouplerInteraction() [{TrainCar?.ID}, {NetId}], coupler is front: {packet.IsFrontCoupler}, flags: {flags} restorestate: {coupler.state}, current state: {coupler.state}, Chain state:{coupler.ChainScript.state}, isCoupled: {coupler.IsCoupled()}"); if (coupler.ChainScript.state != ChainCouplerInteraction.State.Attached_Tight) - { - Multiplayer.LogDebug(() => $"5A Common_ReceiveCouplerInteraction() [{TrainCar?.ID}, {NetId}], coupler is front: {packet.IsFrontCoupler}, flags: {flags} restorestate: {coupler.state}, current state: {coupler.state}, Chain state:{coupler.ChainScript.state}, isCoupled: {coupler.IsCoupled()}"); StartCoroutine(DangleCoupler(coupler)); - } else Multiplayer.LogWarning(() => $"Received Dangle interaction for [{TrainCar?.ID}, {NetId}], coupler is front: {packet.IsFrontCoupler}, but coupler is in the wrong state: {coupler.state}, Chain state:{coupler.ChainScript.state}, isCoupled: {coupler.IsCoupled()}"); } @@ -713,6 +841,11 @@ public void Common_ReceiveCouplerInteraction(CommonCouplerInteractionPacket pack Multiplayer.LogDebug(() => $"7 Common_ReceiveCouplerInteraction() [{TrainCar?.ID}, {NetId}], coupler is front: {packet.IsFrontCoupler}, flags: {flags}"); coupler.ChainScript.fsm.Fire(ChainCouplerInteraction.Trigger.Screw_Used); } + else if(coupler.ChainScript.CurrentState == ChainCouplerInteraction.State.Disabled && coupler.state == ChainCouplerInteraction.State.Attached_Tight) + { + //if it's disabled we'll use the internal routines and the state will restore when this player sees the coupling next + coupler.SetChainTight(false); + } } if (flags.HasFlag(CouplerInteractionType.CouplerTighten)) @@ -723,6 +856,11 @@ public void Common_ReceiveCouplerInteraction(CommonCouplerInteractionPacket pack Multiplayer.LogDebug(() => $"9 Common_ReceiveCouplerInteraction() [{TrainCar?.ID}, {NetId}], coupler is front: {packet.IsFrontCoupler}, flags: {flags}"); coupler.ChainScript.fsm.Fire(ChainCouplerInteraction.Trigger.Screw_Used); } + else if (coupler.ChainScript.CurrentState == ChainCouplerInteraction.State.Disabled && coupler.state == ChainCouplerInteraction.State.Attached_Loose) + { + //if it's disabled we'll use the internal routines and the state will restore when this player sees the coupling next + coupler.SetChainTight(true); + } } if (flags.HasFlag(CouplerInteractionType.CoupleViaUI)) @@ -761,6 +899,12 @@ public void Common_ReceiveCouplerInteraction(CommonCouplerInteractionPacket pack MultipleUnitModule.DisconnectCablesIfMultipleUnitSupported(coupler.train, coupler.isFrontCoupler, !coupler.isFrontCoupler); } } + + //presumably the interaction is now complete, release control to player + if (coupler?.ChainScript?.knobGizmo) + coupler.ChainScript.knobGizmo.InteractionAllowed = true; + if (coupler?.ChainScript?.screwButtonBase) + coupler.ChainScript.screwButtonBase.InteractionAllowed = true; } private IEnumerator LooseAttachCoupler(Coupler coupler, Coupler otherCoupler) @@ -772,8 +916,21 @@ private IEnumerator LooseAttachCoupler(Coupler coupler, Coupler otherCoupler) Multiplayer.LogDebug(() => $"LooseAttachCoupler() [{TrainCar?.ID}], Null reference! Coupler: {coupler != null}, chainscript: {coupler?.ChainScript != null}, other coupler: {otherCoupler != null}, other chainscript: {otherCoupler?.ChainScript != null}, other attach point: {otherCoupler?.ChainScript?.ownAttachPoint}"); yield break; } + ChainCouplerInteraction ccInteraction = coupler.ChainScript; + if(ccInteraction.CurrentState == ChainCouplerInteraction.State.Disabled) + { + //since it's disabled FSM events won't fire. Force a coupling if required, otherwise set state ready for player visibility trigger + + if (coupler.coupledTo == null) + coupler.CoupleTo(otherCoupler, true, true); + else + coupler.state = ChainCouplerInteraction.State.Attached_Loose; + + yield break; + } + //Simulate player pickup coupler.ChainScript.fsm.Fire(ChainCouplerInteraction.Trigger.Picked_Up_By_Player); @@ -804,6 +961,17 @@ private IEnumerator ParkCoupler(Coupler coupler) { ChainCouplerInteraction ccInteraction = coupler.ChainScript; + if (ccInteraction.CurrentState == ChainCouplerInteraction.State.Disabled) + { + //since it's disabled FSM events won't fire, but state will be restored when the coupling is visible to the current player + if(coupler.state == ChainCouplerInteraction.State.Attached_Loose && coupler.coupledTo != null) + coupler.Uncouple(true, false, false, true); + + coupler.state = ChainCouplerInteraction.State.Parked; + + yield break; + } + //Simulate player pickup coupler.ChainScript.fsm.Fire(ChainCouplerInteraction.Trigger.Picked_Up_By_Player); @@ -835,6 +1003,17 @@ private IEnumerator DangleCoupler(Coupler coupler) { ChainCouplerInteraction ccInteraction = coupler.ChainScript; + if (ccInteraction.CurrentState == ChainCouplerInteraction.State.Disabled) + { + //since it's disabled FSM events won't fire, but state will be restored when the coupling is visible to the current player + if (coupler.state == ChainCouplerInteraction.State.Attached_Loose && coupler.coupledTo != null) + coupler.Uncouple(true, false, false, true); + + coupler.state = ChainCouplerInteraction.State.Dangling; + + yield break; + } + //Simulate player pickup coupler.ChainScript.fsm.Fire(ChainCouplerInteraction.Trigger.Picked_Up_By_Player); @@ -925,11 +1104,8 @@ public void Client_ReceiveBrakePressureUpdate(float mainReservoirPressure, float if (!hasSimFlow) return; - //B99 review need / mod brakeSystem.ForceIndependentPipePressure(independentPipePressure); - //B99 review need / mod brakeSystem.ForceTargetIndBrakeCylinderPressure(brakeCylinderPressure); brakeSystem.SetMainReservoirPressure(mainReservoirPressure); - //brakeSystem.SetBrakePipePressure(brakePipePressure); brakeSystem.brakePipePressure = brakePipePressure; brakeSystem.brakeset.pipePressure = brakePipePressure; @@ -988,6 +1164,9 @@ public void Client_CouplerStateChange(ChainCouplerInteraction.State state, Coupl { case ChainCouplerInteraction.State.Being_Dragged: couplerInteraction = coupler; + originalState = coupler.state; + originalCoupledTo = coupler.coupledTo; + interactionFlags = CouplerInteractionType.Start; Multiplayer.LogDebug(() => $"3 Client_CouplerStateChange({state}) trainCar: [{TrainCar?.ID}, {NetId}]"); break; @@ -1026,8 +1205,12 @@ public void Client_CouplerStateChange(ChainCouplerInteraction.State state, Coupl if (interactionFlags != CouplerInteractionType.NoAction) { Multiplayer.LogDebug(() => $"8 Client_CouplerStateChange({state}) trainCar: [{TrainCar?.ID}, {NetId}], coupler is front: {coupler?.isFrontCoupler}, Sending: {interactionFlags}"); - couplerInteraction = null; NetworkLifecycle.Instance.Client.SendCouplerInteraction(interactionFlags, coupler, otherCoupler); + + //finished interaction, clear flag + if (interactionFlags != CouplerInteractionType.Start) + couplerInteraction = null; + return; } Multiplayer.LogDebug(() => $"9 Client_CouplerStateChange({state}) trainCar: [{TrainCar?.ID}, {NetId}]"); diff --git a/Multiplayer/Networking/Data/Train/CouplerInteractionType.cs b/Multiplayer/Networking/Data/Train/CouplerInteractionType.cs index 0944e72..36845ad 100644 --- a/Multiplayer/Networking/Data/Train/CouplerInteractionType.cs +++ b/Multiplayer/Networking/Data/Train/CouplerInteractionType.cs @@ -6,22 +6,23 @@ namespace Multiplayer.Networking.Data.Train; public enum CouplerInteractionType : ushort { NoAction = 0, + Start = 1, - CouplerCouple = 1, - CouplerPark = 2, - CouplerDrop = 4, - CouplerTighten = 8, - CouplerLoosen = 16, + CouplerCouple = 2, + CouplerPark = 4, + CouplerDrop = 8, + CouplerTighten = 16, + CouplerLoosen = 32, - HoseConnect = 32, - HoseDisconnect = 64, + HoseConnect = 64, + HoseDisconnect = 128, - CockOpen = 128, - CockClose = 256, + CockOpen = 256, + CockClose = 512, - CoupleViaUI = 512, - UncoupleViaUI = 1024, + CoupleViaUI = 1024, + UncoupleViaUI = 2048, - CoupleViaRemote = 2048, - UncoupleViaRemote = 4096, + CoupleViaRemote = 4096, + UncoupleViaRemote = 8192, } diff --git a/Multiplayer/Networking/Data/Train/CouplingData.cs b/Multiplayer/Networking/Data/Train/CouplingData.cs index b4469c3..3424cb3 100644 --- a/Multiplayer/Networking/Data/Train/CouplingData.cs +++ b/Multiplayer/Networking/Data/Train/CouplingData.cs @@ -69,7 +69,7 @@ public static CouplingData From(Coupler coupler) hoseConnected: coupler.hoseAndCock.IsHoseConnected, state: coupler.state, connectionNetId: coupler.IsCoupled() ? coupler.coupledTo.train.GetNetId() : (ushort)0, - connectionToFront: coupler.IsCoupled() ? coupler.coupledTo.isFrontCoupler : false, + connectionToFront: coupler.IsCoupled() && coupler.coupledTo.isFrontCoupler, preventAutoCouple: coupler.preventAutoCouple, cockOpen: coupler.IsCockOpen ); diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index f7acc2a..135a7a2 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -1033,7 +1033,7 @@ public void SendCouplerInteraction(CouplerInteractionType flags, Coupler coupler } Log($"Sending coupler interaction {flags} for {coupler?.train?.ID}"); - LogDebug(() => $"SendCouplerInteraction({flags}, {coupler?.train?.ID}, {otherCoupler?.train?.ID}) coupler isFront: {coupler?.isFrontCoupler}, otherCoupler isFront: {otherCoupler?.isFrontCoupler}"); + LogDebug(() => $"SendCouplerInteraction({flags}, {coupler?.train?.ID}, {otherCoupler?.train?.ID}) coupler isFront: {coupler?.isFrontCoupler}, otherCoupler isFront: {otherCouplerIsFront}"); SendPacketToServer(new CommonCouplerInteractionPacket { @@ -1042,7 +1042,7 @@ public void SendCouplerInteraction(CouplerInteractionType flags, Coupler coupler OtherNetId = otherCouplerNetId, IsFrontOtherCoupler = otherCouplerIsFront, Flags = (ushort)flags, - }, DeliveryMethod.ReliableUnordered); + }, DeliveryMethod.ReliableOrdered); } diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index c48f02f..6cad541 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -773,7 +773,36 @@ private void OnCommonRotateTurntablePacket(CommonRotateTurntablePacket packet, N private void OnCommonCouplerInteractionPacket(CommonCouplerInteractionPacket packet, NetPeer peer) { //todo: add validation that to ensure the client is near the coupler - this packet may also be used for remote operations and may need to factor that in in the future - SendPacketToAll(packet, DeliveryMethod.ReliableUnordered, peer); + if(NetworkedTrainCar.Get(packet.NetId, out var netTrainCar)) + { + if(netTrainCar.Server_ValidateCouplerInteraction(packet, peer)) + { + //passed validation, send to all but the originator + SendPacketToAll(packet, DeliveryMethod.ReliableOrdered, peer); + } + else + { + Multiplayer.LogDebug(() => $"OnCommonCouplerInteractionPacket([{packet.Flags}, {netTrainCar.CurrentID}, {packet.NetId}], {peer.Id}) Sending validation failure"); + //failed validation notify client + SendPacket( + peer, + new CommonCouplerInteractionPacket + { + NetId = packet.NetId, + Flags = (ushort)CouplerInteractionType.NoAction, + IsFrontCoupler = packet.IsFrontCoupler, + } + ,DeliveryMethod.ReliableOrdered + ); + } + } + else + { + Multiplayer.LogDebug(() => $"OnCommonCouplerInteractionPacket([{packet.Flags}, {netTrainCar.CurrentID}, {packet.NetId}], {peer.Id}) Sending destroy"); + //Car doesn't exist, tell client to delete it + SendDestroyTrainCar(packet.NetId, peer); + } + } private void OnCommonTrainCouplePacket(CommonTrainCouplePacket packet, NetPeer peer) { diff --git a/Multiplayer/Patches/Train/CouplerChainInteractionPatch.cs b/Multiplayer/Patches/Train/CouplerChainInteractionPatch.cs index 2ee1d5a..a424d23 100644 --- a/Multiplayer/Patches/Train/CouplerChainInteractionPatch.cs +++ b/Multiplayer/Patches/Train/CouplerChainInteractionPatch.cs @@ -14,11 +14,11 @@ private static void OnScrewButtonUsed(ChainCouplerInteraction __instance) Multiplayer.LogDebug(() => $"OnScrewButtonUsed({__instance?.couplerAdapter?.coupler?.train?.ID}) state: {__instance.state}"); - CouplerInteractionType flag = default; + CouplerInteractionType flag = CouplerInteractionType.Start; if (__instance.state == ChainCouplerInteraction.State.Attached_Tightening_Couple || __instance.state == ChainCouplerInteraction.State.Attached_Tight) - flag = CouplerInteractionType.CouplerTighten; + flag |= CouplerInteractionType.CouplerTighten; else if (__instance.state == ChainCouplerInteraction.State.Attached_Loosening_Uncouple || __instance.state == ChainCouplerInteraction.State.Attached_Loose) - flag = CouplerInteractionType.CouplerLoosen; + flag |= CouplerInteractionType.CouplerLoosen; else Multiplayer.LogDebug(() => { diff --git a/Multiplayer/Patches/Train/CouplerInterfacerPatch.cs b/Multiplayer/Patches/Train/CouplerInterfacerPatch.cs index 8dc27c8..e105c80 100644 --- a/Multiplayer/Patches/Train/CouplerInterfacerPatch.cs +++ b/Multiplayer/Patches/Train/CouplerInterfacerPatch.cs @@ -65,7 +65,7 @@ private static void SendCouple(CouplerInterfacer couplerInterfacer, float value, Coupler coupler = couplerInterfacer.GetCoupler(front); Coupler otherCoupler = null; - CouplerInteractionType interaction = CouplerInteractionType.UncoupleViaUI; + CouplerInteractionType interaction = CouplerInteractionType.Start | CouplerInteractionType.UncoupleViaUI; Multiplayer.LogDebug(() => $"CouplerInterfacer.SendCouple({couplerInterfacer?.train?.ID}, {value}, {front}) coupler: {coupler?.train?.ID}, action: {interaction}"); @@ -74,7 +74,7 @@ private static void SendCouple(CouplerInterfacer couplerInterfacer, float value, if (!coupler.IsCoupled()) { - interaction = CouplerInteractionType.CoupleViaUI; + interaction = CouplerInteractionType.Start | CouplerInteractionType.CoupleViaUI; otherCoupler = coupler.GetFirstCouplerInRange(); Multiplayer.LogDebug(() => $"CouplerInterfacer.SendCouple({couplerInterfacer?.train?.ID}, {value}, {front}) coupler: {coupler?.train?.ID}, coupler is front: {coupler?.isFrontCoupler}, otherCoupler: {otherCoupler?.train?.ID}, otherCoupler is front: {otherCoupler?.isFrontCoupler}, action: {interaction}"); diff --git a/Multiplayer/Patches/World/Items/RemoteControllerModulePatch.cs b/Multiplayer/Patches/World/Items/RemoteControllerModulePatch.cs index 41a4375..d62c924 100644 --- a/Multiplayer/Patches/World/Items/RemoteControllerModulePatch.cs +++ b/Multiplayer/Patches/World/Items/RemoteControllerModulePatch.cs @@ -16,7 +16,7 @@ public static class RemoteControllerModulePatch [HarmonyPostfix] static void RemoteControllerCouple(RemoteControllerModule __instance) { - NetworkLifecycle.Instance.Client.SendCouplerInteraction(CouplerInteractionType.CoupleViaRemote, __instance.car.frontCoupler); + NetworkLifecycle.Instance.Client.SendCouplerInteraction((CouplerInteractionType.Start | CouplerInteractionType.CoupleViaRemote), __instance.car.frontCoupler); } [HarmonyPatch(nameof(RemoteControllerModule.Uncouple))] @@ -38,7 +38,7 @@ static void Uncouple(RemoteControllerModule __instance, int selectedCoupler) Multiplayer.LogDebug(() => $"RemoteControllerModule.Uncouple({startCar?.ID}, {selectedCoupler}) nthCouplerFrom: [{nthCouplerFrom?.train?.ID}, {nthCouplerFrom?.train?.GetNetId()}]"); if (nthCouplerFrom != null) { - NetworkLifecycle.Instance.Client.SendCouplerInteraction(CouplerInteractionType.UncoupleViaRemote, nthCouplerFrom); + NetworkLifecycle.Instance.Client.SendCouplerInteraction((CouplerInteractionType.Start | CouplerInteractionType.UncoupleViaRemote), nthCouplerFrom); } } } From c16b500d6c2f009bfb70a58d500878284572d042 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 11 Jan 2025 22:52:49 +1000 Subject: [PATCH 164/188] Fix bug with persistent cache Car IDs removed before fast path cache can be cleared. Store ID and clean up properly. --- .../Networking/Train/NetworkedTrainCar.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs index 94ff7d4..fc50b48 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs @@ -69,7 +69,7 @@ public static bool TryGetFromTrainCar(TrainCar trainCar, out NetworkedTrainCar n private const int MAX_COUPLER_ITERATIONS = 10; - private string currentID; + public string CurrentID { get; private set; } public TrainCar TrainCar; public uint TicksSinceSync = uint.MaxValue; public bool HasPlayers => PlayerManager.Car == TrainCar || GetComponentInChildren() != null; @@ -226,8 +226,8 @@ public void OnDisable() trainCarsToNetworkedTrainCars.Remove(TrainCar); - trainCarIdToNetworkedTrainCars.Remove(currentID); - trainCarIdToTrainCars.Remove(currentID); + trainCarIdToNetworkedTrainCars.Remove(CurrentID); + trainCarIdToTrainCars.Remove(CurrentID); foreach (Coupler coupler in TrainCar.couplers) hoseToCoupler.Remove(coupler.hoseAndCock); @@ -267,7 +267,7 @@ public void OnDisable() } } - currentID = string.Empty; + CurrentID = string.Empty; Destroy(this); } @@ -278,9 +278,9 @@ private void OnLogicCarInitialised() //Multiplayer.LogWarning("OnLogicCarInitialised"); if (TrainCar.logicCar != null) { - currentID = TrainCar.ID; - trainCarIdToNetworkedTrainCars[currentID] = this; - trainCarIdToTrainCars[currentID] = TrainCar; + CurrentID = TrainCar.ID; + trainCarIdToNetworkedTrainCars[CurrentID] = this; + trainCarIdToTrainCars[CurrentID] = TrainCar; TrainCar.LogicCarInitialized -= OnLogicCarInitialised; } From f05eed530c938b5419bc958743dbd425ab75618e Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 11 Jan 2025 22:54:02 +1000 Subject: [PATCH 165/188] Fix null reference issue on ChatGUI --- Multiplayer/Networking/Managers/Client/NetworkClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 135a7a2..b040a41 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -870,7 +870,7 @@ private void OnClientboundDebtStatusPacket(ClientboundDebtStatusPacket packet) } private void OnCommonChatPacket(CommonChatPacket packet) { - chatGUI.ReceiveMessage(packet.message); + chatGUI?.ReceiveMessage(packet.message); } From 4b18adccfdb5d303ec965827e779443b6eef2ba8 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 11 Jan 2025 22:57:09 +1000 Subject: [PATCH 166/188] Enhance SendDestroyTrainCar() to allow packets to be sent to a specific player --- .../Networking/Managers/Server/NetworkServer.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index 6cad541..aa7eafa 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -31,6 +31,7 @@ using Multiplayer.Networking.Packets.Serverbound.Train; using Multiplayer.Networking.Packets.Unconnected; using System.Text; +using Multiplayer.Networking.Data.Train; namespace Multiplayer.Networking.Managers.Server; @@ -314,7 +315,7 @@ public void SendSpawnTrainCar(NetworkedTrainCar networkedTrainCar) SendPacketToAll(ClientboundSpawnTrainCarPacket.FromTrainCar(networkedTrainCar), DeliveryMethod.ReliableOrdered, SelfPeer); } - public void SendDestroyTrainCar(ushort netId) + public void SendDestroyTrainCar(ushort netId, NetPeer peer = null) { //ushort netID = trainCar.GetNetId(); LogDebug(() => $"SendDestroyTrainCar({netId})"); @@ -325,10 +326,12 @@ public void SendDestroyTrainCar(ushort netId) return; } - SendPacketToAll(new ClientboundDestroyTrainCarPacket - { - NetId = netId, - }, DeliveryMethod.ReliableOrdered, SelfPeer); + var packet = new ClientboundDestroyTrainCarPacket{ NetId = netId }; + + if (peer == null) + SendPacketToAll(packet, DeliveryMethod.ReliableOrdered, SelfPeer); + else + SendPacket(peer, packet, DeliveryMethod.ReliableOrdered); } public void SendTrainsetPhysicsUpdate(ClientboundTrainsetPhysicsPacket packet, bool reliable) From 4b153748eb2cd5f4f09286e84e226b02e51f7047 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 11 Jan 2025 22:57:36 +1000 Subject: [PATCH 167/188] Fix fast travel bug deleting cars client and server side --- .../Patches/Train/UnusedTrainCarDeleterPatch.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Multiplayer/Patches/Train/UnusedTrainCarDeleterPatch.cs b/Multiplayer/Patches/Train/UnusedTrainCarDeleterPatch.cs index e8d476c..09a6e67 100644 --- a/Multiplayer/Patches/Train/UnusedTrainCarDeleterPatch.cs +++ b/Multiplayer/Patches/Train/UnusedTrainCarDeleterPatch.cs @@ -9,6 +9,7 @@ using DV.ThingTypes; using DV.Logic.Job; using DV.Utils; +using Multiplayer.Components.Networking; namespace Multiplayer.Patches.Train; @@ -68,6 +69,15 @@ public static IEnumerable Transpiler(IEnumerable Date: Sat, 11 Jan 2025 22:58:11 +1000 Subject: [PATCH 168/188] Fix IPv6 connection issue --- Multiplayer/Networking/Managers/NetworkManager.cs | 2 +- .../Networking/Managers/Server/NetworkServer.cs | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Multiplayer/Networking/Managers/NetworkManager.cs b/Multiplayer/Networking/Managers/NetworkManager.cs index ea147f5..e5d34f8 100644 --- a/Multiplayer/Networking/Managers/NetworkManager.cs +++ b/Multiplayer/Networking/Managers/NetworkManager.cs @@ -26,7 +26,7 @@ protected NetworkManager(Settings settings) netManager = new NetManager(this) { DisconnectTimeout = 10000, - ReconnectDelay = 1000, + //ReconnectDelay = 1000, UnconnectedMessagesEnabled = true, BroadcastReceiveEnabled = true, diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index aa7eafa..df1365d 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -88,12 +88,12 @@ public bool Start(int port) Multiplayer.Log($"Starting server..."); //Try to get our static IPv6 Address we will need this for IPv6 NAT punching to be reliable - //if (IPAddress.TryParse(LobbyServerManager.GetStaticIPv6Address(), out IPAddress ipv6Address)) - //{ - // //start the connection, IPv4 messages can come from anywhere, IPv6 messages need to specifically come from the static IPv6 - // //return netManager.Start(IPAddress.Any, ipv6Address,port); - // return netManager.Start(IPAddress.Any, IPAddress.IPv6Any, port); - //} + if (IPAddress.TryParse(LobbyServerManager.GetStaticIPv6Address(), out IPAddress ipv6Address)) + { + //start the connection, IPv4 messages can come from anywhere, IPv6 messages need to specifically come from the static IPv6 + return netManager.Start(IPAddress.Any, ipv6Address,port); + //return netManager.Start(IPAddress.Any, IPAddress.IPv6Any, port); + } //we're not running IPv6, start as normal return netManager.Start(port); From 715d6dd111d86cab899a03d6939253001c83fff7 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 11 Jan 2025 22:59:13 +1000 Subject: [PATCH 169/188] Prepare for testing and release --- Multiplayer/Multiplayer.csproj | 2 +- info.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Multiplayer/Multiplayer.csproj b/Multiplayer/Multiplayer.csproj index d11332a..d885efe 100644 --- a/Multiplayer/Multiplayer.csproj +++ b/Multiplayer/Multiplayer.csproj @@ -3,7 +3,7 @@ net48 latest Multiplayer - 0.1.9.6 + 0.1.9.7 diff --git a/info.json b/info.json index 53b0b64..a796e06 100644 --- a/info.json +++ b/info.json @@ -1,6 +1,6 @@ { "Id": "Multiplayer", - "Version": "0.1.9.6", + "Version": "0.1.9.7", "DisplayName": "Multiplayer", "Author": "Insprill, Macka, Morm", "EntryMethod": "Multiplayer.Multiplayer.Load", From 0e39fa2258323169a434f0446f44e0d45338f54d Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 11 Jan 2025 23:34:57 +1000 Subject: [PATCH 170/188] Readded quotes --- locale.csv | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/locale.csv b/locale.csv index 1032073..5852458 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.,Изберете игра за присъединяване,选择要加入的游戏,選擇要加入的遊戲,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.,Actualise 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/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,Entrez 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),Entrez 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 за замовчуванням) @@ -39,8 +39,8 @@ sb/no_servers,Label for no servers,No servers found. Refresh or start your own!, 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!,Сервери не знайдено. Оновіть або почніть власний! sb/info/title,Title for server browser info,Server Browser Info,,服务器浏览器介绍,,,,,,Informations du navigateur de serveurs,,,Szerverböngésző információ,,,,,,,,,,,,,, -sb/info/content,Content for server browser info,"Welcome to Derail Valley Multiplayer Mod!\n\nThe server list refreshes automatically every {0} seconds, but you can refresh manually once every {1} seconds.",,欢迎来到脱轨山谷的联机模式!\n\n服务器列表会在每{0}秒刷新,但是你可以手动让它在每{1}秒刷新,,,,,,"Bienvenue dans le mod multijoueur de Derail Valley !\n\nLa liste des serveurs est mise à jour automatiquement toutes les {0} secondes, mais vous pouvez la rafraîchir manuellement toutes les {1} secondes.",,,"Üdvözli a Derail Valley Multiplayer Mod!\n\nA szerverlista automatikusan frissül {0} másodpercenként, de manuálisan is frissíthetsz minden {1} másodpercet.",,,,,,,,,,,,,, -sb/connecting,Connecting dialogue,"Connecting, please wait...\nAttempt: {0}",,正在连接中,请稍候片刻\n尝试次数: {0},,,,,,"Connexion, merci de patienter...\nEssai : {0}",,,"Csatlakozás, kérjük, várjon...\nKísérlet: {0}",,,,,,,,,,,,,, +sb/info/content,Content for server browser info,"Welcome to Derail Valley Multiplayer Mod!\n\nThe server list refreshes automatically every {0} seconds, but you can refresh manually once every {1} seconds.",,"欢迎来到脱轨山谷的联机模式!\n\n服务器列表会在每{0}秒刷新,但是你可以手动让它在每{1}秒刷新",,,,,,"Bienvenue dans le mod multijoueur de Derail Valley !\n\nLa liste des serveurs est mise à jour automatiquement toutes les {0} secondes, mais vous pouvez la rafraîchir manuellement toutes les {1} secondes.",,,"Üdvözli a Derail Valley Multiplayer Mod!\n\nA szerverlista automatikusan frissül {0} másodpercenként, de manuálisan is frissíthetsz minden {1} másodpercet.",,,,,,,,,,,,,, +sb/connecting,Connecting dialogue,"Connecting, please wait...\nAttempt: {0}",,"正在连接中,请稍候片刻\n尝试次数: {0}",,,,,,"Connexion, merci de patienter...\nEssai : {0}",,,"Csatlakozás, kérjük, várjon...\nKísérlet: {0}",,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,, ,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,Ведуча гра @@ -60,13 +60,13 @@ host/start,Maximum players slider label,Start,Започнете,开始,開始,S host/start__tooltip,Maximum players slider tooltip,Start the server.,Стартирайте сървъра.,启动服务器,啟動伺服器。,Spusťte server.,Start serveren.,Start de server.,Käynnistä palvelin.,Démarre le serveur.,Starten Sie den Server.,सर्वर प्रारंभ करें.,Szerver Indul!,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/instructions/first,Instructions for the host 1,"First time hosts, please see the {0}Hosting{1} section of our Wiki.",,"第一次主持游戏的话, 请看我们wiki的{0}Hosting{1} 模块",,,,,,"La première fois que vous hébergez, merci de consulter la section {0}Hébergement{1} sur notre Wiki.",,,"Az első házigazdák, kérjük, tekintse meg Wikink {0}Hosting{1} részét.",,,,,,,,,,,,,, -host/instructions/mod_warning,Instructions for the host 2,Using other mods may cause unexpected behaviour including de-syncs. See {0}Mod Compatibility{1} for more info.,,同时使用其他模组可能会导致游戏出错,比如物品不同步, 看 {0}Mod Compatibility{1} 模块来获取更多信息,,,,,,"L’utilisation d’autres mods peut causer un comportement inattendu, y-compris des désynchronisation. Consultez les {0}Mods compatibles{1} pour plus d’information.",,,"Más modok használata váratlan viselkedést okozhat, beleértve a szinkronizálást. További információért lásd a {0}Modkompatibilitást{1}.",,,,,,,,,,,,,, -host/instructions/recommend,Instructions for the host 3,It is recommended that other mods are disabled and Derail Valley restarted prior to playing in multiplayer.,,推荐你卸载其他模组并重启游戏后,再进行联机,,,,,,Il est recommandé de désactiver les autres mods et de redémarrer Derail Valley avant de joueur en multijoueur.,,,"Javasoljuk, hogy tiltsa le a többi modot, és indítsa újra a Derail Valleyt, mielőtt többjátékos módban játszana.",,,,,,,,,,,,,, +host/instructions/mod_warning,Instructions for the host 2,Using other mods may cause unexpected behaviour including de-syncs. See {0}Mod Compatibility{1} for more info.,,"同时使用其他模组可能会导致游戏出错,比如物品不同步, 看 {0}Mod Compatibility{1} 模块来获取更多信息",,,,,,"L’utilisation d’autres mods peut causer un comportement inattendu, y-compris des désynchronisation. Consultez les {0}Mods compatibles{1} pour plus d’information.",,,"Más modok használata váratlan viselkedést okozhat, beleértve a szinkronizálást. További információért lásd a {0}Modkompatibilitást{1}.",,,,,,,,,,,,,, +host/instructions/recommend,Instructions for the host 3,It is recommended that other mods are disabled and Derail Valley restarted prior to playing in multiplayer.,,"推荐你卸载其他模组并重启游戏后,再进行联机",,,,,,Il est recommandé de désactiver les autres mods et de redémarrer Derail Valley avant de joueur en multijoueur.,,,"Javasoljuk, hogy tiltsa le a többi modot, és indítsa újra a Derail Valleyt, mielőtt többjátékos módban játszana.",,,,,,,,,,,,,, host/instructions/signoff,Instructions for the host 4,We hope to have your favourite mods compatible with multiplayer in the future.,,我们希望未来能让你装联机模组的同时也能玩其他模组,,,,,,Nous espérons avoir vos mods favoris compatibles avec le multijoueur dans le futur.,,,"Reméljük, hogy kedvenc modjai a jövőben kompatibilisek lesznek a többjátékos játékkal.",,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,, ,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/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} @@ -89,7 +89,7 @@ linfo/wait_for_server,Text shown in the loading screen.,Waiting for server to lo 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,Синхронізація стану світу ,,,,,,,,,,,,,,,,,,,,,,,,,,, ,Chat,,,,,,,,,,,,,,,,,,,,,,,,,, -chat/placeholder,Chat input placeholder,Type a message and press Enter!,,在此输入文字,按回车发送,,,,,,Tapez un message et appuyez sur Entrée !,,,Írjon be egy üzenetet és nyomja meg az Entert!,,,,,,,,,,,,,, +chat/placeholder,Chat input placeholder,Type a message and press Enter!,,"在此输入文字,按回车发送",,,,,,Tapez un message et appuyez sur Entrée !,,,Írjon be egy üzenetet és nyomja meg az Entert!,,,,,,,,,,,,,, chat/help/available,Chat help info available commands,Available commands:,,可用命令:,,,,,,Commandes disponibles :,,,Elérhető parancsok:,,,,,,,,,,,,,, chat/help/servermsg,Chat help send message as server,Send a message as the server (host only),,以服务器的身份发消息(仅限房主),,,,,,Envoyer un message au nom du serveur (hôte uniquement),,,Üzenet küldése szerverként (csak gazdagép),,,,,,,,,,,,,, chat/help/whispermsg,Chat help whisper to a player,Whisper to a player,,向一位玩家说悄悄话,,,,,,Chuchoter à un joueur,,,Suttogj egy játékosnak,,,,,,,,,,,,,, From ea42a103e578b97c57a8b139ca21cd4e63ab5433 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 12 Jan 2025 14:21:07 +1000 Subject: [PATCH 171/188] Improve job validation sync --- ...atorJobPatch.cs => BookletCreatorPatch.cs} | 23 +++++++++++++++++-- Multiplayer/Patches/Jobs/JobValidatorPatch.cs | 2 +- 2 files changed, 22 insertions(+), 3 deletions(-) rename Multiplayer/Patches/Jobs/{BookletCreatorJobPatch.cs => BookletCreatorPatch.cs} (66%) diff --git a/Multiplayer/Patches/Jobs/BookletCreatorJobPatch.cs b/Multiplayer/Patches/Jobs/BookletCreatorPatch.cs similarity index 66% rename from Multiplayer/Patches/Jobs/BookletCreatorJobPatch.cs rename to Multiplayer/Patches/Jobs/BookletCreatorPatch.cs index 963aa2b..9bccf2d 100644 --- a/Multiplayer/Patches/Jobs/BookletCreatorJobPatch.cs +++ b/Multiplayer/Patches/Jobs/BookletCreatorPatch.cs @@ -11,7 +11,7 @@ namespace Multiplayer.Patches.Jobs; [HarmonyPatch(typeof(BookletCreator))] -public static class BookletCreatorJob_Patch +public static class BookletCreator_Patch { [HarmonyPatch(nameof(BookletCreator.CreateJobOverview))] [HarmonyPostfix] @@ -41,7 +41,7 @@ private static void CreateJobBooklet(JobBooklet __result, Job job) if (!NetworkedJob.TryGetFromJob(job, out NetworkedJob networkedJob)) { - Multiplayer.LogError($"BookletCreatorJob_Patch.CreateJobBooklet() NetworkedJob not found for Job ID: {job.ID}"); + Multiplayer.LogError($"CreateJobBooklet() NetworkedJob not found for Job ID: {job.ID}"); } else { @@ -50,4 +50,23 @@ private static void CreateJobBooklet(JobBooklet __result, Job job) networkedJob.JobBooklet = netItem; } } + + [HarmonyPatch(nameof(BookletCreator.CreateJobReport))] + [HarmonyPostfix] + private static void CreateJobReport(JobReport __result, Job job) + { + if (!NetworkLifecycle.Instance.IsHost()) + return; + + if (!NetworkedJob.TryGetFromJob(job, out NetworkedJob networkedJob)) + { + Multiplayer.LogError($"CreateJobReport() NetworkedJob not found for Job ID: {job.ID}"); + } + else + { + NetworkedItem netItem = __result.GetOrAddComponent(); + netItem.Initialize(__result, 0, false); + networkedJob.JobReport = netItem; + } + } } diff --git a/Multiplayer/Patches/Jobs/JobValidatorPatch.cs b/Multiplayer/Patches/Jobs/JobValidatorPatch.cs index 35d5243..704e499 100644 --- a/Multiplayer/Patches/Jobs/JobValidatorPatch.cs +++ b/Multiplayer/Patches/Jobs/JobValidatorPatch.cs @@ -108,7 +108,7 @@ private static void SendValidationRequest(JobValidator validator,NetworkedJob ne } private static IEnumerator AwaitResponse(JobValidator validator, NetworkedJob networkedJob) { - yield return new WaitForSecondsRealtime((NetworkLifecycle.Instance.Client.Ping * 3f)/1000); + yield return new WaitForSecondsRealtime((NetworkLifecycle.Instance.Client.Ping * 4f)/1000); bool received = networkedJob.ValidatorResponseReceived; bool accepted = networkedJob.ValidationAccepted; From 64ee11bda7403d5e927e1cb4e52a16ca53966983 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 12 Jan 2025 14:25:38 +1000 Subject: [PATCH 172/188] Sync brake heating states --- .../Networking/Train/NetworkedTrainCar.cs | 43 +++++++++++++++---- .../Managers/Client/NetworkClient.cs | 6 +-- .../Managers/Server/NetworkServer.cs | 9 ++-- ...s => ClientboundBrakeStateUpdatePacket.cs} | 6 ++- 4 files changed, 48 insertions(+), 16 deletions(-) rename Multiplayer/Networking/Packets/Clientbound/Train/{ClientboundBrakePressureUpdatePacket.cs => ClientboundBrakeStateUpdatePacket.cs} (57%) diff --git a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs index fc50b48..4477114 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs @@ -13,6 +13,7 @@ using Multiplayer.Components.Networking.Player; using Multiplayer.Networking.Data; using Multiplayer.Networking.Data.Train; +using Multiplayer.Networking.Packets.Clientbound.Train; using Multiplayer.Networking.Packets.Common.Train; using Multiplayer.Utils; using UnityEngine; @@ -85,8 +86,11 @@ public static bool TryGetFromTrainCar(TrainCar trainCar, out NetworkedTrainCar n private HashSet dirtyPorts; private Dictionary lastSentPortValues; private HashSet dirtyFuses; + private bool handbrakeDirty; private bool mainResPressureDirty; + private bool brakeOverheatDirty; + public bool BogieTracksDirty; private bool cargoDirty; private bool cargoIsLoading; @@ -204,6 +208,7 @@ public void Start() TrainCar.CarDamage.CarEffectiveHealthStateUpdate += Server_CarHealthUpdate; brakeSystem.MainResPressureChanged += Server_MainResUpdate; + brakeSystem.heatController.OverheatingActiveStateChanged += Server_BrakeHeatUpdate; if (firebox != null) { @@ -252,7 +257,10 @@ public void OnDisable() TrainCar.CarDamage.CarEffectiveHealthStateUpdate -= Server_CarHealthUpdate; if(brakeSystem != null) + { brakeSystem.MainResPressureChanged -= Server_MainResUpdate; + brakeSystem.heatController.OverheatingActiveStateChanged -= Server_BrakeHeatUpdate; + } if (firebox != null) { @@ -393,6 +401,11 @@ private void Server_MainResUpdate(float normalizedPressure, float pressure) mainResPressureDirty = true; } + private void Server_BrakeHeatUpdate(bool overheatActive) + { + brakeOverheatDirty = true; + } + private void Server_FireboxUpdate(float normalizedPressure, float pressure) { fireboxDirty = true; @@ -403,7 +416,7 @@ private void Server_OnTick(uint tick) if (UnloadWatcher.isUnloading) return; - Server_SendBrakePressures(); + Server_SendBrakeStates(); Server_SendFireBoxState(); //Server_SendCouplers(); Server_SendCables(); @@ -413,13 +426,18 @@ private void Server_OnTick(uint tick) TicksSinceSync++; //keep track of last full sync } - private void Server_SendBrakePressures() + private void Server_SendBrakeStates() { - if (!mainResPressureDirty) + if (!mainResPressureDirty && !brakeOverheatDirty) return; mainResPressureDirty = false; - NetworkLifecycle.Instance.Server.SendBrakePressures(NetId, brakeSystem.mainReservoirPressure, brakeSystem.brakePipePressure, brakeSystem.brakeCylinderPressure); + var hc = brakeSystem.heatController; + NetworkLifecycle.Instance.Server.SendBrakeState( + NetId, + brakeSystem.mainReservoirPressure, brakeSystem.brakePipePressure, brakeSystem.brakeCylinderPressure, + hc.overheatPercentage, hc.overheatReductionFactor, hc.temperature + ); } private void Server_SendFireBoxState() @@ -1096,7 +1114,7 @@ public void Client_ReceiveTrainPhysicsUpdate(in TrainsetMovementPart movementPar } } - public void Client_ReceiveBrakePressureUpdate(float mainReservoirPressure, float brakePipePressure, float brakeCylinderPressure) + public void Client_ReceiveBrakeStateUpdate(ClientboundBrakeStateUpdatePacket packet) { if (brakeSystem == null) return; @@ -1104,12 +1122,19 @@ public void Client_ReceiveBrakePressureUpdate(float mainReservoirPressure, float if (!hasSimFlow) return; - brakeSystem.SetMainReservoirPressure(mainReservoirPressure); + brakeSystem.SetMainReservoirPressure(packet.MainReservoirPressure); + + brakeSystem.brakePipePressure = packet.BrakePipePressure; + brakeSystem.brakeset.pipePressure = packet.BrakePipePressure; - brakeSystem.brakePipePressure = brakePipePressure; - brakeSystem.brakeset.pipePressure = brakePipePressure; + brakeSystem.brakeCylinderPressure = packet.BrakeCylinderPressure; + + if (brakeSystem.heatController == null) + return; - brakeSystem.brakeCylinderPressure = brakeCylinderPressure; + brakeSystem.heatController.overheatPercentage = packet.OverheatPercent; + brakeSystem.heatController.overheatReductionFactor = packet.OverheatReductionFactor; + brakeSystem.heatController.temperature = packet.Temperature; } private void Client_OnAddCoal(float coalMassDelta) diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index b040a41..04b9714 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -139,7 +139,7 @@ protected override void Subscribe() netPacketProcessor.SubscribeReusable(OnCommonHandbrakePositionPacket); netPacketProcessor.SubscribeReusable(OnCommonSimFlowPacket); netPacketProcessor.SubscribeReusable(OnCommonTrainFusesPacket); - netPacketProcessor.SubscribeReusable(OnClientboundBrakePressureUpdatePacket); + netPacketProcessor.SubscribeReusable(OnClientboundBrakeStateUpdatePacket); netPacketProcessor.SubscribeReusable(OnClientboundFireboxStatePacket); netPacketProcessor.SubscribeReusable(OnClientboundCargoStatePacket); netPacketProcessor.SubscribeReusable(OnClientboundCarHealthUpdatePacket); @@ -705,13 +705,13 @@ private void OnCommonTrainFusesPacket(CommonTrainFusesPacket packet) networkedTrainCar.Common_UpdateFuses(packet); } - private void OnClientboundBrakePressureUpdatePacket(ClientboundBrakePressureUpdatePacket packet) + private void OnClientboundBrakeStateUpdatePacket(ClientboundBrakeStateUpdatePacket packet) { if (!NetworkedTrainCar.Get(packet.NetId, out NetworkedTrainCar networkedTrainCar)) return; - networkedTrainCar.Client_ReceiveBrakePressureUpdate(packet.MainReservoirPressure, packet.BrakePipePressure, packet.BrakeCylinderPressure); + networkedTrainCar.Client_ReceiveBrakeStateUpdate(packet); //LogDebug(() => $"Received Brake Pressures netId {packet.NetId}: {packet.MainReservoirPressure}, {packet.IndependentPipePressure}, {packet.BrakePipePressure}, {packet.BrakeCylinderPressure}"); } diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index df1365d..483bfed 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -339,14 +339,17 @@ public void SendTrainsetPhysicsUpdate(ClientboundTrainsetPhysicsPacket packet, b SendPacketToAll(packet, reliable ? DeliveryMethod.ReliableOrdered : DeliveryMethod.Unreliable, SelfPeer); } - public void SendBrakePressures(ushort netId, float mainReservoirPressure, float brakePipePressure, float brakeCylinderPressure) + public void SendBrakeState(ushort netId, float mainReservoirPressure, float brakePipePressure, float brakeCylinderPressure, float overheatPercent, float overheatReductionFactor, float temperature) { - SendPacketToAll(new ClientboundBrakePressureUpdatePacket + SendPacketToAll(new ClientboundBrakeStateUpdatePacket { NetId = netId, MainReservoirPressure = mainReservoirPressure, BrakePipePressure = brakePipePressure, - BrakeCylinderPressure = brakeCylinderPressure + BrakeCylinderPressure = brakeCylinderPressure, + OverheatPercent = overheatPercent, + OverheatReductionFactor = overheatReductionFactor, + Temperature = temperature }, DeliveryMethod.ReliableOrdered, SelfPeer); //Multiplayer.LogDebug(()=> $"Sending Brake Pressures netId {netId}: {mainReservoirPressure}, {independentPipePressure}, {brakePipePressure}, {brakeCylinderPressure}"); diff --git a/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundBrakePressureUpdatePacket.cs b/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundBrakeStateUpdatePacket.cs similarity index 57% rename from Multiplayer/Networking/Packets/Clientbound/Train/ClientboundBrakePressureUpdatePacket.cs rename to Multiplayer/Networking/Packets/Clientbound/Train/ClientboundBrakeStateUpdatePacket.cs index 5516356..d75f571 100644 --- a/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundBrakePressureUpdatePacket.cs +++ b/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundBrakeStateUpdatePacket.cs @@ -1,9 +1,13 @@ namespace Multiplayer.Networking.Packets.Clientbound.Train; -public class ClientboundBrakePressureUpdatePacket +public class ClientboundBrakeStateUpdatePacket { public ushort NetId { get; set; } public float MainReservoirPressure { get; set; } public float BrakePipePressure { get; set; } public float BrakeCylinderPressure { get; set; } + + public float OverheatPercent { get; set; } + public float OverheatReductionFactor { get; set; } + public float Temperature { get; set; } } From c0d59d64c49b205139059ad3838e520908f756b3 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 12 Jan 2025 14:27:17 +1000 Subject: [PATCH 173/188] Improve job update handling --- .../World/NetworkedStationController.cs | 83 +++++++++++-------- 1 file changed, 50 insertions(+), 33 deletions(-) diff --git a/Multiplayer/Components/Networking/World/NetworkedStationController.cs b/Multiplayer/Components/Networking/World/NetworkedStationController.cs index bed6a24..4d08c82 100644 --- a/Multiplayer/Components/Networking/World/NetworkedStationController.cs +++ b/Multiplayer/Components/Networking/World/NetworkedStationController.cs @@ -6,6 +6,7 @@ using DV.Logic.Job; using DV.ServicePenalty; using DV.Utils; +using DV.ThingTypes; using Multiplayer.Components.Networking.Jobs; using Multiplayer.Components.Networking.Train; using Multiplayer.Networking.Data; @@ -254,7 +255,7 @@ private void AddJob(JobData jobData) NetworkedJobs.Add(networkedJob); - if (networkedJob.Job.State == DV.ThingTypes.JobState.Available) + if (networkedJob.Job.State == JobState.Available) { StationController.logicStation.AddJobToStation(newJob); StationController.processedNewJobs.Add(newJob); @@ -264,7 +265,7 @@ private void AddJob(JobData jobData) GenerateOverview(networkedJob, jobData.ItemNetID, jobData.ItemPosition); } } - else if (networkedJob.Job.State == DV.ThingTypes.JobState.InProgress) + else if (networkedJob.Job.State == JobState.InProgress) { takenJobs.Add(newJob); } @@ -385,66 +386,83 @@ private void UpdateJobOverview(NetworkedJob netJob, JobUpdateStruct job) } } - private void HandleJobStateChange(NetworkedJob netJob, JobUpdateStruct job) + private void HandleJobStateChange(NetworkedJob netJob, JobUpdateStruct updateData) { JobValidator validator = null; NetworkedItem netItem; - NetworkLifecycle.Instance.Client.LogDebug(()=> $"NetworkedStation.HandleJobStateChange() {job.JobNetID}, {job.ValidationStationId}"); + string jobIdStr = $"[{netJob?.Job?.ID}, {netJob.NetId}]"; - if (job.ItemNetID != 0 && job.ValidationStationId != 0) - if (Get(job.ValidationStationId, out var netStation)) - validator = netStation.JobValidator; + NetworkLifecycle.Instance.Client.LogDebug(() => $"HandleJobStateChange({jobIdStr}) Current state: {netJob?.Job?.State}, New state: {updateData.JobState}, ValidationStationNetId: {updateData.ValidationStationId}, ItemNetId: {updateData.ItemNetID}"); + + bool shouldPrint = updateData.JobState == JobState.InProgress || updateData.JobState == JobState.Completed; + bool canPrint = true; - if ((netJob.Job.State == DV.ThingTypes.JobState.InProgress || - netJob.Job.State == DV.ThingTypes.JobState.Completed) && - validator == null) + if (shouldPrint) { - NetworkLifecycle.Instance.Client.LogError($"NetworkedStation.HandleJobStateChange() jobNetId: {job.JobNetID}, Validator required and not found!"); - return; + if (updateData.ValidationStationId != 0 && Get(updateData.ValidationStationId, out var netStation)) + { + validator = netStation.JobValidator; + } + else + { + NetworkLifecycle.Instance.Client.LogError($"HandleJobStateChange({jobIdStr}) Validator not found or data missing! Validator ID: {updateData.ValidationStationId}"); + canPrint = false; + } + + if (updateData.ItemNetID == 0) + { + NetworkLifecycle.Instance.Client.LogError($"HandleJobStateChange({jobIdStr}) Missing item data!"); + canPrint = false; + } } + bool printed = false; switch (netJob.Job.State) { - case DV.ThingTypes.JobState.InProgress: + case JobState.InProgress: availableJobs.Remove(netJob.Job); takenJobs.Add(netJob.Job); - JobBooklet jobBooklet = BookletCreator.CreateJobBooklet(netJob.Job, validator.bookletPrinter.spawnAnchor.position, validator.bookletPrinter.spawnAnchor.rotation, WorldMover.OriginShiftParent, true); - - netItem = jobBooklet.GetOrAddComponent(); - netItem.Initialize(jobBooklet, job.ItemNetID, false); - netJob.JobBooklet = netItem; - printed = true; + if (canPrint) + { + JobBooklet jobBooklet = BookletCreator.CreateJobBooklet(netJob.Job, validator.bookletPrinter.spawnAnchor.position, validator.bookletPrinter.spawnAnchor.rotation, WorldMover.OriginShiftParent, true); + netItem = jobBooklet.GetOrAddComponent(); + netItem.Initialize(jobBooklet, updateData.ItemNetID, false); + netJob.JobBooklet = netItem; + printed = true; + } netJob.JobOverview?.GetTrackedItem()?.DestroyJobOverview(); break; - case DV.ThingTypes.JobState.Completed: + case JobState.Completed: takenJobs.Remove(netJob.Job); completedJobs.Add(netJob.Job); - DisplayableDebt displayableDebt = SingletonBehaviour.Instance.LastStagedJobDebt; - JobReport jobReport = BookletCreator.CreateJobReport(netJob.Job, displayableDebt, validator.bookletPrinter.spawnAnchor.position, validator.bookletPrinter.spawnAnchor.rotation, WorldMover.OriginShiftParent); - - netItem = jobReport.GetOrAddComponent(); - netItem.Initialize(jobReport, job.ItemNetID, false); - netJob.AddReport(netItem); - printed = true; + if (canPrint) + { + DisplayableDebt displayableDebt = SingletonBehaviour.Instance.LastStagedJobDebt; + JobReport jobReport = BookletCreator.CreateJobReport(netJob.Job, displayableDebt, validator.bookletPrinter.spawnAnchor.position, validator.bookletPrinter.spawnAnchor.rotation, WorldMover.OriginShiftParent); + netItem = jobReport.GetOrAddComponent(); + netItem.Initialize(jobReport, updateData.ItemNetID, false); + netJob.AddReport(netItem); + printed = true; + } StartCoroutine(UpdateCarPlates(netJob.JobCars, string.Empty)); netJob.JobBooklet?.GetTrackedItem()?.DestroyJobBooklet(); break; - case DV.ThingTypes.JobState.Abandoned: + case JobState.Abandoned: takenJobs.Remove(netJob.Job); abandonedJobs.Add(netJob.Job); StartCoroutine(UpdateCarPlates(netJob.JobCars, string.Empty)); break; - case DV.ThingTypes.JobState.Expired: + case JobState.Expired: //if (availableJobs.Contains(netJob.Job)) // availableJobs.Remove(netJob.Job); @@ -454,13 +472,12 @@ private void HandleJobStateChange(NetworkedJob netJob, JobUpdateStruct job) break; default: - NetworkLifecycle.Instance.Client.LogError($"NetworkedStation.UpdateJobs() Unrecognised Job State for JobId: {job.JobNetID}, {netJob.Job.ID}"); + NetworkLifecycle.Instance.Client.LogError($"HandleJobStateChange({jobIdStr}) Unrecognised Job State: {updateData.JobState}"); break; } - if (printed && validator != null) + if (printed) { - Multiplayer.Log($"NetworkedStation.UpdateJobs() jobNetId: {job.JobNetID}, Playing sounds"); netJob.ValidatorResponseReceived = true; netJob.ValidationAccepted = true; validator.jobValidatedSound.Play(validator.bookletPrinter.spawnAnchor.position, 1f, 1f, 0f, 1f, 500f, default, null, validator.transform, false, 0f, null); @@ -511,7 +528,7 @@ public static IEnumerator UpdateCarPlates(List carNetIds, string jobId) trainCar.trainPlatesCtrl?.trainCarPlates != null && trainCar.trainPlatesCtrl.trainCarPlates.Count > 0) { - Multiplayer.LogDebug(() => $"UpdateCarPlates({jobId}) car: {carNetId}, frameCount: {frameCounter}. Calling Update"); + //Multiplayer.LogDebug(() => $"UpdateCarPlates({jobId}) car: {carNetId}, frameCount: {frameCounter}. Calling Update"); trainCar.UpdateJobIdOnCarPlates(jobId); break; } From 255f1049e9de5f374f75791d6eb6cf9390cf322a Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 12 Jan 2025 14:28:38 +1000 Subject: [PATCH 174/188] Improve jittering of derailed cars --- .../Components/Networking/Train/NetworkTrainsetWatcher.cs | 1 + .../Components/Networking/Train/NetworkedTrainCar.cs | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs b/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs index 08c0b29..cf29ccf 100644 --- a/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs +++ b/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs @@ -121,6 +121,7 @@ private void Server_TickSet(Trainset set, uint tick) } trainsetParts[i] = new TrainsetMovementPart( + networkedTrainCar.NetId, trainCar.GetForwardSpeed(), trainCar.stress.slowBuildUpStress, BogieData.FromBogie(trainCar.Bogies[0], networkedTrainCar.BogieTracksDirty), diff --git a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs index 4477114..a908e37 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs @@ -1086,6 +1086,9 @@ public void Client_ReceiveTrainPhysicsUpdate(in TrainsetMovementPart movementPar //Multiplayer.LogDebug(() => $"Client_ReceiveTrainPhysicsUpdate({TrainCar.ID}, {tick}): is RigidBody"); TrainCar.Derail(); TrainCar.stress.ResetTrainStress(); + if (TrainCar.rb != null) + TrainCar.rb.constraints = RigidbodyConstraints.FreezeAll; + Client_trainRigidbodyQueue.ReceiveSnapshot(movementPart.RigidbodySnapshot, tick); } else @@ -1112,6 +1115,9 @@ public void Client_ReceiveTrainPhysicsUpdate(in TrainsetMovementPart movementPar } + + if (!TrainCar.derailed && TrainCar.rb != null) + TrainCar.rb.constraints = RigidbodyConstraints.None; } public void Client_ReceiveBrakeStateUpdate(ClientboundBrakeStateUpdatePacket packet) From 084df26c9194ed005b9eed3fb2778aeede402e85 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 12 Jan 2025 14:29:18 +1000 Subject: [PATCH 175/188] Improve position sync when there's a coupling de-sync --- .../Networking/Train/NetworkTrainsetWatcher.cs | 8 +++++++- .../Data/Train/TrainsetMovementPart.cs | 17 +++++++++++++---- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs b/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs index cf29ccf..d5d6752 100644 --- a/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs +++ b/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs @@ -103,7 +103,7 @@ private void Server_TickSet(Trainset set, uint tick) if (trainCar.derailed) { - trainsetParts[i] = new TrainsetMovementPart(RigidbodySnapshot.From(trainCar.rb)); + trainsetParts[i] = new TrainsetMovementPart(networkedTrainCar.NetId, RigidbodySnapshot.From(trainCar.rb)); } else { @@ -157,6 +157,12 @@ public void Client_HandleTrainsetPhysicsUpdate(ClientboundTrainsetPhysicsPacket //log the discrepancies Multiplayer.LogWarning( $"Received {nameof(ClientboundTrainsetPhysicsPacket)} for trainset with FirstNetId: {packet.FirstNetId} and LastNetId: {packet.LastNetId} with {packet.TrainsetParts.Length} parts, but trainset has {set.cars.Count} parts"); + + for (int i = 0; i < packet.TrainsetParts.Length; i++) + { + if (NetworkedTrainCar.Get(packet.TrainsetParts[i].NetId ,out NetworkedTrainCar networkedTrainCar)) + networkedTrainCar.Client_ReceiveTrainPhysicsUpdate(in packet.TrainsetParts[i], packet.Tick); + } return; } diff --git a/Multiplayer/Networking/Data/Train/TrainsetMovementPart.cs b/Multiplayer/Networking/Data/Train/TrainsetMovementPart.cs index 84b5eef..e3d12d3 100644 --- a/Multiplayer/Networking/Data/Train/TrainsetMovementPart.cs +++ b/Multiplayer/Networking/Data/Train/TrainsetMovementPart.cs @@ -6,6 +6,7 @@ namespace Multiplayer.Networking.Data.Train; public readonly struct TrainsetMovementPart { + public readonly ushort NetId; public readonly MovementType typeFlag; public readonly float Speed; public readonly float SlowBuildUpStress; @@ -23,8 +24,10 @@ public enum MovementType : byte Position = 4 } - public TrainsetMovementPart(float speed, float slowBuildUpStress, BogieData bogie1, BogieData bogie2, Vector3? position = null, Quaternion? rotation = null) + public TrainsetMovementPart(ushort netId, float speed, float slowBuildUpStress, BogieData bogie1, BogieData bogie2, Vector3? position = null, Quaternion? rotation = null) { + NetId = netId; + typeFlag = MovementType.Physics; //no rigid body data Speed = speed; @@ -43,8 +46,9 @@ public TrainsetMovementPart(float speed, float slowBuildUpStress, BogieData bogi } } - public TrainsetMovementPart(RigidbodySnapshot rigidbodySnapshot) + public TrainsetMovementPart(ushort netId, RigidbodySnapshot rigidbodySnapshot) { + NetId = netId; typeFlag = MovementType.RigidBody; //rigid body data //Multiplayer.LogDebug(() => $"new TrainsetMovementPart() RigidBody"); @@ -56,6 +60,8 @@ public TrainsetMovementPart(RigidbodySnapshot rigidbodySnapshot) public static void Serialize(NetDataWriter writer, TrainsetMovementPart data) #pragma warning restore EPS05 // Use in-modifier for a readonly struct { + writer.Put(data.NetId); + writer.Put((byte)data.typeFlag); //Multiplayer.LogDebug(() => $"TrainsetMovementPart.Serialize() {data.typeFlag}"); @@ -83,6 +89,7 @@ public static void Serialize(NetDataWriter writer, TrainsetMovementPart data) public static TrainsetMovementPart Deserialize(NetDataReader reader) { + ushort netId = 0; float speed = 0; float slowBuildUpStress = 0; Vector3? position = null; @@ -90,11 +97,13 @@ public static TrainsetMovementPart Deserialize(NetDataReader reader) BogieData bd1 = default; BogieData bd2 = default; + netId = reader.GetUShort(); + MovementType dataType = (MovementType)reader.GetByte(); if (dataType.HasFlag(MovementType.RigidBody)) { - return new TrainsetMovementPart(RigidbodySnapshot.Deserialize(reader)); + return new TrainsetMovementPart(0, RigidbodySnapshot.Deserialize(reader)); } if (dataType.HasFlag(MovementType.Physics)) @@ -111,6 +120,6 @@ public static TrainsetMovementPart Deserialize(NetDataReader reader) rotation = QuaternionSerializer.Deserialize(reader); } - return new TrainsetMovementPart(speed, slowBuildUpStress, bd1, bd2, position, rotation); + return new TrainsetMovementPart(0, speed, slowBuildUpStress, bd1, bd2, position, rotation); } } From 27dc6a27b96538fba353683b267061578fbc48fc Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 12 Jan 2025 14:29:34 +1000 Subject: [PATCH 176/188] Minor logging improvements --- Multiplayer/Components/Networking/Train/NetworkedRigidbody.cs | 3 ++- Multiplayer/Networking/Managers/Client/NetworkClient.cs | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Multiplayer/Components/Networking/Train/NetworkedRigidbody.cs b/Multiplayer/Components/Networking/Train/NetworkedRigidbody.cs index f0bd46e..9ec043d 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedRigidbody.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedRigidbody.cs @@ -2,6 +2,7 @@ using System; using System.Collections; using UnityEngine; +using static Multiplayer.Networking.Data.Train.RigidbodySnapshot; namespace Multiplayer.Components.Networking.Train; @@ -49,7 +50,7 @@ protected override void Process(RigidbodySnapshot snapshot, uint snapshotTick) try { - Multiplayer.LogDebug(() => $"NetworkedRigidBody.Process() {snapshot.IncludedDataFlags}, {snapshot.Position.ToString() ?? "null"}, {snapshot.Rotation.ToString() ?? "null"}, {snapshot.Velocity.ToString() ?? "null"}, {snapshot.AngularVelocity.ToString() ?? "null"}"); + Multiplayer.LogDebug(() => $"NetworkedRigidBody.Process() {(IncludedData)snapshot.IncludedDataFlags}, {snapshot.Position.ToString() ?? "null"}, {snapshot.Rotation.ToString() ?? "null"}, {snapshot.Velocity.ToString() ?? "null"}, {snapshot.AngularVelocity.ToString() ?? "null"}, tick: {snapshotTick}"); snapshot.Apply(rigidbody); } catch (Exception ex) diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 04b9714..7dc23d3 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -806,10 +806,14 @@ private void OnClientboundCarHealthUpdatePacket(ClientboundCarHealthUpdatePacket private void OnClientboundRerailTrainPacket(ClientboundRerailTrainPacket packet) { + if (!NetworkedTrainCar.GetTrainCar(packet.NetId, out TrainCar trainCar)) return; if (!NetworkedRailTrack.Get(packet.TrackId, out NetworkedRailTrack networkedRailTrack)) return; + + Log($"Rerailing [{trainCar?.ID}, {packet.NetId}] to track {networkedRailTrack?.RailTrack?.logicTrack?.ID}"); + LogDebug(() => $"Rerailing [{trainCar?.ID}, {packet.NetId}] track: [{networkedRailTrack?.RailTrack?.logicTrack?.ID}, {packet.TrackId}], raw position: {packet.Position}, adjusted position: {packet.Position + WorldMover.currentMove}, forward: {packet.Forward}"); trainCar.Rerail(networkedRailTrack.RailTrack, packet.Position + WorldMover.currentMove, packet.Forward); } From c3d8957dd3fa3486c2ad744004e2e7e606003c0c Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 12 Jan 2025 15:28:05 +1000 Subject: [PATCH 177/188] Fix for job validation state --- Multiplayer/Patches/Jobs/JobValidatorPatch.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Multiplayer/Patches/Jobs/JobValidatorPatch.cs b/Multiplayer/Patches/Jobs/JobValidatorPatch.cs index 704e499..79c7a90 100644 --- a/Multiplayer/Patches/Jobs/JobValidatorPatch.cs +++ b/Multiplayer/Patches/Jobs/JobValidatorPatch.cs @@ -129,6 +129,7 @@ private static IEnumerator AwaitResponse(JobValidator validator, NetworkedJob ne validator.bookletPrinter.PlayErrorSound(); } + networkedJob.ValidatorRequestSent = false; networkedJob.ValidatorResponseReceived = false; networkedJob.ValidationAccepted = false; From 6c49c554728577bb314544f3a03e390f3a439a4b Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 12 Jan 2025 16:11:38 +1000 Subject: [PATCH 178/188] Improve retrieval of steam user account --- .../Components/MainMenu/ServerBrowserPane.cs | 2 - Multiplayer/Settings.cs | 7 +++- Multiplayer/Utils/SteamWorksUtils.cs | 37 +++++++++++++++++++ 3 files changed, 42 insertions(+), 4 deletions(-) create mode 100644 Multiplayer/Utils/SteamWorksUtils.cs diff --git a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs index 331388a..b6bbb22 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs @@ -1,6 +1,5 @@ using System; using System.Collections; -using System.Text.RegularExpressions; using DV.Localization; using DV.UI; using DV.UIFramework; @@ -19,7 +18,6 @@ using LiteNetLib; using System.Collections.Generic; using Multiplayer.Networking.Managers.Client; -using JetBrains.Annotations; namespace Multiplayer.Components.MainMenu { diff --git a/Multiplayer/Settings.cs b/Multiplayer/Settings.cs index 098cce6..65a0aa4 100644 --- a/Multiplayer/Settings.cs +++ b/Multiplayer/Settings.cs @@ -1,5 +1,6 @@ using System; using Humanizer; +using Multiplayer.Utils; using Steamworks; using UnityEngine; using UnityModManagerNet; @@ -21,6 +22,7 @@ public class Settings : UnityModManager.ModSettings, IDrawable [Draw("Use Steam Name", Tooltip = "Use your Steam name as your username in-game")] public bool UseSteamName = true; public string LastSteamName = string.Empty; + public ulong SteamId = 0; [Draw("Username", Tooltip = "Your username in-game", VisibleOn = "UseSteamName|false")] public string Username = "Player"; public string Guid = System.Guid.NewGuid().ToString(); @@ -132,9 +134,10 @@ public string GetUserName() if (Multiplayer.Settings.UseSteamName) { - if (DVSteamworks.Success) + if (SteamWorksUtils.GetSteamUser(out string steamUsername, out ulong steamId)) { - Multiplayer.Settings.LastSteamName = SteamClient.Name; + Multiplayer.Settings.LastSteamName = steamUsername; + Multiplayer.Settings.SteamId = steamId; } if (Multiplayer.Settings.LastSteamName != string.Empty) diff --git a/Multiplayer/Utils/SteamWorksUtils.cs b/Multiplayer/Utils/SteamWorksUtils.cs new file mode 100644 index 0000000..db7d957 --- /dev/null +++ b/Multiplayer/Utils/SteamWorksUtils.cs @@ -0,0 +1,37 @@ +using Steamworks; +using System; + +namespace Multiplayer.Utils; + +public static class SteamWorksUtils +{ + public static bool GetSteamUser(out string username, out ulong steamId) + { + username = null; + steamId = 0; + + try + { + if (!DVSteamworks.Success) + return false; + + if (!SteamClient.IsValid || !SteamClient.SteamId.IsValid) + { + Multiplayer.Log($"Failed to get SteamID. Status: {SteamClient.IsValid}, {SteamClient.SteamId.IsValid}"); + return false; + } + + steamId = SteamClient.SteamId.Value; + username = SteamClient.Name; + + if (SteamApps.IsAppInstalled(DVSteamworks.APP_ID)) + Multiplayer.Log($"Found Steam Name: {username}, steamId {steamId}"); + } + catch(Exception ex) + { + Multiplayer.LogError($"Failed to obtain Steam user.\r\n{ex.StackTrace}"); + } + + return true; + } +} From 45739a8fc6a57cff4e19c21498260100b0836c79 Mon Sep 17 00:00:00 2001 From: AMacro Date: Mon, 13 Jan 2025 21:52:57 +1000 Subject: [PATCH 179/188] Update PlayerListGUI behaviour Unsubscribe from events on return to main menu and trigger hiding of the GUI. On world loaded grab the local player's name to prevent hitting the Steam API endpoints multiple times --- .../Components/Networking/NetworkLifecycle.cs | 2 ++ .../Components/Networking/UI/PlayerListGUI.cs | 13 ++++++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/Multiplayer/Components/Networking/NetworkLifecycle.cs b/Multiplayer/Components/Networking/NetworkLifecycle.cs index 3bb6597..ddf222e 100644 --- a/Multiplayer/Components/Networking/NetworkLifecycle.cs +++ b/Multiplayer/Components/Networking/NetworkLifecycle.cs @@ -77,6 +77,8 @@ protected override void Awake() { if (scene.buildIndex != (int)DVScenes.MainMenu) return; + + playerList.UnRegisterListeners(); TriggerMainMenuEventLater(); }; StartCoroutine(PollEvents()); diff --git a/Multiplayer/Components/Networking/UI/PlayerListGUI.cs b/Multiplayer/Components/Networking/UI/PlayerListGUI.cs index 395e8ae..0e344bc 100644 --- a/Multiplayer/Components/Networking/UI/PlayerListGUI.cs +++ b/Multiplayer/Components/Networking/UI/PlayerListGUI.cs @@ -7,10 +7,17 @@ namespace Multiplayer.Components.Networking.UI; public class PlayerListGUI : MonoBehaviour { private bool showPlayerList; + private string localPlayerUsername; public void RegisterListeners() { ScreenspaceMouse.Instance.ValueChanged += OnToggle; + localPlayerUsername = Multiplayer.Settings.GetUserName(); + } + public void UnRegisterListeners() + { + ScreenspaceMouse.Instance.ValueChanged -= OnToggle; + OnToggle(false); } private void OnToggle(bool status) @@ -26,14 +33,14 @@ private void OnGUI() GUILayout.Window(157031520, new Rect(Screen.width / 2.0f - 125, 25, 250, 0), DrawPlayerList, Locale.PLAYER_LIST__TITLE); } - private static void DrawPlayerList(int windowId) + private void DrawPlayerList(int windowId) { foreach (string player in GetPlayerList()) GUILayout.Label(player); } // todo: cache this? - private static IEnumerable GetPlayerList() + private IEnumerable GetPlayerList() { if (!NetworkLifecycle.Instance.IsClientRunning) return new[] { "Not in game" }; @@ -48,7 +55,7 @@ private static IEnumerable GetPlayerList() } // The Player of the Client is not in the PlayerManager, so we need to add it separately - playerList[playerList.Length - 1] = $"{Multiplayer.Settings.GetUserName()} ({NetworkLifecycle.Instance.Client.Ping}ms)"; + playerList[playerList.Length - 1] = $"{localPlayerUsername} ({NetworkLifecycle.Instance.Client.Ping}ms)"; return playerList; } } From 2d283219ecfc8c05bd283344cd13635c85ed8443 Mon Sep 17 00:00:00 2001 From: AMacro Date: Mon, 13 Jan 2025 21:53:55 +1000 Subject: [PATCH 180/188] Potential fix for null gameObject exception --- .../Managers/Client/NetworkClient.cs | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 7dc23d3..b050395 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -229,16 +229,6 @@ public override void OnConnectionRequest(ConnectionRequest request) #endregion - #region NAT Punch Events - public override void OnNatIntroductionRequest(IPEndPoint localEndPoint, IPEndPoint remoteEndPoint, string token) - { - //do some stuff here - } - public override void OnNatIntroductionSuccess(IPEndPoint targetEndPoint, NatAddressType type, string token) - { - //do other stuff here - } - #endregion #region Listeners @@ -529,11 +519,11 @@ private void OnClientboundDestroyTrainCarPacket(ClientboundDestroyTrainCarPacket //Protect other players from getting deleted in race conditions - this should be a temporary fix, if another playe's game object is deleted we should just recreate it if(networkedTrainCar == null || networkedTrainCar.gameObject == null || networkedTrainCar.TrainCar == null) { - LogDebug(() => $"OnClientboundDestroyTrainCarPacket({packet?.NetId}) networkedTrainCar: {networkedTrainCar != null}, go: {networkedTrainCar?.gameObject != null}, trainCar: {networkedTrainCar?.TrainCar != null}"); + LogDebug(() => $"OnClientboundDestroyTrainCarPacket({packet?.NetId}) networkedTrainCar: {networkedTrainCar != null}, go: {(networkedTrainCar?.gameObject) != null}, trainCar: {networkedTrainCar?.TrainCar != null}"); } else { - NetworkedPlayer[] componentsInChildren = networkedTrainCar?.GetComponentsInChildren() ?? []; + NetworkedPlayer[] componentsInChildren = (networkedTrainCar?.gameObject != null) ? networkedTrainCar.GetComponentsInChildren() : []; foreach (NetworkedPlayer networkedPlayer in componentsInChildren) { @@ -1302,4 +1292,16 @@ public void SendItemsChangePacket(List items) } #endregion + + public override void OnNatIntroductionSuccess(IPEndPoint targetEndPoint, NatAddressType type, string token) + { + // Add your implementation here + Multiplayer.Log($"NAT introduction successful. Target end point: {targetEndPoint}, Type: {type}, Token: {token}"); + } + + public override void OnNatIntroductionRequest(IPEndPoint localEndPoint, IPEndPoint remoteEndPoint, string token) + { + // Add your implementation here + Multiplayer.Log($"NAT introduction request received. Local end point: {localEndPoint}, Remote end point: {remoteEndPoint}, Token: {token}"); + } } From 70b39a54fd15f559a4d17ed68206e12e42feb952 Mon Sep 17 00:00:00 2001 From: AMacro Date: Tue, 14 Jan 2025 22:47:16 +1000 Subject: [PATCH 181/188] Add check for cached null in NetworkedPlayer --- .../Components/Networking/Player/NetworkedPlayer.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Multiplayer/Components/Networking/Player/NetworkedPlayer.cs b/Multiplayer/Components/Networking/Player/NetworkedPlayer.cs index 59519c5..561267a 100644 --- a/Multiplayer/Components/Networking/Player/NetworkedPlayer.cs +++ b/Multiplayer/Components/Networking/Player/NetworkedPlayer.cs @@ -106,6 +106,14 @@ public void UpdatePosition(Vector3 position, Vector2 moveDir, float rotation, bo public void UpdateCar(ushort netId) { isOnCar = NetworkedTrainCar.GetTrainCar(netId, out TrainCar trainCar); + + if(isOnCar && trainCar == null) + { + //we have a desync! + Multiplayer.LogWarning($"Desync detected! Trying to update player '{username}' position to TrainCar netId {netId}, but car is null!"); + return; + } + selfTransform.SetParent(isOnCar ? trainCar.transform : null, true); targetPos = isOnCar ? transform.localPosition : selfTransform.position; targetRotation = isOnCar ? transform.localRotation : selfTransform.rotation; From 64efdc934c9898aaefd1b82f33a7b3e26b72f205 Mon Sep 17 00:00:00 2001 From: AMacro Date: Tue, 14 Jan 2025 22:47:41 +1000 Subject: [PATCH 182/188] Remove unsafe dictionary access, use TryGet() --- .../Networking/Train/NetworkedTrainCar.cs | 67 ++++++++++--------- 1 file changed, 35 insertions(+), 32 deletions(-) diff --git a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs index a908e37..71a4b7c 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs @@ -43,10 +43,6 @@ public static bool GetTrainCar(ushort netId, out TrainCar obj) return b; } - public static Coupler GetCoupler(HoseAndCock hoseAndCock) - { - return hoseToCoupler[hoseAndCock]; - } public static bool TryGetCoupler(HoseAndCock hoseAndCock, out Coupler coupler) { return hoseToCoupler.TryGetValue(hoseAndCock, out coupler); @@ -157,16 +153,11 @@ public void Start() { hoseToCoupler[coupler.hoseAndCock] = coupler; - Multiplayer.LogDebug(() => $"TrainCar.Start() [{TrainCar?.ID}, {NetId}], Coupler exists: {coupler != null}, ChainScript exists: {coupler.ChainScript != null}"); - try - { + Multiplayer.LogDebug(() => $"TrainCar.Start() [{TrainCar?.ID}, {NetId}], Coupler exists: {coupler != null}, Is front: {coupler.isFrontCoupler}, ChainScript exists: {coupler.ChainScript != null}"); + //Locos with tenders and tenders only have one chainscript each, no trainscript is used for the hitch between the loco and tender + if(coupler.ChainScript != null) coupler.ChainScript.StateChanged += (state) => { Client_CouplerStateChange(state, coupler); }; - } - catch (Exception ex) - { - Multiplayer.LogError($"Error subscribing to coupler state changes [{TrainCar?.ID}, {NetId}]\r\n{ex.Message}\r\n{ex.StackTrace}"); - } } SimController simController = GetComponent(); @@ -329,9 +320,6 @@ public void Server_DirtyAllState() if (simulationFlow.TryGetPort(portId, out Port port)) { lastSentPortValues[portId] = port.value; - - //Multiplayer.Log($"Server_DirtyAllState({TrainCar.ID}): {portId}({port.type}): {port.value}({port.valueType})"); - } } @@ -638,9 +626,16 @@ private void Common_SendPorts() float[] portValues = new float[portIds.Length]; foreach (string portId in dirtyPorts) { - float value = simulationFlow.fullPortIdToPort[portId].Value; - portValues[i++] = value; - lastSentPortValues[portId] = value; + if(simulationFlow.TryGetPort(portId, out Port port)) + { + float value = port.Value; + portValues[i++] = value; + lastSentPortValues[portId] = value; + } + else + { + Multiplayer.LogWarning($"SendPorts() [{CurrentID}, {NetId}] Failed to find port \"{portId}\""); + } } dirtyPorts.Clear(); @@ -657,7 +652,10 @@ private void Common_SendFuses() string[] fuseIds = dirtyFuses.ToArray(); bool[] fuseValues = new bool[fuseIds.Length]; foreach (string fuseId in dirtyFuses) - fuseValues[i++] = simulationFlow.fullFuseIdToFuse[fuseId].State; + if(simulationFlow.TryGetFuse(fuseId, out Fuse fuse)) + fuseValues[i++] = fuse.State; + else + Multiplayer.LogWarning($"SendFuses() [{CurrentID}, {NetId}] Failed to find fuse \"{fuseId}\""); dirtyFuses.Clear(); @@ -712,22 +710,22 @@ public void Common_UpdatePorts(CommonTrainPortsPacket packet) //string log = $"CommonTrainPortsPacket({TrainCar.ID})"; for (int i = 0; i < packet.PortIds.Length; i++) { - Port port = simulationFlow.fullPortIdToPort[packet.PortIds[i]]; - float value = packet.PortValues[i]; - // before = port.value; + if(simulationFlow.TryGetPort(packet.PortIds[i], out Port port)) + { + float value = packet.PortValues[i]; + // before = port.value; - if (port.type == PortType.EXTERNAL_IN) - port.ExternalValueUpdate(value); + if (port.type == PortType.EXTERNAL_IN) + port.ExternalValueUpdate(value); + else + port.Value = value; + } else - port.Value = value; - - /* - if (Multiplayer.Settings.DebugLogging) - log += $"\r\n\tPort name: {port.id}, value before: {before}, value after: {port.value}, value: {value}, port type: {port.type}";) - */ + { + Multiplayer.LogWarning($"Common_UpdatePorts() [{CurrentID}, {NetId}] Failed to find port \"{packet.PortIds[i]}\", Value: {packet.PortValues[i]}"); + } } - //NetworkLifecycle.Instance.Client.LogDebug(() => log); } public void Common_UpdateFuses(CommonTrainFusesPacket packet) @@ -736,7 +734,12 @@ public void Common_UpdateFuses(CommonTrainFusesPacket packet) return; for (int i = 0; i < packet.FuseIds.Length; i++) - simulationFlow.fullFuseIdToFuse[packet.FuseIds[i]].ChangeState(packet.FuseValues[i]); + if (simulationFlow.TryGetFuse(packet.FuseIds[i], out Fuse fuse)) + fuse.ChangeState(packet.FuseValues[i]); + else + Multiplayer.LogWarning($"UpdateFuses() [{CurrentID}, {NetId}] Failed to find fuse \"{packet.FuseIds[i]}\", Value: {packet.FuseValues[i]}"); + + //simulationFlow.fullFuseIdToFuse[packet.FuseIds[i]].ChangeState(packet.FuseValues[i]); } public void Common_ReceiveCouplerInteraction(CommonCouplerInteractionPacket packet) From 7aca2c3a800d0a779d0b42b0f12306ac6779abef Mon Sep 17 00:00:00 2001 From: AMacro Date: Wed, 15 Jan 2025 21:33:08 +1000 Subject: [PATCH 183/188] Added null reference checks for map marker updates --- .../Networking/Player/NetworkedWorldMap.cs | 12 +++++++++++- Multiplayer/Multiplayer.csproj | 2 +- info.json | 2 +- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/Multiplayer/Components/Networking/Player/NetworkedWorldMap.cs b/Multiplayer/Components/Networking/Player/NetworkedWorldMap.cs index 144503a..deb481a 100644 --- a/Multiplayer/Components/Networking/Player/NetworkedWorldMap.cs +++ b/Multiplayer/Components/Networking/Player/NetworkedWorldMap.cs @@ -9,7 +9,7 @@ public class NetworkedMapMarkersController : MonoBehaviour { private MapMarkersController markersController; private GameObject textPrefab; - private readonly Dictionary playerIndicators = new(); + private readonly Dictionary playerIndicators = []; private void Awake() { @@ -82,8 +82,10 @@ private void OnTick(uint obj) public void UpdatePlayers() { + Multiplayer.LogDebug(() => $"NetworkedWorldMap.UpdatePlayers() playerIndicators: {playerIndicators != null}, count: {playerIndicators?.Count}"); foreach (KeyValuePair kvp in playerIndicators) { + Multiplayer.LogDebug(() => $"NetworkedWorldMap.UpdatePlayers() key: {kvp.Key}, value is null: {kvp.Value == null}"); if (!NetworkLifecycle.Instance.Client.ClientPlayerManager.TryGetPlayer(kvp.Key, out NetworkedPlayer networkedPlayer)) { Multiplayer.LogWarning($"Player indicator for {kvp.Key} exists but {nameof(NetworkedPlayer)} does not!"); @@ -91,6 +93,12 @@ public void UpdatePlayers() continue; } + if(kvp.Value == null) + { + Multiplayer.LogWarning($"NetworkedWorldMap.UpdatePlayers() key: {kvp.Key}, value is null skipping"); + continue; + } + WorldMapIndicatorRefs refs = kvp.Value; bool active = Globals.G.gameParams.PlayerMarkerDisplayed; @@ -99,6 +107,8 @@ public void UpdatePlayers() if (!active) return; + Multiplayer.LogDebug(() => $"NetworkedWorldMap.UpdatePlayers() key: {kvp.Key}, Is active"); + Transform playerTransform = networkedPlayer.transform; Vector3 normalized = Vector3.ProjectOnPlane(playerTransform.forward, Vector3.up).normalized; diff --git a/Multiplayer/Multiplayer.csproj b/Multiplayer/Multiplayer.csproj index d885efe..f78b081 100644 --- a/Multiplayer/Multiplayer.csproj +++ b/Multiplayer/Multiplayer.csproj @@ -3,7 +3,7 @@ net48 latest Multiplayer - 0.1.9.7 + 0.1.9.8 diff --git a/info.json b/info.json index a796e06..2b46338 100644 --- a/info.json +++ b/info.json @@ -1,6 +1,6 @@ { "Id": "Multiplayer", - "Version": "0.1.9.7", + "Version": "0.1.9.8", "DisplayName": "Multiplayer", "Author": "Insprill, Macka, Morm", "EntryMethod": "Multiplayer.Multiplayer.Load", From 83017f59306eda140f62a992396cdff232aa47c3 Mon Sep 17 00:00:00 2001 From: AMacro Date: Wed, 15 Jan 2025 21:34:04 +1000 Subject: [PATCH 184/188] additional logging and null checks for OnCommonHoseConnectedPacket --- .../Managers/Client/NetworkClient.cs | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index b050395..7d61015 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -586,20 +586,32 @@ private void OnCommonTrainUncouplePacket(CommonTrainUncouplePacket packet) private void OnCommonHoseConnectedPacket(CommonHoseConnectedPacket packet) { - TrainCar otherTrainCar = null; + bool foundTrainCar = NetworkedTrainCar.GetTrainCar(packet.NetId, out TrainCar trainCar); + bool foundOtherTrainCar = NetworkedTrainCar.GetTrainCar(packet.OtherNetId, out TrainCar otherTrainCar); - if (!NetworkedTrainCar.GetTrainCar(packet.NetId, out TrainCar trainCar) || !NetworkedTrainCar.GetTrainCar(packet.OtherNetId, out otherTrainCar)) + if (!foundTrainCar || trainCar == null || + !foundOtherTrainCar || otherTrainCar == null) { - LogDebug(() => $"OnCommonHoseConnectedPacket() netId: {packet.NetId}, trainCar found?: {trainCar != null}, otherNetId: {packet.OtherNetId}, otherTrainCar found?: {otherTrainCar != null}"); + LogError($"OnCommonHoseConnectedPacket() netId: {packet.NetId}, trainCar found: {foundTrainCar}, trainCar is null: {trainCar == null}, otherNetId: {packet.OtherNetId}, otherTrainCar found: {foundOtherTrainCar}, other trainCar is null: {otherTrainCar == null}"); return; } - LogDebug(() => $"OnCommonHoseConnectedPacket() netId: {packet.NetId}, trainCar: {trainCar.ID}, isFront: {packet.IsFront}, playAudio: {packet.PlayAudio}"); + string carId = $"[{ trainCar?.ID}, { packet.NetId}]"; + string otherCarId = $"[{ otherTrainCar?.ID}, { packet.OtherNetId}]"; + + LogDebug(() => $"OnCommonHoseConnectedPacket() trainCar: {carId}, isFront: {packet.IsFront}, otherTrainCar: {otherCarId}, isFront: {packet.OtherIsFront}, playAudio: {packet.PlayAudio}"); Coupler coupler = packet.IsFront ? trainCar.frontCoupler : trainCar.rearCoupler; Coupler otherCoupler = packet.OtherIsFront ? otherTrainCar.frontCoupler : otherTrainCar.rearCoupler; - if (coupler == null || otherCoupler == null || coupler.hoseAndCock.IsHoseConnected || otherCoupler.hoseAndCock.IsHoseConnected) + if (coupler == null || coupler.hoseAndCock == null || + otherCoupler == null || otherCoupler.hoseAndCock == null) + { + LogError($"OnCommonHoseConnectedPacket() trainCar: {carId}, coupler found: {coupler != null}, otherCoupler found: {otherCoupler != null}, hoseAndCock found: {coupler.hoseAndCock != null}, otherHoseAndCock found: {otherCoupler.hoseAndCock != null}"); + return; + } + + if (coupler.hoseAndCock.IsHoseConnected || otherCoupler.hoseAndCock.IsHoseConnected) { Coupler connectedTo = null; Coupler otherConnectedTo = null; @@ -609,8 +621,8 @@ private void OnCommonHoseConnectedPacket(CommonHoseConnectedPacket packet) if(otherCoupler?.hoseAndCock?.connectedTo != null) NetworkedTrainCar.TryGetCoupler(otherCoupler.hoseAndCock.connectedTo, out otherConnectedTo); - LogWarning($"OnCommonHoseConnectedPacket() netId: {packet.NetId}, trainCar: {trainCar?.ID}, isFront: {packet.IsFront}, IsHoseConnected: {coupler?.hoseAndCock?.IsHoseConnected}, connectedTo: {connectedTo?.train?.ID}," + - $" other trainCar: {otherTrainCar?.ID}, other isFront: {otherCoupler?.isFrontCoupler}, other IsHoseConnected: {otherCoupler?.hoseAndCock?.IsHoseConnected}, other connectedTo: {otherConnectedTo?.train?.ID}"); + LogWarning($"OnCommonHoseConnectedPacket() trainCar: {carId}, isFront: {packet.IsFront}, IsHoseConnected: {coupler?.hoseAndCock?.IsHoseConnected}, connectedTo: {connectedTo?.train?.ID}," + + $" otherTrainCar: {otherCarId}, other isFront: {otherCoupler?.isFrontCoupler}, other IsHoseConnected: {otherCoupler?.hoseAndCock?.IsHoseConnected}, other connectedTo: {otherConnectedTo?.train?.ID}"); } else { From bb722020b87996af948fa85a812006dca06f2a92 Mon Sep 17 00:00:00 2001 From: AMacro Date: Wed, 15 Jan 2025 21:34:38 +1000 Subject: [PATCH 185/188] Additional logging for LobbyServerManager WebRequests --- Multiplayer/Networking/Managers/Server/LobbyServerManager.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs b/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs index b29397f..6fac8b1 100644 --- a/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs +++ b/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs @@ -275,7 +275,7 @@ private IEnumerator SendWebRequest(string uri, string json, Action onSucc { if (webRequest.isNetworkError || webRequest.isHttpError) { - Multiplayer.LogError($"Error: {webRequest.error}\r\n{webRequest.downloadHandler.text}"); + Multiplayer.LogError($"SendWebRequest({uri}) responseCode: {webRequest.responseCode}, Error: {webRequest.error}\r\n{webRequest.downloadHandler.text}"); onError?.Invoke(webRequest); } else From dbd5108f806310bb49c98a91a6c616ea736a8b53 Mon Sep 17 00:00:00 2001 From: AMacro Date: Thu, 16 Jan 2025 21:35:03 +1000 Subject: [PATCH 186/188] Additional coupler interaction logging --- .../Components/Networking/Train/NetworkedTrainCar.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs index 71a4b7c..d3adc28 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs @@ -744,18 +744,19 @@ public void Common_UpdateFuses(CommonTrainFusesPacket packet) public void Common_ReceiveCouplerInteraction(CommonCouplerInteractionPacket packet) { + CouplerInteractionType flags = (CouplerInteractionType)packet.Flags; Coupler coupler = packet.IsFrontCoupler ? TrainCar?.frontCoupler : TrainCar?.rearCoupler; TrainCar otherCar = null; Coupler otherCoupler = null; - + + Multiplayer.LogDebug(() => $"Common_ReceiveCouplerInteraction() couplerNetId: {NetId}, coupler is front: {packet.IsFrontCoupler}, flags: {flags}, otherCouplerNetId: {packet.OtherNetId}, otherCoupler is front: {packet.IsFrontOtherCoupler}"); + if (coupler == null) { Multiplayer.LogWarning($"Common_ReceiveCouplerInteraction() did not find coupler for [{TrainCar?.ID}, {NetId}], coupler is front: {packet.IsFrontCoupler}"); return; } - CouplerInteractionType flags = (CouplerInteractionType)packet.Flags; - if (packet.OtherNetId != 0) { if (GetTrainCar(packet.OtherNetId, out otherCar)) From fbffecd9fee4876a8182305c7bbc7898dc71644c Mon Sep 17 00:00:00 2001 From: AMacro Date: Thu, 16 Jan 2025 21:36:40 +1000 Subject: [PATCH 187/188] Add delta check to OnFireboxUpdate() Reduce Firebox update packets - only send if the delta is more that 0.1 or if the new value is 0 and the last sent value is non-zero --- .../Components/Networking/Train/NetworkedTrainCar.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs index d3adc28..1d49178 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs @@ -65,6 +65,7 @@ public static bool TryGetFromTrainCar(TrainCar trainCar, out NetworkedTrainCar n #endregion private const int MAX_COUPLER_ITERATIONS = 10; + private const float MAX_FIREBOX_DELTA = 0.1f; public string CurrentID { get; private set; } public TrainCar TrainCar; @@ -82,6 +83,7 @@ public static bool TryGetFromTrainCar(TrainCar trainCar, out NetworkedTrainCar n private HashSet dirtyPorts; private Dictionary lastSentPortValues; private HashSet dirtyFuses; + private float lastSentFireboxValue; private bool handbrakeDirty; private bool mainResPressureDirty; @@ -676,12 +678,18 @@ private void Common_OnBrakeCylinderReleased() NetworkLifecycle.Instance.Client.SendBrakeCylinderReleased(NetId); } - private void Common_OnFireboxUpdate(float _) + private void Common_OnFireboxUpdate(float newFireboxValue) { if (NetworkLifecycle.Instance.IsProcessingPacket) return; - fireboxDirty = true; + var delta = Math.Abs(lastSentFireboxValue - newFireboxValue); + if (delta > MAX_FIREBOX_DELTA || (newFireboxValue == 0 && lastSentFireboxValue != 0)) + { + fireboxDirty = true; + lastSentFireboxValue = newFireboxValue; + } + } private void Common_OnPortUpdated(Port port) From 3ab2d27732c3cc466bafd8992d03e2c71327ba5c Mon Sep 17 00:00:00 2001 From: AMacro Date: Thu, 16 Jan 2025 21:38:58 +1000 Subject: [PATCH 188/188] Reduce debug logging for map marker updates Logging spam reduced while still keeping logging functionality --- .../Networking/Player/NetworkedWorldMap.cs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/Multiplayer/Components/Networking/Player/NetworkedWorldMap.cs b/Multiplayer/Components/Networking/Player/NetworkedWorldMap.cs index deb481a..fe99948 100644 --- a/Multiplayer/Components/Networking/Player/NetworkedWorldMap.cs +++ b/Multiplayer/Components/Networking/Player/NetworkedWorldMap.cs @@ -82,10 +82,17 @@ private void OnTick(uint obj) public void UpdatePlayers() { - Multiplayer.LogDebug(() => $"NetworkedWorldMap.UpdatePlayers() playerIndicators: {playerIndicators != null}, count: {playerIndicators?.Count}"); + if (playerIndicators == null) + { + Multiplayer.LogDebug(() => $"NetworkedWorldMap.UpdatePlayers() playerIndicators: {playerIndicators != null}, count: {playerIndicators?.Count}"); + return; + } + foreach (KeyValuePair kvp in playerIndicators) { - Multiplayer.LogDebug(() => $"NetworkedWorldMap.UpdatePlayers() key: {kvp.Key}, value is null: {kvp.Value == null}"); + if(kvp.Value == null) + Multiplayer.LogDebug(() => $"NetworkedWorldMap.UpdatePlayers() key: {kvp.Key}, value is null: {kvp.Value == null}"); + if (!NetworkLifecycle.Instance.Client.ClientPlayerManager.TryGetPlayer(kvp.Key, out NetworkedPlayer networkedPlayer)) { Multiplayer.LogWarning($"Player indicator for {kvp.Key} exists but {nameof(NetworkedPlayer)} does not!"); @@ -105,9 +112,10 @@ public void UpdatePlayers() if (refs.gameObject.activeSelf != active) refs.gameObject.SetActive(active); if (!active) + { + Multiplayer.LogDebug(() => $"NetworkedWorldMap.UpdatePlayers() key: {kvp.Key}, is NOT active"); return; - - Multiplayer.LogDebug(() => $"NetworkedWorldMap.UpdatePlayers() key: {kvp.Key}, Is active"); + } Transform playerTransform = networkedPlayer.transform;