Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ Cultivation Master mechanics without a new approved design update.

### Engine

- Unity 6.5 beta (currently `6000.5.0b7`) + URP. JOY explicitly chose beta over Unity 6.0 LTS for newest features; accept risk of breaking changes between beta builds and that some 3rd-party assets (Opsive UCC, Behavior Designer, Convai) may not be tested against this version yet. Re-evaluate if beta blocks progress.
- Unity 6.5 beta (currently `6000.5.0b8`) + URP. JOY explicitly chose beta over Unity 6.0 LTS for newest features; accept risk of breaking changes between beta builds and that some 3rd-party assets (Opsive UCC, Behavior Designer, Convai) may not be tested against this version yet. Re-evaluate if beta blocks progress.
- Force Text serialization (default Unity 6 - DO NOT change)

### Networking
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ Cultivation Master mechanics without a new approved design update.

### Engine

- Unity 6.5 beta (currently `6000.5.0b7`) + URP. JOY explicitly chose beta over Unity 6.0 LTS for newest features; accept risk of breaking changes between beta builds and that some 3rd-party assets (Opsive UCC, Behavior Designer, Convai) may not be tested against this version yet. Re-evaluate if beta blocks progress.
- Unity 6.5 beta (currently `6000.5.0b8`) + URP. JOY explicitly chose beta over Unity 6.0 LTS for newest features; accept risk of breaking changes between beta builds and that some 3rd-party assets (Opsive UCC, Behavior Designer, Convai) may not be tested against this version yet. Re-evaluate if beta blocks progress.
- Force Text serialization (default Unity 6 - DO NOT change)

### Networking
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ Public docs are published from `/docs` to GitBook:

Requirements:

