Skip to content

Commit 6edcc9e

Browse files
committed
Very crude (and half broken ^^) support for script class instance
1 parent ca59d51 commit 6edcc9e

File tree

9 files changed

+562
-53
lines changed

9 files changed

+562
-53
lines changed

src/godot/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@
3838
# )
3939
from .builtins import * # noqa: E402, F403
4040

41+
from ._lang import exposed # noqa: E402, F401
42+
4143
# from .classes import _load_singleton, _load_class
4244

4345

src/godot/_lang.pyx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@ from .classes cimport _load_class, _load_singleton, _cleanup_loaded_classes_and_
1111
# Extensions definition
1212
#
1313

14-
include "_lang_script_language.pxi"
15-
include "_lang_script.pxi"
1614
include "_lang_resource_format_loader.pxi"
1715
include "_lang_resource_format_saver.pxi"
16+
include "_lang_script_language.pxi"
17+
include "_lang_script.pxi"
18+
include "_lang_script_instance.pxi"
19+
include "_lang_tags.pxi"
1820

1921

2022
#

src/godot/_lang_resource_format_loader.pxi

Lines changed: 71 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
import importlib
2+
import traceback
3+
4+
15
cdef object RESOURCE_TYPE_NAME = "Python"
26
cdef object RESOURCE_EXTENSIONS = ("py", "pyc", "pyo", "pyd")
37

@@ -85,37 +89,81 @@ cdef class PythonResourceFormatLoader:
8589
# godot_extension: method(virtual=True, const=True)
8690
cdef gd_variant_t _load(self, gd_string_t path, gd_string_t original_path, gd_bool_t use_sub_threads, gd_int_t cache_mode):
8791
cdef gd_variant_t ret = gd_variant_new()
88-
cdef object py_path = gdapi.gd_string_to_pystr(&path)
89-
cdef object py_original_path = gdapi.gd_string_to_pystr(&original_path)
92+
cdef str py_path = gdapi.gd_string_to_pystr(&path)
93+
cdef str py_original_path = gdapi.gd_string_to_pystr(&original_path)
9094
gd_string_del(&path)
9195
gd_string_del(&original_path)
9296
spy_log(f"CALLED PythonResourceFormatLoader::_load(path={py_path!r}, original_path={py_original_path!r}, use_sub_threads={use_sub_threads}, cache_mode={cache_mode})")
9397

98+
# 1) Check path and convert it to Python format (e.g. `res://foo/bar.py` -> `foo.bar`)
99+
100+
if not (
101+
py_path.startswith("res://") and
102+
(
103+
py_path.endswith(".py") or
104+
py_path.endswith(".pyc") or
105+
py_path.endswith(".pyo") or
106+
py_path.endswith(".pyd")
107+
)
108+
):
109+
print(
110+
f"Bad python script path `{py_path}`, must starts by `res://` and ends with `.py/pyc/pyo/pyd`", flush=True
111+
)
112+
return gdapi.gd_int_into_variant(Error.ERR_FILE_BAD_PATH)
113+
114+
# TODO: possible bug if res:// is not part of PYTHONPATH
115+
# Remove `res://`, `.py` and replace / by .
116+
modname = py_path[len("res://"):].rsplit(".", 1)[0].replace("/", ".")
117+
118+
try:
119+
importlib.import_module(modname) # Force lazy loading of the module
120+
klass = _get_exposed_class(modname) # `_get_exposed_class` defined in `_lang_tags.pxi`
121+
print('=========> new Godot-Python class', klass)
122+
123+
except BaseException:
124+
# If we are here it could be because the file doesn't exists
125+
# or (more possibly) the file content is not valid python (or
126+
# doesn't provide an exposed class)
127+
print(
128+
f"Got exception loading `{py_path}` (aka `{modname}`): {traceback.format_exc()}", flush=True
129+
)
130+
return gdapi.gd_int_into_variant(Error.ERR_PARSE_ERROR)
131+
132+
if klass is None:
133+
print(
134+
f"Cannot load `{py_path}` (aka `{modname}`) because it doesn't expose any class to Godot", flush=True
135+
)
136+
return gdapi.gd_int_into_variant(Error.ERR_PARSE_ERROR)
137+
138+
139+
94140
cdef PythonScript script
95-
cdef gd_string_t gd_source
96-
cdef gd_string_t gd_script_path
97-
cdef GDString source_code
98-
99-
# Load the source code from file
100-
101-
from godot.classes import FileAccess
102-
# TODO: use `path` directly !
103-
cdef object file = FileAccess.open(GDString(py_path), FileAccess.ModeFlags.READ.value)
104-
if file is None:
105-
spy_log(f"Failed to load Python script {py_original_path}: cannot open file {path}")
106-
# If file loading fails, return the nil variant (already initialized)
107-
return ret
108-
# TODO: what happen if the text is not UTF8 ?
109-
source_code = file.get_as_text()
141+
# cdef gd_string_t gd_source
142+
# cdef gd_string_t gd_script_path
143+
# cdef GDString source_code
144+
145+
# # Load the source code from file
146+
147+
# from godot.classes import FileAccess
148+
# # TODO: use `path` directly !
149+
# cdef object file = FileAccess.open(GDString(py_path), FileAccess.ModeFlags.READ.value)
150+
# if file is None:
151+
# spy_log(f"Failed to load Python script {py_original_path}: cannot open file {path}")
152+
# # If file loading fails, return the nil variant (already initialized)
153+
# return ret
154+
# # TODO: what happen if the text is not UTF8 ?
155+
# source_code = file.get_as_text()
110156

