diff --git a/S1API/Entities/NPC.cs b/S1API/Entities/NPC.cs index f83fd874..3f482155 100644 --- a/S1API/Entities/NPC.cs +++ b/S1API/Entities/NPC.cs @@ -3652,6 +3652,25 @@ internal void CreateFromClientNetworkSpawn() _clientNetworkSpawnHydrationDepth--; } + // Clients never run the instance constructor, so the behaviour owners the host sets there are missing on + // this clone of the template. Without them the host's behaviour RPCs throw here and are dropped. + if (gameObject is null) + { + Logger.Debug("[NPC] Client wrapper has no GameObject; behaviour owners not bound."); + } + else + { + try + { + RepairBehaviourOwnership(gameObject, GetPreferredNpcComponent(gameObject)); + NPCCustomer.BindDealAttendance(gameObject, GetSafeNpcId()); + } + catch (Exception ex) + { + Logger.Error($"[NPC] Binding behaviour owners on client for '{GetSafeNpcId()}' failed: {ex.Message}"); + } + } + MarkCustomNpcFinalized(); } diff --git a/S1API/Entities/NPCCustomer.cs b/S1API/Entities/NPCCustomer.cs index 803cb1d4..48a73f2b 100644 --- a/S1API/Entities/NPCCustomer.cs +++ b/S1API/Entities/NPCCustomer.cs @@ -633,6 +633,88 @@ internal static bool EnsureDealAttendanceSupport(GameObject? prefabRoot, Type? o return true; } + /// + /// INTERNAL: Points a live customer's "Customer attend deal" behaviour at the NPC's own NPCBehaviour + /// and Customer. Co-op clients get custom NPCs as clones of the S1API template, where the behaviour's + /// owner (Behaviour.beh) and its cached _customer stay null. The host's Enable/Activate RPCs then + /// throw inside the behaviour, FishNet drops the exception, and the customer never waits at the drop. + /// + internal static void BindDealAttendance(GameObject? npcRoot, string? npcId) + { + string id = npcId ?? ""; + if (npcRoot == null) + { + Logger.Warning($"Cannot bind deal attendance for '{id}': NPC object is missing."); + return; + } + + var customer = npcRoot.GetComponent(); + if (customer == null) + return; + + var behaviourManager = npcRoot.GetComponentInChildren(true); + if (behaviourManager == null) + { + Logger.Warning($"Cannot bind deal attendance for '{id}': NPCBehaviour is missing."); + return; + } + + var attendance = npcRoot.GetComponentInChildren(true); + if (attendance == null) + { + Logger.Warning($"Cannot bind deal attendance for '{id}': CustomerAttendDealBehaviour is missing."); + return; + } + + var repaired = new List(); +#if IL2CPPMELON + if (attendance.beh == null || attendance.beh.Pointer != behaviourManager.Pointer) + { + attendance.beh = behaviourManager; + repaired.Add("owner"); + } + + if (attendance._customer == null) + { + attendance._customer = customer; + repaired.Add("customer"); + } + + if (customer._attendDealBehaviour == null) + { + customer._attendDealBehaviour = attendance; + repaired.Add("customer link"); + } +#else + if (!ReferenceEquals(attendance.beh, behaviourManager)) + { + if (ReflectionUtils.TrySetFieldOrProperty(attendance, "beh", behaviourManager)) + repaired.Add("owner"); + else + Logger.Error($"Cannot set the deal attendance owner for '{id}'."); + } + + if (ReflectionUtils.TryGetFieldOrProperty(attendance, "_customer") == null) + { + if (ReflectionUtils.TrySetFieldOrProperty(attendance, "_customer", customer)) + repaired.Add("customer"); + else + Logger.Error($"Cannot set the deal attendance customer for '{id}'."); + } + + if (ReflectionUtils.TryGetFieldOrProperty(customer, "_attendDealBehaviour") == null) + { + if (ReflectionUtils.TrySetFieldOrProperty(customer, "_attendDealBehaviour", attendance)) + repaired.Add("customer link"); + else + Logger.Error($"Cannot set the customer's deal attendance for '{id}'."); + } +#endif + + if (repaired.Count > 0) + Logger.Msg($"Bound deal attendance for '{id}': set {string.Join(", ", repaired)}."); + } + /// /// INTERNAL: Ensure UnityEvents are allocated so runtime wiring does not NRE. ///