mini-arcade-native-backend 0.5.0__tar.gz → 0.6.0__tar.gz
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_native_backend-0.5.0 → mini_arcade_native_backend-0.6.0}/.github/workflows/ci.yml +1 -1
- {mini_arcade_native_backend-0.5.0 → mini_arcade_native_backend-0.6.0}/.github/workflows/release-publish.yml +1 -1
- {mini_arcade_native_backend-0.5.0 → mini_arcade_native_backend-0.6.0}/CHANGELOG.md +36 -0
- {mini_arcade_native_backend-0.5.0 → mini_arcade_native_backend-0.6.0}/CMakeLists.txt +2 -1
- {mini_arcade_native_backend-0.5.0 → mini_arcade_native_backend-0.6.0}/PKG-INFO +2 -2
- {mini_arcade_native_backend-0.5.0 → mini_arcade_native_backend-0.6.0}/cpp/bindings.cpp +39 -1
- {mini_arcade_native_backend-0.5.0 → mini_arcade_native_backend-0.6.0}/cpp/engine.cpp +197 -16
- {mini_arcade_native_backend-0.5.0 → mini_arcade_native_backend-0.6.0}/cpp/engine.h +32 -1
- {mini_arcade_native_backend-0.5.0 → mini_arcade_native_backend-0.6.0}/poetry.lock +4 -4
- {mini_arcade_native_backend-0.5.0 → mini_arcade_native_backend-0.6.0}/pyproject.toml +2 -2
- {mini_arcade_native_backend-0.5.0 → mini_arcade_native_backend-0.6.0}/src/mini_arcade_native_backend/__init__.py +170 -26
- {mini_arcade_native_backend-0.5.0 → mini_arcade_native_backend-0.6.0}/.github/workflows/create-release-branch.yml +0 -0
- {mini_arcade_native_backend-0.5.0 → mini_arcade_native_backend-0.6.0}/.github/workflows/release-finalize.yml +0 -0
- {mini_arcade_native_backend-0.5.0 → mini_arcade_native_backend-0.6.0}/.gitignore +0 -0
- {mini_arcade_native_backend-0.5.0 → mini_arcade_native_backend-0.6.0}/.vscode/settings.json +0 -0
- {mini_arcade_native_backend-0.5.0 → mini_arcade_native_backend-0.6.0}/LICENSE +0 -0
- {mini_arcade_native_backend-0.5.0 → mini_arcade_native_backend-0.6.0}/README.md +0 -0
- {mini_arcade_native_backend-0.5.0 → mini_arcade_native_backend-0.6.0}/examples/native_backend_demo.py +0 -0
- {mini_arcade_native_backend-0.5.0 → mini_arcade_native_backend-0.6.0}/poetry.toml +0 -0
- {mini_arcade_native_backend-0.5.0 → mini_arcade_native_backend-0.6.0}/tests/test_init.py +0 -0
{mini_arcade_native_backend-0.5.0 → mini_arcade_native_backend-0.6.0}/.github/workflows/ci.yml
RENAMED
|
@@ -19,7 +19,7 @@ jobs:
|
|
|
19
19
|
with:
|
|
20
20
|
project-name: "mini-arcade-native-backend"
|
|
21
21
|
lint-path: "src/mini_arcade_native_backend"
|
|
22
|
-
apt-deps: "libsdl2-dev libsdl2-ttf-dev"
|
|
22
|
+
apt-deps: "libsdl2-dev libsdl2-ttf-dev libsdl2-mixer-dev"
|
|
23
23
|
secrets:
|
|
24
24
|
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
|
|
25
25
|
SLACK_CHANNEL_ID: ${{ secrets.SLACK_CHANNEL_ID }}
|
|
@@ -17,7 +17,7 @@ jobs:
|
|
|
17
17
|
with:
|
|
18
18
|
release_branch: ${{ inputs.release_branch }}
|
|
19
19
|
version_file: "pyproject.toml"
|
|
20
|
-
apt-deps: "libsdl2-dev libsdl2-ttf-dev"
|
|
20
|
+
apt-deps: "libsdl2-dev libsdl2-ttf-dev libsdl2-mixer-dev"
|
|
21
21
|
poetry-build-args: "-f sdist"
|
|
22
22
|
build_wheels: true
|
|
23
23
|
secrets:
|
|
@@ -6,6 +6,42 @@ This project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.6.0] - 2026-01-23
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- add draw_line method to Engine and bind it in Python
|
|
13
|
+
|
|
14
|
+
### Other
|
|
15
|
+
- Merge release/0.5 into develop
|
|
16
|
+
|
|
17
|
+
## [0.5.3] - 2026-01-23
|
|
18
|
+
|
|
19
|
+
- Internal changes only.
|
|
20
|
+
|
|
21
|
+
## [0.5.2] - 2026-01-23
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
- add window resizing and clipping functions to Engine and update bindings
|
|
25
|
+
- add audio management functions to Engine and integrate with NativeBackend
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
- update apt dependencies to include libsdl2-mixer-dev for CI and release workflows
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
- add TODO for backend interface refactoring to improve structure
|
|
32
|
+
|
|
33
|
+
## [0.5.1] - 2026-01-23
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
- add window resizing and clipping functions to Engine and update bindings
|
|
37
|
+
- add audio management functions to Engine and integrate with NativeBackend
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
- update apt dependencies to include libsdl2-mixer-dev for CI and release workflows
|
|
41
|
+
|
|
42
|
+
### Changed
|
|
43
|
+
- add TODO for backend interface refactoring to improve structure
|
|
44
|
+
|
|
9
45
|
## [0.5.0] - 2026-01-21
|
|
10
46
|
|
|
11
47
|
### Added
|
|
@@ -10,6 +10,7 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
|
|
10
10
|
find_package(pybind11 CONFIG REQUIRED)
|
|
11
11
|
find_package(SDL2 CONFIG REQUIRED)
|
|
12
12
|
find_package(SDL2_ttf CONFIG REQUIRED)
|
|
13
|
+
find_package(SDL2_mixer CONFIG REQUIRED)
|
|
13
14
|
|
|
14
15
|
set(TARGET_NAME _native)
|
|
15
16
|
|
|
@@ -19,7 +20,7 @@ pybind11_add_module(${TARGET_NAME}
|
|
|
19
20
|
cpp/engine.cpp
|
|
20
21
|
)
|
|
21
22
|
|
|
22
|
-
target_link_libraries(${TARGET_NAME} PRIVATE SDL2::SDL2 SDL2_ttf::SDL2_ttf)
|
|
23
|
+
target_link_libraries(${TARGET_NAME} PRIVATE SDL2::SDL2 SDL2_ttf::SDL2_ttf SDL2_mixer::SDL2_mixer)
|
|
23
24
|
|
|
24
25
|
# Install the compiled extension into the Python package directory
|
|
25
26
|
# so it ends up as mini_arcade_native_backend/_native.*.pyd
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: mini-arcade-native-backend
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Native SDL2 backend for mini-arcade-core using SDL2 + pybind11.
|
|
5
5
|
Author-Email: Santiago Rincon <rincores@gmail.com>
|
|
6
6
|
License: Copyright (c) 2025 Santiago Rincón
|
|
@@ -24,7 +24,7 @@ License: Copyright (c) 2025 Santiago Rincón
|
|
|
24
24
|
SOFTWARE.
|
|
25
25
|
|
|
26
26
|
Requires-Python: <3.12,>=3.9
|
|
27
|
-
Requires-Dist: mini-arcade-core~=1.
|
|
27
|
+
Requires-Dist: mini-arcade-core~=1.1
|
|
28
28
|
Provides-Extra: dev
|
|
29
29
|
Requires-Dist: pytest~=8.3; extra == "dev"
|
|
30
30
|
Requires-Dist: pytest-cov~=6.0; extra == "dev"
|
|
@@ -89,5 +89,43 @@ PYBIND11_MODULE(_native, m) {
|
|
|
89
89
|
&mini::Engine::measure_text,
|
|
90
90
|
py::arg("text"),
|
|
91
91
|
py::arg("font_id") = -1
|
|
92
|
-
)
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
.def("init_audio", &mini::Engine::init_audio,
|
|
95
|
+
py::arg("frequency") = 44100,
|
|
96
|
+
py::arg("channels") = 2,
|
|
97
|
+
py::arg("chunk_size") = 2048)
|
|
98
|
+
|
|
99
|
+
.def("shutdown_audio", &mini::Engine::shutdown_audio)
|
|
100
|
+
|
|
101
|
+
.def("load_sound", &mini::Engine::load_sound,
|
|
102
|
+
py::arg("sound_id"),
|
|
103
|
+
py::arg("path"))
|
|
104
|
+
|
|
105
|
+
.def("play_sound", &mini::Engine::play_sound,
|
|
106
|
+
py::arg("sound_id"),
|
|
107
|
+
py::arg("loops") = 0)
|
|
108
|
+
|
|
109
|
+
.def("set_master_volume", &mini::Engine::set_master_volume,
|
|
110
|
+
py::arg("volume"))
|
|
111
|
+
|
|
112
|
+
.def("set_sound_volume", &mini::Engine::set_sound_volume,
|
|
113
|
+
py::arg("sound_id"),
|
|
114
|
+
py::arg("volume"))
|
|
115
|
+
|
|
116
|
+
.def("stop_all_sounds", &mini::Engine::stop_all_sounds)
|
|
117
|
+
.def("resize_window", &mini::Engine::resize_window,
|
|
118
|
+
py::arg("width"),
|
|
119
|
+
py::arg("height"))
|
|
120
|
+
.def("set_clip_rect", &mini::Engine::set_clip_rect,
|
|
121
|
+
py::arg("x"),
|
|
122
|
+
py::arg("y"),
|
|
123
|
+
py::arg("w"),
|
|
124
|
+
py::arg("h"))
|
|
125
|
+
.def("clear_clip_rect", &mini::Engine::clear_clip_rect)
|
|
126
|
+
.def("draw_line", &mini::Engine::draw_line,
|
|
127
|
+
py::arg("x1"), py::arg("y1"),
|
|
128
|
+
py::arg("x2"), py::arg("y2"),
|
|
129
|
+
py::arg("r"), py::arg("g"), py::arg("b"), py::arg("a"));
|
|
130
|
+
|
|
93
131
|
}
|
|
@@ -11,7 +11,6 @@ namespace mini {
|
|
|
11
11
|
: window_(nullptr),
|
|
12
12
|
renderer_(nullptr),
|
|
13
13
|
initialized_(false),
|
|
14
|
-
font_(nullptr),
|
|
15
14
|
clear_color_{0, 0, 0, 255},
|
|
16
15
|
default_font_id_(-1),
|
|
17
16
|
default_alpha_(255)
|
|
@@ -20,10 +19,12 @@ namespace mini {
|
|
|
20
19
|
|
|
21
20
|
Engine::~Engine()
|
|
22
21
|
{
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
shutdown_audio();
|
|
23
|
+
|
|
24
|
+
for (TTF_Font* f : fonts_) {
|
|
25
|
+
if (f) TTF_CloseFont(f);
|
|
26
26
|
}
|
|
27
|
+
fonts_.clear();
|
|
27
28
|
|
|
28
29
|
if (renderer_ != nullptr) {
|
|
29
30
|
SDL_DestroyRenderer(renderer_);
|
|
@@ -35,19 +36,14 @@ namespace mini {
|
|
|
35
36
|
window_ = nullptr;
|
|
36
37
|
}
|
|
37
38
|
|
|
39
|
+
SDL_StopTextInput();
|
|
40
|
+
|
|
38
41
|
if (initialized_) {
|
|
42
|
+
TTF_Quit();
|
|
39
43
|
SDL_Quit();
|
|
40
44
|
initialized_ = false;
|
|
41
45
|
}
|
|
42
46
|
|
|
43
|
-
for (TTF_Font* f : fonts_) {
|
|
44
|
-
if (f) TTF_CloseFont(f);
|
|
45
|
-
}
|
|
46
|
-
fonts_.clear();
|
|
47
|
-
|
|
48
|
-
TTF_Quit();
|
|
49
|
-
SDL_StopTextInput();
|
|
50
|
-
|
|
51
47
|
}
|
|
52
48
|
|
|
53
49
|
void Engine::init(int width, int height, const char* title)
|
|
@@ -72,7 +68,7 @@ namespace mini {
|
|
|
72
68
|
SDL_WINDOWPOS_CENTERED,
|
|
73
69
|
width,
|
|
74
70
|
height,
|
|
75
|
-
SDL_WINDOW_SHOWN
|
|
71
|
+
SDL_WINDOW_SHOWN | SDL_WINDOW_ALLOW_HIGHDPI | SDL_WINDOW_RESIZABLE
|
|
76
72
|
);
|
|
77
73
|
|
|
78
74
|
if (window_ == nullptr) {
|
|
@@ -87,6 +83,7 @@ namespace mini {
|
|
|
87
83
|
-1,
|
|
88
84
|
SDL_RENDERER_ACCELERATED
|
|
89
85
|
);
|
|
86
|
+
// SDL_RenderSetLogicalSize(renderer_, width, height);
|
|
90
87
|
|
|
91
88
|
if (renderer_ == nullptr) {
|
|
92
89
|
std::string msg = std::string("SDL_CreateRenderer Error: ") + SDL_GetError();
|
|
@@ -105,7 +102,6 @@ namespace mini {
|
|
|
105
102
|
initialized_ = true;
|
|
106
103
|
}
|
|
107
104
|
|
|
108
|
-
|
|
109
105
|
void Engine::set_window_title(const char* title)
|
|
110
106
|
{
|
|
111
107
|
if (!initialized_ || !window_) return;
|
|
@@ -309,6 +305,21 @@ namespace mini {
|
|
|
309
305
|
std::vector<Event> events;
|
|
310
306
|
SDL_Event sdl_event;
|
|
311
307
|
|
|
308
|
+
auto scale_mouse = [&](int &x, int &y, int &dx, int &dy) {
|
|
309
|
+
int ww=0, wh=0, rw=0, rh=0;
|
|
310
|
+
SDL_GetWindowSize(window_, &ww, &wh);
|
|
311
|
+
SDL_GetRendererOutputSize(renderer_, &rw, &rh);
|
|
312
|
+
|
|
313
|
+
if (ww > 0 && wh > 0) {
|
|
314
|
+
float sx = (float)rw / (float)ww;
|
|
315
|
+
float sy = (float)rh / (float)wh;
|
|
316
|
+
x = (int)lroundf(x * sx);
|
|
317
|
+
y = (int)lroundf(y * sy);
|
|
318
|
+
dx = (int)lroundf(dx * sx);
|
|
319
|
+
dy = (int)lroundf(dy * sy);
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
|
|
312
323
|
while (SDL_PollEvent(&sdl_event)) {
|
|
313
324
|
Event ev;
|
|
314
325
|
|
|
@@ -339,6 +350,7 @@ namespace mini {
|
|
|
339
350
|
ev.y = sdl_event.motion.y;
|
|
340
351
|
ev.dx = sdl_event.motion.xrel;
|
|
341
352
|
ev.dy = sdl_event.motion.yrel;
|
|
353
|
+
scale_mouse(ev.x, ev.y, ev.dx, ev.dy);
|
|
342
354
|
break;
|
|
343
355
|
|
|
344
356
|
case SDL_MOUSEBUTTONDOWN:
|
|
@@ -353,6 +365,7 @@ namespace mini {
|
|
|
353
365
|
ev.button = (int)sdl_event.button.button;
|
|
354
366
|
ev.x = sdl_event.button.x;
|
|
355
367
|
ev.y = sdl_event.button.y;
|
|
368
|
+
scale_mouse(ev.x, ev.y, ev.dx, ev.dy);
|
|
356
369
|
break;
|
|
357
370
|
|
|
358
371
|
case SDL_MOUSEWHEEL:
|
|
@@ -372,8 +385,14 @@ namespace mini {
|
|
|
372
385
|
sdl_event.window.event == SDL_WINDOWEVENT_SIZE_CHANGED)
|
|
373
386
|
{
|
|
374
387
|
ev.type = EventType::WindowResized;
|
|
375
|
-
|
|
376
|
-
|
|
388
|
+
int rw=0, rh=0;
|
|
389
|
+
if (renderer_ && SDL_GetRendererOutputSize(renderer_, &rw, &rh) == 0) {
|
|
390
|
+
ev.width = rw;
|
|
391
|
+
ev.height = rh;
|
|
392
|
+
} else {
|
|
393
|
+
ev.width = sdl_event.window.data1;
|
|
394
|
+
ev.height = sdl_event.window.data2;
|
|
395
|
+
}
|
|
377
396
|
} else {
|
|
378
397
|
continue; // ignore other window events
|
|
379
398
|
}
|
|
@@ -412,4 +431,166 @@ namespace mini {
|
|
|
412
431
|
return {w, h};
|
|
413
432
|
}
|
|
414
433
|
|
|
434
|
+
void Engine::init_audio(int frequency, int channels, int chunk_size)
|
|
435
|
+
{
|
|
436
|
+
if (audio_initialized_) return;
|
|
437
|
+
|
|
438
|
+
// Make sure SDL audio subsystem is enabled
|
|
439
|
+
if ((SDL_WasInit(SDL_INIT_AUDIO) & SDL_INIT_AUDIO) == 0) {
|
|
440
|
+
if (SDL_InitSubSystem(SDL_INIT_AUDIO) != 0) {
|
|
441
|
+
throw std::runtime_error(std::string("SDL_InitSubSystem(AUDIO) Error: ") + SDL_GetError());
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (Mix_OpenAudio(frequency, MIX_DEFAULT_FORMAT, channels, chunk_size) != 0) {
|
|
446
|
+
throw std::runtime_error(std::string("Mix_OpenAudio Error: ") + Mix_GetError());
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Optional: allow OGG/MP3 decoding if you want
|
|
450
|
+
// Mix_Init(MIX_INIT_OGG | MIX_INIT_MP3);
|
|
451
|
+
|
|
452
|
+
Mix_AllocateChannels(16); // plenty for pong
|
|
453
|
+
Mix_Volume(-1, master_volume_); // -1 = all channels
|
|
454
|
+
|
|
455
|
+
audio_initialized_ = true;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
void Engine::shutdown_audio()
|
|
459
|
+
{
|
|
460
|
+
if (!audio_initialized_) return;
|
|
461
|
+
|
|
462
|
+
stop_all_sounds();
|
|
463
|
+
|
|
464
|
+
for (auto& kv : sounds_) {
|
|
465
|
+
if (kv.second) {
|
|
466
|
+
Mix_FreeChunk(kv.second);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
sounds_.clear();
|
|
470
|
+
|
|
471
|
+
Mix_CloseAudio();
|
|
472
|
+
// Mix_Quit(); // if you used Mix_Init
|
|
473
|
+
|
|
474
|
+
audio_initialized_ = false;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
void Engine::load_sound(const std::string& sound_id, const std::string& path)
|
|
478
|
+
{
|
|
479
|
+
if (!audio_initialized_) {
|
|
480
|
+
// auto-init so Python doesn't need to care
|
|
481
|
+
init_audio();
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (sound_id.empty()) {
|
|
485
|
+
throw std::runtime_error("load_sound: sound_id is empty");
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// If already loaded, free it first (support hot reload)
|
|
489
|
+
auto it = sounds_.find(sound_id);
|
|
490
|
+
if (it != sounds_.end() && it->second) {
|
|
491
|
+
Mix_FreeChunk(it->second);
|
|
492
|
+
it->second = nullptr;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
Mix_Chunk* chunk = Mix_LoadWAV(path.c_str());
|
|
496
|
+
if (!chunk) {
|
|
497
|
+
throw std::runtime_error(std::string("Mix_LoadWAV Error: ") + Mix_GetError());
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
sounds_[sound_id] = chunk;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
void Engine::play_sound(const std::string& sound_id, int loops)
|
|
504
|
+
{
|
|
505
|
+
if (!audio_initialized_) {
|
|
506
|
+
init_audio();
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
auto it = sounds_.find(sound_id);
|
|
510
|
+
if (it == sounds_.end() || it->second == nullptr) {
|
|
511
|
+
// be forgiving: if not preloaded, just do nothing
|
|
512
|
+
// (or throw if you want strict behavior)
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
Mix_Chunk* chunk = it->second;
|
|
517
|
+
|
|
518
|
+
// -1 channel = pick first free channel
|
|
519
|
+
Mix_PlayChannel(-1, chunk, loops);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
void Engine::set_master_volume(int volume)
|
|
523
|
+
{
|
|
524
|
+
if (volume < 0) volume = 0;
|
|
525
|
+
if (volume > MIX_MAX_VOLUME) volume = MIX_MAX_VOLUME;
|
|
526
|
+
|
|
527
|
+
master_volume_ = volume;
|
|
528
|
+
Mix_Volume(-1, master_volume_); // all channels
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
void Engine::set_sound_volume(const std::string& sound_id, int volume)
|
|
532
|
+
{
|
|
533
|
+
if (volume < 0) volume = 0;
|
|
534
|
+
if (volume > MIX_MAX_VOLUME) volume = MIX_MAX_VOLUME;
|
|
535
|
+
|
|
536
|
+
auto it = sounds_.find(sound_id);
|
|
537
|
+
if (it == sounds_.end() || it->second == nullptr) return;
|
|
538
|
+
|
|
539
|
+
Mix_VolumeChunk(it->second, volume);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
void Engine::stop_all_sounds()
|
|
543
|
+
{
|
|
544
|
+
Mix_HaltChannel(-1);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
void Engine::resize_window(int width, int height)
|
|
548
|
+
{
|
|
549
|
+
if (!initialized_ || !window_) return;
|
|
550
|
+
SDL_SetWindowSize(window_, width, height);
|
|
551
|
+
|
|
552
|
+
// if (renderer_) {
|
|
553
|
+
// SDL_RenderSetLogicalSize(renderer_, width, height);
|
|
554
|
+
// }
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
void Engine::set_clip_rect(int x, int y, int w, int h)
|
|
558
|
+
{
|
|
559
|
+
if (!initialized_ || renderer_ == nullptr) return;
|
|
560
|
+
SDL_Rect r{ x, y, w, h };
|
|
561
|
+
SDL_RenderSetClipRect(renderer_, &r);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
void Engine::clear_clip_rect()
|
|
565
|
+
{
|
|
566
|
+
if (!initialized_ || renderer_ == nullptr) return;
|
|
567
|
+
SDL_RenderSetClipRect(renderer_, nullptr);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
void Engine::draw_line(int x1, int y1, int x2, int y2, int r, int g, int b, int a)
|
|
571
|
+
{
|
|
572
|
+
if (!initialized_ || renderer_ == nullptr) {
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
auto clamp = [](int v) {
|
|
577
|
+
if (v < 0) return 0;
|
|
578
|
+
if (v > 255) return 255;
|
|
579
|
+
return v;
|
|
580
|
+
};
|
|
581
|
+
|
|
582
|
+
// alpha or default alpha
|
|
583
|
+
a = (a < 0) ? default_alpha_ : a;
|
|
584
|
+
|
|
585
|
+
SDL_SetRenderDrawColor(
|
|
586
|
+
renderer_,
|
|
587
|
+
static_cast<Uint8>(clamp(r)),
|
|
588
|
+
static_cast<Uint8>(clamp(g)),
|
|
589
|
+
static_cast<Uint8>(clamp(b)),
|
|
590
|
+
static_cast<Uint8>(clamp(a))
|
|
591
|
+
);
|
|
592
|
+
|
|
593
|
+
SDL_RenderDrawLine(renderer_, x1, y1, x2, y2);
|
|
594
|
+
}
|
|
595
|
+
|
|
415
596
|
} // namespace mini
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
#include <SDL.h>
|
|
4
4
|
#include <SDL_ttf.h>
|
|
5
|
+
#include <SDL_mixer.h>
|
|
6
|
+
#include <unordered_map>
|
|
5
7
|
#include <vector>
|
|
6
8
|
#include <string>
|
|
7
9
|
#include <utility>
|
|
@@ -97,15 +99,44 @@ namespace mini {
|
|
|
97
99
|
// Returns (width, height) in pixels. Returns (0,0) if no valid font or error.
|
|
98
100
|
std::pair<int, int> measure_text(const char* text, int font_id = -1);
|
|
99
101
|
|
|
102
|
+
// --- Audio ---
|
|
103
|
+
// Initialize audio subsystem.
|
|
104
|
+
void init_audio(int frequency = 44100, int channels = 2, int chunk_size = 2048);
|
|
105
|
+
// Shutdown audio subsystem.
|
|
106
|
+
void shutdown_audio();
|
|
107
|
+
|
|
108
|
+
// Load, play, and manage sounds.
|
|
109
|
+
void load_sound(const std::string& sound_id, const std::string& path);
|
|
110
|
+
// Play a loaded sound by its ID.
|
|
111
|
+
void play_sound(const std::string& sound_id, int loops = 0);
|
|
112
|
+
// Set the volume for a specific sound (0..128).
|
|
113
|
+
void set_sound_volume(const std::string& sound_id, int volume); // 0..128
|
|
114
|
+
// Set the master volume for all sounds (0..128).
|
|
115
|
+
void set_master_volume(int volume); // 0..128
|
|
116
|
+
// Stop all currently playing sounds.
|
|
117
|
+
void stop_all_sounds();
|
|
118
|
+
|
|
119
|
+
// Resize the application window.
|
|
120
|
+
void resize_window(int width, int height);
|
|
121
|
+
// Set clipping rectangle for rendering.
|
|
122
|
+
void set_clip_rect(int x, int y, int w, int h);
|
|
123
|
+
// Clear clipping rectangle (disable clipping).
|
|
124
|
+
void clear_clip_rect();
|
|
125
|
+
|
|
126
|
+
// Draw a line from (x1, y1) to (x2, y2) with specified color.
|
|
127
|
+
void draw_line(int x1, int y1, int x2, int y2, int r, int g, int b, int a);
|
|
128
|
+
|
|
100
129
|
private:
|
|
101
130
|
SDL_Window* window_; // The main application window.
|
|
102
131
|
SDL_Renderer* renderer_; // The renderer for drawing.
|
|
103
132
|
bool initialized_; // Whether the engine has been initialized.
|
|
104
|
-
TTF_Font* font_; // The current font for text rendering.
|
|
105
133
|
SDL_Color clear_color_; // The clear color for the screen.
|
|
106
134
|
std::vector<TTF_Font*> fonts_; // Loaded fonts.
|
|
107
135
|
int default_font_id_; // Default font index.
|
|
108
136
|
int default_alpha_; // Default alpha value for drawing.
|
|
137
|
+
bool audio_initialized_ = false; // Whether audio subsystem is initialized.
|
|
138
|
+
int master_volume_ = MIX_MAX_VOLUME; // 128 is max volume
|
|
139
|
+
std::unordered_map<std::string, Mix_Chunk*> sounds_; // Audio data
|
|
109
140
|
};
|
|
110
141
|
|
|
111
142
|
} // namespace mini
|
|
@@ -516,14 +516,14 @@ files = [
|
|
|
516
516
|
|
|
517
517
|
[[package]]
|
|
518
518
|
name = "mini-arcade-core"
|
|
519
|
-
version = "1.
|
|
519
|
+
version = "1.1.1"
|
|
520
520
|
description = "Tiny scene-based game loop core for small arcade games."
|
|
521
521
|
optional = false
|
|
522
522
|
python-versions = "<3.12,>=3.9"
|
|
523
523
|
groups = ["main"]
|
|
524
524
|
files = [
|
|
525
|
-
{file = "mini_arcade_core-1.
|
|
526
|
-
{file = "mini_arcade_core-1.
|
|
525
|
+
{file = "mini_arcade_core-1.1.1-py3-none-any.whl", hash = "sha256:cd487278b33ba77e80acfdc895a77a76cc1d07f5c800a26b7537e18aaa97f00e"},
|
|
526
|
+
{file = "mini_arcade_core-1.1.1.tar.gz", hash = "sha256:b3b81a3adefaaf2b5438bc773ed364a5cf997e7f00a67ef3fc8be9d40bdbc61f"},
|
|
527
527
|
]
|
|
528
528
|
|
|
529
529
|
[package.dependencies]
|
|
@@ -1001,4 +1001,4 @@ dev = ["black", "isort", "mypy", "pylint", "pytest", "pytest-cov"]
|
|
|
1001
1001
|
[metadata]
|
|
1002
1002
|
lock-version = "2.1"
|
|
1003
1003
|
python-versions = ">=3.9,<3.12"
|
|
1004
|
-
content-hash = "
|
|
1004
|
+
content-hash = "049cf6813d766ae9a7c9ab36a2ef36d24fce8da7a19ffa70200461aef112012d"
|
|
@@ -8,7 +8,7 @@ build-backend = "scikit_build_core.build"
|
|
|
8
8
|
|
|
9
9
|
[project]
|
|
10
10
|
name = "mini-arcade-native-backend"
|
|
11
|
-
version = "0.
|
|
11
|
+
version = "0.6.0"
|
|
12
12
|
description = "Native SDL2 backend for mini-arcade-core using SDL2 + pybind11."
|
|
13
13
|
authors = [
|
|
14
14
|
{ name = "Santiago Rincon", email = "rincores@gmail.com" },
|
|
@@ -17,7 +17,7 @@ readme = "README.md"
|
|
|
17
17
|
requires-python = ">=3.9,<3.12"
|
|
18
18
|
license = { file = "LICENSE" }
|
|
19
19
|
dependencies = [
|
|
20
|
-
"mini-arcade-core~=1.
|
|
20
|
+
"mini-arcade-core~=1.1",
|
|
21
21
|
]
|
|
22
22
|
|
|
23
23
|
[project.optional-dependencies]
|
|
@@ -6,6 +6,7 @@ from __future__ import annotations
|
|
|
6
6
|
|
|
7
7
|
import os
|
|
8
8
|
import sys
|
|
9
|
+
from dataclasses import dataclass
|
|
9
10
|
from pathlib import Path
|
|
10
11
|
from typing import Optional, Union
|
|
11
12
|
|
|
@@ -42,6 +43,7 @@ if sys.platform == "win32":
|
|
|
42
43
|
# pylint: disable=import-error
|
|
43
44
|
from mini_arcade_core.backend import ( # pyright: ignore[reportMissingImports]
|
|
44
45
|
Backend,
|
|
46
|
+
WindowSettings,
|
|
45
47
|
)
|
|
46
48
|
from mini_arcade_core.backend.events import ( # pyright: ignore[reportMissingImports]
|
|
47
49
|
Event,
|
|
@@ -79,23 +81,49 @@ _NATIVE_TO_CORE = {
|
|
|
79
81
|
}
|
|
80
82
|
|
|
81
83
|
|
|
84
|
+
@dataclass
|
|
85
|
+
class BackendSettings:
|
|
86
|
+
"""
|
|
87
|
+
Settings for the NativeBackend.
|
|
88
|
+
|
|
89
|
+
:ivar font_path (Optional[str]): Optional path to a TTF font file to load.
|
|
90
|
+
:ivar font_size (int): Font size in points to use when loading the font.
|
|
91
|
+
:ivar sounds (Optional[dict[str, str]]): Optional dictionary mapping sound IDs to file paths.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
font_path: Optional[str] = None
|
|
95
|
+
font_size: int = 24
|
|
96
|
+
sounds: Optional[dict[str, str]] = None # sound_id -> path
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# TODO: Refactor backend interface into smaller protocols?
|
|
100
|
+
# Justification: Many public methods needed for backend interface
|
|
101
|
+
# pylint: disable=too-many-public-methods,too-many-instance-attributes
|
|
82
102
|
class NativeBackend(Backend):
|
|
83
103
|
"""Adapter that makes the C++ Engine usable as a mini-arcade backend."""
|
|
84
104
|
|
|
85
|
-
def __init__(self,
|
|
105
|
+
def __init__(self, backend_settings: BackendSettings | None = None):
|
|
86
106
|
"""
|
|
87
|
-
:param
|
|
88
|
-
:type
|
|
89
|
-
|
|
90
|
-
:param font_size: Font size in points to use when loading the font.
|
|
91
|
-
:type font_size: int
|
|
107
|
+
:param backend_settings: Optional settings for the backend.
|
|
108
|
+
:type backend_settings: BackendSettings | None
|
|
92
109
|
"""
|
|
93
110
|
self._engine = native.Engine()
|
|
94
|
-
|
|
95
|
-
self.
|
|
111
|
+
|
|
112
|
+
self._font_path = (
|
|
113
|
+
backend_settings.font_path if backend_settings else None
|
|
114
|
+
)
|
|
115
|
+
self._font_size = (
|
|
116
|
+
backend_settings.font_size if backend_settings else 24
|
|
117
|
+
)
|
|
96
118
|
self._default_font_id: int | None = None
|
|
97
119
|
self._fonts_by_size: dict[int, int] = {}
|
|
98
120
|
|
|
121
|
+
self._sounds = backend_settings.sounds if backend_settings else None
|
|
122
|
+
|
|
123
|
+
self._vp_offset_x = 0
|
|
124
|
+
self._vp_offset_y = 0
|
|
125
|
+
self._vp_scale = 1.0
|
|
126
|
+
|
|
99
127
|
def _get_font_id(self, font_size: int | None) -> int:
|
|
100
128
|
# No font loaded -> keep current “no-op” behavior
|
|
101
129
|
if self._font_path is None:
|
|
@@ -122,22 +150,15 @@ class NativeBackend(Backend):
|
|
|
122
150
|
self._fonts_by_size[font_size] = font_id
|
|
123
151
|
return font_id
|
|
124
152
|
|
|
125
|
-
def init(self,
|
|
153
|
+
def init(self, window_settings: WindowSettings):
|
|
126
154
|
"""
|
|
127
155
|
Initialize the backend with a window of given width, height, and title.
|
|
128
156
|
|
|
129
|
-
:param
|
|
130
|
-
:type
|
|
131
|
-
|
|
132
|
-
:param height: Height of the window in pixels.
|
|
133
|
-
:type height: int
|
|
134
|
-
|
|
135
|
-
:param title: Title of the window.
|
|
136
|
-
:type title: Optional[str]
|
|
157
|
+
:param window_settings: Settings for the backend window.
|
|
158
|
+
:type window_settings: WindowSettings
|
|
137
159
|
"""
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
self._engine.init(width, height, title)
|
|
160
|
+
title = ""
|
|
161
|
+
self._engine.init(window_settings.width, window_settings.height, title)
|
|
141
162
|
|
|
142
163
|
# Load font if provided
|
|
143
164
|
if self._font_path is not None:
|
|
@@ -146,6 +167,11 @@ class NativeBackend(Backend):
|
|
|
146
167
|
)
|
|
147
168
|
self._fonts_by_size[self._font_size] = self._default_font_id
|
|
148
169
|
|
|
170
|
+
# Load sounds if provided
|
|
171
|
+
if self._sounds is not None:
|
|
172
|
+
for sound_id, path in self._sounds.items():
|
|
173
|
+
self.load_sound(sound_id, path)
|
|
174
|
+
|
|
149
175
|
def set_window_title(self, title: str):
|
|
150
176
|
"""
|
|
151
177
|
Set the window title.
|
|
@@ -273,12 +299,18 @@ class NativeBackend(Backend):
|
|
|
273
299
|
if isinstance(alpha, bool):
|
|
274
300
|
raise TypeError("alpha must be a float in [0,1], not bool")
|
|
275
301
|
|
|
276
|
-
|
|
302
|
+
# If it's an int-like value, treat as 0..255
|
|
303
|
+
if isinstance(alpha, int):
|
|
304
|
+
if alpha < 0 or alpha > 255:
|
|
305
|
+
raise ValueError(
|
|
306
|
+
f"int alpha must be in [0, 255], got {alpha!r}"
|
|
307
|
+
)
|
|
308
|
+
return int(alpha)
|
|
277
309
|
|
|
278
|
-
#
|
|
310
|
+
# Otherwise treat as float 0..1
|
|
311
|
+
a = float(alpha)
|
|
279
312
|
if a < 0.0 or a > 1.0:
|
|
280
|
-
raise ValueError(f"alpha must be in [0, 1], got {alpha!r}")
|
|
281
|
-
|
|
313
|
+
raise ValueError(f"float alpha must be in [0, 1], got {alpha!r}")
|
|
282
314
|
return int(round(a * 255))
|
|
283
315
|
|
|
284
316
|
@staticmethod
|
|
@@ -328,7 +360,12 @@ class NativeBackend(Backend):
|
|
|
328
360
|
:type color: tuple[int, ...]
|
|
329
361
|
"""
|
|
330
362
|
r, g, b, a = self._get_color_values(color)
|
|
331
|
-
self.
|
|
363
|
+
sx = int(round(self._vp_offset_x + x * self._vp_scale)) # top-left x
|
|
364
|
+
sy = int(round(self._vp_offset_y + y * self._vp_scale)) # top-left y
|
|
365
|
+
sw = int(round(w * self._vp_scale)) # width
|
|
366
|
+
sh = int(round(h * self._vp_scale)) # height
|
|
367
|
+
self._engine.draw_rect(sx, sy, sw, sh, r, g, b, a)
|
|
368
|
+
# self._engine.draw_rect(x, y, w, h, r, g, b, a)
|
|
332
369
|
|
|
333
370
|
def draw_text(
|
|
334
371
|
self,
|
|
@@ -356,9 +393,22 @@ class NativeBackend(Backend):
|
|
|
356
393
|
"""
|
|
357
394
|
r, g, b, a = self._get_color_values(color)
|
|
358
395
|
font_id = self._get_font_id(font_size)
|
|
396
|
+
sx = int(round(self._vp_offset_x + x * self._vp_scale))
|
|
397
|
+
sy = int(round(self._vp_offset_y + y * self._vp_scale))
|
|
398
|
+
|
|
399
|
+
# optional but recommended: scale font size too
|
|
400
|
+
if font_size is not None:
|
|
401
|
+
scaled = max(8, int(round(font_size * self._vp_scale)))
|
|
402
|
+
else:
|
|
403
|
+
scaled = None
|
|
404
|
+
|
|
405
|
+
font_id = self._get_font_id(scaled)
|
|
359
406
|
self._engine.draw_text(
|
|
360
|
-
text,
|
|
407
|
+
text, sx, sy, int(r), int(g), int(b), int(a), font_id
|
|
361
408
|
)
|
|
409
|
+
# self._engine.draw_text(
|
|
410
|
+
# text, x, y, int(r), int(g), int(b), int(a), font_id
|
|
411
|
+
# )
|
|
362
412
|
|
|
363
413
|
# pylint: enable=too-many-arguments,too-many-positional-arguments
|
|
364
414
|
|
|
@@ -388,3 +438,97 @@ class NativeBackend(Backend):
|
|
|
388
438
|
font_id = self._get_font_id(font_size)
|
|
389
439
|
w, h = self._engine.measure_text(text, font_id)
|
|
390
440
|
return int(w), int(h)
|
|
441
|
+
|
|
442
|
+
def init_audio(
|
|
443
|
+
self, frequency: int = 44100, channels: int = 2, chunk_size: int = 2048
|
|
444
|
+
):
|
|
445
|
+
"""Initialize SDL_mixer audio."""
|
|
446
|
+
self._engine.init_audio(int(frequency), int(channels), int(chunk_size))
|
|
447
|
+
|
|
448
|
+
def shutdown_audio(self):
|
|
449
|
+
"""Shutdown SDL_mixer audio and free loaded sounds."""
|
|
450
|
+
self._engine.shutdown_audio()
|
|
451
|
+
|
|
452
|
+
def load_sound(self, sound_id: str, path: str):
|
|
453
|
+
"""
|
|
454
|
+
Load a WAV sound and store it by ID.
|
|
455
|
+
Example: backend.load_sound("hit", "assets/sfx/hit.wav")
|
|
456
|
+
"""
|
|
457
|
+
if not sound_id:
|
|
458
|
+
raise ValueError("sound_id cannot be empty")
|
|
459
|
+
|
|
460
|
+
p = Path(path)
|
|
461
|
+
if not p.exists():
|
|
462
|
+
raise FileNotFoundError(f"Sound file not found: {p}")
|
|
463
|
+
|
|
464
|
+
self._engine.load_sound(sound_id, str(p))
|
|
465
|
+
|
|
466
|
+
def play_sound(self, sound_id: str, loops: int = 0):
|
|
467
|
+
"""
|
|
468
|
+
Play a loaded sound.
|
|
469
|
+
loops=0 => play once
|
|
470
|
+
loops=-1 => infinite loop
|
|
471
|
+
loops=1 => play twice (SDL convention)
|
|
472
|
+
"""
|
|
473
|
+
self._engine.play_sound(sound_id, int(loops))
|
|
474
|
+
|
|
475
|
+
def set_master_volume(self, volume: int):
|
|
476
|
+
"""
|
|
477
|
+
Master volume: 0..128
|
|
478
|
+
"""
|
|
479
|
+
self._engine.set_master_volume(int(volume))
|
|
480
|
+
|
|
481
|
+
def set_sound_volume(self, sound_id: str, volume: int):
|
|
482
|
+
"""
|
|
483
|
+
Per-sound volume: 0..128
|
|
484
|
+
"""
|
|
485
|
+
self._engine.set_sound_volume(sound_id, int(volume))
|
|
486
|
+
|
|
487
|
+
def stop_all_sounds(self):
|
|
488
|
+
"""Stop all channels."""
|
|
489
|
+
self._engine.stop_all_sounds()
|
|
490
|
+
|
|
491
|
+
def set_viewport_transform(
|
|
492
|
+
self, offset_x: int, offset_y: int, scale: float
|
|
493
|
+
) -> None:
|
|
494
|
+
self._vp_offset_x = int(offset_x)
|
|
495
|
+
self._vp_offset_y = int(offset_y)
|
|
496
|
+
self._vp_scale = float(scale)
|
|
497
|
+
|
|
498
|
+
def clear_viewport_transform(self) -> None:
|
|
499
|
+
self._vp_offset_x = 0
|
|
500
|
+
self._vp_offset_y = 0
|
|
501
|
+
self._vp_scale = 1.0
|
|
502
|
+
|
|
503
|
+
def resize_window(self, width: int, height: int) -> None:
|
|
504
|
+
self._engine.resize_window(int(width), int(height))
|
|
505
|
+
|
|
506
|
+
def set_clip_rect(self, x: int, y: int, w: int, h: int) -> None:
|
|
507
|
+
self._engine.set_clip_rect(int(x), int(y), int(w), int(h))
|
|
508
|
+
|
|
509
|
+
def clear_clip_rect(self) -> None:
|
|
510
|
+
self._engine.clear_clip_rect()
|
|
511
|
+
|
|
512
|
+
# Justification: Many arguments needed for line drawing
|
|
513
|
+
# pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
514
|
+
def draw_line(
|
|
515
|
+
self,
|
|
516
|
+
x1: int,
|
|
517
|
+
y1: int,
|
|
518
|
+
x2: int,
|
|
519
|
+
y2: int,
|
|
520
|
+
color: tuple[int, ...] = (255, 255, 255),
|
|
521
|
+
) -> None:
|
|
522
|
+
r, g, b, a = self._get_color_values(color)
|
|
523
|
+
|
|
524
|
+
sx1 = int(round(self._vp_offset_x + x1 * self._vp_scale))
|
|
525
|
+
sy1 = int(round(self._vp_offset_y + y1 * self._vp_scale))
|
|
526
|
+
sx2 = int(round(self._vp_offset_x + x2 * self._vp_scale))
|
|
527
|
+
sy2 = int(round(self._vp_offset_y + y2 * self._vp_scale))
|
|
528
|
+
|
|
529
|
+
self._engine.draw_line(
|
|
530
|
+
sx1, sy1, sx2, sy2, int(r), int(g), int(b), int(a)
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
# pylint: enable=too-many-arguments,too-many-positional-arguments
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|