diff --git a/CHANGELOG-ZH.md b/CHANGELOG-ZH.md index d9457b2..dcf226c 100644 --- a/CHANGELOG-ZH.md +++ b/CHANGELOG-ZH.md @@ -1,3 +1,9 @@ +## 2.4.5 + +- 修复继承的导出属性在 Inspector 中的显示和默认值。 +- 数值 PackedArray 支持 TypedArray 输入。 +- 修复资源生命周期问题,Godot 仍持有的资源不会因 JavaScript 垃圾回收而失效。 + ## 2.4.4 - 新增导出 `interface[]` 属性的 Inspector 编辑,支持嵌套字段和默认值。 @@ -18,20 +24,16 @@ ## 2.4.1 -- 修复 TypeScript 内置脚本模板:Godot 新建脚本对话框不再因为模板缺少 `origin` metadata 而丢弃 Default 和 Tool 模板。 -- 新增桌面版内置 `gode_node` helper,并为 npm 快照的包路径和 fork 子进程依赖树提供物化缓存,让 `child_process.fork()`、ESM `import.meta.url` 路径推导与原生 `.node` 加载可以执行真实桌面包探测,运行时不再依赖用户机器上的 Node 可执行文件。 -- 新增基于最小 Godot `node-llama-cpp` 项目的打包插件导出 smoke,在 Windows、Linux 和 macOS 上验证导出包基础运行能力。 -- 加固发布打包流程:TypeScript 编译器下载遇到临时网络失败时会自动重试。 -- 新增 PR 提交身份检查 workflow:通过提交作者、提交者以及 `Co-authored-by` / `Signed-off-by` trailer 拦截归属于受限 AI 编码工具的提交,且不会 checkout 或执行 PR 代码。 +- 修复新建 TypeScript 脚本时 Default 和 Tool 模板不显示的问题。 +- 完善桌面 npm 包支持,子进程和原生模块无需额外安装 Node.js。 ## 2.4.0 -- Breaking:将原生扩展拆分为 `gode_runtime` 和桌面专用 `gode_editor`;导出包现在只包含目标平台运行时二进制,并排除编辑器专用文件。 -- Breaking:TypeScript 静态方法不再出现在脚本实例方法表中。默认导出类的静态方法通过脚本资源 API `has_static_method()`、`get_static_method_argument_count()` 和 `call_static()` 检查和调用。 -- 项目级 TypeScript 编译移入 editor 扩展;`GodeTypeScriptCompiler` 直接暴露静态编译方法,移除 `compile_project_static`,导出运行时只读取 manifest 中列出的产物并缓存已校验 manifest。 -- 收紧 TypeScript 编辑器方法发现,只识别默认导出类的直接实例方法,排除静态方法、constructor、getter/setter 和嵌套声明。 -- 统一实例调用、notification 和静态方法调用的异步 Promise rejection 日志处理。 -- 更新构建、打包、CI 和测试覆盖,覆盖拆分后的库布局、Windows `node.dll` Node-API forwarder、iOS deployment target 16.0、静态方法调用和方法过滤。 +- Breaking:插件文件结构调整,升级时请完整更新插件。 +- 导出包仅包含目标平台的运行文件,不再包含编辑器组件。 +- Breaking:TypeScript 静态方法改为通过脚本资源的 `call_static()` 调用。 +- 修复编辑器的脚本方法列表,完善异步错误提示。 +- iOS 导出要求 iOS 16.0 或更新版本。 ## 2.3.3 @@ -45,58 +47,22 @@ ## 2.3.2 -- 将 GitHub Actions 的 artifact 下载步骤更新到 `actions/download-artifact@v8`,消除 release package 和 packaged-plugin smoke test 下载 artifact 时的 Node.js 20 弃用警告。 -- 修复 TypeScript 导出的 Object/Resource/Node 属性:script instance state 保存现在会返回已存储的导出值,Resource 和 Node 的 Inspector hint 会从类型化导出中推断,`static exports` 的默认值也会在 descriptor 类型确定后再解析,不再受字段顺序影响。 -- 改进 generated binding 的构建体验:generator 输入未变化时会跳过 configure-time codegen,通过仅针对 generated C++ 的 unity build 批量编译源码,并在可用时自动使用 `sccache` 或 `ccache`。 -- 修复 libnode 静态链接:各平台构建会强制完整加载 libnode archive,不再依赖链接器默认的按需 archive member 选择。 +- 修复导出 Object、Resource 和 Node 属性的场景保存、Inspector 提示和默认值。 +- 加快插件重复构建,修复原生依赖链接问题。 ## 2.3.1 -- 增加 Godot 数字全局类型的 TypeScript 全局类型别名:`int` 现在解析为 `number | bigint`,与 64 位整数转换行为一致,`float` 解析为 `number`。 -- 加固发布流程:手动触发 release 时会拒绝带 `v` 前缀的 tag,并统一发布不带前缀的 SemVer tag 和 release 名称。 +- TypeScript 脚本新增 `int`(`number | bigint`)和 `float`(`number`)类型别名。 ## 2.3.0 -- 改进内置 TypeScript 编译器:读取 `tsconfig.json` 的 `include`、`exclude`、`baseUrl`、`paths` 和解析后的 root files,通过 TypeScript `matchFiles` 处理 `res://` 虚拟路径,只为 program 实际包含的源文件生成输出,默认启用 TSX 的 `jsx: react`,项目源文件读取失败时明确报错而不是静默跳过,拒绝规整后越过 `res://` 根目录的 module specifier,并在 emit transformer 阶段将项目内路径别名 import/export 重写为运行时可解析的相对 `.js` 路径。 -- 改进 TypeScript metadata parser:外部 interface 和父类属性收集支持 `.tsx`、`.d.ts`、`index.ts(x)`、`index.d.ts` 以及 `.js/.jsx/.mjs/.cjs` specifier 到 TypeScript 源文件的映射,相对 import 路径会按 loader 规则规整并在没有对应 TypeScript 源时回退到真实 JS/CJS/JSX 或无后缀文件,可解析 `class Foo ...; export default Foo` 或 `export { Foo as default }` 这类拆分 default export 声明的默认脚本类,并会在解析静态 `signals`、`rpc_config`、`exports`、`@Export()` 选项和导出属性默认值前解包 `as const`、`satisfies`、非空断言、类型断言和括号这类 TypeScript-only metadata 表达式,同时一致处理 metadata 对象中的 quoted keys,并保留来自 `@Export()` 选项和 `static exports` 的 Inspector `hint` 和 `hint_string` metadata,还将 `static exports` 的 `default` descriptor 按 Godot Variant 兼容值暴露到类型声明;父脚本继承可识别 default import、named import alias 和 namespace import,`extends godot.Node` 或简单泛型基类也会解析出正确的 Godot base type,并在文件读取或 tree-sitter parser 初始化/解析失败时报告或跳过对应输入;数字默认值改为不抛异常的统一解析,支持数字分隔符和 `0b`/`0o`/`0x` 整数字面量,无效 RPC config 值也不再被静默转换成 `0`/`false`。 -- 增加 TypeScript 编译和导出 manifest,基于输入修改时间、当前输入集合签名和已存在输出复用上次编译结果,并将编译桥脚本与内置 TypeScript runtime 的内容哈希纳入签名;同时记录、校验实际 emit 的输出映射,对路径边界做规整校验、拒绝父目录片段,并拒绝无效的 public `.ts`/`.tsx` 源路径请求,`compile_script()`、脚本加载和导出运行时只接受当前 manifest 明确包含的有效输出,重新编译后会清理已失效的旧 `.js`/`.js.map` 缓存产物,避免执行或保留已被 `tsconfig` 排除的旧产物。 -- 加固 TypeScript 和 npm runtime snapshot 导出流程:编译产物路径缺失、输出映射不完整、脚本/依赖文件读取失败或导出配置读取失败时会立即中止导出,避免生成缺少脚本文件、依赖文件或仍带 manifest 的半成品包;npm snapshot 的 include/exclude 路径会规整到 `res://`,拒绝父目录片段和非资源 scheme,`package.json` 读取失败也会明确告警。 -- 加固 TypeScript 资源加载器:读取 `.ts`/`.tsx` 文件失败时返回 `ERR_CANT_OPEN`,加载脚本会在源码编译前绑定规整后的资源路径,并让自维护脚本缓存使用规整后的资源路径作为 key,同时遵循 Godot `CACHE_MODE_IGNORE` / `CACHE_MODE_IGNORE_DEEP`,避免缓存本应忽略的加载结果;default export 类名和基类(包括拆分 default export)以及 `godot` named imports 现在会按 tree-sitter 实际 import clause 节点直接从源码报告且不会启动 Node,资源依赖也直接从 TypeScript import/export specifier 中发现,不会触发编译,Godot dependency rename 只会改写匹配到的相对 import/export/dynamic-import specifier,并为 TypeScript 源文件保留运行时 `.js` 输出 specifier 风格。 -- 修复 TypeScript 动态 `import()` 的依赖扫描:现在只有第一个参数中的字面量 module specifier 会被视为依赖,非字面量表达式或 import attributes/options 里的字符串不会再被误报为依赖,也不会被 dependency rename 改写。 -- 加固 TypeScript 运行时脚本加载:编译后的 `.js` 产物读取失败时会输出明确错误并停止实例化,避免把空代码交给 Node 编译。 -- 加固 TypeScript 实例创建:无效脚本、JavaScript 类构造失败或 Godot script-instance 分配失败时,不再把半初始化的 `ScriptInstance` 留在运行时缓存中。 -- 加固 `ScriptInstance` 的 V8 入口:属性访问、方法查找/调用、notification、`toString()`、构造和 reload 都会先确认 NodeRuntime 正在运行再进入 V8;脚本入口中的 V8 locker/isolate/handle scope 顺序也统一化,返回 N-API handle 的 NodeRuntime 入口改用可逃逸 handle scope,方法参数数量这类 metadata 查询不再不必要地获取 V8 locker。 -- 修复 TypeScript 脚本编译状态:非 dirty 脚本会返回真实有效性,dirty 重编译会先清空旧 metadata、父脚本路径和缓存的 JavaScript default class,源码更新为无效内容时不再保留旧导出属性、旧父脚本或旧类,`static exports` 和同文件父类导出字段现在也会维护 ordered property list,`_get_base_script()` 和 `_inherits_script()` 会暴露并递归检查解析到的 TypeScript 父脚本,reload 失败时也会返回错误而不是报告 `OK`。 -- 改进 NodeRuntime CJS/ESM 编译失败和 reload 处理:bootstrap、`run_script()`、CommonJS compile、ESM compile 和 ESM compile promise rejection 现在会记录 V8 异常 stack/message,不再只返回空 exports 或泛化诊断;通过动态 `import()` 解析到的 ESM module 会先完成 link/evaluate 再交还给 Node;同一路径以新源码或项目侧依赖重新编译时会失效已成功的 ESM cache;`import.meta.url` 会返回有效的 `file://` URL 且不包含内部 cache-busting 后缀;失败后的 ESM/CJS pending module-load cache 和中间 ESM module cache 也会清理,临时的 parse、link、evaluate 或 require 错误在源码修复后可以重试。 -- 改进 TypeScript 语言编辑器接口:补充 TypeScript 保留字/控制流关键字、块注释/文档注释/字符串 delimiter、内置脚本模板、`res://` 下 `.ts`/`.tsx` 脚本路径校验、基于 tree-sitter 的轻量语法 validation 和函数列表、函数 stub 生成、函数定位、named class/global class 类型支持,以及会从最新文件内容刷新缓存脚本或显式传入脚本的 reload hooks,避免 reload 静默空跑或复用旧的内存源码;全局类索引现在直接用 tree-sitter 读取并解析 TypeScript 源码,能识别拆分 default export 和命名空间/泛型形式的 base type,不再通过加载资源触发 TypeScript 编译或 Node 启动。 -- 修复 Godot 64 位整数到 JavaScript 的转换精度:安全整数范围内返回 `number`,超出范围返回 `bigint`,并让生成方法返回值、类常量、枚举值和默认参数统一走精确整数转换。 -- 加固 Godot `Array`、`Dictionary` 和 packed array 到 JavaScript 的转换,增加数组长度范围校验,避免 `uint32_t` 截断,并在元素、键值或 `Map`/对象写入触发异常时立即短路。 -- 更新 TypeScript 声明生成:根据 `meta` 区分窄整数和 64 位整数,`NodePath` 返回值保持为 `NodePath`,输入参数接受 `NodePath | string`,并修正 `GD.typeof()`、`type_convert()`、`type_string()`、`error_string()` 以及 `get_instance_id()` 的类型声明。 -- 修正 `GD.instance_from_id()` 的 TypeScript 声明为 `GodotObject | null`,对齐无效 ObjectID 在运行时返回 null 的行为。 -- 改为从 `extension_api.json` 生成 `VariantType` 和 `VariantOperator` TypeScript alias,不再维护手写 enum 表,并增加完整性测试逐项对照 Godot API 数据校验生成的 enum 值。 -- 放宽 typed `Array`/`Dictionary` 输入的 TypeScript 声明,使嵌套的 `NodePath`、`String`、`StringName`、packed array 和其他 Variant 兼容的元素/值类型使用与直接方法参数一致的输入友好别名。 -- 对生成 class 和 utility function 作用域内完全相同的 TypeScript 声明行去重,避免输入友好别名展开后产生重复 constructor、method 或 operator 签名。 -- 移除生成 builtin class 的 numeric index signature;运行时通过 `get()` / `set()` 方法和 iterator 暴露索引数据,并不支持 JavaScript bracket property access。 -- 将不可实例化的 Godot class 和 singleton 背后的类型在生成的 TypeScript 声明中标记为 `abstract`,使 `new AnimationMixer()` 这类运行时会拒绝的直接构造也能在 TypeScript 阶段被拦截。 -- 修复生成 builtin class enum 值(例如 `Vector2.AXIS_X`)只在 TypeScript 声明中存在、运行时未导出的问题,现在会同时导出到 constructor 和 prototype。 -- 对齐生成的 TypeScript 属性声明和运行时 binding generator:只有 getter 实际绑定时才声明属性,只有对应 setter accessor 实际绑定时才声明 setter。 -- 增加完整性测试,校验生成的 Godot builtin class 常量和 enum 值在 constructor 导出、prototype 导出和 TypeScript 声明之间保持一致。 -- 增加完整性测试,校验每个生成的 Godot builtin constructor overload 都有 TypeScript constructor 签名和运行时 arity/type matcher 覆盖。 -- 增加完整性测试,校验每个可绑定的生成 Godot builtin class 方法都同时存在于 C++ runtime binding 和 TypeScript declaration surface。 -- 增加完整性测试,校验每个可绑定的生成 Godot class 方法都同时存在于 C++ runtime binding 和 TypeScript declaration surface。 -- 增加完整性测试,校验生成的 Godot class signals 同时通过 C++ accessor、header 声明、Godot `Signal` 包装和 TypeScript 声明暴露。 -- 增加完整性测试,校验生成的 Godot class 常量和 enum 值在 constructor 导出、singleton 实例需要的 prototype 导出以及 TypeScript 声明之间保持一致。 -- 增加完整性测试,防止未来 Godot API 中超过 JavaScript 安全整数范围的数值 enum 或常量继续被生成到 `number` 契约中。 -- 增加完整性测试,校验生成 utility functions 在 C++ `GD` runtime object、TypeScript 声明、vararg 声明和 utility-specific 类型 override 之间保持一致。 -- 增加完整性测试,校验 MethodBind out-argument 策略项仍与 extension API、生成的 C++ MethodBind 调用路径、当前 method hash、out-argument 索引和 TypeScript 声明一致。 -- 增加完整性测试,防止生成的 TypeScript 声明继续声称不受运行时支持的 builtin numeric bracket access。 -- 增加完整性测试,校验生成的 Godot class 可实例化性在 extension API metadata、运行时 constructor 分支、singleton 背后类型和 TypeScript `abstract` 声明之间保持一致。 -- 在 C++ binding generator 和 TypeScript declaration generator 之间共享 builtin 成员/方法冲突过滤,避免未来 `get_*/set_*` 成员 accessor 在运行时和声明之间漂移。 -- 在 C++ binding generator 和 TypeScript declaration generator 之间共享 builtin operator 方法命名与跳过策略,并增加完整性测试对照 extension API 校验生成的运行时 binding 和声明。 -- 改进生成的数组迭代器和调用热路径:迭代器复用统一数组长度校验并检查待处理异常,生成调用参数数组预分配容量以减少重复分配,vararg 返回值统一走精确结果转换。 -- 加固 generator 输入校验:缺少 `extension_api.json` 或必需 API section 时,codegen 现在会以非 0 退出失败,不再只打印错误后继续表现为生成成功。 -- 改进 Inspector 整数配置解析,允许数值型配置中的整数浮点值,同时继续拒绝非整数或越界值并输出警告。 -- 补充生成器、仓库完整性、TypeScript 编译器脚本和运行时集成测试,覆盖 `int64`/`bigint` 往返、`tsconfig` include/exclude、TSX `.jsx` specifier 重写、精确整数默认参数、生成绑定契约、直接构造 RefCounted 对象和 builtin enum 运行时导出。 +- 改进项目编辑、运行和导出时对 `tsconfig.json`、TSX 和导入路径别名的支持。 +- 修复导出属性在 Inspector 中的提示、默认值和继承行为。 +- 修复脚本重载、动态导入及模块加载失败后的重试,完善错误提示。 +- 修复导出项目缺少脚本或 npm 依赖的问题,文件缺失时提供更明确的错误信息。 +- 修复 64 位整数精度,超出 JavaScript 安全整数范围的值返回 `bigint`。 +- 修正 Godot API 类型声明和绑定,覆盖数组、字典、枚举、构造函数及只读属性。 +- 完善编辑器语法检查、函数定位和运行时对象稳定性。 ## 2.2.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index 96900b4..547140c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## 2.4.5 + +- Fixed Inspector display and defaults for inherited exported properties. +- Added TypedArray support for numeric packed arrays. +- Fixed resource lifetime handling so resources retained by Godot remain valid after JavaScript garbage collection. + ## 2.4.4 - Added Inspector editing for exported `interface[]` properties, including nested fields and defaults. @@ -18,20 +24,16 @@ ## 2.4.1 -- Fixed TypeScript built-in script templates so Godot no longer drops the Default and Tool templates in the New Script dialog because of missing template `origin` metadata. -- Added a bundled `gode_node` helper and npm snapshot materialization for package paths and forked dependency trees, so `child_process.fork()`, ESM `import.meta.url` path lookups, and native `.node` loading can run real desktop package probes without relying on a user-installed Node executable at runtime. -- Added packaged-plugin export smoke coverage for Windows, Linux, and macOS using a minimal Godot `node-llama-cpp` project. -- Hardened release packaging by retrying transient TypeScript compiler download failures. -- Added a pull-request identity workflow that rejects commits attributed to blocked AI coding tools by checking commit authors, committers, and `Co-authored-by` / `Signed-off-by` trailers without checking out pull-request code. +- Fixed missing Default and Tool templates when creating TypeScript scripts. +- Improved desktop npm package support for subprocesses and native modules without a separate Node.js installation. ## 2.4.0 -- Breaking: split the native extension into `gode_runtime` and desktop-only `gode_editor`; exports now include only the target platform runtime binaries and exclude editor-only files. -- Breaking: TypeScript static methods no longer appear on script instances. Use script-resource APIs `has_static_method()`, `get_static_method_argument_count()`, and `call_static()` for default-class static methods. -- Moved project TypeScript compilation into the editor extension; `GodeTypeScriptCompiler` exposes direct static compile methods, `compile_project_static` is removed, and exported runtimes load only manifest-listed outputs with cached manifest validation. -- Tightened TypeScript editor method discovery to direct instance methods on the default exported class, excluding static methods, constructors, accessors, and nested declarations. -- Shared async Promise rejection reporting across instance calls, notifications, and static-method calls. -- Updated build, package, CI, and test coverage for the split library layout, Windows `node.dll` Node-API forwarder, iOS deployment target 16.0, static-method calls, and editor method filtering. +- Breaking: plugin files have been reorganized; update the complete plugin when upgrading. +- Exports now include only the target platform's runtime files, excluding editor components. +- Breaking: call TypeScript static methods through the script resource's `call_static()`. +- Fixed the editor's script method list and improved async error messages. +- iOS exports require iOS 16.0 or later. ## 2.3.3 @@ -45,58 +47,22 @@ ## 2.3.2 -- Updated GitHub Actions artifact download steps to `actions/download-artifact@v8`, removing Node.js 20 deprecation warnings from release packaging and packaged-plugin smoke-test artifact downloads. -- Fixed TypeScript exported Object/Resource/Node properties so script instance state saving returns stored export values, Resource and Node inspector hints are inferred from typed exports, and static export defaults are parsed after the descriptor type regardless of field order. -- Improved generated-binding build ergonomics by skipping configure-time code generation when generator inputs are unchanged, batching generated C++ sources with generated-only unity builds, and automatically using `sccache` or `ccache` when available. -- Fixed libnode static linking so platform builds force-load the full libnode archive instead of relying on the linker's default demand-loaded archive member selection. +- Fixed scene saving, Inspector hints, and default values for exported Object, Resource, and Node properties. +- Reduced plugin rebuild times and fixed native dependency linking. ## 2.3.1 -- Added ambient TypeScript aliases for Godot numeric global types: `int` now resolves to `number | bigint` to match 64-bit integer conversion behavior, and `float` resolves to `number`. -- Hardened the release workflow so manually dispatched releases reject `v`-prefixed tags and publish plain SemVer tag and release names consistently. +- Added `int` (`number | bigint`) and `float` (`number`) type aliases for TypeScript scripts. ## 2.3.0 -- Improved the bundled TypeScript compiler to honor `tsconfig.json` `include`, `exclude`, `baseUrl`, `paths`, and parsed root files, use TypeScript `matchFiles` over the `res://` virtual path space, emit only sources that are actually part of the program, enable TSX `jsx: react` by default, report unreadable project source files instead of silently omitting them, reject module specifiers that normalize above the `res://` root, and rewrite in-project path-alias imports/exports through an emit transformer to runtime-resolvable relative `.js` specifiers. -- Improved the TypeScript metadata parser so external interfaces and parent-class property collection support `.tsx`, `.d.ts`, `index.ts(x)`, `index.d.ts`, and `.js/.jsx/.mjs/.cjs` specifier mappings back to TypeScript sources, normalize relative import paths with loader-compatible rules, fall back to real JS/CJS/JSX or extensionless files when no matching TypeScript source exists, resolve default script classes declared through split exports such as `class Foo ...; export default Foo` or `export { Foo as default }`, unwrap TypeScript-only metadata expressions such as `as const`, `satisfies`, non-null assertions, type assertions, and parentheses before parsing static `signals`, `rpc_config`, `exports`, `@Export()` options, and exported property defaults, handle quoted metadata object keys consistently, preserve Inspector `hint` and `hint_string` metadata from both `@Export()` options and `static exports`, expose typed `static exports` `default` descriptors as Godot Variant-compatible values, recognize default imports, named-import aliases, and namespace imports for TypeScript parent scripts, parse `extends godot.Node` and simple generic bases into the correct Godot base type, and report or skip inputs that fail file reads or tree-sitter parser setup/parsing; numeric defaults now use non-throwing shared parsing with numeric separator and `0b`/`0o`/`0x` integer literal support, and invalid RPC config values no longer silently coerce to `0`/`false`. -- Added TypeScript compile and export manifests: compilation reuses previous results from input modification times, the current input-set signature, and existing outputs while including content hashes for the compiler bridge script and bundled TypeScript runtime in the signature; it also records and validates the outputs that were actually emitted, canonicalizing path-boundary checks, rejecting parent-directory segments, and refusing invalid public `.ts`/`.tsx` source path requests. `compile_script()`, script loading, and exported runtimes now only accept valid outputs listed by the active manifest, and recompiles prune stale `.js`/`.js.map` cache outputs to avoid running or retaining artifacts from sources later excluded by `tsconfig`. -- Hardened TypeScript and npm runtime snapshot export handling so missing compiled output paths, incomplete output mappings, script/dependency file read failures, or export config read failures abort the export instead of producing a partial package with missing script files, dependency files, or a manifest; npm snapshot include/exclude paths are normalized to `res://`, reject parent-directory segments and non-resource schemes, and `package.json` read failures now emit an explicit warning. -- Hardened the TypeScript resource loader so `.ts`/`.tsx` read failures return `ERR_CANT_OPEN`, loaded scripts bind their normalized resource path before source compilation, the internal script cache now uses normalized resource-path keys while honoring Godot `CACHE_MODE_IGNORE` / `CACHE_MODE_IGNORE_DEEP` instead of caching ignored loads, default-export class names and base classes, including split default exports, and `godot` named imports are reported from source through tree-sitter's actual import-clause nodes without starting Node, resource dependencies are discovered directly from TypeScript import/export specifiers without triggering compilation, and Godot dependency renames rewrite only matching relative import/export/dynamic-import specifiers while preserving runtime `.js` output specifier style for TypeScript sources. -- Fixed TypeScript dependency scanning for dynamic `import()` so only literal first-argument module specifiers are treated as dependencies; string literals inside non-literal expressions or import attributes/options are no longer reported as dependencies or rewritten by dependency rename. -- Hardened TypeScript runtime script loading so compiled `.js` read failures log a concrete error and stop instantiation instead of handing empty code to Node. -- Hardened TypeScript instance creation so invalid scripts, failed JavaScript class construction, or failed Godot script-instance allocation no longer leave half-initialized `ScriptInstance` entries in the runtime caches. -- Hardened `ScriptInstance` V8 entry points so property access, method lookup/call, notifications, `toString()`, construction, and reload all require a running NodeRuntime before entering V8; V8 locker/isolate/handle scopes are now ordered consistently in script entries, NodeRuntime entries that return N-API handles now use escapable handle scopes, and method argument-count metadata queries no longer take a V8 locker unnecessarily. -- Fixed TypeScript script compile state so non-dirty scripts report their real validity, dirty recompiles clear stale metadata, parent-script paths, and cached JavaScript default classes before failing or rebuilding, invalid source updates no longer keep old exported properties, parent scripts, or classes alive, `static exports` and same-file parent exported fields now maintain the ordered property list, `_get_base_script()` / `_inherits_script()` expose and recursively inspect parsed TypeScript parent scripts, and failed reloads now return an error instead of reporting `OK`. -- Improved NodeRuntime CJS/ESM compile failure and reload handling by logging V8 exception stacks/messages for bootstrap, `run_script()`, CommonJS compile, ESM compile, and rejected ESM compile promises instead of returning empty exports with generic diagnostics, by linking/evaluating ESM modules resolved through dynamic `import()` before handing them back to Node, by invalidating successful ESM caches when the same path is recompiled with new source or project-side dependencies, by returning valid `file://` `import.meta.url` values without internal cache-busting suffixes, and by clearing pending ESM/CJS module-load caches plus intermediate ESM module caches after failures so transient parse, link, evaluate, or require errors can be retried after source fixes. -- Improved the TypeScript language editor surface with TypeScript reserved/control-flow keywords, block/doc/string delimiters, built-in script templates, path validation for `.ts`/`.tsx` scripts under `res://`, lightweight tree-sitter syntax validation and function lists, function stub generation, function lookup, named-class/global-class type support, and reload hooks that refresh cached or explicitly provided TypeScript scripts from the latest file contents instead of no-oping or reusing stale in-memory source; global class indexing now reads and parses TypeScript source directly with tree-sitter, recognizes split default exports plus namespaced/generic base types, and avoids loading the resource or starting Node. -- Fixed Godot 64-bit integer conversion to JavaScript: safe integers now return `number`, larger values return `bigint`, and generated method returns, class constants, enum values, and default arguments use the same precise integer conversion path. -- Hardened Godot `Array`, `Dictionary`, and packed-array conversion to JavaScript with array length range checks, no silent `uint32_t` truncation, and early exits when element, key/value, `Map`, or object writes raise JavaScript exceptions. -- Updated TypeScript declaration generation to distinguish narrow integers from 64-bit integers through `meta`, keep `NodePath` returns as `NodePath` while accepting `NodePath | string` inputs, and correct declarations for `GD.typeof()`, `type_convert()`, `type_string()`, `error_string()`, and `get_instance_id()`. -- Corrected `GD.instance_from_id()` declarations to return `GodotObject | null`, matching invalid ObjectID runtime behavior. -- Generated the `VariantType` and `VariantOperator` TypeScript aliases from `extension_api.json` instead of hand-maintained enum tables, with integrity coverage that checks every emitted enum value against the Godot API data. -- Broadened generated TypeScript declarations for typed `Array`/`Dictionary` inputs so nested `NodePath`, `String`, `StringName`, packed arrays, and other Variant-compatible element/value types use the same input-friendly aliases as direct method arguments. -- Deduplicated exact duplicate TypeScript declaration lines within generated class and utility-function scopes so input-friendly aliases do not produce repeated constructor, method, or operator signatures. -- Removed generated numeric index signatures for builtin classes because runtime exposes indexed data through `get()` / `set()` methods and iterators, not JavaScript bracket property access. -- Marked non-instantiable Godot classes and singleton backing types as `abstract` in generated TypeScript declarations so `new AnimationMixer()`-style calls are rejected by TypeScript when the runtime constructor rejects direct construction. -- Fixed generated builtin class enum values such as `Vector2.AXIS_X` so they are exported at runtime on both constructors and prototypes, matching the TypeScript declarations. -- Aligned generated TypeScript property declarations with the runtime binding generator so properties are only declared when their getter is actually bound, and setters are only declared when the matching setter accessor is bound. -- Added integrity coverage that checks generated Godot builtin class constants and enum values stay aligned across constructor exports, prototype exports, and TypeScript declarations. -- Added integrity coverage that checks every generated Godot builtin constructor overload is represented by a TypeScript constructor signature and a runtime arity/type matcher. -- Added integrity coverage that checks every bindable generated Godot builtin class method is present in both the C++ runtime binding and the TypeScript declaration surface. -- Added integrity coverage that checks every bindable generated Godot class method is present in both the C++ runtime binding and the TypeScript declaration surface. -- Added integrity coverage that checks generated Godot class signals are exposed through C++ accessors, header declarations, Godot `Signal` wrappers, and TypeScript declarations. -- Added integrity coverage that checks generated Godot class constants and enum values stay aligned across constructor exports, prototype exports for singleton instances, and TypeScript declarations. -- Added integrity coverage that guards generated numeric enum and constant declarations against future Godot API values that would exceed JavaScript's safe integer range. -- Added integrity coverage that checks generated utility functions stay aligned across the C++ `GD` runtime object, TypeScript declarations, vararg declarations, and utility-specific type overrides. -- Added integrity coverage that checks MethodBind out-argument policy entries still match the extension API, generated C++ MethodBind call path, current method hashes, out-argument indices, and TypeScript declarations. -- Added integrity coverage that prevents generated TypeScript declarations from claiming unsupported builtin numeric bracket access. -- Added integrity coverage that checks generated Godot class instantiability stays aligned across extension API metadata, runtime constructor branches, singleton backing types, and TypeScript `abstract` declarations. -- Shared builtin member/method conflict filtering between the C++ binding generator and TypeScript declaration generator so future `get_*/set_*` member accessors cannot drift between runtime and declarations. -- Shared builtin operator method naming and skipped-operator policy between the C++ binding generator and TypeScript declaration generator, with integrity coverage that checks generated runtime bindings and declarations against the extension API. -- Improved generated array iterators and call hot paths: iterators reuse the shared array length guard and check pending exceptions, generated call argument arrays pre-reserve capacity, and vararg returns use the precise result conversion path. -- Hardened generator input validation so missing `extension_api.json` files or required API sections now fail code generation with a non-zero exit instead of printing an error and continuing as if generation succeeded. -- Improved Inspector integer configuration parsing to accept numeric integral float values while still rejecting non-integer or out-of-range values with warnings. -- Added generator, repository integrity, TypeScript compiler script, and runtime integration coverage for `int64`/`bigint` round-trips, `tsconfig` include/exclude handling, TSX `.jsx` specifier rewriting, precise integer default arguments, generated binding contracts, direct RefCounted construction, and builtin enum runtime exports. +- Improved support for `tsconfig.json`, TSX, and import path aliases across editing, running, and exporting projects. +- Fixed exported-property hints, defaults, and inheritance in the Inspector. +- Fixed script reloads, dynamic imports, and recovery after module-loading errors; improved error messages. +- Fixed missing scripts and npm dependencies in exported projects, with clearer errors when export files are unavailable. +- Preserved 64-bit integer precision by returning `bigint` for values outside JavaScript's safe integer range. +- Corrected Godot API type declarations and bindings, including arrays, dictionaries, enums, constructors, and read-only properties. +- Improved editor syntax checks, function navigation, and runtime object stability. ## 2.2.0 diff --git a/CMakeLists.txt b/CMakeLists.txt index 395bc9d..5617eb6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,7 +3,7 @@ cmake_minimum_required(VERSION 3.22) cmake_policy(SET CMP0091 NEW) set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>" CACHE STRING "Select the MSVC runtime library." FORCE) -project(gode VERSION 2.4.4 LANGUAGES C CXX) +project(gode VERSION 2.4.5 LANGUAGES C CXX) if(NOT CMAKE_CONFIGURATION_TYPES) if(NOT CMAKE_BUILD_TYPE) diff --git a/README-ZH.md b/README-ZH.md index 329c23b..e70d2c3 100644 --- a/README-ZH.md +++ b/README-ZH.md @@ -87,6 +87,20 @@ export default class Demo extends Node { ## 进阶用法 +### 数值数组批量传递 + +接收以下 PackedArray 的构造函数、方法参数和属性赋值,也支持对应的 JavaScript TypedArray: + +| Godot PackedArray | JavaScript 输入 | +| --- | --- | +| `PackedByteArray` | `Uint8Array`、`Uint8ClampedArray` | +| `PackedInt32Array` | `Int32Array` | +| `PackedInt64Array` | `BigInt64Array` | +| `PackedFloat32Array` | `Float32Array` | +| `PackedFloat64Array` | `Float64Array` | + +Gode 一次性将连续数据复制到 Godot 持有的内存中,并遵循视图的偏移和长度。调用后修改源数组不会改变已提交的数据。Int64 值保留完整的 64 位精度。类型不匹配的 TypedArray 会抛出 `TypeError`;需要逐元素转换时,可以使用普通 JavaScript 数组。 + ### TypeScript 与 GDScript 互相调用 下面是一个完整的节点结构示例: @@ -262,6 +276,8 @@ button.connect("pressed", () => { TypeScript 中加载的资源是普通 Godot 资源,在 JavaScript wrapper 持有期间会保持正确的 Godot 生命周期: +JavaScript 包装对象只释放自己成功取得的原生引用。只要 Godot 仍持有资源,包装对象被垃圾回收后资源依然有效。例如,将 `StyleBoxFlat` 交给 `button.add_theme_stylebox_override()` 后,无需额外使用 `style_refs` 数组保活包装对象。 + ```ts import { Node, ResourceLoader } from "godot"; diff --git a/README.md b/README.md index 44483b4..d31a0c5 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,20 @@ export default class Demo extends Node { ## Advanced Usage +### Bulk Numeric Arrays + +Constructors, method arguments and property assignments taking these packed arrays accept the corresponding JavaScript typed input: + +| Godot packed array | JavaScript input | +| --- | --- | +| `PackedByteArray` | `Uint8Array`, `Uint8ClampedArray` | +| `PackedInt32Array` | `Int32Array` | +| `PackedInt64Array` | `BigInt64Array` | +| `PackedFloat32Array` | `Float32Array` | +| `PackedFloat64Array` | `Float64Array` | + +Gode copies the contiguous data into Godot-owned memory in one operation, honoring the view's offset and length. Changing the source after the call does not change submitted data. Int64 values preserve all 64 bits. Mismatched typed inputs throw `TypeError`; use the matching layout or an ordinary JavaScript array for element conversion. + ### Calling Between TypeScript and GDScript Here is a complete node setup: @@ -262,6 +276,8 @@ button.connect("pressed", () => { Resources loaded from TypeScript are normal Godot resources and keep their Godot lifetime while wrapped by JavaScript: +A JavaScript wrapper releases only the native reference it successfully acquired. Resources retained by Godot remain valid after the wrapper is garbage collected. For example, after passing a `StyleBoxFlat` to `button.add_theme_stylebox_override()`, no extra `style_refs` array is needed to keep the wrapper alive. + ```ts import { Node, ResourceLoader } from "godot"; diff --git a/example/addons/gode/plugin.cfg b/example/addons/gode/plugin.cfg index 23d728b..9e7d649 100644 --- a/example/addons/gode/plugin.cfg +++ b/example/addons/gode/plugin.cfg @@ -4,5 +4,5 @@ name="gode" description="Godot with TypeScript and Node.js" website="https://godothub.com" author="GodotHub" -version="2.4.4" +version="2.4.5" script="gode.gd" diff --git a/example/scripts/tests/runtime_integration_test.ts b/example/scripts/tests/runtime_integration_test.ts index 346b172..a9b5067 100644 --- a/example/scripts/tests/runtime_integration_test.ts +++ b/example/scripts/tests/runtime_integration_test.ts @@ -6,7 +6,7 @@ import { fileURLToPath } from "node:url"; import v8 from "node:v8"; import vm from "node:vm"; import * as GodotModule from "godot"; -import { Color, Engine, GD, GDArray, GDDictionary, GDString, GodotObject, Image, ImageTexture, Node, PackedInt32Array, PackedScene, PackedStringArray, PackedVector3Array, PropertyHint, PropertyHint as PropertyHintAlias, Resource, ResourceLoader, ResourceSaver, type VariantArgument, VariantType, Vector2, Vector2i, Vector3 } from "godot"; +import { AudioStreamWAV, Button, Color, DisplayServer, Engine, GD, GDArray, GDDictionary, GDString, GodotObject, Image, ImageTexture, MultiMesh, Node, PackedByteArray, PackedFloat64Array, PackedInt64Array, PackedFloat32Array, PackedInt32Array, PackedScene, PackedStringArray, PackedVector3Array, PropertyHint, PropertyHint as PropertyHintAlias, QuadMesh, Resource, ResourceLoader, ResourceSaver, StyleBoxFlat, type VariantArgument, VariantType, Vector2, Vector2i, Vector3, VideoStreamPlayback, WeakRef as GodotWeakRef } from "godot"; import cjsFixture, { makeCommonPayload } from "./commonjs_fixture.cjs"; import type RuntimeArrayResource from "./runtime_array_resource.js"; import type RuntimeExternalResource from "./runtime_external_resource.js"; @@ -642,6 +642,36 @@ class RuntimeIntegrationTest extends RuntimeSameFileExportBase { nodeAssert.equal(texture.get_image().get_width(), 2); // @ts-expect-error Intentional invalid call to verify object validation. nodeAssert.throws(() => ImageTexture.create_from_image({}), TypeError); + + const themeButton = new Button(); + this.add_child(themeButton); + const makeStyleReferences = (retain: boolean) => { + const style = new StyleBoxFlat(); + style.bg_color = new Color(0.25, 0.5, 0.75, 1); + nodeAssert.equal(style.get_reference_count(), 1); + if (retain) { + themeButton.add_theme_stylebox_override("normal", style); + assert(style.get_reference_count() > 1, "Godot did not acquire its own StyleBox reference"); + } + return { wrapper: new WeakRef(style), native: GD.weakref(style) as GodotWeakRef }; + }; + const retainedStyleRefs = makeStyleReferences(true); + const unretainedStyleRefs = makeStyleReferences(false); + for (let i = 0; i < 20; i++) { + await waitForEventLoopTurn(); + forceGarbageCollection(); + await waitForEventLoopTurn(); + } + nodeAssert.equal(retainedStyleRefs.wrapper.deref(), undefined, "Godot must not keep the JS wrapper alive"); + nodeAssert.equal(unretainedStyleRefs.wrapper.deref(), undefined, "Unretained JS wrapper was not collected"); + nodeAssert.equal(unretainedStyleRefs.native.get_ref(), null, "Unretained native resource was not released"); + const retainedStyle = retainedStyleRefs.native.get_ref() as StyleBoxFlat; + assert(retainedStyle instanceof StyleBoxFlat, "Godot-owned resource was released with its JS wrapper"); + nodeAssert.equal(retainedStyle.bg_color.g, 0.5); + nodeAssert.equal(themeButton.get_theme_stylebox("normal").get_instance_id(), retainedStyle.get_instance_id()); + themeButton.remove_theme_stylebox_override("normal"); + themeButton.queue_free(); + // @ts-expect-error Intentional invalid call to verify type validation. nodeAssert.throws(() => Color.from_ok_hsl("0.58", 0.5, 0.79), TypeError); nodeAssert.throws(() => Color.from_ok_hsl(NaN, 0.5, 0.79), TypeError); @@ -706,6 +736,98 @@ class RuntimeIntegrationTest extends RuntimeSameFileExportBase { // @ts-expect-error Intentional invalid constructor call to verify overload validation. nodeAssert.throws(() => new GDArray(1), /No matching constructor overload for GDArray/); const packedInts = new PackedInt32Array([1, 2, 3]); + const floatStorage = new Float32Array([99, 1.25, -2.5, 88]); + const floatView = floatStorage.subarray(1, 3); + const packedFloats = new PackedFloat32Array(floatView); + nodeAssert.equal(packedFloats.size(), 2); + nodeAssert.equal(packedFloats.get(0), 1.25); + nodeAssert.equal(packedFloats.get(1), -2.5); + floatView[0] = 7; + nodeAssert.equal(packedFloats.get(0), 1.25, "typed input must be copied, not retained"); + packedFloats.append_array(new Float32Array([3.5])); + nodeAssert.equal(packedFloats.get(2), 3.5); + nodeAssert.equal(new PackedFloat32Array(new Float32Array(0)).size(), 0); + // @ts-expect-error Wrong element type must fail instead of reinterpreting bytes. + nodeAssert.throws(() => packedFloats.append_array(new Float64Array([1])), TypeError); + const bytes = new Uint8Array([99, 0, 255, 88]).subarray(1, 3); + const packedBytes = new PackedByteArray(bytes); + nodeAssert.deepEqual([packedBytes.get(0), packedBytes.get(1)], [0, 255]); + bytes[1] = 1; + nodeAssert.equal(packedBytes.get(1), 255); + packedBytes.append_array(new Uint8ClampedArray([-1, 300])); + nodeAssert.deepEqual([packedBytes.get(2), packedBytes.get(3)], [0, 255]); + nodeAssert.equal(new PackedByteArray(new Uint8Array(0)).size(), 0); + nodeAssert.equal(new PackedByteArray(new Uint8ClampedArray(0)).size(), 0); + const ints = new Int32Array([99, -2147483648, 2147483647, 88]).subarray(1, 3); + const packedTypedInts = new PackedInt32Array(ints); + nodeAssert.deepEqual([packedTypedInts.get(0), packedTypedInts.get(1)], [-2147483648, 2147483647]); + ints[0] = 0; + nodeAssert.equal(packedTypedInts.get(0), -2147483648); + packedTypedInts.append_array(new Int32Array([-7])); + nodeAssert.equal(packedTypedInts.get(2), -7); + nodeAssert.equal(new PackedInt32Array(new Int32Array(0)).size(), 0); + const bigInts = new BigInt64Array([99n, -9223372036854775808n, 9223372036854775807n, 88n]).subarray(1, 3); + const packedTypedBigInts = new PackedInt64Array(bigInts); + nodeAssert.deepEqual([packedTypedBigInts.get(0), packedTypedBigInts.get(1)], [-9223372036854775808n, 9223372036854775807n]); + bigInts[0] = 0n; + nodeAssert.equal(packedTypedBigInts.get(0), -9223372036854775808n); + packedTypedBigInts.append_array(new BigInt64Array([9007199254740993n])); + nodeAssert.equal(packedTypedBigInts.get(2), 9007199254740993n); + nodeAssert.equal(new PackedInt64Array(new BigInt64Array(0)).size(), 0); + const doubles = new Float64Array([99, Math.PI, -1e100, 88]).subarray(1, 3); + const packedDoubles = new PackedFloat64Array(doubles); + nodeAssert.deepEqual([packedDoubles.get(0), packedDoubles.get(1)], [Math.PI, -1e100]); + doubles[0] = 0; + nodeAssert.equal(packedDoubles.get(0), Math.PI); + packedDoubles.append_array(new Float64Array([1e-100])); + nodeAssert.equal(packedDoubles.get(2), 1e-100); + nodeAssert.equal(new PackedFloat64Array(new Float64Array(0)).size(), 0); + // @ts-expect-error Wrong layouts must fail without changing existing values. + nodeAssert.throws(() => packedBytes.append_array(new Int8Array([-1])), TypeError); + // @ts-expect-error A floating view is not an integer view. + nodeAssert.throws(() => new PackedInt32Array(new Float32Array([1])), TypeError); + // @ts-expect-error Unsigned int64 cannot be reinterpreted as signed int64. + nodeAssert.throws(() => packedTypedBigInts.append_array(new BigUint64Array([1n])), TypeError); + // @ts-expect-error Float32 and Float64 have different layouts. + nodeAssert.throws(() => packedDoubles.append_array(new Float32Array([1])), TypeError); + nodeAssert.equal(packedBytes.size(), 4); + nodeAssert.equal(packedTypedBigInts.size(), 3); + nodeAssert.equal(packedDoubles.size(), 3); + const wav = new AudioStreamWAV(); + const wavInput = new Uint8Array([99, 10, 20, 88]).subarray(1, 3); + wav.data = wavInput; + wavInput[0] = 0; + nodeAssert.deepEqual([wav.data.get(0), wav.data.get(1)], [10, 20]); + nodeAssert.throws(() => { + // @ts-expect-error Incorrect property input must leave the stored data intact. + wav.data = new Float32Array([1]); + }, TypeError); + nodeAssert.deepEqual([wav.data.get(0), wav.data.get(1)], [10, 20]); + const playback = new VideoStreamPlayback(); + // Zero frames exercises MethodBind conversion without an audio callback. + nodeAssert.equal(playback.mix_audio(0, new Float32Array([99, 1, 2, 88]).subarray(1, 3)), 0); + nodeAssert.equal(playback.mix_audio(0, new Float32Array(0)), 0); + nodeAssert.equal(playback.mix_audio(0, new PackedFloat32Array([1, 2])), 0); + nodeAssert.equal(playback.mix_audio(0, [1, 2]), 0); + // Ordinary array inputs keep the MethodBind path's existing Variant conversion. + nodeAssert.equal(playback.mix_audio(0, [NaN]), 0); + nodeAssert.equal(playback.mix_audio(0), 0); + // @ts-expect-error MethodBind inputs must enforce the same layout as ordinary methods. + nodeAssert.throws(() => playback.mix_audio(0, new Float64Array([1])), /PackedFloat32Array requires a Float32Array/); + // The headless dummy renderer does not store MultiMesh instance data. + if (DisplayServer.get_name() !== "headless") { + const mesh = new MultiMesh(); + mesh.transform_format = MultiMesh.TRANSFORM_2D; + mesh.use_colors = true; + mesh.mesh = new QuadMesh(); + mesh.instance_count = 1; + const meshBuffer = new Float32Array([1, 0, 0, 12, 0, 1, 0, 34, 0.25, 0.5, 0.75, 1]); + mesh.set_buffer(meshBuffer); + meshBuffer[3] = 99; + nodeAssert.equal(mesh.get_instance_transform_2d(0).origin.x, 12); + nodeAssert.equal(mesh.get_instance_transform_2d(0).origin.y, 34); + nodeAssert.equal(mesh.get_instance_color(0).g, 0.5); + } nodeAssert.equal(packedInts.size(), 3); nodeAssert.equal(packedInts.get(1), 2); packedInts.append_array([4, 5]); diff --git a/generator/builtin_classes_generator.py b/generator/builtin_classes_generator.py index 0fc7336..fe8203f 100644 --- a/generator/builtin_classes_generator.py +++ b/generator/builtin_classes_generator.py @@ -12,6 +12,7 @@ from .utils.type_mappings import ( GENERATED_BUILTIN_TYPES, PACKED_ARRAY_TYPES, + PACKED_ARRAY_TYPED_INPUTS, constant_cpp_value, default_arg_napi_expr, get_cpp_type, @@ -47,8 +48,9 @@ def napi_match_expr(type_name, index, allow_number_for_int=True): f"({value}.IsObject() && {value}.As().InstanceOf(ArrayBinding::constructor.Value()))" ) if type_name in PACKED_ARRAY_TYPES: + typed_input = f"{value}.IsTypedArray() || " if type_name in PACKED_ARRAY_TYPED_INPUTS else '' return ( - f"{value}.IsArray() || " + f"{typed_input}{value}.IsArray() || " f"({value}.IsObject() && {value}.As().InstanceOf({type_name}Binding::constructor.Value()))" ) if type_name.startswith('typedarray::'): diff --git a/generator/class_generator.py b/generator/class_generator.py index 0aabb5c..7483da1 100644 --- a/generator/class_generator.py +++ b/generator/class_generator.py @@ -122,7 +122,10 @@ def process_type(type_name): method['out_argument_indices'] = out_argument_indices method['has_out_arguments'] = bool(out_argument_indices) method['out_argument_indices_cpp'] = ', '.join(str(index) for index in out_argument_indices) - method['argument_count'] = len(method.get('arguments', [])) + method['method_bind_argument_types_cpp'] = ', '.join( + get_cpp_type(arg['type'], arg.get('meta', ''), refcounted_classes, True) + for arg in method.get('arguments', []) + ) if out_argument_indices else '' if class_name == 'Node' and method['name'] == 'get_node': method['name_cpp'] = 'get_node_internal' diff --git a/generator/dts_generator.py b/generator/dts_generator.py index 4922da7..5e5450f 100644 --- a/generator/dts_generator.py +++ b/generator/dts_generator.py @@ -15,6 +15,7 @@ from .utils.type_mappings import ( JS_CLASS_RENAME_MAP, PACKED_ARRAY_ELEMENT_TYPES, + PACKED_ARRAY_TYPED_INPUTS, parse_typedarray_element_type, parse_typeddictionary_types, ) @@ -169,7 +170,8 @@ def godot_type_to_ts(type_str: str, is_input: bool = False, singleton_class_name if is_input and type_str in PACKED_ARRAY_ELEMENT_TYPES: element_type = PACKED_ARRAY_ELEMENT_TYPES[type_str] element_ts = godot_type_to_ts(element_type, is_input=True, singleton_class_names=singleton_class_names) - return f'{type_str} | Array<{element_ts}>' + typed_input = f' | {PACKED_ARRAY_TYPED_INPUTS[type_str]}' if type_str in PACKED_ARRAY_TYPED_INPUTS else '' + return f'{type_str} | Array<{element_ts}>{typed_input}' if not is_input and type_str == 'Array': return JS_ARRAY_TYPE diff --git a/generator/templates/class_binding.cpp.jinja2 b/generator/templates/class_binding.cpp.jinja2 index 627aebb..c983bf0 100644 --- a/generator/templates/class_binding.cpp.jinja2 +++ b/generator/templates/class_binding.cpp.jinja2 @@ -44,8 +44,7 @@ void {{ class_name }}Binding_wrap(const Napi::Object &obj, godot::Object *instan binding->referenced_instance = false; godot::RefCounted *ref = godot::Object::cast_to(binding->instance); if (ref) { - ref->reference(); - binding->referenced_instance = true; + binding->referenced_instance = ref->reference(); } } @@ -230,12 +229,11 @@ Napi::Value {{ class_name }}Binding::init(Napi::Env env, Napi::Object exports) { if (instance) { internal_object_id = instance->get_instance_id(); {% if is_ref_counted %} - if (!owns_instance) { - godot::RefCounted *ref = godot::Object::cast_to(instance); - if (ref) { - ref->reference(); - referenced_instance = true; - } + godot::RefCounted *ref = dynamic_cast(instance); + if (ref) { + referenced_instance = owns_instance + ? ref->init_ref() // New object: claim its initial reference. + : ref->reference(); // Existing object: acquire a reference. } {% endif %} register_godot_instance(instance, info.This().As()); @@ -248,7 +246,7 @@ Napi::Value {{ class_name }}Binding::init(Napi::Env env, Napi::Object exports) { godot::Object* obj = godot::ObjectDB::get_instance(internal_object_id); if (obj) { godot::RefCounted* ref = godot::Object::cast_to(obj); - if (ref && (owns_instance || referenced_instance)) { + if (ref && referenced_instance) { if (ref->unreference()) { godot::memdelete(ref); } @@ -307,14 +305,13 @@ Napi::Value {{ class_name }}Binding::{{ method.name_cpp }}(const Napi::CallbackI }; {% endif %} {% if method.has_out_arguments %} - return call_class_method_bind( + return call_class_method_bind<{{ method.method_bind_argument_types_cpp }}>( {{ "nullptr" if method.is_static else "instance" }}, "{{ godot_class_name }}", "{{ method.name }}", {{ method.hash }}, {{ "true" if method.return_type else "false" }}, info, - {{ method.argument_count }}, {{ "default_args" if method.has_default_args else "std::vector{}" }}, { {{ method.out_argument_indices_cpp }} }); {% elif method.live_property_name and not method.is_static and not method.is_vararg %} diff --git a/generator/utils/type_mappings.py b/generator/utils/type_mappings.py index 9d39ce8..460502a 100644 --- a/generator/utils/type_mappings.py +++ b/generator/utils/type_mappings.py @@ -29,6 +29,15 @@ PACKED_ARRAY_TYPES = frozenset(PACKED_ARRAY_ELEMENT_TYPES) +# Exact native element layouts accepted by the bulk-copy conversion path. +PACKED_ARRAY_TYPED_INPUTS = { + 'PackedByteArray': 'Uint8Array | Uint8ClampedArray', + 'PackedInt32Array': 'Int32Array', + 'PackedInt64Array': 'BigInt64Array', + 'PackedFloat32Array': 'Float32Array', + 'PackedFloat64Array': 'Float64Array', +} + JS_CLASS_RENAME_MAP = { 'Object': 'GodotObject', 'String': 'GDString', diff --git a/include/runtime/func_utils.h b/include/runtime/func_utils.h index d7dbd5d..9600737 100644 --- a/include/runtime/func_utils.h +++ b/include/runtime/func_utils.h @@ -111,6 +111,18 @@ inline bool throw_if_godot_call_failed(Napi::Env env, const GDExtensionCallError return true; } +template +inline godot::Variant convert_method_bind_argument(Napi::Value value) { + using ClearType = std::remove_const_t>; + if constexpr (is_godot_packed_array_v) { + if (value.IsTypedArray()) { + return napi_to_godot(value); + } + } + return napi_to_godot(value); +} + +template inline Napi::Value call_class_method_bind( godot::Object *instance, const char *godot_class_name, @@ -118,11 +130,10 @@ inline Napi::Value call_class_method_bind( uint32_t method_hash, bool has_return, const Napi::CallbackInfo &info, - std::size_t target_count, const std::vector &default_args, std::initializer_list out_arg_indices) { std::vector args = to_args_array(info); - if (!prepare_fixed_args(args, target_count, default_args, info.Env())) { + if (!prepare_fixed_args(args, sizeof...(P), default_args, info.Env())) { return info.Env().Undefined(); } @@ -131,11 +142,11 @@ inline Napi::Value call_class_method_bind( variant_args.reserve(args.size()); arg_ptrs.reserve(args.size()); - for (const auto &arg : args) { - variant_args.push_back(napi_to_godot(arg)); - if (info.Env().IsExceptionPending()) { - return info.Env().Undefined(); - } + std::size_t arg_index = 0; + const bool converted = ((variant_args.emplace_back(convert_method_bind_argument

