@ringozz/godot 4.7.2-597 → 4.7.2-604

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/README.md CHANGED
@@ -99,7 +99,7 @@ import foo from './models/foo.gltf';
99
99
  const scene = await foo; // PackedScene
100
100
  ```
101
101
 
102
- Each asset module's default export is its load **promise** (`const { materialize, load } = loadAsset(resPath, Class, files, deps, import.meta.hot.data)` — the default is `load`, memoized on the module's HMR data), started eagerly at module import, and it also exports a `materialize` promise (its own files plus its deps' files). It statically imports its own files Godot reads — `.import` sidecars and native text sources (`tscn`/`tres`/`po`) via the `text` loader, imported products (`.scn`/`.ctex`) via the `file` loader — and imports its **dependencies as generated modules too**, waiting only for their `materialize` before loading itself (dep loads start at module evaluation and run concurrently). Text-based source assets (`.gltf`/`.tscn`/`.tres`/`.obj`) are scanned for references (`res://` paths and relative paths ending in known asset extensions); referenced source assets become dep modules, raw `.mtl` files (OBJ materials) are recursed into, and raw `.bin` buffers are import-time only. The modules are virtual (namespace `godot`), and every import of the same asset resolves to the same one (evaluated once) — so the promise is a stable singleton, read with `await` or React 19's `use()`:
102
+ Each asset module's default export is its load **promise** (`const { materialize, load } = loadAsset(resPath, Class, files, deps, import.meta.hot.data)` — the default is `load`, memoized on the module's HMR data), started eagerly at module import, and it also exports a `materialize` promise (its own files plus its deps' files). The files Godot reads are bundled into the module — `.import` sidecars and native text sources (`tscn`/`tres`/`po`/`gd`) are inlined as text, imported products (`.scn`/`.ctex`) are emitted as files — and the module imports its **dependencies as generated modules too**, waiting only for their `materialize` before loading itself (dep loads start at module evaluation and run concurrently). Text-based source assets (`.gltf`/`.tscn`/`.tres`/`.obj`/`.gd`) are scanned for references (`res://` paths and relative paths ending in known asset extensions); referenced loadable assets become dep modules, native-text leaves are staged as raw text so the engine loads them by `res://` on web, raw `.mtl` files (OBJ materials) are recursed into, and raw `.bin` buffers are import-time only. The modules are virtual (namespace `godot`), and every import of the same asset resolves to the same one (evaluated once) — so the promise is a stable singleton, read with `await` or React 19's `use()`:
103
103
 
104
104
  ```tsx
105
105
  import foo from './models/foo.gltf';
@@ -115,7 +115,7 @@ function Foo() {
115
115
  </Suspense>
116
116
  ```
117
117
 
118
- Each extension maps to its Godot class (source assets only — `gltf`/`tscn`/`obj` → `PackedScene`, `jpg`/`jpeg`/`png`/`webp` → `CompressedTexture2D`, `svg` → `Texture2D`, `exr`/`hdr` → `TextureLayered`, `wav` → `AudioStreamWAV`, `ogg` → `AudioStreamOggVorbis`, `mp3` → `AudioStreamMP3`, `ttf`/`otf` → `FontFile`, `tres` → `Resource`, `po` → `Translation`), declared in `src/assets.d.ts`. Imported products (`.scn`/`.ctex`) are never imported directly.
118
+ Each extension maps to its Godot class (source assets only — `gltf`/`tscn`/`obj` → `PackedScene`, `gd` → `GDScript`, `jpg`/`jpeg`/`png`/`webp`/`svg` → `CompressedTexture2D`, `exr`/`hdr` → `TextureLayered`, `wav` → `AudioStreamWAV`, `ogg` → `AudioStreamOggVorbis`, `mp3` → `AudioStreamMP3`, `ttf`/`otf` → `FontFile`, `tres` → `Resource`, `po` → `Translation`), declared in `src/assets.d.ts`. Imported products (`.scn`/`.ctex`) are never imported directly. `.gd` scripts import as `Promise<GDScript>`, so `import script from './x.gd'; const gd = await script;` works like any other resource — and when a `.tscn` references the same `.gd`, Godot's `ResourceLoader` cache returns the same instance (parsed once).
119
119
 
120
120
  Textures are shared only through Godot's `ResourceLoader` path cache. The glTF import pipeline may produce per-scene texture instances (same `res://` path, separate objects), so don't assume two scenes referencing the same image share one texture object.
121
121
 
@@ -143,4 +143,4 @@ initDebug();
143
143
 
144
144
  ## Development
145
145
 
146
- `gen/` is generated (and gitignored) by `dev/codegen.ts` from `godot --dump-extension-api-with-docs`. Regenerate + typecheck with `bun run prebuild`. Build the native addon with `bun run build`. See [`AGENTS.md`](../../AGENTS.md) for the full workflow.
146
+ `gen/` is generated (and gitignored) by `dev/codegen.ts` from `godot --dump-extension-api-with-docs`. From the repo root: `bun run prebuild` (codegen + typecheck), `bun run predev` (imports `dev/assets/` via the editor), `bun run build` (native addon), `bun run test`, and `bun run dev` (web dev server). See [`AGENTS.md`](../../AGENTS.md) for the full workflow.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ringozz/godot",
3
3
  "author": "Vladimir Davidovich",
4
- "version": "4.7.2-597",
4
+ "version": "4.7.2-604",
5
5
  "description": "Node-API bindings for Godot Engine — call Godot classes from JavaScript",
6
6
  "publishConfig": {
7
7
  "access": "public"
@@ -58,9 +58,9 @@
58
58
  "precision": "single"
59
59
  },
60
60
  "optionalDependencies": {
61
- "@ringozz/godot-macos-arm64": "^4.7.2-597",
62
- "@ringozz/godot-windows-x86_64": "^4.7.2-597",
63
- "@ringozz/godot-web-wasm32": "^4.7.2-597"
61
+ "@ringozz/godot-macos-arm64": "^4.7.2-604",
62
+ "@ringozz/godot-windows-x86_64": "^4.7.2-604",
63
+ "@ringozz/godot-web-wasm32": "^4.7.2-604"
64
64
  },
65
65
  "peerDependencies": {
66
66
  "@types/bun": "*"
package/src/assets.d.ts CHANGED
@@ -10,6 +10,13 @@
10
10
  // a stable module singleton, so it can be passed to React's `use()` under
11
11
  // `<Suspense>`.
12
12
 
13
+ declare module '*.gd' {
14
+ import type { GDScript } from '@ringozz/godot/GDScript';
15
+ const asset: Promise<GDScript>;
16
+ export default asset;
17
+ export const materialize: Promise<unknown>;
18
+ }
19
+
13
20
  declare module '*.tres' {
14
21
  import type { Resource } from '@ringozz/godot/Resource';
15
22
  const asset: Promise<Resource>;
package/src/preload.ts CHANGED
@@ -32,6 +32,7 @@ const FORMATS: Record<string, { cls?: string; scan?: boolean; native?: boolean }
32
32
  gltf: { cls: 'PackedScene', scan: true },
33
33
  tscn: { cls: 'PackedScene', scan: true, native: true },
34
34
  obj: { cls: 'PackedScene', scan: true },
35
+ gd: { cls: 'GDScript', scan: true, native: true },
35
36
  jpg: { cls: 'CompressedTexture2D' },
36
37
  jpeg: { cls: 'CompressedTexture2D' },
37
38
  png: { cls: 'CompressedTexture2D' },
@@ -78,24 +79,29 @@ function parseSidecarDest(text: string): string | null {
78
79
  return remap ? remap[1] : null;
79
80
  }
80
81
 
81
- // Generalized reference extraction for text-based source assets: absolute
82
- // `res://` paths (Godot scenes/resources) and relative path tokens ending in a
83
- // known asset extension (glTF uris, OBJ/MTL textures). The literal dot before
84
- // the extension avoids matching MIME types inside `data:` URIs.
85
- const RES_RE = /res:\/\/[\w./\\-]+/g;
86
- const REL_RE = new RegExp(`([\\w./\\\\-]+\\.(?:${Object.keys(FORMATS).join('|')}))`, 'g');
82
+ // Reference extraction for text-based source assets: absolute `res://` paths
83
+ // (Godot scenes/resources) and relative path tokens ending in a known asset
84
+ // extension (glTF uris, OBJ/MTL textures). The literal dot before the extension
85
+ // avoids matching MIME types inside `data:` URIs. Regexes are built fresh per
86
+ // call: a `/g` instance is stateful, and reusing one module-level regex across
87
+ // many files misbehaves under the dev server's bundler (a stale lastIndex
88
+ // shifted a scan's results). The `//` fragment right after `res:` is a mere
89
+ // path remainder of the `res://` match, not a real relative ref, so it's
90
+ // skipped (otherwise it mis-resolves as a Windows UNC `\\…` path).
91
+ const REL_PATTERN = `([\\w./\\\\-]+\\.(?:${Object.keys(FORMATS).join('|')}))`;
87
92
 
88
93
  function extractRefs(text: string): string[] {
89
94
  const refs: string[] = [];
90
- for (const m of text.matchAll(RES_RE)) refs.push(m[0]);
91
- for (const m of text.matchAll(REL_RE)) refs.push(m[1]);
95
+ for (const m of text.matchAll(/res:\/\/[\w./\\-]+/g)) refs.push(m[0]);
96
+ const rel = new RegExp(REL_PATTERN, 'g');
97
+ for (const m of text.matchAll(rel)) if (!m[1].startsWith('/')) refs.push(m[1]);
92
98
  return refs;
93
99
  }
94
100
 
95
101
  /**
96
102
  * Adds a source asset's own files to `files`: imported assets contribute their
97
103
  * `.import` sidecar (text) + imported product (file); native text formats
98
- * (tscn/tres/po) contribute the file itself (text).
104
+ * (tscn/tres/po/gd) contribute the file itself (text).
99
105
  */
100
106
  function addSource(srcAbs: string, files: Map<string, 'text' | 'file'>): void {
101
107
  const impAbs = srcAbs + '.import';
@@ -106,16 +112,23 @@ function addSource(srcAbs: string, files: Map<string, 'text' | 'file'>): void {
106
112
  files.set(dest, 'file');
107
113
  }
108
114
  } else if (isNativeText(srcAbs)) {
109
- files.set(resOf(srcAbs), 'text');
115
+ stageNativeText(srcAbs, files);
110
116
  }
111
117
  }
112
118
 
119
+ /** Stages a native text source (`res://` path → `text`) so web staging + inlining picks it up. */
120
+ function stageNativeText(srcAbs: string, files: Map<string, 'text' | 'file'>): void {
121
+ files.set(resOf(srcAbs), 'text');
122
+ }
123
+
113
124
  /**
114
- * Collects the referenced source assets (deps) of a text source: source assets
115
- * become dep modules (each handles its own transitive deps); raw text formats
116
- * without a sidecar (`.mtl`) are recursed into. Buffers (`.bin`) are skipped.
125
+ * Collects the referenced source assets (deps) of a text source: every referenced
126
+ * source asset is staged (native text `files`, products dep modules), and
127
+ * loadable source assets also become dep modules so their own files + transitive
128
+ * deps are handled. Raw text formats without a sidecar (`.mtl`) are recursed into.
129
+ * Buffers (`.bin`) are skipped.
117
130
  */
118
- function collectDeps(abs: string, visited: Set<string>, deps: string[]): void {
131
+ function collectDeps(abs: string, visited: Set<string>, deps: string[], files: Map<string, 'text' | 'file'>): void {
119
132
  if (visited.has(abs)) {
120
133
  return;
121
134
  }
@@ -130,10 +143,22 @@ function collectDeps(abs: string, visited: Set<string>, deps: string[]): void {
130
143
  if (visited.has(refAbs)) {
131
144
  continue;
132
145
  }
146
+ // Stage native text (e.g. `.gd`, nested `.tscn`) so the engine loads it
147
+ // by `res://` on web (idempotent); loadable assets also become dep
148
+ // modules for their own files + transitive deps.
133
149
  if (isSourceAsset(refAbs)) {
134
- deps.push(refAbs);
135
- } else if (isScannableText(refAbs)) {
136
- collectDeps(refAbs, visited, deps);
150
+ if (isNativeText(refAbs)) {
151
+ visited.add(refAbs);
152
+ stageNativeText(refAbs, files);
153
+ }
154
+ if (CLASS_BY_EXT[extOf(refAbs)]) {
155
+ deps.push(refAbs);
156
+ }
157
+ }
158
+ // Any scannable ref (a source's own text, or a raw leaf like `.mtl`) is
159
+ // recursed into so its internal `res://` refs are traced/staged too.
160
+ if (isScannableText(refAbs)) {
161
+ collectDeps(refAbs, visited, deps, files);
137
162
  }
138
163
  }
139
164
  }
@@ -142,7 +167,7 @@ function analyze(abs: string): { files: Map<string, 'text' | 'file'>; deps: stri
142
167
  const files = new Map<string, 'text' | 'file'>();
143
168
  const deps: string[] = [];
144
169
  addSource(abs, files);
145
- collectDeps(abs, new Set(), deps);
170
+ collectDeps(abs, new Set(), deps, files);
146
171
  return { files, deps };
147
172
  }
148
173
 
@@ -156,15 +181,20 @@ function ensureModule(abs: string): string {
156
181
  `import { ${cls} } from '@ringozz/godot/${cls}';`,
157
182
  ];
158
183
  const map: string[] = [];
159
- let ti = 0;
160
184
  let fi = 0;
185
+ // Text files (own `.tscn`/`.tres`/`.po`, `.import` sidecars, staged `.gd`
186
+ // leaves) are inlined as string literals — Bun's `with { type: 'text' }`
187
+ // import resolves to a null/default under the dev server's HMR wrapper for
188
+ // a module's own path; products (`.scn`/`.ctex`) are fetched by path.
161
189
  for (const [resPath, kind] of files) {
162
190
  const absP = resolve(ROOT, resPath.slice('res://'.length));
163
- const v = kind === 'text' ? `c${ti++}` : `f${fi++}`;
164
- const type = kind === 'text' ? 'text' : 'file';
165
- const field = kind === 'text' ? 'content' : 'path';
166
- imports.push(`import ${v} from ${JSON.stringify(absP)} with { type: '${type}' };`);
167
- map.push(` ${JSON.stringify(resPath)}: { ${field}: ${v} },`);
191
+ if (kind === 'text') {
192
+ map.push(` ${JSON.stringify(resPath)}: { content: ${JSON.stringify(readFileSync(absP, 'utf8'))} },`);
193
+ continue;
194
+ }
195
+ const v = `f${fi++}`;
196
+ imports.push(`import ${v} from ${JSON.stringify(absP)} with { type: 'file' };`);
197
+ map.push(` ${JSON.stringify(resPath)}: { path: ${v} },`);
168
198
  }
169
199
  deps.forEach((dep, i) => imports.push(`import * as dep${i} from ${JSON.stringify(dep)};`));
170
200