Merge pull request #705 from qsb-dev/dev

Dev
This commit is contained in:
_nebula 2025-02-27 15:33:49 +00:00 committed by GitHub
commit 576011da22
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
40 changed files with 1056 additions and 1843 deletions

View File

@ -1,107 +0,0 @@
> [!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.
- Epic or Steam version of Outer Wilds.
- Keyboard with numpad for in-game debug actions.
We recommend using the Outer Wilds Mod Manager, but you can use OWML on its own if you want.
## Cloning and configuration
- Clone QSB's source code.
- Copy the file `DevEnv.template.targets` and rename it to `DevEnv.targets`.
- In `DevEnv.targets`, edit the entry for `<OwmlDir>` to point to your installation of OWML. This should be the folder named `OWML`. If using the manager, you can find this directory by :
- Legacy Manager : Press the "Mods Directory" button and go up a folder.
- 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!***
## 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.
- 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)
The files will automatically be copied over to your OWML installation and be ready to play - no DLL copying needed.
For documentation reasons, here is the build flow :
- MirrorWeaver is built.
- EpicOnlineTransport is built.
- EpicRerouter is built.
- QSB is built.
- Any `.exe.config` files are removed from the build.
- QSB.dll is processed ("weaved") by MirrorWeaver. This injects all the boilerplate code that Mirror needs to function.
- If needed/possible, any `.dll` or `.exe` files are copied to the Unity project.
## API
Use the API by copying [the API definition](https://github.com/misternebula/quantum-space-buddies/blob/master/QSB/API/IQSBAPI.cs) into your mod.
## 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. 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 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 :
> [!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
{
"instanceIdInLogs": false,
"hookDebugLogs": false,
"avoidTimeSync": false,
"autoStart": false,
"kickEveryone": false,
"disableLoopDeath": false,
"timeout": 25,
"debugMode": false,
"drawGui": false,
"drawLines": false,
"drawLabels": false,
"drawGhostAI": false,
"greySkybox": false
}
```
- 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.
- drawGhostAI - Draws debug lines and labels just for the ghosts.
- greySkybox - Turns the skybox grey. Useful in the Eye, where it's pretty dark.

View File

@ -1,89 +0,0 @@
using System.Collections;
using System.Collections.Generic;
namespace Mirror.FizzySteam
{
public class BidirectionalDictionary<T1, T2> : IEnumerable
{
private Dictionary<T1, T2> t1ToT2Dict = new Dictionary<T1, T2>();
private Dictionary<T2, T1> t2ToT1Dict = new Dictionary<T2, T1>();
public IEnumerable<T1> FirstTypes => t1ToT2Dict.Keys;
public IEnumerable<T2> 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);
}
}
}
}

View File

@ -1,311 +0,0 @@
#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<byte> 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<byte> 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

View File

@ -1,23 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<RootNamespace>Mirror.FizzySteam</RootNamespace>
<AssemblyTitle>Fizzy Steamworks</AssemblyTitle>
<Product>Fizzy Steamworks</Product>
<Title>Fizzy Steamworks</Title>
<Description></Description>
<Authors>Fizz Cube Ltd, Marco Hoffmann, William Corby</Authors>
<Company>Fizz Cube Ltd, Marco Hoffmann, William Corby</Company>
<Copyright></Copyright>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
</PropertyGroup>
<ItemGroup>
<None Include="LICENSE">
<Pack>True</Pack>
<PackagePath>\</PackagePath>
</None>
</ItemGroup>
<ItemGroup>
<PackageReference Include="OuterWildsGameLibs" Version="1.1.14.768" IncludeAssets="compile" />
<Reference Include="..\Lib\*.dll" />
</ItemGroup>
</Project>

View File

@ -1,14 +0,0 @@
namespace Mirror.FizzySteam
{
public interface IClient
{
bool Connected { get; }
bool Error { get; }
void ReceiveData();
void Disconnect();
void FlushData();
void Send(byte[] data, int channelId);
}
}

View File

@ -1,12 +0,0 @@
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();
}
}

View File

@ -1,37 +0,0 @@
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.
===
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

View File

@ -1,177 +0,0 @@
#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<byte[], int> OnReceivedData;
private event Action OnConnected;
private event Action OnDisconnected;
private TimeSpan ConnectionTimeout;
private CSteamID hostSteamID = CSteamID.Nil;
private TaskCompletionSource<Task> 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<byte>(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<Task>();
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

View File

@ -1,180 +0,0 @@
#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<P2PSessionRequest_t> callback_OnNewConnection = null;
private Steamworks.Callback<P2PSessionConnectFail_t> callback_OnConnectFail = null;
protected readonly FizzySteamworks transport;
protected LegacyCommon(FizzySteamworks transport)
{
channels = transport.Channels;
callback_OnNewConnection = Steamworks.Callback<P2PSessionRequest_t>.Create(OnNewConnection);
callback_OnConnectFail = Steamworks.Callback<P2PSessionConnectFail_t>.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

View File

@ -1,170 +0,0 @@
#if !DISABLESTEAMWORKS
using Steamworks;
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Mirror.FizzySteam
{
public class LegacyServer : LegacyCommon, IServer
{
private event Action<int> OnConnected;
private event Action<int, byte[], int> OnReceivedData;
private event Action<int> OnDisconnected;
private event Action<int, TransportError, string> OnReceivedError;
private BidirectionalDictionary<CSteamID, int> 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<byte>(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<CSteamID, int>();
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<CSteamID, int> 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

View File

@ -1,242 +0,0 @@
#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<byte[], int> OnReceivedData;
private event Action OnConnected;
private event Action OnDisconnected;
// CHANGED
private event Action<TransportError, string> OnReceivedError;
private Steamworks.Callback<SteamNetConnectionStatusChangedCallback_t> c_onConnectionChange = null;
private CancellationTokenSource cancelToken;
private TaskCompletionSource<Task> connectedComplete;
private CSteamID hostSteamID = CSteamID.Nil;
private HSteamNetConnection HostConnection;
private List<Action> BufferedData;
private NextClient(FizzySteamworks transport)
{
ConnectionTimeout = TimeSpan.FromSeconds(Math.Max(1, transport.Timeout));
BufferedData = new List<Action>();
}
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<byte>(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<SteamNetConnectionStatusChangedCallback_t>.Create(OnConnectionStatusChanged);
try
{
hostSteamID = new CSteamID(UInt64.Parse(host));
connectedComplete = new TaskCompletionSource<Task>();
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

View File

@ -1,48 +0,0 @@
#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<SteamNetworkingMessage_t>(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

View File

@ -1,246 +0,0 @@
#if !DISABLESTEAMWORKS
using Steamworks;
using System;
using System.Linq;
using UnityEngine;
namespace Mirror.FizzySteam
{
public class NextServer : NextCommon, IServer
{
private event Action<int> OnConnected;
private event Action<int, byte[], int> OnReceivedData;
private event Action<int> OnDisconnected;
private event Action<int, TransportError, string> OnReceivedError;
private BidirectionalDictionary<HSteamNetConnection, int> connToMirrorID;
private BidirectionalDictionary<CSteamID, int> steamIDToMirrorID;
private int maxConnections;
private int nextConnectionID;
private HSteamListenSocket listenSocket;
private Steamworks.Callback<SteamNetConnectionStatusChangedCallback_t> c_onConnectionChange = null;
private NextServer(int maxConnections)
{
this.maxConnections = maxConnections;
connToMirrorID = new BidirectionalDictionary<HSteamNetConnection, int>();
steamIDToMirrorID = new BidirectionalDictionary<CSteamID, int>();
nextConnectionID = 1;
c_onConnectionChange = Steamworks.Callback<SteamNetConnectionStatusChangedCallback_t>.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<byte>(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

18
QSB.sln
View File

@ -19,8 +19,6 @@ 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}") = "FizzySteamworks", "FizzySteamworks\FizzySteamworks.csproj", "{D47034D8-B92D-47DA-884C-C76F735E2D5D}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SteamRerouter", "SteamRerouter\SteamRerouter.csproj", "{750563ED-4F2A-42DD-81A3-3A85DEBB691E}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Mirror", "Mirror", "{851AB4AD-366C-4D69-9F4E-E9EDC7CCD2BB}"
@ -38,6 +36,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QSBPatcher", "QSBPatcher\QS
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QSB-NH", "QSB-NH\QSB-NH.csproj", "{74F84A39-1C9D-4EF7-889A-485D33B7B324}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SteamTransport", "SteamTransport\SteamTransport.csproj", "{83E8A790-CD63-4642-81D1-E406FC9F8EED}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SteamTransportTest", "SteamTransportTest\SteamTransportTest.csproj", "{8E74FA1D-466F-4BE5-981E-8D95E9B2D9AE}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -52,10 +54,6 @@ 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
{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
@ -72,6 +70,14 @@ Global
{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
{83E8A790-CD63-4642-81D1-E406FC9F8EED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{83E8A790-CD63-4642-81D1-E406FC9F8EED}.Debug|Any CPU.Build.0 = Debug|Any CPU
{83E8A790-CD63-4642-81D1-E406FC9F8EED}.Release|Any CPU.ActiveCfg = Release|Any CPU
{83E8A790-CD63-4642-81D1-E406FC9F8EED}.Release|Any CPU.Build.0 = Release|Any CPU
{8E74FA1D-466F-4BE5-981E-8D95E9B2D9AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8E74FA1D-466F-4BE5-981E-8D95E9B2D9AE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8E74FA1D-466F-4BE5-981E-8D95E9B2D9AE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8E74FA1D-466F-4BE5-981E-8D95E9B2D9AE}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@ -98,7 +98,7 @@ public class AnimatorMirror : MonoBehaviour
{
if (_networkAnimator != null)
{
DebugLog.DebugWrite($"Set {fromParam.name} on netanim");
// DebugLog.DebugWrite($"Set {fromParam.name} on netanim");
_networkAnimator.SetTrigger(fromParam.nameHash);
}
@ -109,7 +109,7 @@ public class AnimatorMirror : MonoBehaviour
{
if (_networkAnimator != null)
{
DebugLog.DebugWrite($"Reset {fromParam.name} on netanim");
// DebugLog.DebugWrite($"Reset {fromParam.name} on netanim");
_networkAnimator.ResetTrigger(fromParam.nameHash);
}

View File

@ -83,7 +83,7 @@ public class DeathPatches : QSBPatch
}
var deadPlayersCount = QSBPlayerManager.PlayerList.Count(x => x.IsDead);
if (deadPlayersCount == QSBPlayerManager.PlayerList.Count - 1 && !QSBCore.DebugSettings.DisableLoopDeath)
if (deadPlayersCount == QSBPlayerManager.PlayerList.Count - 1)
{
new EndLoopMessage().Send();
DebugLog.DebugWrite($"- All players are dead.");

View File

@ -331,7 +331,7 @@ public class MenuManager : MonoBehaviour, IAddComponentOnStart
DisconnectPopup = QSBCore.Helper.MenuHelper.PopupMenuManager.CreateTwoChoicePopup(QSBLocalization.Current.DisconnectAreYouSure, QSBLocalization.Current.Yes, QSBLocalization.Current.No);
DisconnectPopup.OnPopupConfirm += Disconnect;
DisconnectButton = QSBCore.Helper.MenuHelper.PauseMenuManager.MakeMenuOpenButton(QSBLocalization.Current.PauseMenuDisconnect, DisconnectPopup, 0, true);
DisconnectButton = QSBCore.Helper.MenuHelper.PauseMenuManager.MakeMenuOpenButton(QSBLocalization.Current.PauseMenuDisconnect, DisconnectPopup, 0, false);
QuitButton = FindObjectOfType<PauseMenuManager>()._exitToMainMenuAction.gameObject;
@ -490,7 +490,8 @@ public class MenuManager : MonoBehaviour, IAddComponentOnStart
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
// this is done cuz otherwise client can get into scene before server. could start on transition but eh
// see issue #710
Delay.RunWhen(() => TimeLoop._initialized, () =>
{
QSBNetworkManager.singleton.StartHost();
@ -506,7 +507,8 @@ public class MenuManager : MonoBehaviour, IAddComponentOnStart
{
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
// this is done cuz otherwise client can get into scene before server. could start on transition but eh
// see issue #710
Delay.RunWhen(() => TimeLoop._initialized, () =>
{
QSBNetworkManager.singleton.StartHost();

View File

@ -62,13 +62,6 @@ public class PlayerJoinMessage : QSBMessage
{
if (QSBCore.IsHost)
{
if (QSBCore.DebugSettings.KickEveryone)
{
DebugLog.ToConsole($"Kicking {PlayerName} because of DebugSettings.KickEveryone", MessageType.Error);
new PlayerKickMessage(From, "This server has DebugSettings.KickEveryone enabled.").Send();
return;
}
if (QSBVersion != QSBCore.QSBVersion)
{
DebugLog.ToConsole($"Error - Client {PlayerName} connecting with wrong QSB version. (Client:{QSBVersion}, Server:{QSBCore.QSBVersion})", MessageType.Error);

View File

@ -66,9 +66,6 @@
</None>
<None Remove="AssetBundles\AssetBundles" />
<None Remove="AssetBundles\*.manifest" />
<None Condition="Exists('debugsettings.json')" Include="debugsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Include="..\Lib\*.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
@ -77,7 +74,7 @@
<PackageReference Include="OuterWildsGameLibs" Version="1.1.15.1018" IncludeAssets="compile" />
<PackageReference Include="OWML" Version="2.15.1" IncludeAssets="compile" />
<Reference Include="..\Lib\*.dll" />
<ProjectReference Include="..\FizzySteamworks\FizzySteamworks.csproj" />
<ProjectReference Include="..\SteamTransport\SteamTransport.csproj" />
<ProjectReference Include="..\SteamRerouter\SteamRerouter.csproj" />
<ProjectReference Include="..\QSBPatcher\QSBPatcher.csproj" />
<ProjectReference Include="..\MirrorWeaver\MirrorWeaver.csproj" ReferenceOutputAssembly="false" />

View File

@ -17,7 +17,6 @@ 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;
@ -72,15 +71,14 @@ public class QSBCore : ModBehaviour
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 int Timeout { get; private set; }
public static GameVendor GameVendor { get; private set; } = GameVendor.None;
public static bool IsStandalone => GameVendor is GameVendor.Epic or GameVendor.Steam;
public static IProfileManager ProfileManager => IsStandalone
? QSBStandaloneProfileManager.SharedInstance
: QSBMSStoreProfileManager.SharedInstance;
public static DebugSettings DebugSettings { get; private set; } = new();
private static string randomSkinType;
private static string randomJetpackType;
public static readonly DebugSettings DebugSettings = new();
public static Assembly QSBNHAssembly = null;
@ -217,32 +215,6 @@ public class QSBCore : ModBehaviour
CheckNewHorizons();
DebugSettings = Helper.Storage.Load<DebugSettings>("debugsettings.json") ?? new DebugSettings();
if (DebugSettings.HookDebugLogs)
{
Application.logMessageReceived += (condition, stackTrace, logType) =>
DebugLog.ToConsole(
$"[Debug] {condition}" +
(stackTrace != string.Empty ? $"\nStacktrace: {stackTrace}" : string.Empty),
logType switch
{
LogType.Error => MessageType.Error,
LogType.Assert => MessageType.Error,
LogType.Warning => MessageType.Warning,
LogType.Log => MessageType.Message,
LogType.Exception => MessageType.Error,
_ => throw new ArgumentOutOfRangeException(nameof(logType), logType, null)
}
);
}
if (DebugSettings.AutoStart)
{
UseKcpTransport = true;
DebugSettings.DebugMode = true;
}
RegisterAddons();
InitAssemblies();
@ -273,19 +245,6 @@ public class QSBCore : ModBehaviour
QSBWorldSync.Managers = components.OfType<WorldObjectManager>().ToArray();
QSBPatchManager.OnPatchType += OnPatchType;
QSBPatchManager.OnUnpatchType += OnUnpatchType;
if (DebugSettings.RandomizeSkins)
{
var skinSetting = (JObject)ModHelper.Config.Settings["skinType"];
var skinOptions = skinSetting["options"].ToObject<string[]>();
randomSkinType = skinOptions[UnityEngine.Random.Range(0, skinOptions.Length - 1)];
var jetpackSetting = (JObject)ModHelper.Config.Settings["jetpackType"];
var jetpackOptions = jetpackSetting["options"].ToObject<string[]>();
randomJetpackType = jetpackOptions[UnityEngine.Random.Range(0, jetpackOptions.Length - 1)];
Configure(ModHelper.Config);
}
}
private AssetBundle LoadBundle(string bundleName)
@ -402,6 +361,25 @@ public class QSBCore : ModBehaviour
public override void Configure(IModConfig config)
{
DebugSettings.Update(config);
Application.logMessageReceived -= OnDebugLog;
if (DebugSettings.HookDebugLogs)
{
Application.logMessageReceived += OnDebugLog;
}
// Configure gets called before Start, so these might not exist yet
if (GetComponent<DebugActions>() != null)
{
GetComponent<DebugActions>().enabled = DebugSettings.DebugMode;
GetComponent<DebugGUI>().enabled = DebugSettings.DebugMode;
}
DebugCameraSettings.UpdateFromDebugSetting();
Timeout = config.GetSettingsValue<int>("timeout");
UseKcpTransport = config.GetSettingsValue<bool>("useKcpTransport") || DebugSettings.AutoStart;
var foundValue = config.GetSettingsValue<int>("kcpPort");
KcpPort = (ushort)Mathf.Clamp(foundValue, ushort.MinValue, ushort.MaxValue);
@ -414,16 +392,8 @@ public class QSBCore : ModBehaviour
TextChatInput = config.GetSettingsValue<bool>("textChatInput");
AlwaysShowPlanetIcons = config.GetSettingsValue<bool>("alwaysShowPlanetIcons");
if (DebugSettings.RandomizeSkins)
{
SkinVariation = randomSkinType;
JetpackVariation = randomJetpackType;
}
else
{
SkinVariation = config.GetSettingsValue<string>("skinType");
JetpackVariation = config.GetSettingsValue<string>("jetpackType");
}
SkinVariation = config.GetSettingsValue<string>("skinType");
JetpackVariation = config.GetSettingsValue<string>("jetpackType");
if (IsHost)
{
@ -440,23 +410,28 @@ public class QSBCore : ModBehaviour
private void Update()
{
if (Keyboard.current[Key.Q].isPressed && Keyboard.current[Key.NumpadEnter].wasPressedThisFrame)
{
DebugSettings.DebugMode = !DebugSettings.DebugMode;
GetComponent<DebugActions>().enabled = DebugSettings.DebugMode;
GetComponent<DebugGUI>().enabled = DebugSettings.DebugMode;
DebugCameraSettings.UpdateFromDebugSetting();
DebugLog.ToConsole($"DEBUG MODE = {DebugSettings.DebugMode}");
}
if (_steamworksInitialized)
{
SteamAPI.RunCallbacks();
}
}
/// <summary>
/// called when unity does Debug log
/// </summary>
private static void OnDebugLog(string condition, string stackTrace, LogType logType)
{
DebugLog.ToConsole($"[Debug] {condition}" + (stackTrace != string.Empty ? $"\nStacktrace: {stackTrace}" : string.Empty), logType switch
{
LogType.Error => MessageType.Error,
LogType.Assert => MessageType.Error,
LogType.Warning => MessageType.Warning,
LogType.Log => MessageType.Message,
LogType.Exception => MessageType.Error,
_ => throw new ArgumentOutOfRangeException(nameof(logType), logType, null)
});
}
private void CheckNewHorizons()
{
if (ModHelper.Interaction.ModExists("xen.NewHorizons"))

View File

@ -1,6 +1,4 @@
using Epic.OnlineServices.Logging;
using Mirror;
using Mirror.FizzySteam;
using Mirror;
using OWML.Common;
using OWML.Utils;
using QSB.Anglerfish.TransformSync;
@ -66,11 +64,11 @@ public class QSBNetworkManager : NetworkManager, IAddComponentOnStart
private GameObject _probePrefab;
private bool _everConnected;
private (TransportError error, string reason) _lastTransportError = (TransportError.Unexpected, "transport did not give an error. uh oh");
private (TransportError error, string reason) _lastTransportError = (TransportError.Unexpected, "transport did not give an error. uh oh. report this please!");
private static LatencySimulation _latencyTransport;
private static kcp2k.KcpTransport _kcpTransport;
private static FizzySteamworks _steamTransport;
private static SteamTransport.SteamTransport _steamTransport;
public override void Awake()
{
@ -79,14 +77,16 @@ public class QSBNetworkManager : NetworkManager, IAddComponentOnStart
{
_kcpTransport = gameObject.AddComponent<kcp2k.KcpTransport>();
// KCP uses milliseconds
_kcpTransport.Timeout = QSBCore.DebugSettings.Timeout * 1000;
_kcpTransport.Timeout = QSBCore.Timeout * 1000;
_kcpTransport.Port = QSBCore.KcpPort;
}
{
_steamTransport = gameObject.AddComponent<FizzySteamworks>();
// Steam uses seconds
_steamTransport.Timeout = QSBCore.DebugSettings.Timeout;
_steamTransport = gameObject.AddComponent<SteamTransport.SteamTransport>();
// Steam uses milliseconds
_steamTransport.Timeout = QSBCore.Timeout * 1000;
_steamTransport.TestIpAddress = QSBCore.DebugSettings.SteamTestIpAddress;
_steamTransport.DoFakeNetworkErrors = QSBCore.DebugSettings.SteamFakeNetworkErrors;
}
{
@ -168,6 +168,11 @@ public class QSBNetworkManager : NetworkManager, IAddComponentOnStart
}
_kcpTransport.Port = QSBCore.KcpPort;
_kcpTransport.Timeout = QSBCore.Timeout * 1000;
_steamTransport.Timeout = QSBCore.Timeout * 1000;
_steamTransport.TestIpAddress = QSBCore.DebugSettings.SteamTestIpAddress;
_steamTransport.DoFakeNetworkErrors = QSBCore.DebugSettings.SteamFakeNetworkErrors;
if (QSBCore.IsInMultiplayer)
{
@ -251,10 +256,14 @@ public class QSBNetworkManager : NetworkManager, IAddComponentOnStart
return template;
}
// who knows why this is a separate function
private void ConfigureNetworkManager()
{
networkAddress = QSBCore.DefaultServerIP;
{
_steamTransport.Log = s => DebugLog.DebugWrite("[Steam] " + s);
}
{
kcp2k.Log.Info = s =>
{
@ -269,6 +278,7 @@ public class QSBNetworkManager : NetworkManager, IAddComponentOnStart
kcp2k.Log.Error = s => DebugLog.DebugWrite("[KCP] " + s, MessageType.Error);
}
// this is where the logic for stopping host in the title screen is
QSBSceneManager.OnPostSceneLoad += (_, loadScene) =>
{
if (QSBCore.IsInMultiplayer && loadScene == OWScene.TitleScreen)
@ -365,7 +375,7 @@ public class QSBNetworkManager : NetworkManager, IAddComponentOnStart
DebugLog.DebugWrite("OnClientDisconnect");
base.OnClientDisconnect();
OnClientDisconnected?.SafeInvoke(_lastTransportError.error, _lastTransportError.reason);
_lastTransportError = (TransportError.Unexpected, "transport did not give an error. uh oh");
_lastTransportError = (TransportError.Unexpected, "transport did not give an error. uh oh. report this please!");
}
public override void OnServerDisconnect(NetworkConnectionToClient conn) // Called on the server when any client disconnects

View File

@ -162,7 +162,7 @@ public class RespawnManager : MonoBehaviour, IAddComponentOnStart
var deadPlayersCount = QSBPlayerManager.PlayerList.Count(x => x.IsDead);
if (deadPlayersCount == QSBPlayerManager.PlayerList.Count && !QSBCore.DebugSettings.DisableLoopDeath)
if (deadPlayersCount == QSBPlayerManager.PlayerList.Count)
{
new EndLoopMessage().Send();
return;

View File

@ -89,4 +89,4 @@ public class QSBInflationTrigger : QSBTrigger<CosmicInflationController>
particles.Stop();
}
}
}
}

View File

@ -160,12 +160,34 @@ public class DebugActions : MonoBehaviour, IAddComponentOnStart
if (Keyboard.current[Key.Numpad2].wasPressedThisFrame)
{
var location = DreamArrivalPoint.Location.Undefined;
if (Keyboard.current[Key.Digit1].isPressed)
{
location = DreamArrivalPoint.Location.Zone1;
}
else if (Keyboard.current[Key.Digit2].isPressed)
{
location = DreamArrivalPoint.Location.Zone2;
}
else if (Keyboard.current[Key.Digit3].isPressed)
{
location = DreamArrivalPoint.Location.Zone3;
}
else if (Keyboard.current[Key.Digit4].isPressed)
{
location = DreamArrivalPoint.Location.Zone4;
}
if (!QSBPlayerManager.LocalPlayer.InDreamWorld)
{
if (location == DreamArrivalPoint.Location.Undefined)
{
return;
}
// modified from DayDream debug thing
var relativeLocation = new RelativeLocationData(Vector3.up * 2 + Vector3.forward * 2, Quaternion.identity, Vector3.zero);
var location = Keyboard.current[Key.LeftShift].isPressed ? DreamArrivalPoint.Location.Zone4 : DreamArrivalPoint.Location.Zone3;
var arrivalPoint = Locator.GetDreamArrivalPoint(location);
var dreamCampfire = Locator.GetDreamCampfire(location);
if (Locator.GetToolModeSwapper().GetItemCarryTool().GetHeldItemType() != ItemType.DreamLantern)
@ -187,6 +209,35 @@ public class DebugActions : MonoBehaviour, IAddComponentOnStart
var dreamLanternItem = QSBPlayerManager.LocalPlayer.AssignedSimulationLantern.AttachedObject;
Locator.GetToolModeSwapper().GetItemCarryTool().PickUpItemInstantly(dreamLanternItem);
}
if (location == DreamArrivalPoint.Location.Undefined)
{
return;
}
// copied from DreamWorldController.FixedUpdate
var relativeLocation = new RelativeLocationData(Vector3.up * 2 + Vector3.forward * 2, Quaternion.identity, Vector3.zero);
var arrivalPoint = Locator.GetDreamArrivalPoint(location);
var controller = Locator.GetDreamWorldController();
Locator.GetPlayerBody().MoveToRelativeLocation(relativeLocation, controller._dreamBody, arrivalPoint.transform);
GlobalMessenger.FireEvent("WarpPlayer");
if (!Physics.autoSyncTransforms)
{
Physics.SyncTransforms();
}
var playerSectorDetector = Locator.GetPlayerSectorDetector();
playerSectorDetector.RemoveFromAllSectors();
var sector = arrivalPoint.GetSector();
while (sector != null)
{
sector.GetTriggerVolume().AddObjectToVolume(playerSectorDetector.gameObject);
sector = sector.GetParentSector();
}
arrivalPoint.OnEnterDreamWorld(QSBPlayerManager.LocalPlayer.AssignedSimulationLantern.AttachedObject);
}
}

View File

@ -6,21 +6,21 @@ public class DebugCameraSettings : MonoBehaviour, IAddComponentOnStart
{
public static void UpdateFromDebugSetting()
{
if (QSBCore.DebugSettings.GreySkybox)
QSBSceneManager.OnSceneLoaded -= OnSceneLoaded;
if (Camera.main)
{
QSBSceneManager.OnSceneLoaded += OnSceneLoaded;
if (Camera.main)
{
Camera.main.backgroundColor = Color.gray;
}
Camera.main.backgroundColor = _origColor;
}
else
if (!QSBCore.DebugSettings.GreySkybox)
{
QSBSceneManager.OnSceneLoaded -= OnSceneLoaded;
if (Camera.main)
{
Camera.main.backgroundColor = _origColor;
}
return;
}
QSBSceneManager.OnSceneLoaded += OnSceneLoaded;
if (Camera.main)
{
Camera.main.backgroundColor = Color.gray;
}
}
@ -35,4 +35,4 @@ public class DebugCameraSettings : MonoBehaviour, IAddComponentOnStart
private static void OnSceneLoaded(OWScene arg1, OWScene arg2, bool arg3)
=> Camera.main.backgroundColor = Color.gray;
}
}

View File

@ -105,7 +105,7 @@ public class DebugGUI : MonoBehaviour, IAddComponentOnStart
WriteLine(1, $"FPS : {Mathf.Round(1f / Time.smoothDeltaTime)}");
WriteLine(1, $"Ping : {Math.Round(NetworkTime.rtt * 1000.0)} ms");
if (!QSBCore.DebugSettings.DrawGui)
if (!QSBCore.DebugSettings.DrawGUI)
{
return;
}
@ -422,16 +422,6 @@ public class DebugGUI : MonoBehaviour, IAddComponentOnStart
}
}
}
else if (QSBCore.DebugSettings.DrawGhostAI)
{
foreach (var obj in QSBWorldSync.GetWorldObjects<IGhostObject>())
{
if (obj.ShouldDisplayDebug())
{
DrawLabel(obj.AttachedObject.transform, obj.ReturnLabel());
}
}
}
}
public void OnRenderObject() => DrawWorldObjectLines();
@ -452,16 +442,6 @@ public class DebugGUI : MonoBehaviour, IAddComponentOnStart
}
}
}
else if (QSBCore.DebugSettings.DrawGhostAI)
{
foreach (var obj in QSBWorldSync.GetWorldObjects<IGhostObject>())
{
if (obj.ShouldDisplayDebug())
{
obj.DisplayLines();
}
}
}
}
public static void DrawLabel(Transform obj, string label)

View File

@ -16,7 +16,7 @@ public static class DebugLog
public static void ToConsole(string message, MessageType type = MessageType.Message)
{
if (QSBCore.DebugSettings.InstanceIdInLogs)
if (QSBCore.DebugSettings.InstanceIDInLogs)
{
message = $"[{ProcessInstanceId}] " + message;
}

View File

@ -1,63 +1,65 @@
using Newtonsoft.Json;
using OWML.Common;
namespace QSB.Utility;
[JsonObject(MemberSerialization.OptIn)]
/// <summary>
/// purely organizational class to store all debug settings
/// </summary>
public class DebugSettings
{
[JsonProperty("logQSBMessages")]
public bool LogQSBMessages;
[JsonProperty("instanceIdInLogs")]
public bool InstanceIdInLogs;
[JsonProperty("hookDebugLogs")]
public bool HookDebugLogs;
[JsonProperty("avoidTimeSync")]
public bool AvoidTimeSync;
[JsonProperty("autoStart")]
public bool AutoStart;
[JsonProperty("kickEveryone")]
public bool KickEveryone;
[JsonProperty("disableLoopDeath")]
public bool DisableLoopDeath;
[JsonProperty("latencySimulation")]
public int LatencySimulation;
[JsonProperty("randomizeSkins")]
public bool RandomizeSkins;
/// <summary>
/// Timeout in seconds
/// </summary>
[JsonProperty("timeout")]
public int Timeout = 40;
[JsonProperty("debugMode")]
public bool DebugMode;
[JsonProperty("drawGui")]
private bool _drawGui;
public bool DrawGui => DebugMode && _drawGui;
private bool _logQSBMessages;
public bool LogQSBMessages => DebugMode && _logQSBMessages;
private bool _instanceIdInLogs;
public bool InstanceIDInLogs => DebugMode && _instanceIdInLogs;
private bool _hookDebugLogs;
public bool HookDebugLogs => DebugMode && _hookDebugLogs;
private bool _avoidTimeSync;
public bool AvoidTimeSync => DebugMode && _avoidTimeSync;
private bool _autoStart;
public bool AutoStart => DebugMode && _autoStart;
private int _latencySimulation;
public int LatencySimulation => DebugMode ? _latencySimulation : 0;
private bool _drawGUI;
public bool DrawGUI => DebugMode && _drawGUI;
[JsonProperty("drawLines")]
private bool _drawLines;
public bool DrawLines => DebugMode && _drawLines;
[JsonProperty("drawLabels")]
private bool _drawLabels;
public bool DrawLabels => DebugMode && _drawLabels;
[JsonProperty("drawGhostAI")]
private bool _drawGhostAI;
public bool DrawGhostAI => DebugMode && _drawGhostAI;
[JsonProperty("greySkybox")]
private bool _greySkybox;
public bool GreySkybox => DebugMode && _greySkybox;
private string _steamTestIpAddress;
public string SteamTestIpAddress => DebugMode ? _steamTestIpAddress : null;
private bool _steamFakeNetworkErrors;
public bool SteamFakeNetworkErrors => DebugMode && _steamFakeNetworkErrors;
public void Update(IModConfig config)
{
DebugMode = config.GetSettingsValue<bool>("debugMode");
_instanceIdInLogs = config.GetSettingsValue<bool>("instanceIdInLogs");
_hookDebugLogs = config.GetSettingsValue<bool>("hookDebugLogs");
_avoidTimeSync = config.GetSettingsValue<bool>("avoidTimeSync");
_autoStart = config.GetSettingsValue<bool>("autoStart");
_drawGUI = config.GetSettingsValue<bool>("drawGui");
_drawLines = config.GetSettingsValue<bool>("drawLines");
_drawLabels = config.GetSettingsValue<bool>("drawLabels");
_greySkybox = config.GetSettingsValue<bool>("greySkybox");
_latencySimulation = config.GetSettingsValue<int>("latencySimulation");
_logQSBMessages = config.GetSettingsValue<bool>("logQSBMessages");
_steamTestIpAddress = config.GetSettingsValue<string>("steamTestIpAddress");
_steamFakeNetworkErrors = config.GetSettingsValue<bool>("steamFakeNetworkErrors");
}
}

View File

@ -34,6 +34,12 @@
"value": "localhost",
"tooltip": "Used if you leave the connect prompt blank."
},
"timeout": {
"title": "Timeout",
"type": "number",
"value": 30,
"tooltip": "How many seconds it takes for a connection to time out. Used both when connecting and checked when playing."
},
"showPlayerNames": {
"title": "Show Player Names",
"type": "toggle",
@ -57,12 +63,93 @@
"type": "toggle",
"value": false,
"tooltip": "If disabled, the planet icon shown for each player will be replaced by a question mark (?) in thematically appropriate situations."
} ,
},
"textChatInput": {
"title": "Text Chat Input",
"type": "toggle",
"value": true,
"tooltip": "Disable this if using NomaiVR, or any other mod with conflicting inputs."
},
"Debug Options": {
"type": "separator"
},
"debugMode": {
"title": "[DEBUG] Debug Mode",
"type": "toggle",
"value": false,
"tooltip": "[DEBUG] Enables debug mode. Prints more helpful logs. If this is disabled, none of the following settings do anything."
},
"instanceIdInLogs": {
"title": "[DEBUG] Instance ID in Logs",
"type": "toggle",
"value": false,
"tooltip": "[DEBUG] Appends the game instance id to every log message sent."
},
"hookDebugLogs": {
"title": "[DEBUG] Hook Debug Logs",
"type": "toggle",
"value": false,
"tooltip": "[DEBUG] Prints every Unity warning + error to the logs."
},
"avoidTimeSync": {
"title": "[DEBUG] Disable Time Sync",
"type": "toggle",
"value": false,
"tooltip": "[DEBUG] If enabled, you will no longer fast forward / pause to match the server time. THIS CAN BREAK A LOT OF STUFF!"
},
"autoStart": {
"title": "[DEBUG] Auto Start",
"type": "toggle",
"value": false,
"tooltip": "[DEBUG] Automatically host or connect to localhost on game launch (depending on which instance of the game is opened first)."
},
"drawGui": {
"title": "[DEBUG] Draw GUI",
"type": "toggle",
"value": false,
"tooltip": "[DEBUG] Draws a lot of helpful text at the top of the screen."
},
"drawLines": {
"title": "[DEBUG] Draw Lines",
"type": "toggle",
"value": false,
"tooltip": "[DEBUG] Draws a lot of helpful lines between stuff. LAGGY!"
},
"drawLabels": {
"title": "[DEBUG] Draw Labels",
"type": "toggle",
"value": false,
"tooltip": "[DEBUG] Draws a lot of helpful labels on stuff. LAGGY!"
},
"greySkybox": {
"title": "[DEBUG] Grey Skybox",
"type": "toggle",
"value": false,
"tooltip": "[DEBUG] Makes the skybox grey."
},
"latencySimulation": {
"title": "[DEBUG] Latency Simulation",
"type": "number",
"value": 0,
"tooltip": "[DEBUG] How many seconds of latency to simulate."
},
"logQSBMessages": {
"title": "[DEBUG] Log QSB Messages",
"type": "toggle",
"value": false,
"tooltip": "[DEBUG] If enabled, QSB creates log files of every network message received and transmitted."
},
"steamTestIpAddress": {
"title": "[DEBUG] Steam Test IP Address",
"type": "text",
"value": "",
"tooltip": "[DEBUG] If not empty, the steam transport will use this ip:port when listening/connecting. Host should probably use 0.0.0.0:port."
},
"steamFakeNetworkErrors": {
"title": "[DEBUG] Steam Fake Network Errors",
"type": "toggle",
"value": false,
"tooltip": "[DEBUG] Aggressively simulates common network errors (loss, lag, reordering, duplication) to test resiliency."
}
}
}
}

View File

@ -4,7 +4,7 @@
"author": "Nebula, John, Alek, & Rai",
"name": "Quantum Space Buddies",
"uniqueName": "Raicuparta.QuantumSpaceBuddies",
"version": "1.3.0",
"version": "1.4.0",
"owmlVersion": "2.14.0",
"dependencies": [ "JohnCorby.VanillaFix" ],
"pathsToPreserve": [ "debugsettings.json" ],

View File

@ -99,9 +99,9 @@ Outer Wilds Online is easier to set up, but much more basic in its features. The
See [TRANSLATING.md](TRANSLATING.md)
## Development Setup / Contributing
## Development Setup / Contributing / Debugging
See [DEVELOPMENT.md](DEVELOPMENT.md)
See the [QSB Developer Wiki](https://github.com/qsb-dev/quantum-space-buddies/wiki)
## Authors and Special Thanks

143
SteamTransport/Client.cs Normal file
View File

@ -0,0 +1,143 @@
using Mirror;
using Steamworks;
using System;
namespace SteamTransport;
public class Client
{
private SteamTransport _transport;
private Steamworks.Callback<SteamNetConnectionStatusChangedCallback_t> _onStatusChanged;
public Client(SteamTransport transport)
{
_transport = transport;
_onStatusChanged = Steamworks.Callback<SteamNetConnectionStatusChangedCallback_t>.Create(t =>
{
_transport.Log($"STATUS CHANGED for {t.m_info.m_szConnectionDescription}\n" +
$" state = {t.m_info.m_eState}\n" +
$" end = {(ESteamNetConnectionEnd)t.m_info.m_eEndReason} {t.m_info.m_szEndDebug}");
// SteamNetworkingSockets.GetDetailedConnectionStatus(t.m_hConn, out var status, 1000);
// _transport.Log(status);
switch (t.m_info.m_eState)
{
case ESteamNetworkingConnectionState.k_ESteamNetworkingConnectionState_Connecting:
IsConnecting = true;
IsConnected = false;
break;
case ESteamNetworkingConnectionState.k_ESteamNetworkingConnectionState_Connected:
IsConnecting = false;
IsConnected = true;
_transport.OnClientConnected?.Invoke();
break;
case ESteamNetworkingConnectionState.k_ESteamNetworkingConnectionState_ClosedByPeer:
case ESteamNetworkingConnectionState.k_ESteamNetworkingConnectionState_ProblemDetectedLocally:
var result = SteamNetworkingSockets.CloseConnection(_conn, t.m_info.m_eEndReason, t.m_info.m_szEndDebug, false);
if (result != true)
{
_transport.Log($"[warn] close returned {result}");
}
IsConnecting = false;
IsConnected = false;
_transport.OnClientError?.Invoke(TransportError.ConnectionClosed, t.m_info.m_szEndDebug);
_transport.OnClientDisconnected?.Invoke();
// OnClientDisconnected will cause mirror to shutdown the transport
break;
}
});
}
public bool IsConnecting;
public bool IsConnected;
private HSteamNetConnection _conn;
public void Connect(string address)
{
var options = Util.MakeOptions(_transport);
if (!string.IsNullOrEmpty(_transport.TestIpAddress))
{
var steamAddr = new SteamNetworkingIPAddr();
var parsed = steamAddr.ParseString(_transport.TestIpAddress);
if (!parsed)
{
_transport.OnClientError?.Invoke(TransportError.DnsResolve, $"couldnt parse address {_transport.TestIpAddress} when connecting");
_transport.OnClientDisconnected?.Invoke(); // will show error box
return;
}
_conn = SteamNetworkingSockets.ConnectByIPAddress(ref steamAddr, options.Length, options);
_transport.Log($"connecting to {steamAddr.ToDebugString()}");
}
else
{
var identity = new SteamNetworkingIdentity();
var parsed = ulong.TryParse(address, out var steamId);
if (!parsed)
{
_transport.OnClientError?.Invoke(TransportError.DnsResolve, $"couldnt parse address {address} when connecting");
_transport.OnClientDisconnected?.Invoke(); // will show error box
return;
}
identity.SetSteamID64(steamId);
_conn = SteamNetworkingSockets.ConnectP2P(ref identity, 0, options.Length, options);
_transport.Log($"connecting to {identity.ToDebugString()}");
}
}
public void Send(ArraySegment<byte> segment, int channelId)
{
var result = _conn.Send(segment, channelId);
if (result != EResult.k_EResultOK)
{
_transport.Log($"[warn] send returned {result}");
}
_transport.OnClientDataSent?.Invoke(segment, channelId);
}
public void Receive()
{
var ppOutMessages = new IntPtr[Util.MaxMessages];
var numMessages = SteamNetworkingSockets.ReceiveMessagesOnConnection(_conn, ppOutMessages, ppOutMessages.Length);
for (var i = 0; i < numMessages; i++)
{
var (segment, channelId) = Util.Receive(ppOutMessages[i]);
_transport.OnClientDataReceived?.Invoke(segment, channelId);
}
}
public void Flush()
{
var result = SteamNetworkingSockets.FlushMessagesOnConnection(_conn);
if (result != EResult.k_EResultOK && result != EResult.k_EResultIgnored) // flush does ignored when connecting. dont log cuz spam
{
_transport.Log($"[warn] flush returned {result}");
}
}
public void Close()
{
// theres a weird case where we arent doing an intentional disconnect but there isnt a status change disonnect either
// not sure whats going on there, but ill slap a stack trace on it
_transport.Log($"client close\n{Environment.StackTrace}");
var result = SteamNetworkingSockets.CloseConnection(_conn, 0, "client closed connection", false);
if (result != true)
{
_transport.Log($"[warn] client close returned {result}");
}
IsConnecting = false;
IsConnected = false;
// its not an error for us to close ourselves intentionally
// but we do it anyway cuz above comment
_transport.OnClientError?.Invoke(TransportError.ConnectionClosed, "client closed connection, but you shouldnt be seeing this in game. make sure [DEBUG] Debug Mode is on and check logs for stack trace. report this please!");
_transport.OnClientDisconnected?.Invoke();
_onStatusChanged.Dispose();
}
}

155
SteamTransport/Server.cs Normal file
View File

@ -0,0 +1,155 @@
using Mirror;
using Steamworks;
using System;
using System.Collections.Generic;
namespace SteamTransport;
public class Server
{
private SteamTransport _transport;
private Steamworks.Callback<SteamNetConnectionStatusChangedCallback_t> _onStatusChanged;
public Server(SteamTransport transport)
{
_transport = transport;
_onStatusChanged = Steamworks.Callback<SteamNetConnectionStatusChangedCallback_t>.Create(t =>
{
_transport.Log($"STATUS CHANGED for {t.m_info.m_szConnectionDescription}\n" +
$" state = {t.m_info.m_eState}\n" +
$" end = {(ESteamNetConnectionEnd)t.m_info.m_eEndReason} {t.m_info.m_szEndDebug}");
// SteamNetworkingSockets.GetDetailedConnectionStatus(t.m_hConn, out var status, 1000);
// _transport.Log(status);
switch (t.m_info.m_eState)
{
case ESteamNetworkingConnectionState.k_ESteamNetworkingConnectionState_Connecting:
{
// mirror handles max connections. client will just get generic disconnect message, but its okay.
var result = SteamNetworkingSockets.AcceptConnection(t.m_hConn);
if (result != EResult.k_EResultOK)
{
_transport.Log($"[warn] accept {t.m_info.m_szConnectionDescription} returned {result}");
}
break;
}
case ESteamNetworkingConnectionState.k_ESteamNetworkingConnectionState_Connected:
_conns.Add(t.m_hConn);
_transport.OnServerConnected?.Invoke((int)t.m_hConn.m_HSteamNetConnection);
break;
case ESteamNetworkingConnectionState.k_ESteamNetworkingConnectionState_ClosedByPeer:
// this logs an error below even tho it isnt really an error. its fine
case ESteamNetworkingConnectionState.k_ESteamNetworkingConnectionState_ProblemDetectedLocally:
{
var result = SteamNetworkingSockets.CloseConnection(t.m_hConn, t.m_info.m_eEndReason, t.m_info.m_szEndDebug, false);
if (result != true)
{
_transport.Log($"[warn] close {t.m_info.m_szConnectionDescription} returned {result}");
}
_conns.Remove(t.m_hConn);
_transport.OnServerError?.Invoke((int)t.m_hConn.m_HSteamNetConnection, TransportError.ConnectionClosed, t.m_info.m_szEndDebug);
_transport.OnServerDisconnected?.Invoke((int)t.m_hConn.m_HSteamNetConnection);
break;
}
}
});
}
public bool IsListening;
private HSteamListenSocket _listenSocket;
// mirror connection id is derived from uint to int cast here. seems to do unchecked cast and be fine
private readonly List<HSteamNetConnection> _conns = new();
public void StartListening()
{
var options = Util.MakeOptions(_transport);
if (!string.IsNullOrEmpty(_transport.TestIpAddress))
{
var steamAddr = new SteamNetworkingIPAddr();
var parsed = steamAddr.ParseString(_transport.TestIpAddress);
if (!parsed)
{
_transport.OnServerError?.Invoke(-1, TransportError.DnsResolve, $"couldnt parse address {_transport.TestIpAddress} when listening");
// dont really need to stop server here. mirror isnt designed to let us fail to listen anyway so this is all kinda silly
return;
}
_listenSocket = SteamNetworkingSockets.CreateListenSocketIP(ref steamAddr, options.Length, options);
_transport.Log($"listening on {steamAddr.ToDebugString()}");
}
else
{
_listenSocket = SteamNetworkingSockets.CreateListenSocketP2P(0, options.Length, options);
_transport.Log($"listening on p2p");
}
IsListening = true;
}
public void Send(int connectionId, ArraySegment<byte> segment, int channelId)
{
var conn = new HSteamNetConnection((uint)connectionId);
var result = conn.Send(segment, channelId);
if (result != EResult.k_EResultOK)
{
_transport.Log($"[warn] send {conn.ToDebugString()} returned {result}");
}
_transport.OnServerDataSent?.Invoke(connectionId, segment, channelId);
}
public void Receive()
{
var ppOutMessages = new IntPtr[Util.MaxMessages];
foreach (var conn in _conns)
{
var numMessages = SteamNetworkingSockets.ReceiveMessagesOnConnection(conn, ppOutMessages, ppOutMessages.Length);
for (var i = 0; i < numMessages; i++)
{
var (segment, channelId) = Util.Receive(ppOutMessages[i]);
_transport.OnServerDataReceived?.Invoke((int)conn.m_HSteamNetConnection, segment, channelId);
}
}
}
public void Flush()
{
foreach (var conn in _conns)
{
var result = SteamNetworkingSockets.FlushMessagesOnConnection(conn);
if (result != EResult.k_EResultOK)
{
_transport.Log($"[warn] flush {conn.ToDebugString()} returned {result}");
}
}
}
public void Disconnect(int connectionId)
{
var conn = new HSteamNetConnection((uint)connectionId);
_transport.Log($"disconnect {conn.ToDebugString()}");
var result = SteamNetworkingSockets.CloseConnection(conn, 0, "disconnected by server", false);
if (result != true)
{
_transport.Log($"[warn] close {conn.ToDebugString()} returned {result}");
}
_conns.Remove(conn);
// its not an error for us to disconnect a client intentionally
_transport.OnServerDisconnected?.Invoke(connectionId);
}
public void Close()
{
// mirror disconnects all clients for us before this
_transport.Log("stop server");
var result = SteamNetworkingSockets.CloseListenSocket(_listenSocket);
if (result != true)
{
_transport.Log($"[warn] stop server returned {result}");
}
IsListening = false;
_onStatusChanged.Dispose();
}
}

View File

@ -0,0 +1,136 @@
// https://github.com/MirrorNetworking/Mirror/blob/master/Assets/Mirror/Core/Transport.cs
// https://partner.steamgames.com/doc/api/ISteamNetworkingSockets
// https://partner.steamgames.com/doc/api/steamnetworkingtypes
using Mirror;
using Steamworks;
using System;
namespace SteamTransport;
public class SteamTransport : Transport
{
private Server _server;
private Client _client;
/// <summary>
/// logs will verbosely go here. must be set
///
/// my policy is to log every potential error here, but otherwise ignore it. then if its an actual issue i do OnClientError and handle it properly.
/// </summary>
public Action<string> Log;
/// <summary>
/// if set, will use this ip address and port for listening/connecting
/// </summary>
public string TestIpAddress;
/// <summary>
/// timeout in ms when connecting, and timeout before detecting a loss in connection.
/// after-connection timeout seems to be around 0-10 more seconds than specified.
/// </summary>
// default from steam https://github.com/ValveSoftware/GameNetworkingSockets/blob/master/src/steamnetworkingsockets/clientlib/csteamnetworkingsockets.cpp#L76
public int Timeout = 10000;
/// <summary>
/// whether or not to simulate fake packet loss, lag, reorder, and dup
/// </summary>
public bool DoFakeNetworkErrors;
public override bool Available() => true;
public override bool ClientConnected() => _client.IsConnected;
public override void ClientConnect(string address)
{
_client = new Client(this);
_client.Connect(address);
}
public override void ClientSend(ArraySegment<byte> segment, int channelId = 0)
{
_client.Send(segment, channelId);
}
public override void ClientDisconnect()
{
// mirror seems to cause this sometimes
if (_client == null)
{
Log("tried to ClientDisconnect when client is null");
return;
}
_client.Close();
_client = null;
}
public override Uri ServerUri() => throw new NotImplementedException("dont need to implement this i think");
public override bool ServerActive() => _server != null && _server.IsListening;
public override void ServerStart()
{
_server = new Server(this);
_server.StartListening();
}
public override void ServerSend(int connectionId, ArraySegment<byte> segment, int channelId = 0)
{
_server.Send(connectionId, segment, channelId);
}
public override void ServerDisconnect(int connectionId)
{
_server.Disconnect(connectionId);
}
public override string ServerGetClientAddress(int connectionId) => throw new NotImplementedException("dont need to implement this i think");
public override void ServerStop()
{
// mirror seems to cause this sometimes
if (_server == null)
{
Log("tried to ServerStop when server is null");
return;
}
_server.Close();
_server = null;
}
public override int GetMaxPacketSize(int channelId = 0) => Constants.k_cbMaxSteamNetworkingSocketsMessageSizeSend;
public override void Shutdown()
{
// gotta null check because might be only one existing
if (_client != null)
{
_client.Close();
_client = null;
}
if (_server != null)
{
_server.Close();
_server = null;
}
}
// all of these update functions run all the time, so we must null check
public override void ClientEarlyUpdate()
{
_client?.Receive();
}
public override void ServerEarlyUpdate()
{
_server?.Receive();
}
public override void ClientLateUpdate()
{
_client?.Flush();
}
public override void ServerLateUpdate()
{
_server?.Flush();
}
}

View File

@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<AssemblyTitle>Steam Transport</AssemblyTitle>
<Product>Steam Transport</Product>
<Title>Steam Transport</Title>
<Description>A custom Mirror transport based on FizzySteamworks</Description>
<Authors>William Corby, Henry Pointer</Authors>
<Company>William Corby, Henry Pointer</Company>
<Copyright>Copyright © William Corby, Henry Pointer 2022-2025</Copyright>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="OuterWildsGameLibs" Version="1.1.14.768" IncludeAssets="compile" />
<PackageReference Include="OWML" Version="2.11.1" IncludeAssets="compile" />
<Reference Include="..\Lib\*.dll" />
</ItemGroup>
</Project>

122
SteamTransport/Util.cs Normal file
View File

@ -0,0 +1,122 @@
using Mirror;
using Steamworks;
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
namespace SteamTransport;
public static class Util
{
public const int MaxMessages = 256; // same as fizzy steamworks
private static int SendFlag2MirrorChannel(int sendFlag) => sendFlag switch
{
Constants.k_nSteamNetworkingSend_Reliable => Channels.Reliable,
Constants.k_nSteamNetworkingSend_Unreliable => Channels.Unreliable
};
private static int MirrorChannel2SendFlag(int mirrorChannel) => mirrorChannel switch
{
Channels.Reliable => Constants.k_nSteamNetworkingSend_Reliable,
Channels.Unreliable => Constants.k_nSteamNetworkingSend_Unreliable
};
public static string ToDebugString(this HSteamNetConnection conn)
{
SteamNetworkingSockets.GetConnectionInfo(conn, out var pInfo);
return pInfo.m_szConnectionDescription;
}
public static string ToDebugString(this SteamNetworkingIdentity ident)
{
ident.ToString(out var s);
return s;
}
public static string ToDebugString(this SteamNetworkingIPAddr addr)
{
addr.ToString(out var s, true);
return s;
}
public static EResult Send(this HSteamNetConnection conn, ArraySegment<byte> segment, int channelId)
{
var handle = GCHandle.Alloc(segment.Array, GCHandleType.Pinned); // prevent moving or gc when passing to native function
var result = SteamNetworkingSockets.SendMessageToConnection(conn, handle.AddrOfPinnedObject() + segment.Offset, (uint)segment.Count, MirrorChannel2SendFlag(channelId), out _);
handle.Free();
return result;
}
public static (ArraySegment<byte> segment, int channelId) Receive(IntPtr ppOutMessage)
{
var msg = SteamNetworkingMessage_t.FromIntPtr(ppOutMessage);
var segment = new ArraySegment<byte>(new byte[msg.m_cbSize]);
Marshal.Copy(msg.m_pData, segment.Array, 0, msg.m_cbSize);
var channelId = SendFlag2MirrorChannel(msg.m_nFlags);
SteamNetworkingMessage_t.Release(ppOutMessage);
return (segment, channelId);
}
public static SteamNetworkingConfigValue_t[] MakeOptions(SteamTransport transport)
{
var result = new List<SteamNetworkingConfigValue_t>();
result.Add(new SteamNetworkingConfigValue_t
{
m_eValue = ESteamNetworkingConfigValue.k_ESteamNetworkingConfig_TimeoutInitial,
m_eDataType = ESteamNetworkingConfigDataType.k_ESteamNetworkingConfig_Int32,
m_val = new SteamNetworkingConfigValue_t.OptionValue
{
m_int32 = transport.Timeout
}
});
result.Add(new SteamNetworkingConfigValue_t
{
m_eValue = ESteamNetworkingConfigValue.k_ESteamNetworkingConfig_TimeoutConnected,
m_eDataType = ESteamNetworkingConfigDataType.k_ESteamNetworkingConfig_Int32,
m_val = new SteamNetworkingConfigValue_t.OptionValue
{
m_int32 = transport.Timeout
}
});
// 20% change of doing all, all delays are 200 ms. leads to about 1 second of rtt ping if enabled on both ends.
if (transport.DoFakeNetworkErrors)
{
// global scope = dont apply to connection
static void SetConfigValue(ESteamNetworkingConfigValue key, object value)
{
var handle = GCHandle.Alloc(value, GCHandleType.Pinned);
SteamNetworkingUtils.SetConfigValue(
key,
ESteamNetworkingConfigScope.k_ESteamNetworkingConfig_Global,
IntPtr.Zero,
handle.Target switch
{
int => ESteamNetworkingConfigDataType.k_ESteamNetworkingConfig_Int32,
float => ESteamNetworkingConfigDataType.k_ESteamNetworkingConfig_Float
},
handle.AddrOfPinnedObject()
);
handle.Free();
}
SetConfigValue(ESteamNetworkingConfigValue.k_ESteamNetworkingConfig_FakePacketLoss_Send, (float)20);
SetConfigValue(ESteamNetworkingConfigValue.k_ESteamNetworkingConfig_FakePacketLoss_Recv, (float)20);
SetConfigValue(ESteamNetworkingConfigValue.k_ESteamNetworkingConfig_FakePacketLag_Send, (int)200);
SetConfigValue(ESteamNetworkingConfigValue.k_ESteamNetworkingConfig_FakePacketLag_Recv, (int)200);
SetConfigValue(ESteamNetworkingConfigValue.k_ESteamNetworkingConfig_FakePacketReorder_Send, (float)20);
SetConfigValue(ESteamNetworkingConfigValue.k_ESteamNetworkingConfig_FakePacketReorder_Recv, (float)20);
SetConfigValue(ESteamNetworkingConfigValue.k_ESteamNetworkingConfig_FakePacketReorder_Time, (int)200);
SetConfigValue(ESteamNetworkingConfigValue.k_ESteamNetworkingConfig_FakePacketDup_Send, (float)20);
SetConfigValue(ESteamNetworkingConfigValue.k_ESteamNetworkingConfig_FakePacketDup_Recv, (float)20);
SetConfigValue(ESteamNetworkingConfigValue.k_ESteamNetworkingConfig_FakePacketDup_TimeMax, (int)200);
}
return result.ToArray();
}
}

View File

@ -0,0 +1,168 @@
using HarmonyLib;
using Steamworks;
using System;
using System.Threading;
namespace SteamTransportTest;
/// <summary>
/// entry point for testing.
/// should probably make this a separate project since it copies over steam api. idc rn
/// </summary>
public static class Program
{
public static void Main(string[] args)
{
Console.WriteLine("This is the test mode for the steam transport");
try
{
// copied from qsbcore and steamworks.net docs
{
if (!Packsize.Test())
{
Console.Error.WriteLine("[Steamworks.NET] Packsize Test returned false, the wrong version of Steamworks.NET is being run in this platform.");
}
if (!DllCheck.Test())
{
Console.Error.WriteLine("[Steamworks.NET] DllCheck Test returned false, One or more of the Steamworks binaries seems to be the wrong version.");
}
// yes, these have to be the same. even for connecting to ip address
Environment.SetEnvironmentVariable("SteamAppId", "480");
Environment.SetEnvironmentVariable("SteamGameId", "480");
var m_bInitialized = SteamAPI.Init();
if (!m_bInitialized)
{
Console.Error.WriteLine("[Steamworks.NET] SteamAPI_Init() failed. Refer to Valve's documentation or the comment above this line for more information.");
return;
}
SteamClient.SetWarningMessageHook((severity, text) => Console.WriteLine(text));
}
Console.WriteLine("press 1 for server, 2 for client");
switch (Console.ReadKey(true).KeyChar)
{
case '1':
Console.WriteLine("server");
DoServer();
break;
case '2':
Console.WriteLine("client");
DoClient();
break;
}
}
finally
{
SteamAPI.Shutdown();
Console.WriteLine("Done. Press any key to exit");
Console.ReadKey();
}
}
private static void DoServer()
{
var transport = new SteamTransport.SteamTransport();
transport.Log = Console.WriteLine;
transport.TestIpAddress = "127.0.0.1:1234";
// make timeout for client detecting server drop different than timeout for server detecting client drop
transport.Timeout = 5000;
transport.DoFakeNetworkErrors = false;
transport.OnServerError = (conn, error, s) => Console.Error.WriteLine($"ERROR {conn} {error} {s}");
var theConn = -1;
transport.OnServerConnected = conn => theConn = conn;
transport.OnServerDataSent = (conn, bytes, i) => Console.WriteLine($"SEND {conn} {i} {bytes.Join()}");
transport.OnServerDataReceived = (conn, bytes, i) => Console.WriteLine($"RECV {conn} {i} {bytes.Join()}");
transport.ServerStart();
try
{
Console.WriteLine("press c to close, s to send, d to disconnect");
var running = true;
while (running)
{
transport.ServerEarlyUpdate();
if (Console.KeyAvailable)
{
switch (Console.ReadKey(true).KeyChar)
{
case 'c':
// running = false;
transport.ServerDisconnect(theConn); // mirror does this for us
transport.ServerStop();
break;
case 's':
transport.ServerSend(theConn, new ArraySegment<byte>(new byte[] { 1, 2, (byte)DateTime.Now.Millisecond, 4, 5 }, 1, 5 - 1));
break;
case 'd':
transport.ServerDisconnect(theConn);
break;
}
}
SteamAPI.RunCallbacks();
transport.ServerLateUpdate();
Thread.Sleep(10);
}
}
finally
{
transport.ServerDisconnect(theConn); // mirror does this for us
transport.ServerStop();
}
}
private static void DoClient()
{
var transport = new SteamTransport.SteamTransport();
transport.Log = Console.WriteLine;
transport.TestIpAddress = "127.0.0.1:1234";
transport.Timeout = 20000;
transport.DoFakeNetworkErrors = true;
transport.OnClientError = (error, s) => Console.Error.WriteLine($"ERROR {error} {s}");
transport.OnClientDataSent = (bytes, i) => Console.WriteLine($"SEND {i} {bytes.Join()}");
transport.OnClientDataReceived = (bytes, i) => Console.WriteLine($"RECV {i} {bytes.Join()}");
transport.ClientConnect("76561198150564286");
try
{
Console.WriteLine("press c to close, s to send");
var running = true;
// transport.OnClientDisconnected = () => running = false; // mirror normally does this
while (running)
{
transport.ClientEarlyUpdate();
if (Console.KeyAvailable)
{
switch (Console.ReadKey(true).KeyChar)
{
case 'c':
// running = false;
transport.ClientDisconnect();
break;
case 's':
transport.ClientSend(new ArraySegment<byte>(new byte[] { 1, 2, (byte)DateTime.Now.Millisecond, 4, 5 }, 1, 5 - 1), 1);
break;
}
}
SteamAPI.RunCallbacks();
transport.ClientLateUpdate();
Thread.Sleep(10);
}
}
finally
{
transport.ClientDisconnect();
}
}
}

View File

@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<AssemblyTitle>Steam Transport Test</AssemblyTitle>
<Product>Steam Transport Test</Product>
<Title>Steam Transport Test</Title>
<Description>Test exe for steam transport</Description>
<Authors>William Corby, Henry Pointer</Authors>
<Company>William Corby, Henry Pointer</Company>
<Copyright>Copyright © William Corby, Henry Pointer 2022-2025</Copyright>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="OuterWildsGameLibs" Version="1.1.14.768" />
<ProjectReference Include="..\SteamTransport\SteamTransport.csproj" />
<PackageReference Include="OWML" Version="2.11.1" />
<Reference Include="..\Lib\*.dll" />
</ItemGroup>
<Target Name="copy standalone dlls" AfterTargets="PostBuildEvent">
<ItemGroup>
<_Files Remove="@(_Files)" />
<_Files Include="..\Lib\*.dll" />
<_Files Include="standaloneLibs\*.dll" />
</ItemGroup>
<Copy SourceFiles="@(_Files)" DestinationFolder="$(OutputPath)" />
</Target>
</Project>

Binary file not shown.