Skip to content

feat: lua script inheritance & feat: is_abstract#225

Open
illarn wants to merge 28 commits into
gilzoide:mainfrom
illarn:feat/script-inheritance
Open

feat: lua script inheritance & feat: is_abstract#225
illarn wants to merge 28 commits into
gilzoide:mainfrom
illarn:feat/script-inheritance

Conversation

@illarn
Copy link
Copy Markdown
Contributor

@illarn illarn commented Apr 13, 2026

Goal

Make it possible to inherit from lua classes (#164) and add abstract lua script property (since now with script inheritance it has effect)

Changes

@illarn illarn changed the title Feat: lua script inheritance Feat: lua script inheritance and is_abstract Apr 13, 2026
@illarn illarn changed the title Feat: lua script inheritance and is_abstract Feat: lua script inheritance & Feat: is_abstract Apr 13, 2026
@illarn
Copy link
Copy Markdown
Contributor Author

illarn commented Apr 14, 2026

@gilzoide hello! I'm in the making of this PR, seems almost done, I'll try finishing it this week. If you have free time feel free to check it and give feedback. I've tested it and all in-editor stuff seems to work (signal inheritance, node selection, method/property inheritance, exported property inheritance)

Btw I've short-sightedly branched it from my main instead of upstream, so it also shows changes from the other PR, sorry for the inconvenience

@illarn illarn marked this pull request as ready for review April 19, 2026 07:47
@illarn
Copy link
Copy Markdown
Contributor Author

illarn commented Apr 19, 2026

@gilzoide hello! I've finished this PR, added an inheritance.lua test, tested all the in-editor functionality, everything seems to work fine

Things that can be improved:

  • adding reload on parent source change. I tried to make it work with _set_source_code, but searching for children was painfully slow (we can store script inheritance tree to solve this, although maybe godot has some hook for this purpose, couldn't find it)
  • adding super. Idk if it's better to implement this as a keyword, method or smth like BaseClass.method(derived, ...), but I think it would be pretty handy
  • adding custom error message when trying to call :new() on an abstract script, right now it's just invalid method new

I think this is ready to merge as is (unless I missed smth), but if you feel that something else is required pls let me know

@illarn illarn changed the title Feat: lua script inheritance & Feat: is_abstract feat: lua script inheritance & feat: is_abstract Apr 20, 2026
Comment thread src/script-language/LuaScript.cpp
Comment thread src/script-language/LuaScript.cpp Outdated
}

StringName LuaScript::_get_instance_base_script_type() const {
return _get_base_script() != nullptr ? metadata.base_class : "";
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't look right. Shouldn't you return something from _get_base_script() in case _get_base_script() != nullptr?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't look right. Shouldn't you return something from _get_base_script() in case _get_base_script() != nullptr?

I thought leaving empty string as a false indicating that there's no base_script makes more sense since the method name specifies script_type. I can change it if you think returning base_type when script doesn't extend another script makes more sense

Comment thread src/script-language/LuaScript.cpp Outdated
Comment thread src/script-language/LuaScript.cpp Outdated
Comment thread src/script-language/LuaScript.cpp Outdated
if (script.is_valid() && script->_is_valid()) {
result["name"] = script->_get_global_name();
result["base_type"] = script->_get_instance_base_type();
result["base_type"] = script->_get_instance_base_script_type().is_empty() ? script->get_instance_base_type() : script->_get_instance_base_script_type();
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not stick to script->_get_instance_base_type() here, since the implementation already handles base scripts correctly?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not stick to script->_get_instance_base_type() here, since the implementation already handles base scripts correctly?

I don't think I understand the question. If I get base_type here in script_class_cache all the scripts would inherit from that type, not base script. Did you mean stick to _get_instance_base_script_type? If so then yes, if I don't return "" this would get much cleaner, I should do it that way

if (extends_script == nullptr) {
WARN_PRINT(String("Specified base class '%s' does not exist, using RefCounted") % Array::make(extends));
} else {
base_class = extends;
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not get the extends_script->get_metadata().base_class here instead and let LuaScript::_get_instance_base_type just return it? So that base_class always has the name of a valid native GDCLASS.
Remember that in this branch extends is the result of extends_script->to_string().

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't want to deal with cross inclusions at that moment and then forgot about it, so yeah, I should fix it

Comment thread src/script-language/LuaScriptMetadata.cpp
Comment on lines +35 to +45
Ref<Script> get_class_script(const String& class_name) {
Array global_classes = ProjectSettings::get_singleton()->get_global_class_list();
for (int i = 0; i < global_classes.size(); i++) {
Dictionary cls = global_classes[i];
if (String(cls["class"]) == class_name) {
return ResourceLoader::get_singleton()->load(cls["path"], "Script");
}
}

return nullptr;
}
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This implementation only accepts global classes declared with class_name. I think this feature is unfinished without the support for loading scripts by path. You should at least support paths relative to res:// and uid://, although supporting relative paths from the current script would be a nice touch, such as is supported by GDScript.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This implementation only accepts global classes declared with class_name. I think this feature is unfinished without the support for loading scripts by path. You should at least support paths relative to res:// and uid://, although supporting relative paths from the current script would be a nice touch, such as is supported by GDScript.

I don't really understand this. If I'm trying to get class's script how can it return a script which isn't declared with class_name? Or are you talking about some code which uses this function?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just googled and understood what you were talking about, didn't know that you can extend scripts by their path. I agree that this should be implemented, I'll think about how I can do it

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the inheritance test should be more comprehensive. You should at least test that overriding methods in the derived class supersedes methods from the base class. Also test that get_script_property_list, get_script_method_list and the like return members from both Base and Derived classes.

@illarn
Copy link
Copy Markdown
Contributor Author

illarn commented Apr 24, 2026

@gilzoide hello! I have fixed everything from the feedback (apart from this, didn't understand the comment). Most notably:

  1. inheritance from absolute/relative path works
  2. metadata base class now always contains base class name
  3. inheritance test expanded, added shadowing test, abs/rel derived test, base test, editor callbacks test (get methods/property/signal list)

I haven't moved get_property from LuaScript for reasons explained in the reply, but if you still think it's not fit there pls explain how should it be realized

@illarn
Copy link
Copy Markdown
Contributor Author

illarn commented Apr 24, 2026

also for some reason with luajit accessing godot Array after retrieving a callable results in a crash with no error. Have no idea why this happens, but only happens on this branch and with luajit. that's why CI test is failing. I'll take a look at why this might happen, LuaScript::get_property is my suspect, but can't be sure, extremely weird case

local node = Control:new()

-- Comment any of the 2 lines or reverse order = no crash, but this exact setup = crash
local methods = node.queue_free
local a = #Array({})
-- terminate called without an active exception
-- make: *** [Makefile:51: test] Aborted (core dumped)

Copy link
Copy Markdown
Owner

@gilzoide gilzoide left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @illarn, thanks a lot for taking the time to update the code. Sorry it took me so long to review again, these last weeks have been busy here and I didn't want to rush this. There are still things that should be addressed, I've added comments in the relevant sections.

Again, thanks a lot for taking the time to implement this feature! Please let me know if there's any help I can further provide. I'll try to debug the crash with LuaJIT you mentioned, maybe I can find something that could help.

WARN_PRINT(String("Specified base class '%s' does not exist, using RefCounted") % Array::make(extends));
} else {
base_class = extends;
base_class = static_cast<Ref<LuaScript>>(extends_script)->get_metadata().base_class;
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't this crash in case extends_script is a Script but not a LuaScript?
If base scripts for Lua should always be a LuaScript and not just a plain Script (such as an instance of GDScript), make explicit checks and add error messages if necessary.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you're right, I can just get_instance_base_type() on base script and it will work with any script, fixed

Comment thread src/script-language/LuaScript.cpp Outdated
if (Ref<LuaScript> base = get_base_script(); base != nullptr)
if (const Variant property = base->_get_property_default_value(p_property))
return property;
return property;
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should still call base->_get_property_default_value(p_property) here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

Comment thread src/script-language/LuaScript.cpp
Comment thread src/script-language/LuaScript.cpp
@gilzoide
Copy link
Copy Markdown
Owner

gilzoide commented May 2, 2026

Hey @illarn, I found what's causing the crash: the Variant.__len metamethod implementation is using sol::variadic_args wrong, which will at least cause an error to be printed and at most crash the app. I just fixed it in #227, you can merge tip of branch main into yours.

@illarn
Copy link
Copy Markdown
Contributor Author

illarn commented May 3, 2026

@gilzoide Hello! Thank you for the feedback, I've returned get_func implementation to LuaScriptProperty as I understood it from your comment (can't reply to it for some reason) and fixed all the other problems. The luajit crash has also been fixed with #227, so everything should be ready for merging

Copy link
Copy Markdown
Owner

@gilzoide gilzoide left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @illarn, thanks a lot for the updates, and sorry again for taking so long to review. I think we are really close now!

Apart from the inline comments, another thing I'd like to comment overall is about LuaScriptMetadata::base_script being a Ref<Script>. If pretty much every feature in LuaScript that traverses the base script chain casts the base script to Ref<LuaScript>, shouldn't we force that Lua scripts only extend other Lua scripts? Both GDScript and CSharpScript support only themselves as base and Godot doesn't traverse the script chain in general, it's a responsibility for the script / script instance.

All that said, I'd say we should either:

  1. Force that Lua scripts extend only Lua scripts (or native classes, of course)
  2. Add real support for extending scripts of other languages

The simplest is option 1, the official script languages already do this, so I don't think we need to bother trying to support the general case. But if you want to try option 2, it's up to you.

Comment on lines +86 to +93
Ref<Script> current = script;
while (current.is_valid()) {
if (current == metadata.base_script)
return true;
current = current->get_base_script();
}

return false;
Copy link
Copy Markdown
Owner

@gilzoide gilzoide May 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This implementation is incorrect, we should iterate over the base script chain for this instead of script's. As a reference, take a look at GDScript::inherits_script.

const LuaScriptMetadata metadata = p_instance->script->get_metadata();

// a) try calling `_get`
if (const LuaScriptMethod *_get = metadata.methods.getptr(string_names->_get)) {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should use LuaScript::get_method here, in case the _get method is defined in a super class.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants