Skip to content

feat(nakama): add actor profile registry#12

Merged
JOY (JOY) merged 6 commits into
devfrom
feat/actor-profile-registry
May 16, 2026
Merged

feat(nakama): add actor profile registry#12
JOY (JOY) merged 6 commits into
devfrom
feat/actor-profile-registry

Conversation

@JOY
Copy link
Copy Markdown
Contributor

Summary

  • Add Nakama actor-profile RPCs for many NPC-like actors per owner: secondspawn_actor_profile_get and secondspawn_actor_memory_add.
  • Persist each actor in a separate secondspawn_actor storage object keyed by actor id.
  • Give each actor its own body profile, stats, traits, soul, memory, policy, runtime, and activity bundle.
  • Keep player profile context separate from actor/NPC profiles so a player can inhabit one current body while NPC-like actors maintain separate memory and state.
  • Extend Nakama runtime tests for multiple independent actor profiles and actor-specific memory.

Stack

This PR is stacked on #11 (feat/character-stats-animation). Retarget to dev after #8 and #11 merge.

Verification

  • npm run build in backend/nakama passed.
  • npm test in backend/nakama passed.
  • Markdownlint passed with the same file scope as .github/workflows/markdown-lint.yml.
  • Changed-file em-dash/en-dash scan passed.
  • git diff --check passed.

Notes

  • This is the prototype storage contract for many NPC/body actors. It does not yet model global world NPC ownership, body selection, reincarnation candidate pools, or Fusion-authoritative spawning.
  • LLM/provider calls remain outside Nakama. Actor profiles only store profile, memory, policy, and observability state.

Gemini (@gemini-code-assist) review
Codex (@codex) review

@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@JOY
Copy link
Copy Markdown
Contributor Author

Local code-review skill pass

Target: PR #12 feat(nakama): add actor profile registry

Engine Specialist Findings: N/A - no engine configured

.Codex/docs/technical-preferences.md is not present in this worktree, so engine-specialist routing is skipped per the local code-review skill.

Testability: TESTABLE

  • Nakama runtime tests cover RPC registration for secondspawn_actor_profile_get and secondspawn_actor_memory_add.
  • Tests create two independent actor profiles under one owner and verify separate actor IDs, owner binding, profile storage keying, default stats, traits, soul, and actor-local memory.
  • Existing profile, memory, agent decision, activity, conflict, and auth bridge tests still pass.

ADR Compliance: NO ADRS FOUND

No direct ADR reference was found in the implementation commit or file headers. The change stays within ADR 0010 direction: Nakama owns game-backend profile/storage extensions, while LLM provider calls remain outside Nakama.

Standards Compliance: PASS

  • The RPCs are small and reuse existing storage/read-write patterns.
  • No client-authoritative gameplay mutation is introduced.
  • No provider keys or LLM calls are added to Unity or Nakama.
  • Actor storage is owner-scoped and uses server-side authenticated ctx.userId, not client-supplied owner IDs.
  • Storage writes preserve create-only and versioned-write semantics through the same helper pattern as player context writes.

Architecture: CLEAN

  • This adds a separate secondspawn_actor collection instead of overloading the player context object.
  • Player profile remains durable account/current-body state; actor profiles are separate NPC-like body records with their own stats, traits, soul, memory, policy, runtime, and activity.
  • The implementation is intentionally prototype-scoped: it does not claim global world NPC ownership, Fusion-authoritative spawning, or reincarnation candidate pools yet.

SOLID: APPROVED WITH SUGGESTIONS

The Nakama module is getting larger, but this follows the current single-module prototype pattern. Once these RPC surfaces settle, split actor profile helpers into a separate runtime module file if Nakama bundling allows it cleanly.

Game-Specific Concerns

Positive Observations

  • The storage shape directly supports the player-spawns-into-body model without confusing account identity and actor identity.
  • Tests prove multiple NPC-like actors can coexist under one owner with separate memories.
  • Existing player profile and offline-agent behavior remains unchanged.

Required Changes

