Merge branch 'dev' into steamworks-v3

This commit is contained in:
_nebula 2023-08-04 15:27:40 +01:00
commit b4763c835e
19 changed files with 195 additions and 84 deletions

View File

@ -4,11 +4,27 @@ using UnityEngine.Events;
public interface IQSBAPI
{
#region General
/// <summary>
/// If called, all players connected to YOUR hosted game must have this mod installed.
/// </summary>
void RegisterRequiredForAllPlayers(IModBehaviour mod);
/// <summary>
/// Returns if the current player is the host.
/// </summary>
bool GetIsHost();
/// <summary>
/// Returns if the current player is in multiplayer.
/// </summary>
bool GetIsInMultiplayer();
#endregion
#region Player
/// <summary>
/// Returns the player ID of the current player.
/// </summary>
@ -22,16 +38,18 @@ public interface IQSBAPI
/// <summary>
/// Returns the list of IDs of all connected players.
///
/// The first player in the list is the host.
/// </summary>
uint[] GetPlayerIDs();
/// <summary>
/// Invoked when a player joins the game.
/// Invoked when any player (local or remote) joins the game.
/// </summary>
UnityEvent<uint> OnPlayerJoin();
/// <summary>
/// Invoked when a player leaves the game.
/// Invoked when any player (local or remote) leaves the game.
/// </summary>
UnityEvent<uint> OnPlayerLeave();
@ -53,8 +71,14 @@ public interface IQSBAPI
/// <returns>The data requested. If key is not valid, returns default.</returns>
T GetCustomData<T>(uint playerId, string key);
#endregion
#region Messaging
/// <summary>
/// Sends a message containing arbitrary data to every player.
///
/// Keep your messages under around 1100 bytes.
/// </summary>
/// <typeparam name="T">The type of the data being sent. This type must be serializable.</typeparam>
/// <param name="messageType">The unique key of the message.</param>
@ -70,4 +94,6 @@ public interface IQSBAPI
/// <param name="messageType">The unique key of the message.</param>
/// <param name="handler">The action to be ran when the message is received. The uint is the player ID that sent the messsage.</param>
void RegisterHandler<T>(string messageType, Action<uint, T> handler);
#endregion
}

View File

@ -8,6 +8,8 @@
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/BRACES_FOR_USING/@EntryValue">Required</s:String>
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/BRACES_FOR_WHILE/@EntryValue">Required</s:String>
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/CASE_BLOCK_BRACES/@EntryValue">NEXT_LINE_SHIFTED_2</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=API/@EntryIndexedValue">API</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=ID/@EntryIndexedValue">ID</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=QSB/@EntryIndexedValue">QSB</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=UWP/@EntryIndexedValue">UWP</s:String>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpKeepExistingMigration/@EntryIndexedValue">True</s:Boolean>

View File

@ -1,27 +1,28 @@
using System;
using System.Collections.Generic;
using OWML.Common;
using QSB.Utility;
namespace QSB.API;
public static class AddonDataManager
{
private static readonly Dictionary<string, Action<uint, object>> _handlers = new();
private static readonly Dictionary<int, Action<uint, object>> _handlers = new();
public static void OnReceiveDataMessage(string messageType, object data, uint from)
public static void OnReceiveDataMessage(int hash, object data, uint from)
{
DebugLog.DebugWrite($"Received data message of message type \"{messageType}\" from {from}!");
if (!_handlers.TryGetValue(messageType, out var handler))
DebugLog.DebugWrite($"Received addon message of with hash {hash} from {from}!");
if (!_handlers.TryGetValue(hash, out var handler))
{
DebugLog.DebugWrite($"unknown addon message type with hash {hash}", MessageType.Error);
return;
}
handler(from, data);
}
public static void RegisterHandler<T>(string messageType, Action<uint, T> handler)
public static void RegisterHandler<T>(int hash, Action<uint, T> handler)
{
DebugLog.DebugWrite($"Registering handler for \"{messageType}\" with type of {typeof(T).Name}");
_handlers.Add(messageType, (from, data) => handler(from, (T)data));
DebugLog.DebugWrite($"Registering addon message handler for hash {hash} with type {typeof(T).Name}");
_handlers.Add(hash, (from, data) => handler(from, (T)data));
}
}

View File

