Skip to content

Commit 9fa2bcd

Browse files
committed
Updated Python extension:
- Fixed memory allocation wrapper - Fix some error handling, particularly for the command Python.Exec and initial code loading - Disable os module and other system-level libraries - Improve Python's print wrapper - Disable pocketpy debugging for profile builds - Add orx.close for sending a game/engine exit signal - Clean up Python extension template section - Improve Python init project code - Add miniscroll module, a Scroll-like convenience layer for orx games in Python
1 parent 97158eb commit 9fa2bcd

File tree

9 files changed

+349
-39
lines changed

9 files changed

+349
-39
lines changed

code/build/template/build/premake4.lua

+7-1
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,12 @@ solution "[name]"
105105
"Symbols",
106106
"StaticRuntime"
107107
}
108+
[+python
109+
110+
defines
111+
{
112+
"PK_ENABLE_OS=0"
113+
}]
108114

109115
configuration {"not xcode*"}
110116
includedirs {"$(ORX)/include"}
@@ -127,7 +133,7 @@ solution "[name]"
127133

128134
configuration {"*Profile*"}
129135
targetsuffix ("p")
130-
defines {"__orxPROFILER__"}
136+
defines {"__orxPROFILER__"[+python ,"NDEBUG"]}
131137
flags {"Optimize", "NoRTTI"}
132138
links {"orxp"}
133139

Original file line numberDiff line numberDiff line change
@@ -1,8 +1,22 @@
1-
import orx
1+
import miniscroll, orx
2+
3+
class Logo(miniscroll.Mini):
4+
def on_update(self, dt: float):
5+
if orx.input.has_been_activated("Reverse"):
6+
self.o.set_angular_velocity(self.o.get_angular_velocity() * -1)
27

38
def init():
9+
miniscroll.classes = {
10+
"Logo": Logo
11+
}
12+
413
# Create the [-movie scene][+movie splash screen]
514
orx.object.create_from_config("[-movie Scene][+movie Splash]");
615

