From 9e6d469f00bd0d370aa0f22abd400f5cb58dd222 Mon Sep 17 00:00:00 2001 From: "YITSUSE, Masami" Date: Mon, 22 Jun 2026 15:09:11 +0900 Subject: [PATCH] feat(player): guard instance nesting depth and emit audio events Add a cyclic/runaway-nesting guard for Instance parts: children now carry parent depth + 1 and `_setup_instance_children` stops descending past MAX_INSTANCE_DEPTH (16) with a warning, instead of recursing until the stack overflows on a cyclic (A -> B -> A) .ssab. Mirrors the Unity player's MaxInstanceDepth. Implement audio event delivery: fill the `// TODO: Audio integration` path to emit a new `audio` signal (Dictionary payload: part_index, sound_list_name_hash, sound_name_hash, sound_name, loop_num), following the existing user_data/signal_emitted pattern. Playback is left to the game side. Documents the signal in docs/{ja,en}/api/player.md. --- docs/en/api/player.md | 13 +++++++++++++ docs/ja/api/player.md | 13 +++++++++++++ ss_player/ss_internal_player.cpp | 33 +++++++++++++++++++++++++++----- ss_player/ss_internal_player.h | 12 ++++++++++++ ss_player/ss_player_node_2d.cpp | 9 +++++++++ 5 files changed, 75 insertions(+), 5 deletions(-) diff --git a/docs/en/api/player.md b/docs/en/api/player.md index bda25f5..968b0ce 100644 --- a/docs/en/api/player.md +++ b/docs/en/api/player.md @@ -63,6 +63,7 @@ func _ready() -> void: | `animation_looped` | `anim_name: String` | The animation loops back to the start | | `user_data` | `payload: Dictionary` | A "User Data" keyframe on the timeline is hit | | `signal_emitted` | `command: String, value: Dictionary` | A "Signal" keyframe on the timeline is hit | +| `audio` | `payload: Dictionary` | An "Audio" keyframe on the timeline is hit | ### `user_data` payload fields @@ -79,6 +80,18 @@ The User Data values configured in SpriteStudio are delivered as a `Dictionary`. The parameters configured on the timeline "Signal" keyframe are delivered as a `Dictionary` keyed by parameter ID, with each value as `bool` / `int` / `float` / `String`, etc. The `command` argument receives the signal name (`command_id`). +### `audio` payload fields + +The information configured on the timeline audio keyframe is delivered as a `Dictionary`. The player does not play sound itself, so bridge it to an `AudioStreamPlayer` (or similar) on the game side. + +| Key | Type | Meaning | +| --- | --- | --- | +| `part_index` | `int` | Index of the part that fired | +| `sound_list_name_hash` | `int` | Hash of the sound list name | +| `sound_name_hash` | `int` | Hash of the sound name | +| `sound_name` | `String` | Sound name (present only when set) | +| `loop_num` | `int` | Loop count | + > [!NOTE] > For the exact types and the latest set of accepted values, also refer to the implementation files `ss_player/ss_player_node_2d.h` and `ss_player/ss_internal_player.cpp`. diff --git a/docs/ja/api/player.md b/docs/ja/api/player.md index 77df95d..46c7c41 100644 --- a/docs/ja/api/player.md +++ b/docs/ja/api/player.md @@ -63,6 +63,7 @@ func _ready() -> void: | `animation_looped` | `anim_name: String` | ループして先頭に戻った時 | | `user_data` | `payload: Dictionary` | タイムライン上の「ユーザーデータ」キーに到達した時 | | `signal_emitted` | `command: String, value: Dictionary` | タイムライン上の「シグナル」キーに到達した時 | +| `audio` | `payload: Dictionary` | タイムライン上の「オーディオ」キーに到達した時 | ### `user_data` の `payload` フィールド @@ -79,6 +80,18 @@ SpriteStudio 上でユーザーデータに設定した値が `Dictionary` と タイムライン上の「シグナル」に設定したパラメータが `Dictionary` として渡されます。パラメータ ID をキーに、各値(`bool` / `int` / `float` / `String` 等)が格納されます。`command` 引数にはシグナル名(`command_id`)が入ります。 +### `audio` の `payload` フィールド + +タイムライン上のオーディオキーに設定された情報が `Dictionary` として渡されます。再生はプレーヤ側では行わないため、ゲーム側で `AudioStreamPlayer` 等に橋渡ししてください。 + +| キー | 型 | 内容 | +| --- | --- | --- | +| `part_index` | `int` | 発火したパーツのインデックス | +| `sound_list_name_hash` | `int` | サウンドリスト名のハッシュ | +| `sound_name_hash` | `int` | サウンド名のハッシュ | +| `sound_name` | `String` | サウンド名(設定時のみ) | +| `loop_num` | `int` | ループ回数 | + > [!NOTE] > 引数の正確な型・最新の取り得る値は実装 `ss_player/ss_player_node_2d.h` / `ss_player/ss_internal_player.cpp` を併せて参照してください。 diff --git a/ss_player/ss_internal_player.cpp b/ss_player/ss_internal_player.cpp index 4dc57bc..d84c38c 100644 --- a/ss_player/ss_internal_player.cpp +++ b/ss_player/ss_internal_player.cpp @@ -709,7 +709,20 @@ void SsInternalPlayer::update(float delta_seconds) { } if (auto audios = events_per_frame->audios()) { - // TODO: Audio integration + for (uint32_t j = 0; j < audios->size(); j++) { + auto audio_event = audios->Get(j); + if (!audio_event || !audio_event->value()) continue; + auto val = audio_event->value(); + + Dictionary payload; + payload["part_index"] = audio_event->part_index(); + payload["sound_list_name_hash"] = (int64_t)val->sound_list_name_hash(); + payload["sound_name_hash"] = (int64_t)val->sound_name_hash(); + if (val->sound_name()) payload["sound_name"] = String::utf8(val->sound_name()->c_str()); + payload["loop_num"] = val->loop_num(); + + if (_event_sink) _event_sink->onAudio(payload); + } } } } @@ -1342,6 +1355,15 @@ void SsInternalPlayer::_setup_instance_children() { auto pt = p->part_type_as_PartTypeInstance(); if (!pt) continue; + // Cyclic / runaway-nesting guard: instance children live one level + // deeper than this player. Refuse to descend past MAX_INSTANCE_DEPTH so + // a cyclic reference (A → B → A authoring mistake) can't recurse until + // the stack overflows on load. + if (_instance_depth + 1 >= MAX_INSTANCE_DEPTH) { + WARN_PRINT(vformat("[SS] instance part %d: nesting exceeded %d levels; skipping to guard against cyclic references", i, MAX_INSTANCE_DEPTH)); + continue; + } + String pack_name = pt->ref_anime_pack() ? String::utf8(pt->ref_anime_pack()->c_str()) : String(); String anim_name = pt->ref_anime_name() ? String::utf8(pt->ref_anime_name()->c_str()) : String(); uint32_t pack_name_hash = pt->ref_anime_pack_hash(); @@ -1356,6 +1378,7 @@ void SsInternalPlayer::_setup_instance_children() { SsInternalPlayer* child = memnew(SsInternalPlayer); child->setParentDriven(true); + child->setInstanceDepth(_instance_depth + 1); child->setSubFrameEnabled(_sub_frame_enabled); // Hand the child the SSAB that actually contains the referenced // animation — may be `_ssabRes` itself or an external sibling. @@ -2585,10 +2608,10 @@ void SsInternalPlayer::_fetchAnimation() { } // Recursive setup — instance children also populate their own - // `_instance_children`, so any depth of nested Instance parts is - // supported. Cross-SSAB cycles (A → B → A authoring mistakes) are not - // detected; they recurse until stack overflow on load. Trust the - // converter / authoring tool to keep references acyclic. + // `_instance_children`, so nested Instance parts are supported. Cross-SSAB + // cycles (A → B → A authoring mistakes) are bounded by MAX_INSTANCE_DEPTH: + // `_setup_instance_children` stops spawning children past that depth (with + // a warning) instead of recursing until the stack overflows on load. _setup_instance_children(); _setup_effect_slots(); diff --git a/ss_player/ss_internal_player.h b/ss_player/ss_internal_player.h index 737f516..de72fff 100644 --- a/ss_player/ss_internal_player.h +++ b/ss_player/ss_internal_player.h @@ -73,6 +73,7 @@ class SsPlayerEventSink { virtual void onAnimationLooped(const String& anim_name) {} virtual void onUserData(const Dictionary& payload) {} virtual void onSignal(const String& command, const Dictionary& value) {} + virtual void onAudio(const Dictionary& payload) {} }; // Engine-integration-agnostic SpriteStudio player core. Owns the @@ -160,6 +161,14 @@ class SsInternalPlayer { void setParentDriven(bool p_enabled); bool isParentDriven() const { return _parent_driven; } + // Nesting depth within an Instance-part chain (0 = the root player the host + // owns). `_setup_instance_children` sets each spawned child to parent depth + // + 1 and refuses to descend past `MAX_INSTANCE_DEPTH`, guarding against + // cyclic Instance references (A → B → A authoring mistakes) that would + // otherwise recurse until the stack overflows on load. + void setInstanceDepth(int p_depth) { _instance_depth = p_depth; } + int getInstanceDepth() const { return _instance_depth; } + // Per-tick update (in seconds; the host typically passes // `process_delta_time`). No-op when paused or in instance-child mode. void update(float delta_seconds); @@ -319,6 +328,9 @@ class SsInternalPlayer { float _speed_rate = 1.0f; bool _sub_frame_enabled = false; bool _parent_driven = false; + int _instance_depth = 0; + // Hard cap on Instance-part nesting depth; see `setInstanceDepth`. + static constexpr int MAX_INSTANCE_DEPTH = 16; SsPlayerEventSink* _event_sink = nullptr; diff --git a/ss_player/ss_player_node_2d.cpp b/ss_player/ss_player_node_2d.cpp index 5b05e72..4af4c43 100644 --- a/ss_player/ss_player_node_2d.cpp +++ b/ss_player/ss_player_node_2d.cpp @@ -28,6 +28,9 @@ class SpriteStudioPlayer2D::_SignalSink : public SsPlayerEventSink { void onSignal(const String& command, const Dictionary& value) override { _owner->emit_signal(SNAME("signal_emitted"), command, value); } + void onAudio(const Dictionary& payload) override { + _owner->emit_signal(SNAME("audio"), payload); + } private: SpriteStudioPlayer2D* _owner; @@ -350,6 +353,12 @@ void SpriteStudioPlayer2D::_bind_methods() { PropertyInfo(Variant::DICTIONARY, "value") ) ); + ADD_SIGNAL( + MethodInfo( + "audio", + PropertyInfo(Variant::DICTIONARY, "payload") + ) + ); ADD_SIGNAL(MethodInfo("animation_changed", PropertyInfo(Variant::STRING, "anim_name"))); ADD_SIGNAL(MethodInfo("animation_started", PropertyInfo(Variant::STRING, "anim_name")));