@nodetool-ai/websocket 0.8.2 → 0.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.nodetool-build-stamp +1 -1
- package/dist/chat-turn-registry.d.ts +2 -1
- package/dist/chat-turn-registry.d.ts.map +1 -1
- package/dist/chat-turn-registry.js +8 -1
- package/dist/chat-turn-registry.js.map +1 -1
- package/dist/collection-api.d.ts.map +1 -1
- package/dist/collection-api.js +3 -5
- package/dist/collection-api.js.map +1 -1
- package/dist/custom-providers.d.ts.map +1 -1
- package/dist/custom-providers.js +6 -1
- package/dist/custom-providers.js.map +1 -1
- package/dist/example-workflows.d.ts.map +1 -1
- package/dist/example-workflows.js +6 -0
- package/dist/example-workflows.js.map +1 -1
- package/dist/fake-runtime.d.ts +41 -6
- package/dist/fake-runtime.d.ts.map +1 -1
- package/dist/fake-runtime.js +207 -13
- package/dist/fake-runtime.js.map +1 -1
- package/dist/file-api.d.ts.map +1 -1
- package/dist/file-api.js +6 -8
- package/dist/file-api.js.map +1 -1
- package/dist/generation-recovery.d.ts.map +1 -1
- package/dist/generation-recovery.js +7 -4
- package/dist/generation-recovery.js.map +1 -1
- package/dist/headless-job-runner.d.ts.map +1 -1
- package/dist/headless-job-runner.js +5 -1
- package/dist/headless-job-runner.js.map +1 -1
- package/dist/http-api.d.ts +3 -1
- package/dist/http-api.d.ts.map +1 -1
- package/dist/http-api.js +51 -9
- package/dist/http-api.js.map +1 -1
- package/dist/http-body-schemas.d.ts +2 -0
- package/dist/http-body-schemas.d.ts.map +1 -1
- package/dist/http-body-schemas.js +8 -1
- package/dist/http-body-schemas.js.map +1 -1
- package/dist/lib/admin.d.ts +7 -0
- package/dist/lib/admin.d.ts.map +1 -0
- package/dist/lib/admin.js +20 -0
- package/dist/lib/admin.js.map +1 -0
- package/dist/lib/app-instances-service.d.ts +35 -0
- package/dist/lib/app-instances-service.d.ts.map +1 -1
- package/dist/lib/app-instances-service.js +3 -4
- package/dist/lib/app-instances-service.js.map +1 -1
- package/dist/lib/app-project-export.d.ts +12 -0
- package/dist/lib/app-project-export.d.ts.map +1 -0
- package/dist/lib/app-project-export.js +42 -0
- package/dist/lib/app-project-export.js.map +1 -0
- package/dist/lib/applications-service.d.ts.map +1 -1
- package/dist/lib/applications-service.js +7 -2
- package/dist/lib/applications-service.js.map +1 -1
- package/dist/lib/asset-export.d.ts +6 -2
- package/dist/lib/asset-export.d.ts.map +1 -1
- package/dist/lib/asset-export.js +15 -7
- package/dist/lib/asset-export.js.map +1 -1
- package/dist/lib/asset-response.d.ts.map +1 -1
- package/dist/lib/asset-response.js +1 -0
- package/dist/lib/asset-response.js.map +1 -1
- package/dist/lib/cloud-profile.d.ts +10 -0
- package/dist/lib/cloud-profile.d.ts.map +1 -0
- package/dist/lib/cloud-profile.js +13 -0
- package/dist/lib/cloud-profile.js.map +1 -0
- package/dist/lib/http-user-id.d.ts +2 -0
- package/dist/lib/http-user-id.d.ts.map +1 -0
- package/dist/lib/http-user-id.js +4 -0
- package/dist/lib/http-user-id.js.map +1 -0
- package/dist/lib/json-body.d.ts +11 -0
- package/dist/lib/json-body.d.ts.map +1 -0
- package/dist/lib/json-body.js +13 -0
- package/dist/lib/json-body.js.map +1 -0
- package/dist/lib/public-routes.d.ts +15 -0
- package/dist/lib/public-routes.d.ts.map +1 -1
- package/dist/lib/public-routes.js +27 -0
- package/dist/lib/public-routes.js.map +1 -1
- package/dist/lib/public-workflow-share.d.ts +12 -1
- package/dist/lib/public-workflow-share.d.ts.map +1 -1
- package/dist/lib/public-workflow-share.js +45 -2
- package/dist/lib/public-workflow-share.js.map +1 -1
- package/dist/lib/python-metadata-roots.d.ts +27 -0
- package/dist/lib/python-metadata-roots.d.ts.map +1 -0
- package/dist/lib/python-metadata-roots.js +92 -0
- package/dist/lib/python-metadata-roots.js.map +1 -0
- package/dist/lib/runtime-detection.d.ts +1 -1
- package/dist/lib/runtime-detection.d.ts.map +1 -1
- package/dist/lib/runtime-detection.js +2 -1
- package/dist/lib/runtime-detection.js.map +1 -1
- package/dist/lib/server-auth-token.d.ts +18 -0
- package/dist/lib/server-auth-token.d.ts.map +1 -0
- package/dist/lib/server-auth-token.js +28 -0
- package/dist/lib/server-auth-token.js.map +1 -0
- package/dist/lib/timeline-bundle.d.ts +11 -0
- package/dist/lib/timeline-bundle.d.ts.map +1 -1
- package/dist/lib/timeline-bundle.js +22 -6
- package/dist/lib/timeline-bundle.js.map +1 -1
- package/dist/lib/timeline-media-export.d.ts +18 -0
- package/dist/lib/timeline-media-export.d.ts.map +1 -0
- package/dist/lib/timeline-media-export.js +66 -0
- package/dist/lib/timeline-media-export.js.map +1 -0
- package/dist/lib/timeline-premiere-import.d.ts +24 -0
- package/dist/lib/timeline-premiere-import.d.ts.map +1 -0
- package/dist/lib/timeline-premiere-import.js +102 -0
- package/dist/lib/timeline-premiere-import.js.map +1 -0
- package/dist/lib/timeline-premiere.d.ts +38 -0
- package/dist/lib/timeline-premiere.d.ts.map +1 -0
- package/dist/lib/timeline-premiere.js +87 -0
- package/dist/lib/timeline-premiere.js.map +1 -0
- package/dist/mcp-agent-tools.d.ts.map +1 -1
- package/dist/mcp-agent-tools.js +2 -0
- package/dist/mcp-agent-tools.js.map +1 -1
- package/dist/mcp-server.d.ts +7 -0
- package/dist/mcp-server.d.ts.map +1 -1
- package/dist/mcp-server.js +163 -16
- package/dist/mcp-server.js.map +1 -1
- package/dist/mcp-skill-prompts.d.ts +23 -0
- package/dist/mcp-skill-prompts.d.ts.map +1 -0
- package/dist/mcp-skill-prompts.js +91 -0
- package/dist/mcp-skill-prompts.js.map +1 -0
- package/dist/mcp-tool-deps.d.ts.map +1 -1
- package/dist/mcp-tool-deps.js +8 -1
- package/dist/mcp-tool-deps.js.map +1 -1
- package/dist/models-api.d.ts.map +1 -1
- package/dist/models-api.js +6 -2
- package/dist/models-api.js.map +1 -1
- package/dist/oauth-api.d.ts.map +1 -1
- package/dist/oauth-api.js +45 -23
- package/dist/oauth-api.js.map +1 -1
- package/dist/openai-api.d.ts.map +1 -1
- package/dist/openai-api.js +10 -2
- package/dist/openai-api.js.map +1 -1
- package/dist/plugins/websocket.d.ts.map +1 -1
- package/dist/plugins/websocket.js +7 -2
- package/dist/plugins/websocket.js.map +1 -1
- package/dist/routes/applications.d.ts +1 -0
- package/dist/routes/applications.d.ts.map +1 -1
- package/dist/routes/applications.js +24 -0
- package/dist/routes/applications.js.map +1 -1
- package/dist/routes/assets.d.ts.map +1 -1
- package/dist/routes/assets.js +31 -41
- package/dist/routes/assets.js.map +1 -1
- package/dist/routes/config.d.ts.map +1 -1
- package/dist/routes/config.js +6 -0
- package/dist/routes/config.js.map +1 -1
- package/dist/routes/documents.d.ts.map +1 -1
- package/dist/routes/documents.js +24 -0
- package/dist/routes/documents.js.map +1 -1
- package/dist/routes/fal-pricing-estimate.d.ts.map +1 -1
- package/dist/routes/fal-pricing-estimate.js +2 -0
- package/dist/routes/fal-pricing-estimate.js.map +1 -1
- package/dist/routes/files.d.ts.map +1 -1
- package/dist/routes/files.js +2 -0
- package/dist/routes/files.js.map +1 -1
- package/dist/routes/games.d.ts +38 -0
- package/dist/routes/games.d.ts.map +1 -0
- package/dist/routes/games.js +158 -0
- package/dist/routes/games.js.map +1 -0
- package/dist/routes/integrations.d.ts.map +1 -1
- package/dist/routes/integrations.js +22 -26
- package/dist/routes/integrations.js.map +1 -1
- package/dist/routes/js-scripts.d.ts +1 -1
- package/dist/routes/js-scripts.d.ts.map +1 -1
- package/dist/routes/js-scripts.js +1 -1
- package/dist/routes/js-scripts.js.map +1 -1
- package/dist/routes/nodes.d.ts.map +1 -1
- package/dist/routes/nodes.js +2 -0
- package/dist/routes/nodes.js.map +1 -1
- package/dist/routes/storage.d.ts.map +1 -1
- package/dist/routes/storage.js +2 -0
- package/dist/routes/storage.js.map +1 -1
- package/dist/routes/storyboards.d.ts.map +1 -1
- package/dist/routes/storyboards.js +6 -3
- package/dist/routes/storyboards.js.map +1 -1
- package/dist/routes/timelines.d.ts +8 -4
- package/dist/routes/timelines.d.ts.map +1 -1
- package/dist/routes/timelines.js +209 -16
- package/dist/routes/timelines.js.map +1 -1
- package/dist/screenshot-docs-seed.d.ts +24 -0
- package/dist/screenshot-docs-seed.d.ts.map +1 -0
- package/dist/screenshot-docs-seed.js +414 -0
- package/dist/screenshot-docs-seed.js.map +1 -0
- package/dist/screenshot-projects.d.ts.map +1 -1
- package/dist/screenshot-projects.js +9 -9
- package/dist/screenshot-projects.js.map +1 -1
- package/dist/screenshot-server.js +111 -16
- package/dist/screenshot-server.js.map +1 -1
- package/dist/server.js +87 -90
- package/dist/server.js.map +1 -1
- package/dist/session/chat-turn.d.ts +10 -0
- package/dist/session/chat-turn.d.ts.map +1 -1
- package/dist/session/chat-turn.js +68 -10
- package/dist/session/chat-turn.js.map +1 -1
- package/dist/session/commands.d.ts +14 -0
- package/dist/session/commands.d.ts.map +1 -1
- package/dist/session/commands.js +113 -14
- package/dist/session/commands.js.map +1 -1
- package/dist/session/inference.d.ts +25 -2
- package/dist/session/inference.d.ts.map +1 -1
- package/dist/session/inference.js +86 -8
- package/dist/session/inference.js.map +1 -1
- package/dist/session/job-execution.d.ts +0 -1
- package/dist/session/job-execution.d.ts.map +1 -1
- package/dist/session/job-execution.js +34 -37
- package/dist/session/job-execution.js.map +1 -1
- package/dist/storage-api.d.ts +0 -4
- package/dist/storage-api.d.ts.map +1 -1
- package/dist/storage-api.js +3 -26
- package/dist/storage-api.js.map +1 -1
- package/dist/test-ui-server.d.ts +11 -0
- package/dist/test-ui-server.d.ts.map +1 -1
- package/dist/test-ui-server.js +63 -3
- package/dist/test-ui-server.js.map +1 -1
- package/dist/triggers/dispatcher.d.ts.map +1 -1
- package/dist/triggers/dispatcher.js +24 -4
- package/dist/triggers/dispatcher.js.map +1 -1
- package/dist/triggers/file-watch.d.ts.map +1 -1
- package/dist/triggers/file-watch.js +35 -19
- package/dist/triggers/file-watch.js.map +1 -1
- package/dist/triggers/scheduler.d.ts.map +1 -1
- package/dist/triggers/scheduler.js +7 -5
- package/dist/triggers/scheduler.js.map +1 -1
- package/dist/trpc/error-formatter.d.ts.map +1 -1
- package/dist/trpc/error-formatter.js +2 -1
- package/dist/trpc/error-formatter.js.map +1 -1
- package/dist/trpc/routers/app-instances.d.ts +63 -0
- package/dist/trpc/routers/app-instances.d.ts.map +1 -1
- package/dist/trpc/routers/app-runs.d.ts +28 -0
- package/dist/trpc/routers/app-runs.d.ts.map +1 -1
- package/dist/trpc/routers/applications.d.ts +7 -0
- package/dist/trpc/routers/applications.d.ts.map +1 -1
- package/dist/trpc/routers/assets.d.ts +8 -0
- package/dist/trpc/routers/assets.d.ts.map +1 -1
- package/dist/trpc/routers/assets.js +16 -5
- package/dist/trpc/routers/assets.js.map +1 -1
- package/dist/trpc/routers/collections.d.ts.map +1 -1
- package/dist/trpc/routers/collections.js +16 -10
- package/dist/trpc/routers/collections.js.map +1 -1
- package/dist/trpc/routers/credits.d.ts.map +1 -1
- package/dist/trpc/routers/credits.js +11 -1
- package/dist/trpc/routers/credits.js.map +1 -1
- package/dist/trpc/routers/extension.d.ts +1 -1
- package/dist/trpc/routers/extension.d.ts.map +1 -1
- package/dist/trpc/routers/extension.js +8 -4
- package/dist/trpc/routers/extension.js.map +1 -1
- package/dist/trpc/routers/gameDraftHistory.d.ts +1 -2
- package/dist/trpc/routers/gameDraftHistory.d.ts.map +1 -1
- package/dist/trpc/routers/gameDraftHistory.js +1 -1
- package/dist/trpc/routers/gameDraftHistory.js.map +1 -1
- package/dist/trpc/routers/games.d.ts +40822 -5154
- package/dist/trpc/routers/games.d.ts.map +1 -1
- package/dist/trpc/routers/games.js +317 -140
- package/dist/trpc/routers/games.js.map +1 -1
- package/dist/trpc/routers/jobs.d.ts +7 -0
- package/dist/trpc/routers/jobs.d.ts.map +1 -1
- package/dist/trpc/routers/jobs.js +5 -3
- package/dist/trpc/routers/jobs.js.map +1 -1
- package/dist/trpc/routers/js-scripts.d.ts.map +1 -1
- package/dist/trpc/routers/js-scripts.js +18 -2
- package/dist/trpc/routers/js-scripts.js.map +1 -1
- package/dist/trpc/routers/messages.d.ts +10 -0
- package/dist/trpc/routers/messages.d.ts.map +1 -1
- package/dist/trpc/routers/messages.js +16 -2
- package/dist/trpc/routers/messages.js.map +1 -1
- package/dist/trpc/routers/models.d.ts.map +1 -1
- package/dist/trpc/routers/models.js +20 -30
- package/dist/trpc/routers/models.js.map +1 -1
- package/dist/trpc/routers/packs.d.ts.map +1 -1
- package/dist/trpc/routers/packs.js +18 -4
- package/dist/trpc/routers/packs.js.map +1 -1
- package/dist/trpc/routers/projects.d.ts +66 -2
- package/dist/trpc/routers/projects.d.ts.map +1 -1
- package/dist/trpc/routers/projects.js +19 -2
- package/dist/trpc/routers/projects.js.map +1 -1
- package/dist/trpc/routers/settings.d.ts.map +1 -1
- package/dist/trpc/routers/settings.js +30 -5
- package/dist/trpc/routers/settings.js.map +1 -1
- package/dist/trpc/routers/sketch.d.ts +30 -0
- package/dist/trpc/routers/sketch.d.ts.map +1 -1
- package/dist/trpc/routers/sketch.js +2 -2
- package/dist/trpc/routers/sketch.js.map +1 -1
- package/dist/trpc/routers/storyboards.d.ts +171 -1
- package/dist/trpc/routers/storyboards.d.ts.map +1 -1
- package/dist/trpc/routers/storyboards.js +8 -5
- package/dist/trpc/routers/storyboards.js.map +1 -1
- package/dist/trpc/routers/threads.d.ts.map +1 -1
- package/dist/trpc/routers/threads.js +8 -2
- package/dist/trpc/routers/threads.js.map +1 -1
- package/dist/trpc/routers/timeline.d.ts +15 -0
- package/dist/trpc/routers/timeline.d.ts.map +1 -1
- package/dist/trpc/routers/timeline.js +15 -2
- package/dist/trpc/routers/timeline.js.map +1 -1
- package/dist/trpc/routers/triggers.d.ts.map +1 -1
- package/dist/trpc/routers/triggers.js +18 -3
- package/dist/trpc/routers/triggers.js.map +1 -1
- package/dist/trpc/routers/users.d.ts +3 -2
- package/dist/trpc/routers/users.d.ts.map +1 -1
- package/dist/trpc/routers/users.js +25 -27
- package/dist/trpc/routers/users.js.map +1 -1
- package/dist/trpc/routers/worker.d.ts.map +1 -1
- package/dist/trpc/routers/worker.js +37 -14
- package/dist/trpc/routers/worker.js.map +1 -1
- package/dist/trpc/routers/workflows.d.ts +146 -0
- package/dist/trpc/routers/workflows.d.ts.map +1 -1
- package/dist/trpc/routers/workflows.js +52 -13
- package/dist/trpc/routers/workflows.js.map +1 -1
- package/dist/trpc/sandbox-coverage.d.ts.map +1 -1
- package/dist/trpc/sandbox-coverage.js +29 -0
- package/dist/trpc/sandbox-coverage.js.map +1 -1
- package/dist/websocket-client-session.d.ts +2 -2
- package/dist/websocket-client-session.d.ts.map +1 -1
- package/dist/websocket-client-session.js +9 -3
- package/dist/websocket-client-session.js.map +1 -1
- package/dist/worker-settings-env.d.ts +23 -0
- package/dist/worker-settings-env.d.ts.map +1 -0
- package/dist/worker-settings-env.js +47 -0
- package/dist/worker-settings-env.js.map +1 -0
- package/dist/workflow-interface-service.d.ts +7 -0
- package/dist/workflow-interface-service.d.ts.map +1 -1
- package/package.json +44 -42
|
@@ -3,23 +3,32 @@ import { join } from "node:path";
|
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import sharp from "sharp";
|
|
5
5
|
import { createLogger, getManagedWorkspaceDir, managedWorkspaceKey, workspaceStorageKind } from "@nodetool-ai/config";
|
|
6
|
-
import { AmbiguousGameIdError, InvalidGameDocumentError, Asset, Game, Project, Workspace } from "@nodetool-ai/models";
|
|
7
|
-
import { exampleGameSummary, anyGameAssetBinding, gameAssetBinding, gameAssetBinding3D, gamePreparedCollider3D, gameModelImportSettings3D, parseGameDocument, anyGameDocument as gameDocument, installExampleGameInput } from "@nodetool-ai/protocol";
|
|
6
|
+
import { AmbiguousGameIdError, InvalidGameDocumentError, MissingGameDraftSourceError, Asset, Game, Project, Workspace } from "@nodetool-ai/models";
|
|
7
|
+
import { exampleGameSummary, anyGameAssetBinding, gameAssetBinding, gameAssetBinding3D, gameAssetSiblingRebinds, gamePreparedCollider3D, gameModelImportSettings3D, parseGameDocument, anyGameDocument as gameDocument, installExampleGameInput, stagedGameCandidateRecord, stagedGameCandidateRecordPath } from "@nodetool-ai/protocol";
|
|
8
8
|
import { createTopDownRoomGame, createNative3DGame, anyGameDocumentOp as gameDocumentOp, GameOpError, decodePreparedGameCollider3D, trackGameAuthoringEdits, validateAnyGame } from "@nodetool-ai/game-runtime";
|
|
9
9
|
import { normalizeGameModel3D, prepareGameModelBinding3D } from "@nodetool-ai/game-renderer/preparation3d";
|
|
10
|
-
import { createCapabilityRun, gateFromContext } from "@nodetool-ai/agents";
|
|
10
|
+
import { contextSecretAvailability, createCapabilityRun, gateFromContext } from "@nodetool-ai/agents";
|
|
11
11
|
import { gameAuthoringCandidate, gameAuthoringProgram, gameAuthoringConflict } from "@nodetool-ai/protocol";
|
|
12
12
|
import { PERMISSION_GATE_CONTEXT_KEY, ProcessingContext, headlessGate } from "@nodetool-ai/runtime";
|
|
13
13
|
import { getAssetAdapter } from "../../lib/storage.js";
|
|
14
14
|
import { getAssetStorageKey, retrieveAssetBytes } from "../../lib/asset-paths.js";
|
|
15
15
|
import { getExampleGameBundle, installExampleGameAssets, listExampleGames } from "../../lib/example-games.js";
|
|
16
16
|
import { workspaceFromRow } from "../../lib/workflow-workspace.js";
|
|
17
|
+
import { loadConfiguredProviders } from "../../configured-providers.js";
|
|
18
|
+
import { createRuntimeContext } from "../../session/model-interfaces.js";
|
|
17
19
|
import { ApiErrorCode } from "../../error-codes.js";
|
|
18
20
|
import { router } from "../index.js";
|
|
19
21
|
import { protectedProcedure } from "../middleware.js";
|
|
20
22
|
import { throwApiError } from "../error-formatter.js";
|
|
21
23
|
import { boundedGameDraftHistory, MAX_GAME_DRAFT_OPS } from "./gameDraftHistory.js";
|
|
22
24
|
const log = createLogger("nodetool.games");
|
|
25
|
+
const gameProcedure = protectedProcedure.use(async ({ next }) => {
|
|
26
|
+
const result = await next();
|
|
27
|
+
if (!result.ok && result.error.cause instanceof MissingGameDraftSourceError) {
|
|
28
|
+
throwApiError(ApiErrorCode.SERVICE_UNAVAILABLE, result.error.cause.message, "PRECONDITION_FAILED");
|
|
29
|
+
}
|
|
30
|
+
return result;
|
|
31
|
+
});
|
|
23
32
|
const idInput = z.object({ id: z.string() });
|
|
24
33
|
const gameInfo = z.object({
|
|
25
34
|
id: z.string(),
|
|
@@ -41,6 +50,27 @@ const authoringPreview = z.object({
|
|
|
41
50
|
changed_dependencies: z.array(z.string()),
|
|
42
51
|
restart_required: z.boolean()
|
|
43
52
|
});
|
|
53
|
+
const assetBrowserInfo = z.object({
|
|
54
|
+
dimension: z.enum(["2d", "3d"]),
|
|
55
|
+
draft_updated_at: z.string(),
|
|
56
|
+
candidate_workspace_id: z.string(),
|
|
57
|
+
candidates: z.array(z.object({
|
|
58
|
+
digest: z.string(), extension: z.string(), media_kind: z.enum(["image", "audio", "font", "model", "collider"]), path: z.string(),
|
|
59
|
+
size: z.number(), modified_at: z.string(), bound_slots: z.array(z.string()), slot: z.string().optional(), prompt: z.string().optional(),
|
|
60
|
+
source: z.string().optional(), recorded: z.boolean()
|
|
61
|
+
})),
|
|
62
|
+
slot_requests: z.record(z.string(), z.object({ kind: z.string(), prompt: z.string().optional(),
|
|
63
|
+
preparation: z.record(z.string(), z.unknown()).optional(), source: z.string() }))
|
|
64
|
+
});
|
|
65
|
+
const assetFrame = z.object({ x: z.number(), y: z.number(), width: z.number(), height: z.number() });
|
|
66
|
+
const generatedAssetResult = z.object({
|
|
67
|
+
error: z.string().optional(),
|
|
68
|
+
binding: anyGameAssetBinding.optional(),
|
|
69
|
+
frames: z.array(assetFrame).optional(),
|
|
70
|
+
tiles: z.array(z.object({ mask: z.number().int(), frame: assetFrame })).optional()
|
|
71
|
+
}).loose();
|
|
72
|
+
const stagedCandidateBinding = z.object({ error: z.string().optional(), binding: anyGameAssetBinding.optional() }).loose();
|
|
73
|
+
const generatedAsset = gameWithDocument.extend({ stagedDigest: z.string().nullable(), installed: z.boolean() });
|
|
44
74
|
const gameRevisionInfo = z.object({ revision: z.string(), modifiedAt: z.number(), current: z.boolean(), message: z.string().nullable() });
|
|
45
75
|
function info(game) {
|
|
46
76
|
return {
|
|
@@ -63,8 +93,18 @@ function authoringRun(userId) {
|
|
|
63
93
|
context.set(PERMISSION_GATE_CONTEXT_KEY, headlessGate("game.authoring"));
|
|
64
94
|
return createCapabilityRun({ context, gate: gateFromContext(context, "game.authoring") });
|
|
65
95
|
}
|
|
96
|
+
/** A run that can reach the user's configured media providers, for editor-initiated asset generation. */
|
|
97
|
+
async function assetGenerationRun(userId, game, workspace) {
|
|
98
|
+
const context = createRuntimeContext({ jobId: `game-asset-${randomUUID()}`, userId, projectId: game.project_id, workspace });
|
|
99
|
+
context.set(PERMISSION_GATE_CONTEXT_KEY, headlessGate("game.assets"));
|
|
100
|
+
return createCapabilityRun({ context, gate: gateFromContext(context, "game.assets"), projectId: game.project_id,
|
|
101
|
+
availableSecrets: contextSecretAvailability(context), providers: await loadConfiguredProviders(userId) });
|
|
102
|
+
}
|
|
66
103
|
function checkAuthoringResult(result) {
|
|
67
104
|
if (result !== null && typeof result === "object" && "error" in result && typeof result.error === "string") {
|
|
105
|
+
if ("code" in result && result.code === "game_draft_source_unavailable") {
|
|
106
|
+
throwApiError(ApiErrorCode.SERVICE_UNAVAILABLE, result.error, "PRECONDITION_FAILED");
|
|
107
|
+
}
|
|
68
108
|
const code = /stale|concurrent|changed since/i.test(result.error)
|
|
69
109
|
? ApiErrorCode.ALREADY_EXISTS : ApiErrorCode.INVALID_INPUT;
|
|
70
110
|
throwApiError(code, result.error);
|
|
@@ -100,7 +140,8 @@ async function gameWorkspace(userId, game) {
|
|
|
100
140
|
}
|
|
101
141
|
return workspace;
|
|
102
142
|
}
|
|
103
|
-
|
|
143
|
+
const CONCURRENT_DRAFT_EDIT = "Game draft was modified concurrently";
|
|
144
|
+
async function persistGameAsset(userId, game, workspace, draftUpdatedAt, slot, binding, bytes, extension, contentType, siblingBindings) {
|
|
104
145
|
await workspace.write(`${game.source_root}/assets/${binding.digest}.${extension}`, bytes, contentType);
|
|
105
146
|
const installed = await Asset.create({
|
|
106
147
|
user_id: userId, project_id: game.project_id, parent_id: userId,
|
|
@@ -116,9 +157,12 @@ async function persistGameAsset(userId, game, workspace, draftUpdatedAt, slot, b
|
|
|
116
157
|
try {
|
|
117
158
|
uri = await storage.store(getAssetStorageKey(userId, installed.id, contentType), bytes, contentType);
|
|
118
159
|
const op = gameDocumentOp.parse({ op: "bind_asset", slot, binding: { ...binding, assetId: installed.id } });
|
|
119
|
-
const
|
|
160
|
+
const parent = siblingBindings ? gameAssetBinding.safeParse({ ...binding, assetId: installed.id }) : null;
|
|
161
|
+
const siblings = siblingBindings && parent?.success
|
|
162
|
+
? Object.entries(siblingBindings(parent.data)).map(([sibling, siblingBinding]) => gameDocumentOp.parse({ op: "bind_asset", slot: sibling, binding: siblingBinding })) : [];
|
|
163
|
+
const saved = await Game.updateDraft(userId, game.id, draftUpdatedAt, [op, ...siblings], workspace, { actor: "user" });
|
|
120
164
|
if (!saved) {
|
|
121
|
-
throwApiError(ApiErrorCode.ALREADY_EXISTS,
|
|
165
|
+
throwApiError(ApiErrorCode.ALREADY_EXISTS, CONCURRENT_DRAFT_EDIT);
|
|
122
166
|
}
|
|
123
167
|
return { game: info(saved.game), document: saved.document };
|
|
124
168
|
}
|
|
@@ -279,8 +323,167 @@ async function createGame(userId, projectId, name, source, dimension = "2d") {
|
|
|
279
323
|
throwApiError(ApiErrorCode.ALREADY_EXISTS, "Game already exists");
|
|
280
324
|
return { game: info(game), document };
|
|
281
325
|
}
|
|
326
|
+
const installCandidateInput = idInput.extend({
|
|
327
|
+
baseRevision: z.string().optional(),
|
|
328
|
+
baseUpdatedAt: z.string().optional(),
|
|
329
|
+
slot: z.string().min(1),
|
|
330
|
+
candidateWorkspaceId: z.string().optional(),
|
|
331
|
+
binding: anyGameAssetBinding
|
|
332
|
+
});
|
|
333
|
+
function siblingBindingsFor(slot, assets, options) {
|
|
334
|
+
const { frames, tiles } = options;
|
|
335
|
+
if (frames?.length) {
|
|
336
|
+
return (installed) => Object.fromEntries(frames.map((frame, index) => [`${slot}.frame.${index}`, { ...installed, frame }]));
|
|
337
|
+
}
|
|
338
|
+
if (tiles?.length) {
|
|
339
|
+
return (installed) => Object.fromEntries(tiles.map((tile) => [`${slot}.tile.${tile.mask}`, { ...installed, frame: tile.frame }]));
|
|
340
|
+
}
|
|
341
|
+
if (!options.replaceInPlace) {
|
|
342
|
+
return undefined;
|
|
343
|
+
}
|
|
344
|
+
return (installed) => Object.fromEntries(gameAssetSiblingRebinds(assets, slot, installed).ops.map((op) => [op.slot, op.binding]));
|
|
345
|
+
}
|
|
346
|
+
/** Install a staged candidate's bytes into the draft, with the sibling bindings `options` names. */
|
|
347
|
+
async function installCandidateBinding(userId, input, options = {}) {
|
|
348
|
+
const ctx = { userId };
|
|
349
|
+
const game = await ownedGame(ctx.userId, input.id);
|
|
350
|
+
const workspace = await gameWorkspace(ctx.userId, game);
|
|
351
|
+
const candidateRow = input.candidateWorkspaceId
|
|
352
|
+
? await Workspace.find(ctx.userId, input.candidateWorkspaceId)
|
|
353
|
+
: null;
|
|
354
|
+
if (input.candidateWorkspaceId && (!candidateRow || candidateRow.project_id !== game.project_id)) {
|
|
355
|
+
throwApiError(ApiErrorCode.NOT_FOUND, "Candidate workspace not found");
|
|
356
|
+
}
|
|
357
|
+
const candidateWorkspace = candidateRow ? workspaceFromRow(candidateRow) : workspace;
|
|
358
|
+
if (!candidateWorkspace) {
|
|
359
|
+
throwApiError(ApiErrorCode.SERVICE_UNAVAILABLE, "Candidate workspace storage is unavailable");
|
|
360
|
+
}
|
|
361
|
+
const digest = input.binding.digest;
|
|
362
|
+
if (!/^[a-f0-9]{64}$/.test(digest)) {
|
|
363
|
+
throwApiError(ApiErrorCode.INVALID_INPUT, "Invalid candidate digest");
|
|
364
|
+
}
|
|
365
|
+
if (input.binding.mediaKind === "font" && !input.binding.fontFormat) {
|
|
366
|
+
throwApiError(ApiErrorCode.INVALID_INPUT, "Font binding needs a TrueType or OpenType format");
|
|
367
|
+
}
|
|
368
|
+
if (input.binding.mediaKind === "hdri") {
|
|
369
|
+
throwApiError(ApiErrorCode.INVALID_INPUT, "HDRI candidates cannot be installed until HDRI preparation verifies their bytes and dimensions");
|
|
370
|
+
}
|
|
371
|
+
const formats = input.binding.mediaKind === "model"
|
|
372
|
+
? [["glb", "model/gltf-binary"]]
|
|
373
|
+
: input.binding.mediaKind === "collider"
|
|
374
|
+
? [["json", "application/json"]]
|
|
375
|
+
: input.binding.mediaKind === "audio"
|
|
376
|
+
? [["wav", "audio/wav"], ["mp3", "audio/mpeg"], ["ogg", "audio/ogg"]]
|
|
377
|
+
: input.binding.mediaKind === "font"
|
|
378
|
+
? [[input.binding.fontFormat, `font/${input.binding.fontFormat}`]]
|
|
379
|
+
: [["png", "image/png"], ["jpg", "image/jpeg"], ["webp", "image/webp"]];
|
|
380
|
+
let candidate = null;
|
|
381
|
+
for (const [extension, contentType] of formats) {
|
|
382
|
+
if (!extension || !contentType)
|
|
383
|
+
continue;
|
|
384
|
+
const path = `${game.source_root}/assets/${digest}.${extension}`;
|
|
385
|
+
const bytes = await candidateWorkspace.read(path);
|
|
386
|
+
if (bytes) {
|
|
387
|
+
candidate = { extension, contentType, bytes };
|
|
388
|
+
break;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
if (!candidate || createHash("sha256").update(candidate.bytes).digest("hex") !== digest) {
|
|
392
|
+
throwApiError(ApiErrorCode.INVALID_INPUT, "Candidate asset is missing or changed");
|
|
393
|
+
}
|
|
394
|
+
const { extension, contentType, bytes } = candidate;
|
|
395
|
+
let binding = input.binding;
|
|
396
|
+
if (binding.mediaKind === "model") {
|
|
397
|
+
const prepared = await prepareGameModelBinding3D(bytes, { assetId: binding.assetId, expectedDigest: digest });
|
|
398
|
+
if (!prepared.ok) {
|
|
399
|
+
throwApiError(ApiErrorCode.INVALID_INPUT, prepared.diagnostics.map((issue) => issue.message).join("; "));
|
|
400
|
+
}
|
|
401
|
+
const sourceAsset = binding.sourceAssetId ? await Asset.find(ctx.userId, binding.sourceAssetId) : null;
|
|
402
|
+
if (binding.sourceAssetId && !sourceAsset) {
|
|
403
|
+
throwApiError(ApiErrorCode.INVALID_INPUT, "Model source asset is not owned");
|
|
404
|
+
}
|
|
405
|
+
const canonical = { ...prepared.binding, required: binding.required };
|
|
406
|
+
if (binding.provenance) {
|
|
407
|
+
canonical.provenance = binding.provenance;
|
|
408
|
+
}
|
|
409
|
+
if (binding.importSettings) {
|
|
410
|
+
canonical.importSettings = binding.importSettings;
|
|
411
|
+
}
|
|
412
|
+
if (binding.sourceDigest) {
|
|
413
|
+
canonical.sourceDigest = binding.sourceDigest;
|
|
414
|
+
}
|
|
415
|
+
if (sourceAsset) {
|
|
416
|
+
canonical.sourceAssetId = sourceAsset.id;
|
|
417
|
+
}
|
|
418
|
+
binding = canonical;
|
|
419
|
+
}
|
|
420
|
+
else if (binding.mediaKind === "collider") {
|
|
421
|
+
let geometry;
|
|
422
|
+
try {
|
|
423
|
+
geometry = JSON.parse(new TextDecoder().decode(bytes));
|
|
424
|
+
}
|
|
425
|
+
catch {
|
|
426
|
+
throwApiError(ApiErrorCode.INVALID_INPUT, "Prepared collider is not valid JSON");
|
|
427
|
+
}
|
|
428
|
+
const parsed = gamePreparedCollider3D.safeParse(geometry);
|
|
429
|
+
if (!parsed.success || binding.shape === "triangleMesh" && !parsed.data.indices?.length) {
|
|
430
|
+
throwApiError(ApiErrorCode.INVALID_INPUT, "Prepared collider has invalid vertices or triangle indices");
|
|
431
|
+
}
|
|
432
|
+
const { vertices, indices } = parsed.data;
|
|
433
|
+
const minimum = { x: Infinity, y: Infinity, z: Infinity };
|
|
434
|
+
const maximum = { x: -Infinity, y: -Infinity, z: -Infinity };
|
|
435
|
+
for (let index = 0; index < vertices.length; index += 3) {
|
|
436
|
+
minimum.x = Math.min(minimum.x, vertices[index]);
|
|
437
|
+
maximum.x = Math.max(maximum.x, vertices[index]);
|
|
438
|
+
minimum.y = Math.min(minimum.y, vertices[index + 1]);
|
|
439
|
+
maximum.y = Math.max(maximum.y, vertices[index + 1]);
|
|
440
|
+
minimum.z = Math.min(minimum.z, vertices[index + 2]);
|
|
441
|
+
maximum.z = Math.max(maximum.z, vertices[index + 2]);
|
|
442
|
+
}
|
|
443
|
+
binding = { ...binding, preparationVersion: "1", bounds: { min: minimum, max: maximum }, vertices: vertices.length / 3, triangles: (indices?.length ?? 0) / 3 };
|
|
444
|
+
try {
|
|
445
|
+
await decodePreparedGameCollider3D(bytes, gameAssetBinding3D.options[1].parse(binding));
|
|
446
|
+
}
|
|
447
|
+
catch (error) {
|
|
448
|
+
throwApiError(ApiErrorCode.INVALID_INPUT, error instanceof Error ? error.message : "Prepared collider is invalid");
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
if (input.baseRevision && game.current_revision !== input.baseRevision) {
|
|
452
|
+
throwApiError(ApiErrorCode.ALREADY_EXISTS, "Game was modified concurrently");
|
|
453
|
+
}
|
|
454
|
+
const draft = await Game.readDraft(ctx.userId, game.id, workspace);
|
|
455
|
+
if (!draft)
|
|
456
|
+
throwApiError(ApiErrorCode.NOT_FOUND, "Game draft not found");
|
|
457
|
+
if (input.baseUpdatedAt && draft.game.draft_updated_at !== input.baseUpdatedAt) {
|
|
458
|
+
throwApiError(ApiErrorCode.ALREADY_EXISTS, "Game draft was modified concurrently");
|
|
459
|
+
}
|
|
460
|
+
const targetBinding = draft.document.schemaVersion === 3 ? gameAssetBinding3D.safeParse(binding) :
|
|
461
|
+
gameAssetBinding.safeParse(binding);
|
|
462
|
+
if (!targetBinding.success || draft.document.schemaVersion !== 3 && (binding.mediaKind === "model" || binding.mediaKind === "collider")) {
|
|
463
|
+
throwApiError(ApiErrorCode.INVALID_INPUT, "Asset binding does not match the game dimension");
|
|
464
|
+
}
|
|
465
|
+
return persistGameAsset(ctx.userId, game, workspace, draft.game.draft_updated_at, input.slot, targetBinding.data, bytes, extension, contentType, draft.document.schemaVersion !== 3 ? siblingBindingsFor(input.slot, draft.document.assets, options) : undefined);
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Install into whichever draft is current when the bytes are ready. An editor
|
|
469
|
+
* session saves while a generation runs, so a slot binding must not depend on
|
|
470
|
+
* the draft token the request started from. The write itself stays a
|
|
471
|
+
* compare-and-swap, retried when a save lands between the read and the write.
|
|
472
|
+
*/
|
|
473
|
+
async function installOnLatestDraft(userId, input, options) {
|
|
474
|
+
for (let attempt = 1;; attempt++) {
|
|
475
|
+
try {
|
|
476
|
+
return await installCandidateBinding(userId, input, options);
|
|
477
|
+
}
|
|
478
|
+
catch (error) {
|
|
479
|
+
if (attempt >= 3 || !(error instanceof Error) || error.message !== CONCURRENT_DRAFT_EDIT) {
|
|
480
|
+
throw error;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
282
485
|
export const gamesRouter = router({
|
|
283
|
-
list:
|
|
486
|
+
list: gameProcedure
|
|
284
487
|
.input(z.object({ projectId: z.string() }))
|
|
285
488
|
.output(z.array(gameInfo))
|
|
286
489
|
.query(async ({ ctx, input }) => {
|
|
@@ -289,7 +492,7 @@ export const gamesRouter = router({
|
|
|
289
492
|
}
|
|
290
493
|
return (await Game.listByProject(ctx.userId, input.projectId)).map(info);
|
|
291
494
|
}),
|
|
292
|
-
create:
|
|
495
|
+
create: gameProcedure
|
|
293
496
|
.input(z.object({ projectId: z.string(), name: z.string().min(1).max(200), dimension: z.enum(["2d", "3d"]).optional(), document: gameDocument.optional() }))
|
|
294
497
|
.output(gameWithDocument)
|
|
295
498
|
.mutation(({ ctx, input }) => {
|
|
@@ -298,10 +501,10 @@ export const gamesRouter = router({
|
|
|
298
501
|
}
|
|
299
502
|
return createGame(ctx.userId, input.projectId, input.name, input.document, input.dimension);
|
|
300
503
|
}),
|
|
301
|
-
examples:
|
|
504
|
+
examples: gameProcedure
|
|
302
505
|
.output(z.array(exampleGameSummary))
|
|
303
506
|
.query(({ ctx }) => listExampleGames(ctx.apiOptions)),
|
|
304
|
-
example:
|
|
507
|
+
example: gameProcedure
|
|
305
508
|
.input(z.object({ slug: z.string().min(1) }))
|
|
306
509
|
.output(z.object({ name: z.string(), document: gameDocument }))
|
|
307
510
|
.query(({ ctx, input }) => {
|
|
@@ -311,7 +514,7 @@ export const gamesRouter = router({
|
|
|
311
514
|
}
|
|
312
515
|
return { name: bundle.name, document: bundle.document };
|
|
313
516
|
}),
|
|
314
|
-
installExample:
|
|
517
|
+
installExample: gameProcedure
|
|
315
518
|
.input(installExampleGameInput)
|
|
316
519
|
.output(gameWithDocument)
|
|
317
520
|
.mutation(async ({ ctx, input }) => {
|
|
@@ -331,7 +534,7 @@ export const gamesRouter = router({
|
|
|
331
534
|
throw error;
|
|
332
535
|
}
|
|
333
536
|
}),
|
|
334
|
-
get:
|
|
537
|
+
get: gameProcedure
|
|
335
538
|
.input(idInput.extend({ revision: z.string().optional() }))
|
|
336
539
|
.output(gameWithDocument)
|
|
337
540
|
.query(async ({ ctx, input }) => {
|
|
@@ -339,7 +542,7 @@ export const gamesRouter = router({
|
|
|
339
542
|
const workspace = await gameWorkspace(ctx.userId, game);
|
|
340
543
|
return { game: info(game), document: await readRevision(workspace, game, input.revision ?? game.current_revision) };
|
|
341
544
|
}),
|
|
342
|
-
getDraft:
|
|
545
|
+
getDraft: gameProcedure
|
|
343
546
|
.input(idInput)
|
|
344
547
|
.output(gameWithDocument)
|
|
345
548
|
.query(async ({ ctx, input }) => {
|
|
@@ -349,7 +552,7 @@ export const gamesRouter = router({
|
|
|
349
552
|
throwApiError(ApiErrorCode.NOT_FOUND, "Game draft not found");
|
|
350
553
|
return { game: info(draft.game), document: draft.document };
|
|
351
554
|
}),
|
|
352
|
-
saveDraft:
|
|
555
|
+
saveDraft: gameProcedure
|
|
353
556
|
.input(idInput.extend({ baseUpdatedAt: z.string(), ops: z.array(gameDocumentOp).min(1).max(MAX_GAME_DRAFT_OPS) }))
|
|
354
557
|
.output(gameWithDocument)
|
|
355
558
|
.mutation(async ({ ctx, input }) => {
|
|
@@ -370,7 +573,25 @@ export const gamesRouter = router({
|
|
|
370
573
|
throw error;
|
|
371
574
|
}
|
|
372
575
|
}),
|
|
373
|
-
|
|
576
|
+
saveDraftDocument: gameProcedure
|
|
577
|
+
.input(idInput.extend({ baseUpdatedAt: z.string(), document: gameDocument }))
|
|
578
|
+
.output(gameWithDocument)
|
|
579
|
+
.mutation(async ({ ctx, input }) => {
|
|
580
|
+
const game = await ownedGame(ctx.userId, input.id);
|
|
581
|
+
try {
|
|
582
|
+
const saved = await Game.saveDraftDocument(ctx.userId, game.id, input.baseUpdatedAt, input.document, await gameWorkspace(ctx.userId, game));
|
|
583
|
+
if (!saved)
|
|
584
|
+
throwApiError(ApiErrorCode.ALREADY_EXISTS, "Game draft was modified concurrently");
|
|
585
|
+
return { game: info(saved.game), document: saved.document };
|
|
586
|
+
}
|
|
587
|
+
catch (error) {
|
|
588
|
+
if (error instanceof InvalidGameDocumentError) {
|
|
589
|
+
throwApiError(ApiErrorCode.INVALID_INPUT, error.message);
|
|
590
|
+
}
|
|
591
|
+
throw error;
|
|
592
|
+
}
|
|
593
|
+
}),
|
|
594
|
+
previewAuthoring: gameProcedure
|
|
374
595
|
.input(idInput.extend({ program: gameAuthoringProgram.optional() }).strict())
|
|
375
596
|
.output(authoringPreview)
|
|
376
597
|
.mutation(async ({ ctx, input }) => {
|
|
@@ -383,7 +604,7 @@ export const gamesRouter = router({
|
|
|
383
604
|
checkAuthoringResult(result);
|
|
384
605
|
return authoringPreview.parse(result);
|
|
385
606
|
}),
|
|
386
|
-
applyAuthoring:
|
|
607
|
+
applyAuthoring: gameProcedure
|
|
387
608
|
.input(idInput.extend({ candidate: gameAuthoringCandidate }).strict())
|
|
388
609
|
.output(gameWithDocument)
|
|
389
610
|
.mutation(async ({ ctx, input }) => {
|
|
@@ -398,7 +619,7 @@ export const gamesRouter = router({
|
|
|
398
619
|
}
|
|
399
620
|
return { game: info(draft.game), document: draft.document };
|
|
400
621
|
}),
|
|
401
|
-
draftChanges:
|
|
622
|
+
draftChanges: gameProcedure
|
|
402
623
|
.input(idInput)
|
|
403
624
|
.output(z.array(z.object({
|
|
404
625
|
id: z.string(), actor: z.enum(["agent", "user"]), threadId: z.string().nullable(),
|
|
@@ -410,7 +631,7 @@ export const gamesRouter = router({
|
|
|
410
631
|
const game = await ownedGame(ctx.userId, input.id);
|
|
411
632
|
return boundedGameDraftHistory((await Game.listDraftChanges(ctx.userId, game.id)).map(({ gameId: _gameId, ...change }) => change));
|
|
412
633
|
}),
|
|
413
|
-
draftBeforeChange:
|
|
634
|
+
draftBeforeChange: gameProcedure
|
|
414
635
|
.input(idInput.extend({ changeId: z.string() }))
|
|
415
636
|
.output(gameDocument)
|
|
416
637
|
.query(async ({ ctx, input }) => {
|
|
@@ -420,7 +641,7 @@ export const gamesRouter = router({
|
|
|
420
641
|
throwApiError(ApiErrorCode.NOT_FOUND, "Game draft change not found");
|
|
421
642
|
return document;
|
|
422
643
|
}),
|
|
423
|
-
revisions:
|
|
644
|
+
revisions: gameProcedure
|
|
424
645
|
.input(idInput)
|
|
425
646
|
.output(z.array(gameRevisionInfo))
|
|
426
647
|
.query(async ({ ctx, input }) => {
|
|
@@ -435,11 +656,11 @@ export const gamesRouter = router({
|
|
|
435
656
|
});
|
|
436
657
|
return revisions.sort((a, b) => b.modifiedAt - a.modifiedAt);
|
|
437
658
|
}),
|
|
438
|
-
publish:
|
|
659
|
+
publish: gameProcedure
|
|
439
660
|
.input(idInput.extend({ baseRevision: z.string(), baseUpdatedAt: z.string().optional(), expectedDigest: z.string().regex(/^[a-f0-9]{64}$/).optional(), document: gameDocument.optional(), message: z.string().trim().max(500).optional() }))
|
|
440
661
|
.output(gameWithDocument)
|
|
441
662
|
.mutation(async ({ ctx, input }) => publishDocument(ctx.userId, await ownedGame(ctx.userId, input.id), input.baseRevision, input.document, input.message, false, input.baseUpdatedAt, input.expectedDigest)),
|
|
442
|
-
restore:
|
|
663
|
+
restore: gameProcedure
|
|
443
664
|
.input(idInput.extend({ baseRevision: z.string(), baseUpdatedAt: z.string(), revision: z.string() }))
|
|
444
665
|
.output(gameWithDocument)
|
|
445
666
|
.mutation(async ({ ctx, input }) => {
|
|
@@ -448,7 +669,7 @@ export const gamesRouter = router({
|
|
|
448
669
|
const oldDocument = await readRevision(workspace, game, input.revision);
|
|
449
670
|
return publishDocument(ctx.userId, game, input.baseRevision, oldDocument, undefined, true, input.baseUpdatedAt);
|
|
450
671
|
}),
|
|
451
|
-
restoreDraft:
|
|
672
|
+
restoreDraft: gameProcedure
|
|
452
673
|
.input(idInput.extend({ baseUpdatedAt: z.string(), revision: z.string() }))
|
|
453
674
|
.output(gameWithDocument)
|
|
454
675
|
.mutation(async ({ ctx, input }) => {
|
|
@@ -468,7 +689,7 @@ export const gamesRouter = router({
|
|
|
468
689
|
throw error;
|
|
469
690
|
}
|
|
470
691
|
}),
|
|
471
|
-
installAsset:
|
|
692
|
+
installAsset: gameProcedure
|
|
472
693
|
.input(idInput.extend({ baseRevision: z.string().optional(), baseUpdatedAt: z.string().optional(), slot: z.string().min(1),
|
|
473
694
|
assetId: z.string(), expectedDigest: z.string().optional(), importSettings: gameModelImportSettings3D.partial().optional(),
|
|
474
695
|
dependencyAssetIds: z.record(z.string(), z.string()).refine((value) => Object.keys(value).length <= 320, "Too many model dependencies").optional() }))
|
|
@@ -545,130 +766,86 @@ export const gamesRouter = router({
|
|
|
545
766
|
}
|
|
546
767
|
return { game: info(saved.game), document: saved.document };
|
|
547
768
|
}),
|
|
548
|
-
installCandidate:
|
|
769
|
+
installCandidate: gameProcedure
|
|
770
|
+
.input(installCandidateInput)
|
|
771
|
+
.output(gameWithDocument)
|
|
772
|
+
.mutation(async ({ ctx, input }) => installCandidateBinding(ctx.userId, input)),
|
|
773
|
+
/** Staged candidates and per-slot generation requests for the asset browser. The catalog itself is computed client-side from the draft. */
|
|
774
|
+
assetBrowser: gameProcedure
|
|
775
|
+
.input(idInput)
|
|
776
|
+
.output(assetBrowserInfo)
|
|
777
|
+
.query(async ({ ctx, input }) => {
|
|
778
|
+
const game = await ownedGame(ctx.userId, input.id);
|
|
779
|
+
const result = await authoringRun(ctx.userId).invoke("browse_native_game_assets", { game_id: game.id });
|
|
780
|
+
checkAuthoringResult(result);
|
|
781
|
+
return assetBrowserInfo.parse(result);
|
|
782
|
+
}),
|
|
783
|
+
/**
|
|
784
|
+
* Generate an asset for one slot through `generate_game_asset`, record the
|
|
785
|
+
* prompt beside the staged bytes, and bind it in place on the draft that is
|
|
786
|
+
* current when the bytes are ready. The editor keeps saving while a
|
|
787
|
+
* generation runs, so the request carries no draft token: a sheet's frame
|
|
788
|
+
* bindings, or the slot's existing frame and tile bindings, are written in
|
|
789
|
+
* the same compare-and-swap as the slot, as the user who asked. A
|
|
790
|
+
* provider and model pin the generator; without them the capability picks one.
|
|
791
|
+
*/
|
|
792
|
+
generateAsset: gameProcedure
|
|
549
793
|
.input(idInput.extend({
|
|
550
|
-
baseRevision: z.string().optional(),
|
|
551
|
-
baseUpdatedAt: z.string().optional(),
|
|
552
794
|
slot: z.string().min(1),
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
.
|
|
795
|
+
kind: z.enum(["image", "audio", "music"]),
|
|
796
|
+
prompt: z.string().trim().min(1).max(4000),
|
|
797
|
+
preparation: z.record(z.string(), z.unknown()).optional(),
|
|
798
|
+
provider: z.string().min(1).optional(),
|
|
799
|
+
model: z.string().min(1).optional()
|
|
800
|
+
}).strict().refine((value) => (value.provider === undefined) === (value.model === undefined), "Pass provider and model together"))
|
|
801
|
+
.output(generatedAsset)
|
|
557
802
|
.mutation(async ({ ctx, input }) => {
|
|
558
803
|
const game = await ownedGame(ctx.userId, input.id);
|
|
559
804
|
const workspace = await gameWorkspace(ctx.userId, game);
|
|
560
|
-
const
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
: input.
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
throwApiError(ApiErrorCode.INVALID_INPUT, "Candidate asset is missing or changed");
|
|
599
|
-
}
|
|
600
|
-
const { extension, contentType, bytes } = candidate;
|
|
601
|
-
let binding = input.binding;
|
|
602
|
-
if (binding.mediaKind === "model") {
|
|
603
|
-
const prepared = await prepareGameModelBinding3D(bytes, { assetId: binding.assetId, expectedDigest: digest });
|
|
604
|
-
if (!prepared.ok) {
|
|
605
|
-
throwApiError(ApiErrorCode.INVALID_INPUT, prepared.diagnostics.map((issue) => issue.message).join("; "));
|
|
606
|
-
}
|
|
607
|
-
const sourceAsset = binding.sourceAssetId ? await Asset.find(ctx.userId, binding.sourceAssetId) : null;
|
|
608
|
-
if (binding.sourceAssetId && !sourceAsset) {
|
|
609
|
-
throwApiError(ApiErrorCode.INVALID_INPUT, "Model source asset is not owned");
|
|
610
|
-
}
|
|
611
|
-
const canonical = { ...prepared.binding, required: binding.required };
|
|
612
|
-
if (binding.provenance) {
|
|
613
|
-
canonical.provenance = binding.provenance;
|
|
614
|
-
}
|
|
615
|
-
if (binding.importSettings) {
|
|
616
|
-
canonical.importSettings = binding.importSettings;
|
|
617
|
-
}
|
|
618
|
-
if (binding.sourceDigest) {
|
|
619
|
-
canonical.sourceDigest = binding.sourceDigest;
|
|
620
|
-
}
|
|
621
|
-
if (sourceAsset) {
|
|
622
|
-
canonical.sourceAssetId = sourceAsset.id;
|
|
623
|
-
}
|
|
624
|
-
binding = canonical;
|
|
625
|
-
}
|
|
626
|
-
else if (binding.mediaKind === "collider") {
|
|
627
|
-
let geometry;
|
|
628
|
-
try {
|
|
629
|
-
geometry = JSON.parse(new TextDecoder().decode(bytes));
|
|
630
|
-
}
|
|
631
|
-
catch {
|
|
632
|
-
throwApiError(ApiErrorCode.INVALID_INPUT, "Prepared collider is not valid JSON");
|
|
633
|
-
}
|
|
634
|
-
const parsed = gamePreparedCollider3D.safeParse(geometry);
|
|
635
|
-
if (!parsed.success || binding.shape === "triangleMesh" && !parsed.data.indices?.length) {
|
|
636
|
-
throwApiError(ApiErrorCode.INVALID_INPUT, "Prepared collider has invalid vertices or triangle indices");
|
|
637
|
-
}
|
|
638
|
-
const { vertices, indices } = parsed.data;
|
|
639
|
-
const minimum = { x: Infinity, y: Infinity, z: Infinity };
|
|
640
|
-
const maximum = { x: -Infinity, y: -Infinity, z: -Infinity };
|
|
641
|
-
for (let index = 0; index < vertices.length; index += 3) {
|
|
642
|
-
minimum.x = Math.min(minimum.x, vertices[index]);
|
|
643
|
-
maximum.x = Math.max(maximum.x, vertices[index]);
|
|
644
|
-
minimum.y = Math.min(minimum.y, vertices[index + 1]);
|
|
645
|
-
maximum.y = Math.max(maximum.y, vertices[index + 1]);
|
|
646
|
-
minimum.z = Math.min(minimum.z, vertices[index + 2]);
|
|
647
|
-
maximum.z = Math.max(maximum.z, vertices[index + 2]);
|
|
648
|
-
}
|
|
649
|
-
binding = { ...binding, preparationVersion: "1", bounds: { min: minimum, max: maximum }, vertices: vertices.length / 3, triangles: (indices?.length ?? 0) / 3 };
|
|
650
|
-
try {
|
|
651
|
-
await decodePreparedGameCollider3D(bytes, gameAssetBinding3D.options[1].parse(binding));
|
|
652
|
-
}
|
|
653
|
-
catch (error) {
|
|
654
|
-
throwApiError(ApiErrorCode.INVALID_INPUT, error instanceof Error ? error.message : "Prepared collider is invalid");
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
if (input.baseRevision && game.current_revision !== input.baseRevision) {
|
|
658
|
-
throwApiError(ApiErrorCode.ALREADY_EXISTS, "Game was modified concurrently");
|
|
659
|
-
}
|
|
660
|
-
const draft = await Game.readDraft(ctx.userId, game.id, workspace);
|
|
661
|
-
if (!draft)
|
|
662
|
-
throwApiError(ApiErrorCode.NOT_FOUND, "Game draft not found");
|
|
663
|
-
if (input.baseUpdatedAt && draft.game.draft_updated_at !== input.baseUpdatedAt) {
|
|
664
|
-
throwApiError(ApiErrorCode.ALREADY_EXISTS, "Game draft was modified concurrently");
|
|
805
|
+
const args = { game_id: game.id, slot: input.slot, kind: input.kind, prompt: input.prompt, install: false };
|
|
806
|
+
if (input.preparation) {
|
|
807
|
+
args["preparation"] = input.preparation;
|
|
808
|
+
}
|
|
809
|
+
if (input.provider && input.model) {
|
|
810
|
+
args["provider"] = input.provider;
|
|
811
|
+
args["model"] = input.model;
|
|
812
|
+
}
|
|
813
|
+
const run = await assetGenerationRun(ctx.userId, game, workspace);
|
|
814
|
+
const result = generatedAssetResult.safeParse(await run.invoke("generate_game_asset", args));
|
|
815
|
+
if (!result.success) {
|
|
816
|
+
throwApiError(ApiErrorCode.INTERNAL_ERROR, "Asset generation returned an invalid result");
|
|
817
|
+
}
|
|
818
|
+
checkAuthoringResult(result.data);
|
|
819
|
+
const { binding, frames, tiles } = result.data;
|
|
820
|
+
if (!binding) {
|
|
821
|
+
throwApiError(ApiErrorCode.INTERNAL_ERROR, "Asset generation did not stage an asset");
|
|
822
|
+
}
|
|
823
|
+
await workspace.write(stagedGameCandidateRecordPath(game.source_root, binding.digest), JSON.stringify(stagedGameCandidateRecord.parse({
|
|
824
|
+
version: 1, digest: binding.digest, slot: input.slot, prompt: input.prompt, source: "generate", stagedAt: new Date().toISOString(), binding
|
|
825
|
+
})), "application/json");
|
|
826
|
+
const installed = await installOnLatestDraft(ctx.userId, { id: game.id, slot: input.slot, binding }, { replaceInPlace: true, frames, tiles });
|
|
827
|
+
return { ...installed, stagedDigest: binding.digest, installed: true };
|
|
828
|
+
}),
|
|
829
|
+
/** Bind a staged candidate to a slot on the current draft. A 2D slot's frame and tile bindings move onto the new bytes. */
|
|
830
|
+
installStagedCandidate: gameProcedure
|
|
831
|
+
.input(idInput.extend({
|
|
832
|
+
slot: z.string().min(1),
|
|
833
|
+
digest: z.string().regex(/^[a-f0-9]{64}$/)
|
|
834
|
+
}).strict())
|
|
835
|
+
.output(gameWithDocument)
|
|
836
|
+
.mutation(async ({ ctx, input }) => {
|
|
837
|
+
const game = await ownedGame(ctx.userId, input.id);
|
|
838
|
+
const picked = stagedCandidateBinding.safeParse(await authoringRun(ctx.userId).invoke("browse_native_game_assets", {
|
|
839
|
+
game_id: game.id, digest: input.digest, slot: input.slot
|
|
840
|
+
}));
|
|
841
|
+
if (!picked.success) {
|
|
842
|
+
throwApiError(ApiErrorCode.INTERNAL_ERROR, "Candidate lookup returned an invalid result");
|
|
665
843
|
}
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
throwApiError(ApiErrorCode.INVALID_INPUT, "Asset binding does not match the game dimension");
|
|
844
|
+
checkAuthoringResult(picked.data);
|
|
845
|
+
if (!picked.data.binding) {
|
|
846
|
+
throwApiError(ApiErrorCode.NOT_FOUND, "Staged candidate not found");
|
|
670
847
|
}
|
|
671
|
-
return
|
|
848
|
+
return installOnLatestDraft(ctx.userId, { id: game.id, slot: input.slot, binding: picked.data.binding }, { replaceInPlace: true });
|
|
672
849
|
})
|
|
673
850
|
});
|
|
674
851
|
//# sourceMappingURL=games.js.map
|