Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ namespace GameCreator.Multiplayer.Runtime.Components
/// </summary>
[RequireComponent(typeof(GameCreatorCharacter))]
[AddComponentMenu("GameCreator/Multiplayer/Network Character V2")]
public class NetworkGameCreatorCharacterV2 : NetworkBehaviour
public class NetworkGameCreatorCharacterV2 : NetworkBehaviour, GameCreatorCharacter.INetworkCharacterAdapter
{
// ============================================================================================
// CONFIGURATION
Expand Down Expand Up @@ -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
// ============================================================================================
Expand Down Expand Up @@ -155,7 +157,13 @@ private void Awake()
{
character = GetComponent<GameCreatorCharacter>();
characterController = GetComponent<CharacterController>(); // 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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -567,7 +576,7 @@ public override void OnNetworkDespawn()
/// Follows Photon Fusion's FixedUpdateNetwork() pattern
/// FIXED 2025-01-13: String interpolation compatibility issues
/// </summary>
private void FixedUpdate()
private void HandleNetworkFixedUpdate()
{
if (!IsSpawned || character == null || character.Motion == null) return;

Expand Down Expand Up @@ -803,12 +812,18 @@ private void FixedUpdate()
}
}

private void FixedUpdate()
{
if (UsesCharacterAdapter) return;
HandleNetworkFixedUpdate();
}

/// <summary>
/// PHASE 1: RENDERING (Visuals)
/// Update() for frame-based, smooth rendering
/// Follows Photon Fusion's Render() pattern
/// </summary>
private void Update()
private void HandleNetworkUpdate()
{
if (!IsSpawned || character == null) return;

Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Helper extensions that route Instruction execution through the multiplayer layer.
/// </summary>
public static class NetworkInstructionExtensions
{
private const string LogPrefix = "[NetworkInstruction]";

/// <summary>
/// Executes an Instruction according to the requested network mode.
/// </summary>
/// <param name="instruction">Instruction instance invoking the execution.</param>
/// <param name="action">Instruction logic to run.</param>
/// <param name="mode">Network execution mode requested by the Instruction.</param>
/// <param name="args">Execution arguments used to locate the network context.</param>
public static async Task ExecuteNetworked(
this Instruction instruction,
Func<Task> 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();
Comment on lines +86 to +90

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge AllClients/ServerAuthority instructions never run remotely

ExecuteNetworked registers the action delegate only on the local process before invoking network RPCs (case NetworkExecutionMode.AllClients here); the RPC handlers in NetworkInstructionAdapter look up the delegate in their own static registry (NetworkInstructionAdapter.cs:23-48, 64-82, 89-107), which is empty on other peers. Consequently AllClients runs only on the server and remote clients log “action not found”, and ServerAuthority calls from a client are dropped because the server cannot resolve the action. An instruction such as InstructionNetworkDebugLog executed in AllClients/ServerAuthority mode will never execute on remote peers, defeating those modes.

Useful? React with 👍 / 👎.

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;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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: -------------------------------------------------------------------------------
Expand Down