Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions Multibonk/Game/GameEvents.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ internal static class GameEvents
public static event Action<Animator, string, int> SetIntEvent;
public static event Action<Animator, string> SetTriggerEvent;

public static event Action<BaseInteractable> BaseInteractableSpawnedEvent;
public static event Action<int> BaseInteractableDestroyedEvent;


public static void TriggerSetBool(Animator animator, string param, bool value) => SetBoolEvent?.Invoke(animator, param, value);
public static void TriggerSetFloat(Animator animator, string param, float value) => SetFloatEvent?.Invoke(animator, param, value);
Expand Down Expand Up @@ -79,5 +82,15 @@ public static void TriggerPlayerRotated(Quaternion newRotation)
{
PlayerRotateEvent?.Invoke(newRotation);
}

public static void TriggerBaseInteractableSpawned(BaseInteractable interactable)
{
BaseInteractableSpawnedEvent?.Invoke(interactable);
}

public static void TriggerBaseInteractableDestroyed(int instanceId)
{
BaseInteractableDestroyedEvent?.Invoke(instanceId);
}
}
}
89 changes: 88 additions & 1 deletion Multibonk/Game/GameFunctions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,94 @@ public static void SpawnNetworkPlayer(ushort playerId, ECharacter character, Vec

GamePatchFlags.PlayersCache.Add(playerId, new SpawnedNetworkPlayer(player));
}

/// <summary>
/// Spawns a BaseInteractable in the scene from network data
/// </summary>
/// <param name="instanceId">Instance ID of the interactable</param>
/// <param name="prefabName">Name of the prefab</param>
/// <param name="position">World position</param>
/// <param name="rotation">Rotation</param>
/// <param name="scale">Scale</param>
/// <param name="isItemSource">Whether it is an item source</param>
/// <param name="showOutline">Whether to show outline</param>
public static void SpawnNetworkInteractable(
int instanceId,
string prefabName,
Vector3 position,
Quaternion rotation,
Vector3 scale,
bool isItemSource,
bool showOutline)
{
try
{
// Try to find the original prefab in the scene
var originalObject = GameObject.Find(prefabName);
if (originalObject == null)
{
MelonLogger.Warning($"Could not find prefab {prefabName} in scene");
return;
}

// Instantiate a copy of the object
var obj = UnityEngine.Object.Instantiate(originalObject, position, rotation);
obj.name = $"{prefabName}_Network_{instanceId}";
obj.transform.localScale = scale;

// Get the BaseInteractable component
var interactable = obj.GetComponent<BaseInteractable>();
if (interactable != null)
{
interactable.isItemSource = isItemSource;
interactable.showOutline = showOutline;

// Register in cache using the instanceId received from network
GamePatchFlags.TrackInteractable(instanceId, interactable);

MelonLogger.Msg($"Spawned network interactable: {prefabName} (ID: {instanceId})");
}
else
{
MelonLogger.Warning($"Object {prefabName} does not have BaseInteractable component");
UnityEngine.Object.Destroy(obj);
}
}
catch (Exception ex)
{
MelonLogger.Error($"Error spawning interactable {prefabName}: {ex.Message}");
}
}

/// <summary>
/// Destroys a network-synchronized BaseInteractable
/// </summary>
/// <param name="instanceId">Instance ID of the interactable</param>
public static void DestroyNetworkInteractable(int instanceId)
{
try
{
var interactable = GamePatchFlags.GetTrackedInteractable(instanceId);
if (interactable != null && !interactable.IsNullOrDestroyed())
{
MelonLogger.Msg($"Destroying network interactable (ID: {instanceId})");

// Remove from cache before destroying to avoid loops
GamePatchFlags.UntrackInteractable(instanceId);

// Destroy the GameObject
UnityEngine.Object.Destroy(interactable.gameObject);
}
else
{
MelonLogger.Warning($"Attempted to destroy non-existent interactable (ID: {instanceId})");
}
}
catch (Exception ex)
{
MelonLogger.Error($"Error destroying interactable {instanceId}: {ex.Message}");
}
}
}


