Skip to content

Commit 5ecb241

Browse files
committed
Fixes on loreline-godot
1 parent fceb103 commit 5ecb241

File tree

7 files changed

+163
-21
lines changed

7 files changed

+163
-21
lines changed

godot/src/loreline_interpreter.cpp

Lines changed: 79 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,66 @@
11
#include "loreline_interpreter.h"
2+
#include "loreline_runtime.h"
23
#include "loreline_options.h"
34

45
#include <godot_cpp/variant/callable.hpp>
6+
#include <godot_cpp/variant/callable_custom.hpp>
57
#include <godot_cpp/variant/utility_functions.hpp>
68

9+
// --- Custom Callables that hold a Ref<LorelineInterpreter> ---
10+
// Keeping a reference to the Callable keeps the interpreter alive.
11+
12+
class LorelineAdvanceCallable : public CallableCustom {
13+
Ref<LorelineInterpreter> _interp;
14+
public:
15+
LorelineAdvanceCallable(const Ref<LorelineInterpreter> &interp) : _interp(interp) {}
16+
17+
uint32_t hash() const override { return _interp->get_instance_id(); }
18+
String get_as_text() const override { return "LorelineInterpreter::advance()"; }
19+
ObjectID get_object() const override { return ObjectID(_interp->get_instance_id()); }
20+
21+
static bool compare_equal(const CallableCustom *a, const CallableCustom *b) {
22+
return a == b;
23+
}
24+
static bool compare_less(const CallableCustom *a, const CallableCustom *b) {
25+
return a < b;
26+
}
27+
CompareEqualFunc get_compare_equal_func() const override { return compare_equal; }
28+
CompareLessFunc get_compare_less_func() const override { return compare_less; }
29+
30+
void call(const Variant **p_arguments, int p_argcount, Variant &r_return_value, GDExtensionCallError &r_call_error) const override {
31+
_interp->advance();
32+
r_call_error.error = GDEXTENSION_CALL_OK;
33+
}
34+
};
35+
36+
class LorelineSelectCallable : public CallableCustom {
37+
Ref<LorelineInterpreter> _interp;
38+
public:
39+
LorelineSelectCallable(const Ref<LorelineInterpreter> &interp) : _interp(interp) {}
40+
41+
uint32_t hash() const override { return _interp->get_instance_id(); }
42+
String get_as_text() const override { return "LorelineInterpreter::select()"; }
43+
ObjectID get_object() const override { return ObjectID(_interp->get_instance_id()); }
44+
45+
static bool compare_equal(const CallableCustom *a, const CallableCustom *b) {
46+
return a == b;
47+
}
48+
static bool compare_less(const CallableCustom *a, const CallableCustom *b) {
49+
return a < b;
50+
}
51+
CompareEqualFunc get_compare_equal_func() const override { return compare_equal; }
52+
CompareLessFunc get_compare_less_func() const override { return compare_less; }
53+
54+
void call(const Variant **p_arguments, int p_argcount, Variant &r_return_value, GDExtensionCallError &r_call_error) const override {
55+
int index = 0;
56+
if (p_argcount > 0 && p_arguments[0]->get_type() == Variant::INT) {
57+
index = *p_arguments[0];
58+
}
59+
_interp->select(index);
60+
r_call_error.error = GDEXTENSION_CALL_OK;
61+
}
62+
};
63+
764
#ifdef LORELINE_USE_JS
865
#include <godot_cpp/classes/java_script_bridge.hpp>
966
#include <godot_cpp/classes/json.hpp>
@@ -19,9 +76,14 @@ LorelineInterpreter::LorelineInterpreter()
1976
: _interp(nullptr), _pending_advance(nullptr), _pending_select(nullptr)
2077
#endif
2178
{
79+
UtilityFunctions::print("[Loreline] Interpreter created: ", Variant((uint64_t)this));
2280
}
2381