111157
# Create a new script instance from the source code
112158

113-
script = PythonScript()
114-
script._set_source_code(source_code.into_gd_data())
115-
# `into_gd_data()` steal the underlying Godot string, so `source_code`
116-
# ends up containing nothing and we'd rather destroy it early to avoid
117-
# confusions.
118-
del source_code
159+
script = PythonScript() # `__cinit__()` initializes `script._gd_ptr`
160+
script._py_cls = klass
161+
script._script_instance_info = generate_instance_info()
162+
# script._set_source_code(source_code.into_gd_data())
163+
# # `into_gd_data()` steal the underlying Godot string, so `source_code`
164+
# # ends up containing nothing and we'd rather destroy it early to avoid
165+
# # confusions.
166+
# del source_code
119167

120168
# Return the script as a variant
121169

src/godot/_lang_script.pxi

Lines changed: 32 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,23 @@
44
# create an instance of this script)
55

66

7+
from godot.hazmat cimport gdptrs
8+
9+
710
# godot_extension: class(parent="ScriptExtension")
811
@cython.final
912
cdef class PythonScript:
1013
# Godot "sees" us through this object
1114
cdef gd_object_t _gd_ptr
15+
# The Python class that *is* the script
16+
cdef object _py_cls
17+
cdef GDExtensionScriptInstanceInfo3 _script_instance_info
1218

1319
# godot_extension: method(virtual=True, const=True)
1420
cdef gd_bool_t _can_instantiate(self):
1521
spy_log("CALLED PythonScript::_can_instantiate()")
16-
return self._is_valid()
22+
return True
23+
# return self._is_valid()
1724

1825
# godot_extension: method(virtual=True)
1926
cdef gd_bool_t _editor_can_reload_from_file(self):
@@ -193,11 +200,18 @@ cdef class PythonScript:
193200
# godot_extension: method(virtual=True, const=True)
194201
cdef void* _instance_create(self, gd_object_t for_object):
195202
spy_log(f"CALLED PythonScript::_instance_create(for_object=<object 0x{<size_t>for_object:x}>)")
196-
# For now, return NULL as we don't have full instance support yet
197-
# This would need to create a Python script instance that can
198-
# execute the script code and handle Godot callbacks
199-
# `gd_object_t` doesn't need to be be deleted (is it just a raw pointer)
200-
return NULL
203+
204+
# Call to __new__ bypasses __init__ constructor
205+
cdef BaseGDObject py_instance = self._py_cls.__new__(self._py_cls)
206+
py_instance._gd_ptr = for_object
207+
# TODO: call `__init__` ? or a init hook ?
208+
Py_INCREF(py_instance)
209+
cdef GDExtensionScriptInstancePtr script_instance = gdptrs.gdptr_script_instance_create3(
210+
&self._script_instance_info,
211+
<void*>py_instance,
212+
)
213+
spy_log(f"CALLED PythonScript::_instance_create(for_object=<object 0x{<size_t>for_object:x}>) -> <object 0x{<size_t>script_instance:x}>")
214+
return script_instance
201215

202216
# godot_extension: method(virtual=True, const=True)
203217
cdef gd_bool_t _instance_has(self, gd_object_t object):
@@ -216,29 +230,23 @@ cdef class PythonScript:
216230
# godot_extension: method(virtual=True, const=True)
217231
cdef gd_bool_t _is_tool(self):
218232
spy_log("CALLED PythonScript::_is_tool()")
219-
# Check if the script contains @tool decorator or similar
220-
if not self._source_code:
233+
if self._py_cls is not None:
234+
return self._py_cls._tool
235+
else:
221236
return False
222-
try:
223-
lines = self._source_code.split('\n')
224-
for line in lines:
225-
if line.strip().startswith('@tool') or '# tool' in line.lower():
226-
return True
227-
except:
228-
pass
229-
return False
230237

231238
# godot_extension: method(virtual=True, const=True)
232239
cdef gd_bool_t _is_valid(self):
233240
spy_log("CALLED PythonScript::_is_valid()")
234-
# A script is valid if it has source code and can be compiled
235-
if not self._source_code:
236-
return False
237-
try:
238-
compile(self._source_code, '<string>', 'exec')
239-
return True
240-
except:
241-
return False
241+
return True
242+
# # A script is valid if it has source code and can be compiled
243+
# if not self._source_code:
244+
# return False
245+
# try:
246+
# compile(self._source_code, '<string>', 'exec')
247+
# return True
248+
# except:
249+
# return False
242250

243251
# godot_extension: method(virtual=True)
244252
cdef void _placeholder_erased(self, void* placeholder):

0 commit comments

Comments
 (0)