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
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Service interfaces for runtime components.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from mini_arcade_core.backend import Backend
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class CapturePort:
|
|
11
|
+
"""Interface for frame capture operations."""
|
|
12
|
+
|
|
13
|
+
backend: Backend
|
|
14
|
+
|
|
15
|
+
def screenshot(self, label: str | None = None) -> str:
|
|
16
|
+
"""
|
|
17
|
+
Capture the current frame.
|
|
18
|
+
|
|
19
|
+
:param label: Optional label for the screenshot file.
|
|
20
|
+
:type label: str | None
|
|
21
|
+
|
|
22
|
+
:return: Screenshot file path.
|
|
23
|
+
:rtype: str
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def screenshot_bytes(self) -> bytes | None:
|
|
27
|
+
"""
|
|
28
|
+
Capture the current frame and return it as bytes.
|
|
29
|
+
|
|
30
|
+
:return: Screenshot data as bytes.
|
|
31
|
+
:rtype: bytes | None
|
|
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
|
+
"""
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Runtime context module.
|
|
3
|
+
Defines the RuntimeContext dataclass for game runtime context.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from mini_arcade_core.engine.commands import CommandQueue
|
|
13
|
+
from mini_arcade_core.engine.game import Game, GameConfig, GameSettings
|
|
14
|
+
from mini_arcade_core.managers.cheats import CheatManager
|
|
15
|
+
from mini_arcade_core.runtime.services import RuntimeServices
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class RuntimeContext:
|
|
20
|
+
"""
|
|
21
|
+
Context for the game runtime.
|
|
22
|
+
|
|
23
|
+
:ivar services (RuntimeServices): Runtime services.
|
|
24
|
+
:ivar config (GameConfig): Game configuration.
|
|
25
|
+
:ivar settings (GameSettings): Game settings.
|
|
26
|
+
:ivar command_queue (CommandQueue | None): Optional command queue.
|
|
27
|
+
:ivar cheats (CheatManager | None): Optional cheat manager.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
services: RuntimeServices
|
|
31
|
+
config: GameConfig
|
|
32
|
+
settings: GameSettings
|
|
33
|
+
command_queue: CommandQueue | None = None
|
|
34
|
+
cheats: CheatManager | None = None
|
|
35
|
+
|
|
36
|
+
@staticmethod
|
|
37
|
+
def from_game(game_entity: Game) -> "RuntimeContext":
|
|
38
|
+
"""
|
|
39
|
+
Create a RuntimeContext from a Game entity.
|
|
40
|
+
|
|
41
|
+
:param game_entity: Game entity to extract context from.
|
|
42
|
+
:type game_entity: Game
|
|
43
|
+
|
|
44
|
+
:return: RuntimeContext instance.
|
|
45
|
+
:rtype: RuntimeContext
|
|
46
|
+
"""
|
|
47
|
+
return RuntimeContext(
|
|
48
|
+
services=game_entity.services,
|
|
49
|
+
config=game_entity.config,
|
|
50
|
+
settings=game_entity.settings,
|
|
51
|
+
command_queue=game_entity.command_queue,
|
|
52
|
+
cheats=game_entity.cheat_manager,
|
|
53
|
+
)
|
|
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.file.file_port import FilePort
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class LocalFilesAdapter(FilePort):
|
|
11
|
+
"""Adapter for local file operations."""
|
|
12
|
+
|
|
13
|
+
def write_text(self, path: str, text: str):
|
|
14
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
15
|
+
f.write(text)
|
|
16
|
+
|
|
17
|
+
def write_bytes(self, path: str, data: bytes):
|
|
18
|
+
with open(path, "wb") as f:
|
|
19
|
+
f.write(data)
|
|
20
|
+
f.write(data)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Service interfaces for runtime components.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class FilePort:
|
|
9
|
+
"""Interface for file operations."""
|
|
10
|
+
|
|
11
|
+
def write_bytes(self, path: str, data: bytes):
|
|
12
|
+
"""
|
|
13
|
+
Write bytes to a file.
|
|
14
|
+
|
|
15
|
+
:param path: Path to the file.
|
|
16
|
+
:type path: str
|
|
17
|
+
|
|
18
|
+
:param data: Data to write.
|
|
19
|
+
:type data: bytes
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def write_text(self, path: str, text: str):
|
|
23
|
+
"""
|
|
24
|
+
Write text to a file.
|
|
25
|
+
|
|
26
|
+
:param path: Path to the file.
|
|
27
|
+
:type path: str
|
|
28
|
+
|
|
29
|
+
:param text: Text to write.
|
|
30
|
+
:type text: str
|
|
31
|
+
"""
|
|
File without changes
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module providing runtime adapters for window and scene management.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
|
|
9
|
+
from mini_arcade_core.backend.events import Event, EventType
|
|
10
|
+
from mini_arcade_core.backend.keys import Key
|
|
11
|
+
from mini_arcade_core.runtime.input.input_port import InputPort
|
|
12
|
+
from mini_arcade_core.runtime.input_frame import InputFrame
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class InputAdapter(InputPort):
|
|
17
|
+
"""Adapter for processing input events."""
|
|
18
|
+
|
|
19
|
+
_down: set[Key] = field(default_factory=set)
|
|
20
|
+
|
|
21
|
+
def build(
|
|
22
|
+
self, events: list[Event], frame_index: int, dt: float
|
|
23
|
+
) -> InputFrame:
|
|
24
|
+
pressed: set[Key] = set()
|
|
25
|
+
released: set[Key] = set()
|
|
26
|
+
quit_req = False
|
|
27
|
+
|
|
28
|
+
for ev in events:
|
|
29
|
+
if ev.type == EventType.QUIT:
|
|
30
|
+
quit_req = True
|
|
31
|
+
|
|
32
|
+
elif ev.type == EventType.KEYDOWN and ev.key is not None:
|
|
33
|
+
if ev.key not in self._down:
|
|
34
|
+
pressed.add(ev.key)
|
|
35
|
+
self._down.add(ev.key)
|
|
36
|
+
|
|
37
|
+
elif ev.type == EventType.KEYUP and ev.key is not None:
|
|
38
|
+
if ev.key in self._down:
|
|
39
|
+
self._down.remove(ev.key)
|
|
40
|
+
released.add(ev.key)
|
|
41
|
+
|
|
42
|
+
return InputFrame(
|
|
43
|
+
frame_index=frame_index,
|
|
44
|
+
dt=dt,
|
|
45
|
+
keys_down=frozenset(self._down),
|
|
46
|
+
keys_pressed=frozenset(pressed),
|
|
47
|
+
keys_released=frozenset(released),
|
|
48
|
+
quit=quit_req,
|
|
49
|
+
)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Service interfaces for runtime components.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from mini_arcade_core.backend.events import Event
|
|
8
|
+
from mini_arcade_core.runtime.input_frame import InputFrame
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class InputPort:
|
|
12
|
+
"""Interface for input handling operations."""
|
|
13
|
+
|
|
14
|
+
def build(
|
|
15
|
+
self, events: list[Event], frame_index: int, dt: float
|
|
16
|
+
) -> InputFrame:
|
|
17
|
+
"""
|
|
18
|
+
Build an InputFrame from the given events.
|
|
19
|
+
|
|
20
|
+
:param events: List of input events.
|
|
21
|
+
:type events: list[Event]
|
|
22
|
+
|
|
23
|
+
:param frame_index: Current frame index.
|
|
24
|
+
:type frame_index: int
|
|
25
|
+
|
|
26
|
+
:param dt: Delta time since last frame.
|
|
27
|
+
:type dt: float
|
|
28
|
+
|
|
29
|
+
:return: Constructed InputFrame.
|
|
30
|
+
:rtype: InputFrame
|
|
31
|
+
"""
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Input frame data structure for capturing input state per frame.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from typing import Dict, FrozenSet, Tuple
|
|
9
|
+
|
|
10
|
+
from mini_arcade_core.backend.keys import Key
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class ButtonState:
|
|
15
|
+
"""
|
|
16
|
+
State of a single action button.
|
|
17
|
+
|
|
18
|
+
:ivar down (bool): Whether the button is currently held down.
|
|
19
|
+
:ivar pressed (bool): Whether the button was pressed this frame.
|
|
20
|
+
:ivar released (bool): Whether the button was released this frame.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
down: bool
|
|
24
|
+
pressed: bool
|
|
25
|
+
released: bool
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# TODO: Solve too-many-instance-attributes warning later
|
|
29
|
+
# Justification: This data class needs multiple attributes to capture input state.
|
|
30
|
+
# pylint: disable=too-many-instance-attributes
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class InputFrame:
|
|
33
|
+
"""
|
|
34
|
+
Snapshot of input state for a single frame.
|
|
35
|
+
|
|
36
|
+
:ivar frame_index (int): Sequential index of the frame.
|
|
37
|
+
:ivar dt (float): Delta time since the last frame in seconds.
|
|
38
|
+
:ivar keys_down (FrozenSet[Key]): Set of currently held down keys.
|
|
39
|
+
:ivar keys_pressed (FrozenSet[Key]): Set of keys pressed this frame.
|
|
40
|
+
:ivar keys_released (FrozenSet[Key]): Set of keys released this frame.
|
|
41
|
+
:ivar buttons (Dict[str, ButtonState]): Mapping of action button names to their states.
|
|
42
|
+
:ivar axes (Dict[str, float]): Mapping of axis names to their float values.
|
|
43
|
+
:ivar mouse_pos (Tuple[int, int]): Current mouse position (x, y).
|
|
44
|
+
:ivar mouse_delta (Tuple[int, int]): Mouse movement delta (dx, dy)
|
|
45
|
+
:ivar text_input (str): Text input entered this frame.
|
|
46
|
+
:ivar quit (bool): Whether a quit request was made this frame.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
frame_index: int
|
|
50
|
+
dt: float
|
|
51
|
+
|
|
52
|
+
# Physical keys (device-level snapshot) – supports cheats & replay
|
|
53
|
+
keys_down: FrozenSet[Key] = frozenset()
|
|
54
|
+
keys_pressed: FrozenSet[Key] = frozenset()
|
|
55
|
+
keys_released: FrozenSet[Key] = frozenset()
|
|
56
|
+
|
|
57
|
+
# action buttons (jump, confirm, pause, etc.)
|
|
58
|
+
buttons: Dict[str, ButtonState] = field(default_factory=dict)
|
|
59
|
+
# axes (move_y, aim_x, etc.)
|
|
60
|
+
axes: Dict[str, float] = field(default_factory=dict)
|
|
61
|
+
|
|
62
|
+
# optional: pass through for UI needs
|
|
63
|
+
mouse_pos: Tuple[int, int] = (0, 0)
|
|
64
|
+
mouse_delta: Tuple[int, int] = (0, 0)
|
|
65
|
+
text_input: str = ""
|
|
66
|
+
|
|
67
|
+
# Window/OS quit request
|
|
68
|
+
quit: bool = False
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# pylint: enable=too-many-instance-attributes
|
|
File without changes
|
|
@@ -0,0 +1,97 @@
|
|
|
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.context import RuntimeContext
|
|
8
|
+
from mini_arcade_core.runtime.scene.scene_port import (
|
|
9
|
+
SceneEntry,
|
|
10
|
+
ScenePolicy,
|
|
11
|
+
ScenePort,
|
|
12
|
+
StackItem,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SceneAdapter(ScenePort):
|
|
17
|
+
"""
|
|
18
|
+
Manages multiple scenes (not implemented).
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, registry, game):
|
|
22
|
+
self._registry = registry
|
|
23
|
+
self._stack = []
|
|
24
|
+
self._game = game
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def current_scene(self):
|
|
28
|
+
return self._stack[-1].entry.scene if self._stack else None
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def visible_stack(self):
|
|
32
|
+
return [e.scene for e in self.visible_entries()]
|
|
33
|
+
|
|
34
|
+
def change(self, scene_id):
|
|
35
|
+
self.clean()
|
|
36
|
+
self.push(scene_id, as_overlay=False)
|
|
37
|
+
|
|
38
|
+
def push(
|
|
39
|
+
self,
|
|
40
|
+
scene_id,
|
|
41
|
+
*,
|
|
42
|
+
as_overlay=False,
|
|
43
|
+
policy=None,
|
|
44
|
+
):
|
|
45
|
+
# default policy based on overlay vs base
|
|
46
|
+
if policy is None:
|
|
47
|
+
# base scenes: do not block anything by default
|
|
48
|
+
policy = ScenePolicy()
|
|
49
|
+
runtime_context = RuntimeContext.from_game(self._game)
|
|
50
|
+
scene = self._registry.create(
|
|
51
|
+
scene_id, runtime_context
|
|
52
|
+
) # or whatever your factory call is
|
|
53
|
+
scene.on_enter()
|
|
54
|
+
|
|
55
|
+
entry = SceneEntry(
|
|
56
|
+
scene_id=scene_id,
|
|
57
|
+
scene=scene,
|
|
58
|
+
is_overlay=as_overlay,
|
|
59
|
+
policy=policy,
|
|
60
|
+
)
|
|
61
|
+
self._stack.append(StackItem(entry=entry))
|
|
62
|
+
|
|
63
|
+
def pop(self):
|
|
64
|
+
if not self._stack:
|
|
65
|
+
return
|
|
66
|
+
item = self._stack.pop()
|
|
67
|
+
item.entry.scene.on_exit()
|
|
68
|
+
|
|
69
|
+
def clean(self):
|
|
70
|
+
while self._stack:
|
|
71
|
+
self.pop()
|
|
72
|
+
|
|
73
|
+
def quit(self):
|
|
74
|
+
self._game.quit()
|
|
75
|
+
|
|
76
|
+
def visible_entries(self):
|
|
77
|
+
entries = [i.entry for i in self._stack]
|
|
78
|
+
# find highest opaque from top down; render starting there
|
|
79
|
+
for idx in range(len(entries) - 1, -1, -1):
|
|
80
|
+
if entries[idx].policy.is_opaque:
|
|
81
|
+
return entries[idx:]
|
|
82
|
+
return entries
|
|
83
|
+
|
|
84
|
+
def update_entries(self):
|
|
85
|
+
vis = self.visible_entries()
|
|
86
|
+
if not vis:
|
|
87
|
+
return []
|
|
88
|
+
out = []
|
|
89
|
+
for entry in reversed(vis): # top->down
|
|
90
|
+
out.append(entry)
|
|
91
|
+
if entry.policy.blocks_update:
|
|
92
|
+
break
|
|
93
|
+
return list(reversed(out)) # bottom->top order
|
|
94
|
+
|
|
95
|
+
def input_entry(self):
|
|
96
|
+
vis = self.visible_entries()
|
|
97
|
+
return vis[-1] if vis else None
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Service interfaces for runtime components.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import TYPE_CHECKING, List
|
|
9
|
+
|
|
10
|
+
from mini_arcade_core.scenes.registry import SceneRegistry
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from mini_arcade_core.engine.game import Game
|
|
14
|
+
from mini_arcade_core.scenes.scene import Scene
|
|
15
|
+
from mini_arcade_core.sim.protocols import SimScene
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class ScenePolicy:
|
|
20
|
+
"""
|
|
21
|
+
Controls how a scene behaves in the scene stack.
|
|
22
|
+
|
|
23
|
+
blocks_update: if True, scenes below do not tick/update (pause modal)
|
|
24
|
+
blocks_input: if True, scenes below do not receive input
|
|
25
|
+
is_opaque: if True, scenes below are not rendered
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
blocks_update: bool = False
|
|
29
|
+
blocks_input: bool = False
|
|
30
|
+
is_opaque: bool = False
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True)
|
|
34
|
+
class SceneEntry:
|
|
35
|
+
"""
|
|
36
|
+
An entry in the scene stack.
|
|
37
|
+
|
|
38
|
+
:ivar scene_id (str): Identifier of the scene.
|
|
39
|
+
:ivar scene (Scene): The scene instance.
|
|
40
|
+
:ivar is_overlay (bool): Whether the scene is an overlay.
|
|
41
|
+
:ivar policy (ScenePolicy): The scene's policy.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
scene_id: str
|
|
45
|
+
scene: SimScene
|
|
46
|
+
is_overlay: bool
|
|
47
|
+
policy: ScenePolicy
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class StackItem:
|
|
52
|
+
"""
|
|
53
|
+
An item in the scene stack.
|
|
54
|
+
|
|
55
|
+
:ivar entry (SceneEntry): The scene entry.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
entry: SceneEntry
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class ScenePort:
|
|
62
|
+
"""Interface for scene management operations."""
|
|
63
|
+
|
|
64
|
+
_registry: SceneRegistry
|
|
65
|
+
_stack: List[StackItem]
|
|
66
|
+
_game: Game
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def current_scene(self) -> "SimScene | None":
|
|
70
|
+
"""
|
|
71
|
+
Get the currently active scene.
|
|
72
|
+
|
|
73
|
+
:return: The active Scene instance, or None if no scene is active.
|
|
74
|
+
:rtype: SimScene | None
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def visible_stack(self) -> List["SimScene"]:
|
|
79
|
+
"""
|
|
80
|
+
Return the list of scenes that should be drawn (base + overlays).
|
|
81
|
+
We draw from the top-most non-overlay scene upward.
|
|
82
|
+
|
|
83
|
+
:return: List of visible Scene instances.
|
|
84
|
+
:rtype: List[SimScene]
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
def change(self, scene_id: str):
|
|
88
|
+
"""
|
|
89
|
+
Change the current scene to the specified scene.
|
|
90
|
+
|
|
91
|
+
:param scene_id: Identifier of the scene to switch to.
|
|
92
|
+
:type scene_id: str
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
def push(self, scene_id: str, *, as_overlay: bool = False):
|
|
96
|
+
"""
|
|
97
|
+
Push a new scene onto the scene stack.
|
|
98
|
+
|
|
99
|
+
:param scene_id: Identifier of the scene to push.
|
|
100
|
+
:type scene_id: str
|
|
101
|
+
|
|
102
|
+
:param as_overlay: Whether to push the scene as an overlay.
|
|
103
|
+
:type as_overlay: bool
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
def pop(self) -> "Scene | None":
|
|
107
|
+
"""
|
|
108
|
+
Pop the current scene from the scene stack.
|
|
109
|
+
|
|
110
|
+
:return: The popped Scene instance, or None if the stack was empty.
|
|
111
|
+
:rtype: Scene | None
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
def clean(self):
|
|
115
|
+
"""
|
|
116
|
+
Clean up all scenes from the scene stack.
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
def quit(self):
|
|
120
|
+
"""
|
|
121
|
+
Quit the game
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
def visible_entries(self) -> list[SceneEntry]:
|
|
125
|
+
"""
|
|
126
|
+
Render from bottom->top unless an opaque entry exists; if so,
|
|
127
|
+
render only from that entry up.
|
|
128
|
+
|
|
129
|
+
:return: List of SceneEntry instances to render.
|
|
130
|
+
:rtype: list[SceneEntry]
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
def update_entries(self) -> list[SceneEntry]:
|
|
134
|
+
"""
|
|
135
|
+
Tick/update scenes considering blocks_update.
|
|
136
|
+
Typical: pause overlay blocks update below it.
|
|
137
|
+
|
|
138
|
+
:return: List of SceneEntry instances to update.
|
|
139
|
+
:rtype: list[SceneEntry]
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
def input_entry(self) -> SceneEntry | None:
|
|
143
|
+
"""
|
|
144
|
+
Who gets input this frame. If top blocks_input, only it receives input.
|
|
145
|
+
If not, top still gets input (v1 simple). Later you can allow fall-through.
|
|
146
|
+
|
|
147
|
+
:return: The SceneEntry that receives input, or None if no scenes are active.
|
|
148
|
+
:rtype: SceneEntry | None
|
|
149
|
+
"""
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Service container for runtime components.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
|
|
9
|
+
from mini_arcade_core.runtime.audio.audio_port import AudioPort
|
|
10
|
+
from mini_arcade_core.runtime.capture.capture_port import CapturePort
|
|
11
|
+
from mini_arcade_core.runtime.file.file_port import FilePort
|
|
12
|
+
from mini_arcade_core.runtime.input.input_port import InputPort
|
|
13
|
+
from mini_arcade_core.runtime.scene.scene_port import ScenePort
|
|
14
|
+
from mini_arcade_core.runtime.window.window_port import WindowPort
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class RuntimeServices:
|
|
19
|
+
"""
|
|
20
|
+
Container for runtime service ports.
|
|
21
|
+
|
|
22
|
+
:ivar window (WindowPort): Window service port.
|
|
23
|
+
:ivar scenes (ScenePort): Scene management service port.
|
|
24
|
+
:ivar audio (AudioPort): Audio service port.
|
|
25
|
+
:ivar files (FilePort): File service port.
|
|
26
|
+
:ivar capture (CapturePort): Capture service port.
|
|
27
|
+
:ivar input (InputPort): Input handling service port.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
window: WindowPort
|
|
31
|
+
scenes: ScenePort
|
|
32
|
+
audio: AudioPort
|
|
33
|
+
files: FilePort
|
|
34
|
+
capture: CapturePort
|
|
35
|
+
input: InputPort
|
|
File without changes
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module providing runtime adapters for window and scene management.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from venv import logger
|
|
8
|
+
|
|
9
|
+
from mini_arcade_core.engine.render.viewport import (
|
|
10
|
+
Viewport,
|
|
11
|
+
ViewportMode,
|
|
12
|
+
ViewportState,
|
|
13
|
+
)
|
|
14
|
+
from mini_arcade_core.runtime.window.window_port import WindowPort
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class WindowAdapter(WindowPort):
|
|
18
|
+
"""
|
|
19
|
+
Manages multiple game windows (not implemented).
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, backend, window_settings):
|
|
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)
|
|
38
|
+
|
|
39
|
+
def set_window_size(self, width, height):
|
|
40
|
+
width = int(width)
|
|
41
|
+
height = int(height)
|
|
42
|
+
self.size = (width, height)
|
|
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)
|
|
69
|
+
|
|
70
|
+
def set_title(self, title):
|
|
71
|
+
self.backend.set_window_title(title)
|
|
72
|
+
|
|
73
|
+
def set_clear_color(self, r, g, b):
|
|
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)
|