diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index a210707..f58feac 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -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 diff --git a/AGENTS.md b/AGENTS.md index 9e5343a..288329f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 diff --git a/README.md b/README.md index 7558fd2..1179e50 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/Unity/Packages/manifest.json b/Unity/Packages/manifest.json index d880700..1f5221e 100644 --- a/Unity/Packages/manifest.json +++ b/Unity/Packages/manifest.json @@ -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", diff --git a/Unity/Packages/packages-lock.json b/Unity/Packages/packages-lock.json index f718263..b2962c6 100644 --- a/Unity/Packages/packages-lock.json +++ b/Unity/Packages/packages-lock.json @@ -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" diff --git a/Unity/ProjectSettings/ProjectVersion.txt b/Unity/ProjectSettings/ProjectVersion.txt index 94688d7..97aab63 100644 --- a/Unity/ProjectSettings/ProjectVersion.txt +++ b/Unity/ProjectSettings/ProjectVersion.txt @@ -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) diff --git a/backend/gateway/deploy/cloudrun.env.yaml b/backend/gateway/deploy/cloudrun.env.yaml index 8d1797f..26249c6 100644 --- a/backend/gateway/deploy/cloudrun.env.yaml +++ b/backend/gateway/deploy/cloudrun.env.yaml @@ -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" diff --git a/backend/gateway/internal/character/profile.go b/backend/gateway/internal/character/profile.go index 7e5fe3e..7d1dc82 100644 --- a/backend/gateway/internal/character/profile.go +++ b/backend/gateway/internal/character/profile.go @@ -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. @@ -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) diff --git a/backend/gateway/internal/character/store.go b/backend/gateway/internal/character/store.go index 75e4bb5..1a20f32 100644 --- a/backend/gateway/internal/character/store.go +++ b/backend/gateway/internal/character/store.go @@ -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, diff --git a/backend/gateway/internal/server/server_test.go b/backend/gateway/internal/server/server_test.go index 271f2a6..fb6105b 100644 --- a/backend/gateway/internal/server/server_test.go +++ b/backend/gateway/internal/server/server_test.go @@ -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", diff --git a/backend/nakama/modules/index.ts b/backend/nakama/modules/index.ts index d233e6c..3e4d804 100644 --- a/backend/nakama/modules/index.ts +++ b/backend/nakama/modules/index.ts @@ -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 { @@ -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 { @@ -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]); @@ -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) { @@ -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 { @@ -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 || @@ -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, @@ -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]); @@ -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; @@ -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); diff --git a/backend/nakama/tests/supabase_custom_auth.test.mjs b/backend/nakama/tests/supabase_custom_auth.test.mjs index 6f76492..80ee569 100644 --- a/backend/nakama/tests/supabase_custom_auth.test.mjs +++ b/backend/nakama/tests/supabase_custom_auth.test.mjs @@ -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, { diff --git a/docs/adr/0005-unity-6-5-beta.md b/docs/adr/0005-unity-6-5-beta.md index 427d363..b6f3a16 100644 --- a/docs/adr/0005-unity-6-5-beta.md +++ b/docs/adr/0005-unity-6-5-beta.md @@ -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 @@ -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 @@ -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. @@ -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. diff --git a/docs/adr/0007-photon-fusion-2-0-12-unity-6-5-beta-incompat.md b/docs/adr/0007-photon-fusion-2-0-12-unity-6-5-beta-incompat.md index d26acb3..1a69c44 100644 --- a/docs/adr/0007-photon-fusion-2-0-12-unity-6-5-beta-incompat.md +++ b/docs/adr/0007-photon-fusion-2-0-12-unity-6-5-beta-incompat.md @@ -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`) diff --git a/docs/design/00-game-concept.md b/docs/design/00-game-concept.md index 83bea83..98f357e 100644 --- a/docs/design/00-game-concept.md +++ b/docs/design/00-game-concept.md @@ -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. | diff --git a/docs/design/09-pirate-adventure-reference-review.md b/docs/design/09-pirate-adventure-reference-review.md index 9946bfb..675bd05 100644 --- a/docs/design/09-pirate-adventure-reference-review.md +++ b/docs/design/09-pirate-adventure-reference-review.md @@ -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. | diff --git a/docs/setup/agent-handoff.md b/docs/setup/agent-handoff.md index 4f90881..126292d 100644 --- a/docs/setup/agent-handoff.md +++ b/docs/setup/agent-handoff.md @@ -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. @@ -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. diff --git a/docs/setup/game-gateway-cloud-run.md b/docs/setup/game-gateway-cloud-run.md index 8221913..8520b46 100644 --- a/docs/setup/game-gateway-cloud-run.md +++ b/docs/setup/game-gateway-cloud-run.md @@ -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