7-
# Setup engine callbacks
8-
orx.on_init = init
16+
def update(dt: float):
17+
# Check to see if we should quit the game
18+
if orx.input.has_been_activated("Quit"):
19+
orx.close()
20+
21+
# Setup engine callbacks for miniscroll
22+
miniscroll.setup(init, update)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
# pyright: strict
2+
3+
from dataclasses import dataclass
4+
from typing import Callable, Self
5+
6+
import orx
7+
8+
@dataclass
9+
class Collision:
10+
body_part_name: str
11+
collider: orx.object.Object
12+
collider_body_part_name: str
13+
position: orx.vector.Vector
14+
normal: orx.vector.Vector
15+
16+
@dataclass
17+
class Separation:
18+
body_part_name: str
19+
collider: orx.object.Object
20+
collider_body_part_name: str
21+
22+
class Context:
23+
def __init__(self, section: str, input_set: str | None = None):
24+
self.config_section = section
25+
self.input_set = input_set
26+
27+
def __enter__(self) -> None:
28+
orx.config.push_section(self.config_section)
29+
if self.input_set:
30+
orx.input.push_set(self.input_set)
31+
32+
def __exit__(self, *_: None) -> None:
33+
orx.config.pop_section()
34+
if self.input_set:
35+
orx.input.pop_set()
36+
37+
class Mini:
38+
o: orx.object.Object
39+
context: Context
40+
instance_context: Context
41+
input_names: list[str]
42+
_objects: dict[orx.object.Object, Self] = {}
43+
44+
def __init__(self, o: orx.object.Object):
45+
self.o = o
46+
config_section = self.o.get_name()
47+
48+
input_set_name = None
49+
self.input_names = []
50+
with orx.config.Section(config_section):
51+
# Initialize input
52+
if orx.config.has_value("Input"):
53+
input_set_name = orx.config.get_string("Input")
54+
orx.input.enable_set(input_set_name)
55+
with orx.input.Set(input_set_name):
56+
self.input_names = orx.input.get_all()
57+
self.context = Context(config_section, input_set_name)
58+
self.instance_context = Context(str(self.o), input_set_name)
59+
60+
# Object-specific on_create initialization
61+
with self.context:
62+
self.on_create()
63+
64+
# Remember the object
65+
type(self)._objects[o] = self
66+
Mini._objects[o] = self
67+
68+
@classmethod
69+
def __cleanup__(cls, o: orx.object.Object):
70+
"""Internal only: Remove object association with the class"""
71+
_ = cls._objects.pop(o, None)
72+
_ = Mini._objects.pop(o, None)
73+
74+
@classmethod
75+
def exists(cls, o: orx.object.Object) -> bool:
76+
"""Check if an object has an instance associated with this class"""
77+
return o in cls._objects
78+
79+
@classmethod
80+
def objects(cls):
81+
"""Generator function for all objects associated with this class"""
82+
for mini in cls._objects.values():
83+
yield mini
84+
85+
@classmethod
86+
def create_from_config(cls, name: str) -> Self | None:
87+
"""Create a new object using the section `name`"""
88+
o = orx.object.create_from_config(name)
89+
if o is None:
90+
return None
91+
return cls(o)
92+
93+
@classmethod
94+
def from_object(cls, o: orx.object.Object) -> Self | None:
95+
"""Find the object from this class associated with `o` if one exists"""
96+
return cls._objects.get(o)
97+
98+
@classmethod
99+
def from_guid(cls, guid: int) -> Self | None:
100+
"""Find the object from this class with the GUID `guid` if one exists"""
101+
o = orx.object.from_guid(guid)
102+
if o is None:
103+
return None
104+
return cls.from_object(o)
105+
106+
def push_config_section(self, instance: bool = False):
107+
"""Push the config section associated with this object onto the stack"""
108+
section_name = str(self.o) if instance else self.o.get_name()
109+
orx.config.push_section(section_name)
110+
111+
def pop_config_section(self):
112+
"""Pop the latest config section from the stack"""
113+
orx.config.pop_section()
114+
115+
def on_create(self) -> None: ...
116+
"""Called on object creation"""
117+
118+
def on_delete(self) -> None: ...
119+
"""Called on object deletion"""
120+
121+
def on_update(self, dt: float): ...
122+
"""Called each clock update"""
123+
124+
def on_collide(self, collision: Collision): ...
125+
"""Called on collision with other Mini-derived objects"""
126+
127+
def on_separate(self, separation: Separation): ...
128+
"""Called on separation from collision with other Mini-derived objects"""
129+
130+
def on_shader_param(self, shader_name: str, param_name: str, param_type: type[float | orx.vector.Vector]) -> float | orx.vector.Vector : ...
131+
"""Called on shader parameter lookups for any shaders associated with the object"""
132+
133+
# Tracking class and object associations
134+
135+
classes: dict[str, type[Mini]] = {}
136+
"""
137+
Bindings from configuration section names (key) to Python classes matching
138+
those names
139+
"""
140+
141+
def _find_bound_class(o: orx.object.Object) -> type[Mini] | None:
142+
"""
143+
Find class bound to a name in object's config section hierarchy
144+
"""
145+
section_name = o.get_name()
146+
while section_name not in classes:
147+
parent = orx.config.get_parent(section_name)
148+
if parent is None:
149+
# No further config parent to check, so this object is not bound to a class
150+
return None
151+
section_name = parent
152+
# name exists as a key in classes
153+
return classes[section_name]
154+
155+
# Engine callbacks
156+
157+
def on_create(o: orx.object.Object):
158+
"""Engine object creation callback"""
159+
# Find a name in the object's hierarchy with a bound class
160+
bound = _find_bound_class(o)
161+
# Nothing to do if this object does not match a bound class
162+
if bound is None:
163+
return
164+
# Create a wrapper Python object if one not been created yet
165+
if not bound.exists(o):
166+
_ = bound(o)
167+
168+
def on_delete(o: orx.object.Object):
169+
"""Engine object deletion callback"""
170+
bound = _find_bound_class(o)
171+
if bound is None:
172+
return
173+
mini = bound.from_object(o)
174+
if mini is None:
175+
return
176+
bound.__cleanup__(o)
177+
with mini.context:
178+
mini.on_delete()
179+
180+
_per_frame_update: Callable[[float], None] | None = None
181+
182+
def on_update(dt: float):
183+
"""Engine clock update callback"""
184+
for mini in Mini.objects():
185+
with mini.context:
186+
# Input triggers
187+
for input_name in mini.input_names:
188+
if orx.input.has_been_activated(input_name):
189+
mini.o.fire_trigger("Input", refinement=[input_name])
190+
# Object clock
191+
clock = mini.o.get_clock()
192+
if clock is not None:
193+
# Scale dt based on object's clock
194+
dt = clock.compute_dt(dt)
195+
# Object update callback
196+
mini.on_update(dt)
197+
198+
if _per_frame_update is not None:
199+
_per_frame_update(dt)
200+
201+
def on_collide(o1: orx.object.Object, body_name1: str, o2: orx.object.Object, body_name2: str, position: orx.vector.Vector, normal: orx.vector.Vector):
202+
"""Engine callback for object collision events"""
203+
mini = Mini.from_object(o1)
204+
if mini:
205+
with mini.context:
206+
mini.on_collide(Collision(body_name1, o2, body_name2, position, normal))
207+
mini = Mini.from_object(o2)
208+
if mini:
209+
with mini.context:
210+
mini.on_collide(Collision(body_name2, o1, body_name1, position, normal))
211+
212+
def on_separate(o1: orx.object.Object, body_name1: str, o2: orx.object.Object, body_name2: str):
213+
"""Engine callback for object separation events"""
214+
mini = Mini.from_object(o1)
215+
if mini:
216+
with mini.context:
217+
mini.on_separate(Separation(body_name1, o2, body_name2))
218+
mini = Mini.from_object(o2)
219+
if mini:
220+
with mini.context:
221+
mini.on_separate(Separation(body_name2, o1, body_name1))
222+
223+
def on_shader_param(o: orx.object.Object, shader_name: str, param_name: str, param_type: type[float | orx.vector.Vector]) -> float | orx.vector.Vector | None:
224+
"""Engine callback for setting shader parameters"""
225+
mini = Mini.from_object(o)
226+
if mini:
227+
with mini.context:
228+
arg = mini.on_shader_param(shader_name, param_name, param_type)
229+
return arg
230+
231+
def setup(init: Callable[[], None] | None = None, update: Callable[[float], None] | None = None):
232+
global _per_frame_update
233+
if init is not None:
234+
orx.on_init = init
235+
if update is not None:
236+
_per_frame_update = update
237+
orx.on_update = on_update
238+
orx.on_create = on_create
239+
orx.on_delete = on_delete
240+
orx.on_collide = on_collide
241+
orx.on_separate = on_separate
242+
orx.on_shader_param = on_shader_param

