From b6d76309f3fb01cc03c4bfe79b665652dc4a0d15 Mon Sep 17 00:00:00 2001 From: Luke Ingram Date: Thu, 11 Dec 2025 23:43:40 -0400 Subject: [PATCH] [godot] Addresses #2899, #2980, #2985 Addresses the loader/saver registration issue in #2899. Additionally, makes it such that you can adjust the SpineSkeletonData in the inspector without crashes. This appears to have been caused by a dangling pointer. Finally, double-clicking on JSON in the inspector opens the text editor and does not crash the Godot editor. --- spine-godot/BUGFIX_SUMMARY_V2.md | 172 ++++ spine-godot/SpineSkeletonDataResource_old.cpp | 853 ++++++++++++++++++ spine-godot/SpineSkeletonDataResource_old.h | 220 +++++ .../spine_godot/SpineSkeletonDataResource.cpp | 13 +- .../spine_godot/SpineSkeletonDataResource.h | 8 + spine-godot/spine_godot/register_types.cpp | 10 +- 6 files changed, 1273 insertions(+), 3 deletions(-) create mode 100644 spine-godot/BUGFIX_SUMMARY_V2.md create mode 100644 spine-godot/SpineSkeletonDataResource_old.cpp create mode 100644 spine-godot/SpineSkeletonDataResource_old.h diff --git a/spine-godot/BUGFIX_SUMMARY_V2.md b/spine-godot/BUGFIX_SUMMARY_V2.md new file mode 100644 index 000000000..acc841cf4 --- /dev/null +++ b/spine-godot/BUGFIX_SUMMARY_V2.md @@ -0,0 +1,172 @@ +# Crash Fix: SpineSkeletonDataResource Destructor (v2) + +## TL;DR + +Fixed segmentation fault during Godot editor shutdown by using `ObjectDB::get_instance()` to safely validate the `EditorFileSystem` pointer before attempting to disconnect the signal in the destructor. The original code used a raw pointer that could become dangling during shutdown; the fix stores the `ObjectID` at construction time and validates it in the destructor. + +**Changed files:** +- `spine_godot/SpineSkeletonDataResource.h` - Added `ObjectID editor_file_system_id` member variable +- `spine_godot/SpineSkeletonDataResource.cpp` - Modified constructor and destructor + +--- + +## Context + +Mario requested a reference for the claim that removing the disconnect code entirely was safe. Upon investigation, that claim was not fully accurate. Godot's automatic signal cleanup in `Object::~Object()` also calls methods on the signal owner, which could crash if the owner is already destroyed. + +The proper fix is to retain the disconnect logic but use Godot's `ObjectDB` system to safely validate that `EditorFileSystem` still exists before calling any methods on it. + +--- + +## The Problem + +The Godot editor crashed with signal 11 (SIGSEGV) during shutdown. The crash occurred in the `SpineSkeletonDataResource` destructor when attempting to disconnect from the `EditorFileSystem` singleton's "resources_reimported" signal. + +**Original crash backtrace:** +``` +[14] SpineSkeletonDataResource::~SpineSkeletonDataResource() + (spine_godot/SpineSkeletonDataResource.cpp:255) +``` + +**Root cause:** During editor shutdown, singletons are destroyed in an undefined order. The destructor used `get_editor_file_system()` which returns a raw pointer. If `EditorFileSystem` was destroyed before `SpineSkeletonDataResource` objects, the pointer became dangling. Calling `efs->is_connected()` on a dangling pointer caused the segfault. + +The check `if (efs)` only verifies the pointer is non-null, not that the memory it points to is valid. + +--- + +## The Solution + +Use Godot's `ObjectDB` system to safely validate object existence: + +1. Store the `ObjectID` of `EditorFileSystem` when connecting the signal (in constructor) +2. Use `ObjectDB::get_instance(id)` in the destructor to check if the object still exists +3. Only proceed with disconnect if the object is valid + +`ObjectDB::get_instance()` is safe to call even if the object has been destroyed - it returns `nullptr` in that case, rather than accessing freed memory. + +--- + +## Code Changes + +### Header file (SpineSkeletonDataResource.h) + +**Added member variable (inside private section, under TOOLS_ENABLED):** +```cpp +#ifdef TOOLS_ENABLED + // Store the ObjectID of EditorFileSystem to safely validate it in destructor. + // Raw pointers to singletons can become dangling during editor shutdown, + // but ObjectID can be safely validated via ObjectDB::get_instance(). + ObjectID editor_file_system_id; +#endif +``` + +### Source file (SpineSkeletonDataResource.cpp) + +**Constructor - store the ObjectID when connecting:** +```cpp +SpineSkeletonDataResource::SpineSkeletonDataResource() + : default_mix(0), skeleton_data(nullptr), animation_state_data(nullptr) { + +#ifdef TOOLS_ENABLED +#if VERSION_MAJOR > 3 + if (Engine::get_singleton()->is_editor_hint()) { + EditorFileSystem *efs = get_editor_file_system(); + if (efs) { + // Store the ObjectID for safe validation in destructor + editor_file_system_id = efs->get_instance_id(); + efs->connect("resources_reimported", callable_mp(this, &SpineSkeletonDataResource::_on_resources_reimported)); + } + } +#else + if (Engine::get_singleton()->is_editor_hint()) { + EditorFileSystem *efs = EditorFileSystem::get_singleton(); + if (efs) { + // Store the ObjectID for safe validation in destructor + editor_file_system_id = efs->get_instance_id(); + efs->connect("resources_reimported", this, "_on_resources_reimported"); + } + } +#endif +#endif +} +``` + +**Destructor - validate via ObjectDB before disconnecting:** +```cpp +SpineSkeletonDataResource::~SpineSkeletonDataResource() { +#ifdef TOOLS_ENABLED +#if VERSION_MAJOR > 3 + if (Engine::get_singleton()->is_editor_hint()) { + // Use ObjectDB::get_instance() to safely check if EditorFileSystem still exists. + // This avoids the dangling pointer problem during editor shutdown where + // EditorFileSystem may be destroyed before SpineSkeletonDataResource objects. + EditorFileSystem *efs = Object::cast_to(ObjectDB::get_instance(editor_file_system_id)); + if (efs && efs->is_connected("resources_reimported", callable_mp(this, &SpineSkeletonDataResource::_on_resources_reimported))) { + efs->disconnect("resources_reimported", callable_mp(this, &SpineSkeletonDataResource::_on_resources_reimported)); + } + } +#else + if (Engine::get_singleton()->is_editor_hint()) { + // Use ObjectDB::get_instance() to safely check if EditorFileSystem still exists. + EditorFileSystem *efs = Object::cast_to(ObjectDB::get_instance(editor_file_system_id)); + if (efs && efs->is_connected("resources_reimported", this, "_on_resources_reimported")) { + efs->disconnect("resources_reimported", this, "_on_resources_reimported"); + } + } +#endif +#endif + + delete skeleton_data; + delete animation_state_data; +} +``` + +--- + +## Why This Works + +1. **ObjectID is just a uint64_t** - It's safe to store and doesn't hold a reference to the object +2. **ObjectDB::get_instance() is safe** - It performs a lookup in Godot's object registry and returns `nullptr` if the object no longer exists, without accessing any potentially-freed memory +3. **The pattern is used throughout Godot** - See `godot/core/object/undo_redo.cpp`, `godot/core/object/message_queue.cpp`, and `godot/core/object/callable_method_pointer.h` for examples + +**From godot/core/object/undo_redo.cpp:355:** +```cpp +Object *obj = ObjectDB::get_instance(op.object); +if (!obj) { //may have been deleted and this is fine + continue; +} +``` + +--- + +## Why Not Just Remove the Disconnect? + +Mario correctly questioned whether removing the disconnect entirely was safe. Investigation revealed: + +1. Godot's `Object::~Object()` does perform automatic signal cleanup (lines 2186-2198 in object.cpp) +2. However, that cleanup also calls `c.signal.get_object()->_disconnect(...)` which accesses the signal owner +3. If the signal owner is destroyed first, the automatic cleanup would also crash + +The automatic cleanup uses `c.callable.get_object()` which does perform ObjectDB validation, but the crash was happening in our custom destructor code before the automatic cleanup ran. + +Retaining explicit disconnect with proper validation is the safer approach and follows patterns used elsewhere in the Godot codebase. + +--- + +## Testing Checklist + +- [ ] Editor starts without errors +- [ ] Editor closes cleanly without crashes +- [ ] Hot-reload works (modify and reimport a Spine asset while editor is running) +- [ ] Normal gameplay with Spine assets works +- [ ] Test with both module build and GDExtension build +- [ ] Test with C# build if applicable + +--- + +## References + +- Godot ObjectDB implementation: `godot/core/object/object.h:1044-1064` +- Godot Object destructor signal cleanup: `godot/core/object/object.cpp:2135-2198` +- Example usage in undo_redo: `godot/core/object/undo_redo.cpp:355` +- GitHub Issue on signal disconnection: https://github.com/godotengine/godot/issues/70414 diff --git a/spine-godot/SpineSkeletonDataResource_old.cpp b/spine-godot/SpineSkeletonDataResource_old.cpp new file mode 100644 index 000000000..62a3b2246 --- /dev/null +++ b/spine-godot/SpineSkeletonDataResource_old.cpp @@ -0,0 +1,853 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated April 5, 2025. Replaces all prior versions. + * + * Copyright (c) 2013-2025, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +#include "SpineSkeletonDataResource.h" +#include "SpineCommon.h" + +#ifdef SPINE_GODOT_EXTENSION +#include +#include +#ifdef TOOLS_ENABLED +#include +#endif +#else +#if VERSION_MAJOR > 3 +#include "core/config/engine.h" +#ifdef TOOLS_ENABLED +#include "editor/editor_interface.h" +#endif +#else +#include "core/engine.h" +#endif +#include +#endif + +#ifdef TOOLS_ENABLED +#ifdef SPINE_GODOT_EXTENSION +#include +#else +#if (VERSION_MAJOR >= 4 && VERSION_MINOR >= 5) +#include "editor/file_system/editor_file_system.h" +#else +#include "editor/editor_file_system.h" +#endif +#endif +#endif + +void SpineAnimationMix::_bind_methods() { + ClassDB::bind_method(D_METHOD("set_from", "from"), + &SpineAnimationMix::set_from); + ClassDB::bind_method(D_METHOD("get_from"), &SpineAnimationMix::get_from); + ClassDB::bind_method(D_METHOD("set_to", "to"), &SpineAnimationMix::set_to); + ClassDB::bind_method(D_METHOD("get_to"), &SpineAnimationMix::get_to); + ClassDB::bind_method(D_METHOD("set_mix", "mix"), &SpineAnimationMix::set_mix); + ClassDB::bind_method(D_METHOD("get_mix"), &SpineAnimationMix::get_mix); + + ADD_PROPERTY(PropertyInfo(Variant::STRING, "from"), "set_from", "get_from"); + ADD_PROPERTY(PropertyInfo(Variant::STRING, "to"), "set_to", "get_to"); +#if VERSION_MAJOR > 3 + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "mix"), "set_mix", "get_mix"); +#else + ADD_PROPERTY(PropertyInfo(Variant::REAL, "mix"), "set_mix", "get_mix"); +#endif +} + +SpineAnimationMix::SpineAnimationMix() : from(""), to(""), mix(0) {} + +void SpineAnimationMix::set_from(const String &_from) { this->from = _from; } + +String SpineAnimationMix::get_from() { return from; } + +void SpineAnimationMix::set_to(const String &_to) { this->to = _to; } + +String SpineAnimationMix::get_to() { return to; } + +void SpineAnimationMix::set_mix(float _mix) { this->mix = _mix; } + +float SpineAnimationMix::get_mix() { return mix; } + +void SpineSkeletonDataResource::_bind_methods() { + ClassDB::bind_method(D_METHOD("is_skeleton_data_loaded"), + &SpineSkeletonDataResource::is_skeleton_data_loaded); + ClassDB::bind_method(D_METHOD("set_atlas_res", "atlas_res"), + &SpineSkeletonDataResource::set_atlas_res); + ClassDB::bind_method(D_METHOD("get_atlas_res"), + &SpineSkeletonDataResource::get_atlas_res); + ClassDB::bind_method(D_METHOD("set_skeleton_file_res", "skeleton_file_res"), + &SpineSkeletonDataResource::set_skeleton_file_res); + ClassDB::bind_method(D_METHOD("get_skeleton_file_res"), + &SpineSkeletonDataResource::get_skeleton_file_res); + ClassDB::bind_method(D_METHOD("set_default_mix", "default_mix"), + &SpineSkeletonDataResource::set_default_mix); + ClassDB::bind_method(D_METHOD("get_default_mix"), + &SpineSkeletonDataResource::get_default_mix); + ClassDB::bind_method(D_METHOD("set_animation_mixes", "mixes"), + &SpineSkeletonDataResource::set_animation_mixes); + ClassDB::bind_method(D_METHOD("get_animation_mixes"), + &SpineSkeletonDataResource::get_animation_mixes); + + // Spine API + ClassDB::bind_method(D_METHOD("find_bone", "bone_name"), + &SpineSkeletonDataResource::find_bone); + ClassDB::bind_method(D_METHOD("find_slot", "slot_name"), + &SpineSkeletonDataResource::find_slot); + ClassDB::bind_method(D_METHOD("find_skin", "skin_name"), + &SpineSkeletonDataResource::find_skin); + ClassDB::bind_method(D_METHOD("find_event", "event_data_name"), + &SpineSkeletonDataResource::find_event); + ClassDB::bind_method(D_METHOD("find_animation", "animation_name"), + &SpineSkeletonDataResource::find_animation); + ClassDB::bind_method(D_METHOD("find_ik_constraint_data", "constraint_name"), + &SpineSkeletonDataResource::find_ik_constraint); + ClassDB::bind_method( + D_METHOD("find_transform_constraint_data", "constraint_name"), + &SpineSkeletonDataResource::find_transform_constraint); + ClassDB::bind_method(D_METHOD("find_path_constraint_data", "constraint_name"), + &SpineSkeletonDataResource::find_path_constraint); + ClassDB::bind_method(D_METHOD("find_physics_constraint_data", "constraint_name"), + &SpineSkeletonDataResource::find_physics_constraint); + ClassDB::bind_method(D_METHOD("get_skeleton_name"), + &SpineSkeletonDataResource::get_skeleton_name); + ClassDB::bind_method(D_METHOD("get_bones"), + &SpineSkeletonDataResource::get_bones); + ClassDB::bind_method(D_METHOD("get_slots"), + &SpineSkeletonDataResource::get_slots); + ClassDB::bind_method(D_METHOD("get_skins"), + &SpineSkeletonDataResource::get_skins); + ClassDB::bind_method(D_METHOD("get_default_skin"), + &SpineSkeletonDataResource::get_default_skin); + ClassDB::bind_method(D_METHOD("set_default_skin", "skin"), + &SpineSkeletonDataResource::set_default_skin); + ClassDB::bind_method(D_METHOD("get_events"), + &SpineSkeletonDataResource::get_events); + ClassDB::bind_method(D_METHOD("get_animations"), + &SpineSkeletonDataResource::get_animations); + ClassDB::bind_method(D_METHOD("get_ik_constraints"), + &SpineSkeletonDataResource::get_ik_constraints); + ClassDB::bind_method(D_METHOD("get_transform_constraints"), + &SpineSkeletonDataResource::get_transform_constraints); + ClassDB::bind_method(D_METHOD("get_path_constraints"), + &SpineSkeletonDataResource::get_path_constraints); + ClassDB::bind_method(D_METHOD("get_physics_constraints"), + &SpineSkeletonDataResource::get_physics_constraints); + ClassDB::bind_method(D_METHOD("get_x"), &SpineSkeletonDataResource::get_x); + ClassDB::bind_method(D_METHOD("get_y"), &SpineSkeletonDataResource::get_y); + ClassDB::bind_method(D_METHOD("get_width"), + &SpineSkeletonDataResource::get_width); + ClassDB::bind_method(D_METHOD("get_height"), + &SpineSkeletonDataResource::get_height); + ClassDB::bind_method(D_METHOD("get_version"), + &SpineSkeletonDataResource::get_version); + ClassDB::bind_method(D_METHOD("get_hash"), + &SpineSkeletonDataResource::get_hash); + ClassDB::bind_method(D_METHOD("get_images_path"), + &SpineSkeletonDataResource::get_images_path); + ClassDB::bind_method(D_METHOD("get_audio_path"), + &SpineSkeletonDataResource::get_audio_path); + ClassDB::bind_method(D_METHOD("get_fps"), + &SpineSkeletonDataResource::get_fps); + ClassDB::bind_method(D_METHOD("get_reference_scale"), + &SpineSkeletonDataResource::get_reference_scale); + ClassDB::bind_method(D_METHOD("set_reference_scale", "reference_scale"), + &SpineSkeletonDataResource::set_reference_scale); + ClassDB::bind_method(D_METHOD("update_skeleton_data"), + &SpineSkeletonDataResource::update_skeleton_data); + + ADD_SIGNAL(MethodInfo("skeleton_data_changed")); + ADD_SIGNAL(MethodInfo("_internal_spine_objects_invalidated")); + + ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "atlas_res", + PropertyHint::PROPERTY_HINT_RESOURCE_TYPE, + "SpineAtlasResource"), + "set_atlas_res", "get_atlas_res"); + ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "skeleton_file_res", + PropertyHint::PROPERTY_HINT_RESOURCE_TYPE, + "SpineSkeletonFileResource"), + "set_skeleton_file_res", "get_skeleton_file_res"); +#if VERSION_MAJOR > 3 + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "default_mix"), "set_default_mix", + "get_default_mix"); +#else + ADD_PROPERTY(PropertyInfo(Variant::REAL, "default_mix"), "set_default_mix", + "get_default_mix"); +#endif + ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "animation_mixes"), + "set_animation_mixes", "get_animation_mixes"); + +#ifdef TOOLS_ENABLED +#if VERSION_MAJOR > 3 + ClassDB::bind_method(D_METHOD("_on_resources_reimported", "resources"), + &SpineSkeletonDataResource::_on_resources_reimported); +#else + ClassDB::bind_method(D_METHOD("_on_resources_reimported", "resources"), + &SpineSkeletonDataResource::_on_resources_reimported); +#endif +#endif +} + +#ifdef TOOLS_ENABLED +EditorFileSystem *get_editor_file_system() { +#ifdef SPINE_GODOT_EXTENSION + EditorInterface *editor_interface = EditorInterface::get_singleton(); + if (editor_interface) { + return editor_interface->get_resource_filesystem(); + } + return nullptr; +#else + return EditorFileSystem::get_singleton(); +#endif +} +#endif + +SpineSkeletonDataResource::SpineSkeletonDataResource() + : default_mix(0), skeleton_data(nullptr), animation_state_data(nullptr) { + +#ifdef TOOLS_ENABLED +#if VERSION_MAJOR > 3 + if (Engine::get_singleton()->is_editor_hint()) { + EditorFileSystem *efs = get_editor_file_system(); + if (efs) { + efs->connect("resources_reimported", callable_mp(this, &SpineSkeletonDataResource::_on_resources_reimported)); + } + } +#else + if (Engine::get_singleton()->is_editor_hint()) { + EditorFileSystem *efs = EditorFileSystem::get_singleton(); + if (efs) { + efs->connect("resources_reimported", this, "_on_resources_reimported"); + } + } +#endif +#endif +} + +SpineSkeletonDataResource::~SpineSkeletonDataResource() { +/*#ifdef TOOLS_ENABLED +#if VERSION_MAJOR > 3 + if (Engine::get_singleton()->is_editor_hint()) { + EditorFileSystem *efs = get_editor_file_system(); + if (efs && efs->is_connected("resources_reimported", callable_mp(this, &SpineSkeletonDataResource::_on_resources_reimported))) { + efs->disconnect("resources_reimported", callable_mp(this, &SpineSkeletonDataResource::_on_resources_reimported)); + } + } +#else + if (Engine::get_singleton()->is_editor_hint()) { + EditorFileSystem *efs = EditorFileSystem::get_singleton(); + if (efs && efs->is_connected("resources_reimported", this, "_on_resources_reimported")) { + efs->disconnect("resources_reimported", this, "_on_resources_reimported"); + } + } +#endif +#endif +*/ + delete skeleton_data; + delete animation_state_data; +} + +#ifdef TOOLS_ENABLED +#if VERSION_MAJOR > 3 +void SpineSkeletonDataResource::_on_resources_reimported(const PackedStringArray &resources) { + for (int i = 0; i < resources.size(); i++) { + if (atlas_res.is_valid() && atlas_res->get_path() == resources[i]) { +#ifdef SPINE_GODOT_EXTENSION + atlas_res = ResourceLoader::get_singleton()->load(resources[i], "SpineAtlasResource", ResourceLoader::CACHE_MODE_IGNORE); +#else + atlas_res = ResourceLoader::load(resources[i], "SpineAtlasResource", ResourceFormatLoader::CACHE_MODE_IGNORE); +#endif + update_skeleton_data(); + } else if (skeleton_file_res.is_valid() && skeleton_file_res->get_path() == resources[i]) { +#ifdef SPINE_GODOT_EXTENSION + skeleton_file_res = ResourceLoader::get_singleton()->load(resources[i], "SpineSkeletonFileResource", ResourceLoader::CACHE_MODE_IGNORE); +#else + skeleton_file_res = ResourceLoader::load(resources[i], "SpineSkeletonFileResource", ResourceFormatLoader::CACHE_MODE_IGNORE); +#endif + update_skeleton_data(); + } + } +} +#else +void SpineSkeletonDataResource::_on_resources_reimported(const PoolStringArray &resources) { + for (int i = 0; i < resources.size(); i++) { + if (atlas_res.is_valid() && atlas_res->get_path() == resources[i]) { + atlas_res = ResourceLoader::load(resources[i]); + update_skeleton_data(); + } else if (skeleton_file_res.is_valid() && skeleton_file_res->get_path() == resources[i]) { + skeleton_file_res = ResourceLoader::load(resources[i]); + update_skeleton_data(); + } + } +} +#endif +#endif + +void SpineSkeletonDataResource::update_skeleton_data() { + if (skeleton_data) { + delete skeleton_data; + skeleton_data = nullptr; + } + if (animation_state_data) { + delete animation_state_data; + animation_state_data = nullptr; + } + + emit_signal(SNAME("_internal_spine_objects_invalidated")); + + if (atlas_res.is_valid() && skeleton_file_res.is_valid()) { + load_resources(atlas_res->get_spine_atlas(), skeleton_file_res->get_json(), + skeleton_file_res->get_binary()); + } + emit_signal(SNAME("skeleton_data_changed")); +#ifdef TOOLS_ENABLED + NOTIFY_PROPERTY_LIST_CHANGED(); +#endif +} + +#ifdef SPINE_GODOT_EXTENSION +void SpineSkeletonDataResource::load_resources(spine::Atlas *atlas, + const String &json, + const PackedByteArray &binary) { +#else +void SpineSkeletonDataResource::load_resources(spine::Atlas *atlas, + const String &json, + const Vector &binary) { +#endif + if ((EMPTY(json) && EMPTY(binary)) || atlas == nullptr) + return; + + spine::SkeletonData *data; + if (!EMPTY(json)) { + spine::SkeletonJson skeletonJson(atlas); + data = skeletonJson.readSkeletonData(json.utf8().ptr()); + if (!data) { + ERR_PRINT(String("Error while loading skeleton data: ") + get_path()); + ERR_PRINT(String("Error message: ") + skeletonJson.getError().buffer()); + return; + } + } else { + spine::SkeletonBinary skeletonBinary(atlas); + data = skeletonBinary.readSkeletonData(binary.ptr(), binary.size()); + if (!data) { + ERR_PRINT(String("Error while loading skeleton data: ") + get_path()); + ERR_PRINT(String("Error message: ") + skeletonBinary.getError().buffer()); + return; + } + } + skeleton_data = data; + animation_state_data = new spine::AnimationStateData(data); + update_mixes(); +} + +bool SpineSkeletonDataResource::is_skeleton_data_loaded() const { + return skeleton_data != nullptr; +} + +void SpineSkeletonDataResource::set_atlas_res( + const Ref &atlas) { + atlas_res = atlas; + update_skeleton_data(); +} + +Ref SpineSkeletonDataResource::get_atlas_res() { + return atlas_res; +} + +void SpineSkeletonDataResource::set_skeleton_file_res( + const Ref &skeleton_file) { + skeleton_file_res = skeleton_file; + update_skeleton_data(); +} + +Ref +SpineSkeletonDataResource::get_skeleton_file_res() { + return skeleton_file_res; +} + +#ifdef SPINE_GODOT_EXTENSION +void SpineSkeletonDataResource::get_animation_names(PackedStringArray &animation_names) const { +#else +void SpineSkeletonDataResource::get_animation_names(Vector &animation_names) const { +#endif + animation_names.clear(); + if (!is_skeleton_data_loaded()) + return; + auto animations = skeleton_data->getAnimations(); + for (size_t i = 0; i < animations.size(); ++i) { + auto animation = animations[i]; + String name; +#if (VERSION_MAJOR >= 4 && VERSION_MINOR >= 5) + name = String::utf8(animation->getName().buffer()); +#else + name.parse_utf8(animation->getName().buffer()); +#endif + animation_names.push_back(name); + } +} + +#ifdef SPINE_GODOT_EXTENSION +void SpineSkeletonDataResource::get_skin_names(PackedStringArray &skin_names) const { +#else +void SpineSkeletonDataResource::get_skin_names(Vector &skin_names) const { +#endif + skin_names.clear(); + if (!is_skeleton_data_loaded()) + return; + auto skins = skeleton_data->getSkins(); + for (size_t i = 0; i < skins.size(); ++i) { + auto skin = skins[i]; + String name; +#if (VERSION_MAJOR >= 4 && VERSION_MINOR >= 5) + name = String::utf8(skin->getName().buffer()); +#else + name.parse_utf8(skin->getName().buffer()); +#endif + skin_names.push_back(name); + } +} + +#ifdef SPINE_GODOT_EXTENSION +void SpineSkeletonDataResource::get_slot_names(PackedStringArray &slot_names) { +#else +void SpineSkeletonDataResource::get_slot_names(Vector &slot_names) { +#endif + slot_names.clear(); + if (!is_skeleton_data_loaded()) + return; + auto slots = skeleton_data->getSlots(); + for (size_t i = 0; i < slots.size(); ++i) { + auto slot = slots[i]; + String name; +#if (VERSION_MAJOR >= 4 && VERSION_MINOR >= 5) + name = String::utf8(slot->getName().buffer()); +#else + name.parse_utf8(slot->getName().buffer()); +#endif + slot_names.push_back(name); + } +} + +#ifdef SPINE_GODOT_EXTENSION +void SpineSkeletonDataResource::get_bone_names(PackedStringArray &bone_names) { +#else +void SpineSkeletonDataResource::get_bone_names(Vector &bone_names) { +#endif + bone_names.clear(); + if (!is_skeleton_data_loaded()) + return; + auto bones = skeleton_data->getBones(); + for (size_t i = 0; i < bones.size(); ++i) { + auto bone = bones[i]; + String name; +#if (VERSION_MAJOR >= 4 && VERSION_MINOR >= 5) + name = String::utf8(bone->getName().buffer()); +#else + name.parse_utf8(bone->getName().buffer()); +#endif + bone_names.push_back(name); + } +} + +void SpineSkeletonDataResource::set_default_mix(float _default_mix) { + this->default_mix = _default_mix; + update_mixes(); +} + +float SpineSkeletonDataResource::get_default_mix() { return default_mix; } + +void SpineSkeletonDataResource::set_animation_mixes(Array _animation_mixes) { + for (int i = 0; i < _animation_mixes.size(); i++) { + auto objectId = Object::cast_to(_animation_mixes[0]); + if (objectId) { + ERR_PRINT("Live-editing of animation mixes is not supported."); + return; + } + } + + this->animation_mixes = _animation_mixes; + update_mixes(); +} + +Array SpineSkeletonDataResource::get_animation_mixes() { + return animation_mixes; +} + +void SpineSkeletonDataResource::update_mixes() { + if (!is_skeleton_data_loaded()) + return; + animation_state_data->clear(); + animation_state_data->setDefaultMix(default_mix); + for (int i = 0; i < animation_mixes.size(); i++) { + Ref mix = animation_mixes[i]; + spine::Animation *from = + skeleton_data->findAnimation(mix->get_from().utf8().ptr()); + spine::Animation *to = + skeleton_data->findAnimation(mix->get_to().utf8().ptr()); + if (!from) { + ERR_PRINT(vformat("Failed to set animation mix %s->%s. Animation %s does " + "not exist in skeleton.", + from, to, from)); + continue; + } + if (!to) { + ERR_PRINT(vformat("Failed to set animation mix %s->%s. Animation %s does " + "not exist in skeleton.", + from, to, to)); + continue; + } + animation_state_data->setMix(from, to, mix->get_mix()); + } +} + +Ref +SpineSkeletonDataResource::find_animation(const String &animation_name) const { + SPINE_CHECK(skeleton_data, nullptr) + if (EMPTY(animation_name)) + return nullptr; + auto animation = + skeleton_data->findAnimation(SPINE_STRING_TMP(animation_name)); + if (!animation) + return nullptr; + Ref animation_ref(memnew(SpineAnimation)); + animation_ref->set_spine_object(this, animation); + return animation_ref; +} + +Ref +SpineSkeletonDataResource::find_bone(const String &bone_name) const { + SPINE_CHECK(skeleton_data, nullptr) + if (EMPTY(bone_name)) + return nullptr; + auto bone = skeleton_data->findBone(SPINE_STRING_TMP(bone_name)); + if (!bone) + return nullptr; + Ref bone_ref(memnew(SpineBoneData)); + bone_ref->set_spine_object(this, bone); + return bone_ref; +} + +Ref +SpineSkeletonDataResource::find_slot(const String &slot_name) const { + SPINE_CHECK(skeleton_data, nullptr) + if (EMPTY(slot_name)) + return nullptr; + auto slot = skeleton_data->findSlot(SPINE_STRING_TMP(slot_name)); + if (!slot) + return nullptr; + Ref slot_ref(memnew(SpineSlotData)); + slot_ref->set_spine_object(this, slot); + return slot_ref; +} + +Ref +SpineSkeletonDataResource::find_skin(const String &skin_name) const { + SPINE_CHECK(skeleton_data, nullptr) + if (EMPTY(skin_name)) + return nullptr; + auto skin = skeleton_data->findSkin(SPINE_STRING_TMP(skin_name)); + if (!skin) + return nullptr; + Ref skin_ref(memnew(SpineSkin)); + skin_ref->set_spine_object(this, skin); + return skin_ref; +} + +Ref +SpineSkeletonDataResource::find_event(const String &event_data_name) const { + SPINE_CHECK(skeleton_data, nullptr) + if (EMPTY(event_data_name)) + return nullptr; + auto event = skeleton_data->findEvent(SPINE_STRING_TMP(event_data_name)); + if (!event) + return nullptr; + Ref event_ref(memnew(SpineEventData)); + event_ref->set_spine_object(this, event); + return event_ref; +} + +Ref SpineSkeletonDataResource::find_ik_constraint( + const String &constraint_name) const { + SPINE_CHECK(skeleton_data, nullptr) + if (EMPTY(constraint_name)) + return nullptr; + auto constraint = + skeleton_data->findIkConstraint(SPINE_STRING_TMP(constraint_name)); + if (!constraint) + return nullptr; + Ref constraint_ref(memnew(SpineIkConstraintData)); + constraint_ref->set_spine_object(this, constraint); + return constraint_ref; +} + +Ref +SpineSkeletonDataResource::find_transform_constraint( + const String &constraint_name) const { + SPINE_CHECK(skeleton_data, nullptr) + if (EMPTY(constraint_name)) + return nullptr; + auto constraint = + skeleton_data->findTransformConstraint(SPINE_STRING_TMP(constraint_name)); + if (!constraint) + return nullptr; + Ref constraint_ref( + memnew(SpineTransformConstraintData)); + constraint_ref->set_spine_object(this, constraint); + return constraint_ref; +} + +Ref SpineSkeletonDataResource::find_path_constraint( + const String &constraint_name) const { + SPINE_CHECK(skeleton_data, nullptr) + if (EMPTY(constraint_name)) + return nullptr; + auto constraint = + skeleton_data->findPathConstraint(SPINE_STRING_TMP(constraint_name)); + if (constraint == nullptr) + return nullptr; + Ref constraint_ref(memnew(SpinePathConstraintData)); + constraint_ref->set_spine_object(this, constraint); + return constraint_ref; +} + +Ref +SpineSkeletonDataResource::find_physics_constraint( + const String &constraint_name) const { + SPINE_CHECK(skeleton_data, nullptr) + if (EMPTY(constraint_name)) + return nullptr; + auto constraint = + skeleton_data->findPhysicsConstraint(SPINE_STRING_TMP(constraint_name)); + if (constraint == nullptr) + return nullptr; + Ref constraint_ref( + memnew(SpinePhysicsConstraintData)); + constraint_ref->set_spine_object(this, constraint); + return constraint_ref; +} + +String SpineSkeletonDataResource::get_skeleton_name() const { + SPINE_CHECK(skeleton_data, "") + String name; +#if (VERSION_MAJOR >= 4 && VERSION_MINOR >= 5) + name = String::utf8(skeleton_data->getName().buffer()); +#else + name.parse_utf8(skeleton_data->getName().buffer()); +#endif + return name; +} + +Array SpineSkeletonDataResource::get_bones() const { + Array result; + SPINE_CHECK(skeleton_data, result) + auto bones = skeleton_data->getBones(); + result.resize((int) bones.size()); + for (int i = 0; i < bones.size(); ++i) { + Ref bone_ref(memnew(SpineBoneData)); + bone_ref->set_spine_object(this, bones[i]); + result[i] = bone_ref; + } + return result; +} + +Array SpineSkeletonDataResource::get_slots() const { + Array result; + SPINE_CHECK(skeleton_data, result) + auto slots = skeleton_data->getSlots(); + result.resize((int) slots.size()); + for (int i = 0; i < slots.size(); ++i) { + Ref slot_ref(memnew(SpineSlotData)); + slot_ref->set_spine_object(this, slots[i]); + result[i] = slot_ref; + } + return result; +} + +Array SpineSkeletonDataResource::get_skins() const { + Array result; + SPINE_CHECK(skeleton_data, result) + auto skins = skeleton_data->getSkins(); + result.resize((int) skins.size()); + for (int i = 0; i < skins.size(); ++i) { + Ref skin_ref(memnew(SpineSkin)); + skin_ref->set_spine_object(this, skins[i]); + result[i] = skin_ref; + } + return result; +} + +Ref SpineSkeletonDataResource::get_default_skin() const { + SPINE_CHECK(skeleton_data, nullptr) + auto skin = skeleton_data->getDefaultSkin(); + if (skin) + return nullptr; + Ref skin_ref(memnew(SpineSkin)); + skin_ref->set_spine_object(this, skin); + return skin_ref; +} + +void SpineSkeletonDataResource::set_default_skin(Ref skin) { + SPINE_CHECK(skeleton_data, ) + skeleton_data->setDefaultSkin(skin.is_valid() && skin->get_spine_object() + ? skin->get_spine_object() + : nullptr); +} + +Array SpineSkeletonDataResource::get_events() const { + Array result; + SPINE_CHECK(skeleton_data, result) + auto events = skeleton_data->getEvents(); + result.resize((int) events.size()); + for (int i = 0; i < events.size(); ++i) { + Ref event_ref(memnew(SpineEventData)); + event_ref->set_spine_object(this, events[i]); + result[i] = event_ref; + } + return result; +} + +Array SpineSkeletonDataResource::get_animations() const { + Array result; + SPINE_CHECK(skeleton_data, result) + auto animations = skeleton_data->getAnimations(); + result.resize((int) animations.size()); + for (int i = 0; i < animations.size(); ++i) { + Ref animation_ref(memnew(SpineAnimation)); + animation_ref->set_spine_object(this, animations[i]); + result[i] = animation_ref; + } + return result; +} + +Array SpineSkeletonDataResource::get_ik_constraints() const { + Array result; + SPINE_CHECK(skeleton_data, result) + auto constraints = skeleton_data->getIkConstraints(); + result.resize((int) constraints.size()); + for (int i = 0; i < constraints.size(); ++i) { + Ref constraint_ref(memnew(SpineIkConstraintData)); + constraint_ref->set_spine_object(this, constraints[i]); + result[i] = constraint_ref; + } + return result; +} + +Array SpineSkeletonDataResource::get_transform_constraints() const { + Array result; + SPINE_CHECK(skeleton_data, result) + auto constraints = skeleton_data->getTransformConstraints(); + result.resize((int) constraints.size()); + for (int i = 0; i < constraints.size(); ++i) { + Ref constraint_ref( + memnew(SpineTransformConstraintData)); + constraint_ref->set_spine_object(this, constraints[i]); + result[i] = constraint_ref; + } + return result; +} + +Array SpineSkeletonDataResource::get_path_constraints() const { + Array result; + SPINE_CHECK(skeleton_data, result) + auto constraints = skeleton_data->getPathConstraints(); + result.resize((int) constraints.size()); + for (int i = 0; i < constraints.size(); ++i) { + Ref constraint_ref( + memnew(SpinePathConstraintData)); + constraint_ref->set_spine_object(this, constraints[i]); + result[i] = constraint_ref; + } + return result; +} + +Array SpineSkeletonDataResource::get_physics_constraints() const { + Array result; + SPINE_CHECK(skeleton_data, result) + auto constraints = skeleton_data->getPhysicsConstraints(); + result.resize((int) constraints.size()); + for (int i = 0; i < constraints.size(); ++i) { + Ref constraint_ref( + memnew(SpinePhysicsConstraintData)); + constraint_ref->set_spine_object(this, constraints[i]); + result[i] = constraint_ref; + } + return result; +} + +float SpineSkeletonDataResource::get_x() const { + SPINE_CHECK(skeleton_data, 0) + return skeleton_data->getX(); +} + +float SpineSkeletonDataResource::get_y() const { + SPINE_CHECK(skeleton_data, 0) + return skeleton_data->getY(); +} + +float SpineSkeletonDataResource::get_width() const { + SPINE_CHECK(skeleton_data, 0) + return skeleton_data->getWidth(); +} + +float SpineSkeletonDataResource::get_height() const { + SPINE_CHECK(skeleton_data, 0) + return skeleton_data->getHeight(); +} + +String SpineSkeletonDataResource::get_version() const { + SPINE_CHECK(skeleton_data, "") + return skeleton_data->getVersion().buffer(); +} + +String SpineSkeletonDataResource::get_hash() const { + SPINE_CHECK(skeleton_data, "") + return skeleton_data->getHash().buffer(); +} + +String SpineSkeletonDataResource::get_images_path() const { + SPINE_CHECK(skeleton_data, "") + return skeleton_data->getImagesPath().buffer(); +} + +String SpineSkeletonDataResource::get_audio_path() const { + SPINE_CHECK(skeleton_data, "") + return skeleton_data->getAudioPath().buffer(); +} + +float SpineSkeletonDataResource::get_fps() const { + SPINE_CHECK(skeleton_data, 0) + return skeleton_data->getFps(); +} + +float SpineSkeletonDataResource::get_reference_scale() const { + SPINE_CHECK(skeleton_data, 100); + return skeleton_data->getReferenceScale(); +} + +void SpineSkeletonDataResource::set_reference_scale(float reference_scale) { + SPINE_CHECK(skeleton_data, ) + skeleton_data->setReferenceScale(reference_scale); +} diff --git a/spine-godot/SpineSkeletonDataResource_old.h b/spine-godot/SpineSkeletonDataResource_old.h new file mode 100644 index 000000000..6292e9c8c --- /dev/null +++ b/spine-godot/SpineSkeletonDataResource_old.h @@ -0,0 +1,220 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated April 5, 2025. Replaces all prior versions. + * + * Copyright (c) 2013-2025, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +#pragma once + +#include "SpineAnimation.h" +#include "SpineAtlasResource.h" +#include "SpineBoneData.h" +#include "SpineEventData.h" +#include "SpineIkConstraintData.h" +#include "SpinePathConstraintData.h" +#include "SpinePhysicsConstraintData.h" +#include "SpineSkeletonFileResource.h" +#include "SpineSkin.h" +#include "SpineSlotData.h" +#include "SpineTransformConstraintData.h" + +class SpineAnimationMix : public Resource { + GDCLASS(SpineAnimationMix, Resource) + +protected: + static void _bind_methods(); + + String from; + String to; + float mix; + +public: + SpineAnimationMix(); + + void set_from(const String &from); + + String get_from(); + + void set_to(const String &to); + + String get_to(); + + void set_mix(float mix); + + float get_mix(); +}; + +class SpineSkeletonDataResource : public Resource { + GDCLASS(SpineSkeletonDataResource, Resource) + +protected: + static void _bind_methods(); + +private: + Ref atlas_res; + Ref skeleton_file_res; + float default_mix; + Array animation_mixes; + + spine::SkeletonData *skeleton_data; + spine::AnimationStateData *animation_state_data; + + void update_skeleton_data(); + +#ifdef SPINE_GODOT_EXTENSION + void load_resources(spine::Atlas *atlas, const String &json, + const PackedByteArray &binary); +#else + void load_resources(spine::Atlas *atlas, const String &json, + const Vector &binary); +#endif + +public: + SpineSkeletonDataResource(); + virtual ~SpineSkeletonDataResource(); + + bool is_skeleton_data_loaded() const; + + void set_atlas_res(const Ref &atlas); + Ref get_atlas_res(); + + void + set_skeleton_file_res(const Ref &skeleton_file); + Ref get_skeleton_file_res(); + + spine::SkeletonData *get_skeleton_data() const { return skeleton_data; } + + spine::AnimationStateData *get_animation_state_data() const { + return animation_state_data; + } + +#ifdef SPINE_GODOT_EXTENSION + void get_animation_names(PackedStringArray &animation_names) const; + + void get_skin_names(PackedStringArray &l) const; + + void get_slot_names(PackedStringArray &slot_names); + + void get_bone_names(PackedStringArray &bone_names); +#else + void get_animation_names(Vector &animation_names) const; + + void get_skin_names(Vector &l) const; + + void get_slot_names(Vector &slot_names); + + void get_bone_names(Vector &bone_names); +#endif + + void set_default_mix(float default_mix); + + float get_default_mix(); + + void set_animation_mixes(Array animation_mixes); + + Array get_animation_mixes(); + + // Used by SpineEditorPropertyAnimationMix(es) to update the underlying + // AnimationState + void update_mixes(); + + // Spine API + Ref find_bone(const String &bone_name) const; + + Ref find_slot(const String &slot_name) const; + + Ref find_skin(const String &skin_name) const; + + Ref find_event(const String &event_data_name) const; + + Ref find_animation(const String &animation_name) const; + + Ref + find_ik_constraint(const String &constraint_name) const; + + Ref + find_transform_constraint(const String &constraint_name) const; + + Ref + find_path_constraint(const String &constraint_name) const; + + Ref + find_physics_constraint(const String &constraint_name) const; + + String get_skeleton_name() const; + + Array get_bones() const; + + Array get_slots() const; + + Array get_skins() const; + + Ref get_default_skin() const; + + void set_default_skin(Ref skin); + + Array get_events() const; + + Array get_animations() const; + + Array get_ik_constraints() const; + + Array get_transform_constraints() const; + + Array get_path_constraints() const; + + Array get_physics_constraints() const; + + float get_x() const; + + float get_y() const; + + float get_width() const; + + float get_height() const; + + String get_version() const; + + String get_hash() const; + + String get_images_path() const; + + String get_audio_path() const; + + float get_fps() const; + + float get_reference_scale() const; + + void set_reference_scale(float reference_scale); + +#ifdef TOOLS_ENABLED +#if VERSION_MAJOR > 3 + void _on_resources_reimported(const PackedStringArray &resources); +#else + void _on_resources_reimported(const PoolStringArray &resources); +#endif +#endif +}; diff --git a/spine-godot/spine_godot/SpineSkeletonDataResource.cpp b/spine-godot/spine_godot/SpineSkeletonDataResource.cpp index eedd9498a..8fc35ca10 100644 --- a/spine-godot/spine_godot/SpineSkeletonDataResource.cpp +++ b/spine-godot/spine_godot/SpineSkeletonDataResource.cpp @@ -1,3 +1,4 @@ +// this is my version (v1), generated with Claude Code /****************************************************************************** * Spine Runtimes License Agreement * Last updated April 5, 2025. Replaces all prior versions. @@ -233,6 +234,8 @@ SpineSkeletonDataResource::SpineSkeletonDataResource() if (Engine::get_singleton()->is_editor_hint()) { EditorFileSystem *efs = get_editor_file_system(); if (efs) { + // Store the ObjectID for safe validation in destructor + editor_file_system_id = efs->get_instance_id(); efs->connect("resources_reimported", callable_mp(this, &SpineSkeletonDataResource::_on_resources_reimported)); } } @@ -240,6 +243,8 @@ SpineSkeletonDataResource::SpineSkeletonDataResource() if (Engine::get_singleton()->is_editor_hint()) { EditorFileSystem *efs = EditorFileSystem::get_singleton(); if (efs) { + // Store the ObjectID for safe validation in destructor + editor_file_system_id = efs->get_instance_id(); efs->connect("resources_reimported", this, "_on_resources_reimported"); } } @@ -251,14 +256,18 @@ SpineSkeletonDataResource::~SpineSkeletonDataResource() { #ifdef TOOLS_ENABLED #if VERSION_MAJOR > 3 if (Engine::get_singleton()->is_editor_hint()) { - EditorFileSystem *efs = get_editor_file_system(); + // Use ObjectDB::get_instance() to safely check if EditorFileSystem still exists. + // This avoids the dangling pointer problem during editor shutdown where + // EditorFileSystem may be destroyed before SpineSkeletonDataResource objects. + EditorFileSystem *efs = Object::cast_to(ObjectDB::get_instance(editor_file_system_id)); if (efs && efs->is_connected("resources_reimported", callable_mp(this, &SpineSkeletonDataResource::_on_resources_reimported))) { efs->disconnect("resources_reimported", callable_mp(this, &SpineSkeletonDataResource::_on_resources_reimported)); } } #else if (Engine::get_singleton()->is_editor_hint()) { - EditorFileSystem *efs = EditorFileSystem::get_singleton(); + // Use ObjectDB::get_instance() to safely check if EditorFileSystem still exists. + EditorFileSystem *efs = Object::cast_to(ObjectDB::get_instance(editor_file_system_id)); if (efs && efs->is_connected("resources_reimported", this, "_on_resources_reimported")) { efs->disconnect("resources_reimported", this, "_on_resources_reimported"); } diff --git a/spine-godot/spine_godot/SpineSkeletonDataResource.h b/spine-godot/spine_godot/SpineSkeletonDataResource.h index 6292e9c8c..95b1441fa 100644 --- a/spine-godot/spine_godot/SpineSkeletonDataResource.h +++ b/spine-godot/spine_godot/SpineSkeletonDataResource.h @@ -1,3 +1,4 @@ +// this is my version (v1), generated with Claude Code /****************************************************************************** * Spine Runtimes License Agreement * Last updated April 5, 2025. Replaces all prior versions. @@ -82,6 +83,13 @@ private: spine::SkeletonData *skeleton_data; spine::AnimationStateData *animation_state_data; +#ifdef TOOLS_ENABLED + // Store the ObjectID of EditorFileSystem to safely validate it in destructor. + // Raw pointers to singletons can become dangling during editor shutdown, + // but ObjectID can be safely validated via ObjectDB::get_instance(). + ObjectID editor_file_system_id; +#endif + void update_skeleton_data(); #ifdef SPINE_GODOT_EXTENSION diff --git a/spine-godot/spine_godot/register_types.cpp b/spine-godot/spine_godot/register_types.cpp index f2c11c1fa..f4b2cf781 100644 --- a/spine-godot/spine_godot/register_types.cpp +++ b/spine-godot/spine_godot/register_types.cpp @@ -88,6 +88,12 @@ void initialize_spine_godot_module(ModuleInitializationLevel level) { EditorPlugins::add_plugin_class(StringName("SpineEditorPlugin")); #endif } + if (level == MODULE_INITIALIZATION_LEVEL_CORE) { + GDREGISTER_CLASS(SpineAtlasResourceFormatLoader); + GDREGISTER_CLASS(SpineAtlasResourceFormatSaver); + GDREGISTER_CLASS(SpineSkeletonFileResourceFormatLoader); + GDREGISTER_CLASS(SpineSkeletonFileResourceFormatSaver); + } if (level != MODULE_INITIALIZATION_LEVEL_SCENE) return; #else #if VERSION_MAJOR > 3 @@ -110,10 +116,12 @@ void register_spine_godot_types() { #endif spine::Bone::setYDown(true); + #ifndef SPINE_GODOT_EXTENSION GDREGISTER_CLASS(SpineAtlasResourceFormatLoader); GDREGISTER_CLASS(SpineAtlasResourceFormatSaver); GDREGISTER_CLASS(SpineSkeletonFileResourceFormatLoader); GDREGISTER_CLASS(SpineSkeletonFileResourceFormatSaver); + #endif GDREGISTER_CLASS(SpineObjectWrapper); GDREGISTER_CLASS(SpineAtlasResource); @@ -223,7 +231,7 @@ extern "C" GDExtensionBool GDE_EXPORT spine_godot_library_init(GDExtensionInterf GDExtensionBinding::InitObject init_obj(p_get_proc_address, p_library, r_initialization); init_obj.register_initializer(initialize_spine_godot_module); init_obj.register_terminator(uninitialize_spine_godot_module); - init_obj.set_minimum_library_initialization_level(MODULE_INITIALIZATION_LEVEL_SCENE); + init_obj.set_minimum_library_initialization_level(MODULE_INITIALIZATION_LEVEL_CORE); return init_obj.init(); } #endif