mini-arcade-core 1.1.1__py3-none-any.whl → 1.2.0__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 +14 -42
- mini_arcade_core/backend/__init__.py +1 -2
- mini_arcade_core/backend/backend.py +182 -184
- mini_arcade_core/backend/types.py +5 -1
- mini_arcade_core/engine/commands.py +8 -8
- mini_arcade_core/engine/game.py +54 -354
- mini_arcade_core/engine/game_config.py +40 -0
- mini_arcade_core/engine/gameplay_settings.py +24 -0
- mini_arcade_core/engine/loop/config.py +20 -0
- mini_arcade_core/engine/loop/hooks.py +77 -0
- mini_arcade_core/engine/loop/runner.py +272 -0
- mini_arcade_core/engine/loop/state.py +32 -0
- mini_arcade_core/engine/managers.py +24 -0
- mini_arcade_core/engine/render/context.py +0 -2
- mini_arcade_core/engine/render/effects/base.py +2 -2
- mini_arcade_core/engine/render/effects/crt.py +4 -4
- mini_arcade_core/engine/render/effects/registry.py +1 -1
- mini_arcade_core/engine/render/effects/vignette.py +8 -8
- mini_arcade_core/engine/render/passes/begin_frame.py +1 -1
- mini_arcade_core/engine/render/passes/end_frame.py +1 -1
- mini_arcade_core/engine/render/passes/postfx.py +1 -1
- mini_arcade_core/engine/render/passes/ui.py +1 -1
- mini_arcade_core/engine/render/passes/world.py +6 -6
- mini_arcade_core/engine/render/pipeline.py +7 -6
- mini_arcade_core/engine/render/viewport.py +10 -4
- mini_arcade_core/engine/scenes/models.py +54 -0
- mini_arcade_core/engine/scenes/scene_manager.py +213 -0
- mini_arcade_core/runtime/audio/audio_adapter.py +4 -3
- mini_arcade_core/runtime/audio/audio_port.py +0 -4
- mini_arcade_core/runtime/capture/capture_adapter.py +13 -6
- mini_arcade_core/runtime/capture/capture_port.py +0 -4
- mini_arcade_core/runtime/context.py +8 -6
- mini_arcade_core/runtime/scene/scene_query_adapter.py +31 -0
- mini_arcade_core/runtime/scene/scene_query_port.py +38 -0
- mini_arcade_core/runtime/services.py +3 -2
- mini_arcade_core/runtime/window/window_adapter.py +43 -41
- mini_arcade_core/runtime/window/window_port.py +3 -17
- mini_arcade_core/scenes/debug_overlay.py +5 -4
- mini_arcade_core/scenes/registry.py +11 -1
- mini_arcade_core/scenes/sim_scene.py +14 -14
- mini_arcade_core/ui/menu.py +54 -16
- mini_arcade_core/utils/__init__.py +2 -1
- mini_arcade_core/utils/logging.py +47 -18
- mini_arcade_core/utils/profiler.py +283 -0
- {mini_arcade_core-1.1.1.dist-info → mini_arcade_core-1.2.0.dist-info}/METADATA +1 -1
- mini_arcade_core-1.2.0.dist-info/RECORD +92 -0
- {mini_arcade_core-1.1.1.dist-info → mini_arcade_core-1.2.0.dist-info}/WHEEL +1 -1
- mini_arcade_core/managers/inputs.py +0 -284
- mini_arcade_core/runtime/scene/scene_adapter.py +0 -125
- mini_arcade_core/runtime/scene/scene_port.py +0 -170
- mini_arcade_core/sim/protocols.py +0 -41
- mini_arcade_core/sim/runner.py +0 -222
- mini_arcade_core-1.1.1.dist-info/RECORD +0 -85
- /mini_arcade_core/{managers → engine}/cheats.py +0 -0
- /mini_arcade_core/{managers → engine/loop}/__init__.py +0 -0
- /mini_arcade_core/{sim → engine/scenes}/__init__.py +0 -0
- {mini_arcade_core-1.1.1.dist-info → mini_arcade_core-1.2.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Models for scene management in the engine.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
|
|
9
|
+
from mini_arcade_core.scenes.sim_scene import SimScene
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class ScenePolicy:
|
|
14
|
+
"""
|
|
15
|
+
Controls how a scene behaves in the scene stack.
|
|
16
|
+
|
|
17
|
+
blocks_update: if True, scenes below do not tick/update (pause modal)
|
|
18
|
+
blocks_input: if True, scenes below do not receive input
|
|
19
|
+
is_opaque: if True, scenes below are not rendered
|
|
20
|
+
receives_input: if True, scene can receive input
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
blocks_update: bool = False
|
|
24
|
+
blocks_input: bool = False
|
|
25
|
+
is_opaque: bool = False
|
|
26
|
+
receives_input: bool = True
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True)
|
|
30
|
+
class SceneEntry:
|
|
31
|
+
"""
|
|
32
|
+
An entry in the scene stack.
|
|
33
|
+
|
|
34
|
+
:ivar scene_id (str): Identifier of the scene.
|
|
35
|
+
:ivar scene (SimScene): The scene instance.
|
|
36
|
+
:ivar is_overlay (bool): Whether the scene is an overlay.
|
|
37
|
+
:ivar policy (ScenePolicy): The scene's policy.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
scene_id: str
|
|
41
|
+
scene: SimScene
|
|
42
|
+
is_overlay: bool
|
|
43
|
+
policy: ScenePolicy
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True)
|
|
47
|
+
class StackItem:
|
|
48
|
+
"""
|
|
49
|
+
An item in the scene stack.
|
|
50
|
+
|
|
51
|
+
:ivar entry (SceneEntry): The scene entry.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
entry: SceneEntry
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module providing runtime adapters for window and scene management.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import TYPE_CHECKING, List
|
|
8
|
+
|
|
9
|
+
from mini_arcade_core.engine.scenes.models import (
|
|
10
|
+
SceneEntry,
|
|
11
|
+
ScenePolicy,
|
|
12
|
+
StackItem,
|
|
13
|
+
)
|
|
14
|
+
from mini_arcade_core.runtime.context import RuntimeContext
|
|
15
|
+
from mini_arcade_core.scenes.registry import SceneRegistry
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from mini_arcade_core.engine.game import Game
|
|
19
|
+
from mini_arcade_core.scenes.sim_scene import SimScene
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SceneAdapter:
|
|
23
|
+
"""
|
|
24
|
+
Manages multiple scenes (not implemented).
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, registry: SceneRegistry, game: Game):
|
|
28
|
+
self._registry = registry
|
|
29
|
+
self._stack: List[StackItem] = []
|
|
30
|
+
self._game = game
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def current_scene(self) -> SimScene | None:
|
|
34
|
+
"""
|
|
35
|
+
Get the currently active scene.
|
|
36
|
+
|
|
37
|
+
:return: The active Scene instance, or None if no scene is active.
|
|
38
|
+
:rtype: SimScene | None
|
|
39
|
+
"""
|
|
40
|
+
return self._stack[-1].entry.scene if self._stack else None
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def visible_stack(self) -> List[SimScene]:
|
|
44
|
+
"""
|
|
45
|
+
Return the list of scenes that should be drawn (base + overlays).
|
|
46
|
+
We draw from the top-most non-overlay scene upward.
|
|
47
|
+
|
|
48
|
+
:return: List of visible Scene instances.
|
|
49
|
+
:rtype: List[SimScene]
|
|
50
|
+
"""
|
|
51
|
+
return [e.scene for e in self.visible_entries()]
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def listed_scenes(self) -> List[SimScene]:
|
|
55
|
+
"""
|
|
56
|
+
Return all scenes in the stack.
|
|
57
|
+
|
|
58
|
+
:return: List of all Scene instances in the stack.
|
|
59
|
+
:rtype: List[SimScene]
|
|
60
|
+
"""
|
|
61
|
+
return [item.entry.scene for item in self._stack]
|
|
62
|
+
|
|
63
|
+
def change(self, scene_id: str):
|
|
64
|
+
"""
|
|
65
|
+
Change the current scene to the specified scene.
|
|
66
|
+
|
|
67
|
+
:param scene_id: Identifier of the scene to switch to.
|
|
68
|
+
:type scene_id: str
|
|
69
|
+
"""
|
|
70
|
+
self.clean()
|
|
71
|
+
self.push(scene_id, as_overlay=False)
|
|
72
|
+
|
|
73
|
+
def push(
|
|
74
|
+
self,
|
|
75
|
+
scene_id: str,
|
|
76
|
+
*,
|
|
77
|
+
as_overlay: bool = False,
|
|
78
|
+
policy: ScenePolicy | None = None,
|
|
79
|
+
):
|
|
80
|
+
"""
|
|
81
|
+
Push a new scene onto the scene stack.
|
|
82
|
+
|
|
83
|
+
:param scene_id: Identifier of the scene to push.
|
|
84
|
+
:type scene_id: str
|
|
85
|
+
|
|
86
|
+
:param as_overlay: Whether to push the scene as an overlay.
|
|
87
|
+
:type as_overlay: bool
|
|
88
|
+
"""
|
|
89
|
+
# default policy based on overlay vs base
|
|
90
|
+
if policy is None:
|
|
91
|
+
# base scenes: do not block anything by default
|
|
92
|
+
policy = ScenePolicy()
|
|
93
|
+
runtime_context = RuntimeContext.from_game(self._game)
|
|
94
|
+
scene = self._registry.create(
|
|
95
|
+
scene_id, runtime_context
|
|
96
|
+
) # or whatever your factory call is
|
|
97
|
+
scene.on_enter()
|
|
98
|
+
|
|
99
|
+
entry = SceneEntry(
|
|
100
|
+
scene_id=scene_id,
|
|
101
|
+
scene=scene,
|
|
102
|
+
is_overlay=as_overlay,
|
|
103
|
+
policy=policy,
|
|
104
|
+
)
|
|
105
|
+
self._stack.append(StackItem(entry=entry))
|
|
106
|
+
|
|
107
|
+
def pop(self) -> SimScene | None:
|
|
108
|
+
"""
|
|
109
|
+
Pop the current scene from the scene stack.
|
|
110
|
+
|
|
111
|
+
:return: The popped Scene instance, or None if the stack was empty.
|
|
112
|
+
:rtype: SimScene | None
|
|
113
|
+
"""
|
|
114
|
+
if not self._stack:
|
|
115
|
+
return
|
|
116
|
+
item = self._stack.pop()
|
|
117
|
+
item.entry.scene.on_exit()
|
|
118
|
+
|
|
119
|
+
def clean(self):
|
|
120
|
+
"""Clean up all scenes from the scene stack."""
|
|
121
|
+
while self._stack:
|
|
122
|
+
self.pop()
|
|
123
|
+
|
|
124
|
+
def quit(self):
|
|
125
|
+
"""Quit the game"""
|
|
126
|
+
self._game.quit()
|
|
127
|
+
|
|
128
|
+
def visible_entries(self) -> list[SceneEntry]:
|
|
129
|
+
"""
|
|
130
|
+
Render from bottom->top unless an opaque entry exists; if so,
|
|
131
|
+
render only from that entry up.
|
|
132
|
+
|
|
133
|
+
:return: List of SceneEntry instances to render.
|
|
134
|
+
:rtype: list[SceneEntry]
|
|
135
|
+
"""
|
|
136
|
+
entries = [i.entry for i in self._stack]
|
|
137
|
+
# find highest opaque from top down; render starting there
|
|
138
|
+
for idx in range(len(entries) - 1, -1, -1):
|
|
139
|
+
if entries[idx].policy.is_opaque:
|
|
140
|
+
return entries[idx:]
|
|
141
|
+
return entries
|
|
142
|
+
|
|
143
|
+
def update_entries(self) -> list[SceneEntry]:
|
|
144
|
+
"""
|
|
145
|
+
Tick/update scenes considering blocks_update.
|
|
146
|
+
Typical: pause overlay blocks update below it.
|
|
147
|
+
|
|
148
|
+
:return: List of SceneEntry instances to update.
|
|
149
|
+
:rtype: list[SceneEntry]
|
|
150
|
+
"""
|
|
151
|
+
vis = self.visible_entries()
|
|
152
|
+
if not vis:
|
|
153
|
+
return []
|
|
154
|
+
out = []
|
|
155
|
+
for entry in reversed(vis): # top->down
|
|
156
|
+
out.append(entry)
|
|
157
|
+
if entry.policy.blocks_update:
|
|
158
|
+
break
|
|
159
|
+
return list(reversed(out)) # bottom->top order
|
|
160
|
+
|
|
161
|
+
def input_entry(self) -> SceneEntry | None:
|
|
162
|
+
"""
|
|
163
|
+
Who gets input this frame. If top blocks_input, only it receives input.
|
|
164
|
+
If not, top still gets input (v1 simple). Later you can allow fall-through.
|
|
165
|
+
|
|
166
|
+
:return: The SceneEntry that receives input, or None if no scenes are active.
|
|
167
|
+
:rtype: SceneEntry | None
|
|
168
|
+
"""
|
|
169
|
+
vis = self.visible_entries()
|
|
170
|
+
if not vis:
|
|
171
|
+
return None
|
|
172
|
+
|
|
173
|
+
# If some scene blocks input, only scenes at/above it can receive.
|
|
174
|
+
start_idx = 0
|
|
175
|
+
for idx in range(len(vis) - 1, -1, -1):
|
|
176
|
+
if vis[idx].policy.blocks_input:
|
|
177
|
+
start_idx = idx
|
|
178
|
+
break
|
|
179
|
+
|
|
180
|
+
candidates = vis[start_idx:]
|
|
181
|
+
|
|
182
|
+
# Pick the top-most candidate that actually receives input.
|
|
183
|
+
for entry in reversed(candidates):
|
|
184
|
+
if entry.policy.receives_input:
|
|
185
|
+
return entry
|
|
186
|
+
|
|
187
|
+
return None
|
|
188
|
+
|
|
189
|
+
def has_scene(self, scene_id: str) -> bool:
|
|
190
|
+
"""
|
|
191
|
+
Check if a scene with the given ID exists in the stack.
|
|
192
|
+
|
|
193
|
+
:param scene_id: Identifier of the scene to check.
|
|
194
|
+
:type scene_id: str
|
|
195
|
+
|
|
196
|
+
:return: True if the scene exists in the stack, False otherwise.
|
|
197
|
+
:rtype: bool
|
|
198
|
+
"""
|
|
199
|
+
return any(item.entry.scene_id == scene_id for item in self._stack)
|
|
200
|
+
|
|
201
|
+
def remove_scene(self, scene_id: str):
|
|
202
|
+
"""
|
|
203
|
+
Remove a scene with the given ID from the stack.
|
|
204
|
+
|
|
205
|
+
:param scene_id: Identifier of the scene to remove.
|
|
206
|
+
:type scene_id: str
|
|
207
|
+
"""
|
|
208
|
+
# remove first match from top (overlay is usually near top)
|
|
209
|
+
for i in range(len(self._stack) - 1, -1, -1):
|
|
210
|
+
if self._stack[i].entry.scene_id == scene_id:
|
|
211
|
+
item = self._stack.pop(i)
|
|
212
|
+
item.entry.scene.on_exit()
|
|
213
|
+
return
|
|
@@ -4,17 +4,18 @@ Module providing runtime adapters for window and scene management.
|
|
|
4
4
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
7
|
+
from mini_arcade_core.backend import Backend
|
|
7
8
|
from mini_arcade_core.runtime.audio.audio_port import AudioPort
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
class SDLAudioAdapter(AudioPort):
|
|
11
12
|
"""A no-op audio adapter."""
|
|
12
13
|
|
|
13
|
-
def __init__(self, backend):
|
|
14
|
+
def __init__(self, backend: Backend):
|
|
14
15
|
self.backend = backend
|
|
15
16
|
|
|
16
17
|
def load_sound(self, sound_id: str, file_path: str):
|
|
17
|
-
self.backend.load_sound(sound_id, file_path)
|
|
18
|
+
self.backend.audio.load_sound(sound_id, file_path)
|
|
18
19
|
|
|
19
20
|
def play(self, sound_id: str, loops: int = 0):
|
|
20
|
-
self.backend.play_sound(sound_id, loops)
|
|
21
|
+
self.backend.audio.play_sound(sound_id, loops)
|
|
@@ -4,14 +4,10 @@ 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
|
-
|
|
9
7
|
|
|
10
8
|
class AudioPort:
|
|
11
9
|
"""Interface for audio playback operations."""
|
|
12
10
|
|
|
13
|
-
backend: Backend
|
|
14
|
-
|
|
15
11
|
def load_sound(self, sound_id: str, file_path: str):
|
|
16
12
|
"""
|
|
17
13
|
Load a sound from a file and associate it with an identifier.
|
|
@@ -74,7 +74,14 @@ class CapturePathBuilder:
|
|
|
74
74
|
|
|
75
75
|
|
|
76
76
|
class CaptureAdapter(CapturePort):
|
|
77
|
-
"""
|
|
77
|
+
"""
|
|
78
|
+
Adapter for capturing frames.
|
|
79
|
+
|
|
80
|
+
:param backend: Backend instance to use for capturing frames.
|
|
81
|
+
:type backend: Backend
|
|
82
|
+
:param path_builder: Optional CapturePathBuilder for building file paths.
|
|
83
|
+
:type path_builder: CapturePathBuilder | None
|
|
84
|
+
"""
|
|
78
85
|
|
|
79
86
|
def __init__(
|
|
80
87
|
self,
|
|
@@ -96,9 +103,9 @@ class CaptureAdapter(CapturePort):
|
|
|
96
103
|
# If backend only saves BMP, capture to a temp bmp next to output
|
|
97
104
|
bmp_path = out_path.with_suffix(".bmp")
|
|
98
105
|
|
|
99
|
-
self.backend.
|
|
106
|
+
self.backend.capture.bmp(str(bmp_path))
|
|
100
107
|
if not bmp_path.exists():
|
|
101
|
-
raise RuntimeError("Backend
|
|
108
|
+
raise RuntimeError("Backend capture.bmp did not create BMP file")
|
|
102
109
|
|
|
103
110
|
self._bmp_to_image(str(bmp_path), str(out_path))
|
|
104
111
|
try:
|
|
@@ -112,7 +119,7 @@ class CaptureAdapter(CapturePort):
|
|
|
112
119
|
return str(out_path)
|
|
113
120
|
|
|
114
121
|
def screenshot_bytes(self) -> bytes:
|
|
115
|
-
data = self.backend.
|
|
122
|
+
data = self.backend.capture.bmp(path=None)
|
|
116
123
|
if data is None:
|
|
117
124
|
raise RuntimeError("Backend returned None for screenshot_bytes()")
|
|
118
125
|
return data
|
|
@@ -125,10 +132,10 @@ class CaptureAdapter(CapturePort):
|
|
|
125
132
|
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
126
133
|
|
|
127
134
|
bmp_path = out_path.with_suffix(".bmp")
|
|
128
|
-
self.backend.
|
|
135
|
+
self.backend.capture.bmp(str(bmp_path))
|
|
129
136
|
|
|
130
137
|
if not bmp_path.exists():
|
|
131
|
-
raise RuntimeError("Backend
|
|
138
|
+
raise RuntimeError("Backend capture.bmp did not create BMP file")
|
|
132
139
|
|
|
133
140
|
self._bmp_to_image(str(bmp_path), str(out_path))
|
|
134
141
|
|
|
@@ -4,14 +4,10 @@ Service interfaces for runtime components.
|
|
|
4
4
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
7
|
-
from mini_arcade_core.backend import Backend
|
|
8
|
-
|
|
9
7
|
|
|
10
8
|
class CapturePort:
|
|
11
9
|
"""Interface for frame capture operations."""
|
|
12
10
|
|
|
13
|
-
backend: Backend
|
|
14
|
-
|
|
15
11
|
def screenshot(self, label: str | None = None) -> str:
|
|
16
12
|
"""
|
|
17
13
|
Capture the current frame.
|
|
@@ -9,12 +9,14 @@ from dataclasses import dataclass
|
|
|
9
9
|
from typing import TYPE_CHECKING
|
|
10
10
|
|
|
11
11
|
if TYPE_CHECKING:
|
|
12
|
+
from mini_arcade_core.engine.cheats import CheatManager
|
|
12
13
|
from mini_arcade_core.engine.commands import CommandQueue
|
|
13
|
-
from mini_arcade_core.engine.game import Game, GameConfig
|
|
14
|
-
from mini_arcade_core.
|
|
14
|
+
from mini_arcade_core.engine.game import Game, GameConfig
|
|
15
|
+
from mini_arcade_core.engine.gameplay_settings import GamePlaySettings
|
|
15
16
|
from mini_arcade_core.runtime.services import RuntimeServices
|
|
16
17
|
|
|
17
18
|
|
|
19
|
+
# TODO: Remove cheats and command_queue from here later if unused.
|
|
18
20
|
@dataclass(frozen=True)
|
|
19
21
|
class RuntimeContext:
|
|
20
22
|
"""
|
|
@@ -22,14 +24,14 @@ class RuntimeContext:
|
|
|
22
24
|
|
|
23
25
|
:ivar services (RuntimeServices): Runtime services.
|
|
24
26
|
:ivar config (GameConfig): Game configuration.
|
|
25
|
-
:ivar settings (
|
|
27
|
+
:ivar settings (GamePlaySettings): Game settings.
|
|
26
28
|
:ivar command_queue (CommandQueue | None): Optional command queue.
|
|
27
29
|
:ivar cheats (CheatManager | None): Optional cheat manager.
|
|
28
30
|
"""
|
|
29
31
|
|
|
30
32
|
services: RuntimeServices
|
|
31
33
|
config: GameConfig
|
|
32
|
-
settings:
|
|
34
|
+
settings: GamePlaySettings
|
|
33
35
|
command_queue: CommandQueue | None = None
|
|
34
36
|
cheats: CheatManager | None = None
|
|
35
37
|
|
|
@@ -48,6 +50,6 @@ class RuntimeContext:
|
|
|
48
50
|
services=game_entity.services,
|
|
49
51
|
config=game_entity.config,
|
|
50
52
|
settings=game_entity.settings,
|
|
51
|
-
command_queue=game_entity.command_queue,
|
|
52
|
-
cheats=game_entity.
|
|
53
|
+
command_queue=game_entity.managers.command_queue,
|
|
54
|
+
cheats=game_entity.managers.cheats,
|
|
53
55
|
)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Scene query adapter implementation.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import Sequence
|
|
9
|
+
|
|
10
|
+
from mini_arcade_core.engine.scenes.models import SceneEntry
|
|
11
|
+
from mini_arcade_core.engine.scenes.scene_manager import SceneAdapter
|
|
12
|
+
from mini_arcade_core.runtime.scene.scene_query_port import SceneQueryPort
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class SceneQueryAdapter(SceneQueryPort):
|
|
17
|
+
"""Adapter that exposes a read-only view of the SceneAdapter manager."""
|
|
18
|
+
|
|
19
|
+
_scenes: SceneAdapter
|
|
20
|
+
|
|
21
|
+
def visible_entries(self) -> Sequence[SceneEntry]:
|
|
22
|
+
return list(self._scenes.visible_entries())
|
|
23
|
+
|
|
24
|
+
def input_entry(self) -> SceneEntry | None:
|
|
25
|
+
return self._scenes.input_entry()
|
|
26
|
+
|
|
27
|
+
def stack_summary(self) -> list[str]:
|
|
28
|
+
out: list[str] = []
|
|
29
|
+
for e in self._scenes.visible_entries():
|
|
30
|
+
out.append(f"{e.scene_id} overlay={e.is_overlay}")
|
|
31
|
+
return out
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Scene query port protocol.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import Protocol, Sequence, runtime_checkable
|
|
8
|
+
|
|
9
|
+
from mini_arcade_core.engine.scenes.models import SceneEntry
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@runtime_checkable
|
|
13
|
+
class SceneQueryPort(Protocol):
|
|
14
|
+
"""Read-only queries over the engine scene stack."""
|
|
15
|
+
|
|
16
|
+
def visible_entries(self) -> Sequence[SceneEntry]:
|
|
17
|
+
"""
|
|
18
|
+
Scenes that should be rendered (policy-aware).
|
|
19
|
+
|
|
20
|
+
:return: Sequence of SceneEntry instances that are visible.
|
|
21
|
+
:rtype: Sequence[SceneEntry]
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def input_entry(self) -> SceneEntry | None:
|
|
25
|
+
"""
|
|
26
|
+
The scene that currently receives input (top-most eligible).
|
|
27
|
+
|
|
28
|
+
:return: SceneEntry that receives input, or None if stack is empty.
|
|
29
|
+
:rtype: SceneEntry | None
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def stack_summary(self) -> list[str]:
|
|
33
|
+
"""
|
|
34
|
+
Convenience: human-readable stack lines for debug overlays.
|
|
35
|
+
|
|
36
|
+
:return: List of strings summarizing the scene stack.
|
|
37
|
+
:rtype: list[str]
|
|
38
|
+
"""
|
|
@@ -11,7 +11,7 @@ from mini_arcade_core.runtime.capture.capture_port import CapturePort
|
|
|
11
11
|
from mini_arcade_core.runtime.file.file_port import FilePort
|
|
12
12
|
from mini_arcade_core.runtime.input.input_port import InputPort
|
|
13
13
|
from mini_arcade_core.runtime.render.render_port import RenderServicePort
|
|
14
|
-
from mini_arcade_core.runtime.scene.
|
|
14
|
+
from mini_arcade_core.runtime.scene.scene_query_port import SceneQueryPort
|
|
15
15
|
from mini_arcade_core.runtime.window.window_port import WindowPort
|
|
16
16
|
|
|
17
17
|
|
|
@@ -26,12 +26,13 @@ class RuntimeServices:
|
|
|
26
26
|
:ivar files (FilePort): File service port.
|
|
27
27
|
:ivar capture (CapturePort): Capture service port.
|
|
28
28
|
:ivar input (InputPort): Input handling service port.
|
|
29
|
+
:ivar render (RenderServicePort): Rendering service port.
|
|
29
30
|
"""
|
|
30
31
|
|
|
31
32
|
window: WindowPort
|
|
32
|
-
scenes: ScenePort
|
|
33
33
|
audio: AudioPort
|
|
34
34
|
files: FilePort
|
|
35
35
|
capture: CapturePort
|
|
36
36
|
input: InputPort
|
|
37
37
|
render: RenderServicePort
|
|
38
|
+
scenes: SceneQueryPort
|
|
@@ -4,14 +4,14 @@ Module providing runtime adapters for window and scene management.
|
|
|
4
4
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
7
|
-
from
|
|
8
|
-
|
|
7
|
+
from mini_arcade_core.backend import Backend
|
|
9
8
|
from mini_arcade_core.engine.render.viewport import (
|
|
10
9
|
Viewport,
|
|
11
10
|
ViewportMode,
|
|
12
11
|
ViewportState,
|
|
13
12
|
)
|
|
14
13
|
from mini_arcade_core.runtime.window.window_port import WindowPort
|
|
14
|
+
from mini_arcade_core.utils import logger
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
class WindowAdapter(WindowPort):
|
|
@@ -19,71 +19,73 @@ class WindowAdapter(WindowPort):
|
|
|
19
19
|
Manages multiple game windows (not implemented).
|
|
20
20
|
"""
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
self.backend = backend
|
|
24
|
-
self.window_settings = window_settings
|
|
25
|
-
|
|
26
|
-
self._initialized = False
|
|
22
|
+
_drawable_size: tuple[int, int]
|
|
27
23
|
|
|
28
|
-
|
|
29
|
-
|
|
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)
|
|
24
|
+
def __init__(self, backend: Backend):
|
|
25
|
+
self.backend = backend
|
|
38
26
|
|
|
39
|
-
|
|
40
|
-
width = int(width)
|
|
41
|
-
height = int(height)
|
|
42
|
-
self.size = (width, height)
|
|
27
|
+
self.backend.init()
|
|
43
28
|
|
|
44
|
-
self.
|
|
45
|
-
self.
|
|
29
|
+
w, h = self.backend.window.size()
|
|
30
|
+
self.size = (w, h)
|
|
31
|
+
self._initialized = True
|
|
46
32
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
else:
|
|
51
|
-
self.backend.resize_window(width, height)
|
|
33
|
+
self._viewport = Viewport(w, h, mode=ViewportMode.FIT)
|
|
34
|
+
self._viewport.resize(w, h)
|
|
35
|
+
self._apply_viewport_transform()
|
|
52
36
|
|
|
53
|
-
|
|
37
|
+
def _apply_viewport_transform(self):
|
|
38
|
+
s = self._viewport.state
|
|
39
|
+
# This is the missing link in the new backend design:
|
|
40
|
+
self.backend.set_viewport_transform(s.offset_x, s.offset_y, s.scale)
|
|
54
41
|
|
|
55
42
|
def set_virtual_resolution(self, width: int, height: int):
|
|
56
43
|
self._viewport.set_virtual_resolution(int(width), int(height))
|
|
57
|
-
|
|
58
|
-
w, h
|
|
44
|
+
w, h = self.backend.window.size()
|
|
45
|
+
self.size = (w, h)
|
|
59
46
|
self._viewport.resize(w, h)
|
|
47
|
+
self._apply_viewport_transform()
|
|
60
48
|
|
|
61
49
|
def set_viewport_mode(self, mode: ViewportMode):
|
|
62
50
|
self._viewport.set_mode(mode)
|
|
51
|
+
# mode change affects scale/offset
|
|
52
|
+
if self._viewport.state is not None:
|
|
53
|
+
self._apply_viewport_transform()
|
|
63
54
|
|
|
64
55
|
def get_viewport(self) -> ViewportState:
|
|
65
56
|
return self._viewport.state
|
|
66
57
|
|
|
67
58
|
def screen_to_virtual(self, x: float, y: float) -> tuple[float, float]:
|
|
68
|
-
|
|
59
|
+
logical_w, logical_h = self.size
|
|
60
|
+
drawable_w, drawable_h = self._drawable_size
|
|
61
|
+
|
|
62
|
+
sx = drawable_w / logical_w if logical_w else 1.0
|
|
63
|
+
sy = drawable_h / logical_h if logical_h else 1.0
|
|
64
|
+
|
|
65
|
+
return self._viewport.screen_to_virtual(x * sx, y * sy)
|
|
69
66
|
|
|
70
67
|
def set_title(self, title):
|
|
71
|
-
self.backend.
|
|
68
|
+
self.backend.window.set_title(title)
|
|
72
69
|
|
|
73
70
|
def set_clear_color(self, r, g, b):
|
|
74
|
-
self.backend.set_clear_color(r, g, b)
|
|
71
|
+
self.backend.render.set_clear_color(r, g, b)
|
|
75
72
|
|
|
76
73
|
def on_window_resized(self, width: int, height: int):
|
|
77
74
|
logger.debug(f"Window resized event: {width}x{height}")
|
|
78
|
-
width = int(width)
|
|
79
|
-
height = int(height)
|
|
80
75
|
|
|
81
|
-
#
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
76
|
+
# logical
|
|
77
|
+
logical_w, logical_h = int(width), int(height)
|
|
78
|
+
|
|
79
|
+
# drawable (pixel)
|
|
80
|
+
drawable_w, drawable_h = self.backend.window.drawable_size()
|
|
81
|
+
|
|
82
|
+
# store both if useful
|
|
83
|
+
self.size = (logical_w, logical_h)
|
|
84
|
+
self._drawable_size = (drawable_w, drawable_h)
|
|
85
85
|
|
|
86
|
-
|
|
86
|
+
# viewport should match what renderer draws to:
|
|
87
|
+
self._viewport.resize(drawable_w, drawable_h)
|
|
88
|
+
self._apply_viewport_transform()
|
|
87
89
|
|
|
88
90
|
def get_virtual_size(self) -> tuple[int, int]:
|
|
89
91
|
s = self.get_viewport()
|
|
@@ -4,28 +4,14 @@ Service interfaces for runtime components.
|
|
|
4
4
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
7
|
-
from
|
|
7
|
+
from typing import Protocol
|
|
8
|
+
|
|
8
9
|
from mini_arcade_core.engine.render.viewport import ViewportMode, ViewportState
|
|
9
10
|
|
|
10
11
|
|
|
11
|
-
class WindowPort:
|
|
12
|
+
class WindowPort(Protocol):
|
|
12
13
|
"""Interface for window-related operations."""
|
|
13
14
|
|
|
14
|
-
backend: Backend
|
|
15
|
-
size: tuple[int, int]
|
|
16
|
-
window_settings_cls: WindowSettings
|
|
17
|
-
|
|
18
|
-
def set_window_size(self, width: int, height: int):
|
|
19
|
-
"""
|
|
20
|
-
Set the size of the window.
|
|
21
|
-
|
|
22
|
-
:param width: Width in pixels.
|
|
23
|
-
:type width: int
|
|
24
|
-
|
|
25
|
-
:param height: Height in pixels.
|
|
26
|
-
:type height: int
|
|
27
|
-
"""
|
|
28
|
-
|
|
29
15
|
def set_viewport_mode(self, mode: ViewportMode):
|
|
30
16
|
"""
|
|
31
17
|
Set the viewport mode for rendering.
|