@ -4,11 +4,27 @@ using UnityEngine.Events;
public interface IQSBAPI
{
#region General
/// <summary>
/// If called, all players connected to YOUR hosted game must have this mod installed.
/// </summary>
void RegisterRequiredForAllPlayers(IModBehaviour mod);
/// <summary>
/// Returns if the current player is the host.
/// </summary>
bool GetIsHost();
/// <summary>
/// Returns if the current player is in multiplayer.
/// </summary>
bool GetIsInMultiplayer();
#endregion
#region Player
/// <summary>
/// Returns the player ID of the current player.
/// </summary>
@ -22,21 +38,25 @@ public interface IQSBAPI
/// <summary>
/// Returns the list of IDs of all connected players.
///
/// The first player in the list is the host.
/// </summary>
uint[] GetPlayerIDs();
/// <summary>
/// Invoked when a player joins the game.
/// Invoked when any player (local or remote) joins the game.
/// </summary>
UnityEvent<uint> OnPlayerJoin();
/// <summary>
/// Invoked when a player leaves the game.
/// Invoked when any player (local or remote) leaves the game.
/// </summary>
UnityEvent<uint> OnPlayerLeave();
/// <summary>
/// Sets some arbitrary data for a given player.
///
/// Not synced.
/// </summary>
/// <typeparam name="T">The type of the data.</typeparam>
/// <param name="playerId">The ID of the player.</param>
@ -46,15 +66,23 @@ public interface IQSBAPI
/// <summary>
/// Returns some arbitrary data from a given player.
///
/// Not synced.
/// </summary>
/// <typeparam name="T">The type of the data.</typeparam>
/// <param name="playerId">The ID of the player.</param>
/// <param name="key">The unique key of the data you want to access.</param>
/// <returns>The data requested. If key is not valid, returns default.</returns>
T GetCustomData<T>(uint playerId, string key);
#endregion
#region Messaging
/// <summary>
/// Sends a message containing arbitrary data to every player.
///
/// Keep your messages under around 1100 bytes.
/// </summary>
/// <typeparam name="T">The type of the data being sent. This type must be serializable.</typeparam>
/// <param name="messageType">The unique key of the message.</param>
@ -70,4 +98,6 @@ public interface IQSBAPI
/// <param name="messageType">The unique key of the message.</param>
/// <param name="handler">The action to be ran when the message is received. The uint is the player ID that sent the messsage.</param>
void RegisterHandler<T>(string messageType, Action<uint, T> handler);
#endregion
}

View File

@ -0,0 +1,11 @@
using QSB.Messaging;
using QSB.Player;
using QSB.Utility;
namespace QSB.API.Messages;
public class AddonCustomDataSyncMessage : QSBMessage<(uint playerId, string key, byte[] data)>
{
public AddonCustomDataSyncMessage(uint playerId, string key, object data) : base((playerId, key, data.ToBytes())) { }
public override void OnReceiveRemote() => QSBPlayerManager.GetPlayer(Data.playerId).SetCustomData(Data.key, Data.data.ToObject());
}

View File