Expand Down Expand Up @@ -125,4 +213,3 @@ public void Rotate(Vector3 rotation)
}
}
}

32 changes: 30 additions & 2 deletions Multibonk/Game/GamePatchFlags.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ public static class GamePatchFlags
/// </summary>
public static List<GameObject> MapDataIndexedPrefabs
{
get {
get
{
if (SelectedMapData == null)
return new List<GameObject>();

Expand Down Expand Up @@ -55,7 +56,11 @@ public static List<GameObject> MapDataIndexedPrefabs
}
}

public static Dictionary<ushort, SpawnedNetworkPlayer> PlayersCache = new Dictionary<ushort, SpawnedNetworkPlayer>();
public static Dictionary<ushort, SpawnedNetworkPlayer> PlayersCache = new Dictionary<ushort, SpawnedNetworkPlayer>();

// Tracks BaseInteractables synchronized over the network
// Key: Unity InstanceID, Value: BaseInteractable
public static Dictionary<int, BaseInteractable> InteractablesCache = new Dictionary<int, BaseInteractable>();

public static int Seed { get; set; } = _rng.Next(int.MinValue, int.MaxValue);

Expand All @@ -64,5 +69,28 @@ public static List<GameObject> MapDataIndexedPrefabs

public static Vector3 LastPlayerPosition { get; set; }
public static Quaternion LastPlayerRotation { get; set; }

public static bool IsInteractableTracked(int instanceId)
{
return InteractablesCache.ContainsKey(instanceId);
}

public static void TrackInteractable(int instanceId, BaseInteractable interactable)
{
if (!InteractablesCache.ContainsKey(instanceId))
{
InteractablesCache.Add(instanceId, interactable);
}
}

public static void UntrackInteractable(int instanceId)
{
InteractablesCache.Remove(instanceId);
}

public static BaseInteractable GetTrackedInteractable(int instanceId)
{
return InteractablesCache.TryGetValue(instanceId, out var interactable) ? interactable : null;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
using Il2Cpp;
using Multibonk.Networking.Comms.Base.Packet;
using Multibonk.Networking.Comms.Multibonk.Networking.Comms;
using Multibonk.Networking.Lobby;

namespace Multibonk.Game.Handlers.NetworkNotify
{
/// <summary>
/// Handler responsible for processing BaseInteractable events and sending them over the network
/// </summary>
public class BaseInteractableEventHandler : GameEventHandler
{
private readonly NetworkService _network;
private readonly LobbyContext _lobbyContext;

public BaseInteractableEventHandler(
NetworkService network,
LobbyContext lobbyContext
)
{
_network = network;
_lobbyContext = lobbyContext;

// BaseInteractable spawn event (server/host only)
GameEvents.BaseInteractableSpawnedEvent += (interactable) =>
{
if (LobbyPatchFlags.IsHosting)
{
int instanceId = interactable.GetInstanceID();

// Register the interactable in cache
GamePatchFlags.TrackInteractable(instanceId, interactable);

// Prepare data to send
string prefabName = interactable.gameObject.name.Replace("(Clone)", "").Trim();
var position = interactable.transform.position;
var rotation = interactable.transform.rotation;
var scale = interactable.transform.localScale;

// Create the packet
var spawnPacket = new SendSpawnInteractablePacket(
instanceId,
prefabName,
position,
rotation,
scale,
interactable.isItemSource,
interactable.showOutline
);

// Send to all clients
_lobbyContext.GetPlayers().ForEach(player =>
{
if (player.Connection != null)
{
player.Connection.EnqueuePacket(spawnPacket);
}
});
}
};

// BaseInteractable destruction event
GameEvents.BaseInteractableDestroyedEvent += (instanceId) =>
{
if (LobbyPatchFlags.IsHosting)
{
// Remove from cache
GamePatchFlags.UntrackInteractable(instanceId);

// Create destruction packet
var destroyPacket = new SendDestroyInteractablePacket(instanceId);

// Send to all clients
_lobbyContext.GetPlayers().ForEach(player =>
{
if (player.Connection != null)
{
player.Connection.EnqueuePacket(destroyPacket);
}
});
}
else
{
// Client notifies the server
var destroyPacket = new SendClientDestroyInteractablePacket(instanceId);
_network.GetClientService().Enqueue(destroyPacket);
}
};
}
}
}

31 changes: 30 additions & 1 deletion Multibonk/Game/Patches/MainMenuPatches.cs
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,35 @@ static void Prefix(Animator __instance, string __0)
}
}

