mini-arcade-core 1.0.0__py3-none-any.whl → 1.0.2__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 +8 -3
- mini_arcade_core/backend/__init__.py +2 -1
- mini_arcade_core/backend/backend.py +142 -11
- mini_arcade_core/engine/commands.py +26 -0
- mini_arcade_core/engine/game.py +34 -14
- mini_arcade_core/engine/render/pipeline.py +27 -3
- mini_arcade_core/engine/render/viewport.py +203 -0
- mini_arcade_core/managers/inputs.py +0 -2
- mini_arcade_core/runtime/audio/audio_adapter.py +9 -2
- mini_arcade_core/runtime/audio/audio_port.py +20 -1
- mini_arcade_core/runtime/capture/capture_port.py +19 -0
- mini_arcade_core/runtime/scene/scene_adapter.py +29 -1
- mini_arcade_core/runtime/scene/scene_port.py +21 -0
- mini_arcade_core/runtime/window/window_adapter.py +66 -2
- mini_arcade_core/runtime/window/window_port.py +63 -1
- mini_arcade_core/scenes/debug_overlay.py +67 -0
- mini_arcade_core/scenes/registry.py +9 -8
- mini_arcade_core/spaces/d2/boundaries2d.py +10 -1
- mini_arcade_core/ui/menu.py +9 -4
- mini_arcade_core/utils/logging.py +2 -8
- {mini_arcade_core-1.0.0.dist-info → mini_arcade_core-1.0.2.dist-info}/METADATA +1 -1
- {mini_arcade_core-1.0.0.dist-info → mini_arcade_core-1.0.2.dist-info}/RECORD +24 -22
- {mini_arcade_core-1.0.0.dist-info → mini_arcade_core-1.0.2.dist-info}/WHEEL +0 -0
- {mini_arcade_core-1.0.0.dist-info → mini_arcade_core-1.0.2.dist-info}/licenses/LICENSE +0 -0
mini_arcade_core/__init__.py
CHANGED
|
@@ -18,6 +18,8 @@ SceneFactoryLike = Union[Type[SimScene], Callable[[Game], SimScene]]
|
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
# TODO: Improve exception handling and logging in run_game
|
|
21
|
+
# TODO: Consider reducing parameters by using a single config object
|
|
22
|
+
# TODO: Delegate responsibilities to Game class where appropriate
|
|
21
23
|
def run_game(
|
|
22
24
|
scene: SceneFactoryLike | None = None,
|
|
23
25
|
config: GameConfig | None = None,
|
|
@@ -28,9 +30,12 @@ def run_game(
|
|
|
28
30
|
Convenience helper to bootstrap and run a game with a single scene.
|
|
29
31
|
|
|
30
32
|
Supports both:
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
- run_game(SceneClass, cfg) # legacy
|
|
34
|
+
- run_game(config=cfg, initial_scene="main", registry=...) # registry-based
|
|
35
|
+
- run_game(cfg) # config-only
|
|
36
|
+
|
|
37
|
+
:param scene: Optional SimScene factory/class to register
|
|
38
|
+
:type scene: SceneFactoryLike | None
|
|
34
39
|
|
|
35
40
|
:param initial_scene: The SimScene ID to start the game with.
|
|
36
41
|
:type initial_scene: str
|
|
@@ -5,12 +5,29 @@ This is the only part of the code that talks to SDL/pygame directly.
|
|
|
5
5
|
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
|
+
from dataclasses import dataclass
|
|
8
9
|
from typing import Iterable, Protocol
|
|
9
10
|
|
|
10
11
|
from .events import Event
|
|
11
12
|
from .types import Color
|
|
12
13
|
|
|
13
14
|
|
|
15
|
+
@dataclass
|
|
16
|
+
class WindowSettings:
|
|
17
|
+
"""
|
|
18
|
+
Settings for the backend window.
|
|
19
|
+
|
|
20
|
+
:ivar width (int): Width of the window in pixels.
|
|
21
|
+
:ivar height (int): Height of the window in pixels.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
width: int
|
|
25
|
+
height: int
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# TODO: Refactor backend interface into smaller protocols?
|
|
29
|
+
# Justification: Many public methods needed for backend interface
|
|
30
|
+
# pylint: disable=too-many-public-methods
|
|
14
31
|
class Backend(Protocol):
|
|
15
32
|
"""
|
|
16
33
|
Interface that any rendering/input backend must implement.
|
|
@@ -18,19 +35,13 @@ class Backend(Protocol):
|
|
|
18
35
|
mini-arcade-core only talks to this protocol, never to SDL/pygame directly.
|
|
19
36
|
"""
|
|
20
37
|
|
|
21
|
-
def init(self,
|
|
38
|
+
def init(self, window_settings: WindowSettings):
|
|
22
39
|
"""
|
|
23
40
|
Initialize the backend and open a window.
|
|
24
41
|
Should be called once before the main loop.
|
|
25
42
|
|
|
26
|
-
:param
|
|
27
|
-
:type
|
|
28
|
-
|
|
29
|
-
:param height: Height of the window in pixels.
|
|
30
|
-
:type height: int
|
|
31
|
-
|
|
32
|
-
:param title: Title of the window.
|
|
33
|
-
:type title: str
|
|
43
|
+
:param window_settings: Settings for the backend window.
|
|
44
|
+
:type window_settings: WindowSettings
|
|
34
45
|
"""
|
|
35
46
|
|
|
36
47
|
def set_window_title(self, title: str):
|
|
@@ -105,14 +116,13 @@ class Backend(Protocol):
|
|
|
105
116
|
:type color: Color
|
|
106
117
|
"""
|
|
107
118
|
|
|
108
|
-
# pylint: enable=too-many-arguments,too-many-positional-arguments
|
|
109
|
-
|
|
110
119
|
def draw_text(
|
|
111
120
|
self,
|
|
112
121
|
x: int,
|
|
113
122
|
y: int,
|
|
114
123
|
text: str,
|
|
115
124
|
color: Color = (255, 255, 255),
|
|
125
|
+
font_size: int | None = None,
|
|
116
126
|
):
|
|
117
127
|
"""
|
|
118
128
|
Draw text at the given position in a default font and color.
|
|
@@ -133,6 +143,8 @@ class Backend(Protocol):
|
|
|
133
143
|
:type color: Color
|
|
134
144
|
"""
|
|
135
145
|
|
|
146
|
+
# pylint: enable=too-many-arguments,too-many-positional-arguments
|
|
147
|
+
|
|
136
148
|
def measure_text(self, text: str) -> tuple[int, int]:
|
|
137
149
|
"""
|
|
138
150
|
Measure the width and height of the given text string in pixels.
|
|
@@ -158,3 +170,122 @@ class Backend(Protocol):
|
|
|
158
170
|
:rtype: bytes | None
|
|
159
171
|
"""
|
|
160
172
|
raise NotImplementedError
|
|
173
|
+
|
|
174
|
+
def init_audio(
|
|
175
|
+
self, frequency: int = 44100, channels: int = 2, chunk_size: int = 2048
|
|
176
|
+
):
|
|
177
|
+
"""
|
|
178
|
+
Initialize SDL_mixer audio.
|
|
179
|
+
|
|
180
|
+
:param frequency: Audio frequency in Hz.
|
|
181
|
+
:type frequency: int
|
|
182
|
+
|
|
183
|
+
:param channels: Number of audio channels (1=mono, 2=stereo).
|
|
184
|
+
:type channels: int
|
|
185
|
+
|
|
186
|
+
:param chunk_size: Size of audio chunks.
|
|
187
|
+
:type chunk_size: int
|
|
188
|
+
"""
|
|
189
|
+
|
|
190
|
+
def shutdown_audio(self):
|
|
191
|
+
"""Shutdown SDL_mixer audio and free loaded sounds."""
|
|
192
|
+
|
|
193
|
+
def load_sound(self, sound_id: str, path: str):
|
|
194
|
+
"""
|
|
195
|
+
Load a WAV sound and store it by ID.
|
|
196
|
+
Example: backend.load_sound("hit", "assets/sfx/hit.wav")
|
|
197
|
+
|
|
198
|
+
:param sound_id: Unique identifier for the sound.
|
|
199
|
+
:type sound_id: str
|
|
200
|
+
|
|
201
|
+
:param path: File path to the WAV sound.
|
|
202
|
+
:type path: str
|
|
203
|
+
"""
|
|
204
|
+
|
|
205
|
+
def play_sound(self, sound_id: str, loops: int = 0):
|
|
206
|
+
"""
|
|
207
|
+
Play a loaded sound.
|
|
208
|
+
loops=0 => play once
|
|
209
|
+
loops=-1 => infinite loop
|
|
210
|
+
loops=1 => play twice (SDL convention)
|
|
211
|
+
|
|
212
|
+
:param sound_id: Unique identifier for the sound.
|
|
213
|
+
:type sound_id: str
|
|
214
|
+
|
|
215
|
+
:param loops: Number of times to loop the sound.
|
|
216
|
+
:type loops: int
|
|
217
|
+
"""
|
|
218
|
+
|
|
219
|
+
def set_master_volume(self, volume: int):
|
|
220
|
+
"""
|
|
221
|
+
Master volume: 0..128
|
|
222
|
+
"""
|
|
223
|
+
|
|
224
|
+
def set_sound_volume(self, sound_id: str, volume: int):
|
|
225
|
+
"""
|
|
226
|
+
Per-sound volume: 0..128
|
|
227
|
+
|
|
228
|
+
:param sound_id: Unique identifier for the sound.
|
|
229
|
+
:type sound_id: str
|
|
230
|
+
|
|
231
|
+
:param volume: Volume level (0-128).
|
|
232
|
+
:type volume: int
|
|
233
|
+
"""
|
|
234
|
+
|
|
235
|
+
def stop_all_sounds(self):
|
|
236
|
+
"""Stop all channels."""
|
|
237
|
+
|
|
238
|
+
def set_viewport_transform(
|
|
239
|
+
self, offset_x: int, offset_y: int, scale: float
|
|
240
|
+
):
|
|
241
|
+
"""
|
|
242
|
+
Apply a transform so draw_* receives VIRTUAL coords and backend maps to screen.
|
|
243
|
+
|
|
244
|
+
:param offset_x: X offset in pixels.
|
|
245
|
+
:type offset_x: int
|
|
246
|
+
|
|
247
|
+
:param offset_y: Y offset in pixels.
|
|
248
|
+
:type offset_y: int
|
|
249
|
+
|
|
250
|
+
:param scale: Scale factor.
|
|
251
|
+
:type scale: float
|
|
252
|
+
"""
|
|
253
|
+
raise NotImplementedError
|
|
254
|
+
|
|
255
|
+
def clear_viewport_transform(self):
|
|
256
|
+
"""Reset any viewport transform back to identity."""
|
|
257
|
+
raise NotImplementedError
|
|
258
|
+
|
|
259
|
+
def resize_window(self, width: int, height: int):
|
|
260
|
+
"""
|
|
261
|
+
Resize the actual OS window (SDL_SetWindowSize in native backend).
|
|
262
|
+
|
|
263
|
+
:param width: New width in pixels.
|
|
264
|
+
:type width: int
|
|
265
|
+
|
|
266
|
+
:param height: New height in pixels.
|
|
267
|
+
:type height: int
|
|
268
|
+
"""
|
|
269
|
+
raise NotImplementedError
|
|
270
|
+
|
|
271
|
+
def set_clip_rect(self, x: int, y: int, w: int, h: int):
|
|
272
|
+
"""
|
|
273
|
+
Set a clipping rectangle for rendering.
|
|
274
|
+
|
|
275
|
+
:param x: X position of the rectangle's top-left corner.
|
|
276
|
+
:type x: int
|
|
277
|
+
|
|
278
|
+
:param y: Y position of the rectangle's top-left corner.
|
|
279
|
+
:type y: int
|
|
280
|
+
|
|
281
|
+
:param w: Width of the rectangle.
|
|
282
|
+
:type w: int
|
|
283
|
+
|
|
284
|
+
:param h: Height of the rectangle.
|
|
285
|
+
:type h: int
|
|
286
|
+
"""
|
|
287
|
+
raise NotImplementedError
|
|
288
|
+
|
|
289
|
+
def clear_clip_rect(self):
|
|
290
|
+
"""Clear any clipping rectangle."""
|
|
291
|
+
raise NotImplementedError
|
|
@@ -7,6 +7,8 @@ from __future__ import annotations
|
|
|
7
7
|
from dataclasses import dataclass, field
|
|
8
8
|
from typing import TYPE_CHECKING, List, Optional, Protocol, TypeVar
|
|
9
9
|
|
|
10
|
+
from mini_arcade_core.runtime.scene.scene_port import ScenePolicy
|
|
11
|
+
|
|
10
12
|
if TYPE_CHECKING:
|
|
11
13
|
from mini_arcade_core.runtime.services import RuntimeServices
|
|
12
14
|
|
|
@@ -167,3 +169,27 @@ class ChangeSceneCommand(Command):
|
|
|
167
169
|
context: CommandContext,
|
|
168
170
|
):
|
|
169
171
|
context.services.scenes.change(self.scene_id)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@dataclass(frozen=True)
|
|
175
|
+
class ToggleDebugOverlayCommand(Command):
|
|
176
|
+
"""Toggle the debug overlay scene."""
|
|
177
|
+
|
|
178
|
+
DEBUG_OVERLAY_ID = "debug_overlay"
|
|
179
|
+
|
|
180
|
+
def execute(self, context: CommandContext) -> None:
|
|
181
|
+
scenes = context.services.scenes
|
|
182
|
+
if scenes.has_scene(self.DEBUG_OVERLAY_ID):
|
|
183
|
+
scenes.remove_scene(self.DEBUG_OVERLAY_ID)
|
|
184
|
+
return
|
|
185
|
+
|
|
186
|
+
scenes.push(
|
|
187
|
+
self.DEBUG_OVERLAY_ID,
|
|
188
|
+
as_overlay=True,
|
|
189
|
+
policy=ScenePolicy(
|
|
190
|
+
blocks_update=False,
|
|
191
|
+
blocks_input=False,
|
|
192
|
+
is_opaque=False,
|
|
193
|
+
receives_input=False,
|
|
194
|
+
),
|
|
195
|
+
)
|
mini_arcade_core/engine/game.py
CHANGED
|
@@ -8,18 +8,19 @@ from dataclasses import dataclass, field
|
|
|
8
8
|
from time import perf_counter, sleep
|
|
9
9
|
from typing import Dict, Literal
|
|
10
10
|
|
|
11
|
-
from mini_arcade_core.backend import Backend
|
|
11
|
+
from mini_arcade_core.backend import Backend, WindowSettings
|
|
12
|
+
from mini_arcade_core.backend.events import EventType
|
|
13
|
+
from mini_arcade_core.backend.keys import Key
|
|
12
14
|
from mini_arcade_core.engine.commands import (
|
|
13
15
|
CommandContext,
|
|
14
16
|
CommandQueue,
|
|
15
17
|
QuitCommand,
|
|
18
|
+
ToggleDebugOverlayCommand,
|
|
16
19
|
)
|
|
17
|
-
|
|
18
|
-
# from mini_arcade_core.sim.runner import SimRunner, SimRunnerConfig
|
|
19
20
|
from mini_arcade_core.engine.render.packet import RenderPacket
|
|
20
21
|
from mini_arcade_core.engine.render.pipeline import RenderPipeline
|
|
21
22
|
from mini_arcade_core.managers.cheats import CheatManager
|
|
22
|
-
from mini_arcade_core.runtime.audio.audio_adapter import
|
|
23
|
+
from mini_arcade_core.runtime.audio.audio_adapter import SDLAudioAdapter
|
|
23
24
|
from mini_arcade_core.runtime.capture.capture_adapter import CaptureAdapter
|
|
24
25
|
from mini_arcade_core.runtime.file.file_adapter import LocalFilesAdapter
|
|
25
26
|
from mini_arcade_core.runtime.input.input_adapter import InputAdapter
|
|
@@ -91,7 +92,7 @@ class FrameTimer:
|
|
|
91
92
|
:ivar marks (Dict[str, float]): Recorded time marks.
|
|
92
93
|
"""
|
|
93
94
|
|
|
94
|
-
enabled: bool =
|
|
95
|
+
enabled: bool = False
|
|
95
96
|
marks: Dict[str, float] = field(default_factory=dict)
|
|
96
97
|
|
|
97
98
|
def mark(self, name: str):
|
|
@@ -177,9 +178,13 @@ class Game:
|
|
|
177
178
|
self.services = RuntimeServices(
|
|
178
179
|
window=WindowAdapter(
|
|
179
180
|
self.backend,
|
|
181
|
+
WindowSettings(
|
|
182
|
+
width=self.config.window.width,
|
|
183
|
+
height=self.config.window.height,
|
|
184
|
+
),
|
|
180
185
|
),
|
|
181
186
|
scenes=SceneAdapter(self.registry, self),
|
|
182
|
-
audio=
|
|
187
|
+
audio=SDLAudioAdapter(self.backend),
|
|
183
188
|
files=LocalFilesAdapter(),
|
|
184
189
|
capture=CaptureAdapter(self.backend),
|
|
185
190
|
input=InputAdapter(),
|
|
@@ -222,7 +227,7 @@ class Game:
|
|
|
222
227
|
packet_cache: dict[int, RenderPacket] = {}
|
|
223
228
|
|
|
224
229
|
timer = FrameTimer(enabled=True)
|
|
225
|
-
report_every = 60 # print once per second at 60fps
|
|
230
|
+
# report_every = 60 # print once per second at 60fps
|
|
226
231
|
|
|
227
232
|
# TODO: Integrate SimRunner for simulation stepping
|
|
228
233
|
# TODO: Fix assignment-from-no-return warning in self.services.input.build
|
|
@@ -239,6 +244,15 @@ class Game:
|
|
|
239
244
|
last_time = now
|
|
240
245
|
|
|
241
246
|
events = list(backend.poll_events())
|
|
247
|
+
|
|
248
|
+
for e in events:
|
|
249
|
+
if e.type == EventType.WINDOWRESIZED and e.size:
|
|
250
|
+
w, h = e.size
|
|
251
|
+
logger.debug(f"Window resized event: {w}x{h}")
|
|
252
|
+
self.services.window.on_window_resized(w, h)
|
|
253
|
+
# if F1 pressed, toggle debug overlay
|
|
254
|
+
if e.type == EventType.KEYDOWN and e.key == Key.F1:
|
|
255
|
+
self.command_queue.push(ToggleDebugOverlayCommand())
|
|
242
256
|
timer.mark("events_polled")
|
|
243
257
|
|
|
244
258
|
input_frame = self.services.input.build(events, frame_index, dt)
|
|
@@ -294,6 +308,7 @@ class Game:
|
|
|
294
308
|
backend.begin_frame()
|
|
295
309
|
timer.mark("begin_frame_done")
|
|
296
310
|
|
|
311
|
+
vp = self.services.window.get_viewport()
|
|
297
312
|
for entry in self.services.scenes.visible_entries():
|
|
298
313
|
scene = entry.scene
|
|
299
314
|
packet = packet_cache.get(id(scene))
|
|
@@ -302,7 +317,7 @@ class Game:
|
|
|
302
317
|
packet = scene.tick(_neutral_input(frame_index, 0.0), 0.0)
|
|
303
318
|
packet_cache[id(scene)] = packet
|
|
304
319
|
|
|
305
|
-
pipeline.draw_packet(backend, packet)
|
|
320
|
+
pipeline.draw_packet(backend, packet, vp)
|
|
306
321
|
|
|
307
322
|
timer.mark("draw_done")
|
|
308
323
|
backend.end_frame()
|
|
@@ -314,12 +329,14 @@ class Game:
|
|
|
314
329
|
timer.mark("sleep_end")
|
|
315
330
|
|
|
316
331
|
# --- report ---
|
|
317
|
-
if
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
332
|
+
# if timer.enabled and (
|
|
333
|
+
# frame_index % report_every == 0 and frame_index > 0
|
|
334
|
+
# ):
|
|
335
|
+
# ms = timer.report_ms()
|
|
336
|
+
# total = (perf_counter() - timer.marks["frame_start"]) * 1000.0
|
|
337
|
+
# logger.debug(
|
|
338
|
+
# f"[Frame {frame_index}] total={total:.2f}ms | {ms}"
|
|
339
|
+
# )
|
|
323
340
|
|
|
324
341
|
frame_index += 1
|
|
325
342
|
|
|
@@ -340,6 +357,9 @@ class Game:
|
|
|
340
357
|
br, bg, bb = self.config.window.background_color
|
|
341
358
|
self.services.window.set_clear_color(br, bg, bb)
|
|
342
359
|
|
|
360
|
+
# the “authoring resolution”
|
|
361
|
+
self.services.window.set_virtual_resolution(800, 600)
|
|
362
|
+
|
|
343
363
|
def _resolve_world(self) -> object | None:
|
|
344
364
|
# Prefer gameplay world underneath overlays:
|
|
345
365
|
# scan from top to bottom and pick the first scene that has .world
|
|
@@ -9,6 +9,7 @@ from dataclasses import dataclass
|
|
|
9
9
|
|
|
10
10
|
from mini_arcade_core.backend import Backend
|
|
11
11
|
from mini_arcade_core.engine.render.packet import RenderPacket
|
|
12
|
+
from mini_arcade_core.engine.render.viewport import ViewportState
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
@dataclass
|
|
@@ -23,7 +24,12 @@ class RenderPipeline:
|
|
|
23
24
|
- backend draw pass
|
|
24
25
|
"""
|
|
25
26
|
|
|
26
|
-
def draw_packet(
|
|
27
|
+
def draw_packet(
|
|
28
|
+
self,
|
|
29
|
+
backend: Backend,
|
|
30
|
+
packet: RenderPacket,
|
|
31
|
+
viewport_state: ViewportState,
|
|
32
|
+
):
|
|
27
33
|
"""
|
|
28
34
|
Draw the given RenderPacket using the provided Backend.
|
|
29
35
|
|
|
@@ -35,5 +41,23 @@ class RenderPipeline:
|
|
|
35
41
|
"""
|
|
36
42
|
if not packet:
|
|
37
43
|
return
|
|
38
|
-
|
|
39
|
-
|
|
44
|
+
|
|
45
|
+
backend.set_viewport_transform(
|
|
46
|
+
viewport_state.offset_x,
|
|
47
|
+
viewport_state.offset_y,
|
|
48
|
+
viewport_state.scale,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# backend.set_clip_rect(
|
|
52
|
+
# viewport_state.offset_x,
|
|
53
|
+
# viewport_state.offset_y,
|
|
54
|
+
# viewport_state.viewport_w,
|
|
55
|
+
# viewport_state.viewport_h,
|
|
56
|
+
# )
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
for op in packet.ops:
|
|
60
|
+
op(backend)
|
|
61
|
+
finally:
|
|
62
|
+
backend.clear_clip_rect()
|
|
63
|
+
backend.clear_viewport_transform()
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Viewport management for virtual to screen coordinate transformations.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from enum import Enum
|
|
9
|
+
|
|
10
|
+
from mini_arcade_core.utils import logger
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ViewportMode(str, Enum):
|
|
14
|
+
"""
|
|
15
|
+
Viewport scaling modes.
|
|
16
|
+
|
|
17
|
+
:cvar FIT: Scale to fit within window, preserving aspect ratio (letterbox).
|
|
18
|
+
:cvar FILL: Scale to fill entire window, preserving aspect ratio (crop).
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
FIT = "fit" # letterbox
|
|
22
|
+
FILL = "fill" # crop
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# Justification: Many attributes needed to describe viewport state
|
|
26
|
+
# pylint: disable=too-many-instance-attributes
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class ViewportState:
|
|
29
|
+
"""
|
|
30
|
+
Current state of the viewport.
|
|
31
|
+
|
|
32
|
+
:ivar virtual_w (int): Virtual canvas width.
|
|
33
|
+
:ivar virtual_h (int): Virtual canvas height.
|
|
34
|
+
:ivar window_w (int): Current window width.
|
|
35
|
+
:ivar window_h (int): Current window height.
|
|
36
|
+
:ivar mode (ViewportMode): Current viewport mode.
|
|
37
|
+
:ivar scale (float): Current scale factor.
|
|
38
|
+
:ivar viewport_w (int): Width of the viewport rectangle on screen.
|
|
39
|
+
:ivar viewport_h (int): Height of the viewport rectangle on screen.
|
|
40
|
+
:ivar offset_x (int): X offset of the viewport rectangle on screen.
|
|
41
|
+
:ivar offset_y (int): Y offset of the viewport rectangle on screen.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
virtual_w: int
|
|
45
|
+
virtual_h: int
|
|
46
|
+
|
|
47
|
+
window_w: int
|
|
48
|
+
window_h: int
|
|
49
|
+
|
|
50
|
+
mode: ViewportMode
|
|
51
|
+
scale: float
|
|
52
|
+
|
|
53
|
+
# viewport rect in screen pixels where the virtual canvas lands
|
|
54
|
+
# (can be larger than window in FILL mode -> offsets can be negative)
|
|
55
|
+
viewport_w: int
|
|
56
|
+
viewport_h: int
|
|
57
|
+
offset_x: int
|
|
58
|
+
offset_y: int
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# pylint: enable=too-many-instance-attributes
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class Viewport:
|
|
65
|
+
"""
|
|
66
|
+
Manages viewport transformations between virtual and screen coordinates.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
def __init__(
|
|
70
|
+
self,
|
|
71
|
+
virtual_w: int,
|
|
72
|
+
virtual_h: int,
|
|
73
|
+
mode: ViewportMode = ViewportMode.FIT,
|
|
74
|
+
):
|
|
75
|
+
"""
|
|
76
|
+
:param virtual_w: Virtual canvas width.
|
|
77
|
+
:type virtual_w: int
|
|
78
|
+
|
|
79
|
+
:param virtual_h: Virtual canvas height.
|
|
80
|
+
:type virtual_h: int
|
|
81
|
+
|
|
82
|
+
:param mode: Viewport scaling mode.
|
|
83
|
+
:type mode: ViewportMode
|
|
84
|
+
"""
|
|
85
|
+
self._virtual_w = int(virtual_w)
|
|
86
|
+
self._virtual_h = int(virtual_h)
|
|
87
|
+
self._mode = mode
|
|
88
|
+
self._state: ViewportState | None = None
|
|
89
|
+
|
|
90
|
+
def set_virtual_resolution(self, w: int, h: int):
|
|
91
|
+
"""
|
|
92
|
+
Set a new virtual resolution.
|
|
93
|
+
|
|
94
|
+
:param w: New virtual width.
|
|
95
|
+
:type w: int
|
|
96
|
+
|
|
97
|
+
:param h: New virtual height.
|
|
98
|
+
:type h: int
|
|
99
|
+
"""
|
|
100
|
+
self._virtual_w = int(w)
|
|
101
|
+
self._virtual_h = int(h)
|
|
102
|
+
if self._state:
|
|
103
|
+
self.resize(self._state.window_w, self._state.window_h)
|
|
104
|
+
|
|
105
|
+
def set_mode(self, mode: ViewportMode):
|
|
106
|
+
"""
|
|
107
|
+
Set a new viewport mode.
|
|
108
|
+
|
|
109
|
+
:param mode: New viewport mode.
|
|
110
|
+
:type mode: ViewportMode
|
|
111
|
+
"""
|
|
112
|
+
self._mode = mode
|
|
113
|
+
if self._state:
|
|
114
|
+
self.resize(self._state.window_w, self._state.window_h)
|
|
115
|
+
|
|
116
|
+
def resize(self, window_w: int, window_h: int):
|
|
117
|
+
"""
|
|
118
|
+
Resize the viewport based on the current window size.
|
|
119
|
+
|
|
120
|
+
:param window_w: Current window width.
|
|
121
|
+
:type window_w: int
|
|
122
|
+
|
|
123
|
+
:param window_h: Current window height.
|
|
124
|
+
:type window_h: int
|
|
125
|
+
"""
|
|
126
|
+
window_w = int(window_w)
|
|
127
|
+
window_h = int(window_h)
|
|
128
|
+
|
|
129
|
+
sx = window_w / self._virtual_w
|
|
130
|
+
sy = window_h / self._virtual_h
|
|
131
|
+
scale = min(sx, sy) if self._mode == ViewportMode.FIT else max(sx, sy)
|
|
132
|
+
|
|
133
|
+
vw = int(round(self._virtual_w * scale))
|
|
134
|
+
vh = int(round(self._virtual_h * scale))
|
|
135
|
+
ox = int(round((window_w - vw) / 2))
|
|
136
|
+
oy = int(round((window_h - vh) / 2))
|
|
137
|
+
|
|
138
|
+
self._state = ViewportState(
|
|
139
|
+
virtual_w=self._virtual_w,
|
|
140
|
+
virtual_h=self._virtual_h,
|
|
141
|
+
window_w=window_w,
|
|
142
|
+
window_h=window_h,
|
|
143
|
+
mode=self._mode,
|
|
144
|
+
scale=float(scale),
|
|
145
|
+
viewport_w=vw,
|
|
146
|
+
viewport_h=vh,
|
|
147
|
+
offset_x=ox,
|
|
148
|
+
offset_y=oy,
|
|
149
|
+
)
|
|
150
|
+
logger.debug(
|
|
151
|
+
f"Viewport resized: window=({window_w}x{window_h}), "
|
|
152
|
+
f"virtual=({self._virtual_w}x{self._virtual_h}), "
|
|
153
|
+
f"mode={self._mode}, scale={scale:.3f}, "
|
|
154
|
+
f"viewport=({vw}x{vh})@({ox},{oy})"
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
@property
|
|
158
|
+
def state(self) -> ViewportState:
|
|
159
|
+
"""
|
|
160
|
+
Get the current viewport state.
|
|
161
|
+
|
|
162
|
+
:return: Current ViewportState.
|
|
163
|
+
:rtype: ViewportState
|
|
164
|
+
|
|
165
|
+
:raises RuntimeError: If the viewport has not been initialized.
|
|
166
|
+
"""
|
|
167
|
+
if self._state is None:
|
|
168
|
+
raise RuntimeError(
|
|
169
|
+
"Viewport not initialized. Call resize(window_w, window_h)."
|
|
170
|
+
)
|
|
171
|
+
return self._state
|
|
172
|
+
|
|
173
|
+
def screen_to_virtual(self, x: float, y: float) -> tuple[float, float]:
|
|
174
|
+
"""
|
|
175
|
+
Convert screen coordinates to virtual coordinates.
|
|
176
|
+
|
|
177
|
+
:param x: X coordinate on the screen.
|
|
178
|
+
:type x: float
|
|
179
|
+
|
|
180
|
+
:param y: Y coordinate on the screen.
|
|
181
|
+
:type y: float
|
|
182
|
+
|
|
183
|
+
:return: Corresponding virtual coordinates (x, y).
|
|
184
|
+
:rtype: tuple[float, float]
|
|
185
|
+
"""
|
|
186
|
+
s = self.state
|
|
187
|
+
return ((x - s.offset_x) / s.scale, (y - s.offset_y) / s.scale)
|
|
188
|
+
|
|
189
|
+
def virtual_to_screen(self, x: float, y: float) -> tuple[float, float]:
|
|
190
|
+
"""
|
|
191
|
+
Convert virtual coordinates to screen coordinates.
|
|
192
|
+
|
|
193
|
+
:param x: X coordinate in virtual space.
|
|
194
|
+
:type x: float
|
|
195
|
+
|
|
196
|
+
:param y: Y coordinate in virtual space.
|
|
197
|
+
:type y: float
|
|
198
|
+
|
|
199
|
+
:return: Corresponding screen coordinates (x, y).
|
|
200
|
+
:rtype: tuple[float, float]
|
|
201
|
+
"""
|
|
202
|
+
s = self.state
|
|
203
|
+
return (s.offset_x + x * s.scale, s.offset_y + y * s.scale)
|
|
@@ -143,8 +143,6 @@ class InputManager:
|
|
|
143
143
|
)
|
|
144
144
|
binding.command.execute(to_inject)
|
|
145
145
|
|
|
146
|
-
# --- Convenience API ------------------------------------------------------
|
|
147
|
-
|
|
148
146
|
def on_quit(self, command: BaseCommand, action: str = "quit"):
|
|
149
147
|
"""
|
|
150
148
|
Bind a command to the QUIT event.
|
|
@@ -7,7 +7,14 @@ from __future__ import annotations
|
|
|
7
7
|
from mini_arcade_core.runtime.audio.audio_port import AudioPort
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
class
|
|
10
|
+
class SDLAudioAdapter(AudioPort):
|
|
11
11
|
"""A no-op audio adapter."""
|
|
12
12
|
|
|
13
|
-
def
|
|
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)
|
|
@@ -4,14 +4,33 @@ Service interfaces for runtime components.
|
|
|
4
4
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
7
|
+
from mini_arcade_core.backend.backend import Backend
|
|
8
|
+
|
|
7
9
|
|
|
8
10
|
class AudioPort:
|
|
9
11
|
"""Interface for audio playback operations."""
|
|
10
12
|
|
|
11
|
-
|
|
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):
|
|
12
27
|
"""
|
|
13
28
|
Play the specified sound.
|
|
14
29
|
|
|
15
30
|
:param sound_id: Identifier of the sound to play.
|
|
16
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
|
|
17
36
|
"""
|
|
@@ -30,3 +30,22 @@ class CapturePort:
|
|
|
30
30
|
:return: Screenshot data as bytes.
|
|
31
31
|
:rtype: bytes | None
|
|
32
32
|
"""
|
|
33
|
+
|
|
34
|
+
def screenshot_sim(
|
|
35
|
+
self, run_id: str, frame_index: int, label: str = "frame"
|
|
36
|
+
) -> str:
|
|
37
|
+
"""
|
|
38
|
+
Capture the current frame in a simulation context.
|
|
39
|
+
|
|
40
|
+
:param run_id: Unique identifier for the simulation run.
|
|
41
|
+
:type run_id: str
|
|
42
|
+
|
|
43
|
+
:param frame_index: Index of the frame in the simulation.
|
|
44
|
+
:type frame_index: int
|
|
45
|
+
|
|
46
|
+
:param label: Optional label for the screenshot file.
|
|
47
|
+
:type label: str
|
|
48
|
+
|
|
49
|
+
:return: Screenshot file path.
|
|
50
|
+
:rtype: str
|
|
51
|
+
"""
|
|
@@ -94,4 +94,32 @@ class SceneAdapter(ScenePort):
|
|
|
94
94
|
|
|
95
95
|
def input_entry(self):
|
|
96
96
|
vis = self.visible_entries()
|
|
97
|
-
|
|
97
|
+
if not vis:
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
# If some scene blocks input, only scenes at/above it can receive.
|
|
101
|
+
start_idx = 0
|
|
102
|
+
for idx in range(len(vis) - 1, -1, -1):
|
|
103
|
+
if vis[idx].policy.blocks_input:
|
|
104
|
+
start_idx = idx
|
|
105
|
+
break
|
|
106
|
+
|
|
107
|
+
candidates = vis[start_idx:]
|
|
108
|
+
|
|
109
|
+
# Pick the top-most candidate that actually receives input.
|
|
110
|
+
for entry in reversed(candidates):
|
|
111
|
+
if entry.policy.receives_input:
|
|
112
|
+
return entry
|
|
113
|
+
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
def has_scene(self, scene_id: str) -> bool:
|
|
117
|
+
return any(item.entry.scene_id == scene_id for item in self._stack)
|
|
118
|
+
|
|
119
|
+
def remove_scene(self, scene_id: str) -> None:
|
|
120
|
+
# remove first match from top (overlay is usually near top)
|
|
121
|
+
for i in range(len(self._stack) - 1, -1, -1):
|
|
122
|
+
if self._stack[i].entry.scene_id == scene_id:
|
|
123
|
+
item = self._stack.pop(i)
|
|
124
|
+
item.entry.scene.on_exit()
|
|
125
|
+
return
|
|
@@ -23,11 +23,13 @@ class ScenePolicy:
|
|
|
23
23
|
blocks_update: if True, scenes below do not tick/update (pause modal)
|
|
24
24
|
blocks_input: if True, scenes below do not receive input
|
|
25
25
|
is_opaque: if True, scenes below are not rendered
|
|
26
|
+
receives_input: if True, scene can receive input
|
|
26
27
|
"""
|
|
27
28
|
|
|
28
29
|
blocks_update: bool = False
|
|
29
30
|
blocks_input: bool = False
|
|
30
31
|
is_opaque: bool = False
|
|
32
|
+
receives_input: bool = True
|
|
31
33
|
|
|
32
34
|
|
|
33
35
|
@dataclass(frozen=True)
|
|
@@ -147,3 +149,22 @@ class ScenePort:
|
|
|
147
149
|
:return: The SceneEntry that receives input, or None if no scenes are active.
|
|
148
150
|
:rtype: SceneEntry | None
|
|
149
151
|
"""
|
|
152
|
+
|
|
153
|
+
def has_scene(self, scene_id: str) -> bool:
|
|
154
|
+
"""
|
|
155
|
+
Check if a scene with the given ID exists in the stack.
|
|
156
|
+
|
|
157
|
+
:param scene_id: Identifier of the scene to check.
|
|
158
|
+
:type scene_id: str
|
|
159
|
+
|
|
160
|
+
:return: True if the scene exists in the stack, False otherwise.
|
|
161
|
+
:rtype: bool
|
|
162
|
+
"""
|
|
163
|
+
|
|
164
|
+
def remove_scene(self, scene_id: str) -> None:
|
|
165
|
+
"""
|
|
166
|
+
Remove a scene with the given ID from the stack.
|
|
167
|
+
|
|
168
|
+
:param scene_id: Identifier of the scene to remove.
|
|
169
|
+
:type scene_id: str
|
|
170
|
+
"""
|
|
@@ -4,6 +4,13 @@ Module providing runtime adapters for window and scene management.
|
|
|
4
4
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
7
|
+
from venv import logger
|
|
8
|
+
|
|
9
|
+
from mini_arcade_core.engine.render.viewport import (
|
|
10
|
+
Viewport,
|
|
11
|
+
ViewportMode,
|
|
12
|
+
ViewportState,
|
|
13
|
+
)
|
|
7
14
|
from mini_arcade_core.runtime.window.window_port import WindowPort
|
|
8
15
|
|
|
9
16
|
|
|
@@ -12,15 +19,72 @@ class WindowAdapter(WindowPort):
|
|
|
12
19
|
Manages multiple game windows (not implemented).
|
|
13
20
|
"""
|
|
14
21
|
|
|
15
|
-
def __init__(self, backend):
|
|
22
|
+
def __init__(self, backend, window_settings):
|
|
16
23
|
self.backend = backend
|
|
24
|
+
self.window_settings = window_settings
|
|
25
|
+
|
|
26
|
+
self._initialized = False
|
|
27
|
+
|
|
28
|
+
# Default: virtual resolution == initial window resolution.
|
|
29
|
+
# You can override via set_virtual_resolution().
|
|
30
|
+
self._viewport = Viewport(
|
|
31
|
+
window_settings.width,
|
|
32
|
+
window_settings.height,
|
|
33
|
+
mode=ViewportMode.FIT,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# Cached current window size
|
|
37
|
+
self.size = (window_settings.width, window_settings.height)
|
|
17
38
|
|
|
18
39
|
def set_window_size(self, width, height):
|
|
40
|
+
width = int(width)
|
|
41
|
+
height = int(height)
|
|
19
42
|
self.size = (width, height)
|
|
20
|
-
|
|
43
|
+
|
|
44
|
+
self.window_settings.width = width
|
|
45
|
+
self.window_settings.height = height
|
|
46
|
+
|
|
47
|
+
if not self._initialized:
|
|
48
|
+
self.backend.init(self.window_settings)
|
|
49
|
+
self._initialized = True
|
|
50
|
+
else:
|
|
51
|
+
self.backend.resize_window(width, height)
|
|
52
|
+
|
|
53
|
+
self._viewport.resize(width, height)
|
|
54
|
+
|
|
55
|
+
def set_virtual_resolution(self, width: int, height: int):
|
|
56
|
+
self._viewport.set_virtual_resolution(int(width), int(height))
|
|
57
|
+
# re-apply using current window size
|
|
58
|
+
w, h = self.size
|
|
59
|
+
self._viewport.resize(w, h)
|
|
60
|
+
|
|
61
|
+
def set_viewport_mode(self, mode: ViewportMode):
|
|
62
|
+
self._viewport.set_mode(mode)
|
|
63
|
+
|
|
64
|
+
def get_viewport(self) -> ViewportState:
|
|
65
|
+
return self._viewport.state
|
|
66
|
+
|
|
67
|
+
def screen_to_virtual(self, x: float, y: float) -> tuple[float, float]:
|
|
68
|
+
return self._viewport.screen_to_virtual(x, y)
|
|
21
69
|
|
|
22
70
|
def set_title(self, title):
|
|
23
71
|
self.backend.set_window_title(title)
|
|
24
72
|
|
|
25
73
|
def set_clear_color(self, r, g, b):
|
|
26
74
|
self.backend.set_clear_color(r, g, b)
|
|
75
|
+
|
|
76
|
+
def on_window_resized(self, width: int, height: int):
|
|
77
|
+
logger.debug(f"Window resized event: {width}x{height}")
|
|
78
|
+
width = int(width)
|
|
79
|
+
height = int(height)
|
|
80
|
+
|
|
81
|
+
# Update cached size, but DO NOT call backend.resize_window here.
|
|
82
|
+
self.size = (width, height)
|
|
83
|
+
self.window_settings.width = width
|
|
84
|
+
self.window_settings.height = height
|
|
85
|
+
|
|
86
|
+
self._viewport.resize(width, height)
|
|
87
|
+
|
|
88
|
+
def get_virtual_size(self) -> tuple[int, int]:
|
|
89
|
+
s = self.get_viewport()
|
|
90
|
+
return (s.virtual_w, s.virtual_h)
|
|
@@ -4,7 +4,8 @@ Service interfaces for runtime components.
|
|
|
4
4
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
7
|
-
from mini_arcade_core.backend import Backend
|
|
7
|
+
from mini_arcade_core.backend import Backend, WindowSettings
|
|
8
|
+
from mini_arcade_core.engine.render.viewport import ViewportMode, ViewportState
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
class WindowPort:
|
|
@@ -12,6 +13,7 @@ class WindowPort:
|
|
|
12
13
|
|
|
13
14
|
backend: Backend
|
|
14
15
|
size: tuple[int, int]
|
|
16
|
+
window_settings_cls: WindowSettings
|
|
15
17
|
|
|
16
18
|
def set_window_size(self, width: int, height: int):
|
|
17
19
|
"""
|
|
@@ -24,6 +26,47 @@ class WindowPort:
|
|
|
24
26
|
:type height: int
|
|
25
27
|
"""
|
|
26
28
|
|
|
29
|
+
def set_viewport_mode(self, mode: ViewportMode):
|
|
30
|
+
"""
|
|
31
|
+
Set the viewport mode for rendering.
|
|
32
|
+
|
|
33
|
+
:param mode: The viewport mode to set.
|
|
34
|
+
:type mode: ViewportMode
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def get_viewport(self) -> ViewportState:
|
|
38
|
+
"""
|
|
39
|
+
Get the current viewport state.
|
|
40
|
+
|
|
41
|
+
:return: The current ViewportState.
|
|
42
|
+
:rtype: ViewportState
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def screen_to_virtual(self, x: float, y: float) -> tuple[float, float]:
|
|
46
|
+
"""
|
|
47
|
+
Convert screen coordinates to virtual coordinates.
|
|
48
|
+
|
|
49
|
+
:param x: X coordinate on the screen.
|
|
50
|
+
:type x: float
|
|
51
|
+
|
|
52
|
+
:param y: Y coordinate on the screen.
|
|
53
|
+
:type y: float
|
|
54
|
+
|
|
55
|
+
:return: Corresponding virtual coordinates (x, y).
|
|
56
|
+
:rtype: tuple[float, float]
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
def set_virtual_resolution(self, width: int, height: int):
|
|
60
|
+
"""
|
|
61
|
+
Set the virtual resolution for rendering.
|
|
62
|
+
|
|
63
|
+
:param width: Virtual width in pixels.
|
|
64
|
+
:type width: int
|
|
65
|
+
|
|
66
|
+
:param height: Virtual height in pixels.
|
|
67
|
+
:type height: int
|
|
68
|
+
"""
|
|
69
|
+
|
|
27
70
|
def set_title(self, title: str):
|
|
28
71
|
"""
|
|
29
72
|
Set the window title.
|
|
@@ -45,3 +88,22 @@ class WindowPort:
|
|
|
45
88
|
:param b: Blue component (0-255).
|
|
46
89
|
:type b: int
|
|
47
90
|
"""
|
|
91
|
+
|
|
92
|
+
def on_window_resized(self, width: int, height: int):
|
|
93
|
+
"""
|
|
94
|
+
Handle window resized event.
|
|
95
|
+
|
|
96
|
+
:param width: New width of the window.
|
|
97
|
+
:type width: int
|
|
98
|
+
|
|
99
|
+
:param height: New height of the window.
|
|
100
|
+
:type height: int
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
def get_virtual_size(self) -> tuple[int, int]:
|
|
104
|
+
"""
|
|
105
|
+
Get the current virtual resolution size.
|
|
106
|
+
|
|
107
|
+
:return: Tuple of (virtual_width, virtual_height).
|
|
108
|
+
:rtype: tuple[int, int]
|
|
109
|
+
"""
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Debug overlay scene that displays FPS, window size, and scene stack information.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from mini_arcade_core.engine.render.packet import RenderPacket
|
|
8
|
+
from mini_arcade_core.runtime.context import RuntimeContext
|
|
9
|
+
from mini_arcade_core.runtime.input_frame import InputFrame
|
|
10
|
+
from mini_arcade_core.scenes.autoreg import register_scene
|
|
11
|
+
from mini_arcade_core.sim.protocols import SimScene
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@register_scene("debug_overlay")
|
|
15
|
+
class DebugOverlayScene(SimScene):
|
|
16
|
+
"""
|
|
17
|
+
A debug overlay scene that displays FPS, window size, and scene stack information.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, ctx: RuntimeContext):
|
|
21
|
+
super().__init__(ctx)
|
|
22
|
+
self._accum = 0.0
|
|
23
|
+
self._frames = 0
|
|
24
|
+
self._fps = 0.0
|
|
25
|
+
|
|
26
|
+
def tick(self, input_frame: InputFrame, dt: float) -> RenderPacket:
|
|
27
|
+
self._accum += dt
|
|
28
|
+
self._frames += 1
|
|
29
|
+
if self._accum >= 0.5:
|
|
30
|
+
self._fps = self._frames / self._accum
|
|
31
|
+
self._accum = 0.0
|
|
32
|
+
self._frames = 0
|
|
33
|
+
|
|
34
|
+
services = (
|
|
35
|
+
self.context.services
|
|
36
|
+
) # or ctx.services (depends on your scene base)
|
|
37
|
+
# Justification: type checker can't infer type here
|
|
38
|
+
# pylint: disable=assignment-from-no-return
|
|
39
|
+
vp = services.window.get_viewport()
|
|
40
|
+
stack = services.scenes.visible_entries()
|
|
41
|
+
# pylint: enable=assignment-from-no-return
|
|
42
|
+
|
|
43
|
+
lines = [
|
|
44
|
+
f"FPS: {self._fps:5.1f}",
|
|
45
|
+
f"dt: {dt*1000.0:5.2f} ms",
|
|
46
|
+
f"virtual: {vp.virtual_w}x{vp.virtual_h}",
|
|
47
|
+
f"window: {vp.window_w}x{vp.window_h}",
|
|
48
|
+
f"scale: {vp.scale:.3f}",
|
|
49
|
+
f"offset: ({vp.offset_x},{vp.offset_y})",
|
|
50
|
+
"stack:",
|
|
51
|
+
]
|
|
52
|
+
for e in stack:
|
|
53
|
+
lines.append(f" - {e.scene_id} overlay={e.is_overlay}")
|
|
54
|
+
|
|
55
|
+
def draw(backend):
|
|
56
|
+
# translucent background panel
|
|
57
|
+
backend.draw_rect(
|
|
58
|
+
8, 8, 360, 18 * (len(lines) + 1), color=(0, 0, 0, 0.65)
|
|
59
|
+
)
|
|
60
|
+
y = 14
|
|
61
|
+
for line in lines:
|
|
62
|
+
backend.draw_text(
|
|
63
|
+
16, y, line, color=(255, 255, 255), font_size=14
|
|
64
|
+
)
|
|
65
|
+
y += 18
|
|
66
|
+
|
|
67
|
+
return RenderPacket(ops=[draw])
|
|
@@ -93,22 +93,23 @@ class SceneRegistry:
|
|
|
93
93
|
for scene_id, cls in catalog.items():
|
|
94
94
|
self.register_cls(scene_id, cls)
|
|
95
95
|
|
|
96
|
-
def discover(self,
|
|
96
|
+
def discover(self, *packages: str) -> "SceneRegistry":
|
|
97
97
|
"""
|
|
98
98
|
Import all modules in a package so @scene decorators run.
|
|
99
99
|
|
|
100
|
-
:param
|
|
101
|
-
:type
|
|
100
|
+
:param packages: The package names to scan for scene modules.
|
|
101
|
+
:type packages: str
|
|
102
102
|
|
|
103
103
|
:return: The SceneRegistry instance (for chaining).
|
|
104
104
|
:rtype: SceneRegistry
|
|
105
105
|
"""
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
106
|
+
for package in packages:
|
|
107
|
+
pkg = importlib.import_module(package)
|
|
108
|
+
if not hasattr(pkg, "__path__"):
|
|
109
|
+
continue
|
|
109
110
|
|
|
110
|
-
|
|
111
|
-
|
|
111
|
+
for mod in pkgutil.walk_packages(pkg.__path__, pkg.__name__ + "."):
|
|
112
|
+
importlib.import_module(mod.name)
|
|
112
113
|
|
|
113
114
|
self.load_catalog(snapshot())
|
|
114
115
|
return self
|
|
@@ -39,25 +39,34 @@ class VerticalBounce:
|
|
|
39
39
|
|
|
40
40
|
bounds: Bounds2D
|
|
41
41
|
|
|
42
|
-
def apply(self, obj: RectKinematic):
|
|
42
|
+
def apply(self, obj: RectKinematic) -> bool:
|
|
43
43
|
"""
|
|
44
44
|
Apply vertical bounce to the given object.
|
|
45
45
|
|
|
46
46
|
:param obj: The object to apply the bounce to.
|
|
47
47
|
:type obj: RectKinematic
|
|
48
|
+
|
|
49
|
+
:return: True if a bounce occurred, False otherwise.
|
|
50
|
+
:rtype: bool
|
|
48
51
|
"""
|
|
49
52
|
top = self.bounds.top
|
|
50
53
|
bottom = self.bounds.bottom
|
|
51
54
|
|
|
55
|
+
bounced = False
|
|
56
|
+
|
|
52
57
|
# Top collision
|
|
53
58
|
if obj.position.y <= top:
|
|
54
59
|
obj.position.y = top
|
|
55
60
|
obj.velocity.vy *= -1
|
|
61
|
+
bounced = True
|
|
56
62
|
|
|
57
63
|
# Bottom collision
|
|
58
64
|
if obj.position.y + obj.size.height >= bottom:
|
|
59
65
|
obj.position.y = bottom - obj.size.height
|
|
60
66
|
obj.velocity.vy *= -1
|
|
67
|
+
bounced = True
|
|
68
|
+
|
|
69
|
+
return bounced
|
|
61
70
|
|
|
62
71
|
|
|
63
72
|
class RectSprite(Protocol):
|
mini_arcade_core/ui/menu.py
CHANGED
|
@@ -128,6 +128,7 @@ class MenuStyle:
|
|
|
128
128
|
class Menu:
|
|
129
129
|
"""A simple text-based menu system."""
|
|
130
130
|
|
|
131
|
+
# TODO: Solve too-many-arguments warning later
|
|
131
132
|
# Justification: Multiple attributes for menu state
|
|
132
133
|
# pylint: disable=too-many-arguments
|
|
133
134
|
def __init__(
|
|
@@ -151,6 +152,9 @@ class Menu:
|
|
|
151
152
|
|
|
152
153
|
:param style: Optional MenuStyle for customizing appearance.
|
|
153
154
|
:type style: MenuStyle | None
|
|
155
|
+
|
|
156
|
+
:param on_select: Optional callback when an item is selected.
|
|
157
|
+
:type on_select: Optional[Callable[[MenuItem], None]]
|
|
154
158
|
"""
|
|
155
159
|
self.items = list(items)
|
|
156
160
|
self.viewport = viewport
|
|
@@ -250,6 +254,7 @@ class Menu:
|
|
|
250
254
|
|
|
251
255
|
return False
|
|
252
256
|
|
|
257
|
+
# TODO: Delegate drawing to a renderer class later
|
|
253
258
|
def draw(self, surface: Backend):
|
|
254
259
|
"""
|
|
255
260
|
Draw the menu onto the given backend surface.
|
|
@@ -342,6 +347,7 @@ class Menu:
|
|
|
342
347
|
font_size=self.style.item_font_size,
|
|
343
348
|
)
|
|
344
349
|
|
|
350
|
+
# TODO: Solve too-many-locals warning later
|
|
345
351
|
# Justification: Local variables for layout calculations
|
|
346
352
|
# pylint: disable=too-many-locals
|
|
347
353
|
def _draw_buttons(self, surface: Backend, x_center: int, cursor_y: int):
|
|
@@ -454,13 +460,14 @@ class Menu:
|
|
|
454
460
|
+ self.style.title_margin_bottom
|
|
455
461
|
)
|
|
456
462
|
|
|
457
|
-
#
|
|
463
|
+
# Sticky width (never shrink)
|
|
458
464
|
if self.stable_width:
|
|
459
465
|
self._max_content_w_seen = max(self._max_content_w_seen, max_w)
|
|
460
466
|
max_w = self._max_content_w_seen
|
|
461
467
|
|
|
462
468
|
return max_w, content_h, title_h
|
|
463
469
|
|
|
470
|
+
# TODO: Solve too-many-arguments warning later
|
|
464
471
|
# Justification: Many arguments for text drawing utility
|
|
465
472
|
# pylint: disable=too-many-arguments
|
|
466
473
|
@staticmethod
|
|
@@ -674,8 +681,6 @@ class BaseMenuScene(SimScene):
|
|
|
674
681
|
super().__init__(ctx)
|
|
675
682
|
self.model = MenuModel()
|
|
676
683
|
|
|
677
|
-
# ---- hooks (same spirit as before) ----
|
|
678
|
-
|
|
679
684
|
@property
|
|
680
685
|
def menu_title(self) -> str | None:
|
|
681
686
|
"""
|
|
@@ -717,7 +722,7 @@ class BaseMenuScene(SimScene):
|
|
|
717
722
|
def on_enter(self):
|
|
718
723
|
self.menu = Menu(
|
|
719
724
|
self._build_display_items(),
|
|
720
|
-
viewport=self.context.services.window.
|
|
725
|
+
viewport=self.context.services.window.get_virtual_size(),
|
|
721
726
|
title=self.menu_title,
|
|
722
727
|
style=self.menu_style(),
|
|
723
728
|
)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Logging utilities for Mini Arcade Core.
|
|
3
|
+
Provides a console logger with colored output and class/function context.
|
|
3
4
|
"""
|
|
4
5
|
|
|
5
6
|
from __future__ import annotations
|
|
@@ -11,6 +12,7 @@ from typing import Optional
|
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
def _classname_from_locals(locals_: dict) -> Optional[str]:
|
|
15
|
+
"""Retrieve the class name from locals dict, if available."""
|
|
14
16
|
self_obj = locals_.get("self")
|
|
15
17
|
if self_obj is not None:
|
|
16
18
|
return type(self_obj).__name__
|
|
@@ -73,10 +75,6 @@ class ConsoleColorFormatter(logging.Formatter):
|
|
|
73
75
|
return f"{color}{msg}{self.COLORS['RESET']}"
|
|
74
76
|
|
|
75
77
|
|
|
76
|
-
# --------------------------------------------------------------------------------------
|
|
77
|
-
# Global logging setup (single source of truth)
|
|
78
|
-
# --------------------------------------------------------------------------------------
|
|
79
|
-
|
|
80
78
|
LOGGER_FORMAT = (
|
|
81
79
|
"%(asctime)s [%(levelname)-8.8s] [%(name)s] "
|
|
82
80
|
"%(module)s.%(classname)s.%(funcName)s: "
|
|
@@ -166,9 +164,5 @@ def configure_logging(level: int = logging.DEBUG):
|
|
|
166
164
|
root.addHandler(console)
|
|
167
165
|
|
|
168
166
|
|
|
169
|
-
# --------------------------------------------------------------------------------------
|
|
170
|
-
# Public logger for DejaBounce
|
|
171
|
-
# --------------------------------------------------------------------------------------
|
|
172
|
-
|
|
173
167
|
configure_logging()
|
|
174
168
|
logger = logging.getLogger("mini-arcade-core")
|
|
@@ -1,27 +1,28 @@
|
|
|
1
|
-
mini_arcade_core/__init__.py,sha256=
|
|
2
|
-
mini_arcade_core/backend/__init__.py,sha256=
|
|
3
|
-
mini_arcade_core/backend/backend.py,sha256=
|
|
1
|
+
mini_arcade_core/__init__.py,sha256=axwl7fiQ2Zu2vPOTMUxwnvR746gI9RhpBWpBCId_yqo,3686
|
|
2
|
+
mini_arcade_core/backend/__init__.py,sha256=E9uOCttCkXwdN_5MlcFHUmG3Bj6RYMatNNOno4C_6aI,312
|
|
3
|
+
mini_arcade_core/backend/backend.py,sha256=RpFclQFDOLSaqCACWR0sT_OQYjDIfYzQaFvnHzURLvI,7788
|
|
4
4
|
mini_arcade_core/backend/events.py,sha256=5Ohve3CQ6n2CztiOhbCoz6yFDY4z0j4v4R9FBKRDRjc,2929
|
|
5
5
|
mini_arcade_core/backend/keys.py,sha256=LTg20SwLBI3kpPIiTNpq2yBft_QUGj-iNFSNm9M-Fus,3010
|
|
6
6
|
mini_arcade_core/backend/sdl_map.py,sha256=_yBRtvaFUcQKy1kcoIf-SPhbbKEW7dzvzBcI6TLmKjc,2060
|
|
7
7
|
mini_arcade_core/backend/types.py,sha256=SuiwXGNmXCZxfPsww6zj3V_NK7k4jpoCuzMn19afS-g,175
|
|
8
8
|
mini_arcade_core/bus.py,sha256=2Etpoa-UWhk33xJjqDlY5YslPDJEjxNoIEVtF3C73vs,1558
|
|
9
9
|
mini_arcade_core/engine/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
-
mini_arcade_core/engine/commands.py,sha256=
|
|
11
|
-
mini_arcade_core/engine/game.py,sha256=
|
|
10
|
+
mini_arcade_core/engine/commands.py,sha256=5caisiwWhLWO5w18rOvEaeYStOUT8mvrTP0O5gpgeoA,4772
|
|
11
|
+
mini_arcade_core/engine/game.py,sha256=stb75hlqY-DsOqXz7zOFpfSR-jtF8rD0Hpmy1qPuSzE,12409
|
|
12
12
|
mini_arcade_core/engine/render/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
13
|
mini_arcade_core/engine/render/packet.py,sha256=OiAPwGoVHo04OcUWMAoA_N1AFPUMyf8yxNgJthGj4-c,1440
|
|
14
|
-
mini_arcade_core/engine/render/pipeline.py,sha256=
|
|
14
|
+
mini_arcade_core/engine/render/pipeline.py,sha256=XdnzAXNle8_CJOPzYJaEy9Dp_UgoCgJwIh-GpW9-n5E,1516
|
|
15
|
+
mini_arcade_core/engine/render/viewport.py,sha256=fbzH3_rc27IGUtDalmxz5cukwHnpt1kopIeVqmTab20,5784
|
|
15
16
|
mini_arcade_core/managers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
17
|
mini_arcade_core/managers/cheats.py,sha256=jMx2a8YnaNCkCG5MPmIzz4uHuS7-_aYf0J45cv2-3v0,5569
|
|
17
|
-
mini_arcade_core/managers/inputs.py,sha256=
|
|
18
|
+
mini_arcade_core/managers/inputs.py,sha256=9HZ0BnJyUX-elfGETPhhPZnTkz2bK83pEKj7GHPbPFU,8523
|
|
18
19
|
mini_arcade_core/runtime/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
19
20
|
mini_arcade_core/runtime/audio/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
20
|
-
mini_arcade_core/runtime/audio/audio_adapter.py,sha256=
|
|
21
|
-
mini_arcade_core/runtime/audio/audio_port.py,sha256=
|
|
21
|
+
mini_arcade_core/runtime/audio/audio_adapter.py,sha256=9ithbInYUB72ErwvlNrbIlI55uw0DT2QD33geYNXT3c,522
|
|
22
|
+
mini_arcade_core/runtime/audio/audio_port.py,sha256=3Mqv7TchEVkmd-RVjUpCD-EqA-yiL0Jf2Sj3rQwP678,907
|
|
22
23
|
mini_arcade_core/runtime/capture/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
23
24
|
mini_arcade_core/runtime/capture/capture_adapter.py,sha256=qJ2JiOLJHbP00IesbAyyPGPBxSaxwPJRTMaMjMU4bXs,4660
|
|
24
|
-
mini_arcade_core/runtime/capture/capture_port.py,sha256=
|
|
25
|
+
mini_arcade_core/runtime/capture/capture_port.py,sha256=NVxMJrQJELiSYuUJ29tvsdIcCBq4f1dTT2rDLZs6gnI,1230
|
|
25
26
|
mini_arcade_core/runtime/context.py,sha256=IQpAhtbN0ZqJVXG6KXFq3FPk0sIGyPmyNgBYviqZl7A,1632
|
|
26
27
|
mini_arcade_core/runtime/file/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
27
28
|
mini_arcade_core/runtime/file/file_adapter.py,sha256=09q7G9Qijml9d4AAjo6HLC1yuoVTjE_7xaT8apT4mk0,523
|
|
@@ -31,15 +32,16 @@ mini_arcade_core/runtime/input/input_adapter.py,sha256=vExQiwFIWTI3zYD8lmnD9TvoQ
|
|
|
31
32
|
mini_arcade_core/runtime/input/input_port.py,sha256=d4ptftwf92_LJdyaUMFxIsLHXBINzQyJACHn4laNyxQ,746
|
|
32
33
|
mini_arcade_core/runtime/input_frame.py,sha256=34-RAfOD-YScVLyRQrarpm7byFTHjsWM77lIH0JsmT8,2384
|
|
33
34
|
mini_arcade_core/runtime/scene/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
34
|
-
mini_arcade_core/runtime/scene/scene_adapter.py,sha256=
|
|
35
|
-
mini_arcade_core/runtime/scene/scene_port.py,sha256=
|
|
35
|
+
mini_arcade_core/runtime/scene/scene_adapter.py,sha256=I2kWc76fU05pNpt2bO-1_aeR6KwkkmZLD2-9fr_UAn8,3481
|
|
36
|
+
mini_arcade_core/runtime/scene/scene_port.py,sha256=F_KtHO-N5O3EgVAE3HaYYHqoc7zpUnCwn6wQdsl1iTA,4481
|
|
36
37
|
mini_arcade_core/runtime/services.py,sha256=9LX7O-AYFTxKgBDewzu0B_D01EJ41WVAdl8U1ZcvEYg,1061
|
|
37
38
|
mini_arcade_core/runtime/window/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
38
|
-
mini_arcade_core/runtime/window/window_adapter.py,sha256=
|
|
39
|
-
mini_arcade_core/runtime/window/window_port.py,sha256=
|
|
39
|
+
mini_arcade_core/runtime/window/window_adapter.py,sha256=_yCcJwvHN0gEPh0rkgLEKtPZ50qRbONsUTbgtV1m7Y8,2689
|
|
40
|
+
mini_arcade_core/runtime/window/window_port.py,sha256=JbSH549De7fa4ifQ0EH5QQoq03Got1n9C4qViLgciUU,2682
|
|
40
41
|
mini_arcade_core/scenes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
41
42
|
mini_arcade_core/scenes/autoreg.py,sha256=wsuY7YUSZFmDyToKHFriAG78OU48-7J4BfL_X6T5GBg,1037
|
|
42
|
-
mini_arcade_core/scenes/
|
|
43
|
+
mini_arcade_core/scenes/debug_overlay.py,sha256=N4sGQ_3pMz-ZvJZX63kqtylpS9U1PTpWeImrkfmVnus,2230
|
|
44
|
+
mini_arcade_core/scenes/registry.py,sha256=taWyLUDLRe7RJN0kqF_qpqRSiGCcbzEeCpcKlmPUFnE,3428
|
|
43
45
|
mini_arcade_core/scenes/sim_scene.py,sha256=b2JsOvPFkHCdCf8pMLJZ90qB0JJ6B8Ka3o5QK4cVshI,1055
|
|
44
46
|
mini_arcade_core/scenes/systems/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
45
47
|
mini_arcade_core/scenes/systems/base_system.py,sha256=GfMrXsO8ynW3xOxWeav7Ug5XUbRnbF0vo8VzmG7gpec,1075
|
|
@@ -49,17 +51,17 @@ mini_arcade_core/sim/protocols.py,sha256=b7d2WAKRokTNbteNhUaWdGx9vc9Fnccxb-5rPwo
|
|
|
49
51
|
mini_arcade_core/sim/runner.py,sha256=ZF-BZJw-NcaFrg4zsUu1zOUUBZwZbRYflqcdF1jDcmM,7446
|
|
50
52
|
mini_arcade_core/spaces/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
51
53
|
mini_arcade_core/spaces/d2/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
52
|
-
mini_arcade_core/spaces/d2/boundaries2d.py,sha256=
|
|
54
|
+
mini_arcade_core/spaces/d2/boundaries2d.py,sha256=xeTnd0pW5DKfqaKsfSBXnufeb45aXNIspgHRyLXWejo,2804
|
|
53
55
|
mini_arcade_core/spaces/d2/collision2d.py,sha256=5IvgLnyVb8i0uzzZuum1noWsNhoxcvHOLaHkmrTMTxQ,1710
|
|
54
56
|
mini_arcade_core/spaces/d2/geometry2d.py,sha256=FuYzef-XdOyb1aeGLJbxINxr0WJHnqFFBgtbPi1WonY,1716
|
|
55
57
|
mini_arcade_core/spaces/d2/kinematics2d.py,sha256=AJ3DhPXNgm6wZYwCljMIE4_2BYx3E2rPcwhXTgQALkU,2030
|
|
56
58
|
mini_arcade_core/spaces/d2/physics2d.py,sha256=OQT7r-zMtmoKD2aWCSNmRAdI0OGIpxGX-pLR8LcAMbQ,1854
|
|
57
59
|
mini_arcade_core/ui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
58
|
-
mini_arcade_core/ui/menu.py,sha256=
|
|
60
|
+
mini_arcade_core/ui/menu.py,sha256=pUC6qfG3lNXa1Ga2Kz_cSgjiAfAWVMRevum61G_RE8k,24646
|
|
59
61
|
mini_arcade_core/utils/__init__.py,sha256=3Q9r6bTyqImYix8BnOGwWjAz25nbTQezGcRq3m5KEYE,189
|
|
60
62
|
mini_arcade_core/utils/deprecated_decorator.py,sha256=yrrW2ZqPskK-4MUTyIrMb465Wc54X2poV53ZQutZWqc,1140
|
|
61
|
-
mini_arcade_core/utils/logging.py,sha256=
|
|
62
|
-
mini_arcade_core-1.0.
|
|
63
|
-
mini_arcade_core-1.0.
|
|
64
|
-
mini_arcade_core-1.0.
|
|
65
|
-
mini_arcade_core-1.0.
|
|
63
|
+
mini_arcade_core/utils/logging.py,sha256=YyirsGRSpGtxegUl3HWz37mGNngK3QkYm2_aZjXJC84,5279
|
|
64
|
+
mini_arcade_core-1.0.2.dist-info/METADATA,sha256=CtAYloxC_GLDTJT53Ap5Sz8JSF3b_2iTPJuo_Kc4vSg,8188
|
|
65
|
+
mini_arcade_core-1.0.2.dist-info/WHEEL,sha256=3ny-bZhpXrU6vSQ1UPG34FoxZBp3lVcvK0LkgUz6VLk,88
|
|
66
|
+
mini_arcade_core-1.0.2.dist-info/licenses/LICENSE,sha256=3lHAuV0584cVS5vAqi2uC6GcsVgxUijvwvtZckyvaZ4,1096
|
|
67
|
+
mini_arcade_core-1.0.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|