@ -1,29 +1,11 @@
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using QSB.Messaging;
using QSB.Messaging;
using QSB.Utility;
namespace QSB.API.Messages;
public class AddonDataMessage : QSBMessage<(string messageType, byte[] data, bool receiveLocally)>
public class AddonDataMessage : QSBMessage<(int hash, byte[] data, bool receiveLocally)>
{
public AddonDataMessage(string messageType, object data, bool receiveLocally) : base((messageType, Obj2Bytes(data), receiveLocally)) { }
private static byte[] Obj2Bytes(object obj)
{
using var ms = new MemoryStream();
var bf = new BinaryFormatter();
bf.Serialize(ms, obj);
var bytes = ms.ToArray();
return bytes;
}
private static object Bytes2Obj(byte[] bytes)
{
using var ms = new MemoryStream(bytes);
var bf = new BinaryFormatter();
var obj = bf.Deserialize(ms);
return obj;
}
public AddonDataMessage(int hash, object data, bool receiveLocally) : base((hash, data.ToBytes(), receiveLocally)) { }
public override void OnReceiveLocal()
{
@ -35,7 +17,7 @@ public class AddonDataMessage : QSBMessage<(string messageType, byte[] data, boo
public override void OnReceiveRemote()
{
var obj = Bytes2Obj(Data.data);
AddonDataManager.OnReceiveDataMessage(Data.messageType, obj, From);
var obj = Data.data.ToObject();
AddonDataManager.OnReceiveDataMessage(Data.hash, obj, From);
}
}

View File

@ -1,9 +1,10 @@
using System;
using System.Linq;
using Mirror;
using OWML.Common;
using QSB.API.Messages;
using QSB.Messaging;
using QSB.Player;
using System;
using System.Linq;
using UnityEngine.Events;
namespace QSB.API;
@ -16,22 +17,24 @@ public class QSBAPI : IQSBAPI
QSBCore.Addons.Add(uniqueName, mod);
}
public bool GetIsHost() => QSBCore.IsHost;
public bool GetIsInMultiplayer() => QSBCore.IsInMultiplayer;
public uint GetLocalPlayerID() => QSBPlayerManager.LocalPlayerId;
public string GetPlayerName(uint playerId) => QSBPlayerManager.GetPlayer(playerId).Name;
public uint[] GetPlayerIDs() => QSBPlayerManager.PlayerList.Select(x => x.PlayerId).ToArray();
public UnityEvent<uint> OnPlayerJoin() => QSBAPIEvents.OnPlayerJoinEvent;
public UnityEvent<uint> OnPlayerLeave() => QSBAPIEvents.OnPlayerLeaveEvent;
public void SetCustomData<T>(uint playerId, string key, T data) => QSBPlayerManager.GetPlayer(playerId).SetCustomData(key, data);
public T GetCustomData<T>(uint playerId, string key) => QSBPlayerManager.GetPlayer(playerId).GetCustomData<T>(key);
public void SendMessage<T>(string messageType, T data, uint to = uint.MaxValue, bool receiveLocally = false)
=> new AddonDataMessage(messageType, data, receiveLocally) {To = to} .Send();
=> new AddonDataMessage(messageType.GetStableHashCode(), data, receiveLocally) { To = to }.Send();
public void RegisterHandler<T>(string messageType, Action<uint, T> handler)
=> AddonDataManager.RegisterHandler(messageType, handler);
=> AddonDataManager.RegisterHandler(messageType.GetStableHashCode(), handler);
}
internal static class QSBAPIEvents
@ -43,6 +46,7 @@ internal static class QSBAPIEvents
}
internal class PlayerEvent : UnityEvent<uint> { }
internal static PlayerEvent OnPlayerJoinEvent = new PlayerEvent();
internal static PlayerEvent OnPlayerLeaveEvent = new PlayerEvent();
internal static readonly PlayerEvent OnPlayerJoinEvent = new();
internal static readonly PlayerEvent OnPlayerLeaveEvent = new();
}

View File

@ -257,7 +257,10 @@ public class RespawnOnDeath : MonoBehaviour
var sectorList = PlayerTransformSync.LocalInstance.SectorDetector.SectorList;
if (sectorList.All(x => x.Type != Sector.Name.TimberHearth))
{
// stops sectors from breaking when you die on TH??
// Spooky scary legacy code?
// Original comment was "stops sectors from breaking when you die on TH??"
// I think dying on TH used to break all the sectors. Something about you not technically re-entering TH when dying inside it.
// I commented out these lines, and everything seemed fine. But I'm not gonna touch them just in case. :P
Locator.GetPlayerSectorDetector().RemoveFromAllSectors();
Locator.GetPlayerCameraDetector().GetComponent<AudioDetector>().DeactivateAllVolumes(0f);
}

View File

@ -645,6 +645,7 @@ public class MenuManager : MonoBehaviour, IAddComponentOnStart
}
LoadGame(PlayerData.GetWarpedToTheEye());
// wait until scene load and then wait until Start has ran
Delay.RunWhen(() => TimeLoop._initialized, QSBNetworkManager.singleton.StartHost);
};
@ -655,6 +656,7 @@ public class MenuManager : MonoBehaviour, IAddComponentOnStart
else
{
LoadGame(PlayerData.GetWarpedToTheEye());
// wait until scene load and then wait until Start has ran
Delay.RunWhen(() => TimeLoop._initialized, QSBNetworkManager.singleton.StartHost);
}
}

View File

