From f4c0de9186659d21ec16a0d247e6c073873d5801 Mon Sep 17 00:00:00 2001 From: SokyranTheDragon <36712560+SokyranTheDragon@users.noreply.github.com> Date: Mon, 12 Jun 2023 20:27:04 +0200 Subject: [PATCH 1/8] Add API for accessing player info and things by ID (#7) --- Source/API/Dummy.cs | 22 ++++++++++++++++++ Source/API/Interfaces.cs | 49 ++++++++++++++++++++++++++++++++++++++-- Source/API/MP.cs | 29 +++++++++++++++++++++++- 3 files changed, 97 insertions(+), 3 deletions(-) diff --git a/Source/API/Dummy.cs b/Source/API/Dummy.cs index dfc18bd..f5887b0 100644 --- a/Source/API/Dummy.cs +++ b/Source/API/Dummy.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Generic; using System.Reflection; +using Verse; namespace Multiplayer.API { @@ -101,5 +103,25 @@ public void RegisterPauseLock(PauseLockDelegate pauseLock) { throw new UninitializedAPI(); } + + public Thing GetThingById(int id) + { + throw new UninitializedAPI(); + } + + public bool TryGetThingById(int id, out Thing value) + { + throw new UninitializedAPI(); + } + + public IReadOnlyList GetPlayers() + { + throw new UninitializedAPI(); + } + + public IPlayerInfo GetPlayerById(int id) + { + throw new UninitializedAPI(); + } } } diff --git a/Source/API/Interfaces.cs b/Source/API/Interfaces.cs index 736db20..d5988b3 100644 --- a/Source/API/Interfaces.cs +++ b/Source/API/Interfaces.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Reflection; using Verse; @@ -475,8 +476,8 @@ public interface ISynchronizable void Sync(SyncWorker sync); } - /// - /// An attribute that marks a method for pause lock checking It needs a return type and a single parameter. + /// + /// An attribute that marks a method for pause lock checking It needs a return type and a single parameter. /// [AttributeUsage(AttributeTargets.Method)] public class PauseLockAttribute : Attribute @@ -521,5 +522,49 @@ public interface IAPI void RegisterDialogNodeTree(MethodInfo method); void RegisterPauseLock(PauseLockDelegate pauseLock); + + Thing GetThingById(int id); + bool TryGetThingById(int id, out Thing value); + + IReadOnlyList GetPlayers(); + IPlayerInfo GetPlayerById(int id); + } + + /// + /// An interface for the class holding player data + /// + public interface IPlayerInfo + { + /// + /// ID of the current player + /// + int Id { get; } + /// + /// Username of the current player + /// + string Username { get; } + /// + /// if the current player is Arbiter instance, in every other case + /// + bool IsArbiter { get; } + + /// + /// of the map the player is on + /// + int CurrentMapIndex { get; } + /// + /// The map the current player is on + /// + Map CurrentMap { get; } + + /// + /// List of all the things the player has selected, as numeric IDs + /// + /// Generally use , unless you're able to access the IDs directly. + IReadOnlyList SelectedThingsByIds { get; } + /// + /// List of all the things the player has selected + /// + IReadOnlyList SelectedThings { get; } } } diff --git a/Source/API/MP.cs b/Source/API/MP.cs index 83ae01d..9863b2f 100644 --- a/Source/API/MP.cs +++ b/Source/API/MP.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Reflection; @@ -245,7 +246,7 @@ static MP() /// /// public static void RegisterSyncDialogNodeTree(MethodInfo method) => Sync.RegisterDialogNodeTree(method); - + /// /// Registers a delegate which will be called to check if the game should be paused on specific map. /// In case async time is active, only that map will be paused, otherwise all of them will be paused. @@ -266,5 +267,31 @@ static MP() /// /// public static void RegisterPauseLock(PauseLockDelegate pauseLock) => Sync.RegisterPauseLock(pauseLock); + + /// + /// Retrieves a with a provided id + /// + /// for the to retrieve + /// with a specific numeric ID. + public static Thing GetThingById(int id) => Sync.GetThingById(id); + /// + /// Retrieves a with a provided id and returns a for success/failure + /// + /// for the to retrieve + /// The value of retrieved , if any. + /// if successful + public static bool TryGetThingById(int id, out Thing value) => Sync.TryGetThingById(id, out value); + + /// + /// Retrieves a list of for every player + /// + /// List with every + public static IReadOnlyList GetPlayers() => Sync.GetPlayers(); + /// + /// Retrieves a with a specific + /// + /// of the player to retrieve + /// Player with specified ID number + public static IPlayerInfo GetPlayerById(int id) => Sync.GetPlayerById(id); } } From c580796f01dc803c2e022cafd774da21094f1eaa Mon Sep 17 00:00:00 2001 From: Zetrith Date: Thu, 6 Jul 2023 23:19:05 +0200 Subject: [PATCH 2/8] Version 0.5, ThingFilterContext and ISyncSimple (#10) --- Source/API/Dummy.cs | 5 +++++ Source/API/Interfaces.cs | 21 +++++++++++++++++++++ Source/API/MP.cs | 18 ++++++++++++++---- Source/API/Properties/AssemblyInfo.cs | 2 +- Source/MultiplayerAPI.csproj | 5 +++-- 5 files changed, 44 insertions(+), 7 deletions(-) diff --git a/Source/API/Dummy.cs b/Source/API/Dummy.cs index f5887b0..7130f7c 100644 --- a/Source/API/Dummy.cs +++ b/Source/API/Dummy.cs @@ -24,6 +24,11 @@ class Dummy : IAPI public bool IsExecutingSyncCommandIssuedBySelf => false; + public void SetThingFilterContext(ThingFilterContext context) + { + throw new UninitializedAPI(); + } + public void WatchBegin() { throw new UninitializedAPI(); diff --git a/Source/API/Interfaces.cs b/Source/API/Interfaces.cs index d5988b3..a270f48 100644 --- a/Source/API/Interfaces.cs +++ b/Source/API/Interfaces.cs @@ -490,6 +490,25 @@ public class PauseLockAttribute : Attribute /// if time should be paused on the specific map public delegate bool PauseLockDelegate(Map map); + /// + /// Objects implementing this marker interface sync their exact type and all declared fields. + /// This is useful when syncing type hierarchies by value. + /// The synced object is created uninitialized using reflection (no constructor is called). + /// + public interface ISyncSimple { } + + /// + /// A ThingFilter context provides information for syncing ThingFilter interactions. + /// Inheriting objects should store the ThingFilter's owner in a record property. + /// The type exists because vanilla ThingFilters don't store references to their owners. + /// + public abstract record ThingFilterContext : ISyncSimple + { + public abstract ThingFilter Filter { get; } + public abstract ThingFilter ParentFilter { get; } + public virtual IEnumerable HiddenFilters => null; + } + public interface IAPI { bool IsHosting { get; } @@ -498,6 +517,8 @@ public interface IAPI bool IsExecutingSyncCommand { get; } bool IsExecutingSyncCommandIssuedBySelf { get; } + void SetThingFilterContext(ThingFilterContext context); + void WatchBegin(); void Watch(Type targetType, string fieldName, object target = null, object index = null); void Watch(object target, string fieldName, object index = null); diff --git a/Source/API/MP.cs b/Source/API/MP.cs index 9863b2f..ceac0c7 100644 --- a/Source/API/MP.cs +++ b/Source/API/MP.cs @@ -15,7 +15,7 @@ namespace Multiplayer.API public static class MP { /// Contains the API version - public const string API = "0.3"; + public const string API = "0.5"; /// /// Returns if API is initialized. @@ -67,16 +67,26 @@ static MP() /// public static string PlayerName => Sync.PlayerName; - /// - /// Returns if currently there's a sync command being executed. + /// + /// Returns if currently there's a sync command being executed. /// public static bool IsExecutingSyncCommand => Sync.IsExecutingSyncCommand; /// - /// Returns if currently there's a sync command being executed that was issued by the current player. + /// Returns if currently there's a sync command being executed that was issued by the current player. /// public static bool IsExecutingSyncCommandIssuedBySelf => Sync.IsExecutingSyncCommandIssuedBySelf; + /// + /// Used to set the ThingFilter context for interactions with ThingFilter UI. + /// Set the context before drawing the ThingFilter and then set it back to after it's drawn. + /// + /// This method is not "reentrant". If you call it twice without setting the context back to , the second call will throw an exception. + /// + /// + /// The ThingFilter context object + public static void SetThingFilterContext(ThingFilterContext context) => Sync.SetThingFilterContext(context); + /// /// Starts a new synchronization stack. /// diff --git a/Source/API/Properties/AssemblyInfo.cs b/Source/API/Properties/AssemblyInfo.cs index fbfd23e..9a26b08 100644 --- a/Source/API/Properties/AssemblyInfo.cs +++ b/Source/API/Properties/AssemblyInfo.cs @@ -18,7 +18,7 @@ // The form "{Major}.{Minor}.*" will automatically update the build and revision, // and "{Major}.{Minor}.{Build}.*" will update just the revision. -[assembly: AssemblyVersion("0.3")] +[assembly: AssemblyVersion(MP.API)] [assembly: AssemblyFileVersion(MP.API)] // The following attributes are used to specify the signing key for the assembly, diff --git a/Source/MultiplayerAPI.csproj b/Source/MultiplayerAPI.csproj index b7e095d..b0de576 100644 --- a/Source/MultiplayerAPI.csproj +++ b/Source/MultiplayerAPI.csproj @@ -2,7 +2,7 @@ RimWorld.MultiplayerAPI - 0.4 + 0.5 notfood https://i.imgur.com/amy7QJE.png notfood @@ -24,7 +24,8 @@ false false None - 0.4 + 0.5 + 10 From cdf75bea462a710d4f5ff57d8c528cc5201d8c9b Mon Sep 17 00:00:00 2001 From: Zetrith Date: Thu, 6 Jul 2023 23:32:42 +0200 Subject: [PATCH 3/8] Convert line endings --- Source/API/Interfaces.cs | 32 ++++++++++++++++---------------- Source/API/MP.cs | 12 ++++++------ 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/Source/API/Interfaces.cs b/Source/API/Interfaces.cs index a270f48..cdc9ad1 100644 --- a/Source/API/Interfaces.cs +++ b/Source/API/Interfaces.cs @@ -551,40 +551,40 @@ public interface IAPI IPlayerInfo GetPlayerById(int id); } - /// - /// An interface for the class holding player data + /// + /// An interface for the class holding player data /// public interface IPlayerInfo { - /// - /// ID of the current player + /// + /// ID of the current player /// int Id { get; } - /// - /// Username of the current player + /// + /// Username of the current player /// string Username { get; } - /// - /// if the current player is Arbiter instance, in every other case + /// + /// if the current player is Arbiter instance, in every other case /// bool IsArbiter { get; } - /// - /// of the map the player is on + /// + /// of the map the player is on /// int CurrentMapIndex { get; } - /// - /// The map the current player is on + /// + /// The map the current player is on /// Map CurrentMap { get; } - /// - /// List of all the things the player has selected, as numeric IDs + /// + /// List of all the things the player has selected, as numeric IDs /// /// Generally use , unless you're able to access the IDs directly. IReadOnlyList SelectedThingsByIds { get; } - /// - /// List of all the things the player has selected + /// + /// List of all the things the player has selected /// IReadOnlyList SelectedThings { get; } } diff --git a/Source/API/MP.cs b/Source/API/MP.cs index ceac0c7..4743a78 100644 --- a/Source/API/MP.cs +++ b/Source/API/MP.cs @@ -278,17 +278,17 @@ static MP() /// public static void RegisterPauseLock(PauseLockDelegate pauseLock) => Sync.RegisterPauseLock(pauseLock); - /// - /// Retrieves a with a provided id - /// - /// for the to retrieve + /// + /// Retrieves a with a provided id + /// + /// for the to retrieve /// with a specific numeric ID. public static Thing GetThingById(int id) => Sync.GetThingById(id); /// /// Retrieves a with a provided id and returns a for success/failure /// - /// for the to retrieve - /// The value of retrieved , if any. + /// for the to retrieve + /// The value of retrieved , if any. /// if successful public static bool TryGetThingById(int id, out Thing value) => Sync.TryGetThingById(id, out value); From 58073886162a5a87f79bd31e077c66bb291afb3e Mon Sep 17 00:00:00 2001 From: SokyranTheDragon <36712560+SokyranTheDragon@users.noreply.github.com> Date: Wed, 27 Dec 2023 00:56:47 +0100 Subject: [PATCH 4/8] Updates and additions to API (#9) - Added CanUseDevMode bool property to check if the current player has access to dev mode - Added sync method/delegate register method for lambdas/local funcs - Added SetHostOnly for sync methods/delegates to bring them on par with sync fields - For sync methods/delegates, added transforming arguments, target, and (only for delegates) fields - Added ExposeFields for sync delegates - Added SetPreInvoke/SetPostInvoke for sync delegates (it was possible to use them before by casting ISyncDelegate to ISyncMethod) - Added CancelIfNoSelectedMapObjects and CancelIfNoSelectedWorldObjects for sync delegates, matching sync methods - Marked CancelIfNoSelectedObjects as obsolete with a message to use CancelIfNoSelectedMapObjects instead (still works as before) as it's more descriptive - Minor changes to XML documentation --- Source/API/Dummy.cs | 34 +++++++++++ Source/API/Interfaces.cs | 113 ++++++++++++++++++++++++++++++++++- Source/API/MP.cs | 92 +++++++++++++++++++++++++++- Source/API/MPTypes.cs | 18 ++++++ Source/API/Serializer.cs | 27 +++++++++ Source/IsExternalInit.cs | 9 +++ Source/MultiplayerAPI.csproj | 2 +- 7 files changed, 291 insertions(+), 4 deletions(-) create mode 100644 Source/API/Serializer.cs create mode 100644 Source/IsExternalInit.cs diff --git a/Source/API/Dummy.cs b/Source/API/Dummy.cs index 7130f7c..ec92d3c 100644 --- a/Source/API/Dummy.cs +++ b/Source/API/Dummy.cs @@ -24,6 +24,10 @@ class Dummy : IAPI public bool IsExecutingSyncCommandIssuedBySelf => false; + public bool CanUseDevMode => false; + + public bool InInterface => false; + public void SetThingFilterContext(ThingFilterContext context) { throw new UninitializedAPI(); @@ -79,6 +83,16 @@ public ISyncMethod RegisterSyncMethod(MethodInfo method, SyncType[] argTypes) throw new UninitializedAPI(); } + public ISyncMethod RegisterSyncMethodLambda(Type parentType, string parentMethod, int lambdaOrdinal, Type[] parentArgs = null, ParentMethodType parentParentMethodType = ParentMethodType.Normal) + { + throw new UninitializedAPI(); + } + + public ISyncMethod RegisterSyncMethodLambdaInGetter(Type parentType, string parentMethod, int lambdaOrdinal) + { + throw new UninitializedAPI(); + } + public ISyncDelegate RegisterSyncDelegate(Type inType, string nestedType, string methodName, string[] fields, Type[] args = null) { throw new UninitializedAPI(); @@ -89,6 +103,21 @@ public ISyncDelegate RegisterSyncDelegate(Type type, string nestedType, string m throw new UninitializedAPI(); } + public ISyncDelegate RegisterSyncDelegateLambda(Type parentType, string parentMethod, int lambdaOrdinal, Type[] parentArgs = null, ParentMethodType parentParentMethodType = ParentMethodType.Normal) + { + throw new UninitializedAPI(); + } + + public ISyncDelegate RegisterSyncDelegateLambdaInGetter(Type parentType, string parentMethod, int lambdaOrdinal) + { + throw new UninitializedAPI(); + } + + public ISyncDelegate RegisterSyncDelegateLocalFunc(Type parentType, string parentMethod, string localFuncName, Type[] parentArgs = null) + { + throw new UninitializedAPI(); + } + public void RegisterSyncWorker(SyncWorkerDelegate syncWorkerDelegate, Type targetType = null, bool isImplicit = false, bool shouldConstruct = false) { throw new UninitializedAPI(); @@ -109,6 +138,11 @@ public void RegisterPauseLock(PauseLockDelegate pauseLock) throw new UninitializedAPI(); } + public void RegisterDefaultLetterChoice(MethodInfo method, Type letterType = null) + { + throw new UninitializedAPI(); + } + public Thing GetThingById(int id) { throw new UninitializedAPI(); diff --git a/Source/API/Interfaces.cs b/Source/API/Interfaces.cs index cdc9ad1..b77356c 100644 --- a/Source/API/Interfaces.cs +++ b/Source/API/Interfaces.cs @@ -163,6 +163,12 @@ public interface ISyncMethod : ISyncCall /// self ISyncMethod SetDebugOnly(); + /// + /// Instructs SyncMethod to synchronize only if it's invoked by the host. + /// + /// self + ISyncMethod SetHostOnly(); + /// /// Adds an Action that runs before a call is replicated on client. /// @@ -184,6 +190,27 @@ public interface ISyncMethod : ISyncCall /// self ISyncMethod SetVersion(int version); + /// + /// Transforms a parameter of a method, result of which will be synced instead of the parameter itself + /// + /// Index at which parameter is going to be transformed + /// A serializer which will transform the argument before and after syncing + /// Check to ensure is the same type as the argument will be dropped. More error-prone (and only detectable at runtime), but allows transforming arguments even if the current assembly cannot reference the specific type. + /// The type which will be transformed before, and type that will be transformed back into after syncing + /// The type which will be synced to other players instead of + /// self + ISyncMethod TransformArgument(int index, Serializer serializer, bool skipTypeCheck = false); + + /// + /// Transforms an object instance within which the synced method is declared, result of which will be synced instead of the instance itself + /// + /// A serializer which will transform the target instance before and after syncing + /// Check to ensure is the same type as the target instance will be dropped fully. More error-prone (and only detectable at runtime), but allows transforming target instance even if the current assembly cannot reference the specific type. + /// The type which will be transformed before, and type that will be transformed back into after syncing + /// The type which will be synced to other players instead of + /// self + ISyncMethod TransformTarget(Serializer serializer, bool skipTypeCheck = false); + string ToString(); } @@ -195,14 +222,14 @@ public interface ISyncMethod : ISyncCall public interface ISyncDelegate : ISyncCall { /// - /// Instructs ISyncDelegate to cancel synchronization except for + /// Instructs ISyncDelegate to cancel synchronization except for /// /// self /// field names to be excluded ISyncDelegate CancelIfAnyFieldNull(params string[] blacklist); /// - /// Instructs ISyncDelegate to cancel synchronization except for + /// Instructs ISyncDelegate to cancel synchronization except for /// /// self /// Whitelist. @@ -212,8 +239,29 @@ public interface ISyncDelegate : ISyncCall /// Cancels if no selected objects. /// /// self + [Obsolete($"Use {nameof(CancelIfNoSelectedMapObjects)} instead")] ISyncDelegate CancelIfNoSelectedObjects(); + /// + /// Instructs SyncDelegate to cancel synchronization if no map objects were selected during call replication. + /// + /// self + ISyncDelegate CancelIfNoSelectedMapObjects(); + + /// + /// Instructs SyncDelegate to cancel synchronization if no world objects were selected during call replication. + /// + /// self + ISyncDelegate CancelIfNoSelectedWorldObjects(); + + /// + /// Use parameter's type's IExposable interface to transfer its data to other clients. + /// + /// IExposable is the interface used for saving data to the save which means it utilizes IExposable.ExposeData() method. + /// self + /// Fields to sync by using IExposable. + ISyncDelegate ExposeFields(params string[] fields); + /// /// Removes the nulls from lists. /// @@ -234,6 +282,58 @@ public interface ISyncDelegate : ISyncCall /// self ISyncDelegate SetDebugOnly(); + /// + /// Instructs SyncDelegate to synchronize only if it's invoked by the host. + /// + /// self + ISyncDelegate SetHostOnly(); + + /// + /// Adds an Action that runs before a call is replicated on client. + /// + /// An action ran before a call is replicated on client. Called with target and value. + /// self + ISyncDelegate SetPreInvoke(Action action); + + /// + /// Adds an Action that runs after a call is replicated on client. + /// + /// An action ran after a call is replicated on client. Called with target and value. + /// self + ISyncDelegate SetPostInvoke(Action action); + + /// + /// Transforms a parameter of a method, result of which will be synced instead of the parameter itself + /// + /// Index at which parameter is going to be transformed + /// A serializer which will transform the argument before and after syncing + /// Check to ensure is the same type as the argument will be dropped. More error-prone (and only detectable at runtime), but allows transforming arguments even if the current assembly cannot reference the specific type. + /// The type which will be transformed before, and type that will be transformed back into after syncing + /// The type which will be synced to other players instead of + /// self + ISyncDelegate TransformArgument(int index, Serializer serializer, bool skipTypeCheck = false); + + /// + /// Transforms an object instance within which the synced method is declared, result of which will be synced instead of the instance itself + /// + /// A serializer which will transform the target instance before and after syncing + /// Check to ensure is the same type as the target instance will be dropped. More error-prone (and only detectable at runtime), but allows transforming target instance even if the current assembly cannot reference the specific type. + /// The type which will be transformed before, and type that will be transformed back into after syncing + /// The type which will be synced to other players instead of + /// self + ISyncDelegate TransformTarget(Serializer serializer, bool skipTypeCheck = false); + + /// + /// Transforms a field inside of the delegate, result of which will be synced instead of the field itself + /// + /// Name of a field which will be transformed before and after syncing. Supports fields inside of fields referencing other delegates, for example: `firstDelegate/anotherDelegate/targetField` + /// A serializer which will transform the field before and after syncing + /// Check to ensure is the same type as the field will be dropped. More error-prone (and only detectable at runtime), but allows transforming fields even if the current assembly cannot reference the specific type. + /// The type which will be transformed before, and type that will be transformed back into after syncing + /// The type which will be synced to other players instead of + /// + ISyncDelegate TransformField(string field, Serializer serializer, bool skipTypeCheck = false); + string ToString(); } @@ -516,6 +616,8 @@ public interface IAPI string PlayerName { get; } bool IsExecutingSyncCommand { get; } bool IsExecutingSyncCommandIssuedBySelf { get; } + bool CanUseDevMode { get; } + bool InInterface { get; } void SetThingFilterContext(ThingFilterContext context); @@ -532,9 +634,14 @@ public interface IAPI ISyncMethod RegisterSyncMethod(Type type, string methodOrPropertyName, SyncType[] argTypes = null); ISyncMethod RegisterSyncMethod(MethodInfo method, SyncType[] argTypes); + ISyncMethod RegisterSyncMethodLambda(Type parentType, string parentMethod, int lambdaOrdinal, Type[] parentArgs = null, ParentMethodType parentParentMethodType = ParentMethodType.Normal); + ISyncMethod RegisterSyncMethodLambdaInGetter(Type parentType, string parentMethod, int lambdaOrdinal); ISyncDelegate RegisterSyncDelegate(Type type, string nestedType, string method); ISyncDelegate RegisterSyncDelegate(Type inType, string nestedType, string methodName, string[] fields, Type[] args = null); + ISyncDelegate RegisterSyncDelegateLambda(Type parentType, string parentMethod, int lambdaOrdinal, Type[] parentArgs = null, ParentMethodType parentParentMethodType = ParentMethodType.Normal); + ISyncDelegate RegisterSyncDelegateLambdaInGetter(Type parentType, string parentMethod, int lambdaOrdinal); + ISyncDelegate RegisterSyncDelegateLocalFunc(Type parentType, string parentMethod, string localFuncName, Type[] parentArgs = null); void RegisterSyncWorker(SyncWorkerDelegate syncWorkerDelegate, Type targetType = null, bool isImplicit = false, bool shouldConstruct = false); @@ -543,6 +650,8 @@ public interface IAPI void RegisterDialogNodeTree(MethodInfo method); void RegisterPauseLock(PauseLockDelegate pauseLock); + + void RegisterDefaultLetterChoice(MethodInfo method, Type letterType = null); Thing GetThingById(int id); bool TryGetThingById(int id, out Thing value); diff --git a/Source/API/MP.cs b/Source/API/MP.cs index 4743a78..5da7ea4 100644 --- a/Source/API/MP.cs +++ b/Source/API/MP.cs @@ -77,6 +77,31 @@ static MP() /// public static bool IsExecutingSyncCommandIssuedBySelf => Sync.IsExecutingSyncCommandIssuedBySelf; + /// + /// Returns if the current player is allowed to use dev mode commands. + /// + public static bool CanUseDevMode => Sync.CanUseDevMode; + + /// + /// Used to determine if the currently running code is potentially unsafe for modifying the game state, allowing for them to be handled differently or synchronized. + /// An example of where this could be useful is harmony patches (besides transpilers) on sync methods - the method would get cancelled and synchronized, but the patches will still run. + /// In situation like that the patch should be cancelled if returns , as it'll run again after synchronizing. + /// + /// + /// Returns if all the following conditions are : + /// + /// Multiplayer mod is enabled + /// The game is currently in multiplayer mode + /// The game is currently not ticking (interface drawing code, etc.) + /// The game is not running sync commands + /// The multiplayer game is not being reloaded + /// is + /// is + /// + /// If any of them are , it returns . + /// + public static bool InInterface => Sync.InInterface; + /// /// Used to set the ThingFilter context for interactions with ThingFilter UI. /// Set the context before drawing the ThingFilter and then set it back to after it's drawn. @@ -199,6 +224,31 @@ static MP() /// public static ISyncMethod RegisterSyncMethod(MethodInfo method, SyncType[] argTypes = null) => Sync.RegisterSyncMethod(method, argTypes); + /// + /// Registers a compiler-generated lambda for syncing and returns its , you will have to figure out the ordinal of your target by decompiling. + /// + /// Type that contains the method. + /// Name of the method the lambda is a child of. + /// For example, with lambdaOrdinal = 3: <FillTab>b__10_3 + /// Arguments of the parent method. Needed if there's an more than 1 method with the same name. + /// The type of the parent method. + /// A new registered + public static ISyncMethod RegisterSyncMethodLambda(Type parentType, string parentMethod, int lambdaOrdinal, Type[] parentArgs = null, ParentMethodType parentParentMethodType = ParentMethodType.Normal) + => Sync.RegisterSyncMethodLambda(parentType, parentMethod, lambdaOrdinal, parentArgs, parentParentMethodType); + + /// + /// Registers a compiler-generated lambda for syncing and returns its , you will have to figure out the ordinal of your target by decompiling. + /// + /// Exists for convenience, the outcome will be the same as calling with parentMethod set to + /// + /// + /// Type that contains the method. + /// Name of the method the lambda is a child of. + /// For example, with lambdaOrdinal = 3: <FillTab>b__10_3 + /// A new registered + public static ISyncMethod RegisterSyncMethodLambdaInGetter(Type parentType, string parentMethod, int lambdaOrdinal) + => Sync.RegisterSyncMethodLambdaInGetter(parentType, parentMethod, lambdaOrdinal); + /// /// Registers the syncDelegate. Handles anonymous nested types, you will have to figure out the name of your target by decompiling. /// @@ -218,6 +268,39 @@ static MP() /// Fields. /// Arguments. public static ISyncDelegate RegisterSyncDelegate(Type inType, string nestedType, string methodName, string[] fields, Type[] args = null) => Sync.RegisterSyncDelegate(inType, nestedType, methodName, fields, args); + + /// + /// Registers the syncDelegate. Handles anonymous nested types, you will have to figure out the name and lambda ordinal of your target by decompiling. + /// + /// The sync delegate. + /// Type that contains the method. + /// Name of the method the lambda is a child of. + /// For example, with lambdaOrdinal = 3: <FillTab>b__10_3 + /// Arguments of the parent method. Needed if there's an more than 1 method with the same name. + /// The type of the parent method. + public static ISyncDelegate RegisterSyncDelegateLambda(Type parentType, string parentMethod, int lambdaOrdinal, Type[] parentArgs = null, ParentMethodType parentParentMethodType = ParentMethodType.Normal) + => Sync.RegisterSyncDelegateLambda(parentType, parentMethod, lambdaOrdinal, parentArgs, parentParentMethodType); + + /// + /// Registers the syncDelegate. Handles anonymous nested types, you will have to figure out the name and lambda ordinal of your target by decompiling. + /// + /// The sync delegate. + /// Type that contains the method. + /// Name of the method the lambda is a child of. + /// For example, with lambdaOrdinal = 3: <FillTab>b__10_3 + public static ISyncDelegate RegisterSyncDelegateLambdaInGetter(Type parentType, string parentMethod, int lambdaOrdinal) + => Sync.RegisterSyncDelegateLambdaInGetter(parentType, parentMethod, lambdaOrdinal); + + /// + /// Registers the syncDelegate. Handles anonymous nested types, you will have to figure out the name and lambda ordinal of your target by decompiling. + /// + /// The sync delegate. + /// Type that contains the method. + /// Name of the method the lambda is a child of. + /// For example, for local function named Start: <DoWindowContents>g__Start|10 + /// Arguments of the parent method. Needed if there's an more than 1 method with the same name. + public static ISyncDelegate RegisterSyncDelegateLocalFunc(Type parentType, string parentMethod, string localFuncName, Type[] parentArgs = null) + => Sync.RegisterSyncDelegateLocalFunc(parentType, parentMethod, localFuncName, parentArgs); /// /// Registers the SyncWorker based on SyncWorkerDelegate. @@ -244,7 +327,6 @@ static MP() /// Registers a method which opens a . The options picked by players will then be synced between all clients. /// /// MethodInfo of a method to register - /// Method's parameter types /// /// It's recommended to use instead, unless you have to otherwise. /// It can be combined with so the call will be replicated by the MPApi on all clients automatically. @@ -278,6 +360,14 @@ static MP() /// public static void RegisterPauseLock(PauseLockDelegate pauseLock) => Sync.RegisterPauseLock(pauseLock); + /// + /// In multiplayer, choice letters don't pause the game when expiring - instead, using a default choice (usually rejecting, if applicable). + /// This does not automatically sync choices, and the choices themselves need syncing through a sync method/delegate. + /// + /// Method that will be called when the letter expires. Can either be a method inside of the letter class itself, or a static method (with the instance as the parameter). + /// The type of the letter. If null, will be used. + public static void RegisterDefaultLetterChoice(MethodInfo method, Type letterType = null) => Sync.RegisterDefaultLetterChoice(method, letterType); + /// /// Retrieves a with a provided id /// diff --git a/Source/API/MPTypes.cs b/Source/API/MPTypes.cs index 9a17052..da82619 100644 --- a/Source/API/MPTypes.cs +++ b/Source/API/MPTypes.cs @@ -118,6 +118,7 @@ public SyncFieldAttribute(SyncContext context = SyncContext.None) /// /// /// An example showing how to mark a method for syncing. + /// /// [SyncDialogNodeTree] /// public void MyMethod(...) /// { @@ -153,4 +154,21 @@ public static implicit operator SyncType(Type type) return new SyncType(type); } } + + /// Specifies the type of method. Those values are identical to Harmony's MethodType enum, and exist here to prevent reliance of this API on Harmony. + public enum ParentMethodType + { + /// This is a normal method + Normal, + /// This is a getter + Getter, + /// This is a setter + Setter, + /// This is a constructor + Constructor, + /// This is a static constructor + StaticConstructor, + /// This targets the MoveNext method of the enumerator result + Enumerator, + } } \ No newline at end of file diff --git a/Source/API/Serializer.cs b/Source/API/Serializer.cs new file mode 100644 index 0000000..850ce85 --- /dev/null +++ b/Source/API/Serializer.cs @@ -0,0 +1,27 @@ +using System; + +namespace Multiplayer.API +{ + public record Serializer( + Func Writer, // (live, target, args) => networked + Func Reader // (networked) => live + ); + + public static class Serializer + { + public static Serializer New(Func writer, Func reader) + { + return new(writer, reader); + } + + public static Serializer New(Func writer, Func reader) + { + return new((live, _, _) => writer(live), reader); + } + + public static Serializer SimpleReader(Func reader) + { + return new((_, _, _) => null, _ => reader()); + } + } +} \ No newline at end of file diff --git a/Source/IsExternalInit.cs b/Source/IsExternalInit.cs new file mode 100644 index 0000000..b2c4d69 --- /dev/null +++ b/Source/IsExternalInit.cs @@ -0,0 +1,9 @@ +using System.ComponentModel; + +// ReSharper disable once CheckNamespace +namespace System.Runtime.CompilerServices; + +// Required for records/init only properties +[EditorBrowsable(EditorBrowsableState.Never)] +internal static class IsExternalInit +{} \ No newline at end of file diff --git a/Source/MultiplayerAPI.csproj b/Source/MultiplayerAPI.csproj index b0de576..35468e8 100644 --- a/Source/MultiplayerAPI.csproj +++ b/Source/MultiplayerAPI.csproj @@ -29,7 +29,7 @@ - + From 0405bb35888c67de0b13202f1549245261d6ab9e Mon Sep 17 00:00:00 2001 From: SokyranTheDragon <36712560+SokyranTheDragon@users.noreply.github.com> Date: Wed, 27 Dec 2023 01:00:18 +0100 Subject: [PATCH 5/8] Update API for session rework (#11) --- Source/API/Dummy.cs | 15 +++++ Source/API/Interfaces.cs | 136 +++++++++++++++++++++++++++++++++++++++ Source/API/MP.cs | 37 +++++++++++ Source/API/MPTypes.cs | 129 +++++++++++++++++++++++++++++++++++++ 4 files changed, 317 insertions(+) diff --git a/Source/API/Dummy.cs b/Source/API/Dummy.cs index ec92d3c..c8c1ab6 100644 --- a/Source/API/Dummy.cs +++ b/Source/API/Dummy.cs @@ -162,5 +162,20 @@ public IPlayerInfo GetPlayerById(int id) { throw new UninitializedAPI(); } + + public ISessionManager GetGlobalSessionManager() + { + throw new UninitializedAPI(); + } + + public ISessionManager GetLocalSessionManager(Map map) + { + throw new UninitializedAPI(); + } + + public void SetCurrentSessionWithTransferables(ISessionWithTransferables session) + { + throw new UninitializedAPI(); + } } } diff --git a/Source/API/Interfaces.cs b/Source/API/Interfaces.cs index b77356c..d636d0e 100644 --- a/Source/API/Interfaces.cs +++ b/Source/API/Interfaces.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Reflection; +using RimWorld; using Verse; namespace Multiplayer.API @@ -649,6 +650,7 @@ public interface IAPI void RegisterDialogNodeTree(MethodInfo method); + [Obsolete($"Use {nameof(Session)} instead.")] void RegisterPauseLock(PauseLockDelegate pauseLock); void RegisterDefaultLetterChoice(MethodInfo method, Type letterType = null); @@ -658,6 +660,10 @@ public interface IAPI IReadOnlyList GetPlayers(); IPlayerInfo GetPlayerById(int id); + + ISessionManager GetGlobalSessionManager(); + ISessionManager GetLocalSessionManager(Map map); + void SetCurrentSessionWithTransferables(ISessionWithTransferables session); } /// @@ -697,4 +703,134 @@ public interface IPlayerInfo /// IReadOnlyList SelectedThings { get; } } + + public interface ISessionManager + { + /// + /// Returns the list of all currently active sessions for this specific . + /// + IReadOnlyList AllSessions { get; } + /// + /// Returns the list of all currently active exposable sessions for this specific . + /// + IReadOnlyList ExposableSessions { get; } + /// + /// Returns the list of all currently active semi-persistent sessions for this specific . + /// + IReadOnlyList SemiPersistentSessions { get; } + /// + /// Returns the list of all currently active ticking sessions for this specific . + /// + IReadOnlyList TickingSessions { get; } + /// + /// A convenience property for checking if any of the sessions is active. + /// + bool AnySessionActive { get; } + + /// + /// Adds a new session to the list of active sessions. + /// + /// The session to try to add to active sessions. + /// if the session was added to active ones, if there was a conflict between sessions. + bool AddSession(Session session); + + /// + /// Tries to get a conflicting session (through the use of ) or, if there was none, returns the input . + /// + /// The session to try to add to active sessions. + /// A session that was conflicting with the input one, or the input itself if there were no conflicts. It may be of a different type than the input. + Session GetOrAddSessionAnyConflict(Session session); + + /// + /// Tries to get a conflicting session (through the use of ) or, if there was none, returns the input . + /// + /// The session to try to add to active sessions. + /// A session that was conflicting with the input one if it's the same type (other is T), null if it's a different type, or the input itself if there were no conflicts. + T GetOrAddSession(T session) where T : Session; + + /// + /// Tries to remove a session from active ones. + /// + /// The session to try to remove from the active sessions. + /// if successfully removed from . Doesn't correspond to if it was successfully removed from other lists of sessions. + bool RemoveSession(Session session); + + /// + /// Returns the first active session of specific type. + /// + /// Type of the session to retrieve. + /// The first session of specified type, or if there are none. + T GetFirstOfType() where T : Session; + + /// + /// Returns the session with specific ID of specific type. + /// + /// The ID of the session to search for. + /// Type of the session to retrieve. + /// The session with provided ID and of specified type, or if there are none. + T GetFirstWithId(int id) where T : Session; + + /// + /// Returns the session with specific ID. + /// + /// The ID of the session to search for. + /// The session with provided ID, or if there are none. + Session GetFirstWithId(int id); + + /// + /// Checks if any of active sessions is currently pausing the game. + /// + /// The map at which the sessions would check if the game is paused. Global session manager accepts for global pausing. + /// if any session is active, otherwise. + /// Local session managers expect the to be the same as the map it's attached to. + bool IsAnySessionCurrentlyPausing(Map map); // Is it necessary for the API? + } + + /// + /// Required by sessions dealing with transferables, like trading or caravan forming. By implementing this interface, Multiplayer will handle majority of syncing of changes in transferables. + /// When drawing the dialog tied to this session, you'll have to set to the proper session, and set it to null once done. + /// + /// For safety, make sure to set in and unset in . + public interface ISessionWithTransferables + { + /// + /// Used when syncing data across players, specifically to retrieve based on the it has. + /// + /// of the . + /// which corresponds to a with specific . + Transferable GetTransferableByThingId(int thingId); + + /// + /// Called when the count in a specific was changed. + /// + /// Transferable whose count was changed. + void Notify_CountChanged(Transferable tr); + } + + /// + /// Interface used by sessions that have restrictions based on other existing sessions, for example limiting them to only 1 session of specific type. + /// + public interface ISessionWithCreationRestrictions + { + /// + /// Method used to check if the current session can be created by checking other . + /// Only sessions in the current context are checked (local map sessions or global sessions). + /// + /// The other session the current one is checked against. Can be of different type. + /// Currently only the current class checks against the existing ones - the existing classed don't check against this one. + /// if the current session should be created, otherwise + bool CanExistWith(Session other); + } + + /// + /// Used by sessions that are are required to tick together with the map/world. + /// + public interface ITickingSession + { + /// + /// Called once per session when the map (for local sessions) or the world (for global sessions) is ticking. + /// + /// The sessions are iterated over backwards using a for loop, so it's safe for them to remove themselves from the session manager. + void Tick(); + } } diff --git a/Source/API/MP.cs b/Source/API/MP.cs index 5da7ea4..407e49d 100644 --- a/Source/API/MP.cs +++ b/Source/API/MP.cs @@ -358,6 +358,7 @@ public static ISyncDelegate RegisterSyncDelegateLocalFunc(Type parentType, strin /// RegisterPauseLock(map => MyOtherClass.shouldPause); /// /// + [Obsolete($"Use {nameof(Session)} instead.")] public static void RegisterPauseLock(PauseLockDelegate pauseLock) => Sync.RegisterPauseLock(pauseLock); /// @@ -393,5 +394,41 @@ public static ISyncDelegate RegisterSyncDelegateLocalFunc(Type parentType, strin /// of the player to retrieve /// Player with specified ID number public static IPlayerInfo GetPlayerById(int id) => Sync.GetPlayerById(id); + + /// + /// Retrieves the global (world) session manager. + /// + /// The global (world) session manager. + /// As long as a multiplayer session is active, there should always be a global session manager. This method should never return in such cases, unless something is very broken. + public static ISessionManager GetGlobalSessionManager() => Sync.GetGlobalSessionManager(); + /// + /// Retrieves the local (map) session manager. + /// + /// The map whose session manager will be retrieved. + /// The local (map) session manager. + /// As long as a multiplayer session is active, all maps should contain a session manager. This method should never return in such cases, unless something is very broken. + /// Thrown when is null. + public static ISessionManager GetLocalSessionManager(Map map) => Sync.GetLocalSessionManager(map); + /// + /// Sets the currently active session with transferables. Used for syncing changes in transferables by letting Multiplayer know which session should be synced. + /// It cannot be set to anything but while a session is currently set as active. + /// The session needs to be set before (potentially) operating on the trasnferables, and unset afterwards. + /// The session should be set/unset in a block. + /// + /// The session to set as the active one, or to unset. + /// + /// + /// try + /// { + /// MP.SetCurrentSessionWithTransferables(session); + /// OperateOnTransferables(); + /// } + /// finally + /// { + /// MP.SetCurrentSessionWithTransferables(null); + /// } + /// + /// + public static void SetCurrentSessionWithTransferables(ISessionWithTransferables session) => Sync.SetCurrentSessionWithTransferables(session); } } diff --git a/Source/API/MPTypes.cs b/Source/API/MPTypes.cs index da82619..d7229c2 100644 --- a/Source/API/MPTypes.cs +++ b/Source/API/MPTypes.cs @@ -1,5 +1,8 @@ using System; using System.Reflection; +using RimWorld; +using RimWorld.Planet; +using Verse; namespace Multiplayer.API { @@ -171,4 +174,130 @@ public enum ParentMethodType /// This targets the MoveNext method of the enumerator result Enumerator, } + + /// + /// Used by Multiplayer's session manager to allow for creation of blocking dialogs, while (in case of async time) only pausing specific maps. + /// Sessions will be reset/reloaded during reloading - to prevent it, implement or . + /// You should avoid implementing this interface directly, instead opting into inheriting for greater compatibility. + /// + public abstract class Session + { + // Use internal to prevent mods from easily modifying it? + protected int sessionId; + // Should it be virtual? + /// + /// Used for syncing session across players by assigning them IDs, similarly to how every receives an ID. + /// Automatically applied by the session manager + /// If inheriting you don't have to worry about this property. + /// + public int SessionId + { + get => sessionId; + set => sessionId = value; + } + + /// + /// Used by the session manager while joining the game - if it returns it'll get removed. + /// + public virtual bool IsSessionValid => true; + + /// + /// Mandatory constructor for any subclass of . + /// + /// The map this session belongs to. It will be provided by session manager when syncing. + protected Session(Map map) { } + + /// + /// Called once the sessions has been added to the list of active sessions. Can be used for initialization. + /// + /// In case of , this will only be called if successfully added. + public virtual void PostAddSession() + { + } + + /// + /// Called once the sessions has been removed to the list of active sessions. Can be used for cleanup. + /// + public virtual void PostRemoveSession() + { + } + + /// + /// A convenience method to switch to a specific map or world. Intended to be used from when opening menu. + /// + /// Map to switch to or to switch to world view. + protected static void SwitchToMapOrWorld(Map map) + { + if (map == null) + { + Find.World.renderer.wantedMode = WorldRenderMode.Planet; + } + else + { + if (WorldRendererUtility.WorldRenderedNow) CameraJumper.TryHideWorld(); + Current.Game.CurrentMap = map; + } + } + + /// + /// The map this session is used by or in case of global sessions. + /// + public abstract Map Map { get; } + + /// + /// Called when checking ticking and if any session returns - it'll force pause the map/game. + /// In case of local (map) sessions, it'll only be called by the current map. In case of global (world) sessions, it'll be called by the world and each map. + /// + /// Current map (when checked from local session manager) or (when checked from local session manager). + /// If there are multiple sessions active, this method is not guaranteed to run if a session before this one returned . + /// if the session should pause the map/game, otherwise. + public abstract bool IsCurrentlyPausing(Map map); + + /// + /// Called when a session is active, and if any session returns a non-null value, a button will be displayed which will display all options. + /// + /// Currently processed colonist bar entry. Will be called once per . + /// Menu option that will be displayed when the session is active. Can be . + public abstract FloatMenuOption GetBlockingWindowOptions(ColonistBar.Entry entry); + } + + /// + /// Sessions inheriting from this class contain persistent data. + /// When inheriting from this class, remember to call base.ExposeData() to let it handle + /// Persistent data: + /// + /// Serialized into XML using RimWorld's Scribe system + /// Save-bound: survives a server restart + /// + /// + public abstract class ExposableSession : Session, IExposable + { + /// + protected ExposableSession(Map map) : base(map) { } + + public virtual void ExposeData() + { + Scribe_Values.Look(ref sessionId, "sessionId"); + } + } + + /// + /// Sessions implementing this interface consist of semi-persistent data. + /// Semi-persistent data: + /// + /// Serialized into binary using the Sync system + /// Session-bound: survives a reload, lost when the server is closed + /// + /// + public abstract class SemiPersistentSession : Session + { + /// + protected SemiPersistentSession(Map map) : base(map) { } + + /// + /// Writes/reads the data used by this session. + /// + /// Sync worker used for writing/reading the data. + public abstract void Sync(SyncWorker sync); + } } \ No newline at end of file From 64985a1446e9963963aced08b540f6639ac7fd58 Mon Sep 17 00:00:00 2001 From: Zetrith Date: Wed, 27 Dec 2023 01:35:23 +0100 Subject: [PATCH 6/8] Refactor: move all types into own files Add RealPlayerFaction getter Remove IsArbiter from IPlayerInfo Improve some docs --- Source/API/Dummy.cs | 319 +++---- Source/API/IAPI.cs | 64 ++ Source/API/IPlayerInfo.cs | 38 + Source/API/Interfaces.cs | 836 ------------------ Source/API/MP.cs | 831 ++++++++--------- Source/API/MPTypes.cs | 303 ------- Source/API/ParentMethodType.cs | 18 + Source/API/Serializer.cs | 27 - Source/API/Sessions/ExposableSession.cs | 23 + Source/API/Sessions/ISessionManager.cs | 86 ++ .../ISessionWithCreationRestrictions.cs | 16 + .../API/Sessions/ISessionWithTransferables.cs | 25 + Source/API/Sessions/ITickingSession.cs | 13 + Source/API/Sessions/PauseLockAttribute.cs | 11 + Source/API/Sessions/PauseLockDelegate.cs | 10 + Source/API/Sessions/SemiPersistentSession.cs | 23 + Source/API/Sessions/Session.cs | 91 ++ Source/API/Sync/ISyncCall.cs | 16 + Source/API/Sync/ISyncDelegate.cs | 125 +++ Source/API/Sync/ISyncField.cs | 93 ++ Source/API/Sync/ISyncMethod.cs | 107 +++ Source/API/Sync/ISyncSimple.cs | 8 + Source/API/Sync/ISynchronizable.cs | 39 + Source/API/Sync/Serializer.cs | 26 + Source/API/Sync/SyncContext.cs | 23 + .../API/Sync/SyncDialogNodeTreeAttribute.cs | 23 + Source/API/Sync/SyncFieldAttribute.cs | 50 ++ Source/API/Sync/SyncMethodAttribute.cs | 44 + Source/API/Sync/SyncType.cs | 28 + Source/API/Sync/SyncWorker.cs | 151 ++++ Source/API/Sync/SyncWorkerAttribute.cs | 48 + Source/API/Sync/SyncWorkerDelegate.cs | 8 + Source/API/Sync/ThingFilterContext.cs | 16 + 33 files changed, 1801 insertions(+), 1738 deletions(-) create mode 100644 Source/API/IAPI.cs create mode 100644 Source/API/IPlayerInfo.cs delete mode 100644 Source/API/Interfaces.cs delete mode 100644 Source/API/MPTypes.cs create mode 100644 Source/API/ParentMethodType.cs delete mode 100644 Source/API/Serializer.cs create mode 100644 Source/API/Sessions/ExposableSession.cs create mode 100644 Source/API/Sessions/ISessionManager.cs create mode 100644 Source/API/Sessions/ISessionWithCreationRestrictions.cs create mode 100644 Source/API/Sessions/ISessionWithTransferables.cs create mode 100644 Source/API/Sessions/ITickingSession.cs create mode 100644 Source/API/Sessions/PauseLockAttribute.cs create mode 100644 Source/API/Sessions/PauseLockDelegate.cs create mode 100644 Source/API/Sessions/SemiPersistentSession.cs create mode 100644 Source/API/Sessions/Session.cs create mode 100644 Source/API/Sync/ISyncCall.cs create mode 100644 Source/API/Sync/ISyncDelegate.cs create mode 100644 Source/API/Sync/ISyncField.cs create mode 100644 Source/API/Sync/ISyncMethod.cs create mode 100644 Source/API/Sync/ISyncSimple.cs create mode 100644 Source/API/Sync/ISynchronizable.cs create mode 100644 Source/API/Sync/Serializer.cs create mode 100644 Source/API/Sync/SyncContext.cs create mode 100644 Source/API/Sync/SyncDialogNodeTreeAttribute.cs create mode 100644 Source/API/Sync/SyncFieldAttribute.cs create mode 100644 Source/API/Sync/SyncMethodAttribute.cs create mode 100644 Source/API/Sync/SyncType.cs create mode 100644 Source/API/Sync/SyncWorker.cs create mode 100644 Source/API/Sync/SyncWorkerAttribute.cs create mode 100644 Source/API/Sync/SyncWorkerDelegate.cs create mode 100644 Source/API/Sync/ThingFilterContext.cs diff --git a/Source/API/Dummy.cs b/Source/API/Dummy.cs index c8c1ab6..2a7c3ad 100644 --- a/Source/API/Dummy.cs +++ b/Source/API/Dummy.cs @@ -1,181 +1,182 @@ using System; using System.Collections.Generic; using System.Reflection; +using RimWorld; using Verse; -namespace Multiplayer.API +namespace Multiplayer.API; + +/// +/// An exception that is thrown if you try to use the API without avaiable host. +/// +public class UninitializedAPI : Exception +{ +} + +class Dummy : IAPI { - /// - /// An exception that is thrown if you try to use the API without avaiable host. - /// - public class UninitializedAPI : Exception + public bool IsHosting => false; + + public bool IsInMultiplayer => false; + + public string PlayerName => null; + + public bool IsExecutingSyncCommand => false; + + public bool IsExecutingSyncCommandIssuedBySelf => false; + + public bool CanUseDevMode => false; + + public bool InInterface => false; + public Faction RealPlayerFaction => null; + + public void SetThingFilterContext(ThingFilterContext context) { + throw new UninitializedAPI(); } - class Dummy : IAPI + public void WatchBegin() { - public bool IsHosting => false; + throw new UninitializedAPI(); + } - public bool IsInMultiplayer => false; + public void Watch(Type targetType, string fieldName, object target = null, object index = null) + { + throw new UninitializedAPI(); + } - public string PlayerName => null; + public void Watch(object target, string fieldName, object index = null) + { + throw new UninitializedAPI(); + } - public bool IsExecutingSyncCommand => false; + public void Watch(string memberPath, object target = null, object index = null) + { + throw new UninitializedAPI(); + } - public bool IsExecutingSyncCommandIssuedBySelf => false; + public void WatchEnd() + { + throw new UninitializedAPI(); + } - public bool CanUseDevMode => false; + public void RegisterAll(Assembly assembly) + { + throw new UninitializedAPI(); + } - public bool InInterface => false; + public ISyncField RegisterSyncField(Type targetType, string memberPath) + { + throw new UninitializedAPI(); + } - public void SetThingFilterContext(ThingFilterContext context) - { - throw new UninitializedAPI(); - } + public ISyncField RegisterSyncField(FieldInfo field) + { + throw new UninitializedAPI(); + } - public void WatchBegin() - { - throw new UninitializedAPI(); - } + public ISyncMethod RegisterSyncMethod(Type type, string methodOrPropertyName, SyncType[] argTypes = null) + { + throw new UninitializedAPI(); + } - public void Watch(Type targetType, string fieldName, object target = null, object index = null) - { - throw new UninitializedAPI(); - } + public ISyncMethod RegisterSyncMethod(MethodInfo method, SyncType[] argTypes) + { + throw new UninitializedAPI(); + } - public void Watch(object target, string fieldName, object index = null) - { - throw new UninitializedAPI(); - } + public ISyncMethod RegisterSyncMethodLambda(Type parentType, string parentMethod, int lambdaOrdinal, Type[] parentArgs = null, ParentMethodType parentParentMethodType = ParentMethodType.Normal) + { + throw new UninitializedAPI(); + } - public void Watch(string memberPath, object target = null, object index = null) - { - throw new UninitializedAPI(); - } - - public void WatchEnd() - { - throw new UninitializedAPI(); - } - - public void RegisterAll(Assembly assembly) - { - throw new UninitializedAPI(); - } - - public ISyncField RegisterSyncField(Type targetType, string memberPath) - { - throw new UninitializedAPI(); - } - - public ISyncField RegisterSyncField(FieldInfo field) - { - throw new UninitializedAPI(); - } - - public ISyncMethod RegisterSyncMethod(Type type, string methodOrPropertyName, SyncType[] argTypes = null) - { - throw new UninitializedAPI(); - } - - public ISyncMethod RegisterSyncMethod(MethodInfo method, SyncType[] argTypes) - { - throw new UninitializedAPI(); - } - - public ISyncMethod RegisterSyncMethodLambda(Type parentType, string parentMethod, int lambdaOrdinal, Type[] parentArgs = null, ParentMethodType parentParentMethodType = ParentMethodType.Normal) - { - throw new UninitializedAPI(); - } - - public ISyncMethod RegisterSyncMethodLambdaInGetter(Type parentType, string parentMethod, int lambdaOrdinal) - { - throw new UninitializedAPI(); - } - - public ISyncDelegate RegisterSyncDelegate(Type inType, string nestedType, string methodName, string[] fields, Type[] args = null) - { - throw new UninitializedAPI(); - } - - public ISyncDelegate RegisterSyncDelegate(Type type, string nestedType, string method) - { - throw new UninitializedAPI(); - } - - public ISyncDelegate RegisterSyncDelegateLambda(Type parentType, string parentMethod, int lambdaOrdinal, Type[] parentArgs = null, ParentMethodType parentParentMethodType = ParentMethodType.Normal) - { - throw new UninitializedAPI(); - } - - public ISyncDelegate RegisterSyncDelegateLambdaInGetter(Type parentType, string parentMethod, int lambdaOrdinal) - { - throw new UninitializedAPI(); - } - - public ISyncDelegate RegisterSyncDelegateLocalFunc(Type parentType, string parentMethod, string localFuncName, Type[] parentArgs = null) - { - throw new UninitializedAPI(); - } - - public void RegisterSyncWorker(SyncWorkerDelegate syncWorkerDelegate, Type targetType = null, bool isImplicit = false, bool shouldConstruct = false) - { - throw new UninitializedAPI(); - } - - public void RegisterDialogNodeTree(Type type, string methodOrPropertyName, SyncType[] argTypes = null) - { - throw new UninitializedAPI(); - } - - public void RegisterDialogNodeTree(MethodInfo method) - { - throw new UninitializedAPI(); - } - - public void RegisterPauseLock(PauseLockDelegate pauseLock) - { - throw new UninitializedAPI(); - } - - public void RegisterDefaultLetterChoice(MethodInfo method, Type letterType = null) - { - throw new UninitializedAPI(); - } - - public Thing GetThingById(int id) - { - throw new UninitializedAPI(); - } - - public bool TryGetThingById(int id, out Thing value) - { - throw new UninitializedAPI(); - } - - public IReadOnlyList GetPlayers() - { - throw new UninitializedAPI(); - } - - public IPlayerInfo GetPlayerById(int id) - { - throw new UninitializedAPI(); - } - - public ISessionManager GetGlobalSessionManager() - { - throw new UninitializedAPI(); - } - - public ISessionManager GetLocalSessionManager(Map map) - { - throw new UninitializedAPI(); - } - - public void SetCurrentSessionWithTransferables(ISessionWithTransferables session) - { - throw new UninitializedAPI(); - } + public ISyncMethod RegisterSyncMethodLambdaInGetter(Type parentType, string parentMethod, int lambdaOrdinal) + { + throw new UninitializedAPI(); } -} + + public ISyncDelegate RegisterSyncDelegate(Type inType, string nestedType, string methodName, string[] fields, Type[] args = null) + { + throw new UninitializedAPI(); + } + + public ISyncDelegate RegisterSyncDelegate(Type type, string nestedType, string method) + { + throw new UninitializedAPI(); + } + + public ISyncDelegate RegisterSyncDelegateLambda(Type parentType, string parentMethod, int lambdaOrdinal, Type[] parentArgs = null, ParentMethodType parentParentMethodType = ParentMethodType.Normal) + { + throw new UninitializedAPI(); + } + + public ISyncDelegate RegisterSyncDelegateLambdaInGetter(Type parentType, string parentMethod, int lambdaOrdinal) + { + throw new UninitializedAPI(); + } + + public ISyncDelegate RegisterSyncDelegateLocalFunc(Type parentType, string parentMethod, string localFuncName, Type[] parentArgs = null) + { + throw new UninitializedAPI(); + } + + public void RegisterSyncWorker(SyncWorkerDelegate syncWorkerDelegate, Type targetType = null, bool isImplicit = false, bool shouldConstruct = false) + { + throw new UninitializedAPI(); + } + + public void RegisterDialogNodeTree(Type type, string methodOrPropertyName, SyncType[] argTypes = null) + { + throw new UninitializedAPI(); + } + + public void RegisterDialogNodeTree(MethodInfo method) + { + throw new UninitializedAPI(); + } + + public void RegisterPauseLock(PauseLockDelegate pauseLock) + { + throw new UninitializedAPI(); + } + + public void RegisterDefaultLetterChoice(MethodInfo method, Type letterType = null) + { + throw new UninitializedAPI(); + } + + public Thing GetThingById(int id) + { + throw new UninitializedAPI(); + } + + public bool TryGetThingById(int id, out Thing value) + { + throw new UninitializedAPI(); + } + + public IReadOnlyList GetPlayers() + { + throw new UninitializedAPI(); + } + + public IPlayerInfo GetPlayerById(int id) + { + throw new UninitializedAPI(); + } + + public ISessionManager GetGlobalSessionManager() + { + throw new UninitializedAPI(); + } + + public ISessionManager GetLocalSessionManager(Map map) + { + throw new UninitializedAPI(); + } + + public void SetCurrentSessionWithTransferables(ISessionWithTransferables session) + { + throw new UninitializedAPI(); + } +} \ No newline at end of file diff --git a/Source/API/IAPI.cs b/Source/API/IAPI.cs new file mode 100644 index 0000000..78b6293 --- /dev/null +++ b/Source/API/IAPI.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using RimWorld; +using Verse; + +namespace Multiplayer.API; + +public interface IAPI +{ + bool IsHosting { get; } + bool IsInMultiplayer { get; } + string PlayerName { get; } + bool IsExecutingSyncCommand { get; } + bool IsExecutingSyncCommandIssuedBySelf { get; } + bool CanUseDevMode { get; } + bool InInterface { get; } + Faction RealPlayerFaction { get; } + + void SetThingFilterContext(ThingFilterContext context); + + void WatchBegin(); + void Watch(Type targetType, string fieldName, object target = null, object index = null); + void Watch(object target, string fieldName, object index = null); + void Watch(string memberPath, object target = null, object index = null); + void WatchEnd(); + + void RegisterAll(Assembly assembly); + + ISyncField RegisterSyncField(Type targetType, string memberPath); + ISyncField RegisterSyncField(FieldInfo field); + + ISyncMethod RegisterSyncMethod(Type type, string methodOrPropertyName, SyncType[] argTypes = null); + ISyncMethod RegisterSyncMethod(MethodInfo method, SyncType[] argTypes); + ISyncMethod RegisterSyncMethodLambda(Type parentType, string parentMethod, int lambdaOrdinal, Type[] parentArgs = null, ParentMethodType parentParentMethodType = ParentMethodType.Normal); + ISyncMethod RegisterSyncMethodLambdaInGetter(Type parentType, string parentMethod, int lambdaOrdinal); + + ISyncDelegate RegisterSyncDelegate(Type type, string nestedType, string method); + ISyncDelegate RegisterSyncDelegate(Type inType, string nestedType, string methodName, string[] fields, Type[] args = null); + ISyncDelegate RegisterSyncDelegateLambda(Type parentType, string parentMethod, int lambdaOrdinal, Type[] parentArgs = null, ParentMethodType parentParentMethodType = ParentMethodType.Normal); + ISyncDelegate RegisterSyncDelegateLambdaInGetter(Type parentType, string parentMethod, int lambdaOrdinal); + ISyncDelegate RegisterSyncDelegateLocalFunc(Type parentType, string parentMethod, string localFuncName, Type[] parentArgs = null); + + void RegisterSyncWorker(SyncWorkerDelegate syncWorkerDelegate, Type targetType = null, bool isImplicit = false, bool shouldConstruct = false); + + void RegisterDialogNodeTree(Type type, string methodOrPropertyName, SyncType[] argTypes = null); + + void RegisterDialogNodeTree(MethodInfo method); + + [Obsolete($"Use {nameof(Session)} instead.")] + void RegisterPauseLock(PauseLockDelegate pauseLock); + + void RegisterDefaultLetterChoice(MethodInfo method, Type letterType = null); + + Thing GetThingById(int id); + bool TryGetThingById(int id, out Thing value); + + IReadOnlyList GetPlayers(); + IPlayerInfo GetPlayerById(int id); + + ISessionManager GetGlobalSessionManager(); + ISessionManager GetLocalSessionManager(Map map); + void SetCurrentSessionWithTransferables(ISessionWithTransferables session); +} \ No newline at end of file diff --git a/Source/API/IPlayerInfo.cs b/Source/API/IPlayerInfo.cs new file mode 100644 index 0000000..a1e9d84 --- /dev/null +++ b/Source/API/IPlayerInfo.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using Verse; + +namespace Multiplayer.API; + +/// +/// An interface for the class holding player data +/// +public interface IPlayerInfo +{ + /// + /// ID of the current player + /// + int Id { get; } + /// + /// Username of the current player + /// + string Username { get; } + + /// + /// of the map the player is on + /// + int CurrentMapIndex { get; } + /// + /// The map the current player is on + /// + Map CurrentMap { get; } + + /// + /// List of all the things the player has selected, as numeric IDs + /// + /// Generally use , unless you're able to access the IDs directly. + IReadOnlyList SelectedThingsByIds { get; } + /// + /// List of all the things the player has selected + /// + IReadOnlyList SelectedThings { get; } +} \ No newline at end of file diff --git a/Source/API/Interfaces.cs b/Source/API/Interfaces.cs deleted file mode 100644 index d636d0e..0000000 --- a/Source/API/Interfaces.cs +++ /dev/null @@ -1,836 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Reflection; -using RimWorld; -using Verse; - -namespace Multiplayer.API -{ - /// - /// SyncField interface. - /// - /// - /// Creates and registers a SyncField that points to myField in object of type MyType and enables its change buffer. - /// - /// MPApi.SyncField(typeof(MyType), "myField").SetBufferChanges(); - /// - /// Creates and registers a SyncField that points to myField which resides in MyStaticClass. - /// - /// MPApi.SyncField(null, "MyAssemblyNamespace.MyStaticClass.myField"); - /// - /// Creates and registers a SyncField that points to myField that resides in an object stored by myEnumberable defined in an object of type MyType. - /// To watch this one you have to supply an index in . - /// - /// MPApi.SyncField(typeof(MyType), "myEnumerable/[]/myField"); - /// - /// - public interface ISyncField - { - /// - /// Instructs SyncField to cancel synchronization if the value of the member it's pointing at is null. - /// - /// self - ISyncField CancelIfValueNull(); - - /// - /// Instructs SyncField to sync in game loop. - /// - /// self - ISyncField InGameLoop(); - - /// - /// Adds an Action that runs after a field is synchronized. - /// - /// An action ran after a field is synchronized. Called with target and value. - /// self - ISyncField PostApply(Action action); - - /// - /// Adds an Action that runs before a field is synchronized. - /// - /// An action ran before a field is synchronized. Called with target and value. - /// self - ISyncField PreApply(Action action); - - /// - /// Instructs SyncField to use a buffer instead of syncing instantly (when is called). - /// - /// self - ISyncField SetBufferChanges(); - - /// - /// Instructs SyncField to synchronize only in debug mode. - /// - /// self - ISyncField SetDebugOnly(); - - /// - /// Instructs SyncField to synchronize only if it's invoked by the host. - /// - /// self - ISyncField SetHostOnly(); - - /// - /// - /// - /// self - ISyncField SetVersion(int version); - - /// - /// - /// - /// An object of type set in the . Set to null if you're watching a static field. - /// Index in the field path set in . - /// self - void Watch(object target = null, object index = null); - - /// - /// Manually syncs a field. - /// - /// An object of type set in the . Set to null if you're watching a static field. - /// Value to apply to the synced field. - /// Index in the field path set in - /// if the change should be canceled. - bool DoSync(object target, object value, object index = null); - - string ToString(); - } - - /// - /// ISyncCall interface. - /// - /// Used internally - public interface ISyncCall - { - /// - /// Manually calls the synced method. - /// - /// Object currently bound to that method. Null if the method is static. - /// Parameters to call the method with. - /// if the original call should be canceled. - bool DoSync(object target, params object[] args); - } - - /// - /// SyncMethod interface. - /// - /// See , and to see how to use it. - public interface ISyncMethod : ISyncCall - { - /// - /// Instructs SyncMethod to cancel synchronization if any arg is null. - /// - /// self - ISyncMethod CancelIfAnyArgNull(); - - /// - /// Instructs SyncMethod to cancel synchronization if no map objects were selected during call replication. - /// - /// self - ISyncMethod CancelIfNoSelectedMapObjects(); - - /// - /// Instructs SyncMethod to cancel synchronization if no world objects were selected during call replication. - /// - /// self - ISyncMethod CancelIfNoSelectedWorldObjects(); - - /// - /// Use parameter's type's IExposable interface to transfer its data to other clients. - /// - /// IExposable is the interface used for saving data to the save which means it utilizes IExposable.ExposeData() method. - /// Index at which parameter is to be marked to expose - /// self - ISyncMethod ExposeParameter(int index); - - /// - /// Currently unused in the Multiplayer mod. - /// - /// Milliseconds between resends - /// self - ISyncMethod MinTime(int time); - - /// - /// Instructs method to send context along with the call. - /// - /// Context is restored after method is called. - /// One or more context flags - /// self - ISyncMethod SetContext(SyncContext context); - - /// - /// Instructs SyncMethod to synchronize only in debug mode. - /// - /// self - ISyncMethod SetDebugOnly(); - - /// - /// Instructs SyncMethod to synchronize only if it's invoked by the host. - /// - /// self - ISyncMethod SetHostOnly(); - - /// - /// Adds an Action that runs before a call is replicated on client. - /// - /// An action ran before a call is replicated on client. Called with target and value. - /// self - ISyncMethod SetPreInvoke(Action action); - - /// - /// Adds an Action that runs after a call is replicated on client. - /// - /// An action ran after a call is replicated on client. Called with target and value. - /// self - ISyncMethod SetPostInvoke(Action action); - - /// - /// - /// - /// Handler version - /// self - ISyncMethod SetVersion(int version); - - /// - /// Transforms a parameter of a method, result of which will be synced instead of the parameter itself - /// - /// Index at which parameter is going to be transformed - /// A serializer which will transform the argument before and after syncing - /// Check to ensure is the same type as the argument will be dropped. More error-prone (and only detectable at runtime), but allows transforming arguments even if the current assembly cannot reference the specific type. - /// The type which will be transformed before, and type that will be transformed back into after syncing - /// The type which will be synced to other players instead of - /// self - ISyncMethod TransformArgument(int index, Serializer serializer, bool skipTypeCheck = false); - - /// - /// Transforms an object instance within which the synced method is declared, result of which will be synced instead of the instance itself - /// - /// A serializer which will transform the target instance before and after syncing - /// Check to ensure is the same type as the target instance will be dropped fully. More error-prone (and only detectable at runtime), but allows transforming target instance even if the current assembly cannot reference the specific type. - /// The type which will be transformed before, and type that will be transformed back into after syncing - /// The type which will be synced to other players instead of - /// self - ISyncMethod TransformTarget(Serializer serializer, bool skipTypeCheck = false); - - string ToString(); - } - - // Todo: Document - /// - /// Sync delegate. - /// - /// See and to see how to use it. - public interface ISyncDelegate : ISyncCall - { - /// - /// Instructs ISyncDelegate to cancel synchronization except for - /// - /// self - /// field names to be excluded - ISyncDelegate CancelIfAnyFieldNull(params string[] blacklist); - - /// - /// Instructs ISyncDelegate to cancel synchronization except for - /// - /// self - /// Whitelist. - ISyncDelegate CancelIfFieldsNull(params string[] whitelist); - - /// - /// Cancels if no selected objects. - /// - /// self - [Obsolete($"Use {nameof(CancelIfNoSelectedMapObjects)} instead")] - ISyncDelegate CancelIfNoSelectedObjects(); - - /// - /// Instructs SyncDelegate to cancel synchronization if no map objects were selected during call replication. - /// - /// self - ISyncDelegate CancelIfNoSelectedMapObjects(); - - /// - /// Instructs SyncDelegate to cancel synchronization if no world objects were selected during call replication. - /// - /// self - ISyncDelegate CancelIfNoSelectedWorldObjects(); - - /// - /// Use parameter's type's IExposable interface to transfer its data to other clients. - /// - /// IExposable is the interface used for saving data to the save which means it utilizes IExposable.ExposeData() method. - /// self - /// Fields to sync by using IExposable. - ISyncDelegate ExposeFields(params string[] fields); - - /// - /// Removes the nulls from lists. - /// - /// self - /// List fields. - ISyncDelegate RemoveNullsFromLists(params string[] listFields); - - /// - /// Sets the context. - /// - /// self - /// Context. - ISyncDelegate SetContext(SyncContext context); - - /// - /// Sets the debug only. - /// - /// self - ISyncDelegate SetDebugOnly(); - - /// - /// Instructs SyncDelegate to synchronize only if it's invoked by the host. - /// - /// self - ISyncDelegate SetHostOnly(); - - /// - /// Adds an Action that runs before a call is replicated on client. - /// - /// An action ran before a call is replicated on client. Called with target and value. - /// self - ISyncDelegate SetPreInvoke(Action action); - - /// - /// Adds an Action that runs after a call is replicated on client. - /// - /// An action ran after a call is replicated on client. Called with target and value. - /// self - ISyncDelegate SetPostInvoke(Action action); - - /// - /// Transforms a parameter of a method, result of which will be synced instead of the parameter itself - /// - /// Index at which parameter is going to be transformed - /// A serializer which will transform the argument before and after syncing - /// Check to ensure is the same type as the argument will be dropped. More error-prone (and only detectable at runtime), but allows transforming arguments even if the current assembly cannot reference the specific type. - /// The type which will be transformed before, and type that will be transformed back into after syncing - /// The type which will be synced to other players instead of - /// self - ISyncDelegate TransformArgument(int index, Serializer serializer, bool skipTypeCheck = false); - - /// - /// Transforms an object instance within which the synced method is declared, result of which will be synced instead of the instance itself - /// - /// A serializer which will transform the target instance before and after syncing - /// Check to ensure is the same type as the target instance will be dropped. More error-prone (and only detectable at runtime), but allows transforming target instance even if the current assembly cannot reference the specific type. - /// The type which will be transformed before, and type that will be transformed back into after syncing - /// The type which will be synced to other players instead of - /// self - ISyncDelegate TransformTarget(Serializer serializer, bool skipTypeCheck = false); - - /// - /// Transforms a field inside of the delegate, result of which will be synced instead of the field itself - /// - /// Name of a field which will be transformed before and after syncing. Supports fields inside of fields referencing other delegates, for example: `firstDelegate/anotherDelegate/targetField` - /// A serializer which will transform the field before and after syncing - /// Check to ensure is the same type as the field will be dropped. More error-prone (and only detectable at runtime), but allows transforming fields even if the current assembly cannot reference the specific type. - /// The type which will be transformed before, and type that will be transformed back into after syncing - /// The type which will be synced to other players instead of - /// - ISyncDelegate TransformField(string field, Serializer serializer, bool skipTypeCheck = false); - - string ToString(); - } - - - /// - /// An attribute that marks a method as a SyncWorker for a type specified in its second parameter. - /// - /// - /// Method with this attribute has to be static. - /// - /// - /// An implementation that manually constructs an object. - /// - /// [SyncWorkerAttribute] - /// public static void MySyncWorker(SyncWorker sync, ref MyClass inst) - /// { - /// if(!sync.isWriting) - /// inst = new MyClass("hello"); - /// - /// sync.bind(ref inst.myField); - /// } - /// - /// An implementation that instead of creating a new object, references its existing one which resides in MyThingComp that inherits ThingComp class. - /// Subclasses of ThingComp are sent as a reference by the multiplayer mod itself. - /// - /// [SyncWorkerAttribute] - /// public static void MySyncWorker(SyncWorker sync, ref MyClass inst) - /// { - /// if(!sync.isWriting) - /// MyThingComp parent = null; - /// sync.Bind(ref parent); // Receive its parent - /// inst = new MyClass(parent); - /// else - /// sync.Bind(ref inst.parent); // Send its parent - /// - /// sync.bind(ref inst.myField); - /// } - /// - /// - [AttributeUsage(AttributeTargets.Method)] - public class SyncWorkerAttribute : Attribute - { - /// Decides if the type specified in the second parameter should also be used as a syncer for all of its subclasses. - public bool isImplicit = false; - - /// Decides if the method should get an already constructed object in case of reading data. - public bool shouldConstruct = false; - } - - /// - /// SyncWorker signature for adding new Types. - /// - /// Target Type - /// for usage examples. - public delegate void SyncWorkerDelegate(SyncWorker sync, ref T obj); - - /// - /// An abstract class that can be both a reader and a writer depending on implementation. - /// - /// See and for usage examples. - public abstract class SyncWorker - { - /// if is currently writing. - public readonly bool isWriting; - - protected SyncWorker(bool isWriting) - { - this.isWriting = isWriting; - } - - public void Write(T obj, SyncType type) - { - if (isWriting) - { - Bind(ref obj, type); - } - } - - /// - /// Write the specified obj, only active during writing. - /// - /// Object to write. - /// Type to write. - public void Write(T obj) { - if (isWriting) { - Bind(ref obj); - } - } - - public T Read(SyncType type) - { - T obj = default(T); - - if (isWriting) - { - return obj; - } - - Bind(ref obj, type); - - return obj; - } - - /// - /// Read the specified Type from the memory stream, only active during reading. - /// - /// The requested Type object. Null if writing. - /// The Type to read. - public T Read() { - T obj = default(T); - - if (isWriting) { - return obj; - } - - Bind(ref obj); - - return obj; - } - - public abstract void Bind(ref T obj, SyncType type); - - /// Reads or writes a referenced by . - /// Base type that derives from. - /// type to bind - public abstract void BindType(ref Type type); - - /// Reads or writes an object referenced by . - /// object to bind - public abstract void Bind(ref byte obj); - - /// Reads or writes an object referenced by . - /// object to bind - public abstract void Bind(ref sbyte obj); - - /// Reads or writes an object referenced by . - /// object to bind - public abstract void Bind(ref short obj); - - /// Reads or writes an object referenced by . - /// object to bind - public abstract void Bind(ref ushort obj); - - /// Reads or writes an object referenced by . - /// object to bind - public abstract void Bind(ref int obj); - - /// Reads or writes an object referenced by . - /// object to bind - public abstract void Bind(ref uint obj); - - /// Reads or writes an object referenced by . - /// object to bind - public abstract void Bind(ref long obj); - - /// Reads or writes an object referenced by . - /// object to bind - public abstract void Bind(ref ulong obj); - - /// Reads or writes an object referenced by . - /// object to bind - public abstract void Bind(ref float obj); - - /// Reads or writes an object referenced by . - /// object to bind - public abstract void Bind(ref double obj); - - /// Reads or writes an object referenced by . - /// object to bind - public abstract void Bind(ref string obj); - - /// Reads or writes an object referenced by . - /// object to bind - public abstract void Bind(ref bool obj); - - /// - /// Reads or writes an object referenced by - /// - /// Can read/write types using user defined syncers, s and readers/writers implemented by the multiplayer mod. - /// type of the object to bind - /// object to bind - public abstract void Bind(ref T obj); - - /// - /// Uses reflection to bind a field or property - /// - /// - /// object where the field or property can be found - /// if null, will point at field from the global namespace - /// - /// path to the field or property - public abstract void Bind(object obj, string name); - - /// - /// Reads or writes an object inheriting interface. - /// - /// Does not create a new object. - /// object to bind - public void Bind(ref ISynchronizable obj) - { - obj.Sync(this); - } - } - - /// - /// An interface that allows syncing objects that inherit it. - /// - public interface ISynchronizable - { - /// - /// An entry point that is used when object is to be read/written. - /// - /// - /// Requires a default constructor that takes no parameters. - /// Check to see how to make a syncer that allows for a manual object construction. - /// - /// A SyncWorker that will read/write data bound with Bind methods. - /// - /// A simple implementation that binds object's fields x, y, z for reading/writing. - /// - /// public void Sync(SyncWorker sync) - /// { - /// sync.Bind(ref this.x); - /// sync.Bind(ref this.y); - /// sync.Bind(ref this.z); - /// } - /// - /// - /// An implementation that sends field a, but saves it back into field b when it's received. - /// - /// public void Sync(SyncWorker sync) - /// { - /// if(sync.isWriting) - /// sync.Bind(ref this.a); - /// else - /// sync.Bind(ref this.b); - /// } - /// - /// - void Sync(SyncWorker sync); - } - - /// - /// An attribute that marks a method for pause lock checking It needs a return type and a single parameter. - /// - [AttributeUsage(AttributeTargets.Method)] - public class PauseLockAttribute : Attribute - { } - - /// - /// Signature for adding new local pause locking methods - /// - /// Current map to check if it should be paused - /// if time should be paused on the specific map - public delegate bool PauseLockDelegate(Map map); - - /// - /// Objects implementing this marker interface sync their exact type and all declared fields. - /// This is useful when syncing type hierarchies by value. - /// The synced object is created uninitialized using reflection (no constructor is called). - /// - public interface ISyncSimple { } - - /// - /// A ThingFilter context provides information for syncing ThingFilter interactions. - /// Inheriting objects should store the ThingFilter's owner in a record property. - /// The type exists because vanilla ThingFilters don't store references to their owners. - /// - public abstract record ThingFilterContext : ISyncSimple - { - public abstract ThingFilter Filter { get; } - public abstract ThingFilter ParentFilter { get; } - public virtual IEnumerable HiddenFilters => null; - } - - public interface IAPI - { - bool IsHosting { get; } - bool IsInMultiplayer { get; } - string PlayerName { get; } - bool IsExecutingSyncCommand { get; } - bool IsExecutingSyncCommandIssuedBySelf { get; } - bool CanUseDevMode { get; } - bool InInterface { get; } - - void SetThingFilterContext(ThingFilterContext context); - - void WatchBegin(); - void Watch(Type targetType, string fieldName, object target = null, object index = null); - void Watch(object target, string fieldName, object index = null); - void Watch(string memberPath, object target = null, object index = null); - void WatchEnd(); - - void RegisterAll(Assembly assembly); - - ISyncField RegisterSyncField(Type targetType, string memberPath); - ISyncField RegisterSyncField(FieldInfo field); - - ISyncMethod RegisterSyncMethod(Type type, string methodOrPropertyName, SyncType[] argTypes = null); - ISyncMethod RegisterSyncMethod(MethodInfo method, SyncType[] argTypes); - ISyncMethod RegisterSyncMethodLambda(Type parentType, string parentMethod, int lambdaOrdinal, Type[] parentArgs = null, ParentMethodType parentParentMethodType = ParentMethodType.Normal); - ISyncMethod RegisterSyncMethodLambdaInGetter(Type parentType, string parentMethod, int lambdaOrdinal); - - ISyncDelegate RegisterSyncDelegate(Type type, string nestedType, string method); - ISyncDelegate RegisterSyncDelegate(Type inType, string nestedType, string methodName, string[] fields, Type[] args = null); - ISyncDelegate RegisterSyncDelegateLambda(Type parentType, string parentMethod, int lambdaOrdinal, Type[] parentArgs = null, ParentMethodType parentParentMethodType = ParentMethodType.Normal); - ISyncDelegate RegisterSyncDelegateLambdaInGetter(Type parentType, string parentMethod, int lambdaOrdinal); - ISyncDelegate RegisterSyncDelegateLocalFunc(Type parentType, string parentMethod, string localFuncName, Type[] parentArgs = null); - - void RegisterSyncWorker(SyncWorkerDelegate syncWorkerDelegate, Type targetType = null, bool isImplicit = false, bool shouldConstruct = false); - - void RegisterDialogNodeTree(Type type, string methodOrPropertyName, SyncType[] argTypes = null); - - void RegisterDialogNodeTree(MethodInfo method); - - [Obsolete($"Use {nameof(Session)} instead.")] - void RegisterPauseLock(PauseLockDelegate pauseLock); - - void RegisterDefaultLetterChoice(MethodInfo method, Type letterType = null); - - Thing GetThingById(int id); - bool TryGetThingById(int id, out Thing value); - - IReadOnlyList GetPlayers(); - IPlayerInfo GetPlayerById(int id); - - ISessionManager GetGlobalSessionManager(); - ISessionManager GetLocalSessionManager(Map map); - void SetCurrentSessionWithTransferables(ISessionWithTransferables session); - } - - /// - /// An interface for the class holding player data - /// - public interface IPlayerInfo - { - /// - /// ID of the current player - /// - int Id { get; } - /// - /// Username of the current player - /// - string Username { get; } - /// - /// if the current player is Arbiter instance, in every other case - /// - bool IsArbiter { get; } - - /// - /// of the map the player is on - /// - int CurrentMapIndex { get; } - /// - /// The map the current player is on - /// - Map CurrentMap { get; } - - /// - /// List of all the things the player has selected, as numeric IDs - /// - /// Generally use , unless you're able to access the IDs directly. - IReadOnlyList SelectedThingsByIds { get; } - /// - /// List of all the things the player has selected - /// - IReadOnlyList SelectedThings { get; } - } - - public interface ISessionManager - { - /// - /// Returns the list of all currently active sessions for this specific . - /// - IReadOnlyList AllSessions { get; } - /// - /// Returns the list of all currently active exposable sessions for this specific . - /// - IReadOnlyList ExposableSessions { get; } - /// - /// Returns the list of all currently active semi-persistent sessions for this specific . - /// - IReadOnlyList SemiPersistentSessions { get; } - /// - /// Returns the list of all currently active ticking sessions for this specific . - /// - IReadOnlyList TickingSessions { get; } - /// - /// A convenience property for checking if any of the sessions is active. - /// - bool AnySessionActive { get; } - - /// - /// Adds a new session to the list of active sessions. - /// - /// The session to try to add to active sessions. - /// if the session was added to active ones, if there was a conflict between sessions. - bool AddSession(Session session); - - /// - /// Tries to get a conflicting session (through the use of ) or, if there was none, returns the input . - /// - /// The session to try to add to active sessions. - /// A session that was conflicting with the input one, or the input itself if there were no conflicts. It may be of a different type than the input. - Session GetOrAddSessionAnyConflict(Session session); - - /// - /// Tries to get a conflicting session (through the use of ) or, if there was none, returns the input . - /// - /// The session to try to add to active sessions. - /// A session that was conflicting with the input one if it's the same type (other is T), null if it's a different type, or the input itself if there were no conflicts. - T GetOrAddSession(T session) where T : Session; - - /// - /// Tries to remove a session from active ones. - /// - /// The session to try to remove from the active sessions. - /// if successfully removed from . Doesn't correspond to if it was successfully removed from other lists of sessions. - bool RemoveSession(Session session); - - /// - /// Returns the first active session of specific type. - /// - /// Type of the session to retrieve. - /// The first session of specified type, or if there are none. - T GetFirstOfType() where T : Session; - - /// - /// Returns the session with specific ID of specific type. - /// - /// The ID of the session to search for. - /// Type of the session to retrieve. - /// The session with provided ID and of specified type, or if there are none. - T GetFirstWithId(int id) where T : Session; - - /// - /// Returns the session with specific ID. - /// - /// The ID of the session to search for. - /// The session with provided ID, or if there are none. - Session GetFirstWithId(int id); - - /// - /// Checks if any of active sessions is currently pausing the game. - /// - /// The map at which the sessions would check if the game is paused. Global session manager accepts for global pausing. - /// if any session is active, otherwise. - /// Local session managers expect the to be the same as the map it's attached to. - bool IsAnySessionCurrentlyPausing(Map map); // Is it necessary for the API? - } - - /// - /// Required by sessions dealing with transferables, like trading or caravan forming. By implementing this interface, Multiplayer will handle majority of syncing of changes in transferables. - /// When drawing the dialog tied to this session, you'll have to set to the proper session, and set it to null once done. - /// - /// For safety, make sure to set in and unset in . - public interface ISessionWithTransferables - { - /// - /// Used when syncing data across players, specifically to retrieve based on the it has. - /// - /// of the . - /// which corresponds to a with specific . - Transferable GetTransferableByThingId(int thingId); - - /// - /// Called when the count in a specific was changed. - /// - /// Transferable whose count was changed. - void Notify_CountChanged(Transferable tr); - } - - /// - /// Interface used by sessions that have restrictions based on other existing sessions, for example limiting them to only 1 session of specific type. - /// - public interface ISessionWithCreationRestrictions - { - /// - /// Method used to check if the current session can be created by checking other . - /// Only sessions in the current context are checked (local map sessions or global sessions). - /// - /// The other session the current one is checked against. Can be of different type. - /// Currently only the current class checks against the existing ones - the existing classed don't check against this one. - /// if the current session should be created, otherwise - bool CanExistWith(Session other); - } - - /// - /// Used by sessions that are are required to tick together with the map/world. - /// - public interface ITickingSession - { - /// - /// Called once per session when the map (for local sessions) or the world (for global sessions) is ticking. - /// - /// The sessions are iterated over backwards using a for loop, so it's safe for them to remove themselves from the session manager. - void Tick(); - } -} diff --git a/Source/API/MP.cs b/Source/API/MP.cs index 407e49d..4c457ac 100644 --- a/Source/API/MP.cs +++ b/Source/API/MP.cs @@ -3,432 +3,437 @@ using System.Diagnostics; using System.Linq; using System.Reflection; - +using RimWorld; using Verse; -namespace Multiplayer.API +namespace Multiplayer.API; + +/// +/// The primary static class that contains methods used to interface with the multiplayer mod. +/// +[StaticConstructorOnStartup] +public static class MP { - /// - /// The primary static class that contains methods used to interface with the multiplayer mod. - /// - [StaticConstructorOnStartup] - public static class MP - { - /// Contains the API version - public const string API = "0.5"; + /// Contains the API version + public const string API = "0.5"; - /// - /// Returns if API is initialized. - /// - public static readonly bool enabled = false; + /// + /// Returns if API is initialized. + /// + public static readonly bool enabled = false; - private static readonly IAPI Sync; + private static readonly IAPI Sync; - static MP() - { - var mpAssembly = LoadedModManager.RunningMods - .SelectMany(m => m.assemblies.loadedAssemblies) - .FirstOrDefault(a => a.GetName().Name == "Multiplayer"); + static MP() + { + var mpAssembly = LoadedModManager.RunningMods + .SelectMany(m => m.assemblies.loadedAssemblies) + .FirstOrDefault(a => a.GetName().Name == "Multiplayer"); - if (mpAssembly == null) - { - Sync = new Dummy(); + if (mpAssembly == null) + { + Sync = new Dummy(); - return; - } + return; + } - // This can fail in older MP versions halting mod loading - try { - Sync = (IAPI) mpAssembly + // This can fail in older MP versions halting mod loading + try { + Sync = (IAPI) mpAssembly .GetType("Multiplayer.Common.MultiplayerAPIBridge") .GetField("Instance") .GetValue(null); - enabled = true; - } catch(Exception e) { - Log.Error("Multiplayer mod detected but it has no MPAPI Bridge\n\n" + e); - } + enabled = true; + } catch(Exception e) { + Log.Error("Multiplayer mod detected but it has no MPAPI Bridge\n\n" + e); } + } + + // Some nice shortcuts ready to be inlined by the JIT compiler :) + + /// + /// Returns if currently running on a host. + /// + public static bool IsHosting => Sync.IsHosting; + + /// + /// Returns if currently running in a multiplayer session (both on client and host). + /// + public static bool IsInMultiplayer => Sync.IsInMultiplayer; + + /// + /// Returns local player's name. + /// + public static string PlayerName => Sync.PlayerName; + + /// + /// Returns if currently there's a sync command being executed. + /// + public static bool IsExecutingSyncCommand => Sync.IsExecutingSyncCommand; + + /// + /// Returns if currently there's a sync command being executed that was issued by the current player. + /// + public static bool IsExecutingSyncCommandIssuedBySelf => Sync.IsExecutingSyncCommandIssuedBySelf; + + /// + /// Returns if the current player is allowed to use dev mode commands. + /// + public static bool CanUseDevMode => Sync.CanUseDevMode; + + /// + /// Returns roughly when not executing code which is running deterministically on all clients. + /// Such code needs to use the Sync system to synchronize game state changes and a method can detect whether it needs to synchronize using this property. + /// It's , for example, in OnGUI (hence the name). + /// + /// + /// Returns if all the following conditions are : + /// + /// Multiplayer mod is enabled + /// The game is currently in multiplayer mode + /// The game is currently not ticking + /// The game is not running sync commands + /// The multiplayer game is not being reloaded + /// is + /// is + /// + /// If any of them are , it returns . + /// + public static bool InInterface => Sync.InInterface; + + /// + /// The actual faction of the player irrespective of the faction context which might be set in multifaction. + /// In singleplayer, returns Faction.OfPlayer. + /// + public static Faction RealPlayerFaction => Sync.RealPlayerFaction; + + /// + /// Used to set the ThingFilter context for interactions with ThingFilter UI. + /// Set the context before drawing the ThingFilter and then set it back to after it's drawn. + /// + /// This method is not "reentrant". If you call it twice without setting the context back to , the second call will throw an exception. + /// + /// + /// The ThingFilter context object + public static void SetThingFilterContext(ThingFilterContext context) => Sync.SetThingFilterContext(context); + + /// + /// Starts a new synchronization stack. + /// + /// + /// Has to be called before invoking Watch methods. + /// See also . + /// + public static void WatchBegin() => Sync.WatchBegin(); + + /// + /// Helper method for given a type. + /// + /// An object of type set in the to watch, for static types + /// name of the field to watch for changes + /// Index in the field path set in + public static void Watch(Type type, string fieldName, object index = null) => Sync.Watch(type, fieldName, index); + + /// + /// Helper method for given an instance. + /// + /// An object of type set in the to watch + /// name of the field to watch for changes + /// Index in the field path set in + public static void Watch(object target, string fieldName, object index = null) => Sync.Watch(target, fieldName, index); + + /// + /// Helper method for given an instance. + /// + /// the memberPath of the ISyncField + /// An object of type set in the to watch, null for static + /// Index in the field path set in + public static void Watch(string memberPath, object target = null, object index = null) => Sync.Watch(memberPath, target, index); + + /// + /// Ends the current synchronization stack and executes it. + /// + /// + /// Has to be called after invoking Watch methods. + /// See also . + /// + public static void WatchEnd() => Sync.WatchEnd(); + + /// + /// Searches current assembly for MPAPI annotations and registers them + /// + /// + /// + public static void RegisterAll() => RegisterAll(new StackTrace().GetFrame(1).GetMethod().ReflectedType.Assembly); + + /// + /// Searches the given assembly for MPAPI annotations and registers them + /// The assembly + /// + /// + /// + public static void RegisterAll(Assembly assembly) => Sync.RegisterAll(assembly); + + /// + /// Registers a field for syncing and returns it's . + /// + /// + /// It's recommended to use instead, unless you have to otherwise. + /// They must be Watched between MP.WatchBegin and MP.WatchEnd with the MP.Watch* methods + /// + /// + /// Type of the target class that contains the specified member + /// if null, will point at field from the global namespace in the "Type/fieldName" format. + /// + /// Path to a member. If the member is to be indexed, it has to end with /[] eg. "myArray/[]" + /// A new registered + public static ISyncField RegisterSyncField(Type targetType, string memberPath) => Sync.RegisterSyncField(targetType, memberPath); + + /// + /// Registers a field for syncing and returns it's . + /// + /// + /// It's recommended to use instead, unless you have to otherwise. + /// They must be Watched between MP.WatchBegin and MP.WatchEnd with the MP.Watch* methods + /// + /// FieldInfo of a field to register + /// A new registered + public static ISyncField RegisterSyncField(FieldInfo field) => Sync.RegisterSyncField(field); + + /// + /// Registers a method for syncing and returns its . + /// + /// + /// It's recommended to use instead, unless you have to otherwise. + /// + /// Type that contains the method + /// Name of the method + /// Method's parameter types + /// A new registered + public static ISyncMethod RegisterSyncMethod(Type type, string methodOrPropertyName, SyncType[] argTypes = null) => Sync.RegisterSyncMethod(type, methodOrPropertyName, argTypes); + + /// + /// Registers a method for syncing and returns its . + /// + /// MethodInfo of a method to register + /// Method's parameter types + /// + /// It's recommended to use instead, unless you have to otherwise. + /// + /// A new registered + /// + /// Register a method for syncing using reflection and set it to debug only. + /// + /// RegisterSyncMethod(typeof(MyType).GetMethod(nameof(MyType.MyMethod))).SetDebugOnly(); + /// + /// + public static ISyncMethod RegisterSyncMethod(MethodInfo method, SyncType[] argTypes = null) => Sync.RegisterSyncMethod(method, argTypes); + + /// + /// Registers a compiler-generated lambda for syncing and returns its , you will have to figure out the ordinal of your target by decompiling. + /// + /// Type that contains the method. + /// Name of the method the lambda is a child of. + /// For example, with lambdaOrdinal = 3: <FillTab>b__10_3 + /// Arguments of the parent method. Needed if there's an more than 1 method with the same name. + /// The type of the parent method. + /// A new registered + public static ISyncMethod RegisterSyncMethodLambda(Type parentType, string parentMethod, int lambdaOrdinal, Type[] parentArgs = null, ParentMethodType parentParentMethodType = ParentMethodType.Normal) + => Sync.RegisterSyncMethodLambda(parentType, parentMethod, lambdaOrdinal, parentArgs, parentParentMethodType); - // Some nice shortcuts ready to be inlined by the JIT compiler :) - - /// - /// Returns if currently running on a host. - /// - public static bool IsHosting => Sync.IsHosting; - - /// - /// Returns if currently running in a multiplayer session (both on client and host). - /// - public static bool IsInMultiplayer => Sync.IsInMultiplayer; - - /// - /// Returns local player's name. - /// - public static string PlayerName => Sync.PlayerName; - - /// - /// Returns if currently there's a sync command being executed. - /// - public static bool IsExecutingSyncCommand => Sync.IsExecutingSyncCommand; - - /// - /// Returns if currently there's a sync command being executed that was issued by the current player. - /// - public static bool IsExecutingSyncCommandIssuedBySelf => Sync.IsExecutingSyncCommandIssuedBySelf; - - /// - /// Returns if the current player is allowed to use dev mode commands. - /// - public static bool CanUseDevMode => Sync.CanUseDevMode; - - /// - /// Used to determine if the currently running code is potentially unsafe for modifying the game state, allowing for them to be handled differently or synchronized. - /// An example of where this could be useful is harmony patches (besides transpilers) on sync methods - the method would get cancelled and synchronized, but the patches will still run. - /// In situation like that the patch should be cancelled if returns , as it'll run again after synchronizing. - /// - /// - /// Returns if all the following conditions are : - /// - /// Multiplayer mod is enabled - /// The game is currently in multiplayer mode - /// The game is currently not ticking (interface drawing code, etc.) - /// The game is not running sync commands - /// The multiplayer game is not being reloaded - /// is - /// is - /// - /// If any of them are , it returns . - /// - public static bool InInterface => Sync.InInterface; - - /// - /// Used to set the ThingFilter context for interactions with ThingFilter UI. - /// Set the context before drawing the ThingFilter and then set it back to after it's drawn. - /// - /// This method is not "reentrant". If you call it twice without setting the context back to , the second call will throw an exception. - /// - /// - /// The ThingFilter context object - public static void SetThingFilterContext(ThingFilterContext context) => Sync.SetThingFilterContext(context); - - /// - /// Starts a new synchronization stack. - /// - /// - /// Has to be called before invoking Watch methods. - /// See also . - /// - public static void WatchBegin() => Sync.WatchBegin(); - - /// - /// Helper method for given a type. - /// - /// An object of type set in the to watch, for static types - /// name of the field to watch for changes - /// Index in the field path set in - public static void Watch(Type type, string fieldName, object index = null) => Sync.Watch(type, fieldName, index); - - /// - /// Helper method for given an instance. - /// - /// An object of type set in the to watch - /// name of the field to watch for changes - /// Index in the field path set in - public static void Watch(object target, string fieldName, object index = null) => Sync.Watch(target, fieldName, index); - - /// - /// Helper method for given an instance. - /// - /// the memberPath of the ISyncField - /// An object of type set in the to watch, null for static - /// Index in the field path set in - public static void Watch(string memberPath, object target = null, object index = null) => Sync.Watch(memberPath, target, index); - - /// - /// Ends the current synchronization stack and executes it. - /// - /// - /// Has to be called after invoking Watch methods. - /// See also . - /// - public static void WatchEnd() => Sync.WatchEnd(); - - /// - /// Searches current assembly for MPAPI annotations and registers them - /// - /// - /// - public static void RegisterAll() => RegisterAll(new StackTrace().GetFrame(1).GetMethod().ReflectedType.Assembly); - - /// - /// Searches the given assembly for MPAPI annotations and registers them - /// The assembly - /// - /// - /// - public static void RegisterAll(Assembly assembly) => Sync.RegisterAll(assembly); - - /// - /// Registers a field for syncing and returns it's . - /// - /// - /// It's recommended to use instead, unless you have to otherwise. - /// They must be Watched between MP.WatchBegin and MP.WatchEnd with the MP.Watch* methods - /// - /// - /// Type of the target class that contains the specified member - /// if null, will point at field from the global namespace in the "Type/fieldName" format. - /// - /// Path to a member. If the member is to be indexed, it has to end with /[] eg. "myArray/[]" - /// A new registered - public static ISyncField RegisterSyncField(Type targetType, string memberPath) => Sync.RegisterSyncField(targetType, memberPath); - - /// - /// Registers a field for syncing and returns it's . - /// - /// - /// It's recommended to use instead, unless you have to otherwise. - /// They must be Watched between MP.WatchBegin and MP.WatchEnd with the MP.Watch* methods - /// - /// FieldInfo of a field to register - /// A new registered - public static ISyncField RegisterSyncField(FieldInfo field) => Sync.RegisterSyncField(field); - - /// - /// Registers a method for syncing and returns its . - /// - /// - /// It's recommended to use instead, unless you have to otherwise. - /// - /// Type that contains the method - /// Name of the method - /// Method's parameter types - /// A new registered - public static ISyncMethod RegisterSyncMethod(Type type, string methodOrPropertyName, SyncType[] argTypes = null) => Sync.RegisterSyncMethod(type, methodOrPropertyName, argTypes); - - /// - /// Registers a method for syncing and returns its . - /// - /// MethodInfo of a method to register - /// Method's parameter types - /// - /// It's recommended to use instead, unless you have to otherwise. - /// - /// A new registered - /// - /// Register a method for syncing using reflection and set it to debug only. - /// - /// RegisterSyncMethod(typeof(MyType).GetMethod(nameof(MyType.MyMethod))).SetDebugOnly(); - /// - /// - public static ISyncMethod RegisterSyncMethod(MethodInfo method, SyncType[] argTypes = null) => Sync.RegisterSyncMethod(method, argTypes); - - /// - /// Registers a compiler-generated lambda for syncing and returns its , you will have to figure out the ordinal of your target by decompiling. - /// - /// Type that contains the method. - /// Name of the method the lambda is a child of. - /// For example, with lambdaOrdinal = 3: <FillTab>b__10_3 - /// Arguments of the parent method. Needed if there's an more than 1 method with the same name. - /// The type of the parent method. - /// A new registered - public static ISyncMethod RegisterSyncMethodLambda(Type parentType, string parentMethod, int lambdaOrdinal, Type[] parentArgs = null, ParentMethodType parentParentMethodType = ParentMethodType.Normal) - => Sync.RegisterSyncMethodLambda(parentType, parentMethod, lambdaOrdinal, parentArgs, parentParentMethodType); - - /// - /// Registers a compiler-generated lambda for syncing and returns its , you will have to figure out the ordinal of your target by decompiling. - /// - /// Exists for convenience, the outcome will be the same as calling with parentMethod set to - /// - /// - /// Type that contains the method. - /// Name of the method the lambda is a child of. - /// For example, with lambdaOrdinal = 3: <FillTab>b__10_3 - /// A new registered - public static ISyncMethod RegisterSyncMethodLambdaInGetter(Type parentType, string parentMethod, int lambdaOrdinal) - => Sync.RegisterSyncMethodLambdaInGetter(parentType, parentMethod, lambdaOrdinal); - - /// - /// Registers the syncDelegate. Handles anonymous nested types, you will have to figure out the name of your target by decompiling. - /// - /// The sync delegate. - /// Type. - /// Nested type. - /// Method. - public static ISyncDelegate RegisterSyncDelegate(Type type, string nestedType, string method) => Sync.RegisterSyncDelegate(type, nestedType, method); - - /// - /// Registers the syncDelegate. Handles anonymous nested types, you will have to figure out the name of your target by decompiling. - /// - /// The sync delegate. - /// In type. - /// Nested type. - /// Method name. - /// Fields. - /// Arguments. - public static ISyncDelegate RegisterSyncDelegate(Type inType, string nestedType, string methodName, string[] fields, Type[] args = null) => Sync.RegisterSyncDelegate(inType, nestedType, methodName, fields, args); + /// + /// Registers a compiler-generated lambda for syncing and returns its , you will have to figure out the ordinal of your target by decompiling. + /// + /// Exists for convenience, the outcome will be the same as calling with parentMethod set to + /// + /// + /// Type that contains the method. + /// Name of the method the lambda is a child of. + /// For example, with lambdaOrdinal = 3: <FillTab>b__10_3 + /// A new registered + public static ISyncMethod RegisterSyncMethodLambdaInGetter(Type parentType, string parentMethod, int lambdaOrdinal) + => Sync.RegisterSyncMethodLambdaInGetter(parentType, parentMethod, lambdaOrdinal); + + /// + /// Registers the syncDelegate. Handles anonymous nested types, you will have to figure out the name of your target by decompiling. + /// + /// The sync delegate. + /// Type. + /// Nested type. + /// Method. + public static ISyncDelegate RegisterSyncDelegate(Type type, string nestedType, string method) => Sync.RegisterSyncDelegate(type, nestedType, method); + + /// + /// Registers the syncDelegate. Handles anonymous nested types, you will have to figure out the name of your target by decompiling. + /// + /// The sync delegate. + /// In type. + /// Nested type. + /// Method name. + /// Fields. + /// Arguments. + public static ISyncDelegate RegisterSyncDelegate(Type inType, string nestedType, string methodName, string[] fields, Type[] args = null) => Sync.RegisterSyncDelegate(inType, nestedType, methodName, fields, args); - /// - /// Registers the syncDelegate. Handles anonymous nested types, you will have to figure out the name and lambda ordinal of your target by decompiling. - /// - /// The sync delegate. - /// Type that contains the method. - /// Name of the method the lambda is a child of. - /// For example, with lambdaOrdinal = 3: <FillTab>b__10_3 - /// Arguments of the parent method. Needed if there's an more than 1 method with the same name. - /// The type of the parent method. - public static ISyncDelegate RegisterSyncDelegateLambda(Type parentType, string parentMethod, int lambdaOrdinal, Type[] parentArgs = null, ParentMethodType parentParentMethodType = ParentMethodType.Normal) - => Sync.RegisterSyncDelegateLambda(parentType, parentMethod, lambdaOrdinal, parentArgs, parentParentMethodType); - - /// - /// Registers the syncDelegate. Handles anonymous nested types, you will have to figure out the name and lambda ordinal of your target by decompiling. - /// - /// The sync delegate. - /// Type that contains the method. - /// Name of the method the lambda is a child of. - /// For example, with lambdaOrdinal = 3: <FillTab>b__10_3 - public static ISyncDelegate RegisterSyncDelegateLambdaInGetter(Type parentType, string parentMethod, int lambdaOrdinal) - => Sync.RegisterSyncDelegateLambdaInGetter(parentType, parentMethod, lambdaOrdinal); - - /// - /// Registers the syncDelegate. Handles anonymous nested types, you will have to figure out the name and lambda ordinal of your target by decompiling. - /// - /// The sync delegate. - /// Type that contains the method. - /// Name of the method the lambda is a child of. - /// For example, for local function named Start: <DoWindowContents>g__Start|10 - /// Arguments of the parent method. Needed if there's an more than 1 method with the same name. - public static ISyncDelegate RegisterSyncDelegateLocalFunc(Type parentType, string parentMethod, string localFuncName, Type[] parentArgs = null) - => Sync.RegisterSyncDelegateLocalFunc(parentType, parentMethod, localFuncName, parentArgs); - - /// - /// Registers the SyncWorker based on SyncWorkerDelegate. - /// - /// - /// It's recommended to use instead, unless you have to otherwise. - /// - /// Sync worker delegate. - /// Type to handle. - /// If set to true the SyncWorker will handle the type and all the derivate Types. - /// If set to true the SyncWorker will be provided with an instance created with no arguments. - /// Type to handle. - public static void RegisterSyncWorker(SyncWorkerDelegate syncWorkerDelegate, Type targetType = null, bool isImplicit = false, bool shouldConstruct = false) => Sync.RegisterSyncWorker(syncWorkerDelegate, targetType, isImplicit: isImplicit, shouldConstruct: shouldConstruct); - - /// - /// Registers a method which opens a . The options picked by players will then be synced between all clients. - /// - /// Type that contains the method - /// Name of the method - /// Method's parameter types - public static void RegisterSyncDialogNodeTree(Type type, string methodOrPropertyName, SyncType[] argTypes = null) => Sync.RegisterDialogNodeTree(type, methodOrPropertyName, argTypes); - - /// - /// Registers a method which opens a . The options picked by players will then be synced between all clients. - /// - /// MethodInfo of a method to register - /// - /// It's recommended to use instead, unless you have to otherwise. - /// It can be combined with so the call will be replicated by the MPApi on all clients automatically. - /// - /// - /// Register a method creating a for syncing using reflection and set it to debug only. - /// - /// RegisterSyncDialogNodeTree(typeof(MyType).GetMethod(nameof(MyType.MyMethod))).SetDebugOnly(); - /// - /// - public static void RegisterSyncDialogNodeTree(MethodInfo method) => Sync.RegisterDialogNodeTree(method); - - /// - /// Registers a delegate which will be called to check if the game should be paused on specific map. - /// In case async time is active, only that map will be paused, otherwise all of them will be paused. - /// If async time is enabled, it'll also be called globally with as the parameter, which will affect all maps. It's skipped if async time is disabled. - /// - /// Delegate with as the only parameter returning a which will be called to see if the game should be paused - /// It's recommended to use instead, unless you have to otherwise. - /// - /// Register a method as a delagate for forced pause locking - /// - /// void Register() => RegisterPauseLock(MyMethod); - /// void MyMethod(Map map) => return MyOtherClass.shouldPause; - /// - /// - /// Register a dynamic method as a delegate for forced pause locking - /// - /// RegisterPauseLock(map => MyOtherClass.shouldPause); - /// - /// - [Obsolete($"Use {nameof(Session)} instead.")] - public static void RegisterPauseLock(PauseLockDelegate pauseLock) => Sync.RegisterPauseLock(pauseLock); - - /// - /// In multiplayer, choice letters don't pause the game when expiring - instead, using a default choice (usually rejecting, if applicable). - /// This does not automatically sync choices, and the choices themselves need syncing through a sync method/delegate. - /// - /// Method that will be called when the letter expires. Can either be a method inside of the letter class itself, or a static method (with the instance as the parameter). - /// The type of the letter. If null, will be used. - public static void RegisterDefaultLetterChoice(MethodInfo method, Type letterType = null) => Sync.RegisterDefaultLetterChoice(method, letterType); - - /// - /// Retrieves a with a provided id - /// - /// for the to retrieve - /// with a specific numeric ID. - public static Thing GetThingById(int id) => Sync.GetThingById(id); - /// - /// Retrieves a with a provided id and returns a for success/failure - /// - /// for the to retrieve - /// The value of retrieved , if any. - /// if successful - public static bool TryGetThingById(int id, out Thing value) => Sync.TryGetThingById(id, out value); - - /// - /// Retrieves a list of for every player - /// - /// List with every - public static IReadOnlyList GetPlayers() => Sync.GetPlayers(); - /// - /// Retrieves a with a specific - /// - /// of the player to retrieve - /// Player with specified ID number - public static IPlayerInfo GetPlayerById(int id) => Sync.GetPlayerById(id); - - /// - /// Retrieves the global (world) session manager. - /// - /// The global (world) session manager. - /// As long as a multiplayer session is active, there should always be a global session manager. This method should never return in such cases, unless something is very broken. - public static ISessionManager GetGlobalSessionManager() => Sync.GetGlobalSessionManager(); - /// - /// Retrieves the local (map) session manager. - /// - /// The map whose session manager will be retrieved. - /// The local (map) session manager. - /// As long as a multiplayer session is active, all maps should contain a session manager. This method should never return in such cases, unless something is very broken. - /// Thrown when is null. - public static ISessionManager GetLocalSessionManager(Map map) => Sync.GetLocalSessionManager(map); - /// - /// Sets the currently active session with transferables. Used for syncing changes in transferables by letting Multiplayer know which session should be synced. - /// It cannot be set to anything but while a session is currently set as active. - /// The session needs to be set before (potentially) operating on the trasnferables, and unset afterwards. - /// The session should be set/unset in a block. - /// - /// The session to set as the active one, or to unset. - /// - /// - /// try - /// { - /// MP.SetCurrentSessionWithTransferables(session); - /// OperateOnTransferables(); - /// } - /// finally - /// { - /// MP.SetCurrentSessionWithTransferables(null); - /// } - /// - /// - public static void SetCurrentSessionWithTransferables(ISessionWithTransferables session) => Sync.SetCurrentSessionWithTransferables(session); - } -} + /// + /// Registers the syncDelegate. Handles anonymous nested types, you will have to figure out the name and lambda ordinal of your target by decompiling. + /// + /// The sync delegate. + /// Type that contains the method. + /// Name of the method the lambda is a child of. + /// For example, with lambdaOrdinal = 3: <FillTab>b__10_3 + /// Arguments of the parent method. Needed if there's an more than 1 method with the same name. + /// The type of the parent method. + public static ISyncDelegate RegisterSyncDelegateLambda(Type parentType, string parentMethod, int lambdaOrdinal, Type[] parentArgs = null, ParentMethodType parentParentMethodType = ParentMethodType.Normal) + => Sync.RegisterSyncDelegateLambda(parentType, parentMethod, lambdaOrdinal, parentArgs, parentParentMethodType); + + /// + /// Registers the syncDelegate. Handles anonymous nested types, you will have to figure out the name and lambda ordinal of your target by decompiling. + /// + /// The sync delegate. + /// Type that contains the method. + /// Name of the method the lambda is a child of. + /// For example, with lambdaOrdinal = 3: <FillTab>b__10_3 + public static ISyncDelegate RegisterSyncDelegateLambdaInGetter(Type parentType, string parentMethod, int lambdaOrdinal) + => Sync.RegisterSyncDelegateLambdaInGetter(parentType, parentMethod, lambdaOrdinal); + + /// + /// Registers the syncDelegate. Handles anonymous nested types, you will have to figure out the name and lambda ordinal of your target by decompiling. + /// + /// The sync delegate. + /// Type that contains the method. + /// Name of the method the lambda is a child of. + /// For example, for local function named Start: <DoWindowContents>g__Start|10 + /// Arguments of the parent method. Needed if there's an more than 1 method with the same name. + public static ISyncDelegate RegisterSyncDelegateLocalFunc(Type parentType, string parentMethod, string localFuncName, Type[] parentArgs = null) + => Sync.RegisterSyncDelegateLocalFunc(parentType, parentMethod, localFuncName, parentArgs); + + /// + /// Registers the SyncWorker based on SyncWorkerDelegate. + /// + /// + /// It's recommended to use instead, unless you have to otherwise. + /// + /// Sync worker delegate. + /// Type to handle. + /// If set to true the SyncWorker will handle the type and all the derivate Types. + /// If set to true the SyncWorker will be provided with an instance created with no arguments. + /// Type to handle. + public static void RegisterSyncWorker(SyncWorkerDelegate syncWorkerDelegate, Type targetType = null, bool isImplicit = false, bool shouldConstruct = false) => Sync.RegisterSyncWorker(syncWorkerDelegate, targetType, isImplicit: isImplicit, shouldConstruct: shouldConstruct); + + /// + /// Registers a method which opens a . The options picked by players will then be synced between all clients. + /// + /// Type that contains the method + /// Name of the method + /// Method's parameter types + public static void RegisterSyncDialogNodeTree(Type type, string methodOrPropertyName, SyncType[] argTypes = null) => Sync.RegisterDialogNodeTree(type, methodOrPropertyName, argTypes); + + /// + /// Registers a method which opens a . The options picked by players will then be synced between all clients. + /// + /// MethodInfo of a method to register + /// + /// It's recommended to use instead, unless you have to otherwise. + /// It can be combined with so the call will be replicated by the MPApi on all clients automatically. + /// + /// + /// Register a method creating a for syncing using reflection and set it to debug only. + /// + /// RegisterSyncDialogNodeTree(typeof(MyType).GetMethod(nameof(MyType.MyMethod))).SetDebugOnly(); + /// + /// + public static void RegisterSyncDialogNodeTree(MethodInfo method) => Sync.RegisterDialogNodeTree(method); + + /// + /// Registers a delegate which will be called to check if the game should be paused on specific map. + /// In case async time is active, only that map will be paused, otherwise all of them will be paused. + /// If async time is enabled, it'll also be called globally with as the parameter, which will affect all maps. It's skipped if async time is disabled. + /// + /// Delegate with as the only parameter returning a which will be called to see if the game should be paused + /// It's recommended to use instead, unless you have to otherwise. + /// + /// Register a method as a delagate for forced pause locking + /// + /// void Register() => RegisterPauseLock(MyMethod); + /// void MyMethod(Map map) => return MyOtherClass.shouldPause; + /// + /// + /// Register a dynamic method as a delegate for forced pause locking + /// + /// RegisterPauseLock(map => MyOtherClass.shouldPause); + /// + /// + [Obsolete($"Use {nameof(Session)} instead.")] + public static void RegisterPauseLock(PauseLockDelegate pauseLock) => Sync.RegisterPauseLock(pauseLock); + + /// + /// In multiplayer, choice letters don't pause the game when expiring - instead, using a default choice (usually rejecting, if applicable). + /// This does not automatically sync choices, and the choices themselves need syncing through a sync method/delegate. + /// + /// Method that will be called when the letter expires. Can either be a method inside of the letter class itself, or a static method (with the instance as the parameter). + /// The type of the letter. If null, will be used. + public static void RegisterDefaultLetterChoice(MethodInfo method, Type letterType = null) => Sync.RegisterDefaultLetterChoice(method, letterType); + + /// + /// Retrieves a with a provided id + /// + /// for the to retrieve + /// with a specific numeric ID. + public static Thing GetThingById(int id) => Sync.GetThingById(id); + /// + /// Retrieves a with a provided id and returns a for success/failure + /// + /// for the to retrieve + /// The value of retrieved , if any. + /// if successful + public static bool TryGetThingById(int id, out Thing value) => Sync.TryGetThingById(id, out value); + + /// + /// Retrieves a list of for every player + /// + /// List with every + public static IReadOnlyList GetPlayers() => Sync.GetPlayers(); + /// + /// Retrieves a with a specific + /// + /// of the player to retrieve + /// Player with specified ID number + public static IPlayerInfo GetPlayerById(int id) => Sync.GetPlayerById(id); + + /// + /// Retrieves the global (world) session manager. + /// + /// The global (world) session manager. + /// As long as a multiplayer session is active, there should always be a global session manager. This method should never return in such cases, unless something is very broken. + public static ISessionManager GetGlobalSessionManager() => Sync.GetGlobalSessionManager(); + /// + /// Retrieves the local (map) session manager. + /// + /// The map whose session manager will be retrieved. + /// The local (map) session manager. + /// As long as a multiplayer session is active, all maps should contain a session manager. This method should never return in such cases, unless something is very broken. + /// Thrown when is null. + public static ISessionManager GetLocalSessionManager(Map map) => Sync.GetLocalSessionManager(map); + /// + /// Sets the currently active session with transferables. Used for syncing changes in transferables by letting Multiplayer know which session should be synced. + /// It cannot be set to anything but while a session is currently set as active. + /// The session needs to be set before (potentially) operating on the trasnferables, and unset afterwards. + /// The session should be set/unset in a block. + /// + /// The session to set as the active one, or to unset. + /// + /// + /// try + /// { + /// MP.SetCurrentSessionWithTransferables(session); + /// OperateOnTransferables(); + /// } + /// finally + /// { + /// MP.SetCurrentSessionWithTransferables(null); + /// } + /// + /// + public static void SetCurrentSessionWithTransferables(ISessionWithTransferables session) => Sync.SetCurrentSessionWithTransferables(session); +} \ No newline at end of file diff --git a/Source/API/MPTypes.cs b/Source/API/MPTypes.cs deleted file mode 100644 index d7229c2..0000000 --- a/Source/API/MPTypes.cs +++ /dev/null @@ -1,303 +0,0 @@ -using System; -using System.Reflection; -using RimWorld; -using RimWorld.Planet; -using Verse; - -namespace Multiplayer.API -{ - /// - /// Context flags which are sent along with a command - /// - [Flags] - public enum SyncContext - { - /// Default value. (no context) - None = 0, - /// Send mouse cell context (emulates mouse position) - MapMouseCell = 1, - /// Send map selected context (object selected on the map) - MapSelected = 2, - /// Send world selected context (object selected on the world map) - WorldSelected = 4, - /// Send order queue context (emulates pressing KeyBindingDefOf.QueueOrder) - QueueOrder_Down = 8, - /// Send current map context - CurrentMap = 16, - } - - /// - /// An attribute that is used to mark methods for syncing. - /// The call will be replicated by the MPApi on all clients automatically. - /// - /// - /// An example showing how to mark a method for syncing. - /// - /// [SyncMethod] - /// public void MyMethod(...) - /// { - /// ... - /// } - /// - /// - [AttributeUsage(AttributeTargets.Method)] - public class SyncMethodAttribute : Attribute - { - public SyncContext context; - - /// Instructs SyncMethod to cancel synchronization if any arg is null (see ). - public bool cancelIfAnyArgNull = false; - - /// Instructs SyncMethod to cancel synchronization if no map objects were selected during the call (see ). - public bool cancelIfNoSelectedMapObjects = false; - - /// Instructs SyncMethod to cancel synchronization if no world objects were selected during call replication(see ). - public bool cancelIfNoSelectedWorldObjects = false; - - /// Instructs SyncMethod to synchronize only in debug mode (see ). - public bool debugOnly = false; - - /// A list of types to expose (see ) - public int[] exposeParameters; - - /// Context - public SyncMethodAttribute(SyncContext context = SyncContext.None) - { - this.context = context; - } - } - - /// - /// An attribute that is used to mark fields for syncing. - /// It will be Watched for changes by the MPApi when instructed. - /// - /// - /// An example showing how to mark a field for syncing. - /// - /// [SyncField] - /// public class MyClass - /// { - /// [SyncField] - /// bool myField; - /// - /// ... - /// } - /// - /// - [AttributeUsage(AttributeTargets.Field)] - public class SyncFieldAttribute : Attribute - { - public SyncContext context; - - /// Instructs SyncField to cancel synchronization if the value of the member it's pointing at is null. - public bool cancelIfValueNull = false; - - /// Instructs SyncField to sync in game loop. - public bool inGameLoop = false; - - /// Instructs SyncField to use a buffer instead of syncing instantly (when is called). - public bool bufferChanges = true; - - /// Instructs SyncField to synchronize only in debug mode. - public bool debugOnly = false; - - /// Instructs SyncField to synchronize only if it's invoked by the host. - public bool hostOnly = false; - - /// - public int version; - - /// Context - public SyncFieldAttribute(SyncContext context = SyncContext.None) - { - this.context = context; - } - } - - /// - /// An attribute that is used to mark methods which create for syncing. - /// Any option picked by a player will be synced between all clients automatically. - /// It can be combined with so the call will be replicated by the MPApi on all clients automatically. - /// - /// - /// An example showing how to mark a method for syncing. - /// - /// [SyncDialogNodeTree] - /// public void MyMethod(...) - /// { - /// ... - /// Find.WindowStack.Add(new Dialog_NodeTree(diaNode, faction)); - /// } - /// - /// - [AttributeUsage(AttributeTargets.Method)] - public class SyncDialogNodeTreeAttribute : Attribute - { } - - public struct SyncType - { - public readonly Type type; - public bool expose; - public bool contextMap; - - public SyncType(Type type) - { - this.type = type; - this.expose = false; - contextMap = false; - } - - public static implicit operator SyncType(ParameterInfo param) - { - return new SyncType(param.ParameterType) { /*expose = param.HasAttribute(), contextMap = param.HasAttribute()*/ }; - } - - public static implicit operator SyncType(Type type) - { - return new SyncType(type); - } - } - - /// Specifies the type of method. Those values are identical to Harmony's MethodType enum, and exist here to prevent reliance of this API on Harmony. - public enum ParentMethodType - { - /// This is a normal method - Normal, - /// This is a getter - Getter, - /// This is a setter - Setter, - /// This is a constructor - Constructor, - /// This is a static constructor - StaticConstructor, - /// This targets the MoveNext method of the enumerator result - Enumerator, - } - - /// - /// Used by Multiplayer's session manager to allow for creation of blocking dialogs, while (in case of async time) only pausing specific maps. - /// Sessions will be reset/reloaded during reloading - to prevent it, implement or . - /// You should avoid implementing this interface directly, instead opting into inheriting for greater compatibility. - /// - public abstract class Session - { - // Use internal to prevent mods from easily modifying it? - protected int sessionId; - // Should it be virtual? - /// - /// Used for syncing session across players by assigning them IDs, similarly to how every receives an ID. - /// Automatically applied by the session manager - /// If inheriting you don't have to worry about this property. - /// - public int SessionId - { - get => sessionId; - set => sessionId = value; - } - - /// - /// Used by the session manager while joining the game - if it returns it'll get removed. - /// - public virtual bool IsSessionValid => true; - - /// - /// Mandatory constructor for any subclass of . - /// - /// The map this session belongs to. It will be provided by session manager when syncing. - protected Session(Map map) { } - - /// - /// Called once the sessions has been added to the list of active sessions. Can be used for initialization. - /// - /// In case of , this will only be called if successfully added. - public virtual void PostAddSession() - { - } - - /// - /// Called once the sessions has been removed to the list of active sessions. Can be used for cleanup. - /// - public virtual void PostRemoveSession() - { - } - - /// - /// A convenience method to switch to a specific map or world. Intended to be used from when opening menu. - /// - /// Map to switch to or to switch to world view. - protected static void SwitchToMapOrWorld(Map map) - { - if (map == null) - { - Find.World.renderer.wantedMode = WorldRenderMode.Planet; - } - else - { - if (WorldRendererUtility.WorldRenderedNow) CameraJumper.TryHideWorld(); - Current.Game.CurrentMap = map; - } - } - - /// - /// The map this session is used by or in case of global sessions. - /// - public abstract Map Map { get; } - - /// - /// Called when checking ticking and if any session returns - it'll force pause the map/game. - /// In case of local (map) sessions, it'll only be called by the current map. In case of global (world) sessions, it'll be called by the world and each map. - /// - /// Current map (when checked from local session manager) or (when checked from local session manager). - /// If there are multiple sessions active, this method is not guaranteed to run if a session before this one returned . - /// if the session should pause the map/game, otherwise. - public abstract bool IsCurrentlyPausing(Map map); - - /// - /// Called when a session is active, and if any session returns a non-null value, a button will be displayed which will display all options. - /// - /// Currently processed colonist bar entry. Will be called once per . - /// Menu option that will be displayed when the session is active. Can be . - public abstract FloatMenuOption GetBlockingWindowOptions(ColonistBar.Entry entry); - } - - /// - /// Sessions inheriting from this class contain persistent data. - /// When inheriting from this class, remember to call base.ExposeData() to let it handle - /// Persistent data: - /// - /// Serialized into XML using RimWorld's Scribe system - /// Save-bound: survives a server restart - /// - /// - public abstract class ExposableSession : Session, IExposable - { - /// - protected ExposableSession(Map map) : base(map) { } - - public virtual void ExposeData() - { - Scribe_Values.Look(ref sessionId, "sessionId"); - } - } - - /// - /// Sessions implementing this interface consist of semi-persistent data. - /// Semi-persistent data: - /// - /// Serialized into binary using the Sync system - /// Session-bound: survives a reload, lost when the server is closed - /// - /// - public abstract class SemiPersistentSession : Session - { - /// - protected SemiPersistentSession(Map map) : base(map) { } - - /// - /// Writes/reads the data used by this session. - /// - /// Sync worker used for writing/reading the data. - public abstract void Sync(SyncWorker sync); - } -} \ No newline at end of file diff --git a/Source/API/ParentMethodType.cs b/Source/API/ParentMethodType.cs new file mode 100644 index 0000000..5062146 --- /dev/null +++ b/Source/API/ParentMethodType.cs @@ -0,0 +1,18 @@ +namespace Multiplayer.API; + +/// Specifies the type of method. Those values are identical to Harmony's MethodType enum, and exist here to prevent reliance of this API on Harmony. +public enum ParentMethodType +{ + /// This is a normal method + Normal, + /// This is a getter + Getter, + /// This is a setter + Setter, + /// This is a constructor + Constructor, + /// This is a static constructor + StaticConstructor, + /// This targets the MoveNext method of the enumerator result + Enumerator, +} \ No newline at end of file diff --git a/Source/API/Serializer.cs b/Source/API/Serializer.cs deleted file mode 100644 index 850ce85..0000000 --- a/Source/API/Serializer.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; - -namespace Multiplayer.API -{ - public record Serializer( - Func Writer, // (live, target, args) => networked - Func Reader // (networked) => live - ); - - public static class Serializer - { - public static Serializer New(Func writer, Func reader) - { - return new(writer, reader); - } - - public static Serializer New(Func writer, Func reader) - { - return new((live, _, _) => writer(live), reader); - } - - public static Serializer SimpleReader(Func reader) - { - return new((_, _, _) => null, _ => reader()); - } - } -} \ No newline at end of file diff --git a/Source/API/Sessions/ExposableSession.cs b/Source/API/Sessions/ExposableSession.cs new file mode 100644 index 0000000..64aee69 --- /dev/null +++ b/Source/API/Sessions/ExposableSession.cs @@ -0,0 +1,23 @@ +using Verse; + +namespace Multiplayer.API; + +/// +/// Sessions inheriting from this class contain persistent data. +/// When inheriting from this class, remember to call base.ExposeData() to let it handle +/// Persistent data: +/// +/// Serialized into XML using RimWorld's Scribe system +/// Save-bound: survives a server restart +/// +/// +public abstract class ExposableSession : Session, IExposable +{ + /// + protected ExposableSession(Map map) : base(map) { } + + public virtual void ExposeData() + { + Scribe_Values.Look(ref sessionId, "sessionId"); + } +} \ No newline at end of file diff --git a/Source/API/Sessions/ISessionManager.cs b/Source/API/Sessions/ISessionManager.cs new file mode 100644 index 0000000..0c9e948 --- /dev/null +++ b/Source/API/Sessions/ISessionManager.cs @@ -0,0 +1,86 @@ +using System.Collections.Generic; +using Verse; + +namespace Multiplayer.API; + +public interface ISessionManager +{ + /// + /// Returns the list of all currently active sessions for this specific . + /// + IReadOnlyList AllSessions { get; } + /// + /// Returns the list of all currently active exposable sessions for this specific . + /// + IReadOnlyList ExposableSessions { get; } + /// + /// Returns the list of all currently active semi-persistent sessions for this specific . + /// + IReadOnlyList SemiPersistentSessions { get; } + /// + /// Returns the list of all currently active ticking sessions for this specific . + /// + IReadOnlyList TickingSessions { get; } + /// + /// A convenience property for checking if any of the sessions is active. + /// + bool AnySessionActive { get; } + + /// + /// Adds a new session to the list of active sessions. + /// + /// The session to try to add to active sessions. + /// if the session was added to active ones, if there was a conflict between sessions. + bool AddSession(Session session); + + /// + /// Tries to get a conflicting session (through the use of ) or, if there was none, returns the input . + /// + /// The session to try to add to active sessions. + /// A session that was conflicting with the input one, or the input itself if there were no conflicts. It may be of a different type than the input. + Session GetOrAddSessionAnyConflict(Session session); + + /// + /// Tries to get a conflicting session (through the use of ) or, if there was none, returns the input . + /// + /// The session to try to add to active sessions. + /// A session that was conflicting with the input one if it's the same type (other is T), null if it's a different type, or the input itself if there were no conflicts. + T GetOrAddSession(T session) where T : Session; + + /// + /// Tries to remove a session from active ones. + /// + /// The session to try to remove from the active sessions. + /// if successfully removed from . Doesn't correspond to if it was successfully removed from other lists of sessions. + bool RemoveSession(Session session); + + /// + /// Returns the first active session of specific type. + /// + /// Type of the session to retrieve. + /// The first session of specified type, or if there are none. + T GetFirstOfType() where T : Session; + + /// + /// Returns the session with specific ID of specific type. + /// + /// The ID of the session to search for. + /// Type of the session to retrieve. + /// The session with provided ID and of specified type, or if there are none. + T GetFirstWithId(int id) where T : Session; + + /// + /// Returns the session with specific ID. + /// + /// The ID of the session to search for. + /// The session with provided ID, or if there are none. + Session GetFirstWithId(int id); + + /// + /// Checks if any of active sessions is currently pausing the game. + /// + /// The map at which the sessions would check if the game is paused. Global session manager accepts for global pausing. + /// if any session is active, otherwise. + /// Local session managers expect the to be the same as the map it's attached to. + bool IsAnySessionCurrentlyPausing(Map map); // Is it necessary for the API? +} \ No newline at end of file diff --git a/Source/API/Sessions/ISessionWithCreationRestrictions.cs b/Source/API/Sessions/ISessionWithCreationRestrictions.cs new file mode 100644 index 0000000..828c188 --- /dev/null +++ b/Source/API/Sessions/ISessionWithCreationRestrictions.cs @@ -0,0 +1,16 @@ +namespace Multiplayer.API; + +/// +/// Interface used by sessions that have restrictions based on other existing sessions, for example limiting them to only 1 session of specific type. +/// +public interface ISessionWithCreationRestrictions +{ + /// + /// Method used to check if the current session can be created by checking other . + /// Only sessions in the current context are checked (local map sessions or global sessions). + /// + /// The other session the current one is checked against. Can be of different type. + /// Currently only the current class checks against the existing ones - the existing classed don't check against this one. + /// if the current session should be created, otherwise + bool CanExistWith(Session other); +} \ No newline at end of file diff --git a/Source/API/Sessions/ISessionWithTransferables.cs b/Source/API/Sessions/ISessionWithTransferables.cs new file mode 100644 index 0000000..52bead0 --- /dev/null +++ b/Source/API/Sessions/ISessionWithTransferables.cs @@ -0,0 +1,25 @@ +using RimWorld; +using Verse; + +namespace Multiplayer.API; + +/// +/// Required by sessions dealing with transferables, like trading or caravan forming. By implementing this interface, Multiplayer will handle majority of syncing of changes in transferables. +/// When drawing the dialog tied to this session, you'll have to set to the proper session, and set it to null once done. +/// +/// For safety, make sure to set in and unset in . +public interface ISessionWithTransferables +{ + /// + /// Used when syncing data across players, specifically to retrieve based on the it has. + /// + /// of the . + /// which corresponds to a with specific . + Transferable GetTransferableByThingId(int thingId); + + /// + /// Called when the count in a specific was changed. + /// + /// Transferable whose count was changed. + void Notify_CountChanged(Transferable tr); +} \ No newline at end of file diff --git a/Source/API/Sessions/ITickingSession.cs b/Source/API/Sessions/ITickingSession.cs new file mode 100644 index 0000000..b891ebc --- /dev/null +++ b/Source/API/Sessions/ITickingSession.cs @@ -0,0 +1,13 @@ +namespace Multiplayer.API; + +/// +/// Used by sessions that are are required to tick together with the map/world. +/// +public interface ITickingSession +{ + /// + /// Called once per session when the map (for local sessions) or the world (for global sessions) is ticking. + /// + /// The sessions are iterated over backwards using a for loop, so it's safe for them to remove themselves from the session manager. + void Tick(); +} \ No newline at end of file diff --git a/Source/API/Sessions/PauseLockAttribute.cs b/Source/API/Sessions/PauseLockAttribute.cs new file mode 100644 index 0000000..83d1c5f --- /dev/null +++ b/Source/API/Sessions/PauseLockAttribute.cs @@ -0,0 +1,11 @@ +using System; +using Verse; + +namespace Multiplayer.API; + +/// +/// An attribute that marks a method for pause lock checking It needs a return type and a single parameter. +/// +[AttributeUsage(AttributeTargets.Method)] +public class PauseLockAttribute : Attribute +{ } \ No newline at end of file diff --git a/Source/API/Sessions/PauseLockDelegate.cs b/Source/API/Sessions/PauseLockDelegate.cs new file mode 100644 index 0000000..88a43a4 --- /dev/null +++ b/Source/API/Sessions/PauseLockDelegate.cs @@ -0,0 +1,10 @@ +using Verse; + +namespace Multiplayer.API; + +/// +/// Signature for adding new local pause locking methods +/// +/// Current map to check if it should be paused +/// if time should be paused on the specific map +public delegate bool PauseLockDelegate(Map map); \ No newline at end of file diff --git a/Source/API/Sessions/SemiPersistentSession.cs b/Source/API/Sessions/SemiPersistentSession.cs new file mode 100644 index 0000000..2ab10f9 --- /dev/null +++ b/Source/API/Sessions/SemiPersistentSession.cs @@ -0,0 +1,23 @@ +using Verse; + +namespace Multiplayer.API; + +/// +/// Sessions implementing this interface consist of semi-persistent data. +/// Semi-persistent data: +/// +/// Serialized into binary using the Sync system +/// Session-bound: survives a reload, lost when the server is closed +/// +/// +public abstract class SemiPersistentSession : Session +{ + /// + protected SemiPersistentSession(Map map) : base(map) { } + + /// + /// Writes/reads the data used by this session. + /// + /// Sync worker used for writing/reading the data. + public abstract void Sync(SyncWorker sync); +} \ No newline at end of file diff --git a/Source/API/Sessions/Session.cs b/Source/API/Sessions/Session.cs new file mode 100644 index 0000000..83bf579 --- /dev/null +++ b/Source/API/Sessions/Session.cs @@ -0,0 +1,91 @@ +using RimWorld; +using RimWorld.Planet; +using Verse; + +namespace Multiplayer.API; + +/// +/// Used by Multiplayer's session manager to allow for creation of blocking dialogs, while (in case of async time) only pausing specific maps. +/// Sessions will be reset/reloaded during reloading - to prevent it, implement or . +/// You should avoid implementing this interface directly, instead opting into inheriting for greater compatibility. +/// +public abstract class Session +{ + // Use internal to prevent mods from easily modifying it? + protected int sessionId; + // Should it be virtual? + /// + /// Used for syncing session across players by assigning them IDs, similarly to how every receives an ID. + /// Automatically applied by the session manager + /// If inheriting you don't have to worry about this property. + /// + public int SessionId + { + get => sessionId; + set => sessionId = value; + } + + /// + /// Used by the session manager while joining the game - if it returns it'll get removed. + /// + public virtual bool IsSessionValid => true; + + /// + /// Mandatory constructor for any subclass of . + /// + /// The map this session belongs to. It will be provided by session manager when syncing. + protected Session(Map map) { } + + /// + /// Called once the sessions has been added to the list of active sessions. Can be used for initialization. + /// + /// In case of , this will only be called if successfully added. + public virtual void PostAddSession() + { + } + + /// + /// Called once the sessions has been removed to the list of active sessions. Can be used for cleanup. + /// + public virtual void PostRemoveSession() + { + } + + /// + /// A convenience method to switch to a specific map or world. Intended to be used from when opening menu. + /// + /// Map to switch to or to switch to world view. + protected static void SwitchToMapOrWorld(Map map) + { + if (map == null) + { + Find.World.renderer.wantedMode = WorldRenderMode.Planet; + } + else + { + if (WorldRendererUtility.WorldRenderedNow) CameraJumper.TryHideWorld(); + Current.Game.CurrentMap = map; + } + } + + /// + /// The map this session is used by or in case of global sessions. + /// + public abstract Map Map { get; } + + /// + /// Called when checking ticking and if any session returns - it'll force pause the map/game. + /// In case of local (map) sessions, it'll only be called by the current map. In case of global (world) sessions, it'll be called by the world and each map. + /// + /// Current map (when checked from local session manager) or (when checked from local session manager). + /// If there are multiple sessions active, this method is not guaranteed to run if a session before this one returned . + /// if the session should pause the map/game, otherwise. + public abstract bool IsCurrentlyPausing(Map map); + + /// + /// Called when a session is active, and if any session returns a non-null value, a button will be displayed which will display all options. + /// + /// Currently processed colonist bar entry. Will be called once per . + /// Menu option that will be displayed when the session is active. Can be . + public abstract FloatMenuOption GetBlockingWindowOptions(ColonistBar.Entry entry); +} \ No newline at end of file diff --git a/Source/API/Sync/ISyncCall.cs b/Source/API/Sync/ISyncCall.cs new file mode 100644 index 0000000..2927378 --- /dev/null +++ b/Source/API/Sync/ISyncCall.cs @@ -0,0 +1,16 @@ +namespace Multiplayer.API; + +/// +/// ISyncCall interface. +/// +/// Used internally +public interface ISyncCall +{ + /// + /// Manually calls the synced method. + /// + /// Object currently bound to that method. Null if the method is static. + /// Parameters to call the method with. + /// if the original call should be canceled. + bool DoSync(object target, params object[] args); +} \ No newline at end of file diff --git a/Source/API/Sync/ISyncDelegate.cs b/Source/API/Sync/ISyncDelegate.cs new file mode 100644 index 0000000..c13713c --- /dev/null +++ b/Source/API/Sync/ISyncDelegate.cs @@ -0,0 +1,125 @@ +using System; + +namespace Multiplayer.API; + +/// +/// Sync delegate. +/// +/// See and to see how to use it. +public interface ISyncDelegate : ISyncCall +{ + /// + /// Instructs ISyncDelegate to cancel synchronization except for + /// + /// self + /// field names to be excluded + ISyncDelegate CancelIfAnyFieldNull(params string[] blacklist); + + /// + /// Instructs ISyncDelegate to cancel synchronization except for + /// + /// self + /// Whitelist. + ISyncDelegate CancelIfFieldsNull(params string[] whitelist); + + /// + /// Cancels if no selected objects. + /// + /// self + [Obsolete($"Use {nameof(CancelIfNoSelectedMapObjects)} instead")] + ISyncDelegate CancelIfNoSelectedObjects(); + + /// + /// Instructs SyncDelegate to cancel synchronization if no map objects were selected during call replication. + /// + /// self + ISyncDelegate CancelIfNoSelectedMapObjects(); + + /// + /// Instructs SyncDelegate to cancel synchronization if no world objects were selected during call replication. + /// + /// self + ISyncDelegate CancelIfNoSelectedWorldObjects(); + + /// + /// Use parameter's type's IExposable interface to transfer its data to other clients. + /// + /// IExposable is the interface used for saving data to the save which means it utilizes IExposable.ExposeData() method. + /// self + /// Fields to sync by using IExposable. + ISyncDelegate ExposeFields(params string[] fields); + + /// + /// Removes the nulls from lists. + /// + /// self + /// List fields. + ISyncDelegate RemoveNullsFromLists(params string[] listFields); + + /// + /// Sets the context. + /// + /// self + /// Context. + ISyncDelegate SetContext(SyncContext context); + + /// + /// Sets the debug only. + /// + /// self + ISyncDelegate SetDebugOnly(); + + /// + /// Instructs SyncDelegate to synchronize only if it's invoked by the host. + /// + /// self + ISyncDelegate SetHostOnly(); + + /// + /// Adds an Action that runs before a call is replicated on client. + /// + /// An action ran before a call is replicated on client. Called with target and value. + /// self + ISyncDelegate SetPreInvoke(Action action); + + /// + /// Adds an Action that runs after a call is replicated on client. + /// + /// An action ran after a call is replicated on client. Called with target and value. + /// self + ISyncDelegate SetPostInvoke(Action action); + + /// + /// Transforms a parameter of a method, result of which will be synced instead of the parameter itself + /// + /// Index at which parameter is going to be transformed + /// A serializer which will transform the argument before and after syncing + /// Check to ensure is the same type as the argument will be dropped. More error-prone (and only detectable at runtime), but allows transforming arguments even if the current assembly cannot reference the specific type. + /// The type which will be transformed before, and type that will be transformed back into after syncing + /// The type which will be synced to other players instead of + /// self + ISyncDelegate TransformArgument(int index, Serializer serializer, bool skipTypeCheck = false); + + /// + /// Transforms an object instance within which the synced method is declared, result of which will be synced instead of the instance itself + /// + /// A serializer which will transform the target instance before and after syncing + /// Check to ensure is the same type as the target instance will be dropped. More error-prone (and only detectable at runtime), but allows transforming target instance even if the current assembly cannot reference the specific type. + /// The type which will be transformed before, and type that will be transformed back into after syncing + /// The type which will be synced to other players instead of + /// self + ISyncDelegate TransformTarget(Serializer serializer, bool skipTypeCheck = false); + + /// + /// Transforms a field inside of the delegate, result of which will be synced instead of the field itself + /// + /// Name of a field which will be transformed before and after syncing. Supports fields inside of fields referencing other delegates, for example: `firstDelegate/anotherDelegate/targetField` + /// A serializer which will transform the field before and after syncing + /// Check to ensure is the same type as the field will be dropped. More error-prone (and only detectable at runtime), but allows transforming fields even if the current assembly cannot reference the specific type. + /// The type which will be transformed before, and type that will be transformed back into after syncing + /// The type which will be synced to other players instead of + /// + ISyncDelegate TransformField(string field, Serializer serializer, bool skipTypeCheck = false); + + string ToString(); +} \ No newline at end of file diff --git a/Source/API/Sync/ISyncField.cs b/Source/API/Sync/ISyncField.cs new file mode 100644 index 0000000..25fd320 --- /dev/null +++ b/Source/API/Sync/ISyncField.cs @@ -0,0 +1,93 @@ +using System; + +namespace Multiplayer.API; + +/// +/// SyncField interface. +/// +/// +/// Creates and registers a SyncField that points to myField in object of type MyType and enables its change buffer. +/// +/// MPApi.SyncField(typeof(MyType), "myField").SetBufferChanges(); +/// +/// Creates and registers a SyncField that points to myField which resides in MyStaticClass. +/// +/// MPApi.SyncField(null, "MyAssemblyNamespace.MyStaticClass.myField"); +/// +/// Creates and registers a SyncField that points to myField that resides in an object stored by myEnumberable defined in an object of type MyType. +/// To watch this one you have to supply an index in . +/// +/// MPApi.SyncField(typeof(MyType), "myEnumerable/[]/myField"); +/// +/// +public interface ISyncField +{ + /// + /// Instructs SyncField to cancel synchronization if the value of the member it's pointing at is null. + /// + /// self + ISyncField CancelIfValueNull(); + + /// + /// Instructs SyncField to sync in game loop. + /// + /// self + ISyncField InGameLoop(); + + /// + /// Adds an Action that runs after a field is synchronized. + /// + /// An action ran after a field is synchronized. Called with target and value. + /// self + ISyncField PostApply(Action action); + + /// + /// Adds an Action that runs before a field is synchronized. + /// + /// An action ran before a field is synchronized. Called with target and value. + /// self + ISyncField PreApply(Action action); + + /// + /// Instructs SyncField to use a buffer instead of syncing instantly (when is called). + /// + /// self + ISyncField SetBufferChanges(); + + /// + /// Instructs SyncField to synchronize only in debug mode. + /// + /// self + ISyncField SetDebugOnly(); + + /// + /// Instructs SyncField to synchronize only if it's invoked by the host. + /// + /// self + ISyncField SetHostOnly(); + + /// + /// + /// + /// self + ISyncField SetVersion(int version); + + /// + /// + /// + /// An object of type set in the . Set to null if you're watching a static field. + /// Index in the field path set in . + /// self + void Watch(object target = null, object index = null); + + /// + /// Manually syncs a field. + /// + /// An object of type set in the . Set to null if you're watching a static field. + /// Value to apply to the synced field. + /// Index in the field path set in + /// if the change should be canceled. + bool DoSync(object target, object value, object index = null); + + string ToString(); +} \ No newline at end of file diff --git a/Source/API/Sync/ISyncMethod.cs b/Source/API/Sync/ISyncMethod.cs new file mode 100644 index 0000000..97397db --- /dev/null +++ b/Source/API/Sync/ISyncMethod.cs @@ -0,0 +1,107 @@ +using System; + +namespace Multiplayer.API; + +/// +/// SyncMethod interface. +/// +/// See , and to see how to use it. +public interface ISyncMethod : ISyncCall +{ + /// + /// Instructs SyncMethod to cancel synchronization if any arg is null. + /// + /// self + ISyncMethod CancelIfAnyArgNull(); + + /// + /// Instructs SyncMethod to cancel synchronization if no map objects were selected during call replication. + /// + /// self + ISyncMethod CancelIfNoSelectedMapObjects(); + + /// + /// Instructs SyncMethod to cancel synchronization if no world objects were selected during call replication. + /// + /// self + ISyncMethod CancelIfNoSelectedWorldObjects(); + + /// + /// Use parameter's type's IExposable interface to transfer its data to other clients. + /// + /// IExposable is the interface used for saving data to the save which means it utilizes IExposable.ExposeData() method. + /// Index at which parameter is to be marked to expose + /// self + ISyncMethod ExposeParameter(int index); + + /// + /// Currently unused in the Multiplayer mod. + /// + /// Milliseconds between resends + /// self + ISyncMethod MinTime(int time); + + /// + /// Instructs method to send context along with the call. + /// + /// Context is restored after method is called. + /// One or more context flags + /// self + ISyncMethod SetContext(SyncContext context); + + /// + /// Instructs SyncMethod to synchronize only in debug mode. + /// + /// self + ISyncMethod SetDebugOnly(); + + /// + /// Instructs SyncMethod to synchronize only if it's invoked by the host. + /// + /// self + ISyncMethod SetHostOnly(); + + /// + /// Adds an Action that runs before a call is replicated on client. + /// + /// An action ran before a call is replicated on client. Called with target and value. + /// self + ISyncMethod SetPreInvoke(Action action); + + /// + /// Adds an Action that runs after a call is replicated on client. + /// + /// An action ran after a call is replicated on client. Called with target and value. + /// self + ISyncMethod SetPostInvoke(Action action); + + /// + /// + /// + /// Handler version + /// self + ISyncMethod SetVersion(int version); + + /// + /// Performs a transformation on a parameter of the synced method, the result of which will be synced instead of the parameter itself + /// + /// Index at which parameter is going to be transformed + /// A serializer which will transform the argument before and after syncing + /// Check to ensure is the same type as the argument will be dropped. More error-prone (and only detectable at runtime), but allows transforming arguments even if the current assembly cannot reference the specific type. + /// The type which will be transformed before, and type that will be transformed back into after syncing + /// The type which will be synced to other players instead of + /// self + ISyncMethod TransformArgument(int index, Serializer serializer, bool skipTypeCheck = false); + + /// + /// Performs a transformation on the target instance of the synced method, the result of which will be synced instead of the instance itself + /// + /// A serializer which will transform the target instance before and after syncing + /// Check to ensure is the same type as the target instance will be dropped fully. More error-prone (and only detectable at runtime), but allows transforming target instance even if the current assembly cannot reference the specific type. + /// The type which will be transformed before, and type that will be transformed back into after syncing + /// The type which will be synced to other players instead of + /// self + ISyncMethod TransformTarget(Serializer serializer, bool skipTypeCheck = false); + + string ToString(); +} \ No newline at end of file diff --git a/Source/API/Sync/ISyncSimple.cs b/Source/API/Sync/ISyncSimple.cs new file mode 100644 index 0000000..9d0f10f --- /dev/null +++ b/Source/API/Sync/ISyncSimple.cs @@ -0,0 +1,8 @@ +namespace Multiplayer.API; + +/// +/// Objects implementing this marker interface sync their exact type and all declared fields. +/// This is useful when syncing type hierarchies by value. +/// The synced object is created uninitialized using reflection (no constructor is called). +/// +public interface ISyncSimple { } \ No newline at end of file diff --git a/Source/API/Sync/ISynchronizable.cs b/Source/API/Sync/ISynchronizable.cs new file mode 100644 index 0000000..4596c4c --- /dev/null +++ b/Source/API/Sync/ISynchronizable.cs @@ -0,0 +1,39 @@ +namespace Multiplayer.API; + +/// +/// An interface that allows syncing objects that inherit it. +/// +public interface ISynchronizable +{ + /// + /// An entry point that is used when object is to be read/written. + /// + /// + /// Requires a default constructor that takes no parameters. + /// Check to see how to make a syncer that allows for a manual object construction. + /// + /// A SyncWorker that will read/write data bound with Bind methods. + /// + /// A simple implementation that binds object's fields x, y, z for reading/writing. + /// + /// public void Sync(SyncWorker sync) + /// { + /// sync.Bind(ref this.x); + /// sync.Bind(ref this.y); + /// sync.Bind(ref this.z); + /// } + /// + /// + /// An implementation that sends field a, but saves it back into field b when it's received. + /// + /// public void Sync(SyncWorker sync) + /// { + /// if(sync.isWriting) + /// sync.Bind(ref this.a); + /// else + /// sync.Bind(ref this.b); + /// } + /// + /// + void Sync(SyncWorker sync); +} \ No newline at end of file diff --git a/Source/API/Sync/Serializer.cs b/Source/API/Sync/Serializer.cs new file mode 100644 index 0000000..f0a27af --- /dev/null +++ b/Source/API/Sync/Serializer.cs @@ -0,0 +1,26 @@ +using System; + +namespace Multiplayer.API; + +public record Serializer( + Func Writer, // (live, target, args) => networked + Func Reader // (networked) => live +); + +public static class Serializer +{ + public static Serializer New(Func writer, Func reader) + { + return new(writer, reader); + } + + public static Serializer New(Func writer, Func reader) + { + return new((live, _, _) => writer(live), reader); + } + + public static Serializer SimpleReader(Func reader) + { + return new((_, _, _) => null, _ => reader()); + } +} \ No newline at end of file diff --git a/Source/API/Sync/SyncContext.cs b/Source/API/Sync/SyncContext.cs new file mode 100644 index 0000000..827ce49 --- /dev/null +++ b/Source/API/Sync/SyncContext.cs @@ -0,0 +1,23 @@ +using System; + +namespace Multiplayer.API; + +/// +/// Context flags which are sent along with a command +/// +[Flags] +public enum SyncContext +{ + /// Default value. (no context) + None = 0, + /// Send mouse cell context (emulates mouse position) + MapMouseCell = 1, + /// Send map selected context (object selected on the map) + MapSelected = 2, + /// Send world selected context (object selected on the world map) + WorldSelected = 4, + /// Send order queue context (emulates pressing KeyBindingDefOf.QueueOrder) + QueueOrder_Down = 8, + /// Send current map context + CurrentMap = 16, +} \ No newline at end of file diff --git a/Source/API/Sync/SyncDialogNodeTreeAttribute.cs b/Source/API/Sync/SyncDialogNodeTreeAttribute.cs new file mode 100644 index 0000000..cd1a932 --- /dev/null +++ b/Source/API/Sync/SyncDialogNodeTreeAttribute.cs @@ -0,0 +1,23 @@ +using System; + +namespace Multiplayer.API; + +/// +/// An attribute that is used to mark methods which create for syncing. +/// Any option picked by a player will be synced between all clients automatically. +/// It can be combined with so the call will be replicated by the MPApi on all clients automatically. +/// +/// +/// An example showing how to mark a method for syncing. +/// +/// [SyncDialogNodeTree] +/// public void MyMethod(...) +/// { +/// ... +/// Find.WindowStack.Add(new Dialog_NodeTree(diaNode, faction)); +/// } +/// +/// +[AttributeUsage(AttributeTargets.Method)] +public class SyncDialogNodeTreeAttribute : Attribute +{ } \ No newline at end of file diff --git a/Source/API/Sync/SyncFieldAttribute.cs b/Source/API/Sync/SyncFieldAttribute.cs new file mode 100644 index 0000000..d3bc41c --- /dev/null +++ b/Source/API/Sync/SyncFieldAttribute.cs @@ -0,0 +1,50 @@ +using System; + +namespace Multiplayer.API; + +/// +/// An attribute that is used to mark fields for syncing. +/// It will be Watched for changes by the MPApi when instructed. +/// +/// +/// An example showing how to mark a field for syncing. +/// +/// [SyncField] +/// public class MyClass +/// { +/// [SyncField] +/// bool myField; +/// +/// ... +/// } +/// +/// +[AttributeUsage(AttributeTargets.Field)] +public class SyncFieldAttribute : Attribute +{ + public SyncContext context; + + /// Instructs SyncField to cancel synchronization if the value of the member it's pointing at is null. + public bool cancelIfValueNull = false; + + /// Instructs SyncField to sync in game loop. + public bool inGameLoop = false; + + /// Instructs SyncField to use a buffer instead of syncing instantly (when is called). + public bool bufferChanges = true; + + /// Instructs SyncField to synchronize only in debug mode. + public bool debugOnly = false; + + /// Instructs SyncField to synchronize only if it's invoked by the host. + public bool hostOnly = false; + + /// + public int version; + + /// Context + public SyncFieldAttribute(SyncContext context = SyncContext.None) + { + this.context = context; + } +} \ No newline at end of file diff --git a/Source/API/Sync/SyncMethodAttribute.cs b/Source/API/Sync/SyncMethodAttribute.cs new file mode 100644 index 0000000..2bd0772 --- /dev/null +++ b/Source/API/Sync/SyncMethodAttribute.cs @@ -0,0 +1,44 @@ +using System; + +namespace Multiplayer.API; + +/// +/// An attribute that is used to mark methods for syncing. +/// The call will be replicated by the MPApi on all clients automatically. +/// +/// +/// An example showing how to mark a method for syncing. +/// +/// [SyncMethod] +/// public void MyMethod(...) +/// { +/// ... +/// } +/// +/// +[AttributeUsage(AttributeTargets.Method)] +public class SyncMethodAttribute : Attribute +{ + public SyncContext context; + + /// Instructs SyncMethod to cancel synchronization if any arg is null (see ). + public bool cancelIfAnyArgNull = false; + + /// Instructs SyncMethod to cancel synchronization if no map objects were selected during the call (see ). + public bool cancelIfNoSelectedMapObjects = false; + + /// Instructs SyncMethod to cancel synchronization if no world objects were selected during call replication(see ). + public bool cancelIfNoSelectedWorldObjects = false; + + /// Instructs SyncMethod to synchronize only in debug mode (see ). + public bool debugOnly = false; + + /// A list of types to expose (see ) + public int[] exposeParameters; + + /// Context + public SyncMethodAttribute(SyncContext context = SyncContext.None) + { + this.context = context; + } +} \ No newline at end of file diff --git a/Source/API/Sync/SyncType.cs b/Source/API/Sync/SyncType.cs new file mode 100644 index 0000000..2fea9bf --- /dev/null +++ b/Source/API/Sync/SyncType.cs @@ -0,0 +1,28 @@ +using System; +using System.Reflection; + +namespace Multiplayer.API; + +public struct SyncType +{ + public readonly Type type; + public bool expose; + public bool contextMap; + + public SyncType(Type type) + { + this.type = type; + this.expose = false; + contextMap = false; + } + + public static implicit operator SyncType(ParameterInfo param) + { + return new SyncType(param.ParameterType) { /*expose = param.HasAttribute(), contextMap = param.HasAttribute()*/ }; + } + + public static implicit operator SyncType(Type type) + { + return new SyncType(type); + } +} \ No newline at end of file diff --git a/Source/API/Sync/SyncWorker.cs b/Source/API/Sync/SyncWorker.cs new file mode 100644 index 0000000..73ba3a6 --- /dev/null +++ b/Source/API/Sync/SyncWorker.cs @@ -0,0 +1,151 @@ +using System; + +namespace Multiplayer.API; + +/// +/// An abstract class that can be both a reader and a writer depending on implementation. +/// +/// See and for usage examples. +public abstract class SyncWorker +{ + /// if is currently writing. + public readonly bool isWriting; + + protected SyncWorker(bool isWriting) + { + this.isWriting = isWriting; + } + + public void Write(T obj, SyncType type) + { + if (isWriting) + { + Bind(ref obj, type); + } + } + + /// + /// Write the specified obj, only active during writing. + /// + /// Object to write. + /// Type to write. + public void Write(T obj) { + if (isWriting) { + Bind(ref obj); + } + } + + public T Read(SyncType type) + { + T obj = default(T); + + if (isWriting) + { + return obj; + } + + Bind(ref obj, type); + + return obj; + } + + /// + /// Read the specified Type from the memory stream, only active during reading. + /// + /// The requested Type object. Null if writing. + /// The Type to read. + public T Read() { + T obj = default(T); + + if (isWriting) { + return obj; + } + + Bind(ref obj); + + return obj; + } + + public abstract void Bind(ref T obj, SyncType type); + + /// Reads or writes a referenced by . + /// Base type that derives from. + /// type to bind + public abstract void BindType(ref Type type); + + /// Reads or writes an object referenced by . + /// object to bind + public abstract void Bind(ref byte obj); + + /// Reads or writes an object referenced by . + /// object to bind + public abstract void Bind(ref sbyte obj); + + /// Reads or writes an object referenced by . + /// object to bind + public abstract void Bind(ref short obj); + + /// Reads or writes an object referenced by . + /// object to bind + public abstract void Bind(ref ushort obj); + + /// Reads or writes an object referenced by . + /// object to bind + public abstract void Bind(ref int obj); + + /// Reads or writes an object referenced by . + /// object to bind + public abstract void Bind(ref uint obj); + + /// Reads or writes an object referenced by . + /// object to bind + public abstract void Bind(ref long obj); + + /// Reads or writes an object referenced by . + /// object to bind + public abstract void Bind(ref ulong obj); + + /// Reads or writes an object referenced by . + /// object to bind + public abstract void Bind(ref float obj); + + /// Reads or writes an object referenced by . + /// object to bind + public abstract void Bind(ref double obj); + + /// Reads or writes an object referenced by . + /// object to bind + public abstract void Bind(ref string obj); + + /// Reads or writes an object referenced by . + /// object to bind + public abstract void Bind(ref bool obj); + + /// + /// Reads or writes an object referenced by + /// + /// Can read/write types using user defined syncers, s and readers/writers implemented by the multiplayer mod. + /// type of the object to bind + /// object to bind + public abstract void Bind(ref T obj); + + /// + /// Uses reflection to bind a field or property + /// + /// + /// object where the field or property can be found + /// if null, will point at field from the global namespace + /// + /// path to the field or property + public abstract void Bind(object obj, string name); + + /// + /// Reads or writes an object inheriting interface. + /// + /// Does not create a new object. + /// object to bind + public void Bind(ref ISynchronizable obj) + { + obj.Sync(this); + } +} \ No newline at end of file diff --git a/Source/API/Sync/SyncWorkerAttribute.cs b/Source/API/Sync/SyncWorkerAttribute.cs new file mode 100644 index 0000000..7f822b2 --- /dev/null +++ b/Source/API/Sync/SyncWorkerAttribute.cs @@ -0,0 +1,48 @@ +using System; + +namespace Multiplayer.API; + +/// +/// An attribute that marks a method as a SyncWorker for a type specified in its second parameter. +/// +/// +/// Method with this attribute has to be static. +/// +/// +/// An implementation that manually constructs an object. +/// +/// [SyncWorkerAttribute] +/// public static void MySyncWorker(SyncWorker sync, ref MyClass inst) +/// { +/// if(!sync.isWriting) +/// inst = new MyClass("hello"); +/// +/// sync.bind(ref inst.myField); +/// } +/// +/// An implementation that instead of creating a new object, references its existing one which resides in MyThingComp that inherits ThingComp class. +/// Subclasses of ThingComp are sent as a reference by the multiplayer mod itself. +/// +/// [SyncWorkerAttribute] +/// public static void MySyncWorker(SyncWorker sync, ref MyClass inst) +/// { +/// if(!sync.isWriting) +/// MyThingComp parent = null; +/// sync.Bind(ref parent); // Receive its parent +/// inst = new MyClass(parent); +/// else +/// sync.Bind(ref inst.parent); // Send its parent +/// +/// sync.bind(ref inst.myField); +/// } +/// +/// +[AttributeUsage(AttributeTargets.Method)] +public class SyncWorkerAttribute : Attribute +{ + /// Decides if the type specified in the second parameter should also be used as a syncer for all of its subclasses. + public bool isImplicit = false; + + /// Decides if the method should get an already constructed object in case of reading data. + public bool shouldConstruct = false; +} \ No newline at end of file diff --git a/Source/API/Sync/SyncWorkerDelegate.cs b/Source/API/Sync/SyncWorkerDelegate.cs new file mode 100644 index 0000000..24581d8 --- /dev/null +++ b/Source/API/Sync/SyncWorkerDelegate.cs @@ -0,0 +1,8 @@ +namespace Multiplayer.API; + +/// +/// SyncWorker signature for adding new Types. +/// +/// Target Type +/// for usage examples. +public delegate void SyncWorkerDelegate(SyncWorker sync, ref T obj); \ No newline at end of file diff --git a/Source/API/Sync/ThingFilterContext.cs b/Source/API/Sync/ThingFilterContext.cs new file mode 100644 index 0000000..79c522f --- /dev/null +++ b/Source/API/Sync/ThingFilterContext.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using Verse; + +namespace Multiplayer.API; + +/// +/// A ThingFilter context provides information for syncing ThingFilter interactions. +/// Inheriting objects should store the ThingFilter's owner in a record property. +/// The type exists because vanilla ThingFilters don't store references to their owners. +/// +public abstract record ThingFilterContext : ISyncSimple +{ + public abstract ThingFilter Filter { get; } + public abstract ThingFilter ParentFilter { get; } + public virtual IEnumerable HiddenFilters => null; +} \ No newline at end of file From 2840872473baf7654fa05fe7a461ad71dae78722 Mon Sep 17 00:00:00 2001 From: Reznal Date: Mon, 14 Jul 2025 03:35:40 +1000 Subject: [PATCH 7/8] Updated for 1.6 (#12) * - Updated to 0.6 - Renamed WorldRenderedNow To WorldRendered * Changed WorldRendered to WorldSelecte * Updated Krafs.Rimworld --- Source/API/MP.cs | 2 +- Source/API/Sessions/Session.cs | 2 +- Source/MultiplayerAPI.csproj | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Source/API/MP.cs b/Source/API/MP.cs index 4c457ac..3bfd23f 100644 --- a/Source/API/MP.cs +++ b/Source/API/MP.cs @@ -15,7 +15,7 @@ namespace Multiplayer.API; public static class MP { /// Contains the API version - public const string API = "0.5"; + public const string API = "0.6"; /// /// Returns if API is initialized. diff --git a/Source/API/Sessions/Session.cs b/Source/API/Sessions/Session.cs index 83bf579..c592fe6 100644 --- a/Source/API/Sessions/Session.cs +++ b/Source/API/Sessions/Session.cs @@ -63,7 +63,7 @@ protected static void SwitchToMapOrWorld(Map map) } else { - if (WorldRendererUtility.WorldRenderedNow) CameraJumper.TryHideWorld(); + if (WorldRendererUtility.WorldSelected) CameraJumper.TryHideWorld(); Current.Game.CurrentMap = map; } } diff --git a/Source/MultiplayerAPI.csproj b/Source/MultiplayerAPI.csproj index 35468e8..404fc0c 100644 --- a/Source/MultiplayerAPI.csproj +++ b/Source/MultiplayerAPI.csproj @@ -2,7 +2,7 @@ RimWorld.MultiplayerAPI - 0.5 + 0.6 notfood https://i.imgur.com/amy7QJE.png notfood @@ -24,12 +24,12 @@ false false None - 0.5 + 0.6 10 - + From c9a886092b4d24e57d8a40e40f68d9f845ca24c5 Mon Sep 17 00:00:00 2001 From: notfood Date: Mon, 14 Jul 2025 12:44:18 -0500 Subject: [PATCH 8/8] Address deprecation --- Source/MultiplayerAPI.csproj | 11 +++++++++-- icon.png | Bin 0 -> 4520 bytes 2 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 icon.png diff --git a/Source/MultiplayerAPI.csproj b/Source/MultiplayerAPI.csproj index 404fc0c..6d02f0c 100644 --- a/Source/MultiplayerAPI.csproj +++ b/Source/MultiplayerAPI.csproj @@ -4,11 +4,12 @@ RimWorld.MultiplayerAPI 0.6 notfood - https://i.imgur.com/amy7QJE.png notfood https://github.com/rwmt/MultiplayerAPI https://github.com/rwmt/MultiplayerAPI/wiki/ - https://github.com/rwmt/MultiplayerAPI/blob/master/LICENSE + icon.png + LICENSE + README.md Multiplayer API for RimWorld rimworld mod RimWorld-MultiplayerAPI @@ -36,4 +37,10 @@ + + + + + + diff --git a/icon.png b/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..60d447e13d9162efb19b0d7c0a4e178cac86afd8 GIT binary patch literal 4520 zcmV;Z5m)YsP)rH%^NVZ`ZLc9tW{%Q!P@ zeoTMNYG=)wnRb43+K%l^)!MFET4kzMG>Rgk$W1OPkZ_Sp$mQf@&-3hW@0Od5l9Yuf8YDOm*;)IZ*PD=0tqCLKmrLQkU#Qov6`&$F1mdq z);tl$;$=YfD6?qi3^u91+cq$2EHLL*V8biLIChkOacSrtjw3KM^n934K>-nezn-g9 ze7=*`SBHll58?U+Opj!kp^%BlNrg&q_``}S;Hufc$zvthyQ7}UQF5hrTsnIE9G_2t z0SVy-*w#u;dLe4YR%7L37OtDi`nQNnu#DBE{P+C1MMaFH_0)dO&Zj_tsKBh@xRr}sftb^T+Obu5_$dolTxS-Q7@t##KBLjo!>G@&fB7}Q zp#x#;-9C!fRoEHEp%{JwXQ!cqZ9WAB#9#b8&iq@n#964Rt-!-iTbO<=zh(b%d`?&N z?}ktbe=?J08!dC!C)KpZdT-u`IF2Pz!eE*N~{csFa56%vePXz!dtODx7W9w5`75O?ofio;um7>&kF zP=d9vWn-Ds-Zq{rXd>@5m7id8z#=DP--?c|OR(VfVl2JCK=WZ$zxnMlr=y((ja9{a zeaTMXCg?{)h@cnfpp8FK8 zIz3C2ft6s{gCQ(h%5*!zqqX^*PYmc_IdbeM@VynB&*w_;FLV5W;}ee0JOj>)hp5qZ zC0EEefF)e#7<~7c2<9xZTuv`VH=ZkTj++ZN!>SAmZ|AQbH()HN2PuA!LhC*p?F zV%C8rLrh;vqc3Wk`Gg1voxlzm@ULlt^$F+HdQb)ArJjczfXQULl}qv)n)VBb!l zg?cC}S2!_AIA2C0u(fW5#NiAaSI@C%2~4;su}cfQw}F7Lh(1ixQ(9(< zy3#y>;;-AURmv+YTz0i+v5no^M-r4tFfEoN>44G~APNE6-kixB3XC}zRa=Qi$mwTX zYd+!Rd52TpcjzF)(frFnjrZcw| zMN9~*7@o)Q<%v&=FLP^^5Nj1}4XcH(+|tU?ffUI;6(B0GsyS|h**BO)cPz*1$AD>9 zg7E<)*{pVpA}bg!SYX{zhI{`Um^8USoy%`G_+W6=_%nhr1q+rGyB4YNpatg}4CM@Y!i&$EoFNYx#bWq7q`Cd>(%#5m@udlQ zyLTvz@LIJj=&B0?io#`sa@!b-o>GSPQ?h97C+`9riq>GZ`S7+B7QVZeHbXYk2jz(DGFdzJ9Nb(Y03I!H|MG z+F43%2L9{cSz7&%iYx=h!%rjoF74HZ9PA`hJMfwD7BRQ-O4A0(XYuHRQrCdcr88pI zq1KSI|1&?QsCB>J``Qag1;hj_>p}kGJ4tKnu=eQ?W-kmm6_s}0LzEB34l3(63{$^n zjd7T{Zw$lK+P^_gyR-sj8}GA!pZT-CKYT=CKv7<-uC<(*H`=^+5%z4?W|z)|sVS*l z$e0g^wm5Z2>{}Q;ZW8{2HO7qVEMCBJdp3ESr)R~T9j3KsauL3h!13^8sOC#@C>u zF>9YL!^}DH3s`xYUw60ZOBJ&|c;^k$TIP&RZy5#66Rj;kmv#$jbPK~`D@T-bX}rW( zad*@JlMjz^5{yHh|Hexpe0f+ogd83F!U-Q6DwlC5B^Qo*G}$*8O|I>PX90ashYp7t3{Bfugz8kb(aFkAY_%G2P>z|0|e3 zjR#MiZj_b{DDn&yrMtFjL87=Fh{v7-CRAC!;Ceqz1VkThp;hX{H~FBuac{vCiRxG` zg_?_&mEE%wYF{Wk{Ek4p@**SVE>om^=dbA*v%Tc47JejuU1NW_;i7n6QSa-?9|Ax8 zJI?C`;LTS|jyp-?y<(ODt|yiqC`S3La>2yq)*bq$dRi>BoE<(;XFiT_(2HljY06a9 ztz6IxTc#of>|j)U`8flK8h--g5!M)PqtXMz%5(MH>m*kEtCF~iwl(XFNBr)214^{h z0CVLm)11sMdQL*v0M3qM$r5klQ>5!>!9kI z1TbL07)TL`sTgLwRU*S_e%;hZVq#wCGtNzMR;2(Y8wDDy?o%~r@r72eZ4XLvM zlYBk?H&Z6Y%%7l=Yl{Z=oKZMB9vE#WO|hjK zPnAymit3Au)|a`5$Vvo;D5_(}S*i|*bRzW&&Hb9h44eD4-T53+sFKz1((@50M-Q$iKW}+5^8itSEZ}uMiSz6w2 zr&ymnk)y70KE?0SsxcT!(xnw)5imNS=Qcp1-gZHD$s`Nf8#AW?qY0-!1cqp$LX=wS zI#7^I#KiKsU78E=yR@o5l8JU5=PovFdKH3hNTY#8N=)c>l0}hx0V2_2+ngsXyqVIiwH;)kP#Nx;+@=Fpq%tzWGbwdc#cDBvJ| zCQPi%b~4&Q2xm1}6;4(XB}|$U2TRtXUeN0=8*39FnaXJeWnYT6;$O1uAZyR$CVIoT ziPV@^bBH;*k6xT*o57!bF;m!TJ03TV9Cv9iOhA%_ zrn7m8l>|(k_tI&4)j&D!(guL^=e(HPg@&mSVqS+D2@e?Mpi5h5Kn8iykcRmA%=Vt@ z4xMpAG0QG(t|Xm+DSC2hVC^Ok;1o#lmU*6E(v38lU3Plc%t6;IkhIJo9YAah-0NiX zYP!9{DTuwLMc>lNA$r%~KG#b`3o9xOw@+7845~U;XLNumWpZHBXs_rflL}ouOA_wq z+cGMKS>C#jAdO~9I)Lb*_bt%*)2G%P-jY)Ep6&H5y{|6qiDO4Ojw3x{f`&OamKL~l~g)IRf0U&_{fCPXf z6)`&hY0h&=lAX~!F96B%OwNFVa^3+F!nqkD3rg1uxrM7&rmB}S*?reXCSBuWF2|mI#g}CwC$cT6$F?<0K)