Skip to content

Commit b1069c1

Browse files
authored
Merge pull request #52 from gilzoide/feature/godot-package-searcher
Add support for local paths in package searchers, `loadfile` and `dofile`
2 parents 9f0535a + ed05438 commit b1069c1

27 files changed

+446
-62
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,15 @@
33
### Added
44
- Editor plugin that registers the Lua REPL tab, where you can try Lua code using an empty `LuaState`
55
- Support for calling Godot String methods using Lua strings
6+
- Optional support for `res://` and `user://` relative paths in package searchers, `loadfile` and `dofile`.
7+
Open the `GODOT_LOCAL_PATHS` library to activate this behavior.
8+
- `LuaState.LoadMode` enum for specifying the Lua load mode: text, binary or any
9+
- `LuaState.do_buffer` and `LuaState.load_buffer` for loading Lua code from possibly binary chunks
610

711
### Changed
812
- The GDExtension is now marked as reloadable
913
- Renamed `LuaCoroutine::LuaCoroutineStatus` to `LuaCoroutine::Status`
14+
- `LuaState.load_file` and `LuaState.do_file` now receive the load mode instead of buffer size
1015

1116
### Removed
1217
- `VariantType::has_static_method` internal method

SConstruct

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@ env.Command(
1313
[
1414
"src/generated/global_enums.hpp",
1515
"src/generated/utility_functions.hpp",
16+
"src/generated/package_searcher.h",
1617
],
1718
[
1819
"src/generate_code.py",
20+
"src/luaopen/package_searcher.lua",
1921
"lib/godot-cpp/gdextension/extension_api.json",
2022
"lib/godot-cpp/gen/include/godot_cpp/variant/utility_functions.hpp",
2123
],

src/LuaState.cpp

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -124,27 +124,38 @@ void LuaState::open_libraries(BitField<Library> libraries) {
124124
if (libraries.has_flag(GODOT_ENUMS)) {
125125
lua_state.require(module_names::enums, &luaopen_godot_enums, false);
126126
}
127+
if (libraries.has_flag(GODOT_LOCAL_PATHS)) {
128+
lua_state.require(module_names::local_paths, &luaopen_godot_local_paths, false);
129+
}
127130
}
128131
}
129132

130133
Ref<LuaTable> LuaState::create_table(const Dictionary& initial_values) {
131134
return memnew(LuaTable(to_table(lua_state, initial_values)));
132135
}
133136

137+
Variant LuaState::load_buffer(const PackedByteArray& chunk, const String& chunkname, LoadMode mode, LuaTable *env) {
138+
return ::luagdextension::load_buffer(lua_state, chunk, chunkname, (sol::load_mode) mode, env);
139+
}
140+
134141
Variant LuaState::load_string(const String& chunk, const String& chunkname, LuaTable *env) {
135-
return ::luagdextension::load_string(lua_state, chunk, chunkname, env);
142+
return ::luagdextension::load_buffer(lua_state, chunk.to_utf8_buffer(), chunkname, sol::load_mode::text, env);
136143
}
137144