@ -19,7 +19,6 @@ using QSB.WorldSync;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.Serialization;
@ -29,20 +28,19 @@ public static class QSBMessageManager
{
#region inner workings
internal static readonly Type[] _types;
internal static readonly Dictionary<Type, ushort> _typeToId = new();
internal static readonly Dictionary<int, Type> _types = new();
private static string _rxPath;
private static string _txPath;
static QSBMessageManager()
{
_types = typeof(QSBMessage).GetDerivedTypes().ToArray();
for (ushort i = 0; i < _types.Length; i++)
foreach (var type in typeof(QSBMessage).GetDerivedTypes())
{
_typeToId.Add(_types[i], i);
var hash = type.FullName.GetStableHashCode();
_types.Add(hash, type);
// call static constructor of message if needed
RuntimeHelpers.RunClassConstructor(_types[i].TypeHandle);
RuntimeHelpers.RunClassConstructor(type.TypeHandle);
}
}
@ -66,6 +64,11 @@ public static class QSBMessageManager
private static void OnServerReceive(QSBMessage msg)
{
if (msg == null)
{
return;
}
if (msg.To == uint.MaxValue)
{
NetworkServer.SendToAll<Wrapper>(msg);
@ -101,9 +104,9 @@ public static class QSBMessageManager
var player = QSBPlayerManager.GetPlayer(msg.From);
if (!player.IsReady
&& player.PlayerId != QSBPlayerManager.LocalPlayerId
&& player.State is ClientState.AliveInSolarSystem or ClientState.AliveInEye or ClientState.DeadInSolarSystem
&& msg is not (PlayerInformationMessage or PlayerReadyMessage or RequestStateResyncMessage or ServerStateMessage))
&& player.PlayerId != QSBPlayerManager.LocalPlayerId
&& player.State is ClientState.AliveInSolarSystem or ClientState.AliveInEye or ClientState.DeadInSolarSystem
&& msg is not (PlayerInformationMessage or PlayerReadyMessage or RequestStateResyncMessage or ServerStateMessage))
{
//DebugLog.ToConsole($"Warning - Got message {msg} from player {msg.From}, but they were not ready. Asking for state resync, just in case.", MessageType.Warning);
new RequestStateResyncMessage().Send();
@ -208,8 +211,12 @@ public static class ReaderWriterExtensions
{
private static QSBMessage ReadQSBMessage(this NetworkReader reader)
{
var id = reader.ReadUShort();
var type = QSBMessageManager._types[id];
var hash = reader.ReadInt();
if (!QSBMessageManager._types.TryGetValue(hash, out var type))
{
DebugLog.DebugWrite($"unknown QSBMessage type with hash {hash}", MessageType.Error);
return null;
}
var msg = (QSBMessage)FormatterServices.GetUninitializedObject(type);
msg.Deserialize(reader);
return msg;
@ -218,8 +225,8 @@ public static class ReaderWriterExtensions
private static void WriteQSBMessage(this NetworkWriter writer, QSBMessage msg)
{
var type = msg.GetType();
var id = QSBMessageManager._typeToId[type];
writer.Write(id);
var hash = type.FullName.GetStableHashCode();
writer.WriteInt(hash);
msg.Serialize(writer);
}
}
}

View File

@ -11,7 +11,7 @@ public abstract class QSBPatch
public void DoPatches(Harmony instance) => instance.PatchAll(GetType());
/// <summary>
/// this is true when a message is received remotely (OnReceiveRemote) or a player leaves (OnRemovePlayer)
/// this is true when a message is received remotely (OnReceiveRemote) or a remote player joins/leaves (OnAddPlayer/OnRemovePlayer)
/// </summary>
public static bool Remote;
}

View File

@ -1,4 +1,5 @@
using OWML.Common;
using QSB.API.Messages;
using QSB.ClientServerStateSync;
using QSB.ClientServerStateSync.Messages;
using QSB.Messaging;
@ -50,5 +51,11 @@ public class RequestStateResyncMessage : QSBMessage
}
new PlayerInformationMessage { To = From }.Send();
// Initial sync of all custom data from APIs
foreach (var kvp in QSBPlayerManager.LocalPlayer._customData)
{
new AddonCustomDataSyncMessage(QSBPlayerManager.LocalPlayerId, kvp.Key, kvp.Value) { To = From }.Send();
}
}
}

View File