2482
LorelineInterpreter::~LorelineInterpreter() {
83+
UtilityFunctions::print("[Loreline] Interpreter destroyed: ", Variant((uint64_t)this));
84+
// Remove from active list if still there (user dropped ref before finish)
85+
Loreline::_release_active_interpreter(this);
86+
2587
#ifdef LORELINE_USE_JS
2688
if (_js_id != 0) {
2789
_js_registry.erase(_js_id);
@@ -182,7 +244,11 @@ void LorelineInterpreter::_on_dialogue(
182244
String godot_text = text.isNull() ? String() : String::utf8(text.c_str());
183245
Array godot_tags = _convert_tags(tags, tagCount);
184246

185-
Callable advance_callable = Callable(self, "advance");
247+
// Release from active list — the Callable now holds the Ref keeping the interpreter alive
248+
Loreline::_release_active_interpreter(self);
249+
250+
Ref<LorelineInterpreter> self_ref = self->_self_ref;
251+
Callable advance_callable(memnew(LorelineAdvanceCallable(self_ref)));
186252
self->emit_signal("dialogue", self, godot_character, godot_text, godot_tags, advance_callable);
187253
}
188254

@@ -198,7 +264,11 @@ void LorelineInterpreter::_on_choice(
198264

199265
Array godot_options = _convert_options(options, optionCount);
200266

201-
Callable select_callable = Callable(self, "select");
267+
// Release from active list — the Callable now holds the Ref keeping the interpreter alive
268+
Loreline::_release_active_interpreter(self);
269+
270+
Ref<LorelineInterpreter> self_ref = self->_self_ref;
271+
Callable select_callable(memnew(LorelineSelectCallable(self_ref)));
202272
self->emit_signal("choice", self, godot_options, select_callable);
203273
}
204274

@@ -209,6 +279,13 @@ void LorelineInterpreter::_on_finish(
209279
self->_pending_advance = nullptr;
210280
self->_pending_select = nullptr;
211281

282+
UtilityFunctions::print("[Loreline] Script finished, cleaning up interpreter: ", Variant((uint64_t)self));
283+
284+
// Keep alive through signal emission via _self_ref, then clean up
285+
Loreline::_release_active_interpreter(self);
286+
Variant guard = self->_self_ref;
287+
self->_self_ref = Variant();
288+
212289
self->emit_signal("finished", self);
213290
}
214291

godot/src/loreline_interpreter.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ class LorelineInterpreter : public RefCounted {
3535
static void _poll_js_events();
3636
#else
3737
Loreline_Interpreter *_interp;
38+
Variant _self_ref; // Stores Ref<LorelineInterpreter> as Variant to keep self alive
3839
void (*_pending_advance)(void);
3940
void (*_pending_select)(int);
4041

godot/src/loreline_runtime.cpp

Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,32 @@ Loreline *Loreline::shared() {
2525
_singleton = memnew(Loreline);
2626
_singleton->set_name("Loreline");
2727

28-
// Add to scene tree root so the node gets READY/PROCESS notifications
29-
// and survives scene changes
28+
// Add to scene tree root so the node gets PROCESS notifications
29+
// and survives scene changes. Must be immediate (not deferred)
30+
// so the singleton is usable right away from GDScript _ready().
3031
SceneTree *tree = Object::cast_to<SceneTree>(Engine::get_singleton()->get_main_loop());
3132
if (tree && tree->get_root()) {
32-
tree->get_root()->call_deferred("add_child", _singleton);
33+
tree->get_root()->add_child(_singleton);
34+
}
35+
36+
// Initialize the runtime immediately
37+
if (!_singleton->_initialized) {
38+
#ifdef LORELINE_USE_JS
39+
JavaScriptBridge *js = JavaScriptBridge::get_singleton();
40+
if (js) {
41+
js->eval(String::utf8(LORELINE_JS_BUNDLE), true);
42+
js->eval(String::utf8(LORELINE_JS_BRIDGE), true);
43+
_singleton->_js_loaded = true;
44+
}
45+
#else
46+
#ifdef ANDROID_ENABLED
47+
Loreline_createThread();
48+
#endif
49+
Loreline_init();
50+
Loreline_update(0);
51+
#endif
52+
_singleton->_initialized = true;
53+
_singleton->set_process(true);
3354
}
3455

3556
return _singleton;
@@ -63,18 +84,15 @@ void Loreline::_bind_methods() {
6384
void Loreline::_notification(int p_what) {
6485
switch (p_what) {
6586
case NOTIFICATION_READY: {
87+
// Already initialized by shared() — nothing to do
88+
if (_initialized) break;
89+
6690
if (_singleton && _singleton != this) {
6791
UtilityFunctions::push_warning("Loreline: shared instance already exists, this node will be ignored.");
6892
return;
6993
}
7094
_singleton = this;
7195

72-
// If this node was placed in a scene (not via shared()), reparent
73-
// to root viewport so it survives scene changes
74-
if (get_parent() != get_tree()->get_root()) {
75-
call_deferred("reparent", get_tree()->get_root());
76-
}
77-
7896
#ifdef LORELINE_USE_JS
7997
if (!_js_loaded) {
8098
JavaScriptBridge *js = JavaScriptBridge::get_singleton();
@@ -403,6 +421,7 @@ Ref<LorelineInterpreter> Loreline::play(const Ref<LorelineScript> &script, const
403421
}
404422
Ref<LorelineInterpreter> interp = script->play(beat_name, options);
405423
if (interp.is_valid()) {
424+
_retain_interpreter(interp);
406425
if (on_dialogue.is_valid()) interp->connect("dialogue", on_dialogue);
407426
if (on_choice.is_valid()) interp->connect("choice", on_choice);
408427
if (on_finished.is_valid()) interp->connect("finished", on_finished);
@@ -421,9 +440,31 @@ Ref<LorelineInterpreter> Loreline::resume(const Ref<LorelineScript> &script, con
421440
}
422441
Ref<LorelineInterpreter> interp = script->resume(save_data, beat_name, options);
423442
if (interp.is_valid()) {
443+
_retain_interpreter(interp);
424444
if (on_dialogue.is_valid()) interp->connect("dialogue", on_dialogue);
425445
if (on_choice.is_valid()) interp->connect("choice", on_choice);
426446
if (on_finished.is_valid()) interp->connect("finished", on_finished);
427447
}
428448
return interp;
429449
}
450+
451+
void Loreline::_retain_interpreter(const Ref<LorelineInterpreter> &interp) {
452+
_active_interpreters.push_back(interp);
453+
UtilityFunctions::print("[Loreline] Retained interpreter (active count: ", _active_interpreters.size(), ")");
454+
}
455+
456+
void Loreline::_release_interpreter(LorelineInterpreter *interp) {
457+
for (int i = 0; i < _active_interpreters.size(); i++) {
458+
if (_active_interpreters[i].ptr() == interp) {
459+
_active_interpreters.remove_at(i);
460+
UtilityFunctions::print("[Loreline] Released interpreter (active count: ", _active_interpreters.size(), ")");
461+
return;
462+
}
463+
}
464+
}
465+
466+
void Loreline::_release_active_interpreter(LorelineInterpreter *interp) {
467+
if (_singleton) {
468+
_singleton->_release_interpreter(interp);
469+
}
470+
}

godot/src/loreline_runtime.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ class Loreline : public Node {
2121
private:
2222
static Loreline *_singleton;
2323
bool _initialized;
24+
Vector<Ref<LorelineInterpreter>> _active_interpreters;
2425

2526
#ifdef LORELINE_USE_JS
2627
bool _js_loaded;
@@ -58,4 +59,8 @@ class Loreline : public Node {
5859

5960
Ref<LorelineInterpreter> play(const Ref<LorelineScript> &script, const Callable &on_dialogue = Callable(), const Callable &on_choice = Callable(), const Callable &on_finished = Callable(), const String &beat_name = "", const Ref<LorelineOptions> &options = Ref<LorelineOptions>());
6061
Ref<LorelineInterpreter> resume(const Ref<LorelineScript> &script, const Callable &on_dialogue, const Callable &on_choice, const Callable &on_finished, const String &save_data, const String &beat_name = "", const Ref<LorelineOptions> &options = Ref<LorelineOptions>());
62+
63+
void _retain_interpreter(const Ref<LorelineInterpreter> &interp);
64+
void _release_interpreter(LorelineInterpreter *interp);
65+
static void _release_active_interpreter(LorelineInterpreter *interp);
6166
};

godot/src/loreline_script.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,10 @@ Ref<LorelineInterpreter> LorelineScript::play(const String &beat_name, const Ref
9999
native_opts = options->build_native_options();
100100
}
101101

102+
// Set _self_ref BEFORE Loreline_play — the first callback fires synchronously
103+
// and needs _self_ref to create the advance/select Callable.
104+
interp->_self_ref = Variant(interp);
105+
102106
interp->_interp = Loreline_play(
103107
_script,
104108
LorelineInterpreter::_on_dialogue,
@@ -113,6 +117,7 @@ Ref<LorelineInterpreter> LorelineScript::play(const String &beat_name, const Ref
113117
}
114118

115119
if (!interp->_interp) {
120+
interp->_self_ref = Variant();
116121
return Ref<LorelineInterpreter>();
117122
}
118123

@@ -180,6 +185,8 @@ Ref<LorelineInterpreter> LorelineScript::resume(const String &save_data, const S
180185
native_opts = options->build_native_options();
181186
}
182187

188+
interp->_self_ref = Variant(interp);
189+
183190
interp->_interp = Loreline_resume(
184191
_script,
185192
LorelineInterpreter::_on_dialogue,
@@ -195,6 +202,7 @@ Ref<LorelineInterpreter> LorelineScript::resume(const String &save_data, const S
195202
}
196203

197204
if (!interp->_interp) {
205+
interp->_self_ref = Variant();
198206
return Ref<LorelineInterpreter>();
199207
}
200208

sample/loreline-godot/addons/loreline/loreline.gdextension

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,12 @@ linux.release.arm64 = "res://addons/loreline/bin/linux/libloreline_godot.arm64.s
1616
; Mobile
1717
android.debug.arm64 = "res://addons/loreline/bin/android/arm64-v8a/libloreline_godot.so"
1818
android.release.arm64 = "res://addons/loreline/bin/android/arm64-v8a/libloreline_godot.so"
19+
android.debug.arm32 = "res://addons/loreline/bin/android/armeabi-v7a/libloreline_godot.so"
20+
android.release.arm32 = "res://addons/loreline/bin/android/armeabi-v7a/libloreline_godot.so"
1921
android.debug.x86_64 = "res://addons/loreline/bin/android/x86_64/libloreline_godot.so"
2022
android.release.x86_64 = "res://addons/loreline/bin/android/x86_64/libloreline_godot.so"
23+
android.debug.x86_32 = "res://addons/loreline/bin/android/x86/libloreline_godot.so"
24+
android.release.x86_32 = "res://addons/loreline/bin/android/x86/libloreline_godot.so"
2125
ios.debug = "res://addons/loreline/bin/ios/loreline_godot.xcframework"
2226
ios.release = "res://addons/loreline/bin/ios/loreline_godot.xcframework"
2327

@@ -34,15 +38,19 @@ macos.debug = { "res://addons/loreline/bin/macos/libLoreline.dylib": "" }
3438
macos.release = { "res://addons/loreline/bin/macos/libLoreline.dylib": "" }
3539
windows.debug.x86_64 = { "res://addons/loreline/bin/windows/Loreline.dll": "" }
3640
windows.release.x86_64 = { "res://addons/loreline/bin/windows/Loreline.dll": "" }
37-
linux.debug.x86_64 = { "res://addons/loreline/bin/linux/libLoreline.so": "" }
38-
linux.release.x86_64 = { "res://addons/loreline/bin/linux/libLoreline.so": "" }
39-
linux.debug.arm64 = { "res://addons/loreline/bin/linux/libLoreline.so": "" }
40-
linux.release.arm64 = { "res://addons/loreline/bin/linux/libLoreline.so": "" }
41+
linux.debug.x86_64 = { "res://addons/loreline/bin/linux/libLoreline.x86_64.so": "" }
42+
linux.release.x86_64 = { "res://addons/loreline/bin/linux/libLoreline.x86_64.so": "" }
43+
linux.debug.arm64 = { "res://addons/loreline/bin/linux/libLoreline.arm64.so": "" }
44+
linux.release.arm64 = { "res://addons/loreline/bin/linux/libLoreline.arm64.so": "" }
4145
; Mobile
4246
android.debug.arm64 = { "res://addons/loreline/bin/android/arm64-v8a/libloreline.so": "" }
4347
android.release.arm64 = { "res://addons/loreline/bin/android/arm64-v8a/libloreline.so": "" }
48+
android.debug.arm32 = { "res://addons/loreline/bin/android/armeabi-v7a/libloreline.so": "" }
49+
android.release.arm32 = { "res://addons/loreline/bin/android/armeabi-v7a/libloreline.so": "" }
4450
android.debug.x86_64 = { "res://addons/loreline/bin/android/x86_64/libloreline.so": "" }
4551
android.release.x86_64 = { "res://addons/loreline/bin/android/x86_64/libloreline.so": "" }
52+
android.debug.x86_32 = { "res://addons/loreline/bin/android/x86/libloreline.so": "" }
53+
android.release.x86_32 = { "res://addons/loreline/bin/android/x86/libloreline.so": "" }
4654
; iOS: Loreline.xcframework dynamic framework alongside the GDExtension
4755
ios.debug = { "res://addons/loreline/bin/ios/Loreline.xcframework": "" }
4856
ios.release = { "res://addons/loreline/bin/ios/Loreline.xcframework": "" }

sample/loreline-godot/scripts/loreline_manager.gd

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
extends Control
22

3-
# Node references
3+
# Loreline runtime
44
var loreline: Loreline = Loreline.shared()
5+
6+
# Node references
57
@onready var scroll_container: ScrollContainer = $ScrollContainer
68
@onready var output_table: HBoxContainer = $ScrollContainer/MarginContainer/OutputTable
79
@onready var content_column: VBoxContainer = $ScrollContainer/MarginContainer/OutputTable/ContentColumn
@@ -153,7 +155,7 @@ func _on_dialogue(interp: LorelineInterpreter, character: String, text: String,
153155
advance.call()
154156

155157

156-
func _on_choice(interp: LorelineInterpreter, options: Array, select: Callable) -> void:
158+
func _on_choice(_interp: LorelineInterpreter, options: Array, select: Callable) -> void:
157159
# Delay before showing choices (matching Unity/web)
158160
await get_tree().create_timer(CHOICE_DELAY).timeout
159161

@@ -271,7 +273,7 @@ func _on_choice_selected(index: int, selected_btn: Button, container: VBoxContai
271273
select.call(index)
272274

273275

274-
func _on_finished(interp: LorelineInterpreter) -> void:
276+
func _on_finished(_interp: LorelineInterpreter) -> void:
275277
var spacer := Control.new()
276278
spacer.custom_minimum_size.y = SECTION_SPACING * 2
277279
_add_content(spacer)
@@ -363,9 +365,9 @@ func _gradient_bbcode(text: String) -> String:
363365
var t_min := 0.30
364366
var t_max := 0.70
365367
var result := ""
366-
var len := text.length()
367-
for i in range(len):
368-
var t: float = 0.4 if len <= 1 else t_min + (t_max - t_min) * float(i) / float(len - 1)
368+
var text_len := text.length()
369+
for i in range(text_len):
370+
var t: float = 0.4 if text_len <= 1 else t_min + (t_max - t_min) * float(i) / float(text_len - 1)
369371
var r: float; var g: float; var b: float
370372
if t <= 0.4:
371373
var s := t / 0.4

0 commit comments

Comments
 (0)