From a6259215e1ea44e050fdf9be3f7af0d3753c118f Mon Sep 17 00:00:00 2001 From: JOY Date: Sun, 17 May 2026 01:06:58 +0700 Subject: [PATCH 1/3] feat(unity): bind scene actors to profiles --- .../Scripts/AI/ActorProfileBinder.cs | 155 ++++++++++++++++++ .../Scripts/AI/ActorProfileBinder.cs.meta | 2 + 2 files changed, 157 insertions(+) create mode 100644 Unity/Assets/_SecondSpawn/Scripts/AI/ActorProfileBinder.cs create mode 100644 Unity/Assets/_SecondSpawn/Scripts/AI/ActorProfileBinder.cs.meta diff --git a/Unity/Assets/_SecondSpawn/Scripts/AI/ActorProfileBinder.cs b/Unity/Assets/_SecondSpawn/Scripts/AI/ActorProfileBinder.cs new file mode 100644 index 0000000..d13201e --- /dev/null +++ b/Unity/Assets/_SecondSpawn/Scripts/AI/ActorProfileBinder.cs @@ -0,0 +1,155 @@ +using System; +using System.Collections; +using UnityEngine; + +namespace SecondSpawn.AI +{ + /// + /// Binds one scene NPC-like body to one Nakama actor profile. + /// Each bound actor has its own body, stats, traits, soul, memory, policy, + /// runtime counters, and activity log. + /// + [DisallowMultipleComponent] + public sealed class ActorProfileBinder : MonoBehaviour + { + [SerializeField] private bool _loadOnStart = true; + [SerializeField] private string _actorId = "npc-guide"; + [SerializeField] private string _actorType = "npc"; + [SerializeField] private string _displayName = "Guide"; + [SerializeField] private string _archetypeId = "prototype-npc"; + [SerializeField] private string _visualPrefabKey = "prototype-npc"; + [SerializeField] private bool _seedMemoryOnStart = true; + [SerializeField] private string _seedMemoryKind = "system"; + [SerializeField, TextArea] private string _seedMemorySummary = + "This NPC-like body has an independent actor profile and memory."; + [SerializeField, Range(1, 10)] private int _seedMemoryImportance = 6; + + private SecondSpawnGatewayClient _gateway; + private Coroutine _loadRoutine; + + public ActorProfileDto CurrentProfile { get; private set; } + public bool IsLoading => _loadRoutine != null; + public bool IsReady => CurrentProfile != null; + public string ActorId => ResolveActorId(); + + public event Action ProfileLoaded; + + private void Awake() + { + _gateway = FindAnyObjectByType(); + } + + private void Start() + { + if (_loadOnStart) + { + Refresh(); + } + } + + public void Refresh() + { + if (_loadRoutine != null) + { + StopCoroutine(_loadRoutine); + } + + _loadRoutine = StartCoroutine(LoadActorProfile()); + } + + public IEnumerator LoadActorProfile() + { + if (_gateway == null) + { + _gateway = FindAnyObjectByType(); + } + + if (_gateway == null) + { + Debug.LogWarning($"[ActorProfileBinder] No gateway client found for actor {ActorId}."); + _loadRoutine = null; + yield break; + } + + ActorProfileDto profile = null; + string profileError = null; + yield return _gateway.GetNakamaActorProfile(BuildProfileRequest(), value => profile = value, error => profileError = error); + if (profile == null) + { + Debug.LogWarning($"[ActorProfileBinder] Actor profile load failed for {ActorId}: {profileError}"); + _loadRoutine = null; + yield break; + } + + CurrentProfile = profile; + ApplyProfile(profile); + + if (_seedMemoryOnStart && !string.IsNullOrWhiteSpace(_seedMemorySummary)) + { + ActorProfileDto memoryProfile = null; + string memoryError = null; + yield return _gateway.AddNakamaActorMemory(new ActorMemoryAddRequestDto + { + actor_id = profile.actor_id, + kind = string.IsNullOrWhiteSpace(_seedMemoryKind) ? "system" : _seedMemoryKind.Trim(), + summary = _seedMemorySummary.Trim(), + importance = Mathf.Clamp(_seedMemoryImportance, 1, 10) + }, value => memoryProfile = value, error => memoryError = error); + + if (memoryProfile != null) + { + CurrentProfile = memoryProfile; + ApplyProfile(memoryProfile); + } + else if (!string.IsNullOrWhiteSpace(memoryError)) + { + Debug.LogWarning($"[ActorProfileBinder] Actor memory seed failed for {ActorId}: {memoryError}"); + } + } + + ProfileLoaded?.Invoke(CurrentProfile); + _loadRoutine = null; + } + + private ActorProfileRequestDto BuildProfileRequest() + { + return new ActorProfileRequestDto + { + actor_id = ResolveActorId(), + actor_type = string.IsNullOrWhiteSpace(_actorType) ? "npc" : _actorType.Trim(), + display_name = string.IsNullOrWhiteSpace(_displayName) ? gameObject.name : _displayName.Trim(), + archetype_id = string.IsNullOrWhiteSpace(_archetypeId) ? "prototype-npc" : _archetypeId.Trim(), + visual_prefab_key = string.IsNullOrWhiteSpace(_visualPrefabKey) ? "prototype-npc" : _visualPrefabKey.Trim() + }; + } + + private void ApplyProfile(ActorProfileDto profile) + { + if (profile == null) + { + return; + } + + if (!string.IsNullOrWhiteSpace(profile.actor_id)) + { + _actorId = profile.actor_id.Trim(); + } + + if (!string.IsNullOrWhiteSpace(profile.display_name)) + { + _displayName = profile.display_name.Trim(); + gameObject.name = _displayName; + } + } + + private string ResolveActorId() + { + if (!string.IsNullOrWhiteSpace(_actorId)) + { + return _actorId.Trim(); + } + + return string.IsNullOrWhiteSpace(gameObject.name) ? "npc-body" : gameObject.name.Trim(); + } + } +} diff --git a/Unity/Assets/_SecondSpawn/Scripts/AI/ActorProfileBinder.cs.meta b/Unity/Assets/_SecondSpawn/Scripts/AI/ActorProfileBinder.cs.meta new file mode 100644 index 0000000..1b916a3 --- /dev/null +++ b/Unity/Assets/_SecondSpawn/Scripts/AI/ActorProfileBinder.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 2bb44b8edfbf4b41a928f752d071c948 From 4ee628e1a6de4ae6961884910b5dadfb7ea7109c Mon Sep 17 00:00:00 2001 From: JOY Date: Sun, 17 May 2026 01:10:43 +0700 Subject: [PATCH 2/3] fix(unity): harden actor profile binder --- .../Scripts/AI/ActorProfileBinder.cs | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/Unity/Assets/_SecondSpawn/Scripts/AI/ActorProfileBinder.cs b/Unity/Assets/_SecondSpawn/Scripts/AI/ActorProfileBinder.cs index d13201e..ce0fcc3 100644 --- a/Unity/Assets/_SecondSpawn/Scripts/AI/ActorProfileBinder.cs +++ b/Unity/Assets/_SecondSpawn/Scripts/AI/ActorProfileBinder.cs @@ -13,11 +13,12 @@ namespace SecondSpawn.AI public sealed class ActorProfileBinder : MonoBehaviour { [SerializeField] private bool _loadOnStart = true; - [SerializeField] private string _actorId = "npc-guide"; + [SerializeField] private string _actorId = ""; [SerializeField] private string _actorType = "npc"; [SerializeField] private string _displayName = "Guide"; [SerializeField] private string _archetypeId = "prototype-npc"; [SerializeField] private string _visualPrefabKey = "prototype-npc"; + [SerializeField] private bool _applyDisplayNameToGameObject; [SerializeField] private bool _seedMemoryOnStart = true; [SerializeField] private string _seedMemoryKind = "system"; [SerializeField, TextArea] private string _seedMemorySummary = @@ -26,9 +27,10 @@ public sealed class ActorProfileBinder : MonoBehaviour private SecondSpawnGatewayClient _gateway; private Coroutine _loadRoutine; + private bool _isLoading; public ActorProfileDto CurrentProfile { get; private set; } - public bool IsLoading => _loadRoutine != null; + public bool IsLoading => _isLoading; public bool IsReady => CurrentProfile != null; public string ActorId => ResolveActorId(); @@ -54,10 +56,18 @@ public void Refresh() StopCoroutine(_loadRoutine); } - _loadRoutine = StartCoroutine(LoadActorProfile()); + _loadRoutine = StartCoroutine(LoadActorProfileRoutine()); } - public IEnumerator LoadActorProfile() + private IEnumerator LoadActorProfileRoutine() + { + _isLoading = true; + yield return LoadActorProfile(); + _isLoading = false; + _loadRoutine = null; + } + + private IEnumerator LoadActorProfile() { if (_gateway == null) { @@ -67,7 +77,6 @@ public IEnumerator LoadActorProfile() if (_gateway == null) { Debug.LogWarning($"[ActorProfileBinder] No gateway client found for actor {ActorId}."); - _loadRoutine = null; yield break; } @@ -77,7 +86,6 @@ public IEnumerator LoadActorProfile() if (profile == null) { Debug.LogWarning($"[ActorProfileBinder] Actor profile load failed for {ActorId}: {profileError}"); - _loadRoutine = null; yield break; } @@ -108,7 +116,6 @@ public IEnumerator LoadActorProfile() } ProfileLoaded?.Invoke(CurrentProfile); - _loadRoutine = null; } private ActorProfileRequestDto BuildProfileRequest() @@ -138,7 +145,10 @@ private void ApplyProfile(ActorProfileDto profile) if (!string.IsNullOrWhiteSpace(profile.display_name)) { _displayName = profile.display_name.Trim(); - gameObject.name = _displayName; + if (_applyDisplayNameToGameObject) + { + gameObject.name = _displayName; + } } } From 755e6722a9a9f4b48723f5cf6f2c1ac0a2c50230 Mon Sep 17 00:00:00 2001 From: JOY Date: Sun, 17 May 2026 07:00:59 +0700 Subject: [PATCH 3/3] fix(unity): normalize actor profile binder inputs --- .../_SecondSpawn/Scripts/AI/ActorProfileBinder.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/Unity/Assets/_SecondSpawn/Scripts/AI/ActorProfileBinder.cs b/Unity/Assets/_SecondSpawn/Scripts/AI/ActorProfileBinder.cs index ce0fcc3..ce8a3c4 100644 --- a/Unity/Assets/_SecondSpawn/Scripts/AI/ActorProfileBinder.cs +++ b/Unity/Assets/_SecondSpawn/Scripts/AI/ActorProfileBinder.cs @@ -54,6 +54,8 @@ public void Refresh() if (_loadRoutine != null) { StopCoroutine(_loadRoutine); + _loadRoutine = null; + _isLoading = false; } _loadRoutine = StartCoroutine(LoadActorProfileRoutine()); @@ -99,7 +101,7 @@ private IEnumerator LoadActorProfile() yield return _gateway.AddNakamaActorMemory(new ActorMemoryAddRequestDto { actor_id = profile.actor_id, - kind = string.IsNullOrWhiteSpace(_seedMemoryKind) ? "system" : _seedMemoryKind.Trim(), + kind = NormalizeIdentifier(_seedMemoryKind, "system"), summary = _seedMemorySummary.Trim(), importance = Mathf.Clamp(_seedMemoryImportance, 1, 10) }, value => memoryProfile = value, error => memoryError = error); @@ -123,7 +125,7 @@ private ActorProfileRequestDto BuildProfileRequest() return new ActorProfileRequestDto { actor_id = ResolveActorId(), - actor_type = string.IsNullOrWhiteSpace(_actorType) ? "npc" : _actorType.Trim(), + actor_type = NormalizeIdentifier(_actorType, "npc"), display_name = string.IsNullOrWhiteSpace(_displayName) ? gameObject.name : _displayName.Trim(), archetype_id = string.IsNullOrWhiteSpace(_archetypeId) ? "prototype-npc" : _archetypeId.Trim(), visual_prefab_key = string.IsNullOrWhiteSpace(_visualPrefabKey) ? "prototype-npc" : _visualPrefabKey.Trim() @@ -161,5 +163,10 @@ private string ResolveActorId() return string.IsNullOrWhiteSpace(gameObject.name) ? "npc-body" : gameObject.name.Trim(); } + + private static string NormalizeIdentifier(string value, string fallback) + { + return string.IsNullOrWhiteSpace(value) ? fallback : value.Trim().ToLowerInvariant(); + } } }