@ -1,10 +1,12 @@
using OWML.Common;
using QSB.Animation.Player;
using QSB.API.Messages;
using QSB.Audio;
using QSB.ClientServerStateSync;
using QSB.HUD;
using QSB.Messaging;
using QSB.ModelShip;
using QSB.Patches;
using QSB.Player.Messages;
using QSB.Player.TransformSync;
using QSB.QuantumSync.WorldObjects;
@ -179,10 +181,18 @@ public partial class PlayerInfo
HUDBox.OnRespawn();
}
private Dictionary<string, object> _customData = new();
// internal for RequestStateResyncMessage
internal readonly Dictionary<string, object> _customData = new();
public void SetCustomData<T>(string key, T data)
=> _customData[key] = data;
{
_customData[key] = data;
if (!QSBPatch.Remote)
{
new AddonCustomDataSyncMessage(PlayerId, key, data).Send();
}
}
public T GetCustomData<T>(string key)
{

View File

@ -36,7 +36,9 @@ public class PlayerTransformSync : SectoredTransformSync
var player = new PlayerInfo(this);
QSBPlayerManager.PlayerList.SafeAdd(player);
base.OnStartClient();
QSBPatch.Remote = !isLocalPlayer;
QSBPlayerManager.OnAddPlayer?.SafeInvoke(Player);
QSBPatch.Remote = false;
DebugLog.DebugWrite($"Create Player : {Player}", MessageType.Info);
JoinLeaveSingularity.Create(Player, true);
@ -49,7 +51,7 @@ public class PlayerTransformSync : SectoredTransformSync
JoinLeaveSingularity.Create(Player, false);
// TODO : Maybe move this to a leave event...? Would ensure everything could finish up before removing the player
QSBPatch.Remote = true;
QSBPatch.Remote = !isLocalPlayer;
QSBPlayerManager.OnRemovePlayer?.SafeInvoke(Player);
QSBPatch.Remote = false;
base.OnStopClient();

View File

@ -301,9 +301,9 @@ public class QSBCore : ModBehaviour
foreach (var type in addonAssembly.GetTypes())
{
if (typeof(QSBMessage).IsAssignableFrom(type) || typeof(WorldObjectManager).IsAssignableFrom(type) || typeof(IWorldObject).IsAssignableFrom(type))
if (typeof(WorldObjectManager).IsAssignableFrom(type) || typeof(IWorldObject).IsAssignableFrom(type))
{
DebugLog.ToConsole($"Addon \"{uniqueName}\" cannot be cosmetic, as it creates networking events or objects.", MessageType.Error);
DebugLog.ToConsole($"Addon \"{uniqueName}\" cannot be cosmetic, as it creates networking objects.", MessageType.Error);
return;
}
}

View File

@ -36,6 +36,7 @@ using QSB.WorldSync;
using System;
using System.Linq;
using System.Text.RegularExpressions;
using QSB.API;
using UnityEngine;
namespace QSB;

View File

@ -21,26 +21,24 @@ public static class DebugLog
message = $"[{ProcessInstanceId}] " + message;
}
var @this = QSBCore.Helper != null ? QSBCore.Helper.Console : ModConsole.OwmlConsole;
var Logger = @this.GetValue<IModLogger>("Logger");
var _socket = @this.GetValue<IModSocket>("_socket");
// copied from https://github.com/ow-mods/owml/blob/master/src/OWML.Logging/ModSocketOutput.cs#L33
Logger?.Log($"{type}: {message}");
_socket.WriteToSocket(new ModSocketMessage
{
var Logger = ModConsole.OwmlConsole.GetValue<IModLogger>("Logger");
var _socket = ModConsole.OwmlConsole.GetValue<IModSocket>("_socket");
SenderName = "QSB",
SenderType = GetCallingType(),
Type = type,
Message = message
});
Logger?.Log($"{type}: {message}");
_socket.WriteToSocket(new ModSocketMessage
{
SenderName = "QSB",
SenderType = GetCallingType(),
Type = type,
Message = message
});
if (type == MessageType.Fatal)
{
_socket.Close();
Process.GetCurrentProcess().Kill();
}
if (type == MessageType.Fatal)
{
_socket.Close();
Process.GetCurrentProcess().Kill();
}
}

View File

@ -4,8 +4,10 @@ using OWML.Common;
using QSB.Player;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Serialization.Formatters.Binary;
using System.Text;
using UnityEngine;
using Object = UnityEngine.Object;
@ -237,5 +239,28 @@ public static class Extensions
return sb.ToString();
}
/// <summary>
/// only works for c# serializable objects
/// </summary>
public static byte[] ToBytes(this object obj)
{
using var ms = new MemoryStream();
var bf = new BinaryFormatter();
bf.Serialize(ms, obj);
var bytes = ms.ToArray();
return bytes;
}
/// <summary>
/// only works for c# serializable objects
/// </summary>
public static object ToObject(this byte[] bytes)
{
using var ms = new MemoryStream(bytes);
var bf = new BinaryFormatter();
var obj = bf.Deserialize(ms);
return obj;
}
#endregion
}

View File

@ -7,7 +7,7 @@
"body": "- Disable *all* other mods. (Can heavily affect performance)\n- Make sure you are not running any other network-intensive applications."
},
"uniqueName": "Raicuparta.QuantumSpaceBuddies",
"version": "0.30.1",
"version": "0.30.2",
"owmlVersion": "2.9.5",
"dependencies": [ "_nebula.MenuFramework", "JohnCorby.VanillaFix" ],
"pathsToPreserve": [ "debugsettings.json" ],