diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index cc5587821..e7bad546b 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,4 +1,3 @@ # These are supported funding model platforms -patreon: qsb custom: ['paypal.me/nebula2056/5', 'paypal.me/johncorby/5'] diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 535998dfc..1fa379e14 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -1,6 +1,11 @@ name: Build -on: push +on: + push: + paths-ignore: + - "*.md" + - "LICENSE" + - ".gitignore" jobs: build: diff --git a/APITestMod/APITestMod.cs b/APITestMod/APITestMod.cs index 50c26fc27..e3e7ad7f1 100644 --- a/APITestMod/APITestMod.cs +++ b/APITestMod/APITestMod.cs @@ -23,6 +23,11 @@ public void Start() qsbAPI.OnPlayerJoin().AddListener((uint playerId) => ModHelper.Console.WriteLine($"{playerId} joined the game!", MessageType.Success)); qsbAPI.OnPlayerLeave().AddListener((uint playerId) => ModHelper.Console.WriteLine($"{playerId} left the game!", MessageType.Success)); + qsbAPI.OnChatMessage().AddListener((string message, uint from) => ModHelper.Console.WriteLine($"Chat message \"{message}\" from {from} ({(from == uint.MaxValue ? "QSB" : qsbAPI.GetPlayerName(from))})")); + + qsbAPI.RegisterHandler("apitest-string", MessageHandler); + qsbAPI.RegisterHandler("apitest-int", MessageHandler); + qsbAPI.RegisterHandler("apitest-float", MessageHandler); button.onClick.AddListener(() => { @@ -42,16 +47,16 @@ public void Start() ModHelper.Console.WriteLine($"Retreiving custom data : {qsbAPI.GetCustomData(qsbAPI.GetLocalPlayerID(), "APITEST.TESTSTRING")}"); ModHelper.Console.WriteLine("Sending string message test..."); - qsbAPI.RegisterHandler("apitest-string", MessageHandler); qsbAPI.SendMessage("apitest-string", "STRING MESSAGE", receiveLocally: true); ModHelper.Console.WriteLine("Sending int message test..."); - qsbAPI.RegisterHandler("apitest-int", MessageHandler); qsbAPI.SendMessage("apitest-int", 123, receiveLocally: true); ModHelper.Console.WriteLine("Sending float message test..."); - qsbAPI.RegisterHandler("apitest-float", MessageHandler); qsbAPI.SendMessage("apitest-float", 3.14f, receiveLocally: true); + + qsbAPI.SendChatMessage("Non-system chat message", false, Color.white); + qsbAPI.SendChatMessage("System chat message", true, Color.cyan); }); }; } diff --git a/APITestMod/APITestMod.csproj b/APITestMod/APITestMod.csproj index e74118fbe..de5cb3b87 100644 --- a/APITestMod/APITestMod.csproj +++ b/APITestMod/APITestMod.csproj @@ -9,8 +9,8 @@ - - + + diff --git a/APITestMod/IQSBAPI.cs b/APITestMod/IQSBAPI.cs index 385bf8657..eadb98cea 100644 --- a/APITestMod/IQSBAPI.cs +++ b/APITestMod/IQSBAPI.cs @@ -1,14 +1,31 @@ using System; using OWML.Common; +using UnityEngine; using UnityEngine.Events; public interface IQSBAPI { + #region General + /// /// If called, all players connected to YOUR hosted game must have this mod installed. /// void RegisterRequiredForAllPlayers(IModBehaviour mod); + /// + /// Returns if the current player is the host. + /// + bool GetIsHost(); + + /// + /// Returns if the current player is in multiplayer. + /// + bool GetIsInMultiplayer(); + + #endregion + + #region Player + /// /// Returns the player ID of the current player. /// @@ -22,23 +39,25 @@ public interface IQSBAPI /// /// Returns the list of IDs of all connected players. + /// + /// The first player in the list is the host. /// uint[] GetPlayerIDs(); /// - /// Invoked when a player joins the game. + /// Invoked when any player (local or remote) joins the game. /// UnityEvent OnPlayerJoin(); /// - /// Invoked when a player leaves the game. + /// Invoked when any player (local or remote) leaves the game. /// UnityEvent OnPlayerLeave(); /// /// Sets some arbitrary data for a given player. /// - /// The type of the data. + /// The type of the data. If not serializable, data will not be synced. /// The ID of the player. /// The unique key to access this data by. /// The data to set. @@ -53,8 +72,14 @@ public interface IQSBAPI /// The data requested. If key is not valid, returns default. T GetCustomData(uint playerId, string key); + #endregion + + #region Messaging + /// /// Sends a message containing arbitrary data to every player. + /// + /// Keep your messages under around 1100 bytes. /// /// The type of the data being sent. This type must be serializable. /// The unique key of the message. @@ -70,4 +95,25 @@ public interface IQSBAPI /// The unique key of the message. /// The action to be ran when the message is received. The uint is the player ID that sent the messsage. void RegisterHandler(string messageType, Action handler); + + #endregion + + #region Chat + + /// + /// Invoked when a chat message is received. + /// The string is the message body. + /// The uint is the player who sent the message. If it's a system message, this is uint.MaxValue. + /// + UnityEvent OnChatMessage(); + + /// + /// Sends a message in chat. + /// + /// The text of the message. + /// If false, the message is sent as if the local player wrote it manually. If true, the message has no player attached to it, like the player join messages. + /// The color of the message. + void SendChatMessage(string message, bool systemMessage, Color color); + + #endregion } diff --git a/APITestMod/manifest.json b/APITestMod/manifest.json index 60efa966e..bf2d1eb90 100644 --- a/APITestMod/manifest.json +++ b/APITestMod/manifest.json @@ -4,6 +4,6 @@ "name": "QSB API Test Mod", "uniqueName": "_nebula.QSBAPITest", "version": "1.0.0", - "owmlVersion": "2.9.5", + "owmlVersion": "2.11.1", "dependencies": [ "Raicuparta.QuantumSpaceBuddies", "_nebula.MenuFramework" ] } diff --git a/Asset Source Files/player-simulation-model.blend b/Asset Source Files/player-simulation-model.blend new file mode 100644 index 000000000..e5fc2cd96 Binary files /dev/null and b/Asset Source Files/player-simulation-model.blend differ diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 2277524a7..b5c0cce98 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -1,8 +1,8 @@ -> :warning: Warning! :warning: -Mod development needs a powerful PC! -Unexpected errors and issues may occur when editing networking code. -Running multiple instances of the game can be very taxing on your computer. -We're not responsible if you push your PC too hard. +> [!WARNING] +> Mod development needs a powerful PC!\ +> Unexpected errors and issues may occur when editing networking code.\ +> Running multiple instances of the game can be very taxing on your computer.\ +> We're not responsible if you push your PC too hard. ## Prerequisites - Visual Studio 2022. @@ -19,12 +19,12 @@ We recommend using the Outer Wilds Mod Manager, but you can use OWML on its own - New Manager : Press the "..." button at the top, and select "Show OWML Folder". - `QSB.sln` should now be ready to open. ***This solution needs to be opened with Visual Studio 2022 or higher!*** -## Steam +## Multiple instances on Steam If using the Steam version of Outer Wilds, you will need to create a file to allow you to run multiple instances of the game. - Navigate to your game install folder. You can find this by right-clicking on the game in Steam, and going `Manage > Browse local files`. - Create a file named `steam_appid.txt`. -- In this file, write `753640` and save. -This file will override some Steam DRM features and allow the game to be ran multiple times at once. +- In this file, write `753640` and save. This file will override some Steam DRM features and allow the game to be ran multiple times at once. +- Either turn on "Force Exe" in the mod manager, or run OuterWilds.exe directly. ## Building Simply build the solution normally. (`Build > Build Solution` or CTRL-SHIFT-B) @@ -48,54 +48,60 @@ Use the API by copying [the API definition](https://github.com/misternebula/quan ## Debugging ### Debug Actions : +> [!NOTE] +> this list is slightly outdated. it will be updated when debug settings are updated + +Press Q + Numpad Enter to toggle debug mode in game (corresponds with the debug setting "debugMode" in the section below). + Hold Q and press : - Numpad 1 - Teleport to nearest player. -- Numpad 2 - If holding LeftShift, warp to the dreamworld Vault fire. If not, warp to the Endless Canyon. +- Numpad 2 - If holding LeftShift, warp to the dreamworld Vault fire. If not, warp to the Endless Canyon. If already in dreamworld, pick up lantern. - Numpad 3 - Unlock the Sealed Vault. - Numpad 4 - Damage the ship's electrical system. - Numpad 5 - Trigger the supernova. - Numpad 6 - Set the flags for having met Solanum and the Prisoner. -- Numpad 7 - Warp to the Vessel. -- Numpad 8 - Insert the Advanced Warp Core into the Vessel. +- Numpad 7 - Warp to the Vessel and insert the warp core. +- Numpad 8 - Spawn a fake player. For Ghostbuster testing. - Numpad 9 - If holding LeftShift, load the SolarSystem scene. If not, load the EyeOfTheUniverse scene. - Numpad 0 - Revive a random dead player. ### Debug Settings : -Create a file called `debugsettings.json` in the mod folder. +> [!NOTE] +> this list is slightly outdated because it will be replaced by mod options at some point + +Create a file called `debugsettings.json` in the QSB folder. The template for this file is this : ```json { - "dumpWorldObjects": false, "instanceIdInLogs": false, "hookDebugLogs": false, "avoidTimeSync": false, "autoStart": false, "kickEveryone": false, "disableLoopDeath": false, + "timeout": 25, "debugMode": false, "drawGui": false, "drawLines": false, "drawLabels": false, - "drawQuantumVisibilityObjects": false, "drawGhostAI": false, "greySkybox": false } ``` -- 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. - avoidTimeSync - Disables the syncing of time. - autoStart - Host/connect automatically for faster testing. - kickEveryone - Kick anyone who joins a game. - disableLoopDeath - Make it so the loop doesn't end when everyone is dead. +- timeout - How many seconds for your connection to timeout, in seconds. - debugMode - Enables debug mode. If this is set to `false`, none of the following settings do anything. - drawGui - Draws a GUI at the top of the screen that gives information on many things. - drawLines - Draws gizmo-esque lines around things. Indicates reference sectors/transforms, triggers, etc. LAGGY. - drawLabels - Draws GUI labels attached to some objects. LAGGY. -- drawQuantumVisibilityObjects - Indicates visibility objects with an orange shape. - drawGhostAI - Draws debug lines and labels just for the ghosts. - greySkybox - Turns the skybox grey. Useful in the Eye, where it's pretty dark. diff --git a/DevEnv.template.targets b/DevEnv.template.targets index 65a271378..d63dfd3fa 100644 --- a/DevEnv.template.targets +++ b/DevEnv.template.targets @@ -1,5 +1,4 @@ - - + $(AppData)\OuterWildsModManager\OWML $(SolutionDir)\qsb-unityproject\Assets diff --git a/Directory.Build.props b/Directory.Build.props index 468bd66a5..33acd137a 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -7,12 +7,11 @@ net48 - default + latest true false false - Henry Pointer, Aleksander Waage, Ricardo Lopes - Copyright © Henry Pointer, Aleksander Waage, Ricardo Lopes 2020-2021 + MSB3270 diff --git a/EpicOnlineTransport/BidirectionalDictionary.cs b/EpicOnlineTransport/BidirectionalDictionary.cs deleted file mode 100644 index cc68a0193..000000000 --- a/EpicOnlineTransport/BidirectionalDictionary.cs +++ /dev/null @@ -1,120 +0,0 @@ -using System.Collections; -using System.Collections.Generic; - -/// -/// Copyright -/// MIT License -/// -/// Copyright Fizz Cube Ltd(c) 2018 -/// -/// 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. -/// -/// === -/// -/// Copyright Marco Hoffmann(c) 2020 -/// -/// 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. -/// -/// MIT License -/// - -namespace EpicTransport { - - public class BidirectionalDictionary : IEnumerable { - private Dictionary t1ToT2Dict = new Dictionary(); - private Dictionary t2ToT1Dict = new Dictionary(); - - public IEnumerable FirstTypes => t1ToT2Dict.Keys; - public IEnumerable SecondTypes => t2ToT1Dict.Keys; - - public IEnumerator GetEnumerator() => t1ToT2Dict.GetEnumerator(); - - public int Count => t1ToT2Dict.Count; - - public void Add(T1 key, T2 value) { - t1ToT2Dict[key] = value; - t2ToT1Dict[value] = key; - } - - public void Add(T2 key, T1 value) { - t2ToT1Dict[key] = value; - t1ToT2Dict[value] = key; - } - - public T2 Get(T1 key) => t1ToT2Dict[key]; - - public T1 Get(T2 key) => t2ToT1Dict[key]; - - public bool TryGetValue(T1 key, out T2 value) => t1ToT2Dict.TryGetValue(key, out value); - - public bool TryGetValue(T2 key, out T1 value) => t2ToT1Dict.TryGetValue(key, out value); - - public bool Contains(T1 key) => t1ToT2Dict.ContainsKey(key); - - public bool Contains(T2 key) => t2ToT1Dict.ContainsKey(key); - - public void Remove(T1 key) { - if (Contains(key)) { - T2 val = t1ToT2Dict[key]; - t1ToT2Dict.Remove(key); - t2ToT1Dict.Remove(val); - } - } - public void Remove(T2 key) { - if (Contains(key)) { - T1 val = t2ToT1Dict[key]; - t1ToT2Dict.Remove(val); - t2ToT1Dict.Remove(key); - } - } - - public T1 this[T2 key] { - get => t2ToT1Dict[key]; - set { - t2ToT1Dict[key] = value; - t1ToT2Dict[value] = key; - } - } - - public T2 this[T1 key] { - get => t1ToT2Dict[key]; - set { - t1ToT2Dict[key] = value; - t2ToT1Dict[value] = key; - } - } - - } -} \ No newline at end of file diff --git a/EpicOnlineTransport/Client.cs b/EpicOnlineTransport/Client.cs deleted file mode 100644 index 1d6c90fc0..000000000 --- a/EpicOnlineTransport/Client.cs +++ /dev/null @@ -1,178 +0,0 @@ -using Epic.OnlineServices; -using Epic.OnlineServices.P2P; -using Mirror; -using System; -using System.Threading; -using System.Threading.Tasks; -using UnityEngine; - -namespace EpicTransport { - public class Client : Common { - - public SocketId socketId; - public ProductUserId serverId; - - public bool Connected { get; private set; } - public bool Error { get; private set; } - - private event Action OnReceivedData; - private event Action OnConnected; - public event Action OnDisconnected; - // CHANGED - private event Action OnReceivedError; - - private TimeSpan ConnectionTimeout; - - public bool isConnecting = false; - public string hostAddress = ""; - private ProductUserId hostProductId = null; - private TaskCompletionSource connectedComplete; - private CancellationTokenSource cancelToken; - - private Client(EosTransport transport) : base(transport) { - ConnectionTimeout = TimeSpan.FromSeconds(Math.Max(1, transport.timeout)); - } - - public static Client CreateClient(EosTransport transport, string host) { - Client c = new Client(transport); - - c.hostAddress = host; - c.socketId = new SocketId() { SocketName = RandomString.Generate(20) }; - - c.OnConnected += () => transport.OnClientConnected.Invoke(); - c.OnDisconnected += () => transport.OnClientDisconnected.Invoke(); - c.OnReceivedData += (data, channel) => transport.OnClientDataReceived.Invoke(new ArraySegment(data), channel); - // CHANGED - c.OnReceivedError += (error, reason) => transport.OnClientError?.Invoke(error, reason); - - return c; - } - - public async void Connect(string host) { - cancelToken = new CancellationTokenSource(); - - try { - hostProductId = ProductUserId.FromString(host); - serverId = hostProductId; - connectedComplete = new TaskCompletionSource(); - - OnConnected += SetConnectedComplete; - - SendInternal(hostProductId, socketId, InternalMessages.CONNECT); - - Task connectedCompleteTask = connectedComplete.Task; - - if (await Task.WhenAny(connectedCompleteTask, Task.Delay(ConnectionTimeout/*, cancelToken.Token*/)) != connectedCompleteTask) { - // CHANGED - OnReceivedError?.Invoke(TransportError.Timeout, $"Connection to {host} timed out."); - Debug.LogError($"Connection to {host} timed out."); - OnConnected -= SetConnectedComplete; - OnConnectionFailed(hostProductId); - } - - OnConnected -= SetConnectedComplete; - } catch (FormatException) { - // CHANGED - 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 - OnReceivedError?.Invoke(TransportError.Unexpected, ex.Message); - Debug.LogError(ex.Message); - Error = true; - OnConnectionFailed(hostProductId); - } finally { - if (Error) { - OnConnectionFailed(null); - } - } - - } - - public void Disconnect() { - if (serverId != null) { - CloseP2PSessionWithUser(serverId, socketId); - - serverId = null; - } else { - return; - } - - SendInternal(hostProductId, socketId, InternalMessages.DISCONNECT); - - Dispose(); - cancelToken?.Cancel(); - - WaitForClose(hostProductId, socketId); - } - - private void SetConnectedComplete() => connectedComplete.SetResult(connectedComplete.Task); - - protected override void OnReceiveData(byte[] data, ProductUserId clientUserId, int channel) { - if (ignoreAllMessages) { - return; - } - - if (clientUserId != hostProductId) { - Debug.LogError("Received a message from an unknown"); - return; - } - - OnReceivedData.Invoke(data, channel); - } - - protected override void OnNewConnection(OnIncomingConnectionRequestInfo result) { - if (ignoreAllMessages) { - return; - } - - if (deadSockets.Contains(result.SocketId.SocketName)) { - Debug.LogError("Received incoming connection request from dead socket"); - return; - } - - if (hostProductId == result.RemoteUserId) { - EOSSDKComponent.GetP2PInterface().AcceptConnection( - new AcceptConnectionOptions() { - LocalUserId = EOSSDKComponent.LocalUserProductId, - RemoteUserId = result.RemoteUserId, - SocketId = result.SocketId - }); - } else { - Debug.LogError("P2P Acceptance Request from unknown host ID."); - } - } - - protected override void OnReceiveInternalData(InternalMessages type, ProductUserId clientUserId, SocketId socketId) { - if (ignoreAllMessages) { - return; - } - - switch (type) { - case InternalMessages.ACCEPT_CONNECT: - Connected = true; - OnConnected.Invoke(); - Debug.Log("Connection established."); - break; - case InternalMessages.DISCONNECT: - // CHANGED - OnReceivedError?.Invoke(TransportError.ConnectionClosed, "host disconnected"); - Connected = false; - Debug.Log("Disconnected."); - - OnDisconnected.Invoke(); - break; - default: - Debug.Log("Received unknown message type"); - break; - } - } - - public void Send(byte[] data, int channelId) => Send(hostProductId, socketId, data, (byte) channelId); - - protected override void OnConnectionFailed(ProductUserId remoteId) => OnDisconnected.Invoke(); - public void EosNotInitialized() => OnDisconnected.Invoke(); - } -} \ No newline at end of file diff --git a/EpicOnlineTransport/Common.cs b/EpicOnlineTransport/Common.cs deleted file mode 100644 index 57abf3da3..000000000 --- a/EpicOnlineTransport/Common.cs +++ /dev/null @@ -1,288 +0,0 @@ - -using Epic.OnlineServices; -using Epic.OnlineServices.P2P; -using System; -using System.Collections; -using System.Collections.Generic; -using UnityEngine; - -namespace EpicTransport { - public abstract class Common { - - private PacketReliability[] channels; - private int internal_ch => channels.Length; - - protected enum InternalMessages : byte { - CONNECT, - ACCEPT_CONNECT, - DISCONNECT - } - - protected struct PacketKey { - public ProductUserId productUserId; - public byte channel; - } - - private OnIncomingConnectionRequestCallback OnIncomingConnectionRequest; - ulong incomingNotificationId = 0; - private OnRemoteConnectionClosedCallback OnRemoteConnectionClosed; - ulong outgoingNotificationId = 0; - - protected readonly EosTransport transport; - - protected List deadSockets; - public bool ignoreAllMessages = false; - - // Mapping from PacketKey to a List of Packet Lists - protected Dictionary>> incomingPackets = new Dictionary>>(); - - protected Common(EosTransport transport) { - channels = transport.Channels; - - deadSockets = new List(); - - AddNotifyPeerConnectionRequestOptions addNotifyPeerConnectionRequestOptions = new AddNotifyPeerConnectionRequestOptions(); - addNotifyPeerConnectionRequestOptions.LocalUserId = EOSSDKComponent.LocalUserProductId; - addNotifyPeerConnectionRequestOptions.SocketId = null; - - OnIncomingConnectionRequest += OnNewConnection; - OnRemoteConnectionClosed += OnConnectFail; - - incomingNotificationId = EOSSDKComponent.GetP2PInterface().AddNotifyPeerConnectionRequest(addNotifyPeerConnectionRequestOptions, - null, OnIncomingConnectionRequest); - - AddNotifyPeerConnectionClosedOptions addNotifyPeerConnectionClosedOptions = new AddNotifyPeerConnectionClosedOptions(); - addNotifyPeerConnectionClosedOptions.LocalUserId = EOSSDKComponent.LocalUserProductId; - addNotifyPeerConnectionClosedOptions.SocketId = null; - - outgoingNotificationId = EOSSDKComponent.GetP2PInterface().AddNotifyPeerConnectionClosed(addNotifyPeerConnectionClosedOptions, - null, OnRemoteConnectionClosed); - - if (outgoingNotificationId == 0 || incomingNotificationId == 0) { - Debug.LogError("Couldn't bind notifications with P2P interface"); - } - - incomingPackets = new Dictionary>>(); - - this.transport = transport; - - } - - protected void Dispose() { - EOSSDKComponent.GetP2PInterface().RemoveNotifyPeerConnectionRequest(incomingNotificationId); - EOSSDKComponent.GetP2PInterface().RemoveNotifyPeerConnectionClosed(outgoingNotificationId); - - transport.ResetIgnoreMessagesAtStartUpTimer(); - } - - protected abstract void OnNewConnection(OnIncomingConnectionRequestInfo result); - - private void OnConnectFail(OnRemoteConnectionClosedInfo result) { - if (ignoreAllMessages) { - return; - } - - OnConnectionFailed(result.RemoteUserId); - - switch (result.Reason) { - case ConnectionClosedReason.ClosedByLocalUser: - throw new Exception("Connection cLosed: The Connection was gracecfully closed by the local user."); - case ConnectionClosedReason.ClosedByPeer: - throw new Exception("Connection closed: The connection was gracefully closed by remote user."); - case ConnectionClosedReason.ConnectionClosed: - throw new Exception("Connection closed: The connection was unexpectedly closed."); - case ConnectionClosedReason.ConnectionFailed: - throw new Exception("Connection failed: Failled to establish connection."); - case ConnectionClosedReason.InvalidData: - throw new Exception("Connection failed: The remote user sent us invalid data.."); - case ConnectionClosedReason.InvalidMessage: - throw new Exception("Connection failed: The remote user sent us an invalid message."); - case ConnectionClosedReason.NegotiationFailed: - throw new Exception("Connection failed: Negotiation failed."); - case ConnectionClosedReason.TimedOut: - throw new Exception("Connection failed: Timeout."); - case ConnectionClosedReason.TooManyConnections: - throw new Exception("Connection failed: Too many connections."); - case ConnectionClosedReason.UnexpectedError: - throw new Exception("Unexpected Error, connection will be closed"); - case ConnectionClosedReason.Unknown: - default: - throw new Exception("Unknown Error, connection has been closed."); - } - } - - protected void SendInternal(ProductUserId target, SocketId socketId, InternalMessages type) { - EOSSDKComponent.GetP2PInterface().SendPacket(new SendPacketOptions() { - AllowDelayedDelivery = true, - Channel = (byte) internal_ch, - Data = new byte[] { (byte) type }, - LocalUserId = EOSSDKComponent.LocalUserProductId, - Reliability = PacketReliability.ReliableOrdered, - RemoteUserId = target, - SocketId = socketId - }); - } - - - protected void Send(ProductUserId host, SocketId socketId, byte[] msgBuffer, byte channel) { - Result result = EOSSDKComponent.GetP2PInterface().SendPacket(new SendPacketOptions() { - AllowDelayedDelivery = true, - Channel = channel, - Data = msgBuffer, - LocalUserId = EOSSDKComponent.LocalUserProductId, - Reliability = channels[channel], - RemoteUserId = host, - SocketId = socketId - }); - - if(result != Result.Success) { - Debug.LogError("Send failed " + result); - } - } - - private bool Receive(out ProductUserId clientProductUserId, out SocketId socketId, out byte[] receiveBuffer, byte channel) { - Result result = EOSSDKComponent.GetP2PInterface().ReceivePacket(new ReceivePacketOptions() { - LocalUserId = EOSSDKComponent.LocalUserProductId, - MaxDataSizeBytes = P2PInterface.MaxPacketSize, - RequestedChannel = channel - }, out clientProductUserId, out socketId, out channel, out receiveBuffer); - - if (result == Result.Success) { - return true; - } - - receiveBuffer = null; - clientProductUserId = null; - return false; - } - - protected virtual void CloseP2PSessionWithUser(ProductUserId clientUserID, SocketId socketId) { - if (socketId == null) { - Debug.LogWarning("Socket ID == null | " + ignoreAllMessages); - return; - } - - if (deadSockets == null) { - Debug.LogWarning("DeadSockets == null"); - return; - } - - if (deadSockets.Contains(socketId.SocketName)) { - return; - } else { - deadSockets.Add(socketId.SocketName); - } - } - - - protected void WaitForClose(ProductUserId clientUserID, SocketId socketId) => transport.StartCoroutine(DelayedClose(clientUserID, socketId)); - private IEnumerator DelayedClose(ProductUserId clientUserID, SocketId socketId) { - yield return null; - CloseP2PSessionWithUser(clientUserID, socketId); - } - - public void ReceiveData() { - try { - // Internal Channel, no fragmentation here - SocketId socketId = new SocketId(); - while (transport.enabled && Receive(out ProductUserId clientUserID, out socketId, out byte[] internalMessage, (byte) internal_ch)) { - if (internalMessage.Length == 1) { - OnReceiveInternalData((InternalMessages) internalMessage[0], clientUserID, socketId); - return; // Wait one frame - } else { - Debug.Log("Incorrect package length on internal channel."); - } - } - - // Insert new packet at the correct location in the incoming queue - for (int chNum = 0; chNum < channels.Length; chNum++) { - while (transport.enabled && Receive(out ProductUserId clientUserID, out socketId, out byte[] receiveBuffer, (byte) chNum)) { - PacketKey incomingPacketKey = new PacketKey(); - incomingPacketKey.productUserId = clientUserID; - incomingPacketKey.channel = (byte)chNum; - - Packet packet = new Packet(); - packet.FromBytes(receiveBuffer); - - if (!incomingPackets.ContainsKey(incomingPacketKey)) { - incomingPackets.Add(incomingPacketKey, new List>()); - } - - int packetListIndex = incomingPackets[incomingPacketKey].Count; - for(int i = 0; i < incomingPackets[incomingPacketKey].Count; i++) { - if(incomingPackets[incomingPacketKey][i][0].id == packet.id) { - packetListIndex = i; - break; - } - } - - if (packetListIndex == incomingPackets[incomingPacketKey].Count) { - incomingPackets[incomingPacketKey].Add(new List()); - } - - int insertionIndex = -1; - - for (int i = 0; i < incomingPackets[incomingPacketKey][packetListIndex].Count; i++) { - if (incomingPackets[incomingPacketKey][packetListIndex][i].fragment > packet.fragment) { - insertionIndex = i; - break; - } - } - - if (insertionIndex >= 0) { - incomingPackets[incomingPacketKey][packetListIndex].Insert(insertionIndex, packet); - } else { - incomingPackets[incomingPacketKey][packetListIndex].Add(packet); - } - } - } - - // Find fully received packets - List> emptyPacketLists = new List>(); - foreach(KeyValuePair>> keyValuePair in incomingPackets) { - for(int packetList = 0; packetList < keyValuePair.Value.Count; packetList++) { - bool packetReady = true; - int packetLength = 0; - for (int packet = 0; packet < keyValuePair.Value[packetList].Count; packet++) { - Packet tempPacket = keyValuePair.Value[packetList][packet]; - if (tempPacket.fragment != packet || (packet == keyValuePair.Value[packetList].Count - 1 && tempPacket.moreFragments)) { - packetReady = false; - } else { - packetLength += tempPacket.data.Length; - } - } - - if (packetReady) { - byte[] data = new byte[packetLength]; - int dataIndex = 0; - - for (int packet = 0; packet < keyValuePair.Value[packetList].Count; packet++) { - Array.Copy(keyValuePair.Value[packetList][packet].data, 0, data, dataIndex, keyValuePair.Value[packetList][packet].data.Length); - dataIndex += keyValuePair.Value[packetList][packet].data.Length; - } - - OnReceiveData(data, keyValuePair.Key.productUserId, keyValuePair.Key.channel); - - if(transport.ServerActive() || transport.ClientActive()) - emptyPacketLists.Add(keyValuePair.Value[packetList]); - } - } - - for (int i = 0; i < emptyPacketLists.Count; i++) { - keyValuePair.Value.Remove(emptyPacketLists[i]); - } - emptyPacketLists.Clear(); - } - - - - } catch (Exception e) { - Debug.LogException(e); - } - } - - protected abstract void OnReceiveInternalData(InternalMessages type, ProductUserId clientUserID, SocketId socketId); - protected abstract void OnReceiveData(byte[] data, ProductUserId clientUserID, int channel); - protected abstract void OnConnectionFailed(ProductUserId remoteId); - } -} \ No newline at end of file diff --git a/EpicOnlineTransport/EOSSDKComponent.cs b/EpicOnlineTransport/EOSSDKComponent.cs deleted file mode 100644 index 6a322b860..000000000 --- a/EpicOnlineTransport/EOSSDKComponent.cs +++ /dev/null @@ -1,435 +0,0 @@ -using Epic.OnlineServices; -using Epic.OnlineServices.Logging; -using Epic.OnlineServices.Platform; - -using System; -using System.Runtime.InteropServices; - -using UnityEngine; - -/// -/// Manages the Epic Online Services SDK -/// Do not destroy this component! -/// The Epic Online Services SDK can only be initialized once, -/// after releasing the SDK the game has to be restarted in order to initialize the SDK again. -/// In the unity editor the OnDestroy function will not run so that we dont have to restart the editor after play. -/// -namespace EpicTransport { - [DefaultExecutionOrder(-32000)] - public class EOSSDKComponent : MonoBehaviour { - - // Unity Inspector shown variables - - [SerializeField] - // CHANGED - public EosApiKey apiKeys; - - [Header("User Login")] - public bool authInterfaceLogin = false; - public Epic.OnlineServices.Auth.LoginCredentialType authInterfaceCredentialType = Epic.OnlineServices.Auth.LoginCredentialType.AccountPortal; - public uint devAuthToolPort = 7878; - public string devAuthToolCredentialName = ""; - public Epic.OnlineServices.ExternalCredentialType connectInterfaceCredentialType = Epic.OnlineServices.ExternalCredentialType.DeviceidAccessToken; - public string deviceModel = "PC Windows 64bit"; - [SerializeField] private string displayName = "User"; - public static string DisplayName { - get { - return Instance.displayName; - } - set { - Instance.displayName = value; - } - } - - [Header("Misc")] - public LogLevel epicLoggerLevel = LogLevel.Error; - - [SerializeField] private bool collectPlayerMetrics = true; - public static bool CollectPlayerMetrics { - get { - return Instance.collectPlayerMetrics; - } - } - - public bool checkForEpicLauncherAndRestart = false; - public bool delayedInitialization = false; - public float platformTickIntervalInSeconds = 0.0f; - private float platformTickTimer = 0f; - public uint tickBudgetInMilliseconds = 0; - - // End Unity Inspector shown variables - - private ulong authExpirationHandle; - - - private string authInterfaceLoginCredentialId = null; - public static void SetAuthInterfaceLoginCredentialId(string credentialId) => Instance.authInterfaceLoginCredentialId = credentialId; - private string authInterfaceCredentialToken = null; - public static void SetAuthInterfaceCredentialToken(string credentialToken) => Instance.authInterfaceCredentialToken = credentialToken; - private string connectInterfaceCredentialToken = null; - public static void SetConnectInterfaceCredentialToken(string credentialToken) => Instance.connectInterfaceCredentialToken = credentialToken; - - private PlatformInterface EOS; - - // Interfaces - public static Epic.OnlineServices.Achievements.AchievementsInterface GetAchievementsInterface() => Instance.EOS.GetAchievementsInterface(); - public static Epic.OnlineServices.Auth.AuthInterface GetAuthInterface() => Instance.EOS.GetAuthInterface(); - public static Epic.OnlineServices.Connect.ConnectInterface GetConnectInterface() => Instance.EOS.GetConnectInterface(); - public static Epic.OnlineServices.Ecom.EcomInterface GetEcomInterface() => Instance.EOS.GetEcomInterface(); - public static Epic.OnlineServices.Friends.FriendsInterface GetFriendsInterface() => Instance.EOS.GetFriendsInterface(); - public static Epic.OnlineServices.Leaderboards.LeaderboardsInterface GetLeaderboardsInterface() => Instance.EOS.GetLeaderboardsInterface(); - public static Epic.OnlineServices.Lobby.LobbyInterface GetLobbyInterface() => Instance.EOS.GetLobbyInterface(); - public static Epic.OnlineServices.Metrics.MetricsInterface GetMetricsInterface() => Instance.EOS.GetMetricsInterface(); // Handled by the transport automatically, only use this interface if Mirror is not used for singleplayer - public static Epic.OnlineServices.Mods.ModsInterface GetModsInterface() => Instance.EOS.GetModsInterface(); - public static Epic.OnlineServices.P2P.P2PInterface GetP2PInterface() => Instance.EOS.GetP2PInterface(); - public static Epic.OnlineServices.PlayerDataStorage.PlayerDataStorageInterface GetPlayerDataStorageInterface() => Instance.EOS.GetPlayerDataStorageInterface(); - public static Epic.OnlineServices.Presence.PresenceInterface GetPresenceInterface() => Instance.EOS.GetPresenceInterface(); - public static Epic.OnlineServices.Sessions.SessionsInterface GetSessionsInterface() => Instance.EOS.GetSessionsInterface(); - public static Epic.OnlineServices.TitleStorage.TitleStorageInterface GetTitleStorageInterface() => Instance.EOS.GetTitleStorageInterface(); - public static Epic.OnlineServices.UI.UIInterface GetUIInterface() => Instance.EOS.GetUIInterface(); - public static Epic.OnlineServices.UserInfo.UserInfoInterface GetUserInfoInterface() => Instance.EOS.GetUserInfoInterface(); - - - protected EpicAccountId localUserAccountId; - public static EpicAccountId LocalUserAccountId { - get { - return Instance.localUserAccountId; - } - } - - protected string localUserAccountIdString; - public static string LocalUserAccountIdString { - get { - return Instance.localUserAccountIdString; - } - } - - protected ProductUserId localUserProductId; - public static ProductUserId LocalUserProductId { - get { - return Instance.localUserProductId; - } - } - - protected string localUserProductIdString; - public static string LocalUserProductIdString { - get { - return Instance.localUserProductIdString; - } - } - - protected bool initialized; - public static bool Initialized { - get { - return Instance.initialized; - } - } - - protected bool isConnecting; - public static bool IsConnecting { - get { - return Instance.isConnecting; - } - } - - protected static EOSSDKComponent instance; - protected static EOSSDKComponent Instance { - get { - if (instance == null) { - return new GameObject("EOSSDKComponent").AddComponent(); - } else { - return instance; - } - } - } - - public static void Tick() { - instance.platformTickTimer -= Time.deltaTime; - instance.EOS.Tick(); - } - - // If we're in editor, we should dynamically load and unload the SDK between play sessions. - // This allows us to initialize the SDK each time the game is run in editor. -#if UNITY_EDITOR_WIN - [DllImport("Kernel32.dll")] - private static extern IntPtr LoadLibrary(string lpLibFileName); - - [DllImport("Kernel32.dll")] - private static extern int FreeLibrary(IntPtr hLibModule); - - [DllImport("Kernel32.dll")] - private static extern IntPtr GetProcAddress(IntPtr hModule, string lpProcName); - - private IntPtr libraryPointer; -#endif - -#if UNITY_EDITOR_LINUX - [DllImport("libdl.so", EntryPoint = "dlopen")] - private static extern IntPtr LoadLibrary(String lpFileName, int flags = 2); - - [DllImport("libdl.so", EntryPoint = "dlclose")] - private static extern int FreeLibrary(IntPtr hLibModule); - - [DllImport("libdl.so")] - private static extern IntPtr dlsym(IntPtr handle, String symbol); - - [DllImport("libdl.so")] - private static extern IntPtr dlerror(); - - private static IntPtr GetProcAddress(IntPtr hModule, string lpProcName) { - // clear previous errors if any - dlerror(); - var res = dlsym(hModule, lpProcName); - var errPtr = dlerror(); - if (errPtr != IntPtr.Zero) { - throw new Exception("dlsym: " + Marshal.PtrToStringAnsi(errPtr)); - } - return res; - } - private IntPtr libraryPointer; -#endif - - private void Awake() { - // Initialize Java version of the SDK with a reference to the VM with JNI - // See https://eoshelp.epicgames.com/s/question/0D54z00006ufJBNCA2/cant-get-createdeviceid-to-work-in-unity-android-c-sdk?language=en_US - if (Application.platform == RuntimePlatform.Android) - { - AndroidJavaClass unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer"); - AndroidJavaObject activity = unityPlayer.GetStatic("currentActivity"); - AndroidJavaObject context = activity.Call("getApplicationContext"); - AndroidJavaClass EOS_SDK_JAVA = new AndroidJavaClass("com.epicgames.mobile.eossdk.EOSSDK"); - EOS_SDK_JAVA.CallStatic("init", context); - } - - // Prevent multiple instances - if (instance != null) { - Destroy(gameObject); - return; - } - instance = this; - -#if UNITY_EDITOR - var libraryPath = "Assets/Mirror/Runtime/Transport/EpicOnlineTransport/EOSSDK/" + Config.LibraryName; - - libraryPointer = LoadLibrary(libraryPath); - if (libraryPointer == IntPtr.Zero) { - throw new Exception("Failed to load library" + libraryPath); - } - - Bindings.Hook(libraryPointer, GetProcAddress); -#endif - - if (!delayedInitialization) { - Initialize(); - } - } - - protected void InitializeImplementation() { - isConnecting = true; - - var initializeOptions = new InitializeOptions() { - ProductName = apiKeys.epicProductName, - ProductVersion = apiKeys.epicProductVersion - }; - - var initializeResult = PlatformInterface.Initialize(initializeOptions); - - // This code is called each time the game is run in the editor, so we catch the case where the SDK has already been initialized in the editor. - var isAlreadyConfiguredInEditor = Application.isEditor && initializeResult == Result.AlreadyConfigured; - if (initializeResult != Result.Success && !isAlreadyConfiguredInEditor) { - throw new System.Exception("Failed to initialize platform: " + initializeResult); - } - - // The SDK outputs lots of information that is useful for debugging. - // Make sure to set up the logging interface as early as possible: after initializing. - LoggingInterface.SetLogLevel(LogCategory.AllCategories, epicLoggerLevel); - LoggingInterface.SetCallback(message => Logger.EpicDebugLog(message)); - - var options = new Options() { - ProductId = apiKeys.epicProductId, - SandboxId = apiKeys.epicSandboxId, - DeploymentId = apiKeys.epicDeploymentId, - ClientCredentials = new ClientCredentials() { - ClientId = apiKeys.epicClientId, - ClientSecret = apiKeys.epicClientSecret - }, - TickBudgetInMilliseconds = tickBudgetInMilliseconds - }; - - EOS = PlatformInterface.Create(options); - if (EOS == null) { - throw new System.Exception("Failed to create platform"); - } - - if (checkForEpicLauncherAndRestart) { - Result result = EOS.CheckForLauncherAndRestart(); - - // If not started through epic launcher the app will be restarted and we can quit - if (result != Result.NoChange) { - - // Log error if launcher check failed, but still quit to prevent hacking - if (result == Result.UnexpectedError) { - Debug.LogError("Unexpected Error while checking if app was started through epic launcher"); - } - - Application.Quit(); - } - } - - // If we use the Auth interface then only login into the Connect interface after finishing the auth interface login - // If we don't use the Auth interface we can directly login to the Connect interface - if (authInterfaceLogin) { - if (authInterfaceCredentialType == Epic.OnlineServices.Auth.LoginCredentialType.Developer) { - authInterfaceLoginCredentialId = "localhost:" + devAuthToolPort; - authInterfaceCredentialToken = devAuthToolCredentialName; - } - - // Login to Auth Interface - Epic.OnlineServices.Auth.LoginOptions loginOptions = new Epic.OnlineServices.Auth.LoginOptions() { - Credentials = new Epic.OnlineServices.Auth.Credentials() { - Type = authInterfaceCredentialType, - Id = authInterfaceLoginCredentialId, - Token = authInterfaceCredentialToken - }, - ScopeFlags = Epic.OnlineServices.Auth.AuthScopeFlags.BasicProfile | Epic.OnlineServices.Auth.AuthScopeFlags.FriendsList | Epic.OnlineServices.Auth.AuthScopeFlags.Presence - }; - - EOS.GetAuthInterface().Login(loginOptions, null, OnAuthInterfaceLogin); - } else { - // Login to Connect Interface - if (connectInterfaceCredentialType == Epic.OnlineServices.ExternalCredentialType.DeviceidAccessToken) { - Epic.OnlineServices.Connect.CreateDeviceIdOptions createDeviceIdOptions = new Epic.OnlineServices.Connect.CreateDeviceIdOptions(); - createDeviceIdOptions.DeviceModel = deviceModel; - EOS.GetConnectInterface().CreateDeviceId(createDeviceIdOptions, null, OnCreateDeviceId); - } else { - ConnectInterfaceLogin(); - } - } - - } - public static void Initialize() { - if (Instance.initialized || Instance.isConnecting) { - return; - } - - Instance.InitializeImplementation(); - } - - private void OnAuthInterfaceLogin(Epic.OnlineServices.Auth.LoginCallbackInfo loginCallbackInfo) { - if (loginCallbackInfo.ResultCode == Result.Success) { - Debug.Log("Auth Interface Login succeeded"); - - string accountIdString; - Result result = loginCallbackInfo.LocalUserId.ToString(out accountIdString); - if (Result.Success == result) { - Debug.Log("EOS User ID:" + accountIdString); - - localUserAccountIdString = accountIdString; - localUserAccountId = loginCallbackInfo.LocalUserId; - } - - ConnectInterfaceLogin(); - } else if(Epic.OnlineServices.Common.IsOperationComplete(loginCallbackInfo.ResultCode)){ - Debug.Log("Login returned " + loginCallbackInfo.ResultCode); - } - } - - private void OnCreateDeviceId(Epic.OnlineServices.Connect.CreateDeviceIdCallbackInfo createDeviceIdCallbackInfo) { - if (createDeviceIdCallbackInfo.ResultCode == Result.Success || createDeviceIdCallbackInfo.ResultCode == Result.DuplicateNotAllowed) { - ConnectInterfaceLogin(); - } else if(Epic.OnlineServices.Common.IsOperationComplete(createDeviceIdCallbackInfo.ResultCode)) { - Debug.Log("Device ID creation returned " + createDeviceIdCallbackInfo.ResultCode); - } - } - - private void ConnectInterfaceLogin() { - var loginOptions = new Epic.OnlineServices.Connect.LoginOptions(); - - if (connectInterfaceCredentialType == Epic.OnlineServices.ExternalCredentialType.Epic) { - Epic.OnlineServices.Auth.Token token; - Result result = EOS.GetAuthInterface().CopyUserAuthToken(new Epic.OnlineServices.Auth.CopyUserAuthTokenOptions(), localUserAccountId, out token); - - if (result == Result.Success) { - connectInterfaceCredentialToken = token.AccessToken; - } else { - Debug.LogError("Failed to retrieve User Auth Token"); - } - } else if (connectInterfaceCredentialType == Epic.OnlineServices.ExternalCredentialType.DeviceidAccessToken) { - loginOptions.UserLoginInfo = new Epic.OnlineServices.Connect.UserLoginInfo(); - loginOptions.UserLoginInfo.DisplayName = displayName; - } - - loginOptions.Credentials = new Epic.OnlineServices.Connect.Credentials(); - loginOptions.Credentials.Type = connectInterfaceCredentialType; - loginOptions.Credentials.Token = connectInterfaceCredentialToken; - - EOS.GetConnectInterface().Login(loginOptions, null, OnConnectInterfaceLogin); - } - - private void OnConnectInterfaceLogin(Epic.OnlineServices.Connect.LoginCallbackInfo loginCallbackInfo) { - if (loginCallbackInfo.ResultCode == Result.Success) { - Debug.Log("Connect Interface Login succeeded"); - - string productIdString; - Result result = loginCallbackInfo.LocalUserId.ToString(out productIdString); - if (Result.Success == result) { - Debug.Log("EOS User Product ID:" + productIdString); - - localUserProductIdString = productIdString; - localUserProductId = loginCallbackInfo.LocalUserId; - } - - initialized = true; - isConnecting = false; - - var authExpirationOptions = new Epic.OnlineServices.Connect.AddNotifyAuthExpirationOptions(); - authExpirationHandle = EOS.GetConnectInterface().AddNotifyAuthExpiration(authExpirationOptions, null, OnAuthExpiration); - } else if (Epic.OnlineServices.Common.IsOperationComplete(loginCallbackInfo.ResultCode)) { - Debug.Log("Login returned " + loginCallbackInfo.ResultCode + "\nRetrying..."); - EOS.GetConnectInterface().CreateUser(new Epic.OnlineServices.Connect.CreateUserOptions() { ContinuanceToken = loginCallbackInfo.ContinuanceToken }, null, (Epic.OnlineServices.Connect.CreateUserCallbackInfo cb) => { - if (cb.ResultCode != Result.Success) { Debug.Log(cb.ResultCode); return; } - localUserProductId = cb.LocalUserId; - ConnectInterfaceLogin(); - }); - } - } - - private void OnAuthExpiration(Epic.OnlineServices.Connect.AuthExpirationCallbackInfo authExpirationCallbackInfo) { - Debug.Log("AuthExpiration callback"); - EOS.GetConnectInterface().RemoveNotifyAuthExpiration(authExpirationHandle); - ConnectInterfaceLogin(); - } - - // Calling tick on a regular interval is required for callbacks to work. - private void LateUpdate() { - if (EOS != null) { - platformTickTimer += Time.deltaTime; - - if (platformTickTimer >= platformTickIntervalInSeconds) { - platformTickTimer = 0; - EOS.Tick(); - } - } - } - - private void OnApplicationQuit() { - if (EOS != null) { - EOS.Release(); - EOS = null; - PlatformInterface.Shutdown(); - } - - // Unhook the library in the editor, this makes it possible to load the library again after stopping to play -#if UNITY_EDITOR - if (libraryPointer != IntPtr.Zero) { - Bindings.Unhook(); - - // Free until the module ref count is 0 - while (FreeLibrary(libraryPointer) != 0) { } - - libraryPointer = IntPtr.Zero; - } -#endif - } - } -} \ No newline at end of file diff --git a/EpicOnlineTransport/EosApiKey.cs b/EpicOnlineTransport/EosApiKey.cs deleted file mode 100644 index ddc037d1a..000000000 --- a/EpicOnlineTransport/EosApiKey.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Collections; -using System.Collections.Generic; -using UnityEngine; - -/// -/// Create an instance of this scriptable object and assign your eos api keys to it -/// Then add a reference to it to the EOSSDKComponent -/// -/// You can right click in your Project view in unity and choose: -/// Create -> EOS -> API Key -/// in order to create an instance of this scriptable object -/// - -[CreateAssetMenu(fileName = "EosApiKey", menuName = "EOS/API Key", order = 1)] -public class EosApiKey : ScriptableObject { - public string epicProductName = "MyApplication"; - public string epicProductVersion = "1.0"; - public string epicProductId = ""; - public string epicSandboxId = ""; - public string epicDeploymentId = ""; - public string epicClientId = ""; - public string epicClientSecret = ""; -} diff --git a/EpicOnlineTransport/EosTransport.cs b/EpicOnlineTransport/EosTransport.cs deleted file mode 100644 index 9def9aa5d..000000000 --- a/EpicOnlineTransport/EosTransport.cs +++ /dev/null @@ -1,331 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using UnityEngine; -using Epic.OnlineServices.P2P; -using Epic.OnlineServices; -using Mirror; -using Epic.OnlineServices.Metrics; -using System.Collections; - -namespace EpicTransport { - - /// - /// EOS Transport following the Mirror transport standard - /// - public class EosTransport : Transport { - private const string EPIC_SCHEME = "epic"; - - private Client client; - private Server server; - - private Common activeNode; - - [SerializeField] - public PacketReliability[] Channels = new PacketReliability[2] { PacketReliability.ReliableOrdered, PacketReliability.UnreliableUnordered }; - - [Tooltip("Timeout for connecting in seconds.")] - public int timeout = 25; - - [Tooltip("The max fragments used in fragmentation before throwing an error.")] - public int maxFragments = 55; - - public float ignoreCachedMessagesAtStartUpInSeconds = 2.0f; - private float ignoreCachedMessagesTimer = 0.0f; - - public RelayControl relayControl = RelayControl.AllowRelays; - - [Header("Info")] - [Tooltip("This will display your Epic Account ID when you start or connect to a server.")] - public ProductUserId productUserId; - - private int packetId = 0; - - private void Awake() { - Debug.Assert(Channels != null && Channels.Length > 0, "No channel configured for EOS Transport."); - Debug.Assert(Channels.Length < byte.MaxValue, "Too many channels configured for EOS Transport"); - - if(Channels[0] != PacketReliability.ReliableOrdered) { - Debug.LogWarning("EOS Transport Channel[0] is not ReliableOrdered, Mirror expects Channel 0 to be ReliableOrdered, only change this if you know what you are doing."); - } - if (Channels[1] != PacketReliability.UnreliableUnordered) { - Debug.LogWarning("EOS Transport Channel[1] is not UnreliableUnordered, Mirror expects Channel 1 to be UnreliableUnordered, only change this if you know what you are doing."); - } - - StartCoroutine("FetchEpicAccountId"); - StartCoroutine("ChangeRelayStatus"); - } - - public override void ClientEarlyUpdate() { - EOSSDKComponent.Tick(); - - if (activeNode != null) { - ignoreCachedMessagesTimer += Time.deltaTime; - - if (ignoreCachedMessagesTimer <= ignoreCachedMessagesAtStartUpInSeconds) { - activeNode.ignoreAllMessages = true; - } else { - activeNode.ignoreAllMessages = false; - - if (client != null && !client.isConnecting) { - if (EOSSDKComponent.Initialized) { - client.Connect(client.hostAddress); - } else { - Debug.LogError("EOS not initialized"); - client.EosNotInitialized(); - } - client.isConnecting = true; - } - } - } - - if (enabled) { - activeNode?.ReceiveData(); - } - } - - public override void ClientLateUpdate() {} - - public override void ServerEarlyUpdate() { - EOSSDKComponent.Tick(); - - if (activeNode != null) { - ignoreCachedMessagesTimer += Time.deltaTime; - - if (ignoreCachedMessagesTimer <= ignoreCachedMessagesAtStartUpInSeconds) { - activeNode.ignoreAllMessages = true; - } else { - activeNode.ignoreAllMessages = false; - } - } - - if (enabled) { - activeNode?.ReceiveData(); - } - } - - public override void ServerLateUpdate() {} - - public override bool ClientConnected() => ClientActive() && client.Connected; - public override void ClientConnect(string address) { - if (!EOSSDKComponent.Initialized) { - Debug.LogError("EOS not initialized. Client could not be started."); - OnClientDisconnected.Invoke(); - return; - } - - StartCoroutine("FetchEpicAccountId"); - - if (ServerActive()) { - Debug.LogError("Transport already running as server!"); - return; - } - - if (!ClientActive() || client.Error) { - Debug.Log($"Starting client, target address {address}."); - - client = Client.CreateClient(this, address); - activeNode = client; - - if (EOSSDKComponent.CollectPlayerMetrics) { - // Start Metrics colletion session - BeginPlayerSessionOptions sessionOptions = new BeginPlayerSessionOptions(); - sessionOptions.AccountId = EOSSDKComponent.LocalUserAccountId; - sessionOptions.ControllerType = UserControllerType.Unknown; - sessionOptions.DisplayName = EOSSDKComponent.DisplayName; - sessionOptions.GameSessionId = null; - sessionOptions.ServerIp = null; - Result result = EOSSDKComponent.GetMetricsInterface().BeginPlayerSession(sessionOptions); - - if(result == Result.Success) { - Debug.Log("Started Metric Session"); - } - } - } else { - Debug.LogError("Client already running!"); - } - } - - public override void ClientConnect(Uri uri) { - if (uri.Scheme != EPIC_SCHEME) - throw new ArgumentException($"Invalid url {uri}, use {EPIC_SCHEME}://EpicAccountId instead", nameof(uri)); - - ClientConnect(uri.Host); - } - - public override void ClientSend(ArraySegment segment, int channelId) { - Send(channelId, segment); - } - - public override void ClientDisconnect() { - if (ClientActive()) { - Shutdown(); - } - } - public bool ClientActive() => client != null; - - - public override bool ServerActive() => server != null; - public override void ServerStart() { - if (!EOSSDKComponent.Initialized) { - Debug.LogError("EOS not initialized. Server could not be started."); - return; - } - - StartCoroutine("FetchEpicAccountId"); - - if (ClientActive()) { - Debug.LogError("Transport already running as client!"); - return; - } - - if (!ServerActive()) { - Debug.Log("Starting server."); - - server = Server.CreateServer(this, NetworkManager.singleton.maxConnections); - activeNode = server; - - if (EOSSDKComponent.CollectPlayerMetrics) { - // Start Metrics colletion session - BeginPlayerSessionOptions sessionOptions = new BeginPlayerSessionOptions(); - sessionOptions.AccountId = EOSSDKComponent.LocalUserAccountId; - sessionOptions.ControllerType = UserControllerType.Unknown; - sessionOptions.DisplayName = EOSSDKComponent.DisplayName; - sessionOptions.GameSessionId = null; - sessionOptions.ServerIp = null; - Result result = EOSSDKComponent.GetMetricsInterface().BeginPlayerSession(sessionOptions); - - if (result == Result.Success) { - Debug.Log("Started Metric Session"); - } - } - } else { - Debug.LogError("Server already started!"); - } - } - - public override Uri ServerUri() { - UriBuilder epicBuilder = new UriBuilder { - Scheme = EPIC_SCHEME, - Host = EOSSDKComponent.LocalUserProductIdString - }; - - return epicBuilder.Uri; - } - - public override void ServerSend(int connectionId, ArraySegment segment, int channelId) { - if (ServerActive()) { - Send( channelId, segment, connectionId); - } - } - public override void ServerDisconnect(int connectionId) => server.Disconnect(connectionId); - public override string ServerGetClientAddress(int connectionId) => ServerActive() ? server.ServerGetClientAddress(connectionId) : string.Empty; - public override void ServerStop() { - if (ServerActive()) { - Shutdown(); - } - } - - private void Send(int channelId, ArraySegment segment, int connectionId = int.MinValue) { - Packet[] packets = GetPacketArray(channelId, segment); - - for(int i = 0; i < packets.Length; i++) { - if (connectionId == int.MinValue) { - if (client == null) - { - OnClientDisconnected.Invoke(); - return; - } - - client.Send(packets[i].ToBytes(), channelId); - } else { - server.SendAll(connectionId, packets[i].ToBytes(), channelId); - } - } - - packetId++; - } - - private Packet[] GetPacketArray(int channelId, ArraySegment segment) { - int packetCount = Mathf.CeilToInt((float) segment.Count / (float)GetMaxSinglePacketSize(channelId)); - Packet[] packets = new Packet[packetCount]; - - for (int i = 0; i < segment.Count; i += GetMaxSinglePacketSize(channelId)) { - int fragment = i / GetMaxSinglePacketSize(channelId); - - packets[fragment] = new Packet(); - packets[fragment].id = packetId; - packets[fragment].fragment = fragment; - packets[fragment].moreFragments = (segment.Count - i) > GetMaxSinglePacketSize(channelId); - packets[fragment].data = new byte[segment.Count - i > GetMaxSinglePacketSize(channelId) ? GetMaxSinglePacketSize(channelId) : segment.Count - i]; - Array.Copy(segment.Array, i, packets[fragment].data, 0, packets[fragment].data.Length); - } - - return packets; - } - - public override void Shutdown() { - if (EOSSDKComponent.CollectPlayerMetrics) { - // Stop Metrics collection session - EndPlayerSessionOptions endSessionOptions = new EndPlayerSessionOptions(); - endSessionOptions.AccountId = EOSSDKComponent.LocalUserAccountId; - Result result = EOSSDKComponent.GetMetricsInterface().EndPlayerSession(endSessionOptions); - - if (result == Result.Success) { - Debug.LogError("Stopped Metric Session"); - } - } - - server?.Shutdown(); - client?.Disconnect(); - - server = null; - client = null; - activeNode = null; - Debug.Log("Transport shut down."); - } - - public int GetMaxSinglePacketSize(int channelId) => P2PInterface.MaxPacketSize - 10; // 1159 bytes, we need to remove 10 bytes for the packet header (id (4 bytes) + fragment (4 bytes) + more fragments (1 byte)) - - public override int GetMaxPacketSize(int channelId) => P2PInterface.MaxPacketSize * maxFragments; - - public override int GetBatchThreshold(int channelId) => P2PInterface.MaxPacketSize; // Use P2PInterface.MaxPacketSize as everything above will get fragmentated and will be counter effective to batching - - public override bool Available() { - try { - return EOSSDKComponent.Initialized; - } catch { - return false; - } - } - - private IEnumerator FetchEpicAccountId() { - while (!EOSSDKComponent.Initialized) { - yield return null; - } - - productUserId = EOSSDKComponent.LocalUserProductId; - } - - private IEnumerator ChangeRelayStatus() { - while (!EOSSDKComponent.Initialized) { - yield return null; - } - - SetRelayControlOptions setRelayControlOptions = new SetRelayControlOptions(); - setRelayControlOptions.RelayControl = relayControl; - - EOSSDKComponent.GetP2PInterface().SetRelayControl(setRelayControlOptions); - } - - public void ResetIgnoreMessagesAtStartUpTimer() { - ignoreCachedMessagesTimer = 0; - } - - private void OnDestroy() { - if (activeNode != null) { - Shutdown(); - } - } - } -} diff --git a/EpicOnlineTransport/EpicOnlineTransport.csproj b/EpicOnlineTransport/EpicOnlineTransport.csproj deleted file mode 100644 index 0b7d1d209..000000000 --- a/EpicOnlineTransport/EpicOnlineTransport.csproj +++ /dev/null @@ -1,20 +0,0 @@ - - - EpicTransport - EpicOnlineTransport - Nudge Nudge Games - Nudge Nudge Games - Copyright © 2021 Nudge Nudge Games - License.md - - - - - - - - True - \ - - - diff --git a/EpicOnlineTransport/Logger.cs b/EpicOnlineTransport/Logger.cs deleted file mode 100644 index 197c6e0c3..000000000 --- a/EpicOnlineTransport/Logger.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Epic.OnlineServices.Logging; -using System; -using System.Collections; -using System.Collections.Generic; -using UnityEngine; - -namespace EpicTransport { - public static class Logger { - - public static void EpicDebugLog(LogMessage message) { - switch (message.Level) { - case LogLevel.Info: - Debug.Log($"Epic Manager: Category - {message.Category} Message - {message.Message}"); - break; - case LogLevel.Error: - Debug.LogError($"Epic Manager: Category - {message.Category} Message - {message.Message}"); - break; - case LogLevel.Warning: - Debug.LogWarning($"Epic Manager: Category - {message.Category} Message - {message.Message}"); - break; - case LogLevel.Fatal: - Debug.LogException(new Exception($"Epic Manager: Category - {message.Category} Message - {message.Message}")); - break; - default: - Debug.Log($"Epic Manager: Unknown log processing. Category - {message.Category} Message - {message.Message}"); - break; - } - } - } -} \ No newline at end of file diff --git a/EpicOnlineTransport/Packet.cs b/EpicOnlineTransport/Packet.cs deleted file mode 100644 index cb43d7556..000000000 --- a/EpicOnlineTransport/Packet.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System; - -namespace EpicTransport { - public struct Packet { - public const int headerSize = sizeof(uint) + sizeof(uint) + 1; - public int size => headerSize + data.Length; - - // header - public int id; - public int fragment; - public bool moreFragments; - - // body - public byte[] data; - - public byte[] ToBytes() { - byte[] array = new byte[size]; - - // Copy id - array[0] = (byte) id; - array[1] = (byte) (id >> 8); - array[2] = (byte) (id >> 0x10); - array[3] = (byte) (id >> 0x18); - - // Copy fragment - array[4] = (byte) fragment; - array[5] = (byte) (fragment >> 8); - array[6] = (byte) (fragment >> 0x10); - array[7] = (byte) (fragment >> 0x18); - - array[8] = moreFragments ? (byte)1 : (byte)0; - - Array.Copy(data, 0, array, 9, data.Length); - - return array; - } - - public void FromBytes(byte[] array) { - id = BitConverter.ToInt32(array, 0); - fragment = BitConverter.ToInt32(array, 4); - moreFragments = array[8] == 1; - - data = new byte[array.Length - 9]; - Array.Copy(array, 9, data, 0, data.Length); - } - } -} \ No newline at end of file diff --git a/EpicOnlineTransport/RandomString.cs b/EpicOnlineTransport/RandomString.cs deleted file mode 100644 index 46b7dc1d6..000000000 --- a/EpicOnlineTransport/RandomString.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using System.Text; - -public class RandomString { - - // Generates a random string with a given size. - public static string Generate(int size) { - var builder = new StringBuilder(size); - - Random random = new Random(); - - // Unicode/ASCII Letters are divided into two blocks - // (Letters 65–90 / 97–122): - // The first group containing the uppercase letters and - // the second group containing the lowercase. - - // char is a single Unicode character - char offsetLowerCase = 'a'; - char offsetUpperCase = 'A'; - const int lettersOffset = 26; // A...Z or a..z: length=26 - - for (var i = 0; i < size; i++) { - char offset; - if(random.Next(0,2) == 0) { - offset = offsetLowerCase; - } else { - offset = offsetUpperCase; - } - - var @char = (char) random.Next(offset, offset + lettersOffset); - builder.Append(@char); - } - - return builder.ToString(); - } -} diff --git a/EpicOnlineTransport/Server.cs b/EpicOnlineTransport/Server.cs deleted file mode 100644 index 9047e9b8e..000000000 --- a/EpicOnlineTransport/Server.cs +++ /dev/null @@ -1,192 +0,0 @@ -using Epic.OnlineServices; -using Epic.OnlineServices.P2P; -using Mirror; -using System; -using System.Collections.Generic; -using UnityEngine; - -namespace EpicTransport { - public class Server : Common { - private event Action OnConnected; - private event Action OnReceivedData; - private event Action OnDisconnected; - // CHANGED - private event Action OnReceivedError; - - private BidirectionalDictionary epicToMirrorIds; - private Dictionary epicToSocketIds; - private int maxConnections; - private int nextConnectionID; - - public static Server CreateServer(EosTransport transport, int maxConnections) { - Server s = new Server(transport, maxConnections); - - s.OnConnected += (id) => transport.OnServerConnected.Invoke(id); - s.OnDisconnected += (id) => transport.OnServerDisconnected.Invoke(id); - s.OnReceivedData += (id, data, channel) => transport.OnServerDataReceived.Invoke(id, new ArraySegment(data), channel); - // CHANGED - s.OnReceivedError += (id, error, reason) => transport.OnServerError?.Invoke(id, error, reason); - - if (!EOSSDKComponent.Initialized) { - Debug.LogError("EOS not initialized."); - } - - return s; - } - - private Server(EosTransport transport, int maxConnections) : base(transport) { - this.maxConnections = maxConnections; - epicToMirrorIds = new BidirectionalDictionary(); - epicToSocketIds = new Dictionary(); - nextConnectionID = 1; - } - - protected override void OnNewConnection(OnIncomingConnectionRequestInfo result) { - if (ignoreAllMessages) { - return; - } - - if (deadSockets.Contains(result.SocketId.SocketName)) { - Debug.LogError("Received incoming connection request from dead socket"); - return; - } - - EOSSDKComponent.GetP2PInterface().AcceptConnection( - new AcceptConnectionOptions() { - LocalUserId = EOSSDKComponent.LocalUserProductId, - RemoteUserId = result.RemoteUserId, - SocketId = result.SocketId - }); - } - - protected override void OnReceiveInternalData(InternalMessages type, ProductUserId clientUserId, SocketId socketId) { - if (ignoreAllMessages) { - return; - } - - switch (type) { - case InternalMessages.CONNECT: - if (epicToMirrorIds.Count >= maxConnections) { - Debug.LogError("Reached max connections"); - //CloseP2PSessionWithUser(clientUserId, socketId); - SendInternal(clientUserId, socketId, InternalMessages.DISCONNECT); - return; - } - - SendInternal(clientUserId, socketId, InternalMessages.ACCEPT_CONNECT); - - int connectionId = nextConnectionID++; - epicToMirrorIds.Add(clientUserId, connectionId); - epicToSocketIds.Add(clientUserId, socketId); - OnConnected.Invoke(connectionId); - - string clientUserIdString; - clientUserId.ToString(out clientUserIdString); - Debug.Log($"Client with Product User ID {clientUserIdString} connected. Assigning connection id {connectionId}"); - break; - case InternalMessages.DISCONNECT: - if (epicToMirrorIds.TryGetValue(clientUserId, out int connId)) { - OnDisconnected.Invoke(connId); - //CloseP2PSessionWithUser(clientUserId, socketId); - epicToMirrorIds.Remove(clientUserId); - epicToSocketIds.Remove(clientUserId); - Debug.Log($"Client with Product User ID {clientUserId} disconnected."); - } else { - // CHANGED - OnReceivedError?.Invoke(-1, TransportError.InvalidReceive, "ERROR Unknown Product User ID"); - } - - break; - default: - Debug.Log("Received unknown message type"); - break; - } - } - - protected override void OnReceiveData(byte[] data, ProductUserId clientUserId, int channel) { - if (ignoreAllMessages) { - return; - } - - if (epicToMirrorIds.TryGetValue(clientUserId, out int connectionId)) { - OnReceivedData.Invoke(connectionId, data, channel); - } else { - SocketId socketId; - epicToSocketIds.TryGetValue(clientUserId, out socketId); - CloseP2PSessionWithUser(clientUserId, socketId); - - string productId; - clientUserId.ToString(out productId); - - Debug.LogError("Data received from epic client thats not known " + productId); - // CHANGED - OnReceivedError?.Invoke(-1, TransportError.InvalidReceive, "ERROR Unknown product ID"); - } - } - - public void Disconnect(int connectionId) { - if (epicToMirrorIds.TryGetValue(connectionId, out ProductUserId userId)) { - SocketId socketId; - epicToSocketIds.TryGetValue(userId, out socketId); - SendInternal(userId, socketId, InternalMessages.DISCONNECT); - epicToMirrorIds.Remove(userId); - epicToSocketIds.Remove(userId); - } else { - Debug.LogWarning("Trying to disconnect unknown connection id: " + connectionId); - } - } - - public void Shutdown() { - foreach (KeyValuePair client in epicToMirrorIds) { - Disconnect(client.Value); - SocketId socketId; - epicToSocketIds.TryGetValue(client.Key, out socketId); - WaitForClose(client.Key, socketId); - } - - ignoreAllMessages = true; - ReceiveData(); - - Dispose(); - } - - public void SendAll(int connectionId, byte[] data, int channelId) { - if (epicToMirrorIds.TryGetValue(connectionId, out ProductUserId userId)) { - SocketId socketId; - epicToSocketIds.TryGetValue(userId, out socketId); - Send(userId, socketId, data, (byte)channelId); - } else { - Debug.LogError("Trying to send on unknown connection: " + connectionId); - // CHANGED - OnReceivedError?.Invoke(connectionId, TransportError.InvalidSend, "ERROR Unknown Connection"); - } - - } - - public string ServerGetClientAddress(int connectionId) { - if (epicToMirrorIds.TryGetValue(connectionId, out ProductUserId userId)) { - string userIdString; - userId.ToString(out userIdString); - return userIdString; - } else { - Debug.LogError("Trying to get info on unknown connection: " + connectionId); - // CHANGED - OnReceivedError?.Invoke(connectionId, TransportError.Unexpected, "ERROR Unknown Connection"); - return string.Empty; - } - } - - protected override void OnConnectionFailed(ProductUserId remoteId) { - if (ignoreAllMessages) { - return; - } - - int connectionId = epicToMirrorIds.TryGetValue(remoteId, out int connId) ? connId : nextConnectionID++; - OnDisconnected.Invoke(connectionId); - - Debug.LogError("Connection Failed, removing user"); - epicToMirrorIds.Remove(remoteId); - epicToSocketIds.Remove(remoteId); - } - } -} \ No newline at end of file diff --git a/EpicRerouter/EpicRerouter.csproj b/EpicRerouter/EpicRerouter.csproj deleted file mode 100644 index a49e2189a..000000000 --- a/EpicRerouter/EpicRerouter.csproj +++ /dev/null @@ -1,20 +0,0 @@ - - - Exe - LICENSE - Epic Rerouter - William Corby, Henry Pointer - William Corby, Henry Pointer - Copyright © William Corby, Henry Pointer 2022-2023 - - - - True - \ - - - - - - - diff --git a/EpicRerouter/ExeSide/EpicEntitlementRetriever.cs b/EpicRerouter/ExeSide/EpicEntitlementRetriever.cs deleted file mode 100644 index afabd61e3..000000000 --- a/EpicRerouter/ExeSide/EpicEntitlementRetriever.cs +++ /dev/null @@ -1,52 +0,0 @@ -using Epic.OnlineServices; -using Epic.OnlineServices.Ecom; - -namespace EpicRerouter.ExeSide; - -public static class EpicEntitlementRetriever -{ - private const string _eosDlcItemID = "49a9ac61fe464cbf8c8c73f46b3f1133"; - - private static EcomInterface _ecomInterface; - private static OwnershipStatus _epicDlcOwnershipStatus; - private static bool _epicResultReceived; - - public static void Init() => - EpicPlatformManager.OnAuthSuccess += EOSQueryOwnership; - - public static void Uninit() => - EpicPlatformManager.OnAuthSuccess -= EOSQueryOwnership; - - public static EntitlementsManager.AsyncOwnershipStatus GetOwnershipStatus() - { - if (!_epicResultReceived) - { - return EntitlementsManager.AsyncOwnershipStatus.NotReady; - } - - return _epicDlcOwnershipStatus == OwnershipStatus.Owned ? - EntitlementsManager.AsyncOwnershipStatus.Owned : EntitlementsManager.AsyncOwnershipStatus.NotOwned; - } - - private static void EOSQueryOwnership() - { - Program.Log("[EOS] querying DLC ownership"); - _ecomInterface = EpicPlatformManager.PlatformInterface.GetEcomInterface(); - var queryOwnershipOptions = new QueryOwnershipOptions - { - LocalUserId = EpicPlatformManager.LocalUserId, - CatalogItemIds = new[] { _eosDlcItemID } - }; - _ecomInterface.QueryOwnership(queryOwnershipOptions, null, OnEOSQueryOwnershipComplete); - } - - private static void OnEOSQueryOwnershipComplete(QueryOwnershipCallbackInfo data) - { - if (data.ResultCode == Result.Success) - { - _epicDlcOwnershipStatus = data.ItemOwnership[0].OwnershipStatus; - _epicResultReceived = true; - Program.Log($"[EOS] Query DLC ownership complete: {_epicDlcOwnershipStatus}"); - } - } -} \ No newline at end of file diff --git a/EpicRerouter/ExeSide/EpicPlatformManager.cs b/EpicRerouter/ExeSide/EpicPlatformManager.cs deleted file mode 100644 index 558e23a17..000000000 --- a/EpicRerouter/ExeSide/EpicPlatformManager.cs +++ /dev/null @@ -1,140 +0,0 @@ -using Epic.OnlineServices; -using Epic.OnlineServices.Auth; -using Epic.OnlineServices.Platform; -using System; -using System.Threading; - -namespace EpicRerouter.ExeSide; - -public static class EpicPlatformManager -{ - private const string _eosProductID = "prod-starfish"; - private const string _eosSandboxID = "starfish"; - private const string _eosDeploymentID = "e176ecc84fbc4dd8934664684f44dc71"; - private const string _eosClientID = "5c553c6accee4111bc8ea3a3ae52229b"; - private const string _eosClientSecret = "k87Nfp75BzPref4nJFnnbNjYXQQR"; - private const float _tickInterval = 0.1f; - - public static PlatformInterface PlatformInterface; - public static EpicAccountId LocalUserId; - - public static OWEvent OnAuthSuccess = new(1); - - public static void Init() - { - if (PlatformInterface == null) - { - try - { - InitPlatform(); - } - catch (EOSInitializeException ex) - { - if (ex.Result == Result.AlreadyConfigured) - { - throw new Exception("[EOS] platform already configured!"); - } - } - } - - Auth(); - } - - public static void Tick() - { - PlatformInterface.Tick(); - Thread.Sleep(TimeSpan.FromSeconds(_tickInterval)); - } - - public static void Uninit() - { - PlatformInterface.Release(); - PlatformInterface = null; - PlatformInterface.Shutdown(); - } - - private static void InitPlatform() - { - var result = PlatformInterface.Initialize(new InitializeOptions - { - ProductName = Program.ProductName, - ProductVersion = Program.Version - }); - if (result != Result.Success) - { - throw new EOSInitializeException("Failed to initialize Epic Online Services platform: ", result); - } - - var options = new Options - { - ProductId = _eosProductID, - SandboxId = _eosSandboxID, - ClientCredentials = new ClientCredentials - { - ClientId = _eosClientID, - ClientSecret = _eosClientSecret - }, - DeploymentId = _eosDeploymentID - }; - PlatformInterface = PlatformInterface.Create(options); - Program.Log("[EOS] Platform interface has been created"); - } - - private static void Auth() - { - Program.Log("[EOS] Authenticating..."); - var loginOptions = new LoginOptions - { - Credentials = new Credentials - { - Type = LoginCredentialType.ExchangeCode, - Id = null, - Token = GetPasswordFromCommandLine() - }, - ScopeFlags = 0 - }; - if (PlatformInterface == null) - { - throw new Exception("[EOS] Platform interface is null!"); - } - - PlatformInterface.GetAuthInterface().Login(loginOptions, null, OnLogin); - } - - private static string GetPasswordFromCommandLine() - { - var commandLineArgs = Environment.GetCommandLineArgs(); - foreach (var arg in commandLineArgs) - { - if (arg.Contains("AUTH_PASSWORD")) - { - return arg.Split('=')[1]; - } - } - - return null; - } - - private static void OnLogin(LoginCallbackInfo loginCallbackInfo) - { - if (loginCallbackInfo.ResultCode == Result.Success) - { - LocalUserId = loginCallbackInfo.LocalUserId; - LocalUserId.ToString(out var s); - Program.Log($"[EOS SDK] login success! user ID: {s}"); - OnAuthSuccess.Invoke(); - return; - } - - throw new Exception("[EOS SDK] Login failed"); - } - - private class EOSInitializeException : Exception - { - public readonly Result Result; - - public EOSInitializeException(string msg, Result initResult) : - base(msg) => - Result = initResult; - } -} \ No newline at end of file diff --git a/EpicRerouter/ExeSide/Program.cs b/EpicRerouter/ExeSide/Program.cs deleted file mode 100644 index cfe486729..000000000 --- a/EpicRerouter/ExeSide/Program.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using System.Reflection; - -namespace EpicRerouter.ExeSide; - -public static class Program -{ - public static string ProductName; - public static string Version; - - private static void Main(string[] args) - { - ProductName = args[0]; - Log($"product name = {ProductName}"); - Version = args[1]; - Log($"version = {Version}"); - var managedDir = args[2]; - Log($"managed dir = {managedDir}"); - var gameArgs = args.Skip(3).ToArray(); - Log($"game args = {string.Join(", ", gameArgs)}"); - - AppDomain.CurrentDomain.AssemblyResolve += (_, e) => - { - var name = new AssemblyName(e.Name).Name + ".dll"; - var path = Path.Combine(managedDir, name); - return File.Exists(path) ? Assembly.LoadFile(path) : null; - }; - - Go(); - } - - private static void Go() - { - try - { - EpicPlatformManager.Init(); - EpicEntitlementRetriever.Init(); - - while (EpicEntitlementRetriever.GetOwnershipStatus() == EntitlementsManager.AsyncOwnershipStatus.NotReady) - { - EpicPlatformManager.Tick(); - } - } - finally - { - EpicEntitlementRetriever.Uninit(); - EpicPlatformManager.Uninit(); - - Environment.Exit((int)EpicEntitlementRetriever.GetOwnershipStatus()); - } - } - - public static void Log(object msg) => Console.Error.WriteLine(msg); -} \ No newline at end of file diff --git a/EpicRerouter/ModSide/Interop.cs b/EpicRerouter/ModSide/Interop.cs deleted file mode 100644 index 9f17d78aa..000000000 --- a/EpicRerouter/ModSide/Interop.cs +++ /dev/null @@ -1,68 +0,0 @@ -using HarmonyLib; -using System; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Reflection; -using UnityEngine; -using Debug = UnityEngine.Debug; - -namespace EpicRerouter.ModSide; - -public static class Interop -{ - public static EntitlementsManager.AsyncOwnershipStatus OwnershipStatus = EntitlementsManager.AsyncOwnershipStatus.NotReady; - - public static void Go() - { - if (typeof(EpicPlatformManager).GetField("_platformInterface", BindingFlags.NonPublic | BindingFlags.Instance) == null) - { - Log("not epic. don't reroute"); - return; - } - - Log("go"); - - Patches.Apply(); - - var processPath = Path.Combine( - Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, - "EpicRerouter.exe" - ); - Log($"process path = {processPath}"); - var gamePath = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(typeof(EpicPlatformManager).Assembly.Location)!, "..")); - Log($"game path = {gamePath}"); - var workingDirectory = Path.Combine(gamePath, "Plugins", "x86_64"); - Log($"working dir = {workingDirectory}"); - var args = new[] - { - Application.productName, - Application.version, - Path.Combine(gamePath, "Managed") - }; - Log($"args = {args.Join()}"); - var gameArgs = Environment.GetCommandLineArgs(); - Log($"game args = {gameArgs.Join()}"); - var process = Process.Start(new ProcessStartInfo - { - FileName = processPath, - WorkingDirectory = workingDirectory, - Arguments = args - .Concat(gameArgs) - .Join(x => $"\"{x}\"", " "), - - UseShellExecute = false, - CreateNoWindow = true, - RedirectStandardOutput = true, - RedirectStandardError = true - }); - process!.WaitForExit(); - OwnershipStatus = (EntitlementsManager.AsyncOwnershipStatus)process.ExitCode; - Log($"ownership status = {OwnershipStatus}"); - - Log($"output:\n{process.StandardOutput.ReadToEnd()}"); - Log($"error:\n{process.StandardError.ReadToEnd()}"); - } - - public static void Log(object msg) => Debug.Log($"[EpicRerouter] {msg}"); -} \ No newline at end of file diff --git a/EpicRerouter/ModSide/Patches.cs b/EpicRerouter/ModSide/Patches.cs deleted file mode 100644 index b7461a9c1..000000000 --- a/EpicRerouter/ModSide/Patches.cs +++ /dev/null @@ -1,65 +0,0 @@ -using HarmonyLib; -using UnityEngine; -using static EntitlementsManager; - -namespace EpicRerouter.ModSide; - -[HarmonyPatch(typeof(EpicPlatformManager))] -public static class Patches -{ - public static void Apply() - { - var harmony = new Harmony(typeof(Patches).FullName); - harmony.PatchAll(typeof(EntitlementsManagerPatches)); - harmony.PatchAll(typeof(EpicPlatformManagerPatches)); - } - - [HarmonyPatch(typeof(EntitlementsManager))] - private static class EntitlementsManagerPatches - { - [HarmonyPrefix] - [HarmonyPatch(nameof(EntitlementsManager.InitializeOnAwake))] - private static bool InitializeOnAwake(EntitlementsManager __instance) - { - Object.Destroy(__instance); - return false; - } - - [HarmonyPrefix] - [HarmonyPatch(nameof(EntitlementsManager.Start))] - private static bool Start() => false; - - [HarmonyPrefix] - [HarmonyPatch(nameof(EntitlementsManager.OnDestroy))] - private static bool OnDestroy() => false; - - [HarmonyPrefix] - [HarmonyPatch(nameof(EntitlementsManager.IsDlcOwned))] - private static bool IsDlcOwned(out AsyncOwnershipStatus __result) - { - __result = Interop.OwnershipStatus; - Interop.Log($"ownership status = {__result}"); - return false; - } - } - - [HarmonyPatch(typeof(EpicPlatformManager))] - private static class EpicPlatformManagerPatches - { - [HarmonyPrefix] - [HarmonyPatch("Awake")] - private static bool Awake(EpicPlatformManager __instance) - { - Object.Destroy(__instance); - return false; - } - - [HarmonyPrefix] - [HarmonyPatch("Start")] - private static bool Start() => false; - - [HarmonyPrefix] - [HarmonyPatch("OnDestroy")] - private static bool OnDestroy() => false; - } -} \ No newline at end of file diff --git a/FizzySteamworks/BidirectionalDictionary.cs b/FizzySteamworks/BidirectionalDictionary.cs new file mode 100644 index 000000000..383219e56 --- /dev/null +++ b/FizzySteamworks/BidirectionalDictionary.cs @@ -0,0 +1,89 @@ +using System.Collections; +using System.Collections.Generic; + +namespace Mirror.FizzySteam +{ + public class BidirectionalDictionary : IEnumerable + { + private Dictionary t1ToT2Dict = new Dictionary(); + private Dictionary t2ToT1Dict = new Dictionary(); + + public IEnumerable FirstTypes => t1ToT2Dict.Keys; + public IEnumerable SecondTypes => t2ToT1Dict.Keys; + + public IEnumerator GetEnumerator() => t1ToT2Dict.GetEnumerator(); + + public int Count => t1ToT2Dict.Count; + + public void Add(T1 key, T2 value) + { + if (t1ToT2Dict.ContainsKey(key)) + { + Remove(key); + } + + t1ToT2Dict[key] = value; + t2ToT1Dict[value] = key; + } + + public void Add(T2 key, T1 value) + { + if (t2ToT1Dict.ContainsKey(key)) + { + Remove(key); + } + + t2ToT1Dict[key] = value; + t1ToT2Dict[value] = key; + } + + public T2 Get(T1 key) => t1ToT2Dict[key]; + + public T1 Get(T2 key) => t2ToT1Dict[key]; + + public bool TryGetValue(T1 key, out T2 value) => t1ToT2Dict.TryGetValue(key, out value); + + public bool TryGetValue(T2 key, out T1 value) => t2ToT1Dict.TryGetValue(key, out value); + + public bool Contains(T1 key) => t1ToT2Dict.ContainsKey(key); + + public bool Contains(T2 key) => t2ToT1Dict.ContainsKey(key); + + public void Remove(T1 key) + { + if (Contains(key)) + { + T2 val = t1ToT2Dict[key]; + t1ToT2Dict.Remove(key); + t2ToT1Dict.Remove(val); + } + } + public void Remove(T2 key) + { + if (Contains(key)) + { + T1 val = t2ToT1Dict[key]; + t1ToT2Dict.Remove(val); + t2ToT1Dict.Remove(key); + } + } + + public T1 this[T2 key] + { + get => t2ToT1Dict[key]; + set + { + Add(key, value); + } + } + + public T2 this[T1 key] + { + get => t1ToT2Dict[key]; + set + { + Add(key, value); + } + } + } +} \ No newline at end of file diff --git a/FizzySteamworks/FizzySteamworks.cs b/FizzySteamworks/FizzySteamworks.cs new file mode 100644 index 000000000..bf03a50b1 --- /dev/null +++ b/FizzySteamworks/FizzySteamworks.cs @@ -0,0 +1,311 @@ +#if !DISABLESTEAMWORKS +using Steamworks; +using System; +using System.IO; +using UnityEngine; + +namespace Mirror.FizzySteam +{ + [HelpURL("https://github.com/Chykary/FizzySteamworks")] + public class FizzySteamworks : Transport + { + private const string STEAM_SCHEME = "steam"; + + private static IClient client; + private static IServer server; + + [SerializeField] + public EP2PSend[] Channels = new EP2PSend[2] { EP2PSend.k_EP2PSendReliable, EP2PSend.k_EP2PSendUnreliableNoDelay }; + + [Tooltip("Timeout for connecting in seconds.")] + public int Timeout = 25; + [Tooltip("Allow or disallow P2P connections to fall back to being relayed through the Steam servers if a direct connection or NAT-traversal cannot be established.")] + public bool AllowSteamRelay = true; + + [Tooltip("Use SteamSockets instead of the (deprecated) SteamNetworking. This will always use Relay.")] + public bool UseNextGenSteamNetworking = true; + + private void OnEnable() + { + Debug.Assert(Channels != null && Channels.Length > 0, "No channel configured for FizzySteamworks."); + Invoke(nameof(InitRelayNetworkAccess), 1f); + } + + public override void ClientEarlyUpdate() + { + if (enabled) + { + client?.ReceiveData(); + } + } + + public override void ServerEarlyUpdate() + { + if (enabled) + { + server?.ReceiveData(); + } + } + + public override void ClientLateUpdate() + { + if (enabled) + { + client?.FlushData(); + } + } + + public override void ServerLateUpdate() + { + if (enabled) + { + server?.FlushData(); + } + } + + public override bool ClientConnected() => ClientActive() && client.Connected; + public override void ClientConnect(string address) + { + try + { +#if UNITY_SERVER + SteamGameServerNetworkingUtils.InitRelayNetworkAccess(); +#else + SteamNetworkingUtils.InitRelayNetworkAccess(); +#endif + + InitRelayNetworkAccess(); + + if (ServerActive()) + { + Debug.LogError("Transport already running as server!"); + return; + } + + if (!ClientActive() || client.Error) + { + if (UseNextGenSteamNetworking) + { + Debug.Log($"Starting client [SteamSockets], target address {address}."); + client = NextClient.CreateClient(this, address); + } + else + { + Debug.Log($"Starting client [DEPRECATED SteamNetworking], target address {address}. Relay enabled: {AllowSteamRelay}"); + SteamNetworking.AllowP2PPacketRelay(AllowSteamRelay); + client = LegacyClient.CreateClient(this, address); + } + } + else + { + Debug.LogError("Client already running!"); + } + } + catch (Exception ex) + { + Debug.LogError("Exception: " + ex.Message + ". Client could not be started."); + OnClientDisconnected.Invoke(); + } + } + + public override void ClientConnect(Uri uri) + { + if (uri.Scheme != STEAM_SCHEME) + throw new ArgumentException($"Invalid url {uri}, use {STEAM_SCHEME}://SteamID instead", nameof(uri)); + + ClientConnect(uri.Host); + } + + public override void ClientSend(ArraySegment segment, int channelId) + { + byte[] data = new byte[segment.Count]; + Array.Copy(segment.Array, segment.Offset, data, 0, segment.Count); + client.Send(data, channelId); + } + + public override void ClientDisconnect() + { + if (ClientActive()) + { + Shutdown(); + } + } + public bool ClientActive() => client != null; + + + public override bool ServerActive() => server != null; + public override void ServerStart() + { + try + { +#if UNITY_SERVER + SteamGameServerNetworkingUtils.InitRelayNetworkAccess(); +#else + SteamNetworkingUtils.InitRelayNetworkAccess(); +#endif + + + InitRelayNetworkAccess(); + + if (ClientActive()) + { + Debug.LogError("Transport already running as client!"); + return; + } + + if (!ServerActive()) + { + if (UseNextGenSteamNetworking) + { + Debug.Log($"Starting server [SteamSockets]."); + server = NextServer.CreateServer(this, NetworkManager.singleton.maxConnections); + } + else + { + Debug.Log($"Starting server [DEPRECATED SteamNetworking]. Relay enabled: {AllowSteamRelay}"); +#if UNITY_SERVER + SteamGameServerNetworking.AllowP2PPacketRelay(AllowSteamRelay); +#else + + SteamNetworking.AllowP2PPacketRelay(AllowSteamRelay); +#endif + server = LegacyServer.CreateServer(this, NetworkManager.singleton.maxConnections); + } + } + else + { + Debug.LogError("Server already started!"); + } + } + catch (Exception ex) + { + Debug.LogException(ex); + return; + } + } + + public override Uri ServerUri() + { + var steamBuilder = new UriBuilder + { + Scheme = STEAM_SCHEME, +#if UNITY_SERVER + Host = SteamGameServer.GetSteamID().m_SteamID.ToString() +#else + Host = SteamUser.GetSteamID().m_SteamID.ToString() +#endif + }; + + return steamBuilder.Uri; + } + + public override void ServerSend(int connectionId, ArraySegment segment, int channelId) + { + if (ServerActive()) + { + byte[] data = new byte[segment.Count]; + Array.Copy(segment.Array, segment.Offset, data, 0, segment.Count); + server.Send(connectionId, data, channelId); + } + } + public override void ServerDisconnect(int connectionId) + { + if (ServerActive()) + { + server.Disconnect(connectionId); + } + } + public override string ServerGetClientAddress(int connectionId) => ServerActive() ? server.ServerGetClientAddress(connectionId) : string.Empty; + public override void ServerStop() + { + if (ServerActive()) + { + Shutdown(); + } + } + + public override void Shutdown() + { + if (server != null) + { + server.Shutdown(); + server = null; + Debug.Log("Transport shut down - was server."); + } + + if (client != null) + { + client.Disconnect(); + client = null; + Debug.Log("Transport shut down - was client."); + } + } + + public override int GetMaxPacketSize(int channelId) + { + if (UseNextGenSteamNetworking) + { + return Constants.k_cbMaxSteamNetworkingSocketsMessageSizeSend; + } + else + { + if (channelId >= Channels.Length) + { + Debug.LogError("Channel Id exceeded configured channels! Please configure more channels."); + return 1200; + } + + switch (Channels[channelId]) + { + case EP2PSend.k_EP2PSendUnreliable: + case EP2PSend.k_EP2PSendUnreliableNoDelay: + return 1200; + case EP2PSend.k_EP2PSendReliable: + case EP2PSend.k_EP2PSendReliableWithBuffering: + return 1048576; + default: + throw new NotSupportedException(); + } + } + } + + public override bool Available() + { + try + { +#if UNITY_SERVER + SteamGameServerNetworkingUtils.InitRelayNetworkAccess(); +#else + SteamNetworkingUtils.InitRelayNetworkAccess(); +#endif + return true; + } + catch + { + return false; + } + } + + private void InitRelayNetworkAccess() + { + try + { + if (UseNextGenSteamNetworking) + { +#if UNITY_SERVER + SteamGameServerNetworkingUtils.InitRelayNetworkAccess(); +#else + SteamNetworkingUtils.InitRelayNetworkAccess(); +#endif + } + } + catch { } + } + + private void OnDestroy() + { + Shutdown(); + } + } +} +#endif // !DISABLESTEAMWORKS \ No newline at end of file diff --git a/FizzySteamworks/FizzySteamworks.csproj b/FizzySteamworks/FizzySteamworks.csproj new file mode 100644 index 000000000..92236d636 --- /dev/null +++ b/FizzySteamworks/FizzySteamworks.csproj @@ -0,0 +1,23 @@ + + + Mirror.FizzySteam + Fizzy Steamworks + Fizzy Steamworks + Fizzy Steamworks + + Fizz Cube Ltd, Marco Hoffmann, William Corby + Fizz Cube Ltd, Marco Hoffmann, William Corby + + LICENSE + + + + True + \ + + + + + + + diff --git a/FizzySteamworks/IClient.cs b/FizzySteamworks/IClient.cs new file mode 100644 index 000000000..043ff4f86 --- /dev/null +++ b/FizzySteamworks/IClient.cs @@ -0,0 +1,14 @@ +namespace Mirror.FizzySteam +{ + public interface IClient + { + bool Connected { get; } + bool Error { get; } + + + void ReceiveData(); + void Disconnect(); + void FlushData(); + void Send(byte[] data, int channelId); + } +} \ No newline at end of file diff --git a/FizzySteamworks/IServer.cs b/FizzySteamworks/IServer.cs new file mode 100644 index 000000000..570bc3b93 --- /dev/null +++ b/FizzySteamworks/IServer.cs @@ -0,0 +1,12 @@ +namespace Mirror.FizzySteam +{ + public interface IServer + { + void ReceiveData(); + void Send(int connectionId, byte[] data, int channelId); + void Disconnect(int connectionId); + void FlushData(); + string ServerGetClientAddress(int connectionId); + void Shutdown(); + } +} \ No newline at end of file diff --git a/EpicOnlineTransport/License.md b/FizzySteamworks/LICENSE similarity index 60% rename from EpicOnlineTransport/License.md rename to FizzySteamworks/LICENSE index 255489bd5..71310579e 100644 --- a/EpicOnlineTransport/License.md +++ b/FizzySteamworks/LICENSE @@ -1,6 +1,20 @@ -MIT License +MIT License -Copyright (c) 2021 Nudge Nudge Games +Copyright Fizz Cube Ltd (c) 2018 + +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. + +=== + +Copyright Marco Hoffmann (c) 2020 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -18,4 +32,6 @@ 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. \ No newline at end of file +SOFTWARE. + +MIT License \ No newline at end of file diff --git a/FizzySteamworks/LegacyClient.cs b/FizzySteamworks/LegacyClient.cs new file mode 100644 index 000000000..39c65f885 --- /dev/null +++ b/FizzySteamworks/LegacyClient.cs @@ -0,0 +1,177 @@ +#if !DISABLESTEAMWORKS +using Steamworks; +using System; +using System.Threading; +using System.Threading.Tasks; +using UnityEngine; + +namespace Mirror.FizzySteam +{ + public class LegacyClient : LegacyCommon, IClient + { + public bool Connected { get; private set; } + public bool Error { get; private set; } + + private event Action OnReceivedData; + private event Action OnConnected; + private event Action OnDisconnected; + + private TimeSpan ConnectionTimeout; + + private CSteamID hostSteamID = CSteamID.Nil; + private TaskCompletionSource connectedComplete; + private CancellationTokenSource cancelToken; + + private LegacyClient(FizzySteamworks transport) : base(transport) + { + ConnectionTimeout = TimeSpan.FromSeconds(Math.Max(1, transport.Timeout)); + } + + public static LegacyClient CreateClient(FizzySteamworks transport, string host) + { + LegacyClient c = new LegacyClient(transport); + + c.OnConnected += () => transport.OnClientConnected.Invoke(); + c.OnDisconnected += () => transport.OnClientDisconnected.Invoke(); + c.OnReceivedData += (data, channel) => transport.OnClientDataReceived.Invoke(new ArraySegment(data), channel); + + + try + { +#if UNITY_SERVER + InteropHelp.TestIfAvailableGameServer(); +#else + InteropHelp.TestIfAvailableClient(); +#endif + c.Connect(host); + } + catch + { + Debug.LogError("SteamWorks not initialized."); + c.OnConnectionFailed(CSteamID.Nil); + } + + return c; + } + + private async void Connect(string host) + { + cancelToken = new CancellationTokenSource(); + + try + { + hostSteamID = new CSteamID(UInt64.Parse(host)); + connectedComplete = new TaskCompletionSource(); + + OnConnected += SetConnectedComplete; + + SendInternal(hostSteamID, InternalMessages.CONNECT); + + Task connectedCompleteTask = connectedComplete.Task; + Task timeOutTask = Task.Delay(ConnectionTimeout, cancelToken.Token); + + if (await Task.WhenAny(connectedCompleteTask, timeOutTask) != connectedCompleteTask) + { + if (cancelToken.IsCancellationRequested) + { + Debug.LogError($"The connection attempt was cancelled."); + } + else if (timeOutTask.IsCompleted) + { + Debug.LogError($"Connection to {host} timed out."); + } + OnConnected -= SetConnectedComplete; + OnConnectionFailed(hostSteamID); + } + + OnConnected -= SetConnectedComplete; + } + catch (FormatException) + { + Debug.LogError($"Connection string was not in the right format. Did you enter a SteamId?"); + Error = true; + OnConnectionFailed(hostSteamID); + } + catch (Exception ex) + { + Debug.LogError(ex.Message); + Error = true; + OnConnectionFailed(hostSteamID); + } + finally + { + if (Error) + { + OnConnectionFailed(CSteamID.Nil); + } + } + } + + public void Disconnect() + { + Debug.Log("Sending Disconnect message"); + SendInternal(hostSteamID, InternalMessages.DISCONNECT); + Dispose(); + cancelToken?.Cancel(); + + WaitForClose(hostSteamID); + } + + private void SetConnectedComplete() => connectedComplete.SetResult(connectedComplete.Task); + + protected override void OnReceiveData(byte[] data, CSteamID clientSteamID, int channel) + { + if (clientSteamID != hostSteamID) + { + Debug.LogError("Received a message from an unknown"); + return; + } + + OnReceivedData.Invoke(data, channel); + } + + protected override void OnNewConnection(P2PSessionRequest_t result) + { + if (hostSteamID == result.m_steamIDRemote) + { + SteamNetworking.AcceptP2PSessionWithUser(result.m_steamIDRemote); + } + else + { + Debug.LogError("P2P Acceptance Request from unknown host ID."); + } + } + + protected override void OnReceiveInternalData(InternalMessages type, CSteamID clientSteamID) + { + switch (type) + { + case InternalMessages.ACCEPT_CONNECT: + if (!Connected) + { + Connected = true; + OnConnected.Invoke(); + Debug.Log("Connection established."); + } + break; + case InternalMessages.DISCONNECT: + if (Connected) + { + Connected = false; + Debug.Log("Disconnected."); + OnDisconnected.Invoke(); + } + break; + default: + Debug.Log("Received unknown message type"); + break; + } + } + + public void Send(byte[] data, int channelId) => Send(hostSteamID, data, channelId); + + protected override void OnConnectionFailed(CSteamID remoteId) => OnDisconnected.Invoke(); + public void FlushData() { } + } +} +#endif // !DISABLESTEAMWORKS \ No newline at end of file diff --git a/FizzySteamworks/LegacyCommon.cs b/FizzySteamworks/LegacyCommon.cs new file mode 100644 index 000000000..f9d19282f --- /dev/null +++ b/FizzySteamworks/LegacyCommon.cs @@ -0,0 +1,180 @@ +#if !DISABLESTEAMWORKS +using Steamworks; +using System; +using System.Collections; +using UnityEngine; + +namespace Mirror.FizzySteam +{ + public abstract class LegacyCommon + { + private EP2PSend[] channels; + private int internal_ch => channels.Length; + + protected enum InternalMessages : byte + { + CONNECT, + ACCEPT_CONNECT, + DISCONNECT + } + + private Steamworks.Callback callback_OnNewConnection = null; + private Steamworks.Callback callback_OnConnectFail = null; + + protected readonly FizzySteamworks transport; + + protected LegacyCommon(FizzySteamworks transport) + { + channels = transport.Channels; + + callback_OnNewConnection = Steamworks.Callback.Create(OnNewConnection); + callback_OnConnectFail = Steamworks.Callback.Create(OnConnectFail); + + this.transport = transport; + } + + protected void Dispose() + { + if (callback_OnNewConnection != null) + { + callback_OnNewConnection.Dispose(); + callback_OnNewConnection = null; + } + + if (callback_OnConnectFail != null) + { + callback_OnConnectFail.Dispose(); + callback_OnConnectFail = null; + } + } + + protected abstract void OnNewConnection(P2PSessionRequest_t result); + + private void OnConnectFail(P2PSessionConnectFail_t result) + { + OnConnectionFailed(result.m_steamIDRemote); + CloseP2PSessionWithUser(result.m_steamIDRemote); + + switch (result.m_eP2PSessionError) + { + case 1: + Debug.LogError("Connection failed: The target user is not running the same game."); + break; + case 2: + Debug.LogError("Connection failed: The local user doesn't own the app that is running."); + break; + case 3: + Debug.LogError("Connection failed: Target user isn't connected to Steam."); + break; + case 4: + Debug.LogError("Connection failed: The connection timed out because the target user didn't respond."); + break; + default: + Debug.LogError("Connection failed: Unknown error."); + break; + } + } + + protected void SendInternal(CSteamID target, InternalMessages type) + { +#if UNITY_SERVER + SteamGameServerNetworking.SendP2PPacket(target, new byte[] { (byte)type }, 1, EP2PSend.k_EP2PSendReliable, internal_ch); +#else + SteamNetworking.SendP2PPacket(target, new byte[] { (byte)type }, 1, EP2PSend.k_EP2PSendReliable, internal_ch); +#endif + } + protected void Send(CSteamID host, byte[] msgBuffer, int channel) + { +#if UNITY_SERVER + SteamGameServerNetworking.SendP2PPacket(host, msgBuffer, (uint)msgBuffer.Length, channels[Mathf.Min(channel, channels.Length - 1)], channel); +#else + SteamNetworking.SendP2PPacket(host, msgBuffer, (uint)msgBuffer.Length, channels[Mathf.Min(channel, channels.Length - 1)], channel); +#endif + } + + private bool Receive(out CSteamID clientSteamID, out byte[] receiveBuffer, int channel) + { +#if UNITY_SERVER + if (SteamGameServerNetworking.IsP2PPacketAvailable(out uint packetSize, channel)) + { + receiveBuffer = new byte[packetSize]; + return SteamGameServerNetworking.ReadP2PPacket(receiveBuffer, packetSize, out _, out clientSteamID, channel); + } +#else + if (SteamNetworking.IsP2PPacketAvailable(out uint packetSize, channel)) + { + receiveBuffer = new byte[packetSize]; + return SteamNetworking.ReadP2PPacket(receiveBuffer, packetSize, out _, out clientSteamID, channel); + } +#endif + + receiveBuffer = null; + clientSteamID = CSteamID.Nil; + return false; + } + + protected void CloseP2PSessionWithUser(CSteamID clientSteamID) + { +#if UNITY_SERVER + SteamGameServerNetworking.CloseP2PSessionWithUser(clientSteamID); +#else + SteamNetworking.CloseP2PSessionWithUser(clientSteamID); +#endif + } + + protected void WaitForClose(CSteamID cSteamID) + { + if (transport.enabled) + { + transport.StartCoroutine(DelayedClose(cSteamID)); + } + else + { + CloseP2PSessionWithUser(cSteamID); + } + } + + private IEnumerator DelayedClose(CSteamID cSteamID) + { + yield return null; + CloseP2PSessionWithUser(cSteamID); + } + + public void ReceiveData() + { + try + { + while (transport.enabled && Receive(out CSteamID clientSteamID, out byte[] internalMessage, internal_ch)) + { + if (internalMessage.Length == 1) + { + OnReceiveInternalData((InternalMessages)internalMessage[0], clientSteamID); + return; // Wait one frame + } + else + { + Debug.Log("Incorrect package length on internal channel."); + } + } + + for (int chNum = 0; chNum < channels.Length; chNum++) + { + while (transport.enabled && Receive(out CSteamID clientSteamID, out byte[] receiveBuffer, chNum)) + { + OnReceiveData(receiveBuffer, clientSteamID, chNum); + } + } + + } + catch (Exception e) + { + Debug.LogException(e); + } + } + + protected abstract void OnReceiveInternalData(InternalMessages type, CSteamID clientSteamID); + protected abstract void OnReceiveData(byte[] data, CSteamID clientSteamID, int channel); + protected abstract void OnConnectionFailed(CSteamID remoteId); + } +} +#endif // !DISABLESTEAMWORKS \ No newline at end of file diff --git a/FizzySteamworks/LegacyServer.cs b/FizzySteamworks/LegacyServer.cs new file mode 100644 index 000000000..7ce89e1ec --- /dev/null +++ b/FizzySteamworks/LegacyServer.cs @@ -0,0 +1,170 @@ +#if !DISABLESTEAMWORKS +using Steamworks; +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace Mirror.FizzySteam +{ + public class LegacyServer : LegacyCommon, IServer + { + private event Action OnConnected; + private event Action OnReceivedData; + private event Action OnDisconnected; + private event Action OnReceivedError; + + private BidirectionalDictionary steamToMirrorIds; + private int maxConnections; + private int nextConnectionID; + + public static LegacyServer CreateServer(FizzySteamworks transport, int maxConnections) + { + LegacyServer s = new LegacyServer(transport, maxConnections); + + s.OnConnected += (id) => transport.OnServerConnected.Invoke(id); + s.OnDisconnected += (id) => transport.OnServerDisconnected.Invoke(id); + s.OnReceivedData += (id, data, channel) => transport.OnServerDataReceived.Invoke(id, new ArraySegment(data), channel); + s.OnReceivedError += (id, error, reason) => transport.OnServerError.Invoke(id, error, reason); + + try + { +#if UNITY_SERVER + InteropHelp.TestIfAvailableGameServer(); +#else + InteropHelp.TestIfAvailableClient(); +#endif + } + catch + { + Debug.LogError("SteamWorks not initialized."); + } + + return s; + } + + private LegacyServer(FizzySteamworks transport, int maxConnections) : base(transport) + { + this.maxConnections = maxConnections; + steamToMirrorIds = new BidirectionalDictionary(); + nextConnectionID = 1; + } + + protected override void OnNewConnection(P2PSessionRequest_t result) + { +#if UNITY_SERVER + SteamGameServerNetworking.AcceptP2PSessionWithUser(result.m_steamIDRemote); +#else + SteamNetworking.AcceptP2PSessionWithUser(result.m_steamIDRemote); +#endif + } + + protected override void OnReceiveInternalData(InternalMessages type, CSteamID clientSteamID) + { + switch (type) + { + case InternalMessages.CONNECT: + if (steamToMirrorIds.Count >= maxConnections) + { + SendInternal(clientSteamID, InternalMessages.DISCONNECT); + return; + } + + SendInternal(clientSteamID, InternalMessages.ACCEPT_CONNECT); + + int connectionId = nextConnectionID++; + steamToMirrorIds.Add(clientSteamID, connectionId); + OnConnected.Invoke(connectionId); + Debug.Log($"Client with SteamID {clientSteamID} connected. Assigning connection id {connectionId}"); + break; + case InternalMessages.DISCONNECT: + if (steamToMirrorIds.TryGetValue(clientSteamID, out int connId)) + { + OnDisconnected.Invoke(connId); + CloseP2PSessionWithUser(clientSteamID); + steamToMirrorIds.Remove(clientSteamID); + Debug.Log($"Client with SteamID {clientSteamID} disconnected."); + } + + break; + default: + Debug.Log("Received unknown message type"); + break; + } + } + + protected override void OnReceiveData(byte[] data, CSteamID clientSteamID, int channel) + { + if (steamToMirrorIds.TryGetValue(clientSteamID, out int connectionId)) + { + OnReceivedData.Invoke(connectionId, data, channel); + } + else + { + CloseP2PSessionWithUser(clientSteamID); + Debug.LogError("Data received from steam client thats not known " + clientSteamID); + OnReceivedError.Invoke(-1, TransportError.DnsResolve, "ERROR Unknown SteamID"); + } + } + + public void Disconnect(int connectionId) + { + if (steamToMirrorIds.TryGetValue(connectionId, out CSteamID steamID)) + { + SendInternal(steamID, InternalMessages.DISCONNECT); + steamToMirrorIds.Remove(connectionId); + } + else + { + Debug.LogWarning("Trying to disconnect unknown connection id: " + connectionId); + } + } + + public void Shutdown() + { + foreach (KeyValuePair client in steamToMirrorIds) + { + Disconnect(client.Value); + WaitForClose(client.Key); + } + + Dispose(); + } + + public void Send(int connectionId, byte[] data, int channelId) + { + if (steamToMirrorIds.TryGetValue(connectionId, out CSteamID steamId)) + { + Send(steamId, data, channelId); + } + else + { + Debug.LogError("Trying to send on unknown connection: " + connectionId); + OnReceivedError.Invoke(connectionId, TransportError.Unexpected, "ERROR Unknown Connection"); + } + } + + public string ServerGetClientAddress(int connectionId) + { + if (steamToMirrorIds.TryGetValue(connectionId, out CSteamID steamId)) + { + return steamId.ToString(); + } + else + { + Debug.LogError("Trying to get info on unknown connection: " + connectionId); + OnReceivedError.Invoke(connectionId, TransportError.Unexpected, "ERROR Unknown Connection"); + return string.Empty; + } + } + + protected override void OnConnectionFailed(CSteamID remoteId) + { + int connectionId = steamToMirrorIds.TryGetValue(remoteId, out int connId) ? connId : nextConnectionID++; + OnDisconnected.Invoke(connectionId); + + steamToMirrorIds.Remove(remoteId); + } + public void FlushData() { } + } +} +#endif // !DISABLESTEAMWORKS \ No newline at end of file diff --git a/FizzySteamworks/NextClient.cs b/FizzySteamworks/NextClient.cs new file mode 100644 index 000000000..17b722150 --- /dev/null +++ b/FizzySteamworks/NextClient.cs @@ -0,0 +1,242 @@ +#if !DISABLESTEAMWORKS +using Steamworks; +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using UnityEngine; + +namespace Mirror.FizzySteam +{ + public class NextClient : NextCommon, IClient + { + public bool Connected { get; private set; } + public bool Error { get; private set; } + + private TimeSpan ConnectionTimeout; + + private event Action OnReceivedData; + private event Action OnConnected; + private event Action OnDisconnected; + // CHANGED + private event Action OnReceivedError; + private Steamworks.Callback c_onConnectionChange = null; + + private CancellationTokenSource cancelToken; + private TaskCompletionSource connectedComplete; + private CSteamID hostSteamID = CSteamID.Nil; + private HSteamNetConnection HostConnection; + private List BufferedData; + + private NextClient(FizzySteamworks transport) + { + ConnectionTimeout = TimeSpan.FromSeconds(Math.Max(1, transport.Timeout)); + BufferedData = new List(); + } + + public static NextClient CreateClient(FizzySteamworks transport, string host) + { + NextClient c = new NextClient(transport); + + c.OnConnected += () => transport.OnClientConnected.Invoke(); + c.OnDisconnected += () => transport.OnClientDisconnected.Invoke(); + c.OnReceivedData += (data, ch) => transport.OnClientDataReceived.Invoke(new ArraySegment(data), ch); + // CHANGED + c.OnReceivedError += (error, reason) => transport.OnClientError.Invoke(error, reason); + + try + { +#if UNITY_SERVER + SteamGameServerNetworkingUtils.InitRelayNetworkAccess(); +#else + SteamNetworkingUtils.InitRelayNetworkAccess(); +#endif + c.Connect(host); + } + catch (Exception ex) + { + Debug.LogException(ex); + c.OnConnectionFailed(); + } + + return c; + } + + private async void Connect(string host) + { + cancelToken = new CancellationTokenSource(); + c_onConnectionChange = Steamworks.Callback.Create(OnConnectionStatusChanged); + + try + { + hostSteamID = new CSteamID(UInt64.Parse(host)); + connectedComplete = new TaskCompletionSource(); + OnConnected += SetConnectedComplete; + + SteamNetworkingIdentity smi = new SteamNetworkingIdentity(); + smi.SetSteamID(hostSteamID); + + SteamNetworkingConfigValue_t[] options = new SteamNetworkingConfigValue_t[] { }; + HostConnection = SteamNetworkingSockets.ConnectP2P(ref smi, 0, options.Length, options); + + Task connectedCompleteTask = connectedComplete.Task; + Task timeOutTask = Task.Delay(ConnectionTimeout, cancelToken.Token); + + if (await Task.WhenAny(connectedCompleteTask, timeOutTask) != connectedCompleteTask) + { + if (cancelToken.IsCancellationRequested) + { + Debug.LogError($"The connection attempt was cancelled."); + } + else if (timeOutTask.IsCompleted) + { + Debug.LogError($"Connection to {host} timed out."); + // CHANGED + OnReceivedError.Invoke(TransportError.Timeout, $"Connection to {host} timed out."); + } + + OnConnected -= SetConnectedComplete; + OnConnectionFailed(); + } + + OnConnected -= SetConnectedComplete; + } + catch (FormatException) + { + Debug.LogError($"Connection string was not in the right format. Did you enter a SteamId?"); + // CHANGED + OnReceivedError.Invoke(TransportError.DnsResolve, $"Connection string was not in the right format. Did you enter a SteamId?"); + Error = true; + OnConnectionFailed(); + } + catch (Exception ex) + { + Debug.LogError(ex.Message); + // CHANGED + OnReceivedError.Invoke(TransportError.Unexpected, ex.Message); + Error = true; + OnConnectionFailed(); + } + finally + { + if (Error) + { + Debug.LogError("Connection failed."); + OnConnectionFailed(); + } + } + } + + private void OnConnectionStatusChanged(SteamNetConnectionStatusChangedCallback_t param) + { + ulong clientSteamID = param.m_info.m_identityRemote.GetSteamID64(); + if (param.m_info.m_eState == ESteamNetworkingConnectionState.k_ESteamNetworkingConnectionState_Connected) + { + Connected = true; + OnConnected.Invoke(); + Debug.Log("Connection established."); + + if (BufferedData.Count > 0) + { + Debug.Log($"{BufferedData.Count} received before connection was established. Processing now."); + { + foreach (Action a in BufferedData) + { + a(); + } + } + } + } + else if (param.m_info.m_eState == ESteamNetworkingConnectionState.k_ESteamNetworkingConnectionState_ClosedByPeer || param.m_info.m_eState == ESteamNetworkingConnectionState.k_ESteamNetworkingConnectionState_ProblemDetectedLocally) + { + Debug.Log($"Connection was closed by peer, {param.m_info.m_szEndDebug}"); + // CHANGED + OnReceivedError.Invoke(TransportError.ConnectionClosed, $"Connection was closed by peer, {param.m_info.m_szEndDebug}"); + Disconnect(); + } + else + { + Debug.Log($"Connection state changed: {param.m_info.m_eState.ToString()} - {param.m_info.m_szEndDebug}"); + } + } + + public void Disconnect() + { + cancelToken?.Cancel(); + Dispose(); + + if (HostConnection.m_HSteamNetConnection != 0) + { + Debug.Log("Sending Disconnect message"); + SteamNetworkingSockets.CloseConnection(HostConnection, 0, "Graceful disconnect", false); + HostConnection.m_HSteamNetConnection = 0; + } + } + + protected void Dispose() + { + if (c_onConnectionChange != null) + { + c_onConnectionChange.Dispose(); + c_onConnectionChange = null; + } + } + + private void InternalDisconnect() + { + Connected = false; + OnDisconnected.Invoke(); + Debug.Log("Disconnected."); + SteamNetworkingSockets.CloseConnection(HostConnection, 0, "Disconnected", false); + } + + public void ReceiveData() + { + IntPtr[] ptrs = new IntPtr[MAX_MESSAGES]; + int messageCount; + + if ((messageCount = SteamNetworkingSockets.ReceiveMessagesOnConnection(HostConnection, ptrs, MAX_MESSAGES)) > 0) + { + for (int i = 0; i < messageCount; i++) + { + (byte[] data, int ch) = ProcessMessage(ptrs[i]); + if (Connected) + { + OnReceivedData(data, ch); + } + else + { + BufferedData.Add(() => OnReceivedData(data, ch)); + } + } + } + } + + public void Send(byte[] data, int channelId) + { + EResult res = SendSocket(HostConnection, data, channelId); + + if (res == EResult.k_EResultNoConnection || res == EResult.k_EResultInvalidParam) + { + Debug.Log($"Connection to server was lost."); + // CHANGED + OnReceivedError.Invoke(TransportError.ConnectionClosed, $"Connection to server was lost."); + InternalDisconnect(); + } + else if (res != EResult.k_EResultOK) + { + Debug.LogError($"Could not send: {res.ToString()}"); + // CHANGED + OnReceivedError.Invoke(TransportError.Unexpected, $"Could not send: {res.ToString()}"); + } + } + + private void SetConnectedComplete() => connectedComplete.SetResult(connectedComplete.Task); + private void OnConnectionFailed() => OnDisconnected.Invoke(); + public void FlushData() + { + SteamNetworkingSockets.FlushMessagesOnConnection(HostConnection); + } + } +} +#endif // !DISABLESTEAMWORKS \ No newline at end of file diff --git a/FizzySteamworks/NextCommon.cs b/FizzySteamworks/NextCommon.cs new file mode 100644 index 000000000..175beaccf --- /dev/null +++ b/FizzySteamworks/NextCommon.cs @@ -0,0 +1,48 @@ +#if !DISABLESTEAMWORKS +using Steamworks; +using System; +using System.Runtime.InteropServices; +using UnityEngine; + +namespace Mirror.FizzySteam +{ + public abstract class NextCommon + { + protected const int MAX_MESSAGES = 256; + + protected EResult SendSocket(HSteamNetConnection conn, byte[] data, int channelId) + { + Array.Resize(ref data, data.Length + 1); + data[data.Length - 1] = (byte)channelId; + + GCHandle pinnedArray = GCHandle.Alloc(data, GCHandleType.Pinned); + IntPtr pData = pinnedArray.AddrOfPinnedObject(); + int sendFlag = channelId == Channels.Unreliable ? Constants.k_nSteamNetworkingSend_Unreliable : Constants.k_nSteamNetworkingSend_Reliable; +#if UNITY_SERVER + EResult res = SteamGameServerNetworkingSockets.SendMessageToConnection(conn, pData, (uint)data.Length, sendFlag, out long _); +#else + EResult res = SteamNetworkingSockets.SendMessageToConnection(conn, pData, (uint)data.Length, sendFlag, out long _); +#endif + if (res != EResult.k_EResultOK) + { + Debug.LogWarning($"Send issue: {res}"); + } + + pinnedArray.Free(); + return res; + } + + protected (byte[], int) ProcessMessage(IntPtr ptrs) + { + SteamNetworkingMessage_t data = Marshal.PtrToStructure(ptrs); + byte[] managedArray = new byte[data.m_cbSize]; + Marshal.Copy(data.m_pData, managedArray, 0, data.m_cbSize); + SteamNetworkingMessage_t.Release(ptrs); + + int channel = managedArray[managedArray.Length - 1]; + Array.Resize(ref managedArray, managedArray.Length - 1); + return (managedArray, channel); + } + } +} +#endif // !DISABLESTEAMWORKS \ No newline at end of file diff --git a/FizzySteamworks/NextServer.cs b/FizzySteamworks/NextServer.cs new file mode 100644 index 000000000..94d2bccee --- /dev/null +++ b/FizzySteamworks/NextServer.cs @@ -0,0 +1,246 @@ +#if !DISABLESTEAMWORKS +using Steamworks; +using System; +using System.Linq; +using UnityEngine; + +namespace Mirror.FizzySteam +{ + public class NextServer : NextCommon, IServer + { + private event Action OnConnected; + private event Action OnReceivedData; + private event Action OnDisconnected; + private event Action OnReceivedError; + + private BidirectionalDictionary connToMirrorID; + private BidirectionalDictionary steamIDToMirrorID; + private int maxConnections; + private int nextConnectionID; + + private HSteamListenSocket listenSocket; + + private Steamworks.Callback c_onConnectionChange = null; + + private NextServer(int maxConnections) + { + this.maxConnections = maxConnections; + connToMirrorID = new BidirectionalDictionary(); + steamIDToMirrorID = new BidirectionalDictionary(); + nextConnectionID = 1; + c_onConnectionChange = Steamworks.Callback.Create(OnConnectionStatusChanged); + } + + public static NextServer CreateServer(FizzySteamworks transport, int maxConnections) + { + NextServer s = new NextServer(maxConnections); + + s.OnConnected += (id) => transport.OnServerConnected.Invoke(id); + s.OnDisconnected += (id) => transport.OnServerDisconnected.Invoke(id); + s.OnReceivedData += (id, data, ch) => transport.OnServerDataReceived.Invoke(id, new ArraySegment(data), ch); + s.OnReceivedError += (id, error, reason) => transport.OnServerError.Invoke(id, error, reason); + + try + { +#if UNITY_SERVER + SteamGameServerNetworkingUtils.InitRelayNetworkAccess(); +#else + SteamNetworkingUtils.InitRelayNetworkAccess(); +#endif + } + catch (Exception ex) + { + Debug.LogException(ex); + } + + s.Host(); + + return s; + } + + private void Host() + { + SteamNetworkingConfigValue_t[] options = new SteamNetworkingConfigValue_t[] { }; +#if UNITY_SERVER + listenSocket = SteamGameServerNetworkingSockets.CreateListenSocketP2P(0, options.Length, options); +#else + listenSocket = SteamNetworkingSockets.CreateListenSocketP2P(0, options.Length, options); +#endif + } + + private void OnConnectionStatusChanged(SteamNetConnectionStatusChangedCallback_t param) + { + ulong clientSteamID = param.m_info.m_identityRemote.GetSteamID64(); + if (param.m_info.m_eState == ESteamNetworkingConnectionState.k_ESteamNetworkingConnectionState_Connecting) + { + if (connToMirrorID.Count >= maxConnections) + { + Debug.Log($"Incoming connection {clientSteamID} would exceed max connection count. Rejecting."); +#if UNITY_SERVER + SteamGameServerNetworkingSockets.CloseConnection(param.m_hConn, 0, "Max Connection Count", false); +#else + SteamNetworkingSockets.CloseConnection(param.m_hConn, 0, "Max Connection Count", false); +#endif + return; + } + + EResult res; + +#if UNITY_SERVER + if ((res = SteamGameServerNetworkingSockets.AcceptConnection(param.m_hConn)) == EResult.k_EResultOK) +#else + if ((res = SteamNetworkingSockets.AcceptConnection(param.m_hConn)) == EResult.k_EResultOK) +#endif + { + Debug.Log($"Accepting connection {clientSteamID}"); + } + else + { + Debug.Log($"Connection {clientSteamID} could not be accepted: {res.ToString()}"); + } + } + else if (param.m_info.m_eState == ESteamNetworkingConnectionState.k_ESteamNetworkingConnectionState_Connected) + { + int connectionId = nextConnectionID++; + connToMirrorID.Add(param.m_hConn, connectionId); + steamIDToMirrorID.Add(param.m_info.m_identityRemote.GetSteamID(), connectionId); + OnConnected.Invoke(connectionId); + Debug.Log($"Client with SteamID {clientSteamID} connected. Assigning connection id {connectionId}"); + } + else if (param.m_info.m_eState == ESteamNetworkingConnectionState.k_ESteamNetworkingConnectionState_ClosedByPeer || param.m_info.m_eState == ESteamNetworkingConnectionState.k_ESteamNetworkingConnectionState_ProblemDetectedLocally) + { + if (connToMirrorID.TryGetValue(param.m_hConn, out int connId)) + { + InternalDisconnect(connId, param.m_hConn); + } + } + else + { + Debug.Log($"Connection {clientSteamID} state changed: {param.m_info.m_eState.ToString()}"); + } + } + + private void InternalDisconnect(int connId, HSteamNetConnection socket) + { + OnDisconnected.Invoke(connId); +#if UNITY_SERVER + SteamGameServerNetworkingSockets.CloseConnection(socket, 0, "Graceful disconnect", false); +#else + SteamNetworkingSockets.CloseConnection(socket, 0, "Graceful disconnect", false); +#endif + connToMirrorID.Remove(connId); + steamIDToMirrorID.Remove(connId); + Debug.Log($"Client with ConnectionID {connId} disconnected."); + } + + public void Disconnect(int connectionId) + { + if (connToMirrorID.TryGetValue(connectionId, out HSteamNetConnection conn)) + { + Debug.Log($"Connection id {connectionId} disconnected."); +#if UNITY_SERVER + SteamGameServerNetworkingSockets.CloseConnection(conn, 0, "Disconnected by server", false); +#else + SteamNetworkingSockets.CloseConnection(conn, 0, "Disconnected by server", false); +#endif + steamIDToMirrorID.Remove(connectionId); + connToMirrorID.Remove(connectionId); + OnDisconnected(connectionId); + } + else + { + Debug.LogWarning("Trying to disconnect unknown connection id: " + connectionId); + } + } + + public void FlushData() + { + foreach (HSteamNetConnection conn in connToMirrorID.FirstTypes) + { +#if UNITY_SERVER + SteamGameServerNetworkingSockets.FlushMessagesOnConnection(conn); +#else + SteamNetworkingSockets.FlushMessagesOnConnection(conn); +#endif + } + } + + public void ReceiveData() + { + foreach (HSteamNetConnection conn in connToMirrorID.FirstTypes.ToList()) + { + if (connToMirrorID.TryGetValue(conn, out int connId)) + { + IntPtr[] ptrs = new IntPtr[MAX_MESSAGES]; + int messageCount; + +#if UNITY_SERVER + if ((messageCount = SteamGameServerNetworkingSockets.ReceiveMessagesOnConnection(conn, ptrs, MAX_MESSAGES)) > 0) +#else + if ((messageCount = SteamNetworkingSockets.ReceiveMessagesOnConnection(conn, ptrs, MAX_MESSAGES)) > 0) +#endif + { + for (int i = 0; i < messageCount; i++) + { + (byte[] data, int ch) = ProcessMessage(ptrs[i]); + OnReceivedData(connId, data, ch); + } + } + } + } + } + + public void Send(int connectionId, byte[] data, int channelId) + { + if (connToMirrorID.TryGetValue(connectionId, out HSteamNetConnection conn)) + { + EResult res = SendSocket(conn, data, channelId); + + if (res == EResult.k_EResultNoConnection || res == EResult.k_EResultInvalidParam) + { + Debug.Log($"Connection to {connectionId} was lost."); + InternalDisconnect(connectionId, conn); + } + else if (res != EResult.k_EResultOK) + { + Debug.LogError($"Could not send: {res.ToString()}"); + } + } + else + { + Debug.LogError("Trying to send on unknown connection: " + connectionId); + OnReceivedError.Invoke(connectionId, TransportError.Unexpected, "ERROR Unknown Connection"); + } + } + + public string ServerGetClientAddress(int connectionId) + { + if (steamIDToMirrorID.TryGetValue(connectionId, out CSteamID steamId)) + { + return steamId.ToString(); + } + else + { + Debug.LogError("Trying to get info on unknown connection: " + connectionId); + OnReceivedError.Invoke(connectionId, TransportError.Unexpected, "ERROR Unknown Connection"); + return string.Empty; + } + } + + public void Shutdown() + { +#if UNITY_SERVER + SteamGameServerNetworkingSockets.CloseListenSocket(listenSocket); +#else + SteamNetworkingSockets.CloseListenSocket(listenSocket); +#endif + + if (c_onConnectionChange != null) + { + c_onConnectionChange.Dispose(); + c_onConnectionChange = null; + } + } + } +} +#endif // !DISABLESTEAMWORKS \ No newline at end of file diff --git a/Lib/Mirror.Components.dll b/Lib/Mirror.Components.dll new file mode 100644 index 000000000..a94e8984a Binary files /dev/null and b/Lib/Mirror.Components.dll differ diff --git a/Mirror/Mirror.Transports.dll b/Lib/Mirror.Transports.dll similarity index 55% rename from Mirror/Mirror.Transports.dll rename to Lib/Mirror.Transports.dll index f9b832637..a9bf52f3c 100644 Binary files a/Mirror/Mirror.Transports.dll and b/Lib/Mirror.Transports.dll differ diff --git a/Lib/Mirror.dll b/Lib/Mirror.dll new file mode 100644 index 000000000..532527216 Binary files /dev/null and b/Lib/Mirror.dll differ diff --git a/Mirror/Telepathy.dll b/Lib/Telepathy.dll similarity index 99% rename from Mirror/Telepathy.dll rename to Lib/Telepathy.dll index c6b7e1eda..e53ea3a30 100644 Binary files a/Mirror/Telepathy.dll and b/Lib/Telepathy.dll differ diff --git a/UniTask/UniTask.dll b/Lib/UniTask.dll similarity index 100% rename from UniTask/UniTask.dll rename to Lib/UniTask.dll diff --git a/Lib/com.rlabrecque.steamworks.net.dll b/Lib/com.rlabrecque.steamworks.net.dll new file mode 100644 index 000000000..39939de16 Binary files /dev/null and b/Lib/com.rlabrecque.steamworks.net.dll differ diff --git a/Lib/kcp2k.dll b/Lib/kcp2k.dll new file mode 100644 index 000000000..76f61323b Binary files /dev/null and b/Lib/kcp2k.dll differ diff --git a/Mirror/Mirror.Components.dll b/Mirror/Mirror.Components.dll deleted file mode 100644 index de1a9bbb6..000000000 Binary files a/Mirror/Mirror.Components.dll and /dev/null differ diff --git a/Mirror/Mirror.dll b/Mirror/Mirror.dll deleted file mode 100644 index 60cc974bf..000000000 Binary files a/Mirror/Mirror.dll and /dev/null differ diff --git a/Mirror/kcp2k.dll b/Mirror/kcp2k.dll deleted file mode 100644 index 2614f3894..000000000 Binary files a/Mirror/kcp2k.dll and /dev/null differ diff --git a/MirrorWeaver/License.md b/MirrorWeaver/LICENSE similarity index 97% rename from MirrorWeaver/License.md rename to MirrorWeaver/LICENSE index 887d9b65c..bbd054789 100644 --- a/MirrorWeaver/License.md +++ b/MirrorWeaver/LICENSE @@ -1,4 +1,4 @@ -MIT License +MIT License Copyright (c) 2015, Unity Technologies Copyright (c) 2019, vis2k, Paul and Contributors @@ -19,4 +19,4 @@ 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. \ No newline at end of file +THE SOFTWARE. diff --git a/MirrorWeaver/MirrorWeaver.csproj b/MirrorWeaver/MirrorWeaver.csproj index 27fa86ea9..43603408d 100644 --- a/MirrorWeaver/MirrorWeaver.csproj +++ b/MirrorWeaver/MirrorWeaver.csproj @@ -1,21 +1,24 @@  Exe - License.md + Mirror Weaver + Mirror Weaver Mirror Weaver + Unity Technologies, vis2k, Paul and Contributors, William Corby Unity Technologies, vis2k, Paul and Contributors, William Corby + LICENSE - - - - - True \ + + + + + diff --git a/MirrorWeaver/Weaver/Extensions.cs b/MirrorWeaver/Weaver/Extensions.cs index db6610cb9..f02c4c8e9 100644 --- a/MirrorWeaver/Weaver/Extensions.cs +++ b/MirrorWeaver/Weaver/Extensions.cs @@ -242,7 +242,16 @@ public static IEnumerable FindAllPublicFields(this TypeDefiniti { foreach (FieldDefinition field in typeDefinition.Fields) { - if (field.IsStatic || field.IsPrivate) + // ignore static, private, protected fields + // fixes: https://github.com/MirrorNetworking/Mirror/issues/3485 + // credit: James Frowen + if (field.IsStatic || field.IsPrivate || field.IsFamily) + continue; + + // also ignore internal fields + // we dont want to create different writers for this type if they are in current dll or another dll + // so we have to ignore internal in all cases + if (field.IsAssembly) continue; if (field.IsNotSerialized) diff --git a/MirrorWeaver/Weaver/Processors/ServerClientAttributeProcessor.cs b/MirrorWeaver/Weaver/Processors/ServerClientAttributeProcessor.cs index 5d5289770..c7eb5c34b 100644 --- a/MirrorWeaver/Weaver/Processors/ServerClientAttributeProcessor.cs +++ b/MirrorWeaver/Weaver/Processors/ServerClientAttributeProcessor.cs @@ -123,7 +123,16 @@ static void InjectGuardParameters(MethodDefinition md, ILProcessor worker, Instr ParameterDefinition param = md.Parameters[index]; if (param.IsOut) { - TypeReference elementType = param.ParameterType.GetElementType(); + // this causes IL2CPP build issues with generic out parameters: + // https://github.com/MirrorNetworking/Mirror/issues/3482 + // TypeReference elementType = param.ParameterType.GetElementType(); + // + // instead we need to use ElementType not GetElementType() + // GetElementType() will get the element type of the inner elementType + // which will return wrong type for arrays and generic + // credit: JamesFrowen + ByReferenceType byRefType = (ByReferenceType)param.ParameterType; + TypeReference elementType = byRefType.ElementType; md.Body.Variables.Add(new VariableDefinition(elementType)); md.Body.InitLocals = true; diff --git a/QSB-NH/Patches/GameStateMessagePatches.cs b/QSB-NH/Patches/GameStateMessagePatches.cs new file mode 100644 index 000000000..df2a746eb --- /dev/null +++ b/QSB-NH/Patches/GameStateMessagePatches.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using HarmonyLib; +using Mirror; +using NewHorizons; +using QSB; +using QSB.Patches; +using QSB.Player; +using QSB.SaveSync.Messages; +using QSB.Utility; + +namespace QSBNH.Patches; + + +/// +/// extremely jank way to inject system and NH addons when joining. +/// this should probably be split into its own separate message, but it doesnt really matter :P +/// +/// BUG: completely explodes if one person has NH and the other does not +/// +internal class GameStateMessagePatches : QSBPatch +{ + public override QSBPatchTypes Type => QSBPatchTypes.OnModStart; + + private static string _initialSystem; + private static int[] _hostAddonHash; + + [HarmonyPostfix] + [HarmonyPatch(typeof(GameStateMessage), nameof(GameStateMessage.Serialize))] + public static void GameStateMessage_Serialize(GameStateMessage __instance, NetworkWriter writer) + { + var currentSystem = QSBNH.Instance.NewHorizonsAPI.GetCurrentStarSystem(); + + writer.Write(currentSystem); + writer.WriteArray(QSBNH.HashAddonsForSystem(currentSystem)); + } + + [HarmonyPostfix] + [HarmonyPatch(typeof(GameStateMessage), nameof(GameStateMessage.Deserialize))] + public static void GameStateMessage_Deserialize(GameStateMessage __instance, NetworkReader reader) + { + _initialSystem = reader.Read(); + _hostAddonHash = reader.ReadArray(); + } + + [HarmonyPostfix] + [HarmonyPatch(typeof(GameStateMessage), nameof(GameStateMessage.OnReceiveRemote))] + public static void GameStateMessage_OnReceiveRemote() + { + if (QSBCore.IsHost) + { + DebugLog.DebugWrite($"Why is the host being given the initial state info?"); + } + else + { + DebugLog.DebugWrite($"Player#{QSBPlayerManager.LocalPlayerId} is being sent to {_initialSystem}"); + + WarpManager.RemoteChangeStarSystem(_initialSystem, false, false, _hostAddonHash); + } + } +} diff --git a/QSB-NH/Patches/NewHorizonsDataPatches.cs b/QSB-NH/Patches/NewHorizonsDataPatches.cs new file mode 100644 index 000000000..ba949c3fb --- /dev/null +++ b/QSB-NH/Patches/NewHorizonsDataPatches.cs @@ -0,0 +1,33 @@ +using HarmonyLib; +using NewHorizons.External; +using QSB; +using QSB.Patches; +using QSB.SaveSync; +using QSB.Utility; + +namespace QSBNH.Patches; + +/// +/// pretends to be a new profile when in multiplayer so NH saves its data to a new place +/// +public class NewHorizonsDataPatches : QSBPatch +{ + public override QSBPatchTypes Type => QSBPatchTypes.OnModStart; + + [HarmonyPrefix] + [HarmonyPatch(typeof(NewHorizonsData), nameof(NewHorizonsData.GetProfileName))] + public static bool NewHorizonsData_GetProfileName(out string __result) + { + if (QSBCore.IsInMultiplayer) + { + __result = QSBStandaloneProfileManager.SharedInstance?.currentProfile?.profileName + "_mult"; + DebugLog.DebugWrite($"using fake multiplayer profile {__result} for NH"); + } + else + { + __result = QSBStandaloneProfileManager.SharedInstance?.currentProfile?.profileName; + } + + return false; + } +} diff --git a/QSB-NH/QSB-NH.csproj b/QSB-NH/QSB-NH.csproj new file mode 100644 index 000000000..3bd1280b8 --- /dev/null +++ b/QSB-NH/QSB-NH.csproj @@ -0,0 +1,32 @@ + + + + net48 + QSBNH + enable + $(OwmlDir)\Mods\Raicuparta.QuantumSpaceBuddies + CS1998;CS0649 + + + + + + + + + + + + + ..\Lib\Mirror.dll + + + lib\NewHorizons.dll + false + + + ..\Lib\UniTask.dll + + + + diff --git a/QSB-NH/QSBNH.cs b/QSB-NH/QSBNH.cs new file mode 100644 index 000000000..01c8e2ad9 --- /dev/null +++ b/QSB-NH/QSBNH.cs @@ -0,0 +1,59 @@ +using Mirror; +using NewHorizons; +using OWML.Common; +using OWML.ModHelper; +using QSB; +using QSB.Utility; +using UnityEngine; + +namespace QSBNH +{ + public class QSBNH : MonoBehaviour + { + public static QSBNH Instance; + + public INewHorizons NewHorizonsAPI; + + private void Start() + { + Instance = this; + DebugLog.DebugWrite($"Start of QSB-NH compatibility code.", MessageType.Success); + NewHorizonsAPI = QSBCore.Helper.Interaction.TryGetModApi("xen.NewHorizons"); + } + + public static string HashToMod(int hash) + { + foreach (var mod in NewHorizons.Main.MountedAddons) + { + var name = mod.ModHelper.Manifest.UniqueName; + if (name.GetStableHashCode() == hash) + { + return name; + } + } + + return null; + } + + public static int[] HashAddonsForSystem(string system) + { + if (NewHorizons.Main.BodyDict.TryGetValue(system, out var bodies)) + { + var addonHashes = bodies + .Where(x => x.Mod.ModHelper.Manifest.UniqueName != "xen.NewHorizons") + .Select(x => x.Mod.ModHelper.Manifest.UniqueName.GetStableHashCode()) + .Distinct(); + + var nhPlanetHashes = bodies + .Where(x => x.Mod.ModHelper.Manifest.UniqueName == "xen.NewHorizons") + .Select(x => x.Config.name.GetStableHashCode()); + + return addonHashes.Concat(nhPlanetHashes).ToArray(); + } + else + { + return null; + } + } + } +} diff --git a/QSB-NH/QuantumPlanet/QuantumPlanetManager.cs b/QSB-NH/QuantumPlanet/QuantumPlanetManager.cs new file mode 100644 index 000000000..540221656 --- /dev/null +++ b/QSB-NH/QuantumPlanet/QuantumPlanetManager.cs @@ -0,0 +1,13 @@ +using Cysharp.Threading.Tasks; +using QSB.WorldSync; +using QSBNH.QuantumPlanet.WorldObjects; + +namespace QSBNH.QuantumPlanet; +public class QuantumPlanetManager : WorldObjectManager +{ + public override WorldObjectScene WorldObjectScene => WorldObjectScene.Both; + public override bool DlcOnly => false; + + public override async UniTask BuildWorldObjects(OWScene scene, CancellationToken ct) => + QSBWorldSync.Init(); +} diff --git a/QSB-NH/QuantumPlanet/WorldObjects/QSBQuantumPlanet.cs b/QSB-NH/QuantumPlanet/WorldObjects/QSBQuantumPlanet.cs new file mode 100644 index 000000000..dbafc8cc7 --- /dev/null +++ b/QSB-NH/QuantumPlanet/WorldObjects/QSBQuantumPlanet.cs @@ -0,0 +1,7 @@ +using QSB.QuantumSync.WorldObjects; + +namespace QSBNH.QuantumPlanet.WorldObjects; + +public class QSBQuantumPlanet : QSBQuantumObject +{ +} diff --git a/QSB-NH/WarpManager.cs b/QSB-NH/WarpManager.cs new file mode 100644 index 000000000..6204856bb --- /dev/null +++ b/QSB-NH/WarpManager.cs @@ -0,0 +1,167 @@ +using HarmonyLib; +using NewHorizons; +using QSB.Menus; +using QSB.Messaging; +using QSB.Player; +using QSB; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Mirror; +using QSB.Patches; +using QSB.Utility; + +namespace QSBNH; +public static class WarpManager +{ + internal static bool RemoteWarp = false; + + private static void Kick(string reason) + { + DebugLog.DebugWrite(reason); + MenuManager.Instance.OnKicked(reason); + NetworkClient.Disconnect(); + } + + public static void RemoteChangeStarSystem(string system, bool ship, bool vessel, int[] hostAddonHash) + { + // Flag to not send a message + RemoteWarp = true; + + DebugLog.DebugWrite($"Remote request received to go to {system}"); + + if (!NewHorizons.Main.SystemDict.ContainsKey(system)) + { + // If you can't go to that system then you have to be disconnected + Kick($"You don't have the mod installed for {system}"); + } + else + { + var localHash = QSBNH.HashAddonsForSystem(system); + if (localHash != hostAddonHash) + { + var missingAddonHashes = hostAddonHash.Except(localHash); + var extraAddonHashes = localHash.Except(hostAddonHash); + + if (missingAddonHashes.Count() > 0) + { + Kick($"You are missing {missingAddonHashes.Count()} addon(s) that effect {system}"); + return; + } + + if (extraAddonHashes.Count() > 0) + { + var extraMods = extraAddonHashes.Select(x => QSBNH.HashToMod(x)); + + // TODO: Disable these mods for the client and do not kick them + + Kick($"You have {extraAddonHashes.Count()} extra addon(s) that effect {system}. Check the logs."); + DebugLog.DebugWrite($"You have mods affecting {system} that the host does not: {string.Join(", ", extraMods)}"); + return; + } + } + + NewHorizons.Main.Instance.ChangeCurrentStarSystem(system, ship, vessel); + } + } + + public class NHWarpMessage : QSBMessage + { + private string _starSystem; + private bool _shipWarp; + private bool _vesselWarp; + + public NHWarpMessage(string starSystem, bool shipWarp, bool vesselWarp) : base() + { + _starSystem = starSystem; + _shipWarp = shipWarp; + _vesselWarp = vesselWarp; + } + + public override void Serialize(NetworkWriter writer) + { + base.Serialize(writer); + + writer.Write(_starSystem); + writer.Write(_shipWarp); + writer.Write(_vesselWarp); + } + + public override void Deserialize(NetworkReader reader) + { + base.Deserialize(reader); + + _starSystem = reader.Read(); + _shipWarp = reader.Read(); + _vesselWarp = reader.Read(); + } + + public override void OnReceiveRemote() + { + DebugLog.DebugWrite($"Player#{From} is telling Player#{To} to warp to {_starSystem}"); + if (QSBCore.IsHost && !NewHorizons.Main.SystemDict.ContainsKey(_starSystem)) + { + // If the host doesn't have that system then we can't + DebugLog.DebugWrite($"The host doesn't have {_starSystem} installed: aborting"); + } + else + { + if (QSBCore.IsHost) + { + new NHWarpMessage(_starSystem, _shipWarp, _vesselWarp).Send(); + } + + RemoteChangeStarSystem(_starSystem, _shipWarp, _vesselWarp, QSBNH.HashAddonsForSystem(_starSystem)); + } + } + } + + public class NHWarpPatch : QSBPatch + { + public override QSBPatchTypes Type => QSBPatchTypes.OnModStart; + + [HarmonyPrefix] + [HarmonyPatch(typeof(NewHorizons.Main), nameof(NewHorizons.Main.ChangeCurrentStarSystem))] + public static bool NewHorizons_ChangeCurrentStarSystem(string newStarSystem, bool warp, bool vessel) + { + if (RemoteWarp) + { + // We're being told to warp so just do it + RemoteWarp = false; + return true; + } + + DebugLog.DebugWrite($"Local request received to go to {newStarSystem}"); + if (QSBCore.IsHost) + { + // The host will tell all other users to warp + DebugLog.DebugWrite($"Host: Telling others to go to {newStarSystem}"); + new NHWarpMessage(newStarSystem, warp, vessel).Send(); + // The host can now warp + return true; + } + else + { + // We're a client that has to tell the host to start warping people + DebugLog.DebugWrite($"Client: Telling host to send us to {newStarSystem}"); + new NHWarpMessage(newStarSystem, warp, vessel) { To = 0 }.Send(); + + // We have to wait for the host to get back to us + return false; + } + } + + [HarmonyPostfix] + [HarmonyPatch(typeof(NewHorizons.Main), nameof(NewHorizons.Main.ChangeCurrentStarSystem))] + public static void NewHorizons_ChangeCurrentStarSystem(NewHorizons.Main __instance) + { + if (__instance.IsWarpingFromShip) + { + // If QSB doesn't say we're piloting the ship then dont keep them on as the one warping + __instance.GetType().GetProperty(nameof(NewHorizons.Main.IsWarpingFromShip)).SetValue(__instance, QSBPlayerManager.LocalPlayer.FlyingShip); + } + } + } +} diff --git a/QSB-NH/lib/NewHorizons.dll b/QSB-NH/lib/NewHorizons.dll new file mode 100644 index 000000000..83f0afc8a Binary files /dev/null and b/QSB-NH/lib/NewHorizons.dll differ diff --git a/QSB.sln b/QSB.sln index e961dc4b0..4b1d685bc 100644 --- a/QSB.sln +++ b/QSB.sln @@ -9,6 +9,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution ProjectSection(SolutionItems) = preProject .gitignore = .gitignore DEVELOPMENT.md = DEVELOPMENT.md + .github\FUNDING.yml = .github\FUNDING.yml LICENSE = LICENSE README.md = README.md TRANSLATING.md = TRANSLATING.md @@ -16,11 +17,24 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MirrorWeaver", "MirrorWeaver\MirrorWeaver.csproj", "{DA8A467E-15BA-456C-9034-6EB80BAF1FF9}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EpicOnlineTransport", "EpicOnlineTransport\EpicOnlineTransport.csproj", "{971AA4A1-6729-40DE-AADF-2754F1E8783A}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FizzySteamworks", "FizzySteamworks\FizzySteamworks.csproj", "{D47034D8-B92D-47DA-884C-C76F735E2D5D}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EpicRerouter", "EpicRerouter\EpicRerouter.csproj", "{639EFAEE-C4A1-4DA2-8457-D0472A9F6343}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SteamRerouter", "SteamRerouter\SteamRerouter.csproj", "{750563ED-4F2A-42DD-81A3-3A85DEBB691E}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "APITestMod", "APITestMod\APITestMod.csproj", "{0A10143E-6C00-409B-B3A5-C54C1B01599D}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Mirror", "Mirror", "{851AB4AD-366C-4D69-9F4E-E9EDC7CCD2BB}" + ProjectSection(SolutionItems) = preProject + Mirror\kcp2k.dll = Mirror\kcp2k.dll + Mirror\Mirror.Components.dll = Mirror\Mirror.Components.dll + Mirror\Mirror.dll = Mirror\Mirror.dll + Mirror\Mirror.Transports.dll = Mirror\Mirror.Transports.dll + Mirror\Telepathy.dll = Mirror\Telepathy.dll + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "APITestMod", "APITestMod\APITestMod.csproj", "{0A10143E-6C00-409B-B3A5-C54C1B01599D}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QSBPatcher", "QSBPatcher\QSBPatcher.csproj", "{CA4CBA2B-54D5-4C4B-9B51-957BC6D77D6B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QSB-NH", "QSB-NH\QSB-NH.csproj", "{74F84A39-1C9D-4EF7-889A-485D33B7B324}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -36,22 +50,33 @@ Global {DA8A467E-15BA-456C-9034-6EB80BAF1FF9}.Debug|Any CPU.Build.0 = Debug|Any CPU {DA8A467E-15BA-456C-9034-6EB80BAF1FF9}.Release|Any CPU.ActiveCfg = Release|Any CPU {DA8A467E-15BA-456C-9034-6EB80BAF1FF9}.Release|Any CPU.Build.0 = Release|Any CPU - {971AA4A1-6729-40DE-AADF-2754F1E8783A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {971AA4A1-6729-40DE-AADF-2754F1E8783A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {971AA4A1-6729-40DE-AADF-2754F1E8783A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {971AA4A1-6729-40DE-AADF-2754F1E8783A}.Release|Any CPU.Build.0 = Release|Any CPU - {639EFAEE-C4A1-4DA2-8457-D0472A9F6343}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {639EFAEE-C4A1-4DA2-8457-D0472A9F6343}.Debug|Any CPU.Build.0 = Debug|Any CPU - {639EFAEE-C4A1-4DA2-8457-D0472A9F6343}.Release|Any CPU.ActiveCfg = Release|Any CPU - {639EFAEE-C4A1-4DA2-8457-D0472A9F6343}.Release|Any CPU.Build.0 = Release|Any CPU + {D47034D8-B92D-47DA-884C-C76F735E2D5D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D47034D8-B92D-47DA-884C-C76F735E2D5D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D47034D8-B92D-47DA-884C-C76F735E2D5D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D47034D8-B92D-47DA-884C-C76F735E2D5D}.Release|Any CPU.Build.0 = Release|Any CPU + {750563ED-4F2A-42DD-81A3-3A85DEBB691E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {750563ED-4F2A-42DD-81A3-3A85DEBB691E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {750563ED-4F2A-42DD-81A3-3A85DEBB691E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {750563ED-4F2A-42DD-81A3-3A85DEBB691E}.Release|Any CPU.Build.0 = Release|Any CPU {0A10143E-6C00-409B-B3A5-C54C1B01599D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {0A10143E-6C00-409B-B3A5-C54C1B01599D}.Debug|Any CPU.Build.0 = Debug|Any CPU {0A10143E-6C00-409B-B3A5-C54C1B01599D}.Release|Any CPU.ActiveCfg = Release|Any CPU {0A10143E-6C00-409B-B3A5-C54C1B01599D}.Release|Any CPU.Build.0 = Release|Any CPU + {CA4CBA2B-54D5-4C4B-9B51-957BC6D77D6B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CA4CBA2B-54D5-4C4B-9B51-957BC6D77D6B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CA4CBA2B-54D5-4C4B-9B51-957BC6D77D6B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CA4CBA2B-54D5-4C4B-9B51-957BC6D77D6B}.Release|Any CPU.Build.0 = Release|Any CPU + {74F84A39-1C9D-4EF7-889A-485D33B7B324}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {74F84A39-1C9D-4EF7-889A-485D33B7B324}.Debug|Any CPU.Build.0 = Debug|Any CPU + {74F84A39-1C9D-4EF7-889A-485D33B7B324}.Release|Any CPU.ActiveCfg = Release|Any CPU + {74F84A39-1C9D-4EF7-889A-485D33B7B324}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {851AB4AD-366C-4D69-9F4E-E9EDC7CCD2BB} = {2569F98D-F671-42AA-82DE-505B05CDCEF2} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {59091249-7726-4F95-86E2-3C3278C9CB9B} EndGlobalSection diff --git a/QSB.sln.DotSettings b/QSB.sln.DotSettings index 5514b0fa5..c7e98ffee 100644 --- a/QSB.sln.DotSettings +++ b/QSB.sln.DotSettings @@ -10,6 +10,7 @@ NEXT_LINE_SHIFTED_2 API ID + OW QSB UWP True diff --git a/QSB/API/AddonDataManager.cs b/QSB/API/AddonDataManager.cs index 48fa1f8cc..1652b1565 100644 --- a/QSB/API/AddonDataManager.cs +++ b/QSB/API/AddonDataManager.cs @@ -1,27 +1,26 @@ using System; using System.Collections.Generic; +using OWML.Common; using QSB.Utility; namespace QSB.API; public static class AddonDataManager { - private static readonly Dictionary> _handlers = new(); + private static readonly Dictionary> _handlers = new(); - public static void OnReceiveDataMessage(string messageType, object data, uint from) + public static void OnReceiveDataMessage(int hash, object data, uint from) { - DebugLog.DebugWrite($"Received data message of message type \"{messageType}\" from {from}!"); - if (!_handlers.TryGetValue(messageType, out var handler)) + if (!_handlers.TryGetValue(hash, out var handler)) { + DebugLog.DebugWrite($"unknown addon message type with hash {hash}", MessageType.Error); return; } - handler(from, data); } - public static void RegisterHandler(string messageType, Action handler) + public static void RegisterHandler(int hash, Action handler) { - DebugLog.DebugWrite($"Registering handler for \"{messageType}\" with type of {typeof(T).Name}"); - _handlers.Add(messageType, (from, data) => handler(from, (T)data)); + _handlers.Add(hash, (from, data) => handler(from, (T)data)); } } diff --git a/QSB/API/IQSBAPI.cs b/QSB/API/IQSBAPI.cs index 886413672..a0befa526 100644 --- a/QSB/API/IQSBAPI.cs +++ b/QSB/API/IQSBAPI.cs @@ -1,14 +1,31 @@ using System; using OWML.Common; +using UnityEngine; using UnityEngine.Events; public interface IQSBAPI { + #region General + /// /// If called, all players connected to YOUR hosted game must have this mod installed. /// void RegisterRequiredForAllPlayers(IModBehaviour mod); + /// + /// Returns if the current player is the host. + /// + bool GetIsHost(); + + /// + /// Returns if the current player is in multiplayer. + /// + bool GetIsInMultiplayer(); + + #endregion + + #region Player + /// /// Returns the player ID of the current player. /// @@ -20,25 +37,51 @@ public interface IQSBAPI /// The ID of the player you want the name of. string GetPlayerName(uint playerID); + /// + /// Returns the body object of a given player. The pivot of this object is at the player's feet. + /// + /// The ID of the player you want the body of. + GameObject GetPlayerBody(uint playerID); + + /// + /// Returns the camera object of a given player. The pivot of this object is at the player's point of view. + /// + /// The ID of the player you want the camera of. + GameObject GetPlayerCamera(uint playerID); + + /// + /// Returns true if a given player has fully loaded into the game. If the local player is still loading into the game, this will return false. + /// + /// The ID of the player. + bool GetPlayerReady(uint playerID); + + /// + /// Returns true if the given player is dead. + /// + /// The ID of the player. + bool GetPlayerDead(uint playerID); + /// /// Returns the list of IDs of all connected players. + /// + /// The first player in the list is the host. /// uint[] GetPlayerIDs(); /// - /// Invoked when a player joins the game. + /// Invoked when any player (local or remote) joins the game. /// UnityEvent OnPlayerJoin(); /// - /// Invoked when a player leaves the game. + /// Invoked when any player (local or remote) leaves the game. /// UnityEvent OnPlayerLeave(); /// /// Sets some arbitrary data for a given player. /// - /// The type of the data. + /// The type of the data. If not serializable, data will not be synced. /// The ID of the player. /// The unique key to access this data by. /// The data to set. @@ -52,9 +95,15 @@ public interface IQSBAPI /// The unique key of the data you want to access. /// The data requested. If key is not valid, returns default. T GetCustomData(uint playerId, string key); - + + #endregion + + #region Messaging + /// /// Sends a message containing arbitrary data to every player. + /// + /// Keep your messages under around 1100 bytes. /// /// The type of the data being sent. This type must be serializable. /// The unique key of the message. @@ -70,4 +119,25 @@ public interface IQSBAPI /// The unique key of the message. /// The action to be ran when the message is received. The uint is the player ID that sent the messsage. void RegisterHandler(string messageType, Action handler); + + #endregion + + #region Chat + + /// + /// Invoked when a chat message is received. + /// The string is the message body. + /// The uint is the player who sent the message. If it's a system message, this is uint.MaxValue. + /// + UnityEvent OnChatMessage(); + + /// + /// Sends a message in chat. + /// + /// The text of the message. + /// If false, the message is sent as if the local player wrote it manually. If true, the message has no player attached to it, like the player join messages. + /// The color of the message. + void SendChatMessage(string message, bool systemMessage, Color color); + + #endregion } diff --git a/QSB/API/Messages/AddonCustomDataSyncMessage.cs b/QSB/API/Messages/AddonCustomDataSyncMessage.cs new file mode 100644 index 000000000..74edc5a4a --- /dev/null +++ b/QSB/API/Messages/AddonCustomDataSyncMessage.cs @@ -0,0 +1,11 @@ +using QSB.Messaging; +using QSB.Player; +using QSB.Utility; + +namespace QSB.API.Messages; + +public class AddonCustomDataSyncMessage : QSBMessage<(uint playerId, string key, byte[] data)> +{ + public AddonCustomDataSyncMessage(uint playerId, string key, object data) : base((playerId, key, data.ToBytes())) { } + public override void OnReceiveRemote() => QSBPlayerManager.GetPlayer(Data.playerId).SetCustomData(Data.key, Data.data.ToObject()); +} diff --git a/QSB/API/Messages/AddonDataMessage.cs b/QSB/API/Messages/AddonDataMessage.cs index 45557aa2b..4abd5f180 100644 --- a/QSB/API/Messages/AddonDataMessage.cs +++ b/QSB/API/Messages/AddonDataMessage.cs @@ -1,29 +1,11 @@ -using System.IO; -using System.Runtime.Serialization.Formatters.Binary; -using QSB.Messaging; +using QSB.Messaging; +using QSB.Utility; namespace QSB.API.Messages; -public class AddonDataMessage : QSBMessage<(string messageType, byte[] data, bool receiveLocally)> +public class AddonDataMessage : QSBMessage<(int hash, byte[] data, bool receiveLocally)> { - public AddonDataMessage(string messageType, object data, bool receiveLocally) : base((messageType, Obj2Bytes(data), receiveLocally)) { } - - private static byte[] Obj2Bytes(object obj) - { - using var ms = new MemoryStream(); - var bf = new BinaryFormatter(); - bf.Serialize(ms, obj); - var bytes = ms.ToArray(); - return bytes; - } - - private static object Bytes2Obj(byte[] bytes) - { - using var ms = new MemoryStream(bytes); - var bf = new BinaryFormatter(); - var obj = bf.Deserialize(ms); - return obj; - } + public AddonDataMessage(int hash, object data, bool receiveLocally) : base((hash, data.ToBytes(), receiveLocally)) { } public override void OnReceiveLocal() { @@ -35,7 +17,7 @@ public override void OnReceiveLocal() public override void OnReceiveRemote() { - var obj = Bytes2Obj(Data.data); - AddonDataManager.OnReceiveDataMessage(Data.messageType, obj, From); + var obj = Data.data.ToObject(); + AddonDataManager.OnReceiveDataMessage(Data.hash, obj, From); } } diff --git a/QSB/API/QSBAPI.cs b/QSB/API/QSBAPI.cs index 3fa844d59..911c1166b 100644 --- a/QSB/API/QSBAPI.cs +++ b/QSB/API/QSBAPI.cs @@ -1,10 +1,14 @@ -using System; -using System.Linq; +using Mirror; using OWML.Common; using QSB.API.Messages; using QSB.Messaging; using QSB.Player; +using System; +using System.Linq; +using QSB.HUD; +using QSB.HUD.Messages; using UnityEngine.Events; +using UnityEngine; namespace QSB.API; @@ -16,22 +20,50 @@ public void RegisterRequiredForAllPlayers(IModBehaviour mod) QSBCore.Addons.Add(uniqueName, mod); } + public bool GetIsHost() => QSBCore.IsHost; + public bool GetIsInMultiplayer() => QSBCore.IsInMultiplayer; + public uint GetLocalPlayerID() => QSBPlayerManager.LocalPlayerId; public string GetPlayerName(uint playerId) => QSBPlayerManager.GetPlayer(playerId).Name; + public GameObject GetPlayerBody(uint playerId) => QSBPlayerManager.GetPlayer(playerId).Body; + public GameObject GetPlayerCamera(uint playerId) => QSBPlayerManager.GetPlayer(playerId).CameraBody; + + public bool GetPlayerReady(uint playerId) + { + var player = QSBPlayerManager.GetPlayer(playerId); + return player.IsReady && player.Body != null; + } + + public bool GetPlayerDead(uint playerId) + { + var player = QSBPlayerManager.GetPlayer(playerId); + return player.IsDead; + } + public uint[] GetPlayerIDs() => QSBPlayerManager.PlayerList.Select(x => x.PlayerId).ToArray(); public UnityEvent OnPlayerJoin() => QSBAPIEvents.OnPlayerJoinEvent; - public UnityEvent OnPlayerLeave() => QSBAPIEvents.OnPlayerLeaveEvent; public void SetCustomData(uint playerId, string key, T data) => QSBPlayerManager.GetPlayer(playerId).SetCustomData(key, data); public T GetCustomData(uint playerId, string key) => QSBPlayerManager.GetPlayer(playerId).GetCustomData(key); public void SendMessage(string messageType, T data, uint to = uint.MaxValue, bool receiveLocally = false) - => new AddonDataMessage(messageType, data, receiveLocally) {To = to} .Send(); + => new AddonDataMessage(messageType.GetStableHashCode(), data, receiveLocally) { To = to }.Send(); public void RegisterHandler(string messageType, Action handler) - => AddonDataManager.RegisterHandler(messageType, handler); + => AddonDataManager.RegisterHandler(messageType.GetStableHashCode(), handler); + + public UnityEvent OnChatMessage() => MultiplayerHUDManager.OnChatMessageEvent; + + public void SendChatMessage(string message, bool systemMessage, Color color) + { + var fromName = systemMessage + ? "QSB" + : QSBPlayerManager.LocalPlayer.Name; + + new ChatMessage($"{fromName}: {message}", color).Send(); + } } internal static class QSBAPIEvents @@ -43,6 +75,7 @@ static QSBAPIEvents() } internal class PlayerEvent : UnityEvent { } - internal static PlayerEvent OnPlayerJoinEvent = new PlayerEvent(); - internal static PlayerEvent OnPlayerLeaveEvent = new PlayerEvent(); + + internal static readonly PlayerEvent OnPlayerJoinEvent = new(); + internal static readonly PlayerEvent OnPlayerLeaveEvent = new(); } diff --git a/QSB/Animation/Player/AnimationSync.cs b/QSB/Animation/Player/AnimationSync.cs index c115fbe6c..18e937382 100644 --- a/QSB/Animation/Player/AnimationSync.cs +++ b/QSB/Animation/Player/AnimationSync.cs @@ -6,6 +6,7 @@ using QSB.Messaging; using QSB.Player; using QSB.Utility; +using QSB.WorldSync.Messages; using System; using UnityEngine; @@ -32,6 +33,11 @@ protected void Awake() NetworkAnimator.enabled = false; } + protected void OnDestroy() + { + GlobalMessenger.RemoveListener("EnableBigHeadMode", new Callback(OnEnableBigHeadMode)); + } + public void Reset() => InSuitedUpState = false; private void InitCommon(Transform modelRoot) @@ -87,6 +93,8 @@ public void InitRemote(Transform body) Delay.RunWhen(() => Player.CameraBody != null, () => body.GetComponent().Init(Player.CameraBody.transform)); + + GlobalMessenger.AddListener("EnableBigHeadMode", new Callback(OnEnableBigHeadMode)); } private void InitAccelerationSync() @@ -96,6 +104,12 @@ private void InitAccelerationSync() Player.JetpackAcceleration.Init(thrusterModel); } + private void OnEnableBigHeadMode() + { + var bone = VisibleAnimator.GetBoneTransform(HumanBodyBones.Head); + bone.localScale = new Vector3(2.5f, 2.5f, 2.5f); + } + public void SetSuitState(bool suitedUp) { if (!Player.IsReady) diff --git a/QSB/Animation/Player/HelmetAnimator.cs b/QSB/Animation/Player/HelmetAnimator.cs new file mode 100644 index 000000000..418e641ec --- /dev/null +++ b/QSB/Animation/Player/HelmetAnimator.cs @@ -0,0 +1,93 @@ +using OWML.Common; +using QSB.PlayerBodySetup.Remote; +using QSB.Utility; +using UnityEngine; + +namespace QSB.Animation.Player; + +[UsedInUnityProject] +public class HelmetAnimator : MonoBehaviour +{ + public Transform FakeHelmet; + public Transform FakeHead; + public GameObject SuitGroup; + + private QSBDitheringAnimator _fakeHelmetDitheringAnimator; + + private const float ANIM_TIME = 0.5f; + private bool _isPuttingOnHelmet; + private bool _isTakingOffHelmet; + + public void Start() + { + _fakeHelmetDitheringAnimator = FakeHelmet.GetComponent(); + + FakeHead.gameObject.SetActive(false); + } + + public void RemoveHelmet() + { + if (!SuitGroup.activeSelf) + { + DebugLog.DebugWrite($"Trying to remove helmet when player is not wearing suit!", MessageType.Error); + return; + } + + _fakeHelmetDitheringAnimator.SetVisible(true); + FakeHelmet.gameObject.SetActive(true); + FakeHead.gameObject.SetActive(true); + _fakeHelmetDitheringAnimator.SetVisible(false, ANIM_TIME); + _isTakingOffHelmet = true; + } + + public void PutOnHelmet() + { + if (!SuitGroup.activeSelf) + { + DebugLog.DebugWrite($"Trying to put on helmet when player is not wearing suit!", MessageType.Error); + return; + } + + _fakeHelmetDitheringAnimator.SetVisible(false); + FakeHead.gameObject.SetActive(true); + FakeHelmet.gameObject.SetActive(true); + _fakeHelmetDitheringAnimator.SetVisible(true, ANIM_TIME); + _isPuttingOnHelmet = true; + } + + public void SetHelmetInstant(bool helmetOn) + { + if (helmetOn) + { + FakeHelmet.gameObject.SetActive(true); + _fakeHelmetDitheringAnimator.SetVisible(true); + FakeHead.gameObject.SetActive(false); + } + else + { + _fakeHelmetDitheringAnimator.SetVisible(false); + FakeHelmet.gameObject.SetActive(false); + // If the player is currently wearing their suit but has no helmet on, make sure to make the head visible (#655) + FakeHead.gameObject.SetActive(SuitGroup.activeSelf); + } + } + + private void Update() + { + if (_isPuttingOnHelmet && _fakeHelmetDitheringAnimator.FullyVisible) + { + _isPuttingOnHelmet = false; + FakeHead.gameObject.SetActive(false); + } + + if (_isTakingOffHelmet && _fakeHelmetDitheringAnimator.FullyInvisible) + { + FakeHelmet.gameObject.SetActive(false); + + if (!SuitGroup.activeSelf) + { + FakeHead.gameObject.SetActive(false); + } + } + } +} diff --git a/QSB/Animation/Player/Messages/PlayerHelmetMessage.cs b/QSB/Animation/Player/Messages/PlayerHelmetMessage.cs new file mode 100644 index 000000000..d19526b1f --- /dev/null +++ b/QSB/Animation/Player/Messages/PlayerHelmetMessage.cs @@ -0,0 +1,43 @@ +using QSB.Messaging; +using QSB.Player.TransformSync; +using QSB.WorldSync; +using QSB.Player; + +namespace QSB.Animation.Player.Messages; + +public class PlayerHelmetMessage : QSBMessage +{ + static PlayerHelmetMessage() + { + GlobalMessenger.AddListener(OWEvents.PutOnHelmet, () => Handle(true)); + GlobalMessenger.AddListener(OWEvents.RemoveHelmet, () => Handle(false)); + } + + private static void Handle(bool on) + { + if (PlayerTransformSync.LocalInstance) + { + new PlayerHelmetMessage(on).Send(); + } + } + + public PlayerHelmetMessage(bool on) : base(on) { } + + public override bool ShouldReceive => QSBWorldSync.AllObjectsReady; + + public override void OnReceiveRemote() + { + var player = QSBPlayerManager.GetPlayer(From); + var animator = player.HelmetAnimator; + if (Data) + { + animator.PutOnHelmet(); + player.AudioController.PlayWearHelmet(); + } + else + { + animator.RemoveHelmet(); + player.AudioController.PlayRemoveHelmet(); + } + } +} diff --git a/QSB/AssetBundles/qsb_debug b/QSB/AssetBundles/qsb_debug deleted file mode 100644 index 5bfb7e365..000000000 Binary files a/QSB/AssetBundles/qsb_debug and /dev/null differ diff --git a/QSB/AssetBundles/qsb_hud b/QSB/AssetBundles/qsb_hud index e10c8f5fc..902396953 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 4da4e11ca..01f3fc70f 100644 Binary files a/QSB/AssetBundles/qsb_network and b/QSB/AssetBundles/qsb_network differ diff --git a/QSB/AssetBundles/qsb_network_big b/QSB/AssetBundles/qsb_network_big index ab53bfb6b..5236cf8f6 100644 Binary files a/QSB/AssetBundles/qsb_network_big and b/QSB/AssetBundles/qsb_network_big differ diff --git a/QSB/AssetBundles/qsb_skins b/QSB/AssetBundles/qsb_skins new file mode 100644 index 000000000..945319afc Binary files /dev/null and b/QSB/AssetBundles/qsb_skins differ diff --git a/QSB/Audio/QSBPlayerAudioController.cs b/QSB/Audio/QSBPlayerAudioController.cs index 0c50def97..1eef2566c 100644 --- a/QSB/Audio/QSBPlayerAudioController.cs +++ b/QSB/Audio/QSBPlayerAudioController.cs @@ -12,22 +12,20 @@ public class QSBPlayerAudioController : MonoBehaviour public OWAudioSource _damageAudioSource; private AudioManager _audioManager; + private float _playWearHelmetTime; public void Start() { _audioManager = Locator.GetAudioManager(); + } - // TODO: This should be done in the Unity project - var damageAudio = new GameObject("DamageAudioSource"); - damageAudio.SetActive(false); - damageAudio.transform.SetParent(transform, false); - damageAudio.transform.localPosition = Vector3.zero; - _damageAudioSource = damageAudio.AddComponent(); - _damageAudioSource._audioSource = damageAudio.GetAddComponent(); - _damageAudioSource.SetTrack(_repairToolSource.GetTrack()); - _damageAudioSource.spatialBlend = 1f; - _damageAudioSource.gameObject.GetAddComponent(); - damageAudio.SetActive(true); + private void Update() + { + if (Time.time > this._playWearHelmetTime) + { + enabled = false; + PlayOneShot(global::AudioType.PlayerSuitWearHelmet); + } } public void PlayEquipTool() @@ -48,6 +46,18 @@ public void PlayWearSuit() public void PlayRemoveSuit() => PlayOneShot(AudioType.PlayerSuitRemoveSuit); + public void PlayRemoveHelmet() + { + enabled = false; + PlayOneShot(AudioType.PlayerSuitRemoveHelmet); + } + + public void PlayWearHelmet() + { + enabled = true; + _playWearHelmetTime = Time.time + 0.4f; + } + public void PlayOneShot(AudioType audioType, float pitch = 1f, float volume = 1f) { if (_oneShotExternalSource) diff --git a/QSB/BodyCustomization/BodyCustomizer.cs b/QSB/BodyCustomization/BodyCustomizer.cs new file mode 100644 index 000000000..9976a5b19 --- /dev/null +++ b/QSB/BodyCustomization/BodyCustomizer.cs @@ -0,0 +1,94 @@ +using System.Collections.Generic; +using OWML.Common; +using QSB.Utility; +using UnityEngine; + +namespace QSB.BodyCustomization; + +public class BodyCustomizer : MonoBehaviour, IAddComponentOnStart +{ + private Dictionary skinMap = new(); + private Dictionary jetpackMap = new(); + + public AssetBundle SkinsBundle { get; private set; } + + public static BodyCustomizer Instance { get; private set; } + + private void Start() + { + Instance = this; + } + + public void OnBundleLoaded(AssetBundle bundle) + { + DebugLog.DebugWrite($"OnBundleLoaded", MessageType.Info); + SkinsBundle = bundle; + LoadAssets(); + } + + private void LoadAssets() + { + DebugLog.DebugWrite($"Loading skin assets...", MessageType.Info); + + skinMap.Add("Default", LoadSkin("Type 0")); + skinMap.Add("Type 1", LoadSkin("Type 1")); + skinMap.Add("Type 2", LoadSkin("Type 2")); + skinMap.Add("Type 3", LoadSkin("Type 3")); + skinMap.Add("Type 4", LoadSkin("Type 4")); + skinMap.Add("Type 5", LoadSkin("Type 5")); + skinMap.Add("Type 6", LoadSkin("Type 6")); + skinMap.Add("Type 7", LoadSkin("Type 7")); + skinMap.Add("Type 8", LoadSkin("Type 8")); + skinMap.Add("Type 9", LoadSkin("Type 9")); + skinMap.Add("Type 10", LoadSkin("Type 10")); + skinMap.Add("Type 11", LoadSkin("Type 11")); + skinMap.Add("Type 12", LoadSkin("Type 12")); + skinMap.Add("Type 13", LoadSkin("Type 13")); + skinMap.Add("Type 14", LoadSkin("Type 14")); + skinMap.Add("Type 15", LoadSkin("Type 15")); + skinMap.Add("Type 16", LoadSkin("Type 16")); + skinMap.Add("Type 17", LoadSkin("Type 17")); + + jetpackMap.Add("Orange", LoadJetpack("default")); + jetpackMap.Add("Yellow", LoadJetpack("yellow")); + jetpackMap.Add("Red", LoadJetpack("red")); + jetpackMap.Add("Pink", LoadJetpack("pink")); + jetpackMap.Add("Purple", LoadJetpack("purple")); + jetpackMap.Add("Dark Blue", LoadJetpack("darkblue")); + jetpackMap.Add("Light Blue", LoadJetpack("lightblue")); + jetpackMap.Add("Cyan", LoadJetpack("cyan")); + jetpackMap.Add("Green", LoadJetpack("green")); + } + + private (Texture2D d, Texture2D n) LoadSkin(string skinName) + { + DebugLog.DebugWrite($"LoadSkin {skinName}", MessageType.Info); + var number = skinName.Replace($"Type ", ""); + return (SkinsBundle.LoadAsset($"Assets/GameAssets/Texture2D/Skin Variations/{number}d.png"), SkinsBundle.LoadAsset($"Assets/GameAssets/Texture2D/Skin Variations/{number}n.png")); + } + + private Texture2D LoadJetpack(string jetpackName) + { + DebugLog.DebugWrite($"LoadJetpack {jetpackName}", MessageType.Info); + return SkinsBundle.LoadAsset($"Assets/GameAssets/Texture2D/Jetpack Variations/{jetpackName}.png"); + } + + public void CustomizeRemoteBody(GameObject REMOTE_Traveller_HEA_Player_v2, GameObject fakeHead, string skinType, string jetpackType) + { + DebugLog.DebugWrite($"CustomizeRemoteBody skin:{skinType} jetpack:{jetpackType}", MessageType.Info); + var headMesh = REMOTE_Traveller_HEA_Player_v2.transform.Find("player_mesh_noSuit:Traveller_HEA_Player/player_mesh_noSuit:Player_Head"); + + var skinMaterial = headMesh.GetComponent().material; + skinMaterial.SetTexture("_MainTex", skinMap[skinType].albedo); + skinMaterial.SetTexture("_BumpMap", skinMap[skinType].normal); + + var fakeHeadMaterial = fakeHead.GetComponent().material; + fakeHeadMaterial.SetTexture("_MainTex", skinMap[skinType].albedo); + fakeHeadMaterial.SetTexture("_BumpMap", skinMap[skinType].normal); + + var jetpackMesh = REMOTE_Traveller_HEA_Player_v2.transform.Find("Traveller_Mesh_v01:Traveller_Geo/Traveller_Mesh_v01:Props_HEA_Jetpack"); + var jetpackMaterial = jetpackMesh.GetComponent().material; + + jetpackMaterial.SetTexture("_MainTex", jetpackMap[jetpackType]); + } +} diff --git a/QSB/ConversationSync/ConversationManager.cs b/QSB/ConversationSync/ConversationManager.cs index f394e6f23..a0205f5f5 100644 --- a/QSB/ConversationSync/ConversationManager.cs +++ b/QSB/ConversationSync/ConversationManager.cs @@ -9,6 +9,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading; +using QSB.Utility.Deterministic; using UnityEngine; using UnityEngine.UI; @@ -41,8 +42,9 @@ public void Start() public override async UniTask BuildWorldObjects(OWScene scene, CancellationToken ct) { - QSBWorldSync.Init(); - QSBWorldSync.Init(); + // dont create worldobjects for NH warp drive stuff + QSBWorldSync.Init(QSBWorldSync.GetUnityObjects().Where(x => x.name != "WarpDriveRemoteTrigger").SortDeterministic()); + QSBWorldSync.Init(QSBWorldSync.GetUnityObjects().Where(x => x.name != "WarpDriveDialogue").SortDeterministic()); } public uint GetPlayerTalkingToTree(CharacterDialogueTree tree) => diff --git a/QSB/ConversationSync/Messages/ConversationStartEndMessage.cs b/QSB/ConversationSync/Messages/ConversationStartEndMessage.cs index 1a1d8eb2e..8e7baecfa 100644 --- a/QSB/ConversationSync/Messages/ConversationStartEndMessage.cs +++ b/QSB/ConversationSync/Messages/ConversationStartEndMessage.cs @@ -1,4 +1,5 @@ -using QSB.ConversationSync.WorldObjects; +using OWML.Utils; +using QSB.ConversationSync.WorldObjects; using QSB.Messaging; using QSB.Player; using QSB.Utility; diff --git a/QSB/ConversationSync/WorldObjects/QSBCharacterDialogueTree.cs b/QSB/ConversationSync/WorldObjects/QSBCharacterDialogueTree.cs index 925b55da7..6f5af1d21 100644 --- a/QSB/ConversationSync/WorldObjects/QSBCharacterDialogueTree.cs +++ b/QSB/ConversationSync/WorldObjects/QSBCharacterDialogueTree.cs @@ -1,4 +1,5 @@ using Cysharp.Threading.Tasks; +using OWML.Utils; using QSB.ConversationSync.Messages; using QSB.Messaging; using QSB.Player; diff --git a/QSB/DeathSync/RespawnOnDeath.cs b/QSB/DeathSync/RespawnOnDeath.cs index 865ce8db7..3aa69cc1a 100644 --- a/QSB/DeathSync/RespawnOnDeath.cs +++ b/QSB/DeathSync/RespawnOnDeath.cs @@ -82,8 +82,8 @@ public void ResetPlayer() ResetCanvases(); var mixer = Locator.GetAudioMixer(); - mixer._deathMixed = false; - mixer._nonEndTimesVolume.FadeTo(1, 0.5f); + //mixer._deathMixed = false; + //mixer._nonEndTimesVolume.FadeTo(1, 0.5f); mixer._endTimesVolume.FadeTo(1, 0.5f); mixer.MixMap(); @@ -257,7 +257,10 @@ private void ResetPlayerComponents() var sectorList = PlayerTransformSync.LocalInstance.SectorDetector.SectorList; if (sectorList.All(x => x.Type != Sector.Name.TimberHearth)) { - // stops sectors from breaking when you die on TH?? + // Spooky scary legacy code? + // Original comment was "stops sectors from breaking when you die on TH??" + // I think dying on TH used to break all the sectors. Something about you not technically re-entering TH when dying inside it. + // I commented out these lines, and everything seemed fine. But I'm not gonna touch them just in case. :P Locator.GetPlayerSectorDetector().RemoveFromAllSectors(); Locator.GetPlayerCameraDetector().GetComponent().DeactivateAllVolumes(0f); } diff --git a/QSB/EchoesOfTheEye/AirlockSync/WorldObjects/QSBAirlockInterface.cs b/QSB/EchoesOfTheEye/AirlockSync/WorldObjects/QSBAirlockInterface.cs index d73543156..a9be199a0 100644 --- a/QSB/EchoesOfTheEye/AirlockSync/WorldObjects/QSBAirlockInterface.cs +++ b/QSB/EchoesOfTheEye/AirlockSync/WorldObjects/QSBAirlockInterface.cs @@ -8,9 +8,12 @@ namespace QSB.EchoesOfTheEye.AirlockSync.WorldObjects; public class QSBAirlockInterface : QSBRotatingElements { protected override IEnumerable LightSensors => AttachedObject._lightSensors; - protected override GameObject NetworkObjectPrefab => QSBNetworkManager.singleton.AirlockPrefab; + // RotatingElements normally releases ownership when not longer being lit + // force the airlocks to keep network updating when they could still be moving + protected override bool LockedActive => AttachedObject.enabled; + public override string ReturnLabel() { var baseString = $"{this}{Environment.NewLine}CurrentRotation:{AttachedObject._currentRotation}"; diff --git a/QSB/EchoesOfTheEye/AlarmTotemSync/AlarmTotemManager.cs b/QSB/EchoesOfTheEye/AlarmTotemSync/AlarmTotemManager.cs index 992f79ee1..c59956598 100644 --- a/QSB/EchoesOfTheEye/AlarmTotemSync/AlarmTotemManager.cs +++ b/QSB/EchoesOfTheEye/AlarmTotemSync/AlarmTotemManager.cs @@ -1,6 +1,6 @@ using Cysharp.Threading.Tasks; using QSB.EchoesOfTheEye.AlarmTotemSync.WorldObjects; -using QSB.Utility; +using QSB.Utility.Deterministic; using QSB.WorldSync; using System.Linq; using System.Threading; diff --git a/QSB/EchoesOfTheEye/AlarmTotemSync/Patches/AlarmTotemPatches.cs b/QSB/EchoesOfTheEye/AlarmTotemSync/Patches/AlarmTotemPatches.cs index 10898524c..98c0690a6 100644 --- a/QSB/EchoesOfTheEye/AlarmTotemSync/Patches/AlarmTotemPatches.cs +++ b/QSB/EchoesOfTheEye/AlarmTotemSync/Patches/AlarmTotemPatches.cs @@ -62,6 +62,7 @@ private static bool OnSectorOccupantRemoved(AlarmTotem __instance, SectorDetecto __instance._isPlayerVisible = false; __instance._secondsConcealed = 0f; Locator.GetAlarmSequenceController().DecreaseAlarmCounter(); + qsbAlarmTotem.SendMessage(new SetVisibleMessage(false)); } } }); @@ -120,18 +121,27 @@ private static bool CheckPlayerVisible(AlarmTotem __instance, out bool __result) } foreach (var player in QSBPlayerManager.PlayerList) { + if (!player.IsReady) + { + continue; + } + var position = player.Camera.transform.position; if (__instance.CheckPointInVisionCone(position) && !__instance.CheckLineOccluded(__instance._sightOrigin.position, position)) { if (player.LightSensor.IsIlluminated()) { + // Player is visible and illuminated. __result = true; return false; } + if (player.AssignedSimulationLantern == null) { + // Player is not holding a lantern. continue; } + var lanternController = player.AssignedSimulationLantern.AttachedObject.GetLanternController(); if (lanternController.IsHeldByPlayer()) { @@ -146,14 +156,16 @@ private static bool CheckPlayerVisible(AlarmTotem __instance, out bool __result) GlobalMessenger.FireEvent("ConcealFromAlarmTotem"); } } - __result = false; - return false; + + continue; } + __result = true; return false; } } } + __result = false; return false; } diff --git a/QSB/EchoesOfTheEye/DreamLantern/Patches/DreamLanternPatches.cs b/QSB/EchoesOfTheEye/DreamLantern/Patches/DreamLanternPatches.cs index 14535bfa7..48db5e028 100644 --- a/QSB/EchoesOfTheEye/DreamLantern/Patches/DreamLanternPatches.cs +++ b/QSB/EchoesOfTheEye/DreamLantern/Patches/DreamLanternPatches.cs @@ -8,12 +8,13 @@ namespace QSB.EchoesOfTheEye.DreamLantern.Patches; +[HarmonyPatch(typeof(DreamLanternController))] public class DreamLanternPatches : QSBPatch { public override QSBPatchTypes Type => QSBPatchTypes.OnClientConnect; [HarmonyPrefix] - [HarmonyPatch(typeof(DreamLanternController), nameof(DreamLanternController.SetLit))] + [HarmonyPatch(nameof(DreamLanternController.SetLit))] public static void SetLit(DreamLanternController __instance, bool lit) { if (Remote) @@ -41,7 +42,7 @@ public static void SetLit(DreamLanternController __instance, bool lit) } [HarmonyPrefix] - [HarmonyPatch(typeof(DreamLanternController), nameof(DreamLanternController.SetConcealed))] + [HarmonyPatch(nameof(DreamLanternController.SetConcealed))] public static void SetConcealed(DreamLanternController __instance, bool concealed) { if (Remote) @@ -69,7 +70,7 @@ public static void SetConcealed(DreamLanternController __instance, bool conceale } [HarmonyPrefix] - [HarmonyPatch(typeof(DreamLanternController), nameof(DreamLanternController.SetFocus))] + [HarmonyPatch(nameof(DreamLanternController.SetFocus))] public static void SetFocus(DreamLanternController __instance, float focus) { if (Remote) @@ -98,7 +99,7 @@ public static void SetFocus(DreamLanternController __instance, float focus) } [HarmonyPrefix] - [HarmonyPatch(typeof(DreamLanternController), nameof(DreamLanternController.SetRange))] + [HarmonyPatch(nameof(DreamLanternController.SetRange))] public static void SetRange(DreamLanternController __instance, float minRange, float maxRange) { if (Remote) @@ -124,4 +125,190 @@ public static void SetRange(DreamLanternController __instance, float minRange, f } qsbDreamLantern.SendMessage(new SetRangeMessage(minRange, maxRange)); } + + [HarmonyPrefix] + [HarmonyPatch(nameof(DreamLanternController.UpdateVisuals))] + public static bool UpdateVisuals(DreamLanternController __instance) + { + if (!QSBWorldSync.AllObjectsReady) + { + return true; + } + + var worldObject = __instance.GetWorldObject(); + + if (worldObject.IsGhostLantern || worldObject.DreamLanternItem._lanternType == DreamLanternType.Malfunctioning) + { + return true; + } + + if (__instance._dirtyFlag_heldByPlayer) + { + var localHeldItem = Locator.GetToolModeSwapper().GetItemCarryTool().GetHeldItem(); + var heldByLocalPlayer = localHeldItem != null && (DreamLanternItem)localHeldItem == worldObject.DreamLanternItem; + // Only change to VM group when the local player is holding, not remote players + if (heldByLocalPlayer) + { + if (__instance._worldModelGroup != null) + { + __instance._worldModelGroup.SetActive(!__instance._heldByPlayer); + } + + if (__instance._viewModelGroup != null) + { + __instance._viewModelGroup.SetActive(__instance._heldByPlayer); + } + } + } + + if (__instance._dirtyFlag_lit || __instance._dirtyFlag_flameStrength) + { + var vector = new Vector4(1f, 1f, 0f, 0f); + vector.w = Mathf.Lerp(0.5f, 0f, __instance._flameStrength); + foreach (var flame in __instance._flameRenderers) + { + flame.SetActivation(__instance._lit || __instance._flameStrength > 0f); + flame.SetMaterialProperty(__instance._propID_MainTex_ST, vector); + } + } + + if (__instance._dirtyFlag_lensFlareStrength) + { + __instance._lensFlare.brightness = __instance._lensFlareStrength; + __instance._lensFlare.enabled = __instance._lensFlareStrength > 0f; + } + + if (__instance._dirtyFlag_focus) + { + var petalRotation = new Vector3(0f, 0f, Mathf.Lerp(90f, 0f, __instance._focus)); + for (var j = 0; j < __instance._focuserPetals.Length; j++) + { + __instance._focuserPetals[j].localEulerAngles = __instance._focuserPetalsBaseEulerAngles[j] + petalRotation; + worldObject.NonVMFocuserPetals[j].localEulerAngles = __instance._focuserPetalsBaseEulerAngles[j] + petalRotation; + } + } + + if (__instance._dirtyFlag_concealment) + { + var rootScale = new Vector3(1f, Mathf.Lerp(0.5f, 1f, __instance._concealment), 1f); + for (var k = 0; k < __instance._concealerRoots.Length; k++) + { + __instance._concealerRoots[k].localScale = Vector3.Scale(__instance._concealerRootsBaseScale[k], rootScale); + worldObject.NonVMConcealerRoots[k].localScale = Vector3.Scale(__instance._concealerRootsBaseScale[k], rootScale); + } + + for (var l = 0; l < __instance._concealerCovers.Length; l++) + { + __instance._concealerCovers[l].localPosition = Vector3.Lerp(__instance._concealerCoverTargets[l], __instance._concealerCoversStartPos[l], __instance._concealment); + __instance._concealerCoversVMPrepass[l].localPosition = Vector3.Lerp(__instance._concealerCoverTargets[l], __instance._concealerCoversStartPos[l], __instance._concealment); + worldObject.NonVMConcealerCovers[l].localPosition = Vector3.Lerp(__instance._concealerCoverTargets[l], __instance._concealerCoversStartPos[l], __instance._concealment); + } + } + + if (__instance._dirtyFlag_flameStrength) + { + var flameActive = __instance._flameStrength > 0f; + __instance._light.SetActivation(flameActive); + } + + if (__instance._dirtyFlag_focus || __instance._dirtyFlag_flameStrength || __instance._dirtyFlag_range) + { + var num = Mathf.Lerp(__instance._minRange, __instance._maxRange, Mathf.Pow(__instance._focus, 5f)) * __instance._flameStrength; + var num2 = Mathf.Lerp(__instance._maxAngle, __instance._minAngle, __instance._focus); + __instance._light.range = num; + __instance._light.GetLight().spotAngle = num2; + __instance.SetDetectorPositionAndSize(num, num2); + } + + if (__instance._grabbedByGhost) + { + var num3 = Mathf.MoveTowards(__instance._light.GetIntensity(), 1.2f, Time.deltaTime * 0.2f); + __instance._light.SetIntensity(num3); + } + else if (__instance._dirtyFlag_socketed || __instance._dirtyFlag_grabbedByGhost) + { + __instance._light.SetIntensity(__instance._socketed ? 0f : 1f); + } + + if (__instance._dirtyFlag_flameStrength) + { + foreach (var light in __instance._flameLights) + { + light.SetActivation(__instance._flameStrength > 0f); + light.SetIntensityScale(__instance._flameStrength); + } + } + + if ((__instance._dirtyFlag_focus || __instance._dirtyFlag_lit || __instance._dirtyFlag_concealed) && __instance._simLightConeUnfocused != null && __instance._simLightConeFocused != null) + { + var flag2 = __instance.IsFocused(0.9f); + __instance._simLightConeUnfocused.SetActive(__instance._lit && !__instance._concealed && !flag2); + __instance._simLightConeFocused.SetActive(__instance._lit && !__instance._concealed && flag2); + } + + __instance.ClearDirtyFlags(); + return false; + } + + #region flare stuff + + [HarmonyPrefix] + [HarmonyPatch(nameof(DreamLanternController.Update))] + public static bool Update(DreamLanternController __instance) + { + // mmm i love not using transpiler LOL cry about it + + var num = 0f; + // we want player lanterns to also have flare so remote player lanterns have it + if (__instance._lit && !__instance._concealed /*&& !__instance._heldByPlayer*/) + { + var vector = Locator.GetActiveCamera().transform.position - __instance._light.transform.position; + var num2 = 1f; + if (vector.sqrMagnitude > __instance._light.GetLight().range * __instance._light.GetLight().range) + { + num2 = 0f; + } + else if (Vector3.Angle(__instance._light.transform.forward, vector) > __instance._light.GetLight().spotAngle * 0.5f) + { + num2 = 0f; + } + num = Mathf.MoveTowards(__instance._lensFlare.brightness, __instance._origLensFlareBrightness * num2, Time.deltaTime * 4f); + } + if (__instance._lensFlareStrength != num) + { + __instance._lensFlareStrength = num; + __instance._dirtyFlag_lensFlareStrength = true; + } + var num3 = 0f; + var num4 = 0.1f; + if (__instance._lit) + { + num3 = __instance._concealed ? 0f : 1f; + if (Time.time - __instance._litTime <= 1f) + { + num4 = 1f; + } + else + { + num4 = __instance._concealed ? 0.2f : 0.5f; + } + } + var num5 = Mathf.MoveTowards(__instance._flameStrength, num3, Time.deltaTime / num4); + if (__instance._flameStrength != num5) + { + __instance._flameStrength = num5; + __instance._dirtyFlag_flameStrength = true; + } + var num6 = Mathf.MoveTowards(__instance._concealment, __instance._concealed ? 1f : 0f, Time.deltaTime / (__instance._concealed ? 0.15f : 0.5f)); + if (__instance._concealment != num6) + { + __instance._concealment = num6; + __instance._dirtyFlag_concealment = true; + } + __instance.UpdateVisuals(); + + return false; + } + + #endregion } diff --git a/QSB/EchoesOfTheEye/DreamLantern/WorldObjects/QSBDreamLanternController.cs b/QSB/EchoesOfTheEye/DreamLantern/WorldObjects/QSBDreamLanternController.cs index c80ff6ad3..8d12c48b5 100644 --- a/QSB/EchoesOfTheEye/DreamLantern/WorldObjects/QSBDreamLanternController.cs +++ b/QSB/EchoesOfTheEye/DreamLantern/WorldObjects/QSBDreamLanternController.cs @@ -1,6 +1,8 @@ using Cysharp.Threading.Tasks; using QSB.WorldSync; using System.Threading; +using QSB.Utility; +using UnityEngine; namespace QSB.EchoesOfTheEye.DreamLantern.WorldObjects; @@ -8,12 +10,61 @@ public class QSBDreamLanternController : WorldObject { public DreamLanternItem DreamLanternItem { get; private set; } + public Transform[] NonVMFocuserPetals; + public Transform[] NonVMConcealerRoots; + public Transform[] NonVMConcealerCovers; + public override async UniTask Init(CancellationToken ct) { // Ghosts don't have the item and instead the effects are controlled by GhostEffects if (!IsGhostLantern) { DreamLanternItem = AttachedObject.GetComponent(); + + if (DreamLanternItem == null) // ghost lanterns don't have DreamLanternItems attached + { + return; + } + + if (DreamLanternItem._lanternType == DreamLanternType.Malfunctioning) + { + return; + } + + AttachedObject._lensFlare.brightness = 0.5f; // ghost lanterns use this. in vanilla its 0 + // also has blue lens flare instead of green. keep it like that for gamplay or wtv + AttachedObject._origLensFlareBrightness = AttachedObject._lensFlare.brightness; + + // Find non-viewmodel transforms for remote player animations + + var focuser = AttachedObject._worldModelGroup.transform.Find("Focuser"); + NonVMFocuserPetals = new Transform[5] + { + focuser.Find("Panel_01"), + focuser.Find("Panel_02"), + focuser.Find("Panel_03"), + focuser.Find("Panel_04"), + focuser.Find("Panel_05") + }; + + var lanternHood = AttachedObject._worldModelGroup.transform.Find("LanternHood"); + var hoodBottom = lanternHood.Find("Hood_Bottom"); + var hoodTop = lanternHood.Find("Hood_Top"); + NonVMConcealerRoots = new Transform[2] + { + hoodBottom, + hoodTop + }; + + NonVMConcealerCovers = new Transform[6] + { + hoodTop.Find("Cover_01"), + hoodTop.Find("Cover_02"), + hoodTop.Find("Cover_03"), + hoodBottom.Find("Cover_04"), + hoodBottom.Find("Cover_05"), + hoodBottom.Find("Cover_06") + }; } } diff --git a/QSB/EchoesOfTheEye/DreamLantern/WorldObjects/QSBDreamLanternItem.cs b/QSB/EchoesOfTheEye/DreamLantern/WorldObjects/QSBDreamLanternItem.cs index 877fc157c..f31af3c90 100644 --- a/QSB/EchoesOfTheEye/DreamLantern/WorldObjects/QSBDreamLanternItem.cs +++ b/QSB/EchoesOfTheEye/DreamLantern/WorldObjects/QSBDreamLanternItem.cs @@ -1,42 +1,16 @@ -using Cysharp.Threading.Tasks; -using QSB.ItemSync.WorldObjects.Items; -using System.Linq; -using System.Threading; +using QSB.ItemSync.WorldObjects.Items; using UnityEngine; namespace QSB.EchoesOfTheEye.DreamLantern.WorldObjects; public class QSBDreamLanternItem : QSBItem { - private Material[] _materials; - - public override async UniTask Init(CancellationToken ct) - { - await base.Init(ct); - - // Some lanterns (ie, nonfunctioning) don't have a view model group - if (AttachedObject._lanternType != DreamLanternType.Nonfunctioning) - { - _materials = AttachedObject._lanternController._viewModelGroup?.GetComponentsInChildren(true)?.SelectMany(x => x.materials)?.ToArray(); - } - } - public override void PickUpItem(Transform holdTransform) { base.PickUpItem(holdTransform); - // Fixes #502: Artifact is visible through the walls if (AttachedObject._lanternType != DreamLanternType.Nonfunctioning) { - foreach (var m in _materials) - { - if (m.renderQueue >= 2000) - { - m.renderQueue -= 2000; - } - } - - // The view model looks much smaller than the dropped item AttachedObject.gameObject.transform.localScale = Vector3.one * 2f; } @@ -49,14 +23,6 @@ public override void DropItem(Vector3 worldPosition, Vector3 worldNormal, Transf if (AttachedObject._lanternType != DreamLanternType.Nonfunctioning) { - foreach (var m in _materials) - { - if (m.renderQueue < 2000) - { - m.renderQueue += 2000; - } - } - AttachedObject.gameObject.transform.localScale = Vector3.one; } diff --git a/QSB/EchoesOfTheEye/Ghosts/Actions/QSBIdentifyIntruderAction.cs b/QSB/EchoesOfTheEye/Ghosts/Actions/QSBIdentifyIntruderAction.cs index 1934de9ec..5d40f2e69 100644 --- a/QSB/EchoesOfTheEye/Ghosts/Actions/QSBIdentifyIntruderAction.cs +++ b/QSB/EchoesOfTheEye/Ghosts/Actions/QSBIdentifyIntruderAction.cs @@ -260,7 +260,7 @@ public override void OnArriveAtPosition() { _controller.SetLanternConcealed(false, true); _controller.ChangeLanternFocus(1f, 2f); - _controller.FaceNodeList(IdentifyIntruderAction.s_nodesToSpotlight, num, TurnSpeed.MEDIUM, 1f, false); + _controller.FaceNodeList(s_nodesToSpotlight, num, TurnSpeed.MEDIUM, 1f, false); return; } } @@ -291,9 +291,9 @@ private int GenerateSpotlightList(GhostNode node, Vector3 ignoreDirection) { if (Vector3.Angle(node.neighbors[i].localPosition - localPosition, ignoreDirection) >= 45f) { - IdentifyIntruderAction.s_nodesToSpotlight[num] = node.neighbors[i]; + s_nodesToSpotlight[num] = node.neighbors[i]; num++; - if (num == IdentifyIntruderAction.s_nodesToSpotlight.Length) + if (num == s_nodesToSpotlight.Length) { break; } diff --git a/QSB/EchoesOfTheEye/Ghosts/GhostManager.cs b/QSB/EchoesOfTheEye/Ghosts/GhostManager.cs index 64f5dbcf6..3cfdec966 100644 --- a/QSB/EchoesOfTheEye/Ghosts/GhostManager.cs +++ b/QSB/EchoesOfTheEye/Ghosts/GhostManager.cs @@ -1,6 +1,6 @@ using Cysharp.Threading.Tasks; using QSB.EchoesOfTheEye.Ghosts.WorldObjects; -using QSB.Utility; +using QSB.Utility.Deterministic; using QSB.WorldSync; using System.Collections.Generic; using System.Linq; diff --git a/QSB/EchoesOfTheEye/Ghosts/Messages/PartyPathResetMessage.cs b/QSB/EchoesOfTheEye/Ghosts/Messages/PartyPathResetMessage.cs new file mode 100644 index 000000000..568b36ff7 --- /dev/null +++ b/QSB/EchoesOfTheEye/Ghosts/Messages/PartyPathResetMessage.cs @@ -0,0 +1,31 @@ +using QSB.EchoesOfTheEye.Ghosts.WorldObjects; +using QSB.Messaging; +using QSB.WorldSync; +using UnityEngine; + +namespace QSB.EchoesOfTheEye.Ghosts.Messages; +public class PartyPathResetMessage : QSBWorldObjectMessage +{ + public PartyPathResetMessage(int indexOne, int indexTwo, int proxyIndex) : base((indexOne, indexTwo, proxyIndex)) { } + + public override void OnReceiveRemote() + { + var __instance = QSBWorldSync.GetUnityObject(); + + WorldObject.AttachedObject.transform.position = __instance._ghostSpawns[Data.indexOne].spawnTransform.position; + WorldObject.AttachedObject.transform.eulerAngles = Vector3.up * __instance._ghostSpawns[Data.indexTwo].spawnTransform.eulerAngles.y; + WorldObject.TabulaRasa(); + + __instance._numEnabledGhostProxies = Data.proxyIndex; + + if (!__instance._disableGhostProxies && __instance._numEnabledGhostProxies < __instance._ghostFinalDestinations.Length) + { + if (__instance._ghostFinalDestinations[__instance._numEnabledGhostProxies].proxyGhost != null) + { + __instance._ghostFinalDestinations[__instance._numEnabledGhostProxies].proxyGhost.Reveal(); + } + + __instance._numEnabledGhostProxies++; + } + } +} diff --git a/QSB/EchoesOfTheEye/Ghosts/Patches/GhostHotelDirectorPatches.cs b/QSB/EchoesOfTheEye/Ghosts/Patches/GhostHotelDirectorPatches.cs index c011964b5..051bc5712 100644 --- a/QSB/EchoesOfTheEye/Ghosts/Patches/GhostHotelDirectorPatches.cs +++ b/QSB/EchoesOfTheEye/Ghosts/Patches/GhostHotelDirectorPatches.cs @@ -41,6 +41,7 @@ public static bool OnDestroy(GhostHotelDirector __instance) /* * I have no idea why, but for some reason unknown to the damned souls that walk this mortal plane, * this method only runs when this patch is here. What the absolute fuck. + * Update - This is still needed. Run while you still can. */ [HarmonyPrefix] diff --git a/QSB/EchoesOfTheEye/Ghosts/Patches/GhostPartyPathDirectorPatches.cs b/QSB/EchoesOfTheEye/Ghosts/Patches/GhostPartyPathDirectorPatches.cs index 58ace11fa..ca9709eef 100644 --- a/QSB/EchoesOfTheEye/Ghosts/Patches/GhostPartyPathDirectorPatches.cs +++ b/QSB/EchoesOfTheEye/Ghosts/Patches/GhostPartyPathDirectorPatches.cs @@ -4,6 +4,8 @@ using QSB.Utility; using QSB.WorldSync; using System.Reflection; +using QSB.EchoesOfTheEye.Ghosts.Messages; +using QSB.Messaging; using UnityEngine; namespace QSB.EchoesOfTheEye.Ghosts.Patches; @@ -76,8 +78,11 @@ public static bool Update(GhostPartyPathDirector __instance) if (!__instance._respawnBlockTrigger.IsTrackingObject(Locator.GetPlayerDetector())) { __instance._dispatchedGhosts.QuickRemoveAt(i); - ghostBrain.AttachedObject.transform.position = __instance._ghostSpawns[Random.Range(0, __instance._ghostSpawns.Length)].spawnTransform.position; - ghostBrain.AttachedObject.transform.eulerAngles = Vector3.up * __instance._ghostSpawns[Random.Range(0, __instance._ghostSpawns.Length)].spawnTransform.eulerAngles.y; + var indexOne = Random.Range(0, __instance._ghostSpawns.Length); + var indexTwo = Random.Range(0, __instance._ghostSpawns.Length); + ghostBrain.SendMessage(new PartyPathResetMessage(indexOne, indexTwo, __instance._numEnabledGhostProxies)); + ghostBrain.AttachedObject.transform.position = __instance._ghostSpawns[indexOne].spawnTransform.position; + ghostBrain.AttachedObject.transform.eulerAngles = Vector3.up * __instance._ghostSpawns[indexTwo].spawnTransform.eulerAngles.y; ghostBrain.TabulaRasa(); partyPathAction.ResetPath(); if (!__instance._disableGhostProxies && __instance._numEnabledGhostProxies < __instance._ghostFinalDestinations.Length) @@ -110,7 +115,7 @@ public static bool Update(GhostPartyPathDirector __instance) __instance._waitingGhosts.RemoveAt(0); __instance._lastDispatchedGhost = ghostBrain2.AttachedObject; __instance._dispatchedGhosts.Add(ghostBrain2.AttachedObject); - __instance._nextGhostDispatchTime = Time.timeSinceLevelLoad + Random.Range(__instance._minGhostDispatchDelay, __instance._maxGhostDispatchDelay); + __instance._nextGhostDispatchTime = Time.timeSinceLevelLoad + UnityEngine.Random.Range(__instance._minGhostDispatchDelay, __instance._maxGhostDispatchDelay); } for (var j = 0; j < __instance._ghostSpawns.Length; j++) diff --git a/QSB/EchoesOfTheEye/Ghosts/WorldObjects/QSBGhostEffects.cs b/QSB/EchoesOfTheEye/Ghosts/WorldObjects/QSBGhostEffects.cs index 310d30907..2794f67fb 100644 --- a/QSB/EchoesOfTheEye/Ghosts/WorldObjects/QSBGhostEffects.cs +++ b/QSB/EchoesOfTheEye/Ghosts/WorldObjects/QSBGhostEffects.cs @@ -98,7 +98,7 @@ public void Update_Effects() var closestPlayer = QSBPlayerManager.GetClosestPlayerToWorldPoint(AttachedObject.transform.position, true); var num5 = (closestPlayer?.AssignedSimulationLantern?.AttachedObject?.GetLanternController()?.GetLight()?.GetFlickerScale() - 1f + 0.07f) / 0.14f ?? 0; num5 = Mathf.Lerp(0.7f, 1f, num5); - AttachedObject.SetEyeGlow(AttachedObject._eyeGlow * num3); + AttachedObject.SetEyeGlow(AttachedObject._eyeGlow * num5); if (AttachedObject._playingDeathSequence) { diff --git a/QSB/EchoesOfTheEye/LightSensorSync/LightSensorManager.cs b/QSB/EchoesOfTheEye/LightSensorSync/LightSensorManager.cs index 4fa61dc63..f65565ca6 100644 --- a/QSB/EchoesOfTheEye/LightSensorSync/LightSensorManager.cs +++ b/QSB/EchoesOfTheEye/LightSensorSync/LightSensorManager.cs @@ -1,6 +1,6 @@ using Cysharp.Threading.Tasks; using QSB.EchoesOfTheEye.LightSensorSync.WorldObjects; -using QSB.Utility; +using QSB.Utility.Deterministic; using QSB.WorldSync; using System.Linq; using System.Threading; diff --git a/QSB/EchoesOfTheEye/LightSensorSync/Patches/LightSensorPatches.cs b/QSB/EchoesOfTheEye/LightSensorSync/Patches/LightSensorPatches.cs index f1f85380b..08bd29787 100644 --- a/QSB/EchoesOfTheEye/LightSensorSync/Patches/LightSensorPatches.cs +++ b/QSB/EchoesOfTheEye/LightSensorSync/Patches/LightSensorPatches.cs @@ -132,6 +132,8 @@ private static bool ManagedFixedUpdate(SingleLightSensor __instance) UpdateLocalIllumination(__instance); if (!locallyIlluminated && qsbLightSensor._locallyIlluminated) { + // force ownership to mask latency + qsbLightSensor.ForceOwnership(); qsbLightSensor.OnDetectLocalLight?.Invoke(); } else if (locallyIlluminated && !qsbLightSensor._locallyIlluminated) diff --git a/QSB/EchoesOfTheEye/LightSensorSync/WorldObjects/QSBLightSensor.cs b/QSB/EchoesOfTheEye/LightSensorSync/WorldObjects/QSBLightSensor.cs index 5e9ca8225..312cb9176 100644 --- a/QSB/EchoesOfTheEye/LightSensorSync/WorldObjects/QSBLightSensor.cs +++ b/QSB/EchoesOfTheEye/LightSensorSync/WorldObjects/QSBLightSensor.cs @@ -32,6 +32,7 @@ public override async UniTask Init(CancellationToken ct) { if (AttachedObject._startIlluminated) { + // dont need to do _illuminated cuz _startIlluminated means it already sets the thing and fires the event in Start _locallyIlluminated = true; OnDetectLocalLight?.Invoke(); } diff --git a/QSB/EchoesOfTheEye/QSBRotatingElements.cs b/QSB/EchoesOfTheEye/QSBRotatingElements.cs index 2cb221706..65fcf319b 100644 --- a/QSB/EchoesOfTheEye/QSBRotatingElements.cs +++ b/QSB/EchoesOfTheEye/QSBRotatingElements.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading; +using QSB.Utility; using UnityEngine; namespace QSB.EchoesOfTheEye; @@ -16,6 +17,7 @@ public abstract class QSBRotatingElements : LinkedWorldObject where U : NetworkBehaviour { protected abstract IEnumerable LightSensors { get; } + protected virtual bool LockedActive => false; private QSBLightSensor[] _qsbLightSensors; private int _litSensors; @@ -61,7 +63,9 @@ private void OnDetectLocalDarkness() _litSensors--; if (_litSensors == 0) { - NetworkBehaviour.netIdentity.UpdateOwnerQueue(OwnerQueueAction.Remove); + Delay.RunWhen( + () => LockedActive == false, + () => NetworkBehaviour.netIdentity.UpdateOwnerQueue(OwnerQueueAction.Remove)); } } diff --git a/QSB/EchoesOfTheEye/RaftSync/Messages/RaftCarrierOnEntryMessage.cs b/QSB/EchoesOfTheEye/RaftSync/Messages/RaftCarrierOnEntryMessage.cs new file mode 100644 index 000000000..24e7e4309 --- /dev/null +++ b/QSB/EchoesOfTheEye/RaftSync/Messages/RaftCarrierOnEntryMessage.cs @@ -0,0 +1,64 @@ +using QSB.EchoesOfTheEye.RaftSync.WorldObjects; +using QSB.Messaging; +using QSB.WorldSync; +using UnityEngine; + +namespace QSB.EchoesOfTheEye.RaftSync.Messages; + +public class RaftCarrierOnEntryMessage : QSBWorldObjectMessage +{ + public RaftCarrierOnEntryMessage(QSBRaft raft) : base(raft.ObjectId) { } + + public override void OnReceiveRemote() + { + // TODO : work out if we can just call RaftCarrier.OnEntry with a right gameobject? tried it with _fluidDetector.gameObject and it didn't work + // even tho that is the gameobject with Raft_Detector tag. what? + + var qsbRaft = Data.GetWorldObject(); + var attachedObj = (RaftCarrier)WorldObject.AttachedObject; + + attachedObj._raft = qsbRaft.AttachedObject; + attachedObj._raft.OnArriveAtTarget += attachedObj.OnArriveAtTarget; + attachedObj.GetAlignDestination().localEulerAngles = Vector3.zero; + + var relativeDockForward = attachedObj.GetAlignDestination().InverseTransformDirection(attachedObj._raft.transform.forward); + relativeDockForward.y = 0f; + + var targetRaftRotation = OWMath.Angle(Vector3.forward, relativeDockForward, Vector3.up); + targetRaftRotation = OWMath.RoundToNearestMultiple(targetRaftRotation, 90f); + attachedObj.GetAlignDestination().localEulerAngles = new Vector3(0f, targetRaftRotation, 0f); + + var raftMovementDirection = attachedObj.GetAlignDestination().position - attachedObj._raft.GetBody().GetPosition(); + raftMovementDirection = Vector3.Project(raftMovementDirection, attachedObj._raft.transform.up); + + var targetPosition = attachedObj.GetAlignDestination().position - attachedObj.GetAlignDestination().up * raftMovementDirection.magnitude; + + attachedObj._raft.MoveToTarget(targetPosition, attachedObj.GetAlignDestination().rotation, attachedObj._raftAlignSpeed, false); + attachedObj._oneShotAudio.PlayOneShot(global::AudioType.Raft_Reel_Start, 1f); + attachedObj._loopingAudio.FadeIn(0.2f, false, false, 1f); + attachedObj._state = RaftCarrier.DockState.AligningBelow; + + if (WorldObject.AttachedObject is RaftDock dock) + { + if (dock._state == RaftCarrier.DockState.AligningBelow) + { + dock.enabled = true; + } + } + else if (WorldObject.AttachedObject is DamRaftLift lift) + { + if (lift._state == RaftCarrier.DockState.AligningBelow) + { + lift.enabled = true; + + foreach (var node in lift._liftNodes) + { + node.localEulerAngles = lift.GetAlignDestination().localEulerAngles; + } + + lift._nodeIndex = 1; + lift._raftDockLights.SetLightsActivation(true, false); + } + } + } +} diff --git a/QSB/EchoesOfTheEye/RaftSync/Patches/RaftPatches.cs b/QSB/EchoesOfTheEye/RaftSync/Patches/RaftPatches.cs index cff95f297..e9dfdae53 100644 --- a/QSB/EchoesOfTheEye/RaftSync/Patches/RaftPatches.cs +++ b/QSB/EchoesOfTheEye/RaftSync/Patches/RaftPatches.cs @@ -42,6 +42,36 @@ private static void RaftDock_OnPressInteract(RaftDock __instance) return; } + if (!QSBWorldSync.AllObjectsReady) + { + return; + } + __instance.GetWorldObject().SendMessage(new RaftDockOnPressInteractMessage()); } + + [HarmonyPrefix] + [HarmonyPatch(typeof(RaftCarrier), nameof(RaftCarrier.OnEntry))] + private static bool RaftCarrier_OnEntry(RaftCarrier __instance, GameObject hitObj) + { + if (!QSBWorldSync.AllObjectsReady) + { + return true; + } + + if (hitObj.CompareTag("RaftDetector") && __instance._state == RaftCarrier.DockState.Ready) + { + var raft = hitObj.GetComponentInParent(); + var qsbRaft = raft.GetWorldObject(); + // owner will handle docking and sends a message telling everyone else to also dock + if (!qsbRaft.NetworkBehaviour.isOwned) + { + return false; + } + + __instance.GetWorldObject().SendMessage(new RaftCarrierOnEntryMessage(qsbRaft)); + } + + return true; + } } diff --git a/QSB/EchoesOfTheEye/RaftSync/RaftManager.cs b/QSB/EchoesOfTheEye/RaftSync/RaftManager.cs index 4dc784a01..83eee6957 100644 --- a/QSB/EchoesOfTheEye/RaftSync/RaftManager.cs +++ b/QSB/EchoesOfTheEye/RaftSync/RaftManager.cs @@ -14,5 +14,6 @@ public override async UniTask BuildWorldObjects(OWScene scene, CancellationToken { QSBWorldSync.Init(); QSBWorldSync.Init(); + QSBWorldSync.Init(); } } diff --git a/QSB/EchoesOfTheEye/RaftSync/TransformSync/RaftTransformSync.cs b/QSB/EchoesOfTheEye/RaftSync/TransformSync/RaftTransformSync.cs index 5b6b2f37b..727107dd9 100644 --- a/QSB/EchoesOfTheEye/RaftSync/TransformSync/RaftTransformSync.cs +++ b/QSB/EchoesOfTheEye/RaftSync/TransformSync/RaftTransformSync.cs @@ -10,8 +10,13 @@ namespace QSB.EchoesOfTheEye.RaftSync.TransformSync; public class RaftTransformSync : UnsectoredRigidbodySync, ILinkedNetworkBehaviour { + /// + /// move if on the raft + /// or in air near the raft + /// private bool ShouldMovePlayer => - Vector3.Distance(AttachedTransform.position, Locator.GetPlayerBody().GetPosition()) < 10; + Locator.GetPlayerController().GetGroundBody() == AttachedRigidbody || + (Locator.GetPlayerController().GetGroundBody() == null && Vector3.Distance(AttachedTransform.position, Locator.GetPlayerBody().GetPosition()) < 10); protected override bool UseInterpolation => !ShouldMovePlayer; private float _lastSetPositionTime; @@ -80,6 +85,15 @@ protected override void Uninit() /// protected override void ApplyToAttached() { + if (_worldObject.AttachedObject is RaftController raft) + { + if (raft.IsDockingOrDocked()) + { + // don't sync position if we're docking, dock sequence moves the raft itself + return; + } + } + var targetPos = ReferenceTransform.FromRelPos(UseInterpolation ? SmoothPosition : transform.position); var targetRot = ReferenceTransform.FromRelRot(UseInterpolation ? SmoothRotation : transform.rotation); diff --git a/QSB/EchoesOfTheEye/RaftSync/WorldObjects/IQSBRaftCarrier.cs b/QSB/EchoesOfTheEye/RaftSync/WorldObjects/IQSBRaftCarrier.cs new file mode 100644 index 000000000..c9743a123 --- /dev/null +++ b/QSB/EchoesOfTheEye/RaftSync/WorldObjects/IQSBRaftCarrier.cs @@ -0,0 +1,5 @@ +using QSB.WorldSync; + +namespace QSB.EchoesOfTheEye.RaftSync.WorldObjects; + +public interface IQSBRaftCarrier : IWorldObject { } diff --git a/QSB/EchoesOfTheEye/RaftSync/WorldObjects/QSBDamRaftLift.cs b/QSB/EchoesOfTheEye/RaftSync/WorldObjects/QSBDamRaftLift.cs new file mode 100644 index 000000000..65dff69df --- /dev/null +++ b/QSB/EchoesOfTheEye/RaftSync/WorldObjects/QSBDamRaftLift.cs @@ -0,0 +1,3 @@ +namespace QSB.EchoesOfTheEye.RaftSync.WorldObjects; + +public class QSBDamRaftLift : QSBRaftCarrier { } diff --git a/QSB/EchoesOfTheEye/RaftSync/WorldObjects/QSBRaft.cs b/QSB/EchoesOfTheEye/RaftSync/WorldObjects/QSBRaft.cs index 5c6dfe1f9..a4979e5cd 100644 --- a/QSB/EchoesOfTheEye/RaftSync/WorldObjects/QSBRaft.cs +++ b/QSB/EchoesOfTheEye/RaftSync/WorldObjects/QSBRaft.cs @@ -15,8 +15,6 @@ public class QSBRaft : LinkedWorldObject, IQS { IItemDropTarget IQSBDropTarget.AttachedObject => AttachedObject; - public override bool ShouldDisplayDebug() => false; - protected override GameObject NetworkObjectPrefab => QSBNetworkManager.singleton.RaftPrefab; protected override bool SpawnWithServerOwnership => false; @@ -49,7 +47,13 @@ private void OnDetectLocalLight() { if (AttachedObject.IsPlayerRiding()) { + // force ownership to mask latency NetworkBehaviour.netIdentity.UpdateOwnerQueue(OwnerQueueAction.Force); } } + + public override string ReturnLabel() + { + return $"ID:{ObjectId}\r\nIsDockedOrDocking:{AttachedObject.IsDockingOrDocked()}\r\nGetLocalAcceleration:{AttachedObject.GetLocalAcceleration()}"; + } } diff --git a/QSB/EchoesOfTheEye/RaftSync/WorldObjects/QSBRaftCarrier.cs b/QSB/EchoesOfTheEye/RaftSync/WorldObjects/QSBRaftCarrier.cs new file mode 100644 index 000000000..3a3193ecd --- /dev/null +++ b/QSB/EchoesOfTheEye/RaftSync/WorldObjects/QSBRaftCarrier.cs @@ -0,0 +1,6 @@ +using QSB.WorldSync; + +namespace QSB.EchoesOfTheEye.RaftSync.WorldObjects; + +public abstract class QSBRaftCarrier : WorldObject, IQSBRaftCarrier + where T : RaftCarrier { } diff --git a/QSB/EchoesOfTheEye/RaftSync/WorldObjects/QSBRaftDock.cs b/QSB/EchoesOfTheEye/RaftSync/WorldObjects/QSBRaftDock.cs index 9b4761f3b..fb8380598 100644 --- a/QSB/EchoesOfTheEye/RaftSync/WorldObjects/QSBRaftDock.cs +++ b/QSB/EchoesOfTheEye/RaftSync/WorldObjects/QSBRaftDock.cs @@ -1,9 +1,8 @@ using QSB.ItemSync.WorldObjects; -using QSB.WorldSync; namespace QSB.EchoesOfTheEye.RaftSync.WorldObjects; -public class QSBRaftDock : WorldObject, IQSBDropTarget +public class QSBRaftDock : QSBRaftCarrier, IQSBDropTarget { IItemDropTarget IQSBDropTarget.AttachedObject => AttachedObject; } diff --git a/QSB/ElevatorSync/Patches/NomaiElevatorPatches.cs b/QSB/ElevatorSync/Patches/NomaiElevatorPatches.cs new file mode 100644 index 000000000..26e72913d --- /dev/null +++ b/QSB/ElevatorSync/Patches/NomaiElevatorPatches.cs @@ -0,0 +1,77 @@ +using HarmonyLib; +using QSB.Patches; +using QSB.Utility; +using UnityEngine; + +namespace QSB.ElevatorSync.Patches; + +[HarmonyPatch] +public class NomaiElevatorPatches : QSBPatch +{ + public override QSBPatchTypes Type => QSBPatchTypes.OnClientConnect; + + private static NomaiElevator blackHoleForge; + private static OWTriggerVolume blackHoleForgeTrigger; + private static NomaiElevator blackHoleForgeEntrance; + private static OWTriggerVolume blackHoleForgeEntranceTrigger; + + private static bool runOnce; + + [HarmonyPostfix] + [HarmonyPatch(typeof(NomaiElevator), nameof(NomaiElevator.FixedUpdate))] + public static void NomaiElevator_FixedUpdate(NomaiElevator __instance) + { + // The forge will handle everything. + if (__instance.name == "BlackHoleForge_EntrancePivot") return; + + if (!blackHoleForge || !blackHoleForgeTrigger) + { + blackHoleForge = GameObject.Find("BlackHoleForgePivot").GetComponent(); + // Use a built-in trigger. + blackHoleForgeTrigger = GameObject.Find("BrittleHollow_Body/Sector_BH/Sector_NorthHemisphere/" + + "Sector_NorthPole/Sector_HangingCity/Sector_HangingCity_BlackHoleForge/BlackHoleForgePivot/" + + "Volumes_BlackHoleForge/DirectionalForceVolume") + .GetComponent(); + } + + if (!blackHoleForgeEntrance || !blackHoleForgeEntranceTrigger) + { + blackHoleForgeEntrance = GameObject.Find("BlackHoleForge_EntrancePivot").GetComponent(); + blackHoleForgeEntranceTrigger = GameObject.Find("FakeSector_BlackHoleForge_EntrancePivot") + .GetComponent(); + } + + var speed = blackHoleForge._speed; + var player = Locator.GetPlayerDetector(); + + var isInForge = blackHoleForgeTrigger.IsTrackingObject(player); + var isInEntrance = blackHoleForgeEntranceTrigger.IsTrackingObject(player); + + if (isInEntrance) + { + // Speed is added to make sure the player moves with the forge AND the entrance. + speed += blackHoleForgeEntrance._speed; + } + + if (isInEntrance || isInForge) + { + // Players do not move with the forge in the game, + // so they remain in place while the forge slides past them. + // This makes sure they move with the forge. + var newPos = Locator.GetPlayerTransform().position + new Vector3(0f, speed * Time.deltaTime, 0f); + Locator.GetPlayerTransform().position = newPos; + } + + if (!runOnce) + { + // Recenter the universe because the player has been manually moved. + runOnce = true; + + Delay.RunWhen(() => !__instance.enabled, () => + { + runOnce = false; + CenterOfTheUniverse.s_instance.OnPlayerRepositioned(); + }); + } + } +} diff --git a/QSB/ElevatorSync/WorldObjects/QSBElevator.cs b/QSB/ElevatorSync/WorldObjects/QSBElevator.cs index 1cc6b6721..b7bcdf3bc 100644 --- a/QSB/ElevatorSync/WorldObjects/QSBElevator.cs +++ b/QSB/ElevatorSync/WorldObjects/QSBElevator.cs @@ -2,6 +2,7 @@ using QSB.ElevatorSync.Messages; using QSB.Messaging; using QSB.Patches; +using QSB.Utility; using QSB.WorldSync; using System.Threading; using UnityEngine; @@ -12,15 +13,26 @@ public class QSBElevator : WorldObject { private OWTriggerVolume _elevatorTrigger; + // Used to reset attach position. This is in local space. + public Vector3 originalAttachPosition; + public override async UniTask Init(CancellationToken ct) { - // BUG : This won't work for the log lift! need to make a different trigger for that - var boxShape = AttachedObject.gameObject.GetAddComponent(); - boxShape.center = new Vector3(0, 1.75f, 0.25f); - boxShape.size = new Vector3(3, 3.5f, 3); + + if (Name == "LogLift") + { + boxShape.size = new Vector3(4.6f, 3.5f, 12); + boxShape.center = new Vector3(0.1f, 1.75f, 1.3f); + } + else + { + boxShape.size = new Vector3(3, 3.5f, 3); + boxShape.center = new Vector3(0, 1.75f, 0.25f); + } _elevatorTrigger = AttachedObject.gameObject.GetAddComponent(); + originalAttachPosition = AttachedObject._attachPoint.transform.localPosition; } public void RemoteCall(bool isGoingUp) @@ -33,7 +45,10 @@ public void RemoteCall(bool isGoingUp) SetDirection(isGoingUp); if (_elevatorTrigger.IsTrackingObject(Locator.GetPlayerDetector())) { - AttachedObject._attachPoint.AttachPlayer(); + var attachPoint = AttachedObject._attachPoint; + attachPoint.transform.position = Locator.GetPlayerTransform().position; + + attachPoint.AttachPlayer(); if (Locator.GetPlayerSuit().IsWearingSuit() && Locator.GetPlayerSuit().IsTrainingSuit()) { Locator.GetPlayerSuit().RemoveSuit(); @@ -41,6 +56,14 @@ public void RemoteCall(bool isGoingUp) } AttachedObject.StartLift(); + + // Runs when the lift/elevator is done moving. + // Reset the position of the attach point. + Delay.RunWhen(() => !AttachedObject.enabled, () => + { + AttachedObject._attachPoint.transform.localPosition = originalAttachPosition; + }); + } private void SetDirection(bool isGoingUp) diff --git a/QSB/HUD/Messages/ChatMessage.cs b/QSB/HUD/Messages/ChatMessage.cs index b8333299c..b21e1e24b 100644 --- a/QSB/HUD/Messages/ChatMessage.cs +++ b/QSB/HUD/Messages/ChatMessage.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; +using QSB.Player; using UnityEngine; namespace QSB.HUD.Messages; @@ -17,5 +18,27 @@ public ChatMessage(string msg, Color color) : base((msg, color)) { } public override void OnReceiveRemote() { MultiplayerHUDManager.Instance.WriteMessage(Data.message, Data.color); + + var fromPlayer = QSBPlayerManager.GetPlayer(From); + var qsb = false; + string name; + if (Data.message.StartsWith("QSB: ")) + { + name = "QSB: "; + qsb = true; + } + else if (Data.message.StartsWith($"{fromPlayer.Name}: ")) + { + name = $"{fromPlayer.Name}: "; + } + else + { + // uhhh idk what happened + MultiplayerHUDManager.OnChatMessageEvent.Invoke(Data.message, From); + return; + } + + var messageWithoutName = Data.message.Remove(Data.message.IndexOf(name), name.Length); + MultiplayerHUDManager.OnChatMessageEvent.Invoke(messageWithoutName, qsb ? uint.MaxValue : From); } } \ No newline at end of file diff --git a/QSB/HUD/MultiplayerHUDManager.cs b/QSB/HUD/MultiplayerHUDManager.cs index 36a771834..c5c225571 100644 --- a/QSB/HUD/MultiplayerHUDManager.cs +++ b/QSB/HUD/MultiplayerHUDManager.cs @@ -9,6 +9,7 @@ using System; using System.Linq; using UnityEngine; +using UnityEngine.Events; using UnityEngine.InputSystem; using UnityEngine.SceneManagement; using UnityEngine.UI; @@ -23,7 +24,6 @@ public class MultiplayerHUDManager : MonoBehaviour, IAddComponentOnStart private Transform _textChat; private InputField _inputField; private Material _markerMaterial; - private bool _ready; public static Sprite UnknownSprite; public static Sprite DeadSprite; @@ -42,6 +42,9 @@ public class MultiplayerHUDManager : MonoBehaviour, IAddComponentOnStart public static readonly ListStack HUDIconStack = new(true); + public class ChatEvent : UnityEvent { } + public static readonly ChatEvent OnChatMessageEvent = new(); + private void Start() { Instance = this; @@ -65,15 +68,10 @@ private void Start() Interloper = QSBCore.HUDAssetBundle.LoadAsset("Assets/MULTIPLAYER_UI/playerbox_interloper.png"); WhiteHole = QSBCore.HUDAssetBundle.LoadAsset("Assets/MULTIPLAYER_UI/playerbox_whitehole.png"); SpaceSprite = QSBCore.HUDAssetBundle.LoadAsset("Assets/MULTIPLAYER_UI/playerbox_space.png"); - - QSBSceneManager.OnPostSceneLoad += (OWScene old, OWScene newScene) => - { - _ready = false; - }; } - private const int LINE_COUNT = 11; - private const int CHAR_COUNT = 41; + private const int LINE_COUNT = 10; + private const int CHAR_COUNT = 33; private const float FADE_DELAY = 5f; private const float FADE_TIME = 2f; @@ -87,13 +85,13 @@ private void Start() // perks of being a qsb dev :-) public void WriteSystemMessage(string message, Color color) { - WriteMessage(message, color); + WriteMessage($"QSB: {message}", color); + OnChatMessageEvent.Invoke(message, uint.MaxValue); } public void WriteMessage(string message, Color color) { - // dont write messages when not ready - if (!_ready) + if (!QSBWorldSync.AllObjectsReady) { return; } @@ -117,7 +115,7 @@ public void WriteMessage(string message, Color color) _messages.PopFromBack(); } - var currentLineIndex = 10; + var currentLineIndex = LINE_COUNT - 1; foreach (var msg in _messages.Reverse()) { @@ -285,7 +283,7 @@ private void OnWakeUp() rect.anchorMax = new Vector2(1, 0.5f); rect.sizeDelta = new Vector2(100, 100); rect.anchoredPosition3D = new Vector3(-267, 0, 0); - rect.localRotation = Quaternion.Euler(0, 55, 0); + rect.localRotation = Quaternion.identity; rect.localScale = Vector3.one; }); @@ -327,8 +325,6 @@ private void OnWakeUp() _lines.Clear(); _messages.Clear(); _textChat.GetComponent().alpha = 0; - - _ready = true; } public void UpdateMinimapMarkers(Minimap minimap) diff --git a/QSB/Inputs/QSBInputManager.cs b/QSB/Inputs/QSBInputManager.cs index 05150c7a8..62aa90ec5 100644 --- a/QSB/Inputs/QSBInputManager.cs +++ b/QSB/Inputs/QSBInputManager.cs @@ -5,50 +5,6 @@ namespace QSB.Inputs; public class QSBInputManager : MonoBehaviour, IAddComponentOnStart { - // TODO : finish instruments - disabled for 0.7.0 release - /* - public static event Action ChertTaunt; - public static event Action EskerTaunt; - public static event Action RiebeckTaunt; - public static event Action GabbroTaunt; - public static event Action FeldsparTaunt; - public static event Action ExitTaunt; - - public void Update() - { - if (Input.GetKey(KeyCode.T)) - { - // Listed order is from sun to dark bramble - if (Input.GetKeyDown(KeyCode.Alpha1)) - { - ChertTaunt?.Invoke(); - } - else if (Input.GetKeyDown(KeyCode.Alpha2)) - { - EskerTaunt?.Invoke(); - } - else if (Input.GetKeyDown(KeyCode.Alpha5)) - { - RiebeckTaunt?.Invoke(); - } - else if (Input.GetKeyDown(KeyCode.Alpha4)) - { - GabbroTaunt?.Invoke(); - } - else if (Input.GetKeyDown(KeyCode.Alpha3)) - { - FeldsparTaunt?.Invoke(); - } - } - - if (OWInput.GetValue(InputLibrary.moveXZ, InputMode.None) != Vector2.zero - || OWInput.GetValue(InputLibrary.jump, InputMode.None) != 0f) - { - ExitTaunt?.Invoke(); - } - } - */ - public static QSBInputManager Instance { get; private set; } public void Start() diff --git a/QSB/ItemSync/ItemManager.cs b/QSB/ItemSync/ItemManager.cs index afa1a8eea..f429acaa4 100644 --- a/QSB/ItemSync/ItemManager.cs +++ b/QSB/ItemSync/ItemManager.cs @@ -4,6 +4,7 @@ using QSB.ItemSync.WorldObjects.Items; using QSB.ItemSync.WorldObjects.Sockets; using QSB.Utility; +using QSB.Utility.Deterministic; using QSB.WorldSync; using System.Linq; using System.Threading; diff --git a/QSB/Localization/Translation.cs b/QSB/Localization/Translation.cs index 4ed59efec..8a2481307 100644 --- a/QSB/Localization/Translation.cs +++ b/QSB/Localization/Translation.cs @@ -10,7 +10,7 @@ public class Translation public string PauseMenuDisconnect; public string PauseMenuStopHosting; public string PublicIPAddress; - public string ProductUserID; + public string SteamID; public string Connect; public string Cancel; public string HostExistingOrNewOrCopy; @@ -23,7 +23,7 @@ public class Translation public string Yes; public string No; public string StopHostingAreYouSure; - public string CopyProductUserIDToClipboard; + public string CopySteamIDToClipboard; public string Connecting; public string OK; public string ServerRefusedConnection; @@ -33,7 +33,6 @@ public class Translation public string DLCMismatch; public string GameProgressLimit; public string AddonMismatch; - public string IncompatibleMod; public string PlayerJoinedTheGame; public string PlayerLeftTheGame; public string PlayerWasKicked; diff --git a/QSB/Menus/MenuManager.cs b/QSB/Menus/MenuManager.cs index e1f1c98b1..7c5caaca3 100644 --- a/QSB/Menus/MenuManager.cs +++ b/QSB/Menus/MenuManager.cs @@ -1,6 +1,6 @@ -using EpicTransport; -using Mirror; +using Mirror; using OWML.Common; +using OWML.Utils; using QSB.Localization; using QSB.Messaging; using QSB.Player.TransformSync; @@ -8,6 +8,7 @@ using QSB.SaveSync.Messages; using QSB.Utility; using QSB.WorldSync; +using Steamworks; using System; using System.Linq; using System.Text; @@ -124,7 +125,7 @@ public void OnLanguageChanged() 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.UseKcpTransport ? QSBLocalization.Current.PublicIPAddress : QSBLocalization.Current.ProductUserID; + var text = QSBCore.UseKcpTransport ? QSBLocalization.Current.PublicIPAddress : QSBLocalization.Current.SteamID; 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, @@ -340,7 +341,7 @@ private void OnCloseInfoPopup(bool confirm) private void CreateCommonPopups() { - var text = QSBCore.UseKcpTransport ? QSBLocalization.Current.PublicIPAddress : QSBLocalization.Current.ProductUserID; + var text = QSBCore.UseKcpTransport ? QSBLocalization.Current.PublicIPAddress : QSBLocalization.Current.SteamID; ConnectPopup = QSBCore.MenuApi.MakeInputFieldPopup(text, text, QSBLocalization.Current.Connect, QSBLocalization.Current.Cancel); ConnectPopup.CloseMenuOnOk(false); ConnectPopup.OnPopupConfirm += () => @@ -638,26 +639,30 @@ private void Host(bool newMultiplayerSave) if (!QSBCore.UseKcpTransport) { - var productUserId = EOSSDKComponent.LocalUserProductIdString; + var steamId = SteamUser.GetSteamID().ToString(); PopupClose += confirm => { if (confirm) { - GUIUtility.systemCopyBuffer = productUserId; + GUIUtility.systemCopyBuffer = steamId; } LoadGame(PlayerData.GetWarpedToTheEye()); + // wait until scene load and then wait until Start has ran + // why is this done? GameStateMessage etc works on title screen since nonhost has to deal with that Delay.RunWhen(() => TimeLoop._initialized, QSBNetworkManager.singleton.StartHost); }; - OpenInfoPopup(string.Format(QSBLocalization.Current.CopyProductUserIDToClipboard, productUserId) + OpenInfoPopup(string.Format(QSBLocalization.Current.CopySteamIDToClipboard, steamId) , QSBLocalization.Current.Yes , QSBLocalization.Current.No); } else { LoadGame(PlayerData.GetWarpedToTheEye()); + // wait until scene load and then wait until Start has ran + // why is this done? GameStateMessage etc works on title screen since nonhost has to deal with that Delay.RunWhen(() => TimeLoop._initialized, QSBNetworkManager.singleton.StartHost); } } diff --git a/QSB/Menus/PreflightChecklistAdjustment.cs b/QSB/Menus/PreflightChecklistAdjustment.cs new file mode 100644 index 000000000..2ec1f562b --- /dev/null +++ b/QSB/Menus/PreflightChecklistAdjustment.cs @@ -0,0 +1,63 @@ +using QSB.Utility; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; +using UnityEngine.UI; + +namespace QSB.Menus; + +internal class PreflightChecklistAdjustment : MonoBehaviour, IAddComponentOnStart +{ + private string[] _preflightOptionsToRemove = new string[] + { + "UIElement-FreezeTimeTranslating", + "UIElement-FreezeTimeShipLog", + "UIElement-FreezeTimeConversations", + "UIElement-FreezeTimeTranslator", + "UIElement-FreezeTimeDialogue" + }; + + private MenuOption[] DestroyFreezeTimeOptions(MenuOption[] options) + { + var remainingMenuOptions = new List(); + foreach (var preflightChecklistOption in options) + { + if (_preflightOptionsToRemove.Contains(preflightChecklistOption.name)) + { + GameObject.Destroy(preflightChecklistOption.gameObject); + } + else + { + remainingMenuOptions.Add(preflightChecklistOption); + } + } + return remainingMenuOptions.ToArray(); + } + + public void Awake() + { + QSBSceneManager.OnPostSceneLoad += (_, loadScene) => + { + if (QSBCore.IsInMultiplayer && loadScene.IsUniverseScene()) + { + // PREFLIGHT MENU IN THE SHIP + var suitMenuManager = GameObject.FindObjectOfType()._mainMenu; + suitMenuManager._menuOptions = DestroyFreezeTimeOptions(suitMenuManager._menuOptions); + + // Remove cosmetic elements from ship preflight checklist + var suitOptionsMenu = GameObject.Find("PauseMenu/PreFlightCanvas/OptionsMenu-Panel/SuitOptionsDisplayPanel/SuitOptionsMainMenu/"); + GameObject.Destroy(suitOptionsMenu.transform.Find("FreezeTimeImage").gameObject); + GameObject.Destroy(suitOptionsMenu.transform.Find("Box-FreezeTimeBorder").gameObject); + + + // PREFLIGHT MENU IN THE OPTIONS MENU + var settingsMenuView = GameObject.FindObjectOfType(); + settingsMenuView._listSettingsOptions = DestroyFreezeTimeOptions(settingsMenuView._listSettingsOptions); + + // This one also points to the same options, have to remove the destroyed objects from it + var menuGameplayPreFlight = GameObject.Find("PauseMenu/OptionsCanvas/OptionsMenu-Panel/OptionsDisplayPanel/GameplayMenu/MenuGameplayPreFl/").GetComponent(); + Delay.RunNextFrame(() => menuGameplayPreFlight._menuOptions = menuGameplayPreFlight._menuOptions.Where(x => x != null).ToArray()); + } + }; + } +} diff --git a/QSB/Messaging/OWEvents.cs b/QSB/Messaging/OWEvents.cs index fa8f0f538..3eadf9d04 100644 --- a/QSB/Messaging/OWEvents.cs +++ b/QSB/Messaging/OWEvents.cs @@ -41,4 +41,6 @@ public static class OWEvents public const string ProbeExitQuantumMoon = nameof(ProbeExitQuantumMoon); public const string EnterCloak = nameof(EnterCloak); public const string ExitCloak = nameof(ExitCloak); + public const string PutOnHelmet = nameof(PutOnHelmet); + public const string RemoveHelmet = nameof(RemoveHelmet); } \ No newline at end of file diff --git a/QSB/Messaging/QSBMessageManager.cs b/QSB/Messaging/QSBMessageManager.cs index e8152508e..c4be56f94 100644 --- a/QSB/Messaging/QSBMessageManager.cs +++ b/QSB/Messaging/QSBMessageManager.cs @@ -1,16 +1,24 @@ using Mirror; using OWML.Common; +using QSB.Audio.Messages; using QSB.ClientServerStateSync; using QSB.ClientServerStateSync.Messages; +using QSB.GeyserSync.Messages; +using QSB.MeteorSync.Messages; +using QSB.OwnershipSync; using QSB.Patches; using QSB.Player; using QSB.Player.Messages; using QSB.Player.TransformSync; +using QSB.QuantumSync.Messages; +using QSB.SaveSync.Messages; +using QSB.TimeSync.Messages; using QSB.Utility; +using QSB.Utility.LinkedWorldObject; using QSB.WorldSync; using System; using System.Collections.Generic; -using System.Linq; +using System.IO; using System.Runtime.CompilerServices; using System.Runtime.Serialization; @@ -20,17 +28,19 @@ public static class QSBMessageManager { #region inner workings - internal static readonly Type[] _types; - internal static readonly Dictionary _typeToId = new(); + internal static readonly Dictionary _types = new(); + + private static string _rxPath; + private static string _txPath; static QSBMessageManager() { - _types = typeof(QSBMessage).GetDerivedTypes().ToArray(); - for (ushort i = 0; i < _types.Length; i++) + foreach (var type in typeof(QSBMessage).GetDerivedTypes()) { - _typeToId.Add(_types[i], i); + var hash = type.FullName.GetStableHashCode(); + _types.Add(hash, type); // call static constructor of message if needed - RuntimeHelpers.RunClassConstructor(_types[i].TypeHandle); + RuntimeHelpers.RunClassConstructor(type.TypeHandle); } } @@ -38,10 +48,27 @@ public static void Init() { NetworkServer.RegisterHandler((_, wrapper) => OnServerReceive(wrapper)); NetworkClient.RegisterHandler(wrapper => OnClientReceive(wrapper)); + + if (!QSBCore.DebugSettings.LogQSBMessages) + { + return; + } + + var time = DateTime.Now.ToString("yyyy-MM-dd--HH-mm-ss"); + _rxPath = Path.Combine(QSBCore.Helper.Manifest.ModFolderPath, $"{time}_rx_log.txt"); + _txPath = Path.Combine(QSBCore.Helper.Manifest.ModFolderPath, $"{time}_tx_log.txt"); + + File.Create(_rxPath); + File.Create(_txPath); } private static void OnServerReceive(QSBMessage msg) { + if (msg == null) + { + return; + } + if (msg.To == uint.MaxValue) { NetworkServer.SendToAll(msg); @@ -66,6 +93,11 @@ private static void OnServerReceive(QSBMessage msg) private static void OnClientReceive(QSBMessage msg) { + if (msg == null) + { + return; + } + if (PlayerTransformSync.LocalInstance == null) { DebugLog.ToConsole($"Warning - Tried to handle message {msg} before local player was established.", MessageType.Warning); @@ -77,9 +109,9 @@ private static void OnClientReceive(QSBMessage msg) var player = QSBPlayerManager.GetPlayer(msg.From); if (!player.IsReady - && player.PlayerId != QSBPlayerManager.LocalPlayerId - && player.State is ClientState.AliveInSolarSystem or ClientState.AliveInEye or ClientState.DeadInSolarSystem - && msg is not (PlayerInformationMessage or PlayerReadyMessage or RequestStateResyncMessage or ServerStateMessage)) + && player.PlayerId != QSBPlayerManager.LocalPlayerId + && player.State is ClientState.AliveInSolarSystem or ClientState.AliveInEye or ClientState.DeadInSolarSystem + && msg is not (PlayerInformationMessage or PlayerReadyMessage or RequestStateResyncMessage or ServerStateMessage)) { //DebugLog.ToConsole($"Warning - Got message {msg} from player {msg.From}, but they were not ready. Asking for state resync, just in case.", MessageType.Warning); new RequestStateResyncMessage().Send(); @@ -95,6 +127,7 @@ private static void OnClientReceive(QSBMessage msg) if (msg.From != QSBPlayerManager.LocalPlayerId) { + SaveRXTX(msg, false); QSBPatch.Remote = true; msg.OnReceiveRemote(); QSBPatch.Remote = false; @@ -122,6 +155,7 @@ public static void Send(this M msg) } msg.From = QSBPlayerManager.LocalPlayerId; + SaveRXTX(msg, true); NetworkClient.Send(msg); } @@ -132,6 +166,42 @@ public static void SendMessage(this T worldObject, M msg) msg.ObjectId = worldObject.ObjectId; Send(msg); } + + public static void SaveRXTX(QSBMessage msg, bool transmit) + { + if (!QSBCore.DebugSettings.LogQSBMessages) + { + return; + } + + if (msg + is ServerTimeMessage + or SocketStateChangeMessage + or OwnerQueueMessage + or GeyserMessage + or MeteorPreLaunchMessage + or MeteorLaunchMessage + or FragmentIntegrityMessage + or LinkMessage + or ShipLogFactSaveMessage + or QuantumOwnershipMessage + or PlayerMovementAudioFootstepMessage) + { + return; + } + + var filepath = transmit ? _txPath : _rxPath; + + DebugLog.DebugWrite($"{(transmit ? "TX" : "RX")} {msg.GetType().Name} from:{msg.From} to:{msg.To}"); + + var fileLines = File.ReadAllLines(filepath); + + var newlines = new List(); + newlines.AddRange(fileLines); + newlines.Add($"{msg.GetType().Name},{msg.From},{msg.To}"); + + File.WriteAllLines(filepath, newlines); + } } internal struct Wrapper : NetworkMessage @@ -146,8 +216,12 @@ public static class ReaderWriterExtensions { private static QSBMessage ReadQSBMessage(this NetworkReader reader) { - var id = reader.ReadUShort(); - var type = QSBMessageManager._types[id]; + var hash = reader.ReadInt(); + if (!QSBMessageManager._types.TryGetValue(hash, out var type)) + { + DebugLog.DebugWrite($"unknown QSBMessage type with hash {hash}", MessageType.Error); + return null; + } var msg = (QSBMessage)FormatterServices.GetUninitializedObject(type); msg.Deserialize(reader); return msg; @@ -156,8 +230,8 @@ private static QSBMessage ReadQSBMessage(this NetworkReader reader) private static void WriteQSBMessage(this NetworkWriter writer, QSBMessage msg) { var type = msg.GetType(); - var id = QSBMessageManager._typeToId[type]; - writer.Write(id); + var hash = type.FullName.GetStableHashCode(); + writer.WriteInt(hash); msg.Serialize(writer); } -} \ No newline at end of file +} diff --git a/QSB/MeteorSync/MeteorManager.cs b/QSB/MeteorSync/MeteorManager.cs index b93704aba..29c841f86 100644 --- a/QSB/MeteorSync/MeteorManager.cs +++ b/QSB/MeteorSync/MeteorManager.cs @@ -1,6 +1,7 @@ using Cysharp.Threading.Tasks; using QSB.MeteorSync.WorldObjects; using QSB.WorldSync; +using System.Linq; using System.Threading; namespace QSB.MeteorSync; @@ -16,7 +17,9 @@ public override async UniTask BuildWorldObjects(OWScene scene, CancellationToken // wait for all late initializers (which includes meteor launchers) to finish await UniTask.WaitUntil(() => LateInitializerManager.isDoneInitializing, cancellationToken: ct); - WhiteHoleVolume = QSBWorldSync.GetUnityObject(); + // NH can make multiple so ensure its the stock whitehole + var whiteHole = QSBWorldSync.GetUnityObjects().First(x => x.GetAstroObjectName() == AstroObject.Name.WhiteHole); + WhiteHoleVolume = whiteHole?.GetComponentInChildren(); QSBWorldSync.Init(); QSBWorldSync.Init(); QSBWorldSync.Init(); diff --git a/QSB/OrbSync/WorldObjects/QSBOrb.cs b/QSB/OrbSync/WorldObjects/QSBOrb.cs index d224e417c..fa8014061 100644 --- a/QSB/OrbSync/WorldObjects/QSBOrb.cs +++ b/QSB/OrbSync/WorldObjects/QSBOrb.cs @@ -1,5 +1,5 @@ -using QSB.OrbSync.TransformSync; -using QSB.Utility; +using OWML.Utils; +using QSB.OrbSync.TransformSync; using QSB.Utility.LinkedWorldObject; using UnityEngine; diff --git a/QSB/OwnershipSync/IOwnedWorldObject_Extensions.cs b/QSB/OwnershipSync/IOwnedWorldObject_Extensions.cs index 56ca22209..c537bc38d 100644 --- a/QSB/OwnershipSync/IOwnedWorldObject_Extensions.cs +++ b/QSB/OwnershipSync/IOwnedWorldObject_Extensions.cs @@ -7,9 +7,15 @@ public static class IOwnedWorldObject_Extensions { /// /// try and gain ownership over the object + /// + /// does nothing if we cant own this object or there is already another owner /// public static void RequestOwnership(this IOwnedWorldObject @this) { + if (!@this.CanOwn) + { + return; + } if (@this.Owner != 0) { return; @@ -17,9 +23,19 @@ public static void RequestOwnership(this IOwnedWorldObject @this) @this.SendMessage(new OwnedWorldObjectMessage(QSBPlayerManager.LocalPlayerId)); } + /// + /// forcibly gain ownership over the object + /// + public static void ForceOwnership(this IOwnedWorldObject @this) + { + @this.SendMessage(new OwnedWorldObjectMessage(QSBPlayerManager.LocalPlayerId)); + } + /// /// release ownership over the object, /// potentially to giving it to someone else + /// + /// does nothing if someone else already owns this /// public static void ReleaseOwnership(this IOwnedWorldObject @this) { diff --git a/QSB/OwnershipSync/OwnedWorldObject.cs b/QSB/OwnershipSync/OwnedWorldObject.cs index e861ea425..085160070 100644 --- a/QSB/OwnershipSync/OwnedWorldObject.cs +++ b/QSB/OwnershipSync/OwnedWorldObject.cs @@ -30,6 +30,7 @@ private void OnPlayerLeave(PlayerInfo player) } if (Owner == player.PlayerId) { + // put CanOwn check here cuz it only does the thingy in OnReceiveRemote and we want to be able to own this ((IOwnedWorldObject)this).SendMessage(new OwnedWorldObjectMessage(CanOwn ? QSBPlayerManager.LocalPlayerId : 0)); } } diff --git a/QSB/OwnershipSync/OwnedWorldObjectMessage.cs b/QSB/OwnershipSync/OwnedWorldObjectMessage.cs index 85b87d365..d30b0f605 100644 --- a/QSB/OwnershipSync/OwnedWorldObjectMessage.cs +++ b/QSB/OwnershipSync/OwnedWorldObjectMessage.cs @@ -4,33 +4,13 @@ namespace QSB.OwnershipSync; /// -/// request or release ownership of a world object +/// sets the owner of a world object +/// also potentially gives ownership to someone else if possible /// public class OwnedWorldObjectMessage : QSBWorldObjectMessage { public OwnedWorldObjectMessage(uint owner) : base(owner) { } - public override bool ShouldReceive - { - get - { - if (!base.ShouldReceive) - { - return false; - } - - // Deciding if to change the object's owner - // Message - // | = 0 | > 0 | - // = 0 | No | Yes | - // > 0 | Yes | No | - // if Obj==Message then No - // Obj - - return (WorldObject.Owner == 0 || Data == 0) && WorldObject.Owner != Data; - } - } - public override void OnReceiveLocal() => WorldObject.Owner = Data; public override void OnReceiveRemote() @@ -39,6 +19,7 @@ public override void OnReceiveRemote() if (WorldObject.Owner == 0 && WorldObject.CanOwn) { // object has no owner, but is still active for this player. request ownership + // means you should wait and check after releasing ownership in case someone else gets it WorldObject.SendMessage(new OwnedWorldObjectMessage(QSBPlayerManager.LocalPlayerId)); } } diff --git a/QSB/OwnershipSync/OwnershipManager.cs b/QSB/OwnershipSync/OwnershipManager.cs index 2cb52de4d..ef0a5c28d 100644 --- a/QSB/OwnershipSync/OwnershipManager.cs +++ b/QSB/OwnershipSync/OwnershipManager.cs @@ -81,11 +81,6 @@ public static void SetOwner(this NetworkIdentity identity, uint id) public static void UpdateOwnerQueue(this NetworkIdentity identity, OwnerQueueAction action) { - if (action == OwnerQueueAction.Force && identity.isOwned) - { - return; - } - new OwnerQueueMessage(identity.netId, action).Send(); } diff --git a/QSB/Patches/QSBPatch.cs b/QSB/Patches/QSBPatch.cs index 618ba1ebe..90dba5cad 100644 --- a/QSB/Patches/QSBPatch.cs +++ b/QSB/Patches/QSBPatch.cs @@ -11,7 +11,7 @@ public abstract class QSBPatch public void DoPatches(Harmony instance) => instance.PatchAll(GetType()); /// - /// this is true when a message is received remotely (OnReceiveRemote) or a player leaves (OnRemovePlayer) + /// this is true when a message is received remotely (OnReceiveRemote) or a remote player joins/leaves (OnAddPlayer/OnRemovePlayer) /// public static bool Remote; } diff --git a/QSB/Patches/QSBPatchManager.cs b/QSB/Patches/QSBPatchManager.cs index 969876bdf..721475cc6 100644 --- a/QSB/Patches/QSBPatchManager.cs +++ b/QSB/Patches/QSBPatchManager.cs @@ -1,5 +1,6 @@ using HarmonyLib; using OWML.Common; +using OWML.Utils; using QSB.Utility; using System; using System.Collections.Generic; @@ -87,10 +88,10 @@ public static void DoPatchType(QSBPatchTypes type) } OnPatchType?.SafeInvoke(type); - DebugLog.DebugWrite($"Patch block {Enum.GetName(typeof(QSBPatchTypes), type)}", MessageType.Info); + //DebugLog.DebugWrite($"Patch block {Enum.GetName(typeof(QSBPatchTypes), type)}", MessageType.Info); foreach (var patch in _patchList.Where(x => x.Type == type && x.PatchVendor.HasFlag(QSBCore.GameVendor))) { - DebugLog.DebugWrite($" - Patching in {patch.GetType().Name}", MessageType.Info); + //DebugLog.DebugWrite($" - Patching in {patch.GetType().Name}", MessageType.Info); try { patch.DoPatches(TypeToInstance[type]); diff --git a/QSB/Player/Messages/PlayerInformationMessage.cs b/QSB/Player/Messages/PlayerInformationMessage.cs index fd8508dc1..ff60426e2 100644 --- a/QSB/Player/Messages/PlayerInformationMessage.cs +++ b/QSB/Player/Messages/PlayerInformationMessage.cs @@ -13,6 +13,7 @@ public class PlayerInformationMessage : QSBMessage private bool IsReady; private bool FlashlightActive; private bool SuitedUp; + private bool HelmetOn; private bool LocalProbeLauncherEquipped; private bool SignalscopeEquipped; private bool TranslatorEquipped; @@ -21,6 +22,8 @@ public class PlayerInformationMessage : QSBMessage private float FieldOfView; private bool IsInShip; private HUDIcon HUDIcon; + private string SkinType; + private string JetpackType; public PlayerInformationMessage() { @@ -29,6 +32,7 @@ public PlayerInformationMessage() IsReady = player.IsReady; FlashlightActive = player.FlashlightActive; SuitedUp = player.SuitedUp; + HelmetOn = Locator.GetPlayerSuit() != null && Locator.GetPlayerSuit().IsWearingHelmet(); LocalProbeLauncherEquipped = player.LocalProbeLauncherEquipped; SignalscopeEquipped = player.SignalscopeEquipped; TranslatorEquipped = player.TranslatorEquipped; @@ -37,6 +41,8 @@ public PlayerInformationMessage() FieldOfView = PlayerData.GetGraphicSettings().fieldOfView; IsInShip = player.IsInShip; HUDIcon = player.HUDBox == null ? HUDIcon.UNKNOWN : player.HUDBox.PlanetIcon; + SkinType = QSBCore.SkinVariation; + JetpackType = QSBCore.JetpackVariation; } public override void Serialize(NetworkWriter writer) @@ -46,6 +52,7 @@ public override void Serialize(NetworkWriter writer) writer.Write(IsReady); writer.Write(FlashlightActive); writer.Write(SuitedUp); + writer.Write(HelmetOn); writer.Write(LocalProbeLauncherEquipped); writer.Write(SignalscopeEquipped); writer.Write(TranslatorEquipped); @@ -54,6 +61,8 @@ public override void Serialize(NetworkWriter writer) writer.Write(FieldOfView); writer.Write(IsInShip); writer.Write(HUDIcon); + writer.Write(SkinType); + writer.Write(JetpackType); } public override void Deserialize(NetworkReader reader) @@ -63,6 +72,7 @@ public override void Deserialize(NetworkReader reader) IsReady = reader.Read(); FlashlightActive = reader.Read(); SuitedUp = reader.Read(); + HelmetOn = reader.Read(); LocalProbeLauncherEquipped = reader.Read(); SignalscopeEquipped = reader.Read(); TranslatorEquipped = reader.Read(); @@ -71,6 +81,8 @@ public override void Deserialize(NetworkReader reader) FieldOfView = reader.ReadFloat(); IsInShip = reader.ReadBool(); HUDIcon = reader.Read(); + SkinType = reader.ReadString(); + JetpackType = reader.ReadString(); } public override void OnReceiveRemote() @@ -83,19 +95,25 @@ public override void OnReceiveRemote() player.IsReady = IsReady; player.FlashlightActive = FlashlightActive; player.SuitedUp = SuitedUp; + player.LocalProbeLauncherEquipped = LocalProbeLauncherEquipped; player.SignalscopeEquipped = SignalscopeEquipped; player.TranslatorEquipped = TranslatorEquipped; player.ProbeActive = ProbeActive; player.IsInShip = IsInShip; - if (QSBPlayerManager.LocalPlayer.IsReady && player.IsReady) + + Delay.RunWhen(() => player.IsReady && QSBPlayerManager.LocalPlayer.IsReady, () => { player.UpdateObjectsFromStates(); - } + player.HelmetAnimator.SetHelmetInstant(HelmetOn); + player.Camera.fieldOfView = FieldOfView; + }); - Delay.RunWhen( - () => player.Camera != null, - () => player.Camera.fieldOfView = FieldOfView); + Delay.RunWhen(() => player.Body != null, () => + { + var REMOTE_Traveller_HEA_Player_v2 = player.Body.transform.Find("REMOTE_Traveller_HEA_Player_v2"); + BodyCustomization.BodyCustomizer.Instance.CustomizeRemoteBody(REMOTE_Traveller_HEA_Player_v2.gameObject, player.HelmetAnimator.FakeHead.gameObject, SkinType, JetpackType); + }); player.State = ClientState; diff --git a/QSB/Player/Messages/PlayerJoinMessage.cs b/QSB/Player/Messages/PlayerJoinMessage.cs index fd6e99569..409e3ae3e 100644 --- a/QSB/Player/Messages/PlayerJoinMessage.cs +++ b/QSB/Player/Messages/PlayerJoinMessage.cs @@ -16,8 +16,6 @@ public class PlayerJoinMessage : QSBMessage private string QSBVersion; private string GameVersion; private bool DlcInstalled; - // empty if no incompatible mods - private string FirstIncompatibleMod; private int[] AddonHashes; @@ -28,18 +26,6 @@ public PlayerJoinMessage(string name) GameVersion = QSBCore.GameVersion; DlcInstalled = QSBCore.DLCInstalled; - var allEnabledMods = QSBCore.Helper.Interaction.GetMods(); - - FirstIncompatibleMod = ""; - - foreach (var mod in allEnabledMods) - { - if (QSBCore.IncompatibleMods.Contains(mod.ModHelper.Manifest.UniqueName)) - { - FirstIncompatibleMod = mod.ModHelper.Manifest.UniqueName; - } - } - AddonHashes = QSBCore.Addons.Keys .Except(QSBCore.CosmeticAddons) .Select(x => x.GetStableHashCode()) @@ -53,7 +39,6 @@ public override void Serialize(NetworkWriter writer) writer.Write(QSBVersion); writer.Write(GameVersion); writer.Write(DlcInstalled); - writer.Write(FirstIncompatibleMod); writer.Write(AddonHashes); } @@ -65,7 +50,6 @@ public override void Deserialize(NetworkReader reader) QSBVersion = reader.ReadString(); GameVersion = reader.ReadString(); DlcInstalled = reader.Read(); - FirstIncompatibleMod = reader.ReadString(); AddonHashes = reader.Read(); } @@ -119,12 +103,6 @@ public override void OnReceiveRemote() new PlayerKickMessage(From, string.Format(QSBLocalization.Current.AddonMismatch, AddonHashes.Length, addonHashes.Length)).Send(); return; } - - if (FirstIncompatibleMod != "" && !QSBCore.IncompatibleModsAllowed) - { - DebugLog.ToConsole($"Error - Client {PlayerName} connecting with incompatible mod. (First mod found was {FirstIncompatibleMod})"); - new PlayerKickMessage(From, string.Format(QSBLocalization.Current.IncompatibleMod, FirstIncompatibleMod)).Send(); - } } var player = QSBPlayerManager.GetPlayer(From); diff --git a/QSB/Player/Messages/RequestStateResyncMessage.cs b/QSB/Player/Messages/RequestStateResyncMessage.cs index 68f747bf9..7b40cf265 100644 --- a/QSB/Player/Messages/RequestStateResyncMessage.cs +++ b/QSB/Player/Messages/RequestStateResyncMessage.cs @@ -1,4 +1,5 @@ using OWML.Common; +using QSB.API.Messages; using QSB.ClientServerStateSync; using QSB.ClientServerStateSync.Messages; using QSB.Messaging; @@ -50,5 +51,16 @@ public override void OnReceiveRemote() } new PlayerInformationMessage { To = From }.Send(); + + // Initial sync of all custom data from APIs + foreach (var kvp in QSBPlayerManager.LocalPlayer._customData) + { + if (!kvp.Value.GetType().IsSerializable) + { + continue; + } + + new AddonCustomDataSyncMessage(QSBPlayerManager.LocalPlayerId, kvp.Key, kvp.Value) { To = From }.Send(); + } } } \ No newline at end of file diff --git a/QSB/Player/Patches/VolumePatches.cs b/QSB/Player/Patches/VolumePatches.cs index e5f15f30c..e13aaa295 100644 --- a/QSB/Player/Patches/VolumePatches.cs +++ b/QSB/Player/Patches/VolumePatches.cs @@ -1,6 +1,5 @@ using HarmonyLib; using QSB.Patches; -using QSB.Utility; using UnityEngine; namespace QSB.Player.Patches; @@ -46,19 +45,12 @@ public static void OnEffectVolumeEnter(RingRiverFluidVolume __instance, GameObje [HarmonyPatch(typeof(ElectricityVolume), nameof(ElectricityVolume.OnEffectVolumeEnter))] [HarmonyPatch(typeof(DreamWarpVolume), nameof(DreamWarpVolume.OnEnterTriggerVolume))] [HarmonyPatch(typeof(NomaiWarpPlatform), nameof(NomaiWarpPlatform.OnEntry))] + [HarmonyPatch(typeof(NomaiWarpReceiver), nameof(NomaiWarpReceiver.OnEntry))] public static bool PreventRemotePlayerEnter(object __instance, GameObject hitObj) - { - DebugLog.DebugWrite($"{__instance} funny prevent enter {hitObj}"); - // TODO: also do this with remote probes - return hitObj.name is not ("REMOTE_PlayerDetector" or "REMOTE_CameraDetector"); - } + => hitObj.name is not ("REMOTE_PlayerDetector" or "REMOTE_CameraDetector" or "REMOTE_ProbeDetector"); [HarmonyPrefix] [HarmonyPatch(typeof(VanishVolume), nameof(VanishVolume.OnTriggerEnter))] public static bool PreventRemotePlayerEnter(object __instance, Collider hitCollider) - { - DebugLog.DebugWrite($"{__instance} funny prevent enter {hitCollider}"); - // TODO: also do this with remote probes - return hitCollider.name is not ("REMOTE_PlayerDetector" or "REMOTE_CameraDetector"); - } + => hitCollider.name is not ("REMOTE_PlayerDetector" or "REMOTE_CameraDetector" or "REMOTE_ProbeDetector"); } diff --git a/QSB/Player/PlayerInfo.cs b/QSB/Player/PlayerInfo.cs index 12e22f171..202491c0e 100644 --- a/QSB/Player/PlayerInfo.cs +++ b/QSB/Player/PlayerInfo.cs @@ -1,10 +1,12 @@ using OWML.Common; using QSB.Animation.Player; +using QSB.API.Messages; using QSB.Audio; using QSB.ClientServerStateSync; using QSB.HUD; using QSB.Messaging; using QSB.ModelShip; +using QSB.Patches; using QSB.Player.Messages; using QSB.Player.TransformSync; using QSB.QuantumSync.WorldObjects; @@ -179,10 +181,18 @@ public void Revive() HUDBox.OnRespawn(); } - private Dictionary _customData = new(); + // internal for RequestStateResyncMessage + internal readonly Dictionary _customData = new(); public void SetCustomData(string key, T data) - => _customData[key] = data; + { + _customData[key] = data; + + if (!QSBPatch.Remote && typeof(T).IsSerializable) + { + new AddonCustomDataSyncMessage(PlayerId, key, data).Send(); + } + } public T GetCustomData(string key) { diff --git a/QSB/Player/PlayerInfoParts/Animation.cs b/QSB/Player/PlayerInfoParts/Animation.cs index 30c43023c..e7c341455 100644 --- a/QSB/Player/PlayerInfoParts/Animation.cs +++ b/QSB/Player/PlayerInfoParts/Animation.cs @@ -12,4 +12,5 @@ public partial class PlayerInfo internal QSBDitheringAnimator _ditheringAnimator; public DreamWorldSpawnAnimator DreamWorldSpawnAnimator { get; set; } public RemotePlayerFluidDetector FluidDetector { get; set; } + public HelmetAnimator HelmetAnimator { get; set; } } diff --git a/QSB/Player/PlayerInfoParts/Body.cs b/QSB/Player/PlayerInfoParts/Body.cs index f2f3702e9..4edc9a315 100644 --- a/QSB/Player/PlayerInfoParts/Body.cs +++ b/QSB/Player/PlayerInfoParts/Body.cs @@ -23,28 +23,7 @@ public OWCamera Camera public GameObject CameraBody { get; set; } - public GameObject Body - { - get - { - if (_body == null && IsReady) - { - DebugLog.ToConsole($"Warning - {PlayerId}.Body is null!", MessageType.Warning); - } - - return _body; - } - set - { - if (value == null) - { - DebugLog.ToConsole($"Warning - Setting {PlayerId}.Body to null.", MessageType.Warning); - } - - _body = value; - } - } - private GameObject _body; + public GameObject Body { get; set; } /// /// remote light sensor is disabled. diff --git a/QSB/Player/TransformSync/PlayerTransformSync.cs b/QSB/Player/TransformSync/PlayerTransformSync.cs index 4cadce5be..27f79e290 100644 --- a/QSB/Player/TransformSync/PlayerTransformSync.cs +++ b/QSB/Player/TransformSync/PlayerTransformSync.cs @@ -1,4 +1,5 @@ using OWML.Common; +using OWML.Utils; using QSB.Messaging; using QSB.Patches; using QSB.Player.Messages; @@ -35,8 +36,16 @@ public override void OnStartClient() { var player = new PlayerInfo(this); QSBPlayerManager.PlayerList.SafeAdd(player); + + if (isLocalPlayer) + { + LocalInstance = this; + } + base.OnStartClient(); + QSBPatch.Remote = !isLocalPlayer; QSBPlayerManager.OnAddPlayer?.SafeInvoke(Player); + QSBPatch.Remote = false; DebugLog.DebugWrite($"Create Player : {Player}", MessageType.Info); JoinLeaveSingularity.Create(Player, true); @@ -49,7 +58,7 @@ public override void OnStopClient() JoinLeaveSingularity.Create(Player, false); // TODO : Maybe move this to a leave event...? Would ensure everything could finish up before removing the player - QSBPatch.Remote = true; + QSBPatch.Remote = !isLocalPlayer; QSBPlayerManager.OnRemovePlayer?.SafeInvoke(Player); QSBPatch.Remote = false; base.OnStopClient(); diff --git a/QSB/PlayerBodySetup/Remote/QSBDitheringAnimator.cs b/QSB/PlayerBodySetup/Remote/QSBDitheringAnimator.cs index 6889e7fad..40933fdad 100644 --- a/QSB/PlayerBodySetup/Remote/QSBDitheringAnimator.cs +++ b/QSB/PlayerBodySetup/Remote/QSBDitheringAnimator.cs @@ -59,6 +59,10 @@ private void Update() private void UpdateRenderers() { + _renderers ??= GetComponentsInChildren(true) + .Select(x => (x.gameObject.GetAddComponent(), x.shadowCastingMode != ShadowCastingMode.Off)) + .ToArray(); + foreach (var (renderer, updateShadows) in _renderers) { if (renderer == null) diff --git a/QSB/PlayerBodySetup/Remote/RemotePlayerCreation.cs b/QSB/PlayerBodySetup/Remote/RemotePlayerCreation.cs index 5ddaedbda..b460c303d 100644 --- a/QSB/PlayerBodySetup/Remote/RemotePlayerCreation.cs +++ b/QSB/PlayerBodySetup/Remote/RemotePlayerCreation.cs @@ -1,4 +1,5 @@ -using QSB.Audio; +using QSB.Animation.Player; +using QSB.Audio; using QSB.EchoesOfTheEye.LightSensorSync; using QSB.Player; using QSB.RoastingSync; @@ -54,6 +55,7 @@ public static Transform CreatePlayer( player.ThrusterLightTracker = player.Body.GetComponentInChildren(); player.FluidDetector = REMOTE_PlayerDetector.GetComponent(); player.RulesetDetector = REMOTE_PlayerDetector.GetComponent(); + player.HelmetAnimator = REMOTE_Traveller_HEA_Player_v2.GetComponent(); player.AnimationSync.InitRemote(REMOTE_Traveller_HEA_Player_v2.transform); diff --git a/QSB/PoolSync/PoolManager.cs b/QSB/PoolSync/PoolManager.cs index 39a48f586..7b5c1c7ee 100644 --- a/QSB/PoolSync/PoolManager.cs +++ b/QSB/PoolSync/PoolManager.cs @@ -1,5 +1,5 @@ using Cysharp.Threading.Tasks; -using QSB.Utility; +using QSB.Utility.Deterministic; using QSB.WorldSync; using System.Threading; diff --git a/QSB/QSB.csproj b/QSB/QSB.csproj index 4065ce97d..c2fa55a6a 100644 --- a/QSB/QSB.csproj +++ b/QSB/QSB.csproj @@ -2,7 +2,13 @@ Quantum Space Buddies Quantum Space Buddies + Quantum Space Buddies Multiplayer mod for Outer Wilds + Henry Pointer, William Corby, Aleksander Waage, Ricardo Lopes + Henry Pointer, William Corby, Aleksander Waage, Ricardo Lopes + Copyright © Henry Pointer, William Corby, Aleksander Waage, Ricardo Lopes 2020-2024 + LICENSE + README.md $(OwmlDir)\Mods\Raicuparta.QuantumSpaceBuddies CS1998;CS0649 @@ -21,17 +27,12 @@ $(UnityAssetsDir)\Dlls - LICENSE - Quantum Space Buddies - Henry Pointer, William Corby, Aleksander Waage, Ricardo Lopes - Copyright © Henry Pointer, William Corby, Aleksander Waage, Ricardo Lopes 2020-2023 - README.md <_Files Remove="@(_Files)" /> - <_Files Include="$(OutputPath)\*.dll" /> - <_Files Include="$(OutputPath)\*.exe" /> + <_Files Include="$(OutputPath)/*.dll" /> + <_Files Include="$(OutputPath)/*.exe" /> @@ -45,6 +46,8 @@ True \ + + PreserveNewest @@ -66,14 +69,17 @@ PreserveNewest + + PreserveNewest + - - - - - - + + + + + + diff --git a/QSB/QSB.csproj.DotSettings b/QSB/QSB.csproj.DotSettings new file mode 100644 index 000000000..b5b235a96 --- /dev/null +++ b/QSB/QSB.csproj.DotSettings @@ -0,0 +1,2 @@ + + Default \ No newline at end of file diff --git a/QSB/QSBCore.cs b/QSB/QSBCore.cs index b2bb53c18..b3335280f 100644 --- a/QSB/QSBCore.cs +++ b/QSB/QSBCore.cs @@ -11,17 +11,24 @@ using QSB.ServerSettings; using QSB.Utility; using QSB.WorldSync; +using Steamworks; using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; +using Newtonsoft.Json.Linq; using QSB.API; +using QSB.BodyCustomization; +using QSB.Player.Messages; using UnityEngine; using UnityEngine.InputSystem; +using Random = System.Random; +using QSB.Utility.Deterministic; /* - Copyright (C) 2020 - 2023 + Copyright (C) 2020 - 2024 Henry Pointer (_nebula / misternebula), Will Corby (JohnCorby), Aleksander Waage (AmazingAlek), @@ -50,7 +57,6 @@ public class QSBCore : ModBehaviour public static string DefaultServerIP; public static AssetBundle NetworkAssetBundle { get; private set; } public static AssetBundle ConversationAssetBundle { get; private set; } - public static AssetBundle DebugAssetBundle { get; private set; } public static AssetBundle HUDAssetBundle { get; private set; } public static bool IsHost => (MenuManager.Instance != null && MenuManager.Instance.WillBeHost) || NetworkServer.active; public static bool IsInMultiplayer; @@ -60,11 +66,12 @@ public class QSBCore : ModBehaviour 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 TextChatInput { get; private set; } + public static string SkinVariation { get; private set; } = "Default"; + public static string JetpackVariation { get; private set; } = "Orange"; 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 @@ -73,19 +80,12 @@ public class QSBCore : ModBehaviour public static IMenuAPI MenuApi { get; private set; } public static DebugSettings DebugSettings { get; private set; } = new(); - public const string NEW_HORIZONS = "xen.NewHorizons"; - public const string NEW_HORIZONS_COMPAT = "xen.NHQSBCompat"; + private static string randomSkinType; + private static string randomJetpackType; - public static readonly string[] IncompatibleMods = - { - // incompatible mods - "Raicuparta.NomaiVR", - "xen.NewHorizons", - "Vesper.AutoResume", - "Vesper.OuterWildsMMO", - "_nebula.StopTime", - "PacificEngine.OW_Randomizer", - }; + public static Assembly QSBNHAssembly = null; + + public static event Action OnSkinsBundleLoaded; public override object GetApi() => new QSBAPI(); @@ -114,13 +114,13 @@ private static void DetermineGameVendor() DebugLog.ToConsole($"FATAL - Could not determine game vendor.", MessageType.Fatal); } - DebugLog.DebugWrite($"Determined game vendor as {GameVendor}", MessageType.Info); + DebugLog.ToConsole($"Determined game vendor as {GameVendor}", MessageType.Info); } + private bool _steamworksInitialized; + public void Awake() { - EpicRerouter.ModSide.Interop.Go(); - // no, we cant localize this - languages are loaded after the splash screen UIHelper.ReplaceUI(UITextType.PleaseUseController, "Quantum Space Buddies is best experienced with friends..."); @@ -129,6 +129,86 @@ public void Awake() QSBPatchManager.Init(); QSBPatchManager.DoPatchType(QSBPatchTypes.OnModStart); + + if (GameVendor != GameVendor.Steam) + { + DebugLog.DebugWrite($"Not steam, initializing Steamworks..."); + + if (!Packsize.Test()) + { + DebugLog.ToConsole("[Steamworks.NET] Packsize Test returned false, the wrong version of Steamworks.NET is being run in this platform.", MessageType.Error); + } + + if (!DllCheck.Test()) + { + DebugLog.ToConsole("[Steamworks.NET] DllCheck Test returned false, One or more of the Steamworks binaries seems to be the wrong version.", MessageType.Error); + } + + // from facepunch.steamworks SteamClient.cs + // Normally, Steam sets these env vars when launching the game through the Steam library. + // These would also be set when running the .exe directly, thanks to Steam's "DRM" in the exe. + // We're setting these manually to 480 - an AppID that every Steam account owns by default. + // This tells Steam and Steamworks that the user is playing a game they own. + // This lets anyone use Steamworks, even if they don't own Outer Wilds. + // We also don't have to worry about Steam achievements or DLC in this case. + Environment.SetEnvironmentVariable("SteamAppId", "480"); + Environment.SetEnvironmentVariable("SteamGameId", "480"); + + if (!SteamAPI.Init()) + { + DebugLog.ToConsole($"FATAL - SteamAPI.Init() failed. Do you have Steam open, and are you logged in?", MessageType.Fatal); + return; + } + + _steamworksInitialized = true; + } + else + { + SteamRerouter.ModSide.Interop.Init(); + + DebugLog.DebugWrite($"Is steam - overriding AppID"); + OverrideAppId(); + } + } + + public void OverrideAppId() + { + // Normally, Steam sets env vars when launching the game through the Steam library. + // These would also be set when running the .exe directly, thanks to Steam's "DRM" in the exe. + // However, for Steam players to be able to join non-Steam players, everyone has to be using Steamworks with the same AppID. + // At this point, OW has already initialized Steamworks. + // Since we handle achievements and DLC ownership in the rerouter, we need to re-initialize Steamworks with the new AppID. + + // (Also, Mobius forgor to change some default Steamworks code, so sometimes these env vars aren't set at all. + // In this instance the overlay and achievements also don't work, but we can't fix that here.) + + // reset steamworks instance + SteamManager.s_EverInitialized = false; + var instance = SteamManager.s_instance; + instance.m_bInitialized = false; + SteamManager.s_instance = null; + + // Releases pointers and frees memory used by Steam to manage the current game. + // Does not unhook the overlay, so we dont have to worry about that :peepoHappy: + SteamAPI.Shutdown(); + + // Set the env vars to an AppID that everyone owns by default. + // from facepunch.steamworks SteamClient.cs + Environment.SetEnvironmentVariable("SteamAppId", "480"); + Environment.SetEnvironmentVariable("SteamGameId", "480"); + + // Re-initialize Steamworks. + instance.InitializeOnAwake(); + + // TODO also reregister hook and gamepad thing or else i think that wont work + } + + public void OnDestroy() + { + if (_steamworksInitialized) + { + SteamAPI.Shutdown(); + } } public void Start() @@ -136,7 +216,7 @@ public void Start() Helper = ModHelper; DebugLog.ToConsole($"* Start of QSB version {QSBVersion} - authored by {Helper.Manifest.Author}", MessageType.Info); - CheckCompatibilityMods(); + CheckNewHorizons(); DebugSettings = Helper.Storage.Load("debugsettings.json") ?? new DebugSettings(); @@ -173,17 +253,14 @@ public void Start() MenuApi = ModHelper.Interaction.TryGetModApi(ModHelper.Manifest.Dependencies[0]); - DebugLog.DebugWrite("loading qsb_network_big bundle", MessageType.Info); - var path = Path.Combine(ModHelper.Manifest.ModFolderPath, "AssetBundles/qsb_network_big"); - var request = AssetBundle.LoadFromFileAsync(path); - request.completed += _ => DebugLog.DebugWrite("qsb_network_big bundle loaded", MessageType.Success); + LoadBundleAsync("qsb_network_big"); + LoadBundleAsync("qsb_skins", request => BodyCustomizer.Instance.OnBundleLoaded(request.assetBundle)); - NetworkAssetBundle = Helper.Assets.LoadBundle("AssetBundles/qsb_network"); - ConversationAssetBundle = Helper.Assets.LoadBundle("AssetBundles/qsb_conversation"); - DebugAssetBundle = Helper.Assets.LoadBundle("AssetBundles/qsb_debug"); - HUDAssetBundle = Helper.Assets.LoadBundle("AssetBundles/qsb_hud"); + NetworkAssetBundle = LoadBundle("qsb_network"); + ConversationAssetBundle = LoadBundle("qsb_conversation"); + HUDAssetBundle = LoadBundle("qsb_hud"); - if (NetworkAssetBundle == null || ConversationAssetBundle == null || DebugAssetBundle == null) + if (NetworkAssetBundle == null || ConversationAssetBundle == null || HUDAssetBundle == null) { DebugLog.ToConsole($"FATAL - An assetbundle is missing! Re-install mod or contact devs.", MessageType.Fatal); return; @@ -199,6 +276,44 @@ public void Start() QSBWorldSync.Managers = components.OfType().ToArray(); QSBPatchManager.OnPatchType += OnPatchType; QSBPatchManager.OnUnpatchType += OnUnpatchType; + + if (DebugSettings.RandomizeSkins) + { + var skinSetting = (JObject)ModHelper.Config.Settings["skinType"]; + var skinOptions = skinSetting["options"].ToObject(); + randomSkinType = skinOptions[UnityEngine.Random.Range(0, skinOptions.Length - 1)]; + + var jetpackSetting = (JObject)ModHelper.Config.Settings["jetpackType"]; + var jetpackOptions = jetpackSetting["options"].ToObject(); + randomJetpackType = jetpackOptions[UnityEngine.Random.Range(0, jetpackOptions.Length - 1)]; + + Configure(ModHelper.Config); + } + } + + private AssetBundle LoadBundle(string bundleName) + { + var timer = new Stopwatch(); + timer.Start(); + var ret = Helper.Assets.LoadBundle(Path.Combine("AssetBundles", bundleName)); + timer.Stop(); + DebugLog.ToConsole($"Bundle {bundleName} loaded in {timer.ElapsedMilliseconds} ms!", MessageType.Success); + return ret; + } + + private void LoadBundleAsync(string bundleName, Action runOnLoaded = null) + { + DebugLog.DebugWrite($"Loading {bundleName}...", MessageType.Info); + var timer = new Stopwatch(); + timer.Start(); + var path = Path.Combine(ModHelper.Manifest.ModFolderPath, "AssetBundles", bundleName); + var request = AssetBundle.LoadFromFileAsync(path); + request.completed += _ => + { + timer.Stop(); + DebugLog.ToConsole($"Bundle {bundleName} loaded in {timer.ElapsedMilliseconds} ms!", MessageType.Success); + runOnLoaded?.Invoke(request); + }; } private static void OnPatchType(QSBPatchTypes type) @@ -232,7 +347,7 @@ private void RegisterAddons() /// /// Registers an addon that shouldn't be considered for hash checks when joining. - /// This addon MUST NOT send any network messages, or create any worldobjects. + /// This addon MUST NOT create any WorldObjects or NetworkBehaviours. /// /// The behaviour of the addon. public static void RegisterNotRequiredForAllPlayers(IModBehaviour addon) @@ -242,9 +357,11 @@ public static void RegisterNotRequiredForAllPlayers(IModBehaviour addon) foreach (var type in addonAssembly.GetTypes()) { - if (typeof(QSBMessage).IsAssignableFrom(type) || typeof(WorldObjectManager).IsAssignableFrom(type) || typeof(IWorldObject).IsAssignableFrom(type)) + if (typeof(WorldObjectManager).IsAssignableFrom(type) || + typeof(IWorldObject).IsAssignableFrom(type) || + typeof(NetworkBehaviour).IsAssignableFrom(type)) { - DebugLog.ToConsole($"Addon \"{uniqueName}\" cannot be cosmetic, as it creates networking events or objects.", MessageType.Error); + DebugLog.ToConsole($"Addon \"{uniqueName}\" cannot be cosmetic, as it creates networking objects.", MessageType.Error); return; } } @@ -268,6 +385,11 @@ static void Init(Assembly assembly) DebugLog.DebugWrite("Running RuntimeInitializeOnLoad methods for our assemblies", MessageType.Info); foreach (var path in Directory.EnumerateFiles(Helper.Manifest.ModFolderPath, "*.dll")) { + if (Path.GetFileNameWithoutExtension(path) == "QSB-NH") + { + continue; + } + var assembly = Assembly.LoadFile(path); Init(assembly); } @@ -287,17 +409,32 @@ public override void Configure(IModConfig config) QSBNetworkManager.UpdateTransport(); DefaultServerIP = config.GetSettingsValue("defaultServerIP"); - IncompatibleModsAllowed = config.GetSettingsValue("incompatibleModsAllowed"); ShowPlayerNames = config.GetSettingsValue("showPlayerNames"); ShipDamage = config.GetSettingsValue("shipDamage"); ShowExtraHUDElements = config.GetSettingsValue("showExtraHud"); TextChatInput = config.GetSettingsValue("textChatInput"); + if (DebugSettings.RandomizeSkins) + { + SkinVariation = randomSkinType; + JetpackVariation = randomJetpackType; + } + else + { + SkinVariation = config.GetSettingsValue("skinType"); + JetpackVariation = config.GetSettingsValue("jetpackType"); + } + if (IsHost) { ServerSettingsManager.ServerShowPlayerNames = ShowPlayerNames; new ServerSettingsMessage().Send(); } + + if (IsInMultiplayer) + { + new PlayerInformationMessage().Send(); + } } private void Update() @@ -308,30 +445,24 @@ private void Update() GetComponent().enabled = DebugSettings.DebugMode; GetComponent().enabled = DebugSettings.DebugMode; - QuantumManager.UpdateFromDebugSetting(); DebugCameraSettings.UpdateFromDebugSetting(); DebugLog.ToConsole($"DEBUG MODE = {DebugSettings.DebugMode}"); } - } - - private void CheckCompatibilityMods() - { - var mainMod = ""; - var compatMod = ""; - var missingCompat = false; - /*if (Helper.Interaction.ModExists(NEW_HORIZONS) && !Helper.Interaction.ModExists(NEW_HORIZONS_COMPAT)) + if (_steamworksInitialized) { - mainMod = NEW_HORIZONS; - compatMod = NEW_HORIZONS_COMPAT; - missingCompat = true; - }*/ + SteamAPI.RunCallbacks(); + } + } - if (missingCompat) + private void CheckNewHorizons() + { + if (ModHelper.Interaction.ModExists("xen.NewHorizons")) { - DebugLog.ToConsole($"FATAL - You have mod \"{mainMod}\" installed, which is not compatible with QSB without the compatibility mod \"{compatMod}\". " + - $"Either disable the mod, or install/enable the compatibility mod.", MessageType.Fatal); + // NH compat has to be in a different DLL since it uses IAddComponentOnStart, and depends on the NH DLL. + QSBNHAssembly = Assembly.LoadFrom(Path.Combine(ModHelper.Manifest.ModFolderPath, "QSB-NH.dll")); + gameObject.AddComponent(QSBNHAssembly.GetType("QSBNH.QSBNH", true)); } } } diff --git a/QSB/QSBNetworkManager.cs b/QSB/QSBNetworkManager.cs index 6cb7504b2..d295de1f0 100644 --- a/QSB/QSBNetworkManager.cs +++ b/QSB/QSBNetworkManager.cs @@ -1,6 +1,6 @@ using Epic.OnlineServices.Logging; -using EpicTransport; using Mirror; +using Mirror.FizzySteam; using OWML.Common; using OWML.Utils; using QSB.Anglerfish.TransformSync; @@ -36,6 +36,7 @@ using System; using System.Linq; using System.Text.RegularExpressions; +using QSB.API; using UnityEngine; namespace QSB; @@ -67,8 +68,9 @@ public class QSBNetworkManager : NetworkManager, IAddComponentOnStart private (TransportError error, string reason) _lastTransportError = (TransportError.Unexpected, "transport did not give an error. uh oh"); + private static LatencySimulation _latencyTransport; private static kcp2k.KcpTransport _kcpTransport; - private static EosTransport _eosTransport; + private static FizzySteamworks _steamTransport; public override void Awake() { @@ -76,25 +78,25 @@ public override void Awake() { _kcpTransport = gameObject.AddComponent(); + // KCP uses milliseconds + _kcpTransport.Timeout = QSBCore.DebugSettings.Timeout * 1000; } + + { + _steamTransport = gameObject.AddComponent(); + // Steam uses seconds + _steamTransport.Timeout = QSBCore.DebugSettings.Timeout; + } + { - // https://dev.epicgames.com/portal/en-US/qsb/sdk/credentials/qsb - var eosApiKey = ScriptableObject.CreateInstance(); - eosApiKey.epicProductName = "QSB"; - eosApiKey.epicProductVersion = "1.0"; - eosApiKey.epicProductId = "d4623220acb64419921c72047931b165"; - eosApiKey.epicSandboxId = "d9bc4035269747668524931b0840ca29"; - eosApiKey.epicDeploymentId = "1f164829371e4cdcb23efedce98d99ad"; - eosApiKey.epicClientId = "xyza7891TmlpkaiDv6KAnJH0f07aAbTu"; - eosApiKey.epicClientSecret = "ft17miukylHF877istFuhTgq+Kw1le3Pfigvf9Dtu20"; - - var eosSdkComponent = gameObject.AddComponent(); - eosSdkComponent.apiKeys = eosApiKey; - eosSdkComponent.epicLoggerLevel = LogLevel.VeryVerbose; - - _eosTransport = gameObject.AddComponent(); + _latencyTransport = gameObject.AddComponent(); + _latencyTransport.reliableLatency = _latencyTransport.unreliableLatency = QSBCore.DebugSettings.LatencySimulation; + _latencyTransport.wrap = QSBCore.UseKcpTransport ? _kcpTransport : _steamTransport; } - transport = QSBCore.UseKcpTransport ? _kcpTransport : _eosTransport; + + transport = QSBCore.DebugSettings.LatencySimulation > 0 + ? _latencyTransport + : QSBCore.UseKcpTransport ? _kcpTransport : _steamTransport; gameObject.SetActive(true); @@ -163,10 +165,20 @@ public static void UpdateTransport() { return; } + if (singleton != null) { - singleton.transport = Transport.active = QSBCore.UseKcpTransport ? _kcpTransport : _eosTransport; + if (QSBCore.DebugSettings.LatencySimulation > 0) + { + _latencyTransport.wrap = QSBCore.UseKcpTransport ? _kcpTransport : _steamTransport; + singleton.transport = Transport.active = _latencyTransport; + } + else + { + singleton.transport = Transport.active = QSBCore.UseKcpTransport ? _kcpTransport : _steamTransport; + } } + if (MenuManager.Instance != null) { MenuManager.Instance.OnLanguageChanged(); // hack to update text @@ -201,10 +213,6 @@ private void InitPlayerName() => DebugLog.ToConsole($"Error - Exception when getting player name : {ex}", MessageType.Error); PlayerName = "Player"; } - - { - EOSSDKComponent.DisplayName = PlayerName; - } }); private static uint _assetId = 2; // 1 is the player @@ -246,7 +254,7 @@ private void ConfigureNetworkManager() // hack if (s == "KcpPeer: received disconnect message") { - OnClientError(TransportError.ConnectionClosed, "host disconnected"); + OnClientError(TransportError.ConnectionClosed, s); } }; kcp2k.Log.Warning = s => DebugLog.DebugWrite("[KCP] " + s, MessageType.Warning); diff --git a/QSB/QSBSceneManager.cs b/QSB/QSBSceneManager.cs index 7f0f1be7d..cc29a0f89 100644 --- a/QSB/QSBSceneManager.cs +++ b/QSB/QSBSceneManager.cs @@ -1,4 +1,5 @@ using OWML.Common; +using OWML.Utils; using QSB.Utility; using System; diff --git a/QSB/QuantumSync/Patches/Common/QuantumMoonPatches.cs b/QSB/QuantumSync/Patches/Common/QuantumMoonPatches.cs index 55591c853..5f7befee0 100644 --- a/QSB/QuantumSync/Patches/Common/QuantumMoonPatches.cs +++ b/QSB/QuantumSync/Patches/Common/QuantumMoonPatches.cs @@ -73,7 +73,16 @@ public static bool CheckPlayerFogProximity(QuantumMoon __instance) } else { - var shouldStayAtEye = __instance._hasSunCollapsed || __instance.IsLockedByProbeSnapshot(); + var anyoneInMoon = QSBPlayerManager.PlayerList.Any(x => x.IsInMoon && !x.IsLocalPlayer); + var playersWhoCanSeeMoon = QuantumManager.IsVisibleUsingCameraFrustum((ShapeVisibilityTracker)__instance._visibilityTracker, true).PlayersWhoCanSee; + var shipInFog = GetShipInFog(__instance); + + var shouldStayAtEye = + __instance._hasSunCollapsed || + __instance.IsLockedByProbeSnapshot() || + anyoneInMoon || + playersWhoCanSeeMoon.Any(x => !(shipInFog && x.IsInShip) && !GetTransformInFog(__instance, x.CameraBody.transform)); + var vector = Locator.GetPlayerTransform().position - __instance.transform.position; Locator.GetPlayerBody().SetVelocity(__instance._moonBody.GetPointVelocity(Locator.GetPlayerTransform().position) - vector.normalized * 5f); var offset = shouldStayAtEye ? 80f : __instance._fogRadius - 1f; diff --git a/QSB/QuantumSync/Patches/Common/QuantumObjectPatches.cs b/QSB/QuantumSync/Patches/Common/QuantumObjectPatches.cs index 4b41a13f0..9026c11e2 100644 --- a/QSB/QuantumSync/Patches/Common/QuantumObjectPatches.cs +++ b/QSB/QuantumSync/Patches/Common/QuantumObjectPatches.cs @@ -18,7 +18,7 @@ public class QuantumObjectPatches : QSBPatch public static bool IsLockedByPlayerContact(out bool __result, QuantumObject __instance) { var playersEntangled = QuantumManager.GetEntangledPlayers(__instance); - __result = playersEntangled.Count() != 0 && __instance.IsIlluminated(); + __result = playersEntangled.Count() != 0 && (__instance.IsIlluminated() || playersEntangled.Any(x => x.FlashlightActive)); return false; } @@ -28,6 +28,8 @@ public static void SetIsQuantum(QuantumObject __instance) { if (QSBWorldSync.AllObjectsReady) { + // non-owners should still be able to set it quantum + // ie the eye reibeck building if a non owner walks over to it __instance.GetWorldObject().SendMessage(new SetIsQuantumMessage(__instance.IsQuantum())); } } diff --git a/QSB/QuantumSync/Patches/Common/SocketedQuantumObjectPatches.cs b/QSB/QuantumSync/Patches/Common/SocketedQuantumObjectPatches.cs index 687d9afc0..ac71f9409 100644 --- a/QSB/QuantumSync/Patches/Common/SocketedQuantumObjectPatches.cs +++ b/QSB/QuantumSync/Patches/Common/SocketedQuantumObjectPatches.cs @@ -18,6 +18,11 @@ public class SocketedQuantumObjectPatches : QSBPatch { public override QSBPatchTypes Type => QSBPatchTypes.OnClientConnect; + /// + /// we dont want to send messages when testing sockets + /// + public static bool TestSocketMove = false; + [HarmonyPrefix] [HarmonyPatch(nameof(SocketedQuantumObject.ChangeQuantumState))] public static bool ChangeQuantumState( @@ -33,6 +38,10 @@ public static bool ChangeQuantumState( return false; } } + else + { + return true; + } foreach (var socket in __instance._childSockets) { @@ -77,9 +86,15 @@ public static bool ChangeQuantumState( for (var i = 0; i < 20; i++) { var index = Random.Range(0, list.Count); + TestSocketMove = true; __instance.MoveToSocket(list[index]); + TestSocketMove = false; if (skipInstantVisibilityCheck) { + if (__instance.GetWorldObject().ControllingPlayer == QSBPlayerManager.LocalPlayerId) + { + __instance.GetWorldObject().SendMessage(new SocketStateChangeMessage(list[index].GetWorldObject().ObjectId, __instance.transform.localRotation)); + } __result = true; return false; } @@ -104,12 +119,16 @@ public static bool ChangeQuantumState( else { // socket not suitable if player is inside object - socketNotSuitable = playersEntangled.Any(x => __instance.CheckPointInside(x.CameraBody.transform.position)); + socketNotSuitable = QSBPlayerManager.PlayerList.Any(x => __instance.CheckPointInside(x.CameraBody.transform.position)); } } if (!socketNotSuitable) { + if (__instance.GetWorldObject().ControllingPlayer == QSBPlayerManager.LocalPlayerId) + { + __instance.GetWorldObject().SendMessage(new SocketStateChangeMessage(list[index].GetWorldObject().ObjectId, __instance.transform.localRotation)); + } __result = true; return false; } @@ -121,7 +140,9 @@ public static bool ChangeQuantumState( } } + TestSocketMove = true; __instance.MoveToSocket(occupiedSocket); + TestSocketMove = false; __result = false; return false; } @@ -135,6 +156,11 @@ public static void MoveToSocket(SocketedQuantumObject __instance, QuantumSocket return; } + if (TestSocketMove) + { + return; + } + if (socket == null) { DebugLog.ToConsole($"Error - Trying to move {__instance.name} to a null socket!", MessageType.Error); diff --git a/QSB/QuantumSync/Patches/Common/Visibility/VisibilityShapePatches.cs b/QSB/QuantumSync/Patches/Common/Visibility/VisibilityShapePatches.cs index 8937eb328..c101785a1 100644 --- a/QSB/QuantumSync/Patches/Common/Visibility/VisibilityShapePatches.cs +++ b/QSB/QuantumSync/Patches/Common/Visibility/VisibilityShapePatches.cs @@ -1,4 +1,5 @@ using HarmonyLib; +using OWML.Utils; using QSB.Patches; using QSB.Utility; diff --git a/QSB/QuantumSync/Patches/Common/Visibility/VisibilityShapeVisibilityTrackerPatches.cs b/QSB/QuantumSync/Patches/Common/Visibility/VisibilityShapeVisibilityTrackerPatches.cs index 7cb022466..b8a43258c 100644 --- a/QSB/QuantumSync/Patches/Common/Visibility/VisibilityShapeVisibilityTrackerPatches.cs +++ b/QSB/QuantumSync/Patches/Common/Visibility/VisibilityShapeVisibilityTrackerPatches.cs @@ -1,5 +1,8 @@ using HarmonyLib; using QSB.Patches; +using QSB.QuantumSync.WorldObjects; +using QSB.WorldSync; +using UnityEngine; namespace QSB.QuantumSync.Patches.Common.Visibility; @@ -23,4 +26,34 @@ public static bool IsVisible(ShapeVisibilityTracker __instance, out bool __resul __result = QuantumManager.IsVisible(__instance, false); return false; } + + [HarmonyPrefix] + [HarmonyPatch(nameof(ShapeVisibilityTracker.IsInFrustum))] + public static bool IsInFrustum(ShapeVisibilityTracker __instance, Plane[] frustumPlanes, out bool __result) + { + // todo : cache this somewhere? seems slow. + var quantumObject = __instance.GetComponentInParent(); + + if (quantumObject == null) + { + __result = false; + return true; + } + + var worldObject = quantumObject.GetWorldObject(); + foreach (var shape in __instance._shapes) + { + // normally only checks if enabled and visible + // helps prevent state change when owner leaves and we are observing + // is this wrong? it feels wrong. + if ((shape.enabled || worldObject.ControllingPlayer == 0) && shape.IsVisible(frustumPlanes)) + { + __result = true; + return false; + } + } + + __result = false; + return false; + } } diff --git a/QSB/QuantumSync/QuantumManager.cs b/QSB/QuantumSync/QuantumManager.cs index 455c9fa14..6769ccd58 100644 --- a/QSB/QuantumSync/QuantumManager.cs +++ b/QSB/QuantumSync/QuantumManager.cs @@ -36,8 +36,6 @@ public override async UniTask BuildWorldObjects(OWScene scene, CancellationToken { Shrine = QSBWorldSync.GetUnityObject(); } - - UpdateFromDebugSetting(); } public void PlayerLeave(PlayerInfo player) @@ -147,81 +145,4 @@ public static void OnRemoveProbeSnapshot(PlayerInfo player) } } } - - #region debug shapes - - private static GameObject _debugSphere, _debugCube, _debugCapsule; - - private class DebugShape : MonoBehaviour { } - - public static void UpdateFromDebugSetting() - { - if (QSBCore.DebugSettings.DrawQuantumVisibilityObjects) - { - if (_debugSphere == null) - { - _debugSphere = QSBCore.DebugAssetBundle.LoadAsset("Assets/Prefabs/Sphere.prefab"); - } - - if (_debugCube == null) - { - _debugCube = QSBCore.DebugAssetBundle.LoadAsset("Assets/Prefabs/Cube.prefab"); - } - - if (_debugCapsule == null) - { - _debugCapsule = QSBCore.DebugAssetBundle.LoadAsset("Assets/Prefabs/Capsule.prefab"); - } - - foreach (var quantumObject in QSBWorldSync.GetWorldObjects()) - { - foreach (var shape in quantumObject.GetAttachedShapes()) - { - if (shape is BoxShape boxShape) - { - var newCube = Instantiate(_debugCube); - newCube.transform.parent = shape.transform; - newCube.transform.localPosition = Vector3.zero; - newCube.transform.localRotation = Quaternion.Euler(0, 0, 0); - newCube.transform.localScale = boxShape.size; - newCube.AddComponent(); - } - else if (shape is SphereShape sphereShape) - { - var newSphere = Instantiate(_debugSphere); - newSphere.transform.parent = shape.transform; - newSphere.transform.localPosition = Vector3.zero; - newSphere.transform.localRotation = Quaternion.Euler(0, 0, 0); - newSphere.transform.localScale = Vector3.one * (sphereShape.radius * 2); - newSphere.AddComponent(); - } - else if (shape is CapsuleShape capsuleShape) - { - var newCapsule = Instantiate(_debugCapsule); - newCapsule.transform.parent = shape.transform; - newCapsule.transform.localPosition = Vector3.zero; - newCapsule.transform.localRotation = Quaternion.Euler(0, 0, 0); - newCapsule.transform.localScale = new Vector3(capsuleShape.radius * 2, capsuleShape.height, capsuleShape.radius * 2); - newCapsule.AddComponent(); - } - } - } - } - else - { - foreach (var quantumObject in QSBWorldSync.GetWorldObjects()) - { - foreach (var shape in quantumObject.GetAttachedShapes()) - { - var debugShape = shape.GetComponentInChildren(); - if (debugShape) - { - Destroy(debugShape.gameObject); - } - } - } - } - } - - #endregion } diff --git a/QSB/RespawnSync/RespawnManager.cs b/QSB/RespawnSync/RespawnManager.cs index 584f5aa7b..7d8b43e9c 100644 --- a/QSB/RespawnSync/RespawnManager.cs +++ b/QSB/RespawnSync/RespawnManager.cs @@ -139,6 +139,12 @@ public void Respawn() cameraEffectController.OpenEyes(1f); OWInput.ChangeInputMode(InputMode.Character); + + var mixer = Locator.GetAudioMixer(); + mixer._deathMixed = false; + mixer._nonEndTimesVolume.FadeTo(1, 0.5f); + mixer._endTimesVolume.FadeTo(1, 0.5f); + mixer.UnmixMap(); } public void OnPlayerDeath(PlayerInfo player) diff --git a/QSB/SaveSync/Messages/GameStateMessage.cs b/QSB/SaveSync/Messages/GameStateMessage.cs index 5a9c20a4c..e6e084cf7 100644 --- a/QSB/SaveSync/Messages/GameStateMessage.cs +++ b/QSB/SaveSync/Messages/GameStateMessage.cs @@ -4,6 +4,8 @@ using QSB.Messaging; using QSB.Utility; using System.Collections.Generic; +using QSB.TimeSync.Patches; +using UnityEngine; namespace QSB.SaveSync.Messages; @@ -19,6 +21,10 @@ public class GameStateMessage : QSBMessage private bool[] KnownFrequencies; private Dictionary KnownSignals; private bool ReducedFrights; + private bool IsLoopAfterStatue; + private Quaternion StatueRotation; + private Vector3 PlayerPosition; + private Quaternion PlayerRotation; public GameStateMessage(uint toId) { @@ -31,6 +37,10 @@ public GameStateMessage(uint toId) KnownFrequencies = gameSave.knownFrequencies; KnownSignals = gameSave.knownSignals; ReducedFrights = PlayerData.GetReducedFrights(); + IsLoopAfterStatue = TimeLoopPatches.IsLoopAfterStatue; + StatueRotation = MemoryUplinkTrigger._savedStatueRotation; + PlayerPosition = PlayerSpawner._localResetPos; + PlayerRotation = PlayerSpawner._localResetRotation; } public override void Serialize(NetworkWriter writer) @@ -51,6 +61,10 @@ public override void Serialize(NetworkWriter writer) } writer.Write(ReducedFrights); + writer.Write(IsLoopAfterStatue); + writer.Write(StatueRotation); + writer.Write(PlayerPosition); + writer.Write(PlayerRotation); } public override void Deserialize(NetworkReader reader) @@ -73,6 +87,10 @@ public override void Deserialize(NetworkReader reader) } ReducedFrights = reader.Read(); + IsLoopAfterStatue = reader.Read(); + StatueRotation = reader.ReadQuaternion(); + PlayerPosition = reader.ReadVector3(); + PlayerRotation = reader.ReadQuaternion(); } public override void OnReceiveRemote() @@ -95,6 +113,12 @@ public override void OnReceiveRemote() PlayerData.SetPersistentCondition("LAUNCH_CODES_GIVEN", LaunchCodesGiven); PlayerData._settingsSave.reducedFrights = ReducedFrights; + TimeLoop._startTimeLoopOnReload = IsLoopAfterStatue; + + MemoryUplinkTrigger._savedStatueRotation = StatueRotation; + PlayerSpawner._localResetPos = PlayerPosition; + PlayerSpawner._localResetRotation = PlayerRotation; + PlayerData.SaveCurrentGame(); MenuManager.Instance.LoadGame(WarpedToTheEye); diff --git a/QSB/SectorSync/QSBSectorManager.cs b/QSB/SectorSync/QSBSectorManager.cs index 21fc185fa..b52c8a1ac 100644 --- a/QSB/SectorSync/QSBSectorManager.cs +++ b/QSB/SectorSync/QSBSectorManager.cs @@ -101,15 +101,9 @@ private static void CreateFakeSectors() // TH elevators foreach (var elevator in QSBWorldSync.GetUnityObjects()) { - // just create a sphere at the attach point lol - // since players will be moved there when riding the elevator - FakeSector.Create(elevator._attachPoint.gameObject, + FakeSector.Create(elevator.gameObject, elevator.GetComponentInParent(), - x => - { - x.gameObject.AddComponent(); - x.gameObject.AddComponent(); - }); + x => x._triggerRoot = elevator.gameObject); } // rafts @@ -120,9 +114,71 @@ private static void CreateFakeSectors() x => x._triggerRoot = raft._rideVolume.gameObject); } - // todo cage elevators - // todo prisoner elevator - // todo black hole forge + // cage elevators + foreach (var cageElevator in QSBWorldSync.GetUnityObjects()) + { + FakeSector.Create(cageElevator._platformBody.gameObject, + cageElevator.gameObject.GetComponentInParent(), + x => + { + x.gameObject.AddComponent(); + var shape = x.gameObject.AddComponent(); + shape.size = new Vector3(2.5f, 4.25f, 2.5f); + shape.center = new Vector3(0, 2.15f, 0); + + // When the cage elevator warps when entering/exiting the underground, + // the player's sector detector is removed from the fake sector. + // So when the elevator is moving and they leave the sector, it means they have warped + // and should be added back in. + x.OnOccupantExitSector.AddListener((e) => + { + if (cageElevator.isMoving) x.AddOccupant(e); + }); + }); + } + + // prisoner elevator + { + var prisonerElevator = QSBWorldSync.GetUnityObject(); + FakeSector.Create(prisonerElevator._elevatorBody.gameObject, + prisonerElevator.gameObject.GetComponentInParent(), + x => + { + x.gameObject.AddComponent(); + var shape = x.gameObject.AddComponent(); + shape.size = new Vector3(4f, 6.75f, 6.7f); + shape.center = new Vector3(0, 3.3f, 3.2f); + }); + } + + + //black hole forge + { + var forge = GameObject.Find("BlackHoleForgePivot"); + FakeSector.Create(forge, + forge.GetComponentInParent(), + x => + { + x._triggerRoot = GameObject.Find("BrittleHollow_Body/Sector_BH/Sector_NorthHemisphere/Sector_NorthPole/" + + "Sector_HangingCity/Sector_HangingCity_BlackHoleForge/BlackHoleForgePivot/" + + "Volumes_BlackHoleForge/DirectionalForceVolume"); + }); + } + + // black hole forge entrance elevator + { + var entrance = GameObject.Find("BlackHoleForge_EntrancePivot"); + var sector = GameObject.Find("Sector_HangingCity_BlackHoleForge").GetComponent(); + FakeSector.Create(entrance, + sector, + x => + { + x.gameObject.AddComponent(); + var shape = x.gameObject.AddComponent(); + shape.size = new Vector3(5.5f, 5.8f, 5.5f); + shape.center = new Vector3(0f, 2.9f, 1.5f); + }); + } // OPC probe { diff --git a/QSB/SectorSync/WorldObjects/QSBSector.cs b/QSB/SectorSync/WorldObjects/QSBSector.cs index 5e8ba3cf7..c969bb3e7 100644 --- a/QSB/SectorSync/WorldObjects/QSBSector.cs +++ b/QSB/SectorSync/WorldObjects/QSBSector.cs @@ -78,7 +78,7 @@ public bool ShouldSyncTo(DynamicOccupant occupantType) public float GetScore(OWRigidbody rigidbody) { - var sqrDistance = (Transform.position - rigidbody.GetPosition()).sqrMagnitude; + var sqrDistance = (AttachedObject._triggerRoot.transform.position - rigidbody.GetPosition()).sqrMagnitude; var radius = GetRadius(); var sqrVelocity = GetSqrVelocity(rigidbody); diff --git a/QSB/ShipSync/Patches/ShipAudioPatches.cs b/QSB/ShipSync/Patches/ShipAudioPatches.cs index 51f685179..c227f699b 100644 --- a/QSB/ShipSync/Patches/ShipAudioPatches.cs +++ b/QSB/ShipSync/Patches/ShipAudioPatches.cs @@ -41,4 +41,57 @@ public static bool ShipThrusterAudio_Update(ShipThrusterAudio __instance) return true; } } + + [HarmonyPrefix] + [HarmonyPatch(typeof(GlobalMusicController), nameof(GlobalMusicController.UpdateTravelMusic))] + public static bool GlobalMusicController_UpdateTravelMusic(GlobalMusicController __instance) + { + // only this line is changed + bool flag = PlayerState.IsInsideShip() + && ShipManager.Instance.CurrentFlyer != uint.MaxValue + && Locator.GetPlayerRulesetDetector().AllowTravelMusic() + && !PlayerState.IsHullBreached() + && !__instance._playingFinalEndTimes; + + + bool flag2 = __instance._travelSource.isPlaying && !__instance._travelSource.IsFadingOut(); + if (flag && !flag2) + { + __instance._travelSource.FadeIn(5f, false, false, 1f); + return false; + } + if (!flag && flag2) + { + __instance._travelSource.FadeOut(5f, OWAudioSource.FadeOutCompleteAction.PAUSE, 0f); + } + + return false; + } + + [HarmonyPrefix] + [HarmonyPatch(typeof(GlobalMusicController), nameof(GlobalMusicController.UpdateBrambleMusic))] + public static bool GlobalMusicController_UpdateBrambleMusic(GlobalMusicController __instance) + { + bool flag = Locator.GetPlayerSectorDetector().InBrambleDimension() + && !Locator.GetPlayerSectorDetector().InVesselDimension() + && PlayerState.IsInsideShip() + && ShipManager.Instance.CurrentFlyer != uint.MaxValue + && !PlayerState.IsHullBreached() + && !__instance._playingFinalEndTimes; + + bool flag2 = __instance._darkBrambleSource.isPlaying && !__instance._darkBrambleSource.IsFadingOut(); + + if (flag && !flag2) + { + __instance._darkBrambleSource.FadeIn(5f, false, false, 1f); + return false; + } + + if (!flag && flag2) + { + __instance._darkBrambleSource.FadeOut(5f, OWAudioSource.FadeOutCompleteAction.STOP, 0f); + } + + return false; + } } diff --git a/QSB/ShipSync/Patches/ShipPatches.cs b/QSB/ShipSync/Patches/ShipPatches.cs index 8eca58c21..aaef8f6eb 100644 --- a/QSB/ShipSync/Patches/ShipPatches.cs +++ b/QSB/ShipSync/Patches/ShipPatches.cs @@ -1,4 +1,5 @@ using HarmonyLib; +using OWML.Utils; using QSB.Messaging; using QSB.Patches; using QSB.ShipSync.Messages; diff --git a/QSB/ShipSync/ShipManager.cs b/QSB/ShipSync/ShipManager.cs index 90e875b7b..0763e80d9 100644 --- a/QSB/ShipSync/ShipManager.cs +++ b/QSB/ShipSync/ShipManager.cs @@ -30,6 +30,10 @@ public class ShipManager : WorldObjectManager public ShipElectricalComponent ShipElectricalComponent; public ShipCockpitUI ShipCockpitUI; private GameObject _shipCustomAttach; + + /// + /// PlayerID of the person who is flying the ship. If no one is flying, this is . + /// public uint CurrentFlyer { get => _currentFlyer; diff --git a/QSB/ShipSync/TransformSync/ShipTransformSync.cs b/QSB/ShipSync/TransformSync/ShipTransformSync.cs index 53dc543b2..464ce5c60 100644 --- a/QSB/ShipSync/TransformSync/ShipTransformSync.cs +++ b/QSB/ShipSync/TransformSync/ShipTransformSync.cs @@ -132,6 +132,10 @@ private static void SetVelocity(OWRigidbody @this, Vector3 newVelocity) #endregion + /// + /// move if inside the ship + /// or in space near the ship + /// private bool ShouldMovePlayer => PlayerState.IsInsideShip() || (PlayerState.InZeroG() && Vector3.Distance(AttachedTransform.position, Locator.GetPlayerBody().GetPosition()) < 100); diff --git a/QSB/ShipSync/WorldObjects/QSBShipComponent.cs b/QSB/ShipSync/WorldObjects/QSBShipComponent.cs index de509a71e..1b0311459 100644 --- a/QSB/ShipSync/WorldObjects/QSBShipComponent.cs +++ b/QSB/ShipSync/WorldObjects/QSBShipComponent.cs @@ -1,4 +1,5 @@ -using QSB.Utility; +using QSB.Utility; +using OWML.Utils; using QSB.WorldSync; namespace QSB.ShipSync.WorldObjects; diff --git a/QSB/ShipSync/WorldObjects/QSBShipHull.cs b/QSB/ShipSync/WorldObjects/QSBShipHull.cs index 49acfea64..606211433 100644 --- a/QSB/ShipSync/WorldObjects/QSBShipHull.cs +++ b/QSB/ShipSync/WorldObjects/QSBShipHull.cs @@ -1,4 +1,5 @@ -using QSB.Utility; +using QSB.Utility; +using OWML.Utils; using QSB.WorldSync; namespace QSB.ShipSync.WorldObjects; diff --git a/QSB/Syncs/Occasional/OccasionalManager.cs b/QSB/Syncs/Occasional/OccasionalManager.cs index 1b0ffb9d7..b60fdda3f 100644 --- a/QSB/Syncs/Occasional/OccasionalManager.cs +++ b/QSB/Syncs/Occasional/OccasionalManager.cs @@ -1,6 +1,7 @@ using Cysharp.Threading.Tasks; using Mirror; using QSB.Utility; +using QSB.Utility.Deterministic; using QSB.WorldSync; using System.Collections.Generic; using System.Linq; diff --git a/QSB/TimeSync/Patches/TimePatches.cs b/QSB/TimeSync/Patches/TimePatches.cs index 64630859e..8e79ae02a 100644 --- a/QSB/TimeSync/Patches/TimePatches.cs +++ b/QSB/TimeSync/Patches/TimePatches.cs @@ -110,3 +110,17 @@ private static void SetSecondsRemaining(float secondsRemaining) new SetSecondsRemainingMessage(secondsRemaining).Send(); } } + +public class TimeLoopPatches : QSBPatch +{ + public override QSBPatchTypes Type => QSBPatchTypes.OnModStart; + + public static bool IsLoopAfterStatue; + + [HarmonyPrefix] + [HarmonyPatch(typeof(TimeLoop), nameof(TimeLoop.Start))] + private static void TimeLoopStart() + { + IsLoopAfterStatue = TimeLoop._startTimeLoopOnReload; + } +} diff --git a/QSB/Tools/ProbeLauncherTool/ProbeLauncherManager.cs b/QSB/Tools/ProbeLauncherTool/ProbeLauncherManager.cs index 3902e0a3c..8f98a005e 100644 --- a/QSB/Tools/ProbeLauncherTool/ProbeLauncherManager.cs +++ b/QSB/Tools/ProbeLauncherTool/ProbeLauncherManager.cs @@ -1,6 +1,6 @@ using Cysharp.Threading.Tasks; using QSB.Tools.ProbeLauncherTool.WorldObjects; -using QSB.Utility; +using QSB.Utility.Deterministic; using QSB.WorldSync; using System.Linq; using System.Threading; diff --git a/QSB/Tools/ProbeTool/Patches/ProbeToolPatches.cs b/QSB/Tools/ProbeTool/Patches/ProbeToolPatches.cs index 7b0f487a9..f528e2dd1 100644 --- a/QSB/Tools/ProbeTool/Patches/ProbeToolPatches.cs +++ b/QSB/Tools/ProbeTool/Patches/ProbeToolPatches.cs @@ -1,4 +1,5 @@ using HarmonyLib; +using OWML.Utils; using QSB.Messaging; using QSB.Patches; using QSB.Tools.ProbeTool.Messages; diff --git a/QSB/Tools/TranslatorTool/QSBNomaiTranslator.cs b/QSB/Tools/TranslatorTool/QSBNomaiTranslator.cs index aaf41bf77..1f6d602c6 100644 --- a/QSB/Tools/TranslatorTool/QSBNomaiTranslator.cs +++ b/QSB/Tools/TranslatorTool/QSBNomaiTranslator.cs @@ -94,7 +94,7 @@ public override void Update() if (_currentNomaiText is NomaiWallText nomaiWallText) { var nomaiTextLine = nomaiWallText.GetClosestTextLineByCenter(raycastHit.point); - if (_lastLineLocked) + if (_lastLineLocked && _lastHighlightedTextLine != null) { var distToCenter = _lastHighlightedTextLine.GetDistToCenter(raycastHit.point); if (distToCenter > _lastLineDist + 0.1f) diff --git a/QSB/Translations/de.json b/QSB/Translations/de.json index 60fd0e3f2..6bd90036e 100644 --- a/QSB/Translations/de.json +++ b/QSB/Translations/de.json @@ -5,7 +5,7 @@ "PauseMenuDisconnect": "VERBINDUNG TRENNEN", "PauseMenuStopHosting": "HOSTEN BEENDEN", "PublicIPAddress": "Öffentliche IP Addresse\n\n(DEIN MEHRSPIELER SPIELSTAND WIRD ÜBERSCHRIEBEN)", - "ProductUserID": "Produktnutzer ID\n\n(DEIN MEHRSPIELER SPIELSTAND WIRD ÜBERSCHRIEBEN)", + "SteamID": "Steam ID\n\n(DEIN MEHRSPIELER SPIELSTAND WIRD ÜBERSCHRIEBEN)", "Connect": "VERBINDEN", "Cancel": "ABBRECHEN", "HostExistingOrNewOrCopy": "Willst du eine bestehende Mehrspieler Expedition, eine neue Expedition oder eine Kopie einer bestehenden Einzelspieler Expedition hosten?", @@ -18,7 +18,7 @@ "Yes": "JA", "No": "NEIN", "StopHostingAreYouSure": "Bist du sicher, dass du aufhören willst zu hosten?\nDies wird die Verbindung alle anderen Spieler trennen und sie zurück in das Hauptmenü bringen.", - "CopyProductUserIDToClipboard": "Server wird gehostet.\nAndere Spieler werden sich mit Hilfe deiner Produktnutzer ID verbinden. Sie lautet :\n{0}\nWillst du das in die Zwischenablage kopieren?", + "CopySteamIDToClipboard": "Server wird gehostet.\nAndere Spieler werden sich mit Hilfe deiner Steam ID verbinden. Sie lautet :\n{0}\nWillst du das in die Zwischenablage kopieren?", "Connecting": "VERBINDEN...", "OK": "BESTÄTIGEN", "ServerRefusedConnection": "Der Server hat die Verbindung abgelehnt.\n{0}", @@ -28,8 +28,8 @@ "DLCMismatch": "DLC Installationsstatus stimmt nicht überein. (Client:{0}, Server:{1})", "GameProgressLimit": "Spiel ist zu weit fortgeschritten.", "AddonMismatch": "Addons stimmen nicht überein. (Client:{0} addons, Server:{1} addons)", - "IncompatibleMod": "Es wird eine inkompatible/unerlaubte Modifikation genutzt. Die erste gefundene Modifikation war {0}", "PlayerJoinedTheGame": "{0} trat bei!", + "PlayerLeftTheGame": "{0} verließ!", "PlayerWasKicked": "{0} wurde gekickt.", "KickedFromServer": "Vom Server gekickt. Grund : {0}", "RespawnPlayer": "Spieler respawnen", diff --git a/QSB/Translations/en.json b/QSB/Translations/en.json index a0430021d..13e1277d9 100644 --- a/QSB/Translations/en.json +++ b/QSB/Translations/en.json @@ -5,7 +5,7 @@ "PauseMenuDisconnect": "DISCONNECT", "PauseMenuStopHosting": "STOP HOSTING", "PublicIPAddress": "Public IP Address\n\n(YOUR MULTIPLAYER SAVE DATA WILL BE OVERWRITTEN)", - "ProductUserID": "Product User ID\n\n(YOUR MULTIPLAYER SAVE DATA WILL BE OVERWRITTEN)", + "SteamID": "Steam ID\n\n(YOUR MULTIPLAYER SAVE DATA WILL BE OVERWRITTEN)", "Connect": "CONNECT", "Cancel": "CANCEL", "HostExistingOrNewOrCopy": "Do you want to host an existing multiplayer expedition, host a new expedition, or copy the existing singleplayer expedition to multiplayer?", @@ -18,7 +18,7 @@ "Yes": "YES", "No": "NO", "StopHostingAreYouSure": "Are you sure you want to stop hosting?\nThis will disconnect all clients and send everyone back to the main menu.", - "CopyProductUserIDToClipboard": "Hosting server.\nClients will connect using your product user id, which is :\n{0}\nDo you want to copy this to the clipboard?", + "CopySteamIDToClipboard": "Hosting server.\nClients will connect using your Steam ID, which is :\n{0}\nDo you want to copy this to the clipboard?", "Connecting": "CONNECTING...", "OK": "OK", "ServerRefusedConnection": "Server refused connection.\n{0}", @@ -28,7 +28,6 @@ "DLCMismatch": "DLC installation state does not match. (Client:{0}, Server:{1})", "GameProgressLimit": "Game has progressed too far.", "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.", diff --git a/QSB/Translations/fr.json b/QSB/Translations/fr.json index 7b94d52ad..37d79af47 100644 --- a/QSB/Translations/fr.json +++ b/QSB/Translations/fr.json @@ -5,7 +5,7 @@ "PauseMenuDisconnect": "DÉCONNECTER", "PauseMenuStopHosting": "ARRÊTER L'HÉBERGEMENT", "PublicIPAddress": "Adresse IP publique\n\n(CELA EFFACERA VOTRE PROGRESSION MULTIJOUEUR)", - "ProductUserID": "ID utilisateur\n\n(CELA EFFACERA VOTRE PROGRESSION MULTIJOUEUR)", + "SteamID": "ID utilisateur\n\n(CELA EFFACERA VOTRE PROGRESSION MULTIJOUEUR)", "Connect": "SE CONNECTER", "Cancel": "ANNULER", "HostExistingOrNewOrCopy": "Veux-tu héberger une expédition multijouer existante, héberger une nouvelle expédition, ou héberger une copie de ton expédition solo existante?", @@ -18,7 +18,7 @@ "Yes": "OUI", "No": "NON", "StopHostingAreYouSure": "Veux-tu vraiment arrêter l'hébergement?\n Cela déconnectera tous les clients et renverra tout le monde au menu principal.", - "CopyProductUserIDToClipboard": "Le serveur est maintenant hébergé. Les clients se connecteront en utilisant votre ID utilisateur, qui est : \n{0}\nVeux-tu copier ceci dans le presse-papier ?", + "CopySteamIDToClipboard": "Le serveur est maintenant hébergé. Les clients se connecteront en utilisant votre ID utilisateur, qui est : \n{0}\nVeux-tu copier ceci dans le presse-papier ?", "Connecting": "CONNEXION EN COURS...", "OK": "D'ACCORD", "ServerRefusedConnection": "Le serveur a refusé la connexion.\n{0}", @@ -28,8 +28,8 @@ "DLCMismatch": "Les états d'installation du DLC ne correspondent pas. (Client:{0}, Serveur:{1})", "GameProgressLimit": "Le jeu a trop progressé.", "AddonMismatch": "Non-concordance des addons. (Client:{0} addons, Serveur:{1} addons)", - "IncompatibleMod": "Tu utilises un mod incompatible/non autorisé. Le premier mod trouvé était {0}", "PlayerJoinedTheGame": "{0} a rejoint!", + "PlayerLeftTheGame": "{0} est parti!", "PlayerWasKicked": "{0} a été expulsé.", "KickedFromServer": "Tu as été expulsé du serveur. Raison : {0}", "RespawnPlayer": "Faire réapparaître le joueur", diff --git a/QSB/Translations/pt-br.json b/QSB/Translations/pt-br.json index 0fefa9600..88c5223c8 100644 --- a/QSB/Translations/pt-br.json +++ b/QSB/Translations/pt-br.json @@ -5,7 +5,7 @@ "PauseMenuDisconnect": "DESCONECTAR", "PauseMenuStopHosting": "PARAR DE HOSPEDAR", "PublicIPAddress": "Endereço de IP Público\n\n(O DADOS DO SEU SAVE DE MULTIJOGADORES SERÃO SOBRESCRITOS)", - "ProductUserID": "ID de Produto de Usuário\n\n(O DADOS DO SEU SAVE DE MULTIJOGADORES SERÃO SOBRESCRITOS)", + "SteamID": "Steam ID\n\n(O DADOS DO SEU SAVE DE MULTIJOGADORES SERÃO SOBRESCRITOS)", "Connect": "CONECTAR", "Cancel": "CANCELAR", "HostExistingOrNewOrCopy": "Você quer hospedar uma expedição de multijogadores pré-existente, hospedar uma nova expedição, ou copiar a já existente expedição do seu save para multiplos jogadores?", @@ -18,7 +18,7 @@ "Yes": "SIM", "No": "NÃO", "StopHostingAreYouSure": "Você tem certeza que quer parar de hospedar\nEssa ação irá de desconectar os jogadores conectados e eviará todos de volta ao menu principal.", - "CopyProductUserIDToClipboard": "Hospedando servidor.\nOutros jogadores poderão se conectar usando o seu ID de produto de usuário, o qual é :\n{0}\nVocê quer copia-lo para a área de transferência?", + "CopySteamIDToClipboard": "Hospedando servidor.\nOutros jogadores poderão se conectar usando o seu Steam ID, que é:\n{0}\nVocê quer copiá-lo para a área de transferência?", "Connecting": "CONECTANDO...", "OK": "OK", "ServerRefusedConnection": "Servidor recusou a conexão.\n{0}", @@ -28,8 +28,8 @@ "DLCMismatch": "O estado da instalação da DLC não correspondem. (Cliente:{0}, Servidor:{1})", "GameProgressLimit": "O jogo progrediu além do limite.", "AddonMismatch": "Incompatibilidade de Addons. (Cliente:{0} addons, Servidor:{1} addons)", - "IncompatibleMod": "Usando um mod incompativel ou não permitido. Primeiro mod encontrado foi {0}", "PlayerJoinedTheGame": "{0} entrou!", + "PlayerLeftTheGame": "{0} saiu!", "PlayerWasKicked": "{0} foi expulso.", "KickedFromServer": "Expulso do servidor. Motivo : {0}", "RespawnPlayer": "Renascer Jogador", diff --git a/QSB/Translations/ru.json b/QSB/Translations/ru.json index ea2971381..f11ee8532 100644 --- a/QSB/Translations/ru.json +++ b/QSB/Translations/ru.json @@ -5,7 +5,7 @@ "PauseMenuDisconnect": "ОТКЛЮЧИТЬСЯ", "PauseMenuStopHosting": "ПРЕКРАТИТЬ ХОСТИНГ", "PublicIPAddress": "Публичный IP-адрес\n\n(ВАШИ МНОГОПОЛЬЗОВАТЕЛЬСКИЕ СОХРАНЁННЫЕ ДАННЫЕ БУДУТ ПЕРЕЗАПИСАНЫ)", - "ProductUserID": "ID игрока\n\n(ВАШИ МНОГОПОЛЬЗОВАТЕЛЬСКИЕ СОХРАНЁННЫЕ ДАННЫЕ БУДУТ ПЕРЕЗАПИСАНЫ)", + "SteamID": "ID Steam\n\n(ВАШИ МНОГОПОЛЬЗОВАТЕЛЬСКИЕ СОХРАНЁННЫЕ ДАННЫЕ БУДУТ ПЕРЕЗАПИСАНЫ)", "Connect": "Подключиться", "Cancel": "Отмена", "HostExistingOrNewOrCopy": "Вы хотите продолжить существующую мультиплеерную экспедицию, начать новую мультиплеерную экспедицию, или скопировать прогресс из одиночной экспедиции?", @@ -18,7 +18,7 @@ "Yes": "ДА", "No": "НЕТ", "StopHostingAreYouSure": "Вы уверены, что хотите прекратить хостинг?\nЭто отключит всех игроков от игры и отправит их в главное меню.", - "CopyProductUserIDToClipboard": "Начинаю хостинг.\nИгроки будут использовать ваш ID продукта :\n{0}\nЖелаете скопировать его в буфер обмена?", + "CopySteamIDToClipboard": "Начинаю хостинг.\nИгроки будут использовать ваш ID Steam:\n{0}\nЖелаете скопировать его в буфер обмена?", "Connecting": "ПОДКЛЮЧЕНИЕ...", "OK": "ЛАДНО", "ServerRefusedConnection": "Сервер отказал в подключении.\n{0}", @@ -28,7 +28,6 @@ "DLCMismatch": "Состояние присутствия DLC отличается. (Клиент:{0}, Сервер:{1})", "GameProgressLimit": "Игра продолжалась слишком долго.", "AddonMismatch": "Аддоны различаются. (Клиент:{0} аддонов, Сервер:{1} аддонов)", - "IncompatibleMod": "Используется несовместимый(ые)/неразрешенный(ые) мод(ы). Первый из них - {0}", "PlayerJoinedTheGame": "{0} подключился!", "PlayerWasKicked": "{0} был отключён.", "KickedFromServer": "Отключён от сервера. Причина : {0}", diff --git a/QSB/Translations/spanish.json b/QSB/Translations/spanish.json index a2f4ce4dd..ea8626f20 100644 --- a/QSB/Translations/spanish.json +++ b/QSB/Translations/spanish.json @@ -5,7 +5,7 @@ "PauseMenuDisconnect": "DESCONECTAR", "PauseMenuStopHosting": "DEJAR DE ALOJAR", "PublicIPAddress": "IP Pública\n\n(TUS DATOS DE GUARDADO DE MULTIJUGADOR SERÁN SOBREESCRITOS)", - "ProductUserID": "ID de Producto\n\n(TUS DATOS DE GUARDADO DE MULTIJUGADOR SERÁN SOBREESCRITOS)", + "SteamID": "ID de Steam\n\n(TUS DATOS DE GUARDADO DE MULTIJUGADOR SERÁN SOBREESCRITOS)", "Connect": "CONECTARSE", "Cancel": "CANCELAR", "HostExistingOrNewOrCopy": "¿Quieres cargar una expedición multijugador ya existente, empezar una nueva expedición, o copiar y cargar tu expedición de un jugador?", @@ -18,7 +18,7 @@ "Yes": "SI", "No": "NO", "StopHostingAreYouSure": "¿Estás seguro que quieres dejar de alojar?\nEsto desconectará a todos los jugadores y los enviará de vuelta al menú principal.", - "CopyProductUserIDToClipboard": "Alojando servidor.\nLos jugadores se conectarán usando tu ID de Producto, la cual es :\n{0}\n¿Quieres copiarla en tu portapapeles?", + "CopySteamIDToClipboard": "Alojando servidor.\nLos jugadores se conectarán usando tu ID de Steam, la cual es :\n{0}\n¿Quieres copiarla en tu portapapeles?", "Connecting": "CONECTANDO...", "OK": "OK", "ServerRefusedConnection": "El servidor ha rechazado la conexión.\n{0}", @@ -28,7 +28,6 @@ "DLCMismatch": "El estado de instalación del DLC no coincide. (Cliente:{0}, Servidor:{1})", "GameProgressLimit": "El juego ha progresado mas del límite permitido.", "AddonMismatch": "Incompatibilidad de Addons. (Cliente:{0} addons, Servidor:{1} addons)", - "IncompatibleMod": "Se está usando un mod incompatible/no permitido. El primer mod encontrado ha sido {0}", "PlayerJoinedTheGame": "¡{0} se ha unido!", "PlayerWasKicked": "{0} ha sido expulsado.", "KickedFromServer": "Expulsado del servidor. Razón : {0}", diff --git a/QSB/Translations/tr.json b/QSB/Translations/tr.json index 86b6876c1..2038eed5f 100644 --- a/QSB/Translations/tr.json +++ b/QSB/Translations/tr.json @@ -5,7 +5,7 @@ "PauseMenuDisconnect": "BAGLANTIYI KES", "PauseMenuStopHosting": "PAYLAŞMAYI DURDUR", "PublicIPAddress": "Genel IP Adresi\n\n(ÇOK OYUNCULU İLERLEMENİZ SİLİNECEKTİR)", - "ProductUserID": "Kullanıcı Kimliği\n\n(ÇOK OYUNCULU İLERLEMENİZ SİLİNECEKTİR)", + "SteamID": "Steam Kimliği\n\n(ÇOK OYUNCULU İLERLEMENİZ SİLİNECEKTİR)", "Connect": "BAĞLAN", "Cancel": "İPTAL", "HostExistingOrNewOrCopy": "Mevcut bir tek oyunculu keşfi çok oyunculu olarak mı paylaşmak (çok oyunculuya kopyalamak), yoksa mevcut olan veya yeni bir çok oyunculu keşfi mi paylaşmak istiyorsun?", @@ -18,7 +18,7 @@ "Yes": "EVET", "No": "HAYIR", "StopHostingAreYouSure": "Paylaşmayı durdurmak istediğinden emin misin?\nBu tüm oyuncuların bağlantısını kesip onları ana menüye döndürecektir.", - "CopyProductUserIDToClipboard": "Keşif paylaşılıyor\nDiğer oyuncular senin aşağıdaki kullanıcı kimliğini girerek bağlanabilirler.\n{0}\nBunu panoya kopyalamak ister misin?", + "CopySteamIDToClipboard": "Keşif paylaşılıyor\nDiğer oyuncular senin aşağıdaki Steam kimliğini girerek bağlanabilirler.\n{0}\nBunu panoya kopyalamak ister misin?", "Connecting": "BAĞLANIYOR...", "OK": "TAMAM", "ServerRefusedConnection": "Sunucu bağlantıyı reddetti.\n{0}", @@ -28,7 +28,6 @@ "DLCMismatch": "DLC yüklenme durumu uyumlu değil. (İstemci:{0}, Sunucu:{1})", "GameProgressLimit": "Oyun çok fazla ilerlemiş.", "AddonMismatch": "Yüklü eklentiler uyumsuz. (İstemci:{0} eklentileri, Sunucu:{1} eklentileri)", - "IncompatibleMod": "İzin verilmeyen veya uyumsuz mod kullanılıyor. İlk bulunan mod {0}", "PlayerJoinedTheGame": "{0} katıldı!", "PlayerLeftTheGame": "{0} ayrıldı.", "PlayerWasKicked": "{0} atıldı.", diff --git a/QSB/Translations/zh_CN.json b/QSB/Translations/zh_CN.json index 79065b52a..23f69d2e1 100644 --- a/QSB/Translations/zh_CN.json +++ b/QSB/Translations/zh_CN.json @@ -5,7 +5,7 @@ "PauseMenuDisconnect": "断开连接", "PauseMenuStopHosting": "关闭服务器", "PublicIPAddress": "公共IP地址\n\n(您的多人存档数据将被覆盖)", - "ProductUserID": "用户ID\n\n(您的多人存档数据将被覆盖)", + "SteamID": "Steam ID\n\n(您的多人存档数据将被覆盖)", "Connect": "连接", "Cancel": "取消", "HostExistingOrNewOrCopy": "您想使用一个现有的多人探险存档,或是开启一个新的存档,还是把现有的单人探险存档复制到多人探险存档?", @@ -18,7 +18,7 @@ "Yes": "是", "No": "否", "StopHostingAreYouSure": "您确定要停止服务器吗?\n将会与所有人断开连接并且使他们退出至主菜单。", - "CopyProductUserIDToClipboard": "开启服务器\n其他玩家需要使用您的用户ID连接至服务器,用户ID是:\n{0}\n您想要复制到剪切板吗?", + "CopySteamIDToClipboard": "其他玩家需要使用您的Steam ID连接至服务器,Steam ID是:\n{0}\n您想要复制到剪切板吗?", "Connecting": "连接中……", "OK": "好的", "ServerRefusedConnection": "服务器拒绝了您的连接\n{0}", @@ -28,7 +28,6 @@ "DLCMismatch": "DLC安装情况不匹配。(客户端:{0},服务端:{1})", "GameProgressLimit": "游戏中时间太久了。", "AddonMismatch": "插件不匹配(客户端:{0}插件,服务端:{1}插件)", - "IncompatibleMod": "使用了不兼容/不允许的模组,检测到的第一个模组是{0}", "PlayerJoinedTheGame": "{0}加入了游戏!", "PlayerWasKicked": "{0}被踢出了游戏。", "KickedFromServer": "被踢出了游戏,理由是:{0}", diff --git a/QSB/Utility/CommandInterpreter.cs b/QSB/Utility/CommandInterpreter.cs index f402d5598..ee4a7ce82 100644 --- a/QSB/Utility/CommandInterpreter.cs +++ b/QSB/Utility/CommandInterpreter.cs @@ -1,8 +1,8 @@ using QSB.HUD; using QSB.Messaging; -using QSB.ShipSync; using QSB.ShipSync.Messages; using QSB.WorldSync; +using Steamworks; using System.Linq; using UnityEngine; @@ -25,25 +25,55 @@ public static bool InterpretCommand(string message) case "ship": ShipCommand(commandParts.Skip(1).ToArray()); break; + case "copy-id": + CopySteamID(); + break; default: - MultiplayerHUDManager.Instance.WriteMessage($"Unknown command \"{command}\".", Color.red); + WriteToChat($"Unknown command \"{command}\".", Color.red); break; } return true; } + private static void WriteToChat(string message, Color color) + { + // TODO : make italics work in chat so we can use them here + MultiplayerHUDManager.Instance.WriteMessage(message, color); + } + + public static void CopySteamID() + { + if (QSBCore.UseKcpTransport) + { + WriteToChat($"Cannot get Steam ID for KCP-hosted server.", Color.red); + return; + } + + var steamID = QSBCore.IsHost + ? SteamUser.GetSteamID().ToString() + : QSBNetworkManager.singleton.networkAddress; + + GUIUtility.systemCopyBuffer = steamID; + WriteToChat($"Copied {steamID} to the clipboard.", Color.green); + } + public static void ShipCommand(string[] arguments) { + if (!QSBCore.DebugSettings.DebugMode) + { + return; + } + var command = arguments[0]; switch (command) { - case "explode": - MultiplayerHUDManager.Instance.WriteMessage($"Blowing up the ship.", Color.green); +/* case "explode": + WriteToChat($"Blowing up the ship.", Color.green); var shipDamageController = Locator.GetShipTransform().GetComponentInChildren(); shipDamageController.Explode(); - break; + break;*/ case "repair": case "damage": var damage = command == "damage"; @@ -56,7 +86,7 @@ public static void ShipCommand(string[] arguments) default: break; } - MultiplayerHUDManager.Instance.WriteMessage($"{(damage ? "Damaging" : "Repairing")} the {arguments[1]}.", Color.green); + WriteToChat($"{(damage ? "Damaging" : "Repairing")} the {arguments[1]}.", Color.green); break; case "open-hatch": QSBWorldSync.GetUnityObject().OpenHatch(); @@ -67,7 +97,7 @@ public static void ShipCommand(string[] arguments) new HatchMessage(false).Send(); break; default: - MultiplayerHUDManager.Instance.WriteMessage($"Unknown ship command \"{command}\".", Color.red); + WriteToChat($"Unknown ship command \"{command}\".", Color.red); break; } } diff --git a/QSB/Utility/DebugActions.cs b/QSB/Utility/DebugActions.cs index 4749c8cc6..1cf23019a 100644 --- a/QSB/Utility/DebugActions.cs +++ b/QSB/Utility/DebugActions.cs @@ -1,4 +1,5 @@ using OWML.Common; +using OWML.Utils; using QSB.EchoesOfTheEye.DreamLantern; using QSB.EchoesOfTheEye.DreamLantern.WorldObjects; using QSB.ItemSync.WorldObjects.Items; @@ -10,7 +11,9 @@ using QSB.Utility.Messages; using QSB.WorldSync; using System; +using System.Collections; using System.Linq; +using QSB.EchoesOfTheEye.RaftSync.WorldObjects; using UnityEngine; using UnityEngine.InputSystem; @@ -18,7 +21,7 @@ namespace QSB.Utility; public class DebugActions : MonoBehaviour, IAddComponentOnStart { - public static Type WorldObjectSelection = typeof(QSBSocketedQuantumObject); + public static Type WorldObjectSelection = typeof(QSBRaft); private static void GoToVessel() { @@ -170,7 +173,7 @@ public void Update() var dreamLanternItem = QSBWorldSync.GetWorldObjects().First(x => x.AttachedObject._lanternType == DreamLanternType.Functioning && QSBPlayerManager.PlayerList.All(y => y.HeldItem != x) && - !x.AttachedObject.GetLanternController().IsLit() + !x.AttachedObject.GetLanternController().IsLit() // lit = someone else is holding. backup in case held item isnt initial state synced ).AttachedObject; Locator.GetToolModeSwapper().GetItemCarryTool().PickUpItemInstantly(dreamLanternItem); } @@ -200,7 +203,10 @@ public void Update() if (Keyboard.current[Key.Numpad4].wasPressedThisFrame) { - DamageShipElectricalSystem(); + if (QSBCore.IsHost) + { + StartCoroutine(SendPacketLossTest()); + } } if (Keyboard.current[Key.Numpad5].wasPressedThisFrame) @@ -242,4 +248,22 @@ public void Update() RespawnManager.Instance.RespawnSomePlayer(); } } + + const int MAX_MESSAGES = 200; + + int currentMessage = 1; + + public static int TotalMessages; + + IEnumerator SendPacketLossTest() + { + currentMessage = 1; + DebugLog.DebugWrite($"STARTING DROPPED MESSAGE TEST..."); + while (currentMessage <= MAX_MESSAGES) + { + new PacketLossTestMessage().Send(); + currentMessage++; + yield return new WaitForSeconds(0.1f); + } + } } diff --git a/QSB/Utility/DebugGUI.cs b/QSB/Utility/DebugGUI.cs index 4d2bd3a3a..4fc4b60c0 100644 --- a/QSB/Utility/DebugGUI.cs +++ b/QSB/Utility/DebugGUI.cs @@ -205,6 +205,8 @@ private static void DrawGui() WriteLine(2, $" - Ref. Sector : {(referenceSector == null ? "NULL" : referenceSector.Name)}", referenceSector == null ? Color.red : Color.white); WriteLine(2, $" - Ref. Transform : {(referenceTransform == null ? "NULL" : referenceTransform.name)}", referenceTransform == null ? Color.red : Color.white); + WriteLine(2, $" - Local Position : {player.Body.transform.localPosition}"); + WriteLine(2, $" - Position : {player.Body.transform.position}"); } } diff --git a/QSB/Utility/DebugLog.cs b/QSB/Utility/DebugLog.cs index ba6ba2945..ee3ce3613 100644 --- a/QSB/Utility/DebugLog.cs +++ b/QSB/Utility/DebugLog.cs @@ -21,32 +21,30 @@ public static void ToConsole(string message, MessageType type = MessageType.Mess message = $"[{ProcessInstanceId}] " + message; } + var @this = QSBCore.Helper != null ? QSBCore.Helper.Console : ModConsole.OwmlConsole; + var Logger = @this.GetValue("Logger"); + var _socket = @this.GetValue("_socket"); // copied from https://github.com/ow-mods/owml/blob/master/src/OWML.Logging/ModSocketOutput.cs#L33 + Logger?.Log($"{type}: {message}"); + + _socket.WriteToSocket(new ModSocketMessage + { + SenderName = "QSB", + SenderType = GetCallingType(), + Type = type, + Message = message + }); + + if (type == MessageType.Fatal) { - var Logger = ModConsole.OwmlConsole.GetValue("Logger"); - var _socket = ModConsole.OwmlConsole.GetValue("_socket"); - - Logger?.Log($"{type}: {message}"); - - _socket.WriteToSocket(new ModSocketMessage - { - SenderName = "QSB", - SenderType = GetCallingType(), - Type = type, - Message = message - }); - - if (type == MessageType.Fatal) - { - _socket.Close(); - Process.GetCurrentProcess().Kill(); - } + _socket.Close(); + Process.GetCurrentProcess().Kill(); } } public static void DebugWrite(string message, MessageType type = MessageType.Message) { - if (QSBCore.DebugSettings.DebugMode) + if (QSBCore.Helper == null || QSBCore.DebugSettings.DebugMode) { ToConsole(message, type); } diff --git a/QSB/Utility/DebugSettings.cs b/QSB/Utility/DebugSettings.cs index c1da6cca7..e1c174045 100644 --- a/QSB/Utility/DebugSettings.cs +++ b/QSB/Utility/DebugSettings.cs @@ -5,8 +5,8 @@ namespace QSB.Utility; [JsonObject(MemberSerialization.OptIn)] public class DebugSettings { - [JsonProperty("dumpWorldObjects")] - public bool DumpWorldObjects; + [JsonProperty("logQSBMessages")] + public bool LogQSBMessages; [JsonProperty("instanceIdInLogs")] public bool InstanceIdInLogs; @@ -26,6 +26,18 @@ public class DebugSettings [JsonProperty("disableLoopDeath")] public bool DisableLoopDeath; + [JsonProperty("latencySimulation")] + public int LatencySimulation; + + [JsonProperty("randomizeSkins")] + public bool RandomizeSkins; + + /// + /// Timeout in seconds + /// + [JsonProperty("timeout")] + public int Timeout = 25; + [JsonProperty("debugMode")] public bool DebugMode; @@ -41,10 +53,6 @@ public class DebugSettings private bool _drawLabels; public bool DrawLabels => DebugMode && _drawLabels; - [JsonProperty("drawQuantumVisibilityObjects")] - private bool _drawQuantumVisibilityObjects; - public bool DrawQuantumVisibilityObjects => DebugMode && _drawQuantumVisibilityObjects; - [JsonProperty("drawGhostAI")] private bool _drawGhostAI; public bool DrawGhostAI => DebugMode && _drawGhostAI; diff --git a/QSB/Utility/Deterministic/DeterministicManager.cs b/QSB/Utility/Deterministic/DeterministicManager.cs new file mode 100644 index 000000000..5b2786361 --- /dev/null +++ b/QSB/Utility/Deterministic/DeterministicManager.cs @@ -0,0 +1,86 @@ +using HarmonyLib; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using UnityEngine; + +namespace QSB.Utility.Deterministic; + +/// +/// holds parenting information used for reliably sorting objects based on path. +/// +/// NOTE: sibling indexes MAY be slightly different between vendors because of extra switch optimization gameobjects, +/// but the order is still the same, so sorting is still deterministic +/// +public static class DeterministicManager +{ + private static readonly Harmony _harmony = new(typeof(DeterministicRigidbodyPatches).FullName); + private static bool _patched; + + public static readonly Dictionary ParentCache = new(); + + public static void Init() + { + QSBSceneManager.OnPreSceneLoad += (_, _) => + { + DebugLog.DebugWrite("cleared deterministic parent cache"); + ParentCache.Clear(); + + if (!_patched) + { + _harmony.PatchAll(typeof(DeterministicRigidbodyPatches)); + _patched = true; + } + }; + } + + /// + /// unpatch DeterministicRigidbodyPatches so rigidbodies added/activated later dont get counted towards the cache. + /// also breaks with e.g. QuantumInstrument since transform is added to the cache twice (once by body and once by instrument) + /// + public static void OnWorldObjectsAdded() + { + if (_patched) + { + _harmony.UnpatchSelf(); + _patched = false; + } + } + + /// + /// only world object managers call this, to do it as early as possible to capture parents before they change + /// + public static string DeterministicPath(this Component component) + { + var sb = new StringBuilder(); + var transform = component.transform; + while (true) + { + if (!ParentCache.TryGetValue(transform, out var data)) + { + data = (transform.GetSiblingIndex(), transform.parent); + ParentCache.Add(transform, data); + } + + if (!data.Parent) + { + break; + } + + sb.Append(transform.name); + sb.Append(' '); + sb.Append(data.SiblingIndex); + sb.Append(' '); + transform = data.Parent; + } + + sb.Append(transform.name); + return sb.ToString(); + } + + /// + /// only world object managers call this, to do it as early as possible to capture parents before they change + /// + public static IEnumerable SortDeterministic(this IEnumerable components) where T : Component + => components.OrderBy(DeterministicPath); +} diff --git a/QSB/Utility/Deterministic/DeterministicRigidbodyPatches.cs b/QSB/Utility/Deterministic/DeterministicRigidbodyPatches.cs new file mode 100644 index 000000000..6742a4415 --- /dev/null +++ b/QSB/Utility/Deterministic/DeterministicRigidbodyPatches.cs @@ -0,0 +1,226 @@ +using HarmonyLib; +using OWML.Utils; +using QSB.Patches; +using System.Collections.Generic; +using UnityEngine; + +namespace QSB.Utility.Deterministic; + +/// +/// used to capture the true path of a rigidbody before it changes parent +/// +[HarmonyPatch(typeof(OWRigidbody))] +public static class DeterministicRigidbodyPatches +{ + /// + /// changing the parent has to be deferred until Start to preserve the sibling index. + /// for example, anglerfish bodies all share the same parent, so unparenting one clobbers the sibling index of all the others. + /// + private static readonly Dictionary _setParentQueue = new(); + + [HarmonyPrefix] + [HarmonyPatch(nameof(OWRigidbody.Awake))] + private static bool Awake(OWRigidbody __instance) + { + __instance._transform = __instance.transform; + + // ADDED + DeterministicManager.ParentCache.Add(__instance._transform, (__instance._transform.GetSiblingIndex(), __instance._transform.parent)); + + if (!__instance._scaleRoot) + { + __instance._scaleRoot = __instance._transform; + } + + CenterOfTheUniverse.TrackRigidbody(__instance); + __instance._offsetApplier = __instance.gameObject.GetAddComponent(); + __instance._offsetApplier.Init(__instance); + if (__instance._simulateInSector) + { + __instance._simulateInSector.OnSectorOccupantsUpdated += __instance.OnSectorOccupantsUpdated; + } + + __instance._origParent = __instance._transform.parent; + __instance._origParentBody = __instance._origParent ? __instance._origParent.GetAttachedOWRigidbody() : null; + + // ADDED + if (__instance._transform.parent) + { + _setParentQueue[__instance] = null; + } + + // REMOVED + /*if (__instance._transform.parent != null) + { + __instance._transform.parent = null; + }*/ + + __instance._rigidbody = __instance.GetRequiredComponent(); + __instance._rigidbody.interpolation = RigidbodyInterpolation.None; + + if (!__instance._autoGenerateCenterOfMass) + { + __instance._rigidbody.centerOfMass = __instance._centerOfMass; + } + + if (__instance.IsSimulatedKinematic()) + { + __instance.EnableKinematicSimulation(); + } + + __instance._origCenterOfMass = __instance.RunningKinematicSimulation() ? __instance._kinematicRigidbody.centerOfMass : __instance._rigidbody.centerOfMass; + __instance._referenceFrame = new ReferenceFrame(__instance); + + return false; + } + + [HarmonyPrefix] + [HarmonyPatch(nameof(OWRigidbody.Start))] + private static void Start(OWRigidbody __instance) + { + if (_setParentQueue.TryGetValue(__instance, out var parent)) + { + __instance._transform.parent = parent; + _setParentQueue.Remove(__instance); + } + } + + [HarmonyPrefix] + [HarmonyPatch(nameof(OWRigidbody.OnDestroy))] + private static void OnDestroy(OWRigidbody __instance) + { + DeterministicManager.ParentCache.Remove(__instance._transform); + _setParentQueue.Remove(__instance); + } + + [HarmonyPrefix] + [HarmonyPatch(nameof(OWRigidbody.Suspend), typeof(Transform), typeof(OWRigidbody))] + private static bool Suspend(OWRigidbody __instance, Transform suspensionParent, OWRigidbody suspensionBody) + { + if (!__instance._suspended || __instance._unsuspendNextUpdate) + { + __instance._suspensionBody = suspensionBody; + var direction = __instance.GetVelocity() - suspensionBody.GetPointVelocity(__instance._transform.position); + __instance._cachedRelativeVelocity = suspensionBody.transform.InverseTransformDirection(direction); + __instance._cachedAngularVelocity = __instance.RunningKinematicSimulation() ? __instance._kinematicRigidbody.angularVelocity : __instance._rigidbody.angularVelocity; + __instance.enabled = false; + __instance._offsetApplier.enabled = false; + + if (__instance.RunningKinematicSimulation()) + { + __instance._kinematicRigidbody.enabled = false; + } + else + { + __instance.MakeKinematic(); + } + + // ADDED + if (_setParentQueue.ContainsKey(__instance)) + { + _setParentQueue[__instance] = suspensionParent; + } + else + { + __instance._transform.parent = suspensionParent; + } + + // REMOVED + // __instance._transform.parent = suspensionParent; + + __instance._suspended = true; + __instance._unsuspendNextUpdate = false; + if (!Physics.autoSyncTransforms) + { + Physics.SyncTransforms(); + } + + if (__instance._childColliders == null) + { + __instance._childColliders = __instance.GetComponentsInChildren(); + + // CLEANED + foreach (var childCollider in __instance._childColliders) + { + childCollider.gameObject.GetAddComponent().ListenForParentBodySuspension(); + } + } + + __instance.RaiseEvent(nameof(__instance.OnSuspendOWRigidbody), __instance); + } + + return false; + } + + [HarmonyPrefix] + [HarmonyPatch(nameof(OWRigidbody.ChangeSuspensionBody))] + private static bool ChangeSuspensionBody(OWRigidbody __instance, OWRigidbody newSuspensionBody) + { + if (__instance._suspended) + { + __instance._cachedRelativeVelocity = Vector3.zero; + __instance._suspensionBody = newSuspensionBody; + + // ADDED + if (_setParentQueue.ContainsKey(__instance)) + { + _setParentQueue[__instance] = newSuspensionBody.transform; + } + else + { + __instance._transform.parent = newSuspensionBody.transform; + } + + // REMOVED + // __instance._transform.parent = newSuspensionBody.transform; + } + + return false; + } + + [HarmonyPrefix] + [HarmonyPatch(nameof(OWRigidbody.UnsuspendImmediate))] + private static bool UnsuspendImmediate(OWRigidbody __instance, bool restoreCachedVelocity) + { + if (__instance._suspended) + { + if (__instance.RunningKinematicSimulation()) + { + __instance._kinematicRigidbody.enabled = true; + } + else + { + __instance.MakeNonKinematic(); + } + + __instance.enabled = true; + + // ADDED + if (_setParentQueue.ContainsKey(__instance)) + { + _setParentQueue[__instance] = null; + } + else + { + __instance._transform.parent = null; + } + + // REMOVED + // __instance._transform.parent = null; + + if (!Physics.autoSyncTransforms) + { + Physics.SyncTransforms(); + } + + var cachedVelocity = restoreCachedVelocity ? __instance._suspensionBody.transform.TransformDirection(__instance._cachedRelativeVelocity) : Vector3.zero; + __instance.SetVelocity(__instance._suspensionBody.GetPointVelocity(__instance._transform.position) + cachedVelocity); + __instance.SetAngularVelocity(restoreCachedVelocity ? __instance._cachedAngularVelocity : Vector3.zero); + __instance._suspended = false; + __instance._suspensionBody = null; + __instance.RaiseEvent(nameof(__instance.OnUnsuspendOWRigidbody), __instance); + } + + return false; + } +} diff --git a/QSB/Utility/DeterministicManager.cs b/QSB/Utility/DeterministicManager.cs deleted file mode 100644 index d6a71aef7..000000000 --- a/QSB/Utility/DeterministicManager.cs +++ /dev/null @@ -1,305 +0,0 @@ -using HarmonyLib; -using QSB.WorldSync; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using UnityEngine; - -namespace QSB.Utility; - -/// -/// TODO make this only do cache clearing on pre scene load when HOSTING instead of just all the time -/// -public static class DeterministicManager -{ - private static readonly Harmony _harmony = new(typeof(DeterministicManager).FullName); - private static bool _patched; - - private static readonly Dictionary _cache = new(); - - public static void Init() => - QSBSceneManager.OnPreSceneLoad += (_, _) => - { - DebugLog.DebugWrite("cleared cache"); - _cache.Clear(); - - if (!_patched) - { - _harmony.PatchAll(typeof(OWRigidbodyPatches)); - _patched = true; - } - }; - - public static void OnWorldObjectsAdded() - { - if (QSBCore.DebugSettings.DumpWorldObjects) - { - using (var file = File.CreateText(Path.Combine(QSBCore.Helper.Manifest.ModFolderPath, $"[{DebugLog.ProcessInstanceId}] world objects.csv"))) - { - file.WriteLine("world object,deterministic path"); - foreach (var worldObject in QSBWorldSync.GetWorldObjects()) - { - file.Write('"'); - file.Write(worldObject); - file.Write('"'); - file.Write(','); - file.Write('"'); - file.Write(worldObject.AttachedObject.DeterministicPath().Replace("\"", "\"\"")); - file.Write('"'); - file.WriteLine(); - } - } - - using (var file = File.CreateText(Path.Combine(QSBCore.Helper.Manifest.ModFolderPath, $"[{DebugLog.ProcessInstanceId}] cache.csv"))) - { - file.WriteLine("name,instance id,sibling index,parent,parent instance id"); - foreach (var (transform, (siblingIndex, parent)) in _cache) - { - file.Write('"'); - file.Write(transform.name.Replace("\"", "\"\"")); - file.Write('"'); - file.Write(','); - file.Write(transform.GetInstanceID()); - file.Write(','); - file.Write(siblingIndex); - file.Write(','); - file.Write('"'); - file.Write(parent ? parent.name.Replace("\"", "\"\"") : default); - file.Write('"'); - file.Write(','); - file.Write(parent ? parent.GetInstanceID() : default); - file.WriteLine(); - } - } - } - - DebugLog.DebugWrite($"cleared cache of {_cache.Count} entries"); - _cache.Clear(); - - if (_patched) - { - _harmony.UnpatchSelf(); - _patched = false; - } - } - - [HarmonyPatch(typeof(OWRigidbody))] - private static class OWRigidbodyPatches - { - private static readonly Dictionary _setParentQueue = new(); - - [HarmonyPrefix] - [HarmonyPatch(nameof(OWRigidbody.Awake))] - private static bool Awake(OWRigidbody __instance) - { - __instance._transform = __instance.transform; - _cache.Add(__instance._transform, (__instance._transform.GetSiblingIndex(), __instance._transform.parent)); - if (!__instance._scaleRoot) - { - __instance._scaleRoot = __instance._transform; - } - - CenterOfTheUniverse.TrackRigidbody(__instance); - __instance._offsetApplier = __instance.gameObject.GetAddComponent(); - __instance._offsetApplier.Init(__instance); - if (__instance._simulateInSector) - { - __instance._simulateInSector.OnSectorOccupantsUpdated += __instance.OnSectorOccupantsUpdated; - } - - __instance._origParent = __instance._transform.parent; - __instance._origParentBody = __instance._origParent ? __instance._origParent.GetAttachedOWRigidbody() : null; - if (__instance._transform.parent) - { - _setParentQueue[__instance] = null; - } - - __instance._rigidbody = __instance.GetRequiredComponent(); - __instance._rigidbody.interpolation = RigidbodyInterpolation.None; - if (!__instance._autoGenerateCenterOfMass) - { - __instance._rigidbody.centerOfMass = __instance._centerOfMass; - } - - if (__instance.IsSimulatedKinematic()) - { - __instance.EnableKinematicSimulation(); - } - - __instance._origCenterOfMass = __instance.RunningKinematicSimulation() ? __instance._kinematicRigidbody.centerOfMass : __instance._rigidbody.centerOfMass; - __instance._referenceFrame = new ReferenceFrame(__instance); - return false; - } - - [HarmonyPrefix] - [HarmonyPatch(nameof(OWRigidbody.Start))] - private static void Start(OWRigidbody __instance) - { - if (_setParentQueue.TryGetValue(__instance, out var parent)) - { - __instance._transform.parent = parent; - _setParentQueue.Remove(__instance); - } - } - - [HarmonyPrefix] - [HarmonyPatch(nameof(OWRigidbody.OnDestroy))] - private static void OnDestroy(OWRigidbody __instance) - { - _cache.Remove(__instance._transform); - _setParentQueue.Remove(__instance); - } - - [HarmonyPrefix] - [HarmonyPatch(nameof(OWRigidbody.Suspend), typeof(Transform), typeof(OWRigidbody))] - private static bool Suspend(OWRigidbody __instance, Transform suspensionParent, OWRigidbody suspensionBody) - { - if (!__instance._suspended || __instance._unsuspendNextUpdate) - { - __instance._suspensionBody = suspensionBody; - var direction = __instance.GetVelocity() - suspensionBody.GetPointVelocity(__instance._transform.position); - __instance._cachedRelativeVelocity = suspensionBody.transform.InverseTransformDirection(direction); - __instance._cachedAngularVelocity = __instance.RunningKinematicSimulation() ? __instance._kinematicRigidbody.angularVelocity : __instance._rigidbody.angularVelocity; - __instance.enabled = false; - __instance._offsetApplier.enabled = false; - if (__instance.RunningKinematicSimulation()) - { - __instance._kinematicRigidbody.enabled = false; - } - else - { - __instance.MakeKinematic(); - } - - if (_setParentQueue.ContainsKey(__instance)) - { - _setParentQueue[__instance] = suspensionParent; - } - else - { - __instance._transform.parent = suspensionParent; - } - - __instance._suspended = true; - __instance._unsuspendNextUpdate = false; - if (!Physics.autoSyncTransforms) - { - Physics.SyncTransforms(); - } - - if (__instance._childColliders == null) - { - __instance._childColliders = __instance.GetComponentsInChildren(); - foreach (var childCollider in __instance._childColliders) - { - childCollider.gameObject.GetAddComponent().ListenForParentBodySuspension(); - } - } - - __instance.RaiseEvent(nameof(__instance.OnSuspendOWRigidbody), __instance); - } - - return false; - } - - [HarmonyPrefix] - [HarmonyPatch(nameof(OWRigidbody.ChangeSuspensionBody))] - private static bool ChangeSuspensionBody(OWRigidbody __instance, OWRigidbody newSuspensionBody) - { - if (__instance._suspended) - { - __instance._cachedRelativeVelocity = Vector3.zero; - __instance._suspensionBody = newSuspensionBody; - if (_setParentQueue.ContainsKey(__instance)) - { - _setParentQueue[__instance] = newSuspensionBody.transform; - } - else - { - __instance._transform.parent = newSuspensionBody.transform; - } - } - - return false; - } - - [HarmonyPrefix] - [HarmonyPatch(nameof(OWRigidbody.UnsuspendImmediate))] - private static bool UnsuspendImmediate(OWRigidbody __instance, bool restoreCachedVelocity) - { - if (__instance._suspended) - { - if (__instance.RunningKinematicSimulation()) - { - __instance._kinematicRigidbody.enabled = true; - } - else - { - __instance.MakeNonKinematic(); - } - - __instance.enabled = true; - if (_setParentQueue.ContainsKey(__instance)) - { - _setParentQueue[__instance] = null; - } - else - { - __instance._transform.parent = null; - } - - if (!Physics.autoSyncTransforms) - { - Physics.SyncTransforms(); - } - - var cachedVelocity = restoreCachedVelocity ? __instance._suspensionBody.transform.TransformDirection(__instance._cachedRelativeVelocity) : Vector3.zero; - __instance.SetVelocity(__instance._suspensionBody.GetPointVelocity(__instance._transform.position) + cachedVelocity); - __instance.SetAngularVelocity(restoreCachedVelocity ? __instance._cachedAngularVelocity : Vector3.zero); - __instance._suspended = false; - __instance._suspensionBody = null; - __instance.RaiseEvent(nameof(__instance.OnUnsuspendOWRigidbody), __instance); - } - - return false; - } - } - - /// - /// only call this before world objects added - /// - public static string DeterministicPath(this Component component) - { - var sb = new StringBuilder(); - var transform = component.transform; - while (true) - { - if (!_cache.TryGetValue(transform, out var data)) - { - data = (transform.GetSiblingIndex(), transform.parent); - _cache.Add(transform, data); - } - - if (!data.Parent) - { - break; - } - - sb.Append(transform.name); - sb.Append(' '); - sb.Append(data.SiblingIndex); - sb.Append(' '); - transform = data.Parent; - } - - sb.Append(transform.name); - return sb.ToString(); - } - - /// - /// only call this before world objects added - /// - public static IEnumerable SortDeterministic(this IEnumerable components) where T : Component - => components.OrderBy(DeterministicPath); -} \ No newline at end of file diff --git a/QSB/Utility/Extensions.cs b/QSB/Utility/Extensions.cs index 68efc695d..c7f415dc5 100644 --- a/QSB/Utility/Extensions.cs +++ b/QSB/Utility/Extensions.cs @@ -4,8 +4,10 @@ using QSB.Player; using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Reflection; +using System.Runtime.Serialization.Formatters.Binary; using System.Text; using UnityEngine; using Object = UnityEngine.Object; @@ -69,21 +71,6 @@ public static void SpawnWithServerOwnership(this GameObject go) => #region C# - public static void SafeInvoke(this MulticastDelegate multicast, params object[] args) - { - foreach (var del in multicast.GetInvocationList()) - { - try - { - del.DynamicInvoke(args); - } - catch (TargetInvocationException ex) - { - DebugLog.ToConsole($"Error invoking delegate! {ex.InnerException}", MessageType.Error); - } - } - } - public static float Map(this float value, float inputFrom, float inputTo, float outputFrom, float outputTo, bool clamp) { var mappedValue = (value - inputFrom) / (inputTo - inputFrom) * (outputTo - outputFrom) + outputFrom; @@ -163,30 +150,21 @@ public static TSource MaxBy(this IEnumerable source, Fun public static bool IsInRange(this IList list, int index) => index >= 0 && index < list.Count; - public static void RaiseEvent(this T instance, string eventName, params object[] args) + public static IEnumerable GetDerivedTypes(this Type type) { - const BindingFlags flags = BindingFlags.Instance - | BindingFlags.Static - | BindingFlags.Public - | BindingFlags.NonPublic - | BindingFlags.DeclaredOnly; - if (typeof(T) - .GetField(eventName, flags)? - .GetValue(instance) is not MulticastDelegate multiDelegate) + var assemblies = QSBCore.Addons.Values + .Select(x => x.GetType().Assembly) + .Append(type.Assembly); + + if (QSBCore.QSBNHAssembly != null) { - return; + assemblies = assemblies.Append(QSBCore.QSBNHAssembly); } - multiDelegate.SafeInvoke(args); - } - - public static IEnumerable GetDerivedTypes(this Type type) => - QSBCore.Addons.Values - .Select(x => x.GetType().Assembly) - .Append(type.Assembly) - .SelectMany(x => x.GetTypes()) + return assemblies.SelectMany(x => x.GetTypes()) .Where(x => !x.IsInterface && !x.IsAbstract && type.IsAssignableFrom(x)) .OrderBy(x => x.FullName); + } public static Guid ToGuid(this int value) { @@ -237,5 +215,44 @@ public static string GetMD5Hash(this IEnumerable list) return sb.ToString(); } + public static string GetMD5Hash(this string input) + { + using var md5 = System.Security.Cryptography.MD5.Create(); + + var bytes = Encoding.ASCII.GetBytes(input); + var hashBytes = md5.ComputeHash(bytes); + + var sb = new StringBuilder(); + for (var i = 0; i < hashBytes.Length; i++) + { + sb.Append(hashBytes[i].ToString("X2")); + } + + return sb.ToString(); + } + + /// + /// only works for c# serializable objects + /// + public static byte[] ToBytes(this object obj) + { + using var ms = new MemoryStream(); + var bf = new BinaryFormatter(); + bf.Serialize(ms, obj); + var bytes = ms.ToArray(); + return bytes; + } + + /// + /// only works for c# serializable objects + /// + public static object ToObject(this byte[] bytes) + { + using var ms = new MemoryStream(bytes); + var bf = new BinaryFormatter(); + var obj = bf.Deserialize(ms); + return obj; + } + #endregion } \ No newline at end of file diff --git a/QSB/Utility/Messages/PacketLossTestMessage.cs b/QSB/Utility/Messages/PacketLossTestMessage.cs new file mode 100644 index 000000000..ce0750b09 --- /dev/null +++ b/QSB/Utility/Messages/PacketLossTestMessage.cs @@ -0,0 +1,23 @@ +using QSB.Messaging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace QSB.Utility.Messages; + +internal class PacketLossTestMessage : QSBMessage +{ + public override void OnReceiveLocal() + { + DebugActions.TotalMessages += 1; + DebugLog.DebugWrite($"Total test messages sent is now {DebugActions.TotalMessages}"); + } + + public override void OnReceiveRemote() + { + DebugActions.TotalMessages += 1; + DebugLog.DebugWrite($"Total test messages recieved is now {DebugActions.TotalMessages}"); + } +} diff --git a/QSB/Utility/QSBNetworkBehaviour.cs b/QSB/Utility/QSBNetworkBehaviour.cs index cf447e157..e21077174 100644 --- a/QSB/Utility/QSBNetworkBehaviour.cs +++ b/QSB/Utility/QSBNetworkBehaviour.cs @@ -1,5 +1,6 @@ using Mirror; using QSB.WorldSync; +using QSB.WorldSync.Messages; using System; namespace QSB.Utility; diff --git a/QSB/WorldSync/HashErrorAnalysis.cs b/QSB/WorldSync/HashErrorAnalysis.cs new file mode 100644 index 000000000..5dea244b8 --- /dev/null +++ b/QSB/WorldSync/HashErrorAnalysis.cs @@ -0,0 +1,66 @@ +using System.Collections.Generic; +using System.Linq; +using OWML.Common; +using QSB.Utility; +using QSB.Player.Messages; +using QSB.Messaging; +using QSB.Utility.Deterministic; + +namespace QSB.WorldSync; + +public class HashErrorAnalysis +{ + public static Dictionary Instances = new(); + + private readonly string _managerName; + + private readonly List<(string hash, string path)> _paths = new(); + + public HashErrorAnalysis(string managerName) => _managerName = managerName; + + public void OnReceiveMessage(string deterministicPath) => _paths.Add((deterministicPath.GetMD5Hash(), deterministicPath)); + + public void AllDataSent(uint from) + { + var serverObjects = QSBWorldSync.GetWorldObjectsFromManager(_managerName); + + var serverDetPaths = serverObjects.Select(x => x.AttachedObject.DeterministicPath()); + var serverDetPathDict = serverDetPaths.Select(path => (path.GetMD5Hash(), path)).ToList<(string hash, string path)>(); + + var serverDoesNotHave = new List(); + var clientDoesNotHave = new List(); + + foreach (var (hash, path) in serverDetPathDict) + { + if (!_paths.Any(x => x.hash == hash)) + { + // client does not contain something from the server + clientDoesNotHave.Add(path); + } + } + + foreach (var (hash, path) in _paths) + { + if (!serverDetPathDict.Any(x => x.hash == hash)) + { + // client does not contain something from the server + serverDoesNotHave.Add(path); + } + } + + DebugLog.ToConsole($"{_managerName} - Client is missing :", MessageType.Error); + foreach (var item in clientDoesNotHave) + { + DebugLog.ToConsole($"- {item}", MessageType.Error); + } + + DebugLog.ToConsole($"{_managerName} - Client has extra :", MessageType.Error); + foreach (var item in serverDoesNotHave) + { + DebugLog.ToConsole($"- {item}", MessageType.Error); + } + Instances.Remove(_managerName); + + new PlayerKickMessage(from, $"WorldObject hash error for {_managerName}").Send(); + } +} diff --git a/QSB/WorldSync/Messages/DataDumpFinishedMessage.cs b/QSB/WorldSync/Messages/DataDumpFinishedMessage.cs new file mode 100644 index 000000000..b8edc5ef9 --- /dev/null +++ b/QSB/WorldSync/Messages/DataDumpFinishedMessage.cs @@ -0,0 +1,10 @@ +using QSB.Messaging; + +namespace QSB.WorldSync.Messages; + +public class DataDumpFinishedMessage : QSBMessage +{ + public DataDumpFinishedMessage(string managerName) : base(managerName) => To = 0; + + public override void OnReceiveRemote() => HashErrorAnalysis.Instances[Data].AllDataSent(From); +} diff --git a/QSB/WorldSync/Messages/HashCheckSucceededMessage.cs b/QSB/WorldSync/Messages/HashCheckSucceededMessage.cs new file mode 100644 index 000000000..c96b49771 --- /dev/null +++ b/QSB/WorldSync/Messages/HashCheckSucceededMessage.cs @@ -0,0 +1,8 @@ +using QSB.Messaging; + +namespace QSB.WorldSync.Messages; + +internal class HashCheckSucceededMessage : QSBMessage +{ + +} \ No newline at end of file diff --git a/QSB/WorldSync/Messages/RequestHashBreakdownMessage.cs b/QSB/WorldSync/Messages/RequestHashBreakdownMessage.cs new file mode 100644 index 000000000..f10fbef59 --- /dev/null +++ b/QSB/WorldSync/Messages/RequestHashBreakdownMessage.cs @@ -0,0 +1,27 @@ +using OWML.Common; +using QSB.Messaging; +using QSB.Utility; + +namespace QSB.WorldSync.Messages; + +/// +/// Sent to clients from the server when a client has an incorrect WorldObject hash. +/// +internal class RequestHashBreakdownMessage : QSBMessage +{ + public RequestHashBreakdownMessage(string managerName) : base(managerName) { } + + public override void OnReceiveRemote() + { + DebugLog.ToConsole($"Received RequestHashBreakdownMessage for {Data}", MessageType.Error); + var objects = QSBWorldSync.GetWorldObjectsFromManager(Data); + + foreach (var worldObject in objects) + { + new WorldObjectInfoMessage(worldObject, Data).Send(); + } + + DebugLog.ToConsole("- Sending finished message.", MessageType.Error); + new DataDumpFinishedMessage(Data).Send(); + } +} diff --git a/QSB/WorldSync/Messages/WorldObjectInfoMessage.cs b/QSB/WorldSync/Messages/WorldObjectInfoMessage.cs new file mode 100644 index 000000000..dc79916ba --- /dev/null +++ b/QSB/WorldSync/Messages/WorldObjectInfoMessage.cs @@ -0,0 +1,17 @@ +using QSB.Messaging; +using QSB.Utility.Deterministic; + +namespace QSB.WorldSync.Messages; + +/// +/// Sent by clients to the server after receiving a RequestHashBreakdown message. +/// +public class WorldObjectInfoMessage : QSBMessage<(string fullPath, string managerName)> +{ + public WorldObjectInfoMessage(IWorldObject obj, string managerName) : base((obj.AttachedObject.DeterministicPath(), managerName)) => To = 0; + + public override void OnReceiveRemote() + { + HashErrorAnalysis.Instances[Data.managerName].OnReceiveMessage(Data.fullPath); + } +} diff --git a/QSB/WorldSync/WorldObjectsHashMessage.cs b/QSB/WorldSync/Messages/WorldObjectsHashMessage.cs similarity index 56% rename from QSB/WorldSync/WorldObjectsHashMessage.cs rename to QSB/WorldSync/Messages/WorldObjectsHashMessage.cs index 45905df5a..b58c1e243 100644 --- a/QSB/WorldSync/WorldObjectsHashMessage.cs +++ b/QSB/WorldSync/Messages/WorldObjectsHashMessage.cs @@ -3,7 +3,7 @@ using QSB.Player.Messages; using QSB.Utility; -namespace QSB.WorldSync; +namespace QSB.WorldSync.Messages; /// /// sends QSBWorldSync.WorldObjectsHash to the server for sanity checking @@ -21,8 +21,14 @@ public override void OnReceiveRemote() if (hash != Data.hash) { // oh fuck oh no oh god - DebugLog.ToConsole($"Kicking {From} because their WorldObjects hash for {Data.managerName} is wrong. (Server:{hash} count:{count}, Client:{Data.hash} count:{Data.count})", MessageType.Error); - new PlayerKickMessage(From, $"WorldObject hash error for {Data.managerName}. (Server:{hash} count:{count}, Client:{Data.hash}, count:{Data.count})").Send(); + /*DebugLog.ToConsole($"Kicking {From} because their WorldObjects hash for {Data.managerName} is wrong. (Server:{hash} count:{count}, Client:{Data.hash} count:{Data.count})", MessageType.Error); + new PlayerKickMessage(From, $"WorldObject hash error for {Data.managerName}. (Server:{hash} count:{count}, Client:{Data.hash}, count:{Data.count})").Send();*/ + + DebugLog.ToConsole($"{From} has an incorrect hash for {Data.managerName}. (S:{hash}:{count}, C:{Data.hash}-{Data.count}) Requesting data for analysis...", MessageType.Error); + + HashErrorAnalysis.Instances.Add(Data.managerName, new HashErrorAnalysis(Data.managerName)); + + new RequestHashBreakdownMessage(Data.managerName) {To = From}.Send(); } }); } diff --git a/QSB/WorldSync/MiscManager.cs b/QSB/WorldSync/MiscManager.cs index 5d69b7b9e..800101ff9 100644 --- a/QSB/WorldSync/MiscManager.cs +++ b/QSB/WorldSync/MiscManager.cs @@ -1,5 +1,5 @@ using Cysharp.Threading.Tasks; -using QSB.Utility; +using QSB.Utility.Deterministic; using System.Linq; using System.Threading; diff --git a/QSB/WorldSync/QSBWorldSync.cs b/QSB/WorldSync/QSBWorldSync.cs index 25e12f6fd..bb806126e 100644 --- a/QSB/WorldSync/QSBWorldSync.cs +++ b/QSB/WorldSync/QSBWorldSync.cs @@ -8,7 +8,9 @@ using QSB.Player.TransformSync; using QSB.TriggerSync.WorldObjects; using QSB.Utility; +using QSB.Utility.Deterministic; using QSB.Utility.LinkedWorldObject; +using QSB.WorldSync.Messages; using System; using System.Collections.Generic; using System.Diagnostics; @@ -229,6 +231,8 @@ private static void GameReset() public static IEnumerable GetWorldObjects() => WorldObjects; + public static IEnumerable GetWorldObjectsFromManager(string managerName) => _managerToBuiltObjects[managerName]; + public static IEnumerable GetWorldObjects() where TWorldObject : IWorldObject => WorldObjects.OfType(); diff --git a/QSB/ZeroGCaveSync/Patches/ZeroGCavePatches.cs b/QSB/ZeroGCaveSync/Patches/ZeroGCavePatches.cs index ccc8043ae..6054dc632 100644 --- a/QSB/ZeroGCaveSync/Patches/ZeroGCavePatches.cs +++ b/QSB/ZeroGCaveSync/Patches/ZeroGCavePatches.cs @@ -1,4 +1,5 @@ using HarmonyLib; +using OWML.Utils; using QSB.Messaging; using QSB.Patches; using QSB.Utility; diff --git a/QSB/ZeroGCaveSync/WorldObjects/QSBSatelliteNode.cs b/QSB/ZeroGCaveSync/WorldObjects/QSBSatelliteNode.cs index 44d37c00b..e8761479a 100644 --- a/QSB/ZeroGCaveSync/WorldObjects/QSBSatelliteNode.cs +++ b/QSB/ZeroGCaveSync/WorldObjects/QSBSatelliteNode.cs @@ -1,4 +1,5 @@ -using QSB.Utility; +using QSB.Utility; +using OWML.Utils; using QSB.WorldSync; namespace QSB.ZeroGCaveSync.WorldObjects; diff --git a/QSB/default-config.json b/QSB/default-config.json index c80d77fc5..306a1221d 100644 --- a/QSB/default-config.json +++ b/QSB/default-config.json @@ -2,6 +2,20 @@ "$schema": "https://raw.githubusercontent.com/ow-mods/owml/master/schemas/config_schema.json", "enabled": true, "settings": { + "skinType": { + "title": "Skin Variation", + "type": "selector", + "options": [ "Default", "Type 1", "Type 2", "Type 3", "Type 4", "Type 5", "Type 6", "Type 7", "Type 8", "Type 9", "Type 10", "Type 11", "Type 12", "Type 13", "Type 14", "Type 15", "Type 16", "Type 17" ], + "value": "Default", + "tooltip": "The variation of skin texture to use for your unsuited model." + }, + "jetpackType": { + "title": "Jetpack Variation", + "type": "selector", + "options": [ "Orange", "Yellow", "Red", "Pink", "Purple", "Dark Blue", "Light Blue", "Cyan", "Green" ], + "value": "Orange", + "tooltip": "The variation of texture to use for the bedroll on your suited model. " + }, "useKcpTransport": { "title": "Use KCP Transport", "type": "toggle", @@ -14,12 +28,6 @@ "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", diff --git a/QSB/manifest.json b/QSB/manifest.json index 5733ad379..7211c7371 100644 --- a/QSB/manifest.json +++ b/QSB/manifest.json @@ -1,15 +1,20 @@ { + "$schema": "https://raw.githubusercontent.com/ow-mods/owml/master/schemas/manifest_schema.json", "filename": "QSB.dll", "author": "Nebula, John, Alek, & Rai", "name": "Quantum Space Buddies", - "warning": { - "title": "Follow these steps before playing multiplayer :", - "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.30.1", - "owmlVersion": "2.9.5", + "version": "1.1.1", + "owmlVersion": "2.11.1", "dependencies": [ "_nebula.MenuFramework", "JohnCorby.VanillaFix" ], "pathsToPreserve": [ "debugsettings.json" ], - "requireLatestVersion": true + "conflicts": [ + "Vesper.AutoResume", + "Vesper.OuterWildsMMO", + "_nebula.StopTime", + "PacificEngine.OW_CommonResources" + ], + "requireLatestVersion": true, + "patcher": "QSBPatcher.exe", + "donateLinks": [ "https://www.paypal.me/nebula2056", "https://www.paypal.me/johncorby" ] } diff --git a/QSBPatcher/QSBPatcher.cs b/QSBPatcher/QSBPatcher.cs new file mode 100644 index 000000000..a324deece --- /dev/null +++ b/QSBPatcher/QSBPatcher.cs @@ -0,0 +1,45 @@ +using System; +using System.IO; + +namespace QSBPatcher; + +public static class QSBPatcher +{ + public static void Main(string[] args) + { + var basePath = args.Length > 0 ? args[0] : "."; + var gamePath = AppDomain.CurrentDomain.BaseDirectory; + + var steamDLLPath = Path.Combine(basePath, "com.rlabrecque.steamworks.net.dll"); + + var managedPath = Path.Combine(gamePath, GetDataPath(gamePath), "Managed"); + + File.Copy(steamDLLPath, Path.Combine(managedPath, "com.rlabrecque.steamworks.net.dll"), true); + } + + private static string GetDataDirectoryName() + { + var gamePath = AppDomain.CurrentDomain.BaseDirectory; + return $"{GetExecutableName(gamePath)}_Data"; + } + + private static string GetDataPath(string gamePath) + { + return Path.Combine(gamePath, $"{GetDataDirectoryName()}"); + } + + private static string GetExecutableName(string gamePath) + { + var executableNames = new[] { "Outer Wilds.exe", "OuterWilds.exe" }; + foreach (var executableName in executableNames) + { + var executablePath = Path.Combine(gamePath, executableName); + if (File.Exists(executablePath)) + { + return Path.GetFileNameWithoutExtension(executablePath); + } + } + + throw new FileNotFoundException($"Outer Wilds exe file not found in {gamePath}"); + } +} diff --git a/QSBPatcher/QSBPatcher.csproj b/QSBPatcher/QSBPatcher.csproj new file mode 100644 index 000000000..bd73382a6 --- /dev/null +++ b/QSBPatcher/QSBPatcher.csproj @@ -0,0 +1,12 @@ + + + Exe + QSB Patcher + QSB Patcher + QSB Patcher + Copies steamworks into the game for non-steam vendors + William Corby, Henry Pointer + William Corby, Henry Pointer + Copyright © William Corby, Henry Pointer 2022-2024 + + diff --git a/README.md b/README.md index 9c6c82aff..ab9f8eb8e 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,6 @@ ![GitHub release (latest by date)](https://img.shields.io/github/downloads/misternebula/quantum-space-buddies/latest/total?style=flat-square) ![GitHub last commit (branch)](https://img.shields.io/github/last-commit/misternebula/quantum-space-buddies/dev?label=last%20commit%20to%20dev&style=flat-square) -[![Support on Patreon](https://img.shields.io/badge/dynamic/json?style=for-the-badge&color=%23e85b46&label=Patreon&query=data.attributes.patron_count&suffix=%20patrons&url=https%3A%2F%2Fwww.patreon.com%2Fapi%2Fcampaigns%2F8528628&logo=patreon)](https://www.patreon.com/qsb) [![Donate with PayPal](https://img.shields.io/badge/PayPal-Donate%20(nebula)-blue?style=for-the-badge&color=blue&logo=paypal)](https://www.paypal.com/paypalme/nebula2056/5) [![Donate with PayPal](https://img.shields.io/badge/PayPal-Donate%20(johncorby)-blue?style=for-the-badge&color=blue&logo=paypal)](https://www.paypal.com/paypalme/johncorby/5) @@ -19,29 +18,25 @@ Spoilers within! ### Easy installation (recommended) -- [Install the Outer Wilds Mod Manager](https://outerwildsmods.com/mod-manager/); -- Install Quantum Space Buddies from the mod list displayed in the application; -- If you can't get the mod manager to work, follow the instructions for manual installation. - -### Manual installation - -- [Install OWML](https://github.com/ow-mods/owml#installation); -- [Download the latest Quantum Space Buddies release](https://github.com/misternebula/quantum-space-buddies/releases/latest); -- Extract the `QSB` directory to the `OWML/Mods` directory; -- Run `OWML.Launcher.exe` to start the game. +- [Install the Outer Wilds Mod Manager](https://outerwildsmods.com/mod-manager/) +- Install Quantum Space Buddies from the mod list displayed in the application ## Hosting / Connecting #### Connecting to a server +- Make sure to have Steam open and logged in. - On the title screen, click the option `CONNECT TO MULTIPLAYER`. -- Enter the Product User ID of the person you are trying to connect to. +- Enter the Steam ID of the person you are trying to connect to. + - If "Use KCP Transport" is enabled, enter the public IP address of the person instead. - Enjoy! #### Hosting a server +- Make sure to have Steam open and logged in. - On the title screen, click the option `OPEN TO MULTIPLAYER`. -- Share your Product User ID with the people who want to connect. +- Share your Steam ID with the people who want to connect. + - If "Use KCP Transport" is enabled, share your public IP address instead. This can be found on websites like https://www.whatismyip.com/. - Enjoy! ## Frequently Asked Questions @@ -49,6 +44,9 @@ Spoilers within! ### 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 either be using KCP, or not using KCP.*** +### Why does SpaceWar show up in my Steam library? +This is for players who own the game on Epic or Xbox. Steam networking only works if you own the game on Steam, so we have to pretend to be SpaceWar (which every Steam user owns) so everyone can play. + ### Requirements - Latest version of OWML. - Latest version of Mod Manager. (If using) @@ -63,17 +61,27 @@ There still might be one or two small mechanics that aren't synced - let us know 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. +QSB relies on object hierarchy to sync objects, so any mod that changes that risks breaking QSB. +QSB also relies on certain game events being called when things happen in-game, so any mod that makes these things happen without calling the correct events will break QSB. + +Most small mods will work fine. The more complex and far reaching the mod, the less likely it will work completely. +Try as many mods as you like, but don't be surprised if things break. + +#### NomaiVr -### Is this mod compatible with NomaiVR? +[Here](https://github.com/qsb-dev/quantum-space-buddies/issues?q=is%3Aissue+is%3Aopen+label%3ANomaiVR) are the known issues. You are welcome to add to this list by creating issues. -Short answer - Kind of. +Most things seem to work _enough_. There are some visual bugs, and I believe a few softlocks, but the experience shouldn't be too bad. -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. +We haven't done too much work to make them compatible, so the things that are broken are unlikely to be fixed. + +#### New Horizons + +[Here](https://github.com/qsb-dev/quantum-space-buddies/issues?q=is%3Aissue+is%3Aopen+label%3A%22New+Horizons%22) are the known issues. You are welcome to add to this list by creating issues. + +We do our best to stay mostly compatible with base New Horizons, but the compatibility of each addon is mixed. +Most of them at least partially work. Most custom mechanics will not work until the addon developer explicitly adds QSB support. ### Why do I keep getting thrown around the ship? @@ -108,13 +116,20 @@ See [DEVELOPMENT.md](DEVELOPMENT.md) ### Contributers +- [xen](https://github.com/xen-42) - Help with syncing particle/sound effects, fixing lantern item bugs, and syncing addon data. +- [Moonstone](https://github.com/MoonstoneStudios) - Improvements to elevators and lifts. - [Chris Yeninas](https://github.com/PhantomGamers) - Help with project files and GitHub workflows. +- [Locochoco](https://github.com/loco-choco) - Code improvements. + +### Translators + - [Tlya](https://github.com/Tllya) - Russian translation. -- [Xen](https://github.com/xen-42) - French translation, and help with particle effects and sounds. -- [ShoosGun](https://github.com/ShoosGun) - Portuguese translation. +- [xen](https://github.com/xen-42) and [MerlinConnected](https://github.com/MerlinConnected) - French translation. +- [Locochoco](https://github.com/loco-choco) - Portuguese translation. - [DertolleDude](https://github.com/DertolleDude) - German translation. -- [SakuradaYuki](https://github.com/SakuradaYuki) - Chinese translation. +- [SakuradaYuki](https://github.com/SakuradaYuki) and [isrecalpear](https://github.com/isrecalpear) - Chinese translation. - [poleshe](https://github.com/poleshe) - Spanish translation. +- [Deniz](https://github.com/dumbdeniz) - Turkish translation. ### Special Thanks - Thanks to Logan Ver Hoef for help with the game code, and for helping make the damn game in the first place. @@ -126,7 +141,7 @@ See [DEVELOPMENT.md](DEVELOPMENT.md) - [Mirror](https://mirror-networking.com/) - [kcp2k](https://github.com/vis2k/kcp2k) - [Telepathy](https://github.com/vis2k/Telepathy) -- [EpicOnlineTransport](https://github.com/FakeByte/EpicOnlineTransport) +- [FizzySteamworks](https://github.com/Chykary/FizzySteamworks) - [HarmonyX](https://github.com/BepInEx/HarmonyX) - [UniTask](https://github.com/Cysharp/UniTask) - Modified code from [Popcron's Gizmos](https://github.com/popcron/gizmos) @@ -137,7 +152,7 @@ See [DEVELOPMENT.md](DEVELOPMENT.md) ## License and legal stuff -Copyright (C) 2020 - 2023 : +Copyright (C) 2020 - 2024 : - Henry Pointer (_nebula or misternebula) - Will Corby (JohnCorby) - Aleksander Waage (AmazingAlek) diff --git a/SteamRerouter/ExeSide/Program.cs b/SteamRerouter/ExeSide/Program.cs new file mode 100644 index 000000000..721d5379c --- /dev/null +++ b/SteamRerouter/ExeSide/Program.cs @@ -0,0 +1,91 @@ +using HarmonyLib; +using Steamworks; +using System; +using System.IO; +using System.Reflection; + +namespace SteamRerouter.ExeSide; + +/// +/// top level file on the exe +/// +public static class Program +{ + private static int Main(string[] args) + { + var managedDir = args[0]; + Log($"managed dir = {managedDir}"); + + AppDomain.CurrentDomain.AssemblyResolve += (_, e) => + { + var name = new AssemblyName(e.Name).Name + ".dll"; + var path = Path.Combine(managedDir, name); + return File.Exists(path) ? Assembly.LoadFile(path) : null; + }; + + var type = int.Parse(args[1]); + Log($"command type = {type}"); + var arg = int.Parse(args[2]); + Log($"command arg = {arg}"); + + return DoCommand(type, arg); + } + + public static void Log(object msg) => Console.Out.WriteLine(msg); + public static void LogError(object msg) => Console.Error.WriteLine(msg); + + private static int DoCommand(int type, int arg = default) + { + // copied from QSBCore + if (!Packsize.Test()) + { + LogError("[Steamworks.NET] Packsize Test returned false, the wrong version of Steamworks.NET is being run in this platform."); + } + + if (!DllCheck.Test()) + { + LogError("[Steamworks.NET] DllCheck Test returned false, One or more of the Steamworks binaries seems to be the wrong version."); + } + + // from facepunch.steamworks SteamClient.cs + Environment.SetEnvironmentVariable("SteamAppId", "753640"); + Environment.SetEnvironmentVariable("SteamGameId", "753640"); + + if (!SteamAPI.Init()) + { + LogError($"FATAL - SteamAPI.Init() failed. Do you have Steam open, and are you logged in?"); + return -1; + } + + var exitCode = -1; + switch (type) + { + // dlc status + case 0: + var owned = SteamApps.BIsDlcInstalled((AppId_t)1622100U); + Log($"dlc owned: {owned}"); + exitCode = owned ? 1 : 0; + break; + + // earn achievement + case 1: + var achievementType = (Achievements.Type)arg; + Log("Earn " + achievementType); + // for some reason even with unsafe code turned on it throws a FieldAccessException + var s_names = (string[])AccessTools.Field(typeof(Achievements), "s_names").GetValue(null); + if (!SteamUserStats.SetAchievement(s_names[(int)achievementType])) + { + LogError("Unable to grant achievement \"" + s_names[(int)achievementType] + "\""); + } + else + { + exitCode = 0; + } + SteamUserStats.StoreStats(); + break; + } + + SteamAPI.Shutdown(); + return exitCode; + } +} diff --git a/SteamRerouter/ModSide/Interop.cs b/SteamRerouter/ModSide/Interop.cs new file mode 100644 index 000000000..57704bc7e --- /dev/null +++ b/SteamRerouter/ModSide/Interop.cs @@ -0,0 +1,89 @@ +using HarmonyLib; +using System.Diagnostics; +using System.IO; +using System.Reflection; +using UnityEngine; +using Debug = UnityEngine.Debug; + +namespace SteamRerouter.ModSide; + +/// +/// top level file on the mod +/// +public static class Interop +{ + public static EntitlementsManager.AsyncOwnershipStatus OwnershipStatus = EntitlementsManager.AsyncOwnershipStatus.NotReady; + + public static void Init() + { + Log("init"); + Harmony.CreateAndPatchAll(typeof(Patches)); + + // Cache DLC ownership since the patched function gets called often. + // This won't work if the player buys the DLC mid-game, but too bad! + OwnershipStatus = IsDlcOwned() + ? EntitlementsManager.AsyncOwnershipStatus.Owned + : EntitlementsManager.AsyncOwnershipStatus.NotOwned; + } + + public static void Log(object msg) => Debug.Log($"[SteamRerouter] {msg}"); + public static void LogError(object msg) => Debug.LogError($"[SteamRerouter] {msg}"); + + private static bool IsDlcOwned() + { + var ownershipStatus = DoCommand(true, 0) != 0; + Log($"dlc owned: {ownershipStatus}"); + return ownershipStatus; + } + + public static void EarnAchivement(Achievements.Type type) + { + Log($"earn achievement {type}"); + DoCommand(false, 1, (int)type); + } + + private static int DoCommand(bool waitForExit, int type, int arg = default) + { + var processPath = Path.Combine( + Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, + "SteamRerouter.exe" + ); + Log($"process path = {processPath}"); + var gamePath = Application.dataPath; + Log($"game path = {gamePath}"); + var workingDirectory = Path.Combine(gamePath, "Plugins", "x86_64"); + Log($"working dir = {workingDirectory}"); + var args = new[] + { + Path.Combine(gamePath, "Managed"), + type.ToString(), + arg.ToString() + }; + + Log($"args = {args.Join()}"); + var process = Process.Start(new ProcessStartInfo + { + FileName = processPath, + WorkingDirectory = workingDirectory, + Arguments = args.Join(x => $"\"{x}\"", " "), + + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardOutput = true, + RedirectStandardError = true + }); + + if (waitForExit) + { + process!.WaitForExit(); + + Log($"StandardOutput:\n{process.StandardOutput.ReadToEnd()}"); + Log($"StandardError:\n{process.StandardError.ReadToEnd()}"); + Log($"ExitCode: {process.ExitCode}"); + + return process.ExitCode; + } + + return -1; + } +} diff --git a/SteamRerouter/ModSide/Patches.cs b/SteamRerouter/ModSide/Patches.cs new file mode 100644 index 000000000..fe93a63ab --- /dev/null +++ b/SteamRerouter/ModSide/Patches.cs @@ -0,0 +1,40 @@ +using HarmonyLib; +using UnityEngine; + +namespace SteamRerouter.ModSide; + +[HarmonyPatch] +public static class Patches +{ + [HarmonyPrefix] + [HarmonyPatch(typeof(EntitlementsManager), nameof(EntitlementsManager.InitializeOnAwake))] + private static bool EntitlementsManager_InitializeOnAwake(EntitlementsManager __instance) + { + Object.Destroy(__instance); + return false; + } + + [HarmonyPrefix] + [HarmonyPatch(typeof(EntitlementsManager), nameof(EntitlementsManager.Start))] + private static bool EntitlementsManager_Start() => false; + + [HarmonyPrefix] + [HarmonyPatch(typeof(EntitlementsManager), nameof(EntitlementsManager.OnDestroy))] + private static bool EntitlementsManager_OnDestroy() => false; + + [HarmonyPrefix] + [HarmonyPatch(typeof(EntitlementsManager), nameof(EntitlementsManager.IsDlcOwned))] + private static bool EntitlementsManager_IsDlcOwned(out EntitlementsManager.AsyncOwnershipStatus __result) + { + __result = Interop.OwnershipStatus; + return false; + } + + [HarmonyPrefix] + [HarmonyPatch(typeof(Achievements), nameof(Achievements.Earn))] + private static bool Achievements_Earn(Achievements.Type type) + { + Interop.EarnAchivement(type); + return false; + } +} diff --git a/SteamRerouter/SteamRerouter.csproj b/SteamRerouter/SteamRerouter.csproj new file mode 100644 index 000000000..97ce5beae --- /dev/null +++ b/SteamRerouter/SteamRerouter.csproj @@ -0,0 +1,23 @@ + + + Exe + Steam Rerouter + Steam Rerouter + Steam Rerouter + Handles game steam interaction, since qsb uses its own app id + William Corby, Henry Pointer + William Corby, Henry Pointer + Copyright © William Corby, Henry Pointer 2022-2024 + LICENSE + + + + True + \ + + + + + + +