Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom InnerNetObjects #92

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
72 changes: 72 additions & 0 deletions Reactor.Example/ExampleInnerNetObject.cs
Original file line number Diff line number Diff line change
@@ -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<InnerNetObject> 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`.
}
}
177 changes: 177 additions & 0 deletions Reactor/Networking/Attributes/IgnoreInnerNetObjectAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
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;

/// <summary>
/// Ignores registering <see cref="InnerNetObject"/>.
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public sealed class IgnoreInnerNetObjectAttribute : Attribute
{
private static readonly HashSet<Assembly> _registeredAssemblies = new();
private static readonly List<(string AssemblyName, MemberInfo Member)> _registeredMembers = new();

/// <summary>
/// Registers all <see cref="InnerNetObject"/>s annotated with out <see cref="IgnoreInnerNetObjectAttribute"/> in the specified <paramref name="assembly"/>.
/// </summary>
/// <remarks>This is called automatically on plugin assemblies so you probably don't need to call this.</remarks>
/// <param name="assembly">The assembly to search.</param>
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<IgnoreInnerNetObjectAttribute>() != 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<GameObject>) ||
method.ReturnType == typeof(Task<InnerNetObject>)
),
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 prefab = await GetPrefabAsync(prefabMember);
if (prefab == null)
{
Warning($"Prefab for {prefabMember.DeclaringType?.FullName}.{prefabMember.Name} is null.");
continue;
}

if (prefab is InnerNetObject netObj)
AddInnerNetObject(netObj);
else if (prefab is GameObject gameObj)
AddInnerNetObject(gameObj);
}
}

private static async Task<object?> GetPrefabAsync(MemberInfo prefabMember)
{
object? prefab = null;

if (prefabMember is MethodInfo method)
{
if (method.ReturnType == typeof(Task<GameObject>) || method.ReturnType == typeof(Task<InnerNetObject>))
{
if (method.Invoke(null, null) is Task task)
{
await task.ConfigureAwait(false);
prefab = method.ReturnType == typeof(Task<GameObject>)
? ((Task<GameObject>) task).Result
: ((Task<InnerNetObject>) 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<InnerNetObject>();
if (netObj != null)
{
AddInnerNetObject(netObj);
}
}
}

internal static void Initialize()
{
IL2CPPChainloader.Instance.PluginLoad += (_, assembly, _) => Register(assembly);
IL2CPPChainloader.Instance.Finished += () => _ = LoadRegisteredAsync();
}
}
14 changes: 14 additions & 0 deletions Reactor/Networking/Attributes/InnerNetObjectPrefabAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System;
using InnerNet;
using UnityEngine;

namespace Reactor.Networking.Attributes;

/// <summary>
/// Attribute for Load prefab method for custom <see cref="InnerNetObject"/>.
/// </summary>
/// <remarks>Must be static and return either <see cref="GameObject"/> with a <see cref="InnerNetObject"/> component, or a <see cref="InnerNetObject"/> prefab.</remarks>
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Field | AttributeTargets.Property)]
public sealed class InnerNetObjectPrefabAttribute : Attribute
{
}
1 change: 1 addition & 0 deletions Reactor/ReactorPlugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ public ReactorPlugin()
RegisterCustomRpcAttribute.Initialize();
MessageConverterAttribute.Initialize();
MethodRpcAttribute.Initialize();
IgnoreInnerNetObjectAttribute.Initialize();

LocalizationManager.Register(new HardCodedLocalizationProvider());
}
Expand Down
91 changes: 91 additions & 0 deletions Reactor/Utilities/InnerNetObjectManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
using System.Linq;
using InnerNet;

namespace Reactor.Utilities;

/// <summary>
/// Provides a standard way of managing <see cref="InnerNetObject"/>s.
/// </summary>
public static class InnerNetObjectManager
{
/// <summary>
/// Retrieves the prefab for a custom <see cref="InnerNetObject"/> of the specified type <typeparamref name="T"/>.
/// </summary>
/// <typeparam name="T">The type of the <see cref="InnerNetObject"/> prefab to retrieve. Must inherit from <see cref="InnerNetObject"/>.</typeparam>
/// <returns>The prefab instance of type <typeparamref name="T"/>.</returns>
/// <remarks>
/// 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.
/// </remarks>
public static InnerNetObject? GetNetObjPrefab<T>() where T : InnerNetObject
{
var prefab = AmongUsClient.Instance.NonAddressableSpawnableObjects
.FirstOrDefault(obj => obj.TryCast<T>() != null);

return prefab;
}

/// <summary>
/// Retrieves the spwan id from the <see cref="InnerNetObject"/> prefab of the specified type <typeparamref name="T"/>.
/// </summary>
/// <typeparam name="T">The type of the <see cref="InnerNetObject"/> prefab to retrieve. Must inherit from <see cref="InnerNetObject"/>.</typeparam>
/// <returns>The spawn id of type <typeparamref name="T"/>, if prefab not found then 0.</returns>
/// <remarks>
/// This method searches through the AmongUsClient.Instance.NonAddressableSpawnableObjects array
/// to find the spwan id of a <see cref="InnerNetObject"/> prefab.
/// </remarks>
public static uint GetSpawnId<T>() where T : InnerNetObject
{
var prefab = GetNetObjPrefab<T>();
if (prefab != null)
{
return prefab.SpawnId;
}

return 0;
}

/// <summary>
/// Spawns a new <see cref="InnerNetObject"/> locally and on the network of type <typeparamref name="T"/>.
/// </summary>
/// <typeparam name="T">The type of the <see cref="InnerNetObject"/> to spawn. Must inherit from <see cref="InnerNetObject"/>.</typeparam>
/// <param name="ownerId">The owner ID for the spawned object. Defaults to -2, which typically means no specific owner.</param>
/// <param name="spawnFlags">The spawn flags to use when spawning the object. Defaults to <see cref="SpawnFlags.None"/>.</param>
/// <returns>The newly spawned <see cref="InnerNetObject"/>, if prefab is null then it will return null.</returns>
public static InnerNetObject? SpawnNewNetObject<T>(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<T>();

if (netObj == null)
{
return null;
}

var netObjSpawn = UnityEngine.Object.Instantiate(netObj);
AmongUsClient.Instance.Spawn(netObjSpawn, ownerId, spawnFlags);
return netObjSpawn;
}

/// <summary>
/// Spawns an existing <see cref="InnerNetObject"/> instance on the network.
/// </summary>
/// <param name="netObj">The <see cref="InnerNetObject"/> instance to spawn.</param>
/// <param name="ownerId">The owner ID for the spawned object. Defaults to -2, which typically means no specific owner.</param>
/// <param name="spawnFlags">The spawn flags to use when spawning the object. Defaults to <see cref="SpawnFlags.None"/>.</param>
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);
}
}