From e23d522a4e966920651802ebc67ab636599bb466 Mon Sep 17 00:00:00 2001 From: Pierce Thompson Date: Tue, 8 Aug 2023 00:26:19 -0400 Subject: [PATCH 001/521] 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 46a11151..c70c6891 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 8302d8a4..da66224e 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 62c15455..4a250c02 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/521] 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 560fb24f..6ddde1e5 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/521] 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 be420685..c11ca961 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 c01fe674..e7c8da3f 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/521] 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 c11ca961..e3f5861e 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 00000000..b3c4016c --- /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 e6d15447..af4998e2 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 33467b43..f393cc44 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 e7c8da3f..b29e88ae 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 f8b269b0..6a08bcde 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/521] 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 02a6d6b2..99200718 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 be420685..a3d2f160 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 00000000..1cda1230 --- /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 74170121..8d0db2b6 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 c70c6891..274c5d6d 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 87ca8b09..04af71f4 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 8f3bdb8b..e9b86a6b 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 c1dc8053..0cd194a3 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 0f799cbf..317b0532 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 be049356..1aa18dca 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 da66224e..7f275472 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 c01fe674..4e2087be 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 00000000..ef66263f --- /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 00000000..e69de29b diff --git a/locale.csv b/locale.csv index 4a250c02..de7e86ab 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/521] 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 560fb24f..ab9d8a0d 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/521] 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 e3f5861e..9cb9b73b 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/521] 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 9cb9b73b..dcf588ef 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/521] 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 00000000..f199c7c4 --- /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 a3d2f160..c75a9b4b 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 00000000..9aa71545 --- /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 00000000..ba61ae23 --- /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 04af71f4..cdbc6cb2 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 e9b86a6b..42304b60 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 7f275472..bf7d62f4 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 ef66263f..00000000 --- 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/521] 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 00000000..9c1271c4 --- /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 02a6d6b2..99200718 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 be420685..8b6844f4 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 00000000..1cda1230 --- /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 00000000..94e76879 --- /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 00000000..a4a21964 --- /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 e6d15447..274c5d6d 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 87ca8b09..e8a1494b 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 70448c48..df191dca 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 0f799cbf..317b0532 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 be049356..65f3c3be 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 33467b43..bf7d62f4 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 c01fe674..4e2087be 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 00000000..dc63f6fc --- /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/521] 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 99200718..b081b369 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 b7348c56..75ed8634 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 318aa6c5..6f6be23b 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 317b0532..7fd486f3 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 9fc2e6db..992959b3 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 bf7d62f4..38131797 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 560fb24f..a58ceb04 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/521] 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 6f6be23b..b269f54a 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 38131797..7f31c20e 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 dc63f6fc..00000000 --- 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 b1c4785e..735f5146 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 b0a87a0f..2a89138c 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 00000000..30a876c5 --- /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 00000000..7239c665 --- /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 00000000..9d0ce882 --- /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 b4953ac5..d948b24d 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 38fde5f3..d638f04d 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/521] 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 75ed8634..1ed75027 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/521] 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 87860e10..145bee5f 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 00000000..f4942fdc --- /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 00000000..2a57cf30 --- /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 00000000..927c6963 --- /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 00000000..c5fbb5ba --- /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 00000000..8f96b388 --- /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 00000000..d654573f --- /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 00000000..c83f021c --- /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/521] 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 1ed75027..6837f329 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/521] 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 145bee5f..d792194b 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 c5fbb5ba..2b81e2d8 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 8f96b388..7023cda5 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 00000000..e863e8b3 --- /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 c83f021c..0729ae6f 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/521] 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 d654573f..e8d937b6 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/521] 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 00000000..ae751d42 --- /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 00000000..13f7566a --- /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 00000000..b92119dd --- /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 00000000..77e65ba4 --- /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 f4942fdc..52073ea0 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 2a57cf30..ca44d4f4 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 00000000..f3833149 --- /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 927c6963..ce5ed94f 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 2b81e2d8..f80e1d82 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 7023cda5..2e80b782 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 e8d937b6..db84e870 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 00000000..bc25a1fb --- /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 00000000..71bc9a51 --- /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 0729ae6f..286a442a 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 00000000..3ffa0092 --- /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 00000000..f8c9f700 --- /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 00000000..a1335a90 --- /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 00000000..b89c13c3 --- /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 00000000..aee1b4c5 --- /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 533b21a7..20fc5e6b 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 b269f54a..e1c122b9 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 97d033be..f43c789f 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 6837f329..4a5eb7be 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 df191dca..0b78777f 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 00000000..c0b3a476 --- /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 00000000..2f649908 --- /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 7f31c20e..36b361ee 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 4e2087be..add60710 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 745ef944..080c30d1 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/521] 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 336dac6c..42fcd332 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/521] 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 00000000..44f3fb27 --- /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 13f7566a..9634991a 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 b92119dd..32a774e7 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 77e65ba4..47531962 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 ca44d4f4..556e8287 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 ce5ed94f..4309b2c1 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 71bc9a51..76eec8de 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 20fc5e6b..28d4d385 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 4a5eb7be..82555fc3 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 7c14288d..734e407f 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 274c5d6d..bc30ea96 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 00000000..ffed4f05 --- /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 00000000..70d093bc --- /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 00000000..f592f9ac --- /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 c0b3a476..00000000 --- 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 93b5cd8b..7d4d4dc0 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 00000000..17f674a0 --- /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 f5129b2a..842bc170 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 2f649908..49542120 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 992959b3..3ece9833 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 36b361ee..4bff4d38 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 0c8067ff..c014da70 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 add60710..903c5c5e 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 080c30d1..5241d93f 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 336dac6c..4217e665 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/521] 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 aa93d61c..f50484c3 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 82555fc3..a8709e37 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 49542120..38122f57 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 903c5c5e..053ab2eb 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 b6f7b0e6..03d0d3e2 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 7b8eacd3..8845a6f1 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/521] 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 44f3fb27..c8f09176 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 47531962..5bc4c506 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 053ab2eb..57ef567d 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/521] 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 a8709e37..df13cf28 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 4bff4d38..e0efd7c5 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/521] 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 df13cf28..c8b5e8ab 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 8845a6f1..94acd696 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/521] 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 c8b5e8ab..deb95c8d 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 0b78777f..a2e5fb9d 100644 --- a/Multiplayer/Multiplayer.csproj +++ b/Multiplayer/Multiplayer.csproj @@ -77,6 +77,7 @@ + diff --git a/locale.csv b/locale.csv index 94acd696..718e29d9 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/521] 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 deb95c8d..4d042591 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 bc30ea96..4d6dca52 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 a58ceb04..0aca45a2 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 718e29d9..9d797c6e 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/521] Fixed translation issue --- locale.csv | 1 + 1 file changed, 1 insertion(+) diff --git a/locale.csv b/locale.csv index 9d797c6e..98523564 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/521] 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 4d042591..335dcf5d 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/521] 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 734e407f..dcaaf5fa 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 ab05efeb..e80cf801 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 8a516fa2..471d050c 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/521] 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 335dcf5d..d3295656 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 dcaaf5fa..e07dda8c 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 00000000..f35100fc --- /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 a2e5fb9d..a10601f6 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 b44d387d..5a158b36 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/521] 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 f35100fc..800b48c0 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/521] 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 800b48c0..e2ff62b8 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 5a158b36..35f93f96 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 00000000..87b042cb --- /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 842bc170..db0ebd68 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 00000000..5f4d2351 --- /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 03d0d3e2..37a82f6a 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/521] 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 e2ff62b8..2c28f01e 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 87b042cb..ffaf47d7 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 db0ebd68..a645f9dc 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/521] 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 2c28f01e..a3213350 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 35f93f96..09d5c510 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 ffaf47d7..d1c80a83 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 5f4d2351..1c511ad8 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/521] 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 a3213350..a5675d17 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 4e367e53..613f25e6 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 d1c80a83..bee85edd 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 a645f9dc..055325ac 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/521] 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 f50484c3..a072dd15 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 00000000..a566ef72 --- /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 e1c122b9..f0ecf14c 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 f43c789f..7f13fb3b 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 335dcf5d..37aed646 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 4d6dca52..dbfd6372 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 98523564..05fdbf58 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/521] 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 17f674a0..90f9ce89 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 842bc170..a5bc5ad8 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/521] 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 b4a18a21..b3fc68e2 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/521] 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 a072dd15..043c683e 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 b081b369..732a9419 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 1cda1230..170caabd 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 113b3f08..f74576a4 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 00000000..1980329f --- /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 249b47fe..03ee184a 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 436649c1..c50a7145 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 00000000..141dd5b9 --- /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 00000000..3f30f62a --- /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 e8a1494b..b54272ea 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 00000000..799199dd --- /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 323884ed..451bbdbd 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 00000000..eb1be238 --- /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 09d5c510..cfdb9b1c 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 7d4d4dc0..0a881302 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 87ab4700..301ea271 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 00000000..4caa869b --- /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 00000000..fc89a410 --- /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 00000000..53b0bd96 --- /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 00000000..895d5fe5 --- /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 00000000..2ee4ab53 --- /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 00000000..f2fc105f --- /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 00000000..ae82b0b6 --- /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 00000000..a0f253d2 --- /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 217630b5..0d82e62d 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 38122f57..17d4e4cd 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 b3fc68e2..fcb12e58 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 37a82f6a..43accd05 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/521] 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 87860e10..d792194b 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 00000000..c8f09176 --- /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 00000000..ae751d42 --- /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 00000000..9634991a --- /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 00000000..32a774e7 --- /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 00000000..5bc4c506 --- /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 00000000..52073ea0 --- /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 00000000..556e8287 --- /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 00000000..f3833149 --- /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 00000000..4309b2c1 --- /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 00000000..f80e1d82 --- /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 00000000..2e80b782 --- /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 00000000..db84e870 --- /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 00000000..e863e8b3 --- /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 00000000..bc25a1fb --- /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 00000000..76eec8de --- /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 00000000..286a442a --- /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 00000000..3ffa0092 --- /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 00000000..f8c9f700 --- /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 00000000..a1335a90 --- /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 00000000..b89c13c3 --- /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 00000000..043c683e --- /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 02a6d6b2..732a9419 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 00000000..28d4d385 --- /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 00000000..170caabd --- /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 00000000..a566ef72 --- /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 00000000..f0ecf14c --- /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 00000000..7f13fb3b --- /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 00000000..f74576a4 --- /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 00000000..1980329f --- /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 7c14288d..e07dda8c 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 249b47fe..03ee184a 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 436649c1..c50a7145 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 00000000..a5675d17 --- /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 ab05efeb..e80cf801 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 8a516fa2..471d050c 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 00000000..141dd5b9 --- /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 c764c937..f9bf95c8 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 00000000..3f30f62a --- /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 af4998e2..dbfd6372 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 87ca8b09..b54272ea 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 70448c48..a10601f6 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 00000000..799199dd --- /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 00000000..ffed4f05 --- /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 00000000..70d093bc --- /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 00000000..f592f9ac --- /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 323884ed..451bbdbd 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 4e367e53..613f25e6 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 00000000..eb1be238 --- /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 b44d387d..cfdb9b1c 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 93b5cd8b..0a881302 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 00000000..bee85edd --- /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 00000000..90f9ce89 --- /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 f5129b2a..301ea271 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 00000000..4caa869b --- /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 00000000..fc89a410 --- /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 00000000..53b0bd96 --- /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 00000000..1c511ad8 --- /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 00000000..895d5fe5 --- /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 00000000..2ee4ab53 --- /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 00000000..f2fc105f --- /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 00000000..ae82b0b6 --- /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 00000000..a0f253d2 --- /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 217630b5..0d82e62d 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 00000000..17d4e4cd --- /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 0f799cbf..7fd486f3 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 be049356..3ece9833 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 f393cc44..8b3c46ec 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 0c8067ff..c014da70 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 b29e88ae..2ca75d30 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 560fb24f..fcb12e58 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 745ef944..5241d93f 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 b1c4785e..735f5146 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 b0a87a0f..2a89138c 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 00000000..30a876c5 --- /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 00000000..7239c665 --- /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 00000000..9d0ce882 --- /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 b4953ac5..d948b24d 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 38fde5f3..d638f04d 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 b6f7b0e6..43accd05 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 6a08bcde..05fdbf58 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/521] 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 dcf588ef..00000000 --- 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 b3c4016c..00000000 --- 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 f74576a4..4d8570a3 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 2ca75d30..9b4ce99e 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/521] 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 fcb12e58..13943da6 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/521] 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 a10601f6..d0aa83d7 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 43accd05..06bd2635 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 474e0e57..00000000 --- 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 00000000..ae93e0be --- /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/521] 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 556e8287..1e70269f 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/521] 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 9634991a..5038f8c1 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 32a774e7..7810f4cb 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 1e70269f..a18569d0 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 f3833149..f323dcbf 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 4309b2c1..50451888 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 76eec8de..36961fdd 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 3ffa0092..a4c9abc4 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 c453427d..01cb8de3 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 03ee184a..af7566f0 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 c50a7145..9ec44bf3 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 70d093bc..5654149e 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 f592f9ac..c4c80a14 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 5d6b6cdb..d00e5bdd 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 d3583861..6e94ca1d 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 0a881302..f54520d3 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 90f9ce89..2a828777 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 2bee7820..a689e3bc 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 0cd194a3..f89ec5dd 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 06c4ab73..79821272 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 2ee4ab53..46878903 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 4e705500..70e2f5c5 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 e2b68fb5..bb1b70ca 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 edb9dbfa..90f752dd 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 e2ffacb1..e786b9fe 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 5241d93f..16121b54 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/521] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 457cd00b..bcda25e8 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/521] Update README.md --- README.md | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index bcda25e8..1ce87c31 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/521] Update README.md --- README.md | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 1ce87c31..544928c0 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/521] 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 5038f8c1..c649d397 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 a18569d0..7b00e06d 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 a566ef72..e36384a3 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 f0ecf14c..e1c122b9 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 7f13fb3b..b674e232 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 01cb8de3..2d4bcc28 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 a689e3bc..50df91d1 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 cd89b2c7..27675695 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 3906a85f..596f5619 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/521] 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 28d4d385..0c4622de 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 2d4bcc28..3d1f2d9c 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 1980329f..3ea1e8e6 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 ffed4f05..7e9da1c2 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 5654149e..1a75af2a 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 c4c80a14..f611cfdb 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 2a828777..2c93f951 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/521] 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 50451888..cd9d574f 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 a5675d17..f8583c3c 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 471d050c..59ae0431 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 6e94ca1d..c862d643 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 72da15ce..98cb07ab 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/521] 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 3d1f2d9c..6a41f7e9 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 ccc044b4..522ecfa3 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 c862d643..83864c4c 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 2c93f951..45c54ac0 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/521] 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 af7566f0..a1b9c7ed 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 b54272ea..d82edf56 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 613f25e6..a90618f1 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 83864c4c..c4c9bcdf 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 bee85edd..0e9b6437 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 b665a594..185ceb6a 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 8ca39e93..2fd8ba7a 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 f89ec5dd..7d17bc3a 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 79821272..f683c343 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 00000000..fed0f444 --- /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 e2a87665..343979a9 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 c394305e..1c83a629 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 40949f22..96aa5242 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 16121b54..72d52a5a 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/521] 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 185ceb6a..a8aaf86b 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 06d2ae42..a163f7f6 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 96aa5242..f26aa57b 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 72d52a5a..1254424b 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/521] 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 7810f4cb..c6caf42b 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 7b00e06d..68751f16 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 732a9419..05b3487d 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 6a41f7e9..6cd490d8 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 e07dda8c..2fdcfc8b 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 dbfd6372..75f693a5 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 c4c9bcdf..68a9d3c8 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 f54520d3..2d9c3eb5 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 0e9b6437..1d3a7bb3 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 a8aaf86b..d3b2f7ad 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 00000000..c682efa1 --- /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 f26aa57b..05162f39 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/521] 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 6cd490d8..38f08571 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 d0aa83d7..03f84b57 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 451bbdbd..31ccf1d3 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 68a9d3c8..db455909 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 d3b2f7ad..b434c7a4 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 06bd2635..e1294a0f 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/521] 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 38f08571..d4f881ce 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 9ec44bf3..eec4757f 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 af0b5df4..d1432737 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 03f84b57..8aedea68 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 db455909..06cfab49 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 b434c7a4..502ef61e 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 00000000..6fdee875 --- /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 e1294a0f..c0e628f2 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/521] 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 00000000..29314c43 --- /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/521] Update README.md --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 544928c0..cb32e0f8 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/521] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cb32e0f8..3524b75e 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/521] 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 d4f881ce..60db854d 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/521] 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 eee6be81..33782074 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 60db854d..e2b002dd 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 522ecfa3..3ff765e7 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 31447e1b..c2127a13 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 a1b9c7ed..ad025e31 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 ad2b0dd8..ac5832d5 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 f9bf95c8..874fe8db 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 d82edf56..7b6b64a9 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 8aedea68..d909432a 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 799199dd..bf140349 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 eb1be238..30e6e28f 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 06cfab49..9a3d3971 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 2d9c3eb5..41dc5183 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 1c83a629..d6035bc5 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 e786b9fe..24d812f5 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 c0e628f2..1015e9d5 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/521] 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 d4161d80..06651f38 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 00000000..582038d3 --- /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 00000000..cd95a4de --- /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/521] 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 eec4757f..bfcb44a4 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 9a3d3971..6e2f7292 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 502ef61e..91ea2301 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 00000000..10551460 --- /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 00000000..9898aac8 --- /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/521] 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 6e2f7292..ae23bfab 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/521] 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 ad025e31..da69b3a9 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 bfcb44a4..49241470 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 62fade00..145847ab 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/521] 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 33782074..eb25935a 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 e2b002dd..9ace5f11 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 2fdcfc8b..b2e519f5 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 17d4e4cd..49c34692 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/521] 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 343979a9..86c625df 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 7b9b49d6..18c02bfe 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 00000000..8e2d6d7c --- /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 1254424b..d52216fc 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/521] 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 18c02bfe..156ba561 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/521] 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 da69b3a9..ad025e31 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 49241470..bfcb44a4 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 145847ab..62fade00 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/521] 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 3ea1e8e6..9433e9ce 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 8e1c4992..ff627459 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 06651f38..445207a2 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 ae23bfab..38db294f 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 91ea2301..2ea4ef20 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 8e2d6d7c..e8d476c2 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 d52216fc..04b7e28a 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/521] 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 c2127a13..30aad3a3 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 ad025e31..b3705fd7 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 4268ceb3..1fa9d442 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 bfcb44a4..a1810d4a 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 d909432a..740fd453 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 62fade00..0c74da51 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 d00e5bdd..c967e7ec 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 1015e9d5..c71ee5fc 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/521] 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 1911f9c1..a3245c2c 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 38db294f..fdfada4b 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 2ea4ef20..433a642c 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 409c3f5d..d925e407 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 bf27e5a9..6abe79fd 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 b4f1f3c6..c13d1417 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 86c625df..e77279f6 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 d6035bc5..81f1e055 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 04b7e28a..0ddc15ad 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/521] 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 9433e9ce..ef0c8641 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 141dd5b9..00000000 --- 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 00000000..01abd5af --- /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 3f30f62a..689552e2 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 bf140349..6703cbbb 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 30e6e28f..00000000 --- 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 00000000..fbe90b19 --- /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 fdfada4b..8cc47cc1 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 433a642c..c93bf3a9 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 10c0c4ce..00000000 --- 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 4caa869b..00000000 --- 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 fc89a410..00000000 --- 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 00000000..c0097147 --- /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 00000000..e1596349 --- /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 582038d3..a3a17637 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 9898aac8..c9ededb7 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 bcf2023b..0de0e6ea 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 8b5da122..62aefade 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 be4950ed..91ec9dcc 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 fed0f444..be3f6a4e 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 f2fc105f..c66aa29f 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 a0f253d2..d4ceec26 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/521] 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 9ace5f11..861221b7 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 740fd453..3ff1b288 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 c71ee5fc..722a2bc4 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/521] 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 861221b7..b4165d82 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/521] 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 eb25935a..daad67a4 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/521] 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 01abd5af..a8c609d5 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 6703cbbb..39132c21 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 fbe90b19..72099488 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 8cc47cc1..d710c076 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 41dc5183..6b81e255 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 c93bf3a9..9af7215d 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/521] 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 ef0c8641..b74a9358 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 a8c609d5..55abe5bb 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 7b6b64a9..f06022cd 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 39132c21..1418790a 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 9af7215d..06e0fa93 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 c0097147..b69d632e 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 46878903..6f1c4e09 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 d4ceec26..add95b29 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/521] 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 06e0fa93..9d258f49 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 f683c343..00403e39 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/521] 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 2fd8ba7a..00000000 --- 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/521] 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 b74a9358..381054db 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 fec0ea65..59519c5c 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 55abe5bb..691eb14b 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 3ff1b288..2af864ae 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 a3245c2c..caf86cde 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 d710c076..345b8b3c 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 9d258f49..b234195a 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 d925e407..befc8c32 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 53b0bd96..00000000 --- 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 00000000..54a7e090 --- /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 b69d632e..61d3c3a7 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 00000000..886e9142 --- /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 895d5fe5..00000000 --- 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 00000000..21190971 --- /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 00000000..c297dc30 --- /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 00000000..3e2617a8 --- /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 6f1c4e09..00000000 --- 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 00000000..5e77d6cd --- /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 722a2bc4..b6873308 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/521] 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 b4165d82..ace13c4f 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 a1810d4a..d913aaf2 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 7e9da1c2..1d4f8e5f 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 345b8b3c..cfbfcba0 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 00000000..fe157f5d --- /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 6b81e255..283d8059 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 b234195a..9d50dbab 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 00000000..0722dd41 --- /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/521] 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 d913aaf2..e82b1088 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 cfbfcba0..eefb020f 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/521] 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 45c54ac0..b65be1ad 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 24d812f5..f88c7d5c 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/521] 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 e82b1088..2a26aad0 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/521] 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 691eb14b..638440bd 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 689552e2..00000000 --- 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/521] 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 00000000..62f507e8 --- /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 00000000..e3f6f2d3 --- /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/521] 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 381054db..02494e99 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 1418790a..ac5e5afc 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 eefb020f..e1a5cd53 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 283d8059..2e30de61 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 9d50dbab..936df451 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 54a7e090..cd35fd70 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 886e9142..cea049a3 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 c297dc30..ba0fd989 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 3e2617a8..dd707205 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 5e77d6cd..2d85ff1c 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/521] 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 f06022cd..04343e0a 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/521] 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 7d17bc3a..731595bb 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 00403e39..3534dab1 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 2d85ff1c..75ea7af4 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/521] 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 05162f39..3f32e50a 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/521] 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 3524b75e..570dd2a6 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/521] Update README.md --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 570dd2a6..502fd3d6 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/521] 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 e1c122b9..64132431 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 b674e232..3a861b32 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 ace13c4f..9eb782bc 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 fe157f5d..639ff40a 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 2e30de61..fb115a8f 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 936df451..c8cedfd4 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 0722dd41..5ee21597 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/521] 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 e36384a3..bf56f5f1 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 638440bd..f6be4e55 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 1a75af2a..49902981 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 f611cfdb..3e632ac2 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/521] 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 daad67a4..63ab038b 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 0c4622de..c6d0c6b0 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 64132431..09e153ab 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 9eb782bc..fb101b23 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 b2e519f5..64eccacb 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 2af864ae..f16678c8 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 1d4f8e5f..2323a886 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 639ff40a..33e027e5 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 fb115a8f..b1d4aee4 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 b65be1ad..71a7beb4 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 c8cedfd4..2cbb40b8 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 00000000..94f8238c --- /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 735f5146..48bf760e 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 2a89138c..7f74bf27 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 f58bced5..7e505072 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/521] 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 fb101b23..ba037c2c 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 33e027e5..d1c7f23f 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/521] 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 63ab038b..f2f8a05a 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 ba037c2c..2dbbf28e 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 04343e0a..bead7dd7 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 d1c7f23f..b9c5d691 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/521] 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 f2f8a05a..5fff99d1 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/521] 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 02494e99..24a0af97 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 62f507e8..9d27f426 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 f6be4e55..caf4d340 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 00000000..9006c332 --- /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 ac5e5afc..6b178b28 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 00000000..c71c56d2 --- /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 00000000..f2b59ec2 --- /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 72099488..3fffaa4d 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 e1a5cd53..c400ed2e 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 b1d4aee4..6bdcbb92 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 2cbb40b8..e68fcef4 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 cd35fd70..e489af20 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 61d3c3a7..4b5d5361 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 cea049a3..72012753 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 21190971..8e51f85a 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 00000000..81ff3514 --- /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 ba0fd989..716af65b 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 75ea7af4..0b102c13 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 c66aa29f..dc89c2d3 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 00000000..39baeefa --- /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/521] 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 24a0af97..663d4a96 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 9d27f426..cec7f082 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 00000000..30736ed3 --- /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 caf4d340..4519e64d 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 9006c332..e8d34e73 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 00000000..7f530936 --- /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 6b178b28..2776a6af 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 00000000..d9f8485c --- /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 c400ed2e..5eac496f 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 e68fcef4..cd965f73 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 72012753..b6f8bbe1 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 00000000..1a5f4075 --- /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 81ff3514..963aa2b1 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 dd707205..c5ba1565 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 e3f6f2d3..d57466e2 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 00000000..4020f237 --- /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 00000000..13cc7f7d --- /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/521] 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 09e153ab..6222a89b 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/521] 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 2a26aad0..b94b9e7b 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 cec7f082..39787e51 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 30736ed3..a6d556d8 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 874fe8db..6492881a 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 5eac496f..290b466f 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 6bdcbb92..eed731f1 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 cd965f73..559afeac 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 1a5f4075..a60111cf 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/521] 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 5fff99d1..852b739e 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 00000000..91cc380c --- /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/521] 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 a6d556d8..0aa0e4db 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 290b466f..cb3ab17d 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 559afeac..aef49e15 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 a60111cf..a6a8cf21 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/521] 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 39787e51..16cc3036 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 0aa0e4db..6ce4bdb0 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 6492881a..52b7c2d3 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 cb3ab17d..da775539 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 eed731f1..0848124d 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 aef49e15..8be99ade 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 a6a8cf21..2b4fa37f 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/521] 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 852b739e..e0973026 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/521] 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 2dbbf28e..8ce2022f 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/521] 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 16cc3036..ca6cf6b7 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 6ce4bdb0..7ab10379 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 52b7c2d3..9ba5514f 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 da775539..71207007 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 8be99ade..d5e6d9f1 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 2b4fa37f..0445a11c 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 4020f237..005fade6 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 13cc7f7d..7e995cd0 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/521] 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 ca6cf6b7..4fcf086c 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 7ab10379..4d97b2fb 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 7f530936..32768a3d 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 a90618f1..d00541cf 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 71207007..692b1305 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 d5e6d9f1..1c175cf7 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/521] 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 4fcf086c..9d8b773f 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 4d97b2fb..17f885d6 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 bead7dd7..e613abc8 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 3fffaa4d..6d5f207f 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 692b1305..b5ab55d5 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 1c175cf7..0b502c1c 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 005fade6..e1401237 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 7e995cd0..4e7dbbc7 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/521] 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 9d8b773f..07da2854 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 17f885d6..1e56e6db 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 32768a3d..d0e82800 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 0b502c1c..1f10f9e5 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 00000000..30f64890 --- /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/521] 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 f8fa3c6a..a233557a 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 07da2854..00fa9d8c 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 1e56e6db..1817f96c 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 f16678c8..d80d6a91 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 d0e82800..85bf29ae 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 b5ab55d5..1893ccaa 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 00000000..24b752fa --- /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 30f64890..333e5c74 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 d57466e2..9c899ec4 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 e1401237..e61a541d 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 4e7dbbc7..0044dd32 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 00000000..5234c993 --- /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 ed75e18a..c587612e 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/521] 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 663d4a96..bffad9cb 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 2776a6af..638a9df7 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 6d5f207f..eaa760fd 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/521] 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 00fa9d8c..9929de8a 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 d9f8485c..02db9d31 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 1893ccaa..5e229676 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 1f10f9e5..4ae7e57e 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 24b752fa..3a5f7131 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 0044dd32..ba23a468 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/521] 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 9929de8a..7676dd43 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 1817f96c..4086fe94 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 d80d6a91..8c4c166c 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 85bf29ae..3c8e8c4c 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 d00541cf..72c4d7e7 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 4ae7e57e..8d5e97c4 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 00000000..737c6c15 --- /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 00000000..f0d5fa95 --- /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 9539e688..f72e9b9d 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/521] 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 75f693a5..6869652c 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 05fdbf58..0f131414 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/521] 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 e0973026..27a179a0 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 3ff765e7..144503a8 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 b94b9e7b..247b3a7d 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 7676dd43..36b372ff 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 4086fe94..c2938e50 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 c4b965f9..cafbf66b 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 9ba5514f..326e5648 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 8c4c166c..6536c0b7 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 eaa760fd..5d1edaaa 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 5e229676..84aa28ef 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 8d5e97c4..65117233 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 36005897..ce04a4c3 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 590c7b0a..de543396 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 00000000..3552affc --- /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 81f1e055..8dbd78a4 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 00000000..ffa0f091 --- /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 ebcd4697..00000000 --- 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 bb1b70ca..03dc2332 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 0ddc15ad..e25ef674 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/521] 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 27a179a0..f81788fb 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 8ce2022f..09a9e88f 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 6869652c..fa9326e8 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 f0d5fa95..df1e9813 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 f72e9b9d..b7449231 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/521] 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 f8583c3c..721eff1c 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 fa9326e8..7a99dd08 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 6536c0b7..af1909ee 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 1d3a7bb3..8d6a05e8 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 502fd3d6..c0bc694e 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 b7449231..fe68cfe4 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 0f131414..cd4a267f 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/521] 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 af1909ee..58b021d5 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 716af65b..6dc12f1a 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/521] 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 00000000..7b8a3f8c --- /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 326e5648..2f14c15d 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 58b021d5..150d9e8d 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 84aa28ef..49f300aa 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 00000000..2c9e0ff1 --- /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 e61a541d..324866d8 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 fe68cfe4..1da5d1ec 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/521] 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 f81788fb..02972aa7 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 c6d0c6b0..997e367a 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 6222a89b..1f0cb5fa 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 09a9e88f..05023b83 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 2323a886..91484b4d 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 b9c5d691..5d897439 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 71a7beb4..2fb92ee5 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 65117233..aea12eba 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/521] 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 d1432737..974a9915 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 00000000..ee87e61f --- /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/521] 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 c967e7ec..fc2c6d93 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 aea12eba..3cc94d72 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 ffe7b930..c036ff0c 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 03dc2332..8b0f67de 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 2c9e0ff1..4338a000 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/521] 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 c2938e50..3e938ca6 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 49f300aa..7c7d77f8 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/521] 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 2fb92ee5..634d056d 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/521] 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 596f5619..3bf5ff31 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/521] 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 7c7d77f8..a7980ee8 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/521] 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 3a861b32..ea526945 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 05023b83..56ac125e 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 721eff1c..27b1db53 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 36b372ff..24f9be33 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 4519e64d..6ca75475 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 5d1edaaa..bd74e6e3 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 a7980ee8..011dfae3 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 3cc94d72..e219e143 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 6ecf96d3..551bb1d5 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 10551460..bbf0250a 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 49c34692..98a67b14 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 27675695..99b491ed 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 8b0f67de..ae4e70fd 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/521] 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 b3705fd7..32c9f685 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 6da72fd2..943cd211 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 1fa9d442..b52e046b 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 00000000..f0bd46ea --- /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 247b3a7d..98078287 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 3e938ca6..6e22d157 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 ff627459..00000000 --- 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 33f6851e..00000000 --- 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 00000000..a4670ae9 --- /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 445207a2..00af0e74 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 0c74da51..e6bb90c8 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 011dfae3..d8ff87cc 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 0848124d..2db9e8e7 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 122de31e..0d69e5ff 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 e81d5356..1d38dc2b 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 aee1b0f2..dd3f41db 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/521] 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 98078287..b0e30650 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 d8ff87cc..2be965e6 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/521] 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 943cd211..c3169482 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/521] 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 32c9f685..08c0b294 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 b52e046b..6cb09471 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 b0e30650..d2701888 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 00000000..008a5551 --- /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 fc2c6d93..00000000 --- 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 2be965e6..44b55ac6 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 e219e143..89b7f33e 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 1d38dc2b..6d9d3968 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 dd3f41db..05903fae 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 89d806c6..71b72ae8 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 a163f7f6..de5bad2b 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/521] 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 24f9be33..295e7452 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 324866d8..01f97641 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/521] 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 6cb09471..103e29bc 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 00000000..2fef7bbc --- /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 00000000..b4469c35 --- /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 008a5551..a8b57614 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/521] 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 d2701888..d7383913 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 e613abc8..2568cd66 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 150d9e8d..baaca995 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 00000000..fe1385a2 --- /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 44b55ac6..f3bf0cbf 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 89b7f33e..fa5ff4a7 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 00000000..8766f280 --- /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 00000000..5a4d50ee --- /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 00000000..eca761b6 --- /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 c036ff0c..b70a8d2a 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 ae4e70fd..6072e96a 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 1da5d1ec..7854455b 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/521] 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 5d897439..35c1d129 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 634d056d..3c74e66a 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 94f8238c..2c01c51d 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 6a85e68412f0cb892b549a1aff3a0cfb03102f05 Mon Sep 17 00:00:00 2001 From: morm075 <124874578+morm075@users.noreply.github.com> Date: Thu, 2 Jan 2025 08:06:48 +1030 Subject: [PATCH 140/521] Updating to include Steam Networhing for NATpunch --- .../Networking/Managers/Client/NetworkClient.cs | 10 ---------- .../Networking/Managers/Server/LobbyServerManager.cs | 2 ++ 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 44b55ac6..422831e0 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -225,16 +225,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 diff --git a/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs b/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs index 634d056d..8fc7142f 100644 --- a/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs +++ b/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs @@ -38,6 +38,8 @@ public class LobbyServerManager : MonoBehaviour private string server_id { get; set; } private string private_key { get; set; } + private Lobby lobby; + private bool initialised = false; private bool sendUpdates = false; private float timePassed = 0f; From 3274b8b4a603e77d0ba5b14bf565158572e66b7c Mon Sep 17 00:00:00 2001 From: AMacro Date: Thu, 2 Jan 2025 09:28:22 +1030 Subject: [PATCH 141/521] 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 56ac125e..11f1ab5c 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 142/521] 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 59ae0431..395e8aed 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 f3bf0cbf..3b074767 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 00000000..705192ca --- /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 f88c7d5c..8c35e652 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 143/521] 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 8c35e652..098cce6a 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 144/521] Edited french locale --- locale.csv | 102 ++++++++++++++++++++++++++--------------------------- 1 file changed, 51 insertions(+), 51 deletions(-) diff --git a/locale.csv b/locale.csv index cd4a267f..10320738 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 145/521] 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 a233557a..9c71626e 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 02972aa7..996f35c1 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 11f1ab5c..331388a8 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 64eccacb..3bb65972 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 d7383913..76160dc0 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 638a9df7..0d4cc6de 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 bd74e6e3..123dc8ff 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 e6bb90c8..84b5eef2 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 caf86cde..eab4bd29 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 3b074767..81e58405 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 35c1d129..ed6f618a 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 2db9e8e7..a7c5163f 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 3c74e66a..b29397f5 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 fa5ff4a7..544831f7 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 4b5d5361..bd51543b 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 71b72ae8..0b407fa8 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 de5bad2b..3b001851 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 5a4d50ee..2ee1d5a4 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 6072e96a..62d33fe6 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 146/521] 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 76160dc0..646c39f1 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 147/521] 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 6ca75475..43d660f8 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 974a9915..5ee3a8f0 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 123dc8ff..1109f998 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 81e58405..b05b55ad 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 148/521] 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 646c39f1..51edeab7 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 544831f7..67808582 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 6fdee875..5516356d 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 149/521] 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 51edeab7..31041f65 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 150/521] 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 31041f65..71e8c476 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 fe1385a2..0944e72e 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 b05b55ad..5e97c782 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 eca761b6..8dc27c87 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 705192ca..41a4375c 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 151/521] 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 43d660f8..0534c6d6 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 5e97c782..89722867 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 67808582..0ba7779c 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 0b102c13..8fa94019 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 152/521] 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 0534c6d6..86556f20 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 0d4cc6de..1a5b676a 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 1109f998..37caf8c8 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 89722867..855c2ddd 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 153/521] 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 855c2ddd..7c0e4d88 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 154/521] 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 7c0e4d88..b4fe0069 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 155/521] 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 00000000..f0243e0c --- /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 156/521] 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 00000000..994adec8 --- /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 0ba7779c..63f80bfe 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 157/521] 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 b4fe0069..7f69d12f 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 158/521] 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 63f80bfe..9e1c40f0 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 159/521] 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 2568cd66..2b173354 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 160/521] 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 a7c5163f..ea147f51 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 161/521] 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 71e8c476..b56b72c9 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 baaca995..d11332ac 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 7854455b..53b0b645 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 162/521] 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 103e29bc..6a6261f5 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 994adec8..1e8dd877 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 a8b57614..afd580d3 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 163/521] 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 bffad9cb..2f3232dc 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 86556f20..bed6a24e 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 7f69d12f..f7acc2ac 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 9e1c40f0..c48f02f7 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 8fa94019..35d52432 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 164/521] 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 6a6261f5..4625a3db 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 b56b72c9..94ff7d47 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 0944e72e..36845ad2 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 b4469c35..3424cb3e 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 f7acc2ac..135a7a21 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 c48f02f7..6cad5411 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 2ee1d5a4..a424d237 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 8dc27c87..e105c80f 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 41a4375c..d62c9242 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 165/521] 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 94ff7d47..fc50b486 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 166/521] 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 135a7a21..b040a417 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 167/521] 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 6cad5411..aa7eafa1 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 168/521] 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 e8d476c2..09a6e679 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 169/521] 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 ea147f51..e5d34f8a 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 aa7eafa1..df1365de 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 170/521] 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 d11332ac..d885efeb 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 53b0b645..a796e061 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 171/521] Readded quotes --- locale.csv | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/locale.csv b/locale.csv index 10320738..58524588 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 172/521] 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 963aa2b1..9bccf2dc 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 35d52432..704e4992 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 173/521] 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 fc50b486..44771141 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 b040a417..04b97141 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 df1365de..483bfedc 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 5516356d..d75f5713 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 174/521] 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 bed6a24e..4d08c82b 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 175/521] 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 08c0b294..cf29ccf4 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 44771141..a908e373 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 176/521] 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 cf29ccf4..d5d67529 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 84b5eef2..e3d12d39 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 177/521] 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 f0bd46ea..9ec043d3 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 04b97141..7dc23d33 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 178/521] 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 704e4992..79c7a909 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 179/521] 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 331388a8..b6bbb222 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 098cce6a..65a0aa4d 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 00000000..db7d957c --- /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 a967ef265482ddd32517b57e14b22c4e77df08e4 Mon Sep 17 00:00:00 2001 From: morm075 <124874578+morm075@users.noreply.github.com> Date: Sun, 12 Jan 2025 17:50:46 +1030 Subject: [PATCH 180/521] Further work to Steam integration --- .../Components/MainMenu/ServerBrowserPane.cs | 19 +++++ .../Components/Networking/NetworkLifecycle.cs | 32 +++++++- Multiplayer/Multiplayer.cs | 1 + Multiplayer/Multiplayer.csproj | 26 +++---- .../Managers/Client/NetworkClient.cs | 12 +++ .../Managers/Client/ServerBrowserClient.cs | 8 +- .../Managers/Server/LobbyServerManager.cs | 74 +++++++++++++++++++ .../Managers/Server/NetworkServer.cs | 65 +++++++++++++++- 8 files changed, 212 insertions(+), 25 deletions(-) diff --git a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs index 56ac125e..bef82d3a 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs @@ -19,6 +19,8 @@ using LiteNetLib; using Multiplayer.Networking.Listeners; using System.Collections.Generic; +using Steamworks; +using Steamworks.Data; namespace Multiplayer.Components.MainMenu { @@ -130,6 +132,7 @@ private enum ConnectionState private Popup connectingPopup; private int attempt; + private Lobby[] lobbies; #region setup @@ -435,6 +438,10 @@ private void RefreshAction() StartCoroutine(GetRequest($"{Multiplayer.Settings.LobbyServerAddress}/list_game_servers")); + if (DVSteamworks.Success) + ListActiveLobbies(); + + //Send a message to find local peers discoveryTimer = 0f; serverBrowserClient?.SendDiscoveryRequest(); @@ -998,6 +1005,18 @@ IEnumerator GetRequest(string uri) } } + private async void ListActiveLobbies() + { + lobbies = await SteamMatchmaking.LobbyList.WithMaxResults(100).RequestAsync(); + foreach (var lobby in lobbies) + { + var name = lobby.GetData("Server Name"); + var difficulty = lobby.GetData("Difficulty"); + Multiplayer.Log($"Steamworks Lobby Server name: \"{name}\", Difficulty: {difficulty}"); + } + + } + private void RefreshGridView() { diff --git a/Multiplayer/Components/Networking/NetworkLifecycle.cs b/Multiplayer/Components/Networking/NetworkLifecycle.cs index 64eccacb..fa512b0c 100644 --- a/Multiplayer/Components/Networking/NetworkLifecycle.cs +++ b/Multiplayer/Components/Networking/NetworkLifecycle.cs @@ -11,6 +11,7 @@ using Multiplayer.Networking.Listeners; using Multiplayer.Utils; using Newtonsoft.Json; +using Steamworks; using UnityEngine; using UnityEngine.SceneManagement; @@ -121,6 +122,13 @@ public void QueueMainMenuEvent(Action action) public bool StartServer(IDifficulty difficulty) { + // Check if the user is a legitimate Steam user before proceeding + if (!IsLegitimateSteamUser()) + { + Multiplayer.Log("User is not a legitimate Steam user. Server start aborted."); + return false; + } + int port = Multiplayer.Settings.Port; if (Server != null) @@ -128,7 +136,7 @@ public bool StartServer(IDifficulty difficulty) if (!isSinglePlayer) { - if(serverData != null) + if (serverData != null) { port = serverData.port; } @@ -137,16 +145,34 @@ public bool StartServer(IDifficulty difficulty) Multiplayer.Log($"Starting server on port {port}"); NetworkServer server = new(difficulty, Multiplayer.Settings, isSinglePlayer, serverData); - //reset for next game + // Reset for next game 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; } + + private bool IsLegitimateSteamUser() + { + if (SteamClient.IsValid) + { + // Verify the Steam ID is valid and owned + if (SteamClient.SteamId.IsValid) + { + System.Console.WriteLine($"Steam ID {SteamClient.SteamId} is valid."); + return true; + } + } + + System.Console.WriteLine("Invalid or pirated Steam account detected."); + return false; + } + public void StartClient(string address, int port, string password, bool isSinglePlayer, Action onDisconnect ) { if (Client != null) diff --git a/Multiplayer/Multiplayer.cs b/Multiplayer/Multiplayer.cs index e613abc8..864111d8 100644 --- a/Multiplayer/Multiplayer.cs +++ b/Multiplayer/Multiplayer.cs @@ -13,6 +13,7 @@ using UnityChan; using UnityEngine; using UnityModManagerNet; +using Steamworks; namespace Multiplayer; diff --git a/Multiplayer/Multiplayer.csproj b/Multiplayer/Multiplayer.csproj index 150d9e8d..48215c66 100644 --- a/Multiplayer/Multiplayer.csproj +++ b/Multiplayer/Multiplayer.csproj @@ -5,17 +5,18 @@ Multiplayer 0.1.9.2 - - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - - + @@ -42,13 +43,11 @@ - - + - - + @@ -66,10 +65,9 @@ ../build/UnityChan.dll - + - - + @@ -86,7 +84,6 @@ - @@ -95,16 +92,13 @@ - - - - + \ No newline at end of file diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 422831e0..1f01ffd2 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -1202,4 +1202,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}"); + } } diff --git a/Multiplayer/Networking/Managers/Client/ServerBrowserClient.cs b/Multiplayer/Networking/Managers/Client/ServerBrowserClient.cs index 5d897439..1b06f31b 100644 --- a/Multiplayer/Networking/Managers/Client/ServerBrowserClient.cs +++ b/Multiplayer/Networking/Managers/Client/ServerBrowserClient.cs @@ -8,6 +8,10 @@ using System.Linq; using Multiplayer.Networking.Managers.Server; using Multiplayer.Networking.Data; +using Steamworks; +using System.Text; +using Steamworks.Data; +using UnityEngine; namespace Multiplayer.Networking.Listeners; @@ -168,7 +172,7 @@ private void OnUnconnectedDiscoveryPacket(UnconnectedDiscoveryPacket packet, IPE { case DiscoveryPacketType.Response: //Log($"OnUnconnectedDiscoveryPacket({packet.PacketType}, {endPoint?.Address}) id: {packet.data.id}"); - OnDiscovery?.Invoke(endPoint,packet.data); + OnDiscovery?.Invoke(endPoint, packet.data); break; } } @@ -236,5 +240,5 @@ public override void OnNatIntroductionSuccess(IPEndPoint targetEndPoint, NatAddr { //do other stuff here } - #endregion + #endregion } diff --git a/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs b/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs index 8fc7142f..640f8f6c 100644 --- a/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs +++ b/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs @@ -16,6 +16,8 @@ using System.Net; using LocoSim.Implementations; using System.Linq; +using Steamworks; +using Steamworks.Data; namespace Multiplayer.Networking.Managers.Server; public class LobbyServerManager : MonoBehaviour @@ -57,6 +59,72 @@ private void Awake() server = NetworkLifecycle.Instance.Server; Multiplayer.Log($"LobbyServerManager New({server != null})"); + + if (DVSteamworks.Success) + { + CreateLobby(); + } + } + + public async void CreateLobby() + { + // Check if the user is a legitimate Steam user + if (!IsLegitimateSteamUser()) + { + server.Log("User is suspected to be using a pirated copy. Lobby creation aborted."); + return; + } + + // Specify the lobby type (public, private, etc.) + var result = await SteamMatchmaking.CreateLobbyAsync(server.serverData.MaxPlayers); + + if (result.HasValue) + { + // Lobby was created successfully + server.Log("Steam Lobby created successfully!"); + lobby = result.Value; + lobby.SetPublic(); + lobby.SetJoinable(true); + lobby.SetData("Server Name", server.serverData.Name); + lobby.SetData("Difficulty", server.serverData.Difficulty.ToString()); + } + else + { + // Handle failure + server.Log("Failed to create lobby."); + } + } + + private bool IsLegitimateSteamUser() + { + // Check if the Steam client is valid + if (SteamClient.IsValid) + { + // Verify the Steam ID is valid + if (SteamClient.SteamId.IsValid) + { + + // Check if the game is installed using the App ID + bool isInstalled = SteamApps.IsAppInstalled(DVSteamworks.APP_ID); + + if (isInstalled) + { + System.Console.WriteLine($"Steam ID {SteamClient.SteamId} is valid and the game is installed."); + return true; + } + else + { + // Log the piracy suspicion + server.Log($"Suspicion: Steam ID {SteamClient.SteamId} does not have the game installed. Potential piracy detected."); + } + } + } + + // If Steam client or Steam ID is not valid, log as suspicious + System.Console.WriteLine("Steam client is invalid or pirated Steam account detected."); + server.Log("Suspicion: Invalid Steam client or pirated Steam account detected."); + + return false; } private IEnumerator Start() @@ -103,6 +171,12 @@ private void OnDestroy() StopAllCoroutines(); StartCoroutine(RemoveFromLobbyServer($"{Multiplayer.Settings.LobbyServerAddress}/{ENDPOINT_REMOVE_SERVER}")); + if (lobby.Id.IsValid) + { + lobby.SetJoinable(false); + lobby.Leave(); + } + discoveryManager?.Stop(); } diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index 89b7f33e..00501f9f 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -32,6 +32,7 @@ using Multiplayer.Networking.Packets.Serverbound.Train; using Multiplayer.Networking.Packets.Unconnected; using System.Text; +using Steamworks; namespace Multiplayer.Networking.Listeners; @@ -149,11 +150,21 @@ private void OnLoaded() Log($"Server loaded, processing {joinQueue.Count} queued players"); IsLoaded = true; + while (joinQueue.Count > 0) { NetPeer peer = joinQueue.Dequeue(); - if (peer.ConnectionState == ConnectionState.Connected) + + // Assuming the `peer.ConnectionState` property exists and is being checked + if (peer.ConnectionState.Equals(LiteNetLib.ConnectionState.Connected)) + { + System.Console.WriteLine("Connection is established."); OnServerboundClientReadyPacket(null, peer); + } + else + { + System.Console.WriteLine("Connection is not established."); + } } } @@ -218,14 +229,60 @@ public override void OnConnectionRequest(ConnectionRequest request) #endregion - #region NAT Punch Events +#region NAT Punch Events + + public override void OnNatIntroductionRequest(IPEndPoint localEndPoint, IPEndPoint remoteEndPoint, string token) { - //do some stuff here + // Validate the Steam ID + if (!IsLegitimateSteamUser()) + { + System.Console.WriteLine($"NAT request rejected from {remoteEndPoint}. Invalid Steam account."); + return; // Reject the NAT punch request + } + + // Proceed with NAT punch-through handling + System.Console.WriteLine($"NAT request accepted from {remoteEndPoint}."); + // Additional logic for NAT punch-through } + public override void OnNatIntroductionSuccess(IPEndPoint targetEndPoint, NatAddressType type, string token) { - //do other stuff here + System.Console.WriteLine($"NAT punch-through successful to {targetEndPoint}."); + // Additional logic after successful NAT punch-through + } + + // Check for a legitimate Steam account + private bool IsLegitimateSteamUser() + { + // Check if the Steam client is valid + if (SteamClient.IsValid) + { + // Verify the Steam ID is valid + if (SteamClient.SteamId.IsValid) + { + + // Check if the game is installed using the App ID + bool isInstalled = SteamApps.IsAppInstalled(DVSteamworks.APP_ID); + + if (isInstalled) + { + System.Console.WriteLine($"Steam ID {SteamClient.SteamId} is valid and the game is installed."); + return true; + } + else + { + // Log the piracy suspicion + Multiplayer.Log($"Suspicion: Steam ID {SteamClient.SteamId} does not have the game installed. Potential piracy detected."); + } + } + } + + // If Steam client or Steam ID is not valid, log as suspicious + System.Console.WriteLine("Steam client is invalid or pirated Steam account detected."); + Multiplayer.Log("Suspicion: Invalid Steam client or pirated Steam account detected."); + + return false; } #endregion From 45739a8fc6a57cff4e19c21498260100b0836c79 Mon Sep 17 00:00:00 2001 From: AMacro Date: Mon, 13 Jan 2025 21:52:57 +1000 Subject: [PATCH 181/521] 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 3bb65972..ddf222ea 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 395e8aed..0e344bcf 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 182/521] 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 7dc23d33..b0503958 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 183/521] 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 59519c5c..561267aa 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 184/521] 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 a908e373..71a4b7ce 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 185/521] 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 144503a8..deb481a5 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 d885efeb..f78b0810 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 a796e061..2b46338d 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 186/521] 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 b0503958..7d610157 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 187/521] 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 b29397f5..6fac8b13 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 188/521] 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 71a4b7ce..d3adc285 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 189/521] 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 d3adc285..1d49178e 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 190/521] 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 deb481a5..fe99948d 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; From c4f358ab7b792959f9de0371a53c5a4fcc9978db Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 18 Jan 2025 12:40:43 +1000 Subject: [PATCH 191/521] Fix UI coupling issues Add support for External Camera UI and fix bugs that are in the vanilla game --- .../Networking/Train/NetworkedTrainCar.cs | 40 +++++++-- .../Managers/Client/NetworkClient.cs | 11 ++- .../Patches/Train/CouplerInterfacerPatch.cs | 87 ------------------- Multiplayer/Patches/Train/CouplerPatch.cs | 1 + .../Patches/Train/UICouplingHelperPatch.cs | 64 ++++++++++++++ 5 files changed, 106 insertions(+), 97 deletions(-) delete mode 100644 Multiplayer/Patches/Train/CouplerInterfacerPatch.cs create mode 100644 Multiplayer/Patches/Train/UICouplingHelperPatch.cs diff --git a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs index 1d49178e..414077b3 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs @@ -498,7 +498,7 @@ private void Server_SendHealthState() public bool Server_ValidateCouplerInteraction(CommonCouplerInteractionPacket packet, NetPeer peer) { Multiplayer.LogDebug(() => - $"Server_ValidateCouplerInteraction([{(CouplerInteractionType)packet.Flags}, {CurrentID}, {packet.NetId}], {peer.Id}) " + + $"Server_ValidateCouplerInteraction([[{(CouplerInteractionType)packet.Flags}], {CurrentID}, {packet.NetId}], {peer.Id}) " + $"isFront: {packet.IsFrontCoupler}, frontInteracting: {frontInteracting}, frontInteractionPeer: {frontInteractionPeer}, " + $"rearInteracting: {rearInteracting}, rearInteractionPeer: {rearInteractionPeer}" ); @@ -895,11 +895,24 @@ public void Common_ReceiveCouplerInteraction(CommonCouplerInteractionPacket pack 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 hose connect also requested, then we want everything to connect, otherwise only connect the chain + bool chainInteraction = !flags.HasFlag(CouplerInteractionType.HoseConnect); + + Multiplayer.LogDebug(() => $"10 Common_ReceiveCouplerInteraction() [{TrainCar?.ID}, {NetId}], coupler is front: {packet.IsFrontCoupler}, flags: [{flags}], other coupler: {otherCoupler != null}, chainInteraction: {chainInteraction}"); 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(() => $"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}, chainInteraction: {chainInteraction}"); + var car = coupler.CoupleTo(otherCoupler, viaChainInteraction: chainInteraction); + + /* fix for bug in vanilla game */ + coupler.SetChainTight(true); + if (coupler.ChainScript.enabled) + { + coupler.ChainScript.enabled = false; + coupler.ChainScript.enabled = true; + } + /* end fix for bug */ + Multiplayer.LogDebug(() => $"10B Common_ReceiveCouplerInteraction() [{TrainCar?.ID}, {NetId}], result: {car != null}"); //todo: rework hose and MU interactions } @@ -907,9 +920,22 @@ public void Common_ReceiveCouplerInteraction(CommonCouplerInteractionPacket pack 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 hose connect also requested, then we want everything to disconnect, otherwise only disconnect the chain + bool chainInteraction = !flags.HasFlag(CouplerInteractionType.HoseDisconnect); + + Multiplayer.LogDebug(() => $"11 Common_ReceiveCouplerInteraction() [{TrainCar?.ID}, {NetId}], coupler is front: {packet.IsFrontCoupler}, flags: {flags}, chainInteraction: {chainInteraction}"); + CouplerLogic.Uncouple(coupler,viaChainInteraction: chainInteraction); + + /* fix for bug in vanilla game */ + coupler.state = ChainCouplerInteraction.State.Parked; + if (coupler.ChainScript.enabled) + { + coupler.ChainScript.enabled = false; + coupler.ChainScript.enabled = true; + } + /* end fix for bug */ + + //todo: rework hose and MU interactions } if (flags.HasFlag(CouplerInteractionType.CoupleViaRemote)) diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 7d610157..4c40ae05 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -1030,6 +1030,7 @@ public void SendCouplerInteraction(CouplerInteractionType flags, Coupler coupler { ushort couplerNetId = coupler?.train?.GetNetId() ?? 0; ushort otherCouplerNetId = otherCoupler?.train?.GetNetId() ?? 0; + bool couplerIsFront = coupler?.isFrontCoupler ?? false; bool otherCouplerIsFront = otherCoupler?.isFrontCoupler ?? false; if (couplerNetId == 0) @@ -1038,13 +1039,17 @@ public void SendCouplerInteraction(CouplerInteractionType flags, Coupler coupler return; } - Log($"Sending coupler interaction {flags} for {coupler?.train?.ID}"); - LogDebug(() => $"SendCouplerInteraction({flags}, {coupler?.train?.ID}, {otherCoupler?.train?.ID}) coupler isFront: {coupler?.isFrontCoupler}, otherCoupler isFront: {otherCouplerIsFront}"); + LogDebug(() => $"SendCouplerInteraction([{flags}], {coupler?.train?.ID}, {otherCoupler?.train?.ID}) coupler isFront: {couplerIsFront}, otherCoupler isFront: {otherCouplerIsFront}"); + + if (coupler == null) + return; + + Log($"Sending coupler interaction [{flags}] for {coupler?.train?.ID}, {(couplerIsFront ? "Front" : "Rear")}"); SendPacketToServer(new CommonCouplerInteractionPacket { NetId = couplerNetId, - IsFrontCoupler = coupler.isFrontCoupler, + IsFrontCoupler = couplerIsFront, OtherNetId = otherCouplerNetId, IsFrontOtherCoupler = otherCouplerIsFront, Flags = (ushort)flags, diff --git a/Multiplayer/Patches/Train/CouplerInterfacerPatch.cs b/Multiplayer/Patches/Train/CouplerInterfacerPatch.cs deleted file mode 100644 index e105c80f..00000000 --- a/Multiplayer/Patches/Train/CouplerInterfacerPatch.cs +++ /dev/null @@ -1,87 +0,0 @@ -using DV.HUD; -using HarmonyLib; -using Multiplayer.Components.Networking; -using Multiplayer.Networking.Data.Train; -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.Start | 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.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}"); - 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 b70a8d2a..de8bffa3 100644 --- a/Multiplayer/Patches/Train/CouplerPatch.cs +++ b/Multiplayer/Patches/Train/CouplerPatch.cs @@ -15,6 +15,7 @@ private static void ConnectAirHose(Coupler __instance, Coupler other, bool playA if (UnloadWatcher.isUnloading || NetworkLifecycle.Instance.IsProcessingPacket) return; + NetworkLifecycle.Instance.Client?.SendHoseConnected(__instance, other, playAudio); } diff --git a/Multiplayer/Patches/Train/UICouplingHelperPatch.cs b/Multiplayer/Patches/Train/UICouplingHelperPatch.cs new file mode 100644 index 00000000..a85f911d --- /dev/null +++ b/Multiplayer/Patches/Train/UICouplingHelperPatch.cs @@ -0,0 +1,64 @@ +using DV.HUD; +using HarmonyLib; +using Multiplayer.Components.Networking; +using Multiplayer.Networking.Data.Train; +using Newtonsoft.Json.Linq; + +namespace Multiplayer.Patches.Train; + + +[HarmonyPatch(typeof(UICouplingHelper))] +public static class UICouplingHelperPatch +{ + [HarmonyPatch(nameof(UICouplingHelper.HandleCoupling))] + [HarmonyPostfix] + private static void HandleCoupling(UICouplingHelper __instance, Coupler coupler, bool advanced) + { + Multiplayer.LogDebug(() => $"UICouplingHelper.HandleCoupling({coupler?.train?.ID}, {advanced})"); + + if (coupler == null) + return; + + Coupler otherCoupler = null; + CouplerInteractionType interaction = CouplerInteractionType.Start; + + if (coupler.IsCoupled()) + { + interaction |= CouplerInteractionType.CoupleViaUI; + otherCoupler = coupler.coupledTo; + + if(advanced) + { + interaction |= CouplerInteractionType.HoseConnect | CouplerInteractionType.CockOpen; + } + + Multiplayer.LogDebug(() => $"UICouplingHelper.HandleCoupling({coupler?.train?.ID}, {advanced}) coupler is front: {coupler?.isFrontCoupler}, otherCoupler: {otherCoupler?.train?.ID}, otherCoupler is front: {otherCoupler?.isFrontCoupler}, action: {interaction}"); + + if (otherCoupler == null) + return; + + /* fix for bug in vanilla game */ + coupler.SetChainTight(true); + coupler.ChainScript.enabled = false; + coupler.ChainScript.enabled = true; + /* end fix for bug */ + } + else + { + interaction |= CouplerInteractionType.UncoupleViaUI; + + if (advanced) + { + interaction |= CouplerInteractionType.HoseDisconnect | CouplerInteractionType.CockClose; + } + + /* fix for bug in vanilla game */ + coupler.state = ChainCouplerInteraction.State.Parked; + coupler.ChainScript.enabled = false; + coupler.ChainScript.enabled = true; + /* end fix for bug */ + } + + NetworkLifecycle.Instance.Client.SendCouplerInteraction(interaction, coupler, otherCoupler); + } +} From 507c2409d53bdf26880422e6d1c332e8dd564033 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 18 Jan 2025 16:39:01 +1000 Subject: [PATCH 192/521] Add train paint updates --- .../Networking/Train/NetworkedTrainCar.cs | 52 ++++++++++++++++++- .../Managers/Client/NetworkClient.cs | 34 +++++++++++- .../Managers/Server/NetworkServer.cs | 13 ++++- .../Common/Train/CommonPaintThemePacket.cs | 8 +++ 4 files changed, 104 insertions(+), 3 deletions(-) create mode 100644 Multiplayer/Networking/Packets/Common/Train/CommonPaintThemePacket.cs diff --git a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs index 414077b3..1230168c 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs @@ -2,6 +2,7 @@ using System.Collections; using System.Collections.Generic; using System.Linq; +using DV.Customization.Paint; using DV.MultipleUnit; using DV.Simulation.Brake; using DV.Simulation.Cars; @@ -188,7 +189,11 @@ public void Start() brakeSystem.HandbrakePositionChanged += Common_OnHandbrakePositionChanged; brakeSystem.BrakeCylinderReleased += Common_OnBrakeCylinderReleased; - + + if (TrainCar.PaintExterior != null) + TrainCar.PaintExterior.OnThemeChanged += Common_OnPaintThemeChange; + if (TrainCar.PaintInterior != null) + TrainCar.PaintInterior.OnThemeChanged += Common_OnPaintThemeChange; NetworkLifecycle.Instance.OnTick += Common_OnTick; if (NetworkLifecycle.Instance.IsHost()) @@ -242,6 +247,11 @@ public void OnDisable() brakeSystem.BrakeCylinderReleased -= Common_OnBrakeCylinderReleased; } + if(TrainCar.PaintExterior != null) + TrainCar.PaintExterior.OnThemeChanged -= Common_OnPaintThemeChange; + if (TrainCar.PaintInterior != null) + TrainCar.PaintInterior.OnThemeChanged -= Common_OnPaintThemeChange; + if (NetworkLifecycle.Instance.IsHost()) { bogie1.TrackChanged -= Server_BogieTrackChanged; @@ -703,6 +713,20 @@ private void Common_OnPortUpdated(Port port) dirtyPorts.Add(port.id); } + private void Common_OnPaintThemeChange(TrainCarPaint paintController) + { + if(paintController == null) + return; + + Multiplayer.LogDebug(() => $"Common_OnPaintThemeChange() target: {paintController.TargetArea}, theme: {paintController.CurrentTheme.name}"); + + byte target = (byte)paintController.TargetArea; + var theme = PaintThemeLookup.Instance.GetThemeIndex(paintController.CurrentTheme); + + Multiplayer.LogDebug(() => $"Common_OnPaintThemeChange() sending [{CurrentID},{NetId}], target: {paintController.TargetArea}, theme: [{paintController.CurrentTheme.name},{theme}]"); + NetworkLifecycle.Instance?.Client.SendPaintThemeChangePacket(NetId,target,theme); + } + private void Common_OnFuseUpdated(Fuse fuse) { if (UnloadWatcher.isUnloading || NetworkLifecycle.Instance.IsProcessingPacket) @@ -1097,6 +1121,32 @@ private IEnumerator DangleCoupler(Coupler coupler) //Drop the chain coupler.ChainScript.fsm.Fire(ChainCouplerInteraction.Trigger.Dropped_By_Player); } + + public void Common_ReceivePaintThemeUpdate(TrainCarPaint.Target target, PaintTheme paint) + { + TrainCarPaint targetPaint = null; + + if (target == TrainCarPaint.Target.Interior) + { + Multiplayer.LogWarning($"Received Paint Theme update for [{CurrentID}, {NetId}], targeting Interior"); + targetPaint = TrainCar.PaintInterior; + } + else if (target == TrainCarPaint.Target.Exterior) + { + Multiplayer.LogWarning($"Received Paint Theme update for [{CurrentID}, {NetId}], targeting Exterior"); + targetPaint = TrainCar.PaintExterior; + } + + if (targetPaint == null || !targetPaint.IsSupported(paint)) + { + Multiplayer.LogWarning($"Received Paint Theme update for [{CurrentID}, {NetId}], but {paint?.assetName} is not supported"); + return; + } + + targetPaint.currentTheme = paint; + targetPaint.UpdateTheme(); + TrainCar.OnPaintThemeChanged(targetPaint); + } #endregion #region Client diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 4c40ae05..3259702f 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -41,6 +41,7 @@ using LiteNetLib.Utils; using DV.UserManagement; using DV.Common; +using DV.Customization.Paint; namespace Multiplayer.Networking.Managers.Client; @@ -137,6 +138,7 @@ protected override void Subscribe() netPacketProcessor.SubscribeReusable(OnCommonCockFiddlePacket); netPacketProcessor.SubscribeReusable(OnCommonBrakeCylinderReleasePacket); netPacketProcessor.SubscribeReusable(OnCommonHandbrakePositionPacket); + netPacketProcessor.SubscribeReusable(OnCommonPaintThemePacket); netPacketProcessor.SubscribeReusable(OnCommonSimFlowPacket); netPacketProcessor.SubscribeReusable(OnCommonTrainFusesPacket); netPacketProcessor.SubscribeReusable(OnClientboundBrakeStateUpdatePacket); @@ -915,7 +917,7 @@ private void OnClientboundJobsUpdatePacket(ClientboundJobsUpdatePacket packet) private void OnClientboundJobValidateResponsePacket(ClientboundJobValidateResponsePacket packet) { - Log($"OnClientboundJobValidateResponsePacket() JobNetId: {packet.JobNetId}, Status: {packet.Invalid}"); + Log($"Job validation response received JobNetId: {packet.JobNetId}, Status: {packet.Invalid}"); if (!NetworkedJob.Get(packet.JobNetId, out NetworkedJob networkedJob)) return; @@ -960,6 +962,31 @@ private void OnCommonItemChangePacket(CommonItemChangePacket packet) //NetworkedItemManager.Instance.ReceiveSnapshots(packet.Items, null); } + private void OnCommonPaintThemePacket(CommonPaintThemePacket packet) + { + if (!NetworkedTrainCar.Get(packet.NetId, out NetworkedTrainCar netTrainCar)) + return; + + Log($"Received paint theme change for {netTrainCar?.CurrentID}"); + + PaintTheme paint = PaintThemeLookup.Instance.GetPaintTheme(packet.PaintThemeId); + + if (paint == null) + { + LogWarning($"Paint theme index {packet.PaintThemeId} does not exist!"); + return; + } + + if (!Enum.IsDefined(typeof(TrainCarPaint.Target), packet.TargetArea)) + { + LogWarning($"TrainCarPaint Target {packet.TargetArea} is not defined!"); + return; + } + + LogDebug(() => $"OnCommonPaintThemePacket() [{netTrainCar?.CurrentID}, {packet.NetId}], area: {(TrainCarPaint.Target)packet.TargetArea}, paint: [{paint?.assetName}, {packet.PaintThemeId}]"); + netTrainCar?.Common_ReceivePaintThemeUpdate((TrainCarPaint.Target)packet.TargetArea, paint); + } + #endregion #region Senders @@ -1308,6 +1335,11 @@ public void SendItemsChangePacket(List items) DeliveryMethod.ReliableOrdered); } + public void SendPaintThemeChangePacket(ushort netId, byte targetArea, sbyte themeIndex) + { + SendPacketToServer(new CommonPaintThemePacket { NetId = netId, TargetArea = targetArea, PaintThemeId = themeIndex }, DeliveryMethod.ReliableUnordered); + } + #endregion public override void OnNatIntroductionSuccess(IPEndPoint targetEndPoint, NatAddressType type, string token) diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index 483bfedc..23b91ad7 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -112,11 +112,16 @@ public override void Stop() protected override void Subscribe() { + //Client management netPacketProcessor.SubscribeReusable(OnServerboundClientLoginPacket); + + //World sync netPacketProcessor.SubscribeReusable(OnServerboundClientReadyPacket); netPacketProcessor.SubscribeReusable(OnServerboundSaveGameDataRequestPacket); - netPacketProcessor.SubscribeReusable(OnServerboundPlayerPositionPacket); netPacketProcessor.SubscribeReusable(OnServerboundTimeAdvancePacket); + + + netPacketProcessor.SubscribeReusable(OnServerboundPlayerPositionPacket); netPacketProcessor.SubscribeReusable(OnServerboundTrainSyncRequestPacket); netPacketProcessor.SubscribeReusable(OnServerboundTrainDeleteRequestPacket); netPacketProcessor.SubscribeReusable(OnServerboundTrainRerailRequestPacket); @@ -133,6 +138,7 @@ protected override void Subscribe() netPacketProcessor.SubscribeReusable(OnCommonCockFiddlePacket); netPacketProcessor.SubscribeReusable(OnCommonBrakeCylinderReleasePacket); netPacketProcessor.SubscribeReusable(OnCommonHandbrakePositionPacket); + netPacketProcessor.SubscribeReusable(OnCommonPaintThemePacket); netPacketProcessor.SubscribeReusable(OnServerboundAddCoalPacket); netPacketProcessor.SubscribeReusable(OnServerboundFireboxIgnitePacket); netPacketProcessor.SubscribeReusable(OnCommonTrainPortsPacket); @@ -855,6 +861,11 @@ private void OnCommonHandbrakePositionPacket(CommonHandbrakePositionPacket packe SendPacketToAll(packet, DeliveryMethod.ReliableOrdered, peer); } + private void OnCommonPaintThemePacket(CommonPaintThemePacket packet, NetPeer peer) + { + SendPacketToAll(packet, DeliveryMethod.ReliableOrdered, peer); + } + private void OnServerboundAddCoalPacket(ServerboundAddCoalPacket packet, NetPeer peer) { if (!TryGetServerPlayer(peer, out ServerPlayer player)) diff --git a/Multiplayer/Networking/Packets/Common/Train/CommonPaintThemePacket.cs b/Multiplayer/Networking/Packets/Common/Train/CommonPaintThemePacket.cs new file mode 100644 index 00000000..236ef104 --- /dev/null +++ b/Multiplayer/Networking/Packets/Common/Train/CommonPaintThemePacket.cs @@ -0,0 +1,8 @@ +namespace Multiplayer.Networking.Packets.Common.Train; + +public class CommonPaintThemePacket +{ + public ushort NetId { get; set; } + public byte TargetArea { get; set; } + public sbyte PaintThemeId { get; set; } +} From 894a4536aa4e35b36acbc7322440fad2b073c0a3 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 18 Jan 2025 16:39:22 +1000 Subject: [PATCH 193/521] Disable redundant logging --- Multiplayer/Components/Networking/Train/NetworkedRigidbody.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Multiplayer/Components/Networking/Train/NetworkedRigidbody.cs b/Multiplayer/Components/Networking/Train/NetworkedRigidbody.cs index 9ec043d3..bd772c4b 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedRigidbody.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedRigidbody.cs @@ -50,7 +50,7 @@ protected override void Process(RigidbodySnapshot snapshot, uint snapshotTick) try { - 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}"); + //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) From 22613978b9d558a162afdb0989e50a21392b5998 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 18 Jan 2025 16:48:18 +1000 Subject: [PATCH 194/521] CommsRadioSpawner fixes Allow host to spawn and prevent client from spawning --- .../CommsRadio/CommsRadioCarSpawnerPatch.cs | 43 +++++++++++++ .../CommsRadio/CommsRadioCrewVehiclePatch.cs | 62 +++++++++++++++++++ Multiplayer/Patches/Train/CarSpawnerPatch.cs | 39 +++++++++++- 3 files changed, 143 insertions(+), 1 deletion(-) create mode 100644 Multiplayer/Patches/CommsRadio/CommsRadioCarSpawnerPatch.cs create mode 100644 Multiplayer/Patches/CommsRadio/CommsRadioCrewVehiclePatch.cs diff --git a/Multiplayer/Patches/CommsRadio/CommsRadioCarSpawnerPatch.cs b/Multiplayer/Patches/CommsRadio/CommsRadioCarSpawnerPatch.cs new file mode 100644 index 00000000..092e5874 --- /dev/null +++ b/Multiplayer/Patches/CommsRadio/CommsRadioCarSpawnerPatch.cs @@ -0,0 +1,43 @@ +using System.Collections; +using DV; +using DV.InventorySystem; +using HarmonyLib; +using Multiplayer.Components.Networking; +using Multiplayer.Components.Networking.Train; +using Multiplayer.Utils; +using UnityEngine; + +namespace Multiplayer.Patches.CommsRadio; + + +[HarmonyPatch(typeof(CommsRadioCarSpawner))] +public static class CommsRadioCarSpawnerPatch +{ + [HarmonyPrefix] + [HarmonyPatch(nameof(CommsRadioCarSpawner.OnUse))] + private static bool OnUse_Prefix(CommsRadioCarSpawner __instance) + { + if (__instance.state != CommsRadioCarSpawner.State.PickDestination) + return true; + if (NetworkLifecycle.Instance.IsHost()) + return true; + + //temporarily disable client spawning + CommsRadioController.PlayAudioFromRadio(__instance.cancelSound, __instance.transform); + __instance.ClearFlags(); + return false; + + } +} + + //private static IEnumerator PlaySoundsLater(CommsRadioCarDeleter __instance, Vector3 trainPosition, bool playMoneyRemovedSound = true) + //{ + // 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. + // __instance.removeCarSound.Play(trainPosition, minDistance: CommsRadioController.CAR_AUDIO_SOURCE_MIN_DISTANCE, parent: WorldMover.Instance.originShiftParent); + // CommsRadioController.PlayAudioFromRadio(__instance.confirmSound, __instance.transform); + //} + + diff --git a/Multiplayer/Patches/CommsRadio/CommsRadioCrewVehiclePatch.cs b/Multiplayer/Patches/CommsRadio/CommsRadioCrewVehiclePatch.cs new file mode 100644 index 00000000..1d20128f --- /dev/null +++ b/Multiplayer/Patches/CommsRadio/CommsRadioCrewVehiclePatch.cs @@ -0,0 +1,62 @@ +using System.Collections; +using DV; +using DV.InventorySystem; +using HarmonyLib; +using Multiplayer.Components.Networking; +using Multiplayer.Components.Networking.Train; +using Multiplayer.Components.Networking.World; +using Multiplayer.Utils; +using UnityEngine; + +namespace Multiplayer.Patches.CommsRadio; + +[HarmonyPatch(typeof(CommsRadioCrewVehicle))] +public static class CommsRadioCrewVehiclePatch +{ + [HarmonyPrefix] + [HarmonyPatch(nameof(CommsRadioCrewVehicle.OnUse))] + private static bool OnUse_Prefix(CommsRadioCrewVehicle __instance) + { + if (__instance.CurrentState != CommsRadioCrewVehicle.State.ConfirmSummon) + return true; + if (NetworkLifecycle.Instance.IsHost()) + return true; + if (Inventory.Instance.PlayerMoney < __instance.SummonPrice) + return true; + + //temporarily disable client spawning + CommsRadioController.PlayAudioFromRadio(__instance.cancelSound, __instance.transform); + __instance.ClearFlags(); + return false; + + /* + if(!NetworkedRailTrack.TryGetFromRailTrack(__instance.destinationTrack, out NetworkedRailTrack netRailTrack)) + { + Multiplayer.LogError($"CommsRadioCrewVehicle unable to spawn car, NetworkedRailTrack not found for: {__instance.destinationTrack.name}"); + CommsRadioController.PlayAudioFromRadio(__instance.cancelSound, __instance.transform); + __instance.ClearFlags(); + return false; + } + + Vector3 absPos = (Vector3)__instance.closestPointOnDestinationTrack.Value.position; + Vector3 fwd = __instance.closestPointOnDestinationTrack.Value.forward; + + NetworkLifecycle.Instance.Client.SendTrainSpawnRequest(__instance.selectedCar.livery.id, netRailTrack.NetId, absPos, fwd); + + CoroutineManager.Instance.StartCoroutine(PlaySoundsLater(__instance, absPos, __instance.SummonPrice > 0)); + __instance.ClearFlags(); + + */ + return false; + } + + private static IEnumerator PlaySoundsLater(CommsRadioCrewVehicle __instance, Vector3 trainPosition, bool playMoneyRemovedSound = true) + { + 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. + //__instance.removeCarSound.Play(trainPosition, minDistance: CommsRadioController.CAR_AUDIO_SOURCE_MIN_DISTANCE, parent: WorldMover.Instance.originShiftParent); + CommsRadioController.PlayAudioFromRadio(__instance.confirmSound, __instance.transform); + } +} diff --git a/Multiplayer/Patches/Train/CarSpawnerPatch.cs b/Multiplayer/Patches/Train/CarSpawnerPatch.cs index 3b001851..c33227a9 100644 --- a/Multiplayer/Patches/Train/CarSpawnerPatch.cs +++ b/Multiplayer/Patches/Train/CarSpawnerPatch.cs @@ -24,6 +24,7 @@ private static void PrepareTrainCarForDeleting(TrainCar trainCar) NetworkLifecycle.Instance.Server?.SendDestroyTrainCar(networkedTrainCar.NetId); } + //Called from [HarmonyPatch(nameof(CarSpawner.SpawnCars))] [HarmonyPostfix] private static void SpawnCars(List __result) @@ -38,8 +39,44 @@ private static void SpawnCars(List __result) 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"); + Multiplayer.LogDebug(() => $"SpawnCars() {__result?.Count} cars spawned, sending to players"); NetworkLifecycle.Instance.Server.SendSpawnTrainset(__result, true, true); } + + [HarmonyPatch(nameof(CarSpawner.SpawnCarFromRemote))] + [HarmonyPostfix] + private static void SpawnCarFromRemote(TrainCar __result) + { + if (UnloadWatcher.isUnloading) + return; + + if (!NetworkLifecycle.Instance.IsHost()) + return; + + if (__result == null) + return; + + Multiplayer.LogDebug(() => $"SpawnCarFromRemote() {__result?.carLivery?.name} spawned, sending to players"); + NetworkLifecycle.Instance.Server.SendSpawnTrainset([__result], true, true); + + } + + [HarmonyPatch(nameof(CarSpawner.SpawnCarOnClosestTrack))] + [HarmonyPostfix] + private static void SpawnCarOnClosestTrack(TrainCar __result) + { + if (UnloadWatcher.isUnloading) + return; + + if (!NetworkLifecycle.Instance.IsHost()) + return; + + if (__result == null) + return; + + Multiplayer.LogDebug(() => $"SpawnCarOnClosestTrack() {__result?.carLivery?.name} spawned, sending to players"); + NetworkLifecycle.Instance.Server.SendSpawnTrainset([__result], true, true); + + } } From ff3883c5d73691ec536e0f29c26bb6b9e79132fe Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 18 Jan 2025 17:31:33 +1000 Subject: [PATCH 195/521] Fix sync of ports for 0 state Original code only sync'd if the delta between last sent and current value was more than 0.001. If a port was zeroed e.g. water draining from a cylinder, but the change was less than 0.001, then the client would not receive an update. New method uses the same delta parameters but also checks if the new state is zero and the last sent state was non-zero. --- .../Components/Networking/Train/NetworkedTrainCar.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs index 1230168c..58326561 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs @@ -67,6 +67,8 @@ public static bool TryGetFromTrainCar(TrainCar trainCar, out NetworkedTrainCar n private const int MAX_COUPLER_ITERATIONS = 10; private const float MAX_FIREBOX_DELTA = 0.1f; + private const float MAX_PORT_DELTA = 0.001f; + public string CurrentID { get; private set; } public TrainCar TrainCar; @@ -708,9 +710,10 @@ private void Common_OnPortUpdated(Port port) return; if (float.IsNaN(port.prevValue) && float.IsNaN(port.Value)) return; - if (lastSentPortValues.TryGetValue(port.id, out float value) && Mathf.Abs(value - port.Value) < 0.001f) - return; - dirtyPorts.Add(port.id); + if (!lastSentPortValues.TryGetValue(port.id, out float lastSentvalue) || + Mathf.Abs(lastSentvalue - port.Value) > MAX_PORT_DELTA || + (port.Value == 0 && lastSentvalue != 0)) + dirtyPorts.Add(port.id); } private void Common_OnPaintThemeChange(TrainCarPaint paintController) From bd781e0f211e64c26ad76f94b8ca3f8cbee52c82 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 18 Jan 2025 19:21:29 +1000 Subject: [PATCH 196/521] Ready for 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 f78b0810..03d4922e 100644 --- a/Multiplayer/Multiplayer.csproj +++ b/Multiplayer/Multiplayer.csproj @@ -3,7 +3,7 @@ net48 latest Multiplayer - 0.1.9.8 + 0.1.9.9 diff --git a/info.json b/info.json index 2b46338d..e9b1b923 100644 --- a/info.json +++ b/info.json @@ -1,6 +1,6 @@ { "Id": "Multiplayer", - "Version": "0.1.9.8", + "Version": "0.1.9.9", "DisplayName": "Multiplayer", "Author": "Insprill, Macka, Morm", "EntryMethod": "Multiplayer.Multiplayer.Load", From 9d3ae395de70828eac798197f3b8c094570dfade Mon Sep 17 00:00:00 2001 From: Macka Date: Sat, 18 Jan 2025 19:22:54 +1000 Subject: [PATCH 197/521] Added credit for french translations --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index c0bc694e..efb6ac76 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,7 @@ If you'd like to help with translations, please create a pull request or send a | :------------ | :------------ | Ádi | Hungarian | | My Name Is BorING | Chinese (Simplified) | +| Harfeur | French | From 4a28222a0bce1514897d0e8c12450e4d69697c19 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 19 Jan 2025 10:35:35 +1000 Subject: [PATCH 198/521] Upgrade to latest LiteNetLib Using NuGet packages from now on, rather than custom implementation --- Multiplayer/Components/IdMonoBehaviour.cs | 2 +- .../Components/Networking/NetworkLifecycle.cs | 47 +- .../Networking/UI/NetworkStatsGui.cs | 32 +- Multiplayer/Multiplayer.csproj | 10 +- .../Networking/Data/LobbyServerData.cs | 3 - .../Managers/Client/NetworkClient.cs | 12 - .../Networking/Managers/NetworkManager.cs | 2 +- .../Managers/Server/NetworkServer.cs | 1 + .../Assets/Scripts/LiteNetLib.meta | 8 - .../Assets/Scripts/LiteNetLib/LICENSE.txt | 21 - .../Scripts/LiteNetLib/LICENSE.txt.meta | 7 - .../Assets/Scripts/LiteNetLib/LiteNetLib.meta | 8 - .../LiteNetLib/LiteNetLib/BaseChannel.cs | 48 - .../LiteNetLib/LiteNetLib/BaseChannel.cs.meta | 11 - .../LiteNetLib/ConnectionRequest.cs | 134 -- .../LiteNetLib/ConnectionRequest.cs.meta | 11 - .../LiteNetLib/INetEventListener.cs | 272 --- .../LiteNetLib/INetEventListener.cs.meta | 11 - .../LiteNetLib/LiteNetLib/InternalPackets.cs | 133 -- .../LiteNetLib/InternalPackets.cs.meta | 11 - .../Scripts/LiteNetLib/LiteNetLib/Layers.meta | 8 - .../LiteNetLib/Layers/Crc32cLayer.cs | 41 - .../LiteNetLib/Layers/Crc32cLayer.cs.meta | 11 - .../LiteNetLib/Layers/PacketLayerBase.cs | 17 - .../LiteNetLib/Layers/PacketLayerBase.cs.meta | 11 - .../LiteNetLib/Layers/XorEncryptLayer.cs | 60 - .../LiteNetLib/Layers/XorEncryptLayer.cs.meta | 11 - .../LiteNetLib/LiteNetLib/LiteNetLib.asmdef | 13 - .../LiteNetLib/LiteNetLib.asmdef.meta | 7 - .../LiteNetLib/LiteNetLib/LiteNetLib.csproj | 62 - .../LiteNetLib/LiteNetLib.csproj.meta | 7 - .../LiteNetLib/LiteNetLib/NatPunchModule.cs | 259 --- .../LiteNetLib/NatPunchModule.cs.meta | 11 - .../LiteNetLib/LiteNetLib/NativeSocket.cs | 301 --- .../LiteNetLib/NativeSocket.cs.meta | 11 - .../LiteNetLib/LiteNetLib/NetConstants.cs | 75 - .../LiteNetLib/NetConstants.cs.meta | 11 - .../Scripts/LiteNetLib/LiteNetLib/NetDebug.cs | 92 - .../LiteNetLib/LiteNetLib/NetDebug.cs.meta | 11 - .../LiteNetLib/NetManager.PacketPool.cs | 82 - .../LiteNetLib/NetManager.PacketPool.cs.meta | 11 - .../LiteNetLib/NetManager.Socket.cs | 732 ------- .../LiteNetLib/NetManager.Socket.cs.meta | 11 - .../LiteNetLib/LiteNetLib/NetManager.cs | 1818 ----------------- .../LiteNetLib/LiteNetLib/NetManager.cs.meta | 11 - .../LiteNetLib/LiteNetLib/NetPacket.cs | 160 -- .../LiteNetLib/LiteNetLib/NetPacket.cs.meta | 11 - .../Scripts/LiteNetLib/LiteNetLib/NetPeer.cs | 1395 ------------- .../LiteNetLib/LiteNetLib/NetPeer.cs.meta | 11 - .../LiteNetLib/LiteNetLib/NetStatistics.cs | 103 - .../LiteNetLib/NetStatistics.cs.meta | 11 - .../Scripts/LiteNetLib/LiteNetLib/NetUtils.cs | 238 --- .../LiteNetLib/LiteNetLib/NetUtils.cs.meta | 11 - .../LiteNetLib/LiteNetLib/PausedSocketFix.cs | 57 - .../LiteNetLib/PausedSocketFix.cs.meta | 11 - .../LiteNetLib/LiteNetLib/PooledPacket.cs | 32 - .../LiteNetLib/PooledPacket.cs.meta | 11 - .../LiteNetLib/LiteNetLib/ReliableChannel.cs | 337 --- .../LiteNetLib/ReliableChannel.cs.meta | 11 - .../LiteNetLib/LiteNetLib/SequencedChannel.cs | 114 -- .../LiteNetLib/SequencedChannel.cs.meta | 11 - .../Scripts/LiteNetLib/LiteNetLib/Utils.meta | 8 - .../LiteNetLib/LiteNetLib/Utils/CRC32C.cs | 150 -- .../LiteNetLib/Utils/CRC32C.cs.meta | 11 - .../LiteNetLib/Utils/FastBitConverter.cs | 175 -- .../LiteNetLib/Utils/FastBitConverter.cs.meta | 11 - .../LiteNetLib/Utils/INetSerializable.cs | 8 - .../LiteNetLib/Utils/INetSerializable.cs.meta | 11 - .../LiteNetLib/Utils/NetDataReader.cs | 673 ------ .../LiteNetLib/Utils/NetDataReader.cs.meta | 11 - .../LiteNetLib/Utils/NetDataWriter.cs | 381 ---- .../LiteNetLib/Utils/NetDataWriter.cs.meta | 11 - .../LiteNetLib/Utils/NetPacketProcessor.cs | 289 --- .../Utils/NetPacketProcessor.cs.meta | 11 - .../LiteNetLib/Utils/NetSerializer.cs | 738 ------- .../LiteNetLib/Utils/NetSerializer.cs.meta | 11 - .../LiteNetLib/LiteNetLib/Utils/NtpPacket.cs | 423 ---- .../LiteNetLib/Utils/NtpPacket.cs.meta | 11 - .../LiteNetLib/LiteNetLib/Utils/NtpRequest.cs | 42 - .../LiteNetLib/Utils/NtpRequest.cs.meta | 11 - .../LiteNetLib/LiteNetLib/Utils/Preserve.cs | 12 - .../LiteNetLib/Utils/Preserve.cs.meta | 11 - .../LiteNetLib/LiteNetLib/package.json | 11 - .../LiteNetLib/LiteNetLib/package.json.meta | 7 - .../Assets/Scripts/LiteNetLib/README.md | 117 -- .../Assets/Scripts/LiteNetLib/README.md.meta | 7 - 86 files changed, 35 insertions(+), 10108 deletions(-) delete mode 100644 MultiplayerAssets/Assets/Scripts/LiteNetLib.meta delete mode 100644 MultiplayerAssets/Assets/Scripts/LiteNetLib/LICENSE.txt delete mode 100644 MultiplayerAssets/Assets/Scripts/LiteNetLib/LICENSE.txt.meta delete mode 100644 MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib.meta delete mode 100644 MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/BaseChannel.cs delete mode 100644 MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/BaseChannel.cs.meta delete mode 100644 MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/ConnectionRequest.cs delete mode 100644 MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/ConnectionRequest.cs.meta delete mode 100644 MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/INetEventListener.cs delete mode 100644 MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/INetEventListener.cs.meta delete mode 100644 MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/InternalPackets.cs delete mode 100644 MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/InternalPackets.cs.meta delete mode 100644 MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Layers.meta delete mode 100644 MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Layers/Crc32cLayer.cs delete mode 100644 MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Layers/Crc32cLayer.cs.meta delete mode 100644 MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Layers/PacketLayerBase.cs delete mode 100644 MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Layers/PacketLayerBase.cs.meta delete mode 100644 MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Layers/XorEncryptLayer.cs delete mode 100644 MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Layers/XorEncryptLayer.cs.meta delete mode 100644 MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/LiteNetLib.asmdef delete mode 100644 MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/LiteNetLib.asmdef.meta delete mode 100644 MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/LiteNetLib.csproj delete mode 100644 MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/LiteNetLib.csproj.meta delete mode 100644 MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NatPunchModule.cs delete mode 100644 MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NatPunchModule.cs.meta delete mode 100644 MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NativeSocket.cs delete mode 100644 MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NativeSocket.cs.meta delete mode 100644 MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetConstants.cs delete mode 100644 MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetConstants.cs.meta delete mode 100644 MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetDebug.cs delete mode 100644 MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetDebug.cs.meta delete mode 100644 MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetManager.PacketPool.cs delete mode 100644 MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetManager.PacketPool.cs.meta delete mode 100644 MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetManager.Socket.cs delete mode 100644 MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetManager.Socket.cs.meta delete mode 100644 MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetManager.cs delete mode 100644 MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetManager.cs.meta delete mode 100644 MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetPacket.cs delete mode 100644 MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetPacket.cs.meta delete mode 100644 MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetPeer.cs delete mode 100644 MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetPeer.cs.meta delete mode 100644 MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetStatistics.cs delete mode 100644 MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetStatistics.cs.meta delete mode 100644 MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetUtils.cs delete mode 100644 MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetUtils.cs.meta delete mode 100644 MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/PausedSocketFix.cs delete mode 100644 MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/PausedSocketFix.cs.meta delete mode 100644 MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/PooledPacket.cs delete mode 100644 MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/PooledPacket.cs.meta delete mode 100644 MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/ReliableChannel.cs delete mode 100644 MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/ReliableChannel.cs.meta delete mode 100644 MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/SequencedChannel.cs delete mode 100644 MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/SequencedChannel.cs.meta delete mode 100644 MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils.meta delete mode 100644 MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/CRC32C.cs delete mode 100644 MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/CRC32C.cs.meta delete mode 100644 MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/FastBitConverter.cs delete mode 100644 MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/FastBitConverter.cs.meta delete mode 100644 MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/INetSerializable.cs delete mode 100644 MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/INetSerializable.cs.meta delete mode 100644 MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NetDataReader.cs delete mode 100644 MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NetDataReader.cs.meta delete mode 100644 MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NetDataWriter.cs delete mode 100644 MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NetDataWriter.cs.meta delete mode 100644 MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NetPacketProcessor.cs delete mode 100644 MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NetPacketProcessor.cs.meta delete mode 100644 MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NetSerializer.cs delete mode 100644 MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NetSerializer.cs.meta delete mode 100644 MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NtpPacket.cs delete mode 100644 MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NtpPacket.cs.meta delete mode 100644 MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NtpRequest.cs delete mode 100644 MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NtpRequest.cs.meta delete mode 100644 MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/Preserve.cs delete mode 100644 MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/Preserve.cs.meta delete mode 100644 MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/package.json delete mode 100644 MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/package.json.meta delete mode 100644 MultiplayerAssets/Assets/Scripts/LiteNetLib/README.md delete mode 100644 MultiplayerAssets/Assets/Scripts/LiteNetLib/README.md.meta diff --git a/Multiplayer/Components/IdMonoBehaviour.cs b/Multiplayer/Components/IdMonoBehaviour.cs index 9c71626e..44e05f8f 100644 --- a/Multiplayer/Components/IdMonoBehaviour.cs +++ b/Multiplayer/Components/IdMonoBehaviour.cs @@ -32,7 +32,7 @@ protected static bool Get(T netId, out IdMonoBehaviour obj) return true; obj = null; if ((netId as dynamic).CompareTo(default(T)) != 0) - Multiplayer.LogDebug(() => $"Got invalid NetId {netId} while processing packet {NetPacketProcessor.CurrentlyProcessingPacket}"); + Multiplayer.LogDebug(() => $"Got invalid NetId {netId} while processing packet {NetworkLifecycle.Instance.IsProcessingPacket}"); return false; } diff --git a/Multiplayer/Components/Networking/NetworkLifecycle.cs b/Multiplayer/Components/Networking/NetworkLifecycle.cs index 9372438c..605f5306 100644 --- a/Multiplayer/Components/Networking/NetworkLifecycle.cs +++ b/Multiplayer/Components/Networking/NetworkLifecycle.cs @@ -71,7 +71,7 @@ protected override void Awake() base.Awake(); playerList = gameObject.AddComponent(); Stats = gameObject.AddComponent(); - RegisterPackets(); + //RegisterPackets(); WorldStreamingInit.LoadingFinished += () => { playerList.RegisterListeners(); }; Settings.OnSettingsUpdated += OnSettingsUpdated; SceneManager.sceneLoaded += (scene, _) => @@ -85,17 +85,17 @@ protected override void Awake() StartCoroutine(PollEvents()); } - private static void RegisterPackets() - { - IReadOnlyDictionary packetMappings = NetPacketProcessor.RegisterPacketTypes(); - Multiplayer.LogDebug(() => - { - StringBuilder stringBuilder = new($"Registered {packetMappings.Count} packets. Mappings:\n"); - foreach (KeyValuePair kvp in packetMappings) - stringBuilder.AppendLine($"{kvp.Value}: {kvp.Key}"); - return stringBuilder; - }); - } + //private static void RegisterPackets() + //{ + // IReadOnlyDictionary packetMappings = NetPacketProcessor.RegisterPacketTypes(); + // Multiplayer.LogDebug(() => + // { + // StringBuilder stringBuilder = new($"Registered {packetMappings.Count} packets. Mappings:\n"); + // foreach (KeyValuePair kvp in packetMappings) + // stringBuilder.AppendLine($"{kvp.Value}: {kvp.Key}"); + // return stringBuilder; + // }); + //} private void OnSettingsUpdated(Settings settings) { @@ -126,13 +126,6 @@ public void QueueMainMenuEvent(Action action) public bool StartServer(IDifficulty difficulty) { - // Check if the user is a legitimate Steam user before proceeding - if (!IsLegitimateSteamUser()) - { - Multiplayer.Log("User is not a legitimate Steam user. Server start aborted."); - return false; - } - int port = Multiplayer.Settings.Port; if (Server != null) @@ -161,22 +154,6 @@ public bool StartServer(IDifficulty difficulty) return true; } - private bool IsLegitimateSteamUser() - { - if (SteamClient.IsValid) - { - // Verify the Steam ID is valid and owned - if (SteamClient.SteamId.IsValid) - { - System.Console.WriteLine($"Steam ID {SteamClient.SteamId} is valid."); - return true; - } - } - - System.Console.WriteLine("Invalid or pirated Steam account detected."); - return false; - } - public void StartClient(string address, int port, string password, bool isSinglePlayer, Action onDisconnect ) { if (Client != null) diff --git a/Multiplayer/Components/Networking/UI/NetworkStatsGui.cs b/Multiplayer/Components/Networking/UI/NetworkStatsGui.cs index e80cf801..720b5f5c 100644 --- a/Multiplayer/Components/Networking/UI/NetworkStatsGui.cs +++ b/Multiplayer/Components/Networking/UI/NetworkStatsGui.cs @@ -17,8 +17,8 @@ public class NetworkStatsGui : MonoBehaviour private long bytesSentPerSecond; private long packetsReceivedPerSecond; private long packetsSentPerSecond; - private Dictionary packetsWrittenByType; - private Dictionary bytesWrittenByType; + //private Dictionary packetsWrittenByType; + //private Dictionary bytesWrittenByType; private Coroutine updateCoro; @@ -47,8 +47,8 @@ private IEnumerator UpdateStats() bytesSentPerSecond = serverStats != null ? serverStats.BytesSent - clientStats.BytesReceived : clientStats.BytesReceived; packetsReceivedPerSecond = serverStats != null ? serverStats.PacketsReceived - clientStats.PacketsSent : clientStats.PacketsReceived; packetsSentPerSecond = serverStats != null ? serverStats.PacketsSent - clientStats.PacketsReceived : clientStats.PacketsReceived; - packetsWrittenByType = serverStats?.PacketsWrittenByType; - bytesWrittenByType = serverStats?.BytesWrittenByType; + //packetsWrittenByType = serverStats?.PacketsWrittenByType; //disabled for steamnetworking + //bytesWrittenByType = serverStats?.BytesWrittenByType; //disabled for steamnetworking serverStats?.Reset(); clientStats?.Reset(); yield return new WaitForSecondsRealtime(1); @@ -66,24 +66,24 @@ private void OnGUI() // Write clean IMGUI code challenge (impossible) private void DrawStats(int windowId) { - int statsListSize = Multiplayer.Settings.StatsListSize; + //int statsListSize = Multiplayer.Settings.StatsListSize; GUILayout.Label($"Send: {bytesSentPerSecond.Bytes().ToFullWords()}/s ({packetsSentPerSecond:N0} packets/s)"); GUILayout.Label($"Receive: {bytesReceivedPerSecond.Bytes().ToFullWords()}/s ({packetsReceivedPerSecond:N0} packets/s)"); - if (serverStats == null) + if (serverStats == null) return; - GUILayout.Space(5); - GUILayout.Label($"Top {statsListSize} sent packets"); - foreach (KeyValuePair kvp in packetsWrittenByType.OrderByDescending(k => k.Value).Take(statsListSize)) - GUILayout.Label($" • {kvp.Key}: {kvp.Value}/s"); - if (packetsWrittenByType.Count < statsListSize) - for (int i = 0; i < statsListSize - packetsWrittenByType.Count; i++) - GUILayout.Label(string.Empty); + //GUILayout.Space(5); + //GUILayout.Label($"Top {statsListSize} sent packets"); + //foreach (KeyValuePair kvp in packetsWrittenByType.OrderByDescending(k => k.Value).Take(statsListSize)) + // GUILayout.Label($" • {kvp.Key}: {kvp.Value}/s"); + //if (packetsWrittenByType.Count < statsListSize) + // for (int i = 0; i < statsListSize - packetsWrittenByType.Count; i++) + // GUILayout.Label(string.Empty); - GUILayout.Label($"Top {statsListSize} sent packets by size"); - foreach (KeyValuePair kvp in bytesWrittenByType.OrderByDescending(k => k.Value).Take(statsListSize)) - GUILayout.Label($" • {kvp.Key}: {kvp.Value.Bytes().ToFullWords()}/s"); + //GUILayout.Label($"Top {statsListSize} sent packets by size"); + //foreach (KeyValuePair kvp in bytesWrittenByType.OrderByDescending(k => k.Value).Take(statsListSize)) + // GUILayout.Label($" • {kvp.Key}: {kvp.Value.Bytes().ToFullWords()}/s"); } } diff --git a/Multiplayer/Multiplayer.csproj b/Multiplayer/Multiplayer.csproj index 810de453..3eb80fdb 100644 --- a/Multiplayer/Multiplayer.csproj +++ b/Multiplayer/Multiplayer.csproj @@ -3,13 +3,14 @@ net48 latest Multiplayer - 0.1.9.9 + 0.1.10.0 all runtime; build; native; contentfiles; analyzers; buildtransitive + @@ -54,9 +55,6 @@ - - ../build/LiteNetLib.dll - ../build/MultiplayerEditor.dll @@ -96,9 +94,9 @@ - + - + diff --git a/Multiplayer/Networking/Data/LobbyServerData.cs b/Multiplayer/Networking/Data/LobbyServerData.cs index 91484b4d..3c8317fa 100644 --- a/Multiplayer/Networking/Data/LobbyServerData.cs +++ b/Multiplayer/Networking/Data/LobbyServerData.cs @@ -1,9 +1,6 @@ using LiteNetLib.Utils; using Multiplayer.Components.MainMenu; using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using System.Reflection; -using UnityEngine.Profiling; namespace Multiplayer.Networking.Data { diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 3259702f..3663a363 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -1341,16 +1341,4 @@ public void SendPaintThemeChangePacket(ushort netId, byte targetArea, sbyte them } #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}"); - } } diff --git a/Multiplayer/Networking/Managers/NetworkManager.cs b/Multiplayer/Networking/Managers/NetworkManager.cs index e5d34f8a..d633778a 100644 --- a/Multiplayer/Networking/Managers/NetworkManager.cs +++ b/Multiplayer/Networking/Managers/NetworkManager.cs @@ -31,7 +31,7 @@ protected NetworkManager(Settings settings) BroadcastReceiveEnabled = true, }; - netPacketProcessor = new NetPacketProcessor(netManager); + netPacketProcessor = new NetPacketProcessor(); RegisterNestedTypes(); OnSettingsUpdated(settings); Settings.OnSettingsUpdated += OnSettingsUpdated; diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index cd7c188b..3fb2f0f0 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -32,6 +32,7 @@ using Multiplayer.Networking.Packets.Unconnected; using System.Text; using Steamworks; +using Multiplayer.Networking.Data.Train; namespace Multiplayer.Networking.Managers.Server; diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib.meta deleted file mode 100644 index 5a667468..00000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: cab0035d8a4c26975a2ef22c8eabcc3d -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LICENSE.txt b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LICENSE.txt deleted file mode 100644 index 6e1e67be..00000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LICENSE.txt +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2020 Ruslan Pyrch - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LICENSE.txt.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LICENSE.txt.meta deleted file mode 100644 index 7af13029..00000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LICENSE.txt.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: 656a4f684a57cd9e0be9d5459a570f2e -TextScriptImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib.meta deleted file mode 100644 index eee7a723..00000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: 7b9b511c4658e6bc7ae2a737cd046421 -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/BaseChannel.cs b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/BaseChannel.cs deleted file mode 100644 index b70c4360..00000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/BaseChannel.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System.Collections.Generic; -using System.Threading; - -namespace LiteNetLib -{ - internal abstract class BaseChannel - { - protected readonly NetPeer Peer; - protected readonly Queue OutgoingQueue = new Queue(NetConstants.DefaultWindowSize); - private int _isAddedToPeerChannelSendQueue; - - public int PacketsInQueue => OutgoingQueue.Count; - - protected BaseChannel(NetPeer peer) - { - Peer = peer; - } - - public void AddToQueue(NetPacket packet) - { - lock (OutgoingQueue) - { - OutgoingQueue.Enqueue(packet); - } - AddToPeerChannelSendQueue(); - } - - protected void AddToPeerChannelSendQueue() - { - if (Interlocked.CompareExchange(ref _isAddedToPeerChannelSendQueue, 1, 0) == 0) - { - Peer.AddToReliableChannelSendQueue(this); - } - } - - public bool SendAndCheckQueue() - { - bool hasPacketsToSend = SendNextPackets(); - if (!hasPacketsToSend) - Interlocked.Exchange(ref _isAddedToPeerChannelSendQueue, 0); - - return hasPacketsToSend; - } - - protected abstract bool SendNextPackets(); - public abstract bool ProcessPacket(NetPacket packet); - } -} diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/BaseChannel.cs.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/BaseChannel.cs.meta deleted file mode 100644 index 2f5c4fac..00000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/BaseChannel.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 22b92fe7d347801cda171a3652d91c34 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/ConnectionRequest.cs b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/ConnectionRequest.cs deleted file mode 100644 index 4a2cdd93..00000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/ConnectionRequest.cs +++ /dev/null @@ -1,134 +0,0 @@ -using System.Net; -using System.Threading; -using LiteNetLib.Utils; - -namespace LiteNetLib -{ - internal enum ConnectionRequestResult - { - None, - Accept, - Reject, - RejectForce - } - - public class ConnectionRequest - { - private readonly NetManager _listener; - private int _used; - - public NetDataReader Data => InternalPacket.Data; - - internal ConnectionRequestResult Result { get; private set; } - internal NetConnectRequestPacket InternalPacket; - - public readonly IPEndPoint RemoteEndPoint; - - internal void UpdateRequest(NetConnectRequestPacket connectRequest) - { - //old request - if (connectRequest.ConnectionTime < InternalPacket.ConnectionTime) - return; - - if (connectRequest.ConnectionTime == InternalPacket.ConnectionTime && - connectRequest.ConnectionNumber == InternalPacket.ConnectionNumber) - return; - - InternalPacket = connectRequest; - } - - private bool TryActivate() - { - return Interlocked.CompareExchange(ref _used, 1, 0) == 0; - } - - internal ConnectionRequest(IPEndPoint remoteEndPoint, NetConnectRequestPacket requestPacket, NetManager listener) - { - InternalPacket = requestPacket; - RemoteEndPoint = remoteEndPoint; - _listener = listener; - } - - public NetPeer AcceptIfKey(string key) - { - if (!TryActivate()) - return null; - try - { - if (Data.GetString() == key) - Result = ConnectionRequestResult.Accept; - } - catch - { - NetDebug.WriteError("[AC] Invalid incoming data"); - } - if (Result == ConnectionRequestResult.Accept) - return _listener.OnConnectionSolved(this, null, 0, 0); - - Result = ConnectionRequestResult.Reject; - _listener.OnConnectionSolved(this, null, 0, 0); - return null; - } - - /// - /// Accept connection and get new NetPeer as result - /// - /// Connected NetPeer - public NetPeer Accept() - { - if (!TryActivate()) - return null; - Result = ConnectionRequestResult.Accept; - return _listener.OnConnectionSolved(this, null, 0, 0); - } - - public void Reject(byte[] rejectData, int start, int length, bool force) - { - if (!TryActivate()) - return; - Result = force ? ConnectionRequestResult.RejectForce : ConnectionRequestResult.Reject; - _listener.OnConnectionSolved(this, rejectData, start, length); - } - - public void Reject(byte[] rejectData, int start, int length) - { - Reject(rejectData, start, length, false); - } - - - public void RejectForce(byte[] rejectData, int start, int length) - { - Reject(rejectData, start, length, true); - } - - public void RejectForce() - { - Reject(null, 0, 0, true); - } - - public void RejectForce(byte[] rejectData) - { - Reject(rejectData, 0, rejectData.Length, true); - } - - public void RejectForce(NetDataWriter rejectData) - { - Reject(rejectData.Data, 0, rejectData.Length, true); - } - - public void Reject() - { - Reject(null, 0, 0, false); - } - - public void Reject(byte[] rejectData) - { - Reject(rejectData, 0, rejectData.Length, false); - } - - public void Reject(NetDataWriter rejectData) - { - Reject(rejectData.Data, 0, rejectData.Length, false); - } - } -} diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/ConnectionRequest.cs.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/ConnectionRequest.cs.meta deleted file mode 100644 index 811d8303..00000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/ConnectionRequest.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 4f53009a5751b2b24a12b6349d4bc0c0 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/INetEventListener.cs b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/INetEventListener.cs deleted file mode 100644 index 13d8852e..00000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/INetEventListener.cs +++ /dev/null @@ -1,272 +0,0 @@ -using System.Net; -using System.Net.Sockets; -using LiteNetLib.Utils; - -namespace LiteNetLib -{ - /// - /// Type of message that you receive in OnNetworkReceiveUnconnected event - /// - public enum UnconnectedMessageType - { - BasicMessage, - Broadcast - } - - /// - /// Disconnect reason that you receive in OnPeerDisconnected event - /// - public enum DisconnectReason - { - ConnectionFailed, - Timeout, - HostUnreachable, - NetworkUnreachable, - RemoteConnectionClose, - DisconnectPeerCalled, - ConnectionRejected, - InvalidProtocol, - UnknownHost, - Reconnect, - PeerToPeerConnection, - PeerNotFound - } - - /// - /// Additional information about disconnection - /// - public struct DisconnectInfo - { - /// - /// Additional info why peer disconnected - /// - public DisconnectReason Reason; - - /// - /// Error code (if reason is SocketSendError or SocketReceiveError) - /// - public SocketError SocketErrorCode; - - /// - /// Additional data that can be accessed (only if reason is RemoteConnectionClose) - /// - public NetPacketReader AdditionalData; - } - - public interface INetEventListener - { - /// - /// New remote peer connected to host, or client connected to remote host - /// - /// Connected peer object - void OnPeerConnected(NetPeer peer); - - /// - /// Peer disconnected - /// - /// disconnected peer - /// additional info about reason, errorCode or data received with disconnect message - void OnPeerDisconnected(NetPeer peer, DisconnectInfo disconnectInfo); - - /// - /// Network error (on send or receive) - /// - /// From endPoint (can be null) - /// Socket error - void OnNetworkError(IPEndPoint endPoint, SocketError socketError); - - /// - /// Received some data - /// - /// From peer - /// DataReader containing all received data - /// Number of channel at which packet arrived - /// Type of received packet - void OnNetworkReceive(NetPeer peer, NetPacketReader reader, byte channelNumber, DeliveryMethod deliveryMethod); - - /// - /// Received unconnected message - /// - /// From address (IP and Port) - /// Message data - /// Message type (simple, discovery request or response) - void OnNetworkReceiveUnconnected(IPEndPoint remoteEndPoint, NetPacketReader reader, UnconnectedMessageType messageType); - - /// - /// Latency information updated - /// - /// Peer with updated latency - /// latency value in milliseconds - void OnNetworkLatencyUpdate(NetPeer peer, int latency); - - /// - /// On peer connection requested - /// - /// Request information (EndPoint, internal id, additional data) - void OnConnectionRequest(ConnectionRequest request); - } - - public interface IDeliveryEventListener - { - /// - /// On reliable message delivered - /// - /// - /// - void OnMessageDelivered(NetPeer peer, object userData); - } - - public interface INtpEventListener - { - /// - /// Ntp response - /// - /// - void OnNtpResponse(NtpPacket packet); - } - - public interface IPeerAddressChangedListener - { - /// - /// Called when peer address changed (when AllowPeerAddressChange is enabled) - /// - /// Peer that changed address (with new address) - /// previous IP - void OnPeerAddressChanged(NetPeer peer, IPEndPoint previousAddress); - } - - public class EventBasedNetListener : INetEventListener, IDeliveryEventListener, INtpEventListener, IPeerAddressChangedListener - { - public delegate void OnPeerConnected(NetPeer peer); - public delegate void OnPeerDisconnected(NetPeer peer, DisconnectInfo disconnectInfo); - public delegate void OnNetworkError(IPEndPoint endPoint, SocketError socketError); - public delegate void OnNetworkReceive(NetPeer peer, NetPacketReader reader, byte channel, DeliveryMethod deliveryMethod); - public delegate void OnNetworkReceiveUnconnected(IPEndPoint remoteEndPoint, NetPacketReader reader, UnconnectedMessageType messageType); - public delegate void OnNetworkLatencyUpdate(NetPeer peer, int latency); - public delegate void OnConnectionRequest(ConnectionRequest request); - public delegate void OnDeliveryEvent(NetPeer peer, object userData); - public delegate void OnNtpResponseEvent(NtpPacket packet); - public delegate void OnPeerAddressChangedEvent(NetPeer peer, IPEndPoint previousAddress); - - public event OnPeerConnected PeerConnectedEvent; - public event OnPeerDisconnected PeerDisconnectedEvent; - public event OnNetworkError NetworkErrorEvent; - public event OnNetworkReceive NetworkReceiveEvent; - public event OnNetworkReceiveUnconnected NetworkReceiveUnconnectedEvent; - public event OnNetworkLatencyUpdate NetworkLatencyUpdateEvent; - public event OnConnectionRequest ConnectionRequestEvent; - public event OnDeliveryEvent DeliveryEvent; - public event OnNtpResponseEvent NtpResponseEvent; - public event OnPeerAddressChangedEvent PeerAddressChangedEvent; - - public void ClearPeerConnectedEvent() - { - PeerConnectedEvent = null; - } - - public void ClearPeerDisconnectedEvent() - { - PeerDisconnectedEvent = null; - } - - public void ClearNetworkErrorEvent() - { - NetworkErrorEvent = null; - } - - public void ClearNetworkReceiveEvent() - { - NetworkReceiveEvent = null; - } - - public void ClearNetworkReceiveUnconnectedEvent() - { - NetworkReceiveUnconnectedEvent = null; - } - - public void ClearNetworkLatencyUpdateEvent() - { - NetworkLatencyUpdateEvent = null; - } - - public void ClearConnectionRequestEvent() - { - ConnectionRequestEvent = null; - } - - public void ClearDeliveryEvent() - { - DeliveryEvent = null; - } - - public void ClearNtpResponseEvent() - { - NtpResponseEvent = null; - } - - public void ClearPeerAddressChangedEvent() - { - PeerAddressChangedEvent = null; - } - - void INetEventListener.OnPeerConnected(NetPeer peer) - { - if (PeerConnectedEvent != null) - PeerConnectedEvent(peer); - } - - void INetEventListener.OnPeerDisconnected(NetPeer peer, DisconnectInfo disconnectInfo) - { - if (PeerDisconnectedEvent != null) - PeerDisconnectedEvent(peer, disconnectInfo); - } - - void INetEventListener.OnNetworkError(IPEndPoint endPoint, SocketError socketErrorCode) - { - if (NetworkErrorEvent != null) - NetworkErrorEvent(endPoint, socketErrorCode); - } - - void INetEventListener.OnNetworkReceive(NetPeer peer, NetPacketReader reader, byte channelNumber, DeliveryMethod deliveryMethod) - { - if (NetworkReceiveEvent != null) - NetworkReceiveEvent(peer, reader, channelNumber, deliveryMethod); - } - - void INetEventListener.OnNetworkReceiveUnconnected(IPEndPoint remoteEndPoint, NetPacketReader reader, UnconnectedMessageType messageType) - { - if (NetworkReceiveUnconnectedEvent != null) - NetworkReceiveUnconnectedEvent(remoteEndPoint, reader, messageType); - } - - void INetEventListener.OnNetworkLatencyUpdate(NetPeer peer, int latency) - { - if (NetworkLatencyUpdateEvent != null) - NetworkLatencyUpdateEvent(peer, latency); - } - - void INetEventListener.OnConnectionRequest(ConnectionRequest request) - { - if (ConnectionRequestEvent != null) - ConnectionRequestEvent(request); - } - - void IDeliveryEventListener.OnMessageDelivered(NetPeer peer, object userData) - { - if (DeliveryEvent != null) - DeliveryEvent(peer, userData); - } - - void INtpEventListener.OnNtpResponse(NtpPacket packet) - { - if (NtpResponseEvent != null) - NtpResponseEvent(packet); - } - - void IPeerAddressChangedListener.OnPeerAddressChanged(NetPeer peer, IPEndPoint previousAddress) - { - if (PeerAddressChangedEvent != null) - PeerAddressChangedEvent(peer, previousAddress); - } - } -} diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/INetEventListener.cs.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/INetEventListener.cs.meta deleted file mode 100644 index 926caffc..00000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/INetEventListener.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 93f8b04a076a8f7daafd08dcfb01344a -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/InternalPackets.cs b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/InternalPackets.cs deleted file mode 100644 index 2eb09fe5..00000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/InternalPackets.cs +++ /dev/null @@ -1,133 +0,0 @@ -using System; -using System.Net; -using LiteNetLib.Utils; - -namespace LiteNetLib -{ - internal sealed class NetConnectRequestPacket - { - public const int HeaderSize = 18; - public readonly long ConnectionTime; - public byte ConnectionNumber; - public readonly byte[] TargetAddress; - public readonly NetDataReader Data; - public readonly int PeerId; - - private NetConnectRequestPacket(long connectionTime, byte connectionNumber, int localId, byte[] targetAddress, NetDataReader data) - { - ConnectionTime = connectionTime; - ConnectionNumber = connectionNumber; - TargetAddress = targetAddress; - Data = data; - PeerId = localId; - } - - public static int GetProtocolId(NetPacket packet) - { - return BitConverter.ToInt32(packet.RawData, 1); - } - - public static NetConnectRequestPacket FromData(NetPacket packet) - { - if (packet.ConnectionNumber >= NetConstants.MaxConnectionNumber) - return null; - - //Getting connection time for peer - long connectionTime = BitConverter.ToInt64(packet.RawData, 5); - - //Get peer id - int peerId = BitConverter.ToInt32(packet.RawData, 13); - - //Get target address - int addrSize = packet.RawData[HeaderSize-1]; - if (addrSize != 16 && addrSize != 28) - return null; - byte[] addressBytes = new byte[addrSize]; - Buffer.BlockCopy(packet.RawData, HeaderSize, addressBytes, 0, addrSize); - - // Read data and create request - var reader = new NetDataReader(null, 0, 0); - if (packet.Size > HeaderSize+addrSize) - reader.SetSource(packet.RawData, HeaderSize + addrSize, packet.Size); - - return new NetConnectRequestPacket(connectionTime, packet.ConnectionNumber, peerId, addressBytes, reader); - } - - public static NetPacket Make(NetDataWriter connectData, SocketAddress addressBytes, long connectTime, int localId) - { - //Make initial packet - var packet = new NetPacket(PacketProperty.ConnectRequest, connectData.Length+addressBytes.Size); - - //Add data - FastBitConverter.GetBytes(packet.RawData, 1, NetConstants.ProtocolId); - FastBitConverter.GetBytes(packet.RawData, 5, connectTime); - FastBitConverter.GetBytes(packet.RawData, 13, localId); - packet.RawData[HeaderSize-1] = (byte)addressBytes.Size; - for (int i = 0; i < addressBytes.Size; i++) - packet.RawData[HeaderSize + i] = addressBytes[i]; - Buffer.BlockCopy(connectData.Data, 0, packet.RawData, HeaderSize + addressBytes.Size, connectData.Length); - return packet; - } - } - - internal sealed class NetConnectAcceptPacket - { - public const int Size = 15; - public readonly long ConnectionTime; - public readonly byte ConnectionNumber; - public readonly int PeerId; - public readonly bool PeerNetworkChanged; - - private NetConnectAcceptPacket(long connectionTime, byte connectionNumber, int peerId, bool peerNetworkChanged) - { - ConnectionTime = connectionTime; - ConnectionNumber = connectionNumber; - PeerId = peerId; - PeerNetworkChanged = peerNetworkChanged; - } - - public static NetConnectAcceptPacket FromData(NetPacket packet) - { - if (packet.Size != Size) - return null; - - long connectionId = BitConverter.ToInt64(packet.RawData, 1); - - //check connect num - byte connectionNumber = packet.RawData[9]; - if (connectionNumber >= NetConstants.MaxConnectionNumber) - return null; - - //check reused flag - byte isReused = packet.RawData[10]; - if (isReused > 1) - return null; - - //get remote peer id - int peerId = BitConverter.ToInt32(packet.RawData, 11); - if (peerId < 0) - return null; - - return new NetConnectAcceptPacket(connectionId, connectionNumber, peerId, isReused == 1); - } - - public static NetPacket Make(long connectTime, byte connectNum, int localPeerId) - { - var packet = new NetPacket(PacketProperty.ConnectAccept, 0); - FastBitConverter.GetBytes(packet.RawData, 1, connectTime); - packet.RawData[9] = connectNum; - FastBitConverter.GetBytes(packet.RawData, 11, localPeerId); - return packet; - } - - public static NetPacket MakeNetworkChanged(NetPeer peer) - { - var packet = new NetPacket(PacketProperty.PeerNotFound, Size-1); - FastBitConverter.GetBytes(packet.RawData, 1, peer.ConnectTime); - packet.RawData[9] = peer.ConnectionNum; - packet.RawData[10] = 1; - FastBitConverter.GetBytes(packet.RawData, 11, peer.RemoteId); - return packet; - } - } -} \ No newline at end of file diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/InternalPackets.cs.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/InternalPackets.cs.meta deleted file mode 100644 index a3dee0c0..00000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/InternalPackets.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 94139cc5687d01e41ac26a582821cd93 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Layers.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Layers.meta deleted file mode 100644 index 98e28a71..00000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Layers.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: 98f2a8c29716ee4e1ac9f90523cde098 -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Layers/Crc32cLayer.cs b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Layers/Crc32cLayer.cs deleted file mode 100644 index 3ee97d6c..00000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Layers/Crc32cLayer.cs +++ /dev/null @@ -1,41 +0,0 @@ -using LiteNetLib.Utils; -using System; -using System.Net; - -namespace LiteNetLib.Layers -{ - public sealed class Crc32cLayer : PacketLayerBase - { - public Crc32cLayer() : base(CRC32C.ChecksumSize) - { - - } - - public override void ProcessInboundPacket(ref IPEndPoint endPoint, ref byte[] data, ref int offset, ref int length) - { - if (length < NetConstants.HeaderSize + CRC32C.ChecksumSize) - { - NetDebug.WriteError("[NM] DataReceived size: bad!"); - //Set length to 0 to have netManager drop the packet. - length = 0; - return; - } - - int checksumPoint = length - CRC32C.ChecksumSize; - if (CRC32C.Compute(data, offset, checksumPoint) != BitConverter.ToUInt32(data, checksumPoint)) - { - NetDebug.Write("[NM] DataReceived checksum: bad!"); - //Set length to 0 to have netManager drop the packet. - length = 0; - return; - } - length -= CRC32C.ChecksumSize; - } - - public override void ProcessOutBoundPacket(ref IPEndPoint endPoint, ref byte[] data, ref int offset, ref int length) - { - FastBitConverter.GetBytes(data, length, CRC32C.Compute(data, offset, length)); - length += CRC32C.ChecksumSize; - } - } -} diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Layers/Crc32cLayer.cs.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Layers/Crc32cLayer.cs.meta deleted file mode 100644 index 55a2fe8e..00000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Layers/Crc32cLayer.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: dfc5584fdbe07f366bce12fcc6651303 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Layers/PacketLayerBase.cs b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Layers/PacketLayerBase.cs deleted file mode 100644 index b3d9b3a7..00000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Layers/PacketLayerBase.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Net; - -namespace LiteNetLib.Layers -{ - public abstract class PacketLayerBase - { - public readonly int ExtraPacketSizeForLayer; - - protected PacketLayerBase(int extraPacketSizeForLayer) - { - ExtraPacketSizeForLayer = extraPacketSizeForLayer; - } - - public abstract void ProcessInboundPacket(ref IPEndPoint endPoint, ref byte[] data, ref int offset, ref int length); - public abstract void ProcessOutBoundPacket(ref IPEndPoint endPoint, ref byte[] data, ref int offset, ref int length); - } -} diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Layers/PacketLayerBase.cs.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Layers/PacketLayerBase.cs.meta deleted file mode 100644 index 5bfbd7d8..00000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Layers/PacketLayerBase.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 3268d8ee9aff4d539b3e255f9494a6f4 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Layers/XorEncryptLayer.cs b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Layers/XorEncryptLayer.cs deleted file mode 100644 index 9b671969..00000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Layers/XorEncryptLayer.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System; -using System.Net; -using System.Text; - -namespace LiteNetLib.Layers -{ - public class XorEncryptLayer : PacketLayerBase - { - private byte[] _byteKey; - - public XorEncryptLayer() : base(0) - { - - } - - public XorEncryptLayer(byte[] key) : this() - { - SetKey(key); - } - - public XorEncryptLayer(string key) : this() - { - SetKey(key); - } - - public void SetKey(string key) - { - _byteKey = Encoding.UTF8.GetBytes(key); - } - - public void SetKey(byte[] key) - { - if (_byteKey == null || _byteKey.Length != key.Length) - _byteKey = new byte[key.Length]; - Buffer.BlockCopy(key, 0, _byteKey, 0, key.Length); - } - - public override void ProcessInboundPacket(ref IPEndPoint endPoint, ref byte[] data, ref int offset, ref int length) - { - if (_byteKey == null) - return; - var cur = offset; - for (var i = 0; i < length; i++, cur++) - { - data[cur] = (byte)(data[cur] ^ _byteKey[i % _byteKey.Length]); - } - } - - public override void ProcessOutBoundPacket(ref IPEndPoint endPoint, ref byte[] data, ref int offset, ref int length) - { - if (_byteKey == null) - return; - var cur = offset; - for (var i = 0; i < length; i++, cur++) - { - data[cur] = (byte)(data[cur] ^ _byteKey[i % _byteKey.Length]); - } - } - } -} diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Layers/XorEncryptLayer.cs.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Layers/XorEncryptLayer.cs.meta deleted file mode 100644 index db230749..00000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Layers/XorEncryptLayer.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 912645dcda5efef528a82cd323caff02 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/LiteNetLib.asmdef b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/LiteNetLib.asmdef deleted file mode 100644 index 530c72ed..00000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/LiteNetLib.asmdef +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "LiteNetLib", - "references": [], - "includePlatforms": [], - "excludePlatforms": [], - "allowUnsafeCode": true, - "overrideReferences": false, - "precompiledReferences": [], - "autoReferenced": true, - "defineConstraints": [], - "versionDefines": [], - "noEngineReferences": false -} \ No newline at end of file diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/LiteNetLib.asmdef.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/LiteNetLib.asmdef.meta deleted file mode 100644 index bcaa3e64..00000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/LiteNetLib.asmdef.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: e9645de4460073e6ab30a874509a9ca9 -AssemblyDefinitionImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/LiteNetLib.csproj b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/LiteNetLib.csproj deleted file mode 100644 index 4a58e9ea..00000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/LiteNetLib.csproj +++ /dev/null @@ -1,62 +0,0 @@ - - - - LiteNetLib - LiteNetLib - net6.0;net5.0;netcoreapp3.1;netstandard2.0;netstandard2.1 - net471;net6.0;net5.0;netstandard2.0;netstandard2.1;netcoreapp3.1 - true - Library - 7.3 - - true - 1701;1702;1705;1591 - 1.1.0 - Lite reliable UDP library for Mono and .NET - - - - TRACE;DEBUG - - - - TRACE - - - - true - $(DefineConstants);LITENETLIB_UNSAFE - udp reliable-udp network - https://github.com/RevenantX/LiteNetLib/releases/tag/v1.1.0 - git - https://github.com/RevenantX/LiteNetLib - https://github.com/RevenantX/LiteNetLib - MIT - True - 1.1.0 - Ruslan Pyrch - Copyright 2023 Ruslan Pyrch - Lite reliable UDP library for .NET, Mono, and .NET Core - LNL.png - README.md - - - - - - - - - - - - True - \ - - - True - \ - - - - \ No newline at end of file diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/LiteNetLib.csproj.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/LiteNetLib.csproj.meta deleted file mode 100644 index 30846960..00000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/LiteNetLib.csproj.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: 47decce79e6fe28a585acafb4b2baf86 -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NatPunchModule.cs b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NatPunchModule.cs deleted file mode 100644 index 0032f3cf..00000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NatPunchModule.cs +++ /dev/null @@ -1,259 +0,0 @@ -using System.Collections.Concurrent; -using System.Net; -using LiteNetLib.Utils; - -namespace LiteNetLib -{ - public enum NatAddressType - { - Internal, - External - } - - public interface INatPunchListener - { - void OnNatIntroductionRequest(IPEndPoint localEndPoint, IPEndPoint remoteEndPoint, string token); - void OnNatIntroductionSuccess(IPEndPoint targetEndPoint, NatAddressType type, string token); - } - - public class EventBasedNatPunchListener : INatPunchListener - { - public delegate void OnNatIntroductionRequest(IPEndPoint localEndPoint, IPEndPoint remoteEndPoint, string token); - public delegate void OnNatIntroductionSuccess(IPEndPoint targetEndPoint, NatAddressType type, string token); - - public event OnNatIntroductionRequest NatIntroductionRequest; - public event OnNatIntroductionSuccess NatIntroductionSuccess; - - void INatPunchListener.OnNatIntroductionRequest(IPEndPoint localEndPoint, IPEndPoint remoteEndPoint, string token) - { - if(NatIntroductionRequest != null) - NatIntroductionRequest(localEndPoint, remoteEndPoint, token); - } - - void INatPunchListener.OnNatIntroductionSuccess(IPEndPoint targetEndPoint, NatAddressType type, string token) - { - if (NatIntroductionSuccess != null) - NatIntroductionSuccess(targetEndPoint, type, token); - } - } - - /// - /// Module for UDP NAT Hole punching operations. Can be accessed from NetManager - /// - public sealed class NatPunchModule - { - struct RequestEventData - { - public IPEndPoint LocalEndPoint; - public IPEndPoint RemoteEndPoint; - public string Token; - } - - struct SuccessEventData - { - public IPEndPoint TargetEndPoint; - public NatAddressType Type; - public string Token; - } - - class NatIntroduceRequestPacket - { - public IPEndPoint Internal { [Preserve] get; [Preserve] set; } - public string Token { [Preserve] get; [Preserve] set; } - } - - class NatIntroduceResponsePacket - { - public IPEndPoint Internal { [Preserve] get; [Preserve] set; } - public IPEndPoint External { [Preserve] get; [Preserve] set; } - public string Token { [Preserve] get; [Preserve] set; } - } - - class NatPunchPacket - { - public string Token { [Preserve] get; [Preserve] set; } - public bool IsExternal { [Preserve] get; [Preserve] set; } - } - - private readonly NetManager _socket; - private readonly ConcurrentQueue _requestEvents = new ConcurrentQueue(); - private readonly ConcurrentQueue _successEvents = new ConcurrentQueue(); - private readonly NetDataReader _cacheReader = new NetDataReader(); - private readonly NetDataWriter _cacheWriter = new NetDataWriter(); - private readonly NetPacketProcessor _netPacketProcessor; - private INatPunchListener _natPunchListener; - public const int MaxTokenLength = 256; - - /// - /// Events automatically will be called without PollEvents method from another thread - /// - public bool UnsyncedEvents = false; - - internal NatPunchModule(NetManager socket) - { - _socket = socket; - _netPacketProcessor = new NetPacketProcessor(_socket, MaxTokenLength); - _netPacketProcessor.SubscribeReusable(OnNatIntroductionResponse); - _netPacketProcessor.SubscribeReusable(OnNatIntroductionRequest); - _netPacketProcessor.SubscribeReusable(OnNatPunch); - } - - internal void ProcessMessage(IPEndPoint senderEndPoint, NetPacket packet) - { - lock (_cacheReader) - { - _cacheReader.SetSource(packet.RawData, NetConstants.HeaderSize, packet.Size); - _netPacketProcessor.ReadAllPackets(_cacheReader, senderEndPoint); - } - } - - public void Init(INatPunchListener listener) - { - _natPunchListener = listener; - } - - private void Send(T packet, IPEndPoint target) where T : class, new() - { - _cacheWriter.Reset(); - _cacheWriter.Put((byte)PacketProperty.NatMessage); - _netPacketProcessor.Write(_cacheWriter, packet); - _socket.SendRaw(_cacheWriter.Data, 0, _cacheWriter.Length, target); - } - - public void NatIntroduce( - IPEndPoint hostInternal, - IPEndPoint hostExternal, - IPEndPoint clientInternal, - IPEndPoint clientExternal, - string additionalInfo) - { - var req = new NatIntroduceResponsePacket - { - Token = additionalInfo - }; - - //First packet (server) send to client - req.Internal = hostInternal; - req.External = hostExternal; - Send(req, clientExternal); - - //Second packet (client) send to server - req.Internal = clientInternal; - req.External = clientExternal; - Send(req, hostExternal); - } - - public void PollEvents() - { - if (UnsyncedEvents) - return; - - if (_natPunchListener == null || (_successEvents.IsEmpty && _requestEvents.IsEmpty)) - return; - - while (_successEvents.TryDequeue(out var evt)) - { - _natPunchListener.OnNatIntroductionSuccess( - evt.TargetEndPoint, - evt.Type, - evt.Token); - } - - while (_requestEvents.TryDequeue(out var evt)) - { - _natPunchListener.OnNatIntroductionRequest(evt.LocalEndPoint, evt.RemoteEndPoint, evt.Token); - } - } - - public void SendNatIntroduceRequest(string host, int port, string additionalInfo) - { - SendNatIntroduceRequest(NetUtils.MakeEndPoint(host, port), additionalInfo); - } - - public void SendNatIntroduceRequest(IPEndPoint masterServerEndPoint, string additionalInfo) - { - //prepare outgoing data - string networkIp = NetUtils.GetLocalIp(LocalAddrType.IPv4); - if (string.IsNullOrEmpty(networkIp)) - { - networkIp = NetUtils.GetLocalIp(LocalAddrType.IPv6); - } - - Send( - new NatIntroduceRequestPacket - { - Internal = NetUtils.MakeEndPoint(networkIp, _socket.LocalPort), - Token = additionalInfo - }, - masterServerEndPoint); - } - - //We got request and must introduce - private void OnNatIntroductionRequest(NatIntroduceRequestPacket req, IPEndPoint senderEndPoint) - { - if (UnsyncedEvents) - { - _natPunchListener.OnNatIntroductionRequest( - req.Internal, - senderEndPoint, - req.Token); - } - else - { - _requestEvents.Enqueue(new RequestEventData - { - LocalEndPoint = req.Internal, - RemoteEndPoint = senderEndPoint, - Token = req.Token - }); - } - } - - //We got introduce and must punch - private void OnNatIntroductionResponse(NatIntroduceResponsePacket req) - { - NetDebug.Write(NetLogLevel.Trace, "[NAT] introduction received"); - - // send internal punch - var punchPacket = new NatPunchPacket {Token = req.Token}; - Send(punchPacket, req.Internal); - NetDebug.Write(NetLogLevel.Trace, $"[NAT] internal punch sent to {req.Internal}"); - - // hack for some routers - _socket.Ttl = 2; - _socket.SendRaw(new[] { (byte)PacketProperty.Empty }, 0, 1, req.External); - - // send external punch - _socket.Ttl = NetConstants.SocketTTL; - punchPacket.IsExternal = true; - Send(punchPacket, req.External); - NetDebug.Write(NetLogLevel.Trace, $"[NAT] external punch sent to {req.External}"); - } - - //We got punch and can connect - private void OnNatPunch(NatPunchPacket req, IPEndPoint senderEndPoint) - { - //Read info - NetDebug.Write(NetLogLevel.Trace, $"[NAT] punch received from {senderEndPoint} - additional info: {req.Token}"); - - //Release punch success to client; enabling him to Connect() to Sender if token is ok - if(UnsyncedEvents) - { - _natPunchListener.OnNatIntroductionSuccess( - senderEndPoint, - req.IsExternal ? NatAddressType.External : NatAddressType.Internal, - req.Token - ); - } - else - { - _successEvents.Enqueue(new SuccessEventData - { - TargetEndPoint = senderEndPoint, - Type = req.IsExternal ? NatAddressType.External : NatAddressType.Internal, - Token = req.Token - }); - } - } - } -} diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NatPunchModule.cs.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NatPunchModule.cs.meta deleted file mode 100644 index fa737432..00000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NatPunchModule.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: e21233a9193cb672790e5a08e578e090 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NativeSocket.cs b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NativeSocket.cs deleted file mode 100644 index fc846c8b..00000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NativeSocket.cs +++ /dev/null @@ -1,301 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Net; -using System.Net.Sockets; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -namespace LiteNetLib -{ - internal readonly struct NativeAddr : IEquatable - { - //common parts - private readonly long _part1; //family, port, etc - private readonly long _part2; - //ipv6 parts - private readonly long _part3; - private readonly int _part4; - - private readonly int _hash; - - public NativeAddr(byte[] address, int len) - { - _part1 = BitConverter.ToInt64(address, 0); - _part2 = BitConverter.ToInt64(address, 8); - if (len > 16) - { - _part3 = BitConverter.ToInt64(address, 16); - _part4 = BitConverter.ToInt32(address, 24); - } - else - { - _part3 = 0; - _part4 = 0; - } - _hash = (int)(_part1 >> 32) ^ (int)_part1 ^ - (int)(_part2 >> 32) ^ (int)_part2 ^ - (int)(_part3 >> 32) ^ (int)_part3 ^ - _part4; - } - - public override int GetHashCode() - { - return _hash; - } - - public bool Equals(NativeAddr other) - { - return _part1 == other._part1 && - _part2 == other._part2 && - _part3 == other._part3 && - _part4 == other._part4; - } - - public override bool Equals(object obj) - { - return obj is NativeAddr other && Equals(other); - } - - public static bool operator ==(NativeAddr left, NativeAddr right) - { - return left.Equals(right); - } - - public static bool operator !=(NativeAddr left, NativeAddr right) - { - return !left.Equals(right); - } - } - - internal class NativeEndPoint : IPEndPoint - { - public readonly byte[] NativeAddress; - - public NativeEndPoint(byte[] address) : base(IPAddress.Any, 0) - { - NativeAddress = new byte[address.Length]; - Buffer.BlockCopy(address, 0, NativeAddress, 0, address.Length); - - short family = (short)((address[1] << 8) | address[0]); - Port =(ushort)((address[2] << 8) | address[3]); - - if ((NativeSocket.UnixMode && family == NativeSocket.AF_INET6) || (!NativeSocket.UnixMode && (AddressFamily)family == AddressFamily.InterNetworkV6)) - { - uint scope = unchecked((uint)( - (address[27] << 24) + - (address[26] << 16) + - (address[25] << 8) + - (address[24]))); -#if NETCOREAPP || NETSTANDARD2_1 || NETSTANDARD2_1_OR_GREATER - Address = new IPAddress(new ReadOnlySpan(address, 8, 16), scope); -#else - byte[] addrBuffer = new byte[16]; - Buffer.BlockCopy(address, 8, addrBuffer, 0, 16); - Address = new IPAddress(addrBuffer, scope); -#endif - } - else //IPv4 - { - long ipv4Addr = unchecked((uint)((address[4] & 0x000000FF) | - (address[5] << 8 & 0x0000FF00) | - (address[6] << 16 & 0x00FF0000) | - (address[7] << 24))); - Address = new IPAddress(ipv4Addr); - } - } - } - - internal static class NativeSocket - { - static -#if LITENETLIB_UNSAFE - unsafe -#endif - class WinSock - { - private const string LibName = "ws2_32.dll"; - - [DllImport(LibName, SetLastError = true)] - public static extern int recvfrom( - IntPtr socketHandle, - [In, Out] byte[] pinnedBuffer, - [In] int len, - [In] SocketFlags socketFlags, - [Out] byte[] socketAddress, - [In, Out] ref int socketAddressSize); - - [DllImport(LibName, SetLastError = true)] - internal static extern int sendto( - IntPtr socketHandle, -#if LITENETLIB_UNSAFE - byte* pinnedBuffer, -#else - [In] byte[] pinnedBuffer, -#endif - [In] int len, - [In] SocketFlags socketFlags, - [In] byte[] socketAddress, - [In] int socketAddressSize); - } - - static -#if LITENETLIB_UNSAFE - unsafe -#endif - class UnixSock - { - private const string LibName = "libc"; - - [DllImport(LibName, SetLastError = true)] - public static extern int recvfrom( - IntPtr socketHandle, - [In, Out] byte[] pinnedBuffer, - [In] int len, - [In] SocketFlags socketFlags, - [Out] byte[] socketAddress, - [In, Out] ref int socketAddressSize); - - [DllImport(LibName, SetLastError = true)] - internal static extern int sendto( - IntPtr socketHandle, -#if LITENETLIB_UNSAFE - byte* pinnedBuffer, -#else - [In] byte[] pinnedBuffer, -#endif - [In] int len, - [In] SocketFlags socketFlags, - [In] byte[] socketAddress, - [In] int socketAddressSize); - } - - public static readonly bool IsSupported = false; - public static readonly bool UnixMode = false; - - public const int IPv4AddrSize = 16; - public const int IPv6AddrSize = 28; - public const int AF_INET = 2; - public const int AF_INET6 = 10; - - private static readonly Dictionary NativeErrorToSocketError = new Dictionary - { - { 13, SocketError.AccessDenied }, //EACCES - { 98, SocketError.AddressAlreadyInUse }, //EADDRINUSE - { 99, SocketError.AddressNotAvailable }, //EADDRNOTAVAIL - { 97, SocketError.AddressFamilyNotSupported }, //EAFNOSUPPORT - { 11, SocketError.WouldBlock }, //EAGAIN - { 114, SocketError.AlreadyInProgress }, //EALREADY - { 9, SocketError.OperationAborted }, //EBADF - { 125, SocketError.OperationAborted }, //ECANCELED - { 103, SocketError.ConnectionAborted }, //ECONNABORTED - { 111, SocketError.ConnectionRefused }, //ECONNREFUSED - { 104, SocketError.ConnectionReset }, //ECONNRESET - { 89, SocketError.DestinationAddressRequired }, //EDESTADDRREQ - { 14, SocketError.Fault }, //EFAULT - { 112, SocketError.HostDown }, //EHOSTDOWN - { 6, SocketError.HostNotFound }, //ENXIO - { 113, SocketError.HostUnreachable }, //EHOSTUNREACH - { 115, SocketError.InProgress }, //EINPROGRESS - { 4, SocketError.Interrupted }, //EINTR - { 22, SocketError.InvalidArgument }, //EINVAL - { 106, SocketError.IsConnected }, //EISCONN - { 24, SocketError.TooManyOpenSockets }, //EMFILE - { 90, SocketError.MessageSize }, //EMSGSIZE - { 100, SocketError.NetworkDown }, //ENETDOWN - { 102, SocketError.NetworkReset }, //ENETRESET - { 101, SocketError.NetworkUnreachable }, //ENETUNREACH - { 23, SocketError.TooManyOpenSockets }, //ENFILE - { 105, SocketError.NoBufferSpaceAvailable }, //ENOBUFS - { 61, SocketError.NoData }, //ENODATA - { 2, SocketError.AddressNotAvailable }, //ENOENT - { 92, SocketError.ProtocolOption }, //ENOPROTOOPT - { 107, SocketError.NotConnected }, //ENOTCONN - { 88, SocketError.NotSocket }, //ENOTSOCK - { 3440, SocketError.OperationNotSupported }, //ENOTSUP - { 1, SocketError.AccessDenied }, //EPERM - { 32, SocketError.Shutdown }, //EPIPE - { 96, SocketError.ProtocolFamilyNotSupported }, //EPFNOSUPPORT - { 93, SocketError.ProtocolNotSupported }, //EPROTONOSUPPORT - { 91, SocketError.ProtocolType }, //EPROTOTYPE - { 94, SocketError.SocketNotSupported }, //ESOCKTNOSUPPORT - { 108, SocketError.Disconnecting }, //ESHUTDOWN - { 110, SocketError.TimedOut }, //ETIMEDOUT - { 0, SocketError.Success } - }; - - static NativeSocket() - { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - { - IsSupported = true; - UnixMode = true; - } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - IsSupported = true; - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int RecvFrom( - IntPtr socketHandle, - byte[] pinnedBuffer, - int len, - byte[] socketAddress, - ref int socketAddressSize) - { - return UnixMode - ? UnixSock.recvfrom(socketHandle, pinnedBuffer, len, 0, socketAddress, ref socketAddressSize) - : WinSock.recvfrom(socketHandle, pinnedBuffer, len, 0, socketAddress, ref socketAddressSize); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public -#if LITENETLIB_UNSAFE - unsafe -#endif - static int SendTo( - IntPtr socketHandle, -#if LITENETLIB_UNSAFE - byte* pinnedBuffer, -#else - byte[] pinnedBuffer, -#endif - int len, - byte[] socketAddress, - int socketAddressSize) - { - return UnixMode - ? UnixSock.sendto(socketHandle, pinnedBuffer, len, 0, socketAddress, socketAddressSize) - : WinSock.sendto(socketHandle, pinnedBuffer, len, 0, socketAddress, socketAddressSize); - } - - public static SocketError GetSocketError() - { - int error = Marshal.GetLastWin32Error(); - if (UnixMode) - return NativeErrorToSocketError.TryGetValue(error, out var err) - ? err - : SocketError.SocketError; - return (SocketError)error; - } - - public static SocketException GetSocketException() - { - int error = Marshal.GetLastWin32Error(); - if (UnixMode) - return NativeErrorToSocketError.TryGetValue(error, out var err) - ? new SocketException((int)err) - : new SocketException((int)SocketError.SocketError); - return new SocketException(error); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static short GetNativeAddressFamily(IPEndPoint remoteEndPoint) - { - return UnixMode - ? (short)(remoteEndPoint.AddressFamily == AddressFamily.InterNetwork ? AF_INET : AF_INET6) - : (short)remoteEndPoint.AddressFamily; - } - } -} diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NativeSocket.cs.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NativeSocket.cs.meta deleted file mode 100644 index 586308e3..00000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NativeSocket.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 29554417dec720ea5a66a5478451889c -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetConstants.cs b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetConstants.cs deleted file mode 100644 index ca7dfbcd..00000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetConstants.cs +++ /dev/null @@ -1,75 +0,0 @@ -namespace LiteNetLib -{ - /// - /// Sending method type - /// - public enum DeliveryMethod : byte - { - /// - /// Unreliable. Packets can be dropped, can be duplicated, can arrive without order. - /// - Unreliable = 4, - - /// - /// Reliable. Packets won't be dropped, won't be duplicated, can arrive without order. - /// - ReliableUnordered = 0, - - /// - /// Unreliable. Packets can be dropped, won't be duplicated, will arrive in order. - /// - Sequenced = 1, - - /// - /// Reliable and ordered. Packets won't be dropped, won't be duplicated, will arrive in order. - /// - ReliableOrdered = 2, - - /// - /// Reliable only last packet. Packets can be dropped (except the last one), won't be duplicated, will arrive in order. - /// Cannot be fragmented - /// - ReliableSequenced = 3 - } - - /// - /// Network constants. Can be tuned from sources for your purposes. - /// - public static class NetConstants - { - //can be tuned - public const int DefaultWindowSize = 64; - public const int SocketBufferSize = 1024 * 1024; //1mb - public const int SocketTTL = 255; - - public const int HeaderSize = 1; - public const int ChanneledHeaderSize = 4; - public const int FragmentHeaderSize = 6; - public const int FragmentedHeaderTotalSize = ChanneledHeaderSize + FragmentHeaderSize; - public const ushort MaxSequence = 32768; - public const ushort HalfMaxSequence = MaxSequence / 2; - - //protocol - internal const int ProtocolId = 13; - internal const int MaxUdpHeaderSize = 68; - internal const int ChannelTypeCount = 4; - - internal static readonly int[] PossibleMtu = - { - 576 - MaxUdpHeaderSize, //minimal (RFC 1191) - 1024, //most games standard - 1232 - MaxUdpHeaderSize, - 1460 - MaxUdpHeaderSize, //google cloud - 1472 - MaxUdpHeaderSize, //VPN - 1492 - MaxUdpHeaderSize, //Ethernet with LLC and SNAP, PPPoE (RFC 1042) - 1500 - MaxUdpHeaderSize //Ethernet II (RFC 1191) - }; - - //Max possible single packet size - public static readonly int MaxPacketSize = PossibleMtu[PossibleMtu.Length - 1]; - public static readonly int MaxUnreliableDataSize = MaxPacketSize - HeaderSize; - - //peer specific - public const byte MaxConnectionNumber = 4; - } -} diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetConstants.cs.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetConstants.cs.meta deleted file mode 100644 index 26bc130a..00000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetConstants.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 00de73a0c90d93374b1213cdb597871a -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetDebug.cs b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetDebug.cs deleted file mode 100644 index 44cb6f3e..00000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetDebug.cs +++ /dev/null @@ -1,92 +0,0 @@ -using System; -using System.Diagnostics; - -namespace LiteNetLib -{ - public class InvalidPacketException : ArgumentException - { - public InvalidPacketException(string message) : base(message) - { - } - } - - public class TooBigPacketException : InvalidPacketException - { - public TooBigPacketException(string message) : base(message) - { - } - } - - public enum NetLogLevel - { - Warning, - Error, - Trace, - Info - } - - /// - /// Interface to implement for your own logger - /// - public interface INetLogger - { - void WriteNet(NetLogLevel level, string str, params object[] args); - } - - /// - /// Static class for defining your own LiteNetLib logger instead of Console.WriteLine - /// or Debug.Log if compiled with UNITY flag - /// - public static class NetDebug - { - public static INetLogger Logger = null; - private static readonly object DebugLogLock = new object(); - private static void WriteLogic(NetLogLevel logLevel, string str, params object[] args) - { - lock (DebugLogLock) - { - if (Logger == null) - { -#if UNITY_5_3_OR_NEWER - UnityEngine.Debug.Log(string.Format(str, args)); -#else - Console.WriteLine(str, args); -#endif - } - else - { - Logger.WriteNet(logLevel, str, args); - } - } - } - - [Conditional("DEBUG_MESSAGES")] - internal static void Write(string str) - { - WriteLogic(NetLogLevel.Trace, str); - } - - [Conditional("DEBUG_MESSAGES")] - internal static void Write(NetLogLevel level, string str) - { - WriteLogic(level, str); - } - - [Conditional("DEBUG_MESSAGES"), Conditional("DEBUG")] - internal static void WriteForce(string str) - { - WriteLogic(NetLogLevel.Trace, str); - } - - [Conditional("DEBUG_MESSAGES"), Conditional("DEBUG")] - internal static void WriteForce(NetLogLevel level, string str) - { - WriteLogic(level, str); - } - - internal static void WriteError(string str) - { - WriteLogic(NetLogLevel.Error, str); - } - } -} diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetDebug.cs.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetDebug.cs.meta deleted file mode 100644 index e74c9ba9..00000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetDebug.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 99fd29417a05299a0aaf9fcf586f92e9 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetManager.PacketPool.cs b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetManager.PacketPool.cs deleted file mode 100644 index 08312209..00000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetManager.PacketPool.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System; - -namespace LiteNetLib -{ - public partial class NetManager - { - private NetPacket _poolHead; - private int _poolCount; - private readonly object _poolLock = new object(); - - /// - /// Maximum packet pool size (increase if you have tons of packets sending) - /// - public int PacketPoolSize = 1000; - - public int PoolCount => _poolCount; - - private NetPacket PoolGetWithData(PacketProperty property, byte[] data, int start, int length) - { - int headerSize = NetPacket.GetHeaderSize(property); - NetPacket packet = PoolGetPacket(length + headerSize); - packet.Property = property; - Buffer.BlockCopy(data, start, packet.RawData, headerSize, length); - return packet; - } - - //Get packet with size - private NetPacket PoolGetWithProperty(PacketProperty property, int size) - { - NetPacket packet = PoolGetPacket(size + NetPacket.GetHeaderSize(property)); - packet.Property = property; - return packet; - } - - private NetPacket PoolGetWithProperty(PacketProperty property) - { - NetPacket packet = PoolGetPacket(NetPacket.GetHeaderSize(property)); - packet.Property = property; - return packet; - } - - internal NetPacket PoolGetPacket(int size) - { - if (size > NetConstants.MaxPacketSize) - return new NetPacket(size); - - NetPacket packet; - lock (_poolLock) - { - packet = _poolHead; - if (packet == null) - return new NetPacket(size); - - _poolHead = _poolHead.Next; - _poolCount--; - } - - packet.Size = size; - if (packet.RawData.Length < size) - packet.RawData = new byte[size]; - return packet; - } - - internal void PoolRecycle(NetPacket packet) - { - if (packet.RawData.Length > NetConstants.MaxPacketSize || _poolCount >= PacketPoolSize) - { - //Don't pool big packets. Save memory - return; - } - - //Clean fragmented flag - packet.RawData[0] = 0; - lock (_poolLock) - { - packet.Next = _poolHead; - _poolHead = packet; - _poolCount++; - } - } - } -} diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetManager.PacketPool.cs.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetManager.PacketPool.cs.meta deleted file mode 100644 index d5998d13..00000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetManager.PacketPool.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: e26aba5fa2d7e2d91b9dce82e304fde2 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetManager.Socket.cs b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetManager.Socket.cs deleted file mode 100644 index aabeaa36..00000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetManager.Socket.cs +++ /dev/null @@ -1,732 +0,0 @@ -using System.Runtime.InteropServices; -using System; -using System.Collections.Generic; -using System.Net; -using System.Net.Sockets; -using System.Threading; -using LiteNetLib.Utils; - -namespace LiteNetLib -{ - public partial class NetManager - { - private const int ReceivePollingTime = 500000; //0.5 second - - private Socket _udpSocketv4; - private Socket _udpSocketv6; - private Thread _receiveThread; - private IPEndPoint _bufferEndPointv4; - private IPEndPoint _bufferEndPointv6; -#if UNITY_2018_3_OR_NEWER - private PausedSocketFix _pausedSocketFix; -#endif - -#if !LITENETLIB_UNSAFE - [ThreadStatic] private static byte[] _sendToBuffer; -#endif - [ThreadStatic] private static byte[] _endPointBuffer; - - private readonly Dictionary _nativeAddrMap = new Dictionary(); - - private const int SioUdpConnreset = -1744830452; //SIO_UDP_CONNRESET = IOC_IN | IOC_VENDOR | 12 - private static readonly IPAddress MulticastAddressV6 = IPAddress.Parse("ff02::1"); - public static readonly bool IPv6Support; - - /// - /// Maximum packets count that will be processed in Manual PollEvents - /// - public int MaxPacketsReceivePerUpdate = 0; - - // special case in iOS (and possibly android that should be resolved in unity) - internal bool NotConnected; - - public short Ttl - { - get - { -#if UNITY_SWITCH - return 0; -#else - return _udpSocketv4.Ttl; -#endif - } - internal set - { -#if !UNITY_SWITCH - _udpSocketv4.Ttl = value; -#endif - } - } - - static NetManager() - { -#if DISABLE_IPV6 - IPv6Support = false; -#elif !UNITY_2019_1_OR_NEWER && !UNITY_2018_4_OR_NEWER && (!UNITY_EDITOR && ENABLE_IL2CPP) - string version = UnityEngine.Application.unityVersion; - IPv6Support = Socket.OSSupportsIPv6 && int.Parse(version.Remove(version.IndexOf('f')).Split('.')[2]) >= 6; -#else - IPv6Support = Socket.OSSupportsIPv6; -#endif - } - - private void RegisterEndPoint(IPEndPoint ep) - { - if (UseNativeSockets && ep is NativeEndPoint nep) - { - _nativeAddrMap.Add(new NativeAddr(nep.NativeAddress, nep.NativeAddress.Length), nep); - } - } - - private void UnregisterEndPoint(IPEndPoint ep) - { - if (UseNativeSockets && ep is NativeEndPoint nep) - { - var nativeAddr = new NativeAddr(nep.NativeAddress, nep.NativeAddress.Length); - _nativeAddrMap.Remove(nativeAddr); - } - } - - private bool ProcessError(SocketException ex) - { - switch (ex.SocketErrorCode) - { - case SocketError.NotConnected: - NotConnected = true; - return true; - case SocketError.Interrupted: - case SocketError.NotSocket: - case SocketError.OperationAborted: - return true; - case SocketError.ConnectionReset: - case SocketError.MessageSize: - case SocketError.TimedOut: - case SocketError.NetworkReset: - //NetDebug.Write($"[R]Ignored error: {(int)ex.SocketErrorCode} - {ex}"); - break; - default: - NetDebug.WriteError($"[R]Error code: {(int)ex.SocketErrorCode} - {ex}"); - CreateEvent(NetEvent.EType.Error, errorCode: ex.SocketErrorCode); - break; - } - return false; - } - - private void ManualReceive(Socket socket, EndPoint bufferEndPoint) - { - //Reading data - try - { - int packetsReceived = 0; - while (socket.Available > 0) - { - ReceiveFrom(socket, ref bufferEndPoint); - packetsReceived++; - if (packetsReceived == MaxPacketsReceivePerUpdate) - break; - } - } - catch (SocketException ex) - { - ProcessError(ex); - } - catch (ObjectDisposedException) - { - - } - catch (Exception e) - { - //protects socket receive thread - NetDebug.WriteError("[NM] SocketReceiveThread error: " + e ); - } - } - - private bool NativeReceiveFrom(ref NetPacket packet, IntPtr s, byte[] addrBuffer, int addrSize) - { - //Reading data - packet.Size = NativeSocket.RecvFrom(s, packet.RawData, NetConstants.MaxPacketSize, addrBuffer, ref addrSize); - if (packet.Size == 0) - return false; //socket closed - if (packet.Size == -1) - { - var errorCode = NativeSocket.GetSocketError(); - //Linux timeout EAGAIN - return errorCode == SocketError.WouldBlock || errorCode == SocketError.TimedOut || ProcessError(new SocketException((int)errorCode)) == false; - } - - var nativeAddr = new NativeAddr(addrBuffer, addrSize); - if (!_nativeAddrMap.TryGetValue(nativeAddr, out var endPoint)) - endPoint = new NativeEndPoint(addrBuffer); - - //All ok! - //NetDebug.WriteForce($"[R]Received data from {endPoint}, result: {packet.Size}"); - OnMessageReceived(packet, endPoint); - packet = PoolGetPacket(NetConstants.MaxPacketSize); - return true; - } - - private void NativeReceiveLogic() - { - IntPtr socketHandle4 = _udpSocketv4.Handle; - IntPtr socketHandle6 = _udpSocketv6?.Handle ?? IntPtr.Zero; - byte[] addrBuffer4 = new byte[NativeSocket.IPv4AddrSize]; - byte[] addrBuffer6 = new byte[NativeSocket.IPv6AddrSize]; - int addrSize4 = addrBuffer4.Length; - int addrSize6 = addrBuffer6.Length; - var selectReadList = new List(2); - var socketv4 = _udpSocketv4; - var socketV6 = _udpSocketv6; - var packet = PoolGetPacket(NetConstants.MaxPacketSize); - - while (IsRunning) - { - if (socketV6 == null) - { - if (NativeReceiveFrom(ref packet, socketHandle4, addrBuffer4, addrSize4) == false) - return; - continue; - } - bool messageReceived = false; - if (socketv4.Available != 0) - { - if (NativeReceiveFrom(ref packet, socketHandle4, addrBuffer4, addrSize4) == false) - return; - messageReceived = true; - } - if (socketV6.Available != 0) - { - if (NativeReceiveFrom(ref packet, socketHandle6, addrBuffer6, addrSize6) == false) - return; - messageReceived = true; - } - if (messageReceived) - continue; - selectReadList.Clear(); - selectReadList.Add(socketv4); - selectReadList.Add(socketV6); - try - { - Socket.Select(selectReadList, null, null, ReceivePollingTime); - } - catch (SocketException ex) - { - if (ProcessError(ex)) - return; - } - catch (ObjectDisposedException) - { - //socket closed - return; - } - catch (ThreadAbortException) - { - //thread closed - return; - } - catch (Exception e) - { - //protects socket receive thread - NetDebug.WriteError("[NM] SocketReceiveThread error: " + e ); - } - } - } - - private void ReceiveFrom(Socket s, ref EndPoint bufferEndPoint) - { - var packet = PoolGetPacket(NetConstants.MaxPacketSize); - packet.Size = s.ReceiveFrom(packet.RawData, 0, NetConstants.MaxPacketSize, SocketFlags.None, ref bufferEndPoint); - OnMessageReceived(packet, (IPEndPoint)bufferEndPoint); - } - - private void ReceiveLogic() - { - EndPoint bufferEndPoint4 = new IPEndPoint(IPAddress.Any, 0); - EndPoint bufferEndPoint6 = new IPEndPoint(IPAddress.IPv6Any, 0); - var selectReadList = new List(2); - var socketv4 = _udpSocketv4; - var socketV6 = _udpSocketv6; - - while (IsRunning) - { - //Reading data - try - { - if (socketV6 == null) - { - if (socketv4.Available == 0 && !socketv4.Poll(ReceivePollingTime, SelectMode.SelectRead)) - continue; - ReceiveFrom(socketv4, ref bufferEndPoint4); - } - else - { - bool messageReceived = false; - if (socketv4.Available != 0) - { - ReceiveFrom(socketv4, ref bufferEndPoint4); - messageReceived = true; - } - if (socketV6.Available != 0) - { - ReceiveFrom(socketV6, ref bufferEndPoint6); - messageReceived = true; - } - if (messageReceived) - continue; - - selectReadList.Clear(); - selectReadList.Add(socketv4); - selectReadList.Add(socketV6); - Socket.Select(selectReadList, null, null, ReceivePollingTime); - } - //NetDebug.Write(NetLogLevel.Trace, $"[R]Received data from {bufferEndPoint}, result: {packet.Size}"); - } - catch (SocketException ex) - { - if (ProcessError(ex)) - return; - } - catch (ObjectDisposedException) - { - //socket closed - return; - } - catch (ThreadAbortException) - { - //thread closed - return; - } - catch (Exception e) - { - //protects socket receive thread - NetDebug.WriteError("[NM] SocketReceiveThread error: " + e ); - } - } - } - - /// - /// Start logic thread and listening on selected port - /// - /// bind to specific ipv4 address - /// bind to specific ipv6 address - /// port to listen - /// mode of library - public bool Start(IPAddress addressIPv4, IPAddress addressIPv6, int port, bool manualMode) - { - if (IsRunning && NotConnected == false) - return false; - - NotConnected = false; - _manualMode = manualMode; - UseNativeSockets = UseNativeSockets && NativeSocket.IsSupported; - _udpSocketv4 = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); - if (!BindSocket(_udpSocketv4, new IPEndPoint(addressIPv4, port))) - return false; - - LocalPort = ((IPEndPoint) _udpSocketv4.LocalEndPoint).Port; - -#if UNITY_2018_3_OR_NEWER - if (_pausedSocketFix == null) - _pausedSocketFix = new PausedSocketFix(this, addressIPv4, addressIPv6, port, manualMode); -#endif - - IsRunning = true; - if (_manualMode) - { - _bufferEndPointv4 = new IPEndPoint(IPAddress.Any, 0); - } - - //Check IPv6 support - if (IPv6Support && IPv6Enabled) - { - _udpSocketv6 = new Socket(AddressFamily.InterNetworkV6, SocketType.Dgram, ProtocolType.Udp); - //Use one port for two sockets - if (BindSocket(_udpSocketv6, new IPEndPoint(addressIPv6, LocalPort))) - { - if (_manualMode) - { - _bufferEndPointv6 = new IPEndPoint(IPAddress.IPv6Any, 0); - } - } - else - { - _udpSocketv6 = null; - } - } - - if (!manualMode) - { - ThreadStart ts = ReceiveLogic; - if (UseNativeSockets) - ts = NativeReceiveLogic; - _receiveThread = new Thread(ts) - { - Name = $"ReceiveThread({LocalPort})", - IsBackground = true - }; - _receiveThread.Start(); - if (_logicThread == null) - { - _logicThread = new Thread(UpdateLogic) { Name = "LogicThread", IsBackground = true }; - _logicThread.Start(); - } - } - - return true; - } - - private bool BindSocket(Socket socket, IPEndPoint ep) - { - //Setup socket - socket.ReceiveTimeout = 500; - socket.SendTimeout = 500; - socket.ReceiveBufferSize = NetConstants.SocketBufferSize; - socket.SendBufferSize = NetConstants.SocketBufferSize; - socket.Blocking = true; - - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - try - { - socket.IOControl(SioUdpConnreset, new byte[] {0}, null); - } - catch - { - //ignored - } - } - - try - { - socket.ExclusiveAddressUse = !ReuseAddress; - socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, ReuseAddress); - } - catch - { - //Unity with IL2CPP throws an exception here, it doesn't matter in most cases so just ignore it - } - if (ep.AddressFamily == AddressFamily.InterNetwork) - { - Ttl = NetConstants.SocketTTL; - - try { socket.EnableBroadcast = true; } - catch (SocketException e) - { - NetDebug.WriteError($"[B]Broadcast error: {e.SocketErrorCode}"); - } - - if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - { - try { socket.DontFragment = true; } - catch (SocketException e) - { - NetDebug.WriteError($"[B]DontFragment error: {e.SocketErrorCode}"); - } - } - } - //Bind - try - { - socket.Bind(ep); - NetDebug.Write(NetLogLevel.Trace, $"[B]Successfully binded to port: {((IPEndPoint)socket.LocalEndPoint).Port}, AF: {socket.AddressFamily}"); - - //join multicast - if (ep.AddressFamily == AddressFamily.InterNetworkV6) - { - try - { -#if !UNITY_2018_3_OR_NEWER - socket.SetSocketOption( - SocketOptionLevel.IPv6, - SocketOptionName.AddMembership, - new IPv6MulticastOption(MulticastAddressV6)); -#endif - } - catch (Exception) - { - // Unity3d throws exception - ignored - } - } - } - catch (SocketException bindException) - { - switch (bindException.SocketErrorCode) - { - //IPv6 bind fix - case SocketError.AddressAlreadyInUse: - if (socket.AddressFamily == AddressFamily.InterNetworkV6) - { - try - { - //Set IPv6Only - socket.DualMode = false; - socket.Bind(ep); - } - catch (SocketException ex) - { - //because its fixed in 2018_3 - NetDebug.WriteError($"[B]Bind exception: {ex}, errorCode: {ex.SocketErrorCode}"); - return false; - } - return true; - } - break; - //hack for iOS (Unity3D) - case SocketError.AddressFamilyNotSupported: - return true; - } - NetDebug.WriteError($"[B]Bind exception: {bindException}, errorCode: {bindException.SocketErrorCode}"); - return false; - } - return true; - } - - internal int SendRawAndRecycle(NetPacket packet, IPEndPoint remoteEndPoint) - { - int result = SendRaw(packet.RawData, 0, packet.Size, remoteEndPoint); - PoolRecycle(packet); - return result; - } - - internal int SendRaw(NetPacket packet, IPEndPoint remoteEndPoint) - { - return SendRaw(packet.RawData, 0, packet.Size, remoteEndPoint); - } - - internal int SendRaw(byte[] message, int start, int length, IPEndPoint remoteEndPoint) - { - if (!IsRunning) - return 0; - - NetPacket expandedPacket = null; - if (_extraPacketLayer != null) - { - expandedPacket = PoolGetPacket(length + _extraPacketLayer.ExtraPacketSizeForLayer); - Buffer.BlockCopy(message, start, expandedPacket.RawData, 0, length); - start = 0; - _extraPacketLayer.ProcessOutBoundPacket(ref remoteEndPoint, ref expandedPacket.RawData, ref start, ref length); - message = expandedPacket.RawData; - } - - var socket = _udpSocketv4; - if (remoteEndPoint.AddressFamily == AddressFamily.InterNetworkV6 && IPv6Support) - { - socket = _udpSocketv6; - if (socket == null) - return 0; - } - - int result; - try - { - if (UseNativeSockets) - { - byte[] socketAddress; - - if (remoteEndPoint is NativeEndPoint nep) - { - socketAddress = nep.NativeAddress; - } - else //Convert endpoint to raw - { - if (_endPointBuffer == null) - _endPointBuffer = new byte[NativeSocket.IPv6AddrSize]; - socketAddress = _endPointBuffer; - - bool ipv4 = remoteEndPoint.AddressFamily == AddressFamily.InterNetwork; - short addressFamily = NativeSocket.GetNativeAddressFamily(remoteEndPoint); - - socketAddress[0] = (byte) (addressFamily); - socketAddress[1] = (byte) (addressFamily >> 8); - socketAddress[2] = (byte) (remoteEndPoint.Port >> 8); - socketAddress[3] = (byte) (remoteEndPoint.Port); - - if (ipv4) - { -#pragma warning disable 618 - long addr = remoteEndPoint.Address.Address; -#pragma warning restore 618 - socketAddress[4] = (byte) (addr); - socketAddress[5] = (byte) (addr >> 8); - socketAddress[6] = (byte) (addr >> 16); - socketAddress[7] = (byte) (addr >> 24); - } - else - { -#if NETCOREAPP || NETSTANDARD2_1 || NETSTANDARD2_1_OR_GREATER - remoteEndPoint.Address.TryWriteBytes(new Span(socketAddress, 8, 16), out _); -#else - byte[] addrBytes = remoteEndPoint.Address.GetAddressBytes(); - Buffer.BlockCopy(addrBytes, 0, socketAddress, 8, 16); -#endif - } - } - -#if LITENETLIB_UNSAFE - unsafe - { - fixed (byte* dataWithOffset = &message[start]) - { - result = - NativeSocket.SendTo(socket.Handle, dataWithOffset, length, socketAddress, socketAddress.Length); - } - } -#else - if (start > 0) - { - if (_sendToBuffer == null) - _sendToBuffer = new byte[NetConstants.MaxPacketSize]; - Buffer.BlockCopy(message, start, _sendToBuffer, 0, length); - message = _sendToBuffer; - } - - result = NativeSocket.SendTo(socket.Handle, message, length, socketAddress, socketAddress.Length); -#endif - if (result == -1) - throw NativeSocket.GetSocketException(); - } - else - { - result = socket.SendTo(message, start, length, SocketFlags.None, remoteEndPoint); - } - //NetDebug.WriteForce("[S]Send packet to {0}, result: {1}", remoteEndPoint, result); - } - catch (SocketException ex) - { - switch (ex.SocketErrorCode) - { - case SocketError.NoBufferSpaceAvailable: - case SocketError.Interrupted: - return 0; - case SocketError.MessageSize: - NetDebug.Write(NetLogLevel.Trace, $"[SRD] 10040, datalen: {length}"); - return 0; - - case SocketError.HostUnreachable: - case SocketError.NetworkUnreachable: - if (DisconnectOnUnreachable && TryGetPeer(remoteEndPoint, out var fromPeer)) - { - DisconnectPeerForce( - fromPeer, - ex.SocketErrorCode == SocketError.HostUnreachable - ? DisconnectReason.HostUnreachable - : DisconnectReason.NetworkUnreachable, - ex.SocketErrorCode, - null); - } - - CreateEvent(NetEvent.EType.Error, remoteEndPoint: remoteEndPoint, errorCode: ex.SocketErrorCode); - return -1; - - case SocketError.Shutdown: - CreateEvent(NetEvent.EType.Error, remoteEndPoint: remoteEndPoint, errorCode: ex.SocketErrorCode); - return -1; - - default: - NetDebug.WriteError($"[S] {ex}"); - return -1; - } - } - catch (Exception ex) - { - NetDebug.WriteError($"[S] {ex}"); - return 0; - } - finally - { - if (expandedPacket != null) - { - PoolRecycle(expandedPacket); - } - } - - if (result <= 0) - return 0; - - if (EnableStatistics) - { - Statistics.IncrementPacketsSent(); - Statistics.AddBytesSent(length); - } - - return result; - } - - public bool SendBroadcast(NetDataWriter writer, int port) - { - return SendBroadcast(writer.Data, 0, writer.Length, port); - } - - public bool SendBroadcast(byte[] data, int port) - { - return SendBroadcast(data, 0, data.Length, port); - } - - public bool SendBroadcast(byte[] data, int start, int length, int port) - { - if (!IsRunning) - return false; - - NetPacket packet; - if (_extraPacketLayer != null) - { - var headerSize = NetPacket.GetHeaderSize(PacketProperty.Broadcast); - packet = PoolGetPacket(headerSize + length + _extraPacketLayer.ExtraPacketSizeForLayer); - packet.Property = PacketProperty.Broadcast; - Buffer.BlockCopy(data, start, packet.RawData, headerSize, length); - var checksumComputeStart = 0; - int preCrcLength = length + headerSize; - IPEndPoint emptyEp = null; - _extraPacketLayer.ProcessOutBoundPacket(ref emptyEp, ref packet.RawData, ref checksumComputeStart, ref preCrcLength); - } - else - { - packet = PoolGetWithData(PacketProperty.Broadcast, data, start, length); - } - - bool broadcastSuccess = false; - bool multicastSuccess = false; - try - { - broadcastSuccess = _udpSocketv4.SendTo( - packet.RawData, - 0, - packet.Size, - SocketFlags.None, - new IPEndPoint(IPAddress.Broadcast, port)) > 0; - - if (_udpSocketv6 != null) - { - multicastSuccess = _udpSocketv6.SendTo( - packet.RawData, - 0, - packet.Size, - SocketFlags.None, - new IPEndPoint(MulticastAddressV6, port)) > 0; - } - } - catch (Exception ex) - { - NetDebug.WriteError($"[S][MCAST] {ex}"); - return broadcastSuccess; - } - finally - { - PoolRecycle(packet); - } - - return broadcastSuccess || multicastSuccess; - } - - private void CloseSocket() - { - IsRunning = false; - _udpSocketv4?.Close(); - _udpSocketv6?.Close(); - _udpSocketv4 = null; - _udpSocketv6 = null; - if (_receiveThread != null && _receiveThread != Thread.CurrentThread) - _receiveThread.Join(); - _receiveThread = null; - } - } -} diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetManager.Socket.cs.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetManager.Socket.cs.meta deleted file mode 100644 index ee9309f5..00000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetManager.Socket.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: d190de687c3f4c86fba8fa624e0e5a2f -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetManager.cs b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetManager.cs deleted file mode 100644 index f4599b48..00000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetManager.cs +++ /dev/null @@ -1,1818 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Diagnostics; -using System.Net; -using System.Net.Sockets; -using System.Threading; -using LiteNetLib.Layers; -using LiteNetLib.Utils; - -namespace LiteNetLib -{ - public sealed class NetPacketReader : NetDataReader - { - private NetPacket _packet; - private readonly NetManager _manager; - private readonly NetEvent _evt; - - internal NetPacketReader(NetManager manager, NetEvent evt) - { - _manager = manager; - _evt = evt; - } - - internal void SetSource(NetPacket packet, int headerSize) - { - if (packet == null) - return; - _packet = packet; - SetSource(packet.RawData, headerSize, packet.Size); - } - - internal void RecycleInternal() - { - Clear(); - if (_packet != null) - _manager.PoolRecycle(_packet); - _packet = null; - _manager.RecycleEvent(_evt); - } - - public void Recycle() - { - if (_manager.AutoRecycle) - return; - RecycleInternal(); - } - } - - internal sealed class NetEvent - { - public NetEvent Next; - - public enum EType - { - Connect, - Disconnect, - Receive, - ReceiveUnconnected, - Error, - ConnectionLatencyUpdated, - Broadcast, - ConnectionRequest, - MessageDelivered, - PeerAddressChanged - } - public EType Type; - - public NetPeer Peer; - public IPEndPoint RemoteEndPoint; - public object UserData; - public int Latency; - public SocketError ErrorCode; - public DisconnectReason DisconnectReason; - public ConnectionRequest ConnectionRequest; - public DeliveryMethod DeliveryMethod; - public byte ChannelNumber; - public readonly NetPacketReader DataReader; - - public NetEvent(NetManager manager) - { - DataReader = new NetPacketReader(manager, this); - } - } - - /// - /// Main class for all network operations. Can be used as client and/or server. - /// - public partial class NetManager : IEnumerable - { - private class IPEndPointComparer : IEqualityComparer - { - public bool Equals(IPEndPoint x, IPEndPoint y) - { - return x.Address.Equals(y.Address) && x.Port == y.Port; - } - - public int GetHashCode(IPEndPoint obj) - { - return obj.GetHashCode(); - } - } - - public struct NetPeerEnumerator : IEnumerator - { - private readonly NetPeer _initialPeer; - private NetPeer _p; - - public NetPeerEnumerator(NetPeer p) - { - _initialPeer = p; - _p = null; - } - - public void Dispose() - { - - } - - public bool MoveNext() - { - _p = _p == null ? _initialPeer : _p.NextPeer; - return _p != null; - } - - public void Reset() - { - throw new NotSupportedException(); - } - - public NetPeer Current => _p; - object IEnumerator.Current => _p; - } - -#if LITENETLIB_DEBUGGING - private struct IncomingData - { - public NetPacket Data; - public IPEndPoint EndPoint; - public DateTime TimeWhenGet; - } - private readonly List _pingSimulationList = new List(); - private readonly Random _randomGenerator = new Random(); - private const int MinLatencyThreshold = 5; -#endif - - private Thread _logicThread; - private bool _manualMode; - private readonly AutoResetEvent _updateTriggerEvent = new AutoResetEvent(true); - - private NetEvent _pendingEventHead; - private NetEvent _pendingEventTail; - - private NetEvent _netEventPoolHead; - private readonly INetEventListener _netEventListener; - private readonly IDeliveryEventListener _deliveryEventListener; - private readonly INtpEventListener _ntpEventListener; - private readonly IPeerAddressChangedListener _peerAddressChangedListener; - - private readonly Dictionary _peersDict = new Dictionary(new IPEndPointComparer()); - private readonly Dictionary _requestsDict = new Dictionary(new IPEndPointComparer()); - private readonly Dictionary _ntpRequests = new Dictionary(new IPEndPointComparer()); - private readonly ReaderWriterLockSlim _peersLock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion); - private volatile NetPeer _headPeer; - private int _connectedPeersCount; - private readonly List _connectedPeerListCache = new List(); - private NetPeer[] _peersArray = new NetPeer[32]; - private readonly PacketLayerBase _extraPacketLayer; - private int _lastPeerId; - private ConcurrentQueue _peerIds = new ConcurrentQueue(); - private byte _channelsCount = 1; - private readonly object _eventLock = new object(); - - //config section - /// - /// Enable messages receiving without connection. (with SendUnconnectedMessage method) - /// - public bool UnconnectedMessagesEnabled = false; - - /// - /// Enable nat punch messages - /// - public bool NatPunchEnabled = false; - - /// - /// Library logic update and send period in milliseconds - /// Lowest values in Windows doesn't change much because of Thread.Sleep precision - /// To more frequent sends (or sends tied to your game logic) use - /// - public int UpdateTime = 15; - - /// - /// Interval for latency detection and checking connection (in milliseconds) - /// - public int PingInterval = 1000; - - /// - /// If NetManager doesn't receive any packet from remote peer during this time (in milliseconds) then connection will be closed - /// (including library internal keepalive packets) - /// - public int DisconnectTimeout = 5000; - - /// - /// Simulate packet loss by dropping random amount of packets. (Works only in DEBUG mode) - /// - public bool SimulatePacketLoss = false; - - /// - /// Simulate latency by holding packets for random time. (Works only in DEBUG mode) - /// - public bool SimulateLatency = false; - - /// - /// Chance of packet loss when simulation enabled. value in percents (1 - 100). - /// - public int SimulationPacketLossChance = 10; - - /// - /// Minimum simulated latency (in milliseconds) - /// - public int SimulationMinLatency = 30; - - /// - /// Maximum simulated latency (in milliseconds) - /// - public int SimulationMaxLatency = 100; - - /// - /// Events automatically will be called without PollEvents method from another thread - /// - public bool UnsyncedEvents = false; - - /// - /// If true - receive event will be called from "receive" thread immediately otherwise on PollEvents call - /// - public bool UnsyncedReceiveEvent = false; - - /// - /// If true - delivery event will be called from "receive" thread immediately otherwise on PollEvents call - /// - public bool UnsyncedDeliveryEvent = false; - - /// - /// Allows receive broadcast packets - /// - public bool BroadcastReceiveEnabled = false; - - /// - /// Delay between initial connection attempts (in milliseconds) - /// - public int ReconnectDelay = 500; - - /// - /// Maximum connection attempts before client stops and call disconnect event. - /// - public int MaxConnectAttempts = 10; - - /// - /// Enables socket option "ReuseAddress" for specific purposes - /// - public bool ReuseAddress = false; - - /// - /// Statistics of all connections - /// - public readonly NetStatistics Statistics = new NetStatistics(); - - /// - /// Toggles the collection of network statistics for the instance and all known peers - /// - public bool EnableStatistics = false; - - /// - /// NatPunchModule for NAT hole punching operations - /// - public readonly NatPunchModule NatPunchModule; - - /// - /// Returns true if socket listening and update thread is running - /// - public bool IsRunning { get; private set; } - - /// - /// Local EndPoint (host and port) - /// - public int LocalPort { get; private set; } - - /// - /// Automatically recycle NetPacketReader after OnReceive event - /// - public bool AutoRecycle; - - /// - /// IPv6 support - /// - public bool IPv6Enabled = true; - - /// - /// Override MTU for all new peers registered in this NetManager, will ignores MTU Discovery! - /// - public int MtuOverride = 0; - - /// - /// Sets initial MTU to lowest possible value according to RFC1191 (576 bytes) - /// - public bool UseSafeMtu = false; - - /// - /// First peer. Useful for Client mode - /// - public NetPeer FirstPeer => _headPeer; - - /// - /// Experimental feature mostly for servers. Only for Windows/Linux - /// use direct socket calls for send/receive to drastically increase speed and reduce GC pressure - /// - public bool UseNativeSockets = false; - - /// - /// Disconnect peers if HostUnreachable or NetworkUnreachable spawned (old behaviour 0.9.x was true) - /// - public bool DisconnectOnUnreachable = false; - - /// - /// Allows peer change it's ip (lte to wifi, wifi to lte, etc). Use only on server - /// - public bool AllowPeerAddressChange = false; - - /// - /// QoS channel count per message type (value must be between 1 and 64 channels) - /// - public byte ChannelsCount - { - get => _channelsCount; - set - { - if (value < 1 || value > 64) - throw new ArgumentException("Channels count must be between 1 and 64"); - _channelsCount = value; - } - } - - /// - /// Returns connected peers list (with internal cached list) - /// - public List ConnectedPeerList - { - get - { - GetPeersNonAlloc(_connectedPeerListCache, ConnectionState.Connected); - return _connectedPeerListCache; - } - } - - /// - /// Gets peer by peer id - /// - /// id of peer - /// Peer if peer with id exist, otherwise null - public NetPeer GetPeerById(int id) - { - if (id >= 0 && id < _peersArray.Length) - { - return _peersArray[id]; - } - - return null; - } - - /// - /// Gets peer by peer id - /// - /// id of peer - /// resulting peer - /// True if peer with id exist, otherwise false - public bool TryGetPeerById(int id, out NetPeer peer) - { - peer = GetPeerById(id); - - return peer != null; - } - - /// - /// Returns connected peers count - /// - public int ConnectedPeersCount => Interlocked.CompareExchange(ref _connectedPeersCount,0,0); - - public int ExtraPacketSizeForLayer => _extraPacketLayer?.ExtraPacketSizeForLayer ?? 0; - - private bool TryGetPeer(IPEndPoint endPoint, out NetPeer peer) - { - _peersLock.EnterReadLock(); - bool result = _peersDict.TryGetValue(endPoint, out peer); - _peersLock.ExitReadLock(); - return result; - } - - private void AddPeer(NetPeer peer) - { - _peersLock.EnterWriteLock(); - if (_headPeer != null) - { - peer.NextPeer = _headPeer; - _headPeer.PrevPeer = peer; - } - _headPeer = peer; - _peersDict.Add(peer.EndPoint, peer); - if (peer.Id >= _peersArray.Length) - { - int newSize = _peersArray.Length * 2; - while (peer.Id >= newSize) - newSize *= 2; - Array.Resize(ref _peersArray, newSize); - } - _peersArray[peer.Id] = peer; - RegisterEndPoint(peer.EndPoint); - _peersLock.ExitWriteLock(); - } - - private void RemovePeer(NetPeer peer) - { - _peersLock.EnterWriteLock(); - RemovePeerInternal(peer); - _peersLock.ExitWriteLock(); - } - - private void RemovePeerInternal(NetPeer peer) - { - if (!_peersDict.Remove(peer.EndPoint)) - return; - if (peer == _headPeer) - _headPeer = peer.NextPeer; - - if (peer.PrevPeer != null) - peer.PrevPeer.NextPeer = peer.NextPeer; - if (peer.NextPeer != null) - peer.NextPeer.PrevPeer = peer.PrevPeer; - peer.PrevPeer = null; - - _peersArray[peer.Id] = null; - _peerIds.Enqueue(peer.Id); - UnregisterEndPoint(peer.EndPoint); - } - - /// - /// NetManager constructor - /// - /// Network events listener (also can implement IDeliveryEventListener) - /// Extra processing of packages, like CRC checksum or encryption. All connected NetManagers must have same layer. - public NetManager(INetEventListener listener, PacketLayerBase extraPacketLayer = null) - { - _netEventListener = listener; - _deliveryEventListener = listener as IDeliveryEventListener; - _ntpEventListener = listener as INtpEventListener; - _peerAddressChangedListener = listener as IPeerAddressChangedListener; - NatPunchModule = new NatPunchModule(this); - _extraPacketLayer = extraPacketLayer; - } - - internal void ConnectionLatencyUpdated(NetPeer fromPeer, int latency) - { - CreateEvent(NetEvent.EType.ConnectionLatencyUpdated, fromPeer, latency: latency); - } - - internal void MessageDelivered(NetPeer fromPeer, object userData) - { - if(_deliveryEventListener != null) - CreateEvent(NetEvent.EType.MessageDelivered, fromPeer, userData: userData); - } - - internal void DisconnectPeerForce(NetPeer peer, - DisconnectReason reason, - SocketError socketErrorCode, - NetPacket eventData) - { - DisconnectPeer(peer, reason, socketErrorCode, true, null, 0, 0, eventData); - } - - private void DisconnectPeer( - NetPeer peer, - DisconnectReason reason, - SocketError socketErrorCode, - bool force, - byte[] data, - int start, - int count, - NetPacket eventData) - { - var shutdownResult = peer.Shutdown(data, start, count, force); - if (shutdownResult == ShutdownResult.None) - return; - if(shutdownResult == ShutdownResult.WasConnected) - Interlocked.Decrement(ref _connectedPeersCount); - CreateEvent( - NetEvent.EType.Disconnect, - peer, - errorCode: socketErrorCode, - disconnectReason: reason, - readerSource: eventData); - } - - private void CreateEvent( - NetEvent.EType type, - NetPeer peer = null, - IPEndPoint remoteEndPoint = null, - SocketError errorCode = 0, - int latency = 0, - DisconnectReason disconnectReason = DisconnectReason.ConnectionFailed, - ConnectionRequest connectionRequest = null, - DeliveryMethod deliveryMethod = DeliveryMethod.Unreliable, - byte channelNumber = 0, - NetPacket readerSource = null, - object userData = null) - { - NetEvent evt; - bool unsyncEvent = UnsyncedEvents; - - if (type == NetEvent.EType.Connect) - Interlocked.Increment(ref _connectedPeersCount); - else if (type == NetEvent.EType.MessageDelivered) - unsyncEvent = UnsyncedDeliveryEvent; - - lock(_eventLock) - { - evt = _netEventPoolHead; - if (evt == null) - evt = new NetEvent(this); - else - _netEventPoolHead = evt.Next; - } - - evt.Next = null; - evt.Type = type; - evt.DataReader.SetSource(readerSource, readerSource?.GetHeaderSize() ?? 0); - evt.Peer = peer; - evt.RemoteEndPoint = remoteEndPoint; - evt.Latency = latency; - evt.ErrorCode = errorCode; - evt.DisconnectReason = disconnectReason; - evt.ConnectionRequest = connectionRequest; - evt.DeliveryMethod = deliveryMethod; - evt.ChannelNumber = channelNumber; - evt.UserData = userData; - - if (unsyncEvent || _manualMode) - { - ProcessEvent(evt); - } - else - { - lock (_eventLock) - { - if (_pendingEventTail == null) - _pendingEventHead = evt; - else - _pendingEventTail.Next = evt; - _pendingEventTail = evt; - } - } - } - - private void ProcessEvent(NetEvent evt) - { - NetDebug.Write("[NM] Processing event: " + evt.Type); - bool emptyData = evt.DataReader.IsNull; - switch (evt.Type) - { - case NetEvent.EType.Connect: - _netEventListener.OnPeerConnected(evt.Peer); - break; - case NetEvent.EType.Disconnect: - var info = new DisconnectInfo - { - Reason = evt.DisconnectReason, - AdditionalData = evt.DataReader, - SocketErrorCode = evt.ErrorCode - }; - _netEventListener.OnPeerDisconnected(evt.Peer, info); - break; - case NetEvent.EType.Receive: - _netEventListener.OnNetworkReceive(evt.Peer, evt.DataReader, evt.ChannelNumber, evt.DeliveryMethod); - break; - case NetEvent.EType.ReceiveUnconnected: - _netEventListener.OnNetworkReceiveUnconnected(evt.RemoteEndPoint, evt.DataReader, UnconnectedMessageType.BasicMessage); - break; - case NetEvent.EType.Broadcast: - _netEventListener.OnNetworkReceiveUnconnected(evt.RemoteEndPoint, evt.DataReader, UnconnectedMessageType.Broadcast); - break; - case NetEvent.EType.Error: - _netEventListener.OnNetworkError(evt.RemoteEndPoint, evt.ErrorCode); - break; - case NetEvent.EType.ConnectionLatencyUpdated: - _netEventListener.OnNetworkLatencyUpdate(evt.Peer, evt.Latency); - break; - case NetEvent.EType.ConnectionRequest: - _netEventListener.OnConnectionRequest(evt.ConnectionRequest); - break; - case NetEvent.EType.MessageDelivered: - _deliveryEventListener.OnMessageDelivered(evt.Peer, evt.UserData); - break; - case NetEvent.EType.PeerAddressChanged: - _peersLock.EnterUpgradeableReadLock(); - IPEndPoint previousAddress = null; - if (_peersDict.ContainsKey(evt.Peer.EndPoint)) - { - _peersLock.EnterWriteLock(); - _peersDict.Remove(evt.Peer.EndPoint); - previousAddress = evt.Peer.EndPoint; - evt.Peer.FinishEndPointChange(evt.RemoteEndPoint); - _peersDict.Add(evt.Peer.EndPoint, evt.Peer); - _peersLock.ExitWriteLock(); - } - _peersLock.ExitUpgradeableReadLock(); - if(previousAddress != null && _peerAddressChangedListener != null) - _peerAddressChangedListener.OnPeerAddressChanged(evt.Peer, previousAddress); - break; - } - //Recycle if not message - if (emptyData) - RecycleEvent(evt); - else if (AutoRecycle) - evt.DataReader.RecycleInternal(); - } - - internal void RecycleEvent(NetEvent evt) - { - evt.Peer = null; - evt.ErrorCode = 0; - evt.RemoteEndPoint = null; - evt.ConnectionRequest = null; - lock(_eventLock) - { - evt.Next = _netEventPoolHead; - _netEventPoolHead = evt; - } - } - - //Update function - private void UpdateLogic() - { - var peersToRemove = new List(); - var stopwatch = new Stopwatch(); - stopwatch.Start(); - - while (IsRunning) - { - try - { - ProcessDelayedPackets(); - int elapsed = (int)stopwatch.ElapsedMilliseconds; - elapsed = elapsed <= 0 ? 1 : elapsed; - stopwatch.Restart(); - - for (var netPeer = _headPeer; netPeer != null; netPeer = netPeer.NextPeer) - { - if (netPeer.ConnectionState == ConnectionState.Disconnected && - netPeer.TimeSinceLastPacket > DisconnectTimeout) - { - peersToRemove.Add(netPeer); - } - else - { - netPeer.Update(elapsed); - } - } - - if (peersToRemove.Count > 0) - { - _peersLock.EnterWriteLock(); - for (int i = 0; i < peersToRemove.Count; i++) - RemovePeerInternal(peersToRemove[i]); - _peersLock.ExitWriteLock(); - peersToRemove.Clear(); - } - - ProcessNtpRequests(elapsed); - - int sleepTime = UpdateTime - (int)stopwatch.ElapsedMilliseconds; - if (sleepTime > 0) - _updateTriggerEvent.WaitOne(sleepTime); - } - catch (ThreadAbortException) - { - return; - } - catch (Exception e) - { - NetDebug.WriteError("[NM] LogicThread error: " + e); - } - } - stopwatch.Stop(); - } - - [Conditional("LITENETLIB_DEBUGGING")] - private void ProcessDelayedPackets() - { -#if LITENETLIB_DEBUGGING - if (!SimulateLatency && _pingSimulationList.Count == 0) - return; - - var time = DateTime.UtcNow; - lock (_pingSimulationList) - { - for (int i = 0; i < _pingSimulationList.Count; i++) - { - var incomingData = _pingSimulationList[i]; - if (incomingData.TimeWhenGet <= time) - { - DebugMessageReceived(incomingData.Data, incomingData.EndPoint); - _pingSimulationList.RemoveAt(i); - i--; - } - } - } -#endif - } - - private void ProcessNtpRequests(int elapsedMilliseconds) - { - List requestsToRemove = null; - foreach (var ntpRequest in _ntpRequests) - { - ntpRequest.Value.Send(_udpSocketv4, elapsedMilliseconds); - if(ntpRequest.Value.NeedToKill) - { - if (requestsToRemove == null) - requestsToRemove = new List(); - requestsToRemove.Add(ntpRequest.Key); - } - } - - if (requestsToRemove != null) - { - foreach (var ipEndPoint in requestsToRemove) - { - _ntpRequests.Remove(ipEndPoint); - } - } - } - - /// - /// Update and send logic. Use this only when NetManager started in manual mode - /// - /// elapsed milliseconds since last update call - public void ManualUpdate(int elapsedMilliseconds) - { - if (!_manualMode) - return; - - for (var netPeer = _headPeer; netPeer != null; netPeer = netPeer.NextPeer) - { - if (netPeer.ConnectionState == ConnectionState.Disconnected && netPeer.TimeSinceLastPacket > DisconnectTimeout) - { - RemovePeerInternal(netPeer); - } - else - { - netPeer.Update(elapsedMilliseconds); - } - } - ProcessNtpRequests(elapsedMilliseconds); - } - - internal NetPeer OnConnectionSolved(ConnectionRequest request, byte[] rejectData, int start, int length) - { - NetPeer netPeer = null; - - if (request.Result == ConnectionRequestResult.RejectForce) - { - NetDebug.Write(NetLogLevel.Trace, "[NM] Peer connect reject force."); - if (rejectData != null && length > 0) - { - var shutdownPacket = PoolGetWithProperty(PacketProperty.Disconnect, length); - shutdownPacket.ConnectionNumber = request.InternalPacket.ConnectionNumber; - FastBitConverter.GetBytes(shutdownPacket.RawData, 1, request.InternalPacket.ConnectionTime); - if (shutdownPacket.Size >= NetConstants.PossibleMtu[0]) - NetDebug.WriteError("[Peer] Disconnect additional data size more than MTU!"); - else - Buffer.BlockCopy(rejectData, start, shutdownPacket.RawData, 9, length); - SendRawAndRecycle(shutdownPacket, request.RemoteEndPoint); - } - } - else - { - _peersLock.EnterUpgradeableReadLock(); - if (_peersDict.TryGetValue(request.RemoteEndPoint, out netPeer)) - { - //already have peer - _peersLock.ExitUpgradeableReadLock(); - } - else if (request.Result == ConnectionRequestResult.Reject) - { - netPeer = new NetPeer(this, request.RemoteEndPoint, GetNextPeerId()); - netPeer.Reject(request.InternalPacket, rejectData, start, length); - AddPeer(netPeer); - _peersLock.ExitUpgradeableReadLock(); - NetDebug.Write(NetLogLevel.Trace, "[NM] Peer connect reject."); - } - else //Accept - { - netPeer = new NetPeer(this, request, GetNextPeerId()); - AddPeer(netPeer); - _peersLock.ExitUpgradeableReadLock(); - CreateEvent(NetEvent.EType.Connect, netPeer); - NetDebug.Write(NetLogLevel.Trace, $"[NM] Received peer connection Id: {netPeer.ConnectTime}, EP: {netPeer.EndPoint}"); - } - } - - lock(_requestsDict) - _requestsDict.Remove(request.RemoteEndPoint); - - return netPeer; - } - - private int GetNextPeerId() - { - return _peerIds.TryDequeue(out int id) ? id : _lastPeerId++; - } - - private void ProcessConnectRequest( - IPEndPoint remoteEndPoint, - NetPeer netPeer, - NetConnectRequestPacket connRequest) - { - //if we have peer - if (netPeer != null) - { - var processResult = netPeer.ProcessConnectRequest(connRequest); - NetDebug.Write($"ConnectRequest LastId: {netPeer.ConnectTime}, NewId: {connRequest.ConnectionTime}, EP: {remoteEndPoint}, Result: {processResult}"); - - switch (processResult) - { - case ConnectRequestResult.Reconnection: - DisconnectPeerForce(netPeer, DisconnectReason.Reconnect, 0, null); - RemovePeer(netPeer); - //go to new connection - break; - case ConnectRequestResult.NewConnection: - RemovePeer(netPeer); - //go to new connection - break; - case ConnectRequestResult.P2PLose: - DisconnectPeerForce(netPeer, DisconnectReason.PeerToPeerConnection, 0, null); - RemovePeer(netPeer); - //go to new connection - break; - default: - //no operations needed - return; - } - //ConnectRequestResult.NewConnection - //Set next connection number - if(processResult != ConnectRequestResult.P2PLose) - connRequest.ConnectionNumber = (byte)((netPeer.ConnectionNum + 1) % NetConstants.MaxConnectionNumber); - //To reconnect peer - } - else - { - NetDebug.Write($"ConnectRequest Id: {connRequest.ConnectionTime}, EP: {remoteEndPoint}"); - } - - ConnectionRequest req; - lock (_requestsDict) - { - if (_requestsDict.TryGetValue(remoteEndPoint, out req)) - { - req.UpdateRequest(connRequest); - return; - } - req = new ConnectionRequest(remoteEndPoint, connRequest, this); - _requestsDict.Add(remoteEndPoint, req); - } - NetDebug.Write($"[NM] Creating request event: {connRequest.ConnectionTime}"); - CreateEvent(NetEvent.EType.ConnectionRequest, connectionRequest: req); - } - - private void OnMessageReceived(NetPacket packet, IPEndPoint remoteEndPoint) - { -#if LITENETLIB_DEBUGGING - if (SimulatePacketLoss && _randomGenerator.NextDouble() * 100 < SimulationPacketLossChance) - { - //drop packet - return; - } - if (SimulateLatency) - { - int latency = _randomGenerator.Next(SimulationMinLatency, SimulationMaxLatency); - if (latency > MinLatencyThreshold) - { - lock (_pingSimulationList) - { - _pingSimulationList.Add(new IncomingData - { - Data = packet, - EndPoint = remoteEndPoint, - TimeWhenGet = DateTime.UtcNow.AddMilliseconds(latency) - }); - } - //hold packet - return; - } - } - - //ProcessEvents - DebugMessageReceived(packet, remoteEndPoint); - } - - private void DebugMessageReceived(NetPacket packet, IPEndPoint remoteEndPoint) - { -#endif - var originalPacketSize = packet.Size; - if (EnableStatistics) - { - Statistics.IncrementPacketsReceived(); - Statistics.AddBytesReceived(originalPacketSize); - } - - if (_ntpRequests.Count > 0) - { - if (_ntpRequests.TryGetValue(remoteEndPoint, out var request)) - { - if (packet.Size < 48) - { - NetDebug.Write(NetLogLevel.Trace, $"NTP response too short: {packet.Size}"); - return; - } - - byte[] copiedData = new byte[packet.Size]; - Buffer.BlockCopy(packet.RawData, 0, copiedData, 0, packet.Size); - NtpPacket ntpPacket = NtpPacket.FromServerResponse(copiedData, DateTime.UtcNow); - try - { - ntpPacket.ValidateReply(); - } - catch (InvalidOperationException ex) - { - NetDebug.Write(NetLogLevel.Trace, $"NTP response error: {ex.Message}"); - ntpPacket = null; - } - - if (ntpPacket != null) - { - _ntpRequests.Remove(remoteEndPoint); - _ntpEventListener?.OnNtpResponse(ntpPacket); - } - return; - } - } - - if (_extraPacketLayer != null) - { - int start = 0; - _extraPacketLayer.ProcessInboundPacket(ref remoteEndPoint, ref packet.RawData, ref start, ref packet.Size); - if (packet.Size == 0) - return; - } - - if (!packet.Verify()) - { - NetDebug.WriteError("[NM] DataReceived: bad!"); - PoolRecycle(packet); - return; - } - - switch (packet.Property) - { - //special case connect request - case PacketProperty.ConnectRequest: - if (NetConnectRequestPacket.GetProtocolId(packet) != NetConstants.ProtocolId) - { - SendRawAndRecycle(PoolGetWithProperty(PacketProperty.InvalidProtocol), remoteEndPoint); - return; - } - break; - //unconnected messages - case PacketProperty.Broadcast: - if (!BroadcastReceiveEnabled) - return; - CreateEvent(NetEvent.EType.Broadcast, remoteEndPoint: remoteEndPoint, readerSource: packet); - return; - case PacketProperty.UnconnectedMessage: - if (!UnconnectedMessagesEnabled) - return; - CreateEvent(NetEvent.EType.ReceiveUnconnected, remoteEndPoint: remoteEndPoint, readerSource: packet); - return; - case PacketProperty.NatMessage: - if (NatPunchEnabled) - NatPunchModule.ProcessMessage(remoteEndPoint, packet); - return; - } - - //Check normal packets - _peersLock.EnterReadLock(); - bool peerFound = _peersDict.TryGetValue(remoteEndPoint, out var netPeer); - _peersLock.ExitReadLock(); - - if (peerFound && EnableStatistics) - { - netPeer.Statistics.IncrementPacketsReceived(); - netPeer.Statistics.AddBytesReceived(originalPacketSize); - } - - switch (packet.Property) - { - case PacketProperty.ConnectRequest: - var connRequest = NetConnectRequestPacket.FromData(packet); - if (connRequest != null) - ProcessConnectRequest(remoteEndPoint, netPeer, connRequest); - break; - case PacketProperty.PeerNotFound: - if (peerFound) //local - { - if (netPeer.ConnectionState != ConnectionState.Connected) - return; - if (packet.Size == 1) - { - //first reply - //send NetworkChanged packet - netPeer.ResetMtu(); - SendRaw(NetConnectAcceptPacket.MakeNetworkChanged(netPeer), remoteEndPoint); - NetDebug.Write($"PeerNotFound sending connection info: {remoteEndPoint}"); - } - else if (packet.Size == 2 && packet.RawData[1] == 1) - { - //second reply - DisconnectPeerForce(netPeer, DisconnectReason.PeerNotFound, 0, null); - } - } - else if (packet.Size > 1) //remote - { - //check if this is old peer - bool isOldPeer = false; - - if (AllowPeerAddressChange) - { - NetDebug.Write($"[NM] Looks like address change: {packet.Size}"); - var remoteData = NetConnectAcceptPacket.FromData(packet); - if (remoteData != null && - remoteData.PeerNetworkChanged && - remoteData.PeerId < _peersArray.Length) - { - _peersLock.EnterUpgradeableReadLock(); - var peer = _peersArray[remoteData.PeerId]; - if (peer != null && - peer.ConnectTime == remoteData.ConnectionTime && - peer.ConnectionNum == remoteData.ConnectionNumber) - { - if (peer.ConnectionState == ConnectionState.Connected) - { - peer.InitiateEndPointChange(); - CreateEvent(NetEvent.EType.PeerAddressChanged, peer, remoteEndPoint); - NetDebug.Write("[NM] PeerNotFound change address of remote peer"); - } - isOldPeer = true; - } - _peersLock.ExitUpgradeableReadLock(); - } - } - - PoolRecycle(packet); - - //else peer really not found - if (!isOldPeer) - { - var secondResponse = PoolGetWithProperty(PacketProperty.PeerNotFound, 1); - secondResponse.RawData[1] = 1; - SendRawAndRecycle(secondResponse, remoteEndPoint); - } - } - break; - case PacketProperty.InvalidProtocol: - if (peerFound && netPeer.ConnectionState == ConnectionState.Outgoing) - DisconnectPeerForce(netPeer, DisconnectReason.InvalidProtocol, 0, null); - break; - case PacketProperty.Disconnect: - if (peerFound) - { - var disconnectResult = netPeer.ProcessDisconnect(packet); - if (disconnectResult == DisconnectResult.None) - { - PoolRecycle(packet); - return; - } - DisconnectPeerForce( - netPeer, - disconnectResult == DisconnectResult.Disconnect - ? DisconnectReason.RemoteConnectionClose - : DisconnectReason.ConnectionRejected, - 0, packet); - } - else - { - PoolRecycle(packet); - } - //Send shutdown - SendRawAndRecycle(PoolGetWithProperty(PacketProperty.ShutdownOk), remoteEndPoint); - break; - case PacketProperty.ConnectAccept: - if (!peerFound) - return; - var connAccept = NetConnectAcceptPacket.FromData(packet); - if (connAccept != null && netPeer.ProcessConnectAccept(connAccept)) - CreateEvent(NetEvent.EType.Connect, netPeer); - break; - default: - if(peerFound) - netPeer.ProcessPacket(packet); - else - SendRawAndRecycle(PoolGetWithProperty(PacketProperty.PeerNotFound), remoteEndPoint); - break; - } - } - - internal void CreateReceiveEvent(NetPacket packet, DeliveryMethod method, byte channelNumber, int headerSize, NetPeer fromPeer) - { - NetEvent evt; - - if (UnsyncedEvents || UnsyncedReceiveEvent || _manualMode) - { - lock (_eventLock) - { - evt = _netEventPoolHead; - if (evt == null) - evt = new NetEvent(this); - else - _netEventPoolHead = evt.Next; - } - evt.Next = null; - evt.Type = NetEvent.EType.Receive; - evt.DataReader.SetSource(packet, headerSize); - evt.Peer = fromPeer; - evt.DeliveryMethod = method; - evt.ChannelNumber = channelNumber; - ProcessEvent(evt); - } - else - { - lock (_eventLock) - { - evt = _netEventPoolHead; - if (evt == null) - evt = new NetEvent(this); - else - _netEventPoolHead = evt.Next; - - evt.Next = null; - evt.Type = NetEvent.EType.Receive; - evt.DataReader.SetSource(packet, headerSize); - evt.Peer = fromPeer; - evt.DeliveryMethod = method; - evt.ChannelNumber = channelNumber; - - if (_pendingEventTail == null) - _pendingEventHead = evt; - else - _pendingEventTail.Next = evt; - _pendingEventTail = evt; - } - } - } - - /// - /// Send data to all connected peers (channel - 0) - /// - /// DataWriter with data - /// Send options (reliable, unreliable, etc.) - public void SendToAll(NetDataWriter writer, DeliveryMethod options) - { - SendToAll(writer.Data, 0, writer.Length, options); - } - - /// - /// Send data to all connected peers (channel - 0) - /// - /// Data - /// Send options (reliable, unreliable, etc.) - public void SendToAll(byte[] data, DeliveryMethod options) - { - SendToAll(data, 0, data.Length, options); - } - - /// - /// Send data to all connected peers (channel - 0) - /// - /// Data - /// Start of data - /// Length of data - /// Send options (reliable, unreliable, etc.) - public void SendToAll(byte[] data, int start, int length, DeliveryMethod options) - { - SendToAll(data, start, length, 0, options); - } - - /// - /// Send data to all connected peers - /// - /// DataWriter with data - /// Number of channel (from 0 to channelsCount - 1) - /// Send options (reliable, unreliable, etc.) - public void SendToAll(NetDataWriter writer, byte channelNumber, DeliveryMethod options) - { - SendToAll(writer.Data, 0, writer.Length, channelNumber, options); - } - - /// - /// Send data to all connected peers - /// - /// Data - /// Number of channel (from 0 to channelsCount - 1) - /// Send options (reliable, unreliable, etc.) - public void SendToAll(byte[] data, byte channelNumber, DeliveryMethod options) - { - SendToAll(data, 0, data.Length, channelNumber, options); - } - - /// - /// Send data to all connected peers - /// - /// Data - /// Start of data - /// Length of data - /// Number of channel (from 0 to channelsCount - 1) - /// Send options (reliable, unreliable, etc.) - public void SendToAll(byte[] data, int start, int length, byte channelNumber, DeliveryMethod options) - { - try - { - _peersLock.EnterReadLock(); - for (var netPeer = _headPeer; netPeer != null; netPeer = netPeer.NextPeer) - netPeer.Send(data, start, length, channelNumber, options); - } - finally - { - _peersLock.ExitReadLock(); - } - } - - /// - /// Send data to all connected peers (channel - 0) - /// - /// DataWriter with data - /// Send options (reliable, unreliable, etc.) - /// Excluded peer - public void SendToAll(NetDataWriter writer, DeliveryMethod options, NetPeer excludePeer) - { - SendToAll(writer.Data, 0, writer.Length, 0, options, excludePeer); - } - - /// - /// Send data to all connected peers (channel - 0) - /// - /// Data - /// Send options (reliable, unreliable, etc.) - /// Excluded peer - public void SendToAll(byte[] data, DeliveryMethod options, NetPeer excludePeer) - { - SendToAll(data, 0, data.Length, 0, options, excludePeer); - } - - /// - /// Send data to all connected peers (channel - 0) - /// - /// Data - /// Start of data - /// Length of data - /// Send options (reliable, unreliable, etc.) - /// Excluded peer - public void SendToAll(byte[] data, int start, int length, DeliveryMethod options, NetPeer excludePeer) - { - SendToAll(data, start, length, 0, options, excludePeer); - } - - /// - /// Send data to all connected peers - /// - /// DataWriter with data - /// Number of channel (from 0 to channelsCount - 1) - /// Send options (reliable, unreliable, etc.) - /// Excluded peer - public void SendToAll(NetDataWriter writer, byte channelNumber, DeliveryMethod options, NetPeer excludePeer) - { - SendToAll(writer.Data, 0, writer.Length, channelNumber, options, excludePeer); - } - - /// - /// Send data to all connected peers - /// - /// Data - /// Number of channel (from 0 to channelsCount - 1) - /// Send options (reliable, unreliable, etc.) - /// Excluded peer - public void SendToAll(byte[] data, byte channelNumber, DeliveryMethod options, NetPeer excludePeer) - { - SendToAll(data, 0, data.Length, channelNumber, options, excludePeer); - } - - - /// - /// Send data to all connected peers - /// - /// Data - /// Start of data - /// Length of data - /// Number of channel (from 0 to channelsCount - 1) - /// Send options (reliable, unreliable, etc.) - /// Excluded peer - public void SendToAll(byte[] data, int start, int length, byte channelNumber, DeliveryMethod options, NetPeer excludePeer) - { - try - { - _peersLock.EnterReadLock(); - for (var netPeer = _headPeer; netPeer != null; netPeer = netPeer.NextPeer) - { - if (netPeer != excludePeer) - netPeer.Send(data, start, length, channelNumber, options); - } - } - finally - { - _peersLock.ExitReadLock(); - } - } - - /// - /// Start logic thread and listening on available port - /// - public bool Start() - { - return Start(0); - } - - /// - /// Start logic thread and listening on selected port - /// - /// bind to specific ipv4 address - /// bind to specific ipv6 address - /// port to listen - public bool Start(IPAddress addressIPv4, IPAddress addressIPv6, int port) - { - return Start(addressIPv4, addressIPv6, port, false); - } - - /// - /// Start logic thread and listening on selected port - /// - /// bind to specific ipv4 address - /// bind to specific ipv6 address - /// port to listen - public bool Start(string addressIPv4, string addressIPv6, int port) - { - IPAddress ipv4 = NetUtils.ResolveAddress(addressIPv4); - IPAddress ipv6 = NetUtils.ResolveAddress(addressIPv6); - return Start(ipv4, ipv6, port); - } - - /// - /// Start logic thread and listening on selected port - /// - /// port to listen - public bool Start(int port) - { - return Start(IPAddress.Any, IPAddress.IPv6Any, port); - } - - /// - /// Start in manual mode and listening on selected port - /// In this mode you should use ManualReceive (without PollEvents) for receive packets - /// and ManualUpdate(...) for update and send packets - /// This mode useful mostly for single-threaded servers - /// - /// bind to specific ipv4 address - /// bind to specific ipv6 address - /// port to listen - public bool StartInManualMode(IPAddress addressIPv4, IPAddress addressIPv6, int port) - { - return Start(addressIPv4, addressIPv6, port, true); - } - - /// - /// Start in manual mode and listening on selected port - /// In this mode you should use ManualReceive (without PollEvents) for receive packets - /// and ManualUpdate(...) for update and send packets - /// This mode useful mostly for single-threaded servers - /// - /// bind to specific ipv4 address - /// bind to specific ipv6 address - /// port to listen - public bool StartInManualMode(string addressIPv4, string addressIPv6, int port) - { - IPAddress ipv4 = NetUtils.ResolveAddress(addressIPv4); - IPAddress ipv6 = NetUtils.ResolveAddress(addressIPv6); - return StartInManualMode(ipv4, ipv6, port); - } - - /// - /// Start in manual mode and listening on selected port - /// In this mode you should use ManualReceive (without PollEvents) for receive packets - /// and ManualUpdate(...) for update and send packets - /// This mode useful mostly for single-threaded servers - /// - /// port to listen - public bool StartInManualMode(int port) - { - return StartInManualMode(IPAddress.Any, IPAddress.IPv6Any, port); - } - - /// - /// Send message without connection - /// - /// Raw data - /// Packet destination - /// Operation result - public bool SendUnconnectedMessage(byte[] message, IPEndPoint remoteEndPoint) - { - return SendUnconnectedMessage(message, 0, message.Length, remoteEndPoint); - } - - /// - /// Send message without connection. WARNING This method allocates a new IPEndPoint object and - /// synchronously makes a DNS request. If you're calling this method every frame it will be - /// much faster to just cache the IPEndPoint. - /// - /// Data serializer - /// Packet destination IP or hostname - /// Packet destination port - /// Operation result - public bool SendUnconnectedMessage(NetDataWriter writer, string address, int port) - { - IPEndPoint remoteEndPoint = NetUtils.MakeEndPoint(address, port); - - return SendUnconnectedMessage(writer.Data, 0, writer.Length, remoteEndPoint); - } - - /// - /// Send message without connection - /// - /// Data serializer - /// Packet destination - /// Operation result - public bool SendUnconnectedMessage(NetDataWriter writer, IPEndPoint remoteEndPoint) - { - return SendUnconnectedMessage(writer.Data, 0, writer.Length, remoteEndPoint); - } - - /// - /// Send message without connection - /// - /// Raw data - /// data start - /// data length - /// Packet destination - /// Operation result - public bool SendUnconnectedMessage(byte[] message, int start, int length, IPEndPoint remoteEndPoint) - { - //No need for CRC here, SendRaw does that - NetPacket packet = PoolGetWithData(PacketProperty.UnconnectedMessage, message, start, length); - return SendRawAndRecycle(packet, remoteEndPoint) > 0; - } - - /// - /// Triggers update and send logic immediately (works asynchronously) - /// - public void TriggerUpdate() - { - _updateTriggerEvent.Set(); - } - - /// - /// Receive all pending events. Call this in game update code - /// In Manual mode it will call also socket Receive (which can be slow) - /// - public void PollEvents() - { - if (_manualMode) - { - if (_udpSocketv4 != null) - ManualReceive(_udpSocketv4, _bufferEndPointv4); - if (_udpSocketv6 != null && _udpSocketv6 != _udpSocketv4) - ManualReceive(_udpSocketv6, _bufferEndPointv6); - ProcessDelayedPackets(); - return; - } - if (UnsyncedEvents) - return; - NetEvent pendingEvent; - lock (_eventLock) - { - pendingEvent = _pendingEventHead; - _pendingEventHead = null; - _pendingEventTail = null; - } - - while (pendingEvent != null) - { - var next = pendingEvent.Next; - ProcessEvent(pendingEvent); - pendingEvent = next; - } - } - - /// - /// Connect to remote host - /// - /// Server IP or hostname - /// Server Port - /// Connection key - /// New NetPeer if new connection, Old NetPeer if already connected, null peer if there is ConnectionRequest awaiting - /// Manager is not running. Call - public NetPeer Connect(string address, int port, string key) - { - return Connect(address, port, NetDataWriter.FromString(key)); - } - - /// - /// Connect to remote host - /// - /// Server IP or hostname - /// Server Port - /// Additional data for remote peer - /// New NetPeer if new connection, Old NetPeer if already connected, null peer if there is ConnectionRequest awaiting - /// Manager is not running. Call - public NetPeer Connect(string address, int port, NetDataWriter connectionData) - { - IPEndPoint ep; - try - { - ep = NetUtils.MakeEndPoint(address, port); - } - catch - { - CreateEvent(NetEvent.EType.Disconnect, disconnectReason: DisconnectReason.UnknownHost); - return null; - } - return Connect(ep, connectionData); - } - - /// - /// Connect to remote host - /// - /// Server end point (ip and port) - /// Connection key - /// New NetPeer if new connection, Old NetPeer if already connected, null peer if there is ConnectionRequest awaiting - /// Manager is not running. Call - public NetPeer Connect(IPEndPoint target, string key) - { - return Connect(target, NetDataWriter.FromString(key)); - } - - /// - /// Connect to remote host - /// - /// Server end point (ip and port) - /// Additional data for remote peer - /// New NetPeer if new connection, Old NetPeer if already connected, null peer if there is ConnectionRequest awaiting - /// Manager is not running. Call - public NetPeer Connect(IPEndPoint target, NetDataWriter connectionData) - { - if (!IsRunning) - throw new InvalidOperationException("Client is not running"); - - lock(_requestsDict) - { - if (_requestsDict.ContainsKey(target)) - return null; - } - - byte connectionNumber = 0; - _peersLock.EnterUpgradeableReadLock(); - if (_peersDict.TryGetValue(target, out var peer)) - { - switch (peer.ConnectionState) - { - //just return already connected peer - case ConnectionState.Connected: - case ConnectionState.Outgoing: - _peersLock.ExitUpgradeableReadLock(); - return peer; - } - //else reconnect - connectionNumber = (byte)((peer.ConnectionNum + 1) % NetConstants.MaxConnectionNumber); - RemovePeer(peer); - } - - //Create reliable connection - //And send connection request - peer = new NetPeer(this, target, GetNextPeerId(), connectionNumber, connectionData); - AddPeer(peer); - _peersLock.ExitUpgradeableReadLock(); - - return peer; - } - - /// - /// Force closes connection and stop all threads. - /// - public void Stop() - { - Stop(true); - } - - /// - /// Force closes connection and stop all threads. - /// - /// Send disconnect messages - public void Stop(bool sendDisconnectMessages) - { - if (!IsRunning) - return; - NetDebug.Write("[NM] Stop"); - -#if UNITY_2018_3_OR_NEWER - _pausedSocketFix.Deinitialize(); - _pausedSocketFix = null; -#endif - - //Send last disconnect - for(var netPeer = _headPeer; netPeer != null; netPeer = netPeer.NextPeer) - netPeer.Shutdown(null, 0, 0, !sendDisconnectMessages); - - //Stop - CloseSocket(); - _updateTriggerEvent.Set(); - if (!_manualMode) - { - _logicThread.Join(); - _logicThread = null; - } - - //clear peers - _peersLock.EnterWriteLock(); - _headPeer = null; - _peersDict.Clear(); - _peersArray = new NetPeer[32]; - _peersLock.ExitWriteLock(); - _peerIds = new ConcurrentQueue(); - _lastPeerId = 0; -#if LITENETLIB_DEBUGGING - lock (_pingSimulationList) - _pingSimulationList.Clear(); -#endif - _connectedPeersCount = 0; - _pendingEventHead = null; - _pendingEventTail = null; - } - - /// - /// Return peers count with connection state - /// - /// peer connection state (you can use as bit flags) - /// peers count - public int GetPeersCount(ConnectionState peerState) - { - int count = 0; - _peersLock.EnterReadLock(); - for (var netPeer = _headPeer; netPeer != null; netPeer = netPeer.NextPeer) - { - if ((netPeer.ConnectionState & peerState) != 0) - count++; - } - _peersLock.ExitReadLock(); - return count; - } - - /// - /// Get copy of peers (without allocations) - /// - /// List that will contain result - /// State of peers - public void GetPeersNonAlloc(List peers, ConnectionState peerState) - { - peers.Clear(); - _peersLock.EnterReadLock(); - for (var netPeer = _headPeer; netPeer != null; netPeer = netPeer.NextPeer) - { - if ((netPeer.ConnectionState & peerState) != 0) - peers.Add(netPeer); - } - _peersLock.ExitReadLock(); - } - - /// - /// Disconnect all peers without any additional data - /// - public void DisconnectAll() - { - DisconnectAll(null, 0, 0); - } - - /// - /// Disconnect all peers with shutdown message - /// - /// Data to send (must be less or equal MTU) - /// Data start - /// Data count - public void DisconnectAll(byte[] data, int start, int count) - { - //Send disconnect packets - _peersLock.EnterReadLock(); - for (var netPeer = _headPeer; netPeer != null; netPeer = netPeer.NextPeer) - { - DisconnectPeer( - netPeer, - DisconnectReason.DisconnectPeerCalled, - 0, - false, - data, - start, - count, - null); - } - _peersLock.ExitReadLock(); - } - - /// - /// Immediately disconnect peer from server without additional data - /// - /// peer to disconnect - public void DisconnectPeerForce(NetPeer peer) - { - DisconnectPeerForce(peer, DisconnectReason.DisconnectPeerCalled, 0, null); - } - - /// - /// Disconnect peer from server - /// - /// peer to disconnect - public void DisconnectPeer(NetPeer peer) - { - DisconnectPeer(peer, null, 0, 0); - } - - /// - /// Disconnect peer from server and send additional data (Size must be less or equal MTU - 8) - /// - /// peer to disconnect - /// additional data - public void DisconnectPeer(NetPeer peer, byte[] data) - { - DisconnectPeer(peer, data, 0, data.Length); - } - - /// - /// Disconnect peer from server and send additional data (Size must be less or equal MTU - 8) - /// - /// peer to disconnect - /// additional data - public void DisconnectPeer(NetPeer peer, NetDataWriter writer) - { - DisconnectPeer(peer, writer.Data, 0, writer.Length); - } - - /// - /// Disconnect peer from server and send additional data (Size must be less or equal MTU - 8) - /// - /// peer to disconnect - /// additional data - /// data start - /// data length - public void DisconnectPeer(NetPeer peer, byte[] data, int start, int count) - { - DisconnectPeer( - peer, - DisconnectReason.DisconnectPeerCalled, - 0, - false, - data, - start, - count, - null); - } - - /// - /// Create the requests for NTP server - /// - /// NTP Server address. - public void CreateNtpRequest(IPEndPoint endPoint) - { - _ntpRequests.Add(endPoint, new NtpRequest(endPoint)); - } - - /// - /// Create the requests for NTP server - /// - /// NTP Server address. - /// port - public void CreateNtpRequest(string ntpServerAddress, int port) - { - IPEndPoint endPoint = NetUtils.MakeEndPoint(ntpServerAddress, port); - _ntpRequests.Add(endPoint, new NtpRequest(endPoint)); - } - - /// - /// Create the requests for NTP server (default port) - /// - /// NTP Server address. - public void CreateNtpRequest(string ntpServerAddress) - { - IPEndPoint endPoint = NetUtils.MakeEndPoint(ntpServerAddress, NtpRequest.DefaultPort); - _ntpRequests.Add(endPoint, new NtpRequest(endPoint)); - } - - public NetPeerEnumerator GetEnumerator() - { - return new NetPeerEnumerator(_headPeer); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return new NetPeerEnumerator(_headPeer); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return new NetPeerEnumerator(_headPeer); - } - } -} diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetManager.cs.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetManager.cs.meta deleted file mode 100644 index 921a22cb..00000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetManager.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: ecc95cc1990e9e72eb034f41bb178b5c -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetPacket.cs b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetPacket.cs deleted file mode 100644 index 4b403084..00000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetPacket.cs +++ /dev/null @@ -1,160 +0,0 @@ -using System; -using LiteNetLib.Utils; - -namespace LiteNetLib -{ - internal enum PacketProperty : byte - { - Unreliable, - Channeled, - Ack, - Ping, - Pong, - ConnectRequest, - ConnectAccept, - Disconnect, - UnconnectedMessage, - MtuCheck, - MtuOk, - Broadcast, - Merged, - ShutdownOk, - PeerNotFound, - InvalidProtocol, - NatMessage, - Empty - } - - internal sealed class NetPacket - { - private static readonly int PropertiesCount = Enum.GetValues(typeof(PacketProperty)).Length; - private static readonly int[] HeaderSizes; - - static NetPacket() - { - HeaderSizes = NetUtils.AllocatePinnedUninitializedArray(PropertiesCount); - for (int i = 0; i < HeaderSizes.Length; i++) - { - switch ((PacketProperty)i) - { - case PacketProperty.Channeled: - case PacketProperty.Ack: - HeaderSizes[i] = NetConstants.ChanneledHeaderSize; - break; - case PacketProperty.Ping: - HeaderSizes[i] = NetConstants.HeaderSize + 2; - break; - case PacketProperty.ConnectRequest: - HeaderSizes[i] = NetConnectRequestPacket.HeaderSize; - break; - case PacketProperty.ConnectAccept: - HeaderSizes[i] = NetConnectAcceptPacket.Size; - break; - case PacketProperty.Disconnect: - HeaderSizes[i] = NetConstants.HeaderSize + 8; - break; - case PacketProperty.Pong: - HeaderSizes[i] = NetConstants.HeaderSize + 10; - break; - default: - HeaderSizes[i] = NetConstants.HeaderSize; - break; - } - } - } - - //Header - public PacketProperty Property - { - get => (PacketProperty)(RawData[0] & 0x1F); - set => RawData[0] = (byte)((RawData[0] & 0xE0) | (byte)value); - } - - public byte ConnectionNumber - { - get => (byte)((RawData[0] & 0x60) >> 5); - set => RawData[0] = (byte) ((RawData[0] & 0x9F) | (value << 5)); - } - - public ushort Sequence - { - get => BitConverter.ToUInt16(RawData, 1); - set => FastBitConverter.GetBytes(RawData, 1, value); - } - - public bool IsFragmented => (RawData[0] & 0x80) != 0; - - public void MarkFragmented() - { - RawData[0] |= 0x80; //set first bit - } - - public byte ChannelId - { - get => RawData[3]; - set => RawData[3] = value; - } - - public ushort FragmentId - { - get => BitConverter.ToUInt16(RawData, 4); - set => FastBitConverter.GetBytes(RawData, 4, value); - } - - public ushort FragmentPart - { - get => BitConverter.ToUInt16(RawData, 6); - set => FastBitConverter.GetBytes(RawData, 6, value); - } - - public ushort FragmentsTotal - { - get => BitConverter.ToUInt16(RawData, 8); - set => FastBitConverter.GetBytes(RawData, 8, value); - } - - //Data - public byte[] RawData; - public int Size; - - //Delivery - public object UserData; - - //Pool node - public NetPacket Next; - - public NetPacket(int size) - { - RawData = new byte[size]; - Size = size; - } - - public NetPacket(PacketProperty property, int size) - { - size += GetHeaderSize(property); - RawData = new byte[size]; - Property = property; - Size = size; - } - - public static int GetHeaderSize(PacketProperty property) - { - return HeaderSizes[(int)property]; - } - - public int GetHeaderSize() - { - return HeaderSizes[RawData[0] & 0x1F]; - } - - public bool Verify() - { - byte property = (byte)(RawData[0] & 0x1F); - if (property >= PropertiesCount) - return false; - int headerSize = HeaderSizes[property]; - bool fragmented = (RawData[0] & 0x80) != 0; - return Size >= headerSize && (!fragmented || Size >= headerSize + NetConstants.FragmentHeaderSize); - } - } -} diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetPacket.cs.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetPacket.cs.meta deleted file mode 100644 index 05991c13..00000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetPacket.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: c4abeec1dd82bf35faaab590069c424d -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetPeer.cs b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetPeer.cs deleted file mode 100644 index 9ae91ed8..00000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetPeer.cs +++ /dev/null @@ -1,1395 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Diagnostics; -using System.Net; -using System.Threading; -using LiteNetLib.Utils; - -namespace LiteNetLib -{ - /// - /// Peer connection state - /// - [Flags] - public enum ConnectionState : byte - { - Outgoing = 1 << 1, - Connected = 1 << 2, - ShutdownRequested = 1 << 3, - Disconnected = 1 << 4, - EndPointChange = 1 << 5, - Any = Outgoing | Connected | ShutdownRequested | EndPointChange - } - - internal enum ConnectRequestResult - { - None, - P2PLose, //when peer connecting - Reconnection, //when peer was connected - NewConnection //when peer was disconnected - } - - internal enum DisconnectResult - { - None, - Reject, - Disconnect - } - - internal enum ShutdownResult - { - None, - Success, - WasConnected - } - - /// - /// Network peer. Main purpose is sending messages to specific peer. - /// - public class NetPeer - { - //Ping and RTT - private int _rtt; - private int _avgRtt; - private int _rttCount; - private double _resendDelay = 27.0; - private int _pingSendTimer; - private int _rttResetTimer; - private readonly Stopwatch _pingTimer = new Stopwatch(); - private int _timeSinceLastPacket; - private long _remoteDelta; - - //Common - private readonly object _shutdownLock = new object(); - - internal volatile NetPeer NextPeer; - internal NetPeer PrevPeer; - - internal byte ConnectionNum - { - get => _connectNum; - private set - { - _connectNum = value; - _mergeData.ConnectionNumber = value; - _pingPacket.ConnectionNumber = value; - _pongPacket.ConnectionNumber = value; - } - } - - //Channels - private readonly Queue _unreliableChannel; - private readonly ConcurrentQueue _channelSendQueue; - private readonly BaseChannel[] _channels; - - //MTU - private int _mtu; - private int _mtuIdx; - private bool _finishMtu; - private int _mtuCheckTimer; - private int _mtuCheckAttempts; - private const int MtuCheckDelay = 1000; - private const int MaxMtuCheckAttempts = 4; - private readonly object _mtuMutex = new object(); - - //Fragment - private class IncomingFragments - { - public NetPacket[] Fragments; - public int ReceivedCount; - public int TotalSize; - public byte ChannelId; - } - private int _fragmentId; - private readonly Dictionary _holdedFragments; - private readonly Dictionary _deliveredFragments; - - //Merging - private readonly NetPacket _mergeData; - private int _mergePos; - private int _mergeCount; - - //Connection - private IPEndPoint _remoteEndPoint; - private int _connectAttempts; - private int _connectTimer; - private long _connectTime; - private byte _connectNum; - private ConnectionState _connectionState; - private NetPacket _shutdownPacket; - private const int ShutdownDelay = 300; - private int _shutdownTimer; - private readonly NetPacket _pingPacket; - private readonly NetPacket _pongPacket; - private readonly NetPacket _connectRequestPacket; - private readonly NetPacket _connectAcceptPacket; - - /// - /// Peer ip address and port - /// - public IPEndPoint EndPoint => _remoteEndPoint; - - /// - /// Peer parent NetManager - /// - public readonly NetManager NetManager; - - /// - /// Current connection state - /// - public ConnectionState ConnectionState => _connectionState; - - /// - /// Connection time for internal purposes - /// - internal long ConnectTime => _connectTime; - - /// - /// Peer id can be used as key in your dictionary of peers - /// - public readonly int Id; - - /// - /// Id assigned from server - /// - public int RemoteId { get; private set; } - - /// - /// Current one-way ping (RTT/2) in milliseconds - /// - public int Ping => _avgRtt/2; - - /// - /// Round trip time in milliseconds - /// - public int RoundTripTime => _avgRtt; - - /// - /// Current MTU - Maximum Transfer Unit ( maximum udp packet size without fragmentation ) - /// - public int Mtu => _mtu; - - /// - /// Delta with remote time in ticks (not accurate) - /// positive - remote time > our time - /// - public long RemoteTimeDelta => _remoteDelta; - - /// - /// Remote UTC time (not accurate) - /// - public DateTime RemoteUtcTime => new DateTime(DateTime.UtcNow.Ticks + _remoteDelta); - - /// - /// Time since last packet received (including internal library packets) - /// - public int TimeSinceLastPacket => _timeSinceLastPacket; - - internal double ResendDelay => _resendDelay; - - /// - /// Application defined object containing data about the connection - /// - public object Tag; - - /// - /// Statistics of peer connection - /// - public readonly NetStatistics Statistics; - - //incoming connection constructor - internal NetPeer(NetManager netManager, IPEndPoint remoteEndPoint, int id) - { - Id = id; - Statistics = new NetStatistics(); - NetManager = netManager; - ResetMtu(); - _remoteEndPoint = remoteEndPoint; - _connectionState = ConnectionState.Connected; - _mergeData = new NetPacket(PacketProperty.Merged, NetConstants.MaxPacketSize); - _pongPacket = new NetPacket(PacketProperty.Pong, 0); - _pingPacket = new NetPacket(PacketProperty.Ping, 0) {Sequence = 1}; - - _unreliableChannel = new Queue(); - _holdedFragments = new Dictionary(); - _deliveredFragments = new Dictionary(); - - _channels = new BaseChannel[netManager.ChannelsCount * NetConstants.ChannelTypeCount]; - _channelSendQueue = new ConcurrentQueue(); - } - - internal void InitiateEndPointChange() - { - ResetMtu(); - _connectionState = ConnectionState.EndPointChange; - } - - internal void FinishEndPointChange(IPEndPoint newEndPoint) - { - if (_connectionState != ConnectionState.EndPointChange) - return; - _connectionState = ConnectionState.Connected; - _remoteEndPoint = newEndPoint; - } - - internal void ResetMtu() - { - _finishMtu = false; - if (NetManager.MtuOverride > 0) - OverrideMtu(NetManager.MtuOverride); - else if (NetManager.UseSafeMtu) - SetMtu(0); - else - SetMtu(1); - } - - private void SetMtu(int mtuIdx) - { - _mtuIdx = mtuIdx; - _mtu = NetConstants.PossibleMtu[mtuIdx] - NetManager.ExtraPacketSizeForLayer; - } - - private void OverrideMtu(int mtuValue) - { - _mtu = mtuValue; - _finishMtu = true; - } - - /// - /// Returns packets count in queue for reliable channel - /// - /// number of channel 0-63 - /// type of channel ReliableOrdered or ReliableUnordered - /// packets count in channel queue - public int GetPacketsCountInReliableQueue(byte channelNumber, bool ordered) - { - int idx = channelNumber * NetConstants.ChannelTypeCount + - (byte) (ordered ? DeliveryMethod.ReliableOrdered : DeliveryMethod.ReliableUnordered); - var channel = _channels[idx]; - return channel != null ? ((ReliableChannel)channel).PacketsInQueue : 0; - } - - /// - /// Create temporary packet (maximum size MTU - headerSize) to send later without additional copies - /// - /// Delivery method (reliable, unreliable, etc.) - /// Number of channel (from 0 to channelsCount - 1) - /// PooledPacket that you can use to write data starting from UserDataOffset - public PooledPacket CreatePacketFromPool(DeliveryMethod deliveryMethod, byte channelNumber) - { - //multithreaded variable - int mtu = _mtu; - var packet = NetManager.PoolGetPacket(mtu); - if (deliveryMethod == DeliveryMethod.Unreliable) - { - packet.Property = PacketProperty.Unreliable; - return new PooledPacket(packet, mtu, 0); - } - else - { - packet.Property = PacketProperty.Channeled; - return new PooledPacket(packet, mtu, (byte)(channelNumber * NetConstants.ChannelTypeCount + (byte)deliveryMethod)); - } - } - - /// - /// Sends pooled packet without data copy - /// - /// packet to send - /// size of user data you want to send - public void SendPooledPacket(PooledPacket packet, int userDataSize) - { - if (_connectionState != ConnectionState.Connected) - return; - packet._packet.Size = packet.UserDataOffset + userDataSize; - if (packet._packet.Property == PacketProperty.Channeled) - { - CreateChannel(packet._channelNumber).AddToQueue(packet._packet); - } - else - { - lock(_unreliableChannel) - _unreliableChannel.Enqueue(packet._packet); - } - } - - private BaseChannel CreateChannel(byte idx) - { - BaseChannel newChannel = _channels[idx]; - if (newChannel != null) - return newChannel; - switch ((DeliveryMethod)(idx % NetConstants.ChannelTypeCount)) - { - case DeliveryMethod.ReliableUnordered: - newChannel = new ReliableChannel(this, false, idx); - break; - case DeliveryMethod.Sequenced: - newChannel = new SequencedChannel(this, false, idx); - break; - case DeliveryMethod.ReliableOrdered: - newChannel = new ReliableChannel(this, true, idx); - break; - case DeliveryMethod.ReliableSequenced: - newChannel = new SequencedChannel(this, true, idx); - break; - } - BaseChannel prevChannel = Interlocked.CompareExchange(ref _channels[idx], newChannel, null); - if (prevChannel != null) - return prevChannel; - - return newChannel; - } - - //"Connect to" constructor - internal NetPeer(NetManager netManager, IPEndPoint remoteEndPoint, int id, byte connectNum, NetDataWriter connectData) - : this(netManager, remoteEndPoint, id) - { - _connectTime = DateTime.UtcNow.Ticks; - _connectionState = ConnectionState.Outgoing; - ConnectionNum = connectNum; - - //Make initial packet - _connectRequestPacket = NetConnectRequestPacket.Make(connectData, remoteEndPoint.Serialize(), _connectTime, id); - _connectRequestPacket.ConnectionNumber = connectNum; - - //Send request - NetManager.SendRaw(_connectRequestPacket, _remoteEndPoint); - - NetDebug.Write(NetLogLevel.Trace, $"[CC] ConnectId: {_connectTime}, ConnectNum: {connectNum}"); - } - - //"Accept" incoming constructor - internal NetPeer(NetManager netManager, ConnectionRequest request, int id) - : this(netManager, request.RemoteEndPoint, id) - { - _connectTime = request.InternalPacket.ConnectionTime; - ConnectionNum = request.InternalPacket.ConnectionNumber; - RemoteId = request.InternalPacket.PeerId; - - //Make initial packet - _connectAcceptPacket = NetConnectAcceptPacket.Make(_connectTime, ConnectionNum, id); - - //Make Connected - _connectionState = ConnectionState.Connected; - - //Send - NetManager.SendRaw(_connectAcceptPacket, _remoteEndPoint); - - NetDebug.Write(NetLogLevel.Trace, $"[CC] ConnectId: {_connectTime}"); - } - - //Reject - internal void Reject(NetConnectRequestPacket requestData, byte[] data, int start, int length) - { - _connectTime = requestData.ConnectionTime; - _connectNum = requestData.ConnectionNumber; - Shutdown(data, start, length, false); - } - - internal bool ProcessConnectAccept(NetConnectAcceptPacket packet) - { - if (_connectionState != ConnectionState.Outgoing) - return false; - - //check connection id - if (packet.ConnectionTime != _connectTime) - { - NetDebug.Write(NetLogLevel.Trace, $"[NC] Invalid connectId: {packet.ConnectionTime} != our({_connectTime})"); - return false; - } - //check connect num - ConnectionNum = packet.ConnectionNumber; - RemoteId = packet.PeerId; - - NetDebug.Write(NetLogLevel.Trace, "[NC] Received connection accept"); - Interlocked.Exchange(ref _timeSinceLastPacket, 0); - _connectionState = ConnectionState.Connected; - return true; - } - - /// - /// Gets maximum size of packet that will be not fragmented. - /// - /// Type of packet that you want send - /// size in bytes - public int GetMaxSinglePacketSize(DeliveryMethod options) - { - return _mtu - NetPacket.GetHeaderSize(options == DeliveryMethod.Unreliable ? PacketProperty.Unreliable : PacketProperty.Channeled); - } - - /// - /// Send data to peer with delivery event called - /// - /// Data - /// Number of channel (from 0 to channelsCount - 1) - /// Delivery method (reliable, unreliable, etc.) - /// User data that will be received in DeliveryEvent - /// - /// If you trying to send unreliable packet type - /// - public void SendWithDeliveryEvent(byte[] data, byte channelNumber, DeliveryMethod deliveryMethod, object userData) - { - if (deliveryMethod != DeliveryMethod.ReliableOrdered && deliveryMethod != DeliveryMethod.ReliableUnordered) - throw new ArgumentException("Delivery event will work only for ReliableOrdered/Unordered packets"); - SendInternal(data, 0, data.Length, channelNumber, deliveryMethod, userData); - } - - /// - /// Send data to peer with delivery event called - /// - /// Data - /// Start of data - /// Length of data - /// Number of channel (from 0 to channelsCount - 1) - /// Delivery method (reliable, unreliable, etc.) - /// User data that will be received in DeliveryEvent - /// - /// If you trying to send unreliable packet type - /// - public void SendWithDeliveryEvent(byte[] data, int start, int length, byte channelNumber, DeliveryMethod deliveryMethod, object userData) - { - if (deliveryMethod != DeliveryMethod.ReliableOrdered && deliveryMethod != DeliveryMethod.ReliableUnordered) - throw new ArgumentException("Delivery event will work only for ReliableOrdered/Unordered packets"); - SendInternal(data, start, length, channelNumber, deliveryMethod, userData); - } - - /// - /// Send data to peer with delivery event called - /// - /// Data - /// Number of channel (from 0 to channelsCount - 1) - /// Delivery method (reliable, unreliable, etc.) - /// User data that will be received in DeliveryEvent - /// - /// If you trying to send unreliable packet type - /// - public void SendWithDeliveryEvent(NetDataWriter dataWriter, byte channelNumber, DeliveryMethod deliveryMethod, object userData) - { - if (deliveryMethod != DeliveryMethod.ReliableOrdered && deliveryMethod != DeliveryMethod.ReliableUnordered) - throw new ArgumentException("Delivery event will work only for ReliableOrdered/Unordered packets"); - SendInternal(dataWriter.Data, 0, dataWriter.Length, channelNumber, deliveryMethod, userData); - } - - /// - /// Send data to peer (channel - 0) - /// - /// Data - /// Send options (reliable, unreliable, etc.) - /// - /// If size exceeds maximum limit: - /// MTU - headerSize bytes for Unreliable - /// Fragment count exceeded ushort.MaxValue - /// - public void Send(byte[] data, DeliveryMethod deliveryMethod) - { - SendInternal(data, 0, data.Length, 0, deliveryMethod, null); - } - - /// - /// Send data to peer (channel - 0) - /// - /// DataWriter with data - /// Send options (reliable, unreliable, etc.) - /// - /// If size exceeds maximum limit: - /// MTU - headerSize bytes for Unreliable - /// Fragment count exceeded ushort.MaxValue - /// - public void Send(NetDataWriter dataWriter, DeliveryMethod deliveryMethod) - { - SendInternal(dataWriter.Data, 0, dataWriter.Length, 0, deliveryMethod, null); - } - - /// - /// Send data to peer (channel - 0) - /// - /// Data - /// Start of data - /// Length of data - /// Send options (reliable, unreliable, etc.) - /// - /// If size exceeds maximum limit: - /// MTU - headerSize bytes for Unreliable - /// Fragment count exceeded ushort.MaxValue - /// - public void Send(byte[] data, int start, int length, DeliveryMethod options) - { - SendInternal(data, start, length, 0, options, null); - } - - /// - /// Send data to peer - /// - /// Data - /// Number of channel (from 0 to channelsCount - 1) - /// Send options (reliable, unreliable, etc.) - /// - /// If size exceeds maximum limit: - /// MTU - headerSize bytes for Unreliable - /// Fragment count exceeded ushort.MaxValue - /// - public void Send(byte[] data, byte channelNumber, DeliveryMethod deliveryMethod) - { - SendInternal(data, 0, data.Length, channelNumber, deliveryMethod, null); - } - - /// - /// Send data to peer - /// - /// DataWriter with data - /// Number of channel (from 0 to channelsCount - 1) - /// Send options (reliable, unreliable, etc.) - /// - /// If size exceeds maximum limit: - /// MTU - headerSize bytes for Unreliable - /// Fragment count exceeded ushort.MaxValue - /// - public void Send(NetDataWriter dataWriter, byte channelNumber, DeliveryMethod deliveryMethod) - { - SendInternal(dataWriter.Data, 0, dataWriter.Length, channelNumber, deliveryMethod, null); - } - - /// - /// Send data to peer - /// - /// Data - /// Start of data - /// Length of data - /// Number of channel (from 0 to channelsCount - 1) - /// Delivery method (reliable, unreliable, etc.) - /// - /// If size exceeds maximum limit: - /// MTU - headerSize bytes for Unreliable - /// Fragment count exceeded ushort.MaxValue - /// - public void Send(byte[] data, int start, int length, byte channelNumber, DeliveryMethod deliveryMethod) - { - SendInternal(data, start, length, channelNumber, deliveryMethod, null); - } - - private void SendInternal( - byte[] data, - int start, - int length, - byte channelNumber, - DeliveryMethod deliveryMethod, - object userData) - { - if (_connectionState != ConnectionState.Connected || channelNumber >= _channels.Length) - return; - - //Select channel - PacketProperty property; - BaseChannel channel = null; - - if (deliveryMethod == DeliveryMethod.Unreliable) - { - property = PacketProperty.Unreliable; - } - else - { - property = PacketProperty.Channeled; - channel = CreateChannel((byte)(channelNumber * NetConstants.ChannelTypeCount + (byte)deliveryMethod)); - } - - //Prepare - NetDebug.Write("[RS]Packet: " + property); - - //Check fragmentation - int headerSize = NetPacket.GetHeaderSize(property); - //Save mtu for multithread - int mtu = _mtu; - if (length + headerSize > mtu) - { - //if cannot be fragmented - if (deliveryMethod != DeliveryMethod.ReliableOrdered && deliveryMethod != DeliveryMethod.ReliableUnordered) - throw new TooBigPacketException("Unreliable or ReliableSequenced packet size exceeded maximum of " + (mtu - headerSize) + " bytes, Check allowed size by GetMaxSinglePacketSize()"); - - int packetFullSize = mtu - headerSize; - int packetDataSize = packetFullSize - NetConstants.FragmentHeaderSize; - int totalPackets = length / packetDataSize + (length % packetDataSize == 0 ? 0 : 1); - - NetDebug.Write($@"FragmentSend: - MTU: {mtu} - headerSize: {headerSize} - packetFullSize: {packetFullSize} - packetDataSize: {packetDataSize} - totalPackets: {totalPackets}"); - - if (totalPackets > ushort.MaxValue) - throw new TooBigPacketException("Data was split in " + totalPackets + " fragments, which exceeds " + ushort.MaxValue); - - ushort currentFragmentId = (ushort)Interlocked.Increment(ref _fragmentId); - - for(ushort partIdx = 0; partIdx < totalPackets; partIdx++) - { - int sendLength = length > packetDataSize ? packetDataSize : length; - - NetPacket p = NetManager.PoolGetPacket(headerSize + sendLength + NetConstants.FragmentHeaderSize); - p.Property = property; - p.UserData = userData; - p.FragmentId = currentFragmentId; - p.FragmentPart = partIdx; - p.FragmentsTotal = (ushort)totalPackets; - p.MarkFragmented(); - - Buffer.BlockCopy(data, start + partIdx * packetDataSize, p.RawData, NetConstants.FragmentedHeaderTotalSize, sendLength); - channel.AddToQueue(p); - - length -= sendLength; - } - return; - } - - //Else just send - NetPacket packet = NetManager.PoolGetPacket(headerSize + length); - packet.Property = property; - Buffer.BlockCopy(data, start, packet.RawData, headerSize, length); - packet.UserData = userData; - - if (channel == null) //unreliable - { - lock(_unreliableChannel) - _unreliableChannel.Enqueue(packet); - } - else - { - channel.AddToQueue(packet); - } - } - -#if LITENETLIB_SPANS || NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1 || NETCOREAPP3_1 || NET5_0 || NETSTANDARD2_1 - /// - /// Send data to peer with delivery event called - /// - /// Data - /// Number of channel (from 0 to channelsCount - 1) - /// Delivery method (reliable, unreliable, etc.) - /// User data that will be received in DeliveryEvent - /// - /// If you trying to send unreliable packet type - /// - public void SendWithDeliveryEvent(ReadOnlySpan data, byte channelNumber, DeliveryMethod deliveryMethod, object userData) - { - if (deliveryMethod != DeliveryMethod.ReliableOrdered && deliveryMethod != DeliveryMethod.ReliableUnordered) - throw new ArgumentException("Delivery event will work only for ReliableOrdered/Unordered packets"); - SendInternal(data, channelNumber, deliveryMethod, userData); - } - - /// - /// Send data to peer (channel - 0) - /// - /// Data - /// Send options (reliable, unreliable, etc.) - /// - /// If size exceeds maximum limit: - /// MTU - headerSize bytes for Unreliable - /// Fragment count exceeded ushort.MaxValue - /// - public void Send(ReadOnlySpan data, DeliveryMethod deliveryMethod) - { - SendInternal(data, 0, deliveryMethod, null); - } - - /// - /// Send data to peer - /// - /// Data - /// Number of channel (from 0 to channelsCount - 1) - /// Send options (reliable, unreliable, etc.) - /// - /// If size exceeds maximum limit: - /// MTU - headerSize bytes for Unreliable - /// Fragment count exceeded ushort.MaxValue - /// - public void Send(ReadOnlySpan data, byte channelNumber, DeliveryMethod deliveryMethod) - { - SendInternal(data, channelNumber, deliveryMethod, null); - } - - private void SendInternal( - ReadOnlySpan data, - byte channelNumber, - DeliveryMethod deliveryMethod, - object userData) - { - if (_connectionState != ConnectionState.Connected || channelNumber >= _channels.Length) - return; - - //Select channel - PacketProperty property; - BaseChannel channel = null; - - if (deliveryMethod == DeliveryMethod.Unreliable) - { - property = PacketProperty.Unreliable; - } - else - { - property = PacketProperty.Channeled; - channel = CreateChannel((byte)(channelNumber * NetConstants.ChannelTypeCount + (byte)deliveryMethod)); - } - - //Prepare - NetDebug.Write("[RS]Packet: " + property); - - //Check fragmentation - int headerSize = NetPacket.GetHeaderSize(property); - //Save mtu for multithread - int mtu = _mtu; - int length = data.Length; - if (length + headerSize > mtu) - { - //if cannot be fragmented - if (deliveryMethod != DeliveryMethod.ReliableOrdered && deliveryMethod != DeliveryMethod.ReliableUnordered) - throw new TooBigPacketException("Unreliable or ReliableSequenced packet size exceeded maximum of " + (mtu - headerSize) + " bytes, Check allowed size by GetMaxSinglePacketSize()"); - - int packetFullSize = mtu - headerSize; - int packetDataSize = packetFullSize - NetConstants.FragmentHeaderSize; - int totalPackets = length / packetDataSize + (length % packetDataSize == 0 ? 0 : 1); - - if (totalPackets > ushort.MaxValue) - throw new TooBigPacketException("Data was split in " + totalPackets + " fragments, which exceeds " + ushort.MaxValue); - - ushort currentFragmentId = (ushort)Interlocked.Increment(ref _fragmentId); - - for (ushort partIdx = 0; partIdx < totalPackets; partIdx++) - { - int sendLength = length > packetDataSize ? packetDataSize : length; - - NetPacket p = NetManager.PoolGetPacket(headerSize + sendLength + NetConstants.FragmentHeaderSize); - p.Property = property; - p.UserData = userData; - p.FragmentId = currentFragmentId; - p.FragmentPart = partIdx; - p.FragmentsTotal = (ushort)totalPackets; - p.MarkFragmented(); - - data.Slice(partIdx * packetDataSize, sendLength).CopyTo(new Span(p.RawData, NetConstants.FragmentedHeaderTotalSize, sendLength)); - channel.AddToQueue(p); - - length -= sendLength; - } - return; - } - - //Else just send - NetPacket packet = NetManager.PoolGetPacket(headerSize + length); - packet.Property = property; - data.CopyTo(new Span(packet.RawData, headerSize, length)); - packet.UserData = userData; - - if (channel == null) //unreliable - { - lock(_unreliableChannel) - _unreliableChannel.Enqueue(packet); - } - else - { - channel.AddToQueue(packet); - } - } -#endif - - public void Disconnect(byte[] data) - { - NetManager.DisconnectPeer(this, data); - } - - public void Disconnect(NetDataWriter writer) - { - NetManager.DisconnectPeer(this, writer); - } - - public void Disconnect(byte[] data, int start, int count) - { - NetManager.DisconnectPeer(this, data, start, count); - } - - public void Disconnect() - { - NetManager.DisconnectPeer(this); - } - - internal DisconnectResult ProcessDisconnect(NetPacket packet) - { - if ((_connectionState == ConnectionState.Connected || _connectionState == ConnectionState.Outgoing) && - packet.Size >= 9 && - BitConverter.ToInt64(packet.RawData, 1) == _connectTime && - packet.ConnectionNumber == _connectNum) - { - return _connectionState == ConnectionState.Connected - ? DisconnectResult.Disconnect - : DisconnectResult.Reject; - } - return DisconnectResult.None; - } - - internal void AddToReliableChannelSendQueue(BaseChannel channel) - { - _channelSendQueue.Enqueue(channel); - } - - internal ShutdownResult Shutdown(byte[] data, int start, int length, bool force) - { - lock (_shutdownLock) - { - //trying to shutdown already disconnected - if (_connectionState == ConnectionState.Disconnected || - _connectionState == ConnectionState.ShutdownRequested) - { - return ShutdownResult.None; - } - - var result = _connectionState == ConnectionState.Connected - ? ShutdownResult.WasConnected - : ShutdownResult.Success; - - //don't send anything - if (force) - { - _connectionState = ConnectionState.Disconnected; - return result; - } - - //reset time for reconnect protection - Interlocked.Exchange(ref _timeSinceLastPacket, 0); - - //send shutdown packet - _shutdownPacket = new NetPacket(PacketProperty.Disconnect, length) {ConnectionNumber = _connectNum}; - FastBitConverter.GetBytes(_shutdownPacket.RawData, 1, _connectTime); - if (_shutdownPacket.Size >= _mtu) - { - //Drop additional data - NetDebug.WriteError("[Peer] Disconnect additional data size more than MTU - 8!"); - } - else if (data != null && length > 0) - { - Buffer.BlockCopy(data, start, _shutdownPacket.RawData, 9, length); - } - _connectionState = ConnectionState.ShutdownRequested; - NetDebug.Write("[Peer] Send disconnect"); - NetManager.SendRaw(_shutdownPacket, _remoteEndPoint); - return result; - } - } - - private void UpdateRoundTripTime(int roundTripTime) - { - _rtt += roundTripTime; - _rttCount++; - _avgRtt = _rtt/_rttCount; - _resendDelay = 25.0 + _avgRtt * 2.1; // 25 ms + double rtt - } - - internal void AddReliablePacket(DeliveryMethod method, NetPacket p) - { - if (p.IsFragmented) - { - NetDebug.Write($"Fragment. Id: {p.FragmentId}, Part: {p.FragmentPart}, Total: {p.FragmentsTotal}"); - //Get needed array from dictionary - ushort packetFragId = p.FragmentId; - byte packetChannelId = p.ChannelId; - if (!_holdedFragments.TryGetValue(packetFragId, out var incomingFragments)) - { - incomingFragments = new IncomingFragments - { - Fragments = new NetPacket[p.FragmentsTotal], - ChannelId = p.ChannelId - }; - _holdedFragments.Add(packetFragId, incomingFragments); - } - - //Cache - var fragments = incomingFragments.Fragments; - - //Error check - if (p.FragmentPart >= fragments.Length || - fragments[p.FragmentPart] != null || - p.ChannelId != incomingFragments.ChannelId) - { - NetManager.PoolRecycle(p); - NetDebug.WriteError("Invalid fragment packet"); - return; - } - //Fill array - fragments[p.FragmentPart] = p; - - //Increase received fragments count - incomingFragments.ReceivedCount++; - - //Increase total size - incomingFragments.TotalSize += p.Size - NetConstants.FragmentedHeaderTotalSize; - - //Check for finish - if (incomingFragments.ReceivedCount != fragments.Length) - return; - - //just simple packet - NetPacket resultingPacket = NetManager.PoolGetPacket(incomingFragments.TotalSize); - - int pos = 0; - for (int i = 0; i < incomingFragments.ReceivedCount; i++) - { - var fragment = fragments[i]; - int writtenSize = fragment.Size - NetConstants.FragmentedHeaderTotalSize; - - if (pos+writtenSize > resultingPacket.RawData.Length) - { - _holdedFragments.Remove(packetFragId); - NetDebug.WriteError($"Fragment error pos: {pos + writtenSize} >= resultPacketSize: {resultingPacket.RawData.Length} , totalSize: {incomingFragments.TotalSize}"); - return; - } - if (fragment.Size > fragment.RawData.Length) - { - _holdedFragments.Remove(packetFragId); - NetDebug.WriteError($"Fragment error size: {fragment.Size} > fragment.RawData.Length: {fragment.RawData.Length}"); - return; - } - - //Create resulting big packet - Buffer.BlockCopy( - fragment.RawData, - NetConstants.FragmentedHeaderTotalSize, - resultingPacket.RawData, - pos, - writtenSize); - pos += writtenSize; - - //Free memory - NetManager.PoolRecycle(fragment); - fragments[i] = null; - } - - //Clear memory - _holdedFragments.Remove(packetFragId); - - //Send to process - NetManager.CreateReceiveEvent(resultingPacket, method, (byte)(packetChannelId / NetConstants.ChannelTypeCount), 0, this); - } - else //Just simple packet - { - NetManager.CreateReceiveEvent(p, method, (byte)(p.ChannelId / NetConstants.ChannelTypeCount), NetConstants.ChanneledHeaderSize, this); - } - } - - private void ProcessMtuPacket(NetPacket packet) - { - //header + int - if (packet.Size < NetConstants.PossibleMtu[0]) - return; - - //first stage check (mtu check and mtu ok) - int receivedMtu = BitConverter.ToInt32(packet.RawData, 1); - int endMtuCheck = BitConverter.ToInt32(packet.RawData, packet.Size - 4); - if (receivedMtu != packet.Size || receivedMtu != endMtuCheck || receivedMtu > NetConstants.MaxPacketSize) - { - NetDebug.WriteError($"[MTU] Broken packet. RMTU {receivedMtu}, EMTU {endMtuCheck}, PSIZE {packet.Size}"); - return; - } - - if (packet.Property == PacketProperty.MtuCheck) - { - _mtuCheckAttempts = 0; - NetDebug.Write("[MTU] check. send back: " + receivedMtu); - packet.Property = PacketProperty.MtuOk; - NetManager.SendRawAndRecycle(packet, _remoteEndPoint); - } - else if(receivedMtu > _mtu && !_finishMtu) //MtuOk - { - //invalid packet - if (receivedMtu != NetConstants.PossibleMtu[_mtuIdx + 1] - NetManager.ExtraPacketSizeForLayer) - return; - - lock (_mtuMutex) - { - SetMtu(_mtuIdx+1); - } - //if maxed - finish. - if (_mtuIdx == NetConstants.PossibleMtu.Length - 1) - _finishMtu = true; - NetManager.PoolRecycle(packet); - NetDebug.Write("[MTU] ok. Increase to: " + _mtu); - } - } - - private void UpdateMtuLogic(int deltaTime) - { - if (_finishMtu) - return; - - _mtuCheckTimer += deltaTime; - if (_mtuCheckTimer < MtuCheckDelay) - return; - - _mtuCheckTimer = 0; - _mtuCheckAttempts++; - if (_mtuCheckAttempts >= MaxMtuCheckAttempts) - { - _finishMtu = true; - return; - } - - lock (_mtuMutex) - { - if (_mtuIdx >= NetConstants.PossibleMtu.Length - 1) - return; - - //Send increased packet - int newMtu = NetConstants.PossibleMtu[_mtuIdx + 1] - NetManager.ExtraPacketSizeForLayer; - var p = NetManager.PoolGetPacket(newMtu); - p.Property = PacketProperty.MtuCheck; - FastBitConverter.GetBytes(p.RawData, 1, newMtu); //place into start - FastBitConverter.GetBytes(p.RawData, p.Size - 4, newMtu);//and end of packet - - //Must check result for MTU fix - if (NetManager.SendRawAndRecycle(p, _remoteEndPoint) <= 0) - _finishMtu = true; - } - } - - internal ConnectRequestResult ProcessConnectRequest(NetConnectRequestPacket connRequest) - { - //current or new request - switch (_connectionState) - { - //P2P case - case ConnectionState.Outgoing: - //fast check - if (connRequest.ConnectionTime < _connectTime) - { - return ConnectRequestResult.P2PLose; - } - //slow rare case check - if (connRequest.ConnectionTime == _connectTime) - { - var remoteBytes = _remoteEndPoint.Serialize(); - var localBytes = connRequest.TargetAddress; - for (int i = remoteBytes.Size-1; i >= 0; i--) - { - byte rb = remoteBytes[i]; - if (rb == localBytes[i]) - continue; - if (rb < localBytes[i]) - return ConnectRequestResult.P2PLose; - } - } - break; - - case ConnectionState.Connected: - //Old connect request - if (connRequest.ConnectionTime == _connectTime) - { - //just reply accept - NetManager.SendRaw(_connectAcceptPacket, _remoteEndPoint); - } - //New connect request - else if (connRequest.ConnectionTime > _connectTime) - { - return ConnectRequestResult.Reconnection; - } - break; - - case ConnectionState.Disconnected: - case ConnectionState.ShutdownRequested: - if (connRequest.ConnectionTime >= _connectTime) - return ConnectRequestResult.NewConnection; - break; - } - return ConnectRequestResult.None; - } - - //Process incoming packet - internal void ProcessPacket(NetPacket packet) - { - //not initialized - if (_connectionState == ConnectionState.Outgoing || _connectionState == ConnectionState.Disconnected) - { - NetManager.PoolRecycle(packet); - return; - } - if (packet.Property == PacketProperty.ShutdownOk) - { - if (_connectionState == ConnectionState.ShutdownRequested) - _connectionState = ConnectionState.Disconnected; - NetManager.PoolRecycle(packet); - return; - } - if (packet.ConnectionNumber != _connectNum) - { - NetDebug.Write(NetLogLevel.Trace, "[RR]Old packet"); - NetManager.PoolRecycle(packet); - return; - } - Interlocked.Exchange(ref _timeSinceLastPacket, 0); - - NetDebug.Write($"[RR]PacketProperty: {packet.Property}"); - switch (packet.Property) - { - case PacketProperty.Merged: - int pos = NetConstants.HeaderSize; - while (pos < packet.Size) - { - ushort size = BitConverter.ToUInt16(packet.RawData, pos); - pos += 2; - if (packet.RawData.Length - pos < size) - break; - - NetPacket mergedPacket = NetManager.PoolGetPacket(size); - Buffer.BlockCopy(packet.RawData, pos, mergedPacket.RawData, 0, size); - mergedPacket.Size = size; - - if (!mergedPacket.Verify()) - break; - - pos += size; - ProcessPacket(mergedPacket); - } - NetManager.PoolRecycle(packet); - break; - //If we get ping, send pong - case PacketProperty.Ping: - if (NetUtils.RelativeSequenceNumber(packet.Sequence, _pongPacket.Sequence) > 0) - { - NetDebug.Write("[PP]Ping receive, send pong"); - FastBitConverter.GetBytes(_pongPacket.RawData, 3, DateTime.UtcNow.Ticks); - _pongPacket.Sequence = packet.Sequence; - NetManager.SendRaw(_pongPacket, _remoteEndPoint); - } - NetManager.PoolRecycle(packet); - break; - - //If we get pong, calculate ping time and rtt - case PacketProperty.Pong: - if (packet.Sequence == _pingPacket.Sequence) - { - _pingTimer.Stop(); - int elapsedMs = (int)_pingTimer.ElapsedMilliseconds; - _remoteDelta = BitConverter.ToInt64(packet.RawData, 3) + (elapsedMs * TimeSpan.TicksPerMillisecond ) / 2 - DateTime.UtcNow.Ticks; - UpdateRoundTripTime(elapsedMs); - NetManager.ConnectionLatencyUpdated(this, elapsedMs / 2); - NetDebug.Write($"[PP]Ping: {packet.Sequence} - {elapsedMs} - {_remoteDelta}"); - } - NetManager.PoolRecycle(packet); - break; - - case PacketProperty.Ack: - case PacketProperty.Channeled: - if (packet.ChannelId > _channels.Length) - { - NetManager.PoolRecycle(packet); - break; - } - var channel = _channels[packet.ChannelId] ?? (packet.Property == PacketProperty.Ack ? null : CreateChannel(packet.ChannelId)); - if (channel != null) - { - if (!channel.ProcessPacket(packet)) - NetManager.PoolRecycle(packet); - } - break; - - //Simple packet without acks - case PacketProperty.Unreliable: - NetManager.CreateReceiveEvent(packet, DeliveryMethod.Unreliable, 0, NetConstants.HeaderSize, this); - return; - - case PacketProperty.MtuCheck: - case PacketProperty.MtuOk: - ProcessMtuPacket(packet); - break; - - default: - NetDebug.WriteError("Error! Unexpected packet type: " + packet.Property); - break; - } - } - - private void SendMerged() - { - if (_mergeCount == 0) - return; - int bytesSent; - if (_mergeCount > 1) - { - NetDebug.Write("[P]Send merged: " + _mergePos + ", count: " + _mergeCount); - bytesSent = NetManager.SendRaw(_mergeData.RawData, 0, NetConstants.HeaderSize + _mergePos, _remoteEndPoint); - } - else - { - //Send without length information and merging - bytesSent = NetManager.SendRaw(_mergeData.RawData, NetConstants.HeaderSize + 2, _mergePos - 2, _remoteEndPoint); - } - - if (NetManager.EnableStatistics) - { - Statistics.IncrementPacketsSent(); - Statistics.AddBytesSent(bytesSent); - } - - _mergePos = 0; - _mergeCount = 0; - } - - internal void SendUserData(NetPacket packet) - { - packet.ConnectionNumber = _connectNum; - int mergedPacketSize = NetConstants.HeaderSize + packet.Size + 2; - const int sizeTreshold = 20; - if (mergedPacketSize + sizeTreshold >= _mtu) - { - NetDebug.Write(NetLogLevel.Trace, "[P]SendingPacket: " + packet.Property); - int bytesSent = NetManager.SendRaw(packet, _remoteEndPoint); - - if (NetManager.EnableStatistics) - { - Statistics.IncrementPacketsSent(); - Statistics.AddBytesSent(bytesSent); - } - - return; - } - if (_mergePos + mergedPacketSize > _mtu) - SendMerged(); - - FastBitConverter.GetBytes(_mergeData.RawData, _mergePos + NetConstants.HeaderSize, (ushort)packet.Size); - Buffer.BlockCopy(packet.RawData, 0, _mergeData.RawData, _mergePos + NetConstants.HeaderSize + 2, packet.Size); - _mergePos += packet.Size + 2; - _mergeCount++; - //DebugWriteForce("Merged: " + _mergePos + "/" + (_mtu - 2) + ", count: " + _mergeCount); - } - - internal void Update(int deltaTime) - { - Interlocked.Add(ref _timeSinceLastPacket, deltaTime); - switch (_connectionState) - { - case ConnectionState.Connected: - if (_timeSinceLastPacket > NetManager.DisconnectTimeout) - { - NetDebug.Write($"[UPDATE] Disconnect by timeout: {_timeSinceLastPacket} > {NetManager.DisconnectTimeout}"); - NetManager.DisconnectPeerForce(this, DisconnectReason.Timeout, 0, null); - return; - } - break; - - case ConnectionState.ShutdownRequested: - if (_timeSinceLastPacket > NetManager.DisconnectTimeout) - { - _connectionState = ConnectionState.Disconnected; - } - else - { - _shutdownTimer += deltaTime; - if (_shutdownTimer >= ShutdownDelay) - { - _shutdownTimer = 0; - NetManager.SendRaw(_shutdownPacket, _remoteEndPoint); - } - } - return; - - case ConnectionState.Outgoing: - _connectTimer += deltaTime; - if (_connectTimer > NetManager.ReconnectDelay) - { - _connectTimer = 0; - _connectAttempts++; - if (_connectAttempts > NetManager.MaxConnectAttempts) - { - NetManager.DisconnectPeerForce(this, DisconnectReason.ConnectionFailed, 0, null); - return; - } - - //else send connect again - NetManager.SendRaw(_connectRequestPacket, _remoteEndPoint); - } - return; - - case ConnectionState.Disconnected: - return; - } - - //Send ping - _pingSendTimer += deltaTime; - if (_pingSendTimer >= NetManager.PingInterval) - { - NetDebug.Write("[PP] Send ping..."); - //reset timer - _pingSendTimer = 0; - //send ping - _pingPacket.Sequence++; - //ping timeout - if (_pingTimer.IsRunning) - UpdateRoundTripTime((int)_pingTimer.ElapsedMilliseconds); - _pingTimer.Restart(); - NetManager.SendRaw(_pingPacket, _remoteEndPoint); - } - - //RTT - round trip time - _rttResetTimer += deltaTime; - if (_rttResetTimer >= NetManager.PingInterval * 3) - { - _rttResetTimer = 0; - _rtt = _avgRtt; - _rttCount = 1; - } - - UpdateMtuLogic(deltaTime); - - //Pending send - int count = _channelSendQueue.Count; - while (count-- > 0) - { - if (!_channelSendQueue.TryDequeue(out var channel)) - break; - if (channel.SendAndCheckQueue()) - { - // still has something to send, re-add it to the send queue - _channelSendQueue.Enqueue(channel); - } - } - - lock (_unreliableChannel) - { - int unreliableCount = _unreliableChannel.Count; - for (int i = 0; i < unreliableCount; i++) - { - var packet = _unreliableChannel.Dequeue(); - SendUserData(packet); - NetManager.PoolRecycle(packet); - } - } - - SendMerged(); - } - - //For reliable channel - internal void RecycleAndDeliver(NetPacket packet) - { - if (packet.UserData != null) - { - if (packet.IsFragmented) - { - _deliveredFragments.TryGetValue(packet.FragmentId, out ushort fragCount); - fragCount++; - if (fragCount == packet.FragmentsTotal) - { - NetManager.MessageDelivered(this, packet.UserData); - _deliveredFragments.Remove(packet.FragmentId); - } - else - { - _deliveredFragments[packet.FragmentId] = fragCount; - } - } - else - { - NetManager.MessageDelivered(this, packet.UserData); - } - packet.UserData = null; - } - NetManager.PoolRecycle(packet); - } - } -} diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetPeer.cs.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetPeer.cs.meta deleted file mode 100644 index eee14064..00000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetPeer.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 275317db1cf564734938a93ee4937011 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetStatistics.cs b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetStatistics.cs deleted file mode 100644 index c369a0f0..00000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetStatistics.cs +++ /dev/null @@ -1,103 +0,0 @@ -using System.Collections.Generic; -using System.Threading; -using LiteNetLib.Utils; - -namespace LiteNetLib -{ - public sealed class NetStatistics - { - private long _packetsSent; - private long _packetsReceived; - private long _bytesSent; - private long _bytesReceived; - private long _packetLoss; - private readonly Dictionary _packetsWrittenByType = new Dictionary(NetPacketProcessor.PacketCount); - private readonly Dictionary _bytesWrittenByType = new Dictionary(NetPacketProcessor.PacketCount); - - public long PacketsSent => Interlocked.Read(ref _packetsSent); - public long PacketsReceived => Interlocked.Read(ref _packetsReceived); - public long BytesSent => Interlocked.Read(ref _bytesSent); - public long BytesReceived => Interlocked.Read(ref _bytesReceived); - public long PacketLoss => Interlocked.Read(ref _packetLoss); - public Dictionary PacketsWrittenByType => new Dictionary(_packetsWrittenByType); - public Dictionary BytesWrittenByType => new Dictionary(_bytesWrittenByType); - - public long PacketLossPercent { - get { - long sent = PacketsSent, loss = PacketLoss; - - return sent == 0 ? 0 : loss * 100 / sent; - } - } - - public void Reset() - { - Interlocked.Exchange(ref _packetsSent, 0); - Interlocked.Exchange(ref _packetsReceived, 0); - Interlocked.Exchange(ref _bytesSent, 0); - Interlocked.Exchange(ref _bytesReceived, 0); - Interlocked.Exchange(ref _packetLoss, 0); - _packetsWrittenByType.Clear(); - _bytesWrittenByType.Clear(); - } - - public void IncrementPacketsSent() - { - Interlocked.Increment(ref _packetsSent); - } - - public void IncrementPacketsReceived() - { - Interlocked.Increment(ref _packetsReceived); - } - - public void AddBytesSent(long bytesSent) - { - Interlocked.Add(ref _bytesSent, bytesSent); - } - - public void AddBytesReceived(long bytesReceived) - { - Interlocked.Add(ref _bytesReceived, bytesReceived); - } - - public void IncrementPacketLoss() - { - Interlocked.Increment(ref _packetLoss); - } - - public void AddPacketLoss(long packetLoss) - { - Interlocked.Add(ref _packetLoss, packetLoss); - } - - public void IncrementPacketsWritten(byte id) - { - if (_packetsWrittenByType.ContainsKey(id)) - _packetsWrittenByType[id]++; - else - _packetsWrittenByType[id] = 1; - } - - public void AddBytesWritten(byte id, int bytesWritten) - { - if (_bytesWrittenByType.ContainsKey(id)) - _bytesWrittenByType[id] += bytesWritten; - else - _bytesWrittenByType[id] = bytesWritten; - } - - public override string ToString() - { - return - string.Format( - "BytesReceived: {0}\nPacketsReceived: {1}\nBytesSent: {2}\nPacketsSent: {3}\nPacketLoss: {4}\nPacketLossPercent: {5}\n", - BytesReceived, - PacketsReceived, - BytesSent, - PacketsSent, - PacketLoss, - PacketLossPercent); - } - } -} diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetStatistics.cs.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetStatistics.cs.meta deleted file mode 100644 index 8407db64..00000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetStatistics.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 9b449e5229fa4502fae05e714d8800f3 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetUtils.cs b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetUtils.cs deleted file mode 100644 index f7b2bd82..00000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetUtils.cs +++ /dev/null @@ -1,238 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Net; -using System.Net.Sockets; -using System.Net.NetworkInformation; - -namespace LiteNetLib -{ - /// - /// Address type that you want to receive from NetUtils.GetLocalIp method - /// - [Flags] - public enum LocalAddrType - { - IPv4 = 1, - IPv6 = 2, - All = IPv4 | IPv6 - } - - /// - /// Some specific network utilities - /// - public static class NetUtils - { - private static readonly NetworkSorter NetworkSorter = new NetworkSorter(); - - public static IPEndPoint MakeEndPoint(string hostStr, int port) - { - return new IPEndPoint(ResolveAddress(hostStr), port); - } - - public static IPAddress ResolveAddress(string hostStr) - { - if(hostStr == "localhost") - return IPAddress.Loopback; - - if (!IPAddress.TryParse(hostStr, out var ipAddress)) - { - if (NetManager.IPv6Support) - ipAddress = ResolveAddress(hostStr, AddressFamily.InterNetworkV6); - if (ipAddress == null) - ipAddress = ResolveAddress(hostStr, AddressFamily.InterNetwork); - } - if (ipAddress == null) - throw new ArgumentException("Invalid address: " + hostStr); - - return ipAddress; - } - - public static IPAddress ResolveAddress(string hostStr, AddressFamily addressFamily) - { - IPAddress[] addresses = Dns.GetHostEntry(hostStr).AddressList; - foreach (IPAddress ip in addresses) - { - if (ip.AddressFamily == addressFamily) - { - return ip; - } - } - return null; - } - - /// - /// Get all local ip addresses - /// - /// type of address (IPv4, IPv6 or both) - /// List with all local ip addresses - public static List GetLocalIpList(LocalAddrType addrType) - { - List targetList = new List(); - GetLocalIpList(targetList, addrType); - return targetList; - } - - /// - /// Get all local ip addresses (non alloc version) - /// - /// result list - /// type of address (IPv4, IPv6 or both) - public static void GetLocalIpList(IList targetList, LocalAddrType addrType) - { - bool ipv4 = (addrType & LocalAddrType.IPv4) == LocalAddrType.IPv4; - bool ipv6 = (addrType & LocalAddrType.IPv6) == LocalAddrType.IPv6; - try - { - // Sort networks interfaces so it prefer Wifi over Cellular networks - // Most cellulars networks seems to be incompatible with NAT Punch - var networks = NetworkInterface.GetAllNetworkInterfaces(); - Array.Sort(networks, NetworkSorter); - - foreach (NetworkInterface ni in networks) - { - //Skip loopback and disabled network interfaces - if (ni.NetworkInterfaceType == NetworkInterfaceType.Loopback || - ni.OperationalStatus != OperationalStatus.Up) - continue; - - var ipProps = ni.GetIPProperties(); - - //Skip address without gateway - if (ipProps.GatewayAddresses.Count == 0) - continue; - - foreach (UnicastIPAddressInformation ip in ipProps.UnicastAddresses) - { - var address = ip.Address; - if ((ipv4 && address.AddressFamily == AddressFamily.InterNetwork) || - (ipv6 && address.AddressFamily == AddressFamily.InterNetworkV6)) - targetList.Add(address.ToString()); - } - } - - //Fallback mode (unity android) - if (targetList.Count == 0) - { - IPAddress[] addresses = Dns.GetHostEntry(Dns.GetHostName()).AddressList; - foreach (IPAddress ip in addresses) - { - if((ipv4 && ip.AddressFamily == AddressFamily.InterNetwork) || - (ipv6 && ip.AddressFamily == AddressFamily.InterNetworkV6)) - targetList.Add(ip.ToString()); - } - } - } - catch - { - //ignored - } - - if (targetList.Count == 0) - { - if(ipv4) - targetList.Add("127.0.0.1"); - if(ipv6) - targetList.Add("::1"); - } - } - - private static readonly List IpList = new List(); - /// - /// Get first detected local ip address - /// - /// type of address (IPv4, IPv6 or both) - /// IP address if available. Else - string.Empty - public static string GetLocalIp(LocalAddrType addrType) - { - lock (IpList) - { - IpList.Clear(); - GetLocalIpList(IpList, addrType); - return IpList.Count == 0 ? string.Empty : IpList[0]; - } - } - - // =========================================== - // Internal and debug log related stuff - // =========================================== - internal static void PrintInterfaceInfos() - { - NetDebug.WriteForce(NetLogLevel.Info, $"IPv6Support: { NetManager.IPv6Support}"); - try - { - foreach (NetworkInterface ni in NetworkInterface.GetAllNetworkInterfaces()) - { - foreach (UnicastIPAddressInformation ip in ni.GetIPProperties().UnicastAddresses) - { - if (ip.Address.AddressFamily == AddressFamily.InterNetwork || - ip.Address.AddressFamily == AddressFamily.InterNetworkV6) - { - NetDebug.WriteForce( - NetLogLevel.Info, - $"Interface: {ni.Name}, Type: {ni.NetworkInterfaceType}, Ip: {ip.Address}, OpStatus: {ni.OperationalStatus}"); - } - } - } - } - catch (Exception e) - { - NetDebug.WriteForce(NetLogLevel.Info, $"Error while getting interface infos: {e}"); - } - } - - internal static int RelativeSequenceNumber(int number, int expected) - { - return (number - expected + NetConstants.MaxSequence + NetConstants.HalfMaxSequence) % NetConstants.MaxSequence - NetConstants.HalfMaxSequence; - } - - internal static T[] AllocatePinnedUninitializedArray(int count) where T : unmanaged - { -#if NET5_0_OR_GREATER || NET5_0 - return GC.AllocateUninitializedArray(count, true); -#else - return new T[count]; -#endif - } - } - - // Pick the most obvious choice for the local IP - // Ethernet > Wifi > Others > Cellular - internal class NetworkSorter : IComparer - { - [SuppressMessage("ReSharper", "PossibleNullReferenceException")] - public int Compare(NetworkInterface a, NetworkInterface b) - { - var isCellularA = a.NetworkInterfaceType == NetworkInterfaceType.Wman || - a.NetworkInterfaceType == NetworkInterfaceType.Wwanpp || - a.NetworkInterfaceType == NetworkInterfaceType.Wwanpp2; - - var isCellularB = b.NetworkInterfaceType == NetworkInterfaceType.Wman || - b.NetworkInterfaceType == NetworkInterfaceType.Wwanpp || - b.NetworkInterfaceType == NetworkInterfaceType.Wwanpp2; - - var isWifiA = a.NetworkInterfaceType == NetworkInterfaceType.Wireless80211; - var isWifiB = b.NetworkInterfaceType == NetworkInterfaceType.Wireless80211; - - var isEthernetA = a.NetworkInterfaceType == NetworkInterfaceType.Ethernet || - a.NetworkInterfaceType == NetworkInterfaceType.Ethernet3Megabit || - a.NetworkInterfaceType == NetworkInterfaceType.GigabitEthernet || - a.NetworkInterfaceType == NetworkInterfaceType.FastEthernetFx || - a.NetworkInterfaceType == NetworkInterfaceType.FastEthernetT; - - var isEthernetB = b.NetworkInterfaceType == NetworkInterfaceType.Ethernet || - b.NetworkInterfaceType == NetworkInterfaceType.Ethernet3Megabit || - b.NetworkInterfaceType == NetworkInterfaceType.GigabitEthernet || - b.NetworkInterfaceType == NetworkInterfaceType.FastEthernetFx || - b.NetworkInterfaceType == NetworkInterfaceType.FastEthernetT; - - var isOtherA = !isCellularA && !isWifiA && !isEthernetA; - var isOtherB = !isCellularB && !isWifiB && !isEthernetB; - - var priorityA = isEthernetA ? 3 : isWifiA ? 2 : isOtherA ? 1 : 0; - var priorityB = isEthernetB ? 3 : isWifiB ? 2 : isOtherB ? 1 : 0; - - return priorityA > priorityB ? -1 : priorityA < priorityB ? 1 : 0; - } - } -} diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetUtils.cs.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetUtils.cs.meta deleted file mode 100644 index a7878912..00000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetUtils.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 10fc3a058b2de05d08a25aae8e958d1d -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/PausedSocketFix.cs b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/PausedSocketFix.cs deleted file mode 100644 index dc92122a..00000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/PausedSocketFix.cs +++ /dev/null @@ -1,57 +0,0 @@ -#if UNITY_2018_3_OR_NEWER -using System.Net; - -namespace LiteNetLib -{ - public class PausedSocketFix - { - private readonly NetManager _netManager; - private readonly IPAddress _ipv4; - private readonly IPAddress _ipv6; - private readonly int _port; - private readonly bool _manualMode; - private bool _initialized; - - public PausedSocketFix(NetManager netManager, IPAddress ipv4, IPAddress ipv6, int port, bool manualMode) - { - _netManager = netManager; - _ipv4 = ipv4; - _ipv6 = ipv6; - _port = port; - _manualMode = manualMode; - UnityEngine.Application.focusChanged += Application_focusChanged; - _initialized = true; - } - - public void Deinitialize() - { - if (_initialized) - UnityEngine.Application.focusChanged -= Application_focusChanged; - _initialized = false; - } - - private void Application_focusChanged(bool focused) - { - //If coming back into focus see if a reconnect is needed. - if (focused) - { - //try reconnect - if (!_initialized) - return; - //Was intentionally disconnected at some point. - if (!_netManager.IsRunning) - return; - //Socket is in working state. - if (_netManager.NotConnected == false) - return; - - //Socket isn't running but should be. Try to start again. - if (!_netManager.Start(_ipv4, _ipv6, _port, _manualMode)) - { - NetDebug.WriteError($"[S] Cannot restore connection. Ipv4 {_ipv4}, Ipv6 {_ipv6}, Port {_port}, ManualMode {_manualMode}"); - } - } - } - } -} -#endif diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/PausedSocketFix.cs.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/PausedSocketFix.cs.meta deleted file mode 100644 index abdd2cf8..00000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/PausedSocketFix.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 1c1550912c878cb868a538c7be33778b -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/PooledPacket.cs b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/PooledPacket.cs deleted file mode 100644 index 26ef7bd8..00000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/PooledPacket.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace LiteNetLib -{ - public readonly ref struct PooledPacket - { - internal readonly NetPacket _packet; - internal readonly byte _channelNumber; - - /// - /// Maximum data size that you can put into such packet - /// - public readonly int MaxUserDataSize; - - /// - /// Offset for user data when writing to Data array - /// - public readonly int UserDataOffset; - - /// - /// Raw packet data. Do not modify header! Use UserDataOffset as start point for your data - /// - public byte[] Data => _packet.RawData; - - internal PooledPacket(NetPacket packet, int maxDataSize, byte channelNumber) - { - _packet = packet; - UserDataOffset = _packet.GetHeaderSize(); - _packet.Size = UserDataOffset; - MaxUserDataSize = maxDataSize - UserDataOffset; - _channelNumber = channelNumber; - } - } -} diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/PooledPacket.cs.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/PooledPacket.cs.meta deleted file mode 100644 index a7adb243..00000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/PooledPacket.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: d76ffcb8ea1890343a4b835240331d42 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/ReliableChannel.cs b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/ReliableChannel.cs deleted file mode 100644 index 4a10d170..00000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/ReliableChannel.cs +++ /dev/null @@ -1,337 +0,0 @@ -using System; - -namespace LiteNetLib -{ - internal sealed class ReliableChannel : BaseChannel - { - private struct PendingPacket - { - private NetPacket _packet; - private long _timeStamp; - private bool _isSent; - - public override string ToString() - { - return _packet == null ? "Empty" : _packet.Sequence.ToString(); - } - - public void Init(NetPacket packet) - { - _packet = packet; - _isSent = false; - } - - //Returns true if there is a pending packet inside - public bool TrySend(long currentTime, NetPeer peer) - { - if (_packet == null) - return false; - - if (_isSent) //check send time - { - double resendDelay = peer.ResendDelay * TimeSpan.TicksPerMillisecond; - double packetHoldTime = currentTime - _timeStamp; - if (packetHoldTime < resendDelay) - return true; - NetDebug.Write($"[RC]Resend: {packetHoldTime} > {resendDelay}"); - } - _timeStamp = currentTime; - _isSent = true; - peer.SendUserData(_packet); - return true; - } - - public bool Clear(NetPeer peer) - { - if (_packet != null) - { - peer.RecycleAndDeliver(_packet); - _packet = null; - return true; - } - return false; - } - } - - private readonly NetPacket _outgoingAcks; //for send acks - private readonly PendingPacket[] _pendingPackets; //for unacked packets and duplicates - private readonly NetPacket[] _receivedPackets; //for order - private readonly bool[] _earlyReceived; //for unordered - - private int _localSeqence; - private int _remoteSequence; - private int _localWindowStart; - private int _remoteWindowStart; - - private bool _mustSendAcks; - - private readonly DeliveryMethod _deliveryMethod; - private readonly bool _ordered; - private readonly int _windowSize; - private const int BitsInByte = 8; - private readonly byte _id; - - public ReliableChannel(NetPeer peer, bool ordered, byte id) : base(peer) - { - _id = id; - _windowSize = NetConstants.DefaultWindowSize; - _ordered = ordered; - _pendingPackets = new PendingPacket[_windowSize]; - for (int i = 0; i < _pendingPackets.Length; i++) - _pendingPackets[i] = new PendingPacket(); - - if (_ordered) - { - _deliveryMethod = DeliveryMethod.ReliableOrdered; - _receivedPackets = new NetPacket[_windowSize]; - } - else - { - _deliveryMethod = DeliveryMethod.ReliableUnordered; - _earlyReceived = new bool[_windowSize]; - } - - _localWindowStart = 0; - _localSeqence = 0; - _remoteSequence = 0; - _remoteWindowStart = 0; - _outgoingAcks = new NetPacket(PacketProperty.Ack, (_windowSize - 1) / BitsInByte + 2) {ChannelId = id}; - } - - //ProcessAck in packet - private void ProcessAck(NetPacket packet) - { - if (packet.Size != _outgoingAcks.Size) - { - NetDebug.Write("[PA]Invalid acks packet size"); - return; - } - - ushort ackWindowStart = packet.Sequence; - int windowRel = NetUtils.RelativeSequenceNumber(_localWindowStart, ackWindowStart); - if (ackWindowStart >= NetConstants.MaxSequence || windowRel < 0) - { - NetDebug.Write("[PA]Bad window start"); - return; - } - - //check relevance - if (windowRel >= _windowSize) - { - NetDebug.Write("[PA]Old acks"); - return; - } - - byte[] acksData = packet.RawData; - lock (_pendingPackets) - { - for (int pendingSeq = _localWindowStart; - pendingSeq != _localSeqence; - pendingSeq = (pendingSeq + 1) % NetConstants.MaxSequence) - { - int rel = NetUtils.RelativeSequenceNumber(pendingSeq, ackWindowStart); - if (rel >= _windowSize) - { - NetDebug.Write("[PA]REL: " + rel); - break; - } - - int pendingIdx = pendingSeq % _windowSize; - int currentByte = NetConstants.ChanneledHeaderSize + pendingIdx / BitsInByte; - int currentBit = pendingIdx % BitsInByte; - if ((acksData[currentByte] & (1 << currentBit)) == 0) - { - if (Peer.NetManager.EnableStatistics) - { - Peer.Statistics.IncrementPacketLoss(); - Peer.NetManager.Statistics.IncrementPacketLoss(); - } - - //Skip false ack - NetDebug.Write($"[PA]False ack: {pendingSeq}"); - continue; - } - - if (pendingSeq == _localWindowStart) - { - //Move window - _localWindowStart = (_localWindowStart + 1) % NetConstants.MaxSequence; - } - - //clear packet - if (_pendingPackets[pendingIdx].Clear(Peer)) - NetDebug.Write($"[PA]Removing reliableInOrder ack: {pendingSeq} - true"); - } - } - } - - protected override bool SendNextPackets() - { - if (_mustSendAcks) - { - _mustSendAcks = false; - NetDebug.Write("[RR]SendAcks"); - lock(_outgoingAcks) - Peer.SendUserData(_outgoingAcks); - } - - long currentTime = DateTime.UtcNow.Ticks; - bool hasPendingPackets = false; - - lock (_pendingPackets) - { - //get packets from queue - lock (OutgoingQueue) - { - while (OutgoingQueue.Count > 0) - { - int relate = NetUtils.RelativeSequenceNumber(_localSeqence, _localWindowStart); - if (relate >= _windowSize) - break; - - var netPacket = OutgoingQueue.Dequeue(); - netPacket.Sequence = (ushort) _localSeqence; - netPacket.ChannelId = _id; - _pendingPackets[_localSeqence % _windowSize].Init(netPacket); - _localSeqence = (_localSeqence + 1) % NetConstants.MaxSequence; - } - } - - //send - for (int pendingSeq = _localWindowStart; pendingSeq != _localSeqence; pendingSeq = (pendingSeq + 1) % NetConstants.MaxSequence) - { - // Please note: TrySend is invoked on a mutable struct, it's important to not extract it into a variable here - if (_pendingPackets[pendingSeq % _windowSize].TrySend(currentTime, Peer)) - hasPendingPackets = true; - } - } - - return hasPendingPackets || _mustSendAcks || OutgoingQueue.Count > 0; - } - - //Process incoming packet - public override bool ProcessPacket(NetPacket packet) - { - if (packet.Property == PacketProperty.Ack) - { - ProcessAck(packet); - return false; - } - int seq = packet.Sequence; - if (seq >= NetConstants.MaxSequence) - { - NetDebug.Write("[RR]Bad sequence"); - return false; - } - - int relate = NetUtils.RelativeSequenceNumber(seq, _remoteWindowStart); - int relateSeq = NetUtils.RelativeSequenceNumber(seq, _remoteSequence); - - if (relateSeq > _windowSize) - { - NetDebug.Write("[RR]Bad sequence"); - return false; - } - - //Drop bad packets - if (relate < 0) - { - //Too old packet doesn't ack - NetDebug.Write("[RR]ReliableInOrder too old"); - return false; - } - if (relate >= _windowSize * 2) - { - //Some very new packet - NetDebug.Write("[RR]ReliableInOrder too new"); - return false; - } - - //If very new - move window - int ackIdx; - int ackByte; - int ackBit; - lock (_outgoingAcks) - { - if (relate >= _windowSize) - { - //New window position - int newWindowStart = (_remoteWindowStart + relate - _windowSize + 1) % NetConstants.MaxSequence; - _outgoingAcks.Sequence = (ushort) newWindowStart; - - //Clean old data - while (_remoteWindowStart != newWindowStart) - { - ackIdx = _remoteWindowStart % _windowSize; - ackByte = NetConstants.ChanneledHeaderSize + ackIdx / BitsInByte; - ackBit = ackIdx % BitsInByte; - _outgoingAcks.RawData[ackByte] &= (byte) ~(1 << ackBit); - _remoteWindowStart = (_remoteWindowStart + 1) % NetConstants.MaxSequence; - } - } - - //Final stage - process valid packet - //trigger acks send - _mustSendAcks = true; - - ackIdx = seq % _windowSize; - ackByte = NetConstants.ChanneledHeaderSize + ackIdx / BitsInByte; - ackBit = ackIdx % BitsInByte; - if ((_outgoingAcks.RawData[ackByte] & (1 << ackBit)) != 0) - { - NetDebug.Write("[RR]ReliableInOrder duplicate"); - //because _mustSendAcks == true - AddToPeerChannelSendQueue(); - return false; - } - - //save ack - _outgoingAcks.RawData[ackByte] |= (byte) (1 << ackBit); - } - - AddToPeerChannelSendQueue(); - - //detailed check - if (seq == _remoteSequence) - { - NetDebug.Write("[RR]ReliableInOrder packet succes"); - Peer.AddReliablePacket(_deliveryMethod, packet); - _remoteSequence = (_remoteSequence + 1) % NetConstants.MaxSequence; - - if (_ordered) - { - NetPacket p; - while ((p = _receivedPackets[_remoteSequence % _windowSize]) != null) - { - //process holden packet - _receivedPackets[_remoteSequence % _windowSize] = null; - Peer.AddReliablePacket(_deliveryMethod, p); - _remoteSequence = (_remoteSequence + 1) % NetConstants.MaxSequence; - } - } - else - { - while (_earlyReceived[_remoteSequence % _windowSize]) - { - //process early packet - _earlyReceived[_remoteSequence % _windowSize] = false; - _remoteSequence = (_remoteSequence + 1) % NetConstants.MaxSequence; - } - } - return true; - } - - //holden packet - if (_ordered) - { - _receivedPackets[ackIdx] = packet; - } - else - { - _earlyReceived[ackIdx] = true; - Peer.AddReliablePacket(_deliveryMethod, packet); - } - return true; - } - } -} diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/ReliableChannel.cs.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/ReliableChannel.cs.meta deleted file mode 100644 index 36ab889e..00000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/ReliableChannel.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: df55950d9aab25fdc849b2f3e39cfc29 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/SequencedChannel.cs b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/SequencedChannel.cs deleted file mode 100644 index bc47f86d..00000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/SequencedChannel.cs +++ /dev/null @@ -1,114 +0,0 @@ -using System; - -namespace LiteNetLib -{ - internal sealed class SequencedChannel : BaseChannel - { - private int _localSequence; - private ushort _remoteSequence; - private readonly bool _reliable; - private NetPacket _lastPacket; - private readonly NetPacket _ackPacket; - private bool _mustSendAck; - private readonly byte _id; - private long _lastPacketSendTime; - - public SequencedChannel(NetPeer peer, bool reliable, byte id) : base(peer) - { - _id = id; - _reliable = reliable; - if (_reliable) - _ackPacket = new NetPacket(PacketProperty.Ack, 0) {ChannelId = id}; - } - - protected override bool SendNextPackets() - { - if (_reliable && OutgoingQueue.Count == 0) - { - long currentTime = DateTime.UtcNow.Ticks; - long packetHoldTime = currentTime - _lastPacketSendTime; - if (packetHoldTime >= Peer.ResendDelay * TimeSpan.TicksPerMillisecond) - { - var packet = _lastPacket; - if (packet != null) - { - _lastPacketSendTime = currentTime; - Peer.SendUserData(packet); - } - } - } - else - { - lock (OutgoingQueue) - { - while (OutgoingQueue.Count > 0) - { - NetPacket packet = OutgoingQueue.Dequeue(); - _localSequence = (_localSequence + 1) % NetConstants.MaxSequence; - packet.Sequence = (ushort)_localSequence; - packet.ChannelId = _id; - Peer.SendUserData(packet); - - if (_reliable && OutgoingQueue.Count == 0) - { - _lastPacketSendTime = DateTime.UtcNow.Ticks; - _lastPacket = packet; - } - else - { - Peer.NetManager.PoolRecycle(packet); - } - } - } - } - - if (_reliable && _mustSendAck) - { - _mustSendAck = false; - _ackPacket.Sequence = _remoteSequence; - Peer.SendUserData(_ackPacket); - } - - return _lastPacket != null; - } - - public override bool ProcessPacket(NetPacket packet) - { - if (packet.IsFragmented) - return false; - if (packet.Property == PacketProperty.Ack) - { - if (_reliable && _lastPacket != null && packet.Sequence == _lastPacket.Sequence) - _lastPacket = null; - return false; - } - int relative = NetUtils.RelativeSequenceNumber(packet.Sequence, _remoteSequence); - bool packetProcessed = false; - if (packet.Sequence < NetConstants.MaxSequence && relative > 0) - { - if (Peer.NetManager.EnableStatistics) - { - Peer.Statistics.AddPacketLoss(relative - 1); - Peer.NetManager.Statistics.AddPacketLoss(relative - 1); - } - - _remoteSequence = packet.Sequence; - Peer.NetManager.CreateReceiveEvent( - packet, - _reliable ? DeliveryMethod.ReliableSequenced : DeliveryMethod.Sequenced, - (byte)(packet.ChannelId / NetConstants.ChannelTypeCount), - NetConstants.ChanneledHeaderSize, - Peer); - packetProcessed = true; - } - - if (_reliable) - { - _mustSendAck = true; - AddToPeerChannelSendQueue(); - } - - return packetProcessed; - } - } -} diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/SequencedChannel.cs.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/SequencedChannel.cs.meta deleted file mode 100644 index 6516eaac..00000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/SequencedChannel.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 72093288874fee2cd959001aa3e3a6d4 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils.meta deleted file mode 100644 index cbde7058..00000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: 81d3c59df7f54ac6b8789a9ca6bee25d -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/CRC32C.cs b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/CRC32C.cs deleted file mode 100644 index 7e85680c..00000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/CRC32C.cs +++ /dev/null @@ -1,150 +0,0 @@ -#if NETCOREAPP3_0_OR_GREATER || NETCOREAPP3_1 || NET5_0 -using System; -using System.Runtime.InteropServices; -using System.Runtime.Intrinsics.X86; -#endif -#if NET5_0_OR_GREATER || NET5_0 -using System.Runtime.Intrinsics.Arm; -#endif - -namespace LiteNetLib.Utils -{ - //Implementation from Crc32.NET - public static class CRC32C - { - public const int ChecksumSize = 4; - private const uint Poly = 0x82F63B78u; - private static readonly uint[] Table; - - static CRC32C() - { -#if NETCOREAPP3_0_OR_GREATER || NETCOREAPP3_1 || NET5_0 - if (Sse42.IsSupported) - return; -#endif -#if NET5_0_OR_GREATER || NET5_0 - if (Crc32.IsSupported) - return; -#endif - Table = NetUtils.AllocatePinnedUninitializedArray(16 * 256); - for (uint i = 0; i < 256; i++) - { - uint res = i; - for (int t = 0; t < 16; t++) - { - for (int k = 0; k < 8; k++) - res = (res & 1) == 1 ? Poly ^ (res >> 1) : (res >> 1); - Table[t * 256 + i] = res; - } - } - } - - /// - /// Compute CRC32C for data - /// - /// input data - /// offset - /// length - /// CRC32C checksum - public static uint Compute(byte[] input, int offset, int length) - { - uint crcLocal = uint.MaxValue; -#if NETCOREAPP3_0_OR_GREATER || NETCOREAPP3_1 || NET5_0 - if (Sse42.IsSupported) - { - var data = new ReadOnlySpan(input, offset, length); - int processed = 0; - if (Sse42.X64.IsSupported && data.Length > sizeof(ulong)) - { - processed = data.Length / sizeof(ulong) * sizeof(ulong); - var ulongs = MemoryMarshal.Cast(data.Slice(0, processed)); - ulong crclong = crcLocal; - for (int i = 0; i < ulongs.Length; i++) - { - crclong = Sse42.X64.Crc32(crclong, ulongs[i]); - } - - crcLocal = (uint)crclong; - } - else if (data.Length > sizeof(uint)) - { - processed = data.Length / sizeof(uint) * sizeof(uint); - var uints = MemoryMarshal.Cast(data.Slice(0, processed)); - for (int i = 0; i < uints.Length; i++) - { - crcLocal = Sse42.Crc32(crcLocal, uints[i]); - } - } - - for (int i = processed; i < data.Length; i++) - { - crcLocal = Sse42.Crc32(crcLocal, data[i]); - } - - return crcLocal ^ uint.MaxValue; - } -#endif -#if NET5_0_OR_GREATER || NET5_0 - if (Crc32.IsSupported) - { - var data = new ReadOnlySpan(input, offset, length); - int processed = 0; - if (Crc32.Arm64.IsSupported && data.Length > sizeof(ulong)) - { - processed = data.Length / sizeof(ulong) * sizeof(ulong); - var ulongs = MemoryMarshal.Cast(data.Slice(0, processed)); - for (int i = 0; i < ulongs.Length; i++) - { - crcLocal = Crc32.Arm64.ComputeCrc32C(crcLocal, ulongs[i]); - } - } - else if (data.Length > sizeof(uint)) - { - processed = data.Length / sizeof(uint) * sizeof(uint); - var uints = MemoryMarshal.Cast(data.Slice(0, processed)); - for (int i = 0; i < uints.Length; i++) - { - crcLocal = Crc32.ComputeCrc32C(crcLocal, uints[i]); - } - } - - for (int i = processed; i < data.Length; i++) - { - crcLocal = Crc32.ComputeCrc32C(crcLocal, data[i]); - } - - return crcLocal ^ uint.MaxValue; - } -#endif - while (length >= 16) - { - var a = Table[(3 * 256) + input[offset + 12]] - ^ Table[(2 * 256) + input[offset + 13]] - ^ Table[(1 * 256) + input[offset + 14]] - ^ Table[(0 * 256) + input[offset + 15]]; - - var b = Table[(7 * 256) + input[offset + 8]] - ^ Table[(6 * 256) + input[offset + 9]] - ^ Table[(5 * 256) + input[offset + 10]] - ^ Table[(4 * 256) + input[offset + 11]]; - - var c = Table[(11 * 256) + input[offset + 4]] - ^ Table[(10 * 256) + input[offset + 5]] - ^ Table[(9 * 256) + input[offset + 6]] - ^ Table[(8 * 256) + input[offset + 7]]; - - var d = Table[(15 * 256) + ((byte)crcLocal ^ input[offset])] - ^ Table[(14 * 256) + ((byte)(crcLocal >> 8) ^ input[offset + 1])] - ^ Table[(13 * 256) + ((byte)(crcLocal >> 16) ^ input[offset + 2])] - ^ Table[(12 * 256) + ((crcLocal >> 24) ^ input[offset + 3])]; - - crcLocal = d ^ c ^ b ^ a; - offset += 16; - length -= 16; - } - while (--length >= 0) - crcLocal = Table[(byte)(crcLocal ^ input[offset++])] ^ crcLocal >> 8; - return crcLocal ^ uint.MaxValue; - } - } -} diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/CRC32C.cs.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/CRC32C.cs.meta deleted file mode 100644 index aa0417ee..00000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/CRC32C.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: a28b2ade64d53b3a4a303a62169c4748 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/FastBitConverter.cs b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/FastBitConverter.cs deleted file mode 100644 index 3ecd10c7..00000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/FastBitConverter.cs +++ /dev/null @@ -1,175 +0,0 @@ -using System; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -namespace LiteNetLib.Utils -{ - public static class FastBitConverter - { -#if (LITENETLIB_UNSAFE || LITENETLIB_UNSAFELIB || NETCOREAPP3_1 || NET5_0 || NETCOREAPP3_0_OR_GREATER) && !BIGENDIAN -#if LITENETLIB_UNSAFE - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static unsafe void GetBytes(byte[] bytes, int startIndex, T value) where T : unmanaged - { - int size = sizeof(T); - if (bytes.Length < startIndex + size) - ThrowIndexOutOfRangeException(); -#if LITENETLIB_UNSAFELIB || NETCOREAPP3_1 || NET5_0 || NETCOREAPP3_0_OR_GREATER - Unsafe.As(ref bytes[startIndex]) = value; -#else - fixed (byte* ptr = &bytes[startIndex]) - { -#if UNITY_ANDROID - // On some android systems, assigning *(T*)ptr throws a NRE if - // the ptr isn't aligned (i.e. if Position is 1,2,3,5, etc.). - // Here we have to use memcpy. - // - // => we can't get a pointer of a struct in C# without - // marshalling allocations - // => instead, we stack allocate an array of type T and use that - // => stackalloc avoids GC and is very fast. it only works for - // value types, but all blittable types are anyway. - T* valueBuffer = stackalloc T[1] { value }; - UnsafeUtility.MemCpy(ptr, valueBuffer, size); -#else - *(T*)ptr = value; -#endif - } -#endif - } -#else - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void GetBytes(byte[] bytes, int startIndex, T value) where T : unmanaged - { - if (bytes.Length < startIndex + Unsafe.SizeOf()) - ThrowIndexOutOfRangeException(); - Unsafe.As(ref bytes[startIndex]) = value; - } -#endif - - private static void ThrowIndexOutOfRangeException() => throw new IndexOutOfRangeException(); -#else - [StructLayout(LayoutKind.Explicit)] - private struct ConverterHelperDouble - { - [FieldOffset(0)] - public ulong Along; - - [FieldOffset(0)] - public double Adouble; - } - - [StructLayout(LayoutKind.Explicit)] - private struct ConverterHelperFloat - { - [FieldOffset(0)] - public int Aint; - - [FieldOffset(0)] - public float Afloat; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void WriteLittleEndian(byte[] buffer, int offset, ulong data) - { -#if BIGENDIAN - buffer[offset + 7] = (byte)(data); - buffer[offset + 6] = (byte)(data >> 8); - buffer[offset + 5] = (byte)(data >> 16); - buffer[offset + 4] = (byte)(data >> 24); - buffer[offset + 3] = (byte)(data >> 32); - buffer[offset + 2] = (byte)(data >> 40); - buffer[offset + 1] = (byte)(data >> 48); - buffer[offset ] = (byte)(data >> 56); -#else - buffer[offset] = (byte)(data); - buffer[offset + 1] = (byte)(data >> 8); - buffer[offset + 2] = (byte)(data >> 16); - buffer[offset + 3] = (byte)(data >> 24); - buffer[offset + 4] = (byte)(data >> 32); - buffer[offset + 5] = (byte)(data >> 40); - buffer[offset + 6] = (byte)(data >> 48); - buffer[offset + 7] = (byte)(data >> 56); -#endif - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void WriteLittleEndian(byte[] buffer, int offset, int data) - { -#if BIGENDIAN - buffer[offset + 3] = (byte)(data); - buffer[offset + 2] = (byte)(data >> 8); - buffer[offset + 1] = (byte)(data >> 16); - buffer[offset ] = (byte)(data >> 24); -#else - buffer[offset] = (byte)(data); - buffer[offset + 1] = (byte)(data >> 8); - buffer[offset + 2] = (byte)(data >> 16); - buffer[offset + 3] = (byte)(data >> 24); -#endif - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteLittleEndian(byte[] buffer, int offset, short data) - { -#if BIGENDIAN - buffer[offset + 1] = (byte)(data); - buffer[offset ] = (byte)(data >> 8); -#else - buffer[offset] = (byte)(data); - buffer[offset + 1] = (byte)(data >> 8); -#endif - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void GetBytes(byte[] bytes, int startIndex, double value) - { - ConverterHelperDouble ch = new ConverterHelperDouble { Adouble = value }; - WriteLittleEndian(bytes, startIndex, ch.Along); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void GetBytes(byte[] bytes, int startIndex, float value) - { - ConverterHelperFloat ch = new ConverterHelperFloat { Afloat = value }; - WriteLittleEndian(bytes, startIndex, ch.Aint); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void GetBytes(byte[] bytes, int startIndex, short value) - { - WriteLittleEndian(bytes, startIndex, value); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void GetBytes(byte[] bytes, int startIndex, ushort value) - { - WriteLittleEndian(bytes, startIndex, (short)value); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void GetBytes(byte[] bytes, int startIndex, int value) - { - WriteLittleEndian(bytes, startIndex, value); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void GetBytes(byte[] bytes, int startIndex, uint value) - { - WriteLittleEndian(bytes, startIndex, (int)value); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void GetBytes(byte[] bytes, int startIndex, long value) - { - WriteLittleEndian(bytes, startIndex, (ulong)value); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void GetBytes(byte[] bytes, int startIndex, ulong value) - { - WriteLittleEndian(bytes, startIndex, value); - } -#endif - } -} diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/FastBitConverter.cs.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/FastBitConverter.cs.meta deleted file mode 100644 index 71323b0b..00000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/FastBitConverter.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: d225fec7b996f854daaf90b7cab04195 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/INetSerializable.cs b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/INetSerializable.cs deleted file mode 100644 index 92f14bee..00000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/INetSerializable.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace LiteNetLib.Utils -{ - public interface INetSerializable - { - void Serialize(NetDataWriter writer); - void Deserialize(NetDataReader reader); - } -} diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/INetSerializable.cs.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/INetSerializable.cs.meta deleted file mode 100644 index 52e88c1a..00000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/INetSerializable.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 9944c6f5e5d319163997f755bf15fcca -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NetDataReader.cs b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NetDataReader.cs deleted file mode 100644 index 6ddab0e5..00000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NetDataReader.cs +++ /dev/null @@ -1,673 +0,0 @@ -using System; -using System.Net; -using System.Runtime.CompilerServices; - -namespace LiteNetLib.Utils -{ - public class NetDataReader - { - protected byte[] _data; - protected int _position; - protected int _dataSize; - private int _offset; - - public byte[] RawData - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => _data; - } - public int RawDataSize - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => _dataSize; - } - public int UserDataOffset - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => _offset; - } - public int UserDataSize - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => _dataSize - _offset; - } - public bool IsNull - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => _data == null; - } - public int Position - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => _position; - } - public bool EndOfData - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => _position == _dataSize; - } - public int AvailableBytes - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => _dataSize - _position; - } - - public void SkipBytes(int count) - { - _position += count; - } - - public void SetPosition(int position) - { - _position = position; - } - - public void SetSource(NetDataWriter dataWriter) - { - _data = dataWriter.Data; - _position = 0; - _offset = 0; - _dataSize = dataWriter.Length; - } - - public void SetSource(byte[] source) - { - _data = source; - _position = 0; - _offset = 0; - _dataSize = source.Length; - } - - public void SetSource(byte[] source, int offset, int maxSize) - { - _data = source; - _position = offset; - _offset = offset; - _dataSize = maxSize; - } - - public NetDataReader() - { - - } - - public NetDataReader(NetDataWriter writer) - { - SetSource(writer); - } - - public NetDataReader(byte[] source) - { - SetSource(source); - } - - public NetDataReader(byte[] source, int offset, int maxSize) - { - SetSource(source, offset, maxSize); - } - - #region GetMethods - public IPEndPoint GetNetEndPoint() - { - string host = GetString(1000); - int port = GetInt(); - return NetUtils.MakeEndPoint(host, port); - } - - public byte GetByte() - { - byte res = _data[_position]; - _position++; - return res; - } - - public sbyte GetSByte() - { - return (sbyte)GetByte(); - } - - public T[] GetArray(ushort size) - { - ushort length = BitConverter.ToUInt16(_data, _position); - _position += 2; - T[] result = new T[length]; - length *= size; - Buffer.BlockCopy(_data, _position, result, 0, length); - _position += length; - return result; - } - - public bool[] GetBoolArray() - { - return GetArray(1); - } - - public ushort[] GetUShortArray() - { - return GetArray(2); - } - - public short[] GetShortArray() - { - return GetArray(2); - } - - public int[] GetIntArray() - { - return GetArray(4); - } - - public uint[] GetUIntArray() - { - return GetArray(4); - } - - public float[] GetFloatArray() - { - return GetArray(4); - } - - public double[] GetDoubleArray() - { - return GetArray(8); - } - - public long[] GetLongArray() - { - return GetArray(8); - } - - public ulong[] GetULongArray() - { - return GetArray(8); - } - - public string[] GetStringArray() - { - ushort length = GetUShort(); - string[] arr = new string[length]; - for (int i = 0; i < length; i++) - { - arr[i] = GetString(); - } - return arr; - } - - /// - /// Note that "maxStringLength" only limits the number of characters in a string, not its size in bytes. - /// Strings that exceed this parameter are returned as empty - /// - public string[] GetStringArray(int maxStringLength) - { - ushort length = GetUShort(); - string[] arr = new string[length]; - for (int i = 0; i < length; i++) - { - arr[i] = GetString(maxStringLength); - } - return arr; - } - - public bool GetBool() - { - return GetByte() == 1; - } - - public char GetChar() - { - return (char)GetUShort(); - } - - public ushort GetUShort() - { - ushort result = BitConverter.ToUInt16(_data, _position); - _position += 2; - return result; - } - - public short GetShort() - { - short result = BitConverter.ToInt16(_data, _position); - _position += 2; - return result; - } - - public long GetLong() - { - long result = BitConverter.ToInt64(_data, _position); - _position += 8; - return result; - } - - public ulong GetULong() - { - ulong result = BitConverter.ToUInt64(_data, _position); - _position += 8; - return result; - } - - public int GetInt() - { - int result = BitConverter.ToInt32(_data, _position); - _position += 4; - return result; - } - - public uint GetUInt() - { - uint result = BitConverter.ToUInt32(_data, _position); - _position += 4; - return result; - } - - public float GetFloat() - { - float result = BitConverter.ToSingle(_data, _position); - _position += 4; - return result; - } - - public double GetDouble() - { - double result = BitConverter.ToDouble(_data, _position); - _position += 8; - return result; - } - - /// - /// Note that "maxLength" only limits the number of characters in a string, not its size in bytes. - /// - /// "string.Empty" if value > "maxLength" - public string GetString(int maxLength) - { - ushort size = GetUShort(); - if (size == 0) - { - return string.Empty; - } - - int actualSize = size - 1; - if (actualSize >= NetDataWriter.StringBufferMaxLength) - { - return null; - } - - ArraySegment data = GetBytesSegment(actualSize); - - return (maxLength > 0 && NetDataWriter.uTF8Encoding.Value.GetCharCount(data.Array, data.Offset, data.Count) > maxLength) ? - string.Empty : - NetDataWriter.uTF8Encoding.Value.GetString(data.Array, data.Offset, data.Count); - } - - public string GetString() - { - ushort size = GetUShort(); - if (size == 0) - { - return string.Empty; - } - - int actualSize = size - 1; - if (actualSize >= NetDataWriter.StringBufferMaxLength) - { - return null; - } - - ArraySegment data = GetBytesSegment(actualSize); - - return NetDataWriter.uTF8Encoding.Value.GetString(data.Array, data.Offset, data.Count); - } - - public ArraySegment GetBytesSegment(int count) - { - ArraySegment segment = new ArraySegment(_data, _position, count); - _position += count; - return segment; - } - - public ArraySegment GetRemainingBytesSegment() - { - ArraySegment segment = new ArraySegment(_data, _position, AvailableBytes); - _position = _data.Length; - return segment; - } - - public T Get() where T : struct, INetSerializable - { - var obj = default(T); - obj.Deserialize(this); - return obj; - } - - public T Get(Func constructor) where T : class, INetSerializable - { - var obj = constructor(); - obj.Deserialize(this); - return obj; - } - - public byte[] GetRemainingBytes() - { - byte[] outgoingData = new byte[AvailableBytes]; - Buffer.BlockCopy(_data, _position, outgoingData, 0, AvailableBytes); - _position = _data.Length; - return outgoingData; - } - - public void GetBytes(byte[] destination, int start, int count) - { - Buffer.BlockCopy(_data, _position, destination, start, count); - _position += count; - } - - public void GetBytes(byte[] destination, int count) - { - Buffer.BlockCopy(_data, _position, destination, 0, count); - _position += count; - } - - public sbyte[] GetSBytesWithLength() - { - return GetArray(1); - } - - public byte[] GetBytesWithLength() - { - return GetArray(1); - } - #endregion - - #region PeekMethods - - public byte PeekByte() - { - return _data[_position]; - } - - public sbyte PeekSByte() - { - return (sbyte)_data[_position]; - } - - public bool PeekBool() - { - return _data[_position] == 1; - } - - public char PeekChar() - { - return (char)PeekUShort(); - } - - public ushort PeekUShort() - { - return BitConverter.ToUInt16(_data, _position); - } - - public short PeekShort() - { - return BitConverter.ToInt16(_data, _position); - } - - public long PeekLong() - { - return BitConverter.ToInt64(_data, _position); - } - - public ulong PeekULong() - { - return BitConverter.ToUInt64(_data, _position); - } - - public int PeekInt() - { - return BitConverter.ToInt32(_data, _position); - } - - public uint PeekUInt() - { - return BitConverter.ToUInt32(_data, _position); - } - - public float PeekFloat() - { - return BitConverter.ToSingle(_data, _position); - } - - public double PeekDouble() - { - return BitConverter.ToDouble(_data, _position); - } - - /// - /// Note that "maxLength" only limits the number of characters in a string, not its size in bytes. - /// - public string PeekString(int maxLength) - { - ushort size = PeekUShort(); - if (size == 0) - { - return string.Empty; - } - - int actualSize = size - 1; - if (actualSize >= NetDataWriter.StringBufferMaxLength) - { - return null; - } - - return (maxLength > 0 && NetDataWriter.uTF8Encoding.Value.GetCharCount(_data, _position + 2, actualSize) > maxLength) ? - string.Empty : - NetDataWriter.uTF8Encoding.Value.GetString(_data, _position + 2, actualSize); - } - - public string PeekString() - { - ushort size = PeekUShort(); - if (size == 0) - { - return string.Empty; - } - - int actualSize = size - 1; - if (actualSize >= NetDataWriter.StringBufferMaxLength) - { - return null; - } - - return NetDataWriter.uTF8Encoding.Value.GetString(_data, _position + 2, actualSize); - } - #endregion - - #region TryGetMethods - public bool TryGetByte(out byte result) - { - if (AvailableBytes >= 1) - { - result = GetByte(); - return true; - } - result = 0; - return false; - } - - public bool TryGetSByte(out sbyte result) - { - if (AvailableBytes >= 1) - { - result = GetSByte(); - return true; - } - result = 0; - return false; - } - - public bool TryGetBool(out bool result) - { - if (AvailableBytes >= 1) - { - result = GetBool(); - return true; - } - result = false; - return false; - } - - public bool TryGetChar(out char result) - { - if (!TryGetUShort(out ushort uShortValue)) - { - result = '\0'; - return false; - } - result = (char)uShortValue; - return true; - } - - public bool TryGetShort(out short result) - { - if (AvailableBytes >= 2) - { - result = GetShort(); - return true; - } - result = 0; - return false; - } - - public bool TryGetUShort(out ushort result) - { - if (AvailableBytes >= 2) - { - result = GetUShort(); - return true; - } - result = 0; - return false; - } - - public bool TryGetInt(out int result) - { - if (AvailableBytes >= 4) - { - result = GetInt(); - return true; - } - result = 0; - return false; - } - - public bool TryGetUInt(out uint result) - { - if (AvailableBytes >= 4) - { - result = GetUInt(); - return true; - } - result = 0; - return false; - } - - public bool TryGetLong(out long result) - { - if (AvailableBytes >= 8) - { - result = GetLong(); - return true; - } - result = 0; - return false; - } - - public bool TryGetULong(out ulong result) - { - if (AvailableBytes >= 8) - { - result = GetULong(); - return true; - } - result = 0; - return false; - } - - public bool TryGetFloat(out float result) - { - if (AvailableBytes >= 4) - { - result = GetFloat(); - return true; - } - result = 0; - return false; - } - - public bool TryGetDouble(out double result) - { - if (AvailableBytes >= 8) - { - result = GetDouble(); - return true; - } - result = 0; - return false; - } - - public bool TryGetString(out string result) - { - if (AvailableBytes >= 2) - { - ushort strSize = PeekUShort(); - if (AvailableBytes >= strSize + 1) - { - result = GetString(); - return true; - } - } - result = null; - return false; - } - - public bool TryGetStringArray(out string[] result) - { - if (!TryGetUShort(out ushort strArrayLength)) { - result = null; - return false; - } - - result = new string[strArrayLength]; - for (int i = 0; i < strArrayLength; i++) - { - if (!TryGetString(out result[i])) - { - result = null; - return false; - } - } - - return true; - } - - public bool TryGetBytesWithLength(out byte[] result) - { - if (AvailableBytes >= 2) - { - ushort length = PeekUShort(); - if (length >= 0 && AvailableBytes >= 2 + length) - { - result = GetBytesWithLength(); - return true; - } - } - result = null; - return false; - } - #endregion - - public void Clear() - { - _position = 0; - _dataSize = 0; - _data = null; - } - } -} diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NetDataReader.cs.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NetDataReader.cs.meta deleted file mode 100644 index 8354523b..00000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NetDataReader.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 7708ab193d4292974897437d01aa2a10 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NetDataWriter.cs b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NetDataWriter.cs deleted file mode 100644 index baa8351a..00000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NetDataWriter.cs +++ /dev/null @@ -1,381 +0,0 @@ -using System; -using System.Net; -using System.Runtime.CompilerServices; -using System.Text; -using System.Threading; - -namespace LiteNetLib.Utils -{ - public class NetDataWriter - { - protected byte[] _data; - protected int _position; - private const int InitialSize = 64; - private readonly bool _autoResize; - - public int Capacity - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => _data.Length; - } - public byte[] Data - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => _data; - } - public int Length - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => _position; - } - - public static readonly ThreadLocal uTF8Encoding = new ThreadLocal(() => new UTF8Encoding(false, true)); - public const int StringBufferMaxLength = 65535; - private readonly byte[] _stringBuffer = new byte[StringBufferMaxLength]; - - public NetDataWriter() : this(true, InitialSize) - { - } - - public NetDataWriter(bool autoResize) : this(autoResize, InitialSize) - { - } - - public NetDataWriter(bool autoResize, int initialSize) - { - _data = new byte[initialSize]; - _autoResize = autoResize; - } - - /// - /// Creates NetDataWriter from existing ByteArray - /// - /// Source byte array - /// Copy array to new location or use existing - public static NetDataWriter FromBytes(byte[] bytes, bool copy) - { - if (copy) - { - var netDataWriter = new NetDataWriter(true, bytes.Length); - netDataWriter.Put(bytes); - return netDataWriter; - } - return new NetDataWriter(true, 0) {_data = bytes, _position = bytes.Length}; - } - - /// - /// Creates NetDataWriter from existing ByteArray (always copied data) - /// - /// Source byte array - /// Offset of array - /// Length of array - public static NetDataWriter FromBytes(byte[] bytes, int offset, int length) - { - var netDataWriter = new NetDataWriter(true, bytes.Length); - netDataWriter.Put(bytes, offset, length); - return netDataWriter; - } - - public static NetDataWriter FromString(string value) - { - var netDataWriter = new NetDataWriter(); - netDataWriter.Put(value); - return netDataWriter; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void ResizeIfNeed(int newSize) - { - if (_data.Length < newSize) - { - Array.Resize(ref _data, Math.Max(newSize, _data.Length * 2)); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void EnsureFit(int additionalSize) - { - if (_data.Length < _position + additionalSize) - { - Array.Resize(ref _data, Math.Max(_position + additionalSize, _data.Length * 2)); - } - } - - public void Reset(int size) - { - ResizeIfNeed(size); - _position = 0; - } - - public void Reset() - { - _position = 0; - } - - public byte[] CopyData() - { - byte[] resultData = new byte[_position]; - Buffer.BlockCopy(_data, 0, resultData, 0, _position); - return resultData; - } - - /// - /// Sets position of NetDataWriter to rewrite previous values - /// - /// new byte position - /// previous position of data writer - public int SetPosition(int position) - { - int prevPosition = _position; - _position = position; - return prevPosition; - } - - public void Put(float value) - { - if (_autoResize) - ResizeIfNeed(_position + 4); - FastBitConverter.GetBytes(_data, _position, value); - _position += 4; - } - - public void Put(double value) - { - if (_autoResize) - ResizeIfNeed(_position + 8); - FastBitConverter.GetBytes(_data, _position, value); - _position += 8; - } - - public void Put(long value) - { - if (_autoResize) - ResizeIfNeed(_position + 8); - FastBitConverter.GetBytes(_data, _position, value); - _position += 8; - } - - public void Put(ulong value) - { - if (_autoResize) - ResizeIfNeed(_position + 8); - FastBitConverter.GetBytes(_data, _position, value); - _position += 8; - } - - public void Put(int value) - { - if (_autoResize) - ResizeIfNeed(_position + 4); - FastBitConverter.GetBytes(_data, _position, value); - _position += 4; - } - - public void Put(uint value) - { - if (_autoResize) - ResizeIfNeed(_position + 4); - FastBitConverter.GetBytes(_data, _position, value); - _position += 4; - } - - public void Put(char value) - { - Put((ushort)value); - } - - public void Put(ushort value) - { - if (_autoResize) - ResizeIfNeed(_position + 2); - FastBitConverter.GetBytes(_data, _position, value); - _position += 2; - } - - public void Put(short value) - { - if (_autoResize) - ResizeIfNeed(_position + 2); - FastBitConverter.GetBytes(_data, _position, value); - _position += 2; - } - - public void Put(sbyte value) - { - if (_autoResize) - ResizeIfNeed(_position + 1); - _data[_position] = (byte)value; - _position++; - } - - public void Put(byte value) - { - if (_autoResize) - ResizeIfNeed(_position + 1); - _data[_position] = value; - _position++; - } - - public void Put(byte[] data, int offset, int length) - { - if (_autoResize) - ResizeIfNeed(_position + length); - Buffer.BlockCopy(data, offset, _data, _position, length); - _position += length; - } - - public void Put(byte[] data) - { - if (_autoResize) - ResizeIfNeed(_position + data.Length); - Buffer.BlockCopy(data, 0, _data, _position, data.Length); - _position += data.Length; - } - - public void PutSBytesWithLength(sbyte[] data, int offset, ushort length) - { - if (_autoResize) - ResizeIfNeed(_position + 2 + length); - FastBitConverter.GetBytes(_data, _position, length); - Buffer.BlockCopy(data, offset, _data, _position + 2, length); - _position += 2 + length; - } - - public void PutSBytesWithLength(sbyte[] data) - { - PutArray(data, 1); - } - - public void PutBytesWithLength(byte[] data, int offset, ushort length) - { - if (_autoResize) - ResizeIfNeed(_position + 2 + length); - FastBitConverter.GetBytes(_data, _position, length); - Buffer.BlockCopy(data, offset, _data, _position + 2, length); - _position += 2 + length; - } - - public void PutBytesWithLength(byte[] data) - { - PutArray(data, 1); - } - - public void Put(bool value) - { - Put((byte)(value ? 1 : 0)); - } - - public void PutArray(Array arr, int sz) - { - ushort length = arr == null ? (ushort) 0 : (ushort)arr.Length; - sz *= length; - if (_autoResize) - ResizeIfNeed(_position + sz + 2); - FastBitConverter.GetBytes(_data, _position, length); - if (arr != null) - Buffer.BlockCopy(arr, 0, _data, _position + 2, sz); - _position += sz + 2; - } - - public void PutArray(float[] value) - { - PutArray(value, 4); - } - - public void PutArray(double[] value) - { - PutArray(value, 8); - } - - public void PutArray(long[] value) - { - PutArray(value, 8); - } - - public void PutArray(ulong[] value) - { - PutArray(value, 8); - } - - public void PutArray(int[] value) - { - PutArray(value, 4); - } - - public void PutArray(uint[] value) - { - PutArray(value, 4); - } - - public void PutArray(ushort[] value) - { - PutArray(value, 2); - } - - public void PutArray(short[] value) - { - PutArray(value, 2); - } - - public void PutArray(bool[] value) - { - PutArray(value, 1); - } - - public void PutArray(string[] value) - { - ushort strArrayLength = value == null ? (ushort)0 : (ushort)value.Length; - Put(strArrayLength); - for (int i = 0; i < strArrayLength; i++) - Put(value[i]); - } - - public void PutArray(string[] value, int strMaxLength) - { - ushort strArrayLength = value == null ? (ushort)0 : (ushort)value.Length; - Put(strArrayLength); - for (int i = 0; i < strArrayLength; i++) - Put(value[i], strMaxLength); - } - - public void Put(IPEndPoint endPoint) - { - Put(endPoint.Address.ToString()); - Put(endPoint.Port); - } - - public void Put(string value) - { - Put(value, 0); - } - - /// - /// Note that "maxLength" only limits the number of characters in a string, not its size in bytes. - /// - public void Put(string value, int maxLength) - { - if (string.IsNullOrEmpty(value)) - { - Put((ushort)0); - return; - } - - int length = maxLength > 0 && value.Length > maxLength ? maxLength : value.Length; - int size = uTF8Encoding.Value.GetBytes(value, 0, length, _stringBuffer, 0); - - if (size == 0 || size >= StringBufferMaxLength) - { - Put((ushort)0); - return; - } - - Put(checked((ushort)(size + 1))); - Put(_stringBuffer, 0, size); - } - - public void Put(T obj) where T : INetSerializable - { - obj.Serialize(this); - } - } -} diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NetDataWriter.cs.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NetDataWriter.cs.meta deleted file mode 100644 index 212eeeeb..00000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NetDataWriter.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: b6b2e544f8091647e9f3de009f7eb041 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NetPacketProcessor.cs b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NetPacketProcessor.cs deleted file mode 100644 index c8e29bc0..00000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NetPacketProcessor.cs +++ /dev/null @@ -1,289 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace LiteNetLib.Utils -{ - public class NetPacketProcessor - { - private static IReadOnlyDictionary PacketIdDict; - - public static byte PacketCount => (byte)(PacketIdDict?.Count ?? 0); - public static byte CurrentlyProcessingPacket { get; private set; } - - public static IReadOnlyDictionary RegisterPacketTypes() - { - Type[] packetTypes = AppDomain.CurrentDomain - .GetAssemblies() - .SelectMany(a => a.GetTypes()) - .Where(t => t.Namespace?.StartsWith("Multiplayer.Networking.Packets") == true || (t.Namespace == "LiteNetLib" && t.Name.EndsWith("Packet"))) - .OrderBy(t => t.FullName) - .ToArray(); - - if (packetTypes.Length > byte.MaxValue) - throw new OverflowException($"There's more than {byte.MaxValue} packet types!"); - - PacketIdDict = packetTypes - .Select((t, i) => new { Key = t, Value = (byte)i }) - .ToDictionary(x => x.Key, x => x.Value); - - return PacketIdDict; - } - - protected delegate void SubscribeDelegate(NetDataReader reader, object userData); - - private readonly NetManager _netManager; - private readonly NetSerializer _netSerializer; - private readonly Dictionary _callbacks = new Dictionary(); - - public NetPacketProcessor(NetManager netManager) - { - _netManager = netManager; - _netSerializer = new NetSerializer(); - } - - public NetPacketProcessor(NetManager netManager, int maxStringLength) - { - _netManager = netManager; - _netSerializer = new NetSerializer(maxStringLength); - } - - private static byte GetId() - { - if (PacketIdDict.TryGetValue(typeof(T), out byte id)) - return id; - throw new ArgumentException($"Failed to find packet ID for {typeof(T)}"); - } - - protected SubscribeDelegate GetCallbackFromData(NetDataReader reader) - { - byte id = reader.GetByte(); - CurrentlyProcessingPacket = id; - if (!_callbacks.TryGetValue(id, out var action)) - { - throw new ParseException($"Undefined packet {id} in NetDataReader"); - } - return action; - } - - private static void WriteId(NetDataWriter writer) - { - writer.Put(GetId()); - } - - /// - /// Register nested property type - /// - /// INetSerializable structure - public void RegisterNestedType() where T : struct, INetSerializable - { - _netSerializer.RegisterNestedType(); - } - - /// - /// Register nested property type - /// - /// - /// - public void RegisterNestedType(Action writeDelegate, Func readDelegate) - { - _netSerializer.RegisterNestedType(writeDelegate, readDelegate); - } - - /// - /// Register nested property type - /// - /// INetSerializable class - public void RegisterNestedType(Func constructor) where T : class, INetSerializable - { - _netSerializer.RegisterNestedType(constructor); - } - - /// - /// Reads all available data from NetDataReader and calls OnReceive delegates - /// - /// NetDataReader with packets data - public void ReadAllPackets(NetDataReader reader) - { - while (reader.AvailableBytes > 0) - ReadPacket(reader); - } - - /// - /// Reads all available data from NetDataReader and calls OnReceive delegates - /// - /// NetDataReader with packets data - /// Argument that passed to OnReceivedEvent - /// Malformed packet - public void ReadAllPackets(NetDataReader reader, object userData) - { - while (reader.AvailableBytes > 0) - ReadPacket(reader, userData); - } - - /// - /// Reads one packet from NetDataReader and calls OnReceive delegate - /// - /// NetDataReader with packet - /// Malformed packet - public void ReadPacket(NetDataReader reader) - { - ReadPacket(reader, null); - } - - public void Write(NetDataWriter writer, T packet) where T : class, new() - { - WriteId(writer); - _netSerializer.Serialize(writer, packet); - if (!_netManager.EnableStatistics) - return; - _netManager.Statistics.IncrementPacketsWritten(GetId()); - _netManager.Statistics.AddBytesWritten(GetId(), writer.Length); - } - - public void WriteNetSerializable(NetDataWriter writer, ref T packet) where T : INetSerializable - { - WriteId(writer); - packet.Serialize(writer); - if (!_netManager.EnableStatistics) - return; - _netManager.Statistics.IncrementPacketsWritten(GetId()); - _netManager.Statistics.AddBytesWritten(GetId(), writer.Length); - } - - /// - /// Reads one packet from NetDataReader and calls OnReceive delegate - /// - /// NetDataReader with packet - /// Argument that passed to OnReceivedEvent - /// Malformed packet - public void ReadPacket(NetDataReader reader, object userData) - { - GetCallbackFromData(reader)(reader, userData); - } - - /// - /// Register and subscribe to packet receive event - /// - /// event that will be called when packet deserialized with ReadPacket method - /// Method that constructs packet instead of slow Activator.CreateInstance - /// 's fields are not supported, or it has no fields - public void Subscribe(Action onReceive, Func packetConstructor) where T : class, new() - { - _netSerializer.Register(); - _callbacks[GetId()] = (reader, userData) => - { - var reference = packetConstructor(); - _netSerializer.Deserialize(reader, reference); - onReceive(reference); - }; - } - - /// - /// Register and subscribe to packet receive event (with userData) - /// - /// event that will be called when packet deserialized with ReadPacket method - /// Method that constructs packet instead of slow Activator.CreateInstance - /// 's fields are not supported, or it has no fields - public void Subscribe(Action onReceive, Func packetConstructor) where T : class, new() - { - _netSerializer.Register(); - _callbacks[GetId()] = (reader, userData) => - { - var reference = packetConstructor(); - _netSerializer.Deserialize(reader, reference); - onReceive(reference, (TUserData)userData); - }; - } - - /// - /// Register and subscribe to packet receive event - /// This method will overwrite last received packet class on receive (less garbage) - /// - /// event that will be called when packet deserialized with ReadPacket method - /// 's fields are not supported, or it has no fields - public void SubscribeReusable(Action onReceive) where T : class, new() - { - _netSerializer.Register(); - var reference = new T(); - _callbacks[GetId()] = (reader, userData) => - { - _netSerializer.Deserialize(reader, reference); - onReceive(reference); - }; - } - - /// - /// Register and subscribe to packet receive event - /// This method will overwrite last received packet class on receive (less garbage) - /// - /// event that will be called when packet deserialized with ReadPacket method - /// 's fields are not supported, or it has no fields - public void SubscribeReusable(Action onReceive) where T : class, new() - { - _netSerializer.Register(); - var reference = new T(); - _callbacks[GetId()] = (reader, userData) => - { - _netSerializer.Deserialize(reader, reference); - onReceive(reference, (TUserData)userData); - }; - } - - public void SubscribeNetSerializable( - Action onReceive, - Func packetConstructor) where T : INetSerializable - { - _callbacks[GetId()] = (reader, userData) => - { - var pkt = packetConstructor(); - pkt.Deserialize(reader); - onReceive(pkt, (TUserData)userData); - }; - } - - public void SubscribeNetSerializable( - Action onReceive, - Func packetConstructor) where T : INetSerializable - { - _callbacks[GetId()] = (reader, userData) => - { - var pkt = packetConstructor(); - pkt.Deserialize(reader); - onReceive(pkt); - }; - } - - public void SubscribeNetSerializable( - Action onReceive) where T : INetSerializable, new() - { - var reference = new T(); - _callbacks[GetId()] = (reader, userData) => - { - reference.Deserialize(reader); - onReceive(reference, (TUserData)userData); - }; - } - - public void SubscribeNetSerializable( - Action onReceive) where T : INetSerializable, new() - { - var reference = new T(); - _callbacks[GetId()] = (reader, userData) => - { - reference.Deserialize(reader); - onReceive(reference); - }; - } - - /// - /// Remove any subscriptions by type - /// - /// Packet type - /// true if remove is success - public bool RemoveSubscription() - { - return _callbacks.Remove(GetId()); - } - } -} diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NetPacketProcessor.cs.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NetPacketProcessor.cs.meta deleted file mode 100644 index 1a463ab0..00000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NetPacketProcessor.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 0639bb1a03a8629d58d4c50b8d0432bf -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NetSerializer.cs b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NetSerializer.cs deleted file mode 100644 index 63f6cd67..00000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NetSerializer.cs +++ /dev/null @@ -1,738 +0,0 @@ -using System; -using System.Reflection; -using System.Collections.Generic; -using System.Net; -using System.Runtime.Serialization; - -namespace LiteNetLib.Utils -{ - public class InvalidTypeException : ArgumentException - { - public InvalidTypeException(string message) : base(message) { } - } - - public class ParseException : Exception - { - public ParseException(string message) : base(message) { } - } - - public class NetSerializer - { - private enum CallType - { - Basic, - Array, - List - } - - private abstract class FastCall - { - public CallType Type; - public virtual void Init(MethodInfo getMethod, MethodInfo setMethod, CallType type) { Type = type; } - public abstract void Read(T inf, NetDataReader r); - public abstract void Write(T inf, NetDataWriter w); - public abstract void ReadArray(T inf, NetDataReader r); - public abstract void WriteArray(T inf, NetDataWriter w); - public abstract void ReadList(T inf, NetDataReader r); - public abstract void WriteList(T inf, NetDataWriter w); - } - - private abstract class FastCallSpecific : FastCall - { - protected Func Getter; - protected Action Setter; - protected Func GetterArr; - protected Action SetterArr; - protected Func> GetterList; - protected Action> SetterList; - - public override void ReadArray(TClass inf, NetDataReader r) { throw new InvalidTypeException("Unsupported type: " + typeof(TProperty) + "[]"); } - public override void WriteArray(TClass inf, NetDataWriter w) { throw new InvalidTypeException("Unsupported type: " + typeof(TProperty) + "[]"); } - public override void ReadList(TClass inf, NetDataReader r) { throw new InvalidTypeException("Unsupported type: List<" + typeof(TProperty) + ">"); } - public override void WriteList(TClass inf, NetDataWriter w) { throw new InvalidTypeException("Unsupported type: List<" + typeof(TProperty) + ">"); } - - protected TProperty[] ReadArrayHelper(TClass inf, NetDataReader r) - { - ushort count = r.GetUShort(); - var arr = GetterArr(inf); - arr = arr == null || arr.Length != count ? new TProperty[count] : arr; - SetterArr(inf, arr); - return arr; - } - - protected TProperty[] WriteArrayHelper(TClass inf, NetDataWriter w) - { - var arr = GetterArr(inf); - w.Put((ushort)arr.Length); - return arr; - } - - protected List ReadListHelper(TClass inf, NetDataReader r, out int len) - { - len = r.GetUShort(); - var list = GetterList(inf); - if (list == null) - { - list = new List(len); - SetterList(inf, list); - } - return list; - } - - protected List WriteListHelper(TClass inf, NetDataWriter w, out int len) - { - var list = GetterList(inf); - if (list == null) - { - len = 0; - w.Put(0); - return null; - } - len = list.Count; - w.Put((ushort)len); - return list; - } - - public override void Init(MethodInfo getMethod, MethodInfo setMethod, CallType type) - { - base.Init(getMethod, setMethod, type); - switch (type) - { - case CallType.Array: - GetterArr = (Func)Delegate.CreateDelegate(typeof(Func), getMethod); - SetterArr = (Action)Delegate.CreateDelegate(typeof(Action), setMethod); - break; - case CallType.List: - GetterList = (Func>)Delegate.CreateDelegate(typeof(Func>), getMethod); - SetterList = (Action>)Delegate.CreateDelegate(typeof(Action>), setMethod); - break; - default: - Getter = (Func)Delegate.CreateDelegate(typeof(Func), getMethod); - Setter = (Action)Delegate.CreateDelegate(typeof(Action), setMethod); - break; - } - } - } - - private abstract class FastCallSpecificAuto : FastCallSpecific - { - protected abstract void ElementRead(NetDataReader r, out TProperty prop); - protected abstract void ElementWrite(NetDataWriter w, ref TProperty prop); - - public override void Read(TClass inf, NetDataReader r) - { - ElementRead(r, out var elem); - Setter(inf, elem); - } - - public override void Write(TClass inf, NetDataWriter w) - { - var elem = Getter(inf); - ElementWrite(w, ref elem); - } - - public override void ReadArray(TClass inf, NetDataReader r) - { - var arr = ReadArrayHelper(inf, r); - for (int i = 0; i < arr.Length; i++) - ElementRead(r, out arr[i]); - } - - public override void WriteArray(TClass inf, NetDataWriter w) - { - var arr = WriteArrayHelper(inf, w); - for (int i = 0; i < arr.Length; i++) - ElementWrite(w, ref arr[i]); - } - } - - private sealed class FastCallStatic : FastCallSpecific - { - private readonly Action _writer; - private readonly Func _reader; - - public FastCallStatic(Action write, Func read) - { - _writer = write; - _reader = read; - } - - public override void Read(TClass inf, NetDataReader r) { Setter(inf, _reader(r)); } - public override void Write(TClass inf, NetDataWriter w) { _writer(w, Getter(inf)); } - - public override void ReadList(TClass inf, NetDataReader r) - { - var list = ReadListHelper(inf, r, out int len); - int listCount = list.Count; - for (int i = 0; i < len; i++) - { - if (i < listCount) - list[i] = _reader(r); - else - list.Add(_reader(r)); - } - if (len < listCount) - list.RemoveRange(len, listCount - len); - } - - public override void WriteList(TClass inf, NetDataWriter w) - { - var list = WriteListHelper(inf, w, out int len); - for (int i = 0; i < len; i++) - _writer(w, list[i]); - } - - public override void ReadArray(TClass inf, NetDataReader r) - { - var arr = ReadArrayHelper(inf, r); - int len = arr.Length; - for (int i = 0; i < len; i++) - arr[i] = _reader(r); - } - - public override void WriteArray(TClass inf, NetDataWriter w) - { - var arr = WriteArrayHelper(inf, w); - int len = arr.Length; - for (int i = 0; i < len; i++) - _writer(w, arr[i]); - } - } - - private sealed class FastCallStruct : FastCallSpecific where TProperty : struct, INetSerializable - { - private TProperty _p; - - public override void Read(TClass inf, NetDataReader r) - { - _p.Deserialize(r); - Setter(inf, _p); - } - - public override void Write(TClass inf, NetDataWriter w) - { - _p = Getter(inf); - _p.Serialize(w); - } - - public override void ReadList(TClass inf, NetDataReader r) - { - var list = ReadListHelper(inf, r, out int len); - int listCount = list.Count; - for (int i = 0; i < len; i++) - { - var itm = default(TProperty); - itm.Deserialize(r); - if(i < listCount) - list[i] = itm; - else - list.Add(itm); - } - if (len < listCount) - list.RemoveRange(len, listCount - len); - } - - public override void WriteList(TClass inf, NetDataWriter w) - { - var list = WriteListHelper(inf, w, out int len); - for (int i = 0; i < len; i++) - list[i].Serialize(w); - } - - public override void ReadArray(TClass inf, NetDataReader r) - { - var arr = ReadArrayHelper(inf, r); - int len = arr.Length; - for (int i = 0; i < len; i++) - arr[i].Deserialize(r); - } - - public override void WriteArray(TClass inf, NetDataWriter w) - { - var arr = WriteArrayHelper(inf, w); - int len = arr.Length; - for (int i = 0; i < len; i++) - arr[i].Serialize(w); - } - } - - private sealed class FastCallClass : FastCallSpecific where TProperty : class, INetSerializable - { - private readonly Func _constructor; - public FastCallClass(Func constructor) { _constructor = constructor; } - - public override void Read(TClass inf, NetDataReader r) - { - var p = _constructor(); - p.Deserialize(r); - Setter(inf, p); - } - - public override void Write(TClass inf, NetDataWriter w) - { - var p = Getter(inf); - p?.Serialize(w); - } - - public override void ReadList(TClass inf, NetDataReader r) - { - var list = ReadListHelper(inf, r, out int len); - int listCount = list.Count; - for (int i = 0; i < len; i++) - { - if (i < listCount) - { - list[i].Deserialize(r); - } - else - { - var itm = _constructor(); - itm.Deserialize(r); - list.Add(itm); - } - } - if (len < listCount) - list.RemoveRange(len, listCount - len); - } - - public override void WriteList(TClass inf, NetDataWriter w) - { - var list = WriteListHelper(inf, w, out int len); - for (int i = 0; i < len; i++) - list[i].Serialize(w); - } - - public override void ReadArray(TClass inf, NetDataReader r) - { - var arr = ReadArrayHelper(inf, r); - int len = arr.Length; - for (int i = 0; i < len; i++) - { - arr[i] = _constructor(); - arr[i].Deserialize(r); - } - } - - public override void WriteArray(TClass inf, NetDataWriter w) - { - var arr = WriteArrayHelper(inf, w); - int len = arr.Length; - for (int i = 0; i < len; i++) - arr[i].Serialize(w); - } - } - - private class IntSerializer : FastCallSpecific - { - public override void Read(T inf, NetDataReader r) { Setter(inf, r.GetInt()); } - public override void Write(T inf, NetDataWriter w) { w.Put(Getter(inf)); } - public override void ReadArray(T inf, NetDataReader r) { SetterArr(inf, r.GetIntArray()); } - public override void WriteArray(T inf, NetDataWriter w) { w.PutArray(GetterArr(inf)); } - } - - private class UIntSerializer : FastCallSpecific - { - public override void Read(T inf, NetDataReader r) { Setter(inf, r.GetUInt()); } - public override void Write(T inf, NetDataWriter w) { w.Put(Getter(inf)); } - public override void ReadArray(T inf, NetDataReader r) { SetterArr(inf, r.GetUIntArray()); } - public override void WriteArray(T inf, NetDataWriter w) { w.PutArray(GetterArr(inf)); } - } - - private class ShortSerializer : FastCallSpecific - { - public override void Read(T inf, NetDataReader r) { Setter(inf, r.GetShort()); } - public override void Write(T inf, NetDataWriter w) { w.Put(Getter(inf)); } - public override void ReadArray(T inf, NetDataReader r) { SetterArr(inf, r.GetShortArray()); } - public override void WriteArray(T inf, NetDataWriter w) { w.PutArray(GetterArr(inf)); } - } - - private class UShortSerializer : FastCallSpecific - { - public override void Read(T inf, NetDataReader r) { Setter(inf, r.GetUShort()); } - public override void Write(T inf, NetDataWriter w) { w.Put(Getter(inf)); } - public override void ReadArray(T inf, NetDataReader r) { SetterArr(inf, r.GetUShortArray()); } - public override void WriteArray(T inf, NetDataWriter w) { w.PutArray(GetterArr(inf)); } - } - - private class LongSerializer : FastCallSpecific - { - public override void Read(T inf, NetDataReader r) { Setter(inf, r.GetLong()); } - public override void Write(T inf, NetDataWriter w) { w.Put(Getter(inf)); } - public override void ReadArray(T inf, NetDataReader r) { SetterArr(inf, r.GetLongArray()); } - public override void WriteArray(T inf, NetDataWriter w) { w.PutArray(GetterArr(inf)); } - } - - private class ULongSerializer : FastCallSpecific - { - public override void Read(T inf, NetDataReader r) { Setter(inf, r.GetULong()); } - public override void Write(T inf, NetDataWriter w) { w.Put(Getter(inf)); } - public override void ReadArray(T inf, NetDataReader r) { SetterArr(inf, r.GetULongArray()); } - public override void WriteArray(T inf, NetDataWriter w) { w.PutArray(GetterArr(inf)); } - } - - private class ByteSerializer : FastCallSpecific - { - public override void Read(T inf, NetDataReader r) { Setter(inf, r.GetByte()); } - public override void Write(T inf, NetDataWriter w) { w.Put(Getter(inf)); } - public override void ReadArray(T inf, NetDataReader r) { SetterArr(inf, r.GetBytesWithLength()); } - public override void WriteArray(T inf, NetDataWriter w) { w.PutBytesWithLength(GetterArr(inf)); } - } - - private class SByteSerializer : FastCallSpecific - { - public override void Read(T inf, NetDataReader r) { Setter(inf, r.GetSByte()); } - public override void Write(T inf, NetDataWriter w) { w.Put(Getter(inf)); } - public override void ReadArray(T inf, NetDataReader r) { SetterArr(inf, r.GetSBytesWithLength()); } - public override void WriteArray(T inf, NetDataWriter w) { w.PutSBytesWithLength(GetterArr(inf)); } - } - - private class FloatSerializer : FastCallSpecific - { - public override void Read(T inf, NetDataReader r) { Setter(inf, r.GetFloat()); } - public override void Write(T inf, NetDataWriter w) { w.Put(Getter(inf)); } - public override void ReadArray(T inf, NetDataReader r) { SetterArr(inf, r.GetFloatArray()); } - public override void WriteArray(T inf, NetDataWriter w) { w.PutArray(GetterArr(inf)); } - } - - private class DoubleSerializer : FastCallSpecific - { - public override void Read(T inf, NetDataReader r) { Setter(inf, r.GetDouble()); } - public override void Write(T inf, NetDataWriter w) { w.Put(Getter(inf)); } - public override void ReadArray(T inf, NetDataReader r) { SetterArr(inf, r.GetDoubleArray()); } - public override void WriteArray(T inf, NetDataWriter w) { w.PutArray(GetterArr(inf)); } - } - - private class BoolSerializer : FastCallSpecific - { - public override void Read(T inf, NetDataReader r) { Setter(inf, r.GetBool()); } - public override void Write(T inf, NetDataWriter w) { w.Put(Getter(inf)); } - public override void ReadArray(T inf, NetDataReader r) { SetterArr(inf, r.GetBoolArray()); } - public override void WriteArray(T inf, NetDataWriter w) { w.PutArray(GetterArr(inf)); } - } - - private class CharSerializer : FastCallSpecificAuto - { - protected override void ElementWrite(NetDataWriter w, ref char prop) { w.Put(prop); } - protected override void ElementRead(NetDataReader r, out char prop) { prop = r.GetChar(); } - } - - private class IPEndPointSerializer : FastCallSpecificAuto - { - protected override void ElementWrite(NetDataWriter w, ref IPEndPoint prop) { w.Put(prop); } - protected override void ElementRead(NetDataReader r, out IPEndPoint prop) { prop = r.GetNetEndPoint(); } - } - - private class StringSerializer : FastCallSpecific - { - private readonly int _maxLength; - public StringSerializer(int maxLength) { _maxLength = maxLength > 0 ? maxLength : short.MaxValue; } - public override void Read(T inf, NetDataReader r) { Setter(inf, r.GetString(_maxLength)); } - public override void Write(T inf, NetDataWriter w) { w.Put(Getter(inf), _maxLength); } - public override void ReadArray(T inf, NetDataReader r) { SetterArr(inf, r.GetStringArray(_maxLength)); } - public override void WriteArray(T inf, NetDataWriter w) { w.PutArray(GetterArr(inf), _maxLength); } - } - - private class EnumByteSerializer : FastCall - { - protected readonly PropertyInfo Property; - protected readonly Type PropertyType; - public EnumByteSerializer(PropertyInfo property, Type propertyType) - { - Property = property; - PropertyType = propertyType; - } - public override void Read(T inf, NetDataReader r) { Property.SetValue(inf, Enum.ToObject(PropertyType, r.GetByte()), null); } - public override void Write(T inf, NetDataWriter w) { w.Put((byte)Property.GetValue(inf, null)); } - public override void ReadArray(T inf, NetDataReader r) { throw new InvalidTypeException("Unsupported type: Enum[]"); } - public override void WriteArray(T inf, NetDataWriter w) { throw new InvalidTypeException("Unsupported type: Enum[]"); } - public override void ReadList(T inf, NetDataReader r) { throw new InvalidTypeException("Unsupported type: List"); } - public override void WriteList(T inf, NetDataWriter w) { throw new InvalidTypeException("Unsupported type: List"); } - } - - private class EnumIntSerializer : EnumByteSerializer - { - public EnumIntSerializer(PropertyInfo property, Type propertyType) : base(property, propertyType) { } - public override void Read(T inf, NetDataReader r) { Property.SetValue(inf, Enum.ToObject(PropertyType, r.GetInt()), null); } - public override void Write(T inf, NetDataWriter w) { w.Put((int)Property.GetValue(inf, null)); } - } - - private sealed class ClassInfo - { - public static ClassInfo Instance; - private readonly FastCall[] _serializers; - private readonly int _membersCount; - - public ClassInfo(List> serializers) - { - _membersCount = serializers.Count; - _serializers = serializers.ToArray(); - } - - public void Write(T obj, NetDataWriter writer) - { - for (int i = 0; i < _membersCount; i++) - { - var s = _serializers[i]; - if (s.Type == CallType.Basic) - s.Write(obj, writer); - else if (s.Type == CallType.Array) - s.WriteArray(obj, writer); - else - s.WriteList(obj, writer); - } - } - - public void Read(T obj, NetDataReader reader) - { - for (int i = 0; i < _membersCount; i++) - { - var s = _serializers[i]; - if (s.Type == CallType.Basic) - s.Read(obj, reader); - else if(s.Type == CallType.Array) - s.ReadArray(obj, reader); - else - s.ReadList(obj, reader); - } - } - } - - private abstract class CustomType - { - public abstract FastCall Get(); - } - - private sealed class CustomTypeStruct : CustomType where TProperty : struct, INetSerializable - { - public override FastCall Get() { return new FastCallStruct(); } - } - - private sealed class CustomTypeClass : CustomType where TProperty : class, INetSerializable - { - private readonly Func _constructor; - public CustomTypeClass(Func constructor) { _constructor = constructor; } - public override FastCall Get() { return new FastCallClass(_constructor); } - } - - private sealed class CustomTypeStatic : CustomType - { - private readonly Action _writer; - private readonly Func _reader; - public CustomTypeStatic(Action writer, Func reader) - { - _writer = writer; - _reader = reader; - } - public override FastCall Get() { return new FastCallStatic(_writer, _reader); } - } - - /// - /// Register custom property type - /// - /// INetSerializable structure - public void RegisterNestedType() where T : struct, INetSerializable - { - _registeredTypes.Add(typeof(T), new CustomTypeStruct()); - } - - /// - /// Register custom property type - /// - /// INetSerializable class - public void RegisterNestedType(Func constructor) where T : class, INetSerializable - { - _registeredTypes.Add(typeof(T), new CustomTypeClass(constructor)); - } - - /// - /// Register custom property type - /// - /// Any packet - /// custom type writer - /// custom type reader - public void RegisterNestedType(Action writer, Func reader) - { - _registeredTypes.Add(typeof(T), new CustomTypeStatic(writer, reader)); - } - - private NetDataWriter _writer; - private readonly int _maxStringLength; - private readonly Dictionary _registeredTypes = new Dictionary(); - - public NetSerializer() : this(0) - { - } - - public NetSerializer(int maxStringLength) - { - _maxStringLength = maxStringLength; - } - - private ClassInfo RegisterInternal() - { - if (ClassInfo.Instance != null) - return ClassInfo.Instance; - - Type t = typeof(T); - var props = t.GetProperties( - BindingFlags.Instance | - BindingFlags.Public | - BindingFlags.GetProperty | - BindingFlags.SetProperty); - var serializers = new List>(); - for (int i = 0; i < props.Length; i++) - { - var property = props[i]; - var propertyType = property.PropertyType; - - var elementType = propertyType.IsArray ? propertyType.GetElementType() : propertyType; - var callType = propertyType.IsArray ? CallType.Array : CallType.Basic; - - if (propertyType.IsGenericType && propertyType.GetGenericTypeDefinition() == typeof(List<>)) - { - elementType = propertyType.GetGenericArguments()[0]; - callType = CallType.List; - } - - if (Attribute.IsDefined(property, typeof(IgnoreDataMemberAttribute))) - continue; - - var getMethod = property.GetGetMethod(); - var setMethod = property.GetSetMethod(); - if (getMethod == null || setMethod == null) - continue; - - FastCall serialzer = null; - if (propertyType.IsEnum) - { - var underlyingType = Enum.GetUnderlyingType(propertyType); - if (underlyingType == typeof(byte)) - serialzer = new EnumByteSerializer(property, propertyType); - else if (underlyingType == typeof(int)) - serialzer = new EnumIntSerializer(property, propertyType); - else - throw new InvalidTypeException("Not supported enum underlying type: " + underlyingType.Name); - } - else if (elementType == typeof(string)) - serialzer = new StringSerializer(_maxStringLength); - else if (elementType == typeof(bool)) - serialzer = new BoolSerializer(); - else if (elementType == typeof(byte)) - serialzer = new ByteSerializer(); - else if (elementType == typeof(sbyte)) - serialzer = new SByteSerializer(); - else if (elementType == typeof(short)) - serialzer = new ShortSerializer(); - else if (elementType == typeof(ushort)) - serialzer = new UShortSerializer(); - else if (elementType == typeof(int)) - serialzer = new IntSerializer(); - else if (elementType == typeof(uint)) - serialzer = new UIntSerializer(); - else if (elementType == typeof(long)) - serialzer = new LongSerializer(); - else if (elementType == typeof(ulong)) - serialzer = new ULongSerializer(); - else if (elementType == typeof(float)) - serialzer = new FloatSerializer(); - else if (elementType == typeof(double)) - serialzer = new DoubleSerializer(); - else if (elementType == typeof(char)) - serialzer = new CharSerializer(); - else if (elementType == typeof(IPEndPoint)) - serialzer = new IPEndPointSerializer(); - else - { - _registeredTypes.TryGetValue(elementType, out var customType); - if (customType != null) - serialzer = customType.Get(); - } - - if (serialzer != null) - { - serialzer.Init(getMethod, setMethod, callType); - serializers.Add(serialzer); - } - else - { - throw new InvalidTypeException("Unknown property type: " + propertyType.FullName); - } - } - ClassInfo.Instance = new ClassInfo(serializers); - return ClassInfo.Instance; - } - - /// 's fields are not supported, or it has no fields - public void Register() - { - RegisterInternal(); - } - - /// - /// Reads packet with known type - /// - /// NetDataReader with packet - /// Returns packet if packet in reader is matched type - /// 's fields are not supported, or it has no fields - public T Deserialize(NetDataReader reader) where T : class, new() - { - var info = RegisterInternal(); - var result = new T(); - try - { - info.Read(result, reader); - } - catch - { - return null; - } - return result; - } - - /// - /// Reads packet with known type (non alloc variant) - /// - /// NetDataReader with packet - /// Deserialization target - /// Returns true if packet in reader is matched type - /// 's fields are not supported, or it has no fields - public bool Deserialize(NetDataReader reader, T target) where T : class, new() - { - var info = RegisterInternal(); - try - { - info.Read(target, reader); - } - catch - { - return false; - } - return true; - } - - /// - /// Serialize object to NetDataWriter (fast) - /// - /// Serialization target NetDataWriter - /// Object to serialize - /// 's fields are not supported, or it has no fields - public void Serialize(NetDataWriter writer, T obj) where T : class, new() - { - RegisterInternal().Write(obj, writer); - } - - /// - /// Serialize object to byte array - /// - /// Object to serialize - /// byte array with serialized data - public byte[] Serialize(T obj) where T : class, new() - { - if (_writer == null) - _writer = new NetDataWriter(); - _writer.Reset(); - Serialize(_writer, obj); - return _writer.CopyData(); - } - } -} diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NetSerializer.cs.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NetSerializer.cs.meta deleted file mode 100644 index b75eed32..00000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NetSerializer.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: f4cee768ab184eb66add2893ecf7648d -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NtpPacket.cs b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NtpPacket.cs deleted file mode 100644 index 1ba52107..00000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NtpPacket.cs +++ /dev/null @@ -1,423 +0,0 @@ -using System; - -namespace LiteNetLib.Utils -{ - /// - /// Represents RFC4330 SNTP packet used for communication to and from a network time server. - /// - /// - /// - /// Most applications should just use the property. - /// - /// - /// The same data structure represents both request and reply packets. - /// Request and reply differ in which properties are set and to what values. - /// - /// - /// The only real property is . - /// All other properties read from and write to the underlying byte array - /// with the exception of , - /// which is not part of the packet on network and it is instead set locally after receiving the packet. - /// - /// - /// Copied from GuerrillaNtp project - /// with permission from Robert Vazan (@robertvazan) under MIT license, see https://github.com/RevenantX/LiteNetLib/pull/236 - /// - /// - public class NtpPacket - { - private static readonly DateTime Epoch = new DateTime(1900, 1, 1); - - /// - /// Gets RFC4330-encoded SNTP packet. - /// - /// - /// Byte array containing RFC4330-encoded SNTP packet. It is at least 48 bytes long. - /// - /// - /// This is the only real property. All other properties except - /// read from or write to this byte array. - /// - public byte[] Bytes { get; } - - /// - /// Gets the leap second indicator. - /// - /// - /// Leap second warning, if any. Special value - /// indicates unsynchronized server clock. - /// Default is . - /// - /// - /// Only servers fill in this property. Clients can consult this property for possible leap second warning. - /// - public NtpLeapIndicator LeapIndicator => (NtpLeapIndicator)((Bytes[0] & 0xC0) >> 6); - - /// - /// Gets or sets protocol version number. - /// - /// - /// SNTP protocol version. Default is 4, which is the latest version at the time of this writing. - /// - /// - /// In request packets, clients should leave this property at default value 4. - /// Servers usually reply with the same protocol version. - /// - public int VersionNumber - { - get => (Bytes[0] & 0x38) >> 3; - private set => Bytes[0] = (byte)((Bytes[0] & ~0x38) | value << 3); - } - - /// - /// Gets or sets SNTP packet mode, i.e. whether this is client or server packet. - /// - /// - /// SNTP packet mode. Default is in newly created packets. - /// Server reply should have this property set to . - /// - public NtpMode Mode - { - get => (NtpMode)(Bytes[0] & 0x07); - private set => Bytes[0] = (byte)((Bytes[0] & ~0x07) | (int)value); - } - - /// - /// Gets server's distance from the reference clock. - /// - /// - /// - /// Distance from the reference clock. This property is set only in server reply packets. - /// Servers connected directly to reference clock hardware set this property to 1. - /// Statum number is incremented by 1 on every hop down the NTP server hierarchy. - /// - /// - /// Special value 0 indicates that this packet is a Kiss-o'-Death message - /// with kiss code stored in . - /// - /// - public int Stratum => Bytes[1]; - - /// - /// Gets server's preferred polling interval. - /// - /// - /// Polling interval in log2 seconds, e.g. 4 stands for 16s and 17 means 131,072s. - /// - public int Poll => Bytes[2]; - - /// - /// Gets the precision of server clock. - /// - /// - /// Clock precision in log2 seconds, e.g. -20 for microsecond precision. - /// - public int Precision => (sbyte)Bytes[3]; - - /// - /// Gets the total round-trip delay from the server to the reference clock. - /// - /// - /// Round-trip delay to the reference clock. Normally a positive value smaller than one second. - /// - public TimeSpan RootDelay => GetTimeSpan32(4); - - /// - /// Gets the estimated error in time reported by the server. - /// - /// - /// Estimated error in time reported by the server. Normally a positive value smaller than one second. - /// - public TimeSpan RootDispersion => GetTimeSpan32(8); - - /// - /// Gets the ID of the time source used by the server or Kiss-o'-Death code sent by the server. - /// - /// - /// - /// ID of server's time source or Kiss-o'-Death code. - /// Purpose of this property depends on value of property. - /// - /// - /// Stratum 1 servers write here one of several special values that describe the kind of hardware clock they use. - /// - /// - /// Stratum 2 and lower servers set this property to IPv4 address of their upstream server. - /// If upstream server has IPv6 address, the address is hashed, because it doesn't fit in this property. - /// - /// - /// When server sets to special value 0, - /// this property contains so called kiss code that instructs the client to stop querying the server. - /// - /// - public uint ReferenceId => GetUInt32BE(12); - - /// - /// Gets or sets the time when the server clock was last set or corrected. - /// - /// - /// Time when the server clock was last set or corrected or null when not specified. - /// - /// - /// This Property is usually set only by servers. It usually lags server's current time by several minutes, - /// so don't use this property for time synchronization. - /// - public DateTime? ReferenceTimestamp => GetDateTime64(16); - - /// - /// Gets or sets the time when the client sent its request. - /// - /// - /// This property is null in request packets. - /// In reply packets, it is the time when the client sent its request. - /// Servers copy this value from - /// that they find in received request packet. - /// - /// - /// - public DateTime? OriginTimestamp => GetDateTime64(24); - - /// - /// Gets or sets the time when the request was received by the server. - /// - /// - /// This property is null in request packets. - /// In reply packets, it is the time when the server received client request. - /// - /// - /// - public DateTime? ReceiveTimestamp => GetDateTime64(32); - - /// - /// Gets or sets the time when the packet was sent. - /// - /// - /// Time when the packet was sent. It should never be null. - /// Default value is . - /// - /// - /// This property must be set by both clients and servers. - /// - /// - /// - public DateTime? TransmitTimestamp { get { return GetDateTime64(40); } private set { SetDateTime64(40, value); } } - - /// - /// Gets or sets the time of reception of response SNTP packet on the client. - /// - /// - /// Time of reception of response SNTP packet on the client. It is null in request packets. - /// - /// - /// This property is not part of the protocol and has to be set when reply packet is received. - /// - /// - /// - public DateTime? DestinationTimestamp { get; private set; } - - /// - /// Gets the round-trip time to the server. - /// - /// - /// Time the request spent traveling to the server plus the time the reply spent traveling back. - /// This is calculated from timestamps in the packet as (t1 - t0) + (t3 - t2) - /// where t0 is , - /// t1 is , - /// t2 is , - /// and t3 is . - /// This property throws an exception in request packets. - /// - public TimeSpan RoundTripTime - { - get - { - CheckTimestamps(); - return (ReceiveTimestamp.Value - OriginTimestamp.Value) + (DestinationTimestamp.Value - TransmitTimestamp.Value); - } - } - - /// - /// Gets the offset that should be added to local time to synchronize it with server time. - /// - /// - /// Time difference between server and client. It should be added to local time to get server time. - /// It is calculated from timestamps in the packet as 0.5 * ((t1 - t0) - (t3 - t2)) - /// where t0 is , - /// t1 is , - /// t2 is , - /// and t3 is . - /// This property throws an exception in request packets. - /// - public TimeSpan CorrectionOffset - { - get - { - CheckTimestamps(); - return TimeSpan.FromTicks(((ReceiveTimestamp.Value - OriginTimestamp.Value) - (DestinationTimestamp.Value - TransmitTimestamp.Value)).Ticks / 2); - } - } - - /// - /// Initializes default request packet. - /// - /// - /// Properties and - /// are set appropriately for request packet. Property - /// is set to . - /// - public NtpPacket() : this(new byte[48]) - { - Mode = NtpMode.Client; - VersionNumber = 4; - TransmitTimestamp = DateTime.UtcNow; - } - - /// - /// Initializes packet from received data. - /// - internal NtpPacket(byte[] bytes) - { - if (bytes.Length < 48) - throw new ArgumentException("SNTP reply packet must be at least 48 bytes long.", "bytes"); - Bytes = bytes; - } - - /// - /// Initializes packet from data received from a server. - /// - /// Data received from the server. - /// Utc time of reception of response SNTP packet on the client. - /// - public static NtpPacket FromServerResponse(byte[] bytes, DateTime destinationTimestamp) - { - return new NtpPacket(bytes) { DestinationTimestamp = destinationTimestamp }; - } - - internal void ValidateRequest() - { - if (Mode != NtpMode.Client) - throw new InvalidOperationException("This is not a request SNTP packet."); - if (VersionNumber == 0) - throw new InvalidOperationException("Protocol version of the request is not specified."); - if (TransmitTimestamp == null) - throw new InvalidOperationException("TransmitTimestamp must be set in request packet."); - } - - internal void ValidateReply() - { - if (Mode != NtpMode.Server) - throw new InvalidOperationException("This is not a reply SNTP packet."); - if (VersionNumber == 0) - throw new InvalidOperationException("Protocol version of the reply is not specified."); - if (Stratum == 0) - throw new InvalidOperationException(string.Format("Received Kiss-o'-Death SNTP packet with code 0x{0:x}.", ReferenceId)); - if (LeapIndicator == NtpLeapIndicator.AlarmCondition) - throw new InvalidOperationException("SNTP server has unsynchronized clock."); - CheckTimestamps(); - } - - private void CheckTimestamps() - { - if (OriginTimestamp == null) - throw new InvalidOperationException("Origin timestamp is missing."); - if (ReceiveTimestamp == null) - throw new InvalidOperationException("Receive timestamp is missing."); - if (TransmitTimestamp == null) - throw new InvalidOperationException("Transmit timestamp is missing."); - if (DestinationTimestamp == null) - throw new InvalidOperationException("Destination timestamp is missing."); - } - - private DateTime? GetDateTime64(int offset) - { - var field = GetUInt64BE(offset); - if (field == 0) - return null; - return new DateTime(Epoch.Ticks + Convert.ToInt64(field * (1.0 / (1L << 32) * 10000000.0))); - } - - private void SetDateTime64(int offset, DateTime? value) - { - SetUInt64BE(offset, value == null ? 0 : Convert.ToUInt64((value.Value.Ticks - Epoch.Ticks) * (0.0000001 * (1L << 32)))); - } - - private TimeSpan GetTimeSpan32(int offset) - { - return TimeSpan.FromSeconds(GetInt32BE(offset) / (double)(1 << 16)); - } - - private ulong GetUInt64BE(int offset) - { - return SwapEndianness(BitConverter.ToUInt64(Bytes, offset)); - } - - private void SetUInt64BE(int offset, ulong value) - { - FastBitConverter.GetBytes(Bytes, offset, SwapEndianness(value)); - } - - private int GetInt32BE(int offset) - { - return (int)GetUInt32BE(offset); - } - - private uint GetUInt32BE(int offset) - { - return SwapEndianness(BitConverter.ToUInt32(Bytes, offset)); - } - - private static uint SwapEndianness(uint x) - { - return ((x & 0xff) << 24) | ((x & 0xff00) << 8) | ((x & 0xff0000) >> 8) | ((x & 0xff000000) >> 24); - } - - private static ulong SwapEndianness(ulong x) - { - return ((ulong)SwapEndianness((uint)x) << 32) | SwapEndianness((uint)(x >> 32)); - } - } - - /// - /// Represents leap second warning from the server that instructs the client to add or remove leap second. - /// - /// - public enum NtpLeapIndicator - { - /// - /// No leap second warning. No action required. - /// - NoWarning, - - /// - /// Warns the client that the last minute of the current day has 61 seconds. - /// - LastMinuteHas61Seconds, - - /// - /// Warns the client that the last minute of the current day has 59 seconds. - /// - LastMinuteHas59Seconds, - - /// - /// Special value indicating that the server clock is unsynchronized and the returned time is unreliable. - /// - AlarmCondition - } - - /// - /// Describes SNTP packet mode, i.e. client or server. - /// - /// - public enum NtpMode - { - /// - /// Identifies client-to-server SNTP packet. - /// - Client = 3, - - /// - /// Identifies server-to-client SNTP packet. - /// - Server = 4, - } -} diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NtpPacket.cs.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NtpPacket.cs.meta deleted file mode 100644 index a47fed20..00000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NtpPacket.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: d1ceb29a3d14d86e98fddc9c1b17e922 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NtpRequest.cs b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NtpRequest.cs deleted file mode 100644 index bd7f74fe..00000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NtpRequest.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System.Net; -using System.Net.Sockets; - -namespace LiteNetLib.Utils -{ - internal sealed class NtpRequest - { - private const int ResendTimer = 1000; - private const int KillTimer = 10000; - public const int DefaultPort = 123; - private readonly IPEndPoint _ntpEndPoint; - private int _resendTime = ResendTimer; - private int _killTime = 0; - - public NtpRequest(IPEndPoint endPoint) - { - _ntpEndPoint = endPoint; - } - - public bool NeedToKill => _killTime >= KillTimer; - - public bool Send(Socket socket, int time) - { - _resendTime += time; - _killTime += time; - if (_resendTime < ResendTimer) - { - return false; - } - var packet = new NtpPacket(); - try - { - int sendCount = socket.SendTo(packet.Bytes, 0, packet.Bytes.Length, SocketFlags.None, _ntpEndPoint); - return sendCount == packet.Bytes.Length; - } - catch - { - return false; - } - } - } -} diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NtpRequest.cs.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NtpRequest.cs.meta deleted file mode 100644 index 6f3e611c..00000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NtpRequest.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: bfa3324d57a7c285ea1dcbff1142e7e2 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/Preserve.cs b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/Preserve.cs deleted file mode 100644 index b73e1b90..00000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/Preserve.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace LiteNetLib.Utils -{ - /// - /// PreserveAttribute prevents byte code stripping from removing a class, method, field, or property. - /// - [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum | AttributeTargets.Constructor | AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Interface | AttributeTargets.Delegate, Inherited = false)] - public class PreserveAttribute : Attribute - { - } -} diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/Preserve.cs.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/Preserve.cs.meta deleted file mode 100644 index b2075b6a..00000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/Preserve.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 7c73743e8060388d79d8ba1cf976a2e1 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/package.json b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/package.json deleted file mode 100644 index e5011459..00000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/package.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "name": "com.revenantx.litenetlib", - "version": "1.0.1-1", - "displayName": "LiteNetLib", - "description": "Lite reliable UDP library for .NET Standard 2.0 (Mono, .NET Core, .NET Framework)", - "unity": "2018.3", - "author": { - "name": "RevenantX", - "url": "https://github.com/RevenantX" - } -} \ No newline at end of file diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/package.json.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/package.json.meta deleted file mode 100644 index fc07e689..00000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/package.json.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: 51f5d4f9655c57c32bf114ffbae054ce -TextScriptImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/README.md b/MultiplayerAssets/Assets/Scripts/LiteNetLib/README.md deleted file mode 100644 index dc0d8aa2..00000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/README.md +++ /dev/null @@ -1,117 +0,0 @@ -# LiteNetLib - -Lite reliable UDP library for .NET Standard 2.0 (Mono, .NET Core, .NET Framework) - -[![Made in Ukraine](https://img.shields.io/badge/made_in-ukraine-ffd700.svg?labelColor=0057b7)](https://stand-with-ukraine.pp.ua) - -**HighLevel API Part**: [LiteEntitySystem](https://github.com/RevenantX/LiteEntitySystem) - -**Discord chat**: [![Discord](https://img.shields.io/discord/501682175930925058.svg)](https://discord.gg/FATFPdy) - -[OLD BRANCH (and examples) for 0.9.x](https://github.com/RevenantX/LiteNetLib/tree/0.9) - -[Little Game Example on Unity](https://github.com/RevenantX/NetGameExample) - -[Documentation](https://revenantx.github.io/LiteNetLib/index.html) - -## Build - -### [NuGet](https://www.nuget.org/packages/LiteNetLib/) [![NuGet](https://img.shields.io/nuget/v/LiteNetLib?color=blue)](https://www.nuget.org/packages/LiteNetLib/) [![NuGet](https://img.shields.io/nuget/vpre/LiteNetLib)](https://www.nuget.org/packages/LiteNetLib/#versions-body-tab) [![NuGet](https://img.shields.io/nuget/dt/LiteNetLib)](https://www.nuget.org/packages/LiteNetLib/) - -### [Release builds](https://github.com/RevenantX/LiteNetLib/releases) [![GitHub (pre-)release](https://img.shields.io/github/release/RevenantX/LiteNetLib/all.svg)](https://github.com/RevenantX/LiteNetLib/releases) - -### [DLL build from master](https://ci.appveyor.com/project/RevenantX/litenetlib/branch/master/artifacts) [![](https://ci.appveyor.com/api/projects/status/354501wnvxs8kuh3/branch/master?svg=true)](https://ci.appveyor.com/project/RevenantX/litenetlib/branch/master) -( Warning! Master branch can be unstable! ) - -## Features - -* Lightweight - * Small CPU and RAM usage - * Small packet size overhead ( 1 byte for unreliable, 4 bytes for reliable packets ) -* Simple connection handling -* Peer to peer connections -* Helper classes for sending and reading messages -* Multiple data channels -* Different send mechanics - * Reliable with order - * Reliable without order - * Reliable sequenced (realiable only last packet) - * Ordered but unreliable with duplication prevention - * Simple UDP packets without order and reliability -* Fast packet serializer [(Usage manual)](https://revenantx.github.io/LiteNetLib/articles/netserializerusage.html) -* Automatic small packets merging -* Automatic fragmentation of reliable packets -* Automatic MTU detection -* Optional CRC32C checksums -* UDP NAT hole punching -* NTP time requests -* Packet loss and latency simulation -* IPv6 support (using separate socket for performance) -* Connection statisitcs -* Multicasting (for discovering hosts in local network) -* Unity support -* Supported platforms: - * Windows/Mac/Linux (.NET Framework, Mono, .NET Core, .NET Standard) - * Lumin OS (Magic Leap) - * Monogame - * Godot - * Unity 2018.3 (Desktop platforms, Android, iOS, Switch) - -## Support developer -[!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/revx) - -## Unity notes!!! -* Minimal supported Unity is 2018.3. For older Unity versions use [0.9.x library](https://github.com/RevenantX/LiteNetLib/tree/0.9) versions -* Always use library sources instead of precompiled DLL files ( because there are platform specific #ifdefs and workarounds for unity bugs ) - -## Usage samples - -### Client -```csharp -EventBasedNetListener listener = new EventBasedNetListener(); -NetManager client = new NetManager(listener); -client.Start(); -client.Connect("localhost" /* host ip or name */, 9050 /* port */, "SomeConnectionKey" /* text key or NetDataWriter */); -listener.NetworkReceiveEvent += (fromPeer, dataReader, deliveryMethod, channel) => -{ - Console.WriteLine("We got: {0}", dataReader.GetString(100 /* max length of string */)); - dataReader.Recycle(); -}; - -while (!Console.KeyAvailable) -{ - client.PollEvents(); - Thread.Sleep(15); -} - -client.Stop(); -``` -### Server -```csharp -EventBasedNetListener listener = new EventBasedNetListener(); -NetManager server = new NetManager(listener); -server.Start(9050 /* port */); - -listener.ConnectionRequestEvent += request => -{ - if(server.ConnectedPeersCount < 10 /* max connections */) - request.AcceptIfKey("SomeConnectionKey"); - else - request.Reject(); -}; - -listener.PeerConnectedEvent += peer => -{ - Console.WriteLine("We got connection: {0}", peer.EndPoint); // Show peer ip - NetDataWriter writer = new NetDataWriter(); // Create writer class - writer.Put("Hello client!"); // Put some string - peer.Send(writer, DeliveryMethod.ReliableOrdered); // Send with reliability -}; - -while (!Console.KeyAvailable) -{ - server.PollEvents(); - Thread.Sleep(15); -} -server.Stop(); -``` diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/README.md.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/README.md.meta deleted file mode 100644 index 72d771a2..00000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/README.md.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: 2929904cd5f7a7400b9842994d31f74d -TextScriptImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: From 610c3dfe810665271572439aadf98e552afe85c5 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 19 Jan 2025 15:23:49 +1000 Subject: [PATCH 199/521] Implement TransportLayer interfaces --- .../Components/Networking/NetworkLifecycle.cs | 7 +- .../Managers/Client/NetworkClient.cs | 9 +- .../Networking/Managers/NetworkManager.cs | 87 +++++++----- .../Managers/Server/NetworkServer.cs | 85 ++---------- .../Networking/TransportLayers/ITransport.cs | 35 +++++ .../TransportLayers/LiteNetLibTransport.cs | 129 ++++++++++++++++++ info.json | 2 +- 7 files changed, 239 insertions(+), 115 deletions(-) create mode 100644 Multiplayer/Networking/TransportLayers/ITransport.cs create mode 100644 Multiplayer/Networking/TransportLayers/LiteNetLibTransport.cs diff --git a/Multiplayer/Components/Networking/NetworkLifecycle.cs b/Multiplayer/Components/Networking/NetworkLifecycle.cs index 605f5306..4c2ad93c 100644 --- a/Multiplayer/Components/Networking/NetworkLifecycle.cs +++ b/Multiplayer/Components/Networking/NetworkLifecycle.cs @@ -186,8 +186,11 @@ private IEnumerator PollEvents() tickWatchdog.Stop(time => Multiplayer.LogWarning($"OnTick took {time} ms!")); } - TickManager(Client); - TickManager(Server); + if(Client != null) + TickManager(Client); + + if(Server != null) + TickManager(Server); float elapsedTime = tickTimer.Stop(); float remainingTime = Mathf.Max(0f, TICK_INTERVAL - elapsedTime); diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 3663a363..4b3791de 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -71,8 +71,11 @@ public NetworkClient(Settings settings) : base(settings) public void Start(string address, int port, string password, bool isSinglePlayer, Action onDisconnect) { + LogDebug(() => $"NetworkClient Constructor"); + this.onDisconnect = onDisconnect; - netManager.Start(); + //netManager.Start(); + base.Start(); ServerboundClientLoginPacket serverboundClientLoginPacket = new() { @@ -83,7 +86,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 = Connect(address, port, cachedWriter); isAlsoHost = NetworkLifecycle.Instance.IsServerRunning; originalSession = UserManager.Instance.CurrentUser.CurrentSession; @@ -224,7 +227,7 @@ public override void OnNetworkLatencyUpdate(NetPeer peer, int latency) Ping = latency; } - public override void OnConnectionRequest(ConnectionRequest request) + public override void OnConnectionRequest(NetDataReader dataReader, ConnectionRequest request) { // todo } diff --git a/Multiplayer/Networking/Managers/NetworkManager.cs b/Multiplayer/Networking/Managers/NetworkManager.cs index d633778a..788c1aac 100644 --- a/Multiplayer/Networking/Managers/NetworkManager.cs +++ b/Multiplayer/Networking/Managers/NetworkManager.cs @@ -6,36 +6,43 @@ using Multiplayer.Networking.Data; using Multiplayer.Networking.Data.Train; using Multiplayer.Networking.Serialization; +using Multiplayer.Networking.TransportLayers; namespace Multiplayer.Networking.Managers; -public abstract class NetworkManager : INetEventListener, INatPunchListener +public abstract class NetworkManager { protected readonly NetPacketProcessor netPacketProcessor; - protected readonly NetManager netManager; protected readonly NetDataWriter cachedWriter = new(); + private readonly LiteNetLibTransport transport; + protected readonly NetManager netManager; + protected abstract string LogPrefix { get; } - public NetStatistics Statistics => netManager.Statistics; - public bool IsRunning => netManager.IsRunning; + public NetStatistics Statistics => transport.Statistics; + public bool IsRunning => transport.IsRunning; public bool IsProcessingPacket { get; private set; } protected NetworkManager(Settings settings) { - netManager = new NetManager(this) - { - DisconnectTimeout = 10000, - //ReconnectDelay = 1000, - UnconnectedMessagesEnabled = true, - BroadcastReceiveEnabled = true, + Multiplayer.LogDebug(() => $"NetworkManager Constructor"); - }; netPacketProcessor = new NetPacketProcessor(); + transport = new LiteNetLibTransport(); + + transport.OnConnectionRequest += OnConnectionRequest; + transport.OnPeerConnected += OnPeerConnected; + transport.OnPeerDisconnected += OnPeerDisconnected; + transport.OnNetworkReceive += OnNetworkReceive; + transport.OnNetworkError += OnNetworkError; + + RegisterNestedTypes(); + OnSettingsUpdated(settings); Settings.OnSettingsUpdated += OnSettingsUpdated; - // ReSharper disable once VirtualMemberCallInConstructor + Subscribe(); } @@ -55,27 +62,37 @@ private void RegisterNestedTypes() private void OnSettingsUpdated(Settings settings) { - if (netManager == null) - return; - netManager.NatPunchEnabled = settings.EnableNatPunch; - netManager.AutoRecycle = settings.ReuseNetPacketReaders; - netManager.UseNativeSockets = settings.UseNativeSockets; - netManager.EnableStatistics = settings.ShowStats; - netManager.SimulatePacketLoss = settings.SimulatePacketLoss; - netManager.SimulateLatency = settings.SimulateLatency; - netManager.SimulationPacketLossChance = settings.SimulationPacketLossChance; - netManager.SimulationMinLatency = settings.SimulationMinLatency; - netManager.SimulationMaxLatency = settings.SimulationMaxLatency; + transport?.UpdateSettings(settings); } public void PollEvents() { - netManager.PollEvents(); + //netManager.PollEvents(); + transport?.PollEvents(); } + public virtual bool Start() + { + return transport.Start(); + } + public virtual bool Start(IPAddress ipv4, IPAddress ipv6, int port) + { + return transport.Start(ipv4, ipv6, port); + } + public virtual bool Start(int port) + { + return transport.Start(port); + } + + protected virtual NetPeer Connect(string address, int port, NetDataWriter netDataWriter) + { + return transport.Connect(address, port, netDataWriter); + } + + public virtual void Stop() { - netManager.Stop(true); + transport.Stop(true); Settings.OnSettingsUpdated -= OnSettingsUpdated; } @@ -103,22 +120,21 @@ public virtual void Stop() peer?.Send(WriteNetSerializablePacket(packet), deliveryMethod); } - protected void SendUnconnectedPacket(T packet, string ipAddress, int port) where T : class, new() - { - netManager.SendUnconnectedMessage(WritePacket(packet), ipAddress, port); - } + //protected void SendUnconnectedPacket(T packet, string ipAddress, int port) where T : class, new() + //{ + // transport.SendUnconnectedMessage(WritePacket(packet), ipAddress, port); + //} protected abstract void Subscribe(); #region Net Events - - public void OnNetworkReceive(NetPeer peer, NetPacketReader reader, byte channelNumber, DeliveryMethod deliveryMethod) + public void OnNetworkReceive(NetPeer connection, NetPacketReader reader, byte channel, DeliveryMethod deliveryMethod) { - //Log($"NetworkManager.OnNetworkReceive()"); + LogDebug(() => $"NetworkManager.OnNetworkReceive()"); try { IsProcessingPacket = true; - netPacketProcessor.ReadAllPackets(reader, peer); + netPacketProcessor.ReadAllPackets(reader, connection); } catch (ParseException e) { @@ -156,12 +172,9 @@ public void OnNetworkReceiveUnconnected(IPEndPoint remoteEndPoint, NetPacketRead //Standard networking callbacks public abstract void OnPeerConnected(NetPeer peer); public abstract void OnPeerDisconnected(NetPeer peer, DisconnectInfo disconnectInfo); + public abstract void OnConnectionRequest(NetDataReader requestData, ConnectionRequest request); 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/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index 3fb2f0f0..80eb307e 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -31,8 +31,8 @@ using Multiplayer.Networking.Packets.Serverbound.Train; using Multiplayer.Networking.Packets.Unconnected; using System.Text; -using Steamworks; using Multiplayer.Networking.Data.Train; +using Multiplayer.Networking.TransportLayers; namespace Multiplayer.Networking.Managers.Server; @@ -52,7 +52,7 @@ public class NetworkServer : NetworkManager public RerailController rerailController; public IReadOnlyCollection ServerPlayers => serverPlayers.Values; - public int PlayerCount => netManager.ConnectedPeersCount; + public int PlayerCount => ServerPlayers.Count; private static NetPeer SelfPeer => NetworkLifecycle.Instance.Client?.SelfPeer; public static byte SelfId => (byte)SelfPeer.Id; @@ -66,6 +66,7 @@ public class NetworkServer : NetworkManager public NetworkServer(IDifficulty difficulty, Settings settings, bool isSinglePlayer, LobbyServerData serverData) : base(settings) { + LogDebug(()=>$"NetworkServer Constructor"); this.isSinglePlayer = isSinglePlayer; this.serverData = serverData; @@ -74,14 +75,10 @@ public NetworkServer(IDifficulty difficulty, Settings settings, bool isSinglePla serverMods = ModInfo.FromModEntries(UnityModManager.modEntries) .Where(mod => !modWhiteList.Contains(mod.Id)).ToArray(); - //Start our NAT punch server - if (Multiplayer.Settings.EnableNatPunch) - { - netManager.NatPunchModule.Init(this); - } + } - public bool Start(int port) + public override bool Start(int port) { //setup paint theme lookup cache PaintThemeLookup.Instance.CheckInstance(); @@ -93,12 +90,12 @@ public bool Start(int 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); + return base.Start(IPAddress.Any, ipv6Address,port); + } //we're not running IPv6, start as normal - return netManager.Start(port); + return base.Start(port); } public override void Stop() @@ -209,7 +206,7 @@ public override void OnPeerDisconnected(NetPeer peer, DisconnectInfo disconnectI serverPlayers.Remove(id); netPeers.Remove(id); - netManager.SendToAll(WritePacket(new ClientboundPlayerDisconnectPacket + SendPacketToAll(WritePacket(new ClientboundPlayerDisconnectPacket { Id = id }), DeliveryMethod.ReliableUnordered); @@ -233,70 +230,14 @@ public override void OnNetworkLatencyUpdate(NetPeer peer, int latency) }, DeliveryMethod.ReliableUnordered); } - public override void OnConnectionRequest(ConnectionRequest request) + public override void OnConnectionRequest(NetDataReader requestData, ConnectionRequest request) { + LogDebug(() => $"NetworkServer OnConnectionRequest"); netPacketProcessor.ReadAllPackets(request.Data, request); } #endregion -#region NAT Punch Events - - - public override void OnNatIntroductionRequest(IPEndPoint localEndPoint, IPEndPoint remoteEndPoint, string token) - { - // Validate the Steam ID - if (!IsLegitimateSteamUser()) - { - System.Console.WriteLine($"NAT request rejected from {remoteEndPoint}. Invalid Steam account."); - return; // Reject the NAT punch request - } - - // Proceed with NAT punch-through handling - System.Console.WriteLine($"NAT request accepted from {remoteEndPoint}."); - // Additional logic for NAT punch-through - } - - public override void OnNatIntroductionSuccess(IPEndPoint targetEndPoint, NatAddressType type, string token) - { - System.Console.WriteLine($"NAT punch-through successful to {targetEndPoint}."); - // Additional logic after successful NAT punch-through - } - - // Check for a legitimate Steam account - private bool IsLegitimateSteamUser() - { - // Check if the Steam client is valid - if (SteamClient.IsValid) - { - // Verify the Steam ID is valid - if (SteamClient.SteamId.IsValid) - { - - // Check if the game is installed using the App ID - bool isInstalled = SteamApps.IsAppInstalled(DVSteamworks.APP_ID); - - if (isInstalled) - { - System.Console.WriteLine($"Steam ID {SteamClient.SteamId} is valid and the game is installed."); - return true; - } - else - { - // Log the piracy suspicion - Multiplayer.Log($"Suspicion: Steam ID {SteamClient.SteamId} does not have the game installed. Potential piracy detected."); - } - } - } - - // If Steam client or Steam ID is not valid, log as suspicious - System.Console.WriteLine("Steam client is invalid or pirated Steam account detected."); - Multiplayer.Log("Suspicion: Invalid Steam client or pirated Steam account detected."); - - return false; - } - #endregion - #region Packet Senders private void SendPacketToAll(T packet, DeliveryMethod deliveryMethod) where T : class, new() @@ -632,7 +573,7 @@ private void OnServerboundClientLoginPacket(ServerboundClientLoginPacket packet, return; } - if (netManager.ConnectedPeersCount >= Multiplayer.Settings.MaxPlayers || isSinglePlayer && netManager.ConnectedPeersCount >= 1) + if (PlayerCount >= Multiplayer.Settings.MaxPlayers || isSinglePlayer && PlayerCount >= 1) { LogWarning("Denied login due to server being full!"); ClientboundServerDenyPacket denyPacket = new() @@ -1144,7 +1085,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) diff --git a/Multiplayer/Networking/TransportLayers/ITransport.cs b/Multiplayer/Networking/TransportLayers/ITransport.cs new file mode 100644 index 00000000..8c77a56e --- /dev/null +++ b/Multiplayer/Networking/TransportLayers/ITransport.cs @@ -0,0 +1,35 @@ +using LiteNetLib; +using System.Net.Sockets; +using System.Net; +using System; +using LiteNetLib.Utils; + +namespace Multiplayer.Networking.TransportLayers; +public interface ITransport +{ + NetStatistics Statistics { get; } + bool IsRunning { get; } + + + bool Start(); + bool Start(int port); + bool Start(IPAddress ipv4, IPAddress ipv6, int port); + void Stop(bool sendDisconnectPackets); + void PollEvents(); + void UpdateSettings(Settings settings); + + // Connection management + NetPeer Connect(string address, int port, NetDataWriter data); + void Send(NetPeer peer, NetDataWriter writer, DeliveryMethod deliveryMethod); + + // Events + event Action OnConnectionRequest; + event Action OnPeerConnected; + event Action OnPeerDisconnected; + event Action OnNetworkReceive; + event Action OnNetworkError; + + +} + + diff --git a/Multiplayer/Networking/TransportLayers/LiteNetLibTransport.cs b/Multiplayer/Networking/TransportLayers/LiteNetLibTransport.cs new file mode 100644 index 00000000..fdfc490e --- /dev/null +++ b/Multiplayer/Networking/TransportLayers/LiteNetLibTransport.cs @@ -0,0 +1,129 @@ +using LiteNetLib; +using LiteNetLib.Utils; +using System; +using System.Net; +using System.Net.Sockets; + +namespace Multiplayer.Networking.TransportLayers; + +public class LiteNetLibTransport : ITransport, INetEventListener +{ + public NetStatistics Statistics => netManager.Statistics; + + private readonly NetManager netManager; + + public bool IsRunning => netManager?.IsRunning ?? false; + + public event Action OnConnectionRequest; + public event Action OnPeerConnected; + public event Action OnPeerDisconnected; + public event Action OnNetworkReceive; + public event Action OnNetworkError; + + public LiteNetLibTransport() + { + Multiplayer.LogDebug(() => $"LiteNetLibTransport.LiteNetLibTransport()"); + netManager = new NetManager(this) + { + DisconnectTimeout = 10000, + UnconnectedMessagesEnabled = true, + BroadcastReceiveEnabled = true, + }; + } + + public bool Start() + { + Multiplayer.LogDebug(() => $"LiteNetLibTransport.Start()"); + return netManager.Start(); + } + + public bool Start(int port) + { + Multiplayer.LogDebug(() => $"LiteNetLibTransport.Start({port})"); + return netManager.Start(port); + } + + public bool Start(IPAddress ipv4, IPAddress ipv6, int port) + { + Multiplayer.LogDebug(() => $"LiteNetLibTransport.Start({ipv4}, {ipv6}, {port})"); + return netManager.Start(ipv4, ipv6, port); + } + + public void Stop(bool sendDisconnectPackets) + { + Multiplayer.LogDebug(() => $"LiteNetLibTransport.Stop()"); + netManager.Stop(sendDisconnectPackets); + } + + public void PollEvents() + { + netManager.PollEvents(); + } + + public NetPeer Connect(string address, int port, NetDataWriter data) + { + Multiplayer.LogDebug(() => $"LiteNetLibTransport.Connect({address}, {port})"); + return netManager.Connect(address, port, data); + } + + public void Send(NetPeer peer, NetDataWriter writer, DeliveryMethod deliveryMethod) + { + Multiplayer.LogDebug(() => $"LiteNetLibTransport.Send({peer.Id}, {deliveryMethod})"); + peer.Send(writer, deliveryMethod); + } + + void INetEventListener.OnConnectionRequest(ConnectionRequest request) + { + Multiplayer.LogDebug(() => $"LiteNetLibTransport.INetEventListener.OnConnectionRequest({request.RemoteEndPoint})"); + OnConnectionRequest?.Invoke(request.Data, request); + } + + void INetEventListener.OnPeerConnected(NetPeer peer) + { + Multiplayer.LogDebug(() => $"LiteNetLibTransport.INetEventListener.OnPeerConnected({peer.Id})"); + OnPeerConnected?.Invoke(peer); + } + + void INetEventListener.OnPeerDisconnected(NetPeer peer, DisconnectInfo disconnectInfo) + { + Multiplayer.LogDebug(() => $"LiteNetLibTransport.INetEventListener.OnPeerDisconnected({peer.Id}, {disconnectInfo.Reason})"); + OnPeerDisconnected?.Invoke(peer, disconnectInfo); + } + + void INetEventListener.OnNetworkReceive(NetPeer peer, NetPacketReader reader, byte channelNumber, DeliveryMethod deliveryMethod) + { + Multiplayer.LogDebug(() => $"LiteNetLibTransport.INetEventListener.OnNetworkReceive({peer.Id}, {channelNumber}, {deliveryMethod})"); + OnNetworkReceive?.Invoke(peer, reader, channelNumber, deliveryMethod); + } + + void INetEventListener.OnNetworkError(IPEndPoint endPoint, SocketError socketError) + { + Multiplayer.LogDebug(() => $"LiteNetLibTransport.INetEventListener.OnNetworkError({endPoint}, {socketError})"); + OnNetworkError?.Invoke(endPoint, socketError); + } + + void INetEventListener.OnNetworkReceiveUnconnected(IPEndPoint remoteEndPoint, NetPacketReader reader, UnconnectedMessageType messageType) + { + Multiplayer.LogDebug(() => $"LiteNetLibTransport.INetEventListener.OnNetworkReceiveUnconnected({remoteEndPoint}, {messageType})"); + } + + void INetEventListener.OnNetworkLatencyUpdate(NetPeer peer, int latency) + { + Multiplayer.LogDebug(() => $"LiteNetLibTransport.INetEventListener.OnNetworkLatencyUpdate({peer.Id}, {latency})"); + } + + public void UpdateSettings(Settings settings) + { + //only look at LiteNetLib settings + netManager.NatPunchEnabled = settings.EnableNatPunch; + netManager.AutoRecycle = settings.ReuseNetPacketReaders; + netManager.UseNativeSockets = settings.UseNativeSockets; + netManager.EnableStatistics = settings.ShowStats; + netManager.SimulatePacketLoss = settings.SimulatePacketLoss; + netManager.SimulateLatency = settings.SimulateLatency; + netManager.SimulationPacketLossChance = settings.SimulationPacketLossChance; + netManager.SimulationMinLatency = settings.SimulationMinLatency; + netManager.SimulationMaxLatency = settings.SimulationMaxLatency; + } + +} diff --git a/info.json b/info.json index e9b1b923..7857c50c 100644 --- a/info.json +++ b/info.json @@ -1,6 +1,6 @@ { "Id": "Multiplayer", - "Version": "0.1.9.9", + "Version": "0.1.10.0", "DisplayName": "Multiplayer", "Author": "Insprill, Macka, Morm", "EntryMethod": "Multiplayer.Multiplayer.Load", From 97a2e35b167705b3d97dd77111d5730861457825 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 19 Jan 2025 23:01:20 +1000 Subject: [PATCH 200/521] Implement SteamTransportLayer Layer framework is in place, but more work and testing required --- .../Components/Networking/NetworkLifecycle.cs | 10 +- .../Networking/Train/NetworkedTrainCar.cs | 3 +- .../Managers/Client/NetworkClient.cs | 13 +- .../Networking/Managers/NetworkManager.cs | 27 +- .../Networking/Managers/Server/ChatManager.cs | 21 +- .../Managers/Server/NetworkServer.cs | 172 +++++----- .../Networking/TransportLayers/ITransport.cs | 34 +- .../TransportLayers/LiteNetLibTransport.cs | 140 ++++++-- .../TransportLayers/SteamworksTransport.cs | 303 ++++++++++++++++++ 9 files changed, 573 insertions(+), 150 deletions(-) create mode 100644 Multiplayer/Networking/TransportLayers/SteamworksTransport.cs diff --git a/Multiplayer/Components/Networking/NetworkLifecycle.cs b/Multiplayer/Components/Networking/NetworkLifecycle.cs index 4c2ad93c..0c605f69 100644 --- a/Multiplayer/Components/Networking/NetworkLifecycle.cs +++ b/Multiplayer/Components/Networking/NetworkLifecycle.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Net; using System.Text; using DV.Scenarios.Common; using DV.Utils; @@ -11,6 +12,7 @@ using Multiplayer.Networking.Managers; using Multiplayer.Networking.Managers.Client; using Multiplayer.Networking.Managers.Server; +using Multiplayer.Networking.TransportLayers; using Multiplayer.Utils; using Newtonsoft.Json; using Steamworks; @@ -39,7 +41,7 @@ public class NetworkLifecycle : SingletonBehaviour public bool IsServerRunning => Server?.IsRunning ?? false; public bool IsClientRunning => Client?.IsRunning ?? false; - public bool IsProcessingPacket => Client.IsProcessingPacket; + public bool IsProcessingPacket => Client?.IsProcessingPacket ?? false; private PlayerListGUI playerList; private NetworkStatsGui Stats; @@ -47,10 +49,10 @@ public class NetworkLifecycle : SingletonBehaviour private readonly ExecutionTimer tickWatchdog = new(0.25f); /// - /// Whether the provided NetPeer is the host. + /// Whether the provided ITransportPeer is the host. /// Note that this does NOT check authority, and should only be used for client-only logic. /// - public bool IsHost(NetPeer peer) + public bool IsHost(ITransportPeer peer) { return Server?.IsRunning == true && Client?.IsRunning == true && Client?.SelfPeer?.Id == peer?.Id; } @@ -150,7 +152,7 @@ public bool StartServer(IDifficulty difficulty) return false; Server = server; - StartClient("localhost", port, Multiplayer.Settings.Password, IsSinglePlayer, null/* (DisconnectReason dr,string msg) =>{ }*/); + StartClient(IPAddress.Loopback.ToString(), port, Multiplayer.Settings.Password, IsSinglePlayer, null/* (DisconnectReason dr,string msg) =>{ }*/); return true; } diff --git a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs index 58326561..719800a7 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs @@ -16,6 +16,7 @@ using Multiplayer.Networking.Data.Train; using Multiplayer.Networking.Packets.Clientbound.Train; using Multiplayer.Networking.Packets.Common.Train; +using Multiplayer.Networking.TransportLayers; using Multiplayer.Utils; using UnityEngine; @@ -507,7 +508,7 @@ private void Server_SendHealthState() NetworkLifecycle.Instance.Server.SendCarHealthUpdate(NetId, TrainCar.CarDamage.currentHealth); } - public bool Server_ValidateCouplerInteraction(CommonCouplerInteractionPacket packet, NetPeer peer) + public bool Server_ValidateCouplerInteraction(CommonCouplerInteractionPacket packet, ITransportPeer peer) { Multiplayer.LogDebug(() => $"Server_ValidateCouplerInteraction([[{(CouplerInteractionType)packet.Flags}], {CurrentID}, {packet.NetId}], {peer.Id}) " + diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 4b3791de..9c26f5e0 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -42,6 +42,7 @@ using DV.UserManagement; using DV.Common; using DV.Customization.Paint; +using Multiplayer.Networking.TransportLayers; namespace Multiplayer.Networking.Managers.Client; @@ -51,12 +52,12 @@ public class NetworkClient : NetworkManager private Action onDisconnect; - public NetPeer SelfPeer { get; private set; } + public ITransportPeer SelfPeer { get; private set; } public readonly ClientPlayerManager ClientPlayerManager; // One way ping in milliseconds public int Ping { get; private set; } - private NetPeer serverPeer; + private ITransportPeer serverPeer; private ChatGUI chatGUI; public bool isSinglePlayer; @@ -164,7 +165,7 @@ protected override void Subscribe() #region Net Events - public override void OnPeerConnected(NetPeer peer) + public override void OnPeerConnected(ITransportPeer peer) { serverPeer = peer; if (NetworkLifecycle.Instance.IsHost(peer)) @@ -173,7 +174,7 @@ public override void OnPeerConnected(NetPeer peer) SendSaveGameDataRequest(); } - public override void OnPeerDisconnected(NetPeer peer, DisconnectInfo disconnectInfo) + public override void OnPeerDisconnected(ITransportPeer peer, DisconnectInfo disconnectInfo) { NetworkLifecycle.Instance.Stop(); @@ -222,12 +223,12 @@ public override void OnPeerDisconnected(NetPeer peer, DisconnectInfo disconnectI });*/ } - public override void OnNetworkLatencyUpdate(NetPeer peer, int latency) + public override void OnNetworkLatencyUpdate(ITransportPeer peer, int latency) { Ping = latency; } - public override void OnConnectionRequest(NetDataReader dataReader, ConnectionRequest request) + public override void OnConnectionRequest(NetDataReader dataReader, IConnectionRequest request) { // todo } diff --git a/Multiplayer/Networking/Managers/NetworkManager.cs b/Multiplayer/Networking/Managers/NetworkManager.cs index 788c1aac..b652ae81 100644 --- a/Multiplayer/Networking/Managers/NetworkManager.cs +++ b/Multiplayer/Networking/Managers/NetworkManager.cs @@ -15,7 +15,7 @@ public abstract class NetworkManager protected readonly NetPacketProcessor netPacketProcessor; protected readonly NetDataWriter cachedWriter = new(); - private readonly LiteNetLibTransport transport; + private readonly ITransport transport; protected readonly NetManager netManager; protected abstract string LogPrefix { get; } @@ -29,13 +29,15 @@ protected NetworkManager(Settings settings) Multiplayer.LogDebug(() => $"NetworkManager Constructor"); netPacketProcessor = new NetPacketProcessor(); - transport = new LiteNetLibTransport(); + //transport = new LiteNetLibTransport(); + transport = new SteamWorksTransport(); transport.OnConnectionRequest += OnConnectionRequest; transport.OnPeerConnected += OnPeerConnected; transport.OnPeerDisconnected += OnPeerDisconnected; transport.OnNetworkReceive += OnNetworkReceive; transport.OnNetworkError += OnNetworkError; + transport.OnNetworkLatencyUpdate += OnNetworkLatencyUpdate; RegisterNestedTypes(); @@ -44,6 +46,7 @@ protected NetworkManager(Settings settings) Settings.OnSettingsUpdated += OnSettingsUpdated; Subscribe(); + } private void RegisterNestedTypes() @@ -84,7 +87,7 @@ public virtual bool Start(int port) return transport.Start(port); } - protected virtual NetPeer Connect(string address, int port, NetDataWriter netDataWriter) + protected virtual ITransportPeer Connect(string address, int port, NetDataWriter netDataWriter) { return transport.Connect(address, port, netDataWriter); } @@ -110,12 +113,12 @@ public virtual void Stop() return cachedWriter; } - protected void SendPacket(NetPeer peer, T packet, DeliveryMethod deliveryMethod) where T : class, new() + protected void SendPacket(ITransportPeer peer, T packet, DeliveryMethod deliveryMethod) where T : class, new() { peer?.Send(WritePacket(packet), deliveryMethod); } - protected void SendNetSerializablePacket(NetPeer peer, T packet, DeliveryMethod deliveryMethod) where T : INetSerializable, new() + protected void SendNetSerializablePacket(ITransportPeer peer, T packet, DeliveryMethod deliveryMethod) where T : INetSerializable, new() { peer?.Send(WriteNetSerializablePacket(packet), deliveryMethod); } @@ -128,13 +131,13 @@ public virtual void Stop() protected abstract void Subscribe(); #region Net Events - public void OnNetworkReceive(NetPeer connection, NetPacketReader reader, byte channel, DeliveryMethod deliveryMethod) + public void OnNetworkReceive(ITransportPeer connection, NetDataReader reader, byte channel, DeliveryMethod deliveryMethod) { - LogDebug(() => $"NetworkManager.OnNetworkReceive()"); + //LogDebug(() => $"NetworkManager.OnNetworkReceive()"); try { IsProcessingPacket = true; - netPacketProcessor.ReadAllPackets(reader, connection); + netPacketProcessor.ReadAllPackets(reader, null); } catch (ParseException e) { @@ -170,10 +173,10 @@ public void OnNetworkReceiveUnconnected(IPEndPoint remoteEndPoint, NetPacketRead } //Standard networking callbacks - public abstract void OnPeerConnected(NetPeer peer); - public abstract void OnPeerDisconnected(NetPeer peer, DisconnectInfo disconnectInfo); - public abstract void OnConnectionRequest(NetDataReader requestData, ConnectionRequest request); - public abstract void OnNetworkLatencyUpdate(NetPeer peer, int latency); + public abstract void OnPeerConnected(ITransportPeer peer); + public abstract void OnPeerDisconnected(ITransportPeer peer, DisconnectInfo disconnectInfo); + public abstract void OnConnectionRequest(NetDataReader requestData, IConnectionRequest request); + public abstract void OnNetworkLatencyUpdate(ITransportPeer peer, int latency); #endregion diff --git a/Multiplayer/Networking/Managers/Server/ChatManager.cs b/Multiplayer/Networking/Managers/Server/ChatManager.cs index 8d6a05e8..c480eaad 100644 --- a/Multiplayer/Networking/Managers/Server/ChatManager.cs +++ b/Multiplayer/Networking/Managers/Server/ChatManager.cs @@ -4,6 +4,7 @@ using Multiplayer.Networking.Data; using System.Text.RegularExpressions; using UnityEngine; +using Multiplayer.Networking.TransportLayers; namespace Multiplayer.Networking.Managers.Server; @@ -23,7 +24,7 @@ public static class ChatManager public const string MESSAGE_COLOUR_SERVER = "9CDCFE"; public const string MESSAGE_COLOUR_HELP = "00FF00"; - public static void ProcessMessage(string message, NetPeer sender) + public static void ProcessMessage(string message, ITransportPeer sender) { if (message == null || message == string.Empty) @@ -89,7 +90,7 @@ public static void ProcessMessage(string message, NetPeer sender) ChatMessage(message, player.Username, sender); } - private static void ChatMessage(string message, string sender, NetPeer peer) + private static void ChatMessage(string message, string sender, ITransportPeer peer) { //clean up the message to stop format injection message = Regex.Replace(message, "", string.Empty, RegexOptions.IgnoreCase); @@ -98,7 +99,7 @@ private static void ChatMessage(string message, string sender, NetPeer peer) NetworkLifecycle.Instance.Server.SendChat(message, peer); } - public static void ServerMessage(string message, NetPeer sender, NetPeer exclude = null, int commandLength =-1) + public static void ServerMessage(string message, ITransportPeer sender, ITransportPeer 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)) @@ -114,9 +115,9 @@ public static void ServerMessage(string message, NetPeer sender, NetPeer exclude NetworkLifecycle.Instance.Server.SendChat(message, exclude); } - private static void WhisperMessage(string message, int commandLength, string senderName, NetPeer sender) + private static void WhisperMessage(string message, int commandLength, string senderName, ITransportPeer sender) { - NetPeer recipient; + ITransportPeer recipient; string recipientName; Multiplayer.Log($"Whispering: \"{message}\", sender: {senderName}, senderID: {sender?.Id}"); @@ -171,9 +172,9 @@ 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) + public static void KickMessage(string message, int commandLength, string senderName, ITransportPeer sender) { - NetPeer player; + ITransportPeer player; string playerName; //If user is not the host, we should ignore - will require changes for dedicated server @@ -204,7 +205,7 @@ public static void KickMessage(string message, int commandLength, string senderN NetworkLifecycle.Instance.Server.SendWhisper(message, sender); } - private static void HelpMessage(NetPeer peer) + private static void HelpMessage(ITransportPeer peer) { string message = $"{Locale.CHAT_HELP_AVAILABLE}" + @@ -243,7 +244,7 @@ private static void HelpMessage(NetPeer peer) } - private static NetPeer NetPeerFromName(string peerName) + private static ITransportPeer NetPeerFromName(string peerName) { if(peerName == null || peerName == string.Empty) @@ -253,7 +254,7 @@ private static NetPeer NetPeerFromName(string peerName) if (player == null) return null; - if(NetworkLifecycle.Instance.Server.TryGetNetPeer(player.Id, out NetPeer peer)) + if(NetworkLifecycle.Instance.Server.TryGetPeer(player.Id, out ITransportPeer peer)) { return peer; } diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index 80eb307e..d294264f 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -42,9 +42,9 @@ public class NetworkServer : NetworkManager public Action PlayerDisconnect; protected override string LogPrefix => "[Server]"; - private readonly Queue joinQueue = new(); + private readonly Queue joinQueue = new(); private readonly Dictionary serverPlayers = []; - private readonly Dictionary netPeers = []; + private readonly Dictionary Peers = []; private LobbyServerManager lobbyServerManager; public bool isSinglePlayer; @@ -54,7 +54,7 @@ public class NetworkServer : NetworkManager public IReadOnlyCollection ServerPlayers => serverPlayers.Values; public int PlayerCount => ServerPlayers.Count; - private static NetPeer SelfPeer => NetworkLifecycle.Instance.Client?.SelfPeer; + private static ITransportPeer SelfPeer => NetworkLifecycle.Instance.Client?.SelfPeer; public static byte SelfId => (byte)SelfPeer.Id; private readonly ModInfo[] serverMods; @@ -112,40 +112,40 @@ public override void Stop() protected override void Subscribe() { //Client management - netPacketProcessor.SubscribeReusable(OnServerboundClientLoginPacket); + netPacketProcessor.SubscribeReusable(OnServerboundClientLoginPacket); //World sync - netPacketProcessor.SubscribeReusable(OnServerboundClientReadyPacket); - netPacketProcessor.SubscribeReusable(OnServerboundSaveGameDataRequestPacket); - netPacketProcessor.SubscribeReusable(OnServerboundTimeAdvancePacket); + netPacketProcessor.SubscribeReusable(OnServerboundClientReadyPacket); + netPacketProcessor.SubscribeReusable(OnServerboundSaveGameDataRequestPacket); + netPacketProcessor.SubscribeReusable(OnServerboundTimeAdvancePacket); - netPacketProcessor.SubscribeReusable(OnServerboundPlayerPositionPacket); + netPacketProcessor.SubscribeReusable(OnServerboundPlayerPositionPacket); netPacketProcessor.SubscribeReusable(OnServerboundTrainSyncRequestPacket); - netPacketProcessor.SubscribeReusable(OnServerboundTrainDeleteRequestPacket); - netPacketProcessor.SubscribeReusable(OnServerboundTrainRerailRequestPacket); - netPacketProcessor.SubscribeReusable(OnServerboundLicensePurchaseRequestPacket); - netPacketProcessor.SubscribeReusable(OnCommonChangeJunctionPacket); - netPacketProcessor.SubscribeReusable(OnCommonRotateTurntablePacket); - netPacketProcessor.SubscribeReusable(OnCommonCouplerInteractionPacket); - netPacketProcessor.SubscribeReusable(OnCommonTrainCouplePacket); - netPacketProcessor.SubscribeReusable(OnCommonTrainUncouplePacket); - netPacketProcessor.SubscribeReusable(OnCommonHoseConnectedPacket); - netPacketProcessor.SubscribeReusable(OnCommonHoseDisconnectedPacket); - netPacketProcessor.SubscribeReusable(OnCommonMuConnectedPacket); - netPacketProcessor.SubscribeReusable(OnCommonMuDisconnectedPacket); - netPacketProcessor.SubscribeReusable(OnCommonCockFiddlePacket); - netPacketProcessor.SubscribeReusable(OnCommonBrakeCylinderReleasePacket); - netPacketProcessor.SubscribeReusable(OnCommonHandbrakePositionPacket); - netPacketProcessor.SubscribeReusable(OnCommonPaintThemePacket); - netPacketProcessor.SubscribeReusable(OnServerboundAddCoalPacket); - netPacketProcessor.SubscribeReusable(OnServerboundFireboxIgnitePacket); - netPacketProcessor.SubscribeReusable(OnCommonTrainPortsPacket); - netPacketProcessor.SubscribeReusable(OnCommonTrainFusesPacket); - netPacketProcessor.SubscribeReusable(OnServerboundJobValidateRequestPacket); - netPacketProcessor.SubscribeReusable(OnCommonChatPacket); + netPacketProcessor.SubscribeReusable(OnServerboundTrainDeleteRequestPacket); + netPacketProcessor.SubscribeReusable(OnServerboundTrainRerailRequestPacket); + netPacketProcessor.SubscribeReusable(OnServerboundLicensePurchaseRequestPacket); + netPacketProcessor.SubscribeReusable(OnCommonChangeJunctionPacket); + netPacketProcessor.SubscribeReusable(OnCommonRotateTurntablePacket); + netPacketProcessor.SubscribeReusable(OnCommonCouplerInteractionPacket); + netPacketProcessor.SubscribeReusable(OnCommonTrainCouplePacket); + netPacketProcessor.SubscribeReusable(OnCommonTrainUncouplePacket); + netPacketProcessor.SubscribeReusable(OnCommonHoseConnectedPacket); + netPacketProcessor.SubscribeReusable(OnCommonHoseDisconnectedPacket); + netPacketProcessor.SubscribeReusable(OnCommonMuConnectedPacket); + netPacketProcessor.SubscribeReusable(OnCommonMuDisconnectedPacket); + netPacketProcessor.SubscribeReusable(OnCommonCockFiddlePacket); + netPacketProcessor.SubscribeReusable(OnCommonBrakeCylinderReleasePacket); + netPacketProcessor.SubscribeReusable(OnCommonHandbrakePositionPacket); + netPacketProcessor.SubscribeReusable(OnCommonPaintThemePacket); + netPacketProcessor.SubscribeReusable(OnServerboundAddCoalPacket); + netPacketProcessor.SubscribeReusable(OnServerboundFireboxIgnitePacket); + netPacketProcessor.SubscribeReusable(OnCommonTrainPortsPacket); + netPacketProcessor.SubscribeReusable(OnCommonTrainFusesPacket); + netPacketProcessor.SubscribeReusable(OnServerboundJobValidateRequestPacket); + netPacketProcessor.SubscribeReusable(OnCommonChatPacket); netPacketProcessor.SubscribeReusable(OnUnconnectedPingPacket); - netPacketProcessor.SubscribeNetSerializable(OnCommonItemChangePacket); + netPacketProcessor.SubscribeNetSerializable(OnCommonItemChangePacket); } private void OnLoaded() @@ -161,10 +161,10 @@ private void OnLoaded() while (joinQueue.Count > 0) { - NetPeer peer = joinQueue.Dequeue(); + ITransportPeer peer = joinQueue.Dequeue(); // Assuming the `peer.ConnectionState` property exists and is being checked - if (peer.ConnectionState.Equals(LiteNetLib.ConnectionState.Connected)) + if (peer.ConnectionState.Equals(TransportConnectionState.Connected)) { System.Console.WriteLine("Connection is established."); OnServerboundClientReadyPacket(null, peer); @@ -176,7 +176,7 @@ private void OnLoaded() } } - public bool TryGetServerPlayer(NetPeer peer, out ServerPlayer player) + public bool TryGetServerPlayer(ITransportPeer peer, out ServerPlayer player) { return serverPlayers.TryGetValue((byte)peer.Id, out player); } @@ -185,18 +185,18 @@ public bool TryGetServerPlayer(byte id, out ServerPlayer player) return serverPlayers.TryGetValue(id, out player); } - public bool TryGetNetPeer(byte id, out NetPeer peer) + public bool TryGetPeer(byte id, out ITransportPeer peer) { - return netPeers.TryGetValue(id, out peer); + return Peers.TryGetValue(id, out peer); } #region Net Events - public override void OnPeerConnected(NetPeer peer) + public override void OnPeerConnected(ITransportPeer peer) { } - public override void OnPeerDisconnected(NetPeer peer, DisconnectInfo disconnectInfo) + public override void OnPeerDisconnected(ITransportPeer peer, DisconnectInfo disconnectInfo) { byte id = (byte)peer.Id; Log($"Player {(serverPlayers.TryGetValue(id, out ServerPlayer player) ? player : id)} disconnected: {disconnectInfo.Reason}"); @@ -205,7 +205,7 @@ public override void OnPeerDisconnected(NetPeer peer, DisconnectInfo disconnectI SaveGameManager.Instance.UpdateInternalData(); serverPlayers.Remove(id); - netPeers.Remove(id); + Peers.Remove(id); SendPacketToAll(WritePacket(new ClientboundPlayerDisconnectPacket { Id = id @@ -214,7 +214,7 @@ public override void OnPeerDisconnected(NetPeer peer, DisconnectInfo disconnectI PlayerDisconnect?.Invoke(id); } - public override void OnNetworkLatencyUpdate(NetPeer peer, int latency) + public override void OnNetworkLatencyUpdate(ITransportPeer peer, int latency) { ClientboundPingUpdatePacket clientboundPingUpdatePacket = new() { @@ -230,10 +230,10 @@ public override void OnNetworkLatencyUpdate(NetPeer peer, int latency) }, DeliveryMethod.ReliableUnordered); } - public override void OnConnectionRequest(NetDataReader requestData, ConnectionRequest request) + public override void OnConnectionRequest(NetDataReader requestData, IConnectionRequest request) { LogDebug(() => $"NetworkServer OnConnectionRequest"); - netPacketProcessor.ReadAllPackets(request.Data, request); + netPacketProcessor.ReadAllPackets(requestData, request); } #endregion @@ -243,14 +243,14 @@ public override void OnConnectionRequest(NetDataReader requestData, ConnectionRe private void SendPacketToAll(T packet, DeliveryMethod deliveryMethod) where T : class, new() { NetDataWriter writer = WritePacket(packet); - foreach (KeyValuePair kvp in netPeers) + foreach (KeyValuePair kvp in Peers) kvp.Value.Send(writer, deliveryMethod); } - private void SendPacketToAll(T packet, DeliveryMethod deliveryMethod, NetPeer excludePeer) where T : class, new() + private void SendPacketToAll(T packet, DeliveryMethod deliveryMethod, ITransportPeer excludePeer) where T : class, new() { NetDataWriter writer = WritePacket(packet); - foreach (KeyValuePair kvp in netPeers) + foreach (KeyValuePair kvp in Peers) { if (kvp.Key == excludePeer.Id) continue; @@ -260,14 +260,14 @@ public override void OnConnectionRequest(NetDataReader requestData, ConnectionRe private void SendNetSerializablePacketToAll(T packet, DeliveryMethod deliveryMethod) where T : INetSerializable, new() { NetDataWriter writer = WriteNetSerializablePacket(packet); - foreach (KeyValuePair kvp in netPeers) + foreach (KeyValuePair kvp in Peers) kvp.Value.Send(writer, deliveryMethod); } - private void SendNetSerializablePacketToAll(T packet, DeliveryMethod deliveryMethod, NetPeer excludePeer) where T : INetSerializable, new() + private void SendNetSerializablePacketToAll(T packet, DeliveryMethod deliveryMethod, ITransportPeer excludePeer) where T : INetSerializable, new() { NetDataWriter writer = WriteNetSerializablePacket(packet); - foreach (KeyValuePair kvp in netPeers) + foreach (KeyValuePair kvp in Peers) { if (kvp.Key == excludePeer.Id) continue; @@ -275,7 +275,7 @@ public override void OnConnectionRequest(NetDataReader requestData, ConnectionRe } } - public void KickPlayer(NetPeer peer) + public void KickPlayer(ITransportPeer peer) { peer.Disconnect(WritePacket(new ClientboundPlayerKickPacket())); } @@ -284,7 +284,7 @@ public void SendGameParams(GameParams gameParams) SendPacketToAll(ClientboundGameParamsPacket.FromGameParams(gameParams), DeliveryMethod.ReliableOrdered, SelfPeer); } - public void SendSpawnTrainset(List set, bool autoCouple, bool sendToAll, NetPeer sendTo = null) + public void SendSpawnTrainset(List set, bool autoCouple, bool sendToAll, ITransportPeer sendTo = null) { LogDebug(() => @@ -320,7 +320,7 @@ public void SendSpawnTrainCar(NetworkedTrainCar networkedTrainCar) SendPacketToAll(ClientboundSpawnTrainCarPacket.FromTrainCar(networkedTrainCar), DeliveryMethod.ReliableOrdered, SelfPeer); } - public void SendDestroyTrainCar(ushort netId, NetPeer peer = null) + public void SendDestroyTrainCar(ushort netId, ITransportPeer peer = null) { //ushort netID = trainCar.GetNetId(); LogDebug(() => $"SendDestroyTrainCar({netId})"); @@ -457,7 +457,7 @@ public void SendDebtStatus(bool hasDebt) }, DeliveryMethod.ReliableUnordered, SelfPeer); } - public void SendJobsCreatePacket(NetworkedStationController networkedStation, NetworkedJob[] jobs, NetPeer peer = null) + public void SendJobsCreatePacket(NetworkedStationController networkedStation, NetworkedJob[] jobs, ITransportPeer peer = null) { Multiplayer.Log($"Sending JobsCreatePacket for stationNetId {networkedStation.NetId} with {jobs.Count()} jobs"); @@ -479,14 +479,14 @@ public void SendItemsChangePacket(List items, ServerPlayer playe { Multiplayer.Log($"Sending SendItemsChangePacket with {items.Count()} items to {player.Username}"); - if (TryGetNetPeer(player.Id, out NetPeer peer) && peer != SelfPeer) + if (Peers.TryGetValue(player.Id, out ITransportPeer peer) && peer != SelfPeer) { SendNetSerializablePacket(peer, new CommonItemChangePacket { Items = items }, DeliveryMethod.ReliableOrdered); } } - public void SendChat(string message, NetPeer exclude = null) + public void SendChat(string message, ITransportPeer exclude = null) { if (exclude != null) @@ -505,7 +505,7 @@ public void SendChat(string message, NetPeer exclude = null) } } - public void SendWhisper(string message, NetPeer recipient) + public void SendWhisper(string message, ITransportPeer recipient) { if (message != null || recipient != null) { @@ -521,7 +521,7 @@ public void SendWhisper(string message, NetPeer recipient) #region Listeners - private void OnServerboundClientLoginPacket(ServerboundClientLoginPacket packet, ConnectionRequest request) + private void OnServerboundClientLoginPacket(ServerboundClientLoginPacket packet, IConnectionRequest request) { // 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); @@ -601,7 +601,7 @@ private void OnServerboundClientLoginPacket(ServerboundClientLoginPacket packet, return; } - NetPeer peer = request.Accept(); + ITransportPeer peer = request.Accept(); ServerPlayer serverPlayer = new() { @@ -614,9 +614,9 @@ private void OnServerboundClientLoginPacket(ServerboundClientLoginPacket packet, serverPlayers.Add(serverPlayer.Id, serverPlayer); } - private void OnServerboundSaveGameDataRequestPacket(ServerboundSaveGameDataRequestPacket packet, NetPeer peer) + private void OnServerboundSaveGameDataRequestPacket(ServerboundSaveGameDataRequestPacket packet, ITransportPeer peer) { - if (netPeers.ContainsKey((byte)peer.Id)) + if (Peers.ContainsKey((byte)peer.Id)) { LogWarning("Denied save game data request from already connected peer!"); return; @@ -628,7 +628,7 @@ private void OnServerboundSaveGameDataRequestPacket(ServerboundSaveGameDataReque SendPacket(peer, ClientboundSaveGameDataPacket.CreatePacket(player), DeliveryMethod.ReliableOrdered); } - private void OnServerboundClientReadyPacket(ServerboundClientReadyPacket packet, NetPeer peer) + private void OnServerboundClientReadyPacket(ServerboundClientReadyPacket packet, ITransportPeer peer) { byte peerId = (byte)peer.Id; @@ -645,7 +645,7 @@ private void OnServerboundClientReadyPacket(ServerboundClientReadyPacket packet, AppUtil.Instance.RequestSystemOnValueChanged(0.0f); // Allow the player to receive packets - netPeers.Add(peerId, peer); + Peers.Add(peerId, peer); // Send the new player to all other players ServerPlayer serverPlayer = serverPlayers[peerId]; @@ -740,7 +740,7 @@ private void OnServerboundClientReadyPacket(ServerboundClientReadyPacket packet, serverPlayer.IsLoaded = true; } - private void OnServerboundPlayerPositionPacket(ServerboundPlayerPositionPacket packet, NetPeer peer) + private void OnServerboundPlayerPositionPacket(ServerboundPlayerPositionPacket packet, ITransportPeer peer) { if (TryGetServerPlayer(peer, out ServerPlayer player)) { @@ -763,7 +763,7 @@ private void OnServerboundPlayerPositionPacket(ServerboundPlayerPositionPacket p SendPacketToAll(clientboundPacket, DeliveryMethod.Sequenced, peer); } - private void OnServerboundTimeAdvancePacket(ServerboundTimeAdvancePacket packet, NetPeer peer) + private void OnServerboundTimeAdvancePacket(ServerboundTimeAdvancePacket packet, ITransportPeer peer) { SendPacketToAll(new ClientboundTimeAdvancePacket { @@ -771,17 +771,17 @@ private void OnServerboundTimeAdvancePacket(ServerboundTimeAdvancePacket packet, }, DeliveryMethod.ReliableUnordered, peer); } - private void OnCommonChangeJunctionPacket(CommonChangeJunctionPacket packet, NetPeer peer) + private void OnCommonChangeJunctionPacket(CommonChangeJunctionPacket packet, ITransportPeer peer) { SendPacketToAll(packet, DeliveryMethod.ReliableUnordered, peer); } - private void OnCommonRotateTurntablePacket(CommonRotateTurntablePacket packet, NetPeer peer) + private void OnCommonRotateTurntablePacket(CommonRotateTurntablePacket packet, ITransportPeer peer) { SendPacketToAll(packet, DeliveryMethod.ReliableOrdered, peer); } - private void OnCommonCouplerInteractionPacket(CommonCouplerInteractionPacket packet, NetPeer peer) + private void OnCommonCouplerInteractionPacket(CommonCouplerInteractionPacket packet, ITransportPeer 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 if(NetworkedTrainCar.Get(packet.NetId, out var netTrainCar)) @@ -815,57 +815,57 @@ private void OnCommonCouplerInteractionPacket(CommonCouplerInteractionPacket pac } } - private void OnCommonTrainCouplePacket(CommonTrainCouplePacket packet, NetPeer peer) + private void OnCommonTrainCouplePacket(CommonTrainCouplePacket packet, ITransportPeer peer) { SendPacketToAll(packet, DeliveryMethod.ReliableUnordered, peer); } - private void OnCommonTrainUncouplePacket(CommonTrainUncouplePacket packet, NetPeer peer) + private void OnCommonTrainUncouplePacket(CommonTrainUncouplePacket packet, ITransportPeer peer) { SendPacketToAll(packet, DeliveryMethod.ReliableUnordered, peer); } - private void OnCommonHoseConnectedPacket(CommonHoseConnectedPacket packet, NetPeer peer) + private void OnCommonHoseConnectedPacket(CommonHoseConnectedPacket packet, ITransportPeer peer) { SendPacketToAll(packet, DeliveryMethod.ReliableUnordered, peer); } - private void OnCommonHoseDisconnectedPacket(CommonHoseDisconnectedPacket packet, NetPeer peer) + private void OnCommonHoseDisconnectedPacket(CommonHoseDisconnectedPacket packet, ITransportPeer peer) { SendPacketToAll(packet, DeliveryMethod.ReliableUnordered, peer); } - private void OnCommonMuConnectedPacket(CommonMuConnectedPacket packet, NetPeer peer) + private void OnCommonMuConnectedPacket(CommonMuConnectedPacket packet, ITransportPeer peer) { SendPacketToAll(packet, DeliveryMethod.ReliableUnordered, peer); } - private void OnCommonMuDisconnectedPacket(CommonMuDisconnectedPacket packet, NetPeer peer) + private void OnCommonMuDisconnectedPacket(CommonMuDisconnectedPacket packet, ITransportPeer peer) { SendPacketToAll(packet, DeliveryMethod.ReliableUnordered, peer); } - private void OnCommonCockFiddlePacket(CommonCockFiddlePacket packet, NetPeer peer) + private void OnCommonCockFiddlePacket(CommonCockFiddlePacket packet, ITransportPeer peer) { SendPacketToAll(packet, DeliveryMethod.ReliableUnordered, peer); } - private void OnCommonBrakeCylinderReleasePacket(CommonBrakeCylinderReleasePacket packet, NetPeer peer) + private void OnCommonBrakeCylinderReleasePacket(CommonBrakeCylinderReleasePacket packet, ITransportPeer peer) { SendPacketToAll(packet, DeliveryMethod.ReliableUnordered, peer); } - private void OnCommonHandbrakePositionPacket(CommonHandbrakePositionPacket packet, NetPeer peer) + private void OnCommonHandbrakePositionPacket(CommonHandbrakePositionPacket packet, ITransportPeer peer) { SendPacketToAll(packet, DeliveryMethod.ReliableOrdered, peer); } - private void OnCommonPaintThemePacket(CommonPaintThemePacket packet, NetPeer peer) + private void OnCommonPaintThemePacket(CommonPaintThemePacket packet, ITransportPeer peer) { SendPacketToAll(packet, DeliveryMethod.ReliableOrdered, peer); } - private void OnServerboundAddCoalPacket(ServerboundAddCoalPacket packet, NetPeer peer) + private void OnServerboundAddCoalPacket(ServerboundAddCoalPacket packet, ITransportPeer peer) { if (!TryGetServerPlayer(peer, out ServerPlayer player)) return; @@ -888,7 +888,7 @@ private void OnServerboundAddCoalPacket(ServerboundAddCoalPacket packet, NetPeer } - private void OnServerboundFireboxIgnitePacket(ServerboundFireboxIgnitePacket packet, NetPeer peer) + private void OnServerboundFireboxIgnitePacket(ServerboundFireboxIgnitePacket packet, ITransportPeer peer) { if (!TryGetServerPlayer(peer, out ServerPlayer player)) return; @@ -905,7 +905,7 @@ private void OnServerboundFireboxIgnitePacket(ServerboundFireboxIgnitePacket pac } } - private void OnCommonTrainPortsPacket(CommonTrainPortsPacket packet, NetPeer peer) + private void OnCommonTrainPortsPacket(CommonTrainPortsPacket packet, ITransportPeer peer) { if (!TryGetServerPlayer(peer, out ServerPlayer player)) return; @@ -927,7 +927,7 @@ private void OnCommonTrainPortsPacket(CommonTrainPortsPacket packet, NetPeer pee SendPacketToAll(packet, DeliveryMethod.ReliableOrdered, peer); } - private void OnCommonTrainFusesPacket(CommonTrainFusesPacket packet, NetPeer peer) + private void OnCommonTrainFusesPacket(CommonTrainFusesPacket packet, ITransportPeer peer) { SendPacketToAll(packet, DeliveryMethod.ReliableOrdered, peer); } @@ -938,7 +938,7 @@ private void OnServerboundTrainSyncRequestPacket(ServerboundTrainSyncRequestPack networkedTrainCar.Server_DirtyAllState(); } - private void OnServerboundTrainDeleteRequestPacket(ServerboundTrainDeleteRequestPacket packet, NetPeer peer) + private void OnServerboundTrainDeleteRequestPacket(ServerboundTrainDeleteRequestPacket packet, ITransportPeer peer) { if (!TryGetServerPlayer(peer, out ServerPlayer player)) return; @@ -973,7 +973,7 @@ private void OnServerboundTrainDeleteRequestPacket(ServerboundTrainDeleteRequest CarSpawner.Instance.DeleteCar(trainCar); } - private void OnServerboundTrainRerailRequestPacket(ServerboundTrainRerailRequestPacket packet, NetPeer peer) + private void OnServerboundTrainRerailRequestPacket(ServerboundTrainRerailRequestPacket packet, ITransportPeer peer) { if (!TryGetServerPlayer(peer, out ServerPlayer player)) return; @@ -998,7 +998,7 @@ private void OnServerboundTrainRerailRequestPacket(ServerboundTrainRerailRequest trainCar.Rerail(networkedRailTrack.RailTrack, position, packet.Forward); } - private void OnServerboundLicensePurchaseRequestPacket(ServerboundLicensePurchaseRequestPacket packet, NetPeer peer) + private void OnServerboundLicensePurchaseRequestPacket(ServerboundLicensePurchaseRequestPacket packet, ITransportPeer peer) { if (!TryGetServerPlayer(peer, out ServerPlayer player)) return; @@ -1035,7 +1035,7 @@ private void OnServerboundLicensePurchaseRequestPacket(ServerboundLicensePurchas } - private void OnServerboundJobValidateRequestPacket(ServerboundJobValidateRequestPacket packet, NetPeer peer) + private void OnServerboundJobValidateRequestPacket(ServerboundJobValidateRequestPacket packet, ITransportPeer peer) { Log($"OnServerboundJobValidateRequestPacket(): {packet.JobNetId}"); @@ -1075,7 +1075,7 @@ private void OnServerboundJobValidateRequestPacket(ServerboundJobValidateRequest //SendPacket(peer, new ClientboundJobValidateResponsePacket { JobNetId = packet.JobNetId, Invalid = false }, DeliveryMethod.ReliableUnordered); } - private void OnCommonChatPacket(CommonChatPacket packet, NetPeer peer) + private void OnCommonChatPacket(CommonChatPacket packet, ITransportPeer peer) { ChatManager.ProcessMessage(packet.message, peer); } @@ -1088,7 +1088,7 @@ private void OnUnconnectedPingPacket(UnconnectedPingPacket packet, IPEndPoint en //SendUnconnectedPacket(packet, endPoint.Address.ToString(), endPoint.Port); } - private void OnCommonItemChangePacket(CommonItemChangePacket packet, NetPeer peer) + private void OnCommonItemChangePacket(CommonItemChangePacket packet, ITransportPeer peer) { //if(!TryGetServerPlayer(peer, out var player)) // return; diff --git a/Multiplayer/Networking/TransportLayers/ITransport.cs b/Multiplayer/Networking/TransportLayers/ITransport.cs index 8c77a56e..97b676a1 100644 --- a/Multiplayer/Networking/TransportLayers/ITransport.cs +++ b/Multiplayer/Networking/TransportLayers/ITransport.cs @@ -19,17 +19,37 @@ public interface ITransport void UpdateSettings(Settings settings); // Connection management - NetPeer Connect(string address, int port, NetDataWriter data); - void Send(NetPeer peer, NetDataWriter writer, DeliveryMethod deliveryMethod); + ITransportPeer Connect(string address, int port, NetDataWriter data); + void Send(ITransportPeer peer, NetDataWriter writer, DeliveryMethod deliveryMethod); // Events - event Action OnConnectionRequest; - event Action OnPeerConnected; - event Action OnPeerDisconnected; - event Action OnNetworkReceive; + event Action OnConnectionRequest; + event Action OnPeerConnected; + event Action OnPeerDisconnected; + event Action OnNetworkReceive; event Action OnNetworkError; + event Action OnNetworkLatencyUpdate; +} - +public interface IConnectionRequest +{ + ITransportPeer Accept(); + void Reject(NetDataWriter data = null); + IPEndPoint RemoteEndPoint { get; } } +public interface ITransportPeer +{ + int Id { get; } + TransportConnectionState ConnectionState { get; } + void Send(NetDataWriter writer, DeliveryMethod deliveryMethod); + void Disconnect(NetDataWriter data = null); +} +public enum TransportConnectionState +{ + Connected, + Connecting, + Disconnected, + Disconnecting +} diff --git a/Multiplayer/Networking/TransportLayers/LiteNetLibTransport.cs b/Multiplayer/Networking/TransportLayers/LiteNetLibTransport.cs index fdfc490e..3811dab7 100644 --- a/Multiplayer/Networking/TransportLayers/LiteNetLibTransport.cs +++ b/Multiplayer/Networking/TransportLayers/LiteNetLibTransport.cs @@ -1,25 +1,31 @@ using LiteNetLib; using LiteNetLib.Utils; using System; +using System.Collections.Generic; +using System.Linq; using System.Net; using System.Net.Sockets; +using static DV.UI.ATutorialsMenuProvider; namespace Multiplayer.Networking.TransportLayers; public class LiteNetLibTransport : ITransport, INetEventListener { public NetStatistics Statistics => netManager.Statistics; - - private readonly NetManager netManager; - public bool IsRunning => netManager?.IsRunning ?? false; - public event Action OnConnectionRequest; - public event Action OnPeerConnected; - public event Action OnPeerDisconnected; - public event Action OnNetworkReceive; + public event Action OnConnectionRequest; + public event Action OnPeerConnected; + public event Action OnPeerDisconnected; + public event Action OnNetworkReceive; public event Action OnNetworkError; + public event Action OnNetworkLatencyUpdate; + + private readonly Dictionary netPeerToPeer = []; + private readonly NetManager netManager; + + #region ITransport public LiteNetLibTransport() { Multiplayer.LogDebug(() => $"LiteNetLibTransport.LiteNetLibTransport()"); @@ -60,40 +66,58 @@ public void PollEvents() netManager.PollEvents(); } - public NetPeer Connect(string address, int port, NetDataWriter data) + public ITransportPeer Connect(string address, int port, NetDataWriter data) { - Multiplayer.LogDebug(() => $"LiteNetLibTransport.Connect({address}, {port})"); - return netManager.Connect(address, port, data); + var netPeer = netManager.Connect(address, port, data); + var peer = new LiteNetLibPeer(netPeer); + + Multiplayer.LogDebug(() => $"LiteNetLibTransport.Connect length: {data.Length}. packet: {BitConverter.ToString(data.Data)}"); + + netPeerToPeer[netPeer] = peer; + return peer; } - public void Send(NetPeer peer, NetDataWriter writer, DeliveryMethod deliveryMethod) + public void Send(ITransportPeer peer, NetDataWriter writer, DeliveryMethod deliveryMethod) { - Multiplayer.LogDebug(() => $"LiteNetLibTransport.Send({peer.Id}, {deliveryMethod})"); - peer.Send(writer, deliveryMethod); + var litePeer = (LiteNetLibPeer)peer; + litePeer.Send(writer, deliveryMethod); } + #endregion + #region INetEventListener void INetEventListener.OnConnectionRequest(ConnectionRequest request) { - Multiplayer.LogDebug(() => $"LiteNetLibTransport.INetEventListener.OnConnectionRequest({request.RemoteEndPoint})"); - OnConnectionRequest?.Invoke(request.Data, request); + //Multiplayer.LogDebug(() => $"LiteNetLibTransport.INetEventListener.OnConnectionRequest({request.RemoteEndPoint})"); + OnConnectionRequest?.Invoke(request.Data, new LiteNetLibConnectionRequest(request)); } - void INetEventListener.OnPeerConnected(NetPeer peer) + void INetEventListener.OnPeerConnected(NetPeer netPeer) { - Multiplayer.LogDebug(() => $"LiteNetLibTransport.INetEventListener.OnPeerConnected({peer.Id})"); + var peer = new LiteNetLibPeer(netPeer); + + netPeerToPeer[netPeer] = peer; + OnPeerConnected?.Invoke(peer); } - void INetEventListener.OnPeerDisconnected(NetPeer peer, DisconnectInfo disconnectInfo) + void INetEventListener.OnPeerDisconnected(NetPeer netPeer, DisconnectInfo disconnectInfo) { - Multiplayer.LogDebug(() => $"LiteNetLibTransport.INetEventListener.OnPeerDisconnected({peer.Id}, {disconnectInfo.Reason})"); + if(!netPeerToPeer.TryGetValue(netPeer, out var peer)) + return; + OnPeerDisconnected?.Invoke(peer, disconnectInfo); + + netPeerToPeer.Remove(netPeer); + CleanupPeerDictionaries(); } - void INetEventListener.OnNetworkReceive(NetPeer peer, NetPacketReader reader, byte channelNumber, DeliveryMethod deliveryMethod) + + void INetEventListener.OnNetworkReceive(NetPeer netPeer, NetPacketReader reader, byte channelNumber, DeliveryMethod deliveryMethod) { - Multiplayer.LogDebug(() => $"LiteNetLibTransport.INetEventListener.OnNetworkReceive({peer.Id}, {channelNumber}, {deliveryMethod})"); - OnNetworkReceive?.Invoke(peer, reader, channelNumber, deliveryMethod); + Multiplayer.LogDebug(() => $"LiteNetLibTransport.OnNetworkReceive length: {reader.AvailableBytes}. packet: {BitConverter.ToString(reader.RawData)}"); + + if (netPeerToPeer.TryGetValue(netPeer, out var peer)) + OnNetworkReceive?.Invoke(peer, reader, channelNumber, deliveryMethod); } void INetEventListener.OnNetworkError(IPEndPoint endPoint, SocketError socketError) @@ -107,13 +131,17 @@ void INetEventListener.OnNetworkReceiveUnconnected(IPEndPoint remoteEndPoint, Ne Multiplayer.LogDebug(() => $"LiteNetLibTransport.INetEventListener.OnNetworkReceiveUnconnected({remoteEndPoint}, {messageType})"); } - void INetEventListener.OnNetworkLatencyUpdate(NetPeer peer, int latency) + void INetEventListener.OnNetworkLatencyUpdate(NetPeer netPeer, int latency) { - Multiplayer.LogDebug(() => $"LiteNetLibTransport.INetEventListener.OnNetworkLatencyUpdate({peer.Id}, {latency})"); + if (netPeerToPeer.TryGetValue(netPeer, out var peer)) + OnNetworkLatencyUpdate?.Invoke(peer, latency); } + #endregion + public void UpdateSettings(Settings settings) { + Multiplayer.LogDebug(() => $"LiteNetLibTransport.INetEventListener.UpdateSettings()"); //only look at LiteNetLib settings netManager.NatPunchEnabled = settings.EnableNatPunch; netManager.AutoRecycle = settings.ReuseNetPacketReaders; @@ -126,4 +154,68 @@ public void UpdateSettings(Settings settings) netManager.SimulationMaxLatency = settings.SimulationMaxLatency; } + private void CleanupPeerDictionaries() + { + var nullPeers = netPeerToPeer.Where(kvp => kvp.Key == null || kvp.Value == null).ToList(); + foreach (var pair in nullPeers) + { + netPeerToPeer.Remove(pair.Key); + } + } + + +} + +public class LiteNetLibConnectionRequest : IConnectionRequest +{ + private readonly ConnectionRequest request; + + public LiteNetLibConnectionRequest(ConnectionRequest request) + { + this.request = request; + } + + public ITransportPeer Accept() + { + var peer = request.Accept(); + return new LiteNetLibPeer(peer); + } + + public void Reject(NetDataWriter data = null) + { + request.Reject(data); + } + + public IPEndPoint RemoteEndPoint => request.RemoteEndPoint; +} + +public class LiteNetLibPeer : ITransportPeer +{ + private readonly NetPeer peer; + public int Id => peer.Id; + + public LiteNetLibPeer(NetPeer peer) + { + this.peer = peer; + } + + public void Send(NetDataWriter writer, DeliveryMethod deliveryMethod) + { + peer.Send(writer, deliveryMethod); + } + + public void Disconnect(NetDataWriter data = null) + { + peer.Disconnect(data); + } + + public TransportConnectionState ConnectionState => peer.ConnectionState switch + { + LiteNetLib.ConnectionState.Connected => TransportConnectionState.Connected, + LiteNetLib.ConnectionState.Outgoing => TransportConnectionState.Connecting, + LiteNetLib.ConnectionState.Disconnected => TransportConnectionState.Disconnected, + LiteNetLib.ConnectionState.ShutdownRequested => TransportConnectionState.Disconnecting, + _ => TransportConnectionState.Disconnected + }; + } diff --git a/Multiplayer/Networking/TransportLayers/SteamworksTransport.cs b/Multiplayer/Networking/TransportLayers/SteamworksTransport.cs new file mode 100644 index 00000000..cbc84255 --- /dev/null +++ b/Multiplayer/Networking/TransportLayers/SteamworksTransport.cs @@ -0,0 +1,303 @@ +using LiteNetLib.Utils; +using LiteNetLib; +using System; +using System.Net.Sockets; +using System.Net; +using Steamworks; +using System.Collections.Generic; +using Steamworks.Data; +using System.Runtime.InteropServices; + + +namespace Multiplayer.Networking.TransportLayers; + +public class SteamWorksTransport : ITransport +{ + public NetStatistics Statistics => new NetStatistics(); + public bool IsRunning => server != null; + + public event Action OnConnectionRequest; + public event Action OnPeerConnected; + public event Action OnPeerDisconnected; + public event Action OnNetworkReceive; + public event Action OnNetworkError; + public event Action OnNetworkLatencyUpdate; + + private SteamSocketManager server; + private SteamConnectionManager client; + + + private readonly Dictionary peerIdToPeer = []; + private readonly Dictionary connectionToPeer = []; + + private int nextPeerId = 1; + + #region ITransport + public SteamWorksTransport() + { + //static fields for SteamNetworking + } + + public bool Start() + { + Multiplayer.LogDebug(() => $"SteamWorksTransport.Start()"); + return true;//return Start(0); + } + + public bool Start(int port) + { + Multiplayer.LogDebug(() => $"SteamWorksTransport.Start({port})"); + server = SteamNetworkingSockets.CreateNormalSocket(NetAddress.AnyIp((ushort)port)); + server.transport = this; + + return server != null; + } + + public bool Start(IPAddress ipv4, IPAddress ipv6, int port) + { + Multiplayer.LogDebug(() => $"SteamWorksTransport.Start({ipv4}, {ipv6}, {port})"); + return Start(port); + } + + public void Stop(bool sendDisconnectPackets) + { + Multiplayer.LogDebug(() => $"SteamWorksTransport.Stop()"); + if (server != null) + { + foreach (var connection in server.Connected) + { + connection.Close(); + } + server = null; + } + } + + public void PollEvents() + { + SteamClient.RunCallbacks(); + client?.Receive(); + server?.Receive(); + } + + public ITransportPeer Connect(string address, int port, NetDataWriter data) + { + Multiplayer.LogDebug(() => $"SteamWorksTransport.Connect({address}, {port}, {data.Length})"); + + var add = NetAddress.From(address, (ushort)port); + + + Multiplayer.LogDebug(() => $"SteamSocketManager.Connect packet: {BitConverter.ToString(data.Data)}"); + + // Create connection manager for client + client = SteamNetworkingSockets.ConnectNormal(add); + client.transport = this; + client.loginPacket = data; + client.peer = CreatePeer(client.Connection); + + return client.peer; + } + + + public void Send(ITransportPeer peer, NetDataWriter writer, DeliveryMethod deliveryMethod) + { + Multiplayer.LogDebug(() => $"SteamWorksTransport.Send({peer.Id}, {deliveryMethod})"); + peer.Send(writer, deliveryMethod); + } + + public void UpdateSettings(Settings settings) + { + //todo: implement any settings + } + + #endregion + + #region SteamManagers + public class SteamSocketManager : SocketManager + { + public SteamWorksTransport transport; + + public override void OnConnecting(Connection connection, ConnectionInfo info) + { + + Multiplayer.LogDebug(() => $"SteamSocketManager.OnConnecting({connection}, {info})"); + connection.Accept(); + } + + public override void OnConnected(Connection connection, ConnectionInfo info) + { + Multiplayer.LogDebug(() => $"SteamSocketManager.OnConnected({connection}, {info})"); + base.OnConnected(connection, info); + + var peer = transport.CreatePeer(connection); + peer.connectionRequest = new SteamConnectionRequest(connection, info, peer); + } + + public override void OnDisconnected(Steamworks.Data.Connection connection, Steamworks.Data.ConnectionInfo info) + { + Multiplayer.LogDebug(() => $"SteamSocketManager.OnDisconnected({connection}, {info})"); + base.OnDisconnected(connection, info); + throw new NotImplementedException(); + } + + public override void OnMessage(Steamworks.Data.Connection connection, Steamworks.Data.NetIdentity identity, IntPtr data, int size, long messageNum, long recvTime, int channel) + { + Multiplayer.LogDebug(() => $"SteamSocketManager.OnMessage({connection}, {identity}, , {size}, {messageNum}, {recvTime}, {channel})"); + + var peer = transport.GetPeer(connection); + + byte[] buffer = new byte[size]; + Marshal.Copy(data, buffer, 0, size); + + + Multiplayer.LogDebug(() => $"SteamSocketManager.Received packet: {BitConverter.ToString(buffer)}"); + + var reader = new NetDataReader(buffer,0,size); + if(peer.connectionRequest != null) + { + transport?.OnConnectionRequest?.Invoke(reader, peer.connectionRequest); + peer.connectionRequest = null; + return; + } + + transport?.OnNetworkReceive?.Invoke(peer, reader, (byte)channel, DeliveryMethod.ReliableOrdered); + } + + public override void OnConnectionChanged(Steamworks.Data.Connection connection, Steamworks.Data.ConnectionInfo info) + { + Multiplayer.LogDebug(() => $"SteamSocketManager.OnConnectionChanged({connection}, {info})"); + base.OnConnectionChanged(connection, info); + } + } + + public class SteamConnectionManager : ConnectionManager + { + public SteamWorksTransport transport; + public NetDataWriter loginPacket; + public SteamPeer peer; + + public override void OnConnected(ConnectionInfo info) + { + Multiplayer.LogDebug(() => $"SteamConnectionManager.OnConnected({info})"); + peer.Send(loginPacket, DeliveryMethod.ReliableUnordered); + } + + public override void OnConnecting(ConnectionInfo info) + { + Multiplayer.LogDebug(() => $"SteamConnectionManager.OnConnecting({info})"); + } + + public override void OnDisconnected(ConnectionInfo info) + { + Multiplayer.LogDebug(() => $"SteamConnectionManager.ConnectionOnDisconnected({info})"); + } + + public override void OnMessage(IntPtr data, int size, long messageNum, long recvTime, int channel) + { + Multiplayer.LogDebug(() => $"SteamConnectionManager.Connection(,{size}, {messageNum}, {recvTime}, {channel})"); + + byte[] buffer = new byte[size]; + Marshal.Copy(data, buffer, 0, size); + + var reader = new NetDataReader(buffer, 0, size); + transport?.OnNetworkReceive(peer, reader, (byte)channel, DeliveryMethod.ReliableOrdered); + } + } + #endregion + + + private SteamPeer CreatePeer(Connection connection) + { + var peer = new SteamPeer(nextPeerId++, connection); + connectionToPeer[connection] = peer; + peerIdToPeer[peer.Id] = peer; + return peer; + } + + private SteamPeer GetPeer(Connection connection) + { + return connectionToPeer.TryGetValue(connection, out var peer) ? peer : null; + } +} + +public class SteamConnectionRequest : IConnectionRequest +{ + private readonly Connection connection; + private readonly ConnectionInfo connectionInfo; + private readonly SteamPeer peer; + + public SteamConnectionRequest(Connection connection, ConnectionInfo connectionInfo, SteamPeer peer) + { + this.connection = connection; + this.connectionInfo = connectionInfo; + this.peer = peer; + } + + public ITransportPeer Accept() + { + return peer; + } + public void Reject(NetDataWriter data = null) + { + if (data != null) + peer.Send(data, DeliveryMethod.ReliableUnordered); + connection.Close(); + } + + public IPEndPoint RemoteEndPoint => new(IPAddress.Any, 0); +} + + +public class SteamPeer : ITransportPeer +{ + private readonly Connection connection; + private TransportConnectionState _currentState; + public SteamConnectionRequest connectionRequest; + public int Id { get; } + + public SteamPeer(int id, Connection connection) + { + Id = (int)id; + this.connection = connection; + } + + public void Send(NetDataWriter writer, DeliveryMethod deliveryMethod) + { + + // Map LiteNetLib delivery method to Steam's SendType + SendType sendType = deliveryMethod switch + { + DeliveryMethod.ReliableOrdered => SendType.Reliable, + DeliveryMethod.ReliableUnordered => SendType.Reliable, + DeliveryMethod.Unreliable => SendType.Unreliable, + DeliveryMethod.ReliableSequenced => SendType.Reliable, + DeliveryMethod.Sequenced => SendType.Unreliable, + _ => SendType.Reliable + }; + + connection.SendMessage(writer.Data, 0, writer.Length, sendType); + } + + public void Disconnect(NetDataWriter data = null) + { + connection.Close(); + } + + public void OnConnectionStatusChanged(Steamworks.ConnectionState state) + { + + _currentState = state switch + { + Steamworks.ConnectionState.Connected => TransportConnectionState.Connected, + Steamworks.ConnectionState.Connecting => TransportConnectionState.Connecting, + Steamworks.ConnectionState.FindingRoute => TransportConnectionState.Connecting, + Steamworks.ConnectionState.ClosedByPeer => TransportConnectionState.Disconnected, + Steamworks.ConnectionState.ProblemDetectedLocally => TransportConnectionState.Disconnected, + Steamworks.ConnectionState.FinWait => TransportConnectionState.Disconnecting, + Steamworks.ConnectionState.Linger => TransportConnectionState.Disconnecting, + Steamworks.ConnectionState.Dead => TransportConnectionState.Disconnected, + Steamworks.ConnectionState.None => TransportConnectionState.Disconnected, + _ => TransportConnectionState.Disconnected + }; + } + public TransportConnectionState ConnectionState => _currentState; +} From fcb094c36a4a17357b4d565057409d12f13dd7a6 Mon Sep 17 00:00:00 2001 From: AMacro Date: Mon, 20 Jan 2025 22:36:35 +1000 Subject: [PATCH 201/521] Refactor of login sequence --- .../Managers/Client/NetworkClient.cs | 28 +++++++++---------- .../Managers/Server/NetworkServer.cs | 21 +++++++++++--- ...t.cs => ClientboundLoginResponsePacket.cs} | 3 +- 3 files changed, 33 insertions(+), 19 deletions(-) rename Multiplayer/Networking/Packets/Clientbound/{ClientboundServerDenyPacket.cs => ClientboundLoginResponsePacket.cs} (80%) diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 9c26f5e0..e9d31bfa 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -110,7 +110,7 @@ public override void Stop() protected override void Subscribe() { - netPacketProcessor.SubscribeReusable(OnClientboundServerDenyPacket); + netPacketProcessor.SubscribeReusable(OnClientboundLoginResponsePacket); netPacketProcessor.SubscribeReusable(OnClientboundPlayerJoinedPacket); netPacketProcessor.SubscribeReusable(OnClientboundPlayerDisconnectPacket); netPacketProcessor.SubscribeReusable(OnClientboundPlayerKickPacket); @@ -168,10 +168,6 @@ protected override void Subscribe() public override void OnPeerConnected(ITransportPeer peer) { serverPeer = peer; - if (NetworkLifecycle.Instance.IsHost(peer)) - SendReadyPacket(); - else - SendSaveGameDataRequest(); } public override void OnPeerDisconnected(ITransportPeer peer, DisconnectInfo disconnectInfo) @@ -238,16 +234,22 @@ public override void OnConnectionRequest(NetDataReader dataReader, IConnectionRe #region Listeners - private void OnClientboundServerDenyPacket(ClientboundServerDenyPacket packet) + private void OnClientboundLoginResponsePacket(ClientboundLoginResponsePacket packet) { - /* - NetworkLifecycle.Instance.QueueMainMenuEvent(() => + if (packet.Accepted) { - Popup popup = MainMenuThingsAndStuff.Instance.ShowOkPopup(); - if (popup == null) - return; - */ + Log($"Received player accepted packet"); + + if (NetworkLifecycle.Instance.IsHost(SelfPeer)) + SendReadyPacket(); + else + SendSaveGameDataRequest(); + + return; + } + + string text = Locale.Get(packet.ReasonKey, packet.ReasonArgs); if (packet.Missing.Length != 0 || packet.Extra.Length != 0) @@ -264,8 +266,6 @@ private void OnClientboundServerDenyPacket(ClientboundServerDenyPacket packet) 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}"); onDisconnect(DisconnectReason.ConnectionRejected, text); } diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index d294264f..c179ab7b 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -553,7 +553,7 @@ private void OnServerboundClientLoginPacket(ServerboundClientLoginPacket packet, if (Multiplayer.Settings.Password != packet.Password) { LogWarning("Denied login due to invalid password!"); - ClientboundServerDenyPacket denyPacket = new() + ClientboundLoginResponsePacket denyPacket = new() { ReasonKey = Locale.DISCONN_REASON__INVALID_PASSWORD_KEY }; @@ -564,7 +564,7 @@ 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() + ClientboundLoginResponsePacket denyPacket = new() { ReasonKey = Locale.DISCONN_REASON__GAME_VERSION_KEY, ReasonArgs = [BuildInfo.BUILD_VERSION_MAJOR.ToString(), packet.BuildMajorVersion.ToString()] @@ -576,7 +576,7 @@ private void OnServerboundClientLoginPacket(ServerboundClientLoginPacket packet, if (PlayerCount >= Multiplayer.Settings.MaxPlayers || isSinglePlayer && PlayerCount >= 1) { LogWarning("Denied login due to server being full!"); - ClientboundServerDenyPacket denyPacket = new() + ClientboundLoginResponsePacket denyPacket = new() { ReasonKey = Locale.DISCONN_REASON__FULL_SERVER_KEY }; @@ -591,7 +591,7 @@ private void OnServerboundClientLoginPacket(ServerboundClientLoginPacket packet, ModInfo[] extra = clientMods.Except(serverMods).ToArray(); LogWarning($"Denied login due to mod mismatch! {missing.Length} missing, {extra.Length} extra"); - ClientboundServerDenyPacket denyPacket = new() + ClientboundLoginResponsePacket denyPacket = new() { ReasonKey = Locale.DISCONN_REASON__MODS_KEY, Missing = missing, @@ -612,6 +612,13 @@ private void OnServerboundClientLoginPacket(ServerboundClientLoginPacket packet, }; serverPlayers.Add(serverPlayer.Id, serverPlayer); + + ClientboundLoginResponsePacket acceptPacket = new() + { + Accepted = true, + }; + + SendPacket(peer, acceptPacket, DeliveryMethod.ReliableUnordered); } private void OnServerboundSaveGameDataRequestPacket(ServerboundSaveGameDataRequestPacket packet, ITransportPeer peer) @@ -630,6 +637,12 @@ private void OnServerboundSaveGameDataRequestPacket(ServerboundSaveGameDataReque private void OnServerboundClientReadyPacket(ServerboundClientReadyPacket packet, ITransportPeer peer) { + if(peer == null) + { + LogError($"OnServerboundClientReadyPacket() peer is null!"); + return; + } + byte peerId = (byte)peer.Id; // Allow clients to connect before the server is fully loaded diff --git a/Multiplayer/Networking/Packets/Clientbound/ClientboundServerDenyPacket.cs b/Multiplayer/Networking/Packets/Clientbound/ClientboundLoginResponsePacket.cs similarity index 80% rename from Multiplayer/Networking/Packets/Clientbound/ClientboundServerDenyPacket.cs rename to Multiplayer/Networking/Packets/Clientbound/ClientboundLoginResponsePacket.cs index 4f77ed32..4e6553f4 100644 --- a/Multiplayer/Networking/Packets/Clientbound/ClientboundServerDenyPacket.cs +++ b/Multiplayer/Networking/Packets/Clientbound/ClientboundLoginResponsePacket.cs @@ -3,8 +3,9 @@ namespace Multiplayer.Networking.Packets.Clientbound; -public class ClientboundServerDenyPacket +public class ClientboundLoginResponsePacket { + public bool Accepted { get; set; } public string ReasonKey { get; set; } public string[] ReasonArgs { get; set; } public ModInfo[] Missing { get; set; } = Array.Empty(); From 1ab627e75ec9b43b0ef50b5dc6a9f4fd46c0b2dc Mon Sep 17 00:00:00 2001 From: AMacro Date: Mon, 20 Jan 2025 22:36:56 +1000 Subject: [PATCH 202/521] Continuation of TransportLayers --- .../Networking/Managers/NetworkManager.cs | 4 +- .../TransportLayers/LiteNetLibTransport.cs | 25 +++- .../TransportLayers/SteamworksTransport.cs | 128 ++++++++++++++---- 3 files changed, 119 insertions(+), 38 deletions(-) diff --git a/Multiplayer/Networking/Managers/NetworkManager.cs b/Multiplayer/Networking/Managers/NetworkManager.cs index b652ae81..f214934e 100644 --- a/Multiplayer/Networking/Managers/NetworkManager.cs +++ b/Multiplayer/Networking/Managers/NetworkManager.cs @@ -131,13 +131,13 @@ public virtual void Stop() protected abstract void Subscribe(); #region Net Events - public void OnNetworkReceive(ITransportPeer connection, NetDataReader reader, byte channel, DeliveryMethod deliveryMethod) + public void OnNetworkReceive(ITransportPeer peer, NetDataReader reader, byte channel, DeliveryMethod deliveryMethod) { //LogDebug(() => $"NetworkManager.OnNetworkReceive()"); try { IsProcessingPacket = true; - netPacketProcessor.ReadAllPackets(reader, null); + netPacketProcessor.ReadAllPackets(reader, peer); } catch (ParseException e) { diff --git a/Multiplayer/Networking/TransportLayers/LiteNetLibTransport.cs b/Multiplayer/Networking/TransportLayers/LiteNetLibTransport.cs index 3811dab7..b1860ca2 100644 --- a/Multiplayer/Networking/TransportLayers/LiteNetLibTransport.cs +++ b/Multiplayer/Networking/TransportLayers/LiteNetLibTransport.cs @@ -71,7 +71,7 @@ public ITransportPeer Connect(string address, int port, NetDataWriter data) var netPeer = netManager.Connect(address, port, data); var peer = new LiteNetLibPeer(netPeer); - Multiplayer.LogDebug(() => $"LiteNetLibTransport.Connect length: {data.Length}. packet: {BitConverter.ToString(data.Data)}"); + //Multiplayer.LogDebug(() => $"LiteNetLibTransport.Connect length: {data.Length}. packet: {BitConverter.ToString(data.Data)}"); netPeerToPeer[netPeer] = peer; return peer; @@ -88,7 +88,7 @@ public void Send(ITransportPeer peer, NetDataWriter writer, DeliveryMethod deliv void INetEventListener.OnConnectionRequest(ConnectionRequest request) { //Multiplayer.LogDebug(() => $"LiteNetLibTransport.INetEventListener.OnConnectionRequest({request.RemoteEndPoint})"); - OnConnectionRequest?.Invoke(request.Data, new LiteNetLibConnectionRequest(request)); + OnConnectionRequest?.Invoke(request.Data, new LiteNetLibConnectionRequest(request, this)); } void INetEventListener.OnPeerConnected(NetPeer netPeer) @@ -114,10 +114,13 @@ void INetEventListener.OnPeerDisconnected(NetPeer netPeer, DisconnectInfo discon void INetEventListener.OnNetworkReceive(NetPeer netPeer, NetPacketReader reader, byte channelNumber, DeliveryMethod deliveryMethod) { - Multiplayer.LogDebug(() => $"LiteNetLibTransport.OnNetworkReceive length: {reader.AvailableBytes}. packet: {BitConverter.ToString(reader.RawData)}"); + //Multiplayer.LogDebug(() => $"LiteNetLibTransport.OnNetworkReceive({netPeer?.Id})"); if (netPeerToPeer.TryGetValue(netPeer, out var peer)) + { + //Multiplayer.LogDebug(() => $"LiteNetLibTransport.OnNetworkReceive({netPeer?.Id}) peer: {peer != null}"); OnNetworkReceive?.Invoke(peer, reader, channelNumber, deliveryMethod); + } } void INetEventListener.OnNetworkError(IPEndPoint endPoint, SocketError socketError) @@ -162,23 +165,31 @@ private void CleanupPeerDictionaries() netPeerToPeer.Remove(pair.Key); } } - + public void RegisterPeer(NetPeer netPeer, LiteNetLibPeer peer) + { + netPeerToPeer[netPeer] = peer; + } } public class LiteNetLibConnectionRequest : IConnectionRequest { private readonly ConnectionRequest request; + private readonly LiteNetLibTransport transport; - public LiteNetLibConnectionRequest(ConnectionRequest request) + public LiteNetLibConnectionRequest(ConnectionRequest request, LiteNetLibTransport transport) { this.request = request; + this.transport = transport; } public ITransportPeer Accept() { - var peer = request.Accept(); - return new LiteNetLibPeer(peer); + var netPeer = request.Accept(); + var peer = new LiteNetLibPeer(netPeer); + transport.RegisterPeer(netPeer, peer); + + return peer; } public void Reject(NetDataWriter data = null) diff --git a/Multiplayer/Networking/TransportLayers/SteamworksTransport.cs b/Multiplayer/Networking/TransportLayers/SteamworksTransport.cs index cbc84255..15fd81e5 100644 --- a/Multiplayer/Networking/TransportLayers/SteamworksTransport.cs +++ b/Multiplayer/Networking/TransportLayers/SteamworksTransport.cs @@ -14,7 +14,7 @@ namespace Multiplayer.Networking.TransportLayers; public class SteamWorksTransport : ITransport { public NetStatistics Statistics => new NetStatistics(); - public bool IsRunning => server != null; + public bool IsRunning { get; private set; } public event Action OnConnectionRequest; public event Action OnPeerConnected; @@ -23,8 +23,8 @@ public class SteamWorksTransport : ITransport public event Action OnNetworkError; public event Action OnNetworkLatencyUpdate; - private SteamSocketManager server; - private SteamConnectionManager client; + private readonly List servers = []; + private SteamClientManager client; private readonly Dictionary peerIdToPeer = []; @@ -47,10 +47,27 @@ public bool Start() public bool Start(int port) { Multiplayer.LogDebug(() => $"SteamWorksTransport.Start({port})"); - server = SteamNetworkingSockets.CreateNormalSocket(NetAddress.AnyIp((ushort)port)); - server.transport = this; - return server != null; + var server = SteamNetworkingSockets.CreateNormalSocket(NetAddress.AnyIp((ushort)port)); + if (server != null) + { + Multiplayer.LogDebug(() => $"SteamWorksTransport.Start({port}) Normal not null"); + server.transport = this; + servers.Add(server); + IsRunning = true; + } + + //server = SteamNetworkingSockets.CreateRelaySocket(); + + //if (server != null) + //{ + // server.transport = this; + // servers.AddItem(server); + // IsRunning = true; + // Multiplayer.Log($"SteamId: {Steamworks.Data.NetIdentity.LocalHost}"); + //} + + return IsRunning; } public bool Start(IPAddress ipv4, IPAddress ipv6, int port) @@ -62,34 +79,75 @@ public bool Start(IPAddress ipv4, IPAddress ipv6, int port) public void Stop(bool sendDisconnectPackets) { Multiplayer.LogDebug(() => $"SteamWorksTransport.Stop()"); - if (server != null) + + for (int i = servers.Count; i >= 0; --i) { - foreach (var connection in server.Connected) + if (servers[i] != null) { - connection.Close(); + foreach (var connection in servers[i].Connected) + connection.Close(); } - server = null; + + servers.RemoveAt(i); } } public void PollEvents() { SteamClient.RunCallbacks(); + client?.Receive(); - server?.Receive(); + + foreach (var server in servers) + { + server?.Receive(); + } } public ITransportPeer Connect(string address, int port, NetDataWriter data) { Multiplayer.LogDebug(() => $"SteamWorksTransport.Connect({address}, {port}, {data.Length})"); + if (port < 0) + return ConnectRelay(address, data); + else + return ConnectNative(address, port, data); + } + + public ITransportPeer ConnectNative(string address, int port, NetDataWriter data) + { + Multiplayer.LogDebug(() => $"SteamWorksTransport.ConnectNative({address}, {port}, {data.Length})"); + var add = NetAddress.From(address, (ushort)port); - - Multiplayer.LogDebug(() => $"SteamSocketManager.Connect packet: {BitConverter.ToString(data.Data)}"); + + //Multiplayer.LogDebug(() => $"SteamWorksTransport.Connect packet: {BitConverter.ToString(data.Data)}"); + + // Create connection manager for client + client = SteamNetworkingSockets.ConnectNormal(add); + client.transport = this; + client.loginPacket = data; + client.peer = CreatePeer(client.Connection); + + return client.peer; + } + + public ITransportPeer ConnectRelay(string steamID, NetDataWriter data) + { + Multiplayer.LogDebug(() => $"SteamWorksTransport.ConnectRelay({steamID})"); + + SteamId id = new(); + if (!ulong.TryParse(steamID, out id.Value)) + { + Multiplayer.LogDebug(() => $"SteamWorksTransport.ConnectRelay({steamID}) failed to parse"); + return null; + } + + + Multiplayer.LogDebug(() => $"SteamWorksTransport.ConnectRelay packet: {BitConverter.ToString(data.Data)}"); // Create connection manager for client - client = SteamNetworkingSockets.ConnectNormal(add); + client = SteamNetworkingSockets.ConnectRelay(id); client.transport = this; client.loginPacket = data; client.peer = CreatePeer(client.Connection); @@ -112,36 +170,37 @@ public void UpdateSettings(Settings settings) #endregion #region SteamManagers - public class SteamSocketManager : SocketManager + public class SteamServerManager : SocketManager { public SteamWorksTransport transport; public override void OnConnecting(Connection connection, ConnectionInfo info) { - Multiplayer.LogDebug(() => $"SteamSocketManager.OnConnecting({connection}, {info})"); + Multiplayer.LogDebug(() => $"SteamServerManager.OnConnecting({connection}, {info})"); connection.Accept(); } public override void OnConnected(Connection connection, ConnectionInfo info) { - Multiplayer.LogDebug(() => $"SteamSocketManager.OnConnected({connection}, {info})"); + Multiplayer.LogDebug(() => $"SteamServerManager.OnConnected({connection}, {info})"); base.OnConnected(connection, info); var peer = transport.CreatePeer(connection); peer.connectionRequest = new SteamConnectionRequest(connection, info, peer); + transport?.OnPeerConnected?.Invoke(peer); } public override void OnDisconnected(Steamworks.Data.Connection connection, Steamworks.Data.ConnectionInfo info) { - Multiplayer.LogDebug(() => $"SteamSocketManager.OnDisconnected({connection}, {info})"); + Multiplayer.LogDebug(() => $"SteamServerManager.OnDisconnected({connection}, {info})"); base.OnDisconnected(connection, info); throw new NotImplementedException(); } public override void OnMessage(Steamworks.Data.Connection connection, Steamworks.Data.NetIdentity identity, IntPtr data, int size, long messageNum, long recvTime, int channel) { - Multiplayer.LogDebug(() => $"SteamSocketManager.OnMessage({connection}, {identity}, , {size}, {messageNum}, {recvTime}, {channel})"); + Multiplayer.LogDebug(() => $"SteamServerManager.OnMessage({connection}, {identity}, , {size}, {messageNum}, {recvTime}, {channel})"); var peer = transport.GetPeer(connection); @@ -149,10 +208,10 @@ public override void OnMessage(Steamworks.Data.Connection connection, Steamworks Marshal.Copy(data, buffer, 0, size); - Multiplayer.LogDebug(() => $"SteamSocketManager.Received packet: {BitConverter.ToString(buffer)}"); + //Multiplayer.LogDebug(() => $"SteamServerManager.Received packet: {BitConverter.ToString(buffer)}"); - var reader = new NetDataReader(buffer,0,size); - if(peer.connectionRequest != null) + var reader = new NetDataReader(buffer, 0, size); + if (peer.connectionRequest != null) { transport?.OnConnectionRequest?.Invoke(reader, peer.connectionRequest); peer.connectionRequest = null; @@ -160,16 +219,22 @@ public override void OnMessage(Steamworks.Data.Connection connection, Steamworks } transport?.OnNetworkReceive?.Invoke(peer, reader, (byte)channel, DeliveryMethod.ReliableOrdered); + + //base.OnMessage(connection,identity,data,size,messageNum,recvTime,channel); } public override void OnConnectionChanged(Steamworks.Data.Connection connection, Steamworks.Data.ConnectionInfo info) { - Multiplayer.LogDebug(() => $"SteamSocketManager.OnConnectionChanged({connection}, {info})"); + Multiplayer.LogDebug(() => $"SteamServerManager.OnConnectionChanged({connection}, {info})"); base.OnConnectionChanged(connection, info); + if (transport.GetPeer(connection) is SteamPeer peer) + { + peer.OnConnectionStatusChanged(info.State); + } } } - public class SteamConnectionManager : ConnectionManager + public class SteamClientManager : ConnectionManager { public SteamWorksTransport transport; public NetDataWriter loginPacket; @@ -177,29 +242,34 @@ public class SteamConnectionManager : ConnectionManager public override void OnConnected(ConnectionInfo info) { - Multiplayer.LogDebug(() => $"SteamConnectionManager.OnConnected({info})"); + Multiplayer.LogDebug(() => $"SteamClientManager.OnConnected({info})"); + base.OnConnected(info); peer.Send(loginPacket, DeliveryMethod.ReliableUnordered); + transport?.OnPeerConnected?.Invoke(peer); } public override void OnConnecting(ConnectionInfo info) { - Multiplayer.LogDebug(() => $"SteamConnectionManager.OnConnecting({info})"); + Multiplayer.LogDebug(() => $"SteamClientManager.OnConnecting({info})"); + base.OnConnecting(info); } public override void OnDisconnected(ConnectionInfo info) { - Multiplayer.LogDebug(() => $"SteamConnectionManager.ConnectionOnDisconnected({info})"); + Multiplayer.LogDebug(() => $"SteamClientManager.ConnectionOnDisconnected({info})"); + //base.OnDisconnected(info); } public override void OnMessage(IntPtr data, int size, long messageNum, long recvTime, int channel) { - Multiplayer.LogDebug(() => $"SteamConnectionManager.Connection(,{size}, {messageNum}, {recvTime}, {channel})"); + Multiplayer.LogDebug(() => $"SteamClientManager.Connection(,{size}, {messageNum}, {recvTime}, {channel})"); byte[] buffer = new byte[size]; Marshal.Copy(data, buffer, 0, size); var reader = new NetDataReader(buffer, 0, size); transport?.OnNetworkReceive(peer, reader, (byte)channel, DeliveryMethod.ReliableOrdered); + //base.OnMessage(data, size, messageNum, recvTime, channel); } } #endregion @@ -262,7 +332,7 @@ public SteamPeer(int id, Connection connection) public void Send(NetDataWriter writer, DeliveryMethod deliveryMethod) { - + Multiplayer.LogDebug(() => $"SteamPeer.Send({writer.Data.Length})\r\n{Environment.StackTrace}"); // Map LiteNetLib delivery method to Steam's SendType SendType sendType = deliveryMethod switch { From 5c3fc6d18072aed6242b7ef7a686732d00de9e98 Mon Sep 17 00:00:00 2001 From: AMacro Date: Wed, 22 Jan 2025 21:28:32 +1000 Subject: [PATCH 203/521] Remove unused unnecessary logging --- Multiplayer/Components/Networking/UI/ChatGUI.cs | 8 ++++---- Multiplayer/Networking/Managers/Server/ChatManager.cs | 2 -- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/Multiplayer/Components/Networking/UI/ChatGUI.cs b/Multiplayer/Components/Networking/UI/ChatGUI.cs index 27b1db53..3522a6ac 100644 --- a/Multiplayer/Components/Networking/UI/ChatGUI.cs +++ b/Multiplayer/Components/Networking/UI/ChatGUI.cs @@ -249,7 +249,7 @@ public void Submit(string text) private void ChatInputChange(string message) { - Multiplayer.Log($"ChatInputChange({message})"); + //Multiplayer.LogDebug(() => $"ChatInputChange({message})"); //allow the user to clear text if(Input.GetKeyDown(KeyCode.Backspace) || Input.GetKeyDown(KeyCode.Delete)) @@ -257,8 +257,8 @@ private void ChatInputChange(string message) if (CheckForWhisper(message, out string localMessage, out string recipient)) { - Multiplayer.Log($"ChatInputChange: message: \"{message}\", localMessage: \"{(localMessage == null ? "null" : localMessage)}" + - $"\", recipient: \"{(recipient == null ? "null" : recipient)}\""); + //Multiplayer.LogDebug(()=>$"ChatInputChange: message: \"{message}\", localMessage: \"{(localMessage == null ? "null" : localMessage)}" + + // $"\", recipient: \"{(recipient == null ? "null" : recipient)}\""); if (localMessage == null || localMessage == string.Empty) { @@ -315,7 +315,7 @@ private bool CheckForWhisper(string message, out string localMessage, out string if (message.StartsWith("/") && message.Length > (ChatManager.COMMAND_WHISPER_SHORT.Length + 2)) { - Multiplayer.Log("CheckForWhisper() starts with /"); + //Multiplayer.LogDebug(()=>"CheckForWhisper() starts with /"); string command = message.Substring(1).Split(' ')[0]; switch (command) { diff --git a/Multiplayer/Networking/Managers/Server/ChatManager.cs b/Multiplayer/Networking/Managers/Server/ChatManager.cs index c480eaad..27769f96 100644 --- a/Multiplayer/Networking/Managers/Server/ChatManager.cs +++ b/Multiplayer/Networking/Managers/Server/ChatManager.cs @@ -1,9 +1,7 @@ -using LiteNetLib; using Multiplayer.Components.Networking; using System.Linq; using Multiplayer.Networking.Data; using System.Text.RegularExpressions; -using UnityEngine; using Multiplayer.Networking.TransportLayers; namespace Multiplayer.Networking.Managers.Server; From 3f7d75c776067f4b3bbc5947e873bd42255965b3 Mon Sep 17 00:00:00 2001 From: AMacro Date: Wed, 22 Jan 2025 21:30:54 +1000 Subject: [PATCH 204/521] Cleanup --- .../Components/Networking/NetworkLifecycle.cs | 12 ------------ .../Managers/Client/ClientPlayerManager.cs | 2 ++ 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/Multiplayer/Components/Networking/NetworkLifecycle.cs b/Multiplayer/Components/Networking/NetworkLifecycle.cs index 0c605f69..d3917a9b 100644 --- a/Multiplayer/Components/Networking/NetworkLifecycle.cs +++ b/Multiplayer/Components/Networking/NetworkLifecycle.cs @@ -87,18 +87,6 @@ protected override void Awake() StartCoroutine(PollEvents()); } - //private static void RegisterPackets() - //{ - // IReadOnlyDictionary packetMappings = NetPacketProcessor.RegisterPacketTypes(); - // Multiplayer.LogDebug(() => - // { - // StringBuilder stringBuilder = new($"Registered {packetMappings.Count} packets. Mappings:\n"); - // foreach (KeyValuePair kvp in packetMappings) - // stringBuilder.AppendLine($"{kvp.Value}: {kvp.Key}"); - // return stringBuilder; - // }); - //} - private void OnSettingsUpdated(Settings settings) { if (!IsClientRunning && !IsServerRunning) diff --git a/Multiplayer/Networking/Managers/Client/ClientPlayerManager.cs b/Multiplayer/Networking/Managers/Client/ClientPlayerManager.cs index eab4bd29..bcd74451 100644 --- a/Multiplayer/Networking/Managers/Client/ClientPlayerManager.cs +++ b/Multiplayer/Networking/Managers/Client/ClientPlayerManager.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using DV; +using Multiplayer.Components.Networking; using Multiplayer.Components.Networking.Player; using UnityEngine; using Object = UnityEngine.Object; @@ -43,6 +44,7 @@ public void RemovePlayer(byte id) { if (!playerMap.TryGetValue(id, out NetworkedPlayer networkedPlayer)) return; + OnPlayerDisconnected?.Invoke(id, networkedPlayer); Object.Destroy(networkedPlayer.gameObject); playerMap.Remove(id); From 49c90358f165a60b7c6bbb3688f70f73d1d09b9d Mon Sep 17 00:00:00 2001 From: AMacro Date: Wed, 22 Jan 2025 23:19:39 +1000 Subject: [PATCH 205/521] Rework player disconnection flow --- .../Components/MainMenu/ServerBrowserPane.cs | 89 ++++++-------- .../Managers/Client/NetworkClient.cs | 65 ++++------ .../Networking/Managers/NetworkManager.cs | 12 +- .../Managers/Server/NetworkServer.cs | 27 +++-- .../ClientboundDisconnectPacket.cs | 6 + .../ClientboundPlayerKickPacket.cs | 4 - .../Networking/TransportLayers/ITransport.cs | 2 +- .../TransportLayers/LiteNetLibTransport.cs | 5 +- .../TransportLayers/SteamworksTransport.cs | 113 +++++++++++++----- 9 files changed, 180 insertions(+), 143 deletions(-) create mode 100644 Multiplayer/Networking/Packets/Clientbound/ClientboundDisconnectPacket.cs delete mode 100644 Multiplayer/Networking/Packets/Clientbound/ClientboundPlayerKickPacket.cs diff --git a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs index 074a0d9a..c9e99326 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs @@ -859,35 +859,33 @@ private void AttemptFail() { connectionState = ConnectionState.Failed; - connectingPopup?.RequestClose(PopupClosedByAction.Abortion, null); + if (connectingPopup != null) + { + connectingPopup.RequestClose(PopupClosedByAction.Abortion, null); + connectingPopup = null; // Clear the reference + } - if(this.gridView != null) - IndexChanged(this.gridView); + if (gameObject != null && gameObject.activeInHierarchy) + { + if (gridView != null) + IndexChanged(gridView); - buttonDirectIP?.ToggleInteractable(true); + if (buttonDirectIP != null && buttonDirectIP.gameObject != null) + buttonDirectIP.ToggleInteractable(true); + } } private void OnDisconnect(DisconnectReason reason, string message) { - Multiplayer.LogError($"Connection failed! {reason}, \"{message}\""); + Multiplayer.Log($"Disconnected due to: {reason}, \"{message}\""); - switch (reason) - { - case DisconnectReason.UnknownHost: - if (message == null || message.Length == 0) - { - message = "Unknown Host"; //TODO: add translations - } - break; - case DisconnectReason.DisconnectPeerCalled: - if (message == null || message.Length == 0) - { - message = "Player Kicked"; //TODO: add translations - } - break; - case DisconnectReason.ConnectionFailed: + string displayMessage = message; - //Check our connectionState + if (string.IsNullOrEmpty(message)) + { + //fallback for no message (server initiated disconnects should have a message) + if (reason == DisconnectReason.ConnectionFailed) + { switch (connectionState) { case ConnectionState.AttemptingIPv6: @@ -911,44 +909,25 @@ private void OnDisconnect(DisconnectReason reason, string message) message = "Host Unreachable"; //TODO: add translations break; } - break; - - case DisconnectReason.ConnectionRejected: - if (message == null || message.Length == 0) - { - message = "Rejected!"; //TODO: add translations - } - break; - case DisconnectReason.RemoteConnectionClose: - if (message == null || message.Length == 0) - { - 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; + displayMessage = GetDisplayMessageForDisconnect(reason); + AttemptFail(); + } + else + { + connectionState = ConnectionState.NotConnected; } - //Multiplayer.LogError($"OnDisconnect() Calling AF"); - AttemptFail(); - - //Multiplayer.LogError($"OnDisconnect() Queuing"); NetworkLifecycle.Instance.QueueMainMenuEvent(() => { - - Multiplayer.LogError($"OnDisconnect() Adding PU"); - MainMenuThingsAndStuff.Instance.ShowOkPopup(message, ()=>{ }); - - //Multiplayer.LogError($"OnDisconnect() Done!"); + Multiplayer.LogDebug(() => "OnDisconnect() Queuing"); + MainMenuThingsAndStuff.Instance?.ShowOkPopup(displayMessage, () => { }); }); } IEnumerator GetRequest(string uri) + private string GetDisplayMessageForDisconnect(DisconnectReason reason) { using UnityWebRequest webRequest = UnityWebRequest.Get(uri); // Request and wait for the desired page. @@ -958,12 +937,22 @@ IEnumerator GetRequest(string uri) int page = pages.Length - 1; if (webRequest.isNetworkError) + return reason switch { Multiplayer.LogError(pages[page] + ": Error: " + webRequest.error); } else { Multiplayer.Log(pages[page] + ":\nReceived: " + webRequest.downloadHandler.text); + DisconnectReason.UnknownHost => "Unknown Host", + DisconnectReason.DisconnectPeerCalled => "Player Kicked", + DisconnectReason.ConnectionFailed => "Host Unreachable", + DisconnectReason.ConnectionRejected => "Rejected!", + DisconnectReason.RemoteConnectionClose => "Server Shutting Down", + DisconnectReason.Timeout => "Server Timed Out", + _ => "Connection Failed" + }; + } LobbyServerData[] response; diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index e9d31bfa..b9d21053 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -51,6 +51,7 @@ public class NetworkClient : NetworkManager protected override string LogPrefix => "[Client]"; private Action onDisconnect; + private string disconnectMessage; public ITransportPeer SelfPeer { get; private set; } public readonly ClientPlayerManager ClientPlayerManager; @@ -111,11 +112,14 @@ public override void Stop() protected override void Subscribe() { netPacketProcessor.SubscribeReusable(OnClientboundLoginResponsePacket); + netPacketProcessor.SubscribeReusable(OnClientboundDisconnectPacket); + netPacketProcessor.SubscribeReusable(OnClientboundPlayerJoinedPacket); netPacketProcessor.SubscribeReusable(OnClientboundPlayerDisconnectPacket); - netPacketProcessor.SubscribeReusable(OnClientboundPlayerKickPacket); + netPacketProcessor.SubscribeReusable(OnClientboundPlayerPositionPacket); netPacketProcessor.SubscribeReusable(OnClientboundPingUpdatePacket); + netPacketProcessor.SubscribeReusable(OnClientboundTickSyncPacket); netPacketProcessor.SubscribeReusable(OnClientboundServerLoadingPacket); netPacketProcessor.SubscribeReusable(OnClientboundBeginWorldSyncPacket); @@ -170,8 +174,11 @@ public override void OnPeerConnected(ITransportPeer peer) serverPeer = peer; } - public override void OnPeerDisconnected(ITransportPeer peer, DisconnectInfo disconnectInfo) + public override void OnPeerDisconnected(ITransportPeer peer, DisconnectReason disconnectReason) { + + LogDebug(()=>$"OnPeerDisconnected({peer.Id}, {disconnectReason}) disconnect message: {disconnectMessage}"); + NetworkLifecycle.Instance.Stop(); TrainStress.globalIgnoreStressCalculation = false; @@ -186,37 +193,7 @@ public override void OnPeerDisconnected(ITransportPeer peer, DisconnectInfo disc MainMenu.GoBackToMainMenu(); } - - 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: - case DisconnectReason.ConnectionRejected: - netPacketProcessor.ReadAllPackets(disconnectInfo.AdditionalData); - return; - case DisconnectReason.RemoteConnectionClose: - netPacketProcessor.ReadAllPackets(disconnectInfo.AdditionalData); - return; - }*/ - - /* - NetworkLifecycle.Instance.QueueMainMenuEvent(() => - { - Popup popup = MainMenuThingsAndStuff.Instance.ShowOkPopup(); - if (popup == null) - return; - popup.labelTMPro.text = text; - });*/ + onDisconnect(disconnectReason, disconnectMessage); } public override void OnNetworkLatencyUpdate(ITransportPeer peer, int latency) @@ -226,7 +203,8 @@ public override void OnNetworkLatencyUpdate(ITransportPeer peer, int latency) public override void OnConnectionRequest(NetDataReader dataReader, IConnectionRequest request) { - // todo + // Clients don't receive incomming requests. + request.Reject(); } #endregion @@ -278,17 +256,26 @@ private void OnClientboundPlayerJoinedPacket(ClientboundPlayerJoinedPacket packe ClientPlayerManager.UpdatePosition(packet.Id, packet.Position, Vector3.zero, packet.Rotation, false, packet.CarID != 0, packet.CarID); } + //For other player left the game private void OnClientboundPlayerDisconnectPacket(ClientboundPlayerDisconnectPacket packet) { - Log($"Received player disconnect packet (Id: {packet.Id})"); + Log($"Received player disconnect packet for player id: {packet.Id}"); 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); + //For server shutting down / player kicked + private void OnClientboundDisconnectPacket(ClientboundDisconnectPacket packet) + { + if (packet.Kicked) + { + Log($"Player was kicked!"); + disconnectMessage = "You were kicked!"; + } + else + { + disconnectMessage = "Server Shutting Down"; + } } private void OnClientboundPlayerPositionPacket(ClientboundPlayerPositionPacket packet) { diff --git a/Multiplayer/Networking/Managers/NetworkManager.cs b/Multiplayer/Networking/Managers/NetworkManager.cs index f214934e..970384e3 100644 --- a/Multiplayer/Networking/Managers/NetworkManager.cs +++ b/Multiplayer/Networking/Managers/NetworkManager.cs @@ -96,6 +96,14 @@ protected virtual ITransportPeer Connect(string address, int port, NetDataWriter public virtual void Stop() { transport.Stop(true); + + transport.OnConnectionRequest -= OnConnectionRequest; + transport.OnPeerConnected -= OnPeerConnected; + transport.OnPeerDisconnected -= OnPeerDisconnected; + transport.OnNetworkReceive -= OnNetworkReceive; + transport.OnNetworkError -= OnNetworkError; + transport.OnNetworkLatencyUpdate -= OnNetworkLatencyUpdate; + Settings.OnSettingsUpdated -= OnSettingsUpdated; } @@ -141,7 +149,7 @@ public void OnNetworkReceive(ITransportPeer peer, NetDataReader reader, byte cha } catch (ParseException e) { - Multiplayer.LogWarning($"Failed to parse packet: {e.Message}"); + Multiplayer.LogWarning($"[{GetType()}] Failed to parse packet: {e.Message}\r\n{e.StackTrace}"); } finally { @@ -174,7 +182,7 @@ public void OnNetworkReceiveUnconnected(IPEndPoint remoteEndPoint, NetPacketRead //Standard networking callbacks public abstract void OnPeerConnected(ITransportPeer peer); - public abstract void OnPeerDisconnected(ITransportPeer peer, DisconnectInfo disconnectInfo); + public abstract void OnPeerDisconnected(ITransportPeer peer, DisconnectReason disconnectInfo); public abstract void OnConnectionRequest(NetDataReader requestData, IConnectionRequest request); public abstract void OnNetworkLatencyUpdate(ITransportPeer peer, int latency); diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index c179ab7b..93f855ee 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -106,6 +106,14 @@ public override void Stop() UnityEngine.Object.Destroy(lobbyServerManager); } + //Alert all clients (except h + var packet = WritePacket(new ClientboundDisconnectPacket()); + foreach (var peer in Peers.Values) + { + if (peer != SelfPeer) + peer?.Disconnect(packet); + } + base.Stop(); } @@ -196,20 +204,21 @@ public override void OnPeerConnected(ITransportPeer peer) { } - public override void OnPeerDisconnected(ITransportPeer peer, DisconnectInfo disconnectInfo) + public override void OnPeerDisconnected(ITransportPeer peer, DisconnectReason disconnectReason) { byte id = (byte)peer.Id; - Log($"Player {(serverPlayers.TryGetValue(id, out ServerPlayer player) ? player : id)} disconnected: {disconnectInfo.Reason}"); + Log($"Player {(serverPlayers.TryGetValue(id, out ServerPlayer player) ? player : id)} disconnected: {disconnectReason}"); if (WorldStreamingInit.isLoaded) SaveGameManager.Instance.UpdateInternalData(); serverPlayers.Remove(id); Peers.Remove(id); - SendPacketToAll(WritePacket(new ClientboundPlayerDisconnectPacket + + SendPacketToAll(new ClientboundPlayerDisconnectPacket { Id = id - }), DeliveryMethod.ReliableUnordered); + }, DeliveryMethod.ReliableUnordered); PlayerDisconnect?.Invoke(id); } @@ -244,7 +253,7 @@ public override void OnConnectionRequest(NetDataReader requestData, IConnectionR { NetDataWriter writer = WritePacket(packet); foreach (KeyValuePair kvp in Peers) - kvp.Value.Send(writer, deliveryMethod); + kvp.Value?.Send(writer, deliveryMethod); } private void SendPacketToAll(T packet, DeliveryMethod deliveryMethod, ITransportPeer excludePeer) where T : class, new() @@ -277,7 +286,8 @@ public override void OnConnectionRequest(NetDataReader requestData, IConnectionR public void KickPlayer(ITransportPeer peer) { - peer.Disconnect(WritePacket(new ClientboundPlayerKickPacket())); + //peer.Send(WritePacket(new ClientboundDisconnectPacket()),DeliveryMethod.ReliableUnordered); + peer.Disconnect(WritePacket(new ClientboundDisconnectPacket { Kicked = true })); } public void SendGameParams(GameParams gameParams) { @@ -637,11 +647,6 @@ private void OnServerboundSaveGameDataRequestPacket(ServerboundSaveGameDataReque private void OnServerboundClientReadyPacket(ServerboundClientReadyPacket packet, ITransportPeer peer) { - if(peer == null) - { - LogError($"OnServerboundClientReadyPacket() peer is null!"); - return; - } byte peerId = (byte)peer.Id; diff --git a/Multiplayer/Networking/Packets/Clientbound/ClientboundDisconnectPacket.cs b/Multiplayer/Networking/Packets/Clientbound/ClientboundDisconnectPacket.cs new file mode 100644 index 00000000..b39519d5 --- /dev/null +++ b/Multiplayer/Networking/Packets/Clientbound/ClientboundDisconnectPacket.cs @@ -0,0 +1,6 @@ +namespace Multiplayer.Networking.Packets.Clientbound; + +public class ClientboundDisconnectPacket +{ + public bool Kicked { get; set; } +} diff --git a/Multiplayer/Networking/Packets/Clientbound/ClientboundPlayerKickPacket.cs b/Multiplayer/Networking/Packets/Clientbound/ClientboundPlayerKickPacket.cs deleted file mode 100644 index c682efa1..00000000 --- a/Multiplayer/Networking/Packets/Clientbound/ClientboundPlayerKickPacket.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace Multiplayer.Networking.Packets.Clientbound; - -public class ClientboundPlayerKickPacket -{} diff --git a/Multiplayer/Networking/TransportLayers/ITransport.cs b/Multiplayer/Networking/TransportLayers/ITransport.cs index 97b676a1..39c4ec12 100644 --- a/Multiplayer/Networking/TransportLayers/ITransport.cs +++ b/Multiplayer/Networking/TransportLayers/ITransport.cs @@ -25,7 +25,7 @@ public interface ITransport // Events event Action OnConnectionRequest; event Action OnPeerConnected; - event Action OnPeerDisconnected; + event Action OnPeerDisconnected; event Action OnNetworkReceive; event Action OnNetworkError; event Action OnNetworkLatencyUpdate; diff --git a/Multiplayer/Networking/TransportLayers/LiteNetLibTransport.cs b/Multiplayer/Networking/TransportLayers/LiteNetLibTransport.cs index b1860ca2..ab8209c3 100644 --- a/Multiplayer/Networking/TransportLayers/LiteNetLibTransport.cs +++ b/Multiplayer/Networking/TransportLayers/LiteNetLibTransport.cs @@ -5,7 +5,6 @@ using System.Linq; using System.Net; using System.Net.Sockets; -using static DV.UI.ATutorialsMenuProvider; namespace Multiplayer.Networking.TransportLayers; @@ -16,7 +15,7 @@ public class LiteNetLibTransport : ITransport, INetEventListener public event Action OnConnectionRequest; public event Action OnPeerConnected; - public event Action OnPeerDisconnected; + public event Action OnPeerDisconnected; public event Action OnNetworkReceive; public event Action OnNetworkError; public event Action OnNetworkLatencyUpdate; @@ -105,7 +104,7 @@ void INetEventListener.OnPeerDisconnected(NetPeer netPeer, DisconnectInfo discon if(!netPeerToPeer.TryGetValue(netPeer, out var peer)) return; - OnPeerDisconnected?.Invoke(peer, disconnectInfo); + OnPeerDisconnected?.Invoke(peer, disconnectInfo.Reason); netPeerToPeer.Remove(netPeer); CleanupPeerDictionaries(); diff --git a/Multiplayer/Networking/TransportLayers/SteamworksTransport.cs b/Multiplayer/Networking/TransportLayers/SteamworksTransport.cs index 15fd81e5..fa44eabf 100644 --- a/Multiplayer/Networking/TransportLayers/SteamworksTransport.cs +++ b/Multiplayer/Networking/TransportLayers/SteamworksTransport.cs @@ -18,7 +18,7 @@ public class SteamWorksTransport : ITransport public event Action OnConnectionRequest; public event Action OnPeerConnected; - public event Action OnPeerDisconnected; + public event Action OnPeerDisconnected; public event Action OnNetworkReceive; public event Action OnNetworkError; public event Action OnNetworkLatencyUpdate; @@ -57,15 +57,15 @@ public bool Start(int port) IsRunning = true; } - //server = SteamNetworkingSockets.CreateRelaySocket(); + server = SteamNetworkingSockets.CreateRelaySocket(); - //if (server != null) - //{ - // server.transport = this; - // servers.AddItem(server); - // IsRunning = true; - // Multiplayer.Log($"SteamId: {Steamworks.Data.NetIdentity.LocalHost}"); - //} + if (server != null) + { + server.transport = this; + servers.Add(server); + IsRunning = true; + Multiplayer.Log($"SteamId: {Steamworks.Data.NetIdentity.LocalHost}"); + } return IsRunning; } @@ -80,15 +80,18 @@ public void Stop(bool sendDisconnectPackets) { Multiplayer.LogDebug(() => $"SteamWorksTransport.Stop()"); - for (int i = servers.Count; i >= 0; --i) + client?.Close(true); + + + while (servers.Count > 0) { - if (servers[i] != null) + if (servers[0] != null) { - foreach (var connection in servers[i].Connected) - connection.Close(); + foreach (var connection in servers[0].Connected) + connection.Close(true, (int)NetConnectionEnd.App_Generic); } - servers.RemoveAt(i); + servers.RemoveAt(0); } } @@ -97,16 +100,27 @@ public void PollEvents() SteamClient.RunCallbacks(); client?.Receive(); + foreach (var server in servers) { server?.Receive(); } + + //update pings + foreach (var kvp in connectionToPeer) + { + var peer = kvp.Value; + var connection = kvp.Key; + + if(peer != null && connection != null) + OnNetworkLatencyUpdate?.Invoke(peer, connection.QuickStatus().Ping / 2); //nromalise to match LiteNetLib's implementation + } } public ITransportPeer Connect(string address, int port, NetDataWriter data) { - Multiplayer.LogDebug(() => $"SteamWorksTransport.Connect({address}, {port}, {data.Length})"); + //Multiplayer.LogDebug(() => $"SteamWorksTransport.Connect({address}, {port}, {data.Length})"); if (port < 0) return ConnectRelay(address, data); @@ -144,7 +158,7 @@ public ITransportPeer ConnectRelay(string steamID, NetDataWriter data) } - Multiplayer.LogDebug(() => $"SteamWorksTransport.ConnectRelay packet: {BitConverter.ToString(data.Data)}"); + //Multiplayer.LogDebug(() => $"SteamWorksTransport.ConnectRelay packet: {BitConverter.ToString(data.Data)}"); // Create connection manager for client client = SteamNetworkingSockets.ConnectRelay(id); @@ -158,7 +172,7 @@ public ITransportPeer ConnectRelay(string steamID, NetDataWriter data) public void Send(ITransportPeer peer, NetDataWriter writer, DeliveryMethod deliveryMethod) { - Multiplayer.LogDebug(() => $"SteamWorksTransport.Send({peer.Id}, {deliveryMethod})"); + //Multiplayer.LogDebug(() => $"SteamWorksTransport.Send({peer.Id}, {deliveryMethod})"); peer.Send(writer, deliveryMethod); } @@ -177,13 +191,13 @@ public class SteamServerManager : SocketManager public override void OnConnecting(Connection connection, ConnectionInfo info) { - Multiplayer.LogDebug(() => $"SteamServerManager.OnConnecting({connection}, {info})"); + //Multiplayer.LogDebug(() => $"SteamServerManager.OnConnecting({connection}, {info})"); connection.Accept(); } public override void OnConnected(Connection connection, ConnectionInfo info) { - Multiplayer.LogDebug(() => $"SteamServerManager.OnConnected({connection}, {info})"); + //Multiplayer.LogDebug(() => $"SteamServerManager.OnConnected({connection}, {info})"); base.OnConnected(connection, info); var peer = transport.CreatePeer(connection); @@ -193,14 +207,16 @@ public override void OnConnected(Connection connection, ConnectionInfo info) public override void OnDisconnected(Steamworks.Data.Connection connection, Steamworks.Data.ConnectionInfo info) { - Multiplayer.LogDebug(() => $"SteamServerManager.OnDisconnected({connection}, {info})"); + //Multiplayer.LogDebug(() => $"SteamServerManager.OnDisconnected({connection}, {info})"); base.OnDisconnected(connection, info); - throw new NotImplementedException(); + var peer = transport.GetPeer(connection); + + transport?.OnPeerDisconnected?.Invoke(peer, NetConnectionEndToDisconnectReason(info.EndReason)); } public override void OnMessage(Steamworks.Data.Connection connection, Steamworks.Data.NetIdentity identity, IntPtr data, int size, long messageNum, long recvTime, int channel) { - Multiplayer.LogDebug(() => $"SteamServerManager.OnMessage({connection}, {identity}, , {size}, {messageNum}, {recvTime}, {channel})"); + //Multiplayer.LogDebug(() => $"SteamServerManager.OnMessage({connection}, {identity}, , {size}, {messageNum}, {recvTime}, {channel})"); var peer = transport.GetPeer(connection); @@ -225,7 +241,7 @@ public override void OnMessage(Steamworks.Data.Connection connection, Steamworks public override void OnConnectionChanged(Steamworks.Data.Connection connection, Steamworks.Data.ConnectionInfo info) { - Multiplayer.LogDebug(() => $"SteamServerManager.OnConnectionChanged({connection}, {info})"); + //Multiplayer.LogDebug(() => $"SteamServerManager.OnConnectionChanged({connection}, {info})"); base.OnConnectionChanged(connection, info); if (transport.GetPeer(connection) is SteamPeer peer) { @@ -244,37 +260,44 @@ public override void OnConnected(ConnectionInfo info) { Multiplayer.LogDebug(() => $"SteamClientManager.OnConnected({info})"); base.OnConnected(info); + transport.IsRunning = true; peer.Send(loginPacket, DeliveryMethod.ReliableUnordered); transport?.OnPeerConnected?.Invoke(peer); } public override void OnConnecting(ConnectionInfo info) { - Multiplayer.LogDebug(() => $"SteamClientManager.OnConnecting({info})"); + //Multiplayer.LogDebug(() => $"SteamClientManager.OnConnecting({info})"); base.OnConnecting(info); } public override void OnDisconnected(ConnectionInfo info) { - Multiplayer.LogDebug(() => $"SteamClientManager.ConnectionOnDisconnected({info})"); - //base.OnDisconnected(info); + Multiplayer.LogDebug(() => $"SteamClientManager.OnDisconnected({info.EndReason})"); + base.OnDisconnected(info); + transport?.OnPeerDisconnected?.Invoke(peer, NetConnectionEndToDisconnectReason(info.EndReason)); } public override void OnMessage(IntPtr data, int size, long messageNum, long recvTime, int channel) { - Multiplayer.LogDebug(() => $"SteamClientManager.Connection(,{size}, {messageNum}, {recvTime}, {channel})"); + //Multiplayer.LogDebug(() => $"SteamClientManager.Connection(,{size}, {messageNum}, {recvTime}, {channel})"); byte[] buffer = new byte[size]; Marshal.Copy(data, buffer, 0, size); var reader = new NetDataReader(buffer, 0, size); - transport?.OnNetworkReceive(peer, reader, (byte)channel, DeliveryMethod.ReliableOrdered); + transport?.OnNetworkReceive?.Invoke(peer, reader, (byte)channel, DeliveryMethod.ReliableOrdered); //base.OnMessage(data, size, messageNum, recvTime, channel); } + + public override void OnConnectionChanged(ConnectionInfo info) + { + base.OnConnectionChanged(info); + peer?.OnConnectionStatusChanged(info.State); + } } #endregion - private SteamPeer CreatePeer(Connection connection) { var peer = new SteamPeer(nextPeerId++, connection); @@ -287,6 +310,26 @@ private SteamPeer GetPeer(Connection connection) { return connectionToPeer.TryGetValue(connection, out var peer) ? peer : null; } + + public static DisconnectReason NetConnectionEndToDisconnectReason(NetConnectionEnd reason) + { + return reason switch + { + NetConnectionEnd.Remote_Timeout => DisconnectReason.Timeout, + NetConnectionEnd.Misc_Timeout => DisconnectReason.Timeout, + NetConnectionEnd.Remote_BadProtocolVersion => DisconnectReason.InvalidProtocol, + NetConnectionEnd.Remote_BadCrypt => DisconnectReason.ConnectionFailed, + NetConnectionEnd.Remote_BadCert => DisconnectReason.ConnectionRejected, + NetConnectionEnd.Local_OfflineMode => DisconnectReason.NetworkUnreachable, + NetConnectionEnd.Local_NetworkConfig => DisconnectReason.NetworkUnreachable, + NetConnectionEnd.Misc_P2P_NAT_Firewall => DisconnectReason.PeerToPeerConnection, + NetConnectionEnd.Local_P2P_ICE_NoPublicAddresses => DisconnectReason.PeerNotFound, + NetConnectionEnd.Remote_P2P_ICE_NoPublicAddresses => DisconnectReason.PeerNotFound, + NetConnectionEnd.Misc_PeerSentNoConnection => DisconnectReason.PeerNotFound, + NetConnectionEnd.App_Generic => DisconnectReason.DisconnectPeerCalled, + _ => DisconnectReason.ConnectionFailed + }; + } } public class SteamConnectionRequest : IConnectionRequest @@ -309,8 +352,9 @@ public ITransportPeer Accept() public void Reject(NetDataWriter data = null) { if (data != null) - peer.Send(data, DeliveryMethod.ReliableUnordered); - connection.Close(); + peer?.Send(data, DeliveryMethod.ReliableUnordered); + + connection.Close(true); } public IPEndPoint RemoteEndPoint => new(IPAddress.Any, 0); @@ -332,7 +376,7 @@ public SteamPeer(int id, Connection connection) public void Send(NetDataWriter writer, DeliveryMethod deliveryMethod) { - Multiplayer.LogDebug(() => $"SteamPeer.Send({writer.Data.Length})\r\n{Environment.StackTrace}"); + //Multiplayer.LogDebug(() => $"SteamPeer.Send({writer.Data.Length})\r\n{Environment.StackTrace}"); // Map LiteNetLib delivery method to Steam's SendType SendType sendType = deliveryMethod switch { @@ -349,7 +393,10 @@ public void Send(NetDataWriter writer, DeliveryMethod deliveryMethod) public void Disconnect(NetDataWriter data = null) { - connection.Close(); + if (data != null) + Send(data, DeliveryMethod.ReliableUnordered); + + connection.Close(true); } public void OnConnectionStatusChanged(Steamworks.ConnectionState state) From 7a2a561f768f4694e58b644895fd866d090a6054 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 25 Jan 2025 09:13:20 +1000 Subject: [PATCH 206/521] Fix issues with port sync Ensure TrainCar `Start()` has been called prior to requesting sync. Fixed issue where valueType STATE may not get sync'd correctly on change. Fixed issue where iterator would not increment if `TryGet()` failed, resulting in values not aligning with the ID array --- .../Networking/Train/NetworkedTrainCar.cs | 55 +++++++++++++------ .../Managers/Client/NetworkClient.cs | 3 - 2 files changed, 37 insertions(+), 21 deletions(-) diff --git a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs index 719800a7..59642886 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs @@ -8,7 +8,6 @@ using DV.Simulation.Cars; using DV.ThingTypes; using JetBrains.Annotations; -using LiteNetLib; using LocoSim.Definitions; using LocoSim.Implementations; using Multiplayer.Components.Networking.Player; @@ -177,7 +176,7 @@ public void Start() foreach (KeyValuePair kvp in simulationFlow.fullPortIdToPort) if (kvp.Value.valueType == PortValueType.CONTROL || NetworkLifecycle.Instance.IsHost()) kvp.Value.ValueUpdatedInternally += _ => { Common_OnPortUpdated(kvp.Value); }; - + dirtyFuses = new HashSet(simulationFlow.fullFuseIdToFuse.Count); foreach (KeyValuePair kvp in simulationFlow.fullFuseIdToFuse) kvp.Value.StateUpdated += _ => { Common_OnFuseUpdated(kvp.Value); }; @@ -219,6 +218,8 @@ public void Start() StartCoroutine(Server_WaitForLogicCar()); } + + NetworkLifecycle.Instance?.Client.SendTrainSyncRequest(NetId); } public void OnDisable() { @@ -332,10 +333,10 @@ public void Server_DirtyAllState() foreach (string portId in simulationFlow.fullPortIdToPort.Keys) { dirtyPorts.Add(portId); - if (simulationFlow.TryGetPort(portId, out Port port)) - { - lastSentPortValues[portId] = port.value; - } + //if (simulationFlow.TryGetPort(portId, out Port port)) + //{ + // lastSentPortValues[portId] = port.value; + //} } foreach (string fuseId in simulationFlow.fullFuseIdToFuse.Keys) @@ -644,13 +645,15 @@ private void Common_SendPorts() if(simulationFlow.TryGetPort(portId, out Port port)) { float value = port.Value; - portValues[i++] = value; + portValues[i] = value; lastSentPortValues[portId] = value; } else { - Multiplayer.LogWarning($"SendPorts() [{CurrentID}, {NetId}] Failed to find port \"{portId}\""); + Multiplayer.LogWarning($"Failed to send port \"{portId}\" for [{CurrentID}, {NetId}]"); } + + i++; } dirtyPorts.Clear(); @@ -666,12 +669,17 @@ private void Common_SendFuses() int i = 0; string[] fuseIds = dirtyFuses.ToArray(); bool[] fuseValues = new bool[fuseIds.Length]; + foreach (string fuseId in dirtyFuses) + { if(simulationFlow.TryGetFuse(fuseId, out Fuse fuse)) - fuseValues[i++] = fuse.State; + fuseValues[i] = fuse.State; else Multiplayer.LogWarning($"SendFuses() [{CurrentID}, {NetId}] Failed to find fuse \"{fuseId}\""); + i++; + } + dirtyFuses.Clear(); NetworkLifecycle.Instance.Client.SendFuses(NetId, fuseIds, fuseValues); @@ -711,10 +719,24 @@ private void Common_OnPortUpdated(Port port) return; if (float.IsNaN(port.prevValue) && float.IsNaN(port.Value)) return; - if (!lastSentPortValues.TryGetValue(port.id, out float lastSentvalue) || - Mathf.Abs(lastSentvalue - port.Value) > MAX_PORT_DELTA || - (port.Value == 0 && lastSentvalue != 0)) - dirtyPorts.Add(port.id); + + bool hasLastSent = lastSentPortValues.TryGetValue(port.id, out float lastSentValue); + float delta = Mathf.Abs(lastSentValue - port.Value); + + if (port.valueType == PortValueType.STATE) + { + if (!hasLastSent || lastSentValue != port.Value) + { + dirtyPorts.Add(port.id); + } + } + else + { + if (!hasLastSent || delta > MAX_PORT_DELTA || (port.Value == 0 && lastSentValue != 0)) + { + dirtyPorts.Add(port.id); + } + } } private void Common_OnPaintThemeChange(TrainCarPaint paintController) @@ -735,6 +757,7 @@ private void Common_OnFuseUpdated(Fuse fuse) { if (UnloadWatcher.isUnloading || NetworkLifecycle.Instance.IsProcessingPacket) return; + dirtyFuses.Add(fuse.id); } @@ -743,13 +766,11 @@ public void Common_UpdatePorts(CommonTrainPortsPacket packet) if (!hasSimFlow) return; - //string log = $"CommonTrainPortsPacket({TrainCar.ID})"; for (int i = 0; i < packet.PortIds.Length; i++) { - if(simulationFlow.TryGetPort(packet.PortIds[i], out Port port)) + 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); @@ -774,8 +795,6 @@ public void Common_UpdateFuses(CommonTrainFusesPacket packet) 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) diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index b9d21053..9078f65c 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -492,9 +492,6 @@ private void OnClientboundSpawnTrainSetPacket(ClientboundSpawnTrainSetPacket pac } NetworkedCarSpawner.SpawnCars(packet.SpawnParts, packet.AutoCouple); - - foreach (TrainsetSpawnPart spawnPart in packet.SpawnParts) - SendTrainSyncRequest(spawnPart.NetId); } private void OnClientboundDestroyTrainCarPacket(ClientboundDestroyTrainCarPacket packet) From 36551d43720a9405ca162a35068357f1e6a9fb46 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 25 Jan 2025 19:22:55 +1000 Subject: [PATCH 207/521] Implement server browser changes to allow relay connections --- .../Components/MainMenu/ServerBrowserPane.cs | 696 +++++++----------- .../Managers/Client/ServerBrowserClient.cs | 493 +++++++------ .../Managers/Server/LobbyServerManager.cs | 126 ++-- .../TransportLayers/SteamworksTransport.cs | 4 +- Multiplayer/Patches/Util/DVSteamworksPatch.cs | 17 + Multiplayer/Settings.cs | 2 +- Multiplayer/Utils/SteamWorksUtils.cs | 42 +- 7 files changed, 636 insertions(+), 744 deletions(-) create mode 100644 Multiplayer/Patches/Util/DVSteamworksPatch.cs diff --git a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs index c9e99326..b5106564 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs @@ -10,7 +10,6 @@ using TMPro; using UnityEngine; using UnityEngine.UI; -using UnityEngine.Networking; using System.Linq; using Multiplayer.Networking.Data; using DV; @@ -19,47 +18,15 @@ using System.Collections.Generic; using Steamworks; using Steamworks.Data; -using Multiplayer.Networking.Managers.Client; - namespace Multiplayer.Components.MainMenu { public class ServerBrowserPane : MonoBehaviour { - private class PingRecord - { - int ping1; - int ping2; - int received; - - public PingRecord() - { - ping1 = -1; - ping2 = -1; - } - - public int Avg() - { - 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++; - } - } - private enum ConnectionState { NotConnected, + AttemptingSteamRelay, AttemptingIPv6, AttemptingIPv6Punch, AttemptingIPv4, @@ -79,21 +46,10 @@ private enum ConnectionState private string serverIDOnRefresh; private IServerBrowserGameDetails selectedServer; - //ping tracking - private readonly List serversToPing = []; - private readonly Dictionary serverPings = []; + //ping tracking private float pingTimer = 0f; 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 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; - private float discoveryTimer = 0f; //Button variables private ButtonDV buttonJoin; @@ -112,11 +68,11 @@ private enum ConnectionState private const int REFRESH_MIN_TIME = 10; //Stop refresh spam private bool remoteRefreshComplete; - private ServerBrowserClient serverBrowserClient; - //connection parameters private string address; private int portNumber; + private Lobby? selectedLobby; + private static Lobby? joinedLobby; string password = null; bool direct = false; @@ -131,7 +87,10 @@ private enum ConnectionState public void Awake() { - //Multiplayer.Log("MultiplayerPane Awake()"); + Multiplayer.Log("MultiplayerPane Awake()"); + joinedLobby?.Leave(); + joinedLobby = null; + CleanUI(); BuildUI(); @@ -154,12 +113,6 @@ public void OnEnable() buttonDirectIP.ToggleInteractable(true); buttonRefresh.ToggleInteractable(true); - //Start the server browser network client - serverBrowserClient = new ServerBrowserClient(Multiplayer.Settings); - serverBrowserClient.OnPing += this.OnPing; - serverBrowserClient.OnDiscovery += this.OnDiscovery; - serverBrowserClient.Start(); - RefreshAction(); } @@ -167,32 +120,14 @@ public void OnEnable() public void OnDisable() { this.SetupListeners(false); - - if (serverBrowserClient != null) - { - serverBrowserClient.OnPing -= this.OnPing; - serverBrowserClient.Stop(); - serverBrowserClient = null; - } - } - - public void OnDestroy() - { - if (serverBrowserClient == null) - return; - - serverBrowserClient.OnPing -= this.OnPing; - serverBrowserClient.Stop(); } public void Update() { - //Poll for any LAN discovery or ping packets - serverBrowserClient?.PollEvents(); + SteamClient.RunCallbacks(); //Handle server refresh interval timePassed += Time.deltaTime; - discoveryTimer += Time.deltaTime; if (!serverRefreshing) { @@ -205,30 +140,21 @@ public void Update() buttonRefresh.ToggleInteractable(true); } } - else if(localRefreshComplete && remoteRefreshComplete) + else if(remoteRefreshComplete) { - ExpireLocalServers(); //remove any that have not been seen in a while RefreshGridView(); IndexChanged(gridView); //Revalidate any selected servers - - localRefreshComplete = false; remoteRefreshComplete = false; serverRefreshing = false; timePassed = 0; } - else - { - if (discoveryTimer >= DISCOVERY_TIMEOUT) - localRefreshComplete = true; - } - //Handle pinging servers pingTimer += Time.deltaTime; - if (pingTimer >= (serversToPing.Count > 0 ? PING_BATCH_INTERVAL : GetPingInterval())) + if (pingTimer >= PING_INTERVAL) { - PingNextBatch(); + UpdatePings(); pingTimer = 0f; } } @@ -427,15 +353,9 @@ private void RefreshAction() buttonJoin.ToggleInteractable(false); buttonRefresh.ToggleInteractable(false); - StartCoroutine(GetRequest($"{Multiplayer.Settings.LobbyServerAddress}/list_game_servers")); - if (DVSteamworks.Success) ListActiveLobbies(); - - //Send a message to find local peers - discoveryTimer = 0f; - serverBrowserClient?.SendDiscoveryRequest(); } private void JoinAction() { @@ -446,17 +366,22 @@ private void JoinAction() //not making a direct connection direct = false; - portNumber = selectedServer.port; - password = null; //clear the password - - if (selectedServer.HasPassword) + portNumber = -1; + var lobby = GetLobbyFromServer(selectedServer); + if (lobby != null) { - ShowPasswordPopup(); - return; - } + selectedLobby = (Lobby)lobby; + password = null; //clear the password - AttemptConnection(); - + if (selectedServer.HasPassword) + { + ShowPasswordPopup(); + return; + } + + AttemptConnection(); + + } } } @@ -470,19 +395,17 @@ private void DirectAction() direct = true; password = null; + //ShowSteamID(); ShowIpPopup(); } private void IndexChanged(AGridView gridView) { - //Debug.Log($"Index: {gridView.SelectedModelIndex}"); if (serverRefreshing) return; if (gridView.SelectedModelIndex >= 0) { - //Multiplayer.Log($"Selected server: {gridViewModel[gridView.SelectedModelIndex].Name}"); - selectedServer = gridViewModel[gridView.SelectedModelIndex]; UpdateDetailsPane(); @@ -521,7 +444,6 @@ private void UpdateDetailsPane() if (selectedServer != null) { - //Multiplayer.Log("Prepping Data"); serverName.text = selectedServer.Name; //note: built-in localisations have a trailing colon e.g. 'Game mode:' @@ -616,6 +538,34 @@ private void ShowIpPopup() }; } + //private void ShowSteamID() + //{ + // var popup = MainMenuThingsAndStuff.Instance.ShowRenamePopup(); + // if (popup == null) + // { + // Multiplayer.LogError("Popup not found."); + // return; + // } + + // popup.labelTMPro.text = "SteamID"; + // //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; + // } + + // steamId = popup.GetComponentInChildren().text; + // Multiplayer.LogDebug(() => $"Attempting to connecto SteamID: {steamId}"); + + // ShowPasswordPopup(); + // }; + //} + private void ShowPortPopup() { @@ -713,18 +663,25 @@ public void ShowConnectingPopup() loc.UpdateLocalization(); - popup.labelTMPro.text = $"Connecting, please wait...\r\nAttempt: {attempt}"; //to be localised + popup.labelTMPro.text = $"Connecting, please wait..."; //to be localised - popup.Closed += _ => + popup.Closed += (PopupResult result) => { connectionState = ConnectionState.Aborted; }; } - private void AttemptConnection() + + #region workflow + private void UpdatePings() + { + UpdatePingsSteam(); + } + + private async void AttemptConnection() { - Multiplayer.Log($"AttemptConnection Direct: {direct}, Address: {address}"); + Multiplayer.Log($"AttemptConnection Direct: {direct}, Address: {address}, Lobby: {selectedLobby?.Id.ToString()}"); attempt = 0; connectionState = ConnectionState.NotConnected; @@ -732,129 +689,144 @@ private void AttemptConnection() 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) + if(selectedLobby != null) { - AttemptIPv6(); - } - - return; - } + joinedLobby = selectedLobby; //store the lobby for when we disconnect - Multiplayer.LogError($"IP address invalid: {address}"); + connectionState = ConnectionState.AttemptingSteamRelay; - AttemptFail(); - } - - private void AttemptIPv6() - { - Multiplayer.Log($"AttemptIPv6() {address}"); - - if (connectionState == ConnectionState.Aborted) - return; - - attempt++; - if (connectingPopup != null) - connectingPopup.labelTMPro.text = $"Connecting, please wait...\r\nAttempt: {attempt}"; - - Multiplayer.Log($"AttemptIPv6() starting attempt"); - connectionState = ConnectionState.AttemptingIPv6; - SingletonBehaviour.Instance.StartClient(address, portNumber, 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, portNumber, password, false, OnDisconnect); - - } - private void AttemptIPv4() - { - Multiplayer.Log($"AttemptIPv4() {address}, {connectionState}"); - - if (connectionState == ConnectionState.Aborted) - return; - - attempt++; - if (connectingPopup != null) - connectingPopup.labelTMPro.text = $"Connecting, please wait...\r\nAttempt: {attempt}"; + var joinResult = await joinedLobby?.Join(); + if (joinResult == RoomEnter.Success) + { + string hostId = ((Lobby)joinedLobby).Owner.Id.Value.ToString(); + NetworkLifecycle.Instance.StartClient(hostId, -1, password, false, OnDisconnect); + } + else + { + Multiplayer.LogDebug(() => "AttemptConnection() Leaving Lobby"); + joinedLobby?.Leave(); + joinedLobby = null; + Multiplayer.Log($"Failed to join lobby: {joinResult}"); + AttemptFail(); + } - if (!direct) - { - if(selectedServer.ipv4 == null || selectedServer.ipv4 == string.Empty) - { - AttemptFail(); return; } - - address = selectedServer.ipv4; } - Multiplayer.Log($"AttemptIPv4() {address}"); + //Multiplayer.Log($"AttemptConnection address: {address}"); - 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, portNumber, password, false, OnDisconnect); - return; - } - } - - Multiplayer.Log($"AttemptIPv4() TryParse failed"); - AttemptFail(); - string message = "Host Unreachable"; - MainMenuThingsAndStuff.Instance.ShowOkPopup(message, () => { }); - } + //if (IPAddress.TryParse(address, out IPAddress IPaddress)) + //{ + // Multiplayer.Log($"AttemptConnection tryParse: {IPaddress.AddressFamily}"); - private void AttemptIPv4Punch() - { - Multiplayer.Log($"AttemptIPv4Punch() {address}"); + // if (IPaddress.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork) + // { + // AttemptIPv4(); + // } + // else if (IPaddress.AddressFamily == System.Net.Sockets.AddressFamily.InterNetworkV6) + // { + // AttemptIPv6(); + // } - if (connectionState == ConnectionState.Aborted) - return; + // return; + //} - attempt++; - if (connectingPopup != null) - connectingPopup.labelTMPro.text = $"Connecting, please wait...\r\nAttempt: {attempt}"; + //Multiplayer.LogError($"IP address invalid: {address}"); - //punching not implemented we'll just try again for now - connectionState = ConnectionState.AttemptingIPv4Punch; - SingletonBehaviour.Instance.StartClient(address, portNumber, password, false, OnDisconnect); + //AttemptFail(); } + //private void AttemptIPv6() + //{ + // Multiplayer.Log($"AttemptIPv6() {address}"); + + // if (connectionState == ConnectionState.Aborted) + // return; + + // attempt++; + // if (connectingPopup != null) + // connectingPopup.labelTMPro.text = $"Connecting, please wait...\r\nAttempt: {attempt}"; + + // Multiplayer.Log($"AttemptIPv6() starting attempt"); + // connectionState = ConnectionState.AttemptingIPv6; + // SingletonBehaviour.Instance.StartClient(address, portNumber, 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, portNumber, password, false, OnDisconnect); + + //} + //private void AttemptIPv4() + //{ + // Multiplayer.Log($"AttemptIPv4() {address}, {connectionState}"); + + // 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)) + // { + // 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, portNumber, password, false, OnDisconnect); + // return; + // } + // } + + // Multiplayer.Log($"AttemptIPv4() TryParse failed"); + // AttemptFail(); + // string message = "Host Unreachable"; + // MainMenuThingsAndStuff.Instance.ShowOkPopup(message, () => { }); + //} + + //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, portNumber, password, false, OnDisconnect); + //} + private void AttemptFail() { connectionState = ConnectionState.Failed; @@ -881,35 +853,35 @@ private void OnDisconnect(DisconnectReason reason, string message) string displayMessage = message; + Multiplayer.LogDebug(() => "OnDisconnect() Leaving Lobby"); + joinedLobby?.Leave(); + joinedLobby = null; + if (string.IsNullOrEmpty(message)) { //fallback for no message (server initiated disconnects should have a message) - if (reason == DisconnectReason.ConnectionFailed) - { - switch (connectionState) - { - case ConnectionState.AttemptingIPv6: - if (Multiplayer.Settings.EnableNatPunch) - AttemptIPv6Punch(); - else - AttemptIPv4(); - return; - case ConnectionState.AttemptingIPv6Punch: - AttemptIPv4(); - return; - case ConnectionState.AttemptingIPv4: - if (Multiplayer.Settings.EnableNatPunch) - AttemptIPv4Punch(); - else - AttemptFail(); - message = "Host Unreachable"; //TODO: add translations - return; - case ConnectionState.AttemptingIPv4Punch: - AttemptFail(); - message = "Host Unreachable"; //TODO: add translations - break; - } - } + //if (reason == DisconnectReason.ConnectionFailed) + //{ + // switch (connectionState) + // { + // case ConnectionState.AttemptingIPv6: + // if (Multiplayer.Settings.EnableNatPunch) + // AttemptIPv6Punch(); + // else + // AttemptIPv4(); + // return; + // case ConnectionState.AttemptingIPv6Punch: + // AttemptIPv4(); + // return; + // case ConnectionState.AttemptingIPv4: + // if (Multiplayer.Settings.EnableNatPunch) + // { + // AttemptIPv4Punch(); + // return; + // } + // break; + // } + //} displayMessage = GetDisplayMessageForDisconnect(reason); AttemptFail(); @@ -926,24 +898,10 @@ private void OnDisconnect(DisconnectReason reason, string message) }); } - IEnumerator GetRequest(string uri) private string GetDisplayMessageForDisconnect(DisconnectReason reason) { - 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) - return reason switch + return reason switch { - Multiplayer.LogError(pages[page] + ": Error: " + webRequest.error); - } - else - { - Multiplayer.Log(pages[page] + ":\nReceived: " + webRequest.downloadHandler.text); DisconnectReason.UnknownHost => "Unknown Host", DisconnectReason.DisconnectPeerCalled => "Player Kicked", DisconnectReason.ConnectionFailed => "Host Unreachable", @@ -953,61 +911,89 @@ private string GetDisplayMessageForDisconnect(DisconnectReason reason) _ => "Connection Failed" }; } + #endregion - LobbyServerData[] response; - response = Newtonsoft.Json.JsonConvert.DeserializeObject(webRequest.downloadHandler.text); + #region steam lobby + private async void ListActiveLobbies() + { + lobbies = await SteamMatchmaking.LobbyList.WithMaxResults(100) + //.WithKeyValue(SteamworksUtils.MP_MOD_KEY, string.Empty) + .RequestAsync(); + + Multiplayer.LogDebug(() => $"ListActiveLobbies() lobbies found: {lobbies?.Count()}"); - Multiplayer.Log($"Serverbrowser servers: {response.Length}"); + remoteServers.Clear(); - foreach (LobbyServerData server in response) + if (lobbies != null) + { + var myLoc = SteamNetworkingUtils.LocalPingLocation; + + foreach (var lobby in lobbies) { - Multiplayer.Log($"Server name: \"{server.Name}\", IPv4: {server.ipv4}, IPv6: {server.ipv6}, Port: {server.port}"); - } + LobbyServerData server = SteamworksUtils.GetLobbyData(lobby); - remoteServers.AddRange(response); + server.id = lobby.Id.ToString(); - } + server.CurrentPlayers = lobby.MemberCount; + server.MaxPlayers = lobby.MaxMembers; + remoteServers.Add(server); + + Multiplayer.LogDebug(() => $"ListActiveLobbies() lobby {server.Name}, {lobby.MemberCount}/{lobby.MaxMembers}"); + + } + } remoteRefreshComplete = true; } - private async void ListActiveLobbies() + private void UpdatePingsSteam() { - lobbies = await SteamMatchmaking.LobbyList.WithMaxResults(100).RequestAsync(); - foreach (var lobby in lobbies) + foreach (var server in gridViewModel) { - var name = lobby.GetData("Server Name"); - var difficulty = lobby.GetData("Difficulty"); - Multiplayer.Log($"Steamworks Lobby Server name: \"{name}\", Difficulty: {difficulty}"); - } + if (server is LobbyServerData lobbyServer) + { + if (ulong.TryParse(server.id,out ulong id)) + { + Lobby? lobby = lobbies.FirstOrDefault(l => l.Id.Value == id); + if (lobby != null) + { + string strLoc = ((Lobby)lobby).GetData(SteamworksUtils.LOBBY_NET_LOCATION_KEY); + NetPingLocation? location = NetPingLocation.TryParseFromString(strLoc); + if (location != null) + server.Ping = SteamNetworkingUtils.EstimatePingTo((NetPingLocation)location) / 2; //normalise to one way ping + } + } + + UpdateElement(lobbyServer); + } + } } - private void RefreshGridView() + private Lobby? GetLobbyFromServer(IServerBrowserGameDetails server) { + if (ulong.TryParse(server.id, out ulong id)) + return lobbies.FirstOrDefault(l => l.Id.Value == id); - bool startPing = gridViewModel.Count == 0; + return null; + } + #endregion + private void RefreshGridView() + { var allServers = new List(); - allServers.AddRange(localServers); - allServers.AddRange(remoteServers.Where(r => !localServers.Any(l => l.id == r.id))); + allServers.AddRange(remoteServers); // 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)}"); - // 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))}"); // 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); } @@ -1058,17 +1044,6 @@ private void RefreshGridView() } serverIDOnRefresh = null; } - - //trigger ping to start - if (startPing && gridViewModel.Count() > 0) - PingNextBatch(); - } - private void SetButtonsActive(params GameObject[] buttons) - { - foreach (var button in buttons) - { - button.SetActive(true); - } } private string ExtractDomainName(string input) @@ -1090,138 +1065,5 @@ private string ExtractDomainName(string input) return input; } - - #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) - { - //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; - - // 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() - { - 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 PingNextBatch() - { - 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) - { - return ipv4Ping; - } - else if (ipv6Ping > -1) - { - return ipv6Ping; - } - return -1; // No ping available - } - - private void OnDiscovery(IPEndPoint endpoint, LobbyServerData data) - { - if (data == null || endpoint == null) - return; - - 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) - { - data.LocalIPv4 = endpoint.Address.ToString(); - Multiplayer.Log($"Setting LocalIPv4 to {data.LocalIPv4}"); - } - else if (endpoint.AddressFamily == System.Net.Sockets.AddressFamily.InterNetworkV6) - { - data.LocalIPv6 = endpoint.Address.ToString(); - Multiplayer.Log($"Setting LocalIPv6 to {data.LocalIPv6}"); - } - - // 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() - { - 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/Networking/Managers/Client/ServerBrowserClient.cs b/Multiplayer/Networking/Managers/Client/ServerBrowserClient.cs index 516ed1a5..532475f9 100644 --- a/Multiplayer/Networking/Managers/Client/ServerBrowserClient.cs +++ b/Multiplayer/Networking/Managers/Client/ServerBrowserClient.cs @@ -1,242 +1,251 @@ -using System; -using System.Net; -using System.Collections.Generic; -using LiteNetLib; -using Multiplayer.Networking.Packets.Unconnected; -using System.Threading.Tasks; -using System.Diagnostics; -using System.Linq; -using Multiplayer.Networking.Data; -using Steamworks; -using System.Text; -using Steamworks.Data; -using UnityEngine; - - -namespace Multiplayer.Networking.Managers.Client; - -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 readonly Dictionary pingInfos = []; - public Action OnPing; // serverId, pingTime, isIPv4 - public Action OnDiscovery; // endPoint, serverId, serverData - - private readonly int[] discoveryPorts = [8888, 8889, 8890]; - - private const int PingTimeoutMs = 5000; // 5 seconds timeout - - public ServerBrowserClient(Settings settings) : base(settings) - { - } - - public void Start() - { - netManager.UseNativeSockets = true; - netManager.IPv6Enabled = true; - netManager.Start(); - - netManager.UpdateTime = 0; - } - 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); - LogDebug(() => $"Cleaned up timed out ping for {serverId}"); - } - } - } - - 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 - - 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)) - { - 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; - - if (isIPv4) - pingInfo.IPv4Received = true; - else - pingInfo.IPv6Received = true; - - OnPing?.Invoke(serverId, pingTime, isIPv4); - - //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}"); - } - } - } - - private void OnUnconnectedDiscoveryPacket(UnconnectedDiscoveryPacket packet, IPEndPoint endPoint) - { - //Log($"OnUnconnectedDiscoveryPacket({packet.PacketType}, {endPoint?.Address})"); - - if (packet.IsResponse) - { - - //Log($"OnUnconnectedDiscoveryPacket({packet.PacketType}, {endPoint?.Address}) id: {packet.data.id}"); - OnDiscovery?.Invoke(endPoint, packet.Data); - } - } - - #endregion - - #region Senders - public void 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(); - pingInfos[serverId] = pingInfo; - - //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)) - { - SendUnconnectedPacket(packet, ipv4, port); - pingInfo.IPv4Sent = true; - } - - // Send to IPv6 if provided - if (!string.IsNullOrEmpty(ipv6)) - { - SendUnconnectedPacket(packet, ipv6, port); - pingInfo.IPv6Sent = true; - } - - // Start a timeout task - _ = StartTimeoutTask(serverId); - } - - public void SendDiscoveryRequest() - { - foreach (int port in discoveryPorts) - { - try - { - netManager.SendBroadcast(WritePacket(new UnconnectedDiscoveryPacket()), port); - } - catch (Exception ex) - { - Multiplayer.Log($"SendDiscoveryRequest() Broadcast error: {ex.Message}\r\n{ex.StackTrace}"); - } - } - } - - #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 -} +//using System; +//using System.Net; +//using System.Collections.Generic; +//using LiteNetLib; +//using Multiplayer.Networking.Packets.Unconnected; +//using System.Threading.Tasks; +//using System.Diagnostics; +//using System.Linq; +//using Multiplayer.Networking.Data; +//using Steamworks; +//using System.Text; +//using Steamworks.Data; +//using UnityEngine; + + +//namespace Multiplayer.Networking.Managers.Client; + +//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 readonly Dictionary pingInfos = []; +// public Action OnPing; // serverId, pingTime, isIPv4 +// public Action OnDiscovery; // endPoint, serverId, serverData + +// private readonly int[] discoveryPorts = [8888, 8889, 8890]; + +// private const int PingTimeoutMs = 5000; // 5 seconds timeout + +// public ServerBrowserClient(Settings settings) : base(settings) +// { +// } + +// public void Start() +// { +// netManager.UseNativeSockets = true; +// netManager.IPv6Enabled = true; +// netManager.Start(); + +// netManager.UpdateTime = 0; +// } +// 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); +// LogDebug(() => $"Cleaned up timed out ping for {serverId}"); +// } +// } +// } + +// 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 + +// 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)) +// { +// 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; + +// if (isIPv4) +// pingInfo.IPv4Received = true; +// else +// pingInfo.IPv6Received = true; + +// OnPing?.Invoke(serverId, pingTime, isIPv4); + +// //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}"); +// } +// } +// } + +// private void OnUnconnectedDiscoveryPacket(UnconnectedDiscoveryPacket packet, IPEndPoint endPoint) +// { +// //Log($"OnUnconnectedDiscoveryPacket({packet.PacketType}, {endPoint?.Address})"); + +// if (packet.IsResponse) +// { + +// //Log($"OnUnconnectedDiscoveryPacket({packet.PacketType}, {endPoint?.Address}) id: {packet.data.id}"); +// OnDiscovery?.Invoke(endPoint, packet.Data); +// } +// } + +// public override void OnConnecting(Connection connection, ConnectionInfo info) +// { +// throw new NotImplementedException(); +// } + +// public override void OnConnected(Connection connection, ConnectionInfo info) +// { +// throw new NotImplementedException(); +// } + +// public override void OnDisconnected(Connection connection, ConnectionInfo info) +// { +// throw new NotImplementedException(); +// } + +// public override void OnMessage(Connection connection, NetIdentity identity, IntPtr data, int size, long messageNum, long recvTime, int channel) +// { +// throw new NotImplementedException(); +// } + +// #endregion + +// #region Senders +// public void 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(); +// pingInfos[serverId] = pingInfo; + +// //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)) +// { +// SendUnconnectedPacket(packet, ipv4, port); +// pingInfo.IPv4Sent = true; +// } + +// // Send to IPv6 if provided +// if (!string.IsNullOrEmpty(ipv6)) +// { +// SendUnconnectedPacket(packet, ipv6, port); +// pingInfo.IPv6Sent = true; +// } + +// // Start a timeout task +// _ = StartTimeoutTask(serverId); +// } + +// public void SendDiscoveryRequest() +// { +// foreach (int port in discoveryPorts) +// { +// try +// { +// netManager.SendBroadcast(WritePacket(new UnconnectedDiscoveryPacket()), port); +// } +// catch (Exception ex) +// { +// Multiplayer.Log($"SendDiscoveryRequest() Broadcast error: {ex.Message}\r\n{ex.StackTrace}"); +// } +// } +// } +// #endregion + +//} diff --git a/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs b/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs index 8810145d..966cc0f5 100644 --- a/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs +++ b/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs @@ -16,6 +16,8 @@ using System.Linq; using Steamworks; using Steamworks.Data; +using Multiplayer.Utils; +using Multiplayer.Networking.TransportLayers; namespace Multiplayer.Networking.Managers.Server; public class LobbyServerManager : MonoBehaviour @@ -38,7 +40,9 @@ public class LobbyServerManager : MonoBehaviour private string server_id; private string private_key; - private Lobby lobby; + //Steam Lobby + public static readonly string[] EXCLUDE_PARAMS = {"id", "ipv4", "ipv6", "port", "LocalIPv4", "LocalIPv6", "Ping", "isPublic", "LastSeen", "CurrentPlayers", "MaxPlayers"}; + private Lobby? lobby; private bool initialised = false; private bool sendUpdates = false; @@ -51,82 +55,23 @@ public class LobbyServerManager : MonoBehaviour private readonly NetDataWriter cachedWriter = new(); public static int[] discoveryPorts = [8888, 8889, 8890]; - #region + #region Unity public void Awake() { server = NetworkLifecycle.Instance.Server; Multiplayer.Log($"LobbyServerManager New({server != null})"); - - if (DVSteamworks.Success) - { - CreateLobby(); - } - } - - public async void CreateLobby() - { - // Check if the user is a legitimate Steam user - if (!IsLegitimateSteamUser()) - { - server.Log("User is suspected to be using a pirated copy. Lobby creation aborted."); - return; - } - - // Specify the lobby type (public, private, etc.) - var result = await SteamMatchmaking.CreateLobbyAsync(server.serverData.MaxPlayers); - - if (result.HasValue) - { - // Lobby was created successfully - server.Log("Steam Lobby created successfully!"); - lobby = result.Value; - lobby.SetPublic(); - lobby.SetJoinable(true); - lobby.SetData("Server Name", server.serverData.Name); - lobby.SetData("Difficulty", server.serverData.Difficulty.ToString()); - } - else - { - // Handle failure - server.Log("Failed to create lobby."); - } } - private bool IsLegitimateSteamUser() + public IEnumerator Start() { - // Check if the Steam client is valid - if (SteamClient.IsValid) + //Create a steam lobby + if (DVSteamworks.Success) { - // Verify the Steam ID is valid - if (SteamClient.SteamId.IsValid) - { - - // Check if the game is installed using the App ID - bool isInstalled = SteamApps.IsAppInstalled(DVSteamworks.APP_ID); - - if (isInstalled) - { - System.Console.WriteLine($"Steam ID {SteamClient.SteamId} is valid and the game is installed."); - return true; - } - else - { - // Log the piracy suspicion - server.Log($"Suspicion: Steam ID {SteamClient.SteamId} does not have the game installed. Potential piracy detected."); - } - } + CreateSteamLobby(); } - // If Steam client or Steam ID is not valid, log as suspicious - System.Console.WriteLine("Steam client is invalid or pirated Steam account detected."); - server.Log("Suspicion: Invalid Steam client or pirated Steam account detected."); - - return false; - } - - public IEnumerator Start() - { + //Register with old php lobby server (provides stats and makes the lobby visible, but not joinable to users on old versions) server.serverData.ipv6 = GetStaticIPv6Address(); server.serverData.LocalIPv4 = GetLocalIPv4Address(); @@ -169,11 +114,9 @@ public void OnDestroy() StopAllCoroutines(); StartCoroutine(RemoveFromLobbyServer($"{Multiplayer.Settings.LobbyServerAddress}/{ENDPOINT_REMOVE_SERVER}")); - if (lobby.Id.IsValid) - { - lobby.SetJoinable(false); - lobby.Leave(); - } + + lobby?.SetJoinable(false); + lobby?.Leave(); discoveryManager?.Stop(); } @@ -189,6 +132,11 @@ public void Update() timePassed = 0f; server.serverData.CurrentPlayers = server.PlayerCount; StartCoroutine(UpdateLobbyServer($"{Multiplayer.Settings.LobbyServerAddress}/{ENDPOINT_UPDATE_SERVER}")); + + if(lobby != null) + { + SteamworksUtils.SetLobbyData((Lobby)lobby, server.serverData, EXCLUDE_PARAMS); + } } }else if (!server.serverData.isPublic || !sendUpdates) { @@ -201,6 +149,40 @@ public void Update() #endregion + #region Steam Lobby + public async void CreateSteamLobby() + { + + // Specify the lobby type (public, private, etc.) + var result = await SteamMatchmaking.CreateLobbyAsync(server.serverData.MaxPlayers); + + if (result.HasValue) + { + // Lobby was created successfully + lobby = result.Value; + + server.Log("Steam Lobby created successfully!"); + server.LogDebug(() => $"Steam lobby ID: {lobby?.Id}"); + + lobby?.SetData(SteamworksUtils.LOBBY_MP_MOD_KEY, string.Empty); //We'll add this in for filtering + lobby?.SetData(SteamworksUtils.LOBBY_NET_LOCATION_KEY, SteamNetworkingUtils.LocalPingLocation.ToString()); //for ping estimation + + SteamworksUtils.SetLobbyData((Lobby)lobby, server.serverData, EXCLUDE_PARAMS); + + //todo implement public/private/friends + if (server.serverData.isPublic) + lobby?.SetPublic(); + + lobby?.SetJoinable(true); + } + else + { + // Handle failure + server.LogError("Failed to create lobby."); + } + } + #endregion + #region Lobby Server public void RemoveFromLobbyServer() { @@ -461,7 +443,7 @@ public void StartDiscoveryServer() BroadcastReceiveEnabled = true, }; - packetProcessor = new NetPacketProcessor(discoveryManager); + packetProcessor = new NetPacketProcessor(); discoveryListener.NetworkReceiveUnconnectedEvent += OnNetworkReceiveUnconnected; diff --git a/Multiplayer/Networking/TransportLayers/SteamworksTransport.cs b/Multiplayer/Networking/TransportLayers/SteamworksTransport.cs index fa44eabf..69d206d8 100644 --- a/Multiplayer/Networking/TransportLayers/SteamworksTransport.cs +++ b/Multiplayer/Networking/TransportLayers/SteamworksTransport.cs @@ -58,14 +58,16 @@ public bool Start(int port) } server = SteamNetworkingSockets.CreateRelaySocket(); - + if (server != null) { server.transport = this; servers.Add(server); IsRunning = true; + Multiplayer.Log($"SteamId: {Steamworks.Data.NetIdentity.LocalHost}"); } + return IsRunning; } diff --git a/Multiplayer/Patches/Util/DVSteamworksPatch.cs b/Multiplayer/Patches/Util/DVSteamworksPatch.cs new file mode 100644 index 00000000..1646e436 --- /dev/null +++ b/Multiplayer/Patches/Util/DVSteamworksPatch.cs @@ -0,0 +1,17 @@ +using HarmonyLib; +using Steamworks; + + +namespace Multiplayer.Patches.Util; + +[HarmonyPatch(typeof(DVSteamworks))] +public static class DVSteamworksPatch +{ + [HarmonyPatch(nameof(DVSteamworks.Awake))] + [HarmonyPostfix] + public static void Awake() + { + if (DVSteamworks.Success) + SteamNetworkingUtils.InitRelayNetworkAccess(); + } +} diff --git a/Multiplayer/Settings.cs b/Multiplayer/Settings.cs index 65a0aa4d..703b6197 100644 --- a/Multiplayer/Settings.cs +++ b/Multiplayer/Settings.cs @@ -134,7 +134,7 @@ public string GetUserName() if (Multiplayer.Settings.UseSteamName) { - if (SteamWorksUtils.GetSteamUser(out string steamUsername, out ulong steamId)) + if (SteamworksUtils.GetSteamUser(out string steamUsername, out ulong steamId)) { Multiplayer.Settings.LastSteamName = steamUsername; Multiplayer.Settings.SteamId = steamId; diff --git a/Multiplayer/Utils/SteamWorksUtils.cs b/Multiplayer/Utils/SteamWorksUtils.cs index db7d957c..c5ad7287 100644 --- a/Multiplayer/Utils/SteamWorksUtils.cs +++ b/Multiplayer/Utils/SteamWorksUtils.cs @@ -1,10 +1,23 @@ +using DV.UIFramework; +using Multiplayer.Components.MainMenu; +using Multiplayer.Components.Networking; +using Multiplayer.Networking.Data; +using Multiplayer.Patches.MainMenu; using Steamworks; +using Steamworks.Data; using System; +using System.Linq; namespace Multiplayer.Utils; -public static class SteamWorksUtils +public static class SteamworksUtils { + public const string LOBBY_MP_MOD_KEY = "MP_MOD"; + public const string LOBBY_NET_LOCATION_KEY = "NetLocation"; + public const string LOBBY_HAS_PASSWORD = "HasPassword"; + + private static bool hasJoinedCL; + public static bool GetSteamUser(out string username, out ulong steamId) { username = null; @@ -34,4 +47,31 @@ public static bool GetSteamUser(out string username, out ulong steamId) return true; } + + public static void SetLobbyData(Lobby lobby, LobbyServerData data, string[] exclude) + { + var properties = typeof(LobbyServerData).GetProperties().Where(p => !exclude.Contains(p.Name)); + foreach (var prop in properties) + { + var value = prop.GetValue(data)?.ToString() ?? ""; + lobby.SetData(prop.Name, value); + } + } + + public static LobbyServerData GetLobbyData(this Lobby lobby) + { + var data = new LobbyServerData(); + var properties = typeof(LobbyServerData).GetProperties(); + + foreach (var prop in properties) + { + var value = lobby.GetData(prop.Name); + if (string.IsNullOrEmpty(value)) continue; + + var converted = Convert.ChangeType(value, prop.PropertyType); + prop.SetValue(data, converted); + } + + return data; + } } From 973d393d7aaf0e0cde80224f59fb863b72bde9b8 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 25 Jan 2025 19:24:14 +1000 Subject: [PATCH 208/521] fix HostGamePane port textbox --- Multiplayer/Components/MainMenu/HostGamePane.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Multiplayer/Components/MainMenu/HostGamePane.cs b/Multiplayer/Components/MainMenu/HostGamePane.cs index 996f35c1..82921c86 100644 --- a/Multiplayer/Components/MainMenu/HostGamePane.cs +++ b/Multiplayer/Components/MainMenu/HostGamePane.cs @@ -268,7 +268,8 @@ private void BuildUI() 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(); + port.placeholder.GetComponent().text = "7777"; + port.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); From c3e1d4c7e479a1ccd4b81f1ba3fdbd260076820d Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 25 Jan 2025 19:26:57 +1000 Subject: [PATCH 209/521] Allow steam invites when game is not running --- .../Components/MainMenu/ServerBrowserPane.cs | 27 ++++ .../MainMenu/LauncherControllerPatch.cs | 18 +-- .../MainMenu/RightPaneControllerPatch.cs | 145 ++++++++++-------- Multiplayer/Utils/SteamWorksUtils.cs | 52 +++++++ 4 files changed, 169 insertions(+), 73 deletions(-) diff --git a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs index b5106564..21bd3585 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs @@ -73,6 +73,7 @@ private enum ConnectionState private int portNumber; private Lobby? selectedLobby; private static Lobby? joinedLobby; + public static Lobby? lobbyToJoin; string password = null; bool direct = false; @@ -96,6 +97,32 @@ public void Awake() SetupServerBrowser(); RefreshGridView(); + + //For invites + Multiplayer.Log($"Invite from command line: {lobbyToJoin != null}"); + if (lobbyToJoin != null) + { + direct = false; + selectedLobby = lobbyToJoin; + + foreach(var item in lobbyToJoin?.Data) + { + Multiplayer.Log($"Invite from command line ({lobbyToJoin?.Id}) Data: {item.Key}, {item.Value}"); + } + string hasPass = lobbyToJoin?.GetData(SteamworksUtils.LOBBY_HAS_PASSWORD); + Multiplayer.Log($"Invite from command line ({lobbyToJoin?.Id}) hasPass: {hasPass}"); + + if (string.IsNullOrEmpty(hasPass)) + { + Multiplayer.Log($"Invite from command line ({lobbyToJoin?.Id}) Attempting..."); + AttemptConnection(); + } + else + { + Multiplayer.Log($"Invite from command line ({lobbyToJoin?.Id}) Ask Password..."); + ShowPasswordPopup(); + } + } } public void OnEnable() diff --git a/Multiplayer/Patches/MainMenu/LauncherControllerPatch.cs b/Multiplayer/Patches/MainMenu/LauncherControllerPatch.cs index 98a67b14..9de50908 100644 --- a/Multiplayer/Patches/MainMenu/LauncherControllerPatch.cs +++ b/Multiplayer/Patches/MainMenu/LauncherControllerPatch.cs @@ -68,12 +68,12 @@ private static void OnEnable(LauncherController __instance) [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) + if (RightPaneController_Patch.hgpInstance == null) return; - RightPaneController_OnEnable_Patch.hgpInstance.saveGame = saveGame; - RightPaneController_OnEnable_Patch.hgpInstance.userProvider = userProvider; - RightPaneController_OnEnable_Patch.hgpInstance.scenarioProvider = scenarioProvider; + RightPaneController_Patch.hgpInstance.saveGame = saveGame; + RightPaneController_Patch.hgpInstance.userProvider = userProvider; + RightPaneController_Patch.hgpInstance.scenarioProvider = scenarioProvider; } @@ -82,12 +82,12 @@ private static void SetData(LauncherController __instance, ISaveGame saveGame, A [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) + if (RightPaneController_Patch.hgpInstance == null) return; - RightPaneController_OnEnable_Patch.hgpInstance.startGameData = startGameData; - RightPaneController_OnEnable_Patch.hgpInstance.userProvider = userProvider; - RightPaneController_OnEnable_Patch.hgpInstance.scenarioProvider = scenarioProvider; + RightPaneController_Patch.hgpInstance.startGameData = startGameData; + RightPaneController_Patch.hgpInstance.userProvider = userProvider; + RightPaneController_Patch.hgpInstance.scenarioProvider = scenarioProvider; } @@ -95,7 +95,7 @@ private static void HostAction() { //Debug.Log("Host button clicked."); - RightPaneController_OnEnable_Patch.uIMenuController.SwitchMenu(RightPaneController_OnEnable_Patch.hostMenuIndex); + RightPaneController_Patch.uIMenuController.SwitchMenu(RightPaneController_Patch.hostMenuIndex); } } diff --git a/Multiplayer/Patches/MainMenu/RightPaneControllerPatch.cs b/Multiplayer/Patches/MainMenu/RightPaneControllerPatch.cs index 99b491ed..d23a2e2e 100644 --- a/Multiplayer/Patches/MainMenu/RightPaneControllerPatch.cs +++ b/Multiplayer/Patches/MainMenu/RightPaneControllerPatch.cs @@ -3,88 +3,105 @@ using DV.UIFramework; using HarmonyLib; using Multiplayer.Components.MainMenu; +using Multiplayer.Components.Networking; using Multiplayer.Utils; +using Steamworks; +using System; +using System.Linq; using System.Reflection; using TMPro; using UnityEngine; -namespace Multiplayer.Patches.MainMenu +namespace Multiplayer.Patches.MainMenu; + +[HarmonyPatch(typeof(RightPaneController))] +public static class RightPaneController_Patch { - [HarmonyPatch(typeof(RightPaneController), "OnEnable")] - public static class RightPaneController_OnEnable_Patch + public static int hostMenuIndex; + public static int joinMenuIndex; + public static UIMenuController uIMenuController; + public static HostGamePane hgpInstance; + + [HarmonyPatch(nameof(RightPaneController.OnEnable))] + [HarmonyPrefix] + private static void OnEnablePre(RightPaneController __instance) { - public static int hostMenuIndex; - public static UIMenuController uIMenuController; - public static HostGamePane hgpInstance; - private static void Prefix(RightPaneController __instance) + uIMenuController = __instance.menuController; + // 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"); + if (basePane == null) { - uIMenuController = __instance.menuController; - // Check if the multiplayer pane already exists - if (__instance.HasChildWithName("PaneRight Multiplayer")) - return; + Multiplayer.LogError("Failed to find Launcher pane!"); + return; + } - // 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; - } + // 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"; - // 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"; + // Add the multiplayer pane to the menu controller + __instance.menuController.controlledMenus.Add(multiplayerPane.GetComponent()); + joinMenuIndex = __instance.menuController.controlledMenus.Count - 1; + UIMenuRequester mpButtonReq = MainMenuController_Awake_Patch.multiplayerButton.GetComponent(); + mpButtonReq.requestedMenuIndex =joinMenuIndex; - // 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; + // Clean up unnecessary components and child objects + GameObject.Destroy(multiplayerPane.GetComponent()); + GameObject.Destroy(multiplayerPane.GetComponent()); + multiplayerPane.AddComponent(); - // Clean up unnecessary components and child objects - GameObject.Destroy(multiplayerPane.GetComponent()); - GameObject.Destroy(multiplayerPane.GetComponent()); - multiplayerPane.AddComponent(); + // 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; + }); - // 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; - }); + // Activate the multiplayer button + MainMenuController_Awake_Patch.multiplayerButton.SetActive(true); + //Multiplayer.Log("At end!"); - // Activate the multiplayer button - MainMenuController_Awake_Patch.multiplayerButton.SetActive(true); - //Multiplayer.Log("At end!"); + // Check if the host pane already exists + if (__instance.HasChildWithName("PaneRight Host")) + return; - // 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; + } - 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"; - // 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(); - 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; + } - // 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; - } + [HarmonyPatch(nameof(RightPaneController.OnEnable))] + [HarmonyPostfix] + private static void OnEnablePost(RightPaneController __instance) + { + if (Environment.GetCommandLineArgs().Contains("+connect_lobby")) + SteamworksUtils.JoinFromCommandLine(); } } diff --git a/Multiplayer/Utils/SteamWorksUtils.cs b/Multiplayer/Utils/SteamWorksUtils.cs index c5ad7287..d4de9061 100644 --- a/Multiplayer/Utils/SteamWorksUtils.cs +++ b/Multiplayer/Utils/SteamWorksUtils.cs @@ -74,4 +74,56 @@ public static LobbyServerData GetLobbyData(this Lobby lobby) return data; } + + public static ulong GetLobbyIdFromArgs() + { + string[] args = Environment.GetCommandLineArgs(); + + for (int i = 0; i < args.Length - 1; i++) + { + if (args[i] == "+connect_lobby") + { + return ulong.Parse(args[i + 1]); + } + } + + return 0; + } + + public static void JoinFromCommandLine() + { + if (hasJoinedCL) + return; + hasJoinedCL = true; + + SteamMatchmaking.OnLobbyDataChanged += OnLobbyDataChanged; + + var id = GetLobbyIdFromArgs(); + var sId = new SteamId + { + Value = id + }; + + var lobby = new Lobby(sId); + var ret = lobby.Refresh(); + } + + private static void OnLobbyDataChanged(Lobby lobby) + { + SteamMatchmaking.OnLobbyDataChanged -= OnLobbyDataChanged; + + NetworkLifecycle.Instance.QueueMainMenuEvent(() => + { + Multiplayer.Log($"OnLobbyDataChanged({lobby.Id}) count: {lobby.Data?.Count()}"); + + foreach (var item in lobby.Data) + { + Multiplayer.Log($"OnEnablePost Data: {item.Key}, Value: {item.Value}"); + } + + ServerBrowserPane.lobbyToJoin = lobby; + MainMenuThingsAndStuff.Instance.SwitchToMenu((byte)RightPaneController_Patch.joinMenuIndex); + }); + } + } From f61bf7fa139d9aa719eae0a530347bacf97add49 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 25 Jan 2025 22:11:50 +1000 Subject: [PATCH 210/521] Refactor MainMenuThingsAndStuff Add ability to access more popup prefabs, in particular the Yes / No popup --- .../Components/MainMenu/HostGamePane.cs | 2 +- .../MainMenu/MainMenuThingsAndStuff.cs | 59 ++++++++++++++----- .../MainMenu/MainMenuControllerPatch.cs | 11 ---- .../MainMenu/RightPaneControllerPatch.cs | 8 ++- .../Patches/Util/FacepunchNetAddressPatch.cs | 26 ++++++++ 5 files changed, 76 insertions(+), 30 deletions(-) create mode 100644 Multiplayer/Patches/Util/FacepunchNetAddressPatch.cs diff --git a/Multiplayer/Components/MainMenu/HostGamePane.cs b/Multiplayer/Components/MainMenu/HostGamePane.cs index 82921c86..3f35b8d7 100644 --- a/Multiplayer/Components/MainMenu/HostGamePane.cs +++ b/Multiplayer/Components/MainMenu/HostGamePane.cs @@ -122,7 +122,7 @@ private void BuildUI() return; } - GameObject inputPrefab = MainMenuThingsAndStuff.Instance.renamePopupPrefab.gameObject.FindChildByName("TextFieldTextIcon"); + GameObject inputPrefab = MainMenuThingsAndStuff.Instance.references.popupTextInput.gameObject.FindChildByName("TextFieldTextIcon"); if (inputPrefab == null) { Multiplayer.LogError("TextFieldTextIcon not found!"); diff --git a/Multiplayer/Components/MainMenu/MainMenuThingsAndStuff.cs b/Multiplayer/Components/MainMenu/MainMenuThingsAndStuff.cs index 05b3487d..c7ea5be6 100644 --- a/Multiplayer/Components/MainMenu/MainMenuThingsAndStuff.cs +++ b/Multiplayer/Components/MainMenu/MainMenuThingsAndStuff.cs @@ -1,4 +1,5 @@ using System; +using DV.UI; using DV.UIFramework; using DV.Utils; using JetBrains.Annotations; @@ -9,14 +10,19 @@ namespace Multiplayer.Components.MainMenu public class MainMenuThingsAndStuff : SingletonBehaviour { public PopupManager popupManager; - public Popup renamePopupPrefab; - public Popup okPopupPrefab; + //public Popup renamePopupPrefab; + //public Popup okPopupPrefab; + //public Popup yesNoPopupPrefab; public UIMenuController uiMenuController; + public PopupNotificationReferences references; protected override void Awake() { bool shouldDestroy = false; + popupManager = GameObject.FindObjectOfType(); + references = GameObject.FindObjectOfType(); + // Check if PopupManager is assigned if (popupManager == null) { @@ -24,19 +30,19 @@ protected override void Awake() shouldDestroy = true; } - // Check if renamePopupPrefab is assigned - if (renamePopupPrefab == null) - { - Multiplayer.LogError($"{nameof(renamePopupPrefab)} is null! 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 okPopupPrefab is assigned + //if (okPopupPrefab == null) + //{ + // Multiplayer.LogError($"{nameof(okPopupPrefab)} is null! Destroying self."); + // shouldDestroy = true; + //} // Check if uiMenuController is assigned if (uiMenuController == null) @@ -72,14 +78,35 @@ public void SwitchToMenu(byte index) public Popup ShowRenamePopup() { Multiplayer.Log("public Popup ShowRenamePopup() ..."); - return ShowPopup(renamePopupPrefab); + return ShowPopup(references.popupTextInput); } // Show the OK popup if possible [CanBeNull] public Popup ShowOkPopup() { - return ShowPopup(okPopupPrefab); + return ShowPopup(references.popupOk); + } + + // Show the Yes No popup if possible + [CanBeNull] + public Popup ShowYesNoPopup() + { + return ShowPopup(references.popupYesNo); + } + + // Show the Wait Spinner popup if possible + [CanBeNull] + public Popup ShowSpinnerPopup() + { + return ShowPopup(references.popupWaitSpinner); + } + + // Show the Slider popup if possible + [CanBeNull] + public Popup ShowSliderPopup() + { + return ShowPopup(references.popupSlider); } // Generic method to show a popup if the PopupManager can show it diff --git a/Multiplayer/Patches/MainMenu/MainMenuControllerPatch.cs b/Multiplayer/Patches/MainMenu/MainMenuControllerPatch.cs index 3ece9833..a26efe28 100644 --- a/Multiplayer/Patches/MainMenu/MainMenuControllerPatch.cs +++ b/Multiplayer/Patches/MainMenu/MainMenuControllerPatch.cs @@ -50,17 +50,6 @@ 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(); - // 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 d23a2e2e..835efa19 100644 --- a/Multiplayer/Patches/MainMenu/RightPaneControllerPatch.cs +++ b/Multiplayer/Patches/MainMenu/RightPaneControllerPatch.cs @@ -49,7 +49,7 @@ private static void OnEnablePre(RightPaneController __instance) __instance.menuController.controlledMenus.Add(multiplayerPane.GetComponent()); joinMenuIndex = __instance.menuController.controlledMenus.Count - 1; UIMenuRequester mpButtonReq = MainMenuController_Awake_Patch.multiplayerButton.GetComponent(); - mpButtonReq.requestedMenuIndex =joinMenuIndex; + mpButtonReq.requestedMenuIndex = joinMenuIndex; // Clean up unnecessary components and child objects GameObject.Destroy(multiplayerPane.GetComponent()); @@ -59,11 +59,13 @@ private static void OnEnablePre(RightPaneController __instance) // 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.okPopupPrefab = __instance.continueLoadNewController.career.okPopupPrefab;*/ manager.uiMenuController = __instance.menuController; }); @@ -103,5 +105,7 @@ private static void OnEnablePost(RightPaneController __instance) { if (Environment.GetCommandLineArgs().Contains("+connect_lobby")) SteamworksUtils.JoinFromCommandLine(); + + SteamMatchmaking.OnLobbyInvite += SteamworksUtils.OnLobbyInviteRequest; } } diff --git a/Multiplayer/Patches/Util/FacepunchNetAddressPatch.cs b/Multiplayer/Patches/Util/FacepunchNetAddressPatch.cs new file mode 100644 index 00000000..afc0c749 --- /dev/null +++ b/Multiplayer/Patches/Util/FacepunchNetAddressPatch.cs @@ -0,0 +1,26 @@ +using HarmonyLib; +using Steamworks.Data; +using System.Net; +using System.Net.Sockets; + +namespace Multiplayer.Patches.Util; + +[HarmonyPatch(typeof(NetAddress))] +public static class FacepunchNetAddressPatch +{ + [HarmonyPatch(nameof(NetAddress.From), new[] { typeof(IPAddress), typeof(ushort) })] + [HarmonyPrefix] + private static bool From(IPAddress address, ushort port, ref NetAddress __result) + { + if (address != null && address.AddressFamily == AddressFamily.InterNetworkV6) + { + Multiplayer.LogDebug(() => $"FacepunchNetAddressPatch.From() IPv6"); + NetAddress cleared = NetAddress.Cleared; + var ipv6Bytes = address.GetAddressBytes(); + NetAddress.InternalSetIPv6(ref cleared, ref ipv6Bytes[0], port); + __result = cleared; + return false; + } + return true; + } +} From 7c6d0a03591b463bfc2fe33209b59f3c328f0c6d Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 25 Jan 2025 22:13:04 +1000 Subject: [PATCH 211/521] Add direct connection and menu invites --- .../Components/MainMenu/ServerBrowserPane.cs | 177 +++++++++--------- Multiplayer/Utils/SteamWorksUtils.cs | 43 +++++ 2 files changed, 132 insertions(+), 88 deletions(-) diff --git a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs index 21bd3585..f3ab768e 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs @@ -97,32 +97,6 @@ public void Awake() SetupServerBrowser(); RefreshGridView(); - - //For invites - Multiplayer.Log($"Invite from command line: {lobbyToJoin != null}"); - if (lobbyToJoin != null) - { - direct = false; - selectedLobby = lobbyToJoin; - - foreach(var item in lobbyToJoin?.Data) - { - Multiplayer.Log($"Invite from command line ({lobbyToJoin?.Id}) Data: {item.Key}, {item.Value}"); - } - string hasPass = lobbyToJoin?.GetData(SteamworksUtils.LOBBY_HAS_PASSWORD); - Multiplayer.Log($"Invite from command line ({lobbyToJoin?.Id}) hasPass: {hasPass}"); - - if (string.IsNullOrEmpty(hasPass)) - { - Multiplayer.Log($"Invite from command line ({lobbyToJoin?.Id}) Attempting..."); - AttemptConnection(); - } - else - { - Multiplayer.Log($"Invite from command line ({lobbyToJoin?.Id}) Ask Password..."); - ShowPasswordPopup(); - } - } } public void OnEnable() @@ -184,6 +158,32 @@ public void Update() UpdatePings(); pingTimer = 0f; } + + if (lobbyToJoin != null && lobbyToJoin?.Data?.Count() > 0) + { + //For invites + Multiplayer.Log($"Player Invite initiated"); + if (lobbyToJoin != null) + { + direct = false; + selectedLobby = lobbyToJoin; + lobbyToJoin = null; + + string hasPass = selectedLobby?.GetData(SteamworksUtils.LOBBY_HAS_PASSWORD); + Multiplayer.Log($"Player Invite ({selectedLobby?.Id}) Has Password: {hasPass}"); + + if (string.IsNullOrEmpty(hasPass)) + { + Multiplayer.Log($"Player Invite ({selectedLobby?.Id}) Attempting connection..."); + AttemptConnection(); + } + else + { + Multiplayer.Log($"Player Invite ({selectedLobby?.Id}) Ask Password..."); + ShowPasswordPopup(); + } + } + } } private void CleanUI() @@ -741,45 +741,46 @@ private async void AttemptConnection() } } - //Multiplayer.Log($"AttemptConnection address: {address}"); + Multiplayer.Log($"AttemptConnection address: {address}"); - //if (IPAddress.TryParse(address, out IPAddress IPaddress)) - //{ - // Multiplayer.Log($"AttemptConnection tryParse: {IPaddress.AddressFamily}"); + 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(); - // } + if (IPaddress.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork) + { + AttemptIPv4(); + } + else if (IPaddress.AddressFamily == System.Net.Sockets.AddressFamily.InterNetworkV6) + { + AttemptIPv6(); + } - // return; - //} + return; + } - //Multiplayer.LogError($"IP address invalid: {address}"); + Multiplayer.LogError($"IP address invalid: {address}"); - //AttemptFail(); + AttemptFail(); } - //private void AttemptIPv6() - //{ - // Multiplayer.Log($"AttemptIPv6() {address}"); + private void AttemptIPv6() + { + Multiplayer.Log($"AttemptIPv6() {address}"); - // if (connectionState == ConnectionState.Aborted) - // return; + if (connectionState == ConnectionState.Aborted) + return; - // attempt++; - // if (connectingPopup != null) - // connectingPopup.labelTMPro.text = $"Connecting, please wait...\r\nAttempt: {attempt}"; + attempt++; + if (connectingPopup != null) + connectingPopup.labelTMPro.text = $"Connecting, please wait...\r\nAttempt: {attempt}"; - // Multiplayer.Log($"AttemptIPv6() starting attempt"); - // connectionState = ConnectionState.AttemptingIPv6; - // SingletonBehaviour.Instance.StartClient(address, portNumber, password, false, OnDisconnect); + Multiplayer.Log($"AttemptIPv6() starting attempt"); + connectionState = ConnectionState.AttemptingIPv6; + SingletonBehaviour.Instance.StartClient(address, portNumber, password, false, OnDisconnect); + + } - //} //private void AttemptIPv6Punch() //{ // Multiplayer.Log($"AttemptIPv6Punch() {address}"); @@ -796,47 +797,47 @@ private async void AttemptConnection() // SingletonBehaviour.Instance.StartClient(address, portNumber, password, false, OnDisconnect); //} - //private void AttemptIPv4() - //{ - // Multiplayer.Log($"AttemptIPv4() {address}, {connectionState}"); + private void AttemptIPv4() + { + Multiplayer.Log($"AttemptIPv4() {address}, {connectionState}"); - // if (connectionState == ConnectionState.Aborted) - // return; + if (connectionState == ConnectionState.Aborted) + return; - // attempt++; - // if (connectingPopup != null) - // connectingPopup.labelTMPro.text = $"Connecting, please wait...\r\nAttempt: {attempt}"; + 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; - // } + if (!direct) + { + if (selectedServer.ipv4 == null || selectedServer.ipv4 == string.Empty) + { + AttemptFail(); + return; + } - // address = selectedServer.ipv4; - // } + address = selectedServer.ipv4; + } - // Multiplayer.Log($"AttemptIPv4() {address}"); + Multiplayer.Log($"AttemptIPv4() {address}"); - // 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, portNumber, password, false, OnDisconnect); - // return; - // } - // } + 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, portNumber, password, false, OnDisconnect); + return; + } + } - // Multiplayer.Log($"AttemptIPv4() TryParse failed"); - // AttemptFail(); - // string message = "Host Unreachable"; - // MainMenuThingsAndStuff.Instance.ShowOkPopup(message, () => { }); - //} + Multiplayer.Log($"AttemptIPv4() TryParse failed"); + AttemptFail(); + string message = "Host Unreachable"; + MainMenuThingsAndStuff.Instance.ShowOkPopup(message, () => { }); + } //private void AttemptIPv4Punch() //{ diff --git a/Multiplayer/Utils/SteamWorksUtils.cs b/Multiplayer/Utils/SteamWorksUtils.cs index d4de9061..cda8e462 100644 --- a/Multiplayer/Utils/SteamWorksUtils.cs +++ b/Multiplayer/Utils/SteamWorksUtils.cs @@ -1,3 +1,4 @@ +using DV.Localization; using DV.UIFramework; using Multiplayer.Components.MainMenu; using Multiplayer.Components.Networking; @@ -126,4 +127,46 @@ private static void OnLobbyDataChanged(Lobby lobby) }); } + public static void OnLobbyInviteRequest(Friend friend, Lobby lobby) + { + Multiplayer.Log($"Received lobby invite: {lobby.Id}"); + + if (NetworkLifecycle.Instance.IsServerRunning || NetworkLifecycle.Instance.IsClientRunning) + return; + + NetworkLifecycle.Instance.QueueMainMenuEvent(() => + { + var popup = MainMenuThingsAndStuff.Instance.ShowYesNoPopup(); + + if (popup == null) + { + Multiplayer.LogError("OnLobbyInviteRequest() Popup not found."); + return; + } + + popup.labelTMPro.text = $"{friend.Name} invited you to play!\r\nDo you wish to join?"; + + Localize locPos = popup.positiveButton.GetComponentInChildren(); + locPos.key = "yes"; + locPos.UpdateLocalization(); + + Localize locNeg = popup.negativeButton.GetComponentInChildren(); + locNeg.key = "no"; + locNeg.UpdateLocalization(); + + popup.Closed += (PopupResult result) => + { + Multiplayer.LogDebug(()=>$"OnLobbyInviteRequest() Popup closed by {result.closedBy}"); + if (result.closedBy == PopupClosedByAction.Positive) + { + MainMenuThingsAndStuff.Instance.SwitchToMenu((byte)RightPaneController_Patch.joinMenuIndex); + ServerBrowserPane.lobbyToJoin = lobby; + } + }; + + }); + + NetworkLifecycle.Instance.TriggerMainMenuEventLater(); + } + } From ed546833f1b224dbee0eefd00749f5976e556ae2 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 25 Jan 2025 22:27:26 +1000 Subject: [PATCH 212/521] Publicise NetAddress --- Multiplayer/Multiplayer.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/Multiplayer/Multiplayer.csproj b/Multiplayer/Multiplayer.csproj index 3eb80fdb..537fb85c 100644 --- a/Multiplayer/Multiplayer.csproj +++ b/Multiplayer/Multiplayer.csproj @@ -16,6 +16,7 @@ + From 202d01239c595a3b91e3634d8152118e1d3f1cf9 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 26 Jan 2025 14:35:36 +1000 Subject: [PATCH 213/521] Enable pings for all mod versions --- .../MainMenu/ServerBrowser/ServerBrowserElement.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserElement.cs b/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserElement.cs index 1f0cb5fa..f925ec6c 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserElement.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserElement.cs @@ -100,10 +100,10 @@ public void UpdateView() networkName.text = data.Name; playerCount.text = $"{data.CurrentPlayers} / {data.MaxPlayers}"; - if (data.MultiplayerVersion == Multiplayer.Ver) + //if (data.MultiplayerVersion == Multiplayer.Ver) ping.text = $"{(data.Ping < 0 ? "?" : data.Ping)} ms"; - else - ping.text = $"N/A"; + //else + // ping.text = $"N/A"; // Hide the icon if the server does not have a password goIconPassword.SetActive(data.HasPassword); From 09fe0a8bc7535516ac8c9319e443d2845a818006 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 26 Jan 2025 14:48:52 +1000 Subject: [PATCH 214/521] Join requests not working if on the Server Browser --- Multiplayer/Components/MainMenu/MainMenuThingsAndStuff.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Multiplayer/Components/MainMenu/MainMenuThingsAndStuff.cs b/Multiplayer/Components/MainMenu/MainMenuThingsAndStuff.cs index c7ea5be6..2dea38f7 100644 --- a/Multiplayer/Components/MainMenu/MainMenuThingsAndStuff.cs +++ b/Multiplayer/Components/MainMenu/MainMenuThingsAndStuff.cs @@ -70,6 +70,9 @@ public void SwitchToDefaultMenu() // Switch to a specific menu by index public void SwitchToMenu(byte index) { + if (uiMenuController.ActiveIndex == index) + return; + uiMenuController.SwitchMenu(index); } From 012b1c189315ecaff1c6c0aa9e984545c367393f Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 26 Jan 2025 17:51:23 +1000 Subject: [PATCH 215/521] Allow request to join from Steam Friends --- .../MainMenu/RightPaneControllerPatch.cs | 6 ++-- Multiplayer/Utils/SteamWorksUtils.cs | 28 ++++++++++++++++--- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/Multiplayer/Patches/MainMenu/RightPaneControllerPatch.cs b/Multiplayer/Patches/MainMenu/RightPaneControllerPatch.cs index 835efa19..1b06e93c 100644 --- a/Multiplayer/Patches/MainMenu/RightPaneControllerPatch.cs +++ b/Multiplayer/Patches/MainMenu/RightPaneControllerPatch.cs @@ -103,9 +103,11 @@ private static void OnEnablePre(RightPaneController __instance) [HarmonyPostfix] private static void OnEnablePost(RightPaneController __instance) { + SteamMatchmaking.OnLobbyDataChanged += SteamworksUtils.OnLobbyDataChanged; + SteamMatchmaking.OnLobbyInvite += SteamworksUtils.OnLobbyInviteRequest; + SteamFriends.OnGameLobbyJoinRequested += SteamworksUtils.OnLobbyJoinRequest; + if (Environment.GetCommandLineArgs().Contains("+connect_lobby")) SteamworksUtils.JoinFromCommandLine(); - - SteamMatchmaking.OnLobbyInvite += SteamworksUtils.OnLobbyInviteRequest; } } diff --git a/Multiplayer/Utils/SteamWorksUtils.cs b/Multiplayer/Utils/SteamWorksUtils.cs index cda8e462..08481488 100644 --- a/Multiplayer/Utils/SteamWorksUtils.cs +++ b/Multiplayer/Utils/SteamWorksUtils.cs @@ -97,8 +97,6 @@ public static void JoinFromCommandLine() return; hasJoinedCL = true; - SteamMatchmaking.OnLobbyDataChanged += OnLobbyDataChanged; - var id = GetLobbyIdFromArgs(); var sId = new SteamId { @@ -109,9 +107,8 @@ public static void JoinFromCommandLine() var ret = lobby.Refresh(); } - private static void OnLobbyDataChanged(Lobby lobby) + public static void OnLobbyDataChanged(Lobby lobby) { - SteamMatchmaking.OnLobbyDataChanged -= OnLobbyDataChanged; NetworkLifecycle.Instance.QueueMainMenuEvent(() => { @@ -127,6 +124,29 @@ private static void OnLobbyDataChanged(Lobby lobby) }); } + public static void OnLobbyJoinRequest(Lobby lobby, SteamId id) + { + Multiplayer.Log($"Received lobby join request: {lobby.Id}, {id.Value}"); + + if (NetworkLifecycle.Instance.IsServerRunning || NetworkLifecycle.Instance.IsClientRunning) + return; + + NetworkLifecycle.Instance.QueueMainMenuEvent(() => + { + Multiplayer.Log($"OnLobbyDataChanged({lobby.Id}) count: {lobby.Data?.Count()}"); + + foreach (var item in lobby.Data) + { + Multiplayer.Log($"OnEnablePost Data: {item.Key}, Value: {item.Value}"); + } + + ServerBrowserPane.lobbyToJoin = lobby; + MainMenuThingsAndStuff.Instance.SwitchToMenu((byte)RightPaneController_Patch.joinMenuIndex); + }); + + NetworkLifecycle.Instance.TriggerMainMenuEventLater(); + } + public static void OnLobbyInviteRequest(Friend friend, Lobby lobby) { Multiplayer.Log($"Received lobby invite: {lobby.Id}"); From c6fa7ed85e37dbb15f1422d701bdac3961004d5f Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 26 Jan 2025 18:36:44 +1000 Subject: [PATCH 216/521] Initial PitStop commit --- .../World/NetworkedPitStopStation.cs | 172 ++++++++++++++++++ .../Managers/Server/NetworkServer.cs | 6 +- .../ClientboundPitStopStationLookupPacket.cs | 30 +++ ...lientboundStationControllerLookupPacket.cs | 8 +- .../Common/CommonPitStopInteractionPacket.cs | 10 + 5 files changed, 221 insertions(+), 5 deletions(-) create mode 100644 Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs create mode 100644 Multiplayer/Networking/Packets/Clientbound/World/ClientboundPitStopStationLookupPacket.cs create mode 100644 Multiplayer/Networking/Packets/Common/CommonPitStopInteractionPacket.cs diff --git a/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs b/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs new file mode 100644 index 00000000..47051c21 --- /dev/null +++ b/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs @@ -0,0 +1,172 @@ +using DV; +using DV.Interaction; +using Multiplayer.Utils; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Text; +using UnityEngine; + + +namespace Multiplayer.Components.Networking.World; + +public class NetworkedPitStopStation : IdMonoBehaviour +{ + #region Lookup Cache + private static readonly Dictionary netPitStopStationToLocation = []; + public static bool Get(ushort netId, out NetworkedPitStopStation obj) + { + bool b = Get(netId, out IdMonoBehaviour rawObj); + obj = (NetworkedPitStopStation)rawObj; + return b; + } + + public static bool GetFromVector(Vector3 position, out NetworkedPitStopStation networkedPitStopStation) + { + return netPitStopStationToLocation.TryGetValue(position, out networkedPitStopStation); + } + + public static Dictionary GetAllPitStopStations() + { + if (netPitStopStationToLocation.Count == 0) + InitialisePitStops(); + + Dictionary result = []; + + foreach (var kvp in netPitStopStationToLocation) + result.Add(kvp.Value.NetId, kvp.Key); + + return new Dictionary(result); + } + + public static void InitialisePitStops() + { + if (netPitStopStationToLocation.Count != 0) + return; + + var stations = Resources.FindObjectsOfTypeAll(); + + Multiplayer.LogDebug(() => $"InitialisePitStops() Found: {stations?.Length}"); + + foreach (var station in stations) + { + Multiplayer.LogDebug(() => $"InitialisePitStops() Station: {station?.transform?.parent?.parent?.name}"); + + var netStation = station.GetOrAddComponent(); + netStation.Station = station; + netStation.Init(); + + Multiplayer.LogDebug(() => $"InitialisePitStops() Parent: {station?.transform?.parent?.name}, parent-parent: {station?.transform?.parent?.parent?.name}, position global: {station?.transform?.position - WorldMover.currentMove}"); + netPitStopStationToLocation[station.transform.position - WorldMover.currentMove] = netStation; + + } + } + #endregion + + protected override bool IsIdServerAuthoritative => true; + + public PitStopStation Station { get; set; } + public string StationName { get; private set; } + + private GrabHandlerHingeJoint carSelectorGrab; + private Dictionary grabberLookup = []; + + + protected override void Awake() + { + base.Awake(); + + StationName = $"{transform.parent.parent.name} - {transform.parent.name}"; + } + + protected override void OnDestroy() + { + netPitStopStationToLocation.Remove(transform.position); + + if (carSelectorGrab != null) + { + carSelectorGrab.Grabbed -= CarSelectorGrabbed; + carSelectorGrab.UnGrabbed -= CarSelectorUnGrabbed; + } + + foreach (var kvp in grabberLookup) + { + var grab = kvp.Key; + var (_, grabbedHandler, ungrabbedHandler) = kvp.Value; + grab.Grabbed -= grabbedHandler; + grab.UnGrabbed -= ungrabbedHandler; + } + + grabberLookup.Clear(); + base.OnDestroy(); + } + + public void Init() + { + var resourceModules = Station?.locoResourceModules?.resourceModules; + + var carSelectorGrab = GetComponentInChildren(); + if (carSelectorGrab != null) + { + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.Init() Grab Handler found: {carSelectorGrab != null}, Name: {carSelectorGrab.name}"); + carSelectorGrab.Grabbed += CarSelectorGrabbed; + carSelectorGrab.UnGrabbed += CarSelectorUnGrabbed; + } + + StringBuilder sb = new(); + sb.AppendLine($"NetworkedPitStopStation.Awake() {StationName} resources:"); + + if (resourceModules != null) + { + foreach (var resourceModule in resourceModules) + { + var grabHandlers = resourceModule.GetComponentsInChildren(); + foreach (var grab in grabHandlers) + { + if (grab != null) + { + //Delegates for handlers + void GrabbedHandler() => LeverGrabbed(resourceModule); + void UnGrabbedHandler() => LeverUnGrabbed(resourceModule); + + //Subscribe + grab.Grabbed += GrabbedHandler; + grab.UnGrabbed += UnGrabbedHandler; + + //Store delegates + grabberLookup[grab] = (resourceModule, GrabbedHandler, UnGrabbedHandler); + + sb.AppendLine($"\t{resourceModule.resourceType}, Grab Handler found: {grab != null}, Name: {grab.name}"); + } + } + } + } + else + { + sb.AppendLine($"ERROR Station is Null {Station == null}, resource modules: {Station?.locoResourceModules}"); + } + + Multiplayer.LogDebug(() => sb.ToString()); + } + + private void CarSelectorGrabbed() + { + Multiplayer.LogDebug(() => $"CarSelectorGrabbed() {StationName}"); + } + + private void CarSelectorUnGrabbed() + { + Multiplayer.LogDebug(() => $"CarSelectorUnGrabbed() {StationName}"); + } + + private void LeverGrabbed(LocoResourceModule module) + { + Multiplayer.LogDebug(() => $"LeverGrabbed() {StationName}, module: {module.resourceType}"); + } + + private void LeverUnGrabbed(LocoResourceModule module) + { + Multiplayer.LogDebug(() => $"LeverUnGrabbed() {StationName}, module: {module.resourceType}"); + } +} diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index 93f855ee..e157e031 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -167,6 +167,9 @@ private void OnLoaded() Log($"Server loaded, processing {joinQueue.Count} queued players"); IsLoaded = true; + //We should initialise object here for dedicated servers, rather than relying on the existance of a client + NetworkedPitStopStation.InitialisePitStops(); //trigger cache build + while (joinQueue.Count > 0) { ITransportPeer peer = joinQueue.Dequeue(); @@ -712,7 +715,8 @@ private void OnServerboundClientReadyPacket(ServerboundClientReadyPacket packet, } // 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); + SendPacket(peer, new ClientboundStationControllerLookupPacket(NetworkedStationController.GetAll().ToArray()), DeliveryMethod.ReliableOrdered); + SendPacket(peer, new ClientboundPitStopStationLookupPacket(NetworkedPitStopStation.GetAllPitStopStations().ToArray()), DeliveryMethod.ReliableOrdered); //send jobs foreach (StationController station in StationController.allStations) diff --git a/Multiplayer/Networking/Packets/Clientbound/World/ClientboundPitStopStationLookupPacket.cs b/Multiplayer/Networking/Packets/Clientbound/World/ClientboundPitStopStationLookupPacket.cs new file mode 100644 index 00000000..6bef6e87 --- /dev/null +++ b/Multiplayer/Networking/Packets/Clientbound/World/ClientboundPitStopStationLookupPacket.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace Multiplayer.Networking.Packets.Clientbound.World; + +public class ClientboundPitStopStationLookupPacket +{ + public ushort[] NetIds { get; set; } + public Vector3[] Locations { get; set; } + + + public ClientboundPitStopStationLookupPacket() { } + + public ClientboundPitStopStationLookupPacket(KeyValuePair[] NetIDtoLocation) + { + if (NetIDtoLocation == null) + throw new ArgumentNullException(nameof(NetIDtoLocation)); + + NetIds = new ushort[NetIDtoLocation.Length]; + Locations = new Vector3[NetIDtoLocation.Length]; + + for (int i = 0; i < NetIDtoLocation.Length; i++) + { + NetIds[i] = NetIDtoLocation[i].Key; + Locations[i] = NetIDtoLocation[i].Value; + } + } + +} diff --git a/Multiplayer/Networking/Packets/Clientbound/World/ClientboundStationControllerLookupPacket.cs b/Multiplayer/Networking/Packets/Clientbound/World/ClientboundStationControllerLookupPacket.cs index e1596349..786fdc2e 100644 --- a/Multiplayer/Networking/Packets/Clientbound/World/ClientboundStationControllerLookupPacket.cs +++ b/Multiplayer/Networking/Packets/Clientbound/World/ClientboundStationControllerLookupPacket.cs @@ -3,14 +3,14 @@ namespace Multiplayer.Networking.Packets.Clientbound.World; -public class ClientBoundStationControllerLookupPacket +public class ClientboundStationControllerLookupPacket { public ushort[] NetID { get; set; } public string[] StationID { get; set; } - public ClientBoundStationControllerLookupPacket() { } + public ClientboundStationControllerLookupPacket() { } - public ClientBoundStationControllerLookupPacket(ushort[] netID, string[] stationID) + public ClientboundStationControllerLookupPacket(ushort[] netID, string[] stationID) { if (netID == null) throw new ArgumentNullException(nameof(netID)); if (stationID == null) throw new ArgumentNullException(nameof(stationID)); @@ -20,7 +20,7 @@ public ClientBoundStationControllerLookupPacket(ushort[] netID, string[] station StationID = stationID; } - public ClientBoundStationControllerLookupPacket(KeyValuePair[] NetIDtoStationID) + public ClientboundStationControllerLookupPacket(KeyValuePair[] NetIDtoStationID) { if (NetIDtoStationID == null) throw new ArgumentNullException(nameof(NetIDtoStationID)); diff --git a/Multiplayer/Networking/Packets/Common/CommonPitStopInteractionPacket.cs b/Multiplayer/Networking/Packets/Common/CommonPitStopInteractionPacket.cs new file mode 100644 index 00000000..908ee7f4 --- /dev/null +++ b/Multiplayer/Networking/Packets/Common/CommonPitStopInteractionPacket.cs @@ -0,0 +1,10 @@ +using DV.ThingTypes; + +namespace Multiplayer.Networking.Packets.Common; + +public class CommonPitStopInteractionPacket +{ + public ushort NetId { get; set; } + public ResourceType? ResourceType { get; set; } + public float State { get; set; } +} From 839125a285d27d9b2aa87b5bdc644d152dd83681 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 26 Jan 2025 14:35:36 +1000 Subject: [PATCH 217/521] Enable pings for all mod versions --- .../MainMenu/ServerBrowser/ServerBrowserElement.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserElement.cs b/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserElement.cs index 1f0cb5fa..f925ec6c 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserElement.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserElement.cs @@ -100,10 +100,10 @@ public void UpdateView() networkName.text = data.Name; playerCount.text = $"{data.CurrentPlayers} / {data.MaxPlayers}"; - if (data.MultiplayerVersion == Multiplayer.Ver) + //if (data.MultiplayerVersion == Multiplayer.Ver) ping.text = $"{(data.Ping < 0 ? "?" : data.Ping)} ms"; - else - ping.text = $"N/A"; + //else + // ping.text = $"N/A"; // Hide the icon if the server does not have a password goIconPassword.SetActive(data.HasPassword); From 4db640dc21c44d0cba4c28f8c210fdf1a2d34613 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 26 Jan 2025 14:48:52 +1000 Subject: [PATCH 218/521] Join requests not working if on the Server Browser --- Multiplayer/Components/MainMenu/MainMenuThingsAndStuff.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Multiplayer/Components/MainMenu/MainMenuThingsAndStuff.cs b/Multiplayer/Components/MainMenu/MainMenuThingsAndStuff.cs index c7ea5be6..2dea38f7 100644 --- a/Multiplayer/Components/MainMenu/MainMenuThingsAndStuff.cs +++ b/Multiplayer/Components/MainMenu/MainMenuThingsAndStuff.cs @@ -70,6 +70,9 @@ public void SwitchToDefaultMenu() // Switch to a specific menu by index public void SwitchToMenu(byte index) { + if (uiMenuController.ActiveIndex == index) + return; + uiMenuController.SwitchMenu(index); } From b279fe5e665c46aa45e3bb6ef2c7d5e299564d62 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 26 Jan 2025 17:51:23 +1000 Subject: [PATCH 219/521] Allow request to join from Steam Friends --- .../MainMenu/RightPaneControllerPatch.cs | 6 ++-- Multiplayer/Utils/SteamWorksUtils.cs | 28 ++++++++++++++++--- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/Multiplayer/Patches/MainMenu/RightPaneControllerPatch.cs b/Multiplayer/Patches/MainMenu/RightPaneControllerPatch.cs index 835efa19..1b06e93c 100644 --- a/Multiplayer/Patches/MainMenu/RightPaneControllerPatch.cs +++ b/Multiplayer/Patches/MainMenu/RightPaneControllerPatch.cs @@ -103,9 +103,11 @@ private static void OnEnablePre(RightPaneController __instance) [HarmonyPostfix] private static void OnEnablePost(RightPaneController __instance) { + SteamMatchmaking.OnLobbyDataChanged += SteamworksUtils.OnLobbyDataChanged; + SteamMatchmaking.OnLobbyInvite += SteamworksUtils.OnLobbyInviteRequest; + SteamFriends.OnGameLobbyJoinRequested += SteamworksUtils.OnLobbyJoinRequest; + if (Environment.GetCommandLineArgs().Contains("+connect_lobby")) SteamworksUtils.JoinFromCommandLine(); - - SteamMatchmaking.OnLobbyInvite += SteamworksUtils.OnLobbyInviteRequest; } } diff --git a/Multiplayer/Utils/SteamWorksUtils.cs b/Multiplayer/Utils/SteamWorksUtils.cs index cda8e462..08481488 100644 --- a/Multiplayer/Utils/SteamWorksUtils.cs +++ b/Multiplayer/Utils/SteamWorksUtils.cs @@ -97,8 +97,6 @@ public static void JoinFromCommandLine() return; hasJoinedCL = true; - SteamMatchmaking.OnLobbyDataChanged += OnLobbyDataChanged; - var id = GetLobbyIdFromArgs(); var sId = new SteamId { @@ -109,9 +107,8 @@ public static void JoinFromCommandLine() var ret = lobby.Refresh(); } - private static void OnLobbyDataChanged(Lobby lobby) + public static void OnLobbyDataChanged(Lobby lobby) { - SteamMatchmaking.OnLobbyDataChanged -= OnLobbyDataChanged; NetworkLifecycle.Instance.QueueMainMenuEvent(() => { @@ -127,6 +124,29 @@ private static void OnLobbyDataChanged(Lobby lobby) }); } + public static void OnLobbyJoinRequest(Lobby lobby, SteamId id) + { + Multiplayer.Log($"Received lobby join request: {lobby.Id}, {id.Value}"); + + if (NetworkLifecycle.Instance.IsServerRunning || NetworkLifecycle.Instance.IsClientRunning) + return; + + NetworkLifecycle.Instance.QueueMainMenuEvent(() => + { + Multiplayer.Log($"OnLobbyDataChanged({lobby.Id}) count: {lobby.Data?.Count()}"); + + foreach (var item in lobby.Data) + { + Multiplayer.Log($"OnEnablePost Data: {item.Key}, Value: {item.Value}"); + } + + ServerBrowserPane.lobbyToJoin = lobby; + MainMenuThingsAndStuff.Instance.SwitchToMenu((byte)RightPaneController_Patch.joinMenuIndex); + }); + + NetworkLifecycle.Instance.TriggerMainMenuEventLater(); + } + public static void OnLobbyInviteRequest(Friend friend, Lobby lobby) { Multiplayer.Log($"Received lobby invite: {lobby.Id}"); From 0e65d0b9d59d1e0947cf2a84cfb44727fb32a243 Mon Sep 17 00:00:00 2001 From: AMacro Date: Mon, 27 Jan 2025 10:27:36 +1000 Subject: [PATCH 220/521] Make non-public games private More work required to allow all modes to be selected --- Multiplayer/Networking/Managers/Server/LobbyServerManager.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs b/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs index 966cc0f5..051fde72 100644 --- a/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs +++ b/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs @@ -172,6 +172,8 @@ public async void CreateSteamLobby() //todo implement public/private/friends if (server.serverData.isPublic) lobby?.SetPublic(); + else + lobby?.SetPrivate(); lobby?.SetJoinable(true); } From d1e0e145954bd29c61e02a4005bb2e558da00e86 Mon Sep 17 00:00:00 2001 From: AMacro Date: Mon, 27 Jan 2025 20:07:58 +1000 Subject: [PATCH 221/521] Fix issue with TrainCars being in reverse order --- .../Components/Networking/Train/NetworkedCarSpawner.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Multiplayer/Components/Networking/Train/NetworkedCarSpawner.cs b/Multiplayer/Components/Networking/Train/NetworkedCarSpawner.cs index 4625a3db..cceaccae 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedCarSpawner.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedCarSpawner.cs @@ -24,8 +24,9 @@ public static void SpawnCars(TrainsetSpawnPart[] parts, bool autoCouple) SetBrakeParams(parts[i].BrakeData, cars[i].TrainCar); //couple them if marked as coupled - for (int i = 0; i < cars.Length; i++) - Couple(parts[i], cars[i].TrainCar, autoCouple); + //- we need to do this back to front otherwise the TrainSet indicies will be wrong! + for (int i = cars.Length - 1; i >= 0; i--) + Couple(in parts[i], cars[i].TrainCar, autoCouple); //update speed queue data for (int i = 0; i < cars.Length; i++) From 5699b1bc1d3c3b5328523f2cbf3f238e07f8731f Mon Sep 17 00:00:00 2001 From: AMacro Date: Mon, 27 Jan 2025 20:08:33 +1000 Subject: [PATCH 222/521] Add PitStopInteraction types --- .../Data/PitStopStationInteractionType.cs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 Multiplayer/Networking/Data/PitStopStationInteractionType.cs diff --git a/Multiplayer/Networking/Data/PitStopStationInteractionType.cs b/Multiplayer/Networking/Data/PitStopStationInteractionType.cs new file mode 100644 index 00000000..77b7aa5e --- /dev/null +++ b/Multiplayer/Networking/Data/PitStopStationInteractionType.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Multiplayer.Networking.Data; + +[Flags] +public enum PitStopStationInteractionType : byte +{ + Reject, + Grab, + Ungrab, + StateUpdate, + SelectCar, + PayOrder, + CancelOrder, + ProcessOrder, +} From bbfeecf48fc1c2c6ed5b6dfc9ec6b356c147f4ba Mon Sep 17 00:00:00 2001 From: AMacro Date: Mon, 27 Jan 2025 20:10:15 +1000 Subject: [PATCH 223/521] Add server/client mapping for PitStops --- .../Managers/Client/NetworkClient.cs | 67 +++++++++++++++---- .../Managers/Server/NetworkServer.cs | 2 +- 2 files changed, 56 insertions(+), 13 deletions(-) diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 9078f65c..a8969997 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -43,6 +43,7 @@ using DV.Common; using DV.Customization.Paint; using Multiplayer.Networking.TransportLayers; +using System.Collections; namespace Multiplayer.Networking.Managers.Client; @@ -111,8 +112,20 @@ public override void Stop() protected override void Subscribe() { + netPacketProcessor.SubscribeReusable(OnClientboundServerLoadingPacket); netPacketProcessor.SubscribeReusable(OnClientboundLoginResponsePacket); netPacketProcessor.SubscribeReusable(OnClientboundDisconnectPacket); + netPacketProcessor.SubscribeReusable(OnClientboundRemoveLoadingScreen); + + netPacketProcessor.SubscribeReusable(OnClientboundTickSyncPacket); + netPacketProcessor.SubscribeReusable(OnClientboundBeginWorldSyncPacket); + netPacketProcessor.SubscribeReusable(OnClientboundGameParamsPacket); + netPacketProcessor.SubscribeReusable(OnClientboundSaveGameDataPacket); + netPacketProcessor.SubscribeReusable(OnClientboundWeatherPacket); + netPacketProcessor.SubscribeReusable(OnClientboundRailwayStatePacket); + netPacketProcessor.SubscribeReusable(OnClientboundStationControllerLookupPacket); + netPacketProcessor.SubscribeReusable(OnClientboundPitStopStationLookupPacket); + netPacketProcessor.SubscribeReusable(OnClientboundPlayerJoinedPacket); netPacketProcessor.SubscribeReusable(OnClientboundPlayerDisconnectPacket); @@ -120,16 +133,7 @@ protected override void Subscribe() netPacketProcessor.SubscribeReusable(OnClientboundPlayerPositionPacket); netPacketProcessor.SubscribeReusable(OnClientboundPingUpdatePacket); - netPacketProcessor.SubscribeReusable(OnClientboundTickSyncPacket); - netPacketProcessor.SubscribeReusable(OnClientboundServerLoadingPacket); - netPacketProcessor.SubscribeReusable(OnClientboundBeginWorldSyncPacket); - netPacketProcessor.SubscribeReusable(OnClientboundGameParamsPacket); - netPacketProcessor.SubscribeReusable(OnClientboundSaveGameDataPacket); - netPacketProcessor.SubscribeReusable(OnClientboundWeatherPacket); - netPacketProcessor.SubscribeReusable(OnClientboundRemoveLoadingScreen); netPacketProcessor.SubscribeReusable(OnClientboundTimeAdvancePacket); - netPacketProcessor.SubscribeReusable(OnClientboundRailwayStatePacket); - netPacketProcessor.SubscribeReusable(OnClientBoundStationControllerLookupPacket); netPacketProcessor.SubscribeReusable(OnCommonChangeJunctionPacket); netPacketProcessor.SubscribeReusable(OnCommonRotateTurntablePacket); netPacketProcessor.SubscribeReusable(OnClientboundSpawnTrainCarPacket); @@ -165,6 +169,8 @@ protected override void Subscribe() netPacketProcessor.SubscribeReusable(OnClientboundJobValidateResponsePacket); netPacketProcessor.SubscribeReusable(OnCommonChatPacket); netPacketProcessor.SubscribeNetSerializable(OnCommonItemChangePacket); + + netPacketProcessor.SubscribeReusable(OnCommonPitStopInteractionPacket); } #region Net Events @@ -342,6 +348,8 @@ private void OnClientboundSaveGameDataPacket(ClientboundSaveGameDataPacket packe NetworkedItemManager.Instance.CheckInstance(); Log($"WorldStreamingInit.LoadingFinished() CacheWorldItems()"); NetworkedItemManager.Instance.CacheWorldItems(); + Log($"WorldStreamingInit.LoadingFinished() InitialisePitStops()"); + NetworkedPitStopStation.InitialisePitStops(); Log($"WorldStreamingInit.LoadingFinished() SendReadyPacket()"); SendReadyPacket(); }; @@ -402,7 +410,7 @@ private void OnClientboundTimeAdvancePacket(ClientboundTimeAdvancePacket 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) + private void OnClientboundStationControllerLookupPacket(ClientboundStationControllerLookupPacket packet) { if (packet == null) @@ -417,7 +425,6 @@ private void OnClientBoundStationControllerLookupPacket(ClientBoundStationContro return; } - for (int i = 0; i < packet.NetID.Length; i++) { if (!NetworkedStationController.GetFromStationId(packet.StationID[i], out NetworkedStationController netStationCont)) @@ -435,8 +442,44 @@ private void OnClientBoundStationControllerLookupPacket(ClientBoundStationContro } } + //Force pitstops to be mapped to same netId across all clients and server - probably should implement for junctions, etc. + private void OnClientboundPitStopStationLookupPacket(ClientboundPitStopStationLookupPacket packet) + { + LogDebug(() => $"OnClientboundPitStopStationLookupPacket({packet.NetIds?.Length})"); + + if (packet == null) + { + LogError("OnClientboundPitStopStationLookupPacket received null packet"); + return; + } + + if (packet.NetIds == null || packet.Locations == null) + { + LogError($"OnClientboundPitStopStationLookupPacket received packet with null arrays: NetIDs is null: {packet.NetIds == null}, Locations is null: {packet.Locations == null}"); + return; + } + + //Log($"WorldStreamingInit.LoadingFinished() CachePitStopStations()"); + //NetworkedPitStopStation.InitialisePitStops(); + + for (int i = 0; i < packet.NetIds.Length; i++) + { + LogDebug(() => $"OnClientboundPitStopStationLookupPacket[{i}] vector: {packet.Locations[i]}, netId: {packet.NetIds[i]}"); + if (NetworkedPitStopStation.GetFromVector(packet.Locations[i], out NetworkedPitStopStation netStation)) + { + netStation.NetId = packet.NetIds[i]; + if (netStation.Station.pitstop != null) + { + netStation.Station.pitstop.currentCarIndex = packet.SelectedCars[i]; + //netStation.Station.pitstop.OnCarSelectionChanged(); + } + } + else + LogError($"Syncing PitStopStations station with coords: {packet.Locations[i]} not found"); + } + } - private void OnClientboundRailwayStatePacket(ClientboundRailwayStatePacket packet) + private void OnClientboundRailwayStatePacket(ClientboundRailwayStatePacket packet) { for (int i = 0; i < packet.SelectedJunctionBranches.Length; i++) { diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index e157e031..d2aee4ca 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -716,7 +716,7 @@ private void OnServerboundClientReadyPacket(ServerboundClientReadyPacket packet, // 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); - SendPacket(peer, new ClientboundPitStopStationLookupPacket(NetworkedPitStopStation.GetAllPitStopStations().ToArray()), DeliveryMethod.ReliableOrdered); + SendPacket(peer, new ClientboundPitStopStationLookupPacket(NetworkedPitStopStation.GetAllPitStopStations()), DeliveryMethod.ReliableOrdered); //send jobs foreach (StationController station in StationController.allStations) From 25b275f2bd8e424530c629f2f1952ea75f06abf4 Mon Sep 17 00:00:00 2001 From: AMacro Date: Mon, 27 Jan 2025 20:11:06 +1000 Subject: [PATCH 224/521] Add packet handling --- .../World/NetworkedPitStopStation.cs | 168 ++++++++++++++++-- .../Managers/Client/NetworkClient.cs | 26 +++ .../Managers/Server/NetworkServer.cs | 28 +++ .../ClientboundPitStopStationLookupPacket.cs | 19 +- .../Common/CommonPitStopInteractionPacket.cs | 4 +- 5 files changed, 224 insertions(+), 21 deletions(-) diff --git a/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs b/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs index 47051c21..bbf4a583 100644 --- a/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs +++ b/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs @@ -1,12 +1,16 @@ using DV; using DV.Interaction; +using Multiplayer.Networking.Packets.Common; +using Multiplayer.Networking.TransportLayers; +using Multiplayer.Networking.Data; using Multiplayer.Utils; using System; using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; using System.Text; using UnityEngine; +using DV.ThingTypes; +using System.Collections; +using System.Collections.ObjectModel; namespace Multiplayer.Components.Networking.World; @@ -27,17 +31,21 @@ public static bool GetFromVector(Vector3 position, out NetworkedPitStopStation n return netPitStopStationToLocation.TryGetValue(position, out networkedPitStopStation); } - public static Dictionary GetAllPitStopStations() + public static Tuple[] GetAllPitStopStations() { if (netPitStopStationToLocation.Count == 0) InitialisePitStops(); - Dictionary result = []; + List > result = []; + int i = 0; foreach (var kvp in netPitStopStationToLocation) - result.Add(kvp.Value.NetId, kvp.Key); + { + var selection = kvp.Value?.Station?.pitstop?.SelectedIndex ?? 0; + result.Add(new (kvp.Value.NetId, kvp.Key, selection)); + } - return new Dictionary(result); + return result.ToArray(); } public static void InitialisePitStops() @@ -55,7 +63,7 @@ public static void InitialisePitStops() var netStation = station.GetOrAddComponent(); netStation.Station = station; - netStation.Init(); + CoroutineManager.Instance.StartCoroutine(netStation.Init()); Multiplayer.LogDebug(() => $"InitialisePitStops() Parent: {station?.transform?.parent?.name}, parent-parent: {station?.transform?.parent?.parent?.name}, position global: {station?.transform?.position - WorldMover.currentMove}"); netPitStopStationToLocation[station.transform.position - WorldMover.currentMove] = netStation; @@ -69,10 +77,16 @@ public static void InitialisePitStops() public PitStopStation Station { get; set; } public string StationName { get; private set; } - private GrabHandlerHingeJoint carSelectorGrab; - private Dictionary grabberLookup = []; + private readonly GrabHandlerHingeJoint carSelectorGrab; + private readonly Dictionary grabberLookup = []; + private readonly Dictionary grabbedHandlerLookup = []; + private bool isGrabbed = false; + private LocoResourceModule grabbedModule; + private RotaryAmplitudeChecker grabbedAmplitudeChecker; + private float lastUnitsToBuy; + #region Unity protected override void Awake() { base.Awake(); @@ -99,11 +113,48 @@ protected override void OnDestroy() } grabberLookup.Clear(); + grabbedHandlerLookup.Clear(); base.OnDestroy(); } - public void Init() + protected void Update() + { + if (isGrabbed && grabbedModule != null && grabbedAmplitudeChecker != null) + { + if(grabbedModule.Data.unitsToBuy != lastUnitsToBuy) + { + lastUnitsToBuy = grabbedModule.Data.unitsToBuy; + NetworkLifecycle.Instance?.Client.SendPitStopInteractionPacket(NetId, PitStopStationInteractionType.StateUpdate, grabbedModule.resourceType, lastUnitsToBuy); + } + } + } + #endregion + + #region Server + + public bool ValidateInteraction(CommonPitStopInteractionPacket packet) + { + //todo: implement validation code (player distance, player interacting, etc.) + return true; + } + + public void OnPlayerDisconnect(ITransportPeer peer) + { + //todo: when a player disconnects, if they are interacting with a lever, cancel the interaction + //Multiplayer.LogWarning($"OnPlayerDisconnect()"); + } + + #endregion + + + #region Common + public IEnumerator Init() { + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.Init() station: {Station == null}, pitstop: {Station?.pitstop == null}"); + + while (Station?.pitstop == null) + yield return new WaitForEndOfFrame(); + var resourceModules = Station?.locoResourceModules?.resourceModules; var carSelectorGrab = GetComponentInChildren(); @@ -112,6 +163,8 @@ public void Init() Multiplayer.LogDebug(() => $"NetworkedPitStopStation.Init() Grab Handler found: {carSelectorGrab != null}, Name: {carSelectorGrab.name}"); carSelectorGrab.Grabbed += CarSelectorGrabbed; carSelectorGrab.UnGrabbed += CarSelectorUnGrabbed; + + Station.pitstop.CarSelected += CarSelected; } StringBuilder sb = new(); @@ -136,6 +189,7 @@ public void Init() //Store delegates grabberLookup[grab] = (resourceModule, GrabbedHandler, UnGrabbedHandler); + grabbedHandlerLookup[resourceModule.resourceType] = grab; sb.AppendLine($"\t{resourceModule.resourceType}, Grab Handler found: {grab != null}, Name: {grab.name}"); } @@ -150,23 +204,117 @@ public void Init() Multiplayer.LogDebug(() => sb.ToString()); } + public void ProcessPacket(CommonPitStopInteractionPacket packet) + { + PitStopStationInteractionType interactionType = (PitStopStationInteractionType)packet.InteractionType; + ResourceType? resourceType = (ResourceType)packet.ResourceType; + + GrabHandlerHingeJoint grab = null; + LocoResourceModule resourceModule = null; + + Multiplayer.LogDebug(() => $"ProcessPacket() [{StationName}, {NetId}] {interactionType}, resource type: {resourceType}, state: {packet.State}"); + + if (resourceType != null && resourceType != 0) + { + if(!grabbedHandlerLookup.TryGetValue((ResourceType)resourceType, out grab)) + Multiplayer.LogError($"Could not find ResourceType in grabbedHandlerLookup for station {StationName}, resource type: {resourceType}"); + else + if(!grabberLookup.TryGetValue(grab, out var tup)) + Multiplayer.LogError($"Could not find GrabHandler in grabberLookup for station {StationName}, resource type: {resourceType}"); + else + (resourceModule, _, _) = tup; + } + + switch (interactionType) + { + case PitStopStationInteractionType.Reject: + + break; + + case PitStopStationInteractionType.Grab: + //block interaction + if (grab != null) + grab.interactionAllowed = false; + + //set direction + if (resourceType != null && resourceType != 0 && resourceModule != null) + resourceModule.Data.unitsToBuy = (int)packet.State; + + break; + + case PitStopStationInteractionType.Ungrab: + //allow interaction + if (grab != null) + grab.interactionAllowed = true; + + //set direction + if (resourceType != null && resourceType != 0 && resourceModule != null) + resourceModule.Data.unitsToBuy = (int)packet.State; + + break; + + case PitStopStationInteractionType.StateUpdate: + + if (resourceType != null && resourceType != 0 && resourceModule != null) + resourceModule.Data.unitsToBuy = (int)packet.State; + break; + + case PitStopStationInteractionType.SelectCar: + Station.pitstop.currentCarIndex = (int)packet.State; + Station.pitstop.OnCarSelectionChanged(); + + break; + case PitStopStationInteractionType.PayOrder: + break; + case PitStopStationInteractionType.CancelOrder: + break; + case PitStopStationInteractionType.ProcessOrder: + break; + } + } + #endregion + + #region Client + private void CarSelectorGrabbed() { Multiplayer.LogDebug(() => $"CarSelectorGrabbed() {StationName}"); + NetworkLifecycle.Instance?.Client.SendPitStopInteractionPacket(NetId, PitStopStationInteractionType.Grab, null, 0); } private void CarSelectorUnGrabbed() { Multiplayer.LogDebug(() => $"CarSelectorUnGrabbed() {StationName}"); + NetworkLifecycle.Instance?.Client.SendPitStopInteractionPacket(NetId, PitStopStationInteractionType.Ungrab, null, Station.pitstop.SelectedIndex); + } + + private void CarSelected() + { + if (NetworkLifecycle.Instance.IsProcessingPacket) + return; + + Multiplayer.LogDebug(() => $"CarSelected() selected: {Station.pitstop.SelectedIndex}"); + + NetworkLifecycle.Instance?.Client.SendPitStopInteractionPacket(NetId, PitStopStationInteractionType.SelectCar, null, Station.pitstop.SelectedIndex); } private void LeverGrabbed(LocoResourceModule module) { Multiplayer.LogDebug(() => $"LeverGrabbed() {StationName}, module: {module.resourceType}"); + isGrabbed = true; + grabbedModule = module; + grabbedAmplitudeChecker = module.GetComponentInChildren(); + lastUnitsToBuy = module.Data.unitsToBuy; + NetworkLifecycle.Instance?.Client.SendPitStopInteractionPacket(NetId, PitStopStationInteractionType.Grab, module.resourceType, lastUnitsToBuy); } private void LeverUnGrabbed(LocoResourceModule module) { Multiplayer.LogDebug(() => $"LeverUnGrabbed() {StationName}, module: {module.resourceType}"); + isGrabbed = false; + grabbedModule = null; + grabbedAmplitudeChecker = null; + NetworkLifecycle.Instance?.Client.SendPitStopInteractionPacket(NetId, PitStopStationInteractionType.Ungrab, module.resourceType, lastUnitsToBuy); } + #endregion } diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index a8969997..56b25d57 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -956,6 +956,20 @@ private void OnClientboundJobValidateResponsePacket(ClientboundJobValidateRespon Object.Destroy(networkedJob.gameObject); } + private void OnCommonPitStopInteractionPacket(CommonPitStopInteractionPacket packet) + { + if (!NetworkedPitStopStation.Get(packet.NetId, out var netPitStop)) + { + LogWarning($"Pit stop Interaction received for netId: {packet.NetId}, but pit stop does not exist!"); + } + + Log($"Pit stop interaction received for {netPitStop.StationName}"); + + LogDebug(() => $"OnCommonPitStopInteractionPacket() [{netPitStop.StationName}, {packet.NetId}], interaction: [{packet.InteractionType}], resource: {packet?.ResourceType}, State: {packet.State}"); + netPitStop.ProcessPacket(packet); + } + + private void OnCommonItemChangePacket(CommonItemChangePacket packet) { //LogDebug(() => $"OnCommonItemChangePacket({packet?.Items?.Count})"); @@ -1356,6 +1370,18 @@ public void SendChat(string message) }, DeliveryMethod.ReliableUnordered); } + public void SendPitStopInteractionPacket(ushort netId, PitStopStationInteractionType interaction, ResourceType? resource, float state) + { + int res = resource == null ? 0 : (int)resource; + SendPacketToServer(new CommonPitStopInteractionPacket + { + NetId = netId, + InteractionType = (byte)interaction, + ResourceType = res, + State = state + }, DeliveryMethod.ReliableOrdered); + } + public void SendItemsChangePacket(List items) { Multiplayer.Log($"Sending SendItemsChangePacket with {items.Count()} items"); diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index d2aee4ca..66f10078 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -154,6 +154,8 @@ protected override void Subscribe() netPacketProcessor.SubscribeReusable(OnCommonChatPacket); netPacketProcessor.SubscribeReusable(OnUnconnectedPingPacket); netPacketProcessor.SubscribeNetSerializable(OnCommonItemChangePacket); + + netPacketProcessor.SubscribeReusable(OnCommonPitStopInteractionPacket); } private void OnLoaded() @@ -1110,6 +1112,32 @@ private void OnUnconnectedPingPacket(UnconnectedPingPacket packet, IPEndPoint en //SendUnconnectedPacket(packet, endPoint.Address.ToString(), endPoint.Port); } + private void OnCommonPitStopInteractionPacket(CommonPitStopInteractionPacket packet, ITransportPeer peer) + { + + if(NetworkedPitStopStation.Get(packet.NetId, out NetworkedPitStopStation controller)) + { + if (controller.ValidateInteraction(packet)) + { + //passed validation, send to all but the originator + SendPacketToAll(packet, DeliveryMethod.ReliableOrdered, peer); + } + else + { + //Failed to validate, player needs to rollback interaction + SendPacket(peer, new CommonPitStopInteractionPacket + { + NetId = packet.NetId, + InteractionType = (byte)PitStopStationInteractionType.Reject + }, DeliveryMethod.ReliableOrdered); + } + } + else + { + LogError($"OnCommonPitStopInteractionPacket() Failed to find PitStopStation with netId: {packet.NetId}"); + } + } + private void OnCommonItemChangePacket(CommonItemChangePacket packet, ITransportPeer peer) { //if(!TryGetServerPlayer(peer, out var player)) diff --git a/Multiplayer/Networking/Packets/Clientbound/World/ClientboundPitStopStationLookupPacket.cs b/Multiplayer/Networking/Packets/Clientbound/World/ClientboundPitStopStationLookupPacket.cs index 6bef6e87..26e30490 100644 --- a/Multiplayer/Networking/Packets/Clientbound/World/ClientboundPitStopStationLookupPacket.cs +++ b/Multiplayer/Networking/Packets/Clientbound/World/ClientboundPitStopStationLookupPacket.cs @@ -8,22 +8,23 @@ public class ClientboundPitStopStationLookupPacket { public ushort[] NetIds { get; set; } public Vector3[] Locations { get; set; } + public int[] SelectedCars { get; set; } public ClientboundPitStopStationLookupPacket() { } - public ClientboundPitStopStationLookupPacket(KeyValuePair[] NetIDtoLocation) + public ClientboundPitStopStationLookupPacket(Tuple[] data) { - if (NetIDtoLocation == null) - throw new ArgumentNullException(nameof(NetIDtoLocation)); + NetIds = new ushort[data.Length]; + Locations = new Vector3[data.Length]; + SelectedCars = new int[data.Length]; - NetIds = new ushort[NetIDtoLocation.Length]; - Locations = new Vector3[NetIDtoLocation.Length]; - - for (int i = 0; i < NetIDtoLocation.Length; i++) + for (int i = 0; i < data.Length; i++) { - NetIds[i] = NetIDtoLocation[i].Key; - Locations[i] = NetIDtoLocation[i].Value; + var (netId, location, selection) = data[i]; + NetIds[i] = netId; + Locations[i] = location; + SelectedCars[i] = selection; } } diff --git a/Multiplayer/Networking/Packets/Common/CommonPitStopInteractionPacket.cs b/Multiplayer/Networking/Packets/Common/CommonPitStopInteractionPacket.cs index 908ee7f4..4b3d35df 100644 --- a/Multiplayer/Networking/Packets/Common/CommonPitStopInteractionPacket.cs +++ b/Multiplayer/Networking/Packets/Common/CommonPitStopInteractionPacket.cs @@ -1,10 +1,10 @@ -using DV.ThingTypes; namespace Multiplayer.Networking.Packets.Common; public class CommonPitStopInteractionPacket { public ushort NetId { get; set; } - public ResourceType? ResourceType { get; set; } + public byte InteractionType { get; set; } + public int ResourceType { get; set; } public float State { get; set; } } From e24ebe00ec537e44025e704c5d2f41651834dbea Mon Sep 17 00:00:00 2001 From: AMacro Date: Tue, 28 Jan 2025 18:34:39 +1000 Subject: [PATCH 225/521] Refactor lobby request workflow After leaving a game the OnLobbyUpdate event is fired, triggering a rejoin. Refactored code to remove the event and reduce code duplication --- .../MainMenu/RightPaneControllerPatch.cs | 2 +- Multiplayer/Utils/SteamWorksUtils.cs | 61 ++++++------------- 2 files changed, 19 insertions(+), 44 deletions(-) diff --git a/Multiplayer/Patches/MainMenu/RightPaneControllerPatch.cs b/Multiplayer/Patches/MainMenu/RightPaneControllerPatch.cs index 1b06e93c..1a75e915 100644 --- a/Multiplayer/Patches/MainMenu/RightPaneControllerPatch.cs +++ b/Multiplayer/Patches/MainMenu/RightPaneControllerPatch.cs @@ -103,7 +103,7 @@ private static void OnEnablePre(RightPaneController __instance) [HarmonyPostfix] private static void OnEnablePost(RightPaneController __instance) { - SteamMatchmaking.OnLobbyDataChanged += SteamworksUtils.OnLobbyDataChanged; + //SteamMatchmaking.OnLobbyDataChanged += SteamworksUtils.OnLobbyDataChanged; SteamMatchmaking.OnLobbyInvite += SteamworksUtils.OnLobbyInviteRequest; SteamFriends.OnGameLobbyJoinRequested += SteamworksUtils.OnLobbyJoinRequest; diff --git a/Multiplayer/Utils/SteamWorksUtils.cs b/Multiplayer/Utils/SteamWorksUtils.cs index 08481488..f52ab614 100644 --- a/Multiplayer/Utils/SteamWorksUtils.cs +++ b/Multiplayer/Utils/SteamWorksUtils.cs @@ -81,12 +81,8 @@ public static ulong GetLobbyIdFromArgs() string[] args = Environment.GetCommandLineArgs(); for (int i = 0; i < args.Length - 1; i++) - { if (args[i] == "+connect_lobby") - { return ulong.Parse(args[i + 1]); - } - } return 0; } @@ -104,54 +100,30 @@ public static void JoinFromCommandLine() }; var lobby = new Lobby(sId); - var ret = lobby.Refresh(); + lobby.Refresh(); } - public static void OnLobbyDataChanged(Lobby lobby) + private static bool CanHandleLobbyRequest() { - - NetworkLifecycle.Instance.QueueMainMenuEvent(() => - { - Multiplayer.Log($"OnLobbyDataChanged({lobby.Id}) count: {lobby.Data?.Count()}"); - - foreach (var item in lobby.Data) - { - Multiplayer.Log($"OnEnablePost Data: {item.Key}, Value: {item.Value}"); - } - - ServerBrowserPane.lobbyToJoin = lobby; - MainMenuThingsAndStuff.Instance.SwitchToMenu((byte)RightPaneController_Patch.joinMenuIndex); - }); + return !NetworkLifecycle.Instance.IsServerRunning && + !NetworkLifecycle.Instance.IsClientRunning; } public static void OnLobbyJoinRequest(Lobby lobby, SteamId id) { Multiplayer.Log($"Received lobby join request: {lobby.Id}, {id.Value}"); - if (NetworkLifecycle.Instance.IsServerRunning || NetworkLifecycle.Instance.IsClientRunning) + if (!CanHandleLobbyRequest()) return; - NetworkLifecycle.Instance.QueueMainMenuEvent(() => - { - Multiplayer.Log($"OnLobbyDataChanged({lobby.Id}) count: {lobby.Data?.Count()}"); - - foreach (var item in lobby.Data) - { - Multiplayer.Log($"OnEnablePost Data: {item.Key}, Value: {item.Value}"); - } - - ServerBrowserPane.lobbyToJoin = lobby; - MainMenuThingsAndStuff.Instance.SwitchToMenu((byte)RightPaneController_Patch.joinMenuIndex); - }); - - NetworkLifecycle.Instance.TriggerMainMenuEventLater(); + QueueLobbyInvite(lobby); } public static void OnLobbyInviteRequest(Friend friend, Lobby lobby) { Multiplayer.Log($"Received lobby invite: {lobby.Id}"); - if (NetworkLifecycle.Instance.IsServerRunning || NetworkLifecycle.Instance.IsClientRunning) + if (!CanHandleLobbyRequest()) return; NetworkLifecycle.Instance.QueueMainMenuEvent(() => @@ -159,10 +131,7 @@ public static void OnLobbyInviteRequest(Friend friend, Lobby lobby) var popup = MainMenuThingsAndStuff.Instance.ShowYesNoPopup(); if (popup == null) - { - Multiplayer.LogError("OnLobbyInviteRequest() Popup not found."); return; - } popup.labelTMPro.text = $"{friend.Name} invited you to play!\r\nDo you wish to join?"; @@ -176,12 +145,8 @@ public static void OnLobbyInviteRequest(Friend friend, Lobby lobby) popup.Closed += (PopupResult result) => { - Multiplayer.LogDebug(()=>$"OnLobbyInviteRequest() Popup closed by {result.closedBy}"); if (result.closedBy == PopupClosedByAction.Positive) - { - MainMenuThingsAndStuff.Instance.SwitchToMenu((byte)RightPaneController_Patch.joinMenuIndex); - ServerBrowserPane.lobbyToJoin = lobby; - } + QueueLobbyInvite(lobby); }; }); @@ -189,4 +154,14 @@ public static void OnLobbyInviteRequest(Friend friend, Lobby lobby) NetworkLifecycle.Instance.TriggerMainMenuEventLater(); } + public static void QueueLobbyInvite(Lobby lobby) + { + NetworkLifecycle.Instance.QueueMainMenuEvent(() => + { + ServerBrowserPane.lobbyToJoin = lobby; + MainMenuThingsAndStuff.Instance.SwitchToMenu((byte)RightPaneController_Patch.joinMenuIndex); + }); + + NetworkLifecycle.Instance.TriggerMainMenuEventLater(); + } } From 61d8cea8b82e914b74a3df8586eb15c2bf40511d Mon Sep 17 00:00:00 2001 From: AMacro Date: Tue, 28 Jan 2025 21:44:40 +1000 Subject: [PATCH 226/521] sync full health states --- .../Networking/Train/NetworkedCarSpawner.cs | 13 ++++ .../Networking/Train/NetworkedTrainCar.cs | 2 +- .../Data/Train/TrainCarHealthData.cs | 73 +++++++++++++++++++ .../Data/Train/TrainsetSpawnPart.cs | 19 ++++- .../Managers/Client/NetworkClient.cs | 23 ++++-- .../Networking/Managers/NetworkManager.cs | 1 + .../Managers/Server/NetworkServer.cs | 2 +- .../Train/ClientboundCarHealthUpdatePacket.cs | 4 +- 8 files changed, 123 insertions(+), 14 deletions(-) create mode 100644 Multiplayer/Networking/Data/Train/TrainCarHealthData.cs diff --git a/Multiplayer/Components/Networking/Train/NetworkedCarSpawner.cs b/Multiplayer/Components/Networking/Train/NetworkedCarSpawner.cs index cceaccae..847fb670 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedCarSpawner.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedCarSpawner.cs @@ -1,4 +1,5 @@ using System.Collections; +using DV.Damage; using DV.LocoRestoration; using DV.Simulation.Brake; using DV.ThingTypes; @@ -60,6 +61,18 @@ public static NetworkedTrainCar SpawnCar(TrainsetSpawnPart spawnPart, bool preve trainCar.uniqueCar = false; trainCar.InitializeExistingLogicCar(spawnPart.CarId, spawnPart.CarGuid); + //set health data + if (spawnPart.Exploded) + { + var explosionBase = trainCar.GetComponent(); + if (explosionBase != null) + explosionBase.UpdateToExplodedStateExternal(); + else + TrainCarExplosion.UpdateModelToExploded(trainCar); + } + + spawnPart.CarHealthData.LoadTo(trainCar); + //Restoration vehicle hack //todo: make it work properly if (spawnPart.IsRestorationLoco) diff --git a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs index 59642886..0435fbee 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs @@ -506,7 +506,7 @@ private void Server_SendHealthState() if (!healthDirty) return; healthDirty = false; - NetworkLifecycle.Instance.Server.SendCarHealthUpdate(NetId, TrainCar.CarDamage.currentHealth); + NetworkLifecycle.Instance.Server.SendCarHealthUpdate(NetId, TrainCarHealthData.From(TrainCar)); } public bool Server_ValidateCouplerInteraction(CommonCouplerInteractionPacket packet, ITransportPeer peer) diff --git a/Multiplayer/Networking/Data/Train/TrainCarHealthData.cs b/Multiplayer/Networking/Data/Train/TrainCarHealthData.cs new file mode 100644 index 00000000..9854ccca --- /dev/null +++ b/Multiplayer/Networking/Data/Train/TrainCarHealthData.cs @@ -0,0 +1,73 @@ +using DV.Damage; +using LiteNetLib.Utils; +using System; + +namespace Multiplayer.Networking.Data.Train; + +public readonly struct TrainCarHealthData +{ + public readonly float BodyHP; + public readonly float WheelsHP; + public readonly float MechanicalPT; + public readonly float ElectricalPT; + public readonly bool WindowsBroken; + + private TrainCarHealthData(float bodyHP, float wheelsHP, float mechanicalPT, float electricalPT, bool windowsBroken) + { + BodyHP = bodyHP; + WheelsHP = wheelsHP; + MechanicalPT = mechanicalPT; + ElectricalPT = electricalPT; + WindowsBroken = windowsBroken; + } + public void LoadTo(TrainCar trainCar) + { + var dmgCtrl = trainCar.GetComponent(); + if (dmgCtrl != null) + { + dmgCtrl.bodyDamage.LoadCarDamageState(BodyHP); + dmgCtrl.wheels?.SetCurrentHealthPercentage(WheelsHP); + dmgCtrl.mechanicalPT?.SetCurrentHealthPercentage(MechanicalPT); + dmgCtrl.electricalPT?.SetCurrentHealthPercentage(ElectricalPT); + + if (dmgCtrl.windows != null) + dmgCtrl.windows.windowsBroken = WindowsBroken; + } + } + + public static TrainCarHealthData From(TrainCar car) + { + var dmgCtrl = car.GetComponent(); + + if (dmgCtrl == null ) + return new TrainCarHealthData(); + + float bodyHP = dmgCtrl?.bodyDamage?.HealthPercentage ?? 0; + float wheelsHP = dmgCtrl?.wheels?.HealthPercentage ?? 0; + float mechanicalPT = dmgCtrl?.mechanicalPT?.HealthPercentage ?? 0; + float electricalPT = dmgCtrl?.electricalPT?.HealthPercentage ?? 0; + bool brokenWindows = dmgCtrl?.windows?.windowsBroken ?? true; + + return new TrainCarHealthData(bodyHP, wheelsHP, mechanicalPT, electricalPT, brokenWindows); + } + + public static void Serialize(NetDataWriter writer, TrainCarHealthData data) + { + writer.Put(data.BodyHP); + writer.Put(data.WheelsHP); + writer.Put(data.MechanicalPT); + writer.Put(data.ElectricalPT); + writer.Put(data.WindowsBroken); + } + + public static TrainCarHealthData Deserialize(NetDataReader reader) + { + float bodyHP = reader.GetFloat(); + float wheelsHP = reader.GetFloat(); + float mechanicalPT = reader.GetFloat(); + float electricalPT = reader.GetFloat(); + bool brokenWindows = reader.GetBool(); + + return new TrainCarHealthData(bodyHP, wheelsHP, mechanicalPT, electricalPT, brokenWindows); + } +} diff --git a/Multiplayer/Networking/Data/Train/TrainsetSpawnPart.cs b/Multiplayer/Networking/Data/Train/TrainsetSpawnPart.cs index afd580d3..e5697a6e 100644 --- a/Multiplayer/Networking/Data/Train/TrainsetSpawnPart.cs +++ b/Multiplayer/Networking/Data/Train/TrainsetSpawnPart.cs @@ -21,6 +21,8 @@ public readonly struct TrainsetSpawnPart public readonly string LiveryId; public readonly string CarId; public readonly string CarGuid; + public readonly bool Exploded; + public readonly TrainCarHealthData CarHealthData; // Customisation details public readonly bool PlayerSpawnedCar; @@ -46,7 +48,8 @@ public readonly struct TrainsetSpawnPart public readonly BrakeSystemData BrakeData; public TrainsetSpawnPart( - ushort netId, string liveryId, string carId, string carGuid, bool playerSpawnedCar, bool isRestoration, LocoRestorationController.RestorationState restorationState, PaintTheme paintExterior, PaintTheme paintInterior, + ushort netId, string liveryId, string carId, string carGuid, bool exploded, TrainCarHealthData carHealthData, + 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,6 +58,8 @@ public TrainsetSpawnPart( LiveryId = liveryId; CarId = carId; CarGuid = carGuid; + Exploded = exploded; + CarHealthData = carHealthData; PlayerSpawnedCar = playerSpawnedCar; IsRestorationLoco = isRestoration; @@ -88,6 +93,9 @@ public static void Serialize(NetDataWriter writer, TrainsetSpawnPart data) writer.PutBytesWithLength(EMPTY_GUID); } + writer.Put(data.Exploded); + TrainCarHealthData.Serialize(writer, data.CarHealthData); + writer.Put(data.PlayerSpawnedCar); writer.Put(data.IsRestorationLoco); @@ -116,8 +124,10 @@ public static TrainsetSpawnPart Deserialize(NetDataReader reader) string liveryId = reader.GetString(); string carId = reader.GetString(); string carGuid = new Guid(reader.GetBytesWithLength()).ToString(); - bool playerSpawnedCar = reader.GetBool(); + bool exploded = reader.GetBool(); + TrainCarHealthData healthData = TrainCarHealthData.Deserialize(reader); + bool playerSpawnedCar = reader.GetBool(); bool isRestoration = reader.GetBool(); LocoRestorationController.RestorationState restorationState = default; if (isRestoration) @@ -142,7 +152,8 @@ public static TrainsetSpawnPart Deserialize(NetDataReader reader) var brakeSet = BrakeSystemData.Deserialize(reader); return new TrainsetSpawnPart( - netId, liveryId, carId, carGuid, playerSpawnedCar, isRestoration, restorationState, exteriorPaint, interiorPaint, + netId, liveryId, carId, carGuid, exploded, healthData, + playerSpawnedCar, isRestoration, restorationState, exteriorPaint, interiorPaint, frontCoupling, rearCoupling, speed, position, rotation, bogie1, bogie2, brakeSet); @@ -162,6 +173,8 @@ public static TrainsetSpawnPart FromTrainCar(NetworkedTrainCar networkedTrainCar trainCar.carLivery.id, trainCar.ID, trainCar.CarGUID, + trainCar.isExploded, + TrainCarHealthData.From(trainCar), trainCar.playerSpawnedCar, restorationController != null, diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 56b25d57..654716e5 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -828,15 +828,19 @@ private void OnClientboundCarHealthUpdatePacket(ClientboundCarHealthUpdatePacket if (!NetworkedTrainCar.GetTrainCar(packet.NetId, out TrainCar trainCar)) return; - CarDamageModel carDamage = trainCar.CarDamage; - float difference = Mathf.Abs(packet.Health - carDamage.currentHealth); - if (difference < 0.0001) - return; + packet.Health.LoadTo(trainCar); + //todo test new code thoroughly + //check that career and comms radio repairs and damage work as expected - if (packet.Health < carDamage.currentHealth) - carDamage.DamageCar(difference); - else - carDamage.RepairCar(difference); + //CarDamageModel carDamage = trainCar.CarDamage; + //float difference = Mathf.Abs(packet.Health - carDamage.currentHealth); + //if (difference < 0.0001) + // return; + + //if (packet.Health < carDamage.currentHealth) + // carDamage.DamageCar(difference); + //else + // carDamage.RepairCar(difference); } private void OnClientboundRerailTrainPacket(ClientboundRerailTrainPacket packet) @@ -1372,6 +1376,8 @@ public void SendChat(string message) public void SendPitStopInteractionPacket(ushort netId, PitStopStationInteractionType interaction, ResourceType? resource, float state) { + Multiplayer.LogDebug(()=>$"SendPitStopInteractionPacket({netId}, [{interaction}], {resource}, {state}"); + int res = resource == null ? 0 : (int)resource; SendPacketToServer(new CommonPitStopInteractionPacket { @@ -1380,6 +1386,7 @@ public void SendPitStopInteractionPacket(ushort netId, PitStopStationInteraction ResourceType = res, State = state }, DeliveryMethod.ReliableOrdered); + } public void SendItemsChangePacket(List items) diff --git a/Multiplayer/Networking/Managers/NetworkManager.cs b/Multiplayer/Networking/Managers/NetworkManager.cs index 970384e3..1340b46c 100644 --- a/Multiplayer/Networking/Managers/NetworkManager.cs +++ b/Multiplayer/Networking/Managers/NetworkManager.cs @@ -59,6 +59,7 @@ private void RegisterNestedTypes() netPacketProcessor.RegisterNestedType(StationsChainNetworkData.Serialize, StationsChainNetworkData.Deserialize); netPacketProcessor.RegisterNestedType(TrainsetMovementPart.Serialize, TrainsetMovementPart.Deserialize); netPacketProcessor.RegisterNestedType(TrainsetSpawnPart.Serialize, TrainsetSpawnPart.Deserialize); + netPacketProcessor.RegisterNestedType(TrainCarHealthData.Serialize, TrainCarHealthData.Deserialize); netPacketProcessor.RegisterNestedType(Vector2Serializer.Serialize, Vector2Serializer.Deserialize); netPacketProcessor.RegisterNestedType(Vector3Serializer.Serialize, Vector3Serializer.Deserialize); } diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index 66f10078..a2c560f2 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -402,7 +402,7 @@ public void SendCargoState(TrainCar trainCar, ushort netId, bool isLoading, byte }, DeliveryMethod.ReliableOrdered, SelfPeer); } - public void SendCarHealthUpdate(ushort netId, float health) + public void SendCarHealthUpdate(ushort netId, TrainCarHealthData health) { SendPacketToAll(new ClientboundCarHealthUpdatePacket { diff --git a/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundCarHealthUpdatePacket.cs b/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundCarHealthUpdatePacket.cs index dd6846d6..1bcfdf70 100644 --- a/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundCarHealthUpdatePacket.cs +++ b/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundCarHealthUpdatePacket.cs @@ -1,7 +1,9 @@ +using Multiplayer.Networking.Data.Train; + namespace Multiplayer.Networking.Packets.Clientbound.Train; public class ClientboundCarHealthUpdatePacket { public ushort NetId { get; set; } - public float Health { get; set; } + public TrainCarHealthData Health { get; set; } } From 52fcb73e64441fbe94bb73ecd2186dd75288671a Mon Sep 17 00:00:00 2001 From: AMacro Date: Tue, 28 Jan 2025 21:45:17 +1000 Subject: [PATCH 227/521] implement loading delay / check --- .../World/NetworkedPitStopStation.cs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs b/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs index bbf4a583..bcfcc09a 100644 --- a/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs +++ b/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs @@ -148,7 +148,7 @@ public void OnPlayerDisconnect(ITransportPeer peer) #region Common - public IEnumerator Init() + private IEnumerator Init() { Multiplayer.LogDebug(() => $"NetworkedPitStopStation.Init() station: {Station == null}, pitstop: {Station?.pitstop == null}"); @@ -157,7 +157,21 @@ public IEnumerator Init() var resourceModules = Station?.locoResourceModules?.resourceModules; - var carSelectorGrab = GetComponentInChildren(); + GrabHandlerHingeJoint carSelectorGrab = null; + + while (carSelectorGrab == null) + { + try + { + carSelectorGrab = GetComponentInChildren(); + } + catch (Exception ex) { } + + if (carSelectorGrab == null) + yield return new WaitForEndOfFrame(); + } + + if (carSelectorGrab != null) { Multiplayer.LogDebug(() => $"NetworkedPitStopStation.Init() Grab Handler found: {carSelectorGrab != null}, Name: {carSelectorGrab.name}"); @@ -314,6 +328,7 @@ private void LeverUnGrabbed(LocoResourceModule module) isGrabbed = false; grabbedModule = null; grabbedAmplitudeChecker = null; + lastUnitsToBuy = module.Data.unitsToBuy; NetworkLifecycle.Instance?.Client.SendPitStopInteractionPacket(NetId, PitStopStationInteractionType.Ungrab, module.resourceType, lastUnitsToBuy); } #endregion From 819c9c08d74c6315c6ca0debd19b07fe10b6400f Mon Sep 17 00:00:00 2001 From: AMacro Date: Fri, 31 Jan 2025 21:05:20 +1000 Subject: [PATCH 228/521] Ensure PitStops are active on the server --- .../PlayerDistanceGameObjectsDisablerPatch.cs | 128 ++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 Multiplayer/Patches/World/PlayerDistanceGameObjectsDisablerPatch.cs diff --git a/Multiplayer/Patches/World/PlayerDistanceGameObjectsDisablerPatch.cs b/Multiplayer/Patches/World/PlayerDistanceGameObjectsDisablerPatch.cs new file mode 100644 index 00000000..037e1c95 --- /dev/null +++ b/Multiplayer/Patches/World/PlayerDistanceGameObjectsDisablerPatch.cs @@ -0,0 +1,128 @@ +using HarmonyLib; +using Multiplayer.Components.Networking; +using Multiplayer.Utils; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; +using UnityEngine; + +namespace Multiplayer.Patches.World; + +[HarmonyPatch] +public static class PlayerDistanceGameObjectsDisablerPatch +{ + const int SKIPS = 2; + static readonly CodeInstruction targetMethod = CodeInstruction.Call(typeof(Vector3), "op_Subtraction", [typeof(Vector3), typeof(Vector3)], null); + static readonly CodeInstruction newMethod = CodeInstruction.Call(typeof(PlayerDistanceGameObjectsDisablerPatch), nameof(CheckConditions), [typeof(Vector3), typeof(Vector3), typeof(PlayerDistanceGameObjectsDisabler)], null); + + + public static IEnumerable TargetMethods() + { + var stuff = typeof(PlayerDistanceGameObjectsDisabler) + .GetNestedTypes(BindingFlags.NonPublic | BindingFlags.Instance) + .Where(t => t.Name.StartsWith("")) + .SelectMany(t => t.GetMethods(BindingFlags.NonPublic | BindingFlags.Instance)); + + foreach (var method in stuff) + { + Multiplayer.LogDebug(() => $"TargetMethods: {method.Name}"); + } + + return typeof(PlayerDistanceGameObjectsDisabler) + .GetNestedTypes(BindingFlags.NonPublic | BindingFlags.Instance) + .Where(t => t.Name.StartsWith("")) + .SelectMany(t => t.GetMethods(BindingFlags.NonPublic | BindingFlags.Instance)) + .Where(m => m.Name == "MoveNext"); + } + + + /* + * We want to find the subtraction (line 79) replace it with loading "this" to the stack + * and then line 80 can call our custom function + * Lines 81 and 82 are not required + * This pattern is used again in the re-enable check (lines 104 - 115) + + 74 00D6 ldfld int32 PlayerDistanceGameObjectsDisabler/'d__6'::'5__2' + 75 00DB callvirt instance !0 class [mscorlib] System.Collections.Generic.List`1::get_Item(int32) + 76 00E0 callvirt instance class [UnityEngine.CoreModule] + UnityEngine.Transform[UnityEngine.CoreModule] UnityEngine.GameObject::get_transform() + 77 00E5 callvirt instance valuetype[UnityEngine.CoreModule] UnityEngine.Vector3 [UnityEngine.CoreModule] UnityEngine.Transform::get_position() + 78 00EA ldloc.2 //parameter for the position of the object we're testing against + + //overwrite with ldloc.1 (pass in 'this' as the final parameter) + 79 00EB call valuetype[UnityEngine.CoreModule] UnityEngine.Vector3[UnityEngine.CoreModule] UnityEngine.Vector3::op_Subtraction(valuetype[UnityEngine.CoreModule] UnityEngine.Vector3, valuetype[UnityEngine.CoreModule] UnityEngine.Vector3) + //overwrite with call to CheckConditions() + //Insert 3 NOPs + 80 00F0 stloc.3 //skip 0 + 81 00F1 ldloca.s V_3(3) //skip 1 + 82 00F3 call instance float32[UnityEngine.CoreModule] UnityEngine.Vector3::get_sqrMagnitude() //Skip 2 + 83 00F8 ldloc.1 + 84 00F9 ldfld float32 PlayerDistanceGameObjectsDisabler::disableSqrDistance + 85 00FE ble.un.s 94 (0119) ldloc.1 + */ + //[HarmonyPatch("GameObjectsDistanceCheck")] + [HarmonyTranspiler] + public static IEnumerable GameObjectsDistanceCheck(IEnumerable instructions) + { + Multiplayer.LogDebug(() => $"Starting transpiler"); + + var code = new List(instructions); + Multiplayer.LogDebug(() => "IL Before:"); + for (int i = 0; i < code.Count; i++) + { + Multiplayer.LogDebug(() => $"{i:D4}: {code[i]}"); + } + + int skipCtr = 0; + bool skipFlag = false; + + var newCode = new List(); + + foreach (CodeInstruction instruction in instructions) + { + Multiplayer.LogDebug(() => $"Checking instruction: {instruction}"); + if (instruction.opcode == OpCodes.Call && instruction.operand?.ToString() == targetMethod.operand?.ToString()) + { + Multiplayer.LogDebug(() => "Found target method, replacing"); + newCode.Add(new CodeInstruction(OpCodes.Ldloc_1)); + newCode.Add(newMethod); //skip 0 + newCode.Add(new CodeInstruction(OpCodes.Nop)); //skip 1 + newCode.Add(new CodeInstruction(OpCodes.Nop)); //skip 2 + skipCtr = 0; + skipFlag = true; + } + else if (skipFlag) + { + if (skipCtr == SKIPS) + { + skipFlag = false; + continue; + } + skipCtr++; + } + else + newCode.Add(instruction); + } + + Multiplayer.LogDebug(() => "IL After:"); + for (int i = 0; i < newCode.Count; i++) + { + Multiplayer.LogDebug(() => $"{i:D4}: {newCode[i]}"); + } + + return newCode; + } + + + public static float CheckConditions(Vector3 vecA, Vector3 vecB, PlayerDistanceGameObjectsDisabler instance) + { + if (instance.gameObject.name == "RefillStations" && NetworkLifecycle.Instance.IsHost()) + { + //Multiplayer.LogDebug(() =>$"CheckConditions({instance?.gameObject?.name}, {vecA}, {vecB}) Camera pos: {PlayerManager.ActiveCamera.transform.position}"); + return vecA.AnyPlayerSqrMag(); + } + + return (vecA - vecB).sqrMagnitude; + } +} From 3e17aba3da3a5463f2378c6a793b4e162275860c Mon Sep 17 00:00:00 2001 From: AMacro Date: Fri, 31 Jan 2025 21:05:41 +1000 Subject: [PATCH 229/521] Add debugging utility --- .../Managers/Server/NetworkServer.cs | 22 +++++++++++++++++++ Multiplayer/Utils/UnityExtensions.cs | 9 ++++++++ 2 files changed, 31 insertions(+) diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index a2c560f2..97eb36bd 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -187,6 +187,28 @@ private void OnLoaded() System.Console.WriteLine("Connection is not established."); } } + + LogDebug(() => + { + StringBuilder sb = new StringBuilder(); + + var objects = Resources.FindObjectsOfTypeAll(); + foreach (var obj in objects) + sb.AppendLine($"PlayerDistanceGameObjectsDisabler() {obj.gameObject.GetObjectPath()}"); + + return sb.ToString(); + }); + + LogDebug(() => + { + StringBuilder sb = new StringBuilder(); + + var objects = Resources.FindObjectsOfTypeAll(); + foreach (var obj in objects) + sb.AppendLine($"PlayerDistanceMultipleGameObjectsOptimizer() {obj.gameObject.GetObjectPath()}"); + + return sb.ToString(); + }); } public bool TryGetServerPlayer(ITransportPeer peer, out ServerPlayer player) diff --git a/Multiplayer/Utils/UnityExtensions.cs b/Multiplayer/Utils/UnityExtensions.cs index c587612e..23620a1c 100644 --- a/Multiplayer/Utils/UnityExtensions.cs +++ b/Multiplayer/Utils/UnityExtensions.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text; using JetBrains.Annotations; using UnityEngine; @@ -123,4 +124,12 @@ public static Color UInt32ToColor(this uint packed) float b = (packed & 0xFF) / 255f; return new Color(r, g, b, a); } + + public static string GetObjectPath(this GameObject obj) + { + if (obj.transform.parent == null) + return obj.name; + + return obj.transform.parent.gameObject.GetObjectPath() + "/" + obj.name; + } } From f16ac39a61baddb77d6a1bbef1cf1596c48e261a Mon Sep 17 00:00:00 2001 From: AMacro Date: Fri, 31 Jan 2025 22:35:47 +1000 Subject: [PATCH 230/521] Update comments and logs for distance checking patch --- .../PlayerDistanceGameObjectsDisablerPatch.cs | 76 ++++++++++--------- 1 file changed, 42 insertions(+), 34 deletions(-) diff --git a/Multiplayer/Patches/World/PlayerDistanceGameObjectsDisablerPatch.cs b/Multiplayer/Patches/World/PlayerDistanceGameObjectsDisablerPatch.cs index 037e1c95..c477cc09 100644 --- a/Multiplayer/Patches/World/PlayerDistanceGameObjectsDisablerPatch.cs +++ b/Multiplayer/Patches/World/PlayerDistanceGameObjectsDisablerPatch.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Reflection; using System.Reflection.Emit; +using System.Text; using UnityEngine; namespace Multiplayer.Patches.World; @@ -14,21 +15,15 @@ public static class PlayerDistanceGameObjectsDisablerPatch { const int SKIPS = 2; static readonly CodeInstruction targetMethod = CodeInstruction.Call(typeof(Vector3), "op_Subtraction", [typeof(Vector3), typeof(Vector3)], null); - static readonly CodeInstruction newMethod = CodeInstruction.Call(typeof(PlayerDistanceGameObjectsDisablerPatch), nameof(CheckConditions), [typeof(Vector3), typeof(Vector3), typeof(PlayerDistanceGameObjectsDisabler)], null); + static readonly CodeInstruction newMethod = CodeInstruction.Call(typeof(PlayerDistanceGameObjectsDisablerPatch), nameof(CustomCalcSqrMagnitude), [typeof(Vector3), typeof(Vector3), typeof(PlayerDistanceGameObjectsDisabler)], null); public static IEnumerable TargetMethods() { - var stuff = typeof(PlayerDistanceGameObjectsDisabler) - .GetNestedTypes(BindingFlags.NonPublic | BindingFlags.Instance) - .Where(t => t.Name.StartsWith("")) - .SelectMany(t => t.GetMethods(BindingFlags.NonPublic | BindingFlags.Instance)); - - foreach (var method in stuff) - { - Multiplayer.LogDebug(() => $"TargetMethods: {method.Name}"); - } - + //We're targeting an 'IEnumerable'; this gets compiled as a state machine with + //a method per state. + //Find all of the resultant states that are a 'MoveNext', these are the methods we need to patch. + //Doing this dynamically reduces the chance a game update breaks the transpiler return typeof(PlayerDistanceGameObjectsDisabler) .GetNestedTypes(BindingFlags.NonPublic | BindingFlags.Instance) .Where(t => t.Name.StartsWith("")) @@ -38,9 +33,11 @@ public static IEnumerable TargetMethods() /* - * We want to find the subtraction (line 79) replace it with loading "this" to the stack - * and then line 80 can call our custom function - * Lines 81 and 82 are not required + * We want to find the call to Vector3 subtraction `(optimizingGameObjects[i].transform.position - position)` + * (found on line 79 of the IL code) and replace it with an instruction + * that loads the current instance "this" to the stack. + * we want to override line 80 so it calls our custom method `CustomCalcSqrMagnitude()` + * Lines 81 and 82 are not required and need to be NOP'd out * This pattern is used again in the re-enable check (lines 104 - 115) 74 00D6 ldfld int32 PlayerDistanceGameObjectsDisabler/'d__6'::'5__2' @@ -48,11 +45,11 @@ public static IEnumerable TargetMethods() 76 00E0 callvirt instance class [UnityEngine.CoreModule] UnityEngine.Transform[UnityEngine.CoreModule] UnityEngine.GameObject::get_transform() 77 00E5 callvirt instance valuetype[UnityEngine.CoreModule] UnityEngine.Vector3 [UnityEngine.CoreModule] UnityEngine.Transform::get_position() - 78 00EA ldloc.2 //parameter for the position of the object we're testing against + 78 00EA ldloc.2 //parameter for the position of the player's camera - //overwrite with ldloc.1 (pass in 'this' as the final parameter) + //overwrite line 79 with ldloc.1 (pass in 'this' as the final parameter of call to CustomCalcSqrMagnitude()) 79 00EB call valuetype[UnityEngine.CoreModule] UnityEngine.Vector3[UnityEngine.CoreModule] UnityEngine.Vector3::op_Subtraction(valuetype[UnityEngine.CoreModule] UnityEngine.Vector3, valuetype[UnityEngine.CoreModule] UnityEngine.Vector3) - //overwrite with call to CheckConditions() + //overwrite with call to CustomCalcSqrMagnitude() (techinically we are inserting the call and skipping thr original) //Insert 3 NOPs 80 00F0 stloc.3 //skip 0 81 00F1 ldloca.s V_3(3) //skip 1 @@ -60,19 +57,23 @@ 81 00F1 ldloca.s V_3(3) //skip 1 83 00F8 ldloc.1 84 00F9 ldfld float32 PlayerDistanceGameObjectsDisabler::disableSqrDistance 85 00FE ble.un.s 94 (0119) ldloc.1 + */ - //[HarmonyPatch("GameObjectsDistanceCheck")] [HarmonyTranspiler] public static IEnumerable GameObjectsDistanceCheck(IEnumerable instructions) { - Multiplayer.LogDebug(() => $"Starting transpiler"); + //Multiplayer.LogDebug(() => + //{ + // var code = new List(instructions); - var code = new List(instructions); - Multiplayer.LogDebug(() => "IL Before:"); - for (int i = 0; i < code.Count; i++) - { - Multiplayer.LogDebug(() => $"{i:D4}: {code[i]}"); - } + // StringBuilder sb = new StringBuilder(); + // sb.AppendLine("Starting transpiler"); + // sb.AppendLine("IL Before:"); + // for (int i = 0; i < code.Count; i++) + // sb.AppendLine($"{i:D4}: {code[i]}"); + + // return sb.ToString(); + //}); int skipCtr = 0; bool skipFlag = false; @@ -89,14 +90,14 @@ public static IEnumerable GameObjectsDistanceCheck(IEnumerable< newCode.Add(newMethod); //skip 0 newCode.Add(new CodeInstruction(OpCodes.Nop)); //skip 1 newCode.Add(new CodeInstruction(OpCodes.Nop)); //skip 2 - skipCtr = 0; + skipCtr = 0; //reset as there are 2 identical sections to the code to be patched. skipFlag = true; } else if (skipFlag) { if (skipCtr == SKIPS) { - skipFlag = false; + skipFlag = false; //stop skipping continue; } skipCtr++; @@ -105,21 +106,28 @@ public static IEnumerable GameObjectsDistanceCheck(IEnumerable< newCode.Add(instruction); } - Multiplayer.LogDebug(() => "IL After:"); - for (int i = 0; i < newCode.Count; i++) - { - Multiplayer.LogDebug(() => $"{i:D4}: {newCode[i]}"); - } + //Multiplayer.LogDebug(() => + //{ + // StringBuilder sb = new StringBuilder(); + // sb.AppendLine("IL After:"); + // for (int i = 0; i < newCode.Count; i++) + // sb.AppendLine($"{i:D4}: {newCode[i]}"); + + // return sb.ToString(); + //}); return newCode; } - public static float CheckConditions(Vector3 vecA, Vector3 vecB, PlayerDistanceGameObjectsDisabler instance) + public static float CustomCalcSqrMagnitude(Vector3 vecA, Vector3 vecB, PlayerDistanceGameObjectsDisabler instance) { + //At present we only need to target instsances of `PlayerDistanceGameObjectsDisabler` + //that are on the 'RefillStations' game object, as this is managing Pit Stop stations + //we need these to be active on the host when any player is nearby. if (instance.gameObject.name == "RefillStations" && NetworkLifecycle.Instance.IsHost()) { - //Multiplayer.LogDebug(() =>$"CheckConditions({instance?.gameObject?.name}, {vecA}, {vecB}) Camera pos: {PlayerManager.ActiveCamera.transform.position}"); + //Multiplayer.LogDebug(() =>$"CustomCalcSqrMagnitude({instance?.gameObject?.name}, {vecA}, {vecB}) Camera pos: {PlayerManager.ActiveCamera.transform.position}"); return vecA.AnyPlayerSqrMag(); } From 2dbc1cd3339b4b0d41504a00c584194dfca9abc1 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 1 Feb 2025 15:32:52 +1000 Subject: [PATCH 231/521] Update PitStop handling improved sync between server and clients. Refactored sync code and application of new state values added documentation cleaned up and simplified initialisation --- .../World/NetworkedPitStopStation.cs | 163 ++++++++++++++---- .../Data/PitStopStationInteractionType.cs | 4 - .../Managers/Client/NetworkClient.cs | 2 +- 3 files changed, 132 insertions(+), 37 deletions(-) diff --git a/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs b/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs index bcfcc09a..0d0d1ef9 100644 --- a/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs +++ b/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs @@ -15,6 +15,9 @@ namespace Multiplayer.Components.Networking.World; +/// +/// Handles networked interactions with pit stop stations, including vehicle selection and resource management. +/// public class NetworkedPitStopStation : IdMonoBehaviour { #region Lookup Cache @@ -74,6 +77,9 @@ public static void InitialisePitStops() protected override bool IsIdServerAuthoritative => true; + const float MAX_DELTA = 0.2f; + const float MIN_UPDATE_TIME = 0.1f; + public PitStopStation Station { get; set; } public string StationName { get; private set; } @@ -82,6 +88,12 @@ public static void InitialisePitStops() private readonly Dictionary grabbedHandlerLookup = []; private bool isGrabbed = false; + private bool wasGrabbed = false; + private bool isRemoteGrabbed = false; + private bool wasRemoteGrabbed = false; + private float lastRemoteValue = 0.0f; + private float lastUpdateTime = 0.0f; + private LocoResourceModule grabbedModule; private RotaryAmplitudeChecker grabbedAmplitudeChecker; private float lastUnitsToBuy; @@ -117,14 +129,70 @@ protected override void OnDestroy() base.OnDestroy(); } - protected void Update() + protected void LateUpdate() { - if (isGrabbed && grabbedModule != null && grabbedAmplitudeChecker != null) + if (grabbedModule == null && grabbedAmplitudeChecker == null) + return; + + //Handle local grab interactions + if (isGrabbed || (wasGrabbed && lastUnitsToBuy != grabbedModule.Data.unitsToBuy)) { - if(grabbedModule.Data.unitsToBuy != lastUnitsToBuy) + //ensure the delta is big enough to be worth sending or we have reached a limit + var delta = Math.Abs(lastUnitsToBuy - grabbedModule.Data.unitsToBuy); + var deltaTime = Time.time - lastUpdateTime; + + //Check if the units to buy have reached a limit (0 or AbsoluteMaxValue), as this overrides a delta below minimum + var unitsToBuyChanged = + (grabbedModule.Data.unitsToBuy == grabbedModule.AbsoluteMinValue && lastUnitsToBuy != grabbedModule.AbsoluteMinValue) + || (grabbedModule.Data.unitsToBuy == grabbedModule.AbsoluteMaxValue && lastUnitsToBuy != grabbedModule.AbsoluteMaxValue); + + //Send the update if we've passed the time threshold AND we have a big enough change or hit a limit + if (deltaTime > MIN_UPDATE_TIME && (delta > MAX_DELTA || unitsToBuyChanged)) { lastUnitsToBuy = grabbedModule.Data.unitsToBuy; - NetworkLifecycle.Instance?.Client.SendPitStopInteractionPacket(NetId, PitStopStationInteractionType.StateUpdate, grabbedModule.resourceType, lastUnitsToBuy); + lastUpdateTime = Time.time; + + NetworkLifecycle.Instance?.Client.SendPitStopInteractionPacket( + NetId, + PitStopStationInteractionType.StateUpdate, + grabbedModule.resourceType, + lastUnitsToBuy + ); + } + } + //Local grab has ended, but needs to be finalised + else if (wasGrabbed) + { + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.LateUpdate() wasGrabbed: {wasGrabbed}, previous: {lastUnitsToBuy}, new: {grabbedModule.Data.unitsToBuy}"); + lastUnitsToBuy = grabbedModule.Data.unitsToBuy; + + NetworkLifecycle.Instance?.Client.SendPitStopInteractionPacket( + NetId, + PitStopStationInteractionType.Ungrab, + grabbedModule.resourceType, + lastUnitsToBuy + ); + + //Reset grab states + wasGrabbed = false; + grabbedModule = null; + grabbedAmplitudeChecker = null; + } + + //allow things to settle after remote grab released + if (!isRemoteGrabbed && wasRemoteGrabbed) + { + float previous = grabbedModule.Data.unitsToBuy; + //grabbedModule.Data.unitsToBuy = lastRemoteValue; + grabbedModule.SetUnitsToBuy(lastRemoteValue); + + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.LateUpdate() wasRemoteGrabbed: {wasRemoteGrabbed}, previous: {previous}, new: {lastRemoteValue}"); + + if (previous == lastRemoteValue) + { + //settled, stop tracking remote + wasRemoteGrabbed = false; + grabbedModule = null; } } } @@ -148,6 +216,9 @@ public void OnPlayerDisconnect(ITransportPeer peer) #region Common + /// + /// Initializes the pit stop station and sets up event handlers for grab interactions. + /// private IEnumerator Init() { Multiplayer.LogDebug(() => $"NetworkedPitStopStation.Init() station: {Station == null}, pitstop: {Station?.pitstop == null}"); @@ -157,20 +228,8 @@ private IEnumerator Init() var resourceModules = Station?.locoResourceModules?.resourceModules; - GrabHandlerHingeJoint carSelectorGrab = null; - - while (carSelectorGrab == null) - { - try - { - carSelectorGrab = GetComponentInChildren(); - } - catch (Exception ex) { } - - if (carSelectorGrab == null) - yield return new WaitForEndOfFrame(); - } - + yield return new WaitUntil(() => GetComponentInChildren(true) != null); + GrabHandlerHingeJoint carSelectorGrab = GetComponentInChildren(true); if (carSelectorGrab != null) { @@ -218,6 +277,10 @@ private IEnumerator Init() Multiplayer.LogDebug(() => sb.ToString()); } + /// + /// Processes incoming network packets for pit stop interactions. + /// + /// The packet containing interaction data. public void ProcessPacket(CommonPitStopInteractionPacket packet) { PitStopStationInteractionType interactionType = (PitStopStationInteractionType)packet.InteractionType; @@ -226,17 +289,23 @@ public void ProcessPacket(CommonPitStopInteractionPacket packet) GrabHandlerHingeJoint grab = null; LocoResourceModule resourceModule = null; - Multiplayer.LogDebug(() => $"ProcessPacket() [{StationName}, {NetId}] {interactionType}, resource type: {resourceType}, state: {packet.State}"); + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.ProcessPacket() [{StationName}, {NetId}] {interactionType}, resource type: {resourceType}, state: {packet.State}"); if (resourceType != null && resourceType != 0) { if(!grabbedHandlerLookup.TryGetValue((ResourceType)resourceType, out grab)) - Multiplayer.LogError($"Could not find ResourceType in grabbedHandlerLookup for station {StationName}, resource type: {resourceType}"); + Multiplayer.LogError($"Could not find ResourceType in grabbedHandlerLookup for Pit Stop station {StationName}, resource type: {resourceType}"); else if(!grabberLookup.TryGetValue(grab, out var tup)) - Multiplayer.LogError($"Could not find GrabHandler in grabberLookup for station {StationName}, resource type: {resourceType}"); + Multiplayer.LogError($"Could not find GrabHandler in grabberLookup for Pit Stop station {StationName}, resource type: {resourceType}"); else (resourceModule, _, _) = tup; + + if (packet.State < resourceModule.AbsoluteMinValue || packet.State > resourceModule.AbsoluteMaxValue) + { + Multiplayer.LogError($"Invalid Pit Stop state value: {packet.State} for resource {resourceModule.resourceType}"); + return; + } } switch (interactionType) @@ -252,25 +321,42 @@ public void ProcessPacket(CommonPitStopInteractionPacket packet) //set direction if (resourceType != null && resourceType != 0 && resourceModule != null) - resourceModule.Data.unitsToBuy = (int)packet.State; + { + grabbedModule = resourceModule; + lastRemoteValue = packet.State; + } + isRemoteGrabbed = true; + wasRemoteGrabbed = true; break; case PitStopStationInteractionType.Ungrab: //allow interaction - if (grab != null) - grab.interactionAllowed = true; + //if (grab != null) + // grab.interactionAllowed = true; - //set direction if (resourceType != null && resourceType != 0 && resourceModule != null) - resourceModule.Data.unitsToBuy = (int)packet.State; + { + lastRemoteValue = packet.State; + //resourceModule.Data.unitsToBuy = lastRemoteValue; + resourceModule.SetUnitsToBuy(lastRemoteValue); + } + + isRemoteGrabbed = false; break; case PitStopStationInteractionType.StateUpdate: if (resourceType != null && resourceType != 0 && resourceModule != null) - resourceModule.Data.unitsToBuy = (int)packet.State; + { + if (isRemoteGrabbed || wasRemoteGrabbed) + { + lastRemoteValue = packet.State; + //resourceModule.Data.unitsToBuy = lastRemoteValue; + resourceModule.SetUnitsToBuy(lastRemoteValue); + } + } break; case PitStopStationInteractionType.SelectCar: @@ -289,19 +375,27 @@ public void ProcessPacket(CommonPitStopInteractionPacket packet) #endregion #region Client - + /// + /// Handles grab interactions for the car selector knob. + /// private void CarSelectorGrabbed() { Multiplayer.LogDebug(() => $"CarSelectorGrabbed() {StationName}"); NetworkLifecycle.Instance?.Client.SendPitStopInteractionPacket(NetId, PitStopStationInteractionType.Grab, null, 0); } + /// + /// Handles end of grab (release) interactions for the car selector knob. + /// private void CarSelectorUnGrabbed() { Multiplayer.LogDebug(() => $"CarSelectorUnGrabbed() {StationName}"); NetworkLifecycle.Instance?.Client.SendPitStopInteractionPacket(NetId, PitStopStationInteractionType.Ungrab, null, Station.pitstop.SelectedIndex); } + /// + /// Handles change of selected car events. + /// private void CarSelected() { if (NetworkLifecycle.Instance.IsProcessingPacket) @@ -312,24 +406,29 @@ private void CarSelected() NetworkLifecycle.Instance?.Client.SendPitStopInteractionPacket(NetId, PitStopStationInteractionType.SelectCar, null, Station.pitstop.SelectedIndex); } + /// + /// Handles grab interactions for resource module levers. + /// + /// The resource module being grabbed. private void LeverGrabbed(LocoResourceModule module) { Multiplayer.LogDebug(() => $"LeverGrabbed() {StationName}, module: {module.resourceType}"); isGrabbed = true; + wasGrabbed = true; grabbedModule = module; grabbedAmplitudeChecker = module.GetComponentInChildren(); lastUnitsToBuy = module.Data.unitsToBuy; NetworkLifecycle.Instance?.Client.SendPitStopInteractionPacket(NetId, PitStopStationInteractionType.Grab, module.resourceType, lastUnitsToBuy); } + /// + /// Handles end of grab (release) interactions for resource module levers. + /// + /// The resource module being grabbed. private void LeverUnGrabbed(LocoResourceModule module) { Multiplayer.LogDebug(() => $"LeverUnGrabbed() {StationName}, module: {module.resourceType}"); isGrabbed = false; - grabbedModule = null; - grabbedAmplitudeChecker = null; - lastUnitsToBuy = module.Data.unitsToBuy; - NetworkLifecycle.Instance?.Client.SendPitStopInteractionPacket(NetId, PitStopStationInteractionType.Ungrab, module.resourceType, lastUnitsToBuy); } #endregion } diff --git a/Multiplayer/Networking/Data/PitStopStationInteractionType.cs b/Multiplayer/Networking/Data/PitStopStationInteractionType.cs index 77b7aa5e..7424c211 100644 --- a/Multiplayer/Networking/Data/PitStopStationInteractionType.cs +++ b/Multiplayer/Networking/Data/PitStopStationInteractionType.cs @@ -1,8 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Multiplayer.Networking.Data; diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 654716e5..9e75d06a 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -1376,7 +1376,7 @@ public void SendChat(string message) public void SendPitStopInteractionPacket(ushort netId, PitStopStationInteractionType interaction, ResourceType? resource, float state) { - Multiplayer.LogDebug(()=>$"SendPitStopInteractionPacket({netId}, [{interaction}], {resource}, {state}"); + Multiplayer.LogDebug(()=>$"SendPitStopInteractionPacket({netId}, [{interaction}], {resource}, {state})"); int res = resource == null ? 0 : (int)resource; SendPacketToServer(new CommonPitStopInteractionPacket From c3b4480f6328524388225949064d4e07e75c08fd Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 1 Feb 2025 23:10:14 +1000 Subject: [PATCH 232/521] Add NetworkedPluggableObject sync Add ability to sync plug items Fix blocking of lever interaction --- .../World/NetworkedPitStopStation.cs | 45 +++++++++--- .../Data/PitStopStationMappingData.cs | 70 ++++++++++++++++++ .../Networking/Data/PlugInteractionType.cs | 17 +++++ .../Managers/Client/NetworkClient.cs | 71 +++++++++++++------ .../Networking/Managers/NetworkManager.cs | 1 + .../Managers/Server/NetworkServer.cs | 32 ++++++++- .../ClientboundPitStopStationLookupPacket.cs | 23 +++--- .../CommonPitStopPlugInteractionPacket.cs | 18 +++++ Multiplayer/Utils/UnityExtensions.cs | 5 ++ 9 files changed, 237 insertions(+), 45 deletions(-) create mode 100644 Multiplayer/Networking/Data/PitStopStationMappingData.cs create mode 100644 Multiplayer/Networking/Data/PlugInteractionType.cs create mode 100644 Multiplayer/Networking/Packets/Clientbound/World/CommonPitStopPlugInteractionPacket.cs diff --git a/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs b/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs index 0d0d1ef9..fb9efe2e 100644 --- a/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs +++ b/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs @@ -1,4 +1,3 @@ -using DV; using DV.Interaction; using Multiplayer.Networking.Packets.Common; using Multiplayer.Networking.TransportLayers; @@ -10,7 +9,7 @@ using UnityEngine; using DV.ThingTypes; using System.Collections; -using System.Collections.ObjectModel; +using System.Linq; namespace Multiplayer.Components.Networking.World; @@ -22,6 +21,7 @@ public class NetworkedPitStopStation : IdMonoBehaviour netPitStopStationToLocation = []; + public static bool Get(ushort netId, out NetworkedPitStopStation obj) { bool b = Get(netId, out IdMonoBehaviour rawObj); @@ -34,6 +34,11 @@ public static bool GetFromVector(Vector3 position, out NetworkedPitStopStation n return netPitStopStationToLocation.TryGetValue(position, out networkedPitStopStation); } + public static NetworkedPitStopStation[] GetAll() + { + return netPitStopStationToLocation.Values.ToArray(); + } + public static Tuple[] GetAllPitStopStations() { if (netPitStopStationToLocation.Count == 0) @@ -41,7 +46,6 @@ public static Tuple[] GetAllPitStopStations() List > result = []; - int i = 0; foreach (var kvp in netPitStopStationToLocation) { var selection = kvp.Value?.Station?.pitstop?.SelectedIndex ?? 0; @@ -86,6 +90,7 @@ public static void InitialisePitStops() private readonly GrabHandlerHingeJoint carSelectorGrab; private readonly Dictionary grabberLookup = []; private readonly Dictionary grabbedHandlerLookup = []; + private readonly Dictionary resourceToPluggableObject = []; private bool isGrabbed = false; private bool wasGrabbed = false; @@ -200,6 +205,15 @@ protected void LateUpdate() #region Server + public Dictionary GetPluggables() + { + Dictionary keyValuePairs = []; + foreach (var kvp in resourceToPluggableObject) + keyValuePairs.Add(kvp.Key, kvp.Value.NetId); + + return keyValuePairs; + } + public bool ValidateInteraction(CommonPitStopInteractionPacket packet) { //todo: implement validation code (player distance, player interacting, etc.) @@ -217,6 +231,13 @@ public void OnPlayerDisconnect(ITransportPeer peer) #region Common /// + /// Looks up Pluggable object by resource type + /// + public bool TryGetPluggable(ResourceType type, out NetworkedPluggableObject netPluggable) + { + return resourceToPluggableObject.TryGetValue(type, out netPluggable); + } + /// /// Initializes the pit stop station and sets up event handlers for grab interactions. /// private IEnumerator Init() @@ -228,6 +249,7 @@ private IEnumerator Init() var resourceModules = Station?.locoResourceModules?.resourceModules; + //Wait for levers an knobs to load yield return new WaitUntil(() => GetComponentInChildren(true) != null); GrabHandlerHingeJoint carSelectorGrab = GetComponentInChildren(true); @@ -267,6 +289,14 @@ private IEnumerator Init() sb.AppendLine($"\t{resourceModule.resourceType}, Grab Handler found: {grab != null}, Name: {grab.name}"); } } + + var plug = resourceModule.resourceHose; + if (plug != null) + { + var netPlug = plug.GetOrAddComponent(); + resourceToPluggableObject[resourceModule.resourceType] = netPlug; + netPlug.InitPitStop(this); + } } } else @@ -311,13 +341,12 @@ public void ProcessPacket(CommonPitStopInteractionPacket packet) switch (interactionType) { case PitStopStationInteractionType.Reject: - + //todo: implement rejection break; case PitStopStationInteractionType.Grab: //block interaction - if (grab != null) - grab.interactionAllowed = false; + grab?.SetMovingDisabled(false); //set direction if (resourceType != null && resourceType != 0 && resourceModule != null) @@ -332,8 +361,8 @@ public void ProcessPacket(CommonPitStopInteractionPacket packet) case PitStopStationInteractionType.Ungrab: //allow interaction - //if (grab != null) - // grab.interactionAllowed = true; + if (grab != null) + grab.SetMovingDisabled(true); if (resourceType != null && resourceType != 0 && resourceModule != null) { diff --git a/Multiplayer/Networking/Data/PitStopStationMappingData.cs b/Multiplayer/Networking/Data/PitStopStationMappingData.cs new file mode 100644 index 00000000..b9bb10e6 --- /dev/null +++ b/Multiplayer/Networking/Data/PitStopStationMappingData.cs @@ -0,0 +1,70 @@ +using DV.ThingTypes; +using LiteNetLib.Utils; +using Multiplayer.Components.Networking.World; +using Multiplayer.Networking.Serialization; +using System.Collections.Generic; +using UnityEngine; + +namespace Multiplayer.Networking.Data; + +public readonly struct PitStopStationMappingData(ushort netId, Vector3 location, int selectedCar, Dictionary plugMapping) +{ + public readonly ushort NetId = netId; + public readonly Vector3 Location = location; + public readonly int SelectedCar = selectedCar; + public readonly Dictionary PlugMapping = plugMapping; + + + public static PitStopStationMappingData From(NetworkedPitStopStation netStation) + { + var netId = netStation.NetId; + var location = netStation.transform.position - WorldMover.currentMove; + var selectedCar = netStation.Station?.pitstop?.SelectedIndex ?? 0; + var plugMapping = netStation.GetPluggables(); + + return new PitStopStationMappingData + ( + netId, + location, + selectedCar, + plugMapping + ); + } + + public static void Serialize(NetDataWriter writer, PitStopStationMappingData data) + { + writer.Put(data.NetId); + Vector3Serializer.Serialize(writer, data.Location); + writer.Put(data.SelectedCar); + + writer.Put(data.PlugMapping.Count); + foreach (var kvp in data.PlugMapping) + { + writer.Put((int)kvp.Key); + writer.Put(kvp.Value); + } + } + + public static PitStopStationMappingData Deserialize(NetDataReader reader) + { + var netId = reader.GetUShort(); + var location = Vector3Serializer.Deserialize(reader); + var selectedCar = reader.GetInt(); + + var dictCount = reader.GetInt(); + + Dictionary plugMapping = []; + for (int i = 0; i < dictCount; i++) + { + plugMapping.Add((ResourceType)reader.GetInt(), reader.GetUShort()); + } + + return new PitStopStationMappingData + ( + netId, + location, + selectedCar, + plugMapping + ); + } +} diff --git a/Multiplayer/Networking/Data/PlugInteractionType.cs b/Multiplayer/Networking/Data/PlugInteractionType.cs new file mode 100644 index 00000000..7f90069c --- /dev/null +++ b/Multiplayer/Networking/Data/PlugInteractionType.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Multiplayer.Networking.Data +{ + public enum PlugInteractionType : byte + { + Rejected, + PickedUp, + Dropped, + DockHome, + DockSocket + } +} diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 9e75d06a..287fbc0c 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -171,6 +171,7 @@ protected override void Subscribe() netPacketProcessor.SubscribeNetSerializable(OnCommonItemChangePacket); netPacketProcessor.SubscribeReusable(OnCommonPitStopInteractionPacket); + netPacketProcessor.SubscribeReusable(OnCommonPitStopPlugInteractionPacket); } #region Net Events @@ -445,41 +446,43 @@ private void OnClientboundStationControllerLookupPacket(ClientboundStationContro //Force pitstops to be mapped to same netId across all clients and server - probably should implement for junctions, etc. private void OnClientboundPitStopStationLookupPacket(ClientboundPitStopStationLookupPacket packet) { - LogDebug(() => $"OnClientboundPitStopStationLookupPacket({packet.NetIds?.Length})"); + LogDebug(() => $"OnClientboundPitStopStationLookupPacket({packet.PitStops?.Length})"); - if (packet == null) - { - LogError("OnClientboundPitStopStationLookupPacket received null packet"); - return; - } - - if (packet.NetIds == null || packet.Locations == null) + if (packet.PitStops == null) { - LogError($"OnClientboundPitStopStationLookupPacket received packet with null arrays: NetIDs is null: {packet.NetIds == null}, Locations is null: {packet.Locations == null}"); + LogError($"OnClientboundPitStopStationLookupPacket received packet with null arrays: NetIDs is null: {packet.PitStops == null}"); return; } - //Log($"WorldStreamingInit.LoadingFinished() CachePitStopStations()"); - //NetworkedPitStopStation.InitialisePitStops(); - - for (int i = 0; i < packet.NetIds.Length; i++) + for (int i = 0; i < packet.PitStops.Length; i++) { - LogDebug(() => $"OnClientboundPitStopStationLookupPacket[{i}] vector: {packet.Locations[i]}, netId: {packet.NetIds[i]}"); - if (NetworkedPitStopStation.GetFromVector(packet.Locations[i], out NetworkedPitStopStation netStation)) + LogDebug(() => $"OnClientboundPitStopStationLookupPacket({i}) vector: {packet.PitStops[i].Location}, netId: {packet.PitStops[i].NetId}"); + if (NetworkedPitStopStation.GetFromVector(packet.PitStops[i].Location, out NetworkedPitStopStation netStation)) { - netStation.NetId = packet.NetIds[i]; + netStation.NetId = packet.PitStops[i].NetId; if (netStation.Station.pitstop != null) { - netStation.Station.pitstop.currentCarIndex = packet.SelectedCars[i]; - //netStation.Station.pitstop.OnCarSelectionChanged(); + netStation.Station.pitstop.currentCarIndex = packet.PitStops[i].SelectedCar; + foreach (var mapping in packet.PitStops[i].PlugMapping) + { + if (netStation.TryGetPluggable(mapping.Key, out var netPluggable)) + { + LogDebug(() => $"OnClientboundPitStopStationLookupPacket({i}) {mapping.Key}, {mapping.Value} Found"); + netPluggable.NetId = mapping.Value; + } + else + { + LogDebug(() => $"OnClientboundPitStopStationLookupPacket({i}) {mapping.Key}, {mapping.Value} Not Found"); + } + } } } else - LogError($"Syncing PitStopStations station with coords: {packet.Locations[i]} not found"); + LogError($"Syncing PitStopStations station with coords: {packet.PitStops[i].Location} not found"); } } - private void OnClientboundRailwayStatePacket(ClientboundRailwayStatePacket packet) + private void OnClientboundRailwayStatePacket(ClientboundRailwayStatePacket packet) { for (int i = 0; i < packet.SelectedJunctionBranches.Length; i++) { @@ -964,7 +967,7 @@ private void OnCommonPitStopInteractionPacket(CommonPitStopInteractionPacket pac { if (!NetworkedPitStopStation.Get(packet.NetId, out var netPitStop)) { - LogWarning($"Pit stop Interaction received for netId: {packet.NetId}, but pit stop does not exist!"); + LogWarning($"Pit Stop Interaction received for netId: {packet.NetId}, but pit stop does not exist!"); } Log($"Pit stop interaction received for {netPitStop.StationName}"); @@ -973,6 +976,19 @@ private void OnCommonPitStopInteractionPacket(CommonPitStopInteractionPacket pac netPitStop.ProcessPacket(packet); } + private void OnCommonPitStopPlugInteractionPacket(CommonPitStopPlugInteractionPacket packet) + { + if (!NetworkedPluggableObject.Get(packet.NetId, out var netPlug)) + { + LogWarning($"Pit Stop Plug Interaction received for plug netId: {packet.NetId}, but pit stop plug does not exist!"); + } + + Log($"Pit Stop Plug Interaction received for {netPlug}"); + + LogDebug(() => $"OnCommonPitStopPlugInteractionPacket() [{netPlug?.transform?.parent?.name}, {packet.NetId}], interaction: [{packet.InteractionType}]"); + netPlug.ProcessPacket(packet); + } + private void OnCommonItemChangePacket(CommonItemChangePacket packet) { @@ -1386,7 +1402,20 @@ public void SendPitStopInteractionPacket(ushort netId, PitStopStationInteraction ResourceType = res, State = state }, DeliveryMethod.ReliableOrdered); + } + + public void SendPitStopPlugInteractionPacket(ushort netId, PlugInteractionType interaction, ushort trainCarNetId = 0, bool left = false) + { + Multiplayer.LogDebug(()=>$"SendPitStopInteractionPacket({netId}, {interaction}, {trainCarNetId}, {left})"); + SendPacketToServer(new CommonPitStopPlugInteractionPacket + { + NetId = netId, + InteractionType = (byte)interaction, + TrainCarNetId = trainCarNetId, + IsLeftSide = left, + + }, DeliveryMethod.ReliableOrdered); } public void SendItemsChangePacket(List items) diff --git a/Multiplayer/Networking/Managers/NetworkManager.cs b/Multiplayer/Networking/Managers/NetworkManager.cs index 1340b46c..3c45b45b 100644 --- a/Multiplayer/Networking/Managers/NetworkManager.cs +++ b/Multiplayer/Networking/Managers/NetworkManager.cs @@ -60,6 +60,7 @@ private void RegisterNestedTypes() netPacketProcessor.RegisterNestedType(TrainsetMovementPart.Serialize, TrainsetMovementPart.Deserialize); netPacketProcessor.RegisterNestedType(TrainsetSpawnPart.Serialize, TrainsetSpawnPart.Deserialize); netPacketProcessor.RegisterNestedType(TrainCarHealthData.Serialize, TrainCarHealthData.Deserialize); + netPacketProcessor.RegisterNestedType(PitStopStationMappingData.Serialize, PitStopStationMappingData.Deserialize); netPacketProcessor.RegisterNestedType(Vector2Serializer.Serialize, Vector2Serializer.Deserialize); netPacketProcessor.RegisterNestedType(Vector3Serializer.Serialize, Vector3Serializer.Deserialize); } diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index 97eb36bd..647304ed 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -156,6 +156,7 @@ protected override void Subscribe() netPacketProcessor.SubscribeNetSerializable(OnCommonItemChangePacket); netPacketProcessor.SubscribeReusable(OnCommonPitStopInteractionPacket); + netPacketProcessor.SubscribeReusable(OnCommonPitStopPlugInteractionPacket); } private void OnLoaded() @@ -740,7 +741,7 @@ private void OnServerboundClientReadyPacket(ServerboundClientReadyPacket packet, // 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); - SendPacket(peer, new ClientboundPitStopStationLookupPacket(NetworkedPitStopStation.GetAllPitStopStations()), DeliveryMethod.ReliableOrdered); + SendPacket(peer, new ClientboundPitStopStationLookupPacket(NetworkedPitStopStation.GetAll()), DeliveryMethod.ReliableOrdered); //send jobs foreach (StationController station in StationController.allStations) @@ -1159,6 +1160,35 @@ private void OnCommonPitStopInteractionPacket(CommonPitStopInteractionPacket pac LogError($"OnCommonPitStopInteractionPacket() Failed to find PitStopStation with netId: {packet.NetId}"); } } + private void OnCommonPitStopPlugInteractionPacket(CommonPitStopPlugInteractionPacket packet, ITransportPeer peer) + { + bool foundPlayer = TryGetServerPlayer(peer, out var player); + if (!foundPlayer) + LogWarning($"Received Pit Stop Plug Interaction, but player was not found"); + + if(NetworkedPluggableObject.Get(packet.NetId, out NetworkedPluggableObject plug) && foundPlayer) + { + if (plug.ValidateInteraction(packet)) + { + //passed validation, send to all but the originator + packet.PlayerId = player.Id; + SendPacketToAll(packet, DeliveryMethod.ReliableOrdered, peer); + } + else + { + //Failed to validate, player needs to rollback interaction + SendPacket(peer, new CommonPitStopPlugInteractionPacket + { + NetId = packet.NetId, + InteractionType = (byte)PitStopStationInteractionType.Reject + }, DeliveryMethod.ReliableOrdered); + } + } + else + { + LogError($"OnCommonPitStopInteractionPacket() Failed to find PitStopStation with netId: {packet.NetId}"); + } + } private void OnCommonItemChangePacket(CommonItemChangePacket packet, ITransportPeer peer) { diff --git a/Multiplayer/Networking/Packets/Clientbound/World/ClientboundPitStopStationLookupPacket.cs b/Multiplayer/Networking/Packets/Clientbound/World/ClientboundPitStopStationLookupPacket.cs index 26e30490..c82b7216 100644 --- a/Multiplayer/Networking/Packets/Clientbound/World/ClientboundPitStopStationLookupPacket.cs +++ b/Multiplayer/Networking/Packets/Clientbound/World/ClientboundPitStopStationLookupPacket.cs @@ -1,31 +1,24 @@ +using Multiplayer.Components.Networking.World; +using Multiplayer.Networking.Data; using System; using System.Collections.Generic; +using System.Linq; using UnityEngine; namespace Multiplayer.Networking.Packets.Clientbound.World; public class ClientboundPitStopStationLookupPacket { - public ushort[] NetIds { get; set; } - public Vector3[] Locations { get; set; } - public int[] SelectedCars { get; set; } - + public PitStopStationMappingData[] PitStops { get; set; } public ClientboundPitStopStationLookupPacket() { } - public ClientboundPitStopStationLookupPacket(Tuple[] data) + public ClientboundPitStopStationLookupPacket(NetworkedPitStopStation[] netStations) { - NetIds = new ushort[data.Length]; - Locations = new Vector3[data.Length]; - SelectedCars = new int[data.Length]; + PitStops = new PitStopStationMappingData[netStations.Count()]; - for (int i = 0; i < data.Length; i++) - { - var (netId, location, selection) = data[i]; - NetIds[i] = netId; - Locations[i] = location; - SelectedCars[i] = selection; - } + for (int i = 0; i < netStations.Count(); i++) + PitStops[i] = PitStopStationMappingData.From(netStations[i]); } } diff --git a/Multiplayer/Networking/Packets/Clientbound/World/CommonPitStopPlugInteractionPacket.cs b/Multiplayer/Networking/Packets/Clientbound/World/CommonPitStopPlugInteractionPacket.cs new file mode 100644 index 00000000..9266102f --- /dev/null +++ b/Multiplayer/Networking/Packets/Clientbound/World/CommonPitStopPlugInteractionPacket.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using UnityEngine; + +namespace Multiplayer.Networking.Packets.Clientbound.World; + +public class CommonPitStopPlugInteractionPacket +{ + public ushort NetId { get; set; } + public byte InteractionType { get; set; } + public byte PlayerId { get; set; } + public ushort TrainCarNetId { get; set; } + public bool IsLeftSide { get; set; } + //public Vector3 Position { get; set; } + //public Quaternion Rotation { get; set; } +} diff --git a/Multiplayer/Utils/UnityExtensions.cs b/Multiplayer/Utils/UnityExtensions.cs index 23620a1c..e91c770b 100644 --- a/Multiplayer/Utils/UnityExtensions.cs +++ b/Multiplayer/Utils/UnityExtensions.cs @@ -125,6 +125,11 @@ public static Color UInt32ToColor(this uint packed) return new Color(r, g, b, a); } + public static string GetObjectPath(this Component component) + { + return component.gameObject.GetObjectPath(); + } + public static string GetObjectPath(this GameObject obj) { if (obj.transform.parent == null) From dd7e0e7e4ca006031e24bd5c28110cbb8c76102f Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 1 Feb 2025 23:10:27 +1000 Subject: [PATCH 233/521] Create NetworkedPluggableObject.cs --- .../World/NetworkedPluggableObject.cs | 212 ++++++++++++++++++ 1 file changed, 212 insertions(+) create mode 100644 Multiplayer/Components/Networking/World/NetworkedPluggableObject.cs diff --git a/Multiplayer/Components/Networking/World/NetworkedPluggableObject.cs b/Multiplayer/Components/Networking/World/NetworkedPluggableObject.cs new file mode 100644 index 00000000..881eda93 --- /dev/null +++ b/Multiplayer/Components/Networking/World/NetworkedPluggableObject.cs @@ -0,0 +1,212 @@ +using DV.CabControls; +using Multiplayer.Components.Networking.Train; +using Multiplayer.Networking.Data; +using Multiplayer.Networking.Packets.Clientbound.World; +using Multiplayer.Networking.Packets.Common; +using Multiplayer.Utils; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using UnityEngine; + +namespace Multiplayer.Components.Networking.World; + +public class NetworkedPluggableObject : IdMonoBehaviour +{ + #region Lookup Cache + private static readonly Dictionary plugToStation = []; + public static bool Get(ushort netId, out NetworkedPluggableObject obj) + { + bool b = Get(netId, out IdMonoBehaviour rawObj); + obj = (NetworkedPluggableObject)rawObj; + return b; + } + #endregion + + protected override bool IsIdServerAuthoritative => true; + + public PluggableObject PluggableObject { get; private set; } + public NetworkedPitStopStation Station { get; private set; } + + private bool handlersInitialised = false; + + private byte playerHolding = 0; + private bool isGrabbed = false; + + #region Unity + protected override void Awake() + { + base.Awake(); + + PluggableObject = GetComponent(); + Multiplayer.LogDebug(() => $"NetworkedPluggableObject.Awake() {PluggableObject?.controlBase?.spec?.name}, {transform.parent.name}"); + } + + private IEnumerator Start() + { + yield return new WaitUntil(() => PluggableObject?.controlBase != null); + + PluggableObject.controlBase.Grabbed += OnGrabbed; + PluggableObject.controlBase.Ungrabbed += OnUngrabbed; + PluggableObject.PluggedIn += OnPlugged; + + handlersInitialised = true; + } + + protected override void OnDestroy() + { + plugToStation.Remove(this); + + if (PluggableObject?.controlBase != null && handlersInitialised) + { + PluggableObject.controlBase.Grabbed -= OnGrabbed; + PluggableObject.controlBase.Ungrabbed -= OnUngrabbed; + PluggableObject.PluggedIn -= OnPlugged; + } + + base.OnDestroy(); + } + #endregion + + #region Server + + public bool ValidateInteraction(CommonPitStopPlugInteractionPacket packet) + { + //todo: implement validation code (player distance, player interacting, etc.) + return true; + } + + #endregion + + #region Common + + public void ProcessPacket(CommonPitStopPlugInteractionPacket packet) + { + var interaction = (PlugInteractionType)packet.InteractionType; + + switch (interaction) + { + case PlugInteractionType.Rejected: + //todo implement rejection + break; + + case PlugInteractionType.PickedUp: + isGrabbed = true; + playerHolding = packet.PlayerId; + PluggableObject.controlGrabbed = true; + BlockInteraction(true); + break; + + case PlugInteractionType.Dropped: + isGrabbed = false; + playerHolding = 0; + PluggableObject.controlGrabbed = false; + BlockInteraction(false); + break; + + case PlugInteractionType.DockHome: + isGrabbed = false; + playerHolding = 0; + PluggableObject.controlGrabbed = false; + PluggableObject.InstantSnapTo(PluggableObject.startAttachedTo); + BlockInteraction(false); + break; + + case PlugInteractionType.DockSocket: + if (NetworkedTrainCar.GetTrainCar(packet.TrainCarNetId, out var trainCar)) + { + isGrabbed = false; + playerHolding = 0; + PluggableObject.controlGrabbed = false; + BlockInteraction(false); + + var sockets = trainCar.GetComponentsInChildren(); + if (packet.IsLeftSide) + PluggableObject.InstantSnapTo(sockets[0]); + else + PluggableObject.InstantSnapTo(sockets[1]); + } + break; + } + } + + private void BlockInteraction(bool block) + { + var rigid = GetComponentInChildren(); + + if (rigid) + rigid.isKinematic = !block; + + if (block) + PluggableObject.DisableColliders(); + else + PluggableObject.EnableColliders(); + } + + public void InitPitStop(NetworkedPitStopStation netPitStop) + { + if(plugToStation.TryGetValue(this, out _)) + { + Multiplayer.LogWarning($"Lookup cache 'plugToStation' already contains NetworkedPitStopStation \"{netPitStop?.StationName}\", skipping Init"); + return; + } + + Station = netPitStop; + plugToStation.Add(this, netPitStop); + } + #endregion + + #region Client + private void OnGrabbed(ControlImplBase control) + { + Multiplayer.LogDebug(() => $"NetworkedPluggableObject.OnGrabbed() [{transform.parent.name}, {NetId}] station: {Station?.StationName}"); + NetworkLifecycle.Instance.Client?.SendPitStopPlugInteractionPacket(NetId, PlugInteractionType.PickedUp); + } + + private void OnUngrabbed(ControlImplBase control) + { + Multiplayer.LogDebug(() => $"NetworkedPluggableObject.OnUngrabbed() [{transform.parent.name}, {NetId}] station: {Station?.StationName}"); + NetworkLifecycle.Instance.Client?.SendPitStopPlugInteractionPacket(NetId, PlugInteractionType.Dropped); + } + + private void OnPlugged(PluggableObject plug, PlugSocket socket) + { + Multiplayer.LogDebug(() => $"NetworkedPluggableObject.OnPlugged() [{transform.parent.name}, {NetId}] station: {Station?.StationName}"); + + PlugInteractionType interaction; + bool left = false; + ushort trainCarNetId = 0; + + if (socket == plug.startAttachedTo) + interaction = PlugInteractionType.DockHome; + else + { + var trainCar = TrainCar.Resolve(socket.gameObject); + if(trainCar != null) + { + if(!NetworkedTrainCar.TryGetFromTrainCar(trainCar, out var netTrainCar)) + { + Multiplayer.LogDebug(() => $"NetworkedPluggableObject.OnPlugged() NetworkedTrainCar: {trainCar?.ID} Not Found! Socket: {socket.GetObjectPath()}"); + return; + } + + trainCarNetId = netTrainCar.NetId; + + interaction = PlugInteractionType.DockSocket; + var sockets = trainCar.GetComponentsInChildren(); + if (socket = sockets[0]) + left = true; + } + else + { + Multiplayer.LogDebug(() => $"NetworkedPluggableObject.OnPlugged() Socket not recognised: {socket.GetObjectPath()}"); + return; + } + } + + NetworkLifecycle.Instance.Client?.SendPitStopPlugInteractionPacket(NetId, interaction, trainCarNetId, left); + } + #endregion +} From e9df7cad5e48ceaf73848ef5ba431bff7dc32423 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 2 Feb 2025 15:26:29 +1000 Subject: [PATCH 234/521] Add ability for NetworkedPlayers to hold items --- .../Networking/Player/NetworkedPlayer.cs | 63 +++++++++++++++++++ .../Managers/Client/ClientPlayerManager.cs | 2 + .../Managers/Client/NetworkClient.cs | 5 ++ 3 files changed, 70 insertions(+) diff --git a/Multiplayer/Components/Networking/Player/NetworkedPlayer.cs b/Multiplayer/Components/Networking/Player/NetworkedPlayer.cs index 561267aa..163cdd59 100644 --- a/Multiplayer/Components/Networking/Player/NetworkedPlayer.cs +++ b/Multiplayer/Components/Networking/Player/NetworkedPlayer.cs @@ -1,12 +1,35 @@ using System; +using DV.Player; using Multiplayer.Components.Networking.Train; using Multiplayer.Editor.Components.Player; using UnityEngine; namespace Multiplayer.Components.Networking.Player; +/// +/// Represents a networked player in the multiplayer environment, handling movement, item holding, and visual state +/// public class NetworkedPlayer : MonoBehaviour { + #region Static Setup + private static Vector3 itemAnchorOffset; + + /// + /// Captures the standard offset position for held items relative to the player transform + /// for mapping to a NetworkedPlayer + /// This must be called as soon as the world is loaded, before the local player moves or crouches + /// + public static void CaptureItemAnchorOffset() + { + //todo: there's some minor inconsistency with return values and may be related to: + // - the direction/rotation of the camera + // - player loading status (maybe posistion hasn't settled yet) + itemAnchorOffset = PlayerManager.PlayerTransform.InverseTransformPoint(ItemPositionController.Instance.itemAnchor.position); + Multiplayer.LogDebug(() => $"NetworkedPlayer.CaptureItemAnchorOffset() itemAnchorOffset: {itemAnchorOffset}"); + } + + #endregion + private const float LERP_SPEED = 5.0f; public byte Id; @@ -33,6 +56,10 @@ public string Username { private Quaternion targetRotation; private Vector2 moveDir; private Vector2 targetMoveDir; + + private GameObject itemHeld; + private Vector3? itemHoldPos; + private Quaternion? itemHoldRot; private void Awake() { @@ -89,6 +116,12 @@ private void Update() selfTransform.position = position; selfTransform.rotation = rotation; } + + if (itemHeld != null) + { + itemHeld.transform.position = selfTransform.position + GetItemOffsetFromPlayer(); + itemHeld.transform.rotation = selfTransform.rotation * (itemHoldRot ?? ItemPositionController.Instance.itemAnchor.localRotation); + } } public void UpdatePosition(Vector3 position, Vector2 moveDir, float rotation, bool isJumping, bool movePacketIsOnCar) @@ -118,4 +151,34 @@ public void UpdateCar(ushort netId) targetPos = isOnCar ? transform.localPosition : selfTransform.position; targetRotation = isOnCar ? transform.localRotation : selfTransform.rotation; } + + /// + /// Sets the player's currently held item with optional position and rotation offsets + /// + /// The item GameObject to hold + /// Optional local position offset + /// Optional local rotation offset + public void HoldItem(GameObject itemGo, Vector3? targetPos = null, Quaternion? targetRot = null) + { + Multiplayer.LogDebug(() => $"NetworkedPlayer.HoldItem({itemGo.GetPath()}) Player: {username}, Before position: {itemGo.transform.localPosition}, rotation: {itemGo.transform.localRotation}, Target pos: {targetPos}, Target rot: {targetRot}"); + + itemHeld = itemGo; + itemHoldPos = targetPos; + itemHoldRot = targetRot; + } + + public void DropItem() + { + itemHeld = null; + itemHoldPos = null; + itemHoldRot = null; + } + + private Vector3 GetItemOffsetFromPlayer() + { + Vector3 baseOffset = itemAnchorOffset; + Vector3 finalOffset = itemHoldPos.HasValue ? baseOffset + itemHoldPos.Value : baseOffset; + return selfTransform.TransformDirection(finalOffset); + } + } diff --git a/Multiplayer/Networking/Managers/Client/ClientPlayerManager.cs b/Multiplayer/Networking/Managers/Client/ClientPlayerManager.cs index bcd74451..287c7114 100644 --- a/Multiplayer/Networking/Managers/Client/ClientPlayerManager.cs +++ b/Multiplayer/Networking/Managers/Client/ClientPlayerManager.cs @@ -1,6 +1,8 @@ using System; +using System.Collections; using System.Collections.Generic; using DV; +using DV.Player; using Multiplayer.Components.Networking; using Multiplayer.Components.Networking.Player; using UnityEngine; diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 287fbc0c..912ffa51 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -70,6 +70,11 @@ public class NetworkClient : NetworkManager public NetworkClient(Settings settings) : base(settings) { ClientPlayerManager = new ClientPlayerManager(); + + WorldStreamingInit.LoadingFinished += () => + { + NetworkedPlayer.CaptureItemAnchorOffset(); + }; } public void Start(string address, int port, string password, bool isSinglePlayer, Action onDisconnect) From b8ebc404a893ad40efa466b1c03ef8a96f77d9b8 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 2 Feb 2025 15:27:59 +1000 Subject: [PATCH 235/521] Fix issues with init and destroy --- .../Networking/World/NetworkedPitStopStation.cs | 11 +++++++---- .../Networking/World/NetworkedPluggableObject.cs | 5 ++++- .../Networking/Managers/Client/NetworkClient.cs | 3 ++- .../World/CommonPitStopPlugInteractionPacket.cs | 2 -- 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs b/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs index fb9efe2e..4e6fe63f 100644 --- a/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs +++ b/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs @@ -60,7 +60,7 @@ public static void InitialisePitStops() if (netPitStopStationToLocation.Count != 0) return; - var stations = Resources.FindObjectsOfTypeAll(); + var stations = Resources.FindObjectsOfTypeAll().Where(p => p.transform.parent != null).ToArray(); Multiplayer.LogDebug(() => $"InitialisePitStops() Found: {stations?.Length}"); @@ -113,7 +113,10 @@ protected override void Awake() protected override void OnDestroy() { - netPitStopStationToLocation.Remove(transform.position); + if (UnloadWatcher.isUnloading) + netPitStopStationToLocation.Clear(); + else + netPitStopStationToLocation.Remove(transform.position); if (carSelectorGrab != null) { @@ -237,6 +240,7 @@ public bool TryGetPluggable(ResourceType type, out NetworkedPluggableObject netP { return resourceToPluggableObject.TryGetValue(type, out netPluggable); } + /// /// Initializes the pit stop station and sets up event handlers for grab interactions. /// @@ -361,8 +365,7 @@ public void ProcessPacket(CommonPitStopInteractionPacket packet) case PitStopStationInteractionType.Ungrab: //allow interaction - if (grab != null) - grab.SetMovingDisabled(true); + grab?.SetMovingDisabled(true); if (resourceType != null && resourceType != 0 && resourceModule != null) { diff --git a/Multiplayer/Components/Networking/World/NetworkedPluggableObject.cs b/Multiplayer/Components/Networking/World/NetworkedPluggableObject.cs index 881eda93..4ae57173 100644 --- a/Multiplayer/Components/Networking/World/NetworkedPluggableObject.cs +++ b/Multiplayer/Components/Networking/World/NetworkedPluggableObject.cs @@ -57,7 +57,10 @@ private IEnumerator Start() protected override void OnDestroy() { - plugToStation.Remove(this); + if (UnloadWatcher.isUnloading) + plugToStation.Clear(); + else + plugToStation.Remove(this); if (PluggableObject?.controlBase != null && handlersInitialised) { diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 912ffa51..43f40719 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -986,11 +986,12 @@ private void OnCommonPitStopPlugInteractionPacket(CommonPitStopPlugInteractionPa if (!NetworkedPluggableObject.Get(packet.NetId, out var netPlug)) { LogWarning($"Pit Stop Plug Interaction received for plug netId: {packet.NetId}, but pit stop plug does not exist!"); + return; } Log($"Pit Stop Plug Interaction received for {netPlug}"); - LogDebug(() => $"OnCommonPitStopPlugInteractionPacket() [{netPlug?.transform?.parent?.name}, {packet.NetId}], interaction: [{packet.InteractionType}]"); + LogDebug(() => $"OnCommonPitStopPlugInteractionPacket() [{netPlug?.transform?.parent?.name}, {packet.NetId}], interaction: [{(PlugInteractionType)packet.InteractionType}]"); netPlug.ProcessPacket(packet); } diff --git a/Multiplayer/Networking/Packets/Clientbound/World/CommonPitStopPlugInteractionPacket.cs b/Multiplayer/Networking/Packets/Clientbound/World/CommonPitStopPlugInteractionPacket.cs index 9266102f..9cd071ab 100644 --- a/Multiplayer/Networking/Packets/Clientbound/World/CommonPitStopPlugInteractionPacket.cs +++ b/Multiplayer/Networking/Packets/Clientbound/World/CommonPitStopPlugInteractionPacket.cs @@ -13,6 +13,4 @@ public class CommonPitStopPlugInteractionPacket public byte PlayerId { get; set; } public ushort TrainCarNetId { get; set; } public bool IsLeftSide { get; set; } - //public Vector3 Position { get; set; } - //public Quaternion Rotation { get; set; } } From 784e96610810fb933f97ba1421fbc04b57461e3d Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 2 Feb 2025 15:28:57 +1000 Subject: [PATCH 236/521] Fix multiple NetworkedPluggableObject issues, add remote grabbing --- .../World/NetworkedPluggableObject.cs | 100 ++++++++++++++++-- 1 file changed, 91 insertions(+), 9 deletions(-) diff --git a/Multiplayer/Components/Networking/World/NetworkedPluggableObject.cs b/Multiplayer/Components/Networking/World/NetworkedPluggableObject.cs index 4ae57173..e56a631e 100644 --- a/Multiplayer/Components/Networking/World/NetworkedPluggableObject.cs +++ b/Multiplayer/Components/Networking/World/NetworkedPluggableObject.cs @@ -1,4 +1,6 @@ using DV.CabControls; +using DV.Interaction; +using Multiplayer.Components.Networking.Player; using Multiplayer.Components.Networking.Train; using Multiplayer.Networking.Data; using Multiplayer.Networking.Packets.Clientbound.World; @@ -30,6 +32,8 @@ public static bool Get(ushort netId, out NetworkedPluggableObject obj) public PluggableObject PluggableObject { get; private set; } public NetworkedPitStopStation Station { get; private set; } + private GrabHandlerGizmoItem grabHandler; + private bool handlersInitialised = false; private byte playerHolding = 0; @@ -48,6 +52,8 @@ private IEnumerator Start() { yield return new WaitUntil(() => PluggableObject?.controlBase != null); + grabHandler = this.GetComponent(); + PluggableObject.controlBase.Grabbed += OnGrabbed; PluggableObject.controlBase.Ungrabbed += OnUngrabbed; PluggableObject.PluggedIn += OnPlugged; @@ -88,6 +94,9 @@ public bool ValidateInteraction(CommonPitStopPlugInteractionPacket packet) public void ProcessPacket(CommonPitStopPlugInteractionPacket packet) { var interaction = (PlugInteractionType)packet.InteractionType; + bool result; + + NetworkedPlayer player = null; switch (interaction) { @@ -96,28 +105,81 @@ public void ProcessPacket(CommonPitStopPlugInteractionPacket packet) break; case PlugInteractionType.PickedUp: + //Handle the picked up state isGrabbed = true; playerHolding = packet.PlayerId; PluggableObject.controlGrabbed = true; BlockInteraction(true); + + PluggableObject.Unplug(); + + Multiplayer.LogDebug(() => $"ProcessPacket() NetId: {NetId}, Picked Up, player: {playerHolding}"); + + //attach to a player + if (NetworkLifecycle.Instance.IsClientRunning && + NetworkLifecycle.Instance.Client.ClientPlayerManager.TryGetPlayer(playerHolding, out player)) + { + var target = grabHandler?.customGrabAnchor?.GetGrabAnchor(); + player.HoldItem(gameObject, target?.localPos, target?.localRot); + } break; case PlugInteractionType.Dropped: + Multiplayer.LogDebug(() => $"ProcessPacket() NetId: {NetId}, Dropped"); + + if (isGrabbed) + { + if (NetworkLifecycle.Instance.IsClientRunning && + NetworkLifecycle.Instance.Client.ClientPlayerManager.TryGetPlayer(playerHolding, out player)) + { + player.DropItem(); + } + } + isGrabbed = false; playerHolding = 0; PluggableObject.controlGrabbed = false; BlockInteraction(false); + + PluggableObject.Unplug(); + break; case PlugInteractionType.DockHome: + Multiplayer.LogDebug(() => $"ProcessPacket() NetId: {NetId}, DockHome"); + + if (isGrabbed) + { + if (NetworkLifecycle.Instance.IsClientRunning && + NetworkLifecycle.Instance.Client.ClientPlayerManager.TryGetPlayer(playerHolding, out player)) + { + player.DropItem(); + } + } + isGrabbed = false; playerHolding = 0; PluggableObject.controlGrabbed = false; - PluggableObject.InstantSnapTo(PluggableObject.startAttachedTo); BlockInteraction(false); + + PluggableObject.Unplug(); + + result = PluggableObject.InstantSnapTo(PluggableObject.startAttachedTo); + Multiplayer.LogDebug(() => $"ProcessPacket() NetId: {NetId}, DockHome, result: {result}"); break; case PlugInteractionType.DockSocket: + Multiplayer.LogDebug(() => $"ProcessPacket() NetId: {NetId}, DockSocket, trainCar: {packet.TrainCarNetId}, isLeft: {packet.IsLeftSide}"); + + if (isGrabbed) + { + if (NetworkLifecycle.Instance.IsClientRunning && + NetworkLifecycle.Instance.Client.ClientPlayerManager.TryGetPlayer(playerHolding, out player)) + { + player.DropItem(); + } + } + if (NetworkedTrainCar.GetTrainCar(packet.TrainCarNetId, out var trainCar)) { isGrabbed = false; @@ -125,11 +187,23 @@ public void ProcessPacket(CommonPitStopPlugInteractionPacket packet) PluggableObject.controlGrabbed = false; BlockInteraction(false); + PluggableObject.Unplug(); + var sockets = trainCar.GetComponentsInChildren(); if (packet.IsLeftSide) - PluggableObject.InstantSnapTo(sockets[0]); + { + result = PluggableObject.InstantSnapTo(sockets[0]); + Multiplayer.LogDebug(() => $"ProcessPacket() NetId: {NetId}, DockSocket, trainCar: {packet.TrainCarNetId}, isLeft: {packet.IsLeftSide}, result: {result}"); + } else - PluggableObject.InstantSnapTo(sockets[1]); + { + result = PluggableObject.InstantSnapTo(sockets[1]); + Multiplayer.LogDebug(() => $"ProcessPacket() NetId: {NetId}, DockSocket, trainCar: {packet.TrainCarNetId}, isLeft: {packet.IsLeftSide}, result: {result}"); + } + } + else + { + Multiplayer.LogDebug(() => $"ProcessPacket() NetId: {NetId}, DockSocket, trainCar: {packet.TrainCarNetId}. TrainCar not found!"); } break; } @@ -137,14 +211,13 @@ public void ProcessPacket(CommonPitStopPlugInteractionPacket packet) private void BlockInteraction(bool block) { - var rigid = GetComponentInChildren(); - - if (rigid) - rigid.isKinematic = !block; - if (block) + { + PluggableObject.DisableStandaloneComponents(); PluggableObject.DisableColliders(); + } else + PluggableObject.EnableStandaloneComponents(); PluggableObject.EnableColliders(); } @@ -164,18 +237,27 @@ public void InitPitStop(NetworkedPitStopStation netPitStop) #region Client private void OnGrabbed(ControlImplBase control) { + if (NetworkLifecycle.Instance.IsProcessingPacket) + return; + Multiplayer.LogDebug(() => $"NetworkedPluggableObject.OnGrabbed() [{transform.parent.name}, {NetId}] station: {Station?.StationName}"); NetworkLifecycle.Instance.Client?.SendPitStopPlugInteractionPacket(NetId, PlugInteractionType.PickedUp); } private void OnUngrabbed(ControlImplBase control) { + if (NetworkLifecycle.Instance.IsProcessingPacket) + return; + Multiplayer.LogDebug(() => $"NetworkedPluggableObject.OnUngrabbed() [{transform.parent.name}, {NetId}] station: {Station?.StationName}"); NetworkLifecycle.Instance.Client?.SendPitStopPlugInteractionPacket(NetId, PlugInteractionType.Dropped); } private void OnPlugged(PluggableObject plug, PlugSocket socket) { + if (NetworkLifecycle.Instance.IsProcessingPacket) + return; + Multiplayer.LogDebug(() => $"NetworkedPluggableObject.OnPlugged() [{transform.parent.name}, {NetId}] station: {Station?.StationName}"); PlugInteractionType interaction; @@ -199,7 +281,7 @@ private void OnPlugged(PluggableObject plug, PlugSocket socket) interaction = PlugInteractionType.DockSocket; var sockets = trainCar.GetComponentsInChildren(); - if (socket = sockets[0]) + if (socket == sockets[0]) left = true; } else From e82b6b56fd34c4ccc6c0fafe96413ce8ccb55f17 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 2 Feb 2025 15:31:00 +1000 Subject: [PATCH 237/521] Ensure DV.Interaction is accessible --- Multiplayer/Multiplayer.csproj | 3 ++- info.json | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Multiplayer/Multiplayer.csproj b/Multiplayer/Multiplayer.csproj index 537fb85c..137a972e 100644 --- a/Multiplayer/Multiplayer.csproj +++ b/Multiplayer/Multiplayer.csproj @@ -3,7 +3,7 @@ net48 latest Multiplayer - 0.1.10.0 + 0.1.11.0 @@ -13,6 +13,7 @@ + diff --git a/info.json b/info.json index 7857c50c..fdb6281f 100644 --- a/info.json +++ b/info.json @@ -1,6 +1,6 @@ { "Id": "Multiplayer", - "Version": "0.1.10.0", + "Version": "0.1.11.0", "DisplayName": "Multiplayer", "Author": "Insprill, Macka, Morm", "EntryMethod": "Multiplayer.Multiplayer.Load", From 7d497f27f5abda9f14f0a731627b52335dc716b1 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 2 Feb 2025 15:42:18 +1000 Subject: [PATCH 238/521] Add clamping to values --- .../Networking/World/NetworkedPitStopStation.cs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs b/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs index 4e6fe63f..82142902 100644 --- a/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs +++ b/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs @@ -192,7 +192,7 @@ protected void LateUpdate() { float previous = grabbedModule.Data.unitsToBuy; //grabbedModule.Data.unitsToBuy = lastRemoteValue; - grabbedModule.SetUnitsToBuy(lastRemoteValue); + SetUnits(grabbedModule, lastRemoteValue); Multiplayer.LogDebug(() => $"NetworkedPitStopStation.LateUpdate() wasRemoteGrabbed: {wasRemoteGrabbed}, previous: {previous}, new: {lastRemoteValue}"); @@ -371,7 +371,7 @@ public void ProcessPacket(CommonPitStopInteractionPacket packet) { lastRemoteValue = packet.State; //resourceModule.Data.unitsToBuy = lastRemoteValue; - resourceModule.SetUnitsToBuy(lastRemoteValue); + SetUnits(resourceModule, lastRemoteValue); } isRemoteGrabbed = false; @@ -386,7 +386,7 @@ public void ProcessPacket(CommonPitStopInteractionPacket packet) { lastRemoteValue = packet.State; //resourceModule.Data.unitsToBuy = lastRemoteValue; - resourceModule.SetUnitsToBuy(lastRemoteValue); + SetUnits(resourceModule, lastRemoteValue); } } break; @@ -404,6 +404,15 @@ public void ProcessPacket(CommonPitStopInteractionPacket packet) break; } } + + private void SetUnits(LocoResourceModule rm, float units) + { + if (rm == null) + return; + + float clamped = Mathf.Clamp(units, rm.AbsoluteMinValue, rm.AbsoluteMaxValue); + rm.SetUnitsToBuy(clamped); + } #endregion #region Client From dbd532ff15d402fa2a1ab690c67ffdaab753c577 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 2 Feb 2025 22:38:54 +1000 Subject: [PATCH 239/521] Use local logging --- .../Managers/Client/NetworkClient.cs | 10 ++++----- .../Managers/Server/NetworkServer.cs | 22 +++++++++---------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 43f40719..63691a47 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -778,7 +778,7 @@ private void OnClientboundCargoStatePacket(ClientboundCargoStatePacket packet) if (logicCar == null) { - Multiplayer.LogWarning($"OnClientboundCargoStatePacket() Failed to find logic car for [{networkedTrainCar.TrainCar.ID}, {packet.NetId}] is initialised: {networkedTrainCar.Client_Initialized}"); + LogWarning($"OnClientboundCargoStatePacket() Failed to find logic car for [{networkedTrainCar.TrainCar.ID}, {packet.NetId}] is initialised: {networkedTrainCar.Client_Initialized}"); return; } @@ -1001,7 +1001,7 @@ private void OnCommonItemChangePacket(CommonItemChangePacket packet) //LogDebug(() => $"OnCommonItemChangePacket({packet?.Items?.Count})"); - //Multiplayer.LogDebug(() => + //LogDebug(() => //{ // string debug = ""; @@ -1398,7 +1398,7 @@ public void SendChat(string message) public void SendPitStopInteractionPacket(ushort netId, PitStopStationInteractionType interaction, ResourceType? resource, float state) { - Multiplayer.LogDebug(()=>$"SendPitStopInteractionPacket({netId}, [{interaction}], {resource}, {state})"); + LogDebug(()=>$"SendPitStopInteractionPacket({netId}, [{interaction}], {resource}, {state})"); int res = resource == null ? 0 : (int)resource; SendPacketToServer(new CommonPitStopInteractionPacket @@ -1412,7 +1412,7 @@ public void SendPitStopInteractionPacket(ushort netId, PitStopStationInteraction public void SendPitStopPlugInteractionPacket(ushort netId, PlugInteractionType interaction, ushort trainCarNetId = 0, bool left = false) { - Multiplayer.LogDebug(()=>$"SendPitStopInteractionPacket({netId}, {interaction}, {trainCarNetId}, {left})"); + LogDebug(()=>$"SendPitStopInteractionPacket({netId}, {interaction}, {trainCarNetId}, {left})"); SendPacketToServer(new CommonPitStopPlugInteractionPacket { @@ -1426,7 +1426,7 @@ public void SendPitStopPlugInteractionPacket(ushort netId, PlugInteractionType i public void SendItemsChangePacket(List items) { - Multiplayer.Log($"Sending SendItemsChangePacket with {items.Count()} items"); + Log($"Sending SendItemsChangePacket with {items.Count()} items"); //SendPacketToServer(new CommonItemChangePacket { Items = items }, // DeliveryMethod.ReliableUnordered); diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index 647304ed..14c825d9 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -85,7 +85,7 @@ public override bool Start(int port) WorldStreamingInit.LoadingFinished += OnLoaded; - Multiplayer.Log($"Starting server..."); + 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)) { @@ -365,7 +365,7 @@ public void SendDestroyTrainCar(ushort netId, ITransportPeer peer = null) if (netId == 0) { - Multiplayer.LogWarning($"SendDestroyTrainCar failed. netId {netId}"); + LogWarning($"SendDestroyTrainCar failed. netId {netId}"); return; } @@ -395,7 +395,7 @@ public void SendBrakeState(ushort netId, float mainReservoirPressure, float brak Temperature = temperature }, DeliveryMethod.ReliableOrdered, SelfPeer); - //Multiplayer.LogDebug(()=> $"Sending Brake Pressures netId {netId}: {mainReservoirPressure}, {independentPipePressure}, {brakePipePressure}, {brakeCylinderPressure}"); + //LogDebug(()=> $"Sending Brake Pressures netId {netId}: {mainReservoirPressure}, {independentPipePressure}, {brakePipePressure}, {brakeCylinderPressure}"); } public void SendFireboxState(ushort netId, float fireboxContents, bool fireboxOn) @@ -407,7 +407,7 @@ public void SendFireboxState(ushort netId, float fireboxContents, bool fireboxOn IsOn = fireboxOn }, DeliveryMethod.ReliableOrdered, SelfPeer); - Multiplayer.LogDebug(() => $"Sending Firebox States netId {netId}: {fireboxContents}, {fireboxOn}"); + LogDebug(() => $"Sending Firebox States netId {netId}: {fireboxContents}, {fireboxOn}"); } public void SendCargoState(TrainCar trainCar, ushort netId, bool isLoading, byte cargoModelIndex) @@ -497,7 +497,7 @@ public void SendDebtStatus(bool hasDebt) public void SendJobsCreatePacket(NetworkedStationController networkedStation, NetworkedJob[] jobs, ITransportPeer peer = null) { - Multiplayer.Log($"Sending JobsCreatePacket for stationNetId {networkedStation.NetId} with {jobs.Count()} jobs"); + Log($"Sending JobsCreatePacket for stationNetId {networkedStation.NetId} with {jobs.Count()} jobs"); var packet = ClientboundJobsCreatePacket.FromNetworkedJobs(networkedStation, jobs); @@ -509,13 +509,13 @@ public void SendJobsCreatePacket(NetworkedStationController networkedStation, Ne public void SendJobsUpdatePacket(ushort stationNetId, NetworkedJob[] jobs) { - Multiplayer.Log($"Sending JobsUpdatePacket for stationNetId {stationNetId} with {jobs.Count()} jobs"); + Log($"Sending JobsUpdatePacket for stationNetId {stationNetId} with {jobs.Count()} jobs"); SendPacketToAll(ClientboundJobsUpdatePacket.FromNetworkedJobs(stationNetId, jobs), DeliveryMethod.ReliableOrdered, SelfPeer); } public void SendItemsChangePacket(List items, ServerPlayer player) { - Multiplayer.Log($"Sending SendItemsChangePacket with {items.Count()} items to {player.Username}"); + Log($"Sending SendItemsChangePacket with {items.Count()} items to {player.Username}"); if (Peers.TryGetValue(player.Id, out ITransportPeer peer) && peer != SelfPeer) { @@ -840,7 +840,7 @@ private void OnCommonCouplerInteractionPacket(CommonCouplerInteractionPacket pac } else { - Multiplayer.LogDebug(() => $"OnCommonCouplerInteractionPacket([{packet.Flags}, {netTrainCar.CurrentID}, {packet.NetId}], {peer.Id}) Sending validation failure"); + LogDebug(() => $"OnCommonCouplerInteractionPacket([{packet.Flags}, {netTrainCar.CurrentID}, {packet.NetId}], {peer.Id}) Sending validation failure"); //failed validation notify client SendPacket( peer, @@ -856,7 +856,7 @@ private void OnCommonCouplerInteractionPacket(CommonCouplerInteractionPacket pac } else { - Multiplayer.LogDebug(() => $"OnCommonCouplerInteractionPacket([{packet.Flags}, {netTrainCar.CurrentID}, {packet.NetId}], {peer.Id}) Sending destroy"); + 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); } @@ -1131,7 +1131,7 @@ private void OnCommonChatPacket(CommonChatPacket packet, ITransportPeer peer) #region Unconnected Packet Handling private void OnUnconnectedPingPacket(UnconnectedPingPacket packet, IPEndPoint endPoint) { - //Multiplayer.Log($"OnUnconnectedPingPacket({endPoint.Address})"); + //Log($"OnUnconnectedPingPacket({endPoint.Address})"); //SendUnconnectedPacket(packet, endPoint.Address.ToString(), endPoint.Port); } @@ -1197,7 +1197,7 @@ private void OnCommonItemChangePacket(CommonItemChangePacket packet, ITransportP //LogDebug(()=>$"OnCommonItemChangePacket({packet?.Items?.Count}, {peer.Id} (\"{player.Username}\"))"); - //Multiplayer.LogDebug(() => + //LogDebug(() => //{ // string debug = ""; From 1abbf7bdbc2a28a405416d647efeef874a755604 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 2 Feb 2025 22:44:10 +1000 Subject: [PATCH 240/521] Begin work on pit stop bulk update and player culling --- .../World/NetworkedPitStopStation.cs | 137 +++++++++++++++++- .../World/NetworkedPluggableObject.cs | 53 ++++++- .../Networking/Data/PitStopPlugData.cs | 101 +++++++++++++ .../Networking/Data/PitStopStationData.cs | 40 +++++ .../Managers/Client/NetworkClient.cs | 14 ++ .../Managers/Server/NetworkServer.cs | 18 ++- .../ClientboundPitStopBulkUpdatePacket.cs | 14 ++ 7 files changed, 372 insertions(+), 5 deletions(-) create mode 100644 Multiplayer/Networking/Data/PitStopPlugData.cs create mode 100644 Multiplayer/Networking/Data/PitStopStationData.cs create mode 100644 Multiplayer/Networking/Packets/Clientbound/World/ClientboundPitStopBulkUpdatePacket.cs diff --git a/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs b/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs index 82142902..51fcd8d0 100644 --- a/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs +++ b/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs @@ -10,7 +10,7 @@ using DV.ThingTypes; using System.Collections; using System.Linq; - +using Multiplayer.Networking.Packets.Clientbound.World; namespace Multiplayer.Components.Networking.World; @@ -83,7 +83,17 @@ public static void InitialisePitStops() const float MAX_DELTA = 0.2f; const float MIN_UPDATE_TIME = 0.1f; + const float LOADING_TIMEOUT = 5f; + + const float DEFAULT_DISABLER_SQR_DISTANCE = 250000f; + #region Server variables + private Dictionary playerToLastNearbyTime; + private Dictionary playerToInitialised; + private float disablerSqrDistance = DEFAULT_DISABLER_SQR_DISTANCE; + #endregion + + #region Common variables public PitStopStation Station { get; set; } public string StationName { get; private set; } @@ -103,12 +113,33 @@ public static void InitialisePitStops() private RotaryAmplitudeChecker grabbedAmplitudeChecker; private float lastUnitsToBuy; + private bool Refreshed = false; + #endregion + #region Unity protected override void Awake() { base.Awake(); StationName = $"{transform.parent.parent.name} - {transform.parent.name}"; + + if (NetworkLifecycle.Instance.IsHost()) + { + playerToInitialised = []; + playerToLastNearbyTime = []; + + var disabler = GetComponentInParent(); + if (disabler != null) + disablerSqrDistance = disabler.disableSqrDistance; + + NetworkLifecycle.Instance.OnTick += PlayerDistanceChecker; + } + } + + protected void OnDisable() + { + if (!NetworkLifecycle.Instance.IsHost()) + Refreshed = false; } protected override void OnDestroy() @@ -229,6 +260,49 @@ public void OnPlayerDisconnect(ITransportPeer peer) //Multiplayer.LogWarning($"OnPlayerDisconnect()"); } + private void PlayerDistanceChecker(uint tick) + { + //if not active then there is no one close by + if (!gameObject.activeInHierarchy || Station == null || Station.pitstop == null) + return; + + foreach (var player in NetworkLifecycle.Instance.Server.ServerPlayers) + { + if (!player.IsLoaded) + continue; + + float sqrDistance = (player.WorldPosition - transform.position).sqrMagnitude; + + if (sqrDistance <= disablerSqrDistance) + { + if(playerToInitialised.TryGetValue(player.Id, out var initialised) && initialised) + continue; + + playerToInitialised[player.Id] = false; + + if (Station.pitstop.IsCarInPitStop()) + { + PitStopStationData[] stateData = new PitStopStationData[Station.pitstop.carList.Count]; + int i; + for (i = 0; i < Station.pitstop.paramsList.Count; i++) + { + stateData[i] = PitStopStationData.From(Station.pitstop.paramsList[i]); + } + + PitStopPlugData[] plugData = new PitStopPlugData[resourceToPluggableObject.Count]; + i = 0; + foreach(var plug in resourceToPluggableObject) + { + plugData[i] = PitStopPlugData.From(plug.Value); + i++; + } + + NetworkLifecycle.Instance.Server.SendPitStopBulkDataPacket(NetId, stateData, plugData); + } + } + } + } + #endregion @@ -405,6 +479,35 @@ public void ProcessPacket(CommonPitStopInteractionPacket packet) } } + public void ProcessBulkUpdate(ClientboundPitStopBulkUpdatePacket packet) + { + if (Station?.pitstop?.carList == null || packet.PitStopData.Count() < Station.pitstop.carList.Count) + { + StartCoroutine(WaitForLoad(packet)); + return; + } + + //todo: process the packet!! + } + + private IEnumerator WaitForLoad(ClientboundPitStopBulkUpdatePacket packet) + { + float time = Time.time; + + yield return new WaitUntil(() => + (Station?.pitstop?.carList == null || packet.PitStopData.Count() < Station.pitstop.carList.Count) + || (Time.time - time) > LOADING_TIMEOUT + ); + + if ((Time.time - time) < LOADING_TIMEOUT) + { + ProcessBulkUpdate(packet); + + } + else + Multiplayer.LogWarning($"NetworkedPitStopStation.WaitForLoad() Station {StationName} failed to process bulk update"); + } + private void SetUnits(LocoResourceModule rm, float units) { if (rm == null) @@ -421,6 +524,13 @@ private void SetUnits(LocoResourceModule rm, float units) /// private void CarSelectorGrabbed() { + if (NetworkLifecycle.Instance.IsProcessingPacket) + return; + + //Prevent new players/players entering the area from sending packets until initalised + if (!Refreshed) + return; + Multiplayer.LogDebug(() => $"CarSelectorGrabbed() {StationName}"); NetworkLifecycle.Instance?.Client.SendPitStopInteractionPacket(NetId, PitStopStationInteractionType.Grab, null, 0); } @@ -430,6 +540,13 @@ private void CarSelectorGrabbed() /// private void CarSelectorUnGrabbed() { + if (NetworkLifecycle.Instance.IsProcessingPacket) + return; + + //Prevent new players/players entering the area from sending packets until initalised + if (!Refreshed) + return; + Multiplayer.LogDebug(() => $"CarSelectorUnGrabbed() {StationName}"); NetworkLifecycle.Instance?.Client.SendPitStopInteractionPacket(NetId, PitStopStationInteractionType.Ungrab, null, Station.pitstop.SelectedIndex); } @@ -442,6 +559,10 @@ private void CarSelected() if (NetworkLifecycle.Instance.IsProcessingPacket) return; + //Prevent new players/players entering the area from sending packets until initalised + if (!Refreshed) + return; + Multiplayer.LogDebug(() => $"CarSelected() selected: {Station.pitstop.SelectedIndex}"); NetworkLifecycle.Instance?.Client.SendPitStopInteractionPacket(NetId, PitStopStationInteractionType.SelectCar, null, Station.pitstop.SelectedIndex); @@ -453,6 +574,13 @@ private void CarSelected() /// The resource module being grabbed. private void LeverGrabbed(LocoResourceModule module) { + if (NetworkLifecycle.Instance.IsProcessingPacket) + return; + + //Prevent new players/players entering the area from sending packets until initalised + if (!Refreshed) + return; + Multiplayer.LogDebug(() => $"LeverGrabbed() {StationName}, module: {module.resourceType}"); isGrabbed = true; wasGrabbed = true; @@ -468,6 +596,13 @@ private void LeverGrabbed(LocoResourceModule module) /// The resource module being grabbed. private void LeverUnGrabbed(LocoResourceModule module) { + if (NetworkLifecycle.Instance.IsProcessingPacket) + return; + + //Prevent new players/players entering the area from sending packets until initalised + if (!Refreshed) + return; + Multiplayer.LogDebug(() => $"LeverUnGrabbed() {StationName}, module: {module.resourceType}"); isGrabbed = false; } diff --git a/Multiplayer/Components/Networking/World/NetworkedPluggableObject.cs b/Multiplayer/Components/Networking/World/NetworkedPluggableObject.cs index e56a631e..17e3767e 100644 --- a/Multiplayer/Components/Networking/World/NetworkedPluggableObject.cs +++ b/Multiplayer/Components/Networking/World/NetworkedPluggableObject.cs @@ -3,14 +3,12 @@ using Multiplayer.Components.Networking.Player; using Multiplayer.Components.Networking.Train; using Multiplayer.Networking.Data; -using Multiplayer.Networking.Packets.Clientbound.World; using Multiplayer.Networking.Packets.Common; using Multiplayer.Utils; using System; using System.Collections; using System.Collections.Generic; using System.Linq; -using System.Text; using UnityEngine; namespace Multiplayer.Components.Networking.World; @@ -29,6 +27,13 @@ public static bool Get(ushort netId, out NetworkedPluggableObject obj) protected override bool IsIdServerAuthoritative => true; + #region Server Variables + public PlugInteractionType CurrentInteration { get; set; } + public ServerPlayer HeldBy { get; private set; } + public ushort TrainCarNetId { get; private set; } + public bool IsConnectedLeft { get; private set; } + + #endregion public PluggableObject PluggableObject { get; private set; } public NetworkedPitStopStation Station { get; private set; } @@ -39,6 +44,8 @@ public static bool Get(ushort netId, out NetworkedPluggableObject obj) private byte playerHolding = 0; private bool isGrabbed = false; + private bool Refreshed = false; + #region Unity protected override void Awake() { @@ -81,9 +88,37 @@ protected override void OnDestroy() #region Server - public bool ValidateInteraction(CommonPitStopPlugInteractionPacket packet) + public bool ValidateInteraction(CommonPitStopPlugInteractionPacket packet, ServerPlayer player) { + PlugInteractionType interactionType = (PlugInteractionType)packet.InteractionType; //todo: implement validation code (player distance, player interacting, etc.) + + //validate and update + CurrentInteration = interactionType; + + if (interactionType == PlugInteractionType.DockSocket) + { + TrainCarNetId = packet.TrainCarNetId; + IsConnectedLeft = packet.IsLeftSide; + HeldBy = null; + } + else + { + HeldBy = null; + if (interactionType == PlugInteractionType.DockHome) + { + //todo + } + else if (interactionType == PlugInteractionType.Dropped) + { + //todo + } + else if (interactionType == PlugInteractionType.PickedUp) + { + HeldBy = player; + } + } + return true; } @@ -240,6 +275,10 @@ private void OnGrabbed(ControlImplBase control) if (NetworkLifecycle.Instance.IsProcessingPacket) return; + //Prevent new players/players entering the area from sending packets until initalised + if (!Refreshed) + return; + Multiplayer.LogDebug(() => $"NetworkedPluggableObject.OnGrabbed() [{transform.parent.name}, {NetId}] station: {Station?.StationName}"); NetworkLifecycle.Instance.Client?.SendPitStopPlugInteractionPacket(NetId, PlugInteractionType.PickedUp); } @@ -249,6 +288,10 @@ private void OnUngrabbed(ControlImplBase control) if (NetworkLifecycle.Instance.IsProcessingPacket) return; + //Prevent new players/players entering the area from sending packets until initalised + if (!Refreshed) + return; + Multiplayer.LogDebug(() => $"NetworkedPluggableObject.OnUngrabbed() [{transform.parent.name}, {NetId}] station: {Station?.StationName}"); NetworkLifecycle.Instance.Client?.SendPitStopPlugInteractionPacket(NetId, PlugInteractionType.Dropped); } @@ -258,6 +301,10 @@ private void OnPlugged(PluggableObject plug, PlugSocket socket) if (NetworkLifecycle.Instance.IsProcessingPacket) return; + //Prevent new players/players entering the area from sending packets until initalised + if (!Refreshed) + return; + Multiplayer.LogDebug(() => $"NetworkedPluggableObject.OnPlugged() [{transform.parent.name}, {NetId}] station: {Station?.StationName}"); PlugInteractionType interaction; diff --git a/Multiplayer/Networking/Data/PitStopPlugData.cs b/Multiplayer/Networking/Data/PitStopPlugData.cs new file mode 100644 index 00000000..6852cb4e --- /dev/null +++ b/Multiplayer/Networking/Data/PitStopPlugData.cs @@ -0,0 +1,101 @@ +using LiteNetLib.Utils; +using Multiplayer.Components.Networking.World; +using Multiplayer.Networking.Serialization; +using UnityEngine; + +namespace Multiplayer.Networking.Data; + +public readonly struct PitStopPlugData(ushort netId, PlugInteractionType state, byte playerId, ushort trainCarNetId, bool isLeft, Vector3 pos, Quaternion rot) +{ + public readonly ushort NetID = netId; + public readonly byte PlayerId = playerId; + public readonly PlugInteractionType State = state; + public readonly ushort TrainCarNetId = trainCarNetId; + public readonly bool IsLeftSide = isLeft; + public readonly Vector3 Position = pos; + public readonly Quaternion Rotation = rot; + + public static PitStopPlugData From(NetworkedPluggableObject plugData) + { + return new PitStopPlugData + ( + plugData.NetId, + plugData.CurrentInteration, + plugData.HeldBy.Id, + plugData.TrainCarNetId, + plugData.IsConnectedLeft, + plugData.transform.AbsolutePosition(), + plugData.transform.rotation + ); + } + + public static void Serialize(NetDataWriter writer, PitStopPlugData data) + { + writer.Put(data.NetID); + writer.Put((byte)data.State); + + switch (data.State) + { + case PlugInteractionType.Rejected: + //do nothing?? + break; + case PlugInteractionType.PickedUp: + writer.Put(data.PlayerId); + break; + case PlugInteractionType.Dropped: + Vector3Serializer.Serialize(writer, data.Position); + QuaternionSerializer.Serialize(writer, data.Rotation); + break; + case PlugInteractionType.DockHome: + //do nothing + break; + case PlugInteractionType.DockSocket: + writer.Put(data.TrainCarNetId); + writer.Put(data.IsLeftSide); + break; + } + } + + public static PitStopPlugData Deserialize(NetDataReader reader) + { + ushort netId = reader.GetUShort(); + PlugInteractionType state = (PlugInteractionType)reader.GetByte(); + byte playerId = 0; + ushort trainCarNetId = 0; + bool isLeft = false; + Vector3 pos = Vector3.zero; + Quaternion rot = Quaternion.identity; + + switch (state) + { + case PlugInteractionType.Rejected: + // No additional data to read + break; + case PlugInteractionType.PickedUp: + playerId = reader.GetByte(); + break; + case PlugInteractionType.Dropped: + pos = Vector3Serializer.Deserialize(reader); + rot = QuaternionSerializer.Deserialize(reader); + break; + case PlugInteractionType.DockHome: + // No additional data to read + break; + case PlugInteractionType.DockSocket: + trainCarNetId = reader.GetUShort(); + isLeft = reader.GetBool(); + break; + } + + return new PitStopPlugData + ( + netId, + state, + playerId, + trainCarNetId, + isLeft, + pos, + rot + ); + } +} diff --git a/Multiplayer/Networking/Data/PitStopStationData.cs b/Multiplayer/Networking/Data/PitStopStationData.cs new file mode 100644 index 00000000..9764702e --- /dev/null +++ b/Multiplayer/Networking/Data/PitStopStationData.cs @@ -0,0 +1,40 @@ +using DV.ThingTypes; +using LiteNetLib.Utils; +using System.Collections.Generic; +using System.Linq; + +namespace Multiplayer.Networking.Data; + +public readonly struct PitStopStationData(Dictionary resourceStates) +{ + public readonly Dictionary ResourceState = resourceStates; + + public static PitStopStationData From(CarPitStopParametersBase pitStopParams) + { + //extract floats + var states = pitStopParams.GetCarPitStopParameters().ToDictionary(param => param.Key, param => param.Value.value); + + return new PitStopStationData(states); + } + + public static void Serialize(NetDataWriter writer, PitStopStationData data) + { + writer.Put(data.ResourceState.Count); + foreach (var kvp in data.ResourceState) + { + writer.Put((int)kvp.Key); + writer.Put(kvp.Value); + } + } + + public static PitStopStationData Deserialize(NetDataReader reader) + { + var statesCount = reader.GetInt(); + + Dictionary states = []; + for (int i = 0; i < statesCount; i++) + states.Add((ResourceType)reader.GetInt(), reader.GetFloat()); + + return new PitStopStationData(states); + } +} diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 63691a47..447a47df 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -177,6 +177,7 @@ protected override void Subscribe() netPacketProcessor.SubscribeReusable(OnCommonPitStopInteractionPacket); netPacketProcessor.SubscribeReusable(OnCommonPitStopPlugInteractionPacket); + netPacketProcessor.SubscribeReusable(OnClientboundPitStopBulkUpdatePacket); } #region Net Events @@ -995,6 +996,19 @@ private void OnCommonPitStopPlugInteractionPacket(CommonPitStopPlugInteractionPa netPlug.ProcessPacket(packet); } + private void OnClientboundPitStopBulkUpdatePacket(ClientboundPitStopBulkUpdatePacket packet) + { + if (!NetworkedPitStopStation.Get(packet.NetId, out var netPitStop)) + { + LogWarning($"Pit Stop Bulk Data received for station netId: {packet.NetId}, but pit stop does not exist!"); + return; + } + + Log($"Pit Stop Bulk Data received for {netPitStop.StationName}"); + + netPitStop.ProcessBulkUpdate(packet); + } + private void OnCommonItemChangePacket(CommonItemChangePacket packet) { diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index 14c825d9..bc03b274 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -524,6 +524,22 @@ public void SendItemsChangePacket(List items, ServerPlayer playe } } + public void SendPitStopBulkDataPacket(ushort netId, PitStopStationData[] stationData, PitStopPlugData[] plugData , ITransportPeer peer = null) + { + LogDebug(() => $"SendPitStopBulkDataPacket({netId}, {stationData.Count()}, {plugData.Count()}, {peer?.Id})"); + + var packet = new ClientboundPitStopBulkUpdatePacket + { + PitStopData = stationData, + PlugData = plugData, + }; + + if (peer == null) + SendPacketToAll(packet, DeliveryMethod.ReliableOrdered); + else + SendPacket(peer, packet, DeliveryMethod.ReliableOrdered); + } + public void SendChat(string message, ITransportPeer exclude = null) { @@ -1168,7 +1184,7 @@ private void OnCommonPitStopPlugInteractionPacket(CommonPitStopPlugInteractionPa if(NetworkedPluggableObject.Get(packet.NetId, out NetworkedPluggableObject plug) && foundPlayer) { - if (plug.ValidateInteraction(packet)) + if (plug.ValidateInteraction(packet, player)) { //passed validation, send to all but the originator packet.PlayerId = player.Id; diff --git a/Multiplayer/Networking/Packets/Clientbound/World/ClientboundPitStopBulkUpdatePacket.cs b/Multiplayer/Networking/Packets/Clientbound/World/ClientboundPitStopBulkUpdatePacket.cs new file mode 100644 index 00000000..8bedf8d8 --- /dev/null +++ b/Multiplayer/Networking/Packets/Clientbound/World/ClientboundPitStopBulkUpdatePacket.cs @@ -0,0 +1,14 @@ +using Multiplayer.Networking.Data; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Multiplayer.Networking.Packets.Clientbound.World; + +public class ClientboundPitStopBulkUpdatePacket +{ + public ushort NetId { get; set; } + public PitStopStationData[] PitStopData { get; set; } + public PitStopPlugData[] PlugData { get; set; } +} From 6fce05e3bc2ac824326c447bab8386f5270b341b Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 2 Feb 2025 22:45:40 +1000 Subject: [PATCH 241/521] Refactor Pit Stop Interaction packets and Types --- .../Data/PitStopStationInteractionType.cs | 17 ++++++++--------- .../CommonPitStopPlugInteractionPacket.cs | 4 ++-- 2 files changed, 10 insertions(+), 11 deletions(-) rename Multiplayer/Networking/Packets/{Clientbound/World => Common}/CommonPitStopPlugInteractionPacket.cs (74%) diff --git a/Multiplayer/Networking/Data/PitStopStationInteractionType.cs b/Multiplayer/Networking/Data/PitStopStationInteractionType.cs index 7424c211..dc4e8847 100644 --- a/Multiplayer/Networking/Data/PitStopStationInteractionType.cs +++ b/Multiplayer/Networking/Data/PitStopStationInteractionType.cs @@ -2,15 +2,14 @@ namespace Multiplayer.Networking.Data; -[Flags] public enum PitStopStationInteractionType : byte { - Reject, - Grab, - Ungrab, - StateUpdate, - SelectCar, - PayOrder, - CancelOrder, - ProcessOrder, + Reject, //bit 0 + Grab, //bit 0 + Ungrab, //bit 1 + StateUpdate, //bit 2 + SelectCar, //bit 3 + PayOrder, //bit 4 + CancelOrder, //bit 5 + ProcessOrder } diff --git a/Multiplayer/Networking/Packets/Clientbound/World/CommonPitStopPlugInteractionPacket.cs b/Multiplayer/Networking/Packets/Common/CommonPitStopPlugInteractionPacket.cs similarity index 74% rename from Multiplayer/Networking/Packets/Clientbound/World/CommonPitStopPlugInteractionPacket.cs rename to Multiplayer/Networking/Packets/Common/CommonPitStopPlugInteractionPacket.cs index 9cd071ab..1ecd922c 100644 --- a/Multiplayer/Networking/Packets/Clientbound/World/CommonPitStopPlugInteractionPacket.cs +++ b/Multiplayer/Networking/Packets/Common/CommonPitStopPlugInteractionPacket.cs @@ -4,9 +4,9 @@ using System.Text; using UnityEngine; -namespace Multiplayer.Networking.Packets.Clientbound.World; +namespace Multiplayer.Networking.Packets.Common; -public class CommonPitStopPlugInteractionPacket +public class CommonPitStopPlugInteractionPacket { public ushort NetId { get; set; } public byte InteractionType { get; set; } From 65f7d6f64103109650bb714146e09c26255c3767 Mon Sep 17 00:00:00 2001 From: Macka Date: Sun, 2 Feb 2025 23:01:08 +1000 Subject: [PATCH 242/521] Create FUNDING.yml --- .github/FUNDING.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..ff43ccbd --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,15 @@ +# These are supported funding model platforms + +github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: macka +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry +polar: # Replace with a single Polar username +buy_me_a_coffee: # Replace with a single Buy Me a Coffee username +thanks_dev: # Replace with a single thanks.dev username +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] From d220e1a2280761e035e5eb439f7d60f92cf690db Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 2 Feb 2025 23:14:08 +1000 Subject: [PATCH 243/521] Revert "Create FUNDING.yml" This reverts commit 65f7d6f64103109650bb714146e09c26255c3767. --- .github/FUNDING.yml | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index ff43ccbd..00000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1,15 +0,0 @@ -# These are supported funding model platforms - -github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] -patreon: # Replace with a single Patreon username -open_collective: # Replace with a single Open Collective username -ko_fi: macka -tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel -community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry -liberapay: # Replace with a single Liberapay username -issuehunt: # Replace with a single IssueHunt username -lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry -polar: # Replace with a single Polar username -buy_me_a_coffee: # Replace with a single Buy Me a Coffee username -thanks_dev: # Replace with a single thanks.dev username -custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] From 067dccd160e9299016d15d5415b9796afbf035e5 Mon Sep 17 00:00:00 2001 From: AMacro Date: Mon, 27 Jan 2025 20:07:58 +1000 Subject: [PATCH 244/521] Fix issue with TrainCars being in reverse order --- .../Components/Networking/Train/NetworkedCarSpawner.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Multiplayer/Components/Networking/Train/NetworkedCarSpawner.cs b/Multiplayer/Components/Networking/Train/NetworkedCarSpawner.cs index 4625a3db..cceaccae 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedCarSpawner.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedCarSpawner.cs @@ -24,8 +24,9 @@ public static void SpawnCars(TrainsetSpawnPart[] parts, bool autoCouple) SetBrakeParams(parts[i].BrakeData, cars[i].TrainCar); //couple them if marked as coupled - for (int i = 0; i < cars.Length; i++) - Couple(parts[i], cars[i].TrainCar, autoCouple); + //- we need to do this back to front otherwise the TrainSet indicies will be wrong! + for (int i = cars.Length - 1; i >= 0; i--) + Couple(in parts[i], cars[i].TrainCar, autoCouple); //update speed queue data for (int i = 0; i < cars.Length; i++) From 651780bdd18626edd0c67e171af2d02a42923f0e Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 15 Feb 2025 10:36:18 +1000 Subject: [PATCH 245/521] Prevent client from sending hose events before initialised --- Multiplayer/Patches/Train/CouplerPatch.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Multiplayer/Patches/Train/CouplerPatch.cs b/Multiplayer/Patches/Train/CouplerPatch.cs index de8bffa3..ce829e9f 100644 --- a/Multiplayer/Patches/Train/CouplerPatch.cs +++ b/Multiplayer/Patches/Train/CouplerPatch.cs @@ -1,5 +1,6 @@ using HarmonyLib; using Multiplayer.Components.Networking; +using Multiplayer.Components.Networking.Train; namespace Multiplayer.Patches.Train; @@ -16,6 +17,13 @@ private static void ConnectAirHose(Coupler __instance, Coupler other, bool playA if (UnloadWatcher.isUnloading || NetworkLifecycle.Instance.IsProcessingPacket) return; + //Ensure local car has initialised and breaks have been connected on spawn before sending any packets + if (!NetworkedTrainCar.TryGetFromTrainCar(__instance?.train, out var netTrainCar) || !netTrainCar.Client_Initialized) + { + Multiplayer.LogDebug(() => $"ConnectAirHose({__instance?.train?.ID}) netTrainCar found: {netTrainCar != null}, Initialised: {netTrainCar?.Client_Initialized}"); + return; + } + NetworkLifecycle.Instance.Client?.SendHoseConnected(__instance, other, playAudio); } @@ -26,6 +34,14 @@ 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; + + //Ensure local car has initialised and breaks have been connected on spawn before sending any packets + if (!NetworkedTrainCar.TryGetFromTrainCar(__instance?.train, out var netTrainCar) || !netTrainCar.Client_Initialized) + { + Multiplayer.LogDebug(() => $"DisconnectAirHose({__instance?.train?.ID}) netTrainCar found: {netTrainCar != null}, Initialised: {netTrainCar?.Client_Initialized}"); + return; + } + NetworkLifecycle.Instance.Client?.SendHoseDisconnected(__instance, playAudio); } From 5ddb83c726aa6801d592592cec41b0826543283e Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 15 Feb 2025 10:36:41 +1000 Subject: [PATCH 246/521] Log warnings for high pings --- Multiplayer/Networking/Managers/Client/NetworkClient.cs | 3 +++ Multiplayer/Networking/Managers/Server/NetworkServer.cs | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 9078f65c..9e515e5e 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -199,6 +199,9 @@ public override void OnPeerDisconnected(ITransportPeer peer, DisconnectReason di public override void OnNetworkLatencyUpdate(ITransportPeer peer, int latency) { Ping = latency; + + if (latency > 150) + LogWarning($"High Ping Detected! {latency}ms"); } public override void OnConnectionRequest(NetDataReader dataReader, IConnectionRequest request) diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index 93f855ee..a829cb17 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -237,6 +237,12 @@ public override void OnNetworkLatencyUpdate(ITransportPeer peer, int latency) { ServerTick = NetworkLifecycle.Instance.Tick }, DeliveryMethod.ReliableUnordered); + + if (latency > 150) + { + serverPlayers.TryGetValue((byte)peer.Id, out var player); + LogWarning($"High Ping Detected! Player: \"{player?.Username}\", ping: {latency}ms"); + } } public override void OnConnectionRequest(NetDataReader requestData, IConnectionRequest request) From cba906413bdb7a8ed6da04474591f2d200344534 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 15 Feb 2025 10:37:11 +1000 Subject: [PATCH 247/521] Log warnings for large changes in car positon --- .../Components/Networking/Train/NetworkedTrainCar.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs index 59642886..fd50aa0b 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs @@ -140,6 +140,7 @@ protected override void Awake() if (NetworkLifecycle.Instance.IsHost()) { NetworkTrainsetWatcher.Instance.CheckInstance(); // Ensure the NetworkTrainsetWatcher is initialized + Client_Initialized = true; } else { @@ -1207,7 +1208,15 @@ public void Client_ReceiveTrainPhysicsUpdate(in TrainsetMovementPart movementPar //move the car to the correct position first - maybe? if (movementPart.typeFlag.HasFlag(TrainsetMovementPart.MovementType.Position)) { - TrainCar.transform.position = movementPart.Position + WorldMover.currentMove; + Vector3 worldPos = movementPart.Position + WorldMover.currentMove; + + Vector3 deltaPos = worldPos - TrainCar.transform.position; + if (deltaPos.magnitude > 5f) + { // Threshold for significant position change + Multiplayer.LogWarning($"[{CurrentID}] Large position correction: {deltaPos.magnitude}m at tick {tick}"); + } + + TrainCar.transform.position = worldPos; TrainCar.transform.rotation = movementPart.Rotation; //clear the queues? From cfd3b4d8c3cc38f53c530d8c26394a3c1df9d2aa Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 15 Feb 2025 10:38:07 +1000 Subject: [PATCH 248/521] Add logging to fault find rollercoaster effect --- .../Components/Networking/TickedQueue.cs | 25 +++++++++++++++++ .../Networking/Train/NetworkedBogie.cs | 28 +++++++++---------- .../Networking/Train/NetworkedRigidbody.cs | 1 - Multiplayer/Multiplayer.cs | 1 - 4 files changed, 39 insertions(+), 16 deletions(-) diff --git a/Multiplayer/Components/Networking/TickedQueue.cs b/Multiplayer/Components/Networking/TickedQueue.cs index 30aad3a3..5842e00b 100644 --- a/Multiplayer/Components/Networking/TickedQueue.cs +++ b/Multiplayer/Components/Networking/TickedQueue.cs @@ -1,3 +1,4 @@ +using Multiplayer.Components.Networking.Train; using System.Collections.Generic; using UnityEngine; @@ -5,11 +6,26 @@ namespace Multiplayer.Components.Networking; public abstract class TickedQueue : MonoBehaviour { + private const float WARNING_THRESHOLD_SECONDS = 1.0f; + private const uint QUEUE_LENGTH_WARNING = (uint)(NetworkLifecycle.TICK_RATE * WARNING_THRESHOLD_SECONDS); + private const uint SNAPSHOT_GAP_WARNING = (uint)(NetworkLifecycle.TICK_RATE * WARNING_THRESHOLD_SECONDS); + private uint lastTick; + private uint lastReceivedTick; private readonly Queue<(uint, T)> snapshots = new(); + private string identifier; protected virtual void OnEnable() { + TrainCar car; + int bogie = 0; + + if(car = TrainCar.Resolve(this.gameObject)) + if(this is NetworkedBogie netBogie) + bogie = (car.Bogies[0] == netBogie.Bogie) ? 1 : 2; + + identifier = $"{car?.ID ?? gameObject.GetPath()}{(bogie > 0 ? $" Bogie {bogie}" : "")}"; + NetworkLifecycle.Instance.OnTick += OnTick; } @@ -20,12 +36,21 @@ protected virtual void OnDisable() NetworkLifecycle.Instance.OnTick -= OnTick; lastTick = 0; snapshots.Clear(); + identifier = string.Empty; } public void ReceiveSnapshot(T snapshot, uint tick) { if (tick <= lastTick) return; + + if (snapshots.Count >= QUEUE_LENGTH_WARNING) + Multiplayer.LogWarning($"[{identifier}] Snapshot queue exceeds {QUEUE_LENGTH_WARNING} items. Current size: {snapshots.Count}"); + + if (lastReceivedTick > 0 && tick - lastReceivedTick > SNAPSHOT_GAP_WARNING) + Multiplayer.LogWarning($"[{identifier}] Large gap between snapshots: {tick - lastReceivedTick} ticks."); + + lastReceivedTick = tick; lastTick = tick; snapshots.Enqueue((tick, snapshot)); } diff --git a/Multiplayer/Components/Networking/Train/NetworkedBogie.cs b/Multiplayer/Components/Networking/Train/NetworkedBogie.cs index c3169482..f0c8334d 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedBogie.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedBogie.cs @@ -8,7 +8,7 @@ namespace Multiplayer.Components.Networking.Train; public class NetworkedBogie : TickedQueue { private const int MAX_FRAMES = 60; - private Bogie bogie; + public Bogie Bogie { get; private set; } protected override void OnEnable() { @@ -19,10 +19,10 @@ protected IEnumerator WaitForBogie() { int counter = 0; - while (bogie == null && counter < MAX_FRAMES) + while (Bogie == null && counter < MAX_FRAMES) { - bogie = GetComponent(); - if (bogie == null) + Bogie = GetComponent(); + if (Bogie == null) { counter++; yield return new WaitForEndOfFrame(); @@ -31,20 +31,20 @@ protected IEnumerator WaitForBogie() base.OnEnable(); - if (bogie == null) + if (Bogie == null) { - Multiplayer.LogError($"{gameObject.name} ({bogie?.Car?.ID}): {nameof(NetworkedBogie)} requires a {nameof(Bogie)} component on the same GameObject! Waited {counter} iterations"); + 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) { - if (bogie.HasDerailed) + if (Bogie.HasDerailed) return; if (snapshot.HasDerailed) { - bogie.Derail(); + Bogie.Derail(); return; } @@ -52,23 +52,23 @@ protected override void Process(BogieData snapshot, uint snapshotTick) { if (!NetworkedRailTrack.Get(snapshot.TrackNetId, out NetworkedRailTrack track)) { - Multiplayer.LogWarning($"NetworkedBogie.Process() Failed to find track {snapshot.TrackNetId} for bogie: {bogie.Car.ID}"); + Multiplayer.LogWarning($"NetworkedBogie.Process() Failed to find track {snapshot.TrackNetId} for bogie: {Bogie.Car.ID}"); return; } - bogie.SetTrack(track.RailTrack, snapshot.PositionAlongTrack, snapshot.TrackDirection); + Bogie.SetTrack(track.RailTrack, snapshot.PositionAlongTrack, snapshot.TrackDirection); } else { - if(bogie.track) - 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!"); + 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; for (int i = 0; i < physicsSteps; i++) - bogie.UpdatePointSetTraveller(); + Bogie.UpdatePointSetTraveller(); } } diff --git a/Multiplayer/Components/Networking/Train/NetworkedRigidbody.cs b/Multiplayer/Components/Networking/Train/NetworkedRigidbody.cs index bd772c4b..884423b6 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedRigidbody.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedRigidbody.cs @@ -2,7 +2,6 @@ using System; using System.Collections; using UnityEngine; -using static Multiplayer.Networking.Data.Train.RigidbodySnapshot; namespace Multiplayer.Components.Networking.Train; diff --git a/Multiplayer/Multiplayer.cs b/Multiplayer/Multiplayer.cs index 84330965..2b173354 100644 --- a/Multiplayer/Multiplayer.cs +++ b/Multiplayer/Multiplayer.cs @@ -14,7 +14,6 @@ using UnityChan; using UnityEngine; using UnityModManagerNet; -using Steamworks; namespace Multiplayer; From ec0b2d18c4194391da8affcb57041d766fe8ccce Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 15 Feb 2025 11:40:16 +1000 Subject: [PATCH 249/521] Add checks and warnings if Steam not detected --- .../Components/MainMenu/HostGamePane.cs | 21 ++++++++++++------- .../Components/MainMenu/ServerBrowserPane.cs | 9 ++++++++ 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/Multiplayer/Components/MainMenu/HostGamePane.cs b/Multiplayer/Components/MainMenu/HostGamePane.cs index 3f35b8d7..1353a024 100644 --- a/Multiplayer/Components/MainMenu/HostGamePane.cs +++ b/Multiplayer/Components/MainMenu/HostGamePane.cs @@ -63,7 +63,12 @@ public void Awake() public void Start() { Multiplayer.Log("HostGamePane Started"); - + + if (DVSteamworks.Success) + return; + + Multiplayer.Log($"Steam not detected, prompt for restart."); + MainMenuThingsAndStuff.Instance.ShowOkPopup("Steam not detected. Please restart the game with Steam running", () => { }); } public void OnEnable() @@ -334,23 +339,23 @@ private void ValidateInputs(string text) bool valid = true; int portNum; + + if (!DVSteamworks.Success) + valid = false; + 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 (!int.TryParse(port.text, out portNum) || portNum < MIN_PORT || portNum > MAX_PORT) + valid = false; } - if( port.text == "" && (Multiplayer.Settings.Port < MIN_PORT || Multiplayer.Settings.Port > MAX_PORT)) + 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() diff --git a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs index f3ab768e..742b435d 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs @@ -186,6 +186,15 @@ public void Update() } } + public void Start() + { + if (DVSteamworks.Success) + return; + + Multiplayer.Log($"Steam not detected, prompt for restart."); + MainMenuThingsAndStuff.Instance.ShowOkPopup("Steam not detected. Please restart the game with Steam running", ()=>{}); + } + private void CleanUI() { GameObject.Destroy(this.FindChildByName("Text Content")); From b9e338cda8f89b8c59a1b2937eb11a13fe5f9495 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 15 Feb 2025 16:19:47 +1000 Subject: [PATCH 250/521] Fixed incorrect shutdown of server --- .../TransportLayers/SteamworksTransport.cs | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/Multiplayer/Networking/TransportLayers/SteamworksTransport.cs b/Multiplayer/Networking/TransportLayers/SteamworksTransport.cs index 69d206d8..d5744341 100644 --- a/Multiplayer/Networking/TransportLayers/SteamworksTransport.cs +++ b/Multiplayer/Networking/TransportLayers/SteamworksTransport.cs @@ -13,7 +13,7 @@ namespace Multiplayer.Networking.TransportLayers; public class SteamWorksTransport : ITransport { - public NetStatistics Statistics => new NetStatistics(); + public NetStatistics Statistics => new(); public bool IsRunning { get; private set; } public event Action OnConnectionRequest; @@ -61,6 +61,7 @@ public bool Start(int port) if (server != null) { + Multiplayer.LogDebug(() => $"SteamWorksTransport.Start({port}) Relay not null"); server.transport = this; servers.Add(server); IsRunning = true; @@ -84,17 +85,22 @@ public void Stop(bool sendDisconnectPackets) client?.Close(true); - - while (servers.Count > 0) + foreach (var server in servers) { - if (servers[0] != null) + if (server != null) { - foreach (var connection in servers[0].Connected) + // Close all connections first + foreach (var connection in server.Connected) + { connection.Close(true, (int)NetConnectionEnd.App_Generic); - } + } - servers.RemoveAt(0); + //close the server + server.Close(); + } } + + servers.Clear(); } public void PollEvents() From 13a6bf0d0c88a02f95d2e22f70b7da52b09b7fd0 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 15 Feb 2025 16:31:03 +1000 Subject: [PATCH 251/521] Add extended server visibility options --- .../Components/MainMenu/HostGamePane.cs | 75 ++++++++++++------- .../IServerBrowserGameDetails.cs | 63 ++++++++-------- .../ServerBrowserDummyElement.cs | 1 - Multiplayer/Locale.cs | 12 +-- .../Networking/Data/LobbyServerData.cs | 2 +- .../Managers/Server/LobbyServerManager.cs | 13 ++-- Multiplayer/Settings.cs | 37 ++++++--- locale.csv | 6 ++ 8 files changed, 127 insertions(+), 82 deletions(-) diff --git a/Multiplayer/Components/MainMenu/HostGamePane.cs b/Multiplayer/Components/MainMenu/HostGamePane.cs index 1353a024..3f0f2c75 100644 --- a/Multiplayer/Components/MainMenu/HostGamePane.cs +++ b/Multiplayer/Components/MainMenu/HostGamePane.cs @@ -40,6 +40,7 @@ public class HostGamePane : MonoBehaviour SliderDV maxPlayers; Toggle gamePublic; + Selector gameVisibility; ButtonDV startButton; public ISaveGame saveGame; @@ -120,10 +121,17 @@ private void BuildUI() return; } + GameObject selectorPrefab = goMMC.FindChildByName("Crosshair").gameObject; + if (selectorPrefab == null) + { + Multiplayer.LogError("selectorPrefab not found!"); + return; + } + GameObject sliderPrefab = goMMC.FindChildByName("Field Of View").gameObject; if (sliderPrefab == null) { - Multiplayer.LogError("SliderLimitSession not found!"); + Multiplayer.LogError("Field Of View not found!"); return; } @@ -207,9 +215,11 @@ private void BuildUI() ContentSizeFitter sizeFitter = controls.AddComponent(); sizeFitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize; + /* + * Server name field + */ 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; @@ -217,7 +227,9 @@ private void BuildUI() go.AddComponent(); go.ResetTooltip(); - + /* + * Server password field + */ go = GameObject.Instantiate(inputPrefab, NewContentGroup(controls, scroller.viewport.sizeDelta).transform, false); go.name = "Password"; password = go.GetComponent(); @@ -227,19 +239,26 @@ private void BuildUI() 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()); + /* + * Server visibility field + */ + go = GameObject.Instantiate(selectorPrefab, NewContentGroup(controls, scroller.viewport.sizeDelta).transform, false); + go.name = "Visibility"; + go.FindChildByName("[text label]").GetComponent().key = Locale.SERVER_HOST_VISIBILITY_KEY; go.ResetTooltip(); - - + go.FindChildByName("[text label]").GetComponent().UpdateLocalization(); + DestroyImmediate(go.GetComponent()); + gameVisibility = go.GetOrAddComponent(); + gameVisibility.LocalizedLabel = true; + gameVisibility.SetLabel(Locale.SERVER_HOST_VISIBILITY_KEY); + gameVisibility.LocalizedValues = true; + gameVisibility.SetValues(Locale.SERVER_HOST_VISIBILITY_MODES.ToList()); + gameVisibility.SetSelectedIndex(3); + gameVisibility.ToggleInteractable(true); + + /* + * Server details field + */ 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); @@ -247,19 +266,21 @@ private void BuildUI() details.characterLimit = MAX_DETAILS_LEN; details.lineType = TMP_InputField.LineType.MultiLineNewline; details.FindChildByName("text [noloc]").GetComponent().alignment = TextAlignmentOptions.TopLeft; - details.placeholder.GetComponent().text = Locale.SERVER_HOST_DETAILS; - + //Divider go = GameObject.Instantiate(dividerPrefab, NewContentGroup(controls, scroller.viewport.sizeDelta).transform, false); go.name = "Divider"; - + /* + * Server max players field + */ 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(); + DestroyImmediate(go.GetComponent()); maxPlayers = go.GetComponent(); maxPlayers.stepIncrement = 1; maxPlayers.minValue = MIN_PLAYERS; @@ -267,7 +288,9 @@ private void BuildUI() maxPlayers.value = Mathf.Clamp(Multiplayer.Settings.MaxPlayers,MIN_PLAYERS,MAX_PLAYERS); maxPlayers.interactable = true; - + /* + * Server port field + */ go = GameObject.Instantiate(inputPrefab, NewContentGroup(controls, scroller.viewport.sizeDelta).transform, false); go.name = "Port"; port = go.GetComponent(); @@ -276,15 +299,15 @@ private void BuildUI() port.placeholder.GetComponent().text = "7777"; port.text = (Multiplayer.Settings.Port >= MIN_PORT && Multiplayer.Settings.Port <= MAX_PORT) ? Multiplayer.Settings.Port.ToString() : DEFAULT_PORT.ToString(); - + /* + * Start Game button + */ 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) @@ -312,9 +335,7 @@ private GameObject NewContentGroup(GameObject parent, Vector2 sizeDelta, int cel return contentGroup; } - - -private void SetupListeners(bool on) + private void SetupListeners(bool on) { if (on) { @@ -366,7 +387,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.Visibility = (ServerVisibility)gameVisibility.SelectedIndex; serverData.GameMode = 0; //replaced with details from save / new game serverData.Difficulty = 0; //replaced with details from save / new game @@ -409,7 +430,7 @@ private void StartClick() Multiplayer.Settings.ServerName = serverData.Name; Multiplayer.Settings.Password = password.text; - Multiplayer.Settings.PublicGame = serverData.isPublic; + Multiplayer.Settings.Visibility = serverData.Visibility; Multiplayer.Settings.Port = serverData.port; Multiplayer.Settings.MaxPlayers = serverData.MaxPlayers; Multiplayer.Settings.Details = serverData.ServerDetails; diff --git a/Multiplayer/Components/MainMenu/ServerBrowser/IServerBrowserGameDetails.cs b/Multiplayer/Components/MainMenu/ServerBrowser/IServerBrowserGameDetails.cs index 997e367a..fb47fc17 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowser/IServerBrowserGameDetails.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowser/IServerBrowserGameDetails.cs @@ -1,37 +1,34 @@ 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 +namespace Multiplayer.Components.MainMenu; + +public enum ServerVisibility : int +{ + Private = 0, + Friends = 1, + Public = 2 +} + +public interface IServerBrowserGameDetails : IDisposable { - // - public interface IServerBrowserGameDetails : IDisposable - { - string id { get; set; } - 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; } - 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; } - bool isPublic { get; set; } - int LastSeen { get; set; } - } + string id { get; set; } + 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; } + 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; } + ServerVisibility Visibility { get; set; } + int LastSeen { get; set; } } diff --git a/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserDummyElement.cs b/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserDummyElement.cs index bf56f5f1..a68c1c09 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserDummyElement.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserDummyElement.cs @@ -5,7 +5,6 @@ using System.ComponentModel; using TMPro; using UnityEngine; -using UnityEngine.UI; namespace Multiplayer.Components.MainMenu.ServerBrowser { diff --git a/Multiplayer/Locale.cs b/Multiplayer/Locale.cs index 7a99dd08..87065e14 100644 --- a/Multiplayer/Locale.cs +++ b/Multiplayer/Locale.cs @@ -2,13 +2,9 @@ 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 { @@ -86,6 +82,11 @@ public static class Locale 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_VISIBILITY => Get(SERVER_HOST_PUBLIC_KEY); + public const string SERVER_HOST_VISIBILITY_KEY = $"{PREFIX_SERVER_HOST}/visibility"; + // public static string SERVER_HOST_VISIBILITY_MODES => Get(SERVER_HOST_VISIBILITY_MODES_KEY); + public static string[] SERVER_HOST_VISIBILITY_MODES = [$"{SERVER_HOST_VISIBILITY_MODES_KEY}/private" , $"{SERVER_HOST_VISIBILITY_MODES_KEY}/friends",$"{SERVER_HOST_VISIBILITY_MODES_KEY}/public"]; + public const string SERVER_HOST_VISIBILITY_MODES_KEY = $"{PREFIX_SERVER_HOST}/visibility/modes"; 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); @@ -106,6 +107,7 @@ public static class Locale #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 index 3c8317fa..b7a562fa 100644 --- a/Multiplayer/Networking/Data/LobbyServerData.cs +++ b/Multiplayer/Networking/Data/LobbyServerData.cs @@ -64,7 +64,7 @@ public class LobbyServerData : IServerBrowserGameDetails [JsonIgnore] public int Ping { get; set; } = -1; [JsonIgnore] - public bool isPublic { get; set; } + public ServerVisibility Visibility { get; set; } = ServerVisibility.Public; [JsonIgnore] public int LastSeen { get; set; } = int.MaxValue; diff --git a/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs b/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs index 051fde72..406ef575 100644 --- a/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs +++ b/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs @@ -18,6 +18,7 @@ using Steamworks.Data; using Multiplayer.Utils; using Multiplayer.Networking.TransportLayers; +using Multiplayer.Components.MainMenu; namespace Multiplayer.Networking.Managers.Server; public class LobbyServerManager : MonoBehaviour @@ -84,7 +85,7 @@ public IEnumerator Start() server.Log("Public IPv6: " + server.serverData.ipv6); server.Log("Private IPv4: " + server.serverData.LocalIPv4); - if (server.serverData.isPublic) + if (server.serverData.Visibility >= ServerVisibility.Private) { Multiplayer.Log($"Registering server at: {Multiplayer.Settings.LobbyServerAddress}/{ENDPOINT_ADD_SERVER}"); StartCoroutine(RegisterWithLobbyServer($"{Multiplayer.Settings.LobbyServerAddress}/{ENDPOINT_ADD_SERVER}")); @@ -138,7 +139,7 @@ public void Update() SteamworksUtils.SetLobbyData((Lobby)lobby, server.serverData, EXCLUDE_PARAMS); } } - }else if (!server.serverData.isPublic || !sendUpdates) + }else if (server.serverData.Visibility == ServerVisibility.Private || !sendUpdates) { server.serverData.CurrentPlayers = server.PlayerCount; } @@ -170,10 +171,12 @@ public async void CreateSteamLobby() SteamworksUtils.SetLobbyData((Lobby)lobby, server.serverData, EXCLUDE_PARAMS); //todo implement public/private/friends - if (server.serverData.isPublic) - lobby?.SetPublic(); - else + if (server.serverData.Visibility == ServerVisibility.Private) lobby?.SetPrivate(); + else if (server.serverData.Visibility == ServerVisibility.Friends) + lobby?.SetFriendsOnly(); + else if (server.serverData.Visibility == ServerVisibility.Public) + lobby?.SetPublic(); lobby?.SetJoinable(true); } diff --git a/Multiplayer/Settings.cs b/Multiplayer/Settings.cs index 703b6197..c98c3a0a 100644 --- a/Multiplayer/Settings.cs +++ b/Multiplayer/Settings.cs @@ -1,7 +1,7 @@ using System; using Humanizer; +using Multiplayer.Components.MainMenu; using Multiplayer.Utils; -using Steamworks; using UnityEngine; using UnityModManagerNet; using Console = DV.Console; @@ -12,11 +12,12 @@ namespace Multiplayer; [DrawFields(DrawFieldMask.OnlyDrawAttr)] public class Settings : UnityModManager.ModSettings, IDrawable { + public const int CURRENT_VERSION = 3; public const byte MAX_USERNAME_LENGTH = 24; public static Action OnSettingsUpdated; - public int SettingsVer = 2; + public int SettingsVer = CURRENT_VERSION; [Header("Player")] [Draw("Use Steam Name", Tooltip = "Use your Steam name as your username in-game")] @@ -33,7 +34,8 @@ public class Settings : UnityModManager.ModSettings, IDrawable 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")] + [Draw("Server Visibility")] + public ServerVisibility Visibility = ServerVisibility.Public; public bool PublicGame = true; [Draw("Max Players", Tooltip = "The maximum number of players that can join your server, including yourself.")] public int MaxPlayers = 4; @@ -45,7 +47,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 = "https://dv.mineit.space";//"http://localhost:8080"; + public string LobbyServerAddress = "https://dv.mineit.space"; [Draw("IPv4 Check Address", Tooltip = "Do not modify unless the service is unavailable")] public string Ipv4AddressCheck = "https://api.ipify.org/"; [Header("Last Server Connected to by IP")] @@ -162,36 +164,51 @@ public static Settings Load(UnityModManager.ModEntry modEntry) private static int GetCurrentVersion() { - return 2; + return CURRENT_VERSION; } // 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") + if (data.LobbyServerAddress == "http://dv.mineit.space") data.LobbyServerAddress = new Settings().LobbyServerAddress; - MigrateSettings(ref data); break; - case 1: + + case 1: if (data.Ipv4AddressCheck == "http://checkip.dyndns.org") data.Ipv4AddressCheck = new Settings().Ipv4AddressCheck; data.ShowAdvancedSettings = true; data.DebugLogging = true; data.ShowPingInNameTags = true; + + break; + + case 2: + + if (data.PublicGame) + data.Visibility = ServerVisibility.Public; + else + data.Visibility = ServerVisibility.Friends; + break; + default: break; } + if (data.SettingsVer < GetCurrentVersion()) + { + data.SettingsVer++; + MigrateSettings(ref data); + } } } diff --git a/locale.csv b/locale.csv index 58524588..1b66de19 100644 --- a/locale.csv +++ b/locale.csv @@ -51,6 +51,12 @@ host/password__tooltip,Password field placeholder,Password for joining the game. 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.,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/visibility,Server visibility selector label,Server Visibility,Видимост на сървъра,,,,,,,,,,,,,,,,,,,,,,,, +host/visibility__tooltip,Server visibility selector tooltip,"Sets the visibility of the server. Private (Unlisted, Invite Only), Friends (Only Steam Friends or Invites), Public (Everyone)","Задава видимостта на сървъра. Частен (нерегистриран, само с покана), приятели (само приятели или покани в Steam), публичен (всички)",,,,,,,,,,,,,,,,,,,,,,,, +host/visibility__tooltip_disabled,Unused,,,,,,,,,,,,,,,,,,,,,,,,,, +host/visibility/modes/private,Possible visibiliy modes,Private,Частно,,,,,,,,,,,,,,,,,,,,,,,, +host/visibility/modes/friends,Possible visibiliy modes,Friends,Приятели,,,,,,,,,,,,,,,,,,,,,,,, +host/visibility/modes/public,Possible visibiliy modes,Public,Публично,,,,,,,,,,,,,,,,,,,,,,,, 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 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,Максимальна кількість гравців From b88cf2196bf6593bafc264a5c96dd198d7ed6122 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 15 Feb 2025 17:10:11 +1000 Subject: [PATCH 252/521] fix issue with visibility parameter --- .../Managers/Server/LobbyServerManager.cs | 6 ++-- Multiplayer/Utils/SteamWorksUtils.cs | 28 +++++++++++++++---- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs b/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs index 406ef575..f2d36dda 100644 --- a/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs +++ b/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs @@ -42,7 +42,7 @@ public class LobbyServerManager : MonoBehaviour private string private_key; //Steam Lobby - public static readonly string[] EXCLUDE_PARAMS = {"id", "ipv4", "ipv6", "port", "LocalIPv4", "LocalIPv6", "Ping", "isPublic", "LastSeen", "CurrentPlayers", "MaxPlayers"}; + public static readonly string[] EXCLUDE_PARAMS = {"id", "ipv4", "ipv6", "port", "LocalIPv4", "LocalIPv6", "Ping", "Visibility", "LastSeen", "CurrentPlayers", "MaxPlayers"}; private Lobby? lobby; private bool initialised = false; @@ -165,12 +165,12 @@ public async void CreateSteamLobby() server.Log("Steam Lobby created successfully!"); server.LogDebug(() => $"Steam lobby ID: {lobby?.Id}"); - lobby?.SetData(SteamworksUtils.LOBBY_MP_MOD_KEY, string.Empty); //We'll add this in for filtering + lobby?.SetData(SteamworksUtils.LOBBY_MP_MOD_KEY, SteamworksUtils.LOBBY_MP_MOD_KEY); //We'll add this in for filtering lobby?.SetData(SteamworksUtils.LOBBY_NET_LOCATION_KEY, SteamNetworkingUtils.LocalPingLocation.ToString()); //for ping estimation SteamworksUtils.SetLobbyData((Lobby)lobby, server.serverData, EXCLUDE_PARAMS); - //todo implement public/private/friends + //Set correct visibility if (server.serverData.Visibility == ServerVisibility.Private) lobby?.SetPrivate(); else if (server.serverData.Visibility == ServerVisibility.Friends) diff --git a/Multiplayer/Utils/SteamWorksUtils.cs b/Multiplayer/Utils/SteamWorksUtils.cs index f52ab614..24e9bbbc 100644 --- a/Multiplayer/Utils/SteamWorksUtils.cs +++ b/Multiplayer/Utils/SteamWorksUtils.cs @@ -63,14 +63,32 @@ public static LobbyServerData GetLobbyData(this Lobby lobby) { var data = new LobbyServerData(); var properties = typeof(LobbyServerData).GetProperties(); + string value = null; foreach (var prop in properties) { - var value = lobby.GetData(prop.Name); - if (string.IsNullOrEmpty(value)) continue; - - var converted = Convert.ChangeType(value, prop.PropertyType); - prop.SetValue(data, converted); + try + { + value = lobby.GetData(prop.Name); + if (string.IsNullOrEmpty(value)) continue; + + if (prop.PropertyType.IsEnum) + { + var enumValue = Enum.Parse(prop.PropertyType, value); + prop.SetValue(data, enumValue); + } + else + { + var converted = Convert.ChangeType(value, prop.PropertyType); + prop.SetValue(data, converted); + } + + value = null; + } + catch (Exception ex) + { + Multiplayer.LogException($"GetLobbyData() Error parsing property: {prop?.Name}, value: {value}", ex); + } } return data; From 826a6eae6917e69cf15c7b0fde5082d0200f7da0 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 15 Feb 2025 17:24:44 +1000 Subject: [PATCH 253/521] Added ability to simulate data loss and high latency --- .../TransportLayers/SteamworksTransport.cs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/Multiplayer/Networking/TransportLayers/SteamworksTransport.cs b/Multiplayer/Networking/TransportLayers/SteamworksTransport.cs index d5744341..af0b88ed 100644 --- a/Multiplayer/Networking/TransportLayers/SteamworksTransport.cs +++ b/Multiplayer/Networking/TransportLayers/SteamworksTransport.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using Steamworks.Data; using System.Runtime.InteropServices; +using UnityEngine; namespace Multiplayer.Networking.TransportLayers; @@ -186,7 +187,20 @@ public void Send(ITransportPeer peer, NetDataWriter writer, DeliveryMethod deliv public void UpdateSettings(Settings settings) { - //todo: implement any settings + int chance = 0; + if (settings.SimulatePacketLoss) + chance = settings.SimulationPacketLossChance; + + SteamNetworkingUtils.FakeRecvPacketLoss = chance; + SteamNetworkingUtils.FakeSendPacketLoss = chance; + + + chance = 0; + if (settings.SimulateLatency) + chance = UnityEngine.Random.Range(settings.SimulationMinLatency, settings.SimulationMaxLatency); + + SteamNetworkingUtils.FakeRecvPacketLag = chance; + SteamNetworkingUtils.FakeSendPacketLag = chance; } #endregion From 6f1681baf4f658bd7b54296a3cf9c439fcc8a4e4 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 15 Feb 2025 20:11:21 +1000 Subject: [PATCH 254/521] Remove CarRollingAudioModulePatch --- .../Patches/Train/CarRollingAudioModulePatch.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Multiplayer/Patches/Train/CarRollingAudioModulePatch.cs b/Multiplayer/Patches/Train/CarRollingAudioModulePatch.cs index eaf86f03..98ed2c54 100644 --- a/Multiplayer/Patches/Train/CarRollingAudioModulePatch.cs +++ b/Multiplayer/Patches/Train/CarRollingAudioModulePatch.cs @@ -7,9 +7,9 @@ namespace Multiplayer.Patches.World; [HarmonyPatch(typeof(CarRollingAudioModule), nameof(CarRollingAudioModule.PlayJointAtBogie))] public static class CarRollingAudioModulePatch { - private static bool Prefix() - { - // todo: There's a bug with bogie joint sounds for clients that causes it to play hundreds of times per-frame. Once that's fixed, this patch can be removed. - return NetworkLifecycle.Instance.IsHost(); - } + //private static bool Prefix() + //{ + // // todo: There's a bug with bogie joint sounds for clients that causes it to play hundreds of times per-frame. Once that's fixed, this patch can be removed. + // return NetworkLifecycle.Instance.IsHost(); + //} } From dc8cd3259c2673848702d2458440ab75403be8c4 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 15 Feb 2025 20:53:03 +1000 Subject: [PATCH 255/521] update transport simulation settings --- Multiplayer/Networking/TransportLayers/SteamworksTransport.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Multiplayer/Networking/TransportLayers/SteamworksTransport.cs b/Multiplayer/Networking/TransportLayers/SteamworksTransport.cs index af0b88ed..796db2e2 100644 --- a/Multiplayer/Networking/TransportLayers/SteamworksTransport.cs +++ b/Multiplayer/Networking/TransportLayers/SteamworksTransport.cs @@ -187,7 +187,7 @@ public void Send(ITransportPeer peer, NetDataWriter writer, DeliveryMethod deliv public void UpdateSettings(Settings settings) { - int chance = 0; + float chance = 0f; if (settings.SimulatePacketLoss) chance = settings.SimulationPacketLossChance; From 04e3f14152e5dea33785eced09ae05ce4716117d Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 16 Feb 2025 09:52:19 +1000 Subject: [PATCH 256/521] Clean up logging --- .../Components/Networking/TickedQueue.cs | 31 ++++++++++++------- .../Networking/Train/NetworkedCarSpawner.cs | 4 +-- .../Networking/Train/NetworkedTrainCar.cs | 28 ++++++++++------- .../Managers/Client/NetworkClient.cs | 2 +- 4 files changed, 39 insertions(+), 26 deletions(-) diff --git a/Multiplayer/Components/Networking/TickedQueue.cs b/Multiplayer/Components/Networking/TickedQueue.cs index 5842e00b..0b98b6c5 100644 --- a/Multiplayer/Components/Networking/TickedQueue.cs +++ b/Multiplayer/Components/Networking/TickedQueue.cs @@ -17,15 +17,6 @@ public abstract class TickedQueue : MonoBehaviour protected virtual void OnEnable() { - TrainCar car; - int bogie = 0; - - if(car = TrainCar.Resolve(this.gameObject)) - if(this is NetworkedBogie netBogie) - bogie = (car.Bogies[0] == netBogie.Bogie) ? 1 : 2; - - identifier = $"{car?.ID ?? gameObject.GetPath()}{(bogie > 0 ? $" Bogie {bogie}" : "")}"; - NetworkLifecycle.Instance.OnTick += OnTick; } @@ -45,10 +36,10 @@ public void ReceiveSnapshot(T snapshot, uint tick) return; if (snapshots.Count >= QUEUE_LENGTH_WARNING) - Multiplayer.LogWarning($"[{identifier}] Snapshot queue exceeds {QUEUE_LENGTH_WARNING} items. Current size: {snapshots.Count}"); + Multiplayer.LogWarning($"[{GetID()}] Snapshot queue exceeds {QUEUE_LENGTH_WARNING} items. Current size: {snapshots.Count}"); if (lastReceivedTick > 0 && tick - lastReceivedTick > SNAPSHOT_GAP_WARNING) - Multiplayer.LogWarning($"[{identifier}] Large gap between snapshots: {tick - lastReceivedTick} ticks."); + Multiplayer.LogWarning($"[{GetID()}] Large gap between snapshots: {tick - lastReceivedTick} ticks."); lastReceivedTick = tick; lastTick = tick; @@ -72,4 +63,22 @@ public void Clear() } protected abstract void Process(T snapshot, uint snapshotTick); + + private string GetID() + { + if (identifier != string.Empty) + return identifier; + + TrainCar car; + int bogie = 0; + + if (car = TrainCar.Resolve(this.gameObject)) + if (this is NetworkedBogie netBogie) + bogie = (car.Bogies[0] == netBogie.Bogie) ? 1 : 2; + + if (car.logicCar != null) + identifier = $"{car?.ID ?? gameObject.GetPath()}{(bogie > 0 ? $" Bogie {bogie}" : "")}"; + + return identifier; + } } diff --git a/Multiplayer/Components/Networking/Train/NetworkedCarSpawner.cs b/Multiplayer/Components/Networking/Train/NetworkedCarSpawner.cs index cceaccae..cd6b3366 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedCarSpawner.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedCarSpawner.cs @@ -112,7 +112,7 @@ 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})"); + //Multiplayer.LogDebug(() =>$"Couple([{sp.CarId}, {sp.NetId}], trainCar, {autoCouple})"); if (autoCouple) { @@ -139,7 +139,7 @@ private static void HandleCoupling(CouplingData couplingData, Coupler currentCo TrainCar tc = currentCoupler.train; var net = tc.GetNetId(); - Multiplayer.LogDebug(() => $"HandleCoupling([{tc?.ID}, {net}]) couplingData: is front: {currentCoupler.isFrontCoupler}, {couplingData.HoseConnected}, {couplingData.CockOpen}"); + //Multiplayer.LogDebug(() => $"HandleCoupling([{tc?.ID}, {net}]) couplingData: is front: {currentCoupler.isFrontCoupler}, {couplingData.HoseConnected}, {couplingData.CockOpen}"); if (couplingData.IsCoupled) { diff --git a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs index fd50aa0b..ca1f683f 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs @@ -159,7 +159,8 @@ public void Start() { hoseToCoupler[coupler.hoseAndCock] = coupler; - Multiplayer.LogDebug(() => $"TrainCar.Start() [{TrainCar?.ID}, {NetId}], Coupler exists: {coupler != null}, Is front: {coupler.isFrontCoupler}, ChainScript exists: {coupler.ChainScript != null}"); + Multiplayer.LogDebug(() => $"TrainCar Created: {TrainCar?.ID}, {NetId}"); + //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) @@ -1210,10 +1211,13 @@ public void Client_ReceiveTrainPhysicsUpdate(in TrainsetMovementPart movementPar { Vector3 worldPos = movementPart.Position + WorldMover.currentMove; - Vector3 deltaPos = worldPos - TrainCar.transform.position; - if (deltaPos.magnitude > 5f) - { // Threshold for significant position change - Multiplayer.LogWarning($"[{CurrentID}] Large position correction: {deltaPos.magnitude}m at tick {tick}"); + //Vector3 deltaPos = worldPos - TrainCar.transform.position; + //if (deltaPos.magnitude > 5f) + //{ + // // Threshold for significant position change + // Multiplayer.LogWarning($"[{CurrentID}] Large position correction: {deltaPos.magnitude}m at tick {tick}"); + //} + } TrainCar.transform.position = worldPos; @@ -1301,7 +1305,7 @@ public void Client_ReceiveFireboxStateUpdate(float fireboxContents, bool isOn) public void Client_CouplerStateChange(ChainCouplerInteraction.State state, Coupler coupler) { - Multiplayer.LogDebug(() => $"1 Client_CouplerStateChange({state}) trainCar: [{TrainCar?.ID}, {NetId}], coupler is front: {coupler?.isFrontCoupler}"); + //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 @@ -1318,7 +1322,7 @@ public void Client_CouplerStateChange(ChainCouplerInteraction.State state, Coupl originalState = coupler.state; originalCoupledTo = coupler.coupledTo; interactionFlags = CouplerInteractionType.Start; - Multiplayer.LogDebug(() => $"3 Client_CouplerStateChange({state}) trainCar: [{TrainCar?.ID}, {NetId}]"); + //Multiplayer.LogDebug(() => $"3 Client_CouplerStateChange({state}) trainCar: [{TrainCar?.ID}, {NetId}]"); break; case ChainCouplerInteraction.State.Attached_Loose: @@ -1327,7 +1331,7 @@ public void Client_CouplerStateChange(ChainCouplerInteraction.State state, Coupl //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}"); + //Multiplayer.LogDebug(() => $"4 Client_CouplerStateChange({state}) trainCar: [{TrainCar?.ID}, {NetId}] coupledTo: {coupler?.coupledTo?.train?.ID}, first Coupler: {otherCoupler?.train?.ID}"); interactionFlags = CouplerInteractionType.CouplerCouple; } break; @@ -1335,7 +1339,7 @@ public void Client_CouplerStateChange(ChainCouplerInteraction.State state, Coupl case ChainCouplerInteraction.State.Parked: if (couplerInteraction != null) { - Multiplayer.LogDebug(() => $"6 Client_CouplerStateChange({state}) trainCar: [{TrainCar?.ID}, {NetId}]"); + //Multiplayer.LogDebug(() => $"6 Client_CouplerStateChange({state}) trainCar: [{TrainCar?.ID}, {NetId}]"); interactionFlags = CouplerInteractionType.CouplerPark; } break; @@ -1343,7 +1347,7 @@ public void Client_CouplerStateChange(ChainCouplerInteraction.State state, Coupl case ChainCouplerInteraction.State.Dangling: if (couplerInteraction != null) { - Multiplayer.LogDebug(() => $"7 Client_CouplerStateChange({state}) trainCar: [{TrainCar?.ID}, {NetId}]"); + //Multiplayer.LogDebug(() => $"7 Client_CouplerStateChange({state}) trainCar: [{TrainCar?.ID}, {NetId}]"); interactionFlags = CouplerInteractionType.CouplerDrop; } break; @@ -1355,7 +1359,7 @@ 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}"); + //Multiplayer.LogDebug(() => $"8 Client_CouplerStateChange({state}) trainCar: [{TrainCar?.ID}, {NetId}], coupler is front: {coupler?.isFrontCoupler}, Sending: {interactionFlags}"); NetworkLifecycle.Instance.Client.SendCouplerInteraction(interactionFlags, coupler, otherCoupler); //finished interaction, clear flag @@ -1364,7 +1368,7 @@ public void Client_CouplerStateChange(ChainCouplerInteraction.State state, Coupl return; } - Multiplayer.LogDebug(() => $"9 Client_CouplerStateChange({state}) trainCar: [{TrainCar?.ID}, {NetId}]"); + //Multiplayer.LogDebug(() => $"9 Client_CouplerStateChange({state}) trainCar: [{TrainCar?.ID}, {NetId}]"); } #endregion } diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 9e515e5e..e60561ea 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -592,7 +592,7 @@ private void OnCommonHoseConnectedPacket(CommonHoseConnectedPacket packet) 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}"); + //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; From 041a09c98d2cfa7ba674cc7e89037a6a7c9c2366 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 16 Feb 2025 09:59:36 +1000 Subject: [PATCH 257/521] Block switches and turntables from sending states before intialised --- .../Components/Networking/World/NetworkedJunction.cs | 10 ++++++---- .../Components/Networking/World/NetworkedTurntable.cs | 8 ++++++-- .../Networking/Managers/Client/NetworkClient.cs | 4 ++-- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/Multiplayer/Components/Networking/World/NetworkedJunction.cs b/Multiplayer/Components/Networking/World/NetworkedJunction.cs index cafbf66b..e90531fd 100644 --- a/Multiplayer/Components/Networking/World/NetworkedJunction.cs +++ b/Multiplayer/Components/Networking/World/NetworkedJunction.cs @@ -11,6 +11,7 @@ public class NetworkedJunction : IdMonoBehaviour protected override bool IsIdServerAuthoritative => false; public Junction Junction; + private bool initialised = false; protected override void Awake() { @@ -23,16 +24,17 @@ private void Junction_Switched(Junction.SwitchMode switchMode, int branch) { if (NetworkLifecycle.Instance.IsProcessingPacket) return; + NetworkLifecycle.Instance.Client.SendJunctionSwitched(NetId, (byte)branch, switchMode); } - public void Switch(byte mode, byte selectedBranch) + public void Switch(byte mode, byte selectedBranch, bool initialising = false) { - //Junction.selectedBranch = (byte)(selectedBranch - 1); // Junction#Switch increments this before processing - //Junction.Switch((Junction.SwitchMode)mode); - //B99 Junction.Switch((Junction.SwitchMode)mode, selectedBranch); + + if (!initialised && initialising) + initialised = true; } public static bool Get(ushort netId, out NetworkedJunction obj) diff --git a/Multiplayer/Components/Networking/World/NetworkedTurntable.cs b/Multiplayer/Components/Networking/World/NetworkedTurntable.cs index ac5832d5..e26559b7 100644 --- a/Multiplayer/Components/Networking/World/NetworkedTurntable.cs +++ b/Multiplayer/Components/Networking/World/NetworkedTurntable.cs @@ -13,6 +13,7 @@ public class NetworkedTurntable : IdMonoBehaviour public TurntableRailTrack TurntableRailTrack; private float lastYRotation; + private bool initialised = false; protected override void Awake() { @@ -31,18 +32,21 @@ protected override void OnDestroy() private void OnTick(uint tick) { - if (Mathf.Approximately(lastYRotation, TurntableRailTrack.targetYRotation) || UnloadWatcher.isUnloading) + if (UnloadWatcher.isUnloading || !initialised || Mathf.Approximately(lastYRotation, TurntableRailTrack.targetYRotation)) return; lastYRotation = TurntableRailTrack.targetYRotation; NetworkLifecycle.Instance.Client.SendTurntableRotation(NetId, lastYRotation); } - public void SetRotation(float rotation, bool forceConnectionRefresh = false) + public void SetRotation(float rotation, bool forceConnectionRefresh = false, bool initialising = false) { lastYRotation = rotation; TurntableRailTrack.targetYRotation = rotation; TurntableRailTrack.RotateToTargetRotation(forceConnectionRefresh); + + if (!initialised && initialising) + initialised = true; } public static bool Get(byte netId, out NetworkedTurntable obj) diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index e60561ea..e4ecee9e 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -445,14 +445,14 @@ private void OnClientboundRailwayStatePacket(ClientboundRailwayStatePacket packe { if (!NetworkedJunction.Get((ushort)(i + 1), out NetworkedJunction junction)) return; - junction.Switch((byte)Junction.SwitchMode.NO_SOUND, packet.SelectedJunctionBranches[i]); + junction.Switch((byte)Junction.SwitchMode.NO_SOUND, packet.SelectedJunctionBranches[i], true); } for (int i = 0; i < packet.TurntableRotations.Length; i++) { if (!NetworkedTurntable.Get((byte)(i + 1), out NetworkedTurntable turntable)) return; - turntable.SetRotation(packet.TurntableRotations[i], true); + turntable.SetRotation(packet.TurntableRotations[i], true, true); } } From 089c3f0639cf91ea59bfe0a332b44e75787c7b4f Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 1 Mar 2025 15:54:05 +1000 Subject: [PATCH 258/521] Relocated TrainUncouple to Server client should never send uncouple messages with the reworked couple interaction, only required for derailment sync from server to clients --- .../Managers/Server/NetworkServer.cs | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index a829cb17..c18620b7 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -473,6 +473,32 @@ public void SendDebtStatus(bool hasDebt) }, DeliveryMethod.ReliableUnordered, SelfPeer); } + public void SendTrainUncouple(Coupler coupler, bool playAudio, bool dueToBrokenCouple, bool viaChainInteraction) + { + ushort couplerNetId = coupler.train.GetNetId(); + + if (couplerNetId == 0) + { + LogWarning($"SendTrainUncouple failed. Coupler: {coupler.name} {couplerNetId}"); + return; + } + + LogDebug(() => $"SendTrainUncouple({coupler.train.ID}, {coupler.isFrontCoupler}, {dueToBrokenCouple}, {viaChainInteraction})"); + + SendPacketToAll( + new CommonTrainUncouplePacket + { + NetId = couplerNetId, + IsFrontCoupler = coupler.isFrontCoupler, + PlayAudio = playAudio, + ViaChainInteraction = viaChainInteraction, + DueToBrokenCouple = dueToBrokenCouple, + }, + DeliveryMethod.ReliableOrdered, + SelfPeer + ); + } + public void SendJobsCreatePacket(NetworkedStationController networkedStation, NetworkedJob[] jobs, ITransportPeer peer = null) { Multiplayer.Log($"Sending JobsCreatePacket for stationNetId {networkedStation.NetId} with {jobs.Count()} jobs"); From 9615973f22d21e29b8e30ce9669ad8083b03a1ed Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 1 Mar 2025 15:56:28 +1000 Subject: [PATCH 259/521] Fixed packet reliability --- .../Managers/Client/NetworkClient.cs | 40 +++++-------------- 1 file changed, 11 insertions(+), 29 deletions(-) diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index e4ecee9e..6aad8245 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -1022,7 +1022,7 @@ public void SendTimeAdvance(float amountOfTimeToSkipInSeconds) SendPacketToServer(new ServerboundTimeAdvancePacket { amountOfTimeToSkipInSeconds = amountOfTimeToSkipInSeconds - }, DeliveryMethod.ReliableUnordered); + }, DeliveryMethod.ReliableOrdered); } public void SendJunctionSwitched(ushort netId, byte selectedBranch, Junction.SwitchMode mode) @@ -1032,7 +1032,7 @@ public void SendJunctionSwitched(ushort netId, byte selectedBranch, Junction.Swi NetId = netId, SelectedBranch = selectedBranch, Mode = (byte)mode - }, DeliveryMethod.ReliableUnordered); + }, DeliveryMethod.ReliableOrdered); } public void SendTurntableRotation(byte netId, float rotation) @@ -1099,26 +1099,6 @@ public void SendTrainCouple(Coupler coupler, Coupler otherCoupler, bool playAudi }, DeliveryMethod.ReliableUnordered); } - public void SendTrainUncouple(Coupler coupler, bool playAudio, bool dueToBrokenCouple, bool viaChainInteraction) - { - ushort couplerNetId = coupler.train.GetNetId(); - - if (couplerNetId == 0) - { - LogWarning($"SendTrainUncouple failed. Coupler: {coupler.name} {couplerNetId}"); - return; - } - - SendPacketToServer(new CommonTrainUncouplePacket - { - NetId = couplerNetId, - IsFrontCoupler = coupler.isFrontCoupler, - PlayAudio = playAudio, - ViaChainInteraction = viaChainInteraction, - DueToBrokenCouple = dueToBrokenCouple - }, DeliveryMethod.ReliableUnordered); - } - public void SendHoseConnected(Coupler coupler, Coupler otherCoupler, bool playAudio) { ushort couplerNetId = coupler.train.GetNetId(); @@ -1137,7 +1117,7 @@ public void SendHoseConnected(Coupler coupler, Coupler otherCoupler, bool playAu OtherNetId = otherCouplerNetId, OtherIsFront = otherCoupler.isFrontCoupler, PlayAudio = playAudio - }, DeliveryMethod.ReliableUnordered); + }, DeliveryMethod.ReliableOrdered); } public void SendHoseDisconnected(Coupler coupler, bool playAudio) @@ -1150,12 +1130,14 @@ public void SendHoseDisconnected(Coupler coupler, bool playAudio) return; } + LogDebug(() => $"SendHoseDisconnected({coupler.train.ID}, {coupler.isFrontCoupler}, {playAudio})"); + SendPacketToServer(new CommonHoseDisconnectedPacket { NetId = couplerNetId, IsFront = coupler.isFrontCoupler, PlayAudio = playAudio - }, DeliveryMethod.ReliableUnordered); + }, DeliveryMethod.ReliableOrdered); } public void SendMuConnected(MultipleUnitCable cable, MultipleUnitCable otherCable, bool playAudio) @@ -1176,7 +1158,7 @@ public void SendMuConnected(MultipleUnitCable cable, MultipleUnitCable otherCabl OtherNetId = otherCableNetId, OtherIsFront = otherCable.isFront, PlayAudio = playAudio - }, DeliveryMethod.ReliableUnordered); + }, DeliveryMethod.ReliableOrdered); } public void SendMuDisconnected(ushort netId, MultipleUnitCable cable, bool playAudio) @@ -1187,7 +1169,7 @@ public void SendMuDisconnected(ushort netId, MultipleUnitCable cable, bool playA NetId = netId, IsFront = cable.isFront, PlayAudio = playAudio - }, DeliveryMethod.ReliableUnordered); + }, DeliveryMethod.ReliableOrdered); } public void SendCockState(ushort netId, Coupler coupler, bool isOpen) @@ -1197,7 +1179,7 @@ public void SendCockState(ushort netId, Coupler coupler, bool isOpen) NetId = netId, IsFront = coupler.isFrontCoupler, IsOpen = isOpen - }, DeliveryMethod.ReliableUnordered); + }, DeliveryMethod.ReliableOrdered); } public void SendBrakeCylinderReleased(ushort netId) @@ -1205,7 +1187,7 @@ public void SendBrakeCylinderReleased(ushort netId) SendPacketToServer(new CommonBrakeCylinderReleasePacket { NetId = netId - }, DeliveryMethod.ReliableUnordered); + }, DeliveryMethod.ReliableOrdered); } public void SendHandbrakePositionChanged(ushort netId, float position) @@ -1259,7 +1241,7 @@ public void SendFuses(ushort netId, string[] fuseIds, bool[] fuseValues) NetId = netId, FuseIds = fuseIds, FuseValues = fuseValues - }, DeliveryMethod.ReliableUnordered); + }, DeliveryMethod.ReliableOrdered); } public void SendTrainSyncRequest(ushort netId) From 54c72b0c0fd0f433ac5f6cae5fda3a0ef76f66c1 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 1 Mar 2025 15:57:07 +1000 Subject: [PATCH 260/521] Reinstated client-side uncouple packet code --- .../Networking/Managers/Client/NetworkClient.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 6aad8245..f749de85 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -565,16 +565,16 @@ private void OnCommonTrainCouplePacket(CommonTrainCouplePacket packet) private void OnCommonTrainUncouplePacket(CommonTrainUncouplePacket packet) { - //if (!NetworkedTrainCar.GetTrainCar(packet.NetId, out TrainCar trainCar)) - //{ - // LogDebug(() => $"OnCommonTrainUncouplePacket() netId: {packet.NetId}, trainCar found?: {trainCar != null}"); - // return; - //} + if (!NetworkedTrainCar.GetTrainCar(packet.NetId, out TrainCar trainCar)) + { + LogDebug(() => $"OnCommonTrainUncouplePacket() netId: {packet.NetId}, trainCar found?: {trainCar != null}"); + return; + } - //LogDebug(() => $"OnCommonTrainUncouplePacket() netId: {packet.NetId}, trainCar: {trainCar.ID}, isFront: {packet.IsFrontCoupler}, playAudio: {packet.PlayAudio}, DueToBrokenCouple: {packet.DueToBrokenCouple}, viaChainInteraction: {packet.ViaChainInteraction}"); + LogDebug(() => $"OnCommonTrainUncouplePacket() netId: {packet.NetId}, trainCar: {trainCar.ID}, isFront: {packet.IsFrontCoupler}, playAudio: {packet.PlayAudio}, DueToBrokenCouple: {packet.DueToBrokenCouple}, viaChainInteraction: {packet.ViaChainInteraction}"); - //Coupler coupler = packet.IsFrontCoupler ? trainCar.frontCoupler : trainCar.rearCoupler; - //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) From 8dbb8fd1049fa6c69263143fe7a5b0a41bf7b9ae Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 1 Mar 2025 15:58:20 +1000 Subject: [PATCH 261/521] Add server-side coupling break detection --- .../Networking/Train/NetworkedTrainCar.cs | 44 ++++++++++++++----- 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs index ca1f683f..22c52e6e 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs @@ -207,6 +207,10 @@ public void Start() bogie1.TrackChanged += Server_BogieTrackChanged; bogie2.TrackChanged += Server_BogieTrackChanged; + + TrainCar.frontCoupler.Uncoupled += Server_CouplerUncoupled; + TrainCar.rearCoupler.Uncoupled += Server_CouplerUncoupled; + TrainCar.CarDamage.CarEffectiveHealthStateUpdate += Server_CarHealthUpdate; brakeSystem.MainResPressureChanged += Server_MainResUpdate; @@ -263,6 +267,9 @@ public void OnDisable() bogie1.TrackChanged -= Server_BogieTrackChanged; bogie2.TrackChanged -= Server_BogieTrackChanged; + TrainCar.frontCoupler.Uncoupled -= Server_CouplerUncoupled; + TrainCar.rearCoupler.Uncoupled -= Server_CouplerUncoupled; + TrainCar.CarDamage.CarEffectiveHealthStateUpdate -= Server_CarHealthUpdate; if(brakeSystem != null) @@ -417,6 +424,11 @@ private void Server_FireboxUpdate(float normalizedPressure, float pressure) fireboxDirty = true; } + private void Server_CouplerUncoupled(object _,UncoupleEventArgs args) + { + sendCouplers |= args.dueToBrokenCouple; + } + private void Server_OnTick(uint tick) { if (UnloadWatcher.isUnloading) @@ -424,7 +436,7 @@ private void Server_OnTick(uint tick) Server_SendBrakeStates(); Server_SendFireBoxState(); - //Server_SendCouplers(); + Server_SendCouplers(); Server_SendCables(); Server_SendCargoState(); Server_SendHealthState(); @@ -462,17 +474,25 @@ private void Server_SendCouplers() 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); - - if (TrainCar.rearCoupler.hoseAndCock.IsHoseConnected) - NetworkLifecycle.Instance.Client.SendHoseConnected(TrainCar.rearCoupler, TrainCar.rearCoupler.coupledTo, false); + if(!TrainCar.frontCoupler.IsCoupled()) + // NetworkLifecycle.Instance.Client.SendTrainCouple(TrainCar.frontCoupler,TrainCar.frontCoupler.coupledTo,false, false); + //else + NetworkLifecycle.Instance.Server.SendTrainUncouple(TrainCar.frontCoupler,true,true, false); + + if(!TrainCar.rearCoupler.IsCoupled()) + // NetworkLifecycle.Instance.Client.SendTrainCouple(TrainCar.rearCoupler,TrainCar.rearCoupler.coupledTo,false, false); + //else + NetworkLifecycle.Instance.Server.SendTrainUncouple(TrainCar.rearCoupler, true, true, false); + + if (!TrainCar.frontCoupler.hoseAndCock.IsHoseConnected) + // NetworkLifecycle.Instance.Client.SendHoseConnected(TrainCar.frontCoupler, TrainCar.frontCoupler.coupledTo, false); + //else + NetworkLifecycle.Instance.Client.SendHoseDisconnected(TrainCar.frontCoupler, true); + + if (!TrainCar.rearCoupler.hoseAndCock.IsHoseConnected) + // NetworkLifecycle.Instance.Client.SendHoseConnected(TrainCar.rearCoupler, TrainCar.rearCoupler.coupledTo, false); + //else + NetworkLifecycle.Instance.Client.SendHoseDisconnected(TrainCar.rearCoupler, true); NetworkLifecycle.Instance.Client.SendCockState(NetId, TrainCar.frontCoupler, TrainCar.frontCoupler.IsCockOpen); NetworkLifecycle.Instance.Client.SendCockState(NetId, TrainCar.rearCoupler, TrainCar.rearCoupler.IsCockOpen); From a0f70a7cd36f97c02e9e4fd1314c5b35590024ae Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 1 Mar 2025 15:59:08 +1000 Subject: [PATCH 262/521] Improve position and rotation sync for TrainCars --- .../Train/NetworkTrainsetWatcher.cs | 31 +++++++++++++++---- .../Networking/Train/NetworkedTrainCar.cs | 23 +++++++++----- 2 files changed, 40 insertions(+), 14 deletions(-) diff --git a/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs b/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs index d5d67529..ef395feb 100644 --- a/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs +++ b/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs @@ -14,6 +14,7 @@ public class NetworkTrainsetWatcher : SingletonBehaviour const float DESIRED_FULL_SYNC_INTERVAL = 2f; // in seconds const int MAX_UNSYNC_TICKS = (int)(NetworkLifecycle.TICK_RATE * DESIRED_FULL_SYNC_INTERVAL); + const float VELOCITY_THRESHOLD = 0.01f; protected override void Awake() { @@ -58,6 +59,7 @@ private void Server_TickSet(Trainset set, uint tick) cachedSendPacket.FirstNetId = set.firstCar.GetNetId(); cachedSendPacket.LastNetId = set.lastCar.GetNetId(); + //car may not be initialised, missing a valid NetID if (cachedSendPacket.FirstNetId == 0 || cachedSendPacket.LastNetId == 0) return; @@ -73,16 +75,24 @@ private void Server_TickSet(Trainset set, uint tick) //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; //Even if the car is stationary, if the max ticks has been exceeded we will still sync anyTracksDirty |= netTC.BogieTracksDirty; } - - //Even if the car is stationary, if the max ticks has been exceeded we will still sync - if (!trainCar.isStationary) + + if (trainCar.derailed) + { + // Check if derailed car is actually moving + float velocityMagnitude = trainCar.rb.velocity.magnitude; + if (velocityMagnitude > VELOCITY_THRESHOLD) + { + anyCarMoving = true; + } + } + else if (!trainCar.isStationary) anyCarMoving = true; - //we can finish checking early if we have BOTH a dirty and a max ticks - if (anyCarMoving && maxTicksReached) + //we can finish checking early if we have either a car moving or a car not sync'd within the max-tick threshold + if (anyCarMoving || maxTicksReached) break; } @@ -161,7 +171,14 @@ public void Client_HandleTrainsetPhysicsUpdate(ClientboundTrainsetPhysicsPacket for (int i = 0; i < packet.TrainsetParts.Length; i++) { if (NetworkedTrainCar.Get(packet.TrainsetParts[i].NetId ,out NetworkedTrainCar networkedTrainCar)) + { + Multiplayer.LogDebug(()=>$"Applying TrainPhysicsUpdate to {packet.TrainsetParts[i].NetId}"); networkedTrainCar.Client_ReceiveTrainPhysicsUpdate(in packet.TrainsetParts[i], packet.Tick); + } + else + { + Multiplayer.LogWarning($"Unable to apply TrainPhysicsUpdate to {packet.TrainsetParts[i].NetId}, NetworkedTrainCar not found!"); + } } return; } @@ -176,6 +193,8 @@ public void Client_HandleTrainsetPhysicsUpdate(ClientboundTrainsetPhysicsPacket { if(set.cars[i].TryNetworked(out NetworkedTrainCar networkedTrainCar)) networkedTrainCar.Client_ReceiveTrainPhysicsUpdate(in packet.TrainsetParts[i], packet.Tick); + else + Multiplayer.LogWarning($"Unable to apply TrainPhysicsUpdate to TrainSet with FirstNetId: {packet.FirstNetId}, NetworkedTrainCar not found!"); } } diff --git a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs index 22c52e6e..f580f58b 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs @@ -1216,13 +1216,17 @@ public void Client_ReceiveTrainPhysicsUpdate(in TrainsetMovementPart movementPar if (movementPart.typeFlag == TrainsetMovementPart.MovementType.RigidBody) { + Vector3 expectedPosition = movementPart.RigidbodySnapshot.Position + WorldMover.currentMove; + Multiplayer.LogDebug(() => $"Processing derailed physics for car {CurrentID} at tick {tick}, current position: {TrainCar.transform.position} expected position: {expectedPosition}"); //Multiplayer.LogDebug(() => $"Client_ReceiveTrainPhysicsUpdate({TrainCar.ID}, {tick}): is RigidBody"); TrainCar.Derail(); - TrainCar.stress.ResetTrainStress(); - if (TrainCar.rb != null) - TrainCar.rb.constraints = RigidbodyConstraints.FreezeAll; + movementPart.RigidbodySnapshot.Apply(TrainCar.rb); + //TrainCar.stress.ResetTrainStress(); + //if (TrainCar.rb != null) + // TrainCar.rb.constraints = RigidbodyConstraints.FreezeAll; Client_trainRigidbodyQueue.ReceiveSnapshot(movementPart.RigidbodySnapshot, tick); + Multiplayer.LogDebug(() => $"Derailed car {TrainCar.ID} positioned at {TrainCar.transform.position}"); } else { @@ -1238,10 +1242,14 @@ public void Client_ReceiveTrainPhysicsUpdate(in TrainsetMovementPart movementPar // Multiplayer.LogWarning($"[{CurrentID}] Large position correction: {deltaPos.magnitude}m at tick {tick}"); //} + if (TrainCar.rb != null) + { + TrainCar.rb.MovePosition(worldPos); + TrainCar.rb.MoveRotation(movementPart.Rotation); } - TrainCar.transform.position = worldPos; - TrainCar.transform.rotation = movementPart.Rotation; + //TrainCar.transform.position = worldPos; + //TrainCar.transform.rotation = movementPart.Rotation; //clear the queues? Client_trainSpeedQueue.Clear(); @@ -1257,11 +1265,10 @@ public void Client_ReceiveTrainPhysicsUpdate(in TrainsetMovementPart movementPar client_bogie1Queue.ReceiveSnapshot(movementPart.Bogie1, tick); client_bogie2Queue.ReceiveSnapshot(movementPart.Bogie2, tick); - } - if (!TrainCar.derailed && TrainCar.rb != null) - TrainCar.rb.constraints = RigidbodyConstraints.None; + //if (!TrainCar.derailed && TrainCar.rb != null) + // TrainCar.rb.constraints = RigidbodyConstraints.None; } public void Client_ReceiveBrakeStateUpdate(ClientboundBrakeStateUpdatePacket packet) From 12b05919be78c76f142fc82ffd5532bc2807aa37 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 1 Mar 2025 16:30:27 +1000 Subject: [PATCH 263/521] Fixed issue with TrainsetMovementPart deserialisation --- Multiplayer/Multiplayer.csproj | 2 +- Multiplayer/Networking/Data/Train/TrainsetMovementPart.cs | 4 ++-- info.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Multiplayer/Multiplayer.csproj b/Multiplayer/Multiplayer.csproj index 537fb85c..857eaba3 100644 --- a/Multiplayer/Multiplayer.csproj +++ b/Multiplayer/Multiplayer.csproj @@ -3,7 +3,7 @@ net48 latest Multiplayer - 0.1.10.0 + 0.1.10.2 diff --git a/Multiplayer/Networking/Data/Train/TrainsetMovementPart.cs b/Multiplayer/Networking/Data/Train/TrainsetMovementPart.cs index e3d12d39..d85efe97 100644 --- a/Multiplayer/Networking/Data/Train/TrainsetMovementPart.cs +++ b/Multiplayer/Networking/Data/Train/TrainsetMovementPart.cs @@ -89,7 +89,7 @@ public static void Serialize(NetDataWriter writer, TrainsetMovementPart data) public static TrainsetMovementPart Deserialize(NetDataReader reader) { - ushort netId = 0; + ushort netId; float speed = 0; float slowBuildUpStress = 0; Vector3? position = null; @@ -120,6 +120,6 @@ public static TrainsetMovementPart Deserialize(NetDataReader reader) rotation = QuaternionSerializer.Deserialize(reader); } - return new TrainsetMovementPart(0, speed, slowBuildUpStress, bd1, bd2, position, rotation); + return new TrainsetMovementPart(netId, speed, slowBuildUpStress, bd1, bd2, position, rotation); } } diff --git a/info.json b/info.json index 7857c50c..853bb72a 100644 --- a/info.json +++ b/info.json @@ -1,6 +1,6 @@ { "Id": "Multiplayer", - "Version": "0.1.10.0", + "Version": "0.1.10.2", "DisplayName": "Multiplayer", "Author": "Insprill, Macka, Morm", "EntryMethod": "Multiplayer.Multiplayer.Load", From ce0139e4ed39166f11b2568dbf8bc831f5bd54cf Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 2 Mar 2025 15:24:06 +1000 Subject: [PATCH 264/521] Fixed issue with TrainsetMovementPart deserialisation --- Multiplayer/Networking/Data/Train/TrainsetMovementPart.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Multiplayer/Networking/Data/Train/TrainsetMovementPart.cs b/Multiplayer/Networking/Data/Train/TrainsetMovementPart.cs index d85efe97..2acc8d81 100644 --- a/Multiplayer/Networking/Data/Train/TrainsetMovementPart.cs +++ b/Multiplayer/Networking/Data/Train/TrainsetMovementPart.cs @@ -103,7 +103,7 @@ public static TrainsetMovementPart Deserialize(NetDataReader reader) if (dataType.HasFlag(MovementType.RigidBody)) { - return new TrainsetMovementPart(0, RigidbodySnapshot.Deserialize(reader)); + return new TrainsetMovementPart(netId, RigidbodySnapshot.Deserialize(reader)); } if (dataType.HasFlag(MovementType.Physics)) From 4997bb365f519887fa8a50c77f4b5e4ed0f203c1 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 2 Mar 2025 15:28:20 +1000 Subject: [PATCH 265/521] Prevent client couplers breaking in a derail During derail the base game changes the way the joints behave, resulting in inconsistent simulation between the server and client(s). `TrainCar.UpdateCouplerJoints` is called on Derail, by patching it so it's only active on the server, the clients are far less likely to break couplings. Broken couplings on the server are communicated with the TrainCarUncouple packet. --- Multiplayer/Patches/Train/TrainCarPatch.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Multiplayer/Patches/Train/TrainCarPatch.cs b/Multiplayer/Patches/Train/TrainCarPatch.cs index 5b92947b..4f807633 100644 --- a/Multiplayer/Patches/Train/TrainCarPatch.cs +++ b/Multiplayer/Patches/Train/TrainCarPatch.cs @@ -41,4 +41,11 @@ private static void Rerail_Prefix(TrainCar __instance, RailTrack rerailTrack, Ve return; NetworkLifecycle.Instance.Server.SendRerailTrainCar(networkedTrainCar.NetId, NetworkedRailTrack.GetFromRailTrack(rerailTrack).NetId, worldPos - WorldMover.currentMove, forward); } + + [HarmonyPrefix] + [HarmonyPatch(nameof(TrainCar.UpdateCouplerJoints))] + private static bool UpdateCouplerJoints(TrainCar __instance) + { + return NetworkLifecycle.Instance.IsHost(); + } } From d034ce67f6eee13c7670f6822752dcb61f1f7b20 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 2 Mar 2025 18:12:44 +1000 Subject: [PATCH 266/521] Fix network spam for derailed car physics Updated `NetworkTrainsetWatcher` to fix issues where states are not being reset correctly. Updated `NetworkedTrainCar.Client_ReceiveTrainPhysicsUpdate()` to make the rigid body kinematic once the physics has settled. --- .../Train/NetworkTrainsetWatcher.cs | 16 ++++---- .../Networking/Train/NetworkedTrainCar.cs | 37 ++++++++++--------- 2 files changed, 28 insertions(+), 25 deletions(-) diff --git a/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs b/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs index ef395feb..2c4053ad 100644 --- a/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs +++ b/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs @@ -14,7 +14,7 @@ public class NetworkTrainsetWatcher : SingletonBehaviour const float DESIRED_FULL_SYNC_INTERVAL = 2f; // in seconds const int MAX_UNSYNC_TICKS = (int)(NetworkLifecycle.TICK_RATE * DESIRED_FULL_SYNC_INTERVAL); - const float VELOCITY_THRESHOLD = 0.01f; + public const float VELOCITY_THRESHOLD = 0.01f; protected override void Awake() { @@ -91,9 +91,12 @@ private void Server_TickSet(Trainset set, uint tick) else if (!trainCar.isStationary) anyCarMoving = true; - //we can finish checking early if we have either a car moving or a car not sync'd within the max-tick threshold + // We can finish checking early if we have either a car moving or a car not sync'd within the max-tick threshold if (anyCarMoving || maxTicksReached) + { + //Multiplayer.LogDebug(() => $"Server_TickSet() TrainCar {trainCar.ID} ({netTC?.NetId}) from set: {cachedSendPacket.FirstNetId} is moving or due for sync! stationary: {trainCar.isStationary}, RB velocity: {trainCar.rb.velocity} {trainCar.rb.velocity.magnitude}, tracks dirty: {netTC?.BogieTracksDirty} sync: {netTC?.TicksSinceSync >= MAX_UNSYNC_TICKS}"); break; + } } //if any car is dirty or exceeded its max ticks we will re-sync the entire train @@ -123,11 +126,8 @@ private void Server_TickSet(Trainset set, uint tick) //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( @@ -140,13 +140,15 @@ private void Server_TickSet(Trainset set, uint tick) rotation //only used in full sync ); } + + //reset this car's states + networkedTrainCar.TicksSinceSync = 0; + networkedTrainCar.BogieTracksDirty = false; } - //Multiplayer.Log($"Server_TickSet({set.firstCar.ID}): SendTrainsetPhysicsUpdate, tick: {cachedSendPacket.Tick}"); cachedSendPacket.TrainsetParts = trainsetParts; NetworkLifecycle.Instance.Server.SendTrainsetPhysicsUpdate(cachedSendPacket, anyTracksDirty); } - #endregion #region Client diff --git a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs index f580f58b..0f80493c 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 const float MAX_FIREBOX_DELTA = 0.1f; private const float MAX_PORT_DELTA = 0.001f; + private const uint MIN_KINEMATIC_CYCLES = 10; public string CurrentID { get; private set; } @@ -121,6 +122,8 @@ public static bool TryGetFromTrainCar(TrainCar trainCar, out NetworkedTrainCar n private Coupler couplerInteraction; private ChainCouplerInteraction.State originalState; private Coupler originalCoupledTo; + + private uint kinematicCycles = 0; #endregion protected override bool IsIdServerAuthoritative => true; @@ -1217,16 +1220,14 @@ public void Client_ReceiveTrainPhysicsUpdate(in TrainsetMovementPart movementPar if (movementPart.typeFlag == TrainsetMovementPart.MovementType.RigidBody) { Vector3 expectedPosition = movementPart.RigidbodySnapshot.Position + WorldMover.currentMove; - Multiplayer.LogDebug(() => $"Processing derailed physics for car {CurrentID} at tick {tick}, current position: {TrainCar.transform.position} expected position: {expectedPosition}"); - //Multiplayer.LogDebug(() => $"Client_ReceiveTrainPhysicsUpdate({TrainCar.ID}, {tick}): is RigidBody"); + //Multiplayer.LogDebug(() => $"Processing derailed physics for car {CurrentID} at tick {tick}, current position: {TrainCar.transform.position} expected position: {expectedPosition}"); + TrainCar.Derail(); movementPart.RigidbodySnapshot.Apply(TrainCar.rb); - //TrainCar.stress.ResetTrainStress(); - //if (TrainCar.rb != null) - // TrainCar.rb.constraints = RigidbodyConstraints.FreezeAll; Client_trainRigidbodyQueue.ReceiveSnapshot(movementPart.RigidbodySnapshot, tick); - Multiplayer.LogDebug(() => $"Derailed car {TrainCar.ID} positioned at {TrainCar.transform.position}"); + + //Multiplayer.LogDebug(() => $"Derailed car {TrainCar.ID} positioned at {TrainCar.transform.position}"); } else { @@ -1235,22 +1236,12 @@ public void Client_ReceiveTrainPhysicsUpdate(in TrainsetMovementPart movementPar { Vector3 worldPos = movementPart.Position + WorldMover.currentMove; - //Vector3 deltaPos = worldPos - TrainCar.transform.position; - //if (deltaPos.magnitude > 5f) - //{ - // // Threshold for significant position change - // Multiplayer.LogWarning($"[{CurrentID}] Large position correction: {deltaPos.magnitude}m at tick {tick}"); - //} - if (TrainCar.rb != null) { TrainCar.rb.MovePosition(worldPos); TrainCar.rb.MoveRotation(movementPart.Rotation); } - //TrainCar.transform.position = worldPos; - //TrainCar.transform.rotation = movementPart.Rotation; - //clear the queues? Client_trainSpeedQueue.Clear(); Client_trainRigidbodyQueue.Clear(); @@ -1267,8 +1258,18 @@ public void Client_ReceiveTrainPhysicsUpdate(in TrainsetMovementPart movementPar } - //if (!TrainCar.derailed && TrainCar.rb != null) - // TrainCar.rb.constraints = RigidbodyConstraints.None; + bool kinematic = movementPart.Speed < NetworkTrainsetWatcher.VELOCITY_THRESHOLD && (movementPart.RigidbodySnapshot != null && movementPart.RigidbodySnapshot.Velocity.magnitude < NetworkTrainsetWatcher.VELOCITY_THRESHOLD); + + if (kinematic && kinematicCycles < MIN_KINEMATIC_CYCLES) + kinematicCycles++; + else + TrainCar.rb.isKinematic = kinematic; + + if (!kinematic) + { + kinematicCycles = 0; + TrainCar.rb.isKinematic = kinematic; + } } public void Client_ReceiveBrakeStateUpdate(ClientboundBrakeStateUpdatePacket packet) From c7d69085da5f023053c5f6460041e41fe5146d04 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 2 Mar 2025 18:13:21 +1000 Subject: [PATCH 267/521] Fix coupling breaking sync Ensure packets are delivered in the correct order --- Multiplayer/Networking/Managers/Server/NetworkServer.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index c18620b7..210cd974 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -494,8 +494,7 @@ public void SendTrainUncouple(Coupler coupler, bool playAudio, bool dueToBrokenC ViaChainInteraction = viaChainInteraction, DueToBrokenCouple = dueToBrokenCouple, }, - DeliveryMethod.ReliableOrdered, - SelfPeer + DeliveryMethod.ReliableOrdered ); } From ce671a02bd209e81c6ecc8ae244e46fe0495df0b Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 2 Mar 2025 18:16:10 +1000 Subject: [PATCH 268/521] Refactor player positioning Ensure when the player is shown in the correct orientation when on a car that's been rotated/laying on its side. --- .../Networking/Player/NetworkedPlayer.cs | 58 ++++++++++++------- 1 file changed, 37 insertions(+), 21 deletions(-) diff --git a/Multiplayer/Components/Networking/Player/NetworkedPlayer.cs b/Multiplayer/Components/Networking/Player/NetworkedPlayer.cs index 561267aa..97f562eb 100644 --- a/Multiplayer/Components/Networking/Player/NetworkedPlayer.cs +++ b/Multiplayer/Components/Networking/Player/NetworkedPlayer.cs @@ -18,15 +18,18 @@ public class NetworkedPlayer : MonoBehaviour private string username; - public string Username { + public string Username + { get => username; - set { + set + { username = value; nameTag.SetUsername(value); } } private bool isOnCar; + private TrainCar trainCar; private Transform selfTransform; private Vector3 targetPos; @@ -74,48 +77,61 @@ private void Update() float t = Time.deltaTime * LERP_SPEED; Vector3 position = Vector3.Lerp(isOnCar ? selfTransform.localPosition : selfTransform.position, isOnCar ? targetPos : targetPos + WorldMover.currentMove, t); - Quaternion rotation = Quaternion.Lerp(isOnCar ? selfTransform.localRotation : selfTransform.rotation, targetRotation, t); - + moveDir = Vector2.Lerp(moveDir, targetMoveDir, t); animationHandler.SetMoveDir(moveDir); - if (isOnCar) + if (isOnCar && trainCar != null) { selfTransform.localPosition = position; - selfTransform.localRotation = rotation; + + // Calculate a world-up-respecting rotation + // This creates a rotation where Y points up in world space + // but the forward direction aligns with the car's forward projected onto the horizontal plane + Vector3 carForward = trainCar.transform.forward; + Vector3 worldUp = Vector3.up; + + // Project car's forward onto the horizontal plane + Vector3 horizontalForward = Vector3.ProjectOnPlane(carForward, worldUp).normalized; + if (horizontalForward.sqrMagnitude < 0.001f) + horizontalForward = Vector3.ProjectOnPlane(trainCar.transform.right, worldUp).normalized; + + // Create base orientation aligned with world up but facing car's forward direction + Quaternion baseRotation = Quaternion.LookRotation(horizontalForward, worldUp); + + // Apply the desired Y rotation (player's facing direction) on top of this base rotation + Quaternion targetWorldRotation = baseRotation * Quaternion.Euler(0, targetRotation.eulerAngles.y, 0); + + // Apply rotation in world space despite being a child transform + selfTransform.rotation = Quaternion.Lerp(selfTransform.rotation, targetWorldRotation, t); } else { selfTransform.position = position; - selfTransform.rotation = rotation; + selfTransform.rotation = Quaternion.Lerp(transform.rotation, targetRotation, t); } } - public void UpdatePosition(Vector3 position, Vector2 moveDir, float rotation, bool isJumping, bool movePacketIsOnCar) + public void UpdatePosition(Vector3 position, Vector2 moveDir, float rotationY, bool isJumping, bool movePacketIsOnCar) { + targetPos = position; targetMoveDir = moveDir; + animationHandler.SetIsJumping(isJumping); if (isOnCar != movePacketIsOnCar) return; - targetPos = position; - targetRotation = Quaternion.Euler(0, rotation, 0); + targetRotation = Quaternion.Euler(0, rotationY, 0); } public void UpdateCar(ushort netId) { - isOnCar = NetworkedTrainCar.GetTrainCar(netId, out TrainCar trainCar); + isOnCar = NetworkedTrainCar.GetTrainCar(netId, out 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; + if (isOnCar) + selfTransform.SetParent(trainCar.transform, true); + else + selfTransform.SetParent(null, true); } } From 48e900a268070656c7aff398ff5d62d9009b4883 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 2 Mar 2025 20:06:20 +1000 Subject: [PATCH 269/521] Rev up for release --- Multiplayer/Multiplayer.csproj | 2 +- info.json | 4 ++-- releases.json | 6 ++++++ 3 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 releases.json diff --git a/Multiplayer/Multiplayer.csproj b/Multiplayer/Multiplayer.csproj index 857eaba3..4a4f6355 100644 --- a/Multiplayer/Multiplayer.csproj +++ b/Multiplayer/Multiplayer.csproj @@ -3,7 +3,7 @@ net48 latest Multiplayer - 0.1.10.2 + 0.1.10.4 diff --git a/info.json b/info.json index 853bb72a..23b50093 100644 --- a/info.json +++ b/info.json @@ -1,6 +1,6 @@ { "Id": "Multiplayer", - "Version": "0.1.10.2", + "Version": "0.1.10.4", "DisplayName": "Multiplayer", "Author": "Insprill, Macka, Morm", "EntryMethod": "Multiplayer.Multiplayer.Load", @@ -8,5 +8,5 @@ "LoadAfter": [ "RemoteDispatch" ], - "Repository": "https://www.andrewcraigmackenzie.com/unitymods/Releases.json" + "Repository": "https://raw.githubusercontent.com/AMacro/dv-multiplayer/refs/heads/beta/releases.json" } diff --git a/releases.json b/releases.json new file mode 100644 index 00000000..da7b7537 --- /dev/null +++ b/releases.json @@ -0,0 +1,6 @@ +{ + "Releases": + [ + {"Id": "Multiplayer", "Version": "0.1.10.4", "DownloadUrl": "https://github.com/AMacro/dv-multiplayer/releases/download/v0.1.10.4-Beta/Multiplayer.0.1.10.4.zip"} + ] +} \ No newline at end of file From 9b310f8d76821b85c3e4de8e0ea9fc038ae7f922 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 15 Mar 2025 16:12:18 +1000 Subject: [PATCH 270/521] Fix issue with tick counter not incrementing --- .../Networking/Managers/Client/NetworkClient.cs | 2 +- .../Networking/Managers/NetworkManager.cs | 2 ++ .../Networking/Managers/Server/NetworkServer.cs | 16 ++++++++++------ 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index f749de85..e4a2e920 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -200,7 +200,7 @@ public override void OnNetworkLatencyUpdate(ITransportPeer peer, int latency) { Ping = latency; - if (latency > 150) + if (latency > LATENCY_FLAG) LogWarning($"High Ping Detected! {latency}ms"); } diff --git a/Multiplayer/Networking/Managers/NetworkManager.cs b/Multiplayer/Networking/Managers/NetworkManager.cs index 970384e3..af3ef91e 100644 --- a/Multiplayer/Networking/Managers/NetworkManager.cs +++ b/Multiplayer/Networking/Managers/NetworkManager.cs @@ -12,6 +12,8 @@ namespace Multiplayer.Networking.Managers; public abstract class NetworkManager { + protected const int LATENCY_FLAG = 150; + protected readonly NetPacketProcessor netPacketProcessor; protected readonly NetDataWriter cachedWriter = new(); diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index 210cd974..a18a855d 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -233,16 +233,20 @@ public override void OnNetworkLatencyUpdate(ITransportPeer peer, int latency) SendPacketToAll(clientboundPingUpdatePacket, DeliveryMethod.ReliableUnordered, peer); - SendPacket(peer, new ClientboundTickSyncPacket - { - ServerTick = NetworkLifecycle.Instance.Tick - }, DeliveryMethod.ReliableUnordered); - - if (latency > 150) + if (latency > LATENCY_FLAG) { serverPlayers.TryGetValue((byte)peer.Id, out var player); LogWarning($"High Ping Detected! Player: \"{player?.Username}\", ping: {latency}ms"); } + + // Ensure we don't send a TickSync packet to ourselves + if (peer.Id == SelfPeer.Id) + return; + + SendPacket(peer, new ClientboundTickSyncPacket + { + ServerTick = NetworkLifecycle.Instance.Tick + }, DeliveryMethod.ReliableUnordered); } public override void OnConnectionRequest(NetDataReader requestData, IConnectionRequest request) From 4eb7d917a2584177873e4472b07324c5969a1aab Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 15 Mar 2025 21:14:44 +1000 Subject: [PATCH 271/521] Refactor bogie position sync Under some conditions track data was not being sent when required. Refactored to always send the track netId if the bogie is not derailed and has a valid track. Track direction has been converted to a flag, rather than sending an Int32. --- .../Components/Networking/TickedQueue.cs | 2 +- .../Train/NetworkTrainsetWatcher.cs | 4 +- .../Networking/Train/NetworkedBogie.cs | 15 ++++--- .../Networking/Train/NetworkedTrainCar.cs | 5 ++- .../Networking/Data/Train/BogieData.cs | 39 +++++++++---------- .../Data/Train/TrainsetSpawnPart.cs | 4 +- .../Managers/Server/NetworkServer.cs | 1 + 7 files changed, 38 insertions(+), 32 deletions(-) diff --git a/Multiplayer/Components/Networking/TickedQueue.cs b/Multiplayer/Components/Networking/TickedQueue.cs index 0b98b6c5..5728fac3 100644 --- a/Multiplayer/Components/Networking/TickedQueue.cs +++ b/Multiplayer/Components/Networking/TickedQueue.cs @@ -13,7 +13,7 @@ public abstract class TickedQueue : MonoBehaviour private uint lastTick; private uint lastReceivedTick; private readonly Queue<(uint, T)> snapshots = new(); - private string identifier; + protected string identifier; protected virtual void OnEnable() { diff --git a/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs b/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs index 2c4053ad..cfd821e6 100644 --- a/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs +++ b/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs @@ -134,8 +134,8 @@ private void Server_TickSet(Trainset set, uint tick) networkedTrainCar.NetId, trainCar.GetForwardSpeed(), trainCar.stress.slowBuildUpStress, - BogieData.FromBogie(trainCar.Bogies[0], networkedTrainCar.BogieTracksDirty), - BogieData.FromBogie(trainCar.Bogies[1], networkedTrainCar.BogieTracksDirty), + BogieData.FromBogie(trainCar.Bogies[0]), + BogieData.FromBogie(trainCar.Bogies[1]), position, //only used in full sync rotation //only used in full sync ); diff --git a/Multiplayer/Components/Networking/Train/NetworkedBogie.cs b/Multiplayer/Components/Networking/Train/NetworkedBogie.cs index f0c8334d..abdfd020 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedBogie.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedBogie.cs @@ -39,6 +39,9 @@ protected IEnumerator WaitForBogie() protected override void Process(BogieData snapshot, uint snapshotTick) { + + //Multiplayer.LogDebug(()=>$"NetworkedBogie.Process({identifier}) DataFlags: {snapshot.DataFlags}, {snapshotTick}, {snapshot.TrackNetId}, {snapshot.PositionAlongTrack} {snapshot.TrackDirection}"); + if (Bogie.HasDerailed) return; @@ -52,19 +55,21 @@ protected override void Process(BogieData snapshot, uint snapshotTick) { if (!NetworkedRailTrack.Get(snapshot.TrackNetId, out NetworkedRailTrack track)) { - Multiplayer.LogWarning($"NetworkedBogie.Process() Failed to find track {snapshot.TrackNetId} for bogie: {Bogie.Car.ID}"); + Multiplayer.LogWarning($"NetworkedBogie.Process({identifier}) Failed to find track {snapshot.TrackNetId} for bogie: {Bogie.Car.ID}"); return; } - Bogie.SetTrack(track.RailTrack, snapshot.PositionAlongTrack, snapshot.TrackDirection); - + if (Bogie.track != track.RailTrack) + Bogie.SetTrack(track.RailTrack, snapshot.PositionAlongTrack, snapshot.TrackDirection); + else + Bogie.traveller.MoveToSpan(snapshot.PositionAlongTrack); } else { - if(Bogie.track) + 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!"); + Multiplayer.LogWarning($"NetworkedBogie.Process({identifier}) 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; diff --git a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs index 0f80493c..cc74b259 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs @@ -1127,6 +1127,7 @@ private IEnumerator ParkCoupler(Coupler coupler) //Drop the chain coupler.ChainScript.fsm.Fire(ChainCouplerInteraction.Trigger.Dropped_By_Player); } + private IEnumerator DangleCoupler(Coupler coupler) { ChainCouplerInteraction ccInteraction = coupler.ChainScript; @@ -1219,13 +1220,13 @@ public void Client_ReceiveTrainPhysicsUpdate(in TrainsetMovementPart movementPar if (movementPart.typeFlag == TrainsetMovementPart.MovementType.RigidBody) { - Vector3 expectedPosition = movementPart.RigidbodySnapshot.Position + WorldMover.currentMove; + //Vector3 expectedPosition = movementPart.RigidbodySnapshot.Position + WorldMover.currentMove; //Multiplayer.LogDebug(() => $"Processing derailed physics for car {CurrentID} at tick {tick}, current position: {TrainCar.transform.position} expected position: {expectedPosition}"); TrainCar.Derail(); movementPart.RigidbodySnapshot.Apply(TrainCar.rb); - Client_trainRigidbodyQueue.ReceiveSnapshot(movementPart.RigidbodySnapshot, tick); + // Client_trainRigidbodyQueue.ReceiveSnapshot(movementPart.RigidbodySnapshot, tick); //Multiplayer.LogDebug(() => $"Derailed car {TrainCar.ID} positioned at {TrainCar.transform.position}"); } diff --git a/Multiplayer/Networking/Data/Train/BogieData.cs b/Multiplayer/Networking/Data/Train/BogieData.cs index a4670ae9..10969d65 100644 --- a/Multiplayer/Networking/Data/Train/BogieData.cs +++ b/Multiplayer/Networking/Data/Train/BogieData.cs @@ -9,19 +9,20 @@ public enum BogieFlags : byte { None = 0, IncludesTrackData = 1, - HasDerailed = 2 + HasDerailed = 2, + TrackReversed = 4 } public readonly struct BogieData { - private readonly BogieFlags DataFlags; + public 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); + public readonly int TrackDirection => DataFlags.HasFlag(BogieFlags.TrackReversed) ? -1 : 1; + public readonly bool IncludesTrackData => DataFlags.HasFlag(BogieFlags.IncludesTrackData); + public readonly bool HasDerailed => DataFlags.HasFlag(BogieFlags.HasDerailed); - private BogieData(BogieFlags flags, double positionAlongTrack, ushort trackNetId, int trackDirection) + private BogieData(BogieFlags flags, double positionAlongTrack, ushort trackNetId) { // Prevent invalid state combinations if (flags.HasFlag(BogieFlags.HasDerailed)) @@ -30,32 +31,34 @@ private BogieData(BogieFlags flags, double positionAlongTrack, ushort trackNetId DataFlags = flags; PositionAlongTrack = positionAlongTrack; TrackNetId = trackNetId; - TrackDirection = trackDirection; } - public static BogieData FromBogie(Bogie bogie, bool includeTrack) + public static BogieData FromBogie(Bogie bogie) { - bool includesTrackData = includeTrack && !bogie.HasDerailed && bogie.track; + bool includesTrackData = !bogie.HasDerailed && bogie.track; BogieFlags flags = BogieFlags.None; + if (includesTrackData) flags |= BogieFlags.IncludesTrackData; if (bogie.HasDerailed) flags |= BogieFlags.HasDerailed; + if (bogie.trackDirection == -1) flags |= BogieFlags.TrackReversed; return new BogieData( flags, bogie.traveller?.Span ?? -1.0, - includesTrackData ? bogie.track.Networked().NetId : (ushort)0, - bogie.trackDirection + includesTrackData ? bogie.track.Networked().NetId : (ushort)0 ); } 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); + + if (!data.HasDerailed) + writer.Put(data.PositionAlongTrack); + + if (data.IncludesTrackData) + writer.Put(data.TrackNetId); } public static BogieData Deserialize(NetDataReader reader) @@ -69,13 +72,9 @@ public static BogieData Deserialize(NetDataReader reader) // 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); + return new BogieData(flags, positionAlongTrack, trackNetId); } } diff --git a/Multiplayer/Networking/Data/Train/TrainsetSpawnPart.cs b/Multiplayer/Networking/Data/Train/TrainsetSpawnPart.cs index afd580d3..c1e44796 100644 --- a/Multiplayer/Networking/Data/Train/TrainsetSpawnPart.cs +++ b/Multiplayer/Networking/Data/Train/TrainsetSpawnPart.cs @@ -175,8 +175,8 @@ public static TrainsetSpawnPart FromTrainCar(NetworkedTrainCar networkedTrainCar trainCar.GetForwardSpeed(), transform.position - WorldMover.currentMove, transform.rotation, - BogieData.FromBogie(trainCar.Bogies[0], true), - BogieData.FromBogie(trainCar.Bogies[1], true), + BogieData.FromBogie(trainCar.Bogies[0]), + BogieData.FromBogie(trainCar.Bogies[1]), BrakeSystemData.From(trainCar.brakeSystem) ); } diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index a18a855d..fa329c42 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -361,6 +361,7 @@ public void SendDestroyTrainCar(ushort netId, ITransportPeer peer = null) public void SendTrainsetPhysicsUpdate(ClientboundTrainsetPhysicsPacket packet, bool reliable) { + //LogDebug(() => $"Sending Physics packet for netId: {packet.FirstNetId}, tick: {packet.Tick}"); SendPacketToAll(packet, reliable ? DeliveryMethod.ReliableOrdered : DeliveryMethod.Unreliable, SelfPeer); } From 18bed145e90e1912e36700d925b22c1c13b82fbe Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 15 Mar 2025 21:22:38 +1000 Subject: [PATCH 272/521] Fix coupler interaction not being re-enabled screwButtonBase is nulled out (but the component still exists) when certain interactions occur. Previously it was possible that interaction could not be restored because it was null. These changes find the component, regardless of screwButtonBase's state. --- .../Networking/Train/NetworkedTrainCar.cs | 47 ++++++++++--------- .../Train/CouplerChainInteractionPatch.cs | 4 +- 2 files changed, 27 insertions(+), 24 deletions(-) diff --git a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs index cc74b259..f2192cd6 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs @@ -2,6 +2,7 @@ using System.Collections; using System.Collections.Generic; using System.Linq; +using DV.CabControls; using DV.Customization.Paint; using DV.MultipleUnit; using DV.Simulation.Brake; @@ -166,7 +167,7 @@ public void Start() //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) + if (coupler.ChainScript != null) coupler.ChainScript.StateChanged += (state) => { Client_CouplerStateChange(state, coupler); }; } @@ -477,24 +478,24 @@ private void Server_SendCouplers() sendCouplers = false; - if(!TrainCar.frontCoupler.IsCoupled()) - // NetworkLifecycle.Instance.Client.SendTrainCouple(TrainCar.frontCoupler,TrainCar.frontCoupler.coupledTo,false, false); - //else - NetworkLifecycle.Instance.Server.SendTrainUncouple(TrainCar.frontCoupler,true,true, false); + if (!TrainCar.frontCoupler.IsCoupled()) + // NetworkLifecycle.Instance.Client.SendTrainCouple(TrainCar.frontCoupler,TrainCar.frontCoupler.coupledTo,false, false); + //else + NetworkLifecycle.Instance.Server.SendTrainUncouple(TrainCar.frontCoupler, true, true, false); - if(!TrainCar.rearCoupler.IsCoupled()) - // NetworkLifecycle.Instance.Client.SendTrainCouple(TrainCar.rearCoupler,TrainCar.rearCoupler.coupledTo,false, false); - //else + if (!TrainCar.rearCoupler.IsCoupled()) + // NetworkLifecycle.Instance.Client.SendTrainCouple(TrainCar.rearCoupler,TrainCar.rearCoupler.coupledTo,false, false); + //else NetworkLifecycle.Instance.Server.SendTrainUncouple(TrainCar.rearCoupler, true, true, false); if (!TrainCar.frontCoupler.hoseAndCock.IsHoseConnected) - // NetworkLifecycle.Instance.Client.SendHoseConnected(TrainCar.frontCoupler, TrainCar.frontCoupler.coupledTo, false); - //else + // NetworkLifecycle.Instance.Client.SendHoseConnected(TrainCar.frontCoupler, TrainCar.frontCoupler.coupledTo, false); + //else NetworkLifecycle.Instance.Client.SendHoseDisconnected(TrainCar.frontCoupler, true); if (!TrainCar.rearCoupler.hoseAndCock.IsHoseConnected) - // NetworkLifecycle.Instance.Client.SendHoseConnected(TrainCar.rearCoupler, TrainCar.rearCoupler.coupledTo, false); - //else + // NetworkLifecycle.Instance.Client.SendHoseConnected(TrainCar.rearCoupler, TrainCar.rearCoupler.coupledTo, false); + //else NetworkLifecycle.Instance.Client.SendHoseDisconnected(TrainCar.rearCoupler, true); NetworkLifecycle.Instance.Client.SendCockState(NetId, TrainCar.frontCoupler, TrainCar.frontCoupler.IsCockOpen); @@ -829,6 +830,8 @@ public void Common_ReceiveCouplerInteraction(CommonCouplerInteractionPacket pack TrainCar otherCar = null; Coupler otherCoupler = null; + ButtonBase buttonBase = coupler?.ChainScript?.screwButton.GetComponent(); + Multiplayer.LogDebug(() => $"Common_ReceiveCouplerInteraction() couplerNetId: {NetId}, coupler is front: {packet.IsFrontCoupler}, flags: {flags}, otherCouplerNetId: {packet.OtherNetId}, otherCoupler is front: {packet.IsFrontOtherCoupler}"); if (coupler == null) @@ -867,7 +870,7 @@ public void Common_ReceiveCouplerInteraction(CommonCouplerInteractionPacket pack StartCoroutine(DangleCoupler(coupler)); break; case ChainCouplerInteraction.State.Attached_Loose: - if(coupler.ChainScript.state == ChainCouplerInteraction.State.Attached_Tight) + if (coupler.ChainScript.state == ChainCouplerInteraction.State.Attached_Tight) coupler.ChainScript.fsm.Fire(ChainCouplerInteraction.Trigger.Screw_Used); else StartCoroutine(LooseAttachCoupler(coupler, originalCoupledTo)); @@ -891,8 +894,8 @@ public void Common_ReceiveCouplerInteraction(CommonCouplerInteractionPacket pack //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; + if (buttonBase) + buttonBase.InteractionAllowed = false; return; } @@ -943,7 +946,7 @@ 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) + 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); @@ -971,7 +974,7 @@ public void Common_ReceiveCouplerInteraction(CommonCouplerInteractionPacket pack bool chainInteraction = !flags.HasFlag(CouplerInteractionType.HoseConnect); Multiplayer.LogDebug(() => $"10 Common_ReceiveCouplerInteraction() [{TrainCar?.ID}, {NetId}], coupler is front: {packet.IsFrontCoupler}, flags: [{flags}], other coupler: {otherCoupler != null}, chainInteraction: {chainInteraction}"); - if(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}, chainInteraction: {chainInteraction}"); var car = coupler.CoupleTo(otherCoupler, viaChainInteraction: chainInteraction); @@ -996,7 +999,7 @@ public void Common_ReceiveCouplerInteraction(CommonCouplerInteractionPacket pack bool chainInteraction = !flags.HasFlag(CouplerInteractionType.HoseDisconnect); Multiplayer.LogDebug(() => $"11 Common_ReceiveCouplerInteraction() [{TrainCar?.ID}, {NetId}], coupler is front: {packet.IsFrontCoupler}, flags: {flags}, chainInteraction: {chainInteraction}"); - CouplerLogic.Uncouple(coupler,viaChainInteraction: chainInteraction); + CouplerLogic.Uncouple(coupler, viaChainInteraction: chainInteraction); /* fix for bug in vanilla game */ coupler.state = ChainCouplerInteraction.State.Parked; @@ -1031,8 +1034,8 @@ public void Common_ReceiveCouplerInteraction(CommonCouplerInteractionPacket pack //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; + if (buttonBase) + buttonBase.InteractionAllowed = true; } private IEnumerator LooseAttachCoupler(Coupler coupler, Coupler otherCoupler) @@ -1047,7 +1050,7 @@ private IEnumerator LooseAttachCoupler(Coupler coupler, Coupler otherCoupler) ChainCouplerInteraction ccInteraction = coupler.ChainScript; - if(ccInteraction.CurrentState == ChainCouplerInteraction.State.Disabled) + 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 @@ -1092,7 +1095,7 @@ private IEnumerator ParkCoupler(Coupler coupler) 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) + if (coupler.state == ChainCouplerInteraction.State.Attached_Loose && coupler.coupledTo != null) coupler.Uncouple(true, false, false, true); coupler.state = ChainCouplerInteraction.State.Parked; diff --git a/Multiplayer/Patches/Train/CouplerChainInteractionPatch.cs b/Multiplayer/Patches/Train/CouplerChainInteractionPatch.cs index a424d237..3fdc86ec 100644 --- a/Multiplayer/Patches/Train/CouplerChainInteractionPatch.cs +++ b/Multiplayer/Patches/Train/CouplerChainInteractionPatch.cs @@ -16,9 +16,9 @@ private static void OnScrewButtonUsed(ChainCouplerInteraction __instance) 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(() => { From f100acb792805bfedc376fc1604f8bfe13e8ab1b Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 15 Mar 2025 21:25:55 +1000 Subject: [PATCH 273/521] Add cargo health sync --- .../Networking/Train/NetworkedTrainCar.cs | 37 +++++++++++++++---- .../Managers/Client/NetworkClient.cs | 25 +++++++++++++ .../Managers/Server/NetworkServer.cs | 23 ++++++++++-- .../ClientboundCargoHealthUpdatePacket.cs | 7 ++++ .../Train/ClientboundCargoStatePacket.cs | 1 + 5 files changed, 83 insertions(+), 10 deletions(-) create mode 100644 Multiplayer/Networking/Packets/Clientbound/Train/ClientboundCargoHealthUpdatePacket.cs diff --git a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs index f2192cd6..13ba12fd 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs @@ -95,7 +95,8 @@ public static bool TryGetFromTrainCar(TrainCar trainCar, out NetworkedTrainCar n private bool brakeOverheatDirty; public bool BogieTracksDirty; - private bool cargoDirty; + private bool cargoStateDirty; + private bool cargoHealthDirty; private bool cargoIsLoading; public byte CargoModelIndex = byte.MaxValue; private bool healthDirty; @@ -325,6 +326,7 @@ private IEnumerator Server_WaitForLogicCar() TrainCar.logicCar.CargoLoaded += Server_OnCargoLoaded; TrainCar.logicCar.CargoUnloaded += Server_OnCargoUnloaded; + TrainCar.CargoDamage.CargoEffectiveHealthStateUpdate += Server_OnHealthUpdate; Server_DirtyAllState(); } @@ -333,7 +335,8 @@ public void Server_DirtyAllState() { handbrakeDirty = true; mainResPressureDirty = true; - cargoDirty = true; + cargoStateDirty = true; + cargoHealthDirty = true; cargoIsLoading = true; healthDirty = true; BogieTracksDirty = true; @@ -397,17 +400,22 @@ private void Server_BogieTrackChanged(RailTrack arg1, Bogie arg2) private void Server_OnCargoLoaded(CargoType obj) { - cargoDirty = true; + cargoStateDirty = true; cargoIsLoading = true; } private void Server_OnCargoUnloaded() { - cargoDirty = true; + cargoStateDirty = true; cargoIsLoading = false; CargoModelIndex = byte.MaxValue; } + private void Server_OnHealthUpdate(float health) + { + cargoHealthDirty = true; + } + private void Server_CarHealthUpdate(float health) { healthDirty = true; @@ -443,6 +451,7 @@ private void Server_OnTick(uint tick) Server_SendCouplers(); Server_SendCables(); Server_SendCargoState(); + Server_SendHealthUpdate(); Server_SendHealthState(); TicksSinceSync++; //keep track of last full sync @@ -519,12 +528,26 @@ private void Server_SendCables() private void Server_SendCargoState() { - if (!cargoDirty) + if (!cargoStateDirty) return; - cargoDirty = false; + cargoStateDirty = false; if (cargoIsLoading && TrainCar.logicCar.CurrentCargoTypeInCar == CargoType.None) return; - NetworkLifecycle.Instance.Server.SendCargoState(TrainCar, NetId, cargoIsLoading, CargoModelIndex); + + NetworkLifecycle.Instance.Server.SendCargoState(this, cargoIsLoading, CargoModelIndex); + } + + private void Server_SendHealthUpdate() + { + if (!cargoHealthDirty) + return; + + cargoHealthDirty = false; + + if (TrainCar.logicCar.CurrentCargoTypeInCar == CargoType.None) + return; + + NetworkLifecycle.Instance.Server.SendCargoHealthUpdate(NetId, TrainCar.CargoDamage.currentHealth); } private void Server_SendHealthState() diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index e4a2e920..96895d36 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -152,6 +152,7 @@ protected override void Subscribe() netPacketProcessor.SubscribeReusable(OnClientboundBrakeStateUpdatePacket); netPacketProcessor.SubscribeReusable(OnClientboundFireboxStatePacket); netPacketProcessor.SubscribeReusable(OnClientboundCargoStatePacket); + netPacketProcessor.SubscribeReusable(OnClientboundCargoHealthUpdatePacket); netPacketProcessor.SubscribeReusable(OnClientboundCarHealthUpdatePacket); netPacketProcessor.SubscribeReusable(OnClientboundRerailTrainPacket); netPacketProcessor.SubscribeReusable(OnClientboundWindowsBrokenPacket); @@ -725,6 +726,8 @@ private void OnClientboundCargoStatePacket(ClientboundCargoStatePacket packet) if (!NetworkedTrainCar.Get(packet.NetId, out NetworkedTrainCar networkedTrainCar)) return; + LogDebug(() => $"OnClientboundCargoStatePacket() {networkedTrainCar.CurrentID}, health: {packet.CargoHealth}"); + networkedTrainCar.CargoModelIndex = packet.CargoModelIndex; Car logicCar = networkedTrainCar.TrainCar.logicCar; @@ -757,7 +760,11 @@ private void OnClientboundCargoStatePacket(ClientboundCargoStatePacket packet) cargoAmount -= logicCar.LoadedCargoAmount; if (cargoAmount > 0) + { logicCar.LoadCargo(cargoAmount, (CargoType)packet.CargoType, warehouse); + } + + networkedTrainCar.TrainCar.CargoDamage.LoadCargoDamageState(packet.CargoHealth); } else { @@ -783,6 +790,24 @@ private void OnClientboundCargoStatePacket(ClientboundCargoStatePacket packet) } } + private void OnClientboundCargoHealthUpdatePacket(ClientboundCargoHealthUpdatePacket packet) + { + if (!NetworkedTrainCar.Get(packet.NetId, out NetworkedTrainCar networkedTrainCar)) + return; + + CargoDamageModel cargoDamageModel = networkedTrainCar.TrainCar.CargoDamage; + + if (networkedTrainCar.TrainCar == null || cargoDamageModel == null) + return; + + float deltaHealth = cargoDamageModel.currentHealth - packet.CargoHealth; + + LogDebug(() => $"OnClientboundCargoHealthUpdatePacket() {networkedTrainCar.CurrentID}, current health: {cargoDamageModel.currentHealth}, new health: {packet.CargoHealth}, delta: {cargoDamageModel}, applySensitivity: {packet.CargoHealth > 0}"); + + if (deltaHealth > 0) + cargoDamageModel.ApplyDamageToCargo(deltaHealth, packet.CargoHealth > 0); + } + private void OnClientboundCarHealthUpdatePacket(ClientboundCarHealthUpdatePacket packet) { if (!NetworkedTrainCar.GetTrainCar(packet.NetId, out TrainCar trainCar)) diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index fa329c42..a1924004 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -393,21 +393,38 @@ public void SendFireboxState(ushort netId, float fireboxContents, bool fireboxOn Multiplayer.LogDebug(() => $"Sending Firebox States netId {netId}: {fireboxContents}, {fireboxOn}"); } - public void SendCargoState(TrainCar trainCar, ushort netId, bool isLoading, byte cargoModelIndex) + public void SendCargoState(NetworkedTrainCar netTraincar, bool isLoading, byte cargoModelIndex) { - Car logicCar = trainCar.logicCar; + Car logicCar = netTraincar?.TrainCar?.logicCar; + + if (logicCar == null) + { + LogWarning($"Attempted to send cargo state for {netTraincar?.CurrentID}, but logic car does not exist!"); + return; + } + CargoType cargoType = isLoading ? logicCar.CurrentCargoTypeInCar : logicCar.LastUnloadedCargoType; SendPacketToAll(new ClientboundCargoStatePacket { - NetId = netId, + NetId = netTraincar.NetId, IsLoading = isLoading, CargoType = (ushort)cargoType, CargoAmount = logicCar.LoadedCargoAmount, + CargoHealth = netTraincar.TrainCar.CargoDamage.HealthPercentage, CargoModelIndex = cargoModelIndex, WarehouseMachineId = logicCar.CargoOriginWarehouse?.ID }, DeliveryMethod.ReliableOrdered, SelfPeer); } + public void SendCargoHealthUpdate(ushort netId, float currentHealth) + { + SendPacketToAll(new ClientboundCargoHealthUpdatePacket + { + NetId = netId, + CargoHealth = currentHealth, + }, DeliveryMethod.ReliableOrdered, SelfPeer); + } + public void SendCarHealthUpdate(ushort netId, float health) { SendPacketToAll(new ClientboundCarHealthUpdatePacket diff --git a/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundCargoHealthUpdatePacket.cs b/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundCargoHealthUpdatePacket.cs new file mode 100644 index 00000000..ac6290ce --- /dev/null +++ b/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundCargoHealthUpdatePacket.cs @@ -0,0 +1,7 @@ +namespace Multiplayer.Networking.Packets.Clientbound.Train; + +public class ClientboundCargoHealthUpdatePacket +{ + public ushort NetId { get; set; } + public float CargoHealth { get; set; } +} diff --git a/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundCargoStatePacket.cs b/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundCargoStatePacket.cs index a37f301e..8347448d 100644 --- a/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundCargoStatePacket.cs +++ b/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundCargoStatePacket.cs @@ -6,6 +6,7 @@ public class ClientboundCargoStatePacket public bool IsLoading { get; set; } public ushort CargoType { get; set; } public float CargoAmount { get; set; } + public float CargoHealth { get; set; } public byte CargoModelIndex { get; set; } public string WarehouseMachineId { get; set; } } From 9e76b4c7fbcb7d5c4044d87e3346ad070950ff22 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 15 Mar 2025 21:27:01 +1000 Subject: [PATCH 274/521] Code tidy up --- .../Networking/Train/NetworkedTrainCar.cs | 39 ++++++------ .../Managers/Client/NetworkClient.cs | 63 ++++++++++--------- .../Managers/Server/NetworkServer.cs | 14 ++--- 3 files changed, 59 insertions(+), 57 deletions(-) diff --git a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs index 13ba12fd..3de2677e 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs @@ -54,7 +54,7 @@ public static bool GetFromTrainId(string carId, out NetworkedTrainCar networkedT { return trainCarIdToNetworkedTrainCars.TryGetValue(carId, out networkedTrainCar); } - public static bool GetTrainCarFromTrainId(string carId, out TrainCar trainCar) + public static bool GetTrainCarFromTrainId(string carId, out TrainCar trainCar) { return trainCarIdToTrainCars.TryGetValue(carId, out trainCar); } @@ -72,7 +72,7 @@ public static bool TryGetFromTrainCar(TrainCar trainCar, out NetworkedTrainCar n private const uint MIN_KINEMATIC_CYCLES = 10; - public string CurrentID { get; private set; } + public string CurrentID { get; private set; } public TrainCar TrainCar; public uint TicksSinceSync = uint.MaxValue; public bool HasPlayers => PlayerManager.Car == TrainCar || GetComponentInChildren() != null; @@ -114,7 +114,7 @@ public static bool TryGetFromTrainCar(TrainCar trainCar, out NetworkedTrainCar n private int rearInteractionPeer; #region Client - public bool Client_Initialized {get; private set;} + public bool Client_Initialized { get; private set; } public TickedQueue Client_trainSpeedQueue; public TickedQueue Client_trainRigidbodyQueue; public TickedQueue client_bogie1Queue; @@ -138,7 +138,7 @@ protected override void Awake() trainCarsToNetworkedTrainCars[TrainCar] = this; TrainCar.LogicCarInitialized += OnLogicCarInitialised; - + bogie1 = TrainCar.Bogies[0]; bogie2 = TrainCar.Bogies[1]; @@ -160,11 +160,12 @@ public void Start() { brakeSystem = TrainCar.brakeSystem; + Multiplayer.LogDebug(() => $"TrainCar Created: {TrainCar?.ID}, {NetId}"); + foreach (Coupler coupler in TrainCar.couplers) { hoseToCoupler[coupler.hoseAndCock] = coupler; - Multiplayer.LogDebug(() => $"TrainCar Created: {TrainCar?.ID}, {NetId}"); //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 @@ -183,11 +184,11 @@ public void Start() foreach (KeyValuePair kvp in simulationFlow.fullPortIdToPort) if (kvp.Value.valueType == PortValueType.CONTROL || NetworkLifecycle.Instance.IsHost()) kvp.Value.ValueUpdatedInternally += _ => { Common_OnPortUpdated(kvp.Value); }; - + 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; @@ -195,7 +196,7 @@ public void Start() firebox.fireboxIgnitionPort.ValueUpdatedInternally += Client_OnIgnite; //Player igniting firebox } } - + brakeSystem.HandbrakePositionChanged += Common_OnHandbrakePositionChanged; brakeSystem.BrakeCylinderReleased += Common_OnBrakeCylinderReleased; @@ -262,7 +263,7 @@ public void OnDisable() brakeSystem.BrakeCylinderReleased -= Common_OnBrakeCylinderReleased; } - if(TrainCar.PaintExterior != null) + if (TrainCar.PaintExterior != null) TrainCar.PaintExterior.OnThemeChanged -= Common_OnPaintThemeChange; if (TrainCar.PaintInterior != null) TrainCar.PaintInterior.OnThemeChanged -= Common_OnPaintThemeChange; @@ -277,7 +278,7 @@ public void OnDisable() TrainCar.CarDamage.CarEffectiveHealthStateUpdate -= Server_CarHealthUpdate; - if(brakeSystem != null) + if (brakeSystem != null) { brakeSystem.MainResPressureChanged -= Server_MainResUpdate; brakeSystem.heatController.OverheatingActiveStateChanged -= Server_BrakeHeatUpdate; @@ -317,7 +318,7 @@ private void OnLogicCarInitialised() { Multiplayer.LogWarning("OnLogicCarInitialised Car Not Initialised!"); } - + } private IEnumerator Server_WaitForLogicCar() { @@ -436,7 +437,7 @@ private void Server_FireboxUpdate(float normalizedPressure, float pressure) fireboxDirty = true; } - private void Server_CouplerUncoupled(object _,UncoupleEventArgs args) + private void Server_CouplerUncoupled(object _, UncoupleEventArgs args) { sendCouplers |= args.dueToBrokenCouple; } @@ -516,7 +517,7 @@ private void Server_SendCables() return; sendCables = false; - if(TrainCar.muModule == null) + if (TrainCar.muModule == null) return; if (TrainCar.muModule.frontCable.IsConnected) @@ -588,7 +589,7 @@ public bool Server_ValidateCouplerInteraction(CommonCouplerInteractionPacket pac rearInteractionPeer = peer.Id; } } - else + else { if (packet.IsFrontCoupler) frontInteracting = false; @@ -610,7 +611,7 @@ private void Server_OnPlayerDisconnect(uint id) Multiplayer.LogWarning($"Server_OnPlayerDisconnect() Coupler interaction in unknown state [{CurrentID}, {NetId}] isFront: {frontInteractionPeer == id}"); if (frontInteractionPeer == id) { - frontInteracting = false ; + frontInteracting = false; //NetworkLifecycle.Instance.Client.SendCouplerInteraction(cou, coupler, otherCoupler); } else @@ -691,7 +692,7 @@ private void Common_SendPorts() float[] portValues = new float[portIds.Length]; foreach (string portId in dirtyPorts) { - if(simulationFlow.TryGetPort(portId, out Port port)) + if (simulationFlow.TryGetPort(portId, out Port port)) { float value = port.Value; portValues[i] = value; @@ -721,7 +722,7 @@ private void Common_SendFuses() foreach (string fuseId in dirtyFuses) { - if(simulationFlow.TryGetFuse(fuseId, out Fuse fuse)) + if (simulationFlow.TryGetFuse(fuseId, out Fuse fuse)) fuseValues[i] = fuse.State; else Multiplayer.LogWarning($"SendFuses() [{CurrentID}, {NetId}] Failed to find fuse \"{fuseId}\""); @@ -790,7 +791,7 @@ private void Common_OnPortUpdated(Port port) private void Common_OnPaintThemeChange(TrainCarPaint paintController) { - if(paintController == null) + if (paintController == null) return; Multiplayer.LogDebug(() => $"Common_OnPaintThemeChange() target: {paintController.TargetArea}, theme: {paintController.CurrentTheme.name}"); @@ -799,7 +800,7 @@ private void Common_OnPaintThemeChange(TrainCarPaint paintController) var theme = PaintThemeLookup.Instance.GetThemeIndex(paintController.CurrentTheme); Multiplayer.LogDebug(() => $"Common_OnPaintThemeChange() sending [{CurrentID},{NetId}], target: {paintController.TargetArea}, theme: [{paintController.CurrentTheme.name},{theme}]"); - NetworkLifecycle.Instance?.Client.SendPaintThemeChangePacket(NetId,target,theme); + NetworkLifecycle.Instance?.Client.SendPaintThemeChangePacket(NetId, target, theme); } private void Common_OnFuseUpdated(Fuse fuse) diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 96895d36..6e6d7ee0 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -178,7 +178,7 @@ public override void OnPeerConnected(ITransportPeer peer) public override void OnPeerDisconnected(ITransportPeer peer, DisconnectReason disconnectReason) { - LogDebug(()=>$"OnPeerDisconnected({peer.Id}, {disconnectReason}) disconnect message: {disconnectMessage}"); + LogDebug(() => $"OnPeerDisconnected({peer.Id}, {disconnectReason}) disconnect message: {disconnectMessage}"); NetworkLifecycle.Instance.Stop(); @@ -222,7 +222,7 @@ private void OnClientboundLoginResponsePacket(ClientboundLoginResponsePacket pac if (packet.Accepted) { Log($"Received player accepted packet"); - + if (NetworkLifecycle.Instance.IsHost(SelfPeer)) SendReadyPacket(); else @@ -511,7 +511,7 @@ 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) + if (networkedTrainCar == null || networkedTrainCar.gameObject == null || networkedTrainCar.TrainCar == null) { LogDebug(() => $"OnClientboundDestroyTrainCarPacket({packet?.NetId}) networkedTrainCar: {networkedTrainCar != null}, go: {(networkedTrainCar?.gameObject) != null}, trainCar: {networkedTrainCar?.TrainCar != null}"); } @@ -531,6 +531,7 @@ private void OnClientboundDestroyTrainCarPacket(ClientboundDestroyTrainCarPacket public void OnClientboundTrainPhysicsPacket(ClientboundTrainsetPhysicsPacket packet) { + //LogDebug(() => $"Received Physics packet for netId: {packet.FirstNetId}, tick: {packet.Tick}"); NetworkTrainsetWatcher.Instance.Client_HandleTrainsetPhysicsUpdate(packet); } @@ -590,8 +591,8 @@ private void OnCommonHoseConnectedPacket(CommonHoseConnectedPacket packet) return; } - string carId = $"[{ trainCar?.ID}, { packet.NetId}]"; - string otherCarId = $"[{ otherTrainCar?.ID}, { packet.OtherNetId}]"; + 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}"); @@ -610,9 +611,9 @@ private void OnCommonHoseConnectedPacket(CommonHoseConnectedPacket packet) Coupler connectedTo = null; Coupler otherConnectedTo = null; - if(coupler?.hoseAndCock?.connectedTo != null) + if (coupler?.hoseAndCock?.connectedTo != null) NetworkedTrainCar.TryGetCoupler(coupler.hoseAndCock.connectedTo, out connectedTo); - if(otherCoupler?.hoseAndCock?.connectedTo != null) + if (otherCoupler?.hoseAndCock?.connectedTo != null) NetworkedTrainCar.TryGetCoupler(otherCoupler.hoseAndCock.connectedTo, out otherConnectedTo); LogWarning($"OnCommonHoseConnectedPacket() trainCar: {carId}, isFront: {packet.IsFront}, IsHoseConnected: {coupler?.hoseAndCock?.IsHoseConnected}, connectedTo: {connectedTo?.train?.ID}," + @@ -826,7 +827,7 @@ 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)) @@ -1100,29 +1101,29 @@ public void SendCouplerInteraction(CouplerInteractionType flags, Coupler coupler } - 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) - { - LogWarning($"SendTrainCouple failed. Coupler: {coupler.name} {couplerNetId}, OtherCoupler: {otherCoupler.name} {otherCouplerNetId}"); - return; - } - - SendPacketToServer(new CommonTrainCouplePacket - { - 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 - }, DeliveryMethod.ReliableUnordered); - } + //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) + // { + // LogWarning($"SendTrainCouple failed. Coupler: {coupler.name} {couplerNetId}, OtherCoupler: {otherCoupler.name} {otherCouplerNetId}"); + // return; + // } + + // SendPacketToServer(new CommonTrainCouplePacket + // { + // 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 + // }, DeliveryMethod.ReliableUnordered); + //} public void SendHoseConnected(Coupler coupler, Coupler otherCoupler, bool playAudio) { diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index a1924004..0c7f823b 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -136,7 +136,7 @@ protected override void Subscribe() netPacketProcessor.SubscribeReusable(OnCommonChangeJunctionPacket); netPacketProcessor.SubscribeReusable(OnCommonRotateTurntablePacket); netPacketProcessor.SubscribeReusable(OnCommonCouplerInteractionPacket); - netPacketProcessor.SubscribeReusable(OnCommonTrainCouplePacket); + //netPacketProcessor.SubscribeReusable(OnCommonTrainCouplePacket); netPacketProcessor.SubscribeReusable(OnCommonTrainUncouplePacket); netPacketProcessor.SubscribeReusable(OnCommonHoseConnectedPacket); netPacketProcessor.SubscribeReusable(OnCommonHoseDisconnectedPacket); @@ -844,7 +844,7 @@ private void OnServerboundTimeAdvancePacket(ServerboundTimeAdvancePacket packet, private void OnCommonChangeJunctionPacket(CommonChangeJunctionPacket packet, ITransportPeer peer) { - SendPacketToAll(packet, DeliveryMethod.ReliableUnordered, peer); + SendPacketToAll(packet, DeliveryMethod.ReliableOrdered, peer); } private void OnCommonRotateTurntablePacket(CommonRotateTurntablePacket packet, ITransportPeer peer) @@ -886,10 +886,10 @@ private void OnCommonCouplerInteractionPacket(CommonCouplerInteractionPacket pac } } - private void OnCommonTrainCouplePacket(CommonTrainCouplePacket packet, ITransportPeer peer) - { - SendPacketToAll(packet, DeliveryMethod.ReliableUnordered, peer); - } + //private void OnCommonTrainCouplePacket(CommonTrainCouplePacket packet, ITransportPeer peer) + //{ + // SendPacketToAll(packet, DeliveryMethod.ReliableUnordered, peer); + //} private void OnCommonTrainUncouplePacket(CommonTrainUncouplePacket packet, ITransportPeer peer) { @@ -923,7 +923,7 @@ private void OnCommonCockFiddlePacket(CommonCockFiddlePacket packet, ITransportP private void OnCommonBrakeCylinderReleasePacket(CommonBrakeCylinderReleasePacket packet, ITransportPeer peer) { - SendPacketToAll(packet, DeliveryMethod.ReliableUnordered, peer); + SendPacketToAll(packet, DeliveryMethod.ReliableOrdered, peer); } private void OnCommonHandbrakePositionPacket(CommonHandbrakePositionPacket packet, ITransportPeer peer) From 266098b9acfe8c11c6c10c8f9402635e65e0a862 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 15 Mar 2025 21:44:05 +1000 Subject: [PATCH 275/521] Ready for release --- Multiplayer/Multiplayer.csproj | 2 +- info.json | 2 +- releases.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Multiplayer/Multiplayer.csproj b/Multiplayer/Multiplayer.csproj index 4a4f6355..3876468c 100644 --- a/Multiplayer/Multiplayer.csproj +++ b/Multiplayer/Multiplayer.csproj @@ -3,7 +3,7 @@ net48 latest Multiplayer - 0.1.10.4 + 0.1.10.6 diff --git a/info.json b/info.json index 23b50093..6d03d248 100644 --- a/info.json +++ b/info.json @@ -1,6 +1,6 @@ { "Id": "Multiplayer", - "Version": "0.1.10.4", + "Version": "0.1.10.6", "DisplayName": "Multiplayer", "Author": "Insprill, Macka, Morm", "EntryMethod": "Multiplayer.Multiplayer.Load", diff --git a/releases.json b/releases.json index da7b7537..466eb5a2 100644 --- a/releases.json +++ b/releases.json @@ -1,6 +1,6 @@ { "Releases": [ - {"Id": "Multiplayer", "Version": "0.1.10.4", "DownloadUrl": "https://github.com/AMacro/dv-multiplayer/releases/download/v0.1.10.4-Beta/Multiplayer.0.1.10.4.zip"} + {"Id": "Multiplayer", "Version": "0.1.10.6", "DownloadUrl": "https://github.com/AMacro/dv-multiplayer/releases/download/v0.1.10.6-Beta/Multiplayer.0.1.10.6.zip"} ] } \ No newline at end of file From f30db5b1d1a1490910e7f870738bf84e501eccc5 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 16 Mar 2025 12:35:30 +1000 Subject: [PATCH 276/521] Fix for vehicles without Cargo Damage controllers --- Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs index 5ac11979..29daeed0 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs @@ -327,7 +327,9 @@ private IEnumerator Server_WaitForLogicCar() TrainCar.logicCar.CargoLoaded += Server_OnCargoLoaded; TrainCar.logicCar.CargoUnloaded += Server_OnCargoUnloaded; - TrainCar.CargoDamage.CargoEffectiveHealthStateUpdate += Server_OnHealthUpdate; + + if (TrainCar.CargoDamage) + TrainCar.CargoDamage.CargoEffectiveHealthStateUpdate += Server_OnHealthUpdate; Server_DirtyAllState(); } From 11ba9842fe484f84f1061e987f1a27ac53949951 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 16 Mar 2025 12:35:30 +1000 Subject: [PATCH 277/521] Fix for vehicles without Cargo Damage controllers --- Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs index 3de2677e..b59850ca 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs @@ -327,7 +327,9 @@ private IEnumerator Server_WaitForLogicCar() TrainCar.logicCar.CargoLoaded += Server_OnCargoLoaded; TrainCar.logicCar.CargoUnloaded += Server_OnCargoUnloaded; - TrainCar.CargoDamage.CargoEffectiveHealthStateUpdate += Server_OnHealthUpdate; + + if (TrainCar.CargoDamage) + TrainCar.CargoDamage.CargoEffectiveHealthStateUpdate += Server_OnHealthUpdate; Server_DirtyAllState(); } From bc4c91d827d72ad5b91b9f7960c0d62e6de2dcad Mon Sep 17 00:00:00 2001 From: AMacro Date: Mon, 17 Mar 2025 13:27:59 +1000 Subject: [PATCH 278/521] Continue work on syncing pitstop data --- .../World/NetworkedPitStopStation.cs | 390 ++++++++++++------ .../World/NetworkedPluggableObject.cs | 13 +- .../SaveGame/NetworkedSaveGameManager.cs | 2 +- .../Networking/Data/LocoResourceModuleData.cs | 42 ++ .../Networking/Data/PitStopPlugData.cs | 4 +- .../Networking/Data/PitStopStationData.cs | 40 -- .../Managers/Client/NetworkClient.cs | 2 +- .../Networking/Managers/NetworkManager.cs | 2 + .../Managers/Server/NetworkServer.cs | 40 +- .../ClientboundPitStopBulkUpdatePacket.cs | 3 +- info.json | 2 +- 11 files changed, 331 insertions(+), 209 deletions(-) create mode 100644 Multiplayer/Networking/Data/LocoResourceModuleData.cs delete mode 100644 Multiplayer/Networking/Data/PitStopStationData.cs diff --git a/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs b/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs index 51fcd8d0..fab927e4 100644 --- a/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs +++ b/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs @@ -11,6 +11,7 @@ using System.Collections; using System.Linq; using Multiplayer.Networking.Packets.Clientbound.World; +using static CashRegisterModule; namespace Multiplayer.Components.Networking.World; @@ -44,12 +45,12 @@ public static Tuple[] GetAllPitStopStations() if (netPitStopStationToLocation.Count == 0) InitialisePitStops(); - List > result = []; + List> result = []; foreach (var kvp in netPitStopStationToLocation) { var selection = kvp.Value?.Station?.pitstop?.SelectedIndex ?? 0; - result.Add(new (kvp.Value.NetId, kvp.Key, selection)); + result.Add(new(kvp.Value.NetId, kvp.Key, selection)); } return result.ToArray(); @@ -86,11 +87,13 @@ public static void InitialisePitStops() const float LOADING_TIMEOUT = 5f; const float DEFAULT_DISABLER_SQR_DISTANCE = 250000f; + const float NEARBY_REMOVAL_DELAY = 3f; #region Server variables - private Dictionary playerToLastNearbyTime; - private Dictionary playerToInitialised; + private Dictionary playerToLastNearbyTime; private float disablerSqrDistance = DEFAULT_DISABLER_SQR_DISTANCE; + + private bool processingAsHost = false; #endregion #region Common variables @@ -125,7 +128,6 @@ protected override void Awake() if (NetworkLifecycle.Instance.IsHost()) { - playerToInitialised = []; playerToLastNearbyTime = []; var disabler = GetComponentInParent(); @@ -133,6 +135,9 @@ protected override void Awake() disablerSqrDistance = disabler.disableSqrDistance; NetworkLifecycle.Instance.OnTick += PlayerDistanceChecker; + + //ensure host can interact + Refreshed = true; } } @@ -191,21 +196,23 @@ protected void LateUpdate() lastUnitsToBuy = grabbedModule.Data.unitsToBuy; lastUpdateTime = Time.time; - NetworkLifecycle.Instance?.Client.SendPitStopInteractionPacket( - NetId, - PitStopStationInteractionType.StateUpdate, - grabbedModule.resourceType, - lastUnitsToBuy - ); + //if (!(NetworkLifecycle.Instance.IsHost() && processingAsHost)) + NetworkLifecycle.Instance?.Client.SendPitStopInteractionPacket( + NetId, + PitStopStationInteractionType.StateUpdate, + grabbedModule.resourceType, + lastUnitsToBuy + ); } } //Local grab has ended, but needs to be finalised - else if (wasGrabbed) + else if (wasGrabbed) { Multiplayer.LogDebug(() => $"NetworkedPitStopStation.LateUpdate() wasGrabbed: {wasGrabbed}, previous: {lastUnitsToBuy}, new: {grabbedModule.Data.unitsToBuy}"); lastUnitsToBuy = grabbedModule.Data.unitsToBuy; - NetworkLifecycle.Instance?.Client.SendPitStopInteractionPacket( + //if (!(NetworkLifecycle.Instance.IsHost() && processingAsHost)) + NetworkLifecycle.Instance?.Client.SendPitStopInteractionPacket( NetId, PitStopStationInteractionType.Ungrab, grabbedModule.resourceType, @@ -248,7 +255,7 @@ public Dictionary GetPluggables() return keyValuePairs; } - public bool ValidateInteraction(CommonPitStopInteractionPacket packet) + public bool ValidateInteraction(CommonPitStopInteractionPacket packet, ITransportPeer peer) { //todo: implement validation code (player distance, player interacting, etc.) return true; @@ -263,46 +270,96 @@ public void OnPlayerDisconnect(ITransportPeer peer) private void PlayerDistanceChecker(uint tick) { //if not active then there is no one close by - if (!gameObject.activeInHierarchy || Station == null || Station.pitstop == null) + if (gameObject == null || !gameObject.activeInHierarchy || Station == null || Station.pitstop == null) return; foreach (var player in NetworkLifecycle.Instance.Server.ServerPlayers) { - if (!player.IsLoaded) + if (player.Id == NetworkLifecycle.Instance.Server.SelfId || !player.IsLoaded) continue; float sqrDistance = (player.WorldPosition - transform.position).sqrMagnitude; - if (sqrDistance <= disablerSqrDistance) + bool initialised = playerToLastNearbyTime.TryGetValue(player.Id, out float lastVisit); + + if (sqrDistance > disablerSqrDistance) { - if(playerToInitialised.TryGetValue(player.Id, out var initialised) && initialised) - continue; + // Too far away for too long, stop tracking + if ((Time.time - lastVisit) > NEARBY_REMOVAL_DELAY) + playerToLastNearbyTime.Remove(player.Id); + + continue; + } + + //player nearby recently, update time + playerToLastNearbyTime[player.Id] = Time.time; - playerToInitialised[player.Id] = false; + if (!initialised) + { + if (!NetworkLifecycle.Instance.Server.TryGetPeer(player.Id, out var peer)) + continue; if (Station.pitstop.IsCarInPitStop()) { - PitStopStationData[] stateData = new PitStopStationData[Station.pitstop.carList.Count]; + // One struct per module type + var resourceCount = Station.locoResourceModules.resourceModules.Count(); + LocoResourceModuleData[] stateData = new LocoResourceModuleData[resourceCount]; int i; - for (i = 0; i < Station.pitstop.paramsList.Count; i++) + for (i = 0; i < resourceCount; i++) { - stateData[i] = PitStopStationData.From(Station.pitstop.paramsList[i]); + stateData[i] = LocoResourceModuleData.From(Station.locoResourceModules.resourceModules[i]); } PitStopPlugData[] plugData = new PitStopPlugData[resourceToPluggableObject.Count]; + i = 0; - foreach(var plug in resourceToPluggableObject) + foreach (var plug in resourceToPluggableObject) { plugData[i] = PitStopPlugData.From(plug.Value); i++; } - NetworkLifecycle.Instance.Server.SendPitStopBulkDataPacket(NetId, stateData, plugData); + // Send current state + NetworkLifecycle.Instance.Server.SendPitStopBulkDataPacket(NetId, Station.pitstop.carList.Count, stateData, plugData, peer); } } } } + public void ProcessInteractionPacketAsHost(CommonPitStopInteractionPacket packet, ITransportPeer peer) + { + Multiplayer.LogDebug(() => $"ProcessInteractionPacketAsHost() from peer: {peer.Id}, selfpeer: {NetworkLifecycle.Instance.Server.SelfId}"); + + if (ValidateInteraction(packet, peer)) + { + processingAsHost = true; + ProcessInteractionPacketAsClient(packet); + LateUpdate(); + processingAsHost = false; + //Send to all other players + foreach (var playerId in playerToLastNearbyTime.Keys) + { + if (NetworkLifecycle.Instance.Server.TryGetPeer(playerId, out var sendPeer) && sendPeer.Id != peer.Id) + { + Multiplayer.LogDebug(() => $"ProcessInteractionPacketAsHost() sending to peer: {sendPeer.Id}"); + NetworkLifecycle.Instance.Server.SendPitStopInteractionPacket(sendPeer, packet); + } + } + } + else + { + Multiplayer.LogDebug(() => $"ProcessInteractionPacketAsHost() failed validation"); + //Failed to validate, player needs to rollback interaction + NetworkLifecycle.Instance.Server.SendPitStopInteractionPacket( + peer, + new CommonPitStopInteractionPacket + { + NetId = packet.NetId, + InteractionType = (byte)PitStopStationInteractionType.Reject + } + ); + } + } #endregion @@ -385,124 +442,18 @@ private IEnumerator Init() Multiplayer.LogDebug(() => sb.ToString()); } - /// - /// Processes incoming network packets for pit stop interactions. - /// - /// The packet containing interaction data. - public void ProcessPacket(CommonPitStopInteractionPacket packet) - { - PitStopStationInteractionType interactionType = (PitStopStationInteractionType)packet.InteractionType; - ResourceType? resourceType = (ResourceType)packet.ResourceType; - - GrabHandlerHingeJoint grab = null; - LocoResourceModule resourceModule = null; - - Multiplayer.LogDebug(() => $"NetworkedPitStopStation.ProcessPacket() [{StationName}, {NetId}] {interactionType}, resource type: {resourceType}, state: {packet.State}"); - - if (resourceType != null && resourceType != 0) - { - if(!grabbedHandlerLookup.TryGetValue((ResourceType)resourceType, out grab)) - Multiplayer.LogError($"Could not find ResourceType in grabbedHandlerLookup for Pit Stop station {StationName}, resource type: {resourceType}"); - else - if(!grabberLookup.TryGetValue(grab, out var tup)) - Multiplayer.LogError($"Could not find GrabHandler in grabberLookup for Pit Stop station {StationName}, resource type: {resourceType}"); - else - (resourceModule, _, _) = tup; - - if (packet.State < resourceModule.AbsoluteMinValue || packet.State > resourceModule.AbsoluteMaxValue) - { - Multiplayer.LogError($"Invalid Pit Stop state value: {packet.State} for resource {resourceModule.resourceType}"); - return; - } - } - - switch (interactionType) - { - case PitStopStationInteractionType.Reject: - //todo: implement rejection - break; - - case PitStopStationInteractionType.Grab: - //block interaction - grab?.SetMovingDisabled(false); - - //set direction - if (resourceType != null && resourceType != 0 && resourceModule != null) - { - grabbedModule = resourceModule; - lastRemoteValue = packet.State; - } - - isRemoteGrabbed = true; - wasRemoteGrabbed = true; - break; - - case PitStopStationInteractionType.Ungrab: - //allow interaction - grab?.SetMovingDisabled(true); - - if (resourceType != null && resourceType != 0 && resourceModule != null) - { - lastRemoteValue = packet.State; - //resourceModule.Data.unitsToBuy = lastRemoteValue; - SetUnits(resourceModule, lastRemoteValue); - } - - isRemoteGrabbed = false; - - break; - - case PitStopStationInteractionType.StateUpdate: - - if (resourceType != null && resourceType != 0 && resourceModule != null) - { - if (isRemoteGrabbed || wasRemoteGrabbed) - { - lastRemoteValue = packet.State; - //resourceModule.Data.unitsToBuy = lastRemoteValue; - SetUnits(resourceModule, lastRemoteValue); - } - } - break; - - case PitStopStationInteractionType.SelectCar: - Station.pitstop.currentCarIndex = (int)packet.State; - Station.pitstop.OnCarSelectionChanged(); - - break; - case PitStopStationInteractionType.PayOrder: - break; - case PitStopStationInteractionType.CancelOrder: - break; - case PitStopStationInteractionType.ProcessOrder: - break; - } - } - - public void ProcessBulkUpdate(ClientboundPitStopBulkUpdatePacket packet) - { - if (Station?.pitstop?.carList == null || packet.PitStopData.Count() < Station.pitstop.carList.Count) - { - StartCoroutine(WaitForLoad(packet)); - return; - } - - //todo: process the packet!! - } - private IEnumerator WaitForLoad(ClientboundPitStopBulkUpdatePacket packet) { float time = Time.time; yield return new WaitUntil(() => - (Station?.pitstop?.carList == null || packet.PitStopData.Count() < Station.pitstop.carList.Count) + (Station?.pitstop?.carList != null && packet.CarCount == Station.pitstop.carList.Count) || (Time.time - time) > LOADING_TIMEOUT ); if ((Time.time - time) < LOADING_TIMEOUT) { ProcessBulkUpdate(packet); - } else Multiplayer.LogWarning($"NetworkedPitStopStation.WaitForLoad() Station {StationName} failed to process bulk update"); @@ -516,15 +467,39 @@ private void SetUnits(LocoResourceModule rm, float units) float clamped = Mathf.Clamp(units, rm.AbsoluteMinValue, rm.AbsoluteMaxValue); rm.SetUnitsToBuy(clamped); } + + /// + /// Initialises data elements for each car in each resource module + /// + private void InitialiseData() + { + foreach (var resourceModule in Station.locoResourceModules.resourceModules) + { + if (resourceModule == null) + continue; + + ResourceType resourceType = resourceModule.resourceType; + + // Make sure resourceData has enough entries for all cars + while (resourceModule.resourceData.Count < Station.pitstop.carList.Count) + { + if (resourceModule.resourceData.Count > 0) + resourceModule.resourceData.Add(new CashRegisterModuleData(resourceModule.resourceData[0])); + else + resourceModule.resourceData.Add(new CashRegisterModuleData()); + } + } + } #endregion + #region Client /// /// Handles grab interactions for the car selector knob. /// private void CarSelectorGrabbed() { - if (NetworkLifecycle.Instance.IsProcessingPacket) + if (NetworkLifecycle.Instance.IsProcessingPacket || (NetworkLifecycle.Instance.IsHost() && processingAsHost)) return; //Prevent new players/players entering the area from sending packets until initalised @@ -540,7 +515,7 @@ private void CarSelectorGrabbed() /// private void CarSelectorUnGrabbed() { - if (NetworkLifecycle.Instance.IsProcessingPacket) + if (NetworkLifecycle.Instance.IsProcessingPacket || (NetworkLifecycle.Instance.IsHost() && processingAsHost)) return; //Prevent new players/players entering the area from sending packets until initalised @@ -556,7 +531,7 @@ private void CarSelectorUnGrabbed() /// private void CarSelected() { - if (NetworkLifecycle.Instance.IsProcessingPacket) + if (NetworkLifecycle.Instance.IsProcessingPacket || (NetworkLifecycle.Instance.IsHost() && processingAsHost)) return; //Prevent new players/players entering the area from sending packets until initalised @@ -574,7 +549,7 @@ private void CarSelected() /// The resource module being grabbed. private void LeverGrabbed(LocoResourceModule module) { - if (NetworkLifecycle.Instance.IsProcessingPacket) + if (NetworkLifecycle.Instance.IsProcessingPacket || (NetworkLifecycle.Instance.IsHost() && processingAsHost)) return; //Prevent new players/players entering the area from sending packets until initalised @@ -587,6 +562,7 @@ private void LeverGrabbed(LocoResourceModule module) grabbedModule = module; grabbedAmplitudeChecker = module.GetComponentInChildren(); lastUnitsToBuy = module.Data.unitsToBuy; + NetworkLifecycle.Instance?.Client.SendPitStopInteractionPacket(NetId, PitStopStationInteractionType.Grab, module.resourceType, lastUnitsToBuy); } @@ -606,5 +582,147 @@ private void LeverUnGrabbed(LocoResourceModule module) Multiplayer.LogDebug(() => $"LeverUnGrabbed() {StationName}, module: {module.resourceType}"); isGrabbed = false; } + + public void ProcessBulkUpdate(ClientboundPitStopBulkUpdatePacket packet) + { + if (Station?.pitstop?.carList == null || Station.pitstop.carList.Count < packet.CarCount) + { + // Allow cars a chance to load in the pitstop + Multiplayer.LogDebug(() => $"PitStop bulk data count mismatch, waiting for load: {packet.CarCount} != {Station.pitstop.carList.Count}"); + CoroutineManager.Instance.StartCoroutine(WaitForLoad(packet)); + return; + } + + // Make sure the data elements exist prior to attempting to load them + InitialiseData(); + + Multiplayer.LogDebug(() => $"PitStop bulk data car count matches"); + + // Load the data for each car and resource module + foreach (var resource in packet.ResourceData) + { + var module = Station.locoResourceModules.resourceModules.FirstOrDefault(lm => lm.resourceType == resource.ResourceType); + + if (module) + if (module.resourceData.Count == resource.Values.Count()) + for (int i = 0; i < module.resourceData.Count; i++) + module.resourceData[i].unitsToBuy = resource.Values[i]; + else + Multiplayer.LogWarning($"PitStop bulk data count mismatch post-force: {module.resourceData.Count} != {resource.Values.Count()}"); + else + Multiplayer.LogWarning($"PitStop module not found for resource type: {resource.ResourceType}"); + } + + //sync plugs + foreach (var plug in packet.PlugData) + { + //todo: set plug states + } + + // Mark data as refreshed to allow player interactions + Refreshed = true; + } + + /// + /// Processes incoming network packets for pit stop interactions. + /// + /// The packet containing interaction data. + public void ProcessInteractionPacketAsClient(CommonPitStopInteractionPacket packet) + { + PitStopStationInteractionType interactionType = (PitStopStationInteractionType)packet.InteractionType; + ResourceType? resourceType = (ResourceType)packet.ResourceType; + + GrabHandlerHingeJoint grab = null; + LocoResourceModule resourceModule = null; + + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.ProcessPacket() [{StationName}, {NetId}] {interactionType}, resource type: {resourceType}, state: {packet.State}"); + + if (resourceType != null && resourceType != 0) + { + if (!grabbedHandlerLookup.TryGetValue((ResourceType)resourceType, out grab)) + Multiplayer.LogError($"Could not find ResourceType in grabbedHandlerLookup for Pit Stop station {StationName}, resource type: {resourceType}"); + else + if (!grabberLookup.TryGetValue(grab, out var tup)) + Multiplayer.LogError($"Could not find GrabHandler in grabberLookup for Pit Stop station {StationName}, resource type: {resourceType}"); + else + (resourceModule, _, _) = tup; + + if (packet.State < resourceModule.AbsoluteMinValue || packet.State > resourceModule.AbsoluteMaxValue) + { + Multiplayer.LogError($"Invalid Pit Stop state value: {packet.State} for resource {resourceModule.resourceType}"); + return; + } + } + + switch (interactionType) + { + case PitStopStationInteractionType.Reject: + //todo: implement rejection + break; + + case PitStopStationInteractionType.Grab: + //block interaction + grab?.SetMovingDisabled(false); + + //set direction + if (resourceType != null && resourceType != 0 && resourceModule != null) + { + grabbedModule = resourceModule; + lastRemoteValue = packet.State; + } + + isRemoteGrabbed = true; + wasRemoteGrabbed = true; + break; + + case PitStopStationInteractionType.Ungrab: + //allow interaction + grab?.SetMovingDisabled(true); + + if (resourceType != null && resourceType != 0 && resourceModule != null) + { + lastRemoteValue = packet.State; + //resourceModule.Data.unitsToBuy = lastRemoteValue; + SetUnits(resourceModule, lastRemoteValue); + } + + isRemoteGrabbed = false; + + break; + + case PitStopStationInteractionType.StateUpdate: + + if (resourceType != null && resourceType != 0 && resourceModule != null) + { + if (isRemoteGrabbed || wasRemoteGrabbed) + { + lastRemoteValue = packet.State; + //resourceModule.Data.unitsToBuy = lastRemoteValue; + SetUnits(resourceModule, lastRemoteValue); + } + } + break; + + case PitStopStationInteractionType.SelectCar: + if (packet.State >= 0 && packet.State < Station.pitstop.carList.Count) + { + Station.pitstop.currentCarIndex = (int)packet.State; + Station.pitstop.OnCarSelectionChanged(); + } + else + { + Multiplayer.LogWarning($"Pit Stop car selection change out of bounds! Requested: {(int)packet.State}, current car count: {Station.pitstop.carList.Count}"); + } + + break; + case PitStopStationInteractionType.PayOrder: + break; + case PitStopStationInteractionType.CancelOrder: + break; + case PitStopStationInteractionType.ProcessOrder: + break; + } + } + #endregion } diff --git a/Multiplayer/Components/Networking/World/NetworkedPluggableObject.cs b/Multiplayer/Components/Networking/World/NetworkedPluggableObject.cs index 17e3767e..b8abb57b 100644 --- a/Multiplayer/Components/Networking/World/NetworkedPluggableObject.cs +++ b/Multiplayer/Components/Networking/World/NetworkedPluggableObject.cs @@ -28,7 +28,7 @@ public static bool Get(ushort netId, out NetworkedPluggableObject obj) protected override bool IsIdServerAuthoritative => true; #region Server Variables - public PlugInteractionType CurrentInteration { get; set; } + public PlugInteractionType CurrentInteraction { get; set; } public ServerPlayer HeldBy { get; private set; } public ushort TrainCarNetId { get; private set; } public bool IsConnectedLeft { get; private set; } @@ -55,10 +55,13 @@ protected override void Awake() Multiplayer.LogDebug(() => $"NetworkedPluggableObject.Awake() {PluggableObject?.controlBase?.spec?.name}, {transform.parent.name}"); } - private IEnumerator Start() + protected IEnumerator Start() { + Multiplayer.LogDebug(() => $"NetworkedPluggableObject.Start() {PluggableObject?.controlBase?.spec?.name}, {transform.parent.name}"); yield return new WaitUntil(() => PluggableObject?.controlBase != null); + Multiplayer.LogDebug(() => $"NetworkedPluggableObject.Start() Controlbase {PluggableObject?.controlBase?.spec?.name}, {transform.parent.name}"); + grabHandler = this.GetComponent(); PluggableObject.controlBase.Grabbed += OnGrabbed; @@ -94,7 +97,7 @@ public bool ValidateInteraction(CommonPitStopPlugInteractionPacket packet, Serve //todo: implement validation code (player distance, player interacting, etc.) //validate and update - CurrentInteration = interactionType; + CurrentInteraction = interactionType; if (interactionType == PlugInteractionType.DockSocket) { @@ -272,9 +275,13 @@ public void InitPitStop(NetworkedPitStopStation netPitStop) #region Client private void OnGrabbed(ControlImplBase control) { + Multiplayer.LogDebug(() => $"NetworkedPluggableObject.OnGrabbed() pre [{transform.parent.name}, {NetId}] station: {Station?.StationName}"); + if (NetworkLifecycle.Instance.IsProcessingPacket) return; + Multiplayer.LogDebug(() => $"NetworkedPluggableObject.OnGrabbed() post [{transform.parent.name}, {NetId}] station: {Station?.StationName}"); + //Prevent new players/players entering the area from sending packets until initalised if (!Refreshed) return; diff --git a/Multiplayer/Components/SaveGame/NetworkedSaveGameManager.cs b/Multiplayer/Components/SaveGame/NetworkedSaveGameManager.cs index 5ee3a8f0..7b39ee65 100644 --- a/Multiplayer/Components/SaveGame/NetworkedSaveGameManager.cs +++ b/Multiplayer/Components/SaveGame/NetworkedSaveGameManager.cs @@ -69,7 +69,7 @@ public void Server_UpdateInternalData(SaveGameData data) foreach (ServerPlayer player in NetworkLifecycle.Instance.Server.ServerPlayers) { - if (player.Id == NetworkServer.SelfId || !player.IsLoaded) + if (player.Id == NetworkLifecycle.Instance.Server.SelfId || !player.IsLoaded) continue; JObject playerData = []; playerData.SetVector3(SaveGameKeys.Player_position, player.AbsoluteWorldPosition); diff --git a/Multiplayer/Networking/Data/LocoResourceModuleData.cs b/Multiplayer/Networking/Data/LocoResourceModuleData.cs new file mode 100644 index 00000000..59c76ce4 --- /dev/null +++ b/Multiplayer/Networking/Data/LocoResourceModuleData.cs @@ -0,0 +1,42 @@ +using DV.ThingTypes; +using LiteNetLib.Utils; +using System.Collections.Generic; +using System.Linq; + +namespace Multiplayer.Networking.Data; + +public readonly struct LocoResourceModuleData(ResourceType resourceType, float[] values) +{ + public readonly ResourceType ResourceType = resourceType; + public readonly float[] Values = values; + + public static LocoResourceModuleData From(LocoResourceModule resources) + { + //extract floats + var values = resources.resourceData.Select(d => d.unitsToBuy).ToArray(); + + return new LocoResourceModuleData(resources.resourceType, values); + } + + public static void Serialize(NetDataWriter writer, LocoResourceModuleData data) + { + writer.Put((int)data.ResourceType); + + writer.Put(data.Values.Count()); + foreach (var val in data.Values) + writer.Put(val); + } + + public static LocoResourceModuleData Deserialize(NetDataReader reader) + { + var type = (ResourceType)reader.GetInt(); + + var valueCount = reader.GetInt(); + + float[] states = new float[valueCount]; + for (int i = 0; i < valueCount; i++) + states[i] = reader.GetFloat(); + + return new LocoResourceModuleData(type, states); + } +} diff --git a/Multiplayer/Networking/Data/PitStopPlugData.cs b/Multiplayer/Networking/Data/PitStopPlugData.cs index 6852cb4e..e0479265 100644 --- a/Multiplayer/Networking/Data/PitStopPlugData.cs +++ b/Multiplayer/Networking/Data/PitStopPlugData.cs @@ -20,8 +20,8 @@ public static PitStopPlugData From(NetworkedPluggableObject plugData) return new PitStopPlugData ( plugData.NetId, - plugData.CurrentInteration, - plugData.HeldBy.Id, + plugData.CurrentInteraction, + plugData.HeldBy?.Id ?? 0, plugData.TrainCarNetId, plugData.IsConnectedLeft, plugData.transform.AbsolutePosition(), diff --git a/Multiplayer/Networking/Data/PitStopStationData.cs b/Multiplayer/Networking/Data/PitStopStationData.cs deleted file mode 100644 index 9764702e..00000000 --- a/Multiplayer/Networking/Data/PitStopStationData.cs +++ /dev/null @@ -1,40 +0,0 @@ -using DV.ThingTypes; -using LiteNetLib.Utils; -using System.Collections.Generic; -using System.Linq; - -namespace Multiplayer.Networking.Data; - -public readonly struct PitStopStationData(Dictionary resourceStates) -{ - public readonly Dictionary ResourceState = resourceStates; - - public static PitStopStationData From(CarPitStopParametersBase pitStopParams) - { - //extract floats - var states = pitStopParams.GetCarPitStopParameters().ToDictionary(param => param.Key, param => param.Value.value); - - return new PitStopStationData(states); - } - - public static void Serialize(NetDataWriter writer, PitStopStationData data) - { - writer.Put(data.ResourceState.Count); - foreach (var kvp in data.ResourceState) - { - writer.Put((int)kvp.Key); - writer.Put(kvp.Value); - } - } - - public static PitStopStationData Deserialize(NetDataReader reader) - { - var statesCount = reader.GetInt(); - - Dictionary states = []; - for (int i = 0; i < statesCount; i++) - states.Add((ResourceType)reader.GetInt(), reader.GetFloat()); - - return new PitStopStationData(states); - } -} diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 862bf74f..b76d9756 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -1008,7 +1008,7 @@ private void OnCommonPitStopInteractionPacket(CommonPitStopInteractionPacket pac Log($"Pit stop interaction received for {netPitStop.StationName}"); LogDebug(() => $"OnCommonPitStopInteractionPacket() [{netPitStop.StationName}, {packet.NetId}], interaction: [{packet.InteractionType}], resource: {packet?.ResourceType}, State: {packet.State}"); - netPitStop.ProcessPacket(packet); + netPitStop.ProcessInteractionPacketAsClient(packet); } private void OnCommonPitStopPlugInteractionPacket(CommonPitStopPlugInteractionPacket packet) diff --git a/Multiplayer/Networking/Managers/NetworkManager.cs b/Multiplayer/Networking/Managers/NetworkManager.cs index 899ae80d..8a5c21da 100644 --- a/Multiplayer/Networking/Managers/NetworkManager.cs +++ b/Multiplayer/Networking/Managers/NetworkManager.cs @@ -63,6 +63,8 @@ private void RegisterNestedTypes() netPacketProcessor.RegisterNestedType(TrainsetSpawnPart.Serialize, TrainsetSpawnPart.Deserialize); netPacketProcessor.RegisterNestedType(TrainCarHealthData.Serialize, TrainCarHealthData.Deserialize); netPacketProcessor.RegisterNestedType(PitStopStationMappingData.Serialize, PitStopStationMappingData.Deserialize); + netPacketProcessor.RegisterNestedType(LocoResourceModuleData.Serialize, LocoResourceModuleData.Deserialize); + netPacketProcessor.RegisterNestedType(PitStopPlugData.Serialize, PitStopPlugData.Deserialize); netPacketProcessor.RegisterNestedType(Vector2Serializer.Serialize, Vector2Serializer.Deserialize); netPacketProcessor.RegisterNestedType(Vector3Serializer.Serialize, Vector3Serializer.Deserialize); } diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index ba7a48a3..1b7ab427 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -54,8 +54,8 @@ public class NetworkServer : NetworkManager public IReadOnlyCollection ServerPlayers => serverPlayers.Values; public int PlayerCount => ServerPlayers.Count; - private static ITransportPeer SelfPeer => NetworkLifecycle.Instance.Client?.SelfPeer; - public static byte SelfId => (byte)SelfPeer.Id; + public ITransportPeer SelfPeer => NetworkLifecycle.Instance.Client?.SelfPeer; + public byte SelfId => (byte)SelfPeer.Id; private readonly ModInfo[] serverMods; public readonly IDifficulty Difficulty; @@ -577,13 +577,15 @@ public void SendItemsChangePacket(List items, ServerPlayer playe } } - public void SendPitStopBulkDataPacket(ushort netId, PitStopStationData[] stationData, PitStopPlugData[] plugData , ITransportPeer peer = null) + public void SendPitStopBulkDataPacket(ushort netId, int carCount, LocoResourceModuleData[] stationData, PitStopPlugData[] plugData , ITransportPeer peer = null) { LogDebug(() => $"SendPitStopBulkDataPacket({netId}, {stationData.Count()}, {plugData.Count()}, {peer?.Id})"); var packet = new ClientboundPitStopBulkUpdatePacket { - PitStopData = stationData, + NetId = netId, + CarCount = carCount, + ResourceData = stationData, PlugData = plugData, }; @@ -593,6 +595,13 @@ public void SendPitStopBulkDataPacket(ushort netId, PitStopStationData[] station SendPacket(peer, packet, DeliveryMethod.ReliableOrdered); } + public void SendPitStopInteractionPacket(ITransportPeer peer, CommonPitStopInteractionPacket packet) + { + LogDebug(() => $"SendPitStopInteractionPacket({peer.Id}, {packet.NetId})"); + + SendPacket(peer, packet, DeliveryMethod.ReliableOrdered); + } + public void SendChat(string message, ITransportPeer exclude = null) { @@ -1207,27 +1216,10 @@ private void OnUnconnectedPingPacket(UnconnectedPingPacket packet, IPEndPoint en private void OnCommonPitStopInteractionPacket(CommonPitStopInteractionPacket packet, ITransportPeer peer) { - if(NetworkedPitStopStation.Get(packet.NetId, out NetworkedPitStopStation controller)) - { - if (controller.ValidateInteraction(packet)) - { - //passed validation, send to all but the originator - SendPacketToAll(packet, DeliveryMethod.ReliableOrdered, peer); - } - else - { - //Failed to validate, player needs to rollback interaction - SendPacket(peer, new CommonPitStopInteractionPacket - { - NetId = packet.NetId, - InteractionType = (byte)PitStopStationInteractionType.Reject - }, DeliveryMethod.ReliableOrdered); - } - } + if (NetworkedPitStopStation.Get(packet.NetId, out NetworkedPitStopStation controller)) + controller.ProcessInteractionPacketAsHost(packet, peer); else - { - LogError($"OnCommonPitStopInteractionPacket() Failed to find PitStopStation with netId: {packet.NetId}"); - } + LogWarning($"OnCommonPitStopInteractionPacket() Failed to find PitStopStation with netId: {packet.NetId}"); } private void OnCommonPitStopPlugInteractionPacket(CommonPitStopPlugInteractionPacket packet, ITransportPeer peer) { diff --git a/Multiplayer/Networking/Packets/Clientbound/World/ClientboundPitStopBulkUpdatePacket.cs b/Multiplayer/Networking/Packets/Clientbound/World/ClientboundPitStopBulkUpdatePacket.cs index 8bedf8d8..ffbbaeda 100644 --- a/Multiplayer/Networking/Packets/Clientbound/World/ClientboundPitStopBulkUpdatePacket.cs +++ b/Multiplayer/Networking/Packets/Clientbound/World/ClientboundPitStopBulkUpdatePacket.cs @@ -9,6 +9,7 @@ namespace Multiplayer.Networking.Packets.Clientbound.World; public class ClientboundPitStopBulkUpdatePacket { public ushort NetId { get; set; } - public PitStopStationData[] PitStopData { get; set; } + public int CarCount { get; set; } + public LocoResourceModuleData[] ResourceData { get; set; } public PitStopPlugData[] PlugData { get; set; } } diff --git a/info.json b/info.json index fdb6281f..c9eeeab2 100644 --- a/info.json +++ b/info.json @@ -8,5 +8,5 @@ "LoadAfter": [ "RemoteDispatch" ], - "Repository": "https://www.andrewcraigmackenzie.com/unitymods/Releases.json" + "Repository": "https://raw.githubusercontent.com/AMacro/dv-multiplayer/refs/heads/beta/releases.json" } From b841b29a4ab582fbe4204e2e13ce1976d176d5e1 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 30 Mar 2025 08:54:05 +1000 Subject: [PATCH 279/521] Rework pitstop sync and add water tower positioning --- .../World/NetworkedPitStopStation.cs | 407 +++++++++++++----- Multiplayer/Multiplayer.csproj | 3 + .../Data/PitStopStationInteractionType.cs | 22 +- .../Managers/Client/NetworkClient.cs | 6 +- .../Common/CommonPitStopInteractionPacket.cs | 2 +- 5 files changed, 327 insertions(+), 113 deletions(-) diff --git a/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs b/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs index fab927e4..111e733f 100644 --- a/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs +++ b/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs @@ -85,6 +85,8 @@ public static void InitialisePitStops() const float MAX_DELTA = 0.2f; const float MIN_UPDATE_TIME = 0.1f; const float LOADING_TIMEOUT = 5f; + const float ROTATION_SMOOTH_SPEED = 5f; + const float FAUCET_SNAP_THRESHOLD = 0.005f; const float DEFAULT_DISABLER_SQR_DISTANCE = 250000f; const float NEARBY_REMOVAL_DELAY = 3f; @@ -100,21 +102,30 @@ public static void InitialisePitStops() public PitStopStation Station { get; set; } public string StationName { get; private set; } - private readonly GrabHandlerHingeJoint carSelectorGrab; + private ResourceType[] resourceTypes = Array.Empty(); + + private GrabHandlerHingeJoint carSelectorGrab; + private GrabHandlerHingeJoint faucetPositionerGrab; + private HingeJointAngleFix faucetPositioner; private readonly Dictionary grabberLookup = []; private readonly Dictionary grabbedHandlerLookup = []; private readonly Dictionary resourceToPluggableObject = []; - private bool isGrabbed = false; - private bool wasGrabbed = false; - private bool isRemoteGrabbed = false; - private bool wasRemoteGrabbed = false; - private float lastRemoteValue = 0.0f; + private readonly Dictionary isResourceGrabbedDict = []; + private readonly Dictionary wasResourceGrabbedDict = []; + private readonly Dictionary isResourceRemoteGrabbedDict = []; + private readonly Dictionary wasResourceRemoteGrabbedDict = []; + private readonly Dictionary lastRemoteValueDict = []; private float lastUpdateTime = 0.0f; - private LocoResourceModule grabbedModule; - private RotaryAmplitudeChecker grabbedAmplitudeChecker; - private float lastUnitsToBuy; + private bool isFaucetGrabbed = false; + private float lastFaucetUpdateTime = 0.0f; + private float lastFaucetSent = 0.0f; + private float faucetTargetPercentage = 0.0f; + private bool faucetTargetReached = true; + + private readonly Dictionary grabbedAmplitudeChecker = []; + private readonly Dictionary lastUnitsToBuyDict = []; private bool Refreshed = false; #endregion @@ -158,6 +169,14 @@ protected override void OnDestroy() { carSelectorGrab.Grabbed -= CarSelectorGrabbed; carSelectorGrab.UnGrabbed -= CarSelectorUnGrabbed; + + Station.pitstop.CarSelected -= CarSelected; + } + + if (faucetPositionerGrab != null) + { + faucetPositionerGrab.Grabbed -= FaucetCrankGrabbed; + faucetPositionerGrab.UnGrabbed -= FaucetCrankUnGrabbed; } foreach (var kvp in grabberLookup) @@ -175,71 +194,114 @@ protected override void OnDestroy() protected void LateUpdate() { - if (grabbedModule == null && grabbedAmplitudeChecker == null) - return; - - //Handle local grab interactions - if (isGrabbed || (wasGrabbed && lastUnitsToBuy != grabbedModule.Data.unitsToBuy)) + foreach (var resourceType in resourceTypes) { - //ensure the delta is big enough to be worth sending or we have reached a limit - var delta = Math.Abs(lastUnitsToBuy - grabbedModule.Data.unitsToBuy); - var deltaTime = Time.time - lastUpdateTime; - - //Check if the units to buy have reached a limit (0 or AbsoluteMaxValue), as this overrides a delta below minimum - var unitsToBuyChanged = - (grabbedModule.Data.unitsToBuy == grabbedModule.AbsoluteMinValue && lastUnitsToBuy != grabbedModule.AbsoluteMinValue) - || (grabbedModule.Data.unitsToBuy == grabbedModule.AbsoluteMaxValue && lastUnitsToBuy != grabbedModule.AbsoluteMaxValue); + var module = Station.locoResourceModules.resourceModules.FirstOrDefault(x => x.resourceType == resourceType); + + if (module == null + || !isResourceGrabbedDict.TryGetValue(resourceType, out var isResourceGrabbed) + || !wasResourceGrabbedDict.TryGetValue(resourceType, out var wasResourceGrabbed) + || !isResourceRemoteGrabbedDict.TryGetValue(resourceType, out var isResourceRemoteGrabbed) + || !wasResourceRemoteGrabbedDict.TryGetValue(resourceType, out var wasResourceRemoteGrabbed) + || !lastRemoteValueDict.TryGetValue(resourceType, out var lastRemoteValue) + || !lastUnitsToBuyDict.TryGetValue(resourceType, out var lastUnitsToBuy) + ) + continue; - //Send the update if we've passed the time threshold AND we have a big enough change or hit a limit - if (deltaTime > MIN_UPDATE_TIME && (delta > MAX_DELTA || unitsToBuyChanged)) + //Handle local grab interactions + if (isResourceGrabbed || (wasResourceGrabbed && lastUnitsToBuy != module.Data.unitsToBuy)) { - lastUnitsToBuy = grabbedModule.Data.unitsToBuy; - lastUpdateTime = Time.time; + //ensure the delta is big enough to be worth sending or we have reached a limit + var delta = Math.Abs(lastUnitsToBuy - module.Data.unitsToBuy); + var deltaTime = Time.time - lastUpdateTime; - //if (!(NetworkLifecycle.Instance.IsHost() && processingAsHost)) + //Check if the units to buy have reached a limit (0 or AbsoluteMaxValue), as this overrides a delta below minimum + var unitsToBuyChanged = + (module.Data.unitsToBuy == module.AbsoluteMinValue && lastUnitsToBuy != module.AbsoluteMinValue) + || (module.Data.unitsToBuy == module.AbsoluteMaxValue && lastUnitsToBuy != module.AbsoluteMaxValue); + + //Send the update if we've passed the time threshold AND we have a big enough change or hit a limit + if (deltaTime > MIN_UPDATE_TIME && (delta > MAX_DELTA || unitsToBuyChanged)) + { + lastUnitsToBuyDict[resourceType] = module.Data.unitsToBuy; + lastUpdateTime = Time.time; + + //if (!(NetworkLifecycle.Instance.IsHost() && processingAsHost)) NetworkLifecycle.Instance?.Client.SendPitStopInteractionPacket( NetId, - PitStopStationInteractionType.StateUpdate, - grabbedModule.resourceType, + PitStopStationInteractionType.ResourceUpdate, + resourceType, lastUnitsToBuy ); + } } - } - //Local grab has ended, but needs to be finalised - else if (wasGrabbed) - { - Multiplayer.LogDebug(() => $"NetworkedPitStopStation.LateUpdate() wasGrabbed: {wasGrabbed}, previous: {lastUnitsToBuy}, new: {grabbedModule.Data.unitsToBuy}"); - lastUnitsToBuy = grabbedModule.Data.unitsToBuy; + //Local grab has ended, but needs to be finalised + else if (wasResourceGrabbed) + { + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.LateUpdate() wasGrabbed: {wasResourceGrabbed}, previous: {lastUnitsToBuy}, new: {module.Data.unitsToBuy}"); + lastUnitsToBuyDict[resourceType] = module.Data.unitsToBuy; - //if (!(NetworkLifecycle.Instance.IsHost() && processingAsHost)) + //if (!(NetworkLifecycle.Instance.IsHost() && processingAsHost)) NetworkLifecycle.Instance?.Client.SendPitStopInteractionPacket( NetId, - PitStopStationInteractionType.Ungrab, - grabbedModule.resourceType, + PitStopStationInteractionType.ResourceUngrab, + resourceType, lastUnitsToBuy ); - //Reset grab states - wasGrabbed = false; - grabbedModule = null; - grabbedAmplitudeChecker = null; + //Reset grab states + wasResourceGrabbedDict[resourceType] = false; + } + + //allow things to settle after remote grab released + if (!isResourceRemoteGrabbed && wasResourceRemoteGrabbed) + { + float previous = module.Data.unitsToBuy; + //grabbedModule.Data.unitsToBuy = lastRemoteValueDict; + SetUnits(module, lastRemoteValue); + + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.LateUpdate() wasRemoteGrabbed: {wasResourceRemoteGrabbed}, previous: {previous}, new: {lastRemoteValue}"); + + if (previous == lastRemoteValue) + { + //settled, stop tracking remote + wasResourceRemoteGrabbedDict[resourceType] = false; + } + } } + } + + protected void Update() + { + var deltaTime = Time.time - lastFaucetUpdateTime; - //allow things to settle after remote grab released - if (!isRemoteGrabbed && wasRemoteGrabbed) + // Handle faucet movement + if (faucetPositioner && !faucetTargetReached) { - float previous = grabbedModule.Data.unitsToBuy; - //grabbedModule.Data.unitsToBuy = lastRemoteValue; - SetUnits(grabbedModule, lastRemoteValue); + var currentPercentage = faucetPositioner.Percentage; + float newPercent = Mathf.Lerp(currentPercentage, faucetTargetPercentage, Time.deltaTime * ROTATION_SMOOTH_SPEED); - Multiplayer.LogDebug(() => $"NetworkedPitStopStation.LateUpdate() wasRemoteGrabbed: {wasRemoteGrabbed}, previous: {previous}, new: {lastRemoteValue}"); + //if we're close enough to the target, snap to it + if (Mathf.Abs(currentPercentage - faucetTargetPercentage) < FAUCET_SNAP_THRESHOLD) + newPercent = faucetTargetPercentage; - if (previous == lastRemoteValue) - { - //settled, stop tracking remote - wasRemoteGrabbed = false; - grabbedModule = null; - } + SetFaucetRotation(newPercent); + //if we're in snap range we can finalise the rotation + faucetTargetReached = Mathf.Abs(newPercent - faucetTargetPercentage) < FAUCET_SNAP_THRESHOLD; + } + + if (isFaucetGrabbed && (deltaTime > MIN_UPDATE_TIME) && lastFaucetSent != faucetPositioner.Percentage) + { + lastFaucetUpdateTime = Time.time; + + lastFaucetSent = faucetPositioner.Percentage; + NetworkLifecycle.Instance?.Client.SendPitStopInteractionPacket + ( + NetId, + PitStopStationInteractionType.FaucetPosition, + null, + lastFaucetSent + ); } } #endregion @@ -382,11 +444,9 @@ private IEnumerator Init() while (Station?.pitstop == null) yield return new WaitForEndOfFrame(); - var resourceModules = Station?.locoResourceModules?.resourceModules; - //Wait for levers an knobs to load yield return new WaitUntil(() => GetComponentInChildren(true) != null); - GrabHandlerHingeJoint carSelectorGrab = GetComponentInChildren(true); + carSelectorGrab = GetComponentInChildren(true); if (carSelectorGrab != null) { @@ -397,6 +457,39 @@ private IEnumerator Init() Station.pitstop.CarSelected += CarSelected; } + // Water tower positioner handle + var faucetGo = transform.parent.FindChildrenByName("FaucetCrank").FirstOrDefault(); + faucetPositionerGrab = faucetGo?.GetComponentInChildren(true); + faucetPositioner = faucetGo?.GetComponentInChildren(true); + + if (faucetPositionerGrab != null && faucetPositioner != null) + { + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.Init() Grab Handler found: {carSelectorGrab != null}, Name: {carSelectorGrab.name}"); + faucetPositionerGrab.Grabbed += FaucetCrankGrabbed; + faucetPositionerGrab.UnGrabbed += FaucetCrankUnGrabbed; + } + + //build dictionaries + var resourceModules = Station?.locoResourceModules?.resourceModules; + if (resourceModules == null) + { + Multiplayer.LogWarning($"No resource modules found for station {StationName}"); + yield break; + } + + resourceTypes = resourceModules?.Select(m => m.resourceType).ToArray(); + + foreach (var resourceType in resourceTypes) + { + isResourceGrabbedDict[resourceType] = false; + wasResourceGrabbedDict[resourceType] = false; + isResourceRemoteGrabbedDict[resourceType] = false; + wasResourceRemoteGrabbedDict[resourceType] = false; + lastRemoteValueDict[resourceType] = 0.0f; + grabbedAmplitudeChecker[resourceType] = null; + lastUnitsToBuyDict[resourceType] = 0.0f; + } + StringBuilder sb = new(); sb.AppendLine($"NetworkedPitStopStation.Awake() {StationName} resources:"); @@ -478,8 +571,6 @@ private void InitialiseData() if (resourceModule == null) continue; - ResourceType resourceType = resourceModule.resourceType; - // Make sure resourceData has enough entries for all cars while (resourceModule.resourceData.Count < Station.pitstop.carList.Count) { @@ -490,6 +581,43 @@ private void InitialiseData() } } } + + /// + /// Sets the rotation of the faucet handle to the specified percentage + /// + public void SetFaucetRotation(float percentage) + { + if (faucetPositioner == null) + return; + + float targetAngle = faucetPositioner.angleOffset + (percentage * faucetPositioner.angleRange); + + Vector3 axis = faucetPositioner.joint.axis; + + // Create a rotation around that axis by the target angle + Quaternion rotationDelta = Quaternion.AngleAxis(targetAngle, axis); + + // Calculate the final target rotation + Quaternion targetRotation = Quaternion.Inverse(faucetPositioner.startRotationInverse) * rotationDelta; + + faucetPositioner.transform.localRotation = targetRotation; + } + + /// + /// Set the car selection index + /// + public void SetCarSelection(int selection) + { + if (selection >= 0 && selection < Station.pitstop.carList.Count) + { + Station.pitstop.currentCarIndex = selection; + Station.pitstop.OnCarSelectionChanged(); + } + else + { + Multiplayer.LogWarning($"Pit Stop car selection change out of bounds! Selected: {selection}, current car count: {Station.pitstop.carList.Count}"); + } + } #endregion @@ -507,7 +635,7 @@ private void CarSelectorGrabbed() return; Multiplayer.LogDebug(() => $"CarSelectorGrabbed() {StationName}"); - NetworkLifecycle.Instance?.Client.SendPitStopInteractionPacket(NetId, PitStopStationInteractionType.Grab, null, 0); + NetworkLifecycle.Instance?.Client.SendPitStopInteractionPacket(NetId, PitStopStationInteractionType.CarSelectorGrab, null, 0); } /// @@ -523,7 +651,7 @@ private void CarSelectorUnGrabbed() return; Multiplayer.LogDebug(() => $"CarSelectorUnGrabbed() {StationName}"); - NetworkLifecycle.Instance?.Client.SendPitStopInteractionPacket(NetId, PitStopStationInteractionType.Ungrab, null, Station.pitstop.SelectedIndex); + NetworkLifecycle.Instance?.Client.SendPitStopInteractionPacket(NetId, PitStopStationInteractionType.CarSelectorUngrab, null, Station.pitstop.SelectedIndex); } /// @@ -540,7 +668,7 @@ private void CarSelected() Multiplayer.LogDebug(() => $"CarSelected() selected: {Station.pitstop.SelectedIndex}"); - NetworkLifecycle.Instance?.Client.SendPitStopInteractionPacket(NetId, PitStopStationInteractionType.SelectCar, null, Station.pitstop.SelectedIndex); + NetworkLifecycle.Instance?.Client.SendPitStopInteractionPacket(NetId, PitStopStationInteractionType.CarSelection, null, Station.pitstop.SelectedIndex); } /// @@ -557,13 +685,13 @@ private void LeverGrabbed(LocoResourceModule module) return; Multiplayer.LogDebug(() => $"LeverGrabbed() {StationName}, module: {module.resourceType}"); - isGrabbed = true; - wasGrabbed = true; - grabbedModule = module; - grabbedAmplitudeChecker = module.GetComponentInChildren(); - lastUnitsToBuy = module.Data.unitsToBuy; - NetworkLifecycle.Instance?.Client.SendPitStopInteractionPacket(NetId, PitStopStationInteractionType.Grab, module.resourceType, lastUnitsToBuy); + isResourceGrabbedDict[module.resourceType] = true; + wasResourceGrabbedDict[module.resourceType] = true; + grabbedAmplitudeChecker[module.resourceType] = module.GetComponentInChildren(); + lastUnitsToBuyDict[module.resourceType] = module.Data.unitsToBuy; + + NetworkLifecycle.Instance?.Client.SendPitStopInteractionPacket(NetId, PitStopStationInteractionType.ResourceGrab, module.resourceType, lastUnitsToBuyDict[module.resourceType]); } /// @@ -580,7 +708,43 @@ private void LeverUnGrabbed(LocoResourceModule module) return; Multiplayer.LogDebug(() => $"LeverUnGrabbed() {StationName}, module: {module.resourceType}"); - isGrabbed = false; + isResourceGrabbedDict[module.resourceType] = false; + wasResourceGrabbedDict[module.resourceType] = true; + } + + /// + /// Handles grab interactions for the faucet positioning handle (water towers). + /// + private void FaucetCrankGrabbed() + { + if (NetworkLifecycle.Instance.IsProcessingPacket || (NetworkLifecycle.Instance.IsHost() && processingAsHost)) + return; + + //Prevent new players/players entering the area from sending packets until initalised + if (!Refreshed) + return; + + Multiplayer.LogDebug(() => $"FaucetCrankGrabbed() {StationName}"); + isFaucetGrabbed = true; + NetworkLifecycle.Instance?.Client.SendPitStopInteractionPacket(NetId, PitStopStationInteractionType.FaucetGrab, null, 0); + } + + /// + /// Handles end of grab (release) interactions for the car selector knob. + /// + private void FaucetCrankUnGrabbed() + { + if (NetworkLifecycle.Instance.IsProcessingPacket || (NetworkLifecycle.Instance.IsHost() && processingAsHost)) + return; + + //Prevent new players/players entering the area from sending packets until initalised + if (!Refreshed) + return; + + Multiplayer.LogDebug(() => $"FaucetCrankUnGrabbed() {StationName}, percentage: {faucetPositioner.Percentage}"); + isFaucetGrabbed = false; + lastFaucetSent = faucetPositioner.Percentage; + NetworkLifecycle.Instance?.Client.SendPitStopInteractionPacket(NetId, PitStopStationInteractionType.FaucetUngrab, null, lastFaucetSent); } public void ProcessBulkUpdate(ClientboundPitStopBulkUpdatePacket packet) @@ -629,15 +793,24 @@ public void ProcessBulkUpdate(ClientboundPitStopBulkUpdatePacket packet) /// The packet containing interaction data. public void ProcessInteractionPacketAsClient(CommonPitStopInteractionPacket packet) { + if (!Enum.IsDefined(typeof(PitStopStationInteractionType), packet.InteractionType)) + { + Multiplayer.LogWarning($"Invalid interaction type: {packet.InteractionType} in ProcessInteractionPacketAsClient()"); + return; + } + PitStopStationInteractionType interactionType = (PitStopStationInteractionType)packet.InteractionType; - ResourceType? resourceType = (ResourceType)packet.ResourceType; + + bool resourceValid = Enum.IsDefined(typeof(ResourceType), packet.ResourceType); + + ResourceType resourceType = resourceValid ? (ResourceType)packet.ResourceType : ResourceType.Fuel; GrabHandlerHingeJoint grab = null; LocoResourceModule resourceModule = null; - Multiplayer.LogDebug(() => $"NetworkedPitStopStation.ProcessPacket() [{StationName}, {NetId}] {interactionType}, resource type: {resourceType}, state: {packet.State}"); + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.ProcessPacket() [{StationName}, {NetId}] {interactionType}, resource type: {resourceType}, state: {packet.Value}"); - if (resourceType != null && resourceType != 0) + if (resourceValid) { if (!grabbedHandlerLookup.TryGetValue((ResourceType)resourceType, out grab)) Multiplayer.LogError($"Could not find ResourceType in grabbedHandlerLookup for Pit Stop station {StationName}, resource type: {resourceType}"); @@ -647,9 +820,9 @@ public void ProcessInteractionPacketAsClient(CommonPitStopInteractionPacket pack else (resourceModule, _, _) = tup; - if (packet.State < resourceModule.AbsoluteMinValue || packet.State > resourceModule.AbsoluteMaxValue) + if (packet.Value < resourceModule.AbsoluteMinValue || packet.Value > resourceModule.AbsoluteMaxValue) { - Multiplayer.LogError($"Invalid Pit Stop state value: {packet.State} for resource {resourceModule.resourceType}"); + Multiplayer.LogError($"Invalid Pit Stop state value: {packet.Value} for resource {resourceModule.resourceType}"); return; } } @@ -660,61 +833,92 @@ public void ProcessInteractionPacketAsClient(CommonPitStopInteractionPacket pack //todo: implement rejection break; - case PitStopStationInteractionType.Grab: + + case PitStopStationInteractionType.ResourceGrab: //block interaction grab?.SetMovingDisabled(false); //set direction - if (resourceType != null && resourceType != 0 && resourceModule != null) + if (resourceValid && resourceModule != null) { - grabbedModule = resourceModule; - lastRemoteValue = packet.State; + lastRemoteValueDict[resourceType] = packet.Value; + isResourceRemoteGrabbedDict[resourceType] = true; + wasResourceRemoteGrabbedDict[resourceType] = true; } - isRemoteGrabbed = true; - wasRemoteGrabbed = true; break; - case PitStopStationInteractionType.Ungrab: + case PitStopStationInteractionType.ResourceUngrab: //allow interaction grab?.SetMovingDisabled(true); - if (resourceType != null && resourceType != 0 && resourceModule != null) + if (isResourceRemoteGrabbedDict[resourceType] || wasResourceRemoteGrabbedDict[resourceType]) { - lastRemoteValue = packet.State; - //resourceModule.Data.unitsToBuy = lastRemoteValue; - SetUnits(resourceModule, lastRemoteValue); + lastRemoteValueDict[resourceType] = packet.Value; + SetUnits(resourceModule, lastRemoteValueDict[resourceType]); + isResourceRemoteGrabbedDict[resourceType] = false; } - - isRemoteGrabbed = false; - break; - case PitStopStationInteractionType.StateUpdate: + case PitStopStationInteractionType.ResourceUpdate: - if (resourceType != null && resourceType != 0 && resourceModule != null) + if (resourceValid && resourceModule != null) { - if (isRemoteGrabbed || wasRemoteGrabbed) + if (isResourceRemoteGrabbedDict[resourceType] || wasResourceRemoteGrabbedDict[resourceType]) { - lastRemoteValue = packet.State; - //resourceModule.Data.unitsToBuy = lastRemoteValue; - SetUnits(resourceModule, lastRemoteValue); + lastRemoteValueDict[resourceType] = packet.Value; + SetUnits(resourceModule, lastRemoteValueDict[resourceType]); } } break; - case PitStopStationInteractionType.SelectCar: - if (packet.State >= 0 && packet.State < Station.pitstop.carList.Count) + + case PitStopStationInteractionType.CarSelectorGrab: + //block interaction + carSelectorGrab?.SetMovingDisabled(false); + break; + + case PitStopStationInteractionType.CarSelectorUngrab: + //allow interaction + carSelectorGrab?.SetMovingDisabled(true); + SetCarSelection((int)packet.Value); + break; + + case PitStopStationInteractionType.CarSelection: + SetCarSelection((int)packet.Value); + break; + + case PitStopStationInteractionType.FaucetGrab: + //block interaction + faucetPositionerGrab?.SetMovingDisabled(false); + break; + + case PitStopStationInteractionType.FaucetUngrab: + //allow interaction + faucetPositionerGrab?.SetMovingDisabled(true); + + if (packet.Value >= -1 && packet.Value <= 1) { - Station.pitstop.currentCarIndex = (int)packet.State; - Station.pitstop.OnCarSelectionChanged(); + if (faucetPositioner.Percentage != packet.Value) + { + faucetTargetPercentage = packet.Value; + faucetTargetReached = false; + } } - else + break; + + case PitStopStationInteractionType.FaucetPosition: + if (packet.Value >= -1 && packet.Value <= 1) { - Multiplayer.LogWarning($"Pit Stop car selection change out of bounds! Requested: {(int)packet.State}, current car count: {Station.pitstop.carList.Count}"); + if (faucetPositioner.Percentage != packet.Value) + { + faucetTargetPercentage = packet.Value; + faucetTargetReached = false; + } } - break; + + case PitStopStationInteractionType.PayOrder: break; case PitStopStationInteractionType.CancelOrder: @@ -723,6 +927,5 @@ public void ProcessInteractionPacketAsClient(CommonPitStopInteractionPacket pack break; } } - #endregion } diff --git a/Multiplayer/Multiplayer.csproj b/Multiplayer/Multiplayer.csproj index 137a972e..51795938 100644 --- a/Multiplayer/Multiplayer.csproj +++ b/Multiplayer/Multiplayer.csproj @@ -16,6 +16,9 @@ + + + diff --git a/Multiplayer/Networking/Data/PitStopStationInteractionType.cs b/Multiplayer/Networking/Data/PitStopStationInteractionType.cs index dc4e8847..ef63755d 100644 --- a/Multiplayer/Networking/Data/PitStopStationInteractionType.cs +++ b/Multiplayer/Networking/Data/PitStopStationInteractionType.cs @@ -4,12 +4,20 @@ namespace Multiplayer.Networking.Data; public enum PitStopStationInteractionType : byte { - Reject, //bit 0 - Grab, //bit 0 - Ungrab, //bit 1 - StateUpdate, //bit 2 - SelectCar, //bit 3 - PayOrder, //bit 4 - CancelOrder, //bit 5 + Reject, + ResourceGrab, + ResourceUngrab, + ResourceUpdate, + + CarSelectorGrab, + CarSelectorUngrab, + CarSelection, + + FaucetGrab, + FaucetUngrab, + FaucetPosition, + + PayOrder, + CancelOrder, ProcessOrder } diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index b76d9756..cc594028 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -1007,7 +1007,7 @@ private void OnCommonPitStopInteractionPacket(CommonPitStopInteractionPacket pac Log($"Pit stop interaction received for {netPitStop.StationName}"); - LogDebug(() => $"OnCommonPitStopInteractionPacket() [{netPitStop.StationName}, {packet.NetId}], interaction: [{packet.InteractionType}], resource: {packet?.ResourceType}, State: {packet.State}"); + LogDebug(() => $"OnCommonPitStopInteractionPacket() [{netPitStop.StationName}, {packet.NetId}], interaction: [{packet.InteractionType}], resource: {packet?.ResourceType}, State: {packet.Value}"); netPitStop.ProcessInteractionPacketAsClient(packet); } @@ -1213,7 +1213,7 @@ public void SendCouplerInteraction(CouplerInteractionType flags, Coupler coupler // { // NetId = couplerNetId, //coupler.train.GetNetId(), // IsFrontCoupler = coupler.isFrontCoupler, - // State = (byte)coupler.state, + // Value = (byte)coupler.state, // OtherNetId = otherCouplerNetId, //otherCoupler.train.GetNetId(), // OtherState = (byte)otherCoupler.state, // OtherCarIsFrontCoupler = otherCoupler.isFrontCoupler, @@ -1431,7 +1431,7 @@ public void SendPitStopInteractionPacket(ushort netId, PitStopStationInteraction NetId = netId, InteractionType = (byte)interaction, ResourceType = res, - State = state + Value = state }, DeliveryMethod.ReliableOrdered); } diff --git a/Multiplayer/Networking/Packets/Common/CommonPitStopInteractionPacket.cs b/Multiplayer/Networking/Packets/Common/CommonPitStopInteractionPacket.cs index 4b3d35df..d3e7a416 100644 --- a/Multiplayer/Networking/Packets/Common/CommonPitStopInteractionPacket.cs +++ b/Multiplayer/Networking/Packets/Common/CommonPitStopInteractionPacket.cs @@ -6,5 +6,5 @@ public class CommonPitStopInteractionPacket public ushort NetId { get; set; } public byte InteractionType { get; set; } public int ResourceType { get; set; } - public float State { get; set; } + public float Value { get; set; } } From ce4e0928df0c832c38b2713d3b83096034c24d49 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 30 Mar 2025 08:55:57 +1000 Subject: [PATCH 280/521] Begin sync of cash register interactions --- .../World/NetworkedCashRegisterWithModules.cs | 25 +++++++++++++++++++ .../World/CashRegisterWithModulesPatch.cs | 17 +++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 Multiplayer/Components/Networking/World/NetworkedCashRegisterWithModules.cs create mode 100644 Multiplayer/Patches/World/CashRegisterWithModulesPatch.cs diff --git a/Multiplayer/Components/Networking/World/NetworkedCashRegisterWithModules.cs b/Multiplayer/Components/Networking/World/NetworkedCashRegisterWithModules.cs new file mode 100644 index 00000000..224b762a --- /dev/null +++ b/Multiplayer/Components/Networking/World/NetworkedCashRegisterWithModules.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using UnityEngine; + +namespace Multiplayer.Components.Networking.World; + +public class NetworkedCashRegisterWithModules : IdMonoBehaviour +{ + #region Lookup Cache + + public static bool Get(ushort netId, out NetworkedCashRegisterWithModules obj) + { + bool b = Get(netId, out IdMonoBehaviour rawObj); + obj = (NetworkedCashRegisterWithModules)rawObj; + return b; + } + + + + #endregion + + protected override bool IsIdServerAuthoritative => true; +} diff --git a/Multiplayer/Patches/World/CashRegisterWithModulesPatch.cs b/Multiplayer/Patches/World/CashRegisterWithModulesPatch.cs new file mode 100644 index 00000000..617d41e7 --- /dev/null +++ b/Multiplayer/Patches/World/CashRegisterWithModulesPatch.cs @@ -0,0 +1,17 @@ +using DV.CashRegister; +using HarmonyLib; +using Multiplayer.Components.Networking.World; +using Multiplayer.Utils; + +namespace Multiplayer.Patches.World; + +[HarmonyPatch(typeof(CashRegisterWithModules))] +public class CashRegisterWithModulesPatch +{ + [HarmonyPostfix] + [HarmonyPatch(nameof(CashRegisterWithModules.Awake))] + private static void Awake(CashRegisterWithModules __instance) + { + __instance.GetOrAddComponent(); + } +} From f2fab4e04f0fa26f1304710ec2b56b57259fcfeb Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 30 Mar 2025 21:46:48 +1000 Subject: [PATCH 281/521] Increase tickedQueue warning above full sync time --- Multiplayer/Components/Networking/TickedQueue.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Multiplayer/Components/Networking/TickedQueue.cs b/Multiplayer/Components/Networking/TickedQueue.cs index 5728fac3..1283f868 100644 --- a/Multiplayer/Components/Networking/TickedQueue.cs +++ b/Multiplayer/Components/Networking/TickedQueue.cs @@ -6,7 +6,7 @@ namespace Multiplayer.Components.Networking; public abstract class TickedQueue : MonoBehaviour { - private const float WARNING_THRESHOLD_SECONDS = 1.0f; + private const float WARNING_THRESHOLD_SECONDS = 3.0f; private const uint QUEUE_LENGTH_WARNING = (uint)(NetworkLifecycle.TICK_RATE * WARNING_THRESHOLD_SECONDS); private const uint SNAPSHOT_GAP_WARNING = (uint)(NetworkLifecycle.TICK_RATE * WARNING_THRESHOLD_SECONDS); From 678d8e42994b0909cc6fc7cdc6332dbb4453c3db Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 30 Mar 2025 21:47:33 +1000 Subject: [PATCH 282/521] Fixed issue with turntable not sync'ing from Host --- Multiplayer/Components/Networking/World/NetworkedJunction.cs | 4 +++- Multiplayer/Components/Networking/World/NetworkedTurntable.cs | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Multiplayer/Components/Networking/World/NetworkedJunction.cs b/Multiplayer/Components/Networking/World/NetworkedJunction.cs index e90531fd..3de0a112 100644 --- a/Multiplayer/Components/Networking/World/NetworkedJunction.cs +++ b/Multiplayer/Components/Networking/World/NetworkedJunction.cs @@ -18,11 +18,13 @@ protected override void Awake() base.Awake(); Junction = GetComponent(); Junction.Switched += Junction_Switched; + + initialised = NetworkLifecycle.Instance.IsHost(); } private void Junction_Switched(Junction.SwitchMode switchMode, int branch) { - if (NetworkLifecycle.Instance.IsProcessingPacket) + if (NetworkLifecycle.Instance.IsProcessingPacket || !initialised) return; NetworkLifecycle.Instance.Client.SendJunctionSwitched(NetId, (byte)branch, switchMode); diff --git a/Multiplayer/Components/Networking/World/NetworkedTurntable.cs b/Multiplayer/Components/Networking/World/NetworkedTurntable.cs index e26559b7..99f2f300 100644 --- a/Multiplayer/Components/Networking/World/NetworkedTurntable.cs +++ b/Multiplayer/Components/Networking/World/NetworkedTurntable.cs @@ -20,6 +20,8 @@ protected override void Awake() base.Awake(); TurntableRailTrack = GetComponent(); NetworkLifecycle.Instance.OnTick += OnTick; + + initialised = NetworkLifecycle.Instance.IsHost(); } protected override void OnDestroy() From 68f37fd5483fc64ca4ab2ffe8ffb7aec1d88102b Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 30 Mar 2025 21:53:29 +1000 Subject: [PATCH 283/521] Ready for release --- Multiplayer/Multiplayer.csproj | 2 +- info.json | 2 +- releases.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Multiplayer/Multiplayer.csproj b/Multiplayer/Multiplayer.csproj index 3876468c..b59390a0 100644 --- a/Multiplayer/Multiplayer.csproj +++ b/Multiplayer/Multiplayer.csproj @@ -3,7 +3,7 @@ net48 latest Multiplayer - 0.1.10.6 + 0.1.10.7 diff --git a/info.json b/info.json index 6d03d248..54e68cf7 100644 --- a/info.json +++ b/info.json @@ -1,6 +1,6 @@ { "Id": "Multiplayer", - "Version": "0.1.10.6", + "Version": "0.1.10.7", "DisplayName": "Multiplayer", "Author": "Insprill, Macka, Morm", "EntryMethod": "Multiplayer.Multiplayer.Load", diff --git a/releases.json b/releases.json index 466eb5a2..d48beaa5 100644 --- a/releases.json +++ b/releases.json @@ -1,6 +1,6 @@ { "Releases": [ - {"Id": "Multiplayer", "Version": "0.1.10.6", "DownloadUrl": "https://github.com/AMacro/dv-multiplayer/releases/download/v0.1.10.6-Beta/Multiplayer.0.1.10.6.zip"} + {"Id": "Multiplayer", "Version": "0.1.10.7", "DownloadUrl": "https://github.com/AMacro/dv-multiplayer/releases/download/v0.1.10.7-Beta/Multiplayer.0.1.10.7.zip"} ] } \ No newline at end of file From 9672d0a25d86d68f6d85c06b32d47b7a49cc7818 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 19 Apr 2025 09:16:08 +1000 Subject: [PATCH 284/521] Log LiteNetLib Version --- Multiplayer/Multiplayer.cs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/Multiplayer/Multiplayer.cs b/Multiplayer/Multiplayer.cs index 2b173354..268e2c49 100644 --- a/Multiplayer/Multiplayer.cs +++ b/Multiplayer/Multiplayer.cs @@ -6,6 +6,7 @@ using DV.UIFramework; using HarmonyLib; using JetBrains.Annotations; +using LiteNetLib; using Multiplayer.Components.MainMenu; using Multiplayer.Components.Networking; using Multiplayer.Editor; @@ -59,7 +60,11 @@ 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()}\r\nBuilbot version: {BuildInfo.BUILDBOT_INFO.ToString()}"); + Log($"\r\nMultiplayer JSON Version: {ModEntry.Info.Version}, Internal Version: {Ver}\r\n" + + $"Game version: {BuildInfo.BUILD_VERSION_MAJOR.ToString()}\r\n" + + $"Buildbot version: {BuildInfo.BUILDBOT_INFO.ToString()}\r\n" + + $"LiteNetLib version: {LiteNetLibVer()}\r\n"); + Log("Patching..."); harmony = new Harmony(ModEntry.Info.Id); @@ -171,6 +176,14 @@ private static void LateUpdate(UnityModManager.ModEntry modEntry, float deltaTim } } + static string LiteNetLibVer() + { + Assembly liteNetLibAssembly = typeof(NetManager).Assembly; + AssemblyName assemblyName = liteNetLibAssembly.GetName(); + + return assemblyName.Version.ToString(); + } + #region Logging public static void LogDebug(Func resolver) From 675c076fb7711d454ca6941862d063c32989ad8f Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 19 Apr 2025 13:54:26 +1000 Subject: [PATCH 285/521] Refactor lobby and connection workflow --- .../Components/MainMenu/ServerBrowserPane.cs | 292 ++++++++---------- .../MainMenu/RightPaneControllerPatch.cs | 2 +- Multiplayer/Utils/SteamWorksUtils.cs | 31 +- 3 files changed, 161 insertions(+), 164 deletions(-) diff --git a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs index 742b435d..ecb18363 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs @@ -18,6 +18,7 @@ using System.Collections.Generic; using Steamworks; using Steamworks.Data; +using System.Threading.Tasks; namespace Multiplayer.Components.MainMenu { @@ -26,11 +27,14 @@ public class ServerBrowserPane : MonoBehaviour private enum ConnectionState { NotConnected, + JoiningLobby, + AwaitingPassword, AttemptingSteamRelay, AttemptingIPv6, AttemptingIPv6Punch, AttemptingIPv4, AttemptingIPv4Punch, + Connected, Failed, Aborted } @@ -131,17 +135,17 @@ public void Update() timePassed += Time.deltaTime; if (!serverRefreshing) - { + { if (timePassed >= AUTO_REFRESH_TIME) { RefreshAction(); } - else if(timePassed >= REFRESH_MIN_TIME) + else if (timePassed >= REFRESH_MIN_TIME) { buttonRefresh.ToggleInteractable(true); } } - else if(remoteRefreshComplete) + else if (remoteRefreshComplete) { RefreshGridView(); IndexChanged(gridView); //Revalidate any selected servers @@ -159,29 +163,20 @@ public void Update() pingTimer = 0f; } - if (lobbyToJoin != null && lobbyToJoin?.Data?.Count() > 0) + if (lobbyToJoin != null && connectionState == ConnectionState.NotConnected) { - //For invites - Multiplayer.Log($"Player Invite initiated"); - if (lobbyToJoin != null) + //For invites/requests + Multiplayer.Log($"Player invite initiated/request"); + + if (lobbyToJoin.Value.Id.IsValid) { direct = false; - selectedLobby = lobbyToJoin; + var _ = JoinLobby((Lobby)lobbyToJoin); + } + else + { + Multiplayer.LogWarning("Received invalid lobby invite"); lobbyToJoin = null; - - string hasPass = selectedLobby?.GetData(SteamworksUtils.LOBBY_HAS_PASSWORD); - Multiplayer.Log($"Player Invite ({selectedLobby?.Id}) Has Password: {hasPass}"); - - if (string.IsNullOrEmpty(hasPass)) - { - Multiplayer.Log($"Player Invite ({selectedLobby?.Id}) Attempting connection..."); - AttemptConnection(); - } - else - { - Multiplayer.Log($"Player Invite ({selectedLobby?.Id}) Ask Password..."); - ShowPasswordPopup(); - } } } } @@ -192,7 +187,7 @@ public void Start() return; Multiplayer.Log($"Steam not detected, prompt for restart."); - MainMenuThingsAndStuff.Instance.ShowOkPopup("Steam not detected. Please restart the game with Steam running", ()=>{}); + MainMenuThingsAndStuff.Instance.ShowOkPopup("Steam not detected. Please restart the game with Steam running", () => { }); } private void CleanUI() @@ -225,7 +220,7 @@ private void BuildUI() //Create new objects GameObject serverScroll = Instantiate(scrollViewGO, serverNameGO.transform.position, Quaternion.identity, serverWindowGO.transform); - + /* * Setup server name @@ -287,7 +282,7 @@ private void BuildUI() scrollRect.content = contentRT; // Create TextMeshProUGUI object - GameObject textContainerGO = new ("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); @@ -309,7 +304,7 @@ private void BuildUI() textRT.offsetMax = new Vector2(0, 0); // Set content size to fit text - contentRT.sizeDelta = new Vector2(contentRT.sizeDelta.x -50, detailsPane.preferredHeight); + 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); @@ -378,7 +373,7 @@ private void SetupListeners(bool on) private void RefreshAction() { if (serverRefreshing) - return; + return; if (selectedServer != null) serverIDOnRefresh = selectedServer.id; @@ -395,43 +390,41 @@ private void RefreshAction() } private void JoinAction() { - if (selectedServer != null) - { - buttonDirectIP.ToggleInteractable(false); - buttonJoin.ToggleInteractable(false); - - //not making a direct connection - direct = false; - portNumber = -1; - var lobby = GetLobbyFromServer(selectedServer); - if (lobby != null) - { - selectedLobby = (Lobby)lobby; - password = null; //clear the password + if (selectedServer == null || connectionState != ConnectionState.NotConnected) + return; - if (selectedServer.HasPassword) - { - ShowPasswordPopup(); - return; - } + buttonDirectIP.ToggleInteractable(false); + buttonJoin.ToggleInteractable(false); - AttemptConnection(); + //not making a direct connection + direct = false; + portNumber = -1; - } + var lobby = GetLobbyFromServer(selectedServer); + if (lobby != null) + { + selectedLobby = (Lobby)lobby; + _ = JoinLobby((Lobby)selectedLobby); + } + else + { + Multiplayer.LogWarning($"JoinAction called but lobby is null"); + AttemptFail(); } } private void DirectAction() { - //Debug.Log($"DirectAction()"); + if(connectionState != ConnectionState.NotConnected) + return; + buttonDirectIP.ToggleInteractable(false); - buttonJoin.ToggleInteractable(false) ; + buttonJoin.ToggleInteractable(false); //making a direct connection direct = true; password = null; - //ShowSteamID(); ShowIpPopup(); } @@ -443,7 +436,7 @@ private void IndexChanged(AGridView gridView) if (gridView.SelectedModelIndex >= 0) { selectedServer = gridViewModel[gridView.SelectedModelIndex]; - + UpdateDetailsPane(); //Check if we can connect to this server @@ -484,7 +477,7 @@ private void UpdateDetailsPane() //note: built-in localisations have a trailing colon e.g. 'Game mode:' - details = "" + LocalizationAPI.L("launcher/game_mode", []) + " " + LobbyServerData.GetGameModeFromInt(selectedServer.GameMode) + "
    "; + 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 + "
    "; @@ -574,34 +567,6 @@ private void ShowIpPopup() }; } - //private void ShowSteamID() - //{ - // var popup = MainMenuThingsAndStuff.Instance.ShowRenamePopup(); - // if (popup == null) - // { - // Multiplayer.LogError("Popup not found."); - // return; - // } - - // popup.labelTMPro.text = "SteamID"; - // //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; - // } - - // steamId = popup.GetComponentInChildren().text; - // Multiplayer.LogDebug(() => $"Attempting to connecto SteamID: {steamId}"); - - // ShowPasswordPopup(); - // }; - //} - private void ShowPortPopup() { @@ -634,7 +599,7 @@ private void ShowPortPopup() ShowPasswordPopup(); } - }; + }; } @@ -658,16 +623,18 @@ private void ShowPasswordPopup() //Set us up to allow a blank password DestroyImmediate(popup.GetComponentInChildren()); popup.GetOrAddComponent(); - } + } popup.Closed += result => { if (result.closedBy == PopupClosedByAction.Abortion) { - buttonDirectIP.ToggleInteractable(true); + AttemptFail(); return; } + password = result.data; + if (direct) { //store params for later @@ -676,9 +643,7 @@ private void ShowPasswordPopup() Multiplayer.Settings.LastRemotePassword = result.data; } - password = result.data; - - AttemptConnection(); + InitiateConnection(); }; } @@ -695,7 +660,7 @@ public void ShowConnectingPopup() connectingPopup = popup; Localize loc = popup.positiveButton.GetComponentInChildren(); - loc.key ="cancel"; + loc.key = "cancel"; loc.UpdateLocalization(); @@ -705,49 +670,29 @@ public void ShowConnectingPopup() { connectionState = ConnectionState.Aborted; }; - + } - + #region workflow private void UpdatePings() { UpdatePingsSteam(); } - private async void AttemptConnection() + private void InitiateConnection() { - Multiplayer.Log($"AttemptConnection Direct: {direct}, Address: {address}, Lobby: {selectedLobby?.Id.ToString()}"); + Multiplayer.Log($"Initiating connection. Direct: {direct}, Address: {address}, Lobby: {selectedLobby?.Id.ToString()}"); attempt = 0; - connectionState = ConnectionState.NotConnected; ShowConnectingPopup(); - if (!direct) + if (!direct && joinedLobby != null) { - if(selectedLobby != null) - { - joinedLobby = selectedLobby; //store the lobby for when we disconnect - - connectionState = ConnectionState.AttemptingSteamRelay; - - var joinResult = await joinedLobby?.Join(); - if (joinResult == RoomEnter.Success) - { - string hostId = ((Lobby)joinedLobby).Owner.Id.Value.ToString(); - NetworkLifecycle.Instance.StartClient(hostId, -1, password, false, OnDisconnect); - } - else - { - Multiplayer.LogDebug(() => "AttemptConnection() Leaving Lobby"); - joinedLobby?.Leave(); - joinedLobby = null; - Multiplayer.Log($"Failed to join lobby: {joinResult}"); - AttemptFail(); - } - - return; - } + connectionState = ConnectionState.AttemptingSteamRelay; + string hostId = ((Lobby)joinedLobby).Owner.Id.Value.ToString(); + NetworkLifecycle.Instance.StartClient(hostId, -1, password, false, OnDisconnect); + return; } Multiplayer.Log($"AttemptConnection address: {address}"); @@ -764,13 +709,12 @@ private async void AttemptConnection() { AttemptIPv6(); } - - return; } - - Multiplayer.LogError($"IP address invalid: {address}"); - - AttemptFail(); + else + { + Multiplayer.LogError($"IP address invalid: {address}"); + AttemptFail(); + } } private void AttemptIPv6() @@ -874,6 +818,9 @@ private void AttemptFail() connectingPopup = null; // Clear the reference } + joinedLobby?.Leave(); + joinedLobby = null; + if (gameObject != null && gameObject.activeInHierarchy) { if (gridView != null) @@ -882,51 +829,30 @@ private void AttemptFail() if (buttonDirectIP != null && buttonDirectIP.gameObject != null) buttonDirectIP.ToggleInteractable(true); } + + StartCoroutine(ResetConnectionState()); + } + + private IEnumerator ResetConnectionState() + { + yield return new WaitForSeconds(1.0f); + connectionState = ConnectionState.NotConnected; } private void OnDisconnect(DisconnectReason reason, string message) { Multiplayer.Log($"Disconnected due to: {reason}, \"{message}\""); - string displayMessage = message; + string displayMessage = !string.IsNullOrEmpty(message) + ? message + : GetDisplayMessageForDisconnect(reason); Multiplayer.LogDebug(() => "OnDisconnect() Leaving Lobby"); joinedLobby?.Leave(); joinedLobby = null; - if (string.IsNullOrEmpty(message)) - { - //fallback for no message (server initiated disconnects should have a message) - //if (reason == DisconnectReason.ConnectionFailed) - //{ - // switch (connectionState) - // { - // case ConnectionState.AttemptingIPv6: - // if (Multiplayer.Settings.EnableNatPunch) - // AttemptIPv6Punch(); - // else - // AttemptIPv4(); - // return; - // case ConnectionState.AttemptingIPv6Punch: - // AttemptIPv4(); - // return; - // case ConnectionState.AttemptingIPv4: - // if (Multiplayer.Settings.EnableNatPunch) - // { - // AttemptIPv4Punch(); - // return; - // } - // break; - // } - //} - - displayMessage = GetDisplayMessageForDisconnect(reason); - AttemptFail(); - } - else - { - connectionState = ConnectionState.NotConnected; - } + connectionState = ConnectionState.NotConnected; + AttemptFail(); NetworkLifecycle.Instance.QueueMainMenuEvent(() => { @@ -978,7 +904,7 @@ private async void ListActiveLobbies() remoteServers.Add(server); Multiplayer.LogDebug(() => $"ListActiveLobbies() lobby {server.Name}, {lobby.MemberCount}/{lobby.MaxMembers}"); - + } } remoteRefreshComplete = true; @@ -990,7 +916,7 @@ private void UpdatePingsSteam() { if (server is LobbyServerData lobbyServer) { - if (ulong.TryParse(server.id,out ulong id)) + if (ulong.TryParse(server.id, out ulong id)) { Lobby? lobby = lobbies.FirstOrDefault(l => l.Id.Value == id); if (lobby != null) @@ -1102,5 +1028,55 @@ private string ExtractDomainName(string input) return input; } + + private async Task JoinLobby(Lobby lobby) + { + + if (connectionState != ConnectionState.NotConnected) + { + Multiplayer.LogWarning($"Cannot join lobby while in state: {connectionState}"); + return false; + } + + connectionState = ConnectionState.JoiningLobby; + Multiplayer.Log($"Attempting to join lobby ({lobby.Id})"); + + var joinResult = await lobby.Join(); + + if (joinResult == RoomEnter.Success) + { + Multiplayer.Log($"Lobby joined ({lobby.Id})"); + + joinedLobby = lobby; + lobbyToJoin = null; + + string hasPass = lobby.GetData(SteamworksUtils.LOBBY_HAS_PASSWORD); + Multiplayer.Log($"Lobby ({lobby.Id}) has password: {hasPass}"); + + if (string.IsNullOrEmpty(hasPass)) + { + Multiplayer.Log($"Attempting connection..."); + InitiateConnection(); + } + else + { + connectionState = ConnectionState.AwaitingPassword; + Multiplayer.Log($"Prompting for password..."); + ShowPasswordPopup(); + } + + return true; + } + else + { + Multiplayer.LogDebug(() => "JoinLobby() Leaving Lobby"); + lobby.Leave(); + joinedLobby = null; + Multiplayer.Log($"Failed to join lobby: {joinResult}"); + AttemptFail(); + } + + return false; + } } } diff --git a/Multiplayer/Patches/MainMenu/RightPaneControllerPatch.cs b/Multiplayer/Patches/MainMenu/RightPaneControllerPatch.cs index 1a75e915..37ffe7d0 100644 --- a/Multiplayer/Patches/MainMenu/RightPaneControllerPatch.cs +++ b/Multiplayer/Patches/MainMenu/RightPaneControllerPatch.cs @@ -108,6 +108,6 @@ private static void OnEnablePost(RightPaneController __instance) SteamFriends.OnGameLobbyJoinRequested += SteamworksUtils.OnLobbyJoinRequest; if (Environment.GetCommandLineArgs().Contains("+connect_lobby")) - SteamworksUtils.JoinFromCommandLine(); + __instance.StartCoroutine(SteamworksUtils.JoinFromCommandLine()); } } diff --git a/Multiplayer/Utils/SteamWorksUtils.cs b/Multiplayer/Utils/SteamWorksUtils.cs index 24e9bbbc..d93565e0 100644 --- a/Multiplayer/Utils/SteamWorksUtils.cs +++ b/Multiplayer/Utils/SteamWorksUtils.cs @@ -1,3 +1,4 @@ +using DV; using DV.Localization; using DV.UIFramework; using Multiplayer.Components.MainMenu; @@ -7,7 +8,9 @@ using Steamworks; using Steamworks.Data; using System; +using System.Collections; using System.Linq; +using UnityEngine; namespace Multiplayer.Utils; @@ -105,12 +108,23 @@ public static ulong GetLobbyIdFromArgs() return 0; } - public static void JoinFromCommandLine() + public static IEnumerator JoinFromCommandLine() { - if (hasJoinedCL) - return; + float time = Time.time; + + Multiplayer.LogDebug(() => $"JoinFromCommandLine() {DVSteamworks.Success}"); + + if (hasJoinedCL || BuildInfo.BUILD_DESTINATION != "steam") + yield break; + hasJoinedCL = true; + //allow steamworks to initialise + yield return new WaitUntil(()=>{ return DVSteamworks.Success || (Time.deltaTime - time) > 5; }); + + if (!DVSteamworks.Success) + yield break; + var id = GetLobbyIdFromArgs(); var sId = new SteamId { @@ -119,6 +133,8 @@ public static void JoinFromCommandLine() var lobby = new Lobby(sId); lobby.Refresh(); + + QueueLobbyInvite(lobby); } private static bool CanHandleLobbyRequest() @@ -134,16 +150,20 @@ public static void OnLobbyJoinRequest(Lobby lobby, SteamId id) if (!CanHandleLobbyRequest()) return; + lobby.Refresh(); + QueueLobbyInvite(lobby); } public static void OnLobbyInviteRequest(Friend friend, Lobby lobby) { - Multiplayer.Log($"Received lobby invite: {lobby.Id}"); + Multiplayer.Log($"Received lobby invite from '{friend.Name}' ({friend.Id}), Lobby: {lobby.Id}"); if (!CanHandleLobbyRequest()) return; + lobby.Refresh(); + NetworkLifecycle.Instance.QueueMainMenuEvent(() => { var popup = MainMenuThingsAndStuff.Instance.ShowYesNoPopup(); @@ -163,8 +183,9 @@ public static void OnLobbyInviteRequest(Friend friend, Lobby lobby) popup.Closed += (PopupResult result) => { + Multiplayer.LogDebug(()=>$"Agreed to join: {result.closedBy}"); if (result.closedBy == PopupClosedByAction.Positive) - QueueLobbyInvite(lobby); + QueueLobbyInvite(lobby); }; }); From 17d2e9e8ed2b49640074021d0263740843435350 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 19 Apr 2025 13:55:05 +1000 Subject: [PATCH 286/521] Add exception for RemoteDispatch No longer enforced on client and server --- Multiplayer/Networking/Managers/Server/NetworkServer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index 0c7f823b..16dab48e 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -62,7 +62,7 @@ public class NetworkServer : NetworkManager 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", "RemoteDispatch"]; public NetworkServer(IDifficulty difficulty, Settings settings, bool isSinglePlayer, LobbyServerData serverData) : base(settings) { From e95c08a3ed5d465864f1647f04d596aa97183140 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 19 Apr 2025 13:55:30 +1000 Subject: [PATCH 287/521] Bypass password for friend-only games --- Multiplayer/Networking/Managers/Server/NetworkServer.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index 16dab48e..c68080ab 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -613,7 +613,9 @@ private void OnServerboundClientLoginPacket(ServerboundClientLoginPacket packet, Log($"Processing login packet for {packet.Username} ({guid}){(Multiplayer.Settings.LogIps ? $" at {request.RemoteEndPoint.Address}" : "")}"); - if (Multiplayer.Settings.Password != packet.Password) + //if the visibility is friends only, bypass the password check + if (serverData.Visibility != Components.MainMenu.ServerVisibility.Friends && + Multiplayer.Settings.Password != packet.Password) { LogWarning("Denied login due to invalid password!"); ClientboundLoginResponsePacket denyPacket = new() From 50b1d300252656e4d2c1d972b233841098148a8d Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 19 Apr 2025 14:20:07 +1000 Subject: [PATCH 288/521] Improve startup logging --- Multiplayer/Multiplayer.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Multiplayer/Multiplayer.cs b/Multiplayer/Multiplayer.cs index 268e2c49..e1c1d626 100644 --- a/Multiplayer/Multiplayer.cs +++ b/Multiplayer/Multiplayer.cs @@ -60,8 +60,11 @@ private static bool Load(UnityModManager.ModEntry modEntry) Locale.Load(ModEntry.Path); + var gameVer = BuildInfo.BUILD_VERSION_MAJOR.ToString() + + (string.IsNullOrEmpty(BuildInfo.BUILD_VERSION_SUFFIX) ? "" : "." + BuildInfo.BUILD_VERSION_SUFFIX); + Log($"\r\nMultiplayer JSON Version: {ModEntry.Info.Version}, Internal Version: {Ver}\r\n" + - $"Game version: {BuildInfo.BUILD_VERSION_MAJOR.ToString()}\r\n" + + $"Game version: {gameVer}\r\n" + $"Buildbot version: {BuildInfo.BUILDBOT_INFO.ToString()}\r\n" + $"LiteNetLib version: {LiteNetLibVer()}\r\n"); From e6e87f8a751a453af275868fb522af5ba123e099 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 19 Apr 2025 18:46:33 +1000 Subject: [PATCH 289/521] Reverse change to bypass password --- Multiplayer/Networking/Managers/Server/NetworkServer.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index c68080ab..16dab48e 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -613,9 +613,7 @@ private void OnServerboundClientLoginPacket(ServerboundClientLoginPacket packet, Log($"Processing login packet for {packet.Username} ({guid}){(Multiplayer.Settings.LogIps ? $" at {request.RemoteEndPoint.Address}" : "")}"); - //if the visibility is friends only, bypass the password check - if (serverData.Visibility != Components.MainMenu.ServerVisibility.Friends && - Multiplayer.Settings.Password != packet.Password) + if (Multiplayer.Settings.Password != packet.Password) { LogWarning("Denied login due to invalid password!"); ClientboundLoginResponsePacket denyPacket = new() From b430f2111558833116865df3e83ca73b016937fc Mon Sep 17 00:00:00 2001 From: AMacro Date: Mon, 21 Apr 2025 17:29:38 +1000 Subject: [PATCH 290/521] B99.4 Compilation fixes Fixes to allow compilation, no testing/debugging --- .../Networking/World/NetworkedJunction.cs | 2 +- .../Networking/World/NetworkedTurntable.cs | 2 +- .../Components/SaveGame/Client_GameSession.cs | 8 +++++--- Multiplayer/Multiplayer.csproj | 13 ++++++++++--- Multiplayer/Networking/Data/TaskNetworkData.cs | 4 ++-- .../Managers/Client/ClientPlayerManager.cs | 2 +- .../Networking/Managers/Client/NetworkClient.cs | 6 +++--- .../Networking/Managers/Server/NetworkServer.cs | 4 ++-- .../CommsRadio/CommsRadioCarDeleterPatch.cs | 2 +- Multiplayer/Patches/Train/TrainsOptimizerPatch.cs | 6 ++---- .../Patches/World/StationLocoSpawnerPatch.cs | 14 ++++++++------ 11 files changed, 36 insertions(+), 27 deletions(-) diff --git a/Multiplayer/Components/Networking/World/NetworkedJunction.cs b/Multiplayer/Components/Networking/World/NetworkedJunction.cs index 3de0a112..9a00ea1c 100644 --- a/Multiplayer/Components/Networking/World/NetworkedJunction.cs +++ b/Multiplayer/Components/Networking/World/NetworkedJunction.cs @@ -6,7 +6,7 @@ namespace Multiplayer.Components.Networking.World; public class NetworkedJunction : IdMonoBehaviour { private static NetworkedJunction[] _indexedJunctions; - public static NetworkedJunction[] IndexedJunctions => _indexedJunctions ??= WorldData.Instance.TrackRootParent.GetComponentsInChildren().OrderBy(nj => nj.NetId).ToArray(); + public static NetworkedJunction[] IndexedJunctions => _indexedJunctions ??= RailTrackRegistry.Instance.TrackRootParent.GetComponentsInChildren().OrderBy(nj => nj.NetId).ToArray(); protected override bool IsIdServerAuthoritative => false; diff --git a/Multiplayer/Components/Networking/World/NetworkedTurntable.cs b/Multiplayer/Components/Networking/World/NetworkedTurntable.cs index 99f2f300..0d8167ac 100644 --- a/Multiplayer/Components/Networking/World/NetworkedTurntable.cs +++ b/Multiplayer/Components/Networking/World/NetworkedTurntable.cs @@ -7,7 +7,7 @@ namespace Multiplayer.Components.Networking.World; public class NetworkedTurntable : IdMonoBehaviour { private static NetworkedTurntable[] _indexedTurntables; - public static NetworkedTurntable[] IndexedTurntables => _indexedTurntables ??= WorldData.Instance.TrackRootParent.GetComponentsInChildren().OrderBy(nj => nj.NetId).ToArray(); + public static NetworkedTurntable[] IndexedTurntables => _indexedTurntables ??= RailTrackRegistry.Instance.TrackRootParent.GetComponentsInChildren().OrderBy(nj => nj.NetId).ToArray(); protected override bool IsIdServerAuthoritative => false; diff --git a/Multiplayer/Components/SaveGame/Client_GameSession.cs b/Multiplayer/Components/SaveGame/Client_GameSession.cs index 7b8a3f8c..4c0fe2b5 100644 --- a/Multiplayer/Components/SaveGame/Client_GameSession.cs +++ b/Multiplayer/Components/SaveGame/Client_GameSession.cs @@ -7,10 +7,7 @@ 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; @@ -94,4 +91,9 @@ int IGameSession.TrimSaves(SaveType type, int maxCount, ISaveGame excluded) { return 0; } + + bool IGameSession.CanCreateNewSaves(SaveType saveType) + { + return false; + } } diff --git a/Multiplayer/Multiplayer.csproj b/Multiplayer/Multiplayer.csproj index b59390a0..6171c32e 100644 --- a/Multiplayer/Multiplayer.csproj +++ b/Multiplayer/Multiplayer.csproj @@ -13,7 +13,9 @@ + + @@ -23,18 +25,22 @@ + + + + @@ -44,8 +50,6 @@ - - @@ -54,6 +58,7 @@ + @@ -62,11 +67,13 @@ + + ../build/UnityChan.dll - + diff --git a/Multiplayer/Networking/Data/TaskNetworkData.cs b/Multiplayer/Networking/Data/TaskNetworkData.cs index 37caf8c8..96debcd3 100644 --- a/Multiplayer/Networking/Data/TaskNetworkData.cs +++ b/Multiplayer/Networking/Data/TaskNetworkData.cs @@ -303,8 +303,8 @@ public override Task ToTask() return new TransportTask( cars, - RailTrackRegistry.Instance.GetTrackWithName(DestinationTrack).logicTrack, - RailTrackRegistry.Instance.GetTrackWithName(StartingTrack).logicTrack, + RailTrackRegistry.Instance.GetTrackWithName(DestinationTrack).LogicTrack(), + RailTrackRegistry.Instance.GetTrackWithName(StartingTrack).LogicTrack(), TransportedCargoPerCar?.ToList() ); } diff --git a/Multiplayer/Networking/Managers/Client/ClientPlayerManager.cs b/Multiplayer/Networking/Managers/Client/ClientPlayerManager.cs index bcd74451..a8abaae8 100644 --- a/Multiplayer/Networking/Managers/Client/ClientPlayerManager.cs +++ b/Multiplayer/Networking/Managers/Client/ClientPlayerManager.cs @@ -30,7 +30,7 @@ public bool TryGetPlayer(byte id, out NetworkedPlayer player) public void AddPlayer(byte id, string username) { - GameObject go = Object.Instantiate(playerPrefab, WorldMover.Instance.originShiftParent); + GameObject go = Object.Instantiate(playerPrefab, WorldMover.OriginShiftParent); go.layer = LayerMask.NameToLayer(Layers.Player); NetworkedPlayer networkedPlayer = go.AddComponent(); networkedPlayer.Id = id; diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 6e6d7ee0..5bbd2a5b 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -370,7 +370,7 @@ private void OnClientboundBeginWorldSyncPacket(ClientboundBeginWorldSyncPacket p private void OnClientboundWeatherPacket(ClientboundWeatherPacket packet) { - WeatherDriver.Instance.LoadSaveData(JObject.FromObject(packet)); + WeatherDriver.Instance.LoadSaveData(JObject.FromObject(packet), Globals.G.GameParams.WeatherEditorAlwaysAllowed); } private void OnClientboundRemoveLoadingScreen(ClientboundRemoveLoadingScreenPacket packet) @@ -833,8 +833,8 @@ private void OnClientboundRerailTrainPacket(ClientboundRerailTrainPacket packet) 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}"); + 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); } diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index 16dab48e..e58c501e 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -742,7 +742,7 @@ private void OnServerboundClientReadyPacket(ServerboundClientReadyPacket packet, SendPacket(peer, new ClientboundBeginWorldSyncPacket(), DeliveryMethod.ReliableOrdered); // Send weather state - SendPacket(peer, WeatherDriver.Instance.GetSaveData().ToObject(), DeliveryMethod.ReliableOrdered); + SendPacket(peer, WeatherDriver.Instance.GetSaveData(Globals.G.GameParams.WeatherEditorAlwaysAllowed).ToObject(), DeliveryMethod.ReliableOrdered); // Send junctions and turntables SendPacket(peer, new ClientboundRailwayStatePacket @@ -1030,7 +1030,7 @@ private void OnServerboundTrainDeleteRequestPacket(ServerboundTrainDeleteRequest return; } - Job job = JobsManager.Instance.GetJobOfCar(trainCar); + Job job = JobsManager.Instance.GetJobOfCar(trainCar.logicCar); switch (job?.State) { case JobState.Available: diff --git a/Multiplayer/Patches/CommsRadio/CommsRadioCarDeleterPatch.cs b/Multiplayer/Patches/CommsRadio/CommsRadioCarDeleterPatch.cs index 731595bb..e7b56046 100644 --- a/Multiplayer/Patches/CommsRadio/CommsRadioCarDeleterPatch.cs +++ b/Multiplayer/Patches/CommsRadio/CommsRadioCarDeleterPatch.cs @@ -46,7 +46,7 @@ private static IEnumerator PlaySoundsLater(CommsRadioCarDeleter __instance, Vect 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. - __instance.removeCarSound.Play(trainPosition, minDistance: CommsRadioController.CAR_AUDIO_SOURCE_MIN_DISTANCE, parent: WorldMover.Instance.originShiftParent); + __instance.removeCarSound.Play(trainPosition, minDistance: CommsRadioController.CAR_AUDIO_SOURCE_MIN_DISTANCE, parent: WorldMover.OriginShiftParent); CommsRadioController.PlayAudioFromRadio(__instance.confirmSound, __instance.transform); } diff --git a/Multiplayer/Patches/Train/TrainsOptimizerPatch.cs b/Multiplayer/Patches/Train/TrainsOptimizerPatch.cs index 4338a000..1703e530 100644 --- a/Multiplayer/Patches/Train/TrainsOptimizerPatch.cs +++ b/Multiplayer/Patches/Train/TrainsOptimizerPatch.cs @@ -19,22 +19,20 @@ public static void ForceOptimizationStateOnCars(TrainsOptimizer __instance, Exce Multiplayer.LogDebug(() => { - Dictionary logicCarToTrainCar = SingletonBehaviour.Instance.logicCarToTrainCar; - if (carsToProcess == null) return $"TrainsOptimizer.ForceOptimizationStateOnCars() carsToProcess is null!"; StringBuilder sb = new StringBuilder(); sb.Append($"TrainsOptimizer.ForceOptimizationStateOnCars() iterating over {carsToProcess?.Count} cars:\r\n"); - int i=0 ; + 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); + bool result = TrainCarRegistry.Instance.logicCarToTrainCar.TryGetValue(car, out TrainCar trainCar); sb.AppendLine($"\tCar {i} id {car?.ID} found TrainCar: {result}, TC ID: {trainCar?.ID}"); } diff --git a/Multiplayer/Patches/World/StationLocoSpawnerPatch.cs b/Multiplayer/Patches/World/StationLocoSpawnerPatch.cs index 3bf5ff31..b36b4a9f 100644 --- a/Multiplayer/Patches/World/StationLocoSpawnerPatch.cs +++ b/Multiplayer/Patches/World/StationLocoSpawnerPatch.cs @@ -1,5 +1,6 @@ using System.Collections; using System.Collections.Generic; +using System.Linq; using DV.Logic.Job; using DV.ThingTypes; using DV.Utils; @@ -22,7 +23,7 @@ private static void Postfix(StationLocoSpawner __instance) private static IEnumerator WaitForSetup(StationLocoSpawner __instance) { - if (!AStartGameData.carsAndJobsLoadingFinished || SingletonBehaviour.Instance.PoolSetupInProgress) + if (!AStartGameData.carsAndJobsLoadingFinished || CarSpawner.Instance.PoolSetupInProgress) yield return null; while (NetworkLifecycle.Instance.Client == null) yield return null; @@ -37,7 +38,6 @@ private static IEnumerator CheckShouldSpawn(StationLocoSpawner __instance) { yield return CHECK_DELAY; - bool anyoneWithinRange = __instance.spawnTrackMiddleAnchor.transform.position.AnyPlayerSqrMag() < __instance.spawnLocoPlayerSqrDistanceFromTrack; switch (__instance.playerEnteredLocoSpawnRange) @@ -55,15 +55,17 @@ private static IEnumerator CheckShouldSpawn(StationLocoSpawner __instance) private static void SpawnLocomotives(StationLocoSpawner stationLocoSpawner) { - List carsFullyOnTrack = stationLocoSpawner.locoSpawnTrack.logicTrack.GetCarsFullyOnTrack(); + List carsFullyOnTrack = stationLocoSpawner.locoSpawnTrack.LogicTrack().GetCarsFullyOnTrack(); if (carsFullyOnTrack.Count != 0 && carsFullyOnTrack.Exists(car => CarTypes.IsLocomotive(car.carType))) return; List trainCarTypes = new(stationLocoSpawner.locoTypeGroupsToSpawn[stationLocoSpawner.nextLocoGroupSpawnIndex].liveries); stationLocoSpawner.nextLocoGroupSpawnIndex = Random.Range(0, stationLocoSpawner.locoTypeGroupsToSpawn.Count); - List unusedTrainCars = - SingletonBehaviour.Instance.SpawnCarTypesOnTrack(trainCarTypes, null, stationLocoSpawner.locoSpawnTrack, true, true, flipTrainConsist: stationLocoSpawner.spawnRotationFlipped); + List unusedTrainCars = + CarSpawner.Instance.SpawnCarTypesOnTrack(trainCarTypes, null, stationLocoSpawner.locoSpawnTrack, true, true, flipTrainConsist: stationLocoSpawner.spawnRotationFlipped) + .Select(TC => TC.logicCar).ToList(); + if (unusedTrainCars != null) - SingletonBehaviour.Instance.MarkForDelete(unusedTrainCars); + UnusedTrainCarDeleter.Instance.MarkForDelete(unusedTrainCars); } } From e92a0fd959ef7f76d436f5f118b9eb86c114b7f9 Mon Sep 17 00:00:00 2001 From: AMacro Date: Mon, 21 Apr 2025 17:30:35 +1000 Subject: [PATCH 291/521] Fix B99.4 Localisation issues --- .../Components/MainMenu/HostGamePane.cs | 47 +++++++++++++++---- .../MainMenu/LocalizationManagerPatch.cs | 2 +- Multiplayer/Utils/DvExtensions.cs | 1 + 3 files changed, 39 insertions(+), 11 deletions(-) diff --git a/Multiplayer/Components/MainMenu/HostGamePane.cs b/Multiplayer/Components/MainMenu/HostGamePane.cs index 3f0f2c75..5809b029 100644 --- a/Multiplayer/Components/MainMenu/HostGamePane.cs +++ b/Multiplayer/Components/MainMenu/HostGamePane.cs @@ -242,18 +242,34 @@ private void BuildUI() /* * Server visibility field */ + selectorPrefab.SetActive(false); go = GameObject.Instantiate(selectorPrefab, NewContentGroup(controls, scroller.viewport.sizeDelta).transform, false); - go.name = "Visibility"; - go.FindChildByName("[text label]").GetComponent().key = Locale.SERVER_HOST_VISIBILITY_KEY; - go.ResetTooltip(); - go.FindChildByName("[text label]").GetComponent().UpdateLocalization(); - DestroyImmediate(go.GetComponent()); + selectorPrefab.SetActive(true); gameVisibility = go.GetOrAddComponent(); + + //clean-up + + if (gameVisibility.labelTMPro?.gameObject.TryGetComponent(out var loc) ?? false) + GameObject.DestroyImmediate(loc); + if (gameVisibility.labelTMPro?.gameObject.TryGetComponent(out var loc2) ?? false) + GameObject.DestroyImmediate(loc2); + + DestroyImmediate(go.GetComponent()); + + go.name = "Visibility"; + gameVisibility.initialized = false; + gameVisibility.LocalizedLabel = true; gameVisibility.SetLabel(Locale.SERVER_HOST_VISIBILITY_KEY); + gameVisibility.labelTMPro.GetComponent().key = Locale.SERVER_HOST_VISIBILITY_KEY; + gameVisibility.LocalizedValues = true; gameVisibility.SetValues(Locale.SERVER_HOST_VISIBILITY_MODES.ToList()); gameVisibility.SetSelectedIndex(3); + + go.SetActive(true); + go.ResetTooltip(); + gameVisibility.ToggleInteractable(true); /* @@ -275,17 +291,28 @@ private void BuildUI() /* * Server max players field */ + sliderPrefab.SetActive(false); go = GameObject.Instantiate(sliderPrefab, NewContentGroup(controls, scroller.viewport.sizeDelta).transform, false); + sliderPrefab.SetActive(true); + maxPlayers = go.GetComponent(); + go.name = "Max Players"; - go.FindChildByName("[text label]").GetComponent().key = Locale.SERVER_HOST_MAX_PLAYERS_KEY; - go.ResetTooltip(); - go.FindChildByName("[text label]").GetComponent().UpdateLocalization(); + var labelGo = go.FindChildByName("[text label]"); + + if (labelGo?.gameObject.TryGetComponent(out loc) ?? false) + GameObject.DestroyImmediate(loc); + DestroyImmediate(go.GetComponent()); - maxPlayers = go.GetComponent(); + + labelGo.GetComponent().key = Locale.SERVER_HOST_MAX_PLAYERS_KEY; + go.ResetTooltip(); + //labelGo.GetComponent().UpdateLocalization(); + maxPlayers.stepIncrement = 1; maxPlayers.minValue = MIN_PLAYERS; maxPlayers.maxValue = MAX_PLAYERS; maxPlayers.value = Mathf.Clamp(Multiplayer.Settings.MaxPlayers,MIN_PLAYERS,MAX_PLAYERS); + go.SetActive(true); maxPlayers.interactable = true; /* @@ -296,7 +323,7 @@ private void BuildUI() port = go.GetComponent(); port.characterValidation = TMP_InputField.CharacterValidation.Integer; port.characterLimit = MAX_PORT_LEN; - port.placeholder.GetComponent().text = "7777"; + port.placeholder.GetComponent().text = DEFAULT_PORT.ToString(); port.text = (Multiplayer.Settings.Port >= MIN_PORT && Multiplayer.Settings.Port <= MAX_PORT) ? Multiplayer.Settings.Port.ToString() : DEFAULT_PORT.ToString(); /* diff --git a/Multiplayer/Patches/MainMenu/LocalizationManagerPatch.cs b/Multiplayer/Patches/MainMenu/LocalizationManagerPatch.cs index 7fd486f3..ceec2547 100644 --- a/Multiplayer/Patches/MainMenu/LocalizationManagerPatch.cs +++ b/Multiplayer/Patches/MainMenu/LocalizationManagerPatch.cs @@ -20,7 +20,7 @@ private static bool TryGetTranslation_Prefix(ref bool __result, string Term, out Translation = string.Empty; // Check if the term starts with the specified locale prefix - if (!Term.StartsWith(Locale.PREFIX)) + if (Term == null || !Term.StartsWith(Locale.PREFIX)) return true; // Attempt to get the translation for the term diff --git a/Multiplayer/Utils/DvExtensions.cs b/Multiplayer/Utils/DvExtensions.cs index e25ef674..3136b43a 100644 --- a/Multiplayer/Utils/DvExtensions.cs +++ b/Multiplayer/Utils/DvExtensions.cs @@ -104,6 +104,7 @@ public static void ResetTooltip(this GameObject button) { // Reset the tooltip keys for the button UIElementTooltip tooltip = button.GetComponent(); + tooltip.initialized = false; tooltip.disabledKey = null; tooltip.enabledKey = null; From 1218324f34154720f2428ed4724323770d8537fc Mon Sep 17 00:00:00 2001 From: AMacro Date: Mon, 21 Apr 2025 20:31:21 +1000 Subject: [PATCH 292/521] Logging changes --- Multiplayer/Multiplayer.cs | 8 ++++---- Multiplayer/Networking/Managers/NetworkManager.cs | 3 --- Multiplayer/Patches/Train/CouplerPatch.cs | 1 + 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/Multiplayer/Multiplayer.cs b/Multiplayer/Multiplayer.cs index e1c1d626..db32a554 100644 --- a/Multiplayer/Multiplayer.cs +++ b/Multiplayer/Multiplayer.cs @@ -63,10 +63,10 @@ private static bool Load(UnityModManager.ModEntry modEntry) var gameVer = BuildInfo.BUILD_VERSION_MAJOR.ToString() + (string.IsNullOrEmpty(BuildInfo.BUILD_VERSION_SUFFIX) ? "" : "." + BuildInfo.BUILD_VERSION_SUFFIX); - Log($"\r\nMultiplayer JSON Version: {ModEntry.Info.Version}, Internal Version: {Ver}\r\n" + - $"Game version: {gameVer}\r\n" + - $"Buildbot version: {BuildInfo.BUILDBOT_INFO.ToString()}\r\n" + - $"LiteNetLib version: {LiteNetLibVer()}\r\n"); + Log($"\r\n\tMultiplayer JSON Version: {ModEntry.Info.Version}, Internal Version: {Ver}\r\n" + + $"\tGame version: {gameVer}\r\n" + + $"\tBuildbot version: {BuildInfo.BUILDBOT_INFO.ToString()}\r\n" + + $"\tLiteNetLib version: {LiteNetLibVer()}\r\n"); Log("Patching..."); diff --git a/Multiplayer/Networking/Managers/NetworkManager.cs b/Multiplayer/Networking/Managers/NetworkManager.cs index af3ef91e..142fc5c1 100644 --- a/Multiplayer/Networking/Managers/NetworkManager.cs +++ b/Multiplayer/Networking/Managers/NetworkManager.cs @@ -28,8 +28,6 @@ public abstract class NetworkManager protected NetworkManager(Settings settings) { - Multiplayer.LogDebug(() => $"NetworkManager Constructor"); - netPacketProcessor = new NetPacketProcessor(); //transport = new LiteNetLibTransport(); transport = new SteamWorksTransport(); @@ -41,7 +39,6 @@ protected NetworkManager(Settings settings) transport.OnNetworkError += OnNetworkError; transport.OnNetworkLatencyUpdate += OnNetworkLatencyUpdate; - RegisterNestedTypes(); OnSettingsUpdated(settings); diff --git a/Multiplayer/Patches/Train/CouplerPatch.cs b/Multiplayer/Patches/Train/CouplerPatch.cs index ce829e9f..2fd823c1 100644 --- a/Multiplayer/Patches/Train/CouplerPatch.cs +++ b/Multiplayer/Patches/Train/CouplerPatch.cs @@ -42,6 +42,7 @@ private static void DisconnectAirHose(Coupler __instance, bool playAudio) return; } + Multiplayer.LogDebug(() => $"DisconnectAirHose({__instance?.train?.ID}, {__instance.isFrontCoupler})"); NetworkLifecycle.Instance.Client?.SendHoseDisconnected(__instance, playAudio); } From 74631985911374d113f04d041f31fa850669061b Mon Sep 17 00:00:00 2001 From: AMacro Date: Mon, 21 Apr 2025 20:32:58 +1000 Subject: [PATCH 293/521] Change physics Ticks tracking Only reset TicksSinceSync if a full sync has occurred Only process new physics packets --- .../Networking/Train/NetworkTrainsetWatcher.cs | 5 ++++- .../Components/Networking/Train/NetworkedTrainCar.cs | 11 ++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs b/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs index cfd821e6..23b35ddc 100644 --- a/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs +++ b/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs @@ -128,6 +128,9 @@ private void Server_TickSet(Trainset set, uint tick) { position = trainCar.transform.position - WorldMover.currentMove; rotation = trainCar.transform.rotation; + + //reset this car's states + networkedTrainCar.TicksSinceSync = 0; } trainsetParts[i] = new TrainsetMovementPart( @@ -142,7 +145,7 @@ private void Server_TickSet(Trainset set, uint tick) } //reset this car's states - networkedTrainCar.TicksSinceSync = 0; + //networkedTrainCar.TicksSinceSync = 0; networkedTrainCar.BogieTracksDirty = false; } diff --git a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs index b59850ca..b1c2a0cd 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs @@ -75,6 +75,8 @@ public static bool TryGetFromTrainCar(TrainCar trainCar, out NetworkedTrainCar n public string CurrentID { get; private set; } public TrainCar TrainCar; public uint TicksSinceSync = uint.MaxValue; + + public uint lastTickProcessed = 0; public bool HasPlayers => PlayerManager.Car == TrainCar || GetComponentInChildren() != null; private Bogie bogie1; @@ -1247,6 +1249,14 @@ public void Client_ReceiveTrainPhysicsUpdate(in TrainsetMovementPart movementPar if (TrainCar.isEligibleForSleep) TrainCar.ForceOptimizationState(false); + if (tick <= lastTickProcessed) + { + Multiplayer.LogWarning($"Received physics update for car {CurrentID} at tick {tick}, but last tick processed was {lastTickProcessed}"); + return; + } + + lastTickProcessed = tick; + if (movementPart.typeFlag == TrainsetMovementPart.MovementType.RigidBody) { //Vector3 expectedPosition = movementPart.RigidbodySnapshot.Position + WorldMover.currentMove; @@ -1285,7 +1295,6 @@ public void Client_ReceiveTrainPhysicsUpdate(in TrainsetMovementPart movementPar TrainCar.stress.slowBuildUpStress = movementPart.SlowBuildUpStress; client_bogie1Queue.ReceiveSnapshot(movementPart.Bogie1, tick); client_bogie2Queue.ReceiveSnapshot(movementPart.Bogie2, tick); - } bool kinematic = movementPart.Speed < NetworkTrainsetWatcher.VELOCITY_THRESHOLD && (movementPart.RigidbodySnapshot != null && movementPart.RigidbodySnapshot.Velocity.magnitude < NetworkTrainsetWatcher.VELOCITY_THRESHOLD); From e261bf95ef987520b60a953379c85fa001999b5b Mon Sep 17 00:00:00 2001 From: AMacro Date: Tue, 28 Jan 2025 21:44:40 +1000 Subject: [PATCH 294/521] sync full health states sync full health states --- .../Networking/Train/NetworkedCarSpawner.cs | 13 +++ .../Networking/Train/NetworkedTrainCar.cs | 20 ++--- .../Data/Train/TrainCarHealthData.cs | 86 +++++++++++++++++++ .../Data/Train/TrainsetSpawnPart.cs | 19 +++- .../Managers/Client/NetworkClient.cs | 10 +-- .../Networking/Managers/NetworkManager.cs | 1 + .../Managers/Server/NetworkServer.cs | 2 +- .../Train/ClientboundCarHealthUpdatePacket.cs | 4 +- 8 files changed, 131 insertions(+), 24 deletions(-) create mode 100644 Multiplayer/Networking/Data/Train/TrainCarHealthData.cs diff --git a/Multiplayer/Components/Networking/Train/NetworkedCarSpawner.cs b/Multiplayer/Components/Networking/Train/NetworkedCarSpawner.cs index cd6b3366..19eb38bf 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedCarSpawner.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedCarSpawner.cs @@ -1,4 +1,5 @@ using System.Collections; +using DV.Damage; using DV.LocoRestoration; using DV.Simulation.Brake; using DV.ThingTypes; @@ -60,6 +61,18 @@ public static NetworkedTrainCar SpawnCar(TrainsetSpawnPart spawnPart, bool preve trainCar.uniqueCar = false; trainCar.InitializeExistingLogicCar(spawnPart.CarId, spawnPart.CarGuid); + //set health data + if (spawnPart.Exploded) + { + var explosionBase = trainCar.GetComponent(); + if (explosionBase != null) + explosionBase.UpdateToExplodedStateExternal(); + else + TrainCarExplosion.UpdateModelToExploded(trainCar); + } + + spawnPart.CarHealthData.LoadTo(trainCar); + //Restoration vehicle hack //todo: make it work properly if (spawnPart.IsRestorationLoco) diff --git a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs index b1c2a0cd..b4e33222 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs @@ -101,7 +101,7 @@ public static bool TryGetFromTrainCar(TrainCar trainCar, out NetworkedTrainCar n private bool cargoHealthDirty; private bool cargoIsLoading; public byte CargoModelIndex = byte.MaxValue; - private bool healthDirty; + private bool carHealthDirty; private bool sendCouplers; private bool sendCables; private bool fireboxDirty; @@ -242,8 +242,8 @@ public void OnDisable() NetworkLifecycle.Instance.OnTick -= Common_OnTick; NetworkLifecycle.Instance.OnTick -= Server_OnTick; - //if (UnloadWatcher.isUnloading) - // return; + + NetworkLifecycle.Instance.Server.PlayerDisconnect -= Server_OnPlayerDisconnect; trainCarsToNetworkedTrainCars.Remove(TrainCar); @@ -331,7 +331,7 @@ private IEnumerator Server_WaitForLogicCar() TrainCar.logicCar.CargoUnloaded += Server_OnCargoUnloaded; if (TrainCar.CargoDamage) - TrainCar.CargoDamage.CargoEffectiveHealthStateUpdate += Server_OnHealthUpdate; + TrainCar.CargoDamage.CargoEffectiveHealthStateUpdate += Server_CargoHealthUpdate; Server_DirtyAllState(); } @@ -343,7 +343,7 @@ public void Server_DirtyAllState() cargoStateDirty = true; cargoHealthDirty = true; cargoIsLoading = true; - healthDirty = true; + carHealthDirty = true; BogieTracksDirty = true; sendCouplers = true; sendCables = true; @@ -416,14 +416,14 @@ private void Server_OnCargoUnloaded() CargoModelIndex = byte.MaxValue; } - private void Server_OnHealthUpdate(float health) + private void Server_CargoHealthUpdate(float health) { cargoHealthDirty = true; } private void Server_CarHealthUpdate(float health) { - healthDirty = true; + carHealthDirty = true; } private void Server_MainResUpdate(float normalizedPressure, float pressure) @@ -557,10 +557,10 @@ private void Server_SendHealthUpdate() private void Server_SendHealthState() { - if (!healthDirty) + if (!carHealthDirty) return; - healthDirty = false; - NetworkLifecycle.Instance.Server.SendCarHealthUpdate(NetId, TrainCar.CarDamage.currentHealth); + carHealthDirty = false; + NetworkLifecycle.Instance.Server.SendCarHealthUpdate(NetId, TrainCarHealthData.From(TrainCar)); } public bool Server_ValidateCouplerInteraction(CommonCouplerInteractionPacket packet, ITransportPeer peer) diff --git a/Multiplayer/Networking/Data/Train/TrainCarHealthData.cs b/Multiplayer/Networking/Data/Train/TrainCarHealthData.cs new file mode 100644 index 00000000..da4aeebc --- /dev/null +++ b/Multiplayer/Networking/Data/Train/TrainCarHealthData.cs @@ -0,0 +1,86 @@ +using DV.Damage; +using LiteNetLib.Utils; +using System; + +namespace Multiplayer.Networking.Data.Train; + +public readonly struct TrainCarHealthData +{ + public readonly float BodyHP; + public readonly float WheelsHP; + public readonly float MechanicalPT; + public readonly float ElectricalPT; + public readonly bool WindowsBroken; + + private TrainCarHealthData(float bodyHP, float wheelsHP, float mechanicalPT, float electricalPT, bool windowsBroken) + { + BodyHP = bodyHP; + WheelsHP = wheelsHP; + MechanicalPT = mechanicalPT; + ElectricalPT = electricalPT; + WindowsBroken = windowsBroken; + } + public void LoadTo(TrainCar trainCar) + { + var dmgCtrl = trainCar.GetComponent(); + if (dmgCtrl != null) + { + dmgCtrl.bodyDamage.LoadCarDamageState(BodyHP); + dmgCtrl.wheels?.SetCurrentHealthPercentage(WheelsHP); + dmgCtrl.mechanicalPT?.SetCurrentHealthPercentage(MechanicalPT); + dmgCtrl.electricalPT?.SetCurrentHealthPercentage(ElectricalPT); + + if (dmgCtrl.windows != null) + dmgCtrl.windows.windowsBroken = WindowsBroken; + } + else + { + //freight cars don't have damage controller, so we need to check if they have a damage model + var dmgModel = trainCar.GetComponent(); + dmgModel?.SetHealth(BodyHP); + } + } + + public static TrainCarHealthData From(TrainCar trainCar) + { + var dmgCtrl = trainCar.GetComponent(); + + if (dmgCtrl == null) + { + //freight cars don't have damage controller, so we need to check if they have a damage model + var dmgModel = trainCar.GetComponent(); + if (dmgModel != null) + return new TrainCarHealthData(dmgModel.currentHealth, 0,0,0,false); + else + return new TrainCarHealthData(); + } + + float bodyHP = dmgCtrl?.bodyDamage?.HealthPercentage ?? 0; + float wheelsHP = dmgCtrl?.wheels?.HealthPercentage ?? 0; + float mechanicalPT = dmgCtrl?.mechanicalPT?.HealthPercentage ?? 0; + float electricalPT = dmgCtrl?.electricalPT?.HealthPercentage ?? 0; + bool brokenWindows = dmgCtrl?.windows?.windowsBroken ?? true; + + return new TrainCarHealthData(bodyHP, wheelsHP, mechanicalPT, electricalPT, brokenWindows); + } + + public static void Serialize(NetDataWriter writer, TrainCarHealthData data) + { + writer.Put(data.BodyHP); + writer.Put(data.WheelsHP); + writer.Put(data.MechanicalPT); + writer.Put(data.ElectricalPT); + writer.Put(data.WindowsBroken); + } + + public static TrainCarHealthData Deserialize(NetDataReader reader) + { + float bodyHP = reader.GetFloat(); + float wheelsHP = reader.GetFloat(); + float mechanicalPT = reader.GetFloat(); + float electricalPT = reader.GetFloat(); + bool brokenWindows = reader.GetBool(); + + return new TrainCarHealthData(bodyHP, wheelsHP, mechanicalPT, electricalPT, brokenWindows); + } +} diff --git a/Multiplayer/Networking/Data/Train/TrainsetSpawnPart.cs b/Multiplayer/Networking/Data/Train/TrainsetSpawnPart.cs index c1e44796..f9d8c9e2 100644 --- a/Multiplayer/Networking/Data/Train/TrainsetSpawnPart.cs +++ b/Multiplayer/Networking/Data/Train/TrainsetSpawnPart.cs @@ -21,6 +21,8 @@ public readonly struct TrainsetSpawnPart public readonly string LiveryId; public readonly string CarId; public readonly string CarGuid; + public readonly bool Exploded; + public readonly TrainCarHealthData CarHealthData; // Customisation details public readonly bool PlayerSpawnedCar; @@ -46,7 +48,8 @@ public readonly struct TrainsetSpawnPart public readonly BrakeSystemData BrakeData; public TrainsetSpawnPart( - ushort netId, string liveryId, string carId, string carGuid, bool playerSpawnedCar, bool isRestoration, LocoRestorationController.RestorationState restorationState, PaintTheme paintExterior, PaintTheme paintInterior, + ushort netId, string liveryId, string carId, string carGuid, bool exploded, TrainCarHealthData carHealthData, + 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,6 +58,8 @@ public TrainsetSpawnPart( LiveryId = liveryId; CarId = carId; CarGuid = carGuid; + Exploded = exploded; + CarHealthData = carHealthData; PlayerSpawnedCar = playerSpawnedCar; IsRestorationLoco = isRestoration; @@ -88,6 +93,9 @@ public static void Serialize(NetDataWriter writer, TrainsetSpawnPart data) writer.PutBytesWithLength(EMPTY_GUID); } + writer.Put(data.Exploded); + TrainCarHealthData.Serialize(writer, data.CarHealthData); + writer.Put(data.PlayerSpawnedCar); writer.Put(data.IsRestorationLoco); @@ -116,8 +124,10 @@ public static TrainsetSpawnPart Deserialize(NetDataReader reader) string liveryId = reader.GetString(); string carId = reader.GetString(); string carGuid = new Guid(reader.GetBytesWithLength()).ToString(); - bool playerSpawnedCar = reader.GetBool(); + bool exploded = reader.GetBool(); + TrainCarHealthData healthData = TrainCarHealthData.Deserialize(reader); + bool playerSpawnedCar = reader.GetBool(); bool isRestoration = reader.GetBool(); LocoRestorationController.RestorationState restorationState = default; if (isRestoration) @@ -142,7 +152,8 @@ public static TrainsetSpawnPart Deserialize(NetDataReader reader) var brakeSet = BrakeSystemData.Deserialize(reader); return new TrainsetSpawnPart( - netId, liveryId, carId, carGuid, playerSpawnedCar, isRestoration, restorationState, exteriorPaint, interiorPaint, + netId, liveryId, carId, carGuid, exploded, healthData, + playerSpawnedCar, isRestoration, restorationState, exteriorPaint, interiorPaint, frontCoupling, rearCoupling, speed, position, rotation, bogie1, bogie2, brakeSet); @@ -162,6 +173,8 @@ public static TrainsetSpawnPart FromTrainCar(NetworkedTrainCar networkedTrainCar trainCar.carLivery.id, trainCar.ID, trainCar.CarGUID, + trainCar.isExploded, + TrainCarHealthData.From(trainCar), trainCar.playerSpawnedCar, restorationController != null, diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 5bbd2a5b..6fdd371c 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -814,15 +814,7 @@ private void OnClientboundCarHealthUpdatePacket(ClientboundCarHealthUpdatePacket if (!NetworkedTrainCar.GetTrainCar(packet.NetId, out TrainCar trainCar)) return; - CarDamageModel carDamage = trainCar.CarDamage; - float difference = Mathf.Abs(packet.Health - carDamage.currentHealth); - if (difference < 0.0001) - return; - - if (packet.Health < carDamage.currentHealth) - carDamage.DamageCar(difference); - else - carDamage.RepairCar(difference); + packet.Health.LoadTo(trainCar); } private void OnClientboundRerailTrainPacket(ClientboundRerailTrainPacket packet) diff --git a/Multiplayer/Networking/Managers/NetworkManager.cs b/Multiplayer/Networking/Managers/NetworkManager.cs index 142fc5c1..68d06194 100644 --- a/Multiplayer/Networking/Managers/NetworkManager.cs +++ b/Multiplayer/Networking/Managers/NetworkManager.cs @@ -58,6 +58,7 @@ private void RegisterNestedTypes() netPacketProcessor.RegisterNestedType(StationsChainNetworkData.Serialize, StationsChainNetworkData.Deserialize); netPacketProcessor.RegisterNestedType(TrainsetMovementPart.Serialize, TrainsetMovementPart.Deserialize); netPacketProcessor.RegisterNestedType(TrainsetSpawnPart.Serialize, TrainsetSpawnPart.Deserialize); + netPacketProcessor.RegisterNestedType(TrainCarHealthData.Serialize, TrainCarHealthData.Deserialize); netPacketProcessor.RegisterNestedType(Vector2Serializer.Serialize, Vector2Serializer.Deserialize); netPacketProcessor.RegisterNestedType(Vector3Serializer.Serialize, Vector3Serializer.Deserialize); } diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index e58c501e..d2c65c85 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -425,7 +425,7 @@ public void SendCargoHealthUpdate(ushort netId, float currentHealth) }, DeliveryMethod.ReliableOrdered, SelfPeer); } - public void SendCarHealthUpdate(ushort netId, float health) + public void SendCarHealthUpdate(ushort netId, TrainCarHealthData health) { SendPacketToAll(new ClientboundCarHealthUpdatePacket { diff --git a/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundCarHealthUpdatePacket.cs b/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundCarHealthUpdatePacket.cs index dd6846d6..1bcfdf70 100644 --- a/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundCarHealthUpdatePacket.cs +++ b/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundCarHealthUpdatePacket.cs @@ -1,7 +1,9 @@ +using Multiplayer.Networking.Data.Train; + namespace Multiplayer.Networking.Packets.Clientbound.Train; public class ClientboundCarHealthUpdatePacket { public ushort NetId { get; set; } - public float Health { get; set; } + public TrainCarHealthData Health { get; set; } } From 8e1ca897801bb67d0193cfc921d710a38197d29c Mon Sep 17 00:00:00 2001 From: AMacro Date: Mon, 21 Apr 2025 22:14:31 +1000 Subject: [PATCH 295/521] B99.4 Fixes Ready for Release --- Multiplayer/Multiplayer.csproj | 2 +- info.json | 3 ++- releases.json | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Multiplayer/Multiplayer.csproj b/Multiplayer/Multiplayer.csproj index 6171c32e..cfefd05d 100644 --- a/Multiplayer/Multiplayer.csproj +++ b/Multiplayer/Multiplayer.csproj @@ -3,7 +3,7 @@ net48 latest Multiplayer - 0.1.10.7 + 0.1.11.0 diff --git a/info.json b/info.json index 54e68cf7..54a43454 100644 --- a/info.json +++ b/info.json @@ -1,6 +1,6 @@ { "Id": "Multiplayer", - "Version": "0.1.10.7", + "Version": "0.1.11.0", "DisplayName": "Multiplayer", "Author": "Insprill, Macka, Morm", "EntryMethod": "Multiplayer.Multiplayer.Load", @@ -8,5 +8,6 @@ "LoadAfter": [ "RemoteDispatch" ], + "HomePage": "https://www.nexusmods.com/derailvalley/mods/1070", "Repository": "https://raw.githubusercontent.com/AMacro/dv-multiplayer/refs/heads/beta/releases.json" } diff --git a/releases.json b/releases.json index d48beaa5..303e465e 100644 --- a/releases.json +++ b/releases.json @@ -1,6 +1,6 @@ { "Releases": [ - {"Id": "Multiplayer", "Version": "0.1.10.7", "DownloadUrl": "https://github.com/AMacro/dv-multiplayer/releases/download/v0.1.10.7-Beta/Multiplayer.0.1.10.7.zip"} + {"Id": "Multiplayer", "Version": "0.1.11.0", "DownloadUrl": "https://github.com/AMacro/dv-multiplayer/releases/download/v0.1.11.0-Beta/Multiplayer.0.1.11.0.zip"} ] } \ No newline at end of file From ae2e91fae002b118dd86e530ac830626565d15c8 Mon Sep 17 00:00:00 2001 From: AMacro Date: Tue, 22 Apr 2025 09:38:13 +1000 Subject: [PATCH 296/521] Code and logging cleanup --- Multiplayer/Components/MainMenu/HostGamePane.cs | 1 - .../Components/Networking/Train/NetworkTrainsetWatcher.cs | 2 +- Multiplayer/Networking/Data/JobData.cs | 2 +- Multiplayer/Networking/Data/TaskNetworkData.cs | 4 ++-- Multiplayer/Networking/Managers/Client/NetworkClient.cs | 6 +++--- 5 files changed, 7 insertions(+), 8 deletions(-) diff --git a/Multiplayer/Components/MainMenu/HostGamePane.cs b/Multiplayer/Components/MainMenu/HostGamePane.cs index 5809b029..3f69ac76 100644 --- a/Multiplayer/Components/MainMenu/HostGamePane.cs +++ b/Multiplayer/Components/MainMenu/HostGamePane.cs @@ -39,7 +39,6 @@ public class HostGamePane : MonoBehaviour TextMeshProUGUI serverDetails; SliderDV maxPlayers; - Toggle gamePublic; Selector gameVisibility; ButtonDV startButton; diff --git a/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs b/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs index 23b35ddc..20ce7407 100644 --- a/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs +++ b/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs @@ -177,7 +177,7 @@ public void Client_HandleTrainsetPhysicsUpdate(ClientboundTrainsetPhysicsPacket { if (NetworkedTrainCar.Get(packet.TrainsetParts[i].NetId ,out NetworkedTrainCar networkedTrainCar)) { - Multiplayer.LogDebug(()=>$"Applying TrainPhysicsUpdate to {packet.TrainsetParts[i].NetId}"); + //Multiplayer.LogDebug(()=>$"Applying TrainPhysicsUpdate to {packet.TrainsetParts[i].NetId}"); networkedTrainCar.Client_ReceiveTrainPhysicsUpdate(in packet.TrainsetParts[i], packet.Tick); } else diff --git a/Multiplayer/Networking/Data/JobData.cs b/Multiplayer/Networking/Data/JobData.cs index 1a5b676a..2a6cd4d3 100644 --- a/Multiplayer/Networking/Data/JobData.cs +++ b/Multiplayer/Networking/Data/JobData.cs @@ -34,7 +34,7 @@ public static JobData FromJob(NetworkedStationController netStation, NetworkedJo ushort itemNetId = 0; ItemPositionData itemPos = new(); - //Multiplayer.Log($"JobData.FromJob({netStation.name}, {job.ID}, {networkedJob.Job.State})"); + //Multiplayer.Log($"JobData.FromJob({netStation.name}, {job.ID}, {networkedJob.Job.Value})"); if (networkedJob.Job.State == JobState.Available) { diff --git a/Multiplayer/Networking/Data/TaskNetworkData.cs b/Multiplayer/Networking/Data/TaskNetworkData.cs index 96debcd3..4a843394 100644 --- a/Multiplayer/Networking/Data/TaskNetworkData.cs +++ b/Multiplayer/Networking/Data/TaskNetworkData.cs @@ -29,7 +29,7 @@ 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() Value {(byte)Value}, {Value}"); writer.Put((byte)State); //Multiplayer.Log($"TaskNetworkData.SerializeCommon() TaskStartTime {TaskStartTime}"); writer.Put(TaskStartTime); @@ -46,7 +46,7 @@ protected void SerializeCommon(NetDataWriter writer) protected void DeserializeCommon(NetDataReader reader) { State = (TaskState)reader.GetByte(); - //Multiplayer.Log($"TaskNetworkData.DeserializeCommon() State {State}"); + //Multiplayer.Log($"TaskNetworkData.DeserializeCommon() Value {Value}"); TaskStartTime = reader.GetFloat(); //Multiplayer.Log($"TaskNetworkData.DeserializeCommon() TaskStartTime {TaskStartTime}"); TaskFinishTime = reader.GetFloat(); diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 45071e8e..d0307400 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -625,7 +625,7 @@ private void OnCommonTrainUncouplePacket(CommonTrainUncouplePacket packet) return; } - LogDebug(() => $"OnCommonTrainUncouplePacket() netId: {packet.NetId}, trainCar: {trainCar.ID}, isFront: {packet.IsFrontCoupler}, playAudio: {packet.PlayAudio}, DueToBrokenCouple: {packet.DueToBrokenCouple}, viaChainInteraction: {packet.ViaChainInteraction}"); + //LogDebug(() => $"OnCommonTrainUncouplePacket() netId: {packet.NetId}, trainCar: {trainCar.ID}, isFront: {packet.IsFrontCoupler}, playAudio: {packet.PlayAudio}, DueToBrokenCouple: {packet.DueToBrokenCouple}, viaChainInteraction: {packet.ViaChainInteraction}"); Coupler coupler = packet.IsFrontCoupler ? trainCar.frontCoupler : trainCar.rearCoupler; coupler.Uncouple(packet.PlayAudio, false, packet.DueToBrokenCouple, false/*B99 packet.ViaChainInteraction*/); @@ -684,7 +684,7 @@ private void OnCommonHoseDisconnectedPacket(CommonHoseDisconnectedPacket packet) TrainCar trainCar = netTrainCar.TrainCar; - LogDebug(() => $"OnCommonHoseDisconnectedPacket() netId: {packet.NetId}, trainCar: {trainCar.ID}, isFront: {packet.IsFront}, playAudio: {packet.PlayAudio}"); + //LogDebug(() => $"OnCommonHoseDisconnectedPacket() netId: {packet.NetId}, trainCar: {trainCar.ID}, isFront: {packet.IsFront}, playAudio: {packet.PlayAudio}"); Coupler coupler = packet.IsFront ? trainCar.frontCoupler : trainCar.rearCoupler; @@ -855,7 +855,7 @@ private void OnClientboundCargoHealthUpdatePacket(ClientboundCargoHealthUpdatePa float deltaHealth = cargoDamageModel.currentHealth - packet.CargoHealth; - LogDebug(() => $"OnClientboundCargoHealthUpdatePacket() {networkedTrainCar.CurrentID}, current health: {cargoDamageModel.currentHealth}, new health: {packet.CargoHealth}, delta: {cargoDamageModel}, applySensitivity: {packet.CargoHealth > 0}"); + //LogDebug(() => $"OnClientboundCargoHealthUpdatePacket() {networkedTrainCar.CurrentID}, current health: {cargoDamageModel.currentHealth}, new health: {packet.CargoHealth}, delta: {cargoDamageModel}, applySensitivity: {packet.CargoHealth > 0}"); if (deltaHealth > 0) cargoDamageModel.ApplyDamageToCargo(deltaHealth, packet.CargoHealth > 0); From 14d8f2cf53eb5dcb724ca13fe0473ccecab51058 Mon Sep 17 00:00:00 2001 From: AMacro Date: Fri, 25 Apr 2025 14:45:54 +1000 Subject: [PATCH 297/521] Create new GridView control --- .../Components/UI/Controls/MPGridView.cs | 447 ++++++++++++++++++ .../Components/UI/Controls/MPViewElement.cs | 61 +++ 2 files changed, 508 insertions(+) create mode 100644 Multiplayer/Components/UI/Controls/MPGridView.cs create mode 100644 Multiplayer/Components/UI/Controls/MPViewElement.cs diff --git a/Multiplayer/Components/UI/Controls/MPGridView.cs b/Multiplayer/Components/UI/Controls/MPGridView.cs new file mode 100644 index 00000000..5c8b72c4 --- /dev/null +++ b/Multiplayer/Components/UI/Controls/MPGridView.cs @@ -0,0 +1,447 @@ +using DV.UIFramework; +using Multiplayer.Components.MainMenu; +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; +using UnityEngine.UI; + +namespace Multiplayer.Components.UI.Controls; + +public class MPGridView : AUIView +{ + public delegate void IndexChangeDelegate(MPGridView sender); + public event IndexChangeDelegate SelectedIndexChanged; + public event IndexChangeDelegate HoveredIndexChanged; + + // Core properties + public GameObject viewElementPrefab; + public GameObject placeholderElementPrefab; + public bool showPlaceholderWhenEmpty = true; + public bool allowHoveringAndSelecting = true; + + // Internal state + private readonly List _items = []; + private MPViewElement _selectedItem; + private MPViewElement _hoveredItem; + private bool _placeholderVisible = false; + private bool previousInteractability; + + // Components + private ScrollRect _scrollBar; + + + // Gridview properties + public IReadOnlyList Items => _items.AsReadOnly(); + public int SelectedIndex { get; private set; } + public int HoveredIndex { get; private set; } + + public T SelectedItem + { + get + { + return (SelectedIndex >= 0 && SelectedIndex < _items.Count) ? + _items[SelectedIndex] + : default; + } + } + + // Item access methods + public T this[int index] + { + get + { + if (index < 0 || index >= _items.Count) + return default; + return _items[index]; + } + } + + public bool Contains(T item) => _items.Contains(item); + public int IndexOf(T item) => _items.IndexOf(item); + public int FindIndex(Predicate match) => _items.FindIndex(match); + public T Find(Predicate match) => _items.Find(match); + + public void Clear() + { + // Clear selection + _selectedItem = null; + _hoveredItem = null; + SelectedIndex = -1; + HoveredIndex = -1; + + // Remove all child elements + for (int i = transform.childCount - 1; i >= 0; i--) + { + Destroy(transform.GetChild(i).gameObject); + } + + // Clear items list + _items.Clear(); + _placeholderVisible = false; + + // Show placeholder if needed + UpdatePlaceholder(); + + // Notify selection changed + SelectedIndexChanged?.Invoke(this); + } + + // Add a single item + public void AddItem(T item) + { + _items.Add(item); + CreateViewElement(item); + UpdatePlaceholder(); + } + + // Add multiple items + public void AddItems(IEnumerable items) + { + if (items == null) + return; + + foreach (var item in items) + { + _items.Add(item); + CreateViewElement(item); + } + + UpdatePlaceholder(); + } + + // Remove an item + public void RemoveItem(T item) + { + int index = _items.IndexOf(item); + if (index >= 0) + { + RemoveItemAt(index); + } + } + + // Remove an item at a specific index + public void RemoveItemAt(int index) + { + if (index < 0 || index >= _items.Count || _placeholderVisible) + return; + + // Check if we're removing the selected item + if (_selectedItem != null && _selectedItem.transform.GetSiblingIndex() == index) + { + _selectedItem = null; + SelectedIndexChanged?.Invoke(this); + } + + // Remove the view element + if (index < transform.childCount) + { + + Destroy(transform.GetChild(index).gameObject); + } + + // Remove from items list + _items.RemoveAt(index); + + // Update placeholder + UpdatePlaceholder(); + } + + // Sort items using a comparison function + public void SortItems(Comparison comparison) + { + if (_items.Count <= 1) + return; + + // Remember selected item + T selectedItem = SelectedItem; + + // Sort the items list + _items.Sort(comparison); + + // Rebuild view elements + for (int i = transform.childCount - 1; i >= 0; i--) + { + // Skip placeholder if it exists + if (_placeholderVisible && i == 0) + continue; + + Destroy(transform.GetChild(i).gameObject); + } + + // Recreate view elements in sorted order + foreach (var item in _items) + { + CreateViewElement(item); + } + + // Restore selection if possible + if (selectedItem != null) + { + int newIndex = _items.IndexOf(selectedItem); + if (newIndex >= 0) + { + SetSelected(newIndex, true); + } + } + } + + // Set selected item by index + public void SetSelected(int index, bool scrollToItem = true) + { + Multiplayer.LogDebug(() => $"MPGridView.SetSelected({index}, {scrollToItem}) child count: {transform.childCount}"); + + if (index < 0 || index >= _items.Count || _placeholderVisible) + return; + + Multiplayer.LogDebug(() => $"MPGridView.SetSelected({index}, {scrollToItem}) items count: {index}"); + + if (index >= transform.childCount) + return; + + Transform child = transform.GetChild(index); + MPViewElement element = child.GetComponent>(); + + Multiplayer.LogDebug(() => + { + var el = element as IServerBrowserGameDetails; + return $"MPGridView.SetSelected({index}, {scrollToItem}) {element?.name}"; + + }); + + if (element != null) + { + UpdateSelectedItem(element); + } + + if (scrollToItem) + ScrollToItem(); + } + + private void ScrollToItem() + { + if (_scrollBar != null && _items.Count > 0 && SelectedIndex >= 0) + { + if (_scrollBar.content != null && _scrollBar.viewport != null && + _selectedItem && _selectedItem.TryGetComponent(out var itemRect) + ) + { + // Get the content RectTransform + RectTransform contentRect = _scrollBar.content; + + // Calculate the normalized position based on the item's position within the content + float itemPosition = itemRect.anchoredPosition.y; + float contentHeight = contentRect.rect.height; + + if (contentHeight == 0) + return; + + // Adjust for the viewport height to center the item + float viewportHeight = _scrollBar.viewport.rect.height; + float adjustment = viewportHeight * 0.5f / contentHeight; + + // Set the normalized position (clamped between 0 and 1) + float normalizedPos = Mathf.Clamp01(itemPosition / contentHeight + adjustment); + _scrollBar.verticalNormalizedPosition = 1f - normalizedPos; + } + else if(_items.Count != 0) + { + _scrollBar.verticalNormalizedPosition = 1f - (float)SelectedIndex / (float)_items.Count; + } + } + } + + // Get view element at index + public MPViewElement GetElementAt(int index) + { + if (index < 0 || index >= _items.Count || _placeholderVisible) + return null; + + if (index < transform.childCount) + { + return transform.GetChild(index).GetComponent>(); + } + + return null; + } + + // Create a view element for an item + private GameObject CreateViewElement(T item) + { + if (viewElementPrefab == null) + return null; + + GameObject element = Instantiate(viewElementPrefab, transform); + MPViewElement viewElement = element.GetComponent>(); + + viewElement.SetData(item); + viewElement.SetInteractable(allowHoveringAndSelecting); + viewElement.SelectionRequested += UpdateSelectedItem; + viewElement.HoverChanged += UpdateHoverState; + + return element; + } + + // Create placeholder element + private GameObject CreatePlaceholderElement() + { + if (placeholderElementPrefab == null) + return null; + + GameObject element = Instantiate(placeholderElementPrefab, transform); + element.transform.SetAsFirstSibling(); + + MPViewElement viewElement = element.GetComponent>(); + viewElement.SetInteractable(false); + + return element; + } + + // Update placeholder visibility + private void UpdatePlaceholder() + { + bool shouldShowPlaceholder = _items.Count == 0 && showPlaceholderWhenEmpty; + + // If placeholder state hasn't changed, do nothing + if (_placeholderVisible == shouldShowPlaceholder) + return; + + _placeholderVisible = shouldShowPlaceholder; + + // Remove existing placeholder if it exists + if (!shouldShowPlaceholder && transform.childCount > 0) + { + // Check for any placeholder + MPViewElement[] placeholders = transform.GetComponentsInChildren>().Where(e => e.IsPlaceholder).ToArray(); + + for (int i = 0; i < placeholders.Length; i++) + Destroy(placeholders[i].gameObject); + } + + // Add placeholder if needed + if (shouldShowPlaceholder) + { + CreatePlaceholderElement(); + } + } + + // Handle selection changes + private void UpdateSelectedItem(MPViewElement element) + { + _selectedItem?.SetSelected(false); + + if (_placeholderVisible) + { + _selectedItem = null; + SelectedIndex = -1; + HoveredIndex = -1; + SelectedIndexChanged?.Invoke(this); + return; + } + + _selectedItem = element; + _selectedItem.SetSelected(true); + + SelectedIndex = element.transform.GetSiblingIndex(); + + SelectedIndexChanged?.Invoke(this); + } + + // Handle hover state changes + private void UpdateHoverState(MPViewElement element, bool hovered) + { + _hoveredItem = hovered ? element : null; + + if (_hoveredItem != null) + HoveredIndex = element.transform.GetSiblingIndex(); + else + HoveredIndex = -1; + + HoveredIndexChanged?.Invoke(this); + } + + // Update interactability of all elements + private void UpdateInteractability() + { + foreach (Transform child in transform) + { + MPViewElement element = child.GetComponent>(); + if (element != null) + { + element.SetInteractable(allowHoveringAndSelecting); + + if (!allowHoveringAndSelecting) + { + element.SetSelected(false); + } + } + } + } + + protected virtual void Awake() + { + ValidatePrefabs(); + + if (_scrollBar == null) + _scrollBar = GetComponentInParent(); + } + + protected virtual void OnValidate() + { + ValidatePrefabs(); + } + + private void ValidatePrefabs() + { + if (viewElementPrefab != null) + { + var viewElement = viewElementPrefab.GetComponent>(); + if (viewElement == null) + { + Multiplayer.LogError($"View element prefab must have an MPViewElement<{typeof(T).Name}> component"); + viewElementPrefab = null; + } + else if (viewElement.IsPlaceholder) + { + Multiplayer.LogError($"View element prefab must not be a placeholder"); + viewElementPrefab = null; + } + + if (viewElementPrefab.GetComponent() == null) + { + Multiplayer.LogError($"View element prefab must have an IMarkable component"); + viewElementPrefab = null; + } + } + + if (placeholderElementPrefab != null) + { + var placeholderElement = placeholderElementPrefab.GetComponent>(); + if (placeholderElement == null) + { + Multiplayer.LogError($"Placeholder element prefab must have an MPViewElement<{typeof(T).Name}> component"); + placeholderElementPrefab = null; + } + else if (placeholderElement.IsPlaceholder == false) + { + Multiplayer.LogError($"Placeholder element prefab must be a placeholder"); + placeholderElementPrefab = null; + } + + if (placeholderElementPrefab.GetComponent() == null) + { + Multiplayer.LogError($"Placeholder element prefab must have an IMarkable component"); + placeholderElementPrefab = null; + } + } + } + + protected void Update() + { + if (previousInteractability != allowHoveringAndSelecting) + { + previousInteractability = allowHoveringAndSelecting; + UpdateInteractability(); + } + } +} diff --git a/Multiplayer/Components/UI/Controls/MPViewElement.cs b/Multiplayer/Components/UI/Controls/MPViewElement.cs new file mode 100644 index 00000000..aba43458 --- /dev/null +++ b/Multiplayer/Components/UI/Controls/MPViewElement.cs @@ -0,0 +1,61 @@ +using DV.UIFramework; +using System; +using UnityEngine; +using UnityEngine.EventSystems; + +namespace Multiplayer.Components.UI.Controls; + +[RequireComponent(typeof(IMarkable))] +public abstract class MPViewElement : NullCheckingMonoBehaviour, ISelectHandler, IEventSystemHandler, IPointerEnterHandler, IPointerExitHandler +{ + public event Action> SelectionRequested; + public event Action, bool> HoverChanged; + + public abstract bool IsPlaceholder { get; } + + public virtual void SetSelected(bool selected) + { + //Multiplayer.LogDebug(() => + //{ + // var data = GetComponent(); + // return $"MPViewElement.SetSelected() {data?.name}"; + //}); + + if (TryGetComponent(out var component)) + { + component.ToggleMarked(selected); + } + } + + public virtual void SetInteractable(bool interactable) + { + if (TryGetComponent(out var component)) + { + component.ToggleInteractable(interactable); + } + } + + public virtual void OnSelect(BaseEventData eventData) + { + + //Multiplayer.LogDebug(()=> + //{ + // var data = GetComponent(); + // return $"MPViewElement.OnSelect() {data?.name}"; + //}); + + SelectionRequested?.Invoke(this); + } + + public virtual void OnPointerEnter(PointerEventData eventData) + { + HoverChanged?.Invoke(this, arg2: true); + } + + public virtual void OnPointerExit(PointerEventData eventData) + { + HoverChanged?.Invoke(this, arg2: false); + } + + public abstract void SetData(T data); +} From d05c610d734ab219bbcc7f2d397162484866e4af Mon Sep 17 00:00:00 2001 From: AMacro Date: Fri, 25 Apr 2025 14:46:50 +1000 Subject: [PATCH 298/521] Update elements for new GridView --- .../ServerBrowser/ServerBrowserElement.cs | 75 ++++++++++--------- .../ServerBrowser/ServerBrowserGridView.cs | 54 ++++++------- ....cs => ServerBrowserPlaceholderElement.cs} | 16 ++-- 3 files changed, 68 insertions(+), 77 deletions(-) rename Multiplayer/Components/MainMenu/ServerBrowser/{ServerBrowserDummyElement.cs => ServerBrowserPlaceholderElement.cs} (78%) diff --git a/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserElement.cs b/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserElement.cs index f925ec6c..18476b38 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserElement.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserElement.cs @@ -1,15 +1,17 @@ -using DV.UIFramework; + +using Multiplayer.Components.UI.Controls; using Multiplayer.Utils; -using System; using TMPro; using UnityEngine; using UnityEngine.UI; namespace Multiplayer.Components.MainMenu.ServerBrowser { - public class ServerBrowserElement : AViewElement + public class ServerBrowserElement : MPViewElement { - private TextMeshProUGUI networkName; + public override bool IsPlaceholder => false; + + private TextMeshProUGUI serverName; private TextMeshProUGUI playerCount; private TextMeshProUGUI ping; private GameObject goIconPassword; @@ -19,7 +21,7 @@ public class ServerBrowserElement : AViewElement 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 const int PING_PADDING_X = 10; private const string PING_COLOR_UNKNOWN = "#808080"; private const string PING_COLOR_EXCELLENT = "#00ff00"; @@ -35,26 +37,36 @@ public class ServerBrowserElement : AViewElement protected override void Awake() { // Find and assign TextMeshProUGUI components for displaying server details - networkName = this.FindChildByName("name [noloc]").GetComponent(); + serverName = this.FindChildByName("name [noloc]").GetComponent(); playerCount = this.FindChildByName("date [noloc]").GetComponent(); ping = this.FindChildByName("time [noloc]").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; - Vector2 nameSize = networkName.rectTransform.sizeDelta; - playerCount.transform.position = new Vector3(namePos.x + nameSize.x, namePos.y, namePos.z); + RectTransform nameRT = serverName.rectTransform; + + // Align player count + RectTransform playerCountRT = playerCount.rectTransform; + playerCountRT.anchorMin = new Vector2(0, 0.5f); + playerCountRT.anchorMax = new Vector2(0, 0.5f); + playerCountRT.pivot = new Vector2(0, 0.5f); - // Adjust the size and position of the ping text - Vector2 rowSize = transform.GetComponentInParent().sizeDelta; - Vector3 pingPos = ping.transform.position; - Vector2 pingSize = ping.rectTransform.sizeDelta; + float nameWidth = nameRT.sizeDelta.x; + playerCountRT.anchoredPosition = new Vector2(nameRT.position.x + nameWidth, nameRT.anchoredPosition.y); - ping.rectTransform.sizeDelta = new Vector2(PING_WIDTH, pingSize.y); - ping.transform.position = new Vector3(PING_POS_X, pingPos.y, pingPos.z); + // Align ping + RectTransform pingRT = ping.rectTransform; + pingRT.anchorMin = new Vector2(0, 0.5f); + pingRT.anchorMax = new Vector2(0, 0.5f); + pingRT.pivot = new Vector2(0, 0.5f); + + RectTransform parentRT = transform as RectTransform; + float pingX = parentRT.rect.width - PING_WIDTH - PING_PADDING_X; + pingRT.anchoredPosition = new Vector2(pingX, nameRT.anchoredPosition.y); + pingRT.sizeDelta = new Vector2(PING_WIDTH, pingRT.sizeDelta.y); ping.alignment = TextAlignmentOptions.Right; + // Set password icon iconPassword.sprite = Multiplayer.AssetIndex.lockIcon; @@ -69,7 +81,7 @@ protected override void Awake() goIconLAN.name = "LAN Icon"; Vector3 LANpos = goIconLAN.transform.localPosition; Vector3 LANSize = goIconLAN.GetComponent().sizeDelta; - LANpos.x += (PING_POS_X - LANpos.x - LANSize.x) / 2; + LANpos.x += (pingRT.position.x - LANpos.x - LANSize.x) / 2; goIconLAN.transform.localPosition = LANpos; iconLAN = goIconLAN.GetComponent(); iconLAN.sprite = Multiplayer.AssetIndex.lanIcon; @@ -77,7 +89,7 @@ protected override void Awake() } - public override void SetData(IServerBrowserGameDetails data, AGridView _) + public override void SetData(IServerBrowserGameDetails data) { // Clear existing data if (this.data != null) @@ -95,15 +107,13 @@ public override void SetData(IServerBrowserGameDetails data, AGridView $"UpdateView() serverName: {data.Name}, ping: {data.Ping}"); // Update the text fields with the data from the server - networkName.text = data.Name; + serverName.text = data.Name; playerCount.text = $"{data.CurrentPlayers} / {data.MaxPlayers}"; - //if (data.MultiplayerVersion == Multiplayer.Ver) - ping.text = $"{(data.Ping < 0 ? "?" : data.Ping)} ms"; - //else - // ping.text = $"N/A"; + ping.text = $"{(data.Ping < 0 ? "?" : data.Ping)} ms"; // Hide the icon if the server does not have a password goIconPassword.SetActive(data.HasPassword); @@ -114,19 +124,14 @@ public void UpdateView() private string GetColourForPing(int ping) { - switch (ping) + return ping switch { - 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 PING_COLOR_POOR; - } + PING_THRESHOLD_NONE => PING_COLOR_UNKNOWN, + < PING_THRESHOLD_EXCELLENT => PING_COLOR_EXCELLENT, + < PING_THRESHOLD_GOOD => PING_COLOR_GOOD, + < PING_THRESHOLD_HIGH => PING_COLOR_HIGH, + _ => PING_COLOR_POOR, + }; } } } diff --git a/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserGridView.cs b/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserGridView.cs index ea526945..ddbef4c9 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserGridView.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserGridView.cs @@ -1,44 +1,36 @@ -using System; using DV.UI; -using DV.UIFramework; -using Multiplayer.Components.MainMenu.ServerBrowser; +using Multiplayer.Components.UI.Controls; using UnityEngine; using UnityEngine.UI; -namespace Multiplayer.Components.MainMenu -{ - [RequireComponent(typeof(ContentSizeFitter))] - [RequireComponent(typeof(VerticalLayoutGroup))] - // - public class ServerBrowserGridView : AGridView - { - - protected override void Awake() - { - //Multiplayer.Log("serverBrowserGridview Awake()"); +namespace Multiplayer.Components.MainMenu.ServerBrowser; - //copy the copy - this.viewElementPrefab.SetActive(false); - this.dummyElementPrefab = Instantiate(this.viewElementPrefab); +[RequireComponent(typeof(ContentSizeFitter))] +[RequireComponent(typeof(VerticalLayoutGroup))] +public class ServerBrowserGridView : MPGridView +{ - //swap controllers - GameObject.Destroy(this.viewElementPrefab.GetComponent()); - GameObject.Destroy(this.dummyElementPrefab.GetComponent()); + protected override void Awake() + { + showPlaceholderWhenEmpty = true; - this.viewElementPrefab.AddComponent(); - this.dummyElementPrefab.AddComponent(); + //copy the copy + viewElementPrefab.SetActive(false); + placeholderElementPrefab = Instantiate(viewElementPrefab); - this.viewElementPrefab.name = "prefabServerBrowserElement"; - this.dummyElementPrefab.name = "prefabServerBrowserDummyElement"; + //swap controllers + Destroy(viewElementPrefab.GetComponent()); + GameObject.Destroy(placeholderElementPrefab.GetComponent()); - this.viewElementPrefab.SetActive(true); - this.dummyElementPrefab.SetActive(true); + viewElementPrefab.AddComponent(); + placeholderElementPrefab.AddComponent(); - } + viewElementPrefab.name = "prefabSBElement"; + placeholderElementPrefab.name = "prefabSBPlaceholderElement"; - public ServerBrowserElement GetElementAt(int index) - { - return transform.GetChild(index + indexOffset).GetComponent(); - } + viewElementPrefab.SetActive(true); + placeholderElementPrefab.SetActive(true); + + base.Awake(); } } diff --git a/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserDummyElement.cs b/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserPlaceholderElement.cs similarity index 78% rename from Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserDummyElement.cs rename to Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserPlaceholderElement.cs index a68c1c09..7e8e872f 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserDummyElement.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserPlaceholderElement.cs @@ -2,21 +2,20 @@ using DV.UIFramework; using DV.Localization; using Multiplayer.Utils; -using System.ComponentModel; -using TMPro; using UnityEngine; +using Multiplayer.Components.UI.Controls; namespace Multiplayer.Components.MainMenu.ServerBrowser { - public class ServerBrowserDummyElement : AViewElement + public class ServerBrowserPlaceholderElement : MPViewElement { - private TextMeshProUGUI networkName; + public override bool IsPlaceholder => true; protected override 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); @@ -41,12 +40,7 @@ protected override void Awake() } - public override void SetData(IServerBrowserGameDetails data, AGridView _) - { - //do nothing - } - - private void UpdateView(object sender = null, PropertyChangedEventArgs e = null) + public override void SetData(IServerBrowserGameDetails data) { //do nothing } From b80dbd062b83795b313721b199760a0f8bb7247a Mon Sep 17 00:00:00 2001 From: AMacro Date: Fri, 25 Apr 2025 14:50:36 +1000 Subject: [PATCH 299/521] Update ServerBrowser to use new GridView --- .../Components/MainMenu/ServerBrowserPane.cs | 139 ++++++------------ 1 file changed, 46 insertions(+), 93 deletions(-) diff --git a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs index ecb18363..24f46c30 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs @@ -1,24 +1,25 @@ -using System; -using System.Collections; +using DV; using DV.Localization; using DV.UI; using DV.UIFramework; -using DV.Util; using DV.Utils; +using LiteNetLib; +using Multiplayer.Components.MainMenu.ServerBrowser; using Multiplayer.Components.Networking; -using Multiplayer.Utils; -using TMPro; -using UnityEngine; -using UnityEngine.UI; -using System.Linq; +using Multiplayer.Components.UI.Controls; using Multiplayer.Networking.Data; -using DV; -using System.Net; -using LiteNetLib; -using System.Collections.Generic; +using Multiplayer.Utils; using Steamworks; using Steamworks.Data; +using System; +using System.Collections.Generic; +using System.Collections; +using System.Linq; +using System.Net; using System.Threading.Tasks; +using TMPro; +using UnityEngine; +using UnityEngine.UI; namespace Multiplayer.Components.MainMenu { @@ -44,13 +45,9 @@ private enum ConnectionState private const int MAX_PORT = 49151; //Gridview variables - private readonly ObservableCollectionExt gridViewModel = []; - private ServerBrowserGridView gridView; - private ScrollRect parentScroller; - private string serverIDOnRefresh; + private ServerBrowserGridView serverGridView; private IServerBrowserGameDetails selectedServer; - //ping tracking private float pingTimer = 0f; private const float PING_INTERVAL = 2f; // base interval to refresh all pings @@ -100,20 +97,11 @@ public void Awake() BuildUI(); SetupServerBrowser(); - RefreshGridView(); } public 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); @@ -148,7 +136,7 @@ public void Update() else if (remoteRefreshComplete) { RefreshGridView(); - IndexChanged(gridView); //Revalidate any selected servers + OnSelectedIndexChanged(serverGridView); //Revalidate any selected servers remoteRefreshComplete = false; serverRefreshing = false; timePassed = 0; @@ -341,11 +329,11 @@ private void SetupServerBrowser() //load our custom controller SaveLoadGridView slgv = GridviewGO.GetComponent(); - gridView = GridviewGO.AddComponent(); + serverGridView = GridviewGO.AddComponent(); //grab the original prefab slgv.viewElementPrefab.SetActive(false); - gridView.viewElementPrefab = Instantiate(slgv.viewElementPrefab); + serverGridView.viewElementPrefab = Instantiate(slgv.viewElementPrefab); slgv.viewElementPrefab.SetActive(true); //Remove original controller @@ -353,18 +341,16 @@ private void SetupServerBrowser() //Don't forget to re-enable! GridviewGO.SetActive(true); - - gridView.showDummyElement = true; } private void SetupListeners(bool on) { if (on) { - this.gridView.SelectedIndexChanged += this.IndexChanged; + serverGridView.SelectedIndexChanged += this.OnSelectedIndexChanged; } else { - this.gridView.SelectedIndexChanged -= this.IndexChanged; + serverGridView.SelectedIndexChanged -= this.OnSelectedIndexChanged; } } #endregion @@ -375,13 +361,10 @@ private void RefreshAction() if (serverRefreshing) return; - if (selectedServer != null) - serverIDOnRefresh = selectedServer.id; - remoteServers.Clear(); serverRefreshing = true; - buttonJoin.ToggleInteractable(false); + //buttonJoin.ToggleInteractable(false); buttonRefresh.ToggleInteractable(false); if (DVSteamworks.Success) @@ -428,15 +411,14 @@ private void DirectAction() ShowIpPopup(); } - private void IndexChanged(AGridView gridView) + private void OnSelectedIndexChanged(MPGridView gridView) { if (serverRefreshing) return; - if (gridView.SelectedModelIndex >= 0) + selectedServer = gridView.SelectedItem; + if (selectedServer != null) { - selectedServer = gridViewModel[gridView.SelectedModelIndex]; - UpdateDetailsPane(); //Check if we can connect to this server @@ -457,12 +439,13 @@ private void IndexChanged(AGridView gridView) private void UpdateElement(IServerBrowserGameDetails element) { - int index = gridViewModel.IndexOf(element); + int index = serverGridView.IndexOf(element); if (index >= 0) { - var viewElement = gridView.GetElementAt(index); + var viewElement = serverGridView.GetElementAt(index) as ServerBrowserElement; viewElement?.UpdateView(); + } } #endregion @@ -511,7 +494,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 + OnSelectedIndexChanged(serverGridView); //re-enable the join button if a valid gridview item is selected return; } @@ -598,9 +581,7 @@ private void ShowPortPopup() { ShowPasswordPopup(); } - }; - } private void ShowPasswordPopup() @@ -823,8 +804,8 @@ private void AttemptFail() if (gameObject != null && gameObject.activeInHierarchy) { - if (gridView != null) - IndexChanged(gridView); + if (serverGridView != null) + OnSelectedIndexChanged(serverGridView); if (buttonDirectIP != null && buttonDirectIP.gameObject != null) buttonDirectIP.ToggleInteractable(true); @@ -881,6 +862,8 @@ private string GetDisplayMessageForDisconnect(DisconnectReason reason) private async void ListActiveLobbies() { lobbies = await SteamMatchmaking.LobbyList.WithMaxResults(100) + .FilterDistanceWorldwide() + .WithSlotsAvailable(-1) //.WithKeyValue(SteamworksUtils.MP_MOD_KEY, string.Empty) .RequestAsync(); @@ -912,7 +895,7 @@ private async void ListActiveLobbies() private void UpdatePingsSteam() { - foreach (var server in gridViewModel) + foreach (var server in serverGridView.Items) { if (server is LobbyServerData lobbyServer) { @@ -944,28 +927,26 @@ private void UpdatePingsSteam() #endregion private void RefreshGridView() { - - var allServers = new List(); - allServers.AddRange(remoteServers); - // Get all active IDs - List activeIDs = allServers.Select(s => s.id).Distinct().ToList(); - - // Find servers to remove - List removeList = gridViewModel.Where(gv => !activeIDs.Contains(gv.id)).ToList(); + List activeIDs = remoteServers.Select(s => s.id).Distinct().ToList(); - // Remove expired servers - foreach (var remove in removeList) + // Remove servers that no longer exist + for (int i = serverGridView.Items.Count - 1; i >= 0; i--) { - gridViewModel.Remove(remove); + if (!activeIDs.Contains(serverGridView.Items[i].id)) + { + serverGridView.RemoveItemAt(i); + } } + Multiplayer.LogDebug(() => $"RefreshGridView() prepare to update/add, remoteServers count: {remoteServers.Count}"); // Update existing servers and add new ones - foreach (var server in allServers) + foreach (var server in remoteServers) { - var existingServer = gridViewModel.FirstOrDefault(gv => gv.id == server.id); + var existingServer = serverGridView.Items.FirstOrDefault(gv => gv.id == server.id); if (existingServer != null) { + Multiplayer.LogDebug(() => $"RefreshGridView() updating server"); // Update existing server existingServer.TimePassed = server.TimePassed; existingServer.CurrentPlayers = server.CurrentPlayers; @@ -974,41 +955,13 @@ private void RefreshGridView() } else { + Multiplayer.LogDebug(() => $"RefreshGridView() adding server"); // Add new server - gridViewModel.Add(server); - } - } - - if (gridViewModel.Count() == 0) - { - gridView.showDummyElement = true; - buttonJoin.ToggleInteractable(false); - } - else - { - gridView.showDummyElement = false; - } - - //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; - } + serverGridView.AddItem(server); } - serverIDOnRefresh = null; } } - + private string ExtractDomainName(string input) { if (input.StartsWith("http://")) From 86de090d110e9742d5d94847576ea2026a598a87 Mon Sep 17 00:00:00 2001 From: AMacro Date: Fri, 25 Apr 2025 14:51:00 +1000 Subject: [PATCH 300/521] Minor code cleanup for HostGamePane --- Multiplayer/Components/MainMenu/HostGamePane.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Multiplayer/Components/MainMenu/HostGamePane.cs b/Multiplayer/Components/MainMenu/HostGamePane.cs index 5809b029..f617de0a 100644 --- a/Multiplayer/Components/MainMenu/HostGamePane.cs +++ b/Multiplayer/Components/MainMenu/HostGamePane.cs @@ -39,7 +39,6 @@ public class HostGamePane : MonoBehaviour TextMeshProUGUI serverDetails; SliderDV maxPlayers; - Toggle gamePublic; Selector gameVisibility; ButtonDV startButton; @@ -385,7 +384,6 @@ private void SetupListeners(bool on) private void ValidateInputs(string text) { bool valid = true; - int portNum; if (!DVSteamworks.Success) @@ -396,7 +394,7 @@ private void ValidateInputs(string text) if (port.text != "") { - if (!int.TryParse(port.text, out portNum) || portNum < MIN_PORT || portNum > MAX_PORT) + if (!int.TryParse(port.text, out int portNum) || portNum < MIN_PORT || portNum > MAX_PORT) valid = false; } From 31e4799d24d7f749abb442d19e64c4f77a15f724 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 26 Apr 2025 10:03:52 +1000 Subject: [PATCH 301/521] B99.4 updates --- Multiplayer/Multiplayer.csproj | 1 + Multiplayer/Networking/Data/PitStopPlugData.cs | 3 ++- .../Networking/Data/Train/TrainCarHealthData.cs | 15 --------------- 3 files changed, 3 insertions(+), 16 deletions(-) diff --git a/Multiplayer/Multiplayer.csproj b/Multiplayer/Multiplayer.csproj index d9812511..c856307f 100644 --- a/Multiplayer/Multiplayer.csproj +++ b/Multiplayer/Multiplayer.csproj @@ -30,6 +30,7 @@ + diff --git a/Multiplayer/Networking/Data/PitStopPlugData.cs b/Multiplayer/Networking/Data/PitStopPlugData.cs index e0479265..3ab56afb 100644 --- a/Multiplayer/Networking/Data/PitStopPlugData.cs +++ b/Multiplayer/Networking/Data/PitStopPlugData.cs @@ -1,6 +1,7 @@ using LiteNetLib.Utils; using Multiplayer.Components.Networking.World; using Multiplayer.Networking.Serialization; +using Multiplayer.Utils; using UnityEngine; namespace Multiplayer.Networking.Data; @@ -24,7 +25,7 @@ public static PitStopPlugData From(NetworkedPluggableObject plugData) plugData.HeldBy?.Id ?? 0, plugData.TrainCarNetId, plugData.IsConnectedLeft, - plugData.transform.AbsolutePosition(), + plugData.transform.GetWorldAbsolutePosition(), plugData.transform.rotation ); } diff --git a/Multiplayer/Networking/Data/Train/TrainCarHealthData.cs b/Multiplayer/Networking/Data/Train/TrainCarHealthData.cs index 3362cb87..0b984cb9 100644 --- a/Multiplayer/Networking/Data/Train/TrainCarHealthData.cs +++ b/Multiplayer/Networking/Data/Train/TrainCarHealthData.cs @@ -35,21 +35,6 @@ public void LoadTo(TrainCar trainCar) } } - public static TrainCarHealthData From(TrainCar car) - { - var dmgCtrl = car.GetComponent(); - - if (dmgCtrl == null ) - return new TrainCarHealthData(); - - else - { - //freight cars don't have damage controller, so we need to check if they have a damage model - var dmgModel = trainCar.GetComponent(); - dmgModel?.SetHealth(BodyHP); - } - } - public static TrainCarHealthData From(TrainCar trainCar) { var dmgCtrl = trainCar.GetComponent(); From 6d3624e5d47859cdc85707270e3b1d97f1ad1d65 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 27 Apr 2025 09:53:31 +1000 Subject: [PATCH 302/521] Fix null reference issue for clients --- Multiplayer/Components/Networking/TickedQueue.cs | 13 ++++++++----- .../Networking/Train/NetworkedTrainCar.cs | 14 ++++++++------ .../Networking/Managers/Server/NetworkServer.cs | 12 ++++++------ Multiplayer/Patches/Train/CouplerPatch.cs | 12 ++++++------ 4 files changed, 28 insertions(+), 23 deletions(-) diff --git a/Multiplayer/Components/Networking/TickedQueue.cs b/Multiplayer/Components/Networking/TickedQueue.cs index 1283f868..ab6a492c 100644 --- a/Multiplayer/Components/Networking/TickedQueue.cs +++ b/Multiplayer/Components/Networking/TickedQueue.cs @@ -66,19 +66,22 @@ public void Clear() private string GetID() { - if (identifier != string.Empty) + if (!string .IsNullOrEmpty(identifier)) return identifier; - TrainCar car; + if (this.gameObject == null) + return "Bad GO"; + + TrainCar car = TrainCar.Resolve(this.gameObject); int bogie = 0; - if (car = TrainCar.Resolve(this.gameObject)) + if (car != null) if (this is NetworkedBogie netBogie) bogie = (car.Bogies[0] == netBogie.Bogie) ? 1 : 2; - if (car.logicCar != null) + if (car?.logicCar != null) identifier = $"{car?.ID ?? gameObject.GetPath()}{(bogie > 0 ? $" Bogie {bogie}" : "")}"; - return identifier; + return identifier ?? "Unknown"; } } diff --git a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs index b4e33222..3073fa0f 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs @@ -240,19 +240,17 @@ public void OnDisable() if (UnloadWatcher.isQuitting) return; - NetworkLifecycle.Instance.OnTick -= Common_OnTick; - NetworkLifecycle.Instance.OnTick -= Server_OnTick; - - NetworkLifecycle.Instance.Server.PlayerDisconnect -= Server_OnPlayerDisconnect; - + //Clean dictionaries trainCarsToNetworkedTrainCars.Remove(TrainCar); - trainCarIdToNetworkedTrainCars.Remove(CurrentID); trainCarIdToTrainCars.Remove(CurrentID); foreach (Coupler coupler in TrainCar.couplers) hoseToCoupler.Remove(coupler.hoseAndCock); + //stop tracking client events + NetworkLifecycle.Instance.OnTick -= Common_OnTick; + if (firebox != null) { firebox.fireboxCoalControlPort.ValueUpdatedInternally -= Client_OnAddCoal; //Player adding coal @@ -270,8 +268,12 @@ public void OnDisable() if (TrainCar.PaintInterior != null) TrainCar.PaintInterior.OnThemeChanged -= Common_OnPaintThemeChange; + //stop tracking server events if (NetworkLifecycle.Instance.IsHost()) { + NetworkLifecycle.Instance.OnTick -= Server_OnTick; + NetworkLifecycle.Instance.Server.PlayerDisconnect -= Server_OnPlayerDisconnect; + bogie1.TrackChanged -= Server_BogieTrackChanged; bogie2.TrackChanged -= Server_BogieTrackChanged; diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index d2c65c85..daf13877 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -893,32 +893,32 @@ private void OnCommonCouplerInteractionPacket(CommonCouplerInteractionPacket pac private void OnCommonTrainUncouplePacket(CommonTrainUncouplePacket packet, ITransportPeer peer) { - SendPacketToAll(packet, DeliveryMethod.ReliableUnordered, peer); + SendPacketToAll(packet, DeliveryMethod.ReliableOrdered, peer); } private void OnCommonHoseConnectedPacket(CommonHoseConnectedPacket packet, ITransportPeer peer) { - SendPacketToAll(packet, DeliveryMethod.ReliableUnordered, peer); + SendPacketToAll(packet, DeliveryMethod.ReliableOrdered, peer); } private void OnCommonHoseDisconnectedPacket(CommonHoseDisconnectedPacket packet, ITransportPeer peer) { - SendPacketToAll(packet, DeliveryMethod.ReliableUnordered, peer); + SendPacketToAll(packet, DeliveryMethod.ReliableOrdered, peer); } private void OnCommonMuConnectedPacket(CommonMuConnectedPacket packet, ITransportPeer peer) { - SendPacketToAll(packet, DeliveryMethod.ReliableUnordered, peer); + SendPacketToAll(packet, DeliveryMethod.ReliableOrdered, peer); } private void OnCommonMuDisconnectedPacket(CommonMuDisconnectedPacket packet, ITransportPeer peer) { - SendPacketToAll(packet, DeliveryMethod.ReliableUnordered, peer); + SendPacketToAll(packet, DeliveryMethod.ReliableOrdered, peer); } private void OnCommonCockFiddlePacket(CommonCockFiddlePacket packet, ITransportPeer peer) { - SendPacketToAll(packet, DeliveryMethod.ReliableUnordered, peer); + SendPacketToAll(packet, DeliveryMethod.ReliableOrdered, peer); } private void OnCommonBrakeCylinderReleasePacket(CommonBrakeCylinderReleasePacket packet, ITransportPeer peer) diff --git a/Multiplayer/Patches/Train/CouplerPatch.cs b/Multiplayer/Patches/Train/CouplerPatch.cs index 2fd823c1..64eb310a 100644 --- a/Multiplayer/Patches/Train/CouplerPatch.cs +++ b/Multiplayer/Patches/Train/CouplerPatch.cs @@ -17,10 +17,10 @@ private static void ConnectAirHose(Coupler __instance, Coupler other, bool playA if (UnloadWatcher.isUnloading || NetworkLifecycle.Instance.IsProcessingPacket) return; - //Ensure local car has initialised and breaks have been connected on spawn before sending any packets - if (!NetworkedTrainCar.TryGetFromTrainCar(__instance?.train, out var netTrainCar) || !netTrainCar.Client_Initialized) + //Ensure local car has initialised and brakes have been connected on spawn before sending any packets + if (!NetworkedTrainCar.TryGetFromTrainCar(__instance?.train, out var netTrainCar) || !(netTrainCar?.Client_Initialized ?? false) || netTrainCar?.gameObject == null) { - Multiplayer.LogDebug(() => $"ConnectAirHose({__instance?.train?.ID}) netTrainCar found: {netTrainCar != null}, Initialised: {netTrainCar?.Client_Initialized}"); + Multiplayer.LogWarning($"ConnectAirHose({__instance?.train?.ID}) netTrainCar found: {netTrainCar != null}, GO exists: {netTrainCar?.gameObject != null}, Initialised: {netTrainCar?.Client_Initialized}"); return; } @@ -35,10 +35,10 @@ private static void DisconnectAirHose(Coupler __instance, bool playAudio) if (UnloadWatcher.isUnloading || NetworkLifecycle.Instance.IsProcessingPacket) return; - //Ensure local car has initialised and breaks have been connected on spawn before sending any packets - if (!NetworkedTrainCar.TryGetFromTrainCar(__instance?.train, out var netTrainCar) || !netTrainCar.Client_Initialized) + //Ensure local car has initialised and brakes have been connected on spawn before sending any packets + if (!NetworkedTrainCar.TryGetFromTrainCar(__instance?.train, out var netTrainCar) || !(netTrainCar?.Client_Initialized ?? false) || netTrainCar?.gameObject == null) { - Multiplayer.LogDebug(() => $"DisconnectAirHose({__instance?.train?.ID}) netTrainCar found: {netTrainCar != null}, Initialised: {netTrainCar?.Client_Initialized}"); + Multiplayer.LogWarning($"DisconnectAirHose({__instance?.train?.ID}) netTrainCar found: {netTrainCar != null}, GO exists: {netTrainCar?.gameObject != null}, Initialised: {netTrainCar?.Client_Initialized}"); return; } From 2b187903f18fc40026549c96feb3509f94372645 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 27 Apr 2025 10:47:12 +1000 Subject: [PATCH 303/521] Ready for release --- Multiplayer/Multiplayer.csproj | 2 +- info.json | 2 +- releases.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Multiplayer/Multiplayer.csproj b/Multiplayer/Multiplayer.csproj index cfefd05d..cb9169f1 100644 --- a/Multiplayer/Multiplayer.csproj +++ b/Multiplayer/Multiplayer.csproj @@ -3,7 +3,7 @@ net48 latest Multiplayer - 0.1.11.0 + 0.1.11.1 diff --git a/info.json b/info.json index 54a43454..97428409 100644 --- a/info.json +++ b/info.json @@ -1,6 +1,6 @@ { "Id": "Multiplayer", - "Version": "0.1.11.0", + "Version": "0.1.11.1", "DisplayName": "Multiplayer", "Author": "Insprill, Macka, Morm", "EntryMethod": "Multiplayer.Multiplayer.Load", diff --git a/releases.json b/releases.json index 303e465e..2a054331 100644 --- a/releases.json +++ b/releases.json @@ -1,6 +1,6 @@ { "Releases": [ - {"Id": "Multiplayer", "Version": "0.1.11.0", "DownloadUrl": "https://github.com/AMacro/dv-multiplayer/releases/download/v0.1.11.0-Beta/Multiplayer.0.1.11.0.zip"} + {"Id": "Multiplayer", "Version": "0.1.11.1", "DownloadUrl": "https://github.com/AMacro/dv-multiplayer/releases/download/v0.1.11.1-Beta/Multiplayer.0.1.11.1.zip"} ] } \ No newline at end of file From df1e1ea9df1984f4dc4eae60167a3af727b97e41 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 4 May 2025 10:58:03 +1000 Subject: [PATCH 304/521] Fixed memory/event subscription leak WorldStreamingInit.LoadingFinished was not unsubscribed and would cause a re-trigger on game load. Minor tidy up of code and logging for style consistency --- .../Managers/Server/NetworkServer.cs | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index daf13877..0eec1b16 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -47,8 +47,8 @@ public class NetworkServer : NetworkManager private readonly Dictionary Peers = []; private LobbyServerManager lobbyServerManager; - public bool isSinglePlayer; - public LobbyServerData serverData; + public readonly bool IsSinglePlayer; + public LobbyServerData ServerData; public RerailController rerailController; public IReadOnlyCollection ServerPlayers => serverPlayers.Values; @@ -64,18 +64,16 @@ public class NetworkServer : NetworkManager //we don't care if the client doesn't have these mods public static string[] modWhiteList = ["RuntimeUnityEditor", "BookletOrganizer", "RemoteDispatch"]; - public NetworkServer(IDifficulty difficulty, Settings settings, bool isSinglePlayer, LobbyServerData serverData) : base(settings) + public NetworkServer(IDifficulty difficulty, Settings settings, bool singlePlayer, LobbyServerData serverData) : base(settings) { - LogDebug(()=>$"NetworkServer Constructor"); - this.isSinglePlayer = isSinglePlayer; - this.serverData = serverData; + Log(()=>$"Server created for {(singlePlayer ? "single player" : "multiplayer")} game"); + IsSinglePlayer = singlePlayer; + ServerData = serverData; Difficulty = difficulty; serverMods = ModInfo.FromModEntries(UnityModManager.modEntries) .Where(mod => !modWhiteList.Contains(mod.Id)).ToArray(); - - } public override bool Start(int port) @@ -100,13 +98,15 @@ public override bool Start(int port) public override void Stop() { + WorldStreamingInit.LoadingFinished -= OnLoaded; + if (lobbyServerManager != null) { lobbyServerManager.RemoveFromLobbyServer(); UnityEngine.Object.Destroy(lobbyServerManager); } - //Alert all clients (except h + //Alert all clients (except host) var packet = WritePacket(new ClientboundDisconnectPacket()); foreach (var peer in Peers.Values) { @@ -158,8 +158,7 @@ protected override void Subscribe() private void OnLoaded() { - //Debug.Log($"Server loaded, isSinglePlayer: {isSinglePlayer} isPublic: {isPublic}"); - if (!isSinglePlayer) + if (!IsSinglePlayer) { lobbyServerManager = NetworkLifecycle.Instance.GetOrAddComponent(); } @@ -636,7 +635,7 @@ private void OnServerboundClientLoginPacket(ServerboundClientLoginPacket packet, return; } - if (PlayerCount >= Multiplayer.Settings.MaxPlayers || isSinglePlayer && PlayerCount >= 1) + if (PlayerCount >= Multiplayer.Settings.MaxPlayers || IsSinglePlayer && PlayerCount >= 1) { LogWarning("Denied login due to server being full!"); ClientboundLoginResponsePacket denyPacket = new() From 6de6773906bb3a253a0777c664d8c4dac4f9b57e Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 4 May 2025 11:01:44 +1000 Subject: [PATCH 305/521] Start client before resetting states --- .../Components/Networking/NetworkLifecycle.cs | 15 +++++++++------ .../Networking/Managers/Client/NetworkClient.cs | 7 +++---- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/Multiplayer/Components/Networking/NetworkLifecycle.cs b/Multiplayer/Components/Networking/NetworkLifecycle.cs index d3917a9b..54b3b291 100644 --- a/Multiplayer/Components/Networking/NetworkLifecycle.cs +++ b/Multiplayer/Components/Networking/NetworkLifecycle.cs @@ -132,15 +132,16 @@ public bool StartServer(IDifficulty difficulty) Multiplayer.Log($"Starting server on port {port}"); NetworkServer server = new(difficulty, Multiplayer.Settings, IsSinglePlayer, serverData); - //reset for next game - IsSinglePlayer = true; - serverData = null; - if (!server.Start(port)) return false; Server = server; - StartClient(IPAddress.Loopback.ToString(), port, Multiplayer.Settings.Password, IsSinglePlayer, null/* (DisconnectReason dr,string msg) =>{ }*/); + StartClient(IPAddress.Loopback.ToString(), port, Multiplayer.Settings.Password, IsSinglePlayer, null); + + //reset for next game + IsSinglePlayer = true; + serverData = null; + return true; } @@ -148,7 +149,7 @@ public void StartClient(string address, int port, string password, bool isSingle { if (Client != null) throw new InvalidOperationException("NetworkManager already exists!"); - NetworkClient client = new(Multiplayer.Settings); + NetworkClient client = new(Multiplayer.Settings, isSinglePlayer); client.Start(address, port, password, isSinglePlayer, onDisconnect); Client = client; OnSettingsUpdated(Multiplayer.Settings); // Show stats if enabled @@ -192,7 +193,9 @@ private void TickManager(NetworkManager manager) { if (manager == null) return; + tickWatchdog.Start(); + try { manager.PollEvents(); diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 6fdd371c..34dd2268 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -1,6 +1,4 @@ using System; -using System.Net; -using System.Text; using System.Collections.Generic; using DV; using DV.Damage; @@ -61,13 +59,14 @@ public class NetworkClient : NetworkManager private ITransportPeer serverPeer; private ChatGUI chatGUI; - public bool isSinglePlayer; + private readonly bool isSinglePlayer; private bool isAlsoHost; IGameSession originalSession; - public NetworkClient(Settings settings) : base(settings) + public NetworkClient(Settings settings, bool singlePlayer) : base(settings) { + isSinglePlayer = singlePlayer; ClientPlayerManager = new ClientPlayerManager(); } From 8d03a4396c2412517e038df391c6bdfbfa388bc0 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 4 May 2025 11:06:44 +1000 Subject: [PATCH 306/521] Fix null reference issues and code tidy up --- .../Managers/Server/LobbyServerManager.cs | 82 +++++++++++++------ 1 file changed, 57 insertions(+), 25 deletions(-) diff --git a/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs b/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs index f2d36dda..59fe6eac 100644 --- a/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs +++ b/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs @@ -17,7 +17,6 @@ using Steamworks; using Steamworks.Data; using Multiplayer.Utils; -using Multiplayer.Networking.TransportLayers; using Multiplayer.Components.MainMenu; namespace Multiplayer.Networking.Managers.Server; @@ -59,13 +58,33 @@ public class LobbyServerManager : MonoBehaviour #region Unity public void Awake() { + bool destroy = false; + server = NetworkLifecycle.Instance.Server; - Multiplayer.Log($"LobbyServerManager New({server != null})"); + if (server == null) + { + server.LogError($"LobbyServerManager Server is null"); + destroy = true; + } + else if (server?.ServerData == null) + { + Multiplayer.LogError($"LobbyServerManager Server Data is null"); + destroy = true; + } + + if (destroy) + { + Multiplayer.LogError($"Failed to load LobbyServerManager"); + Destroy(this); + } } public IEnumerator Start() { + if (server == null || server.ServerData == null) + yield break; + //Create a steam lobby if (DVSteamworks.Success) { @@ -73,19 +92,30 @@ public IEnumerator Start() } //Register with old php lobby server (provides stats and makes the lobby visible, but not joinable to users on old versions) - server.serverData.ipv6 = GetStaticIPv6Address(); - server.serverData.LocalIPv4 = GetLocalIPv4Address(); + server.ServerData.ipv6 = GetStaticIPv6Address(); + server.ServerData.LocalIPv4 = GetLocalIPv4Address(); - StartCoroutine(GetIPv4(Multiplayer.Settings.Ipv4AddressCheck)); + if (!string.IsNullOrEmpty(Multiplayer.Settings.Ipv4AddressCheck)) + { + StartCoroutine(GetIPv4(Multiplayer.Settings.Ipv4AddressCheck)); + } + else + { + server.LogWarning("Ipv4AddressCheck URL is null or empty, skipping IPv4 detection"); + initialised = true; + } - while(!initialised) + 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); + server.Log + ( + $"\r\nPublic IPv4: {server.ServerData.ipv4}\r\n" + + $"Public IPv6: {server.ServerData.ipv6}\r\n" + + $"Private IPv4: {server.ServerData.LocalIPv4}" + ); - if (server.serverData.Visibility >= ServerVisibility.Private) + if (server.ServerData.Visibility >= ServerVisibility.Private) { Multiplayer.Log($"Registering server at: {Multiplayer.Settings.LobbyServerAddress}/{ENDPOINT_ADD_SERVER}"); StartCoroutine(RegisterWithLobbyServer($"{Multiplayer.Settings.LobbyServerAddress}/{ENDPOINT_ADD_SERVER}")); @@ -103,7 +133,7 @@ public IEnumerator Start() server_id = Guid.NewGuid().ToString(); } - server.serverData.id = server_id; + server.ServerData.id = server_id; StartDiscoveryServer(); } @@ -128,20 +158,20 @@ public 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; + server.ServerData.CurrentPlayers = server.PlayerCount; StartCoroutine(UpdateLobbyServer($"{Multiplayer.Settings.LobbyServerAddress}/{ENDPOINT_UPDATE_SERVER}")); if(lobby != null) { - SteamworksUtils.SetLobbyData((Lobby)lobby, server.serverData, EXCLUDE_PARAMS); + SteamworksUtils.SetLobbyData((Lobby)lobby, server.ServerData, EXCLUDE_PARAMS); } } - }else if (server.serverData.Visibility == ServerVisibility.Private || !sendUpdates) + }else if (server?.ServerData?.Visibility == ServerVisibility.Private || !sendUpdates) { - server.serverData.CurrentPlayers = server.PlayerCount; + server.ServerData.CurrentPlayers = server.PlayerCount; } //Keep LAN discovery running @@ -153,9 +183,11 @@ public void Update() #region Steam Lobby public async void CreateSteamLobby() { + if (server == null || server.ServerData == null) + return; // Specify the lobby type (public, private, etc.) - var result = await SteamMatchmaking.CreateLobbyAsync(server.serverData.MaxPlayers); + var result = await SteamMatchmaking.CreateLobbyAsync(server.ServerData.MaxPlayers); if (result.HasValue) { @@ -168,14 +200,14 @@ public async void CreateSteamLobby() lobby?.SetData(SteamworksUtils.LOBBY_MP_MOD_KEY, SteamworksUtils.LOBBY_MP_MOD_KEY); //We'll add this in for filtering lobby?.SetData(SteamworksUtils.LOBBY_NET_LOCATION_KEY, SteamNetworkingUtils.LocalPingLocation.ToString()); //for ping estimation - SteamworksUtils.SetLobbyData((Lobby)lobby, server.serverData, EXCLUDE_PARAMS); + SteamworksUtils.SetLobbyData((Lobby)lobby, server.ServerData, EXCLUDE_PARAMS); //Set correct visibility - if (server.serverData.Visibility == ServerVisibility.Private) + if (server.ServerData.Visibility == ServerVisibility.Private) lobby?.SetPrivate(); - else if (server.serverData.Visibility == ServerVisibility.Friends) + else if (server.ServerData.Visibility == ServerVisibility.Friends) lobby?.SetFriendsOnly(); - else if (server.serverData.Visibility == ServerVisibility.Public) + else if (server.ServerData.Visibility == ServerVisibility.Public) lobby?.SetPublic(); lobby?.SetJoinable(true); @@ -200,7 +232,7 @@ public void RemoveFromLobbyServer() private IEnumerator RegisterWithLobbyServer(string uri) { JsonSerializerSettings jsonSettings = new() { NullValueHandling = NullValueHandling.Ignore }; - string json = JsonConvert.SerializeObject(server.serverData, jsonSettings); + string json = JsonConvert.SerializeObject(server.ServerData, jsonSettings); Multiplayer.LogDebug(()=>$"JsonRequest: {json}"); yield return SendWebRequest( @@ -247,7 +279,7 @@ private IEnumerator UpdateLobbyServer(string uri) server_id, private_key, inGame.ToString("d\\d\\ hh\\h\\ mm\\m\\ ss\\s"), - server.serverData.CurrentPlayers + server.ServerData.CurrentPlayers ); string json = JsonConvert.SerializeObject(reqData, jsonSettings); @@ -285,7 +317,7 @@ private IEnumerator GetIPv4(string uri) if (match != null) { Multiplayer.Log($"IPv4 address extracted: {match.Value}"); - server.serverData.ipv4 = match.Value; + server.ServerData.ipv4 = match.Value; } else { @@ -494,7 +526,7 @@ private void OnUnconnectedDiscoveryPacket(UnconnectedDiscoveryPacket packet, IPE if (!packet.IsResponse) { packet.IsResponse = true; - packet.Data = server.serverData; + packet.Data = server.ServerData; } SendUnconnectedPacket(packet, endPoint.Address.ToString(), endPoint.Port); From b651c8a870dd2b1199ad07df5db83f65c06bab72 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 4 May 2025 12:26:12 +1000 Subject: [PATCH 307/521] Additional error handling for null objects Attempt to reduce scene collapse exceptions and add better null reference handling --- .../Train/NetworkTrainsetWatcher.cs | 37 +++++++++++++------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs b/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs index 23b35ddc..c1a14830 100644 --- a/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs +++ b/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs @@ -38,12 +38,18 @@ 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, tick); + { + if (UnloadWatcher.isUnloading || UnloadWatcher.isQuitting) + return; + + if (set != null) + Server_TickSet(set, tick); + else + Multiplayer.LogError($"Server_OnTick(): Trainset is null!"); + } } private void Server_TickSet(Trainset set, uint tick) { @@ -51,11 +57,8 @@ private void Server_TickSet(Trainset set, uint tick) bool maxTicksReached = false; bool anyTracksDirty = false; - if (set == null) - { - Multiplayer.LogError($"Server_TickSet(): Received null set!"); + if (UnloadWatcher.isUnloading || UnloadWatcher.isQuitting) return; - } cachedSendPacket.FirstNetId = set.firstCar.GetNetId(); cachedSendPacket.LastNetId = set.lastCar.GetNetId(); @@ -66,21 +69,32 @@ private void Server_TickSet(Trainset set, uint tick) foreach (TrainCar trainCar in set.cars) { - if (trainCar == null || !trainCar.gameObject.activeSelf) + if (trainCar == null || trainCar.gameObject == null || !trainCar.gameObject.activeSelf) { - Multiplayer.LogError($"Trainset {set.id} ({set.firstCar?.GetNetId()} has a null or inactive ({trainCar?.gameObject.activeSelf}) car!"); + Multiplayer.LogError($"Trainset {set?.id} ({set.firstCar?.GetNetId()}) has a null or inactive car! trainCar: {trainCar != null}, gameObject: {trainCar?.gameObject != null}, active: {trainCar?.gameObject?.activeSelf}"); 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)) + if (NetworkedTrainCar.TryGetFromTrainCar(trainCar, out NetworkedTrainCar netTC) && netTC != null) { maxTicksReached |= netTC.TicksSinceSync >= MAX_UNSYNC_TICKS; //Even if the car is stationary, if the max ticks has been exceeded we will still sync anyTracksDirty |= netTC.BogieTracksDirty; } + else + { + Multiplayer.LogError($"NetworkedTrainCar not found for TrainCar {trainCar?.ID} in set {set?.id} ({set.firstCar?.GetNetId()})"); + return; + } if (trainCar.derailed) { + if (trainCar?.rb == null) + { + Multiplayer.LogError($"Rigid body not found for TrainCar {trainCar?.ID} in set {set?.id} ({set.firstCar?.GetNetId()})"); + return; + } + // Check if derailed car is actually moving float velocityMagnitude = trainCar.rb.velocity.magnitude; if (velocityMagnitude > VELOCITY_THRESHOLD) @@ -110,7 +124,7 @@ private void Server_TickSet(Trainset set, uint tick) TrainCar trainCar = set.cars[i]; if (!trainCar.TryNetworked(out NetworkedTrainCar networkedTrainCar)) { - Multiplayer.LogDebug(() => $"TrainCar {trainCar.ID} is not networked! Is active? {trainCar.gameObject.activeInHierarchy}"); + Multiplayer.LogDebug(() => $"TrainCar {trainCar?.ID} is not networked! Is active? {trainCar?.gameObject?.activeInHierarchy}"); continue; } @@ -145,7 +159,6 @@ private void Server_TickSet(Trainset set, uint tick) } //reset this car's states - //networkedTrainCar.TicksSinceSync = 0; networkedTrainCar.BogieTracksDirty = false; } From 2448fee52cde36d4f729cffb58263147fc26a41f Mon Sep 17 00:00:00 2001 From: AMacro Date: Mon, 5 May 2025 00:25:54 +1000 Subject: [PATCH 308/521] Rev up --- 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 cb9169f1..c9722a11 100644 --- a/Multiplayer/Multiplayer.csproj +++ b/Multiplayer/Multiplayer.csproj @@ -3,7 +3,7 @@ net48 latest Multiplayer - 0.1.11.1 + 0.1.11.3 diff --git a/info.json b/info.json index 97428409..9d892565 100644 --- a/info.json +++ b/info.json @@ -1,6 +1,6 @@ { "Id": "Multiplayer", - "Version": "0.1.11.1", + "Version": "0.1.11.3", "DisplayName": "Multiplayer", "Author": "Insprill, Macka, Morm", "EntryMethod": "Multiplayer.Multiplayer.Load", From 7ddb233ddb3ce611105aa42a13a3c9017f2bcf59 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 10 May 2025 14:57:22 +1000 Subject: [PATCH 309/521] Fix DVSteamworks for 2025-05-10 updates --- .../Components/MainMenu/HostGamePane.cs | 28 +++++++++--------- .../Components/MainMenu/ServerBrowserPane.cs | 9 +++--- Multiplayer/Multiplayer.csproj | 4 +-- .../Managers/Server/LobbyServerManager.cs | 29 ++++++++++--------- Multiplayer/Patches/Util/DVSteamworksPatch.cs | 1 + Multiplayer/Utils/SteamWorksUtils.cs | 7 +++-- 6 files changed, 42 insertions(+), 36 deletions(-) diff --git a/Multiplayer/Components/MainMenu/HostGamePane.cs b/Multiplayer/Components/MainMenu/HostGamePane.cs index f617de0a..903e543f 100644 --- a/Multiplayer/Components/MainMenu/HostGamePane.cs +++ b/Multiplayer/Components/MainMenu/HostGamePane.cs @@ -1,22 +1,24 @@ -using System; -using System.Reflection; -using DV; -using DV.UI; +using DV.Common; +using DV.Localization; +using DV.Platform.Steam; using DV.UI.PresetEditors; +using DV.UI; using DV.UIFramework; -using DV.Localization; -using DV.Common; +using DV; +using Multiplayer.Components.Networking; +using Multiplayer.Components.Util; +using Multiplayer.Networking.Data; +using Multiplayer.Networking.Managers.Server; using Multiplayer.Utils; +using System.Linq; +using System.Reflection; +using System; using TMPro; -using UnityEngine; -using UnityEngine.UI; using UnityEngine.Events; -using Multiplayer.Networking.Data; -using Multiplayer.Components.Networking; -using Multiplayer.Components.Util; +using UnityEngine.UI; +using UnityEngine; using UnityModManagerNet; -using System.Linq; -using Multiplayer.Networking.Managers.Server; + namespace Multiplayer.Components.MainMenu; public class HostGamePane : MonoBehaviour diff --git a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs index 24f46c30..099fbd62 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs @@ -1,25 +1,26 @@ -using DV; using DV.Localization; +using DV.Platform.Steam; using DV.UI; using DV.UIFramework; using DV.Utils; +using DV; using LiteNetLib; using Multiplayer.Components.MainMenu.ServerBrowser; using Multiplayer.Components.Networking; using Multiplayer.Components.UI.Controls; using Multiplayer.Networking.Data; using Multiplayer.Utils; -using Steamworks; using Steamworks.Data; -using System; +using Steamworks; using System.Collections.Generic; using System.Collections; using System.Linq; using System.Net; using System.Threading.Tasks; +using System; using TMPro; -using UnityEngine; using UnityEngine.UI; +using UnityEngine; namespace Multiplayer.Components.MainMenu { diff --git a/Multiplayer/Multiplayer.csproj b/Multiplayer/Multiplayer.csproj index c9722a11..c05779e4 100644 --- a/Multiplayer/Multiplayer.csproj +++ b/Multiplayer/Multiplayer.csproj @@ -15,7 +15,7 @@ - + @@ -40,7 +40,7 @@ - + diff --git a/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs b/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs index 59fe6eac..16ac1bc2 100644 --- a/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs +++ b/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs @@ -1,23 +1,24 @@ -using System; +using DV.Platform.Steam; +using DV.WeatherSystem; +using LiteNetLib.Utils; +using LiteNetLib; +using Multiplayer.Components.MainMenu; +using Multiplayer.Components.Networking; using Multiplayer.Networking.Data; +using Multiplayer.Networking.Packets.Unconnected; +using Multiplayer.Utils; using Newtonsoft.Json; +using Steamworks.Data; +using Steamworks; using System.Collections; -using UnityEngine; -using UnityEngine.Networking; -using Multiplayer.Components.Networking; -using DV.WeatherSystem; -using System.Text.RegularExpressions; +using System.Linq; using System.Net.NetworkInformation; using System.Net.Sockets; -using LiteNetLib; -using LiteNetLib.Utils; -using Multiplayer.Networking.Packets.Unconnected; using System.Net; -using System.Linq; -using Steamworks; -using Steamworks.Data; -using Multiplayer.Utils; -using Multiplayer.Components.MainMenu; +using System.Text.RegularExpressions; +using System; +using UnityEngine.Networking; +using UnityEngine; namespace Multiplayer.Networking.Managers.Server; public class LobbyServerManager : MonoBehaviour diff --git a/Multiplayer/Patches/Util/DVSteamworksPatch.cs b/Multiplayer/Patches/Util/DVSteamworksPatch.cs index 1646e436..fea9544c 100644 --- a/Multiplayer/Patches/Util/DVSteamworksPatch.cs +++ b/Multiplayer/Patches/Util/DVSteamworksPatch.cs @@ -1,3 +1,4 @@ +using DV.Platform.Steam; using HarmonyLib; using Steamworks; diff --git a/Multiplayer/Utils/SteamWorksUtils.cs b/Multiplayer/Utils/SteamWorksUtils.cs index d93565e0..11bc41c3 100644 --- a/Multiplayer/Utils/SteamWorksUtils.cs +++ b/Multiplayer/Utils/SteamWorksUtils.cs @@ -1,15 +1,16 @@ -using DV; using DV.Localization; +using DV.Platform.Steam; using DV.UIFramework; +using DV; using Multiplayer.Components.MainMenu; using Multiplayer.Components.Networking; using Multiplayer.Networking.Data; using Multiplayer.Patches.MainMenu; -using Steamworks; using Steamworks.Data; -using System; +using Steamworks; using System.Collections; using System.Linq; +using System; using UnityEngine; namespace Multiplayer.Utils; From b3c99b032237fa94cf71ee29743019daf5cdf1ce Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 10 May 2025 20:38:26 +1000 Subject: [PATCH 310/521] Fix serverGridView placeholder if list is empty --- Multiplayer/Components/MainMenu/ServerBrowserPane.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs index 099fbd62..9b355aa0 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs @@ -342,6 +342,7 @@ private void SetupServerBrowser() //Don't forget to re-enable! GridviewGO.SetActive(true); + serverGridView.Clear(); } private void SetupListeners(bool on) { From 2c9557d4f5783df36702c69d28148053a7f8e744 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 10 May 2025 20:39:01 +1000 Subject: [PATCH 311/521] Minor optimisations and code clean-up --- Multiplayer/Components/Networking/UI/ChatGUI.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Multiplayer/Components/Networking/UI/ChatGUI.cs b/Multiplayer/Components/Networking/UI/ChatGUI.cs index 3522a6ac..54e86bb9 100644 --- a/Multiplayer/Components/Networking/UI/ChatGUI.cs +++ b/Multiplayer/Components/Networking/UI/ChatGUI.cs @@ -36,8 +36,8 @@ public class ChatGUI : MonoBehaviour private GameObject messagePrefab; - private List messageList = new List(); - private List sendHistory = new List(); + private readonly List messageList = new List(); + private readonly List sendHistory = new List(); private TMP_InputField chatInputIF; private ScrollRect scrollRect; @@ -63,7 +63,7 @@ public class ChatGUI : MonoBehaviour private GameFeatureFlags.Flag denied; - private void Awake() + protected void Awake() { Multiplayer.Log("ChatGUI Awake() called"); @@ -93,20 +93,20 @@ private void Awake() } - private void OnEnable() + protected void OnEnable() { chatInputIF.onSubmit.AddListener(Submit); chatInputIF.onValueChanged.AddListener(ChatInputChange); } - private void OnDisable() + protected void OnDisable() { chatInputIF.onSubmit.RemoveAllListeners(); chatInputIF.onValueChanged.RemoveAllListeners(); } - private void Update() + protected void Update() { //Handle keypresses to open/close the chat window if (!isOpen && Input.GetKeyDown(KeyCode.Return) && !AppUtil.Instance.IsPauseMenuOpen) From 1792e70d5434653f0b3480d1280ae5b403cc98c6 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 10 May 2025 20:41:02 +1000 Subject: [PATCH 312/521] Add support for chat key binding --- Multiplayer/Components/Networking/UI/ChatGUI.cs | 2 +- Multiplayer/Settings.cs | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/Multiplayer/Components/Networking/UI/ChatGUI.cs b/Multiplayer/Components/Networking/UI/ChatGUI.cs index 54e86bb9..d4165b40 100644 --- a/Multiplayer/Components/Networking/UI/ChatGUI.cs +++ b/Multiplayer/Components/Networking/UI/ChatGUI.cs @@ -109,7 +109,7 @@ protected void OnDisable() protected void Update() { //Handle keypresses to open/close the chat window - if (!isOpen && Input.GetKeyDown(KeyCode.Return) && !AppUtil.Instance.IsPauseMenuOpen) + if (!isOpen && Input.GetKeyDown(Multiplayer.Settings.ChatKey) && !AppUtil.Instance.IsPauseMenuOpen) { isOpen = true; //whole panel is open showingMessage = false; //We don't want to time out diff --git a/Multiplayer/Settings.cs b/Multiplayer/Settings.cs index c98c3a0a..dda156be 100644 --- a/Multiplayer/Settings.cs +++ b/Multiplayer/Settings.cs @@ -28,6 +28,11 @@ public class Settings : UnityModManager.ModSettings, IDrawable public string Username = "Player"; public string Guid = System.Guid.NewGuid().ToString(); + [Space(10)] + [Header("Misc.")] + [Draw("Chat Key Bind", Tooltip ="Key to show chat window.")] + public KeyCode ChatKey = KeyCode.Return; + [Space(10)] [Header("Server")] [Draw("Server Name", Tooltip = "Name of your server in the lobby browser.")] @@ -108,9 +113,13 @@ 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); Password = Password?.Trim(); + + ChatKey = ChatKey == KeyCode.None ? KeyCode.Return : ChatKey; + if (!UnloadWatcher.isQuitting) OnSettingsUpdated?.Invoke(this); Save(this, modEntry); From 75912484627e2686f6fb11598c1e91e138461534 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 10 May 2025 20:41:18 +1000 Subject: [PATCH 313/521] Clean up settings UI tooltips --- Multiplayer/Settings.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Multiplayer/Settings.cs b/Multiplayer/Settings.cs index dda156be..3e78fbfe 100644 --- a/Multiplayer/Settings.cs +++ b/Multiplayer/Settings.cs @@ -20,11 +20,11 @@ public class Settings : UnityModManager.ModSettings, IDrawable public int SettingsVer = CURRENT_VERSION; [Header("Player")] - [Draw("Use Steam Name", Tooltip = "Use your Steam name as 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; public ulong SteamId = 0; - [Draw("Username", Tooltip = "Your username in-game", VisibleOn = "UseSteamName|false")] + [Draw("Username", Tooltip = "Your username in-game.", VisibleOn = "UseSteamName|false")] public string Username = "Player"; public string Guid = System.Guid.NewGuid().ToString(); @@ -46,14 +46,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")] + [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")] + [Draw("Lobby Server address", Tooltip = "Address of lobby server for finding multiplayer games.")] public string LobbyServerAddress = "https://dv.mineit.space"; - [Draw("IPv4 Check Address", Tooltip = "Do not modify unless the service is unavailable")] + [Draw("IPv4 Check Address", Tooltip = "Do not modify unless the service is unavailable.")] 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.")] @@ -76,7 +76,7 @@ public class Settings : UnityModManager.ModSettings, IDrawable public bool ShowAdvancedSettings; [Draw("Show Stats", Tooltip = "Whether to show network statistics.", VisibleOn = "ShowAdvancedSettings|true")] public bool ShowStats; - [Draw("Stats List Size", Tooltip = "How many packets to list in the network statistics gui.", VisibleOn = "ShowStats|true")] + [Draw("Stats List Size", Tooltip = "How many packets to list in the network statistics GUI.", VisibleOn = "ShowStats|true")] public int StatsListSize = 3; [Draw("Debug Logging", Tooltip = "Whether to log extra information. This is useful for debugging, but should otherwise be kept off.", VisibleOn = "ShowAdvancedSettings|true")] public bool DebugLogging; From 6f404fe7e584f61a8fe45916b12af3ae55dd5026 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 11 May 2025 13:23:10 +1000 Subject: [PATCH 314/521] Add logging for car deletion packets --- .../Managers/Client/NetworkClient.cs | 21 ++++++++++++------- .../Managers/Server/NetworkServer.cs | 12 +++++------ Multiplayer/Patches/Train/CarSpawnerPatch.cs | 2 +- 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 34dd2268..cbada316 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -499,32 +499,37 @@ private void OnClientboundSpawnTrainSetPacket(ClientboundSpawnTrainSetPacket pac private void OnClientboundDestroyTrainCarPacket(ClientboundDestroyTrainCarPacket packet) { - if (!NetworkedTrainCar.Get(packet.NetId, out NetworkedTrainCar networkedTrainCar)) + if (!NetworkedTrainCar.Get(packet.NetId, out NetworkedTrainCar netTrainCar)) + { + LogWarning($"Received DestroyTrainCarPacket for netId: {packet.NetId}, but NetworkedTrainCar was not found."); return; + } + + Log($"Received DestroyTrainCarPacket for [{netTrainCar.CurrentID} {packet.NetId}]"); //Protect myself from getting deleted in race conditions - if (PlayerManager.Car == networkedTrainCar.TrainCar) + if (PlayerManager.Car == netTrainCar.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}, netId: {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 - if (networkedTrainCar == null || networkedTrainCar.gameObject == null || networkedTrainCar.TrainCar == null) + if (netTrainCar == null || netTrainCar.gameObject == null || netTrainCar.TrainCar == null) { - LogDebug(() => $"OnClientboundDestroyTrainCarPacket({packet?.NetId}) networkedTrainCar: {networkedTrainCar != null}, go: {(networkedTrainCar?.gameObject) != null}, trainCar: {networkedTrainCar?.TrainCar != null}"); + LogDebug(() => $"OnClientboundDestroyTrainCarPacket({packet?.NetId}) networkedTrainCar: {netTrainCar != null}, trainCar: {netTrainCar?.TrainCar != null}"); } else { - NetworkedPlayer[] componentsInChildren = (networkedTrainCar?.gameObject != null) ? networkedTrainCar.GetComponentsInChildren() : []; + NetworkedPlayer[] componentsInChildren = (netTrainCar?.gameObject != null) ? netTrainCar.GetComponentsInChildren() : []; foreach (NetworkedPlayer networkedPlayer in componentsInChildren) { networkedPlayer.UpdateCar(0); } - networkedTrainCar.TrainCar.UpdateJobIdOnCarPlates(string.Empty); - CarSpawner.Instance.DeleteCar(networkedTrainCar.TrainCar); + netTrainCar.TrainCar.UpdateJobIdOnCarPlates(string.Empty); + CarSpawner.Instance.DeleteCar(netTrainCar.TrainCar); } } diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index 0eec1b16..c76973bb 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -339,18 +339,18 @@ public void SendSpawnTrainCar(NetworkedTrainCar networkedTrainCar) SendPacketToAll(ClientboundSpawnTrainCarPacket.FromTrainCar(networkedTrainCar), DeliveryMethod.ReliableOrdered, SelfPeer); } - public void SendDestroyTrainCar(ushort netId, ITransportPeer peer = null) + public void SendDestroyTrainCar(NetworkedTrainCar netTrainCar, ITransportPeer peer = null) { //ushort netID = trainCar.GetNetId(); - LogDebug(() => $"SendDestroyTrainCar({netId})"); + Log($"Sending DestroyTrainCarPacket for [{netTrainCar.CurrentID} {netTrainCar.NetId}]"); - if (netId == 0) + if (netTrainCar.NetId == 0) { - Multiplayer.LogWarning($"SendDestroyTrainCar failed. netId {netId}"); + Multiplayer.LogWarning($"SendDestroyTrainCar failed. [{netTrainCar.CurrentID} {netTrainCar.NetId}]"); return; } - var packet = new ClientboundDestroyTrainCarPacket{ NetId = netId }; + var packet = new ClientboundDestroyTrainCarPacket{ NetId = netTrainCar.NetId }; if (peer == null) SendPacketToAll(packet, DeliveryMethod.ReliableOrdered, SelfPeer); @@ -881,7 +881,7 @@ private void OnCommonCouplerInteractionPacket(CommonCouplerInteractionPacket pac { 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); + SendDestroyTrainCar(netTrainCar, peer); } } diff --git a/Multiplayer/Patches/Train/CarSpawnerPatch.cs b/Multiplayer/Patches/Train/CarSpawnerPatch.cs index c33227a9..7aaad68f 100644 --- a/Multiplayer/Patches/Train/CarSpawnerPatch.cs +++ b/Multiplayer/Patches/Train/CarSpawnerPatch.cs @@ -21,7 +21,7 @@ private static void PrepareTrainCarForDeleting(TrainCar trainCar) networkedTrainCar.IsDestroying = true; - NetworkLifecycle.Instance.Server?.SendDestroyTrainCar(networkedTrainCar.NetId); + NetworkLifecycle.Instance.Server?.SendDestroyTrainCar(networkedTrainCar); } //Called from From 13fc9ee8a5450ceae34cfed83f0fda9b905028ac Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 11 May 2025 13:26:29 +1000 Subject: [PATCH 315/521] Ready for release --- Multiplayer/Multiplayer.csproj | 2 +- info.json | 2 +- releases.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Multiplayer/Multiplayer.csproj b/Multiplayer/Multiplayer.csproj index c05779e4..fa7bdbc8 100644 --- a/Multiplayer/Multiplayer.csproj +++ b/Multiplayer/Multiplayer.csproj @@ -3,7 +3,7 @@ net48 latest Multiplayer - 0.1.11.3 + 0.1.11.5 diff --git a/info.json b/info.json index 9d892565..dbd0ca1f 100644 --- a/info.json +++ b/info.json @@ -1,6 +1,6 @@ { "Id": "Multiplayer", - "Version": "0.1.11.3", + "Version": "0.1.11.5", "DisplayName": "Multiplayer", "Author": "Insprill, Macka, Morm", "EntryMethod": "Multiplayer.Multiplayer.Load", diff --git a/releases.json b/releases.json index 2a054331..0dfc5339 100644 --- a/releases.json +++ b/releases.json @@ -1,6 +1,6 @@ { "Releases": [ - {"Id": "Multiplayer", "Version": "0.1.11.1", "DownloadUrl": "https://github.com/AMacro/dv-multiplayer/releases/download/v0.1.11.1-Beta/Multiplayer.0.1.11.1.zip"} + {"Id": "Multiplayer", "Version": "0.1.11.5", "DownloadUrl": "https://github.com/AMacro/dv-multiplayer/releases/download/v0.1.11.5-Beta/Multiplayer.0.1.11.5.zip"} ] } \ No newline at end of file From ddf9521e55b7795a7f728daeab13cb2bd14f20dd Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 18 May 2025 17:06:29 +1000 Subject: [PATCH 316/521] Fix bogie issue B99.4 hotfix 2025-05-15 --- .../Networking/Train/NetworkedBogie.cs | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/Multiplayer/Components/Networking/Train/NetworkedBogie.cs b/Multiplayer/Components/Networking/Train/NetworkedBogie.cs index abdfd020..2cf23782 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedBogie.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedBogie.cs @@ -9,6 +9,7 @@ public class NetworkedBogie : TickedQueue { private const int MAX_FRAMES = 60; public Bogie Bogie { get; private set; } + private Rigidbody rb; protected override void OnEnable() { @@ -19,10 +20,15 @@ protected IEnumerator WaitForBogie() { int counter = 0; - while (Bogie == null && counter < MAX_FRAMES) + while (Bogie == null || rb == null && counter < MAX_FRAMES) { - Bogie = GetComponent(); if (Bogie == null) + Bogie = GetComponent(); + + if (rb == null) + rb = GetComponent(); + + if (rb == null || Bogie == null) { counter++; yield return new WaitForEndOfFrame(); @@ -35,6 +41,11 @@ protected IEnumerator WaitForBogie() { Multiplayer.LogError($"{gameObject.name} ({Bogie?.Car?.ID}): {nameof(NetworkedBogie)} requires a {nameof(Bogie)} component on the same GameObject! Waited {counter} iterations"); } + + if (rb == null) + { + Multiplayer.LogError($"{gameObject.name} ({Bogie?.Car?.ID}): {nameof(NetworkedBogie)} requires a {nameof(rb)} component on the same GameObject! Waited {counter} iterations"); + } } protected override void Process(BogieData snapshot, uint snapshotTick) @@ -42,7 +53,7 @@ protected override void Process(BogieData snapshot, uint snapshotTick) //Multiplayer.LogDebug(()=>$"NetworkedBogie.Process({identifier}) DataFlags: {snapshot.DataFlags}, {snapshotTick}, {snapshot.TrackNetId}, {snapshot.PositionAlongTrack} {snapshot.TrackDirection}"); - if (Bogie.HasDerailed) + if (Bogie == null || rb == null || Bogie.HasDerailed) return; if (snapshot.HasDerailed) @@ -74,6 +85,9 @@ protected override void Process(BogieData snapshot, uint snapshotTick) int physicsSteps = Mathf.FloorToInt((NetworkLifecycle.Instance.Tick - (float)snapshotTick) / NetworkLifecycle.TICK_RATE / Time.fixedDeltaTime) + 1; for (int i = 0; i < physicsSteps; i++) - Bogie.UpdatePointSetTraveller(); + { + var z = transform.InverseTransformDirection(rb.velocity).z; + Bogie.UpdatePointSetTraveller(z); + } } } From dbe67ce384aaeba45b0772d839b0738dd6c4b6f1 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 18 May 2025 18:08:17 +1000 Subject: [PATCH 317/521] Ready for release --- Multiplayer/Multiplayer.csproj | 2 +- info.json | 2 +- releases.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Multiplayer/Multiplayer.csproj b/Multiplayer/Multiplayer.csproj index fa7bdbc8..cbe176cc 100644 --- a/Multiplayer/Multiplayer.csproj +++ b/Multiplayer/Multiplayer.csproj @@ -3,7 +3,7 @@ net48 latest Multiplayer - 0.1.11.5 + 0.1.11.6 diff --git a/info.json b/info.json index dbd0ca1f..fef52cbe 100644 --- a/info.json +++ b/info.json @@ -1,6 +1,6 @@ { "Id": "Multiplayer", - "Version": "0.1.11.5", + "Version": "0.1.11.6", "DisplayName": "Multiplayer", "Author": "Insprill, Macka, Morm", "EntryMethod": "Multiplayer.Multiplayer.Load", diff --git a/releases.json b/releases.json index 0dfc5339..648f1fc2 100644 --- a/releases.json +++ b/releases.json @@ -1,6 +1,6 @@ { "Releases": [ - {"Id": "Multiplayer", "Version": "0.1.11.5", "DownloadUrl": "https://github.com/AMacro/dv-multiplayer/releases/download/v0.1.11.5-Beta/Multiplayer.0.1.11.5.zip"} + {"Id": "Multiplayer", "Version": "0.1.11.6", "DownloadUrl": "https://github.com/AMacro/dv-multiplayer/releases/download/v0.1.11.6-Beta/Multiplayer.0.1.11.6.zip"} ] } \ No newline at end of file From 3194eae4954d6b8d5d9e8c5e766d37e7c2dba654 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 24 May 2025 09:54:09 +1000 Subject: [PATCH 318/521] Fix all players receiving jobs when a new player connects When a new player connects all players were receiving the list of jobs. --- Multiplayer/Networking/Managers/Server/NetworkServer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index a7483b21..54e247c3 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -832,7 +832,7 @@ private void OnServerboundClientReadyPacket(ServerboundClientReadyPacket packet, for (int i = 0; i < jobs.Length; i++) { - SendJobsCreatePacket(netStation, [jobs[i]]); + SendJobsCreatePacket(netStation, [jobs[i]], peer); } } else From 1411ecfa2b2567f7867c0a6f60160884b7d05f2a Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 24 May 2025 09:54:09 +1000 Subject: [PATCH 319/521] Fix all players receiving jobs when a new player connects When a new player connects all players were receiving the list of jobs. --- Multiplayer/Networking/Managers/Server/NetworkServer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index c76973bb..ab8930ec 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -778,7 +778,7 @@ private void OnServerboundClientReadyPacket(ServerboundClientReadyPacket packet, for (int i = 0; i < jobs.Length; i++) { - SendJobsCreatePacket(netStation, [jobs[i]]); + SendJobsCreatePacket(netStation, [jobs[i]], peer); } } else From 02b748f5ab154e7c756392e9017eb62055677899 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 24 May 2025 10:30:54 +1000 Subject: [PATCH 320/521] Use static netId allocation for PitStopStations --- .../World/NetworkedPitStopStation.cs | 49 ++++--------------- .../Managers/Client/NetworkClient.cs | 40 --------------- .../Managers/Server/NetworkServer.cs | 1 - .../ClientboundPitStopStationLookupPacket.cs | 24 --------- 4 files changed, 10 insertions(+), 104 deletions(-) delete mode 100644 Multiplayer/Networking/Packets/Clientbound/World/ClientboundPitStopStationLookupPacket.cs diff --git a/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs b/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs index 111e733f..9ae08a5b 100644 --- a/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs +++ b/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs @@ -21,7 +21,6 @@ namespace Multiplayer.Components.Networking.World; public class NetworkedPitStopStation : IdMonoBehaviour { #region Lookup Cache - private static readonly Dictionary netPitStopStationToLocation = []; public static bool Get(ushort netId, out NetworkedPitStopStation obj) { @@ -30,57 +29,32 @@ public static bool Get(ushort netId, out NetworkedPitStopStation obj) return b; } - public static bool GetFromVector(Vector3 position, out NetworkedPitStopStation networkedPitStopStation) - { - return netPitStopStationToLocation.TryGetValue(position, out networkedPitStopStation); - } - - public static NetworkedPitStopStation[] GetAll() - { - return netPitStopStationToLocation.Values.ToArray(); - } - - public static Tuple[] GetAllPitStopStations() - { - if (netPitStopStationToLocation.Count == 0) - InitialisePitStops(); - - List> result = []; - - foreach (var kvp in netPitStopStationToLocation) - { - var selection = kvp.Value?.Station?.pitstop?.SelectedIndex ?? 0; - result.Add(new(kvp.Value.NetId, kvp.Key, selection)); - } - - return result.ToArray(); - } - public static void InitialisePitStops() { - if (netPitStopStationToLocation.Count != 0) - return; - var stations = Resources.FindObjectsOfTypeAll().Where(p => p.transform.parent != null).ToArray(); + //Find all pitstop stations that are placed on the map + //sort them by their hierachy path for consistent ordering + var stations = Resources.FindObjectsOfTypeAll() + .Where(p => p.transform.parent != null) + .OrderBy(p => p.GetObjectPath(), StringComparer.InvariantCulture) + .ToArray(); Multiplayer.LogDebug(() => $"InitialisePitStops() Found: {stations?.Length}"); foreach (var station in stations) { - Multiplayer.LogDebug(() => $"InitialisePitStops() Station: {station?.transform?.parent?.parent?.name}"); - var netStation = station.GetOrAddComponent(); netStation.Station = station; - CoroutineManager.Instance.StartCoroutine(netStation.Init()); - Multiplayer.LogDebug(() => $"InitialisePitStops() Parent: {station?.transform?.parent?.name}, parent-parent: {station?.transform?.parent?.parent?.name}, position global: {station?.transform?.position - WorldMover.currentMove}"); - netPitStopStationToLocation[station.transform.position - WorldMover.currentMove] = netStation; + Multiplayer.LogDebug(() => $"InitialisePitStops() Station: {station?.GetObjectPath()}, netId: {netStation.NetId}"); + + CoroutineManager.Instance.StartCoroutine(netStation.Init()); } } #endregion - protected override bool IsIdServerAuthoritative => true; + protected override bool IsIdServerAuthoritative => false; const float MAX_DELTA = 0.2f; const float MIN_UPDATE_TIME = 0.1f; @@ -161,9 +135,6 @@ protected void OnDisable() protected override void OnDestroy() { if (UnloadWatcher.isUnloading) - netPitStopStationToLocation.Clear(); - else - netPitStopStationToLocation.Remove(transform.position); if (carSelectorGrab != null) { diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 8bc81588..f7bcd617 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -128,7 +128,6 @@ protected override void Subscribe() netPacketProcessor.SubscribeReusable(OnClientboundWeatherPacket); netPacketProcessor.SubscribeReusable(OnClientboundRailwayStatePacket); netPacketProcessor.SubscribeReusable(OnClientboundStationControllerLookupPacket); - netPacketProcessor.SubscribeReusable(OnClientboundPitStopStationLookupPacket); netPacketProcessor.SubscribeReusable(OnClientboundPlayerJoinedPacket); @@ -452,45 +451,6 @@ private void OnClientboundStationControllerLookupPacket(ClientboundStationContro } } - //Force pitstops to be mapped to same netId across all clients and server - probably should implement for junctions, etc. - private void OnClientboundPitStopStationLookupPacket(ClientboundPitStopStationLookupPacket packet) - { - LogDebug(() => $"OnClientboundPitStopStationLookupPacket({packet.PitStops?.Length})"); - - if (packet.PitStops == null) - { - LogError($"OnClientboundPitStopStationLookupPacket received packet with null arrays: NetIDs is null: {packet.PitStops == null}"); - return; - } - - for (int i = 0; i < packet.PitStops.Length; i++) - { - LogDebug(() => $"OnClientboundPitStopStationLookupPacket({i}) vector: {packet.PitStops[i].Location}, netId: {packet.PitStops[i].NetId}"); - if (NetworkedPitStopStation.GetFromVector(packet.PitStops[i].Location, out NetworkedPitStopStation netStation)) - { - netStation.NetId = packet.PitStops[i].NetId; - if (netStation.Station.pitstop != null) - { - netStation.Station.pitstop.currentCarIndex = packet.PitStops[i].SelectedCar; - foreach (var mapping in packet.PitStops[i].PlugMapping) - { - if (netStation.TryGetPluggable(mapping.Key, out var netPluggable)) - { - LogDebug(() => $"OnClientboundPitStopStationLookupPacket({i}) {mapping.Key}, {mapping.Value} Found"); - netPluggable.NetId = mapping.Value; - } - else - { - LogDebug(() => $"OnClientboundPitStopStationLookupPacket({i}) {mapping.Key}, {mapping.Value} Not Found"); - } - } - } - } - else - LogError($"Syncing PitStopStations station with coords: {packet.PitStops[i].Location} not found"); - } - } - private void OnClientboundRailwayStatePacket(ClientboundRailwayStatePacket packet) { for (int i = 0; i < packet.SelectedJunctionBranches.Length; i++) diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index 54e247c3..2da36cfe 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -818,7 +818,6 @@ private void OnServerboundClientReadyPacket(ServerboundClientReadyPacket packet, // 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); - SendPacket(peer, new ClientboundPitStopStationLookupPacket(NetworkedPitStopStation.GetAll()), DeliveryMethod.ReliableOrdered); //send jobs foreach (StationController station in StationController.allStations) diff --git a/Multiplayer/Networking/Packets/Clientbound/World/ClientboundPitStopStationLookupPacket.cs b/Multiplayer/Networking/Packets/Clientbound/World/ClientboundPitStopStationLookupPacket.cs deleted file mode 100644 index c82b7216..00000000 --- a/Multiplayer/Networking/Packets/Clientbound/World/ClientboundPitStopStationLookupPacket.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Multiplayer.Components.Networking.World; -using Multiplayer.Networking.Data; -using System; -using System.Collections.Generic; -using System.Linq; -using UnityEngine; - -namespace Multiplayer.Networking.Packets.Clientbound.World; - -public class ClientboundPitStopStationLookupPacket -{ - public PitStopStationMappingData[] PitStops { get; set; } - - public ClientboundPitStopStationLookupPacket() { } - - public ClientboundPitStopStationLookupPacket(NetworkedPitStopStation[] netStations) - { - PitStops = new PitStopStationMappingData[netStations.Count()]; - - for (int i = 0; i < netStations.Count(); i++) - PitStops[i] = PitStopStationMappingData.From(netStations[i]); - } - -} From e781df0fbfedcb9ea0fe2d706419fb2270ae58cd Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 24 May 2025 10:31:35 +1000 Subject: [PATCH 321/521] Fix memory leak in client LoadingFinished sub --- .../Managers/Client/NetworkClient.cs | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index f7bcd617..6cb82098 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -179,6 +179,20 @@ protected override void Subscribe() netPacketProcessor.SubscribeReusable(OnClientboundPitStopBulkUpdatePacket); } + private void OnLoaded() + { + Log($"WorldStreamingInit.LoadingFinished()"); + NetworkedItemManager.Instance.CheckInstance(); + Log($"WorldStreamingInit.LoadingFinished() CacheWorldItems()"); + NetworkedItemManager.Instance.CacheWorldItems(); + Log($"WorldStreamingInit.LoadingFinished() InitialisePitStops()"); + NetworkedPitStopStation.InitialisePitStops(); + Log($"WorldStreamingInit.LoadingFinished() SendReadyPacket()"); + SendReadyPacket(); + + WorldStreamingInit.LoadingFinished -= OnLoaded; + } + #region Net Events public override void OnPeerConnected(ITransportPeer peer) @@ -351,17 +365,7 @@ private void OnClientboundSaveGameDataPacket(ClientboundSaveGameDataPacket packe Object.DontDestroyOnLoad(go); SceneSwitcher.SwitchToScene(DVScenes.Game); - WorldStreamingInit.LoadingFinished += () => - { - Log($"WorldStreamingInit.LoadingFinished()"); - NetworkedItemManager.Instance.CheckInstance(); - Log($"WorldStreamingInit.LoadingFinished() CacheWorldItems()"); - NetworkedItemManager.Instance.CacheWorldItems(); - Log($"WorldStreamingInit.LoadingFinished() InitialisePitStops()"); - NetworkedPitStopStation.InitialisePitStops(); - Log($"WorldStreamingInit.LoadingFinished() SendReadyPacket()"); - SendReadyPacket(); - }; + WorldStreamingInit.LoadingFinished += OnLoaded; TrainStress.globalIgnoreStressCalculation = true; From a87af100d1472475c2de879847ffb8f2f1091283 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 24 May 2025 10:31:35 +1000 Subject: [PATCH 322/521] Fix memory leak in client LoadingFinished sub --- .../Managers/Client/NetworkClient.cs | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index cbada316..3341b64e 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -167,6 +167,18 @@ protected override void Subscribe() netPacketProcessor.SubscribeNetSerializable(OnCommonItemChangePacket); } + private void OnLoaded() + { + Log($"WorldStreamingInit.LoadingFinished()"); + NetworkedItemManager.Instance.CheckInstance(); + Log($"WorldStreamingInit.LoadingFinished() CacheWorldItems()"); + NetworkedItemManager.Instance.CacheWorldItems(); + Log($"WorldStreamingInit.LoadingFinished() SendReadyPacket()"); + SendReadyPacket(); + + WorldStreamingInit.LoadingFinished -= OnLoaded; + } + #region Net Events public override void OnPeerConnected(ITransportPeer peer) @@ -339,15 +351,7 @@ private void OnClientboundSaveGameDataPacket(ClientboundSaveGameDataPacket packe Object.DontDestroyOnLoad(go); SceneSwitcher.SwitchToScene(DVScenes.Game); - WorldStreamingInit.LoadingFinished += () => - { - Log($"WorldStreamingInit.LoadingFinished()"); - NetworkedItemManager.Instance.CheckInstance(); - Log($"WorldStreamingInit.LoadingFinished() CacheWorldItems()"); - NetworkedItemManager.Instance.CacheWorldItems(); - Log($"WorldStreamingInit.LoadingFinished() SendReadyPacket()"); - SendReadyPacket(); - }; + WorldStreamingInit.LoadingFinished += OnLoaded; TrainStress.globalIgnoreStressCalculation = true; From f18b8702515c18fa0c28b4b58c9b60fbc672c87c Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 24 May 2025 10:48:21 +1000 Subject: [PATCH 323/521] Add cache for future use --- .../Components/Networking/World/NetworkedPitStopStation.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs b/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs index 9ae08a5b..24a91eda 100644 --- a/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs +++ b/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs @@ -21,6 +21,7 @@ namespace Multiplayer.Components.Networking.World; public class NetworkedPitStopStation : IdMonoBehaviour { #region Lookup Cache + private static readonly Dictionary pitStopStationToNetworkedPitStopStation = []; public static bool Get(ushort netId, out NetworkedPitStopStation obj) { @@ -33,7 +34,7 @@ public static void InitialisePitStops() { //Find all pitstop stations that are placed on the map - //sort them by their hierachy path for consistent ordering + //sort them by their hierarchy path for consistent ordering var stations = Resources.FindObjectsOfTypeAll() .Where(p => p.transform.parent != null) .OrderBy(p => p.GetObjectPath(), StringComparer.InvariantCulture) @@ -46,6 +47,8 @@ public static void InitialisePitStops() var netStation = station.GetOrAddComponent(); netStation.Station = station; + pitStopStationToNetworkedPitStopStation[station] = netStation; + Multiplayer.LogDebug(() => $"InitialisePitStops() Station: {station?.GetObjectPath()}, netId: {netStation.NetId}"); CoroutineManager.Instance.StartCoroutine(netStation.Init()); @@ -134,7 +137,7 @@ protected void OnDisable() protected override void OnDestroy() { - if (UnloadWatcher.isUnloading) + pitStopStationToNetworkedPitStopStation.Remove(Station); if (carSelectorGrab != null) { From a2d0dccfc7874fd9f1159eaa858fd900492f88db Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 24 May 2025 11:23:33 +1000 Subject: [PATCH 324/521] Use static netId allocation for CashRegisterWithModules --- .../World/NetworkedCashRegisterWithModules.cs | 39 ++++++++++++++++++- .../Managers/Client/NetworkClient.cs | 2 + .../Managers/Server/NetworkServer.cs | 3 +- .../World/CashRegisterWithModulesPatch.cs | 12 +++--- 4 files changed, 47 insertions(+), 9 deletions(-) diff --git a/Multiplayer/Components/Networking/World/NetworkedCashRegisterWithModules.cs b/Multiplayer/Components/Networking/World/NetworkedCashRegisterWithModules.cs index 224b762a..1dbd25da 100644 --- a/Multiplayer/Components/Networking/World/NetworkedCashRegisterWithModules.cs +++ b/Multiplayer/Components/Networking/World/NetworkedCashRegisterWithModules.cs @@ -1,7 +1,8 @@ +using DV.CashRegister; +using Multiplayer.Utils; using System; using System.Collections.Generic; using System.Linq; -using System.Text; using UnityEngine; namespace Multiplayer.Components.Networking.World; @@ -9,6 +10,7 @@ namespace Multiplayer.Components.Networking.World; public class NetworkedCashRegisterWithModules : IdMonoBehaviour { #region Lookup Cache + private static readonly Dictionary cashRegisterToNetworkedCashRegister = []; public static bool Get(ushort netId, out NetworkedCashRegisterWithModules obj) { @@ -17,9 +19,42 @@ public static bool Get(ushort netId, out NetworkedCashRegisterWithModules obj) return b; } + public static void InitialiseCashRegisters() + { + + //Find all CashRegistersWithModules that are placed on the map + //sort them by their hierarchy path for consistent ordering + var registers = Resources.FindObjectsOfTypeAll() + .Where(p => p.transform.parent != null) + .OrderBy(p => p.GetObjectPath(), StringComparer.InvariantCulture) + .ToArray(); + + Multiplayer.LogDebug(() => $"InitialiseCashRegisters() Found: {registers?.Length}"); + + foreach (var register in registers) + { + var netRegister = register.GetOrAddComponent(); + netRegister.Register = register; + + cashRegisterToNetworkedCashRegister[register] = netRegister; + + Multiplayer.LogDebug(() => $"InitialiseCashRegisters() Register: {register?.GetObjectPath()}, netId: {netRegister.NetId}"); + } + } + #endregion + + protected override bool IsIdServerAuthoritative => false; + + #region Server Variables #endregion - protected override bool IsIdServerAuthoritative => true; + #region Client Variables + + #endregion + + #region Common Variables + CashRegisterWithModules Register; + #endregion } diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 6cb82098..5524848e 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -187,6 +187,8 @@ private void OnLoaded() NetworkedItemManager.Instance.CacheWorldItems(); Log($"WorldStreamingInit.LoadingFinished() InitialisePitStops()"); NetworkedPitStopStation.InitialisePitStops(); + Log($"WorldStreamingInit.LoadingFinished() InitialiseCashRegisters()"); + NetworkedCashRegisterWithModules.InitialiseCashRegisters(); Log($"WorldStreamingInit.LoadingFinished() SendReadyPacket()"); SendReadyPacket(); diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index 2da36cfe..68bf34f6 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -170,7 +170,8 @@ private void OnLoaded() IsLoaded = true; //We should initialise object here for dedicated servers, rather than relying on the existance of a client - NetworkedPitStopStation.InitialisePitStops(); //trigger cache build + NetworkedPitStopStation.InitialisePitStops(); + NetworkedCashRegisterWithModules.InitialiseCashRegisters(); while (joinQueue.Count > 0) { diff --git a/Multiplayer/Patches/World/CashRegisterWithModulesPatch.cs b/Multiplayer/Patches/World/CashRegisterWithModulesPatch.cs index 617d41e7..90ef4640 100644 --- a/Multiplayer/Patches/World/CashRegisterWithModulesPatch.cs +++ b/Multiplayer/Patches/World/CashRegisterWithModulesPatch.cs @@ -8,10 +8,10 @@ namespace Multiplayer.Patches.World; [HarmonyPatch(typeof(CashRegisterWithModules))] public class CashRegisterWithModulesPatch { - [HarmonyPostfix] - [HarmonyPatch(nameof(CashRegisterWithModules.Awake))] - private static void Awake(CashRegisterWithModules __instance) - { - __instance.GetOrAddComponent(); - } + //[HarmonyPostfix] + //[HarmonyPatch(nameof(CashRegisterWithModules.Awake))] + //private static void Awake(CashRegisterWithModules __instance) + //{ + // __instance.GetOrAddComponent(); + //} } From bc2f659b65edef88f5141526de4bdbbb00c299bc Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 25 May 2025 11:35:50 +1000 Subject: [PATCH 325/521] Add RPC timeout calc --- 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 5524848e..f00248e9 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -58,6 +58,7 @@ public class NetworkClient : NetworkManager // One way ping in milliseconds public int Ping { get; private set; } private ITransportPeer serverPeer; + public float RPC_Timeout => (Ping * 4f) / 1000; private ChatGUI chatGUI; private readonly bool isSinglePlayer; From bebfc90c558668b84a46a067e2b67e29cd36a5c7 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 25 May 2025 11:36:46 +1000 Subject: [PATCH 326/521] Store peer reference in ServerPlayer --- Multiplayer/Networking/Data/ServerPlayer.cs | 11 +++++++++++ .../Networking/Managers/Server/NetworkServer.cs | 14 +++++++------- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/Multiplayer/Networking/Data/ServerPlayer.cs b/Multiplayer/Networking/Data/ServerPlayer.cs index 72c4d7e7..2a36d512 100644 --- a/Multiplayer/Networking/Data/ServerPlayer.cs +++ b/Multiplayer/Networking/Data/ServerPlayer.cs @@ -3,12 +3,14 @@ using Multiplayer.Components.Networking; using Multiplayer.Components.Networking.Train; using Multiplayer.Components.Networking.World; +using Multiplayer.Networking.TransportLayers; using UnityEngine; namespace Multiplayer.Networking.Data; public class ServerPlayer { + public ITransportPeer Peer { get; private set; } public byte Id { get; set; } public bool IsLoaded { get; set; } public string Username { get; set; } @@ -26,6 +28,15 @@ public class ServerPlayer private Vector3 _lastWorldPos = Vector3.zero; private Vector3 _lastAbsoluteWorldPosition = Vector3.zero; + public ServerPlayer(ITransportPeer peer, byte id,string username, string originalUsername, Guid guid) + { + Peer = peer; + Id = id; + Username = username; + OriginalUsername = originalUsername; + Guid = guid; + } + #region Positioning public Vector3 AbsoluteWorldPosition { diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index 68bf34f6..d6560e26 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -719,13 +719,13 @@ private void OnServerboundClientLoginPacket(ServerboundClientLoginPacket packet, ITransportPeer peer = request.Accept(); - ServerPlayer serverPlayer = new() - { - Id = (byte)peer.Id, - Username = overrideUsername, - OriginalUsername = packet.Username, - Guid = guid - }; + ServerPlayer serverPlayer = new( + peer, + (byte)peer.Id, + overrideUsername, + packet.Username, + guid + ); serverPlayers.Add(serverPlayer.Id, serverPlayer); From f8a7d57fab1728cfbc0aed16086d4b65b303a438 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 25 May 2025 11:41:15 +1000 Subject: [PATCH 327/521] Intercept CashRegisterWithModules interaction --- .../World/NetworkedCashRegisterWithModules.cs | 84 ++++++++++++++++++- .../Managers/Client/NetworkClient.cs | 13 +++ .../World/CashRegisterWithModulesPatch.cs | 54 ++++++++++-- 3 files changed, 144 insertions(+), 7 deletions(-) diff --git a/Multiplayer/Components/Networking/World/NetworkedCashRegisterWithModules.cs b/Multiplayer/Components/Networking/World/NetworkedCashRegisterWithModules.cs index 1dbd25da..b61be181 100644 --- a/Multiplayer/Components/Networking/World/NetworkedCashRegisterWithModules.cs +++ b/Multiplayer/Components/Networking/World/NetworkedCashRegisterWithModules.cs @@ -1,4 +1,8 @@ using DV.CashRegister; +using DV.Interaction; +using DV.InventorySystem; +using Multiplayer.Networking.Data; +using Multiplayer.Networking.Packets.Common; using Multiplayer.Utils; using System; using System.Collections.Generic; @@ -51,10 +55,88 @@ public static void InitialiseCashRegisters() #endregion #region Client Variables + bool isBuying; + bool buyAccepted; + + bool isCancelling; + bool cancelAccepted; #endregion #region Common Variables - CashRegisterWithModules Register; + CashRegisterWithModules CashRegister; + #endregion + + public IEnumerator Buy() + { + if (isBuying || isCancelling) + yield break; + + DisableInteraction(); + + NetworkLifecycle.Instance.Client.SendCashRegisterAction(NetId, CashRegisterAction.Buy); + + isBuying = true; + buyAccepted = false; + float timeOut = Time.time + NetworkLifecycle.Instance.Client.RPC_Timeout; + + yield return new WaitUntil(() => Time.time >= timeOut || isBuying == false); + + if (!buyAccepted) + CashRegister?.cancelAudio?.Play(transform.position, 1f, 1f, 0f, 1f, 500f, default, null, transform, false, 0f, null); + + isBuying = false; + buyAccepted = false; + + EnableInteraction(); + } + + public IEnumerator Cancel() + { + if (isBuying || isCancelling) + yield break; + + DisableInteraction(); + + NetworkLifecycle.Instance.Client.SendCashRegisterAction(NetId, CashRegisterAction.Cancel); + isCancelling = true; + cancelAccepted = false; + float timeOut = Time.time + NetworkLifecycle.Instance.Client.RPC_Timeout; + + yield return new WaitUntil(() => Time.time >= timeOut || isCancelling == false); + + if (cancelAccepted) + CashRegister?.cancelAudio?.Play(transform.position, 1f, 1f, 0f, 1f, 500f, default, null, transform, false, 0f, null); + + isCancelling = false; + cancelAccepted = false; + + EnableInteraction(); + } + + public void SetCash() + { + if (isBuying || isCancelling) + return; + + NetworkLifecycle.Instance.Client.SendCashRegisterAction(NetId, CashRegisterAction.SetFunds, CashRegister.DepositedCash); + } + + private void DisableInteraction() + { + CashRegister.buyButton.InteractionAllowed = false; + CashRegister.cancelButton.InteractionAllowed = false; + } + + private void EnableInteraction() + { + CashRegister.buyButton.InteractionAllowed = true; + CashRegister.cancelButton.InteractionAllowed = true; + } + + #endregion + + #region Common + #endregion } diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index f00248e9..af1b7b41 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -1423,5 +1423,18 @@ public void SendPaintThemeChangePacket(ushort netId, byte targetArea, sbyte them SendPacketToServer(new CommonPaintThemePacket { NetId = netId, TargetArea = targetArea, PaintThemeId = themeIndex }, DeliveryMethod.ReliableUnordered); } + public void SendCashRegisterAction(ushort netId, CashRegisterAction action, double amount = 0.0f) + { + SendPacketToServer( + new CommonCashRegisterWithModulesActionPacket + { + NetId = netId, + Action = action, + Amount = amount + }, + DeliveryMethod.ReliableOrdered + ); + } + #endregion } diff --git a/Multiplayer/Patches/World/CashRegisterWithModulesPatch.cs b/Multiplayer/Patches/World/CashRegisterWithModulesPatch.cs index 90ef4640..1497197d 100644 --- a/Multiplayer/Patches/World/CashRegisterWithModulesPatch.cs +++ b/Multiplayer/Patches/World/CashRegisterWithModulesPatch.cs @@ -1,5 +1,6 @@ using DV.CashRegister; using HarmonyLib; +using Multiplayer.Components.Networking; using Multiplayer.Components.Networking.World; using Multiplayer.Utils; @@ -8,10 +9,51 @@ namespace Multiplayer.Patches.World; [HarmonyPatch(typeof(CashRegisterWithModules))] public class CashRegisterWithModulesPatch { - //[HarmonyPostfix] - //[HarmonyPatch(nameof(CashRegisterWithModules.Awake))] - //private static void Awake(CashRegisterWithModules __instance) - //{ - // __instance.GetOrAddComponent(); - //} + [HarmonyPrefix] + [HarmonyPatch(nameof(CashRegisterWithModules.OnBuyPressed))] + private static bool OnBuyPressed(CashRegisterWithModules __instance) + { + if (NetworkLifecycle.Instance.IsHost()) + return true; + + if (!NetworkedCashRegisterWithModules.TryGet(__instance, out var netCashRegister)) + { + Multiplayer.LogWarning($"CashRegisterWithModules.OnBuyPressed({__instance.GetObjectPath()}) NetworkedCashRegisterWithModules not found!"); + return false; + } + + CoroutineManager.Instance.StartCoroutine(netCashRegister.Buy()); + + return false; + } + + [HarmonyPrefix] + [HarmonyPatch(nameof(CashRegisterWithModules.Cancel))] + private static bool Cancel(CashRegisterWithModules __instance) + { + if (NetworkLifecycle.Instance.IsHost()) + return true; + + if (!NetworkedCashRegisterWithModules.TryGet(__instance, out var netCashRegister)) + { + Multiplayer.LogWarning($"CashRegisterWithModules.Cancel({__instance.GetObjectPath()}) NetworkedCashRegisterWithModules not found!"); + return false; + } + + CoroutineManager.Instance.StartCoroutine(netCashRegister.Cancel()); + + return false; + } + + [HarmonyPostfix] + [HarmonyPatch(nameof(CashRegisterWithModules.SetCash))] + private static void SetCash(CashRegisterWithModules __instance) + { + Multiplayer.LogDebug(() => $"SetCash() {__instance.GetObjectPath()}, Deposited: {__instance.DepositedCash}"); + + if (!NetworkedCashRegisterWithModules.TryGet(__instance, out var netCashRegister)) + Multiplayer.LogWarning($"CashRegisterWithModules.SetCash({__instance.GetObjectPath()}) NetworkedCashRegisterWithModules not found!"); + else + netCashRegister.SetCash(); + } } From 369bb0f3ccd00cdc28ff0b3ee3f5bc014a8fc811 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 25 May 2025 11:42:36 +1000 Subject: [PATCH 328/521] Add CashRegister action packet handling --- .../World/NetworkedCashRegisterWithModules.cs | 139 +++++++++++++++++- .../Managers/Client/NetworkClient.cs | 17 +++ .../Managers/Server/NetworkServer.cs | 30 ++++ ...mmonCashRegisterWithModulesActionPacket.cs | 22 +++ 4 files changed, 204 insertions(+), 4 deletions(-) create mode 100644 Multiplayer/Networking/Packets/Common/CommonCashRegisterWithModulesActionPacket.cs diff --git a/Multiplayer/Components/Networking/World/NetworkedCashRegisterWithModules.cs b/Multiplayer/Components/Networking/World/NetworkedCashRegisterWithModules.cs index b61be181..e2dff2ea 100644 --- a/Multiplayer/Components/Networking/World/NetworkedCashRegisterWithModules.cs +++ b/Multiplayer/Components/Networking/World/NetworkedCashRegisterWithModules.cs @@ -5,6 +5,7 @@ using Multiplayer.Networking.Packets.Common; using Multiplayer.Utils; using System; +using System.Collections; using System.Collections.Generic; using System.Linq; using UnityEngine; @@ -23,14 +24,19 @@ public static bool Get(ushort netId, out NetworkedCashRegisterWithModules obj) return b; } + public static bool TryGet(CashRegisterWithModules cashRegister, out NetworkedCashRegisterWithModules networkedCashRegisterWithModules) + { + return cashRegisterToNetworkedCashRegister.TryGetValue(cashRegister, out networkedCashRegisterWithModules); + } + public static void InitialiseCashRegisters() { //Find all CashRegistersWithModules that are placed on the map //sort them by their hierarchy path for consistent ordering - var registers = Resources.FindObjectsOfTypeAll() - .Where(p => p.transform.parent != null) - .OrderBy(p => p.GetObjectPath(), StringComparer.InvariantCulture) + var registers = CashRegisterBase.allCashRegisters + .OfType() + .OrderBy(r => r.GetObjectPath(), StringComparer.InvariantCulture) .ToArray(); Multiplayer.LogDebug(() => $"InitialiseCashRegisters() Found: {registers?.Length}"); @@ -38,7 +44,7 @@ public static void InitialiseCashRegisters() foreach (var register in registers) { var netRegister = register.GetOrAddComponent(); - netRegister.Register = register; + netRegister.CashRegister = register; cashRegisterToNetworkedCashRegister[register] = netRegister; @@ -67,6 +73,131 @@ public static void InitialiseCashRegisters() CashRegisterWithModules CashRegister; #endregion + #region Unity + protected override void OnDestroy() + { + cashRegisterToNetworkedCashRegister.Remove(CashRegister); + base.OnDestroy(); + } + #endregion + + #region Server + public void Server_ProcessCashRegisterAction(ServerPlayer player, CommonCashRegisterWithModulesActionPacket packet) + { + float sqrDistance = (player.WorldPosition - transform.position).sqrMagnitude; + bool success = false; + + NetworkLifecycle.Instance.Server?.LogDebug(() => $"Server_ProcessAction({player.Username}, {packet.Action}, {packet.Amount})"); + + if (sqrDistance > GrabberRaycasterDV.FPS_INTERACTION_RANGE_SQR) + { + NetworkLifecycle.Instance.Server?.LogDebug(() => $"Server_ProcessAction({player.Username}, {packet.Action}, {packet.Amount}) {CashRegister.GetObjectPath()}. Player too far! Player pos: {player.WorldPosition}, register pos: {transform.position}, sqrMag: {sqrDistance}"); + return; + } + + switch (packet.Action) + { + case CashRegisterAction.Cancel: + if (CashRegister.cancelButton.InteractionAllowed) + { + CashRegister?.Cancel(); + success = true; + } + + break; + + case CashRegisterAction.Buy: + if (CashRegister.buyButton.InteractionAllowed) + success = CashRegister?.Buy() ?? false; + + break; + + case CashRegisterAction.SetFunds: + double spend = 0; + + NetworkLifecycle.Instance.Server?.LogDebug(() => $"Server_ProcessAction({player.Username}, {packet.Action}, {packet.Amount}) Wallet: {Inventory.Instance.PlayerMoney}"); + if (packet.Amount > 0) + { + if (Inventory.Instance.PlayerMoney >= packet.Amount) + spend = packet.Amount; + else + spend = Inventory.Instance.PlayerMoney; + + success = Inventory.Instance.RemoveMoney(spend); + + if(success) + CashRegister?.AddCash(spend); + } + else + { + NetworkLifecycle.Instance.Server?.LogDebug(() => $"Server_ProcessAction({player.Username}, {packet.Action}, {packet.Amount}) amount negative!"); + } + break; + } + + if (success) + NetworkLifecycle.Instance.Server.SendCashRegisterAction(packet); + else + NetworkLifecycle.Instance.Server.SendCashRegisterAction + ( + new CommonCashRegisterWithModulesActionPacket + { + NetId = NetId, + Action = CashRegisterAction.Reject, + Amount = CashRegister.DepositedCash + }, + player.Peer + ); + } + + #endregion + + #region Client + + public void Client_ProcessCashRegisterAction(CashRegisterAction action, double amount) + { + switch (action) + { + case CashRegisterAction.Cancel: + + if (isCancelling) + cancelAccepted = true; + + isCancelling = false; + isBuying = false; + + CashRegister?.cancelAudio?.Play(transform.position, 1f, 1f, 0f, 1f, 500f, default, null, transform, false, 0f, null); + + break; + + case CashRegisterAction.Buy: + + if (isBuying) + buyAccepted = true; + + isCancelling = false; + isBuying = false; + + CashRegister?.buyAudio?.Play(transform.position, 1f, 1f, 0f, 1f, 500f, default, null, transform, false, 0f, null); + + break; + + case CashRegisterAction.SetFunds: + + if (CashRegister.DepositedCash == 0 && amount > 0) + { + + } + break; + + case CashRegisterAction.Reject: + isBuying = false; + isCancelling = false; + + break; + } + } + public IEnumerator Buy() { if (isBuying || isCancelling) diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index af1b7b41..02654f22 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -178,6 +178,10 @@ protected override void Subscribe() netPacketProcessor.SubscribeReusable(OnCommonPitStopInteractionPacket); netPacketProcessor.SubscribeReusable(OnCommonPitStopPlugInteractionPacket); netPacketProcessor.SubscribeReusable(OnClientboundPitStopBulkUpdatePacket); + + netPacketProcessor.SubscribeReusable(OnCommonCashRegisterWithModulesActionPacket); + + } private void OnLoaded() @@ -1060,6 +1064,19 @@ private void OnCommonPaintThemePacket(CommonPaintThemePacket packet) netTrainCar?.Common_ReceivePaintThemeUpdate((TrainCarPaint.Target)packet.TargetArea, paint); } + private void OnCommonCashRegisterWithModulesActionPacket(CommonCashRegisterWithModulesActionPacket packet) + { + if (!NetworkedCashRegisterWithModules.Get(packet.NetId, out NetworkedCashRegisterWithModules netCashRegister)) + { + LogWarning($"Cash Register With Modules Action received for netId: {packet.NetId}, but cash register does not exist!"); + return; + } + + Log($"Cash Register With Modules Action received for {netCashRegister.GetObjectPath()}, Action: {packet.Action}, Amount: {packet.Amount}"); + + netCashRegister.Client_ProcessCashRegisterAction(packet.Action, packet.Amount); + } + #endregion #region Senders diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index d6560e26..84da69a7 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -157,6 +157,8 @@ protected override void Subscribe() netPacketProcessor.SubscribeReusable(OnCommonPitStopInteractionPacket); netPacketProcessor.SubscribeReusable(OnCommonPitStopPlugInteractionPacket); + + netPacketProcessor.SubscribeReusable(OnCommonCashRegisterWithModulesActionPacket); } private void OnLoaded() @@ -602,6 +604,14 @@ public void SendPitStopInteractionPacket(ITransportPeer peer, CommonPitStopInter SendPacket(peer, packet, DeliveryMethod.ReliableOrdered); } + public void SendCashRegisterAction(CommonCashRegisterWithModulesActionPacket packet, ITransportPeer peer = null) + { + if (peer == null) + SendPacketToAll(packet, DeliveryMethod.ReliableOrdered, SelfPeer); + else + SendPacket(peer, packet, DeliveryMethod.ReliableOrdered); + } + public void SendChat(string message, ITransportPeer exclude = null) { @@ -1288,5 +1298,25 @@ private void OnCommonItemChangePacket(CommonItemChangePacket packet, ITransportP //NetworkedItemManager.Instance.ReceiveSnapshots(packet.Items, player); } + + private void OnCommonCashRegisterWithModulesActionPacket(CommonCashRegisterWithModulesActionPacket packet, ITransportPeer peer) + { + if (TryGetServerPlayer(peer, out var player)) + { + LogWarning($"Cash Register With Modules Action received, but player was not found"); + return; + } + + if (!NetworkedCashRegisterWithModules.Get(packet.NetId, out NetworkedCashRegisterWithModules netCashRegister)) + { + LogWarning($"Cash Register With Modules Action received for netId: {packet.NetId}, but cash register does not exist!"); + return; + } + + Log($"Cash Register With Modules Action received for {netCashRegister.GetObjectPath()}, Action: {packet.Action}, Amount: {packet.Amount}"); + netCashRegister.Server_ProcessCashRegisterAction(player, packet); + + + } #endregion } diff --git a/Multiplayer/Networking/Packets/Common/CommonCashRegisterWithModulesActionPacket.cs b/Multiplayer/Networking/Packets/Common/CommonCashRegisterWithModulesActionPacket.cs new file mode 100644 index 00000000..7d5942ea --- /dev/null +++ b/Multiplayer/Networking/Packets/Common/CommonCashRegisterWithModulesActionPacket.cs @@ -0,0 +1,22 @@ +using Multiplayer.Networking.Data; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Multiplayer.Networking.Packets.Common; + +public enum CashRegisterAction : byte +{ + Cancel, + Buy, + SetFunds, + Reject, + Approve +} +public class CommonCashRegisterWithModulesActionPacket +{ + public ushort NetId { get; set; } + public CashRegisterAction Action { get; set; } + public double Amount { get; set; } +} From 93d2890163263d0b21e0eb85d62b5d946ae3f9ea Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 25 May 2025 11:43:02 +1000 Subject: [PATCH 329/521] Block pitstop interaction packets from being sent to host --- Multiplayer/Networking/Managers/Server/NetworkServer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index 84da69a7..53cd8122 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -592,7 +592,7 @@ public void SendPitStopBulkDataPacket(ushort netId, int carCount, LocoResourceMo }; if (peer == null) - SendPacketToAll(packet, DeliveryMethod.ReliableOrdered); + SendPacketToAll(packet, DeliveryMethod.ReliableOrdered, SelfPeer); else SendPacket(peer, packet, DeliveryMethod.ReliableOrdered); } From 25b0f4cd461630b938eabe08050bd2cbf023e146 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 25 May 2025 12:49:48 +1000 Subject: [PATCH 330/521] Fix client not activating objects --- .../World/NetworkedCashRegisterWithModules.cs | 18 ++++++++++++++++-- .../World/NetworkedPitStopStation.cs | 10 ++++++++-- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/Multiplayer/Components/Networking/World/NetworkedCashRegisterWithModules.cs b/Multiplayer/Components/Networking/World/NetworkedCashRegisterWithModules.cs index e2dff2ea..36289ac3 100644 --- a/Multiplayer/Components/Networking/World/NetworkedCashRegisterWithModules.cs +++ b/Multiplayer/Components/Networking/World/NetworkedCashRegisterWithModules.cs @@ -36,7 +36,9 @@ public static void InitialiseCashRegisters() //sort them by their hierarchy path for consistent ordering var registers = CashRegisterBase.allCashRegisters .OfType() - .OrderBy(r => r.GetObjectPath(), StringComparer.InvariantCulture) + .OrderBy(r => r.transform.position.x) + .ThenBy(r => r.transform.position.y) + .ThenBy(r => r.transform.position.z) .ToArray(); Multiplayer.LogDebug(() => $"InitialiseCashRegisters() Found: {registers?.Length}"); @@ -46,6 +48,9 @@ public static void InitialiseCashRegisters() var netRegister = register.GetOrAddComponent(); netRegister.CashRegister = register; + if (netRegister.NetId == 0) + netRegister.Awake(); + cashRegisterToNetworkedCashRegister[register] = netRegister; Multiplayer.LogDebug(() => $"InitialiseCashRegisters() Register: {register?.GetObjectPath()}, netId: {netRegister.NetId}"); @@ -70,10 +75,19 @@ public static void InitialiseCashRegisters() #endregion #region Common Variables - CashRegisterWithModules CashRegister; + CashRegisterWithModules CashRegister; #endregion #region Unity + + protected override void Awake() + { + Multiplayer.LogDebug(()=>$"CashRegisterWithModules.Awake() {transform.GetObjectPath()}, {transform.position}, netId: {NetId}"); + + if (NetId == 0) + base.Awake(); + } + protected override void OnDestroy() { cashRegisterToNetworkedCashRegister.Remove(CashRegister); diff --git a/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs b/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs index 24a91eda..bb597a12 100644 --- a/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs +++ b/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs @@ -37,7 +37,9 @@ public static void InitialisePitStops() //sort them by their hierarchy path for consistent ordering var stations = Resources.FindObjectsOfTypeAll() .Where(p => p.transform.parent != null) - .OrderBy(p => p.GetObjectPath(), StringComparer.InvariantCulture) + .OrderBy(p => p.transform.position.x) + .ThenBy(p => p.transform.position.y) + .ThenBy(p => p.transform.position.z) .ToArray(); Multiplayer.LogDebug(() => $"InitialisePitStops() Found: {stations?.Length}"); @@ -47,6 +49,9 @@ public static void InitialisePitStops() var netStation = station.GetOrAddComponent(); netStation.Station = station; + if (netStation.NetId == 0) + netStation.Awake(); + pitStopStationToNetworkedPitStopStation[station] = netStation; Multiplayer.LogDebug(() => $"InitialisePitStops() Station: {station?.GetObjectPath()}, netId: {netStation.NetId}"); @@ -110,7 +115,8 @@ public static void InitialisePitStops() #region Unity protected override void Awake() { - base.Awake(); + if (NetId == 0) + base.Awake(); StationName = $"{transform.parent.parent.name} - {transform.parent.name}"; From ab9ee56ecbd79ecdd3de0452816a9aaa6df3688a Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 25 May 2025 12:50:12 +1000 Subject: [PATCH 331/521] Fix CashRegister SetCash patch --- .../World/NetworkedCashRegisterWithModules.cs | 6 +++--- .../World/CashRegisterWithModulesPatch.cs | 19 +++++++++++++------ 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/Multiplayer/Components/Networking/World/NetworkedCashRegisterWithModules.cs b/Multiplayer/Components/Networking/World/NetworkedCashRegisterWithModules.cs index 36289ac3..f6b7aa63 100644 --- a/Multiplayer/Components/Networking/World/NetworkedCashRegisterWithModules.cs +++ b/Multiplayer/Components/Networking/World/NetworkedCashRegisterWithModules.cs @@ -259,12 +259,12 @@ public IEnumerator Cancel() EnableInteraction(); } - public void SetCash() + public void SetCash(double amount) { - if (isBuying || isCancelling) + if (isBuying || isCancelling || NetworkLifecycle.Instance.IsProcessingPacket) return; - NetworkLifecycle.Instance.Client.SendCashRegisterAction(NetId, CashRegisterAction.SetFunds, CashRegister.DepositedCash); + NetworkLifecycle.Instance.Client.SendCashRegisterAction(NetId, CashRegisterAction.SetFunds, amount); } private void DisableInteraction() diff --git a/Multiplayer/Patches/World/CashRegisterWithModulesPatch.cs b/Multiplayer/Patches/World/CashRegisterWithModulesPatch.cs index 1497197d..54ce7cfc 100644 --- a/Multiplayer/Patches/World/CashRegisterWithModulesPatch.cs +++ b/Multiplayer/Patches/World/CashRegisterWithModulesPatch.cs @@ -44,16 +44,23 @@ private static bool Cancel(CashRegisterWithModules __instance) return false; } +} +[HarmonyPatch(typeof(CashRegisterBase))] +public class CashRegisterBasePatch +{ [HarmonyPostfix] - [HarmonyPatch(nameof(CashRegisterWithModules.SetCash))] - private static void SetCash(CashRegisterWithModules __instance) + [HarmonyPatch(nameof(CashRegisterBase.SetCash))] + private static void SetCash(CashRegisterBase __instance, double amount) { - Multiplayer.LogDebug(() => $"SetCash() {__instance.GetObjectPath()}, Deposited: {__instance.DepositedCash}"); + if (__instance is not CashRegisterWithModules cashRegisterWithModules) + return; - if (!NetworkedCashRegisterWithModules.TryGet(__instance, out var netCashRegister)) - Multiplayer.LogWarning($"CashRegisterWithModules.SetCash({__instance.GetObjectPath()}) NetworkedCashRegisterWithModules not found!"); + Multiplayer.LogDebug(() => $"SetCash() {__instance.GetObjectPath()}, Deposited: {amount}"); + + if (!NetworkedCashRegisterWithModules.TryGet(cashRegisterWithModules, out var netCashRegister)) + Multiplayer.LogWarning($"CashRegisterWithModules.SetCash({cashRegisterWithModules.GetObjectPath()}) NetworkedCashRegisterWithModules not found!"); else - netCashRegister.SetCash(); + netCashRegister.SetCash(amount); } } From c481b81a2687fc6016a34c01dc232d319f157e62 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 31 May 2025 00:29:20 +1000 Subject: [PATCH 332/521] Initial commit Beginnings of a structure for an API package --- Multiplayer.sln | 24 +++++-- Multiplayer/API/APIProvider.cs | 23 +++++++ Multiplayer/Multiplayer.cs | 17 +++-- Multiplayer/Multiplayer.csproj | 1 + MultiplayerAPI/Interfaces/IClient.cs | 13 ++++ MultiplayerAPI/Interfaces/IMultiplayerAPI.cs | 35 +++++++++++ MultiplayerAPI/Interfaces/IPlayer.cs | 12 ++++ MultiplayerAPI/Interfaces/IServer.cs | 15 +++++ MultiplayerAPI/MultiplayerAPI.cs | 66 ++++++++++++++++++++ MultiplayerAPI/MultiplayerAPI.csproj | 37 +++++++++++ 10 files changed, 233 insertions(+), 10 deletions(-) create mode 100644 Multiplayer/API/APIProvider.cs create mode 100644 MultiplayerAPI/Interfaces/IClient.cs create mode 100644 MultiplayerAPI/Interfaces/IMultiplayerAPI.cs create mode 100644 MultiplayerAPI/Interfaces/IPlayer.cs create mode 100644 MultiplayerAPI/Interfaces/IServer.cs create mode 100644 MultiplayerAPI/MultiplayerAPI.cs create mode 100644 MultiplayerAPI/MultiplayerAPI.csproj diff --git a/Multiplayer.sln b/Multiplayer.sln index cb957169..a5865858 100644 --- a/Multiplayer.sln +++ b/Multiplayer.sln @@ -1,16 +1,30 @@ - Microsoft Visual Studio Solution File, Format Version 12.00 -Project("{99D6F2A9-1021-4EA5-ACB3-48CBD6FB8D09}") = "Multiplayer", "Multiplayer/Multiplayer.csproj", "{F712C7FB-EEAE-4036-A938-356E022B0455}" +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36121.58 d17.14 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Multiplayer", "Multiplayer\Multiplayer.csproj", "{F712C7FB-EEAE-4036-A938-356E022B0455}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MultiplayerAPI", "MultiplayerAPI\MultiplayerAPI.csproj", "{AB0CA646-6E23-42EC-9F24-176CC0331714}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution - Release|Any CPU = Release|Any CPU Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {F712C7FB-EEAE-4036-A938-356E022B0455}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F712C7FB-EEAE-4036-A938-356E022B0455}.Release|Any CPU.Build.0 = Release|Any CPU {F712C7FB-EEAE-4036-A938-356E022B0455}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F712C7FB-EEAE-4036-A938-356E022B0455}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F712C7FB-EEAE-4036-A938-356E022B0455}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F712C7FB-EEAE-4036-A938-356E022B0455}.Release|Any CPU.Build.0 = Release|Any CPU + {AB0CA646-6E23-42EC-9F24-176CC0331714}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AB0CA646-6E23-42EC-9F24-176CC0331714}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AB0CA646-6E23-42EC-9F24-176CC0331714}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AB0CA646-6E23-42EC-9F24-176CC0331714}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {C12E6469-93BC-497D-BEF6-BD6603B6A89C} EndGlobalSection EndGlobal diff --git a/Multiplayer/API/APIProvider.cs b/Multiplayer/API/APIProvider.cs new file mode 100644 index 00000000..177c77d2 --- /dev/null +++ b/Multiplayer/API/APIProvider.cs @@ -0,0 +1,23 @@ +using MPAPI.Interfaces; +using Multiplayer.Components.Networking; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Multiplayer.API +{ + public class APIProvider : IMultiplayerAPI + { + public bool IsMultiplayerLoaded => true; + + public bool IsConnected => NetworkLifecycle.Instance.IsClientRunning || NetworkLifecycle.Instance.IsServerRunning; + + public bool IsHost => NetworkLifecycle.Instance.IsHost(); + + public bool IsDedicatedServer => throw new NotImplementedException(); + + public bool IsSinglePlayer => NetworkLifecycle.Instance.IsServerRunning && (NetworkLifecycle.Instance?.Server.IsSinglePlayer ?? false); + } +} diff --git a/Multiplayer/Multiplayer.cs b/Multiplayer/Multiplayer.cs index db32a554..47601621 100644 --- a/Multiplayer/Multiplayer.cs +++ b/Multiplayer/Multiplayer.cs @@ -1,17 +1,19 @@ -using System; -using System.IO; -using System.Linq; -using System.Reflection; using DV; using DV.UIFramework; using HarmonyLib; using JetBrains.Annotations; using LiteNetLib; +using MPAPI; +using Multiplayer.API; using Multiplayer.Components.MainMenu; using Multiplayer.Components.Networking; using Multiplayer.Editor; using Multiplayer.Patches.Mods; using Multiplayer.Patches.World; +using System; +using System.IO; +using System.Linq; +using System.Reflection; using UnityChan; using UnityEngine; using UnityModManagerNet; @@ -24,6 +26,7 @@ public static class Multiplayer public static UnityModManager.ModEntry ModEntry; public static Settings Settings; + private static APIProvider _apiProvider; private static AssetBundle assetBundle; public static AssetIndex AssetIndex { get; private set; } @@ -44,7 +47,7 @@ public static string Ver { public static bool specLog = false; [UsedImplicitly] - private static bool Load(UnityModManager.ModEntry modEntry) + public static bool Load(UnityModManager.ModEntry modEntry) { ModEntry = modEntry; Settings = Settings.Load(modEntry);//Settings.Load(modEntry); @@ -98,6 +101,10 @@ private static bool Load(UnityModManager.ModEntry modEntry) Log("Creating NetworkManager..."); NetworkLifecycle.CreateLifecycle(); + + Log("Loading API Provider..."); + _apiProvider = new APIProvider(); + MPAPI.MultiplayerAPI.RegisterAPI(_apiProvider); } catch (Exception ex) { diff --git a/Multiplayer/Multiplayer.csproj b/Multiplayer/Multiplayer.csproj index cbe176cc..5ac28d08 100644 --- a/Multiplayer/Multiplayer.csproj +++ b/Multiplayer/Multiplayer.csproj @@ -60,6 +60,7 @@ + ../build/MultiplayerEditor.dll diff --git a/MultiplayerAPI/Interfaces/IClient.cs b/MultiplayerAPI/Interfaces/IClient.cs new file mode 100644 index 00000000..1f8cac24 --- /dev/null +++ b/MultiplayerAPI/Interfaces/IClient.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; + +namespace MPAPI.Interfaces; + +public interface IClient +{ + //public abstract void SendPacketToServer(T packet, DeliveryMethod deliveryMethod) where T : class, new(); + + //public abstract void SendNetSerializablePacketToServer(T packet, DeliveryMethod deliveryMethod) where T : INetSerializable, new(); + + +} diff --git a/MultiplayerAPI/Interfaces/IMultiplayerAPI.cs b/MultiplayerAPI/Interfaces/IMultiplayerAPI.cs new file mode 100644 index 00000000..37494f78 --- /dev/null +++ b/MultiplayerAPI/Interfaces/IMultiplayerAPI.cs @@ -0,0 +1,35 @@ +using System; + +namespace MPAPI.Interfaces; + +/// +/// Main interface for interacting with the Multiplayer mod +/// +public interface IMultiplayerAPI +{ + /// + /// Gets whether the multiplayer mod is currently loaded and active + /// + bool IsMultiplayerLoaded { get; } + + /// + /// Returns true if either a host or client exist + /// + bool IsConnected { get; } + + /// + /// Gets whether this instance is host + /// + bool IsHost { get; } + + /// + /// Gets whether this instance is a dedicated server + /// + bool IsDedicatedServer { get; } + + /// + /// Gets whether this current session is single player + /// + bool IsSinglePlayer { get; } + +} diff --git a/MultiplayerAPI/Interfaces/IPlayer.cs b/MultiplayerAPI/Interfaces/IPlayer.cs new file mode 100644 index 00000000..f383fc7d --- /dev/null +++ b/MultiplayerAPI/Interfaces/IPlayer.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MultiplayerAPI.Interfaces +{ + internal interface IPlayer + { + } +} diff --git a/MultiplayerAPI/Interfaces/IServer.cs b/MultiplayerAPI/Interfaces/IServer.cs new file mode 100644 index 00000000..2b6d003d --- /dev/null +++ b/MultiplayerAPI/Interfaces/IServer.cs @@ -0,0 +1,15 @@ +using System; +using UnityEngine; + + +namespace MPAPI.Interfaces; + +public interface IServer +{ + public event Action OnPlayerConnected; + public event Action OnPlayerDisconnected; + + public abstract float AnyPlayerSqrMag(GameObject item); + + public abstract float AnyPlayerSqrMag(Vector3 anchor); +} diff --git a/MultiplayerAPI/MultiplayerAPI.cs b/MultiplayerAPI/MultiplayerAPI.cs new file mode 100644 index 00000000..3fd2f418 --- /dev/null +++ b/MultiplayerAPI/MultiplayerAPI.cs @@ -0,0 +1,66 @@ +using MPAPI.Interfaces; + +namespace MPAPI; + +public static class MultiplayerAPI +{ + private static IMultiplayerAPI _instance; + private static IServer _server; + private static IClient _client; + + /// + /// Gets whether the Multiplayer mod is available + /// + public static bool IsMultiplayerLoaded => _instance != null; + + /// + /// Gets the current API instance (null if Multiplayer mod is not loaded) + /// + public static IMultiplayerAPI Instance => _instance; + public static IServer Server => _server; + public static IClient Client => _client; + + /// + /// Internal method for the Multiplayer mod to register itself + /// + /// The API implementation + internal static void RegisterAPI(IMultiplayerAPI apiInstance) + { + _instance = apiInstance; + } + + /// + /// Internal method for the Multiplayer mod to register a client instance + /// + /// The Client implementation + internal static void RegisterClient(IClient client) + { + _client = client; + } + + /// + /// Internal method for the Multiplayer mod to deregister a client instance + /// + internal static void ClearClient() + { + _client = null; + } + + /// + /// Internal method for the Multiplayer mod to register a server instance + /// + /// The API implementation + internal static void RegisterServer(IServer server) + { + _server = server; + } + + + /// + /// Internal method for the Multiplayer mod to deregister a server instance + /// + internal static void ClearServer() + { + _server = null; + } +} diff --git a/MultiplayerAPI/MultiplayerAPI.csproj b/MultiplayerAPI/MultiplayerAPI.csproj new file mode 100644 index 00000000..2a007205 --- /dev/null +++ b/MultiplayerAPI/MultiplayerAPI.csproj @@ -0,0 +1,37 @@ + + + + net48 + latest + MPAPI + 0.0.0.0 + true + DVMultiplayerAPI + Macka + API for interfacing with DV Multiplayer mod + + + + + <_Parameter1>Multiplayer + + + + + + + + + + + + + + + + + + + + + From a45b6999a96b16de6bf97267756d3f77aa05067c Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 31 May 2025 10:13:02 +1000 Subject: [PATCH 333/521] begin implementation of server API --- Multiplayer/API/APIProvider.cs | 8 +-- Multiplayer/API/ServerAPIProvider.cs | 64 +++++++++++++++++++ .../Components/Networking/NetworkLifecycle.cs | 16 +++-- .../Networking/Train/NetworkedTrainCar.cs | 4 +- .../Networking/World/NetworkedItemManager.cs | 2 +- .../Managers/Server/NetworkServer.cs | 5 +- MultiplayerAPI/Interfaces/IPlayer.cs | 8 +-- MultiplayerAPI/Interfaces/IServer.cs | 13 ++-- MultiplayerAPI/MultiplayerAPI.cs | 8 +++ MultiplayerAPI/MultiplayerAPI.csproj | 7 ++ 10 files changed, 109 insertions(+), 26 deletions(-) create mode 100644 Multiplayer/API/ServerAPIProvider.cs diff --git a/Multiplayer/API/APIProvider.cs b/Multiplayer/API/APIProvider.cs index 177c77d2..af766731 100644 --- a/Multiplayer/API/APIProvider.cs +++ b/Multiplayer/API/APIProvider.cs @@ -1,10 +1,6 @@ using MPAPI.Interfaces; using Multiplayer.Components.Networking; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; + namespace Multiplayer.API { @@ -16,7 +12,7 @@ public class APIProvider : IMultiplayerAPI public bool IsHost => NetworkLifecycle.Instance.IsHost(); - public bool IsDedicatedServer => throw new NotImplementedException(); + public bool IsDedicatedServer => false; //feature not implemented public bool IsSinglePlayer => NetworkLifecycle.Instance.IsServerRunning && (NetworkLifecycle.Instance?.Server.IsSinglePlayer ?? false); } diff --git a/Multiplayer/API/ServerAPIProvider.cs b/Multiplayer/API/ServerAPIProvider.cs new file mode 100644 index 00000000..cb6771fe --- /dev/null +++ b/Multiplayer/API/ServerAPIProvider.cs @@ -0,0 +1,64 @@ +using MPAPI.Interfaces; +using Multiplayer.Networking.Managers.Server; +using Multiplayer.Utils; +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace Multiplayer.API +{ + public class ServerAPIProvider : IServer + { + private readonly NetworkServer server; + + public event Action OnPlayerConnected; + public event Action OnPlayerDisconnected; + + #region Server Properties + + public int PlayerCount => server.PlayerCount; + + //public IReadOnlyCollection Players => server.ServerPlayers; + + #endregion + + #region Server Util + public float AnyPlayerSqrMag(GameObject item) => DvExtensions.AnyPlayerSqrMag(item); + + public float AnyPlayerSqrMag(Vector3 anchor) => DvExtensions.AnyPlayerSqrMag(anchor); + #endregion + + + #region Class Helpers + internal ServerAPIProvider(NetworkServer serverInstance) + { + this.server = serverInstance; + + server.PlayerConnected += PlayerConnected; + server.PlayerDisconnected += PlayerDisconnected; + } + + internal void Dispose() + { + server.PlayerConnected -= PlayerConnected; + server.PlayerDisconnected -= PlayerDisconnected; + } + + private void PlayerConnected(uint playerId) + { + //todo resolve player + IPlayer player = null; + + OnPlayerConnected?.Invoke(player); + } + + private void PlayerDisconnected(uint playerId) + { + //todo resolve player + IPlayer player = null; + + OnPlayerDisconnected?.Invoke(player); + } + #endregion + } +} diff --git a/Multiplayer/Components/Networking/NetworkLifecycle.cs b/Multiplayer/Components/Networking/NetworkLifecycle.cs index 54b3b291..8cf5ff05 100644 --- a/Multiplayer/Components/Networking/NetworkLifecycle.cs +++ b/Multiplayer/Components/Networking/NetworkLifecycle.cs @@ -1,12 +1,8 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Net; -using System.Text; using DV.Scenarios.Common; using DV.Utils; using LiteNetLib; using LiteNetLib.Utils; +using Multiplayer.API; using Multiplayer.Components.Networking.UI; using Multiplayer.Networking.Data; using Multiplayer.Networking.Managers; @@ -16,6 +12,11 @@ using Multiplayer.Utils; using Newtonsoft.Json; using Steamworks; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Net; +using System.Text; using UnityEngine; using UnityEngine.SceneManagement; @@ -136,6 +137,11 @@ public bool StartServer(IDifficulty difficulty) return false; Server = server; + + // Register server API + var serverAPI = new ServerAPIProvider(server); + MPAPI.MultiplayerAPI.RegisterServer(serverAPI); + StartClient(IPAddress.Loopback.ToString(), port, Multiplayer.Settings.Password, IsSinglePlayer, null); //reset for next game diff --git a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs index 3073fa0f..86a87854 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs @@ -211,7 +211,7 @@ public void Start() if (NetworkLifecycle.Instance.IsHost()) { NetworkLifecycle.Instance.OnTick += Server_OnTick; - NetworkLifecycle.Instance.Server.PlayerDisconnect += Server_OnPlayerDisconnect; + NetworkLifecycle.Instance.Server.PlayerDisconnected += Server_OnPlayerDisconnect; bogie1.TrackChanged += Server_BogieTrackChanged; bogie2.TrackChanged += Server_BogieTrackChanged; @@ -272,7 +272,7 @@ public void OnDisable() if (NetworkLifecycle.Instance.IsHost()) { NetworkLifecycle.Instance.OnTick -= Server_OnTick; - NetworkLifecycle.Instance.Server.PlayerDisconnect -= Server_OnPlayerDisconnect; + NetworkLifecycle.Instance.Server.PlayerDisconnected -= Server_OnPlayerDisconnect; bogie1.TrackChanged -= Server_BogieTrackChanged; bogie2.TrackChanged -= Server_BogieTrackChanged; diff --git a/Multiplayer/Components/Networking/World/NetworkedItemManager.cs b/Multiplayer/Components/Networking/World/NetworkedItemManager.cs index 6e22d157..aa79072a 100644 --- a/Multiplayer/Components/Networking/World/NetworkedItemManager.cs +++ b/Multiplayer/Components/Networking/World/NetworkedItemManager.cs @@ -55,7 +55,7 @@ protected override void Awake() if (!NetworkLifecycle.Instance.IsHost()) return; - //B99 temporary patch NetworkLifecycle.Instance.Server.PlayerDisconnect += PlayerDisconnected; + //B99 temporary patch NetworkLifecycle.Instance.Server.PlayerDisconnected += PlayerDisconnected; try { diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index ab8930ec..962e3ea6 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -39,7 +39,8 @@ namespace Multiplayer.Networking.Managers.Server; public class NetworkServer : NetworkManager { - public Action PlayerDisconnect; + public Action PlayerConnected; + public Action PlayerDisconnected; protected override string LogPrefix => "[Server]"; private readonly Queue joinQueue = new(); @@ -219,7 +220,7 @@ public override void OnPeerDisconnected(ITransportPeer peer, DisconnectReason di Id = id }, DeliveryMethod.ReliableUnordered); - PlayerDisconnect?.Invoke(id); + PlayerDisconnected?.Invoke(id); } public override void OnNetworkLatencyUpdate(ITransportPeer peer, int latency) diff --git a/MultiplayerAPI/Interfaces/IPlayer.cs b/MultiplayerAPI/Interfaces/IPlayer.cs index f383fc7d..fb644c30 100644 --- a/MultiplayerAPI/Interfaces/IPlayer.cs +++ b/MultiplayerAPI/Interfaces/IPlayer.cs @@ -1,12 +1,8 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -namespace MultiplayerAPI.Interfaces +namespace MPAPI.Interfaces { - internal interface IPlayer + public interface IPlayer { } } diff --git a/MultiplayerAPI/Interfaces/IServer.cs b/MultiplayerAPI/Interfaces/IServer.cs index 2b6d003d..15fae412 100644 --- a/MultiplayerAPI/Interfaces/IServer.cs +++ b/MultiplayerAPI/Interfaces/IServer.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using UnityEngine; @@ -6,10 +7,14 @@ namespace MPAPI.Interfaces; public interface IServer { - public event Action OnPlayerConnected; - public event Action OnPlayerDisconnected; + event Action OnPlayerConnected; + event Action OnPlayerDisconnected; - public abstract float AnyPlayerSqrMag(GameObject item); + int PlayerCount { get; } - public abstract float AnyPlayerSqrMag(Vector3 anchor); + //public IReadOnlyCollection Players { get; } + + float AnyPlayerSqrMag(GameObject item); + + float AnyPlayerSqrMag(Vector3 anchor); } diff --git a/MultiplayerAPI/MultiplayerAPI.cs b/MultiplayerAPI/MultiplayerAPI.cs index 3fd2f418..c135d5c0 100644 --- a/MultiplayerAPI/MultiplayerAPI.cs +++ b/MultiplayerAPI/MultiplayerAPI.cs @@ -17,7 +17,15 @@ public static class MultiplayerAPI /// Gets the current API instance (null if Multiplayer mod is not loaded) /// public static IMultiplayerAPI Instance => _instance; + + /// + /// Gets the current Server API instance (null if Multiplayer mod is not loaded or server not running) + /// public static IServer Server => _server; + + /// + /// Gets the current Client API instance (null if Multiplayer mod is not loaded or client not running) + /// public static IClient Client => _client; /// diff --git a/MultiplayerAPI/MultiplayerAPI.csproj b/MultiplayerAPI/MultiplayerAPI.csproj index 2a007205..89ce31c5 100644 --- a/MultiplayerAPI/MultiplayerAPI.csproj +++ b/MultiplayerAPI/MultiplayerAPI.csproj @@ -34,4 +34,11 @@ + + + + + + + From bce1b4d80ed592c8381725c982471c7714813ba3 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 31 May 2025 10:13:14 +1000 Subject: [PATCH 334/521] Remove redundant comments --- Multiplayer/Utils/DvExtensions.cs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/Multiplayer/Utils/DvExtensions.cs b/Multiplayer/Utils/DvExtensions.cs index 3136b43a..017e5747 100644 --- a/Multiplayer/Utils/DvExtensions.cs +++ b/Multiplayer/Utils/DvExtensions.cs @@ -128,18 +128,11 @@ 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; } From 1ab81147c6ad8fa9fb718c8c8abeb35fae6ed27b Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 31 May 2025 10:13:35 +1000 Subject: [PATCH 335/521] Create API test project --- Multiplayer.sln | 8 +- MultiplayerAPI Tests/MultiplayerAPITest.cs | 84 ++++++++++++++++ .../MultiplayerAPITest.csproj | 99 +++++++++++++++++++ 3 files changed, 190 insertions(+), 1 deletion(-) create mode 100644 MultiplayerAPI Tests/MultiplayerAPITest.cs create mode 100644 MultiplayerAPI Tests/MultiplayerAPITest.csproj diff --git a/Multiplayer.sln b/Multiplayer.sln index a5865858..f8f4eef3 100644 --- a/Multiplayer.sln +++ b/Multiplayer.sln @@ -1,11 +1,13 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 -VisualStudioVersion = 17.14.36121.58 d17.14 +VisualStudioVersion = 17.14.36121.58 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Multiplayer", "Multiplayer\Multiplayer.csproj", "{F712C7FB-EEAE-4036-A938-356E022B0455}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MultiplayerAPI", "MultiplayerAPI\MultiplayerAPI.csproj", "{AB0CA646-6E23-42EC-9F24-176CC0331714}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MultiplayerAPITest", "MultiplayerAPI Tests\MultiplayerAPITest.csproj", "{65E62E81-6FD4-4995-9BF0-3AEF1ED32800}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -20,6 +22,10 @@ Global {AB0CA646-6E23-42EC-9F24-176CC0331714}.Debug|Any CPU.Build.0 = Debug|Any CPU {AB0CA646-6E23-42EC-9F24-176CC0331714}.Release|Any CPU.ActiveCfg = Release|Any CPU {AB0CA646-6E23-42EC-9F24-176CC0331714}.Release|Any CPU.Build.0 = Release|Any CPU + {65E62E81-6FD4-4995-9BF0-3AEF1ED32800}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {65E62E81-6FD4-4995-9BF0-3AEF1ED32800}.Debug|Any CPU.Build.0 = Debug|Any CPU + {65E62E81-6FD4-4995-9BF0-3AEF1ED32800}.Release|Any CPU.ActiveCfg = Release|Any CPU + {65E62E81-6FD4-4995-9BF0-3AEF1ED32800}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/MultiplayerAPI Tests/MultiplayerAPITest.cs b/MultiplayerAPI Tests/MultiplayerAPITest.cs new file mode 100644 index 00000000..e7c5ce6f --- /dev/null +++ b/MultiplayerAPI Tests/MultiplayerAPITest.cs @@ -0,0 +1,84 @@ +using HarmonyLib; +using System; +using JetBrains.Annotations; +using UnityEngine; +using UnityModManagerNet; +using MPAPI; + +namespace MultiplayerAPITest; + +public static class MultiplayerAPITest +{ + public static UnityModManager.ModEntry ModEntry; + + [UsedImplicitly] + public static bool Load(UnityModManager.ModEntry modEntry) + { + ModEntry = modEntry; + //Settings = Settings.Load(modEntry); + //ModEntry.OnGUI = Settings.Draw; + //ModEntry.OnSaveGUI = Settings.Save; + //ModEntry.OnLateUpdate = LateUpdate; + + Harmony harmony = null; + + try + { + Log($"Multiplayer Mod is loaded: {MultiplayerAPI.IsMultiplayerLoaded}"); + + + Log("Patching..."); + harmony = new Harmony(ModEntry.Info.Id); + harmony.PatchAll(); + + Log("Loaded!"); + } + catch (Exception ex) + { + LogException("Failed to load:", ex); + harmony?.UnpatchAll(); + return false; + } + + return true; + } + + #region Logging + + public static void LogDebug(Func resolver) + { + //if (!Settings.DebugLogging) + // return; + WriteLog($"[Debug] {resolver.Invoke()}"); + } + + public static void Log(object msg) + { + WriteLog($"[Info] {msg}"); + } + + public static void LogWarning(object msg) + { + WriteLog($"[Warning] {msg}"); + } + + public static void LogError(object msg) + { + WriteLog($"[Error] {msg}"); + } + + public static void LogException(object msg, Exception e) + { + ModEntry.Logger.LogException($"{msg}", e); + } + + private static void WriteLog(string msg) + { + string str = $"[{DateTime.Now.ToUniversalTime():HH:mm:ss.fff}] {msg}"; + //if (Settings.EnableLogFile) + // File.AppendAllLines(LOG_FILE, new[] { str }); + ModEntry.Logger.Log(str); + } + + #endregion +} diff --git a/MultiplayerAPI Tests/MultiplayerAPITest.csproj b/MultiplayerAPI Tests/MultiplayerAPITest.csproj new file mode 100644 index 00000000..ae27035f --- /dev/null +++ b/MultiplayerAPI Tests/MultiplayerAPITest.csproj @@ -0,0 +1,99 @@ + + + + net48 + latest + MultiplayerAPITest + 0.0.0.0 + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ../build/MultiplayerAPI.dll + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 8485728533c579702e25c866d7951c0d13957342 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 31 May 2025 11:56:27 +1000 Subject: [PATCH 336/521] Rework playerId system --- Multiplayer/Networking/Data/ServerPlayer.cs | 38 ++++++- .../Managers/Server/NetworkServer.cs | 106 ++++++++++++------ 2 files changed, 103 insertions(+), 41 deletions(-) diff --git a/Multiplayer/Networking/Data/ServerPlayer.cs b/Multiplayer/Networking/Data/ServerPlayer.cs index 72c4d7e7..e352a205 100644 --- a/Multiplayer/Networking/Data/ServerPlayer.cs +++ b/Multiplayer/Networking/Data/ServerPlayer.cs @@ -1,15 +1,32 @@ -using System; -using System.Collections.Generic; +using MPAPI.Interfaces; using Multiplayer.Components.Networking; using Multiplayer.Components.Networking.Train; using Multiplayer.Components.Networking.World; +using Multiplayer.Networking.TransportLayers; +using Multiplayer.Utils; +using System; +using System.Collections.Generic; using UnityEngine; namespace Multiplayer.Networking.Data; -public class ServerPlayer +public class ServerPlayer : IPlayer, IDisposable { - public byte Id { get; set; } + #region ID Management + private readonly IdPool idPool; + + public void Dispose() + { + if (Id != 0) + { + idPool.ReleaseId(Id); + Id = 0; + } + } + #endregion + + public ITransportPeer Peer { get; private set; } + public byte Id { get; private set; } public bool IsLoaded { get; set; } public string Username { get; set; } public string OriginalUsername { get; set; } @@ -26,6 +43,19 @@ public class ServerPlayer private Vector3 _lastWorldPos = Vector3.zero; private Vector3 _lastAbsoluteWorldPosition = Vector3.zero; + public ServerPlayer(IdPool idPool, ITransportPeer peer, string username, string originalUsername, Guid guid) + { + this.idPool = idPool; + Id = idPool.NextId; + + Peer = peer; + + Username = username; + OriginalUsername = originalUsername; + Guid = guid; + } + + #region Positioning public Vector3 AbsoluteWorldPosition { diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index 962e3ea6..f14b4275 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -33,19 +33,23 @@ using System.Text; using Multiplayer.Networking.Data.Train; using Multiplayer.Networking.TransportLayers; +using MPAPI.Interfaces; namespace Multiplayer.Networking.Managers.Server; public class NetworkServer : NetworkManager { - public Action PlayerConnected; - public Action PlayerDisconnected; + public Action PlayerConnected; + public Action PlayerDisconnected; protected override string LogPrefix => "[Server]"; - private readonly Queue joinQueue = new(); - private readonly Dictionary serverPlayers = []; - private readonly Dictionary Peers = []; + private readonly Queue joinQueue = new(); //Queue for players attempting to join while server is loading + + private readonly IdPool playerIdPool = new(); //player Id management + private readonly Dictionary serverPlayers = []; //player Id to ServerPlayer mapping + private readonly Dictionary peers = []; //player Id to peer mapping + private readonly Dictionary peerToPlayer = []; //peer to ServerPlayer mapping private LobbyServerManager lobbyServerManager; public readonly bool IsSinglePlayer; @@ -109,12 +113,16 @@ public override void Stop() //Alert all clients (except host) var packet = WritePacket(new ClientboundDisconnectPacket()); - foreach (var peer in Peers.Values) + foreach (var peer in peers.Values) { if (peer != SelfPeer) peer?.Disconnect(packet); } + //Reset player ID pool + foreach (var player in serverPlayers.Values) + player.Dispose(); + base.Stop(); } @@ -195,7 +203,7 @@ public bool TryGetServerPlayer(byte id, out ServerPlayer player) public bool TryGetPeer(byte id, out ITransportPeer peer) { - return Peers.TryGetValue(id, out peer); + return peers.TryGetValue(id, out peer); } #region Net Events @@ -206,21 +214,33 @@ public override void OnPeerConnected(ITransportPeer peer) public override void OnPeerDisconnected(ITransportPeer peer, DisconnectReason disconnectReason) { - byte id = (byte)peer.Id; - Log($"Player {(serverPlayers.TryGetValue(id, out ServerPlayer player) ? player : id)} disconnected: {disconnectReason}"); + if (!peerToPlayer.TryGetValue(peer, out ServerPlayer player)) + { + LogWarning($"Peer {peer.GetType()}, peerId: {peer.Id} disconnected but no player found"); + return; + } + + Log($"Player {player.Username} disconnected: {disconnectReason}"); if (WorldStreamingInit.isLoaded) SaveGameManager.Instance.UpdateInternalData(); - serverPlayers.Remove(id); - Peers.Remove(id); + serverPlayers.Remove(player.Id); + peers.Remove(player.Id); + peerToPlayer.Remove(peer); - SendPacketToAll(new ClientboundPlayerDisconnectPacket - { - Id = id - }, DeliveryMethod.ReliableUnordered); + SendPacketToAll + ( + new ClientboundPlayerDisconnectPacket + { + Id = player.Id + }, + DeliveryMethod.ReliableUnordered + ); + + PlayerDisconnected?.Invoke(player); - PlayerDisconnected?.Invoke(id); + player.Dispose(); } public override void OnNetworkLatencyUpdate(ITransportPeer peer, int latency) @@ -262,14 +282,14 @@ public override void OnConnectionRequest(NetDataReader requestData, IConnectionR private void SendPacketToAll(T packet, DeliveryMethod deliveryMethod) where T : class, new() { NetDataWriter writer = WritePacket(packet); - foreach (KeyValuePair kvp in Peers) + foreach (KeyValuePair kvp in peers) kvp.Value?.Send(writer, deliveryMethod); } private void SendPacketToAll(T packet, DeliveryMethod deliveryMethod, ITransportPeer excludePeer) where T : class, new() { NetDataWriter writer = WritePacket(packet); - foreach (KeyValuePair kvp in Peers) + foreach (KeyValuePair kvp in peers) { if (kvp.Key == excludePeer.Id) continue; @@ -279,14 +299,14 @@ public override void OnConnectionRequest(NetDataReader requestData, IConnectionR private void SendNetSerializablePacketToAll(T packet, DeliveryMethod deliveryMethod) where T : INetSerializable, new() { NetDataWriter writer = WriteNetSerializablePacket(packet); - foreach (KeyValuePair kvp in Peers) + foreach (KeyValuePair kvp in peers) kvp.Value.Send(writer, deliveryMethod); } private void SendNetSerializablePacketToAll(T packet, DeliveryMethod deliveryMethod, ITransportPeer excludePeer) where T : INetSerializable, new() { NetDataWriter writer = WriteNetSerializablePacket(packet); - foreach (KeyValuePair kvp in Peers) + foreach (KeyValuePair kvp in peers) { if (kvp.Key == excludePeer.Id) continue; @@ -542,7 +562,7 @@ public void SendItemsChangePacket(List items, ServerPlayer playe { Multiplayer.Log($"Sending SendItemsChangePacket with {items.Count()} items to {player.Username}"); - if (Peers.TryGetValue(player.Id, out ITransportPeer peer) && peer != SelfPeer) + if (peers.TryGetValue(player.Id, out ITransportPeer peer) && peer != SelfPeer) { SendNetSerializablePacket(peer, new CommonItemChangePacket { Items = items }, DeliveryMethod.ReliableOrdered); @@ -666,15 +686,17 @@ private void OnServerboundClientLoginPacket(ServerboundClientLoginPacket packet, ITransportPeer peer = request.Accept(); - ServerPlayer serverPlayer = new() - { - Id = (byte)peer.Id, - Username = overrideUsername, - OriginalUsername = packet.Username, - Guid = guid - }; + ServerPlayer serverPlayer = new + ( + playerIdPool, + peer, + overrideUsername, + packet.Username, + guid + ); serverPlayers.Add(serverPlayer.Id, serverPlayer); + peerToPlayer.Add(peer, serverPlayer); ClientboundLoginResponsePacket acceptPacket = new() { @@ -686,7 +708,7 @@ private void OnServerboundClientLoginPacket(ServerboundClientLoginPacket packet, private void OnServerboundSaveGameDataRequestPacket(ServerboundSaveGameDataRequestPacket packet, ITransportPeer peer) { - if (Peers.ContainsKey((byte)peer.Id)) + if (peers.ContainsKey((byte)peer.Id)) { LogWarning("Denied save game data request from already connected peer!"); return; @@ -701,7 +723,12 @@ private void OnServerboundSaveGameDataRequestPacket(ServerboundSaveGameDataReque private void OnServerboundClientReadyPacket(ServerboundClientReadyPacket packet, ITransportPeer peer) { - byte peerId = (byte)peer.Id; + if (!peerToPlayer.TryGetValue(peer, out ServerPlayer serverPlayer)) + { + LogError($"Ready packet received for {peer.GetType()}, peerId: {peer.Id}, but ServerPlayer not found"); + peer.Disconnect(); + return; + } // Allow clients to connect before the server is fully loaded if (!IsLoaded) @@ -716,13 +743,12 @@ private void OnServerboundClientReadyPacket(ServerboundClientReadyPacket packet, AppUtil.Instance.RequestSystemOnValueChanged(0.0f); // Allow the player to receive packets - Peers.Add(peerId, peer); + peers.Add(serverPlayer.Id, peer); // Send the new player to all other players - ServerPlayer serverPlayer = serverPlayers[peerId]; ClientboundPlayerJoinedPacket clientboundPlayerJoinedPacket = new() { - Id = peerId, + Id = serverPlayer.Id, Username = serverPlayer.Username, //Guid = serverPlayer.Guid.ToByteArray() }; @@ -791,7 +817,7 @@ private void OnServerboundClientReadyPacket(ServerboundClientReadyPacket packet, // Send existing players foreach (ServerPlayer player in ServerPlayers) { - if (player.Id == peer.Id) + if (player.Id == serverPlayer.Id) continue; SendPacket(peer, new ClientboundPlayerJoinedPacket { @@ -854,17 +880,23 @@ private void OnCommonRotateTurntablePacket(CommonRotateTurntablePacket packet, I private void OnCommonCouplerInteractionPacket(CommonCouplerInteractionPacket packet, ITransportPeer peer) { + if(!peerToPlayer.TryGetValue(peer, out var player)) + { + LogWarning($"Received Coupler Interaction from {peer.GetType()}, peerId: {peer.Id}, but could not find matching player."); + return; + } + //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 if(NetworkedTrainCar.Get(packet.NetId, out var netTrainCar)) { - if(netTrainCar.Server_ValidateCouplerInteraction(packet, peer)) + if(netTrainCar.Server_ValidateCouplerInteraction(packet, player)) { //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"); + LogDebug(() => $"OnCommonCouplerInteractionPacket([{packet.Flags}, {netTrainCar.CurrentID}, {packet.NetId}], {player.Id}) Sending validation failure"); //failed validation notify client SendPacket( peer, @@ -880,7 +912,7 @@ private void OnCommonCouplerInteractionPacket(CommonCouplerInteractionPacket pac } else { - Multiplayer.LogDebug(() => $"OnCommonCouplerInteractionPacket([{packet.Flags}, {netTrainCar.CurrentID}, {packet.NetId}], {peer.Id}) Sending destroy"); + LogDebug(() => $"OnCommonCouplerInteractionPacket([{packet.Flags}, {netTrainCar.CurrentID}, {packet.NetId}], {player.Id}) Sending destroy"); //Car doesn't exist, tell client to delete it SendDestroyTrainCar(netTrainCar, peer); } From 3c772b8384edbbcce9e800310884f6742fa5df0b Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 31 May 2025 12:18:11 +1000 Subject: [PATCH 337/521] Register and deregister Server API Interfaces --- Multiplayer/API/ServerAPIProvider.cs | 24 ++---------- .../Components/Networking/NetworkLifecycle.cs | 7 +++- .../Networking/Train/NetworkedTrainCar.cs | 38 ++++++++++--------- 3 files changed, 30 insertions(+), 39 deletions(-) diff --git a/Multiplayer/API/ServerAPIProvider.cs b/Multiplayer/API/ServerAPIProvider.cs index cb6771fe..db45ccde 100644 --- a/Multiplayer/API/ServerAPIProvider.cs +++ b/Multiplayer/API/ServerAPIProvider.cs @@ -34,30 +34,14 @@ internal ServerAPIProvider(NetworkServer serverInstance) { this.server = serverInstance; - server.PlayerConnected += PlayerConnected; - server.PlayerDisconnected += PlayerDisconnected; + server.PlayerConnected += OnPlayerConnected; + server.PlayerDisconnected += OnPlayerDisconnected; } internal void Dispose() { - server.PlayerConnected -= PlayerConnected; - server.PlayerDisconnected -= PlayerDisconnected; - } - - private void PlayerConnected(uint playerId) - { - //todo resolve player - IPlayer player = null; - - OnPlayerConnected?.Invoke(player); - } - - private void PlayerDisconnected(uint playerId) - { - //todo resolve player - IPlayer player = null; - - OnPlayerDisconnected?.Invoke(player); + server.PlayerConnected -= OnPlayerConnected; + server.PlayerDisconnected -= OnPlayerDisconnected; } #endregion } diff --git a/Multiplayer/Components/Networking/NetworkLifecycle.cs b/Multiplayer/Components/Networking/NetworkLifecycle.cs index 8cf5ff05..d0c8df23 100644 --- a/Multiplayer/Components/Networking/NetworkLifecycle.cs +++ b/Multiplayer/Components/Networking/NetworkLifecycle.cs @@ -2,6 +2,7 @@ using DV.Utils; using LiteNetLib; using LiteNetLib.Utils; +using MPAPI; using Multiplayer.API; using Multiplayer.Components.Networking.UI; using Multiplayer.Networking.Data; @@ -140,7 +141,7 @@ public bool StartServer(IDifficulty difficulty) // Register server API var serverAPI = new ServerAPIProvider(server); - MPAPI.MultiplayerAPI.RegisterServer(serverAPI); + MultiplayerAPI.RegisterServer(serverAPI); StartClient(IPAddress.Loopback.ToString(), port, Multiplayer.Settings.Password, IsSinglePlayer, null); @@ -221,6 +222,10 @@ public void Stop() Stats?.Hide(); Server?.Stop(); Client?.Stop(); + + // Clear API registrations + MultiplayerAPI.ClearServer(); + Server = null; Client = null; } diff --git a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs index 86a87854..aec3c99f 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs @@ -11,6 +11,7 @@ using JetBrains.Annotations; using LocoSim.Definitions; using LocoSim.Implementations; +using MPAPI.Interfaces; using Multiplayer.Components.Networking.Player; using Multiplayer.Networking.Data; using Multiplayer.Networking.Data.Train; @@ -112,8 +113,8 @@ public static bool TryGetFromTrainCar(TrainCar trainCar, out NetworkedTrainCar n private bool frontInteracting = false; private bool rearInteracting = false; - private int frontInteractionPeer; - private int rearInteractionPeer; + private ServerPlayer frontInteractionPlayer; + private ServerPlayer rearInteractionPlayer; #region Client public bool Client_Initialized { get; private set; } @@ -565,34 +566,35 @@ private void Server_SendHealthState() NetworkLifecycle.Instance.Server.SendCarHealthUpdate(NetId, TrainCarHealthData.From(TrainCar)); } - public bool Server_ValidateCouplerInteraction(CommonCouplerInteractionPacket packet, ITransportPeer peer) + public bool Server_ValidateCouplerInteraction(CommonCouplerInteractionPacket packet, ServerPlayer player) { Multiplayer.LogDebug(() => - $"Server_ValidateCouplerInteraction([[{(CouplerInteractionType)packet.Flags}], {CurrentID}, {packet.NetId}], {peer.Id}) " + - $"isFront: {packet.IsFrontCoupler}, frontInteracting: {frontInteracting}, frontInteractionPeer: {frontInteractionPeer}, " + - $"rearInteracting: {rearInteracting}, rearInteractionPeer: {rearInteractionPeer}" + $"Server_ValidateCouplerInteraction([[{(CouplerInteractionType)packet.Flags}], {CurrentID}, {packet.NetId}], {player.Id}) " + + $"isFront: {packet.IsFrontCoupler}, frontInteracting: {frontInteracting}, frontInteractionPeer: {frontInteractionPlayer}, " + + $"rearInteracting: {rearInteracting}, rearInteractionPeer: {rearInteractionPlayer}" ); + //Ensure no one else is interacting - if (packet.IsFrontCoupler && frontInteracting && peer.Id != frontInteractionPeer || - packet.IsFrontCoupler == false && rearInteracting && peer.Id != rearInteractionPeer) + if (packet.IsFrontCoupler && frontInteracting && player != frontInteractionPlayer || + packet.IsFrontCoupler == false && rearInteracting && player != rearInteractionPlayer) { - Multiplayer.LogDebug(() => $"Server_ValidateCouplerInteraction([{packet.Flags}, {CurrentID}, {packet.NetId}], {peer.Id}) Failed to validate!"); + Multiplayer.LogDebug(() => $"Server_ValidateCouplerInteraction([{packet.Flags}, {CurrentID}, {packet.NetId}], {player.Id}) Failed to validate!"); return false; } - Multiplayer.LogDebug(() => $"Server_ValidateCouplerInteraction([{packet.Flags}, {CurrentID}, {packet.NetId}], {peer.Id}) No one interacting"); + Multiplayer.LogDebug(() => $"Server_ValidateCouplerInteraction([{packet.Flags}, {CurrentID}, {packet.NetId}], {player.Id}) No one interacting"); if (((CouplerInteractionType)packet.Flags).HasFlag(CouplerInteractionType.Start)) { if (packet.IsFrontCoupler) { frontInteracting = true; - frontInteractionPeer = peer.Id; + frontInteractionPlayer = player; } else { rearInteracting = true; - rearInteractionPeer = peer.Id; + rearInteractionPlayer = player; } } else @@ -605,17 +607,17 @@ public bool Server_ValidateCouplerInteraction(CommonCouplerInteractionPacket pac //todo: Additional checks for player location/proximity - Multiplayer.LogDebug(() => $"Server_ValidateCouplerInteraction([{packet.Flags}, {CurrentID}, {packet.NetId}], {peer.Id}) Validation passed!"); + Multiplayer.LogDebug(() => $"Server_ValidateCouplerInteraction([{packet.Flags}, {CurrentID}, {packet.NetId}], {player.Id}) Validation passed!"); return true; } - private void Server_OnPlayerDisconnect(uint id) + private void Server_OnPlayerDisconnect(IPlayer player) { - //todo: resove player disconnection during chain interaction - if (frontInteractionPeer == id || rearInteractionPeer == id) + //todo: resolve player disconnection during chain interaction + if (frontInteractionPlayer == player || rearInteractionPlayer == player) { - Multiplayer.LogWarning($"Server_OnPlayerDisconnect() Coupler interaction in unknown state [{CurrentID}, {NetId}] isFront: {frontInteractionPeer == id}"); - if (frontInteractionPeer == id) + Multiplayer.LogWarning($"Server_OnPlayerDisconnect() Coupler interaction in unknown state [{CurrentID}, {NetId}] isFront: {frontInteractionPlayer == player}"); + if (frontInteractionPlayer == player) { frontInteracting = false; //NetworkLifecycle.Instance.Client.SendCouplerInteraction(cou, coupler, otherCoupler); From 8b816e2f62aeba01308fd3385ce8a4d314325521 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 31 May 2025 13:17:47 +1000 Subject: [PATCH 338/521] Rework IPlayer --- Multiplayer/API/ClientPlayerWrapper.cs | 34 +++++++++++++++ Multiplayer/API/ServerPlayerWrapper.cs | 42 +++++++++++++++++++ .../Networking/Player/NetworkedPlayer.cs | 29 ++++++------- .../Networking/Player/NetworkedWorldMap.cs | 20 ++++----- .../Networking/Train/NetworkedTrainCar.cs | 2 +- Multiplayer/Networking/Data/ServerPlayer.cs | 2 +- .../Managers/Client/ClientPlayerManager.cs | 9 ++-- .../Managers/Client/NetworkClient.cs | 22 +++++----- .../Managers/Server/NetworkServer.cs | 4 +- MultiplayerAPI/Interfaces/IPlayer.cs | 13 +++++- MultiplayerAPI/MultiplayerAPI.csproj | 13 ++++++ 11 files changed, 145 insertions(+), 45 deletions(-) create mode 100644 Multiplayer/API/ClientPlayerWrapper.cs create mode 100644 Multiplayer/API/ServerPlayerWrapper.cs diff --git a/Multiplayer/API/ClientPlayerWrapper.cs b/Multiplayer/API/ClientPlayerWrapper.cs new file mode 100644 index 00000000..6d6f54d2 --- /dev/null +++ b/Multiplayer/API/ClientPlayerWrapper.cs @@ -0,0 +1,34 @@ +using MPAPI.Interfaces; +using Multiplayer.Components.Networking.Player; +using Multiplayer.Networking.Data; +using System; +using UnityEngine; + +namespace Multiplayer.API; + +public class ClientPlayerWrapper : IPlayer +{ + private readonly NetworkedPlayer _networkedPlayer; + private readonly bool _isHost; + + public ClientPlayerWrapper(NetworkedPlayer networkedPlayer, bool isHost = false) + { + _networkedPlayer = networkedPlayer; + _isHost = isHost; + } + + public byte Id => _networkedPlayer.Id; + public string Username + { + get => _networkedPlayer.Username; + set => _networkedPlayer.Username = value; + } + public Guid Guid => Guid.Empty; // NetworkedPlayer doesn't store GUID + public Vector3 Position => _networkedPlayer.transform.position; + public float RotationY => _networkedPlayer.transform.rotation.eulerAngles.y; + public bool IsLoaded => true; // If we have the object, it's loaded + public bool IsHost => _isHost; + public int Ping => _networkedPlayer.GetPing(); + public bool IsOnCar => _networkedPlayer.IsOnCar; // You'll need to add this logic + public TrainCar OccupiedCar => _networkedPlayer.OccupiedCar; // You'll need to track this in NetworkedPlayer +} diff --git a/Multiplayer/API/ServerPlayerWrapper.cs b/Multiplayer/API/ServerPlayerWrapper.cs new file mode 100644 index 00000000..979d40d0 --- /dev/null +++ b/Multiplayer/API/ServerPlayerWrapper.cs @@ -0,0 +1,42 @@ +using MPAPI.Interfaces; +using Multiplayer.Components.Networking; +using Multiplayer.Components.Networking.Train; +using Multiplayer.Networking.Data; +using System; +using UnityEngine; + +namespace Multiplayer.API; + +public class ServerPlayerWrapper : IPlayer +{ + private readonly ServerPlayer _serverPlayer; + private readonly bool _isHost; + + public ServerPlayerWrapper(ServerPlayer serverPlayer) + { + _serverPlayer = serverPlayer; + _isHost = NetworkLifecycle.Instance?.IsHost() ?? false; + } + + public byte Id => _serverPlayer.Id; + + public string Username + { + get => _serverPlayer.Username; + set => _serverPlayer.Username = value; + } + + public Vector3 Position => _serverPlayer.WorldPosition; + public float RotationY => _serverPlayer.WorldRotationY; + public bool IsLoaded => _serverPlayer.IsLoaded; + public bool IsHost => _isHost; + public int Ping => 0; // Server doesn't track ping for players + public bool IsOnCar => _serverPlayer.CarId != 0; + public TrainCar OccupiedCar => GetOccupiedCar(); + + internal TrainCar GetOccupiedCar() + { + NetworkedTrainCar.GetTrainCar(_serverPlayer.CarId, out var trainCar); + return trainCar; + } +} diff --git a/Multiplayer/Components/Networking/Player/NetworkedPlayer.cs b/Multiplayer/Components/Networking/Player/NetworkedPlayer.cs index 97f562eb..a42d488b 100644 --- a/Multiplayer/Components/Networking/Player/NetworkedPlayer.cs +++ b/Multiplayer/Components/Networking/Player/NetworkedPlayer.cs @@ -1,4 +1,5 @@ using System; +using MPAPI.Interfaces; using Multiplayer.Components.Networking.Train; using Multiplayer.Editor.Components.Player; using UnityEngine; @@ -9,8 +10,7 @@ public class NetworkedPlayer : MonoBehaviour { private const float LERP_SPEED = 5.0f; - public byte Id; - //public Guid Guid; + public byte Id { get; set; } private AnimationHandler animationHandler; private NameTag nameTag; @@ -28,8 +28,8 @@ public string Username } } - private bool isOnCar; - private TrainCar trainCar; + internal bool IsOnCar { get; private set; } + internal TrainCar OccupiedCar { get; private set; } private Transform selfTransform; private Vector3 targetPos; @@ -37,7 +37,7 @@ public string Username private Vector2 moveDir; private Vector2 targetMoveDir; - private void Awake() + protected void Awake() { animationHandler = GetComponent(); @@ -72,29 +72,29 @@ public int GetPing() return ping; } - private void Update() + protected void Update() { float t = Time.deltaTime * LERP_SPEED; - Vector3 position = Vector3.Lerp(isOnCar ? selfTransform.localPosition : selfTransform.position, isOnCar ? targetPos : targetPos + WorldMover.currentMove, t); + Vector3 position = Vector3.Lerp(IsOnCar ? selfTransform.localPosition : selfTransform.position, IsOnCar ? targetPos : targetPos + WorldMover.currentMove, t); moveDir = Vector2.Lerp(moveDir, targetMoveDir, t); animationHandler.SetMoveDir(moveDir); - if (isOnCar && trainCar != null) + if (IsOnCar && OccupiedCar != null) { selfTransform.localPosition = position; // Calculate a world-up-respecting rotation // This creates a rotation where Y points up in world space // but the forward direction aligns with the car's forward projected onto the horizontal plane - Vector3 carForward = trainCar.transform.forward; + Vector3 carForward = OccupiedCar.transform.forward; Vector3 worldUp = Vector3.up; // Project car's forward onto the horizontal plane Vector3 horizontalForward = Vector3.ProjectOnPlane(carForward, worldUp).normalized; if (horizontalForward.sqrMagnitude < 0.001f) - horizontalForward = Vector3.ProjectOnPlane(trainCar.transform.right, worldUp).normalized; + horizontalForward = Vector3.ProjectOnPlane(OccupiedCar.transform.right, worldUp).normalized; // Create base orientation aligned with world up but facing car's forward direction Quaternion baseRotation = Quaternion.LookRotation(horizontalForward, worldUp); @@ -119,7 +119,7 @@ public void UpdatePosition(Vector3 position, Vector2 moveDir, float rotationY, b animationHandler.SetIsJumping(isJumping); - if (isOnCar != movePacketIsOnCar) + if (IsOnCar != movePacketIsOnCar) return; targetRotation = Quaternion.Euler(0, rotationY, 0); @@ -127,10 +127,11 @@ public void UpdatePosition(Vector3 position, Vector2 moveDir, float rotationY, b public void UpdateCar(ushort netId) { - isOnCar = NetworkedTrainCar.GetTrainCar(netId, out trainCar); + IsOnCar = NetworkedTrainCar.GetTrainCar(netId, out var trainCar); + OccupiedCar = trainCar; - if (isOnCar) - selfTransform.SetParent(trainCar.transform, true); + if (IsOnCar) + selfTransform.SetParent(OccupiedCar.transform, true); else selfTransform.SetParent(null, true); } diff --git a/Multiplayer/Components/Networking/Player/NetworkedWorldMap.cs b/Multiplayer/Components/Networking/Player/NetworkedWorldMap.cs index fe99948d..53850f6e 100644 --- a/Multiplayer/Components/Networking/Player/NetworkedWorldMap.cs +++ b/Multiplayer/Components/Networking/Player/NetworkedWorldMap.cs @@ -9,14 +9,14 @@ public class NetworkedMapMarkersController : MonoBehaviour { private MapMarkersController markersController; private GameObject textPrefab; - private readonly Dictionary playerIndicators = []; + private readonly Dictionary playerIndicators = []; private void Awake() { markersController = GetComponent(); textPrefab = markersController.GetComponentInChildren().gameObject; foreach (NetworkedPlayer networkedPlayer in NetworkLifecycle.Instance.Client.ClientPlayerManager.Players) - OnPlayerConnected(networkedPlayer.Id, networkedPlayer); + OnPlayerConnected(networkedPlayer); NetworkLifecycle.Instance.Client.ClientPlayerManager.OnPlayerConnected += OnPlayerConnected; NetworkLifecycle.Instance.Client.ClientPlayerManager.OnPlayerDisconnected += OnPlayerDisconnected; NetworkLifecycle.Instance.OnTick += OnTick; @@ -33,7 +33,7 @@ private void OnDestroy() NetworkLifecycle.Instance.Client.ClientPlayerManager.OnPlayerDisconnected -= OnPlayerDisconnected; } - private void OnPlayerConnected(byte id, NetworkedPlayer player) + private void OnPlayerConnected(NetworkedPlayer player) { Transform root = new GameObject($"MapMarkerPlayer({player.Username})") { transform = { @@ -62,15 +62,15 @@ private void OnPlayerConnected(byte id, NetworkedPlayer player) text.fontSizeMax = text.fontSize; text.enableAutoSizing = true; - playerIndicators[id] = refs; + playerIndicators[player] = refs; } - private void OnPlayerDisconnected(byte id, NetworkedPlayer player) + private void OnPlayerDisconnected(NetworkedPlayer player) { - if (!playerIndicators.TryGetValue(id, out WorldMapIndicatorRefs refs)) + if (!playerIndicators.TryGetValue(player, out WorldMapIndicatorRefs refs)) return; Destroy(refs.gameObject); - playerIndicators.Remove(id); + playerIndicators.Remove(player); } private void OnTick(uint obj) @@ -88,15 +88,15 @@ public void UpdatePlayers() return; } - foreach (KeyValuePair kvp in playerIndicators) + foreach (KeyValuePair kvp in playerIndicators) { 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)) + if (!NetworkLifecycle.Instance.Client.ClientPlayerManager.TryGetPlayer(kvp.Key.Id, out NetworkedPlayer networkedPlayer)) { Multiplayer.LogWarning($"Player indicator for {kvp.Key} exists but {nameof(NetworkedPlayer)} does not!"); - OnPlayerDisconnected(kvp.Key, null); + OnPlayerDisconnected(kvp.Key); continue; } diff --git a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs index aec3c99f..f3020450 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs @@ -611,7 +611,7 @@ public bool Server_ValidateCouplerInteraction(CommonCouplerInteractionPacket pac return true; } - private void Server_OnPlayerDisconnect(IPlayer player) + private void Server_OnPlayerDisconnect(ServerPlayer player) { //todo: resolve player disconnection during chain interaction if (frontInteractionPlayer == player || rearInteractionPlayer == player) diff --git a/Multiplayer/Networking/Data/ServerPlayer.cs b/Multiplayer/Networking/Data/ServerPlayer.cs index e352a205..aec3213e 100644 --- a/Multiplayer/Networking/Data/ServerPlayer.cs +++ b/Multiplayer/Networking/Data/ServerPlayer.cs @@ -10,7 +10,7 @@ namespace Multiplayer.Networking.Data; -public class ServerPlayer : IPlayer, IDisposable +public class ServerPlayer : IDisposable { #region ID Management private readonly IdPool idPool; diff --git a/Multiplayer/Networking/Managers/Client/ClientPlayerManager.cs b/Multiplayer/Networking/Managers/Client/ClientPlayerManager.cs index a8abaae8..2a7175af 100644 --- a/Multiplayer/Networking/Managers/Client/ClientPlayerManager.cs +++ b/Multiplayer/Networking/Managers/Client/ClientPlayerManager.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using DV; -using Multiplayer.Components.Networking; using Multiplayer.Components.Networking.Player; using UnityEngine; using Object = UnityEngine.Object; @@ -12,8 +11,8 @@ public class ClientPlayerManager { private readonly Dictionary playerMap = new(); - public Action OnPlayerConnected; - public Action OnPlayerDisconnected; + public Action OnPlayerConnected; + public Action OnPlayerDisconnected; public IReadOnlyCollection Players => playerMap.Values; private readonly GameObject playerPrefab; @@ -37,7 +36,7 @@ public void AddPlayer(byte id, string username) networkedPlayer.Username = username; //networkedPlayer.Guid = guid; playerMap.Add(id, networkedPlayer); - OnPlayerConnected?.Invoke(id, networkedPlayer); + OnPlayerConnected?.Invoke(networkedPlayer); } public void RemovePlayer(byte id) @@ -45,7 +44,7 @@ public void RemovePlayer(byte id) if (!playerMap.TryGetValue(id, out NetworkedPlayer networkedPlayer)) return; - OnPlayerDisconnected?.Invoke(id, networkedPlayer); + OnPlayerDisconnected?.Invoke(networkedPlayer); Object.Destroy(networkedPlayer.gameObject); playerMap.Remove(id); } diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 3341b64e..689fe91f 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -1,6 +1,6 @@ -using System; -using System.Collections.Generic; using DV; +using DV.Common; +using DV.Customization.Paint; using DV.Damage; using DV.InventorySystem; using DV.Logic.Job; @@ -8,8 +8,10 @@ using DV.ServicePenalty.UI; using DV.ThingTypes; using DV.UI; +using DV.UserManagement; using DV.WeatherSystem; using LiteNetLib; +using LiteNetLib.Utils; using Multiplayer.Components.MainMenu; using Multiplayer.Components.Networking; using Multiplayer.Components.Networking.Jobs; @@ -19,6 +21,7 @@ using Multiplayer.Components.Networking.World; using Multiplayer.Components.SaveGame; using Multiplayer.Networking.Data; +using Multiplayer.Networking.Data.Train; using Multiplayer.Networking.Packets.Clientbound; using Multiplayer.Networking.Packets.Clientbound.Jobs; using Multiplayer.Networking.Packets.Clientbound.SaveGame; @@ -27,20 +30,17 @@ using Multiplayer.Networking.Packets.Common; using Multiplayer.Networking.Packets.Common.Train; using Multiplayer.Networking.Packets.Serverbound; -using Multiplayer.Networking.Data.Train; +using Multiplayer.Networking.Packets.Serverbound.Train; +using Multiplayer.Networking.TransportLayers; using Multiplayer.Patches.SaveGame; using Multiplayer.Utils; using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Linq; using UnityEngine; using UnityModManagerNet; using Object = UnityEngine.Object; -using Multiplayer.Networking.Packets.Serverbound.Train; -using System.Linq; -using LiteNetLib.Utils; -using DV.UserManagement; -using DV.Common; -using DV.Customization.Paint; -using Multiplayer.Networking.TransportLayers; namespace Multiplayer.Networking.Managers.Client; @@ -1031,7 +1031,7 @@ private void SendReadyPacket() public void SendPlayerPosition(Vector3 position, Vector3 moveDir, float rotationY, ushort carId, bool isJumping, bool isOnCar, bool reliable) { - //LogDebug(() => $"SendPlayerPosition({position}, {moveDir}, {rotationY}, {carId}, {isJumping}, {isOnCar})"); + //LogDebug(() => $"SendPlayerPosition({position}, {moveDir}, {rotationY}, {carId}, {isJumping}, {IsOnCar})"); SendPacketToServer(new ServerboundPlayerPositionPacket { diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index f14b4275..1c6474bc 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -40,8 +40,8 @@ namespace Multiplayer.Networking.Managers.Server; public class NetworkServer : NetworkManager { - public Action PlayerConnected; - public Action PlayerDisconnected; + public Action PlayerConnected; + public Action PlayerDisconnected; protected override string LogPrefix => "[Server]"; private readonly Queue joinQueue = new(); //Queue for players attempting to join while server is loading diff --git a/MultiplayerAPI/Interfaces/IPlayer.cs b/MultiplayerAPI/Interfaces/IPlayer.cs index fb644c30..b8527b19 100644 --- a/MultiplayerAPI/Interfaces/IPlayer.cs +++ b/MultiplayerAPI/Interfaces/IPlayer.cs @@ -1,8 +1,19 @@ -using System; +using UnityEngine; namespace MPAPI.Interfaces { public interface IPlayer { + public byte Id { get; } + public string Username { get; set; } + Vector3 Position { get; } + float RotationY { get; } + bool IsLoaded { get; } + bool IsHost { get; } + int Ping { get; } + + // Car information + bool IsOnCar { get; } + TrainCar OccupiedCar { get; } } } diff --git a/MultiplayerAPI/MultiplayerAPI.csproj b/MultiplayerAPI/MultiplayerAPI.csproj index 89ce31c5..ca635830 100644 --- a/MultiplayerAPI/MultiplayerAPI.csproj +++ b/MultiplayerAPI/MultiplayerAPI.csproj @@ -17,6 +17,19 @@ + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + From de1c21552a61030ed020297d77309916a65e3ec3 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 31 May 2025 13:18:16 +1000 Subject: [PATCH 339/521] Add ClientAPI provider --- Multiplayer/API/ClientAPIProvider.cs | 66 +++++++++++++++++++ .../Components/Networking/NetworkLifecycle.cs | 7 ++ MultiplayerAPI/Interfaces/IClient.cs | 12 ++++ 3 files changed, 85 insertions(+) create mode 100644 Multiplayer/API/ClientAPIProvider.cs diff --git a/Multiplayer/API/ClientAPIProvider.cs b/Multiplayer/API/ClientAPIProvider.cs new file mode 100644 index 00000000..04f058f3 --- /dev/null +++ b/Multiplayer/API/ClientAPIProvider.cs @@ -0,0 +1,66 @@ +using MPAPI.Interfaces; +using Multiplayer.Networking.Managers.Client; +using Multiplayer.Networking.Managers.Server; +using Multiplayer.Utils; +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace Multiplayer.API +{ + public class ClientAPIProvider : IClient + { + private readonly NetworkClient client; + + public event Action OnPlayerConnected; + public event Action OnPlayerDisconnected; + + #region Client Properties + public IReadOnlyCollection Players => + client.ClientPlayerManager.Players + .Select(p => new ClientPlayerWrapper(p)) + .Cast() + .ToList(); + + + public bool IsConnected => throw new NotImplementedException(); + + public int Ping => client.Ping; + + #endregion + + #region Class Helpers + internal ClientAPIProvider(NetworkClient clientInstance) + { + this.client = clientInstance; + + client.ClientPlayerManager.OnPlayerConnected += OnPlayerConnectedInternal; + client.ClientPlayerManager.OnPlayerDisconnected += OnPlayerDisconnectedInternal; + } + + internal void Dispose() + { + client.ClientPlayerManager.OnPlayerConnected -= OnPlayerConnectedInternal; + client.ClientPlayerManager.OnPlayerDisconnected -= OnPlayerDisconnectedInternal; + } + + public IPlayer GetPlayer(byte id) + { + if (client.ClientPlayerManager.TryGetPlayer(id, out var player)) + return new ClientPlayerWrapper(player); + return null; + } + + private void OnPlayerConnectedInternal(Components.Networking.Player.NetworkedPlayer networkedPlayer) + { + OnPlayerConnected?.Invoke(new ClientPlayerWrapper(networkedPlayer)); + } + + private void OnPlayerDisconnectedInternal(Components.Networking.Player.NetworkedPlayer networkedPlayer) + { + OnPlayerDisconnected?.Invoke(new ClientPlayerWrapper(networkedPlayer)); + } + #endregion + } +} diff --git a/Multiplayer/Components/Networking/NetworkLifecycle.cs b/Multiplayer/Components/Networking/NetworkLifecycle.cs index d0c8df23..8b97571e 100644 --- a/Multiplayer/Components/Networking/NetworkLifecycle.cs +++ b/Multiplayer/Components/Networking/NetworkLifecycle.cs @@ -158,7 +158,13 @@ public void StartClient(string address, int port, string password, bool isSingle throw new InvalidOperationException("NetworkManager already exists!"); NetworkClient client = new(Multiplayer.Settings, isSinglePlayer); client.Start(address, port, password, isSinglePlayer, onDisconnect); + Client = client; + + // Register server API + var clientAPI = new ClientAPIProvider(client); + MultiplayerAPI.RegisterClient(clientAPI); + OnSettingsUpdated(Multiplayer.Settings); // Show stats if enabled } @@ -225,6 +231,7 @@ public void Stop() // Clear API registrations MultiplayerAPI.ClearServer(); + MultiplayerAPI.ClearClient(); Server = null; Client = null; diff --git a/MultiplayerAPI/Interfaces/IClient.cs b/MultiplayerAPI/Interfaces/IClient.cs index 1f8cac24..c0421003 100644 --- a/MultiplayerAPI/Interfaces/IClient.cs +++ b/MultiplayerAPI/Interfaces/IClient.cs @@ -5,6 +5,18 @@ namespace MPAPI.Interfaces; public interface IClient { + + event Action OnPlayerConnected; + event Action OnPlayerDisconnected; + + // Player access + IReadOnlyCollection Players { get; } + IPlayer GetPlayer(byte id); + + // Client info + bool IsConnected { get; } + int Ping { get; } + //public abstract void SendPacketToServer(T packet, DeliveryMethod deliveryMethod) where T : class, new(); //public abstract void SendNetSerializablePacketToServer(T packet, DeliveryMethod deliveryMethod) where T : INetSerializable, new(); From db937db84a032452a7bfc698ac0ebc321670c876 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 31 May 2025 13:18:43 +1000 Subject: [PATCH 340/521] Update Player connection events --- Multiplayer/API/ServerAPIProvider.cs | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/Multiplayer/API/ServerAPIProvider.cs b/Multiplayer/API/ServerAPIProvider.cs index db45ccde..12df0676 100644 --- a/Multiplayer/API/ServerAPIProvider.cs +++ b/Multiplayer/API/ServerAPIProvider.cs @@ -3,6 +3,7 @@ using Multiplayer.Utils; using System; using System.Collections.Generic; +using System.Linq; using UnityEngine; namespace Multiplayer.API @@ -18,7 +19,11 @@ public class ServerAPIProvider : IServer public int PlayerCount => server.PlayerCount; - //public IReadOnlyCollection Players => server.ServerPlayers; + public IReadOnlyCollection Players => + server.ServerPlayers + .Select(p => new ServerPlayerWrapper(p)) + .Cast() + .ToList(); #endregion @@ -34,14 +39,24 @@ internal ServerAPIProvider(NetworkServer serverInstance) { this.server = serverInstance; - server.PlayerConnected += OnPlayerConnected; - server.PlayerDisconnected += OnPlayerDisconnected; + server.PlayerConnected += OnPlayerConnectedInternal; + server.PlayerDisconnected += OnPlayerDisconnectedInternal; } internal void Dispose() { - server.PlayerConnected -= OnPlayerConnected; - server.PlayerDisconnected -= OnPlayerDisconnected; + server.PlayerConnected -= OnPlayerConnectedInternal; + server.PlayerDisconnected -= OnPlayerDisconnectedInternal; + } + + private void OnPlayerConnectedInternal(Networking.Data.ServerPlayer serverPlayer) + { + OnPlayerConnected?.Invoke(new ServerPlayerWrapper(serverPlayer)); + } + + private void OnPlayerDisconnectedInternal(Networking.Data.ServerPlayer serverPlayer) + { + OnPlayerDisconnected?.Invoke(new ServerPlayerWrapper(serverPlayer)); } #endregion } From 89b6bcff3328a89791d0129665404b886ac8b773 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 31 May 2025 15:22:40 +1000 Subject: [PATCH 341/521] Implement mod packet system --- .../API/ExternalSerializablePacketWrapper.cs | 38 +++++++++++++++++++ MultiplayerAPI/Interfaces/Packets/IPacket.cs | 9 +++++ .../Interfaces/Packets/ISerializablePacket.cs | 22 +++++++++++ .../Interfaces/Packets/PacketHandler.cs | 9 +++++ 4 files changed, 78 insertions(+) create mode 100644 Multiplayer/API/ExternalSerializablePacketWrapper.cs create mode 100644 MultiplayerAPI/Interfaces/Packets/IPacket.cs create mode 100644 MultiplayerAPI/Interfaces/Packets/ISerializablePacket.cs create mode 100644 MultiplayerAPI/Interfaces/Packets/PacketHandler.cs diff --git a/Multiplayer/API/ExternalSerializablePacketWrapper.cs b/Multiplayer/API/ExternalSerializablePacketWrapper.cs new file mode 100644 index 00000000..8d3972da --- /dev/null +++ b/Multiplayer/API/ExternalSerializablePacketWrapper.cs @@ -0,0 +1,38 @@ +using LiteNetLib.Utils; +using MPAPI.Interfaces.Packets; +using System; +using System.Collections.Generic; +using System.IO; + +namespace Multiplayer.API; + +/// +/// Wrapper for external serializable packets to integrate with LiteNetLib +/// +/// The packet type +public class ExternalSerializablePacketWrapper : INetSerializable where T : class, ISerializablePacket, new() +{ + public T Packet { get; set; } + + public void Serialize(NetDataWriter writer) + { + using var memoryStream = new MemoryStream(); + using var binaryWriter = new BinaryWriter(memoryStream); + + Packet.Serialize(binaryWriter); + + var data = memoryStream.ToArray(); + writer.PutBytesWithLength(data); + } + + public void Deserialize(NetDataReader reader) + { + var data = reader.GetBytesWithLength(); + + using var memoryStream = new MemoryStream(data); + using var binaryReader = new BinaryReader(memoryStream); + + Packet = new T(); + Packet.Deserialize(binaryReader); + } +} diff --git a/MultiplayerAPI/Interfaces/Packets/IPacket.cs b/MultiplayerAPI/Interfaces/Packets/IPacket.cs new file mode 100644 index 00000000..e8365e47 --- /dev/null +++ b/MultiplayerAPI/Interfaces/Packets/IPacket.cs @@ -0,0 +1,9 @@ +namespace MPAPI.Interfaces.Packets; + +/// +/// Base interface for packets using automatic serialization +/// +public interface IPacket +{ + // Empty interface - Multiplayer Mod/LiteNetLib handles serialization automatically for you via public properties +} diff --git a/MultiplayerAPI/Interfaces/Packets/ISerializablePacket.cs b/MultiplayerAPI/Interfaces/Packets/ISerializablePacket.cs new file mode 100644 index 00000000..33eb4685 --- /dev/null +++ b/MultiplayerAPI/Interfaces/Packets/ISerializablePacket.cs @@ -0,0 +1,22 @@ +using System.IO; + +namespace MPAPI.Interfaces.Packets; + +/// +/// Base interface for packets using manual serialization +/// Implementing classes must handle their own serialization/deserialization. +/// +public interface ISerializablePacket +{ + /// + /// Serialize the packet data to the provided writer + /// + /// Writer to serialize data to + void Serialize(BinaryWriter writer); + + /// + /// Deserialize the packet data from the provided reader + /// + /// Reader to deserialize data from + void Deserialize(BinaryReader reader); +} diff --git a/MultiplayerAPI/Interfaces/Packets/PacketHandler.cs b/MultiplayerAPI/Interfaces/Packets/PacketHandler.cs new file mode 100644 index 00000000..70197b4d --- /dev/null +++ b/MultiplayerAPI/Interfaces/Packets/PacketHandler.cs @@ -0,0 +1,9 @@ +namespace MPAPI.Interfaces.Packets; + +/// +/// Delegate for handling received packets +/// +/// Packet type +/// The received packet +/// The player who sent the packet (null if from server) +public delegate void PacketHandler(T packet, IPlayer sender) where T : class; From 876d9faa46b3f4515c998e9aec701a7dbda99389 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 31 May 2025 15:23:01 +1000 Subject: [PATCH 342/521] Implement server mod packet handling --- Multiplayer/API/ServerAPIProvider.cs | 33 ++++++ .../Managers/Server/NetworkServer.cs | 100 ++++++++++++++++-- MultiplayerAPI/Interfaces/IServer.cs | 61 ++++++++++- 3 files changed, 182 insertions(+), 12 deletions(-) diff --git a/Multiplayer/API/ServerAPIProvider.cs b/Multiplayer/API/ServerAPIProvider.cs index 12df0676..88d20b7c 100644 --- a/Multiplayer/API/ServerAPIProvider.cs +++ b/Multiplayer/API/ServerAPIProvider.cs @@ -1,4 +1,5 @@ using MPAPI.Interfaces; +using MPAPI.Interfaces.Packets; using Multiplayer.Networking.Managers.Server; using Multiplayer.Utils; using System; @@ -27,6 +28,38 @@ public class ServerAPIProvider : IServer #endregion + #region Packet API + public void RegisterPacket(PacketHandler handler) where T : class, IPacket, new() + { + server.RegisterExternalPacket(handler); + } + public void RegisterSerializablePacket(PacketHandler handler) where T : class, ISerializablePacket, new() + { + server.RegisterExternalSerializablePacket(handler); + } + + + public void SendPacketToAll(T packet, bool reliable = true, IPlayer excludePlayer = null) where T : class, IPacket, new() + { + server.SendExternalPacketToAll(packet, reliable, excludePlayer.Id); + } + + public void SendSerializablePacketToAll(T packet, bool reliable = true, IPlayer excludePlayer = null) where T : class, ISerializablePacket, new() + { + server.SendExternalSerializablePacketToAll(packet, reliable, excludePlayer.Id); + } + + public void SendPacketToPlayer(T packet, IPlayer player, bool reliable = true) where T : class, IPacket, new() + { + server.SendExternalPacketToPlayer(packet, player.Id, reliable); + } + + public void SendSerializablePacketToPlayer(T packet, IPlayer player, bool reliable = true) where T : class, ISerializablePacket, new() + { + server.SendExternalSerializablePacketToPlayer(packet, player.Id, reliable); + } + #endregion + #region Server Util public float AnyPlayerSqrMag(GameObject item) => DvExtensions.AnyPlayerSqrMag(item); diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index 1c6474bc..2b39c204 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using DV; using DV.InventorySystem; using DV.Logic.Job; @@ -11,11 +8,14 @@ using Humanizer; using LiteNetLib; using LiteNetLib.Utils; +using MPAPI.Interfaces.Packets; +using Multiplayer.API; using Multiplayer.Components.Networking; +using Multiplayer.Components.Networking.Jobs; using Multiplayer.Components.Networking.Train; using Multiplayer.Components.Networking.World; -using Multiplayer.Components.Networking.Jobs; using Multiplayer.Networking.Data; +using Multiplayer.Networking.Data.Train; using Multiplayer.Networking.Packets.Clientbound; using Multiplayer.Networking.Packets.Clientbound.Jobs; using Multiplayer.Networking.Packets.Clientbound.SaveGame; @@ -24,16 +24,18 @@ using Multiplayer.Networking.Packets.Common; using Multiplayer.Networking.Packets.Common.Train; using Multiplayer.Networking.Packets.Serverbound; -using Multiplayer.Utils; -using UnityEngine; -using UnityModManagerNet; -using System.Net; using Multiplayer.Networking.Packets.Serverbound.Train; using Multiplayer.Networking.Packets.Unconnected; -using System.Text; -using Multiplayer.Networking.Data.Train; using Multiplayer.Networking.TransportLayers; -using MPAPI.Interfaces; +using Multiplayer.Utils; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Text; +using UnityEngine; +using UnityModManagerNet; +using static DV.Interaction.Inputs.InputManager; namespace Multiplayer.Networking.Managers.Server; @@ -165,6 +167,27 @@ protected override void Subscribe() netPacketProcessor.SubscribeNetSerializable(OnCommonItemChangePacket); } + //allow mods to register their own packets + public void RegisterExternalPacket(PacketHandler handler) where T : class, IPacket, new() + { + netPacketProcessor.SubscribeReusable((packet, peer) => + { + var serverPlayer = TryGetServerPlayer(peer, out var player) ? new ServerPlayerWrapper(player) : null; + handler(packet, serverPlayer); + }); + } + + public void RegisterExternalSerializablePacket(PacketHandler handler) where T : class, ISerializablePacket, new() + { + netPacketProcessor.SubscribeNetSerializable, ITransportPeer>((wrapper, peer) => + { + var serverPlayer = TryGetServerPlayer(peer, out var player) ? new ServerPlayerWrapper(player) : null; + handler(wrapper.Packet, serverPlayer); + }, + () => new ExternalSerializablePacketWrapper() + ); + } + private void OnLoaded() { if (!IsSinglePlayer) @@ -296,6 +319,7 @@ public override void OnConnectionRequest(NetDataReader requestData, IConnectionR kvp.Value.Send(writer, deliveryMethod); } } + private void SendNetSerializablePacketToAll(T packet, DeliveryMethod deliveryMethod) where T : INetSerializable, new() { NetDataWriter writer = WriteNetSerializablePacket(packet); @@ -314,6 +338,60 @@ public override void OnConnectionRequest(NetDataReader requestData, IConnectionR } } + #region Mod Packets + public void SendExternalPacketToAll(T packet, bool reliable) where T : class, IPacket, new() + { + var deliveryMethod = reliable ? DeliveryMethod.ReliableUnordered : DeliveryMethod.Unreliable; + SendPacketToAll(packet, deliveryMethod); + } + + public void SendExternalPacketToAll(T packet, bool reliable, byte excludePlayerId) where T : class, IPacket, new() + { + if (!TryGetPeer(excludePlayerId, out var peer)) + return; + + var deliveryMethod = reliable ? DeliveryMethod.ReliableUnordered : DeliveryMethod.Unreliable; + SendPacketToAll(packet, deliveryMethod, peer); + } + + public void SendExternalSerializablePacketToAll(T packet, bool reliable) where T : class, ISerializablePacket, new() + { + var deliveryMethod = reliable ? DeliveryMethod.ReliableUnordered : DeliveryMethod.Unreliable; + var wrapper = new ExternalSerializablePacketWrapper { Packet = packet }; + SendNetSerializablePacketToAll(wrapper, deliveryMethod); + } + + public void SendExternalSerializablePacketToAll(T packet, bool reliable, byte excludePlayerId) where T : class, ISerializablePacket, new() + { + if (!TryGetPeer(excludePlayerId, out var peer)) + return; + + var deliveryMethod = reliable ? DeliveryMethod.ReliableUnordered : DeliveryMethod.Unreliable; + var wrapper = new ExternalSerializablePacketWrapper { Packet = packet }; + SendNetSerializablePacketToAll(wrapper, deliveryMethod, peer); + } + + public void SendExternalPacketToPlayer(T packet, byte playerId, bool reliable) where T : class, IPacket, new() + { + if (!TryGetPeer(playerId, out var peer)) + return; + + var deliveryMethod = reliable ? DeliveryMethod.ReliableUnordered : DeliveryMethod.Unreliable; + SendPacket(peer, packet, deliveryMethod); + } + + public void SendExternalSerializablePacketToPlayer(T packet, byte playerId, bool reliable) where T : class, ISerializablePacket, new() + { + if (!TryGetPeer(playerId, out var peer)) + return; + + var deliveryMethod = reliable ? DeliveryMethod.ReliableUnordered : DeliveryMethod.Unreliable; + var wrapper = new ExternalSerializablePacketWrapper { Packet = packet }; + SendNetSerializablePacket(peer, wrapper, deliveryMethod); + } + + #endregion + public void KickPlayer(ITransportPeer peer) { //peer.Send(WritePacket(new ClientboundDisconnectPacket()),DeliveryMethod.ReliableUnordered); diff --git a/MultiplayerAPI/Interfaces/IServer.cs b/MultiplayerAPI/Interfaces/IServer.cs index 15fae412..a0fc9c97 100644 --- a/MultiplayerAPI/Interfaces/IServer.cs +++ b/MultiplayerAPI/Interfaces/IServer.cs @@ -1,3 +1,4 @@ +using MPAPI.Interfaces.Packets; using System; using System.Collections.Generic; using UnityEngine; @@ -10,11 +11,69 @@ public interface IServer event Action OnPlayerConnected; event Action OnPlayerDisconnected; + #region Server Properties int PlayerCount { get; } - //public IReadOnlyCollection Players { get; } + public IReadOnlyCollection Players { get; } + #endregion + + #region Packet API + /// + /// Register a packet type that uses automatic serialization + /// + /// Packet type implementing IPacket + /// Handler to call when packet is received + void RegisterPacket(PacketHandler handler) where T : class, IPacket, new(); + + /// + /// Register a packet type that uses manual serialization + /// + /// Packet type implementing ISerializablePacket + /// Handler to call when packet is received + void RegisterSerializablePacket(PacketHandler handler) where T : class, ISerializablePacket, new(); + + + /// + /// Send a packet to all connected players + /// + /// Packet type + /// Packet to send + /// Whether to send reliably + /// Exclude this player + void SendPacketToAll(T packet, bool reliable = true, IPlayer excludePlayer = null) where T : class, IPacket, new(); + + /// + /// Send a packet to all connected players + /// + /// Packet type + /// Packet to send + /// Whether to send reliably + /// Exclude this player + void SendSerializablePacketToAll(T packet, bool reliable = true, IPlayer excludePlayer = null) where T : class, ISerializablePacket, new(); + + /// + /// Send a packet to a specific player + /// + /// Packet type + /// Packet to send + /// Target player + /// Whether to send reliably + void SendPacketToPlayer(T packet, IPlayer player, bool reliable = true) where T : class, IPacket, new(); + + /// + /// Send a packet to a specific player + /// + /// Packet type + /// Packet to send + /// Target player + /// Whether to send reliably + void SendSerializablePacketToPlayer(T packet, IPlayer player, bool reliable = true) where T : class, ISerializablePacket, new(); + #endregion + + #region Server Util float AnyPlayerSqrMag(GameObject item); float AnyPlayerSqrMag(Vector3 anchor); + #endregion } From 45b646e4d230a8be29e2196f8c2653b210713cc6 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 31 May 2025 15:40:06 +1000 Subject: [PATCH 343/521] Update packet handling --- Multiplayer/API/ServerAPIProvider.cs | 4 ++-- .../Networking/Managers/Server/NetworkServer.cs | 4 ++-- MultiplayerAPI/Interfaces/IServer.cs | 4 ++-- MultiplayerAPI/Interfaces/Packets/PacketHandler.cs | 13 ++++++++++--- 4 files changed, 16 insertions(+), 9 deletions(-) diff --git a/Multiplayer/API/ServerAPIProvider.cs b/Multiplayer/API/ServerAPIProvider.cs index 88d20b7c..66b59c97 100644 --- a/Multiplayer/API/ServerAPIProvider.cs +++ b/Multiplayer/API/ServerAPIProvider.cs @@ -29,11 +29,11 @@ public class ServerAPIProvider : IServer #endregion #region Packet API - public void RegisterPacket(PacketHandler handler) where T : class, IPacket, new() + public void RegisterPacket(ServerPacketHandler handler) where T : class, IPacket, new() { server.RegisterExternalPacket(handler); } - public void RegisterSerializablePacket(PacketHandler handler) where T : class, ISerializablePacket, new() + public void RegisterSerializablePacket(ServerPacketHandler handler) where T : class, ISerializablePacket, new() { server.RegisterExternalSerializablePacket(handler); } diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index 2b39c204..7919b06e 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -168,7 +168,7 @@ protected override void Subscribe() } //allow mods to register their own packets - public void RegisterExternalPacket(PacketHandler handler) where T : class, IPacket, new() + public void RegisterExternalPacket(ServerPacketHandler handler) where T : class, IPacket, new() { netPacketProcessor.SubscribeReusable((packet, peer) => { @@ -177,7 +177,7 @@ protected override void Subscribe() }); } - public void RegisterExternalSerializablePacket(PacketHandler handler) where T : class, ISerializablePacket, new() + public void RegisterExternalSerializablePacket(ServerPacketHandler handler) where T : class, ISerializablePacket, new() { netPacketProcessor.SubscribeNetSerializable, ITransportPeer>((wrapper, peer) => { diff --git a/MultiplayerAPI/Interfaces/IServer.cs b/MultiplayerAPI/Interfaces/IServer.cs index a0fc9c97..9874e043 100644 --- a/MultiplayerAPI/Interfaces/IServer.cs +++ b/MultiplayerAPI/Interfaces/IServer.cs @@ -24,14 +24,14 @@ public interface IServer /// /// Packet type implementing IPacket /// Handler to call when packet is received - void RegisterPacket(PacketHandler handler) where T : class, IPacket, new(); + void RegisterPacket(ServerPacketHandler handler) where T : class, IPacket, new(); /// /// Register a packet type that uses manual serialization /// /// Packet type implementing ISerializablePacket /// Handler to call when packet is received - void RegisterSerializablePacket(PacketHandler handler) where T : class, ISerializablePacket, new(); + void RegisterSerializablePacket(ServerPacketHandler handler) where T : class, ISerializablePacket, new(); /// diff --git a/MultiplayerAPI/Interfaces/Packets/PacketHandler.cs b/MultiplayerAPI/Interfaces/Packets/PacketHandler.cs index 70197b4d..06617924 100644 --- a/MultiplayerAPI/Interfaces/Packets/PacketHandler.cs +++ b/MultiplayerAPI/Interfaces/Packets/PacketHandler.cs @@ -1,9 +1,16 @@ namespace MPAPI.Interfaces.Packets; /// -/// Delegate for handling received packets +/// Delegate for handling received packets on the server /// /// Packet type /// The received packet -/// The player who sent the packet (null if from server) -public delegate void PacketHandler(T packet, IPlayer sender) where T : class; +/// The player who sent the packet +public delegate void ServerPacketHandler(T packet, IPlayer sender) where T : class; + +/// +/// Delegate for handling received packets on the client +/// +/// Packet type +/// The received packet +public delegate void ClientPacketHandler(T packet) where T : class; From a04d6a75a13a93e08d26fcf43f8ed171c4a7e9b7 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 31 May 2025 15:40:20 +1000 Subject: [PATCH 344/521] Implement client packet API --- Multiplayer/API/ClientAPIProvider.cs | 24 ++++++++++++ .../Managers/Client/NetworkClient.cs | 36 +++++++++++++++++ MultiplayerAPI/Interfaces/IClient.cs | 39 ++++++++++++++++--- 3 files changed, 94 insertions(+), 5 deletions(-) diff --git a/Multiplayer/API/ClientAPIProvider.cs b/Multiplayer/API/ClientAPIProvider.cs index 04f058f3..1b774c7a 100644 --- a/Multiplayer/API/ClientAPIProvider.cs +++ b/Multiplayer/API/ClientAPIProvider.cs @@ -1,4 +1,5 @@ using MPAPI.Interfaces; +using MPAPI.Interfaces.Packets; using Multiplayer.Networking.Managers.Client; using Multiplayer.Networking.Managers.Server; using Multiplayer.Utils; @@ -6,6 +7,7 @@ using System.Collections.Generic; using System.Linq; using UnityEngine; +using static Humanizer.In; namespace Multiplayer.API { @@ -30,6 +32,28 @@ public class ClientAPIProvider : IClient #endregion + #region Packet API + public void RegisterPacket(ClientPacketHandler handler) where T : class, IPacket, new() + { + client.RegisterExternalPacket(handler); + } + public void RegisterSerializablePacket(ClientPacketHandler handler) where T : class, ISerializablePacket, new() + { + client.RegisterExternalSerializablePacket(handler); + } + + + public void SendPacketToServer(T packet, bool reliable = true) where T : class, IPacket, new() + { + client.SendExternalPacketToServer(packet, reliable); + } + + public void SendSerializablePacketToServer(T packet, bool reliable = true) where T : class, ISerializablePacket, new() + { + client.SendExternalSerializablePacketToServer(packet, reliable); + } + #endregion + #region Class Helpers internal ClientAPIProvider(NetworkClient clientInstance) { diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 689fe91f..32a6a780 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -12,6 +12,8 @@ using DV.WeatherSystem; using LiteNetLib; using LiteNetLib.Utils; +using MPAPI.Interfaces.Packets; +using Multiplayer.API; using Multiplayer.Components.MainMenu; using Multiplayer.Components.Networking; using Multiplayer.Components.Networking.Jobs; @@ -167,6 +169,25 @@ protected override void Subscribe() netPacketProcessor.SubscribeNetSerializable(OnCommonItemChangePacket); } + //allow mods to register their own packets + public void RegisterExternalPacket(ClientPacketHandler handler) where T : class, IPacket, new() + { + netPacketProcessor.SubscribeReusable((packet) => + { + handler(packet); + }); + } + + public void RegisterExternalSerializablePacket(ClientPacketHandler handler) where T : class, ISerializablePacket, new() + { + netPacketProcessor.SubscribeNetSerializable>((wrapper) => + { + handler(wrapper.Packet); + }, + () => new ExternalSerializablePacketWrapper() + ); + } + private void OnLoaded() { Log($"WorldStreamingInit.LoadingFinished()"); @@ -1018,6 +1039,21 @@ private void OnCommonPaintThemePacket(CommonPaintThemePacket packet) SendNetSerializablePacket(serverPeer, packet, deliveryMethod); } + + #region Mod Packets + public void SendExternalPacketToServer(T packet, bool reliable) where T : class, IPacket, new() + { + var deliveryMethod = reliable ? DeliveryMethod.ReliableUnordered : DeliveryMethod.Unreliable; + SendPacketToServer(packet, deliveryMethod); + } + + public void SendExternalSerializablePacketToServer(T packet, bool reliable) where T : class, ISerializablePacket, new() + { + var deliveryMethod = reliable ? DeliveryMethod.ReliableUnordered : DeliveryMethod.Unreliable; + var wrapper = new ExternalSerializablePacketWrapper { Packet = packet }; + SendNetSerializablePacketToServer(wrapper, deliveryMethod); + } + #endregion public void SendSaveGameDataRequest() { SendPacketToServer(new ServerboundSaveGameDataRequestPacket(), DeliveryMethod.ReliableOrdered); diff --git a/MultiplayerAPI/Interfaces/IClient.cs b/MultiplayerAPI/Interfaces/IClient.cs index c0421003..7364ab7e 100644 --- a/MultiplayerAPI/Interfaces/IClient.cs +++ b/MultiplayerAPI/Interfaces/IClient.cs @@ -1,3 +1,4 @@ +using MPAPI.Interfaces.Packets; using System; using System.Collections.Generic; @@ -17,9 +18,37 @@ public interface IClient bool IsConnected { get; } int Ping { get; } - //public abstract void SendPacketToServer(T packet, DeliveryMethod deliveryMethod) where T : class, new(); - - //public abstract void SendNetSerializablePacketToServer(T packet, DeliveryMethod deliveryMethod) where T : INetSerializable, new(); - - + #region Packet API + /// + /// Register a packet type that uses automatic serialization + /// + /// Packet type implementing IPacket + /// Handler to call when packet is received + void RegisterPacket(ClientPacketHandler handler) where T : class, IPacket, new(); + + /// + /// Register a packet type that uses manual serialization + /// + /// Packet type implementing ISerializablePacket + /// Handler to call when packet is received + void RegisterSerializablePacket(ClientPacketHandler handler) where T : class, ISerializablePacket, new(); + + + /// + /// Send a packet to the server + /// + /// Packet type + /// Packet to send + /// Whether to send reliably + void SendPacketToServer(T packet, bool reliable = true) where T : class, IPacket, new(); + + /// + /// Send a packet to all connected players + /// + /// Packet type + /// Packet to send + /// Whether to send reliably + void SendSerializablePacketToServer(T packet, bool reliable = true) where T : class, ISerializablePacket, new(); + + #endregion } From 1bb9b1fa52e322272f67f97d40c2acf976db111b Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 1 Jun 2025 11:25:29 +1000 Subject: [PATCH 345/521] Add interfaces for mapping game objects to NetIds Add method for getting NetId from TrainCar --- Multiplayer/API/APIProvider.cs | 15 +++++ Multiplayer/API/NetIdProvider.cs | 59 +++++++++++++++++++ .../Networking/Train/NetworkedTrainCar.cs | 11 ++++ MultiplayerAPI/Interfaces/IMultiplayerAPI.cs | 13 +++- MultiplayerAPI/Interfaces/INetId.cs | 8 +++ 5 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 Multiplayer/API/NetIdProvider.cs create mode 100644 MultiplayerAPI/Interfaces/INetId.cs diff --git a/Multiplayer/API/APIProvider.cs b/Multiplayer/API/APIProvider.cs index af766731..13a9a166 100644 --- a/Multiplayer/API/APIProvider.cs +++ b/Multiplayer/API/APIProvider.cs @@ -6,6 +6,11 @@ namespace Multiplayer.API { public class APIProvider : IMultiplayerAPI { + public APIProvider() + { + NetIdProvider.Instance.CheckInitialization(); + } + public bool IsMultiplayerLoaded => true; public bool IsConnected => NetworkLifecycle.Instance.IsClientRunning || NetworkLifecycle.Instance.IsServerRunning; @@ -15,5 +20,15 @@ public class APIProvider : IMultiplayerAPI public bool IsDedicatedServer => false; //feature not implemented public bool IsSinglePlayer => NetworkLifecycle.Instance.IsServerRunning && (NetworkLifecycle.Instance?.Server.IsSinglePlayer ?? false); + + public bool TryGetNetId(T obj, out ushort netId) where T : class + { + return NetIdProvider.Instance.TryGetNetId(obj, out netId); + } + + public bool TryGetObjectFromNetId(ushort netId, out T obj) where T : class + { + return NetIdProvider.Instance.TryGetObject(netId, out obj); + } } } diff --git a/Multiplayer/API/NetIdProvider.cs b/Multiplayer/API/NetIdProvider.cs new file mode 100644 index 00000000..d285319d --- /dev/null +++ b/Multiplayer/API/NetIdProvider.cs @@ -0,0 +1,59 @@ +using DV.Utils; +using JetBrains.Annotations; +using MPAPI.Interfaces; +using Multiplayer.Components.Networking.Train; +using System; +using System.Collections.Generic; + +namespace Multiplayer.API; + +public delegate bool TryGetNetIdDelegate(T obj, out ushort netId) where T : class; +public delegate bool TryGetObjectDelegate(ushort netId, out T obj) where T : class; + +internal class NetIdProvider : SingletonBehaviour, INetIdProvider +{ + private readonly Dictionary handlers = []; + + protected override void Awake() + { + base.Awake(); + RegisterHandler(NetworkedTrainCar.TryGetNetIdFromTrainCar, NetworkedTrainCar.GetTrainCar); + } + + public void RegisterHandler(TryGetNetIdDelegate tryGetNetId, TryGetObjectDelegate tryGetObject) where T : class + { + handlers[typeof(T)] = (tryGetNetId, tryGetObject); + } + + public bool TryGetNetId(T obj, out ushort netId) where T : class + { + netId = 0; + + if (obj == null) + return false; + + if (handlers.TryGetValue(typeof(T), out var handler) && handler is (TryGetNetIdDelegate tryGetNetId, TryGetObjectDelegate _)) + return tryGetNetId(obj, out netId); + + return false; + } + + public bool TryGetObject(ushort netId, out T obj) where T : class + { + obj = null; + + if (netId == 0) + return false; + + if (handlers.TryGetValue(typeof(T), out var handler) && handler is (TryGetNetIdDelegate _, TryGetObjectDelegate tryGetObject)) + return tryGetObject(netId, out obj); + + return false; + } + + [UsedImplicitly] + protected new static string AllowAutoCreate() + { + return $"[{nameof(NetIdProvider)}]"; + } +} diff --git a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs index f3020450..ae744c20 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs @@ -65,6 +65,17 @@ public static bool TryGetFromTrainCar(TrainCar trainCar, out NetworkedTrainCar n return trainCarsToNetworkedTrainCars.TryGetValue(trainCar, out networkedTrainCar); } + public static bool TryGetNetIdFromTrainCar(TrainCar trainCar, out ushort netId) + { + netId = 0; + + if (!trainCarsToNetworkedTrainCars.TryGetValue(trainCar, out var networkedTrainCar) || networkedTrainCar == false || networkedTrainCar.NetId == 0) + return false; + + netId = networkedTrainCar.NetId; + return true; + } + #endregion private const int MAX_COUPLER_ITERATIONS = 10; diff --git a/MultiplayerAPI/Interfaces/IMultiplayerAPI.cs b/MultiplayerAPI/Interfaces/IMultiplayerAPI.cs index 37494f78..32910980 100644 --- a/MultiplayerAPI/Interfaces/IMultiplayerAPI.cs +++ b/MultiplayerAPI/Interfaces/IMultiplayerAPI.cs @@ -1,4 +1,3 @@ -using System; namespace MPAPI.Interfaces; @@ -32,4 +31,16 @@ public interface IMultiplayerAPI /// bool IsSinglePlayer { get; } + /// + // Gets the NetId for an object + // returns true if the object has a NetId + /// + bool TryGetNetId(T obj, out ushort netId) where T : class; + + /// + // Gets the object for an NetId + // returns true if the object was found + /// + bool TryGetObjectFromNetId(ushort netId, out T obj) where T : class; + } diff --git a/MultiplayerAPI/Interfaces/INetId.cs b/MultiplayerAPI/Interfaces/INetId.cs new file mode 100644 index 00000000..ee2d2f54 --- /dev/null +++ b/MultiplayerAPI/Interfaces/INetId.cs @@ -0,0 +1,8 @@ + +namespace MPAPI.Interfaces; + +public interface INetIdProvider +{ + bool TryGetNetId(T obj, out ushort netId) where T : class; + bool TryGetObject(ushort netId, out T obj) where T : class; +} From ffc61d71137df35d5de554e3d3899ecfd172e121 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 1 Jun 2025 11:24:40 +1000 Subject: [PATCH 346/521] Cleanup --- Multiplayer/API/ClientPlayerWrapper.cs | 5 ++--- Multiplayer/Components/IdMonoBehaviour.cs | 1 - 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/Multiplayer/API/ClientPlayerWrapper.cs b/Multiplayer/API/ClientPlayerWrapper.cs index 6d6f54d2..568d89f2 100644 --- a/Multiplayer/API/ClientPlayerWrapper.cs +++ b/Multiplayer/API/ClientPlayerWrapper.cs @@ -1,6 +1,5 @@ using MPAPI.Interfaces; using Multiplayer.Components.Networking.Player; -using Multiplayer.Networking.Data; using System; using UnityEngine; @@ -29,6 +28,6 @@ public string Username public bool IsLoaded => true; // If we have the object, it's loaded public bool IsHost => _isHost; public int Ping => _networkedPlayer.GetPing(); - public bool IsOnCar => _networkedPlayer.IsOnCar; // You'll need to add this logic - public TrainCar OccupiedCar => _networkedPlayer.OccupiedCar; // You'll need to track this in NetworkedPlayer + public bool IsOnCar => _networkedPlayer.IsOnCar; + public TrainCar OccupiedCar => _networkedPlayer.OccupiedCar; } diff --git a/Multiplayer/Components/IdMonoBehaviour.cs b/Multiplayer/Components/IdMonoBehaviour.cs index 44e05f8f..732e4339 100644 --- a/Multiplayer/Components/IdMonoBehaviour.cs +++ b/Multiplayer/Components/IdMonoBehaviour.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using LiteNetLib.Utils; using Multiplayer.Components.Networking; using Multiplayer.Utils; using UnityEngine; From d00f26900a129a74aeb0a7981f7827a55a3b692e Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 1 Jun 2025 11:26:22 +1000 Subject: [PATCH 347/521] Add events for server and client creation and destruction --- MultiplayerAPI/MultiplayerAPI.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/MultiplayerAPI/MultiplayerAPI.cs b/MultiplayerAPI/MultiplayerAPI.cs index c135d5c0..8e5a9ea3 100644 --- a/MultiplayerAPI/MultiplayerAPI.cs +++ b/MultiplayerAPI/MultiplayerAPI.cs @@ -1,9 +1,16 @@ using MPAPI.Interfaces; +using System; namespace MPAPI; public static class MultiplayerAPI { + public static event Action ServerStarted; + public static event Action ClientStarted; + + public static event Action ServerStopped; + public static event Action ClientStopped; + private static IMultiplayerAPI _instance; private static IServer _server; private static IClient _client; @@ -44,6 +51,7 @@ internal static void RegisterAPI(IMultiplayerAPI apiInstance) internal static void RegisterClient(IClient client) { _client = client; + ClientStarted?.Invoke(client); } /// @@ -52,6 +60,7 @@ internal static void RegisterClient(IClient client) internal static void ClearClient() { _client = null; + ClientStopped?.Invoke(); } /// @@ -61,6 +70,7 @@ internal static void ClearClient() internal static void RegisterServer(IServer server) { _server = server; + ServerStarted?.Invoke(server); } @@ -70,5 +80,6 @@ internal static void RegisterServer(IServer server) internal static void ClearServer() { _server = null; + ServerStopped?.Invoke(); } } From 27df7653116ffd77873e1f164ff3878b9ee90f35 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 1 Jun 2025 11:26:41 +1000 Subject: [PATCH 348/521] Continue work on Test/Example --- MultiplayerAPI Tests/MultiplayerAPITest.cs | 19 ++++ .../Packets/ComplexModPacket.cs | 49 +++++++++ .../Packets/SimpleModPacket.cs | 24 ++++ .../Packets/SimplePacketWithNetId.cs | 22 ++++ .../TestComponents/ClientTest.cs | 20 ++++ .../TestComponents/ServerTest.cs | 104 ++++++++++++++++++ 6 files changed, 238 insertions(+) create mode 100644 MultiplayerAPI Tests/Packets/ComplexModPacket.cs create mode 100644 MultiplayerAPI Tests/Packets/SimpleModPacket.cs create mode 100644 MultiplayerAPI Tests/Packets/SimplePacketWithNetId.cs create mode 100644 MultiplayerAPI Tests/TestComponents/ClientTest.cs create mode 100644 MultiplayerAPI Tests/TestComponents/ServerTest.cs diff --git a/MultiplayerAPI Tests/MultiplayerAPITest.cs b/MultiplayerAPI Tests/MultiplayerAPITest.cs index e7c5ce6f..73eb56dc 100644 --- a/MultiplayerAPI Tests/MultiplayerAPITest.cs +++ b/MultiplayerAPI Tests/MultiplayerAPITest.cs @@ -4,6 +4,8 @@ using UnityEngine; using UnityModManagerNet; using MPAPI; +using MPAPI.Interfaces; +using MultiplayerAPITest.TestComponents; namespace MultiplayerAPITest; @@ -26,6 +28,11 @@ public static bool Load(UnityModManager.ModEntry modEntry) { Log($"Multiplayer Mod is loaded: {MultiplayerAPI.IsMultiplayerLoaded}"); + if (MultiplayerAPI.IsMultiplayerLoaded) + { + MultiplayerAPI.ServerStarted += OnServerStarted; + MultiplayerAPI.ClientStarted += OnClientStarted; + } Log("Patching..."); harmony = new Harmony(ModEntry.Info.Id); @@ -43,6 +50,18 @@ public static bool Load(UnityModManager.ModEntry modEntry) return true; } + private static void OnServerStarted(IServer server) + { + GameObject go = new GameObject("MPAPI ServerTest", [typeof(ServerTest)]); + GameObject.DontDestroyOnLoad(go); + } + + private static void OnClientStarted(IClient client) + { + GameObject go = new GameObject("MPAPI ClientTest", [typeof(ServerTest)]); + GameObject.DontDestroyOnLoad(go); + } + #region Logging public static void LogDebug(Func resolver) diff --git a/MultiplayerAPI Tests/Packets/ComplexModPacket.cs b/MultiplayerAPI Tests/Packets/ComplexModPacket.cs new file mode 100644 index 00000000..be7a7e59 --- /dev/null +++ b/MultiplayerAPI Tests/Packets/ComplexModPacket.cs @@ -0,0 +1,49 @@ +using MPAPI.Interfaces.Packets; +using System.Collections.Generic; +using System.IO; +using UnityEngine; + +namespace MultiplayerAPITest.Packets +{ + internal class ComplexModPacket : ISerializablePacket + { + //Complex packets require manual serialization + //Altenatively, implement methods to convert complex data structures to/from arrays and use the automatic serialization + + public Dictionary CarToPositionMap { get; set; } + + public void Deserialize(BinaryReader reader) + { + //retrieve the dictionary length + var length = reader.ReadInt32(); + + CarToPositionMap = []; + + //retrieve each key and value + for (int i = 0; i < length; i++) + { + var key = reader.ReadString(); + var x = reader.ReadSingle(); + var y = reader.ReadSingle(); + var z = reader.ReadSingle(); + + CarToPositionMap.Add(key, new Vector3 (x, y, z)); + } + } + + public void Serialize(BinaryWriter writer) + { + //write out the length of the dictionary + writer.Write(CarToPositionMap.Count); + + //write out each key and value + foreach (var kvp in CarToPositionMap) + { + writer.Write(kvp.Key); + writer.Write(kvp.Value.x); + writer.Write(kvp.Value.y); + writer.Write(kvp.Value.z); + } + } + } +} diff --git a/MultiplayerAPI Tests/Packets/SimpleModPacket.cs b/MultiplayerAPI Tests/Packets/SimpleModPacket.cs new file mode 100644 index 00000000..27ab2a4a --- /dev/null +++ b/MultiplayerAPI Tests/Packets/SimpleModPacket.cs @@ -0,0 +1,24 @@ +using MPAPI.Interfaces.Packets; +using UnityEngine; + +namespace MultiplayerAPITest.Packets +{ + internal class SimpleModPacket : IPacket + { + //Public properties are automatically serialised + //acceptable types are: + // Primitives (bool, byte, sbyte, short, ushort, int, uint, long, ulong, float, double, string, char, IPEndPoint, Guid) + // Arrays of primitives + // Enums derived from primitives e.g. `enum MyEnum : byte` + // UnityEngine: Vector2, Vector3, Quarternion + + //Be mindful of the amount of data per packet. + // Avoid sending long strings or large structures + // Consider using an numeric Id system to represent objects + // Use the MP API to get the NetId (ushort) for TrainCars, rather than the car's string Id + public string CarId { get; set; } //It's better to use ushort and call `MultiplayerAPI.GetTrainCarNetId(TrainCar)` + //See SimplePacketWithNetId for an example + public Vector3 Position { get; set; } + + } +} diff --git a/MultiplayerAPI Tests/Packets/SimplePacketWithNetId.cs b/MultiplayerAPI Tests/Packets/SimplePacketWithNetId.cs new file mode 100644 index 00000000..c9aa8bad --- /dev/null +++ b/MultiplayerAPI Tests/Packets/SimplePacketWithNetId.cs @@ -0,0 +1,22 @@ +using MPAPI.Interfaces.Packets; + + +namespace MultiplayerAPITest.Packets +{ + + //for dynamic info a hash or other numbering system could be used, rather than a static enum. + public enum WheelArrangement : byte + { + Default = 0, + American440 = 1, + Atlantic442 = 2, + Reading444 = 3 + } + + //example packet with netId and an enum. + internal class SimplePacketWithNetId : IPacket + { + public ushort CarNetId { get; set; } + public WheelArrangement WheelArrangement { get; set; } + } +} diff --git a/MultiplayerAPI Tests/TestComponents/ClientTest.cs b/MultiplayerAPI Tests/TestComponents/ClientTest.cs new file mode 100644 index 00000000..786daa0d --- /dev/null +++ b/MultiplayerAPI Tests/TestComponents/ClientTest.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using UnityEngine; + +namespace MultiplayerAPITest.TestComponents +{ + internal class ClientTest : MonoBehaviour + { + protected void Awake() { } + + protected void Start() { } + + protected void Update() { } + + protected void OnDestroy() { } + } +} diff --git a/MultiplayerAPI Tests/TestComponents/ServerTest.cs b/MultiplayerAPI Tests/TestComponents/ServerTest.cs new file mode 100644 index 00000000..eb6a4b8e --- /dev/null +++ b/MultiplayerAPI Tests/TestComponents/ServerTest.cs @@ -0,0 +1,104 @@ +using MPAPI; +using MPAPI.Interfaces; +using MultiplayerAPITest.Packets; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using UnityEngine; + +namespace MultiplayerAPITest.TestComponents; + + +internal class ServerTest : MonoBehaviour +{ + const string LogPrefix = "ServerTest"; + + IServer server; + + protected void Awake() + { + server = MultiplayerAPI.Server; + + //subscribe to packets + Subscribe(); + } + + protected void Start() { } + + protected void Update() { } + + protected void OnDestroy() { } + + + //Setup subscriptions for the packets we want to/expect to receive + private void Subscribe() + { + server.RegisterPacket(OnTestSimpleModPacket); + server.RegisterPacket(OnSimplePacketWithNetId); + server.RegisterSerializablePacket(OnTestComplexModPacket); + } + + + #region Packet Callbacks + + //method called when a `TestSimplePacket` packet is received + private void OnTestSimpleModPacket(SimpleModPacket packet, IPlayer player) + { + + Log($"Received {packet.GetType()} from player: {player.Username}"); + + Log($"CarId: {packet.CarId}, Position: {packet.Position}"); + + } + + //method called when a `TestSimplePacket` packet is received + private void OnSimplePacketWithNetId(SimplePacketWithNetId packet, IPlayer player) + { + + Log($"Received {packet.GetType()} from player: {player.Username}"); + + Log($"CarId: {packet.CarId}, Position: {packet.Position}"); + + } + + //method called when a `TestComplexModPacket` packet is received + private void OnTestComplexModPacket(ComplexModPacket packet, IPlayer player) + { + + Log($"Received {packet.GetType()} from player: {player.Username}"); + + StringBuilder sb = new("\r\nPacket Data"); + + foreach (var kvp in packet.CarToPositionMap) + sb.AppendLine($"CarId: {kvp.Key}, Position: {kvp.Value}"); + + Log(sb.ToString()); + } + #endregion + + #region Logging + + public void LogDebug(Func resolver) + { + MultiplayerAPITest.LogDebug(() => $"{LogPrefix} {resolver.Invoke()}"); + } + + public void Log(object msg) + { + MultiplayerAPITest.Log($"{LogPrefix} {msg}"); + } + + public void LogWarning(object msg) + { + MultiplayerAPITest.LogWarning($"{LogPrefix} {msg}"); + } + + public void LogError(object msg) + { + MultiplayerAPITest.LogError($"{LogPrefix} {msg}"); + } + + #endregion +} From 4cb19d7b9936e121977720c73ae414b246280bad Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 1 Jun 2025 12:11:31 +1000 Subject: [PATCH 349/521] Fix issue with service stations not loading --- .../World/NetworkedPitStopStation.cs | 98 +++++++++++-------- 1 file changed, 57 insertions(+), 41 deletions(-) diff --git a/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs b/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs index bb597a12..23e48391 100644 --- a/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs +++ b/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs @@ -71,11 +71,13 @@ public static void InitialisePitStops() const float FAUCET_SNAP_THRESHOLD = 0.005f; const float DEFAULT_DISABLER_SQR_DISTANCE = 250000f; + const float DEFAULT_DISABLER_INTERVAL = 2f; const float NEARBY_REMOVAL_DELAY = 3f; #region Server variables private Dictionary playerToLastNearbyTime; private float disablerSqrDistance = DEFAULT_DISABLER_SQR_DISTANCE; + private float disablerCheckInterval = DEFAULT_DISABLER_INTERVAL; private bool processingAsHost = false; #endregion @@ -126,9 +128,12 @@ protected override void Awake() var disabler = GetComponentInParent(); if (disabler != null) + { disablerSqrDistance = disabler.disableSqrDistance; + disablerCheckInterval = disabler.checkPeriodPerGO; + } - NetworkLifecycle.Instance.OnTick += PlayerDistanceChecker; + StartCoroutine(PlayerDistanceChecker()); //ensure host can interact Refreshed = true; @@ -145,6 +150,9 @@ protected override void OnDestroy() { pitStopStationToNetworkedPitStopStation.Remove(Station); + if (NetworkLifecycle.Instance.IsHost()) + StopCoroutine(PlayerDistanceChecker()); + if (carSelectorGrab != null) { carSelectorGrab.Grabbed -= CarSelectorGrabbed; @@ -309,60 +317,68 @@ public void OnPlayerDisconnect(ITransportPeer peer) //Multiplayer.LogWarning($"OnPlayerDisconnect()"); } - private void PlayerDistanceChecker(uint tick) + private IEnumerator PlayerDistanceChecker() { - //if not active then there is no one close by - if (gameObject == null || !gameObject.activeInHierarchy || Station == null || Station.pitstop == null) - return; + //wait for game to finish loading + yield return new WaitForSeconds(1f); - foreach (var player in NetworkLifecycle.Instance.Server.ServerPlayers) + while (true) { - if (player.Id == NetworkLifecycle.Instance.Server.SelfId || !player.IsLoaded) - continue; - - float sqrDistance = (player.WorldPosition - transform.position).sqrMagnitude; - - bool initialised = playerToLastNearbyTime.TryGetValue(player.Id, out float lastVisit); + yield return new WaitForSeconds(disablerCheckInterval); - if (sqrDistance > disablerSqrDistance) + //if not active then there is no one close by + if (gameObject != null && gameObject.activeInHierarchy && Station != null && Station.pitstop != null) { - // Too far away for too long, stop tracking - if ((Time.time - lastVisit) > NEARBY_REMOVAL_DELAY) - playerToLastNearbyTime.Remove(player.Id); - - continue; - } + foreach (var player in NetworkLifecycle.Instance.Server.ServerPlayers) + { + if (player.Id == NetworkLifecycle.Instance.Server.SelfId || !player.IsLoaded) + continue; - //player nearby recently, update time - playerToLastNearbyTime[player.Id] = Time.time; + float sqrDistance = (player.WorldPosition - transform.position).sqrMagnitude; - if (!initialised) - { - if (!NetworkLifecycle.Instance.Server.TryGetPeer(player.Id, out var peer)) - continue; + bool initialised = playerToLastNearbyTime.TryGetValue(player.Id, out float lastVisit); - if (Station.pitstop.IsCarInPitStop()) - { - // One struct per module type - var resourceCount = Station.locoResourceModules.resourceModules.Count(); - LocoResourceModuleData[] stateData = new LocoResourceModuleData[resourceCount]; - int i; - for (i = 0; i < resourceCount; i++) + if (sqrDistance > disablerSqrDistance) { - stateData[i] = LocoResourceModuleData.From(Station.locoResourceModules.resourceModules[i]); + // Too far away for too long, stop tracking + if ((Time.time - lastVisit) > NEARBY_REMOVAL_DELAY) + playerToLastNearbyTime.Remove(player.Id); + + continue; } - PitStopPlugData[] plugData = new PitStopPlugData[resourceToPluggableObject.Count]; + //player nearby recently, update time + playerToLastNearbyTime[player.Id] = Time.time; - i = 0; - foreach (var plug in resourceToPluggableObject) + if (!initialised) { - plugData[i] = PitStopPlugData.From(plug.Value); - i++; + if (!NetworkLifecycle.Instance.Server.TryGetPeer(player.Id, out var peer)) + continue; + + if (Station.pitstop.IsCarInPitStop()) + { + // One struct per module type + var resourceCount = Station.locoResourceModules.resourceModules.Count(); + LocoResourceModuleData[] stateData = new LocoResourceModuleData[resourceCount]; + int i; + for (i = 0; i < resourceCount; i++) + { + stateData[i] = LocoResourceModuleData.From(Station.locoResourceModules.resourceModules[i]); + } + + PitStopPlugData[] plugData = new PitStopPlugData[resourceToPluggableObject.Count]; + + i = 0; + foreach (var plug in resourceToPluggableObject) + { + plugData[i] = PitStopPlugData.From(plug.Value); + i++; + } + + // Send current state + NetworkLifecycle.Instance.Server.SendPitStopBulkDataPacket(NetId, Station.pitstop.carList.Count, stateData, plugData, peer); + } } - - // Send current state - NetworkLifecycle.Instance.Server.SendPitStopBulkDataPacket(NetId, Station.pitstop.carList.Count, stateData, plugData, peer); } } } From 17a60181d5cb3179dc7b9c27e868f870f3255838 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 1 Jun 2025 13:13:35 +1000 Subject: [PATCH 350/521] Fix bulk Pitstop updates --- .../World/NetworkedPitStopStation.cs | 16 +++-- .../World/NetworkedPluggableObject.cs | 46 +++++++++--- .../Networking/Data/PitStopPlugData.cs | 4 +- .../Networking/Data/PitStopPlugMappingData.cs | 56 +++++++++++++++ .../Data/PitStopStationMappingData.cs | 70 ------------------- .../Networking/Managers/NetworkManager.cs | 2 +- .../Managers/Server/NetworkServer.cs | 3 +- .../CommonPitStopPlugInteractionPacket.cs | 5 -- 8 files changed, 109 insertions(+), 93 deletions(-) create mode 100644 Multiplayer/Networking/Data/PitStopPlugMappingData.cs delete mode 100644 Multiplayer/Networking/Data/PitStopStationMappingData.cs diff --git a/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs b/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs index 23e48391..aec90a00 100644 --- a/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs +++ b/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs @@ -86,7 +86,7 @@ public static void InitialisePitStops() public PitStopStation Station { get; set; } public string StationName { get; private set; } - private ResourceType[] resourceTypes = Array.Empty(); + private ResourceType[] resourceTypes = []; private GrabHandlerHingeJoint carSelectorGrab; private GrabHandlerHingeJoint faucetPositionerGrab; @@ -756,31 +756,37 @@ public void ProcessBulkUpdate(ClientboundPitStopBulkUpdatePacket packet) // Make sure the data elements exist prior to attempting to load them InitialiseData(); - Multiplayer.LogDebug(() => $"PitStop bulk data car count matches"); + Multiplayer.LogDebug(() => $"PitStop bulk data car count matches. Station module count: {Station?.locoResourceModules?.resourceModules?.Count()}, Packet resource count: {packet?.ResourceData?.Count()}"); // Load the data for each car and resource module foreach (var resource in packet.ResourceData) { var module = Station.locoResourceModules.resourceModules.FirstOrDefault(lm => lm.resourceType == resource.ResourceType); - if (module) + if (module != null) if (module.resourceData.Count == resource.Values.Count()) for (int i = 0; i < module.resourceData.Count; i++) - module.resourceData[i].unitsToBuy = resource.Values[i]; + module.SetUnitsToBuy(resource.Values[i]); + //module.resourceData[i].unitsToBuy = resource.Values[i]; else Multiplayer.LogWarning($"PitStop bulk data count mismatch post-force: {module.resourceData.Count} != {resource.Values.Count()}"); else Multiplayer.LogWarning($"PitStop module not found for resource type: {resource.ResourceType}"); } + Multiplayer.LogDebug(() => $"PitStop bulk data Plugs"); + //sync plugs foreach (var plug in packet.PlugData) { - //todo: set plug states + NetworkedPluggableObject.Get(plug.NetId, out var netPlug); + netPlug.ProcessBulkUpdate(plug); } // Mark data as refreshed to allow player interactions Refreshed = true; + + Multiplayer.LogDebug(() => $"PitStop bulk data Refreshed"); } /// diff --git a/Multiplayer/Components/Networking/World/NetworkedPluggableObject.cs b/Multiplayer/Components/Networking/World/NetworkedPluggableObject.cs index b8abb57b..064c5016 100644 --- a/Multiplayer/Components/Networking/World/NetworkedPluggableObject.cs +++ b/Multiplayer/Components/Networking/World/NetworkedPluggableObject.cs @@ -25,7 +25,7 @@ public static bool Get(ushort netId, out NetworkedPluggableObject obj) } #endregion - protected override bool IsIdServerAuthoritative => true; + protected override bool IsIdServerAuthoritative => false; #region Server Variables public PlugInteractionType CurrentInteraction { get; set; } @@ -49,10 +49,15 @@ public static bool Get(ushort netId, out NetworkedPluggableObject obj) #region Unity protected override void Awake() { - base.Awake(); + if (NetId == 0) + base.Awake(); PluggableObject = GetComponent(); Multiplayer.LogDebug(() => $"NetworkedPluggableObject.Awake() {PluggableObject?.controlBase?.spec?.name}, {transform.parent.name}"); + Multiplayer.LogDebug(() => $"NetworkedPluggableObject.Awake() {this.GetObjectPath()}, netId: {NetId}"); + + if (NetworkLifecycle.Instance.IsHost()) + Refreshed = true; } protected IEnumerator Start() @@ -132,6 +137,26 @@ public bool ValidateInteraction(CommonPitStopPlugInteractionPacket packet, Serve public void ProcessPacket(CommonPitStopPlugInteractionPacket packet) { var interaction = (PlugInteractionType)packet.InteractionType; + ProcessInteraction(interaction, packet.PlayerId, packet.TrainCarNetId, packet.IsLeftSide); + } + + public void ProcessBulkUpdate(PitStopPlugData data) + { + var interaction = data.State; + ProcessInteraction(interaction, data.PlayerId, data.TrainCarNetId, data.IsLeftSide); + + if (data.State == PlugInteractionType.Dropped) + { + transform.position = data.Position + WorldMover.currentMove; + transform.rotation = data.Rotation; + } + + Refreshed = true; + } + + public void ProcessInteraction(PlugInteractionType interaction, byte playerId, ushort trainNetId, bool isLeftSide) + { + bool result; NetworkedPlayer player = null; @@ -145,7 +170,7 @@ public void ProcessPacket(CommonPitStopPlugInteractionPacket packet) case PlugInteractionType.PickedUp: //Handle the picked up state isGrabbed = true; - playerHolding = packet.PlayerId; + playerHolding = playerId; PluggableObject.controlGrabbed = true; BlockInteraction(true); @@ -207,7 +232,7 @@ public void ProcessPacket(CommonPitStopPlugInteractionPacket packet) break; case PlugInteractionType.DockSocket: - Multiplayer.LogDebug(() => $"ProcessPacket() NetId: {NetId}, DockSocket, trainCar: {packet.TrainCarNetId}, isLeft: {packet.IsLeftSide}"); + Multiplayer.LogDebug(() => $"ProcessPacket() NetId: {NetId}, DockSocket, trainCar: {trainNetId}, isLeft: {isLeftSide}"); if (isGrabbed) { @@ -218,7 +243,7 @@ public void ProcessPacket(CommonPitStopPlugInteractionPacket packet) } } - if (NetworkedTrainCar.GetTrainCar(packet.TrainCarNetId, out var trainCar)) + if (NetworkedTrainCar.GetTrainCar(trainNetId, out var trainCar)) { isGrabbed = false; playerHolding = 0; @@ -228,20 +253,20 @@ public void ProcessPacket(CommonPitStopPlugInteractionPacket packet) PluggableObject.Unplug(); var sockets = trainCar.GetComponentsInChildren(); - if (packet.IsLeftSide) + if (isLeftSide) { result = PluggableObject.InstantSnapTo(sockets[0]); - Multiplayer.LogDebug(() => $"ProcessPacket() NetId: {NetId}, DockSocket, trainCar: {packet.TrainCarNetId}, isLeft: {packet.IsLeftSide}, result: {result}"); + Multiplayer.LogDebug(() => $"ProcessPacket() NetId: {NetId}, DockSocket, trainCar: {trainNetId}, isLeft: {isLeftSide}, result: {result}"); } else { result = PluggableObject.InstantSnapTo(sockets[1]); - Multiplayer.LogDebug(() => $"ProcessPacket() NetId: {NetId}, DockSocket, trainCar: {packet.TrainCarNetId}, isLeft: {packet.IsLeftSide}, result: {result}"); + Multiplayer.LogDebug(() => $"ProcessPacket() NetId: {NetId}, DockSocket, trainCar: {trainNetId}, isLeft: {isLeftSide}, result: {result}"); } } else { - Multiplayer.LogDebug(() => $"ProcessPacket() NetId: {NetId}, DockSocket, trainCar: {packet.TrainCarNetId}. TrainCar not found!"); + Multiplayer.LogDebug(() => $"ProcessPacket() NetId: {NetId}, DockSocket, trainCar: {trainNetId}. TrainCar not found!"); } break; } @@ -261,6 +286,9 @@ private void BlockInteraction(bool block) public void InitPitStop(NetworkedPitStopStation netPitStop) { + if (NetId == 0) + base.Awake(); + if(plugToStation.TryGetValue(this, out _)) { Multiplayer.LogWarning($"Lookup cache 'plugToStation' already contains NetworkedPitStopStation \"{netPitStop?.StationName}\", skipping Init"); diff --git a/Multiplayer/Networking/Data/PitStopPlugData.cs b/Multiplayer/Networking/Data/PitStopPlugData.cs index 3ab56afb..31b224d7 100644 --- a/Multiplayer/Networking/Data/PitStopPlugData.cs +++ b/Multiplayer/Networking/Data/PitStopPlugData.cs @@ -8,7 +8,7 @@ namespace Multiplayer.Networking.Data; public readonly struct PitStopPlugData(ushort netId, PlugInteractionType state, byte playerId, ushort trainCarNetId, bool isLeft, Vector3 pos, Quaternion rot) { - public readonly ushort NetID = netId; + public readonly ushort NetId = netId; public readonly byte PlayerId = playerId; public readonly PlugInteractionType State = state; public readonly ushort TrainCarNetId = trainCarNetId; @@ -32,7 +32,7 @@ public static PitStopPlugData From(NetworkedPluggableObject plugData) public static void Serialize(NetDataWriter writer, PitStopPlugData data) { - writer.Put(data.NetID); + writer.Put(data.NetId); writer.Put((byte)data.State); switch (data.State) diff --git a/Multiplayer/Networking/Data/PitStopPlugMappingData.cs b/Multiplayer/Networking/Data/PitStopPlugMappingData.cs new file mode 100644 index 00000000..f96192cc --- /dev/null +++ b/Multiplayer/Networking/Data/PitStopPlugMappingData.cs @@ -0,0 +1,56 @@ +using DV.ThingTypes; +using LiteNetLib.Utils; +using Multiplayer.Components.Networking.World; +using System.Collections.Generic; + +namespace Multiplayer.Networking.Data; + +public readonly struct PitStopPlugMappingData(ushort netId, Dictionary plugMapping) +{ + public readonly ushort NetId = netId; + public readonly Dictionary PlugMapping = plugMapping; + + + public static PitStopPlugMappingData From(NetworkedPitStopStation netStation) + { + var netId = netStation.NetId; + var plugMapping = netStation.GetPluggables(); + + return new PitStopPlugMappingData + ( + netId, + plugMapping + ); + } + + public static void Serialize(NetDataWriter writer, PitStopPlugMappingData data) + { + writer.Put(data.NetId); + + writer.Put(data.PlugMapping.Count); + foreach (var kvp in data.PlugMapping) + { + writer.Put((int)kvp.Key); + writer.Put(kvp.Value); + } + } + + public static PitStopPlugMappingData Deserialize(NetDataReader reader) + { + var netId = reader.GetUShort(); + + var dictCount = reader.GetInt(); + + Dictionary plugMapping = []; + for (int i = 0; i < dictCount; i++) + { + plugMapping.Add((ResourceType)reader.GetInt(), reader.GetUShort()); + } + + return new PitStopPlugMappingData + ( + netId, + plugMapping + ); + } +} diff --git a/Multiplayer/Networking/Data/PitStopStationMappingData.cs b/Multiplayer/Networking/Data/PitStopStationMappingData.cs deleted file mode 100644 index b9bb10e6..00000000 --- a/Multiplayer/Networking/Data/PitStopStationMappingData.cs +++ /dev/null @@ -1,70 +0,0 @@ -using DV.ThingTypes; -using LiteNetLib.Utils; -using Multiplayer.Components.Networking.World; -using Multiplayer.Networking.Serialization; -using System.Collections.Generic; -using UnityEngine; - -namespace Multiplayer.Networking.Data; - -public readonly struct PitStopStationMappingData(ushort netId, Vector3 location, int selectedCar, Dictionary plugMapping) -{ - public readonly ushort NetId = netId; - public readonly Vector3 Location = location; - public readonly int SelectedCar = selectedCar; - public readonly Dictionary PlugMapping = plugMapping; - - - public static PitStopStationMappingData From(NetworkedPitStopStation netStation) - { - var netId = netStation.NetId; - var location = netStation.transform.position - WorldMover.currentMove; - var selectedCar = netStation.Station?.pitstop?.SelectedIndex ?? 0; - var plugMapping = netStation.GetPluggables(); - - return new PitStopStationMappingData - ( - netId, - location, - selectedCar, - plugMapping - ); - } - - public static void Serialize(NetDataWriter writer, PitStopStationMappingData data) - { - writer.Put(data.NetId); - Vector3Serializer.Serialize(writer, data.Location); - writer.Put(data.SelectedCar); - - writer.Put(data.PlugMapping.Count); - foreach (var kvp in data.PlugMapping) - { - writer.Put((int)kvp.Key); - writer.Put(kvp.Value); - } - } - - public static PitStopStationMappingData Deserialize(NetDataReader reader) - { - var netId = reader.GetUShort(); - var location = Vector3Serializer.Deserialize(reader); - var selectedCar = reader.GetInt(); - - var dictCount = reader.GetInt(); - - Dictionary plugMapping = []; - for (int i = 0; i < dictCount; i++) - { - plugMapping.Add((ResourceType)reader.GetInt(), reader.GetUShort()); - } - - return new PitStopStationMappingData - ( - netId, - location, - selectedCar, - plugMapping - ); - } -} diff --git a/Multiplayer/Networking/Managers/NetworkManager.cs b/Multiplayer/Networking/Managers/NetworkManager.cs index 96956b0a..2a0c5947 100644 --- a/Multiplayer/Networking/Managers/NetworkManager.cs +++ b/Multiplayer/Networking/Managers/NetworkManager.cs @@ -59,7 +59,7 @@ private void RegisterNestedTypes() netPacketProcessor.RegisterNestedType(TrainsetMovementPart.Serialize, TrainsetMovementPart.Deserialize); netPacketProcessor.RegisterNestedType(TrainsetSpawnPart.Serialize, TrainsetSpawnPart.Deserialize); netPacketProcessor.RegisterNestedType(TrainCarHealthData.Serialize, TrainCarHealthData.Deserialize); - netPacketProcessor.RegisterNestedType(PitStopStationMappingData.Serialize, PitStopStationMappingData.Deserialize); + netPacketProcessor.RegisterNestedType(PitStopPlugMappingData.Serialize, PitStopPlugMappingData.Deserialize); netPacketProcessor.RegisterNestedType(LocoResourceModuleData.Serialize, LocoResourceModuleData.Deserialize); netPacketProcessor.RegisterNestedType(PitStopPlugData.Serialize, PitStopPlugData.Deserialize); netPacketProcessor.RegisterNestedType(Vector2Serializer.Serialize, Vector2Serializer.Deserialize); diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index 53cd8122..f52e0795 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -592,7 +592,7 @@ public void SendPitStopBulkDataPacket(ushort netId, int carCount, LocoResourceMo }; if (peer == null) - SendPacketToAll(packet, DeliveryMethod.ReliableOrdered, SelfPeer); + SendPacketToAll(packet, DeliveryMethod.ReliableOrdered); else SendPacket(peer, packet, DeliveryMethod.ReliableOrdered); } @@ -1230,6 +1230,7 @@ private void OnCommonPitStopInteractionPacket(CommonPitStopInteractionPacket pac else LogWarning($"OnCommonPitStopInteractionPacket() Failed to find PitStopStation with netId: {packet.NetId}"); } + private void OnCommonPitStopPlugInteractionPacket(CommonPitStopPlugInteractionPacket packet, ITransportPeer peer) { bool foundPlayer = TryGetServerPlayer(peer, out var player); diff --git a/Multiplayer/Networking/Packets/Common/CommonPitStopPlugInteractionPacket.cs b/Multiplayer/Networking/Packets/Common/CommonPitStopPlugInteractionPacket.cs index 1ecd922c..00049c3d 100644 --- a/Multiplayer/Networking/Packets/Common/CommonPitStopPlugInteractionPacket.cs +++ b/Multiplayer/Networking/Packets/Common/CommonPitStopPlugInteractionPacket.cs @@ -1,8 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using UnityEngine; namespace Multiplayer.Networking.Packets.Common; From 04a8d68ac6343812a3df7ff38984c61cefd7e5d1 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 1 Jun 2025 13:14:33 +1000 Subject: [PATCH 351/521] Clean up cash register logic --- .../World/NetworkedCashRegisterWithModules.cs | 60 ++++++++++++------- .../Managers/Server/NetworkServer.cs | 2 +- ...mmonCashRegisterWithModulesActionPacket.cs | 8 +-- 3 files changed, 43 insertions(+), 27 deletions(-) diff --git a/Multiplayer/Components/Networking/World/NetworkedCashRegisterWithModules.cs b/Multiplayer/Components/Networking/World/NetworkedCashRegisterWithModules.cs index f6b7aa63..7bbcdb1c 100644 --- a/Multiplayer/Components/Networking/World/NetworkedCashRegisterWithModules.cs +++ b/Multiplayer/Components/Networking/World/NetworkedCashRegisterWithModules.cs @@ -62,7 +62,7 @@ public static void InitialiseCashRegisters() protected override bool IsIdServerAuthoritative => false; #region Server Variables - + bool processingAction = false; #endregion #region Client Variables @@ -100,15 +100,17 @@ public void Server_ProcessCashRegisterAction(ServerPlayer player, CommonCashRegi { float sqrDistance = (player.WorldPosition - transform.position).sqrMagnitude; bool success = false; + CashRegisterAction response = CashRegisterAction.RejectGeneric; NetworkLifecycle.Instance.Server?.LogDebug(() => $"Server_ProcessAction({player.Username}, {packet.Action}, {packet.Amount})"); - if (sqrDistance > GrabberRaycasterDV.FPS_INTERACTION_RANGE_SQR) + if (sqrDistance > GrabberRaycasterDV.FPS_INTERACTION_RANGE_SQR * 2) //need to find the real distance, likely related to player capsual size { NetworkLifecycle.Instance.Server?.LogDebug(() => $"Server_ProcessAction({player.Username}, {packet.Action}, {packet.Amount}) {CashRegister.GetObjectPath()}. Player too far! Player pos: {player.WorldPosition}, register pos: {transform.position}, sqrMag: {sqrDistance}"); return; } + processingAction = true; switch (packet.Action) { case CashRegisterAction.Cancel: @@ -122,7 +124,12 @@ public void Server_ProcessCashRegisterAction(ServerPlayer player, CommonCashRegi case CashRegisterAction.Buy: if (CashRegister.buyButton.InteractionAllowed) + { success = CashRegister?.Buy() ?? false; + if (Inventory.Instance.PlayerMoney <= CashRegister.GetTotalCost()) + response = CashRegisterAction.RejectFunds; + + } break; @@ -157,11 +164,13 @@ public void Server_ProcessCashRegisterAction(ServerPlayer player, CommonCashRegi new CommonCashRegisterWithModulesActionPacket { NetId = NetId, - Action = CashRegisterAction.Reject, + Action = response, Amount = CashRegister.DepositedCash }, player.Peer ); + + processingAction = false; } #endregion @@ -170,6 +179,7 @@ public void Server_ProcessCashRegisterAction(ServerPlayer player, CommonCashRegi public void Client_ProcessCashRegisterAction(CashRegisterAction action, double amount) { + NetworkLifecycle.Instance.Client?.LogDebug(() => $"Client_ProcessCashRegisterAction({action}, {amount}) isBuying: {isBuying}, buyAccepted: {buyAccepted}, isCancelling: {isCancelling}, cancelAccepted: {cancelAccepted}"); switch (action) { case CashRegisterAction.Cancel: @@ -182,6 +192,11 @@ public void Client_ProcessCashRegisterAction(CashRegisterAction action, double a CashRegister?.cancelAudio?.Play(transform.position, 1f, 1f, 0f, 1f, 500f, default, null, transform, false, 0f, null); + //foreach (var module in CashRegister.registerModules) + // module.ResetData(); + + CashRegister?.Cancel(); + break; case CashRegisterAction.Buy: @@ -194,65 +209,70 @@ public void Client_ProcessCashRegisterAction(CashRegisterAction action, double a CashRegister?.buyAudio?.Play(transform.position, 1f, 1f, 0f, 1f, 500f, default, null, transform, false, 0f, null); + foreach(var module in CashRegister.registerModules) + module.ResetData(); + + CashRegister?.OnUnitsToBuyChanged(); + + CashRegister.IsProcessingTransaction = false; + break; case CashRegisterAction.SetFunds: + CashRegister?.SetCash(amount); - if (CashRegister.DepositedCash == 0 && amount > 0) - { - - } break; - case CashRegisterAction.Reject: + case CashRegisterAction.RejectGeneric: isBuying = false; isCancelling = false; + break; + + case CashRegisterAction.RejectFunds: + isBuying = false; + isCancelling = false; + + CashRegister?.notEnoughMoneyAudio?.Play(transform.position, 1f, 1f, 0f, 1f, 500f, default, null, transform, false, 0f, null); + break; } } public IEnumerator Buy() { - if (isBuying || isCancelling) + if (isBuying || isCancelling || NetworkLifecycle.Instance.IsProcessingPacket) yield break; DisableInteraction(); + CashRegister.IsProcessingTransaction = true; NetworkLifecycle.Instance.Client.SendCashRegisterAction(NetId, CashRegisterAction.Buy); isBuying = true; - buyAccepted = false; float timeOut = Time.time + NetworkLifecycle.Instance.Client.RPC_Timeout; yield return new WaitUntil(() => Time.time >= timeOut || isBuying == false); - if (!buyAccepted) - CashRegister?.cancelAudio?.Play(transform.position, 1f, 1f, 0f, 1f, 500f, default, null, transform, false, 0f, null); - isBuying = false; - buyAccepted = false; + CashRegister.IsProcessingTransaction = false; EnableInteraction(); } public IEnumerator Cancel() { - if (isBuying || isCancelling) + if (isBuying || isCancelling || NetworkLifecycle.Instance.IsProcessingPacket) yield break; DisableInteraction(); NetworkLifecycle.Instance.Client.SendCashRegisterAction(NetId, CashRegisterAction.Cancel); isCancelling = true; - cancelAccepted = false; float timeOut = Time.time + NetworkLifecycle.Instance.Client.RPC_Timeout; yield return new WaitUntil(() => Time.time >= timeOut || isCancelling == false); - if (cancelAccepted) - CashRegister?.cancelAudio?.Play(transform.position, 1f, 1f, 0f, 1f, 500f, default, null, transform, false, 0f, null); - isCancelling = false; cancelAccepted = false; @@ -261,7 +281,7 @@ public IEnumerator Cancel() public void SetCash(double amount) { - if (isBuying || isCancelling || NetworkLifecycle.Instance.IsProcessingPacket) + if (isBuying || isCancelling || processingAction || NetworkLifecycle.Instance.IsProcessingPacket) return; NetworkLifecycle.Instance.Client.SendCashRegisterAction(NetId, CashRegisterAction.SetFunds, amount); diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index f52e0795..a367fc65 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -1302,7 +1302,7 @@ private void OnCommonItemChangePacket(CommonItemChangePacket packet, ITransportP private void OnCommonCashRegisterWithModulesActionPacket(CommonCashRegisterWithModulesActionPacket packet, ITransportPeer peer) { - if (TryGetServerPlayer(peer, out var player)) + if (!TryGetServerPlayer(peer, out var player)) { LogWarning($"Cash Register With Modules Action received, but player was not found"); return; diff --git a/Multiplayer/Networking/Packets/Common/CommonCashRegisterWithModulesActionPacket.cs b/Multiplayer/Networking/Packets/Common/CommonCashRegisterWithModulesActionPacket.cs index 7d5942ea..666d8e8d 100644 --- a/Multiplayer/Networking/Packets/Common/CommonCashRegisterWithModulesActionPacket.cs +++ b/Multiplayer/Networking/Packets/Common/CommonCashRegisterWithModulesActionPacket.cs @@ -1,8 +1,3 @@ -using Multiplayer.Networking.Data; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; namespace Multiplayer.Networking.Packets.Common; @@ -11,7 +6,8 @@ public enum CashRegisterAction : byte Cancel, Buy, SetFunds, - Reject, + RejectGeneric, + RejectFunds, Approve } public class CommonCashRegisterWithModulesActionPacket From 564c6b03706ab97ed83d7541b0b2bd62e557386f Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 1 Jun 2025 16:09:34 +1000 Subject: [PATCH 352/521] Fix loading health state for non-loco TrainCars --- Multiplayer/Networking/Data/Train/TrainCarHealthData.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Multiplayer/Networking/Data/Train/TrainCarHealthData.cs b/Multiplayer/Networking/Data/Train/TrainCarHealthData.cs index 0b984cb9..d1e45232 100644 --- a/Multiplayer/Networking/Data/Train/TrainCarHealthData.cs +++ b/Multiplayer/Networking/Data/Train/TrainCarHealthData.cs @@ -1,5 +1,6 @@ using DV.Damage; using LiteNetLib.Utils; +using Multiplayer.Utils; using System; namespace Multiplayer.Networking.Data.Train; @@ -32,7 +33,12 @@ public void LoadTo(TrainCar trainCar) if (dmgCtrl.windows != null) dmgCtrl.windows.windowsBroken = WindowsBroken; + + return; } + + var dmgModel = trainCar.GetComponent(); + dmgModel?.SetHealth(BodyHP); } public static TrainCarHealthData From(TrainCar trainCar) From 02b82bc4d54e8f0939392f5e21564ec27d7383b1 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 1 Jun 2025 16:13:00 +1000 Subject: [PATCH 353/521] Complete cash register sync --- .../World/NetworkedCashRegisterWithModules.cs | 40 ++++++++--------- .../World/CashRegisterWithModulesPatch.cs | 45 +++++++++++++++++++ 2 files changed, 63 insertions(+), 22 deletions(-) diff --git a/Multiplayer/Components/Networking/World/NetworkedCashRegisterWithModules.cs b/Multiplayer/Components/Networking/World/NetworkedCashRegisterWithModules.cs index 7bbcdb1c..30a39978 100644 --- a/Multiplayer/Components/Networking/World/NetworkedCashRegisterWithModules.cs +++ b/Multiplayer/Components/Networking/World/NetworkedCashRegisterWithModules.cs @@ -114,25 +114,18 @@ public void Server_ProcessCashRegisterAction(ServerPlayer player, CommonCashRegi switch (packet.Action) { case CashRegisterAction.Cancel: - if (CashRegister.cancelButton.InteractionAllowed) - { CashRegister?.Cancel(); success = true; - } break; case CashRegisterAction.Buy: - if (CashRegister.buyButton.InteractionAllowed) - { success = CashRegister?.Buy() ?? false; if (Inventory.Instance.PlayerMoney <= CashRegister.GetTotalCost()) response = CashRegisterAction.RejectFunds; - } - break; - + case CashRegisterAction.SetFunds: double spend = 0; @@ -146,7 +139,7 @@ public void Server_ProcessCashRegisterAction(ServerPlayer player, CommonCashRegi success = Inventory.Instance.RemoveMoney(spend); - if(success) + if(success && player.Id != NetworkLifecycle.Instance.Server.SelfId) CashRegister?.AddCash(spend); } else @@ -184,36 +177,40 @@ public void Client_ProcessCashRegisterAction(CashRegisterAction action, double a { case CashRegisterAction.Cancel: - if (isCancelling) - cancelAccepted = true; - isCancelling = false; isBuying = false; - CashRegister?.cancelAudio?.Play(transform.position, 1f, 1f, 0f, 1f, 500f, default, null, transform, false, 0f, null); + foreach (var module in CashRegister.registerModules) + module.ResetData(); + + CashRegister.OnUnitsToBuyChanged(); - //foreach (var module in CashRegister.registerModules) - // module.ResetData(); + if (CashRegister.DepositedCash > 0) + { + CashRegister?.cancelAudio?.Play(CashRegister.transform.position, 1f, 1f, 0f, 1f, 500f, default, null, CashRegister.transform, false, 0f, null); + CashRegister.DepositedCash = 0; + CashRegister?.OnDepositedUpdated(); + } - CashRegister?.Cancel(); + //CashRegister?.Cancel(); break; case CashRegisterAction.Buy: - if (isBuying) - buyAccepted = true; - isCancelling = false; isBuying = false; - CashRegister?.buyAudio?.Play(transform.position, 1f, 1f, 0f, 1f, 500f, default, null, transform, false, 0f, null); + CashRegister?.buyAudio?.Play(CashRegister.transform.position, 1f, 1f, 0f, 1f, 500f, default, null, CashRegister.transform, false, 0f, null); foreach(var module in CashRegister.registerModules) module.ResetData(); CashRegister?.OnUnitsToBuyChanged(); + CashRegister.DepositedCash = 0; + CashRegister?.OnDepositedUpdated(); + CashRegister.IsProcessingTransaction = false; break; @@ -233,7 +230,7 @@ public void Client_ProcessCashRegisterAction(CashRegisterAction action, double a isBuying = false; isCancelling = false; - CashRegister?.notEnoughMoneyAudio?.Play(transform.position, 1f, 1f, 0f, 1f, 500f, default, null, transform, false, 0f, null); + CashRegister?.notEnoughMoneyAudio?.Play(CashRegister.transform.position, 1f, 1f, 0f, 1f, 500f, default, null, CashRegister.transform, false, 0f, null); break; } @@ -274,7 +271,6 @@ public IEnumerator Cancel() yield return new WaitUntil(() => Time.time >= timeOut || isCancelling == false); isCancelling = false; - cancelAccepted = false; EnableInteraction(); } diff --git a/Multiplayer/Patches/World/CashRegisterWithModulesPatch.cs b/Multiplayer/Patches/World/CashRegisterWithModulesPatch.cs index 54ce7cfc..4e62a575 100644 --- a/Multiplayer/Patches/World/CashRegisterWithModulesPatch.cs +++ b/Multiplayer/Patches/World/CashRegisterWithModulesPatch.cs @@ -2,6 +2,7 @@ using HarmonyLib; using Multiplayer.Components.Networking; using Multiplayer.Components.Networking.World; +using Multiplayer.Networking.Packets.Common; using Multiplayer.Utils; namespace Multiplayer.Patches.World; @@ -27,6 +28,28 @@ private static bool OnBuyPressed(CashRegisterWithModules __instance) return false; } + [HarmonyPostfix] + [HarmonyPatch(nameof(CashRegisterWithModules.OnBuyPressed))] + private static void OnBuyPressed_Postfix(CashRegisterWithModules __instance) + { + if (!NetworkLifecycle.Instance.IsHost()) + return; + + if (!NetworkedCashRegisterWithModules.TryGet(__instance, out var netCashRegister)) + { + Multiplayer.LogWarning($"CashRegisterWithModules.OnBuyPressed_Postfix({__instance.GetObjectPath()}) NetworkedCashRegisterWithModules not found!"); + return; + } + + // Send buy action to all clients + NetworkLifecycle.Instance.Server.SendCashRegisterAction(new CommonCashRegisterWithModulesActionPacket + { + NetId = netCashRegister.NetId, + Action = CashRegisterAction.Buy, + Amount = __instance.DepositedCash + }); + } + [HarmonyPrefix] [HarmonyPatch(nameof(CashRegisterWithModules.Cancel))] private static bool Cancel(CashRegisterWithModules __instance) @@ -44,6 +67,28 @@ private static bool Cancel(CashRegisterWithModules __instance) return false; } + + [HarmonyPostfix] + [HarmonyPatch(nameof(CashRegisterWithModules.Cancel))] + private static void Cancel_Postfix(CashRegisterWithModules __instance) + { + if (!NetworkLifecycle.Instance.IsHost()) + return; + + if (!NetworkedCashRegisterWithModules.TryGet(__instance, out var netCashRegister)) + { + Multiplayer.LogWarning($"CashRegisterWithModules.Cancel_Postfix({__instance.GetObjectPath()}) NetworkedCashRegisterWithModules not found!"); + return; + } + + // Send cancel action to all clients + NetworkLifecycle.Instance.Server.SendCashRegisterAction(new CommonCashRegisterWithModulesActionPacket + { + NetId = netCashRegister.NetId, + Action = CashRegisterAction.Cancel, + Amount = __instance.DepositedCash + }); + } } [HarmonyPatch(typeof(CashRegisterBase))] From f39376840dcca0fa99b4d7a9fcb8b579b5eecafe Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 1 Jun 2025 17:48:52 +1000 Subject: [PATCH 354/521] Track changes to TrainDamage TrainDamage is used for repairs and does not trigger the `CarDamageModel.CarEffectiveHealthStateUpdate` event. --- .../Networking/Train/NetworkedTrainCar.cs | 71 ++++++++++++++++--- .../Data/Train/TrainCarHealthData.cs | 9 +++ 2 files changed, 71 insertions(+), 9 deletions(-) diff --git a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs index 3073fa0f..fe9f67fa 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs @@ -1,9 +1,6 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; using DV.CabControls; using DV.Customization.Paint; +using DV.Damage; using DV.MultipleUnit; using DV.Simulation.Brake; using DV.Simulation.Cars; @@ -18,6 +15,11 @@ using Multiplayer.Networking.Packets.Common.Train; using Multiplayer.Networking.TransportLayers; using Multiplayer.Utils; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; using UnityEngine; namespace Multiplayer.Components.Networking.Train; @@ -86,11 +88,13 @@ public static bool TryGetFromTrainCar(TrainCar trainCar, out NetworkedTrainCar n private bool hasSimFlow; private SimulationFlow simulationFlow; public FireboxSimController firebox; + private readonly Dictionary trainDamageDelegates = []; private HashSet dirtyPorts; private Dictionary lastSentPortValues; private HashSet dirtyFuses; private float lastSentFireboxValue; + private readonly Dictionary lastSentTrainDamages = []; private bool handbrakeDirty; private bool mainResPressureDirty; @@ -208,6 +212,7 @@ public void Start() TrainCar.PaintInterior.OnThemeChanged += Common_OnPaintThemeChange; NetworkLifecycle.Instance.OnTick += Common_OnTick; + if (NetworkLifecycle.Instance.IsHost()) { NetworkLifecycle.Instance.OnTick += Server_OnTick; @@ -221,6 +226,36 @@ public void Start() TrainCar.CarDamage.CarEffectiveHealthStateUpdate += Server_CarHealthUpdate; + //find all TrainDamages and subscribe + if (TryGetComponent(out DamageController damageController)) + { + var trainDamageFields = typeof(DamageController) + .GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) + .Where(field => field.FieldType == typeof(TrainDamage)) + .Select(field => new { Field = field, Damage = (TrainDamage)field.GetValue(damageController) }) + .Where(value => value != null) + .ToArray(); + + if (trainDamageFields != null && trainDamageFields.Length > 0) + { + for (int i = 0; i < trainDamageFields.Length; i++) + { + var fieldName = trainDamageFields[i].Field.Name; + var fieldValue = trainDamageFields[i].Damage; + + //create a delegate for each field + void DamagesUpdate(float health) => Server_TrainDamagesHealthUpdate(fieldName, health); + + //subscribe to the event + trainDamageFields[i].Damage.HealthPercentageChanged += DamagesUpdate; + + //store delegates and set a last sent value to an impossible value + trainDamageDelegates.Add(fieldValue, DamagesUpdate); + lastSentTrainDamages.Add(fieldName, -1f); + } + } + } + brakeSystem.MainResPressureChanged += Server_MainResUpdate; brakeSystem.heatController.OverheatingActiveStateChanged += Server_BrakeHeatUpdate; @@ -235,6 +270,7 @@ public void Start() NetworkLifecycle.Instance?.Client.SendTrainSyncRequest(NetId); } + public void OnDisable() { if (UnloadWatcher.isQuitting) @@ -282,6 +318,11 @@ public void OnDisable() TrainCar.CarDamage.CarEffectiveHealthStateUpdate -= Server_CarHealthUpdate; + //Unsubscribe from damage updates + if (trainDamageDelegates != null && lastSentTrainDamages.Count > 0) + foreach (var kvp in trainDamageDelegates) + kvp.Key.HealthPercentageChanged -= kvp.Value; + if (brakeSystem != null) { brakeSystem.MainResPressureChanged -= Server_MainResUpdate; @@ -356,10 +397,6 @@ public void Server_DirtyAllState() foreach (string portId in simulationFlow.fullPortIdToPort.Keys) { dirtyPorts.Add(portId); - //if (simulationFlow.TryGetPort(portId, out Port port)) - //{ - // lastSentPortValues[portId] = port.value; - //} } foreach (string fuseId in simulationFlow.fullFuseIdToFuse.Keys) @@ -425,9 +462,25 @@ private void Server_CargoHealthUpdate(float health) private void Server_CarHealthUpdate(float health) { + //Multiplayer.LogDebug(() => $"Server_CarHealthUpdate({health}) netId: {NetId}"); carHealthDirty = true; } + private void Server_TrainDamagesHealthUpdate(string field, float health) + { + //Multiplayer.LogDebug(() => $"Server_TrainDamagesHealthUpdate({field}, {health}) netId: {NetId}"); + + // Check if value has changed before updating + if (!lastSentTrainDamages.TryGetValue(field, out float lastValue) + || Mathf.Abs(lastValue - health) > MAX_PORT_DELTA + || (health == 0 && lastValue != 0) + || (health == 1 && lastValue != 1)) + { + lastSentTrainDamages[field] = health; + carHealthDirty = true; + } + } + private void Server_MainResUpdate(float normalizedPressure, float pressure) { mainResPressureDirty = true; @@ -1267,7 +1320,7 @@ public void Client_ReceiveTrainPhysicsUpdate(in TrainsetMovementPart movementPar TrainCar.Derail(); movementPart.RigidbodySnapshot.Apply(TrainCar.rb); - // Client_trainRigidbodyQueue.ReceiveSnapshot(movementPart.RigidbodySnapshot, tick); + // Client_trainRigidbodyQueue.ReceiveSnapshot(movementPart.RigidbodySnapshot, tick); //Multiplayer.LogDebug(() => $"Derailed car {TrainCar.ID} positioned at {TrainCar.transform.position}"); } diff --git a/Multiplayer/Networking/Data/Train/TrainCarHealthData.cs b/Multiplayer/Networking/Data/Train/TrainCarHealthData.cs index d1e45232..83a05d33 100644 --- a/Multiplayer/Networking/Data/Train/TrainCarHealthData.cs +++ b/Multiplayer/Networking/Data/Train/TrainCarHealthData.cs @@ -23,9 +23,17 @@ private TrainCarHealthData(float bodyHP, float wheelsHP, float mechanicalPT, flo } public void LoadTo(TrainCar trainCar) { + //Multiplayer.LogDebug(() => $"TrainCarHealthData.LoadTo([{trainCar.ID}, {trainCar.GetNetId()}])"); var dmgCtrl = trainCar.GetComponent(); if (dmgCtrl != null) { + //Multiplayer.LogDebug(() => $"TrainCarHealthData.LoadTo([{trainCar.ID}, {trainCar.GetNetId()}])\r\n" + + //$"Damage Controller: Body: {dmgCtrl?.bodyDamage?.HealthPercentage}, " + + //$"Wheels: {dmgCtrl?.wheels?.HealthPercentage}, " + + //$"Mechanical: {dmgCtrl?.mechanicalPT?.HealthPercentage}, " + + //$"Electrical: {dmgCtrl?.electricalPT?.HealthPercentage}, " + + //$"Windows: {dmgCtrl?.windows?.windowsBroken}"); + dmgCtrl.bodyDamage.LoadCarDamageState(BodyHP); dmgCtrl.wheels?.SetCurrentHealthPercentage(WheelsHP); dmgCtrl.mechanicalPT?.SetCurrentHealthPercentage(MechanicalPT); @@ -38,6 +46,7 @@ public void LoadTo(TrainCar trainCar) } var dmgModel = trainCar.GetComponent(); + //Multiplayer.LogDebug(() => $"TrainCarHealthData.LoadTo([{trainCar.ID}, {trainCar.GetNetId()}]) Using CarDamageModel: {dmgModel !=null}"); dmgModel?.SetHealth(BodyHP); } From b9c5f1d2d326d7cc0471fd01872c6cb0df65bc43 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 1 Jun 2025 17:50:02 +1000 Subject: [PATCH 355/521] Update Logging --- Multiplayer/Networking/Managers/Client/NetworkClient.cs | 3 ++- Multiplayer/Networking/Managers/Server/NetworkServer.cs | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 02654f22..63cd698a 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -41,7 +41,6 @@ using DV.Common; using DV.Customization.Paint; using Multiplayer.Networking.TransportLayers; -using System.Collections; namespace Multiplayer.Networking.Managers.Client; @@ -838,6 +837,8 @@ private void OnClientboundCargoHealthUpdatePacket(ClientboundCargoHealthUpdatePa private void OnClientboundCarHealthUpdatePacket(ClientboundCarHealthUpdatePacket packet) { + //LogDebug(() => $"Received Car Health Update for netId {packet.NetId}: BodyHP: {packet.Health.BodyHP}, WheelsHP: {packet.Health.WheelsHP}, MechanicalPT: {packet.Health.MechanicalPT}, ElectricalPT: {packet.Health.ElectricalPT}, WindowsBroken: {packet.Health.WindowsBroken}"); + if (!NetworkedTrainCar.GetTrainCar(packet.NetId, out TrainCar trainCar)) return; diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index a367fc65..ee0dbc0a 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -66,7 +66,7 @@ public class NetworkServer : NetworkManager public NetworkServer(IDifficulty difficulty, Settings settings, bool singlePlayer, LobbyServerData serverData) : base(settings) { - Log(()=>$"Server created for {(singlePlayer ? "single player" : "multiplayer")} game"); + Log($"Server created for {(singlePlayer ? "single player" : "multiplayer")} game"); IsSinglePlayer = singlePlayer; ServerData = serverData; @@ -457,6 +457,9 @@ public void SendCargoHealthUpdate(ushort netId, float currentHealth) public void SendCarHealthUpdate(ushort netId, TrainCarHealthData health) { + + //LogDebug(() => $"Sending Car Health Update for netId {netId}: BodyHP: {health.BodyHP}, WheelsHP: {health.WheelsHP}, MechanicalPT: {health.MechanicalPT}, ElectricalPT: {health.ElectricalPT}, WindowsBroken: {health.WindowsBroken}"); + SendPacketToAll(new ClientboundCarHealthUpdatePacket { NetId = netId, From dfb1d10e747aa5c1cb097fc914db6d83921b535a Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 7 Jun 2025 14:41:36 +1000 Subject: [PATCH 356/521] Remove unnecessary logging --- .../Networking/Train/NetworkedTrainCar.cs | 16 +++++++++++++++- .../World/NetworkedCashRegisterWithModules.cs | 6 +++--- .../Networking/World/NetworkedPitStopStation.cs | 4 ++-- .../PlayerDistanceGameObjectsDisablerPatch.cs | 4 ++-- 4 files changed, 22 insertions(+), 8 deletions(-) diff --git a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs index fe9f67fa..da877b7d 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs @@ -166,7 +166,7 @@ public void Start() { brakeSystem = TrainCar.brakeSystem; - Multiplayer.LogDebug(() => $"TrainCar Created: {TrainCar?.ID}, {NetId}"); + Multiplayer.LogDebug(() => $"NetworkedTrainCar.Start({TrainCar?.ID}, {NetId})"); foreach (Coupler coupler in TrainCar.couplers) { @@ -179,6 +179,8 @@ public void Start() coupler.ChainScript.StateChanged += (state) => { Client_CouplerStateChange(state, coupler); }; } + //Multiplayer.LogDebug(() => $"NetworkedTrainCar.Start({TrainCar?.ID}, {NetId}) Couplers complete"); + SimController simController = GetComponent(); if (simController != null) { @@ -203,6 +205,8 @@ public void Start() } } + //Multiplayer.LogDebug(() => $"NetworkedTrainCar.Start({TrainCar?.ID}, {NetId}) SimController complete"); + brakeSystem.HandbrakePositionChanged += Common_OnHandbrakePositionChanged; brakeSystem.BrakeCylinderReleased += Common_OnBrakeCylinderReleased; @@ -213,6 +217,8 @@ public void Start() NetworkLifecycle.Instance.OnTick += Common_OnTick; + //Multiplayer.LogDebug(() => $"NetworkedTrainCar.Start({TrainCar?.ID}, {NetId}) CommonTick subscribed"); + if (NetworkLifecycle.Instance.IsHost()) { NetworkLifecycle.Instance.OnTick += Server_OnTick; @@ -226,6 +232,8 @@ public void Start() TrainCar.CarDamage.CarEffectiveHealthStateUpdate += Server_CarHealthUpdate; + //Multiplayer.LogDebug(() => $"NetworkedTrainCar.Start({TrainCar?.ID}, {NetId}) Subscribing to DamageControllers"); + //find all TrainDamages and subscribe if (TryGetComponent(out DamageController damageController)) { @@ -236,6 +244,8 @@ public void Start() .Where(value => value != null) .ToArray(); + //Multiplayer.LogDebug(() => $"NetworkedTrainCar.Start({TrainCar?.ID}, {NetId}) Subscribing to DamageControllers. TrainDamageFields: {trainDamageFields?.Length}"); + if (trainDamageFields != null && trainDamageFields.Length > 0) { for (int i = 0; i < trainDamageFields.Length; i++) @@ -256,6 +266,8 @@ public void Start() } } + //Multiplayer.LogDebug(() => $"NetworkedTrainCar.Start({TrainCar?.ID}, {NetId}) DamageControllers subscribed"); + brakeSystem.MainResPressureChanged += Server_MainResUpdate; brakeSystem.heatController.OverheatingActiveStateChanged += Server_BrakeHeatUpdate; @@ -265,6 +277,8 @@ public void Start() firebox.fireOnPort.ValueUpdatedInternally += Common_OnFireboxUpdate; } + //Multiplayer.LogDebug(() => $"NetworkedTrainCar.Start({TrainCar?.ID}, {NetId}) Firebox subscribed"); + StartCoroutine(Server_WaitForLogicCar()); } diff --git a/Multiplayer/Components/Networking/World/NetworkedCashRegisterWithModules.cs b/Multiplayer/Components/Networking/World/NetworkedCashRegisterWithModules.cs index 30a39978..9d3f5d54 100644 --- a/Multiplayer/Components/Networking/World/NetworkedCashRegisterWithModules.cs +++ b/Multiplayer/Components/Networking/World/NetworkedCashRegisterWithModules.cs @@ -41,7 +41,7 @@ public static void InitialiseCashRegisters() .ThenBy(r => r.transform.position.z) .ToArray(); - Multiplayer.LogDebug(() => $"InitialiseCashRegisters() Found: {registers?.Length}"); + //Multiplayer.LogDebug(() => $"InitialiseCashRegisters() Found: {registers?.Length}"); foreach (var register in registers) { @@ -53,7 +53,7 @@ public static void InitialiseCashRegisters() cashRegisterToNetworkedCashRegister[register] = netRegister; - Multiplayer.LogDebug(() => $"InitialiseCashRegisters() Register: {register?.GetObjectPath()}, netId: {netRegister.NetId}"); + //Multiplayer.LogDebug(() => $"InitialiseCashRegisters() Register: {register?.GetObjectPath()}, netId: {netRegister.NetId}"); } } @@ -82,7 +82,7 @@ public static void InitialiseCashRegisters() protected override void Awake() { - Multiplayer.LogDebug(()=>$"CashRegisterWithModules.Awake() {transform.GetObjectPath()}, {transform.position}, netId: {NetId}"); + //Multiplayer.LogDebug(()=>$"CashRegisterWithModules.Awake() {transform.GetObjectPath()}, {transform.position}, netId: {NetId}"); if (NetId == 0) base.Awake(); diff --git a/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs b/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs index aec90a00..e4bbecae 100644 --- a/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs +++ b/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs @@ -42,7 +42,7 @@ public static void InitialisePitStops() .ThenBy(p => p.transform.position.z) .ToArray(); - Multiplayer.LogDebug(() => $"InitialisePitStops() Found: {stations?.Length}"); + //Multiplayer.LogDebug(() => $"InitialisePitStops() Found: {stations?.Length}"); foreach (var station in stations) { @@ -54,7 +54,7 @@ public static void InitialisePitStops() pitStopStationToNetworkedPitStopStation[station] = netStation; - Multiplayer.LogDebug(() => $"InitialisePitStops() Station: {station?.GetObjectPath()}, netId: {netStation.NetId}"); + //Multiplayer.LogDebug(() => $"InitialisePitStops() Station: {station?.GetObjectPath()}, netId: {netStation.NetId}"); CoroutineManager.Instance.StartCoroutine(netStation.Init()); diff --git a/Multiplayer/Patches/World/PlayerDistanceGameObjectsDisablerPatch.cs b/Multiplayer/Patches/World/PlayerDistanceGameObjectsDisablerPatch.cs index c477cc09..73683670 100644 --- a/Multiplayer/Patches/World/PlayerDistanceGameObjectsDisablerPatch.cs +++ b/Multiplayer/Patches/World/PlayerDistanceGameObjectsDisablerPatch.cs @@ -82,10 +82,10 @@ public static IEnumerable GameObjectsDistanceCheck(IEnumerable< foreach (CodeInstruction instruction in instructions) { - Multiplayer.LogDebug(() => $"Checking instruction: {instruction}"); + //Multiplayer.LogDebug(() => $"Checking instruction: {instruction}"); if (instruction.opcode == OpCodes.Call && instruction.operand?.ToString() == targetMethod.operand?.ToString()) { - Multiplayer.LogDebug(() => "Found target method, replacing"); + //Multiplayer.LogDebug(() => "Found target method, replacing"); newCode.Add(new CodeInstruction(OpCodes.Ldloc_1)); newCode.Add(newMethod); //skip 0 newCode.Add(new CodeInstruction(OpCodes.Nop)); //skip 1 From 8d6c6c84b5a49bcf2c2668838530dfebe139c03f Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 7 Jun 2025 16:37:30 +1000 Subject: [PATCH 357/521] Fix issue with DamageController reflection --- Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs index da877b7d..abdca4fa 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs @@ -235,13 +235,13 @@ public void Start() //Multiplayer.LogDebug(() => $"NetworkedTrainCar.Start({TrainCar?.ID}, {NetId}) Subscribing to DamageControllers"); //find all TrainDamages and subscribe - if (TryGetComponent(out DamageController damageController)) + if (TryGetComponent(out DamageController damageController) && damageController != null) { var trainDamageFields = typeof(DamageController) .GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) .Where(field => field.FieldType == typeof(TrainDamage)) .Select(field => new { Field = field, Damage = (TrainDamage)field.GetValue(damageController) }) - .Where(value => value != null) + .Where(value => value.Damage != null) .ToArray(); //Multiplayer.LogDebug(() => $"NetworkedTrainCar.Start({TrainCar?.ID}, {NetId}) Subscribing to DamageControllers. TrainDamageFields: {trainDamageFields?.Length}"); From 2625bc203f173d9d8e6f71afb74657a010c6ea97 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 7 Jun 2025 17:02:52 +1000 Subject: [PATCH 358/521] Update interaction workflow --- .../World/NetworkedPitStopStation.cs | 246 +++++++++--------- .../Data/PitStopStationInteractionType.cs | 7 +- 2 files changed, 131 insertions(+), 122 deletions(-) diff --git a/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs b/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs index e4bbecae..a3b2e2d1 100644 --- a/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs +++ b/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs @@ -1,16 +1,16 @@ using DV.Interaction; +using DV.ThingTypes; +using Multiplayer.Networking.Data; +using Multiplayer.Networking.Packets.Clientbound.World; using Multiplayer.Networking.Packets.Common; using Multiplayer.Networking.TransportLayers; -using Multiplayer.Networking.Data; using Multiplayer.Utils; using System; +using System.Collections; using System.Collections.Generic; +using System.Linq; using System.Text; using UnityEngine; -using DV.ThingTypes; -using System.Collections; -using System.Linq; -using Multiplayer.Networking.Packets.Clientbound.World; using static CashRegisterModule; namespace Multiplayer.Components.Networking.World; @@ -42,6 +42,8 @@ public static void InitialisePitStops() .ThenBy(p => p.transform.position.z) .ToArray(); + pitStopStationToNetworkedPitStopStation.Clear(); + //Multiplayer.LogDebug(() => $"InitialisePitStops() Found: {stations?.Length}"); foreach (var station in stations) @@ -86,12 +88,15 @@ public static void InitialisePitStops() public PitStopStation Station { get; set; } public string StationName { get; private set; } + private bool initialised = false; + private ResourceType[] resourceTypes = []; private GrabHandlerHingeJoint carSelectorGrab; private GrabHandlerHingeJoint faucetPositionerGrab; private HingeJointAngleFix faucetPositioner; - private readonly Dictionary grabberLookup = []; + + private readonly Dictionary leverHandler)> leverStateLookup = []; private readonly Dictionary grabbedHandlerLookup = []; private readonly Dictionary resourceToPluggableObject = []; @@ -108,7 +113,6 @@ public static void InitialisePitStops() private float faucetTargetPercentage = 0.0f; private bool faucetTargetReached = true; - private readonly Dictionary grabbedAmplitudeChecker = []; private readonly Dictionary lastUnitsToBuyDict = []; private bool Refreshed = false; @@ -157,7 +161,10 @@ protected override void OnDestroy() { carSelectorGrab.Grabbed -= CarSelectorGrabbed; carSelectorGrab.UnGrabbed -= CarSelectorUnGrabbed; + } + if (Station?.pitstop != null) + { Station.pitstop.CarSelected -= CarSelected; } @@ -167,15 +174,13 @@ protected override void OnDestroy() faucetPositionerGrab.UnGrabbed -= FaucetCrankUnGrabbed; } - foreach (var kvp in grabberLookup) + foreach (var kvp in leverStateLookup) { - var grab = kvp.Key; - var (_, grabbedHandler, ungrabbedHandler) = kvp.Value; - grab.Grabbed -= grabbedHandler; - grab.UnGrabbed -= ungrabbedHandler; + var (leverAmplitudeChecker, _, leverStateHandler) = kvp.Value; + leverAmplitudeChecker.RotaryStateChanged -= leverStateHandler; } - grabberLookup.Clear(); + leverStateLookup.Clear(); grabbedHandlerLookup.Clear(); base.OnDestroy(); } @@ -214,13 +219,15 @@ protected void LateUpdate() lastUnitsToBuyDict[resourceType] = module.Data.unitsToBuy; lastUpdateTime = Time.time; - //if (!(NetworkLifecycle.Instance.IsHost() && processingAsHost)) - NetworkLifecycle.Instance?.Client.SendPitStopInteractionPacket( - NetId, - PitStopStationInteractionType.ResourceUpdate, - resourceType, - lastUnitsToBuy - ); + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.LateUpdate() isGrabbed: {isResourceGrabbed}, wasGrabbed: {wasResourceGrabbed}, previous: {lastUnitsToBuy}, new: {module.Data.unitsToBuy}, processingAsHost: {processingAsHost}. Sending PitstopInteractionPacket ResourceUpdate "); + + if (!(NetworkLifecycle.Instance.IsHost() && processingAsHost)) + NetworkLifecycle.Instance?.Client.SendPitStopInteractionPacket( + NetId, + PitStopStationInteractionType.ResourceUpdate, + resourceType, + lastUnitsToBuy + ); } } //Local grab has ended, but needs to be finalised @@ -229,13 +236,13 @@ protected void LateUpdate() Multiplayer.LogDebug(() => $"NetworkedPitStopStation.LateUpdate() wasGrabbed: {wasResourceGrabbed}, previous: {lastUnitsToBuy}, new: {module.Data.unitsToBuy}"); lastUnitsToBuyDict[resourceType] = module.Data.unitsToBuy; - //if (!(NetworkLifecycle.Instance.IsHost() && processingAsHost)) - NetworkLifecycle.Instance?.Client.SendPitStopInteractionPacket( - NetId, - PitStopStationInteractionType.ResourceUngrab, - resourceType, - lastUnitsToBuy - ); + if (!(NetworkLifecycle.Instance.IsHost() && processingAsHost)) + NetworkLifecycle.Instance?.Client.SendPitStopInteractionPacket( + NetId, + PitStopStationInteractionType.ResourceUpdate, + resourceType, + lastUnitsToBuy + ); //Reset grab states wasResourceGrabbedDict[resourceType] = false; @@ -245,7 +252,7 @@ protected void LateUpdate() if (!isResourceRemoteGrabbed && wasResourceRemoteGrabbed) { float previous = module.Data.unitsToBuy; - //grabbedModule.Data.unitsToBuy = lastRemoteValueDict; + SetUnits(module, lastRemoteValue); Multiplayer.LogDebug(() => $"NetworkedPitStopStation.LateUpdate() wasRemoteGrabbed: {wasResourceRemoteGrabbed}, previous: {previous}, new: {lastRemoteValue}"); @@ -390,9 +397,14 @@ public void ProcessInteractionPacketAsHost(CommonPitStopInteractionPacket packet if (ValidateInteraction(packet, peer)) { + processingAsHost = true; - ProcessInteractionPacketAsClient(packet); - LateUpdate(); + if (peer.Id != NetworkLifecycle.Instance.Server.SelfPeer.Id) + { + Multiplayer.LogDebug(() => $"ProcessInteractionPacketAsHost() ProcessPacketAsClient()"); + ProcessInteractionPacketAsClient(packet); + LateUpdate(); + } processingAsHost = false; //Send to all other players foreach (var playerId in playerToLastNearbyTime.Keys) @@ -482,7 +494,7 @@ private IEnumerator Init() isResourceRemoteGrabbedDict[resourceType] = false; wasResourceRemoteGrabbedDict[resourceType] = false; lastRemoteValueDict[resourceType] = 0.0f; - grabbedAmplitudeChecker[resourceType] = null; + //grabbedAmplitudeChecker[resourceType] = null; lastUnitsToBuyDict[resourceType] = 0.0f; } @@ -493,25 +505,29 @@ private IEnumerator Init() { foreach (var resourceModule in resourceModules) { - var grabHandlers = resourceModule.GetComponentsInChildren(); - foreach (var grab in grabHandlers) + yield return new WaitUntil(() => resourceModule.initialized); + + var checker = resourceModule.GetComponentInChildren(); + var grab = resourceModule.GetComponentInChildren(); + if (checker != null && grab != null) { - if (grab != null) - { - //Delegates for handlers - void GrabbedHandler() => LeverGrabbed(resourceModule); - void UnGrabbedHandler() => LeverUnGrabbed(resourceModule); + + //Delegates for handlers + void LeverStatehandler(int state) => OnLeverPositionChange(resourceModule, state); - //Subscribe - grab.Grabbed += GrabbedHandler; - grab.UnGrabbed += UnGrabbedHandler; + //Subscribe + checker.RotaryStateChanged += LeverStatehandler; - //Store delegates - grabberLookup[grab] = (resourceModule, GrabbedHandler, UnGrabbedHandler); - grabbedHandlerLookup[resourceModule.resourceType] = grab; + //Store delegate + leverStateLookup[resourceModule.resourceType] = (checker, resourceModule, LeverStatehandler); + grabbedHandlerLookup[resourceModule.resourceType] = grab; - sb.AppendLine($"\t{resourceModule.resourceType}, Grab Handler found: {grab != null}, Name: {grab.name}"); - } + //sb.AppendLine($"\t{resourceModule.resourceType}, Grab Handler found: {grab != null}, Name: {grab.name}"); + sb.AppendLine($"\t{resourceModule.resourceType}, Rotary Amplitude Handler found: {checker != null}, Name: {checker.name}"); + } + else + { + sb.AppendLine($"\t{resourceModule.resourceType}, Failed to find component. Grab Handler found: {grab != null}, Amplitude Checker found: {checker != null}"); } var plug = resourceModule.resourceHose; @@ -529,23 +545,34 @@ private IEnumerator Init() } Multiplayer.LogDebug(() => sb.ToString()); + + initialised = true; } + /// + /// Waits for all pitstop components to complete loading before processing the bulk update + /// private IEnumerator WaitForLoad(ClientboundPitStopBulkUpdatePacket packet) { float time = Time.time; - yield return new WaitUntil(() => - (Station?.pitstop?.carList != null && packet.CarCount == Station.pitstop.carList.Count) + yield return new WaitUntil + ( + () => + (initialised && Station?.pitstop?.carList != null && packet.CarCount == Station.pitstop.carList.Count) || (Time.time - time) > LOADING_TIMEOUT - ); + ); if ((Time.time - time) < LOADING_TIMEOUT) { ProcessBulkUpdate(packet); } else - Multiplayer.LogWarning($"NetworkedPitStopStation.WaitForLoad() Station {StationName} failed to process bulk update"); + { + Multiplayer.LogWarning($"PitStop [{StationName}] timed out waiting for load. PitStop Initialised: {initialised}, Car Count Matched: {packet.CarCount == Station.pitstop?.carList?.Count}"); + if (initialised) + Refreshed = true; //lets hope the car sync is just a little slow + } } private void SetUnits(LocoResourceModule rm, float units) @@ -671,41 +698,28 @@ private void CarSelected() /// Handles grab interactions for resource module levers. /// /// The resource module being grabbed. - private void LeverGrabbed(LocoResourceModule module) + private void OnLeverPositionChange(LocoResourceModule module, int state) { - if (NetworkLifecycle.Instance.IsProcessingPacket || (NetworkLifecycle.Instance.IsHost() && processingAsHost)) - return; - //Prevent new players/players entering the area from sending packets until initalised if (!Refreshed) return; - Multiplayer.LogDebug(() => $"LeverGrabbed() {StationName}, module: {module.resourceType}"); - - isResourceGrabbedDict[module.resourceType] = true; - wasResourceGrabbedDict[module.resourceType] = true; - grabbedAmplitudeChecker[module.resourceType] = module.GetComponentInChildren(); - lastUnitsToBuyDict[module.resourceType] = module.Data.unitsToBuy; + Multiplayer.LogDebug(() => $"OnLeverPositionChange() {StationName}, module: {module.resourceType}, state: {state}"); - NetworkLifecycle.Instance?.Client.SendPitStopInteractionPacket(NetId, PitStopStationInteractionType.ResourceGrab, module.resourceType, lastUnitsToBuyDict[module.resourceType]); - } - - /// - /// Handles end of grab (release) interactions for resource module levers. - /// - /// The resource module being grabbed. - private void LeverUnGrabbed(LocoResourceModule module) - { - if (NetworkLifecycle.Instance.IsProcessingPacket) - return; - - //Prevent new players/players entering the area from sending packets until initalised - if (!Refreshed) - return; + if (state == 0) + { + //lever returned home + isResourceGrabbedDict[module.resourceType] = false; + wasResourceGrabbedDict[module.resourceType] = true; + } + else + { + isResourceGrabbedDict[module.resourceType] = true; + wasResourceGrabbedDict[module.resourceType] = true; + lastUnitsToBuyDict[module.resourceType] = module.Data.unitsToBuy; + } - Multiplayer.LogDebug(() => $"LeverUnGrabbed() {StationName}, module: {module.resourceType}"); - isResourceGrabbedDict[module.resourceType] = false; - wasResourceGrabbedDict[module.resourceType] = true; + NetworkLifecycle.Instance?.Client.SendPitStopInteractionPacket(NetId, PitStopStationInteractionType.LeverState, module.resourceType, state); } /// @@ -745,10 +759,10 @@ private void FaucetCrankUnGrabbed() public void ProcessBulkUpdate(ClientboundPitStopBulkUpdatePacket packet) { - if (Station?.pitstop?.carList == null || Station.pitstop.carList.Count < packet.CarCount) + if (!initialised || Station?.pitstop?.carList == null || Station.pitstop.carList.Count < packet.CarCount) { - // Allow cars a chance to load in the pitstop - Multiplayer.LogDebug(() => $"PitStop bulk data count mismatch, waiting for load: {packet.CarCount} != {Station.pitstop.carList.Count}"); + // Allow pitstop to complete loading and cars to load in the pitstop + Multiplayer.Log($"PitStop [{StationName}] waiting for load"); CoroutineManager.Instance.StartCoroutine(WaitForLoad(packet)); return; } @@ -809,23 +823,32 @@ public void ProcessInteractionPacketAsClient(CommonPitStopInteractionPacket pack GrabHandlerHingeJoint grab = null; LocoResourceModule resourceModule = null; + RotaryAmplitudeChecker amplitudeChecker = null; Multiplayer.LogDebug(() => $"NetworkedPitStopStation.ProcessPacket() [{StationName}, {NetId}] {interactionType}, resource type: {resourceType}, state: {packet.Value}"); if (resourceValid) { - if (!grabbedHandlerLookup.TryGetValue((ResourceType)resourceType, out grab)) + if (!grabbedHandlerLookup.TryGetValue(resourceType, out grab)) + { Multiplayer.LogError($"Could not find ResourceType in grabbedHandlerLookup for Pit Stop station {StationName}, resource type: {resourceType}"); + } else - if (!grabberLookup.TryGetValue(grab, out var tup)) - Multiplayer.LogError($"Could not find GrabHandler in grabberLookup for Pit Stop station {StationName}, resource type: {resourceType}"); - else - (resourceModule, _, _) = tup; - - if (packet.Value < resourceModule.AbsoluteMinValue || packet.Value > resourceModule.AbsoluteMaxValue) { - Multiplayer.LogError($"Invalid Pit Stop state value: {packet.Value} for resource {resourceModule.resourceType}"); - return; + if (!leverStateLookup.TryGetValue(resourceType, out var tup)) + { + Multiplayer.LogError($"Could not find Rotary Amplitude Handler in rotaryAmplitudeLookup for Pit Stop station {StationName}, resource type: {resourceType}"); + } + else + { + (amplitudeChecker, resourceModule, _) = tup; + + if (packet.Value < resourceModule.AbsoluteMinValue || packet.Value > resourceModule.AbsoluteMaxValue) + { + Multiplayer.LogError($"Invalid Pit Stop state value: {packet.Value} for resource {resourceModule.resourceType}"); + return; + } + } } } @@ -835,41 +858,40 @@ public void ProcessInteractionPacketAsClient(CommonPitStopInteractionPacket pack //todo: implement rejection break; + case PitStopStationInteractionType.LeverState: - case PitStopStationInteractionType.ResourceGrab: - //block interaction - grab?.SetMovingDisabled(false); + bool grabbed = (packet.Value != 0); - //set direction - if (resourceValid && resourceModule != null) - { - lastRemoteValueDict[resourceType] = packet.Value; - isResourceRemoteGrabbedDict[resourceType] = true; - wasResourceRemoteGrabbedDict[resourceType] = true; - } + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.ProcessPacket() [{StationName}, {NetId}] {interactionType}, resource type: {resourceType}, state: {packet.Value}, grabbed: {grabbed}, resourceValid: {resourceValid}, resourceModule: {resourceModule != null}, isResourceRemoteGrabbed: {isResourceRemoteGrabbedDict[resourceType]}, wasResourceRemoteGrabbed: {wasResourceRemoteGrabbedDict[resourceType]}"); - break; + // Set interaction state - disable when grabbed, enable when released + grab?.SetMovingDisabled(grabbed); - case PitStopStationInteractionType.ResourceUngrab: - //allow interaction - grab?.SetMovingDisabled(true); + if (grabbed) + grab?.ForceEndInteraction(); + + resourceModule.OnValvePositionChange((int)packet.Value); - if (isResourceRemoteGrabbedDict[resourceType] || wasResourceRemoteGrabbedDict[resourceType]) + // Update remote grab state + if (resourceValid && resourceModule != null) { - lastRemoteValueDict[resourceType] = packet.Value; - SetUnits(resourceModule, lastRemoteValueDict[resourceType]); - isResourceRemoteGrabbedDict[resourceType] = false; + isResourceRemoteGrabbedDict[resourceType] = grabbed; + if (grabbed) + wasResourceRemoteGrabbedDict[resourceType] = true; } + break; case PitStopStationInteractionType.ResourceUpdate: + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.ProcessPacket() [{StationName}, {NetId}] {interactionType}, resource type: {resourceType}, state: {packet.Value}, resourceValid: {resourceValid}, resourceModule: {resourceModule != null}, isResourceRemoteGrabbed: {isResourceRemoteGrabbedDict[resourceType]}, wasResourceRemoteGrabbed: {wasResourceRemoteGrabbedDict[resourceType]}"); if (resourceValid && resourceModule != null) { if (isResourceRemoteGrabbedDict[resourceType] || wasResourceRemoteGrabbedDict[resourceType]) { lastRemoteValueDict[resourceType] = packet.Value; SetUnits(resourceModule, lastRemoteValueDict[resourceType]); + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.ProcessPacket() [{StationName}, {NetId}] {interactionType}, resource type: {resourceType}, state: {packet.Value}. SetUnits()"); } } break; @@ -919,14 +941,6 @@ public void ProcessInteractionPacketAsClient(CommonPitStopInteractionPacket pack } } break; - - - case PitStopStationInteractionType.PayOrder: - break; - case PitStopStationInteractionType.CancelOrder: - break; - case PitStopStationInteractionType.ProcessOrder: - break; } } #endregion diff --git a/Multiplayer/Networking/Data/PitStopStationInteractionType.cs b/Multiplayer/Networking/Data/PitStopStationInteractionType.cs index ef63755d..bd500642 100644 --- a/Multiplayer/Networking/Data/PitStopStationInteractionType.cs +++ b/Multiplayer/Networking/Data/PitStopStationInteractionType.cs @@ -5,8 +5,7 @@ namespace Multiplayer.Networking.Data; public enum PitStopStationInteractionType : byte { Reject, - ResourceGrab, - ResourceUngrab, + LeverState, ResourceUpdate, CarSelectorGrab, @@ -16,8 +15,4 @@ public enum PitStopStationInteractionType : byte FaucetGrab, FaucetUngrab, FaucetPosition, - - PayOrder, - CancelOrder, - ProcessOrder } From 94b46fd6f2bc6fae541a13375c3fe3504c92e69d Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 7 Jun 2025 17:03:37 +1000 Subject: [PATCH 359/521] Prevent clients calling Cancel() on CashRegisters while loading --- .../World/NetworkedCashRegisterWithModules.cs | 4 +--- .../World/CashRegisterWithModulesPatch.cs | 22 +++++++++++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/Multiplayer/Components/Networking/World/NetworkedCashRegisterWithModules.cs b/Multiplayer/Components/Networking/World/NetworkedCashRegisterWithModules.cs index 9d3f5d54..098ea127 100644 --- a/Multiplayer/Components/Networking/World/NetworkedCashRegisterWithModules.cs +++ b/Multiplayer/Components/Networking/World/NetworkedCashRegisterWithModules.cs @@ -67,10 +67,8 @@ public static void InitialiseCashRegisters() #region Client Variables bool isBuying; - bool buyAccepted; bool isCancelling; - bool cancelAccepted; #endregion @@ -172,7 +170,7 @@ public void Server_ProcessCashRegisterAction(ServerPlayer player, CommonCashRegi public void Client_ProcessCashRegisterAction(CashRegisterAction action, double amount) { - NetworkLifecycle.Instance.Client?.LogDebug(() => $"Client_ProcessCashRegisterAction({action}, {amount}) isBuying: {isBuying}, buyAccepted: {buyAccepted}, isCancelling: {isCancelling}, cancelAccepted: {cancelAccepted}"); + NetworkLifecycle.Instance.Client?.LogDebug(() => $"Client_ProcessCashRegisterAction({action}, {amount}) isBuying: {isBuying}, isCancelling: {isCancelling}"); switch (action) { case CashRegisterAction.Cancel: diff --git a/Multiplayer/Patches/World/CashRegisterWithModulesPatch.cs b/Multiplayer/Patches/World/CashRegisterWithModulesPatch.cs index 4e62a575..82cb48c3 100644 --- a/Multiplayer/Patches/World/CashRegisterWithModulesPatch.cs +++ b/Multiplayer/Patches/World/CashRegisterWithModulesPatch.cs @@ -108,4 +108,26 @@ private static void SetCash(CashRegisterBase __instance, double amount) else netCashRegister.SetCash(amount); } + + [HarmonyPrefix] + [HarmonyPatch(nameof(CashRegisterBase.OnEnable))] + private static bool OnEnable(CashRegisterBase __instance) + { + if (__instance is not CashRegisterWithModules) + return true; + + //prevent clients from clearing cash registers when loading + return NetworkLifecycle.Instance.IsHost(); + } + + [HarmonyPrefix] + [HarmonyPatch(nameof(CashRegisterBase.OnDisable))] + private static bool OnDisable(CashRegisterBase __instance) + { + if (__instance is not CashRegisterWithModules) + return true; + + //prevent clients from clearing cash registers when loading the game or leaving the area + return NetworkLifecycle.Instance.IsHost(); + } } From 422a0d9085a171b2b69a171d2c1baa6b00110354 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 7 Jun 2025 19:49:17 +1000 Subject: [PATCH 360/521] Add position update to NetworkPluggableObject on drop --- .../World/NetworkedPluggableObject.cs | 29 ++++---- .../Networking/Data/PlugInteractionType.cs | 5 -- .../Managers/Client/NetworkClient.cs | 10 +-- .../CommonPitStopPlugInteractionPacket.cs | 69 ++++++++++++++++++- 4 files changed, 87 insertions(+), 26 deletions(-) diff --git a/Multiplayer/Components/Networking/World/NetworkedPluggableObject.cs b/Multiplayer/Components/Networking/World/NetworkedPluggableObject.cs index 064c5016..26a04fa8 100644 --- a/Multiplayer/Components/Networking/World/NetworkedPluggableObject.cs +++ b/Multiplayer/Components/Networking/World/NetworkedPluggableObject.cs @@ -137,24 +137,17 @@ public bool ValidateInteraction(CommonPitStopPlugInteractionPacket packet, Serve public void ProcessPacket(CommonPitStopPlugInteractionPacket packet) { var interaction = (PlugInteractionType)packet.InteractionType; - ProcessInteraction(interaction, packet.PlayerId, packet.TrainCarNetId, packet.IsLeftSide); + ProcessInteraction(interaction, packet.PlayerId, packet.TrainCarNetId, packet.IsLeftSide, packet.Position, packet.Rotation); } public void ProcessBulkUpdate(PitStopPlugData data) { var interaction = data.State; - ProcessInteraction(interaction, data.PlayerId, data.TrainCarNetId, data.IsLeftSide); - - if (data.State == PlugInteractionType.Dropped) - { - transform.position = data.Position + WorldMover.currentMove; - transform.rotation = data.Rotation; - } - + ProcessInteraction(interaction, data.PlayerId, data.TrainCarNetId, data.IsLeftSide, data.Position, data.Rotation); Refreshed = true; } - public void ProcessInteraction(PlugInteractionType interaction, byte playerId, ushort trainNetId, bool isLeftSide) + public void ProcessInteraction(PlugInteractionType interaction, byte playerId, ushort trainNetId, bool isLeftSide, Vector3? newPosition, Quaternion? newRotation) { bool result; @@ -205,7 +198,13 @@ public void ProcessInteraction(PlugInteractionType interaction, byte playerId, u BlockInteraction(false); PluggableObject.Unplug(); - + + if (newPosition == null || newRotation == null) + return; + + transform.position = (Vector3)newPosition + WorldMover.currentMove; + transform.rotation = (Quaternion)newRotation; + break; case PlugInteractionType.DockHome: @@ -328,7 +327,7 @@ private void OnUngrabbed(ControlImplBase control) return; Multiplayer.LogDebug(() => $"NetworkedPluggableObject.OnUngrabbed() [{transform.parent.name}, {NetId}] station: {Station?.StationName}"); - NetworkLifecycle.Instance.Client?.SendPitStopPlugInteractionPacket(NetId, PlugInteractionType.Dropped); + NetworkLifecycle.Instance.Client?.SendPitStopPlugInteractionPacket(NetId, PlugInteractionType.Dropped, transform.position - WorldMover.currentMove, transform.rotation); } private void OnPlugged(PluggableObject plug, PlugSocket socket) @@ -344,7 +343,7 @@ private void OnPlugged(PluggableObject plug, PlugSocket socket) PlugInteractionType interaction; bool left = false; - ushort trainCarNetId = 0; + ushort carNetId = 0; if (socket == plug.startAttachedTo) interaction = PlugInteractionType.DockHome; @@ -359,7 +358,7 @@ private void OnPlugged(PluggableObject plug, PlugSocket socket) return; } - trainCarNetId = netTrainCar.NetId; + carNetId = netTrainCar.NetId; interaction = PlugInteractionType.DockSocket; var sockets = trainCar.GetComponentsInChildren(); @@ -373,7 +372,7 @@ private void OnPlugged(PluggableObject plug, PlugSocket socket) } } - NetworkLifecycle.Instance.Client?.SendPitStopPlugInteractionPacket(NetId, interaction, trainCarNetId, left); + NetworkLifecycle.Instance.Client?.SendPitStopPlugInteractionPacket(NetId, interaction, trainCarNetId: carNetId, isConnectedLeft: left); } #endregion } diff --git a/Multiplayer/Networking/Data/PlugInteractionType.cs b/Multiplayer/Networking/Data/PlugInteractionType.cs index 7f90069c..be0c7627 100644 --- a/Multiplayer/Networking/Data/PlugInteractionType.cs +++ b/Multiplayer/Networking/Data/PlugInteractionType.cs @@ -1,8 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Multiplayer.Networking.Data { diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 63cd698a..3f6127a5 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -1412,16 +1412,18 @@ public void SendPitStopInteractionPacket(ushort netId, PitStopStationInteraction }, DeliveryMethod.ReliableOrdered); } - public void SendPitStopPlugInteractionPacket(ushort netId, PlugInteractionType interaction, ushort trainCarNetId = 0, bool left = false) + public void SendPitStopPlugInteractionPacket(ushort netId, PlugInteractionType interaction, Vector3? position = null, Quaternion? rotation = null, ushort trainCarNetId = 0, bool isConnectedLeft = false) { - LogDebug(()=>$"SendPitStopInteractionPacket({netId}, {interaction}, {trainCarNetId}, {left})"); + LogDebug(()=>$"SendPitStopInteractionPacket({netId}, {interaction}, {position}, {rotation}, {trainCarNetId}, {isConnectedLeft})"); SendPacketToServer(new CommonPitStopPlugInteractionPacket { NetId = netId, - InteractionType = (byte)interaction, + InteractionType = interaction, TrainCarNetId = trainCarNetId, - IsLeftSide = left, + IsLeftSide = isConnectedLeft, + Position = position, + Rotation = rotation, }, DeliveryMethod.ReliableOrdered); } diff --git a/Multiplayer/Networking/Packets/Common/CommonPitStopPlugInteractionPacket.cs b/Multiplayer/Networking/Packets/Common/CommonPitStopPlugInteractionPacket.cs index 00049c3d..2a2dfe91 100644 --- a/Multiplayer/Networking/Packets/Common/CommonPitStopPlugInteractionPacket.cs +++ b/Multiplayer/Networking/Packets/Common/CommonPitStopPlugInteractionPacket.cs @@ -1,11 +1,76 @@ +using LiteNetLib.Utils; +using Multiplayer.Networking.Data; +using Multiplayer.Networking.Serialization; +using UnityEngine; + namespace Multiplayer.Networking.Packets.Common; -public class CommonPitStopPlugInteractionPacket +public class CommonPitStopPlugInteractionPacket : INetSerializable { public ushort NetId { get; set; } - public byte InteractionType { get; set; } + public PlugInteractionType InteractionType { get; set; } public byte PlayerId { get; set; } public ushort TrainCarNetId { get; set; } public bool IsLeftSide { get; set; } + public Vector3? Position { get; set; } + public Quaternion? Rotation { get; set; } + + public void Deserialize(NetDataReader reader) + { + NetId = reader.GetUShort(); + InteractionType = (PlugInteractionType)reader.GetByte(); + + switch (InteractionType) + { + case PlugInteractionType.Rejected: + break; + + case PlugInteractionType.PickedUp: + PlayerId = reader.GetByte(); + break; + + case PlugInteractionType.Dropped: + Position = Vector3Serializer.Deserialize(reader); + Rotation = QuaternionSerializer.Deserialize(reader); + break; + + case PlugInteractionType.DockHome: + break; + + case PlugInteractionType.DockSocket: + TrainCarNetId = reader.GetUShort(); + IsLeftSide = reader.GetBool(); + break; + } + } + + public void Serialize(NetDataWriter writer) + { + writer.Put(NetId); + writer.Put((byte)InteractionType); + + switch (InteractionType) + { + case PlugInteractionType.Rejected: + break; + + case PlugInteractionType.PickedUp: + writer.Put(PlayerId); + break; + + case PlugInteractionType.Dropped: + Vector3Serializer.Serialize(writer, Position ?? Vector3.zero); + QuaternionSerializer.Serialize(writer, Rotation ?? Quaternion.identity); + break; + + case PlugInteractionType.DockHome: + break; + + case PlugInteractionType.DockSocket: + writer.Put(TrainCarNetId); + writer.Put(IsLeftSide); + break; + } + } } From b923de62671899cf8dee7f46a42c6283a43f7691 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 7 Jun 2025 22:26:24 +1000 Subject: [PATCH 361/521] Standardise NetworkedTrainCar.TryGet() --- Multiplayer/API/ServerPlayerWrapper.cs | 2 +- .../Networking/Player/NetworkedPlayer.cs | 2 +- .../Train/NetworkTrainsetWatcher.cs | 2 +- .../Networking/Train/NetworkedCarSpawner.cs | 2 +- .../Networking/Train/NetworkedTrainCar.cs | 12 ++--- .../Networking/World/NetworkedItem.cs | 2 +- Multiplayer/Networking/Data/ServerPlayer.cs | 6 +-- .../Networking/Data/TaskNetworkData.cs | 4 +- .../Managers/Client/NetworkClient.cs | 48 +++++++++---------- .../Managers/Server/NetworkServer.cs | 16 +++---- 10 files changed, 48 insertions(+), 48 deletions(-) diff --git a/Multiplayer/API/ServerPlayerWrapper.cs b/Multiplayer/API/ServerPlayerWrapper.cs index 979d40d0..bcd3773b 100644 --- a/Multiplayer/API/ServerPlayerWrapper.cs +++ b/Multiplayer/API/ServerPlayerWrapper.cs @@ -36,7 +36,7 @@ public string Username internal TrainCar GetOccupiedCar() { - NetworkedTrainCar.GetTrainCar(_serverPlayer.CarId, out var trainCar); + NetworkedTrainCar.TryGet(_serverPlayer.CarId, out TrainCar trainCar); return trainCar; } } diff --git a/Multiplayer/Components/Networking/Player/NetworkedPlayer.cs b/Multiplayer/Components/Networking/Player/NetworkedPlayer.cs index a42d488b..8aff731b 100644 --- a/Multiplayer/Components/Networking/Player/NetworkedPlayer.cs +++ b/Multiplayer/Components/Networking/Player/NetworkedPlayer.cs @@ -127,7 +127,7 @@ public void UpdatePosition(Vector3 position, Vector2 moveDir, float rotationY, b public void UpdateCar(ushort netId) { - IsOnCar = NetworkedTrainCar.GetTrainCar(netId, out var trainCar); + IsOnCar = NetworkedTrainCar.TryGet(netId, out TrainCar trainCar); OccupiedCar = trainCar; if (IsOnCar) diff --git a/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs b/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs index c1a14830..eac5d8f4 100644 --- a/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs +++ b/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs @@ -188,7 +188,7 @@ public void Client_HandleTrainsetPhysicsUpdate(ClientboundTrainsetPhysicsPacket for (int i = 0; i < packet.TrainsetParts.Length; i++) { - if (NetworkedTrainCar.Get(packet.TrainsetParts[i].NetId ,out NetworkedTrainCar networkedTrainCar)) + if (NetworkedTrainCar.TryGet(packet.TrainsetParts[i].NetId ,out NetworkedTrainCar networkedTrainCar)) { Multiplayer.LogDebug(()=>$"Applying TrainPhysicsUpdate to {packet.TrainsetParts[i].NetId}"); networkedTrainCar.Client_ReceiveTrainPhysicsUpdate(in packet.TrainsetParts[i], packet.Tick); diff --git a/Multiplayer/Components/Networking/Train/NetworkedCarSpawner.cs b/Multiplayer/Components/Networking/Train/NetworkedCarSpawner.cs index 19eb38bf..c6982934 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedCarSpawner.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedCarSpawner.cs @@ -156,7 +156,7 @@ private static void HandleCoupling(CouplingData couplingData, Coupler currentCo if (couplingData.IsCoupled) { - if (!NetworkedTrainCar.GetTrainCar(couplingData.ConnectionNetId, out TrainCar otherCar)) + if (!NetworkedTrainCar.TryGet(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}"); } diff --git a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs index ae744c20..d3bbedfa 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs @@ -32,17 +32,17 @@ public class NetworkedTrainCar : IdMonoBehaviour private static readonly Dictionary trainCarIdToTrainCars = []; private static readonly Dictionary hoseToCoupler = []; - public static bool Get(ushort netId, out NetworkedTrainCar obj) + public static bool TryGet(ushort netId, out NetworkedTrainCar obj) { bool b = Get(netId, out IdMonoBehaviour rawObj); obj = (NetworkedTrainCar)rawObj; return b; } - public static bool GetTrainCar(ushort netId, out TrainCar obj) + public static bool TryGet(ushort netId, out TrainCar trainCar) { - bool b = Get(netId, out NetworkedTrainCar networkedTrainCar); - obj = b ? networkedTrainCar.TrainCar : null; + bool b = TryGet(netId, out NetworkedTrainCar networkedTrainCar); + trainCar = b ? networkedTrainCar.TrainCar : null; return b; } @@ -65,7 +65,7 @@ public static bool TryGetFromTrainCar(TrainCar trainCar, out NetworkedTrainCar n return trainCarsToNetworkedTrainCars.TryGetValue(trainCar, out networkedTrainCar); } - public static bool TryGetNetIdFromTrainCar(TrainCar trainCar, out ushort netId) + public static bool TryGetNetId(TrainCar trainCar, out ushort netId) { netId = 0; @@ -885,7 +885,7 @@ public void Common_ReceiveCouplerInteraction(CommonCouplerInteractionPacket pack if (packet.OtherNetId != 0) { - if (GetTrainCar(packet.OtherNetId, out otherCar)) + if (TryGet(packet.OtherNetId, out otherCar)) otherCoupler = packet.IsFrontOtherCoupler ? otherCar?.frontCoupler : otherCar?.rearCoupler; } diff --git a/Multiplayer/Components/Networking/World/NetworkedItem.cs b/Multiplayer/Components/Networking/World/NetworkedItem.cs index 295e7452..c9a75bd1 100644 --- a/Multiplayer/Components/Networking/World/NetworkedItem.cs +++ b/Multiplayer/Components/Networking/World/NetworkedItem.cs @@ -568,7 +568,7 @@ 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)) + if (!NetworkedTrainCar.TryGet(snapshot.CarNetId, out TrainCar trainCar)) { Multiplayer.LogWarning($"NetworkedItem.HandleAttachedState() CarNetId: {snapshot?.CarNetId} not found for ItemNetId: {snapshot?.ItemNetId}"); return; diff --git a/Multiplayer/Networking/Data/ServerPlayer.cs b/Multiplayer/Networking/Data/ServerPlayer.cs index aec3213e..381250db 100644 --- a/Multiplayer/Networking/Data/ServerPlayer.cs +++ b/Multiplayer/Networking/Data/ServerPlayer.cs @@ -65,7 +65,7 @@ public Vector3 AbsoluteWorldPosition Vector3 pos; try { - if (CarId == 0 || !NetworkedTrainCar.Get(CarId, out NetworkedTrainCar car)) + if (CarId == 0 || !NetworkedTrainCar.TryGet(CarId, out NetworkedTrainCar car)) { if (CarId != 0) Multiplayer.LogDebug(() => $"AbsoluteWorldPosition() noID {Username}: CarId: {CarId}"); @@ -99,7 +99,7 @@ public Vector3 WorldPosition { Vector3 pos; try { - if (CarId == 0 || !NetworkedTrainCar.Get(CarId, out NetworkedTrainCar car)) + if (CarId == 0 || !NetworkedTrainCar.TryGet(CarId, out NetworkedTrainCar car)) { if(CarId != 0) Multiplayer.LogDebug(() =>$"WorldPosition() noID {Username}: CarId: {CarId}"); @@ -126,7 +126,7 @@ public Vector3 WorldPosition { return pos; } } - public float WorldRotationY => CarId == 0 || !NetworkedTrainCar.Get(CarId, out NetworkedTrainCar car) + public float WorldRotationY => CarId == 0 || !NetworkedTrainCar.TryGet(CarId, out NetworkedTrainCar car) ? RawRotationY : (Quaternion.Euler(0, RawRotationY, 0) * car.transform.rotation).eulerAngles.y; #endregion diff --git a/Multiplayer/Networking/Data/TaskNetworkData.cs b/Multiplayer/Networking/Data/TaskNetworkData.cs index 96debcd3..3d73be85 100644 --- a/Multiplayer/Networking/Data/TaskNetworkData.cs +++ b/Multiplayer/Networking/Data/TaskNetworkData.cs @@ -180,7 +180,7 @@ public override Task ToTask() { List cars = CarNetIDs - .Select(netId => NetworkedTrainCar.GetTrainCar(netId, out TrainCar trainCar) ? trainCar : null) + .Select(netId => NetworkedTrainCar.TryGet(netId, out TrainCar trainCar) ? trainCar : null) .Where(car => car != null) .Select(car =>car.logicCar) .ToList(); @@ -297,7 +297,7 @@ 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) + .Select(netId => NetworkedTrainCar.TryGet(netId, out TrainCar trainCar) ? trainCar.logicCar : null) .Where(car => car != null) .ToList(); diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 32a6a780..c8eb60da 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -524,7 +524,7 @@ private void OnClientboundSpawnTrainSetPacket(ClientboundSpawnTrainSetPacket pac private void OnClientboundDestroyTrainCarPacket(ClientboundDestroyTrainCarPacket packet) { - if (!NetworkedTrainCar.Get(packet.NetId, out NetworkedTrainCar netTrainCar)) + if (!NetworkedTrainCar.TryGet(packet.NetId, out NetworkedTrainCar netTrainCar)) { LogWarning($"Received DestroyTrainCarPacket for netId: {packet.NetId}, but NetworkedTrainCar was not found."); return; @@ -566,7 +566,7 @@ public void OnClientboundTrainPhysicsPacket(ClientboundTrainsetPhysicsPacket pac private void OnCommonCouplerInteractionPacket(CommonCouplerInteractionPacket packet) { - if (!NetworkedTrainCar.Get(packet.NetId, out var netTrainCar)) + if (!NetworkedTrainCar.TryGet(packet.NetId, out NetworkedTrainCar netTrainCar)) { LogError($"OnCommonCouplerInteractionPacket netId: {packet.NetId}, TrainCar not found!"); return; @@ -579,7 +579,7 @@ private void OnCommonTrainCouplePacket(CommonTrainCouplePacket packet) // TrainCar trainCar = null; // TrainCar otherTrainCar = null; - // if (!NetworkedTrainCar.GetTrainCar(packet.NetId, out trainCar) || !NetworkedTrainCar.GetTrainCar(packet.OtherNetId, out otherTrainCar)) + // if (!NetworkedTrainCar.TryGet(packet.NetId, out trainCar) || !NetworkedTrainCar.TryGet(packet.OtherNetId, out otherTrainCar)) // { // LogDebug(() => $"OnCommonTrainCouplePacket() netId: {packet.NetId}, trainCar found?: {trainCar != null}, otherNetId: {packet.OtherNetId}, otherTrainCar found?: {otherTrainCar != null}"); // return; @@ -596,7 +596,7 @@ private void OnCommonTrainCouplePacket(CommonTrainCouplePacket packet) private void OnCommonTrainUncouplePacket(CommonTrainUncouplePacket packet) { - if (!NetworkedTrainCar.GetTrainCar(packet.NetId, out TrainCar trainCar)) + if (!NetworkedTrainCar.TryGet(packet.NetId, out TrainCar trainCar)) { LogDebug(() => $"OnCommonTrainUncouplePacket() netId: {packet.NetId}, trainCar found?: {trainCar != null}"); return; @@ -610,8 +610,8 @@ private void OnCommonTrainUncouplePacket(CommonTrainUncouplePacket packet) private void OnCommonHoseConnectedPacket(CommonHoseConnectedPacket packet) { - bool foundTrainCar = NetworkedTrainCar.GetTrainCar(packet.NetId, out TrainCar trainCar); - bool foundOtherTrainCar = NetworkedTrainCar.GetTrainCar(packet.OtherNetId, out TrainCar otherTrainCar); + bool foundTrainCar = NetworkedTrainCar.TryGet(packet.NetId, out TrainCar trainCar); + bool foundOtherTrainCar = NetworkedTrainCar.TryGet(packet.OtherNetId, out TrainCar otherTrainCar); if (!foundTrainCar || trainCar == null || !foundOtherTrainCar || otherTrainCar == null) @@ -656,7 +656,7 @@ private void OnCommonHoseConnectedPacket(CommonHoseConnectedPacket packet) private void OnCommonHoseDisconnectedPacket(CommonHoseDisconnectedPacket packet) { - if (!NetworkedTrainCar.Get(packet.NetId, out NetworkedTrainCar netTrainCar) || netTrainCar.IsDestroying) + if (!NetworkedTrainCar.TryGet(packet.NetId, out NetworkedTrainCar netTrainCar) || netTrainCar.IsDestroying) return; TrainCar trainCar = netTrainCar.TrainCar; @@ -670,7 +670,7 @@ private void OnCommonHoseDisconnectedPacket(CommonHoseDisconnectedPacket packet) private void OnCommonMuConnectedPacket(CommonMuConnectedPacket packet) { - if (!NetworkedTrainCar.GetTrainCar(packet.NetId, out TrainCar trainCar) || !NetworkedTrainCar.GetTrainCar(packet.OtherNetId, out TrainCar otherTrainCar)) + if (!NetworkedTrainCar.TryGet(packet.NetId, out TrainCar trainCar) || !NetworkedTrainCar.TryGet(packet.OtherNetId, out TrainCar otherTrainCar)) return; MultipleUnitCable cable = packet.IsFront ? trainCar.muModule.frontCable : trainCar.muModule.rearCable; @@ -681,7 +681,7 @@ private void OnCommonMuConnectedPacket(CommonMuConnectedPacket packet) private void OnCommonMuDisconnectedPacket(CommonMuDisconnectedPacket packet) { - if (!NetworkedTrainCar.GetTrainCar(packet.NetId, out TrainCar trainCar)) + if (!NetworkedTrainCar.TryGet(packet.NetId, out TrainCar trainCar)) return; MultipleUnitCable cable = packet.IsFront ? trainCar.muModule.frontCable : trainCar.muModule.rearCable; @@ -691,7 +691,7 @@ private void OnCommonMuDisconnectedPacket(CommonMuDisconnectedPacket packet) private void OnCommonCockFiddlePacket(CommonCockFiddlePacket packet) { - if (!NetworkedTrainCar.GetTrainCar(packet.NetId, out TrainCar trainCar)) + if (!NetworkedTrainCar.TryGet(packet.NetId, out TrainCar trainCar)) return; Coupler coupler = packet.IsFront ? trainCar.frontCoupler : trainCar.rearCoupler; @@ -701,7 +701,7 @@ private void OnCommonCockFiddlePacket(CommonCockFiddlePacket packet) private void OnCommonBrakeCylinderReleasePacket(CommonBrakeCylinderReleasePacket packet) { - if (!NetworkedTrainCar.GetTrainCar(packet.NetId, out TrainCar trainCar)) + if (!NetworkedTrainCar.TryGet(packet.NetId, out TrainCar trainCar)) return; trainCar.brakeSystem.ReleaseBrakeCylinderPressure(); @@ -709,7 +709,7 @@ private void OnCommonBrakeCylinderReleasePacket(CommonBrakeCylinderReleasePacket private void OnCommonHandbrakePositionPacket(CommonHandbrakePositionPacket packet) { - if (!NetworkedTrainCar.GetTrainCar(packet.NetId, out TrainCar trainCar)) + if (!NetworkedTrainCar.TryGet(packet.NetId, out TrainCar trainCar)) return; trainCar.brakeSystem.SetHandbrakePosition(packet.Position); @@ -717,7 +717,7 @@ private void OnCommonHandbrakePositionPacket(CommonHandbrakePositionPacket packe private void OnCommonSimFlowPacket(CommonTrainPortsPacket packet) { - if (!NetworkedTrainCar.Get(packet.NetId, out NetworkedTrainCar networkedTrainCar)) + if (!NetworkedTrainCar.TryGet(packet.NetId, out NetworkedTrainCar networkedTrainCar)) return; networkedTrainCar.Common_UpdatePorts(packet); @@ -725,7 +725,7 @@ private void OnCommonSimFlowPacket(CommonTrainPortsPacket packet) private void OnCommonTrainFusesPacket(CommonTrainFusesPacket packet) { - if (!NetworkedTrainCar.Get(packet.NetId, out NetworkedTrainCar networkedTrainCar)) + if (!NetworkedTrainCar.TryGet(packet.NetId, out NetworkedTrainCar networkedTrainCar)) return; networkedTrainCar.Common_UpdateFuses(packet); @@ -733,7 +733,7 @@ private void OnCommonTrainFusesPacket(CommonTrainFusesPacket packet) private void OnClientboundBrakeStateUpdatePacket(ClientboundBrakeStateUpdatePacket packet) { - if (!NetworkedTrainCar.Get(packet.NetId, out NetworkedTrainCar networkedTrainCar)) + if (!NetworkedTrainCar.TryGet(packet.NetId, out NetworkedTrainCar networkedTrainCar)) return; @@ -744,7 +744,7 @@ private void OnClientboundBrakeStateUpdatePacket(ClientboundBrakeStateUpdatePack private void OnClientboundFireboxStatePacket(ClientboundFireboxStatePacket packet) { - if (!NetworkedTrainCar.Get(packet.NetId, out NetworkedTrainCar networkedTrainCar)) + if (!NetworkedTrainCar.TryGet(packet.NetId, out NetworkedTrainCar networkedTrainCar)) return; @@ -753,7 +753,7 @@ private void OnClientboundFireboxStatePacket(ClientboundFireboxStatePacket packe private void OnClientboundCargoStatePacket(ClientboundCargoStatePacket packet) { - if (!NetworkedTrainCar.Get(packet.NetId, out NetworkedTrainCar networkedTrainCar)) + if (!NetworkedTrainCar.TryGet(packet.NetId, out NetworkedTrainCar networkedTrainCar)) return; LogDebug(() => $"OnClientboundCargoStatePacket() {networkedTrainCar.CurrentID}, health: {packet.CargoHealth}"); @@ -822,7 +822,7 @@ private void OnClientboundCargoStatePacket(ClientboundCargoStatePacket packet) private void OnClientboundCargoHealthUpdatePacket(ClientboundCargoHealthUpdatePacket packet) { - if (!NetworkedTrainCar.Get(packet.NetId, out NetworkedTrainCar networkedTrainCar)) + if (!NetworkedTrainCar.TryGet(packet.NetId, out NetworkedTrainCar networkedTrainCar)) return; CargoDamageModel cargoDamageModel = networkedTrainCar.TrainCar.CargoDamage; @@ -840,7 +840,7 @@ private void OnClientboundCargoHealthUpdatePacket(ClientboundCargoHealthUpdatePa private void OnClientboundCarHealthUpdatePacket(ClientboundCarHealthUpdatePacket packet) { - if (!NetworkedTrainCar.GetTrainCar(packet.NetId, out TrainCar trainCar)) + if (!NetworkedTrainCar.TryGet(packet.NetId, out TrainCar trainCar)) return; packet.Health.LoadTo(trainCar); @@ -849,9 +849,9 @@ private void OnClientboundCarHealthUpdatePacket(ClientboundCarHealthUpdatePacket private void OnClientboundRerailTrainPacket(ClientboundRerailTrainPacket packet) { - if (!NetworkedTrainCar.GetTrainCar(packet.NetId, out TrainCar trainCar)) + if (!NetworkedTrainCar.TryGet(packet.NetId, out TrainCar trainCar)) return; - if (!NetworkedRailTrack.Get(packet.TrackId, out NetworkedRailTrack networkedRailTrack)) + if (!NetworkedRailTrack.TryGet(packet.TrackId, out NetworkedRailTrack networkedRailTrack)) return; Log($"Rerailing [{trainCar?.ID}, {packet.NetId}] to track {networkedRailTrack?.RailTrack?.LogicTrack()?.ID}"); @@ -861,7 +861,7 @@ private void OnClientboundRerailTrainPacket(ClientboundRerailTrainPacket packet) private void OnClientboundWindowsBrokenPacket(ClientboundWindowsBrokenPacket packet) { - if (!NetworkedTrainCar.GetTrainCar(packet.NetId, out TrainCar trainCar)) + if (!NetworkedTrainCar.TryGet(packet.NetId, out TrainCar trainCar)) return; DamageController damageController = trainCar.GetComponent(); if (damageController == null) @@ -874,7 +874,7 @@ private void OnClientboundWindowsBrokenPacket(ClientboundWindowsBrokenPacket pac private void OnClientboundWindowsRepairedPacket(ClientboundWindowsRepairedPacket packet) { - if (!NetworkedTrainCar.GetTrainCar(packet.NetId, out TrainCar trainCar)) + if (!NetworkedTrainCar.TryGet(packet.NetId, out TrainCar trainCar)) return; DamageController damageController = trainCar.GetComponent(); if (damageController == null) @@ -1002,7 +1002,7 @@ private void OnCommonItemChangePacket(CommonItemChangePacket packet) private void OnCommonPaintThemePacket(CommonPaintThemePacket packet) { - if (!NetworkedTrainCar.Get(packet.NetId, out NetworkedTrainCar netTrainCar)) + if (!NetworkedTrainCar.TryGet(packet.NetId, out NetworkedTrainCar netTrainCar)) return; Log($"Received paint theme change for {netTrainCar?.CurrentID}"); diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index 7919b06e..a0b2d224 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -965,7 +965,7 @@ private void OnCommonCouplerInteractionPacket(CommonCouplerInteractionPacket pac } //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 - if(NetworkedTrainCar.Get(packet.NetId, out var netTrainCar)) + if(NetworkedTrainCar.TryGet(packet.NetId, out NetworkedTrainCar netTrainCar)) { if(netTrainCar.Server_ValidateCouplerInteraction(packet, player)) { @@ -1051,7 +1051,7 @@ private void OnServerboundAddCoalPacket(ServerboundAddCoalPacket packet, ITransp if (!TryGetServerPlayer(peer, out ServerPlayer player)) return; - if (!NetworkedTrainCar.Get(packet.NetId, out NetworkedTrainCar networkedTrainCar)) + if (!NetworkedTrainCar.TryGet(packet.NetId, out NetworkedTrainCar networkedTrainCar)) return; //is value valid? @@ -1074,7 +1074,7 @@ private void OnServerboundFireboxIgnitePacket(ServerboundFireboxIgnitePacket pac if (!TryGetServerPlayer(peer, out ServerPlayer player)) return; - if (!NetworkedTrainCar.Get(packet.NetId, out NetworkedTrainCar networkedTrainCar)) + if (!NetworkedTrainCar.TryGet(packet.NetId, out NetworkedTrainCar networkedTrainCar)) return; if (!NetworkLifecycle.Instance.IsHost(peer)) @@ -1090,7 +1090,7 @@ private void OnCommonTrainPortsPacket(CommonTrainPortsPacket packet, ITransportP { if (!TryGetServerPlayer(peer, out ServerPlayer player)) return; - if (!NetworkedTrainCar.Get(packet.NetId, out NetworkedTrainCar networkedTrainCar)) + if (!NetworkedTrainCar.TryGet(packet.NetId, out NetworkedTrainCar networkedTrainCar)) return; //if not the host && validation fails then ignore packet @@ -1115,7 +1115,7 @@ private void OnCommonTrainFusesPacket(CommonTrainFusesPacket packet, ITransportP private void OnServerboundTrainSyncRequestPacket(ServerboundTrainSyncRequestPacket packet) { - if (NetworkedTrainCar.Get(packet.NetId, out NetworkedTrainCar networkedTrainCar)) + if (NetworkedTrainCar.TryGet(packet.NetId, out NetworkedTrainCar networkedTrainCar)) networkedTrainCar.Server_DirtyAllState(); } @@ -1123,7 +1123,7 @@ private void OnServerboundTrainDeleteRequestPacket(ServerboundTrainDeleteRequest { if (!TryGetServerPlayer(peer, out ServerPlayer player)) return; - if (!NetworkedTrainCar.Get(packet.NetId, out NetworkedTrainCar networkedTrainCar)) + if (!NetworkedTrainCar.TryGet(packet.NetId, out NetworkedTrainCar networkedTrainCar)) return; if (networkedTrainCar.HasPlayers) @@ -1158,9 +1158,9 @@ private void OnServerboundTrainRerailRequestPacket(ServerboundTrainRerailRequest { if (!TryGetServerPlayer(peer, out ServerPlayer player)) return; - if (!NetworkedTrainCar.Get(packet.NetId, out NetworkedTrainCar networkedTrainCar)) + if (!NetworkedTrainCar.TryGet(packet.NetId, out NetworkedTrainCar networkedTrainCar)) return; - if (!NetworkedRailTrack.Get(packet.TrackId, out NetworkedRailTrack networkedRailTrack)) + if (!NetworkedRailTrack.TryGet(packet.TrackId, out NetworkedRailTrack networkedRailTrack)) return; TrainCar trainCar = networkedTrainCar.TrainCar; From 00f41eaf8c2604132a6212739bf9c53eaae4a79f Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 7 Jun 2025 22:27:42 +1000 Subject: [PATCH 362/521] Register sync'd objects with NetIdProvider --- Multiplayer/API/NetIdProvider.cs | 18 ++++- .../Networking/Jobs/NetworkedJob.cs | 12 +++ .../Networking/Train/NetworkedBogie.cs | 2 +- .../Networking/Train/NetworkedCarSpawner.cs | 4 +- .../Networking/World/NetworkedItem.cs | 12 +++ .../Networking/World/NetworkedJunction.cs | 54 +++++++++++-- .../Networking/World/NetworkedRailTrack.cs | 53 +++++++++---- .../World/NetworkedStationController.cs | 77 ++++++++++++++++++- .../Networking/World/NetworkedTurntable.cs | 37 ++++++++- 9 files changed, 241 insertions(+), 28 deletions(-) diff --git a/Multiplayer/API/NetIdProvider.cs b/Multiplayer/API/NetIdProvider.cs index d285319d..b2d5e1d1 100644 --- a/Multiplayer/API/NetIdProvider.cs +++ b/Multiplayer/API/NetIdProvider.cs @@ -1,7 +1,11 @@ +using DV.CabControls; +using DV.Logic.Job; using DV.Utils; using JetBrains.Annotations; using MPAPI.Interfaces; +using Multiplayer.Components.Networking.Jobs; using Multiplayer.Components.Networking.Train; +using Multiplayer.Components.Networking.World; using System; using System.Collections.Generic; @@ -17,7 +21,19 @@ internal class NetIdProvider : SingletonBehaviour, INetIdProvider protected override void Awake() { base.Awake(); - RegisterHandler(NetworkedTrainCar.TryGetNetIdFromTrainCar, NetworkedTrainCar.GetTrainCar); + RegisterHandler(NetworkedTrainCar.TryGetNetId, NetworkedTrainCar.TryGet); + + RegisterHandler(NetworkedJunction.TryGetNetId, NetworkedJunction.TryGet); + RegisterHandler(NetworkedTurntable.TryGetNetId, NetworkedTurntable.TryGet); + RegisterHandler(NetworkedRailTrack.TryGetNetId, NetworkedRailTrack.TryGet); + + RegisterHandler(NetworkedStationController.TryGetNetId, NetworkedStationController.TryGet); + RegisterHandler(NetworkedStationController.TryGetNetId, NetworkedStationController.TryGet); + RegisterHandler(NetworkedStationController.TryGetNetId, NetworkedStationController.TryGet); + + RegisterHandler(NetworkedJob.TryGetNetId, NetworkedJob.GetJob); + + RegisterHandler(NetworkedItem.TryGetNetId, NetworkedItem.GetItem); } public void RegisterHandler(TryGetNetIdDelegate tryGetNetId, TryGetObjectDelegate tryGetObject) where T : class diff --git a/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs b/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs index 2f3232dc..0f995847 100644 --- a/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs +++ b/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs @@ -44,6 +44,18 @@ public static bool TryGetFromJobId(string jobId, out NetworkedJob networkedJob) return jobIdToNetworkedJob.TryGetValue(jobId, out networkedJob); } + public static bool TryGetNetId(Job job, out ushort netId) + { + if (TryGetFromJob(job, out var networkedJob)) + { + netId = networkedJob.NetId; + return true; + } + + netId = 0; + return false; + } + #endregion protected override bool IsIdServerAuthoritative => true; public enum DirtyCause diff --git a/Multiplayer/Components/Networking/Train/NetworkedBogie.cs b/Multiplayer/Components/Networking/Train/NetworkedBogie.cs index 2cf23782..e8072b48 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedBogie.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedBogie.cs @@ -64,7 +64,7 @@ protected override void Process(BogieData snapshot, uint snapshotTick) if (snapshot.IncludesTrackData) { - if (!NetworkedRailTrack.Get(snapshot.TrackNetId, out NetworkedRailTrack track)) + if (!NetworkedRailTrack.TryGet(snapshot.TrackNetId, out NetworkedRailTrack track)) { Multiplayer.LogWarning($"NetworkedBogie.Process({identifier}) Failed to find track {snapshot.TrackNetId} for bogie: {Bogie.Car.ID}"); return; diff --git a/Multiplayer/Components/Networking/Train/NetworkedCarSpawner.cs b/Multiplayer/Components/Networking/Train/NetworkedCarSpawner.cs index c6982934..52ed1196 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedCarSpawner.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedCarSpawner.cs @@ -36,13 +36,13 @@ public static void SpawnCars(TrainsetSpawnPart[] parts, bool autoCouple) public static NetworkedTrainCar SpawnCar(TrainsetSpawnPart spawnPart, bool preventCoupling = false) { - if (!NetworkedRailTrack.Get(spawnPart.Bogie1.TrackNetId, out NetworkedRailTrack bogie1Track) && spawnPart.Bogie1.TrackNetId != 0) + if (!NetworkedRailTrack.TryGet(spawnPart.Bogie1.TrackNetId, out NetworkedRailTrack bogie1Track) && spawnPart.Bogie1.TrackNetId != 0) { NetworkLifecycle.Instance.Client.LogDebug(() => $"Tried spawning car but couldn't find track with index {spawnPart.Bogie1.TrackNetId}"); return null; } - if (!NetworkedRailTrack.Get(spawnPart.Bogie2.TrackNetId, out NetworkedRailTrack bogie2Track) && spawnPart.Bogie2.TrackNetId != 0) + if (!NetworkedRailTrack.TryGet(spawnPart.Bogie2.TrackNetId, out NetworkedRailTrack bogie2Track) && spawnPart.Bogie2.TrackNetId != 0) { NetworkLifecycle.Instance.Client.LogDebug(() => $"Tried spawning car but couldn't find track with index {spawnPart.Bogie2.TrackNetId}"); return null; diff --git a/Multiplayer/Components/Networking/World/NetworkedItem.cs b/Multiplayer/Components/Networking/World/NetworkedItem.cs index c9a75bd1..c12afc70 100644 --- a/Multiplayer/Components/Networking/World/NetworkedItem.cs +++ b/Multiplayer/Components/Networking/World/NetworkedItem.cs @@ -57,6 +57,18 @@ public static bool TryGetNetworkedItem(ItemBase item, out NetworkedItem networke { return itemBaseToNetworkedItem.TryGetValue(item, out networkedItem); } + + public static bool TryGetNetId(ItemBase item, out ushort netID) + { + if (itemBaseToNetworkedItem.TryGetValue(item, out var networkedItem)) + { + netID = networkedItem.NetId; + return true; + } + + netID = 0; + return false; + } #endregion private const float PositionThreshold = 0.1f; diff --git a/Multiplayer/Components/Networking/World/NetworkedJunction.cs b/Multiplayer/Components/Networking/World/NetworkedJunction.cs index 9a00ea1c..f57d3c7a 100644 --- a/Multiplayer/Components/Networking/World/NetworkedJunction.cs +++ b/Multiplayer/Components/Networking/World/NetworkedJunction.cs @@ -1,13 +1,48 @@ +using System.Collections.Generic; using System.Linq; -using DV; namespace Multiplayer.Components.Networking.World; public class NetworkedJunction : IdMonoBehaviour { + #region Lookup Cache private static NetworkedJunction[] _indexedJunctions; + private static readonly Dictionary junctionToNetworkedJunction = []; public static NetworkedJunction[] IndexedJunctions => _indexedJunctions ??= RailTrackRegistry.Instance.TrackRootParent.GetComponentsInChildren().OrderBy(nj => nj.NetId).ToArray(); + public static bool Get(ushort netId, out NetworkedJunction obj) + { + bool b = Get(netId, out IdMonoBehaviour rawObj); + obj = (NetworkedJunction)rawObj; + return b; + } + + public static bool TryGet(ushort netId, out Junction junction) + { + if(Get(netId, out var networkedJunction)) + { + junction = networkedJunction.Junction; + return true; + } + + junction = null; + return false; + } + + public static bool TryGetNetId(Junction junction, out ushort netId) + { + if (junctionToNetworkedJunction.TryGetValue(junction, out var networkedJunction)) + { + netId = networkedJunction.NetId; + return true; + } + + netId = 0; + return false; + } + + #endregion + protected override bool IsIdServerAuthoritative => false; public Junction Junction; @@ -18,9 +53,19 @@ protected override void Awake() base.Awake(); Junction = GetComponent(); Junction.Switched += Junction_Switched; + junctionToNetworkedJunction[Junction] = this; initialised = NetworkLifecycle.Instance.IsHost(); } + protected override void OnDestroy() + { + base.OnDestroy(); + + if (UnloadWatcher.isQuitting) + return; + + junctionToNetworkedJunction.Remove(Junction); + } private void Junction_Switched(Junction.SwitchMode switchMode, int branch) { @@ -38,11 +83,4 @@ public void Switch(byte mode, byte selectedBranch, bool initialising = false) if (!initialised && initialising) initialised = true; } - - public static bool Get(ushort netId, out NetworkedJunction obj) - { - bool b = Get(netId, out IdMonoBehaviour rawObj); - obj = (NetworkedJunction)rawObj; - return b; - } } diff --git a/Multiplayer/Components/Networking/World/NetworkedRailTrack.cs b/Multiplayer/Components/Networking/World/NetworkedRailTrack.cs index 9d4f8c77..45f35ce3 100644 --- a/Multiplayer/Components/Networking/World/NetworkedRailTrack.cs +++ b/Multiplayer/Components/Networking/World/NetworkedRailTrack.cs @@ -4,7 +4,46 @@ namespace Multiplayer.Components.Networking.World; public class NetworkedRailTrack : IdMonoBehaviour { - private static readonly Dictionary railTracksToNetworkedRailTracks = new(); + #region Lookup Cache + private static readonly Dictionary railTracksToNetworkedRailTracks = []; + + public static bool TryGet(ushort netId, out NetworkedRailTrack networkedRailTrack) + { + bool b = Get(netId, out IdMonoBehaviour rawObj); + networkedRailTrack = (NetworkedRailTrack)rawObj; + return b; + } + + public static bool TryGet(ushort netId, out RailTrack railTrack) + { + if (TryGet(netId, out NetworkedRailTrack networkedRailTrack)) + { + railTrack = networkedRailTrack.RailTrack; + return true; + } + + railTrack = null; + return false; + } + + public static bool TryGetNetId(RailTrack track, out ushort netId) + { + if (railTracksToNetworkedRailTracks.TryGetValue(track, out var networkedRailTrack)) + { + netId = networkedRailTrack.NetId; + return true; + } + + netId = 0; + return false; + } + + public static NetworkedRailTrack GetFromRailTrack(RailTrack railTrack) + { + return railTracksToNetworkedRailTracks[railTrack]; + } + + #endregion protected override bool IsIdServerAuthoritative => false; @@ -22,16 +61,4 @@ protected override void OnDestroy() base.OnDestroy(); railTracksToNetworkedRailTracks.Remove(RailTrack); } - - public static bool Get(ushort netId, out NetworkedRailTrack obj) - { - bool b = Get(netId, out IdMonoBehaviour rawObj); - obj = (NetworkedRailTrack)rawObj; - return b; - } - - public static NetworkedRailTrack GetFromRailTrack(RailTrack railTrack) - { - return railTracksToNetworkedRailTracks[railTrack]; - } } diff --git a/Multiplayer/Components/Networking/World/NetworkedStationController.cs b/Multiplayer/Components/Networking/World/NetworkedStationController.cs index 4d08c82b..b6482580 100644 --- a/Multiplayer/Components/Networking/World/NetworkedStationController.cs +++ b/Multiplayer/Components/Networking/World/NetworkedStationController.cs @@ -32,6 +32,79 @@ public static bool Get(ushort netId, out NetworkedStationController obj) return b; } + + public static bool TryGet(ushort netId, out StationController stationController) + { + if (Get(netId, out var networkedStationController)) + { + stationController = networkedStationController.StationController; + return true; + } + + stationController = null; + return false; + } + + public static bool TryGet(ushort netId, out Station station) + { + if (Get(netId, out var networkedStationController)) + { + station = networkedStationController.StationController.logicStation; + return true; + } + + station = null; + return false; + } + + public static bool TryGet(ushort netId, out JobValidator jobValidator) + { + if (Get(netId, out var networkedStationController)) + { + jobValidator = networkedStationController.JobValidator; + return true; + } + + jobValidator = null; + return false; + } + + public static bool TryGetNetId(StationController stationController, out ushort netId) + { + if (GetFromStationController(stationController, out var networkedStationController)) + { + netId = networkedStationController.NetId; + return true; + } + + netId = 0; + return false; + } + + public static bool TryGetNetId(Station station, out ushort netId) + { + if (GetFromStation(station, out var networkedStationController)) + { + netId = networkedStationController.NetId; + return true; + } + + netId = 0; + return false; + } + + public static bool TryGetNetId(JobValidator jobValidator, out ushort netId) + { + if (GetFromJobValidator(jobValidator, out var networkedStationController)) + { + netId = networkedStationController.NetId; + return true; + } + + netId = 0; + return false; + } + public static DictionaryGetAll() { Dictionary result = []; @@ -328,7 +401,7 @@ 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) + if (!NetworkedTrainCar.TryGet(carNetId, out NetworkedTrainCar car) || !car.Client_Initialized) { //car not spawned or not yet initialised return false; @@ -523,7 +596,7 @@ public static IEnumerator UpdateCarPlates(List carNetIds, string jobId) while (frameCounter < MAX_FRAMES) { - if (NetworkedTrainCar.GetTrainCar(carNetId, out trainCar) && + if (NetworkedTrainCar.TryGet(carNetId, out trainCar) && trainCar != null && trainCar.trainPlatesCtrl?.trainCarPlates != null && trainCar.trainPlatesCtrl.trainCarPlates.Count > 0) diff --git a/Multiplayer/Components/Networking/World/NetworkedTurntable.cs b/Multiplayer/Components/Networking/World/NetworkedTurntable.cs index 0d8167ac..d1655d93 100644 --- a/Multiplayer/Components/Networking/World/NetworkedTurntable.cs +++ b/Multiplayer/Components/Networking/World/NetworkedTurntable.cs @@ -1,14 +1,44 @@ -using System.Linq; using DV; +using System.Collections.Generic; +using System.Linq; using UnityEngine; namespace Multiplayer.Components.Networking.World; public class NetworkedTurntable : IdMonoBehaviour { + #region Lookup Cache private static NetworkedTurntable[] _indexedTurntables; + private static readonly Dictionary turntableToNetworkedTurntable = []; public static NetworkedTurntable[] IndexedTurntables => _indexedTurntables ??= RailTrackRegistry.Instance.TrackRootParent.GetComponentsInChildren().OrderBy(nj => nj.NetId).ToArray(); + + public static bool TryGet(ushort netId, out TurntableRailTrack turntable) + { + if (Get((byte)netId, out var networkedTurntable)) + { + turntable = networkedTurntable.TurntableRailTrack; + return true; + } + + turntable = null; + return false; + } + + public static bool TryGetNetId(TurntableRailTrack turntable, out ushort netId) + { + if (turntableToNetworkedTurntable.TryGetValue(turntable, out var networkedTurntable)) + { + netId = networkedTurntable.NetId; + return true; + } + + netId = 0; + return false; + } + + #endregion + protected override bool IsIdServerAuthoritative => false; public TurntableRailTrack TurntableRailTrack; @@ -19,6 +49,8 @@ protected override void Awake() { base.Awake(); TurntableRailTrack = GetComponent(); + turntableToNetworkedTurntable[TurntableRailTrack] = this; + NetworkLifecycle.Instance.OnTick += OnTick; initialised = NetworkLifecycle.Instance.IsHost(); @@ -29,6 +61,9 @@ protected override void OnDestroy() base.OnDestroy(); if (UnloadWatcher.isQuitting) return; + + turntableToNetworkedTurntable.Remove(TurntableRailTrack); + NetworkLifecycle.Instance.OnTick -= OnTick; } From cba11e1c6e0e9b2196a2a1e8ab6ced6af8ac9ef8 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 7 Jun 2025 23:28:10 +1000 Subject: [PATCH 363/521] Add framework for chat integration system --- Multiplayer/API/ServerAPIProvider.cs | 20 +++++++++++++ MultiplayerAPI/Interfaces/IServer.cs | 43 ++++++++++++++++++++++++++-- 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/Multiplayer/API/ServerAPIProvider.cs b/Multiplayer/API/ServerAPIProvider.cs index 66b59c97..c0db5702 100644 --- a/Multiplayer/API/ServerAPIProvider.cs +++ b/Multiplayer/API/ServerAPIProvider.cs @@ -1,5 +1,6 @@ using MPAPI.Interfaces; using MPAPI.Interfaces.Packets; +using Multiplayer.Networking.Data; using Multiplayer.Networking.Managers.Server; using Multiplayer.Utils; using System; @@ -66,6 +67,25 @@ public class ServerAPIProvider : IServer public float AnyPlayerSqrMag(Vector3 anchor) => DvExtensions.AnyPlayerSqrMag(anchor); #endregion + #region Chat + public void SendServerChatMessage(string message, IPlayer player = null) + { + ServerPlayer serverPlayer = player as ServerPlayer; + ChatManager.ServerMessage(message, null, serverPlayer?.Peer); + } + + public bool RegisterChatCommand(string commandLong, string commandShort, Func helpMessage, Action callback) + { + //todo: create chat command registration system + throw new NotImplementedException(); + } + + public void RegisterChatFilter(Func callback) + { + //todo: create chat filter system + throw new NotImplementedException(); + } + #endregion #region Class Helpers internal ServerAPIProvider(NetworkServer serverInstance) diff --git a/MultiplayerAPI/Interfaces/IServer.cs b/MultiplayerAPI/Interfaces/IServer.cs index 9874e043..c76eb1c1 100644 --- a/MultiplayerAPI/Interfaces/IServer.cs +++ b/MultiplayerAPI/Interfaces/IServer.cs @@ -72,8 +72,47 @@ public interface IServer #endregion #region Server Util - float AnyPlayerSqrMag(GameObject item); + /// + /// Returns the distance (Square Magnitude) of the closest player to a given GameObject + /// + /// GameObject to compare players against + // Returns the distance (Square Magnitude) of the closest player, or float.MaxValue if no player is nearby + float AnyPlayerSqrMag(GameObject gameObject); + /// + /// Returns the distance (Square Magnitude) of the closest player to a given point + /// + /// Anchor point to compare players against + /// Returns the distance (Square Magnitude) of the closest player, or float.MaxValue if no player is nearby float AnyPlayerSqrMag(Vector3 anchor); - #endregion + #endregion + + #region Chat + /// + /// Sends a server chat message + /// + /// Message to be sent + /// Player to exclude. If null, message will go to all players + void SendServerChatMessage(string message, IPlayer excludePlayer = null); + + /// + /// Registers a chat command e.g. `/server` and optional short command '/s' + /// + /// Command to be filtered for, without a leading '/' e.g. 'server' + /// Optional short command to be filtered for, without a leading '/' e.g. 's' + /// Optional callback for a help message e.g. "Send a message as the server (host only)\r\n\t\t/server \r\n\t\t/s " It is recommended to provide localisation/translation for this string + /// Action to execute when the command is triggered. First parameter contains command arguments as string array, second parameter is the player who executed the command. + /// True if the command was successfully registered, false if registration failed (e.g. command already exists). + bool RegisterChatCommand(string commandLong, string commandShort, Func helpMessage, Action callback); + + + /// + /// Registers a chat filter that processes non-command messages in registration order. + /// Filters form a chain where each can either allow the message to continue to the next filter or block further processing. + /// If all filters return true, the message will be sent to all players (default action). + /// + /// Filter function that processes the message. First parameter is the message content, second parameter is the player who sent the message. Return true to pass the message to the next filter/default action, false to block propagation. + void RegisterChatFilter(Func callback); + + #endregion } From d4d0aad89d94de886ed98e02ea4f27c6bcdd59ee Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 8 Jun 2025 11:28:06 +1000 Subject: [PATCH 364/521] Implement chat message callbacks for Server API --- Multiplayer/API/ServerAPIProvider.cs | 11 +- .../Networking/Managers/Server/ChatManager.cs | 304 +++++++++++------- .../Managers/Server/NetworkServer.cs | 11 +- MultiplayerAPI/Interfaces/IServer.cs | 15 +- 4 files changed, 202 insertions(+), 139 deletions(-) diff --git a/Multiplayer/API/ServerAPIProvider.cs b/Multiplayer/API/ServerAPIProvider.cs index c0db5702..38142144 100644 --- a/Multiplayer/API/ServerAPIProvider.cs +++ b/Multiplayer/API/ServerAPIProvider.cs @@ -70,20 +70,17 @@ public class ServerAPIProvider : IServer #region Chat public void SendServerChatMessage(string message, IPlayer player = null) { - ServerPlayer serverPlayer = player as ServerPlayer; - ChatManager.ServerMessage(message, null, serverPlayer?.Peer); + server.ChatManager.ServerMessage(message, null, player); } - public bool RegisterChatCommand(string commandLong, string commandShort, Func helpMessage, Action callback) + public bool RegisterChatCommand(string commandLong, string commandShort, Func helpMessage, Action callback) { - //todo: create chat command registration system - throw new NotImplementedException(); + return server.ChatManager.RegisterChatCommand(commandLong, commandShort, helpMessage, callback); } public void RegisterChatFilter(Func callback) { - //todo: create chat filter system - throw new NotImplementedException(); + server.ChatManager.RegisterChatFilter(callback); } #endregion diff --git a/Multiplayer/Networking/Managers/Server/ChatManager.cs b/Multiplayer/Networking/Managers/Server/ChatManager.cs index 27769f96..ca050fc7 100644 --- a/Multiplayer/Networking/Managers/Server/ChatManager.cs +++ b/Multiplayer/Networking/Managers/Server/ChatManager.cs @@ -1,12 +1,16 @@ +using MPAPI.Interfaces; using Multiplayer.Components.Networking; -using System.Linq; using Multiplayer.Networking.Data; -using System.Text.RegularExpressions; using Multiplayer.Networking.TransportLayers; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; namespace Multiplayer.Networking.Managers.Server; -public static class ChatManager +public class ChatManager { public const string COMMAND_SERVER = "server"; public const string COMMAND_SERVER_SHORT = "s"; @@ -17,209 +21,259 @@ public static class ChatManager 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"; - public static void ProcessMessage(string message, ITransportPeer sender) + private readonly Dictionary> _registeredCommands = []; + private readonly Dictionary> _registeredHelpMessages = []; + private readonly List> _chatFilters = []; + + public ChatManager() { + RegisterBuiltInCommands(); + } - if (message == null || message == string.Empty) - return; + private void RegisterBuiltInCommands() + { + RegisterChatCommand + ( + COMMAND_SERVER, + COMMAND_SERVER_SHORT, + () => $"\r\n\r\n\t{Locale.CHAT_HELP_SERVER_MSG}" + + $"\r\n\t\t/{COMMAND_SERVER} <{Locale.CHAT_HELP_MSG}>" + + $"\r\n\t\t/{COMMAND_SERVER_SHORT} <{Locale.CHAT_HELP_MSG}>", + (message, sender) => ServerMessage(message, sender, null) + ); + + RegisterChatCommand + ( + COMMAND_WHISPER, + COMMAND_WHISPER_SHORT, + ()=> $"\r\n\r\n\t{Locale.CHAT_HELP_WHISPER_MSG}" + + $"\r\n\t\t/{COMMAND_WHISPER} <{Locale.CHAT_HELP_PLAYER_NAME}> <{Locale.CHAT_HELP_MSG}>" + + $"\r\n\t\t/{COMMAND_WHISPER_SHORT} <{Locale.CHAT_HELP_PLAYER_NAME}> <{Locale.CHAT_HELP_MSG}>", + WhisperMessage + ); + + RegisterChatCommand + ( + COMMAND_HELP, + COMMAND_HELP_SHORT, + ()=> $"\r\n\r\n\t{Locale.CHAT_HELP_HELP}" + + $"\r\n\t\t/{COMMAND_HELP}" + + $"\r\n\t\t/{COMMAND_HELP_SHORT}", + HelpMessage + ); + + RegisterChatCommand + ( + COMMAND_KICK, + null, + () => $"\r\n\r\n\tKick a player from the server (must be host)" + + $"\r\n\t\t/{COMMAND_KICK}", + KickMessage + ); - //Check we could find the sender player data - if (!NetworkLifecycle.Instance.Server.TryGetServerPlayer(sender, out var player)) - return; +#if DEBUG + RegisterChatCommand + ( + COMMAND_LOG, + COMMAND_LOG_SHORT, + null, + (args, sender) => + Multiplayer.specLog = !Multiplayer.specLog); +#endif + } + public bool RegisterChatCommand(string commandLong, string commandShort, Func helpMessage, Action callback) + { + if (string.IsNullOrEmpty(commandLong) || callback == null) + return false; - //Check if we have a command - if (message.StartsWith("/")) + if (_registeredCommands.ContainsKey(commandLong.ToLower()) || + (!string.IsNullOrEmpty(commandShort) && _registeredCommands.ContainsKey(commandShort.ToLower()))) + return false; + + _registeredCommands[commandLong.ToLower()] = callback; + + if (!string.IsNullOrEmpty(commandShort) && !_registeredCommands.ContainsKey(commandShort.ToLower())) { - string command = message.Substring(1).Split(' ')[0]; + _registeredCommands[commandShort.ToLower()] = callback; + } - 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; - - case COMMAND_KICK: - KickMessage(message, COMMAND_KICK.Length, player.Username, sender); - break; + if (helpMessage != null) + { + _registeredHelpMessages[commandLong.ToLower()] = helpMessage; + } -#if DEBUG - case COMMAND_LOG_SHORT: - Multiplayer.specLog = !Multiplayer.specLog; - break; - case COMMAND_LOG: - Multiplayer.specLog = !Multiplayer.specLog; - break; -#endif + return true; + } - //allow messages that are not commands to go through - default: - ChatMessage(message,player.Username, sender); - break; - } + public void RegisterChatFilter(Func callback) + { + if (callback != null) + { + _chatFilters.Add(callback); + } + } + + public void ProcessMessage(string message, IPlayer sender) + { + if (string.IsNullOrEmpty(message)) return; + //Check if we have a command + if (message.StartsWith("/")) + { + string[] messageParams = message.Substring(1).Split(' '); + string command = messageParams[0].ToLower(); + + //check registered commands + if (!string.IsNullOrEmpty(command) && _registeredCommands.TryGetValue(command, out var commandCallback)) + { + //remove the command, leading slash and trailing space + message = message.Substring(command.Length + 2); + commandCallback(message, sender); + + return; + } } //not a server command, process as normal message - ChatMessage(message, player.Username, sender); + ProcessChatMessage(message, sender); } - private static void ChatMessage(string message, string sender, ITransportPeer peer) + private void ProcessChatMessage(string message, IPlayer sender) { + if (sender is not ServerPlayer player) + return; + //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); + //call each filter until either a filter returns false or all filters have been called + foreach (var filter in _chatFilters) + { + if (!filter(message, sender)) + return; + } + + message = $"{sender.Username}: {message}"; + NetworkLifecycle.Instance.Server.SendChat(message, player.Peer); } - public static void ServerMessage(string message, ITransportPeer sender, ITransportPeer exclude = null, int commandLength =-1) + public void ServerMessage(string message, IPlayer sender, IPlayer exclude = null) { - //If user is not the host, we should ignore - will require changes for dedicated server - if (sender !=null && !NetworkLifecycle.Instance.IsHost(sender)) - return; + ServerPlayer senderPlayer = null; + ITransportPeer excludePeer = null; - //Remove the command "/server" or "/s" - if (commandLength > 0) + if (sender != null) { - message = message.Substring(commandLength + 2); + if(sender is not ServerPlayer sp) + return; + + senderPlayer = sp; + } + + if (exclude != null) + { + if (exclude is not ServerPlayer ep) + return; + excludePeer = ep.Peer; } + + //If user is not the host, we should ignore - will require changes for dedicated server + if (senderPlayer != null && !NetworkLifecycle.Instance.IsHost(senderPlayer.Peer)) + return; message = $"{message}"; - NetworkLifecycle.Instance.Server.SendChat(message, exclude); + NetworkLifecycle.Instance.Server.SendChat(message, excludePeer); } - private static void WhisperMessage(string message, int commandLength, string senderName, ITransportPeer sender) + private void WhisperMessage(string message, IPlayer sender) { - ITransportPeer recipient; - string recipientName; - - Multiplayer.Log($"Whispering: \"{message}\", sender: {senderName}, senderID: {sender?.Id}"); + Multiplayer.LogDebug(() => $"Whispering: \"{message}\", sender: {sender?.Username}, senderID: {sender?.Id}"); - //Remove the command "/whisper" or "/w" - message = message.Substring(commandLength + 2); + if (sender == null || sender is not ServerPlayer senderPlayer) + return; - if (message == null || message == string.Empty) + if (string.IsNullOrEmpty(message)) 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; + string[] parts = message.Split([' '], 2, StringSplitOptions.RemoveEmptyEntries); - recipientName = message.Substring(1, endQuote); + if (parts.Length < 2) + return; - //Remove the peer name - message = message.Substring(recipientName.Length + 3); - } - else - {*/ - recipientName = message.Split(' ')[0]; + string recipientName = parts[0]; + string whisperMessage = parts[1]; - //Remove the peer name - message = message.Substring(recipientName.Length + 1); - //} - Multiplayer.Log($"Whispering parse 1: \"{message}\", sender: {senderName}, senderID: {sender?.Id}, peerName: {recipientName}"); + Multiplayer.LogDebug(()=>$"Whispering parse 1: \"{message}\", sender: {sender?.Username}, senderID: {sender?.Id}, peerName: {recipientName}"); //look up the peer ID - recipient = NetPeerFromName(recipientName); + ITransportPeer recipient = NetPeerFromName(recipientName); if(recipient == null) { - Multiplayer.Log($"Whispering failed: \"{message}\", sender: {senderName}, senderID: {sender?.Id}, peerName: {recipientName}"); + Multiplayer.LogDebug(() => $"Whispering failed: \"{message}\", sender: {sender?.Username}, senderID: {sender?.Id}, peerName: {recipientName}"); - message = $"{recipientName} not found - you're whispering into the void!"; - NetworkLifecycle.Instance.Server.SendWhisper(message, sender); + whisperMessage = $"{recipientName} not found - you're whispering into the void!"; //todo: add translation + NetworkLifecycle.Instance.Server.SendWhisper(whisperMessage, senderPlayer.Peer); return; } - Multiplayer.Log($"Whispering parse 2: \"{message}\", sender: {senderName}, senderID: {sender?.Id}, peerName: {recipientName}, peerID: {recipient?.Id}"); + Multiplayer.LogDebug(() => $"Whispering parse 2: \"{message}\", sender: {sender?.Username}, senderID: {sender?.Id}, peerName: {recipientName}, peerID: {recipient?.Id}"); //clean up the message to stop format injection - message = Regex.Replace(message, "", string.Empty, RegexOptions.IgnoreCase); + whisperMessage = Regex.Replace(whisperMessage, "", string.Empty, RegexOptions.IgnoreCase); - message = "" + senderName + ": " + message + ""; + whisperMessage = "" + sender.Username + ": " + whisperMessage + ""; - NetworkLifecycle.Instance.Server.SendWhisper(message, recipient); + NetworkLifecycle.Instance.Server.SendWhisper(whisperMessage, recipient); } - public static void KickMessage(string message, int commandLength, string senderName, ITransportPeer sender) + public void KickMessage(string message, IPlayer sender) { ITransportPeer 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)) + if (sender == null || sender is not ServerPlayer senderPlayer || !NetworkLifecycle.Instance.IsHost(senderPlayer.Peer)) return; - //Remove the command "/server" or "/s" - if (commandLength > 0) - { - message = message.Substring(commandLength + 2); - } - playerName = message.Split(' ')[0]; + if (string.IsNullOrEmpty(playerName)) + return; player = NetPeerFromName(playerName); if (player == null || NetworkLifecycle.Instance.IsHost(player)) { - message = $"Unable to kick {playerName}"; + message = $"Unable to kick {playerName}"; //todo: translate } else { - message = $"{playerName} was kicked"; + message = $"{playerName} was kicked"; //todo: translate NetworkLifecycle.Instance.Server.KickPlayer(player); } - NetworkLifecycle.Instance.Server.SendWhisper(message, sender); + NetworkLifecycle.Instance.Server.SendWhisper(message, senderPlayer.Peer); } - private static void HelpMessage(ITransportPeer peer) + private void HelpMessage(string _, IPlayer peer) { - 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}>" + + if (peer == null || peer is not ServerPlayer player) + return; - $"\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}>" + + StringBuilder sb = new($"{Locale.CHAT_HELP_AVAILABLE}"); - $"\r\n\r\n\t{Locale.CHAT_HELP_HELP}" + - "\r\n\t\t/help" + - "\r\n\t\t/?" + + foreach (var helpMessage in _registeredHelpMessages) + sb.AppendLine(helpMessage.Value?.Invoke()); - ""; + sb.AppendLine(""); /* * $"Available commands:" + @@ -238,11 +292,11 @@ private static void HelpMessage(ITransportPeer peer) ""; */ - NetworkLifecycle.Instance.Server.SendWhisper(message, peer); + NetworkLifecycle.Instance.Server.SendWhisper(sb.ToString(), player.Peer); } - private static ITransportPeer NetPeerFromName(string peerName) + private ITransportPeer NetPeerFromName(string peerName) { if(peerName == null || peerName == string.Empty) diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index a0b2d224..09c317a7 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -8,6 +8,7 @@ using Humanizer; using LiteNetLib; using LiteNetLib.Utils; +using MPAPI.Interfaces; using MPAPI.Interfaces.Packets; using Multiplayer.API; using Multiplayer.Components.Networking; @@ -35,8 +36,6 @@ using System.Text; using UnityEngine; using UnityModManagerNet; -using static DV.Interaction.Inputs.InputManager; - namespace Multiplayer.Networking.Managers.Server; @@ -68,6 +67,9 @@ public class NetworkServer : NetworkManager public readonly IDifficulty Difficulty; private bool IsLoaded; + private readonly ChatManager _chatManager; + public ChatManager ChatManager => _chatManager; + //we don't care if the client doesn't have these mods public static string[] modWhiteList = ["RuntimeUnityEditor", "BookletOrganizer", "RemoteDispatch"]; @@ -832,7 +834,7 @@ private void OnServerboundClientReadyPacket(ServerboundClientReadyPacket packet, }; SendPacketToAll(clientboundPlayerJoinedPacket, DeliveryMethod.ReliableOrdered, peer); - ChatManager.ServerMessage(serverPlayer.Username + " joined the game", null, peer); + ChatManager.ServerMessage(serverPlayer.Username + " joined the game", null, (IPlayer) serverPlayer); Log($"Client {peer.Id} is ready. Sending world state"); @@ -1258,7 +1260,8 @@ private void OnServerboundJobValidateRequestPacket(ServerboundJobValidateRequest private void OnCommonChatPacket(CommonChatPacket packet, ITransportPeer peer) { - ChatManager.ProcessMessage(packet.message, peer); + if (TryGetServerPlayer(peer, out ServerPlayer player)) + ChatManager.ProcessMessage(packet.message, (IPlayer)player); } #endregion diff --git a/MultiplayerAPI/Interfaces/IServer.cs b/MultiplayerAPI/Interfaces/IServer.cs index c76eb1c1..09bddc8f 100644 --- a/MultiplayerAPI/Interfaces/IServer.cs +++ b/MultiplayerAPI/Interfaces/IServer.cs @@ -12,8 +12,17 @@ public interface IServer event Action OnPlayerDisconnected; #region Server Properties + + /// + /// Gets number of players currently connected to the server + /// + /// Positive integer representing the number of connected players int PlayerCount { get; } + /// + /// Gets IPlayer objects for all players connected to the server + /// + /// Read-only collection of IPlayer objects public IReadOnlyCollection Players { get; } #endregion @@ -76,7 +85,7 @@ public interface IServer /// Returns the distance (Square Magnitude) of the closest player to a given GameObject /// /// GameObject to compare players against - // Returns the distance (Square Magnitude) of the closest player, or float.MaxValue if no player is nearby + /// Returns the distance (Square Magnitude) of the closest player, or float.MaxValue if no player is nearby float AnyPlayerSqrMag(GameObject gameObject); /// @@ -101,9 +110,9 @@ public interface IServer /// Command to be filtered for, without a leading '/' e.g. 'server' /// Optional short command to be filtered for, without a leading '/' e.g. 's' /// Optional callback for a help message e.g. "Send a message as the server (host only)\r\n\t\t/server \r\n\t\t/s " It is recommended to provide localisation/translation for this string - /// Action to execute when the command is triggered. First parameter contains command arguments as string array, second parameter is the player who executed the command. + /// Action to execute when the command is triggered. First parameter contains message without the command e.g. '/command parameter1 parameter2' will become 'parameter1 parameter2', second parameter is the player who executed the command. /// True if the command was successfully registered, false if registration failed (e.g. command already exists). - bool RegisterChatCommand(string commandLong, string commandShort, Func helpMessage, Action callback); + bool RegisterChatCommand(string commandLong, string commandShort, Func helpMessage, Action callback); /// From 718c908386e0dc8ec65761536c271328f0c7fb68 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 8 Jun 2025 14:10:07 +1000 Subject: [PATCH 365/521] Fix packet registration error --- .../Managers/Client/NetworkClient.cs | 33 ++++++++--------- .../Managers/Server/NetworkServer.cs | 35 +++++++++---------- 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 3f6127a5..97900a20 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -1,6 +1,5 @@ -using System; -using System.Collections.Generic; -using DV; +using DV.Common; +using DV.Customization.Paint; using DV.Damage; using DV.InventorySystem; using DV.Logic.Job; @@ -8,39 +7,41 @@ using DV.ServicePenalty.UI; using DV.ThingTypes; using DV.UI; +using DV.UserManagement; using DV.WeatherSystem; +using DV; +using LiteNetLib.Utils; using LiteNetLib; 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.Networking; using Multiplayer.Components.SaveGame; +using Multiplayer.Networking.Data.Train; 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; -using Multiplayer.Networking.Packets.Common; +using Multiplayer.Networking.Packets.Clientbound; using Multiplayer.Networking.Packets.Common.Train; +using Multiplayer.Networking.Packets.Common; +using Multiplayer.Networking.Packets.Serverbound.Train; using Multiplayer.Networking.Packets.Serverbound; -using Multiplayer.Networking.Data.Train; +using Multiplayer.Networking.TransportLayers; using Multiplayer.Patches.SaveGame; using Multiplayer.Utils; using Newtonsoft.Json.Linq; -using UnityEngine; -using UnityModManagerNet; using Object = UnityEngine.Object; -using Multiplayer.Networking.Packets.Serverbound.Train; +using System.Collections.Generic; using System.Linq; -using LiteNetLib.Utils; -using DV.UserManagement; -using DV.Common; -using DV.Customization.Paint; -using Multiplayer.Networking.TransportLayers; +using System; +using UnityEngine; +using UnityModManagerNet; + namespace Multiplayer.Networking.Managers.Client; @@ -175,7 +176,7 @@ protected override void Subscribe() netPacketProcessor.SubscribeNetSerializable(OnCommonItemChangePacket); netPacketProcessor.SubscribeReusable(OnCommonPitStopInteractionPacket); - netPacketProcessor.SubscribeReusable(OnCommonPitStopPlugInteractionPacket); + netPacketProcessor.SubscribeNetSerializable(OnCommonPitStopPlugInteractionPacket); netPacketProcessor.SubscribeReusable(OnClientboundPitStopBulkUpdatePacket); netPacketProcessor.SubscribeReusable(OnCommonCashRegisterWithModulesActionPacket); diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index ee0dbc0a..4f182980 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -1,39 +1,38 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using DV; using DV.InventorySystem; using DV.Logic.Job; using DV.Scenarios.Common; using DV.ServicePenalty; using DV.ThingTypes; using DV.WeatherSystem; +using DV; using Humanizer; -using LiteNetLib; using LiteNetLib.Utils; -using Multiplayer.Components.Networking; +using LiteNetLib; +using Multiplayer.Components.Networking.Jobs; using Multiplayer.Components.Networking.Train; using Multiplayer.Components.Networking.World; -using Multiplayer.Components.Networking.Jobs; +using Multiplayer.Components.Networking; +using Multiplayer.Networking.Data.Train; 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; -using Multiplayer.Networking.Packets.Common; +using Multiplayer.Networking.Packets.Clientbound; using Multiplayer.Networking.Packets.Common.Train; +using Multiplayer.Networking.Packets.Common; +using Multiplayer.Networking.Packets.Serverbound.Train; using Multiplayer.Networking.Packets.Serverbound; +using Multiplayer.Networking.Packets.Unconnected; +using Multiplayer.Networking.TransportLayers; using Multiplayer.Utils; -using UnityEngine; -using UnityModManagerNet; +using System.Collections.Generic; +using System.Linq; using System.Net; -using Multiplayer.Networking.Packets.Serverbound.Train; -using Multiplayer.Networking.Packets.Unconnected; using System.Text; -using Multiplayer.Networking.Data.Train; -using Multiplayer.Networking.TransportLayers; - +using System; +using UnityEngine; +using UnityModManagerNet; namespace Multiplayer.Networking.Managers.Server; @@ -136,7 +135,7 @@ protected override void Subscribe() netPacketProcessor.SubscribeReusable(OnCommonChangeJunctionPacket); netPacketProcessor.SubscribeReusable(OnCommonRotateTurntablePacket); netPacketProcessor.SubscribeReusable(OnCommonCouplerInteractionPacket); - //netPacketProcessor.SubscribeReusable(OnCommonTrainCouplePacket); + netPacketProcessor.SubscribeReusable(OnCommonTrainUncouplePacket); netPacketProcessor.SubscribeReusable(OnCommonHoseConnectedPacket); netPacketProcessor.SubscribeReusable(OnCommonHoseDisconnectedPacket); @@ -156,7 +155,7 @@ protected override void Subscribe() netPacketProcessor.SubscribeNetSerializable(OnCommonItemChangePacket); netPacketProcessor.SubscribeReusable(OnCommonPitStopInteractionPacket); - netPacketProcessor.SubscribeReusable(OnCommonPitStopPlugInteractionPacket); + netPacketProcessor.SubscribeNetSerializable(OnCommonPitStopPlugInteractionPacket); netPacketProcessor.SubscribeReusable(OnCommonCashRegisterWithModulesActionPacket); } From 611d19ca44208c3c1e3af8d442c66fdf4917054d Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 8 Jun 2025 14:07:08 +1000 Subject: [PATCH 366/521] Fix issue with PluggableObjects not syncing correctly on load PluggableObjects automatically return home at `Awake()` - patch this behaviour and prevent the bulk update from applying until the initialisation is complete --- .../World/NetworkedPluggableObject.cs | 23 ++++++++++++++----- .../Patches/World/PluggableObjectPatch.cs | 20 ++++++++++++++++ 2 files changed, 37 insertions(+), 6 deletions(-) create mode 100644 Multiplayer/Patches/World/PluggableObjectPatch.cs diff --git a/Multiplayer/Components/Networking/World/NetworkedPluggableObject.cs b/Multiplayer/Components/Networking/World/NetworkedPluggableObject.cs index 26a04fa8..8e17ff33 100644 --- a/Multiplayer/Components/Networking/World/NetworkedPluggableObject.cs +++ b/Multiplayer/Components/Networking/World/NetworkedPluggableObject.cs @@ -5,10 +5,8 @@ using Multiplayer.Networking.Data; using Multiplayer.Networking.Packets.Common; using Multiplayer.Utils; -using System; using System.Collections; using System.Collections.Generic; -using System.Linq; using UnityEngine; namespace Multiplayer.Components.Networking.World; @@ -53,7 +51,7 @@ protected override void Awake() base.Awake(); PluggableObject = GetComponent(); - Multiplayer.LogDebug(() => $"NetworkedPluggableObject.Awake() {PluggableObject?.controlBase?.spec?.name}, {transform.parent.name}"); + //Multiplayer.LogDebug(() => $"NetworkedPluggableObject.Awake() {PluggableObject?.controlBase?.spec?.name}, {transform.parent.name}"); Multiplayer.LogDebug(() => $"NetworkedPluggableObject.Awake() {this.GetObjectPath()}, netId: {NetId}"); if (NetworkLifecycle.Instance.IsHost()) @@ -62,10 +60,10 @@ protected override void Awake() protected IEnumerator Start() { - Multiplayer.LogDebug(() => $"NetworkedPluggableObject.Start() {PluggableObject?.controlBase?.spec?.name}, {transform.parent.name}"); + //Multiplayer.LogDebug(() => $"NetworkedPluggableObject.Start() {PluggableObject?.controlBase?.spec?.name}, {transform.parent.name}"); yield return new WaitUntil(() => PluggableObject?.controlBase != null); - Multiplayer.LogDebug(() => $"NetworkedPluggableObject.Start() Controlbase {PluggableObject?.controlBase?.spec?.name}, {transform.parent.name}"); + //Multiplayer.LogDebug(() => $"NetworkedPluggableObject.Start() Controlbase {PluggableObject?.controlBase?.spec?.name}, {transform.parent.name}"); grabHandler = this.GetComponent(); @@ -76,6 +74,11 @@ protected IEnumerator Start() handlersInitialised = true; } + protected void OnDisable() + { + Refreshed = false; + } + protected override void OnDestroy() { if (UnloadWatcher.isUnloading) @@ -142,6 +145,13 @@ public void ProcessPacket(CommonPitStopPlugInteractionPacket packet) public void ProcessBulkUpdate(PitStopPlugData data) { + CoroutineManager.Instance.StartCoroutine(WaitForInit(data)); + } + + private IEnumerator WaitForInit(PitStopPlugData data) + { + yield return new WaitUntil(()=> PluggableObject != null && PluggableObject.initialized); + var interaction = data.State; ProcessInteraction(interaction, data.PlayerId, data.TrainCarNetId, data.IsLeftSide, data.Position, data.Rotation); Refreshed = true; @@ -149,7 +159,6 @@ public void ProcessBulkUpdate(PitStopPlugData data) public void ProcessInteraction(PlugInteractionType interaction, byte playerId, ushort trainNetId, bool isLeftSide, Vector3? newPosition, Quaternion? newRotation) { - bool result; NetworkedPlayer player = null; @@ -279,8 +288,10 @@ private void BlockInteraction(bool block) PluggableObject.DisableColliders(); } else + { PluggableObject.EnableStandaloneComponents(); PluggableObject.EnableColliders(); + } } public void InitPitStop(NetworkedPitStopStation netPitStop) diff --git a/Multiplayer/Patches/World/PluggableObjectPatch.cs b/Multiplayer/Patches/World/PluggableObjectPatch.cs new file mode 100644 index 00000000..1e654107 --- /dev/null +++ b/Multiplayer/Patches/World/PluggableObjectPatch.cs @@ -0,0 +1,20 @@ +using HarmonyLib; +using Multiplayer.Components.Networking; + +namespace Multiplayer.Patches.World; + +[HarmonyPatch(typeof(PluggableObject))] +public static class PluggableObjectPatch +{ + [HarmonyPatch(nameof(PluggableObject.Awake))] + [HarmonyPrefix] + public static bool Awake(PluggableObject __instance) + { + if (NetworkLifecycle.Instance.IsHost()) + return true; + + // Allow the client to setup the plug, but don't allow `InstantSnapTo(this.startAttachedTo);` to be called + __instance.CheckInitialization(); + return false; + } +} From 567337513ca5aa575bf7dd21af8ddfa886664a57 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 8 Jun 2025 14:23:35 +1000 Subject: [PATCH 367/521] Rework NetworkedPitStopStation sync system --- .../World/NetworkedPitStopStation.cs | 143 ++++++++++-------- 1 file changed, 82 insertions(+), 61 deletions(-) diff --git a/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs b/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs index a3b2e2d1..3c83d02a 100644 --- a/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs +++ b/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs @@ -99,6 +99,7 @@ public static void InitialisePitStops() private readonly Dictionary leverHandler)> leverStateLookup = []; private readonly Dictionary grabbedHandlerLookup = []; private readonly Dictionary resourceToPluggableObject = []; + private readonly Dictionary resourceToModule = []; private readonly Dictionary isResourceGrabbedDict = []; private readonly Dictionary wasResourceGrabbedDict = []; @@ -226,7 +227,7 @@ protected void LateUpdate() NetId, PitStopStationInteractionType.ResourceUpdate, resourceType, - lastUnitsToBuy + lastUnitsToBuyDict[resourceType] ); } } @@ -241,7 +242,7 @@ protected void LateUpdate() NetId, PitStopStationInteractionType.ResourceUpdate, resourceType, - lastUnitsToBuy + lastUnitsToBuyDict[resourceType] ); //Reset grab states @@ -252,7 +253,7 @@ protected void LateUpdate() if (!isResourceRemoteGrabbed && wasResourceRemoteGrabbed) { float previous = module.Data.unitsToBuy; - + SetUnits(module, lastRemoteValue); Multiplayer.LogDebug(() => $"NetworkedPitStopStation.LateUpdate() wasRemoteGrabbed: {wasResourceRemoteGrabbed}, previous: {previous}, new: {lastRemoteValue}"); @@ -484,7 +485,7 @@ private IEnumerator Init() Multiplayer.LogWarning($"No resource modules found for station {StationName}"); yield break; } - + resourceTypes = resourceModules?.Select(m => m.resourceType).ToArray(); foreach (var resourceType in resourceTypes) @@ -494,7 +495,6 @@ private IEnumerator Init() isResourceRemoteGrabbedDict[resourceType] = false; wasResourceRemoteGrabbedDict[resourceType] = false; lastRemoteValueDict[resourceType] = 0.0f; - //grabbedAmplitudeChecker[resourceType] = null; lastUnitsToBuyDict[resourceType] = 0.0f; } @@ -507,11 +507,13 @@ private IEnumerator Init() { yield return new WaitUntil(() => resourceModule.initialized); + resourceToModule[resourceModule.resourceType] = resourceModule; + var checker = resourceModule.GetComponentInChildren(); var grab = resourceModule.GetComponentInChildren(); if (checker != null && grab != null) { - + //Delegates for handlers void LeverStatehandler(int state) => OnLeverPositionChange(resourceModule, state); @@ -781,7 +783,7 @@ public void ProcessBulkUpdate(ClientboundPitStopBulkUpdatePacket packet) if (module.resourceData.Count == resource.Values.Count()) for (int i = 0; i < module.resourceData.Count; i++) module.SetUnitsToBuy(resource.Values[i]); - //module.resourceData[i].unitsToBuy = resource.Values[i]; + //module.resourceData[i].unitsToBuy = resource.Values[i]; else Multiplayer.LogWarning($"PitStop bulk data count mismatch post-force: {module.resourceData.Count} != {resource.Values.Count()}"); else @@ -809,6 +811,10 @@ public void ProcessBulkUpdate(ClientboundPitStopBulkUpdatePacket packet) /// The packet containing interaction data. public void ProcessInteractionPacketAsClient(CommonPitStopInteractionPacket packet) { + GrabHandlerHingeJoint grab = null; + RotaryAmplitudeChecker amplitudeChecker = null; + + // Validate interaction type if (!Enum.IsDefined(typeof(PitStopStationInteractionType), packet.InteractionType)) { Multiplayer.LogWarning($"Invalid interaction type: {packet.InteractionType} in ProcessInteractionPacketAsClient()"); @@ -817,39 +823,22 @@ public void ProcessInteractionPacketAsClient(CommonPitStopInteractionPacket pack PitStopStationInteractionType interactionType = (PitStopStationInteractionType)packet.InteractionType; - bool resourceValid = Enum.IsDefined(typeof(ResourceType), packet.ResourceType); - - ResourceType resourceType = resourceValid ? (ResourceType)packet.ResourceType : ResourceType.Fuel; + // Validate resource type + if (!Enum.IsDefined(typeof(ResourceType), packet.ResourceType)) + { + Multiplayer.LogWarning($"Received invalid ResourceType \"{packet.ResourceType}\" at Pit Stop station {StationName}"); + return; + } - GrabHandlerHingeJoint grab = null; - LocoResourceModule resourceModule = null; - RotaryAmplitudeChecker amplitudeChecker = null; + ResourceType resourceType = (ResourceType)packet.ResourceType; Multiplayer.LogDebug(() => $"NetworkedPitStopStation.ProcessPacket() [{StationName}, {NetId}] {interactionType}, resource type: {resourceType}, state: {packet.Value}"); - if (resourceValid) + // Validate resource module exists + if (!resourceToModule.TryGetValue(resourceType, out LocoResourceModule resourceModule)) { - if (!grabbedHandlerLookup.TryGetValue(resourceType, out grab)) - { - Multiplayer.LogError($"Could not find ResourceType in grabbedHandlerLookup for Pit Stop station {StationName}, resource type: {resourceType}"); - } - else - { - if (!leverStateLookup.TryGetValue(resourceType, out var tup)) - { - Multiplayer.LogError($"Could not find Rotary Amplitude Handler in rotaryAmplitudeLookup for Pit Stop station {StationName}, resource type: {resourceType}"); - } - else - { - (amplitudeChecker, resourceModule, _) = tup; - - if (packet.Value < resourceModule.AbsoluteMinValue || packet.Value > resourceModule.AbsoluteMaxValue) - { - Multiplayer.LogError($"Invalid Pit Stop state value: {packet.Value} for resource {resourceModule.resourceType}"); - return; - } - } - } + Multiplayer.LogWarning($"Could not find LocoResourceModule for ResourceType \"{resourceType}\" at Pit Stop station {StationName}"); + return; } switch (interactionType) @@ -860,51 +849,83 @@ public void ProcessInteractionPacketAsClient(CommonPitStopInteractionPacket pack case PitStopStationInteractionType.LeverState: - bool grabbed = (packet.Value != 0); + if (!grabbedHandlerLookup.TryGetValue(resourceType, out grab)) + { + Multiplayer.LogError($"Could not find ResourceType in grabbedHandlerLookup for Pit Stop station {StationName}, resource type: {resourceType}"); + return; + } + else + { + if (!leverStateLookup.TryGetValue(resourceType, out var tup)) + { + Multiplayer.LogError($"Could not find Rotary Amplitude Handler in rotaryAmplitudeLookup for Pit Stop station {StationName}, resource type: {resourceType}"); + return; + } + else + { + (amplitudeChecker, resourceModule, _) = tup; - Multiplayer.LogDebug(() => $"NetworkedPitStopStation.ProcessPacket() [{StationName}, {NetId}] {interactionType}, resource type: {resourceType}, state: {packet.Value}, grabbed: {grabbed}, resourceValid: {resourceValid}, resourceModule: {resourceModule != null}, isResourceRemoteGrabbed: {isResourceRemoteGrabbedDict[resourceType]}, wasResourceRemoteGrabbed: {wasResourceRemoteGrabbedDict[resourceType]}"); + if (packet.Value < RotaryAmplitudeChecker.MIN_REACHED || packet.Value > RotaryAmplitudeChecker.MAX_REACHED) + { + Multiplayer.LogError($"Invalid lever value ({packet.Value}) received for Pit Stop station {StationName}, resource type: {resourceType}"); + return; + } + } + } - // Set interaction state - disable when grabbed, enable when released - grab?.SetMovingDisabled(grabbed); + bool grabbed = (packet.Value != 0); + bool isLocallyGrabbed = isResourceGrabbedDict.TryGetValue(resourceType, out var localGrabbed) && localGrabbed; - if (grabbed) - grab?.ForceEndInteraction(); + if (!isLocallyGrabbed) + { + grab?.SetMovingDisabled(grabbed); + if (grabbed) + grab?.ForceEndInteraction(); + } + + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.ProcessPacket() [{StationName}, {NetId}] {interactionType}, resource type: {resourceType}, state: {packet.Value}, grabbed: {grabbed}, resourceModule: {resourceModule != null}, isResourceRemoteGrabbed: {isResourceRemoteGrabbedDict[resourceType]}, wasResourceRemoteGrabbed: {wasResourceRemoteGrabbedDict[resourceType]}"); resourceModule.OnValvePositionChange((int)packet.Value); // Update remote grab state - if (resourceValid && resourceModule != null) - { - isResourceRemoteGrabbedDict[resourceType] = grabbed; - if (grabbed) - wasResourceRemoteGrabbedDict[resourceType] = true; - } + isResourceRemoteGrabbedDict[resourceType] = grabbed; + if (grabbed) + wasResourceRemoteGrabbedDict[resourceType] = true; break; case PitStopStationInteractionType.ResourceUpdate: - Multiplayer.LogDebug(() => $"NetworkedPitStopStation.ProcessPacket() [{StationName}, {NetId}] {interactionType}, resource type: {resourceType}, state: {packet.Value}, resourceValid: {resourceValid}, resourceModule: {resourceModule != null}, isResourceRemoteGrabbed: {isResourceRemoteGrabbedDict[resourceType]}, wasResourceRemoteGrabbed: {wasResourceRemoteGrabbedDict[resourceType]}"); - if (resourceValid && resourceModule != null) + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.ProcessPacket() [{StationName}, {NetId}] {interactionType}, resource type: {resourceType}, state: {packet.Value}, resourceModule: {resourceModule != null}, isResourceRemoteGrabbed: {isResourceRemoteGrabbedDict[resourceType]}, wasResourceRemoteGrabbed: {wasResourceRemoteGrabbedDict[resourceType]}"); + + // Validate the value range + if (packet.Value < resourceModule.AbsoluteMinValue || packet.Value > resourceModule.AbsoluteMaxValue) { - if (isResourceRemoteGrabbedDict[resourceType] || wasResourceRemoteGrabbedDict[resourceType]) - { - lastRemoteValueDict[resourceType] = packet.Value; - SetUnits(resourceModule, lastRemoteValueDict[resourceType]); - Multiplayer.LogDebug(() => $"NetworkedPitStopStation.ProcessPacket() [{StationName}, {NetId}] {interactionType}, resource type: {resourceType}, state: {packet.Value}. SetUnits()"); - } + Multiplayer.LogError($"Invalid Pit Stop state value: {packet.Value} for resource {resourceModule.resourceType}"); + return; } - break; + if (isResourceRemoteGrabbedDict[resourceType]) + { + lastRemoteValueDict[resourceType] = packet.Value; + SetUnits(resourceModule, lastRemoteValueDict[resourceType]); + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.ProcessPacket() [{StationName}, {NetId}] {interactionType}, resource type: {resourceType}, state: {packet.Value}. SetUnits()"); + } + else if (wasResourceRemoteGrabbedDict[resourceType]) + { + lastRemoteValueDict[resourceType] = packet.Value; + } + + break; case PitStopStationInteractionType.CarSelectorGrab: //block interaction - carSelectorGrab?.SetMovingDisabled(false); + carSelectorGrab?.SetMovingDisabled(true); break; case PitStopStationInteractionType.CarSelectorUngrab: //allow interaction - carSelectorGrab?.SetMovingDisabled(true); + carSelectorGrab?.SetMovingDisabled(false); SetCarSelection((int)packet.Value); break; @@ -914,12 +935,12 @@ public void ProcessInteractionPacketAsClient(CommonPitStopInteractionPacket pack case PitStopStationInteractionType.FaucetGrab: //block interaction - faucetPositionerGrab?.SetMovingDisabled(false); + faucetPositionerGrab?.SetMovingDisabled(true); break; case PitStopStationInteractionType.FaucetUngrab: //allow interaction - faucetPositionerGrab?.SetMovingDisabled(true); + faucetPositionerGrab?.SetMovingDisabled(false); if (packet.Value >= -1 && packet.Value <= 1) { @@ -934,7 +955,7 @@ public void ProcessInteractionPacketAsClient(CommonPitStopInteractionPacket pack case PitStopStationInteractionType.FaucetPosition: if (packet.Value >= -1 && packet.Value <= 1) { - if (faucetPositioner.Percentage != packet.Value) + if (faucetPositioner != null && faucetPositioner.Percentage != packet.Value) { faucetTargetPercentage = packet.Value; faucetTargetReached = false; From 840b51e15d427cc82206777dbb050e3d8117f6de Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 8 Jun 2025 14:55:24 +1000 Subject: [PATCH 368/521] Fix ProcessBulkUpdate() not loading data correctly --- .../Networking/World/NetworkedPitStopStation.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs b/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs index 3c83d02a..6815ef01 100644 --- a/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs +++ b/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs @@ -99,7 +99,7 @@ public static void InitialisePitStops() private readonly Dictionary leverHandler)> leverStateLookup = []; private readonly Dictionary grabbedHandlerLookup = []; private readonly Dictionary resourceToPluggableObject = []; - private readonly Dictionary resourceToModule = []; + private readonly Dictionary resourceTypeToLocoResourceModule = []; private readonly Dictionary isResourceGrabbedDict = []; private readonly Dictionary wasResourceGrabbedDict = []; @@ -507,7 +507,7 @@ private IEnumerator Init() { yield return new WaitUntil(() => resourceModule.initialized); - resourceToModule[resourceModule.resourceType] = resourceModule; + resourceTypeToLocoResourceModule[resourceModule.resourceType] = resourceModule; var checker = resourceModule.GetComponentInChildren(); var grab = resourceModule.GetComponentInChildren(); @@ -777,13 +777,13 @@ public void ProcessBulkUpdate(ClientboundPitStopBulkUpdatePacket packet) // Load the data for each car and resource module foreach (var resource in packet.ResourceData) { - var module = Station.locoResourceModules.resourceModules.FirstOrDefault(lm => lm.resourceType == resource.ResourceType); + if (!resourceTypeToLocoResourceModule.TryGetValue(resource.ResourceType, out var module)) + continue; if (module != null) if (module.resourceData.Count == resource.Values.Count()) for (int i = 0; i < module.resourceData.Count; i++) - module.SetUnitsToBuy(resource.Values[i]); - //module.resourceData[i].unitsToBuy = resource.Values[i]; + module.resourceData[i].unitsToBuy = resource.Values[i]; else Multiplayer.LogWarning($"PitStop bulk data count mismatch post-force: {module.resourceData.Count} != {resource.Values.Count()}"); else @@ -796,7 +796,7 @@ public void ProcessBulkUpdate(ClientboundPitStopBulkUpdatePacket packet) foreach (var plug in packet.PlugData) { NetworkedPluggableObject.Get(plug.NetId, out var netPlug); - netPlug.ProcessBulkUpdate(plug); + netPlug?.ProcessBulkUpdate(plug); } // Mark data as refreshed to allow player interactions @@ -835,7 +835,7 @@ public void ProcessInteractionPacketAsClient(CommonPitStopInteractionPacket pack Multiplayer.LogDebug(() => $"NetworkedPitStopStation.ProcessPacket() [{StationName}, {NetId}] {interactionType}, resource type: {resourceType}, state: {packet.Value}"); // Validate resource module exists - if (!resourceToModule.TryGetValue(resourceType, out LocoResourceModule resourceModule)) + if (!resourceTypeToLocoResourceModule.TryGetValue(resourceType, out LocoResourceModule resourceModule)) { Multiplayer.LogWarning($"Could not find LocoResourceModule for ResourceType \"{resourceType}\" at Pit Stop station {StationName}"); return; From a28863024f93a95d2f3cdbe7b45abefeb5730d7b Mon Sep 17 00:00:00 2001 From: AMacro Date: Thu, 12 Jun 2025 17:00:35 +1000 Subject: [PATCH 369/521] Remove LiteNetLib reference from Asset Bundle script --- MultiplayerAssets/Assets/Scripts/Multiplayer/Exporter.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/MultiplayerAssets/Assets/Scripts/Multiplayer/Exporter.cs b/MultiplayerAssets/Assets/Scripts/Multiplayer/Exporter.cs index ebcff252..e259f357 100644 --- a/MultiplayerAssets/Assets/Scripts/Multiplayer/Exporter.cs +++ b/MultiplayerAssets/Assets/Scripts/Multiplayer/Exporter.cs @@ -14,7 +14,6 @@ public static class Exporter private const string DEST_DIR = "../build"; private const string ASSET_BUNDLE_DEST_NAME = ASSET_BUNDLE_NAME + ".assetbundle"; private static readonly string[] COPY_DLLS = { - "LiteNetLib.dll", "MultiplayerEditor.dll", "UnityChan.dll" }; From de0e0d3420953b918d512c9f4b3bb8b95e8a8ada Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 14 Jun 2025 19:13:51 +1000 Subject: [PATCH 370/521] Rework NetIdProvider workflow --- Multiplayer/API/APIProvider.cs | 4 ---- Multiplayer/API/NetIdProvider.cs | 2 +- Multiplayer/Networking/Managers/NetworkManager.cs | 10 +++++++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Multiplayer/API/APIProvider.cs b/Multiplayer/API/APIProvider.cs index 13a9a166..e170522b 100644 --- a/Multiplayer/API/APIProvider.cs +++ b/Multiplayer/API/APIProvider.cs @@ -6,10 +6,6 @@ namespace Multiplayer.API { public class APIProvider : IMultiplayerAPI { - public APIProvider() - { - NetIdProvider.Instance.CheckInitialization(); - } public bool IsMultiplayerLoaded => true; diff --git a/Multiplayer/API/NetIdProvider.cs b/Multiplayer/API/NetIdProvider.cs index b2d5e1d1..715c8928 100644 --- a/Multiplayer/API/NetIdProvider.cs +++ b/Multiplayer/API/NetIdProvider.cs @@ -68,7 +68,7 @@ public bool TryGetObject(ushort netId, out T obj) where T : class } [UsedImplicitly] - protected new static string AllowAutoCreate() + public new static string AllowAutoCreate() { return $"[{nameof(NetIdProvider)}]"; } diff --git a/Multiplayer/Networking/Managers/NetworkManager.cs b/Multiplayer/Networking/Managers/NetworkManager.cs index 68d06194..8d79e0e4 100644 --- a/Multiplayer/Networking/Managers/NetworkManager.cs +++ b/Multiplayer/Networking/Managers/NetworkManager.cs @@ -1,12 +1,13 @@ -using System; -using System.Net; -using System.Net.Sockets; using LiteNetLib; using LiteNetLib.Utils; +using Multiplayer.API; using Multiplayer.Networking.Data; using Multiplayer.Networking.Data.Train; using Multiplayer.Networking.Serialization; using Multiplayer.Networking.TransportLayers; +using System; +using System.Net; +using System.Net.Sockets; namespace Multiplayer.Networking.Managers; @@ -76,6 +77,7 @@ public void PollEvents() public virtual bool Start() { + NetIdProvider.Instance.CheckInitialization(); return transport.Start(); } public virtual bool Start(IPAddress ipv4, IPAddress ipv6, int port) @@ -105,6 +107,8 @@ public virtual void Stop() transport.OnNetworkLatencyUpdate -= OnNetworkLatencyUpdate; Settings.OnSettingsUpdated -= OnSettingsUpdated; + + NetIdProvider.Destroy(NetIdProvider.Instance); } protected NetDataWriter WritePacket(T packet) where T : class, new() From 5ce5794a81c3eb295545c82283bee29fcc810a02 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 14 Jun 2025 19:15:33 +1000 Subject: [PATCH 371/521] Rework xPlayerWrappers --- Multiplayer/API/ClientPlayerWrapper.cs | 1 - Multiplayer/API/ServerPlayerWrapper.cs | 5 ++++- MultiplayerAPI/Interfaces/IPlayer.cs | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Multiplayer/API/ClientPlayerWrapper.cs b/Multiplayer/API/ClientPlayerWrapper.cs index 568d89f2..ac5c6ab5 100644 --- a/Multiplayer/API/ClientPlayerWrapper.cs +++ b/Multiplayer/API/ClientPlayerWrapper.cs @@ -22,7 +22,6 @@ public string Username get => _networkedPlayer.Username; set => _networkedPlayer.Username = value; } - public Guid Guid => Guid.Empty; // NetworkedPlayer doesn't store GUID public Vector3 Position => _networkedPlayer.transform.position; public float RotationY => _networkedPlayer.transform.rotation.eulerAngles.y; public bool IsLoaded => true; // If we have the object, it's loaded diff --git a/Multiplayer/API/ServerPlayerWrapper.cs b/Multiplayer/API/ServerPlayerWrapper.cs index bcd3773b..d15ce381 100644 --- a/Multiplayer/API/ServerPlayerWrapper.cs +++ b/Multiplayer/API/ServerPlayerWrapper.cs @@ -2,6 +2,7 @@ using Multiplayer.Components.Networking; using Multiplayer.Components.Networking.Train; using Multiplayer.Networking.Data; +using Multiplayer.Networking.TransportLayers; using System; using UnityEngine; @@ -9,7 +10,7 @@ namespace Multiplayer.API; public class ServerPlayerWrapper : IPlayer { - private readonly ServerPlayer _serverPlayer; + internal readonly ServerPlayer _serverPlayer; private readonly bool _isHost; public ServerPlayerWrapper(ServerPlayer serverPlayer) @@ -39,4 +40,6 @@ internal TrainCar GetOccupiedCar() NetworkedTrainCar.TryGet(_serverPlayer.CarId, out TrainCar trainCar); return trainCar; } + + internal ITransportPeer Peer => _serverPlayer.Peer; } diff --git a/MultiplayerAPI/Interfaces/IPlayer.cs b/MultiplayerAPI/Interfaces/IPlayer.cs index b8527b19..4ae79952 100644 --- a/MultiplayerAPI/Interfaces/IPlayer.cs +++ b/MultiplayerAPI/Interfaces/IPlayer.cs @@ -1,3 +1,4 @@ +using System; using UnityEngine; namespace MPAPI.Interfaces @@ -5,7 +6,7 @@ namespace MPAPI.Interfaces public interface IPlayer { public byte Id { get; } - public string Username { get; set; } + public string Username { get; } Vector3 Position { get; } float RotationY { get; } bool IsLoaded { get; } From c347b3d87af9ff7749a99171205ebd6235c78ca1 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 14 Jun 2025 19:16:47 +1000 Subject: [PATCH 372/521] Rework ChatManager & ServerAPIProvider --- Multiplayer/API/ServerAPIProvider.cs | 227 ++++++++++++------ .../Networking/Managers/Server/ChatManager.cs | 135 +++++------ .../Managers/Server/NetworkServer.cs | 60 ++--- MultiplayerAPI/Interfaces/IServer.cs | 21 +- 4 files changed, 269 insertions(+), 174 deletions(-) diff --git a/Multiplayer/API/ServerAPIProvider.cs b/Multiplayer/API/ServerAPIProvider.cs index 38142144..cbfe52e1 100644 --- a/Multiplayer/API/ServerAPIProvider.cs +++ b/Multiplayer/API/ServerAPIProvider.cs @@ -2,112 +2,199 @@ using MPAPI.Interfaces.Packets; using Multiplayer.Networking.Data; using Multiplayer.Networking.Managers.Server; +using Multiplayer.Networking.TransportLayers; using Multiplayer.Utils; using System; using System.Collections.Generic; using System.Linq; using UnityEngine; -namespace Multiplayer.API +namespace Multiplayer.API; + +public class ServerAPIProvider : IServer { - public class ServerAPIProvider : IServer + private readonly Dictionary _playerWrapperCache = []; + private readonly NetworkServer server; + + public event Action OnPlayerConnected; + public event Action OnPlayerDisconnected; + public event Action OnPlayerReady; + + #region Server Properties + + public int PlayerCount => server.PlayerCount; + + public IReadOnlyCollection Players => server.ServerPlayers.Select(GetWrapper).ToList().AsReadOnly(); + + public IPlayer GetPlayer(byte id) { - private readonly NetworkServer server; + _playerWrapperCache.TryGetValue(id, out var player); + return player; + } + #endregion - public event Action OnPlayerConnected; - public event Action OnPlayerDisconnected; + #region Packet API + public void RegisterPacket(ServerPacketHandler handler) where T : class, IPacket, new() + { + server.RegisterExternalPacket(handler); + } + public void RegisterSerializablePacket(ServerPacketHandler handler) where T : class, ISerializablePacket, new() + { + server.RegisterExternalSerializablePacket(handler); + } - #region Server Properties - public int PlayerCount => server.PlayerCount; + public void SendPacketToAll(T packet, bool reliable = true, IPlayer excludePlayer = null) where T : class, IPacket, new() + { + ITransportPeer peer = null; - public IReadOnlyCollection Players => - server.ServerPlayers - .Select(p => new ServerPlayerWrapper(p)) - .Cast() - .ToList(); + if (excludePlayer != null) + peer = GetPeerFromPlayer(excludePlayer, $"SendPacketToAll<{typeof(T).Name}>"); - #endregion + if (peer != null) + server.SendExternalPacketToAll(packet, reliable, peer); + } - #region Packet API - public void RegisterPacket(ServerPacketHandler handler) where T : class, IPacket, new() - { - server.RegisterExternalPacket(handler); - } - public void RegisterSerializablePacket(ServerPacketHandler handler) where T : class, ISerializablePacket, new() - { - server.RegisterExternalSerializablePacket(handler); - } + public void SendSerializablePacketToAll(T packet, bool reliable = true, IPlayer excludePlayer = null) where T : class, ISerializablePacket, new() + { + ITransportPeer peer = null; + if(excludePlayer != null) + peer = GetPeerFromPlayer(excludePlayer, $"SendSerializablePacketToAll<{typeof(T).Name}>"); - public void SendPacketToAll(T packet, bool reliable = true, IPlayer excludePlayer = null) where T : class, IPacket, new() - { - server.SendExternalPacketToAll(packet, reliable, excludePlayer.Id); - } + server.SendExternalSerializablePacketToAll(packet, reliable, peer); + } - public void SendSerializablePacketToAll(T packet, bool reliable = true, IPlayer excludePlayer = null) where T : class, ISerializablePacket, new() - { - server.SendExternalSerializablePacketToAll(packet, reliable, excludePlayer.Id); - } + public void SendPacketToPlayer(T packet, IPlayer player, bool reliable = true) where T : class, IPacket, new() + { + var peer = GetPeerFromPlayer(player, $"SendPacketToPlayer<{typeof(T).Name}>"); - public void SendPacketToPlayer(T packet, IPlayer player, bool reliable = true) where T : class, IPacket, new() - { - server.SendExternalPacketToPlayer(packet, player.Id, reliable); - } + if (peer != null) + server.SendExternalPacketToPlayer(packet, peer, reliable); + } - public void SendSerializablePacketToPlayer(T packet, IPlayer player, bool reliable = true) where T : class, ISerializablePacket, new() - { - server.SendExternalSerializablePacketToPlayer(packet, player.Id, reliable); - } - #endregion + public void SendSerializablePacketToPlayer(T packet, IPlayer player, bool reliable = true) where T : class, ISerializablePacket, new() + { + var peer = GetPeerFromPlayer(player, $"SendSerializablePacketToPlayer<{typeof(T).Name}>"); - #region Server Util - public float AnyPlayerSqrMag(GameObject item) => DvExtensions.AnyPlayerSqrMag(item); + if (peer != null) + server.SendExternalSerializablePacketToPlayer(packet, peer, reliable); + } + #endregion - public float AnyPlayerSqrMag(Vector3 anchor) => DvExtensions.AnyPlayerSqrMag(anchor); - #endregion + #region Server Util + public float AnyPlayerSqrMag(GameObject item) => DvExtensions.AnyPlayerSqrMag(item); - #region Chat - public void SendServerChatMessage(string message, IPlayer player = null) - { - server.ChatManager.ServerMessage(message, null, player); - } + public float AnyPlayerSqrMag(Vector3 anchor) => DvExtensions.AnyPlayerSqrMag(anchor); + #endregion - public bool RegisterChatCommand(string commandLong, string commandShort, Func helpMessage, Action callback) - { - return server.ChatManager.RegisterChatCommand(commandLong, commandShort, helpMessage, callback); - } + #region Chat + public void SendServerChatMessage(string message, IPlayer excludePlayer = null) + { + var excludedServerPlayer = GetServerPlayerFromIPlayer(excludePlayer); + if (excludedServerPlayer != null) + server.ChatManager.ServerMessage(message, null, excludedServerPlayer); + } - public void RegisterChatFilter(Func callback) + public void SendWhisperChatMessage(string message, IPlayer player) + { + var serverPlayer = GetServerPlayerFromIPlayer(player); + if (serverPlayer != null) + server.SendWhisper(message, serverPlayer); + } + + public bool RegisterChatCommand(string commandLong, string commandShort, Func helpMessage, ChatCommandCallback callback) + { + ChatCommandCallbackInternal internalCallback = (message, serverPlayer) => { - server.ChatManager.RegisterChatFilter(callback); - } - #endregion + var playerWrapper = GetWrapper(serverPlayer); + callback(message, playerWrapper); + }; + + return server.ChatManager.RegisterChatCommand(commandLong, commandShort, helpMessage, internalCallback); + } - #region Class Helpers - internal ServerAPIProvider(NetworkServer serverInstance) + public void RegisterChatFilter(ChatFilterDelegate callback) + { + ChatFilterDelegateInternal internalCallback = (ref string message, ServerPlayer serverPlayer) => { - this.server = serverInstance; + var playerWrapper = GetWrapper(serverPlayer); + return callback(ref message, playerWrapper); + }; - server.PlayerConnected += OnPlayerConnectedInternal; - server.PlayerDisconnected += OnPlayerDisconnectedInternal; - } + server.ChatManager.RegisterChatFilter(internalCallback); + } + #endregion - internal void Dispose() + #region Class Helpers + internal ServerAPIProvider(NetworkServer serverInstance) + { + this.server = serverInstance; + + server.PlayerConnected += OnPlayerConnectedInternal; + server.PlayerDisconnected += OnPlayerDisconnectedInternal; + server.PlayerReady += OnPlayerReadyInternal; + } + + private ServerPlayerWrapper GetWrapper(ServerPlayer serverPlayer) + { + if (!_playerWrapperCache.TryGetValue(serverPlayer.Id, out var wrapper)) { - server.PlayerConnected -= OnPlayerConnectedInternal; - server.PlayerDisconnected -= OnPlayerDisconnectedInternal; + wrapper = new ServerPlayerWrapper(serverPlayer); + _playerWrapperCache[serverPlayer.Id] = wrapper; } + return wrapper; + } - private void OnPlayerConnectedInternal(Networking.Data.ServerPlayer serverPlayer) + private ITransportPeer GetPeerFromPlayer(IPlayer player, string operationName) + { + if (player == null) { - OnPlayerConnected?.Invoke(new ServerPlayerWrapper(serverPlayer)); + server.LogDebug(() => $"{operationName}: Player is null"); + return null; } - private void OnPlayerDisconnectedInternal(Networking.Data.ServerPlayer serverPlayer) + if (player is ServerPlayerWrapper playerWrapper) { - OnPlayerDisconnected?.Invoke(new ServerPlayerWrapper(serverPlayer)); + return playerWrapper.Peer; } - #endregion + + server.LogWarning($"{operationName}: Player '{player.Username}' is not a ServerPlayerWrapper (got {player.GetType().Name})"); + return null; + } + + private ServerPlayer GetServerPlayerFromIPlayer(IPlayer player) + { + if (player == null) + return null; + + if (player is ServerPlayerWrapper wrapper) + return wrapper._serverPlayer; // You'll need to make this internal accessible + + server.LogWarning($"GetServerPlayerFromIPlayer: Player '{player.Username}' is not a ServerPlayerWrapper (got {player.GetType().Name})"); + return null; + } + + internal void Dispose() + { + server.PlayerConnected -= OnPlayerConnectedInternal; + server.PlayerDisconnected -= OnPlayerDisconnectedInternal; + } + + private void OnPlayerConnectedInternal(ServerPlayer serverPlayer) + { + OnPlayerConnected?.Invoke(GetWrapper(serverPlayer)); + } + + private void OnPlayerDisconnectedInternal(ServerPlayer serverPlayer) + { + OnPlayerDisconnected?.Invoke(GetWrapper(serverPlayer)); + _playerWrapperCache.Remove(serverPlayer.Id); + } + + private void OnPlayerReadyInternal(ServerPlayer serverPlayer) + { + OnPlayerReady?.Invoke(GetWrapper(serverPlayer)); } + #endregion } diff --git a/Multiplayer/Networking/Managers/Server/ChatManager.cs b/Multiplayer/Networking/Managers/Server/ChatManager.cs index ca050fc7..d9dca625 100644 --- a/Multiplayer/Networking/Managers/Server/ChatManager.cs +++ b/Multiplayer/Networking/Managers/Server/ChatManager.cs @@ -1,7 +1,5 @@ -using MPAPI.Interfaces; using Multiplayer.Components.Networking; using Multiplayer.Networking.Data; -using Multiplayer.Networking.TransportLayers; using System; using System.Collections.Generic; using System.Linq; @@ -9,15 +7,16 @@ using System.Text.RegularExpressions; namespace Multiplayer.Networking.Managers.Server; - +public delegate void ChatCommandCallbackInternal(string message, ServerPlayer sender); +public delegate bool ChatFilterDelegateInternal(ref string message, ServerPlayer sender); public 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 COMMAND_HELP_SHORT = "?"; public const string COMMAND_LOG = "log"; public const string COMMAND_LOG_SHORT = "l"; public const string COMMAND_KICK = "kick"; @@ -25,9 +24,9 @@ public class ChatManager public const string MESSAGE_COLOUR_SERVER = "9CDCFE"; public const string MESSAGE_COLOUR_HELP = "00FF00"; - private readonly Dictionary> _registeredCommands = []; + private readonly Dictionary _registeredCommands = []; private readonly Dictionary> _registeredHelpMessages = []; - private readonly List> _chatFilters = []; + private readonly List _chatFilters = []; public ChatManager() { @@ -40,7 +39,7 @@ private void RegisterBuiltInCommands() ( COMMAND_SERVER, COMMAND_SERVER_SHORT, - () => $"\r\n\r\n\t{Locale.CHAT_HELP_SERVER_MSG}" + + () => $"{Locale.CHAT_HELP_SERVER_MSG}" + $"\r\n\t\t/{COMMAND_SERVER} <{Locale.CHAT_HELP_MSG}>" + $"\r\n\t\t/{COMMAND_SERVER_SHORT} <{Locale.CHAT_HELP_MSG}>", (message, sender) => ServerMessage(message, sender, null) @@ -50,7 +49,7 @@ private void RegisterBuiltInCommands() ( COMMAND_WHISPER, COMMAND_WHISPER_SHORT, - ()=> $"\r\n\r\n\t{Locale.CHAT_HELP_WHISPER_MSG}" + + ()=> $"{Locale.CHAT_HELP_WHISPER_MSG}" + $"\r\n\t\t/{COMMAND_WHISPER} <{Locale.CHAT_HELP_PLAYER_NAME}> <{Locale.CHAT_HELP_MSG}>" + $"\r\n\t\t/{COMMAND_WHISPER_SHORT} <{Locale.CHAT_HELP_PLAYER_NAME}> <{Locale.CHAT_HELP_MSG}>", WhisperMessage @@ -60,7 +59,7 @@ private void RegisterBuiltInCommands() ( COMMAND_HELP, COMMAND_HELP_SHORT, - ()=> $"\r\n\r\n\t{Locale.CHAT_HELP_HELP}" + + ()=> $"{Locale.CHAT_HELP_HELP}" + $"\r\n\t\t/{COMMAND_HELP}" + $"\r\n\t\t/{COMMAND_HELP_SHORT}", HelpMessage @@ -70,7 +69,7 @@ private void RegisterBuiltInCommands() ( COMMAND_KICK, null, - () => $"\r\n\r\n\tKick a player from the server (must be host)" + + () => $"Kick a player from the server (must be host)" + $"\r\n\t\t/{COMMAND_KICK}", KickMessage ); @@ -86,7 +85,7 @@ private void RegisterBuiltInCommands() #endif } - public bool RegisterChatCommand(string commandLong, string commandShort, Func helpMessage, Action callback) + public bool RegisterChatCommand(string commandLong, string commandShort, Func helpMessage, ChatCommandCallbackInternal callback) { if (string.IsNullOrEmpty(commandLong) || callback == null) return false; @@ -110,7 +109,7 @@ public bool RegisterChatCommand(string commandLong, string commandShort, Func callback) + public void RegisterChatFilter(ChatFilterDelegateInternal callback) { if (callback != null) { @@ -118,25 +117,32 @@ public void RegisterChatFilter(Func callback) } } - public void ProcessMessage(string message, IPlayer sender) + public void ProcessMessage(string message, ServerPlayer sender) { if (string.IsNullOrEmpty(message)) return; + Multiplayer.LogDebug(() => $"ProcessMessage(\'{message}\')"); + //Check if we have a command if (message.StartsWith("/")) { string[] messageParams = message.Substring(1).Split(' '); string command = messageParams[0].ToLower(); + Multiplayer.LogDebug(() => $"ProcessMessage(\'{message}\') starts with, substr: {message.Substring(0)}), command: {command}"); + //check registered commands if (!string.IsNullOrEmpty(command) && _registeredCommands.TryGetValue(command, out var commandCallback)) { //remove the command, leading slash and trailing space - message = message.Substring(command.Length + 2); - commandCallback(message, sender); + var cleanedMessage = message.Substring(command.Length + 1).Trim(); + + Multiplayer.LogDebug(() => $"ProcessMessage(\'{message}\') cleaned message: {cleanedMessage}"); + commandCallback(cleanedMessage, sender); + return; } } @@ -145,9 +151,9 @@ public void ProcessMessage(string message, IPlayer sender) ProcessChatMessage(message, sender); } - private void ProcessChatMessage(string message, IPlayer sender) + private void ProcessChatMessage(string message, ServerPlayer sender) { - if (sender is not ServerPlayer player) + if (sender == null) return; //clean up the message to stop format injection @@ -156,47 +162,29 @@ private void ProcessChatMessage(string message, IPlayer sender) //call each filter until either a filter returns false or all filters have been called foreach (var filter in _chatFilters) { - if (!filter(message, sender)) + if (!filter(ref message, sender)) return; } message = $"{sender.Username}: {message}"; - NetworkLifecycle.Instance.Server.SendChat(message, player.Peer); + NetworkLifecycle.Instance.Server.SendChat(message, sender); } - public void ServerMessage(string message, IPlayer sender, IPlayer exclude = null) + public void ServerMessage(string message, ServerPlayer sender, ServerPlayer exclude = null) { - ServerPlayer senderPlayer = null; - ITransportPeer excludePeer = null; - - if (sender != null) - { - if(sender is not ServerPlayer sp) - return; - - senderPlayer = sp; - } - - if (exclude != null) - { - if (exclude is not ServerPlayer ep) - return; - excludePeer = ep.Peer; - } - //If user is not the host, we should ignore - will require changes for dedicated server - if (senderPlayer != null && !NetworkLifecycle.Instance.IsHost(senderPlayer.Peer)) + if (sender != null && !NetworkLifecycle.Instance.IsHost(sender.Peer)) return; message = $"{message}"; - NetworkLifecycle.Instance.Server.SendChat(message, excludePeer); + NetworkLifecycle.Instance.Server.SendChat(message, exclude); } - private void WhisperMessage(string message, IPlayer sender) + private void WhisperMessage(string message, ServerPlayer sender) { Multiplayer.LogDebug(() => $"Whispering: \"{message}\", sender: {sender?.Username}, senderID: {sender?.Id}"); - if (sender == null || sender is not ServerPlayer senderPlayer) + if (sender == null) return; if (string.IsNullOrEmpty(message)) @@ -214,13 +202,13 @@ private void WhisperMessage(string message, IPlayer sender) Multiplayer.LogDebug(()=>$"Whispering parse 1: \"{message}\", sender: {sender?.Username}, senderID: {sender?.Id}, peerName: {recipientName}"); //look up the peer ID - ITransportPeer recipient = NetPeerFromName(recipientName); + var recipient = ServerPlayerFromUsername(recipientName); if(recipient == null) { Multiplayer.LogDebug(() => $"Whispering failed: \"{message}\", sender: {sender?.Username}, senderID: {sender?.Id}, peerName: {recipientName}"); whisperMessage = $"{recipientName} not found - you're whispering into the void!"; //todo: add translation - NetworkLifecycle.Instance.Server.SendWhisper(whisperMessage, senderPlayer.Peer); + NetworkLifecycle.Instance.Server.SendWhisper(whisperMessage, sender); return; } @@ -229,49 +217,64 @@ private void WhisperMessage(string message, IPlayer sender) //clean up the message to stop format injection whisperMessage = Regex.Replace(whisperMessage, "", string.Empty, RegexOptions.IgnoreCase); + //call each chat filter until either a filter returns false or all filters have been called + foreach (var filter in _chatFilters) + { + if (!filter(ref message, sender)) + return; + } + whisperMessage = "" + sender.Username + ": " + whisperMessage + ""; NetworkLifecycle.Instance.Server.SendWhisper(whisperMessage, recipient); } - public void KickMessage(string message, IPlayer sender) + public void KickMessage(string message, ServerPlayer sender) { - ITransportPeer player; + ServerPlayer playerToKick; string playerName; + string whisper; //If user is not the host, we should ignore - will require changes for dedicated server - if (sender == null || sender is not ServerPlayer senderPlayer || !NetworkLifecycle.Instance.IsHost(senderPlayer.Peer)) + if (sender == null || !NetworkLifecycle.Instance.IsHost(sender.Peer)) return; playerName = message.Split(' ')[0]; if (string.IsNullOrEmpty(playerName)) return; - player = NetPeerFromName(playerName); + playerToKick = ServerPlayerFromUsername(playerName); - if (player == null || NetworkLifecycle.Instance.IsHost(player)) + if (playerToKick == null || NetworkLifecycle.Instance.IsHost(playerToKick.Peer)) { - message = $"Unable to kick {playerName}"; //todo: translate + whisper = $"Unable to kick {playerName}"; //todo: translate } else { - message = $"{playerName} was kicked"; //todo: translate + whisper = $"{playerName} was kicked"; //todo: translate - NetworkLifecycle.Instance.Server.KickPlayer(player); + NetworkLifecycle.Instance.Server.KickPlayer(playerToKick); } - NetworkLifecycle.Instance.Server.SendWhisper(message, senderPlayer.Peer); + NetworkLifecycle.Instance.Server.SendWhisper(whisper, sender); } - private void HelpMessage(string _, IPlayer peer) + private void HelpMessage(string _, ServerPlayer player) { - if (peer == null || peer is not ServerPlayer player) + + if (player == null) return; + Multiplayer.LogDebug(() => $"HelpMessage()"); + StringBuilder sb = new($"{Locale.CHAT_HELP_AVAILABLE}"); foreach (var helpMessage in _registeredHelpMessages) - sb.AppendLine(helpMessage.Value?.Invoke()); + { + var help = helpMessage.Value?.Invoke(); + if (help != null) + sb.AppendLine("\r\n\t" + help); + } sb.AppendLine(""); @@ -282,7 +285,7 @@ private void HelpMessage(string _, IPlayer peer) "\r\n\t\t/server " + "\r\n\t\t/s " + - "\r\n\r\n\tWhisper to a player" + + "\r\n\r\n\tWhisper to a playerToKick" + "\r\n\t\t/whisper " + "\r\n\t\t/w " + @@ -292,26 +295,16 @@ private void HelpMessage(string _, IPlayer peer) ""; */ - NetworkLifecycle.Instance.Server.SendWhisper(sb.ToString(), player.Peer); + NetworkLifecycle.Instance.Server.SendWhisper(sb.ToString(), player); } - private ITransportPeer NetPeerFromName(string peerName) + private ServerPlayer ServerPlayerFromUsername(string playerName) { - if(peerName == null || peerName == string.Empty) - return null; - - ServerPlayer player = NetworkLifecycle.Instance.Server.ServerPlayers.Where(p => p.Username == peerName).FirstOrDefault(); - if (player == null) + if(string.IsNullOrEmpty(playerName)) return null; - if(NetworkLifecycle.Instance.Server.TryGetPeer(player.Id, out ITransportPeer peer)) - { - return peer; - } - - return null; - + return NetworkLifecycle.Instance.Server.ServerPlayers.Where(p => p.Username == playerName).FirstOrDefault(); } } diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index 09c317a7..5981c6d7 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -43,6 +43,7 @@ public class NetworkServer : NetworkManager { public Action PlayerConnected; public Action PlayerDisconnected; + public Action PlayerReady; protected override string LogPrefix => "[Server]"; private readonly Queue joinQueue = new(); //Queue for players attempting to join while server is loading @@ -67,7 +68,7 @@ public class NetworkServer : NetworkManager public readonly IDifficulty Difficulty; private bool IsLoaded; - private readonly ChatManager _chatManager; + private readonly ChatManager _chatManager = new(); public ChatManager ChatManager => _chatManager; //we don't care if the client doesn't have these mods @@ -347,13 +348,14 @@ public override void OnConnectionRequest(NetDataReader requestData, IConnectionR SendPacketToAll(packet, deliveryMethod); } - public void SendExternalPacketToAll(T packet, bool reliable, byte excludePlayerId) where T : class, IPacket, new() + public void SendExternalPacketToAll(T packet, bool reliable, ITransportPeer excludePeer) where T : class, IPacket, new() { - if (!TryGetPeer(excludePlayerId, out var peer)) - return; - var deliveryMethod = reliable ? DeliveryMethod.ReliableUnordered : DeliveryMethod.Unreliable; - SendPacketToAll(packet, deliveryMethod, peer); + + if(excludePeer == null) + SendPacketToAll(packet, deliveryMethod); + else + SendPacketToAll(packet, deliveryMethod, excludePeer); } public void SendExternalSerializablePacketToAll(T packet, bool reliable) where T : class, ISerializablePacket, new() @@ -363,41 +365,39 @@ public override void OnConnectionRequest(NetDataReader requestData, IConnectionR SendNetSerializablePacketToAll(wrapper, deliveryMethod); } - public void SendExternalSerializablePacketToAll(T packet, bool reliable, byte excludePlayerId) where T : class, ISerializablePacket, new() + public void SendExternalSerializablePacketToAll(T packet, bool reliable, ITransportPeer excludePeer) where T : class, ISerializablePacket, new() { - if (!TryGetPeer(excludePlayerId, out var peer)) - return; - var deliveryMethod = reliable ? DeliveryMethod.ReliableUnordered : DeliveryMethod.Unreliable; var wrapper = new ExternalSerializablePacketWrapper { Packet = packet }; - SendNetSerializablePacketToAll(wrapper, deliveryMethod, peer); + + if (excludePeer == null) + SendNetSerializablePacketToAll(wrapper, deliveryMethod); + else + SendNetSerializablePacketToAll(wrapper, deliveryMethod, excludePeer); } - public void SendExternalPacketToPlayer(T packet, byte playerId, bool reliable) where T : class, IPacket, new() + public void SendExternalPacketToPlayer(T packet, ITransportPeer peer, bool reliable) where T : class, IPacket, new() { - if (!TryGetPeer(playerId, out var peer)) - return; - var deliveryMethod = reliable ? DeliveryMethod.ReliableUnordered : DeliveryMethod.Unreliable; SendPacket(peer, packet, deliveryMethod); } - public void SendExternalSerializablePacketToPlayer(T packet, byte playerId, bool reliable) where T : class, ISerializablePacket, new() + public void SendExternalSerializablePacketToPlayer(T packet, ITransportPeer peer, bool reliable) where T : class, ISerializablePacket, new() { - if (!TryGetPeer(playerId, out var peer)) - return; - var deliveryMethod = reliable ? DeliveryMethod.ReliableUnordered : DeliveryMethod.Unreliable; var wrapper = new ExternalSerializablePacketWrapper { Packet = packet }; + SendNetSerializablePacket(peer, wrapper, deliveryMethod); } #endregion - public void KickPlayer(ITransportPeer peer) + public void KickPlayer(ServerPlayer player) { - //peer.Send(WritePacket(new ClientboundDisconnectPacket()),DeliveryMethod.ReliableUnordered); - peer.Disconnect(WritePacket(new ClientboundDisconnectPacket { Kicked = true })); + if (player == null || player.Peer == null) + return; + + player.Peer.Disconnect(WritePacket(new ClientboundDisconnectPacket { Kicked = true })); } public void SendGameParams(GameParams gameParams) { @@ -649,7 +649,7 @@ public void SendItemsChangePacket(List items, ServerPlayer playe } } - public void SendChat(string message, ITransportPeer exclude = null) + public void SendChat(string message, ServerPlayer exclude = null) { if (exclude != null) @@ -657,7 +657,7 @@ public void SendChat(string message, ITransportPeer exclude = null) NetworkLifecycle.Instance.Server.SendPacketToAll(new CommonChatPacket { message = message - }, DeliveryMethod.ReliableUnordered, exclude); + }, DeliveryMethod.ReliableUnordered, exclude.Peer); } else { @@ -668,11 +668,11 @@ public void SendChat(string message, ITransportPeer exclude = null) } } - public void SendWhisper(string message, ITransportPeer recipient) + public void SendWhisper(string message, ServerPlayer recipient) { - if (message != null || recipient != null) + if (!string.IsNullOrEmpty(message) && recipient != null && recipient.Peer != null) { - NetworkLifecycle.Instance.Server.SendPacket(recipient, new CommonChatPacket + NetworkLifecycle.Instance.Server.SendPacket(recipient.Peer, new CommonChatPacket { message = message }, DeliveryMethod.ReliableUnordered); @@ -834,7 +834,8 @@ private void OnServerboundClientReadyPacket(ServerboundClientReadyPacket packet, }; SendPacketToAll(clientboundPlayerJoinedPacket, DeliveryMethod.ReliableOrdered, peer); - ChatManager.ServerMessage(serverPlayer.Username + " joined the game", null, (IPlayer) serverPlayer); + LogDebug(() => $"Chatmanager"); + ChatManager.ServerMessage(serverPlayer.Username + " joined the game", null, serverPlayer); Log($"Client {peer.Id} is ready. Sending world state"); @@ -915,6 +916,7 @@ private void OnServerboundClientReadyPacket(ServerboundClientReadyPacket packet, SendPacket(peer, new ClientboundRemoveLoadingScreenPacket(), DeliveryMethod.ReliableOrdered); serverPlayer.IsLoaded = true; + } private void OnServerboundPlayerPositionPacket(ServerboundPlayerPositionPacket packet, ITransportPeer peer) @@ -1261,7 +1263,7 @@ private void OnServerboundJobValidateRequestPacket(ServerboundJobValidateRequest private void OnCommonChatPacket(CommonChatPacket packet, ITransportPeer peer) { if (TryGetServerPlayer(peer, out ServerPlayer player)) - ChatManager.ProcessMessage(packet.message, (IPlayer)player); + ChatManager.ProcessMessage(packet.message, player); } #endregion diff --git a/MultiplayerAPI/Interfaces/IServer.cs b/MultiplayerAPI/Interfaces/IServer.cs index 09bddc8f..97fd239a 100644 --- a/MultiplayerAPI/Interfaces/IServer.cs +++ b/MultiplayerAPI/Interfaces/IServer.cs @@ -6,10 +6,13 @@ namespace MPAPI.Interfaces; +public delegate void ChatCommandCallback(string message, IPlayer sender); +public delegate bool ChatFilterDelegate(ref string message, IPlayer sender); public interface IServer { event Action OnPlayerConnected; event Action OnPlayerDisconnected; + event Action OnPlayerReady; #region Server Properties @@ -25,6 +28,8 @@ public interface IServer /// Read-only collection of IPlayer objects public IReadOnlyCollection Players { get; } + IPlayer GetPlayer(byte id); + #endregion #region Packet API @@ -96,7 +101,7 @@ public interface IServer float AnyPlayerSqrMag(Vector3 anchor); #endregion - #region Chat + #region Chat API /// /// Sends a server chat message /// @@ -104,6 +109,13 @@ public interface IServer /// Player to exclude. If null, message will go to all players void SendServerChatMessage(string message, IPlayer excludePlayer = null); + /// + /// Sends a chat message to a specific player + /// + /// Message to be sent + /// Recipient player + void SendWhisperChatMessage(string message, IPlayer player); + /// /// Registers a chat command e.g. `/server` and optional short command '/s' /// @@ -112,16 +124,17 @@ public interface IServer /// Optional callback for a help message e.g. "Send a message as the server (host only)\r\n\t\t/server \r\n\t\t/s " It is recommended to provide localisation/translation for this string /// Action to execute when the command is triggered. First parameter contains message without the command e.g. '/command parameter1 parameter2' will become 'parameter1 parameter2', second parameter is the player who executed the command. /// True if the command was successfully registered, false if registration failed (e.g. command already exists). - bool RegisterChatCommand(string commandLong, string commandShort, Func helpMessage, Action callback); + bool RegisterChatCommand(string commandLong, string commandShort, Func helpMessage, ChatCommandCallback callback); /// /// Registers a chat filter that processes non-command messages in registration order. /// Filters form a chain where each can either allow the message to continue to the next filter or block further processing. /// If all filters return true, the message will be sent to all players (default action). + /// This filter also applies to whispered messages, regardless of source (player or server) /// - /// Filter function that processes the message. First parameter is the message content, second parameter is the player who sent the message. Return true to pass the message to the next filter/default action, false to block propagation. - void RegisterChatFilter(Func callback); + /// Filter function type `ChatFilterDelegate` that processes the message. First delegate parameter is the message content, second parameter is the player who sent the message. Return true to pass the message to the next filter/default action, false to block propagation. + void RegisterChatFilter(ChatFilterDelegate callback); #endregion } From 2a1aa773ba4d0a80e549a012ed4cc9a05be2339c Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 14 Jun 2025 19:21:39 +1000 Subject: [PATCH 373/521] Rework ClientAPIProvider --- Multiplayer/API/ClientAPIProvider.cs | 37 +++++++++++++++++----------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/Multiplayer/API/ClientAPIProvider.cs b/Multiplayer/API/ClientAPIProvider.cs index 1b774c7a..14ddc1ce 100644 --- a/Multiplayer/API/ClientAPIProvider.cs +++ b/Multiplayer/API/ClientAPIProvider.cs @@ -1,32 +1,34 @@ using MPAPI.Interfaces; using MPAPI.Interfaces.Packets; +using Multiplayer.Components.Networking.Player; +using Multiplayer.Networking.Data; using Multiplayer.Networking.Managers.Client; -using Multiplayer.Networking.Managers.Server; using Multiplayer.Utils; using System; using System.Collections.Generic; using System.Linq; using UnityEngine; -using static Humanizer.In; namespace Multiplayer.API { public class ClientAPIProvider : IClient { + private readonly Dictionary _playerWrapperCache = []; private readonly NetworkClient client; public event Action OnPlayerConnected; public event Action OnPlayerDisconnected; #region Client Properties - public IReadOnlyCollection Players => - client.ClientPlayerManager.Players - .Select(p => new ClientPlayerWrapper(p)) - .Cast() - .ToList(); + public IReadOnlyCollection Players => client.ClientPlayerManager.Players.Select(GetWrapper).ToList().AsReadOnly(); - - public bool IsConnected => throw new NotImplementedException(); + public IPlayer GetPlayer(byte id) + { + _playerWrapperCache.TryGetValue(id, out var player); + return player; + } + + public bool IsConnected => client.IsRunning; public int Ping => client.Ping; @@ -69,21 +71,26 @@ internal void Dispose() client.ClientPlayerManager.OnPlayerDisconnected -= OnPlayerDisconnectedInternal; } - public IPlayer GetPlayer(byte id) + + private ClientPlayerWrapper GetWrapper(NetworkedPlayer networkedPlayer) { - if (client.ClientPlayerManager.TryGetPlayer(id, out var player)) - return new ClientPlayerWrapper(player); - return null; + if (!_playerWrapperCache.TryGetValue(networkedPlayer.Id, out var wrapper)) + { + wrapper = new ClientPlayerWrapper(networkedPlayer); + _playerWrapperCache[networkedPlayer.Id] = wrapper; + } + return wrapper; } private void OnPlayerConnectedInternal(Components.Networking.Player.NetworkedPlayer networkedPlayer) { - OnPlayerConnected?.Invoke(new ClientPlayerWrapper(networkedPlayer)); + OnPlayerConnected?.Invoke(GetWrapper(networkedPlayer)); } private void OnPlayerDisconnectedInternal(Components.Networking.Player.NetworkedPlayer networkedPlayer) { - OnPlayerDisconnected?.Invoke(new ClientPlayerWrapper(networkedPlayer)); + OnPlayerDisconnected?.Invoke(GetWrapper(networkedPlayer)); + _playerWrapperCache.Remove(networkedPlayer.Id); } #endregion } From b54184365a1075d43d75f04c1e832091cb688a37 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 14 Jun 2025 19:22:30 +1000 Subject: [PATCH 374/521] Update MultiplayerAPI Tests --- .../Enums/WheelArrangement.cs | 10 + MultiplayerAPI Tests/MultiplayerAPITest.cs | 2 +- .../MultiplayerAPITest.csproj | 1 + .../{SimpleModPacket.cs => SimplePacket.cs} | 14 +- .../Packets/SimplePacketWithNetId.cs | 26 +- .../TestComponents/ServerTest.cs | 375 +++++++++++++++++- 6 files changed, 401 insertions(+), 27 deletions(-) create mode 100644 MultiplayerAPI Tests/Enums/WheelArrangement.cs rename MultiplayerAPI Tests/Packets/{SimpleModPacket.cs => SimplePacket.cs} (61%) diff --git a/MultiplayerAPI Tests/Enums/WheelArrangement.cs b/MultiplayerAPI Tests/Enums/WheelArrangement.cs new file mode 100644 index 00000000..c887fe1d --- /dev/null +++ b/MultiplayerAPI Tests/Enums/WheelArrangement.cs @@ -0,0 +1,10 @@ +namespace MultiplayerAPITest.Enums; + +//for dynamic info a hash or other numbering system could be used, rather than a static enum. +public enum WheelArrangement : byte +{ + Default = 0, + American440 = 1, + Atlantic442 = 2, + Reading444 = 3 +} diff --git a/MultiplayerAPI Tests/MultiplayerAPITest.cs b/MultiplayerAPI Tests/MultiplayerAPITest.cs index 73eb56dc..cb1f575d 100644 --- a/MultiplayerAPI Tests/MultiplayerAPITest.cs +++ b/MultiplayerAPI Tests/MultiplayerAPITest.cs @@ -58,7 +58,7 @@ private static void OnServerStarted(IServer server) private static void OnClientStarted(IClient client) { - GameObject go = new GameObject("MPAPI ClientTest", [typeof(ServerTest)]); + GameObject go = new GameObject("MPAPI ClientTest", [typeof(ClientTest)]); GameObject.DontDestroyOnLoad(go); } diff --git a/MultiplayerAPI Tests/MultiplayerAPITest.csproj b/MultiplayerAPI Tests/MultiplayerAPITest.csproj index ae27035f..797b363f 100644 --- a/MultiplayerAPI Tests/MultiplayerAPITest.csproj +++ b/MultiplayerAPI Tests/MultiplayerAPITest.csproj @@ -56,6 +56,7 @@ + diff --git a/MultiplayerAPI Tests/Packets/SimpleModPacket.cs b/MultiplayerAPI Tests/Packets/SimplePacket.cs similarity index 61% rename from MultiplayerAPI Tests/Packets/SimpleModPacket.cs rename to MultiplayerAPI Tests/Packets/SimplePacket.cs index 27ab2a4a..34dc7430 100644 --- a/MultiplayerAPI Tests/Packets/SimpleModPacket.cs +++ b/MultiplayerAPI Tests/Packets/SimplePacket.cs @@ -1,9 +1,10 @@ using MPAPI.Interfaces.Packets; +using MultiplayerAPITest.Enums; using UnityEngine; namespace MultiplayerAPITest.Packets { - internal class SimpleModPacket : IPacket + internal class SimplePacket : IPacket { //Public properties are automatically serialised //acceptable types are: @@ -14,11 +15,12 @@ internal class SimpleModPacket : IPacket //Be mindful of the amount of data per packet. // Avoid sending long strings or large structures - // Consider using an numeric Id system to represent objects - // Use the MP API to get the NetId (ushort) for TrainCars, rather than the car's string Id - public string CarId { get; set; } //It's better to use ushort and call `MultiplayerAPI.GetTrainCarNetId(TrainCar)` - //See SimplePacketWithNetId for an example + // Consider using a numeric Id system to represent objects. + // The MP API provides Net Ids for common objects (e.g. TrainCars, Jobs, Switches, Turntables and RailTrack), + // see `TryGetNetId(T obj, out ushort netId)` + public string CarId { get; set; } //It's better to use ushort. See SimplePacketWithNetId for an example public Vector3 Position { get; set; } - + public WheelArrangement WheelArrangement { get; set; } + } } diff --git a/MultiplayerAPI Tests/Packets/SimplePacketWithNetId.cs b/MultiplayerAPI Tests/Packets/SimplePacketWithNetId.cs index c9aa8bad..18bcc9d8 100644 --- a/MultiplayerAPI Tests/Packets/SimplePacketWithNetId.cs +++ b/MultiplayerAPI Tests/Packets/SimplePacketWithNetId.cs @@ -1,22 +1,26 @@ using MPAPI.Interfaces.Packets; +using MultiplayerAPITest.Enums; +using UnityEngine; namespace MultiplayerAPITest.Packets { - - //for dynamic info a hash or other numbering system could be used, rather than a static enum. - public enum WheelArrangement : byte - { - Default = 0, - American440 = 1, - Atlantic442 = 2, - Reading444 = 3 - } + //Public properties are automatically serialised + //acceptable types are: + // Primitives and inbuilt structs (bool, byte, sbyte, short, ushort, int, uint, long, ulong, float, double, string, char, IPEndPoint, Guid) + // Arrays of primitives (e.g bool[], byte[], etc. + // Enums derived from primitives e.g. `enum MyEnum : byte` + // UnityEngine: Vector2, Vector3, Quarternion - //example packet with netId and an enum. + //Be mindful of the amount of data per packet. + // Avoid sending long strings or large structures + // Consider using a numeric Id system to identify objects + // The MP API provides Net Ids for common objects (e.g. TrainCars, Jobs, Switches, Turntables and RailTrack), + // see `TryGetNetId(T obj, out ushort netId)` internal class SimplePacketWithNetId : IPacket { - public ushort CarNetId { get; set; } + public ushort CarNetId { get; set; } // example use of a Net Id used to identify a TrainCar + public Vector3 Position { get; set; } public WheelArrangement WheelArrangement { get; set; } } } diff --git a/MultiplayerAPI Tests/TestComponents/ServerTest.cs b/MultiplayerAPI Tests/TestComponents/ServerTest.cs index eb6a4b8e..e099bd6d 100644 --- a/MultiplayerAPI Tests/TestComponents/ServerTest.cs +++ b/MultiplayerAPI Tests/TestComponents/ServerTest.cs @@ -1,19 +1,25 @@ +using CommandTerminal; +using DV.Logic.Job; +using I2.Loc; using MPAPI; using MPAPI.Interfaces; +using MultiplayerAPITest.Enums; using MultiplayerAPITest.Packets; using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Security.Cryptography; using System.Text; -using System.Threading.Tasks; using UnityEngine; namespace MultiplayerAPITest.TestComponents; - internal class ServerTest : MonoBehaviour { const string LogPrefix = "ServerTest"; + const string MESSAGE_COLOUR_SERVER = "9CDCFE"; IServer server; @@ -21,7 +27,12 @@ protected void Awake() { server = MultiplayerAPI.Server; - //subscribe to packets + // Subscribe to player events + server.OnPlayerConnected += OnPlayerConnected; + server.OnPlayerDisconnected += OnPlayerDisconnected; + server.OnPlayerReady += OnPlayerReady; + + //subscribe to packets and chat commands; Subscribe(); } @@ -29,28 +40,73 @@ protected void Start() { } protected void Update() { } - protected void OnDestroy() { } + protected void OnDestroy() + { + // Unsubscribe from player events + server.OnPlayerConnected -= OnPlayerConnected; + server.OnPlayerDisconnected -= OnPlayerDisconnected; + server.OnPlayerReady -= OnPlayerReady; + } //Setup subscriptions for the packets we want to/expect to receive private void Subscribe() { - server.RegisterPacket(OnTestSimpleModPacket); + // Subscribe to network packets + server.RegisterPacket(OnTestSimpleModPacket); server.RegisterPacket(OnSimplePacketWithNetId); server.RegisterSerializablePacket(OnTestComplexModPacket); + + // Subscribe to chat commands - these have been added for API testing and examples + server.RegisterChatCommand("packet", "p", OnChatCommandSendPacketHelp, OnChatCommandSendPacket); //this command allows testing of simple packet sending + server.RegisterChatCommand("locopos", "lp", OnChatCommandSendLocoPosHelp, OnChatCommandSendLocoPos); //this command allows testing of complex packet sending + server.RegisterChatCommand("closest", "cd", OnChatCommandClosestPlayerHelp, OnChatCommandClosestPlayer); //this command returns the distance of the closest player to a given TrainCar + server.RegisterChatCommand("stats", null, OnChatCommandStatsHelp, OnChatCommandStats); //this command returns the number of connected players and all player names + + // Subscribe to chat filters + server.RegisterChatFilter(OnChatMessage); + } + + #region Player Events + private void OnPlayerConnected(IPlayer player) + { + // Send mod settings, parameters, etc. + // Note: This event occurs when the player is authenticated and before the player receives game state info + + Log($"Player \"{player?.Id}\" has connected. (Is Loaded: {player?.IsLoaded})"); } + private void OnPlayerReady(IPlayer player) + { + // Player has indicated the world is loaded and they are ready to receive game state info + // Note: This event occurs after the server has sent the game state, it does not guarantee the player has finished generating all cars, jobs, etc. + + Log($"Player \"{player?.Id}\" is ready. (Is Loaded: {player?.IsLoaded})"); + + //Send an anouncement to all players + server.SendServerChatMessage($"Please welcome our newest driver {player?.Id}!"); + } + + private void OnPlayerDisconnected(IPlayer player) + { + // Player has disconnected + // Note: This event occurs immediately prior to destroying the player object + // Complete all cleanup prior to returning from this method + + Log($"Player \"{player?.Id}\" has disconnected"); + } + #endregion #region Packet Callbacks //method called when a `TestSimplePacket` packet is received - private void OnTestSimpleModPacket(SimpleModPacket packet, IPlayer player) + private void OnTestSimpleModPacket(SimplePacket packet, IPlayer player) { Log($"Received {packet.GetType()} from player: {player.Username}"); - Log($"CarId: {packet.CarId}, Position: {packet.Position}"); - + Log($"CarId: {packet.CarId}, Position: {packet.Position}, WheelArraangement: {packet.WheelArrangement}"); + } //method called when a `TestSimplePacket` packet is received @@ -59,7 +115,7 @@ private void OnSimplePacketWithNetId(SimplePacketWithNetId packet, IPlayer playe Log($"Received {packet.GetType()} from player: {player.Username}"); - Log($"CarId: {packet.CarId}, Position: {packet.Position}"); + Log($"CarId: {packet.CarNetId}, Position: {packet.Position}, Wheel Arrangement: {packet.WheelArrangement}"); } @@ -78,6 +134,307 @@ private void OnTestComplexModPacket(ComplexModPacket packet, IPlayer player) } #endregion + #region Packet Senders + public void SendSimplePacketToAll(string carId, Vector3 position, WheelArrangement arrangement, IPlayer excludePlayer = null) + { + SimplePacket packet = new() + { + CarId = carId, + Position = position, + WheelArrangement = arrangement + }; + + //send the packet reliably (ensure it makes it to all players) + server.SendPacketToAll(packet, true, excludePlayer); + } + + public void SendSimplePacketWithNetIdToAll(ushort carId, Vector3 position, WheelArrangement arrangement, IPlayer excludePlayer = null) + { + + SimplePacketWithNetId packet = new() + { + CarNetId = carId, + Position = position, + WheelArrangement = arrangement + }; + + //send the packet reliably (ensure it makes it to all players) + server.SendPacketToAll(packet, true, excludePlayer); + } + + public void SendComplexPacket(Dictionary carToPos, IPlayer excludePlayer = null) + { + + ComplexModPacket packet = new() + { + CarToPositionMap = carToPos + }; + + //send the packet reliably (ensure it makes it to all players) + server.SendSerializablePacketToAll(packet, true, excludePlayer); + } + #endregion + + #region Chat Command Callbacks + private void OnChatCommandSendPacket(string message, IPlayer sender) + { + string[] args = message.Split(' '); + string whisper; + + if (args.Length < 2) + { + LogWarning($"Received 'SendPacket' chat command from player \"{sender.Username}\", but not enough arguments were specified. Command: {message}"); + return; + } + + if (string.IsNullOrEmpty(args[1])) + { + LogWarning($"Received 'SendPacket' chat command from player \"{sender.Username}\", but the second argument is empty. Command: {message}"); + } + + var tc = GetTrainCarFromID(args[1]); + var pos = tc.transform.position - WorldMover.currentMove; + + switch (args[0].ToLower()) + { + case "simple": //send a simple packet + + if (tc) + { + // Send a simple packet to all players using TrainCar id as a string, TrainCar position and a random wheel arrangement + SendSimplePacketToAll(args[1], pos, GetRandomWheelArrangement()); + } + else + { + // Send a whisper back to the player who sent the command + whisper = $"TrainCar '{args[1]}' not found"; + server.SendWhisperChatMessage(whisper, sender); + } + break; + + case "net": //send a simple packet using a netId + + if (tc && MultiplayerAPI.Instance.TryGetNetId(tc, out ushort netId)) + { + // Send a simple packet to all players using TrainCar NetId, TrainCar position and a random wheel arrangement + SendSimplePacketWithNetIdToAll(netId, pos, GetRandomWheelArrangement()); + } + else + { + // Send a whisper back to the player who sent the command + whisper = $"TrainCar '{args[1]}' not found"; + server.SendWhisperChatMessage(whisper, sender); + } + break; + + default: + LogWarning($"Received 'SendPacket' chat command from player \"{sender.Username}\", but the packet type '{args[0].ToLower()}' was not recognised. Command: {message}"); + + // Send a whisper back to the player who sent the command + whisper = $"Packet type '{args[0].ToLower()}' was not recognised"; + server.SendWhisperChatMessage(whisper, sender); + + break; + } + } + + private void OnChatCommandSendLocoPos(string message, IPlayer sender) + { + //this chat command has no arguments + Dictionary carMap = []; + + foreach (var kvp in TrainCarRegistry.Instance.logicCarToTrainCar) + { + Car logicCar = kvp.Key; + TrainCar trainCar = kvp.Value; + + //locos only + if (!trainCar.IsLoco) + continue; + + if (!string.IsNullOrEmpty(logicCar.ID) && trainCar != null) + carMap[logicCar.ID] = trainCar.transform.position - WorldMover.currentMove; + } + + if (carMap.Count > 0) + SendComplexPacket(carMap); + } + + private void OnChatCommandClosestPlayer(string message, IPlayer sender) + { + string[] args = message.Split(' '); + string whisper; + + if (args.Length < 1) + { + LogWarning($"Received 'ClosestPlayer' chat command from player \"{sender.Username}\", but not enough arguments were specified. Command: {message}"); + return; + } + + if (string.IsNullOrEmpty(args[0])) + { + LogWarning($"Received 'ClosestPlayer' chat command from player \"{sender.Username}\", but the second argument is empty. Command: {message}"); + } + + var tc = GetTrainCarFromID(args[0]); + + if (tc != null) + { + // Check the distance between all players and the TrainCar + float closestSq = server.AnyPlayerSqrMag(tc.gameObject); + float closest = Mathf.Sqrt(closestSq); + + // Send a whisper back to the player who sent the command + whisper = $"The closest player to {tc.ID} is {closest:F2} metres away"; + } + else + { + whisper = $"TrainCar '{args[0]}' not found"; + } + + server.SendWhisperChatMessage(whisper, sender); + } + + private void OnChatCommandStats(string message, IPlayer sender) + { + StringBuilder whisper = new($"There {(server.PlayerCount > 1 ? "are" : "is")} {server.PlayerCount} connected player{(server.PlayerCount > 1 ? "s" : "")}:"); + + foreach (var player in server.Players) + whisper.Append($"
    \t{(player.IsHost? "" : "")}{player.Username}{(player.IsHost ? "" : "")} Id: {player.Id}, Ping: {player.Ping}{(player.IsOnCar? $", Riding {player.OccupiedCar.ID}" : "")}"); + + whisper.Append(""); + + server.SendWhisperChatMessage(whisper.ToString(), sender); + } + #endregion + + #region Chat Help Callbacks + private string OnChatCommandSendPacketHelp() + { + // this is a very basic example and a better localisation system should be used + return LocalizationManager.CurrentLanguage switch + { + "German" => "Aktiviere den Server um ein Testpaket zu senden" + + "\r\n\t\t/packet " + + "\r\n\t\t/p " + + "\r\n\t\t/packet simple L-025", + + "Italian" => "Attiva il server per inviare un pacchetto di prova" + + "\r\n\t\t/packet " + + "\r\n\t\t/p " + + "\r\n\t\t/packet simple L-025", + + _ => "Trigger server to send a test packet" + + "\r\n\t\t/packet " + + "\r\n\t\t/p " + + "\r\n\t\t/packet simple L-025", + }; + } + private string OnChatCommandSendLocoPosHelp() + { + // this is a very basic example and a better localisation system should be used + return LocalizationManager.CurrentLanguage switch + { + "German" => "Aktiviere den Server um ein Paket mit der Lokomotive und ihrer Position zu senden" + + "\r\n\t\t/locopos" + + "\r\n\t\t/lp", + + "Italian" => "Attiva il server per inviare un pacchetto complesso di auto e le loro posizioni" + + "\r\n\t\t/locopos" + + "\r\n\t\t/lp", + + _ => "Trigger server to send a complex packet of cars and their positions" + + "\r\n\t\t/locopos" + + "\r\n\t\t/lp", + }; + } + + private string OnChatCommandClosestPlayerHelp() + { + // this is a very basic example and a better localisation system should be used + return LocalizationManager.CurrentLanguage switch + { + "German" => "Aktiviere den Server um die Entfernung zwischen dem Spieler und dem nächsten Auto zu senden" + + "\r\n\t\t/closest " + + "\r\n\t\t/cd ", + + "Italian" => "Restituisce la distanza tra un dato vagone e il giocatore più vicino" + + "\r\n\t\t/closest " + + "\r\n\t\t/cd ", + + _ => "Returns the distance between a given TrainCar and the closest player" + + "\r\n\t\t/closest " + + "\r\n\t\t/cd ", + }; + } + + private string OnChatCommandStatsHelp() + { + // this is a very basic example and a better localisation system should be used + return LocalizationManager.CurrentLanguage switch + { + "German" => "Gibt die Spieleranzahl und eine Liste aller verbundenen Spieler zurück" + + "\r\n\t\t/stats", + + "Italian" => "Restituisce il conteggio dei giocatori e l'elenco di tutti i giocatori connessi" + + "\r\n\t\t/stats", + + _ => "Returns player count and list of all connected players" + + "\r\n\t\t/stats", + }; + } + + #endregion + + #region Chat Message Filters + + //simple swear filter (not intended for real use) - this is a basic example of a chat filter,but could be used for much more. + private bool OnChatMessage(ref string message, IPlayer sender) + { + string[] veryBadWords = { "poo", "loser" }; + string[] moderatelyBadWords = { "bum", "dumb" }; + + //check for very bad words - block the message entirely if found + string localMessage = message; + if (veryBadWords.Any(word => localMessage.IndexOf(word, StringComparison.OrdinalIgnoreCase) >= 0)) + { + //send a whisper back to the player + var whisper = $"Please do not swear on this server"; + server.SendWhisperChatMessage(whisper, sender); + + //block the message from being sent + return false; + } + + //check for moderately bad words - allow the message but replace with astersiks + foreach (string badWord in moderatelyBadWords) + { + var badWordstart = message.IndexOf(badWord, StringComparison.OrdinalIgnoreCase); + while (badWordstart >= 0) + { + message = message.Remove(badWordstart,badWord.Length).Insert(badWordstart, new string('*', badWord.Length)); + badWordstart = message.IndexOf(badWord, badWordstart + badWord.Length, StringComparison.OrdinalIgnoreCase); + } + } + + return true; + } + #endregion + + #region helpers + private TrainCar GetTrainCarFromID(string carId) + { + return TrainCarRegistry.Instance.logicCarToTrainCar.FirstOrDefault(kvp => kvp.Value.ID == carId).Value; + } + + private WheelArrangement GetRandomWheelArrangement() + { + var values = Enum.GetValues(typeof(WheelArrangement)); + var random = new System.Random(); + return (WheelArrangement)values.GetValue(random.Next(values.Length)); + } + #endregion + #region Logging public void LogDebug(Func resolver) From 91c33856f6a3ae20c99e3377c385aa53d07c7cb8 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 15 Jun 2025 10:36:45 +1000 Subject: [PATCH 375/521] Refactor Types --- Multiplayer/API/ClientAPIProvider.cs | 1 + Multiplayer/API/ClientPlayerWrapper.cs | 1 - Multiplayer/API/ServerAPIProvider.cs | 1 + Multiplayer/API/ServerPlayerWrapper.cs | 1 - Multiplayer/Networking/Managers/Client/NetworkClient.cs | 1 + Multiplayer/Networking/Managers/Server/NetworkServer.cs | 2 +- MultiplayerAPI/Interfaces/IClient.cs | 1 + MultiplayerAPI/Interfaces/IServer.cs | 1 + MultiplayerAPI/{Interfaces/Packets => Types}/PacketHandler.cs | 4 +++- 9 files changed, 9 insertions(+), 4 deletions(-) rename MultiplayerAPI/{Interfaces/Packets => Types}/PacketHandler.cs (92%) diff --git a/Multiplayer/API/ClientAPIProvider.cs b/Multiplayer/API/ClientAPIProvider.cs index 14ddc1ce..5925232a 100644 --- a/Multiplayer/API/ClientAPIProvider.cs +++ b/Multiplayer/API/ClientAPIProvider.cs @@ -1,5 +1,6 @@ using MPAPI.Interfaces; using MPAPI.Interfaces.Packets; +using MPAPI.Types; using Multiplayer.Components.Networking.Player; using Multiplayer.Networking.Data; using Multiplayer.Networking.Managers.Client; diff --git a/Multiplayer/API/ClientPlayerWrapper.cs b/Multiplayer/API/ClientPlayerWrapper.cs index ac5c6ab5..f96ea488 100644 --- a/Multiplayer/API/ClientPlayerWrapper.cs +++ b/Multiplayer/API/ClientPlayerWrapper.cs @@ -1,6 +1,5 @@ using MPAPI.Interfaces; using Multiplayer.Components.Networking.Player; -using System; using UnityEngine; namespace Multiplayer.API; diff --git a/Multiplayer/API/ServerAPIProvider.cs b/Multiplayer/API/ServerAPIProvider.cs index cbfe52e1..af849978 100644 --- a/Multiplayer/API/ServerAPIProvider.cs +++ b/Multiplayer/API/ServerAPIProvider.cs @@ -1,5 +1,6 @@ using MPAPI.Interfaces; using MPAPI.Interfaces.Packets; +using MPAPI.Types; using Multiplayer.Networking.Data; using Multiplayer.Networking.Managers.Server; using Multiplayer.Networking.TransportLayers; diff --git a/Multiplayer/API/ServerPlayerWrapper.cs b/Multiplayer/API/ServerPlayerWrapper.cs index d15ce381..d5d7c444 100644 --- a/Multiplayer/API/ServerPlayerWrapper.cs +++ b/Multiplayer/API/ServerPlayerWrapper.cs @@ -3,7 +3,6 @@ using Multiplayer.Components.Networking.Train; using Multiplayer.Networking.Data; using Multiplayer.Networking.TransportLayers; -using System; using UnityEngine; namespace Multiplayer.API; diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index c8eb60da..c9700883 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -13,6 +13,7 @@ using LiteNetLib; using LiteNetLib.Utils; using MPAPI.Interfaces.Packets; +using MPAPI.Types; using Multiplayer.API; using Multiplayer.Components.MainMenu; using Multiplayer.Components.Networking; diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index 5981c6d7..d64ab343 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -8,8 +8,8 @@ using Humanizer; using LiteNetLib; using LiteNetLib.Utils; -using MPAPI.Interfaces; using MPAPI.Interfaces.Packets; +using MPAPI.Types; using Multiplayer.API; using Multiplayer.Components.Networking; using Multiplayer.Components.Networking.Jobs; diff --git a/MultiplayerAPI/Interfaces/IClient.cs b/MultiplayerAPI/Interfaces/IClient.cs index 7364ab7e..15f3da4d 100644 --- a/MultiplayerAPI/Interfaces/IClient.cs +++ b/MultiplayerAPI/Interfaces/IClient.cs @@ -1,4 +1,5 @@ using MPAPI.Interfaces.Packets; +using MPAPI.Types; using System; using System.Collections.Generic; diff --git a/MultiplayerAPI/Interfaces/IServer.cs b/MultiplayerAPI/Interfaces/IServer.cs index 97fd239a..d2bbe3d9 100644 --- a/MultiplayerAPI/Interfaces/IServer.cs +++ b/MultiplayerAPI/Interfaces/IServer.cs @@ -1,4 +1,5 @@ using MPAPI.Interfaces.Packets; +using MPAPI.Types; using System; using System.Collections.Generic; using UnityEngine; diff --git a/MultiplayerAPI/Interfaces/Packets/PacketHandler.cs b/MultiplayerAPI/Types/PacketHandler.cs similarity index 92% rename from MultiplayerAPI/Interfaces/Packets/PacketHandler.cs rename to MultiplayerAPI/Types/PacketHandler.cs index 06617924..c95f1520 100644 --- a/MultiplayerAPI/Interfaces/Packets/PacketHandler.cs +++ b/MultiplayerAPI/Types/PacketHandler.cs @@ -1,4 +1,6 @@ -namespace MPAPI.Interfaces.Packets; +using MPAPI.Interfaces; + +namespace MPAPI.Types; /// /// Delegate for handling received packets on the server From 23eedef974857f9b1d64af7371becfc20862de90 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 15 Jun 2025 19:11:30 +1000 Subject: [PATCH 376/521] Fix lever blocking --- .../Networking/World/NetworkedPitStopStation.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs b/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs index 6815ef01..41d94370 100644 --- a/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs +++ b/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs @@ -1,3 +1,4 @@ +using DV.CabControls.NonVR; using DV.Interaction; using DV.ThingTypes; using Multiplayer.Networking.Data; @@ -98,6 +99,7 @@ public static void InitialisePitStops() private readonly Dictionary leverHandler)> leverStateLookup = []; private readonly Dictionary grabbedHandlerLookup = []; + private readonly Dictionary leverLookup = []; private readonly Dictionary resourceToPluggableObject = []; private readonly Dictionary resourceTypeToLocoResourceModule = []; @@ -183,6 +185,7 @@ protected override void OnDestroy() leverStateLookup.Clear(); grabbedHandlerLookup.Clear(); + leverLookup.Clear(); base.OnDestroy(); } @@ -511,6 +514,7 @@ private IEnumerator Init() var checker = resourceModule.GetComponentInChildren(); var grab = resourceModule.GetComponentInChildren(); + var lever = resourceModule.GetComponentInChildren(); if (checker != null && grab != null) { @@ -524,6 +528,9 @@ private IEnumerator Init() leverStateLookup[resourceModule.resourceType] = (checker, resourceModule, LeverStatehandler); grabbedHandlerLookup[resourceModule.resourceType] = grab; + if(lever != null) + leverLookup[resourceModule.resourceType] = lever; + //sb.AppendLine($"\t{resourceModule.resourceType}, Grab Handler found: {grab != null}, Name: {grab.name}"); sb.AppendLine($"\t{resourceModule.resourceType}, Rotary Amplitude Handler found: {checker != null}, Name: {checker.name}"); } @@ -813,6 +820,7 @@ public void ProcessInteractionPacketAsClient(CommonPitStopInteractionPacket pack { GrabHandlerHingeJoint grab = null; RotaryAmplitudeChecker amplitudeChecker = null; + LeverNonVR lever = null; // Validate interaction type if (!Enum.IsDefined(typeof(PitStopStationInteractionType), packet.InteractionType)) @@ -848,6 +856,7 @@ public void ProcessInteractionPacketAsClient(CommonPitStopInteractionPacket pack break; case PitStopStationInteractionType.LeverState: + leverLookup.TryGetValue(resourceType, out lever); if (!grabbedHandlerLookup.TryGetValue(resourceType, out grab)) { @@ -878,7 +887,10 @@ public void ProcessInteractionPacketAsClient(CommonPitStopInteractionPacket pack if (!isLocallyGrabbed) { + + lever?.BlockControl(grabbed); grab?.SetMovingDisabled(grabbed); + if (grabbed) grab?.ForceEndInteraction(); } From e0d2dded66863f7e6806993d8d29cf476c4138a7 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 15 Jun 2025 19:12:07 +1000 Subject: [PATCH 377/521] Fix merge error --- .../Networking/Managers/Client/NetworkClient.cs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 82cc9a81..97900a20 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -200,18 +200,6 @@ private void OnLoaded() WorldStreamingInit.LoadingFinished -= OnLoaded; } - private void OnLoaded() - { - Log($"WorldStreamingInit.LoadingFinished()"); - NetworkedItemManager.Instance.CheckInstance(); - Log($"WorldStreamingInit.LoadingFinished() CacheWorldItems()"); - NetworkedItemManager.Instance.CacheWorldItems(); - Log($"WorldStreamingInit.LoadingFinished() SendReadyPacket()"); - SendReadyPacket(); - - WorldStreamingInit.LoadingFinished -= OnLoaded; - } - #region Net Events public override void OnPeerConnected(ITransportPeer peer) From 47e4dfcd4f9f7bd7c465bb1d6dd6614999dbb461 Mon Sep 17 00:00:00 2001 From: AMacro Date: Tue, 17 Jun 2025 22:13:34 +1000 Subject: [PATCH 378/521] Change value sync method Rather than using late updates and relying on the client to send the value, use the fill/drain started and stopped events. set a flag when filling/draining starts and then on server tick, communicate the latest fill value for all resources filling or draining. Upon release clear the flag and send the final value. Server side tracking of which resources are being interacted with is required for updating players entering the culling zone --- .../World/NetworkedPitStopStation.cs | 192 ++++++++---------- .../World/CashRegisterWithModulesPatch.cs | 5 + 2 files changed, 88 insertions(+), 109 deletions(-) diff --git a/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs b/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs index 41d94370..4824bd28 100644 --- a/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs +++ b/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs @@ -82,6 +82,9 @@ public static void InitialisePitStops() private float disablerSqrDistance = DEFAULT_DISABLER_SQR_DISTANCE; private float disablerCheckInterval = DEFAULT_DISABLER_INTERVAL; + private readonly Dictionary resourceStartStopDelegates = []; + private readonly Dictionary resourceFlowing = []; + private bool processingAsHost = false; #endregion @@ -104,20 +107,15 @@ public static void InitialisePitStops() private readonly Dictionary resourceTypeToLocoResourceModule = []; private readonly Dictionary isResourceGrabbedDict = []; - private readonly Dictionary wasResourceGrabbedDict = []; private readonly Dictionary isResourceRemoteGrabbedDict = []; - private readonly Dictionary wasResourceRemoteGrabbedDict = []; private readonly Dictionary lastRemoteValueDict = []; - private float lastUpdateTime = 0.0f; - + private bool isFaucetGrabbed = false; private float lastFaucetUpdateTime = 0.0f; private float lastFaucetSent = 0.0f; private float faucetTargetPercentage = 0.0f; private bool faucetTargetReached = true; - private readonly Dictionary lastUnitsToBuyDict = []; - private bool Refreshed = false; #endregion @@ -142,6 +140,7 @@ protected override void Awake() StartCoroutine(PlayerDistanceChecker()); + NetworkLifecycle.Instance.OnTick += OnTick; //ensure host can interact Refreshed = true; } @@ -158,8 +157,21 @@ protected override void OnDestroy() pitStopStationToNetworkedPitStopStation.Remove(Station); if (NetworkLifecycle.Instance.IsHost()) + { StopCoroutine(PlayerDistanceChecker()); + foreach (var kvp in resourceStartStopDelegates) + { + var (fillStart, fillStop, drainStart, drainStop) = kvp.Value; + kvp.Key.FillStarted -= fillStart; + kvp.Key.FillStopped -= fillStop; + kvp.Key.DrainStarted -= drainStart; + kvp.Key.DrainStopped -= drainStop; + } + + resourceStartStopDelegates.Clear(); + } + if (carSelectorGrab != null) { carSelectorGrab.Grabbed -= CarSelectorGrabbed; @@ -189,87 +201,6 @@ protected override void OnDestroy() base.OnDestroy(); } - protected void LateUpdate() - { - foreach (var resourceType in resourceTypes) - { - var module = Station.locoResourceModules.resourceModules.FirstOrDefault(x => x.resourceType == resourceType); - - if (module == null - || !isResourceGrabbedDict.TryGetValue(resourceType, out var isResourceGrabbed) - || !wasResourceGrabbedDict.TryGetValue(resourceType, out var wasResourceGrabbed) - || !isResourceRemoteGrabbedDict.TryGetValue(resourceType, out var isResourceRemoteGrabbed) - || !wasResourceRemoteGrabbedDict.TryGetValue(resourceType, out var wasResourceRemoteGrabbed) - || !lastRemoteValueDict.TryGetValue(resourceType, out var lastRemoteValue) - || !lastUnitsToBuyDict.TryGetValue(resourceType, out var lastUnitsToBuy) - ) - continue; - - //Handle local grab interactions - if (isResourceGrabbed || (wasResourceGrabbed && lastUnitsToBuy != module.Data.unitsToBuy)) - { - //ensure the delta is big enough to be worth sending or we have reached a limit - var delta = Math.Abs(lastUnitsToBuy - module.Data.unitsToBuy); - var deltaTime = Time.time - lastUpdateTime; - - //Check if the units to buy have reached a limit (0 or AbsoluteMaxValue), as this overrides a delta below minimum - var unitsToBuyChanged = - (module.Data.unitsToBuy == module.AbsoluteMinValue && lastUnitsToBuy != module.AbsoluteMinValue) - || (module.Data.unitsToBuy == module.AbsoluteMaxValue && lastUnitsToBuy != module.AbsoluteMaxValue); - - //Send the update if we've passed the time threshold AND we have a big enough change or hit a limit - if (deltaTime > MIN_UPDATE_TIME && (delta > MAX_DELTA || unitsToBuyChanged)) - { - lastUnitsToBuyDict[resourceType] = module.Data.unitsToBuy; - lastUpdateTime = Time.time; - - Multiplayer.LogDebug(() => $"NetworkedPitStopStation.LateUpdate() isGrabbed: {isResourceGrabbed}, wasGrabbed: {wasResourceGrabbed}, previous: {lastUnitsToBuy}, new: {module.Data.unitsToBuy}, processingAsHost: {processingAsHost}. Sending PitstopInteractionPacket ResourceUpdate "); - - if (!(NetworkLifecycle.Instance.IsHost() && processingAsHost)) - NetworkLifecycle.Instance?.Client.SendPitStopInteractionPacket( - NetId, - PitStopStationInteractionType.ResourceUpdate, - resourceType, - lastUnitsToBuyDict[resourceType] - ); - } - } - //Local grab has ended, but needs to be finalised - else if (wasResourceGrabbed) - { - Multiplayer.LogDebug(() => $"NetworkedPitStopStation.LateUpdate() wasGrabbed: {wasResourceGrabbed}, previous: {lastUnitsToBuy}, new: {module.Data.unitsToBuy}"); - lastUnitsToBuyDict[resourceType] = module.Data.unitsToBuy; - - if (!(NetworkLifecycle.Instance.IsHost() && processingAsHost)) - NetworkLifecycle.Instance?.Client.SendPitStopInteractionPacket( - NetId, - PitStopStationInteractionType.ResourceUpdate, - resourceType, - lastUnitsToBuyDict[resourceType] - ); - - //Reset grab states - wasResourceGrabbedDict[resourceType] = false; - } - - //allow things to settle after remote grab released - if (!isResourceRemoteGrabbed && wasResourceRemoteGrabbed) - { - float previous = module.Data.unitsToBuy; - - SetUnits(module, lastRemoteValue); - - Multiplayer.LogDebug(() => $"NetworkedPitStopStation.LateUpdate() wasRemoteGrabbed: {wasResourceRemoteGrabbed}, previous: {previous}, new: {lastRemoteValue}"); - - if (previous == lastRemoteValue) - { - //settled, stop tracking remote - wasResourceRemoteGrabbedDict[resourceType] = false; - } - } - } - } - protected void Update() { var deltaTime = Time.time - lastFaucetUpdateTime; @@ -407,7 +338,6 @@ public void ProcessInteractionPacketAsHost(CommonPitStopInteractionPacket packet { Multiplayer.LogDebug(() => $"ProcessInteractionPacketAsHost() ProcessPacketAsClient()"); ProcessInteractionPacketAsClient(packet); - LateUpdate(); } processingAsHost = false; //Send to all other players @@ -434,6 +364,50 @@ public void ProcessInteractionPacketAsHost(CommonPitStopInteractionPacket packet ); } } + + private void OnFlowStarted(LocoResourceModule module) + { + resourceFlowing[module] = true; + } + + private void OnFlowStopped(LocoResourceModule module) + { + resourceFlowing[module] = false; + SendResourceUpdate(module); + } + + private void OnTick(uint tick) + { + foreach (var kvp in resourceFlowing) + { + if (!kvp.Value) + continue; + + var module = kvp.Key; + + SendResourceUpdate(module); + } + } + + private void SendResourceUpdate(LocoResourceModule module) + { + CommonPitStopInteractionPacket packet = new() + { + NetId = this.NetId, + InteractionType = (byte)PitStopStationInteractionType.ResourceUpdate, + ResourceType = (int)module.resourceType, + Value = module.Data.unitsToBuy + }; + + foreach (var playerId in playerToLastNearbyTime.Keys) + { + if (NetworkLifecycle.Instance.Server.TryGetPeer(playerId, out var sendPeer)) + { + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.SendResourceUpdate({module.resourceType}) sending to peer: {sendPeer.Id}, value: {module.Data.unitsToBuy}, flowing: {module.IsFlowing}"); + NetworkLifecycle.Instance.Server.SendPitStopInteractionPacket(sendPeer, packet); + } + } + } #endregion @@ -494,11 +468,8 @@ private IEnumerator Init() foreach (var resourceType in resourceTypes) { isResourceGrabbedDict[resourceType] = false; - wasResourceGrabbedDict[resourceType] = false; isResourceRemoteGrabbedDict[resourceType] = false; - wasResourceRemoteGrabbedDict[resourceType] = false; lastRemoteValueDict[resourceType] = 0.0f; - lastUnitsToBuyDict[resourceType] = 0.0f; } StringBuilder sb = new(); @@ -512,6 +483,22 @@ private IEnumerator Init() resourceTypeToLocoResourceModule[resourceModule.resourceType] = resourceModule; + //subscribe to fill/drain stop events + if (NetworkLifecycle.Instance.IsHost()) + { + void FillStartHandler() => OnFlowStarted(resourceModule); + void FillStopHandler() => OnFlowStopped(resourceModule); + void DrainStartHandler() => OnFlowStarted(resourceModule); + void DrainStopHandler() => OnFlowStopped(resourceModule); + + resourceModule.FillStarted += FillStartHandler; + resourceModule.FillStopped += FillStopHandler; + resourceModule.DrainStarted += DrainStartHandler; + resourceModule.DrainStopped += DrainStopHandler; + + resourceStartStopDelegates[resourceModule] = (FillStartHandler, FillStopHandler, DrainStartHandler, DrainStopHandler); + } + var checker = resourceModule.GetComponentInChildren(); var grab = resourceModule.GetComponentInChildren(); var lever = resourceModule.GetComponentInChildren(); @@ -719,13 +706,10 @@ private void OnLeverPositionChange(LocoResourceModule module, int state) { //lever returned home isResourceGrabbedDict[module.resourceType] = false; - wasResourceGrabbedDict[module.resourceType] = true; } else { isResourceGrabbedDict[module.resourceType] = true; - wasResourceGrabbedDict[module.resourceType] = true; - lastUnitsToBuyDict[module.resourceType] = module.Data.unitsToBuy; } NetworkLifecycle.Instance?.Client.SendPitStopInteractionPacket(NetId, PitStopStationInteractionType.LeverState, module.resourceType, state); @@ -887,7 +871,6 @@ public void ProcessInteractionPacketAsClient(CommonPitStopInteractionPacket pack if (!isLocallyGrabbed) { - lever?.BlockControl(grabbed); grab?.SetMovingDisabled(grabbed); @@ -895,20 +878,18 @@ public void ProcessInteractionPacketAsClient(CommonPitStopInteractionPacket pack grab?.ForceEndInteraction(); } - Multiplayer.LogDebug(() => $"NetworkedPitStopStation.ProcessPacket() [{StationName}, {NetId}] {interactionType}, resource type: {resourceType}, state: {packet.Value}, grabbed: {grabbed}, resourceModule: {resourceModule != null}, isResourceRemoteGrabbed: {isResourceRemoteGrabbedDict[resourceType]}, wasResourceRemoteGrabbed: {wasResourceRemoteGrabbedDict[resourceType]}"); + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.ProcessPacket() [{StationName}, {NetId}] {interactionType}, resource type: {resourceType}, state: {packet.Value}, grabbed: {grabbed}, resourceModule: {resourceModule != null}, isResourceRemoteGrabbed: {isResourceRemoteGrabbedDict[resourceType]}"); resourceModule.OnValvePositionChange((int)packet.Value); // Update remote grab state isResourceRemoteGrabbedDict[resourceType] = grabbed; - if (grabbed) - wasResourceRemoteGrabbedDict[resourceType] = true; break; case PitStopStationInteractionType.ResourceUpdate: - Multiplayer.LogDebug(() => $"NetworkedPitStopStation.ProcessPacket() [{StationName}, {NetId}] {interactionType}, resource type: {resourceType}, state: {packet.Value}, resourceModule: {resourceModule != null}, isResourceRemoteGrabbed: {isResourceRemoteGrabbedDict[resourceType]}, wasResourceRemoteGrabbed: {wasResourceRemoteGrabbedDict[resourceType]}"); + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.ProcessPacket() [{StationName}, {NetId}] {interactionType}, resource type: {resourceType}, state: {packet.Value}, resourceModule: {resourceModule != null}, isResourceRemoteGrabbed: {isResourceRemoteGrabbedDict[resourceType]}"); // Validate the value range if (packet.Value < resourceModule.AbsoluteMinValue || packet.Value > resourceModule.AbsoluteMaxValue) @@ -917,16 +898,9 @@ public void ProcessInteractionPacketAsClient(CommonPitStopInteractionPacket pack return; } - if (isResourceRemoteGrabbedDict[resourceType]) - { - lastRemoteValueDict[resourceType] = packet.Value; - SetUnits(resourceModule, lastRemoteValueDict[resourceType]); - Multiplayer.LogDebug(() => $"NetworkedPitStopStation.ProcessPacket() [{StationName}, {NetId}] {interactionType}, resource type: {resourceType}, state: {packet.Value}. SetUnits()"); - } - else if (wasResourceRemoteGrabbedDict[resourceType]) - { - lastRemoteValueDict[resourceType] = packet.Value; - } + lastRemoteValueDict[resourceType] = packet.Value; + SetUnits(resourceModule, lastRemoteValueDict[resourceType]); + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.ProcessPacket() [{StationName}, {NetId}] {interactionType}, resource type: {resourceType}, state: {packet.Value}, flowing: {resourceModule.IsFlowing}"); break; diff --git a/Multiplayer/Patches/World/CashRegisterWithModulesPatch.cs b/Multiplayer/Patches/World/CashRegisterWithModulesPatch.cs index 82cb48c3..271d4fc6 100644 --- a/Multiplayer/Patches/World/CashRegisterWithModulesPatch.cs +++ b/Multiplayer/Patches/World/CashRegisterWithModulesPatch.cs @@ -4,6 +4,7 @@ using Multiplayer.Components.Networking.World; using Multiplayer.Networking.Packets.Common; using Multiplayer.Utils; +using System; namespace Multiplayer.Patches.World; @@ -54,6 +55,9 @@ private static void OnBuyPressed_Postfix(CashRegisterWithModules __instance) [HarmonyPatch(nameof(CashRegisterWithModules.Cancel))] private static bool Cancel(CashRegisterWithModules __instance) { + + Multiplayer.LogDebug(()=>$"CashRegisterWithModules.Cancel({__instance.GetObjectPath()})\r\n{Environment.StackTrace}"); + if (NetworkLifecycle.Instance.IsHost()) return true; @@ -128,6 +132,7 @@ private static bool OnDisable(CashRegisterBase __instance) return true; //prevent clients from clearing cash registers when loading the game or leaving the area + __instance.StopAllCoroutines(); return NetworkLifecycle.Instance.IsHost(); } } From c86486016526c168dcf6bd6125ee02286223e05f Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 21 Jun 2025 18:05:36 +1000 Subject: [PATCH 379/521] Implement mod compatibility identification Allows mods to report their compatibility with multiplayer either through their `info.json` or through a call to `MultiplayerAPI.Instance.SetModCompatibility()` --- Multiplayer/API/APIProvider.cs | 7 +- Multiplayer/API/ModCompatibilityManager.cs | 208 ++++++++++++++++++ .../Components/MainMenu/HostGamePane.cs | 33 +-- .../Components/MainMenu/ServerBrowserPane.cs | 25 ++- Multiplayer/Locale.cs | 3 + Multiplayer/Multiplayer.cs | 9 +- .../Managers/Client/NetworkClient.cs | 12 +- .../Managers/Server/NetworkServer.cs | 18 +- .../ClientboundLoginResponsePacket.cs | 4 +- .../ServerboundClientLoginPacket.cs | 2 +- MultiplayerAPI/Interfaces/IMultiplayerAPI.cs | 7 + .../Types/MultiplayerCompatibility.cs | 41 ++++ locale.csv | 1 + 13 files changed, 326 insertions(+), 44 deletions(-) create mode 100644 Multiplayer/API/ModCompatibilityManager.cs create mode 100644 MultiplayerAPI/Types/MultiplayerCompatibility.cs diff --git a/Multiplayer/API/APIProvider.cs b/Multiplayer/API/APIProvider.cs index e170522b..712beda3 100644 --- a/Multiplayer/API/APIProvider.cs +++ b/Multiplayer/API/APIProvider.cs @@ -1,4 +1,5 @@ using MPAPI.Interfaces; +using MPAPI.Types; using Multiplayer.Components.Networking; @@ -6,7 +7,6 @@ namespace Multiplayer.API { public class APIProvider : IMultiplayerAPI { - public bool IsMultiplayerLoaded => true; public bool IsConnected => NetworkLifecycle.Instance.IsClientRunning || NetworkLifecycle.Instance.IsServerRunning; @@ -26,5 +26,10 @@ public bool TryGetObjectFromNetId(ushort netId, out T obj) where T : class { return NetIdProvider.Instance.TryGetObject(netId, out obj); } + + public void SetModCompatibility(string modId, MultiplayerCompatibility compatibility) + { + ModCompatibilityManager.Instance.RegisterCompatibility(modId, compatibility); + } } } diff --git a/Multiplayer/API/ModCompatibilityManager.cs b/Multiplayer/API/ModCompatibilityManager.cs new file mode 100644 index 00000000..aff50311 --- /dev/null +++ b/Multiplayer/API/ModCompatibilityManager.cs @@ -0,0 +1,208 @@ +using DV.JObjectExtstensions; +using DV.Utils; +using JetBrains.Annotations; +using MPAPI.Types; +using Multiplayer.Components.MainMenu; +using Multiplayer.Networking.Data; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using UnityModManagerNet; + +namespace Multiplayer.API; + +public class ModCompatibilityManager : SingletonBehaviour +{ + private const string JSON_FILE = "info.json"; + private const string JSON_MP_COMPAT_KEY = "MultiplayerCompatibility"; + private static readonly Dictionary _modCompatibility = []; + + protected override void Awake() + { + base.Awake(); + + DontDestroyOnLoad(this); + + //we don't care if the client does/doesn't have these mods + RegisterCompatibility("RuntimeUnityEditor", MultiplayerCompatibility.Client); + RegisterCompatibility("BookletOrganizer", MultiplayerCompatibility.Client); + RegisterCompatibility("RemoteDispatch", MultiplayerCompatibility.Client); + RegisterCompatibility("DVDiscordPresenceMod", MultiplayerCompatibility.Client); + + //Json entries will override hardcoded entries + ReadModJsons(); + + //Hardcoded and json entries will be overridden by API calls + } + + public void RegisterCompatibility(string modId, MultiplayerCompatibility compatibility) + { + Multiplayer.LogDebug(() => $"RegisterCompatibility({modId}, {compatibility})"); + + if (!string.IsNullOrEmpty(modId)) + _modCompatibility[modId] = compatibility; + } + + private void ReadModJsons() + { + foreach (var modEntry in UnityModManager.modEntries) + { + var jsonPath = Path.Combine(modEntry.Path, JSON_FILE); + + if (File.Exists(jsonPath)) + { + try + { + var json = File.ReadAllText(jsonPath); + var jobj = JObject.Parse(json); + var compatStr = jobj.GetString(JSON_MP_COMPAT_KEY); + + var parsed = Enum.TryParse(compatStr, out MultiplayerCompatibility compatibility); + + Multiplayer.LogDebug(() => $"Mod '{modEntry.Info.DisplayName}' ({modEntry.Info.Id}) has MP mod compatibility entry \'{compatStr}\', parses to: {compatibility}"); + + if (parsed) + RegisterCompatibility(modEntry.Info.Id, compatibility); + } + catch (Exception e) + { + Multiplayer.LogException($"Failed to parse mod entry {modEntry.Info.Id}", e); + } + } + else + { + Multiplayer.LogWarning($"No json found for {modEntry.Info.Id}"); + } + } + } + + public bool TryGetCompatibility(string modId, out MultiplayerCompatibility compatibility) + { + return _modCompatibility.TryGetValue(modId, out compatibility); + } + + public MultiplayerCompatibility GetCompatibility(ModInfo mod) + { + if (TryGetCompatibility(mod.Id, out var compatibility)) + return compatibility; + + return MultiplayerCompatibility.Undefined; + } + + public ModValidationResult ValidateClientMods(ModInfo[] clientMods) + { + var localMods = GetLocalMods(); + var localModIds = localMods.Select(l => l.Id); + + var clientModIds = clientMods.Select(c => c.Id); + + List missing = clientMods.Where(c => !localModIds.Contains(c.Id)).ToList(); + List extra = localMods.Where(l => !clientModIds.Contains(l.Id)).ToList(); + + bool valid = (missing.Count == 0) && (extra.Count == 0); + + return new() + { + IsValid = valid, + Missing = missing, + Extra = extra + }; + } + + /// + /// Checks if any incompatible mods are enabled and generates an message box to alert the user + /// + /// True if incompatible mods have been found + public bool CheckModCompatibility() + { + List incompatible = []; + + foreach (var modInfo in ModInfo.FromModEntries(UnityModManager.modEntries)) + { + if (TryGetCompatibility(modInfo.Id, out var compatibility)) + { + if (compatibility == MultiplayerCompatibility.Incompatible) + incompatible.Add(modInfo.Id); + } + } + + if (incompatible.Count == 0) + return false; + + var message = $"{Locale.MAIN_MENU__INCOMPATIBLE_MODS} {string.Join(", ", incompatible)}"; + + MainMenuThingsAndStuff.Instance.ShowOkPopup(message, () => { }); + + return true; + } + + // Returns a list of mods for use in the lobby data + public string GetRequiredMods() + { + List requiredMods = []; + + var local = GetLocalMods(); + + if (local == null) + return null; + + foreach (var modInfo in local) + requiredMods.Add(modInfo.Id); + + return string.Join(", ", requiredMods); + } + + //Returns a list of mods installed and enabled, filtered for mods that are required for hosts and clients + public ModInfo[] GetLocalMods() + { + List localMods = []; + + foreach (var modInfo in ModInfo.FromModEntries(UnityModManager.modEntries)) + { + if (TryGetCompatibility(modInfo.Id, out var compatibility)) + { + // Only include mods that are relevant for client validation + switch (compatibility) + { + case MultiplayerCompatibility.Undefined: + case MultiplayerCompatibility.All: + //undefined and "All" mods are required by all clients + localMods.Add(modInfo); + break; + + case MultiplayerCompatibility.Incompatible: + //There shouldn't be any at this stage + return null; + + case MultiplayerCompatibility.Host: + case MultiplayerCompatibility.Client: + // Not required, should have no impact on game play + break; + } + } + else + { + // No compatibility info - include for validation (safe default) + Multiplayer.LogWarning($"No compatibility info for mod {modInfo.Id}, including in validation"); + localMods.Add(modInfo); + } + } + return localMods.ToArray(); + + } + + [UsedImplicitly] + public new static string AllowAutoCreate() + { + return $"[{nameof(ModCompatibilityManager)}]"; + } +} + +public class ModValidationResult +{ + public bool IsValid { get; set; } + public List Missing { get; set; } = []; + public List Extra { get; set; } = []; +} diff --git a/Multiplayer/Components/MainMenu/HostGamePane.cs b/Multiplayer/Components/MainMenu/HostGamePane.cs index 903e543f..88af8b0e 100644 --- a/Multiplayer/Components/MainMenu/HostGamePane.cs +++ b/Multiplayer/Components/MainMenu/HostGamePane.cs @@ -8,7 +8,6 @@ using Multiplayer.Components.Networking; using Multiplayer.Components.Util; using Multiplayer.Networking.Data; -using Multiplayer.Networking.Managers.Server; using Multiplayer.Utils; using System.Linq; using System.Reflection; @@ -17,7 +16,7 @@ using UnityEngine.Events; using UnityEngine.UI; using UnityEngine; -using UnityModManagerNet; +using Multiplayer.API; namespace Multiplayer.Components.MainMenu; @@ -51,6 +50,8 @@ public class HostGamePane : MonoBehaviour LauncherController lcInstance; public Action continueCareerRequested; + + private bool incompatibleMods = true; #region setup public void Awake() @@ -77,6 +78,9 @@ public void OnEnable() { //Multiplayer.Log("HostGamePane OnEnable()"); this.SetupListeners(true); + + incompatibleMods = ModCompatibilityManager.Instance.CheckModCompatibility(); + ValidateInputs(null); } // Disable listeners @@ -335,7 +339,7 @@ private void BuildUI() startButton = go.GetComponent(); startButton.onClick.RemoveAllListeners(); - startButton.onClick.AddListener(StartClick); + startButton.onClick.AddListener(OnStartClick); } private GameObject NewContentGroup(GameObject parent, Vector2 sizeDelta, int cellMaxHeight = 53) @@ -383,10 +387,12 @@ private void SetupListeners(bool on) #endregion #region UI callbacks - private void ValidateInputs(string text) + private void ValidateInputs(string _) { bool valid = true; + if (incompatibleMods) + valid = false; if (!DVSteamworks.Success) valid = false; @@ -406,7 +412,7 @@ private void ValidateInputs(string text) startButton.ToggleInteractable(valid); } - private void StartClick() + private void OnStartClick() { using (LobbyServerData serverData = new()) @@ -423,17 +429,18 @@ private void StartClick() 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(); - - string requiredMods = ""; - if (serverMods.Length > 0) + // final check before we start the server + string requiredMods = ModCompatibilityManager.Instance.GetRequiredMods(); + if (requiredMods == null) { - requiredMods = string.Join(", ", serverMods.Select(mod => $"{{{mod.Id}, {mod.Version}}}")); + + incompatibleMods = true; + ValidateInputs(null); + return; } - serverData.RequiredMods = requiredMods; //FIX THIS - get the mods required - serverData.GameVersion = BuildInfo.BUILD_VERSION_MAJOR.ToString(); + serverData.RequiredMods = requiredMods; + serverData.GameVersion = Multiplayer.LocalBuildInfo; serverData.MultiplayerVersion = Multiplayer.Ver; serverData.ServerDetails = details.text.Trim(); diff --git a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs index 9b355aa0..6000d18e 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs @@ -1,26 +1,27 @@ +using DV; using DV.Localization; using DV.Platform.Steam; using DV.UI; using DV.UIFramework; using DV.Utils; -using DV; using LiteNetLib; +using Multiplayer.API; using Multiplayer.Components.MainMenu.ServerBrowser; using Multiplayer.Components.Networking; using Multiplayer.Components.UI.Controls; using Multiplayer.Networking.Data; using Multiplayer.Utils; -using Steamworks.Data; using Steamworks; -using System.Collections.Generic; +using Steamworks.Data; +using System; using System.Collections; +using System.Collections.Generic; using System.Linq; using System.Net; using System.Threading.Tasks; -using System; using TMPro; -using UnityEngine.UI; using UnityEngine; +using UnityEngine.UI; namespace Multiplayer.Components.MainMenu { @@ -85,6 +86,7 @@ private enum ConnectionState private Lobby[] lobbies; + private bool incompatibleMods = true; #region setup @@ -102,6 +104,9 @@ public void Awake() public void OnEnable() { + //ensure no incompatible mods are loaded + incompatibleMods = ModCompatibilityManager.Instance.CheckModCompatibility(); + this.SetupListeners(true); buttonDirectIP.ToggleInteractable(true); @@ -419,16 +424,16 @@ private void OnSelectedIndexChanged(MPGridView gridVi return; selectedServer = gridView.SelectedItem; - if (selectedServer != null) + if (selectedServer != null && incompatibleMods == false) { UpdateDetailsPane(); //Check if we can connect to this server Multiplayer.Log($"Server: \"{selectedServer.GameVersion}\" \"{selectedServer.MultiplayerVersion}\""); - Multiplayer.Log($"Client: \"{BuildInfo.BUILD_VERSION_MAJOR}\" \"{Multiplayer.Ver}\""); - Multiplayer.Log($"Result: \"{selectedServer.GameVersion == BuildInfo.BUILD_VERSION_MAJOR.ToString()}\" \"{selectedServer.MultiplayerVersion == Multiplayer.Ver}\""); + Multiplayer.Log($"Client: \"{Multiplayer.LocalBuildInfo}\" \"{Multiplayer.Ver}\""); + Multiplayer.Log($"Result: \"{selectedServer.GameVersion == Multiplayer.LocalBuildInfo}\" \"{selectedServer.MultiplayerVersion == Multiplayer.Ver}\""); - bool canConnect = selectedServer.GameVersion == BuildInfo.BUILD_VERSION_MAJOR.ToString() && + bool canConnect = selectedServer.GameVersion == Multiplayer.LocalBuildInfo && selectedServer.MultiplayerVersion == Multiplayer.Ver; buttonJoin.ToggleInteractable(canConnect); @@ -469,7 +474,7 @@ private void UpdateDetailsPane() 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) + "
    "; details += "
    "; - details += "" + Locale.SERVER_BROWSER__GAME_VERSION + ": " + (selectedServer.GameVersion != BuildInfo.BUILD_VERSION_MAJOR.ToString() ? "" : "") + selectedServer.GameVersion + "
    "; + details += "" + Locale.SERVER_BROWSER__GAME_VERSION + ": " + (selectedServer.GameVersion != Multiplayer.LocalBuildInfo ? "" : "") + selectedServer.GameVersion + "
    "; details += "" + Locale.SERVER_BROWSER__MOD_VERSION + ": " + (selectedServer.MultiplayerVersion != Multiplayer.Ver ? "" : "") + selectedServer.MultiplayerVersion + "
    "; details += "
    "; details += selectedServer.ServerDetails; diff --git a/Multiplayer/Locale.cs b/Multiplayer/Locale.cs index 87065e14..62f4f2ab 100644 --- a/Multiplayer/Locale.cs +++ b/Multiplayer/Locale.cs @@ -28,6 +28,9 @@ public static class Locale #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"; + + public static string MAIN_MENU__INCOMPATIBLE_MODS => Get(MAIN_MENU__INCOMPATIBLE_MODS_KEY); + public const string MAIN_MENU__INCOMPATIBLE_MODS_KEY = $"{PREFIX_MAIN_MENU}/incompatible_mods"; #endregion #region Server Browser diff --git a/Multiplayer/Multiplayer.cs b/Multiplayer/Multiplayer.cs index 47601621..d7e7168a 100644 --- a/Multiplayer/Multiplayer.cs +++ b/Multiplayer/Multiplayer.cs @@ -42,7 +42,9 @@ public static string Ver { return info.InformationalVersion.Split('+')[0]; } } - + + public static string LocalBuildInfo => BuildInfo.BUILD_VERSION_MAJOR.ToString() + " - " + BuildInfo.BUILDBOT_INFO; + public static bool specLog = false; @@ -102,9 +104,12 @@ public static bool Load(UnityModManager.ModEntry modEntry) Log("Creating NetworkManager..."); NetworkLifecycle.CreateLifecycle(); + Log("Loading Compatibility Manager..."); + ModCompatibilityManager.Instance.CheckInstance(); + Log("Loading API Provider..."); _apiProvider = new APIProvider(); - MPAPI.MultiplayerAPI.RegisterAPI(_apiProvider); + MultiplayerAPI.RegisterAPI(_apiProvider); } catch (Exception ex) { diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index c9700883..e2aaabdb 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -42,7 +42,6 @@ using System.Collections.Generic; using System.Linq; using UnityEngine; -using UnityModManagerNet; using Object = UnityEngine.Object; namespace Multiplayer.Networking.Managers.Client; @@ -86,8 +85,8 @@ public void Start(string address, int port, string password, bool isSinglePlayer Username = Multiplayer.Settings.GetUserName(), Guid = Multiplayer.Settings.GetGuid().ToByteArray(), Password = password, - BuildMajorVersion = (ushort)BuildInfo.BUILD_VERSION_MAJOR, - Mods = ModInfo.FromModEntries(UnityModManager.modEntries) + BuildVersion = Multiplayer.LocalBuildInfo, + Mods = ModCompatibilityManager.Instance.GetLocalMods() }; netPacketProcessor.Write(cachedWriter, serverboundClientLoginPacket); SelfPeer = Connect(address, port, cachedWriter); @@ -270,15 +269,18 @@ private void OnClientboundLoginResponsePacket(ClientboundLoginResponsePacket pac if (packet.Missing.Length != 0 || packet.Extra.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"; } if (packet.Extra.Length != 0) + { + if (packet.Missing.Length != 0) + text += "\n"; text += Locale.Get(Locale.DISCONN_REASON__MODS_EXTRA_KEY, placeholders: string.Join("\n - ", packet.Extra)); + } } Log($"Received player deny packet: {text}"); diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index d64ab343..8d5e0d65 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -35,7 +35,6 @@ using System.Net; using System.Text; using UnityEngine; -using UnityModManagerNet; namespace Multiplayer.Networking.Managers.Server; @@ -725,12 +724,13 @@ private void OnServerboundClientLoginPacket(ServerboundClientLoginPacket packet, } if (packet.BuildMajorVersion != BuildInfo.BUILD_VERSION_MAJOR) + if (packet.BuildVersion != Multiplayer.LocalBuildInfo) { - LogWarning($"Denied login to incorrect game version! Got: {packet.BuildMajorVersion}, expected: {BuildInfo.BUILD_VERSION_MAJOR}"); + LogWarning($"Denied login to incorrect game version! Got: {packet.BuildVersion}, expected: {Multiplayer.LocalBuildInfo}"); ClientboundLoginResponsePacket denyPacket = new() { ReasonKey = Locale.DISCONN_REASON__GAME_VERSION_KEY, - ReasonArgs = [BuildInfo.BUILD_VERSION_MAJOR.ToString(), packet.BuildMajorVersion.ToString()] + ReasonArgs = [Multiplayer.LocalBuildInfo, packet.BuildVersion.ToString()] }; request.Reject(WritePacket(denyPacket)); return; @@ -747,18 +747,16 @@ private void OnServerboundClientLoginPacket(ServerboundClientLoginPacket packet, return; } - ModInfo[] clientMods = packet.Mods.Where(mod => !modWhiteList.Contains(mod.Id)).ToArray(); - if (!serverMods.SequenceEqual(clientMods)) + var validation = ModCompatibilityManager.Instance.ValidateClientMods(packet.Mods); + if (!validation.IsValid) { - 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"); + LogWarning($"Denied login due to mod mismatch! {validation.Missing.Count} missing, {validation.Extra} extra"); ClientboundLoginResponsePacket denyPacket = new() { ReasonKey = Locale.DISCONN_REASON__MODS_KEY, - Missing = missing, - Extra = extra + Missing = validation.Missing.ToArray(), + Extra = validation.Extra.ToArray(), }; request.Reject(WritePacket(denyPacket)); return; diff --git a/Multiplayer/Networking/Packets/Clientbound/ClientboundLoginResponsePacket.cs b/Multiplayer/Networking/Packets/Clientbound/ClientboundLoginResponsePacket.cs index 4e6553f4..184bcd9d 100644 --- a/Multiplayer/Networking/Packets/Clientbound/ClientboundLoginResponsePacket.cs +++ b/Multiplayer/Networking/Packets/Clientbound/ClientboundLoginResponsePacket.cs @@ -8,6 +8,6 @@ public class ClientboundLoginResponsePacket public bool Accepted { get; set; } public string ReasonKey { get; set; } public string[] ReasonArgs { get; set; } - public ModInfo[] Missing { get; set; } = Array.Empty(); - public ModInfo[] Extra { get; set; } = Array.Empty(); + public ModInfo[] Missing { get; set; } = []; + public ModInfo[] Extra { get; set; } = []; } diff --git a/Multiplayer/Networking/Packets/Serverbound/ServerboundClientLoginPacket.cs b/Multiplayer/Networking/Packets/Serverbound/ServerboundClientLoginPacket.cs index 9d76d2ee..0f725dac 100644 --- a/Multiplayer/Networking/Packets/Serverbound/ServerboundClientLoginPacket.cs +++ b/Multiplayer/Networking/Packets/Serverbound/ServerboundClientLoginPacket.cs @@ -7,6 +7,6 @@ public class ServerboundClientLoginPacket public string Username { get; set; } public byte[] Guid { get; set; } public string Password { get; set; } - public ushort BuildMajorVersion { get; set; } + public string BuildVersion { get; set; } public ModInfo[] Mods { get; set; } } diff --git a/MultiplayerAPI/Interfaces/IMultiplayerAPI.cs b/MultiplayerAPI/Interfaces/IMultiplayerAPI.cs index 32910980..03d18b41 100644 --- a/MultiplayerAPI/Interfaces/IMultiplayerAPI.cs +++ b/MultiplayerAPI/Interfaces/IMultiplayerAPI.cs @@ -1,3 +1,4 @@ +using MPAPI.Types; namespace MPAPI.Interfaces; @@ -11,6 +12,12 @@ public interface IMultiplayerAPI ///
    bool IsMultiplayerLoaded { get; } + + /// Sets the mod's compatibility requirements + /// String representing the your mod's Id (`ModEntry.Info.Id`) + /// ModCompatibility flags representing installation host/client requirements + void SetModCompatibility(string modId, MultiplayerCompatibility compatibility); + /// /// Returns true if either a host or client exist /// diff --git a/MultiplayerAPI/Types/MultiplayerCompatibility.cs b/MultiplayerAPI/Types/MultiplayerCompatibility.cs new file mode 100644 index 00000000..ae8fa26c --- /dev/null +++ b/MultiplayerAPI/Types/MultiplayerCompatibility.cs @@ -0,0 +1,41 @@ + +namespace MPAPI.Types; + +/// +/// Defines how a mod works with multiplayer functionality. +/// +public enum MultiplayerCompatibility : byte +{ + + /// + /// Mod has not defined compatibility. + /// If the host is using this mod all clients must also have it. + /// If a client is using this mod and the host is not, the client will be unable to join the game. + /// + Undefined, + + /// + /// Mod is incompatible with multiplayer. + /// The mod must be disabled if Multiplayer Mod is enabled. + /// + Incompatible, + + /// + /// Mod must be installed on the host and all clients. + /// Players without this mod will be unable to join the game. + /// Mods are responsible for disabling behaviour when connecting to a host without the mod. + /// + All, + + /// + /// Mod must be installed on the host. + /// Mods are responsible for disabling their behaviour if the player is not the host. + /// + Host, + + /// + /// Mod has no effect on the gamne play and can be ignored + /// This should be used for client-only mods e.g. GUI enhancements, controller mods, RUE, etc. + /// + Client, +} diff --git a/locale.csv b/locale.csv index 1b66de19..41f2ad69 100644 --- a/locale.csv +++ b/locale.csv @@ -8,6 +8,7 @@ Key,Description,English,Bulgarian,Chinese (Simplified),Chinese (Traditional),Cze 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,,,,,,,,,,,,,,,,,,,,,,,,,, +mm/incompatible_mods,Message box for incompatible mods detected,Incompatible mods detected!\nPlease disable the following mods and reload the game:,,,,,,,,,,,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,, ,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ı,Браузер сервера From fe502e48b85d6877c9bd110d8ea3ef3b5ca1d611 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 21 Jun 2025 19:28:20 +1000 Subject: [PATCH 380/521] General cleanup --- .../Components/MainMenu/HostGamePane.cs | 34 +++++----- Multiplayer/Locale.cs | 15 +++++ .../Managers/Server/NetworkServer.cs | 64 +++++++++++-------- Multiplayer/Utils/DvExtensions.cs | 8 +-- .../TestComponents/ServerTest.cs | 45 +++++-------- locale.csv | 4 ++ 6 files changed, 91 insertions(+), 79 deletions(-) diff --git a/Multiplayer/Components/MainMenu/HostGamePane.cs b/Multiplayer/Components/MainMenu/HostGamePane.cs index 88af8b0e..5d5cb553 100644 --- a/Multiplayer/Components/MainMenu/HostGamePane.cs +++ b/Multiplayer/Components/MainMenu/HostGamePane.cs @@ -76,7 +76,6 @@ public void Start() public void OnEnable() { - //Multiplayer.Log("HostGamePane OnEnable()"); this.SetupListeners(true); incompatibleMods = ModCompatibilityManager.Instance.CheckModCompatibility(); @@ -139,7 +138,7 @@ private void BuildUI() Multiplayer.LogError("Field Of View not found!"); return; } - + GameObject inputPrefab = MainMenuThingsAndStuff.Instance.references.popupTextInput.gameObject.FindChildByName("TextFieldTextIcon"); if (inputPrefab == null) { @@ -178,12 +177,12 @@ private void BuildUI() 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.


    " + + /*"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.

    " + + "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 @@ -215,7 +214,7 @@ private void BuildUI() layoutGroup.childForceExpandHeight = true; layoutGroup.spacing = 0; // Adjust the spacing as needed - layoutGroup.padding = new RectOffset(0,0,0,0); + layoutGroup.padding = new RectOffset(0, 0, 0, 0); ContentSizeFitter sizeFitter = controls.AddComponent(); sizeFitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize; @@ -223,10 +222,10 @@ private void BuildUI() /* * Server name field */ - GameObject go = GameObject.Instantiate(inputPrefab, NewContentGroup(controls, scroller.viewport.sizeDelta).transform,false); + GameObject go = GameObject.Instantiate(inputPrefab, NewContentGroup(controls, scroller.viewport.sizeDelta).transform, false); go.name = "Server Name"; serverName = go.GetComponent(); - serverName.text = Multiplayer.Settings.ServerName?.Trim().Substring(0,Mathf.Min(Multiplayer.Settings.ServerName.Trim().Length,MAX_SERVER_NAME_LEN)); + 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(); @@ -253,7 +252,7 @@ private void BuildUI() gameVisibility = go.GetOrAddComponent(); //clean-up - + if (gameVisibility.labelTMPro?.gameObject.TryGetComponent(out var loc) ?? false) GameObject.DestroyImmediate(loc); if (gameVisibility.labelTMPro?.gameObject.TryGetComponent(out var loc2) ?? false) @@ -280,7 +279,7 @@ private void BuildUI() /* * Server details field */ - go = GameObject.Instantiate(inputPrefab, NewContentGroup(controls, scroller.viewport.sizeDelta,106).transform, false); + 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(); @@ -312,11 +311,11 @@ private void BuildUI() labelGo.GetComponent().key = Locale.SERVER_HOST_MAX_PLAYERS_KEY; go.ResetTooltip(); //labelGo.GetComponent().UpdateLocalization(); - + maxPlayers.stepIncrement = 1; maxPlayers.minValue = MIN_PLAYERS; maxPlayers.maxValue = MAX_PLAYERS; - maxPlayers.value = Mathf.Clamp(Multiplayer.Settings.MaxPlayers,MIN_PLAYERS,MAX_PLAYERS); + maxPlayers.value = Mathf.Clamp(Multiplayer.Settings.MaxPlayers, MIN_PLAYERS, MAX_PLAYERS); go.SetActive(true); maxPlayers.interactable = true; @@ -329,14 +328,14 @@ private void BuildUI() port.characterValidation = TMP_InputField.CharacterValidation.Integer; port.characterLimit = MAX_PORT_LEN; port.placeholder.GetComponent().text = DEFAULT_PORT.ToString(); - port.text = (Multiplayer.Settings.Port >= MIN_PORT && Multiplayer.Settings.Port <= MAX_PORT) ? Multiplayer.Settings.Port.ToString() : DEFAULT_PORT.ToString(); + port.text = (Multiplayer.Settings.Port >= MIN_PORT && Multiplayer.Settings.Port <= MAX_PORT) ? Multiplayer.Settings.Port.ToString() : DEFAULT_PORT.ToString(); /* * Start Game button */ 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(OnStartClick); @@ -351,7 +350,7 @@ private GameObject NewContentGroup(GameObject parent, Vector2 sizeDelta, int cel contentGroup.transform.SetParent(parent.transform, false); groupRect.sizeDelta = sizeDelta; - ContentSizeFitter sizeFitter = contentGroup.AddComponent(); + ContentSizeFitter sizeFitter = contentGroup.AddComponent(); sizeFitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize; // Add VerticalLayoutGroup and ContentSizeFitter @@ -482,9 +481,6 @@ private void OnStartClick() //Multiplayer.Log($"OnRunClicked exists: {ContinueGameRequested != null}"); ContinueGameRequested?.Invoke(lcInstance, null); } - - - #endregion diff --git a/Multiplayer/Locale.cs b/Multiplayer/Locale.cs index 62f4f2ab..55ab7743 100644 --- a/Multiplayer/Locale.cs +++ b/Multiplayer/Locale.cs @@ -31,6 +31,18 @@ public static class Locale public static string MAIN_MENU__INCOMPATIBLE_MODS => Get(MAIN_MENU__INCOMPATIBLE_MODS_KEY); public const string MAIN_MENU__INCOMPATIBLE_MODS_KEY = $"{PREFIX_MAIN_MENU}/incompatible_mods"; + + public static string MAIN_MENU__UPDATE_TITLE => Get(MAIN_MENU__MAIN_MENU__UPDATE_TITLE_KEY); + public const string MAIN_MENU__MAIN_MENU__UPDATE_TITLE_KEY = $"{PREFIX_MAIN_MENU}/update_title"; + + public static string MAIN_MENU__UPDATE_LATEST => Get(MAIN_MENU__UPDATE_LATEST_KEY); + public const string MAIN_MENU__UPDATE_LATEST_KEY = $"{PREFIX_MAIN_MENU}/update_latest"; + + public static string MAIN_MENU__UPDATE_INSTALLED => Get(MAIN_MENU__UPDATE_INSTALLED_KEY); + public const string MAIN_MENU__UPDATE_INSTALLED_KEY = $"{PREFIX_MAIN_MENU}/update_installed"; + + public static string MAIN_MENU__UPDATE_ACTION => Get(MAIN_MENU__UPDATE_ACTION_KEY); + public const string MAIN_MENU__UPDATE_ACTION_KEY = $"{PREFIX_MAIN_MENU}/update_action"; #endregion #region Server Browser @@ -132,6 +144,9 @@ public static class Locale 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 DISCONN_REASON__MODS_INCOMPATIBLE => Get(DISCONN_REASON__MODS_INCOMPATIBLE_KEY); + public const string DISCONN_REASON__MODS_INCOMPATIBLE_KEY = $"{PREFIX_DISCONN_REASON}/mods_incompatible"; #endregion #region Career Manager diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index 8d5e0d65..943fb7ab 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -62,27 +62,20 @@ public class NetworkServer : NetworkManager private static ITransportPeer SelfPeer => NetworkLifecycle.Instance.Client?.SelfPeer; public static byte SelfId => (byte)SelfPeer.Id; - private readonly ModInfo[] serverMods; public readonly IDifficulty Difficulty; private bool IsLoaded; - private readonly ChatManager _chatManager = new(); + private readonly ChatManager _chatManager = new(); public ChatManager ChatManager => _chatManager; - //we don't care if the client doesn't have these mods - public static string[] modWhiteList = ["RuntimeUnityEditor", "BookletOrganizer", "RemoteDispatch"]; - public NetworkServer(IDifficulty difficulty, Settings settings, bool singlePlayer, LobbyServerData serverData) : base(settings) { - Log(()=>$"Server created for {(singlePlayer ? "single player" : "multiplayer")} game"); + Log(() => $"Server created for {(singlePlayer ? "single player" : "multiplayer")} game"); IsSinglePlayer = singlePlayer; ServerData = serverData; Difficulty = difficulty; - - serverMods = ModInfo.FromModEntries(UnityModManager.modEntries) - .Where(mod => !modWhiteList.Contains(mod.Id)).ToArray(); } public override bool Start(int port) @@ -97,7 +90,7 @@ public override bool Start(int 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 base.Start(IPAddress.Any, ipv6Address,port); + return base.Start(IPAddress.Any, ipv6Address, port); } @@ -116,7 +109,7 @@ public override void Stop() } //Alert all clients (except host) - var packet = WritePacket(new ClientboundDisconnectPacket()); + var packet = WritePacket(new ClientboundDisconnectPacket()); foreach (var peer in peers.Values) { if (peer != SelfPeer) @@ -351,7 +344,7 @@ public override void OnConnectionRequest(NetDataReader requestData, IConnectionR { var deliveryMethod = reliable ? DeliveryMethod.ReliableUnordered : DeliveryMethod.Unreliable; - if(excludePeer == null) + if (excludePeer == null) SendPacketToAll(packet, deliveryMethod); else SendPacketToAll(packet, deliveryMethod, excludePeer); @@ -450,7 +443,7 @@ public void SendDestroyTrainCar(NetworkedTrainCar netTrainCar, ITransportPeer pe return; } - var packet = new ClientboundDestroyTrainCarPacket{ NetId = netTrainCar.NetId }; + var packet = new ClientboundDestroyTrainCarPacket { NetId = netTrainCar.NetId }; if (peer == null) SendPacketToAll(packet, DeliveryMethod.ReliableOrdered, SelfPeer); @@ -625,7 +618,7 @@ public void SendJobsCreatePacket(NetworkedStationController networkedStation, Ne var packet = ClientboundJobsCreatePacket.FromNetworkedJobs(networkedStation, jobs); - if (peer ==null) + if (peer == null) SendPacketToAll(packet, DeliveryMethod.ReliableOrdered, SelfPeer); else SendPacket(peer, packet, DeliveryMethod.ReliableOrdered); @@ -723,7 +716,6 @@ private void OnServerboundClientLoginPacket(ServerboundClientLoginPacket packet, return; } - if (packet.BuildMajorVersion != BuildInfo.BUILD_VERSION_MAJOR) if (packet.BuildVersion != Multiplayer.LocalBuildInfo) { LogWarning($"Denied login to incorrect game version! Got: {packet.BuildVersion}, expected: {Multiplayer.LocalBuildInfo}"); @@ -752,6 +744,28 @@ private void OnServerboundClientLoginPacket(ServerboundClientLoginPacket packet, { LogWarning($"Denied login due to mod mismatch! {validation.Missing.Count} missing, {validation.Extra} extra"); + LogDebug(() => + { + StringBuilder sb = new("Mod mis-match:"); + sb.AppendLine("Server Mods:"); + foreach (ModInfo mod in ModCompatibilityManager.Instance.GetLocalMods()) + sb.AppendLine($"\t{mod.Id} {mod.Version}, Status: {ModCompatibilityManager.Instance.GetCompatibility(mod)}"); + + sb.AppendLine("\r\nClient Mods:"); + foreach (ModInfo mod in packet.Mods) + sb.AppendLine($"\t{mod.Id} {mod.Version}, Status (if known): {ModCompatibilityManager.Instance.GetCompatibility(mod)}"); + + sb.AppendLine("\r\nMissing Mods:"); + foreach (ModInfo mod in validation.Missing) + sb.AppendLine($"\t{mod.Id} {mod.Version}, Status: {ModCompatibilityManager.Instance.GetCompatibility(mod)}"); + + sb.AppendLine("\r\nExtra Mods:"); + foreach (ModInfo mod in validation.Extra) + sb.AppendLine($"\t{mod.Id} {mod.Version}, Status (if known): {ModCompatibilityManager.Instance.GetCompatibility(mod)}"); + + return sb.ToString(); + }); + ClientboundLoginResponsePacket denyPacket = new() { ReasonKey = Locale.DISCONN_REASON__MODS_KEY, @@ -960,16 +974,16 @@ private void OnCommonRotateTurntablePacket(CommonRotateTurntablePacket packet, I private void OnCommonCouplerInteractionPacket(CommonCouplerInteractionPacket packet, ITransportPeer peer) { - if(!peerToPlayer.TryGetValue(peer, out var player)) + if (!peerToPlayer.TryGetValue(peer, out var player)) { LogWarning($"Received Coupler Interaction from {peer.GetType()}, peerId: {peer.Id}, but could not find matching player."); return; } //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 - if(NetworkedTrainCar.TryGet(packet.NetId, out NetworkedTrainCar netTrainCar)) + if (NetworkedTrainCar.TryGet(packet.NetId, out NetworkedTrainCar netTrainCar)) { - if(netTrainCar.Server_ValidateCouplerInteraction(packet, player)) + if (netTrainCar.Server_ValidateCouplerInteraction(packet, player)) { //passed validation, send to all but the originator SendPacketToAll(packet, DeliveryMethod.ReliableOrdered, peer); @@ -981,12 +995,12 @@ private void OnCommonCouplerInteractionPacket(CommonCouplerInteractionPacket pac SendPacket( peer, new CommonCouplerInteractionPacket - { - NetId = packet.NetId, - Flags = (ushort)CouplerInteractionType.NoAction, - IsFrontCoupler = packet.IsFrontCoupler, - } - ,DeliveryMethod.ReliableOrdered + { + NetId = packet.NetId, + Flags = (ushort)CouplerInteractionType.NoAction, + IsFrontCoupler = packet.IsFrontCoupler, + } + , DeliveryMethod.ReliableOrdered ); } } @@ -996,7 +1010,7 @@ private void OnCommonCouplerInteractionPacket(CommonCouplerInteractionPacket pac //Car doesn't exist, tell client to delete it SendDestroyTrainCar(netTrainCar, peer); } - + } //private void OnCommonTrainCouplePacket(CommonTrainCouplePacket packet, ITransportPeer peer) //{ diff --git a/Multiplayer/Utils/DvExtensions.cs b/Multiplayer/Utils/DvExtensions.cs index 017e5747..39549275 100644 --- a/Multiplayer/Utils/DvExtensions.cs +++ b/Multiplayer/Utils/DvExtensions.cs @@ -1,14 +1,12 @@ -using System; +using DV.Localization; using DV.UI; using DV.UIFramework; -using DV.Localization; +using Multiplayer.Components.Networking; using Multiplayer.Components.Networking.Train; using Multiplayer.Components.Networking.World; +using Multiplayer.Networking.Data; using UnityEngine; using UnityEngine.UI; -using System.Diagnostics; -using Multiplayer.Components.Networking; -using Multiplayer.Networking.Data; diff --git a/MultiplayerAPI Tests/TestComponents/ServerTest.cs b/MultiplayerAPI Tests/TestComponents/ServerTest.cs index e099bd6d..dc781edc 100644 --- a/MultiplayerAPI Tests/TestComponents/ServerTest.cs +++ b/MultiplayerAPI Tests/TestComponents/ServerTest.cs @@ -1,4 +1,3 @@ -using CommandTerminal; using DV.Logic.Job; using I2.Loc; using MPAPI; @@ -8,9 +7,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Reflection; -using System.Runtime.InteropServices; -using System.Security.Cryptography; using System.Text; using UnityEngine; @@ -32,7 +28,7 @@ protected void Awake() server.OnPlayerDisconnected += OnPlayerDisconnected; server.OnPlayerReady += OnPlayerReady; - //subscribe to packets and chat commands; + // Subscribe to packets and chat commands Subscribe(); } @@ -48,11 +44,9 @@ protected void OnDestroy() server.OnPlayerReady -= OnPlayerReady; } - - //Setup subscriptions for the packets we want to/expect to receive private void Subscribe() { - // Subscribe to network packets + // Subscribe to network packets - note: only packets that will be received by server need to be registered here server.RegisterPacket(OnTestSimpleModPacket); server.RegisterPacket(OnSimplePacketWithNetId); server.RegisterSerializablePacket(OnTestComplexModPacket); @@ -93,42 +87,31 @@ private void OnPlayerDisconnected(IPlayer player) // Note: This event occurs immediately prior to destroying the player object // Complete all cleanup prior to returning from this method - Log($"Player \"{player?.Id}\" has disconnected"); + Log($"Player \"{player?.Username}\" has disconnected"); } #endregion #region Packet Callbacks - //method called when a `TestSimplePacket` packet is received + // Method called when a `SimplePacket` packet is received private void OnTestSimpleModPacket(SimplePacket packet, IPlayer player) { - - Log($"Received {packet.GetType()} from player: {player.Username}"); - - Log($"CarId: {packet.CarId}, Position: {packet.Position}, WheelArraangement: {packet.WheelArrangement}"); - + Log($"Received {packet.GetType()} from player: {player.Username}, CarId: {packet.CarId}, Position: {packet.Position}, WheelArraangement: {packet.WheelArrangement}"); } - //method called when a `TestSimplePacket` packet is received + // Method called when a `SimplePacketWithNetId` packet is received private void OnSimplePacketWithNetId(SimplePacketWithNetId packet, IPlayer player) { - - Log($"Received {packet.GetType()} from player: {player.Username}"); - - Log($"CarId: {packet.CarNetId}, Position: {packet.Position}, Wheel Arrangement: {packet.WheelArrangement}"); - + Log($"Received {packet.GetType()} from player: {player.Username}, CarId: {packet.CarNetId}, Position: {packet.Position}, Wheel Arrangement: {packet.WheelArrangement}"); } - //method called when a `TestComplexModPacket` packet is received + //method called when a `ComplexModPacket` packet is received private void OnTestComplexModPacket(ComplexModPacket packet, IPlayer player) { - - Log($"Received {packet.GetType()} from player: {player.Username}"); - - StringBuilder sb = new("\r\nPacket Data"); + StringBuilder sb = new($"Received {packet.GetType()}\r\nPacket Data"); foreach (var kvp in packet.CarToPositionMap) - sb.AppendLine($"CarId: {kvp.Key}, Position: {kvp.Value}"); + sb.AppendLine($"\tCarId: {kvp.Key}, Position: {kvp.Value}"); Log(sb.ToString()); } @@ -176,6 +159,7 @@ public void SendComplexPacket(Dictionary carToPos, IPlayer excl #endregion #region Chat Command Callbacks + // Called when a player uses the chat command '/packet' or '/p' private void OnChatCommandSendPacket(string message, IPlayer sender) { string[] args = message.Split(' '); @@ -238,6 +222,7 @@ private void OnChatCommandSendPacket(string message, IPlayer sender) } } + // Called when a player uses the chat command '/locopos' or '/lp' private void OnChatCommandSendLocoPos(string message, IPlayer sender) { //this chat command has no arguments @@ -276,7 +261,7 @@ private void OnChatCommandClosestPlayer(string message, IPlayer sender) LogWarning($"Received 'ClosestPlayer' chat command from player \"{sender.Username}\", but the second argument is empty. Command: {message}"); } - var tc = GetTrainCarFromID(args[0]); + var tc = GetTrainCarFromID(args[0].ToUpper()); if (tc != null) { @@ -300,7 +285,7 @@ private void OnChatCommandStats(string message, IPlayer sender) StringBuilder whisper = new($"There {(server.PlayerCount > 1 ? "are" : "is")} {server.PlayerCount} connected player{(server.PlayerCount > 1 ? "s" : "")}:"); foreach (var player in server.Players) - whisper.Append($"
    \t{(player.IsHost? "" : "")}{player.Username}{(player.IsHost ? "" : "")} Id: {player.Id}, Ping: {player.Ping}{(player.IsOnCar? $", Riding {player.OccupiedCar.ID}" : "")}"); + whisper.Append($"
    \t{(player.IsHost ? "" : "")}{player.Username}{(player.IsHost ? "" : "")} Id: {player.Id}, Ping: {player.Ping}{(player.IsOnCar ? $", Riding {player.OccupiedCar.ID}" : "")}"); whisper.Append(""); @@ -412,7 +397,7 @@ private bool OnChatMessage(ref string message, IPlayer sender) var badWordstart = message.IndexOf(badWord, StringComparison.OrdinalIgnoreCase); while (badWordstart >= 0) { - message = message.Remove(badWordstart,badWord.Length).Insert(badWordstart, new string('*', badWord.Length)); + message = message.Remove(badWordstart, badWord.Length).Insert(badWordstart, new string('*', badWord.Length)); badWordstart = message.IndexOf(badWord, badWordstart + badWord.Length, StringComparison.OrdinalIgnoreCase); } } diff --git a/locale.csv b/locale.csv index 41f2ad69..96ed5269 100644 --- a/locale.csv +++ b/locale.csv @@ -9,6 +9,10 @@ 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.,Присъединете се към мултиплейър сесия.,加入多人游戏,加入多人遊戲會話。,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,,,,,,,,,,,,,,,,,,,,,,,,,, mm/incompatible_mods,Message box for incompatible mods detected,Incompatible mods detected!\nPlease disable the following mods and reload the game:,,,,,,,,,,,,,,,,,,,,,,,,, +mm/update_title,Message box title for Multiplayer mod update,Multiplayer Mod Update Available!,,,,,,,,,,,,,,,,,,,,,,,,, +mm/update_latest,Message box title for Multiplayer mod update,Latest version: {0},,,,,,,,,,,,,,,,,,,,,,,,, +mm/update_installed,Message box title for Multiplayer mod update,Installed version: {0},,,,,,,,,,,,,,,,,,,,,,,,, +mm/update_action,Message box title for Multiplayer mod update,Run Unity Mod Manager Installer to apply the update.,,,,,,,,,,,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,, ,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ı,Браузер сервера From d99fef24c518c17a1250f0e9aa73b99b4be23c68 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 22 Jun 2025 08:14:45 +1000 Subject: [PATCH 381/521] Add translations for chat commands --- .../Components/Networking/UI/ChatGUI.cs | 2 +- Multiplayer/Locale.cs | 11 ++++++++++ .../Networking/Managers/Server/ChatManager.cs | 22 +++++++++---------- locale.csv | 3 +++ 4 files changed, 26 insertions(+), 12 deletions(-) diff --git a/Multiplayer/Components/Networking/UI/ChatGUI.cs b/Multiplayer/Components/Networking/UI/ChatGUI.cs index d4165b40..615e3375 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 = Locale.CHAT_PLACEHOLDER;// "Type a message and press Enter!"; //translate + chatInputIF.placeholder.GetComponent().text = Locale.CHAT_PLACEHOLDER;// "Type a message and press Enter!"; //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 55ab7743..fc3345d0 100644 --- a/Multiplayer/Locale.cs +++ b/Multiplayer/Locale.cs @@ -182,6 +182,17 @@ public static class Locale 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"; + + public static string CHAT_WHISPER_NOT_FOUND => Get(CHAT_WHISPER_NOT_FOUND_KEY); + public const string CHAT_WHISPER_NOT_FOUND_KEY = $"{PREFIX_CHAT_INFO}/whisper/not_found"; + + public static string CHAT_KICK_UNABLE => Get(CHAT_KICK_UNABLE_KEY); + public const string CHAT_KICK_UNABLE_KEY = $"{PREFIX_CHAT_INFO}/kick/unable"; + public static string CHAT_KICK_KICKED => Get(CHAT_KICK_KICKED_KEY); + public const string CHAT_KICK_KICKED_KEY = $"{PREFIX_CHAT_INFO}/kick/kicked"; + + + #endregion #region Pause Menu diff --git a/Multiplayer/Networking/Managers/Server/ChatManager.cs b/Multiplayer/Networking/Managers/Server/ChatManager.cs index d9dca625..46409b2b 100644 --- a/Multiplayer/Networking/Managers/Server/ChatManager.cs +++ b/Multiplayer/Networking/Managers/Server/ChatManager.cs @@ -20,7 +20,7 @@ public class ChatManager public const string COMMAND_LOG = "log"; public const string COMMAND_LOG_SHORT = "l"; public const string COMMAND_KICK = "kick"; - + public const string MESSAGE_COLOUR_SERVER = "9CDCFE"; public const string MESSAGE_COLOUR_HELP = "00FF00"; @@ -49,7 +49,7 @@ private void RegisterBuiltInCommands() ( COMMAND_WHISPER, COMMAND_WHISPER_SHORT, - ()=> $"{Locale.CHAT_HELP_WHISPER_MSG}" + + () => $"{Locale.CHAT_HELP_WHISPER_MSG}" + $"\r\n\t\t/{COMMAND_WHISPER} <{Locale.CHAT_HELP_PLAYER_NAME}> <{Locale.CHAT_HELP_MSG}>" + $"\r\n\t\t/{COMMAND_WHISPER_SHORT} <{Locale.CHAT_HELP_PLAYER_NAME}> <{Locale.CHAT_HELP_MSG}>", WhisperMessage @@ -59,7 +59,7 @@ private void RegisterBuiltInCommands() ( COMMAND_HELP, COMMAND_HELP_SHORT, - ()=> $"{Locale.CHAT_HELP_HELP}" + + () => $"{Locale.CHAT_HELP_HELP}" + $"\r\n\t\t/{COMMAND_HELP}" + $"\r\n\t\t/{COMMAND_HELP_SHORT}", HelpMessage @@ -142,7 +142,7 @@ public void ProcessMessage(string message, ServerPlayer sender) Multiplayer.LogDebug(() => $"ProcessMessage(\'{message}\') cleaned message: {cleanedMessage}"); commandCallback(cleanedMessage, sender); - + return; } } @@ -199,15 +199,15 @@ private void WhisperMessage(string message, ServerPlayer sender) string whisperMessage = parts[1]; - Multiplayer.LogDebug(()=>$"Whispering parse 1: \"{message}\", sender: {sender?.Username}, senderID: {sender?.Id}, peerName: {recipientName}"); + Multiplayer.LogDebug(() => $"Whispering parse 1: \"{message}\", sender: {sender?.Username}, senderID: {sender?.Id}, peerName: {recipientName}"); //look up the peer ID var recipient = ServerPlayerFromUsername(recipientName); - if(recipient == null) + if (recipient == null) { Multiplayer.LogDebug(() => $"Whispering failed: \"{message}\", sender: {sender?.Username}, senderID: {sender?.Id}, peerName: {recipientName}"); - whisperMessage = $"{recipientName} not found - you're whispering into the void!"; //todo: add translation + whisperMessage = $"{Locale.Get(Locale.CHAT_WHISPER_NOT_FOUND_KEY, recipientName)}"; NetworkLifecycle.Instance.Server.SendWhisper(whisperMessage, sender); return; } @@ -247,11 +247,11 @@ public void KickMessage(string message, ServerPlayer sender) if (playerToKick == null || NetworkLifecycle.Instance.IsHost(playerToKick.Peer)) { - whisper = $"Unable to kick {playerName}"; //todo: translate + whisper = $"{Locale.Get(Locale.CHAT_KICK_UNABLE_KEY, playerName)}"; } else { - whisper = $"{playerName} was kicked"; //todo: translate + whisper = $"{Locale.Get(Locale.CHAT_KICK_KICKED_KEY, playerName)}"; NetworkLifecycle.Instance.Server.KickPlayer(playerToKick); } @@ -301,8 +301,8 @@ private void HelpMessage(string _, ServerPlayer player) private ServerPlayer ServerPlayerFromUsername(string playerName) { - - if(string.IsNullOrEmpty(playerName)) + + if (string.IsNullOrEmpty(playerName)) return null; return NetworkLifecycle.Instance.Server.ServerPlayers.Where(p => p.Username == playerName).FirstOrDefault(); diff --git a/locale.csv b/locale.csv index 96ed5269..70675de5 100644 --- a/locale.csv +++ b/locale.csv @@ -107,6 +107,9 @@ chat/help/whispermsg,Chat help whisper to a player,Whisper to a player,,向一 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,,,,,,,,,,,,,, +chat/whisper/not_found,Whisper error player not found 'player1 not found - you're whispering into the void!',{0} not found - you're whispering into the void!,,,,,,,,,,,,,,,,,,,,,,,,, +chat/kick/unable,Unable to kick player 'Unable to kick player1',Unable to kick {0},,,,,,,,,,,,,,,,,,,,,,,,, +chat/kick/kicked,Alert that a player has bee kicked 'player1 was kicked',{0} was kicked,,,,,,,,,,,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,, ,Pause Menu,,,,,,,,,,,,,,,,,,,,,,,,,, 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?,,,,,,,,,,,,,, From 26eeebb7e7cd2e03ef3f7be792e4fc5f1edea87f Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 22 Jun 2025 10:45:09 +1000 Subject: [PATCH 382/521] Add translation keys for updates --- Multiplayer/Multiplayer.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Multiplayer/Multiplayer.cs b/Multiplayer/Multiplayer.cs index d7e7168a..4867ca67 100644 --- a/Multiplayer/Multiplayer.cs +++ b/Multiplayer/Multiplayer.cs @@ -166,10 +166,20 @@ private static void LateUpdate(UnityModManager.ModEntry modEntry, float deltaTim 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."; + */ + + var latestVer = Locale.Get(Locale.MAIN_MENU__UPDATE_LATEST_KEY, $"\t\t{ModEntry.NewestVersion}"); + var installedVer = Locale.Get(Locale.MAIN_MENU__UPDATE_INSTALLED_KEY, $"\t\t{ModEntry.Version}"); + + update.labelTMPro.text = Locale.MAIN_MENU__UPDATE_TITLE + + $"\r\n\r\n{latestVer}" + + $"\r\n{installedVer}\r\n\r\n" + + $"{Locale.MAIN_MENU__UPDATE_ACTION}"; Vector3 currPos = update.labelTMPro.transform.localPosition; Vector2 size = update.labelTMPro.rectTransform.sizeDelta; From 1b46c4bf687abcfcea42b2e36e1b2d0faee4f418 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 22 Jun 2025 10:47:58 +1000 Subject: [PATCH 383/521] Add interface for Tick Events --- Multiplayer/API/APIProvider.cs | 24 ++++++++++++++ Multiplayer/API/ClientAPIProvider.cs | 4 --- Multiplayer/API/ServerAPIProvider.cs | 2 +- MultiplayerAPI Tests/MultiplayerAPITest.cs | 7 ++-- .../TestComponents/ServerTest.cs | 33 ++++++++++++++++++- MultiplayerAPI/Interfaces/IMultiplayerAPI.cs | 23 +++++++++++++ 6 files changed, 84 insertions(+), 9 deletions(-) diff --git a/Multiplayer/API/APIProvider.cs b/Multiplayer/API/APIProvider.cs index 712beda3..f5baef73 100644 --- a/Multiplayer/API/APIProvider.cs +++ b/Multiplayer/API/APIProvider.cs @@ -1,6 +1,7 @@ using MPAPI.Interfaces; using MPAPI.Types; using Multiplayer.Components.Networking; +using System; namespace Multiplayer.API @@ -17,6 +18,10 @@ public class APIProvider : IMultiplayerAPI public bool IsSinglePlayer => NetworkLifecycle.Instance.IsServerRunning && (NetworkLifecycle.Instance?.Server.IsSinglePlayer ?? false); + public event Action OnTick; + public uint TICK_RATE => NetworkLifecycle.TICK_RATE; + public uint CurrentTick => NetworkLifecycle.Instance.Tick; + public bool TryGetNetId(T obj, out ushort netId) where T : class { return NetIdProvider.Instance.TryGetNetId(obj, out netId); @@ -31,5 +36,24 @@ public void SetModCompatibility(string modId, MultiplayerCompatibility compatibi { ModCompatibilityManager.Instance.RegisterCompatibility(modId, compatibility); } + + #region Class Helpers + + internal APIProvider() + { + NetworkLifecycle.Instance.OnTick += OnTickInternal; + } + + internal void Dispose() + { + NetworkLifecycle.Instance.OnTick -= OnTickInternal; + } + + private void OnTickInternal(uint tick) + { + OnTick?.Invoke(tick); + } + + #endregion } } diff --git a/Multiplayer/API/ClientAPIProvider.cs b/Multiplayer/API/ClientAPIProvider.cs index 5925232a..9bf59729 100644 --- a/Multiplayer/API/ClientAPIProvider.cs +++ b/Multiplayer/API/ClientAPIProvider.cs @@ -2,13 +2,10 @@ using MPAPI.Interfaces.Packets; using MPAPI.Types; using Multiplayer.Components.Networking.Player; -using Multiplayer.Networking.Data; using Multiplayer.Networking.Managers.Client; -using Multiplayer.Utils; using System; using System.Collections.Generic; using System.Linq; -using UnityEngine; namespace Multiplayer.API { @@ -45,7 +42,6 @@ public IPlayer GetPlayer(byte id) client.RegisterExternalSerializablePacket(handler); } - public void SendPacketToServer(T packet, bool reliable = true) where T : class, IPacket, new() { client.SendExternalPacketToServer(packet, reliable); diff --git a/Multiplayer/API/ServerAPIProvider.cs b/Multiplayer/API/ServerAPIProvider.cs index af849978..a4bf6b53 100644 --- a/Multiplayer/API/ServerAPIProvider.cs +++ b/Multiplayer/API/ServerAPIProvider.cs @@ -170,7 +170,7 @@ private ServerPlayer GetServerPlayerFromIPlayer(IPlayer player) return null; if (player is ServerPlayerWrapper wrapper) - return wrapper._serverPlayer; // You'll need to make this internal accessible + return wrapper._serverPlayer; server.LogWarning($"GetServerPlayerFromIPlayer: Player '{player.Username}' is not a ServerPlayerWrapper (got {player.GetType().Name})"); return null; diff --git a/MultiplayerAPI Tests/MultiplayerAPITest.cs b/MultiplayerAPI Tests/MultiplayerAPITest.cs index cb1f575d..fb6811e8 100644 --- a/MultiplayerAPI Tests/MultiplayerAPITest.cs +++ b/MultiplayerAPI Tests/MultiplayerAPITest.cs @@ -1,11 +1,12 @@ using HarmonyLib; -using System; using JetBrains.Annotations; -using UnityEngine; -using UnityModManagerNet; using MPAPI; using MPAPI.Interfaces; +using MPAPI.Types; using MultiplayerAPITest.TestComponents; +using System; +using UnityEngine; +using UnityModManagerNet; namespace MultiplayerAPITest; diff --git a/MultiplayerAPI Tests/TestComponents/ServerTest.cs b/MultiplayerAPI Tests/TestComponents/ServerTest.cs index dc781edc..85cad6c0 100644 --- a/MultiplayerAPI Tests/TestComponents/ServerTest.cs +++ b/MultiplayerAPI Tests/TestComponents/ServerTest.cs @@ -16,6 +16,9 @@ internal class ServerTest : MonoBehaviour { const string LogPrefix = "ServerTest"; const string MESSAGE_COLOUR_SERVER = "9CDCFE"; + const int DELAY_INTERVAL = 10; // 10 seconds + + uint lastLogTick = 0; IServer server; @@ -23,6 +26,9 @@ protected void Awake() { server = MultiplayerAPI.Server; + // Subscribe to game tick events + MultiplayerAPI.Instance.OnTick += OnTick; + // Subscribe to player events server.OnPlayerConnected += OnPlayerConnected; server.OnPlayerDisconnected += OnPlayerDisconnected; @@ -38,6 +44,9 @@ protected void Update() { } protected void OnDestroy() { + // Unsubscribe from game tick events + MultiplayerAPI.Instance.OnTick -= OnTick; + // Unsubscribe from player events server.OnPlayerConnected -= OnPlayerConnected; server.OnPlayerDisconnected -= OnPlayerDisconnected; @@ -61,6 +70,28 @@ private void Subscribe() server.RegisterChatFilter(OnChatMessage); } + #region Example Tick Event + private void OnTick(uint tick) + { + // This event is called every tick + // This code is purely for testing purposes, not a recommened use case; normally it would be used for synchronising + // and batching changes or to track how long since an update has been received for a specific object. + + // The TICK_RATE is fixed at both client and server; currently the rate is 24 ticks/second + if ((tick - lastLogTick) > MultiplayerAPI.Instance.TICK_RATE * DELAY_INTERVAL) + { + //DELAY_INTERVAL (10 seconds) passed, log the ping for all players + StringBuilder sb = new($"Player pings at {tick}:"); + foreach (IPlayer player in server.Players) + sb.AppendLine($"\"{player?.Id}\" {player.Ping} ms"); + + Log(sb.ToString()); + + lastLogTick = tick; + } + } + #endregion + #region Player Events private void OnPlayerConnected(IPlayer player) { @@ -424,7 +455,7 @@ private WheelArrangement GetRandomWheelArrangement() public void LogDebug(Func resolver) { - MultiplayerAPITest.LogDebug(() => $"{LogPrefix} {resolver.Invoke()}"); + MultiplayerAPITest.LogDebug(() => $"{LogPrefix} {resolver?.Invoke()}"); } public void Log(object msg) diff --git a/MultiplayerAPI/Interfaces/IMultiplayerAPI.cs b/MultiplayerAPI/Interfaces/IMultiplayerAPI.cs index 03d18b41..8cf945fe 100644 --- a/MultiplayerAPI/Interfaces/IMultiplayerAPI.cs +++ b/MultiplayerAPI/Interfaces/IMultiplayerAPI.cs @@ -1,4 +1,5 @@ using MPAPI.Types; +using System; namespace MPAPI.Interfaces; @@ -38,6 +39,28 @@ public interface IMultiplayerAPI /// bool IsSinglePlayer { get; } + /// + /// Event fired when a game/network tick occurs + /// Ticks occur at a fixed interval (TICK_INTERVAL = 1/TICK_RATE) and are useful for synchronisation, batching, and processing changes. + /// + /// The tick parameter can be used to determine if non-reliable packets have been dropped and to sequence actions for rollbacks or preventing stale data from being processed. + /// + /// Example: In Multiplayer's TrainCar simulation sync, small changes are cached when they occur but sent as a single packet per TrainCar when OnTick fires, reducing network overhead. + /// + /// The current game tick number, incremented each tick cycle + event Action OnTick; + + /// + /// The number of ticks per second (currently 24). + /// Used to calculate the fixed tick interval: TICK_INTERVAL = 1.0f / TICK_RATE. + /// + uint TICK_RATE { get; } + + /// + /// The current game tick. + /// + uint CurrentTick { get; } + /// // Gets the NetId for an object // returns true if the object has a NetId From e171e7e79e964cff9a1ebf833972c17c7993153e Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 22 Jun 2025 10:49:51 +1000 Subject: [PATCH 384/521] Refactor MultiplayerAPITest loading workflow --- MultiplayerAPI Tests/MultiplayerAPITest.cs | 36 +++++++++++++++++----- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/MultiplayerAPI Tests/MultiplayerAPITest.cs b/MultiplayerAPI Tests/MultiplayerAPITest.cs index fb6811e8..b74a2744 100644 --- a/MultiplayerAPI Tests/MultiplayerAPITest.cs +++ b/MultiplayerAPI Tests/MultiplayerAPITest.cs @@ -21,7 +21,7 @@ public static bool Load(UnityModManager.ModEntry modEntry) //Settings = Settings.Load(modEntry); //ModEntry.OnGUI = Settings.Draw; //ModEntry.OnSaveGUI = Settings.Save; - //ModEntry.OnLateUpdate = LateUpdate; + ModEntry.OnLateUpdate = LateUpdate; Harmony harmony = null; @@ -29,12 +29,6 @@ public static bool Load(UnityModManager.ModEntry modEntry) { Log($"Multiplayer Mod is loaded: {MultiplayerAPI.IsMultiplayerLoaded}"); - if (MultiplayerAPI.IsMultiplayerLoaded) - { - MultiplayerAPI.ServerStarted += OnServerStarted; - MultiplayerAPI.ClientStarted += OnClientStarted; - } - Log("Patching..."); harmony = new Harmony(ModEntry.Info.Id); harmony.PatchAll(); @@ -51,14 +45,42 @@ public static bool Load(UnityModManager.ModEntry modEntry) return true; } + + private static void LateUpdate(UnityModManager.ModEntry modEntry, float dt) + { + //Mod loading order can't be guaranteed, so we should wait for all mods to load prior to checking for multiplayer. + //Alternatively, set 'Multiplayer' in the 'LoadAfter' parameter in your 'info.json' + Log($"MultiplayerAPITest.LateUpdate() Multiplayer Mod is loaded: {MultiplayerAPI.IsMultiplayerLoaded}"); + if (MultiplayerAPI.IsMultiplayerLoaded) + { + //Register that this mod needs to be installed on both server and client + MultiplayerAPI.Instance.SetModCompatibility(ModEntry.Info.Id, MultiplayerCompatibility.All); + + // Register for server start and client start events + // Note: for a non dedicated server both server and client events will be fired + MultiplayerAPI.ServerStarted += OnServerStarted; + MultiplayerAPI.ClientStarted += OnClientStarted; + } + + modEntry.OnLateUpdate -= LateUpdate; + } + private static void OnServerStarted(IServer server) { + // How you handle the server starting is up to you + // In this test/example mod we are injecting a server manager into the scene, but you could + // also just integrate it into your mod's existing workflow. + // Keep in mind on a non-dedicated server, both the client and server will run concurrently GameObject go = new GameObject("MPAPI ServerTest", [typeof(ServerTest)]); GameObject.DontDestroyOnLoad(go); } private static void OnClientStarted(IClient client) { + // How you handle the client starting is up to you + // In this test/example mod we are injecting a client manager into the scene, but you could + // also just integrate it into your mod's existing workflow. + // Keep in mind on a non-dedicated host, both the client and server will run concurrently GameObject go = new GameObject("MPAPI ClientTest", [typeof(ClientTest)]); GameObject.DontDestroyOnLoad(go); } From 90c19c332eaf92fce5fc4f17fac98e5c7a9703ec Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 22 Jun 2025 10:50:16 +1000 Subject: [PATCH 385/521] Implement ClientTest --- .../TestComponents/ClientTest.cs | 196 +++++++++++++++++- 1 file changed, 187 insertions(+), 9 deletions(-) diff --git a/MultiplayerAPI Tests/TestComponents/ClientTest.cs b/MultiplayerAPI Tests/TestComponents/ClientTest.cs index 786daa0d..41e8352b 100644 --- a/MultiplayerAPI Tests/TestComponents/ClientTest.cs +++ b/MultiplayerAPI Tests/TestComponents/ClientTest.cs @@ -1,20 +1,198 @@ +using MPAPI; +using MPAPI.Interfaces; +using MultiplayerAPITest.Enums; +using MultiplayerAPITest.Packets; using System; -using System.Collections.Generic; -using System.Linq; using System.Text; -using System.Threading.Tasks; using UnityEngine; -namespace MultiplayerAPITest.TestComponents +namespace MultiplayerAPITest.TestComponents; + +internal class ClientTest : MonoBehaviour { - internal class ClientTest : MonoBehaviour + const string LogPrefix = "ClientTest"; + const int DELAY_INTERVAL = 10; // 10 seconds + + IClient client; + + uint lastLogTick; + + protected void Awake() + { + client = MultiplayerAPI.Client; + + // Subscribe to game tick events + MultiplayerAPI.Instance.OnTick += OnTick; + + // Subscribe to player events + client.OnPlayerConnected += OnPlayerConnected; + client.OnPlayerDisconnected += OnPlayerDisconnected; + + // Subscribe to packets + Subscribe(); + + // Check if we are also a host - some mods may need to do nothing on a client-only game + // e.g. Clients should not generate jobs + if (MultiplayerAPI.Instance.IsHost) + { + var gameType = MultiplayerAPI.Instance.IsSinglePlayer ? "single player" : "multiplayer"; + Log($"We are in a {gameType} self-hosted game"); + } + else if (MultiplayerAPI.Instance.IsDedicatedServer) + { + //Dedicated servers have not been implemented yet, IsDedicatedServer will always return false + Log("We are a dedicated server"); + } + else + { + Log("We are only a client game"); + } + } + + protected void Start() { } + + protected void Update() { } + + protected void OnDestroy() + { + // Unsubscribe from game tick events + MultiplayerAPI.Instance.OnTick -= OnTick; + + // Unsubscribe from player events + client.OnPlayerConnected -= OnPlayerConnected; + client.OnPlayerDisconnected -= OnPlayerDisconnected; + } + + private void Subscribe() + { + // Subscribe to network packets + // Note: only packets that will be received by client need to be registered here + client.RegisterPacket(OnTestSimpleModPacket); + client.RegisterPacket(OnSimplePacketWithNetId); + client.RegisterSerializablePacket(OnTestComplexModPacket); + } + + #region Example Tick Event + private void OnTick(uint tick) + { + // This event is called every tick + // This code is purely for testing purposes, not a recommened use case; normally it would be used for synchronising + // and batching changes or to track how long since an update has been received for a specific object. + + // The TICK_RATE is fixed at both client and server; currently the rate is 24 ticks/second + if ((tick - lastLogTick) > MultiplayerAPI.Instance.TICK_RATE * DELAY_INTERVAL) + { + //DELAY_INTERVAL (10 seconds) passed. + //log my ping + Log($"My current ping is {client.Ping} ms"); + + //log the ping for all players + StringBuilder sb = new($"Player pings at {tick}:"); + foreach (IPlayer player in client.Players) + sb.AppendLine($"\"{player?.Id}\" {player.Ping} ms"); + + Log(sb.ToString()); + + lastLogTick = tick; + } + } + #endregion + + #region Player Events + private void OnPlayerConnected(IPlayer player) + { + // This event is called when another player connects + + Log($"Player \"{player?.Id}\" has connected."); + } + + private void OnPlayerDisconnected(IPlayer player) { - protected void Awake() { } + // This event is called when another player disconnects - protected void Start() { } + Log($"Player \"{player?.Id}\" has connected."); + } + #endregion + + #region Packet Callbacks + //method called when a `TestSimplePacket` packet is received + private void OnTestSimpleModPacket(SimplePacket packet) + { + // We will just log this, but in a real use case you would validate the packet data, look up the referenced object and apply any updates required for your mod. + Log($"Received {packet.GetType()}, CarId: {packet.CarId}, Position: {packet.Position}, WheelArraangement: {packet.WheelArrangement}"); + + // For the purposes of testing and example, we will send the data back to the server + SendSimplePacket(packet.CarId, packet.Position, packet.WheelArrangement); + } + + //method called when a `TestSimplePacket` packet is received + private void OnSimplePacketWithNetId(SimplePacketWithNetId packet) + { + // Let's locate the car + + if (packet.CarNetId == 0) + { + LogWarning("Received SimplePacketWithNetId with a CarNetId of 0!"); + return; + } - protected void Update() { } + if(!MultiplayerAPI.Instance.TryGetObjectFromNetId(packet.CarNetId, out TrainCar car)) + { + LogWarning($"Received SimplePacketWithNetId with a CarNetId of {packet.CarNetId}, but TrainCar was not found!"); + return; + } - protected void OnDestroy() { } + Log($"Received {packet.GetType()}, CarNetId: {packet.CarNetId}, CarId: {car.ID}, Car Livery: {car.carLivery}, Position: {packet.Position}, Wheel Arrangement: {packet.WheelArrangement}"); } + + //method called when a `TestComplexModPacket` packet is received + private void OnTestComplexModPacket(ComplexModPacket packet) + { + StringBuilder sb = new($"Received {packet.GetType()}\r\nPacket Data"); + + foreach (var kvp in packet.CarToPositionMap) + sb.AppendLine($"\tCarId: {kvp.Key}, Position: {kvp.Value}"); + + Log(sb.ToString()); + } + #endregion + + #region Packet Senders + public void SendSimplePacket(string carId, Vector3 position, WheelArrangement arrangement) + { + SimplePacket packet = new() + { + CarId = carId, + Position = position, + WheelArrangement = arrangement + }; + + //send the packet reliably + client.SendPacketToServer(packet, true); + } + #endregion + + #region Logging + + public void LogDebug(Func resolver) + { + MultiplayerAPITest.LogDebug(() => $"{LogPrefix} {resolver?.Invoke()}"); + } + + public void Log(object msg) + { + MultiplayerAPITest.Log($"{LogPrefix} {msg}"); + } + + public void LogWarning(object msg) + { + MultiplayerAPITest.LogWarning($"{LogPrefix} {msg}"); + } + + public void LogError(object msg) + { + MultiplayerAPITest.LogError($"{LogPrefix} {msg}"); + } + + #endregion } From d62f235f8bb24d73c9aae9d6cb67f0048ecc24d0 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 22 Jun 2025 12:27:48 +1000 Subject: [PATCH 386/521] Add calls to Player events in NetworkServer --- Multiplayer/Networking/Managers/Server/NetworkServer.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index 943fb7ab..4f20173f 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -71,7 +71,7 @@ public class NetworkServer : NetworkManager public NetworkServer(IDifficulty difficulty, Settings settings, bool singlePlayer, LobbyServerData serverData) : base(settings) { - Log(() => $"Server created for {(singlePlayer ? "single player" : "multiplayer")} game"); + Log($"Server created for {(singlePlayer ? "single player" : "multiplayer")} game"); IsSinglePlayer = singlePlayer; ServerData = serverData; @@ -790,6 +790,8 @@ private void OnServerboundClientLoginPacket(ServerboundClientLoginPacket packet, serverPlayers.Add(serverPlayer.Id, serverPlayer); peerToPlayer.Add(peer, serverPlayer); + PlayerConnected?.Invoke(serverPlayer); + ClientboundLoginResponsePacket acceptPacket = new() { Accepted = true, @@ -855,6 +857,8 @@ private void OnServerboundClientReadyPacket(ServerboundClientReadyPacket packet, if (NetworkLifecycle.Instance.IsHost(peer)) { SendPacket(peer, new ClientboundRemoveLoadingScreenPacket(), DeliveryMethod.ReliableOrdered); + serverPlayer.IsLoaded = true; + PlayerReady?.Invoke(serverPlayer); return; } @@ -929,6 +933,7 @@ private void OnServerboundClientReadyPacket(ServerboundClientReadyPacket packet, serverPlayer.IsLoaded = true; + PlayerReady?.Invoke(serverPlayer); } private void OnServerboundPlayerPositionPacket(ServerboundPlayerPositionPacket packet, ITransportPeer peer) From a1e144245692207869672fba4012a5b5fe027f52 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 22 Jun 2025 12:28:40 +1000 Subject: [PATCH 387/521] Ensure Multiplayer registers its own compatibility --- Multiplayer/API/ModCompatibilityManager.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Multiplayer/API/ModCompatibilityManager.cs b/Multiplayer/API/ModCompatibilityManager.cs index aff50311..57913d4c 100644 --- a/Multiplayer/API/ModCompatibilityManager.cs +++ b/Multiplayer/API/ModCompatibilityManager.cs @@ -25,6 +25,9 @@ protected override void Awake() DontDestroyOnLoad(this); + //Register ourselves + RegisterCompatibility("Multiplayer", MultiplayerCompatibility.All); + //we don't care if the client does/doesn't have these mods RegisterCompatibility("RuntimeUnityEditor", MultiplayerCompatibility.Client); RegisterCompatibility("BookletOrganizer", MultiplayerCompatibility.Client); From 75d1cdb791faff747992e0c9df6acc6ab6f9d4c7 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 22 Jun 2025 12:40:44 +1000 Subject: [PATCH 388/521] Remove errant null check in SendPacketToAll API --- Multiplayer/API/ServerAPIProvider.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Multiplayer/API/ServerAPIProvider.cs b/Multiplayer/API/ServerAPIProvider.cs index a4bf6b53..2e4a7983 100644 --- a/Multiplayer/API/ServerAPIProvider.cs +++ b/Multiplayer/API/ServerAPIProvider.cs @@ -52,8 +52,7 @@ public IPlayer GetPlayer(byte id) if (excludePlayer != null) peer = GetPeerFromPlayer(excludePlayer, $"SendPacketToAll<{typeof(T).Name}>"); - if (peer != null) - server.SendExternalPacketToAll(packet, reliable, peer); + server.SendExternalPacketToAll(packet, reliable, peer); } public void SendSerializablePacketToAll(T packet, bool reliable = true, IPlayer excludePlayer = null) where T : class, ISerializablePacket, new() From ff98c24e77d45cc23968bc19ecb44c76b9f4c3e0 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 22 Jun 2025 12:29:48 +1000 Subject: [PATCH 389/521] Add PlayerCount to IClient --- Multiplayer/API/ClientAPIProvider.cs | 1 + .../TestComponents/ClientTest.cs | 32 ++++++++++------- MultiplayerAPI/Interfaces/IClient.cs | 35 +++++++++++++++++-- 3 files changed, 52 insertions(+), 16 deletions(-) diff --git a/Multiplayer/API/ClientAPIProvider.cs b/Multiplayer/API/ClientAPIProvider.cs index 9bf59729..84affd1e 100644 --- a/Multiplayer/API/ClientAPIProvider.cs +++ b/Multiplayer/API/ClientAPIProvider.cs @@ -19,6 +19,7 @@ public class ClientAPIProvider : IClient #region Client Properties public IReadOnlyCollection Players => client.ClientPlayerManager.Players.Select(GetWrapper).ToList().AsReadOnly(); + public int PlayerCount => client.ClientPlayerManager.Players.Count + 1; // add 1 for local player public IPlayer GetPlayer(byte id) { diff --git a/MultiplayerAPI Tests/TestComponents/ClientTest.cs b/MultiplayerAPI Tests/TestComponents/ClientTest.cs index 41e8352b..6130a83e 100644 --- a/MultiplayerAPI Tests/TestComponents/ClientTest.cs +++ b/MultiplayerAPI Tests/TestComponents/ClientTest.cs @@ -35,13 +35,16 @@ protected void Awake() // e.g. Clients should not generate jobs if (MultiplayerAPI.Instance.IsHost) { - var gameType = MultiplayerAPI.Instance.IsSinglePlayer ? "single player" : "multiplayer"; - Log($"We are in a {gameType} self-hosted game"); - } - else if (MultiplayerAPI.Instance.IsDedicatedServer) - { - //Dedicated servers have not been implemented yet, IsDedicatedServer will always return false - Log("We are a dedicated server"); + if (MultiplayerAPI.Instance.IsDedicatedServer) + { + //Dedicated servers have not been implemented yet, IsDedicatedServer will always return false + Log("We are a dedicated server"); + } + else + { + var gameType = MultiplayerAPI.Instance.IsSinglePlayer ? "single player" : "multiplayer"; + Log($"We are in a {gameType} self-hosted game"); + } } else { @@ -86,12 +89,15 @@ private void OnTick(uint tick) //log my ping Log($"My current ping is {client.Ping} ms"); - //log the ping for all players - StringBuilder sb = new($"Player pings at {tick}:"); - foreach (IPlayer player in client.Players) - sb.AppendLine($"\"{player?.Id}\" {player.Ping} ms"); + //Log the ping for all players + if (client.PlayerCount > 1) + { + StringBuilder sb = new($"Tick {tick}.\r\nThere are {client.PlayerCount} players, their pings are:"); + foreach (IPlayer player in client.Players) + sb.AppendLine($"\"{player?.Id}\" {player.Ping} ms"); - Log(sb.ToString()); + Log(sb.ToString()); + } lastLogTick = tick; } @@ -136,7 +142,7 @@ private void OnSimplePacketWithNetId(SimplePacketWithNetId packet) return; } - if(!MultiplayerAPI.Instance.TryGetObjectFromNetId(packet.CarNetId, out TrainCar car)) + if (!MultiplayerAPI.Instance.TryGetObjectFromNetId(packet.CarNetId, out TrainCar car)) { LogWarning($"Received SimplePacketWithNetId with a CarNetId of {packet.CarNetId}, but TrainCar was not found!"); return; diff --git a/MultiplayerAPI/Interfaces/IClient.cs b/MultiplayerAPI/Interfaces/IClient.cs index 15f3da4d..0f320caf 100644 --- a/MultiplayerAPI/Interfaces/IClient.cs +++ b/MultiplayerAPI/Interfaces/IClient.cs @@ -7,16 +7,45 @@ namespace MPAPI.Interfaces; public interface IClient { - + /// + /// Event fired when a player connects. + /// + /// IPlayer object for the connected player event Action OnPlayerConnected; + + /// + /// Event fired when a player disconnects, but before the IPlayer object is destroyed + /// + /// IPlayer object for the disconnected player event Action OnPlayerDisconnected; - // Player access + + /// + /// Gets IPlayer objects for all players connected to the server + /// + /// Read-only collection of IPlayer objects IReadOnlyCollection Players { get; } + + /// + /// Gets number of players currently connected to the server + /// + /// Positive integer representing the number of connected players + int PlayerCount { get; } + + /// + /// Gets IPlayer for player by Id + /// + /// IPlayer object if found, otherwise null IPlayer GetPlayer(byte id); - // Client info + /// + /// Gets connection state for the client + /// bool IsConnected { get; } + + /// + /// Gets ping for the client + /// int Ping { get; } #region Packet API From 33ca527f58fd9b17e7af8cb357690ea4a3b9cb84 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 22 Jun 2025 13:06:29 +1000 Subject: [PATCH 390/521] Update IClient.cs --- MultiplayerAPI/Interfaces/IClient.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/MultiplayerAPI/Interfaces/IClient.cs b/MultiplayerAPI/Interfaces/IClient.cs index 0f320caf..9110abde 100644 --- a/MultiplayerAPI/Interfaces/IClient.cs +++ b/MultiplayerAPI/Interfaces/IClient.cs @@ -10,32 +10,32 @@ public interface IClient /// /// Event fired when a player connects. /// - /// IPlayer object for the connected player + /// IPlayer object for the connected player event Action OnPlayerConnected; /// /// Event fired when a player disconnects, but before the IPlayer object is destroyed /// - /// IPlayer object for the disconnected player + /// IPlayer object for the disconnected player event Action OnPlayerDisconnected; /// /// Gets IPlayer objects for all players connected to the server /// - /// Read-only collection of IPlayer objects + /// Read-only collection of IPlayer objects IReadOnlyCollection Players { get; } /// /// Gets number of players currently connected to the server /// - /// Positive integer representing the number of connected players + /// Positive integer representing the number of connected players int PlayerCount { get; } /// /// Gets IPlayer for player by Id /// - /// IPlayer object if found, otherwise null + /// IPlayer object if found, otherwise null IPlayer GetPlayer(byte id); /// From 8a57d0a488772065e8864f0cf8caddb38513d7e4 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 22 Jun 2025 13:10:40 +1000 Subject: [PATCH 391/521] Update documentation --- MultiplayerAPI/Interfaces/IClient.cs | 3 ++ MultiplayerAPI/Interfaces/IMultiplayerAPI.cs | 13 +++--- MultiplayerAPI/Interfaces/IServer.cs | 42 +++++++++++++++++--- 3 files changed, 48 insertions(+), 10 deletions(-) diff --git a/MultiplayerAPI/Interfaces/IClient.cs b/MultiplayerAPI/Interfaces/IClient.cs index 9110abde..ffb277d4 100644 --- a/MultiplayerAPI/Interfaces/IClient.cs +++ b/MultiplayerAPI/Interfaces/IClient.cs @@ -5,6 +5,9 @@ namespace MPAPI.Interfaces; +/// +/// Interface for interacting with Multiplayer mod client instances +/// public interface IClient { /// diff --git a/MultiplayerAPI/Interfaces/IMultiplayerAPI.cs b/MultiplayerAPI/Interfaces/IMultiplayerAPI.cs index 8cf945fe..aa5c6337 100644 --- a/MultiplayerAPI/Interfaces/IMultiplayerAPI.cs +++ b/MultiplayerAPI/Interfaces/IMultiplayerAPI.cs @@ -13,7 +13,6 @@ public interface IMultiplayerAPI /// bool IsMultiplayerLoaded { get; } - /// Sets the mod's compatibility requirements /// String representing the your mod's Id (`ModEntry.Info.Id`) /// ModCompatibility flags representing installation host/client requirements @@ -62,15 +61,19 @@ public interface IMultiplayerAPI uint CurrentTick { get; } /// - // Gets the NetId for an object - // returns true if the object has a NetId + /// Gets the NetId for an object /// + /// The object you want the NetId for + /// When this method returns, contains the NetId associated with the specified object, if found; otherwise, 0 + /// True if a NetId for the object was found; otherwise, false bool TryGetNetId(T obj, out ushort netId) where T : class; /// - // Gets the object for an NetId - // returns true if the object was found + /// Gets the object for a NetId /// + /// The non-zero NetId for the object + /// When this method returns, contains the object associated with the NetId, if found; otherwise null + /// True if the object was found; otherwise, false bool TryGetObjectFromNetId(ushort netId, out T obj) where T : class; } diff --git a/MultiplayerAPI/Interfaces/IServer.cs b/MultiplayerAPI/Interfaces/IServer.cs index d2bbe3d9..dcb16ac4 100644 --- a/MultiplayerAPI/Interfaces/IServer.cs +++ b/MultiplayerAPI/Interfaces/IServer.cs @@ -4,31 +4,63 @@ using System.Collections.Generic; using UnityEngine; - namespace MPAPI.Interfaces; +/// +/// Represents the method that will handle chat command execution +/// +/// The message content without the command prefix (e.g. '/command parameter1 parameter2' becomes 'parameter1 parameter2') +/// The player who executed the command public delegate void ChatCommandCallback(string message, IPlayer sender); + +/// +/// Represents the method that will handle chat message filtering +/// +/// The message content that can be modified by reference +/// The player who sent the message +/// True to pass the message to the next filter or send to players; false to block the message from further processing public delegate bool ChatFilterDelegate(ref string message, IPlayer sender); + +/// +/// Interface for interacting with Multiplayer mod server instances +/// public interface IServer { + + /// + /// Event fired when a player connects and is authenticated, but before the player receives game state information + /// event Action OnPlayerConnected; + + /// + /// Event fired when a player disconnects, but before the IPlayer object is destroyed + /// event Action OnPlayerDisconnected; + + /// + /// Event fired when a player has signalled they are ready for game state information + /// This event occurs after the server has sent the game state, it does not guarantee the player has finished generating all cars, jobs, etc. + /// event Action OnPlayerReady; #region Server Properties - /// /// Gets number of players currently connected to the server /// - /// Positive integer representing the number of connected players + /// Positive integer representing the number of connected players int PlayerCount { get; } /// /// Gets IPlayer objects for all players connected to the server /// - /// Read-only collection of IPlayer objects + /// Read-only collection of IPlayer objects public IReadOnlyCollection Players { get; } + /// + /// Gets IPlayer for player by Id + /// + /// Id for the player + /// IPlayer object if found, otherwise null IPlayer GetPlayer(byte id); #endregion @@ -122,7 +154,7 @@ public interface IServer /// /// Command to be filtered for, without a leading '/' e.g. 'server' /// Optional short command to be filtered for, without a leading '/' e.g. 's' - /// Optional callback for a help message e.g. "Send a message as the server (host only)\r\n\t\t/server \r\n\t\t/s " It is recommended to provide localisation/translation for this string + /// Optional callback for a help message e.g. \r\n\t\t/s "]]>. It is recommended to provide localisation/translation for this string /// Action to execute when the command is triggered. First parameter contains message without the command e.g. '/command parameter1 parameter2' will become 'parameter1 parameter2', second parameter is the player who executed the command. /// True if the command was successfully registered, false if registration failed (e.g. command already exists). bool RegisterChatCommand(string commandLong, string commandShort, Func helpMessage, ChatCommandCallback callback); From 11feeccbe295b4dfd499e59645048ade5a150d49 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 22 Jun 2025 13:12:05 +1000 Subject: [PATCH 392/521] Refactor and bug fixes for ServerTest chat commands --- .../TestComponents/ServerTest.cs | 63 ++++++++++++------- 1 file changed, 41 insertions(+), 22 deletions(-) diff --git a/MultiplayerAPI Tests/TestComponents/ServerTest.cs b/MultiplayerAPI Tests/TestComponents/ServerTest.cs index 85cad6c0..c9d3b37e 100644 --- a/MultiplayerAPI Tests/TestComponents/ServerTest.cs +++ b/MultiplayerAPI Tests/TestComponents/ServerTest.cs @@ -16,7 +16,7 @@ internal class ServerTest : MonoBehaviour { const string LogPrefix = "ServerTest"; const string MESSAGE_COLOUR_SERVER = "9CDCFE"; - const int DELAY_INTERVAL = 10; // 10 seconds + const int DELAY_INTERVAL = 20; // seconds uint lastLogTick = 0; @@ -80,8 +80,13 @@ private void OnTick(uint tick) // The TICK_RATE is fixed at both client and server; currently the rate is 24 ticks/second if ((tick - lastLogTick) > MultiplayerAPI.Instance.TICK_RATE * DELAY_INTERVAL) { - //DELAY_INTERVAL (10 seconds) passed, log the ping for all players - StringBuilder sb = new($"Player pings at {tick}:"); + //Log the ping for all players + if (server.PlayerCount == 0) + { + Log($"Tick {tick}.\r\nThere are no players connected"); + } + + StringBuilder sb = new($"Tick {tick}.\r\nThere are {server.PlayerCount} players, their pings are:"); foreach (IPlayer player in server.Players) sb.AppendLine($"\"{player?.Id}\" {player.Ping} ms"); @@ -98,7 +103,7 @@ private void OnPlayerConnected(IPlayer player) // Send mod settings, parameters, etc. // Note: This event occurs when the player is authenticated and before the player receives game state info - Log($"Player \"{player?.Id}\" has connected. (Is Loaded: {player?.IsLoaded})"); + Log($"Player {player?.Id} (\"{player?.Username}\") has connected. (Is Loaded: {player?.IsLoaded})"); } private void OnPlayerReady(IPlayer player) @@ -202,44 +207,54 @@ private void OnChatCommandSendPacket(string message, IPlayer sender) return; } + if (string.IsNullOrEmpty(args[0])) + { + LogWarning($"Received 'SendPacket' chat command from player \"{sender.Username}\", but the first argument is empty. Command: {message}"); + whisper = $"Not enough arguments supplied. Type /? for help."; + server.SendWhisperChatMessage(whisper, sender); + return; + } + if (string.IsNullOrEmpty(args[1])) { LogWarning($"Received 'SendPacket' chat command from player \"{sender.Username}\", but the second argument is empty. Command: {message}"); } - var tc = GetTrainCarFromID(args[1]); + LogDebug(() => $"OnChatCommandSendPacket({message}, {sender?.Username}) post-args checks"); + + var tc = GetTrainCarFromID(args[1].ToUpper()); + + if (tc == null) + { + // Send a whisper back to the player who sent the command + whisper = $"TrainCar '{args[1]}' not found"; + server.SendWhisperChatMessage(whisper, sender); + return; + } + var pos = tc.transform.position - WorldMover.currentMove; switch (args[0].ToLower()) { case "simple": //send a simple packet - - if (tc) - { - // Send a simple packet to all players using TrainCar id as a string, TrainCar position and a random wheel arrangement - SendSimplePacketToAll(args[1], pos, GetRandomWheelArrangement()); - } - else - { - // Send a whisper back to the player who sent the command - whisper = $"TrainCar '{args[1]}' not found"; - server.SendWhisperChatMessage(whisper, sender); - } + // Send a simple packet to all players using TrainCar id as a string, TrainCar position and a random wheel arrangement + SendSimplePacketToAll(args[1], pos, GetRandomWheelArrangement()); + whisper = $"Sending simple packet for '{args[1]}'"; break; case "net": //send a simple packet using a netId - if (tc && MultiplayerAPI.Instance.TryGetNetId(tc, out ushort netId)) + if (MultiplayerAPI.Instance.TryGetNetId(tc, out ushort netId)) { // Send a simple packet to all players using TrainCar NetId, TrainCar position and a random wheel arrangement SendSimplePacketWithNetIdToAll(netId, pos, GetRandomWheelArrangement()); + whisper = $"Sending net packet for '{args[1]}'"; } else { - // Send a whisper back to the player who sent the command - whisper = $"TrainCar '{args[1]}' not found"; - server.SendWhisperChatMessage(whisper, sender); + whisper = $"NetId not found for TrainCar '{args[1]}'"; } + break; default: @@ -247,10 +262,11 @@ private void OnChatCommandSendPacket(string message, IPlayer sender) // Send a whisper back to the player who sent the command whisper = $"Packet type '{args[0].ToLower()}' was not recognised"; - server.SendWhisperChatMessage(whisper, sender); break; } + + server.SendWhisperChatMessage(whisper, sender); } // Called when a player uses the chat command '/locopos' or '/lp' @@ -274,6 +290,9 @@ private void OnChatCommandSendLocoPos(string message, IPlayer sender) if (carMap.Count > 0) SendComplexPacket(carMap); + + var whisper = $"Loco Position packet sent"; + server.SendWhisperChatMessage(whisper, sender); } private void OnChatCommandClosestPlayer(string message, IPlayer sender) From c1a0676460c6e47a3987eacf0074ac737d579516 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 22 Jun 2025 13:22:32 +1000 Subject: [PATCH 393/521] Ready for testing and release --- Multiplayer/API/APIProvider.cs | 20 +++++++++++++- MultiplayerAPI Tests/MultiplayerAPITest.cs | 2 +- MultiplayerAPI/Interfaces/IMultiplayerAPI.cs | 5 ++++ MultiplayerAPI/MultiplayerAPI.csproj | 29 +++++++++++++------- 4 files changed, 44 insertions(+), 12 deletions(-) diff --git a/Multiplayer/API/APIProvider.cs b/Multiplayer/API/APIProvider.cs index f5baef73..2db25680 100644 --- a/Multiplayer/API/APIProvider.cs +++ b/Multiplayer/API/APIProvider.cs @@ -1,15 +1,33 @@ +using MPAPI; using MPAPI.Interfaces; using MPAPI.Types; using Multiplayer.Components.Networking; using System; +using System.Linq; +using System.Reflection; namespace Multiplayer.API { public class APIProvider : IMultiplayerAPI { + public string Version + { + get + { + AssemblyInformationalVersionAttribute info = (AssemblyInformationalVersionAttribute)typeof(MultiplayerAPI).Assembly. + GetCustomAttributes(typeof(AssemblyInformationalVersionAttribute), false) + .FirstOrDefault(); + + if (info == null) + return ""; + + return info.InformationalVersion.Split('+')[0]; + } + } + public bool IsMultiplayerLoaded => true; - + public bool IsConnected => NetworkLifecycle.Instance.IsClientRunning || NetworkLifecycle.Instance.IsServerRunning; public bool IsHost => NetworkLifecycle.Instance.IsHost(); diff --git a/MultiplayerAPI Tests/MultiplayerAPITest.cs b/MultiplayerAPI Tests/MultiplayerAPITest.cs index b74a2744..82491343 100644 --- a/MultiplayerAPI Tests/MultiplayerAPITest.cs +++ b/MultiplayerAPI Tests/MultiplayerAPITest.cs @@ -50,7 +50,7 @@ private static void LateUpdate(UnityModManager.ModEntry modEntry, float dt) { //Mod loading order can't be guaranteed, so we should wait for all mods to load prior to checking for multiplayer. //Alternatively, set 'Multiplayer' in the 'LoadAfter' parameter in your 'info.json' - Log($"MultiplayerAPITest.LateUpdate() Multiplayer Mod is loaded: {MultiplayerAPI.IsMultiplayerLoaded}"); + Log($"MultiplayerAPITest.LateUpdate() Multiplayer Mod is loaded: {MultiplayerAPI.IsMultiplayerLoaded}, API Version: {MultiplayerAPI.Instance.Version} "); if (MultiplayerAPI.IsMultiplayerLoaded) { //Register that this mod needs to be installed on both server and client diff --git a/MultiplayerAPI/Interfaces/IMultiplayerAPI.cs b/MultiplayerAPI/Interfaces/IMultiplayerAPI.cs index aa5c6337..1a0dcffc 100644 --- a/MultiplayerAPI/Interfaces/IMultiplayerAPI.cs +++ b/MultiplayerAPI/Interfaces/IMultiplayerAPI.cs @@ -8,6 +8,11 @@ namespace MPAPI.Interfaces; /// public interface IMultiplayerAPI { + /// + /// Returns the version of the Multiplayer API if multiplayer is loaded, otherwise returns null + /// + public string Version { get; } + /// /// Gets whether the multiplayer mod is currently loaded and active /// diff --git a/MultiplayerAPI/MultiplayerAPI.csproj b/MultiplayerAPI/MultiplayerAPI.csproj index ca635830..475c5807 100644 --- a/MultiplayerAPI/MultiplayerAPI.csproj +++ b/MultiplayerAPI/MultiplayerAPI.csproj @@ -4,11 +4,28 @@ net48 latest MPAPI - 0.0.0.0 + 0.1.0 + 0.1.0 + + true DVMultiplayerAPI + Derail Valley Multiplayer API Macka - API for interfacing with DV Multiplayer mod + API for interfacing with DV Multiplayer mod. Provides events and interfaces for server/client interactions in Derail Valley multiplayer scenarios. + derail-valley;multiplayer;gaming;api;mod + https://github.com/AMacro/dv-multiplayer + https://github.com/AMacro/dv-multiplayer + git + MIT + Initial release of DV Multiplayer API + false + + + true + bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml + true + snupkg @@ -17,14 +34,6 @@ - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - From f3e1c9db79aba60fa71492fe67696055dbaf4ca7 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 22 Jun 2025 20:27:38 +1000 Subject: [PATCH 394/521] Fix bulk sync and car selection changes --- .../World/NetworkedPitStopStation.cs | 83 +++++++++++++++++-- .../Networking/Data/LocoResourceModuleData.cs | 30 ++++++- .../Managers/Client/NetworkClient.cs | 2 +- .../Managers/Server/NetworkServer.cs | 8 +- .../ClientboundPitStopBulkUpdatePacket.cs | 1 + 5 files changed, 108 insertions(+), 16 deletions(-) diff --git a/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs b/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs index 4824bd28..1afe7de0 100644 --- a/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs +++ b/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs @@ -299,6 +299,9 @@ private IEnumerator PlayerDistanceChecker() if (Station.pitstop.IsCarInPitStop()) { + // Ensure all resource data exists + InitialiseData(); + // One struct per module type var resourceCount = Station.locoResourceModules.resourceModules.Count(); LocoResourceModuleData[] stateData = new LocoResourceModuleData[resourceCount]; @@ -308,6 +311,9 @@ private IEnumerator PlayerDistanceChecker() stateData[i] = LocoResourceModuleData.From(Station.locoResourceModules.resourceModules[i]); } + // Car selection and lever states + int carIndex = Station.pitstop.SelectedIndex; + PitStopPlugData[] plugData = new PitStopPlugData[resourceToPluggableObject.Count]; i = 0; @@ -318,7 +324,7 @@ private IEnumerator PlayerDistanceChecker() } // Send current state - NetworkLifecycle.Instance.Server.SendPitStopBulkDataPacket(NetId, Station.pitstop.carList.Count, stateData, plugData, peer); + NetworkLifecycle.Instance.Server.SendPitStopBulkDataPacket(NetId, Station.pitstop.carList.Count, carIndex, stateData, plugData, peer); } } } @@ -559,13 +565,16 @@ private IEnumerator WaitForLoad(ClientboundPitStopBulkUpdatePacket packet) || (Time.time - time) > LOADING_TIMEOUT ); - if ((Time.time - time) < LOADING_TIMEOUT) + yield return new WaitForEndOfFrame(); + yield return new WaitForEndOfFrame(); + + if ((Time.time - time) <= LOADING_TIMEOUT) { ProcessBulkUpdate(packet); } else { - Multiplayer.LogWarning($"PitStop [{StationName}] timed out waiting for load. PitStop Initialised: {initialised}, Car Count Matched: {packet.CarCount == Station.pitstop?.carList?.Count}"); + Multiplayer.LogWarning($"PitStop [{StationName}] timed out waiting for load. PitStop Initialised: {initialised}, Packet Car Count: {packet.CarCount}, Station Car Count: {Station.pitstop?.carList?.Count}, Car Count Matched: {packet.CarCount == Station.pitstop?.carList?.Count}"); if (initialised) Refreshed = true; //lets hope the car sync is just a little slow } @@ -760,6 +769,7 @@ public void ProcessBulkUpdate(ClientboundPitStopBulkUpdatePacket packet) return; } + Multiplayer.LogDebug(() => $"ProcessBulkUpdate() car count: {packet.CarCount}, resource data count: {packet.ResourceData.Count()}, resource data: [{string.Join(", ", packet.ResourceData.Select(x => $"{x.ResourceType}: {{{string.Join(", ", x.Values)}}}"))}]"); // Make sure the data elements exist prior to attempting to load them InitialiseData(); @@ -769,24 +779,72 @@ public void ProcessBulkUpdate(ClientboundPitStopBulkUpdatePacket packet) foreach (var resource in packet.ResourceData) { if (!resourceTypeToLocoResourceModule.TryGetValue(resource.ResourceType, out var module)) + { + Multiplayer.LogDebug(() => $"ProcessBulkUpdate() Failed to find resource module for type {resource.ResourceType}"); continue; + } if (module != null) + { if (module.resourceData.Count == resource.Values.Count()) + { for (int i = 0; i < module.resourceData.Count; i++) + { module.resourceData[i].unitsToBuy = resource.Values[i]; + } + + } else + { + Multiplayer.LogWarning($"PitStop bulk data count mismatch post-force: {module.resourceData.Count} != {resource.Values.Count()}"); + } + + } else Multiplayer.LogWarning($"PitStop module not found for resource type: {resource.ResourceType}"); + + //set the grab state + bool grabbed = (resource.FillingState != LocoResourceModuleFillingState.None); + bool isLocallyGrabbed = isResourceGrabbedDict.TryGetValue(resource.ResourceType, out var localGrabbed) && localGrabbed; + + leverLookup.TryGetValue(resource.ResourceType, out LeverNonVR lever); + grabbedHandlerLookup.TryGetValue(resource.ResourceType, out GrabHandlerHingeJoint grab); + + if (!isLocallyGrabbed) + { + lever?.BlockControl(grabbed); + grab?.SetMovingDisabled(grabbed); + + if (grabbed) + grab?.ForceEndInteraction(); + } + + int valvePos = resource.FillingState switch + { + LocoResourceModuleFillingState.Filling => -1, + LocoResourceModuleFillingState.Draining => 1, + _ => 0 + }; + + module.OnValvePositionChange(valvePos); + + // Update remote grab state + isResourceRemoteGrabbedDict[resource.ResourceType] = grabbed; } - Multiplayer.LogDebug(() => $"PitStop bulk data Plugs"); + Multiplayer.LogDebug(() => $"PitStop bulk data Car Index: {packet.CarSelection}"); + SetCarSelection(packet.CarSelection); + + + Multiplayer.LogDebug(() => $"PitStop bulk data Plugs {packet.PlugData.Count()}"); //sync plugs foreach (var plug in packet.PlugData) { - NetworkedPluggableObject.Get(plug.NetId, out var netPlug); + var result = NetworkedPluggableObject.Get(plug.NetId, out var netPlug); + Multiplayer.LogDebug(() => $"PitStop bulk data Plugs netId: {plug.NetId}, found: {result}"); + netPlug?.ProcessBulkUpdate(plug); } @@ -805,6 +863,7 @@ public void ProcessInteractionPacketAsClient(CommonPitStopInteractionPacket pack GrabHandlerHingeJoint grab = null; RotaryAmplitudeChecker amplitudeChecker = null; LeverNonVR lever = null; + LocoResourceModule resourceModule = null; // Validate interaction type if (!Enum.IsDefined(typeof(PitStopStationInteractionType), packet.InteractionType)) @@ -815,8 +874,16 @@ public void ProcessInteractionPacketAsClient(CommonPitStopInteractionPacket pack PitStopStationInteractionType interactionType = (PitStopStationInteractionType)packet.InteractionType; - // Validate resource type - if (!Enum.IsDefined(typeof(ResourceType), packet.ResourceType)) + bool isCarSelection = interactionType switch + { + PitStopStationInteractionType.CarSelectorGrab => true, + PitStopStationInteractionType.CarSelectorUngrab => true, + PitStopStationInteractionType.CarSelection => true, + _ => false, + }; + + // Validate resource type (no resource type for car selectors + if (!isCarSelection && !Enum.IsDefined(typeof(ResourceType), packet.ResourceType)) { Multiplayer.LogWarning($"Received invalid ResourceType \"{packet.ResourceType}\" at Pit Stop station {StationName}"); return; @@ -827,7 +894,7 @@ public void ProcessInteractionPacketAsClient(CommonPitStopInteractionPacket pack Multiplayer.LogDebug(() => $"NetworkedPitStopStation.ProcessPacket() [{StationName}, {NetId}] {interactionType}, resource type: {resourceType}, state: {packet.Value}"); // Validate resource module exists - if (!resourceTypeToLocoResourceModule.TryGetValue(resourceType, out LocoResourceModule resourceModule)) + if (!isCarSelection && !resourceTypeToLocoResourceModule.TryGetValue(resourceType, out resourceModule)) { Multiplayer.LogWarning($"Could not find LocoResourceModule for ResourceType \"{resourceType}\" at Pit Stop station {StationName}"); return; diff --git a/Multiplayer/Networking/Data/LocoResourceModuleData.cs b/Multiplayer/Networking/Data/LocoResourceModuleData.cs index 59c76ce4..fb7d9101 100644 --- a/Multiplayer/Networking/Data/LocoResourceModuleData.cs +++ b/Multiplayer/Networking/Data/LocoResourceModuleData.cs @@ -1,21 +1,39 @@ using DV.ThingTypes; using LiteNetLib.Utils; -using System.Collections.Generic; using System.Linq; namespace Multiplayer.Networking.Data; -public readonly struct LocoResourceModuleData(ResourceType resourceType, float[] values) +public enum LocoResourceModuleFillingState : byte +{ + None = 0, + Filling = 1, + Draining = 2, +} +public readonly struct LocoResourceModuleData(ResourceType resourceType, float[] values, LocoResourceModuleFillingState fillingState) { public readonly ResourceType ResourceType = resourceType; public readonly float[] Values = values; + public readonly LocoResourceModuleFillingState FillingState = fillingState; public static LocoResourceModuleData From(LocoResourceModule resources) { //extract floats var values = resources.resourceData.Select(d => d.unitsToBuy).ToArray(); - return new LocoResourceModuleData(resources.resourceType, values); + LocoResourceModuleFillingState fillingState = LocoResourceModuleFillingState.None; + if (resources.isFilling) + { + fillingState = LocoResourceModuleFillingState.Filling; + } + else if (resources.isDraining) + { + fillingState = LocoResourceModuleFillingState.Draining; + } + + Multiplayer.LogDebug(() => $"LocoResourceModuleData.From({resources.resourceType}) values count: {values.Length}, values: [{string.Join(", ", values)}]"); + + return new LocoResourceModuleData(resources.resourceType, values, fillingState); } public static void Serialize(NetDataWriter writer, LocoResourceModuleData data) @@ -25,6 +43,8 @@ public static void Serialize(NetDataWriter writer, LocoResourceModuleData data) writer.Put(data.Values.Count()); foreach (var val in data.Values) writer.Put(val); + + writer.Put((byte)data.FillingState); } public static LocoResourceModuleData Deserialize(NetDataReader reader) @@ -37,6 +57,8 @@ public static LocoResourceModuleData Deserialize(NetDataReader reader) for (int i = 0; i < valueCount; i++) states[i] = reader.GetFloat(); - return new LocoResourceModuleData(type, states); + LocoResourceModuleFillingState fillingState = (LocoResourceModuleFillingState)reader.GetByte(); + + return new LocoResourceModuleData(type, states, fillingState); } } diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 97900a20..35944fed 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -1417,7 +1417,7 @@ public void SendPitStopPlugInteractionPacket(ushort netId, PlugInteractionType i { LogDebug(()=>$"SendPitStopInteractionPacket({netId}, {interaction}, {position}, {rotation}, {trainCarNetId}, {isConnectedLeft})"); - SendPacketToServer(new CommonPitStopPlugInteractionPacket + SendNetSerializablePacketToServer(new CommonPitStopPlugInteractionPacket { NetId = netId, InteractionType = interaction, diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index 4f182980..04336080 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -581,7 +581,7 @@ public void SendItemsChangePacket(List items, ServerPlayer playe } } - public void SendPitStopBulkDataPacket(ushort netId, int carCount, LocoResourceModuleData[] stationData, PitStopPlugData[] plugData , ITransportPeer peer = null) + public void SendPitStopBulkDataPacket(ushort netId, int carCount, int carIndex, LocoResourceModuleData[] stationData, PitStopPlugData[] plugData , ITransportPeer peer = null) { LogDebug(() => $"SendPitStopBulkDataPacket({netId}, {stationData.Count()}, {plugData.Count()}, {peer?.Id})"); @@ -589,6 +589,7 @@ public void SendPitStopBulkDataPacket(ushort netId, int carCount, LocoResourceMo { NetId = netId, CarCount = carCount, + CarSelection = carIndex, ResourceData = stationData, PlugData = plugData, }; @@ -1244,13 +1245,14 @@ private void OnCommonPitStopPlugInteractionPacket(CommonPitStopPlugInteractionPa if (plug.ValidateInteraction(packet, player)) { //passed validation, send to all but the originator + //todo: refactor for culling packet.PlayerId = player.Id; - SendPacketToAll(packet, DeliveryMethod.ReliableOrdered, peer); + SendNetSerializablePacketToAll(packet, DeliveryMethod.ReliableOrdered, peer); } else { //Failed to validate, player needs to rollback interaction - SendPacket(peer, new CommonPitStopPlugInteractionPacket + SendNetSerializablePacket(peer, new CommonPitStopPlugInteractionPacket { NetId = packet.NetId, InteractionType = (byte)PitStopStationInteractionType.Reject diff --git a/Multiplayer/Networking/Packets/Clientbound/World/ClientboundPitStopBulkUpdatePacket.cs b/Multiplayer/Networking/Packets/Clientbound/World/ClientboundPitStopBulkUpdatePacket.cs index ffbbaeda..22a90740 100644 --- a/Multiplayer/Networking/Packets/Clientbound/World/ClientboundPitStopBulkUpdatePacket.cs +++ b/Multiplayer/Networking/Packets/Clientbound/World/ClientboundPitStopBulkUpdatePacket.cs @@ -10,6 +10,7 @@ public class ClientboundPitStopBulkUpdatePacket { public ushort NetId { get; set; } public int CarCount { get; set; } + public int CarSelection { get; set; } public LocoResourceModuleData[] ResourceData { get; set; } public PitStopPlugData[] PlugData { get; set; } } From 0fbdea9cf45902fe2582429b1a997c441a883da6 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 22 Jun 2025 20:35:23 +1000 Subject: [PATCH 395/521] Fix pitstop pluggable object sync --- .../World/NetworkedPitStopStation.cs | 2 +- .../World/NetworkedPluggableObject.cs | 18 +++++---- .../Networking/Data/PitStopPlugData.cs | 37 ++++++++++++++----- .../Patches/World/PluggableObjectPatch.cs | 7 ++++ 4 files changed, 46 insertions(+), 18 deletions(-) diff --git a/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs b/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs index 1afe7de0..93204c66 100644 --- a/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs +++ b/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs @@ -319,7 +319,7 @@ private IEnumerator PlayerDistanceChecker() i = 0; foreach (var plug in resourceToPluggableObject) { - plugData[i] = PitStopPlugData.From(plug.Value); + plugData[i] = PitStopPlugData.From(plug.Value, true); i++; } diff --git a/Multiplayer/Components/Networking/World/NetworkedPluggableObject.cs b/Multiplayer/Components/Networking/World/NetworkedPluggableObject.cs index 8e17ff33..bbd8cd67 100644 --- a/Multiplayer/Components/Networking/World/NetworkedPluggableObject.cs +++ b/Multiplayer/Components/Networking/World/NetworkedPluggableObject.cs @@ -26,7 +26,7 @@ public static bool Get(ushort netId, out NetworkedPluggableObject obj) protected override bool IsIdServerAuthoritative => false; #region Server Variables - public PlugInteractionType CurrentInteraction { get; set; } + public PlugInteractionType CurrentInteraction { get; set; } public ServerPlayer HeldBy { get; private set; } public ushort TrainCarNetId { get; private set; } public bool IsConnectedLeft { get; private set; } @@ -52,7 +52,7 @@ protected override void Awake() PluggableObject = GetComponent(); //Multiplayer.LogDebug(() => $"NetworkedPluggableObject.Awake() {PluggableObject?.controlBase?.spec?.name}, {transform.parent.name}"); - Multiplayer.LogDebug(() => $"NetworkedPluggableObject.Awake() {this.GetObjectPath()}, netId: {NetId}"); + Multiplayer.LogDebug(() => $"NetworkedPluggableObject.Awake() {this.GetObjectPath()}, netId: {NetId}, PluggableObject found: {PluggableObject != null}"); if (NetworkLifecycle.Instance.IsHost()) Refreshed = true; @@ -145,12 +145,15 @@ public void ProcessPacket(CommonPitStopPlugInteractionPacket packet) public void ProcessBulkUpdate(PitStopPlugData data) { + Multiplayer.LogDebug(() => $"NetworkedPluggableObject.ProcessBulkUpdate() netId: {NetId}"); CoroutineManager.Instance.StartCoroutine(WaitForInit(data)); } private IEnumerator WaitForInit(PitStopPlugData data) { - yield return new WaitUntil(()=> PluggableObject != null && PluggableObject.initialized); + yield return new WaitUntil(() => PluggableObject != null && PluggableObject.initialized); + + Multiplayer.LogDebug(() => $"NetworkedPluggableObject.WaitForInit() netId: {NetId} Complete"); var interaction = data.State; ProcessInteraction(interaction, data.PlayerId, data.TrainCarNetId, data.IsLeftSide, data.Position, data.Rotation); @@ -160,6 +163,7 @@ private IEnumerator WaitForInit(PitStopPlugData data) public void ProcessInteraction(PlugInteractionType interaction, byte playerId, ushort trainNetId, bool isLeftSide, Vector3? newPosition, Quaternion? newRotation) { bool result; + Multiplayer.LogDebug(() => $"NetworkedPluggableObject.ProcessInteraction({interaction}, {playerId}, {trainNetId}, {isLeftSide}, {newPosition?.ToString()}, {newRotation?.ToString()}) netId: {NetId}"); NetworkedPlayer player = null; @@ -247,7 +251,7 @@ public void ProcessInteraction(PlugInteractionType interaction, byte playerId, u if (NetworkLifecycle.Instance.IsClientRunning && NetworkLifecycle.Instance.Client.ClientPlayerManager.TryGetPlayer(playerHolding, out player)) { - player.DropItem(); + player?.DropItem(); } } @@ -299,7 +303,7 @@ public void InitPitStop(NetworkedPitStopStation netPitStop) if (NetId == 0) base.Awake(); - if(plugToStation.TryGetValue(this, out _)) + if (plugToStation.TryGetValue(this, out _)) { Multiplayer.LogWarning($"Lookup cache 'plugToStation' already contains NetworkedPitStopStation \"{netPitStop?.StationName}\", skipping Init"); return; @@ -361,9 +365,9 @@ private void OnPlugged(PluggableObject plug, PlugSocket socket) else { var trainCar = TrainCar.Resolve(socket.gameObject); - if(trainCar != null) + if (trainCar != null) { - if(!NetworkedTrainCar.TryGetFromTrainCar(trainCar, out var netTrainCar)) + if (!NetworkedTrainCar.TryGetFromTrainCar(trainCar, out var netTrainCar)) { Multiplayer.LogDebug(() => $"NetworkedPluggableObject.OnPlugged() NetworkedTrainCar: {trainCar?.ID} Not Found! Socket: {socket.GetObjectPath()}"); return; diff --git a/Multiplayer/Networking/Data/PitStopPlugData.cs b/Multiplayer/Networking/Data/PitStopPlugData.cs index 31b224d7..e82e6cae 100644 --- a/Multiplayer/Networking/Data/PitStopPlugData.cs +++ b/Multiplayer/Networking/Data/PitStopPlugData.cs @@ -16,18 +16,18 @@ public readonly struct PitStopPlugData(ushort netId, PlugInteractionType state, public readonly Vector3 Position = pos; public readonly Quaternion Rotation = rot; - public static PitStopPlugData From(NetworkedPluggableObject plugData) + public static PitStopPlugData From(NetworkedPluggableObject plugData, bool bulk = false) { return new PitStopPlugData - ( - plugData.NetId, - plugData.CurrentInteraction, - plugData.HeldBy?.Id ?? 0, - plugData.TrainCarNetId, - plugData.IsConnectedLeft, - plugData.transform.GetWorldAbsolutePosition(), - plugData.transform.rotation - ); + ( + plugData.NetId, + GetInteractionType(plugData, bulk), + plugData.HeldBy?.Id ?? 0, + plugData.TrainCarNetId, + plugData.IsConnectedLeft, + plugData.transform.GetWorldAbsolutePosition(), + plugData.transform.rotation + ); } public static void Serialize(NetDataWriter writer, PitStopPlugData data) @@ -99,4 +99,21 @@ public static PitStopPlugData Deserialize(NetDataReader reader) rot ); } + + private static PlugInteractionType GetInteractionType(NetworkedPluggableObject plugData, bool bulk) + { + if (!bulk) + return plugData.CurrentInteraction; + + if (plugData.HeldBy != null) + return PlugInteractionType.PickedUp; + + if (plugData.TrainCarNetId != 0) + return PlugInteractionType.DockSocket; + + if (plugData.PluggableObject.Socket == plugData.PluggableObject.startAttachedTo) + return PlugInteractionType.DockHome; + + return PlugInteractionType.Dropped; + } } diff --git a/Multiplayer/Patches/World/PluggableObjectPatch.cs b/Multiplayer/Patches/World/PluggableObjectPatch.cs index 1e654107..006aed9d 100644 --- a/Multiplayer/Patches/World/PluggableObjectPatch.cs +++ b/Multiplayer/Patches/World/PluggableObjectPatch.cs @@ -17,4 +17,11 @@ public static bool Awake(PluggableObject __instance) __instance.CheckInitialization(); return false; } + + [HarmonyPatch(nameof(PluggableObject.InstantSnapTo))] + [HarmonyPrefix] + public static bool InstantSnapTo(PluggableObject __instance) + { + return false; + } } From c3b841492539228ec96c4023f564e0fa54641255 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 22 Jun 2025 20:35:49 +1000 Subject: [PATCH 396/521] Remove excessive logging --- Multiplayer/Patches/World/CashRegisterWithModulesPatch.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Multiplayer/Patches/World/CashRegisterWithModulesPatch.cs b/Multiplayer/Patches/World/CashRegisterWithModulesPatch.cs index 271d4fc6..dd9f9e27 100644 --- a/Multiplayer/Patches/World/CashRegisterWithModulesPatch.cs +++ b/Multiplayer/Patches/World/CashRegisterWithModulesPatch.cs @@ -56,7 +56,7 @@ private static void OnBuyPressed_Postfix(CashRegisterWithModules __instance) private static bool Cancel(CashRegisterWithModules __instance) { - Multiplayer.LogDebug(()=>$"CashRegisterWithModules.Cancel({__instance.GetObjectPath()})\r\n{Environment.StackTrace}"); + //Multiplayer.LogDebug(()=>$"CashRegisterWithModules.Cancel({__instance.GetObjectPath()})\r\n{Environment.StackTrace}"); if (NetworkLifecycle.Instance.IsHost()) return true; From bfd14d034a679f4ab904fa5620285e694a02c648 Mon Sep 17 00:00:00 2001 From: Chump-the-Lump <110000050+Chump-the-Lump@users.noreply.github.com> Date: Mon, 23 Jun 2025 15:51:43 -0700 Subject: [PATCH 397/521] Loading machine Display is not accurate for clients --- .../NetworkedWarehouseMachineController.cs | 28 ++++++++++ Multiplayer/Networking/Data/WarehouseData.cs | 8 +++ .../Managers/Client/NetworkClient.cs | 22 ++++++++ .../Managers/Server/NetworkServer.cs | 47 ++++++++++++++++ ...WarehouseMachineControllerRequestPacket.cs | 8 +++ .../Jobs/WarehouseMachineControllerPatch.cs | 55 +++++++++++++++++++ 6 files changed, 168 insertions(+) create mode 100644 Multiplayer/Components/Networking/Jobs/NetworkedWarehouseMachineController.cs create mode 100644 Multiplayer/Networking/Data/WarehouseData.cs create mode 100644 Multiplayer/Networking/Packets/Serverbound/Jobs/ServerboundWarehouseMachineControllerRequestPacket.cs create mode 100644 Multiplayer/Patches/Jobs/WarehouseMachineControllerPatch.cs diff --git a/Multiplayer/Components/Networking/Jobs/NetworkedWarehouseMachineController.cs b/Multiplayer/Components/Networking/Jobs/NetworkedWarehouseMachineController.cs new file mode 100644 index 00000000..929f8d36 --- /dev/null +++ b/Multiplayer/Components/Networking/Jobs/NetworkedWarehouseMachineController.cs @@ -0,0 +1,28 @@ +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; + + +namespace Multiplayer.Components.Networking.Jobs; + +public class NetworkedWarehouseMachineController +{ + public static WarehouseMachineController FindFomID(string ID) + { + foreach (var warehouse in WarehouseMachineController.allControllers) + { + if (warehouse.warehouseMachine.ID == ID) + { + return warehouse; + } + } + return null; + } +} diff --git a/Multiplayer/Networking/Data/WarehouseData.cs b/Multiplayer/Networking/Data/WarehouseData.cs new file mode 100644 index 00000000..22e649e9 --- /dev/null +++ b/Multiplayer/Networking/Data/WarehouseData.cs @@ -0,0 +1,8 @@ + +namespace Multiplayer.Networking.Data; + +public enum WarehouseAction : byte +{ + Load, + Unload +} diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 3341b64e..7c1a4723 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -224,9 +224,15 @@ public override void OnConnectionRequest(NetDataReader dataReader, IConnectionRe #endregion +<<<<<<< Updated upstream #region Listeners +======= + + #region Listeners + +>>>>>>> Stashed changes private void OnClientboundLoginResponsePacket(ClientboundLoginResponsePacket packet) { @@ -1316,12 +1322,28 @@ public void SendJobValidateRequest(NetworkedJob job, NetworkedStationController }, DeliveryMethod.ReliableUnordered); } +<<<<<<< Updated upstream +======= + public void SendWarehouseRequest(WarehouseAction action, string track) + { + SendPacketToServer(new ServerboundWarehouseMachineControllerRequestPacket + { + warehouseAction = action, + WarehouseMachineID = track + }, DeliveryMethod.ReliableUnordered); + } + +>>>>>>> Stashed changes public void SendChat(string message) { SendPacketToServer(new CommonChatPacket { message = message +<<<<<<< Updated upstream }, DeliveryMethod.ReliableUnordered); +======= + }, DeliveryMethod.Unreliable); +>>>>>>> Stashed changes } public void SendItemsChangePacket(List items) diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index ab8930ec..fb181ada 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -151,6 +151,10 @@ protected override void Subscribe() netPacketProcessor.SubscribeReusable(OnCommonTrainPortsPacket); netPacketProcessor.SubscribeReusable(OnCommonTrainFusesPacket); netPacketProcessor.SubscribeReusable(OnServerboundJobValidateRequestPacket); +<<<<<<< Updated upstream +======= + netPacketProcessor.SubscribeReusable(OnServerboundWarehouseMachineControllerRequestPacket); +>>>>>>> Stashed changes netPacketProcessor.SubscribeReusable(OnCommonChatPacket); netPacketProcessor.SubscribeReusable(OnUnconnectedPingPacket); netPacketProcessor.SubscribeNetSerializable(OnCommonItemChangePacket); @@ -883,7 +887,11 @@ private void OnCommonCouplerInteractionPacket(CommonCouplerInteractionPacket pac //Car doesn't exist, tell client to delete it SendDestroyTrainCar(netTrainCar, peer); } +<<<<<<< Updated upstream +======= + +>>>>>>> Stashed changes } //private void OnCommonTrainCouplePacket(CommonTrainCouplePacket packet, ITransportPeer peer) //{ @@ -1104,7 +1112,10 @@ private void OnServerboundLicensePurchaseRequestPacket(ServerboundLicensePurchas LicenseManager.Instance.AcquireGeneralLicense(generalLicense); } +<<<<<<< Updated upstream +======= +>>>>>>> Stashed changes private void OnServerboundJobValidateRequestPacket(ServerboundJobValidateRequestPacket packet, ITransportPeer peer) { Log($"OnServerboundJobValidateRequestPacket(): {packet.JobNetId}"); @@ -1145,6 +1156,42 @@ private void OnServerboundJobValidateRequestPacket(ServerboundJobValidateRequest //SendPacket(peer, new ClientboundJobValidateResponsePacket { JobNetId = packet.JobNetId, Invalid = false }, DeliveryMethod.ReliableUnordered); } +<<<<<<< Updated upstream +======= + private void OnServerboundWarehouseMachineControllerRequestPacket(ServerboundWarehouseMachineControllerRequestPacket packet, ITransportPeer peer) + { + Log($"OnServerboundWarehouseMachineControllerRequestPacket(): {packet.WarehouseMachineID}"); + + if (!TryGetServerPlayer(peer, out ServerPlayer player)) + { + LogWarning($"OnServerboundWarehouseMachineControllerRequestPacket() ServerPlayer not found: {peer.Id}"); + return; + } + + //Find the warehouse + WarehouseMachineController targetWarehouse = NetworkedWarehouseMachineController.FindFomID(packet.WarehouseMachineID); + + + if (targetWarehouse == null) + { + LogWarning($"OnServerboundWarehouseMachineControllerRequestPacket() WarehouseMachineController not found. WarehouseMachineControllerID: {packet.WarehouseMachineID}"); + return; + } + + LogDebug(() => $"OnServerboundWarehouseMachineControllerRequestPacket() {packet.WarehouseMachineID}, Action Type: {packet.warehouseAction}"); + switch (packet.warehouseAction) + { + case WarehouseAction.Load: + targetWarehouse.StartLoadSequence(); + break; + + case WarehouseAction.Unload: + targetWarehouse.StartUnloadSequence(); + break; + } + } + +>>>>>>> Stashed changes private void OnCommonChatPacket(CommonChatPacket packet, ITransportPeer peer) { ChatManager.ProcessMessage(packet.message, peer); diff --git a/Multiplayer/Networking/Packets/Serverbound/Jobs/ServerboundWarehouseMachineControllerRequestPacket.cs b/Multiplayer/Networking/Packets/Serverbound/Jobs/ServerboundWarehouseMachineControllerRequestPacket.cs new file mode 100644 index 00000000..8e08e364 --- /dev/null +++ b/Multiplayer/Networking/Packets/Serverbound/Jobs/ServerboundWarehouseMachineControllerRequestPacket.cs @@ -0,0 +1,8 @@ +using Multiplayer.Networking.Data; +namespace Multiplayer.Networking.Packets.Clientbound.Jobs; + +public class ServerboundWarehouseMachineControllerRequestPacket +{ + public string WarehouseMachineID { get; set; } + public WarehouseAction warehouseAction { get; set; } +} diff --git a/Multiplayer/Patches/Jobs/WarehouseMachineControllerPatch.cs b/Multiplayer/Patches/Jobs/WarehouseMachineControllerPatch.cs new file mode 100644 index 00000000..2089151f --- /dev/null +++ b/Multiplayer/Patches/Jobs/WarehouseMachineControllerPatch.cs @@ -0,0 +1,55 @@ +using System.Collections; +using Multiplayer.Components.Networking; +using Multiplayer.Components.Networking.Jobs; +using Multiplayer.Networking.Data; +using UnityEngine; + +namespace Multiplayer.Patches.Jobs; + +using HarmonyLib; + +[HarmonyPatch(typeof(WarehouseMachineController))] +public class WarehouseMachineControllerPatch +{ + [HarmonyPrefix] + [HarmonyPatch("StartUnloadSequence")] + public static void StartUnloadSequence_Prefix(WarehouseMachineController __instance) + { + __instance.displayTrainInRangeText.text = __instance.warehouseMachine.ID; + + if (!NetworkLifecycle.Instance.IsHost()) + { + SendValidationRequest(__instance, WarehouseAction.Unload); + } + + } + + [HarmonyPrefix] + [HarmonyPatch("StartLoadSequence")] + public static void StartLoadSequence_Prefix(WarehouseMachineController __instance) + { + __instance.displayTrainInRangeText.text = __instance.warehouseMachine.ID; + + if (!NetworkLifecycle.Instance.IsHost()) + { + SendValidationRequest(__instance, WarehouseAction.Load); + } + + } + + private static void SendValidationRequest(WarehouseMachineController machine,WarehouseAction action) + { + //find the current station we're at + if (!string.IsNullOrEmpty(machine.warehouseTrackName)) + { + string id = machine.warehouseMachine.ID; + + NetworkLifecycle.Instance.Client.SendWarehouseRequest(action, id); + //CoroutineManager.Instance.StartCoroutine(AwaitResponse(machine, action)); + } + else + { + NetworkLifecycle.Instance.Client.LogError($"Failed to validate {action} for {machine.warehouseMachine.ID}. Warehouse not found!"); + } + } +} From a8852c7ed3b0cb4ffe2d54c7f55b0258b1020ca9 Mon Sep 17 00:00:00 2001 From: Chump-the-Lump <110000050+Chump-the-Lump@users.noreply.github.com> Date: Mon, 23 Jun 2025 15:58:08 -0700 Subject: [PATCH 398/521] WHY DID GITHUB LEAVE THE MEGE CONFLICTS IN THE FUCKING FILE --- .../Networking/Managers/Client/NetworkClient.cs | 16 ++-------------- .../Networking/Managers/Server/NetworkServer.cs | 16 ---------------- 2 files changed, 2 insertions(+), 30 deletions(-) diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 7c1a4723..1c2ec120 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -224,15 +224,8 @@ public override void OnConnectionRequest(NetDataReader dataReader, IConnectionRe #endregion -<<<<<<< Updated upstream - - #region Listeners - -======= - #region Listeners ->>>>>>> Stashed changes private void OnClientboundLoginResponsePacket(ClientboundLoginResponsePacket packet) { @@ -1322,8 +1315,6 @@ public void SendJobValidateRequest(NetworkedJob job, NetworkedStationController }, DeliveryMethod.ReliableUnordered); } -<<<<<<< Updated upstream -======= public void SendWarehouseRequest(WarehouseAction action, string track) { SendPacketToServer(new ServerboundWarehouseMachineControllerRequestPacket @@ -1333,17 +1324,14 @@ public void SendWarehouseRequest(WarehouseAction action, string track) }, DeliveryMethod.ReliableUnordered); } ->>>>>>> Stashed changes + public void SendChat(string message) { SendPacketToServer(new CommonChatPacket { message = message -<<<<<<< Updated upstream }, DeliveryMethod.ReliableUnordered); -======= - }, DeliveryMethod.Unreliable); ->>>>>>> Stashed changes + } public void SendItemsChangePacket(List items) diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index fb181ada..970cc9f0 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -151,10 +151,7 @@ protected override void Subscribe() netPacketProcessor.SubscribeReusable(OnCommonTrainPortsPacket); netPacketProcessor.SubscribeReusable(OnCommonTrainFusesPacket); netPacketProcessor.SubscribeReusable(OnServerboundJobValidateRequestPacket); -<<<<<<< Updated upstream -======= netPacketProcessor.SubscribeReusable(OnServerboundWarehouseMachineControllerRequestPacket); ->>>>>>> Stashed changes netPacketProcessor.SubscribeReusable(OnCommonChatPacket); netPacketProcessor.SubscribeReusable(OnUnconnectedPingPacket); netPacketProcessor.SubscribeNetSerializable(OnCommonItemChangePacket); @@ -887,11 +884,6 @@ private void OnCommonCouplerInteractionPacket(CommonCouplerInteractionPacket pac //Car doesn't exist, tell client to delete it SendDestroyTrainCar(netTrainCar, peer); } -<<<<<<< Updated upstream - -======= - ->>>>>>> Stashed changes } //private void OnCommonTrainCouplePacket(CommonTrainCouplePacket packet, ITransportPeer peer) //{ @@ -1111,11 +1103,6 @@ private void OnServerboundLicensePurchaseRequestPacket(ServerboundLicensePurchas else LicenseManager.Instance.AcquireGeneralLicense(generalLicense); } - -<<<<<<< Updated upstream - -======= ->>>>>>> Stashed changes private void OnServerboundJobValidateRequestPacket(ServerboundJobValidateRequestPacket packet, ITransportPeer peer) { Log($"OnServerboundJobValidateRequestPacket(): {packet.JobNetId}"); @@ -1156,8 +1143,6 @@ private void OnServerboundJobValidateRequestPacket(ServerboundJobValidateRequest //SendPacket(peer, new ClientboundJobValidateResponsePacket { JobNetId = packet.JobNetId, Invalid = false }, DeliveryMethod.ReliableUnordered); } -<<<<<<< Updated upstream -======= private void OnServerboundWarehouseMachineControllerRequestPacket(ServerboundWarehouseMachineControllerRequestPacket packet, ITransportPeer peer) { Log($"OnServerboundWarehouseMachineControllerRequestPacket(): {packet.WarehouseMachineID}"); @@ -1191,7 +1176,6 @@ private void OnServerboundWarehouseMachineControllerRequestPacket(ServerboundWar } } ->>>>>>> Stashed changes private void OnCommonChatPacket(CommonChatPacket packet, ITransportPeer peer) { ChatManager.ProcessMessage(packet.message, peer); From 849f0b3fc200d4ca1d275f98623f40e6b3031539 Mon Sep 17 00:00:00 2001 From: AMacro Date: Tue, 24 Jun 2025 14:59:59 +1000 Subject: [PATCH 399/521] Add PlayerDistanceGameObjectsDisablerPatch from feature/self-service --- .../PlayerDistanceGameObjectsDisablerPatch.cs | 136 ++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 Multiplayer/Patches/World/PlayerDistanceGameObjectsDisablerPatch.cs diff --git a/Multiplayer/Patches/World/PlayerDistanceGameObjectsDisablerPatch.cs b/Multiplayer/Patches/World/PlayerDistanceGameObjectsDisablerPatch.cs new file mode 100644 index 00000000..73683670 --- /dev/null +++ b/Multiplayer/Patches/World/PlayerDistanceGameObjectsDisablerPatch.cs @@ -0,0 +1,136 @@ +using HarmonyLib; +using Multiplayer.Components.Networking; +using Multiplayer.Utils; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; +using System.Text; +using UnityEngine; + +namespace Multiplayer.Patches.World; + +[HarmonyPatch] +public static class PlayerDistanceGameObjectsDisablerPatch +{ + const int SKIPS = 2; + static readonly CodeInstruction targetMethod = CodeInstruction.Call(typeof(Vector3), "op_Subtraction", [typeof(Vector3), typeof(Vector3)], null); + static readonly CodeInstruction newMethod = CodeInstruction.Call(typeof(PlayerDistanceGameObjectsDisablerPatch), nameof(CustomCalcSqrMagnitude), [typeof(Vector3), typeof(Vector3), typeof(PlayerDistanceGameObjectsDisabler)], null); + + + public static IEnumerable TargetMethods() + { + //We're targeting an 'IEnumerable'; this gets compiled as a state machine with + //a method per state. + //Find all of the resultant states that are a 'MoveNext', these are the methods we need to patch. + //Doing this dynamically reduces the chance a game update breaks the transpiler + return typeof(PlayerDistanceGameObjectsDisabler) + .GetNestedTypes(BindingFlags.NonPublic | BindingFlags.Instance) + .Where(t => t.Name.StartsWith("")) + .SelectMany(t => t.GetMethods(BindingFlags.NonPublic | BindingFlags.Instance)) + .Where(m => m.Name == "MoveNext"); + } + + + /* + * We want to find the call to Vector3 subtraction `(optimizingGameObjects[i].transform.position - position)` + * (found on line 79 of the IL code) and replace it with an instruction + * that loads the current instance "this" to the stack. + * we want to override line 80 so it calls our custom method `CustomCalcSqrMagnitude()` + * Lines 81 and 82 are not required and need to be NOP'd out + * This pattern is used again in the re-enable check (lines 104 - 115) + + 74 00D6 ldfld int32 PlayerDistanceGameObjectsDisabler/'d__6'::'5__2' + 75 00DB callvirt instance !0 class [mscorlib] System.Collections.Generic.List`1::get_Item(int32) + 76 00E0 callvirt instance class [UnityEngine.CoreModule] + UnityEngine.Transform[UnityEngine.CoreModule] UnityEngine.GameObject::get_transform() + 77 00E5 callvirt instance valuetype[UnityEngine.CoreModule] UnityEngine.Vector3 [UnityEngine.CoreModule] UnityEngine.Transform::get_position() + 78 00EA ldloc.2 //parameter for the position of the player's camera + + //overwrite line 79 with ldloc.1 (pass in 'this' as the final parameter of call to CustomCalcSqrMagnitude()) + 79 00EB call valuetype[UnityEngine.CoreModule] UnityEngine.Vector3[UnityEngine.CoreModule] UnityEngine.Vector3::op_Subtraction(valuetype[UnityEngine.CoreModule] UnityEngine.Vector3, valuetype[UnityEngine.CoreModule] UnityEngine.Vector3) + //overwrite with call to CustomCalcSqrMagnitude() (techinically we are inserting the call and skipping thr original) + //Insert 3 NOPs + 80 00F0 stloc.3 //skip 0 + 81 00F1 ldloca.s V_3(3) //skip 1 + 82 00F3 call instance float32[UnityEngine.CoreModule] UnityEngine.Vector3::get_sqrMagnitude() //Skip 2 + 83 00F8 ldloc.1 + 84 00F9 ldfld float32 PlayerDistanceGameObjectsDisabler::disableSqrDistance + 85 00FE ble.un.s 94 (0119) ldloc.1 + + */ + [HarmonyTranspiler] + public static IEnumerable GameObjectsDistanceCheck(IEnumerable instructions) + { + //Multiplayer.LogDebug(() => + //{ + // var code = new List(instructions); + + // StringBuilder sb = new StringBuilder(); + // sb.AppendLine("Starting transpiler"); + // sb.AppendLine("IL Before:"); + // for (int i = 0; i < code.Count; i++) + // sb.AppendLine($"{i:D4}: {code[i]}"); + + // return sb.ToString(); + //}); + + int skipCtr = 0; + bool skipFlag = false; + + var newCode = new List(); + + foreach (CodeInstruction instruction in instructions) + { + //Multiplayer.LogDebug(() => $"Checking instruction: {instruction}"); + if (instruction.opcode == OpCodes.Call && instruction.operand?.ToString() == targetMethod.operand?.ToString()) + { + //Multiplayer.LogDebug(() => "Found target method, replacing"); + newCode.Add(new CodeInstruction(OpCodes.Ldloc_1)); + newCode.Add(newMethod); //skip 0 + newCode.Add(new CodeInstruction(OpCodes.Nop)); //skip 1 + newCode.Add(new CodeInstruction(OpCodes.Nop)); //skip 2 + skipCtr = 0; //reset as there are 2 identical sections to the code to be patched. + skipFlag = true; + } + else if (skipFlag) + { + if (skipCtr == SKIPS) + { + skipFlag = false; //stop skipping + continue; + } + skipCtr++; + } + else + newCode.Add(instruction); + } + + //Multiplayer.LogDebug(() => + //{ + // StringBuilder sb = new StringBuilder(); + // sb.AppendLine("IL After:"); + // for (int i = 0; i < newCode.Count; i++) + // sb.AppendLine($"{i:D4}: {newCode[i]}"); + + // return sb.ToString(); + //}); + + return newCode; + } + + + public static float CustomCalcSqrMagnitude(Vector3 vecA, Vector3 vecB, PlayerDistanceGameObjectsDisabler instance) + { + //At present we only need to target instsances of `PlayerDistanceGameObjectsDisabler` + //that are on the 'RefillStations' game object, as this is managing Pit Stop stations + //we need these to be active on the host when any player is nearby. + if (instance.gameObject.name == "RefillStations" && NetworkLifecycle.Instance.IsHost()) + { + //Multiplayer.LogDebug(() =>$"CustomCalcSqrMagnitude({instance?.gameObject?.name}, {vecA}, {vecB}) Camera pos: {PlayerManager.ActiveCamera.transform.position}"); + return vecA.AnyPlayerSqrMag(); + } + + return (vecA - vecB).sqrMagnitude; + } +} From 142e8487192c24098046838a68a1ffa19bec5c96 Mon Sep 17 00:00:00 2001 From: AMacro Date: Tue, 24 Jun 2025 18:19:31 +1000 Subject: [PATCH 400/521] Add '*_office_anchor' objects to PlayerDistanceGameObjectsDisabler Patch Ensure warehouse machines are active on the host whenever any player is near --- .../PlayerDistanceGameObjectsDisablerPatch.cs | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/Multiplayer/Patches/World/PlayerDistanceGameObjectsDisablerPatch.cs b/Multiplayer/Patches/World/PlayerDistanceGameObjectsDisablerPatch.cs index 73683670..4562cec7 100644 --- a/Multiplayer/Patches/World/PlayerDistanceGameObjectsDisablerPatch.cs +++ b/Multiplayer/Patches/World/PlayerDistanceGameObjectsDisablerPatch.cs @@ -122,10 +122,8 @@ public static IEnumerable GameObjectsDistanceCheck(IEnumerable< public static float CustomCalcSqrMagnitude(Vector3 vecA, Vector3 vecB, PlayerDistanceGameObjectsDisabler instance) { - //At present we only need to target instsances of `PlayerDistanceGameObjectsDisabler` - //that are on the 'RefillStations' game object, as this is managing Pit Stop stations - //we need these to be active on the host when any player is nearby. - if (instance.gameObject.name == "RefillStations" && NetworkLifecycle.Instance.IsHost()) + //Ensure we are only using the custom calc for certain instances and we are the host + if (ShouldUseCustomCalc(instance) && NetworkLifecycle.Instance.IsHost()) { //Multiplayer.LogDebug(() =>$"CustomCalcSqrMagnitude({instance?.gameObject?.name}, {vecA}, {vecB}) Camera pos: {PlayerManager.ActiveCamera.transform.position}"); return vecA.AnyPlayerSqrMag(); @@ -133,4 +131,24 @@ public static float CustomCalcSqrMagnitude(Vector3 vecA, Vector3 vecB, PlayerDis return (vecA - vecB).sqrMagnitude; } + + private static bool ShouldUseCustomCalc(PlayerDistanceGameObjectsDisabler instance) + { + var go = instance.gameObject; + + //At present we only need to target certain instances of `PlayerDistanceGameObjectsDisabler` + //we need these to be active on the host when any player is nearby. + + //Ensure refill stations are enabled + if (go.name == "RefillStations") + return true; + + //Ensure warehouse machines are enabled + var parent = go.transform.parent; + if (parent != null && parent.name.EndsWith("_office_anchor")) + return true; + + //Ignore all other instances + return false; + } } From c712fd381b0adeea90801fc72903ff971cda049f Mon Sep 17 00:00:00 2001 From: AMacro Date: Tue, 24 Jun 2025 18:25:54 +1000 Subject: [PATCH 401/521] Squashed commit of the following: commit 142e8487192c24098046838a68a1ffa19bec5c96 Author: AMacro Date: Tue Jun 24 18:19:31 2025 +1000 Add '*_office_anchor' objects to PlayerDistanceGameObjectsDisabler Patch Ensure warehouse machines are active on the host whenever any player is near commit 849f0b3fc200d4ca1d275f98623f40e6b3031539 Author: AMacro Date: Tue Jun 24 14:59:59 2025 +1000 Add PlayerDistanceGameObjectsDisablerPatch from feature/self-service --- .../PlayerDistanceGameObjectsDisablerPatch.cs | 154 ++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 Multiplayer/Patches/World/PlayerDistanceGameObjectsDisablerPatch.cs diff --git a/Multiplayer/Patches/World/PlayerDistanceGameObjectsDisablerPatch.cs b/Multiplayer/Patches/World/PlayerDistanceGameObjectsDisablerPatch.cs new file mode 100644 index 00000000..4562cec7 --- /dev/null +++ b/Multiplayer/Patches/World/PlayerDistanceGameObjectsDisablerPatch.cs @@ -0,0 +1,154 @@ +using HarmonyLib; +using Multiplayer.Components.Networking; +using Multiplayer.Utils; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; +using System.Text; +using UnityEngine; + +namespace Multiplayer.Patches.World; + +[HarmonyPatch] +public static class PlayerDistanceGameObjectsDisablerPatch +{ + const int SKIPS = 2; + static readonly CodeInstruction targetMethod = CodeInstruction.Call(typeof(Vector3), "op_Subtraction", [typeof(Vector3), typeof(Vector3)], null); + static readonly CodeInstruction newMethod = CodeInstruction.Call(typeof(PlayerDistanceGameObjectsDisablerPatch), nameof(CustomCalcSqrMagnitude), [typeof(Vector3), typeof(Vector3), typeof(PlayerDistanceGameObjectsDisabler)], null); + + + public static IEnumerable TargetMethods() + { + //We're targeting an 'IEnumerable'; this gets compiled as a state machine with + //a method per state. + //Find all of the resultant states that are a 'MoveNext', these are the methods we need to patch. + //Doing this dynamically reduces the chance a game update breaks the transpiler + return typeof(PlayerDistanceGameObjectsDisabler) + .GetNestedTypes(BindingFlags.NonPublic | BindingFlags.Instance) + .Where(t => t.Name.StartsWith("")) + .SelectMany(t => t.GetMethods(BindingFlags.NonPublic | BindingFlags.Instance)) + .Where(m => m.Name == "MoveNext"); + } + + + /* + * We want to find the call to Vector3 subtraction `(optimizingGameObjects[i].transform.position - position)` + * (found on line 79 of the IL code) and replace it with an instruction + * that loads the current instance "this" to the stack. + * we want to override line 80 so it calls our custom method `CustomCalcSqrMagnitude()` + * Lines 81 and 82 are not required and need to be NOP'd out + * This pattern is used again in the re-enable check (lines 104 - 115) + + 74 00D6 ldfld int32 PlayerDistanceGameObjectsDisabler/'d__6'::'5__2' + 75 00DB callvirt instance !0 class [mscorlib] System.Collections.Generic.List`1::get_Item(int32) + 76 00E0 callvirt instance class [UnityEngine.CoreModule] + UnityEngine.Transform[UnityEngine.CoreModule] UnityEngine.GameObject::get_transform() + 77 00E5 callvirt instance valuetype[UnityEngine.CoreModule] UnityEngine.Vector3 [UnityEngine.CoreModule] UnityEngine.Transform::get_position() + 78 00EA ldloc.2 //parameter for the position of the player's camera + + //overwrite line 79 with ldloc.1 (pass in 'this' as the final parameter of call to CustomCalcSqrMagnitude()) + 79 00EB call valuetype[UnityEngine.CoreModule] UnityEngine.Vector3[UnityEngine.CoreModule] UnityEngine.Vector3::op_Subtraction(valuetype[UnityEngine.CoreModule] UnityEngine.Vector3, valuetype[UnityEngine.CoreModule] UnityEngine.Vector3) + //overwrite with call to CustomCalcSqrMagnitude() (techinically we are inserting the call and skipping thr original) + //Insert 3 NOPs + 80 00F0 stloc.3 //skip 0 + 81 00F1 ldloca.s V_3(3) //skip 1 + 82 00F3 call instance float32[UnityEngine.CoreModule] UnityEngine.Vector3::get_sqrMagnitude() //Skip 2 + 83 00F8 ldloc.1 + 84 00F9 ldfld float32 PlayerDistanceGameObjectsDisabler::disableSqrDistance + 85 00FE ble.un.s 94 (0119) ldloc.1 + + */ + [HarmonyTranspiler] + public static IEnumerable GameObjectsDistanceCheck(IEnumerable instructions) + { + //Multiplayer.LogDebug(() => + //{ + // var code = new List(instructions); + + // StringBuilder sb = new StringBuilder(); + // sb.AppendLine("Starting transpiler"); + // sb.AppendLine("IL Before:"); + // for (int i = 0; i < code.Count; i++) + // sb.AppendLine($"{i:D4}: {code[i]}"); + + // return sb.ToString(); + //}); + + int skipCtr = 0; + bool skipFlag = false; + + var newCode = new List(); + + foreach (CodeInstruction instruction in instructions) + { + //Multiplayer.LogDebug(() => $"Checking instruction: {instruction}"); + if (instruction.opcode == OpCodes.Call && instruction.operand?.ToString() == targetMethod.operand?.ToString()) + { + //Multiplayer.LogDebug(() => "Found target method, replacing"); + newCode.Add(new CodeInstruction(OpCodes.Ldloc_1)); + newCode.Add(newMethod); //skip 0 + newCode.Add(new CodeInstruction(OpCodes.Nop)); //skip 1 + newCode.Add(new CodeInstruction(OpCodes.Nop)); //skip 2 + skipCtr = 0; //reset as there are 2 identical sections to the code to be patched. + skipFlag = true; + } + else if (skipFlag) + { + if (skipCtr == SKIPS) + { + skipFlag = false; //stop skipping + continue; + } + skipCtr++; + } + else + newCode.Add(instruction); + } + + //Multiplayer.LogDebug(() => + //{ + // StringBuilder sb = new StringBuilder(); + // sb.AppendLine("IL After:"); + // for (int i = 0; i < newCode.Count; i++) + // sb.AppendLine($"{i:D4}: {newCode[i]}"); + + // return sb.ToString(); + //}); + + return newCode; + } + + + public static float CustomCalcSqrMagnitude(Vector3 vecA, Vector3 vecB, PlayerDistanceGameObjectsDisabler instance) + { + //Ensure we are only using the custom calc for certain instances and we are the host + if (ShouldUseCustomCalc(instance) && NetworkLifecycle.Instance.IsHost()) + { + //Multiplayer.LogDebug(() =>$"CustomCalcSqrMagnitude({instance?.gameObject?.name}, {vecA}, {vecB}) Camera pos: {PlayerManager.ActiveCamera.transform.position}"); + return vecA.AnyPlayerSqrMag(); + } + + return (vecA - vecB).sqrMagnitude; + } + + private static bool ShouldUseCustomCalc(PlayerDistanceGameObjectsDisabler instance) + { + var go = instance.gameObject; + + //At present we only need to target certain instances of `PlayerDistanceGameObjectsDisabler` + //we need these to be active on the host when any player is nearby. + + //Ensure refill stations are enabled + if (go.name == "RefillStations") + return true; + + //Ensure warehouse machines are enabled + var parent = go.transform.parent; + if (parent != null && parent.name.EndsWith("_office_anchor")) + return true; + + //Ignore all other instances + return false; + } +} From 3f4c91abb5414419c566783619980b40d69695e9 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 28 Jun 2025 10:15:25 +1000 Subject: [PATCH 402/521] Refactored JobData Moved `NetworkedStationController.CreateJobFromJobData()` to `JobData.ToJob` --- .../World/NetworkedStationController.cs | 18 +----------------- Multiplayer/Networking/Data/JobData.cs | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/Multiplayer/Components/Networking/World/NetworkedStationController.cs b/Multiplayer/Components/Networking/World/NetworkedStationController.cs index 4d08c82b..9d308ebb 100644 --- a/Multiplayer/Components/Networking/World/NetworkedStationController.cs +++ b/Multiplayer/Components/Networking/World/NetworkedStationController.cs @@ -248,7 +248,7 @@ public void AddJobs(JobData[] jobs) private void AddJob(JobData jobData) { - Job newJob = CreateJobFromJobData(jobData); + Job newJob = JobData.ToJob(jobData); var carNetIds = jobData.GetCars(); NetworkedJob networkedJob = CreateNetworkedJob(newJob, jobData.NetID, carNetIds); @@ -283,22 +283,6 @@ private void AddJob(JobData jobData) 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(jobData.ChainData.ChainOriginYardId, jobData.ChainData.ChainDestinationYardId); - - 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; diff --git a/Multiplayer/Networking/Data/JobData.cs b/Multiplayer/Networking/Data/JobData.cs index 1a5b676a..005d0353 100644 --- a/Multiplayer/Networking/Data/JobData.cs +++ b/Multiplayer/Networking/Data/JobData.cs @@ -79,6 +79,21 @@ public static JobData FromJob(NetworkedStationController netStation, NetworkedJo }; } + public static Job ToJob(JobData jobData) + { + List tasks = jobData.Tasks.Select(taskData => taskData.ToTask()).ToList(); + StationsChainData chainData = new(jobData.ChainData.ChainOriginYardId, jobData.ChainData.ChainDestinationYardId); + + 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; + } + public static void Serialize(NetDataWriter writer, JobData data) { //NetworkLifecycle.Instance.Server.Log($"JobData.Serialize({data.ID}) NetID {data.NetID}"); From 813698e69aef7cc1098900c0de8a27593cab97e6 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 28 Jun 2025 12:22:14 +1000 Subject: [PATCH 403/521] Ensure job started correctly Calling `Job.TakeJob()` causes warehouse tasks to be added to the `WarehouseMachine`'s current tasks, allowing correct information to be displayed on the the machine. --- .../Networking/World/NetworkedStationController.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Multiplayer/Components/Networking/World/NetworkedStationController.cs b/Multiplayer/Components/Networking/World/NetworkedStationController.cs index 4d08c82b..41d73270 100644 --- a/Multiplayer/Components/Networking/World/NetworkedStationController.cs +++ b/Multiplayer/Components/Networking/World/NetworkedStationController.cs @@ -267,7 +267,8 @@ private void AddJob(JobData jobData) } else if (networkedJob.Job.State == JobState.InProgress) { - takenJobs.Add(newJob); + takenJobs.Add(newJob); + newJob.TakeJob(true); //take job as if loaded from save to prevent debt controller kicking in } else { @@ -424,6 +425,8 @@ private void HandleJobStateChange(NetworkedJob netJob, JobUpdateStruct updateDat availableJobs.Remove(netJob.Job); takenJobs.Add(netJob.Job); + netJob.Job.TakeJob(true); //take job as if loaded from save to prevent debt controller kicking in + if (canPrint) { JobBooklet jobBooklet = BookletCreator.CreateJobBooklet(netJob.Job, validator.bookletPrinter.spawnAnchor.position, validator.bookletPrinter.spawnAnchor.rotation, WorldMover.OriginShiftParent, true); @@ -440,6 +443,7 @@ private void HandleJobStateChange(NetworkedJob netJob, JobUpdateStruct updateDat case JobState.Completed: takenJobs.Remove(netJob.Job); completedJobs.Add(netJob.Job); + netJob.Job.CompleteJob(); if (canPrint) { @@ -459,6 +463,7 @@ private void HandleJobStateChange(NetworkedJob netJob, JobUpdateStruct updateDat case JobState.Abandoned: takenJobs.Remove(netJob.Job); abandonedJobs.Add(netJob.Job); + netJob.Job.AbandonJob(); StartCoroutine(UpdateCarPlates(netJob.JobCars, string.Empty)); break; From d2d1ba3db5de35edb5abdf38cce64d30084a0dce Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 28 Jun 2025 17:09:11 +1000 Subject: [PATCH 404/521] Extend NetworkedWarehouseMachineController --- .../NetworkedWarehouseMachineController.cs | 94 +++++++++++++++++-- .../Managers/Server/NetworkServer.cs | 27 ++---- .../Jobs/WarehouseMachineControllerPatch.cs | 16 ++-- 3 files changed, 102 insertions(+), 35 deletions(-) diff --git a/Multiplayer/Components/Networking/Jobs/NetworkedWarehouseMachineController.cs b/Multiplayer/Components/Networking/Jobs/NetworkedWarehouseMachineController.cs index 929f8d36..f5638db3 100644 --- a/Multiplayer/Components/Networking/Jobs/NetworkedWarehouseMachineController.cs +++ b/Multiplayer/Components/Networking/Jobs/NetworkedWarehouseMachineController.cs @@ -1,20 +1,55 @@ -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 DV.ThingTypes; +using DV.ThingTypes.TransitionHelpers; +using Multiplayer.Components.Networking.Train; using Multiplayer.Networking.Data; -using Newtonsoft.Json.Linq; -using UnityEngine; +using Multiplayer.Networking.Packets.Clientbound.Jobs; +using System.Collections.Generic; +using static WarehouseMachineController; + namespace Multiplayer.Components.Networking.Jobs; -public class NetworkedWarehouseMachineController +public class NetworkedWarehouseMachineController : IdMonoBehaviour { - public static WarehouseMachineController FindFomID(string ID) + #region Lookup Cache + private static readonly Dictionary warehouseMachineControllerToNetworked = []; + private static readonly Dictionary warehouseMachineToNetworked = []; + + public static bool Get(ushort netId, out NetworkedWarehouseMachineController obj) + { + bool b = Get(netId, out IdMonoBehaviour rawObj); + obj = (NetworkedWarehouseMachineController)rawObj; + return b; + } + + public static NetworkedWarehouseMachineController GetFromWarehouseMachineController(WarehouseMachineController warehouseMachineController) + { + warehouseMachineControllerToNetworked.TryGetValue(warehouseMachineController, out var netWarehouseMachineController); + return netWarehouseMachineController; + } + + public static NetworkedWarehouseMachineController GetFromWarehouseMachine(WarehouseMachine warehouseMachine) + { + //fast path lookup + if (warehouseMachineToNetworked.TryGetValue(warehouseMachine, out NetworkedWarehouseMachineController networkedWarehouseMachineController)) + return networkedWarehouseMachineController; + + //cache miss, try to find parent WarehouseMachineController + var warehouseMachineController = GetFomId(warehouseMachine.ID); + if (warehouseMachineController != null) + { + //Warehouse Machine Controller found, check for NetworkedWarehouseMachineController + networkedWarehouseMachineController = GetFromWarehouseMachineController(warehouseMachineController); + if (networkedWarehouseMachineController != null) + warehouseMachineToNetworked[warehouseMachine] = GetFromWarehouseMachineController(warehouseMachineController); + } + + return networkedWarehouseMachineController; + } + + private static WarehouseMachineController GetFomId(string ID) { foreach (var warehouse in WarehouseMachineController.allControllers) { @@ -25,4 +60,43 @@ public static WarehouseMachineController FindFomID(string ID) } return null; } + + #endregion + protected override bool IsIdServerAuthoritative => false; + + public string Id => WarehouseMachine?.ID; + public WarehouseMachineController WarehouseMachineController { get; private set; } + public WarehouseMachine WarehouseMachine => WarehouseMachineController?.warehouseMachine; + + protected override void Awake() + { + base.Awake(); + WarehouseMachineController = GetComponent(); + warehouseMachineControllerToNetworked[WarehouseMachineController] = this; + } + + protected override void OnDestroy() + { + base.OnDestroy(); + + warehouseMachineControllerToNetworked.Remove(WarehouseMachineController); + + if (WarehouseMachineController.warehouseMachine != null) + warehouseMachineToNetworked.Remove(WarehouseMachineController.warehouseMachine); + } + + public void ServerProcessWarehouseAction(WarehouseAction action) + { + Multiplayer.LogDebug(() => $"ServerProcessWarehouseAction() {Id}, Action Type: {action}"); + switch (action) + { + case WarehouseAction.Load: + WarehouseMachineController.StartLoadSequence(); + break; + + case WarehouseAction.Unload: + WarehouseMachineController.StartUnloadSequence(); + break; + } + } } diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index 970cc9f0..7cc27f47 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -1145,35 +1145,26 @@ private void OnServerboundJobValidateRequestPacket(ServerboundJobValidateRequest private void OnServerboundWarehouseMachineControllerRequestPacket(ServerboundWarehouseMachineControllerRequestPacket packet, ITransportPeer peer) { - Log($"OnServerboundWarehouseMachineControllerRequestPacket(): {packet.WarehouseMachineID}"); + LogDebug(()=>$"ServerboundWarehouseMachineControllerRequestPacket(): {packet.NetId}"); if (!TryGetServerPlayer(peer, out ServerPlayer player)) { - LogWarning($"OnServerboundWarehouseMachineControllerRequestPacket() ServerPlayer not found: {peer.Id}"); + LogWarning($"ServerboundWarehouseMachineControllerRequestPacket() ServerPlayer not found: {peer.Id}"); return; } - //Find the warehouse - WarehouseMachineController targetWarehouse = NetworkedWarehouseMachineController.FindFomID(packet.WarehouseMachineID); - + //Todo: add check for player authorisation to use loading/uloading machines - if (targetWarehouse == null) + //Find the warehouse + if(!NetworkedWarehouseMachineController.Get(packet.NetId, out var targetWarehouse)) { - LogWarning($"OnServerboundWarehouseMachineControllerRequestPacket() WarehouseMachineController not found. WarehouseMachineControllerID: {packet.WarehouseMachineID}"); - return; + LogWarning($"ServerboundWarehouseMachineControllerRequestPacket() WarehouseMachineController not found. NetId: {packet.NetId}"); + return; } - LogDebug(() => $"OnServerboundWarehouseMachineControllerRequestPacket() {packet.WarehouseMachineID}, Action Type: {packet.warehouseAction}"); - switch (packet.warehouseAction) - { - case WarehouseAction.Load: - targetWarehouse.StartLoadSequence(); - break; + //Todo: add check for player distance from machine - case WarehouseAction.Unload: - targetWarehouse.StartUnloadSequence(); - break; - } + targetWarehouse.ServerProcessWarehouseAction(packet.WarehouseAction); } private void OnCommonChatPacket(CommonChatPacket packet, ITransportPeer peer) diff --git a/Multiplayer/Patches/Jobs/WarehouseMachineControllerPatch.cs b/Multiplayer/Patches/Jobs/WarehouseMachineControllerPatch.cs index 2089151f..d445d5c6 100644 --- a/Multiplayer/Patches/Jobs/WarehouseMachineControllerPatch.cs +++ b/Multiplayer/Patches/Jobs/WarehouseMachineControllerPatch.cs @@ -1,21 +1,23 @@ -using System.Collections; +using DV.Logic.Job; +using DV.ThingTypes; +using HarmonyLib; using Multiplayer.Components.Networking; using Multiplayer.Components.Networking.Jobs; +using Multiplayer.Components.Networking.Train; using Multiplayer.Networking.Data; -using UnityEngine; +using static WarehouseMachineController; namespace Multiplayer.Patches.Jobs; -using HarmonyLib; - [HarmonyPatch(typeof(WarehouseMachineController))] public class WarehouseMachineControllerPatch { [HarmonyPrefix] - [HarmonyPatch("StartUnloadSequence")] - public static void StartUnloadSequence_Prefix(WarehouseMachineController __instance) + [HarmonyPatch(nameof(WarehouseMachineController.Awake))] + public static void Awake(WarehouseMachineController __instance) { - __instance.displayTrainInRangeText.text = __instance.warehouseMachine.ID; + __instance.gameObject.AddComponent(); + } if (!NetworkLifecycle.Instance.IsHost()) { From af190c3e1d4d9141c21b5d8653dbfcf8553a3602 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 28 Jun 2025 17:11:56 +1000 Subject: [PATCH 405/521] Refactor to use NetworkedWarehouseMachineController NetIds --- .../Managers/Client/NetworkClient.cs | 6 +-- .../Managers/Server/NetworkServer.cs | 1 + ...WarehouseMachineControllerRequestPacket.cs | 7 +-- .../Jobs/WarehouseMachineControllerPatch.cs | 45 ++++++++++++------- 4 files changed, 37 insertions(+), 22 deletions(-) diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 1c2ec120..44b79c3f 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -1315,12 +1315,12 @@ public void SendJobValidateRequest(NetworkedJob job, NetworkedStationController }, DeliveryMethod.ReliableUnordered); } - public void SendWarehouseRequest(WarehouseAction action, string track) + public void SendWarehouseRequest(WarehouseAction action, ushort netId) { SendPacketToServer(new ServerboundWarehouseMachineControllerRequestPacket { - warehouseAction = action, - WarehouseMachineID = track + NetId = netId, + WarehouseAction = action, }, DeliveryMethod.ReliableUnordered); } diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index 7cc27f47..c431c39d 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -33,6 +33,7 @@ using System.Text; using Multiplayer.Networking.Data.Train; using Multiplayer.Networking.TransportLayers; +using Multiplayer.Networking.Packets.Serverbound.Jobs; namespace Multiplayer.Networking.Managers.Server; diff --git a/Multiplayer/Networking/Packets/Serverbound/Jobs/ServerboundWarehouseMachineControllerRequestPacket.cs b/Multiplayer/Networking/Packets/Serverbound/Jobs/ServerboundWarehouseMachineControllerRequestPacket.cs index 8e08e364..f7fc7515 100644 --- a/Multiplayer/Networking/Packets/Serverbound/Jobs/ServerboundWarehouseMachineControllerRequestPacket.cs +++ b/Multiplayer/Networking/Packets/Serverbound/Jobs/ServerboundWarehouseMachineControllerRequestPacket.cs @@ -1,8 +1,9 @@ using Multiplayer.Networking.Data; -namespace Multiplayer.Networking.Packets.Clientbound.Jobs; + +namespace Multiplayer.Networking.Packets.Serverbound.Jobs; public class ServerboundWarehouseMachineControllerRequestPacket { - public string WarehouseMachineID { get; set; } - public WarehouseAction warehouseAction { get; set; } + public ushort NetId { get; set; } + public WarehouseAction WarehouseAction { get; set; } } diff --git a/Multiplayer/Patches/Jobs/WarehouseMachineControllerPatch.cs b/Multiplayer/Patches/Jobs/WarehouseMachineControllerPatch.cs index d445d5c6..bcddeb45 100644 --- a/Multiplayer/Patches/Jobs/WarehouseMachineControllerPatch.cs +++ b/Multiplayer/Patches/Jobs/WarehouseMachineControllerPatch.cs @@ -27,31 +27,44 @@ public static void Awake(WarehouseMachineController __instance) } [HarmonyPrefix] - [HarmonyPatch("StartLoadSequence")] - public static void StartLoadSequence_Prefix(WarehouseMachineController __instance) + [HarmonyPatch("StartUnloadSequence")] + public static bool StartUnloadSequence_Prefix(WarehouseMachineController __instance) { - __instance.displayTrainInRangeText.text = __instance.warehouseMachine.ID; + if (NetworkLifecycle.Instance.IsHost()) + return true; - if (!NetworkLifecycle.Instance.IsHost()) - { - SendValidationRequest(__instance, WarehouseAction.Load); - } + SendValidationRequest(__instance, WarehouseAction.Unload); + return false; + } + [HarmonyPrefix] + [HarmonyPatch("StartLoadSequence")] + public static bool StartLoadSequence_Prefix(WarehouseMachineController __instance) + { + if (NetworkLifecycle.Instance.IsHost()) + return true; + + SendValidationRequest(__instance, WarehouseAction.Load); + return false; } - private static void SendValidationRequest(WarehouseMachineController machine,WarehouseAction action) + private static void SendValidationRequest(WarehouseMachineController machine, WarehouseAction action) { - //find the current station we're at - if (!string.IsNullOrEmpty(machine.warehouseTrackName)) - { - string id = machine.warehouseMachine.ID; + string id = machine?.warehouseMachine?.ID; + var netController = NetworkedWarehouseMachineController.GetFromWarehouseMachineController(machine); - NetworkLifecycle.Instance.Client.SendWarehouseRequest(action, id); - //CoroutineManager.Instance.StartCoroutine(AwaitResponse(machine, action)); + if (string.IsNullOrEmpty(id)) + { + NetworkLifecycle.Instance.Client.LogError($"Failed to validate {action} for {machine?.name} at {machine?.warehouseTrackName}. Warehouse not found!"); + return; } - else + + if (netController == null) { - NetworkLifecycle.Instance.Client.LogError($"Failed to validate {action} for {machine.warehouseMachine.ID}. Warehouse not found!"); + NetworkLifecycle.Instance.Client.LogError($"Failed to find NetworkedWarehouseMachineController {machine?.warehouseTrackName}. Warehouse not found!"); + return; } + + NetworkLifecycle.Instance.Client.SendWarehouseRequest(action, netController.NetId); } } From 6c1507ca183d6eb8e58ab8afbacf4f3a6859c40c Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 28 Jun 2025 17:13:00 +1000 Subject: [PATCH 406/521] Make loading and unloading server authoratative --- .../NetworkedWarehouseMachineController.cs | 67 +++++++++++++++++++ .../Managers/Client/NetworkClient.cs | 36 +++++++--- .../Managers/Server/NetworkServer.cs | 16 +++++ ...entboundWarehouseControllerUpdatePacket.cs | 12 ++++ .../Jobs/WarehouseMachineControllerPatch.cs | 64 +++++++++++++++++- 5 files changed, 184 insertions(+), 11 deletions(-) create mode 100644 Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundWarehouseControllerUpdatePacket.cs diff --git a/Multiplayer/Components/Networking/Jobs/NetworkedWarehouseMachineController.cs b/Multiplayer/Components/Networking/Jobs/NetworkedWarehouseMachineController.cs index f5638db3..0c49b9b6 100644 --- a/Multiplayer/Components/Networking/Jobs/NetworkedWarehouseMachineController.cs +++ b/Multiplayer/Components/Networking/Jobs/NetworkedWarehouseMachineController.cs @@ -99,4 +99,71 @@ public void ServerProcessWarehouseAction(WarehouseAction action) break; } } + + public void ClientProcessUpdate(ClientboundWarehouseControllerUpdatePacket packet) + { + TextPreset preset = (TextPreset)packet.Preset; + bool isLoading = packet.IsLoading; + string jobId = null; + Car car = null; + CargoType_v2 cargoType_V2 = null; + string extra = null; + + if (WarehouseMachineController == null) + return; + + if (packet.CarNetId != 0) + { + if (!NetworkedTrainCar.Get(packet.CarNetId, out var networkedCar)) + { + Multiplayer.LogWarning($"NetworkedWarehouseMachineController failed to find TrainCar with NetId: {packet.NetId}"); + return; + } + + car = networkedCar.TrainCar.logicCar; + } + + if (packet.JobNetId != 0) + { + if (!NetworkedJob.Get(packet.JobNetId, out var networkedJob)) + { + Multiplayer.LogWarning($"NetworkedWarehouseMachineController failed to find Job with NetId: {packet.JobNetId}"); + return; + } + + jobId = networkedJob.Job.ID; + } + + if (car != null && jobId != null) + { + cargoType_V2 = ((CargoType)packet.CargoType).ToV2(); + } + + WarehouseMachineController?.SetScreen(preset, isLoading, jobId, car, cargoType_V2, extra); + + //special case for car updated - remove task from machine + if (preset == TextPreset.CarUpdated && WarehouseMachine != null) + { + CleanupTask(isLoading, car); + } + + //special case for clearing - play sound + if (preset == TextPreset.ClearDesc) + WarehouseMachineController?.machineSound?.Play(WarehouseMachineController.transform.position, 1f, 1f, 0f, 1f, 500f, default, null, base.transform, false, 0f, null); + + } + + private void CleanupTask(bool isLoading, Car car) + { + List currentLoadUnloadData = WarehouseMachine.GetCurrentLoadUnloadData(isLoading ? WarehouseTaskType.Loading : WarehouseTaskType.Unloading); + + foreach (var data in currentLoadUnloadData) + { + if (data.tasksAvailableToProcess == null) + continue; + + foreach (var task in data.tasksAvailableToProcess) + WarehouseMachine.RemoveWarehouseTask(task); + } + } } diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 44b79c3f..fa475529 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -1,6 +1,6 @@ -using System; -using System.Collections.Generic; using DV; +using DV.Common; +using DV.Customization.Paint; using DV.Damage; using DV.InventorySystem; using DV.Logic.Job; @@ -8,8 +8,10 @@ using DV.ServicePenalty.UI; using DV.ThingTypes; using DV.UI; +using DV.UserManagement; using DV.WeatherSystem; using LiteNetLib; +using LiteNetLib.Utils; using Multiplayer.Components.MainMenu; using Multiplayer.Components.Networking; using Multiplayer.Components.Networking.Jobs; @@ -19,6 +21,7 @@ using Multiplayer.Components.Networking.World; using Multiplayer.Components.SaveGame; using Multiplayer.Networking.Data; +using Multiplayer.Networking.Data.Train; using Multiplayer.Networking.Packets.Clientbound; using Multiplayer.Networking.Packets.Clientbound.Jobs; using Multiplayer.Networking.Packets.Clientbound.SaveGame; @@ -27,20 +30,18 @@ using Multiplayer.Networking.Packets.Common; using Multiplayer.Networking.Packets.Common.Train; using Multiplayer.Networking.Packets.Serverbound; -using Multiplayer.Networking.Data.Train; +using Multiplayer.Networking.Packets.Serverbound.Jobs; +using Multiplayer.Networking.Packets.Serverbound.Train; +using Multiplayer.Networking.TransportLayers; using Multiplayer.Patches.SaveGame; using Multiplayer.Utils; using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Linq; using UnityEngine; using UnityModManagerNet; using Object = UnityEngine.Object; -using Multiplayer.Networking.Packets.Serverbound.Train; -using System.Linq; -using LiteNetLib.Utils; -using DV.UserManagement; -using DV.Common; -using DV.Customization.Paint; -using Multiplayer.Networking.TransportLayers; namespace Multiplayer.Networking.Managers.Client; @@ -150,9 +151,12 @@ protected override void Subscribe() netPacketProcessor.SubscribeReusable(OnCommonTrainFusesPacket); netPacketProcessor.SubscribeReusable(OnClientboundBrakeStateUpdatePacket); netPacketProcessor.SubscribeReusable(OnClientboundFireboxStatePacket); + netPacketProcessor.SubscribeReusable(OnClientboundCargoStatePacket); netPacketProcessor.SubscribeReusable(OnClientboundCargoHealthUpdatePacket); netPacketProcessor.SubscribeReusable(OnClientboundCarHealthUpdatePacket); + netPacketProcessor.SubscribeReusable(OnClientboundWarehouseControllerUpdatePacket); + netPacketProcessor.SubscribeReusable(OnClientboundRerailTrainPacket); netPacketProcessor.SubscribeReusable(OnClientboundWindowsBrokenPacket); netPacketProcessor.SubscribeReusable(OnClientboundWindowsRepairedPacket); @@ -824,6 +828,18 @@ private void OnClientboundCarHealthUpdatePacket(ClientboundCarHealthUpdatePacket packet.Health.LoadTo(trainCar); } + private void OnClientboundWarehouseControllerUpdatePacket(ClientboundWarehouseControllerUpdatePacket packet) + { + LogDebug(() => $"OnClientboundWarehouseControllerUpdatePacket() NetId: {packet.NetId}, IsLoading: {packet.IsLoading}, JobNetId: {packet.JobNetId}, CarNetId: {packet.CarNetId}, CargoType: {packet.CargoType}, Preset: [{(WarehouseMachineController.TextPreset) packet.Preset}, {packet.Preset}]"); + if (!NetworkedWarehouseMachineController.Get(packet.NetId, out NetworkedWarehouseMachineController networkedWarehouseMachineController)) + { + LogWarning($"OnClientboundWarehouseControllerUpdatePacket() Failed to find networked warehouse machine controller for [{packet.NetId}]"); + return; + } + + networkedWarehouseMachineController.ClientProcessUpdate(packet); + } + private void OnClientboundRerailTrainPacket(ClientboundRerailTrainPacket packet) { diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index c431c39d..a71b1def 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -417,6 +417,22 @@ public void SendCargoState(NetworkedTrainCar netTraincar, bool isLoading, byte c }, DeliveryMethod.ReliableOrdered, SelfPeer); } + public void SendWarehouseControllerUpdate(ushort netId, bool isLoading, ushort jobNetId, ushort carNetId, CargoType cargoType, WarehouseMachineController.TextPreset preset) + { + LogDebug(() =>$"SendWarehouseControllerUpdate({netId}, {isLoading}, {jobNetId}, {carNetId}, {cargoType}, {preset})"); + + SendPacketToAll(new ClientboundWarehouseControllerUpdatePacket() + { + NetId = netId, + IsLoading = isLoading, + JobNetId = jobNetId, + CarNetId = carNetId, + CargoType = (ushort)cargoType, + Preset = (ushort)preset, + }, + DeliveryMethod.Sequenced, SelfPeer); + } + public void SendCargoHealthUpdate(ushort netId, float currentHealth) { SendPacketToAll(new ClientboundCargoHealthUpdatePacket diff --git a/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundWarehouseControllerUpdatePacket.cs b/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundWarehouseControllerUpdatePacket.cs new file mode 100644 index 00000000..efef8f1c --- /dev/null +++ b/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundWarehouseControllerUpdatePacket.cs @@ -0,0 +1,12 @@ + +namespace Multiplayer.Networking.Packets.Clientbound.Jobs; + +public class ClientboundWarehouseControllerUpdatePacket +{ + public ushort NetId { get; set; } + public bool IsLoading { get; set; } + public ushort JobNetId { get; set; } + public ushort CarNetId { get; set; } + public ushort CargoType { get; set; } + public ushort Preset { get; set; } +} diff --git a/Multiplayer/Patches/Jobs/WarehouseMachineControllerPatch.cs b/Multiplayer/Patches/Jobs/WarehouseMachineControllerPatch.cs index bcddeb45..861cfaae 100644 --- a/Multiplayer/Patches/Jobs/WarehouseMachineControllerPatch.cs +++ b/Multiplayer/Patches/Jobs/WarehouseMachineControllerPatch.cs @@ -19,11 +19,73 @@ public static void Awake(WarehouseMachineController __instance) __instance.gameObject.AddComponent(); } + [HarmonyPrefix] + [HarmonyPatch(nameof(WarehouseMachineController.SetScreen))] + public static bool SetScreen(WarehouseMachineController __instance, TextPreset preset, bool isLoading, string jobId, Car car, CargoType_v2 cargoType) + { if (!NetworkLifecycle.Instance.IsHost()) + return true; + + Multiplayer.LogDebug(() => $"WarehouseMachineControllerPatch.SetScreen() is host"); + + bool skip = preset switch + { + TextPreset.Idle => true, + TextPreset.TrainInRange => true, + TextPreset.ClearTrainInRange => true, + _ => false + }; + + Multiplayer.LogDebug(() => $"WarehouseMachineControllerPatch.SetScreen() skipping: {skip}"); + if (skip) + return true; + + var netMachine = NetworkedWarehouseMachineController.GetFromWarehouseMachineController(__instance); + if (netMachine == null) { - SendValidationRequest(__instance, WarehouseAction.Unload); + Multiplayer.LogError($"WarehouseMachineControllerPatch.SetScreen(): Failed to get NetworkedWarehouseMachineController for {__instance.warehouseTrackName}"); + return true; } + Multiplayer.LogDebug(() => $"WarehouseMachineControllerPatch.SetScreen() NetMachine found"); + + //obtain serialisable info + ushort carNetId = 0; + ushort jobNetId = 0; + CargoType cargoTypeV1 = CargoType.None; + + if (car != null) + { + Multiplayer.LogDebug(() => $"WarehouseMachineControllerPatch.SetScreen() car not null"); + var tc = car.TrainCar(); + if (tc == null || !NetworkedTrainCar.TryGetFromTrainCar(tc, out var netTC)) + { + Multiplayer.LogWarning($"WarehouseMachineControllerPatch.SetScreen() Failed to get NetworkedTrainCar for {car?.ID}"); + return true; + } + + Multiplayer.LogDebug(() => $"WarehouseMachineControllerPatch.SetScreen() NetCar found"); + carNetId = netTC.NetId; + } + + if (!string.IsNullOrEmpty(jobId)) + { + if(!NetworkedJob.TryGetFromJobId(jobId, out var netJob)) + { + Multiplayer.LogWarning($"WarehouseMachineControllerPatch.SetScreen() Failed to get NetworkedJob for {jobId}"); + return true; + } + + Multiplayer.LogDebug(() => $"WarehouseMachineControllerPatch.SetScreen() NetJob found"); + jobNetId = netJob.NetId; + } + + if (cargoType != null) + cargoTypeV1 = cargoType.v1; + + NetworkLifecycle.Instance.Server.SendWarehouseControllerUpdate(netMachine.NetId, isLoading, jobNetId, carNetId, cargoTypeV1, preset); + + return false; } [HarmonyPrefix] From 37fc8ac5bc8e9176003e174db416171560ff741d Mon Sep 17 00:00:00 2001 From: AMacro Date: Sat, 28 Jun 2025 18:26:22 +1000 Subject: [PATCH 407/521] Ready for release --- Multiplayer/Multiplayer.csproj | 4 ++-- .../Networking/Managers/Client/NetworkClient.cs | 2 +- .../Jobs/WarehouseMachineControllerPatch.cs | 14 +++++++------- info.json | 2 +- releases.json | 2 +- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Multiplayer/Multiplayer.csproj b/Multiplayer/Multiplayer.csproj index cbe176cc..f9c41b0c 100644 --- a/Multiplayer/Multiplayer.csproj +++ b/Multiplayer/Multiplayer.csproj @@ -1,9 +1,9 @@ - + net48 latest Multiplayer - 0.1.11.6 + 0.1.12.0 diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index fa475529..f2f22879 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -830,7 +830,7 @@ private void OnClientboundCarHealthUpdatePacket(ClientboundCarHealthUpdatePacket private void OnClientboundWarehouseControllerUpdatePacket(ClientboundWarehouseControllerUpdatePacket packet) { - LogDebug(() => $"OnClientboundWarehouseControllerUpdatePacket() NetId: {packet.NetId}, IsLoading: {packet.IsLoading}, JobNetId: {packet.JobNetId}, CarNetId: {packet.CarNetId}, CargoType: {packet.CargoType}, Preset: [{(WarehouseMachineController.TextPreset) packet.Preset}, {packet.Preset}]"); + LogDebug(() => $"OnClientboundWarehouseControllerUpdatePacket() NetId: {packet.NetId}, IsLoading: {packet.IsLoading}, JobNetId: {packet.JobNetId}, CarNetId: {packet.CarNetId}, CargoType: {packet.CargoType}, Preset: [{(WarehouseMachineController.TextPreset)packet.Preset}, {packet.Preset}]"); if (!NetworkedWarehouseMachineController.Get(packet.NetId, out NetworkedWarehouseMachineController networkedWarehouseMachineController)) { LogWarning($"OnClientboundWarehouseControllerUpdatePacket() Failed to find networked warehouse machine controller for [{packet.NetId}]"); diff --git a/Multiplayer/Patches/Jobs/WarehouseMachineControllerPatch.cs b/Multiplayer/Patches/Jobs/WarehouseMachineControllerPatch.cs index 861cfaae..1de6e298 100644 --- a/Multiplayer/Patches/Jobs/WarehouseMachineControllerPatch.cs +++ b/Multiplayer/Patches/Jobs/WarehouseMachineControllerPatch.cs @@ -26,7 +26,7 @@ public static bool SetScreen(WarehouseMachineController __instance, TextPreset p if (!NetworkLifecycle.Instance.IsHost()) return true; - Multiplayer.LogDebug(() => $"WarehouseMachineControllerPatch.SetScreen() is host"); + //Multiplayer.LogDebug(() => $"WarehouseMachineControllerPatch.SetScreen() is host"); bool skip = preset switch { @@ -36,7 +36,7 @@ public static bool SetScreen(WarehouseMachineController __instance, TextPreset p _ => false }; - Multiplayer.LogDebug(() => $"WarehouseMachineControllerPatch.SetScreen() skipping: {skip}"); + //Multiplayer.LogDebug(() => $"WarehouseMachineControllerPatch.SetScreen() skipping: {skip}"); if (skip) return true; @@ -47,7 +47,7 @@ public static bool SetScreen(WarehouseMachineController __instance, TextPreset p return true; } - Multiplayer.LogDebug(() => $"WarehouseMachineControllerPatch.SetScreen() NetMachine found"); + //Multiplayer.LogDebug(() => $"WarehouseMachineControllerPatch.SetScreen() NetMachine found"); //obtain serialisable info ushort carNetId = 0; @@ -56,15 +56,15 @@ public static bool SetScreen(WarehouseMachineController __instance, TextPreset p if (car != null) { - Multiplayer.LogDebug(() => $"WarehouseMachineControllerPatch.SetScreen() car not null"); + //Multiplayer.LogDebug(() => $"WarehouseMachineControllerPatch.SetScreen() car not null"); var tc = car.TrainCar(); if (tc == null || !NetworkedTrainCar.TryGetFromTrainCar(tc, out var netTC)) { - Multiplayer.LogWarning($"WarehouseMachineControllerPatch.SetScreen() Failed to get NetworkedTrainCar for {car?.ID}"); + //Multiplayer.LogWarning($"WarehouseMachineControllerPatch.SetScreen() Failed to get NetworkedTrainCar for {car?.ID}"); return true; } - Multiplayer.LogDebug(() => $"WarehouseMachineControllerPatch.SetScreen() NetCar found"); + //Multiplayer.LogDebug(() => $"WarehouseMachineControllerPatch.SetScreen() NetCar found"); carNetId = netTC.NetId; } @@ -76,7 +76,7 @@ public static bool SetScreen(WarehouseMachineController __instance, TextPreset p return true; } - Multiplayer.LogDebug(() => $"WarehouseMachineControllerPatch.SetScreen() NetJob found"); + //Multiplayer.LogDebug(() => $"WarehouseMachineControllerPatch.SetScreen() NetJob found"); jobNetId = netJob.NetId; } diff --git a/info.json b/info.json index fef52cbe..c6233d01 100644 --- a/info.json +++ b/info.json @@ -1,6 +1,6 @@ { "Id": "Multiplayer", - "Version": "0.1.11.6", + "Version": "0.1.12.0", "DisplayName": "Multiplayer", "Author": "Insprill, Macka, Morm", "EntryMethod": "Multiplayer.Multiplayer.Load", diff --git a/releases.json b/releases.json index 648f1fc2..5f9a2366 100644 --- a/releases.json +++ b/releases.json @@ -1,6 +1,6 @@ { "Releases": [ - {"Id": "Multiplayer", "Version": "0.1.11.6", "DownloadUrl": "https://github.com/AMacro/dv-multiplayer/releases/download/v0.1.11.6-Beta/Multiplayer.0.1.11.6.zip"} + {"Id": "Multiplayer", "Version": "0.1.12.0", "DownloadUrl": "https://github.com/AMacro/dv-multiplayer/releases/download/v0.1.12.0-Beta/Multiplayer.0.1.12.0.zip"} ] } \ No newline at end of file From 97823745ad3808ff1ea43b6d4349367fd167dfa0 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 29 Jun 2025 14:27:47 +1000 Subject: [PATCH 408/521] Change BlockInput to prevent disabling Manual Service --- Multiplayer/Components/Networking/UI/ChatGUI.cs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/Multiplayer/Components/Networking/UI/ChatGUI.cs b/Multiplayer/Components/Networking/UI/ChatGUI.cs index d4165b40..83fd9039 100644 --- a/Multiplayer/Components/Networking/UI/ChatGUI.cs +++ b/Multiplayer/Components/Networking/UI/ChatGUI.cs @@ -653,25 +653,32 @@ private void BuildUI() private void BlockInput(bool block) { - //player.Locomotion.inputEnabled = !block; - //hotbarController.enabled = !block; if (block) { denied = GameFeatureFlags.DeniedFlags; - GameFeatureFlags.Deny(GameFeatureFlags.Flag.ALL); + GameFeatureFlags.Deny(GameFeatureFlags.Flag.Movement); + GameFeatureFlags.Deny(GameFeatureFlags.Flag.Look); + GameFeatureFlags.Deny(GameFeatureFlags.Flag.Hotbar); + GameFeatureFlags.Deny(GameFeatureFlags.Flag.Inventory); + GameFeatureFlags.Deny(GameFeatureFlags.Flag.ItemGrab); + GameFeatureFlags.Deny(GameFeatureFlags.Flag.WorldInteraction); + GameFeatureFlags.Deny(GameFeatureFlags.Flag.MouseMode); + GameFeatureFlags.Deny(GameFeatureFlags.Flag.KeyboardDriving); + 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(); + //InputFocusManager.Instance.ReleaseKeyboardFocus(); } } - #endregion } From db1d634e50f2820217c704b5604497ff3bb8d431 Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 29 Jun 2025 19:46:09 +1000 Subject: [PATCH 409/521] Improve culling algorithm --- .../World/NetworkedPitStopStation.cs | 34 ++++++++++++++----- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs b/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs index 93204c66..6ffdf1df 100644 --- a/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs +++ b/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs @@ -69,7 +69,7 @@ public static void InitialisePitStops() const float MAX_DELTA = 0.2f; const float MIN_UPDATE_TIME = 0.1f; - const float LOADING_TIMEOUT = 5f; + const float LOADING_TIMEOUT = 10f; const float ROTATION_SMOOTH_SPEED = 5f; const float FAUCET_SNAP_THRESHOLD = 0.005f; @@ -80,6 +80,7 @@ public static void InitialisePitStops() #region Server variables private Dictionary playerToLastNearbyTime; private float disablerSqrDistance = DEFAULT_DISABLER_SQR_DISTANCE; + private float EnablerSqrDistance => disablerSqrDistance / 2; private float disablerCheckInterval = DEFAULT_DISABLER_INTERVAL; private readonly Dictionary resourceStartStopDelegates = []; @@ -109,7 +110,7 @@ public static void InitialisePitStops() private readonly Dictionary isResourceGrabbedDict = []; private readonly Dictionary isResourceRemoteGrabbedDict = []; private readonly Dictionary lastRemoteValueDict = []; - + private bool isFaucetGrabbed = false; private float lastFaucetUpdateTime = 0.0f; private float lastFaucetSent = 0.0f; @@ -289,11 +290,13 @@ private IEnumerator PlayerDistanceChecker() continue; } - //player nearby recently, update time - playerToLastNearbyTime[player.Id] = Time.time; - + //if not initialised if (!initialised) { + //make sure they are close by before we add them to the nearby list + if (sqrDistance > EnablerSqrDistance) + continue; + if (!NetworkLifecycle.Instance.Server.TryGetPeer(player.Id, out var peer)) continue; @@ -327,6 +330,9 @@ private IEnumerator PlayerDistanceChecker() NetworkLifecycle.Instance.Server.SendPitStopBulkDataPacket(NetId, Station.pitstop.carList.Count, carIndex, stateData, plugData, peer); } } + + //player nearby recently, update time + playerToLastNearbyTime[player.Id] = Time.time; } } } @@ -391,7 +397,7 @@ private void OnTick(uint tick) var module = kvp.Key; - SendResourceUpdate(module); + SendResourceUpdate(module); } } @@ -521,7 +527,7 @@ private IEnumerator Init() leverStateLookup[resourceModule.resourceType] = (checker, resourceModule, LeverStatehandler); grabbedHandlerLookup[resourceModule.resourceType] = grab; - if(lever != null) + if (lever != null) leverLookup[resourceModule.resourceType] = lever; //sb.AppendLine($"\t{resourceModule.resourceType}, Grab Handler found: {grab != null}, Name: {grab.name}"); @@ -561,10 +567,20 @@ private IEnumerator WaitForLoad(ClientboundPitStopBulkUpdatePacket packet) yield return new WaitUntil ( () => - (initialised && Station?.pitstop?.carList != null && packet.CarCount == Station.pitstop.carList.Count) - || (Time.time - time) > LOADING_TIMEOUT + { + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.WaitForLoad() PitStop [{StationName}] PitStop Initialised: {initialised}, Packet Car Count: {packet.CarCount}, Station Car Count: {Station.pitstop?.carList?.Count}, Car Count Matched: {packet.CarCount == Station.pitstop?.carList?.Count}, time elapsed: {(Time.time - time)}"); + + //try to trigger colliders manually + if (initialised && Station?.pitstop?.carList != null && packet.CarCount != Station.pitstop.carList.Count) + Station?.pitstop?.RefreshPitStopCarPresence(); + + return (initialised && Station?.pitstop?.carList != null && packet.CarCount == Station.pitstop.carList.Count) + || (Time.time - time) > LOADING_TIMEOUT; + + } ); + yield return new WaitForEndOfFrame(); yield return new WaitForEndOfFrame(); From 8dce628d4630a51b17452dd0c634518b6bc4038e Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 6 Jul 2025 09:49:04 +1000 Subject: [PATCH 410/521] Implement a generic CullingManager --- .../Managers/Server/CullingManager.cs | 120 ++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 Multiplayer/Networking/Managers/Server/CullingManager.cs diff --git a/Multiplayer/Networking/Managers/Server/CullingManager.cs b/Multiplayer/Networking/Managers/Server/CullingManager.cs new file mode 100644 index 00000000..7b7a4552 --- /dev/null +++ b/Multiplayer/Networking/Managers/Server/CullingManager.cs @@ -0,0 +1,120 @@ +using Multiplayer.Components.Networking; +using Multiplayer.Networking.Data; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace Multiplayer.Networking.Managers.Server; + +public class CullingManager : IDisposable +{ + private const float DEFAULT_CULL_SQR_DISTANCE = 250000f; + + public event Action PlayerEnteredActivationRegion; + public event Action PlayerEnteredCullingRegion; + + public List ActivePlayers => playerToLastNearbyTime.Keys.ToList(); + + private readonly Dictionary playerToLastNearbyTime = []; + private readonly float _checkInterval = 2f; + private readonly float _cullSqrDistance = DEFAULT_CULL_SQR_DISTANCE; + private readonly float _activationSqrDistance = DEFAULT_CULL_SQR_DISTANCE / 2; + private readonly float _cullDelay = 3f; + private GameObject _referenceObject = null; + + private Coroutine checkCoro; + + public CullingManager(float checkInterval, float cullSqrDistance, float activationSqrDistance, float cullDelay, GameObject referenceObject) + { + if (checkInterval > 0) + _checkInterval = checkInterval; + + if (cullSqrDistance > 0) + _cullSqrDistance = cullSqrDistance; + + if (activationSqrDistance > 0) + _activationSqrDistance = activationSqrDistance; + + if (cullDelay >= 0) + _cullDelay = cullDelay; + + if (referenceObject != null) + _referenceObject = referenceObject; + else + throw new Exception("Reference object is null!"); + + checkCoro = CoroutineManager.Instance.StartCoroutine(PlayerDistanceChecker()); + + NetworkLifecycle.Instance.Server.PlayerDisconnect += OnPlayerDisconnected; + } + + public void Dispose() + { + if (checkCoro != null) + CoroutineManager.Instance.Stop(checkCoro); + + NetworkLifecycle.Instance.Server.PlayerDisconnect -= OnPlayerDisconnected; + } + + //todo: fix when merged with ModAPI branch + private void OnPlayerDisconnected(uint playerId) + { + var player = playerToLastNearbyTime.Keys.Where(p => p.Id == playerId).FirstOrDefault(); + + if (player == null) + return; + + playerToLastNearbyTime.Remove(player); + } + + private IEnumerator PlayerDistanceChecker() + { + //wait for game to finish loading + yield return new WaitForSeconds(2f); + + while (true) + { + yield return new WaitForSeconds(_checkInterval); + + //if not active then there is no one close by + if (_referenceObject != null && _referenceObject.activeInHierarchy) + { + foreach (var player in NetworkLifecycle.Instance.Server.ServerPlayers) + { + if (player.Id == NetworkLifecycle.Instance.Server.SelfId || !player.IsLoaded) + continue; + + float sqrDistance = (player.WorldPosition - _referenceObject.transform.position).sqrMagnitude; + + bool initialised = playerToLastNearbyTime.TryGetValue(player, out float lastVisit); + + if (initialised && sqrDistance > _cullSqrDistance) + { + // Too far away for too long, stop tracking + if ((Time.time - lastVisit) > _cullDelay) + { + playerToLastNearbyTime.Remove(player); + PlayerEnteredCullingRegion?.Invoke(player); + } + + continue; + } + + if (!initialised) + { + //make sure they are close by before we add them to the nearby list + if (sqrDistance > _activationSqrDistance) + continue; + + PlayerEnteredActivationRegion?.Invoke(player); + } + + //player nearby recently, update time + playerToLastNearbyTime[player] = Time.time; + } + } + } + } +} From 8d8b4b9a4a4063b47971e6602299b0ce84dcc04a Mon Sep 17 00:00:00 2001 From: AMacro Date: Mon, 7 Jul 2025 21:42:57 +1000 Subject: [PATCH 411/521] Refactor to use Culling Manager --- .../World/NetworkedPitStopStation.cs | 169 ++++++++---------- 1 file changed, 75 insertions(+), 94 deletions(-) diff --git a/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs b/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs index 6ffdf1df..58879f94 100644 --- a/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs +++ b/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs @@ -2,6 +2,7 @@ using DV.Interaction; using DV.ThingTypes; using Multiplayer.Networking.Data; +using Multiplayer.Networking.Managers.Server; using Multiplayer.Networking.Packets.Clientbound.World; using Multiplayer.Networking.Packets.Common; using Multiplayer.Networking.TransportLayers; @@ -78,10 +79,7 @@ public static void InitialisePitStops() const float NEARBY_REMOVAL_DELAY = 3f; #region Server variables - private Dictionary playerToLastNearbyTime; - private float disablerSqrDistance = DEFAULT_DISABLER_SQR_DISTANCE; - private float EnablerSqrDistance => disablerSqrDistance / 2; - private float disablerCheckInterval = DEFAULT_DISABLER_INTERVAL; + public CullingManager CullingManager { get; private set; } private readonly Dictionary resourceStartStopDelegates = []; private readonly Dictionary resourceFlowing = []; @@ -130,16 +128,22 @@ protected override void Awake() if (NetworkLifecycle.Instance.IsHost()) { - playerToLastNearbyTime = []; - var disabler = GetComponentInParent(); + + var cullingSqrDistance = DEFAULT_DISABLER_SQR_DISTANCE; + var cullingCheckInterval = DEFAULT_DISABLER_INTERVAL; + if (disabler != null) { - disablerSqrDistance = disabler.disableSqrDistance; - disablerCheckInterval = disabler.checkPeriodPerGO; + cullingSqrDistance = disabler.disableSqrDistance; + cullingCheckInterval = disabler.checkPeriodPerGO; } - StartCoroutine(PlayerDistanceChecker()); + var activationSqrDistance = cullingSqrDistance / 2; + + CullingManager = new(cullingCheckInterval, cullingSqrDistance, activationSqrDistance, NEARBY_REMOVAL_DELAY, gameObject); + CullingManager.PlayerEnteredActivationRegion += OnPlayerEnteredActivationRegion; + CullingManager.PlayerEnteredCullingRegion += OnPlayerEnteredCullingRegion; NetworkLifecycle.Instance.OnTick += OnTick; //ensure host can interact @@ -159,8 +163,6 @@ protected override void OnDestroy() if (NetworkLifecycle.Instance.IsHost()) { - StopCoroutine(PlayerDistanceChecker()); - foreach (var kvp in resourceStartStopDelegates) { var (fillStart, fillStop, drainStart, drainStop) = kvp.Value; @@ -171,6 +173,15 @@ protected override void OnDestroy() } resourceStartStopDelegates.Clear(); + + if (CullingManager != null) + { + CullingManager.PlayerEnteredActivationRegion -= OnPlayerEnteredActivationRegion; + CullingManager.PlayerEnteredCullingRegion -= OnPlayerEnteredCullingRegion; + CullingManager.Dispose(); + } + + NetworkLifecycle.Instance.OnTick -= OnTick; } if (carSelectorGrab != null) @@ -260,114 +271,77 @@ public void OnPlayerDisconnect(ITransportPeer peer) //Multiplayer.LogWarning($"OnPlayerDisconnect()"); } - private IEnumerator PlayerDistanceChecker() + public void OnPlayerEnteredActivationRegion(ServerPlayer player) { - //wait for game to finish loading - yield return new WaitForSeconds(1f); - - while (true) + if (Station.pitstop.IsCarInPitStop()) { - yield return new WaitForSeconds(disablerCheckInterval); - - //if not active then there is no one close by - if (gameObject != null && gameObject.activeInHierarchy && Station != null && Station.pitstop != null) - { - foreach (var player in NetworkLifecycle.Instance.Server.ServerPlayers) - { - if (player.Id == NetworkLifecycle.Instance.Server.SelfId || !player.IsLoaded) - continue; - - float sqrDistance = (player.WorldPosition - transform.position).sqrMagnitude; + // Ensure all resource data exists + InitialiseData(); - bool initialised = playerToLastNearbyTime.TryGetValue(player.Id, out float lastVisit); - - if (sqrDistance > disablerSqrDistance) - { - // Too far away for too long, stop tracking - if ((Time.time - lastVisit) > NEARBY_REMOVAL_DELAY) - playerToLastNearbyTime.Remove(player.Id); - - continue; - } + // One struct per module type + var resourceCount = Station.locoResourceModules.resourceModules.Count(); + LocoResourceModuleData[] stateData = new LocoResourceModuleData[resourceCount]; - //if not initialised - if (!initialised) - { - //make sure they are close by before we add them to the nearby list - if (sqrDistance > EnablerSqrDistance) - continue; + int i; + for (i = 0; i < resourceCount; i++) + { + stateData[i] = LocoResourceModuleData.From(Station.locoResourceModules.resourceModules[i]); + } - if (!NetworkLifecycle.Instance.Server.TryGetPeer(player.Id, out var peer)) - continue; + // Car selection and lever states + int carIndex = Station.pitstop.SelectedIndex; - if (Station.pitstop.IsCarInPitStop()) - { - // Ensure all resource data exists - InitialiseData(); - - // One struct per module type - var resourceCount = Station.locoResourceModules.resourceModules.Count(); - LocoResourceModuleData[] stateData = new LocoResourceModuleData[resourceCount]; - int i; - for (i = 0; i < resourceCount; i++) - { - stateData[i] = LocoResourceModuleData.From(Station.locoResourceModules.resourceModules[i]); - } - - // Car selection and lever states - int carIndex = Station.pitstop.SelectedIndex; - - PitStopPlugData[] plugData = new PitStopPlugData[resourceToPluggableObject.Count]; - - i = 0; - foreach (var plug in resourceToPluggableObject) - { - plugData[i] = PitStopPlugData.From(plug.Value, true); - i++; - } - - // Send current state - NetworkLifecycle.Instance.Server.SendPitStopBulkDataPacket(NetId, Station.pitstop.carList.Count, carIndex, stateData, plugData, peer); - } - } + PitStopPlugData[] plugData = new PitStopPlugData[resourceToPluggableObject.Count]; - //player nearby recently, update time - playerToLastNearbyTime[player.Id] = Time.time; - } + i = 0; + foreach (var plug in resourceToPluggableObject) + { + plugData[i] = PitStopPlugData.From(plug.Value, true); + i++; } + + // Send current state + NetworkLifecycle.Instance.Server.SendPitStopBulkDataPacket(NetId, Station.pitstop.carList.Count, carIndex, stateData, plugData, player.Peer); } } - public void ProcessInteractionPacketAsHost(CommonPitStopInteractionPacket packet, ITransportPeer peer) + public void OnPlayerEnteredCullingRegion(ServerPlayer player) { - Multiplayer.LogDebug(() => $"ProcessInteractionPacketAsHost() from peer: {peer.Id}, selfpeer: {NetworkLifecycle.Instance.Server.SelfId}"); + //todo: when a player leaves the region cancel any interactions + //Multiplayer.LogWarning($"OnPlayerDisconnect()"); + } + + public void ProcessInteractionPacketAsHost(CommonPitStopInteractionPacket packet, ServerPlayer senderPlayer) + { + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.ProcessInteractionPacketAsHost() from: {senderPlayer.Username}, id: {senderPlayer.Id}, selfpeer: {NetworkLifecycle.Instance.Server.SelfId}"); - if (ValidateInteraction(packet, peer)) + if (ValidateInteraction(packet, senderPlayer)) { processingAsHost = true; - if (peer.Id != NetworkLifecycle.Instance.Server.SelfPeer.Id) + if (senderPlayer.Id != NetworkLifecycle.Instance.Server.SelfId) { - Multiplayer.LogDebug(() => $"ProcessInteractionPacketAsHost() ProcessPacketAsClient()"); + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.ProcessInteractionPacketAsHost() ProcessPacketAsClient()"); ProcessInteractionPacketAsClient(packet); } processingAsHost = false; + //Send to all other players - foreach (var playerId in playerToLastNearbyTime.Keys) + foreach (var player in CullingManager.ActivePlayers) { - if (NetworkLifecycle.Instance.Server.TryGetPeer(playerId, out var sendPeer) && sendPeer.Id != peer.Id) + if (player.Id != senderPlayer.Id) { - Multiplayer.LogDebug(() => $"ProcessInteractionPacketAsHost() sending to peer: {sendPeer.Id}"); - NetworkLifecycle.Instance.Server.SendPitStopInteractionPacket(sendPeer, packet); + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.ProcessInteractionPacketAsHost() sending to player: {player.Username}"); + NetworkLifecycle.Instance.Server.SendPitStopInteractionPacket(player, packet); } } } else { - Multiplayer.LogDebug(() => $"ProcessInteractionPacketAsHost() failed validation"); + Multiplayer.LogDebug(() => $"NetworkedPitStopStationProcessInteractionPacketAsHost() failed validation"); //Failed to validate, player needs to rollback interaction NetworkLifecycle.Instance.Server.SendPitStopInteractionPacket( - peer, + senderPlayer, new CommonPitStopInteractionPacket { NetId = packet.NetId, @@ -411,12 +385,12 @@ private void SendResourceUpdate(LocoResourceModule module) Value = module.Data.unitsToBuy }; - foreach (var playerId in playerToLastNearbyTime.Keys) + foreach (var player in CullingManager.ActivePlayers) { - if (NetworkLifecycle.Instance.Server.TryGetPeer(playerId, out var sendPeer)) + if (player != null) { - Multiplayer.LogDebug(() => $"NetworkedPitStopStation.SendResourceUpdate({module.resourceType}) sending to peer: {sendPeer.Id}, value: {module.Data.unitsToBuy}, flowing: {module.IsFlowing}"); - NetworkLifecycle.Instance.Server.SendPitStopInteractionPacket(sendPeer, packet); + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.SendResourceUpdate({module.resourceType}) sending to peer: {player.Username}, value: {module.Data.unitsToBuy}, flowing: {module.IsFlowing}"); + NetworkLifecycle.Instance.Server.SendPitStopInteractionPacket(player, packet); } } } @@ -568,7 +542,14 @@ private IEnumerator WaitForLoad(ClientboundPitStopBulkUpdatePacket packet) ( () => { - Multiplayer.LogDebug(() => $"NetworkedPitStopStation.WaitForLoad() PitStop [{StationName}] PitStop Initialised: {initialised}, Packet Car Count: {packet.CarCount}, Station Car Count: {Station.pitstop?.carList?.Count}, Car Count Matched: {packet.CarCount == Station.pitstop?.carList?.Count}, time elapsed: {(Time.time - time)}"); + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.WaitForLoad() PitStop [{StationName}] PitStop Initialised: {initialised}, PitStop Active:{Station?.gameObject?.activeInHierarchy}, Packet Car Count: {packet.CarCount}, Station Car Count: {Station.pitstop?.carList?.Count}, Car Count Matched: {packet.CarCount == Station.pitstop?.carList?.Count}, time elapsed: {(Time.time - time)}"); + + if (Station?.gameObject?.activeInHierarchy == false) + { + //don't time out if we're waiting for the object to be enabled + time = Time.time; + return false; + } //try to trigger colliders manually if (initialised && Station?.pitstop?.carList != null && packet.CarCount != Station.pitstop.carList.Count) From 7b16d21ce5edf2fefa5221fc60ab51dc4f22c444 Mon Sep 17 00:00:00 2001 From: AMacro Date: Mon, 7 Jul 2025 21:46:05 +1000 Subject: [PATCH 412/521] Rework PluggableObject workflow --- .../World/NetworkedPluggableObject.cs | 631 ++++++++++++++---- .../Networking/Data/PitStopPlugData.cs | 39 +- .../Networking/Data/PlugInteractionType.cs | 1 + .../Managers/Client/NetworkClient.cs | 16 +- .../Managers/Server/NetworkServer.cs | 6 + .../CommonPitStopPlugInteractionPacket.cs | 24 +- .../Patches/World/PluggableObjectPatch.cs | 36 +- Multiplayer/Patches/World/PropHosePatch.cs | 212 ++++++ 8 files changed, 820 insertions(+), 145 deletions(-) create mode 100644 Multiplayer/Patches/World/PropHosePatch.cs diff --git a/Multiplayer/Components/Networking/World/NetworkedPluggableObject.cs b/Multiplayer/Components/Networking/World/NetworkedPluggableObject.cs index bbd8cd67..84359ea0 100644 --- a/Multiplayer/Components/Networking/World/NetworkedPluggableObject.cs +++ b/Multiplayer/Components/Networking/World/NetworkedPluggableObject.cs @@ -1,10 +1,10 @@ using DV.CabControls; using DV.Interaction; -using Multiplayer.Components.Networking.Player; using Multiplayer.Components.Networking.Train; using Multiplayer.Networking.Data; using Multiplayer.Networking.Packets.Common; using Multiplayer.Utils; +using System; using System.Collections; using System.Collections.Generic; using UnityEngine; @@ -13,49 +13,83 @@ namespace Multiplayer.Components.Networking.World; public class NetworkedPluggableObject : IdMonoBehaviour { + private const float DISTANCE_TOLERANCE = 1.05f; //allow 5% tolerance for interactions coming from clients + private const float GRAB_SQR_DISTANCE = GrabberRaycaster.SPHERE_CAST_MAX_DIST * GrabberRaycaster.SPHERE_CAST_MAX_DIST * DISTANCE_TOLERANCE; + private const float DOCK_SQR_DISTANCE = 2f * 2f * DISTANCE_TOLERANCE; //no accessible constant available, hardcoded in to `PluggableObject.ScanForHit()` + + private const sbyte INVALID_SOCKET = -1; + private const ushort INVALID_NETID = 0; + #region Lookup Cache private static readonly Dictionary plugToStation = []; + private static readonly Dictionary plugToNetworkedPluggable = []; + public static bool Get(ushort netId, out NetworkedPluggableObject obj) { bool b = Get(netId, out IdMonoBehaviour rawObj); obj = (NetworkedPluggableObject)rawObj; return b; } + + public static bool Get(PluggableObject pluggableObject, out NetworkedPluggableObject obj) + { + bool b = plugToNetworkedPluggable.TryGetValue(pluggableObject, out obj); + return b; + } #endregion protected override bool IsIdServerAuthoritative => false; #region Server Variables - public PlugInteractionType CurrentInteraction { get; set; } public ServerPlayer HeldBy { get; private set; } - public ushort TrainCarNetId { get; private set; } - public bool IsConnectedLeft { get; private set; } - #endregion + + #region Common Variables public PluggableObject PluggableObject { get; private set; } + public Rigidbody PlugRB { get; private set; } + public PropHose Hose { get; private set; } public NetworkedPitStopStation Station { get; private set; } + public bool IsConnecting { get; set; } = false; + + public bool IsHeld => playerHolding != 0 || HeldBy != null || PluggableObject.controlGrabbed; private GrabHandlerGizmoItem grabHandler; private bool handlersInitialised = false; - private byte playerHolding = 0; - private bool isGrabbed = false; + public ushort TrainCarNetId { get; private set; } = INVALID_NETID; //initialise to invalid TrainCar + public sbyte SocketIndex { get; private set; } = INVALID_SOCKET; //initialise to invalid socket + private PlugInteractionType currentInteraction = PlugInteractionType.Rejected; + + private bool processingAsHost = false; + #endregion + + #region Client Variables private bool Refreshed = false; + private byte playerHolding; + #endregion #region Unity protected override void Awake() { - if (NetId == 0) + if (NetId == INVALID_NETID) base.Awake(); PluggableObject = GetComponent(); + Hose = transform.parent.GetComponentInChildren(); + PlugRB = PluggableObject.GetComponent(); + + //Multiplayer.LogDebug(() => $"NetworkedPluggableObject.Awake() {PluggableObject?.controlBase?.spec?.name}, {transform.parent.name}"); - Multiplayer.LogDebug(() => $"NetworkedPluggableObject.Awake() {this.GetObjectPath()}, netId: {NetId}, PluggableObject found: {PluggableObject != null}"); + Multiplayer.LogDebug(() => $"NetworkedPluggableObject.Awake() {this.GetObjectPath()}, netId: {NetId}, PluggableObject found: {PluggableObject != null}, RB Found: {PlugRB != null}, Hose found: {Hose != null}"); if (NetworkLifecycle.Instance.IsHost()) + { + NetworkLifecycle.Instance.Server.PlayerDisconnect += OnPlayerDisconnected; + Refreshed = true; + } } protected IEnumerator Start() @@ -69,78 +103,352 @@ protected IEnumerator Start() PluggableObject.controlBase.Grabbed += OnGrabbed; PluggableObject.controlBase.Ungrabbed += OnUngrabbed; - PluggableObject.PluggedIn += OnPlugged; + PluggableObject.PluggedIn += OnPluggedIn; handlersInitialised = true; } protected void OnDisable() { - Refreshed = false; + if (!NetworkLifecycle.Instance.IsHost()) + Refreshed = false; } protected override void OnDestroy() { if (UnloadWatcher.isUnloading) + { plugToStation.Clear(); + plugToNetworkedPluggable.Clear(); + } else + { plugToStation.Remove(this); + plugToNetworkedPluggable.Remove(PluggableObject); + } if (PluggableObject?.controlBase != null && handlersInitialised) { PluggableObject.controlBase.Grabbed -= OnGrabbed; PluggableObject.controlBase.Ungrabbed -= OnUngrabbed; - PluggableObject.PluggedIn -= OnPlugged; + PluggableObject.PluggedIn -= OnPluggedIn; + } + + if (NetworkLifecycle.Instance.IsHost()) + { + NetworkLifecycle.Instance.Server.PlayerDisconnect -= OnPlayerDisconnected; } base.OnDestroy(); } + + protected void LateUpdate() + { + if (currentInteraction == PlugInteractionType.Rejected) + return; + + Multiplayer.LogDebug(()=>$"NetworkedPluggableObject.LateUpdate()station: {Station?.StationName}, processing: {NetworkLifecycle.Instance.IsProcessingPacket}, processing as Host: {processingAsHost}, refreshed: {Refreshed}, isConnecting: {IsConnecting}"); + if (!processingAsHost) + { + NetworkLifecycle.Instance.Client?.SendPitStopPlugInteractionPacket(NetId, currentInteraction, transform.position - WorldMover.currentMove, transform.rotation, TrainCarNetId, SocketIndex); + } + else + { + //this will only trigger when there's a valid state to be sent (current interaction is not rejected) + //and the host is processing a packet + //this should be the end of the processing, even for docking plugs, so we can clear the connecting and processing flags + IsConnecting = false; + processingAsHost = false; + } + + currentInteraction = PlugInteractionType.Rejected; + } #endregion #region Server + public void ProcessInteractionPacketAsHost(CommonPitStopPlugInteractionPacket packet, ServerPlayer senderPlayer) + { + Multiplayer.LogDebug(() => $"NetworkedPluggableObject.ProcessInteractionPacketAsHost() NetId: {NetId}, InteractionType: {packet.InteractionType}, from player: {senderPlayer.Username}"); + + if (ValidateInteraction(packet, senderPlayer)) + { + //passed validation, set server states + Multiplayer.LogDebug(() => $"NetworkedPluggableObject.ProcessInteractionPacketAsHost() VALIDATION PASSED for NetId: {NetId}"); + + switch (packet.InteractionType) + { + case PlugInteractionType.PickedUp: + Multiplayer.LogDebug(() => $"NetworkedPluggableObject.ProcessInteractionPacketAsHost() Processing {packet.InteractionType} for NetId: {NetId}"); + HeldBy = senderPlayer; + TrainCarNetId = INVALID_NETID; + SocketIndex = INVALID_SOCKET; + + break; + + case PlugInteractionType.Dropped: + Multiplayer.LogDebug(() => $"NetworkedPluggableObject.ProcessInteractionPacketAsHost() Processing {packet.InteractionType} for NetId: {NetId}"); + HeldBy = null; + TrainCarNetId = INVALID_NETID; + SocketIndex = INVALID_SOCKET; + + break; + + case PlugInteractionType.Yanked: + Multiplayer.LogDebug(() => $"NetworkedPluggableObject.ProcessInteractionPacketAsHost() Processing {packet.InteractionType} for NetId: {NetId}"); + //we should never reach this as Yanked is only sent by the server and should be rejected by validation + HeldBy = null; + TrainCarNetId = INVALID_NETID; + SocketIndex = INVALID_SOCKET; + + break; + + case PlugInteractionType.DockHome: + Multiplayer.LogDebug(() => $"NetworkedPluggableObject.ProcessInteractionPacketAsHost() Processing {packet.InteractionType} for NetId: {NetId}"); + HeldBy = null; + TrainCarNetId = INVALID_NETID; + SocketIndex = INVALID_SOCKET; + + break; + + case PlugInteractionType.DockSocket: + Multiplayer.LogDebug(() => $"NetworkedPluggableObject.ProcessInteractionPacketAsHost() Processing {packet.InteractionType} for NetId: {NetId}"); + HeldBy = null; + TrainCarNetId = packet.TrainCarNetId; + SocketIndex = packet.SocketIndex; + + break; + } + + packet.PlayerId = senderPlayer.Id; + + //Allow host to process packet if not from a local client + if (!NetworkLifecycle.Instance.IsClientRunning || (NetworkLifecycle.Instance.IsClientRunning && senderPlayer.Id != NetworkLifecycle.Instance.Server.SelfId)) + { + processingAsHost = true; + ProcessPacket(packet); + } + + //send to all players in active area, except originator + if (Station == null || Station.CullingManager == null || Station.CullingManager.ActivePlayers.Count == 0) + return; + + foreach (var player in Station.CullingManager.ActivePlayers) + { + if (player.Id != senderPlayer.Id) + { + Multiplayer.LogDebug(() => $"NetworkedPluggableObject.ProcessInteractionPacketAsHost() Sending interaction packet to player: {player.Username}"); + NetworkLifecycle.Instance.Server.SendPitStopPlugInteractionPacket(player, packet); + } + } + + } + else + { + //Failed to validate, player needs to rollback interaction + NetworkLifecycle.Instance.Server.SendPitStopPlugInteractionPacket(senderPlayer, new CommonPitStopPlugInteractionPacket + { + NetId = NetId, + InteractionType = PlugInteractionType.Rejected, + }); + } + } + public bool ValidateInteraction(CommonPitStopPlugInteractionPacket packet, ServerPlayer player) { - PlugInteractionType interactionType = (PlugInteractionType)packet.InteractionType; - //todo: implement validation code (player distance, player interacting, etc.) + PlugInteractionType interactionType = packet.InteractionType; - //validate and update - CurrentInteraction = interactionType; + if (interactionType == PlugInteractionType.Rejected || interactionType == PlugInteractionType.Yanked) + { + NetworkLifecycle.Instance.Server.LogWarning($"{player.Username} sent an invalid interaction type ({interactionType})!"); + return false; + } + + //validate ownership of object + if (HeldBy == null && interactionType != PlugInteractionType.PickedUp) + { + NetworkLifecycle.Instance.Server.LogWarning($"{player.Username} attempted to interact with a plug that they are not holding!"); + return false; + } + + //ensure the player is holding the object or no one is holding the object + if (HeldBy != null && HeldBy != player) + { + NetworkLifecycle.Instance.Server.LogWarning($"{player.Username} attempted to interact with a plug that is held by {HeldBy.Username}"); + return false; + } if (interactionType == PlugInteractionType.DockSocket) { - TrainCarNetId = packet.TrainCarNetId; - IsConnectedLeft = packet.IsLeftSide; - HeldBy = null; + + //verify TrainCar + if (packet.TrainCarNetId == 0 || !NetworkedTrainCar.Get(packet.TrainCarNetId, out var networkedTrainCar) || networkedTrainCar == null) + { + Multiplayer.LogDebug(() => $"NetworkedPluggableObject.ValidateInteraction() NetId: {NetId}, trainCarNetId: {packet.TrainCarNetId}, NetworkedTrainCar not found!"); + return false; + } + + //verify TrainCar is in station + if (!(Station?.Station?.pitstop?.carList?.Contains(networkedTrainCar.TrainCar) ?? false)) + { + Multiplayer.LogDebug(() => $"NetworkedPluggableObject.ValidateInteraction() NetId: {NetId}, trainCarNetId: {packet.TrainCarNetId}, Not in Pitstop car list!"); + return false; + } + + //verify socket exists (only locos have sockets) + var socket = GetTrainCarSocket(networkedTrainCar, packet.SocketIndex); + if (socket == null) + { + NetworkLifecycle.Instance.Server.LogWarning($"{player.Username} attempted to insert plug that into a socket that doesn't exist!"); + return false; + } + + //verify socket is compatible + if (!socket.CanAccept(PluggableObject) && socket.Plug != PluggableObject) + { + NetworkLifecycle.Instance.Server.LogWarning($"{player.Username} attempted to dock a {PluggableObject.connectionTag} plug into a {socket.connectionTag} socket, but socket is not compatible!"); + return false; + } + + //verify distance to socket + float sqrDistance = (socket.transform.GetWorldAbsolutePosition() - PluggableObject.transform.GetWorldAbsolutePosition()).sqrMagnitude; + if (sqrDistance > DOCK_SQR_DISTANCE) + { + NetworkLifecycle.Instance.Server.LogWarning($"{player.Username} attempted to dock a plug into {networkedTrainCar.TrainCar.ID}, but socket is too far away!"); + return false; + } } else { - HeldBy = null; if (interactionType == PlugInteractionType.DockHome) { - //todo + //verify distance to socket + var socket = PluggableObject.startAttachedTo; + float sqrDistance = (socket.transform.GetWorldAbsolutePosition() - PluggableObject.transform.GetWorldAbsolutePosition()).sqrMagnitude; + if (sqrDistance > DOCK_SQR_DISTANCE) + { + NetworkLifecycle.Instance.Server.LogWarning($"{player.Username} attempted to dock a plug into the stand, but socket is too far away!"); + return false; + } } else if (interactionType == PlugInteractionType.Dropped) { - //todo + // no verifications required } else if (interactionType == PlugInteractionType.PickedUp) { - HeldBy = player; + float sqrDistance = (player.AbsoluteWorldPosition - PluggableObject.transform.GetWorldAbsolutePosition()).sqrMagnitude; + + Multiplayer.LogDebug(() => $"NetworkedPluggableObject.ValidateInteraction() NetId: {NetId}, {interactionType}, player pos: {player.AbsoluteWorldPosition}, plug pos: {PluggableObject.transform.GetWorldAbsolutePosition()}, sqrDistance: {sqrDistance}, Raycast distance: {GRAB_SQR_DISTANCE}"); + if (sqrDistance > GRAB_SQR_DISTANCE) + { + NetworkLifecycle.Instance.Server.LogWarning($"{player.Username} attempted to interact with a plug that is too far away!"); + return false; + } } } return true; } + public void YankedByRope(Vector3 force, ForceMode mode) + { + Multiplayer.LogDebug(() => $"NetworkedPluggableObject.YankedByRope() [{transform.parent.name}, {NetId}] station: {Station?.StationName}, force: {force}"); + + //cancel any client events + currentInteraction = PlugInteractionType.Rejected; + + HeldBy = null; + TrainCarNetId = INVALID_NETID; + SocketIndex = INVALID_SOCKET; + IsConnecting = false; + + var packet = new CommonPitStopPlugInteractionPacket + { + NetId = NetId, + InteractionType = PlugInteractionType.Yanked, + Position = transform.position - WorldMover.currentMove, + Rotation = transform.rotation, + YankForce = force, + YankMode = mode, + }; + + //Allow host to process packet + processingAsHost = true; + ProcessPacket(packet); + processingAsHost = false; + + //send to all players in active area, except originator and self client + if (Station == null || Station.CullingManager == null || Station.CullingManager.ActivePlayers.Count == 0) + return; + + foreach (var player in Station.CullingManager.ActivePlayers) + NetworkLifecycle.Instance.Server.SendPitStopPlugInteractionPacket(player, packet); + } + + public void SnappedByRope() + { + Multiplayer.LogDebug(() => $"NetworkedPluggableObject.SnappedByRope() [{transform.parent.name}, {NetId}] station: {Station?.StationName}"); + + //cancel any client events + currentInteraction = PlugInteractionType.Rejected; + + HeldBy = null; + TrainCarNetId = INVALID_NETID; + SocketIndex = INVALID_SOCKET; + IsConnecting = false; + + var packet = new CommonPitStopPlugInteractionPacket + { + NetId = NetId, + InteractionType = PlugInteractionType.DockHome, + }; + + //Allow host to process packet + processingAsHost = true; + ProcessPacket(packet); + processingAsHost = false; + + //send to all players in active area, except originator and self client + if (Station == null || Station.CullingManager == null || Station.CullingManager.ActivePlayers.Count == 0) + return; + + foreach (var player in Station.CullingManager.ActivePlayers) + NetworkLifecycle.Instance.Server.SendPitStopPlugInteractionPacket(player, packet); + } + + private void OnPlayerDisconnected(uint disconnectedPlayerId) + { + if (HeldBy == null || HeldBy.Id != disconnectedPlayerId) + return; + + HeldBy = null; + DropPlug(); + + if (Station == null || Station.CullingManager == null || Station.CullingManager.ActivePlayers.Count == 0) + return; + + //cache packet + var packet = new CommonPitStopPlugInteractionPacket + { + NetId = NetId, + InteractionType = PlugInteractionType.Dropped, + }; + + foreach (var player in Station.CullingManager.ActivePlayers) + { + if (player.Id != disconnectedPlayerId && player.Id != NetworkLifecycle.Instance.Server.SelfId) + NetworkLifecycle.Instance.Server.SendPitStopPlugInteractionPacket(player, packet); + } + } + #endregion #region Common public void ProcessPacket(CommonPitStopPlugInteractionPacket packet) { - var interaction = (PlugInteractionType)packet.InteractionType; - ProcessInteraction(interaction, packet.PlayerId, packet.TrainCarNetId, packet.IsLeftSide, packet.Position, packet.Rotation); + ProcessInteraction(packet.InteractionType, packet.PlayerId, packet.TrainCarNetId, packet.SocketIndex, packet.Position, packet.Rotation, packet.YankForce, packet.YankMode); } public void ProcessBulkUpdate(PitStopPlugData data) @@ -156,16 +464,22 @@ private IEnumerator WaitForInit(PitStopPlugData data) Multiplayer.LogDebug(() => $"NetworkedPluggableObject.WaitForInit() netId: {NetId} Complete"); var interaction = data.State; - ProcessInteraction(interaction, data.PlayerId, data.TrainCarNetId, data.IsLeftSide, data.Position, data.Rotation); + ProcessInteraction(interaction, data.PlayerId, data.TrainCarNetId, data.SocketIndex, data.Position, data.Rotation); + + //wait 1 frame for plugs that are docking + yield return null; + //clear the docking flag + if (interaction == PlugInteractionType.DockSocket || interaction == PlugInteractionType.DockHome) + IsConnecting = false; + + //allow the player to interact Refreshed = true; } - public void ProcessInteraction(PlugInteractionType interaction, byte playerId, ushort trainNetId, bool isLeftSide, Vector3? newPosition, Quaternion? newRotation) + public void ProcessInteraction(PlugInteractionType interaction, byte playerId, ushort trainNetId, sbyte socketIndex, Vector3? newPosition, Quaternion? newRotation, Vector3? yankForce = null, ForceMode yankMode = ForceMode.Impulse) { bool result; - Multiplayer.LogDebug(() => $"NetworkedPluggableObject.ProcessInteraction({interaction}, {playerId}, {trainNetId}, {isLeftSide}, {newPosition?.ToString()}, {newRotation?.ToString()}) netId: {NetId}"); - - NetworkedPlayer player = null; + Multiplayer.LogDebug(() => $"NetworkedPluggableObject.ProcessInteraction({interaction}, {playerId}, {trainNetId}, {socketIndex}, {newPosition?.ToString()}, {newRotation?.ToString()}, {yankForce}, {yankMode}) netId: {NetId}"); switch (interaction) { @@ -175,42 +489,16 @@ public void ProcessInteraction(PlugInteractionType interaction, byte playerId, u case PlugInteractionType.PickedUp: //Handle the picked up state - isGrabbed = true; - playerHolding = playerId; - PluggableObject.controlGrabbed = true; - BlockInteraction(true); - - PluggableObject.Unplug(); - Multiplayer.LogDebug(() => $"ProcessPacket() NetId: {NetId}, Picked Up, player: {playerHolding}"); - //attach to a player - if (NetworkLifecycle.Instance.IsClientRunning && - NetworkLifecycle.Instance.Client.ClientPlayerManager.TryGetPlayer(playerHolding, out player)) - { - var target = grabHandler?.customGrabAnchor?.GetGrabAnchor(); - player.HoldItem(gameObject, target?.localPos, target?.localRot); - } + GrabPlug(playerId); + break; case PlugInteractionType.Dropped: Multiplayer.LogDebug(() => $"ProcessPacket() NetId: {NetId}, Dropped"); - if (isGrabbed) - { - if (NetworkLifecycle.Instance.IsClientRunning && - NetworkLifecycle.Instance.Client.ClientPlayerManager.TryGetPlayer(playerHolding, out player)) - { - player.DropItem(); - } - } - - isGrabbed = false; - playerHolding = 0; - PluggableObject.controlGrabbed = false; - BlockInteraction(false); - - PluggableObject.Unplug(); + DropPlug(); if (newPosition == null || newRotation == null) return; @@ -220,72 +508,43 @@ public void ProcessInteraction(PlugInteractionType interaction, byte playerId, u break; - case PlugInteractionType.DockHome: - Multiplayer.LogDebug(() => $"ProcessPacket() NetId: {NetId}, DockHome"); + case PlugInteractionType.Yanked: + Multiplayer.LogDebug(() => $"ProcessPacket() NetId: {NetId}, Yanked"); + + DropPlug(); - if (isGrabbed) + if (newPosition != null || newRotation != null) { - if (NetworkLifecycle.Instance.IsClientRunning && - NetworkLifecycle.Instance.Client.ClientPlayerManager.TryGetPlayer(playerHolding, out player)) - { - player.DropItem(); - } + transform.position = (Vector3)newPosition + WorldMover.currentMove; + transform.rotation = (Quaternion)newRotation; } - isGrabbed = false; - playerHolding = 0; - PluggableObject.controlGrabbed = false; - BlockInteraction(false); + PlugRB?.AddForce((Vector3)yankForce, yankMode); + + CoroutineManager.Instance.StartCoroutine(WaitForYankSettle()); + + break; + + case PlugInteractionType.DockHome: + Multiplayer.LogDebug(() => $"ProcessPacket() NetId: {NetId}, DockHome"); - PluggableObject.Unplug(); + DropPlug(); result = PluggableObject.InstantSnapTo(PluggableObject.startAttachedTo); Multiplayer.LogDebug(() => $"ProcessPacket() NetId: {NetId}, DockHome, result: {result}"); break; case PlugInteractionType.DockSocket: - Multiplayer.LogDebug(() => $"ProcessPacket() NetId: {NetId}, DockSocket, trainCar: {trainNetId}, isLeft: {isLeftSide}"); + Multiplayer.LogDebug(() => $"ProcessPacket() NetId: {NetId}, DockSocket, trainCar: {trainNetId}, isLeft: {socketIndex}"); - if (isGrabbed) - { - if (NetworkLifecycle.Instance.IsClientRunning && - NetworkLifecycle.Instance.Client.ClientPlayerManager.TryGetPlayer(playerHolding, out player)) - { - player?.DropItem(); - } - } - - if (NetworkedTrainCar.GetTrainCar(trainNetId, out var trainCar)) - { - isGrabbed = false; - playerHolding = 0; - PluggableObject.controlGrabbed = false; - BlockInteraction(false); - - PluggableObject.Unplug(); - - var sockets = trainCar.GetComponentsInChildren(); - if (isLeftSide) - { - result = PluggableObject.InstantSnapTo(sockets[0]); - Multiplayer.LogDebug(() => $"ProcessPacket() NetId: {NetId}, DockSocket, trainCar: {trainNetId}, isLeft: {isLeftSide}, result: {result}"); - } - else - { - result = PluggableObject.InstantSnapTo(sockets[1]); - Multiplayer.LogDebug(() => $"ProcessPacket() NetId: {NetId}, DockSocket, trainCar: {trainNetId}, isLeft: {isLeftSide}, result: {result}"); - } - } - else - { - Multiplayer.LogDebug(() => $"ProcessPacket() NetId: {NetId}, DockSocket, trainCar: {trainNetId}. TrainCar not found!"); - } + DockTrainCar(trainNetId, socketIndex); break; } } private void BlockInteraction(bool block) { + Multiplayer.LogDebug(() => $"BlockInteraction({block})"); if (block) { PluggableObject.DisableStandaloneComponents(); @@ -311,29 +570,146 @@ public void InitPitStop(NetworkedPitStopStation netPitStop) Station = netPitStop; plugToStation.Add(this, netPitStop); + + if (PluggableObject == null) + PluggableObject = GetComponent(); + + if (PluggableObject != null) + plugToNetworkedPluggable.Add(PluggableObject, this); + } + + public void DropPlug() + { + if (playerHolding != 0) + { + if (NetworkLifecycle.Instance.IsClientRunning && + NetworkLifecycle.Instance.Client.ClientPlayerManager.TryGetPlayer(playerHolding, out var player)) + { + player.DropItem(); + } + } + + playerHolding = 0; + PluggableObject.controlGrabbed = false; + BlockInteraction(false); + + PluggableObject.Unplug(); + PluggableObject.controlBase?.ForceEndInteraction(); + + TrainCarNetId = INVALID_NETID; + SocketIndex = INVALID_SOCKET; + } + + public void GrabPlug(byte playerId) + { + playerHolding = playerId; + PluggableObject.controlGrabbed = true; + BlockInteraction(true); + + PluggableObject.Unplug(); + + TrainCarNetId = INVALID_NETID; + SocketIndex = INVALID_SOCKET; + + //attach to a player + if (NetworkLifecycle.Instance.IsClientRunning && + NetworkLifecycle.Instance.Client.ClientPlayerManager.TryGetPlayer(playerHolding, out var player)) + { + var target = grabHandler?.customGrabAnchor?.GetGrabAnchor(); + player.HoldItem(gameObject, target?.localPos, target?.localRot); + } + } + + private void DockTrainCar(ushort trainNetId, sbyte socketIndex) + { + DropPlug(); + + if (NetworkedTrainCar.Get(trainNetId, out var netTrainCar)) + { + var socket = GetTrainCarSocket(netTrainCar, socketIndex); + if (socket == null) + { + Multiplayer.LogWarning($"Failed to dock plug in loco socket, socket not found! Plug NetId: {NetId}, TrainCar: [{netTrainCar.CurrentID}, {trainNetId}]"); + return; + } + + bool result = PluggableObject.InstantSnapTo(socket); + + if (result) + { + TrainCarNetId = trainNetId; + SocketIndex = socketIndex; + } + + Multiplayer.LogDebug(() => $"ProcessPacket() NetId: {NetId}, DockSocket, trainCar: {trainNetId}, isLeft: {socketIndex}, result: {result}"); + } + else + { + Multiplayer.LogDebug(() => $"ProcessPacket() NetId: {NetId}, DockSocket, trainCar: {trainNetId}. TrainCar not found!"); + } + } + + public PlugSocket GetTrainCarSocket(NetworkedTrainCar netTrainCar, sbyte socketIndex) + { + if (netTrainCar == null || netTrainCar.TrainCar == null) + return null; + + if (socketIndex < 0 || socketIndex >= netTrainCar.TrainCar.FuelSockets.Length) + { + Multiplayer.LogWarning($"Failed to find socket {socketIndex} in TrainCar: [{netTrainCar.CurrentID}, {netTrainCar.NetId}], index is out of bounds!"); + return null; + } + + return netTrainCar.TrainCar.FuelSockets[socketIndex]; + } + + private IEnumerator WaitForYankSettle() + { + Multiplayer.LogDebug(()=>$"WaitForYankSettle() PluggableObject.yankOutOfHand: {PluggableObject.yankOutOfHand}, velocity: {PlugRB.velocity.sqrMagnitude}"); + PluggableObject.yankOutOfHand = false; //block docking + + //allow force to be applied + yield return new WaitForFixedUpdate(); + Multiplayer.LogDebug(()=>$"WaitForYankSettle() post-WaitForFixed, PluggableObject.yankOutOfHand: {PluggableObject.yankOutOfHand}, velocity: {PlugRB.velocity.sqrMagnitude}"); + + float time = Time.time; + + //wait for rigid body to come to rest + yield return new WaitUntil(() => Mathf.Approximately(PlugRB.velocity.sqrMagnitude, 0.0f) || (Time.time - time > 2.0f)); + + Multiplayer.LogDebug(() => $"WaitForYankSettle() PluggableObject.yankOutOfHand: {PluggableObject.yankOutOfHand}, velocity: {PlugRB.velocity.sqrMagnitude}, delta Time: {Time.time - time}"); + + //wait for plug to come to rest (prevent docking home) + yield return new WaitForFixedUpdate(); + + PluggableObject.yankOutOfHand = true; //unblock docking } #endregion #region Client private void OnGrabbed(ControlImplBase control) { - Multiplayer.LogDebug(() => $"NetworkedPluggableObject.OnGrabbed() pre [{transform.parent.name}, {NetId}] station: {Station?.StationName}"); + Multiplayer.LogDebug(() => $"NetworkedPluggableObject.OnGrabbed() station: {Station?.StationName}, processing: {NetworkLifecycle.Instance.IsProcessingPacket}, processing as Host: {processingAsHost}, refreshed: {Refreshed}, isConnecting: {IsConnecting}"); if (NetworkLifecycle.Instance.IsProcessingPacket) return; - Multiplayer.LogDebug(() => $"NetworkedPluggableObject.OnGrabbed() post [{transform.parent.name}, {NetId}] station: {Station?.StationName}"); + //Multiplayer.LogDebug(() => $"NetworkedPluggableObject.OnGrabbed() post [{transform.parent.name}, {NetId}] station: {Station?.StationName}"); //Prevent new players/players entering the area from sending packets until initalised if (!Refreshed) return; - Multiplayer.LogDebug(() => $"NetworkedPluggableObject.OnGrabbed() [{transform.parent.name}, {NetId}] station: {Station?.StationName}"); - NetworkLifecycle.Instance.Client?.SendPitStopPlugInteractionPacket(NetId, PlugInteractionType.PickedUp); + TrainCarNetId = INVALID_NETID; + SocketIndex = INVALID_SOCKET; + + currentInteraction = PlugInteractionType.PickedUp; } private void OnUngrabbed(ControlImplBase control) { + Multiplayer.LogDebug(() => $"NetworkedPluggableObject.OnUngrabbed() station: {Station?.StationName}, processing: {NetworkLifecycle.Instance.IsProcessingPacket}, processing as Host: {processingAsHost}, refreshed: {Refreshed}, isConnecting: {IsConnecting}"); + if (NetworkLifecycle.Instance.IsProcessingPacket) return; @@ -341,12 +717,22 @@ private void OnUngrabbed(ControlImplBase control) if (!Refreshed) return; - Multiplayer.LogDebug(() => $"NetworkedPluggableObject.OnUngrabbed() [{transform.parent.name}, {NetId}] station: {Station?.StationName}"); - NetworkLifecycle.Instance.Client?.SendPitStopPlugInteractionPacket(NetId, PlugInteractionType.Dropped, transform.position - WorldMover.currentMove, transform.rotation); + //Multiplayer.LogDebug(() => $"NetworkedPluggableObject.OnUngrabbed() station: {Station?.StationName}, plugging state: {PluggableObject.State}"); + + // If we're snapping to a socket, don't send the Dropped packet + if (IsConnecting) + return; + + TrainCarNetId = INVALID_NETID; + SocketIndex = INVALID_SOCKET; + + currentInteraction = PlugInteractionType.Dropped; } - private void OnPlugged(PluggableObject plug, PlugSocket socket) + private void OnPluggedIn(PluggableObject plug, PlugSocket socket) { + Multiplayer.LogDebug(() => $"NetworkedPluggableObject.OnPlugged() station: {Station?.StationName}, processing: {NetworkLifecycle.Instance.IsProcessingPacket}, processing as Host: {processingAsHost}, refreshed: {Refreshed}, isConnecting: {IsConnecting}"); + if (NetworkLifecycle.Instance.IsProcessingPacket) return; @@ -357,15 +743,17 @@ private void OnPlugged(PluggableObject plug, PlugSocket socket) Multiplayer.LogDebug(() => $"NetworkedPluggableObject.OnPlugged() [{transform.parent.name}, {NetId}] station: {Station?.StationName}"); PlugInteractionType interaction; - bool left = false; ushort carNetId = 0; if (socket == plug.startAttachedTo) + { interaction = PlugInteractionType.DockHome; + SocketIndex = INVALID_SOCKET; + } else { var trainCar = TrainCar.Resolve(socket.gameObject); - if (trainCar != null) + if (trainCar != null && trainCar.FuelSockets != null) { if (!NetworkedTrainCar.TryGetFromTrainCar(trainCar, out var netTrainCar)) { @@ -376,9 +764,10 @@ private void OnPlugged(PluggableObject plug, PlugSocket socket) carNetId = netTrainCar.NetId; interaction = PlugInteractionType.DockSocket; - var sockets = trainCar.GetComponentsInChildren(); - if (socket == sockets[0]) - left = true; + SocketIndex = (sbyte)Array.IndexOf(trainCar.FuelSockets, socket); + + if (SocketIndex < 0) + Multiplayer.LogWarning(() => $"Socket not recognised for TrainCar [{trainCar.ID}, {netTrainCar.NetId}], socket: {socket.GetObjectPath()}"); } else { @@ -387,7 +776,9 @@ private void OnPlugged(PluggableObject plug, PlugSocket socket) } } - NetworkLifecycle.Instance.Client?.SendPitStopPlugInteractionPacket(NetId, interaction, trainCarNetId: carNetId, isConnectedLeft: left); + currentInteraction = interaction; + TrainCarNetId = carNetId; + IsConnecting = false; } #endregion } diff --git a/Multiplayer/Networking/Data/PitStopPlugData.cs b/Multiplayer/Networking/Data/PitStopPlugData.cs index e82e6cae..320c73b3 100644 --- a/Multiplayer/Networking/Data/PitStopPlugData.cs +++ b/Multiplayer/Networking/Data/PitStopPlugData.cs @@ -6,25 +6,28 @@ namespace Multiplayer.Networking.Data; -public readonly struct PitStopPlugData(ushort netId, PlugInteractionType state, byte playerId, ushort trainCarNetId, bool isLeft, Vector3 pos, Quaternion rot) +public readonly struct PitStopPlugData(ushort netId, PlugInteractionType state, byte playerId, ushort trainCarNetId, sbyte socketIndex, Vector3 pos, Quaternion rot) { public readonly ushort NetId = netId; public readonly byte PlayerId = playerId; public readonly PlugInteractionType State = state; public readonly ushort TrainCarNetId = trainCarNetId; - public readonly bool IsLeftSide = isLeft; + public readonly sbyte SocketIndex = socketIndex; public readonly Vector3 Position = pos; public readonly Quaternion Rotation = rot; public static PitStopPlugData From(NetworkedPluggableObject plugData, bool bulk = false) { + var interaction = GetInteractionType(plugData, bulk); + + Multiplayer.LogDebug(() => $"PitStopPlugData.From() NetId: {plugData.NetId}, Interaction: {interaction}"); return new PitStopPlugData ( plugData.NetId, - GetInteractionType(plugData, bulk), + interaction, plugData.HeldBy?.Id ?? 0, plugData.TrainCarNetId, - plugData.IsConnectedLeft, + plugData.SocketIndex, plugData.transform.GetWorldAbsolutePosition(), plugData.transform.rotation ); @@ -52,7 +55,7 @@ public static void Serialize(NetDataWriter writer, PitStopPlugData data) break; case PlugInteractionType.DockSocket: writer.Put(data.TrainCarNetId); - writer.Put(data.IsLeftSide); + writer.Put(data.SocketIndex); break; } } @@ -63,7 +66,7 @@ public static PitStopPlugData Deserialize(NetDataReader reader) PlugInteractionType state = (PlugInteractionType)reader.GetByte(); byte playerId = 0; ushort trainCarNetId = 0; - bool isLeft = false; + sbyte socketIndex = -1; Vector3 pos = Vector3.zero; Quaternion rot = Quaternion.identity; @@ -84,7 +87,7 @@ public static PitStopPlugData Deserialize(NetDataReader reader) break; case PlugInteractionType.DockSocket: trainCarNetId = reader.GetUShort(); - isLeft = reader.GetBool(); + socketIndex = reader.GetSByte(); break; } @@ -94,26 +97,30 @@ public static PitStopPlugData Deserialize(NetDataReader reader) state, playerId, trainCarNetId, - isLeft, + socketIndex, pos, rot ); } - private static PlugInteractionType GetInteractionType(NetworkedPluggableObject plugData, bool bulk) + private static PlugInteractionType GetInteractionType(NetworkedPluggableObject netPlug, bool bulk) { - if (!bulk) - return plugData.CurrentInteraction; + Multiplayer.LogDebug(() => $"NetworkedPluggableObject.GetInteractionType() netId: {netPlug.NetId} bulk: {bulk}, Heldby:{netPlug.HeldBy}, TrainCarNetId: {netPlug.TrainCarNetId} socket not null: {netPlug.PluggableObject.Socket != null}, socket path: {netPlug.PluggableObject.Socket?.GetObjectPath()}, start attached to not null: {netPlug.PluggableObject.startAttachedTo != null}, start attached to path: {netPlug.PluggableObject.startAttachedTo?.GetObjectPath()}"); + //if (!bulk) + // return plugData.CurrentInteraction; - if (plugData.HeldBy != null) + if (netPlug.HeldBy != null) return PlugInteractionType.PickedUp; - if (plugData.TrainCarNetId != 0) - return PlugInteractionType.DockSocket; + if (netPlug.PluggableObject.Socket == null) + return PlugInteractionType.Dropped; - if (plugData.PluggableObject.Socket == plugData.PluggableObject.startAttachedTo) + if (netPlug.PluggableObject.Socket == netPlug.PluggableObject.startAttachedTo) return PlugInteractionType.DockHome; - return PlugInteractionType.Dropped; + if (netPlug.TrainCarNetId != 0) + return PlugInteractionType.DockSocket; + + return PlugInteractionType.Rejected; } } diff --git a/Multiplayer/Networking/Data/PlugInteractionType.cs b/Multiplayer/Networking/Data/PlugInteractionType.cs index be0c7627..db4417d1 100644 --- a/Multiplayer/Networking/Data/PlugInteractionType.cs +++ b/Multiplayer/Networking/Data/PlugInteractionType.cs @@ -6,6 +6,7 @@ public enum PlugInteractionType : byte Rejected, PickedUp, Dropped, + Yanked, DockHome, DockSocket } diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index f81493dd..5abb2a9c 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -1438,16 +1438,24 @@ public void SendPitStopInteractionPacket(ushort netId, PitStopStationInteraction }, DeliveryMethod.ReliableOrdered); } - public void SendPitStopPlugInteractionPacket(ushort netId, PlugInteractionType interaction, Vector3? position = null, Quaternion? rotation = null, ushort trainCarNetId = 0, bool isConnectedLeft = false) - { - LogDebug(()=>$"SendPitStopInteractionPacket({netId}, {interaction}, {position}, {rotation}, {trainCarNetId}, {isConnectedLeft})"); + public void SendPitStopPlugInteractionPacket + ( + ushort netId, + PlugInteractionType interaction, + Vector3? position = null, + Quaternion? rotation = null, + ushort trainCarNetId = 0, + sbyte socketIndex = -1 + ) + { + LogDebug(()=>$"SendPitStopInteractionPacket({netId}, {interaction}, pos: {position}, rot: {rotation}, trainNetId: {trainCarNetId}, socketIndex: {socketIndex})"); SendNetSerializablePacketToServer(new CommonPitStopPlugInteractionPacket { NetId = netId, InteractionType = interaction, TrainCarNetId = trainCarNetId, - IsLeftSide = isConnectedLeft, + SocketIndex = socketIndex, Position = position, Rotation = rotation, diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index 384dd073..985cadcd 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -625,6 +625,12 @@ public void SendPitStopInteractionPacket(ITransportPeer peer, CommonPitStopInter SendPacket(peer, packet, DeliveryMethod.ReliableOrdered); } + public void SendPitStopPlugInteractionPacket(ServerPlayer player, CommonPitStopPlugInteractionPacket packet) + { + LogDebug(() => $"SendPitStopPlugInteractionPacket({packet.NetId}, {packet.InteractionType}, {packet.PlayerId}, {packet.Position}, {packet.Rotation}, {packet.TrainCarNetId}, {packet.SocketIndex}, {packet.YankForce}, {packet.YankMode})"); + SendNetSerializablePacket(player.Peer, packet, DeliveryMethod.ReliableOrdered); + } + public void SendCashRegisterAction(CommonCashRegisterWithModulesActionPacket packet, ITransportPeer peer = null) { if (peer == null) diff --git a/Multiplayer/Networking/Packets/Common/CommonPitStopPlugInteractionPacket.cs b/Multiplayer/Networking/Packets/Common/CommonPitStopPlugInteractionPacket.cs index 2a2dfe91..5d13ca4a 100644 --- a/Multiplayer/Networking/Packets/Common/CommonPitStopPlugInteractionPacket.cs +++ b/Multiplayer/Networking/Packets/Common/CommonPitStopPlugInteractionPacket.cs @@ -12,9 +12,11 @@ public class CommonPitStopPlugInteractionPacket : INetSerializable public PlugInteractionType InteractionType { get; set; } public byte PlayerId { get; set; } public ushort TrainCarNetId { get; set; } - public bool IsLeftSide { get; set; } + public sbyte SocketIndex { get; set; } public Vector3? Position { get; set; } public Quaternion? Rotation { get; set; } + public Vector3? YankForce { get; set; } + public ForceMode YankMode { get; set; } public void Deserialize(NetDataReader reader) { @@ -35,12 +37,20 @@ public void Deserialize(NetDataReader reader) Rotation = QuaternionSerializer.Deserialize(reader); break; + case PlugInteractionType.Yanked: + Position = Vector3Serializer.Deserialize(reader); + Rotation = QuaternionSerializer.Deserialize(reader); + + YankForce = Vector3Serializer.Deserialize(reader); + YankMode = (ForceMode)reader.GetByte(); + break; + case PlugInteractionType.DockHome: break; case PlugInteractionType.DockSocket: TrainCarNetId = reader.GetUShort(); - IsLeftSide = reader.GetBool(); + SocketIndex = reader.GetSByte(); break; } } @@ -64,12 +74,20 @@ public void Serialize(NetDataWriter writer) QuaternionSerializer.Serialize(writer, Rotation ?? Quaternion.identity); break; + case PlugInteractionType.Yanked: + Vector3Serializer.Serialize(writer, Position ?? Vector3.zero); + QuaternionSerializer.Serialize(writer, Rotation ?? Quaternion.identity); + + Vector3Serializer.Serialize(writer, YankForce ?? Vector3.zero); + writer.Put((byte)YankMode); + break; + case PlugInteractionType.DockHome: break; case PlugInteractionType.DockSocket: writer.Put(TrainCarNetId); - writer.Put(IsLeftSide); + writer.Put(SocketIndex); break; } } diff --git a/Multiplayer/Patches/World/PluggableObjectPatch.cs b/Multiplayer/Patches/World/PluggableObjectPatch.cs index 006aed9d..102c0f10 100644 --- a/Multiplayer/Patches/World/PluggableObjectPatch.cs +++ b/Multiplayer/Patches/World/PluggableObjectPatch.cs @@ -1,5 +1,8 @@ using HarmonyLib; using Multiplayer.Components.Networking; +using Multiplayer.Components.Networking.World; +using Multiplayer.Utils; +using System; namespace Multiplayer.Patches.World; @@ -18,10 +21,39 @@ public static bool Awake(PluggableObject __instance) return false; } - [HarmonyPatch(nameof(PluggableObject.InstantSnapTo))] + [HarmonyPatch(nameof(PluggableObject.IsHeldInHand), MethodType.Getter)] [HarmonyPrefix] - public static bool InstantSnapTo(PluggableObject __instance) + public static bool IsHeldInHand(PluggableObject __instance, ref bool __result) { + var result = __result; + Multiplayer.LogDebug(() => $"IsHeldInHand({result})"); + + if (NetworkedPluggableObject.Get(__instance, out var networkedPluggableObject)) + __result = networkedPluggableObject.IsHeld; + else + __result = __instance.controlGrabbed; + + result = __result; + Multiplayer.LogDebug(() => $"IsHeldInHand() result: {result}, net found: {networkedPluggableObject != null}"); + return false; } + + [HarmonyPatch(nameof(PluggableObject.ConnectingRoutine))] + [HarmonyPrefix] + public static void ConnectingRoutine(PluggableObject __instance) + { + Multiplayer.LogDebug(() => $"ConnectingRoutine()"); + if (NetworkedPluggableObject.Get(__instance, out var networkedPluggableObject)) + { + networkedPluggableObject.IsConnecting = true; + } + } + + //[HarmonyPatch(typeof(PluggableObject), nameof(PluggableObject.InstantSnapTo))] + //[HarmonyPrefix] + //public static void InstantSnapTo_Prefix(PluggableObject __instance, PlugSocket socket) + //{ + // Multiplayer.LogDebug(() => $"PluggableObject.InstantSnapTo() called: {__instance.GetObjectPath()} -> {socket.GetObjectPath()}\r\n {Environment.StackTrace}"); + //} } diff --git a/Multiplayer/Patches/World/PropHosePatch.cs b/Multiplayer/Patches/World/PropHosePatch.cs new file mode 100644 index 00000000..8ee22e33 --- /dev/null +++ b/Multiplayer/Patches/World/PropHosePatch.cs @@ -0,0 +1,212 @@ +using HarmonyLib; +using Multiplayer.Components.Networking; +using Multiplayer.Components.Networking.World; +using Multiplayer.Utils; +using System.Collections.Generic; +using System.Reflection; +using System.Reflection.Emit; +using UnityEngine; + +namespace Multiplayer.Patches.World; + +[HarmonyPatch(typeof(PropHose))] +public static class PropHosePatch +{ + static readonly CodeInstruction targetUnplugMethod = CodeInstruction.Call(typeof(PluggableObject), nameof(PluggableObject.Unplug), [], null); + static readonly CodeInstruction override_UnplugMethod = CodeInstruction.Call(typeof(PropHosePatch), nameof(PropHosePatch.Override_Unplug), [typeof(PluggableObject)], null); + + static readonly CodeInstruction targetYankOutOfHandMethod = CodeInstruction.Call(typeof(PluggableObject), nameof(PluggableObject.YankOutOfHand), [], null); + static readonly CodeInstruction override_YankOutOfHandMethod = CodeInstruction.Call(typeof(PropHosePatch), nameof(PropHosePatch.Override_YankOutOfHand), [typeof(PluggableObject)], null); + + static readonly CodeInstruction targetAddForceMethod = CodeInstruction.Call(typeof(Rigidbody), nameof(Rigidbody.AddForce), [typeof(Vector3), typeof(ForceMode)], null); + static readonly CodeInstruction override_AddForceMethod = CodeInstruction.Call(typeof(PropHosePatch), nameof(PropHosePatch.Override_AddForce), [typeof(Rigidbody), typeof(Vector3), typeof(ForceMode), typeof(PluggableObject)], null); + + static readonly CodeInstruction targetInstantSnapToMethod = CodeInstruction.Call(typeof(PluggableObject), nameof(PluggableObject.InstantSnapTo), [typeof(PlugSocket)], null); + static readonly CodeInstruction override_InstantSnapToMethod = CodeInstruction.Call(typeof(PropHosePatch), nameof(PropHosePatch.Override_InstantSnapTo), [typeof(PluggableObject), typeof(PlugSocket)], null); + + private static readonly string UnplugOperand = targetUnplugMethod.operand?.ToString(); + private static readonly string YankOutOfHandOperand = targetYankOutOfHandMethod.operand?.ToString(); + private static readonly string AddForceOperand = targetAddForceMethod.operand?.ToString(); + private static readonly string InstantSnapToOperand = targetInstantSnapToMethod.operand?.ToString(); + + [HarmonyPatch(nameof(PropHose.OnEnable))] + [HarmonyPrefix] + public static bool OnEnable() + { + if (NetworkLifecycle.Instance.IsHost()) + return true; + + //prevent client from snapping manual service plug to home position + return false; + } + + [HarmonyPatch(nameof(PropHose.Update))] + [HarmonyTranspiler] + public static IEnumerable Update(IEnumerable instructions) + { + var newCode = new List(); + + //Multiplayer.LogDebug(() => + //{ + // var code = new List(instructions); + + // StringBuilder sb = new StringBuilder(); + // sb.AppendLine("Starting transpiler PropHose.Update"); + // sb.AppendLine("IL Before:"); + // for (int i = 0; i < code.Count; i++) + // sb.AppendLine($"{i:D4}: {code[i]}"); + // return sb.ToString(); + //}); + + + foreach (CodeInstruction instruction in instructions) + { + + if (instruction.opcode == OpCodes.Callvirt) + { + string operand = instruction.operand?.ToString(); + + if (operand == UnplugOperand) + { + //We are switching from a 'CallVirt' to a 'Call'. + //we already have a reference to PluggableObject on the stack as `this.plug` (first param of CallVirt) + //the next item on the stack is the PlugSocket + + //Multiplayer.LogDebug(() => $"PropHose.Update() {instruction}, replacing: {override_UnplugMethod}"); + + //call our override method + newCode.Add(override_UnplugMethod); + + } + else if (operand == YankOutOfHandOperand) + { + //We are switching from a 'CallVirt' to a 'Call'. + //we already have a reference to PluggableObject on the stack as `this.plug` (first param of CallVirt) + //the next item on the stack is the PlugSocket + + //Multiplayer.LogDebug(() => $"PropHose.Update() {instruction}, replacing: {override_YankOutOfHandMethod}"); + + //call our override method + newCode.Add(override_YankOutOfHandMethod); + } + else if (operand == AddForceOperand) + { + //We are switching from a 'CallVirt' to a 'Call'. + //we already have a reference to rb on the stack as `this.plugBody` (first param of CallVirt) + //the next item on the stack is the force, then the mode + //we will manually add the plug instance + + //Multiplayer.LogDebug(() => $"PropHose.Update() {instruction}, replacing: {override_AddForceMethod}"); + + //load instance/"this" to the stack + newCode.Add(new CodeInstruction(OpCodes.Ldarg_0)); + //load PropHose.plug reference on to the stack ("this.plug") + newCode.Add(new CodeInstruction(OpCodes.Ldfld, typeof(PropHose).GetField(nameof(PropHose.plug), BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance))); + //call our override method + newCode.Add(override_AddForceMethod); + + } + else if (operand == InstantSnapToOperand) + { + //We are switching from a 'CallVirt' to a 'Call'. + //we already have a reference to PluggableObject on the stack as `this.plug` (first param of CallVirt) + //the next item on the stack is the PlugSocket + + //Multiplayer.LogDebug(() => $"PropHose.Update() {instruction}, replacing: {override_InstantSnapToMethod}"); + + //call our override method + newCode.Add(override_InstantSnapToMethod); + } + else + { + //Multiplayer.LogDebug(() => $"PropHose.Update() {instruction}"); + newCode.Add(instruction); + } + } + else + { + //Multiplayer.LogDebug(() => $"PropHose.Update() {instruction}"); + newCode.Add(instruction); + } + } + + //Multiplayer.LogDebug(() => + //{ + // StringBuilder sb = new StringBuilder(); + // sb.AppendLine("IL After:"); + // for (int i = 0; i < newCode.Count; i++) + // sb.AppendLine($"{i:D4}: {newCode[i]}"); + + // return sb.ToString(); + //}); + + return newCode; + } + + private static void Override_Unplug(PluggableObject instance) + { + Multiplayer.LogDebug(() => $"Override_Unplug({instance.GetObjectPath()})"); + + if (!NetworkLifecycle.Instance.IsHost()) + return; + + Multiplayer.LogDebug(() => $"Override_Unplug({instance.GetObjectPath()}) Unplugging"); + instance.Unplug(); + } + + private static bool Override_YankOutOfHand(PluggableObject instance) + { + Multiplayer.LogDebug(() => $"Override_YankOutOfHand({instance.GetObjectPath()})"); + + if (!NetworkLifecycle.Instance.IsHost()) + return false; // result is unused by Update(), we can return true or false + + if (NetworkedPluggableObject.Get(instance, out var netPlug)) + netPlug.DropPlug(); + + Multiplayer.LogDebug(() => $"Override_YankOutOfHand({instance.GetObjectPath()}) Yanking"); + return instance.YankOutOfHand(); + } + + private static void Override_AddForce(Rigidbody rb, Vector3 force, ForceMode mode, PluggableObject instance) + { + Multiplayer.LogDebug(() => $"Override_AddForce() station: {instance.GetObjectPath()}, force: {force}, mode: {mode}"); + + if (!NetworkLifecycle.Instance.IsHost()) + return; + + if (NetworkedPluggableObject.Get(instance, out var netPlug)) + { + Multiplayer.LogDebug(() => $"Override_AddForce() station: {netPlug.Station.StationName}, force: {force}, mode: {mode}"); + + //The force will be applied when the packet is processed as the host + //rb.AddForce(force, mode); + netPlug.YankedByRope(force, mode); + } + } + + private static bool Override_InstantSnapTo(PluggableObject instance, PlugSocket socket) + { + Multiplayer.LogDebug(() => $"Override_InstantSnapTo({instance.GetObjectPath()}, {socket.GetObjectPath()}) instance.yankOutOfHand: {instance.yankOutOfHand}"); + + if (!NetworkLifecycle.Instance.IsHost()) + return false; // result is unused by Update(), we can return true or false + + if(!instance.yankOutOfHand) + { + Multiplayer.LogDebug(() => $"Override_InstantSnapTo({instance.GetObjectPath()}, {socket.GetObjectPath()}) Blocked by yank settlement"); + return false; + } + + Multiplayer.LogDebug(() => $"Override_InstantSnapTo({instance.GetObjectPath()}, {socket.GetObjectPath()}) Snapping"); + + if (NetworkedPluggableObject.Get(instance, out var netPlug)) + { + netPlug.SnappedByRope(); + return false; + } + + // no player holding, we can allow the snap + return instance.InstantSnapTo(socket); + } +} From 716de8d60c6df0d27721a3dad0e0382ff1606097 Mon Sep 17 00:00:00 2001 From: AMacro Date: Mon, 7 Jul 2025 21:46:53 +1000 Subject: [PATCH 413/521] General cleanup --- .../World/NetworkedPitStopStation.cs | 12 +++-- .../Managers/Client/NetworkClient.cs | 4 +- .../Managers/Server/NetworkServer.cs | 45 +++++++++---------- 3 files changed, 33 insertions(+), 28 deletions(-) diff --git a/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs b/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs index 58879f94..aeb98394 100644 --- a/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs +++ b/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs @@ -70,7 +70,7 @@ public static void InitialisePitStops() const float MAX_DELTA = 0.2f; const float MIN_UPDATE_TIME = 0.1f; - const float LOADING_TIMEOUT = 10f; + const float LOADING_TIMEOUT = 5f; const float ROTATION_SMOOTH_SPEED = 5f; const float FAUCET_SNAP_THRESHOLD = 0.005f; @@ -146,6 +146,9 @@ protected override void Awake() CullingManager.PlayerEnteredCullingRegion += OnPlayerEnteredCullingRegion; NetworkLifecycle.Instance.OnTick += OnTick; + + NetworkLifecycle.Instance.Server.PlayerDisconnect += OnPlayerDisconnect; + //ensure host can interact Refreshed = true; } @@ -182,6 +185,8 @@ protected override void OnDestroy() } NetworkLifecycle.Instance.OnTick -= OnTick; + + NetworkLifecycle.Instance.Server.PlayerDisconnect -= OnPlayerDisconnect; } if (carSelectorGrab != null) @@ -259,13 +264,14 @@ public Dictionary GetPluggables() return keyValuePairs; } - public bool ValidateInteraction(CommonPitStopInteractionPacket packet, ITransportPeer peer) + public bool ValidateInteraction(CommonPitStopInteractionPacket packet, ServerPlayer player) { //todo: implement validation code (player distance, player interacting, etc.) return true; } - public void OnPlayerDisconnect(ITransportPeer peer) + //todo: update when merged with ModAPI branch + public void OnPlayerDisconnect(uint playerId) { //todo: when a player disconnects, if they are interacting with a lever, cancel the interaction //Multiplayer.LogWarning($"OnPlayerDisconnect()"); diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 5abb2a9c..f5dec33b 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -998,9 +998,9 @@ private void OnCommonPitStopPlugInteractionPacket(CommonPitStopPlugInteractionPa return; } - Log($"Pit Stop Plug Interaction received for {netPlug}"); + Log($"Pit Stop Plug Interaction received for {netPlug.NetId}"); - LogDebug(() => $"OnCommonPitStopPlugInteractionPacket() [{netPlug?.transform?.parent?.name}, {packet.NetId}], interaction: [{(PlugInteractionType)packet.InteractionType}]"); + LogDebug(() => $"OnCommonPitStopPlugInteractionPacket() [{netPlug?.transform?.name}, {packet.NetId}], interaction: [{(PlugInteractionType)packet.InteractionType}]"); netPlug.ProcessPacket(packet); } diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index 985cadcd..a2013bdd 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -618,11 +618,11 @@ public void SendPitStopBulkDataPacket(ushort netId, int carCount, int carIndex, SendPacket(peer, packet, DeliveryMethod.ReliableOrdered); } - public void SendPitStopInteractionPacket(ITransportPeer peer, CommonPitStopInteractionPacket packet) + public void SendPitStopInteractionPacket(ServerPlayer player, CommonPitStopInteractionPacket packet) { - LogDebug(() => $"SendPitStopInteractionPacket({peer.Id}, {packet.NetId})"); + LogDebug(() => $"SendPitStopInteractionPacket({player.Username}, {packet.NetId})"); - SendPacket(peer, packet, DeliveryMethod.ReliableOrdered); + SendPacket(player.Peer, packet, DeliveryMethod.ReliableOrdered); } public void SendPitStopPlugInteractionPacket(ServerPlayer player, CommonPitStopPlugInteractionPacket packet) @@ -1272,37 +1272,36 @@ private void OnUnconnectedPingPacket(UnconnectedPingPacket packet, IPEndPoint en private void OnCommonPitStopInteractionPacket(CommonPitStopInteractionPacket packet, ITransportPeer peer) { - - if (NetworkedPitStopStation.Get(packet.NetId, out NetworkedPitStopStation controller)) - controller.ProcessInteractionPacketAsHost(packet, peer); + bool foundPlayer = TryGetServerPlayer(peer, out var player); + if (!foundPlayer) + { + LogWarning($"Received Pit Stop Plug Interaction, but player was not found"); + } else - LogWarning($"OnCommonPitStopInteractionPacket() Failed to find PitStopStation with netId: {packet.NetId}"); + { + if (NetworkedPitStopStation.Get(packet.NetId, out NetworkedPitStopStation controller)) + controller.ProcessInteractionPacketAsHost(packet, player); + else + LogWarning($"OnCommonPitStopInteractionPacket() Failed to find PitStopStation with netId: {packet.NetId}"); + } } private void OnCommonPitStopPlugInteractionPacket(CommonPitStopPlugInteractionPacket packet, ITransportPeer peer) { bool foundPlayer = TryGetServerPlayer(peer, out var player); if (!foundPlayer) + { LogWarning($"Received Pit Stop Plug Interaction, but player was not found"); + SendNetSerializablePacket(peer, new CommonPitStopPlugInteractionPacket + { + NetId = packet.NetId, + InteractionType = (byte)PitStopStationInteractionType.Reject + }, DeliveryMethod.ReliableOrdered); + } if(NetworkedPluggableObject.Get(packet.NetId, out NetworkedPluggableObject plug) && foundPlayer) { - if (plug.ValidateInteraction(packet, player)) - { - //passed validation, send to all but the originator - //todo: refactor for culling - packet.PlayerId = player.Id; - SendNetSerializablePacketToAll(packet, DeliveryMethod.ReliableOrdered, peer); - } - else - { - //Failed to validate, player needs to rollback interaction - SendNetSerializablePacket(peer, new CommonPitStopPlugInteractionPacket - { - NetId = packet.NetId, - InteractionType = (byte)PitStopStationInteractionType.Reject - }, DeliveryMethod.ReliableOrdered); - } + plug.ProcessInteractionPacketAsHost(packet, player); } else { From 9d2bbf6e6d2d52113ef430e70d6e62760838b275 Mon Sep 17 00:00:00 2001 From: AMacro Date: Mon, 7 Jul 2025 22:01:33 +1000 Subject: [PATCH 414/521] Change to StartSnappingTo Changed to enable client side audio for snapping/plugging in --- .../Networking/World/NetworkedPluggableObject.cs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/Multiplayer/Components/Networking/World/NetworkedPluggableObject.cs b/Multiplayer/Components/Networking/World/NetworkedPluggableObject.cs index 84359ea0..de570f62 100644 --- a/Multiplayer/Components/Networking/World/NetworkedPluggableObject.cs +++ b/Multiplayer/Components/Networking/World/NetworkedPluggableObject.cs @@ -50,7 +50,7 @@ public static bool Get(PluggableObject pluggableObject, out NetworkedPluggableOb public PropHose Hose { get; private set; } public NetworkedPitStopStation Station { get; private set; } public bool IsConnecting { get; set; } = false; - + public bool IsHeld => playerHolding != 0 || HeldBy != null || PluggableObject.controlGrabbed; private GrabHandlerGizmoItem grabHandler; @@ -147,7 +147,7 @@ protected void LateUpdate() if (currentInteraction == PlugInteractionType.Rejected) return; - Multiplayer.LogDebug(()=>$"NetworkedPluggableObject.LateUpdate()station: {Station?.StationName}, processing: {NetworkLifecycle.Instance.IsProcessingPacket}, processing as Host: {processingAsHost}, refreshed: {Refreshed}, isConnecting: {IsConnecting}"); + Multiplayer.LogDebug(() => $"NetworkedPluggableObject.LateUpdate()station: {Station?.StationName}, processing: {NetworkLifecycle.Instance.IsProcessingPacket}, processing as Host: {processingAsHost}, refreshed: {Refreshed}, isConnecting: {IsConnecting}"); if (!processingAsHost) { NetworkLifecycle.Instance.Client?.SendPitStopPlugInteractionPacket(NetId, currentInteraction, transform.position - WorldMover.currentMove, transform.rotation, TrainCarNetId, SocketIndex); @@ -530,7 +530,8 @@ public void ProcessInteraction(PlugInteractionType interaction, byte playerId, u DropPlug(); - result = PluggableObject.InstantSnapTo(PluggableObject.startAttachedTo); + //result = PluggableObject.InstantSnapTo(PluggableObject.startAttachedTo); + result = PluggableObject.StartSnappingTo(PluggableObject.startAttachedTo, true); Multiplayer.LogDebug(() => $"ProcessPacket() NetId: {NetId}, DockHome, result: {result}"); break; @@ -633,7 +634,8 @@ private void DockTrainCar(ushort trainNetId, sbyte socketIndex) return; } - bool result = PluggableObject.InstantSnapTo(socket); + //bool result = PluggableObject.InstantSnapTo(socket); + bool result = PluggableObject.StartSnappingTo(socket, true); if (result) { @@ -665,12 +667,12 @@ public PlugSocket GetTrainCarSocket(NetworkedTrainCar netTrainCar, sbyte socketI private IEnumerator WaitForYankSettle() { - Multiplayer.LogDebug(()=>$"WaitForYankSettle() PluggableObject.yankOutOfHand: {PluggableObject.yankOutOfHand}, velocity: {PlugRB.velocity.sqrMagnitude}"); + Multiplayer.LogDebug(() => $"WaitForYankSettle() PluggableObject.yankOutOfHand: {PluggableObject.yankOutOfHand}, velocity: {PlugRB.velocity.sqrMagnitude}"); PluggableObject.yankOutOfHand = false; //block docking //allow force to be applied yield return new WaitForFixedUpdate(); - Multiplayer.LogDebug(()=>$"WaitForYankSettle() post-WaitForFixed, PluggableObject.yankOutOfHand: {PluggableObject.yankOutOfHand}, velocity: {PlugRB.velocity.sqrMagnitude}"); + Multiplayer.LogDebug(() => $"WaitForYankSettle() post-WaitForFixed, PluggableObject.yankOutOfHand: {PluggableObject.yankOutOfHand}, velocity: {PlugRB.velocity.sqrMagnitude}"); float time = Time.time; From 2b3eb9032e70d4df320d89a85dfcf7487bd5b06a Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 13 Jul 2025 17:19:03 +1000 Subject: [PATCH 415/521] Update API documentation --- MultiplayerAPI/Interfaces/IMultiplayerAPI.cs | 2 +- MultiplayerAPI/Interfaces/INetId.cs | 36 ++++++++++++++ MultiplayerAPI/Interfaces/IPlayer.cs | 51 +++++++++++++++++++- MultiplayerAPI/MultiplayerAPI.cs | 29 ++++++++++- 4 files changed, 115 insertions(+), 3 deletions(-) diff --git a/MultiplayerAPI/Interfaces/IMultiplayerAPI.cs b/MultiplayerAPI/Interfaces/IMultiplayerAPI.cs index 1a0dcffc..332b6738 100644 --- a/MultiplayerAPI/Interfaces/IMultiplayerAPI.cs +++ b/MultiplayerAPI/Interfaces/IMultiplayerAPI.cs @@ -51,7 +51,7 @@ public interface IMultiplayerAPI /// /// Example: In Multiplayer's TrainCar simulation sync, small changes are cached when they occur but sent as a single packet per TrainCar when OnTick fires, reducing network overhead. /// - /// The current game tick number, incremented each tick cycle + /// The parameter represents the current tick number event Action OnTick; /// diff --git a/MultiplayerAPI/Interfaces/INetId.cs b/MultiplayerAPI/Interfaces/INetId.cs index ee2d2f54..bf920157 100644 --- a/MultiplayerAPI/Interfaces/INetId.cs +++ b/MultiplayerAPI/Interfaces/INetId.cs @@ -1,8 +1,44 @@ namespace MPAPI.Interfaces; +/// +/// Provides methods for mapping between built-in game objects and their network identifiers in the Multiplayer system. +/// +/// +/// This interface enables bidirectional lookup between game objects and their corresponding network IDs, +/// which are used to synchronise object references across the network. Only objects that are actively +/// synchronised by Multiplayer mod will have associated network identifiers. +/// +/// Additional objects from the base game will be added as Multiplayer features are implemented. If there are +/// specific object types you would like to see supported, please create an issue on the Multiplayer Mod GitHub repository. +/// public interface INetIdProvider { + /// + /// Attempts to retrieve the network identifier for the specified object. + /// + /// The type of object to get the network ID for. Must be a reference type. + /// The object to get the network identifier for. + /// + /// When this method returns, contains the network identifier associated with the object if found; + /// otherwise, the default value for the type. + /// + /// + /// true if the network identifier was successfully retrieved; otherwise, false. + /// bool TryGetNetId(T obj, out ushort netId) where T : class; + + /// + /// Attempts to retrieve the object associated with the specified network identifier. + /// + /// The type of object to retrieve. Must be a reference type. + /// The network identifier of the object to retrieve. + /// + /// When this method returns, contains the object associated with the network identifier if found; + /// otherwise, the default value for the type. + /// + /// + /// true if the object was successfully retrieved; otherwise, false. + /// bool TryGetObject(ushort netId, out T obj) where T : class; } diff --git a/MultiplayerAPI/Interfaces/IPlayer.cs b/MultiplayerAPI/Interfaces/IPlayer.cs index 4ae79952..420063b8 100644 --- a/MultiplayerAPI/Interfaces/IPlayer.cs +++ b/MultiplayerAPI/Interfaces/IPlayer.cs @@ -3,18 +3,67 @@ namespace MPAPI.Interfaces { + /// + /// Represents a player in the multiplayer session, providing access to player state and information. + /// public interface IPlayer { + /// + /// Gets the identifier for the player within the session. + /// + /// + /// This identifier can be used as a network ID for referencing the player across the network. + /// If the player leaves the session the Id will be reassigned to the next player to join. + /// public byte Id { get; } + + /// + /// Gets the username/display name of the player. + /// public string Username { get; } + + /// + /// Gets the current world position of the player. + /// Vector3 Position { get; } + + /// + /// Gets the current Y-axis rotation of the player. + /// float RotationY { get; } + + /// + /// Gets a value indicating whether the player has finished loading the game world. + /// + /// + /// true if the player has completed world loading and is ready to receive game state updates; otherwise, false. + /// bool IsLoaded { get; } + + /// + /// Gets a value indicating whether this player is the host of the multiplayer session. + /// + /// true if the player is the session host; otherwise, false. bool IsHost { get; } + + /// + /// Gets the current network ping/latency for this player. + /// + /// The one-way time in milliseconds between the server and this player. int Ping { get; } - // Car information + /// + /// Gets the current network ping/latency for this player. + /// + /// The round-trip time in milliseconds between the server and this player. bool IsOnCar { get; } + + /// + /// Gets the train car that the player is currently occupying. + /// + /// + /// The instance the player is on, or null if the player is not on any car. + /// TrainCar OccupiedCar { get; } } } diff --git a/MultiplayerAPI/MultiplayerAPI.cs b/MultiplayerAPI/MultiplayerAPI.cs index 8e5a9ea3..80c31261 100644 --- a/MultiplayerAPI/MultiplayerAPI.cs +++ b/MultiplayerAPI/MultiplayerAPI.cs @@ -3,12 +3,39 @@ namespace MPAPI; +/// +/// Provides an API interface for accessing Multiplayer Mod functionality and managing server/client instances. +/// +/// +/// This class serves as the main entry point for the Multiplayer API, providing events for server and client lifecycle management, +/// and access to the current server, client, and API instances. +/// public static class MultiplayerAPI { + /// + /// Event fired when a server instance has been created. + /// + /// + /// This event provides access to the instance that was started. + /// public static event Action ServerStarted; + + /// + /// Event fired when a client instance has been created. + /// + /// + /// This event provides access to the instance that was started. + /// public static event Action ClientStarted; + /// + /// Event fired when a server instance is stopped. + /// public static event Action ServerStopped; + + /// + /// Event fired when a client instance is stopped. + /// public static event Action ClientStopped; private static IMultiplayerAPI _instance; @@ -66,7 +93,7 @@ internal static void ClearClient() /// /// Internal method for the Multiplayer mod to register a server instance /// - /// The API implementation + /// The API implementation internal static void RegisterServer(IServer server) { _server = server; From e68bc34d9f109b802fec867d60ef0594c48e727b Mon Sep 17 00:00:00 2001 From: AMacro Date: Sun, 13 Jul 2025 22:36:49 +1000 Subject: [PATCH 416/521] Fix update localisation issue --- Multiplayer/Multiplayer.cs | 4 ++-- locale.csv | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Multiplayer/Multiplayer.cs b/Multiplayer/Multiplayer.cs index 4867ca67..648f8255 100644 --- a/Multiplayer/Multiplayer.cs +++ b/Multiplayer/Multiplayer.cs @@ -173,8 +173,8 @@ private static void LateUpdate(UnityModManager.ModEntry modEntry, float deltaTim "Run Unity Mod Manager Installer to apply the update."; */ - var latestVer = Locale.Get(Locale.MAIN_MENU__UPDATE_LATEST_KEY, $"\t\t{ModEntry.NewestVersion}"); - var installedVer = Locale.Get(Locale.MAIN_MENU__UPDATE_INSTALLED_KEY, $"\t\t{ModEntry.Version}"); + var latestVer = Locale.Get(Locale.MAIN_MENU__UPDATE_LATEST_KEY, [$"\t\t{ModEntry.NewestVersion}"]); + var installedVer = Locale.Get(Locale.MAIN_MENU__UPDATE_INSTALLED_KEY, [$"\t\t{ModEntry.Version}"]); update.labelTMPro.text = Locale.MAIN_MENU__UPDATE_TITLE + $"\r\n\r\n{latestVer}" + diff --git a/locale.csv b/locale.csv index 70675de5..51f37603 100644 --- a/locale.csv +++ b/locale.csv @@ -10,8 +10,8 @@ mm/join_server__tooltip,The tooltip shown when hovering over the 'Join Server' b mm/join_server__tooltip_disabled,Unused,,,,,,,,,,,,,,,,,,,,,,,,,, mm/incompatible_mods,Message box for incompatible mods detected,Incompatible mods detected!\nPlease disable the following mods and reload the game:,,,,,,,,,,,,,,,,,,,,,,,,, mm/update_title,Message box title for Multiplayer mod update,Multiplayer Mod Update Available!,,,,,,,,,,,,,,,,,,,,,,,,, -mm/update_latest,Message box title for Multiplayer mod update,Latest version: {0},,,,,,,,,,,,,,,,,,,,,,,,, -mm/update_installed,Message box title for Multiplayer mod update,Installed version: {0},,,,,,,,,,,,,,,,,,,,,,,,, +mm/update_latest,Message box title for Multiplayer mod update,Latest version:{0},,,,,,,,,,,,,,,,,,,,,,,,, +mm/update_installed,Message box title for Multiplayer mod update,Installed version:{0},,,,,,,,,,,,,,,,,,,,,,,,, mm/update_action,Message box title for Multiplayer mod update,Run Unity Mod Manager Installer to apply the update.,,,,,,,,,,,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,, ,Server Browser,,,,,,,,,,,,,,,,,,,,,,,,,, From 2ff90c5bc6d1dae0df32009946fa98858363c56c Mon Sep 17 00:00:00 2001 From: Macka Date: Sat, 19 Jul 2025 09:17:25 +1000 Subject: [PATCH 417/521] Fix compatibility with B99.6 --- Multiplayer/Patches/Train/TrainsOptimizerPatch.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Multiplayer/Patches/Train/TrainsOptimizerPatch.cs b/Multiplayer/Patches/Train/TrainsOptimizerPatch.cs index 1703e530..d37f704e 100644 --- a/Multiplayer/Patches/Train/TrainsOptimizerPatch.cs +++ b/Multiplayer/Patches/Train/TrainsOptimizerPatch.cs @@ -1,10 +1,9 @@ using HarmonyLib; using System; using System.Collections.Generic; -using System.Linq; using System.Text; using DV.Logic.Job; -using DV.Utils; +using DV.Optimizers; namespace Multiplayer.Patches.Train; [HarmonyPatch(typeof(TrainsOptimizer))] From 683966bd8cbd89fba4f92200a8da1c6e7ea364f4 Mon Sep 17 00:00:00 2001 From: Macka Date: Sat, 19 Jul 2025 09:17:25 +1000 Subject: [PATCH 418/521] Fix compatibility with B99.6 --- Multiplayer/Patches/Train/TrainsOptimizerPatch.cs | 3 +-- .../Patches/World/PlayerDistanceGameObjectsDisablerPatch.cs | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Multiplayer/Patches/Train/TrainsOptimizerPatch.cs b/Multiplayer/Patches/Train/TrainsOptimizerPatch.cs index 1703e530..d37f704e 100644 --- a/Multiplayer/Patches/Train/TrainsOptimizerPatch.cs +++ b/Multiplayer/Patches/Train/TrainsOptimizerPatch.cs @@ -1,10 +1,9 @@ using HarmonyLib; using System; using System.Collections.Generic; -using System.Linq; using System.Text; using DV.Logic.Job; -using DV.Utils; +using DV.Optimizers; namespace Multiplayer.Patches.Train; [HarmonyPatch(typeof(TrainsOptimizer))] diff --git a/Multiplayer/Patches/World/PlayerDistanceGameObjectsDisablerPatch.cs b/Multiplayer/Patches/World/PlayerDistanceGameObjectsDisablerPatch.cs index 4562cec7..f7d85930 100644 --- a/Multiplayer/Patches/World/PlayerDistanceGameObjectsDisablerPatch.cs +++ b/Multiplayer/Patches/World/PlayerDistanceGameObjectsDisablerPatch.cs @@ -1,3 +1,4 @@ +using DV.Optimizers; using HarmonyLib; using Multiplayer.Components.Networking; using Multiplayer.Utils; @@ -5,7 +6,6 @@ using System.Linq; using System.Reflection; using System.Reflection.Emit; -using System.Text; using UnityEngine; namespace Multiplayer.Patches.World; From 82eeae7bfe02c8c8cdcf0396379330b7fb894009 Mon Sep 17 00:00:00 2001 From: Macka Date: Sat, 19 Jul 2025 09:57:02 +1000 Subject: [PATCH 419/521] Ready for release 0.1.12.2 --- Multiplayer/Multiplayer.csproj | 2 +- info.json | 2 +- releases.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Multiplayer/Multiplayer.csproj b/Multiplayer/Multiplayer.csproj index f9c41b0c..10c2925f 100644 --- a/Multiplayer/Multiplayer.csproj +++ b/Multiplayer/Multiplayer.csproj @@ -3,7 +3,7 @@ net48 latest Multiplayer - 0.1.12.0 + 0.1.12.2 diff --git a/info.json b/info.json index c6233d01..5e1d60bd 100644 --- a/info.json +++ b/info.json @@ -1,6 +1,6 @@ { "Id": "Multiplayer", - "Version": "0.1.12.0", + "Version": "0.1.12.2", "DisplayName": "Multiplayer", "Author": "Insprill, Macka, Morm", "EntryMethod": "Multiplayer.Multiplayer.Load", diff --git a/releases.json b/releases.json index 5f9a2366..f2b839ad 100644 --- a/releases.json +++ b/releases.json @@ -1,6 +1,6 @@ { "Releases": [ - {"Id": "Multiplayer", "Version": "0.1.12.0", "DownloadUrl": "https://github.com/AMacro/dv-multiplayer/releases/download/v0.1.12.0-Beta/Multiplayer.0.1.12.0.zip"} + {"Id": "Multiplayer", "Version": "0.1.12.2", "DownloadUrl": "https://github.com/AMacro/dv-multiplayer/releases/download/v0.1.12.2-Beta/Multiplayer.0.1.12.2.zip"} ] } \ No newline at end of file From 80d8f3f778cf871866f8b741923a4bf60cd8e45e Mon Sep 17 00:00:00 2001 From: Macka Date: Sat, 19 Jul 2025 10:57:39 +1000 Subject: [PATCH 420/521] Add API version support --- Multiplayer/API/APIProvider.cs | 92 +++++++++----------- Multiplayer/Multiplayer.cs | 28 +++--- MultiplayerAPI Tests/MultiplayerAPITest.cs | 2 +- MultiplayerAPI/Interfaces/IMultiplayerAPI.cs | 9 +- MultiplayerAPI/MultiplayerAPI.cs | 37 ++++++++ MultiplayerAPI/MultiplayerAPI.csproj | 4 +- 6 files changed, 103 insertions(+), 69 deletions(-) diff --git a/Multiplayer/API/APIProvider.cs b/Multiplayer/API/APIProvider.cs index 2db25680..b379fdf1 100644 --- a/Multiplayer/API/APIProvider.cs +++ b/Multiplayer/API/APIProvider.cs @@ -1,77 +1,63 @@ -using MPAPI; using MPAPI.Interfaces; using MPAPI.Types; using Multiplayer.Components.Networking; using System; -using System.Linq; -using System.Reflection; +namespace Multiplayer.API; -namespace Multiplayer.API +public class APIProvider : IMultiplayerAPI { - public class APIProvider : IMultiplayerAPI - { - public string Version - { - get - { - AssemblyInformationalVersionAttribute info = (AssemblyInformationalVersionAttribute)typeof(MultiplayerAPI).Assembly. - GetCustomAttributes(typeof(AssemblyInformationalVersionAttribute), false) - .FirstOrDefault(); - - if (info == null) - return ""; + internal const string BUILT_AGAINST_API_VERSION = "0.1.0.0"; - return info.InformationalVersion.Split('+')[0]; - } - } + public string SupportedApiVersion => BUILT_AGAINST_API_VERSION; - public bool IsMultiplayerLoaded => true; + public string MultiplayerVersion => Multiplayer.Ver; - public bool IsConnected => NetworkLifecycle.Instance.IsClientRunning || NetworkLifecycle.Instance.IsServerRunning; + public bool IsMultiplayerLoaded => true; - public bool IsHost => NetworkLifecycle.Instance.IsHost(); + public bool IsConnected => NetworkLifecycle.Instance.IsClientRunning || NetworkLifecycle.Instance.IsServerRunning; - public bool IsDedicatedServer => false; //feature not implemented + public bool IsHost => NetworkLifecycle.Instance.IsHost(); - public bool IsSinglePlayer => NetworkLifecycle.Instance.IsServerRunning && (NetworkLifecycle.Instance?.Server.IsSinglePlayer ?? false); + public bool IsDedicatedServer => false; //feature not implemented - public event Action OnTick; - public uint TICK_RATE => NetworkLifecycle.TICK_RATE; - public uint CurrentTick => NetworkLifecycle.Instance.Tick; + public bool IsSinglePlayer => NetworkLifecycle.Instance.IsServerRunning && (NetworkLifecycle.Instance?.Server.IsSinglePlayer ?? false); - public bool TryGetNetId(T obj, out ushort netId) where T : class - { - return NetIdProvider.Instance.TryGetNetId(obj, out netId); - } + public event Action OnTick; + public uint TICK_RATE => NetworkLifecycle.TICK_RATE; + public uint CurrentTick => NetworkLifecycle.Instance.Tick; - public bool TryGetObjectFromNetId(ushort netId, out T obj) where T : class - { - return NetIdProvider.Instance.TryGetObject(netId, out obj); - } + public bool TryGetNetId(T obj, out ushort netId) where T : class + { + return NetIdProvider.Instance.TryGetNetId(obj, out netId); + } - public void SetModCompatibility(string modId, MultiplayerCompatibility compatibility) - { - ModCompatibilityManager.Instance.RegisterCompatibility(modId, compatibility); - } + public bool TryGetObjectFromNetId(ushort netId, out T obj) where T : class + { + return NetIdProvider.Instance.TryGetObject(netId, out obj); + } - #region Class Helpers + public void SetModCompatibility(string modId, MultiplayerCompatibility compatibility) + { + ModCompatibilityManager.Instance.RegisterCompatibility(modId, compatibility); + } - internal APIProvider() - { - NetworkLifecycle.Instance.OnTick += OnTickInternal; - } + #region Class Helpers - internal void Dispose() - { - NetworkLifecycle.Instance.OnTick -= OnTickInternal; - } + internal APIProvider() + { + NetworkLifecycle.Instance.OnTick += OnTickInternal; + } - private void OnTickInternal(uint tick) - { - OnTick?.Invoke(tick); - } + internal void Dispose() + { + NetworkLifecycle.Instance.OnTick -= OnTickInternal; + } - #endregion + internal void OnTickInternal(uint tick) + { + OnTick?.Invoke(tick); } + + #endregion } diff --git a/Multiplayer/Multiplayer.cs b/Multiplayer/Multiplayer.cs index 648f8255..6c0de175 100644 --- a/Multiplayer/Multiplayer.cs +++ b/Multiplayer/Multiplayer.cs @@ -68,11 +68,24 @@ public static bool Load(UnityModManager.ModEntry modEntry) var gameVer = BuildInfo.BUILD_VERSION_MAJOR.ToString() + (string.IsNullOrEmpty(BuildInfo.BUILD_VERSION_SUFFIX) ? "" : "." + BuildInfo.BUILD_VERSION_SUFFIX); - Log($"\r\n\tMultiplayer JSON Version: {ModEntry.Info.Version}, Internal Version: {Ver}\r\n" + - $"\tGame version: {gameVer}\r\n" + - $"\tBuildbot version: {BuildInfo.BUILDBOT_INFO.ToString()}\r\n" + - $"\tLiteNetLib version: {LiteNetLibVer()}\r\n"); + bool APIcompatible = false; + if (Version.TryParse(APIProvider.BUILT_AGAINST_API_VERSION, out var builtVerAPI) && Version.TryParse(MultiplayerAPI.LoadedApiVersion, out var loadedVerAPI)) + { + APIcompatible = loadedVerAPI >= builtVerAPI; + } + + Log($"\r\n\r\n" + + $"\tMultiplayer JSON Version: {ModEntry.Info.Version}, Internal Version: {Ver}\r\n" + + $"\tGame Version: {gameVer}\r\n" + + $"\tBuildbot Version: {BuildInfo.BUILDBOT_INFO.ToString()}\r\n" + + $"\tLiteNetLib Version: {LiteNetLibVer()}\r\n" + + $"\tMultiplayer API Required Version: {APIProvider.BUILT_AGAINST_API_VERSION}, Loaded Version: {MultiplayerAPI.LoadedApiVersion}\r\n" + + $"\tMultiplayer API Compatible: {APIcompatible}\r\n"); + if (!APIcompatible) + { + throw new Exception("Multiplayer API version mismatch! One or more mods are using a newer version of the Multiplayer API, please update Multiplayer Mod or disable these mods.\r\n"); + } Log("Patching..."); harmony = new Harmony(ModEntry.Info.Id); @@ -86,13 +99,6 @@ public 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..."); - // PassengerJobsMod.Init(); - //} - if (!LoadAssets()) return false; diff --git a/MultiplayerAPI Tests/MultiplayerAPITest.cs b/MultiplayerAPI Tests/MultiplayerAPITest.cs index 82491343..489962bc 100644 --- a/MultiplayerAPI Tests/MultiplayerAPITest.cs +++ b/MultiplayerAPI Tests/MultiplayerAPITest.cs @@ -50,7 +50,7 @@ private static void LateUpdate(UnityModManager.ModEntry modEntry, float dt) { //Mod loading order can't be guaranteed, so we should wait for all mods to load prior to checking for multiplayer. //Alternatively, set 'Multiplayer' in the 'LoadAfter' parameter in your 'info.json' - Log($"MultiplayerAPITest.LateUpdate() Multiplayer Mod is loaded: {MultiplayerAPI.IsMultiplayerLoaded}, API Version: {MultiplayerAPI.Instance.Version} "); + Log($"MultiplayerAPITest.LateUpdate() Multiplayer Mod is loaded: {MultiplayerAPI.IsMultiplayerLoaded}, API Version: {MultiplayerAPI.LoadedApiVersion} "); if (MultiplayerAPI.IsMultiplayerLoaded) { //Register that this mod needs to be installed on both server and client diff --git a/MultiplayerAPI/Interfaces/IMultiplayerAPI.cs b/MultiplayerAPI/Interfaces/IMultiplayerAPI.cs index 332b6738..75d3db93 100644 --- a/MultiplayerAPI/Interfaces/IMultiplayerAPI.cs +++ b/MultiplayerAPI/Interfaces/IMultiplayerAPI.cs @@ -9,9 +9,14 @@ namespace MPAPI.Interfaces; public interface IMultiplayerAPI { /// - /// Returns the version of the Multiplayer API if multiplayer is loaded, otherwise returns null + /// Gets the version of the Multiplayer API that the Multiplayer mod supports. /// - public string Version { get; } + public string SupportedApiVersion { get; } + + /// + /// Gets the version of the Multiplayer mod itself. + /// + public string MultiplayerVersion { get; } /// /// Gets whether the multiplayer mod is currently loaded and active diff --git a/MultiplayerAPI/MultiplayerAPI.cs b/MultiplayerAPI/MultiplayerAPI.cs index 80c31261..8aac1b06 100644 --- a/MultiplayerAPI/MultiplayerAPI.cs +++ b/MultiplayerAPI/MultiplayerAPI.cs @@ -1,5 +1,7 @@ using MPAPI.Interfaces; using System; +using System.Linq; +using System.Reflection; namespace MPAPI; @@ -12,6 +14,41 @@ namespace MPAPI; /// public static class MultiplayerAPI { + /// + /// Gets the version of the Multiplayer API DLL that is currently loaded. + /// + /// The version string of the API DLL. + public static string LoadedApiVersion + { + get + { + AssemblyInformationalVersionAttribute info = (AssemblyInformationalVersionAttribute)typeof(MultiplayerAPI).Assembly. + GetCustomAttributes(typeof(AssemblyInformationalVersionAttribute), false) + .FirstOrDefault(); + + if (info == null) + return ""; + + return info.InformationalVersion.Split('+')[0]; + } + } + + /// + /// Gets the version of the Multiplayer API that the Multiplayer mod supports. + /// + /// The supported API version string, or null if multiplayer is not loaded. + /// + /// This indicates the API version that the Multiplayer mod was built against and is compatible with. + /// If this differs from , there may be compatibility issues. + /// + public static string SupportedApiVersion => _instance?.SupportedApiVersion; + + /// + /// Gets the version of the Multiplayer mod itself. + /// + /// The Multiplayer mod version string, or null if multiplayer is not loaded. + public static string MultiplayerVersion => _instance?.MultiplayerVersion; + /// /// Event fired when a server instance has been created. /// diff --git a/MultiplayerAPI/MultiplayerAPI.csproj b/MultiplayerAPI/MultiplayerAPI.csproj index 475c5807..a65fba32 100644 --- a/MultiplayerAPI/MultiplayerAPI.csproj +++ b/MultiplayerAPI/MultiplayerAPI.csproj @@ -4,8 +4,8 @@ net48 latest MPAPI - 0.1.0 - 0.1.0 + 0.1.0.0 + 0.1.0.0 true From ed43ffae9eab01dfa3702a695041b27d40629036 Mon Sep 17 00:00:00 2001 From: Macka Date: Sat, 19 Jul 2025 13:24:31 +1000 Subject: [PATCH 421/521] Refactor paint theme system to use uint theme IDs Replaces sbyte-based paint theme indices with uint-based theme IDs using FNV-1a hashing for unique identification. Updates all relevant serialisation, deserialisation, packet structures, and method signatures to use the new theme ID system. This change improves extensibility and avoids index collisions, allowing for more robust theme registration and lookup. --- .../Networking/Train/NetworkedTrainCar.cs | 9 +- .../Networking/Train/PaintThemeLookup.cs | 99 ++++++++++--------- .../Data/Train/TrainsetSpawnPart.cs | 12 +-- .../Managers/Client/NetworkClient.cs | 24 ++--- .../Common/Train/CommonPaintThemePacket.cs | 6 +- 5 files changed, 79 insertions(+), 71 deletions(-) diff --git a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs index d3bbedfa..ea863c5e 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs @@ -815,11 +815,10 @@ private void Common_OnPaintThemeChange(TrainCarPaint paintController) Multiplayer.LogDebug(() => $"Common_OnPaintThemeChange() target: {paintController.TargetArea}, theme: {paintController.CurrentTheme.name}"); - byte target = (byte)paintController.TargetArea; - var theme = PaintThemeLookup.Instance.GetThemeIndex(paintController.CurrentTheme); + var themeId = PaintThemeLookup.Instance.GetThemeId(paintController.CurrentTheme); - Multiplayer.LogDebug(() => $"Common_OnPaintThemeChange() sending [{CurrentID},{NetId}], target: {paintController.TargetArea}, theme: [{paintController.CurrentTheme.name},{theme}]"); - NetworkLifecycle.Instance?.Client.SendPaintThemeChangePacket(NetId, target, theme); + Multiplayer.LogDebug(() => $"Common_OnPaintThemeChange() sending [{CurrentID},{NetId}], target: {paintController.TargetArea}, theme: [{paintController.CurrentTheme.name}, {themeId}]"); + NetworkLifecycle.Instance?.Client.SendPaintThemeChangePacket(NetId, paintController.TargetArea, themeId); } private void Common_OnFuseUpdated(Fuse fuse) @@ -1234,7 +1233,7 @@ public void Common_ReceivePaintThemeUpdate(TrainCarPaint.Target target, PaintThe if (targetPaint == null || !targetPaint.IsSupported(paint)) { - Multiplayer.LogWarning($"Received Paint Theme update for [{CurrentID}, {NetId}], but {paint?.assetName} is not supported"); + Multiplayer.LogWarning($"Received Paint Theme update for [{CurrentID}, {NetId}], but {paint?.AssetName} is not supported"); return; } diff --git a/Multiplayer/Components/Networking/Train/PaintThemeLookup.cs b/Multiplayer/Components/Networking/Train/PaintThemeLookup.cs index 1e8dd877..3938084b 100644 --- a/Multiplayer/Components/Networking/Train/PaintThemeLookup.cs +++ b/Multiplayer/Components/Networking/Train/PaintThemeLookup.cs @@ -10,93 +10,100 @@ namespace Multiplayer.Components.Networking.Train; public class PaintThemeLookup : SingletonBehaviour { - private readonly Dictionary themeIndices = []; - private string[] themeNames; + private readonly Dictionary hashToThemeName = []; protected override void Awake() { base.Awake(); - themeNames = Resources.LoadAll("").Where(x => x is PaintTheme) - .Select(x => x.name.ToLower()) + var themeNames = Resources.LoadAll("").Where(x => x is PaintTheme) + .Select(x => ((PaintTheme)x).AssetName) .ToArray(); - for (sbyte 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}"))}"; - }); + foreach (var themeName in themeNames) + RegisterTheme(themeName); } - public PaintTheme GetPaintTheme(sbyte index) + public PaintTheme GetPaintTheme(uint themeId) { PaintTheme theme = null; - var themeName = GetThemeName(index); + var themeName = GetThemeNameFromId(themeId); if (themeName != null) - PaintTheme.TryLoad(GetThemeName(index), out theme); + PaintTheme.TryLoad(themeName, out theme); return theme; } - public string GetThemeName(sbyte index) + public string GetThemeNameFromId(uint themeId) { - return (index >= 0 && index < themeNames.Length) ? themeNames[index] : null; + hashToThemeName.TryGetValue(themeId, out string themeName); + + return themeName; } - public sbyte GetThemeIndex(PaintTheme theme) + public uint GetThemeId(PaintTheme theme) { if(theme == null) - return -1; + return 0; - return GetThemeIndex(theme.assetName); + return GetThemeId(theme.AssetName); } - public sbyte GetThemeIndex(string themeName) + public uint GetThemeId(string themeName) { - return themeIndices.TryGetValue(themeName.ToLower(), out sbyte index) ? index : (sbyte)-1; + return Fnv1aHash(themeName); } - /* - * Allow other mods to register custom themes - - public void RegisterTheme(string themeName) + public uint 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; + if (string.IsNullOrEmpty(themeName)) + return 0; + + var hash = GetThemeId(themeName); - // Add to dictionary - themeIndices.Add(themeName, newIndex); + if (hashToThemeName.ContainsKey(hash)) + { + Multiplayer.LogWarning($"Theme '{themeName}' is already registered with id: {hash}."); + return hash; } + + hashToThemeName[hash] = themeName; + + Multiplayer.Log($"Theme '{themeName}' registered with id: {hash}."); + + return hash; } public void UnregisterTheme(string themeName) { - themeName = themeName.ToLower(); - if (themeIndices.TryGetValue(themeName, out int index)) + var hash = GetThemeId(themeName); + + if (hashToThemeName.TryGetValue(hash, out _)) + { + hashToThemeName.Remove(hash); + } + else { - // Remove from dictionary - themeIndices.Remove(themeName); + Multiplayer.LogWarning($"Tried to unregister theme '{themeName}', but theme is not registered."); + } + } + - // Remove from array and shift remaining elements - for (int i = index; i < themeNames.Length - 1; i++) + private uint Fnv1aHash(string text) + { + unchecked + { + const uint fnvPrime = 0x01000193; + uint hash = 0x811C9DC5; + foreach (char c in text) { - themeNames[i] = themeNames[i + 1]; - themeIndices[themeNames[i]] = i; // Update indices + hash ^= c; + hash *= fnvPrime; } - Array.Resize(ref themeNames, themeNames.Length - 1); + return hash; } } - */ [UsedImplicitly] public new static string AllowAutoCreate() diff --git a/Multiplayer/Networking/Data/Train/TrainsetSpawnPart.cs b/Multiplayer/Networking/Data/Train/TrainsetSpawnPart.cs index f9d8c9e2..1f6ee6dd 100644 --- a/Multiplayer/Networking/Data/Train/TrainsetSpawnPart.cs +++ b/Multiplayer/Networking/Data/Train/TrainsetSpawnPart.cs @@ -102,8 +102,8 @@ public static void Serialize(NetDataWriter writer, TrainsetSpawnPart data) if(data.IsRestorationLoco) writer.Put((byte) data.RestorationState); - writer.Put(PaintThemeLookup.Instance.GetThemeIndex(data.PaintExterior)); - writer.Put(PaintThemeLookup.Instance.GetThemeIndex(data.PaintInterior)); + writer.Put(PaintThemeLookup.Instance.GetThemeId(data.PaintExterior)); + writer.Put(PaintThemeLookup.Instance.GetThemeId(data.PaintInterior)); CouplingData.Serialize(writer, data.FrontCoupling); @@ -133,12 +133,12 @@ public static TrainsetSpawnPart Deserialize(NetDataReader reader) if (isRestoration) restorationState = (LocoRestorationController.RestorationState)reader.GetByte(); - sbyte extThemeIndex = reader.GetSByte(); - sbyte intThemeIndex = reader.GetSByte(); + uint extThemeId = reader.GetUInt(); + uint intThemeId = reader.GetUInt(); - PaintTheme exteriorPaint = PaintThemeLookup.Instance.GetPaintTheme(extThemeIndex); - PaintTheme interiorPaint = PaintThemeLookup.Instance.GetPaintTheme(intThemeIndex); + PaintTheme exteriorPaint = PaintThemeLookup.Instance.GetPaintTheme(extThemeId); + PaintTheme interiorPaint = PaintThemeLookup.Instance.GetPaintTheme(intThemeId); var frontCoupling = CouplingData.Deserialize(reader); var rearCoupling = CouplingData.Deserialize(reader); diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index e2aaabdb..f16c2ffb 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -1008,24 +1008,24 @@ private void OnCommonPaintThemePacket(CommonPaintThemePacket packet) if (!NetworkedTrainCar.TryGet(packet.NetId, out NetworkedTrainCar netTrainCar)) return; - Log($"Received paint theme change for {netTrainCar?.CurrentID}"); - PaintTheme paint = PaintThemeLookup.Instance.GetPaintTheme(packet.PaintThemeId); if (paint == null) { - LogWarning($"Paint theme index {packet.PaintThemeId} does not exist!"); + LogWarning($"Received paint theme change for {netTrainCar?.CurrentID}, but paint theme id '{packet.PaintThemeId}' does not exist."); return; } - if (!Enum.IsDefined(typeof(TrainCarPaint.Target), packet.TargetArea)) - { - LogWarning($"TrainCarPaint Target {packet.TargetArea} is not defined!"); - return; - } + Log($"Received paint theme change for {netTrainCar?.CurrentID}, theme '{paint.AssetName}'"); + + //if (!Enum.IsDefined(typeof(TrainCarPaint.Target), packet.TargetArea)) + //{ + // LogWarning($"TrainCarPaint Target {packet.TargetArea} is not defined!"); + // return; + //} - LogDebug(() => $"OnCommonPaintThemePacket() [{netTrainCar?.CurrentID}, {packet.NetId}], area: {(TrainCarPaint.Target)packet.TargetArea}, paint: [{paint?.assetName}, {packet.PaintThemeId}]"); - netTrainCar?.Common_ReceivePaintThemeUpdate((TrainCarPaint.Target)packet.TargetArea, paint); + LogDebug(() => $"OnCommonPaintThemePacket() [{netTrainCar?.CurrentID}, {packet.NetId}], area: {packet.TargetArea}, paint: [{paint?.AssetName}, {packet.PaintThemeId}]"); + netTrainCar?.Common_ReceivePaintThemeUpdate(packet.TargetArea, paint); } #endregion @@ -1373,9 +1373,9 @@ public void SendItemsChangePacket(List items) DeliveryMethod.ReliableOrdered); } - public void SendPaintThemeChangePacket(ushort netId, byte targetArea, sbyte themeIndex) + public void SendPaintThemeChangePacket(ushort netId, TrainCarPaint.Target targetArea, uint themeId) { - SendPacketToServer(new CommonPaintThemePacket { NetId = netId, TargetArea = targetArea, PaintThemeId = themeIndex }, DeliveryMethod.ReliableUnordered); + SendPacketToServer(new CommonPaintThemePacket { NetId = netId, TargetArea = targetArea, PaintThemeId = themeId }, DeliveryMethod.ReliableUnordered); } #endregion diff --git a/Multiplayer/Networking/Packets/Common/Train/CommonPaintThemePacket.cs b/Multiplayer/Networking/Packets/Common/Train/CommonPaintThemePacket.cs index 236ef104..a4423bb4 100644 --- a/Multiplayer/Networking/Packets/Common/Train/CommonPaintThemePacket.cs +++ b/Multiplayer/Networking/Packets/Common/Train/CommonPaintThemePacket.cs @@ -1,8 +1,10 @@ +using DV.Customization.Paint; + namespace Multiplayer.Networking.Packets.Common.Train; public class CommonPaintThemePacket { public ushort NetId { get; set; } - public byte TargetArea { get; set; } - public sbyte PaintThemeId { get; set; } + public TrainCarPaint.Target TargetArea { get; set; } + public uint PaintThemeId { get; set; } } From 06ee5dcd2593c8c827862fdc56fcbdebe9adaa41 Mon Sep 17 00:00:00 2001 From: Macka Date: Sun, 20 Jul 2025 13:18:20 +1000 Subject: [PATCH 422/521] Fix compatibility with B99.6 --- .../World/NetworkedPitStopStation.cs | 2 +- .../Managers/Server/NetworkServer.cs | 32 +++++++++---------- .../Patches/Train/TrainsOptimizerPatch.cs | 3 +- .../PlayerDistanceGameObjectsDisablerPatch.cs | 2 +- 4 files changed, 19 insertions(+), 20 deletions(-) diff --git a/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs b/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs index aeb98394..b36f0179 100644 --- a/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs +++ b/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs @@ -1,11 +1,11 @@ using DV.CabControls.NonVR; using DV.Interaction; using DV.ThingTypes; +using DV.Optimizers; using Multiplayer.Networking.Data; using Multiplayer.Networking.Managers.Server; using Multiplayer.Networking.Packets.Clientbound.World; using Multiplayer.Networking.Packets.Common; -using Multiplayer.Networking.TransportLayers; using Multiplayer.Utils; using System; using System.Collections; diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index a2013bdd..c220b081 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -192,27 +192,27 @@ private void OnLoaded() } } - LogDebug(() => - { - StringBuilder sb = new StringBuilder(); + //LogDebug(() => + //{ + // StringBuilder sb = new StringBuilder(); - var objects = Resources.FindObjectsOfTypeAll(); - foreach (var obj in objects) - sb.AppendLine($"PlayerDistanceGameObjectsDisabler() {obj.gameObject.GetObjectPath()}"); + // var objects = Resources.FindObjectsOfTypeAll(); + // foreach (var obj in objects) + // sb.AppendLine($"PlayerDistanceGameObjectsDisabler() {obj.gameObject.GetObjectPath()}"); - return sb.ToString(); - }); + // return sb.ToString(); + //}); - LogDebug(() => - { - StringBuilder sb = new StringBuilder(); + //LogDebug(() => + //{ + // StringBuilder sb = new StringBuilder(); - var objects = Resources.FindObjectsOfTypeAll(); - foreach (var obj in objects) - sb.AppendLine($"PlayerDistanceMultipleGameObjectsOptimizer() {obj.gameObject.GetObjectPath()}"); + // var objects = Resources.FindObjectsOfTypeAll(); + // foreach (var obj in objects) + // sb.AppendLine($"PlayerDistanceMultipleGameObjectsOptimizer() {obj.gameObject.GetObjectPath()}"); - return sb.ToString(); - }); + // return sb.ToString(); + //}); } public bool TryGetServerPlayer(ITransportPeer peer, out ServerPlayer player) diff --git a/Multiplayer/Patches/Train/TrainsOptimizerPatch.cs b/Multiplayer/Patches/Train/TrainsOptimizerPatch.cs index 1703e530..d37f704e 100644 --- a/Multiplayer/Patches/Train/TrainsOptimizerPatch.cs +++ b/Multiplayer/Patches/Train/TrainsOptimizerPatch.cs @@ -1,10 +1,9 @@ using HarmonyLib; using System; using System.Collections.Generic; -using System.Linq; using System.Text; using DV.Logic.Job; -using DV.Utils; +using DV.Optimizers; namespace Multiplayer.Patches.Train; [HarmonyPatch(typeof(TrainsOptimizer))] diff --git a/Multiplayer/Patches/World/PlayerDistanceGameObjectsDisablerPatch.cs b/Multiplayer/Patches/World/PlayerDistanceGameObjectsDisablerPatch.cs index d8366dab..824d5018 100644 --- a/Multiplayer/Patches/World/PlayerDistanceGameObjectsDisablerPatch.cs +++ b/Multiplayer/Patches/World/PlayerDistanceGameObjectsDisablerPatch.cs @@ -1,3 +1,4 @@ +using DV.Optimizers; using HarmonyLib; using Multiplayer.Components.Networking; using Multiplayer.Utils; @@ -5,7 +6,6 @@ using System.Linq; using System.Reflection; using System.Reflection.Emit; -using System.Text; using UnityEngine; namespace Multiplayer.Patches.World; From 3960533edb52beb8f46e67484529fa43b61121d6 Mon Sep 17 00:00:00 2001 From: Macka Date: Tue, 29 Jul 2025 22:09:43 +1000 Subject: [PATCH 423/521] Fix faucet positioning --- .../World/NetworkedPitStopStation.cs | 189 ++++++++++++------ .../Managers/Server/NetworkServer.cs | 2 +- .../ClientboundPitStopBulkUpdatePacket.cs | 1 + 3 files changed, 133 insertions(+), 59 deletions(-) diff --git a/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs b/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs index b36f0179..fbfd976b 100644 --- a/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs +++ b/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs @@ -14,6 +14,7 @@ using System.Text; using UnityEngine; using static CashRegisterModule; +using DV.CabControls; namespace Multiplayer.Components.Networking.World; @@ -68,12 +69,8 @@ public static void InitialisePitStops() protected override bool IsIdServerAuthoritative => false; - const float MAX_DELTA = 0.2f; - const float MIN_UPDATE_TIME = 0.1f; const float LOADING_TIMEOUT = 5f; - const float ROTATION_SMOOTH_SPEED = 5f; - const float FAUCET_SNAP_THRESHOLD = 0.005f; - + const float ROTATION_SMOOTH_SPEED = 0.5f; const float DEFAULT_DISABLER_SQR_DISTANCE = 250000f; const float DEFAULT_DISABLER_INTERVAL = 2f; const float NEARBY_REMOVAL_DELAY = 3f; @@ -98,6 +95,7 @@ public static void InitialisePitStops() private GrabHandlerHingeJoint carSelectorGrab; private GrabHandlerHingeJoint faucetPositionerGrab; private HingeJointAngleFix faucetPositioner; + private SteppedJoint faucetCrankSteppedJoint; private readonly Dictionary leverHandler)> leverStateLookup = []; private readonly Dictionary grabbedHandlerLookup = []; @@ -115,6 +113,8 @@ public static void InitialisePitStops() private float faucetTargetPercentage = 0.0f; private bool faucetTargetReached = true; + private Coroutine faucetMoveCoroutine; + private bool Refreshed = false; #endregion @@ -206,6 +206,11 @@ protected override void OnDestroy() faucetPositionerGrab.UnGrabbed -= FaucetCrankUnGrabbed; } + if (faucetCrankSteppedJoint != null) + { + faucetCrankSteppedJoint.PositionChanged -= FaucetCrankPositionChanged; + } + foreach (var kvp in leverStateLookup) { var (leverAmplitudeChecker, _, leverStateHandler) = kvp.Value; @@ -218,39 +223,39 @@ protected override void OnDestroy() base.OnDestroy(); } - protected void Update() - { - var deltaTime = Time.time - lastFaucetUpdateTime; - - // Handle faucet movement - if (faucetPositioner && !faucetTargetReached) - { - var currentPercentage = faucetPositioner.Percentage; - float newPercent = Mathf.Lerp(currentPercentage, faucetTargetPercentage, Time.deltaTime * ROTATION_SMOOTH_SPEED); - - //if we're close enough to the target, snap to it - if (Mathf.Abs(currentPercentage - faucetTargetPercentage) < FAUCET_SNAP_THRESHOLD) - newPercent = faucetTargetPercentage; - - SetFaucetRotation(newPercent); - //if we're in snap range we can finalise the rotation - faucetTargetReached = Mathf.Abs(newPercent - faucetTargetPercentage) < FAUCET_SNAP_THRESHOLD; - } - - if (isFaucetGrabbed && (deltaTime > MIN_UPDATE_TIME) && lastFaucetSent != faucetPositioner.Percentage) - { - lastFaucetUpdateTime = Time.time; - - lastFaucetSent = faucetPositioner.Percentage; - NetworkLifecycle.Instance?.Client.SendPitStopInteractionPacket - ( - NetId, - PitStopStationInteractionType.FaucetPosition, - null, - lastFaucetSent - ); - } - } + //protected void Update() + //{ + // var deltaTime = Time.time - lastFaucetUpdateTime; + + // // Handle faucet movement + // if (faucetPositioner && !faucetTargetReached) + // { + // var currentPercentage = faucetPositioner.Percentage; + // float newPercent = Mathf.Lerp(currentPercentage, faucetTargetPercentage, Time.deltaTime * ROTATION_SMOOTH_SPEED); + + // //if we're close enough to the target, snap to it + // if (Mathf.Abs(currentPercentage - faucetTargetPercentage) <= FAUCET_SNAP_THRESHOLD) + // newPercent = faucetTargetPercentage; + + // SetFaucetRotation(newPercent); + // //if we're in snap range we can finalise the rotation + // faucetTargetReached = Mathf.Abs(newPercent - faucetTargetPercentage) <= FAUCET_SNAP_THRESHOLD; + // } + + // if (isFaucetGrabbed && (deltaTime > MIN_UPDATE_TIME) && lastFaucetSent != faucetPositioner.Percentage) + // { + // lastFaucetUpdateTime = Time.time; + + // lastFaucetSent = faucetPositioner.Percentage; + // NetworkLifecycle.Instance?.Client.SendPitStopInteractionPacket + // ( + // NetId, + // PitStopStationInteractionType.FaucetPosition, + // null, + // lastFaucetSent + // ); + // } + //} #endregion #region Server @@ -306,8 +311,12 @@ public void OnPlayerEnteredActivationRegion(ServerPlayer player) i++; } + float faucetPos = 0.0f; + if (faucetPositioner != null) + faucetPos = faucetPositioner.Percentage; + // Send current state - NetworkLifecycle.Instance.Server.SendPitStopBulkDataPacket(NetId, Station.pitstop.carList.Count, carIndex, stateData, plugData, player.Peer); + NetworkLifecycle.Instance.Server.SendPitStopBulkDataPacket(NetId, Station.pitstop.carList.Count, carIndex, faucetPos, stateData, plugData, player.Peer); } } @@ -439,12 +448,14 @@ private IEnumerator Init() var faucetGo = transform.parent.FindChildrenByName("FaucetCrank").FirstOrDefault(); faucetPositionerGrab = faucetGo?.GetComponentInChildren(true); faucetPositioner = faucetGo?.GetComponentInChildren(true); + faucetCrankSteppedJoint = faucetGo?.GetComponentInChildren(true); - if (faucetPositionerGrab != null && faucetPositioner != null) + if (faucetPositionerGrab != null && faucetPositioner != null && faucetCrankSteppedJoint != null) { Multiplayer.LogDebug(() => $"NetworkedPitStopStation.Init() Grab Handler found: {carSelectorGrab != null}, Name: {carSelectorGrab.name}"); faucetPositionerGrab.Grabbed += FaucetCrankGrabbed; faucetPositionerGrab.UnGrabbed += FaucetCrankUnGrabbed; + faucetCrankSteppedJoint.PositionChanged += FaucetCrankPositionChanged; } //build dictionaries @@ -537,6 +548,7 @@ private IEnumerator Init() initialised = true; } + /// /// Waits for all pitstop components to complete loading before processing the bulk update /// @@ -616,24 +628,58 @@ private void InitialiseData() /// /// Sets the rotation of the faucet handle to the specified percentage /// - public void SetFaucetRotation(float percentage) + public void SetFaucetRotation(int notch) { if (faucetPositioner == null) return; - float targetAngle = faucetPositioner.angleOffset + (percentage * faucetPositioner.angleRange); + if (faucetMoveCoroutine != null) + StopCoroutine(faucetMoveCoroutine); + + faucetMoveCoroutine = StartCoroutine(SmoothMoveToNotch(notch)); + } + + private IEnumerator SmoothMoveToNotch(int targetNotch) + { + float min = faucetCrankSteppedJoint.joint.limits.min; + float max = faucetCrankSteppedJoint.joint.limits.max; + + float startAngle = faucetCrankSteppedJoint.jointAngleFix.Angle; + float endAngle = faucetCrankSteppedJoint.AngleForNotch(targetNotch); + float elapsed = 0f; + + //Multiplayer.LogDebug(() => $"NetworkedPitStopStation.SmoothMoveToNotch() targetNotch: {targetNotch}, startAngle: {startAngle}, endAngle: {endAngle}"); + + targetNotch = Mathf.Clamp(targetNotch, 0, faucetCrankSteppedJoint.notches - 1); - Vector3 axis = faucetPositioner.joint.axis; + while (faucetCrankSteppedJoint.currentNotch != targetNotch && elapsed < 2f) + { + //Multiplayer.LogDebug(() => $"NetworkedPitStopStation.SmoothMoveToNotch() targetNotch: {targetNotch}, currentNotch: {faucetCrankSteppedJoint.currentNotch}"); + elapsed += Time.deltaTime; + float t = Mathf.Clamp01(elapsed / ROTATION_SMOOTH_SPEED); + float newAngle = Mathf.Lerp(startAngle, endAngle, t); + + //Multiplayer.LogDebug(() => $"NetworkedPitStopStation.SmoothMoveToNotch() targetNotch: {targetNotch}, startAngle: {startAngle}, endAngle: {endAngle}, newAngleUnclamped: {newAngle}"); + + newAngle = Mathf.Clamp(newAngle, min, max); + + //Multiplayer.LogDebug(() => $"NetworkedPitStopStation.SmoothMoveToNotch() targetNotch: {targetNotch}, startAngle: {startAngle}, endAngle: {endAngle}, newAngleClamped: {newAngle}"); - // Create a rotation around that axis by the target angle - Quaternion rotationDelta = Quaternion.AngleAxis(targetAngle, axis); + var spring = faucetCrankSteppedJoint.joint.spring; + spring.targetPosition = newAngle; + faucetCrankSteppedJoint.joint.spring = spring; + + //Multiplayer.LogDebug(() => $"NetworkedPitStopStation.SmoothMoveToNotch()targetNotch: {targetNotch}, newAngle: {newAngle}, t: {t}, elapsed: {elapsed}"); + + yield return null; + } - // Calculate the final target rotation - Quaternion targetRotation = Quaternion.Inverse(faucetPositioner.startRotationInverse) * rotationDelta; + yield return null; - faucetPositioner.transform.localRotation = targetRotation; + //Multiplayer.LogDebug(() => $"NetworkedPitStopStation.SmoothMoveToNotch() Finished moving to notch: {targetNotch}, final angle: {faucetCrankSteppedJoint.jointAngleFix.Angle}"); } + /// /// Set the car selection index /// @@ -762,6 +808,18 @@ private void FaucetCrankUnGrabbed() NetworkLifecycle.Instance?.Client.SendPitStopInteractionPacket(NetId, PitStopStationInteractionType.FaucetUngrab, null, lastFaucetSent); } + private void FaucetCrankPositionChanged(ValueChangedEventArgs args) + { + Multiplayer.LogDebug(() => $"FaucetCrankPositionChanged() {StationName}, oldValue: {args.oldValue}, newValue: {args.newValue}, delta: {args.delta}"); + + if (NetworkLifecycle.Instance.IsProcessingPacket || (NetworkLifecycle.Instance.IsHost() && processingAsHost)) + return; + + //Prevent new players/players entering the area from sending packets until initalised + if (!Refreshed) + return; + } + public void ProcessBulkUpdate(ClientboundPitStopBulkUpdatePacket packet) { if (!initialised || Station?.pitstop?.carList == null || Station.pitstop.carList.Count < packet.CarCount) @@ -795,14 +853,11 @@ public void ProcessBulkUpdate(ClientboundPitStopBulkUpdatePacket packet) { module.resourceData[i].unitsToBuy = resource.Values[i]; } - } else { - Multiplayer.LogWarning($"PitStop bulk data count mismatch post-force: {module.resourceData.Count} != {resource.Values.Count()}"); } - } else Multiplayer.LogWarning($"PitStop module not found for resource type: {resource.ResourceType}"); @@ -851,6 +906,19 @@ public void ProcessBulkUpdate(ClientboundPitStopBulkUpdatePacket packet) netPlug?.ProcessBulkUpdate(plug); } + //sync faucet position + if (faucetPositioner != null) + SetFaucetRotation(packet.FaucetNotch); + + //DV.CabControls.Spec.Lever; + //HingeJointDrivenTransformAdjuster; + //DV.CabControls.NonVR.LeverNonVR; + //DV.CabControls.VRTK.LeverVRTK; + //HingeJointAngleFix; + //UnityEngine.HingeJoint; + //DV.Interaction.GrabHandlerHingeJoint; + //SteppedJoint; + // Mark data as refreshed to allow player interactions Refreshed = true; @@ -877,16 +945,21 @@ public void ProcessInteractionPacketAsClient(CommonPitStopInteractionPacket pack PitStopStationInteractionType interactionType = (PitStopStationInteractionType)packet.InteractionType; - bool isCarSelection = interactionType switch + bool isResourceSelection = interactionType switch { - PitStopStationInteractionType.CarSelectorGrab => true, - PitStopStationInteractionType.CarSelectorUngrab => true, - PitStopStationInteractionType.CarSelection => true, - _ => false, + PitStopStationInteractionType.CarSelectorGrab => false, + PitStopStationInteractionType.CarSelectorUngrab => false, + PitStopStationInteractionType.CarSelection => false, + + PitStopStationInteractionType.FaucetGrab => false, + PitStopStationInteractionType.FaucetUngrab => false, + PitStopStationInteractionType.FaucetPosition => false, + + _ => true, }; // Validate resource type (no resource type for car selectors - if (!isCarSelection && !Enum.IsDefined(typeof(ResourceType), packet.ResourceType)) + if (isResourceSelection && !Enum.IsDefined(typeof(ResourceType), packet.ResourceType)) { Multiplayer.LogWarning($"Received invalid ResourceType \"{packet.ResourceType}\" at Pit Stop station {StationName}"); return; @@ -897,7 +970,7 @@ public void ProcessInteractionPacketAsClient(CommonPitStopInteractionPacket pack Multiplayer.LogDebug(() => $"NetworkedPitStopStation.ProcessPacket() [{StationName}, {NetId}] {interactionType}, resource type: {resourceType}, state: {packet.Value}"); // Validate resource module exists - if (!isCarSelection && !resourceTypeToLocoResourceModule.TryGetValue(resourceType, out resourceModule)) + if (isResourceSelection && !resourceTypeToLocoResourceModule.TryGetValue(resourceType, out resourceModule)) { Multiplayer.LogWarning($"Could not find LocoResourceModule for ResourceType \"{resourceType}\" at Pit Stop station {StationName}"); return; diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index c220b081..c813c6f4 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -599,7 +599,7 @@ public void SendItemsChangePacket(List items, ServerPlayer playe } } - public void SendPitStopBulkDataPacket(ushort netId, int carCount, int carIndex, LocoResourceModuleData[] stationData, PitStopPlugData[] plugData , ITransportPeer peer = null) + public void SendPitStopBulkDataPacket(ushort netId, int carCount, int carIndex, float faucetPos, LocoResourceModuleData[] stationData, PitStopPlugData[] plugData , ITransportPeer peer = null) { LogDebug(() => $"SendPitStopBulkDataPacket({netId}, {stationData.Count()}, {plugData.Count()}, {peer?.Id})"); diff --git a/Multiplayer/Networking/Packets/Clientbound/World/ClientboundPitStopBulkUpdatePacket.cs b/Multiplayer/Networking/Packets/Clientbound/World/ClientboundPitStopBulkUpdatePacket.cs index 22a90740..0043253f 100644 --- a/Multiplayer/Networking/Packets/Clientbound/World/ClientboundPitStopBulkUpdatePacket.cs +++ b/Multiplayer/Networking/Packets/Clientbound/World/ClientboundPitStopBulkUpdatePacket.cs @@ -11,6 +11,7 @@ public class ClientboundPitStopBulkUpdatePacket public ushort NetId { get; set; } public int CarCount { get; set; } public int CarSelection { get; set; } + public int FaucetNotch { get; set; } public LocoResourceModuleData[] ResourceData { get; set; } public PitStopPlugData[] PlugData { get; set; } } From 8e0f1405aef618abb21cf3b208b6e08106f6a3a1 Mon Sep 17 00:00:00 2001 From: Macka Date: Tue, 29 Jul 2025 22:10:57 +1000 Subject: [PATCH 424/521] Rework PaintThemeLookup --- Multiplayer/API/APIProvider.cs | 33 +++++++++++++++++++ .../Networking/Train/PaintThemeLookup.cs | 18 ++++++++-- MultiplayerAPI/Interfaces/IMultiplayerAPI.cs | 16 +++++++++ 3 files changed, 65 insertions(+), 2 deletions(-) diff --git a/Multiplayer/API/APIProvider.cs b/Multiplayer/API/APIProvider.cs index b379fdf1..75e471f3 100644 --- a/Multiplayer/API/APIProvider.cs +++ b/Multiplayer/API/APIProvider.cs @@ -1,6 +1,7 @@ using MPAPI.Interfaces; using MPAPI.Types; using Multiplayer.Components.Networking; +using Multiplayer.Components.Networking.Train; using System; namespace Multiplayer.API; @@ -42,6 +43,38 @@ public void SetModCompatibility(string modId, MultiplayerCompatibility compatibi ModCompatibilityManager.Instance.RegisterCompatibility(modId, compatibility); } + public uint RegisterPaintTheme(string assetName) + { + if (string.IsNullOrEmpty(assetName)) + { + Multiplayer.LogWarning("APIProvider.RegisterPaintTheme() called with empty assetName"); + return 0; + } + + if (!NetworkLifecycle.Instance.IsServerRunning || !NetworkLifecycle.Instance.IsClientRunning) + { + Multiplayer.LogWarning("APIProvider.RegisterPaintTheme() called when server or client is not running"); + return 0; + } + + return PaintThemeLookup.Instance.RegisterTheme(assetName); + } + + public void UnregisterPaintTheme(uint themeId) + { + if (themeId == 0) + { + Multiplayer.LogWarning("APIProvider.UnregisterPaintTheme() called with themeId 0"); + return; + } + + if (!NetworkLifecycle.Instance.IsServerRunning || !NetworkLifecycle.Instance.IsClientRunning) + { + Multiplayer.LogWarning("APIProvider.UnregisterPaintTheme() called when server or client is not running"); + return; + } + } + #region Class Helpers internal APIProvider() diff --git a/Multiplayer/Components/Networking/Train/PaintThemeLookup.cs b/Multiplayer/Components/Networking/Train/PaintThemeLookup.cs index 3938084b..c74d2f09 100644 --- a/Multiplayer/Components/Networking/Train/PaintThemeLookup.cs +++ b/Multiplayer/Components/Networking/Train/PaintThemeLookup.cs @@ -1,9 +1,9 @@ using DV.Customization.Paint; using DV.Utils; +using JetBrains.Annotations; using System.Collections.Generic; using System.Linq; using UnityEngine; -using JetBrains.Annotations; namespace Multiplayer.Components.Networking.Train; @@ -11,6 +11,7 @@ namespace Multiplayer.Components.Networking.Train; public class PaintThemeLookup : SingletonBehaviour { private readonly Dictionary hashToThemeName = []; + private readonly Dictionary hashToBaseThemeName = []; protected override void Awake() { @@ -20,7 +21,11 @@ protected override void Awake() .ToArray(); foreach (var themeName in themeNames) - RegisterTheme(themeName); + { + var id = RegisterTheme(themeName); + if (id != 0) + hashToBaseThemeName[id] = themeName; + } } public PaintTheme GetPaintTheme(uint themeId) @@ -52,6 +57,9 @@ public uint GetThemeId(PaintTheme theme) public uint GetThemeId(string themeName) { + if (string.IsNullOrEmpty(themeName)) + return 0; + return Fnv1aHash(themeName); } @@ -79,6 +87,12 @@ public void UnregisterTheme(string themeName) { var hash = GetThemeId(themeName); + if(hashToBaseThemeName.ContainsKey(hash)) + { + Multiplayer.LogWarning($"Tried to unregister a base-game theme '{themeName}'."); + return; + } + if (hashToThemeName.TryGetValue(hash, out _)) { hashToThemeName.Remove(hash); diff --git a/MultiplayerAPI/Interfaces/IMultiplayerAPI.cs b/MultiplayerAPI/Interfaces/IMultiplayerAPI.cs index 75d3db93..714dad33 100644 --- a/MultiplayerAPI/Interfaces/IMultiplayerAPI.cs +++ b/MultiplayerAPI/Interfaces/IMultiplayerAPI.cs @@ -86,4 +86,20 @@ public interface IMultiplayerAPI /// True if the object was found; otherwise, false bool TryGetObjectFromNetId(ushort netId, out T obj) where T : class; + /// + /// Registers a PaintTheme and returns its ID + /// + /// The string representing the `PaintTheme.AssetName` + /// Non-zero, unique Id if the theme was successfully registered, otherwise 0 + /// PaintThemes must be registered each time the client or server starts, registration is not persistent across sessions. + uint RegisterPaintTheme(string assetName); + + /// + /// Unregisters a PaintTheme + /// + /// The Id of the PaintTheme to be unregistered + void UnregisterPaintTheme(uint themeId); + + /// + /// Returns } From 67f6d6b5b9d3bdb605f6e3cd4b44e7867186beec Mon Sep 17 00:00:00 2001 From: Macka Date: Sun, 10 Aug 2025 22:13:08 +1000 Subject: [PATCH 425/521] Fix issue with clients clearing cash registers --- .../World/CashRegisterWithModulesPatch.cs | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/Multiplayer/Patches/World/CashRegisterWithModulesPatch.cs b/Multiplayer/Patches/World/CashRegisterWithModulesPatch.cs index dd9f9e27..250c47b6 100644 --- a/Multiplayer/Patches/World/CashRegisterWithModulesPatch.cs +++ b/Multiplayer/Patches/World/CashRegisterWithModulesPatch.cs @@ -11,6 +11,20 @@ namespace Multiplayer.Patches.World; [HarmonyPatch(typeof(CashRegisterWithModules))] public class CashRegisterWithModulesPatch { + [HarmonyPrefix] + [HarmonyPatch(nameof(CashRegisterWithModules.OnDisable))] + private static bool OnDisable(CashRegisterWithModules __instance) + { + //Multiplayer.LogDebug(() => $"CashRegisterWithModules.OnDisable({__instance.GetObjectPath()})"); + + __instance.StopAllCoroutines(); + __instance.textController.Clear(); + __instance.SetupListeners(false); + + // Prevent clients from cancelling/returning cash on cash registers when loading the game or leaving the area + return NetworkLifecycle.Instance.IsHost(); + } + [HarmonyPrefix] [HarmonyPatch(nameof(CashRegisterWithModules.OnBuyPressed))] private static bool OnBuyPressed(CashRegisterWithModules __instance) @@ -76,6 +90,7 @@ private static bool Cancel(CashRegisterWithModules __instance) [HarmonyPatch(nameof(CashRegisterWithModules.Cancel))] private static void Cancel_Postfix(CashRegisterWithModules __instance) { + //Multiplayer.LogWarning($"CashRegisterWithModules.Cancel_Postfix({__instance.GetObjectPath()})"); if (!NetworkLifecycle.Instance.IsHost()) return; @@ -117,10 +132,11 @@ private static void SetCash(CashRegisterBase __instance, double amount) [HarmonyPatch(nameof(CashRegisterBase.OnEnable))] private static bool OnEnable(CashRegisterBase __instance) { + //Multiplayer.LogDebug(() => $"CashRegisterBase.OnEnable({__instance.GetObjectPath()}) {__instance.GetType()}"); if (__instance is not CashRegisterWithModules) return true; - //prevent clients from clearing cash registers when loading + // Prevent clients from cancelling/returning cash on cash registers when loading the game or leaving the area return NetworkLifecycle.Instance.IsHost(); } @@ -128,10 +144,11 @@ private static bool OnEnable(CashRegisterBase __instance) [HarmonyPatch(nameof(CashRegisterBase.OnDisable))] private static bool OnDisable(CashRegisterBase __instance) { + //Multiplayer.LogDebug(() => $"CashRegisterBase.OnDisable({__instance.GetObjectPath()}) {__instance.GetType()}"); if (__instance is not CashRegisterWithModules) return true; - //prevent clients from clearing cash registers when loading the game or leaving the area + // Prevent clients from cancelling/returning cash on cash registers when loading the game or leaving the area __instance.StopAllCoroutines(); return NetworkLifecycle.Instance.IsHost(); } From 821f6cce6f02d20af59a7f723f5a1bbc66995038 Mon Sep 17 00:00:00 2001 From: Macka Date: Sun, 10 Aug 2025 22:36:19 +1000 Subject: [PATCH 426/521] Rework BulkUpdate processing Fix issues with faucet position, cash register update and data corruption --- .../World/NetworkedPitStopStation.cs | 300 +++++++++--------- .../Networking/Data/LocoResourceModuleData.cs | 2 +- .../Managers/Client/NetworkClient.cs | 4 +- .../Managers/Server/NetworkServer.cs | 5 +- 4 files changed, 158 insertions(+), 153 deletions(-) diff --git a/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs b/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs index fbfd976b..62fda669 100644 --- a/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs +++ b/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs @@ -1,4 +1,6 @@ +using DV.CabControls; using DV.CabControls.NonVR; +using DV.CashRegister; using DV.Interaction; using DV.ThingTypes; using DV.Optimizers; @@ -14,7 +16,6 @@ using System.Text; using UnityEngine; using static CashRegisterModule; -using DV.CabControls; namespace Multiplayer.Components.Networking.World; @@ -90,6 +91,8 @@ public static void InitialisePitStops() private bool initialised = false; + private CashRegisterWithModules register; + private ResourceType[] resourceTypes = []; private GrabHandlerHingeJoint carSelectorGrab; @@ -107,10 +110,6 @@ public static void InitialisePitStops() private readonly Dictionary isResourceRemoteGrabbedDict = []; private readonly Dictionary lastRemoteValueDict = []; - private bool isFaucetGrabbed = false; - private float lastFaucetUpdateTime = 0.0f; - private float lastFaucetSent = 0.0f; - private float faucetTargetPercentage = 0.0f; private bool faucetTargetReached = true; private Coroutine faucetMoveCoroutine; @@ -128,6 +127,7 @@ protected override void Awake() if (NetworkLifecycle.Instance.IsHost()) { + // Setup culling var disabler = GetComponentInParent(); var cullingSqrDistance = DEFAULT_DISABLER_SQR_DISTANCE; @@ -145,11 +145,12 @@ protected override void Awake() CullingManager.PlayerEnteredActivationRegion += OnPlayerEnteredActivationRegion; CullingManager.PlayerEnteredCullingRegion += OnPlayerEnteredCullingRegion; + // Setup network events NetworkLifecycle.Instance.OnTick += OnTick; NetworkLifecycle.Instance.Server.PlayerDisconnect += OnPlayerDisconnect; - //ensure host can interact + // Ensure host can interact Refreshed = true; } } @@ -222,40 +223,6 @@ protected override void OnDestroy() leverLookup.Clear(); base.OnDestroy(); } - - //protected void Update() - //{ - // var deltaTime = Time.time - lastFaucetUpdateTime; - - // // Handle faucet movement - // if (faucetPositioner && !faucetTargetReached) - // { - // var currentPercentage = faucetPositioner.Percentage; - // float newPercent = Mathf.Lerp(currentPercentage, faucetTargetPercentage, Time.deltaTime * ROTATION_SMOOTH_SPEED); - - // //if we're close enough to the target, snap to it - // if (Mathf.Abs(currentPercentage - faucetTargetPercentage) <= FAUCET_SNAP_THRESHOLD) - // newPercent = faucetTargetPercentage; - - // SetFaucetRotation(newPercent); - // //if we're in snap range we can finalise the rotation - // faucetTargetReached = Mathf.Abs(newPercent - faucetTargetPercentage) <= FAUCET_SNAP_THRESHOLD; - // } - - // if (isFaucetGrabbed && (deltaTime > MIN_UPDATE_TIME) && lastFaucetSent != faucetPositioner.Percentage) - // { - // lastFaucetUpdateTime = Time.time; - - // lastFaucetSent = faucetPositioner.Percentage; - // NetworkLifecycle.Instance?.Client.SendPitStopInteractionPacket - // ( - // NetId, - // PitStopStationInteractionType.FaucetPosition, - // null, - // lastFaucetSent - // ); - // } - //} #endregion #region Server @@ -290,12 +257,14 @@ public void OnPlayerEnteredActivationRegion(ServerPlayer player) InitialiseData(); // One struct per module type - var resourceCount = Station.locoResourceModules.resourceModules.Count(); + int resourceCount = Station.locoResourceModules.resourceModules.Length; LocoResourceModuleData[] stateData = new LocoResourceModuleData[resourceCount]; + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.OnPlayerEnteredActivationRegion() [{StationName}, {NetId}] player: {player.Username}, car count: {Station.pitstop.carList.Count}, resourceCount: {resourceCount}"); int i; for (i = 0; i < resourceCount; i++) { + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.OnPlayerEnteredActivationRegion() [{StationName}, {NetId}] player: {player.Username}, i: {i}, data count: {Station.locoResourceModules.resourceModules[i].resourceData.Count}"); stateData[i] = LocoResourceModuleData.From(Station.locoResourceModules.resourceModules[i]); } @@ -307,13 +276,22 @@ public void OnPlayerEnteredActivationRegion(ServerPlayer player) i = 0; foreach (var plug in resourceToPluggableObject) { + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.OnPlayerEnteredActivationRegion() [{StationName}, {NetId}] player: {player.Username}, plug: {plug.Key}, plug netId: {plug.Value.NetId}"); plugData[i] = PitStopPlugData.From(plug.Value, true); i++; } - float faucetPos = 0.0f; - if (faucetPositioner != null) - faucetPos = faucetPositioner.Percentage; + int faucetPos = -1; + if (faucetCrankSteppedJoint != null) + { + faucetPos = faucetCrankSteppedJoint.currentNotch; + } + else + { + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.OnPlayerEnteredActivationRegion() [{StationName}] faucetCrankSteppedJoint is null"); + } + + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.OnPlayerEnteredActivationRegion() [{StationName}] faucetPos: {faucetPos}"); // Send current state NetworkLifecycle.Instance.Server.SendPitStopBulkDataPacket(NetId, Station.pitstop.carList.Count, carIndex, faucetPos, stateData, plugData, player.Peer); @@ -341,7 +319,7 @@ public void ProcessInteractionPacketAsHost(CommonPitStopInteractionPacket packet } processingAsHost = false; - //Send to all other players + // Send to all other players foreach (var player in CullingManager.ActivePlayers) { if (player.Id != senderPlayer.Id) @@ -354,7 +332,7 @@ public void ProcessInteractionPacketAsHost(CommonPitStopInteractionPacket packet else { Multiplayer.LogDebug(() => $"NetworkedPitStopStationProcessInteractionPacketAsHost() failed validation"); - //Failed to validate, player needs to rollback interaction + // Failed to validate, player needs to rollback interaction NetworkLifecycle.Instance.Server.SendPitStopInteractionPacket( senderPlayer, new CommonPitStopInteractionPacket @@ -368,11 +346,14 @@ public void ProcessInteractionPacketAsHost(CommonPitStopInteractionPacket packet private void OnFlowStarted(LocoResourceModule module) { + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.OnFlowStarted() {module.resourceType} [{StationName}, {NetId}]"); resourceFlowing[module] = true; } private void OnFlowStopped(LocoResourceModule module) { + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.OnFlowStopped() {module.resourceType} [{StationName}, {NetId}]"); + resourceFlowing[module] = false; SendResourceUpdate(module); } @@ -392,6 +373,8 @@ private void OnTick(uint tick) private void SendResourceUpdate(LocoResourceModule module) { + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.SendResourceUpdate({module.resourceType}) [{StationName}, {NetId}], active players: {CullingManager.ActivePlayers.Count}"); + CommonPitStopInteractionPacket packet = new() { NetId = this.NetId, @@ -404,9 +387,13 @@ private void SendResourceUpdate(LocoResourceModule module) { if (player != null) { - Multiplayer.LogDebug(() => $"NetworkedPitStopStation.SendResourceUpdate({module.resourceType}) sending to peer: {player.Username}, value: {module.Data.unitsToBuy}, flowing: {module.IsFlowing}"); + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.SendResourceUpdate({module.resourceType}) [{StationName}, {NetId}], sending to peer: {player.Username}, value: {module.Data.unitsToBuy}, flowing: {module.IsFlowing}"); NetworkLifecycle.Instance.Server.SendPitStopInteractionPacket(player, packet); } + else + { + Multiplayer.LogWarning(() => $"NetworkedPitStopStation.SendResourceUpdate({module.resourceType}) [{StationName}, {NetId}], player is null, skipping send"); + } } } #endregion @@ -431,6 +418,12 @@ private IEnumerator Init() while (Station?.pitstop == null) yield return new WaitForEndOfFrame(); + register = transform.parent.GetComponentInChildren(true); + if (register == null) + { + Multiplayer.LogWarning($"NetworkedPitStopStation.Init() No CashRegisterWithModules found for station {StationName}"); + } + //Wait for levers an knobs to load yield return new WaitUntil(() => GetComponentInChildren(true) != null); carSelectorGrab = GetComponentInChildren(true); @@ -476,7 +469,7 @@ private IEnumerator Init() } StringBuilder sb = new(); - sb.AppendLine($"NetworkedPitStopStation.Awake() {StationName} resources:"); + sb.AppendLine($"NetworkedPitStopStation.Init() {StationName} resources:"); if (resourceModules != null) { @@ -548,53 +541,6 @@ private IEnumerator Init() initialised = true; } - - /// - /// Waits for all pitstop components to complete loading before processing the bulk update - /// - private IEnumerator WaitForLoad(ClientboundPitStopBulkUpdatePacket packet) - { - float time = Time.time; - - yield return new WaitUntil - ( - () => - { - Multiplayer.LogDebug(() => $"NetworkedPitStopStation.WaitForLoad() PitStop [{StationName}] PitStop Initialised: {initialised}, PitStop Active:{Station?.gameObject?.activeInHierarchy}, Packet Car Count: {packet.CarCount}, Station Car Count: {Station.pitstop?.carList?.Count}, Car Count Matched: {packet.CarCount == Station.pitstop?.carList?.Count}, time elapsed: {(Time.time - time)}"); - - if (Station?.gameObject?.activeInHierarchy == false) - { - //don't time out if we're waiting for the object to be enabled - time = Time.time; - return false; - } - - //try to trigger colliders manually - if (initialised && Station?.pitstop?.carList != null && packet.CarCount != Station.pitstop.carList.Count) - Station?.pitstop?.RefreshPitStopCarPresence(); - - return (initialised && Station?.pitstop?.carList != null && packet.CarCount == Station.pitstop.carList.Count) - || (Time.time - time) > LOADING_TIMEOUT; - - } - ); - - - yield return new WaitForEndOfFrame(); - yield return new WaitForEndOfFrame(); - - if ((Time.time - time) <= LOADING_TIMEOUT) - { - ProcessBulkUpdate(packet); - } - else - { - Multiplayer.LogWarning($"PitStop [{StationName}] timed out waiting for load. PitStop Initialised: {initialised}, Packet Car Count: {packet.CarCount}, Station Car Count: {Station.pitstop?.carList?.Count}, Car Count Matched: {packet.CarCount == Station.pitstop?.carList?.Count}"); - if (initialised) - Refreshed = true; //lets hope the car sync is just a little slow - } - } - private void SetUnits(LocoResourceModule rm, float units) { if (rm == null) @@ -636,6 +582,8 @@ public void SetFaucetRotation(int notch) if (faucetMoveCoroutine != null) StopCoroutine(faucetMoveCoroutine); + faucetTargetReached = false; + faucetMoveCoroutine = StartCoroutine(SmoothMoveToNotch(notch)); } @@ -676,6 +624,8 @@ private IEnumerator SmoothMoveToNotch(int targetNotch) yield return null; + faucetTargetReached = true; + //Multiplayer.LogDebug(() => $"NetworkedPitStopStation.SmoothMoveToNotch() Finished moving to notch: {targetNotch}, final angle: {faucetCrankSteppedJoint.jointAngleFix.Angle}"); } @@ -685,6 +635,7 @@ private IEnumerator SmoothMoveToNotch(int targetNotch) /// public void SetCarSelection(int selection) { + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.SetCarSelection({selection}) [{StationName}, {NetId}] car count: {Station.pitstop.carList.Count}"); if (selection >= 0 && selection < Station.pitstop.carList.Count) { Station.pitstop.currentCarIndex = selection; @@ -736,10 +687,12 @@ private void CarSelectorUnGrabbed() /// private void CarSelected() { + Multiplayer.LogDebug(() => $"CarSelected() [{StationName}, {NetId}] selected: {Station.pitstop.SelectedIndex}"); + if (NetworkLifecycle.Instance.IsProcessingPacket || (NetworkLifecycle.Instance.IsHost() && processingAsHost)) return; - //Prevent new players/players entering the area from sending packets until initalised + // Prevent new players/players entering the area from sending packets until initalised if (!Refreshed) return; @@ -754,7 +707,7 @@ private void CarSelected() /// The resource module being grabbed. private void OnLeverPositionChange(LocoResourceModule module, int state) { - //Prevent new players/players entering the area from sending packets until initalised + // Prevent new players/players entering the area from sending packets until initalised if (!Refreshed) return; @@ -786,12 +739,18 @@ private void FaucetCrankGrabbed() return; Multiplayer.LogDebug(() => $"FaucetCrankGrabbed() {StationName}"); - isFaucetGrabbed = true; - NetworkLifecycle.Instance?.Client.SendPitStopInteractionPacket(NetId, PitStopStationInteractionType.FaucetGrab, null, 0); + + int notch = -1; + if (faucetCrankSteppedJoint != null) + { + notch = faucetCrankSteppedJoint.currentNotch; + } + + NetworkLifecycle.Instance?.Client.SendPitStopInteractionPacket(NetId, PitStopStationInteractionType.FaucetGrab, null, notch); } /// - /// Handles end of grab (release) interactions for the car selector knob. + /// Handles end of grab (release) interactions for the faucet positioning handle (water towers). /// private void FaucetCrankUnGrabbed() { @@ -803,11 +762,17 @@ private void FaucetCrankUnGrabbed() return; Multiplayer.LogDebug(() => $"FaucetCrankUnGrabbed() {StationName}, percentage: {faucetPositioner.Percentage}"); - isFaucetGrabbed = false; - lastFaucetSent = faucetPositioner.Percentage; - NetworkLifecycle.Instance?.Client.SendPitStopInteractionPacket(NetId, PitStopStationInteractionType.FaucetUngrab, null, lastFaucetSent); + + int notch = -1; + if (faucetCrankSteppedJoint != null) + notch = faucetCrankSteppedJoint.currentNotch; + + NetworkLifecycle.Instance?.Client.SendPitStopInteractionPacket(NetId, PitStopStationInteractionType.FaucetUngrab, null, notch); } + /// + /// Handles non-grab changes to the faucet positioning handle (water towers), e.g. scrolling. + /// private void FaucetCrankPositionChanged(ValueChangedEventArgs args) { Multiplayer.LogDebug(() => $"FaucetCrankPositionChanged() {StationName}, oldValue: {args.oldValue}, newValue: {args.newValue}, delta: {args.delta}"); @@ -818,36 +783,83 @@ private void FaucetCrankPositionChanged(ValueChangedEventArgs args) //Prevent new players/players entering the area from sending packets until initalised if (!Refreshed) return; - } - public void ProcessBulkUpdate(ClientboundPitStopBulkUpdatePacket packet) - { - if (!initialised || Station?.pitstop?.carList == null || Station.pitstop.carList.Count < packet.CarCount) + if (!faucetTargetReached) { - // Allow pitstop to complete loading and cars to load in the pitstop - Multiplayer.Log($"PitStop [{StationName}] waiting for load"); - CoroutineManager.Instance.StartCoroutine(WaitForLoad(packet)); + Multiplayer.LogDebug(() => $"FaucetCrankPositionChanged() {StationName} faucet target not reached, ignoring position change"); return; } - Multiplayer.LogDebug(() => $"ProcessBulkUpdate() car count: {packet.CarCount}, resource data count: {packet.ResourceData.Count()}, resource data: [{string.Join(", ", packet.ResourceData.Select(x => $"{x.ResourceType}: {{{string.Join(", ", x.Values)}}}"))}]"); + int notch = -1; + if (faucetCrankSteppedJoint != null) + { + notch = faucetCrankSteppedJoint.currentNotch; + } + + NetworkLifecycle.Instance?.Client.SendPitStopInteractionPacket(NetId, PitStopStationInteractionType.FaucetPosition, null, notch); + } + + public void ProcessBulkUpdate(ClientboundPitStopBulkUpdatePacket packet) + { + // Packet is broken up due to SubscribeResusable reusing/overwriting packet data + CoroutineManager.Instance.StartCoroutine(ProcessBulkUpdate_Internal(packet.CarCount, packet.CarSelection,packet.FaucetNotch,packet.ResourceData, packet.PlugData)); + } + + private IEnumerator ProcessBulkUpdate_Internal(int carCount, int carSelection, int faucetNotch, LocoResourceModuleData[] resourceData, PitStopPlugData[] plugData) + { + float time = Time.time; + + // Allow pit stop to complete loading and cars to load in the pit stop + Multiplayer.Log($"ProcessBulkUpdate_Internal() [{StationName}, {NetId}] waiting for load"); + + yield return new WaitUntil + ( + () => + { + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.ProcessBulkUpdate_Internal() [{StationName}, {NetId}] Initialised: {initialised}, Active:{Station?.gameObject?.activeInHierarchy}, Packet Car Count: {carCount}, Station Car Count: {Station.pitstop?.carList?.Count}, Car Count Matched: {carCount == Station.pitstop?.carList?.Count}, time elapsed: {(Time.time - time)}"); + + if (Station?.gameObject?.activeInHierarchy == false) + { + // Don't time out if we're waiting for the object to be enabled + time = Time.time; + return false; + } + + // Try to trigger colliders manually + if (initialised && Station?.pitstop?.carList != null && carCount != Station.pitstop.carList.Count) + Station?.pitstop?.RefreshPitStopCarPresence(); + + return (initialised && Station?.pitstop?.carList != null && carCount == Station.pitstop.carList.Count) + || (Time.time - time) > LOADING_TIMEOUT; + + } + ); + + + yield return new WaitForEndOfFrame(); + yield return new WaitForEndOfFrame(); + + if ((Time.time - time) > LOADING_TIMEOUT) + Multiplayer.LogWarning($"PitStop [{StationName}] timed out waiting for load. PitStop Initialised: {initialised}, Packet Car Count: {carCount}, Station Car Count: {Station.pitstop?.carList?.Count}, Car Count Matched: {carCount == Station.pitstop?.carList?.Count}"); + + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.ProcessBulkUpdate_Internal() [{StationName}] Car count: {carCount}, resource data count: {resourceData.Count()}, resource data: [{string.Join(", ", resourceData.Select(x => $"{x.ResourceType}: {{{string.Join(", ", x.Values)}}}"))}]"); // Make sure the data elements exist prior to attempting to load them InitialiseData(); - Multiplayer.LogDebug(() => $"PitStop bulk data car count matches. Station module count: {Station?.locoResourceModules?.resourceModules?.Count()}, Packet resource count: {packet?.ResourceData?.Count()}"); + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.ProcessBulkUpdate_Internal() [{StationName}] PitStop bulk data car count matches. Station module count: {Station?.locoResourceModules?.resourceModules?.Count()}, Packet resource count: {resourceData?.Count()}"); // Load the data for each car and resource module - foreach (var resource in packet.ResourceData) + foreach (var resource in resourceData) { if (!resourceTypeToLocoResourceModule.TryGetValue(resource.ResourceType, out var module)) { - Multiplayer.LogDebug(() => $"ProcessBulkUpdate() Failed to find resource module for type {resource.ResourceType}"); + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.ProcessBulkUpdate_Internal() [{StationName}, {NetId}] Failed to find resource module for type {resource.ResourceType}"); continue; } if (module != null) { - if (module.resourceData.Count == resource.Values.Count()) + if (module.resourceData.Count == resource.Values.Length) { for (int i = 0; i < module.resourceData.Count; i++) { @@ -862,7 +874,7 @@ public void ProcessBulkUpdate(ClientboundPitStopBulkUpdatePacket packet) else Multiplayer.LogWarning($"PitStop module not found for resource type: {resource.ResourceType}"); - //set the grab state + // Set the grab state bool grabbed = (resource.FillingState != LocoResourceModuleFillingState.None); bool isLocallyGrabbed = isResourceGrabbedDict.TryGetValue(resource.ResourceType, out var localGrabbed) && localGrabbed; @@ -891,38 +903,39 @@ public void ProcessBulkUpdate(ClientboundPitStopBulkUpdatePacket packet) isResourceRemoteGrabbedDict[resource.ResourceType] = grabbed; } - Multiplayer.LogDebug(() => $"PitStop bulk data Car Index: {packet.CarSelection}"); - SetCarSelection(packet.CarSelection); + // Refresh the cash register display + register?.OnUnitsToBuyChanged(); + + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.ProcessBulkUpdate_Internal() [{StationName}, {NetId}] Car Index: {carSelection}"); + SetCarSelection(carSelection); - Multiplayer.LogDebug(() => $"PitStop bulk data Plugs {packet.PlugData.Count()}"); + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.ProcessBulkUpdate_Internal() [{StationName}, {NetId}] bulk data Plugs {plugData.Count()}"); - //sync plugs - foreach (var plug in packet.PlugData) + // Sync plugs + foreach (var plug in plugData) { var result = NetworkedPluggableObject.Get(plug.NetId, out var netPlug); - Multiplayer.LogDebug(() => $"PitStop bulk data Plugs netId: {plug.NetId}, found: {result}"); + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.ProcessBulkUpdate_Internal() [{StationName}, {NetId}] Plugs netId: {plug.NetId}, found: {result}"); netPlug?.ProcessBulkUpdate(plug); } - //sync faucet position + // Sync faucet position if (faucetPositioner != null) - SetFaucetRotation(packet.FaucetNotch); + { + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.ProcessBulkUpdate_Internal() [{StationName}, {NetId}] Faucet notch: {faucetNotch}"); + + SetFaucetRotation(faucetNotch); - //DV.CabControls.Spec.Lever; - //HingeJointDrivenTransformAdjuster; - //DV.CabControls.NonVR.LeverNonVR; - //DV.CabControls.VRTK.LeverVRTK; - //HingeJointAngleFix; - //UnityEngine.HingeJoint; - //DV.Interaction.GrabHandlerHingeJoint; - //SteppedJoint; + while (!faucetTargetReached) + yield return null; + } // Mark data as refreshed to allow player interactions Refreshed = true; - Multiplayer.LogDebug(() => $"PitStop bulk data Refreshed"); + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.ProcessBulkUpdate_Internal() [{StationName}, {NetId}] Bulk data refreshed"); } /// @@ -1070,26 +1083,15 @@ public void ProcessInteractionPacketAsClient(CommonPitStopInteractionPacket pack case PitStopStationInteractionType.FaucetUngrab: //allow interaction faucetPositionerGrab?.SetMovingDisabled(false); + + SetFaucetRotation((int)packet.Value); - if (packet.Value >= -1 && packet.Value <= 1) - { - if (faucetPositioner.Percentage != packet.Value) - { - faucetTargetPercentage = packet.Value; - faucetTargetReached = false; - } - } break; case PitStopStationInteractionType.FaucetPosition: - if (packet.Value >= -1 && packet.Value <= 1) - { - if (faucetPositioner != null && faucetPositioner.Percentage != packet.Value) - { - faucetTargetPercentage = packet.Value; - faucetTargetReached = false; - } - } + + SetFaucetRotation((int)packet.Value); + break; } } diff --git a/Multiplayer/Networking/Data/LocoResourceModuleData.cs b/Multiplayer/Networking/Data/LocoResourceModuleData.cs index fb7d9101..e750abed 100644 --- a/Multiplayer/Networking/Data/LocoResourceModuleData.cs +++ b/Multiplayer/Networking/Data/LocoResourceModuleData.cs @@ -40,7 +40,7 @@ public static void Serialize(NetDataWriter writer, LocoResourceModuleData data) { writer.Put((int)data.ResourceType); - writer.Put(data.Values.Count()); + writer.Put(data.Values.Length); foreach (var val in data.Values) writer.Put(val); diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index f5dec33b..a51018bc 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -1006,6 +1006,8 @@ private void OnCommonPitStopPlugInteractionPacket(CommonPitStopPlugInteractionPa private void OnClientboundPitStopBulkUpdatePacket(ClientboundPitStopBulkUpdatePacket packet) { + LogDebug(() => $"OnClientboundPitStopBulkUpdatePacket() NetId: {packet.NetId}, CarCount: {packet.CarCount}, CarSelection: { packet.CarSelection}, FaucetNotch: {packet.FaucetNotch}, ResourceData Count: {packet.ResourceData.Length}, PlugData: {packet.PlugData.Length}"); + if (!NetworkedPitStopStation.Get(packet.NetId, out var netPitStop)) { LogWarning($"Pit Stop Bulk Data received for station netId: {packet.NetId}, but pit stop does not exist!"); @@ -1448,7 +1450,7 @@ public void SendPitStopPlugInteractionPacket sbyte socketIndex = -1 ) { - LogDebug(()=>$"SendPitStopInteractionPacket({netId}, {interaction}, pos: {position}, rot: {rotation}, trainNetId: {trainCarNetId}, socketIndex: {socketIndex})"); + LogDebug(()=>$"SendPitStopPlugInteractionPacket({netId}, {interaction}, pos: {position}, rot: {rotation}, trainNetId: {trainCarNetId}, socketIndex: {socketIndex})"); SendNetSerializablePacketToServer(new CommonPitStopPlugInteractionPacket { diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index c813c6f4..37ac5927 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -599,15 +599,16 @@ public void SendItemsChangePacket(List items, ServerPlayer playe } } - public void SendPitStopBulkDataPacket(ushort netId, int carCount, int carIndex, float faucetPos, LocoResourceModuleData[] stationData, PitStopPlugData[] plugData , ITransportPeer peer = null) + public void SendPitStopBulkDataPacket(ushort netId, int carCount, int carIndex, int faucetNotch, LocoResourceModuleData[] stationData, PitStopPlugData[] plugData , ITransportPeer peer = null) { - LogDebug(() => $"SendPitStopBulkDataPacket({netId}, {stationData.Count()}, {plugData.Count()}, {peer?.Id})"); + LogDebug(() => $"SendPitStopBulkDataPacket({netId}, {carCount}, {carIndex}, {faucetNotch}, {stationData.Count()}, {plugData.Count()}, {peer?.Id})"); var packet = new ClientboundPitStopBulkUpdatePacket { NetId = netId, CarCount = carCount, CarSelection = carIndex, + FaucetNotch = faucetNotch, ResourceData = stationData, PlugData = plugData, }; From 05034755507d2d3904e937fe9f3c186289bffb66 Mon Sep 17 00:00:00 2001 From: Macka Date: Sun, 10 Aug 2025 22:44:40 +1000 Subject: [PATCH 427/521] Fix issue with colliders being optimised out --- .../World/NetworkedPitStopStation.cs | 30 +++++++++ .../Train/ScriptStripperRuntimePatch.cs | 61 +++++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 Multiplayer/Patches/Train/ScriptStripperRuntimePatch.cs diff --git a/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs b/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs index 62fda669..8903c101 100644 --- a/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs +++ b/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs @@ -188,6 +188,9 @@ protected override void OnDestroy() NetworkLifecycle.Instance.OnTick -= OnTick; NetworkLifecycle.Instance.Server.PlayerDisconnect -= OnPlayerDisconnect; + + // Monitor changes to vehicles in the pit stop + Station.pitstop.CarEntered -= OnCarPitStopEntered; } if (carSelectorGrab != null) @@ -310,6 +313,8 @@ public void ProcessInteractionPacketAsHost(CommonPitStopInteractionPacket packet if (ValidateInteraction(packet, senderPlayer)) { + // Ensure colliders for water, coal, etc. are loaded + OnCarPitStopEntered(); processingAsHost = true; if (senderPlayer.Id != NetworkLifecycle.Instance.Server.SelfId) @@ -396,6 +401,21 @@ private void SendResourceUpdate(LocoResourceModule module) } } } + + private void OnCarPitStopEntered() + { + foreach (var car in Station.pitstop.carList) + { + if (car == null) + continue; + + if (!car.AreExternalInteractablesLoaded && !car.AreDummyExternalInteractablesLoaded) + { + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.OnCarPitStopEntered() [{StationName}, {NetId}] Loading dummy external interactables for car: {car.ID}"); + car.LoadDummyExternalInteractables(); + } + } + } #endregion @@ -418,6 +438,16 @@ private IEnumerator Init() while (Station?.pitstop == null) yield return new WaitForEndOfFrame(); + if (NetworkLifecycle.Instance.IsHost()) + { + // Monitor changes to vehicles in the pit stop + Station.pitstop.CarEntered += OnCarPitStopEntered; + + // Ensure any cars already in the pit stop have external interactables loaded + if (Station.pitstop.carList.Count > 0) + OnCarPitStopEntered(); + } + register = transform.parent.GetComponentInChildren(true); if (register == null) { diff --git a/Multiplayer/Patches/Train/ScriptStripperRuntimePatch.cs b/Multiplayer/Patches/Train/ScriptStripperRuntimePatch.cs new file mode 100644 index 00000000..697129df --- /dev/null +++ b/Multiplayer/Patches/Train/ScriptStripperRuntimePatch.cs @@ -0,0 +1,61 @@ +using DV.Optimizers; +using HarmonyLib; +using Multiplayer.Components.Networking; +using Multiplayer.Components.Networking.Train; +using UnityEngine; + +namespace Multiplayer.Patches.Train; + +[HarmonyPatch(typeof(ScriptStripperRuntime))] +public static class ScriptStripperRuntimePatch +{ + [HarmonyPatch(nameof(ScriptStripperRuntime.Strip))] + [HarmonyPrefix] + public static bool Strip(GameObject goToStrip) + { + if(!NetworkLifecycle.Instance.IsHost()) + return true; + + var trainCar = TrainCar.Resolve(goToStrip); + + if (trainCar == null) + return true; + + MonoBehaviour[] scripts = goToStrip.GetComponentsInChildren(); + Joint[] joints = goToStrip.GetComponentsInChildren(); + Rigidbody[] rigidBodies = goToStrip.GetComponentsInChildren(); + Collider[] colliders = goToStrip.GetComponentsInChildren(); + + for (int i = 0; i < joints.Length; i++) + { + Object.Destroy(joints[i]); + } + + for (int i = 0; i < rigidBodies.Length; i++) + { + Object.Destroy(rigidBodies[i]); + } + + for (int i = 0; i < colliders.Length; i++) + { + if(!colliders[i].TryGetComponent(out _)) + Object.Destroy(colliders[i]); + else + { + Multiplayer.LogDebug(() => $"ScriptStripperRuntimePatch.Strip() Keeping collider {colliders[i].gameObject.GetPath()} for {trainCar.ID}, has LocoResourceReceiver component."); + } + } + + for (int i = 0; i < scripts.Length; i++) + { + if (!scripts[i].GetType().Equals(typeof(LocoResourceReceiver))) + Object.Destroy(scripts[i]); + else + { + Multiplayer.LogDebug(() => $"ScriptStripperRuntimePatch.Strip() Keeping script {scripts[i].gameObject.GetPath()} for {trainCar.ID}, is LocoResourceReceiver component."); + } + } + + return false; + } +} From a4ce3b331ce9d1868aeedb89c5026cd7533098f9 Mon Sep 17 00:00:00 2001 From: Macka Date: Sat, 16 Aug 2025 15:35:50 +1000 Subject: [PATCH 428/521] Minor clean up of namespace --- Multiplayer/Patches/Train/CarVisitCheckerPatch.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Multiplayer/Patches/Train/CarVisitCheckerPatch.cs b/Multiplayer/Patches/Train/CarVisitCheckerPatch.cs index 156ba561..30ba5cb8 100644 --- a/Multiplayer/Patches/Train/CarVisitCheckerPatch.cs +++ b/Multiplayer/Patches/Train/CarVisitCheckerPatch.cs @@ -4,14 +4,14 @@ using Multiplayer.Components.Networking.Train; using Multiplayer.Networking.Data; -namespace Multiplayer.Patches.World; +namespace Multiplayer.Patches.Train; [HarmonyPatch(typeof(CarVisitChecker))] public static class CarVisitCheckerPatch { [HarmonyPrefix] [HarmonyPatch(nameof(CarVisitChecker.IsRecentlyVisited), MethodType.Getter)] - private static bool IsRecentlyVisited_Prefix(CarVisitChecker __instance, ref bool __result) + public static bool IsRecentlyVisited_Prefix(CarVisitChecker __instance, ref bool __result) { if (NetworkLifecycle.Instance.IsHost() && NetworkLifecycle.Instance.Server.PlayerCount == 1) return true; //playing in "vanilla mode" allow game code to run From 6955748c13d3bbf921535503d0a77d2013f07439 Mon Sep 17 00:00:00 2001 From: Macka Date: Sat, 23 Aug 2025 20:13:06 +1000 Subject: [PATCH 429/521] Refactor CashRegister*Patch --- .../Patches/World/CashRegisterBasePatch.cs | 62 +++++++++++++++++++ .../World/CashRegisterWithModulesPatch.cs | 45 -------------- 2 files changed, 62 insertions(+), 45 deletions(-) create mode 100644 Multiplayer/Patches/World/CashRegisterBasePatch.cs diff --git a/Multiplayer/Patches/World/CashRegisterBasePatch.cs b/Multiplayer/Patches/World/CashRegisterBasePatch.cs new file mode 100644 index 00000000..1a18c02f --- /dev/null +++ b/Multiplayer/Patches/World/CashRegisterBasePatch.cs @@ -0,0 +1,62 @@ +using DV.CashRegister; +using HarmonyLib; +using Multiplayer.Components.Networking; +using Multiplayer.Components.Networking.World; +using Multiplayer.Utils; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; +using UnityEngine; + +namespace Multiplayer.Patches.World; + +[HarmonyPatch(typeof(CashRegisterBase))] +public class CashRegisterBasePatch +{ + [HarmonyPostfix] + [HarmonyPatch(nameof(CashRegisterBase.SetCash))] + private static void SetCash(CashRegisterBase __instance, double amount) + { + if (__instance is not CashRegisterWithModules cashRegisterWithModules) + return; + + Multiplayer.LogDebug(() => $"SetCash() {__instance.GetObjectPath()}, Deposited: {amount}"); + + if (!NetworkedCashRegisterWithModules.TryGet(cashRegisterWithModules, out var netCashRegister)) + { + Multiplayer.LogWarning($"Attempting to SetCash, but NetworkedCashRegisterWithModules not found for {cashRegisterWithModules.GetObjectPath()}"); + return; + } + + if (netCashRegister.IsShopRegister) + return; + + netCashRegister.SetCash(amount); + } + + [HarmonyPrefix] + [HarmonyPatch(nameof(CashRegisterBase.OnEnable))] + private static bool OnEnable(CashRegisterBase __instance) + { + //Multiplayer.LogDebug(() => $"CashRegisterBase.OnEnable({__instance.GetObjectPath()}) {__instance.GetType()}"); + if (__instance is not CashRegisterWithModules) + return true; + + return NetworkLifecycle.Instance.IsHost(); + } + + [HarmonyPrefix] + [HarmonyPatch(nameof(CashRegisterBase.OnDisable))] + private static bool OnDisable(CashRegisterBase __instance) + { + //Multiplayer.LogDebug(() => $"CashRegisterBase.OnDisable({__instance.GetObjectPath()}) {__instance.GetType()}"); + if (__instance is not CashRegisterWithModules) + return true; + + // Prevent clients from cancelling/returning cash on cash registers when loading the game or leaving the area + __instance.StopAllCoroutines(); + return NetworkLifecycle.Instance.IsHost(); + } +} + diff --git a/Multiplayer/Patches/World/CashRegisterWithModulesPatch.cs b/Multiplayer/Patches/World/CashRegisterWithModulesPatch.cs index 250c47b6..5cbdaefe 100644 --- a/Multiplayer/Patches/World/CashRegisterWithModulesPatch.cs +++ b/Multiplayer/Patches/World/CashRegisterWithModulesPatch.cs @@ -4,7 +4,6 @@ using Multiplayer.Components.Networking.World; using Multiplayer.Networking.Packets.Common; using Multiplayer.Utils; -using System; namespace Multiplayer.Patches.World; @@ -109,47 +108,3 @@ private static void Cancel_Postfix(CashRegisterWithModules __instance) }); } } - -[HarmonyPatch(typeof(CashRegisterBase))] -public class CashRegisterBasePatch -{ - [HarmonyPostfix] - [HarmonyPatch(nameof(CashRegisterBase.SetCash))] - private static void SetCash(CashRegisterBase __instance, double amount) - { - if (__instance is not CashRegisterWithModules cashRegisterWithModules) - return; - - Multiplayer.LogDebug(() => $"SetCash() {__instance.GetObjectPath()}, Deposited: {amount}"); - - if (!NetworkedCashRegisterWithModules.TryGet(cashRegisterWithModules, out var netCashRegister)) - Multiplayer.LogWarning($"CashRegisterWithModules.SetCash({cashRegisterWithModules.GetObjectPath()}) NetworkedCashRegisterWithModules not found!"); - else - netCashRegister.SetCash(amount); - } - - [HarmonyPrefix] - [HarmonyPatch(nameof(CashRegisterBase.OnEnable))] - private static bool OnEnable(CashRegisterBase __instance) - { - //Multiplayer.LogDebug(() => $"CashRegisterBase.OnEnable({__instance.GetObjectPath()}) {__instance.GetType()}"); - if (__instance is not CashRegisterWithModules) - return true; - - // Prevent clients from cancelling/returning cash on cash registers when loading the game or leaving the area - return NetworkLifecycle.Instance.IsHost(); - } - - [HarmonyPrefix] - [HarmonyPatch(nameof(CashRegisterBase.OnDisable))] - private static bool OnDisable(CashRegisterBase __instance) - { - //Multiplayer.LogDebug(() => $"CashRegisterBase.OnDisable({__instance.GetObjectPath()}) {__instance.GetType()}"); - if (__instance is not CashRegisterWithModules) - return true; - - // Prevent clients from cancelling/returning cash on cash registers when loading the game or leaving the area - __instance.StopAllCoroutines(); - return NetworkLifecycle.Instance.IsHost(); - } -} From 694736079adfbc3f39667488747dd7afb2b663a0 Mon Sep 17 00:00:00 2001 From: Macka Date: Sat, 23 Aug 2025 20:13:45 +1000 Subject: [PATCH 430/521] Block ReturnMoneyToPlayerCheck Prevent cash register from resetting when the host is not nearby --- .../Patches/World/CashRegisterBasePatch.cs | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/Multiplayer/Patches/World/CashRegisterBasePatch.cs b/Multiplayer/Patches/World/CashRegisterBasePatch.cs index 1a18c02f..8463ea62 100644 --- a/Multiplayer/Patches/World/CashRegisterBasePatch.cs +++ b/Multiplayer/Patches/World/CashRegisterBasePatch.cs @@ -60,3 +60,50 @@ private static bool OnDisable(CashRegisterBase __instance) } } +[HarmonyPatch] +public class CashRegisterBaseReturnMoneyToPlayerCheckPatch +{ + const int TARGET_NOPS = 3; + static readonly CodeInstruction targetMethod = CodeInstruction.Call(typeof(Vector3), "op_Subtraction", [typeof(Vector3), typeof(Vector3)], null); + + public static IEnumerable TargetMethods() + { + //We're targeting an 'IEnumerable'; this gets compiled as a state machine with + //a method per state. + //Find all of the resultant states that are a 'MoveNext', these are the methods we need to patch. + //Doing this dynamically reduces the chance a game update breaks the transpiler + return typeof(CashRegisterBase) + .GetNestedTypes(BindingFlags.NonPublic | BindingFlags.Instance) + .Where(t => t.Name.StartsWith("")) + .SelectMany(t => t.GetMethods(BindingFlags.NonPublic | BindingFlags.Instance)) + .Where(m => m.Name == "MoveNext"); + } + + private static IEnumerable Transpiler(IEnumerable instructions) + { + int nopCtr = 0; + bool foundEntry = false; + + List newCode = [] ; + + foreach (CodeInstruction instruction in instructions) + { + if (instruction.opcode == OpCodes.Call && instruction.operand?.ToString() == targetMethod.operand?.ToString()) + { + foundEntry = true; + newCode.Add(CodeInstruction.Call(typeof(DvExtensions), nameof(DvExtensions.AnyPlayerSqrMag), [typeof(Vector3)], null)); //inject our method + } + else if (foundEntry && nopCtr < TARGET_NOPS) + { + nopCtr++; + newCode.Add(new CodeInstruction(OpCodes.Nop)); + } + else + { + newCode.Add(instruction); + } + } + + return newCode; + } +} From c8a59107d2e318882120ddbb1bc5279b01ddc5bb Mon Sep 17 00:00:00 2001 From: Macka Date: Sat, 23 Aug 2025 20:14:42 +1000 Subject: [PATCH 431/521] Ignore shop cash registers --- .../Networking/World/NetworkedCashRegisterWithModules.cs | 8 +++++++- .../Patches/World/CashRegisterWithModulesPatch.cs | 9 +++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/Multiplayer/Components/Networking/World/NetworkedCashRegisterWithModules.cs b/Multiplayer/Components/Networking/World/NetworkedCashRegisterWithModules.cs index 098ea127..5a0d0ea0 100644 --- a/Multiplayer/Components/Networking/World/NetworkedCashRegisterWithModules.cs +++ b/Multiplayer/Components/Networking/World/NetworkedCashRegisterWithModules.cs @@ -1,6 +1,7 @@ using DV.CashRegister; using DV.Interaction; using DV.InventorySystem; +using DV.Shops; using Multiplayer.Networking.Data; using Multiplayer.Networking.Packets.Common; using Multiplayer.Utils; @@ -31,6 +32,10 @@ public static bool TryGet(CashRegisterWithModules cashRegister, out NetworkedCas public static void InitialiseCashRegisters() { + // Find all shop cash registers + var shopRegisters = GlobalShopController.Instance.globalShopList + .Select(shop => shop.cashRegister) + .ToArray(); //Find all CashRegistersWithModules that are placed on the map //sort them by their hierarchy path for consistent ordering @@ -47,6 +52,7 @@ public static void InitialiseCashRegisters() { var netRegister = register.GetOrAddComponent(); netRegister.CashRegister = register; + netRegister.IsShopRegister = shopRegisters.Contains(register); if (netRegister.NetId == 0) netRegister.Awake(); @@ -69,7 +75,7 @@ public static void InitialiseCashRegisters() bool isBuying; bool isCancelling; - + public bool IsShopRegister { get; set; } = false; #endregion #region Common Variables diff --git a/Multiplayer/Patches/World/CashRegisterWithModulesPatch.cs b/Multiplayer/Patches/World/CashRegisterWithModulesPatch.cs index 5cbdaefe..5216bbd2 100644 --- a/Multiplayer/Patches/World/CashRegisterWithModulesPatch.cs +++ b/Multiplayer/Patches/World/CashRegisterWithModulesPatch.cs @@ -37,6 +37,9 @@ private static bool OnBuyPressed(CashRegisterWithModules __instance) return false; } + if (netCashRegister.IsShopRegister) + return true; + CoroutineManager.Instance.StartCoroutine(netCashRegister.Buy()); return false; @@ -80,6 +83,9 @@ private static bool Cancel(CashRegisterWithModules __instance) return false; } + if (netCashRegister.IsShopRegister) + return true; + CoroutineManager.Instance.StartCoroutine(netCashRegister.Cancel()); return false; @@ -99,6 +105,9 @@ private static void Cancel_Postfix(CashRegisterWithModules __instance) return; } + if (netCashRegister.IsShopRegister) + return; + // Send cancel action to all clients NetworkLifecycle.Instance.Server.SendCashRegisterAction(new CommonCashRegisterWithModulesActionPacket { From a9fa369f4af289a8b1f659239c0aab15a2400ee5 Mon Sep 17 00:00:00 2001 From: Macka Date: Sat, 23 Aug 2025 20:17:04 +1000 Subject: [PATCH 432/521] Add rejection types and fix logging --- .../World/NetworkedCashRegisterWithModules.cs | 46 +++++++++++++++---- ...mmonCashRegisterWithModulesActionPacket.cs | 1 + 2 files changed, 38 insertions(+), 9 deletions(-) diff --git a/Multiplayer/Components/Networking/World/NetworkedCashRegisterWithModules.cs b/Multiplayer/Components/Networking/World/NetworkedCashRegisterWithModules.cs index 5a0d0ea0..6aa93d81 100644 --- a/Multiplayer/Components/Networking/World/NetworkedCashRegisterWithModules.cs +++ b/Multiplayer/Components/Networking/World/NetworkedCashRegisterWithModules.cs @@ -73,7 +73,6 @@ public static void InitialiseCashRegisters() #region Client Variables bool isBuying; - bool isCancelling; public bool IsShopRegister { get; set; } = false; #endregion @@ -106,11 +105,11 @@ public void Server_ProcessCashRegisterAction(ServerPlayer player, CommonCashRegi bool success = false; CashRegisterAction response = CashRegisterAction.RejectGeneric; - NetworkLifecycle.Instance.Server?.LogDebug(() => $"Server_ProcessAction({player.Username}, {packet.Action}, {packet.Amount})"); + NetworkLifecycle.Instance.Server?.LogDebug(() => $"NetworkedCashRegisterWithModules.Server_ProcessAction({player.Username}, {packet.Action}, {packet.Amount})"); if (sqrDistance > GrabberRaycasterDV.FPS_INTERACTION_RANGE_SQR * 2) //need to find the real distance, likely related to player capsual size { - NetworkLifecycle.Instance.Server?.LogDebug(() => $"Server_ProcessAction({player.Username}, {packet.Action}, {packet.Amount}) {CashRegister.GetObjectPath()}. Player too far! Player pos: {player.WorldPosition}, register pos: {transform.position}, sqrMag: {sqrDistance}"); + NetworkLifecycle.Instance.Server?.LogDebug(() => $"NetworkedCashRegisterWithModules.Server_ProcessAction({player.Username}, {packet.Action}, {packet.Amount}) {CashRegister.GetObjectPath()}. Player too far! Player pos: {player.WorldPosition}, register pos: {transform.position}, sqrMag: {sqrDistance}"); return; } @@ -124,16 +123,30 @@ public void Server_ProcessCashRegisterAction(ServerPlayer player, CommonCashRegi break; case CashRegisterAction.Buy: - success = CashRegister?.Buy() ?? false; - if (Inventory.Instance.PlayerMoney <= CashRegister.GetTotalCost()) - response = CashRegisterAction.RejectFunds; + + Multiplayer.LogDebug(() => $"NetworkedCashRegisterWithModules.Server_ProcessAction({packet.Action}) Player Money: {Inventory.Instance.PlayerMoney}, TotalCost: {CashRegister.GetTotalCost()}, TotalUnitsInBasket: {CashRegister.TotalUnitsInBasket()}"); + + if (CashRegister.TotalUnitsInBasket() <= 0) + { + response = CashRegisterAction.RejectedNoItems; + } + else if (Inventory.Instance.PlayerMoney <= CashRegister.GetTotalCost()) + { + response = CashRegisterAction.RejectFunds; + } + else + { + success = CashRegister?.Buy() ?? false; + } + + Multiplayer.LogDebug(() => $"NetworkedCashRegisterWithModules.Server_ProcessAction({packet.Action}, {packet.Amount}) Response: {response}, Buy success: {success}, Player Money: {Inventory.Instance.PlayerMoney}, TotalCost: {CashRegister.GetTotalCost()}, TotalUnitsInBasket: {CashRegister.TotalUnitsInBasket()}"); break; case CashRegisterAction.SetFunds: double spend = 0; - NetworkLifecycle.Instance.Server?.LogDebug(() => $"Server_ProcessAction({player.Username}, {packet.Action}, {packet.Amount}) Wallet: {Inventory.Instance.PlayerMoney}"); + NetworkLifecycle.Instance.Server?.LogDebug(() => $"NetworkedCashRegisterWithModules.Server_ProcessAction({player.Username}, {packet.Action}, {packet.Amount}) Wallet: {Inventory.Instance.PlayerMoney}"); if (packet.Amount > 0) { if (Inventory.Instance.PlayerMoney >= packet.Amount) @@ -148,7 +161,7 @@ public void Server_ProcessCashRegisterAction(ServerPlayer player, CommonCashRegi } else { - NetworkLifecycle.Instance.Server?.LogDebug(() => $"Server_ProcessAction({player.Username}, {packet.Action}, {packet.Amount}) amount negative!"); + NetworkLifecycle.Instance.Server?.LogDebug(() => $"NetworkedCashRegisterWithModules.Server_ProcessAction({player.Username}, {packet.Action}, {packet.Amount}) amount negative!"); } break; } @@ -176,7 +189,7 @@ public void Server_ProcessCashRegisterAction(ServerPlayer player, CommonCashRegi public void Client_ProcessCashRegisterAction(CashRegisterAction action, double amount) { - NetworkLifecycle.Instance.Client?.LogDebug(() => $"Client_ProcessCashRegisterAction({action}, {amount}) isBuying: {isBuying}, isCancelling: {isCancelling}"); + NetworkLifecycle.Instance.Client?.LogDebug(() => $"NetworkedCashRegisterWithModules.Client_ProcessCashRegisterAction({action}, {amount}) isBuying: {isBuying}, isCancelling: {isCancelling}"); switch (action) { case CashRegisterAction.Cancel: @@ -237,6 +250,21 @@ public void Client_ProcessCashRegisterAction(CashRegisterAction action, double a CashRegister?.notEnoughMoneyAudio?.Play(CashRegister.transform.position, 1f, 1f, 0f, 1f, 500f, default, null, CashRegister.transform, false, 0f, null); break; + + case CashRegisterAction.RejectedNoItems: + isBuying = false; + isCancelling = false; + + foreach (var module in CashRegister.registerModules) + module.ResetData(); + + CashRegister?.OnUnitsToBuyChanged(); + + CashRegister.DepositedCash = 0; + CashRegister?.OnDepositedUpdated(); + + CashRegister?.buyAudio?.Play(CashRegister.transform.position, 1f, 1f, 0f, 1f, 500f, default, null, CashRegister.transform, false, 0f, null); + break; } } diff --git a/Multiplayer/Networking/Packets/Common/CommonCashRegisterWithModulesActionPacket.cs b/Multiplayer/Networking/Packets/Common/CommonCashRegisterWithModulesActionPacket.cs index 666d8e8d..6452d73e 100644 --- a/Multiplayer/Networking/Packets/Common/CommonCashRegisterWithModulesActionPacket.cs +++ b/Multiplayer/Networking/Packets/Common/CommonCashRegisterWithModulesActionPacket.cs @@ -8,6 +8,7 @@ public enum CashRegisterAction : byte SetFunds, RejectGeneric, RejectFunds, + RejectedNoItems, Approve } public class CommonCashRegisterWithModulesActionPacket From b1400de9ce15ca01de01c2a577db09a72a7890cc Mon Sep 17 00:00:00 2001 From: Macka Date: Sat, 23 Aug 2025 20:17:49 +1000 Subject: [PATCH 433/521] Fix resource sync issues --- .../World/NetworkedPitStopStation.cs | 60 ++++++++++++++----- 1 file changed, 45 insertions(+), 15 deletions(-) diff --git a/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs b/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs index 8903c101..0921e5d8 100644 --- a/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs +++ b/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs @@ -2,8 +2,8 @@ using DV.CabControls.NonVR; using DV.CashRegister; using DV.Interaction; -using DV.ThingTypes; using DV.Optimizers; +using DV.ThingTypes; using Multiplayer.Networking.Data; using Multiplayer.Networking.Managers.Server; using Multiplayer.Networking.Packets.Clientbound.World; @@ -80,7 +80,7 @@ public static void InitialisePitStops() public CullingManager CullingManager { get; private set; } private readonly Dictionary resourceStartStopDelegates = []; - private readonly Dictionary resourceFlowing = []; + private readonly Dictionary resourceFlowing = []; private bool processingAsHost = false; #endregion @@ -352,27 +352,36 @@ public void ProcessInteractionPacketAsHost(CommonPitStopInteractionPacket packet private void OnFlowStarted(LocoResourceModule module) { Multiplayer.LogDebug(() => $"NetworkedPitStopStation.OnFlowStarted() {module.resourceType} [{StationName}, {NetId}]"); - resourceFlowing[module] = true; + resourceFlowing[module] = (isFlowing: true, wasFlowing: false, lastUpdate: false); } private void OnFlowStopped(LocoResourceModule module) { Multiplayer.LogDebug(() => $"NetworkedPitStopStation.OnFlowStopped() {module.resourceType} [{StationName}, {NetId}]"); - resourceFlowing[module] = false; + resourceFlowing[module] = (isFlowing: false, wasFlowing: true, lastUpdate: false); SendResourceUpdate(module); } private void OnTick(uint tick) { - foreach (var kvp in resourceFlowing) + var modules = resourceFlowing.Keys.ToList(); + foreach (var module in modules) { - if (!kvp.Value) - continue; - - var module = kvp.Key; + // Ensure the final value is sent, we need perfect sync for payments to work + if (resourceFlowing[module].isFlowing || resourceFlowing[module].wasFlowing) + { + SendResourceUpdate(module); - SendResourceUpdate(module); + if (!resourceFlowing[module].isFlowing) + { + // We want one final update to ensure race conditions between flow stopping and game ticks do not cause sync issues + if (!resourceFlowing[module].lastUpdate) + resourceFlowing[module] = (isFlowing: false, wasFlowing: true, lastUpdate: true); + else + resourceFlowing[module] = (isFlowing: false, wasFlowing: false, lastUpdate: false); + } + } } } @@ -388,6 +397,8 @@ private void SendResourceUpdate(LocoResourceModule module) Value = module.Data.unitsToBuy }; + lastRemoteValueDict[module.resourceType] = module.Data.unitsToBuy; + foreach (var player in CullingManager.ActivePlayers) { if (player != null) @@ -495,7 +506,6 @@ private IEnumerator Init() { isResourceGrabbedDict[resourceType] = false; isResourceRemoteGrabbedDict[resourceType] = false; - lastRemoteValueDict[resourceType] = 0.0f; } StringBuilder sb = new(); @@ -571,12 +581,28 @@ private IEnumerator Init() initialised = true; } + private IEnumerator SetUnitsDelayed(LocoResourceModule rm) + { + if(rm == null || !isResourceRemoteGrabbedDict.ContainsKey(rm.resourceType)) + yield break; + + var resourceType = rm.resourceType; + + yield return new WaitUntil(()=> !isResourceRemoteGrabbedDict[resourceType] && !rm.IsFlowing); + + SetUnits(rm, lastRemoteValueDict[resourceType]); + } + private void SetUnits(LocoResourceModule rm, float units) { if (rm == null) return; float clamped = Mathf.Clamp(units, rm.AbsoluteMinValue, rm.AbsoluteMaxValue); + + lastRemoteValueDict[rm.resourceType] = clamped; + + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.SetUnits({rm.resourceType}, {units}) clamped: {clamped}, flowMultiplier: {rm.flowMultiplier}, flowRate: {rm.flowRate}, isFlowing: {rm.IsFlowing}"); rm.SetUnitsToBuy(clamped); } @@ -840,7 +866,7 @@ private IEnumerator ProcessBulkUpdate_Internal(int carCount, int carSelection, i float time = Time.time; // Allow pit stop to complete loading and cars to load in the pit stop - Multiplayer.Log($"ProcessBulkUpdate_Internal() [{StationName}, {NetId}] waiting for load"); + Multiplayer.Log($"Processing bulk update for [{StationName}, {NetId}]"); yield return new WaitUntil ( @@ -1068,9 +1094,14 @@ public void ProcessInteractionPacketAsClient(CommonPitStopInteractionPacket pack resourceModule.OnValvePositionChange((int)packet.Value); - // Update remote grab state + // Update remote grab state and delay set units + bool wasRemoteGrabbed = isResourceRemoteGrabbedDict[resourceType]; isResourceRemoteGrabbedDict[resourceType] = grabbed; + if (wasRemoteGrabbed && !grabbed) + { + CoroutineManager.Instance.StartCoroutine(SetUnitsDelayed(resourceModule)); + } break; case PitStopStationInteractionType.ResourceUpdate: @@ -1084,8 +1115,7 @@ public void ProcessInteractionPacketAsClient(CommonPitStopInteractionPacket pack return; } - lastRemoteValueDict[resourceType] = packet.Value; - SetUnits(resourceModule, lastRemoteValueDict[resourceType]); + SetUnits(resourceModule, packet.Value); Multiplayer.LogDebug(() => $"NetworkedPitStopStation.ProcessPacket() [{StationName}, {NetId}] {interactionType}, resource type: {resourceType}, state: {packet.Value}, flowing: {resourceModule.IsFlowing}"); break; From d48d5a335c9c6e7e147adb2eed71e0b5b9695fec Mon Sep 17 00:00:00 2001 From: Macka Date: Sat, 23 Aug 2025 20:18:24 +1000 Subject: [PATCH 434/521] minor clean up --- .gitattributes | 2 +- .../Networking/Managers/Client/NetworkClient.cs | 1 + .../Patches/Train/UnusedTrainCarDeleterPatch.cs | 10 ++-------- .../World/PlayerDistanceGameObjectsDisablerPatch.cs | 2 +- 4 files changed, 5 insertions(+), 10 deletions(-) diff --git a/.gitattributes b/.gitattributes index 464971cb..28142e2f 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1 @@ -* text=auto eol=lf +*.cs text eol=crlf \ No newline at end of file diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index a51018bc..3b93680c 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -312,6 +312,7 @@ private void OnClientboundDisconnectPacket(ClientboundDisconnectPacket packet) } else { + Log($"Server Shutting Down"); disconnectMessage = "Server Shutting Down"; } } diff --git a/Multiplayer/Patches/Train/UnusedTrainCarDeleterPatch.cs b/Multiplayer/Patches/Train/UnusedTrainCarDeleterPatch.cs index 09a6e679..8fec5588 100644 --- a/Multiplayer/Patches/Train/UnusedTrainCarDeleterPatch.cs +++ b/Multiplayer/Patches/Train/UnusedTrainCarDeleterPatch.cs @@ -1,15 +1,9 @@ using HarmonyLib; -using System; -using System.Collections.Generic; +using Multiplayer.Components.Networking; using Multiplayer.Utils; +using System.Collections.Generic; using System.Reflection.Emit; using UnityEngine; -using static HarmonyLib.Code; -using Multiplayer.Networking.Data; -using DV.ThingTypes; -using DV.Logic.Job; -using DV.Utils; -using Multiplayer.Components.Networking; namespace Multiplayer.Patches.Train; diff --git a/Multiplayer/Patches/World/PlayerDistanceGameObjectsDisablerPatch.cs b/Multiplayer/Patches/World/PlayerDistanceGameObjectsDisablerPatch.cs index 824d5018..ade5de6d 100644 --- a/Multiplayer/Patches/World/PlayerDistanceGameObjectsDisablerPatch.cs +++ b/Multiplayer/Patches/World/PlayerDistanceGameObjectsDisablerPatch.cs @@ -49,7 +49,7 @@ 76 00E0 callvirt instance class [UnityEngine.CoreModule] //overwrite line 79 with ldloc.1 (pass in 'this' as the final parameter of call to CustomCalcSqrMagnitude()) 79 00EB call valuetype[UnityEngine.CoreModule] UnityEngine.Vector3[UnityEngine.CoreModule] UnityEngine.Vector3::op_Subtraction(valuetype[UnityEngine.CoreModule] UnityEngine.Vector3, valuetype[UnityEngine.CoreModule] UnityEngine.Vector3) - //overwrite with call to CustomCalcSqrMagnitude() (techinically we are inserting the call and skipping thr original) + //overwrite with call to CustomCalcSqrMagnitude() (techinically we are inserting the call and skipping the original) //Insert 3 NOPs 80 00F0 stloc.3 //skip 0 81 00F1 ldloca.s V_3(3) //skip 1 From 292a47741a0028ea6fd6d05fcb61c5146fc11ebf Mon Sep 17 00:00:00 2001 From: Macka Date: Sat, 23 Aug 2025 20:18:52 +1000 Subject: [PATCH 435/521] Ready for alpha testing --- 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 f2c823d1..528e0550 100644 --- a/Multiplayer/Multiplayer.csproj +++ b/Multiplayer/Multiplayer.csproj @@ -3,7 +3,7 @@ net48 latest Multiplayer - 0.1.12.0 + 0.1.12.4 diff --git a/info.json b/info.json index c6233d01..af49836c 100644 --- a/info.json +++ b/info.json @@ -1,6 +1,6 @@ { "Id": "Multiplayer", - "Version": "0.1.12.0", + "Version": "0.1.12.4", "DisplayName": "Multiplayer", "Author": "Insprill, Macka, Morm", "EntryMethod": "Multiplayer.Multiplayer.Load", From 06a9632fb7b8a6dd3264d9f927655f84be7b3af1 Mon Sep 17 00:00:00 2001 From: Macka Date: Sat, 23 Aug 2025 23:42:22 +1000 Subject: [PATCH 436/521] Refactor to use BinaryWriter/Reader and move base to MultiplayerAPI --- Multiplayer/Multiplayer.csproj | 2 +- Multiplayer/Networking/Data/JobData.cs | 29 ++- .../Networking/Data/TaskNetworkData.cs | 227 +++++++++--------- MultiplayerAPI/Types/TaskNetworkType.cs | 60 +++++ .../Util/BinaryReaderWriterExtensions.cs | 63 +++++ 5 files changed, 254 insertions(+), 127 deletions(-) create mode 100644 MultiplayerAPI/Types/TaskNetworkType.cs create mode 100644 MultiplayerAPI/Util/BinaryReaderWriterExtensions.cs diff --git a/Multiplayer/Multiplayer.csproj b/Multiplayer/Multiplayer.csproj index 5ac28d08..853bce0e 100644 --- a/Multiplayer/Multiplayer.csproj +++ b/Multiplayer/Multiplayer.csproj @@ -3,7 +3,7 @@ net48 latest Multiplayer - 0.1.11.6 + 0.1.12.5 diff --git a/Multiplayer/Networking/Data/JobData.cs b/Multiplayer/Networking/Data/JobData.cs index 1a5b676a..561a074c 100644 --- a/Multiplayer/Networking/Data/JobData.cs +++ b/Multiplayer/Networking/Data/JobData.cs @@ -1,6 +1,7 @@ using DV.Logic.Job; using DV.ThingTypes; using LiteNetLib.Utils; +using MPAPI.Types; using Multiplayer.Components.Networking; using Multiplayer.Components.Networking.Jobs; using Multiplayer.Components.Networking.World; @@ -94,13 +95,22 @@ public static void Serialize(NetDataWriter writer, JobData data) 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); + using (MemoryStream taskMemStream = new()) + using (BinaryWriter taskSerialiser = new(taskMemStream)) + { + task.Serialize(taskSerialiser); + + if (taskMemStream.Length > int.MaxValue) + { + Multiplayer.LogError($"Task {task.TaskType} too large: {taskMemStream.Length}"); + throw new InvalidOperationException($"Task {task.TaskType} data is too large to serialize."); + } + + bw.Write((int)taskMemStream.Length); + bw.Write(taskMemStream.ToArray()); + } } byte[] compressedData = PacketCompression.Compress(ms.ToArray()); @@ -149,10 +159,13 @@ public static JobData Deserialize(NetDataReader reader) 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); + using (MemoryStream taskStream = new MemoryStream(br.ReadBytes(taskLength))) + using (BinaryReader taskReader = new BinaryReader(taskStream)) + { + tasks[i] = TaskNetworkDataFactory.ConvertTask(taskType); + tasks[i].Deserialize(taskReader); + } } } diff --git a/Multiplayer/Networking/Data/TaskNetworkData.cs b/Multiplayer/Networking/Data/TaskNetworkData.cs index 3d73be85..55ee020c 100644 --- a/Multiplayer/Networking/Data/TaskNetworkData.cs +++ b/Multiplayer/Networking/Data/TaskNetworkData.cs @@ -1,78 +1,51 @@ -using System; -using System.Collections.Generic; -using System.Linq; using DV.Logic.Job; using DV.ThingTypes; -using LiteNetLib.Utils; +using MPAPI.Types; +using MPAPI.Util; using Multiplayer.Components.Networking.Train; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; namespace Multiplayer.Networking.Data; -#region TaskData Base Class -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 abstract List GetCars(); -} -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}"); - } -} - -#endregion - #region Extension of TaskTypes public static class TaskNetworkDataFactory { private static readonly Dictionary> TypeToTaskNetworkData = []; private static readonly Dictionary> EnumToEmptyTaskNetworkData = []; + internal static readonly List baseTasks = []; + internal static readonly List baseTaskTypes = []; - public static void RegisterTaskType(TaskType taskType, Func converter, Func emptyCreator) + public static bool RegisterTaskType(TaskType taskType, Func converter, Func emptyCreator) where TGameTask : Task { + if (TypeToTaskNetworkData.Keys.Contains(typeof(TGameTask)) || EnumToEmptyTaskNetworkData.Keys.Contains(taskType)) + { + Multiplayer.LogError($"Task Type {typeof(TGameTask)} already registered!"); + return false; + } + TypeToTaskNetworkData[typeof(TGameTask)] = task => converter((TGameTask)task); EnumToEmptyTaskNetworkData[taskType] = emptyCreator; + + return true; + } + + public static bool UnRegisterTaskType(TaskType taskType) + where TGameTask : Task + { + if(baseTasks.Contains(typeof(TGameTask)) || baseTaskTypes.Contains(taskType)) + { + Multiplayer.LogError($"Cannot unregister base task type {typeof(TGameTask)} with TaskType {taskType}"); + return false; + } + + TypeToTaskNetworkData.Remove(typeof(TGameTask)); + EnumToEmptyTaskNetworkData.Remove(taskType); + + return true; } public static TaskNetworkData ConvertTask(Task task) @@ -107,25 +80,42 @@ static TaskNetworkDataFactory() task => new WarehouseTaskData { TaskType = TaskType.Warehouse }.FromTask(task), type => new WarehouseTaskData { TaskType = type } ); + + baseTasks.Add(typeof(WarehouseTask)); + baseTaskTypes.Add(TaskType.Warehouse); + RegisterTaskType( TaskType.Transport, task => new TransportTaskData { TaskType = TaskType.Transport }.FromTask(task), type => new TransportTaskData { TaskType = type } ); + + baseTasks.Add(typeof(TransportTask)); + baseTaskTypes.Add(TaskType.Transport); + RegisterTaskType( TaskType.Sequential, task => new SequentialTasksData { TaskType = TaskType.Sequential }.FromTask(task), type => new SequentialTasksData { TaskType = type } ); + + baseTasks.Add(typeof(SequentialTasks)); + baseTaskTypes.Add(TaskType.Sequential); + RegisterTaskType( TaskType.Parallel, task => new ParallelTasksData { TaskType = TaskType.Parallel }.FromTask(task), type => new ParallelTasksData { TaskType = type } ); + + baseTasks.Add(typeof(ParallelTasks)); + baseTaskTypes.Add(TaskType.Parallel); } } #endregion +#region Base Task Types + public class WarehouseTaskData : TaskNetworkData { public ushort[] CarNetIDs { get; set; } @@ -135,26 +125,26 @@ public class WarehouseTaskData : TaskNetworkData public float CargoAmount { get; set; } public bool ReadyForMachine { get; set; } - public override void Serialize(NetDataWriter writer) + public override void Serialize(BinaryWriter writer) { SerializeCommon(writer); - writer.PutArray(CarNetIDs); - writer.Put((byte)WarehouseTaskType); - writer.Put(WarehouseMachine); - writer.Put((int)CargoType); - writer.Put(CargoAmount); - writer.Put(ReadyForMachine); + writer.WriteUShortArray(CarNetIDs); + writer.Write((byte)WarehouseTaskType); + writer.Write(WarehouseMachine); + writer.Write((int)CargoType); + writer.Write(CargoAmount); + writer.Write(ReadyForMachine); } - public override void Deserialize(NetDataReader reader) + public override void Deserialize(BinaryReader reader) { DeserializeCommon(reader); - CarNetIDs = reader.GetUShortArray(); - WarehouseTaskType = (WarehouseTaskType)reader.GetByte(); - WarehouseMachine = reader.GetString(); - CargoType = (CargoType)reader.GetInt(); - CargoAmount = reader.GetFloat(); - ReadyForMachine = reader.GetBool(); + CarNetIDs = reader.ReadUShortArray(); + WarehouseTaskType = (WarehouseTaskType)reader.ReadByte(); + WarehouseMachine = reader.ReadString(); + CargoType = (CargoType)reader.ReadInt32(); + CargoAmount = reader.ReadSingle(); + ReadyForMachine = reader.ReadBoolean(); } public override WarehouseTaskData FromTask(Task task) @@ -182,16 +172,16 @@ public override Task ToTask() List cars = CarNetIDs .Select(netId => NetworkedTrainCar.TryGet(netId, out TrainCar trainCar) ? trainCar : null) .Where(car => car != null) - .Select(car =>car.logicCar) + .Select(car => car.logicCar) .ToList(); - WarehouseTask newWareTask = new WarehouseTask( - cars, - WarehouseTaskType, - JobSaveManager.Instance.GetWarehouseMachineWithId(WarehouseMachine), - CargoType, - CargoAmount - ); + WarehouseTask newWareTask = new WarehouseTask( + cars, + WarehouseTaskType, + JobSaveManager.Instance.GetWarehouseMachineWithId(WarehouseMachine), + CargoType, + CargoAmount + ); newWareTask.readyForMachine = ReadyForMachine; @@ -213,59 +203,59 @@ public class TransportTaskData : TaskNetworkData public bool CouplingRequiredAndNotDone { get; set; } public bool AnyHandbrakeRequiredAndNotDone { get; set; } - public override void Serialize(NetDataWriter writer) + public override void Serialize(BinaryWriter writer) { SerializeCommon(writer); //Multiplayer.LogDebug(() => $"TransportTaskData.Serialize() CarNetIDs count: {CarNetIDs.Length}, Values: [{string.Join(", ", CarNetIDs?.Select(id => id.ToString()))}]"); - writer.PutArray(CarNetIDs); + writer.WriteUShortArray(CarNetIDs); //Multiplayer.LogDebug(() => $"TransportTaskData.Serialize() raw after: [{string.Join(", ", writer.Data?.Select(id => id.ToString()))}]"); //Multiplayer.Log($"TaskNetworkData.Serialize() StartingTrack {StartingTrack}"); - writer.Put(StartingTrack); + writer.Write(StartingTrack); //Multiplayer.Log($"TaskNetworkData.Serialize() DestinationTrack {DestinationTrack}"); - writer.Put(DestinationTrack); + writer.Write(DestinationTrack); //Multiplayer.Log($"TaskNetworkData.Serialize() TransportedCargoPerCar != null {TransportedCargoPerCar != null}"); - writer.Put(TransportedCargoPerCar != null); + writer.Write(TransportedCargoPerCar != null); if (TransportedCargoPerCar != null) { //Multiplayer.Log($"TaskNetworkData.Serialize() TransportedCargoPerCar.PutArray() length: {TransportedCargoPerCar.Length}"); - writer.PutArray(TransportedCargoPerCar.Select(x => (int)x).ToArray()); + writer.WriteInt32Array(TransportedCargoPerCar.Select(x => (int)x).ToArray()); } //Multiplayer.Log($"TaskNetworkData.Serialize() CouplingRequiredAndNotDone {CouplingRequiredAndNotDone}"); - writer.Put(CouplingRequiredAndNotDone); + writer.Write(CouplingRequiredAndNotDone); //Multiplayer.Log($"TaskNetworkData.Serialize() AnyHandbrakeRequiredAndNotDone {AnyHandbrakeRequiredAndNotDone}"); - writer.Put(AnyHandbrakeRequiredAndNotDone); + writer.Write(AnyHandbrakeRequiredAndNotDone); } - public override void Deserialize(NetDataReader reader) + public override void Deserialize(BinaryReader reader) { DeserializeCommon(reader); - CarNetIDs = reader.GetUShortArray(); + CarNetIDs = reader.ReadUShortArray(); //Multiplayer.LogDebug(() => $"TransportTaskData.Deserialize() CarNetIDs count: {CarNetIDs.Length}, Values: [{string.Join(", ", CarNetIDs?.Select(id => id.ToString()))}]"); - StartingTrack = reader.GetString(); + StartingTrack = reader.ReadString(); //Multiplayer.Log($"TaskNetworkData.Deserialize() StartingTrack {StartingTrack}"); - DestinationTrack = reader.GetString(); + DestinationTrack = reader.ReadString(); //Multiplayer.Log($"TaskNetworkData.Deserialize() DestinationTrack {DestinationTrack}"); - if (reader.GetBool()) + if (reader.ReadBoolean()) { //Multiplayer.Log($"TaskNetworkData.Deserialize() TransportedCargoPerCar != null True"); - TransportedCargoPerCar = reader.GetIntArray().Select(x => (CargoType)x).ToArray(); + TransportedCargoPerCar = reader.ReadInt32Array().Select(x => (CargoType)x).ToArray(); } //else //{ // Multiplayer.LogWarning($"TaskNetworkData.Deserialize() TransportedCargoPerCar != null False"); //} - CouplingRequiredAndNotDone = reader.GetBool(); + CouplingRequiredAndNotDone = reader.ReadBoolean(); //Multiplayer.Log($"TaskNetworkData.Deserialize() CouplingRequiredAndNotDone {CouplingRequiredAndNotDone}"); - AnyHandbrakeRequiredAndNotDone = reader.GetBool(); + AnyHandbrakeRequiredAndNotDone = reader.ReadBoolean(); //Multiplayer.Log($"TaskNetworkData.Deserialize() AnyHandbrakeRequiredAndNotDone {AnyHandbrakeRequiredAndNotDone}"); } @@ -279,7 +269,7 @@ public override TransportTaskData FromTask(Task task) .Select(car => NetworkedTrainCar.GetFromTrainId(car.ID, out var networkedTrainCar) ? networkedTrainCar.NetId : (ushort)0) - .ToArray(); + .ToArray(); //Multiplayer.LogDebug(() => $"TransportTaskData.FromTask() after CarNetIDs count: {CarNetIDs.Length}, Values: [{string.Join(", ", CarNetIDs.Select(id => id.ToString()))}]"); @@ -320,7 +310,7 @@ public class SequentialTasksData : TaskNetworkData public TaskNetworkData[] Tasks { get; set; } public byte CurrentTaskIndex { get; set; } - public override void Serialize(NetDataWriter writer) + public override void Serialize(BinaryWriter writer) { //Multiplayer.Log($"SequentialTasksData.Serialize({writer != null})"); @@ -328,30 +318,30 @@ public override void Serialize(NetDataWriter writer) //Multiplayer.Log($"SequentialTasksData.Serialize() {Tasks.Length}"); - writer.Put((byte)Tasks.Length); + writer.Write((byte)Tasks.Length); foreach (var task in Tasks) { //Multiplayer.Log($"SequentialTasksData.Serialize() {task.TaskType} {task.GetType()}"); - writer.Put((byte)task.TaskType); + writer.Write((byte)task.TaskType); task.Serialize(writer); } - writer.Put(CurrentTaskIndex); + writer.Write(CurrentTaskIndex); } - public override void Deserialize(NetDataReader reader) + public override void Deserialize(BinaryReader reader) { DeserializeCommon(reader); - var tasksLength = reader.GetByte(); + var tasksLength = reader.ReadByte(); Tasks = new TaskNetworkData[tasksLength]; for (int i = 0; i < tasksLength; i++) { - var taskType = (TaskType)reader.GetByte(); + var taskType = (TaskType)reader.ReadByte(); Tasks[i] = TaskNetworkDataFactory.ConvertTask(taskType); Tasks[i].Deserialize(reader); } - CurrentTaskIndex = reader.GetByte(); + CurrentTaskIndex = reader.ReadByte(); } @@ -364,12 +354,12 @@ public override SequentialTasksData FromTask(Task task) Tasks = TaskNetworkDataFactory.ConvertTasks(sequentialTasks.tasks); - bool found=false; + bool found = false; CurrentTaskIndex = 0; - foreach(Task subTask in sequentialTasks.tasks) + foreach (Task subTask in sequentialTasks.tasks) { - if(subTask == sequentialTasks.currentTask.Value) + if (subTask == sequentialTasks.currentTask.Value) { found = true; break; @@ -396,9 +386,9 @@ public override Task ToTask() SequentialTasks newSeqTask = new SequentialTasks(Tasks.Select(t => t.ToTask()).ToList()); - if(CurrentTaskIndex <= newSeqTask.tasks.Count()) + if (CurrentTaskIndex <= newSeqTask.tasks.Count()) newSeqTask.currentTask = new LinkedListNode(newSeqTask.tasks.ToArray()[CurrentTaskIndex]); - + return newSeqTask; } @@ -420,25 +410,25 @@ public class ParallelTasksData : TaskNetworkData { public TaskNetworkData[] Tasks { get; set; } - public override void Serialize(NetDataWriter writer) + public override void Serialize(BinaryWriter writer) { SerializeCommon(writer); - writer.Put((byte)Tasks.Length); + writer.Write((byte)Tasks.Length); foreach (var task in Tasks) { - writer.Put((byte)task.TaskType); + writer.Write((byte)task.TaskType); task.Serialize(writer); } } - public override void Deserialize(NetDataReader reader) + public override void Deserialize(BinaryReader reader) { DeserializeCommon(reader); - var tasksLength = reader.GetByte(); + var tasksLength = reader.ReadByte(); Tasks = new TaskNetworkData[tasksLength]; for (int i = 0; i < tasksLength; i++) { - var taskType = (TaskType)reader.GetByte(); + var taskType = (TaskType)reader.ReadByte(); Tasks[i] = TaskNetworkDataFactory.ConvertTask(taskType); Tasks[i].Deserialize(reader); } @@ -463,7 +453,7 @@ public override List GetCars() { List result = []; - foreach(var task in Tasks) + foreach (var task in Tasks) { var cars = task.GetCars(); result.AddRange(cars); @@ -472,3 +462,4 @@ public override List GetCars() return result; } } +#endregion diff --git a/MultiplayerAPI/Types/TaskNetworkType.cs b/MultiplayerAPI/Types/TaskNetworkType.cs new file mode 100644 index 00000000..ec9fd6f3 --- /dev/null +++ b/MultiplayerAPI/Types/TaskNetworkType.cs @@ -0,0 +1,60 @@ +using DV.Logic.Job; +using System.Collections.Generic; +using System.IO; + +namespace MPAPI.Types; + +#region TaskData Base Class +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(BinaryWriter writer); + public abstract void Deserialize(BinaryReader reader); + public abstract Task ToTask(); + public abstract List GetCars(); +} + +public abstract class TaskNetworkData : TaskNetworkData where T : TaskNetworkData +{ + public abstract T FromTask(Task task); + + protected void SerializeCommon(BinaryWriter writer) + { + //Multiplayer.Log($"TaskNetworkData.SerializeCommon() State {(byte)State}, {State}"); + writer.Write((byte)State); + //Multiplayer.Log($"TaskNetworkData.SerializeCommon() TaskStartTime {TaskStartTime}"); + writer.Write(TaskStartTime); + //Multiplayer.Log($"TaskNetworkData.SerializeCommon() TaskFinishTime {TaskFinishTime}"); + writer.Write(TaskFinishTime); + //Multiplayer.Log($"TaskNetworkData.SerializeCommon() IsLastTask {IsLastTask}"); + writer.Write(IsLastTask); + //Multiplayer.Log($"TaskNetworkData.SerializeCommon() TimeLimit {TimeLimit}"); + writer.Write(TimeLimit); + //Multiplayer.Log($"TaskNetworkData.SerializeCommon() TaskType {(byte)TaskType}, {TaskType}"); + writer.Write((byte)TaskType); + } + + protected void DeserializeCommon(BinaryReader reader) + { + State = (TaskState)reader.ReadByte(); + //Multiplayer.Log($"TaskNetworkData.DeserializeCommon() State {State}"); + TaskStartTime = reader.ReadSingle(); + //Multiplayer.Log($"TaskNetworkData.DeserializeCommon() TaskStartTime {TaskStartTime}"); + TaskFinishTime = reader.ReadSingle(); + //Multiplayer.Log($"TaskNetworkData.DeserializeCommon() TaskFinishTime {TaskFinishTime}"); + IsLastTask = reader.ReadBoolean(); + //Multiplayer.Log($"TaskNetworkData.DeserializeCommon() IsLastTask {IsLastTask}"); + TimeLimit = reader.ReadSingle(); + //Multiplayer.Log($"TaskNetworkData.DeserializeCommon() TimeLimit {TimeLimit}"); + TaskType = (TaskType)reader.ReadByte(); + //Multiplayer.Log($"TaskNetworkData.DeserializeCommon() TaskType {TaskType}"); + } +} + +#endregion diff --git a/MultiplayerAPI/Util/BinaryReaderWriterExtensions.cs b/MultiplayerAPI/Util/BinaryReaderWriterExtensions.cs new file mode 100644 index 00000000..cfa7f509 --- /dev/null +++ b/MultiplayerAPI/Util/BinaryReaderWriterExtensions.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MPAPI.Util; + +public static class BinaryReaderWriterExtensions +{ + public static void WriteUShortArray(this BinaryWriter writer, ushort[] array) + { + if (array == null) + { + writer.Write(0); + return; + } + writer.Write(array.Length); + foreach (ushort value in array) + { + writer.Write(value); + } + } + + public static void WriteInt32Array(this BinaryWriter writer, int[] array) + { + if (array == null) + { + writer.Write(0); + return; + } + writer.Write(array.Length); + foreach (int value in array) + { + writer.Write(value); + } + } + + public static ushort[] ReadUShortArray(this BinaryReader reader) + { + var length = reader.ReadInt32(); + + var ret = new ushort[length]; + for (int i = 0; i < length; i++) + { + ret[i] = reader.ReadUInt16(); + } + return ret; + } + + public static int[] ReadInt32Array(this BinaryReader reader) + { + var length = reader.ReadInt32(); + + var ret = new int[length]; + for (int i = 0; i < length; i++) + { + ret[i] = reader.ReadInt32(); + } + return ret; + } +} From 3aea96985c4a600752ff085333aa93592b3610fc Mon Sep 17 00:00:00 2001 From: Macka Date: Sat, 23 Aug 2025 23:43:03 +1000 Subject: [PATCH 437/521] Expose registration of custom task serialiser and deserialiser --- Multiplayer/API/APIProvider.cs | 12 +++++++ MultiplayerAPI/Interfaces/IMultiplayerAPI.cs | 36 +++++++++++++++++++- 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/Multiplayer/API/APIProvider.cs b/Multiplayer/API/APIProvider.cs index 75e471f3..4b9a1c55 100644 --- a/Multiplayer/API/APIProvider.cs +++ b/Multiplayer/API/APIProvider.cs @@ -1,7 +1,9 @@ +using DV.Logic.Job; using MPAPI.Interfaces; using MPAPI.Types; using Multiplayer.Components.Networking; using Multiplayer.Components.Networking.Train; +using Multiplayer.Networking.Data; using System; namespace Multiplayer.API; @@ -75,6 +77,16 @@ public void UnregisterPaintTheme(uint themeId) } } + public bool RegisterTaskType(TaskType taskType, Func converter, Func emptyCreator) where TGameTask : Task + { + return TaskNetworkDataFactory.RegisterTaskType(taskType, converter, emptyCreator); + } + + public bool UnRegisterTaskType(TaskType taskType) where TGameTask : Task + { + return TaskNetworkDataFactory.UnRegisterTaskType(taskType); + } + #region Class Helpers internal APIProvider() diff --git a/MultiplayerAPI/Interfaces/IMultiplayerAPI.cs b/MultiplayerAPI/Interfaces/IMultiplayerAPI.cs index 714dad33..88acdc96 100644 --- a/MultiplayerAPI/Interfaces/IMultiplayerAPI.cs +++ b/MultiplayerAPI/Interfaces/IMultiplayerAPI.cs @@ -1,3 +1,4 @@ +using DV.Logic.Job; using MPAPI.Types; using System; @@ -101,5 +102,38 @@ public interface IMultiplayerAPI void UnregisterPaintTheme(uint themeId); /// - /// Returns + /// Registers a serialiser and deserialiser for a custom type for multiplayer synchronization. + /// + /// The concrete type to register. + /// The enum value + /// + /// A function that takes an instance of and returns a corresponding object + /// for serialisation. + /// + /// + /// A function that takes a and returns an empty instance for deserialisation. + /// + /// + /// true if the task type was successfully registered; false if the task type was already registered or registration failed. + /// + /// + /// This method allows the Multiplayer mod to correctly serialise and deserialise custom or extended task types. + /// + bool RegisterTaskType(TaskType taskType, Func converter, Func emptyCreator) + where TGameTask : Task; + + /// + /// Unregisters a previously registered custom type. + /// + /// The concrete type to unregister. + /// The enum value associated with the task type to unregister. + /// + /// true if the task type was successfully unregistered; false if the task type is a base game task type. + /// + /// + /// This method allows removal of custom or extended task types from the multiplayer system. + /// Base task types cannot be unregistered. + /// + bool UnRegisterTaskType(TaskType taskType) where TGameTask : Task; + } From 721c4a9616e58f30f8ea936c65ec52e2eff18d96 Mon Sep 17 00:00:00 2001 From: Macka Date: Sun, 24 Aug 2025 13:16:42 +1000 Subject: [PATCH 438/521] Add documentation for TaskNetworkData --- MultiplayerAPI/Interfaces/IMultiplayerAPI.cs | 2 +- MultiplayerAPI/Types/TaskNetworkType.cs | 92 +++++++++++++++++--- 2 files changed, 81 insertions(+), 13 deletions(-) diff --git a/MultiplayerAPI/Interfaces/IMultiplayerAPI.cs b/MultiplayerAPI/Interfaces/IMultiplayerAPI.cs index 88acdc96..232d55a7 100644 --- a/MultiplayerAPI/Interfaces/IMultiplayerAPI.cs +++ b/MultiplayerAPI/Interfaces/IMultiplayerAPI.cs @@ -126,7 +126,7 @@ bool RegisterTaskType(TaskType taskType, Func type. /// /// The concrete type to unregister. - /// The enum value associated with the task type to unregister. + /// The enum value associated with the task type to unregister. /// /// true if the task type was successfully unregistered; false if the task type is a base game task type. /// diff --git a/MultiplayerAPI/Types/TaskNetworkType.cs b/MultiplayerAPI/Types/TaskNetworkType.cs index ec9fd6f3..ad39a784 100644 --- a/MultiplayerAPI/Types/TaskNetworkType.cs +++ b/MultiplayerAPI/Types/TaskNetworkType.cs @@ -5,55 +5,123 @@ namespace MPAPI.Types; #region TaskData Base Class +/// +/// Base class for serialising and deserialising job task data for transmission by Multiplayer mod. +/// Not intended for direct use; inherit via . +/// +/// public abstract class TaskNetworkData { + /// + /// Gets or sets the current state of the task. + /// See for possible values. + /// public TaskState State { get; set; } + + /// + /// Gets or sets the time at which the task started, in seconds since the job began. + /// public float TaskStartTime { get; set; } + + /// + /// Gets or sets the time at which the task finished, in seconds since the job began. + /// public float TaskFinishTime { get; set; } + + /// + /// Gets or sets a value indicating whether this is the last task in the job sequence. + /// public bool IsLastTask { get; set; } + + /// + /// Gets or sets the time limit for completing the task, in seconds. + /// public float TimeLimit { get; set; } + + /// + /// Gets or sets the type of the task. + /// See for possible values. + /// public TaskType TaskType { get; set; } + /// + /// Serializes the task network data to the specified . + /// Implementations should write all relevant fields for network transmission. + /// + /// + /// The first line of the implementation should call . + /// + /// The to write data to. public abstract void Serialize(BinaryWriter writer); + + /// + /// Deserializes the task network data from the specified . + /// Implementations should read all relevant fields in the same order and size as written by . + /// + /// + /// The first line of the implementation should call . + /// + /// The to read data from. public abstract void Deserialize(BinaryReader reader); + + /// + /// Converts this network data instance into a object + /// compatible with the job/task system. + /// + /// A instance representing the deserialized data. public abstract Task ToTask(); + + /// + /// Gets a list of car IDs () associated with this task. + /// + /// A list of car IDs relevant to the task. public abstract List GetCars(); } +/// +/// Generic abstract base class providing type-safe conversion for serialising and deserialising job task data. +/// Inherit from this class to implement serialisers for custom types. +/// +/// The concrete type that inherits from this class. public abstract class TaskNetworkData : TaskNetworkData where T : TaskNetworkData { + /// + /// Populates this instance from the specified object. + /// + /// The to extract data from. + /// This method is called by Multiplayer mod when serialising a job. + /// This instance, populated with data from the provided task. public abstract T FromTask(Task task); + /// + /// Serialises the common task data fields to the specified . + /// Should be called as the first step in the Serialize implementation of derived classes. + /// + /// The to write data to. protected void SerializeCommon(BinaryWriter writer) { - //Multiplayer.Log($"TaskNetworkData.SerializeCommon() State {(byte)State}, {State}"); writer.Write((byte)State); - //Multiplayer.Log($"TaskNetworkData.SerializeCommon() TaskStartTime {TaskStartTime}"); writer.Write(TaskStartTime); - //Multiplayer.Log($"TaskNetworkData.SerializeCommon() TaskFinishTime {TaskFinishTime}"); writer.Write(TaskFinishTime); - //Multiplayer.Log($"TaskNetworkData.SerializeCommon() IsLastTask {IsLastTask}"); writer.Write(IsLastTask); - //Multiplayer.Log($"TaskNetworkData.SerializeCommon() TimeLimit {TimeLimit}"); writer.Write(TimeLimit); - //Multiplayer.Log($"TaskNetworkData.SerializeCommon() TaskType {(byte)TaskType}, {TaskType}"); writer.Write((byte)TaskType); } + + /// + /// Deserialises the common task data fields from the specified . + /// Should be called as the first step in the Deserialize implementation of derived classes. + /// + /// The to read data from. protected void DeserializeCommon(BinaryReader reader) { State = (TaskState)reader.ReadByte(); - //Multiplayer.Log($"TaskNetworkData.DeserializeCommon() State {State}"); TaskStartTime = reader.ReadSingle(); - //Multiplayer.Log($"TaskNetworkData.DeserializeCommon() TaskStartTime {TaskStartTime}"); TaskFinishTime = reader.ReadSingle(); - //Multiplayer.Log($"TaskNetworkData.DeserializeCommon() TaskFinishTime {TaskFinishTime}"); IsLastTask = reader.ReadBoolean(); - //Multiplayer.Log($"TaskNetworkData.DeserializeCommon() IsLastTask {IsLastTask}"); TimeLimit = reader.ReadSingle(); - //Multiplayer.Log($"TaskNetworkData.DeserializeCommon() TimeLimit {TimeLimit}"); TaskType = (TaskType)reader.ReadByte(); - //Multiplayer.Log($"TaskNetworkData.DeserializeCommon() TaskType {TaskType}"); } } From 0fb30e5f9c2ad686aa95ac40a45a517fa3785d76 Mon Sep 17 00:00:00 2001 From: Macka Date: Sat, 30 Aug 2025 08:38:35 +1000 Subject: [PATCH 439/521] Add auto compression to ExternalSerializablePacketWrapper --- .../API/ExternalSerializablePacketWrapper.cs | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/Multiplayer/API/ExternalSerializablePacketWrapper.cs b/Multiplayer/API/ExternalSerializablePacketWrapper.cs index 8d3972da..f5b7d404 100644 --- a/Multiplayer/API/ExternalSerializablePacketWrapper.cs +++ b/Multiplayer/API/ExternalSerializablePacketWrapper.cs @@ -1,7 +1,5 @@ using LiteNetLib.Utils; using MPAPI.Interfaces.Packets; -using System; -using System.Collections.Generic; using System.IO; namespace Multiplayer.API; @@ -12,23 +10,43 @@ namespace Multiplayer.API; /// The packet type public class ExternalSerializablePacketWrapper : INetSerializable where T : class, ISerializablePacket, new() { + const int COMPRESSION_THRESHOLD = 1024; + public T Packet { get; set; } public void Serialize(NetDataWriter writer) { + byte[] data; + using var memoryStream = new MemoryStream(); using var binaryWriter = new BinaryWriter(memoryStream); Packet.Serialize(binaryWriter); - var data = memoryStream.ToArray(); + data = memoryStream.ToArray(); + + bool shouldCompress = memoryStream.Length >= COMPRESSION_THRESHOLD; + writer.Put(shouldCompress); + + if (shouldCompress) + { + var lenBefore = data.Length; + data = PacketCompression.Compress(data); + + Multiplayer.LogDebug(() => $"ExternalSerializablePacketWrapper<{typeof(T).Name}>: Compressed {lenBefore} to {data.Length} bytes"); + } + writer.PutBytesWithLength(data); } public void Deserialize(NetDataReader reader) { + bool isCompressed = reader.GetBool(); var data = reader.GetBytesWithLength(); + if (isCompressed) + data = PacketCompression.Decompress(data); + using var memoryStream = new MemoryStream(data); using var binaryReader = new BinaryReader(memoryStream); From 2bf163851f9a7de58cec71ff85ec4615d9a87c58 Mon Sep 17 00:00:00 2001 From: Macka Date: Sat, 30 Aug 2025 08:42:53 +1000 Subject: [PATCH 440/521] fix possible null reference issues --- Multiplayer/Networking/Data/TaskNetworkData.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Multiplayer/Networking/Data/TaskNetworkData.cs b/Multiplayer/Networking/Data/TaskNetworkData.cs index 55ee020c..aabf8271 100644 --- a/Multiplayer/Networking/Data/TaskNetworkData.cs +++ b/Multiplayer/Networking/Data/TaskNetworkData.cs @@ -130,7 +130,7 @@ public override void Serialize(BinaryWriter writer) SerializeCommon(writer); writer.WriteUShortArray(CarNetIDs); writer.Write((byte)WarehouseTaskType); - writer.Write(WarehouseMachine); + writer.Write(WarehouseMachine ?? string.Empty); writer.Write((int)CargoType); writer.Write(CargoAmount); writer.Write(ReadyForMachine); From 3706c6e8f9145f38137b301d28667ecd008d4849 Mon Sep 17 00:00:00 2001 From: Macka Date: Sat, 30 Aug 2025 12:32:47 +1000 Subject: [PATCH 441/521] Add ColorSerializer for Unity.Color --- .../Networking/Managers/NetworkManager.cs | 1 + .../Serialization/ColorSerializer.cs | 22 +++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 Multiplayer/Networking/Serialization/ColorSerializer.cs diff --git a/Multiplayer/Networking/Managers/NetworkManager.cs b/Multiplayer/Networking/Managers/NetworkManager.cs index 8d79e0e4..2445c57b 100644 --- a/Multiplayer/Networking/Managers/NetworkManager.cs +++ b/Multiplayer/Networking/Managers/NetworkManager.cs @@ -62,6 +62,7 @@ private void RegisterNestedTypes() netPacketProcessor.RegisterNestedType(TrainCarHealthData.Serialize, TrainCarHealthData.Deserialize); netPacketProcessor.RegisterNestedType(Vector2Serializer.Serialize, Vector2Serializer.Deserialize); netPacketProcessor.RegisterNestedType(Vector3Serializer.Serialize, Vector3Serializer.Deserialize); + netPacketProcessor.RegisterNestedType(ColorSerializer.Serialize, ColorSerializer.Deserialize); } private void OnSettingsUpdated(Settings settings) diff --git a/Multiplayer/Networking/Serialization/ColorSerializer.cs b/Multiplayer/Networking/Serialization/ColorSerializer.cs new file mode 100644 index 00000000..1493cd91 --- /dev/null +++ b/Multiplayer/Networking/Serialization/ColorSerializer.cs @@ -0,0 +1,22 @@ +using LiteNetLib.Utils; +using Multiplayer.Utils; +using UnityEngine; + +namespace Multiplayer.Networking.Serialization +{ + public class ColorSerializer + { + public static void Serialize(NetDataWriter writer, Color colour) + { + writer.Put(colour.ColorToUInt32()); + } + + public static Color Deserialize(NetDataReader reader) + { + var colour = reader.GetUInt(); + + return colour.UInt32ToColor(); + } + } + +} From 6e772f87ebe966fa8fbca1d7ac42159e1ab13a34 Mon Sep 17 00:00:00 2001 From: Macka Date: Sat, 30 Aug 2025 13:56:30 +1000 Subject: [PATCH 442/521] Add Vector3 and Quaternion serialisation and XML docs --- .../Util/BinaryReaderWriterExtensions.cs | 86 +++++++++++++++++-- 1 file changed, 80 insertions(+), 6 deletions(-) diff --git a/MultiplayerAPI/Util/BinaryReaderWriterExtensions.cs b/MultiplayerAPI/Util/BinaryReaderWriterExtensions.cs index cfa7f509..fe8abdf8 100644 --- a/MultiplayerAPI/Util/BinaryReaderWriterExtensions.cs +++ b/MultiplayerAPI/Util/BinaryReaderWriterExtensions.cs @@ -1,14 +1,18 @@ -using System; -using System.Collections.Generic; using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using UnityEngine; namespace MPAPI.Util; +/// +/// Provides extension methods for and to handle arrays and Unity types. +/// public static class BinaryReaderWriterExtensions { + /// + /// Serialises a array. + /// + /// The to write to. + /// The array to write. If null, writes 0 as the length. public static void WriteUShortArray(this BinaryWriter writer, ushort[] array) { if (array == null) @@ -22,7 +26,12 @@ public static void WriteUShortArray(this BinaryWriter writer, ushort[] array) writer.Write(value); } } - + + /// + /// Serialises an array. + /// + /// The to write to. + /// The array to serialise. If null, serialises 0 as the length. public static void WriteInt32Array(this BinaryWriter writer, int[] array) { if (array == null) @@ -37,6 +46,36 @@ public static void WriteInt32Array(this BinaryWriter writer, int[] array) } } + /// + /// Serialises a . + /// + /// The to write to. + /// The to serialise. + public static void WriteVector3(this BinaryWriter writer, Vector3 vector) + { + writer.Write(vector.x); + writer.Write(vector.y); + writer.Write(vector.z); + } + + /// + /// Serialises a . + /// + /// The to write to. + /// The to serialise. + public static void WriteQuaternion(this BinaryWriter writer, Quaternion quaternion) + { + writer.Write(quaternion.w); + writer.Write(quaternion.x); + writer.Write(quaternion.y); + writer.Write(quaternion.z); + } + + /// + /// Deserialises a array. + /// + /// The to deserialise from. + /// The deserialised array. public static ushort[] ReadUShortArray(this BinaryReader reader) { var length = reader.ReadInt32(); @@ -49,6 +88,11 @@ public static ushort[] ReadUShortArray(this BinaryReader reader) return ret; } + /// + /// Deserialises an array. + /// + /// The to deserialise from. + /// The deserialised array. public static int[] ReadInt32Array(this BinaryReader reader) { var length = reader.ReadInt32(); @@ -60,4 +104,34 @@ public static int[] ReadInt32Array(this BinaryReader reader) } return ret; } + + /// + /// Deserialises a . + /// + /// The to deserialise from. + /// The deserialised . + public static Vector3 ReadVector3(this BinaryReader reader) + { + float x = reader.ReadSingle(); + float y = reader.ReadSingle(); + float z = reader.ReadSingle(); + + return new Vector3(x, y, z); + } + + /// + /// Deserialises a . + /// + /// The to deserialise from. + /// The deserialised . + + public static Quaternion ReadQuaternion(this BinaryReader reader) + { + float w = reader.ReadSingle(); + float x = reader.ReadSingle(); + float y = reader.ReadSingle(); + float z = reader.ReadSingle(); + + return new Quaternion(x, y, z, w); + } } From 4d2f902b960208208da1a5a108a2b150e5506c14 Mon Sep 17 00:00:00 2001 From: Macka Date: Sat, 30 Aug 2025 15:14:16 +1000 Subject: [PATCH 443/521] Fix incorrect IsHost() in ServerPlayerWrapper --- Multiplayer/API/ServerPlayerWrapper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Multiplayer/API/ServerPlayerWrapper.cs b/Multiplayer/API/ServerPlayerWrapper.cs index d5d7c444..a347234e 100644 --- a/Multiplayer/API/ServerPlayerWrapper.cs +++ b/Multiplayer/API/ServerPlayerWrapper.cs @@ -15,7 +15,7 @@ public class ServerPlayerWrapper : IPlayer public ServerPlayerWrapper(ServerPlayer serverPlayer) { _serverPlayer = serverPlayer; - _isHost = NetworkLifecycle.Instance?.IsHost() ?? false; + _isHost = NetworkLifecycle.Instance?.IsHost(serverPlayer.Peer) ?? false; } public byte Id => _serverPlayer.Id; From 9fb28f37a40fde9d07299ee34a572e025a42288b Mon Sep 17 00:00:00 2001 From: Macka Date: Tue, 2 Sep 2025 22:31:53 +1000 Subject: [PATCH 444/521] Refactor use of player.Id and peer.id --- Multiplayer/API/ClientAPIProvider.cs | 10 +- Multiplayer/API/ClientPlayerWrapper.cs | 2 +- Multiplayer/API/ServerAPIProvider.cs | 10 +- Multiplayer/API/ServerPlayerWrapper.cs | 4 +- .../Components/Networking/NetworkLifecycle.cs | 46 +++---- .../Networking/Player/NetworkedPlayer.cs | 4 +- .../Networking/Player/NetworkedWorldMap.cs | 2 +- .../Networking/Train/NetworkedTrainCar.cs | 8 +- .../SaveGame/NetworkedSaveGameManager.cs | 6 +- Multiplayer/Networking/Data/ServerPlayer.cs | 23 ++-- .../Managers/Client/ClientPlayerManager.cs | 28 ++-- .../Managers/Client/NetworkClient.cs | 20 +-- .../Networking/Managers/Server/ChatManager.cs | 24 ++-- .../Managers/Server/NetworkServer.cs | 123 +++++++++++------- .../ClientboundLoginResponsePacket.cs | 2 +- .../ClientboundPingUpdatePacket.cs | 2 +- .../ClientboundPlayerDisconnectPacket.cs | 2 +- .../ClientboundPlayerJoinedPacket.cs | 2 +- .../ClientboundPlayerPositionPacket.cs | 2 +- .../TestComponents/ClientTest.cs | 6 +- .../TestComponents/ServerTest.cs | 10 +- MultiplayerAPI/Interfaces/IMultiplayerAPI.cs | 2 +- MultiplayerAPI/Interfaces/IPlayer.cs | 2 +- 23 files changed, 182 insertions(+), 158 deletions(-) diff --git a/Multiplayer/API/ClientAPIProvider.cs b/Multiplayer/API/ClientAPIProvider.cs index 84affd1e..05e92fb2 100644 --- a/Multiplayer/API/ClientAPIProvider.cs +++ b/Multiplayer/API/ClientAPIProvider.cs @@ -21,9 +21,9 @@ public class ClientAPIProvider : IClient public IReadOnlyCollection Players => client.ClientPlayerManager.Players.Select(GetWrapper).ToList().AsReadOnly(); public int PlayerCount => client.ClientPlayerManager.Players.Count + 1; // add 1 for local player - public IPlayer GetPlayer(byte id) + public IPlayer GetPlayer(byte playerId) { - _playerWrapperCache.TryGetValue(id, out var player); + _playerWrapperCache.TryGetValue(playerId, out var player); return player; } @@ -72,10 +72,10 @@ internal void Dispose() private ClientPlayerWrapper GetWrapper(NetworkedPlayer networkedPlayer) { - if (!_playerWrapperCache.TryGetValue(networkedPlayer.Id, out var wrapper)) + if (!_playerWrapperCache.TryGetValue(networkedPlayer.PlayerId, out var wrapper)) { wrapper = new ClientPlayerWrapper(networkedPlayer); - _playerWrapperCache[networkedPlayer.Id] = wrapper; + _playerWrapperCache[networkedPlayer.PlayerId] = wrapper; } return wrapper; } @@ -88,7 +88,7 @@ private void OnPlayerConnectedInternal(Components.Networking.Player.NetworkedPla private void OnPlayerDisconnectedInternal(Components.Networking.Player.NetworkedPlayer networkedPlayer) { OnPlayerDisconnected?.Invoke(GetWrapper(networkedPlayer)); - _playerWrapperCache.Remove(networkedPlayer.Id); + _playerWrapperCache.Remove(networkedPlayer.PlayerId); } #endregion } diff --git a/Multiplayer/API/ClientPlayerWrapper.cs b/Multiplayer/API/ClientPlayerWrapper.cs index f96ea488..7cd79f2c 100644 --- a/Multiplayer/API/ClientPlayerWrapper.cs +++ b/Multiplayer/API/ClientPlayerWrapper.cs @@ -15,7 +15,7 @@ public ClientPlayerWrapper(NetworkedPlayer networkedPlayer, bool isHost = false) _isHost = isHost; } - public byte Id => _networkedPlayer.Id; + public byte PlayerId => _networkedPlayer.PlayerId; public string Username { get => _networkedPlayer.Username; diff --git a/Multiplayer/API/ServerAPIProvider.cs b/Multiplayer/API/ServerAPIProvider.cs index 2e4a7983..4bf1326a 100644 --- a/Multiplayer/API/ServerAPIProvider.cs +++ b/Multiplayer/API/ServerAPIProvider.cs @@ -27,9 +27,9 @@ public class ServerAPIProvider : IServer public IReadOnlyCollection Players => server.ServerPlayers.Select(GetWrapper).ToList().AsReadOnly(); - public IPlayer GetPlayer(byte id) + public IPlayer GetPlayer(byte PlayerId) { - _playerWrapperCache.TryGetValue(id, out var player); + _playerWrapperCache.TryGetValue(PlayerId, out var player); return player; } #endregion @@ -138,10 +138,10 @@ internal ServerAPIProvider(NetworkServer serverInstance) private ServerPlayerWrapper GetWrapper(ServerPlayer serverPlayer) { - if (!_playerWrapperCache.TryGetValue(serverPlayer.Id, out var wrapper)) + if (!_playerWrapperCache.TryGetValue(serverPlayer.PlayerId, out var wrapper)) { wrapper = new ServerPlayerWrapper(serverPlayer); - _playerWrapperCache[serverPlayer.Id] = wrapper; + _playerWrapperCache[serverPlayer.PlayerId] = wrapper; } return wrapper; } @@ -189,7 +189,7 @@ private void OnPlayerConnectedInternal(ServerPlayer serverPlayer) private void OnPlayerDisconnectedInternal(ServerPlayer serverPlayer) { OnPlayerDisconnected?.Invoke(GetWrapper(serverPlayer)); - _playerWrapperCache.Remove(serverPlayer.Id); + _playerWrapperCache.Remove(serverPlayer.PlayerId); } private void OnPlayerReadyInternal(ServerPlayer serverPlayer) diff --git a/Multiplayer/API/ServerPlayerWrapper.cs b/Multiplayer/API/ServerPlayerWrapper.cs index a347234e..93eeb173 100644 --- a/Multiplayer/API/ServerPlayerWrapper.cs +++ b/Multiplayer/API/ServerPlayerWrapper.cs @@ -15,10 +15,10 @@ public class ServerPlayerWrapper : IPlayer public ServerPlayerWrapper(ServerPlayer serverPlayer) { _serverPlayer = serverPlayer; - _isHost = NetworkLifecycle.Instance?.IsHost(serverPlayer.Peer) ?? false; + _isHost = NetworkLifecycle.Instance?.IsHost(serverPlayer) ?? false; } - public byte Id => _serverPlayer.Id; + public byte PlayerId => _serverPlayer.PlayerId; public string Username { diff --git a/Multiplayer/Components/Networking/NetworkLifecycle.cs b/Multiplayer/Components/Networking/NetworkLifecycle.cs index 8b97571e..8c866e81 100644 --- a/Multiplayer/Components/Networking/NetworkLifecycle.cs +++ b/Multiplayer/Components/Networking/NetworkLifecycle.cs @@ -1,25 +1,20 @@ using DV.Scenarios.Common; using DV.Utils; using LiteNetLib; -using LiteNetLib.Utils; using MPAPI; using Multiplayer.API; using Multiplayer.Components.Networking.UI; using Multiplayer.Networking.Data; -using Multiplayer.Networking.Managers; using Multiplayer.Networking.Managers.Client; using Multiplayer.Networking.Managers.Server; -using Multiplayer.Networking.TransportLayers; +using Multiplayer.Networking.Managers; using Multiplayer.Utils; -using Newtonsoft.Json; -using Steamworks; -using System; -using System.Collections; using System.Collections.Generic; +using System.Collections; using System.Net; -using System.Text; -using UnityEngine; +using System; using UnityEngine.SceneManagement; +using UnityEngine; namespace Multiplayer.Components.Networking; @@ -54,9 +49,9 @@ public class NetworkLifecycle : SingletonBehaviour /// Whether the provided ITransportPeer is the host. /// Note that this does NOT check authority, and should only be used for client-only logic. /// - public bool IsHost(ITransportPeer peer) + public bool IsHost(ServerPlayer player) { - return Server?.IsRunning == true && Client?.IsRunning == true && Client?.SelfPeer?.Id == peer?.Id; + return Server?.IsRunning == true && Client?.IsRunning == true && Client.PlayerId == player.PlayerId; } /// @@ -65,7 +60,7 @@ public bool IsHost(ITransportPeer peer) /// public bool IsHost() { - return IsHost(Client?.SelfPeer); + return Server?.IsRunning == true; } private readonly Queue mainMenuLoadedQueue = new(); @@ -152,7 +147,7 @@ public bool StartServer(IDifficulty difficulty) return true; } - public void StartClient(string address, int port, string password, bool isSinglePlayer, Action onDisconnect ) + public void StartClient(string address, int port, string password, bool isSinglePlayer, Action onDisconnect) { if (Client != null) throw new InvalidOperationException("NetworkManager already exists!"); @@ -190,10 +185,10 @@ private IEnumerator PollEvents() tickWatchdog.Stop(time => Multiplayer.LogWarning($"OnTick took {time} ms!")); } - if(Client != null) + if (Client != null) TickManager(Client); - if(Server != null) + if (Server != null) TickManager(Server); float elapsedTime = tickTimer.Stop(); @@ -226,18 +221,23 @@ private void TickManager(NetworkManager manager) public void Stop() { Stats?.Hide(); - Server?.Stop(); - Client?.Stop(); - // Clear API registrations - MultiplayerAPI.ClearServer(); - MultiplayerAPI.ClearClient(); + if (Server != null) + { + Server?.Stop(); + MultiplayerAPI.ClearServer(); + Server = null; + } - Server = null; - Client = null; + if (Client != null) + { + Client?.Stop(); + MultiplayerAPI.ClearClient(); + Client = null; + } } - private void OnApplicationQuit() + protected void OnApplicationQuit() { Stop(); } diff --git a/Multiplayer/Components/Networking/Player/NetworkedPlayer.cs b/Multiplayer/Components/Networking/Player/NetworkedPlayer.cs index 8aff731b..9e5da4f5 100644 --- a/Multiplayer/Components/Networking/Player/NetworkedPlayer.cs +++ b/Multiplayer/Components/Networking/Player/NetworkedPlayer.cs @@ -1,5 +1,3 @@ -using System; -using MPAPI.Interfaces; using Multiplayer.Components.Networking.Train; using Multiplayer.Editor.Components.Player; using UnityEngine; @@ -10,7 +8,7 @@ public class NetworkedPlayer : MonoBehaviour { private const float LERP_SPEED = 5.0f; - public byte Id { get; set; } + public byte PlayerId { get; set; } private AnimationHandler animationHandler; private NameTag nameTag; diff --git a/Multiplayer/Components/Networking/Player/NetworkedWorldMap.cs b/Multiplayer/Components/Networking/Player/NetworkedWorldMap.cs index 53850f6e..0c702f03 100644 --- a/Multiplayer/Components/Networking/Player/NetworkedWorldMap.cs +++ b/Multiplayer/Components/Networking/Player/NetworkedWorldMap.cs @@ -93,7 +93,7 @@ public void UpdatePlayers() 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.Id, out NetworkedPlayer networkedPlayer)) + if (!NetworkLifecycle.Instance.Client.ClientPlayerManager.TryGetPlayer(kvp.Key.PlayerId, out NetworkedPlayer networkedPlayer)) { Multiplayer.LogWarning($"Player indicator for {kvp.Key} exists but {nameof(NetworkedPlayer)} does not!"); OnPlayerDisconnected(kvp.Key); diff --git a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs index ea863c5e..a5553f63 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs @@ -580,7 +580,7 @@ private void Server_SendHealthState() public bool Server_ValidateCouplerInteraction(CommonCouplerInteractionPacket packet, ServerPlayer player) { Multiplayer.LogDebug(() => - $"Server_ValidateCouplerInteraction([[{(CouplerInteractionType)packet.Flags}], {CurrentID}, {packet.NetId}], {player.Id}) " + + $"Server_ValidateCouplerInteraction([[{(CouplerInteractionType)packet.Flags}], {CurrentID}, {packet.NetId}], {player.PlayerId}) " + $"isFront: {packet.IsFrontCoupler}, frontInteracting: {frontInteracting}, frontInteractionPeer: {frontInteractionPlayer}, " + $"rearInteracting: {rearInteracting}, rearInteractionPeer: {rearInteractionPlayer}" ); @@ -589,11 +589,11 @@ public bool Server_ValidateCouplerInteraction(CommonCouplerInteractionPacket pac if (packet.IsFrontCoupler && frontInteracting && player != frontInteractionPlayer || packet.IsFrontCoupler == false && rearInteracting && player != rearInteractionPlayer) { - Multiplayer.LogDebug(() => $"Server_ValidateCouplerInteraction([{packet.Flags}, {CurrentID}, {packet.NetId}], {player.Id}) Failed to validate!"); + Multiplayer.LogDebug(() => $"Server_ValidateCouplerInteraction([{packet.Flags}, {CurrentID}, {packet.NetId}], {player.PlayerId}) Failed to validate!"); return false; } - Multiplayer.LogDebug(() => $"Server_ValidateCouplerInteraction([{packet.Flags}, {CurrentID}, {packet.NetId}], {player.Id}) No one interacting"); + Multiplayer.LogDebug(() => $"Server_ValidateCouplerInteraction([{packet.Flags}, {CurrentID}, {packet.NetId}], {player.PlayerId}) No one interacting"); if (((CouplerInteractionType)packet.Flags).HasFlag(CouplerInteractionType.Start)) { @@ -618,7 +618,7 @@ public bool Server_ValidateCouplerInteraction(CommonCouplerInteractionPacket pac //todo: Additional checks for player location/proximity - Multiplayer.LogDebug(() => $"Server_ValidateCouplerInteraction([{packet.Flags}, {CurrentID}, {packet.NetId}], {player.Id}) Validation passed!"); + Multiplayer.LogDebug(() => $"Server_ValidateCouplerInteraction([{packet.Flags}, {CurrentID}, {packet.NetId}], {player.PlayerId}) Validation passed!"); return true; } diff --git a/Multiplayer/Components/SaveGame/NetworkedSaveGameManager.cs b/Multiplayer/Components/SaveGame/NetworkedSaveGameManager.cs index 5ee3a8f0..f93873fe 100644 --- a/Multiplayer/Components/SaveGame/NetworkedSaveGameManager.cs +++ b/Multiplayer/Components/SaveGame/NetworkedSaveGameManager.cs @@ -1,4 +1,3 @@ -using System; using DV.InventorySystem; using DV.JObjectExtstensions; using DV.ThingTypes; @@ -6,8 +5,8 @@ using JetBrains.Annotations; using Multiplayer.Components.Networking; using Multiplayer.Networking.Data; -using Multiplayer.Networking.Managers.Server; using Newtonsoft.Json.Linq; +using System; namespace Multiplayer.Components.SaveGame; @@ -69,8 +68,9 @@ public void Server_UpdateInternalData(SaveGameData data) foreach (ServerPlayer player in NetworkLifecycle.Instance.Server.ServerPlayers) { - if (player.Id == NetworkServer.SelfId || !player.IsLoaded) + if (player.Peer == NetworkLifecycle.Instance.Server.SelfPeer || !player.IsLoaded) continue; + JObject playerData = []; playerData.SetVector3(SaveGameKeys.Player_position, player.AbsoluteWorldPosition); playerData.SetFloat(SaveGameKeys.Player_rotation, player.WorldRotationY); diff --git a/Multiplayer/Networking/Data/ServerPlayer.cs b/Multiplayer/Networking/Data/ServerPlayer.cs index 381250db..1092394c 100644 --- a/Multiplayer/Networking/Data/ServerPlayer.cs +++ b/Multiplayer/Networking/Data/ServerPlayer.cs @@ -1,11 +1,10 @@ -using MPAPI.Interfaces; -using Multiplayer.Components.Networking; using Multiplayer.Components.Networking.Train; using Multiplayer.Components.Networking.World; +using Multiplayer.Components.Networking; using Multiplayer.Networking.TransportLayers; using Multiplayer.Utils; -using System; using System.Collections.Generic; +using System; using UnityEngine; namespace Multiplayer.Networking.Data; @@ -13,20 +12,21 @@ namespace Multiplayer.Networking.Data; public class ServerPlayer : IDisposable { #region ID Management - private readonly IdPool idPool; + private static readonly IdPool idPool = new(); public void Dispose() { - if (Id != 0) + Multiplayer.LogDebug(() => $"Disposing ServerPlayer {Username} ({PlayerId})"); + if (PlayerId != 0) { - idPool.ReleaseId(Id); - Id = 0; + idPool.ReleaseId(PlayerId); + PlayerId = 0; } } #endregion public ITransportPeer Peer { get; private set; } - public byte Id { get; private set; } + public byte PlayerId { get; private set; } public bool IsLoaded { get; set; } public string Username { get; set; } public string OriginalUsername { get; set; } @@ -43,10 +43,9 @@ public void Dispose() private Vector3 _lastWorldPos = Vector3.zero; private Vector3 _lastAbsoluteWorldPosition = Vector3.zero; - public ServerPlayer(IdPool idPool, ITransportPeer peer, string username, string originalUsername, Guid guid) + public ServerPlayer(ITransportPeer peer, string username, string originalUsername, Guid guid) { - this.idPool = idPool; - Id = idPool.NextId; + PlayerId = idPool.NextId; Peer = peer; @@ -173,6 +172,6 @@ public bool TryGetOwnedItem(ushort itemNetId, out NetworkedItem item) public override string ToString() { - return $"{Id} ({Username}, {Guid.ToString()})"; + return $"{PlayerId} ({Username}, {Guid.ToString()})"; } } diff --git a/Multiplayer/Networking/Managers/Client/ClientPlayerManager.cs b/Multiplayer/Networking/Managers/Client/ClientPlayerManager.cs index 2a7175af..0728d8ad 100644 --- a/Multiplayer/Networking/Managers/Client/ClientPlayerManager.cs +++ b/Multiplayer/Networking/Managers/Client/ClientPlayerManager.cs @@ -1,7 +1,7 @@ -using System; -using System.Collections.Generic; using DV; using Multiplayer.Components.Networking.Player; +using System.Collections.Generic; +using System; using UnityEngine; using Object = UnityEngine.Object; @@ -22,43 +22,43 @@ public ClientPlayerManager() playerPrefab = Multiplayer.AssetIndex.playerPrefab; } - public bool TryGetPlayer(byte id, out NetworkedPlayer player) + public bool TryGetPlayer(byte playerid, out NetworkedPlayer player) { - return playerMap.TryGetValue(id, out player); + return playerMap.TryGetValue(playerid, out player); } - public void AddPlayer(byte id, string username) + public void AddPlayer(byte playerId, string username) { GameObject go = Object.Instantiate(playerPrefab, WorldMover.OriginShiftParent); go.layer = LayerMask.NameToLayer(Layers.Player); NetworkedPlayer networkedPlayer = go.AddComponent(); - networkedPlayer.Id = id; + networkedPlayer.PlayerId = playerId; networkedPlayer.Username = username; //networkedPlayer.Guid = guid; - playerMap.Add(id, networkedPlayer); + playerMap.Add(playerId, networkedPlayer); OnPlayerConnected?.Invoke(networkedPlayer); } - public void RemovePlayer(byte id) + public void RemovePlayer(byte playerid) { - if (!playerMap.TryGetValue(id, out NetworkedPlayer networkedPlayer)) + if (!TryGetPlayer(playerid, out NetworkedPlayer networkedPlayer)) return; OnPlayerDisconnected?.Invoke(networkedPlayer); Object.Destroy(networkedPlayer.gameObject); - playerMap.Remove(id); + playerMap.Remove(playerid); } - public void UpdatePing(byte id, int ping) + public void UpdatePing(byte playerId, int ping) { - if (!playerMap.TryGetValue(id, out NetworkedPlayer player)) + if (!TryGetPlayer(playerId, out NetworkedPlayer player)) return; player.SetPing(ping); } - public void UpdatePosition(byte id, Vector3 position, Vector3 moveDir, float rotation, bool isJumping, bool isOnCar, ushort carId) + public void UpdatePosition(byte playerid, Vector3 position, Vector3 moveDir, float rotation, bool isJumping, bool isOnCar, ushort carId) { - if (!playerMap.TryGetValue(id, out NetworkedPlayer player)) + if (!TryGetPlayer(playerid, out NetworkedPlayer player)) return; player.UpdateCar(carId); player.UpdatePosition(position, moveDir, rotation, isJumping, isOnCar); diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index f16c2ffb..5acce2e3 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -53,7 +53,8 @@ public class NetworkClient : NetworkManager private Action onDisconnect; private string disconnectMessage; - public ITransportPeer SelfPeer { get; private set; } + private ITransportPeer selfPeer; + public byte PlayerId{ get; private set; } public readonly ClientPlayerManager ClientPlayerManager; // One way ping in milliseconds @@ -89,7 +90,7 @@ public void Start(string address, int port, string password, bool isSinglePlayer Mods = ModCompatibilityManager.Instance.GetLocalMods() }; netPacketProcessor.Write(cachedWriter, serverboundClientLoginPacket); - SelfPeer = Connect(address, port, cachedWriter); + selfPeer = Connect(address, port, cachedWriter); isAlsoHost = NetworkLifecycle.Instance.IsServerRunning; originalSession = UserManager.Instance.CurrentUser.CurrentSession; @@ -254,8 +255,9 @@ private void OnClientboundLoginResponsePacket(ClientboundLoginResponsePacket pac if (packet.Accepted) { Log($"Received player accepted packet"); + PlayerId = packet.PlayerId; - if (NetworkLifecycle.Instance.IsHost(SelfPeer)) + if (NetworkLifecycle.Instance.IsHost()) SendReadyPacket(); else SendSaveGameDataRequest(); @@ -290,16 +292,16 @@ private void OnClientboundLoginResponsePacket(ClientboundLoginResponsePacket pac private void OnClientboundPlayerJoinedPacket(ClientboundPlayerJoinedPacket packet) { //Guid guid = new(packet.Guid); - ClientPlayerManager.AddPlayer(packet.Id, packet.Username); + ClientPlayerManager.AddPlayer(packet.PlayerId, packet.Username); - ClientPlayerManager.UpdatePosition(packet.Id, packet.Position, Vector3.zero, packet.Rotation, false, packet.CarID != 0, packet.CarID); + ClientPlayerManager.UpdatePosition(packet.PlayerId, packet.Position, Vector3.zero, packet.Rotation, false, packet.CarID != 0, packet.CarID); } //For other player left the game private void OnClientboundPlayerDisconnectPacket(ClientboundPlayerDisconnectPacket packet) { - Log($"Received player disconnect packet for player id: {packet.Id}"); - ClientPlayerManager.RemovePlayer(packet.Id); + Log($"Received player disconnect packet for player id: {packet.PlayerId}"); + ClientPlayerManager.RemovePlayer(packet.PlayerId); } @@ -318,12 +320,12 @@ private void OnClientboundDisconnectPacket(ClientboundDisconnectPacket packet) } private void OnClientboundPlayerPositionPacket(ClientboundPlayerPositionPacket packet) { - ClientPlayerManager.UpdatePosition(packet.Id, packet.Position, packet.MoveDir, packet.RotationY, packet.IsJumping, packet.IsOnCar, packet.CarID); + ClientPlayerManager.UpdatePosition(packet.PlayerId, packet.Position, packet.MoveDir, packet.RotationY, packet.IsJumping, packet.IsOnCar, packet.CarID); } private void OnClientboundPingUpdatePacket(ClientboundPingUpdatePacket packet) { - ClientPlayerManager.UpdatePing(packet.Id, packet.Ping); + ClientPlayerManager.UpdatePing(packet.PlayerId, packet.Ping); } private void OnClientboundTickSyncPacket(ClientboundTickSyncPacket packet) diff --git a/Multiplayer/Networking/Managers/Server/ChatManager.cs b/Multiplayer/Networking/Managers/Server/ChatManager.cs index 46409b2b..6d836f79 100644 --- a/Multiplayer/Networking/Managers/Server/ChatManager.cs +++ b/Multiplayer/Networking/Managers/Server/ChatManager.cs @@ -1,14 +1,16 @@ using Multiplayer.Components.Networking; using Multiplayer.Networking.Data; -using System; using System.Collections.Generic; using System.Linq; -using System.Text; using System.Text.RegularExpressions; +using System.Text; +using System; namespace Multiplayer.Networking.Managers.Server; + public delegate void ChatCommandCallbackInternal(string message, ServerPlayer sender); public delegate bool ChatFilterDelegateInternal(ref string message, ServerPlayer sender); + public class ChatManager { public const string COMMAND_SERVER = "server"; @@ -173,7 +175,7 @@ private void ProcessChatMessage(string message, ServerPlayer sender) public void ServerMessage(string message, ServerPlayer sender, ServerPlayer exclude = null) { //If user is not the host, we should ignore - will require changes for dedicated server - if (sender != null && !NetworkLifecycle.Instance.IsHost(sender.Peer)) + if (sender != null && !NetworkLifecycle.Instance.IsHost(sender)) return; message = $"{message}"; @@ -182,7 +184,7 @@ public void ServerMessage(string message, ServerPlayer sender, ServerPlayer excl private void WhisperMessage(string message, ServerPlayer sender) { - Multiplayer.LogDebug(() => $"Whispering: \"{message}\", sender: {sender?.Username}, senderID: {sender?.Id}"); + Multiplayer.LogDebug(() => $"Whispering: \"{message}\", sender: {sender?.Username}, senderID: {sender?.PlayerId}"); if (sender == null) return; @@ -199,20 +201,20 @@ private void WhisperMessage(string message, ServerPlayer sender) string whisperMessage = parts[1]; - Multiplayer.LogDebug(() => $"Whispering parse 1: \"{message}\", sender: {sender?.Username}, senderID: {sender?.Id}, peerName: {recipientName}"); + Multiplayer.LogDebug(() => $"Whispering parse 1: \"{message}\", sender: {sender?.Username}, senderID: {sender?.PlayerId}, peerName: {recipientName}"); //look up the peer ID var recipient = ServerPlayerFromUsername(recipientName); if (recipient == null) { - Multiplayer.LogDebug(() => $"Whispering failed: \"{message}\", sender: {sender?.Username}, senderID: {sender?.Id}, peerName: {recipientName}"); + Multiplayer.LogDebug(() => $"Whispering failed: \"{message}\", sender: {sender?.Username}, senderID: {sender?.PlayerId}, peerName: {recipientName}"); whisperMessage = $"{Locale.Get(Locale.CHAT_WHISPER_NOT_FOUND_KEY, recipientName)}"; NetworkLifecycle.Instance.Server.SendWhisper(whisperMessage, sender); return; } - Multiplayer.LogDebug(() => $"Whispering parse 2: \"{message}\", sender: {sender?.Username}, senderID: {sender?.Id}, peerName: {recipientName}, peerID: {recipient?.Id}"); + Multiplayer.LogDebug(() => $"Whispering parse 2: \"{message}\", sender: {sender?.Username}, senderID: {sender?.PlayerId}, peerName: {recipientName}, peerID: {recipient?.PlayerId}"); //clean up the message to stop format injection whisperMessage = Regex.Replace(whisperMessage, "", string.Empty, RegexOptions.IgnoreCase); @@ -236,7 +238,7 @@ public void KickMessage(string message, ServerPlayer sender) string whisper; //If user is not the host, we should ignore - will require changes for dedicated server - if (sender == null || !NetworkLifecycle.Instance.IsHost(sender.Peer)) + if (sender == null || !NetworkLifecycle.Instance.IsHost(sender)) return; playerName = message.Split(' ')[0]; @@ -245,13 +247,13 @@ public void KickMessage(string message, ServerPlayer sender) playerToKick = ServerPlayerFromUsername(playerName); - if (playerToKick == null || NetworkLifecycle.Instance.IsHost(playerToKick.Peer)) + if (playerToKick == null || NetworkLifecycle.Instance.IsHost(playerToKick)) { - whisper = $"{Locale.Get(Locale.CHAT_KICK_UNABLE_KEY, playerName)}"; + whisper = $"{Locale.Get(Locale.CHAT_KICK_UNABLE_KEY, [playerName])}"; } else { - whisper = $"{Locale.Get(Locale.CHAT_KICK_KICKED_KEY, playerName)}"; + whisper = $"{Locale.Get(Locale.CHAT_KICK_KICKED_KEY, [playerName])}"; NetworkLifecycle.Instance.Server.KickPlayer(playerToKick); } diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index 4f20173f..54159078 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -47,7 +47,6 @@ public class NetworkServer : NetworkManager private readonly Queue joinQueue = new(); //Queue for players attempting to join while server is loading - private readonly IdPool playerIdPool = new(); //player Id management private readonly Dictionary serverPlayers = []; //player Id to ServerPlayer mapping private readonly Dictionary peers = []; //player Id to peer mapping private readonly Dictionary peerToPlayer = []; //peer to ServerPlayer mapping @@ -60,8 +59,20 @@ public class NetworkServer : NetworkManager public IReadOnlyCollection ServerPlayers => serverPlayers.Values; public int PlayerCount => ServerPlayers.Count; - private static ITransportPeer SelfPeer => NetworkLifecycle.Instance.Client?.SelfPeer; - public static byte SelfId => (byte)SelfPeer.Id; + private ITransportPeer _selfPeer; + public ITransportPeer SelfPeer + { + get + { + if (_selfPeer != null) + return _selfPeer; + + peers.TryGetValue(SelfId, out _selfPeer); + return _selfPeer; + } + } + + public byte SelfId => NetworkLifecycle.Instance.Client?.PlayerId ?? 0; public readonly IDifficulty Difficulty; private bool IsLoaded; @@ -212,26 +223,24 @@ private void OnLoaded() public bool TryGetServerPlayer(ITransportPeer 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); + return peerToPlayer.TryGetValue(peer, out player); } - public bool TryGetPeer(byte id, out ITransportPeer peer) + public bool TryGetServerPlayer(byte playerId, out ServerPlayer player) { - return peers.TryGetValue(id, out peer); + return serverPlayers.TryGetValue(playerId, out player); } #region Net Events public override void OnPeerConnected(ITransportPeer peer) { + LogDebug(() => $"OnPeerConnected({peer.Id})"); } public override void OnPeerDisconnected(ITransportPeer peer, DisconnectReason disconnectReason) { + LogDebug(() => $"OnPeerDisconnected({peer.Id})"); if (!peerToPlayer.TryGetValue(peer, out ServerPlayer player)) { LogWarning($"Peer {peer.GetType()}, peerId: {peer.Id} disconnected but no player found"); @@ -243,15 +252,15 @@ public override void OnPeerDisconnected(ITransportPeer peer, DisconnectReason di if (WorldStreamingInit.isLoaded) SaveGameManager.Instance.UpdateInternalData(); - serverPlayers.Remove(player.Id); - peers.Remove(player.Id); + serverPlayers.Remove(player.PlayerId); + peers.Remove(player.PlayerId); peerToPlayer.Remove(peer); SendPacketToAll ( new ClientboundPlayerDisconnectPacket { - Id = player.Id + PlayerId = player.PlayerId }, DeliveryMethod.ReliableUnordered ); @@ -263,9 +272,12 @@ public override void OnPeerDisconnected(ITransportPeer peer, DisconnectReason di public override void OnNetworkLatencyUpdate(ITransportPeer peer, int latency) { + if (!TryGetServerPlayer(peer, out var player)) + return; + ClientboundPingUpdatePacket clientboundPingUpdatePacket = new() { - Id = (byte)peer.Id, + PlayerId = player.PlayerId, Ping = latency }; @@ -273,12 +285,11 @@ public override void OnNetworkLatencyUpdate(ITransportPeer peer, int latency) if (latency > LATENCY_FLAG) { - serverPlayers.TryGetValue((byte)peer.Id, out var player); - LogWarning($"High Ping Detected! Player: \"{player?.Username}\", ping: {latency}ms"); + LogWarning($"High Ping Detected! Player: \"{player.Username}\", ping: {latency}ms"); } // Ensure we don't send a TickSync packet to ourselves - if (peer.Id == SelfPeer.Id) + if (peer == SelfPeer) return; SendPacket(peer, new ClientboundTickSyncPacket @@ -300,36 +311,36 @@ public override void OnConnectionRequest(NetDataReader requestData, IConnectionR private void SendPacketToAll(T packet, DeliveryMethod deliveryMethod) where T : class, new() { NetDataWriter writer = WritePacket(packet); - foreach (KeyValuePair kvp in peers) - kvp.Value?.Send(writer, deliveryMethod); + foreach (var peer in peers.Values) + peer?.Send(writer, deliveryMethod); } private void SendPacketToAll(T packet, DeliveryMethod deliveryMethod, ITransportPeer excludePeer) where T : class, new() { NetDataWriter writer = WritePacket(packet); - foreach (KeyValuePair kvp in peers) + foreach (var peer in peers.Values) { - if (kvp.Key == excludePeer.Id) + if (peer == excludePeer) continue; - kvp.Value.Send(writer, deliveryMethod); + peer?.Send(writer, deliveryMethod); } } private void SendNetSerializablePacketToAll(T packet, DeliveryMethod deliveryMethod) where T : INetSerializable, new() { NetDataWriter writer = WriteNetSerializablePacket(packet); - foreach (KeyValuePair kvp in peers) - kvp.Value.Send(writer, deliveryMethod); + foreach (var peer in peers.Values) + peer?.Send(writer, deliveryMethod); } private void SendNetSerializablePacketToAll(T packet, DeliveryMethod deliveryMethod, ITransportPeer excludePeer) where T : INetSerializable, new() { NetDataWriter writer = WriteNetSerializablePacket(packet); - foreach (KeyValuePair kvp in peers) + foreach (var peer in peers.Values) { - if (kvp.Key == excludePeer.Id) + if (peer == excludePeer) continue; - kvp.Value.Send(writer, deliveryMethod); + peer?.Send(writer, deliveryMethod); } } @@ -634,9 +645,9 @@ public void SendItemsChangePacket(List items, ServerPlayer playe { Multiplayer.Log($"Sending SendItemsChangePacket with {items.Count()} items to {player.Username}"); - if (peers.TryGetValue(player.Id, out ITransportPeer peer) && peer != SelfPeer) + if (player.Peer != null && player.Peer != SelfPeer) { - SendNetSerializablePacket(peer, new CommonItemChangePacket { Items = items }, + SendNetSerializablePacket(player.Peer, new CommonItemChangePacket { Items = items }, DeliveryMethod.ReliableOrdered); } } @@ -678,6 +689,8 @@ public void SendWhisper(string message, ServerPlayer recipient) private void OnServerboundClientLoginPacket(ServerboundClientLoginPacket packet, IConnectionRequest request) { + LogDebug(() => $"OnServerboundClientLoginPacket from {packet.Username}"); + // 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; @@ -780,14 +793,13 @@ private void OnServerboundClientLoginPacket(ServerboundClientLoginPacket packet, ServerPlayer serverPlayer = new ( - playerIdPool, peer, overrideUsername, packet.Username, guid ); - serverPlayers.Add(serverPlayer.Id, serverPlayer); + serverPlayers.Add(serverPlayer.PlayerId, serverPlayer); peerToPlayer.Add(peer, serverPlayer); PlayerConnected?.Invoke(serverPlayer); @@ -795,6 +807,7 @@ private void OnServerboundClientLoginPacket(ServerboundClientLoginPacket packet, ClientboundLoginResponsePacket acceptPacket = new() { Accepted = true, + PlayerId = serverPlayer.PlayerId, }; SendPacket(peer, acceptPacket, DeliveryMethod.ReliableUnordered); @@ -802,13 +815,20 @@ private void OnServerboundClientLoginPacket(ServerboundClientLoginPacket packet, private void OnServerboundSaveGameDataRequestPacket(ServerboundSaveGameDataRequestPacket packet, ITransportPeer peer) { - if (peers.ContainsKey((byte)peer.Id)) + LogDebug(() => $"OnServerboundSaveGameDataRequestPacket from peerId: {peer.Id}"); + + if (!TryGetServerPlayer(peer, out ServerPlayer player)) { - LogWarning("Denied save game data request from already connected peer!"); + LogError($"Save game data request received for {peer.GetType()}, peerId: {peer.Id}, but ServerPlayer not found"); + peer.Disconnect(); return; } - TryGetServerPlayer(peer, out ServerPlayer player); + //if (peers.ContainsKey((byte)peer.Id)) + //{ + // LogWarning("Denied save game data request from already connected peer!"); + // return; + //} SendPacket(peer, ClientboundGameParamsPacket.FromGameParams(Globals.G.GameParams), DeliveryMethod.ReliableOrdered); SendPacket(peer, ClientboundSaveGameDataPacket.CreatePacket(player), DeliveryMethod.ReliableOrdered); @@ -816,6 +836,7 @@ private void OnServerboundSaveGameDataRequestPacket(ServerboundSaveGameDataReque private void OnServerboundClientReadyPacket(ServerboundClientReadyPacket packet, ITransportPeer peer) { + LogDebug(() => $"OnServerboundClientReadyPacket from peerId: {peer.Id}"); if (!peerToPlayer.TryGetValue(peer, out ServerPlayer serverPlayer)) { @@ -837,12 +858,12 @@ private void OnServerboundClientReadyPacket(ServerboundClientReadyPacket packet, AppUtil.Instance.RequestSystemOnValueChanged(0.0f); // Allow the player to receive packets - peers.Add(serverPlayer.Id, peer); + peers.Add(serverPlayer.PlayerId, peer); // Send the new player to all other players ClientboundPlayerJoinedPacket clientboundPlayerJoinedPacket = new() { - Id = serverPlayer.Id, + PlayerId = serverPlayer.PlayerId, Username = serverPlayer.Username, //Guid = serverPlayer.Guid.ToByteArray() }; @@ -854,7 +875,7 @@ private void OnServerboundClientReadyPacket(ServerboundClientReadyPacket packet, Log($"Client {peer.Id} is ready. Sending world state"); // No need to sync the world state if the player is the host - if (NetworkLifecycle.Instance.IsHost(peer)) + if (NetworkLifecycle.Instance.IsHost(serverPlayer)) { SendPacket(peer, new ClientboundRemoveLoadingScreenPacket(), DeliveryMethod.ReliableOrdered); serverPlayer.IsLoaded = true; @@ -914,11 +935,11 @@ private void OnServerboundClientReadyPacket(ServerboundClientReadyPacket packet, // Send existing players foreach (ServerPlayer player in ServerPlayers) { - if (player.Id == serverPlayer.Id) + if (player.PlayerId == serverPlayer.PlayerId) continue; SendPacket(peer, new ClientboundPlayerJoinedPacket { - Id = player.Id, + PlayerId = player.PlayerId, Username = player.Username, //Guid = player.Guid.ToByteArray(), CarID = player.CarId, @@ -938,17 +959,19 @@ private void OnServerboundClientReadyPacket(ServerboundClientReadyPacket packet, private void OnServerboundPlayerPositionPacket(ServerboundPlayerPositionPacket packet, ITransportPeer peer) { - if (TryGetServerPlayer(peer, out ServerPlayer player)) + if (!TryGetServerPlayer(peer, out ServerPlayer player)) { - player.CarId = packet.CarID; - player.RawPosition = packet.Position; - player.RawRotationY = packet.RotationY; - + LogWarning($"Received Player Position from {peer.GetType()}, peerId: {peer.Id}, but could not find matching player."); + return; } + + player.CarId = packet.CarID; + player.RawPosition = packet.Position; + player.RawRotationY = packet.RotationY; ClientboundPlayerPositionPacket clientboundPacket = new() { - Id = (byte)peer.Id, + PlayerId = player.PlayerId, Position = packet.Position, MoveDir = packet.MoveDir, RotationY = packet.RotationY, @@ -995,7 +1018,7 @@ private void OnCommonCouplerInteractionPacket(CommonCouplerInteractionPacket pac } else { - LogDebug(() => $"OnCommonCouplerInteractionPacket([{packet.Flags}, {netTrainCar.CurrentID}, {packet.NetId}], {player.Id}) Sending validation failure"); + LogDebug(() => $"OnCommonCouplerInteractionPacket([{packet.Flags}, {netTrainCar.CurrentID}, {packet.NetId}], {player.PlayerId}) Sending validation failure"); //failed validation notify client SendPacket( peer, @@ -1011,7 +1034,7 @@ private void OnCommonCouplerInteractionPacket(CommonCouplerInteractionPacket pac } else { - LogDebug(() => $"OnCommonCouplerInteractionPacket([{packet.Flags}, {netTrainCar.CurrentID}, {packet.NetId}], {player.Id}) Sending destroy"); + LogDebug(() => $"OnCommonCouplerInteractionPacket([{packet.Flags}, {netTrainCar.CurrentID}, {packet.NetId}], {player.PlayerId}) Sending destroy"); //Car doesn't exist, tell client to delete it SendDestroyTrainCar(netTrainCar, peer); } @@ -1079,7 +1102,7 @@ private void OnServerboundAddCoalPacket(ServerboundAddCoalPacket packet, ITransp if (float.IsNaN(packet.CoalMassDelta)) return; - if (!NetworkLifecycle.Instance.IsHost(peer)) + if (!NetworkLifecycle.Instance.IsHost(player)) { float carLength = CarSpawner.Instance.carLiveryToCarLength[networkedTrainCar.TrainCar.carLivery]; @@ -1098,7 +1121,7 @@ private void OnServerboundFireboxIgnitePacket(ServerboundFireboxIgnitePacket pac if (!NetworkedTrainCar.TryGet(packet.NetId, out NetworkedTrainCar networkedTrainCar)) return; - if (!NetworkLifecycle.Instance.IsHost(peer)) + if (!NetworkLifecycle.Instance.IsHost(player)) { //is player close enough to ignite firebox? float carLength = CarSpawner.Instance.carLiveryToCarLength[networkedTrainCar.TrainCar.carLivery]; @@ -1115,7 +1138,7 @@ private void OnCommonTrainPortsPacket(CommonTrainPortsPacket packet, ITransportP return; //if not the host && validation fails then ignore packet - if (!NetworkLifecycle.Instance.IsHost(peer)) + if (!NetworkLifecycle.Instance.IsHost(player)) { bool flag = networkedTrainCar.Server_ValidateClientSimFlowPacket(player, packet); diff --git a/Multiplayer/Networking/Packets/Clientbound/ClientboundLoginResponsePacket.cs b/Multiplayer/Networking/Packets/Clientbound/ClientboundLoginResponsePacket.cs index 184bcd9d..c29327a6 100644 --- a/Multiplayer/Networking/Packets/Clientbound/ClientboundLoginResponsePacket.cs +++ b/Multiplayer/Networking/Packets/Clientbound/ClientboundLoginResponsePacket.cs @@ -1,4 +1,3 @@ -using System; using Multiplayer.Networking.Data; namespace Multiplayer.Networking.Packets.Clientbound; @@ -6,6 +5,7 @@ namespace Multiplayer.Networking.Packets.Clientbound; public class ClientboundLoginResponsePacket { public bool Accepted { get; set; } + public byte PlayerId { get; set; } public string ReasonKey { get; set; } public string[] ReasonArgs { get; set; } public ModInfo[] Missing { get; set; } = []; diff --git a/Multiplayer/Networking/Packets/Clientbound/ClientboundPingUpdatePacket.cs b/Multiplayer/Networking/Packets/Clientbound/ClientboundPingUpdatePacket.cs index 7fed6121..6eec4cbd 100644 --- a/Multiplayer/Networking/Packets/Clientbound/ClientboundPingUpdatePacket.cs +++ b/Multiplayer/Networking/Packets/Clientbound/ClientboundPingUpdatePacket.cs @@ -2,6 +2,6 @@ namespace Multiplayer.Networking.Packets.Clientbound; public class ClientboundPingUpdatePacket { - public byte Id { get; set; } + public byte PlayerId { get; set; } public int Ping { get; set; } } diff --git a/Multiplayer/Networking/Packets/Clientbound/ClientboundPlayerDisconnectPacket.cs b/Multiplayer/Networking/Packets/Clientbound/ClientboundPlayerDisconnectPacket.cs index 2035379e..ac1732f0 100644 --- a/Multiplayer/Networking/Packets/Clientbound/ClientboundPlayerDisconnectPacket.cs +++ b/Multiplayer/Networking/Packets/Clientbound/ClientboundPlayerDisconnectPacket.cs @@ -2,5 +2,5 @@ namespace Multiplayer.Networking.Packets.Clientbound; public class ClientboundPlayerDisconnectPacket { - public byte Id { get; set; } + public byte PlayerId { get; set; } } diff --git a/Multiplayer/Networking/Packets/Clientbound/ClientboundPlayerJoinedPacket.cs b/Multiplayer/Networking/Packets/Clientbound/ClientboundPlayerJoinedPacket.cs index befc8c32..806ddfc9 100644 --- a/Multiplayer/Networking/Packets/Clientbound/ClientboundPlayerJoinedPacket.cs +++ b/Multiplayer/Networking/Packets/Clientbound/ClientboundPlayerJoinedPacket.cs @@ -4,7 +4,7 @@ namespace Multiplayer.Networking.Packets.Clientbound; public class ClientboundPlayerJoinedPacket { - public byte Id { get; set; } + public byte PlayerId { get; set; } public string Username { get; set; } //public byte[] Guid { get; set; } public ushort CarID { get; set; } diff --git a/Multiplayer/Networking/Packets/Clientbound/ClientboundPlayerPositionPacket.cs b/Multiplayer/Networking/Packets/Clientbound/ClientboundPlayerPositionPacket.cs index 6abe79fd..65144a88 100644 --- a/Multiplayer/Networking/Packets/Clientbound/ClientboundPlayerPositionPacket.cs +++ b/Multiplayer/Networking/Packets/Clientbound/ClientboundPlayerPositionPacket.cs @@ -4,7 +4,7 @@ namespace Multiplayer.Networking.Packets.Clientbound; public class ClientboundPlayerPositionPacket { - public byte Id { get; set; } + public byte PlayerId { get; set; } public Vector3 Position { get; set; } public Vector2 MoveDir { get; set; } public float RotationY { get; set; } diff --git a/MultiplayerAPI Tests/TestComponents/ClientTest.cs b/MultiplayerAPI Tests/TestComponents/ClientTest.cs index 6130a83e..6f4bbabc 100644 --- a/MultiplayerAPI Tests/TestComponents/ClientTest.cs +++ b/MultiplayerAPI Tests/TestComponents/ClientTest.cs @@ -94,7 +94,7 @@ private void OnTick(uint tick) { StringBuilder sb = new($"Tick {tick}.\r\nThere are {client.PlayerCount} players, their pings are:"); foreach (IPlayer player in client.Players) - sb.AppendLine($"\"{player?.Id}\" {player.Ping} ms"); + sb.AppendLine($"\"{player?.PlayerId}\" {player.Ping} ms"); Log(sb.ToString()); } @@ -109,14 +109,14 @@ private void OnPlayerConnected(IPlayer player) { // This event is called when another player connects - Log($"Player \"{player?.Id}\" has connected."); + Log($"Player \"{player?.PlayerId}\" has connected."); } private void OnPlayerDisconnected(IPlayer player) { // This event is called when another player disconnects - Log($"Player \"{player?.Id}\" has connected."); + Log($"Player \"{player?.PlayerId}\" has connected."); } #endregion diff --git a/MultiplayerAPI Tests/TestComponents/ServerTest.cs b/MultiplayerAPI Tests/TestComponents/ServerTest.cs index c9d3b37e..28471907 100644 --- a/MultiplayerAPI Tests/TestComponents/ServerTest.cs +++ b/MultiplayerAPI Tests/TestComponents/ServerTest.cs @@ -88,7 +88,7 @@ private void OnTick(uint tick) StringBuilder sb = new($"Tick {tick}.\r\nThere are {server.PlayerCount} players, their pings are:"); foreach (IPlayer player in server.Players) - sb.AppendLine($"\"{player?.Id}\" {player.Ping} ms"); + sb.AppendLine($"\"{player?.PlayerId}\" {player.Ping} ms"); Log(sb.ToString()); @@ -103,7 +103,7 @@ private void OnPlayerConnected(IPlayer player) // Send mod settings, parameters, etc. // Note: This event occurs when the player is authenticated and before the player receives game state info - Log($"Player {player?.Id} (\"{player?.Username}\") has connected. (Is Loaded: {player?.IsLoaded})"); + Log($"Player {player?.PlayerId} (\"{player?.Username}\") has connected. (Is Loaded: {player?.IsLoaded})"); } private void OnPlayerReady(IPlayer player) @@ -111,10 +111,10 @@ private void OnPlayerReady(IPlayer player) // Player has indicated the world is loaded and they are ready to receive game state info // Note: This event occurs after the server has sent the game state, it does not guarantee the player has finished generating all cars, jobs, etc. - Log($"Player \"{player?.Id}\" is ready. (Is Loaded: {player?.IsLoaded})"); + Log($"Player \"{player?.PlayerId}\" is ready. (Is Loaded: {player?.IsLoaded})"); //Send an anouncement to all players - server.SendServerChatMessage($"Please welcome our newest driver {player?.Id}!"); + server.SendServerChatMessage($"Please welcome our newest driver {player?.PlayerId}!"); } private void OnPlayerDisconnected(IPlayer player) @@ -335,7 +335,7 @@ private void OnChatCommandStats(string message, IPlayer sender) StringBuilder whisper = new($"There {(server.PlayerCount > 1 ? "are" : "is")} {server.PlayerCount} connected player{(server.PlayerCount > 1 ? "s" : "")}:"); foreach (var player in server.Players) - whisper.Append($"
    \t{(player.IsHost ? "" : "")}{player.Username}{(player.IsHost ? "" : "")} Id: {player.Id}, Ping: {player.Ping}{(player.IsOnCar ? $", Riding {player.OccupiedCar.ID}" : "")}"); + whisper.Append($"
    \t{(player.IsHost ? "" : "")}{player.Username}{(player.IsHost ? "" : "")} Id: {player.PlayerId}, Ping: {player.Ping}{(player.IsOnCar ? $", Riding {player.OccupiedCar.ID}" : "")}"); whisper.Append(""); diff --git a/MultiplayerAPI/Interfaces/IMultiplayerAPI.cs b/MultiplayerAPI/Interfaces/IMultiplayerAPI.cs index 232d55a7..f8d99616 100644 --- a/MultiplayerAPI/Interfaces/IMultiplayerAPI.cs +++ b/MultiplayerAPI/Interfaces/IMultiplayerAPI.cs @@ -33,7 +33,7 @@ public interface IMultiplayerAPI /// Returns true if either a host or client exist /// bool IsConnected { get; } - + /// /// Gets whether this instance is host /// diff --git a/MultiplayerAPI/Interfaces/IPlayer.cs b/MultiplayerAPI/Interfaces/IPlayer.cs index 420063b8..e93a3800 100644 --- a/MultiplayerAPI/Interfaces/IPlayer.cs +++ b/MultiplayerAPI/Interfaces/IPlayer.cs @@ -15,7 +15,7 @@ public interface IPlayer /// This identifier can be used as a network ID for referencing the player across the network. /// If the player leaves the session the Id will be reassigned to the next player to join. ///
    - public byte Id { get; } + public byte PlayerId { get; } /// /// Gets the username/display name of the player. From 49abc4c6d98e586a7e274a8bb1a0128f06a20f7b Mon Sep 17 00:00:00 2001 From: Macka Date: Tue, 2 Sep 2025 22:43:38 +1000 Subject: [PATCH 445/521] Add PlayerId property to IClient and ClientAPIProvider Introduces a PlayerId property to the IClient interface and exposes it in ClientAPIProvider. This allows retrieval of the local player's ID. Also updates GetPlayer parameter name for clarity. --- Multiplayer/API/ClientAPIProvider.cs | 1 + MultiplayerAPI/Interfaces/IClient.cs | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/Multiplayer/API/ClientAPIProvider.cs b/Multiplayer/API/ClientAPIProvider.cs index 05e92fb2..56bf1d5b 100644 --- a/Multiplayer/API/ClientAPIProvider.cs +++ b/Multiplayer/API/ClientAPIProvider.cs @@ -18,6 +18,7 @@ public class ClientAPIProvider : IClient public event Action OnPlayerDisconnected; #region Client Properties + public byte PlayerId => client.PlayerId; public IReadOnlyCollection Players => client.ClientPlayerManager.Players.Select(GetWrapper).ToList().AsReadOnly(); public int PlayerCount => client.ClientPlayerManager.Players.Count + 1; // add 1 for local player diff --git a/MultiplayerAPI/Interfaces/IClient.cs b/MultiplayerAPI/Interfaces/IClient.cs index ffb277d4..ad9ac174 100644 --- a/MultiplayerAPI/Interfaces/IClient.cs +++ b/MultiplayerAPI/Interfaces/IClient.cs @@ -22,6 +22,13 @@ public interface IClient /// IPlayer object for the disconnected player event Action OnPlayerDisconnected; + /// + /// Gets Player Id of the local player + /// + /// + /// The local player does not have an IPlayer object + /// + byte PlayerId { get; } /// /// Gets IPlayer objects for all players connected to the server @@ -39,7 +46,7 @@ public interface IClient /// Gets IPlayer for player by Id /// /// IPlayer object if found, otherwise null - IPlayer GetPlayer(byte id); + IPlayer GetPlayer(byte playerId); /// /// Gets connection state for the client From 481facc273f773b462250c27f1bcad684b7334eb Mon Sep 17 00:00:00 2001 From: Macka Date: Sat, 6 Sep 2025 14:37:59 +1000 Subject: [PATCH 446/521] Move player connected event to post-authentication Relocated the PlayerConnected event invocation in NetworkServer to occur after player authentication. Fixes race condition for self-hosted client. Updated IServer interface documentation to clarify that the event is not triggered for the host player. --- Multiplayer/Networking/Managers/Server/NetworkServer.cs | 4 ++-- MultiplayerAPI/Interfaces/IServer.cs | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index 54159078..509ea90e 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -802,8 +802,6 @@ private void OnServerboundClientLoginPacket(ServerboundClientLoginPacket packet, serverPlayers.Add(serverPlayer.PlayerId, serverPlayer); peerToPlayer.Add(peer, serverPlayer); - PlayerConnected?.Invoke(serverPlayer); - ClientboundLoginResponsePacket acceptPacket = new() { Accepted = true, @@ -824,6 +822,8 @@ private void OnServerboundSaveGameDataRequestPacket(ServerboundSaveGameDataReque return; } + PlayerConnected?.Invoke(player); + //if (peers.ContainsKey((byte)peer.Id)) //{ // LogWarning("Denied save game data request from already connected peer!"); diff --git a/MultiplayerAPI/Interfaces/IServer.cs b/MultiplayerAPI/Interfaces/IServer.cs index dcb16ac4..bf34443d 100644 --- a/MultiplayerAPI/Interfaces/IServer.cs +++ b/MultiplayerAPI/Interfaces/IServer.cs @@ -30,6 +30,9 @@ public interface IServer /// /// Event fired when a player connects and is authenticated, but before the player receives game state information /// + /// + /// Event is not triggered for the host player + /// event Action OnPlayerConnected; /// From a2d518abf6985db17d5878436d57760816bc2133 Mon Sep 17 00:00:00 2001 From: Macka Date: Sat, 13 Sep 2025 22:37:11 +1000 Subject: [PATCH 447/521] Add periodic weather state updates to fix time drift Introduces a WEATHER_UPDATE_INTERVAL constant and logic to periodically send weather state updates to all clients. Refactors weather state sending into a dedicated SendWeatherState method and ensures weather updates are sent on player connection and at regular intervals during server operation. --- .../Managers/Server/NetworkServer.cs | 50 +++++++++++++++---- 1 file changed, 40 insertions(+), 10 deletions(-) diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index 8d702833..ee867929 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -1,47 +1,48 @@ -using DV; using DV.InventorySystem; using DV.Logic.Job; using DV.Scenarios.Common; using DV.ServicePenalty; using DV.ThingTypes; using DV.WeatherSystem; +using DV; using Humanizer; -using LiteNetLib; using LiteNetLib.Utils; +using LiteNetLib; using MPAPI.Interfaces.Packets; using MPAPI.Types; using Multiplayer.API; -using Multiplayer.Components.Networking; using Multiplayer.Components.Networking.Jobs; using Multiplayer.Components.Networking.Train; using Multiplayer.Components.Networking.World; -using Multiplayer.Networking.Data; +using Multiplayer.Components.Networking; using Multiplayer.Networking.Data.Train; -using Multiplayer.Networking.Packets.Clientbound; +using Multiplayer.Networking.Data; using Multiplayer.Networking.Packets.Clientbound.Jobs; using Multiplayer.Networking.Packets.Clientbound.SaveGame; using Multiplayer.Networking.Packets.Clientbound.Train; using Multiplayer.Networking.Packets.Clientbound.World; -using Multiplayer.Networking.Packets.Common; +using Multiplayer.Networking.Packets.Clientbound; using Multiplayer.Networking.Packets.Common.Train; -using Multiplayer.Networking.Packets.Serverbound; +using Multiplayer.Networking.Packets.Common; using Multiplayer.Networking.Packets.Serverbound.Jobs; using Multiplayer.Networking.Packets.Serverbound.Train; +using Multiplayer.Networking.Packets.Serverbound; using Multiplayer.Networking.Packets.Unconnected; using Multiplayer.Networking.TransportLayers; using Multiplayer.Utils; -using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Text; +using System; using UnityEngine; - namespace Multiplayer.Networking.Managers.Server; public class NetworkServer : NetworkManager { + private const int WEATHER_UPDATE_INTERVAL = 30; //seconds + public Action PlayerConnected; public Action PlayerDisconnected; public Action PlayerReady; @@ -82,6 +83,8 @@ public ITransportPeer SelfPeer private readonly ChatManager _chatManager = new(); public ChatManager ChatManager => _chatManager; + private uint lastTick; + public NetworkServer(IDifficulty difficulty, Settings settings, bool singlePlayer, LobbyServerData serverData) : base(settings) { Log($"Server created for {(singlePlayer ? "single player" : "multiplayer")} game"); @@ -133,6 +136,8 @@ public override void Stop() foreach (var player in serverPlayers.Values) player.Dispose(); + NetworkLifecycle.Instance.OnTick -= OnTick; + base.Stop(); } @@ -222,6 +227,21 @@ private void OnLoaded() System.Console.WriteLine("Connection is not established."); } } + + lastTick = NetworkLifecycle.Instance.Tick; + NetworkLifecycle.Instance.OnTick += OnTick; + } + + private void OnTick(uint tick) + { + if (!IsLoaded) + return; + + if ((NetworkLifecycle.Instance.Tick - lastTick) > NetworkLifecycle.TICK_RATE * WEATHER_UPDATE_INTERVAL) + { + SendWeatherState(); + lastTick = NetworkLifecycle.Instance.Tick; + } } public bool TryGetServerPlayer(ITransportPeer peer, out ServerPlayer player) @@ -410,6 +430,16 @@ public void SendGameParams(GameParams gameParams) SendPacketToAll(ClientboundGameParamsPacket.FromGameParams(gameParams), DeliveryMethod.ReliableOrdered, SelfPeer); } + public void SendWeatherState(ITransportPeer peer = null) + { + var packet = WeatherDriver.Instance.GetSaveData(Globals.G.GameParams.WeatherEditorAlwaysAllowed).ToObject(); + + if (peer != null) + SendPacket(peer, packet, DeliveryMethod.ReliableOrdered); + else + SendPacketToAll(packet, DeliveryMethod.ReliableOrdered, SelfPeer); + } + public void SendSpawnTrainset(List set, bool autoCouple, bool sendToAll, ITransportPeer sendTo = null) { @@ -905,7 +935,7 @@ private void OnServerboundClientReadyPacket(ServerboundClientReadyPacket packet, SendPacket(peer, new ClientboundBeginWorldSyncPacket(), DeliveryMethod.ReliableOrdered); // Send weather state - SendPacket(peer, WeatherDriver.Instance.GetSaveData(Globals.G.GameParams.WeatherEditorAlwaysAllowed).ToObject(), DeliveryMethod.ReliableOrdered); + SendWeatherState(peer); // Send junctions and turntables SendPacket(peer, new ClientboundRailwayStatePacket From e8cf951e5ec2d47d190cc17f41a114c0f2ad971f Mon Sep 17 00:00:00 2001 From: Macka Date: Sat, 13 Sep 2025 23:09:57 +1000 Subject: [PATCH 448/521] Add time limit and last task support to task sync Extended WarehouseTaskData, TransportTaskData, SequentialTasksData, and ParallelTasksData to include TimeLimit and IsLastTask properties for improved task synchronisation. --- .../Networking/Data/TaskNetworkData.cs | 41 +++++++++++++------ 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/Multiplayer/Networking/Data/TaskNetworkData.cs b/Multiplayer/Networking/Data/TaskNetworkData.cs index aabf8271..403b5490 100644 --- a/Multiplayer/Networking/Data/TaskNetworkData.cs +++ b/Multiplayer/Networking/Data/TaskNetworkData.cs @@ -3,10 +3,10 @@ using MPAPI.Types; using MPAPI.Util; using Multiplayer.Components.Networking.Train; -using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System; namespace Multiplayer.Networking.Data; @@ -21,6 +21,8 @@ public static class TaskNetworkDataFactory public static bool RegisterTaskType(TaskType taskType, Func converter, Func emptyCreator) where TGameTask : Task { + Multiplayer.LogDebug(() => $"Registering Task Type {typeof(TGameTask)} with TaskType {taskType}"); + if (TypeToTaskNetworkData.Keys.Contains(typeof(TGameTask)) || EnumToEmptyTaskNetworkData.Keys.Contains(taskType)) { Multiplayer.LogError($"Task Type {typeof(TGameTask)} already registered!"); @@ -36,7 +38,8 @@ public static bool RegisterTaskType(TaskType taskType, Func(TaskType taskType) where TGameTask : Task { - if(baseTasks.Contains(typeof(TGameTask)) || baseTaskTypes.Contains(taskType)) + Multiplayer.LogDebug(() => $"Unregistering Task Type {typeof(TGameTask)} with TaskType {taskType}"); + if (baseTasks.Contains(typeof(TGameTask)) || baseTaskTypes.Contains(taskType)) { Multiplayer.LogError($"Cannot unregister base task type {typeof(TGameTask)} with TaskType {taskType}"); return false; @@ -50,7 +53,7 @@ public static bool UnRegisterTaskType(TaskType taskType) public static TaskNetworkData ConvertTask(Task task) { - //Multiplayer.LogDebug(()=>$"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); @@ -65,6 +68,7 @@ public static TaskNetworkData[] ConvertTasks(IEnumerable tasks) public static TaskNetworkData ConvertTask(TaskType type) { + //Multiplayer.LogDebug(() => $"TaskNetworkDataFactory.ConvertTask({type})"); if (EnumToEmptyTaskNetworkData.TryGetValue(type, out var creator)) { return creator(type); @@ -180,7 +184,9 @@ public override Task ToTask() WarehouseTaskType, JobSaveManager.Instance.GetWarehouseMachineWithId(WarehouseMachine), CargoType, - CargoAmount + CargoAmount, + (long)TimeLimit, + IsLastTask ); newWareTask.readyForMachine = ReadyForMachine; @@ -295,7 +301,9 @@ public override Task ToTask() cars, RailTrackRegistry.Instance.GetTrackWithName(DestinationTrack).LogicTrack(), RailTrackRegistry.Instance.GetTrackWithName(StartingTrack).LogicTrack(), - TransportedCargoPerCar?.ToList() + TransportedCargoPerCar?.ToList(), + (long)TimeLimit, + IsLastTask ); } @@ -378,16 +386,23 @@ public override Task ToTask() List tasks = []; 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()); + SequentialTasks newSeqTask = new(tasks, (long)TimeLimit); + + // Rebuild linked list task states - this is the equivalent of OverrideTasksStates(TaskSaveData[] tasksData) + int index = 0; + for (var currentNode = newSeqTask.tasks.First; currentNode != null; currentNode = currentNode.Next) + { + currentNode.Value.state = tasks[index].state; + currentNode.Value.taskStartTime = tasks[index].taskStartTime; + currentNode.Value.taskFinishTime = tasks[index].taskFinishTime; - if (CurrentTaskIndex <= newSeqTask.tasks.Count()) - newSeqTask.currentTask = new LinkedListNode(newSeqTask.tasks.ToArray()[CurrentTaskIndex]); + if (tasks[index].state == TaskState.Done && currentNode != newSeqTask.tasks.Last) + newSeqTask.currentTask = currentNode.Next; + + index++; + } return newSeqTask; } @@ -446,7 +461,7 @@ public override ParallelTasksData FromTask(Task task) public override Task ToTask() { - return new ParallelTasks(Tasks.Select(t => t.ToTask()).ToList()); + return new ParallelTasks(Tasks.Select(t => t.ToTask()).ToList(), (long)TimeLimit, IsLastTask); } public override List GetCars() From 9e8aaf5fc5f9945d0d188faf22c34a950faafb8b Mon Sep 17 00:00:00 2001 From: Macka Date: Sat, 13 Sep 2025 23:25:26 +1000 Subject: [PATCH 449/521] Ensure jobs are loaded prior to taking When a job is in progress, the new job is now added to the station and the processed jobs list before being taken. This ensures proper tracking and processing of jobs. --- .../Components/Networking/World/NetworkedStationController.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Multiplayer/Components/Networking/World/NetworkedStationController.cs b/Multiplayer/Components/Networking/World/NetworkedStationController.cs index 3d7c10e6..23b6dddd 100644 --- a/Multiplayer/Components/Networking/World/NetworkedStationController.cs +++ b/Multiplayer/Components/Networking/World/NetworkedStationController.cs @@ -340,6 +340,9 @@ private void AddJob(JobData jobData) } else if (networkedJob.Job.State == JobState.InProgress) { + StationController.logicStation.AddJobToStation(newJob); + StationController.processedNewJobs.Add(newJob); + takenJobs.Add(newJob); newJob.TakeJob(true); //take job as if loaded from save to prevent debt controller kicking in } From 0349dbec2259f540eba5b891697106d12a658f2e Mon Sep 17 00:00:00 2001 From: Macka Date: Sun, 14 Sep 2025 22:40:42 +1000 Subject: [PATCH 450/521] Implement job task synchronisation Adds per-task network IDs and synchronisation logic to job tasks, enabling accurate state updates across clients and server. Introduces `ClientboundTaskUpdatePacket`, server/client handling, and refactors task serialisation/deserialisation to support mapping network IDs to tasks. Also patches Task to send updates when state changes and ensures tasks are correctly associated with jobs on creation. --- Multiplayer/API/APIProvider.cs | 4 +- .../Networking/Jobs/NetworkedJob.cs | 83 +++++++++++++++-- .../World/NetworkedStationController.cs | 24 +++-- Multiplayer/Networking/Data/JobData.cs | 26 +++--- .../Networking/Data/TaskNetworkData.cs | 93 ++++++++++++++----- .../Managers/Client/NetworkClient.cs | 26 ++++++ .../Managers/Server/NetworkServer.cs | 13 +++ .../Jobs/ClientboundJobsCreatePacket.cs | 3 +- .../Jobs/ClientboundTaskUpdatePacket.cs | 12 +++ Multiplayer/Patches/Jobs/TaskPatch.cs | 44 +++++++++ MultiplayerAPI/Interfaces/IMultiplayerAPI.cs | 2 +- MultiplayerAPI/MultiplayerAPI.csproj | 8 ++ MultiplayerAPI/Types/TaskNetworkType.cs | 50 +++++++++- 13 files changed, 327 insertions(+), 61 deletions(-) create mode 100644 Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundTaskUpdatePacket.cs create mode 100644 Multiplayer/Patches/Jobs/TaskPatch.cs diff --git a/Multiplayer/API/APIProvider.cs b/Multiplayer/API/APIProvider.cs index 4b9a1c55..759dc1f1 100644 --- a/Multiplayer/API/APIProvider.cs +++ b/Multiplayer/API/APIProvider.cs @@ -82,9 +82,9 @@ public bool RegisterTaskType(TaskType taskType, Func(taskType, converter, emptyCreator); } - public bool UnRegisterTaskType(TaskType taskType) where TGameTask : Task + public bool UnregisterTaskType(TaskType taskType) where TGameTask : Task { - return TaskNetworkDataFactory.UnRegisterTaskType(taskType); + return TaskNetworkDataFactory.UnregisterTaskType(taskType); } #region Class Helpers diff --git a/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs b/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs index 0f995847..9e9b435e 100644 --- a/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs +++ b/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs @@ -1,14 +1,10 @@ -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; - +using Multiplayer.Utils; +using System.Collections.Generic; +using System.Linq; +using System; namespace Multiplayer.Components.Networking.Jobs; @@ -57,6 +53,9 @@ public static bool TryGetNetId(Job job, out ushort netId) } #endregion + + private static readonly Dictionary> pendingJobTasks = []; + protected override bool IsIdServerAuthoritative => true; public enum DirtyCause { @@ -139,12 +138,15 @@ public NetworkedItem JobReport public List JobCars = []; + private readonly IdPool idPool = new(); + private readonly Dictionary taskMap = []; + protected override void Awake() { base.Awake(); } - private void Start() + protected void Start() { if (Job != null) { @@ -172,6 +174,13 @@ public void Initialize(Job job, NetworkedStationController station) { AddToCache(); } + + //Check for any pending tasks that were added before the NetworkedJob was created + if(pendingJobTasks.TryGetValue(job, out var taskList) && taskList != null) + { + taskList.ForEach(t => AddTask(t)); + pendingJobTasks.Remove(job); + } } private void AddToCache() @@ -245,7 +254,7 @@ public void ClearReports() JobReports.Clear(); } - private void OnDisable() + protected void OnDisable() { if (UnloadWatcher.isQuitting || UnloadWatcher.isUnloading) return; @@ -263,4 +272,58 @@ private void OnDisable() Destroy(this); } + + public static void EnqueTask(Task task) + { + if (!pendingJobTasks.TryGetValue(task.Job, out var taskList)) + { + taskList = []; + pendingJobTasks[task.Job] = taskList; + } + taskList.Add(task); + } + + private void AddTask(Task task) + { + if (taskMap.Values.Contains(task)) + return; + + var netId = idPool.NextId; + + Multiplayer.LogDebug(() => $"NetworkedJob.AddTask() JobId: [{Job.ID}, {NetId}], TaskType: {task.InstanceTaskType}, taskNetId: {netId}"); + taskMap[netId] = task; + } + + public void AddTask(ushort netId, Task task) + { + if (taskMap.Values.Contains(task)) + return; + + if (taskMap.ContainsKey(netId)) + { + Multiplayer.LogError($"NetworkedJob.AddTask({task.InstanceTaskType}) Attempted to add a task with duplicate netId: {netId} JobId: [{Job.ID}, {NetId}]"); + return; + } + + Multiplayer.LogDebug(() => $"NetworkedJob.AddTask({netId}, {task.InstanceTaskType}) JobId: [{Job.ID}, {NetId}]"); + + taskMap[netId] = task; + } + + public ushort GetTaskNetId(Task task) + { + foreach (var kvp in taskMap) + { + if (kvp.Value == task) + return kvp.Key; + } + return 0; + } + + public Task GetTaskFromNetId(ushort netId) + { + if (taskMap.TryGetValue(netId, out var task)) + return task; + return null; + } } diff --git a/Multiplayer/Components/Networking/World/NetworkedStationController.cs b/Multiplayer/Components/Networking/World/NetworkedStationController.cs index 23b6dddd..83eba763 100644 --- a/Multiplayer/Components/Networking/World/NetworkedStationController.cs +++ b/Multiplayer/Components/Networking/World/NetworkedStationController.cs @@ -1,16 +1,15 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; using DV.Booklets; using DV.Logic.Job; using DV.ServicePenalty; -using DV.Utils; using DV.ThingTypes; +using DV.Utils; using Multiplayer.Components.Networking.Jobs; using Multiplayer.Components.Networking.Train; using Multiplayer.Networking.Data; using Multiplayer.Utils; +using System.Collections.Generic; +using System.Collections; +using System; using UnityEngine; namespace Multiplayer.Components.Networking.World; @@ -321,11 +320,22 @@ public void AddJobs(JobData[] jobs) private void AddJob(JobData jobData) { - Job newJob = JobData.ToJob(jobData); + var newJobData = JobData.ToJob(jobData); + + Job newJob = newJobData.newJob; + var netIdToTask = newJobData.netIdToTask; + var carNetIds = jobData.GetCars(); NetworkedJob networkedJob = CreateNetworkedJob(newJob, jobData.NetID, carNetIds); + foreach(var kvp in netIdToTask) + { + ushort netTaskId = kvp.Key; + Task task = kvp.Value; + networkedJob.AddTask(netTaskId, task); + } + NetworkedJobs.Add(networkedJob); if (networkedJob.Job.State == JobState.Available) @@ -612,7 +622,7 @@ public static IEnumerator UpdateCarPlates(List carNetIds, string jobId) private void GenerateOverview(NetworkedJob networkedJob, ushort itemNetId, ItemPositionData posData) { - Multiplayer.Log($"GenerateOverview({networkedJob.Job.ID}, {itemNetId}) Position: {posData.Position}, Less currentMove: {posData.Position + WorldMover.currentMove} "); + Multiplayer.Log($"GenerateOverview([{networkedJob.Job.ID},{networkedJob.Job.jobType}], {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/JobData.cs b/Multiplayer/Networking/Data/JobData.cs index e11f6691..d7a8f523 100644 --- a/Multiplayer/Networking/Data/JobData.cs +++ b/Multiplayer/Networking/Data/JobData.cs @@ -1,14 +1,13 @@ -using DV.Logic.Job; -using DV.ThingTypes; -using LiteNetLib.Utils; -using MPAPI.Types; -using Multiplayer.Components.Networking; -using Multiplayer.Components.Networking.Jobs; -using Multiplayer.Components.Networking.World; using System; -using System.Collections.Generic; -using System.IO; using System.Linq; +using System.IO; +using System.Collections.Generic; +using Multiplayer.Components.Networking.World; +using Multiplayer.Components.Networking.Jobs; +using MPAPI.Types; +using LiteNetLib.Utils; +using DV.ThingTypes; +using DV.Logic.Job; namespace Multiplayer.Networking.Data; @@ -80,9 +79,12 @@ public static JobData FromJob(NetworkedStationController netStation, NetworkedJo }; } - public static Job ToJob(JobData jobData) + public static (Job newJob, Dictionary netIdToTask) ToJob(JobData jobData) { - List tasks = jobData.Tasks.Select(taskData => taskData.ToTask()).ToList(); + Dictionary netIdToTask = []; + + List tasks = jobData.Tasks.Select(taskData => taskData.ToTask(ref netIdToTask)).ToList(); + StationsChainData chainData = new(jobData.ChainData.ChainOriginYardId, jobData.ChainData.ChainDestinationYardId); Job newJob = new(tasks, jobData.JobType, jobData.TimeLimit, jobData.InitialWage, chainData, jobData.ID, jobData.RequiredLicenses) @@ -92,7 +94,7 @@ public static Job ToJob(JobData jobData) State = jobData.State, }; - return newJob; + return new (newJob, netIdToTask); } public static void Serialize(NetDataWriter writer, JobData data) diff --git a/Multiplayer/Networking/Data/TaskNetworkData.cs b/Multiplayer/Networking/Data/TaskNetworkData.cs index 403b5490..2cce880b 100644 --- a/Multiplayer/Networking/Data/TaskNetworkData.cs +++ b/Multiplayer/Networking/Data/TaskNetworkData.cs @@ -2,6 +2,7 @@ using DV.ThingTypes; using MPAPI.Types; using MPAPI.Util; +using Multiplayer.Components.Networking.Jobs; using Multiplayer.Components.Networking.Train; using System.Collections.Generic; using System.IO; @@ -35,7 +36,7 @@ public static bool RegisterTaskType(TaskType taskType, Func(TaskType taskType) + public static bool UnregisterTaskType(TaskType taskType) where TGameTask : Task { Multiplayer.LogDebug(() => $"Unregistering Task Type {typeof(TGameTask)} with TaskType {taskType}"); @@ -53,10 +54,22 @@ public static bool UnRegisterTaskType(TaskType taskType) public static TaskNetworkData ConvertTask(Task task) { - Multiplayer.LogDebug(()=>$"TaskNetworkDataFactory.ConvertTask: Processing task of type {task.GetType()}"); + Multiplayer.LogDebug(() => $"TaskNetworkDataFactory.ConvertTask: Processing task of type {task.InstanceTaskType}"); if (TypeToTaskNetworkData.TryGetValue(task.GetType(), out var converter)) { - return converter(task); + var taskData = converter(task); + + if (NetworkedJob.TryGetFromJob(task.Job, out var netJob) && netJob != null) + { + var taskNetId = netJob.GetTaskNetId(task); + taskData.TaskNetId = taskNetId; + } + else + { + Multiplayer.LogError($"TaskNetworkDataFactory.ConvertTask: Could not find NetworkedJob for jobId: {task.Job.ID}, taskType: {task.InstanceTaskType}"); + } + + return taskData; } throw new ArgumentException($"Unknown task type: {task.GetType()}"); } @@ -156,6 +169,8 @@ public override WarehouseTaskData FromTask(Task task) if (task is not WarehouseTask warehouseTask) throw new ArgumentException("Task is not a WarehouseTask"); + FromTaskCommon(task); + CarNetIDs = warehouseTask.cars .Select(car => NetworkedTrainCar.GetFromTrainId(car.ID, out var networkedTrainCar) ? networkedTrainCar.NetId @@ -170,16 +185,16 @@ public override WarehouseTaskData FromTask(Task task) return this; } - public override Task ToTask() + public override Task ToTask(ref Dictionary netIdToTask) { - List cars = CarNetIDs .Select(netId => NetworkedTrainCar.TryGet(netId, out TrainCar trainCar) ? trainCar : null) .Where(car => car != null) .Select(car => car.logicCar) .ToList(); - WarehouseTask newWareTask = new WarehouseTask( + WarehouseTask newWarehouseTask = new + ( cars, WarehouseTaskType, JobSaveManager.Instance.GetWarehouseMachineWithId(WarehouseMachine), @@ -187,11 +202,15 @@ public override Task ToTask() CargoAmount, (long)TimeLimit, IsLastTask - ); + ); + + ToTaskCommon(newWarehouseTask); + + newWarehouseTask.readyForMachine = ReadyForMachine; - newWareTask.readyForMachine = ReadyForMachine; + netIdToTask.Add(TaskNetId, newWarehouseTask); - return newWareTask; + return newWarehouseTask; } public override List GetCars() @@ -270,6 +289,8 @@ public override TransportTaskData FromTask(Task task) if (task is not TransportTask transportTask) throw new ArgumentException("Task is not a TransportTask"); + FromTaskCommon(task); + //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) @@ -288,16 +309,15 @@ public override TransportTaskData FromTask(Task task) return this; } - public override Task ToTask() + public override Task ToTask(ref Dictionary netIdToTask) { - //Multiplayer.LogDebug(() => $"TransportTaskData.ToTask() CarNetIDs !null {CarNetIDs != null}, count: {CarNetIDs?.Length}"); - List cars = CarNetIDs .Select(netId => NetworkedTrainCar.TryGet(netId, out TrainCar trainCar) ? trainCar.logicCar : null) .Where(car => car != null) .ToList(); - return new TransportTask( + var newTransportTask = new TransportTask + ( cars, RailTrackRegistry.Instance.GetTrackWithName(DestinationTrack).LogicTrack(), RailTrackRegistry.Instance.GetTrackWithName(StartingTrack).LogicTrack(), @@ -305,6 +325,12 @@ public override Task ToTask() (long)TimeLimit, IsLastTask ); + + ToTaskCommon(newTransportTask); + + netIdToTask.Add(TaskNetId, newTransportTask); + + return newTransportTask; } public override List GetCars() @@ -350,7 +376,6 @@ public override void Deserialize(BinaryReader reader) } CurrentTaskIndex = reader.ReadByte(); - } public override SequentialTasksData FromTask(Task task) @@ -358,7 +383,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}"); + FromTaskCommon(task); Tasks = TaskNetworkDataFactory.ConvertTasks(sequentialTasks.tasks); @@ -381,30 +406,37 @@ public override SequentialTasksData FromTask(Task task) return this; } - public override Task ToTask() + public override Task ToTask(ref Dictionary netIdToTask) { List tasks = []; foreach (var task in Tasks) - tasks.Add(task.ToTask()); + { + var taskResults = task.ToTask(ref netIdToTask); + tasks.Add(taskResults); + } + + SequentialTasks newSequentialTask = new(tasks, (long)TimeLimit); + + ToTaskCommon(newSequentialTask); - SequentialTasks newSeqTask = new(tasks, (long)TimeLimit); + netIdToTask.Add(TaskNetId, newSequentialTask); // Rebuild linked list task states - this is the equivalent of OverrideTasksStates(TaskSaveData[] tasksData) int index = 0; - for (var currentNode = newSeqTask.tasks.First; currentNode != null; currentNode = currentNode.Next) + for (var currentNode = newSequentialTask.tasks.First; currentNode != null; currentNode = currentNode.Next) { currentNode.Value.state = tasks[index].state; currentNode.Value.taskStartTime = tasks[index].taskStartTime; currentNode.Value.taskFinishTime = tasks[index].taskFinishTime; - if (tasks[index].state == TaskState.Done && currentNode != newSeqTask.tasks.Last) - newSeqTask.currentTask = currentNode.Next; + if (tasks[index].state == TaskState.Done && currentNode != newSequentialTask.tasks.Last) + newSequentialTask.currentTask = currentNode.Next; index++; } - return newSeqTask; + return newSequentialTask; } public override List GetCars() @@ -454,14 +486,27 @@ public override ParallelTasksData FromTask(Task task) if (task is not ParallelTasks parallelTasks) throw new ArgumentException("Task is not a ParallelTasks"); + FromTaskCommon(task); + Tasks = TaskNetworkDataFactory.ConvertTasks(parallelTasks.tasks); return this; } - public override Task ToTask() + public override Task ToTask(ref Dictionary netIdToTask) { - return new ParallelTasks(Tasks.Select(t => t.ToTask()).ToList(), (long)TimeLimit, IsLastTask); + List taskList = new(Tasks.Length); + + for (int i = 0; i < Tasks.Length; i++) + taskList.Add(Tasks[i].ToTask(ref netIdToTask)); + + var newParallelTasks = new ParallelTasks(taskList, (long)TimeLimit, IsLastTask); + + ToTaskCommon(newParallelTasks); + + netIdToTask.Add(TaskNetId, newParallelTasks); + + return newParallelTasks; } public override List GetCars() diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 23f85018..3883d6c3 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -171,7 +171,10 @@ protected override void Subscribe() netPacketProcessor.SubscribeReusable(OnClientboundJobsUpdatePacket); netPacketProcessor.SubscribeReusable(OnClientboundJobsCreatePacket); netPacketProcessor.SubscribeReusable(OnClientboundJobValidateResponsePacket); + netPacketProcessor.SubscribeReusable(OnClientboundTaskUpdatePacket); + netPacketProcessor.SubscribeReusable(OnCommonChatPacket); + netPacketProcessor.SubscribeNetSerializable(OnCommonItemChangePacket); } @@ -973,6 +976,29 @@ private void OnClientboundJobsUpdatePacket(ClientboundJobsUpdatePacket packet) networkedStationController.UpdateJobs(packet.JobUpdates); } + private void OnClientboundTaskUpdatePacket(ClientboundTaskUpdatePacket packet) + { + if (NetworkLifecycle.Instance.IsHost()) + return; + + if (!NetworkedJob.Get(packet.JobNetId, out NetworkedJob networkedJob) || networkedJob == null) + { + LogError($"Received task update for jobNetId {packet.JobNetId}, but job was not found!"); + return; + } + + var task = networkedJob.GetTaskFromNetId(packet.TaskNetId); + + if (task == null) + { + LogError($"Received task update for job [{networkedJob.Job.ID}, {packet.JobNetId}], with taskNetId {packet.TaskNetId}, task was not found"); + return; + } + + task.SetState(packet.NewState); + task.taskStartTime = packet.TaskStartTime; + task.taskFinishTime = packet.TaskFinishTime; + } private void OnClientboundJobValidateResponsePacket(ClientboundJobValidateResponsePacket packet) { diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index ee867929..afdb88ac 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -690,6 +690,19 @@ public void SendJobsUpdatePacket(ushort stationNetId, NetworkedJob[] jobs) SendPacketToAll(ClientboundJobsUpdatePacket.FromNetworkedJobs(stationNetId, jobs), DeliveryMethod.ReliableOrdered, SelfPeer); } + public void SendTaskUpdate(ushort netId, ushort taskNetId, TaskState newState, float taskStartTime, float taskFinishTime) + { + Multiplayer.Log($"Sending TaskUpdate for jobNetId {netId}, taskNetId {taskNetId}, newState {newState}"); + SendPacketToAll(new ClientboundTaskUpdatePacket + { + JobNetId = netId, + TaskNetId = taskNetId, + NewState = newState, + TaskStartTime = taskStartTime, + TaskFinishTime = taskFinishTime + }, DeliveryMethod.ReliableOrdered, SelfPeer); + } + public void SendItemsChangePacket(List items, ServerPlayer player) { Multiplayer.Log($"Sending SendItemsChangePacket with {items.Count()} items to {player.Username}"); diff --git a/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobsCreatePacket.cs b/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobsCreatePacket.cs index bd51543b..29bad650 100644 --- a/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobsCreatePacket.cs +++ b/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobsCreatePacket.cs @@ -1,7 +1,8 @@ -using System.Collections.Generic; using Multiplayer.Components.Networking.Jobs; using Multiplayer.Components.Networking.World; using Multiplayer.Networking.Data; +using System.Collections.Generic; + namespace Multiplayer.Networking.Packets.Clientbound.Jobs; public class ClientboundJobsCreatePacket diff --git a/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundTaskUpdatePacket.cs b/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundTaskUpdatePacket.cs new file mode 100644 index 00000000..ce8b7f49 --- /dev/null +++ b/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundTaskUpdatePacket.cs @@ -0,0 +1,12 @@ +using DV.Logic.Job; + +namespace Multiplayer.Networking.Packets.Clientbound.Jobs; + +internal class ClientboundTaskUpdatePacket +{ + public ushort JobNetId { get; set; } + public ushort TaskNetId { get; set; } + public TaskState NewState { get; set; } + public float TaskStartTime { get; set; } + public float TaskFinishTime { get; set; } +} diff --git a/Multiplayer/Patches/Jobs/TaskPatch.cs b/Multiplayer/Patches/Jobs/TaskPatch.cs new file mode 100644 index 00000000..ab396ec9 --- /dev/null +++ b/Multiplayer/Patches/Jobs/TaskPatch.cs @@ -0,0 +1,44 @@ +using DV.Logic.Job; +using HarmonyLib; +using Multiplayer.Components.Networking; +using Multiplayer.Components.Networking.Jobs; + +namespace Multiplayer.Patches.Jobs; + +[HarmonyPatch(typeof(Task))] +public static class TaskPatch +{ + [HarmonyPatch(nameof(Task.SetState))] + [HarmonyPrefix] + public static void SetStatePrefix(Task __instance, TaskState newState) + { + if (!NetworkLifecycle.Instance.IsHost()) + return; + + if (newState == TaskState.InProgress) + return; + + if (NetworkedJob.TryGetFromJob(__instance.Job, out var netJob) && netJob != null) + { + var taskNetId = netJob.GetTaskNetId(__instance); + + if (taskNetId == 0) + { + Multiplayer.LogError($"Task.SetState() could not find task index for jobId: {__instance.Job.ID}, taskType: {__instance.InstanceTaskType}"); + return; + } + NetworkLifecycle.Instance.Server.SendTaskUpdate(netJob.NetId, taskNetId, newState, __instance.taskStartTime, __instance.taskFinishTime); + } + + } + + [HarmonyPatch(nameof(Task.SetJobBelonging))] + [HarmonyPostfix] + public static void SetJobBelongingPostfix(Task __instance) + { + if (!NetworkLifecycle.Instance.IsHost()) + return; + + NetworkedJob.EnqueTask(__instance); + } +} diff --git a/MultiplayerAPI/Interfaces/IMultiplayerAPI.cs b/MultiplayerAPI/Interfaces/IMultiplayerAPI.cs index f8d99616..f83c8ea8 100644 --- a/MultiplayerAPI/Interfaces/IMultiplayerAPI.cs +++ b/MultiplayerAPI/Interfaces/IMultiplayerAPI.cs @@ -134,6 +134,6 @@ bool RegisterTaskType(TaskType taskType, Func - bool UnRegisterTaskType(TaskType taskType) where TGameTask : Task; + bool UnregisterTaskType(TaskType taskType) where TGameTask : Task; } diff --git a/MultiplayerAPI/MultiplayerAPI.csproj b/MultiplayerAPI/MultiplayerAPI.csproj index a65fba32..7e1843a3 100644 --- a/MultiplayerAPI/MultiplayerAPI.csproj +++ b/MultiplayerAPI/MultiplayerAPI.csproj @@ -34,6 +34,14 @@ + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/MultiplayerAPI/Types/TaskNetworkType.cs b/MultiplayerAPI/Types/TaskNetworkType.cs index ad39a784..e8d3ce32 100644 --- a/MultiplayerAPI/Types/TaskNetworkType.cs +++ b/MultiplayerAPI/Types/TaskNetworkType.cs @@ -9,9 +9,13 @@ namespace MPAPI.Types; /// Base class for serialising and deserialising job task data for transmission by Multiplayer mod. /// Not intended for direct use; inherit via . /// -/// public abstract class TaskNetworkData { + /// + /// Gets or sets the unique network identifier for this task within its job. + /// + public ushort TaskNetId { get; set; } + /// /// Gets or sets the current state of the task. /// See for possible values. @@ -65,11 +69,19 @@ public abstract class TaskNetworkData public abstract void Deserialize(BinaryReader reader); /// - /// Converts this network data instance into a object - /// compatible with the job/task system. + /// Converts this instance into a object + /// compatible with the job/task system, and adds them to the provided dictionary. /// + /// + /// A reference to a that will be populated with deserialized instances. + /// Each key is a netTaskId (ushort), and each value is the corresponding object. + /// /// A instance representing the deserialized data. - public abstract Task ToTask(); + /// + /// Implementations should add all relevant instances to . + /// This allows aggregation of multiple tasks from different objects into a single dictionary. + /// + public abstract Task ToTask(ref Dictionary netIdToTask); /// /// Gets a list of car IDs () associated with this task. @@ -93,6 +105,34 @@ public abstract class TaskNetworkData : TaskNetworkData where T : TaskNetwork /// This instance, populated with data from the provided task. public abstract T FromTask(Task task); + /// + /// Extracts and populates the common task data fields from the specified object. + /// Should be called as the first step in the FromTask implementation of derived classes. + /// + /// The to extract data from. + protected void FromTaskCommon(Task task) + { + State = task.state; + TaskStartTime = task.taskStartTime; + TaskFinishTime = task.taskFinishTime; + IsLastTask = task.IsLastTask; + TimeLimit = task.TimeLimit; + } + + /// + /// Populates common task data fields to the specified object. + /// Should be called after the new task has been instantiated ToTask implementation of derived classes. + /// + /// The to populate. + protected void ToTaskCommon(Task task) + { + task.state = State; + task.taskStartTime = TaskStartTime; + task.taskFinishTime = TaskFinishTime; + task.isLastTask = IsLastTask; + task.TimeLimit = TimeLimit; + } + /// /// Serialises the common task data fields to the specified . /// Should be called as the first step in the Serialize implementation of derived classes. @@ -100,6 +140,7 @@ public abstract class TaskNetworkData : TaskNetworkData where T : TaskNetwork /// The to write data to. protected void SerializeCommon(BinaryWriter writer) { + writer.Write(TaskNetId); writer.Write((byte)State); writer.Write(TaskStartTime); writer.Write(TaskFinishTime); @@ -116,6 +157,7 @@ protected void SerializeCommon(BinaryWriter writer) /// The to read data from. protected void DeserializeCommon(BinaryReader reader) { + TaskNetId = reader.ReadUInt16(); State = (TaskState)reader.ReadByte(); TaskStartTime = reader.ReadSingle(); TaskFinishTime = reader.ReadSingle(); From 271c233a22f85fc0d02ca85e7ded422b4e2549ac Mon Sep 17 00:00:00 2001 From: Macka Date: Sun, 14 Sep 2025 22:47:17 +1000 Subject: [PATCH 451/521] Reorder job time updates in job sync loop Moved the assignment of startTime and finishTime for jobs to occur before state and overview updates in the job synchronisation loop. This ensures job timing data is up-to-date before dependent job reports are generated. --- .../Networking/World/NetworkedStationController.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Multiplayer/Components/Networking/World/NetworkedStationController.cs b/Multiplayer/Components/Networking/World/NetworkedStationController.cs index 83eba763..d1dcba12 100644 --- a/Multiplayer/Components/Networking/World/NetworkedStationController.cs +++ b/Multiplayer/Components/Networking/World/NetworkedStationController.cs @@ -426,11 +426,12 @@ public void UpdateJobs(JobUpdateStruct[] jobs) if (!NetworkedJob.Get(job.JobNetID, out NetworkedJob netJob)) continue; + netJob.Job.startTime = job.StartTime; + netJob.Job.finishTime = job.FinishTime; + UpdateJobState(netJob, job); UpdateJobOverview(netJob, job); - netJob.Job.startTime = job.StartTime; - netJob.Job.finishTime = job.FinishTime; } } From 7fa28e68061c7a1bfcf3bd851520fbe20e540e12 Mon Sep 17 00:00:00 2001 From: Macka Date: Sun, 14 Sep 2025 22:48:41 +1000 Subject: [PATCH 452/521] Sync JobManager time in save game data packets Adds JobManagerTime to ClientboundSaveGameDataPacket and ensures it is loaded on the client when restoring game state. This change improves consistency of job timing between server and client during multiplayer save/load operations. --- .../Components/SaveGame/StartGameData_ServerSave.cs | 3 +++ .../Packets/Clientbound/ClientboundSaveGameDataPacket.cs | 8 +++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/Multiplayer/Components/SaveGame/StartGameData_ServerSave.cs b/Multiplayer/Components/SaveGame/StartGameData_ServerSave.cs index 2f14c15d..a079682c 100644 --- a/Multiplayer/Components/SaveGame/StartGameData_ServerSave.cs +++ b/Multiplayer/Components/SaveGame/StartGameData_ServerSave.cs @@ -5,6 +5,7 @@ using DV; using DV.CabControls; using DV.Common; +using DV.Logic.Job; using DV.UserManagement; using DV.Utils; using Multiplayer.Components.Networking; @@ -46,6 +47,8 @@ public void SetFromPacket(ClientboundSaveGameDataPacket packet) CareerManagerDebtControllerPatch.HasDebt = packet.HasDebt; + JobsManager.Instance.LoadTime(packet.JobManagerTime); + Multiplayer.LogDebug(() => { string unlockedGen = string.Join(", ", UnlockablesManager.Instance.UnlockedGeneralLicenses); diff --git a/Multiplayer/Networking/Packets/Clientbound/ClientboundSaveGameDataPacket.cs b/Multiplayer/Networking/Packets/Clientbound/ClientboundSaveGameDataPacket.cs index 551bb1d5..b6a2a610 100644 --- a/Multiplayer/Networking/Packets/Clientbound/ClientboundSaveGameDataPacket.cs +++ b/Multiplayer/Networking/Packets/Clientbound/ClientboundSaveGameDataPacket.cs @@ -1,5 +1,6 @@ using DV.InventorySystem; using DV.JObjectExtstensions; +using DV.Logic.Job; using DV.ServicePenalty; using DV.UserManagement; using Multiplayer.Components.Networking; @@ -31,8 +32,11 @@ public class ClientboundSaveGameDataPacket // public string Debt_deleted_jobless_cars { get; set; } // public string Debt_insurance { get; set; } + public float JobManagerTime { get; set; } + public static ClientboundSaveGameDataPacket CreatePacket(ServerPlayer player) { + Multiplayer.LogDebug(() => $"ClientboundSaveGameDataPacket.CreatePacket() for player (is null: {player == null}) {player?.Username} ({player?.Guid})"); if (WorldStreamingInit.isLoaded) SaveGameManager.Instance.UpdateInternalData(); @@ -63,7 +67,7 @@ public static ClientboundSaveGameDataPacket CreatePacket(ServerPlayer player) UnlockedGarages = data.GetStringArray(SaveGameKeys.Garages), Position = playerData?.GetVector3(SaveGameKeys.Player_position) ?? LevelInfo.DefaultSpawnPosition, Rotation = playerData?.GetFloat(SaveGameKeys.Player_rotation) ?? LevelInfo.DefaultSpawnRotation.y, - HasDebt = data.GetFloat(SaveGameKeys.Debt_total).GetValueOrDefault(CareerManagerDebtController.Instance != null ? CareerManagerDebtController.Instance.NumberOfNonZeroPricedDebts : 0) > 0 + HasDebt = data.GetFloat(SaveGameKeys.Debt_total).GetValueOrDefault(CareerManagerDebtController.Instance != null ? CareerManagerDebtController.Instance.NumberOfNonZeroPricedDebts : 0) > 0, // Debt_existing_locos = data.GetJObjectArray(SaveGameKeys.Debt_existing_locos)?.NotNull().Select(j => j.ToString()).ToArray(), // Debt_deleted_locos = data.GetJObjectArray(SaveGameKeys.Debt_deleted_locos)?.NotNull().Select(j => j.ToString()).ToArray(), // Debt_existing_jobs = data.GetJObjectArray(SaveGameKeys.Debt_existing_jobs)?.NotNull().Select(j => j.ToString()).ToArray(), @@ -71,6 +75,8 @@ public static ClientboundSaveGameDataPacket CreatePacket(ServerPlayer player) // Debt_existing_jobless_cars = data.GetJObject(SaveGameKeys.Debt_existing_jobless_cars)?.ToString(), // Debt_deleted_jobless_cars = data.GetJObject(SaveGameKeys.Debt_deleted_jobless_cars)?.ToString(), // Debt_insurance = data.GetJObject(SaveGameKeys.Debt_insurance)?.ToString() + + JobManagerTime = JobsManager.Instance.Time }; } From 894d56503994c89129c6e71bac92b66fcb1d7fec Mon Sep 17 00:00:00 2001 From: Macka Date: Sun, 14 Sep 2025 22:53:20 +1000 Subject: [PATCH 453/521] Refactor warehouse machine and cargo state sync Introduces NetId-based handling for WarehouseMachine and WarehouseMachineController. Updates ClientboundCargoStatePacket to use WarehouseMachineNetId instead of string ID, and refactors related client/server logic and patches to use new methods for retrieving networked warehouse machine controllers and their NetIds. Improves reliability and consistency of cargo state synchronisation between client and server. --- Multiplayer/API/NetIdProvider.cs | 2 + .../NetworkedWarehouseMachineController.cs | 61 +++++++++++++++++-- .../Managers/Client/NetworkClient.cs | 24 +++++--- .../Managers/Server/NetworkServer.cs | 17 +++++- .../Train/ClientboundCargoStatePacket.cs | 6 +- .../Jobs/WarehouseMachineControllerPatch.cs | 6 +- 6 files changed, 93 insertions(+), 23 deletions(-) diff --git a/Multiplayer/API/NetIdProvider.cs b/Multiplayer/API/NetIdProvider.cs index 715c8928..4484e295 100644 --- a/Multiplayer/API/NetIdProvider.cs +++ b/Multiplayer/API/NetIdProvider.cs @@ -30,6 +30,8 @@ protected override void Awake() RegisterHandler(NetworkedStationController.TryGetNetId, NetworkedStationController.TryGet); RegisterHandler(NetworkedStationController.TryGetNetId, NetworkedStationController.TryGet); RegisterHandler(NetworkedStationController.TryGetNetId, NetworkedStationController.TryGet); + RegisterHandler(NetworkedWarehouseMachineController.TryGetNetId, NetworkedWarehouseMachineController.TryGet); + RegisterHandler(NetworkedWarehouseMachineController.TryGetNetId, NetworkedWarehouseMachineController.TryGet); RegisterHandler(NetworkedJob.TryGetNetId, NetworkedJob.GetJob); diff --git a/Multiplayer/Components/Networking/Jobs/NetworkedWarehouseMachineController.cs b/Multiplayer/Components/Networking/Jobs/NetworkedWarehouseMachineController.cs index 0c49b9b6..64389ea8 100644 --- a/Multiplayer/Components/Networking/Jobs/NetworkedWarehouseMachineController.cs +++ b/Multiplayer/Components/Networking/Jobs/NetworkedWarehouseMachineController.cs @@ -25,9 +25,58 @@ public static bool Get(ushort netId, out NetworkedWarehouseMachineController obj } public static NetworkedWarehouseMachineController GetFromWarehouseMachineController(WarehouseMachineController warehouseMachineController) + public static bool TryGetNetId(WarehouseMachineController warehouseMachineController, out ushort netId) { - warehouseMachineControllerToNetworked.TryGetValue(warehouseMachineController, out var netWarehouseMachineController); - return netWarehouseMachineController; + if (GetFromWarehouseMachineController(warehouseMachineController, out var networkedWarehouseMachineController)) + { + netId = networkedWarehouseMachineController.NetId; + return true; + } + + netId = 0; + return false; + } + + public static bool TryGetNetId(WarehouseMachine warehouseMachine, out ushort netId) + { + var networkedWarehouseMachineController = GetFromWarehouseMachine(warehouseMachine); + if (networkedWarehouseMachineController != null) + { + netId = networkedWarehouseMachineController.NetId; + return true; + } + + netId = 0; + return false; + } + + public static bool TryGet(ushort netId, out WarehouseMachineController warehouseMachineController) + { + if (Get(netId, out var networkedWarehouseMachineController)) + { + warehouseMachineController = networkedWarehouseMachineController.WarehouseMachineController; + return true; + } + + warehouseMachineController = null; + return false; + } + + public static bool TryGet(ushort netId, out WarehouseMachine warehouseMachine) + { + if (Get(netId, out var networkedWarehouseMachineController)) + { + warehouseMachine = networkedWarehouseMachineController.WarehouseMachine; + return true; + } + + warehouseMachine = null; + return false; + } + + public static bool GetFromWarehouseMachineController(WarehouseMachineController warehouseMachineController, out NetworkedWarehouseMachineController networkedWarehouseMachineController) + { + return warehouseMachineControllerToNetworked.TryGetValue(warehouseMachineController, out networkedWarehouseMachineController); } public static NetworkedWarehouseMachineController GetFromWarehouseMachine(WarehouseMachine warehouseMachine) @@ -41,9 +90,8 @@ public static NetworkedWarehouseMachineController GetFromWarehouseMachine(Wareho if (warehouseMachineController != null) { //Warehouse Machine Controller found, check for NetworkedWarehouseMachineController - networkedWarehouseMachineController = GetFromWarehouseMachineController(warehouseMachineController); - if (networkedWarehouseMachineController != null) - warehouseMachineToNetworked[warehouseMachine] = GetFromWarehouseMachineController(warehouseMachineController); + if (!GetFromWarehouseMachineController(warehouseMachineController, out networkedWarehouseMachineController) && networkedWarehouseMachineController != null) + warehouseMachineToNetworked[warehouseMachine] = networkedWarehouseMachineController; } return networkedWarehouseMachineController; @@ -114,7 +162,7 @@ public void ClientProcessUpdate(ClientboundWarehouseControllerUpdatePacket packe if (packet.CarNetId != 0) { - if (!NetworkedTrainCar.Get(packet.CarNetId, out var networkedCar)) + if (!NetworkedTrainCar.TryGet(packet.CarNetId, out NetworkedTrainCar networkedCar)) { Multiplayer.LogWarning($"NetworkedWarehouseMachineController failed to find TrainCar with NetId: {packet.NetId}"); return; @@ -164,6 +212,7 @@ private void CleanupTask(bool isLoading, Car car) foreach (var task in data.tasksAvailableToProcess) WarehouseMachine.RemoveWarehouseTask(task); + } } } diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 3883d6c3..2c164414 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -43,7 +43,6 @@ using System.Collections.Generic; using System.Linq; using UnityEngine; -using UnityModManagerNet; using Object = UnityEngine.Object; namespace Multiplayer.Networking.Managers.Client; @@ -768,7 +767,7 @@ private void OnClientboundCargoStatePacket(ClientboundCargoStatePacket packet) if (!NetworkedTrainCar.TryGet(packet.NetId, out NetworkedTrainCar networkedTrainCar)) return; - LogDebug(() => $"OnClientboundCargoStatePacket() {networkedTrainCar.CurrentID}, health: {packet.CargoHealth}"); + LogDebug(() => $"OnClientboundCargoStatePacket() {networkedTrainCar.CurrentID}, IsLoading: {packet.IsLoading}, CargoType: {packet.CargoType}, CargoAmount: {packet.CargoAmount}, Health: {packet.CargoHealth}, CargoModelIndex: {packet.CargoModelIndex}, WarehouseMachineId: {packet.WarehouseMachineNetId}"); networkedTrainCar.CargoModelIndex = packet.CargoModelIndex; Car logicCar = networkedTrainCar.TrainCar.logicCar; @@ -780,6 +779,7 @@ private void OnClientboundCargoStatePacket(ClientboundCargoStatePacket packet) } if (packet.CargoType == (ushort)CargoType.None && logicCar.CurrentCargoTypeInCar == CargoType.None) + if (packet.CargoType == CargoType.None && logicCar.CurrentCargoTypeInCar == CargoType.None) return; //packet.CargoAmount is the total amount, not the amount to load/unload @@ -787,48 +787,54 @@ private void OnClientboundCargoStatePacket(ClientboundCargoStatePacket packet) // todo: cache warehouse machine WarehouseMachine warehouse = string.IsNullOrEmpty(packet.WarehouseMachineId) ? null : JobSaveManager.Instance.GetWarehouseMachineWithId(packet.WarehouseMachineId); + if (packet.WarehouseMachineNetId != 0 && (!NetworkedWarehouseMachineController.TryGet(packet.WarehouseMachineNetId, out warehouseMachine) || warehouseMachine == null)) if (packet.IsLoading) { + LogDebug(() => $"OnClientboundCargoStatePacket() Loading cargo: {packet.CargoType} into {networkedTrainCar.CurrentID}, current amount: {packet.CargoAmount}"); //Check correct cargo is loaded and the amount is correct - if (logicCar.LoadedCargoAmount == cargoAmount && logicCar.CurrentCargoTypeInCar == (CargoType)packet.CargoType) + if (logicCar.LoadedCargoAmount == cargoAmount && logicCar.CurrentCargoTypeInCar == 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) + if (logicCar.CurrentCargoTypeInCar != CargoType.None && logicCar.CurrentCargoTypeInCar != packet.CargoType) logicCar.DumpCargo(); //We have the correct cargo, but not the right amount, calculate the delta if (logicCar.CurrentCargoTypeInCar == (CargoType)packet.CargoType) + if (logicCar.CurrentCargoTypeInCar == packet.CargoType) cargoAmount -= logicCar.LoadedCargoAmount; if (cargoAmount > 0) { - logicCar.LoadCargo(cargoAmount, (CargoType)packet.CargoType, warehouse); + logicCar.LoadCargo(cargoAmount, packet.CargoType, warehouseMachine); } networkedTrainCar.TrainCar.CargoDamage.LoadCargoDamageState(packet.CargoHealth); } else { + LogDebug(() => $"OnClientboundCargoStatePacket() Unloading cargo: {packet.CargoType} into {networkedTrainCar.CurrentID}, current amount: {packet.CargoAmount}"); + //Check correct cargo is loaded and the amount is correct - if (logicCar.LoadedCargoAmount == cargoAmount && logicCar.CurrentCargoTypeInCar == (CargoType)packet.CargoType) + if (logicCar.LoadedCargoAmount == cargoAmount && logicCar.CurrentCargoTypeInCar == 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) + if (logicCar.CurrentCargoTypeInCar == CargoType.None || logicCar.CurrentCargoTypeInCar != packet.CargoType) { //avoid triggering the load event by backdooring it logicCar.LastUnloadedCargoType = logicCar.CurrentCargoTypeInCar; - logicCar.CurrentCargoTypeInCar = (CargoType)packet.CargoType; + logicCar.CurrentCargoTypeInCar = packet.CargoType; logicCar.LoadedCargoAmount = cargoAmount; } //We have the correct cargo, calculate the delta - if (logicCar.CurrentCargoTypeInCar == (CargoType)packet.CargoType) + if (logicCar.CurrentCargoTypeInCar == packet.CargoType) cargoAmount = logicCar.LoadedCargoAmount - cargoAmount; if (cargoAmount > 0) - logicCar.UnloadCargo(cargoAmount, (CargoType)packet.CargoType, warehouse); + logicCar.UnloadCargo(cargoAmount, packet.CargoType, warehouseMachine); } } diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index afdb88ac..3216c79b 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -540,15 +540,28 @@ public void SendCargoState(NetworkedTrainCar netTraincar, bool isLoading, byte c } CargoType cargoType = isLoading ? logicCar.CurrentCargoTypeInCar : logicCar.LastUnloadedCargoType; + + ushort netMachineId = 0; + if (logicCar.CargoOriginWarehouse != null) + { + if (!NetworkedWarehouseMachineController.TryGetNetId(logicCar.CargoOriginWarehouse, out netMachineId)) + { + Log($"Attempting to send cargo state for {netTraincar.CurrentID}, for warehouse machine at track {logicCar.CargoOriginWarehouse.WarehouseTrack.ID}, but Warehouse Machine Controller was not found"); + return; + } + } + + + SendPacketToAll(new ClientboundCargoStatePacket { NetId = netTraincar.NetId, IsLoading = isLoading, - CargoType = (ushort)cargoType, + CargoType = cargoType, CargoAmount = logicCar.LoadedCargoAmount, CargoHealth = netTraincar.TrainCar.CargoDamage.HealthPercentage, CargoModelIndex = cargoModelIndex, - WarehouseMachineId = logicCar.CargoOriginWarehouse?.ID + WarehouseMachineNetId = netMachineId, }, DeliveryMethod.ReliableOrdered, SelfPeer); } diff --git a/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundCargoStatePacket.cs b/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundCargoStatePacket.cs index 8347448d..2e0ec6a8 100644 --- a/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundCargoStatePacket.cs +++ b/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundCargoStatePacket.cs @@ -1,12 +1,14 @@ +using DV.ThingTypes; + namespace Multiplayer.Networking.Packets.Clientbound.Train; public class ClientboundCargoStatePacket { public ushort NetId { get; set; } public bool IsLoading { get; set; } - public ushort CargoType { get; set; } + public CargoType CargoType { get; set; } public float CargoAmount { get; set; } public float CargoHealth { get; set; } public byte CargoModelIndex { get; set; } - public string WarehouseMachineId { get; set; } + public ushort WarehouseMachineNetId { get; set; } } diff --git a/Multiplayer/Patches/Jobs/WarehouseMachineControllerPatch.cs b/Multiplayer/Patches/Jobs/WarehouseMachineControllerPatch.cs index 1de6e298..ed42c8a4 100644 --- a/Multiplayer/Patches/Jobs/WarehouseMachineControllerPatch.cs +++ b/Multiplayer/Patches/Jobs/WarehouseMachineControllerPatch.cs @@ -40,8 +40,7 @@ public static bool SetScreen(WarehouseMachineController __instance, TextPreset p if (skip) return true; - var netMachine = NetworkedWarehouseMachineController.GetFromWarehouseMachineController(__instance); - if (netMachine == null) + if (!NetworkedWarehouseMachineController.GetFromWarehouseMachineController(__instance, out var netMachine) || netMachine == null) { Multiplayer.LogError($"WarehouseMachineControllerPatch.SetScreen(): Failed to get NetworkedWarehouseMachineController for {__instance.warehouseTrackName}"); return true; @@ -113,7 +112,6 @@ public static bool StartLoadSequence_Prefix(WarehouseMachineController __instanc private static void SendValidationRequest(WarehouseMachineController machine, WarehouseAction action) { string id = machine?.warehouseMachine?.ID; - var netController = NetworkedWarehouseMachineController.GetFromWarehouseMachineController(machine); if (string.IsNullOrEmpty(id)) { @@ -121,7 +119,7 @@ private static void SendValidationRequest(WarehouseMachineController machine, Wa return; } - if (netController == null) + if (!NetworkedWarehouseMachineController.GetFromWarehouseMachineController(machine, out var netController) || netController == null) { NetworkLifecycle.Instance.Client.LogError($"Failed to find NetworkedWarehouseMachineController {machine?.warehouseTrackName}. Warehouse not found!"); return; From 6ee1a68ef27d90236e08ac0398185f8cf769bbbd Mon Sep 17 00:00:00 2001 From: Macka Date: Sun, 14 Sep 2025 22:54:14 +1000 Subject: [PATCH 454/521] Remove unused GetFromWarehouseMachineController method --- .../Networking/Jobs/NetworkedWarehouseMachineController.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Multiplayer/Components/Networking/Jobs/NetworkedWarehouseMachineController.cs b/Multiplayer/Components/Networking/Jobs/NetworkedWarehouseMachineController.cs index 64389ea8..f36594e8 100644 --- a/Multiplayer/Components/Networking/Jobs/NetworkedWarehouseMachineController.cs +++ b/Multiplayer/Components/Networking/Jobs/NetworkedWarehouseMachineController.cs @@ -24,7 +24,6 @@ public static bool Get(ushort netId, out NetworkedWarehouseMachineController obj return b; } - public static NetworkedWarehouseMachineController GetFromWarehouseMachineController(WarehouseMachineController warehouseMachineController) public static bool TryGetNetId(WarehouseMachineController warehouseMachineController, out ushort netId) { if (GetFromWarehouseMachineController(warehouseMachineController, out var networkedWarehouseMachineController)) From b05bfea48d8bd8da3b570cd7fcbe2e1afb1f45a6 Mon Sep 17 00:00:00 2001 From: Macka Date: Sun, 14 Sep 2025 22:54:38 +1000 Subject: [PATCH 455/521] Add compatibility status for three new mods Registered CommsRadioAPI, DVLangHelper, and LightingOverhaul for client compatibility in ModCompatibilityManager. --- Multiplayer/API/ModCompatibilityManager.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Multiplayer/API/ModCompatibilityManager.cs b/Multiplayer/API/ModCompatibilityManager.cs index 57913d4c..c9f00d1a 100644 --- a/Multiplayer/API/ModCompatibilityManager.cs +++ b/Multiplayer/API/ModCompatibilityManager.cs @@ -33,6 +33,9 @@ protected override void Awake() RegisterCompatibility("BookletOrganizer", MultiplayerCompatibility.Client); RegisterCompatibility("RemoteDispatch", MultiplayerCompatibility.Client); RegisterCompatibility("DVDiscordPresenceMod", MultiplayerCompatibility.Client); + RegisterCompatibility("CommsRadioAPI", MultiplayerCompatibility.Client); + RegisterCompatibility("DVLangHelper", MultiplayerCompatibility.Client); + RegisterCompatibility("LightingOverhaul", MultiplayerCompatibility.Client); //Json entries will override hardcoded entries ReadModJsons(); From 5806f256f3973c994c61b0c9e2e490b83900a906 Mon Sep 17 00:00:00 2001 From: Macka Date: Sun, 14 Sep 2025 22:55:37 +1000 Subject: [PATCH 456/521] Refactor warehouse machine lookup in cargo state handler --- .../Networking/Managers/Client/NetworkClient.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 2c164414..2d3b56b3 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -778,16 +778,19 @@ private void OnClientboundCargoStatePacket(ClientboundCargoStatePacket packet) return; } - if (packet.CargoType == (ushort)CargoType.None && logicCar.CurrentCargoTypeInCar == CargoType.None) if (packet.CargoType == 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); + WarehouseMachine warehouseMachine = null; if (packet.WarehouseMachineNetId != 0 && (!NetworkedWarehouseMachineController.TryGet(packet.WarehouseMachineNetId, out warehouseMachine) || warehouseMachine == null)) + { + LogWarning($"OnClientboundCargoStatePacket() Failed to find WarehouseMachine for netId {packet.WarehouseMachineNetId}"); + return; + } + if (packet.IsLoading) { LogDebug(() => $"OnClientboundCargoStatePacket() Loading cargo: {packet.CargoType} into {networkedTrainCar.CurrentID}, current amount: {packet.CargoAmount}"); @@ -796,12 +799,10 @@ private void OnClientboundCargoStatePacket(ClientboundCargoStatePacket packet) 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) if (logicCar.CurrentCargoTypeInCar != CargoType.None && logicCar.CurrentCargoTypeInCar != packet.CargoType) logicCar.DumpCargo(); //We have the correct cargo, but not the right amount, calculate the delta - if (logicCar.CurrentCargoTypeInCar == (CargoType)packet.CargoType) if (logicCar.CurrentCargoTypeInCar == packet.CargoType) cargoAmount -= logicCar.LoadedCargoAmount; From 8f6ae04e68739b258630780937189f9ef9657cc1 Mon Sep 17 00:00:00 2001 From: Macka Date: Sun, 12 Oct 2025 10:03:52 +1000 Subject: [PATCH 457/521] Add mod-controlled ready-block system to client Introduces RegisterReadyBlock and CancelReadyBlock methods to allow mods to delay the client 'Ready' signal until all required mods have finished loading. --- Multiplayer/API/ClientAPIProvider.cs | 11 +++++ .../Managers/Client/NetworkClient.cs | 45 ++++++++++++++++++- MultiplayerAPI/Interfaces/IClient.cs | 19 ++++++++ MultiplayerAPI/MultiplayerAPI.csproj | 5 +++ 4 files changed, 78 insertions(+), 2 deletions(-) diff --git a/Multiplayer/API/ClientAPIProvider.cs b/Multiplayer/API/ClientAPIProvider.cs index 56bf1d5b..7bbbe4a7 100644 --- a/Multiplayer/API/ClientAPIProvider.cs +++ b/Multiplayer/API/ClientAPIProvider.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Linq; +using static UnityModManagerNet.UnityModManager; namespace Multiplayer.API { @@ -17,6 +18,16 @@ public class ClientAPIProvider : IClient public event Action OnPlayerConnected; public event Action OnPlayerDisconnected; + public void RegisterReadyBlock(ModInfo modInfo) + { + client.RegisterReadyBlock(modInfo.DisplayName); + } + + public void CancelReadyBlock(ModInfo modInfo) + { + client.CancelReadyBlock(modInfo.DisplayName); + } + #region Client Properties public byte PlayerId => client.PlayerId; public IReadOnlyCollection Players => client.ClientPlayerManager.Players.Select(GetWrapper).ToList().AsReadOnly(); diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 2d3b56b3..1a11dd73 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -40,6 +40,7 @@ using Multiplayer.Utils; using Newtonsoft.Json.Linq; using System; +using System.Collections; using System.Collections.Generic; using System.Linq; using UnityEngine; @@ -55,7 +56,7 @@ public class NetworkClient : NetworkManager private string disconnectMessage; private ITransportPeer selfPeer; - public byte PlayerId{ get; private set; } + public byte PlayerId { get; private set; } public readonly ClientPlayerManager ClientPlayerManager; // One way ping in milliseconds @@ -68,6 +69,9 @@ public class NetworkClient : NetworkManager private bool isAlsoHost; IGameSession originalSession; + // Allow mods to add to the wait Queue + private readonly List readyBlocks = []; + public NetworkClient(Settings settings, bool singlePlayer) : base(settings) { isSinglePlayer = singlePlayer; @@ -196,6 +200,29 @@ protected override void Subscribe() ); } + // Allow mods to register ready blocks + internal void RegisterReadyBlock(string modName) + { + Log($"Ready Block has been registered by {modName}"); + + if (readyBlocks.Contains(modName)) + return; + + readyBlocks.Add(modName); + } + + internal void CancelReadyBlock(string modName) + { + Log($"Ready Block has been cleared by {modName}"); + + if (readyBlocks.Contains(modName)) + { + readyBlocks.Remove(modName); + DisplayLoadingInfo displayLoadingInfo = Object.FindObjectOfType(); + displayLoadingInfo?.OnLoadingStatusChanged($"Mod {modName} loaded", false, 100); + } + } + private void OnLoaded() { Log($"WorldStreamingInit.LoadingFinished()"); @@ -203,11 +230,25 @@ private void OnLoaded() Log($"WorldStreamingInit.LoadingFinished() CacheWorldItems()"); NetworkedItemManager.Instance.CacheWorldItems(); Log($"WorldStreamingInit.LoadingFinished() SendReadyPacket()"); - SendReadyPacket(); + CoroutineManager.Instance.StartCoroutine(WaitForReadyBlocks()); WorldStreamingInit.LoadingFinished -= OnLoaded; } + private IEnumerator WaitForReadyBlocks() + { + DisplayLoadingInfo displayLoadingInfo = Object.FindObjectOfType(); + foreach (string modName in readyBlocks) + displayLoadingInfo?.OnLoadingStatusChanged($"Waiting for mod {modName} to load", false, 100); + + while (readyBlocks.Count > 0) + { + yield return null; + } + + SendReadyPacket(); + } + #region Net Events public override void OnPeerConnected(ITransportPeer peer) diff --git a/MultiplayerAPI/Interfaces/IClient.cs b/MultiplayerAPI/Interfaces/IClient.cs index ad9ac174..06153cbe 100644 --- a/MultiplayerAPI/Interfaces/IClient.cs +++ b/MultiplayerAPI/Interfaces/IClient.cs @@ -2,6 +2,7 @@ using MPAPI.Types; using System; using System.Collections.Generic; +using static UnityModManagerNet.UnityModManager; namespace MPAPI.Interfaces; @@ -22,6 +23,24 @@ public interface IClient /// IPlayer object for the disconnected player event Action OnPlayerDisconnected; + /// + /// Registers a block to prevent the client from sending the 'Ready' signal to the server until all mods have called 'CancelReadyBlock'. + /// + /// Mod information + /// + /// Only required if the mod needs complete loading prior to receiving game state from the server. + /// + void RegisterReadyBlock(ModInfo modInfo); + + /// + /// Cancels a previously registered ready block + /// + /// Mod information + /// + /// All registered blocks must be cancelled prior to the client sending the 'Ready' signal to the server. + /// + void CancelReadyBlock(ModInfo modInfo); + /// /// Gets Player Id of the local player /// diff --git a/MultiplayerAPI/MultiplayerAPI.csproj b/MultiplayerAPI/MultiplayerAPI.csproj index 7e1843a3..93a430ac 100644 --- a/MultiplayerAPI/MultiplayerAPI.csproj +++ b/MultiplayerAPI/MultiplayerAPI.csproj @@ -42,6 +42,11 @@ + + + + + From ec5a2932c3b585f6d158b187bd371c14e158bb36 Mon Sep 17 00:00:00 2001 From: Macka Date: Sun, 12 Oct 2025 10:14:41 +1000 Subject: [PATCH 458/521] Refactor task networking to use NetworkedTask Introduces NetworkedTask for improved task management and lookup, refactors NetworkedJob to delegate task handling to NetworkedTask, and updates related logic and packet structures to remove redundant job references. This change streamlines task synchronization and lookup, improves code clarity, and fixes issues with task initialization and network ID assignment. --- Multiplayer/API/NetIdProvider.cs | 5 +- .../Networking/Jobs/NetworkedJob.cs | 136 +++++++++--------- .../Networking/Jobs/NetworkedTask.cs | 76 ++++++++++ .../World/NetworkedStationController.cs | 19 +-- Multiplayer/Networking/Data/JobData.cs | 37 ++--- .../Networking/Data/TaskNetworkData.cs | 7 +- .../Managers/Client/NetworkClient.cs | 12 +- .../Jobs/ClientboundJobsCreatePacket.cs | 1 - .../Jobs/ClientboundTaskUpdatePacket.cs | 1 - Multiplayer/Patches/Jobs/TaskPatch.cs | 23 ++- MultiplayerAPI/Interfaces/IMultiplayerAPI.cs | 1 - MultiplayerAPI/MultiplayerAPI.cs | 1 - MultiplayerAPI/Types/TaskNetworkType.cs | 2 +- 13 files changed, 189 insertions(+), 132 deletions(-) create mode 100644 Multiplayer/Components/Networking/Jobs/NetworkedTask.cs diff --git a/Multiplayer/API/NetIdProvider.cs b/Multiplayer/API/NetIdProvider.cs index 4484e295..9feb02dd 100644 --- a/Multiplayer/API/NetIdProvider.cs +++ b/Multiplayer/API/NetIdProvider.cs @@ -33,7 +33,8 @@ protected override void Awake() RegisterHandler(NetworkedWarehouseMachineController.TryGetNetId, NetworkedWarehouseMachineController.TryGet); RegisterHandler(NetworkedWarehouseMachineController.TryGetNetId, NetworkedWarehouseMachineController.TryGet); - RegisterHandler(NetworkedJob.TryGetNetId, NetworkedJob.GetJob); + RegisterHandler(NetworkedJob.TryGetNetId, NetworkedJob.TryGetJob); + RegisterHandler(NetworkedTask.TryGetNetId, NetworkedTask.TryGet); RegisterHandler(NetworkedItem.TryGetNetId, NetworkedItem.GetItem); } @@ -66,7 +67,7 @@ public bool TryGetObject(ushort netId, out T obj) where T : class if (handlers.TryGetValue(typeof(T), out var handler) && handler is (TryGetNetIdDelegate _, TryGetObjectDelegate tryGetObject)) return tryGetObject(netId, out obj); - return false; + return false; } [UsedImplicitly] diff --git a/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs b/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs index 9e9b435e..4293e09a 100644 --- a/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs +++ b/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs @@ -1,10 +1,9 @@ using DV.Logic.Job; using Multiplayer.Components.Networking.World; using Multiplayer.Networking.Data; -using Multiplayer.Utils; -using System.Collections.Generic; -using System.Linq; using System; +using System.Collections.Generic; +using UnityEngine; namespace Multiplayer.Components.Networking.Jobs; @@ -12,9 +11,9 @@ 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(); + private static readonly Dictionary jobToNetworkedJob = []; + private static readonly Dictionary jobIdToNetworkedJob = []; + private static readonly Dictionary jobIdToJob = []; public static bool Get(ushort netId, out NetworkedJob obj) { @@ -23,7 +22,7 @@ public static bool Get(ushort netId, out NetworkedJob obj) return b; } - public static bool GetJob(ushort netId, out Job obj) + public static bool TryGetJob(ushort netId, out Job obj) { bool b = Get(netId, out NetworkedJob networkedJob); obj = b ? networkedJob.Job : null; @@ -122,7 +121,7 @@ public NetworkedItem JobReport } } - private List JobReports = new List(); + private readonly List JobReports = []; public Guid OwnedBy { get; set; } = Guid.Empty; public JobValidator JobValidator { get; set; } @@ -138,8 +137,7 @@ public NetworkedItem JobReport public List JobCars = []; - private readonly IdPool idPool = new(); - private readonly Dictionary taskMap = []; + private bool tasksInitialized = false; protected override void Awake() { @@ -163,6 +161,8 @@ public void Initialize(Job job, NetworkedStationController station) Job = job; Station = station; + transform.SetParent(station.transform); + // Setup handlers job.JobTaken += OnJobTaken; job.JobAbandoned += OnJobAbandoned; @@ -175,12 +175,20 @@ public void Initialize(Job job, NetworkedStationController station) AddToCache(); } - //Check for any pending tasks that were added before the NetworkedJob was created - if(pendingJobTasks.TryGetValue(job, out var taskList) && taskList != null) + // Check for any pending tasks that were added before the NetworkedJob was created + if (pendingJobTasks.TryGetValue(job, out var taskList) && taskList != null) { - taskList.ForEach(t => AddTask(t)); pendingJobTasks.Remove(job); + + Multiplayer.LogDebug(() => $"NetworkedJob.Initialize(): Found {taskList.Count} pending tasks for jobId {job.ID}"); + + foreach (var task in taskList) + CreateNetworkedTask(task); } + + tasksInitialized = true; + + Multiplayer.LogDebug(() => $"NetworkedJob.Initialize(): Initialized NetworkedJob for jobId {job.ID} with {Job.tasks.Count} tasks"); } private void AddToCache() @@ -191,6 +199,53 @@ private void AddToCache() //Multiplayer.Log($"NetworkedJob added to cache: {Job.ID}"); } + public static void EnqueueTask(Task task, Job job) + { + if (TryGetFromJob(job, out var netJob) || netJob != null && netJob.tasksInitialized) + { + Multiplayer.LogDebug(() => $"NetworkedJob.EnqueueTask(): Creating task immediately for jobId {task.Job.ID}"); + netJob.CreateNetworkedTask(task); + return; + } + + Multiplayer.LogDebug(() => $"NetworkedJob.EnqueueTask(): Enqueuing task for later creation for jobId {task.Job.ID}"); + + if (!pendingJobTasks.TryGetValue(task.Job, out var taskList)) + { + taskList = []; + pendingJobTasks[task.Job] = taskList; + } + taskList.Add(task); + } + + public void SetTasksFromServer(Dictionary netIdToTask) + { + if (netIdToTask == null) + { + Multiplayer.LogError($"NetworkedJob.SetTasksFromServer(): netIdToTask is null for jobId {Job?.ID}"); + return; + } + + foreach (var kvp in netIdToTask) + { + CreateNetworkedTask(kvp.Value, kvp.Key); + } + } + + private void CreateNetworkedTask(Task task, ushort netId = 0) + { + if (task == null) + { + Multiplayer.LogError($"NetworkedJob.CreateNetworkedTask(): Task is null for jobId {Job?.ID}"); + return; + } + + NetworkedTask taskObj = new GameObject().AddComponent(); + taskObj.Initialize(task, netId); + taskObj.name = $"{Job.ID}-{taskObj.NetId}"; + taskObj.transform.SetParent(transform); + } + private void OnJobTaken(Job job, bool viaLoadGame) { if (viaLoadGame) @@ -227,7 +282,7 @@ public void AddReport(NetworkedItem item) } Type reportType = item.TrackedItemType; - if( reportType == typeof(JobReport) || + if (reportType == typeof(JobReport) || reportType == typeof(JobExpiredReport) || reportType == typeof(JobMissingLicenseReport) /*|| reportType == typeof(Debtre) ||*/ @@ -273,57 +328,4 @@ protected void OnDisable() Destroy(this); } - public static void EnqueTask(Task task) - { - if (!pendingJobTasks.TryGetValue(task.Job, out var taskList)) - { - taskList = []; - pendingJobTasks[task.Job] = taskList; - } - taskList.Add(task); - } - - private void AddTask(Task task) - { - if (taskMap.Values.Contains(task)) - return; - - var netId = idPool.NextId; - - Multiplayer.LogDebug(() => $"NetworkedJob.AddTask() JobId: [{Job.ID}, {NetId}], TaskType: {task.InstanceTaskType}, taskNetId: {netId}"); - taskMap[netId] = task; - } - - public void AddTask(ushort netId, Task task) - { - if (taskMap.Values.Contains(task)) - return; - - if (taskMap.ContainsKey(netId)) - { - Multiplayer.LogError($"NetworkedJob.AddTask({task.InstanceTaskType}) Attempted to add a task with duplicate netId: {netId} JobId: [{Job.ID}, {NetId}]"); - return; - } - - Multiplayer.LogDebug(() => $"NetworkedJob.AddTask({netId}, {task.InstanceTaskType}) JobId: [{Job.ID}, {NetId}]"); - - taskMap[netId] = task; - } - - public ushort GetTaskNetId(Task task) - { - foreach (var kvp in taskMap) - { - if (kvp.Value == task) - return kvp.Key; - } - return 0; - } - - public Task GetTaskFromNetId(ushort netId) - { - if (taskMap.TryGetValue(netId, out var task)) - return task; - return null; - } } diff --git a/Multiplayer/Components/Networking/Jobs/NetworkedTask.cs b/Multiplayer/Components/Networking/Jobs/NetworkedTask.cs new file mode 100644 index 00000000..77c3fa04 --- /dev/null +++ b/Multiplayer/Components/Networking/Jobs/NetworkedTask.cs @@ -0,0 +1,76 @@ +using DV.Logic.Job; +using System; +using System.Collections.Generic; + +namespace Multiplayer.Components.Networking.Jobs; + +public class NetworkedTask : IdMonoBehaviour +{ + #region Lookup Cache + private static readonly Dictionary taskToNetworkedTask = []; + + public static bool TryGet(ushort netId, out NetworkedTask networkedTask) + { + bool b = Get(netId, out IdMonoBehaviour rawObj); + networkedTask = (NetworkedTask)rawObj; + return b; + } + + public static bool TryGet(ushort netId, out Task task) + { + task = null; + + if (!Get(netId, out IdMonoBehaviour rawObj) || rawObj == null) + return false; + + task = ((NetworkedTask)rawObj).Task; + + return task != null; + } + + public static bool TryGetNetId(Task task, out ushort netId) + { + if (taskToNetworkedTask.TryGetValue(task, out var networkedTask) && networkedTask != null) + { + netId = networkedTask.NetId; + return true; + } + + netId = 0; + return false; + } + #endregion + + protected override bool IsIdServerAuthoritative => true; + + public Task Task { get; private set; } + + public void Initialize(Task task, ushort netId = 0) + { + if (task == null) + { + Multiplayer.LogError($"NetworkedTask.Initialize(): Task is null\r\n{Environment.StackTrace}"); + return; + } + + if (taskToNetworkedTask.ContainsKey(task)) + { + Multiplayer.LogError($"NetworkedTask.Initialize(): Task {task.InstanceTaskType} for jobId {task.Job.ID} is already registered"); + Destroy(this); + return; + } + + Task = task; + taskToNetworkedTask[Task] = this; + if (netId != 0) + NetId = netId; + } + + protected override void OnDestroy() + { + base.OnDestroy(); + + if (Task != null) + taskToNetworkedTask.Remove(Task); + } +} diff --git a/Multiplayer/Components/Networking/World/NetworkedStationController.cs b/Multiplayer/Components/Networking/World/NetworkedStationController.cs index d1dcba12..f3a2c66b 100644 --- a/Multiplayer/Components/Networking/World/NetworkedStationController.cs +++ b/Multiplayer/Components/Networking/World/NetworkedStationController.cs @@ -327,15 +327,7 @@ private void AddJob(JobData jobData) var carNetIds = jobData.GetCars(); - NetworkedJob networkedJob = CreateNetworkedJob(newJob, jobData.NetID, carNetIds); - - foreach(var kvp in netIdToTask) - { - ushort netTaskId = kvp.Key; - Task task = kvp.Value; - networkedJob.AddTask(netTaskId, task); - } - + NetworkedJob networkedJob = CreateNetworkedJob(newJob, jobData.NetID, carNetIds, netIdToTask); NetworkedJobs.Add(networkedJob); if (networkedJob.Job.State == JobState.Available) @@ -374,7 +366,7 @@ private IEnumerator DelayCreateJob(JobData jobData) { int frameCounter = 0; - Multiplayer.LogDebug(()=>$"DelayCreateJob({jobData.NetID}) job type: {jobData.JobType}"); + Multiplayer.LogDebug(() => $"DelayCreateJob([{jobData.NetID}, {jobData.ID}]) job type: {jobData.JobType}"); yield return new WaitForEndOfFrame(); @@ -382,7 +374,7 @@ private IEnumerator DelayCreateJob(JobData jobData) { if (CheckCarsLoaded(jobData)) { - Multiplayer.LogDebug(() => $"DelayCreateJob({jobData.NetID}) job type: {jobData.JobType}. Successfully created cars!"); + Multiplayer.LogDebug(() => $"DelayCreateJob([{jobData.NetID}, {jobData.ID}]) job type: {jobData.JobType}. Successfully created cars!"); AddJob(jobData); yield break; } @@ -391,7 +383,7 @@ private IEnumerator DelayCreateJob(JobData jobData) yield return new WaitForEndOfFrame(); } - Multiplayer.LogWarning($"Timeout waiting for cars to load for job {jobData.NetID}"); + Multiplayer.LogWarning($"Timeout waiting for cars to load for job [{jobData.NetID}, {jobData.ID}]"); } private bool CheckCarsLoaded(JobData jobData) @@ -409,11 +401,12 @@ private bool CheckCarsLoaded(JobData jobData) return true; } - private NetworkedJob CreateNetworkedJob(Job job, ushort netId, List carNetIds) + private NetworkedJob CreateNetworkedJob(Job job, ushort netId, List carNetIds, Dictionary netIdToTask) { NetworkedJob networkedJob = new GameObject($"NetworkedJob {job.ID}").AddComponent(); networkedJob.NetId = netId; networkedJob.Initialize(job, this); + networkedJob.SetTasksFromServer(netIdToTask); networkedJob.OnJobDirty += OnJobDirty; networkedJob.JobCars = carNetIds; return networkedJob; diff --git a/Multiplayer/Networking/Data/JobData.cs b/Multiplayer/Networking/Data/JobData.cs index d7a8f523..209a3897 100644 --- a/Multiplayer/Networking/Data/JobData.cs +++ b/Multiplayer/Networking/Data/JobData.cs @@ -1,13 +1,13 @@ -using System; -using System.Linq; -using System.IO; -using System.Collections.Generic; -using Multiplayer.Components.Networking.World; -using Multiplayer.Components.Networking.Jobs; -using MPAPI.Types; -using LiteNetLib.Utils; -using DV.ThingTypes; using DV.Logic.Job; +using DV.ThingTypes; +using LiteNetLib.Utils; +using MPAPI.Types; +using Multiplayer.Components.Networking.Jobs; +using Multiplayer.Components.Networking.World; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System; namespace Multiplayer.Networking.Data; @@ -52,7 +52,7 @@ public static JobData FromJob(NetworkedStationController netStation, NetworkedJo itemPos = ItemPositionData.FromItem(networkedJob.JobBooklet); } } - else if(job.State == JobState.Completed) + else if (job.State == JobState.Completed) { if (networkedJob.JobReport != null) { @@ -94,7 +94,7 @@ public static (Job newJob, Dictionary netIdToTask) ToJob(JobData j State = jobData.State, }; - return new (newJob, netIdToTask); + return new(newJob, netIdToTask); } public static void Serialize(NetDataWriter writer, JobData data) @@ -132,7 +132,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); } @@ -150,12 +150,15 @@ public static void Serialize(NetDataWriter writer, JobData data) public static JobData Deserialize(NetDataReader reader) { + ushort netID = 0; + JobType jobType = JobType.Custom; + string id = string.Empty; + try { - - ushort netID = reader.GetUShort(); - JobType jobType = (JobType)reader.GetByte(); - string id = reader.GetString(); + netID = reader.GetUShort(); + jobType = (JobType)reader.GetByte(); + id = reader.GetString(); //Decompress task data byte[] compressedData = reader.GetBytesWithLength(); @@ -216,7 +219,7 @@ public static JobData Deserialize(NetDataReader reader) } catch (Exception ex) { - Multiplayer.Log($"JobData.Deserialize() Failed! {ex.Message}\r\n{ex.StackTrace}"); + Multiplayer.Log($"JobData.Deserialize() Failed! netId: {netID}, jobId: {id} {ex.Message}\r\n{ex.StackTrace}"); return null; } } diff --git a/Multiplayer/Networking/Data/TaskNetworkData.cs b/Multiplayer/Networking/Data/TaskNetworkData.cs index 2cce880b..828be086 100644 --- a/Multiplayer/Networking/Data/TaskNetworkData.cs +++ b/Multiplayer/Networking/Data/TaskNetworkData.cs @@ -59,15 +59,10 @@ public static TaskNetworkData ConvertTask(Task task) { var taskData = converter(task); - if (NetworkedJob.TryGetFromJob(task.Job, out var netJob) && netJob != null) - { - var taskNetId = netJob.GetTaskNetId(task); + if (NetworkedTask.TryGetNetId(task, out var taskNetId) && taskNetId != 0) taskData.TaskNetId = taskNetId; - } else - { Multiplayer.LogError($"TaskNetworkDataFactory.ConvertTask: Could not find NetworkedJob for jobId: {task.Job.ID}, taskType: {task.InstanceTaskType}"); - } return taskData; } diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 1a11dd73..4d3649de 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -1029,17 +1029,9 @@ private void OnClientboundTaskUpdatePacket(ClientboundTaskUpdatePacket packet) if (NetworkLifecycle.Instance.IsHost()) return; - if (!NetworkedJob.Get(packet.JobNetId, out NetworkedJob networkedJob) || networkedJob == null) + if (!NetworkedTask.TryGet(packet.TaskNetId, out Task task) || task == null) { - LogError($"Received task update for jobNetId {packet.JobNetId}, but job was not found!"); - return; - } - - var task = networkedJob.GetTaskFromNetId(packet.TaskNetId); - - if (task == null) - { - LogError($"Received task update for job [{networkedJob.Job.ID}, {packet.JobNetId}], with taskNetId {packet.TaskNetId}, task was not found"); + LogError($"Received task update for taskNetId {packet.TaskNetId}, task was not found"); return; } diff --git a/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobsCreatePacket.cs b/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobsCreatePacket.cs index 29bad650..7598ac52 100644 --- a/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobsCreatePacket.cs +++ b/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobsCreatePacket.cs @@ -16,7 +16,6 @@ public static ClientboundJobsCreatePacket FromNetworkedJobs(NetworkedStationCont foreach (var job in jobs) { JobData jd = JobData.FromJob(netStation, job); - Multiplayer.Log($"JobData: jobNetId: {jd.NetID}, jobId: {jd.ID}, itemNetId {jd.ItemNetID}"); jobData.Add(jd); } diff --git a/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundTaskUpdatePacket.cs b/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundTaskUpdatePacket.cs index ce8b7f49..abf7ff5d 100644 --- a/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundTaskUpdatePacket.cs +++ b/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundTaskUpdatePacket.cs @@ -4,7 +4,6 @@ namespace Multiplayer.Networking.Packets.Clientbound.Jobs; internal class ClientboundTaskUpdatePacket { - public ushort JobNetId { get; set; } public ushort TaskNetId { get; set; } public TaskState NewState { get; set; } public float TaskStartTime { get; set; } diff --git a/Multiplayer/Patches/Jobs/TaskPatch.cs b/Multiplayer/Patches/Jobs/TaskPatch.cs index ab396ec9..454466e3 100644 --- a/Multiplayer/Patches/Jobs/TaskPatch.cs +++ b/Multiplayer/Patches/Jobs/TaskPatch.cs @@ -2,6 +2,7 @@ using HarmonyLib; using Multiplayer.Components.Networking; using Multiplayer.Components.Networking.Jobs; +using System; namespace Multiplayer.Patches.Jobs; @@ -18,27 +19,25 @@ public static void SetStatePrefix(Task __instance, TaskState newState) if (newState == TaskState.InProgress) return; - if (NetworkedJob.TryGetFromJob(__instance.Job, out var netJob) && netJob != null) + + Multiplayer.LogDebug(()=>$"Task.SetState() called for jobId: {__instance.Job.ID}, taskType: {__instance.InstanceTaskType}, newState: {newState}"); + + if (!NetworkedTask.TryGetNetId(__instance, out var taskNetId) ||taskNetId == 0) { - var taskNetId = netJob.GetTaskNetId(__instance); - - if (taskNetId == 0) - { - Multiplayer.LogError($"Task.SetState() could not find task index for jobId: {__instance.Job.ID}, taskType: {__instance.InstanceTaskType}"); - return; - } - NetworkLifecycle.Instance.Server.SendTaskUpdate(netJob.NetId, taskNetId, newState, __instance.taskStartTime, __instance.taskFinishTime); + Multiplayer.LogError($"Task.SetState() could not find task index for jobId: {__instance.Job.ID}, taskType: {__instance.InstanceTaskType}"); + return; } - + + NetworkLifecycle.Instance.Server.SendTaskUpdate(taskNetId, newState, __instance.taskStartTime, __instance.taskFinishTime); } [HarmonyPatch(nameof(Task.SetJobBelonging))] [HarmonyPostfix] - public static void SetJobBelongingPostfix(Task __instance) + public static void SetJobBelongingPostfix(Task __instance, Job Job) { if (!NetworkLifecycle.Instance.IsHost()) return; - NetworkedJob.EnqueTask(__instance); + NetworkedJob.EnqueueTask(__instance, Job); } } diff --git a/MultiplayerAPI/Interfaces/IMultiplayerAPI.cs b/MultiplayerAPI/Interfaces/IMultiplayerAPI.cs index f83c8ea8..1755d61c 100644 --- a/MultiplayerAPI/Interfaces/IMultiplayerAPI.cs +++ b/MultiplayerAPI/Interfaces/IMultiplayerAPI.cs @@ -135,5 +135,4 @@ bool RegisterTaskType(TaskType taskType, Func bool UnregisterTaskType(TaskType taskType) where TGameTask : Task; - } diff --git a/MultiplayerAPI/MultiplayerAPI.cs b/MultiplayerAPI/MultiplayerAPI.cs index 8aac1b06..b2fe7cc1 100644 --- a/MultiplayerAPI/MultiplayerAPI.cs +++ b/MultiplayerAPI/MultiplayerAPI.cs @@ -137,7 +137,6 @@ internal static void RegisterServer(IServer server) ServerStarted?.Invoke(server); } - /// /// Internal method for the Multiplayer mod to deregister a server instance /// diff --git a/MultiplayerAPI/Types/TaskNetworkType.cs b/MultiplayerAPI/Types/TaskNetworkType.cs index e8d3ce32..d8a4f33b 100644 --- a/MultiplayerAPI/Types/TaskNetworkType.cs +++ b/MultiplayerAPI/Types/TaskNetworkType.cs @@ -73,7 +73,7 @@ public abstract class TaskNetworkData /// compatible with the job/task system, and adds them to the provided dictionary. /// /// - /// A reference to a that will be populated with deserialized instances. + /// A reference to a that will be populated with deserialized instances. /// Each key is a netTaskId (ushort), and each value is the corresponding object. /// /// A instance representing the deserialized data. From e42d201b2f1a514ad28509d15563604a968e906b Mon Sep 17 00:00:00 2001 From: Macka Date: Sun, 12 Oct 2025 10:17:48 +1000 Subject: [PATCH 459/521] Add WarehouseMachineLookup for improved net ID handling Introduces WarehouseMachineLookup to manage WarehouseMachine net IDs, replacing previous NetworkedWarehouseMachineController references. Updates NetIdProvider, NetworkClient, and NetworkServer to use the new lookup. Adds a Harmony patch to auto-register WarehouseMachines when their ID is set, improving reliability and maintainability of warehouse machine networking. --- Multiplayer/API/NetIdProvider.cs | 3 +- .../Networking/Jobs/WarehouseMachineLookup.cs | 91 +++++++++++++++++++ .../Managers/Client/NetworkClient.cs | 4 +- .../Managers/Server/NetworkServer.cs | 25 +++-- .../Patches/Jobs/WarehouseMachinePatch.cs | 17 ++++ 5 files changed, 124 insertions(+), 16 deletions(-) create mode 100644 Multiplayer/Components/Networking/Jobs/WarehouseMachineLookup.cs create mode 100644 Multiplayer/Patches/Jobs/WarehouseMachinePatch.cs diff --git a/Multiplayer/API/NetIdProvider.cs b/Multiplayer/API/NetIdProvider.cs index 9feb02dd..bfa73fce 100644 --- a/Multiplayer/API/NetIdProvider.cs +++ b/Multiplayer/API/NetIdProvider.cs @@ -30,7 +30,8 @@ protected override void Awake() RegisterHandler(NetworkedStationController.TryGetNetId, NetworkedStationController.TryGet); RegisterHandler(NetworkedStationController.TryGetNetId, NetworkedStationController.TryGet); RegisterHandler(NetworkedStationController.TryGetNetId, NetworkedStationController.TryGet); - RegisterHandler(NetworkedWarehouseMachineController.TryGetNetId, NetworkedWarehouseMachineController.TryGet); + + RegisterHandler(WarehouseMachineLookup.TryGetNetId, WarehouseMachineLookup.TryGet); RegisterHandler(NetworkedWarehouseMachineController.TryGetNetId, NetworkedWarehouseMachineController.TryGet); RegisterHandler(NetworkedJob.TryGetNetId, NetworkedJob.TryGetJob); diff --git a/Multiplayer/Components/Networking/Jobs/WarehouseMachineLookup.cs b/Multiplayer/Components/Networking/Jobs/WarehouseMachineLookup.cs new file mode 100644 index 00000000..a1910a84 --- /dev/null +++ b/Multiplayer/Components/Networking/Jobs/WarehouseMachineLookup.cs @@ -0,0 +1,91 @@ +using DV.Logic.Job; +using DV.Utils; +using JetBrains.Annotations; +using System.Collections.Generic; + +namespace Multiplayer.Components.Networking.Jobs; + +public class WarehouseMachineLookup : SingletonBehaviour +{ + private static readonly Dictionary netIdToWarehouseMachine = []; + + public void RegisterWarehouseMachine(WarehouseMachine machine) + { + Multiplayer.LogDebug(() => $"RegisterWarehouseMachine() {machine.WarehouseTrack.ID}, machineID: {machine.ID}"); + + if (machine == null) + return; + + if (string.IsNullOrEmpty(machine.ID)) + { + Multiplayer.LogDebug(() => $"Attempted to register WarehouseMachine with null or empty ID for track {machine.WarehouseTrack.ID}"); + return; + } + + ushort netId = GenerateNetId(machine.ID); + + if (netIdToWarehouseMachine.ContainsKey(netId)) + { + var existing = netIdToWarehouseMachine[netId]; + Multiplayer.LogWarning(() => $"Registering WarehouseMachine for track {machine.WarehouseTrack.ID}, machineID: {machine.ID} failed! More than one WarehouseMachine with the same ID!"); + return; + } + + Multiplayer.LogDebug(() => $"Registered WarehouseMachine for track {machine.WarehouseTrack.ID}, machineID: {machine.ID}, netId: {netId}"); + netIdToWarehouseMachine[netId] = machine; + } + + public static bool TryGet(ushort netId, out WarehouseMachine machine) + { + var result = netIdToWarehouseMachine.TryGetValue(netId, out machine); + + if (result && machine == null) + { + netIdToWarehouseMachine.Remove(netId); + return false; + } + + return result; + } + + public static bool TryGetNetId(WarehouseMachine machine, out ushort netId) + { + //Multiplayer.LogDebug(() => $"Trying to get NetID for WarehouseMachine on track {machine?.WarehouseTrack?.ID}, machineID: {machine?.ID}"); + + if (machine != null && !string.IsNullOrEmpty(machine.ID)) + { + netId = GenerateNetId(machine.ID); + var temp = netId; + Multiplayer.LogDebug(() => $"Trying to get NetID for WarehouseMachine on track {machine?.WarehouseTrack?.ID}, machineID: {machine?.ID}, netId: {temp}"); + + if (netIdToWarehouseMachine.ContainsKey(netId)) + return true; + } + + netId = 0; + return false; + } + + private static ushort GenerateNetId(string id) + { + unchecked + { + int hash = id.GetHashCode(); + ushort result = (ushort)((hash & 0xFFFF) ^ ((hash >> 16) & 0xFFFF)); + return result == 0 ? (ushort)1 : result; + } + } + + [UsedImplicitly] + public new static string AllowAutoCreate() + { + return $"[{nameof(WarehouseMachineLookup)}]"; + } + + protected override void Awake() + { + base.Awake(); + netIdToWarehouseMachine.Clear(); + Multiplayer.LogDebug(() => $"{nameof(WarehouseMachineLookup)} Awake, cleared existing lookup dictionary."); + } +} diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 4d3649de..e28184a8 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -181,7 +181,7 @@ protected override void Subscribe() netPacketProcessor.SubscribeNetSerializable(OnCommonItemChangePacket); } - //allow mods to register their own packets + // Allow mods to register their own packets public void RegisterExternalPacket(ClientPacketHandler handler) where T : class, IPacket, new() { netPacketProcessor.SubscribeReusable((packet) => @@ -826,7 +826,7 @@ private void OnClientboundCargoStatePacket(ClientboundCargoStatePacket packet) float cargoAmount = Mathf.Clamp(packet.CargoAmount, 0, logicCar.capacity); WarehouseMachine warehouseMachine = null; - if (packet.WarehouseMachineNetId != 0 && (!NetworkedWarehouseMachineController.TryGet(packet.WarehouseMachineNetId, out warehouseMachine) || warehouseMachine == null)) + if (packet.WarehouseMachineNetId != 0 && (!WarehouseMachineLookup.TryGet(packet.WarehouseMachineNetId, out warehouseMachine) || warehouseMachine == null)) { LogWarning($"OnClientboundCargoStatePacket() Failed to find WarehouseMachine for netId {packet.WarehouseMachineNetId}"); return; diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index 3216c79b..10e533fb 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -533,6 +533,8 @@ public void SendCargoState(NetworkedTrainCar netTraincar, bool isLoading, byte c { Car logicCar = netTraincar?.TrainCar?.logicCar; + LogDebug(() => $"SendCargoState({netTraincar?.CurrentID}, isLoading: {isLoading}, cargoModelIndex: {cargoModelIndex}), logicCar: {logicCar?.ID}, WareHouseMachineID: {logicCar.CargoOriginWarehouse?.ID}, warehouse track: {logicCar.CargoOriginWarehouse?.WarehouseTrack?.ID}"); + if (logicCar == null) { LogWarning($"Attempted to send cargo state for {netTraincar?.CurrentID}, but logic car does not exist!"); @@ -544,15 +546,13 @@ public void SendCargoState(NetworkedTrainCar netTraincar, bool isLoading, byte c ushort netMachineId = 0; if (logicCar.CargoOriginWarehouse != null) { - if (!NetworkedWarehouseMachineController.TryGetNetId(logicCar.CargoOriginWarehouse, out netMachineId)) + if (!WarehouseMachineLookup.TryGetNetId(logicCar.CargoOriginWarehouse, out netMachineId)) { - Log($"Attempting to send cargo state for {netTraincar.CurrentID}, for warehouse machine at track {logicCar.CargoOriginWarehouse.WarehouseTrack.ID}, but Warehouse Machine Controller was not found"); + Log($"Attempting to send cargo state for {netTraincar.CurrentID}, for warehouse machine at track {logicCar.CargoOriginWarehouse?.WarehouseTrack?.ID}, but Warehouse Machine was not found"); return; } } - - SendPacketToAll(new ClientboundCargoStatePacket { NetId = netTraincar.NetId, @@ -567,7 +567,7 @@ public void SendCargoState(NetworkedTrainCar netTraincar, bool isLoading, byte c public void SendWarehouseControllerUpdate(ushort netId, bool isLoading, ushort jobNetId, ushort carNetId, CargoType cargoType, WarehouseMachineController.TextPreset preset) { - LogDebug(() =>$"SendWarehouseControllerUpdate({netId}, {isLoading}, {jobNetId}, {carNetId}, {cargoType}, {preset})"); + LogDebug(() => $"SendWarehouseControllerUpdate({netId}, {isLoading}, {jobNetId}, {carNetId}, {cargoType}, {preset})"); SendPacketToAll(new ClientboundWarehouseControllerUpdatePacket() { @@ -703,12 +703,11 @@ public void SendJobsUpdatePacket(ushort stationNetId, NetworkedJob[] jobs) SendPacketToAll(ClientboundJobsUpdatePacket.FromNetworkedJobs(stationNetId, jobs), DeliveryMethod.ReliableOrdered, SelfPeer); } - public void SendTaskUpdate(ushort netId, ushort taskNetId, TaskState newState, float taskStartTime, float taskFinishTime) + public void SendTaskUpdate(ushort taskNetId, TaskState newState, float taskStartTime, float taskFinishTime) { - Multiplayer.Log($"Sending TaskUpdate for jobNetId {netId}, taskNetId {taskNetId}, newState {newState}"); + Multiplayer.Log($"Sending TaskUpdate for taskNetId {taskNetId}, newState {newState}"); SendPacketToAll(new ClientboundTaskUpdatePacket { - JobNetId = netId, TaskNetId = taskNetId, NewState = newState, TaskStartTime = taskStartTime, @@ -1039,7 +1038,7 @@ private void OnServerboundPlayerPositionPacket(ServerboundPlayerPositionPacket p LogWarning($"Received Player Position from {peer.GetType()}, peerId: {peer.Id}, but could not find matching player."); return; } - + player.CarId = packet.CarID; player.RawPosition = packet.Position; player.RawRotationY = packet.RotationY; @@ -1114,7 +1113,7 @@ private void OnCommonCouplerInteractionPacket(CommonCouplerInteractionPacket pac SendDestroyTrainCar(netTrainCar, peer); } } - + //private void OnCommonTrainCouplePacket(CommonTrainCouplePacket packet, ITransportPeer peer) //{ // SendPacketToAll(packet, DeliveryMethod.ReliableUnordered, peer); @@ -1375,7 +1374,7 @@ private void OnServerboundJobValidateRequestPacket(ServerboundJobValidateRequest private void OnServerboundWarehouseMachineControllerRequestPacket(ServerboundWarehouseMachineControllerRequestPacket packet, ITransportPeer peer) { - LogDebug(()=>$"ServerboundWarehouseMachineControllerRequestPacket(): {packet.NetId}"); + LogDebug(() => $"ServerboundWarehouseMachineControllerRequestPacket(): {packet.NetId}"); if (!TryGetServerPlayer(peer, out ServerPlayer player)) { @@ -1386,10 +1385,10 @@ private void OnServerboundWarehouseMachineControllerRequestPacket(ServerboundWar //Todo: add check for player authorisation to use loading/uloading machines //Find the warehouse - if(!NetworkedWarehouseMachineController.Get(packet.NetId, out var targetWarehouse)) + if (!NetworkedWarehouseMachineController.Get(packet.NetId, out var targetWarehouse)) { LogWarning($"ServerboundWarehouseMachineControllerRequestPacket() WarehouseMachineController not found. NetId: {packet.NetId}"); - return; + return; } //Todo: add check for player distance from machine diff --git a/Multiplayer/Patches/Jobs/WarehouseMachinePatch.cs b/Multiplayer/Patches/Jobs/WarehouseMachinePatch.cs new file mode 100644 index 00000000..14a115c1 --- /dev/null +++ b/Multiplayer/Patches/Jobs/WarehouseMachinePatch.cs @@ -0,0 +1,17 @@ +using DV.Logic.Job; +using HarmonyLib; +using Multiplayer.Components.Networking.Jobs; + +namespace Multiplayer.Patches.Jobs; + +[HarmonyPatch(typeof(WarehouseMachine))] +public class WarehouseMachinePatch +{ + [HarmonyPatch(nameof(WarehouseMachine.ID))] + [HarmonyPatch(MethodType.Setter)] + [HarmonyPostfix] + public static void ID_Set(WarehouseMachine __instance) + { + WarehouseMachineLookup.Instance.RegisterWarehouseMachine( __instance ); + } +} From 45c5b68b47f2b5790f4bb6b74c13f7350d9f0dad Mon Sep 17 00:00:00 2001 From: Macka Date: Sat, 25 Oct 2025 09:22:26 +1000 Subject: [PATCH 460/521] Fix password check in server browser --- 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 6000d18e..c85eb64a 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs @@ -1013,7 +1013,7 @@ private async Task JoinLobby(Lobby lobby) string hasPass = lobby.GetData(SteamworksUtils.LOBBY_HAS_PASSWORD); Multiplayer.Log($"Lobby ({lobby.Id}) has password: {hasPass}"); - if (string.IsNullOrEmpty(hasPass)) + if (string.IsNullOrEmpty(hasPass) || hasPass == "False") { Multiplayer.Log($"Attempting connection..."); InitiateConnection(); From 19f828491d87440d01ccbb9468cc72cdadb7bc09 Mon Sep 17 00:00:00 2001 From: Macka Date: Sat, 25 Oct 2025 09:34:46 +1000 Subject: [PATCH 461/521] Stop sync of TrainCar rotation while railed Disabled the MoveRotation call on TrainCar's Rigidbody due to motion sickness issues. --- Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs index a5553f63..6d95b396 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs @@ -1293,7 +1293,7 @@ public void Client_ReceiveTrainPhysicsUpdate(in TrainsetMovementPart movementPar if (TrainCar.rb != null) { TrainCar.rb.MovePosition(worldPos); - TrainCar.rb.MoveRotation(movementPart.Rotation); + //TrainCar.rb.MoveRotation(movementPart.Rotation); // removed due to motion sickness issues } //clear the queues? From 3c673835bb8c54381a07c4592ae58863ffb85cff Mon Sep 17 00:00:00 2001 From: Macka Date: Sat, 25 Oct 2025 09:36:17 +1000 Subject: [PATCH 462/521] Refactor for future dedicated server Remove reliance on a local client --- .../Networking/Train/NetworkedTrainCar.cs | 8 ++-- .../Managers/Server/NetworkServer.cs | 40 +++++++++++++++++++ 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs index 6d95b396..a3478539 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs @@ -519,15 +519,15 @@ private void Server_SendCouplers() if (!TrainCar.frontCoupler.hoseAndCock.IsHoseConnected) // NetworkLifecycle.Instance.Client.SendHoseConnected(TrainCar.frontCoupler, TrainCar.frontCoupler.coupledTo, false); //else - NetworkLifecycle.Instance.Client.SendHoseDisconnected(TrainCar.frontCoupler, true); + NetworkLifecycle.Instance.Server.SendHoseDisconnected(TrainCar.frontCoupler, true); if (!TrainCar.rearCoupler.hoseAndCock.IsHoseConnected) // NetworkLifecycle.Instance.Client.SendHoseConnected(TrainCar.rearCoupler, TrainCar.rearCoupler.coupledTo, false); //else - NetworkLifecycle.Instance.Client.SendHoseDisconnected(TrainCar.rearCoupler, true); + NetworkLifecycle.Instance.Server.SendHoseDisconnected(TrainCar.rearCoupler, true); - NetworkLifecycle.Instance.Client.SendCockState(NetId, TrainCar.frontCoupler, TrainCar.frontCoupler.IsCockOpen); - NetworkLifecycle.Instance.Client.SendCockState(NetId, TrainCar.rearCoupler, TrainCar.rearCoupler.IsCockOpen); + NetworkLifecycle.Instance.Server.SendCockState(NetId, TrainCar.frontCoupler, TrainCar.frontCoupler.IsCockOpen); + NetworkLifecycle.Instance.Server.SendCockState(NetId, TrainCar.rearCoupler, TrainCar.rearCoupler.IsCockOpen); } private void Server_SendCables() { diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index 10e533fb..e49b345d 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -685,6 +685,46 @@ public void SendTrainUncouple(Coupler coupler, bool playAudio, bool dueToBrokenC ); } + public void SendHoseDisconnected(Coupler coupler, bool playAudio) + { + ushort couplerNetId = coupler.train.GetNetId(); + + if (couplerNetId == 0) + { + LogWarning($"SendHoseDisconnected failed. Coupler: {coupler.name} {couplerNetId}"); + return; + } + + LogDebug(() => $"SendHoseDisconnected({coupler.train.ID}, {coupler.isFrontCoupler}, {playAudio})"); + + SendPacketToAll + ( + new CommonHoseDisconnectedPacket + { + NetId = couplerNetId, + IsFront = coupler.isFrontCoupler, + PlayAudio = playAudio + }, + DeliveryMethod.ReliableOrdered, + excludeSelf: true + ); + } + + public void SendCockState(ushort netId, Coupler coupler, bool isOpen) + { + SendPacketToAll + ( + new CommonCockFiddlePacket + { + NetId = netId, + IsFront = coupler.isFrontCoupler, + IsOpen = isOpen + }, + DeliveryMethod.ReliableOrdered, + true + ); + } + public void SendJobsCreatePacket(NetworkedStationController networkedStation, NetworkedJob[] jobs, ITransportPeer peer = null) { Multiplayer.Log($"Sending JobsCreatePacket for stationNetId {networkedStation.NetId} with {jobs.Count()} jobs"); From bf50aa69aecd9d0c07c88f671f0ec24764042787 Mon Sep 17 00:00:00 2001 From: Macka Date: Sat, 25 Oct 2025 10:14:56 +1000 Subject: [PATCH 463/521] Add excludeSelf option to packet broadcast methods Introduces an excludeSelf parameter to SendPacketToAll and SendSerializablePacketToAll methods in IServer and their implementations, allowing control over whether packets are sent to the local client. Updates usages and documentation to reflect the new parameter, improving flexibility for server-side packet broadcasting. --- Multiplayer/API/ServerAPIProvider.cs | 8 +- .../Managers/Server/NetworkServer.cs | 198 +++++++++++------- .../TestComponents/ServerTest.cs | 10 +- MultiplayerAPI/Interfaces/IServer.cs | 6 +- 4 files changed, 134 insertions(+), 88 deletions(-) diff --git a/Multiplayer/API/ServerAPIProvider.cs b/Multiplayer/API/ServerAPIProvider.cs index 4bf1326a..cc4580f6 100644 --- a/Multiplayer/API/ServerAPIProvider.cs +++ b/Multiplayer/API/ServerAPIProvider.cs @@ -45,24 +45,24 @@ public IPlayer GetPlayer(byte PlayerId) } - public void SendPacketToAll(T packet, bool reliable = true, IPlayer excludePlayer = null) where T : class, IPacket, new() + public void SendPacketToAll(T packet, bool reliable = true, bool excludeSelf = false, IPlayer excludePlayer = null) where T : class, IPacket, new() { ITransportPeer peer = null; if (excludePlayer != null) peer = GetPeerFromPlayer(excludePlayer, $"SendPacketToAll<{typeof(T).Name}>"); - server.SendExternalPacketToAll(packet, reliable, peer); + server.SendExternalPacketToAll(packet, reliable, peer, excludeSelf); } - public void SendSerializablePacketToAll(T packet, bool reliable = true, IPlayer excludePlayer = null) where T : class, ISerializablePacket, new() + public void SendSerializablePacketToAll(T packet, bool reliable = true, bool excludeSelf = false, IPlayer excludePlayer = null) where T : class, ISerializablePacket, new() { ITransportPeer peer = null; if(excludePlayer != null) peer = GetPeerFromPlayer(excludePlayer, $"SendSerializablePacketToAll<{typeof(T).Name}>"); - server.SendExternalSerializablePacketToAll(packet, reliable, peer); + server.SendExternalSerializablePacketToAll(packet, reliable, peer, excludeSelf); } public void SendPacketToPlayer(T packet, IPlayer player, bool reliable = true) where T : class, IPacket, new() diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index e49b345d..ed35118f 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -331,75 +331,86 @@ public override void OnConnectionRequest(NetDataReader requestData, IConnectionR #region Packet Senders - private void SendPacketToAll(T packet, DeliveryMethod deliveryMethod) where T : class, new() + private void SendPacketToAll(T packet, DeliveryMethod deliveryMethod, bool excludeSelf = false) where T : class, new() { NetDataWriter writer = WritePacket(packet); foreach (var peer in peers.Values) + { + if (excludeSelf && peer == SelfPeer) + continue; + peer?.Send(writer, deliveryMethod); + } } - private void SendPacketToAll(T packet, DeliveryMethod deliveryMethod, ITransportPeer excludePeer) where T : class, new() + private void SendPacketToAll(T packet, DeliveryMethod deliveryMethod, ITransportPeer excludePeer, bool excludeSelf = false) where T : class, new() { NetDataWriter writer = WritePacket(packet); foreach (var peer in peers.Values) { - if (peer == excludePeer) + if (peer == excludePeer || (excludeSelf && peer == SelfPeer)) continue; + peer?.Send(writer, deliveryMethod); } } - private void SendNetSerializablePacketToAll(T packet, DeliveryMethod deliveryMethod) where T : INetSerializable, new() + private void SendNetSerializablePacketToAll(T packet, DeliveryMethod deliveryMethod, bool excludeSelf = false) where T : INetSerializable, new() { NetDataWriter writer = WriteNetSerializablePacket(packet); foreach (var peer in peers.Values) + { + if (excludeSelf && peer == SelfPeer) + continue; + peer?.Send(writer, deliveryMethod); + } } - private void SendNetSerializablePacketToAll(T packet, DeliveryMethod deliveryMethod, ITransportPeer excludePeer) where T : INetSerializable, new() + private void SendNetSerializablePacketToAll(T packet, DeliveryMethod deliveryMethod, ITransportPeer excludePeer, bool excludeSelf = false) where T : INetSerializable, new() { NetDataWriter writer = WriteNetSerializablePacket(packet); foreach (var peer in peers.Values) { - if (peer == excludePeer) + if (peer == excludePeer || (excludeSelf && peer == SelfPeer)) continue; peer?.Send(writer, deliveryMethod); } } #region Mod Packets - public void SendExternalPacketToAll(T packet, bool reliable) where T : class, IPacket, new() + public void SendExternalPacketToAll(T packet, bool reliable, bool excludeSelf = false) where T : class, IPacket, new() { var deliveryMethod = reliable ? DeliveryMethod.ReliableUnordered : DeliveryMethod.Unreliable; - SendPacketToAll(packet, deliveryMethod); + SendPacketToAll(packet, deliveryMethod, excludeSelf); } - public void SendExternalPacketToAll(T packet, bool reliable, ITransportPeer excludePeer) where T : class, IPacket, new() + public void SendExternalPacketToAll(T packet, bool reliable, ITransportPeer excludePeer, bool excludeSelf = false) where T : class, IPacket, new() { var deliveryMethod = reliable ? DeliveryMethod.ReliableUnordered : DeliveryMethod.Unreliable; if (excludePeer == null) - SendPacketToAll(packet, deliveryMethod); + SendPacketToAll(packet, deliveryMethod, excludeSelf); else - SendPacketToAll(packet, deliveryMethod, excludePeer); + SendPacketToAll(packet, deliveryMethod, excludePeer, excludeSelf); } - public void SendExternalSerializablePacketToAll(T packet, bool reliable) where T : class, ISerializablePacket, new() + public void SendExternalSerializablePacketToAll(T packet, bool reliable, bool excludeSelf = false) where T : class, ISerializablePacket, new() { var deliveryMethod = reliable ? DeliveryMethod.ReliableUnordered : DeliveryMethod.Unreliable; var wrapper = new ExternalSerializablePacketWrapper { Packet = packet }; - SendNetSerializablePacketToAll(wrapper, deliveryMethod); + SendNetSerializablePacketToAll(wrapper, deliveryMethod, excludeSelf); } - public void SendExternalSerializablePacketToAll(T packet, bool reliable, ITransportPeer excludePeer) where T : class, ISerializablePacket, new() + public void SendExternalSerializablePacketToAll(T packet, bool reliable, ITransportPeer excludePeer, bool excludeSelf = false) where T : class, ISerializablePacket, new() { var deliveryMethod = reliable ? DeliveryMethod.ReliableUnordered : DeliveryMethod.Unreliable; var wrapper = new ExternalSerializablePacketWrapper { Packet = packet }; if (excludePeer == null) - SendNetSerializablePacketToAll(wrapper, deliveryMethod); + SendNetSerializablePacketToAll(wrapper, deliveryMethod, excludeSelf); else - SendNetSerializablePacketToAll(wrapper, deliveryMethod, excludePeer); + SendNetSerializablePacketToAll(wrapper, deliveryMethod, excludePeer, excludeSelf); } public void SendExternalPacketToPlayer(T packet, ITransportPeer peer, bool reliable) where T : class, IPacket, new() @@ -427,7 +438,7 @@ public void KickPlayer(ServerPlayer player) } public void SendGameParams(GameParams gameParams) { - SendPacketToAll(ClientboundGameParamsPacket.FromGameParams(gameParams), DeliveryMethod.ReliableOrdered, SelfPeer); + SendPacketToAll(ClientboundGameParamsPacket.FromGameParams(gameParams), DeliveryMethod.ReliableOrdered, excludeSelf: true); } public void SendWeatherState(ITransportPeer peer = null) @@ -437,7 +448,7 @@ public void SendWeatherState(ITransportPeer peer = null) if (peer != null) SendPacket(peer, packet, DeliveryMethod.ReliableOrdered); else - SendPacketToAll(packet, DeliveryMethod.ReliableOrdered, SelfPeer); + SendPacketToAll(packet, DeliveryMethod.ReliableOrdered, excludeSelf: true); } public void SendSpawnTrainset(List set, bool autoCouple, bool sendToAll, ITransportPeer sendTo = null) @@ -473,7 +484,7 @@ public void SendSpawnTrainset(List set, bool autoCouple, bool sendToAl public void SendSpawnTrainCar(NetworkedTrainCar networkedTrainCar) { - SendPacketToAll(ClientboundSpawnTrainCarPacket.FromTrainCar(networkedTrainCar), DeliveryMethod.ReliableOrdered, SelfPeer); + SendPacketToAll(ClientboundSpawnTrainCarPacket.FromTrainCar(networkedTrainCar), DeliveryMethod.ReliableOrdered, excludeSelf: true); } public void SendDestroyTrainCar(NetworkedTrainCar netTrainCar, ITransportPeer peer = null) @@ -612,7 +623,9 @@ 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 @@ -621,35 +634,55 @@ public void SendWindowsBroken(ushort netId, Vector3 forceDirection) public void SendWindowsRepaired(ushort netId) { - SendPacketToAll(new ClientboundWindowsRepairedPacket - { - NetId = netId - }, DeliveryMethod.ReliableUnordered, SelfPeer); + SendPacketToAll + ( + new ClientboundWindowsRepairedPacket + { + NetId = netId + }, + DeliveryMethod.ReliableUnordered, + excludeSelf: true + ); } public void SendMoney(float amount) { - SendPacketToAll(new ClientboundMoneyPacket - { - Amount = amount - }, DeliveryMethod.ReliableUnordered, SelfPeer); + SendPacketToAll + ( + new ClientboundMoneyPacket + { + Amount = amount + }, + DeliveryMethod.ReliableUnordered, + excludeSelf: true + ); } public void SendLicense(string id, bool isJobLicense) { - SendPacketToAll(new ClientboundLicenseAcquiredPacket - { - Id = id, - IsJobLicense = isJobLicense - }, DeliveryMethod.ReliableUnordered, SelfPeer); + SendPacketToAll + ( + new ClientboundLicenseAcquiredPacket + { + Id = id, + IsJobLicense = isJobLicense + }, + DeliveryMethod.ReliableUnordered, + excludeSelf: true + ); } public void SendGarage(string id) { - SendPacketToAll(new ClientboundGarageUnlockPacket - { - Id = id - }, DeliveryMethod.ReliableUnordered, SelfPeer); + SendPacketToAll + ( + new ClientboundGarageUnlockPacket + { + Id = id + }, + DeliveryMethod.ReliableUnordered, + excludeSelf: true + ); } public void SendDebtStatus(bool hasDebt) @@ -681,7 +714,8 @@ public void SendTrainUncouple(Coupler coupler, bool playAudio, bool dueToBrokenC ViaChainInteraction = viaChainInteraction, DueToBrokenCouple = dueToBrokenCouple, }, - DeliveryMethod.ReliableOrdered + DeliveryMethod.ReliableOrdered, + excludeSelf: true ); } @@ -732,7 +766,7 @@ public void SendJobsCreatePacket(NetworkedStationController networkedStation, Ne var packet = ClientboundJobsCreatePacket.FromNetworkedJobs(networkedStation, jobs); if (peer == null) - SendPacketToAll(packet, DeliveryMethod.ReliableOrdered, SelfPeer); + SendPacketToAll(packet, DeliveryMethod.ReliableOrdered, excludeSelf: true); else SendPacket(peer, packet, DeliveryMethod.ReliableOrdered); } @@ -740,19 +774,24 @@ public void SendJobsCreatePacket(NetworkedStationController networkedStation, Ne public void SendJobsUpdatePacket(ushort stationNetId, NetworkedJob[] jobs) { Multiplayer.Log($"Sending JobsUpdatePacket for stationNetId {stationNetId} with {jobs.Count()} jobs"); - SendPacketToAll(ClientboundJobsUpdatePacket.FromNetworkedJobs(stationNetId, jobs), DeliveryMethod.ReliableOrdered, SelfPeer); + SendPacketToAll(ClientboundJobsUpdatePacket.FromNetworkedJobs(stationNetId, jobs), DeliveryMethod.ReliableOrdered, excludeSelf: true); } public void SendTaskUpdate(ushort taskNetId, TaskState newState, float taskStartTime, float taskFinishTime) { Multiplayer.Log($"Sending TaskUpdate for taskNetId {taskNetId}, newState {newState}"); - SendPacketToAll(new ClientboundTaskUpdatePacket - { - TaskNetId = taskNetId, - NewState = newState, - TaskStartTime = taskStartTime, - TaskFinishTime = taskFinishTime - }, DeliveryMethod.ReliableOrdered, SelfPeer); + SendPacketToAll + ( + new ClientboundTaskUpdatePacket + { + TaskNetId = taskNetId, + NewState = newState, + TaskStartTime = taskStartTime, + TaskFinishTime = taskFinishTime + }, + DeliveryMethod.ReliableOrdered, + excludeSelf: true + ); } public void SendItemsChangePacket(List items, ServerPlayer player) @@ -768,31 +807,30 @@ public void SendItemsChangePacket(List items, ServerPlayer playe public void SendChat(string message, ServerPlayer exclude = null) { + var packet = new CommonChatPacket + { + message = message + }; if (exclude != null) - { - NetworkLifecycle.Instance.Server.SendPacketToAll(new CommonChatPacket - { - message = message - }, DeliveryMethod.ReliableUnordered, exclude.Peer); - } + SendPacketToAll(packet, DeliveryMethod.ReliableUnordered, exclude.Peer); else - { - NetworkLifecycle.Instance.Server.SendPacketToAll(new CommonChatPacket - { - message = message - }, DeliveryMethod.ReliableUnordered); - } + SendPacketToAll(packet, DeliveryMethod.ReliableUnordered); } public void SendWhisper(string message, ServerPlayer recipient) { if (!string.IsNullOrEmpty(message) && recipient != null && recipient.Peer != null) { - NetworkLifecycle.Instance.Server.SendPacket(recipient.Peer, new CommonChatPacket - { - message = message - }, DeliveryMethod.ReliableUnordered); + NetworkLifecycle.Instance.Server.SendPacket + ( + recipient.Peer, + new CommonChatPacket + { + message = message + }, + DeliveryMethod.ReliableUnordered + ); } } @@ -1098,10 +1136,15 @@ private void OnServerboundPlayerPositionPacket(ServerboundPlayerPositionPacket p private void OnServerboundTimeAdvancePacket(ServerboundTimeAdvancePacket packet, ITransportPeer peer) { - SendPacketToAll(new ClientboundTimeAdvancePacket - { - amountOfTimeToSkipInSeconds = packet.amountOfTimeToSkipInSeconds - }, DeliveryMethod.ReliableUnordered, peer); + SendPacketToAll + ( + new ClientboundTimeAdvancePacket + { + amountOfTimeToSkipInSeconds = packet.amountOfTimeToSkipInSeconds + }, + DeliveryMethod.ReliableUnordered, + peer + ); } private void OnCommonChangeJunctionPacket(CommonChangeJunctionPacket packet, ITransportPeer peer) @@ -1134,16 +1177,17 @@ private void OnCommonCouplerInteractionPacket(CommonCouplerInteractionPacket pac { LogDebug(() => $"OnCommonCouplerInteractionPacket([{packet.Flags}, {netTrainCar.CurrentID}, {packet.NetId}], {player.PlayerId}) Sending validation failure"); //failed validation notify client - SendPacket( - peer, - new CommonCouplerInteractionPacket - { - NetId = packet.NetId, - Flags = (ushort)CouplerInteractionType.NoAction, - IsFrontCoupler = packet.IsFrontCoupler, - } - , DeliveryMethod.ReliableOrdered - ); + SendPacket + ( + peer, + new CommonCouplerInteractionPacket + { + NetId = packet.NetId, + Flags = (ushort)CouplerInteractionType.NoAction, + IsFrontCoupler = packet.IsFrontCoupler, + }, + DeliveryMethod.ReliableOrdered + ); } } else diff --git a/MultiplayerAPI Tests/TestComponents/ServerTest.cs b/MultiplayerAPI Tests/TestComponents/ServerTest.cs index 28471907..e2e17897 100644 --- a/MultiplayerAPI Tests/TestComponents/ServerTest.cs +++ b/MultiplayerAPI Tests/TestComponents/ServerTest.cs @@ -163,8 +163,8 @@ public void SendSimplePacketToAll(string carId, Vector3 position, WheelArrangeme WheelArrangement = arrangement }; - //send the packet reliably (ensure it makes it to all players) - server.SendPacketToAll(packet, true, excludePlayer); + // Send the packet reliably (ensure it makes it to all players), allow sending to the local client (true will block sending to a local client), exclude a player if specified + server.SendPacketToAll(packet, true, false, excludePlayer); } public void SendSimplePacketWithNetIdToAll(ushort carId, Vector3 position, WheelArrangement arrangement, IPlayer excludePlayer = null) @@ -178,7 +178,7 @@ public void SendSimplePacketWithNetIdToAll(ushort carId, Vector3 position, Wheel }; //send the packet reliably (ensure it makes it to all players) - server.SendPacketToAll(packet, true, excludePlayer); + server.SendPacketToAll(packet, true, true, excludePlayer); } public void SendComplexPacket(Dictionary carToPos, IPlayer excludePlayer = null) @@ -189,8 +189,8 @@ public void SendComplexPacket(Dictionary carToPos, IPlayer excl CarToPositionMap = carToPos }; - //send the packet reliably (ensure it makes it to all players) - server.SendSerializablePacketToAll(packet, true, excludePlayer); + // Send the packet reliably (ensure it makes it to all players), allow sending to the local client (true will block sending to a local client), exclude a player if specified + server.SendSerializablePacketToAll(packet, true, false, excludePlayer); } #endregion diff --git a/MultiplayerAPI/Interfaces/IServer.cs b/MultiplayerAPI/Interfaces/IServer.cs index bf34443d..4489f878 100644 --- a/MultiplayerAPI/Interfaces/IServer.cs +++ b/MultiplayerAPI/Interfaces/IServer.cs @@ -90,8 +90,9 @@ public interface IServer /// Packet type /// Packet to send /// Whether to send reliably + /// Sends the packet to the local client when false, skips sending to the local client when true /// Exclude this player - void SendPacketToAll(T packet, bool reliable = true, IPlayer excludePlayer = null) where T : class, IPacket, new(); + void SendPacketToAll(T packet, bool reliable = true, bool excludeSelf = false, IPlayer excludePlayer = null) where T : class, IPacket, new(); /// /// Send a packet to all connected players @@ -99,8 +100,9 @@ public interface IServer /// Packet type /// Packet to send /// Whether to send reliably + /// Sends the packet to the local client when false, skips sending to the local client when true /// Exclude this player - void SendSerializablePacketToAll(T packet, bool reliable = true, IPlayer excludePlayer = null) where T : class, ISerializablePacket, new(); + void SendSerializablePacketToAll(T packet, bool reliable = true, bool excludeSelf = false, IPlayer excludePlayer = null) where T : class, ISerializablePacket, new(); /// /// Send a packet to a specific player From 808a9afaea6b936e0b6678c8e7d9012e0bdb7a17 Mon Sep 17 00:00:00 2001 From: Macka Date: Sat, 25 Oct 2025 10:25:40 +1000 Subject: [PATCH 464/521] Code cleanup and doc improvements Improved code formatting and spacing in NetworkedTrainCar, NetworkedStationController, and NetworkClient. Commented out unused subscription and handler for CommonTrainCouplePacket in NetworkClient. --- .../Networking/Train/NetworkedTrainCar.cs | 15 ++++--- .../World/NetworkedStationController.cs | 16 ++++---- Multiplayer/Multiplayer.csproj | 7 +++- .../Managers/Client/NetworkClient.cs | 40 +++++++++---------- MultiplayerAPI/Interfaces/IClient.cs | 3 +- MultiplayerAPI/MultiplayerAPI.cs | 18 ++++----- 6 files changed, 52 insertions(+), 47 deletions(-) diff --git a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs index a3478539..e80eac4a 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; using DV.CabControls; using DV.Customization.Paint; using DV.MultipleUnit; @@ -11,14 +7,16 @@ using JetBrains.Annotations; using LocoSim.Definitions; using LocoSim.Implementations; -using MPAPI.Interfaces; using Multiplayer.Components.Networking.Player; -using Multiplayer.Networking.Data; using Multiplayer.Networking.Data.Train; +using Multiplayer.Networking.Data; using Multiplayer.Networking.Packets.Clientbound.Train; using Multiplayer.Networking.Packets.Common.Train; -using Multiplayer.Networking.TransportLayers; using Multiplayer.Utils; +using System.Collections.Generic; +using System.Collections; +using System.Linq; +using System; using UnityEngine; namespace Multiplayer.Components.Networking.Train; @@ -529,6 +527,7 @@ private void Server_SendCouplers() NetworkLifecycle.Instance.Server.SendCockState(NetId, TrainCar.frontCoupler, TrainCar.frontCoupler.IsCockOpen); NetworkLifecycle.Instance.Server.SendCockState(NetId, TrainCar.rearCoupler, TrainCar.rearCoupler.IsCockOpen); } + private void Server_SendCables() { if (!sendCables) @@ -1279,7 +1278,7 @@ public void Client_ReceiveTrainPhysicsUpdate(in TrainsetMovementPart movementPar TrainCar.Derail(); movementPart.RigidbodySnapshot.Apply(TrainCar.rb); - // Client_trainRigidbodyQueue.ReceiveSnapshot(movementPart.RigidbodySnapshot, tick); + // Client_trainRigidbodyQueue.ReceiveSnapshot(movementPart.RigidbodySnapshot, tick); //Multiplayer.LogDebug(() => $"Derailed car {TrainCar.ID} positioned at {TrainCar.transform.position}"); } diff --git a/Multiplayer/Components/Networking/World/NetworkedStationController.cs b/Multiplayer/Components/Networking/World/NetworkedStationController.cs index f3a2c66b..208d6d1b 100644 --- a/Multiplayer/Components/Networking/World/NetworkedStationController.cs +++ b/Multiplayer/Components/Networking/World/NetworkedStationController.cs @@ -104,11 +104,11 @@ public static bool TryGetNetId(JobValidator jobValidator, out ushort netId) return false; } - public static DictionaryGetAll() + public static Dictionary GetAll() { Dictionary result = []; - foreach (var kvp in stationIdToNetworkedStationController ) + foreach (var kvp in stationIdToNetworkedStationController) { //Multiplayer.Log($"GetAll() adding {kvp.Value.NetId}, {kvp.Key}"); result.Add(kvp.Value.NetId, kvp.Key); @@ -156,7 +156,7 @@ public static void RegisterStationController(NetworkedStationController networke { string stationID = stationController.logicStation.ID; - stationControllerToNetworkedStationController.Add(stationController,networkedStationController); + stationControllerToNetworkedStationController.Add(stationController, networkedStationController); stationIdToNetworkedStationController.Add(stationID, networkedStationController); stationIdToStationController.Add(stationID, stationController); stationToNetworkedStationController.Add(stationController.logicStation, networkedStationController); @@ -177,7 +177,7 @@ private static void RegisterJobValidator(JobValidator jobValidator, NetworkedSta } #endregion - const int MAX_FRAMES = 120; + const int MAX_FRAMES = 120; protected override bool IsIdServerAuthoritative => true; @@ -249,7 +249,7 @@ private IEnumerator WaitForLogicStation() string stationName = validator.transform.parent.name ?? ""; stationName += "_office_anchor"; - if(this.transform.parent.name.Equals(stationName, StringComparison.OrdinalIgnoreCase)) + if (this.transform.parent.name.Equals(stationName, StringComparison.OrdinalIgnoreCase)) { JobValidator = validator; RegisterJobValidator(validator, this); @@ -266,7 +266,7 @@ public void AddJob(Job job) NetworkedJob networkedJob = new GameObject($"NetworkedJob {job.ID}").AddComponent(); networkedJob.Initialize(job, this); NetworkedJobs.Add(networkedJob); - + NewJobs.Add(networkedJob); //Setup handlers @@ -355,7 +355,7 @@ private void AddJob(JobData jobData) return; } - + Multiplayer.LogDebug(() => $"AddJob({jobData.ID}) Starting plate update {newJob.ID} count: {jobData.GetCars().Count}"); StartCoroutine(UpdateCarPlates(carNetIds, newJob.ID)); @@ -617,7 +617,7 @@ public static IEnumerator UpdateCarPlates(List carNetIds, string jobId) private void GenerateOverview(NetworkedJob networkedJob, ushort itemNetId, ItemPositionData posData) { Multiplayer.Log($"GenerateOverview([{networkedJob.Job.ID},{networkedJob.Job.jobType}], {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); + JobOverview jobOverview = BookletCreator_JobOverview.Create(networkedJob.Job, posData.Position + WorldMover.currentMove, posData.Rotation, WorldMover.OriginShiftParent); NetworkedItem netItem = jobOverview.GetOrAddComponent(); netItem.Initialize(jobOverview, itemNetId, false); diff --git a/Multiplayer/Multiplayer.csproj b/Multiplayer/Multiplayer.csproj index 26d2573a..61307315 100644 --- a/Multiplayer/Multiplayer.csproj +++ b/Multiplayer/Multiplayer.csproj @@ -3,7 +3,7 @@ net48 latest Multiplayer - 0.1.12.5 + 0.1.12.8 @@ -20,6 +20,11 @@ + + + $(NoWarn);CS0436 + + diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index e28184a8..11eb466d 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -144,7 +144,7 @@ protected override void Subscribe() netPacketProcessor.SubscribeReusable(OnClientboundDestroyTrainCarPacket); netPacketProcessor.SubscribeReusable(OnClientboundTrainPhysicsPacket); netPacketProcessor.SubscribeReusable(OnCommonCouplerInteractionPacket); - netPacketProcessor.SubscribeReusable(OnCommonTrainCouplePacket); + //netPacketProcessor.SubscribeReusable(OnCommonTrainCouplePacket); netPacketProcessor.SubscribeReusable(OnCommonTrainUncouplePacket); netPacketProcessor.SubscribeReusable(OnCommonHoseConnectedPacket); netPacketProcessor.SubscribeReusable(OnCommonHoseDisconnectedPacket); @@ -351,7 +351,6 @@ private void OnClientboundPlayerDisconnectPacket(ClientboundPlayerDisconnectPack ClientPlayerManager.RemovePlayer(packet.PlayerId); } - //For server shutting down / player kicked private void OnClientboundDisconnectPacket(ClientboundDisconnectPacket packet) { @@ -365,6 +364,7 @@ private void OnClientboundDisconnectPacket(ClientboundDisconnectPacket packet) disconnectMessage = "Server Shutting Down"; } } + private void OnClientboundPlayerPositionPacket(ClientboundPlayerPositionPacket packet) { ClientPlayerManager.UpdatePosition(packet.PlayerId, packet.Position, packet.MoveDir, packet.RotationY, packet.IsJumping, packet.IsOnCar, packet.CarID); @@ -515,7 +515,6 @@ private void OnClientBoundStationControllerLookupPacket(ClientBoundStationContro } } - private void OnClientboundRailwayStatePacket(ClientboundRailwayStatePacket packet) { for (int i = 0; i < packet.SelectedJunctionBranches.Length; i++) @@ -626,25 +625,26 @@ private void OnCommonCouplerInteractionPacket(CommonCouplerInteractionPacket pac netTrainCar.Common_ReceiveCouplerInteraction(packet); } - private void OnCommonTrainCouplePacket(CommonTrainCouplePacket packet) - { - // TrainCar trainCar = null; - // TrainCar otherTrainCar = null; - // if (!NetworkedTrainCar.TryGet(packet.NetId, out trainCar) || !NetworkedTrainCar.TryGet(packet.OtherNetId, out otherTrainCar)) - // { - // LogDebug(() => $"OnCommonTrainCouplePacket() netId: {packet.NetId}, trainCar found?: {trainCar != null}, otherNetId: {packet.OtherNetId}, otherTrainCar found?: {otherTrainCar != null}"); - // return; - // } + //private void OnCommonTrainCouplePacket(CommonTrainCouplePacket packet) + //{ + // TrainCar trainCar = null; + // TrainCar otherTrainCar = null; - // LogDebug(() => $"OnCommonTrainCouplePacket() netId: {packet.NetId}, trainCar: {trainCar.ID}, otherNetId: {packet.OtherNetId}, otherTrainCar: {otherTrainCar.ID}"); + // if (!NetworkedTrainCar.TryGet(packet.NetId, out trainCar) || !NetworkedTrainCar.TryGet(packet.OtherNetId, out otherTrainCar)) + // { + // LogDebug(() => $"OnCommonTrainCouplePacket() netId: {packet.NetId}, trainCar found?: {trainCar != null}, otherNetId: {packet.OtherNetId}, otherTrainCar found?: {otherTrainCar != null}"); + // return; + // } - // Coupler coupler = packet.IsFrontCoupler ? trainCar.frontCoupler : trainCar.rearCoupler; - // Coupler otherCoupler = packet.OtherCarIsFrontCoupler ? otherTrainCar.frontCoupler : otherTrainCar.rearCoupler; + // LogDebug(() => $"OnCommonTrainCouplePacket() netId: {packet.NetId}, trainCar: {trainCar.ID}, otherNetId: {packet.OtherNetId}, otherTrainCar: {otherTrainCar.ID}"); - // 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!"); - } + // 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!"); + //} private void OnCommonTrainUncouplePacket(CommonTrainUncouplePacket packet) { @@ -986,12 +986,12 @@ private void OnClientboundDebtStatusPacket(ClientboundDebtStatusPacket packet) { CareerManagerDebtControllerPatch.HasDebt = packet.HasDebt; } + private void OnCommonChatPacket(CommonChatPacket packet) { chatGUI?.ReceiveMessage(packet.message); } - private void OnClientboundJobsCreatePacket(ClientboundJobsCreatePacket packet) { if (NetworkLifecycle.Instance.IsHost()) @@ -1347,6 +1347,7 @@ public void SendHandbrakePositionChanged(ushort netId, float position) Position = position }, DeliveryMethod.ReliableOrdered); } + public void SendAddCoal(ushort netId, float coalMassDelta) { SendPacketToServer(new ServerboundAddCoalPacket @@ -1448,7 +1449,6 @@ public void SendWarehouseRequest(WarehouseAction action, ushort netId) }, DeliveryMethod.ReliableUnordered); } - public void SendChat(string message) { SendPacketToServer(new CommonChatPacket diff --git a/MultiplayerAPI/Interfaces/IClient.cs b/MultiplayerAPI/Interfaces/IClient.cs index 06153cbe..71379ab6 100644 --- a/MultiplayerAPI/Interfaces/IClient.cs +++ b/MultiplayerAPI/Interfaces/IClient.cs @@ -42,8 +42,9 @@ public interface IClient void CancelReadyBlock(ModInfo modInfo); /// - /// Gets Player Id of the local player + /// Gets Player Id of the local player. /// + /// /// The local player does not have an IPlayer object /// diff --git a/MultiplayerAPI/MultiplayerAPI.cs b/MultiplayerAPI/MultiplayerAPI.cs index b2fe7cc1..0bd2e303 100644 --- a/MultiplayerAPI/MultiplayerAPI.cs +++ b/MultiplayerAPI/MultiplayerAPI.cs @@ -80,27 +80,27 @@ public static string LoadedApiVersion private static IClient _client; /// - /// Gets whether the Multiplayer mod is available + /// Gets whether the Multiplayer mod is available. /// public static bool IsMultiplayerLoaded => _instance != null; /// - /// Gets the current API instance (null if Multiplayer mod is not loaded) + /// Gets the current API instance (null if Multiplayer mod is not loaded). /// public static IMultiplayerAPI Instance => _instance; /// - /// Gets the current Server API instance (null if Multiplayer mod is not loaded or server not running) + /// Gets the current Server API instance (null if Multiplayer mod is not loaded or server not running). /// public static IServer Server => _server; /// - /// Gets the current Client API instance (null if Multiplayer mod is not loaded or client not running) + /// Gets the current Client API instance (null if Multiplayer mod is not loaded or client not running). /// public static IClient Client => _client; /// - /// Internal method for the Multiplayer mod to register itself + /// Internal method for the Multiplayer mod to register itself. /// /// The API implementation internal static void RegisterAPI(IMultiplayerAPI apiInstance) @@ -109,7 +109,7 @@ internal static void RegisterAPI(IMultiplayerAPI apiInstance) } /// - /// Internal method for the Multiplayer mod to register a client instance + /// Internal method for the Multiplayer mod to register a client instance. /// /// The Client implementation internal static void RegisterClient(IClient client) @@ -119,7 +119,7 @@ internal static void RegisterClient(IClient client) } /// - /// Internal method for the Multiplayer mod to deregister a client instance + /// Internal method for the Multiplayer mod to deregister a client instance. /// internal static void ClearClient() { @@ -128,7 +128,7 @@ internal static void ClearClient() } /// - /// Internal method for the Multiplayer mod to register a server instance + /// Internal method for the Multiplayer mod to register a server instance. /// /// The API implementation internal static void RegisterServer(IServer server) @@ -138,7 +138,7 @@ internal static void RegisterServer(IServer server) } /// - /// Internal method for the Multiplayer mod to deregister a server instance + /// Internal method for the Multiplayer mod to deregister a server instance. /// internal static void ClearServer() { From b5c5e02d72102fc2cd41dafc64aa4277308f5e44 Mon Sep 17 00:00:00 2001 From: Macka Date: Sat, 25 Oct 2025 10:26:20 +1000 Subject: [PATCH 465/521] Improve job validation response waiting logic Replaced WaitForSecondsRealtime with WaitUntil to better handle job validation response timing. Added a 3-second timeout to accommodate longer asset loading times, improving reliability for book spawns and similar cases. --- Multiplayer/Patches/Jobs/JobValidatorPatch.cs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/Multiplayer/Patches/Jobs/JobValidatorPatch.cs b/Multiplayer/Patches/Jobs/JobValidatorPatch.cs index 79c7a909..8ab6af3e 100644 --- a/Multiplayer/Patches/Jobs/JobValidatorPatch.cs +++ b/Multiplayer/Patches/Jobs/JobValidatorPatch.cs @@ -12,6 +12,8 @@ namespace Multiplayer.Patches.Jobs; [HarmonyPatch(typeof(JobValidator))] public static class JobValidator_Patch { + private const float TIME_OUT = 3f; + [HarmonyPatch(nameof(JobValidator.Start))] [HarmonyPostfix] private static void Start(JobValidator __instance) @@ -108,7 +110,20 @@ private static void SendValidationRequest(JobValidator validator,NetworkedJob ne } private static IEnumerator AwaitResponse(JobValidator validator, NetworkedJob networkedJob) { - yield return new WaitForSecondsRealtime((NetworkLifecycle.Instance.Client.Ping * 4f)/1000); + Multiplayer.LogDebug(() => $"Awaiting validation response for {networkedJob?.Job?.ID}..."); + + float timeout = Time.time; + + //Book spawns can take a few seconds, this may be due to how the asset is loaded and rendered + yield return new WaitUntil + ( + () => + { + return networkedJob.ValidatorResponseReceived || (Time.time - timeout > TIME_OUT); + } + ); + + //WaitForSecondsRealtime(Math.Max(4f,(NetworkLifecycle.Instance.Client.Ping * 4f)/1000)); bool received = networkedJob.ValidatorResponseReceived; bool accepted = networkedJob.ValidationAccepted; From 7eaa8da55e95bc43cf75f4801281d1c44f5d2dbe Mon Sep 17 00:00:00 2001 From: Macka Date: Fri, 7 Nov 2025 22:45:51 +1000 Subject: [PATCH 466/521] Update mod compatibility and patch WarehouseMachineController Changed compatibility for BookletOrganizer to Host and added DVCustomCargo to compatibility list. Refactored WarehouseMachineControllerPatch.SetScreen to use void return type and early returns instead of bool, improving clarity. Fixed documentation typo in MultiplayerCompatibility and corrected XML comments in IPlayer and IClient interfaces. Bumped version to 0.1.12.8. --- Multiplayer/API/ModCompatibilityManager.cs | 5 +++-- .../Jobs/WarehouseMachineControllerPatch.cs | 14 ++++++-------- MultiplayerAPI Tests/Packets/SimplePacket.cs | 2 +- MultiplayerAPI/Interfaces/IClient.cs | 1 - MultiplayerAPI/Interfaces/IPlayer.cs | 4 ++-- MultiplayerAPI/Types/MultiplayerCompatibility.cs | 2 +- info.json | 2 +- 7 files changed, 14 insertions(+), 16 deletions(-) diff --git a/Multiplayer/API/ModCompatibilityManager.cs b/Multiplayer/API/ModCompatibilityManager.cs index c9f00d1a..a13169aa 100644 --- a/Multiplayer/API/ModCompatibilityManager.cs +++ b/Multiplayer/API/ModCompatibilityManager.cs @@ -30,10 +30,11 @@ protected override void Awake() //we don't care if the client does/doesn't have these mods RegisterCompatibility("RuntimeUnityEditor", MultiplayerCompatibility.Client); - RegisterCompatibility("BookletOrganizer", MultiplayerCompatibility.Client); + RegisterCompatibility("BookletOrganizer", MultiplayerCompatibility.Host); RegisterCompatibility("RemoteDispatch", MultiplayerCompatibility.Client); - RegisterCompatibility("DVDiscordPresenceMod", MultiplayerCompatibility.Client); RegisterCompatibility("CommsRadioAPI", MultiplayerCompatibility.Client); + RegisterCompatibility("DVCustomCargo", MultiplayerCompatibility.All); + RegisterCompatibility("DVDiscordPresenceMod", MultiplayerCompatibility.Client); RegisterCompatibility("DVLangHelper", MultiplayerCompatibility.Client); RegisterCompatibility("LightingOverhaul", MultiplayerCompatibility.Client); diff --git a/Multiplayer/Patches/Jobs/WarehouseMachineControllerPatch.cs b/Multiplayer/Patches/Jobs/WarehouseMachineControllerPatch.cs index ed42c8a4..0900d1df 100644 --- a/Multiplayer/Patches/Jobs/WarehouseMachineControllerPatch.cs +++ b/Multiplayer/Patches/Jobs/WarehouseMachineControllerPatch.cs @@ -21,10 +21,10 @@ public static void Awake(WarehouseMachineController __instance) [HarmonyPrefix] [HarmonyPatch(nameof(WarehouseMachineController.SetScreen))] - public static bool SetScreen(WarehouseMachineController __instance, TextPreset preset, bool isLoading, string jobId, Car car, CargoType_v2 cargoType) + public static void SetScreen(WarehouseMachineController __instance, TextPreset preset, bool isLoading, string jobId, Car car, CargoType_v2 cargoType) { if (!NetworkLifecycle.Instance.IsHost()) - return true; + return; //Multiplayer.LogDebug(() => $"WarehouseMachineControllerPatch.SetScreen() is host"); @@ -38,12 +38,12 @@ public static bool SetScreen(WarehouseMachineController __instance, TextPreset p //Multiplayer.LogDebug(() => $"WarehouseMachineControllerPatch.SetScreen() skipping: {skip}"); if (skip) - return true; + return; if (!NetworkedWarehouseMachineController.GetFromWarehouseMachineController(__instance, out var netMachine) || netMachine == null) { Multiplayer.LogError($"WarehouseMachineControllerPatch.SetScreen(): Failed to get NetworkedWarehouseMachineController for {__instance.warehouseTrackName}"); - return true; + return; } //Multiplayer.LogDebug(() => $"WarehouseMachineControllerPatch.SetScreen() NetMachine found"); @@ -60,7 +60,7 @@ public static bool SetScreen(WarehouseMachineController __instance, TextPreset p if (tc == null || !NetworkedTrainCar.TryGetFromTrainCar(tc, out var netTC)) { //Multiplayer.LogWarning($"WarehouseMachineControllerPatch.SetScreen() Failed to get NetworkedTrainCar for {car?.ID}"); - return true; + return; } //Multiplayer.LogDebug(() => $"WarehouseMachineControllerPatch.SetScreen() NetCar found"); @@ -72,7 +72,7 @@ public static bool SetScreen(WarehouseMachineController __instance, TextPreset p if(!NetworkedJob.TryGetFromJobId(jobId, out var netJob)) { Multiplayer.LogWarning($"WarehouseMachineControllerPatch.SetScreen() Failed to get NetworkedJob for {jobId}"); - return true; + return; } //Multiplayer.LogDebug(() => $"WarehouseMachineControllerPatch.SetScreen() NetJob found"); @@ -83,8 +83,6 @@ public static bool SetScreen(WarehouseMachineController __instance, TextPreset p cargoTypeV1 = cargoType.v1; NetworkLifecycle.Instance.Server.SendWarehouseControllerUpdate(netMachine.NetId, isLoading, jobNetId, carNetId, cargoTypeV1, preset); - - return false; } [HarmonyPrefix] diff --git a/MultiplayerAPI Tests/Packets/SimplePacket.cs b/MultiplayerAPI Tests/Packets/SimplePacket.cs index 34dc7430..07a247fa 100644 --- a/MultiplayerAPI Tests/Packets/SimplePacket.cs +++ b/MultiplayerAPI Tests/Packets/SimplePacket.cs @@ -11,7 +11,7 @@ internal class SimplePacket : IPacket // Primitives (bool, byte, sbyte, short, ushort, int, uint, long, ulong, float, double, string, char, IPEndPoint, Guid) // Arrays of primitives // Enums derived from primitives e.g. `enum MyEnum : byte` - // UnityEngine: Vector2, Vector3, Quarternion + // UnityEngine: Vector2, Vector3, Quaternion //Be mindful of the amount of data per packet. // Avoid sending long strings or large structures diff --git a/MultiplayerAPI/Interfaces/IClient.cs b/MultiplayerAPI/Interfaces/IClient.cs index 71379ab6..ade5473d 100644 --- a/MultiplayerAPI/Interfaces/IClient.cs +++ b/MultiplayerAPI/Interfaces/IClient.cs @@ -44,7 +44,6 @@ public interface IClient /// /// Gets Player Id of the local player. /// - /// /// The local player does not have an IPlayer object /// diff --git a/MultiplayerAPI/Interfaces/IPlayer.cs b/MultiplayerAPI/Interfaces/IPlayer.cs index e93a3800..5c37184b 100644 --- a/MultiplayerAPI/Interfaces/IPlayer.cs +++ b/MultiplayerAPI/Interfaces/IPlayer.cs @@ -53,9 +53,9 @@ public interface IPlayer int Ping { get; } /// - /// Gets the current network ping/latency for this player. + /// Gets a value indicating whether this player is on a car. /// - /// The round-trip time in milliseconds between the server and this player. + /// true if the player is on a car; otherwise, false. bool IsOnCar { get; } /// diff --git a/MultiplayerAPI/Types/MultiplayerCompatibility.cs b/MultiplayerAPI/Types/MultiplayerCompatibility.cs index ae8fa26c..1de39d0e 100644 --- a/MultiplayerAPI/Types/MultiplayerCompatibility.cs +++ b/MultiplayerAPI/Types/MultiplayerCompatibility.cs @@ -34,7 +34,7 @@ public enum MultiplayerCompatibility : byte Host, /// - /// Mod has no effect on the gamne play and can be ignored + /// Mod has no effect on the game play and can be ignored. /// This should be used for client-only mods e.g. GUI enhancements, controller mods, RUE, etc. /// Client, diff --git a/info.json b/info.json index 5e1d60bd..1307d35a 100644 --- a/info.json +++ b/info.json @@ -1,6 +1,6 @@ { "Id": "Multiplayer", - "Version": "0.1.12.2", + "Version": "0.1.12.8", "DisplayName": "Multiplayer", "Author": "Insprill, Macka, Morm", "EntryMethod": "Multiplayer.Multiplayer.Load", From 9a164eb18bc1ce20641a035124b813ad34dddbb6 Mon Sep 17 00:00:00 2001 From: Macka Date: Sat, 8 Nov 2025 23:15:12 +1000 Subject: [PATCH 467/521] Refactor task network data registration and conversion Simplifies the registration of custom Task types for multiplayer by requiring only the Task type and its associated TaskNetworkData type, removing the need for explicit converter and creator functions. Updates APIProvider and IMultiplayerAPI to reflect this change and adds new conversion helper methods. Cleans up SequentialTasksData by removing unused CurrentTaskIndex logic. Renames TaskNetworkType.cs to TaskNetworkDataType.cs for clarity. --- Multiplayer/API/APIProvider.cs | 25 ++++++- .../Networking/Data/TaskNetworkData.cs | 66 +++++-------------- MultiplayerAPI/Interfaces/IMultiplayerAPI.cs | 54 ++++++++++----- ...kNetworkType.cs => TaskNetworkDataType.cs} | 4 +- 4 files changed, 79 insertions(+), 70 deletions(-) rename MultiplayerAPI/Types/{TaskNetworkType.cs => TaskNetworkDataType.cs} (98%) diff --git a/Multiplayer/API/APIProvider.cs b/Multiplayer/API/APIProvider.cs index 759dc1f1..f48d4ef8 100644 --- a/Multiplayer/API/APIProvider.cs +++ b/Multiplayer/API/APIProvider.cs @@ -5,6 +5,7 @@ using Multiplayer.Components.Networking.Train; using Multiplayer.Networking.Data; using System; +using System.Collections.Generic; namespace Multiplayer.API; @@ -77,9 +78,12 @@ public void UnregisterPaintTheme(uint themeId) } } - public bool RegisterTaskType(TaskType taskType, Func converter, Func emptyCreator) where TGameTask : Task + #region Task Serialisation + public bool RegisterTaskType(TaskType taskType) + where TGameTask : Task + where TNetworkData : TaskNetworkData, new() { - return TaskNetworkDataFactory.RegisterTaskType(taskType, converter, emptyCreator); + return TaskNetworkDataFactory.RegisterTaskType(taskType); } public bool UnregisterTaskType(TaskType taskType) where TGameTask : Task @@ -87,6 +91,23 @@ public bool UnregisterTaskType(TaskType taskType) where TGameTask : T return TaskNetworkDataFactory.UnregisterTaskType(taskType); } + public TaskNetworkData[] ConvertTasks(IEnumerable tasks) + { + return TaskNetworkDataFactory.ConvertTasks(tasks); + } + + public TaskNetworkData ConvertTask(Task task) + { + return TaskNetworkDataFactory.ConvertTask(task); + } + + public TaskNetworkData ConvertTask(TaskType type) + { + return TaskNetworkDataFactory.ConvertTask(type); + } + + #endregion + #region Class Helpers internal APIProvider() diff --git a/Multiplayer/Networking/Data/TaskNetworkData.cs b/Multiplayer/Networking/Data/TaskNetworkData.cs index 828be086..d316c8ea 100644 --- a/Multiplayer/Networking/Data/TaskNetworkData.cs +++ b/Multiplayer/Networking/Data/TaskNetworkData.cs @@ -19,8 +19,9 @@ public static class TaskNetworkDataFactory internal static readonly List baseTasks = []; internal static readonly List baseTaskTypes = []; - public static bool RegisterTaskType(TaskType taskType, Func converter, Func emptyCreator) + public static bool RegisterTaskType(TaskType taskType) where TGameTask : Task + where TNetworkData : TaskNetworkData, new() { Multiplayer.LogDebug(() => $"Registering Task Type {typeof(TGameTask)} with TaskType {taskType}"); @@ -30,8 +31,13 @@ public static bool RegisterTaskType(TaskType taskType, Func converter((TGameTask)task); - EnumToEmptyTaskNetworkData[taskType] = emptyCreator; + TypeToTaskNetworkData[typeof(TGameTask)] = task => + { + var networkData = new TNetworkData { TaskType = taskType }; + return ((TaskNetworkData)networkData).FromTask(task); + }; + + EnumToEmptyTaskNetworkData[taskType] = type => new TNetworkData { TaskType = type }; return true; } @@ -74,51 +80,35 @@ public static TaskNetworkData[] ConvertTasks(IEnumerable tasks) return tasks.Select(ConvertTask).ToArray(); } - public static TaskNetworkData ConvertTask(TaskType type) + public static TaskNetworkData ConvertTask(TaskType taskType) { //Multiplayer.LogDebug(() => $"TaskNetworkDataFactory.ConvertTask({type})"); - if (EnumToEmptyTaskNetworkData.TryGetValue(type, out var creator)) + if (EnumToEmptyTaskNetworkData.TryGetValue(taskType, out var creator)) { - return creator(type); + return creator(taskType); } - throw new ArgumentException($"Unknown task type: {type}"); + throw new ArgumentException($"Unknown task type: {taskType}"); } // Register base task types static TaskNetworkDataFactory() { - RegisterTaskType( - TaskType.Warehouse, - task => new WarehouseTaskData { TaskType = TaskType.Warehouse }.FromTask(task), - type => new WarehouseTaskData { TaskType = type } - ); + RegisterTaskType(TaskType.Warehouse); baseTasks.Add(typeof(WarehouseTask)); baseTaskTypes.Add(TaskType.Warehouse); - RegisterTaskType( - TaskType.Transport, - task => new TransportTaskData { TaskType = TaskType.Transport }.FromTask(task), - type => new TransportTaskData { TaskType = type } - ); + RegisterTaskType(TaskType.Transport); baseTasks.Add(typeof(TransportTask)); baseTaskTypes.Add(TaskType.Transport); - RegisterTaskType( - TaskType.Sequential, - task => new SequentialTasksData { TaskType = TaskType.Sequential }.FromTask(task), - type => new SequentialTasksData { TaskType = type } - ); + RegisterTaskType(TaskType.Sequential); baseTasks.Add(typeof(SequentialTasks)); baseTaskTypes.Add(TaskType.Sequential); - RegisterTaskType( - TaskType.Parallel, - task => new ParallelTasksData { TaskType = TaskType.Parallel }.FromTask(task), - type => new ParallelTasksData { TaskType = type } - ); + RegisterTaskType(TaskType.Parallel); baseTasks.Add(typeof(ParallelTasks)); baseTaskTypes.Add(TaskType.Parallel); @@ -337,7 +327,7 @@ public override List GetCars() public class SequentialTasksData : TaskNetworkData { public TaskNetworkData[] Tasks { get; set; } - public byte CurrentTaskIndex { get; set; } + public override void Serialize(BinaryWriter writer) { @@ -354,8 +344,6 @@ public override void Serialize(BinaryWriter writer) writer.Write((byte)task.TaskType); task.Serialize(writer); } - - writer.Write(CurrentTaskIndex); } public override void Deserialize(BinaryReader reader) @@ -369,8 +357,6 @@ public override void Deserialize(BinaryReader reader) Tasks[i] = TaskNetworkDataFactory.ConvertTask(taskType); Tasks[i].Deserialize(reader); } - - CurrentTaskIndex = reader.ReadByte(); } public override SequentialTasksData FromTask(Task task) @@ -382,22 +368,6 @@ public override SequentialTasksData FromTask(Task task) 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; } diff --git a/MultiplayerAPI/Interfaces/IMultiplayerAPI.cs b/MultiplayerAPI/Interfaces/IMultiplayerAPI.cs index 1755d61c..de9ec01b 100644 --- a/MultiplayerAPI/Interfaces/IMultiplayerAPI.cs +++ b/MultiplayerAPI/Interfaces/IMultiplayerAPI.cs @@ -102,37 +102,55 @@ public interface IMultiplayerAPI void UnregisterPaintTheme(uint themeId); /// - /// Registers a serialiser and deserialiser for a custom type for multiplayer synchronization. - /// - /// The concrete type to register. - /// The enum value - /// - /// A function that takes an instance of and returns a corresponding object - /// for serialisation. - /// - /// - /// A function that takes a and returns an empty instance for deserialisation. - /// + /// Registers a serialiser/deserialiser for a custom type for multiplayer synchronisation. + /// + /// The concrete type to register. + /// + /// The type that handles serialisation and deserialisation for . + /// Must have a parameterless constructor and implement to convert from the task. + /// + /// The enum value associated with this task type. /// /// true if the task type was successfully registered; false if the task type was already registered or registration failed. /// /// - /// This method allows the Multiplayer mod to correctly serialise and deserialise custom or extended task types. + /// This method automatically handles conversion by instantiating , calling its + /// method for serialisation, and creating empty instances for deserialisation. /// - bool RegisterTaskType(TaskType taskType, Func converter, Func emptyCreator) - where TGameTask : Task; + bool RegisterTaskType(TaskType taskType) where TCustomTask : Task where TTaskNetworkData : TaskNetworkData, new(); /// /// Unregisters a previously registered custom type. /// - /// The concrete type to unregister. + /// The concrete type to unregister. /// The enum value associated with the task type to unregister. /// - /// true if the task type was successfully unregistered; false if the task type is a base game task type. + /// true if the task type was successfully unregistered; false if the task type was not found or is a base-game task type. /// /// /// This method allows removal of custom or extended task types from the multiplayer system. - /// Base task types cannot be unregistered. + /// Base-game task types cannot be unregistered. /// - bool UnregisterTaskType(TaskType taskType) where TGameTask : Task; + bool UnregisterTaskType(TaskType taskType) where TCustomTask : Task; + + /// + /// Converts an IEnumerable collection of into an array of . + /// + /// The collection of tasks to convert. + /// An array of representing the tasks. + TaskNetworkData[] ConvertTasks(IEnumerable tasks); + + /// + /// Converts a into a . + /// + /// The task to convert. + /// A representing the task. + TaskNetworkData ConvertTask(Task task); + + /// + /// Retrieves a for the specified . + /// + /// The task type to convert. + /// A representing the task. + TaskNetworkData ConvertTask(TaskType taskType); } diff --git a/MultiplayerAPI/Types/TaskNetworkType.cs b/MultiplayerAPI/Types/TaskNetworkDataType.cs similarity index 98% rename from MultiplayerAPI/Types/TaskNetworkType.cs rename to MultiplayerAPI/Types/TaskNetworkDataType.cs index d8a4f33b..046d8387 100644 --- a/MultiplayerAPI/Types/TaskNetworkType.cs +++ b/MultiplayerAPI/Types/TaskNetworkDataType.cs @@ -49,7 +49,7 @@ public abstract class TaskNetworkData public TaskType TaskType { get; set; } /// - /// Serializes the task network data to the specified . + /// Serialises the task network data to the specified . /// Implementations should write all relevant fields for network transmission. /// /// @@ -59,7 +59,7 @@ public abstract class TaskNetworkData public abstract void Serialize(BinaryWriter writer); /// - /// Deserializes the task network data from the specified . + /// Deserialises the task network data from the specified . /// Implementations should read all relevant fields in the same order and size as written by . /// /// From f045b7ad0b10230538e410b3a685eebea777323d Mon Sep 17 00:00:00 2001 From: Macka Date: Sat, 8 Nov 2025 23:18:40 +1000 Subject: [PATCH 468/521] Improve XML documentation for API interfaces Updated and clarified XML documentation comments across MultiplayerAPI interfaces to improve consistency, accuracy, and developer guidance. Changes include more precise remarks, parameter descriptions, and usage of tags for type references. No functional code changes were made. --- MultiplayerAPI/Interfaces/IClient.cs | 68 +++++---- MultiplayerAPI/Interfaces/IMultiplayerAPI.cs | 49 +++--- MultiplayerAPI/Interfaces/INetId.cs | 2 +- MultiplayerAPI/Interfaces/IServer.cs | 142 +++++++++--------- MultiplayerAPI/Interfaces/Packets/IPacket.cs | 4 +- .../Interfaces/Packets/ISerializablePacket.cs | 12 +- MultiplayerAPI/MultiplayerAPI.cs | 10 +- 7 files changed, 149 insertions(+), 138 deletions(-) diff --git a/MultiplayerAPI/Interfaces/IClient.cs b/MultiplayerAPI/Interfaces/IClient.cs index ade5473d..fcecc78c 100644 --- a/MultiplayerAPI/Interfaces/IClient.cs +++ b/MultiplayerAPI/Interfaces/IClient.cs @@ -7,35 +7,39 @@ namespace MPAPI.Interfaces; /// -/// Interface for interacting with Multiplayer mod client instances +/// Interface for interacting with Multiplayer mod client instances. /// public interface IClient { /// /// Event fired when a player connects. /// - /// IPlayer object for the connected player + /// + /// The event handler receives an object for the connected player. + /// event Action OnPlayerConnected; /// - /// Event fired when a player disconnects, but before the IPlayer object is destroyed + /// Event fired when a player disconnects, but before the object is destroyed. /// - /// IPlayer object for the disconnected player + /// + /// The event handler receives an object for the disconnected player. + /// event Action OnPlayerDisconnected; - /// + /// /// Registers a block to prevent the client from sending the 'Ready' signal to the server until all mods have called 'CancelReadyBlock'. - /// - /// Mod information + /// + /// Mod information. /// /// Only required if the mod needs complete loading prior to receiving game state from the server. /// void RegisterReadyBlock(ModInfo modInfo); /// - /// Cancels a previously registered ready block + /// Cancels a previously registered ready block. /// - /// Mod information + /// Mod information. /// /// All registered blocks must be cancelled prior to the client sending the 'Ready' signal to the server. /// @@ -45,68 +49,68 @@ public interface IClient /// Gets Player Id of the local player. /// /// - /// The local player does not have an IPlayer object + /// The local player does not have an object. /// byte PlayerId { get; } /// - /// Gets IPlayer objects for all players connected to the server + /// Gets objects for all players connected to the server. /// - /// Read-only collection of IPlayer objects + /// Read-only collection of objects. IReadOnlyCollection Players { get; } /// - /// Gets number of players currently connected to the server + /// Gets number of players currently connected to the server. /// - /// Positive integer representing the number of connected players + /// Positive integer representing the number of connected players. int PlayerCount { get; } /// - /// Gets IPlayer for player by Id + /// Gets the for player by Id. /// - /// IPlayer object if found, otherwise null + /// object if found, otherwise null. IPlayer GetPlayer(byte playerId); /// - /// Gets connection state for the client + /// Gets connection state for the client. /// bool IsConnected { get; } /// - /// Gets ping for the client + /// Gets ping for the client. /// int Ping { get; } #region Packet API /// - /// Register a packet type that uses automatic serialization + /// Register a packet type that uses automatic serialisation. /// - /// Packet type implementing IPacket - /// Handler to call when packet is received + /// Packet type implementing . + /// Handler to call when packet is received. void RegisterPacket(ClientPacketHandler handler) where T : class, IPacket, new(); /// - /// Register a packet type that uses manual serialization + /// Register a packet type that uses manual serialisation. /// - /// Packet type implementing ISerializablePacket - /// Handler to call when packet is received + /// Packet type implementing . + /// Handler to call when packet is received. void RegisterSerializablePacket(ClientPacketHandler handler) where T : class, ISerializablePacket, new(); /// - /// Send a packet to the server + /// Send a packet based on to the server. /// - /// Packet type - /// Packet to send - /// Whether to send reliably + /// Packet type. + /// Packet to send. + /// Whether to send reliably. void SendPacketToServer(T packet, bool reliable = true) where T : class, IPacket, new(); /// - /// Send a packet to all connected players + /// Send a packet based on to the server. /// - /// Packet type - /// Packet to send - /// Whether to send reliably + /// Packet type. + /// Packet to send. + /// Whether to send reliably. void SendSerializablePacketToServer(T packet, bool reliable = true) where T : class, ISerializablePacket, new(); #endregion diff --git a/MultiplayerAPI/Interfaces/IMultiplayerAPI.cs b/MultiplayerAPI/Interfaces/IMultiplayerAPI.cs index de9ec01b..9a34e1bb 100644 --- a/MultiplayerAPI/Interfaces/IMultiplayerAPI.cs +++ b/MultiplayerAPI/Interfaces/IMultiplayerAPI.cs @@ -1,11 +1,12 @@ using DV.Logic.Job; using MPAPI.Types; using System; +using System.Collections.Generic; namespace MPAPI.Interfaces; /// -/// Main interface for interacting with the Multiplayer mod +/// Main interface for interacting with the Multiplayer mod. /// public interface IMultiplayerAPI { @@ -20,44 +21,44 @@ public interface IMultiplayerAPI public string MultiplayerVersion { get; } /// - /// Gets whether the multiplayer mod is currently loaded and active + /// Gets whether the multiplayer mod is currently loaded and active. /// bool IsMultiplayerLoaded { get; } - /// Sets the mod's compatibility requirements - /// String representing the your mod's Id (`ModEntry.Info.Id`) - /// ModCompatibility flags representing installation host/client requirements + /// Sets the mod's compatibility requirements. + /// String representing the your mod's Id (`ModEntry.Info.Id`). + /// ModCompatibility flags representing installation host/client requirements. void SetModCompatibility(string modId, MultiplayerCompatibility compatibility); /// - /// Returns true if either a host or client exist + /// Returns true if either a host or client exist. /// bool IsConnected { get; } /// - /// Gets whether this instance is host + /// Gets whether this instance is host. /// bool IsHost { get; } /// - /// Gets whether this instance is a dedicated server + /// Gets whether this instance is a dedicated server. /// bool IsDedicatedServer { get; } /// - /// Gets whether this current session is single player + /// Gets whether this current session is single player. /// bool IsSinglePlayer { get; } /// - /// Event fired when a game/network tick occurs + /// Event fired when a game/network tick occurs. /// Ticks occur at a fixed interval (TICK_INTERVAL = 1/TICK_RATE) and are useful for synchronisation, batching, and processing changes. /// /// The tick parameter can be used to determine if non-reliable packets have been dropped and to sequence actions for rollbacks or preventing stale data from being processed. /// /// Example: In Multiplayer's TrainCar simulation sync, small changes are cached when they occur but sent as a single packet per TrainCar when OnTick fires, reducing network overhead. /// - /// The parameter represents the current tick number + /// The event handler receives a representing the current tick number. event Action OnTick; /// @@ -72,33 +73,33 @@ public interface IMultiplayerAPI uint CurrentTick { get; } /// - /// Gets the NetId for an object + /// Gets the NetId for an object. /// - /// The object you want the NetId for - /// When this method returns, contains the NetId associated with the specified object, if found; otherwise, 0 - /// True if a NetId for the object was found; otherwise, false + /// The object you want the NetId for. + /// When this method returns, contains the NetId associated with the specified object, if found; otherwise, 0. + /// True if a NetId for the object was found; otherwise, false. bool TryGetNetId(T obj, out ushort netId) where T : class; /// - /// Gets the object for a NetId + /// Gets the object for a NetId. /// - /// The non-zero NetId for the object - /// When this method returns, contains the object associated with the NetId, if found; otherwise null - /// True if the object was found; otherwise, false + /// The non-zero NetId for the object. + /// When this method returns, contains the object associated with the NetId, if found; otherwise null. + /// True if the object was found; otherwise, false. bool TryGetObjectFromNetId(ushort netId, out T obj) where T : class; /// - /// Registers a PaintTheme and returns its ID + /// Registers a PaintTheme and returns its ID. /// - /// The string representing the `PaintTheme.AssetName` - /// Non-zero, unique Id if the theme was successfully registered, otherwise 0 + /// The string representing the `PaintTheme.AssetName`. + /// Non-zero, unique Id if the theme was successfully registered, otherwise 0. /// PaintThemes must be registered each time the client or server starts, registration is not persistent across sessions. uint RegisterPaintTheme(string assetName); /// - /// Unregisters a PaintTheme + /// Unregisters a PaintTheme. /// - /// The Id of the PaintTheme to be unregistered + /// The Id of the PaintTheme to be unregistered. void UnregisterPaintTheme(uint themeId); /// diff --git a/MultiplayerAPI/Interfaces/INetId.cs b/MultiplayerAPI/Interfaces/INetId.cs index bf920157..5fca80ea 100644 --- a/MultiplayerAPI/Interfaces/INetId.cs +++ b/MultiplayerAPI/Interfaces/INetId.cs @@ -9,7 +9,7 @@ namespace MPAPI.Interfaces; /// which are used to synchronise object references across the network. Only objects that are actively /// synchronised by Multiplayer mod will have associated network identifiers. /// -/// Additional objects from the base game will be added as Multiplayer features are implemented. If there are +/// Additional objects from the base-game will be added as Multiplayer features are implemented. If there are /// specific object types you would like to see supported, please create an issue on the Multiplayer Mod GitHub repository. /// public interface INetIdProvider diff --git a/MultiplayerAPI/Interfaces/IServer.cs b/MultiplayerAPI/Interfaces/IServer.cs index 4489f878..55f25a69 100644 --- a/MultiplayerAPI/Interfaces/IServer.cs +++ b/MultiplayerAPI/Interfaces/IServer.cs @@ -7,159 +7,165 @@ namespace MPAPI.Interfaces; /// -/// Represents the method that will handle chat command execution +/// Represents the method that will handle chat command execution. /// -/// The message content without the command prefix (e.g. '/command parameter1 parameter2' becomes 'parameter1 parameter2') -/// The player who executed the command +/// The message content without the command prefix (e.g. '/command parameter1 parameter2' becomes 'parameter1 parameter2'). +/// The player who executed the command. public delegate void ChatCommandCallback(string message, IPlayer sender); /// -/// Represents the method that will handle chat message filtering +/// Represents the method that will handle chat message filtering. /// -/// The message content that can be modified by reference -/// The player who sent the message -/// True to pass the message to the next filter or send to players; false to block the message from further processing +/// The message content that can be modified by reference. +/// The player who sent the message. +/// True to pass the message to the next filter or send to players; false to block the message from further processing. public delegate bool ChatFilterDelegate(ref string message, IPlayer sender); /// -/// Interface for interacting with Multiplayer mod server instances +/// Interface for interacting with Multiplayer mod server instances. /// public interface IServer { /// - /// Event fired when a player connects and is authenticated, but before the player receives game state information + /// Event fired when a player connects and is authenticated, but before the player receives game state information. /// /// - /// Event is not triggered for the host player + /// The event handler receives an object for the connected player. This event is not triggered for the local-client player on the host. /// event Action OnPlayerConnected; /// - /// Event fired when a player disconnects, but before the IPlayer object is destroyed + /// Event fired when a player disconnects, but before the object is destroyed. /// + /// + /// The event handler receives an object for the connected player. + /// event Action OnPlayerDisconnected; /// - /// Event fired when a player has signalled they are ready for game state information - /// This event occurs after the server has sent the game state, it does not guarantee the player has finished generating all cars, jobs, etc. + /// Event fired when a player has signalled they are ready for game state information. /// + /// + /// This event occurs after the server has sent the game state, it does not guarantee the player has finished generating all cars, jobs, etc. + /// event Action OnPlayerReady; #region Server Properties /// - /// Gets number of players currently connected to the server + /// Gets number of players currently connected to the server. /// - /// Positive integer representing the number of connected players + /// Positive integer representing the number of connected players. int PlayerCount { get; } /// - /// Gets IPlayer objects for all players connected to the server + /// Gets objects for all players connected to the server. /// - /// Read-only collection of IPlayer objects + /// Read-only collection of objects. public IReadOnlyCollection Players { get; } /// - /// Gets IPlayer for player by Id + /// Gets for player by Id. /// - /// Id for the player - /// IPlayer object if found, otherwise null + /// Id for the player. + /// object if found, otherwise null. IPlayer GetPlayer(byte id); #endregion #region Packet API /// - /// Register a packet type that uses automatic serialization + /// Register a packet type that uses automatic serialisation. /// - /// Packet type implementing IPacket - /// Handler to call when packet is received + /// Packet type implementing . + /// Handler to call when packet is received. void RegisterPacket(ServerPacketHandler handler) where T : class, IPacket, new(); /// - /// Register a packet type that uses manual serialization + /// Register a packet type that uses manual serialisation. /// - /// Packet type implementing ISerializablePacket - /// Handler to call when packet is received + /// Packet type implementing . + /// Handler to call when packet is received. void RegisterSerializablePacket(ServerPacketHandler handler) where T : class, ISerializablePacket, new(); /// - /// Send a packet to all connected players + /// Send a packet based on to all connected players. /// - /// Packet type - /// Packet to send - /// Whether to send reliably - /// Sends the packet to the local client when false, skips sending to the local client when true - /// Exclude this player + /// Packet type. + /// Packet to send. + /// Whether to send reliably. + /// Sends the packet to the local client when false, skips sending to the local client when true. + /// To be excluded from this broadcast. void SendPacketToAll(T packet, bool reliable = true, bool excludeSelf = false, IPlayer excludePlayer = null) where T : class, IPacket, new(); /// - /// Send a packet to all connected players + /// Send a packet based on to all connected players. /// - /// Packet type - /// Packet to send - /// Whether to send reliably - /// Sends the packet to the local client when false, skips sending to the local client when true - /// Exclude this player + /// Packet type. + /// Packet to send. + /// Whether to send reliably. + /// Sends the packet to the local client when false, skips sending to the local client when true. + /// To be excluded from this broadcast. void SendSerializablePacketToAll(T packet, bool reliable = true, bool excludeSelf = false, IPlayer excludePlayer = null) where T : class, ISerializablePacket, new(); /// - /// Send a packet to a specific player + /// Send a packet based on to a specific player. /// - /// Packet type - /// Packet to send - /// Target player - /// Whether to send reliably + /// Packet type. + /// Packet to send. + /// Target player. + /// Whether to send reliably. void SendPacketToPlayer(T packet, IPlayer player, bool reliable = true) where T : class, IPacket, new(); /// - /// Send a packet to a specific player + /// Send a packet based on to a specific player. /// - /// Packet type - /// Packet to send - /// Target player - /// Whether to send reliably + /// Packet type. + /// Packet to send. + /// Target player. + /// Whether to send reliably. void SendSerializablePacketToPlayer(T packet, IPlayer player, bool reliable = true) where T : class, ISerializablePacket, new(); #endregion #region Server Util /// - /// Returns the distance (Square Magnitude) of the closest player to a given GameObject + /// Returns the distance (Square Magnitude) of the closest player to a given GameObject. /// - /// GameObject to compare players against - /// Returns the distance (Square Magnitude) of the closest player, or float.MaxValue if no player is nearby + /// GameObject to compare players against. + /// Returns the distance (Square Magnitude) of the closest player, or float.MaxValue if no player is nearby. float AnyPlayerSqrMag(GameObject gameObject); /// - /// Returns the distance (Square Magnitude) of the closest player to a given point + /// Returns the distance (Square Magnitude) of the closest player to a given point. /// - /// Anchor point to compare players against - /// Returns the distance (Square Magnitude) of the closest player, or float.MaxValue if no player is nearby + /// Anchor point to compare players against. + /// Returns the distance (Square Magnitude) of the closest player, or float.MaxValue if no player is nearby. float AnyPlayerSqrMag(Vector3 anchor); #endregion #region Chat API /// - /// Sends a server chat message + /// Sends a server chat message. /// - /// Message to be sent - /// Player to exclude. If null, message will go to all players + /// Message to be sent. + /// Player to exclude. If null, message will go to all players. + /// Server chat messages are messsages sent from the server, not from a specific player and will be stylised as such. void SendServerChatMessage(string message, IPlayer excludePlayer = null); /// - /// Sends a chat message to a specific player + /// Sends a chat message to a specific player. /// - /// Message to be sent - /// Recipient player + /// Message to be sent. + /// Recipient player. void SendWhisperChatMessage(string message, IPlayer player); /// - /// Registers a chat command e.g. `/server` and optional short command '/s' + /// Registers a chat command e.g. `/server` and optional short command '/s'. /// - /// Command to be filtered for, without a leading '/' e.g. 'server' - /// Optional short command to be filtered for, without a leading '/' e.g. 's' - /// Optional callback for a help message e.g. \r\n\t\t/s "]]>. It is recommended to provide localisation/translation for this string + /// Command to be filtered for, without a leading '/' e.g. 'server'. + /// Optional short command to be filtered for, without a leading '/' e.g. 's'. + /// Optional callback for a help message e.g. \r\n\t\t/s "]]>. It is recommended to provide localisation/translation for this string. /// Action to execute when the command is triggered. First parameter contains message without the command e.g. '/command parameter1 parameter2' will become 'parameter1 parameter2', second parameter is the player who executed the command. /// True if the command was successfully registered, false if registration failed (e.g. command already exists). bool RegisterChatCommand(string commandLong, string commandShort, Func helpMessage, ChatCommandCallback callback); @@ -167,11 +173,11 @@ public interface IServer /// /// Registers a chat filter that processes non-command messages in registration order. - /// Filters form a chain where each can either allow the message to continue to the next filter or block further processing. - /// If all filters return true, the message will be sent to all players (default action). - /// This filter also applies to whispered messages, regardless of source (player or server) + /// Filters form a chain where each filter can either allow the message to continue to the next filter or block further processing. + /// If all filters return true, the message will be sent to all players (default action). + /// This filter also applies to whispered messages, regardless of source (player or server). /// - /// Filter function type `ChatFilterDelegate` that processes the message. First delegate parameter is the message content, second parameter is the player who sent the message. Return true to pass the message to the next filter/default action, false to block propagation. + /// Filter function type `ChatFilterDelegate` that processes the message. First delegate parameter is the message content, second parameter is the player who sent the message. Return true to pass the message to the next filter/default action, false to block propagation. void RegisterChatFilter(ChatFilterDelegate callback); #endregion diff --git a/MultiplayerAPI/Interfaces/Packets/IPacket.cs b/MultiplayerAPI/Interfaces/Packets/IPacket.cs index e8365e47..83db49e2 100644 --- a/MultiplayerAPI/Interfaces/Packets/IPacket.cs +++ b/MultiplayerAPI/Interfaces/Packets/IPacket.cs @@ -1,9 +1,9 @@ namespace MPAPI.Interfaces.Packets; /// -/// Base interface for packets using automatic serialization +/// Base interface for packets using automatic serialisation. /// public interface IPacket { - // Empty interface - Multiplayer Mod/LiteNetLib handles serialization automatically for you via public properties + // Empty interface - Multiplayer Mod/LiteNetLib handles serialisation automatically for you via public properties. } diff --git a/MultiplayerAPI/Interfaces/Packets/ISerializablePacket.cs b/MultiplayerAPI/Interfaces/Packets/ISerializablePacket.cs index 33eb4685..e5c082c0 100644 --- a/MultiplayerAPI/Interfaces/Packets/ISerializablePacket.cs +++ b/MultiplayerAPI/Interfaces/Packets/ISerializablePacket.cs @@ -3,20 +3,20 @@ namespace MPAPI.Interfaces.Packets; /// -/// Base interface for packets using manual serialization -/// Implementing classes must handle their own serialization/deserialization. +/// Base interface for packets using manual serialisation. +/// Implementing classes must handle their own serialisation/deserialisation. /// public interface ISerializablePacket { /// - /// Serialize the packet data to the provided writer + /// Serialise the packet data to the provided . /// - /// Writer to serialize data to + /// to serialise data to. void Serialize(BinaryWriter writer); /// - /// Deserialize the packet data from the provided reader + /// Deserialise the packet data from the provided . /// - /// Reader to deserialize data from + /// to deserialise data from. void Deserialize(BinaryReader reader); } diff --git a/MultiplayerAPI/MultiplayerAPI.cs b/MultiplayerAPI/MultiplayerAPI.cs index 0bd2e303..32b2cf61 100644 --- a/MultiplayerAPI/MultiplayerAPI.cs +++ b/MultiplayerAPI/MultiplayerAPI.cs @@ -85,24 +85,24 @@ public static string LoadedApiVersion public static bool IsMultiplayerLoaded => _instance != null; /// - /// Gets the current API instance (null if Multiplayer mod is not loaded). + /// Gets the current API instance (null if Multiplayer mod is not loaded). /// public static IMultiplayerAPI Instance => _instance; /// - /// Gets the current Server API instance (null if Multiplayer mod is not loaded or server not running). + /// Gets the current Server API instance (null if Multiplayer mod is not loaded or server not running). /// public static IServer Server => _server; /// - /// Gets the current Client API instance (null if Multiplayer mod is not loaded or client not running). + /// Gets the current Client API instance (null if Multiplayer mod is not loaded or client not running). /// public static IClient Client => _client; /// /// Internal method for the Multiplayer mod to register itself. /// - /// The API implementation + /// The API implementation. internal static void RegisterAPI(IMultiplayerAPI apiInstance) { _instance = apiInstance; @@ -130,7 +130,7 @@ internal static void ClearClient() /// /// Internal method for the Multiplayer mod to register a server instance. /// - /// The API implementation + /// The API implementation. internal static void RegisterServer(IServer server) { _server = server; From 2122c06ecd0a9c58bfc5f702ec26dc0b7812aa94 Mon Sep 17 00:00:00 2001 From: Macka Date: Sat, 8 Nov 2025 23:18:58 +1000 Subject: [PATCH 469/521] Add Car support to NetIdProvider and NetworkedTrainCar NetIdProvider now registers handlers for Car objects, enabling network ID retrieval and mapping for Car instances. NetworkedTrainCar adds TryGet and TryGetNetId methods for Car, allowing conversion between Car and network IDs. --- Multiplayer/API/NetIdProvider.cs | 1 + .../Networking/Train/NetworkedTrainCar.cs | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/Multiplayer/API/NetIdProvider.cs b/Multiplayer/API/NetIdProvider.cs index bfa73fce..19eecc52 100644 --- a/Multiplayer/API/NetIdProvider.cs +++ b/Multiplayer/API/NetIdProvider.cs @@ -22,6 +22,7 @@ protected override void Awake() { base.Awake(); RegisterHandler(NetworkedTrainCar.TryGetNetId, NetworkedTrainCar.TryGet); + RegisterHandler(NetworkedTrainCar.TryGetNetId, NetworkedTrainCar.TryGet); RegisterHandler(NetworkedJunction.TryGetNetId, NetworkedJunction.TryGet); RegisterHandler(NetworkedTurntable.TryGetNetId, NetworkedTurntable.TryGet); diff --git a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs index e80eac4a..33d13e83 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs @@ -1,5 +1,6 @@ using DV.CabControls; using DV.Customization.Paint; +using DV.Logic.Job; using DV.MultipleUnit; using DV.Simulation.Brake; using DV.Simulation.Cars; @@ -44,6 +45,13 @@ public static bool TryGet(ushort netId, out TrainCar trainCar) return b; } + public static bool TryGet(ushort netId, out Car trainCar) + { + bool b = TryGet(netId, out NetworkedTrainCar networkedTrainCar); + trainCar = b ? networkedTrainCar.TrainCar?.logicCar : null; + return b; + } + public static bool TryGetCoupler(HoseAndCock hoseAndCock, out Coupler coupler) { return hoseToCoupler.TryGetValue(hoseAndCock, out coupler); @@ -74,6 +82,17 @@ public static bool TryGetNetId(TrainCar trainCar, out ushort netId) return true; } + public static bool TryGetNetId(Car car, out ushort netId) + { + netId = 0; + + if (car == null || !GetFromTrainId(car.ID, out var networkedTrainCar) || networkedTrainCar == false || networkedTrainCar.NetId == 0) + return false; + + netId = networkedTrainCar.NetId; + return true; + } + #endregion private const int MAX_COUPLER_ITERATIONS = 10; From d6eabd75c60731c4129d317ad4c5aaeb4d84b5e5 Mon Sep 17 00:00:00 2001 From: Macka Date: Sat, 8 Nov 2025 23:19:28 +1000 Subject: [PATCH 470/521] Add script to convert docs for GitHub Wiki Introduces Convert-ToGitHubWiki.ps1 to process XMLDoc2Markdown output for GitHub Wiki compatibility, including link and structure adjustments. Updates .gitignore to exclude /docs, and modifies the csproj post-build steps to automate documentation generation and conversion, with links now pointing to the API overview in the wiki. --- .gitignore | 1 + Convert-ToGitHubWiki.ps1 | 348 +++++++++++++++++++++++++++ MultiplayerAPI/MultiplayerAPI.csproj | 9 +- 3 files changed, 357 insertions(+), 1 deletion(-) create mode 100644 Convert-ToGitHubWiki.ps1 diff --git a/.gitignore b/.gitignore index d792194b..22093f15 100644 --- a/.gitignore +++ b/.gitignore @@ -308,3 +308,4 @@ MultiplayerAssets/ProjectSettings/* !MultiplayerAssets/Packages /Lobby Servers/Rust Server/target *.pem +/docs diff --git a/Convert-ToGitHubWiki.ps1 b/Convert-ToGitHubWiki.ps1 new file mode 100644 index 00000000..1dea901a --- /dev/null +++ b/Convert-ToGitHubWiki.ps1 @@ -0,0 +1,348 @@ +param( + [Parameter(Mandatory=$false)] + [string]$InputPath = ".", + + [Parameter(Mandatory=$false)] + [string]$OutputPath = "./wiki-output", + + [Parameter(Mandatory=$false)] + [switch]$Flatten, + + [Parameter(Mandatory=$false)] + [switch]$InPlace +) + +<# +.SYNOPSIS + Converts XMLDoc2Markdown output to GitHub Wiki compatible format. + +.DESCRIPTION + This script processes markdown files generated by XMLDoc2Markdown and converts them + to be compatible with GitHub Wiki by: + - Removing .md extensions from links + - Removing ./ prefixes from links + - Optionally flattening all files into a single markdown file + - Optionally modifying files in place + +.NOTES + Created with assistance from GitHub Copilot + +.PARAMETER InputPath + The path to the directory containing the markdown files. Default is current directory. + +.PARAMETER OutputPath + The path where converted files will be saved. Default is ./wiki-output. + Ignored if -InPlace is used. + +.PARAMETER Flatten + If specified, all markdown files will be combined into a single file. + +.PARAMETER InPlace + If specified, modifies files in place instead of creating copies. + +.EXAMPLE + .\Convert-ToGitHubWiki.ps1 + Converts all markdown files in current directory to wiki-output folder. + +.EXAMPLE + .\Convert-ToGitHubWiki.ps1 -InPlace + Modifies all markdown files in place. + +.EXAMPLE + .\Convert-ToGitHubWiki.ps1 -Flatten -OutputPath "./wiki.md" + Combines all markdown files into a single wiki.md file. +#> + +function Convert-MarkdownLinks { + param( + [string]$Content, + [bool]$UseAnchors = $false + ) + + if ($UseAnchors) { + # Convert relative links to anchor links for flattened output + # Match patterns like [Text](./path/file.md) or [Text](path/file.md) + $Content = [regex]::Replace($Content, '\[([^\]]+)\]\(\.?/?([^)]+?)(?:\.md)?\)', { + param($match) + $linkText = $match.Groups[1].Value + $linkPath = $match.Groups[2].Value + + # Skip external links (http/https) + if ($linkPath -match '^https?://') { + return $match.Value + } + + # Convert path to anchor + # Remove path separators and convert to lowercase + $anchor = $linkPath -replace '[/\\]', '-' -replace '[^a-zA-Z0-9\s-]', '' -replace '\s+', '-' -replace '-+', '-' + $anchor = $anchor.ToLower().Trim('-') + + return "[$linkText](#$anchor)" + }) + } else { + # For GitHub Wiki: Convert links to use only filename (no directory path) + # Match patterns like [Text](./path/to/file.md) or [Text](path/to/file.md#anchor) + $Content = [regex]::Replace($Content, '\[([^\]]+)\]\(\.?/?([^)#]+?)(?:\.md)?(#[^)]+)?\)', { + param($match) + $linkText = $match.Groups[1].Value + $linkPath = $match.Groups[2].Value + $anchor = $match.Groups[3].Value + + # Skip external links (http/https) + if ($linkPath -match '^https?://') { + return $match.Value + } + + # Extract just the filename from the path (remove directory) + $filename = Split-Path -Path $linkPath -Leaf + + # Return with just filename and optional anchor + return "[$linkText]($filename$anchor)" + }) + } + + return $Content +} + +function Get-MarkdownFiles { + param( + [string]$Path, + [string]$ExcludePath = $null, + [switch]$ExcludeIndex + ) + + $files = Get-ChildItem -Path $Path -Filter "*.md" -Recurse | Where-Object { $_.Name -ne "README.md" } + + # Exclude index.md if requested (for flattened output which generates its own TOC) + if ($ExcludeIndex) { + $files = $files | Where-Object { $_.Name -ne "index.md" } + } + + # Exclude files in the output directory + if ($ExcludePath) { + $excludeFullPath = (Resolve-Path -Path $ExcludePath -ErrorAction SilentlyContinue) + if ($excludeFullPath) { + $files = $files | Where-Object { !$_.FullName.StartsWith($excludeFullPath) } + } + } + + return $files +} + +function Flatten-ToSingleFile { + param( + [string]$InputPath, + [string]$OutputFile + ) + + Write-Host "Flattening markdown files into single document..." -ForegroundColor Cyan + + # Get output directory to exclude it from processing + $outputDir = Split-Path -Path $OutputFile -Parent + if ($outputDir) { + $outputDir = Join-Path -Path $InputPath -ChildPath $outputDir + } + + $files = Get-MarkdownFiles -Path $InputPath -ExcludePath $outputDir -ExcludeIndex | Sort-Object FullName + + $combinedContent = @() + + # Build mapping of file paths to their heading anchors (filename -> heading anchor) + $fileToAnchor = @{} + + foreach ($file in $files) { + $relativePath = $file.FullName.Substring($InputPath.Length).TrimStart('\', '/') + $content = Get-Content -Path $file.FullName -Raw -Encoding UTF8 + + # Extract first heading + if ($content -match '^#\s+(.+)') { + $heading = $matches[1].Trim() + $anchor = $heading -replace '[^a-zA-Z0-9\s-]', '' -replace '\s+', '-' -replace '-+', '-' + $anchor = $anchor.ToLower().Trim('-') + + # Store mapping: filename (without extension) -> anchor + $filename = [System.IO.Path]::GetFileNameWithoutExtension($file.Name) + $fileToAnchor[$filename.ToLower()] = $anchor + + # Also store full relative path mapping (with forward slashes, no extension) + $pathWithoutExt = $relativePath -replace '\.md$', '' -replace '\\', '/' + $fileToAnchor[$pathWithoutExt.ToLower()] = $anchor + } + } + + # Add table of contents header + $combinedContent += "## Table of Contents" + $combinedContent += "" + + # Build hierarchical TOC based on namespace from files + $tocStructure = @{} + + foreach ($file in $files) { + $relativePath = $file.FullName.Substring($InputPath.Length).TrimStart('\', '/') + $content = Get-Content -Path $file.FullName -Raw -Encoding UTF8 + + # Extract first heading and namespace + $heading = $null + $namespace = $null + + if ($content -match '^#\s+(.+)') { + $heading = $matches[1].Trim() + } + + if ($content -match 'Namespace:\s*([^\r\n]+)') { + $namespace = $matches[1].Trim() + } + + if ($heading) { + $anchor = $heading -replace '[^a-zA-Z0-9\s-]', '' -replace '\s+', '-' -replace '-+', '-' + $anchor = $anchor.ToLower().Trim('-') + + # Use namespace if available, otherwise use root + $category = if ($namespace) { $namespace } else { '_root' } + + if (!$tocStructure.ContainsKey($category)) { + $tocStructure[$category] = @() + } + $tocStructure[$category] += @{ Heading = $heading; Anchor = $anchor } + } + } + + # Output TOC with sections + $orderedCategories = $tocStructure.Keys | Sort-Object | Where-Object { $_ -ne '_root' } + + # Root items first if any + if ($tocStructure.ContainsKey('_root')) { + foreach ($item in $tocStructure['_root']) { + $combinedContent += "- [$($item.Heading)](#$($item.Anchor))" + } + $combinedContent += "" + } + + # Then categorized items + foreach ($category in $orderedCategories) { + $combinedContent += "### $category" + $combinedContent += "" + foreach ($item in $tocStructure[$category]) { + $combinedContent += "- [$($item.Heading)](#$($item.Anchor))" + } + $combinedContent += "" + } + + $combinedContent += "---" + $combinedContent += "

    " + $combinedContent += "" + + # Add content from each file + foreach ($file in $files) { + $relativePath = $file.FullName.Substring($InputPath.Length).TrimStart('\', '/') + Write-Host " Processing: $relativePath" + + $content = Get-Content -Path $file.FullName -Raw -Encoding UTF8 + + # Convert links by replacing each known path with its anchor + foreach ($path in $fileToAnchor.Keys) { + $anchor = $fileToAnchor[$path] + $escapedPath = [regex]::Escape($path) + # Replace links like [Text](path) with [Text](#anchor) + $content = $content -replace "\]\($escapedPath\)", "](#$anchor)" + # Also handle links with existing fragment anchors - preserve the fragment + # but this is tricky, so for now just do the simple case + } + + # Add source file comment + $combinedContent += "" + $combinedContent += "" + + # Add content + $combinedContent += $content.TrimEnd() + $combinedContent += "" + $combinedContent += "---" + $combinedContent += "

    " + $combinedContent += "" + } + + # Ensure output directory exists + if ($outputDir -and !(Test-Path -Path $outputDir)) { + New-Item -Path $outputDir -ItemType Directory -Force | Out-Null + } + + # Write combined file + $combinedContent -join "`n" | Set-Content -Path $OutputFile -Encoding UTF8 -NoNewline + + Write-Host "Created flattened file: $OutputFile" -ForegroundColor Green + Write-Host "Processed $($files.Count) files" -ForegroundColor Green +} + +function Convert-Files { + param( + [string]$InputPath, + [string]$OutputPath, + [bool]$InPlace + ) + + Write-Host "Converting markdown files for GitHub Wiki..." -ForegroundColor Cyan + + $files = Get-MarkdownFiles -Path $InputPath + $count = 0 + + foreach ($file in $files) { + $relativePath = $file.FullName.Substring($InputPath.Length).TrimStart('\', '/') + Write-Host " Processing: $relativePath" + + $content = Get-Content -Path $file.FullName -Raw -Encoding UTF8 + $convertedContent = Convert-MarkdownLinks -Content $content + + if ($InPlace) { + $outputFile = $file.FullName + } else { + $outputFile = Join-Path -Path $OutputPath -ChildPath $relativePath + $outputDir = Split-Path -Path $outputFile -Parent + + if (!(Test-Path -Path $outputDir)) { + New-Item -Path $outputDir -ItemType Directory -Force | Out-Null + } + } + + $convertedContent | Set-Content -Path $outputFile -Encoding UTF8 -NoNewline + $count++ + } + + if ($InPlace) { + Write-Host "Modified $count files in place" -ForegroundColor Green + } else { + Write-Host "Converted $count files to: $OutputPath" -ForegroundColor Green + } +} + +# Main execution +try { + # Resolve full path + $InputPath = Resolve-Path -Path $InputPath -ErrorAction Stop + + if ($Flatten) { + # Determine output file path + if ($OutputPath -like "*.md") { + $flattenedFile = $OutputPath + } else { + $flattenedFile = Join-Path -Path $OutputPath -ChildPath "API-Documentation.md" + } + + Flatten-ToSingleFile -InputPath $InputPath -OutputFile $flattenedFile + } else { + if ($InPlace) { + $confirmation = Read-Host "This will modify files in place. Continue? (y/n)" + if ($confirmation -ne 'y') { + Write-Host "Operation cancelled." -ForegroundColor Yellow + exit + } + } + + Convert-Files -InputPath $InputPath -OutputPath $OutputPath -InPlace $InPlace + } + + Write-Host "`nConversion complete!" -ForegroundColor Green + +} catch { + Write-Host "Error: $_" -ForegroundColor Red + exit 1 +} diff --git a/MultiplayerAPI/MultiplayerAPI.csproj b/MultiplayerAPI/MultiplayerAPI.csproj index 93a430ac..ea4372c2 100644 --- a/MultiplayerAPI/MultiplayerAPI.csproj +++ b/MultiplayerAPI/MultiplayerAPI.csproj @@ -14,7 +14,7 @@ Macka API for interfacing with DV Multiplayer mod. Provides events and interfaces for server/client interactions in Derail Valley multiplayer scenarios. derail-valley;multiplayer;gaming;api;mod - https://github.com/AMacro/dv-multiplayer + https://github.com/AMacro/dv-multiplayer/wiki/API-Overview https://github.com/AMacro/dv-multiplayer git MIT @@ -73,7 +73,14 @@ + + + + + + + From f7333d2ccb8de4303331d2ea06222636ee2d26af Mon Sep 17 00:00:00 2001 From: Macka Date: Sun, 9 Nov 2025 14:41:10 +1000 Subject: [PATCH 471/521] Add debug patch checking and bump version to 0.1.12.9 Introduced a CheckPatches method for debugging Harmony patches, enabled Harmony debug mode in DEBUG builds, and updated project and info.json version to 0.1.12.9. --- Multiplayer/Multiplayer.cs | 30 ++++++++++++++++++++++++++++++ Multiplayer/Multiplayer.csproj | 2 +- info.json | 2 +- 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/Multiplayer/Multiplayer.cs b/Multiplayer/Multiplayer.cs index 6c0de175..26773f76 100644 --- a/Multiplayer/Multiplayer.cs +++ b/Multiplayer/Multiplayer.cs @@ -14,6 +14,7 @@ using System.IO; using System.Linq; using System.Reflection; +using System.Text; using UnityChan; using UnityEngine; using UnityModManagerNet; @@ -89,6 +90,9 @@ public static bool Load(UnityModManager.ModEntry modEntry) Log("Patching..."); harmony = new Harmony(ModEntry.Info.Id); +#if DEBUG + Harmony.DEBUG = true; +#endif harmony.PatchAll(); SimComponent_Tick_Patch.Patch(harmony); @@ -116,6 +120,11 @@ public static bool Load(UnityModManager.ModEntry modEntry) Log("Loading API Provider..."); _apiProvider = new APIProvider(); MultiplayerAPI.RegisterAPI(_apiProvider); + +#if DEBUG + CheckPatches(); +#endif + } catch (Exception ex) { @@ -159,6 +168,9 @@ private static void LateUpdate(UnityModManager.ModEntry modEntry, float deltaTim { if (ModEntry.NewestVersion != null && ModEntry.NewestVersion.ToString() != "") { +#if DEBUG + CheckPatches(); +#endif Log($"Multiplayer Latest Version: {ModEntry.NewestVersion}"); ModEntry.OnLateUpdate -= Multiplayer.LateUpdate; @@ -214,6 +226,24 @@ static string LiteNetLibVer() return assemblyName.Version.ToString(); } +#if DEBUG + public static void CheckPatches() + { + StringBuilder sb = new StringBuilder("Harmony patches:"); + foreach (var info in Harmony.GetAllPatchedMethods()) + { + var patches = Harmony.GetPatchInfo(info); + sb.Append($"\r\n- {info.DeclaringType.FullName}.{info.Name} patched by:"); + foreach (var p in patches.Prefixes) + sb.Append($"\r\n - Prefix: {p.PatchMethod.DeclaringType.FullName}.{p.PatchMethod.Name}"); + foreach (var p in patches.Postfixes) + sb.Append($"\r\n - Postfix: {p.PatchMethod.DeclaringType.FullName}.{p.PatchMethod.Name}"); + } + + LogDebug(()=>sb.ToString()); + } +#endif + #region Logging diff --git a/Multiplayer/Multiplayer.csproj b/Multiplayer/Multiplayer.csproj index 61307315..941c6dcc 100644 --- a/Multiplayer/Multiplayer.csproj +++ b/Multiplayer/Multiplayer.csproj @@ -3,7 +3,7 @@ net48 latest Multiplayer - 0.1.12.8 + 0.1.12.9 diff --git a/info.json b/info.json index 1307d35a..3097b46c 100644 --- a/info.json +++ b/info.json @@ -1,6 +1,6 @@ { "Id": "Multiplayer", - "Version": "0.1.12.8", + "Version": "0.1.12.9", "DisplayName": "Multiplayer", "Author": "Insprill, Macka, Morm", "EntryMethod": "Multiplayer.Multiplayer.Load", From f82239ee9dffe649b9d585945533ea73d281011c Mon Sep 17 00:00:00 2001 From: Macka Date: Sun, 9 Nov 2025 17:02:50 +1000 Subject: [PATCH 472/521] Refactor player identification to use PlayerId property Replaces usage of the 'Id' property with 'PlayerId' for player identification across networking components. Updates event handler signatures to use ServerPlayer objects instead of raw player IDs, and replaces 'Get' with 'TryGet' for NetworkedTrainCar retrieval to improve clarity and safety. --- .../World/NetworkedCashRegisterWithModules.cs | 2 +- .../World/NetworkedPitStopStation.cs | 13 ++++++------ .../World/NetworkedPluggableObject.cs | 20 +++++++++---------- .../Networking/Data/PitStopPlugData.cs | 2 +- .../Managers/Server/CullingManager.cs | 10 +++++----- 5 files changed, 24 insertions(+), 23 deletions(-) diff --git a/Multiplayer/Components/Networking/World/NetworkedCashRegisterWithModules.cs b/Multiplayer/Components/Networking/World/NetworkedCashRegisterWithModules.cs index 6aa93d81..4cc62b00 100644 --- a/Multiplayer/Components/Networking/World/NetworkedCashRegisterWithModules.cs +++ b/Multiplayer/Components/Networking/World/NetworkedCashRegisterWithModules.cs @@ -156,7 +156,7 @@ public void Server_ProcessCashRegisterAction(ServerPlayer player, CommonCashRegi success = Inventory.Instance.RemoveMoney(spend); - if(success && player.Id != NetworkLifecycle.Instance.Server.SelfId) + if(success && player.PlayerId != NetworkLifecycle.Instance.Server.SelfId) CashRegister?.AddCash(spend); } else diff --git a/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs b/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs index 0921e5d8..c226d775 100644 --- a/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs +++ b/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs @@ -16,6 +16,7 @@ using System.Text; using UnityEngine; using static CashRegisterModule; +using static DV.Interaction.Inputs.InputManager; namespace Multiplayer.Components.Networking.World; @@ -148,7 +149,7 @@ protected override void Awake() // Setup network events NetworkLifecycle.Instance.OnTick += OnTick; - NetworkLifecycle.Instance.Server.PlayerDisconnect += OnPlayerDisconnect; + NetworkLifecycle.Instance.Server.PlayerDisconnected += OnPlayerDisconnect; // Ensure host can interact Refreshed = true; @@ -187,7 +188,7 @@ protected override void OnDestroy() NetworkLifecycle.Instance.OnTick -= OnTick; - NetworkLifecycle.Instance.Server.PlayerDisconnect -= OnPlayerDisconnect; + NetworkLifecycle.Instance.Server.PlayerDisconnected -= OnPlayerDisconnect; // Monitor changes to vehicles in the pit stop Station.pitstop.CarEntered -= OnCarPitStopEntered; @@ -246,7 +247,7 @@ public bool ValidateInteraction(CommonPitStopInteractionPacket packet, ServerPla } //todo: update when merged with ModAPI branch - public void OnPlayerDisconnect(uint playerId) + public void OnPlayerDisconnect(ServerPlayer player) { //todo: when a player disconnects, if they are interacting with a lever, cancel the interaction //Multiplayer.LogWarning($"OnPlayerDisconnect()"); @@ -309,7 +310,7 @@ public void OnPlayerEnteredCullingRegion(ServerPlayer player) public void ProcessInteractionPacketAsHost(CommonPitStopInteractionPacket packet, ServerPlayer senderPlayer) { - Multiplayer.LogDebug(() => $"NetworkedPitStopStation.ProcessInteractionPacketAsHost() from: {senderPlayer.Username}, id: {senderPlayer.Id}, selfpeer: {NetworkLifecycle.Instance.Server.SelfId}"); + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.ProcessInteractionPacketAsHost() from: {senderPlayer.Username}, id: {senderPlayer.PlayerId}, selfpeer: {NetworkLifecycle.Instance.Server.SelfId}"); if (ValidateInteraction(packet, senderPlayer)) { @@ -317,7 +318,7 @@ public void ProcessInteractionPacketAsHost(CommonPitStopInteractionPacket packet OnCarPitStopEntered(); processingAsHost = true; - if (senderPlayer.Id != NetworkLifecycle.Instance.Server.SelfId) + if (senderPlayer.PlayerId != NetworkLifecycle.Instance.Server.SelfId) { Multiplayer.LogDebug(() => $"NetworkedPitStopStation.ProcessInteractionPacketAsHost() ProcessPacketAsClient()"); ProcessInteractionPacketAsClient(packet); @@ -327,7 +328,7 @@ public void ProcessInteractionPacketAsHost(CommonPitStopInteractionPacket packet // Send to all other players foreach (var player in CullingManager.ActivePlayers) { - if (player.Id != senderPlayer.Id) + if (player.PlayerId != senderPlayer.PlayerId) { Multiplayer.LogDebug(() => $"NetworkedPitStopStation.ProcessInteractionPacketAsHost() sending to player: {player.Username}"); NetworkLifecycle.Instance.Server.SendPitStopInteractionPacket(player, packet); diff --git a/Multiplayer/Components/Networking/World/NetworkedPluggableObject.cs b/Multiplayer/Components/Networking/World/NetworkedPluggableObject.cs index de570f62..91bd55bf 100644 --- a/Multiplayer/Components/Networking/World/NetworkedPluggableObject.cs +++ b/Multiplayer/Components/Networking/World/NetworkedPluggableObject.cs @@ -86,7 +86,7 @@ protected override void Awake() if (NetworkLifecycle.Instance.IsHost()) { - NetworkLifecycle.Instance.Server.PlayerDisconnect += OnPlayerDisconnected; + NetworkLifecycle.Instance.Server.PlayerDisconnected += OnPlayerDisconnected; Refreshed = true; } @@ -136,7 +136,7 @@ protected override void OnDestroy() if (NetworkLifecycle.Instance.IsHost()) { - NetworkLifecycle.Instance.Server.PlayerDisconnect -= OnPlayerDisconnected; + NetworkLifecycle.Instance.Server.PlayerDisconnected -= OnPlayerDisconnected; } base.OnDestroy(); @@ -220,10 +220,10 @@ public void ProcessInteractionPacketAsHost(CommonPitStopPlugInteractionPacket pa break; } - packet.PlayerId = senderPlayer.Id; + packet.PlayerId = senderPlayer.PlayerId; //Allow host to process packet if not from a local client - if (!NetworkLifecycle.Instance.IsClientRunning || (NetworkLifecycle.Instance.IsClientRunning && senderPlayer.Id != NetworkLifecycle.Instance.Server.SelfId)) + if (!NetworkLifecycle.Instance.IsClientRunning || (NetworkLifecycle.Instance.IsClientRunning && senderPlayer.PlayerId != NetworkLifecycle.Instance.Server.SelfId)) { processingAsHost = true; ProcessPacket(packet); @@ -235,7 +235,7 @@ public void ProcessInteractionPacketAsHost(CommonPitStopPlugInteractionPacket pa foreach (var player in Station.CullingManager.ActivePlayers) { - if (player.Id != senderPlayer.Id) + if (player.PlayerId != senderPlayer.PlayerId) { Multiplayer.LogDebug(() => $"NetworkedPluggableObject.ProcessInteractionPacketAsHost() Sending interaction packet to player: {player.Username}"); NetworkLifecycle.Instance.Server.SendPitStopPlugInteractionPacket(player, packet); @@ -282,7 +282,7 @@ public bool ValidateInteraction(CommonPitStopPlugInteractionPacket packet, Serve { //verify TrainCar - if (packet.TrainCarNetId == 0 || !NetworkedTrainCar.Get(packet.TrainCarNetId, out var networkedTrainCar) || networkedTrainCar == null) + if (packet.TrainCarNetId == 0 || !NetworkedTrainCar.TryGet(packet.TrainCarNetId, out NetworkedTrainCar networkedTrainCar) || networkedTrainCar == null) { Multiplayer.LogDebug(() => $"NetworkedPluggableObject.ValidateInteraction() NetId: {NetId}, trainCarNetId: {packet.TrainCarNetId}, NetworkedTrainCar not found!"); return false; @@ -417,9 +417,9 @@ public void SnappedByRope() NetworkLifecycle.Instance.Server.SendPitStopPlugInteractionPacket(player, packet); } - private void OnPlayerDisconnected(uint disconnectedPlayerId) + private void OnPlayerDisconnected(ServerPlayer disconnectedPlayer) { - if (HeldBy == null || HeldBy.Id != disconnectedPlayerId) + if (HeldBy == null || HeldBy != disconnectedPlayer) return; HeldBy = null; @@ -437,7 +437,7 @@ private void OnPlayerDisconnected(uint disconnectedPlayerId) foreach (var player in Station.CullingManager.ActivePlayers) { - if (player.Id != disconnectedPlayerId && player.Id != NetworkLifecycle.Instance.Server.SelfId) + if (player != disconnectedPlayer && player.PlayerId != NetworkLifecycle.Instance.Server.SelfId) NetworkLifecycle.Instance.Server.SendPitStopPlugInteractionPacket(player, packet); } } @@ -625,7 +625,7 @@ private void DockTrainCar(ushort trainNetId, sbyte socketIndex) { DropPlug(); - if (NetworkedTrainCar.Get(trainNetId, out var netTrainCar)) + if (NetworkedTrainCar.TryGet(trainNetId, out NetworkedTrainCar netTrainCar)) { var socket = GetTrainCarSocket(netTrainCar, socketIndex); if (socket == null) diff --git a/Multiplayer/Networking/Data/PitStopPlugData.cs b/Multiplayer/Networking/Data/PitStopPlugData.cs index 320c73b3..e43da4f7 100644 --- a/Multiplayer/Networking/Data/PitStopPlugData.cs +++ b/Multiplayer/Networking/Data/PitStopPlugData.cs @@ -25,7 +25,7 @@ public static PitStopPlugData From(NetworkedPluggableObject plugData, bool bulk ( plugData.NetId, interaction, - plugData.HeldBy?.Id ?? 0, + plugData.HeldBy?.PlayerId ?? 0, plugData.TrainCarNetId, plugData.SocketIndex, plugData.transform.GetWorldAbsolutePosition(), diff --git a/Multiplayer/Networking/Managers/Server/CullingManager.cs b/Multiplayer/Networking/Managers/Server/CullingManager.cs index 7b7a4552..e2d6edf8 100644 --- a/Multiplayer/Networking/Managers/Server/CullingManager.cs +++ b/Multiplayer/Networking/Managers/Server/CullingManager.cs @@ -47,7 +47,7 @@ public CullingManager(float checkInterval, float cullSqrDistance, float activati checkCoro = CoroutineManager.Instance.StartCoroutine(PlayerDistanceChecker()); - NetworkLifecycle.Instance.Server.PlayerDisconnect += OnPlayerDisconnected; + NetworkLifecycle.Instance.Server.PlayerDisconnected += OnPlayerDisconnected; } public void Dispose() @@ -55,13 +55,13 @@ public void Dispose() if (checkCoro != null) CoroutineManager.Instance.Stop(checkCoro); - NetworkLifecycle.Instance.Server.PlayerDisconnect -= OnPlayerDisconnected; + NetworkLifecycle.Instance.Server.PlayerDisconnected -= OnPlayerDisconnected; } //todo: fix when merged with ModAPI branch - private void OnPlayerDisconnected(uint playerId) + private void OnPlayerDisconnected(ServerPlayer serverPlayer) { - var player = playerToLastNearbyTime.Keys.Where(p => p.Id == playerId).FirstOrDefault(); + var player = playerToLastNearbyTime.Keys.Where(p => p == serverPlayer).FirstOrDefault(); if (player == null) return; @@ -83,7 +83,7 @@ private IEnumerator PlayerDistanceChecker() { foreach (var player in NetworkLifecycle.Instance.Server.ServerPlayers) { - if (player.Id == NetworkLifecycle.Instance.Server.SelfId || !player.IsLoaded) + if (player.PlayerId == NetworkLifecycle.Instance.Server.SelfId || !player.IsLoaded) continue; float sqrDistance = (player.WorldPosition - _referenceObject.transform.position).sqrMagnitude; From a44b1f81c0d42e2b9aa0ad4a08a63fe40f792fa7 Mon Sep 17 00:00:00 2001 From: Macka Date: Sat, 15 Nov 2025 12:28:07 +1000 Subject: [PATCH 473/521] Refactor Fnv1aHash to StringHashing utility class Moved the Fnv1aHash implementation from PaintThemeLookup to a new internal utility class StringHashing. Updated PaintThemeLookup to use StringHashing.Fnv1aHash. Added overloads to APIProvider for uint netId support. --- Multiplayer/API/APIProvider.cs | 11 +++++++++++ .../Networking/Train/PaintThemeLookup.cs | 18 ++---------------- Multiplayer/Utils/StringHashing.cs | 19 +++++++++++++++++++ 3 files changed, 32 insertions(+), 16 deletions(-) create mode 100644 Multiplayer/Utils/StringHashing.cs diff --git a/Multiplayer/API/APIProvider.cs b/Multiplayer/API/APIProvider.cs index f48d4ef8..3c096689 100644 --- a/Multiplayer/API/APIProvider.cs +++ b/Multiplayer/API/APIProvider.cs @@ -36,11 +36,22 @@ public bool TryGetNetId(T obj, out ushort netId) where T : class return NetIdProvider.Instance.TryGetNetId(obj, out netId); } + + public bool TryGetNetId(T obj, out uint netId) where T : class + { + return NetIdProvider.Instance.TryGetNetId(obj, out netId); + } + public bool TryGetObjectFromNetId(ushort netId, out T obj) where T : class { return NetIdProvider.Instance.TryGetObject(netId, out obj); } + public bool TryGetObjectFromNetId(uint netId, out T obj) where T : class + { + return NetIdProvider.Instance.TryGetObject(netId, out obj); + } + public void SetModCompatibility(string modId, MultiplayerCompatibility compatibility) { ModCompatibilityManager.Instance.RegisterCompatibility(modId, compatibility); diff --git a/Multiplayer/Components/Networking/Train/PaintThemeLookup.cs b/Multiplayer/Components/Networking/Train/PaintThemeLookup.cs index c74d2f09..2294cf8a 100644 --- a/Multiplayer/Components/Networking/Train/PaintThemeLookup.cs +++ b/Multiplayer/Components/Networking/Train/PaintThemeLookup.cs @@ -1,6 +1,7 @@ using DV.Customization.Paint; using DV.Utils; using JetBrains.Annotations; +using Multiplayer.Utils; using System.Collections.Generic; using System.Linq; using UnityEngine; @@ -60,7 +61,7 @@ public uint GetThemeId(string themeName) if (string.IsNullOrEmpty(themeName)) return 0; - return Fnv1aHash(themeName); + return StringHashing.Fnv1aHash(themeName); } public uint RegisterTheme(string themeName) @@ -104,21 +105,6 @@ public void UnregisterTheme(string themeName) } - private uint Fnv1aHash(string text) - { - unchecked - { - const uint fnvPrime = 0x01000193; - uint hash = 0x811C9DC5; - foreach (char c in text) - { - hash ^= c; - hash *= fnvPrime; - } - return hash; - } - } - [UsedImplicitly] public new static string AllowAutoCreate() { diff --git a/Multiplayer/Utils/StringHashing.cs b/Multiplayer/Utils/StringHashing.cs new file mode 100644 index 00000000..4d0a7b57 --- /dev/null +++ b/Multiplayer/Utils/StringHashing.cs @@ -0,0 +1,19 @@ +namespace Multiplayer.Utils; + +internal static class StringHashing +{ + public static uint Fnv1aHash(string text) + { + unchecked + { + const uint fnvPrime = 0x01000193; + uint hash = 0x811C9DC5; + foreach (char c in text) + { + hash ^= c; + hash *= fnvPrime; + } + return hash; + } + } +} From 9f1ad266c15de6dc9181fc88a3140325967030c8 Mon Sep 17 00:00:00 2001 From: Macka Date: Sat, 15 Nov 2025 12:28:58 +1000 Subject: [PATCH 474/521] Add CargoTypeLookup for networked cargo type mapping Map cargo types to network Ids and vice versa. Provides methods for registering and retrieving cargo types and their network identifiers, supporting multiplayer synchronization. --- .../Networking/Train/CargoTypeLookup.cs | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 Multiplayer/Components/Networking/Train/CargoTypeLookup.cs diff --git a/Multiplayer/Components/Networking/Train/CargoTypeLookup.cs b/Multiplayer/Components/Networking/Train/CargoTypeLookup.cs new file mode 100644 index 00000000..2b36e079 --- /dev/null +++ b/Multiplayer/Components/Networking/Train/CargoTypeLookup.cs @@ -0,0 +1,99 @@ +using DV; +using DV.ThingTypes; +using DV.ThingTypes.TransitionHelpers; +using DV.Utils; +using JetBrains.Annotations; +using Multiplayer.Utils; +using System.Collections.Generic; +using System.Linq; + +namespace Multiplayer.Components.Networking.Train; + +public class CargoTypeLookup : SingletonBehaviour +{ + private readonly Dictionary hashToCargoTypeV2 = []; + private readonly Dictionary cargoTypeV2ToHash = []; + + protected override void Awake() + { + base.Awake(); + + hashToCargoTypeV2.Clear(); + cargoTypeV2ToHash.Clear(); + + RebuildCache(); + } + + protected void RebuildCache() + { + var missingCargoTypes = Globals.G.Types.cargos.Where(c => !cargoTypeV2ToHash.ContainsKey(c)); + + if (!missingCargoTypes.Any()) + return; + + Multiplayer.LogDebug(() => $"CargoTypeLookup: Found {missingCargoTypes.Count()} missing cargo types, registering..."); + + foreach (var cargoType in missingCargoTypes) + TryGetNetId(cargoType, out _); + } + + public bool TryGet(uint netId, out CargoType_v2 cargoType) + { + if (hashToCargoTypeV2.TryGetValue(netId, out cargoType)) + return true; + + Multiplayer.LogWarning($"CargoTypeLookup: Could not find CargoType_v2 for netId {netId}"); + RebuildCache(); + + if (hashToCargoTypeV2.TryGetValue(netId, out cargoType)) + return true; + + cargoType = CargoType.None.ToV2(); + return false; + } + + public bool TryGet(uint netId, out CargoType cargoType) + { + if (TryGet(netId, out CargoType_v2 cargoTypeV2)) + { + cargoType = cargoTypeV2.v1; + return true; + } + + cargoType = CargoType.None; + return false; + } + + public bool TryGetNetId(CargoType_v2 cargoType, out uint netId) + { + if (cargoTypeV2ToHash.TryGetValue(cargoType, out netId)) + return true; + + uint hash = StringHashing.Fnv1aHash(cargoType.id); + Multiplayer.LogDebug(() => $"Registering cargo type '{cargoType.id}', netId: {hash}"); + + if (hash == 0 || hash == uint.MaxValue) + { + Multiplayer.LogError($"Computed hash for cargo type '{cargoType.id}' is {hash}, which is reserved."); + netId = 0; + return false; + } + + cargoTypeV2ToHash[cargoType] = hash; + hashToCargoTypeV2[hash] = cargoType; + + netId = hash; + return true; + } + + public bool TryGetNetId(CargoType cargoType, out uint netId) + { + return TryGetNetId(cargoType.ToV2(), out netId); + } + + [UsedImplicitly] + public new static string AllowAutoCreate() + { + return $"[{nameof(CargoTypeLookup)}]"; + } +} From f42fca9fb771a94a37519666dafd6c210f235cdd Mon Sep 17 00:00:00 2001 From: Macka Date: Sat, 15 Nov 2025 12:30:56 +1000 Subject: [PATCH 475/521] Refactor cargo type serialization to use net IDs Updated networking code to serialize and transmit cargo types using network IDs instead of enum values. This improves consistency and future-proofs cargo type handling across client and server, affecting warehouse machine controllers, task data, and related packets. --- .../NetworkedWarehouseMachineController.cs | 5 ++- .../Networking/Data/TaskNetworkData.cs | 41 +++++++++++++------ .../Managers/Client/NetworkClient.cs | 30 +++++++------- .../Managers/Server/NetworkServer.cs | 12 +++--- ...entboundWarehouseControllerUpdatePacket.cs | 2 +- .../Train/ClientboundCargoStatePacket.cs | 3 +- .../Jobs/WarehouseMachineControllerPatch.cs | 6 +-- 7 files changed, 61 insertions(+), 38 deletions(-) diff --git a/Multiplayer/Components/Networking/Jobs/NetworkedWarehouseMachineController.cs b/Multiplayer/Components/Networking/Jobs/NetworkedWarehouseMachineController.cs index f36594e8..bb89aecf 100644 --- a/Multiplayer/Components/Networking/Jobs/NetworkedWarehouseMachineController.cs +++ b/Multiplayer/Components/Networking/Jobs/NetworkedWarehouseMachineController.cs @@ -183,7 +183,10 @@ public void ClientProcessUpdate(ClientboundWarehouseControllerUpdatePacket packe if (car != null && jobId != null) { - cargoType_V2 = ((CargoType)packet.CargoType).ToV2(); + if (!CargoTypeLookup.Instance.TryGet(packet.CargoTypeNetId, out cargoType_V2)) + { + Multiplayer.LogWarning($"NetworkedWarehouseMachineController failed to find CargoType with netId: {packet.CargoTypeNetId} for JobId: {jobId} on Car: {car.ID}"); + } } WarehouseMachineController?.SetScreen(preset, isLoading, jobId, car, cargoType_V2, extra); diff --git a/Multiplayer/Networking/Data/TaskNetworkData.cs b/Multiplayer/Networking/Data/TaskNetworkData.cs index d316c8ea..9ebdf9de 100644 --- a/Multiplayer/Networking/Data/TaskNetworkData.cs +++ b/Multiplayer/Networking/Data/TaskNetworkData.cs @@ -8,6 +8,7 @@ using System.IO; using System.Linq; using System; +using DV.ThingTypes.TransitionHelpers; namespace Multiplayer.Networking.Data; @@ -133,7 +134,11 @@ public override void Serialize(BinaryWriter writer) writer.WriteUShortArray(CarNetIDs); writer.Write((byte)WarehouseTaskType); writer.Write(WarehouseMachine ?? string.Empty); - writer.Write((int)CargoType); + + if (!CargoTypeLookup.Instance.TryGetNetId(CargoType.ToV2(), out var cargoNetId)) + Multiplayer.LogError($"WarehouseTaskData.Serialize(): Could not find netId for CargoType {CargoType}"); + + writer.Write(cargoNetId); writer.Write(CargoAmount); writer.Write(ReadyForMachine); } @@ -144,7 +149,11 @@ public override void Deserialize(BinaryReader reader) CarNetIDs = reader.ReadUShortArray(); WarehouseTaskType = (WarehouseTaskType)reader.ReadByte(); WarehouseMachine = reader.ReadString(); - CargoType = (CargoType)reader.ReadInt32(); + + uint cargoNetId = reader.ReadUInt32(); + CargoTypeLookup.Instance.TryGet(cargoNetId, out CargoType cargoType); + CargoType = cargoType; + CargoAmount = reader.ReadSingle(); ReadyForMachine = reader.ReadBoolean(); } @@ -227,12 +236,16 @@ public override void Serialize(BinaryWriter writer) writer.Write(DestinationTrack); //Multiplayer.Log($"TaskNetworkData.Serialize() TransportedCargoPerCar != null {TransportedCargoPerCar != null}"); - writer.Write(TransportedCargoPerCar != null); + + writer.Write(TransportedCargoPerCar?.Length ?? 0); if (TransportedCargoPerCar != null) { - //Multiplayer.Log($"TaskNetworkData.Serialize() TransportedCargoPerCar.PutArray() length: {TransportedCargoPerCar.Length}"); - writer.WriteInt32Array(TransportedCargoPerCar.Select(x => (int)x).ToArray()); + foreach (var cargoType in TransportedCargoPerCar) + { + CargoTypeLookup.Instance.TryGetNetId(cargoType.ToV2(), out var cargoNetId); + writer.Write(cargoNetId); + } } //Multiplayer.Log($"TaskNetworkData.Serialize() CouplingRequiredAndNotDone {CouplingRequiredAndNotDone}"); @@ -254,15 +267,19 @@ public override void Deserialize(BinaryReader reader) DestinationTrack = reader.ReadString(); //Multiplayer.Log($"TaskNetworkData.Deserialize() DestinationTrack {DestinationTrack}"); - if (reader.ReadBoolean()) + var cargoCount = reader.ReadInt32(); + if (cargoCount > 0) { - //Multiplayer.Log($"TaskNetworkData.Deserialize() TransportedCargoPerCar != null True"); - TransportedCargoPerCar = reader.ReadInt32Array().Select(x => (CargoType)x).ToArray(); + TransportedCargoPerCar = new CargoType[cargoCount]; + + for (var i = 0; i < cargoCount; i++) + { + uint cargoNetId = reader.ReadUInt32(); + CargoTypeLookup.Instance.TryGet(cargoNetId, out CargoType cargoType); + TransportedCargoPerCar[i] = cargoType; + } } - //else - //{ - // Multiplayer.LogWarning($"TaskNetworkData.Deserialize() TransportedCargoPerCar != null False"); - //} + CouplingRequiredAndNotDone = reader.ReadBoolean(); //Multiplayer.Log($"TaskNetworkData.Deserialize() CouplingRequiredAndNotDone {CouplingRequiredAndNotDone}"); AnyHandbrakeRequiredAndNotDone = reader.ReadBoolean(); diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 4b6d14aa..f32435e8 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -828,7 +828,7 @@ private void OnClientboundCargoStatePacket(ClientboundCargoStatePacket packet) if (!NetworkedTrainCar.TryGet(packet.NetId, out NetworkedTrainCar networkedTrainCar)) return; - LogDebug(() => $"OnClientboundCargoStatePacket() {networkedTrainCar.CurrentID}, IsLoading: {packet.IsLoading}, CargoType: {packet.CargoType}, CargoAmount: {packet.CargoAmount}, Health: {packet.CargoHealth}, CargoModelIndex: {packet.CargoModelIndex}, WarehouseMachineId: {packet.WarehouseMachineNetId}"); + LogDebug(() => $"OnClientboundCargoStatePacket() {networkedTrainCar.CurrentID}, IsLoading: {packet.IsLoading}, CargoType: {packet.CargoTypeNetId}, CargoAmount: {packet.CargoAmount}, Health: {packet.CargoHealth}, CargoModelIndex: {packet.CargoModelIndex}, WarehouseMachineId: {packet.WarehouseMachineNetId}"); networkedTrainCar.CargoModelIndex = packet.CargoModelIndex; Car logicCar = networkedTrainCar.TrainCar.logicCar; @@ -839,7 +839,9 @@ private void OnClientboundCargoStatePacket(ClientboundCargoStatePacket packet) return; } - if (packet.CargoType == CargoType.None && logicCar.CurrentCargoTypeInCar == CargoType.None) + CargoTypeLookup.Instance.TryGet(packet.CargoTypeNetId, out CargoType cargoType); + + if (cargoType == CargoType.None && logicCar.CurrentCargoTypeInCar == CargoType.None) return; //packet.CargoAmount is the total amount, not the amount to load/unload @@ -854,49 +856,49 @@ private void OnClientboundCargoStatePacket(ClientboundCargoStatePacket packet) if (packet.IsLoading) { - LogDebug(() => $"OnClientboundCargoStatePacket() Loading cargo: {packet.CargoType} into {networkedTrainCar.CurrentID}, current amount: {packet.CargoAmount}"); + LogDebug(() => $"OnClientboundCargoStatePacket() Loading cargo: {cargoType} into {networkedTrainCar.CurrentID}, current amount: {packet.CargoAmount}"); //Check correct cargo is loaded and the amount is correct - if (logicCar.LoadedCargoAmount == cargoAmount && logicCar.CurrentCargoTypeInCar == packet.CargoType) + if (logicCar.LoadedCargoAmount == cargoAmount && logicCar.CurrentCargoTypeInCar == 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 != packet.CargoType) + if (logicCar.CurrentCargoTypeInCar != CargoType.None && logicCar.CurrentCargoTypeInCar != cargoType) logicCar.DumpCargo(); //We have the correct cargo, but not the right amount, calculate the delta - if (logicCar.CurrentCargoTypeInCar == packet.CargoType) + if (logicCar.CurrentCargoTypeInCar == cargoType) cargoAmount -= logicCar.LoadedCargoAmount; if (cargoAmount > 0) { - logicCar.LoadCargo(cargoAmount, packet.CargoType, warehouseMachine); + logicCar.LoadCargo(cargoAmount, cargoType, warehouseMachine); } networkedTrainCar.TrainCar.CargoDamage.LoadCargoDamageState(packet.CargoHealth); } else { - LogDebug(() => $"OnClientboundCargoStatePacket() Unloading cargo: {packet.CargoType} into {networkedTrainCar.CurrentID}, current amount: {packet.CargoAmount}"); + LogDebug(() => $"OnClientboundCargoStatePacket() Unloading cargo: {cargoType} into {networkedTrainCar.CurrentID}, current amount: {packet.CargoAmount}"); //Check correct cargo is loaded and the amount is correct - if (logicCar.LoadedCargoAmount == cargoAmount && logicCar.CurrentCargoTypeInCar == packet.CargoType) + if (logicCar.LoadedCargoAmount == cargoAmount && logicCar.CurrentCargoTypeInCar == cargoType) return; //If there is different cargo we need to remove it, then load the appropriate amount - if (logicCar.CurrentCargoTypeInCar == CargoType.None || logicCar.CurrentCargoTypeInCar != packet.CargoType) + if (logicCar.CurrentCargoTypeInCar == CargoType.None || logicCar.CurrentCargoTypeInCar != cargoType) { //avoid triggering the load event by backdooring it logicCar.LastUnloadedCargoType = logicCar.CurrentCargoTypeInCar; - logicCar.CurrentCargoTypeInCar = packet.CargoType; + logicCar.CurrentCargoTypeInCar = cargoType; logicCar.LoadedCargoAmount = cargoAmount; } //We have the correct cargo, calculate the delta - if (logicCar.CurrentCargoTypeInCar == packet.CargoType) + if (logicCar.CurrentCargoTypeInCar == cargoType) cargoAmount = logicCar.LoadedCargoAmount - cargoAmount; if (cargoAmount > 0) - logicCar.UnloadCargo(cargoAmount, packet.CargoType, warehouseMachine); + logicCar.UnloadCargo(cargoAmount, cargoType, warehouseMachine); } } @@ -928,7 +930,7 @@ private void OnClientboundCarHealthUpdatePacket(ClientboundCarHealthUpdatePacket private void OnClientboundWarehouseControllerUpdatePacket(ClientboundWarehouseControllerUpdatePacket packet) { - LogDebug(() => $"OnClientboundWarehouseControllerUpdatePacket() NetId: {packet.NetId}, IsLoading: {packet.IsLoading}, JobNetId: {packet.JobNetId}, CarNetId: {packet.CarNetId}, CargoType: {packet.CargoType}, Preset: [{(WarehouseMachineController.TextPreset)packet.Preset}, {packet.Preset}]"); + LogDebug(() => $"OnClientboundWarehouseControllerUpdatePacket() NetId: {packet.NetId}, IsLoading: {packet.IsLoading}, JobNetId: {packet.JobNetId}, CarNetId: {packet.CarNetId}, CargoType: {packet.CargoTypeNetId}, Preset: [{(WarehouseMachineController.TextPreset)packet.Preset}, {packet.Preset}]"); if (!NetworkedWarehouseMachineController.Get(packet.NetId, out NetworkedWarehouseMachineController networkedWarehouseMachineController)) { LogWarning($"OnClientboundWarehouseControllerUpdatePacket() Failed to find networked warehouse machine controller for [{packet.NetId}]"); diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index 2d5085f2..3ef09e19 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -561,7 +561,9 @@ public void SendCargoState(NetworkedTrainCar netTraincar, bool isLoading, byte c return; } - CargoType cargoType = isLoading ? logicCar.CurrentCargoTypeInCar : logicCar.LastUnloadedCargoType; + CargoType cargoTypeV1 = isLoading ? logicCar.CurrentCargoTypeInCar : logicCar.LastUnloadedCargoType; + + CargoTypeLookup.Instance.TryGetNetId(cargoTypeV1, out uint cargoType); ushort netMachineId = 0; if (logicCar.CargoOriginWarehouse != null) @@ -577,7 +579,7 @@ public void SendCargoState(NetworkedTrainCar netTraincar, bool isLoading, byte c { NetId = netTraincar.NetId, IsLoading = isLoading, - CargoType = cargoType, + CargoTypeNetId = cargoType, CargoAmount = logicCar.LoadedCargoAmount, CargoHealth = netTraincar.TrainCar.CargoDamage.HealthPercentage, CargoModelIndex = cargoModelIndex, @@ -585,9 +587,9 @@ public void SendCargoState(NetworkedTrainCar netTraincar, bool isLoading, byte c }, DeliveryMethod.ReliableOrdered, SelfPeer); } - public void SendWarehouseControllerUpdate(ushort netId, bool isLoading, ushort jobNetId, ushort carNetId, CargoType cargoType, WarehouseMachineController.TextPreset preset) + public void SendWarehouseControllerUpdate(ushort netId, bool isLoading, ushort jobNetId, ushort carNetId, uint cargoTypeNetId, WarehouseMachineController.TextPreset preset) { - LogDebug(() => $"SendWarehouseControllerUpdate({netId}, {isLoading}, {jobNetId}, {carNetId}, {cargoType}, {preset})"); + LogDebug(() => $"SendWarehouseControllerUpdate({netId}, {isLoading}, {jobNetId}, {carNetId}, {cargoTypeNetId}, {preset})"); SendPacketToAll(new ClientboundWarehouseControllerUpdatePacket() { @@ -595,7 +597,7 @@ public void SendWarehouseControllerUpdate(ushort netId, bool isLoading, ushort j IsLoading = isLoading, JobNetId = jobNetId, CarNetId = carNetId, - CargoType = (ushort)cargoType, + CargoTypeNetId = cargoTypeNetId, Preset = (ushort)preset, }, DeliveryMethod.Sequenced, SelfPeer); diff --git a/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundWarehouseControllerUpdatePacket.cs b/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundWarehouseControllerUpdatePacket.cs index efef8f1c..42d6f545 100644 --- a/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundWarehouseControllerUpdatePacket.cs +++ b/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundWarehouseControllerUpdatePacket.cs @@ -7,6 +7,6 @@ public class ClientboundWarehouseControllerUpdatePacket public bool IsLoading { get; set; } public ushort JobNetId { get; set; } public ushort CarNetId { get; set; } - public ushort CargoType { get; set; } + public uint CargoTypeNetId { get; set; } public ushort Preset { get; set; } } diff --git a/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundCargoStatePacket.cs b/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundCargoStatePacket.cs index 2e0ec6a8..b9d11c2f 100644 --- a/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundCargoStatePacket.cs +++ b/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundCargoStatePacket.cs @@ -1,4 +1,5 @@ using DV.ThingTypes; +using System; namespace Multiplayer.Networking.Packets.Clientbound.Train; @@ -6,7 +7,7 @@ public class ClientboundCargoStatePacket { public ushort NetId { get; set; } public bool IsLoading { get; set; } - public CargoType CargoType { get; set; } + public uint CargoTypeNetId { get; set; } public float CargoAmount { get; set; } public float CargoHealth { get; set; } public byte CargoModelIndex { get; set; } diff --git a/Multiplayer/Patches/Jobs/WarehouseMachineControllerPatch.cs b/Multiplayer/Patches/Jobs/WarehouseMachineControllerPatch.cs index 0900d1df..dc45d326 100644 --- a/Multiplayer/Patches/Jobs/WarehouseMachineControllerPatch.cs +++ b/Multiplayer/Patches/Jobs/WarehouseMachineControllerPatch.cs @@ -51,7 +51,6 @@ public static void SetScreen(WarehouseMachineController __instance, TextPreset p //obtain serialisable info ushort carNetId = 0; ushort jobNetId = 0; - CargoType cargoTypeV1 = CargoType.None; if (car != null) { @@ -79,10 +78,9 @@ public static void SetScreen(WarehouseMachineController __instance, TextPreset p jobNetId = netJob.NetId; } - if (cargoType != null) - cargoTypeV1 = cargoType.v1; + CargoTypeLookup.Instance.TryGetNetId(cargoType, out uint cargoTypeNetId); - NetworkLifecycle.Instance.Server.SendWarehouseControllerUpdate(netMachine.NetId, isLoading, jobNetId, carNetId, cargoTypeV1, preset); + NetworkLifecycle.Instance.Server.SendWarehouseControllerUpdate(netMachine.NetId, isLoading, jobNetId, carNetId, cargoTypeNetId, preset); } [HarmonyPrefix] From 50c3c71561491295f924fc8ae6ac549c21488fd8 Mon Sep 17 00:00:00 2001 From: Macka Date: Sat, 15 Nov 2025 12:31:25 +1000 Subject: [PATCH 476/521] Add support for uint NetId in network API Introduces methods and delegates to handle network identifiers as unsigned 32-bit integers (uint) in NetIdProvider, INetIdProvider, and IMultiplayerAPI interfaces. This enables support for V2 types and objects requiring uint-based NetIds, alongside the existing ushort-based methods. --- Multiplayer/API/NetIdProvider.cs | 38 ++++++++++++++++++++ MultiplayerAPI/Interfaces/IMultiplayerAPI.cs | 16 +++++++++ MultiplayerAPI/Interfaces/INetId.cs | 34 ++++++++++++++++++ 3 files changed, 88 insertions(+) diff --git a/Multiplayer/API/NetIdProvider.cs b/Multiplayer/API/NetIdProvider.cs index 19eecc52..17e48606 100644 --- a/Multiplayer/API/NetIdProvider.cs +++ b/Multiplayer/API/NetIdProvider.cs @@ -1,5 +1,6 @@ using DV.CabControls; using DV.Logic.Job; +using DV.ThingTypes; using DV.Utils; using JetBrains.Annotations; using MPAPI.Interfaces; @@ -14,6 +15,9 @@ namespace Multiplayer.API; public delegate bool TryGetNetIdDelegate(T obj, out ushort netId) where T : class; public delegate bool TryGetObjectDelegate(ushort netId, out T obj) where T : class; +public delegate bool TryGetUIntNetIdDelegate(T obj, out uint netId) where T : class; +public delegate bool TryGetObjectUIntDelegate(uint netId, out T obj) where T : class; + internal class NetIdProvider : SingletonBehaviour, INetIdProvider { private readonly Dictionary handlers = []; @@ -24,6 +28,9 @@ protected override void Awake() RegisterHandler(NetworkedTrainCar.TryGetNetId, NetworkedTrainCar.TryGet); RegisterHandler(NetworkedTrainCar.TryGetNetId, NetworkedTrainCar.TryGet); + //RegisterUIntHandler(CargoTypeLookup.Instance.TryGetNetId, CargoTypeLookup.Instance.TryGet); + RegisterHandler(CargoTypeLookup.Instance.TryGetNetId, CargoTypeLookup.Instance.TryGet); + RegisterHandler(NetworkedJunction.TryGetNetId, NetworkedJunction.TryGet); RegisterHandler(NetworkedTurntable.TryGetNetId, NetworkedTurntable.TryGet); RegisterHandler(NetworkedRailTrack.TryGetNetId, NetworkedRailTrack.TryGet); @@ -46,6 +53,11 @@ public void RegisterHandler(TryGetNetIdDelegate tryGetNetId, TryGetObjectD handlers[typeof(T)] = (tryGetNetId, tryGetObject); } + public void RegisterHandler(TryGetUIntNetIdDelegate tryGetNetId, TryGetObjectUIntDelegate tryGetObject) where T : class + { + handlers[typeof(T)] = (tryGetNetId, tryGetObject); + } + public bool TryGetNetId(T obj, out ushort netId) where T : class { netId = 0; @@ -72,6 +84,32 @@ public bool TryGetObject(ushort netId, out T obj) where T : class return false; } + public bool TryGetNetId(T obj, out uint netId) where T : class + { + netId = 0; + + if (obj == null) + return false; + + if (handlers.TryGetValue(typeof(T), out var handler) && handler is (TryGetUIntNetIdDelegate tryGetNetId, TryGetObjectUIntDelegate _)) + return tryGetNetId(obj, out netId); + + return false; + } + + public bool TryGetObject(uint netId, out T obj) where T : class + { + obj = null; + + if (netId == 0) + return false; + + if (handlers.TryGetValue(typeof(T), out var handler) && handler is (TryGetUIntNetIdDelegate _, TryGetObjectUIntDelegate tryGetObject)) + return tryGetObject(netId, out obj); + + return false; + } + [UsedImplicitly] public new static string AllowAutoCreate() { diff --git a/MultiplayerAPI/Interfaces/IMultiplayerAPI.cs b/MultiplayerAPI/Interfaces/IMultiplayerAPI.cs index 9a34e1bb..45c90bee 100644 --- a/MultiplayerAPI/Interfaces/IMultiplayerAPI.cs +++ b/MultiplayerAPI/Interfaces/IMultiplayerAPI.cs @@ -88,6 +88,22 @@ public interface IMultiplayerAPI /// True if the object was found; otherwise, false. bool TryGetObjectFromNetId(ushort netId, out T obj) where T : class; + /// + /// Gets the NetId for an object. + /// + /// The object you want the NetId for. + /// When this method returns, contains the NetId associated with the specified object, if found; otherwise, 0. + /// True if a NetId for the object was found; otherwise, false. + bool TryGetNetId(T obj, out uint netId) where T : class; + + /// + /// Gets the object for a NetId. + /// + /// The non-zero NetId for the object. + /// When this method returns, contains the object associated with the NetId, if found; otherwise null. + /// True if the object was found; otherwise, false. + bool TryGetObjectFromNetId(uint netId, out T obj) where T : class; + /// /// Registers a PaintTheme and returns its ID. /// diff --git a/MultiplayerAPI/Interfaces/INetId.cs b/MultiplayerAPI/Interfaces/INetId.cs index 5fca80ea..94ed56df 100644 --- a/MultiplayerAPI/Interfaces/INetId.cs +++ b/MultiplayerAPI/Interfaces/INetId.cs @@ -41,4 +41,38 @@ public interface INetIdProvider /// true if the object was successfully retrieved; otherwise, false. /// bool TryGetObject(ushort netId, out T obj) where T : class; + + /// + /// Attempts to retrieve the network identifier for the specified object. + /// + /// The type of object to get the network ID for. Must be a reference type. + /// The object to get the network identifier for. + /// + /// When this method returns, contains the network identifier associated with the object if found; + /// otherwise, the default value for the type. + /// + /// + /// true if the network identifier was successfully retrieved; otherwise, false. + /// + /// + /// This method is for network identifiers represented as unsigned 32-bit integers, and is typically used where V2 types are involved. + /// + bool TryGetNetId(T obj, out uint netId) where T : class; + + /// + /// Attempts to retrieve the object associated with the specified network identifier. + /// + /// The type of object to retrieve. Must be a reference type. + /// The network identifier of the object to retrieve. + /// + /// When this method returns, contains the object associated with the network identifier if found; + /// otherwise, the default value for the type. + /// + /// + /// true if the object was successfully retrieved; otherwise, false. + /// + /// + /// This method is for network identifiers represented as unsigned 32-bit integers, and is typically used where V2 types are involved. + /// + bool TryGetObject(uint netId, out T obj) where T : class; } From 1ad188c2c51977715cdef1e232da28ffe320bd6f Mon Sep 17 00:00:00 2001 From: Macka Date: Sat, 15 Nov 2025 12:32:56 +1000 Subject: [PATCH 477/521] Delay job manager time load until logic controller init Moved JobsManager.Instance.LoadTime to occur after LogicController.Instance is initialised. This fixes an issue where Custom Cargo attempts to access the LogicController before it's initialised --- .../Components/SaveGame/StartGameData_ServerSave.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Multiplayer/Components/SaveGame/StartGameData_ServerSave.cs b/Multiplayer/Components/SaveGame/StartGameData_ServerSave.cs index a079682c..8430ecf8 100644 --- a/Multiplayer/Components/SaveGame/StartGameData_ServerSave.cs +++ b/Multiplayer/Components/SaveGame/StartGameData_ServerSave.cs @@ -47,8 +47,6 @@ public void SetFromPacket(ClientboundSaveGameDataPacket packet) CareerManagerDebtControllerPatch.HasDebt = packet.HasDebt; - JobsManager.Instance.LoadTime(packet.JobManagerTime); - Multiplayer.LogDebug(() => { string unlockedGen = string.Join(", ", UnlockablesManager.Instance.UnlockedGeneralLicenses); @@ -89,6 +87,12 @@ public override IEnumerator DoLoad(Transform playerContainer) LicenseManager.Instance.LoadData(saveGameData); + Multiplayer.Log("Waiting for Logic Controller..."); + yield return new WaitUntil(() => LogicController.Instance.initialized); + Multiplayer.Log("Logic Controller initialised."); + + JobsManager.Instance.LoadTime(packet.JobManagerTime); + if (saveGameData.GetString(SaveGameKeys.Game_mode) == "FreeRoam") LicenseManager.Instance.GrabAllGameModeSpecificUnlockables(SaveGameKeys.Game_mode); //else From bd33346d64d9a46e67abf0f84ed30c1093ac8d58 Mon Sep 17 00:00:00 2001 From: Macka Date: Sat, 22 Nov 2025 14:26:24 +1000 Subject: [PATCH 478/521] Fix player rotation handling on vehicles for VR players Corrects how player rotation is calculated and applied when on vehicles by using camera rotation and relative yaw. Also adds cleanup for settings event subscription in NetworkedPlayer. --- .../Networking/Player/NetworkedPlayer.cs | 14 ++++++++++++-- Multiplayer/Networking/Data/ServerPlayer.cs | 3 ++- .../Player/CustomFirstPersonControllerPatch.cs | 2 +- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/Multiplayer/Components/Networking/Player/NetworkedPlayer.cs b/Multiplayer/Components/Networking/Player/NetworkedPlayer.cs index e446292f..2e5cbaf8 100644 --- a/Multiplayer/Components/Networking/Player/NetworkedPlayer.cs +++ b/Multiplayer/Components/Networking/Player/NetworkedPlayer.cs @@ -80,6 +80,11 @@ protected void Awake() targetMoveDir = Vector2.zero; } + protected void OnDestroy() + { + Settings.OnSettingsUpdated -= OnSettingsUpdated; + } + private void OnSettingsUpdated(Settings settings) { nameTag.ShowUsername(settings.ShowNameTags); @@ -124,8 +129,13 @@ protected void Update() // Create base orientation aligned with world up but facing car's forward direction Quaternion baseRotation = Quaternion.LookRotation(horizontalForward, worldUp); + // Calculate the relative rotation: how much is the player rotated relative to the car? + float carYaw = baseRotation.eulerAngles.y; + float playerYaw = targetRotation.eulerAngles.y; + float relativeYaw = playerYaw - carYaw; + // Apply the desired Y rotation (player's facing direction) on top of this base rotation - Quaternion targetWorldRotation = baseRotation * Quaternion.Euler(0, targetRotation.eulerAngles.y, 0); + Quaternion targetWorldRotation = baseRotation * Quaternion.Euler(0, relativeYaw, 0); // Apply rotation in world space despite being a child transform selfTransform.rotation = Quaternion.Lerp(selfTransform.rotation, targetWorldRotation, t); @@ -139,7 +149,7 @@ protected void Update() if (itemHeld != null) { itemHeld.transform.position = selfTransform.position + GetItemOffsetFromPlayer(); - itemHeld.transform.rotation = selfTransform.rotation * (itemHoldRot ?? ItemPositionController.Instance.itemAnchor.localRotation); + itemHeld.transform.rotation = selfTransform.rotation * (itemHoldRot ?? Quaternion.identity);//ItemPositionController.Instance.itemAnchor.localRotation); } } diff --git a/Multiplayer/Networking/Data/ServerPlayer.cs b/Multiplayer/Networking/Data/ServerPlayer.cs index a2346708..cde22259 100644 --- a/Multiplayer/Networking/Data/ServerPlayer.cs +++ b/Multiplayer/Networking/Data/ServerPlayer.cs @@ -123,7 +123,8 @@ public Vector3 WorldPosition { return pos; } - } + } + public float WorldRotationY => CarId == 0 || !NetworkedTrainCar.TryGet(CarId, out NetworkedTrainCar car) ? RawRotationY : (Quaternion.Euler(0, RawRotationY, 0) * car.transform.rotation).eulerAngles.y; diff --git a/Multiplayer/Patches/Player/CustomFirstPersonControllerPatch.cs b/Multiplayer/Patches/Player/CustomFirstPersonControllerPatch.cs index 8dbd78a4..5d45d7a4 100644 --- a/Multiplayer/Patches/Player/CustomFirstPersonControllerPatch.cs +++ b/Multiplayer/Patches/Player/CustomFirstPersonControllerPatch.cs @@ -54,7 +54,7 @@ private static void OnTick(uint tick) return; Vector3 position = isOnCar ? PlayerManager.PlayerTransform.localPosition : PlayerManager.PlayerTransform.GetWorldAbsolutePosition(); - float rotationY = (isOnCar ? PlayerManager.PlayerTransform.localEulerAngles : PlayerManager.PlayerTransform.eulerAngles).y; + float rotationY = PlayerManager.PlayerCamera.transform.eulerAngles.y; //bool positionOrRotationChanged = lastPosition != position || !Mathf.Approximately(lastRotationY, rotationY); From 3cb05900d6dd1b77c6645e1b98cc073317cb5ca7 Mon Sep 17 00:00:00 2001 From: Macka Date: Sat, 22 Nov 2025 14:31:08 +1000 Subject: [PATCH 479/521] Fix edge case where car is unintentionally deleted Hosts loading from a save onto a vehicle not visited recently may have the vehicle deleted while they are still on it, breaking game play. Ensures a host's vehicle is properly recorded in the ServerPlayer object. --- .../Player/CustomFirstPersonControllerPatch.cs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/Multiplayer/Patches/Player/CustomFirstPersonControllerPatch.cs b/Multiplayer/Patches/Player/CustomFirstPersonControllerPatch.cs index 5d45d7a4..65e6ee1b 100644 --- a/Multiplayer/Patches/Player/CustomFirstPersonControllerPatch.cs +++ b/Multiplayer/Patches/Player/CustomFirstPersonControllerPatch.cs @@ -13,6 +13,8 @@ public static class CustomFirstPersonControllerPatch private static CustomFirstPersonController fps; + private static bool lastOnCar; + private static ushort lastCarNetId; private static Vector3 lastPosition; private static float lastRotationY; private static bool sentFinalPosition; @@ -27,6 +29,7 @@ private static void CharacterMovement(CustomFirstPersonController __instance) { fps = __instance; isOnCar = PlayerManager.Car != null; + car = PlayerManager.Car; NetworkLifecycle.Instance.OnTick += OnTick; PlayerManager.CarChanged += OnCarChanged; } @@ -53,22 +56,28 @@ private static void OnTick(uint tick) if(UnloadWatcher.isUnloading) return; + if (isOnCar && car == null) + { + car = PlayerManager.Car; + isOnCar = car != null; + } + Vector3 position = isOnCar ? PlayerManager.PlayerTransform.localPosition : PlayerManager.PlayerTransform.GetWorldAbsolutePosition(); float rotationY = PlayerManager.PlayerCamera.transform.eulerAngles.y; - //bool positionOrRotationChanged = lastPosition != position || !Mathf.Approximately(lastRotationY, rotationY); + ushort carNetID = isOnCar ? car.GetNetId() : (ushort)0; - bool positionOrRotationChanged = Vector3.Distance(lastPosition, position) > 0 || Math.Abs(lastRotationY - rotationY) > ROTATION_THRESHOLD; + bool positionOrRotationChanged = lastOnCar != isOnCar || (isOnCar && (lastCarNetId != carNetID)) || Vector3.Distance(lastPosition, position) > 0 || Math.Abs(lastRotationY - rotationY) > 0.2f;//ROTATION_THRESHOLD; if (!positionOrRotationChanged && sentFinalPosition) return; + lastOnCar = isOnCar; + lastCarNetId = carNetID; lastPosition = position; lastRotationY = rotationY; sentFinalPosition = !positionOrRotationChanged; - 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; } From 9fd3cc51cb1380d31cab01a16b8b80013d2daa5b Mon Sep 17 00:00:00 2001 From: Macka Date: Sat, 22 Nov 2025 14:32:28 +1000 Subject: [PATCH 480/521] Replace GrabHandlerGizmoItem with CustomNonVrGrabAnchor Updated NetworkedPluggableObject to use CustomNonVrGrabAnchor instead of GrabHandlerGizmoItem for non-VR grab anchor handling. Also removed debug logging from PluggableObjectPatch for cleaner output. --- .../Networking/World/NetworkedPluggableObject.cs | 7 ++++--- Multiplayer/Patches/World/PluggableObjectPatch.cs | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Multiplayer/Components/Networking/World/NetworkedPluggableObject.cs b/Multiplayer/Components/Networking/World/NetworkedPluggableObject.cs index 91bd55bf..0ccef08f 100644 --- a/Multiplayer/Components/Networking/World/NetworkedPluggableObject.cs +++ b/Multiplayer/Components/Networking/World/NetworkedPluggableObject.cs @@ -53,7 +53,7 @@ public static bool Get(PluggableObject pluggableObject, out NetworkedPluggableOb public bool IsHeld => playerHolding != 0 || HeldBy != null || PluggableObject.controlGrabbed; - private GrabHandlerGizmoItem grabHandler; + private CustomNonVrGrabAnchor nonVrGrabAnchor; private bool handlersInitialised = false; @@ -99,7 +99,7 @@ protected IEnumerator Start() //Multiplayer.LogDebug(() => $"NetworkedPluggableObject.Start() Controlbase {PluggableObject?.controlBase?.spec?.name}, {transform.parent.name}"); - grabHandler = this.GetComponent(); + nonVrGrabAnchor = this.GetComponent(); PluggableObject.controlBase.Grabbed += OnGrabbed; PluggableObject.controlBase.Ungrabbed += OnUngrabbed; @@ -616,7 +616,8 @@ public void GrabPlug(byte playerId) if (NetworkLifecycle.Instance.IsClientRunning && NetworkLifecycle.Instance.Client.ClientPlayerManager.TryGetPlayer(playerHolding, out var player)) { - var target = grabHandler?.customGrabAnchor?.GetGrabAnchor(); + var target = nonVrGrabAnchor?.GetGrabAnchor(); + Multiplayer.LogDebug(() => $"GrabPlug() NetId: {NetId}, player: {player.Username}, targetPos: {target?.localPos}, targetRot: {target?.localRot}"); player.HoldItem(gameObject, target?.localPos, target?.localRot); } } diff --git a/Multiplayer/Patches/World/PluggableObjectPatch.cs b/Multiplayer/Patches/World/PluggableObjectPatch.cs index 102c0f10..f2ab7fea 100644 --- a/Multiplayer/Patches/World/PluggableObjectPatch.cs +++ b/Multiplayer/Patches/World/PluggableObjectPatch.cs @@ -26,7 +26,7 @@ public static bool Awake(PluggableObject __instance) public static bool IsHeldInHand(PluggableObject __instance, ref bool __result) { var result = __result; - Multiplayer.LogDebug(() => $"IsHeldInHand({result})"); + //Multiplayer.LogDebug(() => $"IsHeldInHand({result})"); if (NetworkedPluggableObject.Get(__instance, out var networkedPluggableObject)) __result = networkedPluggableObject.IsHeld; @@ -34,7 +34,7 @@ public static bool IsHeldInHand(PluggableObject __instance, ref bool __result) __result = __instance.controlGrabbed; result = __result; - Multiplayer.LogDebug(() => $"IsHeldInHand() result: {result}, net found: {networkedPluggableObject != null}"); + //Multiplayer.LogDebug(() => $"IsHeldInHand() result: {result}, net found: {networkedPluggableObject != null}"); return false; } From c2485550769e6f3928bc682e3d2e8de78094b684 Mon Sep 17 00:00:00 2001 From: Macka Date: Sat, 22 Nov 2025 14:32:40 +1000 Subject: [PATCH 481/521] Add null check in TryGetNetId for CargoType_v2 Ensures TryGetNetId returns false and sets netId to 0 when cargoType is null, preventing potential null reference exceptions. --- Multiplayer/Components/Networking/Train/CargoTypeLookup.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Multiplayer/Components/Networking/Train/CargoTypeLookup.cs b/Multiplayer/Components/Networking/Train/CargoTypeLookup.cs index 2b36e079..299193c1 100644 --- a/Multiplayer/Components/Networking/Train/CargoTypeLookup.cs +++ b/Multiplayer/Components/Networking/Train/CargoTypeLookup.cs @@ -66,6 +66,10 @@ public bool TryGet(uint netId, out CargoType cargoType) public bool TryGetNetId(CargoType_v2 cargoType, out uint netId) { + netId = 0; + if ( cargoType == null) + return false; + if (cargoTypeV2ToHash.TryGetValue(cargoType, out netId)) return true; From e4af9cbf270afa39737cb17b7fb4852e9c4b2b2c Mon Sep 17 00:00:00 2001 From: Macka Date: Sat, 22 Nov 2025 14:33:25 +1000 Subject: [PATCH 482/521] Skip item anchor offset capture in VR mode Added a check to prevent capturing item anchor offset when VR is enabled. This fixes an issue that breaks VR player movement --- .../Components/Networking/Player/NetworkedPlayer.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Multiplayer/Components/Networking/Player/NetworkedPlayer.cs b/Multiplayer/Components/Networking/Player/NetworkedPlayer.cs index 2e5cbaf8..a8c290f6 100644 --- a/Multiplayer/Components/Networking/Player/NetworkedPlayer.cs +++ b/Multiplayer/Components/Networking/Player/NetworkedPlayer.cs @@ -23,8 +23,14 @@ public static void CaptureItemAnchorOffset() //todo: there's some minor inconsistency with return values and may be related to: // - the direction/rotation of the camera // - player loading status (maybe posistion hasn't settled yet) - itemAnchorOffset = PlayerManager.PlayerTransform.InverseTransformPoint(ItemPositionController.Instance.itemAnchor.position); - Multiplayer.LogDebug(() => $"NetworkedPlayer.CaptureItemAnchorOffset() itemAnchorOffset: {itemAnchorOffset}"); + if (VRManager.IsVREnabled()) + { + } + else + { + itemAnchorOffset = PlayerManager.PlayerTransform.InverseTransformPoint(ItemPositionController.Instance.itemAnchor.position); + Multiplayer.LogDebug(() => $"NetworkedPlayer.CaptureItemAnchorOffset() itemAnchorOffset: {itemAnchorOffset}"); + } } #endregion From 6d1db3534988619605c165127058c24c40f4a18b Mon Sep 17 00:00:00 2001 From: Macka Date: Sat, 22 Nov 2025 14:38:30 +1000 Subject: [PATCH 483/521] Fix issue where pitstop can not be initialised on clients Ensure OnPlayerEnteredActivationRegion always sends a pit stop bulk data packet regardless of car presence. This ensures clients receive a packet and set `Refreshec` to true, allowing client interactions. --- .../Components/Networking/World/NetworkedPitStopStation.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs b/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs index c226d775..02a56dc1 100644 --- a/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs +++ b/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs @@ -255,8 +255,6 @@ public void OnPlayerDisconnect(ServerPlayer player) public void OnPlayerEnteredActivationRegion(ServerPlayer player) { - if (Station.pitstop.IsCarInPitStop()) - { // Ensure all resource data exists InitialiseData(); @@ -299,7 +297,6 @@ public void OnPlayerEnteredActivationRegion(ServerPlayer player) // Send current state NetworkLifecycle.Instance.Server.SendPitStopBulkDataPacket(NetId, Station.pitstop.carList.Count, carIndex, faucetPos, stateData, plugData, player.Peer); - } } public void OnPlayerEnteredCullingRegion(ServerPlayer player) From 7bc6588f5f4825e4321484e9806309e1866d5cae Mon Sep 17 00:00:00 2001 From: Macka Date: Sat, 22 Nov 2025 14:40:32 +1000 Subject: [PATCH 484/521] Initialise itemAnchorOffset with default values Sets the static itemAnchorOffset field to a default Vector3 value (0.2f, 1.5f, 0.4f) in NetworkedPlayer Required due to VR not utilising `ItemPositionController`. --- Multiplayer/Components/Networking/Player/NetworkedPlayer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Multiplayer/Components/Networking/Player/NetworkedPlayer.cs b/Multiplayer/Components/Networking/Player/NetworkedPlayer.cs index a8c290f6..7734b7ec 100644 --- a/Multiplayer/Components/Networking/Player/NetworkedPlayer.cs +++ b/Multiplayer/Components/Networking/Player/NetworkedPlayer.cs @@ -11,7 +11,7 @@ namespace Multiplayer.Components.Networking.Player; public class NetworkedPlayer : MonoBehaviour { #region Static Setup - private static Vector3 itemAnchorOffset; + private static Vector3 itemAnchorOffset = new(0.2f, 1.5f, 0.4f); /// /// Captures the standard offset position for held items relative to the player transform From 6a18733728937ebda0863a466aca3be527a04686 Mon Sep 17 00:00:00 2001 From: Macka Date: Sat, 22 Nov 2025 22:04:20 +1000 Subject: [PATCH 485/521] Fix issues with NetworkedPitStopStation in VR Replaces usage of GrabHandlerHingeJoint and LeverNonVR with LeverBase and RotaryBase for grab and lever interactions in NetworkedPitStopStation. Updates event handler signatures and logic to match new types, and replaces SetMovingDisabled with BlockControl and InteractionAllowed properties for improved interaction control. --- .../World/NetworkedPitStopStation.cs | 67 +++++++++++-------- 1 file changed, 38 insertions(+), 29 deletions(-) diff --git a/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs b/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs index 02a56dc1..cc3c7e37 100644 --- a/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs +++ b/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs @@ -16,7 +16,6 @@ using System.Text; using UnityEngine; using static CashRegisterModule; -using static DV.Interaction.Inputs.InputManager; namespace Multiplayer.Components.Networking.World; @@ -96,14 +95,14 @@ public static void InitialisePitStops() private ResourceType[] resourceTypes = []; - private GrabHandlerHingeJoint carSelectorGrab; - private GrabHandlerHingeJoint faucetPositionerGrab; + private RotaryBase carSelectorGrab; + private LeverBase faucetPositionerGrab; private HingeJointAngleFix faucetPositioner; private SteppedJoint faucetCrankSteppedJoint; private readonly Dictionary leverHandler)> leverStateLookup = []; - private readonly Dictionary grabbedHandlerLookup = []; - private readonly Dictionary leverLookup = []; + private readonly Dictionary grabbedHandlerLookup = []; + private readonly Dictionary leverLookup = []; private readonly Dictionary resourceToPluggableObject = []; private readonly Dictionary resourceTypeToLocoResourceModule = []; @@ -197,7 +196,7 @@ protected override void OnDestroy() if (carSelectorGrab != null) { carSelectorGrab.Grabbed -= CarSelectorGrabbed; - carSelectorGrab.UnGrabbed -= CarSelectorUnGrabbed; + carSelectorGrab.Ungrabbed -= CarSelectorUnGrabbed; } if (Station?.pitstop != null) @@ -208,7 +207,7 @@ protected override void OnDestroy() if (faucetPositionerGrab != null) { faucetPositionerGrab.Grabbed -= FaucetCrankGrabbed; - faucetPositionerGrab.UnGrabbed -= FaucetCrankUnGrabbed; + faucetPositionerGrab.Ungrabbed -= FaucetCrankUnGrabbed; } if (faucetCrankSteppedJoint != null) @@ -464,21 +463,21 @@ private IEnumerator Init() } //Wait for levers an knobs to load - yield return new WaitUntil(() => GetComponentInChildren(true) != null); - carSelectorGrab = GetComponentInChildren(true); + yield return new WaitUntil(() => GetComponentInChildren(true) != null); + carSelectorGrab = GetComponentInChildren(true); if (carSelectorGrab != null) { Multiplayer.LogDebug(() => $"NetworkedPitStopStation.Init() Grab Handler found: {carSelectorGrab != null}, Name: {carSelectorGrab.name}"); carSelectorGrab.Grabbed += CarSelectorGrabbed; - carSelectorGrab.UnGrabbed += CarSelectorUnGrabbed; + carSelectorGrab.Ungrabbed += CarSelectorUnGrabbed; Station.pitstop.CarSelected += CarSelected; } // Water tower positioner handle var faucetGo = transform.parent.FindChildrenByName("FaucetCrank").FirstOrDefault(); - faucetPositionerGrab = faucetGo?.GetComponentInChildren(true); + faucetPositionerGrab = faucetGo?.GetComponentInChildren(true); faucetPositioner = faucetGo?.GetComponentInChildren(true); faucetCrankSteppedJoint = faucetGo?.GetComponentInChildren(true); @@ -486,7 +485,7 @@ private IEnumerator Init() { Multiplayer.LogDebug(() => $"NetworkedPitStopStation.Init() Grab Handler found: {carSelectorGrab != null}, Name: {carSelectorGrab.name}"); faucetPositionerGrab.Grabbed += FaucetCrankGrabbed; - faucetPositionerGrab.UnGrabbed += FaucetCrankUnGrabbed; + faucetPositionerGrab.Ungrabbed += FaucetCrankUnGrabbed; faucetCrankSteppedJoint.PositionChanged += FaucetCrankPositionChanged; } @@ -534,8 +533,8 @@ private IEnumerator Init() } var checker = resourceModule.GetComponentInChildren(); - var grab = resourceModule.GetComponentInChildren(); - var lever = resourceModule.GetComponentInChildren(); + var grab = resourceModule.GetComponentInChildren(); + var lever = resourceModule.GetComponentInChildren(); if (checker != null && grab != null) { @@ -707,7 +706,7 @@ public void SetCarSelection(int selection) /// /// Handles grab interactions for the car selector knob. /// - private void CarSelectorGrabbed() + private void CarSelectorGrabbed(ControlImplBase _) { if (NetworkLifecycle.Instance.IsProcessingPacket || (NetworkLifecycle.Instance.IsHost() && processingAsHost)) return; @@ -723,7 +722,7 @@ private void CarSelectorGrabbed() /// /// Handles end of grab (release) interactions for the car selector knob. /// - private void CarSelectorUnGrabbed() + private void CarSelectorUnGrabbed(ControlImplBase _) { if (NetworkLifecycle.Instance.IsProcessingPacket || (NetworkLifecycle.Instance.IsHost() && processingAsHost)) return; @@ -783,7 +782,7 @@ private void OnLeverPositionChange(LocoResourceModule module, int state) /// /// Handles grab interactions for the faucet positioning handle (water towers). /// - private void FaucetCrankGrabbed() + private void FaucetCrankGrabbed(ControlImplBase _) { if (NetworkLifecycle.Instance.IsProcessingPacket || (NetworkLifecycle.Instance.IsHost() && processingAsHost)) return; @@ -806,7 +805,7 @@ private void FaucetCrankGrabbed() /// /// Handles end of grab (release) interactions for the faucet positioning handle (water towers). /// - private void FaucetCrankUnGrabbed() + private void FaucetCrankUnGrabbed(ControlImplBase _) { if (NetworkLifecycle.Instance.IsProcessingPacket || (NetworkLifecycle.Instance.IsHost() && processingAsHost)) return; @@ -932,13 +931,14 @@ private IEnumerator ProcessBulkUpdate_Internal(int carCount, int carSelection, i bool grabbed = (resource.FillingState != LocoResourceModuleFillingState.None); bool isLocallyGrabbed = isResourceGrabbedDict.TryGetValue(resource.ResourceType, out var localGrabbed) && localGrabbed; - leverLookup.TryGetValue(resource.ResourceType, out LeverNonVR lever); - grabbedHandlerLookup.TryGetValue(resource.ResourceType, out GrabHandlerHingeJoint grab); + leverLookup.TryGetValue(resource.ResourceType, out LeverBase lever); + grabbedHandlerLookup.TryGetValue(resource.ResourceType, out LeverBase grab); if (!isLocallyGrabbed) { lever?.BlockControl(grabbed); - grab?.SetMovingDisabled(grabbed); + if (lever != null) + lever.InteractionAllowed = !grabbed; if (grabbed) grab?.ForceEndInteraction(); @@ -998,9 +998,9 @@ private IEnumerator ProcessBulkUpdate_Internal(int carCount, int carSelection, i /// The packet containing interaction data. public void ProcessInteractionPacketAsClient(CommonPitStopInteractionPacket packet) { - GrabHandlerHingeJoint grab = null; + LeverBase grab = null; RotaryAmplitudeChecker amplitudeChecker = null; - LeverNonVR lever = null; + LeverBase lever = null; LocoResourceModule resourceModule = null; // Validate interaction type @@ -1082,7 +1082,8 @@ public void ProcessInteractionPacketAsClient(CommonPitStopInteractionPacket pack if (!isLocallyGrabbed) { lever?.BlockControl(grabbed); - grab?.SetMovingDisabled(grabbed); + if (lever != null) + lever.InteractionAllowed = !grabbed; if (grabbed) grab?.ForceEndInteraction(); @@ -1120,12 +1121,16 @@ public void ProcessInteractionPacketAsClient(CommonPitStopInteractionPacket pack case PitStopStationInteractionType.CarSelectorGrab: //block interaction - carSelectorGrab?.SetMovingDisabled(true); + carSelectorGrab?.BlockControl(true); + if (carSelectorGrab != null) + carSelectorGrab.InteractionAllowed = false; break; case PitStopStationInteractionType.CarSelectorUngrab: //allow interaction - carSelectorGrab?.SetMovingDisabled(false); + carSelectorGrab?.BlockControl(false); + if (carSelectorGrab != null) + carSelectorGrab.InteractionAllowed = true; SetCarSelection((int)packet.Value); break; @@ -1135,13 +1140,17 @@ public void ProcessInteractionPacketAsClient(CommonPitStopInteractionPacket pack case PitStopStationInteractionType.FaucetGrab: //block interaction - faucetPositionerGrab?.SetMovingDisabled(true); + faucetPositionerGrab?.BlockControl(true); + if (faucetPositionerGrab != null) + faucetPositionerGrab.InteractionAllowed = false; break; case PitStopStationInteractionType.FaucetUngrab: //allow interaction - faucetPositionerGrab?.SetMovingDisabled(false); - + faucetPositionerGrab?.BlockControl(false); + if (faucetPositionerGrab != null) + faucetPositionerGrab.InteractionAllowed = true; + SetFaucetRotation((int)packet.Value); break; From 7a844fd1c1fc18406b4ed63a55a9e119568ad26f Mon Sep 17 00:00:00 2001 From: Macka Date: Sat, 22 Nov 2025 22:13:44 +1000 Subject: [PATCH 486/521] Refactor train coal/firebox networking Removed clientbound firebox state packet and related logic, shifting firebox state handling to local simulation. Added support for tender coal pile interactions via new ServerboundTenderCoalPacket, including client and server handling. Updated NetworkedTrainCar to wire up coal pile events and refactored method names for clarity. --- .../Networking/Train/NetworkedTrainCar.cs | 121 ++++++------------ .../Managers/Client/NetworkClient.cs | 20 ++- .../Managers/Server/NetworkServer.cs | 34 +++-- .../Train/ClientboundFireboxStatePacket.cs | 8 -- .../Train/ServerboundTenderCoalPacket.cs | 7 + 5 files changed, 77 insertions(+), 113 deletions(-) delete mode 100644 Multiplayer/Networking/Packets/Clientbound/Train/ClientboundFireboxStatePacket.cs create mode 100644 Multiplayer/Networking/Packets/Serverbound/Train/ServerboundTenderCoalPacket.cs diff --git a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs index 6d742664..fd39f415 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs @@ -5,21 +5,21 @@ using DV.MultipleUnit; using DV.Simulation.Brake; using DV.Simulation.Cars; +using DV.Simulation.Controllers; using DV.ThingTypes; using JetBrains.Annotations; using LocoSim.Definitions; using LocoSim.Implementations; using Multiplayer.Components.Networking.Player; -using Multiplayer.Networking.Data.Train; 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 System.Collections.Generic; using System.Collections; +using System.Collections.Generic; using System.Linq; using System.Reflection; -using System; using UnityEngine; namespace Multiplayer.Components.Networking.Train; @@ -98,7 +98,6 @@ public static bool TryGetNetId(Car car, out ushort netId) #endregion private const int MAX_COUPLER_ITERATIONS = 10; - private const float MAX_FIREBOX_DELTA = 0.1f; private const float MAX_PORT_DELTA = 0.001f; private const uint MIN_KINEMATIC_CYCLES = 10; @@ -117,6 +116,7 @@ public static bool TryGetNetId(Car car, out ushort netId) private bool hasSimFlow; private SimulationFlow simulationFlow; public FireboxSimController firebox; + public CoalPileSimController coalPile; private readonly Dictionary trainDamageDelegates = []; private HashSet dirtyPorts; @@ -137,7 +137,6 @@ public static bool TryGetNetId(Car car, out ushort netId) private bool carHealthDirty; private bool sendCouplers; private bool sendCables; - private bool fireboxDirty; public bool IsDestroying; @@ -208,7 +207,7 @@ public void Start() coupler.ChainScript.StateChanged += (state) => { Client_CouplerStateChange(state, coupler); }; } - //Multiplayer.LogDebug(() => $"NetworkedTrainCar.Start({TrainCar?.ID}, {NetId}) Couplers complete"); + Multiplayer.LogDebug(() => $"NetworkedTrainCar.Start({CurrentID}, {NetId}) Couplers complete"); SimController simController = GetComponent(); if (simController != null) @@ -226,12 +225,20 @@ public void Start() foreach (KeyValuePair kvp in simulationFlow.fullFuseIdToFuse) kvp.Value.StateUpdated += _ => { Common_OnFuseUpdated(kvp.Value); }; - if (simController.firebox != null) + firebox = simController.firebox; + coalPile = simController.coalPile; + + // Ports pulsed on an event (adding coal, igniting firebox, etc) + if (firebox != null) { - firebox = simController.firebox; - firebox.fireboxCoalControlPort.ValueUpdatedInternally += Client_OnAddCoal; //Player adding coal + firebox.fireboxCoalControlPort.ValueUpdatedInternally += Client_OnFireboxAddCoal; //Player adding coal firebox.fireboxIgnitionPort.ValueUpdatedInternally += Client_OnIgnite; //Player igniting firebox } + + if (coalPile != null) + { + coalPile.coalConsumePort.ValueUpdatedInternally += Client_OnCoalPileInteraction; //Coal being added/removed by shovel or feeder + } } //Multiplayer.LogDebug(() => $"NetworkedTrainCar.Start({TrainCar?.ID}, {NetId}) SimController complete"); @@ -246,8 +253,6 @@ public void Start() NetworkLifecycle.Instance.OnTick += Common_OnTick; - //Multiplayer.LogDebug(() => $"NetworkedTrainCar.Start({TrainCar?.ID}, {NetId}) CommonTick subscribed"); - if (NetworkLifecycle.Instance.IsHost()) { NetworkLifecycle.Instance.OnTick += Server_OnTick; @@ -261,8 +266,6 @@ public void Start() TrainCar.CarDamage.CarEffectiveHealthStateUpdate += Server_CarHealthUpdate; - //Multiplayer.LogDebug(() => $"NetworkedTrainCar.Start({TrainCar?.ID}, {NetId}) Subscribing to DamageControllers"); - //find all TrainDamages and subscribe if (TryGetComponent(out DamageController damageController) && damageController != null) { @@ -273,8 +276,6 @@ public void Start() .Where(value => value.Damage != null) .ToArray(); - //Multiplayer.LogDebug(() => $"NetworkedTrainCar.Start({TrainCar?.ID}, {NetId}) Subscribing to DamageControllers. TrainDamageFields: {trainDamageFields?.Length}"); - if (trainDamageFields != null && trainDamageFields.Length > 0) { for (int i = 0; i < trainDamageFields.Length; i++) @@ -295,19 +296,9 @@ public void Start() } } - //Multiplayer.LogDebug(() => $"NetworkedTrainCar.Start({TrainCar?.ID}, {NetId}) DamageControllers subscribed"); - brakeSystem.MainResPressureChanged += Server_MainResUpdate; brakeSystem.heatController.OverheatingActiveStateChanged += Server_BrakeHeatUpdate; - if (firebox != null) - { - firebox.fireboxContentsPort.ValueUpdatedInternally += Common_OnFireboxUpdate; - firebox.fireOnPort.ValueUpdatedInternally += Common_OnFireboxUpdate; - } - - //Multiplayer.LogDebug(() => $"NetworkedTrainCar.Start({TrainCar?.ID}, {NetId}) Firebox subscribed"); - StartCoroutine(Server_WaitForLogicCar()); } @@ -332,10 +323,15 @@ public void OnDisable() if (firebox != null) { - firebox.fireboxCoalControlPort.ValueUpdatedInternally -= Client_OnAddCoal; //Player adding coal + firebox.fireboxCoalControlPort.ValueUpdatedInternally -= Client_OnFireboxAddCoal; //Player adding coal firebox.fireboxIgnitionPort.ValueUpdatedInternally -= Client_OnIgnite; //Player igniting firebox } + if (coalPile != null) + { + coalPile.coalConsumePort.ValueUpdatedInternally -= Client_OnCoalPileInteraction; //Coal being added/removed by shovel or feeder + } + if (brakeSystem != null) { brakeSystem.HandbrakePositionChanged -= Common_OnHandbrakePositionChanged; @@ -372,12 +368,6 @@ public void OnDisable() brakeSystem.heatController.OverheatingActiveStateChanged -= Server_BrakeHeatUpdate; } - if (firebox != null) - { - firebox.fireboxContentsPort.ValueUpdatedInternally -= Common_OnFireboxUpdate; - firebox.fireOnPort.ValueUpdatedInternally -= Common_OnFireboxUpdate; - } - if (TrainCar.logicCar != null) { TrainCar.logicCar.CargoLoaded -= Server_OnCargoLoaded; @@ -433,7 +423,6 @@ public void Server_DirtyAllState() BogieTracksDirty = true; sendCouplers = true; sendCables = true; - fireboxDirty = firebox != null; //only dirty if exists if (!hasSimFlow) return; @@ -534,11 +523,6 @@ private void Server_BrakeHeatUpdate(bool overheatActive) brakeOverheatDirty = true; } - private void Server_FireboxUpdate(float normalizedPressure, float pressure) - { - fireboxDirty = true; - } - private void Server_CouplerUncoupled(object _, UncoupleEventArgs args) { sendCouplers |= args.dueToBrokenCouple; @@ -550,12 +534,11 @@ private void Server_OnTick(uint tick) return; Server_SendBrakeStates(); - Server_SendFireBoxState(); Server_SendCouplers(); Server_SendCables(); Server_SendCargoState(); - Server_SendHealthUpdate(); - Server_SendHealthState(); + Server_SendCargoHealthUpdate(); + Server_SendCarHealthState(); TicksSinceSync++; //keep track of last full sync } @@ -567,20 +550,12 @@ private void Server_SendBrakeStates() mainResPressureDirty = false; 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() - { - if (!fireboxDirty || firebox == null) - return; - - fireboxDirty = false; - NetworkLifecycle.Instance.Server.SendFireboxState(NetId, firebox.fireboxContentsPort.value, firebox.IsFireOn); + NetworkLifecycle.Instance.Server.SendBrakeState + ( + NetId, + brakeSystem.mainReservoirPressure, brakeSystem.brakePipePressure, brakeSystem.brakeCylinderPressure, + hc.overheatPercentage, hc.overheatReductionFactor, hc.temperature + ); } private void Server_SendCouplers() @@ -641,7 +616,7 @@ private void Server_SendCargoState() NetworkLifecycle.Instance.Server.SendCargoState(this, cargoIsLoading, CargoModelIndex); } - private void Server_SendHealthUpdate() + private void Server_SendCargoHealthUpdate() { if (!cargoHealthDirty) return; @@ -654,7 +629,7 @@ private void Server_SendHealthUpdate() NetworkLifecycle.Instance.Server.SendCargoHealthUpdate(NetId, TrainCar.CargoDamage.currentHealth); } - private void Server_SendHealthState() + private void Server_SendCarHealthState() { if (!carHealthDirty) return; @@ -853,20 +828,6 @@ private void Common_OnBrakeCylinderReleased() NetworkLifecycle.Instance.Client.SendBrakeCylinderReleased(NetId); } - private void Common_OnFireboxUpdate(float newFireboxValue) - { - if (NetworkLifecycle.Instance.IsProcessingPacket) - return; - - 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) { if (UnloadWatcher.isUnloading || NetworkLifecycle.Instance.IsProcessingPacket) @@ -935,7 +896,6 @@ public void Common_UpdatePorts(CommonTrainPortsPacket packet) Multiplayer.LogWarning($"Common_UpdatePorts() [{CurrentID}, {NetId}] Failed to find port \"{packet.PortIds[i]}\", Value: {packet.PortValues[i]}"); } } - } public void Common_UpdateFuses(CommonTrainFusesPacket packet) @@ -1433,7 +1393,7 @@ public void Client_ReceiveBrakeStateUpdate(ClientboundBrakeStateUpdatePacket pac brakeSystem.heatController.temperature = packet.Temperature; } - private void Client_OnAddCoal(float coalMassDelta) + private void Client_OnFireboxAddCoal(float coalMassDelta) { if (NetworkLifecycle.Instance.IsProcessingPacket) return; @@ -1441,7 +1401,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(() => $"Client_OnFireboxAddCoal({CurrentID}): coalMassDelta: {coalMassDelta}"); NetworkLifecycle.Instance.Client.SendAddCoal(NetId, coalMassDelta); } @@ -1453,20 +1413,17 @@ private void Client_OnIgnite(float ignition) if (ignition == 0f) return; - NetworkLifecycle.Instance.Client.LogDebug(() => $"Common_OnIgnite({TrainCar.ID})"); + NetworkLifecycle.Instance.Client.LogDebug(() => $"Common_OnIgnite({CurrentID})"); NetworkLifecycle.Instance.Client.SendFireboxIgnition(NetId); } - public void Client_ReceiveFireboxStateUpdate(float fireboxContents, bool isOn) + private void Client_OnCoalPileInteraction(float coalMassDelta) { - if (firebox == null) - return; - - if (!hasSimFlow) + if (NetworkLifecycle.Instance.IsProcessingPacket) return; - firebox.fireboxContentsPort.Value = fireboxContents; - firebox.fireOnPort.Value = isOn ? 1f : 0f; + NetworkLifecycle.Instance.Client.LogDebug(() => $"Client_OnCoalPileInteraction({CurrentID}): coalMassDelta: {coalMassDelta}"); + NetworkLifecycle.Instance.Client.SendTenderCoalPileInteraction(NetId, coalMassDelta); } public void Client_CouplerStateChange(ChainCouplerInteraction.State state, Coupler coupler) diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index f32435e8..a8695746 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -152,7 +152,6 @@ protected override void Subscribe() netPacketProcessor.SubscribeReusable(OnClientboundDestroyTrainCarPacket); netPacketProcessor.SubscribeReusable(OnClientboundTrainPhysicsPacket); netPacketProcessor.SubscribeReusable(OnCommonCouplerInteractionPacket); - //netPacketProcessor.SubscribeReusable(OnCommonTrainCouplePacket); netPacketProcessor.SubscribeReusable(OnCommonTrainUncouplePacket); netPacketProcessor.SubscribeReusable(OnCommonHoseConnectedPacket); netPacketProcessor.SubscribeReusable(OnCommonHoseDisconnectedPacket); @@ -165,7 +164,6 @@ protected override void Subscribe() netPacketProcessor.SubscribeReusable(OnCommonSimFlowPacket); netPacketProcessor.SubscribeReusable(OnCommonTrainFusesPacket); netPacketProcessor.SubscribeReusable(OnClientboundBrakeStateUpdatePacket); - netPacketProcessor.SubscribeReusable(OnClientboundFireboxStatePacket); netPacketProcessor.SubscribeReusable(OnClientboundCargoStatePacket); netPacketProcessor.SubscribeReusable(OnClientboundCargoHealthUpdatePacket); @@ -814,15 +812,6 @@ private void OnClientboundBrakeStateUpdatePacket(ClientboundBrakeStateUpdatePack //LogDebug(() => $"Received Brake Pressures netId {packet.NetId}: {packet.MainReservoirPressure}, {packet.IndependentPipePressure}, {packet.BrakePipePressure}, {packet.BrakeCylinderPressure}"); } - private void OnClientboundFireboxStatePacket(ClientboundFireboxStatePacket packet) - { - if (!NetworkedTrainCar.TryGet(packet.NetId, out NetworkedTrainCar networkedTrainCar)) - return; - - - networkedTrainCar.Client_ReceiveFireboxStateUpdate(packet.Contents, packet.IsOn); - } - private void OnClientboundCargoStatePacket(ClientboundCargoStatePacket packet) { if (!NetworkedTrainCar.TryGet(packet.NetId, out NetworkedTrainCar networkedTrainCar)) @@ -1435,6 +1424,15 @@ public void SendAddCoal(ushort netId, float coalMassDelta) }, DeliveryMethod.ReliableOrdered); } + public void SendTenderCoalPileInteraction(ushort netId, float coalMassDelta) + { + SendPacketToServer(new ServerboundTenderCoalPacket + { + NetId = netId, + CoalMassDelta = coalMassDelta + }, DeliveryMethod.ReliableOrdered); + } + public void SendFireboxIgnition(ushort netId) { SendPacketToServer(new ServerboundFireboxIgnitePacket diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index 3ef09e19..9aede707 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -171,6 +171,7 @@ protected override void Subscribe() netPacketProcessor.SubscribeReusable(OnCommonHandbrakePositionPacket); netPacketProcessor.SubscribeReusable(OnCommonPaintThemePacket); netPacketProcessor.SubscribeReusable(OnServerboundAddCoalPacket); + netPacketProcessor.SubscribeReusable(OnServerboundTenderCoalPacket); netPacketProcessor.SubscribeReusable(OnServerboundFireboxIgnitePacket); netPacketProcessor.SubscribeReusable(OnCommonTrainPortsPacket); netPacketProcessor.SubscribeReusable(OnCommonTrainFusesPacket); @@ -537,18 +538,6 @@ public void SendBrakeState(ushort netId, float mainReservoirPressure, float brak //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); - - LogDebug(() => $"Sending Firebox States netId {netId}: {fireboxContents}, {fireboxOn}"); - } - public void SendCargoState(NetworkedTrainCar netTraincar, bool isLoading, byte cargoModelIndex) { Car logicCar = netTraincar?.TrainCar?.logicCar; @@ -1322,7 +1311,28 @@ private void OnServerboundAddCoalPacket(ServerboundAddCoalPacket packet, ITransp if ((player.WorldPosition - networkedTrainCar.transform.position).sqrMagnitude <= carLength * carLength) networkedTrainCar.firebox?.fireboxCoalControlPort.ExternalValueUpdate(packet.CoalMassDelta); } + } + + private void OnServerboundTenderCoalPacket(ServerboundTenderCoalPacket packet, ITransportPeer peer) + { + if (!TryGetServerPlayer(peer, out ServerPlayer player)) + return; + if (!NetworkedTrainCar.TryGet(packet.NetId, out NetworkedTrainCar networkedTrainCar)) + return; + + // is value valid? + if (float.IsNaN(packet.CoalMassDelta)) + return; + + if (!NetworkLifecycle.Instance.IsHost(player)) + { + float carLength = CarSpawner.Instance.carLiveryToCarLength[networkedTrainCar.TrainCar.carLivery]; + + //is player close enough to add/remove coal? + if ((player.WorldPosition - networkedTrainCar.transform.position).sqrMagnitude <= carLength * carLength) + networkedTrainCar.coalPile?.coalConsumePort.ExternalValueUpdate(packet.CoalMassDelta); + } } private void OnServerboundFireboxIgnitePacket(ServerboundFireboxIgnitePacket packet, ITransportPeer peer) diff --git a/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundFireboxStatePacket.cs b/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundFireboxStatePacket.cs deleted file mode 100644 index bbf0250a..00000000 --- a/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundFireboxStatePacket.cs +++ /dev/null @@ -1,8 +0,0 @@ -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/Train/ServerboundTenderCoalPacket.cs b/Multiplayer/Networking/Packets/Serverbound/Train/ServerboundTenderCoalPacket.cs new file mode 100644 index 00000000..2efe682b --- /dev/null +++ b/Multiplayer/Networking/Packets/Serverbound/Train/ServerboundTenderCoalPacket.cs @@ -0,0 +1,7 @@ +namespace Multiplayer.Networking.Packets.Serverbound.Train; + +public class ServerboundTenderCoalPacket +{ + public ushort NetId { get; set; } + public float CoalMassDelta { get; set; } +} From 4e7272ff330bd28614f8ad1d6bce826ff10f03b2 Mon Sep 17 00:00:00 2001 From: Macka Date: Sat, 22 Nov 2025 22:15:20 +1000 Subject: [PATCH 487/521] Comment out debug logging in networking and patch files Debug log statements have been commented out in NetworkedJob, NetworkServer, TaskPatch, and ScriptStripperRuntimePatch to reduce log verbosity and improve performance. A general log message remains in NetworkServer for cargo state updates. --- .../Components/Networking/Jobs/NetworkedJob.cs | 4 ++-- .../Networking/Jobs/WarehouseMachineLookup.cs | 2 +- .../Networking/Managers/Server/NetworkServer.cs | 4 +++- Multiplayer/Patches/Jobs/TaskPatch.cs | 2 +- .../Patches/Train/ScriptStripperRuntimePatch.cs | 17 ++++++++--------- 5 files changed, 15 insertions(+), 14 deletions(-) diff --git a/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs b/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs index 4293e09a..4ee5ecd5 100644 --- a/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs +++ b/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs @@ -180,7 +180,7 @@ public void Initialize(Job job, NetworkedStationController station) { pendingJobTasks.Remove(job); - Multiplayer.LogDebug(() => $"NetworkedJob.Initialize(): Found {taskList.Count} pending tasks for jobId {job.ID}"); + //Multiplayer.LogDebug(() => $"NetworkedJob.Initialize(): Found {taskList.Count} pending tasks for jobId {job.ID}"); foreach (var task in taskList) CreateNetworkedTask(task); @@ -188,7 +188,7 @@ public void Initialize(Job job, NetworkedStationController station) tasksInitialized = true; - Multiplayer.LogDebug(() => $"NetworkedJob.Initialize(): Initialized NetworkedJob for jobId {job.ID} with {Job.tasks.Count} tasks"); + //Multiplayer.LogDebug(() => $"NetworkedJob.Initialize(): Initialized NetworkedJob for jobId {job.ID} with {Job.tasks.Count} tasks"); } private void AddToCache() diff --git a/Multiplayer/Components/Networking/Jobs/WarehouseMachineLookup.cs b/Multiplayer/Components/Networking/Jobs/WarehouseMachineLookup.cs index a1910a84..afeb48f8 100644 --- a/Multiplayer/Components/Networking/Jobs/WarehouseMachineLookup.cs +++ b/Multiplayer/Components/Networking/Jobs/WarehouseMachineLookup.cs @@ -56,7 +56,7 @@ public static bool TryGetNetId(WarehouseMachine machine, out ushort netId) { netId = GenerateNetId(machine.ID); var temp = netId; - Multiplayer.LogDebug(() => $"Trying to get NetID for WarehouseMachine on track {machine?.WarehouseTrack?.ID}, machineID: {machine?.ID}, netId: {temp}"); + //Multiplayer.LogDebug(() => $"Trying to get NetID for WarehouseMachine on track {machine?.WarehouseTrack?.ID}, machineID: {machine?.ID}, netId: {temp}"); if (netIdToWarehouseMachine.ContainsKey(netId)) return true; diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index 9aede707..a61773bf 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -542,7 +542,9 @@ public void SendCargoState(NetworkedTrainCar netTraincar, bool isLoading, byte c { Car logicCar = netTraincar?.TrainCar?.logicCar; - LogDebug(() => $"SendCargoState({netTraincar?.CurrentID}, isLoading: {isLoading}, cargoModelIndex: {cargoModelIndex}), logicCar: {logicCar?.ID}, WareHouseMachineID: {logicCar.CargoOriginWarehouse?.ID}, warehouse track: {logicCar.CargoOriginWarehouse?.WarehouseTrack?.ID}"); + //LogDebug(() => $"SendCargoState({netTraincar?.CurrentID}, isLoading: {isLoading}, cargoModelIndex: {cargoModelIndex}), logicCar: {logicCar?.ID}, WareHouseMachineID: {logicCar.CargoOriginWarehouse?.ID}, warehouse track: {logicCar.CargoOriginWarehouse?.WarehouseTrack?.ID}"); + + Log($"Sending Cargo State for {netTraincar?.CurrentID}, isLoading: {isLoading}, cargoModelIndex: {cargoModelIndex}"); if (logicCar == null) { diff --git a/Multiplayer/Patches/Jobs/TaskPatch.cs b/Multiplayer/Patches/Jobs/TaskPatch.cs index 454466e3..6e26e363 100644 --- a/Multiplayer/Patches/Jobs/TaskPatch.cs +++ b/Multiplayer/Patches/Jobs/TaskPatch.cs @@ -20,7 +20,7 @@ public static void SetStatePrefix(Task __instance, TaskState newState) return; - Multiplayer.LogDebug(()=>$"Task.SetState() called for jobId: {__instance.Job.ID}, taskType: {__instance.InstanceTaskType}, newState: {newState}"); + //Multiplayer.LogDebug(()=>$"Task.SetState() called for jobId: {__instance.Job.ID}, taskType: {__instance.InstanceTaskType}, newState: {newState}"); if (!NetworkedTask.TryGetNetId(__instance, out var taskNetId) ||taskNetId == 0) { diff --git a/Multiplayer/Patches/Train/ScriptStripperRuntimePatch.cs b/Multiplayer/Patches/Train/ScriptStripperRuntimePatch.cs index 697129df..57108a6b 100644 --- a/Multiplayer/Patches/Train/ScriptStripperRuntimePatch.cs +++ b/Multiplayer/Patches/Train/ScriptStripperRuntimePatch.cs @@ -1,7 +1,6 @@ using DV.Optimizers; using HarmonyLib; using Multiplayer.Components.Networking; -using Multiplayer.Components.Networking.Train; using UnityEngine; namespace Multiplayer.Patches.Train; @@ -40,20 +39,20 @@ public static bool Strip(GameObject goToStrip) { if(!colliders[i].TryGetComponent(out _)) Object.Destroy(colliders[i]); - else - { - Multiplayer.LogDebug(() => $"ScriptStripperRuntimePatch.Strip() Keeping collider {colliders[i].gameObject.GetPath()} for {trainCar.ID}, has LocoResourceReceiver component."); - } + //else + //{ + // Multiplayer.LogDebug(() => $"ScriptStripperRuntimePatch.Strip() Keeping collider {colliders[i].gameObject.GetPath()} for {trainCar.ID}, has LocoResourceReceiver component."); + //} } for (int i = 0; i < scripts.Length; i++) { if (!scripts[i].GetType().Equals(typeof(LocoResourceReceiver))) Object.Destroy(scripts[i]); - else - { - Multiplayer.LogDebug(() => $"ScriptStripperRuntimePatch.Strip() Keeping script {scripts[i].gameObject.GetPath()} for {trainCar.ID}, is LocoResourceReceiver component."); - } + //else + //{ + // Multiplayer.LogDebug(() => $"ScriptStripperRuntimePatch.Strip() Keeping script {scripts[i].gameObject.GetPath()} for {trainCar.ID}, is LocoResourceReceiver component."); + //} } return false; From c384f63ace0367aba6ab5f933e43fcf2f29f86c0 Mon Sep 17 00:00:00 2001 From: Macka Date: Sun, 23 Nov 2025 06:33:46 +1000 Subject: [PATCH 488/521] Refactor train port and fuse IDs to use uint Changed train port and fuse identifiers from string to uint throughout networking components and packets for improved efficiency and consistency. Added mapping utilities in NetworkedTrainCar to convert between string IDs and uint net IDs. Introduced new packets for train control authority state and requests. --- .../Networking/Train/NetworkedTrainCar.cs | 147 +++++++++++++----- .../Managers/Client/NetworkClient.cs | 4 +- .../Common/Train/CommonTrainFusesPacket.cs | 4 +- .../Common/Train/CommonTrainPortsPacket.cs | 4 +- 4 files changed, 114 insertions(+), 45 deletions(-) diff --git a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs index fd39f415..943958e0 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs @@ -101,6 +101,49 @@ public static bool TryGetNetId(Car car, out ushort netId) private const float MAX_PORT_DELTA = 0.001f; private const uint MIN_KINEMATIC_CYCLES = 10; + #region Port and Fuse Map + + private static readonly Dictionary netIdToPort = []; + private static readonly Dictionary portToNetId = []; + private static readonly Dictionary netIdToFuse = []; + private static readonly Dictionary fuseToNetId = []; + + static uint GetPortNetId(string portId) + { + if (portToNetId.TryGetValue(portId, out var netId)) + return netId; + + netId = StringHashing.Fnv1aHash(portId); + netIdToPort[netId] = portId; + portToNetId[portId] = netId; + + return netId; + } + + static string GetPort(uint netId) + { + netIdToPort.TryGetValue(netId, out var portId); + return portId; + } + + static uint GetFuseNetId(string fuseId) + { + if (fuseToNetId.TryGetValue(fuseId, out var netId)) + return netId; + + netId = StringHashing.Fnv1aHash(fuseId); + netIdToFuse[netId] = fuseId; + fuseToNetId[fuseId] = netId; + + return netId; + } + static string GetFuse(uint netId) + { + netIdToFuse.TryGetValue(netId, out var portId); + return portId; + } + #endregion + public string CurrentID { get; private set; } public TrainCar TrainCar; @@ -119,10 +162,9 @@ public static bool TryGetNetId(Car car, out ushort netId) public CoalPileSimController coalPile; private readonly Dictionary trainDamageDelegates = []; - private HashSet dirtyPorts; - private Dictionary lastSentPortValues; - private HashSet dirtyFuses; - private float lastSentFireboxValue; + private HashSet dirtyPorts; + private Dictionary lastSentPortValues; + private HashSet dirtyFuses; private readonly Dictionary lastSentTrainDamages = []; private bool handbrakeDirty; @@ -140,13 +182,17 @@ public static bool TryGetNetId(Car car, out ushort netId) public bool IsDestroying; + #region Server Variables //Coupler interaction private bool frontInteracting = false; private bool rearInteracting = false; private ServerPlayer frontInteractionPlayer; private ServerPlayer rearInteractionPlayer; - #region Client + + #endregion + + #region Client Variables public bool Client_Initialized { get; private set; } public TickedQueue Client_trainSpeedQueue; @@ -160,6 +206,8 @@ public static bool TryGetNetId(Car car, out ushort netId) private Coupler originalCoupledTo; private uint kinematicCycles = 0; + + #endregion #endregion protected override bool IsIdServerAuthoritative => true; @@ -215,15 +263,21 @@ public void Start() hasSimFlow = true; simulationFlow = simController.SimulationFlow; - dirtyPorts = new HashSet(simulationFlow.fullPortIdToPort.Count); - lastSentPortValues = new Dictionary(dirtyPorts.Count); + dirtyPorts = new HashSet(simulationFlow.fullPortIdToPort.Count); + lastSentPortValues = new Dictionary(dirtyPorts.Count); foreach (KeyValuePair kvp in simulationFlow.fullPortIdToPort) + { + _ = GetPortNetId(kvp.Key); //ensure this port is registered if (kvp.Value.valueType == PortValueType.CONTROL || NetworkLifecycle.Instance.IsHost()) kvp.Value.ValueUpdatedInternally += _ => { Common_OnPortUpdated(kvp.Value); }; + } - dirtyFuses = new HashSet(simulationFlow.fullFuseIdToFuse.Count); + dirtyFuses = new HashSet(simulationFlow.fullFuseIdToFuse.Count); foreach (KeyValuePair kvp in simulationFlow.fullFuseIdToFuse) + { + _ = GetFuseNetId(kvp.Key); //ensure this fuse is registered kvp.Value.StateUpdated += _ => { Common_OnFuseUpdated(kvp.Value); }; + } firebox = simController.firebox; coalPile = simController.coalPile; @@ -428,31 +482,39 @@ public void Server_DirtyAllState() return; foreach (string portId in simulationFlow.fullPortIdToPort.Keys) { - dirtyPorts.Add(portId); + var netId = GetPortNetId(portId); + dirtyPorts.Add(netId); } foreach (string fuseId in simulationFlow.fullFuseIdToFuse.Keys) - dirtyFuses.Add(fuseId); + { + var netId = GetFuseNetId(fuseId); + dirtyFuses.Add(netId); + } } public bool Server_ValidateClientSimFlowPacket(ServerPlayer player, CommonTrainPortsPacket packet) { // Only allow control ports to be updated by clients if (hasSimFlow) - foreach (string portId in packet.PortIds) + foreach (uint portNetId in packet.PortIds) + { + + var portId = GetPort(portNetId); if (simulationFlow.TryGetPort(portId, out Port port)) { if (port.valueType != PortValueType.CONTROL) { - NetworkLifecycle.Instance.Server.LogWarning($"Player {player.Username} tried to send a non-control port! ({portId} on [{TrainCar?.ID}, {NetId}])"); + NetworkLifecycle.Instance.Server.LogWarning($"Player {player.Username} tried to send a non-control port! ([{portId}, {portNetId}] on [{CurrentID}, {NetId}])"); Common_DirtyPorts(packet.PortIds); return false; } } else { - NetworkLifecycle.Instance.Server.LogWarning($"Player {player.Username} sent portId: {portId}, value type: {port.valueType}, but the port was not found"); + NetworkLifecycle.Instance.Server.LogWarning($"Player {player.Username} sent portId: {portNetId}, value type: {port.valueType}, but the port was not found"); } + } // Only allow the player to update ports on the car they are in/near if (player.CarId == packet.NetId) @@ -724,40 +786,39 @@ private void Common_SendHandbrakePosition() NetworkLifecycle.Instance.Client.SendHandbrakePositionChanged(NetId, brakeSystem.handbrakePosition); } - public void Common_DirtyPorts(string[] portIds) + public void Common_DirtyPorts(uint[] portNetIds) { if (!hasSimFlow) return; - foreach (string portId in portIds) + foreach (uint portNetId in portNetIds) { + var portId = GetPort(portNetId); 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!"); + Multiplayer.LogWarning($"Tried to dirty port [{portId}, {portNetId}] on {CurrentID} but port doesn't exist!"); continue; } - dirtyPorts.Add(portId); + dirtyPorts.Add(portNetId); } } - public void Common_DirtyFuses(string[] fuseIds) + public void Common_DirtyFuses(uint[] fuseNetIds) { if (!hasSimFlow) return; - foreach (string fuseId in fuseIds) + foreach (uint fuseNetId in fuseNetIds) { + var fuseId = GetFuse(fuseNetId); 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!"); + Multiplayer.LogWarning($"Tried to dirty port [{fuseId}, {fuseNetId}] on {CurrentID} but it doesn't exist!"); continue; } - dirtyFuses.Add(fuseId); + dirtyFuses.Add(fuseNetId); } } @@ -767,15 +828,16 @@ private void Common_SendPorts() return; int i = 0; - string[] portIds = dirtyPorts.ToArray(); + uint[] portIds = dirtyPorts.ToArray(); float[] portValues = new float[portIds.Length]; - foreach (string portId in dirtyPorts) + foreach (uint portNetId in dirtyPorts) { + var portId = GetPort(portNetId); if (simulationFlow.TryGetPort(portId, out Port port)) { float value = port.Value; portValues[i] = value; - lastSentPortValues[portId] = value; + lastSentPortValues[portNetId] = value; } else { @@ -796,15 +858,16 @@ private void Common_SendFuses() return; int i = 0; - string[] fuseIds = dirtyFuses.ToArray(); + uint[] fuseIds = dirtyFuses.ToArray(); bool[] fuseValues = new bool[fuseIds.Length]; - foreach (string fuseId in dirtyFuses) + foreach (uint fuseNetId in dirtyFuses) { + var fuseId = GetFuse(fuseNetId); if (simulationFlow.TryGetFuse(fuseId, out Fuse fuse)) fuseValues[i] = fuse.State; else - Multiplayer.LogWarning($"SendFuses() [{CurrentID}, {NetId}] Failed to find fuse \"{fuseId}\""); + Multiplayer.LogWarning($"Failed to send fuse \"{fuseId}\" for [{CurrentID}, {NetId}]"); i++; } @@ -835,21 +898,22 @@ private void Common_OnPortUpdated(Port port) if (float.IsNaN(port.prevValue) && float.IsNaN(port.Value)) return; - bool hasLastSent = lastSentPortValues.TryGetValue(port.id, out float lastSentValue); + var netId = GetPortNetId(port.id); + bool hasLastSent = lastSentPortValues.TryGetValue(netId, out float lastSentValue); float delta = Mathf.Abs(lastSentValue - port.Value); if (port.valueType == PortValueType.STATE) { if (!hasLastSent || lastSentValue != port.Value) { - dirtyPorts.Add(port.id); + dirtyPorts.Add(netId); } } else { if (!hasLastSent || delta > MAX_PORT_DELTA || (port.Value == 0 && lastSentValue != 0)) { - dirtyPorts.Add(port.id); + dirtyPorts.Add(netId); } } } @@ -871,8 +935,8 @@ private void Common_OnFuseUpdated(Fuse fuse) { if (UnloadWatcher.isUnloading || NetworkLifecycle.Instance.IsProcessingPacket) return; - - dirtyFuses.Add(fuse.id); + var netId = GetFuseNetId(fuse.id); + dirtyFuses.Add(netId); } public void Common_UpdatePorts(CommonTrainPortsPacket packet) @@ -882,7 +946,8 @@ public void Common_UpdatePorts(CommonTrainPortsPacket packet) for (int i = 0; i < packet.PortIds.Length; i++) { - if (simulationFlow.TryGetPort(packet.PortIds[i], out Port port)) + var portId = GetPort(packet.PortIds[i]); + if (simulationFlow.TryGetPort(portId, out Port port)) { float value = packet.PortValues[i]; @@ -893,7 +958,7 @@ public void Common_UpdatePorts(CommonTrainPortsPacket packet) } else { - Multiplayer.LogWarning($"Common_UpdatePorts() [{CurrentID}, {NetId}] Failed to find port \"{packet.PortIds[i]}\", Value: {packet.PortValues[i]}"); + Multiplayer.LogWarning($"Failed to update port [\"portId\", {packet.PortIds[i]}] with value \"{packet.PortValues[i]}\" for [{CurrentID}, {NetId}]"); } } } @@ -904,10 +969,13 @@ public void Common_UpdateFuses(CommonTrainFusesPacket packet) return; for (int i = 0; i < packet.FuseIds.Length; i++) - if (simulationFlow.TryGetFuse(packet.FuseIds[i], out Fuse fuse)) + { + var fuseId = GetFuse(packet.FuseIds[i]); + if (simulationFlow.TryGetFuse(fuseId, 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]}"); + Multiplayer.LogWarning($"Failed to update fuse [\"fuseId\", {packet.FuseIds[i]}] with value \"{packet.FuseValues[i]}\" for [{CurrentID}, {NetId}]"); + } } public void Common_ReceiveCouplerInteraction(CommonCouplerInteractionPacket packet) @@ -1338,6 +1406,7 @@ public void Client_ReceiveTrainPhysicsUpdate(in TrainsetMovementPart movementPar if (TrainCar.rb != null) { TrainCar.rb.MovePosition(worldPos); + //TrainCar.rb.MoveRotation(movementPart.Rotation); // removed due to motion sickness issues } diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index a8695746..13ea9a88 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -1441,7 +1441,7 @@ public void SendFireboxIgnition(ushort netId) }, DeliveryMethod.ReliableOrdered); } - public void SendPorts(ushort netId, string[] portIds, float[] portValues) + public void SendPorts(ushort netId, uint[] portIds, float[] portValues) { SendPacketToServer(new CommonTrainPortsPacket { @@ -1460,7 +1460,7 @@ public void SendPorts(ushort netId, string[] portIds, float[] portValues) */ } - public void SendFuses(ushort netId, string[] fuseIds, bool[] fuseValues) + public void SendFuses(ushort netId, uint[] fuseIds, bool[] fuseValues) { SendPacketToServer(new CommonTrainFusesPacket { diff --git a/Multiplayer/Networking/Packets/Common/Train/CommonTrainFusesPacket.cs b/Multiplayer/Networking/Packets/Common/Train/CommonTrainFusesPacket.cs index 4b723a35..5550bbdc 100644 --- a/Multiplayer/Networking/Packets/Common/Train/CommonTrainFusesPacket.cs +++ b/Multiplayer/Networking/Packets/Common/Train/CommonTrainFusesPacket.cs @@ -1,8 +1,8 @@ -namespace Multiplayer.Networking.Packets.Common.Train; +namespace Multiplayer.Networking.Packets.Common.Train; public class CommonTrainFusesPacket { public ushort NetId { get; set; } - public string[] FuseIds { get; set; } + public uint[] FuseIds { get; set; } public bool[] FuseValues { get; set; } } diff --git a/Multiplayer/Networking/Packets/Common/Train/CommonTrainPortsPacket.cs b/Multiplayer/Networking/Packets/Common/Train/CommonTrainPortsPacket.cs index 649cff84..c23720a1 100644 --- a/Multiplayer/Networking/Packets/Common/Train/CommonTrainPortsPacket.cs +++ b/Multiplayer/Networking/Packets/Common/Train/CommonTrainPortsPacket.cs @@ -1,8 +1,8 @@ -namespace Multiplayer.Networking.Packets.Common.Train; +namespace Multiplayer.Networking.Packets.Common.Train; public class CommonTrainPortsPacket { public ushort NetId { get; set; } - public string[] PortIds { get; set; } + public uint[] PortIds { get; set; } public float[] PortValues { get; set; } } From dc70e3b66c66adf9597aa24b1736a28018b63ba0 Mon Sep 17 00:00:00 2001 From: Macka Date: Sun, 23 Nov 2025 20:34:08 +1000 Subject: [PATCH 489/521] Refactor task state update logic to reduce spamming Introduced NetworkedTask.SetState to handle state changes properly, reducing traffic. --- .../Networking/Jobs/NetworkedTask.cs | 21 +++++++++++++++++++ Multiplayer/Patches/Jobs/TaskPatch.cs | 7 +++---- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/Multiplayer/Components/Networking/Jobs/NetworkedTask.cs b/Multiplayer/Components/Networking/Jobs/NetworkedTask.cs index 77c3fa04..a938b837 100644 --- a/Multiplayer/Components/Networking/Jobs/NetworkedTask.cs +++ b/Multiplayer/Components/Networking/Jobs/NetworkedTask.cs @@ -16,6 +16,11 @@ public static bool TryGet(ushort netId, out NetworkedTask networkedTask) return b; } + public static bool TryGet(Task task, out NetworkedTask networkedTask) + { + return taskToNetworkedTask.TryGetValue(task, out networkedTask); + } + public static bool TryGet(ushort netId, out Task task) { task = null; @@ -45,6 +50,10 @@ public static bool TryGetNetId(Task task, out ushort netId) public Task Task { get; private set; } + private float lastStartTime; + private float lastFinishTime; + private TaskState lastState; + public void Initialize(Task task, ushort netId = 0) { if (task == null) @@ -73,4 +82,16 @@ protected override void OnDestroy() if (Task != null) taskToNetworkedTask.Remove(Task); } + + public void SetState(TaskState newState) + { + if (lastState == newState && lastStartTime == Task.taskStartTime && lastFinishTime == Task.taskFinishTime) + return; + + lastState = newState; + lastStartTime = Task.taskStartTime; + lastFinishTime = Task.taskFinishTime; + + NetworkLifecycle.Instance.Server.SendTaskUpdate(NetId, newState, Task.taskStartTime, Task.taskFinishTime); + } } diff --git a/Multiplayer/Patches/Jobs/TaskPatch.cs b/Multiplayer/Patches/Jobs/TaskPatch.cs index 6e26e363..796c43bf 100644 --- a/Multiplayer/Patches/Jobs/TaskPatch.cs +++ b/Multiplayer/Patches/Jobs/TaskPatch.cs @@ -21,14 +21,13 @@ public static void SetStatePrefix(Task __instance, TaskState newState) //Multiplayer.LogDebug(()=>$"Task.SetState() called for jobId: {__instance.Job.ID}, taskType: {__instance.InstanceTaskType}, newState: {newState}"); - - if (!NetworkedTask.TryGetNetId(__instance, out var taskNetId) ||taskNetId == 0) + if(!NetworkedTask.TryGet(__instance, out var networkedTask)) { - Multiplayer.LogError($"Task.SetState() could not find task index for jobId: {__instance.Job.ID}, taskType: {__instance.InstanceTaskType}"); + Multiplayer.LogError($"Task.SetState() could not find NetworkedTask for jobId: {__instance.Job.ID}, taskType: {__instance.InstanceTaskType}"); return; } - NetworkLifecycle.Instance.Server.SendTaskUpdate(taskNetId, newState, __instance.taskStartTime, __instance.taskFinishTime); + networkedTask.SetState(newState); } [HarmonyPatch(nameof(Task.SetJobBelonging))] From 8d27565fe507684d78e3be84ba4404cc89841961 Mon Sep 17 00:00:00 2001 From: Macka Date: Sun, 23 Nov 2025 20:49:27 +1000 Subject: [PATCH 490/521] Add train control authority request and update system Implements a client-server system for requesting and updating control authority over train controls. Adds new packets for authority requests and updates, updates NetworkedTrainCar to manage control authority state, and integrates authority handling into client and server managers. This enables multiplayer blocking and releasing of train controls based on player proximity and interaction. --- .../Networking/Train/NetworkedTrainCar.cs | 260 +++++++++++++++++- .../Managers/Client/NetworkClient.cs | 23 ++ .../Managers/Server/NetworkServer.cs | 32 ++- ...tboundTrainControlAuthorityUpdatePacket.cs | 18 ++ .../ServerboundTrainControlAuthorityPacket.cs | 12 + 5 files changed, 342 insertions(+), 3 deletions(-) create mode 100644 Multiplayer/Networking/Packets/Clientbound/Train/ClientboundTrainControlAuthorityUpdatePacket.cs create mode 100644 Multiplayer/Networking/Packets/Serverbound/Train/ServerboundTrainControlAuthorityPacket.cs diff --git a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs index 943958e0..97b5e82f 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs @@ -1,6 +1,7 @@ using DV.CabControls; using DV.Customization.Paint; using DV.Damage; +using DV.HUD; using DV.Logic.Job; using DV.MultipleUnit; using DV.Simulation.Brake; @@ -114,6 +115,9 @@ static uint GetPortNetId(string portId) return netId; netId = StringHashing.Fnv1aHash(portId); + + Multiplayer.LogDebug(() => $"GetPortNetId({portId}) Registering with {netId}"); + netIdToPort[netId] = portId; portToNetId[portId] = netId; @@ -158,6 +162,7 @@ static string GetFuse(uint netId) private bool hasSimFlow; private SimulationFlow simulationFlow; + SimController simController; public FireboxSimController firebox; public CoalPileSimController coalPile; private readonly Dictionary trainDamageDelegates = []; @@ -190,6 +195,8 @@ static string GetFuse(uint netId) private ServerPlayer frontInteractionPlayer; private ServerPlayer rearInteractionPlayer; + private readonly Dictionary portAuthority = []; + #endregion #region Client Variables @@ -207,7 +214,13 @@ static string GetFuse(uint netId) private uint kinematicCycles = 0; + private readonly Dictionary portNetIdToBlockState = []; + private readonly Dictionary portNetIdToControl = []; + private readonly Dictionary controlToPortNetId = []; #endregion + + #region Common Variables + #endregion protected override bool IsIdServerAuthoritative => true; @@ -257,19 +270,28 @@ public void Start() Multiplayer.LogDebug(() => $"NetworkedTrainCar.Start({CurrentID}, {NetId}) Couplers complete"); - SimController simController = GetComponent(); + simController = GetComponent(); if (simController != null) { hasSimFlow = true; simulationFlow = simController.SimulationFlow; + TrainCar.InteriorLoaded += OnTrainCarInteriorLoaded; + TrainCar.InteriorAboutToBeUnloaded += OnTrainCarInteriorUnloaded; + + if (TrainCar.loadedInterior != null) + OnTrainCarInteriorLoaded(TrainCar.loadedInterior.gameObject); + dirtyPorts = new HashSet(simulationFlow.fullPortIdToPort.Count); lastSentPortValues = new Dictionary(dirtyPorts.Count); foreach (KeyValuePair kvp in simulationFlow.fullPortIdToPort) { _ = GetPortNetId(kvp.Key); //ensure this port is registered if (kvp.Value.valueType == PortValueType.CONTROL || NetworkLifecycle.Instance.IsHost()) + { + Multiplayer.LogDebug(() => $"NetworkedTrainCar.Start({CurrentID}, {NetId}) Subscribing to port {kvp.Key}"); kvp.Value.ValueUpdatedInternally += _ => { Common_OnPortUpdated(kvp.Value); }; + } } dirtyFuses = new HashSet(simulationFlow.fullFuseIdToFuse.Count); @@ -359,6 +381,115 @@ public void Start() NetworkLifecycle.Instance?.Client.SendTrainSyncRequest(NetId); } + private void OnTrainCarInteriorLoaded(GameObject interior) + { + Multiplayer.LogDebug(()=> $"OnTrainCarInteriorLoaded() {CurrentID}, interior is null: {interior == null}"); + + StartCoroutine(WaitForInterior()); + } + + private IEnumerator WaitForInterior() + { + float time = Time.time; + InteriorControlsManager interiorControlsManager = null; + + yield return new WaitUntil + ( + ()=> + { + return TrainCar.loadedInterior != null || Time.time - time > 2000f; + } + ); + + yield return new WaitForFixedUpdate(); + + if (TrainCar.loadedInterior == null) + { + Multiplayer.LogError($"TrainCar {CurrentID} failed to load an interior"); + yield break; + } + + time = Time.time; + + yield return new WaitUntil + ( + ()=> + { + return TrainCar.loadedInterior.TryGetComponent(out interiorControlsManager) || Time.time - time > 2000f; + } + ); + + yield return new WaitForFixedUpdate(); + + if (!interiorControlsManager.Initialized) + { + interiorControlsManager.OnInitialized += HookControls; + yield break; + } + + yield return new WaitForSecondsRealtime(2f); + + HookControls(interiorControlsManager); + } + + private void HookControls(InteriorControlsManager interiorControlsManager) + { + interiorControlsManager.OnInitialized -= HookControls; + + // Find all control overrides + foreach (var control in interiorControlsManager.controls.Values) + { + var controlPortId = control.overridableBaseControl?.portId; + + if (string.IsNullOrEmpty(controlPortId)) + { + Multiplayer.LogDebug(() => $"HookControls() Control, {NetId}] has no controlPortId on car {CurrentID}"); + continue; + } + + Multiplayer.LogDebug(() => $"HookControls() Control [{controlPortId}] found on car {CurrentID}"); + var netId = GetPortNetId(controlPortId); + + + if (control.controlImplBase == null) + { + Multiplayer.LogDebug(() => $"HookControls() Control [{controlPortId}, {netId}] has no implementation on car {CurrentID}"); + continue; + } + + Multiplayer.LogDebug(() => $"HookControls() Control [{controlPortId}, {netId}] hooking events on car {CurrentID}, hash: {control.controlImplBase.GetHashCode()}, instance: {control.controlImplBase.GetInstanceID()}"); + + portNetIdToControl[netId] = control.controlImplBase; + controlToPortNetId[control.controlImplBase] = netId; + + control.controlImplBase.Grabbed += Client_ControlGrabbed; + control.controlImplBase.Ungrabbed += Client_ControlUngrabbed; + + if (portNetIdToBlockState.TryGetValue(netId, out var isBlocked) && isBlocked) + { + Multiplayer.LogDebug(() => $"WaitForInterior() Control [{controlPortId}, {netId}] is blocked on car {CurrentID}"); + } + } + } + + private void OnTrainCarInteriorUnloaded(GameObject interior) + { + Multiplayer.LogDebug(()=>$"OnTrainCarInteriorUnloaded() {CurrentID}"); + + foreach (var control in controlToPortNetId.Keys) + { + if (control == null) + continue; + + control.Grabbed -= Client_ControlGrabbed; + control.Ungrabbed -= Client_ControlUngrabbed; + } + + portNetIdToControl.Clear(); + controlToPortNetId.Clear(); + } + + public void OnDisable() { if (UnloadWatcher.isQuitting) @@ -744,6 +875,51 @@ public bool Server_ValidateCouplerInteraction(CommonCouplerInteractionPacket pac return true; } + public void Server_ReceiveAuthorityRequest(uint portNetId, ServerPlayer player, bool requestAuthority) + { + portAuthority.TryGetValue(portNetId, out var currentAuth); + + if (requestAuthority) + { + if (currentAuth == null) + { + float carLength = CarSpawner.Instance.carLiveryToCarLength[TrainCar.carLivery]; + if ((player.WorldPosition - transform.position).sqrMagnitude > carLength * carLength) + { + NetworkLifecycle.Instance.Server.LogWarning($"Player \"{player.Username}\" attempted to gain authority for a control on car {CurrentID}, but they are too far away!"); + NetworkLifecycle.Instance.Server.SendTrainControlAuthorityUpdate(NetId, portNetId, ControlAuthorityState.Denied, player); + NetworkLifecycle.Instance.Server.SendTrainControlAuthorityUpdate(NetId, portNetId, ControlAuthorityState.Released, player); + return; + } + + // No authority exists (or cleanup failed) - grant authority and communicate to all players + NetworkLifecycle.Instance.Server.LogDebug(() => $"Player \"{player.Username}\" granted authority for a control on car {CurrentID}"); + portAuthority[portNetId] = player; + NetworkLifecycle.Instance.Server.SendTrainControlAuthorityUpdate(NetId, portNetId, ControlAuthorityState.Blocked, excludePlayer: player); + } + else if (currentAuth != player) + { + NetworkLifecycle.Instance.Server.LogWarning($"Player \"{player.Username}\" attempted to gain authority for a control that's in use on car {CurrentID}"); + NetworkLifecycle.Instance.Server.SendTrainControlAuthorityUpdate(NetId, portNetId, ControlAuthorityState.Denied, player); + } + } + else + { + // Release request + if (currentAuth == player) + { + NetworkLifecycle.Instance.Server.LogDebug(()=>$"Player \"{player.Username}\" released authority for a control on car {CurrentID}"); + portAuthority.Remove(portNetId); + NetworkLifecycle.Instance.Server.SendTrainControlAuthorityUpdate(NetId, portNetId, ControlAuthorityState.Released); + } + else if(currentAuth != null) + { + NetworkLifecycle.Instance.Server.LogWarning($"Player \"{player.Username}\" attempted to release authority for a control that's not theirs on car {CurrentID}"); + NetworkLifecycle.Instance.Server.SendTrainControlAuthorityUpdate(NetId, portNetId, ControlAuthorityState.Denied, player); + } + } + } + private void Server_OnPlayerDisconnect(ServerPlayer player) { //todo: resolve player disconnection during chain interaction @@ -760,6 +936,13 @@ private void Server_OnPlayerDisconnect(ServerPlayer player) rearInteracting = false; } } + + // Clean up blocked controls + foreach (var kvp in portAuthority.Where(kvp => kvp.Value == player)) + { + portAuthority.Remove(kvp.Key); + NetworkLifecycle.Instance.Server.SendTrainControlAuthorityUpdate(NetId, kvp.Key, ControlAuthorityState.Released); + } } #endregion @@ -893,6 +1076,13 @@ private void Common_OnBrakeCylinderReleased() private void Common_OnPortUpdated(Port port) { + + if (port.valueType != PortValueType.CONTROL && !NetworkLifecycle.Instance.IsHost()) + { + Multiplayer.LogDebug(() => $"Common_OnPortUpdated() Ignoring non-control port update for [{port.id}] on [{CurrentID}, {NetId}]"); + return; + } + if (UnloadWatcher.isUnloading || NetworkLifecycle.Instance.IsProcessingPacket) return; if (float.IsNaN(port.prevValue) && float.IsNaN(port.Value)) @@ -915,6 +1105,11 @@ private void Common_OnPortUpdated(Port port) { dirtyPorts.Add(netId); } + + if (port.valueType == PortValueType.CONTROL) + { + + } } } @@ -1562,5 +1757,68 @@ public void Client_CouplerStateChange(ChainCouplerInteraction.State state, Coupl } //Multiplayer.LogDebug(() => $"9 Client_CouplerStateChange({state}) trainCar: [{TrainCar?.ID}, {NetId}]"); } + + private void Client_ControlGrabbed(ControlImplBase control) + { + Multiplayer.LogDebug(() => $"Client_ControlGrabbed() Control {control.name}, car: {CurrentID}"); + if (!controlToPortNetId.TryGetValue(control, out var portNetId)) + { + Multiplayer.LogWarning($"Control \"{control.name}\" grabbed but netId not found on TrainCar \"{CurrentID}\", hash: {control.GetHashCode()}, instance: {control.GetInstanceID()}"); + return; + } + + if (portNetIdToBlockState.TryGetValue(portNetId, out var isBlocked) && isBlocked) + { + Multiplayer.LogDebug(() => $"Client_ControlGrabbed() Control [{control.name}, {portNetId}] is blocked on car {CurrentID}, ending interaction"); + control.ForceEndInteraction(); + } + else + { + Multiplayer.LogDebug(() => $"Client_ControlGrabbed() Control [{control.name}, {portNetId}] is not blocked on car {CurrentID}, requesting authority"); + NetworkLifecycle.Instance.Client?.SendTrainControlAuthorityRequest(NetId, portNetId, true); + } + } + + private void Client_ControlUngrabbed(ControlImplBase control) + { + Multiplayer.LogDebug(() => $"Client_ControlUngrabbed() Control {control.name}, car: {CurrentID}"); + if (!controlToPortNetId.TryGetValue(control, out var portNetId)) + { + Multiplayer.LogWarning($"Control \"{control.name}\" ungrabbed but netId not found on TrainCar \"{CurrentID}\""); + return; + } + + if (!portNetIdToBlockState.ContainsKey(portNetId)) + portNetIdToBlockState[portNetId] = false; + + if (portNetIdToBlockState.TryGetValue(portNetId, out var isBlocked) && !isBlocked) + { + Multiplayer.LogDebug(() => $"Client_ControlUngrabbed() Control [{control.name}, {portNetId}] not blocked, releasing authority for car {CurrentID}"); + NetworkLifecycle.Instance.Client?.SendTrainControlAuthorityRequest(NetId, portNetId, false); + } + } + + public void Client_ReceiveAuthorityUpdate(uint portNetId, ControlAuthorityState state) + { + bool shouldBlock = state == ControlAuthorityState.Blocked || state == ControlAuthorityState.Denied; + portNetIdToBlockState[portNetId] = shouldBlock; + + Multiplayer.LogDebug(() => $"Client_ReceiveAuthorityUpdate({portNetId}, {state}) for [{CurrentID}, {NetId}]"); + + if (!portNetIdToControl.TryGetValue(portNetId, out var control) || control == null) + return; + + if (shouldBlock) + { + control.ForceEndInteraction(); + control.BlockControl(true); + control.InteractionAllowed = false; + } + else + { + control.BlockControl(false); + control.InteractionAllowed = true; + } + } #endregion } diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 13ea9a88..acced76a 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -163,6 +163,7 @@ protected override void Subscribe() netPacketProcessor.SubscribeReusable(OnCommonPaintThemePacket); netPacketProcessor.SubscribeReusable(OnCommonSimFlowPacket); netPacketProcessor.SubscribeReusable(OnCommonTrainFusesPacket); + netPacketProcessor.SubscribeReusable(OnClientboundTrainControlAuthorityUpdatePacket); netPacketProcessor.SubscribeReusable(OnClientboundBrakeStateUpdatePacket); netPacketProcessor.SubscribeReusable(OnClientboundCargoStatePacket); @@ -801,6 +802,14 @@ private void OnCommonTrainFusesPacket(CommonTrainFusesPacket packet) networkedTrainCar.Common_UpdateFuses(packet); } + private void OnClientboundTrainControlAuthorityUpdatePacket(ClientboundTrainControlAuthorityUpdatePacket packet) + { + if (!NetworkedTrainCar.TryGet(packet.NetId, out NetworkedTrainCar networkedTrainCar)) + return; + + networkedTrainCar.Client_ReceiveAuthorityUpdate(packet.PortNetId,packet.State); + } + private void OnClientboundBrakeStateUpdatePacket(ClientboundBrakeStateUpdatePacket packet) { if (!NetworkedTrainCar.TryGet(packet.NetId, out NetworkedTrainCar networkedTrainCar)) @@ -1600,5 +1609,19 @@ public void SendCashRegisterAction(ushort netId, CashRegisterAction action, doub ); } + public void SendTrainControlAuthorityRequest(ushort netId, uint portNetId, bool requestAuthority) + { + SendPacketToServer + ( + new ServerboundTrainControlAuthorityPacket + { + NetId = netId, + PortNetId = portNetId, + RequestAuthority = requestAuthority + }, + DeliveryMethod.ReliableOrdered + ); + } + #endregion } diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index a61773bf..f8bcd329 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -167,6 +167,7 @@ protected override void Subscribe() netPacketProcessor.SubscribeReusable(OnCommonMuConnectedPacket); netPacketProcessor.SubscribeReusable(OnCommonMuDisconnectedPacket); netPacketProcessor.SubscribeReusable(OnCommonCockFiddlePacket); + netPacketProcessor.SubscribeReusable(OnServerboundTrainControlAuthorityPacket); netPacketProcessor.SubscribeReusable(OnCommonBrakeCylinderReleasePacket); netPacketProcessor.SubscribeReusable(OnCommonHandbrakePositionPacket); netPacketProcessor.SubscribeReusable(OnCommonPaintThemePacket); @@ -764,6 +765,24 @@ public void SendCockState(ushort netId, Coupler coupler, bool isOpen) ); } + public void SendTrainControlAuthorityUpdate(ushort netId, uint portNetId, ControlAuthorityState state, ServerPlayer sendToPlayer = null, ServerPlayer excludePlayer = null) + { + var packet = new ClientboundTrainControlAuthorityUpdatePacket + { + NetId = netId, + PortNetId = portNetId, + State = state + }; + + if (sendToPlayer == null) + if (excludePlayer == null) + SendPacketToAll(packet, DeliveryMethod.ReliableOrdered, excludeSelf: true); + else + SendPacketToAll(packet, DeliveryMethod.ReliableOrdered, excludePlayer.Peer, true); + else + SendPacket(sendToPlayer.Peer, packet, DeliveryMethod.ReliableOrdered); + } + public void SendJobsCreatePacket(NetworkedStationController networkedStation, NetworkedJob[] jobs, ITransportPeer peer = null) { Log($"Sending JobsCreatePacket for stationNetId {networkedStation.NetId} with {jobs.Count()} jobs"); @@ -1376,6 +1395,16 @@ private void OnCommonTrainPortsPacket(CommonTrainPortsPacket packet, ITransportP SendPacketToAll(packet, DeliveryMethod.ReliableOrdered, peer); } + private void OnServerboundTrainControlAuthorityPacket(ServerboundTrainControlAuthorityPacket packet, ITransportPeer peer) + { + if (!TryGetServerPlayer(peer, out ServerPlayer player)) + return; + if (!NetworkedTrainCar.TryGet(packet.NetId, out NetworkedTrainCar networkedTrainCar)) + return; + + networkedTrainCar.Server_ReceiveAuthorityRequest(packet.PortNetId, player, packet.RequestAuthority); + } + private void OnCommonTrainFusesPacket(CommonTrainFusesPacket packet, ITransportPeer peer) { SendPacketToAll(packet, DeliveryMethod.ReliableOrdered, peer); @@ -1654,8 +1683,7 @@ private void OnCommonCashRegisterWithModulesActionPacket(CommonCashRegisterWithM Log($"Cash Register With Modules Action received for {netCashRegister.GetObjectPath()}, Action: {packet.Action}, Amount: {packet.Amount}"); netCashRegister.Server_ProcessCashRegisterAction(player, packet); - - } + #endregion } diff --git a/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundTrainControlAuthorityUpdatePacket.cs b/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundTrainControlAuthorityUpdatePacket.cs new file mode 100644 index 00000000..ee14656f --- /dev/null +++ b/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundTrainControlAuthorityUpdatePacket.cs @@ -0,0 +1,18 @@ + +using DV.HUD; + +namespace Multiplayer.Networking.Packets.Clientbound.Train; + +public enum ControlAuthorityState : byte +{ + Released, + Blocked, + Denied +} + +public class ClientboundTrainControlAuthorityUpdatePacket +{ + public ushort NetId { get; set; } + public uint PortNetId { get; set; } + public ControlAuthorityState State { get; set; } +} diff --git a/Multiplayer/Networking/Packets/Serverbound/Train/ServerboundTrainControlAuthorityPacket.cs b/Multiplayer/Networking/Packets/Serverbound/Train/ServerboundTrainControlAuthorityPacket.cs new file mode 100644 index 00000000..316d982f --- /dev/null +++ b/Multiplayer/Networking/Packets/Serverbound/Train/ServerboundTrainControlAuthorityPacket.cs @@ -0,0 +1,12 @@ +using DV.HUD; + +namespace Multiplayer.Networking.Packets.Serverbound.Train; + +public class ServerboundTrainControlAuthorityPacket +{ + public ushort NetId { get; set; } + public uint PortNetId { get; set; } + public InteriorControlsManager.ControlType ControlType { get; set; } + public bool RequestAuthority { get; set; } + +} From 310c4c1bfe2b1d2d9f99325476091fa28a936882 Mon Sep 17 00:00:00 2001 From: Macka Date: Sat, 29 Nov 2025 12:49:01 +1000 Subject: [PATCH 491/521] Refactor PaintTheme registration and lookup API Changed PaintTheme registration and lookup to use PaintTheme objects instead of asset name strings throughout the API, lookup, and serialisation logic. Improved hash collision handling, modded theme discovery, and netId mapping. Updated related interfaces and serialisation/deserialisation code to use the new approach for consistency and reliability. --- Multiplayer/API/APIProvider.cs | 18 +- Multiplayer/API/NetIdProvider.cs | 3 +- .../Networking/Train/NetworkedTrainCar.cs | 9 +- .../Networking/Train/PaintThemeLookup.cs | 169 ++++++++++++------ .../Data/Train/TrainsetSpawnPart.cs | 11 +- .../Managers/Client/NetworkClient.cs | 10 +- .../Managers/Server/NetworkServer.cs | 33 +++- MultiplayerAPI/Interfaces/IMultiplayerAPI.cs | 12 +- 8 files changed, 173 insertions(+), 92 deletions(-) diff --git a/Multiplayer/API/APIProvider.cs b/Multiplayer/API/APIProvider.cs index 3c096689..f240d7ce 100644 --- a/Multiplayer/API/APIProvider.cs +++ b/Multiplayer/API/APIProvider.cs @@ -1,3 +1,4 @@ +using DV.Customization.Paint; using DV.Logic.Job; using MPAPI.Interfaces; using MPAPI.Types; @@ -36,7 +37,6 @@ public bool TryGetNetId(T obj, out ushort netId) where T : class return NetIdProvider.Instance.TryGetNetId(obj, out netId); } - public bool TryGetNetId(T obj, out uint netId) where T : class { return NetIdProvider.Instance.TryGetNetId(obj, out netId); @@ -57,11 +57,11 @@ public void SetModCompatibility(string modId, MultiplayerCompatibility compatibi ModCompatibilityManager.Instance.RegisterCompatibility(modId, compatibility); } - public uint RegisterPaintTheme(string assetName) + public uint RegisterPaintTheme(PaintTheme theme) { - if (string.IsNullOrEmpty(assetName)) + if (theme == null || string.IsNullOrEmpty(theme.AssetName)) { - Multiplayer.LogWarning("APIProvider.RegisterPaintTheme() called with empty assetName"); + Multiplayer.LogWarning("APIProvider.RegisterPaintTheme() called with null theme or empty AssetName"); return 0; } @@ -71,14 +71,14 @@ public uint RegisterPaintTheme(string assetName) return 0; } - return PaintThemeLookup.Instance.RegisterTheme(assetName); + return PaintThemeLookup.Instance.RegisterTheme(theme); } - public void UnregisterPaintTheme(uint themeId) + public void UnregisterPaintTheme(PaintTheme theme) { - if (themeId == 0) + if (theme == null || string.IsNullOrEmpty(theme.AssetName)) { - Multiplayer.LogWarning("APIProvider.UnregisterPaintTheme() called with themeId 0"); + Multiplayer.LogWarning("APIProvider.UnregisterPaintTheme() called with null theme or empty AssetName"); return; } @@ -87,6 +87,8 @@ public void UnregisterPaintTheme(uint themeId) Multiplayer.LogWarning("APIProvider.UnregisterPaintTheme() called when server or client is not running"); return; } + + PaintThemeLookup.Instance.UnregisterTheme(theme); } #region Task Serialisation diff --git a/Multiplayer/API/NetIdProvider.cs b/Multiplayer/API/NetIdProvider.cs index 17e48606..9e5e3043 100644 --- a/Multiplayer/API/NetIdProvider.cs +++ b/Multiplayer/API/NetIdProvider.cs @@ -1,4 +1,5 @@ using DV.CabControls; +using DV.Customization.Paint; using DV.Logic.Job; using DV.ThingTypes; using DV.Utils; @@ -28,8 +29,8 @@ protected override void Awake() RegisterHandler(NetworkedTrainCar.TryGetNetId, NetworkedTrainCar.TryGet); RegisterHandler(NetworkedTrainCar.TryGetNetId, NetworkedTrainCar.TryGet); - //RegisterUIntHandler(CargoTypeLookup.Instance.TryGetNetId, CargoTypeLookup.Instance.TryGet); RegisterHandler(CargoTypeLookup.Instance.TryGetNetId, CargoTypeLookup.Instance.TryGet); + RegisterHandler(PaintThemeLookup.Instance.TryGetNetId, PaintThemeLookup.Instance.TryGet); RegisterHandler(NetworkedJunction.TryGetNetId, NetworkedJunction.TryGet); RegisterHandler(NetworkedTurntable.TryGetNetId, NetworkedTurntable.TryGet); diff --git a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs index 97b5e82f..c2823e16 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs @@ -1120,7 +1120,11 @@ private void Common_OnPaintThemeChange(TrainCarPaint paintController) Multiplayer.LogDebug(() => $"Common_OnPaintThemeChange() target: {paintController.TargetArea}, theme: {paintController.CurrentTheme.name}"); - var themeId = PaintThemeLookup.Instance.GetThemeId(paintController.CurrentTheme); + if (!PaintThemeLookup.Instance.TryGetNetId(paintController.CurrentTheme, out var themeId)) + { + Multiplayer.LogWarning($"Common_OnPaintThemeChange() could not find themeId for theme {paintController.CurrentTheme.name} on [{CurrentID}, {NetId}]"); + return; + } Multiplayer.LogDebug(() => $"Common_OnPaintThemeChange() sending [{CurrentID},{NetId}], target: {paintController.TargetArea}, theme: [{paintController.CurrentTheme.name}, {themeId}]"); NetworkLifecycle.Instance?.Client.SendPaintThemeChangePacket(NetId, paintController.TargetArea, themeId); @@ -1530,12 +1534,11 @@ public void Common_ReceivePaintThemeUpdate(TrainCarPaint.Target target, PaintThe if (target == TrainCarPaint.Target.Interior) { - Multiplayer.LogWarning($"Received Paint Theme update for [{CurrentID}, {NetId}], targeting Interior"); + Multiplayer.LogDebug(() => $"Received Paint Theme update for [{CurrentID}, {NetId}], targeting Interior"); targetPaint = TrainCar.PaintInterior; } else if (target == TrainCarPaint.Target.Exterior) { - Multiplayer.LogWarning($"Received Paint Theme update for [{CurrentID}, {NetId}], targeting Exterior"); targetPaint = TrainCar.PaintExterior; } diff --git a/Multiplayer/Components/Networking/Train/PaintThemeLookup.cs b/Multiplayer/Components/Networking/Train/PaintThemeLookup.cs index 2294cf8a..40d12dd0 100644 --- a/Multiplayer/Components/Networking/Train/PaintThemeLookup.cs +++ b/Multiplayer/Components/Networking/Train/PaintThemeLookup.cs @@ -3,111 +3,172 @@ using JetBrains.Annotations; using Multiplayer.Utils; using System.Collections.Generic; -using System.Linq; using UnityEngine; - namespace Multiplayer.Components.Networking.Train; public class PaintThemeLookup : SingletonBehaviour { private readonly Dictionary hashToThemeName = []; + private readonly Dictionary themeNameToHash = []; private readonly Dictionary hashToBaseThemeName = []; + private readonly HashSet baseThemeNamesSet = []; + + private bool moddedThemesSearched = false; + + [UsedImplicitly] + public new static string AllowAutoCreate() + { + return $"[{nameof(PaintThemeLookup)}]"; + } protected override void Awake() { base.Awake(); - var themeNames = Resources.LoadAll("").Where(x => x is PaintTheme) - .Select(x => ((PaintTheme)x).AssetName) - .ToArray(); - foreach (var themeName in themeNames) + var themes = Resources.LoadAll(""); + + foreach (var theme in themes) { - var id = RegisterTheme(themeName); - if (id != 0) - hashToBaseThemeName[id] = themeName; + if (theme != null && !string.IsNullOrEmpty(theme.AssetName)) + { + var id = RegisterTheme(theme); + if (id != 0) + hashToBaseThemeName[id] = theme.AssetName; + } } + + baseThemeNamesSet.UnionWith(hashToBaseThemeName.Values); } - public PaintTheme GetPaintTheme(uint themeId) + #region Public Methods + public bool TryGetNetId(PaintTheme theme, out uint netId) { - PaintTheme theme = null; + netId = 0; - var themeName = GetThemeNameFromId(themeId); + if (theme == null) + return false; - if (themeName != null) - PaintTheme.TryLoad(themeName, out theme); + if (themeNameToHash.TryGetValue(theme.AssetName, out netId)) + return true; - return theme; - } + netId = GetThemeId(theme.AssetName); - public string GetThemeNameFromId(uint themeId) - { - hashToThemeName.TryGetValue(themeId, out string themeName); + if (hashToThemeName.ContainsKey(netId)) + return true; - return themeName; - } + // Skin Manager might not have been updated for Multiplayer yet, or another mod may have added themes + // Try to find themes added by mods + FindModdedThemes(); - public uint GetThemeId(PaintTheme theme) - { - if(theme == null) - return 0; + if (hashToThemeName.ContainsKey(netId)) + return true; - return GetThemeId(theme.AssetName); + netId = 0; + return false; } - public uint GetThemeId(string themeName) + public bool TryGet(uint themeId, out PaintTheme paintTheme) { - if (string.IsNullOrEmpty(themeName)) - return 0; + paintTheme = null; - return StringHashing.Fnv1aHash(themeName); + if (!hashToThemeName.TryGetValue(themeId, out string themeName)) + { + // Skin Manager might not have been updated for Multiplayer yet, or another mod may have added themes + // Try to find themes added by mods + FindModdedThemes(); + + hashToThemeName.TryGetValue(themeId, out themeName); + } + + if (themeName == null) + return false; + + return PaintTheme.TryLoad(themeName, out paintTheme); } - public uint RegisterTheme(string themeName) + public uint RegisterTheme(PaintTheme theme) { - if (string.IsNullOrEmpty(themeName)) + if (theme == null || string.IsNullOrEmpty(theme.AssetName)) return 0; - var hash = GetThemeId(themeName); + var hash = GetThemeId(theme.AssetName); + + if (hash == 0 || hash == uint.MaxValue) + return 0; - if (hashToThemeName.ContainsKey(hash)) + if (hashToThemeName.TryGetValue(hash, out var existingTheme)) { - Multiplayer.LogWarning($"Theme '{themeName}' is already registered with id: {hash}."); - return hash; + // Check for hash collision + if (existingTheme != theme.AssetName) + { + Multiplayer.LogWarning($"Hash collision detected! Theme '{theme.AssetName}' has same hash as '{existingTheme}': {hash}."); + return 0; + } + else + { + Multiplayer.LogWarning($"Theme '{theme.AssetName}' is already registered with Id: {hash}."); + return hash; + } } - hashToThemeName[hash] = themeName; - - Multiplayer.Log($"Theme '{themeName}' registered with id: {hash}."); + hashToThemeName[hash] = theme.AssetName; + themeNameToHash[theme.AssetName] = hash; + Multiplayer.Log($"PaintTheme '{theme.AssetName}' registered with netId: {hash}."); return hash; } - public void UnregisterTheme(string themeName) + public void UnregisterTheme(PaintTheme theme) { - var hash = GetThemeId(themeName); + if (theme == null || string.IsNullOrEmpty(theme.AssetName)) + return; - if(hashToBaseThemeName.ContainsKey(hash)) + var hash = GetThemeId(theme.AssetName); + + if (hashToBaseThemeName.ContainsKey(hash)) { - Multiplayer.LogWarning($"Tried to unregister a base-game theme '{themeName}'."); + Multiplayer.LogWarning($"Tried to unregister a base-game theme '{theme.AssetName}'."); return; } - if (hashToThemeName.TryGetValue(hash, out _)) - { - hashToThemeName.Remove(hash); - } + if (hashToThemeName.Remove(hash)) + themeNameToHash.Remove(theme.AssetName); else - { - Multiplayer.LogWarning($"Tried to unregister theme '{themeName}', but theme is not registered."); - } + Multiplayer.LogWarning($"Tried to unregister theme '{theme.AssetName}', but theme is not registered."); } - + #endregion - [UsedImplicitly] - public new static string AllowAutoCreate() + #region Helper Methods + + private uint GetThemeId(string themeName) { - return $"[{nameof(PaintThemeLookup)}]"; + if (string.IsNullOrEmpty(themeName)) + return 0; + + return StringHashing.Fnv1aHash(themeName); + } + + private void FindModdedThemes() + { + if (moddedThemesSearched) + return; + + // Find all themes excluding base themes and register non-base themes + var themes = Object.FindObjectsOfType(); + + foreach (var theme in themes) + { + if (theme != null && + !string.IsNullOrEmpty(theme.AssetName) && + !baseThemeNamesSet.Contains(theme.AssetName)) + { + RegisterTheme(theme); + } + } + + moddedThemesSearched = true; } + + #endregion } diff --git a/Multiplayer/Networking/Data/Train/TrainsetSpawnPart.cs b/Multiplayer/Networking/Data/Train/TrainsetSpawnPart.cs index 1f6ee6dd..e713bf04 100644 --- a/Multiplayer/Networking/Data/Train/TrainsetSpawnPart.cs +++ b/Multiplayer/Networking/Data/Train/TrainsetSpawnPart.cs @@ -102,8 +102,11 @@ public static void Serialize(NetDataWriter writer, TrainsetSpawnPart data) if(data.IsRestorationLoco) writer.Put((byte) data.RestorationState); - writer.Put(PaintThemeLookup.Instance.GetThemeId(data.PaintExterior)); - writer.Put(PaintThemeLookup.Instance.GetThemeId(data.PaintInterior)); + PaintThemeLookup.Instance.TryGetNetId(data.PaintExterior, out var extPaintNetId); + writer.Put(extPaintNetId); + + PaintThemeLookup.Instance.TryGetNetId(data.PaintInterior, out var intPaintNetId); + writer.Put(intPaintNetId); CouplingData.Serialize(writer, data.FrontCoupling); @@ -137,8 +140,8 @@ public static TrainsetSpawnPart Deserialize(NetDataReader reader) uint intThemeId = reader.GetUInt(); - PaintTheme exteriorPaint = PaintThemeLookup.Instance.GetPaintTheme(extThemeId); - PaintTheme interiorPaint = PaintThemeLookup.Instance.GetPaintTheme(intThemeId); + PaintThemeLookup.Instance.TryGet(extThemeId, out PaintTheme exteriorPaint); + PaintThemeLookup.Instance.TryGet(intThemeId, out PaintTheme interiorPaint); var frontCoupling = CouplingData.Deserialize(reader); var rearCoupling = CouplingData.Deserialize(reader); diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index acced76a..9562a6bb 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -1155,9 +1155,7 @@ private void OnCommonPaintThemePacket(CommonPaintThemePacket packet) if (!NetworkedTrainCar.TryGet(packet.NetId, out NetworkedTrainCar netTrainCar)) return; - PaintTheme paint = PaintThemeLookup.Instance.GetPaintTheme(packet.PaintThemeId); - - if (paint == null) + if (!PaintThemeLookup.Instance.TryGet(packet.PaintThemeId, out PaintTheme paint) || paint == null) { LogWarning($"Received paint theme change for {netTrainCar?.CurrentID}, but paint theme id '{packet.PaintThemeId}' does not exist."); return; @@ -1165,12 +1163,6 @@ private void OnCommonPaintThemePacket(CommonPaintThemePacket packet) Log($"Received paint theme change for {netTrainCar?.CurrentID}, theme '{paint.AssetName}'"); - //if (!Enum.IsDefined(typeof(TrainCarPaint.Target), packet.TargetArea)) - //{ - // LogWarning($"TrainCarPaint Target {packet.TargetArea} is not defined!"); - // return; - //} - LogDebug(() => $"OnCommonPaintThemePacket() [{netTrainCar?.CurrentID}, {packet.NetId}], area: {packet.TargetArea}, paint: [{paint?.AssetName}, {packet.PaintThemeId}]"); netTrainCar?.Common_ReceivePaintThemeUpdate(packet.TargetArea, paint); } diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index f8bcd329..2770ff8b 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -1,40 +1,41 @@ +using DV; +using DV.Customization.Paint; using DV.InventorySystem; using DV.Logic.Job; using DV.Scenarios.Common; using DV.ServicePenalty; using DV.ThingTypes; using DV.WeatherSystem; -using DV; using Humanizer; -using LiteNetLib.Utils; using LiteNetLib; +using LiteNetLib.Utils; using MPAPI.Interfaces.Packets; using MPAPI.Types; using Multiplayer.API; +using Multiplayer.Components.Networking; using Multiplayer.Components.Networking.Jobs; using Multiplayer.Components.Networking.Train; using Multiplayer.Components.Networking.World; -using Multiplayer.Components.Networking; -using Multiplayer.Networking.Data.Train; using Multiplayer.Networking.Data; +using Multiplayer.Networking.Data.Train; +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; -using Multiplayer.Networking.Packets.Clientbound; -using Multiplayer.Networking.Packets.Common.Train; using Multiplayer.Networking.Packets.Common; +using Multiplayer.Networking.Packets.Common.Train; +using Multiplayer.Networking.Packets.Serverbound; using Multiplayer.Networking.Packets.Serverbound.Jobs; using Multiplayer.Networking.Packets.Serverbound.Train; -using Multiplayer.Networking.Packets.Serverbound; using Multiplayer.Networking.Packets.Unconnected; using Multiplayer.Networking.TransportLayers; using Multiplayer.Utils; +using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Text; -using System; using UnityEngine; namespace Multiplayer.Networking.Managers.Server; @@ -1309,6 +1310,22 @@ private void OnCommonHandbrakePositionPacket(CommonHandbrakePositionPacket packe private void OnCommonPaintThemePacket(CommonPaintThemePacket packet, ITransportPeer peer) { + // TODO: Add validation to ensure player is allowed to change paint themes + + if (!NetworkedTrainCar.TryGet(packet.NetId, out NetworkedTrainCar netTrainCar)) + return; + + if (!PaintThemeLookup.Instance.TryGet(packet.PaintThemeId, out PaintTheme paint) || paint == null) + { + LogWarning($"Received paint theme change for {netTrainCar?.CurrentID}, but paint theme id '{packet.PaintThemeId}' does not exist."); + return; + } + + Log($"Received paint theme change for {netTrainCar?.CurrentID}, theme '{paint.AssetName}'"); + + LogDebug(() => $"OnCommonPaintThemePacket() [{netTrainCar?.CurrentID}, {packet.NetId}], area: {packet.TargetArea}, paint: [{paint?.AssetName}, {packet.PaintThemeId}]"); + netTrainCar?.Common_ReceivePaintThemeUpdate(packet.TargetArea, paint); + SendPacketToAll(packet, DeliveryMethod.ReliableOrdered, peer); } diff --git a/MultiplayerAPI/Interfaces/IMultiplayerAPI.cs b/MultiplayerAPI/Interfaces/IMultiplayerAPI.cs index 45c90bee..ce48a8fc 100644 --- a/MultiplayerAPI/Interfaces/IMultiplayerAPI.cs +++ b/MultiplayerAPI/Interfaces/IMultiplayerAPI.cs @@ -1,3 +1,4 @@ +using DV.Customization.Paint; using DV.Logic.Job; using MPAPI.Types; using System; @@ -105,18 +106,19 @@ public interface IMultiplayerAPI bool TryGetObjectFromNetId(uint netId, out T obj) where T : class; /// - /// Registers a PaintTheme and returns its ID. + /// Registers a PaintTheme and returns its netId. /// - /// The string representing the `PaintTheme.AssetName`. + /// The to be registered. /// Non-zero, unique Id if the theme was successfully registered, otherwise 0. /// PaintThemes must be registered each time the client or server starts, registration is not persistent across sessions. - uint RegisterPaintTheme(string assetName); + uint RegisterPaintTheme(PaintTheme theme); /// /// Unregisters a PaintTheme. /// - /// The Id of the PaintTheme to be unregistered. - void UnregisterPaintTheme(uint themeId); + /// The to be unregistered. + /// Base game PaintThemes cannot be unregistered. + void UnregisterPaintTheme(PaintTheme theme); /// /// Registers a serialiser/deserialiser for a custom type for multiplayer synchronisation. From 7f4f5c8154af14b5069d3f18219a639c755647be Mon Sep 17 00:00:00 2001 From: Macka Date: Sat, 29 Nov 2025 13:04:25 +1000 Subject: [PATCH 492/521] Improve cargo type registration and logging Enhanced TryGetNetId to detect and log hash collisions when registering cargo types. Improved logging messages for missing cargo types and successful registrations. Minor formatting adjustments for consistency. --- .../Networking/Train/CargoTypeLookup.cs | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/Multiplayer/Components/Networking/Train/CargoTypeLookup.cs b/Multiplayer/Components/Networking/Train/CargoTypeLookup.cs index 299193c1..7704e750 100644 --- a/Multiplayer/Components/Networking/Train/CargoTypeLookup.cs +++ b/Multiplayer/Components/Networking/Train/CargoTypeLookup.cs @@ -42,12 +42,13 @@ public bool TryGet(uint netId, out CargoType_v2 cargoType) if (hashToCargoTypeV2.TryGetValue(netId, out cargoType)) return true; - Multiplayer.LogWarning($"CargoTypeLookup: Could not find CargoType_v2 for netId {netId}"); RebuildCache(); if (hashToCargoTypeV2.TryGetValue(netId, out cargoType)) return true; + Multiplayer.LogWarning($"CargoTypeLookup: Could not find CargoType for netId {netId}"); + cargoType = CargoType.None.ToV2(); return false; } @@ -67,14 +68,14 @@ public bool TryGet(uint netId, out CargoType cargoType) public bool TryGetNetId(CargoType_v2 cargoType, out uint netId) { netId = 0; - if ( cargoType == null) + if (cargoType == null) return false; if (cargoTypeV2ToHash.TryGetValue(cargoType, out netId)) return true; uint hash = StringHashing.Fnv1aHash(cargoType.id); - Multiplayer.LogDebug(() => $"Registering cargo type '{cargoType.id}', netId: {hash}"); + Multiplayer.LogDebug(()=> $"Registering cargo type '{cargoType.id}', netId: {hash}"); if (hash == 0 || hash == uint.MaxValue) { @@ -83,10 +84,22 @@ public bool TryGetNetId(CargoType_v2 cargoType, out uint netId) return false; } + if (hashToCargoTypeV2.TryGetValue(hash, out var existingCargoType)) + { + if (existingCargoType.id != cargoType.id) + { + Multiplayer.LogError($"Hash collision detected! Cargo type '{cargoType.id}' has same hash as '{existingCargoType.id}': {hash}."); + netId = 0; + return false; + } + } + cargoTypeV2ToHash[cargoType] = hash; hashToCargoTypeV2[hash] = cargoType; - netId = hash; + + Multiplayer.Log($"CargoType '{cargoType.id}' registered with netId: {netId}"); + return true; } From eaab4cf825ff1512d6da9a8fec5b8ed02442a4f9 Mon Sep 17 00:00:00 2001 From: Macka Date: Sat, 29 Nov 2025 18:40:32 +1000 Subject: [PATCH 493/521] Fix VR check logic in item anchor offset capture Corrects the conditional to only capture item anchor offset when VR is not enabled, addressing inconsistencies related to camera direction and player loading status. --- .../Components/Networking/Player/NetworkedPlayer.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/Multiplayer/Components/Networking/Player/NetworkedPlayer.cs b/Multiplayer/Components/Networking/Player/NetworkedPlayer.cs index 7734b7ec..5be853fd 100644 --- a/Multiplayer/Components/Networking/Player/NetworkedPlayer.cs +++ b/Multiplayer/Components/Networking/Player/NetworkedPlayer.cs @@ -23,11 +23,8 @@ public static void CaptureItemAnchorOffset() //todo: there's some minor inconsistency with return values and may be related to: // - the direction/rotation of the camera // - player loading status (maybe posistion hasn't settled yet) - if (VRManager.IsVREnabled()) - { - } - else - { + if (!VRManager.IsVREnabled()) + { itemAnchorOffset = PlayerManager.PlayerTransform.InverseTransformPoint(ItemPositionController.Instance.itemAnchor.position); Multiplayer.LogDebug(() => $"NetworkedPlayer.CaptureItemAnchorOffset() itemAnchorOffset: {itemAnchorOffset}"); } From b33b76bb2df798bf0ae7ad59d4d330aee8b21cef Mon Sep 17 00:00:00 2001 From: Macka Date: Sat, 29 Nov 2025 18:43:53 +1000 Subject: [PATCH 494/521] Add paint theme change validation and syncing Introduces server-side validation for paint theme changes based on player proximity in NetworkedTrainCar. Refactors paint theme change packet sending for both client and server, and ensures paint theme updates are only processed if the player is close enough to the train car. Also updates related methods to use the new validation and syncing logic. --- .../Networking/Train/NetworkedTrainCar.cs | 102 ++++++++++++++---- .../Managers/Client/NetworkClient.cs | 6 +- .../Managers/Server/NetworkServer.cs | 25 ++++- 3 files changed, 107 insertions(+), 26 deletions(-) diff --git a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs index c2823e16..e03d9822 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs @@ -1,3 +1,4 @@ +using DV; using DV.CabControls; using DV.Customization.Paint; using DV.Damage; @@ -101,6 +102,8 @@ public static bool TryGetNetId(Car car, out ushort netId) private const int MAX_COUPLER_ITERATIONS = 10; private const float MAX_PORT_DELTA = 0.001f; private const uint MIN_KINEMATIC_CYCLES = 10; + private const float DISTANCE_TOLERANCE = 2f; + private const float MAX_PAINT_DISTANCE_SQ = (CommsRadioPaintjob.SIGNAL_RANGE + DISTANCE_TOLERANCE) * (CommsRadioPaintjob.SIGNAL_RANGE + DISTANCE_TOLERANCE); #region Port and Fuse Map @@ -148,6 +151,7 @@ static string GetFuse(uint netId) } #endregion + public float CarLengthSq => CarSpawner.Instance.carLiveryToCarLength[TrainCar.carLivery] * CarSpawner.Instance.carLiveryToCarLength[TrainCar.carLivery]; public string CurrentID { get; private set; } public TrainCar TrainCar; @@ -170,6 +174,7 @@ static string GetFuse(uint netId) private HashSet dirtyPorts; private Dictionary lastSentPortValues; private HashSet dirtyFuses; + private readonly HashSet dirtyPaints = []; private readonly Dictionary lastSentTrainDamages = []; private bool handbrakeDirty; @@ -383,7 +388,7 @@ public void Start() private void OnTrainCarInteriorLoaded(GameObject interior) { - Multiplayer.LogDebug(()=> $"OnTrainCarInteriorLoaded() {CurrentID}, interior is null: {interior == null}"); + Multiplayer.LogDebug(() => $"OnTrainCarInteriorLoaded() {CurrentID}, interior is null: {interior == null}"); StartCoroutine(WaitForInterior()); } @@ -395,7 +400,7 @@ private IEnumerator WaitForInterior() yield return new WaitUntil ( - ()=> + () => { return TrainCar.loadedInterior != null || Time.time - time > 2000f; } @@ -413,7 +418,7 @@ private IEnumerator WaitForInterior() yield return new WaitUntil ( - ()=> + () => { return TrainCar.loadedInterior.TryGetComponent(out interiorControlsManager) || Time.time - time > 2000f; } @@ -474,7 +479,7 @@ private void HookControls(InteriorControlsManager interiorControlsManager) private void OnTrainCarInteriorUnloaded(GameObject interior) { - Multiplayer.LogDebug(()=>$"OnTrainCarInteriorUnloaded() {CurrentID}"); + Multiplayer.LogDebug(() => $"OnTrainCarInteriorUnloaded() {CurrentID}"); foreach (var control in controlToPortNetId.Keys) { @@ -653,8 +658,7 @@ public bool Server_ValidateClientSimFlowPacket(ServerPlayer player, CommonTrainP // Some ports can be updated by the player even if they are not in the car, like doors and windows. // Only deny the request if the player is more than 5 meters away from any point of the car. - float carLength = CarSpawner.Instance.carLiveryToCarLength[TrainCar.carLivery]; - if ((player.WorldPosition - transform.position).sqrMagnitude <= carLength * carLength) + if ((player.WorldPosition - transform.position).sqrMagnitude <= CarLengthSq) return true; NetworkLifecycle.Instance.Server.LogWarning($"Player {player.Username} tried to send a sim flow packet for a car they are not in!"); @@ -883,8 +887,7 @@ public void Server_ReceiveAuthorityRequest(uint portNetId, ServerPlayer player, { if (currentAuth == null) { - float carLength = CarSpawner.Instance.carLiveryToCarLength[TrainCar.carLivery]; - if ((player.WorldPosition - transform.position).sqrMagnitude > carLength * carLength) + if ((player.WorldPosition - transform.position).sqrMagnitude > CarLengthSq) { NetworkLifecycle.Instance.Server.LogWarning($"Player \"{player.Username}\" attempted to gain authority for a control on car {CurrentID}, but they are too far away!"); NetworkLifecycle.Instance.Server.SendTrainControlAuthorityUpdate(NetId, portNetId, ControlAuthorityState.Denied, player); @@ -908,11 +911,11 @@ public void Server_ReceiveAuthorityRequest(uint portNetId, ServerPlayer player, // Release request if (currentAuth == player) { - NetworkLifecycle.Instance.Server.LogDebug(()=>$"Player \"{player.Username}\" released authority for a control on car {CurrentID}"); + NetworkLifecycle.Instance.Server.LogDebug(() => $"Player \"{player.Username}\" released authority for a control on car {CurrentID}"); portAuthority.Remove(portNetId); NetworkLifecycle.Instance.Server.SendTrainControlAuthorityUpdate(NetId, portNetId, ControlAuthorityState.Released); } - else if(currentAuth != null) + else if (currentAuth != null) { NetworkLifecycle.Instance.Server.LogWarning($"Player \"{player.Username}\" attempted to release authority for a control that's not theirs on car {CurrentID}"); NetworkLifecycle.Instance.Server.SendTrainControlAuthorityUpdate(NetId, portNetId, ControlAuthorityState.Denied, player); @@ -920,6 +923,43 @@ public void Server_ReceiveAuthorityRequest(uint portNetId, ServerPlayer player, } } + public void Server_ValidatePaintThemeChange(TrainCarPaint.Target target, PaintTheme theme, ServerPlayer player) + { + var playerPos = TrainCar.transform.InverseTransformPoint(player.WorldPosition); + Vector3 localClosestPoint = TrainCar.Bounds.ClosestPoint(playerPos); + Vector3 worldClosestPoint = TrainCar.transform.TransformPoint(localClosestPoint); + + if ((player.WorldPosition - worldClosestPoint).sqrMagnitude > MAX_PAINT_DISTANCE_SQ) + { + NetworkLifecycle.Instance.Server.LogWarning($"Player \"{player.Username}\" attempted to change the paint theme for car {CurrentID}, but they are too far away!"); + + var paintControllers = GetComponents(); + int i = 0; + while (i < paintControllers.Length) + { + if (paintControllers[i].TargetArea == target) + break; + + i++; + } + + if (i >= paintControllers.Length) + return; + + if (!PaintThemeLookup.Instance.TryGetNetId(paintControllers[i].CurrentTheme, out var themeNetId)) + { + Multiplayer.LogWarning($"ValidatePaintThemeChange could not find themeNetId for theme {paintControllers[i].CurrentTheme.AssetName} on [{CurrentID}, {NetId}]"); + return; + } + + NetworkLifecycle.Instance.Server.SendPaintThemeChange(this, target, themeNetId, player); + + return; + } + + Common_ReceivePaintThemeUpdate(target, theme); + } + private void Server_OnPlayerDisconnect(ServerPlayer player) { //todo: resolve player disconnection during chain interaction @@ -956,6 +996,7 @@ private void Common_OnTick(uint tick) Common_SendHandbrakePosition(); Common_SendFuses(); Common_SendPorts(); + Common_SendPaintThemes(); } private void Common_SendHandbrakePosition() @@ -1060,6 +1101,29 @@ private void Common_SendFuses() NetworkLifecycle.Instance.Client.SendFuses(NetId, fuseIds, fuseValues); } + private void Common_SendPaintThemes() + { + if (dirtyPaints.Count == 0) + return; + + //Multiplayer.LogDebug(() => $"Common_SendPaintThemes() target: {paintController.TargetArea}, theme: {paintController.CurrentTheme.name}"); + foreach (var paintController in dirtyPaints) + { + if (!PaintThemeLookup.Instance.TryGetNetId(paintController.CurrentTheme, out var themeNetId)) + { + Multiplayer.LogWarning($"Common_SendPaintThemes() could not find themeId for theme {paintController.CurrentTheme.name} on [{CurrentID}, {NetId}]"); + return; + } + + if (NetworkLifecycle.Instance.IsHost()) + NetworkLifecycle.Instance.Server.SendPaintThemeChange(this, paintController.targetArea, themeNetId); + else + NetworkLifecycle.Instance.Client.SendPaintThemeChange(this, paintController.targetArea, themeNetId); + } + + dirtyPaints.Clear(); + } + private void Common_OnHandbrakePositionChanged((float, bool) data) { if (NetworkLifecycle.Instance.IsProcessingPacket) @@ -1108,32 +1172,27 @@ private void Common_OnPortUpdated(Port port) if (port.valueType == PortValueType.CONTROL) { - + } } } private void Common_OnPaintThemeChange(TrainCarPaint paintController) { - if (paintController == null) + if (UnloadWatcher.isUnloading || NetworkLifecycle.Instance.IsProcessingPacket) return; - Multiplayer.LogDebug(() => $"Common_OnPaintThemeChange() target: {paintController.TargetArea}, theme: {paintController.CurrentTheme.name}"); - - if (!PaintThemeLookup.Instance.TryGetNetId(paintController.CurrentTheme, out var themeId)) - { - Multiplayer.LogWarning($"Common_OnPaintThemeChange() could not find themeId for theme {paintController.CurrentTheme.name} on [{CurrentID}, {NetId}]"); + if (paintController == null) return; - } - Multiplayer.LogDebug(() => $"Common_OnPaintThemeChange() sending [{CurrentID},{NetId}], target: {paintController.TargetArea}, theme: [{paintController.CurrentTheme.name}, {themeId}]"); - NetworkLifecycle.Instance?.Client.SendPaintThemeChangePacket(NetId, paintController.TargetArea, themeId); + dirtyPaints.Add(paintController); } private void Common_OnFuseUpdated(Fuse fuse) { if (UnloadWatcher.isUnloading || NetworkLifecycle.Instance.IsProcessingPacket) return; + var netId = GetFuseNetId(fuse.id); dirtyFuses.Add(netId); } @@ -1539,10 +1598,11 @@ public void Common_ReceivePaintThemeUpdate(TrainCarPaint.Target target, PaintThe } else if (target == TrainCarPaint.Target.Exterior) { + Multiplayer.LogDebug(() => $"Received Paint Theme update for [{CurrentID}, {NetId}], targeting Exterior"); targetPaint = TrainCar.PaintExterior; } - if (targetPaint == null || !targetPaint.IsSupported(paint)) + if (targetPaint == null /*|| !targetPaint.IsSupported(paint)*/ ) { Multiplayer.LogWarning($"Received Paint Theme update for [{CurrentID}, {NetId}], but {paint?.AssetName} is not supported"); return; diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 9562a6bb..776ea6ce 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -1583,9 +1583,11 @@ public void SendItemsChangePacket(List items) DeliveryMethod.ReliableOrdered); } - public void SendPaintThemeChangePacket(ushort netId, TrainCarPaint.Target targetArea, uint themeId) + public void SendPaintThemeChange(NetworkedTrainCar netTraincar, TrainCarPaint.Target targetArea, uint themeId) { - SendPacketToServer(new CommonPaintThemePacket { NetId = netId, TargetArea = targetArea, PaintThemeId = themeId }, DeliveryMethod.ReliableUnordered); + Log($"Sending paint theme change for {netTraincar.CurrentID}"); + + SendPacketToServer(new CommonPaintThemePacket { NetId = netTraincar.NetId, TargetArea = targetArea, PaintThemeId = themeId }, DeliveryMethod.ReliableUnordered); } public void SendCashRegisterAction(ushort netId, CashRegisterAction action, double amount = 0.0f) diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index 2770ff8b..e5c7987a 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -580,6 +580,23 @@ public void SendCargoState(NetworkedTrainCar netTraincar, bool isLoading, byte c }, DeliveryMethod.ReliableOrdered, SelfPeer); } + public void SendPaintThemeChange(NetworkedTrainCar netTraincar, TrainCarPaint.Target targetArea, uint themeNetId, ServerPlayer sendToPlayer = null) + { + var packet = new CommonPaintThemePacket + { + NetId = netTraincar.NetId, + TargetArea = targetArea, + PaintThemeId = themeNetId + }; + + Log($"Sending paint theme change for {netTraincar.CurrentID}"); + + if (sendToPlayer != null) + SendPacket(sendToPlayer.Peer, packet, DeliveryMethod.ReliableUnordered); + else + SendPacketToAll(packet, DeliveryMethod.ReliableUnordered, true); + } + public void SendWarehouseControllerUpdate(ushort netId, bool isLoading, ushort jobNetId, ushort carNetId, uint cargoTypeNetId, WarehouseMachineController.TextPreset preset) { LogDebug(() => $"SendWarehouseControllerUpdate({netId}, {isLoading}, {jobNetId}, {carNetId}, {cargoTypeNetId}, {preset})"); @@ -1310,7 +1327,8 @@ private void OnCommonHandbrakePositionPacket(CommonHandbrakePositionPacket packe private void OnCommonPaintThemePacket(CommonPaintThemePacket packet, ITransportPeer peer) { - // TODO: Add validation to ensure player is allowed to change paint themes + if (!TryGetServerPlayer(peer, out ServerPlayer player)) + return; if (!NetworkedTrainCar.TryGet(packet.NetId, out NetworkedTrainCar netTrainCar)) return; @@ -1324,9 +1342,10 @@ private void OnCommonPaintThemePacket(CommonPaintThemePacket packet, ITransportP Log($"Received paint theme change for {netTrainCar?.CurrentID}, theme '{paint.AssetName}'"); LogDebug(() => $"OnCommonPaintThemePacket() [{netTrainCar?.CurrentID}, {packet.NetId}], area: {packet.TargetArea}, paint: [{paint?.AssetName}, {packet.PaintThemeId}]"); - netTrainCar?.Common_ReceivePaintThemeUpdate(packet.TargetArea, paint); - SendPacketToAll(packet, DeliveryMethod.ReliableOrdered, peer); + netTrainCar?.Server_ValidatePaintThemeChange(packet.TargetArea, paint, player); + + //SendPacketToAll(packet, DeliveryMethod.ReliableOrdered, peer); } private void OnServerboundAddCoalPacket(ServerboundAddCoalPacket packet, ITransportPeer peer) From 1845650e1dfbc5869e5b2395103dd5156faea3c9 Mon Sep 17 00:00:00 2001 From: Macka Date: Sat, 29 Nov 2025 18:44:42 +1000 Subject: [PATCH 495/521] Refactor pit stop interaction packet type to enum Changed CommonPitStopInteractionPacket.InteractionType from byte to PitStopStationInteractionType enum for improved type safety. Updated related method signatures and usages in NetworkedPitStopStation, NetworkClient, and NetworkServer. Also replaced car length calculations with cached CarLengthSq property for efficiency. --- .../World/NetworkedPitStopStation.cs | 10 ++++---- .../Managers/Client/NetworkClient.cs | 13 +++++------ .../Managers/Server/NetworkServer.cs | 23 +++++++------------ .../Common/CommonPitStopInteractionPacket.cs | 4 +++- 4 files changed, 21 insertions(+), 29 deletions(-) diff --git a/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs b/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs index cc3c7e37..96163d5e 100644 --- a/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs +++ b/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs @@ -1,7 +1,5 @@ using DV.CabControls; -using DV.CabControls.NonVR; using DV.CashRegister; -using DV.Interaction; using DV.Optimizers; using DV.ThingTypes; using Multiplayer.Networking.Data; @@ -295,7 +293,7 @@ public void OnPlayerEnteredActivationRegion(ServerPlayer player) Multiplayer.LogDebug(() => $"NetworkedPitStopStation.OnPlayerEnteredActivationRegion() [{StationName}] faucetPos: {faucetPos}"); // Send current state - NetworkLifecycle.Instance.Server.SendPitStopBulkDataPacket(NetId, Station.pitstop.carList.Count, carIndex, faucetPos, stateData, plugData, player.Peer); + NetworkLifecycle.Instance.Server.SendPitStopBulkDataPacket(NetId, Station.pitstop.carList.Count, carIndex, faucetPos, stateData, plugData, player); } public void OnPlayerEnteredCullingRegion(ServerPlayer player) @@ -340,7 +338,7 @@ public void ProcessInteractionPacketAsHost(CommonPitStopInteractionPacket packet new CommonPitStopInteractionPacket { NetId = packet.NetId, - InteractionType = (byte)PitStopStationInteractionType.Reject + InteractionType = PitStopStationInteractionType.Reject } ); } @@ -388,8 +386,8 @@ private void SendResourceUpdate(LocoResourceModule module) CommonPitStopInteractionPacket packet = new() { - NetId = this.NetId, - InteractionType = (byte)PitStopStationInteractionType.ResourceUpdate, + NetId = NetId, + InteractionType = PitStopStationInteractionType.ResourceUpdate, ResourceType = (int)module.resourceType, Value = module.Data.unitsToBuy }; diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 776ea6ce..cc546cb9 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -193,7 +193,6 @@ protected override void Subscribe() netPacketProcessor.SubscribeReusable(OnCommonCashRegisterWithModulesActionPacket); - } // Allow mods to register their own packets @@ -807,7 +806,7 @@ private void OnClientboundTrainControlAuthorityUpdatePacket(ClientboundTrainCont if (!NetworkedTrainCar.TryGet(packet.NetId, out NetworkedTrainCar networkedTrainCar)) return; - networkedTrainCar.Client_ReceiveAuthorityUpdate(packet.PortNetId,packet.State); + networkedTrainCar.Client_ReceiveAuthorityUpdate(packet.PortNetId, packet.State); } private void OnClientboundBrakeStateUpdatePacket(ClientboundBrakeStateUpdatePacket packet) @@ -1099,7 +1098,7 @@ private void OnCommonPitStopPlugInteractionPacket(CommonPitStopPlugInteractionPa private void OnClientboundPitStopBulkUpdatePacket(ClientboundPitStopBulkUpdatePacket packet) { - LogDebug(() => $"OnClientboundPitStopBulkUpdatePacket() NetId: {packet.NetId}, CarCount: {packet.CarCount}, CarSelection: { packet.CarSelection}, FaucetNotch: {packet.FaucetNotch}, ResourceData Count: {packet.ResourceData.Length}, PlugData: {packet.PlugData.Length}"); + LogDebug(() => $"OnClientboundPitStopBulkUpdatePacket() NetId: {packet.NetId}, CarCount: {packet.CarCount}, CarSelection: {packet.CarSelection}, FaucetNotch: {packet.FaucetNotch}, ResourceData Count: {packet.ResourceData.Length}, PlugData: {packet.PlugData.Length}"); if (!NetworkedPitStopStation.Get(packet.NetId, out var netPitStop)) { @@ -1537,13 +1536,13 @@ public void SendChat(string message) public void SendPitStopInteractionPacket(ushort netId, PitStopStationInteractionType interaction, ResourceType? resource, float state) { - LogDebug(()=>$"SendPitStopInteractionPacket({netId}, [{interaction}], {resource}, {state})"); + LogDebug(() => $"SendPitStopInteractionPacket({netId}, [{interaction}], {resource}, {state})"); int res = resource == null ? 0 : (int)resource; SendPacketToServer(new CommonPitStopInteractionPacket { NetId = netId, - InteractionType = (byte)interaction, + InteractionType = interaction, ResourceType = res, Value = state }, DeliveryMethod.ReliableOrdered); @@ -1559,7 +1558,7 @@ public void SendPitStopPlugInteractionPacket sbyte socketIndex = -1 ) { - LogDebug(()=>$"SendPitStopPlugInteractionPacket({netId}, {interaction}, pos: {position}, rot: {rotation}, trainNetId: {trainCarNetId}, socketIndex: {socketIndex})"); + LogDebug(() => $"SendPitStopPlugInteractionPacket({netId}, {interaction}, pos: {position}, rot: {rotation}, trainNetId: {trainCarNetId}, socketIndex: {socketIndex})"); SendNetSerializablePacketToServer(new CommonPitStopPlugInteractionPacket { @@ -1575,7 +1574,7 @@ public void SendPitStopPlugInteractionPacket public void SendItemsChangePacket(List items) { - Log($"Sending SendItemsChangePacket with {items.Count()} items"); + Log($"Sending CommonItemChangePacket with {items.Count()} items"); //SendPacketToServer(new CommonItemChangePacket { Items = items }, // DeliveryMethod.ReliableUnordered); diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index e5c7987a..6dbeb068 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -847,9 +847,9 @@ public void SendItemsChangePacket(List items, ServerPlayer playe } } - public void SendPitStopBulkDataPacket(ushort netId, int carCount, int carIndex, int faucetNotch, LocoResourceModuleData[] stationData, PitStopPlugData[] plugData , ITransportPeer peer = null) + public void SendPitStopBulkDataPacket(ushort netId, int carCount, int carIndex, int faucetNotch, LocoResourceModuleData[] stationData, PitStopPlugData[] plugData , ServerPlayer player) { - LogDebug(() => $"SendPitStopBulkDataPacket({netId}, {carCount}, {carIndex}, {faucetNotch}, {stationData.Count()}, {plugData.Count()}, {peer?.Id})"); + LogDebug(() => $"SendPitStopBulkDataPacket({netId}, {carCount}, {carIndex}, {faucetNotch}, {stationData.Count()}, {plugData.Count()}, {player})"); var packet = new ClientboundPitStopBulkUpdatePacket { @@ -861,10 +861,8 @@ public void SendPitStopBulkDataPacket(ushort netId, int carCount, int carIndex, PlugData = plugData, }; - if (peer == null) - SendPacketToAll(packet, DeliveryMethod.ReliableOrdered); - else - SendPacket(peer, packet, DeliveryMethod.ReliableOrdered); + if (player.Peer != SelfPeer) + SendPacket(player.Peer, packet, DeliveryMethod.ReliableOrdered); } public void SendPitStopInteractionPacket(ServerPlayer player, CommonPitStopInteractionPacket packet) @@ -883,7 +881,7 @@ public void SendPitStopPlugInteractionPacket(ServerPlayer player, CommonPitStopP public void SendCashRegisterAction(CommonCashRegisterWithModulesActionPacket packet, ITransportPeer peer = null) { if (peer == null) - SendPacketToAll(packet, DeliveryMethod.ReliableOrdered, SelfPeer); + SendPacketToAll(packet, DeliveryMethod.ReliableOrdered, true); else SendPacket(peer, packet, DeliveryMethod.ReliableOrdered); } @@ -1362,10 +1360,8 @@ private void OnServerboundAddCoalPacket(ServerboundAddCoalPacket packet, ITransp if (!NetworkLifecycle.Instance.IsHost(player)) { - float carLength = CarSpawner.Instance.carLiveryToCarLength[networkedTrainCar.TrainCar.carLivery]; - //is player close enough to add coal? - if ((player.WorldPosition - networkedTrainCar.transform.position).sqrMagnitude <= carLength * carLength) + if ((player.WorldPosition - networkedTrainCar.transform.position).sqrMagnitude <= networkedTrainCar.CarLengthSq) networkedTrainCar.firebox?.fireboxCoalControlPort.ExternalValueUpdate(packet.CoalMassDelta); } } @@ -1384,10 +1380,8 @@ private void OnServerboundTenderCoalPacket(ServerboundTenderCoalPacket packet, I if (!NetworkLifecycle.Instance.IsHost(player)) { - float carLength = CarSpawner.Instance.carLiveryToCarLength[networkedTrainCar.TrainCar.carLivery]; - //is player close enough to add/remove coal? - if ((player.WorldPosition - networkedTrainCar.transform.position).sqrMagnitude <= carLength * carLength) + if ((player.WorldPosition - networkedTrainCar.transform.position).sqrMagnitude <= networkedTrainCar.CarLengthSq) networkedTrainCar.coalPile?.coalConsumePort.ExternalValueUpdate(packet.CoalMassDelta); } } @@ -1403,8 +1397,7 @@ private void OnServerboundFireboxIgnitePacket(ServerboundFireboxIgnitePacket pac if (!NetworkLifecycle.Instance.IsHost(player)) { //is player close enough to ignite firebox? - float carLength = CarSpawner.Instance.carLiveryToCarLength[networkedTrainCar.TrainCar.carLivery]; - if ((player.WorldPosition - networkedTrainCar.transform.position).sqrMagnitude <= carLength * carLength) + if ((player.WorldPosition - networkedTrainCar.transform.position).sqrMagnitude <= networkedTrainCar.CarLengthSq) networkedTrainCar.firebox?.Ignite(); } } diff --git a/Multiplayer/Networking/Packets/Common/CommonPitStopInteractionPacket.cs b/Multiplayer/Networking/Packets/Common/CommonPitStopInteractionPacket.cs index d3e7a416..a4d82831 100644 --- a/Multiplayer/Networking/Packets/Common/CommonPitStopInteractionPacket.cs +++ b/Multiplayer/Networking/Packets/Common/CommonPitStopInteractionPacket.cs @@ -1,10 +1,12 @@ +using Multiplayer.Networking.Data; + namespace Multiplayer.Networking.Packets.Common; public class CommonPitStopInteractionPacket { public ushort NetId { get; set; } - public byte InteractionType { get; set; } + public PitStopStationInteractionType InteractionType { get; set; } public int ResourceType { get; set; } public float Value { get; set; } } From 23d3c6d04637f79f4b068dea9bf26fa8e2ee25b1 Mon Sep 17 00:00:00 2001 From: Macka Date: Sat, 29 Nov 2025 18:47:40 +1000 Subject: [PATCH 496/521] Add mod compatibility and silence cargo type log Registered 'dv-improved-job-overview' for client compatibility in ModCompatibilityManager. Commented out cargo type registration log in CargoTypeLookup to reduce log verbosity. --- Multiplayer/API/ModCompatibilityManager.cs | 1 + Multiplayer/Components/Networking/Train/CargoTypeLookup.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Multiplayer/API/ModCompatibilityManager.cs b/Multiplayer/API/ModCompatibilityManager.cs index a13169aa..e2d56080 100644 --- a/Multiplayer/API/ModCompatibilityManager.cs +++ b/Multiplayer/API/ModCompatibilityManager.cs @@ -37,6 +37,7 @@ protected override void Awake() RegisterCompatibility("DVDiscordPresenceMod", MultiplayerCompatibility.Client); RegisterCompatibility("DVLangHelper", MultiplayerCompatibility.Client); RegisterCompatibility("LightingOverhaul", MultiplayerCompatibility.Client); + RegisterCompatibility("dv-improved-job-overview", MultiplayerCompatibility.Client); //Json entries will override hardcoded entries ReadModJsons(); diff --git a/Multiplayer/Components/Networking/Train/CargoTypeLookup.cs b/Multiplayer/Components/Networking/Train/CargoTypeLookup.cs index 7704e750..36bddd19 100644 --- a/Multiplayer/Components/Networking/Train/CargoTypeLookup.cs +++ b/Multiplayer/Components/Networking/Train/CargoTypeLookup.cs @@ -98,7 +98,7 @@ public bool TryGetNetId(CargoType_v2 cargoType, out uint netId) hashToCargoTypeV2[hash] = cargoType; netId = hash; - Multiplayer.Log($"CargoType '{cargoType.id}' registered with netId: {netId}"); + //Multiplayer.Log($"CargoType '{cargoType.id}' registered with netId: {netId}"); return true; } From 1b44a8e4956ee18c754c7ba9bd036f2a01df3ec4 Mon Sep 17 00:00:00 2001 From: Macka Date: Sat, 29 Nov 2025 20:20:50 +1000 Subject: [PATCH 497/521] Add compatibility for dv_f_spammer mod Registered the 'dv_f_spammer' mod for client-side compatibility in ModCompatibilityManager --- Multiplayer/API/ModCompatibilityManager.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Multiplayer/API/ModCompatibilityManager.cs b/Multiplayer/API/ModCompatibilityManager.cs index e2d56080..2216af8e 100644 --- a/Multiplayer/API/ModCompatibilityManager.cs +++ b/Multiplayer/API/ModCompatibilityManager.cs @@ -38,6 +38,7 @@ protected override void Awake() RegisterCompatibility("DVLangHelper", MultiplayerCompatibility.Client); RegisterCompatibility("LightingOverhaul", MultiplayerCompatibility.Client); RegisterCompatibility("dv-improved-job-overview", MultiplayerCompatibility.Client); + RegisterCompatibility("dv_f_spammer", MultiplayerCompatibility.Client); //Json entries will override hardcoded entries ReadModJsons(); From 6b5b4ea83c02a08fc12a60d2f0a9dc7f491bd8c6 Mon Sep 17 00:00:00 2001 From: Macka Date: Sat, 29 Nov 2025 19:16:34 +1000 Subject: [PATCH 498/521] Update to Multiplayer API v1.0.0.0 and mod version 0.1.13.0 Bumped API version to 1.0.0.0 and mod version to 0.1.13.0. Updated project files to reflect new versions, changed license to Apache-2.0 for the API, added VRTK reference, included README.md in MultiplayerAPI package, and updated release and info metadata. --- Multiplayer/API/APIProvider.cs | 2 +- Multiplayer/Multiplayer.csproj | 3 ++- MultiplayerAPI/MultiplayerAPI.csproj | 11 ++++++++--- MultiplayerAPI/ReadMe.md | 10 ++++++++++ info.json | 2 +- releases.json | 2 +- 6 files changed, 23 insertions(+), 7 deletions(-) create mode 100644 MultiplayerAPI/ReadMe.md diff --git a/Multiplayer/API/APIProvider.cs b/Multiplayer/API/APIProvider.cs index f240d7ce..da3a8a2c 100644 --- a/Multiplayer/API/APIProvider.cs +++ b/Multiplayer/API/APIProvider.cs @@ -12,7 +12,7 @@ namespace Multiplayer.API; public class APIProvider : IMultiplayerAPI { - internal const string BUILT_AGAINST_API_VERSION = "0.1.0.0"; + internal const string BUILT_AGAINST_API_VERSION = "1.0.0.0"; public string SupportedApiVersion => BUILT_AGAINST_API_VERSION; diff --git a/Multiplayer/Multiplayer.csproj b/Multiplayer/Multiplayer.csproj index e0433f4a..2fc35c7c 100644 --- a/Multiplayer/Multiplayer.csproj +++ b/Multiplayer/Multiplayer.csproj @@ -3,7 +3,7 @@ net48 latest Multiplayer - 0.1.12.9 + 0.1.13.0 @@ -102,6 +102,7 @@ + diff --git a/MultiplayerAPI/MultiplayerAPI.csproj b/MultiplayerAPI/MultiplayerAPI.csproj index ea4372c2..489bfb88 100644 --- a/MultiplayerAPI/MultiplayerAPI.csproj +++ b/MultiplayerAPI/MultiplayerAPI.csproj @@ -4,8 +4,8 @@ net48 latest MPAPI - 0.1.0.0 - 0.1.0.0 + 1.0.0.0 + 1.0.0.0 true @@ -17,9 +17,10 @@ https://github.com/AMacro/dv-multiplayer/wiki/API-Overview https://github.com/AMacro/dv-multiplayer git - MIT + Apache-2.0 Initial release of DV Multiplayer API false + README.md true @@ -70,6 +71,10 @@ + + + + diff --git a/MultiplayerAPI/ReadMe.md b/MultiplayerAPI/ReadMe.md new file mode 100644 index 00000000..4146a87f --- /dev/null +++ b/MultiplayerAPI/ReadMe.md @@ -0,0 +1,10 @@ +# Derail Valley Multiplayer Mod API +API for interfacing with DV Multiplayer mod. Provides events and interfaces for server/client interactions in Derail Valley multiplayer scenarios. + +This package is licenced under Apache 2.0, please see the [repository](https://github.com/AMacro/dv-multiplayer) for the full licence and source code. + +For full documentation and examples, please see the [wiki](https://github.com/AMacro/dv-multiplayer/wiki/API-Overview). + +All issues should be reported in the repository's [issue tracker](https://github.com/AMacro/dv-multiplayer/issues). + +General support can be found in the [Multiplayer mod.](https://discord.com/channels/332511223536943105/1234574186161377363) thread on the [Altfuture Discord](https://discord.gg/7QKaeuHkKC) server. diff --git a/info.json b/info.json index 3097b46c..8f6cc773 100644 --- a/info.json +++ b/info.json @@ -1,6 +1,6 @@ { "Id": "Multiplayer", - "Version": "0.1.12.9", + "Version": "0.1.13.0", "DisplayName": "Multiplayer", "Author": "Insprill, Macka, Morm", "EntryMethod": "Multiplayer.Multiplayer.Load", diff --git a/releases.json b/releases.json index f2b839ad..7496cc25 100644 --- a/releases.json +++ b/releases.json @@ -1,6 +1,6 @@ { "Releases": [ - {"Id": "Multiplayer", "Version": "0.1.12.2", "DownloadUrl": "https://github.com/AMacro/dv-multiplayer/releases/download/v0.1.12.2-Beta/Multiplayer.0.1.12.2.zip"} + {"Id": "Multiplayer", "Version": "0.1.13.0", "DownloadUrl": "https://github.com/AMacro/dv-multiplayer/releases/download/v0.1.13.0-Beta/Multiplayer.0.1.13.0.zip"} ] } \ No newline at end of file From 17033f35ba0ee93f4a4e86dec99074a0213c5d9a Mon Sep 17 00:00:00 2001 From: Macka Date: Sun, 30 Nov 2025 21:33:31 +1000 Subject: [PATCH 499/521] Update .gitattributes for line ending normalization Added rules to normalize line endings across the repository, enforcing CRLF for C#, .csproj, and .sln files, and setting text=auto for all files. This helps maintain consistent line endings in a Windows development environment. --- .gitattributes | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.gitattributes b/.gitattributes index 28142e2f..72bff4bf 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,9 @@ -*.cs text eol=crlf \ No newline at end of file +# Set default behavior to automatically normalize line endings +* text=auto + +# Force CRLF line endings for C# files +*.cs text eol=crlf + +# Force CRLF for other common Windows development files (optional) +*.csproj text eol=crlf +*.sln text eol=crlf From e6fa62f393fa672c4f973194e6d8edbb2d43575a Mon Sep 17 00:00:00 2001 From: Macka Date: Sun, 7 Dec 2025 10:18:01 +1000 Subject: [PATCH 500/521] Add sync for generic switches Introduces NetworkedGenericSwitch to synchronise GenericSwitch states across players. Also adds player reach validation for interactions with controls. --- .../World/NetworkedGenericSwitch.cs | 128 ++++++++++++++++++ .../Managers/Client/NetworkClient.cs | 36 ++++- .../Managers/Server/NetworkServer.cs | 33 +++++ .../Common/CommonGenericSwitchStatePacket.cs | 8 ++ .../Patches/World/GenericSwitchPatch.cs | 31 +++++ Multiplayer/Utils/DvExtensions.cs | 24 ++++ 6 files changed, 255 insertions(+), 5 deletions(-) create mode 100644 Multiplayer/Components/Networking/World/NetworkedGenericSwitch.cs create mode 100644 Multiplayer/Networking/Packets/Common/CommonGenericSwitchStatePacket.cs create mode 100644 Multiplayer/Patches/World/GenericSwitchPatch.cs diff --git a/Multiplayer/Components/Networking/World/NetworkedGenericSwitch.cs b/Multiplayer/Components/Networking/World/NetworkedGenericSwitch.cs new file mode 100644 index 00000000..49185f50 --- /dev/null +++ b/Multiplayer/Components/Networking/World/NetworkedGenericSwitch.cs @@ -0,0 +1,128 @@ +using DV.Interaction; +using Multiplayer.Networking.Data; +using Multiplayer.Utils; +using System.Collections.Generic; +using UnityEngine; + +namespace Multiplayer.Components.Networking.World; + +public class NetworkedGenericSwitch : MonoBehaviour +{ + #region lookup cache + private static readonly Dictionary netIdtoNetworked = []; + private static readonly Dictionary networkedToNetId = []; + private static readonly Dictionary genericSwitchToNetId = []; + + public static bool TryGet(uint netId, out NetworkedGenericSwitch netSwitch) + { + return netIdtoNetworked.TryGetValue(netId, out netSwitch); + } + + public static bool TryGetNetId(NetworkedGenericSwitch netSwitch, out uint netId) + { + return networkedToNetId.TryGetValue(netSwitch, out netId); + } + + public static bool TryGetNetId(GenericSwitch genericSwitch, out uint netId) + { + return genericSwitchToNetId.TryGetValue(genericSwitch, out netId); + } + + + #endregion + + public uint NetId { get; private set; } + public GenericSwitch Switch { get; private set; } + + + + protected void Awake() + { + + Switch = GetComponent(); + if (Switch == null) + { + Multiplayer.LogError($"NetworkedGenericSwitch.Awake() {nameof(GenericSwitch)} not found."); + return; + } + + GenerateNetId(); + + Switch.onTurnedOff.AddListener(OnSwitchValueChanged); + Switch.onTurnedOn.AddListener(OnSwitchValueChanged); + + Multiplayer.LogDebug(()=>$"NetworkedGenericSwitch.Awake() Persistence Key: \"{Switch.persistenceKey}\", netId: {NetId}"); + } + + protected void OnDestroy() + { + if (Switch != null) + { + Switch.onTurnedOff.RemoveListener(OnSwitchValueChanged); + Switch.onTurnedOn.RemoveListener(OnSwitchValueChanged); + } + + networkedToNetId.Remove(this); + netIdtoNetworked.Remove(NetId); + genericSwitchToNetId.Remove(Switch); + } + + #region server + public void Server_ReceiveSwitchState(bool isOn, ServerPlayer player) + { + if (!transform.PlayerCanReach(player)) + { + Multiplayer.LogWarning($"Player \"{player.Username}\" tried to change switch [\"{Switch.persistenceKey}\", {NetId}] state but is too far away."); + NetworkLifecycle.Instance.Server.SendGenericSwitchState(NetId, Switch.IsOn, player); + return; + } + + if (Switch.IsOn != isOn) + Switch.IsOn = isOn; + } + + #endregion + public void Client_ReceiveSwitchState(bool isOn) + { + if (Switch.IsOn != isOn) + Switch.IsOn = isOn; + } + #region client + + #endregion + + #region common + private void GenerateNetId() + { + var hash = StringHashing.Fnv1aHash(Switch.persistenceKey); + if(hash == 0 || hash == uint.MaxValue) + { + Multiplayer.LogError($"NetworkedGenericSwitch.GenerateNetId() generated invalid NetId for persistenceKey '{Switch.persistenceKey}'"); + return; + } + + NetId = hash; + + if (netIdtoNetworked.ContainsKey(hash)) + { + Multiplayer.LogError($"NetworkedGenericSwitch.GenerateNetId() generated duplicate NetId {hash} for persistenceKey '{Switch.persistenceKey}'"); + return; + } + + netIdtoNetworked[hash] = this; + networkedToNetId[this] = hash; + genericSwitchToNetId[Switch] = hash; + } + + private void OnSwitchValueChanged() + { + if (NetworkLifecycle.Instance.IsProcessingPacket) + return; + + if (NetworkLifecycle.Instance.IsHost()) + NetworkLifecycle.Instance.Server.SendGenericSwitchState(NetId, Switch.IsOn); + else + NetworkLifecycle.Instance.Client.SendGenericSwitchState(NetId, Switch.IsOn); + } + #endregion +} diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index cc546cb9..9a13b1ab 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -1,3 +1,4 @@ +using DV; using DV.Common; using DV.Customization.Paint; using DV.Damage; @@ -16,25 +17,25 @@ using MPAPI.Types; using Multiplayer.API; 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.Networking; using Multiplayer.Components.SaveGame; -using Multiplayer.Networking.Data.Train; using Multiplayer.Networking.Data; +using Multiplayer.Networking.Data.Train; +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; -using Multiplayer.Networking.Packets.Clientbound; -using Multiplayer.Networking.Packets.Common.Train; using Multiplayer.Networking.Packets.Common; +using Multiplayer.Networking.Packets.Common.Train; +using Multiplayer.Networking.Packets.Serverbound; using Multiplayer.Networking.Packets.Serverbound.Jobs; using Multiplayer.Networking.Packets.Serverbound.Train; -using Multiplayer.Networking.Packets.Serverbound; using Multiplayer.Networking.TransportLayers; using Multiplayer.Patches.SaveGame; using Multiplayer.Utils; @@ -193,6 +194,8 @@ protected override void Subscribe() netPacketProcessor.SubscribeReusable(OnCommonCashRegisterWithModulesActionPacket); + netPacketProcessor.SubscribeReusable(OnCommonGenericSwitchStatePacket); + } // Allow mods to register their own packets @@ -1179,6 +1182,17 @@ private void OnCommonCashRegisterWithModulesActionPacket(CommonCashRegisterWithM netCashRegister.Client_ProcessCashRegisterAction(packet.Action, packet.Amount); } + private void OnCommonGenericSwitchStatePacket(CommonGenericSwitchStatePacket packet) + { + if (!NetworkedGenericSwitch.TryGet(packet.NetId, out NetworkedGenericSwitch netSwitch)) + { + LogWarning($"Received Generic Switch State for switch {packet.NetId}, but switch does not exist!"); + return; + } + + netSwitch.Client_ReceiveSwitchState(packet.IsOn); + } + #endregion #region Senders @@ -1616,5 +1630,17 @@ public void SendTrainControlAuthorityRequest(ushort netId, uint portNetId, bool ); } + public void SendGenericSwitchState(uint netId, bool isOn) + { + SendPacketToServer + ( + new CommonGenericSwitchStatePacket + { + NetId = netId, + IsOn = isOn + }, + deliveryMethod: DeliveryMethod.ReliableOrdered + ); + } #endregion } diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index 6dbeb068..ff242320 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -187,6 +187,8 @@ protected override void Subscribe() netPacketProcessor.SubscribeNetSerializable(OnCommonPitStopPlugInteractionPacket); netPacketProcessor.SubscribeReusable(OnCommonCashRegisterWithModulesActionPacket); + + netPacketProcessor.SubscribeReusable(OnCommonGenericSwitchStatePacket); } //allow mods to register their own packets @@ -886,6 +888,20 @@ public void SendCashRegisterAction(CommonCashRegisterWithModulesActionPacket pac SendPacket(peer, packet, DeliveryMethod.ReliableOrdered); } + public void SendGenericSwitchState(uint netId, bool isOn, ServerPlayer player = null) + { + var packet = new CommonGenericSwitchStatePacket + { + NetId = netId, + IsOn = isOn + }; + + if (player != null) + SendPacket(player.Peer, packet, DeliveryMethod.ReliableOrdered); + else + SendPacketToAll(packet, deliveryMethod: DeliveryMethod.ReliableOrdered, true); + } + public void SendChat(string message, ServerPlayer exclude = null) { var packet = new CommonChatPacket @@ -1714,5 +1730,22 @@ private void OnCommonCashRegisterWithModulesActionPacket(CommonCashRegisterWithM netCashRegister.Server_ProcessCashRegisterAction(player, packet); } + private void OnCommonGenericSwitchStatePacket(CommonGenericSwitchStatePacket packet, ITransportPeer peer) + { + if (!TryGetServerPlayer(peer, out var player)) + { + LogWarning($"Received Generic Switch State, but player was not found"); + return; + } + + if (!NetworkedGenericSwitch.TryGet(packet.NetId, out NetworkedGenericSwitch netSwitch)) + { + LogWarning($"Received Generic Switch State from \"{player.Username}\" for switch {packet.NetId}, but switch does not exist!"); + return; + } + + netSwitch.Server_ReceiveSwitchState(packet.IsOn, player); + } + #endregion } diff --git a/Multiplayer/Networking/Packets/Common/CommonGenericSwitchStatePacket.cs b/Multiplayer/Networking/Packets/Common/CommonGenericSwitchStatePacket.cs new file mode 100644 index 00000000..6d0e1ca0 --- /dev/null +++ b/Multiplayer/Networking/Packets/Common/CommonGenericSwitchStatePacket.cs @@ -0,0 +1,8 @@ + +namespace Multiplayer.Networking.Packets.Common; + +public class CommonGenericSwitchStatePacket +{ + public uint NetId { get; set; } + public bool IsOn { get; set; } +} diff --git a/Multiplayer/Patches/World/GenericSwitchPatch.cs b/Multiplayer/Patches/World/GenericSwitchPatch.cs new file mode 100644 index 00000000..4369f48f --- /dev/null +++ b/Multiplayer/Patches/World/GenericSwitchPatch.cs @@ -0,0 +1,31 @@ +using DV.Interaction; +using HarmonyLib; +using Multiplayer.Components.Networking.World; +using System.Collections; +using UnityEngine; + +namespace Multiplayer.Patches.World; + +[HarmonyPatch(typeof(GenericSwitch))] +public class GenericSwitchPatch +{ + [HarmonyPatch(typeof(GenericSwitch), MethodType.Constructor)] + [HarmonyPostfix] + + public static void GenericSwitch_Constructor(GenericSwitch __instance) + { + Multiplayer.LogDebug(() => $"GenericSwitch.Constructor() persistenceKey: {__instance.persistenceKey}"); + CoroutineManager.Instance.StartCoroutine(WaitForGenericSwitch(__instance)); + } + + private static IEnumerator WaitForGenericSwitch(GenericSwitch genericSwitch) + { + + while (string.IsNullOrEmpty(genericSwitch.persistenceKey)) + yield return new WaitForEndOfFrame(); + + Multiplayer.LogDebug(() => $"WaitForGenericSwitch() persistenceKey: {genericSwitch.persistenceKey}"); + + genericSwitch.gameObject.AddComponent(); + } +} diff --git a/Multiplayer/Utils/DvExtensions.cs b/Multiplayer/Utils/DvExtensions.cs index 39549275..6dcacb16 100644 --- a/Multiplayer/Utils/DvExtensions.cs +++ b/Multiplayer/Utils/DvExtensions.cs @@ -1,3 +1,5 @@ +using DV.Interaction; +using DV.KeyboardInput; using DV.Localization; using DV.UI; using DV.UIFramework; @@ -134,10 +136,32 @@ public static float AnyPlayerSqrMag(this Vector3 anchor) return result; } + public static bool PlayerCanReach(this GameObject item, ServerPlayer player, float extraRange = 0f) + { + return PlayerCanReach (item.transform, player, extraRange); + } + + public static bool PlayerCanReach(this Transform item, ServerPlayer player, float extraRange = 0f) + { + float reachRange = AKeyboardInput.XZ_SQR_REACH_RANGE + GrabberRaycasterDV.FPS_INTERACTION_RANGE_SQR + (extraRange * extraRange); + + var delta = player.WorldPosition - item.transform.position; + + if (Mathf.Abs(delta.y) > AKeyboardInput.Y_REACH_RANGE) + return false; + + delta.y = 0f; + + float sqrMag = (delta).sqrMagnitude; + + return sqrMag <= reachRange; + } + public static Vector3 GetWorldAbsolutePosition(this GameObject go) { return go.transform.GetWorldAbsolutePosition(); } + public static Vector3 GetWorldAbsolutePosition(this Transform transform) { return transform.position - WorldMover.currentMove; From 791ff7ea74fccdc886104f36444af396a7449be0 Mon Sep 17 00:00:00 2001 From: Macka Date: Tue, 9 Dec 2025 17:53:14 +1000 Subject: [PATCH 501/521] Optimise TrainCar position updates Force awake logic only triggered for derailments or when the TrainCar is moving > `NetworkTrainsetWatcher.VELOCITY_THRESHOLD` Position updates for TrainCars are now only applied if the difference between current position and new position exceeds `POSITION_UPDATE_THRESHOLD`, reducing jitter when stationary. --- .../Networking/Train/NetworkedTrainCar.cs | 37 ++++++++++++------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs index e03d9822..55e8cf19 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs @@ -104,6 +104,7 @@ public static bool TryGetNetId(Car car, out ushort netId) private const uint MIN_KINEMATIC_CYCLES = 10; private const float DISTANCE_TOLERANCE = 2f; private const float MAX_PAINT_DISTANCE_SQ = (CommsRadioPaintjob.SIGNAL_RANGE + DISTANCE_TOLERANCE) * (CommsRadioPaintjob.SIGNAL_RANGE + DISTANCE_TOLERANCE); + private const float POSITION_UPDATE_THRESHOLD = 0.1f; // TrainCar must have a bigger delta to apply position update #region Port and Fuse Map @@ -1631,9 +1632,6 @@ public void Client_ReceiveTrainPhysicsUpdate(in TrainsetMovementPart movementPar if (!Client_Initialized) return; - if (TrainCar.isEligibleForSleep) - TrainCar.ForceOptimizationState(false); - if (tick <= lastTickProcessed) { Multiplayer.LogWarning($"Received physics update for car {CurrentID} at tick {tick}, but last tick processed was {lastTickProcessed}"); @@ -1647,6 +1645,9 @@ public void Client_ReceiveTrainPhysicsUpdate(in TrainsetMovementPart movementPar //Vector3 expectedPosition = movementPart.RigidbodySnapshot.Position + WorldMover.currentMove; //Multiplayer.LogDebug(() => $"Processing derailed physics for car {CurrentID} at tick {tick}, current position: {TrainCar.transform.position} expected position: {expectedPosition}"); + if (TrainCar.isEligibleForSleep) + TrainCar.ForceOptimizationState(false); + TrainCar.Derail(); movementPart.RigidbodySnapshot.Apply(TrainCar.rb); @@ -1656,25 +1657,35 @@ public void Client_ReceiveTrainPhysicsUpdate(in TrainsetMovementPart movementPar } else { + // Only force awake if there's movement + if (Mathf.Abs(movementPart.Speed) > NetworkTrainsetWatcher.VELOCITY_THRESHOLD && TrainCar.isEligibleForSleep) + TrainCar.ForceOptimizationState(false); + //move the car to the correct position first - maybe? if (movementPart.typeFlag.HasFlag(TrainsetMovementPart.MovementType.Position)) { Vector3 worldPos = movementPart.Position + WorldMover.currentMove; - if (TrainCar.rb != null) + // Only apply position update if change exceeds threshold (stops cariages from jittering while stationary) + float positionDelta = Vector3.Distance(TrainCar.transform.position, worldPos); + + if (positionDelta > POSITION_UPDATE_THRESHOLD) { - TrainCar.rb.MovePosition(worldPos); + if (TrainCar.rb != null) + { + TrainCar.rb.MovePosition(worldPos); - //TrainCar.rb.MoveRotation(movementPart.Rotation); // removed due to motion sickness issues - } + //TrainCar.rb.MoveRotation(movementPart.Rotation); // removed due to motion sickness issues + } - //clear the queues? - Client_trainSpeedQueue.Clear(); - Client_trainRigidbodyQueue.Clear(); - client_bogie1Queue.Clear(); - client_bogie2Queue.Clear(); + //clear the queues? + Client_trainSpeedQueue.Clear(); + Client_trainRigidbodyQueue.Clear(); + client_bogie1Queue.Clear(); + client_bogie2Queue.Clear(); - TrainCar.stress.ResetTrainStress(); + TrainCar.stress.ResetTrainStress(); + } } Client_trainSpeedQueue.ReceiveSnapshot(movementPart.Speed, tick); From e49c8e4082af72df6528f784ba968ea1d9802043 Mon Sep 17 00:00:00 2001 From: Macka Date: Fri, 19 Dec 2025 08:40:11 +1000 Subject: [PATCH 502/521] Fix issue with cut-out not syncing after repair --- Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs index 55e8cf19..78dec9bc 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs @@ -1173,7 +1173,7 @@ private void Common_OnPortUpdated(Port port) if (port.valueType == PortValueType.CONTROL) { - + dirtyPorts.Add(netId); } } } From 5a590e769a6a032443d91b93343e4978570b5307 Mon Sep 17 00:00:00 2001 From: Macka Date: Fri, 19 Dec 2025 08:41:16 +1000 Subject: [PATCH 503/521] Additional null checks --- .../Networking/Train/NetworkTrainsetWatcher.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs b/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs index bdf30195..43f5ad76 100644 --- a/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs +++ b/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs @@ -45,10 +45,10 @@ private void Server_OnTick(uint tick) if (UnloadWatcher.isUnloading || UnloadWatcher.isQuitting) return; - if (set != null) + if (set != null && set.cars != null) Server_TickSet(set, tick); else - Multiplayer.LogError($"Server_OnTick(): Trainset is null!"); + Multiplayer.LogWarning($"Server_OnTick(): Trainset or cars are null. Set Id: {set?.id}, Cars: {set?.cars?.Count}"); } } private void Server_TickSet(Trainset set, uint tick) @@ -60,6 +60,11 @@ private void Server_TickSet(Trainset set, uint tick) if (UnloadWatcher.isUnloading || UnloadWatcher.isQuitting) return; + if (set.firstCar == null || set.lastCar == null) + { + Multiplayer.LogWarning($"Trainset {set?.id} has null end cars! firstCar: {set?.firstCar != null}, lastCar: {set?.lastCar != null}"); + return; + } cachedSendPacket.FirstNetId = set.firstCar.GetNetId(); cachedSendPacket.LastNetId = set.lastCar.GetNetId(); From 378816625c768e6e1800619d6c6ad61f288d0559 Mon Sep 17 00:00:00 2001 From: Macka Date: Fri, 19 Dec 2025 08:44:37 +1000 Subject: [PATCH 504/521] Fix pointset traveller spam Patch out of 'Point Set Traveller not moving even though velocity is:' logging --- Multiplayer/Patches/Train/BogiePatch.cs | 50 +++++++++++++++++++++---- 1 file changed, 42 insertions(+), 8 deletions(-) diff --git a/Multiplayer/Patches/Train/BogiePatch.cs b/Multiplayer/Patches/Train/BogiePatch.cs index 0b407fa8..0989eb13 100644 --- a/Multiplayer/Patches/Train/BogiePatch.cs +++ b/Multiplayer/Patches/Train/BogiePatch.cs @@ -2,24 +2,58 @@ using Multiplayer.Components.Networking; using Multiplayer.Components.Networking.Train; using Multiplayer.Utils; +using System.Collections.Generic; +using System.Reflection; +using System.Reflection.Emit; +using UnityEngine; namespace Multiplayer.Patches.Train; -[HarmonyPatch(typeof(Bogie), nameof(Bogie.SetupPhysics))] -public static class Bogie_SetupPhysics_Patch +[HarmonyPatch(typeof(Bogie))] +public static class BogiePatch { - private static void Postfix(Bogie __instance) + + [HarmonyTranspiler] + [HarmonyPatch(nameof(Bogie.UpdatePointSetTraveller))] + private static IEnumerable UpdatePointSetTraveller(IEnumerable instructions) + { + var codes = new List(instructions); + + // Find the Debug.LogError call and remove it along with its argument preparation + for (int i = 0; i < codes.Count; i++) + { + // Look for the Debug.LogError call + if (codes[i].opcode == OpCodes.Call && + codes[i].operand is MethodInfo method && + method.DeclaringType == typeof(Debug) && + method.Name == nameof(Debug.LogError)) + { + // Remove the 5 instructions that prepare and call LogError: + // ldstr, ldarg.1, box, call String.Format, call Debug.LogError + if (i >= 4) + { + codes.RemoveRange(i - 4, 5); + break; + } + } + } + + return codes; + } + + [HarmonyPostfix] + [HarmonyPatch(nameof(Bogie.SetupPhysics))] + private static void SetupPhysics(Bogie __instance) { if (!NetworkLifecycle.Instance.IsHost()) __instance.gameObject.GetOrAddComponent(); } -} -[HarmonyPatch(typeof(Bogie), nameof(Bogie.SwitchJunctionIfNeeded))] -public static class Bogie_SwitchJunctionIfNeeded_Patch -{ - private static bool Prefix() + [HarmonyPrefix] + [HarmonyPatch(nameof(Bogie.SwitchJunctionIfNeeded))] + private static bool SwitchJunctionIfNeeded() { return NetworkLifecycle.Instance.IsHost(); } } + From 7fe0529e2d6353d13ffac270250f490ab5c170a9 Mon Sep 17 00:00:00 2001 From: Macka Date: Fri, 19 Dec 2025 08:43:38 +1000 Subject: [PATCH 505/521] Fix issues with cash register stealing cash Refactor adding cash to cash registers --- .../World/NetworkedCashRegisterWithModules.cs | 213 +++++++++++++----- .../Managers/Server/NetworkServer.cs | 7 +- ...mmonCashRegisterWithModulesActionPacket.cs | 1 + .../Patches/World/CashRegisterBasePatch.cs | 24 +- .../World/CashRegisterWithModulesPatch.cs | 4 + 5 files changed, 181 insertions(+), 68 deletions(-) diff --git a/Multiplayer/Components/Networking/World/NetworkedCashRegisterWithModules.cs b/Multiplayer/Components/Networking/World/NetworkedCashRegisterWithModules.cs index 4cc62b00..e127b438 100644 --- a/Multiplayer/Components/Networking/World/NetworkedCashRegisterWithModules.cs +++ b/Multiplayer/Components/Networking/World/NetworkedCashRegisterWithModules.cs @@ -3,6 +3,7 @@ using DV.InventorySystem; using DV.Shops; using Multiplayer.Networking.Data; +using Multiplayer.Networking.Managers.Server; using Multiplayer.Networking.Packets.Common; using Multiplayer.Utils; using System; @@ -69,16 +70,21 @@ public static void InitialiseCashRegisters() #region Server Variables bool processingAction = false; + CullingManager _cullingManager; #endregion #region Client Variables + public bool IsBusy => isBuying || isCancelling || isAddingCash || processingAction; bool isBuying; bool isCancelling; + bool isAddingCash; public bool IsShopRegister { get; set; } = false; + + double pendingCashToAdd = 0; #endregion #region Common Variables - CashRegisterWithModules CashRegister; + CashRegisterWithModules CashRegister; #endregion #region Unity @@ -94,80 +100,128 @@ protected override void Awake() protected override void OnDestroy() { cashRegisterToNetworkedCashRegister.Remove(CashRegister); + + if (_cullingManager != null) + _cullingManager.PlayerEnteredActivationRegion -= CullingManager_PlayerEnteredActivationRegion; + base.OnDestroy(); } #endregion #region Server - public void Server_ProcessCashRegisterAction(ServerPlayer player, CommonCashRegisterWithModulesActionPacket packet) + + public void Server_InitCashRegister(CullingManager cullingManager) { - float sqrDistance = (player.WorldPosition - transform.position).sqrMagnitude; - bool success = false; - CashRegisterAction response = CashRegisterAction.RejectGeneric; + if (!NetworkLifecycle.Instance.IsHost() || cullingManager == null) + return; - NetworkLifecycle.Instance.Server?.LogDebug(() => $"NetworkedCashRegisterWithModules.Server_ProcessAction({player.Username}, {packet.Action}, {packet.Amount})"); + Multiplayer.LogDebug(() => $"NetworkedCashRegisterWithModules.Server_InitCashRegister({CashRegister.GetObjectPath()})"); + _cullingManager = cullingManager; + + if (_cullingManager != null) + _cullingManager.PlayerEnteredActivationRegion += CullingManager_PlayerEnteredActivationRegion; + } - if (sqrDistance > GrabberRaycasterDV.FPS_INTERACTION_RANGE_SQR * 2) //need to find the real distance, likely related to player capsual size + private void CullingManager_PlayerEnteredActivationRegion(ServerPlayer serverPlayer) + { + Multiplayer.LogDebug(() => $"NetworkedCashRegisterWithModules.CullingManager_PlayerEnteredActivationRegion({serverPlayer.Username}) deposited cash: {CashRegister.DepositedCash}"); + if (CashRegister.DepositedCash > 0f) { - NetworkLifecycle.Instance.Server?.LogDebug(() => $"NetworkedCashRegisterWithModules.Server_ProcessAction({player.Username}, {packet.Action}, {packet.Amount}) {CashRegister.GetObjectPath()}. Player too far! Player pos: {player.WorldPosition}, register pos: {transform.position}, sqrMag: {sqrDistance}"); - return; + NetworkLifecycle.Instance.Server.SendCashRegisterAction + ( + new CommonCashRegisterWithModulesActionPacket + { + NetId = NetId, + Action = CashRegisterAction.SetFunds, + Amount = CashRegister.DepositedCash + }, + [serverPlayer] + ); } + } + + public void Server_ProcessCashRegisterAction(ServerPlayer player, CommonCashRegisterWithModulesActionPacket packet) + { + bool success = false; + CashRegisterAction response = CashRegisterAction.RejectGeneric; - processingAction = true; - switch (packet.Action) + NetworkLifecycle.Instance.Server?.LogDebug(() => $"NetworkedCashRegisterWithModules.Server_ProcessAction({player.Username}, {packet.Action}, {packet.Amount})"); + if (transform.PlayerCanReach(player, 1)) { - case CashRegisterAction.Cancel: + processingAction = true; + switch (packet.Action) + { + case CashRegisterAction.Cancel: CashRegister?.Cancel(); success = true; - break; + break; - case CashRegisterAction.Buy: + case CashRegisterAction.Buy: - Multiplayer.LogDebug(() => $"NetworkedCashRegisterWithModules.Server_ProcessAction({packet.Action}) Player Money: {Inventory.Instance.PlayerMoney}, TotalCost: {CashRegister.GetTotalCost()}, TotalUnitsInBasket: {CashRegister.TotalUnitsInBasket()}"); + Multiplayer.LogDebug(() => $"NetworkedCashRegisterWithModules.Server_ProcessAction({packet.Action}) Player Money: {Inventory.Instance.PlayerMoney}, TotalCost: {CashRegister.GetTotalCost()}, TotalUnitsInBasket: {CashRegister.TotalUnitsInBasket()}"); - if (CashRegister.TotalUnitsInBasket() <= 0) - { - response = CashRegisterAction.RejectedNoItems; - } - else if (Inventory.Instance.PlayerMoney <= CashRegister.GetTotalCost()) - { - response = CashRegisterAction.RejectFunds; - } - else - { - success = CashRegister?.Buy() ?? false; - } + if (CashRegister.TotalUnitsInBasket() <= 0) + { + response = CashRegisterAction.RejectedNoItems; + } + else if (Inventory.Instance.PlayerMoney <= CashRegister.GetTotalCost()) + { + response = CashRegisterAction.RejectFunds; + } + else + { + success = CashRegister?.Buy() ?? false; + } - Multiplayer.LogDebug(() => $"NetworkedCashRegisterWithModules.Server_ProcessAction({packet.Action}, {packet.Amount}) Response: {response}, Buy success: {success}, Player Money: {Inventory.Instance.PlayerMoney}, TotalCost: {CashRegister.GetTotalCost()}, TotalUnitsInBasket: {CashRegister.TotalUnitsInBasket()}"); + Multiplayer.LogDebug(() => $"NetworkedCashRegisterWithModules.Server_ProcessAction({packet.Action}, {packet.Amount}) Response: {response}, Buy success: {success}, Player Money: {Inventory.Instance.PlayerMoney}, TotalCost: {CashRegister.GetTotalCost()}, TotalUnitsInBasket: {CashRegister.TotalUnitsInBasket()}"); - break; - - case CashRegisterAction.SetFunds: - double spend = 0; + break; - NetworkLifecycle.Instance.Server?.LogDebug(() => $"NetworkedCashRegisterWithModules.Server_ProcessAction({player.Username}, {packet.Action}, {packet.Amount}) Wallet: {Inventory.Instance.PlayerMoney}"); - if (packet.Amount > 0) - { - if (Inventory.Instance.PlayerMoney >= packet.Amount) - spend = packet.Amount; + case CashRegisterAction.AddCash: + + double remainingCost = CashRegister.GetRemainingCost(); + + if (remainingCost <= 0) + { + Multiplayer.LogDebug(() => $"NetworkedCashRegisterWithModules.Server_ProcessAction({packet.Action}) No remaining cost to add cash for."); + processingAction = false; + // No action needed, no response required + return; + } + else if (CashRegister.TotalUnitsInBasket() <= 0) + { + Multiplayer.LogDebug(() => $"NetworkedCashRegisterWithModules.Server_ProcessAction({packet.Action}) No items in basket to add cash for."); + response = CashRegisterAction.RejectedNoItems; + success = false; + } else - spend = Inventory.Instance.PlayerMoney; + { + double amountToAdd = Math.Min(remainingCost, Inventory.Instance.PlayerMoney); - success = Inventory.Instance.RemoveMoney(spend); + Inventory.Instance.RemoveMoney(amountToAdd); + CashRegister.SetCash(CashRegister.DepositedCash + amountToAdd); - if(success && player.PlayerId != NetworkLifecycle.Instance.Server.SelfId) - CashRegister?.AddCash(spend); - } - else - { - NetworkLifecycle.Instance.Server?.LogDebug(() => $"NetworkedCashRegisterWithModules.Server_ProcessAction({player.Username}, {packet.Action}, {packet.Amount}) amount negative!"); - } - break; + NetworkLifecycle.Instance.Server?.LogDebug(() => $"NetworkedCashRegisterWithModules.Server_ProcessAction({packet.Action}) Added cash: {amountToAdd}, New DepositedCash: {CashRegister.DepositedCash}, Player Money: {Inventory.Instance.PlayerMoney}"); + packet.Action = CashRegisterAction.SetFunds; + packet.Amount = CashRegister.DepositedCash; + success = true; + } + + break; + + case CashRegisterAction.SetFunds: + //NetworkLifecycle.Instance.Server?.LogDebug(() => $"NetworkedCashRegisterWithModules.Server_ProcessAction({player.Username}, {packet.Action}, {packet.Amount}) Wallet: {Inventory.Instance.PlayerMoney}"); + break; + } + } + else + { + NetworkLifecycle.Instance.Server?.LogDebug(() => $"Player \"{player.Username}\" tried to interact with Cash Register , but they are too far away"); } if (success) - NetworkLifecycle.Instance.Server.SendCashRegisterAction(packet); + NetworkLifecycle.Instance.Server.SendCashRegisterAction(packet, _cullingManager.ActivePlayers.ToArray()); else NetworkLifecycle.Instance.Server.SendCashRegisterAction ( @@ -177,7 +231,7 @@ public void Server_ProcessCashRegisterAction(ServerPlayer player, CommonCashRegi Action = response, Amount = CashRegister.DepositedCash }, - player.Peer + [player] ); processingAction = false; @@ -220,7 +274,7 @@ public void Client_ProcessCashRegisterAction(CashRegisterAction action, double a CashRegister?.buyAudio?.Play(CashRegister.transform.position, 1f, 1f, 0f, 1f, 500f, default, null, CashRegister.transform, false, 0f, null); - foreach(var module in CashRegister.registerModules) + foreach (var module in CashRegister.registerModules) module.ResetData(); CashRegister?.OnUnitsToBuyChanged(); @@ -232,7 +286,11 @@ public void Client_ProcessCashRegisterAction(CashRegisterAction action, double a break; + case CashRegisterAction.AddCash: + break; + case CashRegisterAction.SetFunds: + Multiplayer.LogDebug(() => $"NetworkedCashRegisterWithModules.Client_ProcessCashRegisterAction({action}, {amount}) Setting deposited cash."); CashRegister?.SetCash(amount); break; @@ -240,13 +298,29 @@ public void Client_ProcessCashRegisterAction(CashRegisterAction action, double a case CashRegisterAction.RejectGeneric: isBuying = false; isCancelling = false; - + + //if (isAddingCash) + //{ + //Inventory.Instance.AddMoney(pendingCashToAdd); + pendingCashToAdd = 0; + //} + + isAddingCash = false; + break; case CashRegisterAction.RejectFunds: isBuying = false; isCancelling = false; + //if (isAddingCash) + //{ + //Inventory.Instance.AddMoney(pendingCashToAdd); + pendingCashToAdd = 0; + //} + + isAddingCash = false; + CashRegister?.notEnoughMoneyAudio?.Play(CashRegister.transform.position, 1f, 1f, 0f, 1f, 500f, default, null, CashRegister.transform, false, 0f, null); break; @@ -255,6 +329,14 @@ public void Client_ProcessCashRegisterAction(CashRegisterAction action, double a isBuying = false; isCancelling = false; + //if (isAddingCash) + //{ + //Inventory.Instance.AddMoney(pendingCashToAdd); + pendingCashToAdd = 0; + //} + + isAddingCash = false; + foreach (var module in CashRegister.registerModules) module.ResetData(); @@ -291,12 +373,13 @@ public IEnumerator Buy() public IEnumerator Cancel() { - if (isBuying || isCancelling || NetworkLifecycle.Instance.IsProcessingPacket) + if (isBuying || isCancelling || isAddingCash || NetworkLifecycle.Instance.IsProcessingPacket) yield break; DisableInteraction(); NetworkLifecycle.Instance.Client.SendCashRegisterAction(NetId, CashRegisterAction.Cancel); + isCancelling = true; float timeOut = Time.time + NetworkLifecycle.Instance.Client.RPC_Timeout; @@ -307,12 +390,30 @@ public IEnumerator Cancel() EnableInteraction(); } - public void SetCash(double amount) + public IEnumerator AddCash(double amount) { - if (isBuying || isCancelling || processingAction || NetworkLifecycle.Instance.IsProcessingPacket) - return; + if (isBuying || isCancelling || isAddingCash || processingAction || NetworkLifecycle.Instance.IsProcessingPacket) + yield break; - NetworkLifecycle.Instance.Client.SendCashRegisterAction(NetId, CashRegisterAction.SetFunds, amount); + DisableInteraction(); + + Multiplayer.LogDebug(() => $"NetworkedCashRegisterWithModules.AddCash({amount}) Sending AddCash action."); + NetworkLifecycle.Instance.Client.SendCashRegisterAction(NetId, CashRegisterAction.AddCash); + + isAddingCash = true; + pendingCashToAdd = amount; + + float timeOut = Time.time + NetworkLifecycle.Instance.Client.RPC_Timeout; + + Multiplayer.LogDebug(() => $"NetworkedCashRegisterWithModules.AddCash({amount}) Waiting"); + yield return new WaitUntil(() => Time.time >= timeOut || isAddingCash == false); + + Multiplayer.LogDebug(() => $"NetworkedCashRegisterWithModules.AddCash({amount}) Wait complete, time-out: {Time.time >= timeOut}, isAddingCash: {isAddingCash}"); + + pendingCashToAdd = 0; + isAddingCash = false; + + EnableInteraction(); } private void DisableInteraction() diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index ff242320..4898dea5 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -880,12 +880,13 @@ public void SendPitStopPlugInteractionPacket(ServerPlayer player, CommonPitStopP SendNetSerializablePacket(player.Peer, packet, DeliveryMethod.ReliableOrdered); } - public void SendCashRegisterAction(CommonCashRegisterWithModulesActionPacket packet, ITransportPeer peer = null) + public void SendCashRegisterAction(CommonCashRegisterWithModulesActionPacket packet, ServerPlayer[] players = null) { - if (peer == null) + if (players == null) SendPacketToAll(packet, DeliveryMethod.ReliableOrdered, true); else - SendPacket(peer, packet, DeliveryMethod.ReliableOrdered); + foreach (var player in players) + SendPacket(player.Peer, packet, DeliveryMethod.ReliableOrdered); } public void SendGenericSwitchState(uint netId, bool isOn, ServerPlayer player = null) diff --git a/Multiplayer/Networking/Packets/Common/CommonCashRegisterWithModulesActionPacket.cs b/Multiplayer/Networking/Packets/Common/CommonCashRegisterWithModulesActionPacket.cs index 6452d73e..780a0811 100644 --- a/Multiplayer/Networking/Packets/Common/CommonCashRegisterWithModulesActionPacket.cs +++ b/Multiplayer/Networking/Packets/Common/CommonCashRegisterWithModulesActionPacket.cs @@ -5,6 +5,7 @@ public enum CashRegisterAction : byte { Cancel, Buy, + AddCash, SetFunds, RejectGeneric, RejectFunds, diff --git a/Multiplayer/Patches/World/CashRegisterBasePatch.cs b/Multiplayer/Patches/World/CashRegisterBasePatch.cs index 8463ea62..8ffead33 100644 --- a/Multiplayer/Patches/World/CashRegisterBasePatch.cs +++ b/Multiplayer/Patches/World/CashRegisterBasePatch.cs @@ -1,8 +1,10 @@ using DV.CashRegister; +using DV.InventorySystem; using HarmonyLib; using Multiplayer.Components.Networking; using Multiplayer.Components.Networking.World; using Multiplayer.Utils; +using System; using System.Collections.Generic; using System.Linq; using System.Reflection; @@ -14,25 +16,29 @@ namespace Multiplayer.Patches.World; [HarmonyPatch(typeof(CashRegisterBase))] public class CashRegisterBasePatch { - [HarmonyPostfix] - [HarmonyPatch(nameof(CashRegisterBase.SetCash))] - private static void SetCash(CashRegisterBase __instance, double amount) + [HarmonyPrefix] + [HarmonyPatch(nameof(CashRegisterBase.AddCash))] + private static bool AddCash(CashRegisterBase __instance, double amount) { if (__instance is not CashRegisterWithModules cashRegisterWithModules) - return; + return true; - Multiplayer.LogDebug(() => $"SetCash() {__instance.GetObjectPath()}, Deposited: {amount}"); + Multiplayer.LogDebug(() => $"AddCash() {__instance.GetObjectPath()}, Deposited: {amount}\r\n{Environment.StackTrace}"); if (!NetworkedCashRegisterWithModules.TryGet(cashRegisterWithModules, out var netCashRegister)) { - Multiplayer.LogWarning($"Attempting to SetCash, but NetworkedCashRegisterWithModules not found for {cashRegisterWithModules.GetObjectPath()}"); - return; + Multiplayer.LogWarning($"Attempting to AddCash, but NetworkedCashRegisterWithModules not found for {cashRegisterWithModules.GetObjectPath()}"); + return true; } if (netCashRegister.IsShopRegister) - return; + return true; + + Inventory.Instance.AddMoney(amount); + + CoroutineManager.Instance.StartCoroutine(netCashRegister.AddCash(amount)); - netCashRegister.SetCash(amount); + return false; } [HarmonyPrefix] diff --git a/Multiplayer/Patches/World/CashRegisterWithModulesPatch.cs b/Multiplayer/Patches/World/CashRegisterWithModulesPatch.cs index 5216bbd2..777ed144 100644 --- a/Multiplayer/Patches/World/CashRegisterWithModulesPatch.cs +++ b/Multiplayer/Patches/World/CashRegisterWithModulesPatch.cs @@ -28,6 +28,10 @@ private static bool OnDisable(CashRegisterWithModules __instance) [HarmonyPatch(nameof(CashRegisterWithModules.OnBuyPressed))] private static bool OnBuyPressed(CashRegisterWithModules __instance) { + var player = PlayerManager.PlayerTransform.position; + var reg = __instance.transform.position; + var sqrMag = (player - reg).sqrMagnitude; + Multiplayer.LogDebug(() => $"CashRegisterWithModules.OnBuyPressed() player pos: {player} register pos: {reg}, sqrMag: {sqrMag}"); if (NetworkLifecycle.Instance.IsHost()) return true; From 2daa0532a5f9b7c8c7fccb46925dec3cfbbe71a9 Mon Sep 17 00:00:00 2001 From: Macka Date: Fri, 19 Dec 2025 08:51:01 +1000 Subject: [PATCH 506/521] Refactor pit stop station networking and add tick to packets Removed unused grabbedHandlerLookup and updated lever handling logic in NetworkedPitStopStation. Improved logging and initialization flow for pit stop stations and cash registers. Added Tick property to CommonPitStopInteractionPacket and ensured it is set when sending interaction packets. Minor code style and logging improvements in NetworkClient and NetworkServer. --- .../World/NetworkedPitStopStation.cs | 143 ++++++++++-------- .../Managers/Client/NetworkClient.cs | 6 +- .../Managers/Server/NetworkServer.cs | 12 +- .../Common/CommonPitStopInteractionPacket.cs | 1 + 4 files changed, 90 insertions(+), 72 deletions(-) diff --git a/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs b/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs index 96163d5e..be6dc0a7 100644 --- a/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs +++ b/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs @@ -99,7 +99,7 @@ public static void InitialisePitStops() private SteppedJoint faucetCrankSteppedJoint; private readonly Dictionary leverHandler)> leverStateLookup = []; - private readonly Dictionary grabbedHandlerLookup = []; + //private readonly Dictionary grabbedHandlerLookup = []; private readonly Dictionary leverLookup = []; private readonly Dictionary resourceToPluggableObject = []; private readonly Dictionary resourceTypeToLocoResourceModule = []; @@ -220,7 +220,7 @@ protected override void OnDestroy() } leverStateLookup.Clear(); - grabbedHandlerLookup.Clear(); + //grabbedHandlerLookup.Clear(); leverLookup.Clear(); base.OnDestroy(); } @@ -252,48 +252,44 @@ public void OnPlayerDisconnect(ServerPlayer player) public void OnPlayerEnteredActivationRegion(ServerPlayer player) { - // Ensure all resource data exists - InitialiseData(); + // Ensure all resource data exists + InitialiseData(); - // One struct per module type - int resourceCount = Station.locoResourceModules.resourceModules.Length; - LocoResourceModuleData[] stateData = new LocoResourceModuleData[resourceCount]; + // One struct per module type + int resourceCount = Station.locoResourceModules.resourceModules.Length; + LocoResourceModuleData[] stateData = new LocoResourceModuleData[resourceCount]; - Multiplayer.LogDebug(() => $"NetworkedPitStopStation.OnPlayerEnteredActivationRegion() [{StationName}, {NetId}] player: {player.Username}, car count: {Station.pitstop.carList.Count}, resourceCount: {resourceCount}"); - int i; - for (i = 0; i < resourceCount; i++) - { - Multiplayer.LogDebug(() => $"NetworkedPitStopStation.OnPlayerEnteredActivationRegion() [{StationName}, {NetId}] player: {player.Username}, i: {i}, data count: {Station.locoResourceModules.resourceModules[i].resourceData.Count}"); - stateData[i] = LocoResourceModuleData.From(Station.locoResourceModules.resourceModules[i]); - } + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.OnPlayerEnteredActivationRegion() [{StationName}, {NetId}] player: {player.Username}, car count: {Station.pitstop.carList.Count}, resourceCount: {resourceCount}"); + int i; + for (i = 0; i < resourceCount; i++) + { + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.OnPlayerEnteredActivationRegion() [{StationName}, {NetId}] player: {player.Username}, i: {i}, data count: {Station.locoResourceModules.resourceModules[i].resourceData.Count}"); + stateData[i] = LocoResourceModuleData.From(Station.locoResourceModules.resourceModules[i]); + } - // Car selection and lever states - int carIndex = Station.pitstop.SelectedIndex; + // Car selection and lever states + int carIndex = Station.pitstop.SelectedIndex; - PitStopPlugData[] plugData = new PitStopPlugData[resourceToPluggableObject.Count]; + PitStopPlugData[] plugData = new PitStopPlugData[resourceToPluggableObject.Count]; - i = 0; - foreach (var plug in resourceToPluggableObject) - { - Multiplayer.LogDebug(() => $"NetworkedPitStopStation.OnPlayerEnteredActivationRegion() [{StationName}, {NetId}] player: {player.Username}, plug: {plug.Key}, plug netId: {plug.Value.NetId}"); - plugData[i] = PitStopPlugData.From(plug.Value, true); - i++; - } + i = 0; + foreach (var plug in resourceToPluggableObject) + { + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.OnPlayerEnteredActivationRegion() [{StationName}, {NetId}] player: {player.Username}, plug: {plug.Key}, plug netId: {plug.Value.NetId}"); + plugData[i] = PitStopPlugData.From(plug.Value, true); + i++; + } - int faucetPos = -1; - if (faucetCrankSteppedJoint != null) - { - faucetPos = faucetCrankSteppedJoint.currentNotch; - } - else - { - Multiplayer.LogDebug(() => $"NetworkedPitStopStation.OnPlayerEnteredActivationRegion() [{StationName}] faucetCrankSteppedJoint is null"); - } + int faucetPos = -1; + if (faucetCrankSteppedJoint != null) + faucetPos = faucetCrankSteppedJoint.currentNotch; + else + Multiplayer.LogWarning($"NetworkedPitStopStation.OnPlayerEnteredActivationRegion() [{StationName}] faucetCrankSteppedJoint is null"); - Multiplayer.LogDebug(() => $"NetworkedPitStopStation.OnPlayerEnteredActivationRegion() [{StationName}] faucetPos: {faucetPos}"); + //Multiplayer.LogDebug(() => $"NetworkedPitStopStation.OnPlayerEnteredActivationRegion() [{StationName}] faucetPos: {faucetPos}"); - // Send current state - NetworkLifecycle.Instance.Server.SendPitStopBulkDataPacket(NetId, Station.pitstop.carList.Count, carIndex, faucetPos, stateData, plugData, player); + // Send current state + NetworkLifecycle.Instance.Server.SendPitStopBulkDataPacket(NetId, Station.pitstop.carList.Count, carIndex, faucetPos, stateData, plugData, player); } public void OnPlayerEnteredCullingRegion(ServerPlayer player) @@ -304,7 +300,7 @@ public void OnPlayerEnteredCullingRegion(ServerPlayer player) public void ProcessInteractionPacketAsHost(CommonPitStopInteractionPacket packet, ServerPlayer senderPlayer) { - Multiplayer.LogDebug(() => $"NetworkedPitStopStation.ProcessInteractionPacketAsHost() from: {senderPlayer.Username}, id: {senderPlayer.PlayerId}, selfpeer: {NetworkLifecycle.Instance.Server.SelfId}"); + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.ProcessInteractionPacketAsHost() from: {senderPlayer.Username}, tick: {packet.Tick}, id: {senderPlayer.PlayerId}, selfpeer: {NetworkLifecycle.Instance.Server.SelfId}"); if (ValidateInteraction(packet, senderPlayer)) { @@ -386,6 +382,7 @@ private void SendResourceUpdate(LocoResourceModule module) CommonPitStopInteractionPacket packet = new() { + Tick = NetworkLifecycle.Instance.Tick, NetId = NetId, InteractionType = PitStopStationInteractionType.ResourceUpdate, ResourceType = (int)module.resourceType, @@ -439,11 +436,13 @@ public bool TryGetPluggable(ResourceType type, out NetworkedPluggableObject netP /// private IEnumerator Init() { - Multiplayer.LogDebug(() => $"NetworkedPitStopStation.Init() station: {Station == null}, pitstop: {Station?.pitstop == null}"); + Multiplayer.Log($"Initialising Station {Station.GetObjectPath()}"); while (Station?.pitstop == null) yield return new WaitForEndOfFrame(); + Multiplayer.Log($"Pitstop {Station.GetObjectPath()} initialised"); + if (NetworkLifecycle.Instance.IsHost()) { // Monitor changes to vehicles in the pit stop @@ -454,12 +453,29 @@ private IEnumerator Init() OnCarPitStopEntered(); } + // Wait for cash registers to load + yield return new WaitUntil(() => transform.parent.GetComponentInChildren(true) != null); register = transform.parent.GetComponentInChildren(true); - if (register == null) + + if (NetworkLifecycle.Instance.IsHost()) { - Multiplayer.LogWarning($"NetworkedPitStopStation.Init() No CashRegisterWithModules found for station {StationName}"); + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.Init() Waiting for NetworkedCashRegisterWithModules {StationName}"); + + NetworkedCashRegisterWithModules netRegister = null; + + yield return new WaitUntil( + () => + { + //Multiplayer.LogDebug(() => $"NetworkedPitStopStation.Init() Waiting for NetworkedCashRegisterWithModules {StationName} - spin...."); + return NetworkedCashRegisterWithModules.TryGet(register, out netRegister) && netRegister != null; + } + ); + + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.Init() Initialising Cash Register for station {StationName}"); + netRegister.Server_InitCashRegister(CullingManager); } + //Wait for levers an knobs to load yield return new WaitUntil(() => GetComponentInChildren(true) != null); carSelectorGrab = GetComponentInChildren(true); @@ -544,10 +560,8 @@ private IEnumerator Init() //Store delegate leverStateLookup[resourceModule.resourceType] = (checker, resourceModule, LeverStatehandler); - grabbedHandlerLookup[resourceModule.resourceType] = grab; - - if (lever != null) - leverLookup[resourceModule.resourceType] = lever; + //grabbedHandlerLookup[resourceModule.resourceType] = grab; + leverLookup[resourceModule.resourceType] = lever; //sb.AppendLine($"\t{resourceModule.resourceType}, Grab Handler found: {grab != null}, Name: {grab.name}"); sb.AppendLine($"\t{resourceModule.resourceType}, Rotary Amplitude Handler found: {checker != null}, Name: {checker.name}"); @@ -578,12 +592,13 @@ private IEnumerator Init() private IEnumerator SetUnitsDelayed(LocoResourceModule rm) { - if(rm == null || !isResourceRemoteGrabbedDict.ContainsKey(rm.resourceType)) + if (rm == null || !isResourceRemoteGrabbedDict.ContainsKey(rm.resourceType)) yield break; var resourceType = rm.resourceType; - yield return new WaitUntil(()=> !isResourceRemoteGrabbedDict[resourceType] && !rm.IsFlowing); + yield return new WaitUntil(() => !isResourceRemoteGrabbedDict[resourceType] && !rm.IsFlowing); + yield return null; SetUnits(rm, lastRemoteValueDict[resourceType]); } @@ -853,7 +868,7 @@ private void FaucetCrankPositionChanged(ValueChangedEventArgs args) public void ProcessBulkUpdate(ClientboundPitStopBulkUpdatePacket packet) { // Packet is broken up due to SubscribeResusable reusing/overwriting packet data - CoroutineManager.Instance.StartCoroutine(ProcessBulkUpdate_Internal(packet.CarCount, packet.CarSelection,packet.FaucetNotch,packet.ResourceData, packet.PlugData)); + CoroutineManager.Instance.StartCoroutine(ProcessBulkUpdate_Internal(packet.CarCount, packet.CarSelection, packet.FaucetNotch, packet.ResourceData, packet.PlugData)); } private IEnumerator ProcessBulkUpdate_Internal(int carCount, int carSelection, int faucetNotch, LocoResourceModuleData[] resourceData, PitStopPlugData[] plugData) @@ -925,12 +940,14 @@ private IEnumerator ProcessBulkUpdate_Internal(int carCount, int carSelection, i else Multiplayer.LogWarning($"PitStop module not found for resource type: {resource.ResourceType}"); + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.ProcessBulkUpdate_Internal() [{StationName}, {NetId}] Resource module data loaded for {resource.ResourceType}"); + // Set the grab state bool grabbed = (resource.FillingState != LocoResourceModuleFillingState.None); bool isLocallyGrabbed = isResourceGrabbedDict.TryGetValue(resource.ResourceType, out var localGrabbed) && localGrabbed; leverLookup.TryGetValue(resource.ResourceType, out LeverBase lever); - grabbedHandlerLookup.TryGetValue(resource.ResourceType, out LeverBase grab); + //grabbedHandlerLookup.TryGetValue(resource.ResourceType, out LeverBase grab); if (!isLocallyGrabbed) { @@ -939,9 +956,11 @@ private IEnumerator ProcessBulkUpdate_Internal(int carCount, int carSelection, i lever.InteractionAllowed = !grabbed; if (grabbed) - grab?.ForceEndInteraction(); + lever?.ForceEndInteraction(); } + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.ProcessBulkUpdate_Internal() [{StationName}, {NetId}] Grab states set for {resource.ResourceType}, state: {grabbed}"); + int valvePos = resource.FillingState switch { LocoResourceModuleFillingState.Filling => -1, @@ -951,6 +970,8 @@ private IEnumerator ProcessBulkUpdate_Internal(int carCount, int carSelection, i module.OnValvePositionChange(valvePos); + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.ProcessBulkUpdate_Internal() [{StationName}, {NetId}] Valve position set for {resource.ResourceType}, position: {valvePos}"); + // Update remote grab state isResourceRemoteGrabbedDict[resource.ResourceType] = grabbed; } @@ -973,6 +994,8 @@ private IEnumerator ProcessBulkUpdate_Internal(int carCount, int carSelection, i netPlug?.ProcessBulkUpdate(plug); } + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.ProcessBulkUpdate_Internal() [{StationName}, {NetId}] Plugs synced"); + // Sync faucet position if (faucetPositioner != null) { @@ -1001,6 +1024,8 @@ public void ProcessInteractionPacketAsClient(CommonPitStopInteractionPacket pack LeverBase lever = null; LocoResourceModule resourceModule = null; + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.ProcessInteractionPacketAsClient() [{StationName}, {NetId}] Tick: {packet.Tick}, Packet InteractionType: {packet.InteractionType}, ResourceType: {packet.ResourceType}, Value: {packet.Value}"); + // Validate interaction type if (!Enum.IsDefined(typeof(PitStopStationInteractionType), packet.InteractionType)) { @@ -1050,28 +1075,20 @@ public void ProcessInteractionPacketAsClient(CommonPitStopInteractionPacket pack case PitStopStationInteractionType.LeverState: leverLookup.TryGetValue(resourceType, out lever); - if (!grabbedHandlerLookup.TryGetValue(resourceType, out grab)) + if (!leverStateLookup.TryGetValue(resourceType, out var tup)) { - Multiplayer.LogError($"Could not find ResourceType in grabbedHandlerLookup for Pit Stop station {StationName}, resource type: {resourceType}"); + Multiplayer.LogError($"Could not find Rotary Amplitude Handler in rotaryAmplitudeLookup for Pit Stop station {StationName}, resource type: {resourceType}"); return; } else { - if (!leverStateLookup.TryGetValue(resourceType, out var tup)) + (amplitudeChecker, resourceModule, _) = tup; + + if (packet.Value < RotaryAmplitudeChecker.MIN_REACHED || packet.Value > RotaryAmplitudeChecker.MAX_REACHED) { - Multiplayer.LogError($"Could not find Rotary Amplitude Handler in rotaryAmplitudeLookup for Pit Stop station {StationName}, resource type: {resourceType}"); + Multiplayer.LogError($"Invalid lever value ({packet.Value}) received for Pit Stop station {StationName}, resource type: {resourceType}"); return; } - else - { - (amplitudeChecker, resourceModule, _) = tup; - - if (packet.Value < RotaryAmplitudeChecker.MIN_REACHED || packet.Value > RotaryAmplitudeChecker.MAX_REACHED) - { - Multiplayer.LogError($"Invalid lever value ({packet.Value}) received for Pit Stop station {StationName}, resource type: {resourceType}"); - return; - } - } } bool grabbed = (packet.Value != 0); diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 9a13b1ab..280d7a83 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -10,7 +10,6 @@ using DV.UI; using DV.UserManagement; using DV.WeatherSystem; -using DV; using LiteNetLib; using LiteNetLib.Utils; using MPAPI.Interfaces.Packets; @@ -246,10 +245,10 @@ private void OnLoaded() NetworkedItemManager.Instance.CheckInstance(); Log($"WorldStreamingInit.LoadingFinished() CacheWorldItems()"); NetworkedItemManager.Instance.CacheWorldItems(); - Log($"WorldStreamingInit.LoadingFinished() InitialisePitStops()"); - NetworkedPitStopStation.InitialisePitStops(); Log($"WorldStreamingInit.LoadingFinished() InitialiseCashRegisters()"); NetworkedCashRegisterWithModules.InitialiseCashRegisters(); + Log($"WorldStreamingInit.LoadingFinished() InitialisePitStops()"); + NetworkedPitStopStation.InitialisePitStops(); Log($"WorldStreamingInit.LoadingFinished() SendReadyPacket()"); CoroutineManager.Instance.StartCoroutine(WaitForReadyBlocks()); @@ -1555,6 +1554,7 @@ public void SendPitStopInteractionPacket(ushort netId, PitStopStationInteraction int res = resource == null ? 0 : (int)resource; SendPacketToServer(new CommonPitStopInteractionPacket { + Tick = NetworkLifecycle.Instance.Tick, NetId = netId, InteractionType = interaction, ResourceType = res, diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index 4898dea5..fda80dbe 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -652,10 +652,10 @@ public void SendWindowsBroken(ushort netId, Vector3 forceDirection) SendPacketToAll ( new ClientboundWindowsBrokenPacket - { - NetId = netId, - ForceDirection = forceDirection - }, DeliveryMethod.ReliableUnordered, SelfPeer); + { + NetId = netId, + ForceDirection = forceDirection + }, DeliveryMethod.ReliableUnordered, SelfPeer); } public void SendWindowsRepaired(ushort netId) @@ -849,7 +849,7 @@ public void SendItemsChangePacket(List items, ServerPlayer playe } } - public void SendPitStopBulkDataPacket(ushort netId, int carCount, int carIndex, int faucetNotch, LocoResourceModuleData[] stationData, PitStopPlugData[] plugData , ServerPlayer player) + public void SendPitStopBulkDataPacket(ushort netId, int carCount, int carIndex, int faucetNotch, LocoResourceModuleData[] stationData, PitStopPlugData[] plugData, ServerPlayer player) { LogDebug(() => $"SendPitStopBulkDataPacket({netId}, {carCount}, {carIndex}, {faucetNotch}, {stationData.Count()}, {plugData.Count()}, {player})"); @@ -1664,7 +1664,7 @@ private void OnCommonPitStopPlugInteractionPacket(CommonPitStopPlugInteractionPa }, DeliveryMethod.ReliableOrdered); } - if(NetworkedPluggableObject.Get(packet.NetId, out NetworkedPluggableObject plug) && foundPlayer) + if (NetworkedPluggableObject.Get(packet.NetId, out NetworkedPluggableObject plug) && foundPlayer) { plug.ProcessInteractionPacketAsHost(packet, player); } diff --git a/Multiplayer/Networking/Packets/Common/CommonPitStopInteractionPacket.cs b/Multiplayer/Networking/Packets/Common/CommonPitStopInteractionPacket.cs index a4d82831..40aeaff0 100644 --- a/Multiplayer/Networking/Packets/Common/CommonPitStopInteractionPacket.cs +++ b/Multiplayer/Networking/Packets/Common/CommonPitStopInteractionPacket.cs @@ -5,6 +5,7 @@ namespace Multiplayer.Networking.Packets.Common; public class CommonPitStopInteractionPacket { + public uint Tick { get; set; } public ushort NetId { get; set; } public PitStopStationInteractionType InteractionType { get; set; } public int ResourceType { get; set; } From 293fa6091e1f9c0da53d18ef30a43bae4a047fc3 Mon Sep 17 00:00:00 2001 From: Macka Date: Fri, 19 Dec 2025 08:54:49 +1000 Subject: [PATCH 507/521] Ensure player objects are cleaned up --- .../Managers/Server/NetworkServer.cs | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index fda80dbe..2f32e89b 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -279,12 +279,9 @@ public override void OnPeerDisconnected(ITransportPeer peer, DisconnectReason di { LogDebug(() => $"OnPeerDisconnected({peer.Id})"); if (!peerToPlayer.TryGetValue(peer, out ServerPlayer player)) - { LogWarning($"Peer {peer.GetType()}, peerId: {peer.Id} disconnected but no player found"); - return; - } - - Log($"Player {player.Username} disconnected: {disconnectReason}"); + else + Log($"Player {player?.Username} disconnected: {disconnectReason}"); if (WorldStreamingInit.isLoaded) SaveGameManager.Instance.UpdateInternalData(); @@ -294,17 +291,17 @@ public override void OnPeerDisconnected(ITransportPeer peer, DisconnectReason di peerToPlayer.Remove(peer); SendPacketToAll - ( - new ClientboundPlayerDisconnectPacket - { - PlayerId = player.PlayerId - }, - DeliveryMethod.ReliableUnordered - ); + ( + new ClientboundPlayerDisconnectPacket + { + PlayerId = player.PlayerId + }, + DeliveryMethod.ReliableUnordered + ); PlayerDisconnected?.Invoke(player); - player.Dispose(); + player?.Dispose(); } public override void OnNetworkLatencyUpdate(ITransportPeer peer, int latency) From 3f569208ae09301b5e9aa05243e928818c217acd Mon Sep 17 00:00:00 2001 From: Macka Date: Fri, 19 Dec 2025 09:18:04 +1000 Subject: [PATCH 508/521] Ready for release 0.1.13.4 --- Multiplayer/Multiplayer.csproj | 2 +- info.json | 2 +- releases.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Multiplayer/Multiplayer.csproj b/Multiplayer/Multiplayer.csproj index 2fc35c7c..a4aef290 100644 --- a/Multiplayer/Multiplayer.csproj +++ b/Multiplayer/Multiplayer.csproj @@ -3,7 +3,7 @@ net48 latest Multiplayer - 0.1.13.0 + 0.1.13.4 diff --git a/info.json b/info.json index 8f6cc773..0e7c9f15 100644 --- a/info.json +++ b/info.json @@ -1,6 +1,6 @@ { "Id": "Multiplayer", - "Version": "0.1.13.0", + "Version": "0.1.13.4", "DisplayName": "Multiplayer", "Author": "Insprill, Macka, Morm", "EntryMethod": "Multiplayer.Multiplayer.Load", diff --git a/releases.json b/releases.json index 7496cc25..baad24ff 100644 --- a/releases.json +++ b/releases.json @@ -1,6 +1,6 @@ { "Releases": [ - {"Id": "Multiplayer", "Version": "0.1.13.0", "DownloadUrl": "https://github.com/AMacro/dv-multiplayer/releases/download/v0.1.13.0-Beta/Multiplayer.0.1.13.0.zip"} + {"Id": "Multiplayer", "Version": "0.1.13.4", "DownloadUrl": "https://github.com/AMacro/dv-multiplayer/releases/download/v0.1.13.4-Beta/Multiplayer.0.1.13.4.zip"} ] } \ No newline at end of file From 5a9d440682b38bae4af68da90b796948cb6390b6 Mon Sep 17 00:00:00 2001 From: AMacro Date: Wed, 24 Dec 2025 21:48:58 +1030 Subject: [PATCH 509/521] Enhance mod requirements in server browser UI ModInfo now includes a trusted URL field and serialises mod lists as JSON for improved compatibility checks. The server browser details pane displays mod status with hyperlinks, and version checks now use a unified build version string. --- Multiplayer/API/ModCompatibilityManager.cs | 12 +- .../Components/MainMenu/HostGamePane.cs | 14 +-- .../Components/MainMenu/ServerBrowserPane.cs | 113 ++++++++++++++---- .../Components/Util/HyperlinkHandler.cs | 6 +- .../Networking/Data/LobbyServerData.cs | 1 + Multiplayer/Networking/Data/ModInfo.cs | 70 ++++++++++- .../Managers/Client/NetworkClient.cs | 3 +- .../MainMenu/MainMenuControllerPatch.cs | 100 ++++++++-------- .../MainMenu/RightPaneControllerPatch.cs | 4 +- 9 files changed, 228 insertions(+), 95 deletions(-) diff --git a/Multiplayer/API/ModCompatibilityManager.cs b/Multiplayer/API/ModCompatibilityManager.cs index 2216af8e..66ec4a08 100644 --- a/Multiplayer/API/ModCompatibilityManager.cs +++ b/Multiplayer/API/ModCompatibilityManager.cs @@ -141,7 +141,7 @@ public bool CheckModCompatibility() return false; var message = $"{Locale.MAIN_MENU__INCOMPATIBLE_MODS} {string.Join(", ", incompatible)}"; - + MainMenuThingsAndStuff.Instance.ShowOkPopup(message, () => { }); return true; @@ -150,20 +150,15 @@ public bool CheckModCompatibility() // Returns a list of mods for use in the lobby data public string GetRequiredMods() { - List requiredMods = []; - var local = GetLocalMods(); if (local == null) return null; - foreach (var modInfo in local) - requiredMods.Add(modInfo.Id); - - return string.Join(", ", requiredMods); + return Newtonsoft.Json.JsonConvert.SerializeObject(local); } - //Returns a list of mods installed and enabled, filtered for mods that are required for hosts and clients + // Returns a list of mods installed and enabled, filtered for mods that are required for hosts and clients public ModInfo[] GetLocalMods() { List localMods = []; @@ -199,7 +194,6 @@ public ModInfo[] GetLocalMods() } } return localMods.ToArray(); - } [UsedImplicitly] diff --git a/Multiplayer/Components/MainMenu/HostGamePane.cs b/Multiplayer/Components/MainMenu/HostGamePane.cs index 5d5cb553..16cd6702 100644 --- a/Multiplayer/Components/MainMenu/HostGamePane.cs +++ b/Multiplayer/Components/MainMenu/HostGamePane.cs @@ -1,22 +1,23 @@ +using DV; using DV.Common; using DV.Localization; using DV.Platform.Steam; -using DV.UI.PresetEditors; using DV.UI; +using DV.UI.PresetEditors; using DV.UIFramework; -using DV; +using Multiplayer.API; using Multiplayer.Components.Networking; using Multiplayer.Components.Util; using Multiplayer.Networking.Data; +using Multiplayer.Patches.MainMenu; using Multiplayer.Utils; +using System; using System.Linq; using System.Reflection; -using System; using TMPro; +using UnityEngine; using UnityEngine.Events; using UnityEngine.UI; -using UnityEngine; -using Multiplayer.API; namespace Multiplayer.Components.MainMenu; @@ -432,14 +433,13 @@ private void OnStartClick() string requiredMods = ModCompatibilityManager.Instance.GetRequiredMods(); if (requiredMods == null) { - incompatibleMods = true; ValidateInputs(null); return; } serverData.RequiredMods = requiredMods; - serverData.GameVersion = Multiplayer.LocalBuildInfo; + serverData.GameVersion = MainMenuControllerPatch.MenuProvider.BuildVersionString; serverData.MultiplayerVersion = Multiplayer.Ver; serverData.ServerDetails = details.text.Trim(); diff --git a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs index c85eb64a..ad884cc1 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs @@ -1,15 +1,17 @@ -using DV; using DV.Localization; using DV.Platform.Steam; using DV.UI; using DV.UIFramework; using DV.Utils; using LiteNetLib; +using MPAPI.Types; using Multiplayer.API; using Multiplayer.Components.MainMenu.ServerBrowser; using Multiplayer.Components.Networking; using Multiplayer.Components.UI.Controls; +using Multiplayer.Components.Util; using Multiplayer.Networking.Data; +using Multiplayer.Patches.MainMenu; using Multiplayer.Utils; using Steamworks; using Steamworks.Data; @@ -18,6 +20,7 @@ using System.Collections.Generic; using System.Linq; using System.Net; +using System.Text; using System.Threading.Tasks; using TMPro; using UnityEngine; @@ -62,6 +65,7 @@ private enum ConnectionState //Misc GUI Elements private TextMeshProUGUI serverName; private TextMeshProUGUI detailsPane; + private HyperlinkHandler modHyperlinkHandler; //Remote server tracking private readonly List remoteServers = []; @@ -300,6 +304,12 @@ private void BuildUI() // Set content size to fit text contentRT.sizeDelta = new Vector2(contentRT.sizeDelta.x - 50, detailsPane.preferredHeight); + // Enable hyperlink parsing + modHyperlinkHandler = textGO.GetOrAddComponent(); + + modHyperlinkHandler.linkColor = new UnityEngine.Color(0.302f, 0.651f, 1f); // #4DA6FF + modHyperlinkHandler.linkHoverColor = new UnityEngine.Color(0.498f, 0.749f, 1f); // #7FBFFF + // 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); @@ -405,7 +415,7 @@ private void JoinAction() private void DirectAction() { - if(connectionState != ConnectionState.NotConnected) + if (connectionState != ConnectionState.NotConnected) return; buttonDirectIP.ToggleInteractable(false); @@ -428,12 +438,12 @@ private void OnSelectedIndexChanged(MPGridView gridVi { UpdateDetailsPane(); - //Check if we can connect to this server + // Check if we can connect to this server Multiplayer.Log($"Server: \"{selectedServer.GameVersion}\" \"{selectedServer.MultiplayerVersion}\""); - Multiplayer.Log($"Client: \"{Multiplayer.LocalBuildInfo}\" \"{Multiplayer.Ver}\""); - Multiplayer.Log($"Result: \"{selectedServer.GameVersion == Multiplayer.LocalBuildInfo}\" \"{selectedServer.MultiplayerVersion == Multiplayer.Ver}\""); + Multiplayer.Log($"Client: \"{MainMenuControllerPatch.MenuProvider.BuildVersionString}\" \"{Multiplayer.Ver}\""); + Multiplayer.Log($"Result: \"{selectedServer.GameVersion == MainMenuControllerPatch.MenuProvider.BuildVersionString}\" \"{selectedServer.MultiplayerVersion == Multiplayer.Ver}\""); - bool canConnect = selectedServer.GameVersion == Multiplayer.LocalBuildInfo && + bool canConnect = selectedServer.GameVersion == MainMenuControllerPatch.MenuProvider.BuildVersionString && selectedServer.MultiplayerVersion == Multiplayer.Ver; buttonJoin.ToggleInteractable(canConnect); @@ -459,28 +469,89 @@ private void UpdateElement(IServerBrowserGameDetails element) private void UpdateDetailsPane() { - string details; + StringBuilder details = new(); + StringBuilder modDetails = new("Mods:"); + + var localMods = ModCompatibilityManager.Instance.GetLocalMods(); + + Multiplayer.LogDebug(() => $"Temp Mod Json: \"{tempReqMods}\""); + if (!string.IsNullOrEmpty(selectedServer.RequiredMods)) + { + var modData = ModInfo.DeserializeRequiredMods(selectedServer.RequiredMods); + + Multiplayer.LogDebug(() => $"Parsed {modData?.Length} mods from server \"{selectedServer?.Name}\""); + + foreach (var mod in modData) + { + ModInfo modMatch = localMods.FirstOrDefault(l => l.Id == mod.Id); + + Multiplayer.LogDebug(() => $"Checking mod \"{mod.Id}\" v\"{mod.Version}\" - Found: \"{modMatch.Id}\" v\"{modMatch.Version}\""); + + bool modFound = modMatch.Id == mod.Id; + bool modVersionMatch = modFound && modMatch.Version == mod.Version; + + string status; + if (modFound && modVersionMatch) + status = "OK"; + else if (modFound && !modVersionMatch) + status = "Version Mismatch"; + else + status = "Missing"; + + var link = !string.IsNullOrEmpty(mod.Url) ? $"{mod.Id}" : mod.Id; + + modDetails.Append($"
    {link} ({mod.Version}) - {status}"); + } + Multiplayer.LogDebug(() => $"Mod details for server \"{selectedServer.Name}\":\r\n{modDetails.ToString()}"); + var extraMods = localMods.Where(l => !modData.Any(m => m.Id == l.Id)).ToArray(); + Multiplayer.LogDebug(() => $"Found {extraMods.Length} extra mods on client for server \"{selectedServer.Name}\""); + + if (extraMods.Length > 0) + { + foreach (var mod in extraMods) + { + var compatibility = ModCompatibilityManager.Instance.GetCompatibility(mod); + if (compatibility == MultiplayerCompatibility.Incompatible) + { + modDetails.Append($"
    {mod.Id} ({mod.Version}) - Incompatible"); + } + else if (compatibility == MultiplayerCompatibility.Undefined || compatibility == MultiplayerCompatibility.All) + { + modDetails.Append($"
    {mod.Id} ({mod.Version}) - Extra Mod"); + } + } + } + } + Multiplayer.LogDebug(() => $"Finished compiling mod details for server \"{selectedServer.Name}\""); + Multiplayer.LogDebug(() => $"Version: {MainMenuControllerPatch.MenuProvider.BuildVersionString}"); if (selectedServer != null) { serverName.text = selectedServer.Name; //note: built-in localisations have a trailing colon e.g. 'Game mode:' - 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) + "
    "; - details += "
    "; - details += "" + Locale.SERVER_BROWSER__GAME_VERSION + ": " + (selectedServer.GameVersion != Multiplayer.LocalBuildInfo ? "" : "") + selectedServer.GameVersion + "
    "; - details += "" + Locale.SERVER_BROWSER__MOD_VERSION + ": " + (selectedServer.MultiplayerVersion != Multiplayer.Ver ? "" : "") + selectedServer.MultiplayerVersion + "
    "; - details += "
    "; - details += selectedServer.ServerDetails; + details.Append("" + LocalizationAPI.L("launcher/game_mode", []) + " " + LobbyServerData.GetGameModeFromInt(selectedServer.GameMode) + "
    "); + details.Append("" + LocalizationAPI.L("launcher/difficulty", []) + " " + LobbyServerData.GetDifficultyFromInt(selectedServer.Difficulty) + "
    "); + details.Append("" + LocalizationAPI.L("launcher/in_game_time_passed", []) + " " + selectedServer.TimePassed + "
    "); + details.Append("" + Locale.SERVER_BROWSER__PLAYERS + ": " + selectedServer.CurrentPlayers + '/' + selectedServer.MaxPlayers + "
    "); + details.Append("" + Locale.SERVER_BROWSER__PASSWORD_REQUIRED + ": " + (selectedServer.HasPassword ? Locale.SERVER_BROWSER__YES : Locale.SERVER_BROWSER__NO) + "
    "); + details.Append("" + Locale.SERVER_BROWSER__MODS_REQUIRED + ": " + (string.IsNullOrEmpty(selectedServer.RequiredMods) ? Locale.SERVER_BROWSER__NO : Locale.SERVER_BROWSER__YES) + "
    "); + details.Append("
    "); + details.Append("" + Locale.SERVER_BROWSER__GAME_VERSION + ": " + (selectedServer.GameVersion != MainMenuControllerPatch.MenuProvider.BuildVersionString ? "" : "") + selectedServer.GameVersion + "
    "); + details.Append("" + Locale.SERVER_BROWSER__MOD_VERSION + ": " + (selectedServer.MultiplayerVersion != Multiplayer.Ver ? "" : "") + selectedServer.MultiplayerVersion + "
    "); + details.Append("
    "); + details.Append(selectedServer.ServerDetails); + + if (selectedServer.ServerDetails != null && selectedServer.ServerDetails.Length > 0) + details.Append("
    "); + + details.Append(modDetails.ToString()); //Multiplayer.Log("Finished Prepping Data"); - detailsPane.text = details; + detailsPane.text = details.ToString(); + + modHyperlinkHandler.ApplyLinkStyling(); } } @@ -968,7 +1039,7 @@ private void RefreshGridView() } } } - + private string ExtractDomainName(string input) { if (input.StartsWith("http://")) @@ -1006,7 +1077,7 @@ private async Task JoinLobby(Lobby lobby) if (joinResult == RoomEnter.Success) { Multiplayer.Log($"Lobby joined ({lobby.Id})"); - + joinedLobby = lobby; lobbyToJoin = null; diff --git a/Multiplayer/Components/Util/HyperlinkHandler.cs b/Multiplayer/Components/Util/HyperlinkHandler.cs index 91cc380c..22f590d6 100644 --- a/Multiplayer/Components/Util/HyperlinkHandler.cs +++ b/Multiplayer/Components/Util/HyperlinkHandler.cs @@ -20,7 +20,7 @@ public class HyperlinkHandler : MonoBehaviour, IPointerClickHandler private int hoveredLinkIndex = -1; private bool underlineLinks = true; - void Start() + protected void Start() { if (textComponent == null) { @@ -36,7 +36,7 @@ void Start() ApplyLinkStyling(); } - void Update() + protected void Update() { int linkIndex = TMP_TextUtilities.FindIntersectingLink(textComponent, Input.mousePosition, canvasCamera); @@ -71,7 +71,7 @@ public void OnPointerClick(PointerEventData eventData) } } - private void ApplyLinkStyling() + public void ApplyLinkStyling() { string text = textComponent.text; string pattern = @"(.*?)<\/link>"; diff --git a/Multiplayer/Networking/Data/LobbyServerData.cs b/Multiplayer/Networking/Data/LobbyServerData.cs index b7a562fa..9f7d8b02 100644 --- a/Multiplayer/Networking/Data/LobbyServerData.cs +++ b/Multiplayer/Networking/Data/LobbyServerData.cs @@ -70,6 +70,7 @@ public class LobbyServerData : IServerBrowserGameDetails public void Dispose() { } + public static int GetDifficultyFromString(string difficulty) { int diff = 0; diff --git a/Multiplayer/Networking/Data/ModInfo.cs b/Multiplayer/Networking/Data/ModInfo.cs index 31ccf1d3..63ab110d 100644 --- a/Multiplayer/Networking/Data/ModInfo.cs +++ b/Multiplayer/Networking/Data/ModInfo.cs @@ -1,7 +1,7 @@ +using LiteNetLib.Utils; using System; using System.Collections.Generic; using System.Linq; -using LiteNetLib.Utils; using UnityModManagerNet; namespace Multiplayer.Networking.Data; @@ -11,11 +11,17 @@ public readonly struct ModInfo { public readonly string Id; public readonly string Version; - - public ModInfo(string id, string version) + public readonly string Url; + + public ModInfo(string id, string version, string url) { Id = id; Version = version; + + if (IsTrustedURL(url)) + Url = url; + else + Url = ""; } public override string ToString() @@ -27,11 +33,19 @@ public static void Serialize(NetDataWriter writer, ModInfo modInfo) { writer.Put(modInfo.Id); writer.Put(modInfo.Version); + writer.Put(modInfo.Url); } public static ModInfo Deserialize(NetDataReader reader) { - return new ModInfo(reader.GetString(), reader.GetString()); + string id = reader.GetString(); + string version = reader.GetString(); + string url = ""; + + if (reader.AvailableBytes > 0) + url = reader.GetString(); + + return new ModInfo(id, version, url); } public static ModInfo[] FromModEntries(IEnumerable modEntries) @@ -39,7 +53,53 @@ public static ModInfo[] FromModEntries(IEnumerable mod 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)) + .Select(entry => new ModInfo(entry.Info.Id, entry.Info.Version, entry.Info?.HomePage)) .ToArray(); } + + private static bool IsTrustedURL(string url) + { + if (string.IsNullOrEmpty(url)) + return false; + + if (!Uri.TryCreate(url, UriKind.Absolute, out var uriResult)) + return false; + + var host = uriResult.Host.ToLowerInvariant(); + + if (host == "nexusmods.com" || host == "www.nexusmods.com") + { + Multiplayer.LogDebug(() => $"IsTrustedURL() \"{url}\" is Nexus Mods"); + return true; + } + + if (host == "github.com" || host == "www.github.com") + { + Multiplayer.LogDebug(() => $"IsTrustedURL() \"{url}\" is Github"); + return true; + } + + Multiplayer.LogDebug(() => $"IsTrustedURL() \"{url}\" is untrusted"); + return false; + } + + public static ModInfo[] DeserializeRequiredMods(string json) + { + // Handle null or empty for backward compatibility + if (string.IsNullOrEmpty(json)) + { + Multiplayer.LogWarning("No mod data received (likely from older client/server version)"); + return []; + } + + try + { + return Newtonsoft.Json.JsonConvert.DeserializeObject(json); + } + catch (Exception e) + { + Multiplayer.LogException("Failed to deserialize mod data", e); + return []; + } + } } diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 280d7a83..41f4850f 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -36,6 +36,7 @@ using Multiplayer.Networking.Packets.Serverbound.Jobs; using Multiplayer.Networking.Packets.Serverbound.Train; using Multiplayer.Networking.TransportLayers; +using Multiplayer.Patches.MainMenu; using Multiplayer.Patches.SaveGame; using Multiplayer.Utils; using Newtonsoft.Json.Linq; @@ -97,7 +98,7 @@ public void Start(string address, int port, string password, bool isSinglePlayer Username = Multiplayer.Settings.GetUserName(), Guid = Multiplayer.Settings.GetGuid().ToByteArray(), Password = password, - BuildVersion = Multiplayer.LocalBuildInfo, + BuildVersion = MainMenuControllerPatch.MenuProvider.BuildVersionString, Mods = ModCompatibilityManager.Instance.GetLocalMods() }; netPacketProcessor.Write(cachedWriter, serverboundClientLoginPacket); diff --git a/Multiplayer/Patches/MainMenu/MainMenuControllerPatch.cs b/Multiplayer/Patches/MainMenu/MainMenuControllerPatch.cs index a26efe28..28cc7fc9 100644 --- a/Multiplayer/Patches/MainMenu/MainMenuControllerPatch.cs +++ b/Multiplayer/Patches/MainMenu/MainMenuControllerPatch.cs @@ -5,66 +5,72 @@ using UnityEngine; using UnityEngine.UI; -namespace Multiplayer.Patches.MainMenu +namespace Multiplayer.Patches.MainMenu; + +/// +/// Harmony patch MainMenuController to add a Multiplayer button. +/// +[HarmonyPatch(typeof(MainMenuController))] +public static class MainMenuControllerPatch { + public static AMainMenuProvider MenuProvider => _mainMenuControllerInstance.provider; + public static GameObject MultiplayerButton { get; private set; } + + private static MainMenuController _mainMenuControllerInstance; + /// - /// Harmony patch for the Awake method of MainMenuController to add a Multiplayer button. + /// Prefix method to run before MainMenuController's Awake method. /// - [HarmonyPatch(typeof(MainMenuController), "Awake")] - public static class MainMenuController_Awake_Patch + /// The instance of MainMenuController. + [HarmonyPatch(typeof(MainMenuController), nameof(MainMenuController.Awake))] + [HarmonyPrefix] + private static void Awake(MainMenuController __instance) { - public static GameObject multiplayerButton; + _mainMenuControllerInstance = __instance; - /// - /// 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 + GameObject sessionsButton = __instance.FindChildByName("ButtonSelectable Sessions"); + if (sessionsButton == null) { - // 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; - } + 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); + // Deactivate the sessions button temporarily to duplicate it + sessionsButton.SetActive(false); + MultiplayerButton = Object.Instantiate(sessionsButton, sessionsButton.transform.parent); + sessionsButton.SetActive(true); - // Configure the new Multiplayer button - multiplayerButton.transform.SetSiblingIndex(sessionsButton.transform.GetSiblingIndex() + 1); - multiplayerButton.name = "ButtonSelectable Multiplayer"; + // Configure the new Multiplayer button + MultiplayerButton.transform.SetSiblingIndex(sessionsButton.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; + // Set the localization key for the new button + Localize localize = MultiplayerButton.GetComponentInChildren(); + localize.key = Locale.MAIN_MENU__JOIN_SERVER_KEY; - // Remove existing localization components to reset them - Object.Destroy(multiplayerButton.GetComponentInChildren()); - multiplayerButton.ResetTooltip(); + // Remove existing localization components to reset them + Object.Destroy(MultiplayerButton.GetComponentInChildren()); + MultiplayerButton.ResetTooltip(); - // Set the icon for the new Multiplayer button - SetButtonIcon(multiplayerButton); - } + // Set the icon for the new Multiplayer button + SetButtonIcon(MultiplayerButton); + } - /// - /// Sets the icon for the Multiplayer button. - /// - /// The button to set the icon for. - private static void SetButtonIcon(GameObject button) + /// + /// 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) { - 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; + 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 37ffe7d0..74c16e0e 100644 --- a/Multiplayer/Patches/MainMenu/RightPaneControllerPatch.cs +++ b/Multiplayer/Patches/MainMenu/RightPaneControllerPatch.cs @@ -48,7 +48,7 @@ private static void OnEnablePre(RightPaneController __instance) // Add the multiplayer pane to the menu controller __instance.menuController.controlledMenus.Add(multiplayerPane.GetComponent()); joinMenuIndex = __instance.menuController.controlledMenus.Count - 1; - UIMenuRequester mpButtonReq = MainMenuController_Awake_Patch.multiplayerButton.GetComponent(); + UIMenuRequester mpButtonReq = MainMenuControllerPatch.MultiplayerButton.GetComponent(); mpButtonReq.requestedMenuIndex = joinMenuIndex; // Clean up unnecessary components and child objects @@ -70,7 +70,7 @@ private static void OnEnablePre(RightPaneController __instance) }); // Activate the multiplayer button - MainMenuController_Awake_Patch.multiplayerButton.SetActive(true); + MainMenuControllerPatch.MultiplayerButton.SetActive(true); //Multiplayer.Log("At end!"); // Check if the host pane already exists From 345416bd5889b63cd5830aea81cc374ee4a4588c Mon Sep 17 00:00:00 2001 From: AMacro Date: Wed, 24 Dec 2025 21:49:34 +1030 Subject: [PATCH 510/521] Update build version check to use MenuProvider Replaced usage of Multiplayer.LocalBuildInfo with MainMenuControllerPatch.MenuProvider.BuildVersionString for build version validation during login. --- Multiplayer/Networking/Managers/Server/NetworkServer.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index 2f32e89b..9c0468fa 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -30,6 +30,7 @@ using Multiplayer.Networking.Packets.Serverbound.Train; using Multiplayer.Networking.Packets.Unconnected; using Multiplayer.Networking.TransportLayers; +using Multiplayer.Patches.MainMenu; using Multiplayer.Utils; using System; using System.Collections.Generic; @@ -975,13 +976,13 @@ private void OnServerboundClientLoginPacket(ServerboundClientLoginPacket packet, return; } - if (packet.BuildVersion != Multiplayer.LocalBuildInfo) + if (packet.BuildVersion != MainMenuControllerPatch.MenuProvider.BuildVersionString) { - LogWarning($"Denied login to incorrect game version! Got: {packet.BuildVersion}, expected: {Multiplayer.LocalBuildInfo}"); + LogWarning($"Denied login to incorrect game version! Got: {packet.BuildVersion}, expected: {MainMenuControllerPatch.MenuProvider.BuildVersionString}"); ClientboundLoginResponsePacket denyPacket = new() { ReasonKey = Locale.DISCONN_REASON__GAME_VERSION_KEY, - ReasonArgs = [Multiplayer.LocalBuildInfo, packet.BuildVersion.ToString()] + ReasonArgs = [MainMenuControllerPatch.MenuProvider.BuildVersionString, packet.BuildVersion.ToString()] }; request.Reject(WritePacket(denyPacket)); return; From a7d9d7a6d1921c3d3245b4f1130dbc150096bac1 Mon Sep 17 00:00:00 2001 From: Macka Date: Thu, 1 Jan 2026 15:39:28 +1000 Subject: [PATCH 511/521] Refactor LobbyServerData 'required mods' to use ModInfo[] instead of string Changed the representation of required mods so it is correctly serialised to JSON, rather than serialising to a string containing JSON. --- Multiplayer/API/ModCompatibilityManager.cs | 11 ----------- Multiplayer/Components/MainMenu/HostGamePane.cs | 2 +- .../ServerBrowser/IServerBrowserGameDetails.cs | 3 ++- Multiplayer/Components/MainMenu/ServerBrowserPane.cs | 7 +++---- Multiplayer/Networking/Data/LobbyServerData.cs | 2 +- 5 files changed, 7 insertions(+), 18 deletions(-) diff --git a/Multiplayer/API/ModCompatibilityManager.cs b/Multiplayer/API/ModCompatibilityManager.cs index 66ec4a08..e595707e 100644 --- a/Multiplayer/API/ModCompatibilityManager.cs +++ b/Multiplayer/API/ModCompatibilityManager.cs @@ -147,17 +147,6 @@ public bool CheckModCompatibility() return true; } - // Returns a list of mods for use in the lobby data - public string GetRequiredMods() - { - var local = GetLocalMods(); - - if (local == null) - return null; - - return Newtonsoft.Json.JsonConvert.SerializeObject(local); - } - // Returns a list of mods installed and enabled, filtered for mods that are required for hosts and clients public ModInfo[] GetLocalMods() { diff --git a/Multiplayer/Components/MainMenu/HostGamePane.cs b/Multiplayer/Components/MainMenu/HostGamePane.cs index 16cd6702..d05f2549 100644 --- a/Multiplayer/Components/MainMenu/HostGamePane.cs +++ b/Multiplayer/Components/MainMenu/HostGamePane.cs @@ -430,7 +430,7 @@ private void OnStartClick() serverData.MaxPlayers = (int)maxPlayers.value; // final check before we start the server - string requiredMods = ModCompatibilityManager.Instance.GetRequiredMods(); + var requiredMods = ModCompatibilityManager.Instance.GetLocalMods(); if (requiredMods == null) { incompatibleMods = true; diff --git a/Multiplayer/Components/MainMenu/ServerBrowser/IServerBrowserGameDetails.cs b/Multiplayer/Components/MainMenu/ServerBrowser/IServerBrowserGameDetails.cs index fb47fc17..ddbba69d 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowser/IServerBrowserGameDetails.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowser/IServerBrowserGameDetails.cs @@ -1,3 +1,4 @@ +using Multiplayer.Networking.Data; using System; namespace Multiplayer.Components.MainMenu; @@ -24,7 +25,7 @@ public interface IServerBrowserGameDetails : IDisposable string TimePassed { get; set; } int CurrentPlayers { get; set; } int MaxPlayers { get; set; } - string RequiredMods { get; set; } + ModInfo[] RequiredMods { get; set; } string GameVersion { get; set; } string MultiplayerVersion { get; set; } string ServerDetails { get; set; } diff --git a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs index ad884cc1..5a629a2e 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs @@ -474,10 +474,9 @@ private void UpdateDetailsPane() var localMods = ModCompatibilityManager.Instance.GetLocalMods(); - Multiplayer.LogDebug(() => $"Temp Mod Json: \"{tempReqMods}\""); - if (!string.IsNullOrEmpty(selectedServer.RequiredMods)) + if (selectedServer.RequiredMods != null && selectedServer.RequiredMods.Length > 0) { - var modData = ModInfo.DeserializeRequiredMods(selectedServer.RequiredMods); + var modData = selectedServer.RequiredMods; // ModInfo.DeserializeRequiredMods(selectedServer.RequiredMods); Multiplayer.LogDebug(() => $"Parsed {modData?.Length} mods from server \"{selectedServer?.Name}\""); @@ -536,7 +535,7 @@ private void UpdateDetailsPane() details.Append("" + LocalizationAPI.L("launcher/in_game_time_passed", []) + " " + selectedServer.TimePassed + "
    "); details.Append("" + Locale.SERVER_BROWSER__PLAYERS + ": " + selectedServer.CurrentPlayers + '/' + selectedServer.MaxPlayers + "
    "); details.Append("" + Locale.SERVER_BROWSER__PASSWORD_REQUIRED + ": " + (selectedServer.HasPassword ? Locale.SERVER_BROWSER__YES : Locale.SERVER_BROWSER__NO) + "
    "); - details.Append("" + Locale.SERVER_BROWSER__MODS_REQUIRED + ": " + (string.IsNullOrEmpty(selectedServer.RequiredMods) ? Locale.SERVER_BROWSER__NO : Locale.SERVER_BROWSER__YES) + "
    "); + details.Append("" + Locale.SERVER_BROWSER__MODS_REQUIRED + ": " + ((selectedServer.RequiredMods != null && selectedServer.RequiredMods.Length > 0) ? Locale.SERVER_BROWSER__NO : Locale.SERVER_BROWSER__YES) + "
    "); details.Append("
    "); details.Append("" + Locale.SERVER_BROWSER__GAME_VERSION + ": " + (selectedServer.GameVersion != MainMenuControllerPatch.MenuProvider.BuildVersionString ? "" : "") + selectedServer.GameVersion + "
    "); details.Append("" + Locale.SERVER_BROWSER__MOD_VERSION + ": " + (selectedServer.MultiplayerVersion != Multiplayer.Ver ? "" : "") + selectedServer.MultiplayerVersion + "
    "); diff --git a/Multiplayer/Networking/Data/LobbyServerData.cs b/Multiplayer/Networking/Data/LobbyServerData.cs index 9f7d8b02..8a1909fc 100644 --- a/Multiplayer/Networking/Data/LobbyServerData.cs +++ b/Multiplayer/Networking/Data/LobbyServerData.cs @@ -47,7 +47,7 @@ public class LobbyServerData : IServerBrowserGameDetails [JsonProperty("required_mods")] - public string RequiredMods { get; set; } + public ModInfo[] RequiredMods { get; set; } [JsonProperty("game_version")] From aeccba0e8dff59d0e8bdf8f6bb3fde355c882531 Mon Sep 17 00:00:00 2001 From: Macka Date: Sun, 4 Jan 2026 12:40:32 +1000 Subject: [PATCH 512/521] Improve lobby data handling for ModInfo --- .../Networking/Data/LobbyServerData.cs | 12 +++---- Multiplayer/Networking/Data/ModInfo.cs | 10 ++++-- Multiplayer/Utils/SteamWorksUtils.cs | 34 ++++++++++++++++--- 3 files changed, 41 insertions(+), 15 deletions(-) diff --git a/Multiplayer/Networking/Data/LobbyServerData.cs b/Multiplayer/Networking/Data/LobbyServerData.cs index 8a1909fc..e06707f8 100644 --- a/Multiplayer/Networking/Data/LobbyServerData.cs +++ b/Multiplayer/Networking/Data/LobbyServerData.cs @@ -8,7 +8,7 @@ public class LobbyServerData : IServerBrowserGameDetails { [JsonProperty("game_server_id")] public string id { get; set; } - + public string ipv4 { get; set; } public string ipv6 { get; set; } public int port { get; set; } @@ -155,21 +155,17 @@ public static string GetGameModeFromInt(int difficulty) public static void Serialize(NetDataWriter writer, LobbyServerData data) { - //Multiplayer.Log($"LobbyServerData.Serialize() {writer != null }, {data != null} "); - - //have we got data? + // Data available flag 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()) + // Check data available flag + if (reader.GetBool()) return new NetSerializer().Deserialize(reader); else return null; diff --git a/Multiplayer/Networking/Data/ModInfo.cs b/Multiplayer/Networking/Data/ModInfo.cs index 63ab110d..7f9e74ad 100644 --- a/Multiplayer/Networking/Data/ModInfo.cs +++ b/Multiplayer/Networking/Data/ModInfo.cs @@ -98,8 +98,14 @@ public static ModInfo[] DeserializeRequiredMods(string json) } catch (Exception e) { - Multiplayer.LogException("Failed to deserialize mod data", e); - return []; + // Try legacy format: comma-separated string of mod names + var modNames = json.Split(',') + .Select(m => m.Trim()) + .Where(m => !string.IsNullOrEmpty(m)) + .Select(m => new ModInfo(m, "Unknown", "")) + .ToArray(); + + return modNames; } } } diff --git a/Multiplayer/Utils/SteamWorksUtils.cs b/Multiplayer/Utils/SteamWorksUtils.cs index 11bc41c3..e4d2115c 100644 --- a/Multiplayer/Utils/SteamWorksUtils.cs +++ b/Multiplayer/Utils/SteamWorksUtils.cs @@ -45,7 +45,7 @@ public static bool GetSteamUser(out string username, out ulong steamId) if (SteamApps.IsAppInstalled(DVSteamworks.APP_ID)) Multiplayer.Log($"Found Steam Name: {username}, steamId {steamId}"); } - catch(Exception ex) + catch (Exception ex) { Multiplayer.LogError($"Failed to obtain Steam user.\r\n{ex.StackTrace}"); } @@ -59,6 +59,19 @@ public static void SetLobbyData(Lobby lobby, LobbyServerData data, string[] excl foreach (var prop in properties) { var value = prop.GetValue(data)?.ToString() ?? ""; + if (prop.Name == nameof(LobbyServerData.RequiredMods)) + { + try + { + value = Newtonsoft.Json.JsonConvert.SerializeObject((ModInfo[])prop.GetValue(data)); + } + catch (Exception ex) + { + Multiplayer.LogException($"SetLobbyData() Error serializing RequiredMods property", ex); + } + + Multiplayer.LogDebug(() => $"SetLobbyData() Setting property: {prop.Name}, value: {value}"); + } lobby.SetData(prop.Name, value); } } @@ -76,6 +89,17 @@ public static LobbyServerData GetLobbyData(this Lobby lobby) value = lobby.GetData(prop.Name); if (string.IsNullOrEmpty(value)) continue; + Multiplayer.LogDebug(() => $"GetLobbyData() Retrieving property: {prop.Name}, value: {value}"); + + // Backward compatibility for non-JSON strings + if (prop.Name == nameof(LobbyServerData.RequiredMods)) + { + var mods = ModInfo.DeserializeRequiredMods(value); + + prop.SetValue(data, mods); + continue; + } + if (prop.PropertyType.IsEnum) { var enumValue = Enum.Parse(prop.PropertyType, value); @@ -121,7 +145,7 @@ public static IEnumerator JoinFromCommandLine() hasJoinedCL = true; //allow steamworks to initialise - yield return new WaitUntil(()=>{ return DVSteamworks.Success || (Time.deltaTime - time) > 5; }); + yield return new WaitUntil(() => { return DVSteamworks.Success || (Time.deltaTime - time) > 5; }); if (!DVSteamworks.Success) yield break; @@ -184,10 +208,10 @@ public static void OnLobbyInviteRequest(Friend friend, Lobby lobby) popup.Closed += (PopupResult result) => { - Multiplayer.LogDebug(()=>$"Agreed to join: {result.closedBy}"); + Multiplayer.LogDebug(() => $"Agreed to join: {result.closedBy}"); if (result.closedBy == PopupClosedByAction.Positive) - QueueLobbyInvite(lobby); - }; + QueueLobbyInvite(lobby); + }; }); From 0911be5aecd9e32aca228a86a685c8c669d61da6 Mon Sep 17 00:00:00 2001 From: Macka Date: Sun, 4 Jan 2026 12:45:17 +1000 Subject: [PATCH 513/521] Change handling of incompatible mods in compatibility manager Previously, incompatible mods were ignored by returning null. Now, they are added to the localMods list. --- Multiplayer/API/ModCompatibilityManager.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Multiplayer/API/ModCompatibilityManager.cs b/Multiplayer/API/ModCompatibilityManager.cs index e595707e..ce14ed01 100644 --- a/Multiplayer/API/ModCompatibilityManager.cs +++ b/Multiplayer/API/ModCompatibilityManager.cs @@ -167,7 +167,8 @@ public ModInfo[] GetLocalMods() case MultiplayerCompatibility.Incompatible: //There shouldn't be any at this stage - return null; + localMods.Add(modInfo); + break; case MultiplayerCompatibility.Host: case MultiplayerCompatibility.Client: From 0596db911a9921938be18d13c0835b4bd0b5a2c8 Mon Sep 17 00:00:00 2001 From: Macka Date: Sun, 4 Jan 2026 12:44:50 +1000 Subject: [PATCH 514/521] Publicise `DV.UIFramework.CollapsibleElement` Updated the project file to publicise DV.UIFramework.CollapsibleElement. Changed the Awake method from protected to public override in ServerBrowserElement and ServerBrowserPlaceholderElement due to new publicised protection levels. --- .../MainMenu/ServerBrowser/ServerBrowserElement.cs | 2 +- .../ServerBrowser/ServerBrowserPlaceholderElement.cs | 2 +- Multiplayer/Multiplayer.csproj | 5 +++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserElement.cs b/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserElement.cs index 18476b38..8cbef966 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserElement.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserElement.cs @@ -34,7 +34,7 @@ public class ServerBrowserElement : MPViewElement private const int PING_THRESHOLD_GOOD = 100; private const int PING_THRESHOLD_HIGH = 150; - protected override void Awake() + public override void Awake() { // Find and assign TextMeshProUGUI components for displaying server details serverName = this.FindChildByName("name [noloc]").GetComponent(); diff --git a/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserPlaceholderElement.cs b/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserPlaceholderElement.cs index 7e8e872f..1b58419c 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserPlaceholderElement.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserPlaceholderElement.cs @@ -11,7 +11,7 @@ public class ServerBrowserPlaceholderElement : MPViewElement true; - protected override void Awake() + public override void Awake() { // Find and assign TextMeshProUGUI components for displaying server details GameObject networkNameGO = this.FindChildByName("name [noloc]"); diff --git a/Multiplayer/Multiplayer.csproj b/Multiplayer/Multiplayer.csproj index a4aef290..62b20deb 100644 --- a/Multiplayer/Multiplayer.csproj +++ b/Multiplayer/Multiplayer.csproj @@ -1,4 +1,4 @@ - + net48 latest @@ -22,6 +22,7 @@ +
    @@ -121,4 +122,4 @@ - \ No newline at end of file + From ba46fafd44320d8368a2b6c996dd414c51358759 Mon Sep 17 00:00:00 2001 From: Macka Date: Sun, 4 Jan 2026 12:54:14 +1000 Subject: [PATCH 515/521] Use collapsible elements for mod details Server browser now displays required and extra mods with compatibility status, including localised labels for OK, mismatch, missing, incompatible, and extra mods. UI elements for mod lists are collapsible and support hyperlinks. Locale keys and translations for new mod status labels were added. Refactored MainMenuControllerPatch to expose MainMenuControllerInstance for easier access. --- .../Components/MainMenu/ServerBrowserPane.cs | 301 ++++++++++++++---- .../Components/Util/HyperlinkHandler.cs | 39 ++- Multiplayer/Locale.cs | 22 +- .../MainMenu/MainMenuControllerPatch.cs | 7 +- locale.csv | 9 +- 5 files changed, 300 insertions(+), 78 deletions(-) diff --git a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs index 5a629a2e..6d205ae8 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs @@ -1,6 +1,7 @@ using DV.Localization; using DV.Platform.Steam; using DV.UI; +using DV.UI.Manual; using DV.UIFramework; using DV.Utils; using LiteNetLib; @@ -25,11 +26,15 @@ using TMPro; using UnityEngine; using UnityEngine.UI; +using Color = UnityEngine.Color; namespace Multiplayer.Components.MainMenu { + public class ServerBrowserPane : MonoBehaviour { + private const string FORMAT_ALPHA = ""; + private enum ConnectionState { NotConnected, @@ -49,25 +54,29 @@ private enum ConnectionState private const int MIN_PORT = 1024; private const int MAX_PORT = 49151; - //Gridview variables + // Gridview variables private ServerBrowserGridView serverGridView; private IServerBrowserGameDetails selectedServer; - //ping tracking + // Ping tracking private float pingTimer = 0f; private const float PING_INTERVAL = 2f; // base interval to refresh all pings - //Button variables + // Button variables private ButtonDV buttonJoin; private ButtonDV buttonRefresh; private ButtonDV buttonDirectIP; - //Misc GUI Elements + // Misc GUI Elements private TextMeshProUGUI serverName; private TextMeshProUGUI detailsPane; - private HyperlinkHandler modHyperlinkHandler; + private GameObject navigationButtonPrefab; + private Transform modsContainer; + private CollapsibleElement elementRequiredMods; + private CollapsibleElement elementExtraMods; - //Remote server tracking + + // Remote server tracking private readonly List remoteServers = []; private bool serverRefreshing = false; private float timePassed = 0f; //time since last refresh @@ -75,7 +84,7 @@ private enum ConnectionState private const int REFRESH_MIN_TIME = 10; //Stop refresh spam private bool remoteRefreshComplete; - //connection parameters + // Connection parameters private string address; private int portNumber; private Lobby? selectedLobby; @@ -293,6 +302,8 @@ private void BuildUI() detailsPane.fontSize = 18; 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."; + SetupModsGroup(content); + // Adjust text RectTransform to fit content RectTransform textRT = textGO.GetComponent(); textRT.pivot = new Vector2(0.5f, 1f); @@ -304,11 +315,6 @@ private void BuildUI() // Set content size to fit text contentRT.sizeDelta = new Vector2(contentRT.sizeDelta.x - 50, detailsPane.preferredHeight); - // Enable hyperlink parsing - modHyperlinkHandler = textGO.GetOrAddComponent(); - - modHyperlinkHandler.linkColor = new UnityEngine.Color(0.302f, 0.651f, 1f); // #4DA6FF - modHyperlinkHandler.linkHoverColor = new UnityEngine.Color(0.498f, 0.749f, 1f); // #7FBFFF // 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); @@ -335,6 +341,7 @@ private void BuildUI() //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"); @@ -359,15 +366,102 @@ private void SetupServerBrowser() GridviewGO.SetActive(true); serverGridView.Clear(); } + + private void SetupModsGroup(GameObject content) + { + ManualController manualController = MainMenuControllerPatch.MainMenuControllerInstance.GetComponentInChildren(true); + if (manualController == null) + { + Multiplayer.LogError("SetupModsGroup() ManualController not found"); + return; + } + + GameObject navigationPrefab = manualController.gameObject.FindChildByName("Navigation"); + GameObject navigationGroup = Instantiate(navigationPrefab, content.transform); + navigationGroup.name = "Mods Container"; + modsContainer = navigationGroup.transform; + + foreach (GameObject child in navigationGroup.GetChildren()) + GameObject.Destroy(child); + + navigationButtonPrefab = manualController.navigationButtonPrefab; + + elementRequiredMods = CreateModElement($"{FORMAT_ALPHA}{Locale.SERVER_BROWSER__REQUIRED_MODS}"); + elementRequiredMods.name = "Required Mods"; + elementRequiredMods.Collapse(true); + + elementExtraMods = CreateModElement($"{FORMAT_ALPHA}{Locale.SERVER_BROWSER__EXTRA_MODS}"); + elementExtraMods.name = "Extra Mods"; + elementExtraMods.Collapse(true); + } + + private CollapsibleElement CreateModElement(string label, CollapsibleElement parent = null) + { + // Container for required mods + RectTransform rt = Instantiate(navigationButtonPrefab, modsContainer).GetComponent(); + + CollapsibleElement element = rt.GetComponent(); + CollapsibleElementVisualController controller = rt.GetComponent(); + + controller.categoryTextColor = Color.white; + controller.articleTextColor = Color.white; + controller.collapseIndicatorImage.color = new(1f, 1f, 1f, 0x50 / 255f); + + element.SetText(label); + + if (parent != null) + { + var last = parent.childElements.LastOrDefault() ?? parent; + element.transform.SetSiblingIndex(last.transform.GetSiblingIndex() + 1); + parent.AddChild(element); + + // Remove the Button to allow the hyperlink handler to work + Component.Destroy(element.GetComponentInChildren(true)); + + //// Enable hyperlink parsing + HyperlinkHandler modHyperlinkHandler = controller.elementText.GetOrAddComponent(); + modHyperlinkHandler.linkColor = new UnityEngine.Color(0.302f, 0.651f, 1f); // #4DA6FF + modHyperlinkHandler.linkHoverColor = new UnityEngine.Color(0.498f, 0.749f, 1f); // #7FBFFF + modHyperlinkHandler.ApplyLinkStyling(); + } + + return element; + } + + private void ClearModElements(CollapsibleElement parent) + { + if (parent == null) + return; + + parent.Collapse(true); + + if (parent.childElements.Count == 0) + return; + + foreach (var element in parent.childElements) + GameObject.Destroy(element.gameObject); + + parent.childElements.Clear(); + } + + private void CollapsibleElementClicked(CollapsibleElement element) + { + element.Toggle(); + } + private void SetupListeners(bool on) { if (on) { serverGridView.SelectedIndexChanged += this.OnSelectedIndexChanged; + elementRequiredMods.CollapsibleElementClicked += CollapsibleElementClicked; + elementExtraMods.CollapsibleElementClicked += CollapsibleElementClicked; } else { serverGridView.SelectedIndexChanged -= this.OnSelectedIndexChanged; + elementRequiredMods.CollapsibleElementClicked -= CollapsibleElementClicked; + elementExtraMods.CollapsibleElementClicked -= CollapsibleElementClicked; } } #endregion @@ -462,7 +556,6 @@ private void UpdateElement(IServerBrowserGameDetails element) { var viewElement = serverGridView.GetElementAt(index) as ServerBrowserElement; viewElement?.UpdateView(); - } } #endregion @@ -470,17 +563,69 @@ private void UpdateElement(IServerBrowserGameDetails element) private void UpdateDetailsPane() { StringBuilder details = new(); - StringBuilder modDetails = new("Mods:"); - var localMods = ModCompatibilityManager.Instance.GetLocalMods(); + if (selectedServer != null) + { + serverName.text = selectedServer.Name; - if (selectedServer.RequiredMods != null && selectedServer.RequiredMods.Length > 0) + // Note: built-in localisations have a trailing colon e.g. 'Game mode:' + + details.Append(FORMAT_ALPHA + LocalizationAPI.L("launcher/game_mode", []) + " " + LobbyServerData.GetGameModeFromInt(selectedServer.GameMode) + "
    "); + details.Append(FORMAT_ALPHA + LocalizationAPI.L("launcher/difficulty", []) + " " + LobbyServerData.GetDifficultyFromInt(selectedServer.Difficulty) + "
    "); + details.Append(FORMAT_ALPHA + LocalizationAPI.L("launcher/in_game_time_passed", []) + " " + selectedServer.TimePassed + "
    "); + details.Append(FORMAT_ALPHA + Locale.SERVER_BROWSER__PLAYERS + ": " + selectedServer.CurrentPlayers + '/' + selectedServer.MaxPlayers + "
    "); + details.Append(FORMAT_ALPHA + Locale.SERVER_BROWSER__PASSWORD_REQUIRED + ": " + (selectedServer.HasPassword ? Locale.SERVER_BROWSER__YES : Locale.SERVER_BROWSER__NO) + "
    "); + details.Append(FORMAT_ALPHA + Locale.SERVER_BROWSER__GAME_VERSION + ": " + (selectedServer.GameVersion != MainMenuControllerPatch.MenuProvider.BuildVersionString ? "" : "") + selectedServer.GameVersion + "
    "); + details.Append(selectedServer.ServerDetails); + + if (selectedServer.ServerDetails != null && selectedServer.ServerDetails.Length > 0) + details.Append("
    "); + + detailsPane.text = details.ToString(); + + // Build mod lists + ClearModElements(elementRequiredMods); + ClearModElements(elementExtraMods); + + var localMods = ModCompatibilityManager.Instance.GetLocalMods(); + + BuildServerMods(selectedServer.RequiredMods, localMods); + BuildLocalMods(localMods); + } + else + { + serverName.text = Locale.SERVER_BROWSER__INFO_TITLE;// "Server Browser Info"; + 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."; + + ClearModElements(elementRequiredMods); + ClearModElements(elementExtraMods); + } + + } + + /// + /// Validates the client has all required mods for the server and the versions match. + /// Populates the mod details list. + /// + /// + /// + /// + /// true if all required mods are present and have correct versions, false if any mods are missing or there is a version mismatch. + private bool BuildServerMods(ModInfo[] serverMods, ModInfo[] localMods) + { + bool modsOk = true; + + if (serverMods == null || localMods == null) { - var modData = selectedServer.RequiredMods; // ModInfo.DeserializeRequiredMods(selectedServer.RequiredMods); + Multiplayer.LogWarning("BuildServerMods() called with null serverMods or localMods"); + return false; + } - Multiplayer.LogDebug(() => $"Parsed {modData?.Length} mods from server \"{selectedServer?.Name}\""); + if (selectedServer.RequiredMods != null && selectedServer.RequiredMods.Length > 0) + { + Multiplayer.LogDebug(() => $"Parsed {serverMods?.Length} mods from server \"{selectedServer?.Name}\""); - foreach (var mod in modData) + foreach (var mod in serverMods) { ModInfo modMatch = localMods.FirstOrDefault(l => l.Id == mod.Id); @@ -489,69 +634,100 @@ private void UpdateDetailsPane() bool modFound = modMatch.Id == mod.Id; bool modVersionMatch = modFound && modMatch.Version == mod.Version; + modsOk &= modVersionMatch; + string status; if (modFound && modVersionMatch) - status = "OK"; + status = $"{Locale.SERVER_BROWSER__OK}"; else if (modFound && !modVersionMatch) - status = "Version Mismatch"; + status = $"{Locale.SERVER_BROWSER__MISMATCH}"; else - status = "Missing"; + status = $"{Locale.SERVER_BROWSER__MISSING}"; var link = !string.IsNullOrEmpty(mod.Url) ? $"{mod.Id}" : mod.Id; - modDetails.Append($"
    {link} ({mod.Version}) - {status}"); + var element = CreateModElement(mod.Id, elementRequiredMods); + element.isLeaf = true; + element.SetText($"{link} ({mod.Version}) - {status}"); } - Multiplayer.LogDebug(() => $"Mod details for server \"{selectedServer.Name}\":\r\n{modDetails.ToString()}"); - var extraMods = localMods.Where(l => !modData.Any(m => m.Id == l.Id)).ToArray(); - Multiplayer.LogDebug(() => $"Found {extraMods.Length} extra mods on client for server \"{selectedServer.Name}\""); + elementRequiredMods.Expand(false); - if (extraMods.Length > 0) + if (modsOk) { - foreach (var mod in extraMods) - { - var compatibility = ModCompatibilityManager.Instance.GetCompatibility(mod); - if (compatibility == MultiplayerCompatibility.Incompatible) - { - modDetails.Append($"
    {mod.Id} ({mod.Version}) - Incompatible"); - } - else if (compatibility == MultiplayerCompatibility.Undefined || compatibility == MultiplayerCompatibility.All) - { - modDetails.Append($"
    {mod.Id} ({mod.Version}) - Extra Mod"); - } - } + elementRequiredMods.Collapse(false); + elementExtraMods.SetText($"{Locale.SERVER_BROWSER__REQUIRED_MODS}: {Locale.SERVER_BROWSER__OK}"); + } + else + { + elementRequiredMods.SetText($"{Locale.SERVER_BROWSER__REQUIRED_MODS}"); } } - Multiplayer.LogDebug(() => $"Finished compiling mod details for server \"{selectedServer.Name}\""); - Multiplayer.LogDebug(() => $"Version: {MainMenuControllerPatch.MenuProvider.BuildVersionString}"); - if (selectedServer != null) + + return modsOk; + } + + /// + /// Validates the client does not have any mods that the server is not running and does not have any mods incompatible with Multiplayer. + /// Populates the mod details list. + /// + /// + /// + /// + /// true if there are no conflicting mods, false if any mods can not be used with this server. + private bool BuildLocalMods(ModInfo[] localMods) + { + bool modsOk = true; + + if (localMods == null || selectedServer?.RequiredMods == null) { - serverName.text = selectedServer.Name; + Multiplayer.LogWarning($"BuildLocalMods() localMods is null: {localMods == null}, requiredMods is null: {selectedServer?.RequiredMods == null}"); + return false; + } - //note: built-in localisations have a trailing colon e.g. 'Game mode:' - - details.Append("" + LocalizationAPI.L("launcher/game_mode", []) + " " + LobbyServerData.GetGameModeFromInt(selectedServer.GameMode) + "
    "); - details.Append("" + LocalizationAPI.L("launcher/difficulty", []) + " " + LobbyServerData.GetDifficultyFromInt(selectedServer.Difficulty) + "
    "); - details.Append("" + LocalizationAPI.L("launcher/in_game_time_passed", []) + " " + selectedServer.TimePassed + "
    "); - details.Append("" + Locale.SERVER_BROWSER__PLAYERS + ": " + selectedServer.CurrentPlayers + '/' + selectedServer.MaxPlayers + "
    "); - details.Append("" + Locale.SERVER_BROWSER__PASSWORD_REQUIRED + ": " + (selectedServer.HasPassword ? Locale.SERVER_BROWSER__YES : Locale.SERVER_BROWSER__NO) + "
    "); - details.Append("" + Locale.SERVER_BROWSER__MODS_REQUIRED + ": " + ((selectedServer.RequiredMods != null && selectedServer.RequiredMods.Length > 0) ? Locale.SERVER_BROWSER__NO : Locale.SERVER_BROWSER__YES) + "
    "); - details.Append("
    "); - details.Append("" + Locale.SERVER_BROWSER__GAME_VERSION + ": " + (selectedServer.GameVersion != MainMenuControllerPatch.MenuProvider.BuildVersionString ? "" : "") + selectedServer.GameVersion + "
    "); - details.Append("" + Locale.SERVER_BROWSER__MOD_VERSION + ": " + (selectedServer.MultiplayerVersion != Multiplayer.Ver ? "" : "") + selectedServer.MultiplayerVersion + "
    "); - details.Append("
    "); - details.Append(selectedServer.ServerDetails); + var extraMods = localMods.Where(l => !selectedServer.RequiredMods.Any(m => m.Id == l.Id)).ToArray(); + Multiplayer.LogDebug(() => $"Found {extraMods.Length} extra mods on client for server \"{selectedServer.Name}\""); - if (selectedServer.ServerDetails != null && selectedServer.ServerDetails.Length > 0) - details.Append("
    "); + if (extraMods.Length > 0) + { + string status; + foreach (var mod in extraMods) + { + var compatibility = ModCompatibilityManager.Instance.GetCompatibility(mod); + if (compatibility == MultiplayerCompatibility.Incompatible) + { + status = $"{Locale.SERVER_BROWSER__INCOMPATIBLE}"; + modsOk = false; + } + else if (compatibility == MultiplayerCompatibility.Undefined || compatibility == MultiplayerCompatibility.All) + { + status = $"{Locale.SERVER_BROWSER__EXTRA_MOD}"; + modsOk = false; + } + else + { + status = $"{Locale.SERVER_BROWSER__OK}"; + } - details.Append(modDetails.ToString()); + var element = CreateModElement(mod.Id, elementExtraMods); + element.isLeaf = true; + element.SetText($"{mod.Id} ({mod.Version}) - {status}"); + } - //Multiplayer.Log("Finished Prepping Data"); - detailsPane.text = details.ToString(); + elementExtraMods.Expand(false); - modHyperlinkHandler.ApplyLinkStyling(); + if (modsOk) + { + elementExtraMods.Collapse(false); + elementExtraMods.SetText($"{Locale.SERVER_BROWSER__EXTRA_MODS}: {Locale.SERVER_BROWSER__OK}"); + } + else + { + elementExtraMods.SetText($"{Locale.SERVER_BROWSER__EXTRA_MODS}"); + } } + + return modsOk; } private void ShowIpPopup() @@ -1002,6 +1178,7 @@ private void UpdatePingsSteam() return null; } #endregion + private void RefreshGridView() { // Get all active IDs diff --git a/Multiplayer/Components/Util/HyperlinkHandler.cs b/Multiplayer/Components/Util/HyperlinkHandler.cs index 22f590d6..e8d8a149 100644 --- a/Multiplayer/Components/Util/HyperlinkHandler.cs +++ b/Multiplayer/Components/Util/HyperlinkHandler.cs @@ -1,7 +1,7 @@ -using UnityEngine; +using System.Text.RegularExpressions; using TMPro; +using UnityEngine; using UnityEngine.EventSystems; -using System.Text.RegularExpressions; namespace Multiplayer.Components.Util { @@ -20,24 +20,41 @@ public class HyperlinkHandler : MonoBehaviour, IPointerClickHandler private int hoveredLinkIndex = -1; private bool underlineLinks = true; + protected void Awake() + { + InitializeComponents(); + } + protected void Start() + { + InitializeComponents(); + ApplyLinkStyling(); + } + + private void InitializeComponents() { if (textComponent == null) { textComponent = GetComponent(); + + textComponent.raycastTarget = true; } - canvas = GetComponentInParent(); - if (canvas != null) + if (canvas == null) { - canvasCamera = canvas.renderMode == RenderMode.ScreenSpaceOverlay ? null : canvas.worldCamera; + canvas = GetComponentInParent(); + if (canvas != null) + { + canvasCamera = canvas.renderMode == RenderMode.ScreenSpaceOverlay ? null : canvas.worldCamera; + } } - - ApplyLinkStyling(); } protected void Update() { + if (textComponent == null || canvas == null) + return; + int linkIndex = TMP_TextUtilities.FindIntersectingLink(textComponent, Input.mousePosition, canvasCamera); if (linkIndex != -1 && linkIndex != hoveredLinkIndex) @@ -61,18 +78,26 @@ protected void Update() public void OnPointerClick(PointerEventData eventData) { + if (textComponent == null) + return; + + Multiplayer.LogDebug(() => $"HyperlinkHandler.OnPointerClick() mouse pos: {Input.mousePosition}"); int linkIndex = TMP_TextUtilities.FindIntersectingLink(textComponent, Input.mousePosition, canvasCamera); if (linkIndex != -1) { TMP_LinkInfo linkInfo = textComponent.textInfo.linkInfo[linkIndex]; string url = linkInfo.GetLinkID(); + Multiplayer.LogDebug(() => $"HyperlinkHandler: Opening URL: {url}"); Application.OpenURL(url); } } public void ApplyLinkStyling() { + if (textComponent == null) + return; + string text = textComponent.text; string pattern = @"(.*?)<\/link>"; string replacement = underlineLinks diff --git a/Multiplayer/Locale.cs b/Multiplayer/Locale.cs index fc3345d0..d8294bdf 100644 --- a/Multiplayer/Locale.cs +++ b/Multiplayer/Locale.cs @@ -49,9 +49,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__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 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); @@ -80,6 +80,20 @@ 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__OK => Get(SERVER_BROWSER__OK_KEY); + private const string SERVER_BROWSER__OK_KEY = $"{PREFIX_SERVER_BROWSER}/ok"; + public static string SERVER_BROWSER__MISMATCH => Get(SERVER_BROWSER__MISMATCH_KEY); + private const string SERVER_BROWSER__MISMATCH_KEY = $"{PREFIX_SERVER_BROWSER}/mismatch"; + public static string SERVER_BROWSER__MISSING => Get(SERVER_BROWSER__MISSING_KEY); + private const string SERVER_BROWSER__MISSING_KEY = $"{PREFIX_SERVER_BROWSER}/missing"; + public static string SERVER_BROWSER__REQUIRED_MODS => Get(SERVER_BROWSER__REQUIRED_MODS_KEY); + private const string SERVER_BROWSER__REQUIRED_MODS_KEY = $"{PREFIX_SERVER_BROWSER}/required_mods"; + public static string SERVER_BROWSER__EXTRA_MODS => Get(SERVER_BROWSER__EXTRA_MODS_KEY); + private const string SERVER_BROWSER__EXTRA_MODS_KEY = $"{PREFIX_SERVER_BROWSER}/extra_mods"; + public static string SERVER_BROWSER__INCOMPATIBLE => Get(SERVER_BROWSER__INCOMPATIBLE_KEY); + private const string SERVER_BROWSER__INCOMPATIBLE_KEY = $"{PREFIX_SERVER_BROWSER}/incompatible"; + public static string SERVER_BROWSER__EXTRA_MOD => Get(SERVER_BROWSER__EXTRA_MOD_KEY); + private const string SERVER_BROWSER__EXTRA_MOD_KEY = $"{PREFIX_SERVER_BROWSER}/extra_mod"; 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); @@ -99,8 +113,8 @@ public static class Locale public const string SERVER_HOST_PUBLIC_KEY = $"{PREFIX_SERVER_HOST}/public"; public static string SERVER_HOST_VISIBILITY => Get(SERVER_HOST_PUBLIC_KEY); public const string SERVER_HOST_VISIBILITY_KEY = $"{PREFIX_SERVER_HOST}/visibility"; - // public static string SERVER_HOST_VISIBILITY_MODES => Get(SERVER_HOST_VISIBILITY_MODES_KEY); - public static string[] SERVER_HOST_VISIBILITY_MODES = [$"{SERVER_HOST_VISIBILITY_MODES_KEY}/private" , $"{SERVER_HOST_VISIBILITY_MODES_KEY}/friends",$"{SERVER_HOST_VISIBILITY_MODES_KEY}/public"]; + + public static string[] SERVER_HOST_VISIBILITY_MODES = [$"{SERVER_HOST_VISIBILITY_MODES_KEY}/private", $"{SERVER_HOST_VISIBILITY_MODES_KEY}/friends", $"{SERVER_HOST_VISIBILITY_MODES_KEY}/public"]; public const string SERVER_HOST_VISIBILITY_MODES_KEY = $"{PREFIX_SERVER_HOST}/visibility/modes"; public static string SERVER_HOST_DETAILS => Get(SERVER_HOST_DETAILS_KEY); public const string SERVER_HOST_DETAILS_KEY = $"{PREFIX_SERVER_HOST}/details"; diff --git a/Multiplayer/Patches/MainMenu/MainMenuControllerPatch.cs b/Multiplayer/Patches/MainMenu/MainMenuControllerPatch.cs index 28cc7fc9..149a5611 100644 --- a/Multiplayer/Patches/MainMenu/MainMenuControllerPatch.cs +++ b/Multiplayer/Patches/MainMenu/MainMenuControllerPatch.cs @@ -13,10 +13,9 @@ namespace Multiplayer.Patches.MainMenu; [HarmonyPatch(typeof(MainMenuController))] public static class MainMenuControllerPatch { - public static AMainMenuProvider MenuProvider => _mainMenuControllerInstance.provider; + public static AMainMenuProvider MenuProvider => MainMenuControllerInstance.provider; public static GameObject MultiplayerButton { get; private set; } - - private static MainMenuController _mainMenuControllerInstance; + public static MainMenuController MainMenuControllerInstance { get; private set; } /// /// Prefix method to run before MainMenuController's Awake method. @@ -26,7 +25,7 @@ public static class MainMenuControllerPatch [HarmonyPrefix] private static void Awake(MainMenuController __instance) { - _mainMenuControllerInstance = __instance; + MainMenuControllerInstance = __instance; // Find the Sessions button to base the Multiplayer button on GameObject sessionsButton = __instance.FindChildByName("ButtonSelectable Sessions"); diff --git a/locale.csv b/locale.csv index 51f37603..21cc2535 100644 --- a/locale.csv +++ b/locale.csv @@ -35,11 +35,18 @@ sb/port_invalid,Invalid port popup.,Invalid Port!,Невалиден порт!, 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,Потрібні модифікації +sb/mods_required,Mods status label,Mods,Модове,模组,模組,Módy,Mods,Mods,Modit,Mods,Mods,मॉड,Modok,Mod,モッド,모드,Mods,Mody,Mods,Mods,Moduri,Модификации,Módy,Mods,Moddar,Modlar,Модифікації 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/ok,'OK' for details text,OK,Добре,是,是,Ano,Ja,Ja,Kyllä,Oui,Ja,हाँ,Igen,Sì,はい,예,Ja,Tak,Sim,Sim,Da,Да,Áno,Sí,Ja,Evet,Так +sb/mismatch,'mismatch' for details text,Mismatch,Несъответствие,不匹配,不符,Nesoulad,Uoverensstemmelse,Niet-overeenkomend,Ristiriita,Non-concordance,Diskrepanz,बेमेल,Eltérés,Non corrispondenza,不一致,불일치,Avvik,Niezgodność,Incompatibilidade,Incompatibilidade,Nepotrivire,Несоответствие,Nesúlad,Discordancia,Oöverensstämmelse,Uyumsuzluk,Невідповідність +sb/missing,'Missing' for mod status,Missing,Липсва,缺少,缺少,Chybí,Mangler,Ontbreekt,Puuttuu,Manquant,Fehlt,गायब,Hiányzik,Mancante,欠落,누락,Mangler,Brak,Ausente,Ausente,Lipsă,Отсутствует,Chýba,Falta,Saknas,Eksik,Відсутній +sb/required_mods,'Required Mods' header,Required Mods,Необходими модификации,必需模组,必需模組,Požadované módy,Krævede mods,Vereiste mods,Vaaditut modit,Mods requis,Erforderliche Mods,आवश्यक मॉड,Szükséges modok,Mod richiesti,必須MOD,필수 모드,Nødvendige modifikasjoner,Wymagane mody,Mods necessários,Mods necessários,Moduri necesare,Требуемые моды,Požadované módy,Mods requeridos,Nödvändiga moddar,Gerekli modlar,Необхідні моди +sb/extra_mods,'Extra Mods' header,Extra Mods,Допълнителни модификации,额外模组,額外模組,Extra módy,Ekstra mods,Extra mods,Ylimääräiset modit,Mods supplémentaires,Zusätzliche Mods,अतिरिक्त मॉड,Extra modok,Mod extra,追加MOD,추가 모드,Ekstra modifikasjoner,Dodatkowe mody,Mods extras,Mods extras,Moduri suplimentare,Дополнительные моды,Extra módy,Mods adicionales,Extra moddar,Ekstra modlar,Додаткові моди +sb/incompatible,'Incompatible' for mod status,Incompatible,Несъвместим,不兼容,不相容,Nekompatibilní,Inkompatibel,Incompatibel,Yhteensopimaton,Incompatible,Inkompatibel,असंगत,Nem kompatibilis,Incompatibile,非互換,호환 불가,Inkompatibel,Niekompatybilny,Incompatível,Incompatível,Incompatibil,Несовместимо,Nekompatibilné,Incompatible,Inkompatibel,Uyumsuz,Несумісний +sb/extra_mod,'Extra Mod' status message,Extra Mod,Допълнителна модификация,额外模组,額外模組,Extra mód,Ekstra mod,Extra mod,Ylimääräinen modi,Mod supplémentaire,Zusätzlicher Mod,अतिरिक्त मॉड,Extra mod,Mod extra,追加MOD,추가 모드,Ekstra modifikasjon,Dodatkowy mod,Mod extra,Mod extra,Mod suplimentar,Дополнительный мод,Extra mód,Mod adicional,Extra modd,Ekstra mod,Додатковий мод 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!,Сервери не знайдено. Оновіть або почніть власний! From e5775ed55497c09954a3b2138d87ba1a8fc2de0b Mon Sep 17 00:00:00 2001 From: Macka Date: Sun, 4 Jan 2026 21:41:26 +1000 Subject: [PATCH 516/521] Refactor server details and mods UI in ServerBrowserPane Replaces modsContainer with detailsContent for mod elements, updates how mod groups are set up, and improves formatting for server details and mod labels. --- .../Components/MainMenu/ServerBrowserPane.cs | 40 ++++++------------- 1 file changed, 13 insertions(+), 27 deletions(-) diff --git a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs index 6d205ae8..49d1c0b0 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs @@ -71,11 +71,10 @@ private enum ConnectionState private TextMeshProUGUI serverName; private TextMeshProUGUI detailsPane; private GameObject navigationButtonPrefab; - private Transform modsContainer; + private Transform detailsContent; private CollapsibleElement elementRequiredMods; private CollapsibleElement elementExtraMods; - // Remote server tracking private readonly List remoteServers = []; private bool serverRefreshing = false; @@ -274,7 +273,8 @@ private void BuildUI() // Create Content GameObject.Destroy(serverScroll.FindChildByName("GRID VIEW").gameObject); GameObject content = new("Content", typeof(RectTransform), typeof(ContentSizeFitter), typeof(VerticalLayoutGroup)); - content.transform.SetParent(viewport.transform, false); + detailsContent = content.transform; + detailsContent.SetParent(viewport.transform, false); ContentSizeFitter contentSF = content.GetComponent(); contentSF.verticalFit = ContentSizeFitter.FitMode.PreferredSize; VerticalLayoutGroup contentVLG = content.GetComponent(); @@ -287,22 +287,17 @@ private void BuildUI() contentRT.offsetMin = Vector2.zero; contentRT.offsetMax = Vector2.zero; scrollRect.content = contentRT; - - // Create TextMeshProUGUI object - 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); - + // Create TextMeshProUGUI object GameObject textGO = new("Details Text", typeof(TextMeshProUGUI)); - textGO.transform.SetParent(textContainerGO.transform, false); - HorizontalLayoutGroup textHLG = textGO.GetComponent(); + textGO.transform.SetParent(contentRT.transform, false); detailsPane = textGO.GetComponent(); detailsPane.textWrappingMode = TextWrappingModes.Normal; detailsPane.fontSize = 18; 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."; - SetupModsGroup(content); + SetupModsGroup(); // Adjust text RectTransform to fit content RectTransform textRT = textGO.GetComponent(); @@ -315,7 +310,6 @@ private void BuildUI() // 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); @@ -367,7 +361,7 @@ private void SetupServerBrowser() serverGridView.Clear(); } - private void SetupModsGroup(GameObject content) + private void SetupModsGroup() { ManualController manualController = MainMenuControllerPatch.MainMenuControllerInstance.GetComponentInChildren(true); if (manualController == null) @@ -376,14 +370,6 @@ private void SetupModsGroup(GameObject content) return; } - GameObject navigationPrefab = manualController.gameObject.FindChildByName("Navigation"); - GameObject navigationGroup = Instantiate(navigationPrefab, content.transform); - navigationGroup.name = "Mods Container"; - modsContainer = navigationGroup.transform; - - foreach (GameObject child in navigationGroup.GetChildren()) - GameObject.Destroy(child); - navigationButtonPrefab = manualController.navigationButtonPrefab; elementRequiredMods = CreateModElement($"{FORMAT_ALPHA}{Locale.SERVER_BROWSER__REQUIRED_MODS}"); @@ -398,7 +384,7 @@ private void SetupModsGroup(GameObject content) private CollapsibleElement CreateModElement(string label, CollapsibleElement parent = null) { // Container for required mods - RectTransform rt = Instantiate(navigationButtonPrefab, modsContainer).GetComponent(); + RectTransform rt = Instantiate(navigationButtonPrefab, detailsContent).GetComponent(); CollapsibleElement element = rt.GetComponent(); CollapsibleElementVisualController controller = rt.GetComponent(); @@ -576,7 +562,7 @@ private void UpdateDetailsPane() details.Append(FORMAT_ALPHA + Locale.SERVER_BROWSER__PLAYERS + ": " + selectedServer.CurrentPlayers + '/' + selectedServer.MaxPlayers + "
    "); details.Append(FORMAT_ALPHA + Locale.SERVER_BROWSER__PASSWORD_REQUIRED + ": " + (selectedServer.HasPassword ? Locale.SERVER_BROWSER__YES : Locale.SERVER_BROWSER__NO) + "
    "); details.Append(FORMAT_ALPHA + Locale.SERVER_BROWSER__GAME_VERSION + ": " + (selectedServer.GameVersion != MainMenuControllerPatch.MenuProvider.BuildVersionString ? "" : "") + selectedServer.GameVersion + "
    "); - details.Append(selectedServer.ServerDetails); + details.Append($"{FORMAT_ALPHA}{Locale.SERVER_HOST_DETAILS}:
    " + selectedServer.ServerDetails); if (selectedServer.ServerDetails != null && selectedServer.ServerDetails.Length > 0) details.Append("
    "); @@ -656,11 +642,11 @@ private bool BuildServerMods(ModInfo[] serverMods, ModInfo[] localMods) if (modsOk) { elementRequiredMods.Collapse(false); - elementExtraMods.SetText($"{Locale.SERVER_BROWSER__REQUIRED_MODS}: {Locale.SERVER_BROWSER__OK}"); + elementExtraMods.SetText($"{FORMAT_ALPHA}{Locale.SERVER_BROWSER__REQUIRED_MODS}: {Locale.SERVER_BROWSER__OK}"); } else { - elementRequiredMods.SetText($"{Locale.SERVER_BROWSER__REQUIRED_MODS}"); + elementRequiredMods.SetText($"{FORMAT_ALPHA}{Locale.SERVER_BROWSER__REQUIRED_MODS}"); } } @@ -719,11 +705,11 @@ private bool BuildLocalMods(ModInfo[] localMods) if (modsOk) { elementExtraMods.Collapse(false); - elementExtraMods.SetText($"{Locale.SERVER_BROWSER__EXTRA_MODS}: {Locale.SERVER_BROWSER__OK}"); + elementExtraMods.SetText($"{FORMAT_ALPHA}{Locale.SERVER_BROWSER__EXTRA_MODS}: {Locale.SERVER_BROWSER__OK}"); } else { - elementExtraMods.SetText($"{Locale.SERVER_BROWSER__EXTRA_MODS}"); + elementExtraMods.SetText($"{FORMAT_ALPHA}{Locale.SERVER_BROWSER__EXTRA_MODS}"); } } From 604be526dacb9165353b72f3809a639bf76f32ed Mon Sep 17 00:00:00 2001 From: Macka Date: Thu, 8 Jan 2026 17:51:58 +1000 Subject: [PATCH 517/521] Improve localisation for server browser and disconnect reasons Replaced hardcoded disconnect reason strings with localized values in ServerBrowserPane. Added separate localized 'yes' and 'no' responses for password required in server details. Updated Locale.cs and locale.csv to support new keys for these values. --- .../Components/MainMenu/HostGamePane.cs | 8 +- .../Components/MainMenu/ServerBrowserPane.cs | 1898 ++++++++--------- Multiplayer/Locale.cs | 256 ++- locale.csv | 7 +- 4 files changed, 1095 insertions(+), 1074 deletions(-) diff --git a/Multiplayer/Components/MainMenu/HostGamePane.cs b/Multiplayer/Components/MainMenu/HostGamePane.cs index d05f2549..1967d507 100644 --- a/Multiplayer/Components/MainMenu/HostGamePane.cs +++ b/Multiplayer/Components/MainMenu/HostGamePane.cs @@ -460,7 +460,6 @@ private void OnStartClick() serverData.GameMode = LobbyServerData.GetGameModeFromString(startGameData.session.GameMode); } - Multiplayer.Settings.ServerName = serverData.Name; Multiplayer.Settings.Password = password.text; Multiplayer.Settings.Visibility = serverData.Visibility; @@ -468,13 +467,12 @@ private void OnStartClick() Multiplayer.Settings.MaxPlayers = serverData.MaxPlayers; Multiplayer.Settings.Details = serverData.ServerDetails; - - //Pass the server data to the NetworkLifecycle manager + // Pass the server data to the NetworkLifecycle manager NetworkLifecycle.Instance.serverData = serverData; } - //Mark it as a real multiplayer game - NetworkLifecycle.Instance.IsSinglePlayer = false; + // Mark it as a real multiplayer game + NetworkLifecycle.Instance.IsSinglePlayer = false; var ContinueGameRequested = lcInstance.GetType().GetMethod("OnRunClicked", BindingFlags.NonPublic | BindingFlags.Instance); diff --git a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs index 49d1c0b0..f3916d84 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs @@ -28,1248 +28,1246 @@ using UnityEngine.UI; using Color = UnityEngine.Color; -namespace Multiplayer.Components.MainMenu +namespace Multiplayer.Components.MainMenu; + +public class ServerBrowserPane : MonoBehaviour { + private const string FORMAT_ALPHA = ""; - public class ServerBrowserPane : MonoBehaviour + private enum ConnectionState { - private const string FORMAT_ALPHA = ""; - - private enum ConnectionState - { - NotConnected, - JoiningLobby, - AwaitingPassword, - AttemptingSteamRelay, - AttemptingIPv6, - AttemptingIPv6Punch, - AttemptingIPv4, - AttemptingIPv4Punch, - Connected, - Failed, - Aborted - } + NotConnected, + JoiningLobby, + AwaitingPassword, + AttemptingSteamRelay, + AttemptingIPv6, + AttemptingIPv6Punch, + AttemptingIPv4, + AttemptingIPv4Punch, + Connected, + Failed, + Aborted + } - private const int MAX_PORT_LEN = 5; - private const int MIN_PORT = 1024; - private const int MAX_PORT = 49151; - - // Gridview variables - private ServerBrowserGridView serverGridView; - private IServerBrowserGameDetails selectedServer; - - // Ping tracking - private float pingTimer = 0f; - private const float PING_INTERVAL = 2f; // base interval to refresh all pings - - // Button variables - private ButtonDV buttonJoin; - private ButtonDV buttonRefresh; - private ButtonDV buttonDirectIP; - - // Misc GUI Elements - private TextMeshProUGUI serverName; - private TextMeshProUGUI detailsPane; - private GameObject navigationButtonPrefab; - private Transform detailsContent; - private CollapsibleElement elementRequiredMods; - private CollapsibleElement elementExtraMods; - - // Remote server tracking - 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 - private const int REFRESH_MIN_TIME = 10; //Stop refresh spam - private bool remoteRefreshComplete; - - // Connection parameters - private string address; - private int portNumber; - private Lobby? selectedLobby; - private static Lobby? joinedLobby; - public static Lobby? lobbyToJoin; - string password = null; - bool direct = false; - - private ConnectionState connectionState = ConnectionState.NotConnected; - private Popup connectingPopup; - private int attempt; - - private Lobby[] lobbies; - - private bool incompatibleMods = true; - - #region setup - - public void Awake() - { - Multiplayer.Log("MultiplayerPane Awake()"); - joinedLobby?.Leave(); - joinedLobby = null; + private const int MAX_PORT_LEN = 5; + private const int MIN_PORT = 1024; + private const int MAX_PORT = 49151; + + // Gridview variables + private ServerBrowserGridView serverGridView; + private IServerBrowserGameDetails selectedServer; + + // Ping tracking + private float pingTimer = 0f; + private const float PING_INTERVAL = 2f; // base interval to refresh all pings + + // Button variables + private ButtonDV buttonJoin; + private ButtonDV buttonRefresh; + private ButtonDV buttonDirectIP; + + // Misc GUI Elements + private TextMeshProUGUI serverName; + private TextMeshProUGUI detailsPane; + private GameObject navigationButtonPrefab; + private Transform detailsContent; + private CollapsibleElement elementRequiredMods; + private CollapsibleElement elementExtraMods; + + // Remote server tracking + 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 + private const int REFRESH_MIN_TIME = 10; //Stop refresh spam + private bool remoteRefreshComplete; + + // Connection parameters + private string address; + private int portNumber; + private Lobby? selectedLobby; + private static Lobby? joinedLobby; + public static Lobby? lobbyToJoin; + string password = null; + bool direct = false; + + private ConnectionState connectionState = ConnectionState.NotConnected; + private Popup connectingPopup; + private int attempt; + + private Lobby[] lobbies; + + private bool incompatibleMods = true; + + #region setup + + public void Awake() + { + Multiplayer.Log("MultiplayerPane Awake()"); + joinedLobby?.Leave(); + joinedLobby = null; - CleanUI(); - BuildUI(); + CleanUI(); + BuildUI(); - SetupServerBrowser(); - } + SetupServerBrowser(); + } - public void OnEnable() - { - //ensure no incompatible mods are loaded - incompatibleMods = ModCompatibilityManager.Instance.CheckModCompatibility(); + public void OnEnable() + { + //ensure no incompatible mods are loaded + incompatibleMods = ModCompatibilityManager.Instance.CheckModCompatibility(); - this.SetupListeners(true); + this.SetupListeners(true); - buttonDirectIP.ToggleInteractable(true); - buttonRefresh.ToggleInteractable(true); + buttonDirectIP.ToggleInteractable(true); + buttonRefresh.ToggleInteractable(true); - RefreshAction(); - } + RefreshAction(); + } - // Disable listeners - public void OnDisable() - { - this.SetupListeners(false); - } + // Disable listeners + public void OnDisable() + { + this.SetupListeners(false); + } - public void Update() - { - SteamClient.RunCallbacks(); + public void Update() + { + SteamClient.RunCallbacks(); - //Handle server refresh interval - timePassed += Time.deltaTime; + //Handle server refresh interval + timePassed += Time.deltaTime; - if (!serverRefreshing) + if (!serverRefreshing) + { + if (timePassed >= AUTO_REFRESH_TIME) { - if (timePassed >= AUTO_REFRESH_TIME) - { - RefreshAction(); - } - else if (timePassed >= REFRESH_MIN_TIME) - { - buttonRefresh.ToggleInteractable(true); - } + RefreshAction(); } - else if (remoteRefreshComplete) + else if (timePassed >= REFRESH_MIN_TIME) { - RefreshGridView(); - OnSelectedIndexChanged(serverGridView); //Revalidate any selected servers - remoteRefreshComplete = false; - serverRefreshing = false; - timePassed = 0; + buttonRefresh.ToggleInteractable(true); } + } + else if (remoteRefreshComplete) + { + RefreshGridView(); + OnSelectedIndexChanged(serverGridView); //Revalidate any selected servers + remoteRefreshComplete = false; + serverRefreshing = false; + timePassed = 0; + } + + //Handle pinging servers + pingTimer += Time.deltaTime; - //Handle pinging servers - pingTimer += Time.deltaTime; + if (pingTimer >= PING_INTERVAL) + { + UpdatePings(); + pingTimer = 0f; + } - if (pingTimer >= PING_INTERVAL) + if (lobbyToJoin != null && connectionState == ConnectionState.NotConnected) + { + //For invites/requests + Multiplayer.Log($"Player invite initiated/request"); + + if (lobbyToJoin.Value.Id.IsValid) { - UpdatePings(); - pingTimer = 0f; + direct = false; + var _ = JoinLobby((Lobby)lobbyToJoin); } - - if (lobbyToJoin != null && connectionState == ConnectionState.NotConnected) + else { - //For invites/requests - Multiplayer.Log($"Player invite initiated/request"); - - if (lobbyToJoin.Value.Id.IsValid) - { - direct = false; - var _ = JoinLobby((Lobby)lobbyToJoin); - } - else - { - Multiplayer.LogWarning("Received invalid lobby invite"); - lobbyToJoin = null; - } + Multiplayer.LogWarning("Received invalid lobby invite"); + lobbyToJoin = null; } } + } - public void Start() - { - if (DVSteamworks.Success) - return; + public void Start() + { + if (DVSteamworks.Success) + return; - Multiplayer.Log($"Steam not detected, prompt for restart."); - MainMenuThingsAndStuff.Instance.ShowOkPopup("Steam not detected. Please restart the game with Steam running", () => { }); - } + Multiplayer.Log($"Steam not detected, prompt for restart."); + MainMenuThingsAndStuff.Instance.ShowOkPopup("Steam not detected. Please restart the game with Steam running", () => { }); + } - private void CleanUI() - { - GameObject.Destroy(this.FindChildByName("Text Content")); + private void CleanUI() + { + GameObject.Destroy(this.FindChildByName("Text Content")); - GameObject.Destroy(this.FindChildByName("HardcoreSavingBanner")); - GameObject.Destroy(this.FindChildByName("TutorialSavingBanner")); + GameObject.Destroy(this.FindChildByName("HardcoreSavingBanner")); + GameObject.Destroy(this.FindChildByName("TutorialSavingBanner")); - GameObject.Destroy(this.FindChildByName("Thumbnail")); + GameObject.Destroy(this.FindChildByName("Thumbnail")); - GameObject.Destroy(this.FindChildByName("ButtonIcon OpenFolder")); - GameObject.Destroy(this.FindChildByName("ButtonIcon Rename")); - GameObject.Destroy(this.FindChildByName("ButtonTextIcon Load")); + GameObject.Destroy(this.FindChildByName("ButtonIcon OpenFolder")); + GameObject.Destroy(this.FindChildByName("ButtonIcon Rename")); + GameObject.Destroy(this.FindChildByName("ButtonTextIcon Load")); - } - private void BuildUI() + } + 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 = Locale.SERVER_BROWSER__INFO_TITLE;// "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("Content", typeof(RectTransform), typeof(ContentSizeFitter), typeof(VerticalLayoutGroup)); + detailsContent = content.transform; + detailsContent.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; + contentRT.localPosition = new Vector3(contentRT.localPosition.x + 10, contentRT.localPosition.y, contentRT.localPosition.z); + + // Create TextMeshProUGUI object + GameObject textGO = new("Details Text", typeof(TextMeshProUGUI)); + textGO.transform.SetParent(contentRT.transform, false); + detailsPane = textGO.GetComponent(); + detailsPane.textWrappingMode = TextWrappingModes.Normal; + detailsPane.fontSize = 18; + 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."; + + SetupModsGroup(); + + // 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; + } - // 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 = Locale.SERVER_BROWSER__INFO_TITLE;// "Server Browser Info"; + // Set up event listeners + buttonDirectIP = goDirectIP.GetComponent(); + buttonDirectIP.onClick.AddListener(DirectAction); - /* - * 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("Content", typeof(RectTransform), typeof(ContentSizeFitter), typeof(VerticalLayoutGroup)); - detailsContent = content.transform; - detailsContent.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; - contentRT.localPosition = new Vector3(contentRT.localPosition.x + 10, contentRT.localPosition.y, contentRT.localPosition.z); - - // Create TextMeshProUGUI object - GameObject textGO = new("Details Text", typeof(TextMeshProUGUI)); - textGO.transform.SetParent(contentRT.transform, false); - detailsPane = textGO.GetComponent(); - detailsPane.textWrappingMode = TextWrappingModes.Normal; - detailsPane.fontSize = 18; - 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."; + buttonJoin = goJoin.GetComponent(); + buttonJoin.onClick.AddListener(JoinAction); - SetupModsGroup(); + buttonRefresh = goRefresh.GetComponent(); + buttonRefresh.onClick.AddListener(RefreshAction); - // 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); + //Lock out the join button until a server has been selected + buttonJoin.ToggleInteractable(false); + } - // Set content size to fit text - contentRT.sizeDelta = new Vector2(contentRT.sizeDelta.x - 50, detailsPane.preferredHeight); + private void SetupServerBrowser() + { + GameObject GridviewGO = this.FindChildByName("Scroll View").FindChildByName("GRID VIEW"); - // 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); + //Disable before we make any changes + GridviewGO.SetActive(false); - if (goDirectIP == null || goJoin == null || goRefresh == null) - { - Multiplayer.LogError("One or more buttons not found."); - return; - } + //load our custom controller + SaveLoadGridView slgv = GridviewGO.GetComponent(); + serverGridView = GridviewGO.AddComponent(); - // Set up event listeners - buttonDirectIP = goDirectIP.GetComponent(); - buttonDirectIP.onClick.AddListener(DirectAction); + //grab the original prefab + slgv.viewElementPrefab.SetActive(false); + serverGridView.viewElementPrefab = Instantiate(slgv.viewElementPrefab); + slgv.viewElementPrefab.SetActive(true); - buttonJoin = goJoin.GetComponent(); - buttonJoin.onClick.AddListener(JoinAction); + //Remove original controller + GameObject.Destroy(slgv); - buttonRefresh = goRefresh.GetComponent(); - buttonRefresh.onClick.AddListener(RefreshAction); + //Don't forget to re-enable! + GridviewGO.SetActive(true); + serverGridView.Clear(); + } - //Lock out the join button until a server has been selected - buttonJoin.ToggleInteractable(false); + private void SetupModsGroup() + { + ManualController manualController = MainMenuControllerPatch.MainMenuControllerInstance.GetComponentInChildren(true); + if (manualController == null) + { + Multiplayer.LogError("SetupModsGroup() ManualController not found"); + return; } - private void SetupServerBrowser() - { - GameObject GridviewGO = this.FindChildByName("Scroll View").FindChildByName("GRID VIEW"); + navigationButtonPrefab = manualController.navigationButtonPrefab; - //Disable before we make any changes - GridviewGO.SetActive(false); + elementRequiredMods = CreateModElement($"{FORMAT_ALPHA}{Locale.SERVER_BROWSER__REQUIRED_MODS}"); + elementRequiredMods.name = "Required Mods"; + elementRequiredMods.Collapse(true); + elementExtraMods = CreateModElement($"{FORMAT_ALPHA}{Locale.SERVER_BROWSER__EXTRA_MODS}"); + elementExtraMods.name = "Extra Mods"; + elementExtraMods.Collapse(true); + } - //load our custom controller - SaveLoadGridView slgv = GridviewGO.GetComponent(); - serverGridView = GridviewGO.AddComponent(); + private CollapsibleElement CreateModElement(string label, CollapsibleElement parent = null) + { + // Container for required mods + RectTransform rt = Instantiate(navigationButtonPrefab, detailsContent).GetComponent(); - //grab the original prefab - slgv.viewElementPrefab.SetActive(false); - serverGridView.viewElementPrefab = Instantiate(slgv.viewElementPrefab); - slgv.viewElementPrefab.SetActive(true); + CollapsibleElement element = rt.GetComponent(); + CollapsibleElementVisualController controller = rt.GetComponent(); - //Remove original controller - GameObject.Destroy(slgv); + controller.categoryTextColor = Color.white; + controller.articleTextColor = Color.white; + controller.collapseIndicatorImage.color = new(1f, 1f, 1f, 0x50 / 255f); - //Don't forget to re-enable! - GridviewGO.SetActive(true); - serverGridView.Clear(); - } + element.SetText(label); - private void SetupModsGroup() + if (parent != null) { - ManualController manualController = MainMenuControllerPatch.MainMenuControllerInstance.GetComponentInChildren(true); - if (manualController == null) - { - Multiplayer.LogError("SetupModsGroup() ManualController not found"); - return; - } - - navigationButtonPrefab = manualController.navigationButtonPrefab; + var last = parent.childElements.LastOrDefault() ?? parent; + element.transform.SetSiblingIndex(last.transform.GetSiblingIndex() + 1); + parent.AddChild(element); + + // Remove the Button to allow the hyperlink handler to work + Component.Destroy(element.GetComponentInChildren(true)); + + //// Enable hyperlink parsing + HyperlinkHandler modHyperlinkHandler = controller.elementText.GetOrAddComponent(); + modHyperlinkHandler.linkColor = new UnityEngine.Color(0.302f, 0.651f, 1f); // #4DA6FF + modHyperlinkHandler.linkHoverColor = new UnityEngine.Color(0.498f, 0.749f, 1f); // #7FBFFF + modHyperlinkHandler.ApplyLinkStyling(); + } - elementRequiredMods = CreateModElement($"{FORMAT_ALPHA}{Locale.SERVER_BROWSER__REQUIRED_MODS}"); - elementRequiredMods.name = "Required Mods"; - elementRequiredMods.Collapse(true); + return element; + } - elementExtraMods = CreateModElement($"{FORMAT_ALPHA}{Locale.SERVER_BROWSER__EXTRA_MODS}"); - elementExtraMods.name = "Extra Mods"; - elementExtraMods.Collapse(true); - } + private void ClearModElements(CollapsibleElement parent) + { + if (parent == null) + return; - private CollapsibleElement CreateModElement(string label, CollapsibleElement parent = null) - { - // Container for required mods - RectTransform rt = Instantiate(navigationButtonPrefab, detailsContent).GetComponent(); + parent.Collapse(true); - CollapsibleElement element = rt.GetComponent(); - CollapsibleElementVisualController controller = rt.GetComponent(); + if (parent.childElements.Count == 0) + return; - controller.categoryTextColor = Color.white; - controller.articleTextColor = Color.white; - controller.collapseIndicatorImage.color = new(1f, 1f, 1f, 0x50 / 255f); + foreach (var element in parent.childElements) + GameObject.Destroy(element.gameObject); - element.SetText(label); + parent.childElements.Clear(); + } - if (parent != null) - { - var last = parent.childElements.LastOrDefault() ?? parent; - element.transform.SetSiblingIndex(last.transform.GetSiblingIndex() + 1); - parent.AddChild(element); - - // Remove the Button to allow the hyperlink handler to work - Component.Destroy(element.GetComponentInChildren(true)); - - //// Enable hyperlink parsing - HyperlinkHandler modHyperlinkHandler = controller.elementText.GetOrAddComponent(); - modHyperlinkHandler.linkColor = new UnityEngine.Color(0.302f, 0.651f, 1f); // #4DA6FF - modHyperlinkHandler.linkHoverColor = new UnityEngine.Color(0.498f, 0.749f, 1f); // #7FBFFF - modHyperlinkHandler.ApplyLinkStyling(); - } + private void CollapsibleElementClicked(CollapsibleElement element) + { + element.Toggle(); + } - return element; + private void SetupListeners(bool on) + { + if (on) + { + serverGridView.SelectedIndexChanged += this.OnSelectedIndexChanged; + elementRequiredMods.CollapsibleElementClicked += CollapsibleElementClicked; + elementExtraMods.CollapsibleElementClicked += CollapsibleElementClicked; } - - private void ClearModElements(CollapsibleElement parent) + else { - if (parent == null) - return; + serverGridView.SelectedIndexChanged -= this.OnSelectedIndexChanged; + elementRequiredMods.CollapsibleElementClicked -= CollapsibleElementClicked; + elementExtraMods.CollapsibleElementClicked -= CollapsibleElementClicked; + } + } + #endregion - parent.Collapse(true); + #region UI callbacks + private void RefreshAction() + { + if (serverRefreshing) + return; - if (parent.childElements.Count == 0) - return; + remoteServers.Clear(); - foreach (var element in parent.childElements) - GameObject.Destroy(element.gameObject); + serverRefreshing = true; + //buttonJoin.ToggleInteractable(false); + buttonRefresh.ToggleInteractable(false); - parent.childElements.Clear(); - } + if (DVSteamworks.Success) + ListActiveLobbies(); + + } + private void JoinAction() + { + if (selectedServer == null || connectionState != ConnectionState.NotConnected) + return; - private void CollapsibleElementClicked(CollapsibleElement element) + buttonDirectIP.ToggleInteractable(false); + buttonJoin.ToggleInteractable(false); + + //not making a direct connection + direct = false; + portNumber = -1; + + var lobby = GetLobbyFromServer(selectedServer); + if (lobby != null) { - element.Toggle(); + selectedLobby = (Lobby)lobby; + _ = JoinLobby((Lobby)selectedLobby); } - - private void SetupListeners(bool on) + else { - if (on) - { - serverGridView.SelectedIndexChanged += this.OnSelectedIndexChanged; - elementRequiredMods.CollapsibleElementClicked += CollapsibleElementClicked; - elementExtraMods.CollapsibleElementClicked += CollapsibleElementClicked; - } - else - { - serverGridView.SelectedIndexChanged -= this.OnSelectedIndexChanged; - elementRequiredMods.CollapsibleElementClicked -= CollapsibleElementClicked; - elementExtraMods.CollapsibleElementClicked -= CollapsibleElementClicked; - } + Multiplayer.LogWarning($"JoinAction called but lobby is null"); + AttemptFail(); } - #endregion + } - #region UI callbacks - private void RefreshAction() - { - if (serverRefreshing) - return; + private void DirectAction() + { + if (connectionState != ConnectionState.NotConnected) + return; - remoteServers.Clear(); + buttonDirectIP.ToggleInteractable(false); + buttonJoin.ToggleInteractable(false); - serverRefreshing = true; - //buttonJoin.ToggleInteractable(false); - buttonRefresh.ToggleInteractable(false); + //making a direct connection + direct = true; + password = null; - if (DVSteamworks.Success) - ListActiveLobbies(); + ShowIpPopup(); + } - } - private void JoinAction() + private void OnSelectedIndexChanged(MPGridView gridView) + { + if (serverRefreshing) + return; + + selectedServer = gridView.SelectedItem; + if (selectedServer != null && incompatibleMods == false) { - if (selectedServer == null || connectionState != ConnectionState.NotConnected) - return; + UpdateDetailsPane(); - buttonDirectIP.ToggleInteractable(false); - buttonJoin.ToggleInteractable(false); + // Check if we can connect to this server + Multiplayer.Log($"Server: \"{selectedServer.GameVersion}\" \"{selectedServer.MultiplayerVersion}\""); + Multiplayer.Log($"Client: \"{MainMenuControllerPatch.MenuProvider.BuildVersionString}\" \"{Multiplayer.Ver}\""); + Multiplayer.Log($"Result: \"{selectedServer.GameVersion == MainMenuControllerPatch.MenuProvider.BuildVersionString}\" \"{selectedServer.MultiplayerVersion == Multiplayer.Ver}\""); - //not making a direct connection - direct = false; - portNumber = -1; + bool canConnect = selectedServer.GameVersion == MainMenuControllerPatch.MenuProvider.BuildVersionString && + selectedServer.MultiplayerVersion == Multiplayer.Ver; - var lobby = GetLobbyFromServer(selectedServer); - if (lobby != null) - { - selectedLobby = (Lobby)lobby; - _ = JoinLobby((Lobby)selectedLobby); - } - else - { - Multiplayer.LogWarning($"JoinAction called but lobby is null"); - AttemptFail(); - } + buttonJoin.ToggleInteractable(canConnect); } - - private void DirectAction() + else { - if (connectionState != ConnectionState.NotConnected) - return; - - buttonDirectIP.ToggleInteractable(false); buttonJoin.ToggleInteractable(false); + } + } - //making a direct connection - direct = true; - password = null; + private void UpdateElement(IServerBrowserGameDetails element) + { + int index = serverGridView.IndexOf(element); - ShowIpPopup(); + if (index >= 0) + { + var viewElement = serverGridView.GetElementAt(index) as ServerBrowserElement; + viewElement?.UpdateView(); } + } + #endregion - private void OnSelectedIndexChanged(MPGridView gridView) + private void UpdateDetailsPane() + { + StringBuilder details = new(); + + if (selectedServer != null) { - if (serverRefreshing) - return; + serverName.text = selectedServer.Name; - selectedServer = gridView.SelectedItem; - if (selectedServer != null && incompatibleMods == false) - { - UpdateDetailsPane(); + // Note: built-in localisations have a trailing colon e.g. 'Game mode:' - // Check if we can connect to this server - Multiplayer.Log($"Server: \"{selectedServer.GameVersion}\" \"{selectedServer.MultiplayerVersion}\""); - Multiplayer.Log($"Client: \"{MainMenuControllerPatch.MenuProvider.BuildVersionString}\" \"{Multiplayer.Ver}\""); - Multiplayer.Log($"Result: \"{selectedServer.GameVersion == MainMenuControllerPatch.MenuProvider.BuildVersionString}\" \"{selectedServer.MultiplayerVersion == Multiplayer.Ver}\""); + details.Append(FORMAT_ALPHA + LocalizationAPI.L("launcher/game_mode", []) + " " + LobbyServerData.GetGameModeFromInt(selectedServer.GameMode) + "
    "); + details.Append(FORMAT_ALPHA + LocalizationAPI.L("launcher/difficulty", []) + " " + LobbyServerData.GetDifficultyFromInt(selectedServer.Difficulty) + "
    "); + details.Append(FORMAT_ALPHA + LocalizationAPI.L("launcher/in_game_time_passed", []) + " " + selectedServer.TimePassed + "
    "); + details.Append(FORMAT_ALPHA + Locale.SERVER_BROWSER__PLAYERS + ": " + selectedServer.CurrentPlayers + '/' + selectedServer.MaxPlayers + "
    "); + details.Append(FORMAT_ALPHA + Locale.SERVER_BROWSER__PASSWORD_REQUIRED + ": " + (selectedServer.HasPassword ? Locale.SERVER_BROWSER__PASSWORD_REQUIRED_YES : Locale.SERVER_BROWSER__PASSWORD_REQUIRED_NO) + "
    "); + details.Append(FORMAT_ALPHA + Locale.SERVER_BROWSER__GAME_VERSION + ": " + (selectedServer.GameVersion != MainMenuControllerPatch.MenuProvider.BuildVersionString ? "" : "") + selectedServer.GameVersion + "
    "); + details.Append($"{FORMAT_ALPHA}{Locale.SERVER_HOST_DETAILS}:
    " + selectedServer.ServerDetails); - bool canConnect = selectedServer.GameVersion == MainMenuControllerPatch.MenuProvider.BuildVersionString && - selectedServer.MultiplayerVersion == Multiplayer.Ver; + if (selectedServer.ServerDetails != null && selectedServer.ServerDetails.Length > 0) + details.Append("
    "); - buttonJoin.ToggleInteractable(canConnect); - } - else - { - buttonJoin.ToggleInteractable(false); - } - } + detailsPane.text = details.ToString(); + + // Build mod lists + ClearModElements(elementRequiredMods); + ClearModElements(elementExtraMods); - private void UpdateElement(IServerBrowserGameDetails element) + var localMods = ModCompatibilityManager.Instance.GetLocalMods(); + + BuildServerMods(selectedServer.RequiredMods, localMods); + BuildLocalMods(localMods); + } + else { - int index = serverGridView.IndexOf(element); + serverName.text = Locale.SERVER_BROWSER__INFO_TITLE;// "Server Browser Info"; + 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."; - if (index >= 0) - { - var viewElement = serverGridView.GetElementAt(index) as ServerBrowserElement; - viewElement?.UpdateView(); - } + ClearModElements(elementRequiredMods); + ClearModElements(elementExtraMods); } - #endregion - private void UpdateDetailsPane() + } + + /// + /// Validates the client has all required mods for the server and the versions match. + /// Populates the mod details list. + /// + /// + /// + /// + /// true if all required mods are present and have correct versions, false if any mods are missing or there is a version mismatch. + private bool BuildServerMods(ModInfo[] serverMods, ModInfo[] localMods) + { + bool modsOk = true; + + if (serverMods == null || localMods == null) { - StringBuilder details = new(); + Multiplayer.LogWarning("BuildServerMods() called with null serverMods or localMods"); + return false; + } - if (selectedServer != null) + if (selectedServer.RequiredMods != null && selectedServer.RequiredMods.Length > 0) + { + Multiplayer.LogDebug(() => $"Parsed {serverMods?.Length} mods from server \"{selectedServer?.Name}\""); + + foreach (var mod in serverMods) { - serverName.text = selectedServer.Name; + ModInfo modMatch = localMods.FirstOrDefault(l => l.Id == mod.Id); + + Multiplayer.LogDebug(() => $"Checking mod \"{mod.Id}\" v\"{mod.Version}\" - Found: \"{modMatch.Id}\" v\"{modMatch.Version}\""); - // Note: built-in localisations have a trailing colon e.g. 'Game mode:' + bool modFound = modMatch.Id == mod.Id; + bool modVersionMatch = modFound && modMatch.Version == mod.Version; - details.Append(FORMAT_ALPHA + LocalizationAPI.L("launcher/game_mode", []) + " " + LobbyServerData.GetGameModeFromInt(selectedServer.GameMode) + "
    "); - details.Append(FORMAT_ALPHA + LocalizationAPI.L("launcher/difficulty", []) + " " + LobbyServerData.GetDifficultyFromInt(selectedServer.Difficulty) + "
    "); - details.Append(FORMAT_ALPHA + LocalizationAPI.L("launcher/in_game_time_passed", []) + " " + selectedServer.TimePassed + "
    "); - details.Append(FORMAT_ALPHA + Locale.SERVER_BROWSER__PLAYERS + ": " + selectedServer.CurrentPlayers + '/' + selectedServer.MaxPlayers + "
    "); - details.Append(FORMAT_ALPHA + Locale.SERVER_BROWSER__PASSWORD_REQUIRED + ": " + (selectedServer.HasPassword ? Locale.SERVER_BROWSER__YES : Locale.SERVER_BROWSER__NO) + "
    "); - details.Append(FORMAT_ALPHA + Locale.SERVER_BROWSER__GAME_VERSION + ": " + (selectedServer.GameVersion != MainMenuControllerPatch.MenuProvider.BuildVersionString ? "" : "") + selectedServer.GameVersion + "
    "); - details.Append($"{FORMAT_ALPHA}{Locale.SERVER_HOST_DETAILS}:
    " + selectedServer.ServerDetails); + modsOk &= modVersionMatch; - if (selectedServer.ServerDetails != null && selectedServer.ServerDetails.Length > 0) - details.Append("
    "); + string status; + if (modFound && modVersionMatch) + status = $"{Locale.SERVER_BROWSER__OK}"; + else if (modFound && !modVersionMatch) + status = $"{Locale.SERVER_BROWSER__MISMATCH}"; + else + status = $"{Locale.SERVER_BROWSER__MISSING}"; - detailsPane.text = details.ToString(); + var link = !string.IsNullOrEmpty(mod.Url) ? $"{mod.Id}" : mod.Id; - // Build mod lists - ClearModElements(elementRequiredMods); - ClearModElements(elementExtraMods); + var element = CreateModElement(mod.Id, elementRequiredMods); + element.isLeaf = true; + element.SetText($"{link} ({mod.Version}) - {status}"); + } - var localMods = ModCompatibilityManager.Instance.GetLocalMods(); + elementRequiredMods.Expand(false); - BuildServerMods(selectedServer.RequiredMods, localMods); - BuildLocalMods(localMods); + if (modsOk) + { + elementRequiredMods.Collapse(false); + elementExtraMods.SetText($"{FORMAT_ALPHA}{Locale.SERVER_BROWSER__REQUIRED_MODS}: {Locale.SERVER_BROWSER__OK}"); } else { - serverName.text = Locale.SERVER_BROWSER__INFO_TITLE;// "Server Browser Info"; - 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."; - - ClearModElements(elementRequiredMods); - ClearModElements(elementExtraMods); + elementRequiredMods.SetText($"{FORMAT_ALPHA}{Locale.SERVER_BROWSER__REQUIRED_MODS}"); } - } - /// - /// Validates the client has all required mods for the server and the versions match. - /// Populates the mod details list. - /// - /// - /// - /// - /// true if all required mods are present and have correct versions, false if any mods are missing or there is a version mismatch. - private bool BuildServerMods(ModInfo[] serverMods, ModInfo[] localMods) + return modsOk; + } + + /// + /// Validates the client does not have any mods that the server is not running and does not have any mods incompatible with Multiplayer. + /// Populates the mod details list. + /// + /// + /// + /// + /// true if there are no conflicting mods, false if any mods can not be used with this server. + private bool BuildLocalMods(ModInfo[] localMods) + { + bool modsOk = true; + + if (localMods == null || selectedServer?.RequiredMods == null) { - bool modsOk = true; + Multiplayer.LogWarning($"BuildLocalMods() localMods is null: {localMods == null}, requiredMods is null: {selectedServer?.RequiredMods == null}"); + return false; + } - if (serverMods == null || localMods == null) - { - Multiplayer.LogWarning("BuildServerMods() called with null serverMods or localMods"); - return false; - } + var extraMods = localMods.Where(l => !selectedServer.RequiredMods.Any(m => m.Id == l.Id)).ToArray(); + Multiplayer.LogDebug(() => $"Found {extraMods.Length} extra mods on client for server \"{selectedServer.Name}\""); - if (selectedServer.RequiredMods != null && selectedServer.RequiredMods.Length > 0) + if (extraMods.Length > 0) + { + string status; + foreach (var mod in extraMods) { - Multiplayer.LogDebug(() => $"Parsed {serverMods?.Length} mods from server \"{selectedServer?.Name}\""); - - foreach (var mod in serverMods) + var compatibility = ModCompatibilityManager.Instance.GetCompatibility(mod); + if (compatibility == MultiplayerCompatibility.Incompatible) { - ModInfo modMatch = localMods.FirstOrDefault(l => l.Id == mod.Id); - - Multiplayer.LogDebug(() => $"Checking mod \"{mod.Id}\" v\"{mod.Version}\" - Found: \"{modMatch.Id}\" v\"{modMatch.Version}\""); - - bool modFound = modMatch.Id == mod.Id; - bool modVersionMatch = modFound && modMatch.Version == mod.Version; - - modsOk &= modVersionMatch; - - string status; - if (modFound && modVersionMatch) - status = $"{Locale.SERVER_BROWSER__OK}"; - else if (modFound && !modVersionMatch) - status = $"{Locale.SERVER_BROWSER__MISMATCH}"; - else - status = $"{Locale.SERVER_BROWSER__MISSING}"; - - var link = !string.IsNullOrEmpty(mod.Url) ? $"{mod.Id}" : mod.Id; - - var element = CreateModElement(mod.Id, elementRequiredMods); - element.isLeaf = true; - element.SetText($"{link} ({mod.Version}) - {status}"); + status = $"{Locale.SERVER_BROWSER__INCOMPATIBLE}"; + modsOk = false; } - - elementRequiredMods.Expand(false); - - if (modsOk) + else if (compatibility == MultiplayerCompatibility.Undefined || compatibility == MultiplayerCompatibility.All) { - elementRequiredMods.Collapse(false); - elementExtraMods.SetText($"{FORMAT_ALPHA}{Locale.SERVER_BROWSER__REQUIRED_MODS}: {Locale.SERVER_BROWSER__OK}"); + status = $"{Locale.SERVER_BROWSER__EXTRA_MOD}"; + modsOk = false; } else { - elementRequiredMods.SetText($"{FORMAT_ALPHA}{Locale.SERVER_BROWSER__REQUIRED_MODS}"); + status = $"{Locale.SERVER_BROWSER__OK}"; } - } - return modsOk; - } + var element = CreateModElement(mod.Id, elementExtraMods); + element.isLeaf = true; + element.SetText($"{mod.Id} ({mod.Version}) - {status}"); + } - /// - /// Validates the client does not have any mods that the server is not running and does not have any mods incompatible with Multiplayer. - /// Populates the mod details list. - /// - /// - /// - /// - /// true if there are no conflicting mods, false if any mods can not be used with this server. - private bool BuildLocalMods(ModInfo[] localMods) - { - bool modsOk = true; + elementExtraMods.Expand(false); - if (localMods == null || selectedServer?.RequiredMods == null) + if (modsOk) { - Multiplayer.LogWarning($"BuildLocalMods() localMods is null: {localMods == null}, requiredMods is null: {selectedServer?.RequiredMods == null}"); - return false; + elementExtraMods.Collapse(false); + elementExtraMods.SetText($"{FORMAT_ALPHA}{Locale.SERVER_BROWSER__EXTRA_MODS}: {Locale.SERVER_BROWSER__OK}"); } - - var extraMods = localMods.Where(l => !selectedServer.RequiredMods.Any(m => m.Id == l.Id)).ToArray(); - Multiplayer.LogDebug(() => $"Found {extraMods.Length} extra mods on client for server \"{selectedServer.Name}\""); - - if (extraMods.Length > 0) + else { - string status; - foreach (var mod in extraMods) - { - var compatibility = ModCompatibilityManager.Instance.GetCompatibility(mod); - if (compatibility == MultiplayerCompatibility.Incompatible) - { - status = $"{Locale.SERVER_BROWSER__INCOMPATIBLE}"; - modsOk = false; - } - else if (compatibility == MultiplayerCompatibility.Undefined || compatibility == MultiplayerCompatibility.All) - { - status = $"{Locale.SERVER_BROWSER__EXTRA_MOD}"; - modsOk = false; - } - else - { - status = $"{Locale.SERVER_BROWSER__OK}"; - } - - var element = CreateModElement(mod.Id, elementExtraMods); - element.isLeaf = true; - element.SetText($"{mod.Id} ({mod.Version}) - {status}"); - } - - elementExtraMods.Expand(false); - - if (modsOk) - { - elementExtraMods.Collapse(false); - elementExtraMods.SetText($"{FORMAT_ALPHA}{Locale.SERVER_BROWSER__EXTRA_MODS}: {Locale.SERVER_BROWSER__OK}"); - } - else - { - elementExtraMods.SetText($"{FORMAT_ALPHA}{Locale.SERVER_BROWSER__EXTRA_MODS}"); - } + elementExtraMods.SetText($"{FORMAT_ALPHA}{Locale.SERVER_BROWSER__EXTRA_MODS}"); } + } - return modsOk; + return modsOk; + } + + private void ShowIpPopup() + { + var popup = MainMenuThingsAndStuff.Instance.ShowRenamePopup(); + if (popup == null) + { + Multiplayer.LogError("Popup not found."); + return; } - private void ShowIpPopup() + popup.labelTMPro.text = Locale.SERVER_BROWSER__IP; + popup.GetComponentInChildren().text = Multiplayer.Settings.LastRemoteIP; + + popup.Closed += result => { - var popup = MainMenuThingsAndStuff.Instance.ShowRenamePopup(); - if (popup == null) + if (result.closedBy == PopupClosedByAction.Abortion) { - Multiplayer.LogError("Popup not found."); + buttonDirectIP.ToggleInteractable(true); + OnSelectedIndexChanged(serverGridView); //re-enable the join button if a valid gridview item is selected return; } - popup.labelTMPro.text = Locale.SERVER_BROWSER__IP; - popup.GetComponentInChildren().text = Multiplayer.Settings.LastRemoteIP; - - popup.Closed += result => + if (!IPAddress.TryParse(result.data, out IPAddress parsedAddress)) { - if (result.closedBy == PopupClosedByAction.Abortion) + string inputUrl = result.data; + + if (!inputUrl.StartsWith("http://") && !inputUrl.StartsWith("https://")) { - buttonDirectIP.ToggleInteractable(true); - OnSelectedIndexChanged(serverGridView); //re-enable the join button if a valid gridview item is selected - return; + inputUrl = "http://" + inputUrl; } - if (!IPAddress.TryParse(result.data, out IPAddress parsedAddress)) - { - string inputUrl = result.data; + bool isValidURL = Uri.TryCreate(inputUrl, UriKind.Absolute, out Uri uriResult) + && (uriResult.Scheme == Uri.UriSchemeHttp || uriResult.Scheme == Uri.UriSchemeHttps); - if (!inputUrl.StartsWith("http://") && !inputUrl.StartsWith("https://")) + if (isValidURL) + { + string domainName = ExtractDomainName(result.data); + try { - inputUrl = "http://" + inputUrl; - } - - bool isValidURL = Uri.TryCreate(inputUrl, UriKind.Absolute, out Uri uriResult) - && (uriResult.Scheme == Uri.UriSchemeHttp || uriResult.Scheme == Uri.UriSchemeHttps); + IPHostEntry hostEntry = Dns.GetHostEntry(domainName); + IPAddress[] addresses = hostEntry.AddressList; - if (isValidURL) - { - string domainName = ExtractDomainName(result.data); - try + if (addresses.Length > 0) { - IPHostEntry hostEntry = Dns.GetHostEntry(domainName); - IPAddress[] addresses = hostEntry.AddressList; - - if (addresses.Length > 0) - { - string address2 = addresses[0].ToString(); + string address2 = addresses[0].ToString(); - address = address2; - Multiplayer.Log(address); + address = address2; + Multiplayer.Log(address); - ShowPortPopup(); - return; - } - } - catch (Exception ex) - { - Multiplayer.LogError($"An error occurred: {ex.Message}"); + ShowPortPopup(); + return; } } - - MainMenuThingsAndStuff.Instance.ShowOkPopup(Locale.SERVER_BROWSER__IP_INVALID, ShowIpPopup); + catch (Exception ex) + { + Multiplayer.LogError($"An error occurred: {ex.Message}"); + } } + + MainMenuThingsAndStuff.Instance.ShowOkPopup(Locale.SERVER_BROWSER__IP_INVALID, ShowIpPopup); + } + else + { + if (parsedAddress.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork) + connectionState = ConnectionState.AttemptingIPv4; else - { - if (parsedAddress.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork) - connectionState = ConnectionState.AttemptingIPv4; - else - connectionState = ConnectionState.AttemptingIPv6; + connectionState = ConnectionState.AttemptingIPv6; - address = result.data; - ShowPortPopup(); - } - }; - } + address = result.data; + ShowPortPopup(); + } + }; + } - private void ShowPortPopup() + private void ShowPortPopup() + { + + var popup = MainMenuThingsAndStuff.Instance.ShowRenamePopup(); + if (popup == null) { + Multiplayer.LogError("Popup not found."); + return; + } - var popup = MainMenuThingsAndStuff.Instance.ShowRenamePopup(); - if (popup == null) + 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) { - Multiplayer.LogError("Popup not found."); + buttonDirectIP.ToggleInteractable(true); 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 (!int.TryParse(result.data, out portNumber) || portNumber < MIN_PORT || portNumber > MAX_PORT) { - if (result.closedBy == PopupClosedByAction.Abortion) - { - buttonDirectIP.ToggleInteractable(true); - return; - } + MainMenuThingsAndStuff.Instance.ShowOkPopup(Locale.SERVER_BROWSER__PORT_INVALID, ShowIpPopup); + } + else + { + ShowPasswordPopup(); + } + }; + } - if (!int.TryParse(result.data, out portNumber) || portNumber < MIN_PORT || portNumber > MAX_PORT) - { - MainMenuThingsAndStuff.Instance.ShowOkPopup(Locale.SERVER_BROWSER__PORT_INVALID, ShowIpPopup); - } - else - { - ShowPasswordPopup(); - } - }; + private void ShowPasswordPopup() + { + var popup = MainMenuThingsAndStuff.Instance.ShowRenamePopup(); + if (popup == null) + { + Multiplayer.LogError("Popup not found."); + return; } - private void ShowPasswordPopup() + popup.labelTMPro.text = Locale.SERVER_BROWSER__PASSWORD; + + //direct IP connection + if (direct) { - var popup = MainMenuThingsAndStuff.Instance.ShowRenamePopup(); - if (popup == null) + //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) { - Multiplayer.LogError("Popup not found."); + AttemptFail(); return; } - popup.labelTMPro.text = Locale.SERVER_BROWSER__PASSWORD; + password = result.data; - //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(); + //store params for later + Multiplayer.Settings.LastRemoteIP = address; + Multiplayer.Settings.LastRemotePort = portNumber; + Multiplayer.Settings.LastRemotePassword = result.data; } - popup.Closed += result => - { - if (result.closedBy == PopupClosedByAction.Abortion) - { - AttemptFail(); - return; - } - - password = result.data; - - if (direct) - { - //store params for later - Multiplayer.Settings.LastRemoteIP = address; - Multiplayer.Settings.LastRemotePort = portNumber; - Multiplayer.Settings.LastRemotePassword = result.data; - } + InitiateConnection(); + }; + } - InitiateConnection(); - }; - } + public void ShowConnectingPopup() + { + var popup = MainMenuThingsAndStuff.Instance.ShowOkPopup(); - public void ShowConnectingPopup() + if (popup == null) { - var popup = MainMenuThingsAndStuff.Instance.ShowOkPopup(); + Multiplayer.LogError("ShowConnectingPopup() Popup not found."); + return; + } - if (popup == null) - { - Multiplayer.LogError("ShowConnectingPopup() Popup not found."); - return; - } + connectingPopup = popup; - connectingPopup = popup; + Localize loc = popup.positiveButton.GetComponentInChildren(); + loc.key = "cancel"; + loc.UpdateLocalization(); - Localize loc = popup.positiveButton.GetComponentInChildren(); - loc.key = "cancel"; - loc.UpdateLocalization(); + popup.labelTMPro.text = $"Connecting, please wait..."; //to be localised - popup.labelTMPro.text = $"Connecting, please wait..."; //to be localised + popup.Closed += (PopupResult result) => + { + connectionState = ConnectionState.Aborted; + }; - popup.Closed += (PopupResult result) => - { - connectionState = ConnectionState.Aborted; - }; + } - } + #region workflow + private void UpdatePings() + { + UpdatePingsSteam(); + } - #region workflow - private void UpdatePings() - { - UpdatePingsSteam(); - } + private void InitiateConnection() + { - private void InitiateConnection() - { + Multiplayer.Log($"Initiating connection. Direct: {direct}, Address: {address}, Lobby: {selectedLobby?.Id.ToString()}"); - Multiplayer.Log($"Initiating connection. Direct: {direct}, Address: {address}, Lobby: {selectedLobby?.Id.ToString()}"); + attempt = 0; + ShowConnectingPopup(); - attempt = 0; - ShowConnectingPopup(); + if (!direct && joinedLobby != null) + { + connectionState = ConnectionState.AttemptingSteamRelay; + string hostId = ((Lobby)joinedLobby).Owner.Id.Value.ToString(); + NetworkLifecycle.Instance.StartClient(hostId, -1, password, false, OnDisconnect); + return; + } - if (!direct && joinedLobby != null) - { - connectionState = ConnectionState.AttemptingSteamRelay; - string hostId = ((Lobby)joinedLobby).Owner.Id.Value.ToString(); - NetworkLifecycle.Instance.StartClient(hostId, -1, password, false, OnDisconnect); - return; - } + Multiplayer.Log($"AttemptConnection address: {address}"); - Multiplayer.Log($"AttemptConnection address: {address}"); + if (IPAddress.TryParse(address, out IPAddress IPaddress)) + { + Multiplayer.Log($"AttemptConnection tryParse: {IPaddress.AddressFamily}"); - if (IPAddress.TryParse(address, out IPAddress IPaddress)) + if (IPaddress.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork) { - 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(); - } + AttemptIPv4(); } - else + else if (IPaddress.AddressFamily == System.Net.Sockets.AddressFamily.InterNetworkV6) { - Multiplayer.LogError($"IP address invalid: {address}"); - AttemptFail(); + AttemptIPv6(); } } - - private void AttemptIPv6() + else { - Multiplayer.Log($"AttemptIPv6() {address}"); + Multiplayer.LogError($"IP address invalid: {address}"); + AttemptFail(); + } + } - if (connectionState == ConnectionState.Aborted) - return; + private void AttemptIPv6() + { + Multiplayer.Log($"AttemptIPv6() {address}"); - attempt++; - if (connectingPopup != null) - connectingPopup.labelTMPro.text = $"Connecting, please wait...\r\nAttempt: {attempt}"; + if (connectionState == ConnectionState.Aborted) + return; - Multiplayer.Log($"AttemptIPv6() starting attempt"); - connectionState = ConnectionState.AttemptingIPv6; - SingletonBehaviour.Instance.StartClient(address, portNumber, password, false, OnDisconnect); + attempt++; + if (connectingPopup != null) + connectingPopup.labelTMPro.text = $"Connecting, please wait...\r\nAttempt: {attempt}"; - } + Multiplayer.Log($"AttemptIPv6() starting attempt"); + connectionState = ConnectionState.AttemptingIPv6; + SingletonBehaviour.Instance.StartClient(address, portNumber, password, false, OnDisconnect); - //private void AttemptIPv6Punch() - //{ - // Multiplayer.Log($"AttemptIPv6Punch() {address}"); + } - // if (connectionState == ConnectionState.Aborted) - // return; + //private void AttemptIPv6Punch() + //{ + // Multiplayer.Log($"AttemptIPv6Punch() {address}"); - // attempt++; - // if (connectingPopup != null) - // connectingPopup.labelTMPro.text = $"Connecting, please wait...\r\nAttempt: {attempt}"; + // if (connectionState == ConnectionState.Aborted) + // return; - // //punching not implemented we'll just try again for now - // connectionState = ConnectionState.AttemptingIPv6Punch; - // SingletonBehaviour.Instance.StartClient(address, portNumber, password, false, OnDisconnect); + // attempt++; + // if (connectingPopup != null) + // connectingPopup.labelTMPro.text = $"Connecting, please wait...\r\nAttempt: {attempt}"; - //} - private void AttemptIPv4() - { - Multiplayer.Log($"AttemptIPv4() {address}, {connectionState}"); + // //punching not implemented we'll just try again for now + // connectionState = ConnectionState.AttemptingIPv6Punch; + // SingletonBehaviour.Instance.StartClient(address, portNumber, password, false, OnDisconnect); - if (connectionState == ConnectionState.Aborted) - return; + //} + private void AttemptIPv4() + { + Multiplayer.Log($"AttemptIPv4() {address}, {connectionState}"); - attempt++; - if (connectingPopup != null) - connectingPopup.labelTMPro.text = $"Connecting, please wait...\r\nAttempt: {attempt}"; + if (connectionState == ConnectionState.Aborted) + return; - if (!direct) - { - if (selectedServer.ipv4 == null || selectedServer.ipv4 == string.Empty) - { - AttemptFail(); - return; - } + attempt++; + if (connectingPopup != null) + connectingPopup.labelTMPro.text = $"Connecting, please wait...\r\nAttempt: {attempt}"; - address = selectedServer.ipv4; + if (!direct) + { + if (selectedServer.ipv4 == null || selectedServer.ipv4 == string.Empty) + { + AttemptFail(); + return; } - Multiplayer.Log($"AttemptIPv4() {address}"); + address = selectedServer.ipv4; + } - if (IPAddress.TryParse(address, out IPAddress IPaddress)) + Multiplayer.Log($"AttemptIPv4() {address}"); + + if (IPAddress.TryParse(address, out IPAddress IPaddress)) + { + Multiplayer.Log($"AttemptIPv4() TryParse passed"); + if (IPaddress.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork) { - 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, portNumber, password, false, OnDisconnect); - return; - } + Multiplayer.Log($"AttemptIPv4() starting attempt"); + connectionState = ConnectionState.AttemptingIPv4; + 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() - //{ - // Multiplayer.Log($"AttemptIPv4Punch() {address}"); + Multiplayer.Log($"AttemptIPv4() TryParse failed"); + AttemptFail(); + string message = "Host Unreachable"; + MainMenuThingsAndStuff.Instance.ShowOkPopup(message, () => { }); + } - // if (connectionState == ConnectionState.Aborted) - // return; + //private void AttemptIPv4Punch() + //{ + // Multiplayer.Log($"AttemptIPv4Punch() {address}"); - // attempt++; - // if (connectingPopup != null) - // connectingPopup.labelTMPro.text = $"Connecting, please wait...\r\nAttempt: {attempt}"; + // if (connectionState == ConnectionState.Aborted) + // return; - // //punching not implemented we'll just try again for now - // connectionState = ConnectionState.AttemptingIPv4Punch; - // SingletonBehaviour.Instance.StartClient(address, portNumber, password, false, OnDisconnect); - //} + // attempt++; + // if (connectingPopup != null) + // connectingPopup.labelTMPro.text = $"Connecting, please wait...\r\nAttempt: {attempt}"; - private void AttemptFail() - { - connectionState = ConnectionState.Failed; + // //punching not implemented we'll just try again for now + // connectionState = ConnectionState.AttemptingIPv4Punch; + // SingletonBehaviour.Instance.StartClient(address, portNumber, password, false, OnDisconnect); + //} - if (connectingPopup != null) - { - connectingPopup.RequestClose(PopupClosedByAction.Abortion, null); - connectingPopup = null; // Clear the reference - } + private void AttemptFail() + { + connectionState = ConnectionState.Failed; - joinedLobby?.Leave(); - joinedLobby = null; + if (connectingPopup != null) + { + connectingPopup.RequestClose(PopupClosedByAction.Abortion, null); + connectingPopup = null; // Clear the reference + } - if (gameObject != null && gameObject.activeInHierarchy) - { - if (serverGridView != null) - OnSelectedIndexChanged(serverGridView); + joinedLobby?.Leave(); + joinedLobby = null; - if (buttonDirectIP != null && buttonDirectIP.gameObject != null) - buttonDirectIP.ToggleInteractable(true); - } + if (gameObject != null && gameObject.activeInHierarchy) + { + if (serverGridView != null) + OnSelectedIndexChanged(serverGridView); - StartCoroutine(ResetConnectionState()); + if (buttonDirectIP != null && buttonDirectIP.gameObject != null) + buttonDirectIP.ToggleInteractable(true); } - private IEnumerator ResetConnectionState() - { - yield return new WaitForSeconds(1.0f); - connectionState = ConnectionState.NotConnected; - } + StartCoroutine(ResetConnectionState()); + } - private void OnDisconnect(DisconnectReason reason, string message) - { - Multiplayer.Log($"Disconnected due to: {reason}, \"{message}\""); + private IEnumerator ResetConnectionState() + { + yield return new WaitForSeconds(1.0f); + connectionState = ConnectionState.NotConnected; + } + + private void OnDisconnect(DisconnectReason reason, string message) + { + Multiplayer.Log($"Disconnected due to: {reason}, \"{message}\""); - string displayMessage = !string.IsNullOrEmpty(message) - ? message - : GetDisplayMessageForDisconnect(reason); + string displayMessage = !string.IsNullOrEmpty(message) + ? message + : GetDisplayMessageForDisconnect(reason); - Multiplayer.LogDebug(() => "OnDisconnect() Leaving Lobby"); - joinedLobby?.Leave(); - joinedLobby = null; + Multiplayer.LogDebug(() => "OnDisconnect() Leaving Lobby"); + joinedLobby?.Leave(); + joinedLobby = null; - connectionState = ConnectionState.NotConnected; - AttemptFail(); + connectionState = ConnectionState.NotConnected; + AttemptFail(); - NetworkLifecycle.Instance.QueueMainMenuEvent(() => - { - Multiplayer.LogDebug(() => "OnDisconnect() Queuing"); - MainMenuThingsAndStuff.Instance?.ShowOkPopup(displayMessage, () => { }); - }); - } + NetworkLifecycle.Instance.QueueMainMenuEvent(() => + { + Multiplayer.LogDebug(() => "OnDisconnect() Queuing"); + MainMenuThingsAndStuff.Instance?.ShowOkPopup(displayMessage, () => { }); + }); + } - private string GetDisplayMessageForDisconnect(DisconnectReason reason) + private string GetDisplayMessageForDisconnect(DisconnectReason reason) + { + return reason switch { - return reason switch - { - DisconnectReason.UnknownHost => "Unknown Host", - DisconnectReason.DisconnectPeerCalled => "Player Kicked", - DisconnectReason.ConnectionFailed => "Host Unreachable", - DisconnectReason.ConnectionRejected => "Rejected!", - DisconnectReason.RemoteConnectionClose => "Server Shutting Down", - DisconnectReason.Timeout => "Server Timed Out", - _ => "Connection Failed" - }; - } - #endregion + DisconnectReason.UnknownHost => Locale.DISCONN_REASON__UNKNOWN_HOST, //"Unknown Host", + DisconnectReason.DisconnectPeerCalled => Locale.DISCONN_REASON__PLAYER_KICKED, //"Player Kicked", + DisconnectReason.ConnectionFailed => Locale.DISCONN_REASON__HOST_UNREACHABLE, //"Host Unreachable", + DisconnectReason.ConnectionRejected => Locale.DISCONN_REASON__REJECTED, //"Rejected!", + DisconnectReason.RemoteConnectionClose => Locale.DISCONN_REASON__SHUTTING_DOWN, //"Server Shutting Down", + DisconnectReason.Timeout => Locale.DISCONN_REASON__HOST_TIMED_OUT, //"Server Timed Out", + _ => "Connection Failed" + }; + } + #endregion - #region steam lobby - private async void ListActiveLobbies() - { - lobbies = await SteamMatchmaking.LobbyList.WithMaxResults(100) - .FilterDistanceWorldwide() - .WithSlotsAvailable(-1) - //.WithKeyValue(SteamworksUtils.MP_MOD_KEY, string.Empty) - .RequestAsync(); + #region steam lobby + private async void ListActiveLobbies() + { + lobbies = await SteamMatchmaking.LobbyList.WithMaxResults(100) + .FilterDistanceWorldwide() + .WithSlotsAvailable(-1) + //.WithKeyValue(SteamworksUtils.MP_MOD_KEY, string.Empty) + .RequestAsync(); - Multiplayer.LogDebug(() => $"ListActiveLobbies() lobbies found: {lobbies?.Count()}"); + Multiplayer.LogDebug(() => $"ListActiveLobbies() lobbies found: {lobbies?.Count()}"); - remoteServers.Clear(); + remoteServers.Clear(); - if (lobbies != null) - { - var myLoc = SteamNetworkingUtils.LocalPingLocation; + if (lobbies != null) + { + var myLoc = SteamNetworkingUtils.LocalPingLocation; - foreach (var lobby in lobbies) - { - LobbyServerData server = SteamworksUtils.GetLobbyData(lobby); + foreach (var lobby in lobbies) + { + LobbyServerData server = SteamworksUtils.GetLobbyData(lobby); - server.id = lobby.Id.ToString(); + server.id = lobby.Id.ToString(); - server.CurrentPlayers = lobby.MemberCount; - server.MaxPlayers = lobby.MaxMembers; + server.CurrentPlayers = lobby.MemberCount; + server.MaxPlayers = lobby.MaxMembers; - remoteServers.Add(server); + remoteServers.Add(server); - Multiplayer.LogDebug(() => $"ListActiveLobbies() lobby {server.Name}, {lobby.MemberCount}/{lobby.MaxMembers}"); + Multiplayer.LogDebug(() => $"ListActiveLobbies() lobby {server.Name}, {lobby.MemberCount}/{lobby.MaxMembers}"); - } } - remoteRefreshComplete = true; } + remoteRefreshComplete = true; + } - private void UpdatePingsSteam() + private void UpdatePingsSteam() + { + foreach (var server in serverGridView.Items) { - foreach (var server in serverGridView.Items) + if (server is LobbyServerData lobbyServer) { - if (server is LobbyServerData lobbyServer) + if (ulong.TryParse(server.id, out ulong id)) { - if (ulong.TryParse(server.id, out ulong id)) + Lobby? lobby = lobbies.FirstOrDefault(l => l.Id.Value == id); + if (lobby != null) { - Lobby? lobby = lobbies.FirstOrDefault(l => l.Id.Value == id); - if (lobby != null) - { - string strLoc = ((Lobby)lobby).GetData(SteamworksUtils.LOBBY_NET_LOCATION_KEY); - NetPingLocation? location = NetPingLocation.TryParseFromString(strLoc); + string strLoc = ((Lobby)lobby).GetData(SteamworksUtils.LOBBY_NET_LOCATION_KEY); + NetPingLocation? location = NetPingLocation.TryParseFromString(strLoc); - if (location != null) - server.Ping = SteamNetworkingUtils.EstimatePingTo((NetPingLocation)location) / 2; //normalise to one way ping - } + if (location != null) + server.Ping = SteamNetworkingUtils.EstimatePingTo((NetPingLocation)location) / 2; //normalise to one way ping } - - UpdateElement(lobbyServer); } + + UpdateElement(lobbyServer); } } + } - private Lobby? GetLobbyFromServer(IServerBrowserGameDetails server) - { - if (ulong.TryParse(server.id, out ulong id)) - return lobbies.FirstOrDefault(l => l.Id.Value == id); - - return null; - } - #endregion + private Lobby? GetLobbyFromServer(IServerBrowserGameDetails server) + { + if (ulong.TryParse(server.id, out ulong id)) + return lobbies.FirstOrDefault(l => l.Id.Value == id); - private void RefreshGridView() - { - // Get all active IDs - List activeIDs = remoteServers.Select(s => s.id).Distinct().ToList(); + return null; + } + #endregion - // Remove servers that no longer exist - for (int i = serverGridView.Items.Count - 1; i >= 0; i--) - { - if (!activeIDs.Contains(serverGridView.Items[i].id)) - { - serverGridView.RemoveItemAt(i); - } - } + private void RefreshGridView() + { + // Get all active IDs + List activeIDs = remoteServers.Select(s => s.id).Distinct().ToList(); - Multiplayer.LogDebug(() => $"RefreshGridView() prepare to update/add, remoteServers count: {remoteServers.Count}"); - // Update existing servers and add new ones - foreach (var server in remoteServers) + // Remove servers that no longer exist + for (int i = serverGridView.Items.Count - 1; i >= 0; i--) + { + if (!activeIDs.Contains(serverGridView.Items[i].id)) { - var existingServer = serverGridView.Items.FirstOrDefault(gv => gv.id == server.id); - if (existingServer != null) - { - Multiplayer.LogDebug(() => $"RefreshGridView() updating server"); - // Update existing server - existingServer.TimePassed = server.TimePassed; - existingServer.CurrentPlayers = server.CurrentPlayers; - existingServer.LocalIPv4 = server.LocalIPv4; - existingServer.LastSeen = server.LastSeen; - } - else - { - Multiplayer.LogDebug(() => $"RefreshGridView() adding server"); - // Add new server - serverGridView.AddItem(server); - } + serverGridView.RemoveItemAt(i); } } - private string ExtractDomainName(string input) + Multiplayer.LogDebug(() => $"RefreshGridView() prepare to update/add, remoteServers count: {remoteServers.Count}"); + // Update existing servers and add new ones + foreach (var server in remoteServers) { - if (input.StartsWith("http://")) + var existingServer = serverGridView.Items.FirstOrDefault(gv => gv.id == server.id); + if (existingServer != null) { - input = input.Substring(7); + Multiplayer.LogDebug(() => $"RefreshGridView() updating server"); + // Update existing server + existingServer.TimePassed = server.TimePassed; + existingServer.CurrentPlayers = server.CurrentPlayers; + existingServer.LocalIPv4 = server.LocalIPv4; + existingServer.LastSeen = server.LastSeen; } - else if (input.StartsWith("https://")) - { - input = input.Substring(8); - } - - int portIndex = input.IndexOf(':'); - if (portIndex != -1) + else { - input = input.Substring(0, portIndex); + Multiplayer.LogDebug(() => $"RefreshGridView() adding server"); + // Add new server + serverGridView.AddItem(server); } + } + } - return input; + private string ExtractDomainName(string input) + { + if (input.StartsWith("http://")) + { + input = input.Substring(7); + } + else if (input.StartsWith("https://")) + { + input = input.Substring(8); } - private async Task JoinLobby(Lobby lobby) + int portIndex = input.IndexOf(':'); + if (portIndex != -1) { + input = input.Substring(0, portIndex); + } - if (connectionState != ConnectionState.NotConnected) - { - Multiplayer.LogWarning($"Cannot join lobby while in state: {connectionState}"); - return false; - } + return input; + } - connectionState = ConnectionState.JoiningLobby; - Multiplayer.Log($"Attempting to join lobby ({lobby.Id})"); + private async Task JoinLobby(Lobby lobby) + { - var joinResult = await lobby.Join(); + if (connectionState != ConnectionState.NotConnected) + { + Multiplayer.LogWarning($"Cannot join lobby while in state: {connectionState}"); + return false; + } - if (joinResult == RoomEnter.Success) - { - Multiplayer.Log($"Lobby joined ({lobby.Id})"); + connectionState = ConnectionState.JoiningLobby; + Multiplayer.Log($"Attempting to join lobby ({lobby.Id})"); - joinedLobby = lobby; - lobbyToJoin = null; + var joinResult = await lobby.Join(); - string hasPass = lobby.GetData(SteamworksUtils.LOBBY_HAS_PASSWORD); - Multiplayer.Log($"Lobby ({lobby.Id}) has password: {hasPass}"); + if (joinResult == RoomEnter.Success) + { + Multiplayer.Log($"Lobby joined ({lobby.Id})"); - if (string.IsNullOrEmpty(hasPass) || hasPass == "False") - { - Multiplayer.Log($"Attempting connection..."); - InitiateConnection(); - } - else - { - connectionState = ConnectionState.AwaitingPassword; - Multiplayer.Log($"Prompting for password..."); - ShowPasswordPopup(); - } + joinedLobby = lobby; + lobbyToJoin = null; + + string hasPass = lobby.GetData(SteamworksUtils.LOBBY_HAS_PASSWORD); + Multiplayer.Log($"Lobby ({lobby.Id}) has password: {hasPass}"); - return true; + if (string.IsNullOrEmpty(hasPass) || hasPass == "False") + { + Multiplayer.Log($"Attempting connection..."); + InitiateConnection(); } else { - Multiplayer.LogDebug(() => "JoinLobby() Leaving Lobby"); - lobby.Leave(); - joinedLobby = null; - Multiplayer.Log($"Failed to join lobby: {joinResult}"); - AttemptFail(); + connectionState = ConnectionState.AwaitingPassword; + Multiplayer.Log($"Prompting for password..."); + ShowPasswordPopup(); } - return false; + return true; + } + else + { + Multiplayer.LogDebug(() => "JoinLobby() Leaving Lobby"); + lobby.Leave(); + joinedLobby = null; + Multiplayer.Log($"Failed to join lobby: {joinResult}"); + AttemptFail(); } + + return false; } } diff --git a/Multiplayer/Locale.cs b/Multiplayer/Locale.cs index d8294bdf..d8cb51e8 100644 --- a/Multiplayer/Locale.cs +++ b/Multiplayer/Locale.cs @@ -68,10 +68,14 @@ public static class Locale 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__PASSWORD_REQUIRED_YES => Get(SERVER_BROWSER__PASSWORD_REQUIRED_YES_KEY); + private const string SERVER_BROWSER__PASSWORD_REQUIRED_YES_KEY = $"{PREFIX_SERVER_BROWSER}/password_required_yes"; + public static string SERVER_BROWSER__PASSWORD_REQUIRED_NO => Get(SERVER_BROWSER__PASSWORD_REQUIRED_NO_KEY); + private const string SERVER_BROWSER__PASSWORD_REQUIRED_NO_KEY = $"{PREFIX_SERVER_BROWSER}/password_required_no"; + 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); @@ -86,8 +90,10 @@ public static class Locale private const string SERVER_BROWSER__MISMATCH_KEY = $"{PREFIX_SERVER_BROWSER}/mismatch"; public static string SERVER_BROWSER__MISSING => Get(SERVER_BROWSER__MISSING_KEY); private const string SERVER_BROWSER__MISSING_KEY = $"{PREFIX_SERVER_BROWSER}/missing"; + public static string SERVER_BROWSER__REQUIRED_MODS => Get(SERVER_BROWSER__REQUIRED_MODS_KEY); private const string SERVER_BROWSER__REQUIRED_MODS_KEY = $"{PREFIX_SERVER_BROWSER}/required_mods"; + public static string SERVER_BROWSER__EXTRA_MODS => Get(SERVER_BROWSER__EXTRA_MODS_KEY); private const string SERVER_BROWSER__EXTRA_MODS_KEY = $"{PREFIX_SERVER_BROWSER}/extra_mods"; public static string SERVER_BROWSER__INCOMPATIBLE => Get(SERVER_BROWSER__INCOMPATIBLE_KEY); @@ -137,146 +143,164 @@ public static class Locale #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 Disconnect Reason +public static string DISCONN_REASON__UNKNOWN_HOST => Get(DISCONN_REASON__UNKNOWN_HOST_KEY); +public const string DISCONN_REASON__UNKNOWN_HOST_KEY = $"{PREFIX_DISCONN_REASON}/unknown"; - 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__HOST_UNREACHABLE => Get(DISCONN_REASON__HOST_UNREACHABLE_KEY); +public const string DISCONN_REASON__HOST_UNREACHABLE_KEY = $"{PREFIX_DISCONN_REASON}/unreachable"; - 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__HOST_TIMED_OUT => Get(DISCONN_REASON__HOST_TIMED_OUT_KEY); +public const string DISCONN_REASON__HOST_TIMED_OUT_KEY = $"{PREFIX_DISCONN_REASON}/timeout"; - 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__REJECTED => Get(DISCONN_REASON__REJECTED_KEY); +public const string DISCONN_REASON__REJECTED_KEY = $"{PREFIX_DISCONN_REASON}/rejected"; - 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__SHUTTING_DOWN => Get(DISCONN_REASON__SHUTTING_DOWN_KEY); +public const string DISCONN_REASON__SHUTTING_DOWN_KEY = $"{PREFIX_DISCONN_REASON}/shutdown"; - 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__PLAYER_KICKED => Get(DISCONN_REASON__PLAYER_KICKED_KEY); +public const string DISCONN_REASON__PLAYER_KICKED_KEY = $"{PREFIX_DISCONN_REASON}/kicked"; - 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 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__MODS_INCOMPATIBLE => Get(DISCONN_REASON__MODS_INCOMPATIBLE_KEY); - public const string DISCONN_REASON__MODS_INCOMPATIBLE_KEY = $"{PREFIX_DISCONN_REASON}/mods_incompatible"; - #endregion +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"; - #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 DISCONN_REASON__FULL_SERVER => Get(DISCONN_REASON__FULL_SERVER_KEY); +public const string DISCONN_REASON__FULL_SERVER_KEY = $"{PREFIX_DISCONN_REASON}/full_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 DISCONN_REASON__MODS => Get(DISCONN_REASON__MODS_KEY); +public const string DISCONN_REASON__MODS_KEY = $"{PREFIX_DISCONN_REASON}/mods"; - #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 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 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 +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"; - #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"; +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 CHAT_WHISPER_NOT_FOUND => Get(CHAT_WHISPER_NOT_FOUND_KEY); - public const string CHAT_WHISPER_NOT_FOUND_KEY = $"{PREFIX_CHAT_INFO}/whisper/not_found"; +public static string DISCONN_REASON__MODS_INCOMPATIBLE => Get(DISCONN_REASON__MODS_INCOMPATIBLE_KEY); +public const string DISCONN_REASON__MODS_INCOMPATIBLE_KEY = $"{PREFIX_DISCONN_REASON}/mods_incompatible"; +#endregion - public static string CHAT_KICK_UNABLE => Get(CHAT_KICK_UNABLE_KEY); - public const string CHAT_KICK_UNABLE_KEY = $"{PREFIX_CHAT_INFO}/kick/unable"; - public static string CHAT_KICK_KICKED => Get(CHAT_KICK_KICKED_KEY); - public const string CHAT_KICK_KICKED_KEY = $"{PREFIX_CHAT_INFO}/kick/kicked"; +#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"; - #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 - #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"; +#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"; - public static string PAUSE_MENU_QUIT => Get(PAUSE_MENU_QUIT_KEY); - public const string PAUSE_MENU_QUIT_KEY = $"{PREFIX_PAUSE_MENU}/quit_msg"; - #endregion +public static string CHAT_WHISPER_NOT_FOUND => Get(CHAT_WHISPER_NOT_FOUND_KEY); +public const string CHAT_WHISPER_NOT_FOUND_KEY = $"{PREFIX_CHAT_INFO}/whisper/not_found"; - private static bool initializeAttempted; - private static ReadOnlyDictionary> csv; +public static string CHAT_KICK_UNABLE => Get(CHAT_KICK_UNABLE_KEY); +public const string CHAT_KICK_UNABLE_KEY = $"{PREFIX_CHAT_INFO}/kick/unable"; +public static string CHAT_KICK_KICKED => Get(CHAT_KICK_KICKED_KEY); +public const string CHAT_KICK_KICKED_KEY = $"{PREFIX_CHAT_INFO}/kick/kicked"; - 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; - } - - csv = Csv.Parse(File.ReadAllText(path)); - //Multiplayer.LogDebug(() => $"Locale dump: {Csv.Dump(csv)}"); - } - public static string Get(string key, string overrideLanguage = null) + +#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; + +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; + } + + 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"); + + if (csv == null) + return MISSING_TRANSLATION; + + string locale = overrideLanguage ?? LocalizationManager.CurrentLanguage; + if (!csv.ContainsKey(locale)) + { + if (locale == DEFAULT_LANGUAGE) { - if (!initializeAttempted) - throw new InvalidOperationException("Not initialized"); - - 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}"); - } - - 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}'!"); + 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; } - public static string Get(string key, params object[] placeholders) - { - return string.Format(Get(key), placeholders); - } + locale = DEFAULT_LANGUAGE; + Multiplayer.LogWarning($"Failed to find locale language {locale}"); + } - public static string Get(string key, params string[] placeholders) - { - return Get(key, (object[])placeholders); - } + 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/locale.csv b/locale.csv index 21cc2535..48b0b5a2 100644 --- a/locale.csv +++ b/locale.csv @@ -35,7 +35,8 @@ sb/port_invalid,Invalid port popup.,Invalid Port!,Невалиден порт!, 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 status label,Mods,Модове,模组,模組,Módy,Mods,Mods,Modit,Mods,Mods,मॉड,Modok,Mod,モッド,모드,Mods,Mody,Mods,Mods,Moduri,Модификации,Módy,Mods,Moddar,Modlar,Модифікації +sb/password_required_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/password_required_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/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,Так @@ -44,9 +45,9 @@ sb/ok,'OK' for details text,OK,Добре,是,是,Ano,Ja,Ja,Kyllä,Oui,Ja,हा sb/mismatch,'mismatch' for details text,Mismatch,Несъответствие,不匹配,不符,Nesoulad,Uoverensstemmelse,Niet-overeenkomend,Ristiriita,Non-concordance,Diskrepanz,बेमेल,Eltérés,Non corrispondenza,不一致,불일치,Avvik,Niezgodność,Incompatibilidade,Incompatibilidade,Nepotrivire,Несоответствие,Nesúlad,Discordancia,Oöverensstämmelse,Uyumsuzluk,Невідповідність sb/missing,'Missing' for mod status,Missing,Липсва,缺少,缺少,Chybí,Mangler,Ontbreekt,Puuttuu,Manquant,Fehlt,गायब,Hiányzik,Mancante,欠落,누락,Mangler,Brak,Ausente,Ausente,Lipsă,Отсутствует,Chýba,Falta,Saknas,Eksik,Відсутній sb/required_mods,'Required Mods' header,Required Mods,Необходими модификации,必需模组,必需模組,Požadované módy,Krævede mods,Vereiste mods,Vaaditut modit,Mods requis,Erforderliche Mods,आवश्यक मॉड,Szükséges modok,Mod richiesti,必須MOD,필수 모드,Nødvendige modifikasjoner,Wymagane mody,Mods necessários,Mods necessários,Moduri necesare,Требуемые моды,Požadované módy,Mods requeridos,Nödvändiga moddar,Gerekli modlar,Необхідні моди -sb/extra_mods,'Extra Mods' header,Extra Mods,Допълнителни модификации,额外模组,額外模組,Extra módy,Ekstra mods,Extra mods,Ylimääräiset modit,Mods supplémentaires,Zusätzliche Mods,अतिरिक्त मॉड,Extra modok,Mod extra,追加MOD,추가 모드,Ekstra modifikasjoner,Dodatkowe mody,Mods extras,Mods extras,Moduri suplimentare,Дополнительные моды,Extra módy,Mods adicionales,Extra moddar,Ekstra modlar,Додаткові моди +sb/extra_mods,'Extra Mods' header,Extra Mods,Допълнителни модификации,多余模组,額外模組,Extra módy,Ekstra mods,Extra mods,Ylimääräiset modit,Mods supplémentaires,Zusätzliche Mods,अतिरिक्त मॉड,Extra modok,Mod extra,追加MOD,추가 모드,Ekstra modifikasjoner,Dodatkowe mody,Mods extras,Mods extras,Moduri suplimentare,Дополнительные моды,Extra módy,Mods adicionales,Extra moddar,Ekstra modlar,Додаткові моди sb/incompatible,'Incompatible' for mod status,Incompatible,Несъвместим,不兼容,不相容,Nekompatibilní,Inkompatibel,Incompatibel,Yhteensopimaton,Incompatible,Inkompatibel,असंगत,Nem kompatibilis,Incompatibile,非互換,호환 불가,Inkompatibel,Niekompatybilny,Incompatível,Incompatível,Incompatibil,Несовместимо,Nekompatibilné,Incompatible,Inkompatibel,Uyumsuz,Несумісний -sb/extra_mod,'Extra Mod' status message,Extra Mod,Допълнителна модификация,额外模组,額外模組,Extra mód,Ekstra mod,Extra mod,Ylimääräinen modi,Mod supplémentaire,Zusätzlicher Mod,अतिरिक्त मॉड,Extra mod,Mod extra,追加MOD,추가 모드,Ekstra modifikasjon,Dodatkowy mod,Mod extra,Mod extra,Mod suplimentar,Дополнительный мод,Extra mód,Mod adicional,Extra modd,Ekstra mod,Додатковий мод +sb/extra_mod,'Extra Mod' status message,Extra Mod,Допълнителна модификация,多余模组,額外模組,Extra mód,Ekstra mod,Extra mod,Ylimääräinen modi,Mod supplémentaire,Zusätzlicher Mod,अतिरिक्त मॉड,Extra mod,Mod extra,追加MOD,추가 모드,Ekstra modifikasjon,Dodatkowy mod,Mod extra,Mod extra,Mod suplimentar,Дополнительный мод,Extra mód,Mod adicional,Extra modd,Ekstra mod,Додатковий мод 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!,Сервери не знайдено. Оновіть або почніть власний! From 4bba74496ab40a9d4b554cfbbd30c2a2b12c2b6f Mon Sep 17 00:00:00 2001 From: Macka Date: Thu, 8 Jan 2026 19:24:43 +1000 Subject: [PATCH 518/521] Remove duplicate host details label in server info Eliminates redundant display of the host details label in the server browser pane by appending only the server details string. This improves clarity and prevents repeated information in the server details section. --- Multiplayer/Components/MainMenu/ServerBrowserPane.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs index f3916d84..81a07683 100644 --- a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs +++ b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs @@ -561,7 +561,8 @@ private void UpdateDetailsPane() details.Append(FORMAT_ALPHA + Locale.SERVER_BROWSER__PLAYERS + ": " + selectedServer.CurrentPlayers + '/' + selectedServer.MaxPlayers + "
    "); details.Append(FORMAT_ALPHA + Locale.SERVER_BROWSER__PASSWORD_REQUIRED + ": " + (selectedServer.HasPassword ? Locale.SERVER_BROWSER__PASSWORD_REQUIRED_YES : Locale.SERVER_BROWSER__PASSWORD_REQUIRED_NO) + "
    "); details.Append(FORMAT_ALPHA + Locale.SERVER_BROWSER__GAME_VERSION + ": " + (selectedServer.GameVersion != MainMenuControllerPatch.MenuProvider.BuildVersionString ? "" : "") + selectedServer.GameVersion + "
    "); - details.Append($"{FORMAT_ALPHA}{Locale.SERVER_HOST_DETAILS}:
    " + selectedServer.ServerDetails); + + details.Append(selectedServer.ServerDetails); if (selectedServer.ServerDetails != null && selectedServer.ServerDetails.Length > 0) details.Append("
    "); From 3a80a19960f6455e0b9a3ef75bb6fac55bd66322 Mon Sep 17 00:00:00 2001 From: Macka Date: Thu, 8 Jan 2026 19:25:58 +1000 Subject: [PATCH 519/521] Fix issue preventing player markers from being hidden Updated references from 'gameParams' to 'GameParams' to match the correct property. --- Multiplayer/Components/Networking/Player/NetworkedWorldMap.cs | 2 +- Multiplayer/Networking/Managers/Client/NetworkClient.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Multiplayer/Components/Networking/Player/NetworkedWorldMap.cs b/Multiplayer/Components/Networking/Player/NetworkedWorldMap.cs index 0c702f03..0eb4866e 100644 --- a/Multiplayer/Components/Networking/Player/NetworkedWorldMap.cs +++ b/Multiplayer/Components/Networking/Player/NetworkedWorldMap.cs @@ -108,7 +108,7 @@ public void UpdatePlayers() WorldMapIndicatorRefs refs = kvp.Value; - bool active = Globals.G.gameParams.PlayerMarkerDisplayed; + bool active = Globals.G.GameParams.PlayerMarkerDisplayed; if (refs.gameObject.activeSelf != active) refs.gameObject.SetActive(active); if (!active) diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 41f4850f..c07b2f08 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -419,8 +419,8 @@ private void OnClientboundServerLoadingPacket(ClientboundServerLoadingPacket pac private void OnClientboundGameParamsPacket(ClientboundGameParamsPacket packet) { LogDebug(() => $"Received {nameof(ClientboundGameParamsPacket)} ({packet.SerializedGameParams.Length} chars)"); - if (Globals.G.gameParams != null) - packet.Apply(Globals.G.gameParams); + if (Globals.G.GameParams != null) + packet.Apply(Globals.G.GameParams); if (Globals.G.gameParamsInstance != null) packet.Apply(Globals.G.gameParamsInstance); } From 0a9bbbfd99807a23c8a601231d833534e2901df5 Mon Sep 17 00:00:00 2001 From: Macka Date: Thu, 8 Jan 2026 19:28:00 +1000 Subject: [PATCH 520/521] Ready for release 0.1.13.5 --- Multiplayer/Multiplayer.csproj | 2 +- info.json | 2 +- releases.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Multiplayer/Multiplayer.csproj b/Multiplayer/Multiplayer.csproj index 62b20deb..5fcb89f4 100644 --- a/Multiplayer/Multiplayer.csproj +++ b/Multiplayer/Multiplayer.csproj @@ -3,7 +3,7 @@ net48 latest Multiplayer - 0.1.13.4 + 0.1.13.5 diff --git a/info.json b/info.json index 0e7c9f15..e597f41e 100644 --- a/info.json +++ b/info.json @@ -1,6 +1,6 @@ { "Id": "Multiplayer", - "Version": "0.1.13.4", + "Version": "0.1.13.5", "DisplayName": "Multiplayer", "Author": "Insprill, Macka, Morm", "EntryMethod": "Multiplayer.Multiplayer.Load", diff --git a/releases.json b/releases.json index baad24ff..24e70580 100644 --- a/releases.json +++ b/releases.json @@ -1,6 +1,6 @@ { "Releases": [ - {"Id": "Multiplayer", "Version": "0.1.13.4", "DownloadUrl": "https://github.com/AMacro/dv-multiplayer/releases/download/v0.1.13.4-Beta/Multiplayer.0.1.13.4.zip"} + {"Id": "Multiplayer", "Version": "0.1.13.5", "DownloadUrl": "https://github.com/AMacro/dv-multiplayer/releases/download/v0.1.13.5-Beta/Multiplayer.0.1.13.5.zip"} ] } \ No newline at end of file From 06c5110a72ba78298f48c5dc862447b9924b0c84 Mon Sep 17 00:00:00 2001 From: Macka Date: Sat, 10 Jan 2026 12:33:28 +1000 Subject: [PATCH 521/521] Remove over the top logging --- Multiplayer/Components/Networking/Player/NetworkedWorldMap.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Multiplayer/Components/Networking/Player/NetworkedWorldMap.cs b/Multiplayer/Components/Networking/Player/NetworkedWorldMap.cs index 0eb4866e..c731a93a 100644 --- a/Multiplayer/Components/Networking/Player/NetworkedWorldMap.cs +++ b/Multiplayer/Components/Networking/Player/NetworkedWorldMap.cs @@ -113,7 +113,7 @@ public void UpdatePlayers() refs.gameObject.SetActive(active); if (!active) { - Multiplayer.LogDebug(() => $"NetworkedWorldMap.UpdatePlayers() key: {kvp.Key}, is NOT active"); + //Multiplayer.LogDebug(() => $"NetworkedWorldMap.UpdatePlayers() key: {kvp.Key}, is NOT active"); return; }