diff --git a/Reactor.Example/ExampleInnerNetObject.cs b/Reactor.Example/ExampleInnerNetObject.cs new file mode 100644 index 0000000..f09b11b --- /dev/null +++ b/Reactor.Example/ExampleInnerNetObject.cs @@ -0,0 +1,72 @@ +using System; +using System.Threading.Tasks; +using Hazel; +using InnerNet; +using Reactor.Networking.Attributes; + +namespace Reactor.Example; + +// The `IgnoreInnerNetObject` attribute is used to prevent this custom InnerNetObject +// from being automatically registered by Reactor. +[IgnoreInnerNetObject] +public class ExampleInnerNetObject : InnerNetObject +{ + // The `InnerNetObjectPrefab` attribute is used to define how the prefab for this + // custom InnerNetObject is retrieved. The prefab is a template object that is used + // when spawning instances of this object in the game. + // The return type can either be a InnerNetObject, or GameObject but must have a InnerNetObject component attached. + // There are three examples provided for retrieving the prefab: + + // Example 1: Directly assign a prefab + // This is the simplest method, where the prefab is assigned directly to a static field or property. + [InnerNetObjectPrefab] + public static InnerNetObject? PrefabField; + // or + [InnerNetObjectPrefab] + public static InnerNetObject? PrefabProperty { get; set; } + + // Example 2: Retrieve the prefab via a static method + // This method allows for more complex logic to retrieve the prefab. + // The method must be static and return an InnerNetObject (or a GameObject with an InnerNetObject component). + [InnerNetObjectPrefab] + public static InnerNetObject GetPrefab() + { + throw new NotImplementedException($"GetPrefab prefab retrieval not implemented!"); + } + + // Example 3: Retrieve the prefab asynchronously + // This method is similar to Example 2 but allows for asynchronous operations, + // such as loading assets from disk. + [InnerNetObjectPrefab] + public static async Task GetPrefabAsync() + { + throw new NotImplementedException($"GetPrefab prefab retrieval not implemented!"); + } + + // The `HandleRpc` method is required abstract to handle Remote Procedure Calls (RPCs) for this object. + // RPCs are used to communicate between clients and the server. + // The `callId` parameter identifies the type of RPC, and the `reader` parameter provides the data. + public override void HandleRpc(byte callId, MessageReader reader) + { + // Implement logic to handle specific RPCs based on the `callId`. + // For example, you might switch on `callId` to handle different types of RPCs. + } + + // The `Serialize` method is required abstract to serialize the state of this object into a `MessageWriter`. + // This is used to synchronize the object's state across the network. + // The `initialState` parameter indicates whether this is the first time the object is being serialized. + public override bool Serialize(MessageWriter writer, bool initialState) + { + // Implement logic to write the object's state to the `writer`. + // Return `true` if the state was serialized successfully, otherwise `false`. + return false; + } + + // The `Deserialize` method is required abstract to deserialize the state of this object from a `MessageReader`. + // This is used to update the object's state based on data received from the network. + // The `initialState` parameter indicates whether this is the first time the object is being deserialized. + public override void Deserialize(MessageReader reader, bool initialState) + { + // Implement logic to read the object's state from the `reader`. + } +} diff --git a/Reactor/Networking/Attributes/IgnoreInnerNetObjectAttribute.cs b/Reactor/Networking/Attributes/IgnoreInnerNetObjectAttribute.cs new file mode 100644 index 0000000..05c5bbf --- /dev/null +++ b/Reactor/Networking/Attributes/IgnoreInnerNetObjectAttribute.cs @@ -0,0 +1,192 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using BepInEx.Unity.IL2CPP; +using InnerNet; +using UnityEngine; + +namespace Reactor.Networking.Attributes; + +/// +/// Ignores registering . +/// +[AttributeUsage(AttributeTargets.Class)] +public sealed class IgnoreInnerNetObjectAttribute : Attribute +{ + private static readonly HashSet _registeredPrefabs = new(); + private static readonly HashSet _registeredAssemblies = new(); + private static readonly List<(string AssemblyName, MemberInfo Member)> _registeredMembers = new(); + + /// + /// Registers all s annotated without in the specified . + /// + /// This is called automatically on plugin assemblies so you probably don't need to call this. + /// The assembly to search. + public static void Register(Assembly assembly) + { + if (_registeredAssemblies.Contains(assembly)) return; + _registeredAssemblies.Add(assembly); + + var assemblyName = assembly.GetName().Name; + + foreach (var type in assembly.GetTypes()) + { + if (!type.IsSubclassOf(typeof(InnerNetObject))) continue; + if (type.GetCustomAttribute() != null) continue; + + try + { + var members = type.GetMembers(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static); + var prefabMember = members.FirstOrDefault(IsValidPrefabMember); + + if (prefabMember != null && assemblyName != null) + { + _registeredMembers.Add((assemblyName, prefabMember)); + } + else + { + Warning($"No valid prefab member found for {type.FullName} in {assemblyName}."); + } + } + catch (Exception ex) + { + Warning($"Failed to register {type.FullName}: {ex}"); + } + } + } + + private static bool IsValidPrefabMember(MemberInfo member) + { + return member switch + { + MethodInfo method => method.IsStatic && ( + method.ReturnType == typeof(GameObject) || + method.ReturnType == typeof(InnerNetObject) || + method.ReturnType == typeof(Task) || + method.ReturnType == typeof(Task) + ), + FieldInfo field => field.IsStatic && ( + field.FieldType == typeof(GameObject) || + field.FieldType == typeof(InnerNetObject) + ), + PropertyInfo property => property.GetMethod?.IsStatic == true && ( + property.PropertyType == typeof(GameObject) || + property.PropertyType == typeof(InnerNetObject) + ), + _ => false, + }; + } + + internal static async Task LoadRegisteredAsync() + { + while (AmongUsClient.Instance == null) + await Task.Delay(1000); + + var orderedMembers = _registeredMembers + .OrderBy(x => x.AssemblyName) + .ThenBy(x => x.Member.DeclaringType?.FullName) + .ThenBy(x => x.Member.Name) + .Select(x => x.Member); + + foreach (var prefabMember in orderedMembers) + { + var prefabFullName = $"{prefabMember.DeclaringType?.FullName}.{prefabMember.Name}"; + var prefab = await GetPrefabAsync(prefabMember); + if (prefab == null) + { + Warning($"Prefab for {prefabFullName} is null."); + continue; + } + + if (!_registeredPrefabs.Contains(prefabFullName)) + { + _registeredPrefabs.Add(prefabFullName); + + if (prefab is InnerNetObject netObj) + { + AddInnerNetObject(netObj); + } + else if (prefab is GameObject gameObj) + { + AddInnerNetObject(gameObj); + } + } + else + { + Warning($"Prefab {prefabFullName} has already been registered, which shouldn't be possible but indicates there is a duplicate..."); + } + } + } + + private static async Task GetPrefabAsync(MemberInfo prefabMember) + { + object? prefab = null; + + if (prefabMember is MethodInfo method) + { + if (method.ReturnType == typeof(Task) || method.ReturnType == typeof(Task)) + { + if (method.Invoke(null, null) is Task task) + { + await task.ConfigureAwait(false); + prefab = method.ReturnType == typeof(Task) + ? ((Task) task).Result + : ((Task) task).Result; + } + } + else + { + prefab = method.Invoke(null, null); + } + } + else if (prefabMember is FieldInfo field) + { + prefab = field.GetValue(null); + } + else if (prefabMember is PropertyInfo property) + { + prefab = property.GetValue(null); + } + + return prefab; + } + + private static void AddInnerNetObject(InnerNetObject prefab) + { + var innerNetClient = AmongUsClient.Instance; + + // Setup InnerNetObject. + prefab.SpawnId = (uint) innerNetClient.SpawnableObjects.Length; + UnityEngine.Object.DontDestroyOnLoad(prefab); + + // Add InnerNetObject to NonAddressableSpawnableObjects. + var list = innerNetClient.NonAddressableSpawnableObjects.ToList(); + list.Add(prefab); + innerNetClient.NonAddressableSpawnableObjects = list.ToArray(); + + // Increase array length by one because of beginning if check in InnerNetClient.CoHandleSpawn() + var list2 = innerNetClient.SpawnableObjects.ToList(); + list2.Add(new()); + innerNetClient.SpawnableObjects = list2.ToArray(); + } + + private static void AddInnerNetObject(GameObject prefab) + { + if (prefab != null) + { + var netObj = prefab.GetComponent(); + if (netObj != null) + { + AddInnerNetObject(netObj); + } + } + } + + internal static void Initialize() + { + IL2CPPChainloader.Instance.PluginLoad += (_, assembly, _) => Register(assembly); + IL2CPPChainloader.Instance.Finished += () => _ = LoadRegisteredAsync(); + } +} diff --git a/Reactor/Networking/Attributes/InnerNetObjectPrefabAttribute.cs b/Reactor/Networking/Attributes/InnerNetObjectPrefabAttribute.cs new file mode 100644 index 0000000..efc0e25 --- /dev/null +++ b/Reactor/Networking/Attributes/InnerNetObjectPrefabAttribute.cs @@ -0,0 +1,14 @@ +using System; +using InnerNet; +using UnityEngine; + +namespace Reactor.Networking.Attributes; + +/// +/// Attribute for Load prefab method for custom . +/// +/// Must be static and return either with a component, or a prefab. +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Field | AttributeTargets.Property)] +public sealed class InnerNetObjectPrefabAttribute : Attribute +{ +} diff --git a/Reactor/ReactorPlugin.cs b/Reactor/ReactorPlugin.cs index df51844..45f93d8 100644 --- a/Reactor/ReactorPlugin.cs +++ b/Reactor/ReactorPlugin.cs @@ -51,6 +51,7 @@ public ReactorPlugin() RegisterCustomRpcAttribute.Initialize(); MessageConverterAttribute.Initialize(); MethodRpcAttribute.Initialize(); + IgnoreInnerNetObjectAttribute.Initialize(); LocalizationManager.Register(new HardCodedLocalizationProvider()); } diff --git a/Reactor/Utilities/InnerNetObjectManager.cs b/Reactor/Utilities/InnerNetObjectManager.cs new file mode 100644 index 0000000..1f86490 --- /dev/null +++ b/Reactor/Utilities/InnerNetObjectManager.cs @@ -0,0 +1,91 @@ +using System.Linq; +using InnerNet; + +namespace Reactor.Utilities; + +/// +/// Provides a standard way of managing s. +/// +public static class InnerNetObjectManager +{ + /// + /// Retrieves the prefab for a custom of the specified type . + /// + /// The type of the prefab to retrieve. Must inherit from . + /// The prefab instance of type . + /// + /// This method searches through the AmongUsClient.Instance.NonAddressableSpawnableObjects array + /// to find a prefab of the specified type. If no matching prefab is found, an exception is thrown. + /// + public static InnerNetObject? GetNetObjPrefab() where T : InnerNetObject + { + var prefab = AmongUsClient.Instance.NonAddressableSpawnableObjects + .FirstOrDefault(obj => obj.TryCast() != null); + + return prefab; + } + + /// + /// Retrieves the spwan id from the prefab of the specified type . + /// + /// The type of the prefab to retrieve. Must inherit from . + /// The spawn id of type , if prefab not found then 0. + /// + /// This method searches through the AmongUsClient.Instance.NonAddressableSpawnableObjects array + /// to find the spwan id of a prefab. + /// + public static uint GetSpawnId() where T : InnerNetObject + { + var prefab = GetNetObjPrefab(); + if (prefab != null) + { + return prefab.SpawnId; + } + + return 0; + } + + /// + /// Spawns a new locally and on the network of type . + /// + /// The type of the to spawn. Must inherit from . + /// The owner ID for the spawned object. Defaults to -2, which typically means no specific owner. + /// The spawn flags to use when spawning the object. Defaults to . + /// The newly spawned , if prefab is null then it will return null. + public static InnerNetObject? SpawnNewNetObject(int ownerId = -2, SpawnFlags spawnFlags = SpawnFlags.None) where T : InnerNetObject + { + if (!AmongUsClient.Instance.AmHost) + { + Warning("You can only spawn a InnerNetObject as Host."); + return null; + } + + var netObj = GetNetObjPrefab(); + + if (netObj == null) + { + return null; + } + + var netObjSpawn = UnityEngine.Object.Instantiate(netObj); + AmongUsClient.Instance.Spawn(netObjSpawn, ownerId, spawnFlags); + return netObjSpawn; + } + + /// + /// Spawns an existing instance on the network. + /// + /// The instance to spawn. + /// The owner ID for the spawned object. Defaults to -2, which typically means no specific owner. + /// The spawn flags to use when spawning the object. Defaults to . + public static void SpawnNetObject(this InnerNetObject netObj, int ownerId = -2, SpawnFlags spawnFlags = SpawnFlags.None) + { + if (!AmongUsClient.Instance.AmHost) + { + Warning("You can only spawn a InnerNetObject as Host."); + return; + } + + AmongUsClient.Instance.Spawn(netObj, ownerId, spawnFlags); + } +}