mini-arcade-core 0.10.0__py3-none-any.whl → 1.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mini_arcade_core/__init__.py +51 -63
- mini_arcade_core/backend/__init__.py +2 -6
- mini_arcade_core/backend/backend.py +148 -8
- mini_arcade_core/backend/events.py +1 -1
- mini_arcade_core/{keymaps/sdl.py → backend/sdl_map.py} +1 -1
- mini_arcade_core/engine/__init__.py +0 -0
- mini_arcade_core/engine/commands.py +169 -0
- mini_arcade_core/engine/game.py +369 -0
- mini_arcade_core/engine/render/__init__.py +0 -0
- mini_arcade_core/engine/render/packet.py +56 -0
- mini_arcade_core/engine/render/pipeline.py +63 -0
- mini_arcade_core/engine/render/viewport.py +203 -0
- mini_arcade_core/managers/__init__.py +0 -22
- mini_arcade_core/managers/cheats.py +71 -240
- mini_arcade_core/managers/inputs.py +5 -3
- mini_arcade_core/runtime/__init__.py +0 -0
- mini_arcade_core/runtime/audio/__init__.py +0 -0
- mini_arcade_core/runtime/audio/audio_adapter.py +20 -0
- mini_arcade_core/runtime/audio/audio_port.py +36 -0
- mini_arcade_core/runtime/capture/__init__.py +0 -0
- mini_arcade_core/runtime/capture/capture_adapter.py +143 -0
- mini_arcade_core/runtime/capture/capture_port.py +51 -0
- mini_arcade_core/runtime/context.py +53 -0
- mini_arcade_core/runtime/file/__init__.py +0 -0
- mini_arcade_core/runtime/file/file_adapter.py +20 -0
- mini_arcade_core/runtime/file/file_port.py +31 -0
- mini_arcade_core/runtime/input/__init__.py +0 -0
- mini_arcade_core/runtime/input/input_adapter.py +49 -0
- mini_arcade_core/runtime/input/input_port.py +31 -0
- mini_arcade_core/runtime/input_frame.py +71 -0
- mini_arcade_core/runtime/scene/__init__.py +0 -0
- mini_arcade_core/runtime/scene/scene_adapter.py +97 -0
- mini_arcade_core/runtime/scene/scene_port.py +149 -0
- mini_arcade_core/runtime/services.py +35 -0
- mini_arcade_core/runtime/window/__init__.py +0 -0
- mini_arcade_core/runtime/window/window_adapter.py +90 -0
- mini_arcade_core/runtime/window/window_port.py +109 -0
- mini_arcade_core/scenes/__init__.py +0 -22
- mini_arcade_core/scenes/autoreg.py +1 -1
- mini_arcade_core/scenes/registry.py +21 -19
- mini_arcade_core/scenes/sim_scene.py +41 -0
- mini_arcade_core/scenes/systems/__init__.py +0 -0
- mini_arcade_core/scenes/systems/base_system.py +40 -0
- mini_arcade_core/scenes/systems/system_pipeline.py +57 -0
- mini_arcade_core/sim/__init__.py +0 -0
- mini_arcade_core/sim/protocols.py +41 -0
- mini_arcade_core/sim/runner.py +222 -0
- mini_arcade_core/spaces/__init__.py +0 -12
- mini_arcade_core/spaces/d2/__init__.py +0 -30
- mini_arcade_core/spaces/d2/boundaries2d.py +10 -1
- mini_arcade_core/spaces/d2/collision2d.py +25 -28
- mini_arcade_core/spaces/d2/geometry2d.py +18 -0
- mini_arcade_core/spaces/d2/kinematics2d.py +2 -8
- mini_arcade_core/spaces/d2/physics2d.py +9 -0
- mini_arcade_core/ui/__init__.py +0 -26
- mini_arcade_core/ui/menu.py +271 -85
- mini_arcade_core/utils/__init__.py +10 -0
- mini_arcade_core/utils/deprecated_decorator.py +45 -0
- mini_arcade_core/utils/logging.py +168 -0
- {mini_arcade_core-0.10.0.dist-info → mini_arcade_core-1.0.1.dist-info}/METADATA +1 -1
- mini_arcade_core-1.0.1.dist-info/RECORD +66 -0
- {mini_arcade_core-0.10.0.dist-info → mini_arcade_core-1.0.1.dist-info}/WHEEL +1 -1
- mini_arcade_core/commands.py +0 -84
- mini_arcade_core/entity.py +0 -72
- mini_arcade_core/game.py +0 -287
- mini_arcade_core/keymaps/__init__.py +0 -15
- mini_arcade_core/managers/base.py +0 -132
- mini_arcade_core/managers/entities.py +0 -38
- mini_arcade_core/managers/overlays.py +0 -53
- mini_arcade_core/managers/system.py +0 -26
- mini_arcade_core/scenes/model.py +0 -34
- mini_arcade_core/scenes/runtime.py +0 -29
- mini_arcade_core/scenes/scene.py +0 -109
- mini_arcade_core/scenes/system.py +0 -69
- mini_arcade_core/ui/overlays.py +0 -41
- mini_arcade_core-0.10.0.dist-info/RECORD +0 -40
- /mini_arcade_core/{keymaps → backend}/keys.py +0 -0
- {mini_arcade_core-0.10.0.dist-info → mini_arcade_core-1.0.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Managers module for Mini Arcade Core.
|
|
3
|
-
Provides various manager classes for handling game entities and resources.
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
|
-
from __future__ import annotations
|
|
7
|
-
|
|
8
|
-
from .cheats import BaseCheatCommand, CheatCode, CheatManager
|
|
9
|
-
from .entities import EntityManager
|
|
10
|
-
from .inputs import InputManager
|
|
11
|
-
from .overlays import OverlayManager
|
|
12
|
-
from .system import SystemManager
|
|
13
|
-
|
|
14
|
-
__all__ = [
|
|
15
|
-
"EntityManager",
|
|
16
|
-
"OverlayManager",
|
|
17
|
-
"CheatCode",
|
|
18
|
-
"CheatManager",
|
|
19
|
-
"BaseCheatCommand",
|
|
20
|
-
"InputManager",
|
|
21
|
-
"SystemManager",
|
|
22
|
-
]
|
|
@@ -5,80 +5,19 @@ Provides cheat codes and related functionality.
|
|
|
5
5
|
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
|
-
from abc import ABC, abstractmethod
|
|
9
8
|
from collections import deque
|
|
10
|
-
from dataclasses import dataclass
|
|
11
|
-
from
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
Deque,
|
|
16
|
-
Dict,
|
|
17
|
-
Generic,
|
|
18
|
-
Optional,
|
|
19
|
-
Protocol,
|
|
20
|
-
Sequence,
|
|
21
|
-
TypeVar,
|
|
22
|
-
)
|
|
23
|
-
|
|
24
|
-
from mini_arcade_core.backend import Event, EventType
|
|
25
|
-
from mini_arcade_core.scenes.system import BaseSceneSystem
|
|
26
|
-
|
|
27
|
-
if TYPE_CHECKING:
|
|
28
|
-
from mini_arcade_core.scenes import Scene
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import Callable, Deque, Dict, Optional, Sequence, TypeVar
|
|
11
|
+
|
|
12
|
+
from mini_arcade_core.engine.commands import Command, CommandQueue
|
|
13
|
+
from mini_arcade_core.runtime.input_frame import InputFrame
|
|
29
14
|
|
|
30
15
|
# Justification: We want to keep the type variable name simple here.
|
|
31
16
|
# pylint: disable=invalid-name
|
|
32
17
|
TContext = TypeVar("TContext")
|
|
33
|
-
TScene = TypeVar("TScene", bound="Scene")
|
|
34
18
|
# pylint: enable=invalid-name
|
|
35
19
|
|
|
36
20
|
|
|
37
|
-
@dataclass
|
|
38
|
-
class BaseCheatCommand(ABC, Generic[TContext]):
|
|
39
|
-
"""
|
|
40
|
-
Base class for cheat codes.
|
|
41
|
-
|
|
42
|
-
:ivar enabled (bool): Whether the cheat code is enabled.
|
|
43
|
-
"""
|
|
44
|
-
|
|
45
|
-
enabled: bool = True
|
|
46
|
-
|
|
47
|
-
def __call__(self, context: TContext) -> None:
|
|
48
|
-
"""
|
|
49
|
-
Execute the cheat code with the given context.
|
|
50
|
-
|
|
51
|
-
:param context: Context object for cheat execution.
|
|
52
|
-
:type context: TContext
|
|
53
|
-
"""
|
|
54
|
-
if not self.enabled:
|
|
55
|
-
return False
|
|
56
|
-
self.execute(context)
|
|
57
|
-
return True
|
|
58
|
-
|
|
59
|
-
@abstractmethod
|
|
60
|
-
def execute(self, context: TContext):
|
|
61
|
-
"""
|
|
62
|
-
Execute the cheat code with the given context.
|
|
63
|
-
|
|
64
|
-
:param context: Context object for cheat execution.
|
|
65
|
-
:type context: TContext
|
|
66
|
-
"""
|
|
67
|
-
raise NotImplementedError("CheatCommand.execute must be overridden.")
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
class CheatAction(Protocol[TContext]):
|
|
71
|
-
"""
|
|
72
|
-
Protocol for cheat code actions.
|
|
73
|
-
|
|
74
|
-
:ivar enabled: Whether the cheat action is enabled.
|
|
75
|
-
"""
|
|
76
|
-
|
|
77
|
-
enabled: bool
|
|
78
|
-
|
|
79
|
-
def __call__(self, ctx: TContext) -> bool: ...
|
|
80
|
-
|
|
81
|
-
|
|
82
21
|
@dataclass(frozen=True)
|
|
83
22
|
class CheatCode:
|
|
84
23
|
"""
|
|
@@ -93,141 +32,130 @@ class CheatCode:
|
|
|
93
32
|
|
|
94
33
|
name: str
|
|
95
34
|
sequence: tuple[str, ...]
|
|
96
|
-
|
|
35
|
+
command_factory: Optional[Callable[[TContext], Command]] = None
|
|
97
36
|
clear_buffer_on_match: bool = False
|
|
98
37
|
enabled: bool = True
|
|
99
38
|
|
|
100
39
|
|
|
40
|
+
@dataclass
|
|
101
41
|
class CheatManager:
|
|
102
42
|
"""
|
|
103
43
|
Reusable cheat code matcher.
|
|
104
44
|
Keeps a rolling buffer of recent keys and triggers callbacks on sequence match.
|
|
105
45
|
"""
|
|
106
46
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
enabled: bool = True,
|
|
112
|
-
normalize: Optional[Callable[[str], str]] = None,
|
|
113
|
-
key_getter: Optional[Callable[[object], Optional[str]]] = None,
|
|
114
|
-
):
|
|
115
|
-
"""
|
|
116
|
-
:param buffer_size: Maximum size of the input buffer.
|
|
117
|
-
:type buffer_size: int
|
|
118
|
-
|
|
119
|
-
:param enabled: Whether the cheat manager is enabled.
|
|
120
|
-
:type enabled: bool
|
|
121
|
-
|
|
122
|
-
:param normalize: Optional function to normalize key strings.
|
|
123
|
-
:type normalize: Callable[[str], str] | None
|
|
124
|
-
|
|
125
|
-
:param key_getter: Optional function to extract key string from event object.
|
|
126
|
-
:type key_getter: Callable[[object], Optional[str]] | None
|
|
127
|
-
"""
|
|
128
|
-
self.enabled = enabled
|
|
129
|
-
self._buffer: Deque[str] = deque(maxlen=buffer_size)
|
|
130
|
-
self._codes: Dict[str, CheatCode] = {}
|
|
47
|
+
buffer_size: int = 16
|
|
48
|
+
enabled: bool = True
|
|
49
|
+
_buffer: Deque[str] = field(default_factory=lambda: deque(maxlen=16))
|
|
50
|
+
_codes: Dict[str, CheatCode[TContext]] = field(default_factory=dict)
|
|
131
51
|
|
|
132
|
-
|
|
133
|
-
#
|
|
134
|
-
self.
|
|
52
|
+
def __post_init__(self):
|
|
53
|
+
# ensure deque maxlen matches buffer_size
|
|
54
|
+
self._buffer = deque(maxlen=self.buffer_size)
|
|
135
55
|
|
|
136
|
-
#
|
|
56
|
+
# TODO: ISolve too-many-arguments warning later
|
|
57
|
+
# Justification: The method needs multiple optional parameters for flexibility.
|
|
137
58
|
# pylint: disable=too-many-arguments
|
|
138
|
-
def
|
|
59
|
+
def register(
|
|
139
60
|
self,
|
|
140
61
|
name: str,
|
|
141
|
-
sequence: Sequence[str],
|
|
142
|
-
command: BaseCheatCommand[TContext],
|
|
143
62
|
*,
|
|
63
|
+
sequence: Sequence[str],
|
|
64
|
+
command_factory: Callable[[TContext], Command],
|
|
144
65
|
clear_buffer_on_match: bool = False,
|
|
145
66
|
enabled: bool = True,
|
|
146
67
|
):
|
|
147
68
|
"""
|
|
148
|
-
Register a new cheat code
|
|
69
|
+
Register a new cheat code.
|
|
149
70
|
|
|
150
|
-
:param name: Unique name
|
|
71
|
+
:param name: Unique name of the cheat code.
|
|
151
72
|
:type name: str
|
|
152
73
|
|
|
153
74
|
:param sequence: Sequence of key strings that trigger the cheat.
|
|
154
75
|
:type sequence: Sequence[str]
|
|
155
76
|
|
|
156
|
-
:param
|
|
157
|
-
:type
|
|
77
|
+
:param command_factory: Factory function to create the Command when the cheat is activated.
|
|
78
|
+
:type command_factory: Callable[[TContext], Command]
|
|
158
79
|
|
|
159
80
|
:param clear_buffer_on_match: Whether to clear the input buffer after a match.
|
|
160
81
|
:type clear_buffer_on_match: bool
|
|
161
82
|
|
|
162
83
|
:param enabled: Whether the cheat code is enabled.
|
|
163
84
|
:type enabled: bool
|
|
85
|
+
|
|
86
|
+
:raises ValueError: If name is empty or sequence is empty.
|
|
164
87
|
"""
|
|
165
88
|
if not name:
|
|
166
|
-
raise ValueError("Cheat
|
|
89
|
+
raise ValueError("Cheat name must be non-empty.")
|
|
167
90
|
if not sequence:
|
|
168
|
-
raise ValueError(
|
|
169
|
-
f"Cheat code '{name}' sequence must be non-empty."
|
|
170
|
-
)
|
|
91
|
+
raise ValueError(f"Cheat '{name}' sequence must be non-empty.")
|
|
171
92
|
|
|
172
|
-
norm_seq = tuple(self.
|
|
93
|
+
norm_seq = tuple(self._norm(s) for s in sequence)
|
|
173
94
|
self._codes[name] = CheatCode(
|
|
174
95
|
name=name,
|
|
175
96
|
sequence=norm_seq,
|
|
176
|
-
|
|
97
|
+
command_factory=command_factory,
|
|
177
98
|
clear_buffer_on_match=clear_buffer_on_match,
|
|
178
99
|
enabled=enabled,
|
|
179
100
|
)
|
|
180
101
|
|
|
181
102
|
# pylint: enable=too-many-arguments
|
|
182
103
|
|
|
183
|
-
def
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
:
|
|
188
|
-
:
|
|
104
|
+
def process_frame(
|
|
105
|
+
self,
|
|
106
|
+
input_frame: InputFrame,
|
|
107
|
+
*,
|
|
108
|
+
context: TContext,
|
|
109
|
+
queue: CommandQueue,
|
|
110
|
+
) -> list[str]:
|
|
189
111
|
"""
|
|
190
|
-
|
|
112
|
+
Process an InputFrame for cheat code matches.
|
|
191
113
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
self._buffer.clear()
|
|
114
|
+
:param input_frame: InputFrame containing current inputs.
|
|
115
|
+
:type input_frame: InputFrame
|
|
195
116
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
Call from Scene when a key is pressed.
|
|
199
|
-
Returns list of cheat names matched (often 0 or 1).
|
|
117
|
+
:param context: Context to pass to command factories.
|
|
118
|
+
:type context: TContext
|
|
200
119
|
|
|
201
|
-
:param
|
|
202
|
-
:type
|
|
120
|
+
:param queue: CommandQueue to push matched commands into.
|
|
121
|
+
:type queue: CommandQueue
|
|
203
122
|
|
|
204
|
-
:
|
|
205
|
-
:
|
|
123
|
+
:return: List of names of matched cheat codes.
|
|
124
|
+
:rtype: list[str]
|
|
206
125
|
"""
|
|
207
126
|
if not self.enabled:
|
|
208
127
|
return []
|
|
209
|
-
key = self._key_getter(event)
|
|
210
|
-
if not key:
|
|
211
|
-
return []
|
|
212
|
-
return self.process_key(key, context)
|
|
213
128
|
|
|
214
|
-
|
|
129
|
+
matched: list[str] = []
|
|
130
|
+
for key in input_frame.keys_pressed:
|
|
131
|
+
key_name = getattr(key, "name", str(key))
|
|
132
|
+
matched.extend(
|
|
133
|
+
self.process_key(key_name, context=context, queue=queue)
|
|
134
|
+
)
|
|
135
|
+
return matched
|
|
136
|
+
|
|
137
|
+
def process_key(
|
|
138
|
+
self, key: str, *, context: TContext, queue: CommandQueue
|
|
139
|
+
) -> list[str]:
|
|
215
140
|
"""
|
|
216
|
-
|
|
141
|
+
Process a single key input.
|
|
217
142
|
|
|
218
143
|
:param key: The key string to process.
|
|
219
144
|
:type key: str
|
|
220
145
|
|
|
221
|
-
:param context: Context
|
|
146
|
+
:param context: Context to pass to command factories.
|
|
222
147
|
:type context: TContext
|
|
223
148
|
|
|
224
|
-
:
|
|
149
|
+
:param queue: CommandQueue to push matched commands into.
|
|
150
|
+
:type queue: CommandQueue
|
|
151
|
+
|
|
152
|
+
:return: List of names of matched cheat codes.
|
|
225
153
|
:rtype: list[str]
|
|
226
154
|
"""
|
|
227
155
|
if not self.enabled:
|
|
228
156
|
return []
|
|
229
157
|
|
|
230
|
-
k = self.
|
|
158
|
+
k = self._norm(key)
|
|
231
159
|
if not k:
|
|
232
160
|
return []
|
|
233
161
|
|
|
@@ -235,121 +163,24 @@ class CheatManager:
|
|
|
235
163
|
buf = tuple(self._buffer)
|
|
236
164
|
|
|
237
165
|
matched: list[str] = []
|
|
238
|
-
# Check if buffer ends with any cheat sequence
|
|
239
166
|
for code in self._codes.values():
|
|
240
167
|
if not code.enabled:
|
|
241
168
|
continue
|
|
169
|
+
|
|
242
170
|
seq = code.sequence
|
|
243
171
|
if len(seq) > len(buf):
|
|
244
172
|
continue
|
|
173
|
+
|
|
245
174
|
if buf[-len(seq) :] == seq:
|
|
246
|
-
code.
|
|
175
|
+
queue.push(code.command_factory(context))
|
|
247
176
|
matched.append(code.name)
|
|
177
|
+
|
|
248
178
|
if code.clear_buffer_on_match:
|
|
249
|
-
self.
|
|
250
|
-
break
|
|
179
|
+
self._buffer.clear()
|
|
180
|
+
break
|
|
251
181
|
|
|
252
182
|
return matched
|
|
253
183
|
|
|
254
184
|
@staticmethod
|
|
255
|
-
def
|
|
256
|
-
|
|
257
|
-
Best-effort extraction:
|
|
258
|
-
- event.key
|
|
259
|
-
- event.key_code
|
|
260
|
-
- event.code
|
|
261
|
-
- event.scancode
|
|
262
|
-
- dict-like {"key": "..."}
|
|
263
|
-
Adjust/override with key_getter in your engine if needed.
|
|
264
|
-
|
|
265
|
-
:param event: The event object.
|
|
266
|
-
:type event: object
|
|
267
|
-
|
|
268
|
-
:return: Extracted key string, or None if not found.
|
|
269
|
-
:rtype: Optional[str]
|
|
270
|
-
"""
|
|
271
|
-
if event is None:
|
|
272
|
-
return None
|
|
273
|
-
|
|
274
|
-
# dict-like
|
|
275
|
-
if isinstance(event, dict):
|
|
276
|
-
v = (
|
|
277
|
-
event.get("key")
|
|
278
|
-
or event.get("key_code")
|
|
279
|
-
or event.get("code")
|
|
280
|
-
or event.get("scancode")
|
|
281
|
-
)
|
|
282
|
-
if isinstance(v, Enum):
|
|
283
|
-
return v.name
|
|
284
|
-
return str(v) if v is not None else None
|
|
285
|
-
|
|
286
|
-
# object-like
|
|
287
|
-
for attr in ("key", "key_code", "code", "scancode"):
|
|
288
|
-
if hasattr(event, attr):
|
|
289
|
-
v = getattr(event, attr)
|
|
290
|
-
if v is None:
|
|
291
|
-
continue
|
|
292
|
-
if isinstance(v, Enum):
|
|
293
|
-
return v.name # <-- THIS is the important bit
|
|
294
|
-
return str(v)
|
|
295
|
-
|
|
296
|
-
return None
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
class CheatSystem(BaseSceneSystem, Generic[TScene]):
|
|
300
|
-
"""
|
|
301
|
-
Scene system for handling cheat codes.
|
|
302
|
-
|
|
303
|
-
:ivar priority (int): Priority of the system (lower runs first).
|
|
304
|
-
:ivar enabled (bool): Whether the system is enabled.
|
|
305
|
-
"""
|
|
306
|
-
|
|
307
|
-
priority = 10
|
|
308
|
-
|
|
309
|
-
def __init__(self, scene: TScene, buffer_size=16):
|
|
310
|
-
"""
|
|
311
|
-
:param buffer_size: Size of the cheat input buffer.
|
|
312
|
-
:type buffer_size: int
|
|
313
|
-
"""
|
|
314
|
-
super().__init__(scene)
|
|
315
|
-
self.cheats = CheatManager(buffer_size=buffer_size)
|
|
316
|
-
|
|
317
|
-
def register(self, name: str, seq: list[str], cmd: Callable, **kwargs):
|
|
318
|
-
"""
|
|
319
|
-
Helper to register a cheat command.
|
|
320
|
-
|
|
321
|
-
:param name: Unique name for the cheat code.
|
|
322
|
-
:type name: str
|
|
323
|
-
|
|
324
|
-
:param seq: Sequence of key strings that trigger the cheat.
|
|
325
|
-
:type seq: list[str]
|
|
326
|
-
|
|
327
|
-
:param cmd: Callable to execute when the cheat is activated.
|
|
328
|
-
:type cmd: Callable
|
|
329
|
-
|
|
330
|
-
:param kwargs: Additional keyword arguments for cheat registration.
|
|
331
|
-
"""
|
|
332
|
-
self.cheats.register_command(name, seq, cmd, **kwargs)
|
|
333
|
-
|
|
334
|
-
def on_enter(self):
|
|
335
|
-
"""
|
|
336
|
-
Called when the scene is entered.
|
|
337
|
-
"""
|
|
338
|
-
# register codes here (or via a builder)
|
|
339
|
-
|
|
340
|
-
def handle_event(self, event: Event) -> bool:
|
|
341
|
-
"""
|
|
342
|
-
Handle an event.
|
|
343
|
-
|
|
344
|
-
:param event: The event to handle.
|
|
345
|
-
:type event: Event
|
|
346
|
-
|
|
347
|
-
:param scene: The scene receiving the event.
|
|
348
|
-
:type scene: TScene
|
|
349
|
-
|
|
350
|
-
:return: True if the event was handled, False otherwise.
|
|
351
|
-
:rtype: bool
|
|
352
|
-
"""
|
|
353
|
-
if event.type == EventType.KEYDOWN:
|
|
354
|
-
self.cheats.process_event(event, self.scene)
|
|
355
|
-
return False # usually don't consume
|
|
185
|
+
def _norm(s: str) -> str:
|
|
186
|
+
return s.strip().upper()
|
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
Input manager for handling input bindings and commands.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
+
# TODO: Implement this manager into the new input system
|
|
6
|
+
# Justification: These module will be used later.
|
|
7
|
+
# pylint: disable=no-name-in-module,import-error,used-before-assignment
|
|
8
|
+
|
|
5
9
|
from __future__ import annotations
|
|
6
10
|
|
|
7
11
|
import logging
|
|
@@ -9,10 +13,10 @@ from dataclasses import dataclass
|
|
|
9
13
|
from typing import TYPE_CHECKING, Callable, Dict, Optional
|
|
10
14
|
|
|
11
15
|
from mini_arcade_core.backend import Event, EventType
|
|
12
|
-
from mini_arcade_core.commands import BaseCommand, BaseSceneCommand
|
|
13
16
|
from mini_arcade_core.keymaps import Key
|
|
14
17
|
|
|
15
18
|
if TYPE_CHECKING:
|
|
19
|
+
from mini_arcade_core.engine.commands import BaseCommand, BaseSceneCommand
|
|
16
20
|
from mini_arcade_core.scenes.scene import Scene
|
|
17
21
|
|
|
18
22
|
logger = logging.getLogger(__name__)
|
|
@@ -139,8 +143,6 @@ class InputManager:
|
|
|
139
143
|
)
|
|
140
144
|
binding.command.execute(to_inject)
|
|
141
145
|
|
|
142
|
-
# --- Convenience API ------------------------------------------------------
|
|
143
|
-
|
|
144
146
|
def on_quit(self, command: BaseCommand, action: str = "quit"):
|
|
145
147
|
"""
|
|
146
148
|
Bind a command to the QUIT event.
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module providing runtime adapters for window and scene management.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from mini_arcade_core.runtime.audio.audio_port import AudioPort
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SDLAudioAdapter(AudioPort):
|
|
11
|
+
"""A no-op audio adapter."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, backend):
|
|
14
|
+
self.backend = backend
|
|
15
|
+
|
|
16
|
+
def load_sound(self, sound_id: str, file_path: str):
|
|
17
|
+
self.backend.load_sound(sound_id, file_path)
|
|
18
|
+
|
|
19
|
+
def play(self, sound_id: str, loops: int = 0):
|
|
20
|
+
self.backend.play_sound(sound_id, loops)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Service interfaces for runtime components.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from mini_arcade_core.backend.backend import Backend
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AudioPort:
|
|
11
|
+
"""Interface for audio playback operations."""
|
|
12
|
+
|
|
13
|
+
backend: Backend
|
|
14
|
+
|
|
15
|
+
def load_sound(self, sound_id: str, file_path: str):
|
|
16
|
+
"""
|
|
17
|
+
Load a sound from a file and associate it with an identifier.
|
|
18
|
+
|
|
19
|
+
:param sound_id: Identifier to associate with the sound.
|
|
20
|
+
:type sound_id: str
|
|
21
|
+
|
|
22
|
+
:param file_path: Path to the sound file.
|
|
23
|
+
:type file_path: str
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def play(self, sound_id: str, loops: int = 0):
|
|
27
|
+
"""
|
|
28
|
+
Play the specified sound.
|
|
29
|
+
|
|
30
|
+
:param sound_id: Identifier of the sound to play.
|
|
31
|
+
:type sound_id: str
|
|
32
|
+
|
|
33
|
+
:param loops: Number of times to loop the sound.
|
|
34
|
+
0 = play once, -1 = infinite loop, 1 = play twice, etc.
|
|
35
|
+
:type loops: int
|
|
36
|
+
"""
|
|
File without changes
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module providing runtime adapters for window and scene management.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
from PIL import Image
|
|
13
|
+
|
|
14
|
+
from mini_arcade_core.backend import Backend
|
|
15
|
+
from mini_arcade_core.runtime.capture.capture_port import CapturePort
|
|
16
|
+
from mini_arcade_core.utils import logger
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class CapturePathBuilder:
|
|
21
|
+
"""
|
|
22
|
+
Helper to build file paths for captured screenshots.
|
|
23
|
+
|
|
24
|
+
:ivar directory (str): Directory to save screenshots in.
|
|
25
|
+
:ivar prefix (str): Prefix for screenshot filenames.
|
|
26
|
+
:ivar ext (str): File extension/format for screenshots.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
directory: str = "screenshots"
|
|
30
|
+
prefix: str = ""
|
|
31
|
+
ext: str = "png" # final output format
|
|
32
|
+
|
|
33
|
+
def build(self, label: str) -> Path:
|
|
34
|
+
"""
|
|
35
|
+
Build a file path for a screenshot with the given label.
|
|
36
|
+
|
|
37
|
+
:param label: Label to include in the filename.
|
|
38
|
+
:type label: str
|
|
39
|
+
|
|
40
|
+
:return: Full path for the screenshot file.
|
|
41
|
+
:rtype: Path
|
|
42
|
+
"""
|
|
43
|
+
stamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
|
|
44
|
+
safe_label = "".join(
|
|
45
|
+
c if c.isalnum() or c in ("-", "_") else "_" for c in label
|
|
46
|
+
)
|
|
47
|
+
name = f"{self.prefix}{stamp}_{safe_label}.{self.ext}"
|
|
48
|
+
return Path(self.directory) / name
|
|
49
|
+
|
|
50
|
+
def build_sim(self, run_id: str, frame_index: int, label: str) -> Path:
|
|
51
|
+
"""
|
|
52
|
+
Build a file path for a simulation frame screenshot.
|
|
53
|
+
|
|
54
|
+
:param run_id: Unique identifier for the simulation run.
|
|
55
|
+
:type run_id: str
|
|
56
|
+
|
|
57
|
+
:param frame_index: Index of the frame in the simulation.
|
|
58
|
+
:type frame_index: int
|
|
59
|
+
|
|
60
|
+
:param label: Label to include in the filename.
|
|
61
|
+
:type label: str
|
|
62
|
+
|
|
63
|
+
:return: Full path for the screenshot file.
|
|
64
|
+
:rtype: Path
|
|
65
|
+
"""
|
|
66
|
+
safe_label = "".join(
|
|
67
|
+
c if c.isalnum() or c in ("-", "_") else "_" for c in label
|
|
68
|
+
)
|
|
69
|
+
# deterministic: run_id + frame index
|
|
70
|
+
name = (
|
|
71
|
+
f"{self.prefix}{run_id}_f{frame_index:08d}_{safe_label}.{self.ext}"
|
|
72
|
+
)
|
|
73
|
+
return Path(self.directory) / run_id / name
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class CaptureAdapter(CapturePort):
|
|
77
|
+
"""Adapter for capturing frames."""
|
|
78
|
+
|
|
79
|
+
def __init__(
|
|
80
|
+
self,
|
|
81
|
+
backend: Backend,
|
|
82
|
+
path_builder: Optional[CapturePathBuilder] = None,
|
|
83
|
+
):
|
|
84
|
+
self.backend = backend
|
|
85
|
+
self.path_builder = path_builder or CapturePathBuilder()
|
|
86
|
+
|
|
87
|
+
def _bmp_to_image(self, bmp_path: str, out_path: str):
|
|
88
|
+
img = Image.open(bmp_path)
|
|
89
|
+
img.save(out_path)
|
|
90
|
+
|
|
91
|
+
def screenshot(self, label: str | None = None) -> str:
|
|
92
|
+
label = label or "shot"
|
|
93
|
+
out_path = self.path_builder.build(label)
|
|
94
|
+
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
95
|
+
|
|
96
|
+
# If backend only saves BMP, capture to a temp bmp next to output
|
|
97
|
+
bmp_path = out_path.with_suffix(".bmp")
|
|
98
|
+
|
|
99
|
+
self.backend.capture_frame(str(bmp_path))
|
|
100
|
+
if not bmp_path.exists():
|
|
101
|
+
raise RuntimeError("Backend capture_frame did not create BMP file")
|
|
102
|
+
|
|
103
|
+
self._bmp_to_image(str(bmp_path), str(out_path))
|
|
104
|
+
try:
|
|
105
|
+
bmp_path.unlink(missing_ok=True)
|
|
106
|
+
# Justification: Various exceptions can occur on file deletion
|
|
107
|
+
# pylint: disable=broad-exception-caught
|
|
108
|
+
except Exception:
|
|
109
|
+
logger.warning(f"Failed to delete temporary BMP file: {bmp_path}")
|
|
110
|
+
# pylint: enable=broad-exception-caught
|
|
111
|
+
|
|
112
|
+
return str(out_path)
|
|
113
|
+
|
|
114
|
+
def screenshot_bytes(self) -> bytes:
|
|
115
|
+
data = self.backend.capture_frame(path=None)
|
|
116
|
+
if data is None:
|
|
117
|
+
raise RuntimeError("Backend returned None for screenshot_bytes()")
|
|
118
|
+
return data
|
|
119
|
+
|
|
120
|
+
def screenshot_sim(
|
|
121
|
+
self, run_id: str, frame_index: int, label: str = "frame"
|
|
122
|
+
) -> str:
|
|
123
|
+
"""Screenshot for simulation frames."""
|
|
124
|
+
out_path = self.path_builder.build_sim(run_id, frame_index, label)
|
|
125
|
+
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
126
|
+
|
|
127
|
+
bmp_path = out_path.with_suffix(".bmp")
|
|
128
|
+
self.backend.capture_frame(str(bmp_path))
|
|
129
|
+
|
|
130
|
+
if not bmp_path.exists():
|
|
131
|
+
raise RuntimeError("Backend capture_frame did not create BMP file")
|
|
132
|
+
|
|
133
|
+
self._bmp_to_image(str(bmp_path), str(out_path))
|
|
134
|
+
|
|
135
|
+
try:
|
|
136
|
+
bmp_path.unlink(missing_ok=True)
|
|
137
|
+
# Justification: Various exceptions can occur on file deletion
|
|
138
|
+
# pylint: disable=broad-exception-caught
|
|
139
|
+
except Exception:
|
|
140
|
+
logger.warning(f"Failed to delete temporary BMP file: {bmp_path}")
|
|
141
|
+
# pylint: enable=broad-exception-caught
|
|
142
|
+
|
|
143
|
+
return str(out_path)
|