diff --git a/EpicOnlineTransport/Client.cs b/EpicOnlineTransport/Client.cs index 4ee1616b..1d6c90fc 100644 --- a/EpicOnlineTransport/Client.cs +++ b/EpicOnlineTransport/Client.cs @@ -19,7 +19,7 @@ namespace EpicTransport { private event Action OnConnected; public event Action OnDisconnected; // CHANGED - private Action SetTransportError; + private event Action OnReceivedError; private TimeSpan ConnectionTimeout; @@ -43,7 +43,7 @@ namespace EpicTransport { c.OnDisconnected += () => transport.OnClientDisconnected.Invoke(); c.OnReceivedData += (data, channel) => transport.OnClientDataReceived.Invoke(new ArraySegment(data), channel); // CHANGED - c.SetTransportError = transport.SetTransportError; + c.OnReceivedError += (error, reason) => transport.OnClientError?.Invoke(error, reason); return c; } @@ -64,7 +64,7 @@ namespace EpicTransport { if (await Task.WhenAny(connectedCompleteTask, Task.Delay(ConnectionTimeout/*, cancelToken.Token*/)) != connectedCompleteTask) { // CHANGED - SetTransportError($"Connection to {host} timed out."); + OnReceivedError?.Invoke(TransportError.Timeout, $"Connection to {host} timed out."); Debug.LogError($"Connection to {host} timed out."); OnConnected -= SetConnectedComplete; OnConnectionFailed(hostProductId); @@ -73,13 +73,13 @@ namespace EpicTransport { OnConnected -= SetConnectedComplete; } catch (FormatException) { // CHANGED - SetTransportError("Connection string was not in the right format. Did you enter a ProductId?"); + OnReceivedError?.Invoke(TransportError.DnsResolve, "Connection string was not in the right format. Did you enter a ProductId?"); Debug.LogError($"Connection string was not in the right format. Did you enter a ProductId?"); Error = true; OnConnectionFailed(hostProductId); } catch (Exception ex) { // CHANGED - SetTransportError(ex.Message); + OnReceivedError?.Invoke(TransportError.Unexpected, ex.Message); Debug.LogError(ex.Message); Error = true; OnConnectionFailed(hostProductId); @@ -158,7 +158,7 @@ namespace EpicTransport { break; case InternalMessages.DISCONNECT: // CHANGED - SetTransportError("host disconnected"); + OnReceivedError?.Invoke(TransportError.ConnectionClosed, "host disconnected"); Connected = false; Debug.Log("Disconnected."); diff --git a/EpicOnlineTransport/EosTransport.cs b/EpicOnlineTransport/EosTransport.cs index ca4c6015..9def9aa5 100644 --- a/EpicOnlineTransport/EosTransport.cs +++ b/EpicOnlineTransport/EosTransport.cs @@ -40,9 +40,6 @@ namespace EpicTransport { public ProductUserId productUserId; private int packetId = 0; - - // CHANGED - public Action SetTransportError; private void Awake() { Debug.Assert(Channels != null && Channels.Length > 0, "No channel configured for EOS Transport."); diff --git a/EpicOnlineTransport/Server.cs b/EpicOnlineTransport/Server.cs index 6cd8e7de..9047e9b8 100644 --- a/EpicOnlineTransport/Server.cs +++ b/EpicOnlineTransport/Server.cs @@ -1,5 +1,6 @@ using Epic.OnlineServices; using Epic.OnlineServices.P2P; +using Mirror; using System; using System.Collections.Generic; using UnityEngine; @@ -9,7 +10,8 @@ namespace EpicTransport { private event Action OnConnected; private event Action OnReceivedData; private event Action OnDisconnected; - private event Action OnReceivedError; + // CHANGED + private event Action OnReceivedError; private BidirectionalDictionary epicToMirrorIds; private Dictionary epicToSocketIds; @@ -23,7 +25,7 @@ namespace EpicTransport { s.OnDisconnected += (id) => transport.OnServerDisconnected.Invoke(id); s.OnReceivedData += (id, data, channel) => transport.OnServerDataReceived.Invoke(id, new ArraySegment(data), channel); // CHANGED - s.OnReceivedError += (id, exception) => transport.OnServerError?.Invoke(id, Mirror.TransportError.Unexpected, exception.ToString()); + s.OnReceivedError += (id, error, reason) => transport.OnServerError?.Invoke(id, error, reason); if (!EOSSDKComponent.Initialized) { Debug.LogError("EOS not initialized."); @@ -90,7 +92,8 @@ namespace EpicTransport { epicToSocketIds.Remove(clientUserId); Debug.Log($"Client with Product User ID {clientUserId} disconnected."); } else { - OnReceivedError.Invoke(-1, new Exception("ERROR Unknown Product User ID")); + // CHANGED + OnReceivedError?.Invoke(-1, TransportError.InvalidReceive, "ERROR Unknown Product User ID"); } break; @@ -116,7 +119,8 @@ namespace EpicTransport { clientUserId.ToString(out productId); Debug.LogError("Data received from epic client thats not known " + productId); - OnReceivedError.Invoke(-1, new Exception("ERROR Unknown product ID")); + // CHANGED + OnReceivedError?.Invoke(-1, TransportError.InvalidReceive, "ERROR Unknown product ID"); } } @@ -153,7 +157,8 @@ namespace EpicTransport { Send(userId, socketId, data, (byte)channelId); } else { Debug.LogError("Trying to send on unknown connection: " + connectionId); - OnReceivedError.Invoke(connectionId, new Exception("ERROR Unknown Connection")); + // CHANGED + OnReceivedError?.Invoke(connectionId, TransportError.InvalidSend, "ERROR Unknown Connection"); } } @@ -165,7 +170,8 @@ namespace EpicTransport { return userIdString; } else { Debug.LogError("Trying to get info on unknown connection: " + connectionId); - OnReceivedError.Invoke(connectionId, new Exception("ERROR Unknown Connection")); + // CHANGED + OnReceivedError?.Invoke(connectionId, TransportError.Unexpected, "ERROR Unknown Connection"); return string.Empty; } } diff --git a/QSB/AssetBundles/qsb_hud b/QSB/AssetBundles/qsb_hud index 3233cc3e..bf94b8c7 100644 Binary files a/QSB/AssetBundles/qsb_hud and b/QSB/AssetBundles/qsb_hud differ diff --git a/QSB/AssetBundles/qsb_network b/QSB/AssetBundles/qsb_network index 036ad744..4da4e11c 100644 Binary files a/QSB/AssetBundles/qsb_network and b/QSB/AssetBundles/qsb_network differ diff --git a/QSB/ConversationSync/WorldObjects/QSBCharacterDialogueTree.cs b/QSB/ConversationSync/WorldObjects/QSBCharacterDialogueTree.cs index 45fac711..a631214a 100644 --- a/QSB/ConversationSync/WorldObjects/QSBCharacterDialogueTree.cs +++ b/QSB/ConversationSync/WorldObjects/QSBCharacterDialogueTree.cs @@ -1,14 +1,26 @@ -using QSB.ConversationSync.Messages; +using Cysharp.Threading.Tasks; +using QSB.ConversationSync.Messages; using QSB.Messaging; +using QSB.Player; +using QSB.Utility; using QSB.WorldSync; +using System.Threading; +using UnityEngine; namespace QSB.ConversationSync.WorldObjects; -/// -/// BUG: do conversation leave on player leave so other people can actually talk lol -/// public class QSBCharacterDialogueTree : WorldObject { + public override async UniTask Init(CancellationToken ct) + { + QSBPlayerManager.OnRemovePlayer += OnRemovePlayer; + } + + public override void OnRemoval() + { + QSBPlayerManager.OnRemovePlayer -= OnRemovePlayer; + } + public override void SendInitialState(uint to) { var playerId = ConversationManager.Instance.GetPlayerTalkingToTree(AttachedObject); @@ -18,4 +30,15 @@ public class QSBCharacterDialogueTree : WorldObject } // TODO: maybe also sync the dialogue box and player box? } + + private void OnRemovePlayer(PlayerInfo player) + { + if (player.CurrentCharacterDialogueTree == this) + { + AttachedObject.GetInteractVolume().EnableInteraction(); + AttachedObject.RaiseEvent(nameof(CharacterDialogueTree.OnEndConversation)); + Object.Destroy(ConversationManager.Instance.BoxMappings[AttachedObject]); + Object.Destroy(player.CurrentDialogueBox); + } + } } diff --git a/QSB/DeathSync/Messages/PlayerDeathMessage.cs b/QSB/DeathSync/Messages/PlayerDeathMessage.cs index 9f2f2b9f..5aedbef6 100644 --- a/QSB/DeathSync/Messages/PlayerDeathMessage.cs +++ b/QSB/DeathSync/Messages/PlayerDeathMessage.cs @@ -1,5 +1,6 @@ using Mirror; using QSB.ClientServerStateSync; +using QSB.HUD; using QSB.Messaging; using QSB.Player; using QSB.RespawnSync; @@ -40,7 +41,7 @@ public class PlayerDeathMessage : QSBMessage var deathMessage = Necronomicon.GetPhrase(Data, NecronomiconIndex); if (deathMessage != null) { - DebugLog.ToAll(string.Format(deathMessage, playerName)); + MultiplayerHUDManager.Instance.WriteMessage($"{string.Format(deathMessage, playerName)}"); } RespawnManager.Instance.OnPlayerDeath(player); diff --git a/QSB/EchoesOfTheEye/Ghosts/GhostManager.cs b/QSB/EchoesOfTheEye/Ghosts/GhostManager.cs index 2fbec4a5..f5d48e37 100644 --- a/QSB/EchoesOfTheEye/Ghosts/GhostManager.cs +++ b/QSB/EchoesOfTheEye/Ghosts/GhostManager.cs @@ -53,6 +53,9 @@ internal class GhostManager : WorldObjectManager _zone2Director._cityGhosts[i].GetWorldObject().OnIdentifyIntruder += CustomOnCityGhostsIdentifiedIntruder; } + // the collision group sector is smaller than the one for ghost light sensors, + // so ghosts can see thru walls. + // fix this by just changing the collision group sector :P var allCollisionGroups = Resources.FindObjectsOfTypeAll(); var city = allCollisionGroups.First(x => x.name == "City"); city.SetSector(_zone2Director._sector); diff --git a/QSB/EchoesOfTheEye/LightSensorSync/WorldObjects/QSBLightSensor.cs b/QSB/EchoesOfTheEye/LightSensorSync/WorldObjects/QSBLightSensor.cs index 7a4f3ef2..44fb9a27 100644 --- a/QSB/EchoesOfTheEye/LightSensorSync/WorldObjects/QSBLightSensor.cs +++ b/QSB/EchoesOfTheEye/LightSensorSync/WorldObjects/QSBLightSensor.cs @@ -14,11 +14,6 @@ using System.Threading; namespace QSB.EchoesOfTheEye.LightSensorSync.WorldObjects; -/// -/// BUG: this breaks in zone2. -/// the sector it's enabled in is bigger than the sector the zone2 walls are enabled in :( -/// maybe this can be fixed by making the collision group use the same sector. -/// internal class QSBLightSensor : AuthWorldObject { internal bool _locallyIlluminated; diff --git a/QSB/HUD/Messages/ChatMessage.cs b/QSB/HUD/Messages/ChatMessage.cs new file mode 100644 index 00000000..a8404ade --- /dev/null +++ b/QSB/HUD/Messages/ChatMessage.cs @@ -0,0 +1,20 @@ +using QSB.Messaging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace QSB.HUD.Messages; + +internal class ChatMessage : QSBMessage +{ + public ChatMessage(string msg) : base(msg) { } + + public override void OnReceiveLocal() => OnReceiveRemote(); + + public override void OnReceiveRemote() + { + MultiplayerHUDManager.Instance.WriteMessage(Data); + } +} \ No newline at end of file diff --git a/QSB/HUD/MultiplayerHUDManager.cs b/QSB/HUD/MultiplayerHUDManager.cs index cac810e2..db963918 100644 --- a/QSB/HUD/MultiplayerHUDManager.cs +++ b/QSB/HUD/MultiplayerHUDManager.cs @@ -1,12 +1,16 @@ -using QSB.HUD.Messages; +using OWML.Common; +using QSB.HUD.Messages; +using QSB.Localization; using QSB.Messaging; using QSB.Player; using QSB.ServerSettings; using QSB.Utility; using QSB.WorldSync; +using System; using System.Linq; using UnityEngine; using UnityEngine.SceneManagement; +using UnityEngine.UI; namespace QSB.HUD; @@ -15,6 +19,8 @@ internal class MultiplayerHUDManager : MonoBehaviour, IAddComponentOnStart public static MultiplayerHUDManager Instance; private Transform _playerList; + private Transform _textChat; + private InputField _inputField; private Material _markerMaterial; public static Sprite UnknownSprite; @@ -32,7 +38,7 @@ internal class MultiplayerHUDManager : MonoBehaviour, IAddComponentOnStart public static Sprite Interloper; public static Sprite WhiteHole; - public static ListStack HUDIconStack = new(); + public static readonly ListStack HUDIconStack = new(true); private void Start() { @@ -59,6 +65,88 @@ internal class MultiplayerHUDManager : MonoBehaviour, IAddComponentOnStart SpaceSprite = QSBCore.HUDAssetBundle.LoadAsset("Assets/MULTIPLAYER_UI/playerbox_space.png"); } + private const int LINE_COUNT = 11; + private const int CHAR_COUNT = 41; + + private bool _writingMessage; + private readonly string[] _lines = new string[LINE_COUNT]; + // this should really be a deque, but eh + private readonly ListStack _messages = new(false); + + public void WriteMessage(string message) + { + /* Tricky problem to solve. + * - 11 available lines for text to fit onto + * - Each line can be max 41 characters + * - Newest messages apepear at the bottom, and get pushed up by newer messages. + * - Messages can use several lines. + * + * From newest to oldest message, work out how many lines it needs + * and set the lines correctly bottom-up. + */ + + _messages.Push(message); + + if (_messages.Count > LINE_COUNT) + { + _messages.RemoveFirstElementAndShift(); + } + + var currentLineIndex = 10; + + foreach (var msg in _messages.Reverse()) + { + var characterCount = msg.Length; + var linesNeeded = Mathf.CeilToInt((float)characterCount / CHAR_COUNT); + var chunk = 0; + for (var i = linesNeeded - 1; i >= 0; i--) + { + if (currentLineIndex - i < 0) + { + chunk++; + continue; + } + + var chunkString = string.Concat(msg.Skip(CHAR_COUNT * chunk).Take(CHAR_COUNT)); + _lines[currentLineIndex - i] = chunkString; + chunk++; + } + + currentLineIndex -= linesNeeded; + + if (currentLineIndex < 0) + { + break; + } + } + + var finalText = ""; + foreach (var line in _lines) + { + if (line == default) + { + finalText += Environment.NewLine; + } + else if (line.Length == 42) + { + finalText += line; + } + else + { + finalText += $"{line}{Environment.NewLine}"; + } + } + + _textChat.Find("Messages").Find("Message").GetComponent().text = finalText; + + if (Locator.GetPlayerSuit().IsWearingHelmet()) + { + var audioController = Locator.GetPlayerAudioController(); + audioController.PlayNotificationTextScrolling(); + Delay.RunFramesLater(10, () => audioController.StopNotificationTextScrolling()); + } + } + private void Update() { if (!QSBWorldSync.AllObjectsReady || _playerList == null) @@ -67,6 +155,34 @@ internal class MultiplayerHUDManager : MonoBehaviour, IAddComponentOnStart } _playerList.gameObject.SetActive(ServerSettingsManager.ShowExtraHUD); + + var inSuit = Locator.GetPlayerSuit().IsWearingHelmet(); + + if (OWInput.IsNewlyPressed(InputLibrary.enter, InputMode.Character) && !_writingMessage && inSuit) + { + OWInput.ChangeInputMode(InputMode.KeyboardInput); + _writingMessage = true; + _inputField.ActivateInputField(); + } + + if (OWInput.IsNewlyPressed(InputLibrary.enter, InputMode.KeyboardInput) && _writingMessage) + { + OWInput.RestorePreviousInputs(); + _writingMessage = false; + _inputField.DeactivateInputField(); + + var message = _inputField.text; + _inputField.text = ""; + message = message.Replace("\n", "").Replace("\r", ""); + message = $"{QSBPlayerManager.LocalPlayer.Name}: {message}"; + new ChatMessage(message).Send(); + } + + if (OWInput.IsNewlyPressed(InputLibrary.escape, InputMode.KeyboardInput) && _writingMessage) + { + OWInput.RestorePreviousInputs(); + _writingMessage = false; + } } private void OnWakeUp() @@ -119,6 +235,14 @@ internal class MultiplayerHUDManager : MonoBehaviour, IAddComponentOnStart HUDIconStack.Push(HUDIcon.TIMBER_HEARTH); new PlanetMessage(HUDIcon.TIMBER_HEARTH).Send(); + + _textChat = multiplayerGroup.transform.Find("TextChat"); + var inputFieldGO = _textChat.Find("InputField"); + _inputField = inputFieldGO.GetComponent(); + _inputField.text = ""; + _textChat.Find("Messages").Find("Message").GetComponent().text = ""; + _lines.Clear(); + _messages.Clear(); } public void UpdateMinimapMarkers(Minimap minimap) @@ -136,7 +260,7 @@ internal class MultiplayerHUDManager : MonoBehaviour, IAddComponentOnStart { if (player.Body != null) { - DebugLog.ToConsole($"Error - {player.PlayerId}'s RulesetDetector is null.", OWML.Common.MessageType.Error); + DebugLog.ToConsole($"Error - {player.PlayerId}'s RulesetDetector is null.", MessageType.Error); } continue; @@ -157,12 +281,40 @@ internal class MultiplayerHUDManager : MonoBehaviour, IAddComponentOnStart { player.MinimapPlayerMarker.localPosition = GetLocalMapPosition(player, minimap); player.MinimapPlayerMarker.LookAt(minimap._globeMeshTransform, minimap._globeMeshTransform.up); + player.MinimapPlayerMarker.GetComponent().enabled = true; } else { player.MinimapPlayerMarker.localPosition = Vector3.zero; player.MinimapPlayerMarker.localRotation = Quaternion.identity; - } + player.MinimapPlayerMarker.GetComponent().enabled = false; + } + } + } + + public void HideMinimap(Minimap minimap) + { + foreach (var player in QSBPlayerManager.PlayerList) + { + if (player.MinimapPlayerMarker == null) + { + continue; + } + + player.MinimapPlayerMarker.GetComponent().enabled = false; + } + } + + public void ShowMinimap(Minimap minimap) + { + foreach (var player in QSBPlayerManager.PlayerList) + { + if (player.MinimapPlayerMarker == null) + { + continue; + } + + player.MinimapPlayerMarker.GetComponent().enabled = true; } } @@ -221,6 +373,9 @@ internal class MultiplayerHUDManager : MonoBehaviour, IAddComponentOnStart private void OnRemovePlayer(PlayerInfo player) { Destroy(player.HUDBox?.gameObject); + Destroy(player.MinimapPlayerMarker); + + WriteMessage($"{string.Format(QSBLocalization.Current.PlayerLeftTheGame, player.Name)}"); } private PlanetTrigger CreateTrigger(string parentPath, HUDIcon icon) @@ -273,4 +428,4 @@ internal class MultiplayerHUDManager : MonoBehaviour, IAddComponentOnStart return go; } -} \ No newline at end of file +} diff --git a/QSB/HUD/Patches/MinimapPatches.cs b/QSB/HUD/Patches/MinimapPatches.cs index 07189831..0ba50671 100644 --- a/QSB/HUD/Patches/MinimapPatches.cs +++ b/QSB/HUD/Patches/MinimapPatches.cs @@ -12,6 +12,29 @@ internal class MinimapPatches : QSBPatch [HarmonyPatch(nameof(Minimap.UpdateMarkers))] public static void UpdateMarkers(Minimap __instance) { - MultiplayerHUDManager.Instance.UpdateMinimapMarkers(__instance); + if (__instance._minimapMode == Minimap.MinimapMode.Player) + { + MultiplayerHUDManager.Instance.UpdateMinimapMarkers(__instance); + } + } + + [HarmonyPostfix] + [HarmonyPatch(nameof(Minimap.HideMinimap))] + public static void HideMinimap(Minimap __instance) + { + if (__instance._minimapMode == Minimap.MinimapMode.Player) + { + MultiplayerHUDManager.Instance.HideMinimap(__instance); + } + } + + [HarmonyPostfix] + [HarmonyPatch(nameof(Minimap.ShowMinimap))] + public static void ShowMinimap(Minimap __instance) + { + if (__instance._minimapMode == Minimap.MinimapMode.Player) + { + MultiplayerHUDManager.Instance.ShowMinimap(__instance); + } } } diff --git a/QSB/Localization/Translation.cs b/QSB/Localization/Translation.cs index 72addba5..4ed59efe 100644 --- a/QSB/Localization/Translation.cs +++ b/QSB/Localization/Translation.cs @@ -35,6 +35,7 @@ public class Translation public string AddonMismatch; public string IncompatibleMod; public string PlayerJoinedTheGame; + public string PlayerLeftTheGame; public string PlayerWasKicked; public string KickedFromServer; public string RespawnPlayer; diff --git a/QSB/Menus/MenuManager.cs b/QSB/Menus/MenuManager.cs index 7d19164a..55166f55 100644 --- a/QSB/Menus/MenuManager.cs +++ b/QSB/Menus/MenuManager.cs @@ -1,5 +1,6 @@ using EpicTransport; using Mirror; +using OWML.Common; using QSB.Localization; using QSB.Messaging; using QSB.Player.TransformSync; @@ -111,17 +112,17 @@ internal class MenuManager : MonoBehaviour, IAddComponentOnStart _nowLoadingSB.Length = 0; } - private void OnLanguageChanged() + public void OnLanguageChanged() { if (QSBSceneManager.CurrentScene != OWScene.TitleScreen) { - DebugLog.ToConsole("Error - Language changed while not in title screen?! Should be impossible!", OWML.Common.MessageType.Error); + DebugLog.ToConsole("Error - Language changed while not in title screen?! Should be impossible!", MessageType.Error); return; } HostButton.transform.GetChild(0).GetChild(1).GetComponent().text = QSBLocalization.Current.MainMenuHost; ConnectButton.transform.GetChild(0).GetChild(1).GetComponent().text = QSBLocalization.Current.MainMenuConnect; - var text = QSBCore.DebugSettings.UseKcpTransport ? QSBLocalization.Current.PublicIPAddress : QSBLocalization.Current.ProductUserID; + var text = QSBCore.UseKcpTransport ? QSBLocalization.Current.PublicIPAddress : QSBLocalization.Current.ProductUserID; ConnectPopup.SetUpPopup(text, InputLibrary.menuConfirm, InputLibrary.cancel, new ScreenPrompt(QSBLocalization.Current.Connect), new ScreenPrompt(QSBLocalization.Current.Cancel), false); ConnectPopup.SetInputFieldPlaceholderText(text); ExistingNewCopyPopup.SetUpPopup(QSBLocalization.Current.HostExistingOrNewOrCopy, @@ -337,7 +338,7 @@ internal class MenuManager : MonoBehaviour, IAddComponentOnStart private void CreateCommonPopups() { - var text = QSBCore.DebugSettings.UseKcpTransport ? QSBLocalization.Current.PublicIPAddress : QSBLocalization.Current.ProductUserID; + var text = QSBCore.UseKcpTransport ? QSBLocalization.Current.PublicIPAddress : QSBLocalization.Current.ProductUserID; ConnectPopup = QSBCore.MenuApi.MakeInputFieldPopup(text, text, QSBLocalization.Current.Connect, QSBLocalization.Current.Cancel); ConnectPopup.CloseMenuOnOk(false); ConnectPopup.OnPopupConfirm += () => @@ -434,13 +435,17 @@ internal class MenuManager : MonoBehaviour, IAddComponentOnStart { if (button == null) { - DebugLog.DebugWrite($"Warning - Tried to set button to {active}, but it was null.", OWML.Common.MessageType.Warning); + DebugLog.DebugWrite($"Warning - Tried to set button to {active}, but it was null.", MessageType.Warning); return; } - var titleAnimationController = QSBWorldSync.GetUnityObject()._gfxController; + var activeAlpha = 1; - var activeAlpha = titleAnimationController.IsTitleAnimationComplete() ? 1 : 0; + if (QSBSceneManager.CurrentScene == OWScene.TitleScreen) + { + var titleAnimationController = QSBWorldSync.GetUnityObject()._gfxController; + activeAlpha = titleAnimationController.IsTitleAnimationComplete() ? 1 : 0; + } button.SetActive(active); button.GetComponent().alpha = active ? activeAlpha : 0; @@ -628,7 +633,7 @@ internal class MenuManager : MonoBehaviour, IAddComponentOnStart SetButtonActive(NewGameButton, false); _loadingText = HostButton.transform.GetChild(0).GetChild(1).GetComponent(); - if (!QSBCore.DebugSettings.UseKcpTransport) + if (!QSBCore.UseKcpTransport) { var productUserId = EOSSDKComponent.LocalUserProductIdString; @@ -684,8 +689,6 @@ internal class MenuManager : MonoBehaviour, IAddComponentOnStart Locator.GetMenuInputModule().DisableInputs(); QSBNetworkManager.singleton.networkAddress = address; - // hack to get disconnect call if start client fails immediately (happens on kcp transport when failing to resolve host name) - typeof(NetworkClient).GetProperty(nameof(NetworkClient.connection))!.SetValue(null, new NetworkConnectionToServer()); QSBNetworkManager.singleton.StartClient(); } @@ -714,7 +717,7 @@ internal class MenuManager : MonoBehaviour, IAddComponentOnStart OpenInfoPopup(string.Format(QSBLocalization.Current.ServerRefusedConnection, reason), QSBLocalization.Current.OK); } - private void OnDisconnected(string error) + private void OnDisconnected(TransportError error, string reason) { QSBCore.IsInMultiplayer = false; @@ -733,7 +736,7 @@ internal class MenuManager : MonoBehaviour, IAddComponentOnStart } }; - OpenInfoPopup(string.Format(QSBLocalization.Current.ClientDisconnectWithError, error), QSBLocalization.Current.OK); + OpenInfoPopup(string.Format(QSBLocalization.Current.ClientDisconnectWithError, reason), QSBLocalization.Current.OK); } SetButtonActive(DisconnectButton, false); diff --git a/QSB/Player/Messages/PlayerJoinMessage.cs b/QSB/Player/Messages/PlayerJoinMessage.cs index e5281c2b..6a8bc110 100644 --- a/QSB/Player/Messages/PlayerJoinMessage.cs +++ b/QSB/Player/Messages/PlayerJoinMessage.cs @@ -1,6 +1,7 @@ using HarmonyLib; using Mirror; using OWML.Common; +using QSB.HUD; using QSB.Localization; using QSB.Messaging; using QSB.Utility; @@ -125,7 +126,7 @@ public class PlayerJoinMessage : QSBMessage var player = QSBPlayerManager.GetPlayer(From); player.Name = PlayerName; - DebugLog.ToAll(string.Format(QSBLocalization.Current.PlayerJoinedTheGame, player.Name), MessageType.Info); + MultiplayerHUDManager.Instance.WriteMessage($"{string.Format(QSBLocalization.Current.PlayerJoinedTheGame, player.Name)}"); DebugLog.DebugWrite($"{player} joined. qsbVersion:{QSBVersion}, gameVersion:{GameVersion}, dlcInstalled:{DlcInstalled}", MessageType.Info); } diff --git a/QSB/Player/Messages/PlayerKickMessage.cs b/QSB/Player/Messages/PlayerKickMessage.cs index 3374e762..7cc62816 100644 --- a/QSB/Player/Messages/PlayerKickMessage.cs +++ b/QSB/Player/Messages/PlayerKickMessage.cs @@ -1,4 +1,5 @@ using Mirror; +using QSB.HUD; using QSB.Localization; using QSB.Menus; using QSB.Messaging; @@ -34,15 +35,15 @@ internal class PlayerKickMessage : QSBMessage { if (QSBPlayerManager.PlayerExists(PlayerId)) { - DebugLog.ToAll(string.Format(QSBLocalization.Current.PlayerWasKicked, QSBPlayerManager.GetPlayer(PlayerId).Name)); + MultiplayerHUDManager.Instance.WriteMessage($"{string.Format(QSBLocalization.Current.PlayerWasKicked, QSBPlayerManager.GetPlayer(PlayerId).Name)}"); return; } - DebugLog.ToAll(string.Format(QSBLocalization.Current.PlayerWasKicked, PlayerId)); + MultiplayerHUDManager.Instance.WriteMessage($"{string.Format(QSBLocalization.Current.PlayerWasKicked, PlayerId)}"); return; } - DebugLog.ToAll(string.Format(QSBLocalization.Current.KickedFromServer, Data)); + MultiplayerHUDManager.Instance.WriteMessage($"{string.Format(QSBLocalization.Current.KickedFromServer, Data)}"); MenuManager.Instance.OnKicked(Data); NetworkClient.Disconnect(); diff --git a/QSB/QSBCore.cs b/QSB/QSBCore.cs index c1442b2f..995e1ec8 100644 --- a/QSB/QSBCore.cs +++ b/QSB/QSBCore.cs @@ -58,10 +58,11 @@ public class QSBCore : ModBehaviour // ignore the last patch numbers like the title screen does Application.version.Split('.').Take(3).Join(delimiter: "."); public static bool DLCInstalled => EntitlementsManager.IsDlcOwned() == EntitlementsManager.AsyncOwnershipStatus.Owned; + public static bool UseKcpTransport { get; private set; } public static bool IncompatibleModsAllowed { get; private set; } public static bool ShowPlayerNames { get; private set; } public static bool ShipDamage { get; private set; } - public static bool ShowExtraHUDElements { get ; private set; } + public static bool ShowExtraHUDElements { get; private set; } public static GameVendor GameVendor { get; private set; } = GameVendor.None; public static bool IsStandalone => GameVendor is GameVendor.Epic or GameVendor.Steam; public static IProfileManager ProfileManager => IsStandalone @@ -155,7 +156,7 @@ public class QSBCore : ModBehaviour if (DebugSettings.AutoStart) { - DebugSettings.UseKcpTransport = true; + UseKcpTransport = true; DebugSettings.DebugMode = true; } @@ -254,6 +255,9 @@ public class QSBCore : ModBehaviour public override void Configure(IModConfig config) { + UseKcpTransport = config.GetSettingsValue("useKcpTransport") || DebugSettings.AutoStart; + QSBNetworkManager.UpdateTransport(); + DefaultServerIP = config.GetSettingsValue("defaultServerIP"); IncompatibleModsAllowed = config.GetSettingsValue("incompatibleModsAllowed"); ShowPlayerNames = config.GetSettingsValue("showPlayerNames"); diff --git a/QSB/QSBNetworkManager.cs b/QSB/QSBNetworkManager.cs index 477cee38..fb6156ff 100644 --- a/QSB/QSBNetworkManager.cs +++ b/QSB/QSBNetworkManager.cs @@ -12,6 +12,7 @@ using QSB.EchoesOfTheEye.EclipseDoors.VariableSync; using QSB.EchoesOfTheEye.EclipseElevators.VariableSync; using QSB.EchoesOfTheEye.RaftSync.TransformSync; using QSB.JellyfishSync.TransformSync; +using QSB.Menus; using QSB.Messaging; using QSB.ModelShip; using QSB.ModelShip.TransformSync; @@ -44,7 +45,7 @@ public class QSBNetworkManager : NetworkManager, IAddComponentOnStart public new static QSBNetworkManager singleton => (QSBNetworkManager)NetworkManager.singleton; public event Action OnClientConnected; - public event Action OnClientDisconnected; + public event Action OnClientDisconnected; public GameObject OrbPrefab { get; private set; } public GameObject ShipPrefab { get; private set; } @@ -64,24 +65,18 @@ public class QSBNetworkManager : NetworkManager, IAddComponentOnStart private GameObject _probePrefab; private bool _everConnected; - private string _lastTransportError; - private static readonly string[] _kcpErrorLogs = - { - "KcpPeer: received disconnect message", - "Failed to resolve host: .*" - }; + private (TransportError error, string reason) _lastTransportError = (TransportError.Unexpected, "transport did not give an error. uh oh"); + + private static kcp2k.KcpTransport _kcpTransport; + private static EosTransport _eosTransport; public override void Awake() { gameObject.SetActive(false); - if (QSBCore.DebugSettings.UseKcpTransport) { - var kcpTransport = gameObject.AddComponent(); - kcpTransport.Timeout = int.MaxValue; // effectively disables kcp ping and timeout (good for testing) - transport = kcpTransport; + _kcpTransport = gameObject.AddComponent(); } - else { // https://dev.epicgames.com/portal/en-US/qsb/sdk/credentials/qsb var eosApiKey = ScriptableObject.CreateInstance(); @@ -97,10 +92,9 @@ public class QSBNetworkManager : NetworkManager, IAddComponentOnStart eosSdkComponent.apiKeys = eosApiKey; eosSdkComponent.epicLoggerLevel = LogLevel.VeryVerbose; - var eosTransport = gameObject.AddComponent(); - eosTransport.SetTransportError = error => _lastTransportError = error; - transport = eosTransport; + _eosTransport = gameObject.AddComponent(); } + transport = QSBCore.UseKcpTransport ? _kcpTransport : _eosTransport; gameObject.SetActive(true); @@ -163,6 +157,22 @@ public class QSBNetworkManager : NetworkManager, IAddComponentOnStart ConfigureNetworkManager(); } + public static void UpdateTransport() + { + if (QSBCore.IsInMultiplayer) + { + return; + } + if (singleton != null) + { + singleton.transport = Transport.active = QSBCore.UseKcpTransport ? _kcpTransport : _eosTransport; + } + if (MenuManager.Instance != null) + { + MenuManager.Instance.OnLanguageChanged(); // hack to update text + } + } + private void InitPlayerName() => Delay.RunWhen(PlayerData.IsLoaded, () => { @@ -192,7 +202,6 @@ public class QSBNetworkManager : NetworkManager, IAddComponentOnStart PlayerName = "Player"; } - if (!QSBCore.DebugSettings.UseKcpTransport) { EOSSDKComponent.DisplayName = PlayerName; } @@ -225,26 +234,18 @@ public class QSBNetworkManager : NetworkManager, IAddComponentOnStart { networkAddress = QSBCore.DefaultServerIP; - if (QSBCore.DebugSettings.UseKcpTransport) { kcp2k.Log.Info = s => { DebugLog.DebugWrite("[KCP] " + s); - if (_kcpErrorLogs.Any(p => Regex.IsMatch(s, p))) + // hack + if (s == "KcpPeer: received disconnect message") { - _lastTransportError = s; + OnClientError(TransportError.ConnectionClosed, "host disconnected"); } }; - kcp2k.Log.Warning = s => - { - DebugLog.DebugWrite("[KCP] " + s, MessageType.Warning); - _lastTransportError = s; - }; - kcp2k.Log.Error = s => - { - DebugLog.DebugWrite("[KCP] " + s, MessageType.Error); - _lastTransportError = s; - }; + kcp2k.Log.Warning = s => DebugLog.DebugWrite("[KCP] " + s, MessageType.Warning); + kcp2k.Log.Error = s => DebugLog.DebugWrite("[KCP] " + s, MessageType.Error); } QSBSceneManager.OnPostSceneLoad += (_, loadScene) => @@ -342,8 +343,8 @@ public class QSBNetworkManager : NetworkManager, IAddComponentOnStart { DebugLog.DebugWrite("OnClientDisconnect"); base.OnClientDisconnect(); - OnClientDisconnected?.SafeInvoke(_lastTransportError); - _lastTransportError = null; + OnClientDisconnected?.SafeInvoke(_lastTransportError.error, _lastTransportError.reason); + _lastTransportError = (TransportError.Unexpected, "transport did not give an error. uh oh"); } public override void OnServerDisconnect(NetworkConnectionToClient conn) // Called on the server when any client disconnects @@ -409,4 +410,16 @@ public class QSBNetworkManager : NetworkManager, IAddComponentOnStart base.OnStopServer(); } + + public override void OnServerError(NetworkConnectionToClient conn, TransportError error, string reason) + { + DebugLog.DebugWrite($"OnServerError({conn}, {error}, {reason})", MessageType.Error); + _lastTransportError = (error, reason); + } + + public override void OnClientError(TransportError error, string reason) + { + DebugLog.DebugWrite($"OnClientError({error}, {reason})", MessageType.Error); + _lastTransportError = (error, reason); + } } diff --git a/QSB/QuantumSync/Patches/Common/QuantumObjectPatches.cs b/QSB/QuantumSync/Patches/Common/QuantumObjectPatches.cs index 716287a8..786a820b 100644 --- a/QSB/QuantumSync/Patches/Common/QuantumObjectPatches.cs +++ b/QSB/QuantumSync/Patches/Common/QuantumObjectPatches.cs @@ -57,7 +57,7 @@ public class QuantumObjectPatches : QSBPatch var worldObject = __instance.GetWorldObject(); var visibleToProbePlayers = worldObject.GetVisibleToProbePlayers(); - __result = visibleToProbePlayers.Any(x => x.ProbeLauncherEquipped != default); + __result = visibleToProbePlayers.Any(); return false; } } diff --git a/QSB/QuantumSync/WorldObjects/QSBQuantumObject.cs b/QSB/QuantumSync/WorldObjects/QSBQuantumObject.cs index a3ebd71e..e1f28d97 100644 --- a/QSB/QuantumSync/WorldObjects/QSBQuantumObject.cs +++ b/QSB/QuantumSync/WorldObjects/QSBQuantumObject.cs @@ -338,6 +338,13 @@ internal abstract class QSBQuantumObject : WorldObject, IQSBQuantumObject } label += $"VisibleInProbeSnapshot:{AttachedObject._visibleInProbeSnapshot}\r\n"; + label += $"IsLockedByProbeSnapshot:{AttachedObject.IsLockedByProbeSnapshot()}\r\n"; + label += "VisibleToProbePlayers:\r\n"; + + foreach (var player in GetVisibleToProbePlayers()) + { + label += $" ID: {player.PlayerId}\r\n"; + } return label; } diff --git a/QSB/RespawnSync/RespawnManager.cs b/QSB/RespawnSync/RespawnManager.cs index ef9112f7..c2808644 100644 --- a/QSB/RespawnSync/RespawnManager.cs +++ b/QSB/RespawnSync/RespawnManager.cs @@ -1,4 +1,5 @@ -using QSB.DeathSync.Messages; +using Mirror; +using QSB.DeathSync.Messages; using QSB.Messaging; using QSB.Patches; using QSB.Player; @@ -48,7 +49,7 @@ internal class RespawnManager : MonoBehaviour, IAddComponentOnStart } } - private void OnDisconnected(string error) + private void OnDisconnected(TransportError error, string reason) { _owRecoveryPoint?.SetActive(true); _qsbRecoveryPoint?.SetActive(false); diff --git a/QSB/Translations/en.json b/QSB/Translations/en.json index e0605cfc..a0430021 100644 --- a/QSB/Translations/en.json +++ b/QSB/Translations/en.json @@ -30,6 +30,7 @@ "AddonMismatch": "Addon mismatch. (Client:{0} addons, Server:{1} addons)", "IncompatibleMod": "Using an incompatible/disallowed mod. First mod found was {0}", "PlayerJoinedTheGame": "{0} joined!", + "PlayerLeftTheGame": "{0} left!", "PlayerWasKicked": "{0} was kicked.", "KickedFromServer": "Kicked from server. Reason : {0}", "RespawnPlayer": "Respawn Player", diff --git a/QSB/Utility/DebugSettings.cs b/QSB/Utility/DebugSettings.cs index 6d2267dc..c1da6cca 100644 --- a/QSB/Utility/DebugSettings.cs +++ b/QSB/Utility/DebugSettings.cs @@ -5,9 +5,6 @@ namespace QSB.Utility; [JsonObject(MemberSerialization.OptIn)] public class DebugSettings { - [JsonProperty("useKcpTransport")] - public bool UseKcpTransport; - [JsonProperty("dumpWorldObjects")] public bool DumpWorldObjects; diff --git a/QSB/Utility/ListStack.cs b/QSB/Utility/ListStack.cs index bfc7b81d..98cd65c9 100644 --- a/QSB/Utility/ListStack.cs +++ b/QSB/Utility/ListStack.cs @@ -6,14 +6,23 @@ namespace QSB.Utility; public class ListStack : IEnumerable { - private readonly List _items = new(); + private List _items = new(); + + public int Count => _items.Count; + + private readonly bool _removeDuplicates; + + public ListStack(bool removeDuplicates) + { + _removeDuplicates = removeDuplicates; + } public void Clear() => _items.Clear(); public void Push(T item) { - if (_items.Contains(item)) + if (_removeDuplicates && _items.Contains(item)) { RemoveAll(x => EqualityComparer.Default.Equals(x, item)); } @@ -33,6 +42,27 @@ public class ListStack : IEnumerable return default; } + public T RemoveFirstElementAndShift() + { + if (_items.Count == 0) + { + return default; + } + + var firstElement = _items[0]; + + if (_items.Count == 0) + { + return firstElement; + } + + // shift list left + // allocates blehhh who cares + _items = _items.GetRange(1, _items.Count - 1); + + return firstElement; + } + public T Peek() => _items.Count > 0 ? _items[_items.Count - 1] : default; diff --git a/QSB/default-config.json b/QSB/default-config.json index 7672cee1..99f11d5f 100644 --- a/QSB/default-config.json +++ b/QSB/default-config.json @@ -1,35 +1,42 @@ { - "enabled": true, - "settings": { - "defaultServerIP": { - "title": "Last Entered IP/ID", - "type": "text", - "value": "localhost", - "tooltip": "Used if you leave the connect prompt blank." - }, - "incompatibleModsAllowed": { - "title": "Incompatible Mods Allowed", - "type": "toggle", - "value": false, - "tooltip": "Kicks players if they have certain mods." - }, - "showPlayerNames": { - "title": "Show Player Names", - "type": "toggle", - "value": true, - "tooltip": "Shows player names in the HUD and the map view." - }, - "shipDamage": { - "title": "Ship Damage", - "type": "toggle", - "value": true, - "tooltip": "Take impact damage when inside the ship." - }, - "showExtraHud": { - "title": "Show Extra HUD Elements", - "type": "toggle", - "value": true, - "tooltip" : "Show extra HUD elements, like player status and minimap icons." - } - } + "$schema": "https://raw.githubusercontent.com/ow-mods/owml/master/schemas/config_schema.json", + "enabled": true, + "settings": { + "useKcpTransport": { + "title": "Use KCP Transport", + "type": "toggle", + "value": false, + "tooltip": "Use alternative transport that requires port forwarding but seems to be more reliable. The port to forward is 7777 as TCP/UDP. Use this if you are having trouble connecting. ALL PLAYERS MUST HAVE THIS AS THE SAME VALUE." + }, + "defaultServerIP": { + "title": "Last Entered IP/ID", + "type": "text", + "value": "localhost", + "tooltip": "Used if you leave the connect prompt blank." + }, + "incompatibleModsAllowed": { + "title": "Incompatible Mods Allowed", + "type": "toggle", + "value": false, + "tooltip": "Kicks players if they have certain mods." + }, + "showPlayerNames": { + "title": "Show Player Names", + "type": "toggle", + "value": true, + "tooltip": "Shows player names in the HUD and the map view." + }, + "shipDamage": { + "title": "Ship Damage", + "type": "toggle", + "value": true, + "tooltip": "Take impact damage when inside the ship." + }, + "showExtraHud": { + "title": "Show Extra HUD Elements", + "type": "toggle", + "value": true, + "tooltip": "Show extra HUD elements, like player status and minimap icons." + } + } } \ No newline at end of file diff --git a/QSB/manifest.json b/QSB/manifest.json index 5daedbd8..43a6dbda 100644 --- a/QSB/manifest.json +++ b/QSB/manifest.json @@ -7,7 +7,7 @@ "body": "- Disable *all* other mods. (Can heavily affect performance)\n- Make sure you are not running any other network-intensive applications." }, "uniqueName": "Raicuparta.QuantumSpaceBuddies", - "version": "0.27.0", + "version": "0.28.0", "owmlVersion": "2.9.0", "dependencies": [ "_nebula.MenuFramework", "JohnCorby.VanillaFix" ], "pathsToPreserve": [ "debugsettings.json" ], diff --git a/README.md b/README.md index 83c15d4d..76fa21a0 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,9 @@ Spoilers within! ## Frequently Asked Questions +### I keep timing out when trying to connect! +Check the mod settings for "Use KCP Transport". You have to forward port 7777 as TCP/UDP, or use Hamachi. ALL PLAYERS MUST HAVE THIS AS THE SAME VALUE. + ### Requirements - Latest version of OWML. - Latest version of Mod Manager. (If using) @@ -55,22 +58,28 @@ Spoilers within! ### How complete is this mod? How far through the game can I play? -The base game is around 95% done, whereas EotE is around 80% done. +You can play the entire game, plus DLC! +There still might be one or two small mechanics that aren't synced - let us know if you find an obvious one that we've missed. +Also, you might encounter bugs that mean you can't progress in multiplayer. Again, let us know if you find one! ### Compatibility with other mods TL;DR - Don't use any mods with QSB that aren't marked as QSB compatible. QSB relies on object hierarchy to sync objects, so any mod that changes that risks breaking QSB. Also, QSB relies on certain game events being called when things happen in-game. Any mod that makes these things happen without calling the correct events will break QSB. Some mods will work fine and have been tested, like CrouchMod. Others may only work partly, like EnableDebugMode and TAICheat. -### Will you make this compatible with NomaiVR? +### Is this mod compatible with NomaiVR? -Maybe. +Short answer - Kind of. + +Long answer - We've done our best to try to keep them compatible, but no work has been done to explicitly make them play nice. Some things may work, others may not. +Getting both mods to work together is a big undertaking, and would require rewrites to a lot of code in both mods. +If you want to play with VR, make sure the server host has "Incompatible Mods Allowed" enabled. ### Why do I keep getting thrown around the ship? Boring boring physics stuff. The velocity of the ship is synced, as well as the angular velocity. However, this velocity is not also applied to the player. (Or it is sometimes. I don't 100% know.) This means the ship will accelerate, leaving the player "behind". Which makes you fly into the walls alot. -**Update**: you can attach/detach yourself to/from the ship using the prompt in the center of the screen. +To fix this, whilst in the ship you can attach yourself to it. Look at the top-left of your screen when inside the ship for the buttons to press. ### What's the difference between QSB and Outer Wilds Online? @@ -131,7 +140,6 @@ The template for this file is this : ``` { - "useKcpTransport": false, "dumpWorldObjects": false, "instanceIdInLogs": false, "hookDebugLogs": false, @@ -149,7 +157,6 @@ The template for this file is this : } ``` -- useKcpTransport - Allows you to directly connect to IP addresses, rather than use the Epic relay. - dumpWorldObjects - Creates a file with information about the WorldObjects that were created. - instanceIdInLogs - Appends the game instance id to every log message sent. - hookDebugLogs - Print Unity logs and warnings. @@ -199,7 +206,6 @@ The template for this file is this : - [Mirror](https://mirror-networking.com/) - [kcp2k](https://github.com/vis2k/kcp2k) - [Telepathy](https://github.com/vis2k/Telepathy) - - [where-allocation](https://github.com/vis2k/where-allocation) - [EpicOnlineTransport](https://github.com/FakeByte/EpicOnlineTransport) - [HarmonyX](https://github.com/BepInEx/HarmonyX) - [UniTask](https://github.com/Cysharp/UniTask)