code/build/template/data/[+python python]/orx/__init__.pyi

+3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ from . import command, config, input, object, vector
44
def log(message: str) -> None: ...
55
"""Log a message to the terminal and console using orx's logging"""
66

7+
def close() -> None: ...
8+
"""Send a close event to the engine. Can be used to exit the game loop."""
9+
710
# Engine callbacks
811
def on_init() -> None: ...
912
def on_exit() -> None: ...

code/build/template/data/config/ExtensionTemplate.ini

+1-5
Original file line numberDiff line numberDiff line change
@@ -195,11 +195,7 @@ WindowSize = [Vector]; NB: Used to override the default window size
195195
[+python
196196

197197
[Python]
198-
Main = path/to/main.py; Main Python source file. This can import other local Python sources/modules.
199-
Init = python_init_function; Python function to call during engine initialization. Defaults to orx_init.
200-
Update = python_update_function; Python function to call with each frame. Defaults to orx_update.
201-
Exit = python_exit_function; Python function to call at engine exit. Defaults to orx_exit.
202-
EnableOS = false; If this is true, OS-level functionality like stdout/stderr and file I/O as provided by pocketpy will be available.]
198+
Main = path/to/main.py; Main Python source file. This can import other local Python sources/modules.]
203199
[+cheat
204200

205201
[Cheats]

code/build/template/data/config/[=name].ini

+4
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@ FXList = FadeIn # ColorCycle
8080
ShaderList = NoiseShader]
8181
[+inspector
8282
OnCreate = Inspector.RegisterObject ^]
83+
[+python
84+
Input = @
85+
KEY_SPACE = Reverse
86+
MOUSE_LEFT = Reverse]
8387
[+scroll
8488
TriggerList = Reverse
8589
Input = @

0 commit comments

Comments
 (0)