Merge pull request #622 from misternebula/dev

0.28.0
This commit is contained in:
_nebula 2023-05-08 14:52:43 +01:00 committed by GitHub
commit 688b239043
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 424 additions and 129 deletions

View File

@ -19,7 +19,7 @@ namespace EpicTransport {
private event Action OnConnected;
public event Action OnDisconnected;
// CHANGED
private Action<string> SetTransportError;
private event Action<TransportError, string> OnReceivedError;
private TimeSpan ConnectionTimeout;
@ -43,7 +43,7 @@ namespace EpicTransport {
c.OnDisconnected += () => transport.OnClientDisconnected.Invoke();
c.OnReceivedData += (data, channel) => transport.OnClientDataReceived.Invoke(new ArraySegment<byte>(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.");

View File

@ -40,9 +40,6 @@ namespace EpicTransport {
public ProductUserId productUserId;
private int packetId = 0;
// CHANGED
public Action<string> SetTransportError;
private void Awake() {
Debug.Assert(Channels != null && Channels.Length > 0, "No channel configured for EOS Transport.");

View File

@ -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<int> OnConnected;
private event Action<int, byte[], int> OnReceivedData;
private event Action<int> OnDisconnected;
private event Action<int, Exception> OnReceivedError;
// CHANGED
private event Action<int, TransportError, string> OnReceivedError;
private BidirectionalDictionary<ProductUserId, int> epicToMirrorIds;
private Dictionary<ProductUserId, SocketId> 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<byte>(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;
}
}

Binary file not shown.

Binary file not shown.

View File

@ -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;
/// <summary>
/// BUG: do conversation leave on player leave so other people can actually talk lol
/// </summary>
public class QSBCharacterDialogueTree : WorldObject<CharacterDialogueTree>
{
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<CharacterDialogueTree>
}
// 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);
}
}
}

View File

@ -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<DeathType>
var deathMessage = Necronomicon.GetPhrase(Data, NecronomiconIndex);
if (deathMessage != null)
{
DebugLog.ToAll(string.Format(deathMessage, playerName));
MultiplayerHUDManager.Instance.WriteMessage($"<color=brown>{string.Format(deathMessage, playerName)}</color>");
}
RespawnManager.Instance.OnPlayerDeath(player);

View File

@ -53,6 +53,9 @@ internal class GhostManager : WorldObjectManager
_zone2Director._cityGhosts[i].GetWorldObject<QSBGhostBrain>().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<SectorCollisionGroup>();
var city = allCollisionGroups.First(x => x.name == "City");
city.SetSector(_zone2Director._sector);

View File

@ -14,11 +14,6 @@ using System.Threading;
namespace QSB.EchoesOfTheEye.LightSensorSync.WorldObjects;
/// <summary>
/// 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.
/// </summary>
internal class QSBLightSensor : AuthWorldObject<SingleLightSensor>
{
internal bool _locallyIlluminated;

View File

@ -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<string>
{
public ChatMessage(string msg) : base(msg) { }
public override void OnReceiveLocal() => OnReceiveRemote();
public override void OnReceiveRemote()
{
MultiplayerHUDManager.Instance.WriteMessage(Data);
}
}

View File

@ -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<HUDIcon> HUDIconStack = new();
public static readonly ListStack<HUDIcon> HUDIconStack = new(true);
private void Start()
{
@ -59,6 +65,88 @@ internal class MultiplayerHUDManager : MonoBehaviour, IAddComponentOnStart
SpaceSprite = QSBCore.HUDAssetBundle.LoadAsset<Sprite>("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<string> _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>().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>();
_inputField.text = "";
_textChat.Find("Messages").Find("Message").GetComponent<Text>().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<MeshRenderer>().enabled = true;
}
else
{
player.MinimapPlayerMarker.localPosition = Vector3.zero;
player.MinimapPlayerMarker.localRotation = Quaternion.identity;
}
player.MinimapPlayerMarker.GetComponent<MeshRenderer>().enabled = false;
}
}
}
public void HideMinimap(Minimap minimap)
{
foreach (var player in QSBPlayerManager.PlayerList)
{
if (player.MinimapPlayerMarker == null)
{
continue;
}
player.MinimapPlayerMarker.GetComponent<MeshRenderer>().enabled = false;
}
}
public void ShowMinimap(Minimap minimap)
{
foreach (var player in QSBPlayerManager.PlayerList)
{
if (player.MinimapPlayerMarker == null)
{
continue;
}
player.MinimapPlayerMarker.GetComponent<MeshRenderer>().enabled = true;
}
}
@ -221,6 +373,9 @@ internal class MultiplayerHUDManager : MonoBehaviour, IAddComponentOnStart
private void OnRemovePlayer(PlayerInfo player)
{
Destroy(player.HUDBox?.gameObject);
Destroy(player.MinimapPlayerMarker);
WriteMessage($"<color=yellow>{string.Format(QSBLocalization.Current.PlayerLeftTheGame, player.Name)}</color>");
}
private PlanetTrigger CreateTrigger(string parentPath, HUDIcon icon)
@ -273,4 +428,4 @@ internal class MultiplayerHUDManager : MonoBehaviour, IAddComponentOnStart
return go;
}
}
}

View File

@ -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);
}
}
}

