diff --git a/docs/en/api/player.md b/docs/en/api/player.md index bca4790..1209a8b 100644 --- a/docs/en/api/player.md +++ b/docs/en/api/player.md @@ -57,7 +57,7 @@ func _ready() -> void: * `get_part_names() -> PackedStringArray`: Every part name in the current animation. * `get_part_index(part_name: String) -> int`: Resolves a part name to its part index, or `-1` if it does not exist. -* `get_part_transform(part_name: String) -> Transform2D`: The part's local `Transform2D` on the current frame. Returns the identity when the part is unknown. +* `get_part_transform(part_name: String) -> Transform2D`: The part's `Transform2D` on the current frame, in the player node's local space (`flip_h` / `flip_v` / `offset` included). Returns the identity when the part is unknown. * `is_part_hidden(part_name: String) -> bool`: Whether the part is hidden on the current frame. ## Part overrides diff --git a/docs/ja/api/player.md b/docs/ja/api/player.md index 2f1d6c8..918db77 100644 --- a/docs/ja/api/player.md +++ b/docs/ja/api/player.md @@ -57,7 +57,7 @@ func _ready() -> void: * `get_part_names() -> PackedStringArray`: 現在のアニメーションに含まれる全パーツ名。 * `get_part_index(part_name: String) -> int`: パーツ名をパーツインデックスへ解決します。存在しない場合は `-1`。 -* `get_part_transform(part_name: String) -> Transform2D`: 現在のフレームでのパーツのローカル `Transform2D`。パーツが不明な場合は単位行列を返します。 +* `get_part_transform(part_name: String) -> Transform2D`: 現在のフレームでのパーツの `Transform2D`(プレイヤーノードのローカル空間。`flip_h` / `flip_v` / `offset` を含みます)。パーツが不明な場合は単位行列を返します。 * `is_part_hidden(part_name: String) -> bool`: 現在のフレームでそのパーツが非表示かどうか。 ## パーツオーバーライド diff --git a/ss_player/shaders/ss_mask_write.gdshader b/ss_player/shaders/ss_mask_write.gdshader index 32297a9..ef7bcfb 100644 --- a/ss_player/shaders/ss_mask_write.gdshader +++ b/ss_player/shaders/ss_mask_write.gdshader @@ -7,6 +7,15 @@ R"GLSL( // RGBA8 channel (e.g. R for bits 0..7, value (1<<(bit%8))/255). Additive // blending then ORs the disjoint bits together across writers. // +// LIMITATION: add is only equivalent to OR while no texel receives the same bit +// twice. That holds across writers (their bits are disjoint) and for the quad / +// pentagon and triangulated-shape geometry we emit, but a single writer whose +// own triangles overlap (e.g. a bone-deformed mesh folded onto itself) adds its +// bit twice into the shared texels, which carries into the neighbouring bit — +// those texels then read as a different writer's mask. Godot's canvas render +// modes offer no saturating (max) blend, so fixing this properly needs a +// stencil-style write rather than a shader change. +// // Cutout: a texel contributes to the mask only where the writer sprite's alpha // is strictly greater than `mask_threshold`. The player derives the threshold // from the part's `mask` attribute (PartState.mask()); mask == 0 maps to diff --git a/ss_player/ss_import_dock.cpp b/ss_player/ss_import_dock.cpp index dc2c3eb..af12868 100644 --- a/ss_player/ss_import_dock.cpp +++ b/ss_player/ss_import_dock.cpp @@ -290,22 +290,25 @@ void SSImportControl::start_intercepting() { void SSImportControl::stop_intercepting() { if (!is_intercepting) return; - auto window = get_window(); - if (!window) return; + // Drop the intercept state up-front: every exit path below must leave this + // control un-intercepting, or a later start_intercepting() early-returns and + // the editor's own files_dropped handlers stay disconnected for the session. + is_intercepting = false; - if (window->is_connected("files_dropped", Callable(this, "_on_window_files_dropped"))) { - window->disconnect("files_dropped", Callable(this, "_on_window_files_dropped")); - } + auto window = get_window(); + if (window) { + if (window->is_connected("files_dropped", Callable(this, "_on_window_files_dropped"))) { + window->disconnect("files_dropped", Callable(this, "_on_window_files_dropped")); + } - for (int i = 0; i < original_drop_handlers.size(); i++) { - const Callable &handler = original_drop_handlers[i]; - if (handler.is_valid() && !window->is_connected("files_dropped", handler)) { - window->connect("files_dropped", handler); + for (int i = 0; i < original_drop_handlers.size(); i++) { + const Callable &handler = original_drop_handlers[i]; + if (handler.is_valid() && !window->is_connected("files_dropped", handler)) { + window->connect("files_dropped", handler); + } } } original_drop_handlers.clear(); - - is_intercepting = false; } #ifdef SPRITESTUDIO_GODOT_EXTENSION @@ -359,6 +362,10 @@ void SSImportControl::_on_window_files_dropped(const Vector &p_files) { if (!dirs.is_empty()) { // A folder was dropped: scan it (and any loose .sspj) and import all. + if (!importer) { + ERR_PRINT("SSImportControl: importer is not set."); + return; + } importer->queue_scan_and_import(dirs, sspj_files, path_line_edit->get_text()); } else { _start_import(sspj_files); @@ -701,11 +708,16 @@ void SSImportControl::_ensure_output_dir_exists() { auto *efs = EditorInterface::get_singleton()->get_resource_file_system(); #endif if (!efs) return; -#ifdef SPRITESTUDIO_GODOT_EXTENSION - efs->scan_sources(); -#else - efs->scan_changes(); -#endif + // Full scan: scan_sources()/scan_changes() are mtime-driven and do not + // reliably notice a brand-new directory (parent-mtime granularity on + // Windows), so the folder would not show up in the FileSystem dock until an + // unrelated rescan. The importer's own sync phase uses a full scan for the + // same reason (see SSImporter::_poll_fs_sync). A scan issued while one is + // already running is dropped, so skip it then — the in-flight scan (and the + // importer's own, on the next import) covers the new folder. + if (!efs->is_scanning()) { + efs->scan(); + } } #endif // #ifdef TOOLS_ENABLED diff --git a/ss_player/ss_internal_player.cpp b/ss_player/ss_internal_player.cpp index 2fae113..c4887bf 100644 --- a/ss_player/ss_internal_player.cpp +++ b/ss_player/ss_internal_player.cpp @@ -721,11 +721,18 @@ void SsInternalPlayer::update(float delta_seconds) { // — a timing-dependent loss seen in the editor preview but not a running // project. Events must fire whenever the runtime reports them, regardless of // whether we also redraw. - if (_currentAnimationData && _currentAnimationData->events() != nullptr) { + const auto* events = _currentAnimationData ? _currentAnimationData->events() : nullptr; + if (events != nullptr) { + const uint32_t events_size = events->size(); int event_count = ss_runtime_get_passed_event_count(runtime_ctx); for (int i = 0; i < event_count; i++) { int event_idx = ss_runtime_get_passed_event_index(runtime_ctx, i); - auto events_per_frame = _currentAnimationData->events()->Get(event_idx); + // The index comes from the runtime and is only meaningful against the + // animation currently bound there; a not-found (-1) or an index left + // over from a different animation would read past the FlatBuffer. + if (event_idx < 0 || (uint32_t)event_idx >= events_size) continue; + auto events_per_frame = events->Get((uint32_t)event_idx); + if (!events_per_frame) continue; if (auto users = events_per_frame->users()) { for (uint32_t j = 0; j < users->size(); j++) { @@ -819,12 +826,22 @@ void SsInternalPlayer::update(float delta_seconds) { bool SsInternalPlayer::_build_mask_writers(const DrawFrame& f) { _mask_writers.clear(); + // Clear before any early return: a part that stopped masking this frame must + // stop being suppressed by the emit paths. + if (!_part_pure_mask.is_empty()) { + memset(_part_pure_mask.ptr(), 0, _part_pure_mask.size()); + } if (!f.frameData || !f.binary) return false; auto parts_meta = f.binary->parts(); auto draw_order = f.frameData->draw_order(); if (!parts_meta || !draw_order) return false; const int total_meta = (int)parts_meta->size(); + if ((int)_part_pure_mask.size() != total_meta) { + _part_pure_mask.resize(total_meta); + if (total_meta > 0) memset(_part_pure_mask.ptr(), 0, total_meta); + } + uint8_t* pure_mask_flags = _part_pure_mask.ptr(); const uint32_t n = draw_order->size(); for (uint32_t rank = 0; rank < n; rank++) { const int p_idx = (int)draw_order->Get(rank); @@ -842,7 +859,14 @@ bool SsInternalPlayer::_build_mask_writers(const DrawFrame& f) { const bool writes = pure_mask || pd->mask_write(); if (!writes) continue; - if ((int)_mask_writers.size() >= MAX_MASK_WRITERS) break; // bitmap holds 32 + // Flag it even when the writer list is already full: a pure mask that does + // not fit the bitmap still masks nothing, so it must not fall back to + // drawing its own colour over the scene. + if (pure_mask) pure_mask_flags[p_idx] = 1; + + // Bitmap holds 24 writers; keep scanning past that so the pure masks that + // did not fit are still flagged above. + if ((int)_mask_writers.size() >= MAX_MASK_WRITERS) continue; MaskWriter w; w.part_index = p_idx; @@ -1110,7 +1134,14 @@ void SsInternalPlayer::_render_mask_coverage(const DrawFrame& f) { uv.columns[0] = Vector2(1.0f / bsize.x, 0); uv.columns[1] = Vector2(0, 1.0f / bsize.y); uv.columns[2] = Vector2(-bmin.x / bsize.x, -bmin.y / bsize.y); - _mask_local_to_uv = uv; + // What the shaders sample this frame is the coverage rendered LAST frame (the + // UPDATE_ONCE latency below), rasterized with last frame's bbox -> viewport + // transform. So publish last frame's UV transform and hold this one back; + // publishing `uv` now would offset/scale the lookup by the per-frame bbox + // delta. The same holds on a size-class transition, where `_mask_prev` is + // sampled: it too was rendered with the previous frame's bbox. + _mask_local_to_uv = _mask_local_to_uv_pending; + _mask_local_to_uv_pending = uv; // Frame mask state consumed by the maskable emit path (P3). _mask_coverage_tex = rs->viewport_get_texture(_mask_target->viewport); @@ -1167,10 +1198,8 @@ bool SsInternalPlayer::_part_in_mask_scope(uint16_t rank) const { } bool SsInternalPlayer::_is_pure_mask_part(int p_idx) const { - for (int i = 0; i < _mask_writers.size(); i++) { - if (_mask_writers[i].part_index == p_idx) return !_mask_writers[i].is_clipping; - } - return false; + if (p_idx < 0 || (uint32_t)p_idx >= _part_pure_mask.size()) return false; + return _part_pure_mask[p_idx] != 0; } void SsInternalPlayer::_set_mask_uv_uniform(Ref mat, const Transform2D& local_to_uv) { @@ -1346,16 +1375,16 @@ void SsInternalPlayer::_drawAnimation(float frame_no, float delta_seconds, bool // Pure masks feed only the coverage bitmap and must not draw their own // colour (PartTypeMask has no draw branch below; this also suppresses // shape/text/nines *_mask color draws so they read as holes, not fills). - if (_mask_coverage_valid && batch->count() > 0 - && _is_pure_mask_part((int)draw_order_data[batch->start_rank()])) { - continue; - } + // Normal / Shape / Mesh batches group several parts by texture + blend + // and can mix mask and non-mask parts, so those test per part inside + // their emit loops; the single-part kinds are handled here. if (kind == ss::runtime::DrawBatchKind_Normal) { _emit_normal_batch(f, ci, batch, draw_order_data); } else if (kind == ss::runtime::DrawBatchKind_Shape) { _emit_shape_batch(f, ci, batch, draw_order_data); } else if (kind == ss::runtime::DrawBatchKind_Instance) { int p_idx = (int)draw_order_data[batch->start_rank()]; + if (_is_pure_mask_part(p_idx)) continue; const auto* part = (p_idx >= 0 && p_idx < (int)_parts_by_idx.size()) ? _parts_by_idx[p_idx] : nullptr; if (!part) continue; const float* drawing_m = f.get_world_matrix(p_idx); @@ -1363,6 +1392,7 @@ void SsInternalPlayer::_drawAnimation(float frame_no, float delta_seconds, bool _emit_instance_slot(f, ci, p_idx, drawing_m, batch->start_rank()); } else if (kind == ss::runtime::DrawBatchKind_Effect) { int p_idx = (int)draw_order_data[batch->start_rank()]; + if (_is_pure_mask_part(p_idx)) continue; const auto* part = (p_idx >= 0 && p_idx < (int)_parts_by_idx.size()) ? _parts_by_idx[p_idx] : nullptr; if (!part) continue; const float* drawing_m = f.get_world_matrix(p_idx); @@ -1675,7 +1705,10 @@ void SsInternalPlayer::_emit_effect_slot(const DrawFrame& f, RID ci, int p_idx, // The returned EffectDrawPlan carries everything Godot needs to draw: // commands keyed by cellmap_hash + blend, plus flat verts/uvs/colors/indices. const ss_effect_event_info ev = ss_runtime_get_active_effect_event(runtime_ctx, (uint32_t)p_idx); - const float fps = _currentAnimationData->fps() > 0 ? (float)_currentAnimationData->fps() : 60.0f; + // _currentAnimationData is null whenever _fetchAnimation bailed out; the + // effect simulator still needs a sane frame rate to step with. + const float fps = (_currentAnimationData && _currentAnimationData->fps() > 0) + ? (float)_currentAnimationData->fps() : 60.0f; const ss_effect_step_result step = ss_effect_slot_step( slot.effect_slot, ev, f.frame_no, f.delta_seconds, fps, f.parent_looped, /*y_flip*/ true, /*vert_stride*/ 2); @@ -2259,6 +2292,11 @@ void SsInternalPlayer::_emit_normal_batch(const DrawFrame& f, RID ci, SsFloatArray& custom0 = _normal_custom0; SsIntArray& indices = _normal_indices; + // The accumulators are peak-retained (grown above, never shrunk), so their + // size is the largest batch seen so far, not this run's. Remember the + // capacity to restore after each run trims itself down for the emit. + const int verts_cap = verts.size(); // uvs / colors track this exactly + Vector2* verts_ptr = verts.ptrw(); Vector2* uvs_ptr = uvs.ptrw(); Color* colors_ptr = colors.ptrw(); @@ -2316,7 +2354,16 @@ void SsInternalPlayer::_emit_normal_batch(const DrawFrame& f, RID ci, } else { run_ci = _acquire_per_part_canvas_item(); // fresh CI, draw_index = _draw_seq++ } + // Trim every stream to what this run actually wrote. mesh_add_surface_from_arrays + // uploads whatever it is handed and derives the surface AABB from + // ARRAY_VERTEX, so leaving the peak-retained tail in place would upload + // the previous (larger) batch's vertex count on every small run and size + // the culling rect against geometry that is not drawn. indices.resize(ibase); + verts.resize(vbase); + uvs.resize(vbase); + colors.resize(vbase); + custom0.resize(vbase * 4); _apply_partcolor_material(rs, run_ci, s_default_shader_id_hash, ssab_blend); rs->canvas_item_set_transform(run_ci, Transform2D()); _emit_partcolor_mesh(rs, run_ci, indices, verts, colors, uvs, custom0, tex_rid); @@ -2324,6 +2371,10 @@ void SsInternalPlayer::_emit_normal_batch(const DrawFrame& f, RID ci, // re-fetch the write pointers (the arrays were shared with the emitted // mesh during the call above). indices.resize(index_count); + verts.resize(verts_cap); + uvs.resize(verts_cap); + colors.resize(verts_cap); + custom0.resize(verts_cap * 4); verts_ptr = verts.ptrw(); uvs_ptr = uvs.ptrw(); colors_ptr = colors.ptrw(); @@ -2336,6 +2387,9 @@ void SsInternalPlayer::_emit_normal_batch(const DrawFrame& f, RID ci, for (uint16_t k = 0; k < count; k++) { int p_idx = (int)draw_order_data[batch->start_rank() + k]; if (p_idx < 0 || p_idx >= (int)_parts_by_idx.size()) continue; + // Pure masks feed the coverage bitmap only; drawing their colour would + // paint the mask art over the scene instead of cutting a hole in it. + if (_is_pure_mask_part(p_idx)) continue; const auto* part = _parts_by_idx[p_idx]; if (!part) continue; @@ -2492,6 +2546,8 @@ void SsInternalPlayer::_emit_shape_batch(const DrawFrame& f, RID ci, for (uint16_t k = 0; k < count; k++) { int p_idx = (int)draw_order_data[batch->start_rank() + k]; if (p_idx < 0 || p_idx >= (int)_parts_by_idx.size()) continue; + // A shape flagged as a mask is coverage-only (see _emit_normal_batch). + if (_is_pure_mask_part(p_idx)) continue; const auto* part = _parts_by_idx[p_idx]; if (!part) continue; @@ -2600,6 +2656,8 @@ void SsInternalPlayer::_emit_mesh_batch(const DrawFrame& f, RID ci, for (uint16_t k = 0; k < count; k++) { int p_idx = (int)draw_order_data[batch->start_rank() + k]; if (p_idx < 0 || p_idx >= (int)_parts_by_idx.size()) continue; + // A mesh flagged as a mask is coverage-only (see _emit_normal_batch). + if (_is_pure_mask_part(p_idx)) continue; const auto* part = _parts_by_idx[p_idx]; if (!part) continue; @@ -2760,6 +2818,12 @@ void SsInternalPlayer::_fetchAnimation() { _free_per_part_canvas_items(); _free_mask_targets(); + // Drop the previous animation up-front: it points into the OLD resource's + // FlatBuffer, which setSSABResource may already have freed by releasing the + // last Ref. Every failure path below returns without re-assigning it, and + // update() / _emit_effect_slot dereference it on the next tick. + _currentAnimationData = nullptr; + if (runtime_res != nullptr) { ss_resource_destroy(runtime_res); runtime_res = nullptr; @@ -2806,6 +2870,9 @@ void SsInternalPlayer::_fetchAnimation() { bool setup = ss_runtime_setup_animation_by_hash(runtime_ctx, _animationSelectedHash); if (!setup) { ERR_PRINT("SSAB Setup Animation Failed by hash: " + String::num_int64(_animationSelectedHash)); + // The runtime has no animation bound, so nothing may consume this frame's + // animation data either. + _currentAnimationData = nullptr; return; } diff --git a/ss_player/ss_internal_player.h b/ss_player/ss_internal_player.h index f936bb0..f576dd8 100644 --- a/ss_player/ss_internal_player.h +++ b/ss_player/ss_internal_player.h @@ -518,6 +518,10 @@ class SsInternalPlayer { // premultiplied-blend coverage accumulator), so cap at 24 writers / frame. static constexpr int MAX_MASK_WRITERS = 24; Vector _mask_writers; + // Per part index (parallel to `_parts_by_idx`): 1 when the part is a "pure" + // mask this frame. Rebuilt with `_mask_writers` so the emit paths can test it + // once per part without walking the writer list. + LocalVector _part_pure_mask; // Populate `_mask_writers` from this frame's draw_order + static PartData. // Returns true if at least one writer is present (i.e., masking is active // this frame). Only the top-root player owns the mask state. @@ -547,6 +551,12 @@ class SsInternalPlayer { // matrices already applied) -> coverage UV [0,1]. Set per frame by the // coverage pass and read by maskable shaders. Identity until masking runs. Transform2D _mask_local_to_uv; + // The UV transform derived from THIS frame's writer bbox. Shaders sample the + // coverage rendered on the PREVIOUS frame (UPDATE_ONCE latency), so this is + // held back one frame and only then promoted to `_mask_local_to_uv` — using + // it immediately would map positions through a bbox the sampled texels were + // never rasterized with, sliding the mask off its target as the bbox moves. + Transform2D _mask_local_to_uv_pending; bool _mask_coverage_valid = false; // true if this frame drew coverage // Coverage-bitmap dimension bounds in pixels. The per-axis size is the // mask's on-screen footprint, quantized up to a power-of-two size class in @@ -593,7 +603,10 @@ class SsInternalPlayer { bool _part_in_mask_scope(uint16_t rank) const; // True if the part is a "pure" mask (PartTypeMask or a shape/text/nines // *_mask): it feeds the coverage bitmap but must not draw its own colour. - // write_mask (clipping) writers are NOT pure — they draw AND mask. + // write_mask (clipping) writers are NOT pure — they draw AND mask. Valid for + // any player with writers this frame, coverage rendered or not (an instance + // child's mask parts must stay invisible even though the parent owns the + // coverage pass), so callers must NOT gate it on `_mask_coverage_valid`. bool _is_pure_mask_part(int p_idx) const; void _apply_mask_uniforms(Ref mat, uint16_t rank, bool visible_inside); void _set_mask_uv_uniform(Ref mat, const Transform2D& local_to_uv); diff --git a/ss_player/ss_player_node_2d.cpp b/ss_player/ss_player_node_2d.cpp index fa6ea0d..4442f8b 100644 --- a/ss_player/ss_player_node_2d.cpp +++ b/ss_player/ss_player_node_2d.cpp @@ -136,8 +136,12 @@ int SpriteStudioPlayer2D::get_part_index(const String& part_name) const { Transform2D SpriteStudioPlayer2D::get_part_transform(const String& part_name) const { Transform2D xf; int idx = _internal->resolve_part_index(part_name); - if (idx >= 0) { - _internal->try_get_part_local_transform(idx, xf); + if (idx >= 0 && _internal->try_get_part_local_transform(idx, xf)) { + // The runtime's world matrix is relative to the internal root canvas + // item, which carries flip / offset. Compose it here so the result is + // the part's transform in THIS node's local space — otherwise an + // attachment pinned to a part detaches the moment the player is flipped. + xf = _make_root_transform() * xf; } return xf; } @@ -294,12 +298,16 @@ void SpriteStudioPlayer2D::set_offset(const Vector2& p_offset) { Vector2 SpriteStudioPlayer2D::get_offset() const { return _offset; } -void SpriteStudioPlayer2D::_update_root_transform() { +Transform2D SpriteStudioPlayer2D::_make_root_transform() const { Transform2D xform; xform.columns[0].x = _flip_h ? -1.0 : 1.0; xform.columns[1].y = _flip_v ? -1.0 : 1.0; xform.columns[2] = _offset; - _internal->setRootTransform(xform); + return xform; +} + +void SpriteStudioPlayer2D::_update_root_transform() { + _internal->setRootTransform(_make_root_transform()); } void SpriteStudioPlayer2D::_push_coverage_screen_scale() { diff --git a/ss_player/ss_player_node_2d.h b/ss_player/ss_player_node_2d.h index 565edc6..9d969f1 100644 --- a/ss_player/ss_player_node_2d.h +++ b/ss_player/ss_player_node_2d.h @@ -164,6 +164,10 @@ class SpriteStudioPlayer2D : public Node2D { Vector2 _offset; AnimationProcessMode _process_mode = ANIMATION_PROCESS_IDLE; + // flip_h / flip_v / offset as one matrix. Applied to the internal root canvas + // item (so it is NOT part of the Node2D transform) and composed onto part + // transforms by get_part_transform, which must report where a part is drawn. + Transform2D _make_root_transform() const; void _update_root_transform(); void _push_coverage_screen_scale(); diff --git a/ss_player/ss_resource_inspector.cpp b/ss_player/ss_resource_inspector.cpp index 3d9c279..a5a6f33 100644 --- a/ss_player/ss_resource_inspector.cpp +++ b/ss_player/ss_resource_inspector.cpp @@ -187,8 +187,16 @@ void SSResourceInspectorPlugin::_on_generate_animation_library_pressed(const Str int track_frame = anim->add_track(Animation::TYPE_VALUE); anim->track_set_path(track_frame, NodePath(".:frame")); anim->track_insert_key(track_frame, 0.0, 0.0f); - anim->track_insert_key(track_frame, length, (float)total_frames); - + // The player exposes frames 0..total_frame-1, and the last frame occupies + // the final 1/fps slice of `length`. Keying the last VALID frame at its own + // start time keeps the interpolated rate at exactly one frame per 1/fps and + // holds it until the clip ends; keying `total_frames` at `length` instead + // would push one frame past the animation and get clamped by the runtime. + const int max_frame = total_frames - 1; + if (max_frame > 0) { + anim->track_insert_key(track_frame, (double)max_frame / fps, (float)max_frame); + } + anim->value_track_set_update_mode(track_frame, Animation::UPDATE_CONTINUOUS); anim->track_set_interpolation_type(track_frame, Animation::INTERPOLATION_LINEAR); @@ -203,7 +211,12 @@ void SSResourceInspectorPlugin::_on_generate_animation_library_pressed(const Str #endif if (err == OK) { - EditorInterface::get_singleton()->get_resource_filesystem()->scan(); +#if defined(SPRITESTUDIO_GODOT_EXTENSION) || (VERSION_MAJOR >= 4 && VERSION_MINOR >= 6) + auto *efs = EditorInterface::get_singleton()->get_resource_filesystem(); +#else + auto *efs = EditorInterface::get_singleton()->get_resource_file_system(); +#endif + if (efs) efs->scan(); #ifdef SPRITESTUDIO_GODOT_EXTENSION UtilityFunctions::print("Generated AnimationLibrary: " + out_path); #else diff --git a/ss_player/ssab_resource.cpp b/ss_player/ssab_resource.cpp index f2218f0..ffda816 100644 --- a/ss_player/ssab_resource.cpp +++ b/ss_player/ssab_resource.cpp @@ -21,6 +21,18 @@ void SSABResource::_bind_methods() { ClassDB::bind_method(D_METHOD("get_sound_info", "sound_list_name_hash", "sound_name_hash"), &SSABResource::get_sound_info); } bool SSABResource::is_valid() const { + // Verification walks the whole buffer, and nearly every accessor below gates + // on it (selecting a player in the editor alone triggers several, and each + // Instance child does one per animation lookup), so a multi-megabyte SSAB + // would be re-verified many times per operation. The buffer is immutable + // between load_from_file / copy_from, both of which reset the cache. + if (_valid_cache < 0) { + _valid_cache = _verify_binary() ? 1 : 0; + } + return _valid_cache != 0; +} + +bool SSABResource::_verify_binary() const { if (binary.size() == 0) { return false; } @@ -58,6 +70,8 @@ bool SSABResource::is_valid() const { Error SSABResource::load_from_file(const String &path) { Error error = OK; _parent_dir = path.get_base_dir(); + // New buffer: drop the cached verification verdict before touching `binary`. + _valid_cache = -1; #ifdef SPRITESTUDIO_GODOT_EXTENSION binary = FileAccess::get_file_as_bytes(path); if (binary.size() == 0) { @@ -72,6 +86,7 @@ Error SSABResource::load_from_file(const String &path) { if (!is_valid()) { binary.clear(); + _valid_cache = -1; return ERR_INVALID_DATA; } @@ -332,6 +347,7 @@ Error SSABResource::copy_from(const Ref &p_resource) { const Ref &ssabFile = static_cast &>(p_resource); this->binary = ssabFile->binary; + _valid_cache = -1; return OK; } #endif diff --git a/ss_player/ssab_resource.h b/ss_player/ssab_resource.h index 15e397e..56f0899 100644 --- a/ss_player/ssab_resource.h +++ b/ss_player/ssab_resource.h @@ -80,6 +80,12 @@ class SSABResource : public Resource { #endif private: String _parent_dir; + // Cached result of the whole-buffer FlatBuffers verification run by + // is_valid(): -1 = not verified yet, 0 = invalid, 1 = valid. Reset to -1 + // wherever `binary` is replaced (load_from_file / copy_from). + mutable int8_t _valid_cache = -1; + // The actual verification; is_valid() is the cached front-end. + bool _verify_binary() const; // (sound_list_name_hash << 32 | sound_name_hash) -> loaded AudioStream. // Populated lazily by get_sound_stream so repeated events share one stream. HashMap> _sound_cache;