[HarmonyPatch(typeof(BaseInteractable), "Start")]
class BaseInteractableStartPatch
{
static void Postfix(BaseInteractable __instance)
{
// Only the host should propagate interactable creation
if (LobbyPatchFlags.IsHosting)
{
GameEvents.TriggerBaseInteractableSpawned(__instance);
}
}
}

[HarmonyPatch(typeof(BaseInteractable), "OnDestroy")]
class BaseInteractableDestroyPatch
{
static void Prefix(BaseInteractable __instance)
{
// Anyone can propagate destruction (host or client)
int instanceId = __instance.GetInstanceID();

// Check if this interactable is being tracked
if (GamePatchFlags.IsInteractableTracked(instanceId))
{
GameEvents.TriggerBaseInteractableDestroyed(instanceId);
}
}
}


//[HarmonyPatch(typeof(MapGenerationController), "Awake")]
//class GeneratorAll
Expand All @@ -283,7 +312,7 @@ class GenerateHookPatch2
{
static bool Prefix(RandomObjectPlacer __instance)
{
if(LobbyPatchFlags.IsHosting)
if (LobbyPatchFlags.IsHosting)
return true;

MelonLogger.Msg("Generating interactables called");
Expand Down
6 changes: 5 additions & 1 deletion Multibonk/Multibonk.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public class MultibonkMod : MelonMod

public override void OnGUI()
{
if(manager != null)
if (manager != null)
manager.OnGUI();

}
Expand Down Expand Up @@ -53,6 +53,7 @@ public override void OnInitializeMelon()
services.AddSingleton<IGameEventHandler, PlayerMovementEventHandler>();
services.AddSingleton<IGameEventHandler, StartGameEventHandler>();
services.AddSingleton<IGameEventHandler, PlayerLevelEventHandler>();
services.AddSingleton<IGameEventHandler, BaseInteractableEventHandler>();
services.AddSingleton<IGameEventHandler, GameDispatcher>();

services.AddSingleton<EventHandlerExecutor>();
Expand All @@ -64,6 +65,7 @@ public override void OnInitializeMelon()
services.AddSingleton<IServerPacketHandler, PlayerAnimatorPacketHandler>();
services.AddSingleton<IServerPacketHandler, GameLoadedPacketHandler>();
services.AddSingleton<IServerPacketHandler, PlayerPickupXpPacketHandler>();
services.AddSingleton<IServerPacketHandler, Multibonk.Networking.Comms.Server.Handlers.DestroyInteractablePacketHandler>();

services.AddSingleton<IClientPacketHandler, LobbyPlayerListPacketHandler>();
services.AddSingleton<IClientPacketHandler, PlayerSelectedCharacterPacketHandler>();
Expand All @@ -75,6 +77,8 @@ public override void OnInitializeMelon()
services.AddSingleton<IClientPacketHandler, PlayerRotatedPacketHandler>();
services.AddSingleton<IClientPacketHandler, MapFinishedLoadingPacketHandler>();
services.AddSingleton<IClientPacketHandler, MapObjectChunkPacketHandler>();
services.AddSingleton<IClientPacketHandler, SpawnInteractablePacketHandler>();
services.AddSingleton<IClientPacketHandler, Multibonk.Networking.Comms.Client.Handlers.DestroyInteractablePacketHandler>();

services.AddSingleton<ClientProtocol>();
services.AddSingleton<ServerProtocol>();
Expand Down
Loading