- Unity 6.5 beta `6000.5.0b7`
- Unity 6.5 beta `6000.5.0b8`
- Git LFS
- Photon Fusion 2 app ID
- Nakama local backend for backend work
Expand Down
2 changes: 1 addition & 1 deletion Unity/Packages/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"com.unity.ide.visualstudio": "2.0.27",
"com.unity.inputsystem": "1.19.0",
"com.unity.multiplayer.center": "1.0.1",
"com.unity.nuget.mono-cecil": "1.10.2",
"com.unity.nuget.mono-cecil": "1.11.6",
"com.unity.render-pipelines.universal": "17.5.0",
"com.unity.test-framework": "1.7.0",
"com.unity.timeline": "1.8.12",
Expand Down
2 changes: 1 addition & 1 deletion Unity/Packages/packages-lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@
},
"com.unity.nuget.mono-cecil": {
"version": "1.11.6",
"depth": 2,
"depth": 0,
"source": "registry",
"dependencies": {},
"url": "https://packages.unity.com"
Expand Down
4 changes: 2 additions & 2 deletions Unity/ProjectSettings/ProjectVersion.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
m_EditorVersion: 6000.5.0b7
m_EditorVersionWithRevision: 6000.5.0b7 (c8c45dd2de2f)
m_EditorVersion: 6000.5.0b8
m_EditorVersionWithRevision: 6000.5.0b8 (9be2869246c2)
2 changes: 1 addition & 1 deletion backend/gateway/deploy/cloudrun.env.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
GATEWAY_ENV: staging
AGENT_DECISION_MODEL: "claude-haiku-4-5"
LLM_RATE_LIMIT_PER_PLAYER_PER_MIN: "30"
LLM_TOKEN_BUDGET_PER_PLAYER_DAY: "50000"
LLM_TOKEN_BUDGET_PER_PLAYER_DAY: "500000"
10 changes: 7 additions & 3 deletions backend/gateway/internal/character/profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ import (

// PlayerProfile is durable account-level identity. It survives body death.
type PlayerProfile struct {
PlayerID string `json:"player_id"`
DisplayName string `json:"display_name"`
CreatedAt time.Time `json:"created_at"`
PlayerID string `json:"player_id"`
DisplayName string `json:"display_name"`
SecondBalanceSeconds int64 `json:"second_balance_seconds"`
ReincarnationCount int64 `json:"reincarnation_count"`
CreatedAt time.Time `json:"created_at"`
}

// BodyProfile is the current synthetic body. It is replaced on reincarnation.
Expand Down Expand Up @@ -184,6 +186,8 @@ func BuildAgentContextPrompt(ctx AgentContext, maxMemories int) string {
var b strings.Builder
writeKV(&b, "player_id", ctx.Player.PlayerID)
writeKV(&b, "display_name", ctx.Player.DisplayName)
writeKV(&b, "second_balance_seconds", fmt.Sprintf("%d", ctx.Player.SecondBalanceSeconds))
writeKV(&b, "reincarnation_count", fmt.Sprintf("%d", ctx.Player.ReincarnationCount))
writeKV(&b, "body_id", ctx.Body.BodyID)
writeKV(&b, "archetype_id", ctx.Body.ArchetypeID)
writeKV(&b, "visual_prefab_key", ctx.Body.VisualPrefabKey)
Expand Down
8 changes: 5 additions & 3 deletions backend/gateway/internal/character/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,9 +158,11 @@ func NewDefaultAgentContext(playerID string, now time.Time) AgentContext {

return AgentContext{
Player: PlayerProfile{
PlayerID: playerID,
DisplayName: displayName,
CreatedAt: now,
PlayerID: playerID,
DisplayName: displayName,
SecondBalanceSeconds: 7 * 24 * 60 * 60,
ReincarnationCount: 0,
CreatedAt: now,
},
Body: BodyProfile{
BodyID: "body-" + playerID,
Expand Down
4 changes: 3 additions & 1 deletion backend/gateway/internal/server/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,9 @@ func TestAgentDecidePrototype(t *testing.T) {
"context": {
"player": {
"player_id": "user-1",
"display_name": "user-1"
"display_name": "user-1",
"second_balance_seconds": 604800,
"reincarnation_count": 0
},
"body": {
"body_id": "body-user-1",
Expand Down
75 changes: 66 additions & 9 deletions backend/nakama/modules/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -355,7 +355,7 @@ function getOrCreateAgentContextState(ctx: nkruntime.Context, nk: nkruntime.Naka
}

var context = defaultAgentContext(userId);
writeAgentContext(nk, context, "");
writeAgentContext(nk, context, "*");
var created = readAgentContext(nk, userId);
if (created) {
return {
Expand All @@ -374,7 +374,15 @@ function normalizeExistingAgentContextState(nk: nkruntime.Nakama, userId: string
var before = JSON.stringify(existing.value || {});
var context = ensureAgentContext(existing.value || {}, userId);
if (JSON.stringify(context) !== before) {
writeAgentContext(nk, context, existing.version);
try {
writeAgentContext(nk, context, existing.version);
} catch (err) {
var raced = readAgentContext(nk, userId);
if (raced) {
return normalizeRacedAgentContextState(nk, userId, raced);
}
throw err;
}
var rewritten = readAgentContext(nk, userId);
if (rewritten) {
return {
Expand Down Expand Up @@ -416,7 +424,7 @@ function writeAgentContext(nk: nkruntime.Nakama, context: any, version: string):
permissionRead: 1,
permissionWrite: 0
};
if (typeof version === "string") {
if (typeof version === "string" && version.length > 0) {
write.version = version;
}
nk.storageWrite([write]);
Expand All @@ -432,7 +440,7 @@ function getOrCreateActorProfileState(ctx: nkruntime.Context, nk: nkruntime.Naka

var profile = defaultActorProfile(ownerId, actorId, request);
try {
writeActorProfile(nk, profile, "");
writeActorProfile(nk, profile, "*");
} catch (err) {
var raced = readActorProfile(nk, ownerId, actorId);
if (raced) {
Expand All @@ -459,7 +467,15 @@ function normalizeExistingActorProfileState(nk: nkruntime.Nakama, ownerId: strin
var profile = ensureActorProfile(existing.value || {}, ownerId, actorId);
if (needsPersistence) {
profile.updated_at = new Date().toISOString();
writeActorProfile(nk, profile, existing.version);
try {
writeActorProfile(nk, profile, existing.version);
} catch (err) {
var raced = readActorProfile(nk, ownerId, actorId);
if (raced) {
return normalizeRacedActorProfileState(nk, ownerId, actorId, raced);
}
throw err;
}
var rewritten = readActorProfile(nk, ownerId, actorId);
if (rewritten) {
return {
Expand All @@ -475,6 +491,26 @@ function normalizeExistingActorProfileState(nk: nkruntime.Nakama, ownerId: strin
};
}

function normalizeRacedAgentContextState(nk: nkruntime.Nakama, userId: string, existing: any): any {
var before = JSON.stringify(existing.value || {});
var context = ensureAgentContext(existing.value || {}, userId);
if (JSON.stringify(context) !== before) {
writeAgentContext(nk, context, existing.version);
var rewritten = readAgentContext(nk, userId);
if (rewritten) {
return {
context: ensureAgentContext(rewritten.value, userId),
version: rewritten.version
};
}
}

return {
context: context,
version: existing.version
};
}

function actorProfileNeedsNormalization(profile: any): boolean {
return !profile ||
!profile.actor_id ||
Expand All @@ -499,6 +535,27 @@ function actorProfileNeedsNormalization(profile: any): boolean {
!profile.updated_at;
}

function normalizeRacedActorProfileState(nk: nkruntime.Nakama, ownerId: string, actorId: string, existing: any): any {
var needsPersistence = actorProfileNeedsNormalization(existing.value || {});
var profile = ensureActorProfile(existing.value || {}, ownerId, actorId);
if (needsPersistence) {
profile.updated_at = new Date().toISOString();
writeActorProfile(nk, profile, existing.version);
var rewritten = readActorProfile(nk, ownerId, actorId);
if (rewritten) {
return {
profile: ensureActorProfile(rewritten.value, ownerId, actorId),
version: rewritten.version
};
}
}

return {
profile: profile,
version: existing.version
};
}

function readActorProfile(nk: nkruntime.Nakama, ownerId: string, actorId: string): any {
var objects = nk.storageRead([{
collection: collectionActor,
Expand All @@ -525,7 +582,7 @@ function writeActorProfile(nk: nkruntime.Nakama, profile: any, version: string):
permissionRead: 1,
permissionWrite: 0
};
if (typeof version === "string") {
if (typeof version === "string" && version.length > 0) {
write.version = version;
}
nk.storageWrite([write]);
Expand Down Expand Up @@ -574,9 +631,9 @@ function defaultActorProfile(ownerId: string, actorId: string, request: any): an

function ensureActorProfile(profile: any, ownerId: string, actorId: string): any {
var timestamp = new Date().toISOString();
profile.actor_id = normalizeActorId(profile.actor_id || actorId);
profile.actor_id = actorId;
profile.actor_type = normalizeActorType(profile.actor_type);
profile.owner_player_id = trimString(profile.owner_player_id) || ownerId;
profile.owner_player_id = ownerId;
profile.display_name = trimString(profile.display_name) || actorDisplayName(profile.actor_id);
profile.body = profile.body || {};
profile.body.body_id = trimString(profile.body.body_id) || "body-" + profile.actor_id;
Expand Down Expand Up @@ -650,7 +707,7 @@ function defaultBodyProfile(playerId: string, displayName: string, timestamp: st
function ensureAgentContext(context: any, playerId: string): any {
var timestamp = new Date().toISOString();
context.player = context.player || {};
context.player.player_id = trimString(context.player.player_id) || playerId;
context.player.player_id = playerId;
context.player.display_name = trimString(context.player.display_name) || context.player.player_id;
context.player.created_at = trimString(context.player.created_at) || timestamp;
ensureSecondBalance(context);
Expand Down
2 changes: 1 addition & 1 deletion backend/nakama/tests/supabase_custom_auth.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ function createRuntimeHarness(module) {
conflictOnNextVersionedWrite = false;
}
if (Object.prototype.hasOwnProperty.call(request, "version")) {
if (request.version === "") {
if (request.version === "*") {
if (conflictOnNextCreateOnlyWrite) {
storageVersion += 1;
storage.set(key, {
Expand Down
11 changes: 7 additions & 4 deletions docs/adr/0005-unity-6-5-beta.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
# ADR 0005: Use Unity 6.5 beta (`6000.5.0b7`) instead of Unity 6.0 LTS
# ADR 0005: Use Unity 6.5 beta instead of Unity 6.0 LTS

**Status:** Accepted
**Date:** 2026-05-14
**Deciders:** JOY (sole decision-maker, solo dev)

**Current baseline:** Unity `6000.5.0b8` as of 2026-05-17. The original
decision was made against local Unity Hub build `6000.5.0b7`.

## Context

When initializing the Unity project, the default Editor available in the
Expand All @@ -20,7 +23,7 @@ Two options were on the table:
Designer, Convai, Photon Fusion 2) are tested against this version.
Predictable for a 3-6 month vertical slice timeline. Ecosystem
packages (URP 17.x, Input System) are stable on 6.0.
- **Option B: Stay on Unity 6.5 beta `6000.5.0b7`.** Newer features,
- **Option B: Stay on Unity 6.5 beta.** Newer features,
some performance improvements, but breaking changes between beta
builds (b7 -> b8 -> RC -> GA) are likely; 3rd-party assets may have
un-tested behavior; ecosystem packages may not have stable releases
Expand All @@ -31,7 +34,7 @@ CLI second-pass) was Option A.

## Decision

**Option B - stay on Unity 6.5 beta `6000.5.0b7`.**
**Option B - stay on Unity 6.5 beta.**

JOY explicitly chose to keep the beta install rather than roll back.

Expand Down Expand Up @@ -78,7 +81,7 @@ This is a mutable decision. Re-evaluate if any of the following:

### Mitigations

- Pin the exact build (`6000.5.0b7`) in
- Pin the current exact build (`6000.5.0b8`) in
[.claude/CLAUDE.md](../../.claude/CLAUDE.md) and
[Unity/ProjectSettings/ProjectVersion.txt](../../Unity/ProjectSettings/ProjectVersion.txt)
so anyone reproducing the env knows the target.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,4 +131,5 @@ If JOY is willing to wait but doesn't want to downgrade: pick Option C and we pr
- [ADR 0006: Fusion 2 scratch over template](0006-fusion-2-scratch-over-template.md)
- [GDD 05: Networking Architecture](../design/05-networking-architecture.md)
- Photon Fusion 2.0.12-Stable-1861 (current installed version)
- Unity 6.5 beta `6000.5.0b7` (current installed Editor)
- Unity 6.5 beta `6000.5.0b7` (installed Editor at failure time; project
baseline later moved to `6000.5.0b8`)
2 changes: 1 addition & 1 deletion docs/design/00-game-concept.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ Complete a quest line or dungeon clear; converse with hub-town NPCs (LLM-driven)

| Consideration | Assessment |
| ---- | ---- |
| **Engine** | Unity 6.5 beta (currently `6000.5.0b7`) + URP. JOY chose beta for newest features. |
| **Engine** | Unity 6.5 beta (currently `6000.5.0b8`) + URP. JOY chose beta for newest features. |
| **Networking** | Photon Fusion 2 (Server Mode dedicated for production; Host Mode + Photon Cloud free 20 CCU for dev) |
| **Persistence** | Nakama OSS + Postgres (profile, inventory, quest, NFT lock state, level/stats) |
| **LLM** | Convai phase 1 (NPC dialogue) -> `api.dos.ai` / Go LLM Gateway phase 2 (Haiku 4.5 for NPC chat, Sonnet 4.6 for boss / quest-critical NPCs). Server-side intent validation only. |
Expand Down
2 changes: 1 addition & 1 deletion docs/design/09-pirate-adventure-reference-review.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ Use it as a reference, not as code to copy.

| Area | Pirate Adventure | SECOND SPAWN Current State | Notes |
| ---- | ---- | ---- | ---- |
| Unity version | Unity `2021.3.45f2` | Unity `6000.5.0b7` | Sample must be treated as a pattern source, not imported wholesale. |
| Unity version | Unity `2021.3.45f2` | Unity `6000.5.0b8` | Sample must be treated as a pattern source, not imported wholesale. |
| Fusion version | `2.0.12 Stable 1861` | `2.1.1 Release-Candidate 2037` | API drift is possible. Validate in Unity after each integration step. |
| KCC | Simple KCC addon `2.0.15` DLL | Installed at `Unity/Assets/Photon/FusionAddons/SimpleKCC/` | Strong candidate for next controller prototype branch. |
| Topology | Shared Mode | Server Mode production, Host Mode dev | Do not copy Shared Mode authority assumptions. |
Expand Down
9 changes: 5 additions & 4 deletions docs/setup/agent-handoff.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ Do not touch:

## Current manual JOY actions

1. Add Unity Linux Dedicated Server Build Support for Unity `6000.5.0b7` via
1. Add Unity Linux Dedicated Server Build Support for Unity `6000.5.0b8` via
Unity Hub before dedicated server build work.
2. Import asset store packages in separate passes: Opsive UCC, then Behavior
Designer, then Convai.
Expand Down Expand Up @@ -105,9 +105,10 @@ Do not touch:
- `V`: check voice-session contract
- Voice is a local prototype cue plus text bubble in Unity. Real TTS still
requires server-side ephemeral token minting.
- On 2026-05-16, Cloud Run revision `second-spawn-gateway-00003-779` served
100 percent of traffic. Smoke tests passed for `/readyz`,
`/v1/characters/dev-player/context`, and duplicate memory POST dedupe.
- On 2026-05-17, Cloud Run revision `second-spawn-gateway-00008-cnn` served
100 percent of traffic. Smoke tests passed for `/readyz` and
`/v1/agent/decide` with Unity's current `second_balance_seconds` player
context field.
- On 2026-05-16, CoplayDev MCP Play Mode verification spawned a generated
Hammer Warrior visual with a clean console. Prototype agent input moved the
spawned player from `x=1.50` to `x=14.03`, then cleared control.
Expand Down
25 changes: 7 additions & 18 deletions docs/setup/game-gateway-cloud-run.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,36 +55,25 @@ gcloud run deploy second-spawn-gateway \
--region asia-southeast1 \
--allow-unauthenticated \
--env-vars-file backend/gateway/deploy/cloudrun.env.yaml \
--set-secrets SUPABASE_JWT_SECRET=second-spawn-supabase-jwt-secret:latest,SUPABASE_SERVICE_ROLE_KEY=second-spawn-supabase-service-role-key:latest,ANTHROPIC_API_KEY=second-spawn-anthropic-api-key:latest,OPENAI_API_KEY=second-spawn-openai-api-key:latest,CONVAI_API_KEY=second-spawn-convai-api-key:latest
--set-secrets ANTHROPIC_API_KEY=ANTHROPIC_API_KEY:latest
```

`--allow-unauthenticated` is intentional for the public game endpoint. Application
auth still happens with Supabase JWTs at the gateway level. Do not put Cloud Run
behind IAM auth for normal game clients.
auth happens with Supabase JWTs once the Unity bearer-token path is wired. Do
not put Cloud Run behind IAM auth for normal game clients.

## Secret Creation

Create secrets once, then paste values interactively:

```bash
gcloud secrets create second-spawn-supabase-jwt-secret --replication-policy automatic
gcloud secrets versions add second-spawn-supabase-jwt-secret --data-file -

gcloud secrets create second-spawn-supabase-service-role-key --replication-policy automatic
gcloud secrets versions add second-spawn-supabase-service-role-key --data-file -

gcloud secrets create second-spawn-anthropic-api-key --replication-policy automatic
gcloud secrets versions add second-spawn-anthropic-api-key --data-file -

gcloud secrets create second-spawn-openai-api-key --replication-policy automatic
gcloud secrets versions add second-spawn-openai-api-key --data-file -

gcloud secrets create second-spawn-convai-api-key --replication-policy automatic
gcloud secrets versions add second-spawn-convai-api-key --data-file -
gcloud secrets create ANTHROPIC_API_KEY --replication-policy automatic
gcloud secrets versions add ANTHROPIC_API_KEY --data-file -
```

For the current prototype, provider keys can be omitted only if `GATEWAY_ENV` is
not `production`. Production must have real provider credentials.
not `production`. Production must have real provider credentials and a wired
Supabase bearer-token path before setting `SUPABASE_JWT_SECRET`.

## Smoke Test

Expand Down
Loading