138-
Variant LuaState::load_file(const String& filename, int buffer_size, LuaTable *env) {
139-
return ::luagdextension::load_file(lua_state, filename, buffer_size, env);
145+
Variant LuaState::load_file(const String& filename, LoadMode mode, LuaTable *env) {
146+
return ::luagdextension::load_file(lua_state, filename, (sol::load_mode) mode, env);
147+
}
148+
149+
Variant LuaState::do_buffer(const PackedByteArray& chunk, const String& chunkname, LoadMode mode, LuaTable *env) {
150+
return ::luagdextension::do_buffer(lua_state, chunk, chunkname, (sol::load_mode) mode, env);
140151
}
141152

142153
Variant LuaState::do_string(const String& chunk, const String& chunkname, LuaTable *env) {
143-
return ::luagdextension::do_string(lua_state, chunk, chunkname, env);
154+
return ::luagdextension::do_buffer(lua_state, chunk.to_utf8_buffer(), chunkname, sol::load_mode::text, env);
144155
}
145156

146-
Variant LuaState::do_file(const String& filename, int buffer_size, LuaTable *env) {
147-
return ::luagdextension::do_file(lua_state, filename, buffer_size, env);
157+
Variant LuaState::do_file(const String& filename, LoadMode mode, LuaTable *env) {
158+
return ::luagdextension::do_file(lua_state, filename, (sol::load_mode) mode, env);
148159
}
149160

150161
LuaTable *LuaState::get_globals() const {
@@ -187,17 +198,25 @@ void LuaState::_bind_methods() {
187198
BIND_BITFIELD_FLAG(GODOT_SINGLETONS);
188199
BIND_BITFIELD_FLAG(GODOT_CLASSES);
189200
BIND_BITFIELD_FLAG(GODOT_ENUMS);
201+
BIND_BITFIELD_FLAG(GODOT_LOCAL_PATHS);
190202
BIND_BITFIELD_FLAG(GODOT_ALL_LIBS);
191203

192204
BIND_BITFIELD_FLAG(ALL_LIBS);
193205

206+
// LoadMode enum
207+
BIND_ENUM_CONSTANT(LOAD_MODE_ANY);
208+
BIND_ENUM_CONSTANT(LOAD_MODE_TEXT);
209+
BIND_ENUM_CONSTANT(LOAD_MODE_BINARY);
210+
194211
// Methods
195212
ClassDB::bind_method(D_METHOD("open_libraries", "libraries"), &LuaState::open_libraries, DEFVAL(BitField<Library>(ALL_LIBS)));
196213
ClassDB::bind_method(D_METHOD("create_table", "initial_values"), &LuaState::create_table, DEFVAL(Dictionary()));
214+
ClassDB::bind_method(D_METHOD("load_buffer", "chunk", "chunkname", "mode", "env"), &LuaState::load_buffer, DEFVAL(""), DEFVAL(LOAD_MODE_ANY), DEFVAL(nullptr));
197215
ClassDB::bind_method(D_METHOD("load_string", "chunk", "chunkname", "env"), &LuaState::load_string, DEFVAL(""), DEFVAL(nullptr));
198-
ClassDB::bind_method(D_METHOD("load_file", "filename", "buffer_size", "env"), &LuaState::load_file, DEFVAL(1024), DEFVAL(nullptr));
216+
ClassDB::bind_method(D_METHOD("load_file", "filename", "mode", "env"), &LuaState::load_file, DEFVAL(LOAD_MODE_ANY), DEFVAL(nullptr));
217+
ClassDB::bind_method(D_METHOD("do_buffer", "chunk", "chunkname", "mode", "env"), &LuaState::do_buffer, DEFVAL(""), DEFVAL(LOAD_MODE_ANY), DEFVAL(nullptr));
199218
ClassDB::bind_method(D_METHOD("do_string", "chunk", "chunkname", "env"), &LuaState::do_string, DEFVAL(""), DEFVAL(nullptr));
200-
ClassDB::bind_method(D_METHOD("do_file", "filename", "buffer_size", "env"), &LuaState::do_file, DEFVAL(1024), DEFVAL(nullptr));
219+
ClassDB::bind_method(D_METHOD("do_file", "filename", "mode", "env"), &LuaState::do_file, DEFVAL(LOAD_MODE_ANY), DEFVAL(nullptr));
201220
ClassDB::bind_method(D_METHOD("get_globals"), &LuaState::get_globals);
202221
ClassDB::bind_method(D_METHOD("get_registry"), &LuaState::get_registry);
203222

src/LuaState.hpp

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,18 +70,31 @@ class LuaState : public RefCounted {
7070
LUA_ALL_LIBS = LUA_BASE | LUA_PACKAGE | LUA_COROUTINE | LUA_STRING | LUA_OS | LUA_MATH | LUA_TABLE | LUA_DEBUG | LUA_BIT32 | LUA_IO | LUA_FFI | LUA_JIT | LUA_UTF8,
7171

7272
// ----- Godot ----
73+
// Variant types + String methods
7374
GODOT_VARIANT = 1 << 13,
75+
// utility functions, like "is_same" and "deg_to_rad". Also sets "print" to Godot's "printt"
7476
GODOT_UTILITY_FUNCTIONS = 1 << 14,
77+
// allows acessing singleton instances in _G
7578
GODOT_SINGLETONS = 1 << 15,
79+
// allows accessing classes by name in _G
7680
GODOT_CLASSES = 1 << 16,
81+
// global enums, like "KEY_A" and "PROPERTY_HINT_NONE"
7782
GODOT_ENUMS = 1 << 17,
83+
// add support for "res://" and "user://" paths in package searchers, "loadfile" and "dofile"
84+
GODOT_LOCAL_PATHS = 1 << 18,
7885

7986
// all of the above
80-
GODOT_ALL_LIBS = GODOT_VARIANT | GODOT_UTILITY_FUNCTIONS | GODOT_SINGLETONS | GODOT_CLASSES | GODOT_ENUMS,
87+
GODOT_ALL_LIBS = GODOT_VARIANT | GODOT_UTILITY_FUNCTIONS | GODOT_SINGLETONS | GODOT_CLASSES | GODOT_ENUMS | GODOT_LOCAL_PATHS,
8188

8289
ALL_LIBS = LUA_ALL_LIBS | GODOT_ALL_LIBS,
8390
};
8491

92+
enum LoadMode {
93+
LOAD_MODE_ANY = (int) sol::load_mode::any,
94+
LOAD_MODE_TEXT = (int) sol::load_mode::text,
95+
LOAD_MODE_BINARY = (int) sol::load_mode::binary,
96+
};
97+
8598
LuaState();
8699
~LuaState();
87100

@@ -90,10 +103,12 @@ class LuaState : public RefCounted {
90103
void open_libraries(BitField<Library> libraries = ALL_LIBS);
91104

92105
Ref<LuaTable> create_table(const Dictionary& initial_values = {});
106+
Variant load_buffer(const PackedByteArray& chunk, const String& chunkname = "", LoadMode mode = LOAD_MODE_ANY, LuaTable *env = nullptr);
93107
Variant load_string(const String& chunk, const String& chunkname = "", LuaTable *env = nullptr);
94-
Variant load_file(const String& filename, int buffer_size = 1024, LuaTable *env = nullptr);
108+
Variant load_file(const String& filename, LoadMode mode = LOAD_MODE_ANY, LuaTable *env = nullptr);
109+
Variant do_buffer(const PackedByteArray& chunk, const String& chunkname = "", LoadMode mode = LOAD_MODE_ANY, LuaTable *env = nullptr);
95110
Variant do_string(const String& chunk, const String& chunkname = "", LuaTable *env = nullptr);
96-
Variant do_file(const String& filename, int buffer_size = 1024, LuaTable *env = nullptr);
111+
Variant do_file(const String& filename, LoadMode mode = LOAD_MODE_ANY, LuaTable *env = nullptr);
97112

98113
LuaTable *get_globals() const;
99114
LuaTable *get_registry() const;
@@ -115,5 +130,6 @@ class LuaState : public RefCounted {
115130

116131
}
117132
VARIANT_BITFIELD_CAST(luagdextension::LuaState::Library);
133+
VARIANT_ENUM_CAST(luagdextension::LuaState::LoadMode);
118134

119135
#endif

src/generate_code.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
SRC_DIR = os.path.dirname(__file__)
55
DEST_DIR = os.path.join(SRC_DIR, "generated")
66
API_JSON_PATH = os.path.join(SRC_DIR, "..", "lib", "godot-cpp", "gdextension", "extension_api.json")
7+
PACKAGE_SEARCHER_SRC = os.path.join(SRC_DIR, "luaopen", "package_searcher.lua")
78
PRIMITIVE_VARIANTS = [
89
"bool",
910
"int",
@@ -18,7 +19,8 @@
1819

1920
def generate_utility_functions(utility_functions):
2021
lines = [
21-
"#undef register_utility_functions\n#define register_utility_functions(state)"
22+
"// This file was automatically generated by generate_code.py",
23+
"#undef register_utility_functions\n#define register_utility_functions(state)",
2224
]
2325
for f in utility_functions:
2426
name = f["name"]
@@ -39,7 +41,8 @@ def generate_utility_functions(utility_functions):
3941

4042
def generate_enums(global_enums):
4143
lines = [
42-
"#undef register_global_enums\n#define register_global_enums(state)"
44+
"// This file was automatically generated by generate_code.py",
45+
"#undef register_global_enums\n#define register_global_enums(state)",
4346
]
4447
for enum in global_enums:
4548
lines.append(f"\t/* {enum['name']} */")
@@ -48,6 +51,19 @@ def generate_enums(global_enums):
4851
return " \\\n".join(lines) + "\n"
4952

5053

54+
def generate_package_searcher():
55+
lines = [
56+
"// This file was automatically generated by generate_code.py",
57+
"const char package_searcher_lua[] = ",
58+
]
59+
with open(PACKAGE_SEARCHER_SRC, "r") as f:
60+
for line in f:
61+
line = line.replace("\\", "\\\\").replace('"', '\\"').rstrip("\r\n")
62+
lines.append('"' + line + '\\n"')
63+
lines.append(";")
64+
return "\n".join(lines)
65+
66+
5167
def main():
5268
with open(API_JSON_PATH) as f:
5369
api = json.load(f)
@@ -60,6 +76,10 @@ def main():
6076
with open(os.path.join(DEST_DIR, "global_enums.hpp"), "w") as f:
6177
code = generate_enums(api["global_enums"])
6278
f.write(code)
79+
80+
with open(os.path.join(DEST_DIR, "package_searcher.h"), "w") as f:
81+
code = generate_package_searcher()
82+
f.write(code)
6383

6484

6585
if __name__ == "__main__":

src/luaopen/enums.cpp

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,9 @@
2020
* SOFTWARE.
2121
*/
2222
#include "../generated/global_enums.hpp"
23-
#include "../utils/module_names.hpp"
2423

2524
#include <sol/sol.hpp>
2625

27-
using namespace luagdextension;
28-
2926
extern "C" int luaopen_godot_enums(lua_State *L) {
3027
sol::state_view state = L;
3128

src/luaopen/godot.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ extern "C" int luaopen_godot(lua_State *L) {
3636
state.require(module_names::singleton_access, &luaopen_godot_singleton_access, false);
3737
state.require(module_names::classes, &luaopen_godot_classes, false);
3838
state.require(module_names::enums, &luaopen_godot_enums, false);
39+
state.require(module_names::local_paths, &luaopen_godot_local_paths, false);
3940

4041
return 0;
4142
}

src/luaopen/godot.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ int luaopen_godot_utility_functions(lua_State *L);
3232
int luaopen_godot_singleton_access(lua_State *L);
3333
int luaopen_godot_classes(lua_State *L);
3434
int luaopen_godot_enums(lua_State *L);
35+
int luaopen_godot_local_paths(lua_State *L);
3536

3637
}
3738

src/luaopen/local_paths.cpp

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
/**
2+
* Copyright (C) 2025 Gil Barbosa Reis.
3+
*
4+
* Permission is hereby granted, free of charge, to any person obtaining a copy of
5+
* this software and associated documentation files (the “Software”), to deal in
6+
* the Software without restriction, including without limitation the rights to
7+
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
8+
* of the Software, and to permit persons to whom the Software is furnished to do
9+
* so, subject to the following conditions:
10+
*
11+
* The above copyright notice and this permission notice shall be included in all
12+
* copies or substantial portions of the Software.
13+
*
14+
* THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20+
* SOFTWARE.
21+
*/
22+
23+
#include "../LuaTable.hpp"
24+
#include "../utils/convert_godot_lua.hpp"
25+
#include "../utils/load_fileaccess.hpp"
26+
#include "sol/forward.hpp"
27+
#include "sol/load_result.hpp"
28+
29+
#include <godot_cpp/classes/engine.hpp>
30+
#include <godot_cpp/classes/file_access.hpp>
31+
#include <godot_cpp/classes/os.hpp>
32+
#include <godot_cpp/classes/project_settings.hpp>
33+
34+
#include "../generated/package_searcher.h"
35+
36+
using namespace luagdextension;
37+
38+
static int l_searchpath(lua_State *L) {
39+
String name = luaL_checkstring(L, 1);
40+
String path = luaL_checkstring(L, 2);
41+
String sep = luaL_optstring(L, 3, ".");
42+
String rep = luaL_optstring(L, 4, "/");
43+
if (!sep.is_empty()) {
44+
name = name.replace(sep, rep);
45+
}
46+
47+
String execdir_repl = Engine::get_singleton()->is_editor_hint()
48+
? ProjectSettings::get_singleton()->globalize_path("res://")
49+
: OS::get_singleton()->get_executable_path().get_base_dir();
50+
51+
PackedStringArray path_list = path.split(";", false);
52+
PackedStringArray not_found_list;
53+
for (const String& path_template : path_list) {
54+
String filename = path_template.replace("?", name).replace("!", execdir_repl);
55+
if (FileAccess::file_exists(filename)) {
56+
sol::stack::push(L, filename);
57+
return 1;
58+
}
59+
else {
60+
not_found_list.append(filename);
61+
}
62+
}
63+
64+
// path not found, return a formatted error message
65+
String error_message;
66+
for (const String& filename : not_found_list) {
67+
error_message += String("\n\tno file \"%s\"") % filename;
68+
}
69+
sol::stack::push(L, sol::nil);
70+
sol::stack::push(L, error_message);
71+
return 2;
72+
}
73+
74+
static int l_loadfile(lua_State *L) {
75+
sol::state_view state(L);
76+
String filename = luaL_optstring(L, 1, "");
77+
String mode = luaL_optstring(L, 2, "bt");
78+
Variant env = to_variant(sol::stack_object(L, 3));
79+
80+
sol::load_result result = load_fileaccess(state, filename, mode, Object::cast_to<LuaTable>(env));
81+
if (result.valid()) {
82+
result = sol::load_result(); // avoid popping result
83+
return 1;
84+
}
85+
else {
86+
lua_pushnil(L);
87+
lua_rotate(L, result.stack_index(), 1);
88+
result = sol::load_result(); // avoid popping error
89+
return 2;
90+
}
91+
}
92+
93+
static int l_dofile(lua_State *L) {
94+
sol::state_view state(L);
95+
String filename = luaL_optstring(L, 1, "");
96+
97+
int previous_top = lua_gettop(L);
98+
sol::load_result result = load_fileaccess(state, filename);
99+
if (result.valid()) {
100+
result = sol::load_result(); // avoid over-popping function
101+
lua_call(L, 0, LUA_MULTRET);
102+
return lua_gettop(L) - previous_top;
103+
}
104+
else {
105+
result = sol::load_result(); // avoid popping error
106+
return lua_error(L);
107+
}
108+
}
109+
110+
extern "C" int luaopen_godot_local_paths(lua_State *L) {
111+
sol::state_view state = L;
112+
113+
if (auto package = state.get<sol::optional<sol::table>>("package")) {
114+
(*package)["searchpath"] = l_searchpath;
115+
state.do_string(package_searcher_lua);
116+
}
117+
118+
state["loadfile"] = l_loadfile;
119+
state["dofile"] = l_dofile;
120+
121+
return 0;
122+
}
123+
124+

0 commit comments

Comments
 (0)