(args[arg_index++])), + !info.Env().IsExceptionPending()) && ...); + if (!converted) { + return info.Env().Undefined(); } for (size_t i = 0; i < variant_args.size(); i++) { arg_ptrs.push_back(&variant_args[i]); diff --git a/include/runtime/value_convert.h b/include/runtime/value_convert.h index fcad125..c7b7208 100644 --- a/include/runtime/value_convert.h +++ b/include/runtime/value_convert.h @@ -25,6 +25,11 @@ void sync_godot_array_to_js_array(Napi::Env env, const Napi::Value &target, cons void sync_godot_variant_out_argument(Napi::Env env, const Napi::Value &target, const godot::Variant &variant); int64_t napi_to_godot_int64(Napi::Value value); uint64_t napi_to_godot_uint64(Napi::Value value); +godot::PackedByteArray js_uint8_array_to_packed_array(const Napi::Uint8Array &value); +godot::PackedInt32Array js_int32_array_to_packed_array(const Napi::Int32Array &value); +godot::PackedInt64Array js_int64_array_to_packed_array(const Napi::BigInt64Array &value); +godot::PackedFloat32Array js_float32_array_to_packed_array(const Napi::Float32Array &value); +godot::PackedFloat64Array js_float64_array_to_packed_array(const Napi::Float64Array &value); template Napi::Value godot_result_to_napi(Napi::Env env, const T &value) { @@ -375,6 +380,56 @@ std::remove_const_t> napi_to_godot(Napi::Value value) } return napi_to_godot(value); } else if constexpr (is_godot_packed_array_v) { + if constexpr (std::is_same_v) { + if (value.IsTypedArray()) { + const auto type = value.As().TypedArrayType(); + if (type != napi_uint8_array && type != napi_uint8_clamped_array) { + Napi::TypeError::New(value.Env(), "PackedByteArray requires a Uint8Array or Uint8ClampedArray typed input").ThrowAsJavaScriptException(); + return ClearType(); + } + return js_uint8_array_to_packed_array(value.As()); + } + } + else if constexpr (std::is_same_v) { + if (value.IsTypedArray()) { + const auto type = value.As().TypedArrayType(); + if (type != napi_int32_array) { + Napi::TypeError::New(value.Env(), "PackedInt32Array requires a Int32Array typed input").ThrowAsJavaScriptException(); + return ClearType(); + } + return js_int32_array_to_packed_array(value.As()); + } + } + else if constexpr (std::is_same_v) { + if (value.IsTypedArray()) { + const auto type = value.As().TypedArrayType(); + if (type != napi_bigint64_array) { + Napi::TypeError::New(value.Env(), "PackedInt64Array requires a BigInt64Array typed input").ThrowAsJavaScriptException(); + return ClearType(); + } + return js_int64_array_to_packed_array(value.As()); + } + } + else if constexpr (std::is_same_v) { + if (value.IsTypedArray()) { + const auto type = value.As().TypedArrayType(); + if (type != napi_float32_array) { + Napi::TypeError::New(value.Env(), "PackedFloat32Array requires a Float32Array typed input").ThrowAsJavaScriptException(); + return ClearType(); + } + return js_float32_array_to_packed_array(value.As()); + } + } + else if constexpr (std::is_same_v) { + if (value.IsTypedArray()) { + const auto type = value.As().TypedArrayType(); + if (type != napi_float64_array) { + Napi::TypeError::New(value.Env(), "PackedFloat64Array requires a Float64Array typed input").ThrowAsJavaScriptException(); + return ClearType(); + } + return js_float64_array_to_packed_array(value.As()); + } + } if (value.IsArray()) { return js_array_to_packed_array(value.As()); } diff --git a/src/runtime/value_convert.cpp b/src/runtime/value_convert.cpp index 5dbd2fb..3ccb75c 100644 --- a/src/runtime/value_convert.cpp +++ b/src/runtime/value_convert.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -63,6 +64,47 @@ using namespace godot; namespace gode { +template +static PackedArray copy_numeric_typed_array(const JsArray &value) { + PackedArray array; + const size_t length = value.ElementLength(); + using Element = typename packed_array_element::type; + if (length > static_cast(std::numeric_limits::max()) || + length > std::numeric_limits::max() / sizeof(Element)) { + Napi::RangeError::New(value.Env(), "TypedArray is too large for Godot").ThrowAsJavaScriptException(); + return array; + } + if (array.resize(static_cast(length)) != 0) { + Napi::Error::New(value.Env(), "Could not allocate Godot PackedArray").ThrowAsJavaScriptException(); + return array; + } + if (length != 0) { + // Data() includes the view's byte offset. Godot owns this copy after the call. + std::memcpy(array.ptrw(), value.Data(), length * sizeof(Element)); + } + return array; +} + +godot::PackedByteArray js_uint8_array_to_packed_array(const Napi::Uint8Array &value) { + return copy_numeric_typed_array(value); +} + +godot::PackedInt32Array js_int32_array_to_packed_array(const Napi::Int32Array &value) { + return copy_numeric_typed_array(value); +} + +godot::PackedInt64Array js_int64_array_to_packed_array(const Napi::BigInt64Array &value) { + return copy_numeric_typed_array(value); +} + +godot::PackedFloat32Array js_float32_array_to_packed_array(const Napi::Float32Array &value) { + return copy_numeric_typed_array(value); +} + +godot::PackedFloat64Array js_float64_array_to_packed_array(const Napi::Float64Array &value) { + return copy_numeric_typed_array(value); +} + static std::unordered_map class_registry; static std::vector class_order; static std::unordered_map object_cache; diff --git a/test/test_generator_output.py b/test/test_generator_output.py index 7d00da5..76a98d9 100644 --- a/test/test_generator_output.py +++ b/test/test_generator_output.py @@ -90,14 +90,34 @@ def test_typed_collection_parsing_is_shared_and_stable(self): godot_type_to_ts("Dictionary"), ) self.assertEqual( - "PackedInt32Array | Array", + "PackedInt32Array | Array | Int32Array", godot_type_to_ts("PackedInt32Array", is_input=True), ) + self.assertEqual( + "PackedFloat32Array | Array | Float32Array", + godot_type_to_ts("PackedFloat32Array", is_input=True), + ) self.assertEqual( "PackedStringArray | Array", godot_type_to_ts("PackedStringArray", is_input=True), ) + def test_all_numeric_packed_array_inputs(self): + from generator.dts_generator import godot_type_to_ts + from generator.builtin_classes_generator import napi_match_expr + expected = { + "PackedByteArray": "Array | Uint8Array | Uint8ClampedArray", + "PackedInt32Array": "Array | Int32Array", + "PackedInt64Array": "Array | BigInt64Array", + "PackedFloat32Array": "Array | Float32Array", + "PackedFloat64Array": "Array | Float64Array", + } + for packed, inputs in expected.items(): + with self.subTest(packed=packed): + self.assertEqual(f"{packed} | {inputs}", godot_type_to_ts(packed, is_input=True)) + self.assertTrue(napi_match_expr(packed, 0).startswith("info[0].IsTypedArray() ||")) + self.assertEqual(packed, godot_type_to_ts(packed)) + def test_property_accessor_resolution_is_shared_and_stable(self): from generator.utils.binding_policy import ( builtin_operator_method_name, @@ -131,7 +151,7 @@ def test_builtin_argument_matching_accepts_js_arrays_for_array_types(self): napi_match_expr("Array", 0), ) self.assertEqual( - "info[0].IsArray() || (info[0].IsObject() && info[0].As().InstanceOf(PackedInt32ArrayBinding::constructor.Value()))", + "info[0].IsTypedArray() || info[0].IsArray() || (info[0].IsObject() && info[0].As().InstanceOf(PackedInt32ArrayBinding::constructor.Value()))", napi_match_expr("PackedInt32Array", 0), ) self.assertEqual("info[1].IsArray() || info[1].IsObject()", napi_match_expr("typedarray::Node", 1)) diff --git a/test/test_repository_integrity.py b/test/test_repository_integrity.py index 99d3843..c31fb6a 100644 --- a/test/test_repository_integrity.py +++ b/test/test_repository_integrity.py @@ -2589,7 +2589,7 @@ def test_js_arrays_are_first_class_array_arguments(self): self.assertIn("new PackedInt32Array([1, 2, 3])", runtime_test) self.assertIn("load_threaded_get_status", runtime_test) self.assertIn("threadedProgress[0]", runtime_test) - self.assertIn("call_class_method_bind(", resource_loader_source) + self.assertIn("call_class_method_bind<", resource_loader_source) self.assertIn('"load_threaded_get_status"', resource_loader_source) self.assertNotIn("call_builtin_method(&godot::ResourceLoader::load_threaded_get_status", resource_loader_source) self.assertIn("info[0].IsArray() || (info[0].IsObject() && info[0].As().InstanceOf(PackedInt32ArrayBinding::constructor.Value()))", packed_source) @@ -2645,7 +2645,7 @@ def class_body(dts_name: str) -> str: function_body = function_match.group("body") expected_indices = ", ".join(str(index) for index in out_indices) for token in ( - "call_class_method_bind(", + "call_class_method_bind<", f'"{class_name}"', f'"{method_name}"', str(method["hash"]), @@ -3205,21 +3205,51 @@ def test_generated_class_object_wrappers_use_external_factory(self): def test_generated_refcounted_wrappers_hold_external_references(self): api = load_extension_api() missing = [] + refcounted_classes = [cls for cls in api.get("classes", []) if cls.get("is_refcounted")] + self.assertTrue(refcounted_classes, "Extension API must contain RefCounted classes") - for cls in api.get("classes", []): - if not cls.get("is_refcounted"): - continue + for cls in refcounted_classes: class_name = cls["name"] source_path = ROOT / "src/generated/classes" / f"{to_snake_case(class_name)}_binding.gen.cpp" source = source_path.read_text(encoding="utf-8") - if "if (!owns_instance) {" not in source: - missing.append(f"{class_name}: missing non-owned reference guard") - if "ref->reference();" not in source: - missing.append(f"{class_name}: missing RefCounted reference") - if "referenced_instance = true;" not in source: - missing.append(f"{class_name}: missing referenced_instance flag") - - self.assertEqual([], missing) + # Check each lifetime operation in its own function. Ignore comments and + # whitespace so formatting changes do not alter the ownership contract. + functions = { + "constructor": f"{class_name}Binding::{class_name}Binding(", + "destructor": f"{class_name}Binding::~{class_name}Binding(", + "wrap": f"void {class_name}Binding_wrap(", + } + bodies = {} + for operation, signature in functions.items(): + match = re.search( + rf"{re.escape(signature)}[^\n]*\{{(?P.*?)^\}}", + source, re.DOTALL | re.MULTILINE, + ) + if not match: + missing.append(f"{class_name}: missing {operation} function") + continue + body = re.sub(r"//[^\n]*|/\*.*?\*/", "", match.group("body"), flags=re.DOTALL) + bodies[operation] = re.sub(r"\s+", "", body) + + constructor = bodies.get("constructor", "") + if "if(ref){referenced_instance=owns_instance?ref->init_ref():ref->reference();}" not in constructor: + missing.append(f"{class_name}: constructor must record initial or external reference acquisition") + destructor = bodies.get("destructor", "") + if "if(ref&&referenced_instance){if(ref->unreference()){godot::memdelete(ref);}}" not in destructor: + missing.append(f"{class_name}: destructor must release only an acquired reference and delete at zero") + if destructor.count("ref->unreference()") != 1: + missing.append(f"{class_name}: destructor must unreference exactly once") + wrap = bodies.get("wrap", "") + if "binding->referenced_instance=false;" not in wrap or "if(ref){binding->referenced_instance=ref->reference();}" not in wrap: + missing.append(f"{class_name}: wrap must record whether reference acquisition succeeded") + if any("referenced_instance=true;" in body for body in bodies.values()): + missing.append(f"{class_name}: reference flag must not be set unconditionally") + header_path = ROOT / "include/generated/classes" / f"{to_snake_case(class_name)}_binding.gen.h" + header = re.sub(r"\s+", "", header_path.read_text(encoding="utf-8")) + if "boolreferenced_instance=false;" not in header: + missing.append(f"{class_name}: reference flag must default to false") + + self.assertFalse(missing, f"{len(missing)} RefCounted lifetime violations:\n" + "\n".join(missing[:20])) def test_generated_default_args_do_not_emit_raw_godot_string_markers(self): offenders = []