View File

@ -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;

View File

@ -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>().text = QSBLocalization.Current.MainMenuHost;
ConnectButton.transform.GetChild(0).GetChild(1).GetComponent<Text>().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<TitleScreenManager>()._gfxController;
var activeAlpha = 1;
var activeAlpha = titleAnimationController.IsTitleAnimationComplete() ? 1 : 0;
if (QSBSceneManager.CurrentScene == OWScene.TitleScreen)
{
var titleAnimationController = QSBWorldSync.GetUnityObject<TitleScreenManager>()._gfxController;
activeAlpha = titleAnimationController.IsTitleAnimationComplete() ? 1 : 0;
}
button.SetActive(active);
button.GetComponent<CanvasGroup>().alpha = active ? activeAlpha : 0;
@ -628,7 +633,7 @@ internal class MenuManager : MonoBehaviour, IAddComponentOnStart
SetButtonActive(NewGameButton, false);
_loadingText = HostButton.transform.GetChild(0).GetChild(1).GetComponent<Text>();
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);

View File

@ -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($"<color=green>{string.Format(QSBLocalization.Current.PlayerJoinedTheGame, player.Name)}</color>");
DebugLog.DebugWrite($"{player} joined. qsbVersion:{QSBVersion}, gameVersion:{GameVersion}, dlcInstalled:{DlcInstalled}", MessageType.Info);
}

View File

@ -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<string>
{
if (QSBPlayerManager.PlayerExists(PlayerId))
{
DebugLog.ToAll(string.Format(QSBLocalization.Current.PlayerWasKicked, QSBPlayerManager.GetPlayer(PlayerId).Name));
MultiplayerHUDManager.Instance.WriteMessage($"<color=red>{string.Format(QSBLocalization.Current.PlayerWasKicked, QSBPlayerManager.GetPlayer(PlayerId).Name)}</color>");
return;
}
DebugLog.ToAll(string.Format(QSBLocalization.Current.PlayerWasKicked, PlayerId));
MultiplayerHUDManager.Instance.WriteMessage($"<color=red>{string.Format(QSBLocalization.Current.PlayerWasKicked, PlayerId)}</color>");
return;
}
DebugLog.ToAll(string.Format(QSBLocalization.Current.KickedFromServer, Data));
MultiplayerHUDManager.Instance.WriteMessage($"<color=red>{string.Format(QSBLocalization.Current.KickedFromServer, Data)}</color>");
MenuManager.Instance.OnKicked(Data);
NetworkClient.Disconnect();

View File

@ -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<bool>("useKcpTransport") || DebugSettings.AutoStart;
QSBNetworkManager.UpdateTransport();
DefaultServerIP = config.GetSettingsValue<string>("defaultServerIP");
IncompatibleModsAllowed = config.GetSettingsValue<bool>("incompatibleModsAllowed");
ShowPlayerNames = config.GetSettingsValue<bool>("showPlayerNames");

View File

@ -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<string> OnClientDisconnected;
public event Action<TransportError, string> 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<kcp2k.KcpTransport>();
kcpTransport.Timeout = int.MaxValue; // effectively disables kcp ping and timeout (good for testing)
transport = kcpTransport;
_kcpTransport = gameObject.AddComponent<kcp2k.KcpTransport>();
}
else
{
// https://dev.epicgames.com/portal/en-US/qsb/sdk/credentials/qsb
var eosApiKey = ScriptableObject.CreateInstance<EosApiKey>();
@ -97,10 +92,9 @@ public class QSBNetworkManager : NetworkManager, IAddComponentOnStart
eosSdkComponent.apiKeys = eosApiKey;
eosSdkComponent.epicLoggerLevel = LogLevel.VeryVerbose;
var eosTransport = gameObject.AddComponent<EosTransport>();
eosTransport.SetTransportError = error => _lastTransportError = error;
transport = eosTransport;
_eosTransport = gameObject.AddComponent<EosTransport>();
}
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);
}
}

View File

@ -57,7 +57,7 @@ public class QuantumObjectPatches : QSBPatch
var worldObject = __instance.GetWorldObject<IQSBQuantumObject>();
var visibleToProbePlayers = worldObject.GetVisibleToProbePlayers();
__result = visibleToProbePlayers.Any(x => x.ProbeLauncherEquipped != default);
__result = visibleToProbePlayers.Any();
return false;
}
}

View File

@ -338,6 +338,13 @@ internal abstract class QSBQuantumObject<T> : WorldObject<T>, 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;
}

View File

@ -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);

View File

@ -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",

View File

@ -5,9 +5,6 @@ namespace QSB.Utility;
[JsonObject(MemberSerialization.OptIn)]
public class DebugSettings
{
[JsonProperty("useKcpTransport")]
public bool UseKcpTransport;
[JsonProperty("dumpWorldObjects")]
public bool DumpWorldObjects;

View File

@ -6,14 +6,23 @@ namespace QSB.Utility;
public class ListStack<T> : IEnumerable<T>
{
private readonly List<T> _items = new();
private List<T> _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<T>.Default.Equals(x, item));
}
@ -33,6 +42,27 @@ public class ListStack<T> : IEnumerable<T>
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;

View File

@ -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."
}
}
}

View File

@ -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" ],

View File

@ -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)