diff --git a/engine/Sandbox.Engine/Scene/Components/Component.Network.cs b/engine/Sandbox.Engine/Scene/Components/Component.Network.cs index 68f4c7d53..fb6653133 100644 --- a/engine/Sandbox.Engine/Scene/Components/Component.Network.cs +++ b/engine/Sandbox.Engine/Scene/Components/Component.Network.cs @@ -99,7 +99,10 @@ protected T __sync_GetValue( WrappedPropertyGet p ) [MethodImpl( MethodImplOptions.AggressiveInlining )] protected void __rpc_Wrapper( in WrappedMethod m, params object[] argumentList ) { - Rpc.OnCallInstanceRpc( GameObject, this, m, argumentList ); + using ( Rpc.WithCaller( Rpc.ConsumePendingRpcCaller() ) ) + { + Rpc.OnCallInstanceRpc( GameObject, this, m, argumentList ); + } } /// diff --git a/engine/Sandbox.Engine/Scene/GameObject/GameObject.Network.cs b/engine/Sandbox.Engine/Scene/GameObject/GameObject.Network.cs index c60c32abe..b11729325 100644 --- a/engine/Sandbox.Engine/Scene/GameObject/GameObject.Network.cs +++ b/engine/Sandbox.Engine/Scene/GameObject/GameObject.Network.cs @@ -414,7 +414,10 @@ protected T __sync_GetValue( WrappedPropertyGet p ) [MethodImpl( MethodImplOptions.AggressiveInlining )] protected void __rpc_Wrapper( in WrappedMethod m, params object[] argumentList ) { - Rpc.OnCallInstanceRpc( this, default, m, argumentList ); + using ( Rpc.WithCaller( Rpc.ConsumePendingRpcCaller() ) ) + { + Rpc.OnCallInstanceRpc( this, default, m, argumentList ); + } } /// diff --git a/engine/Sandbox.Engine/Scene/GameObjectSystem/GameObjectSystem.Network.cs b/engine/Sandbox.Engine/Scene/GameObjectSystem/GameObjectSystem.Network.cs index d9ae016a4..0774110f6 100644 --- a/engine/Sandbox.Engine/Scene/GameObjectSystem/GameObjectSystem.Network.cs +++ b/engine/Sandbox.Engine/Scene/GameObjectSystem/GameObjectSystem.Network.cs @@ -42,7 +42,10 @@ internal void ForceChangeId( Guid guid ) [MethodImpl( MethodImplOptions.AggressiveInlining )] protected void __rpc_Wrapper( in WrappedMethod m, params object[] argumentList ) { - Rpc.OnCallInstanceRpc( this, m, argumentList ); + using ( Rpc.WithCaller( Rpc.ConsumePendingRpcCaller() ) ) + { + Rpc.OnCallInstanceRpc( this, m, argumentList ); + } } [EditorBrowsable( EditorBrowsableState.Never )] diff --git a/engine/Sandbox.Engine/Scene/Networking/Rpc.InstanceRpc.cs b/engine/Sandbox.Engine/Scene/Networking/Rpc.InstanceRpc.cs index 56149c37a..ca6bbac2a 100644 --- a/engine/Sandbox.Engine/Scene/Networking/Rpc.InstanceRpc.cs +++ b/engine/Sandbox.Engine/Scene/Networking/Rpc.InstanceRpc.cs @@ -123,7 +123,7 @@ static void InvokeInstanceRpc( in SceneRpcMsg rpc, in object targetObject, in Co NetworkDebugSystem.Current?.Track( rpcName, rpc ); - using ( WithCaller( source ) ) + using ( WithPendingCaller( source ) ) { try { @@ -164,7 +164,7 @@ static void InvokeInstanceRpc( in ObjectRpcMsg rpc, in TypeDescription typeDesc, NetworkDebugSystem.Current?.Track( rpcName, rpc ); - using ( WithCaller( source ) ) + using ( WithPendingCaller( source ) ) { try { @@ -226,7 +226,7 @@ public static void OnCallInstanceRpc( in GameObjectSystem system, in WrappedMeth // // Send over network // - if ( !Calling && Networking.IsActive ) + if ( !IsRemoteCall && Networking.IsActive ) { SendInstanceRpc( system, m, argumentList, attribute ); } @@ -241,8 +241,6 @@ public static void OnCallInstanceRpc( in GameObjectSystem system, in WrappedMeth if ( attribute.Mode == RpcMode.Owner && !isOwner ) return; if ( attribute.Mode == RpcMode.Host && !Networking.IsHost ) return; - PreCall(); - if ( !HasHostInstancePermission( Caller ?? Connection.Local, attribute.Flags ) ) return; Resume( m ); @@ -260,7 +258,7 @@ public static void OnCallInstanceRpc( in GameObject go, in Component component, // // Send over network // - if ( !Calling && Networking.IsActive ) + if ( !IsRemoteCall && Networking.IsActive ) { SendInstanceRpc( go, component, m, argumentList, attribute ); } @@ -275,8 +273,6 @@ public static void OnCallInstanceRpc( in GameObject go, in Component component, if ( attribute.Mode == RpcMode.Owner && !isOwner ) return; if ( attribute.Mode == RpcMode.Host && !Networking.IsHost ) return; - PreCall(); - // Can they even call this shit if ( !HasInstancePermission( Caller ?? Connection.Local, go, attribute.Flags ) ) return; diff --git a/engine/Sandbox.Engine/Scene/Networking/Rpc.StaticRpc.cs b/engine/Sandbox.Engine/Scene/Networking/Rpc.StaticRpc.cs index 249619406..75fd1c7fb 100644 --- a/engine/Sandbox.Engine/Scene/Networking/Rpc.StaticRpc.cs +++ b/engine/Sandbox.Engine/Scene/Networking/Rpc.StaticRpc.cs @@ -24,7 +24,7 @@ internal static void IncomingStaticRpcMsg( StaticRpcMsg message, Connection sour NetworkDebugSystem.Current?.Track( $"{method.TypeDescription.FullName}.{method.Name}", message ); - using ( WithCaller( source ) ) + using ( WithPendingCaller( source ) ) { try { @@ -43,30 +43,31 @@ internal static void IncomingStaticRpcMsg( StaticRpcMsg message, Connection sour [EditorBrowsable( EditorBrowsableState.Never )] public static void OnCallRpc( WrappedMethod m, params object[] argumentList ) { - var attribute = m.GetAttribute(); - if ( attribute is null ) return; - - // - // Send over network - // - if ( !Calling && Networking.IsActive ) + using ( WithCaller( ConsumePendingRpcCaller() ) ) { - SendStaticRpc( m, argumentList, attribute ); - } + var attribute = m.GetAttribute(); + if ( attribute is null ) return; - // Was filtered out - if ( Filter.HasValue && !Filter.Value.IsRecipient( Connection.Local ) ) return; + // + // Send over network + // + if ( !IsRemoteCall && Networking.IsActive ) + { + SendStaticRpc( m, argumentList, attribute ); + } - // Was not included in the filter - if ( attribute.Mode == RpcMode.Owner && !Networking.IsHost ) return; - if ( attribute.Mode == RpcMode.Host && !Networking.IsHost ) return; + // Was filtered out + if ( Filter.HasValue && !Filter.Value.IsRecipient( Connection.Local ) ) return; - PreCall(); + // Was not included in the filter + if ( attribute.Mode == RpcMode.Owner && !Networking.IsHost ) return; + if ( attribute.Mode == RpcMode.Host && !Networking.IsHost ) return; - // Can they even call this shit - if ( !HasStaticPermission( Caller ?? Connection.Local, attribute.Flags ) ) return; + // Can they even call this shit + if ( !HasStaticPermission( Caller, attribute.Flags ) ) return; - Resume( m ); + Resume( m ); + } } /// diff --git a/engine/Sandbox.Engine/Scene/Networking/Rpc.cs b/engine/Sandbox.Engine/Scene/Networking/Rpc.cs index f13965ffe..8a8d06ba1 100644 --- a/engine/Sandbox.Engine/Scene/Networking/Rpc.cs +++ b/engine/Sandbox.Engine/Scene/Networking/Rpc.cs @@ -109,30 +109,77 @@ public static partial class Rpc /// /// Whether we're currently being called from a remote . /// - public static bool Calling { get; private set; } + public static bool IsRemoteCall { get; private set; } internal static Connection.Filter? Filter { get; private set; } internal static DisposeAction WithCaller( Connection caller ) { var oldCaller = Caller; - var oldCalling = Calling; + var oldIsRemoteCall = IsRemoteCall; - Calling = true; + IsRemoteCall = caller != Connection.Local; Caller = caller; unsafe { - return new DisposeAction( &RestoreCaller, oldCaller, oldCalling ); + return new DisposeAction( &RestoreCaller, oldCaller, oldIsRemoteCall ); } } - static void RestoreCaller( Connection oldCaller, bool oldCalling ) + static void RestoreCaller( Connection oldCaller, bool oldIsRemoteCall ) { - Calling = oldCalling; + IsRemoteCall = oldIsRemoteCall; Caller = oldCaller; } + /// + /// Temporarily stores the connection that initiated a remote RPC. + /// This is consumed by to establish the caller context. + /// + /// This deferred approach ensures nested RPC calls maintain correct caller information: + /// - Remote RPC arrives → sets PendingRpcCaller + /// - First wrapper consumes it → creates WithCaller scope, resets to null + /// - Nested RPCs see null → correctly use Connection.Local + /// + /// + private static Connection PendingRpcCaller { get; set; } + + /// + /// Consumes (if set) and resets it to null. + /// Returns the caller to use for the current RPC execution context. + /// + /// The remote caller if pending, otherwise + internal static Connection ConsumePendingRpcCaller() + { + var caller = PendingRpcCaller ?? Connection.Local; + PendingRpcCaller = null; + return caller; + } + + /// + /// Sets the pending caller for a remote RPC invocation. + /// + /// The remote connection invoking the RPC + internal static DisposeAction WithPendingCaller( Connection source ) + { + PendingRpcCaller = source; + + unsafe + { + return new DisposeAction( ResetPendingCaller ); + } + } + + /// + /// Resets the to null. + /// This doesn't need to restore the previous value as there will never be nested remote RPCs. + /// + private static void ResetPendingCaller() + { + PendingRpcCaller = null; + } + /// /// Resume a method from an RPC. If the RPC caller is our local connection then we'll /// first disable any active filter and restore it afterwards. @@ -166,20 +213,6 @@ internal static void Resume( WrappedMethod m ) } } - /// - /// Called right before calling an RPC function. - /// - public static void PreCall() - { - if ( Calling ) - { - Calling = false; - return; - } - - Caller = Connection.Local; - } - /// /// Filter the recipients of any Rpc called in this scope to only include the specified set. ///