diff --git a/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/Components/NetworkGameCreatorCharacter.cs b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/Components/NetworkGameCreatorCharacter.cs index a7e1d1733..ee8761ed0 100644 --- a/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/Components/NetworkGameCreatorCharacter.cs +++ b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/Components/NetworkGameCreatorCharacter.cs @@ -16,7 +16,7 @@ namespace GameCreator.Multiplayer.Runtime.Components /// [RequireComponent(typeof(GameCreatorCharacter))] [AddComponentMenu("GameCreator/Multiplayer/Network Character V2")] - public class NetworkGameCreatorCharacterV2 : NetworkBehaviour + public class NetworkGameCreatorCharacterV2 : NetworkBehaviour, GameCreatorCharacter.INetworkCharacterAdapter { // ============================================================================================ // CONFIGURATION @@ -106,6 +106,8 @@ public class NetworkGameCreatorCharacterV2 : NetworkBehaviour private Vector3 intendedSpawnPosition; private Quaternion intendedSpawnRotation; + private bool UsesCharacterAdapter => character != null && character.IsNetworkCharacter && character.NetworkAdapter == this; + // ============================================================================================ // SPAWN STATE MACHINE - Full control over execution timing and order // ============================================================================================ @@ -155,7 +157,13 @@ private void Awake() { character = GetComponent(); characterController = GetComponent(); // Cache for velocity access - + + if (character != null) + { + character.IsNetworkCharacter = true; + character.NetworkAdapter = this; + } + // Capture position IMMEDIATELY in Awake before anything can reset it // NOTE: SetIntendedSpawnPosition() is called AFTER Awake() by NetworkPlayerManager // So we capture transform.position here as a fallback, but NetworkPlayerManager @@ -347,13 +355,14 @@ public void InitializeSpawn(Vector3 spawnPosition, Quaternion spawnRotation) if (character != null) { - // 🔥 INVASIVE FIX 2025-11-12: DISABLE Character component entirely for network characters! - // Character.Update() keeps resetting position and fighting our direct movement - // We're taking FULL CONTROL - no more GameCreator interference - if (character.enabled) + // Ensure the Character component is wired to the network adapter + character.IsNetworkCharacter = true; + character.NetworkAdapter = this; + + if (!character.enabled) { - Debug.LogWarning($"{role} [CharacterV2] 🔥 INVASIVE: DISABLING Character component for network control!"); - character.enabled = false; + Debug.LogWarning($"{role} [CharacterV2] ⚠️ Character component was disabled! Enabling for network adapter gating..."); + character.enabled = true; } // CRITICAL: Ensure CharacterController is enabled (required for movement) @@ -567,7 +576,7 @@ public override void OnNetworkDespawn() /// Follows Photon Fusion's FixedUpdateNetwork() pattern /// FIXED 2025-01-13: String interpolation compatibility issues /// - private void FixedUpdate() + private void HandleNetworkFixedUpdate() { if (!IsSpawned || character == null || character.Motion == null) return; @@ -803,12 +812,18 @@ private void FixedUpdate() } } + private void FixedUpdate() + { + if (UsesCharacterAdapter) return; + HandleNetworkFixedUpdate(); + } + /// /// PHASE 1: RENDERING (Visuals) /// Update() for frame-based, smooth rendering /// Follows Photon Fusion's Render() pattern /// - private void Update() + private void HandleNetworkUpdate() { if (!IsSpawned || character == null) return; @@ -940,6 +955,40 @@ private void Update() } } + private void Update() + { + if (UsesCharacterAdapter) return; + HandleNetworkUpdate(); + } + + private void HandleNetworkLateUpdate() + { + if (!IsSpawned || character == null) return; + } + + public bool IsNetworkOwner => IsOwner; + + public bool ShouldProcessUpdate() => IsSpawned && IsOwner; + + public bool ShouldProcessFixedUpdate() => IsSpawned && IsOwner; + + public bool ShouldProcessLateUpdate() => IsSpawned && IsOwner; + + public void OnCharacterUpdate() + { + HandleNetworkUpdate(); + } + + public void OnCharacterFixedUpdate() + { + HandleNetworkFixedUpdate(); + } + + public void OnCharacterLateUpdate() + { + HandleNetworkLateUpdate(); + } + // ============================================================================================ // NETWORK VARIABLE CALLBACKS diff --git a/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/VisualScripting/Core/NetworkInstructionExtensions.cs b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/VisualScripting/Core/NetworkInstructionExtensions.cs new file mode 100644 index 000000000..d6a8ec80c --- /dev/null +++ b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/VisualScripting/Core/NetworkInstructionExtensions.cs @@ -0,0 +1,121 @@ +using System; +using System.Threading.Tasks; +using GameCreator.Runtime.Common; +using GameCreator.Runtime.VisualScripting; +using Unity.Netcode; +using UnityEngine; + +namespace GameCreator.Multiplayer.Runtime.VisualScripting.Core +{ + /// + /// Helper extensions that route Instruction execution through the multiplayer layer. + /// + public static class NetworkInstructionExtensions + { + private const string LogPrefix = "[NetworkInstruction]"; + + /// + /// Executes an Instruction according to the requested network mode. + /// + /// Instruction instance invoking the execution. + /// Instruction logic to run. + /// Network execution mode requested by the Instruction. + /// Execution arguments used to locate the network context. + public static async Task ExecuteNetworked( + this Instruction instruction, + Func action, + NetworkExecutionMode mode, + Args args) + { + if (action == null) + { + Debug.LogWarning($"{LogPrefix} No action provided for networked execution"); + return; + } + + if (mode == NetworkExecutionMode.LocalOnly) + { + await action(); + return; + } + + GameObject contextObject = args != null ? args.Self : null; + NetworkExecutionContext context = NetworkExecutionContext.FromGameObject(contextObject); + string instructionName = instruction != null + ? $"{instruction.GetType().FullName}:{Guid.NewGuid():N}" + : $"Instruction:{Guid.NewGuid():N}"; + + // Fall back to local execution if we cannot find a network context + if (context == null || !context.IsSpawned || NetworkManager.Singleton == null) + { + Debug.LogWarning( + $"{LogPrefix} '{instructionName}' running locally because no network context is available", + contextObject + ); + + await action(); + return; + } + + switch (mode) + { + case NetworkExecutionMode.OwnerOnly: + if (context.IsOwner) + { + await action(); + } + else + { + Debug.LogWarning($"{LogPrefix} Skipping '{instructionName}' because this client is not the owner", contextObject); + } + + break; + + case NetworkExecutionMode.ServerOnly: + if (context.IsServer) + { + await action(); + } + else + { + Debug.LogWarning($"{LogPrefix} Skipping '{instructionName}' because this instance is not the server", contextObject); + } + + break; + + case NetworkExecutionMode.AllClients: + if (context.IsServer) + { + // Execute locally on the server and broadcast to every other client + await action(); + context.ExecuteOnAllClientsRpc(instructionName, action); + } + else + { + Debug.LogWarning($"{LogPrefix} AllClients execution requested from a non-server instance; running locally only", contextObject); + await action(); + } + + break; + + case NetworkExecutionMode.ServerAuthority: + if (context.IsServer) + { + await action(); + context.BroadcastResultToClients(instructionName); + } + else + { + context.RequestServerExecutionRpc(instructionName, action); + } + + break; + + default: + Debug.LogWarning($"{LogPrefix} Unknown network execution mode {mode}, running locally", contextObject); + await action(); + break; + } + } + } +} diff --git a/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/VisualScripting/Instructions/InstructionNetworkDebugLog.cs b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/VisualScripting/Instructions/InstructionNetworkDebugLog.cs index 06cac6b99..e66fd17ec 100644 --- a/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/VisualScripting/Instructions/InstructionNetworkDebugLog.cs +++ b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/VisualScripting/Instructions/InstructionNetworkDebugLog.cs @@ -2,6 +2,7 @@ using System.Threading.Tasks; using GameCreator.Runtime.Common; using GameCreator.Runtime.VisualScripting; +using GameCreator.Multiplayer.Runtime.VisualScripting.Core; using UnityEngine; namespace GameCreator.Multiplayer.Runtime.VisualScripting.Instructions @@ -44,7 +45,7 @@ protected override async Task Run(Args args) // PHASE 6A PATTERN: Use ExecuteNetworked() helper // This eliminates manual RPC boilerplate! - await ExecuteNetworked(async () => + await this.ExecuteNetworked(async () => { // Your Instruction logic goes here // This lambda will be executed based on NetworkExecutionMode: @@ -72,7 +73,7 @@ await ExecuteNetworked(async () => await Task.CompletedTask; - }, m_NetworkMode); // <-- Network execution mode specified here + }, m_NetworkMode, args); // <-- Network execution mode specified here } // HELPERS: -------------------------------------------------------------------------------