@jgengine/assets 0.7.0 → 0.9.0
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/CHANGELOG.md +63 -0
- package/README.md +45 -5
- package/dist/cli/paths.d.ts +4 -0
- package/dist/cli/paths.js +11 -0
- package/dist/cli/pull.d.ts +7 -1
- package/dist/cli/pull.js +175 -48
- package/dist/download.d.ts +45 -2
- package/dist/download.js +93 -4
- package/dist/find.d.ts +35 -0
- package/dist/find.js +147 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/indexGen.js +23 -2
- package/dist/manifest.d.ts +2 -0
- package/dist/registry-catalog.json +455 -0
- package/dist/registry.d.ts +14 -0
- package/dist/registry.js +6 -0
- package/dist/snippet.d.ts +13 -0
- package/dist/snippet.js +47 -0
- package/dist/sources/kaykit.js +1 -1
- package/dist/sources/quaternius.js +2 -0
- package/llms.txt +997 -0
- package/package.json +5 -3
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,69 @@ Agents building on the published SDK can also read this programmatically:
|
|
|
11
11
|
same data as typed values, so an updater can diff its installed version against
|
|
12
12
|
the latest and surface the migration steps.
|
|
13
13
|
|
|
14
|
+
## Unreleased
|
|
15
|
+
|
|
16
|
+
## 0.9.0
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- Mobile HUD fit — design-resolution UI scaling now applies to every game by default: `HudCanvas` measures the live viewport and scales the whole HUD from `PlayableGame.hudFit.designSize` (default 1600×900, clamps `minScale`/`maxScale` 0.4–1) so desktop-authored layouts shrink to fit a phone instead of overflowing it; `hudFit.mobile` overrides tune the phone fit separately. Pure math lives in `@jgengine/core/ui/hudScale` (`hudScaleForViewport`, `resolveHudFit`, `rectOverflow`, `overflowingPanels`); the shell mounts `@jgengine/react`'s `HudViewportProvider` around `GameUI` so games need no wiring.
|
|
21
|
+
- Graphics → UI scale — a player-facing `graphics.uiScale` slider (0.5–1.5, default 1) multiplies the computed HUD scale on every platform; the same resolution system drives desktop preference and mobile shrink.
|
|
22
|
+
- HUD overflow gate — `HudCanvas` measures every `HudPanel` against the viewport at runtime and reports offenders on a `data-hud-overflow` attribute (plus a console warning); `bun run shoot <game> --device mobile|both` now exits non-zero naming the panels that escape the viewport.
|
|
23
|
+
- Automatic visibility & streaming defaults — an engine-level `VisibilitySystem` (exported from `@jgengine/core`) gives every scene renderer-agnostic culling and asset-streaming policy with no per-object wiring: distance culling, preload margins, hysteresis, delayed unloading, multi-camera awareness (including cameras excluded from streaming), and per-object overrides (always-visible, never-unload, disabled culling/streaming, custom render distance and preload margin).
|
|
24
|
+
- Self-hosted asset mirror — `DEFAULT_RELEASE_BASE` now points at this repo's rolling `packs` release instead of the upstream host; a catalog-driven `mirror-assets` workflow (weekly cron + manual dispatch) keeps the mirror in sync with the asset catalog.
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- Mobile HUD fit is on by default — design-resolution HUD scaling now applies to every game with no config change (previously opt-in via `platforms: ["web", "mobile"]`). A desktop-only game keeps the legacy fixed 0.85 compact zoom by declaring `platforms: ["web"]` (without `"mobile"`).
|
|
29
|
+
|
|
30
|
+
## 0.8.0
|
|
31
|
+
|
|
32
|
+
Transport-agnostic multiplayer (socket.io, WebRTC p2p, LAN/Fly adapters) plus grid/voxel
|
|
33
|
+
and puzzle primitives, HUD-only presentation, cumulative leveling, and a round of
|
|
34
|
+
controller/camera/sensor additions. **Breaking:** the `gameui` component kit moved out of
|
|
35
|
+
`@jgengine/react` onto the shadcn registry.
|
|
36
|
+
|
|
37
|
+
### Migrate
|
|
38
|
+
|
|
39
|
+
- Bump every `@jgengine/*` dependency to `^0.8.0` (the eight packages version in lockstep).
|
|
40
|
+
- Additive only, except for `gameui` — every other 0.7.0 API is unchanged; opt into any of the below by importing it directly.
|
|
41
|
+
- **Breaking:** replace any `@jgengine/react/gameui` import with the equivalent registry component (`npx shadcn@latest add https://jgengine.com/r/<name>.json`), and swap `GameUiThemeProvider` for `--jg-*` CSS variables on a wrapper element. `GameIcon` and friends moved to `@jgengine/react/gameIcons`.
|
|
42
|
+
- `leveling({ thresholdMode: 'cumulative' })` is opt-in; the default `perLevel` behavior is unchanged.
|
|
43
|
+
- `defineGame.physics.gravity`/`jumpVelocity` are now read by the built-in kinematics controller every frame — if a game already set them expecting a no-op, jump/fall now actually reflects them; omit both to keep the previous defaults.
|
|
44
|
+
|
|
45
|
+
### Added
|
|
46
|
+
|
|
47
|
+
- Cumulative leveling — `leveling({ thresholdMode: 'cumulative' })` tracks xp as a lifetime total that resolves upward across levels and clamps at the max-level threshold once capped (#12).
|
|
48
|
+
- Direction-aware pool depletion — `EffectSystem.canReceive(instanceId, effect, magnitude?)` takes an optional signed magnitude; negative checks the opposite direction and returns `pools-depleted` only when every stat in the receive order is already at max, so heals reach fully-depleted targets (#168).
|
|
49
|
+
- Puzzle primitives — `puzzle/cellGrid` (uniform-cell boards: line-clear, match-3 cascade, run detection) and `puzzle/fallingPiece` (rotation-state shapes, ghost drop, lock delay, classic gravity/level/score curves) for Tetris/match-3 games (#166).
|
|
50
|
+
- Voxel field — `world/voxelField` (`createVoxelField`, chunked block lattice, neighbors/exposedFaces, 3D DDA raycast, dirty-tracked `chunkVersion`) for voxel games and instanced renderers; assert on `field.summary()` the way environment worlds assert on `summarizeEnvironment` (#166).
|
|
51
|
+
- `defineGame` games may omit `assets` (an empty catalog is injected); `PlayableGame.presentation: 'hud'` mounts no 3D canvas/camera/pointer for board/card/menu games; an `environment()` world auto-renders as the shell's backdrop when `PlayableGame.environment` is unset (#166).
|
|
52
|
+
- Declared-action intent board — `turn/intent` `createIntentBoard` for one-turn-ahead intents (Slay-the-Spire style): declare/peek/all/consume/clear (#168).
|
|
53
|
+
- `turnLoop` lifecycle hooks — `config.onTurnStart`/`onTurnEnd` fire on every `advanceTurn()`; `ctx.game.turn.loop(id, config)` lazily creates/returns a notify-wrapped `TurnLoop` so every mutation (`advanceTurn`/`advancePhase`/`advanceRound`/`spend`/`gain`/`refill`/`setOrder`/...) auto-bumps `ctx.version()` with no manual wiring (#163/#168).
|
|
54
|
+
- `ctx.game.store` — a reactive per-game keyed store (`set`/`delete`/`get`/`has`/`subscribe`/`mapSnapshot`/`arraySnapshot`) plus `@jgengine/react`'s `useGameStore` selector hook, replacing hand-rolled module-level stores for ad-hoc reactive game state; `ctx.game.cards.pile(id, config?)` lazily creates/returns a notify-wrapped `CardPile` the same way; `createCardPile` gained an `onChange` hook for headless use; `CommandDefinition.apply` may return void for side-effect-only commands (#163).
|
|
55
|
+
- Camera — `sideScroll` rig (fixed lateral follow for 2.5D platformers/beat-'em-ups), a `none` rig (no camera mounted; pairs with `PlayableGame.presentation: 'hud'`), `rts.pan: false` (static backdrop camera: no pan/edge-scroll/rotate/zoom, still re-centers on the follow target), and the `observer` rig now defaults to the local player when `bind` is unset (#167).
|
|
56
|
+
- Sensors + session — `sensor/concealment` (`colorDistance`/`concealmentScore`/`createConcealmentSensor`), `sensor/freezeMonitor` (`createFreezeMonitor`), `session/roles` (`assignRoles`); `createRoundState`'s `RoundConfig.teams` accepts per-team roles and an optional `winCondition` that `evaluate()` checks each tick, and takes an optional `phaseOrder` for arbitrary named phase cycles (`concludeRound`/`evaluate` settle only while the current phase is neither the first nor the last entry) (#151).
|
|
57
|
+
- Appearance replication — presence rows carry an optional per-slot appearance channel (cosmetic ids, hex tints, model keys) alongside pose, riding the existing pose message with no protocol bump; wire `ctx.player.cosmetics.get(userId)` into the outgoing pose (#151).
|
|
58
|
+
- `ctx.input` — a per-frame held-action snapshot (`publish(held)`/`isDown(action)`/`held()`) without bumping `ctx.version()`; action bindings gained `repeatMs` (repeat-fire while held); every command resolved from a bound action now carries `aim`; `pointer.secondaryCommand` runs a command on right-click off the same raycast as move/ping (#164).
|
|
59
|
+
- Object spatial queries + entity patching — `ctx.scene.object.at`/`inBox`/`raycast`/`raycastAll` over unit-box objects; `ctx.scene.entity.update(id, patch)` for name/position/rotation/role/movement/behaviors/meta; per-instance `renderObject`/`objectStyles` overrides; `pointerService.worldHitCenter()` + pointer-lock center-ray aiming (#165).
|
|
60
|
+
- Controller movement config — `PlayerMovementConfig` (`mode`: free/axis/grid, `axis`, `cellSize`, `collideObjects`, `beforeCommit` pre-commit hook) for the shell-driven walk controller; `defineGame.physics.gravity`/`jumpVelocity` are honored by the built-in walk controller (distinct from the standalone `physics/physicsWorld` rigid-body sim); `ctx.player.motion.impulse`/`setVerticalVelocity`/`setY`/`takePending` (`MotionIntents`); `entity.spawnPoseOf`/`resetToSpawn`/`resetAllToSpawn` (#162).
|
|
61
|
+
- Model material/animation + paint — `ModelConfig.tint`/`metalness`/`roughness`/`animation` (GLTF clip playback, paused pose holds); `PointerHit.uv` + `pointerService.sampleSurface()` for material-aware picking; `ctx.scene.entity.paint` runtime paint layer, auto-rendered via a per-instance canvas texture with no per-game wiring; remote-player appearance tint recolors the shell's default capsule (#151).
|
|
62
|
+
- **Transport pipe seam** (`@jgengine/ws/pipe`) — `createWsBackend` runs over any bidirectional string channel, not just a raw `WebSocket`: `TransportPipe`/`TransportPipeHandlers`/`TransportPipeFactory`, with `webSocketPipe(url, webSocketFactory?)` as the default. `createWsBackend({ userId, url?, pipe? })` — `url` stays the common case, `pipe` opens the seam to socket.io, WebRTC, and in-process loopback below.
|
|
63
|
+
- **Browser-safe authoritative host** — `createGameHost` and `memoryPersistence` moved from `@jgengine/node` to `@jgengine/ws/host` (zero Node dependencies; `@jgengine/node` re-exports both unchanged from `@jgengine/node/host` / `@jgengine/node/persistence`, so existing imports keep working). `@jgengine/ws/hostRouter`'s `createHostRouter({ host, authenticate?, poseRules?, positionHistoryMs?, chatRateLimit?, chatHistoryLimit?, chatMaxBodyLength?, now? })` extracts the ws wire-protocol session logic out of `createGameWsServer` into a transport-agnostic `HostRouter` (`connect(transport) → { handleRaw, close }`, `rewind`, `close`); `@jgengine/node`'s `createGameWsServer` is now a thin binding of this router onto the `ws` npm package, same public API. `loopbackPipe(router): TransportPipeFactory` connects a `createWsBackend` straight into an in-process router.
|
|
64
|
+
- **Socket.IO transport** — `@jgengine/ws/socketIoPipe` (`SocketIoLikeSocket` structural type, `socketIoPipe(socket)`, `createSocketIoBackend({ socket, userId, … })`) and `@jgengine/node/socketIoServer` (`attachGameSocketIoServer({ io, host, …router options }): { rewind, close }`, structural `SocketIoLikeServer`/`SocketIoLikeServerSocket`) ride the existing ws JSON protocol over socket.io's `send`/`message` frames — no socket.io dependency in either package's types. New `socketIo({ topology?, url? })` adapter in `@jgengine/core/runtime/adapter`.
|
|
65
|
+
- **WebRTC peer-to-peer** (`@jgengine/ws/peer`) — one browser tab hosts, authoritatively, with no server process. `createPeerHost({ userId, host?, runtimes?, persistence?, tickMs?, router?, rtc? })` runs a `GameHost` + `HostRouter` in the host tab (`backend` is the host player's own loopback connection, `accept(offerCode) → Promise<answerCode>` per guest); `createPeerGuest({ userId, token?, rtc? })` offers/connects from the joining side. `encodePeerSignal`/`decodePeerSignal` turn SDP into copy-pasteable base64url codes for manual cross-machine signaling; `broadcastChannelSignaling(room)` automates it for same-origin multi-tab play, with `announcePeerHost`/`joinPeerSession` wiring host/guest to a `PeerSignaling` in one call. New `p2p({ topology?, room? })` adapter (topology defaults `"private"`).
|
|
66
|
+
- **LAN adapter + Fly sugar** — `lan({ topology?, port?, path? })` in `@jgengine/core/runtime/adapter` resolves through `@jgengine/shell/multiplayer`'s `resolveShellMultiplayer` to `ws(s)://<page hostname>:<port ?? 8080><path ?? /ws>` derived from `window.location`, so any browser on the LAN auto-connects to whichever machine served the page — no URL configuration. `fly({ app, topology?, path? })` is `ws` sugar for a Fly.io deploy: resolves to `wss://<app>.fly.dev<path ?? "/ws">`. `apps/dev`'s Vite server now listens on the network (`server: { host: true }`) and exposes `?p2p=host` / `?p2p=join` query params wired through the new `resolvePeerShellMultiplayer({ gameId, role, room?, userId?, feedActions? })`.
|
|
67
|
+
- `ws()` (`@jgengine/core/runtime/adapter`) gained an optional `url` field, carried through by `resolveShellMultiplayer` (`args.url ?? adapter.url ?? default`).
|
|
68
|
+
|
|
69
|
+
### Removed
|
|
70
|
+
|
|
71
|
+
- **The `gameui` component kit** (`@jgengine/react/gameui`) — the themed HUD kit (bars, slots, feedback, meters, panels, screens, reticles, icons) and its `GameUiThemeProvider`/`useGameUiTheme` theming have been removed. **Breaking** for anyone importing `@jgengine/react/gameui` (or its subpaths/barrel). The components now ship as installable shadcn registry items at `https://jgengine.com/r/<name>.json` (`npx shadcn@latest add https://jgengine.com/r/<name>.json`), styled with Tailwind + `--jg-*` CSS variables instead of a theme object. The icon catalog (`GameIcon`, `iconForAction`, `iconForItemId`, `isGameIconName`, `GameIconName`) moved to `@jgengine/react/gameIcons`. To theme, set the `--jg-*` variables on a wrapper element (the registry's `jg-theme` presets mirror the old `ember`/`synthwave`/`fieldkit` palettes) instead of wrapping in `GameUiThemeProvider`.
|
|
72
|
+
|
|
73
|
+
### Docs
|
|
74
|
+
|
|
75
|
+
- Added [CREDITS.md](CREDITS.md) crediting [achrefelouafi](https://github.com/achrefelouafi) for the MIT Three.js reference projects behind the procedural building, water, rain, and snow renderers, with links from the root, `@jgengine/core`, and `@jgengine/shell` READMEs.
|
|
76
|
+
|
|
14
77
|
## 0.7.0
|
|
15
78
|
|
|
16
79
|
The engine-gaps release — 22 system-level additions across turn/tactics, cards & boards,
|
package/README.md
CHANGED
|
@@ -18,18 +18,57 @@ A self-generating, license-verified index of thousands of CC0 3D models — host
|
|
|
18
18
|
|
|
19
19
|
Everything collapses into the core `AssetCatalog` via `buildCatalog({ basePath })`.
|
|
20
20
|
|
|
21
|
+
## `add` — one command for anything
|
|
22
|
+
|
|
23
|
+
`assets add <query>` is the front door. It fuzzy-searches **every** catalog at once — 3D models, whole packs, HUD components (the shadcn registry), and `game-icon` glyphs — then does the fetch and prints the exact copy-paste wiring. One mental model instead of four.
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
assets add astronaut --dir ../../apps/dev/public # model → pull + reindex + print the assets.ts snippet
|
|
27
|
+
assets add nature --dir ../../apps/dev/public # whole pack → pull + reindex + how to wire an id
|
|
28
|
+
assets add "mana bar" # HUD component → the `npx shadcn add` cmd + <VitalBar/> usage
|
|
29
|
+
assets add sword # icon → the game-icon name to drop in a slot
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
- A **model / pack** match is fully automated: if the pack isn't already in `<dir>/models/`, it's pulled and extracted, then `reindex` runs so the id is addressable, then the `buildCatalog` + model-seam snippet is printed.
|
|
33
|
+
- A **component / icon** match prints the one-liner to run and the import + usage — no bytes to pull.
|
|
34
|
+
- Ambiguous query? `add` lists the top matches across kinds; narrow with `--kind model|pack|component|icon` or a more specific term. `--json` emits the ranked matches for scripting.
|
|
35
|
+
|
|
36
|
+
The same ranking is available programmatically: `import { findAssets } from "@jgengine/assets"`.
|
|
37
|
+
|
|
21
38
|
## CLI
|
|
22
39
|
|
|
23
40
|
```
|
|
41
|
+
assets add <query> [--kind <k>] [--dir <dir>] [--mirror <baseUrl>] [--json]
|
|
42
|
+
# unified import: models, packs, components, icons
|
|
24
43
|
assets list [--category <c>] [--source <s>] # browse the generated index
|
|
25
44
|
assets search <term> # grep the index
|
|
26
|
-
assets pull <source-id> [--dir public]
|
|
27
|
-
|
|
45
|
+
assets pull <source-id> [--dir public] [--mirror <baseUrl>] [--offline]
|
|
46
|
+
# download + extract a whole pack's GLBs into <dir>/models/<source>/
|
|
47
|
+
assets register <path|url> --category <c> --license <l> [--author <a>]
|
|
48
|
+
# register a one-off single into the shipped index
|
|
28
49
|
assets reindex [public/models] # regenerate generated/*.json from pulled packs
|
|
29
50
|
assets verify # license + alias-integrity gate
|
|
30
51
|
```
|
|
31
52
|
|
|
32
|
-
Run in-repo with `bun run --cwd packages/assets src/cli/pull.ts <verb> …`.
|
|
53
|
+
Run in-repo with `bun run --cwd packages/assets src/cli/pull.ts <verb> …`. (`add <path|url> --license …` still works as an alias for `register`.)
|
|
54
|
+
|
|
55
|
+
### Mirror fallback and offline pulls
|
|
56
|
+
|
|
57
|
+
`pull` never has just one path to the bytes. For a given `source-id` it tries, in order, until one succeeds:
|
|
58
|
+
|
|
59
|
+
1. **Mirror base override** — `--mirror <baseUrl>` (or the `JGENGINE_ASSETS_MIRROR` env var if `--mirror` is not passed) — the archive is expected at `<baseUrl>/<provider>/<source-id>.zip`, e.g. `https://my-mirror.example.com/kenney/kenney-nature.zip`.
|
|
60
|
+
2. **The default GitHub-release mirror** — `https://github.com/Noisemaker111/jgengine/releases/download/packs/<provider>-<source-id>.zip`, on this repo's own rolling `packs` release (no separate assets repo). github.com is reachable from every cloud sandbox with no network-policy change, so zero-setup sessions still pull. `.github/workflows/mirror-assets.yml` (weekly cron + manual dispatch) keeps the release in sync with `src/sources/*.ts` automatically — adding a catalog entry is the whole publishing step, no manual upload. Skip this hop with `JGENGINE_ASSETS_NO_DEFAULT_MIRROR=1`.
|
|
61
|
+
3. **The primary provider path** — the source's pinned `{ url, sha256? }` or, for providers that rotate URLs (Kenney), a scrape of the asset page.
|
|
62
|
+
4. **The pack's own `mirror`** — an optional direct archive URL set on the `AssetSource` entry itself (`src/sources/*.ts`), tried as a last resort.
|
|
63
|
+
|
|
64
|
+
If every attempt fails, `pull` throws one aggregated error naming every URL it tried and why each one failed. Whenever the source's `download` is pinned with a `sha256`, the downloaded bytes are hashed and checked against it **no matter which path supplied them** — a mirror serving stale or tampered bytes is rejected and the next source in the chain is tried instead.
|
|
65
|
+
|
|
66
|
+
`--offline` skips the network entirely: it succeeds immediately if `<dir>/models/<source-id>/` already has files in it, and otherwise fails fast with a message telling you to pull once on a connected machine or point `--mirror`/`JGENGINE_ASSETS_MIRROR` at a reachable archive — useful for CI scripts that should error in seconds instead of hanging on a blocked fetch.
|
|
67
|
+
|
|
68
|
+
**Network-restricted environments** — a `403` on `CONNECT` from an outbound proxy means the environment's network policy blocks that provider host (common in sandboxed cloud sessions); it is a policy decision, not a transient failure, so retrying never helps. Either allowlist the host in the environment settings, or pull once on a machine that can reach the providers, then:
|
|
69
|
+
|
|
70
|
+
- commit (or otherwise host) the resulting `public/models/<source-id>/` directory so restricted environments read it straight off disk and use `assets pull --offline` as a fast, honest no-op check, or
|
|
71
|
+
- host your own mirror of the zip archives at `<baseUrl>/<provider>/<source-id>.zip` and set `JGENGINE_ASSETS_MIRROR=<baseUrl>` (or pass `--mirror <baseUrl>`) so `pull` fetches from it instead of the original provider.
|
|
33
72
|
|
|
34
73
|
## Adding assets
|
|
35
74
|
|
|
@@ -46,7 +85,7 @@ bun src/cli/pull.ts reindex ../../apps/dev/public/models # regenerate
|
|
|
46
85
|
bun src/cli/pull.ts verify # license + alias gate
|
|
47
86
|
```
|
|
48
87
|
|
|
49
|
-
A **new provider** is just a new `src/sources/<provider>.ts` added to the `sources` array in `src/sources/index.ts`. Pinned providers use `download: { url, sha256? }`; providers that rotate URLs (Kenney) use `download: { scrape: <page> }`.
|
|
88
|
+
A **new provider** is just a new `src/sources/<provider>.ts` added to the `sources` array in `src/sources/index.ts`. Pinned providers use `download: { url, sha256? }`; providers that rotate URLs (Kenney) use `download: { scrape: <page> }`. Any entry can also carry an optional top-level `mirror: <archiveUrl>` — a direct URL `pull` falls back to if both a `--mirror`/`JGENGINE_ASSETS_MIRROR` override and the primary path fail (see "Mirror fallback and offline pulls" above).
|
|
50
89
|
|
|
51
90
|
**A single one-off** — no code edit, zero bytes stored (URL) or copied into `local/` (path):
|
|
52
91
|
|
|
@@ -78,7 +117,7 @@ export const game: PlayableGame = {
|
|
|
78
117
|
};
|
|
79
118
|
```
|
|
80
119
|
|
|
81
|
-
`buildCatalog({ sources: ["kenney-nature"] })` restricts to chosen packs; `includeAliases` / `includeSingles` default true. Discover ids with `assets search <term>` / `assets list --category <c>` instead of memorizing them.
|
|
120
|
+
`buildCatalog({ sources: ["kenney-nature"] })` restricts to chosen packs; `includeAliases` / `includeSingles` default true. Discover ids with `assets add <query>` (or `assets search <term>` / `assets list --category <c>`) instead of memorizing them.
|
|
82
121
|
|
|
83
122
|
### Serving the bytes
|
|
84
123
|
|
|
@@ -94,3 +133,4 @@ bun src/cli/pull.ts pull kenney-nature --dir ../../apps/dev/public # → apps/
|
|
|
94
133
|
|
|
95
134
|
- Kenney rotates download URLs, so its sources `scrape` the asset page at pull time.
|
|
96
135
|
- Quaternius / KayKit pages gate downloads behind JS; automated `pull` falls back to a clear error when no archive link is found — download those manually into the staging dir, then `reindex`.
|
|
136
|
+
- `pull`'s mirror fallback and `--offline` guard exist for network-restricted environments (CI, sandboxes without provider access); see "Mirror fallback and offline pulls" above.
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Sibling of `cli/` under `src/` (dev) or `dist/` (published) so reindex writes the tree consumers import. */
|
|
2
|
+
export declare function resolveGeneratedDir(cliDir: string): string;
|
|
3
|
+
export declare function resolvePackageTreeRoot(cliDir: string): string;
|
|
4
|
+
export declare function resolvePackageRoot(cliDir: string): string;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { join, resolve } from "node:path";
|
|
2
|
+
/** Sibling of `cli/` under `src/` (dev) or `dist/` (published) so reindex writes the tree consumers import. */
|
|
3
|
+
export function resolveGeneratedDir(cliDir) {
|
|
4
|
+
return join(resolve(cliDir, ".."), "generated");
|
|
5
|
+
}
|
|
6
|
+
export function resolvePackageTreeRoot(cliDir) {
|
|
7
|
+
return resolve(cliDir, "..");
|
|
8
|
+
}
|
|
9
|
+
export function resolvePackageRoot(cliDir) {
|
|
10
|
+
return resolve(cliDir, "..", "..");
|
|
11
|
+
}
|
package/dist/cli/pull.d.ts
CHANGED
|
@@ -1,2 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
2
|
+
/** Sibling of `cli/` under `src/` (dev) or `dist/` (published) so reindex writes the tree consumers import. */
|
|
3
|
+
export declare const generatedDir: string;
|
|
4
|
+
export { resolveGeneratedDir } from "./paths.js";
|
|
5
|
+
export declare function flag(argv: string[], name: string): string | undefined;
|
|
6
|
+
export declare function describeNetworkFailure(error: unknown): string;
|
|
7
|
+
export declare function isPopulated(dir: string): boolean;
|
|
8
|
+
export declare function cmdPull(argv: string[]): Promise<void>;
|
package/dist/cli/pull.js
CHANGED
|
@@ -1,21 +1,27 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { basename, dirname, join, resolve } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import {
|
|
5
|
+
import { downloadPackArchive, extractGlbs, extractTextures } from "../download.js";
|
|
6
|
+
import { rankAssets } from "../find.js";
|
|
6
7
|
import { generatedIndex } from "../generated/index.js";
|
|
7
8
|
import { reindex } from "../indexGen.js";
|
|
8
9
|
import { isScrapeDownload } from "../manifest.js";
|
|
10
|
+
import { registryCatalog } from "../registry.js";
|
|
11
|
+
import { componentWiringSnippet, iconWiringSnippet, modelWiringSnippet } from "../snippet.js";
|
|
9
12
|
import { sourceById } from "../sources/index.js";
|
|
10
13
|
import { verifyManifest } from "../verify.js";
|
|
14
|
+
import { resolveGeneratedDir, resolvePackageRoot, resolvePackageTreeRoot } from "./paths.js";
|
|
11
15
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
+
const packageTreeRoot = resolvePackageTreeRoot(here);
|
|
17
|
+
const pkgRoot = resolvePackageRoot(here);
|
|
18
|
+
/** Sibling of `cli/` under `src/` (dev) or `dist/` (published) so reindex writes the tree consumers import. */
|
|
19
|
+
export const generatedDir = resolveGeneratedDir(here);
|
|
20
|
+
export { resolveGeneratedDir } from "./paths.js";
|
|
21
|
+
const singlesJson = join(packageTreeRoot, "singles.json");
|
|
16
22
|
const localDir = join(pkgRoot, "local");
|
|
17
23
|
const CDN_BASE = "https://cdn.jsdelivr.net/gh/Noisemaker111/jgengine@main/packages/assets/local";
|
|
18
|
-
function flag(argv, name) {
|
|
24
|
+
export function flag(argv, name) {
|
|
19
25
|
const index = argv.indexOf(`--${name}`);
|
|
20
26
|
return index >= 0 ? argv[index + 1] : undefined;
|
|
21
27
|
}
|
|
@@ -23,6 +29,19 @@ function fail(message) {
|
|
|
23
29
|
console.error(`error: ${message}`);
|
|
24
30
|
process.exit(1);
|
|
25
31
|
}
|
|
32
|
+
export function describeNetworkFailure(error) {
|
|
33
|
+
const chain = [];
|
|
34
|
+
let current = error;
|
|
35
|
+
while (current instanceof Error) {
|
|
36
|
+
chain.push(current.message);
|
|
37
|
+
current = current.cause;
|
|
38
|
+
}
|
|
39
|
+
const text = chain.length > 0 ? chain.join(" — ") : String(error);
|
|
40
|
+
const looksPolicyBlocked = /\b403\b|CONNECT|proxy|ENOTFOUND|ECONNREFUSED|ETIMEDOUT|fetch failed/i.test(text);
|
|
41
|
+
return looksPolicyBlocked
|
|
42
|
+
? `${text}\nhint: a 403/CONNECT failure here usually means the sandbox network policy blocks this host — not a transient error, retrying won't help. Allowlist the host in the environment settings, point JGENGINE_ASSETS_MIRROR at a reachable mirror, or build with procedural geometry.`
|
|
43
|
+
: text;
|
|
44
|
+
}
|
|
26
45
|
function cmdList(argv) {
|
|
27
46
|
const category = flag(argv, "category");
|
|
28
47
|
const source = flag(argv, "source");
|
|
@@ -46,28 +65,42 @@ function cmdSearch(argv) {
|
|
|
46
65
|
}
|
|
47
66
|
console.log(`— ${Math.min(rows.length, limit)} of ${rows.length} matches for "${term}"`);
|
|
48
67
|
}
|
|
49
|
-
|
|
68
|
+
export function isPopulated(dir) {
|
|
69
|
+
return existsSync(dir) && readdirSync(dir).length > 0;
|
|
70
|
+
}
|
|
71
|
+
export async function cmdPull(argv) {
|
|
50
72
|
const sourceId = argv[0];
|
|
51
|
-
if (sourceId === undefined)
|
|
52
|
-
fail("usage: pull <source-id> [--dir <dir>]");
|
|
73
|
+
if (sourceId === undefined) {
|
|
74
|
+
fail("usage: pull <source-id> [--dir <dir>] [--mirror <baseUrl>] [--offline]");
|
|
75
|
+
}
|
|
53
76
|
const source = sourceById.get(sourceId);
|
|
54
77
|
if (source === undefined)
|
|
55
78
|
fail(`unknown source: ${sourceId}`);
|
|
56
79
|
const outRoot = resolve(flag(argv, "dir") ?? "public");
|
|
57
80
|
const outDir = join(outRoot, "models", sourceId);
|
|
58
|
-
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if (actual !== source.download.sha256) {
|
|
65
|
-
fail(`sha256 mismatch for ${sourceId}: expected ${source.download.sha256}, got ${actual}`);
|
|
81
|
+
const offline = argv.includes("--offline");
|
|
82
|
+
const mirrorBase = flag(argv, "mirror") ?? process.env.JGENGINE_ASSETS_MIRROR;
|
|
83
|
+
if (offline) {
|
|
84
|
+
if (!isPopulated(outDir)) {
|
|
85
|
+
fail(`--offline set but ${outDir} is empty; pull it once on a connected machine (or via ` +
|
|
86
|
+
`--mirror/JGENGINE_ASSETS_MIRROR) and commit/host that directory before running offline`);
|
|
66
87
|
}
|
|
88
|
+
console.log(`offline: ${outDir} already populated, skipping network`);
|
|
89
|
+
return;
|
|
67
90
|
}
|
|
91
|
+
const result = await fetchPackInto(source, outRoot, { mirrorBase });
|
|
92
|
+
const textureNote = result.textures > 0 ? ` + ${result.textures} texture(s)` : "";
|
|
93
|
+
console.log(`pulled ${result.models} models${textureNote} -> ${result.outDir}`);
|
|
94
|
+
}
|
|
95
|
+
async function fetchPackInto(source, outRoot, options) {
|
|
96
|
+
const outDir = join(outRoot, "models", source.id);
|
|
97
|
+
console.log(`resolving ${source.id}${isScrapeDownload(source.download) ? " (scrape)" : ""}` +
|
|
98
|
+
`${options.mirrorBase !== undefined ? ` [mirror override: ${options.mirrorBase}]` : ""}…`);
|
|
99
|
+
const { archive, url, attempted } = await downloadPackArchive(source, { mirrorBase: options.mirrorBase });
|
|
100
|
+
console.log(`downloaded ${url}${attempted.length > 1 ? ` (after ${attempted.length - 1} failed attempt(s))` : ""}`);
|
|
68
101
|
const glbs = extractGlbs(archive);
|
|
69
102
|
if (glbs.length === 0)
|
|
70
|
-
fail(`no .glb files found in ${
|
|
103
|
+
fail(`no .glb files found in ${source.id} archive`);
|
|
71
104
|
mkdirSync(outDir, { recursive: true });
|
|
72
105
|
for (const glb of glbs)
|
|
73
106
|
writeFileSync(join(outDir, glb.file), glb.bytes);
|
|
@@ -77,8 +110,7 @@ async function cmdPull(argv) {
|
|
|
77
110
|
mkdirSync(dirname(dest), { recursive: true });
|
|
78
111
|
writeFileSync(dest, texture.bytes);
|
|
79
112
|
}
|
|
80
|
-
|
|
81
|
-
console.log(`pulled ${glbs.length} models${textureNote} -> ${outDir}`);
|
|
113
|
+
return { outDir, models: glbs.length, textures: textures.length, url };
|
|
82
114
|
}
|
|
83
115
|
function readSingles() {
|
|
84
116
|
if (!existsSync(singlesJson))
|
|
@@ -88,10 +120,11 @@ function readSingles() {
|
|
|
88
120
|
function writeSingles(list) {
|
|
89
121
|
writeFileSync(singlesJson, `${JSON.stringify(list, null, 2)}\n`);
|
|
90
122
|
}
|
|
91
|
-
function
|
|
123
|
+
function cmdRegisterSingle(argv) {
|
|
92
124
|
const target = argv[0];
|
|
93
|
-
if (target === undefined)
|
|
94
|
-
fail("usage:
|
|
125
|
+
if (target === undefined) {
|
|
126
|
+
fail("usage: register <path|url> --category <c> --license <l> [--author <a>] [--id <id>]");
|
|
127
|
+
}
|
|
95
128
|
const license = flag(argv, "license");
|
|
96
129
|
if (license === undefined)
|
|
97
130
|
fail("add requires --license");
|
|
@@ -124,6 +157,95 @@ function cmdAdd(argv) {
|
|
|
124
157
|
writeSingles(singles);
|
|
125
158
|
console.log(`added single ${id} -> ${url}${isUrl ? " (0 bytes stored)" : " (copied to local/)"}`);
|
|
126
159
|
}
|
|
160
|
+
const KINDS = ["model", "pack", "component", "icon"];
|
|
161
|
+
function describeMatch(match) {
|
|
162
|
+
switch (match.kind) {
|
|
163
|
+
case "model":
|
|
164
|
+
return `[model] ${match.id}${match.via === "alias" ? " (alias)" : match.via === "single" ? " (single)" : ""}`;
|
|
165
|
+
case "pack":
|
|
166
|
+
return `[pack] ${match.source} — ${match.title}`;
|
|
167
|
+
case "component":
|
|
168
|
+
return `[component] ${match.name} — ${match.title}`;
|
|
169
|
+
case "icon":
|
|
170
|
+
return `[icon] ${match.name}`;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
async function performAdd(match, argv) {
|
|
174
|
+
if (match.kind === "component") {
|
|
175
|
+
const component = registryCatalog.components.find((entry) => entry.name === match.name);
|
|
176
|
+
if (component === undefined)
|
|
177
|
+
fail(`component vanished from catalog: ${match.name}`);
|
|
178
|
+
console.log(`component: ${component.title} — ${component.description}\n`);
|
|
179
|
+
console.log(componentWiringSnippet(component));
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (match.kind === "icon") {
|
|
183
|
+
console.log(`icon: ${match.name}\n`);
|
|
184
|
+
console.log(iconWiringSnippet(match.name));
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const source = sourceById.get(match.source);
|
|
188
|
+
if (source === undefined)
|
|
189
|
+
fail(`match resolves to unknown source: ${match.source}`);
|
|
190
|
+
const outRoot = resolve(flag(argv, "dir") ?? "public");
|
|
191
|
+
const outDir = join(outRoot, "models", match.source);
|
|
192
|
+
const mirrorBase = flag(argv, "mirror") ?? process.env.JGENGINE_ASSETS_MIRROR;
|
|
193
|
+
if (isPopulated(outDir)) {
|
|
194
|
+
console.log(`${match.source} already present in ${outDir}`);
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
const result = await fetchPackInto(source, outRoot, { mirrorBase });
|
|
198
|
+
const textureNote = result.textures > 0 ? ` + ${result.textures} texture(s)` : "";
|
|
199
|
+
console.log(`pulled ${result.models} models${textureNote} -> ${result.outDir}`);
|
|
200
|
+
}
|
|
201
|
+
const result = reindex(join(outRoot, "models"), generatedDir);
|
|
202
|
+
console.log(`reindexed ${result.total} entries -> ${generatedDir}`);
|
|
203
|
+
if (match.kind === "model") {
|
|
204
|
+
console.log(`\nwire it in (characters/enemies go in entityModels instead):\n`);
|
|
205
|
+
console.log(modelWiringSnippet(match.id));
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
console.log(`\n${match.title}: browse ids with \`assets list --source ${match.source}\`, then wire like:\n`);
|
|
209
|
+
console.log(modelWiringSnippet(`${match.source}/<model>`));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
async function cmdAdd(argv) {
|
|
213
|
+
const query = argv[0];
|
|
214
|
+
if (query === undefined) {
|
|
215
|
+
fail("usage: add <query> [--kind model|pack|component|icon] [--dir <dir>] [--mirror <baseUrl>] [--json]\n" +
|
|
216
|
+
" add <path|url> --license <l> [--category <c>] (register a one-off single into the shipped index)");
|
|
217
|
+
}
|
|
218
|
+
// Back-compat: the old `add` registered a one-off single, always with --license.
|
|
219
|
+
if (flag(argv, "license") !== undefined) {
|
|
220
|
+
cmdRegisterSingle(argv);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const kindFlag = flag(argv, "kind");
|
|
224
|
+
if (kindFlag !== undefined && !KINDS.includes(kindFlag)) {
|
|
225
|
+
fail(`--kind must be one of ${KINDS.join(", ")}`);
|
|
226
|
+
}
|
|
227
|
+
const kind = kindFlag;
|
|
228
|
+
const ranked = rankAssets(query, { kind, limit: Number(flag(argv, "limit") ?? "12") });
|
|
229
|
+
if (ranked.length === 0) {
|
|
230
|
+
fail(`no asset matches "${query}" — try a broader term or \`assets search <term>\``);
|
|
231
|
+
}
|
|
232
|
+
if (argv.includes("--json")) {
|
|
233
|
+
console.log(JSON.stringify(ranked, null, 2));
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
const top = ranked[0];
|
|
237
|
+
const runnerUp = ranked[1]?.score ?? 0;
|
|
238
|
+
const decisive = ranked.length === 1 || kind !== undefined || top.score - runnerUp >= 20;
|
|
239
|
+
if (!decisive) {
|
|
240
|
+
console.log(`several matches for "${query}" — narrow with --kind or a more specific term:\n`);
|
|
241
|
+
for (const entry of ranked.slice(0, 8))
|
|
242
|
+
console.log(` ${describeMatch(entry.match)}`);
|
|
243
|
+
console.log(`\nthen re-run, e.g. \`assets add "${query}" --kind ${top.match.kind}\``);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
console.log(`→ ${describeMatch(top.match)}\n`);
|
|
247
|
+
await performAdd(top.match, argv);
|
|
248
|
+
}
|
|
127
249
|
function cmdReindex(argv) {
|
|
128
250
|
const modelsDir = resolve(argv[0] ?? join("public", "models"));
|
|
129
251
|
if (!existsSync(modelsDir))
|
|
@@ -143,28 +265,33 @@ function cmdVerify() {
|
|
|
143
265
|
console.error(` ✗ ${error}`);
|
|
144
266
|
fail(`verify failed with ${result.errors.length} problem(s)`);
|
|
145
267
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
268
|
+
if (import.meta.main) {
|
|
269
|
+
const [command, ...rest] = process.argv.slice(2);
|
|
270
|
+
switch (command) {
|
|
271
|
+
case "list":
|
|
272
|
+
cmdList(rest);
|
|
273
|
+
break;
|
|
274
|
+
case "search":
|
|
275
|
+
cmdSearch(rest);
|
|
276
|
+
break;
|
|
277
|
+
case "pull":
|
|
278
|
+
await cmdPull(rest).catch((error) => fail(describeNetworkFailure(error)));
|
|
279
|
+
break;
|
|
280
|
+
case "add":
|
|
281
|
+
await cmdAdd(rest).catch((error) => fail(describeNetworkFailure(error)));
|
|
282
|
+
break;
|
|
283
|
+
case "register":
|
|
284
|
+
cmdRegisterSingle(rest);
|
|
285
|
+
break;
|
|
286
|
+
case "reindex":
|
|
287
|
+
cmdReindex(rest);
|
|
288
|
+
break;
|
|
289
|
+
case "verify":
|
|
290
|
+
cmdVerify();
|
|
291
|
+
break;
|
|
292
|
+
default:
|
|
293
|
+
console.log("usage: assets <add|list|search|pull|register|reindex|verify> [...args]");
|
|
294
|
+
if (command !== undefined && command !== "help")
|
|
295
|
+
process.exit(1);
|
|
296
|
+
}
|
|
170
297
|
}
|
package/dist/download.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type AssetSource } from "./manifest.js";
|
|
2
|
+
export type FetchLike = typeof fetch;
|
|
2
3
|
export interface ExtractedGlb {
|
|
3
4
|
file: string;
|
|
4
5
|
bytes: Uint8Array;
|
|
@@ -9,8 +10,50 @@ export interface ExtractedTexture {
|
|
|
9
10
|
bytes: Uint8Array;
|
|
10
11
|
}
|
|
11
12
|
export declare function findArchiveUrl(html: string, pageUrl: string): string | null;
|
|
12
|
-
export declare function resolveArchiveUrl(source: AssetSource): Promise<string>;
|
|
13
|
-
export declare function downloadArchive(url: string): Promise<Uint8Array>;
|
|
13
|
+
export declare function resolveArchiveUrl(source: AssetSource, fetchImpl?: FetchLike): Promise<string>;
|
|
14
|
+
export declare function downloadArchive(url: string, fetchImpl?: FetchLike): Promise<Uint8Array>;
|
|
15
|
+
/**
|
|
16
|
+
* Layout for the `--mirror` / `JGENGINE_ASSETS_MIRROR` base URL override: the
|
|
17
|
+
* archive for a pack is expected at `<baseUrl>/<provider>/<packId>.zip`, e.g.
|
|
18
|
+
* `https://my-mirror.example.com/kenney/kenney-nature.zip`.
|
|
19
|
+
*/
|
|
20
|
+
export declare function mirrorOverrideUrl(baseUrl: string, source: AssetSource): string;
|
|
21
|
+
/**
|
|
22
|
+
* Default asset mirror: this repo's own GitHub Releases, reachable from every
|
|
23
|
+
* cloud sandbox without network-policy changes (github.com is on the default
|
|
24
|
+
* allowlist). Assets live on the rolling `packs` release, one flat zip per
|
|
25
|
+
* pack named `<provider>-<packId>.zip`, kept in sync with the source catalog
|
|
26
|
+
* by `.github/workflows/mirror-assets.yml` — adding a catalog entry is the
|
|
27
|
+
* whole publishing step. Override the chain with `--mirror` /
|
|
28
|
+
* `JGENGINE_ASSETS_MIRROR`, or disable this hop with
|
|
29
|
+
* `JGENGINE_ASSETS_NO_DEFAULT_MIRROR=1`.
|
|
30
|
+
*/
|
|
31
|
+
export declare const DEFAULT_RELEASE_BASE = "https://github.com/Noisemaker111/jgengine/releases/download/packs";
|
|
32
|
+
/** URL of `source`'s archive on the default GitHub-release mirror. */
|
|
33
|
+
export declare function defaultReleaseUrl(source: AssetSource): string;
|
|
34
|
+
export interface DownloadPackOptions {
|
|
35
|
+
/** Explicit mirror base override (CLI `--mirror` / `JGENGINE_ASSETS_MIRROR`), tried before the primary provider path. */
|
|
36
|
+
mirrorBase?: string;
|
|
37
|
+
fetchImpl?: FetchLike;
|
|
38
|
+
}
|
|
39
|
+
export interface DownloadPackResult {
|
|
40
|
+
archive: Uint8Array;
|
|
41
|
+
url: string;
|
|
42
|
+
/** Every URL attempted, in order, ending with the one that succeeded. */
|
|
43
|
+
attempted: readonly string[];
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Resolves and downloads a pack's archive, trying sources in order until one
|
|
47
|
+
* succeeds: (1) the mirror base override at `mirrorOverrideUrl`, (2) the
|
|
48
|
+
* default GitHub-release mirror at `defaultReleaseUrl` (skipped when
|
|
49
|
+
* `JGENGINE_ASSETS_NO_DEFAULT_MIRROR=1`), (3) the primary provider path
|
|
50
|
+
* (`resolveArchiveUrl`: scrape or pinned URL), (4) the pack's own `mirror`
|
|
51
|
+
* URL. A pinned `sha256` is verified against whichever
|
|
52
|
+
* source supplied the bytes; a mismatch is treated as a failed attempt so the
|
|
53
|
+
* next source in the chain is tried. Throws with every attempted URL and its
|
|
54
|
+
* failure reason when all sources fail.
|
|
55
|
+
*/
|
|
56
|
+
export declare function downloadPackArchive(source: AssetSource, options?: DownloadPackOptions): Promise<DownloadPackResult>;
|
|
14
57
|
export declare function extractGlbs(archive: Uint8Array): ExtractedGlb[];
|
|
15
58
|
export declare function extractTextures(archive: Uint8Array): ExtractedTexture[];
|
|
16
59
|
export declare function sha256Hex(bytes: Uint8Array): Promise<string>;
|