Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 23 additions & 57 deletions CHANGELOG-ZH.md

Large diffs are not rendered by default.

80 changes: 23 additions & 57 deletions CHANGELOG.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ cmake_minimum_required(VERSION 3.22)
cmake_policy(SET CMP0091 NEW)
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>: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)
Expand Down
16 changes: 16 additions & 0 deletions README-ZH.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 互相调用

下面是一个完整的节点结构示例:
Expand Down Expand Up @@ -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";

Expand Down
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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";

Expand Down
2 changes: 1 addition & 1 deletion example/addons/gode/plugin.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -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"
124 changes: 123 additions & 1 deletion example/scripts/tests/runtime_integration_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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]);
Expand Down
4 changes: 3 additions & 1 deletion generator/builtin_classes_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -47,8 +48,9 @@ def napi_match_expr(type_name, index, allow_number_for_int=True):
f"({value}.IsObject() && {value}.As<Napi::Object>().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<Napi::Object>().InstanceOf({type_name}Binding::constructor.Value()))"
)
if type_name.startswith('typedarray::'):
Expand Down
5 changes: 4 additions & 1 deletion generator/class_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down
4 changes: 3 additions & 1 deletion generator/dts_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand Down Expand Up @@ -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
Expand Down
19 changes: 8 additions & 11 deletions generator/templates/class_binding.cpp.jinja2
Original file line number Diff line number Diff line change
Expand Up @@ -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<godot::RefCounted>(binding->instance);
if (ref) {
ref->reference();
binding->referenced_instance = true;
binding->referenced_instance = ref->reference();
}
}

Expand Down Expand Up @@ -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<godot::RefCounted>(instance);
if (ref) {
ref->reference();
referenced_instance = true;
}
godot::RefCounted *ref = dynamic_cast<godot::RefCounted *>(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<Napi::Object>());
Expand All @@ -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<godot::RefCounted>(obj);
if (ref && (owns_instance || referenced_instance)) {
if (ref && referenced_instance) {
if (ref->unreference()) {
godot::memdelete(ref);
}
Expand Down Expand Up @@ -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<Napi::Value>{}" }},
{ {{ method.out_argument_indices_cpp }} });
{% elif method.live_property_name and not method.is_static and not method.is_vararg %}
Expand Down
9 changes: 9 additions & 0 deletions generator/utils/type_mappings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
25 changes: 18 additions & 7 deletions include/runtime/func_utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -111,18 +111,29 @@ inline bool throw_if_godot_call_failed(Napi::Env env, const GDExtensionCallError
return true;
}

template <typename Param>
inline godot::Variant convert_method_bind_argument(Napi::Value value) {
using ClearType = std::remove_const_t<std::remove_reference_t<Param>>;
if constexpr (is_godot_packed_array_v<ClearType>) {
if (value.IsTypedArray()) {
return napi_to_godot<Param>(value);
}
}
return napi_to_godot(value);
}

template <typename... P>
inline Napi::Value call_class_method_bind(
godot::Object *instance,
const char *godot_class_name,
const char *method_name,
uint32_t method_hash,
bool has_return,
const Napi::CallbackInfo &info,
std::size_t target_count,
const std::vector<Napi::Value> &default_args,
std::initializer_list<std::size_t> out_arg_indices) {
std::vector<Napi::Value> 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();
}

Expand All @@ -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<P>(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]);
Expand Down
Loading
Loading