None from local code-review.

Suggestions

  • Add actor listing once the UI or world-spawn flow needs it. Do not add it before a caller exists.
  • Later, define global/system-owned NPC storage separately from user-owned actor profiles.
  • Later, connect actor profiles to Fusion spawn authority and reincarnation candidate selection.

Verdict: APPROVED WITH SUGGESTIONS

Verification observed:

  • npm run build in backend/nakama passed.
  • npm test in backend/nakama passed.
  • Markdownlint passed with the repo CI scope.
  • Changed-file em-dash/en-dash scan passed.
  • git diff --check passed.
  • GitHub gateway test + vet passed.

Codex Cloud review was requested in the PR body, but the connector replied that code review usage limits are reached. Gemini review is still pending.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces actor profile management and memory tracking by adding new RPC endpoints and storage logic. The review feedback highlights critical issues in the data normalization functions, specifically where the use of the logical OR operator (||) incorrectly overwrites valid 0 values for character stats and body timers with default values. Additionally, a suggestion was made to improve the normalization of the cultivation object to ensure all fields are correctly defaulted and clamped.

Comment thread backend/nakama/modules/index.ts Outdated
Comment on lines +832 to +846
function normalizeStats(stats: any): any {
var defaults = defaultCharacterStats();
return {
level: clampNumber(stats.level || defaults.level, 1, 100),
vitality: clampNumber(stats.vitality || defaults.vitality, 1, 9999),
force: clampNumber(stats.force || defaults.force, 1, 9999),
agility: clampNumber(stats.agility || defaults.agility, 1, 9999),
focus: clampNumber(stats.focus || defaults.focus, 1, 9999),
resilience: clampNumber(stats.resilience || defaults.resilience, 1, 9999),
max_health: clampNumber(stats.max_health || defaults.max_health, 1, 999999),
max_energy: clampNumber(stats.max_energy || defaults.max_energy, 0, 999999),
attack_power: clampNumber(stats.attack_power || defaults.attack_power, 0, 999999),
defense_power: clampNumber(stats.defense_power || defaults.defense_power, 0, 999999)
};
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

Using the || operator for defaults in normalizeStats will cause valid 0 values for max_energy, attack_power, and defense_power to be overwritten by their non-zero defaults. A check for undefined or null should be used instead to allow 0 as a valid input.

Suggested change
function normalizeStats(stats: any): any {
var defaults = defaultCharacterStats();
return {
level: clampNumber(stats.level || defaults.level, 1, 100),
vitality: clampNumber(stats.vitality || defaults.vitality, 1, 9999),
force: clampNumber(stats.force || defaults.force, 1, 9999),
agility: clampNumber(stats.agility || defaults.agility, 1, 9999),
focus: clampNumber(stats.focus || defaults.focus, 1, 9999),
resilience: clampNumber(stats.resilience || defaults.resilience, 1, 9999),
max_health: clampNumber(stats.max_health || defaults.max_health, 1, 999999),
max_energy: clampNumber(stats.max_energy || defaults.max_energy, 0, 999999),
attack_power: clampNumber(stats.attack_power || defaults.attack_power, 0, 999999),
defense_power: clampNumber(stats.defense_power || defaults.defense_power, 0, 999999)
};
}
function normalizeStats(stats: any): any {
var defaults = defaultCharacterStats();
var get = function (v: any, d: any) { return (v !== undefined && v !== null) ? v : d; };
return {
level: clampNumber(get(stats.level, defaults.level), 1, 100),
vitality: clampNumber(get(stats.vitality, defaults.vitality), 1, 9999),
force: clampNumber(get(stats.force, defaults.force), 1, 9999),
agility: clampNumber(get(stats.agility, defaults.agility), 1, 9999),
focus: clampNumber(get(stats.focus, defaults.focus), 1, 9999),
resilience: clampNumber(get(stats.resilience, defaults.resilience), 1, 9999),
max_health: clampNumber(get(stats.max_health, defaults.max_health), 1, 999999),
max_energy: clampNumber(get(stats.max_energy, defaults.max_energy), 0, 999999),
attack_power: clampNumber(get(stats.attack_power, defaults.attack_power), 0, 999999),
defense_power: clampNumber(get(stats.defense_power, defaults.defense_power), 0, 999999)
};
}

Comment thread backend/nakama/modules/index.ts Outdated
Comment on lines +848 to +854
function normalizeBodyTime(time: any): any {
return {
remaining_seconds: clampNumber(time.remaining_seconds || 86400, 0, 31536000),
max_seconds: clampNumber(time.max_seconds || 86400, 1, 31536000),
danger_drain_rate: clampNumber(time.danger_drain_rate || 1, 0, 1000)
};
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

In normalizeBodyTime, using || for remaining_seconds and danger_drain_rate will incorrectly reset them to defaults if they are set to 0. This could lead to critical bugs in gameplay timers where an actor with no time left is suddenly reset to a full day.

Suggested change
function normalizeBodyTime(time: any): any {
return {
remaining_seconds: clampNumber(time.remaining_seconds || 86400, 0, 31536000),
max_seconds: clampNumber(time.max_seconds || 86400, 1, 31536000),
danger_drain_rate: clampNumber(time.danger_drain_rate || 1, 0, 1000)
};
}
function normalizeBodyTime(time: any): any {
var get = function (v: any, d: any) { return (v !== undefined && v !== null) ? v : d; };
return {
remaining_seconds: clampNumber(get(time.remaining_seconds, 86400), 0, 31536000),
max_seconds: clampNumber(get(time.max_seconds, 86400), 1, 31536000),
danger_drain_rate: clampNumber(get(time.danger_drain_rate, 1), 0, 1000)
};
}

Comment thread backend/nakama/modules/index.ts Outdated
profile.body.stats = normalizeStats(profile.body.stats || {});
profile.body.characteristics = normalizeTraits(profile.body.characteristics || {});
profile.body.time = normalizeBodyTime(profile.body.time || {});
profile.body.cultivation = profile.body.cultivation || { tier: "Awakening", progress_xp: 0 };
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The cultivation object is not fully normalized. If it exists but is missing fields like tier or progress_xp, they won't be defaulted or clamped. It's better to normalize its properties individually to ensure data integrity.

Suggested change
profile.body.cultivation = profile.body.cultivation || { tier: "Awakening", progress_xp: 0 };
profile.body.cultivation = profile.body.cultivation || {};
profile.body.cultivation.tier = trimString(profile.body.cultivation.tier) || "Awakening";
profile.body.cultivation.progress_xp = clampNumber(profile.body.cultivation.progress_xp, 0, 1000000000);

@JOY
Copy link
Copy Markdown
Contributor Author

Unity client delta added after the initial PR:

  • Added ActorProfileDto, ActorProfileRequestDto, and ActorMemoryAddRequestDto to the shared Unity AI DTOs.
  • Added SecondSpawnGatewayClient.GetNakamaActorProfile(...) overloads for secondspawn_actor_profile_get.
  • Added SecondSpawnGatewayClient.AddNakamaActorMemory(...) for secondspawn_actor_memory_add.

Additional verification on latest head 254d3c0:

  • npm run build in backend/nakama passed.
  • npm test in backend/nakama passed.
  • Changed-file em-dash/en-dash scan passed.
  • git diff --check passed.

Unity caveat:

  • Unity batchmode compile in this clean worktree is still blocked before script compilation by the existing Package Manager failure: The "path" argument must be of type string. Received undefined. No packages loaded.
  • The Unity delta is DTOs plus RPC wrapper methods only, but it still needs Editor/MCP compile verification in the primary Unity project before merge-ready claim.

Gemini (@gemini-code-assist) please include latest commit 254d3c0 in the pending PR #12 review.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a comprehensive system for managing actor profiles and memories, including new DTOs in Unity and RPC endpoints in the Nakama backend. The changes allow for retrieving actor profiles and adding memories, supported by a suite of normalization and storage helper functions. Feedback focuses on ensuring that normalization changes are persisted to storage during profile retrieval, optimizing the creation flow by returning storage acknowledgments to avoid redundant database reads, and implementing length validation for actor IDs to prevent storage key failures.

Comment on lines +205 to +206
var state = getOrCreateActorProfileState(ctx, nk, request);
return JSON.stringify(state.profile);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

The rpcActorProfileGet function should persist any normalization changes made by ensureActorProfile. Currently, if ensureActorProfile (called inside getOrCreateActorProfileState) fills in missing defaults or bootstraps the agent_runtime, these changes are returned to the client but never saved to storage. This mirrors the logic in rpcProfileGet (lines 61-63). Consider updating ensureActorProfile to return a boolean indicating if changes were made, and performing a writeActorProfile if so.

Comment thread backend/nakama/modules/index.ts Outdated
Comment on lines +356 to +363
writeActorProfile(nk, profile, "");
var created = readActorProfile(nk, ownerId, actorId);
if (created) {
return {
profile: ensureActorProfile(created.value, ownerId, actorId),
version: created.version
};
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Efficiency: writeActorProfile can return the storage acks from nk.storageWrite. This allows getOrCreateActorProfileState to obtain the new object version without performing an additional readActorProfile call immediately after writing. This reduces database load during actor creation.

  var acks = writeActorProfile(nk, profile, "");
  return {
    profile: profile,
    version: acks[0].version
  };

};
}

function writeActorProfile(nk: nkruntime.Nakama, profile: any, version: string): void {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Update writeActorProfile to return the storage acks to support the efficiency improvement in getOrCreateActorProfileState.

Suggested change
function writeActorProfile(nk: nkruntime.Nakama, profile: any, version: string): void {
function writeActorProfile(nk: nkruntime.Nakama, profile: any, version: string): nkruntime.StorageAck[] {

if (typeof version === "string") {
write.version = version;
}
nk.storageWrite([write]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Return the result of nk.storageWrite to provide the version info to callers.

Suggested change
nk.storageWrite([write]);
return nk.storageWrite([write]);

Comment on lines +932 to +938
function normalizeActorId(actorId: any): string {
var normalized = sanitizeNakamaIdentifier(trimString(actorId), "");
if (!normalized) {
throw new Error("actor_id is required");
}
return normalized;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Validation: actor_id should be validated for length. Nakama storage keys have a limit of 128 bytes. Since the key is prefixed with profile: (8 bytes), the actor_id should be restricted to 120 characters to prevent storageWrite failures when using long identifiers.

function normalizeActorId(actorId: any): string {
  var normalized = sanitizeNakamaIdentifier(trimString(actorId), "");
  if (!normalized) {
    throw new Error("actor_id is required");
  }
  if (normalized.length > 120) {
    throw new Error("actor_id is too long (max 120 characters)");
  }
  return normalized;
}

@JOY
Copy link
Copy Markdown
Contributor Author

Delta after Gemini review on actor normalization:

  • Verified the finding before changing code. The reviewer was correct that actor profile normalization used || in places where 0 can be a meaningful value.
  • Replaced actor stat and BodyTime defaulting with explicit null/undefined/empty checks through numberOrDefault, so valid zeros are preserved before clamping.
  • Added normalizeCultivation so actor cultivation always has a tier and clamped progress_xp.
  • Updated actor profile creation to accept request-provided stats, time, and cultivation fields.
  • Added regression assertions for zero-like actor values: max_energy: 0, attack_power: 0, remaining_seconds: 0, danger_drain_rate: 0, and progress_xp: 0. Values that are invalid by design still clamp to minimums, such as level: 0 -> 1, max_health: 0 -> 1, and max_seconds: 0 -> 1.
  • Updated Unity ActorProfileRequestDto to include optional stats, time, and cultivation fields so the client DTO matches the backend surface.

Verification on latest head b894bdd:

  • npm run build in backend/nakama passed.
  • npm test in backend/nakama passed.
  • Markdownlint passed with the repo CI scope.
  • Changed-file em-dash/en-dash scan passed.
  • git diff --check passed.

Unity caveat remains unchanged: clean-worktree Unity batchmode is blocked before script compilation by the existing Package Manager path undefined issue, so Unity Editor compile still needs verification in the primary project before merge-ready claim.

Gemini (@gemini-code-assist) please delta re-review latest commit b894bdd.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a new Actor Profile system, including Unity DTOs, gateway client methods, and Nakama backend RPCs for retrieving and updating actor profiles and memories. Key additions include ActorProfileDto, ActorProfileRequestDto, and backend logic for managing actor state in Nakama storage. Feedback suggests optimizing the backend implementation by persisting normalized profiles during retrieval to prevent redundant processing and leveraging storageWrite acknowledgments to avoid unnecessary read operations when creating new profiles.

Comment on lines +205 to +206
var state = getOrCreateActorProfileState(ctx, nk, request);
return JSON.stringify(state.profile);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The rpcActorProfileGet function currently returns the profile after normalization, but it does not persist any changes made by ensureActorProfile back to storage. This means that if a profile requires migration or default field population, it will be re-processed on every GET request.

Consider refactoring ensureActorProfile to return a boolean indicating if changes were made, and then conditionally calling writeActorProfile here, similar to the pattern used in rpcProfileGet (lines 61-63).

Comment thread backend/nakama/modules/index.ts Outdated
Comment on lines +356 to +362
writeActorProfile(nk, profile, "");
var created = readActorProfile(nk, ownerId, actorId);
if (created) {
return {
profile: ensureActorProfile(created.value, ownerId, actorId),
version: created.version
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

This implementation performs a redundant storageRead immediately after a storageWrite to retrieve the object version. nk.storageWrite returns an array of StorageAck objects, each containing the new version of the written object.

You can optimize this by updating writeActorProfile to return the version string from the write acknowledgment, eliminating the need for the second read.

  var version = writeActorProfile(nk, profile, "");
  return {
    profile: ensureActorProfile(profile, ownerId, actorId),
    version: version
  };

};
}

function writeActorProfile(nk: nkruntime.Nakama, profile: any, version: string): void {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Update this function to return the version string from the nk.storageWrite acknowledgment to allow callers to avoid redundant reads.

Suggested change
function writeActorProfile(nk: nkruntime.Nakama, profile: any, version: string): void {
function writeActorProfile(nk: nkruntime.Nakama, profile: any, version: string): string {
var write: any = {
collection: collectionActor,
key: actorStorageKey(profile.actor_id),
userId: profile.owner_player_id,
value: profile,
permissionRead: 1,
permissionWrite: 0
};
if (typeof version === "string" && version !== "") {
write.version = version;
}
var acks = nk.storageWrite([write]);
return acks[0].version;
}

@JOY
Copy link
Copy Markdown
Contributor Author

Delta after Gemini review on actor profile retrieval:

  • Accepted the normalized-read persistence finding. Existing actor profiles are now normalized once, written back with the current storage version, and reread so the returned state carries the fresh version for follow-up writes.
  • Added regression coverage that deletes a stored actor �ody.time, calls secondspawn_actor_profile_get, and asserts the normalized �ody.time is persisted back to storage with a new version.
  • Accepted the storage-key safety concern.
    ormalizeActorId now rejects actor IDs longer than 64 sanitized characters instead of letting long IDs flow into storage keys and generated memory IDs.
  • Added a runtime test for long actor ID rejection.
  • Did not implement storageWrite ack optimization in this delta because the local Nakama TypeScript runtime shim exposes storageWrite(...): void, and the verified server-runtime path here does not provide an ack contract to rely on.

Verification on latest head d32614f:

pm run build in �ackend/nakama passed.

pm test in �ackend/nakama passed.

  • Markdownlint passed with the repo CI scope.
  • Changed-file em-dash/en-dash scan passed.
  • git diff --check passed.

Gemini (@gemini-code-assist) please delta re-review latest commit d32614f.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces actor profiles and actor-specific memory management to the Nakama backend and Unity client. Key changes include the addition of ActorProfileDto and related request objects, new RPC endpoints for retrieving actor profiles and adding memories, and comprehensive normalization logic for character stats and attributes. Feedback focuses on several critical improvements: reducing actorIdMaxLength to 56 to prevent storage key length violations, eliminating redundant database reads by utilizing the version returned from storageWrite, handling potential version conflicts during memory updates, and resolving structural inconsistencies in the ActorProfileDto to align with existing player profile patterns.

Comment thread backend/nakama/modules/index.ts Outdated
var rpcIdActorMemoryAdd = "secondspawn_actor_memory_add";
var agentActivityLogLimit = 32;
var agentRuntimeMetricMax = 1000000000;
var actorIdMaxLength = 64;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

The actorIdMaxLength of 64 is too high because the storage key generated in actorStorageKey (line 962) prepends profile:. Nakama storage keys have a hard limit of 64 characters. If a user provides an actor_id of 64 characters, the resulting key will be 72 characters, causing storageWrite to fail.

Suggested change
var actorIdMaxLength = 64;
var actorIdMaxLength = 56;

Comment on lines +20 to +22
public MemoryRecordDto[] memory;
public AgentRuntimeDto agent_runtime;
public AgentActivityRecordDto[] agent_activity;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

The ActorProfileDto contains memory, agent_runtime, and agent_activity fields at the root level, but these fields also exist within the BodyProfileDto (lines 47-49). This duplication creates inconsistency between the player profile (which uses body.memory) and the actor profile. It is recommended to move these behavioral state fields into the body object for actors as well, ensuring a consistent interface for all entities.


var profile = defaultActorProfile(ownerId, actorId, request);
writeActorProfile(nk, profile, "");
var created = readActorProfile(nk, ownerId, actorId);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

This readActorProfile call is redundant. The nk.storageWrite method returns an array of acknowledgment objects that include the new version of the stored object. You can use the version from the write result instead of performing an additional database read.

}
state.profile.memory = upsertMemoryRecord(state.profile.memory || [], memory);
state.profile.updated_at = new Date().toISOString();
writeActorProfile(nk, state.profile, state.version);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

This call to writeActorProfile uses a specific version for optimistic concurrency control but does not handle potential version conflict errors. If multiple requests attempt to update the same actor's memory simultaneously, one will fail with a storage conflict. Consider adding a retry loop or catching the error to provide a more robust implementation.

@JOY
Copy link
Copy Markdown
Contributor Author

Delta after Gemini review on actor profile storage safety:

  • Accepted the actor ID key-length concern. actorIdMaxLength is now 56 so profile:<actor_id> stays within a 64-character storage-key budget.
  • Added actor memory write conflict regression coverage. The test creates an actor profile, simulates an external version bump, then asserts secondspawn_actor_memory_add surfaces the storage version conflict instead of silently overwriting actor memory.
  • Rechecked the storageWrite acknowledgement suggestion and did not apply it because the verified local TypeScript runtime shim still exposes storageWrite(...): void. This path should not rely on an unverified return contract.
  • Rechecked the ActorProfile DTO structure comment. The current shape intentionally keeps actor-level memory/runtime/activity on the actor profile, while body holds the vessel state. I am not changing that in this delta because it matches the GDD actor-profile bundle model and avoids duplicating actor memory inside body state.

Verification on latest head bdf5940:

  • npm run build in backend/nakama passed.
  • npm test in backend/nakama passed.
  • Markdownlint passed with the repo CI scope.
  • Changed-file em-dash/en-dash scan passed.
  • git diff --check passed.

Gemini (@gemini-code-assist) please delta re-review latest commit bdf5940.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces actor profile management, adding new DTOs in Unity and corresponding RPC endpoints in the Nakama backend for retrieving actor profiles and adding memories. The backend implementation includes logic for actor profile storage, normalization, and state management. Feedback focuses on optimizing the profile retrieval process by avoiding expensive deep comparisons using JSON.stringify and addressing a potential race condition during the initial creation of actor profiles to prevent unhandled storage conflict errors.

Comment thread backend/nakama/modules/index.ts Outdated
Comment on lines +370 to +372
var before = JSON.stringify(existing.value || {});
var profile = ensureActorProfile(existing.value || {}, ownerId, actorId);
if (JSON.stringify(profile) !== before) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Using JSON.stringify for deep comparison on every profile retrieval is inefficient, especially as the actor profile grows with memory and activity logs. Consider tracking a dirty flag within ensureActorProfile or comparing specific critical fields to determine if a write-back is necessary.

Comment thread backend/nakama/modules/index.ts Outdated
Comment on lines +354 to +355
writeActorProfile(nk, profile, "");
var created = readActorProfile(nk, ownerId, actorId);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

There is a potential race condition here. If two concurrent requests attempt to create the same actorId for the same user, the second writeActorProfile call with version "" (create-only) will throw a storage conflict error. Consider wrapping the initial write in a try-catch block to gracefully handle the case where the profile was created by a parallel request.

References
  1. Ensure that invalid states or race conditions in storage operations are safely handled, particularly during object creation.

@JOY JOY (JOY) force-pushed the feat/character-stats-animation branch from ca363a0 to c7d00a8 Compare May 16, 2026 23:52
@JOY JOY (JOY) force-pushed the feat/actor-profile-registry branch from bdf5940 to ca4087e Compare May 16, 2026 23:57
@JOY JOY (JOY) changed the base branch from feat/character-stats-animation to dev May 16, 2026 23:57
@JOY
Copy link
Copy Markdown
Contributor Author

Delta after Gemini review and stack rebase:

Verification on latest head ca4087e:

  • npm run build in backend/nakama passed.
  • npm test in backend/nakama passed.
  • go test -count=1 ./... in backend/gateway passed.
  • go vet ./... in backend/gateway passed.
  • Markdownlint passed with the repo CI scope.
  • git diff --check passed.
  • Changed-file em-dash/en-dash scan passed.

Gemini (@gemini-code-assist) please delta re-review latest commit ca4087e.

@gemini-code-assist
Copy link
Copy Markdown

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@JOY
Copy link
Copy Markdown
Contributor Author

Local code-review delta pass

Target: PR #12 latest head ca4087e after rebase onto dev.

Engine Specialist Findings: N/A - no engine configured

.Codex/docs/technical-preferences.md is not present, so engine-specialist routing is skipped per the local code-review skill.

Testability: TESTABLE

  • Actor profile creation, independent actor memory, normalization persistence, actor ID limits, memory version conflict, and create-race recovery are covered by Nakama runtime tests.
  • Existing gateway and markdown checks pass after the rebase.

ADR Compliance: COMPLIANT

The change stays within ADR 0010: actor profiles are game-backend state in Nakama runtime modules. No separate game API gateway or client-authoritative state mutation is introduced.

Standards Compliance: PASS

  • Actor profile creation now handles create-only write races by rereading storage.
  • Actor normalization no longer relies on full JSON.stringify deep comparisons.
  • Actor IDs remain bounded to keep storage keys within Nakama limits.
  • Unity DTO/RPC wrappers do not store provider keys or mutate gameplay state directly.

Architecture: CLEAN

Actor profiles remain separate from player account/current-body state and support many NPC-like bodies with their own stats, traits, soul, memory, policy, runtime, and activity.

SOLID: APPROVED WITH SUGGESTIONS

No blocking issue found. Split Nakama runtime modules later if the single prototype file becomes harder to maintain.

Required Changes

None.

Suggestions

  • Add actor listing only when a caller exists.
  • Later define system-owned/global NPC storage separately from user-owned actor records.

Verdict: APPROVED WITH SUGGESTIONS

Gemini delta review is currently blocked by daily quota, but this local pass verifies the latest Gemini findings were addressed and CI is green.

@JOY JOY (JOY) merged commit a585c63 into dev May 16, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant