Compare commits
10 Commits
acb3925bb4
...
6d073c136d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d073c136d | ||
|
|
8b3e21b5f3 | ||
|
|
9238ce61c6 | ||
|
|
7ac9875d11 | ||
|
|
0677b2cbca | ||
|
|
0263be65d4 | ||
|
|
869056c5d2 | ||
|
|
aa8387e9ec | ||
|
|
76c0776aec | ||
|
|
8d57d63da2 |
54
BENCH.md
Normal file
54
BENCH.md
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
# Kobo Deluxe DOD Refactor — Benchmark Scoreboard
|
||||||
|
|
||||||
|
Each row is one phase of the refactor (see
|
||||||
|
`~/.claude/plans/can-you-explain-the-noble-wilkes.md`). Numbers are median
|
||||||
|
ns/frame from `KOBO_PROF=1 KOBO_STRESS=1500 ./build/kobodl`, ~1000 frames,
|
||||||
|
release build (`-O2 -g`).
|
||||||
|
|
||||||
|
## How to capture a baseline
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd build
|
||||||
|
cmake --build . -j
|
||||||
|
KOBO_PROF=1 KOBO_STRESS=1500 ./kobodl
|
||||||
|
# play through a stage; profile dumps every 600 frames to stderr.
|
||||||
|
# Redirect stderr to a file if you want to keep it:
|
||||||
|
KOBO_PROF=1 KOBO_STRESS=1500 ./kobodl 2>prof.log
|
||||||
|
```
|
||||||
|
|
||||||
|
The profiler dumps a table per zone with `calls`, `total_ms`, `ns/call`.
|
||||||
|
Record the `ns/call` for these zones (they are the moving parts the refactor
|
||||||
|
targets):
|
||||||
|
|
||||||
|
| Zone | What it measures |
|
||||||
|
|-----------------------|-----------------------------------------------------|
|
||||||
|
| `run_game` | Full game tick |
|
||||||
|
| `myship.move` | Player + bolt update |
|
||||||
|
| `enemies.move` | Realize pass + move pass combined |
|
||||||
|
| `enemies.realize` | Just the realize-reserved pass |
|
||||||
|
| `enemies.move_pass` | Just the per-enemy move pass (the hot loop) |
|
||||||
|
| `hit_structure` | Player vs map + bolt vs map |
|
||||||
|
| `hit_bolt` | Bolt-vs-enemy scan (called from inside enemy move) |
|
||||||
|
|
||||||
|
## Scoreboard
|
||||||
|
|
||||||
|
| Phase | run_game | myship.move | enemies.move_pass | hit_bolt | Notes |
|
||||||
|
|------------------------------|---------:|------------:|------------------:|---------:|-------|
|
||||||
|
| 0 — baseline | ? | ? | ? | ? | TBD — user to capture |
|
||||||
|
| 1 — hot/cold split | | | | | |
|
||||||
|
| 2 — SoA pool | | | | | |
|
||||||
|
| 3 — bucket dispatch | | | | | |
|
||||||
|
| 4 — flipped bolt collision | | | | | |
|
||||||
|
| 5 — render decouple | | | | | |
|
||||||
|
| 6 — SIMD integrator (opt.) | | | | | |
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- `KOBO_STRESS=N` keeps the pool topped up with rocks; N=1500 sits well
|
||||||
|
below ENEMY_MAX=2048 so the spawner doesn't thrash on full-pool checks.
|
||||||
|
- The stress spawner uses `pubrand`, which is acceptable for benchmarking
|
||||||
|
but will *not* produce identical demos across builds — that's fine, we
|
||||||
|
only care about steady-state cost.
|
||||||
|
- For a stable run, start the game, get into a stage, then let it sit for
|
||||||
|
~1000 frames before reading numbers. The first dump (frame 600) will
|
||||||
|
include startup costs and should be discarded.
|
||||||
@ -14,6 +14,13 @@ option(KOBO_ENABLE_OSS "Build OSS audio backend (Linux)" OFF)
|
|||||||
option(KOBO_ENABLE_ALSA "Build ALSA audio backend (Linux)" OFF)
|
option(KOBO_ENABLE_ALSA "Build ALSA audio backend (Linux)" OFF)
|
||||||
option(KOBO_ENABLE_EPM "Extreme Pickyness Mode (-Wall -Werror)" OFF)
|
option(KOBO_ENABLE_EPM "Extreme Pickyness Mode (-Wall -Werror)" OFF)
|
||||||
|
|
||||||
|
# Emscripten target: SDL renderer maps to WebGL, no desktop GL needed.
|
||||||
|
if(EMSCRIPTEN)
|
||||||
|
set(KOBO_ENABLE_OPENGL OFF CACHE BOOL "Use OpenGL rendering layer" FORCE)
|
||||||
|
set(KOBO_ENABLE_OSS OFF CACHE BOOL "Build OSS audio backend" FORCE)
|
||||||
|
set(KOBO_ENABLE_ALSA OFF CACHE BOOL "Build ALSA audio backend" FORCE)
|
||||||
|
endif()
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Feature detection
|
# Feature detection
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@ -36,8 +43,22 @@ check_symbol_exists(_vsnprintf stdio.h HAVE__VSNPRINTF)
|
|||||||
# Third-party libraries
|
# Third-party libraries
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Migrating to SDL 3. Homebrew: `brew install sdl3 sdl3_image`.
|
# Migrating to SDL 3. Homebrew: `brew install sdl3 sdl3_image`.
|
||||||
find_package(SDL3 CONFIG REQUIRED)
|
# Emscripten 5.x ships an sdl3 port but no sdl3_image yet — we substitute
|
||||||
find_package(SDL3_image CONFIG REQUIRED)
|
# libpng + a small shim (graphics/img_compat.c) for IMG_Load. The
|
||||||
|
# `--use-port` flags must appear in both compile and link phases.
|
||||||
|
if(EMSCRIPTEN)
|
||||||
|
add_library(SDL3::SDL3 INTERFACE IMPORTED)
|
||||||
|
target_compile_options(SDL3::SDL3 INTERFACE --use-port=sdl3)
|
||||||
|
target_link_options(SDL3::SDL3 INTERFACE --use-port=sdl3)
|
||||||
|
|
||||||
|
add_library(SDL3_image::SDL3_image INTERFACE IMPORTED)
|
||||||
|
target_compile_options(SDL3_image::SDL3_image INTERFACE --use-port=libpng)
|
||||||
|
target_link_options(SDL3_image::SDL3_image INTERFACE --use-port=libpng)
|
||||||
|
else()
|
||||||
|
find_package(SDL3 CONFIG REQUIRED)
|
||||||
|
find_package(SDL3_image CONFIG REQUIRED)
|
||||||
|
find_package(CURL REQUIRED)
|
||||||
|
endif()
|
||||||
|
|
||||||
if(KOBO_ENABLE_OPENGL)
|
if(KOBO_ENABLE_OPENGL)
|
||||||
find_package(OpenGL)
|
find_package(OpenGL)
|
||||||
@ -65,7 +86,19 @@ endif()
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Platform-specific install layout (matches the old autotools behavior)
|
# Platform-specific install layout (matches the old autotools behavior)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
if(APPLE)
|
if(EMSCRIPTEN)
|
||||||
|
# In the browser, "data" is preloaded into Emscripten's virtual FS at /.
|
||||||
|
# Config/scores live under /home which we mount as IDBFS for persistence
|
||||||
|
# (see Module.preRun in the HTML shell / build-web.sh).
|
||||||
|
set(KOBO_BUNDLE_STYLE "web")
|
||||||
|
# index.html so the game can be served from a clean subpath URL
|
||||||
|
# (e.g. https://lindholm.dev/kobo/ — no explicit filename).
|
||||||
|
set(KOBO_EXEFILE "index.html")
|
||||||
|
set(KOBO_DATA_DIR "/data")
|
||||||
|
set(KOBO_SCORE_DIR "/home/scores")
|
||||||
|
set(KOBO_CONFIG_DIR "/home")
|
||||||
|
set(KOBO_CONFIG_FILE ".kobodlrc")
|
||||||
|
elseif(APPLE)
|
||||||
set(KOBO_BUNDLE_STYLE "macosx")
|
set(KOBO_BUNDLE_STYLE "macosx")
|
||||||
set(KOBO_EXEFILE "kobodl")
|
set(KOBO_EXEFILE "kobodl")
|
||||||
set(KOBO_DATA_DIR "EXE>>../Resources")
|
set(KOBO_DATA_DIR "EXE>>../Resources")
|
||||||
@ -139,6 +172,13 @@ if(UNIX AND NOT APPLE)
|
|||||||
target_link_libraries(kobo_deps INTERFACE m)
|
target_link_libraries(kobo_deps INTERFACE m)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
if(EMSCRIPTEN)
|
||||||
|
# -pthread enables wasm atomics+bulk-memory; every TU must agree on it or
|
||||||
|
# wasm-ld rejects --shared-memory.
|
||||||
|
target_compile_options(kobo_deps INTERFACE -pthread)
|
||||||
|
target_link_options(kobo_deps INTERFACE -pthread)
|
||||||
|
endif()
|
||||||
|
|
||||||
if(KOBO_ENABLE_EPM)
|
if(KOBO_ENABLE_EPM)
|
||||||
target_compile_options(kobo_deps INTERFACE
|
target_compile_options(kobo_deps INTERFACE
|
||||||
-Wall -Werror -Wwrite-strings -fno-builtin
|
-Wall -Werror -Wwrite-strings -fno-builtin
|
||||||
@ -160,14 +200,52 @@ set(KOBO_SOURCES
|
|||||||
myship.cpp radar.cpp random.cpp scenes.cpp score.cpp screen.cpp
|
myship.cpp radar.cpp random.cpp scenes.cpp score.cpp screen.cpp
|
||||||
filemap.cpp prefs.cpp cfgform.cpp options.cpp gamestate.cpp
|
filemap.cpp prefs.cpp cfgform.cpp options.cpp gamestate.cpp
|
||||||
states.cpp form.cpp cfgparse.cpp game.cpp kobo.cpp logger.c
|
states.cpp form.cpp cfgparse.cpp game.cpp kobo.cpp logger.c
|
||||||
dashboard.cpp sound.cpp
|
dashboard.cpp sound.cpp prof.cpp bench.cpp leaderboard.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if(EMSCRIPTEN)
|
||||||
|
list(APPEND KOBO_SOURCES httpclient_web.cpp)
|
||||||
|
else()
|
||||||
|
list(APPEND KOBO_SOURCES httpclient_native.cpp)
|
||||||
|
endif()
|
||||||
|
|
||||||
add_executable(kobodl ${KOBO_SOURCES})
|
add_executable(kobodl ${KOBO_SOURCES})
|
||||||
target_include_directories(kobodl PRIVATE ${CMAKE_SOURCE_DIR}/data/sfx)
|
target_include_directories(kobodl PRIVATE ${CMAKE_SOURCE_DIR}/data/sfx)
|
||||||
target_link_libraries(kobodl PRIVATE graphics sound eel kobo_deps)
|
target_link_libraries(kobodl PRIVATE graphics sound eel kobo_deps)
|
||||||
set_target_properties(kobodl PROPERTIES OUTPUT_NAME ${KOBO_EXEFILE})
|
set_target_properties(kobodl PROPERTIES OUTPUT_NAME ${KOBO_EXEFILE})
|
||||||
|
|
||||||
|
if(EMSCRIPTEN)
|
||||||
|
target_link_options(kobodl PRIVATE -sFETCH=1)
|
||||||
|
else()
|
||||||
|
target_link_libraries(kobodl PRIVATE CURL::libcurl)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(EMSCRIPTEN)
|
||||||
|
# ASYNCIFY lets the existing blocking main loop (gengine->run(), SDL_Delay)
|
||||||
|
# yield to the browser without rewriting it around emscripten_set_main_loop.
|
||||||
|
# pthread (atomics+bulk-memory) is applied via kobo_deps for every TU.
|
||||||
|
# The data directory is baked into the .data sidecar via --preload-file.
|
||||||
|
target_link_options(kobodl PRIVATE
|
||||||
|
-sASYNCIFY=1
|
||||||
|
-sALLOW_MEMORY_GROWTH=1
|
||||||
|
-sINITIAL_MEMORY=128MB
|
||||||
|
-sMAXIMUM_MEMORY=1gb
|
||||||
|
-sPTHREAD_POOL_SIZE=4
|
||||||
|
-sEXIT_RUNTIME=1
|
||||||
|
-sFORCE_FILESYSTEM=1
|
||||||
|
-sEXPORTED_RUNTIME_METHODS=FS,callMain
|
||||||
|
-lidbfs.js
|
||||||
|
--preload-file ${CMAKE_SOURCE_DIR}/data@/data
|
||||||
|
)
|
||||||
|
# Generate index.html alongside kobodl.html so it can be served as the
|
||||||
|
# default document. The shell file mounts /home as IDBFS for persistence.
|
||||||
|
if(EXISTS ${CMAKE_SOURCE_DIR}/web/shell.html)
|
||||||
|
target_link_options(kobodl PRIVATE
|
||||||
|
--shell-file ${CMAKE_SOURCE_DIR}/web/shell.html)
|
||||||
|
endif()
|
||||||
|
set_target_properties(kobodl PROPERTIES SUFFIX "")
|
||||||
|
endif()
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Install rules
|
# Install rules
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@ -189,6 +267,14 @@ elseif(KOBO_BUNDLE_STYLE STREQUAL "simple")
|
|||||||
install(TARGETS kobodl RUNTIME DESTINATION KoboDeluxe)
|
install(TARGETS kobodl RUNTIME DESTINATION KoboDeluxe)
|
||||||
install(DIRECTORY data/gfx data/sfx DESTINATION KoboDeluxe)
|
install(DIRECTORY data/gfx data/sfx DESTINATION KoboDeluxe)
|
||||||
install(FILES COPYING COPYING.LIB README ChangeLog TODO DESTINATION KoboDeluxe)
|
install(FILES COPYING COPYING.LIB README ChangeLog TODO DESTINATION KoboDeluxe)
|
||||||
|
elseif(KOBO_BUNDLE_STYLE STREQUAL "web")
|
||||||
|
# Emscripten emits index.html / .js / .wasm / .data — install all four.
|
||||||
|
install(TARGETS kobodl RUNTIME DESTINATION web)
|
||||||
|
install(FILES
|
||||||
|
${CMAKE_CURRENT_BINARY_DIR}/index.js
|
||||||
|
${CMAKE_CURRENT_BINARY_DIR}/index.wasm
|
||||||
|
${CMAKE_CURRENT_BINARY_DIR}/index.data
|
||||||
|
DESTINATION web OPTIONAL)
|
||||||
else()
|
else()
|
||||||
include(GNUInstallDirs)
|
include(GNUInstallDirs)
|
||||||
install(TARGETS kobodl RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
|
install(TARGETS kobodl RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
|
||||||
|
|||||||
35
bench.cpp
Normal file
35
bench.cpp
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
#include "bench.h"
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
int kobo_bench_enabled = 0;
|
||||||
|
int kobo_bench_frames = 1800;
|
||||||
|
Uint32 kobo_bench_seed = 12345;
|
||||||
|
|
||||||
|
extern "C" void kobo_bench_init_env(void)
|
||||||
|
{
|
||||||
|
const char *e = getenv("KOBO_BENCH");
|
||||||
|
kobo_bench_enabled = (e && *e && *e != '0') ? 1 : 0;
|
||||||
|
|
||||||
|
const char *f = getenv("KOBO_BENCH_FRAMES");
|
||||||
|
if (f && *f) kobo_bench_frames = atoi(f);
|
||||||
|
|
||||||
|
const char *s = getenv("KOBO_BENCH_SEED");
|
||||||
|
if (s && *s) kobo_bench_seed = (Uint32)strtoul(s, NULL, 10);
|
||||||
|
|
||||||
|
if (!kobo_bench_enabled) return;
|
||||||
|
|
||||||
|
// Default a steady stress workload so the move pass has predictable cost.
|
||||||
|
if (!getenv("KOBO_STRESS"))
|
||||||
|
setenv("KOBO_STRESS", "1500", 1);
|
||||||
|
// Profiler is the whole point of running in bench mode.
|
||||||
|
if (!getenv("KOBO_PROF"))
|
||||||
|
setenv("KOBO_PROF", "1", 1);
|
||||||
|
|
||||||
|
fprintf(stderr,
|
||||||
|
"[bench] enabled: frames=%d seed=%u stress=%s prof=%s\n",
|
||||||
|
kobo_bench_frames,
|
||||||
|
(unsigned)kobo_bench_seed,
|
||||||
|
getenv("KOBO_STRESS"),
|
||||||
|
getenv("KOBO_PROF"));
|
||||||
|
}
|
||||||
36
bench.h
Normal file
36
bench.h
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
// Deterministic, hands-off benchmark mode.
|
||||||
|
//
|
||||||
|
// Activated by KOBO_BENCH=1 in the environment. When on, the game:
|
||||||
|
// * skips the intro/menu sequence and drops straight into stage 0
|
||||||
|
// * seeds gamerand and pubrand from KOBO_BENCH_SEED (default 12345)
|
||||||
|
// * enables cmd_indicator (collisions register but neither side takes
|
||||||
|
// damage — true invulnerability for deterministic workload),
|
||||||
|
// cmd_cheat (skip score recording), and always_fire (auto-fire)
|
||||||
|
// * defaults KOBO_STRESS=1500 unless overridden, for a steady workload
|
||||||
|
// * dumps the profiler table and exits after KOBO_BENCH_FRAMES
|
||||||
|
// run_game frames (default 1800 = ~60s at the game's tick rate)
|
||||||
|
//
|
||||||
|
// Together this makes a single command (`KOBO_BENCH=1 ./kobodl`) produce a
|
||||||
|
// reproducible profiler dump across phases — no menu navigation, no keyboard
|
||||||
|
// input, identical RNG and spawn pattern run-to-run.
|
||||||
|
|
||||||
|
#ifndef KOBO_BENCH_H
|
||||||
|
#define KOBO_BENCH_H
|
||||||
|
|
||||||
|
#include "sdl_compat.h"
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
extern int kobo_bench_enabled;
|
||||||
|
extern int kobo_bench_frames;
|
||||||
|
extern Uint32 kobo_bench_seed;
|
||||||
|
|
||||||
|
void kobo_bench_init_env(void);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif // KOBO_BENCH_H
|
||||||
48
build-web.sh
Executable file
48
build-web.sh
Executable file
@ -0,0 +1,48 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Build Kobo Deluxe as a WebAssembly app via Emscripten.
|
||||||
|
#
|
||||||
|
# Prereq: install and activate emsdk so emcmake / emcc are on PATH.
|
||||||
|
# git clone https://github.com/emscripten-core/emsdk ~/emsdk
|
||||||
|
# ~/emsdk/emsdk install latest && ~/emsdk/emsdk activate latest
|
||||||
|
# . ~/emsdk/emsdk_env.sh
|
||||||
|
#
|
||||||
|
# Output: build-web/kobodl.{html,js,wasm,data}
|
||||||
|
# Serve from build-web/ with any static server that sets the COOP/COEP headers
|
||||||
|
# required for SharedArrayBuffer (pthreads). Example:
|
||||||
|
# ./build-web.sh && ./build-web.sh serve
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
BUILD="$HERE/build-web"
|
||||||
|
|
||||||
|
case "${1:-build}" in
|
||||||
|
serve)
|
||||||
|
cd "$BUILD"
|
||||||
|
exec python3 -c "
|
||||||
|
import http.server, socketserver
|
||||||
|
class H(http.server.SimpleHTTPRequestHandler):
|
||||||
|
def end_headers(self):
|
||||||
|
self.send_header('Cross-Origin-Opener-Policy', 'same-origin')
|
||||||
|
self.send_header('Cross-Origin-Embedder-Policy', 'require-corp')
|
||||||
|
super().end_headers()
|
||||||
|
socketserver.TCPServer(('', 8000), H).serve_forever()
|
||||||
|
"
|
||||||
|
;;
|
||||||
|
clean)
|
||||||
|
rm -rf "$BUILD"
|
||||||
|
;;
|
||||||
|
build|*)
|
||||||
|
command -v emcmake >/dev/null 2>&1 || {
|
||||||
|
echo "emcmake not found. Source emsdk_env.sh first." >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
mkdir -p "$BUILD"
|
||||||
|
cd "$BUILD"
|
||||||
|
emcmake cmake -DCMAKE_BUILD_TYPE=Release "$HERE"
|
||||||
|
cmake --build . -j
|
||||||
|
echo
|
||||||
|
echo "Built: $BUILD/index.html"
|
||||||
|
echo "Serve: ./build-web.sh serve (then open http://localhost:8000/)"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
@ -309,33 +309,32 @@ void dashboard_window_t::nibble(int tool)
|
|||||||
}
|
}
|
||||||
gengine->invalidate();
|
gengine->invalidate();
|
||||||
gengine->flip();
|
gengine->flip();
|
||||||
if(gengine->doublebuffer())
|
// SDL 3: one logical back buffer; previously a second pass was
|
||||||
|
// only needed when actual hardware double-buffering was in use.
|
||||||
|
for(i = 0; i < dt * 4; ++i)
|
||||||
{
|
{
|
||||||
for(i = 0; i < dt * 4; ++i)
|
if(last_index >= NIBBLE_TILES)
|
||||||
|
break;
|
||||||
|
switch (tool)
|
||||||
{
|
{
|
||||||
if(last_index >= NIBBLE_TILES)
|
case 0:
|
||||||
break;
|
fillrect(x[last_index] +
|
||||||
switch (tool)
|
NIBBLE_W / 2,
|
||||||
{
|
y[last_index] +
|
||||||
case 0:
|
NIBBLE_H / 2,
|
||||||
fillrect(x[last_index] +
|
NIBBLE_W,
|
||||||
NIBBLE_W / 2,
|
NIBBLE_H);
|
||||||
y[last_index] +
|
break;
|
||||||
NIBBLE_H / 2,
|
default:
|
||||||
NIBBLE_W,
|
sprite(x[last_index] - 8 +
|
||||||
NIBBLE_H);
|
NIBBLE_W / 2,
|
||||||
break;
|
y[last_index] - 8 +
|
||||||
default:
|
NIBBLE_H / 2,
|
||||||
sprite(x[last_index] - 8 +
|
B_BRUSHES,
|
||||||
NIBBLE_W / 2,
|
tool - 1);
|
||||||
y[last_index] - 8 +
|
break;
|
||||||
NIBBLE_H / 2,
|
|
||||||
B_BRUSHES,
|
|
||||||
tool - 1);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
++last_index;
|
|
||||||
}
|
}
|
||||||
|
++last_index;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mode(DASHBOARD_BLACK);
|
mode(DASHBOARD_BLACK);
|
||||||
|
|||||||
19
enemies.cpp
19
enemies.cpp
@ -24,6 +24,7 @@
|
|||||||
#include "enemies.h"
|
#include "enemies.h"
|
||||||
#include "random.h"
|
#include "random.h"
|
||||||
#include "radar.h"
|
#include "radar.h"
|
||||||
|
#include "prof.h"
|
||||||
|
|
||||||
_enemy _enemies::enemy[ENEMY_MAX];
|
_enemy _enemies::enemy[ENEMY_MAX];
|
||||||
_enemy *_enemies::enemy_max;
|
_enemy *_enemies::enemy_max;
|
||||||
@ -99,14 +100,20 @@ int _enemies::init()
|
|||||||
void _enemies::move()
|
void _enemies::move()
|
||||||
{
|
{
|
||||||
_enemy *enemyp;
|
_enemy *enemyp;
|
||||||
/* realize reserved enemies */
|
|
||||||
for(enemyp = enemy; enemyp < enemy + ENEMY_MAX; enemyp++)
|
|
||||||
{
|
{
|
||||||
if(enemyp->realize())
|
PROF_ZONE("enemies.realize");
|
||||||
enemy_max = enemyp;
|
/* realize reserved enemies */
|
||||||
|
for(enemyp = enemy; enemyp < enemy + ENEMY_MAX; enemyp++)
|
||||||
|
{
|
||||||
|
if(enemyp->realize())
|
||||||
|
enemy_max = enemyp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{
|
||||||
|
PROF_ZONE("enemies.move_pass");
|
||||||
|
for(enemyp = enemy; enemyp <= enemy_max; enemyp++)
|
||||||
|
enemyp->move();
|
||||||
}
|
}
|
||||||
for(enemyp = enemy; enemyp <= enemy_max; enemyp++)
|
|
||||||
enemyp->move();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _enemies::move_intro()
|
void _enemies::move_intro()
|
||||||
|
|||||||
@ -72,7 +72,7 @@ extern const enemy_kind bombdeto;
|
|||||||
extern const enemy_kind cannon;
|
extern const enemy_kind cannon;
|
||||||
extern const enemy_kind pipe1;
|
extern const enemy_kind pipe1;
|
||||||
extern const enemy_kind core;
|
extern const enemy_kind core;
|
||||||
extern const enemy_kind pipe2;
|
extern const enemy_kind pipe2k;
|
||||||
extern const enemy_kind rock;
|
extern const enemy_kind rock;
|
||||||
extern const enemy_kind ring;
|
extern const enemy_kind ring;
|
||||||
extern const enemy_kind enemy_m1;
|
extern const enemy_kind enemy_m1;
|
||||||
@ -434,7 +434,7 @@ inline int _enemy::realize()
|
|||||||
|
|
||||||
inline int _enemy::is_pipe()
|
inline int _enemy::is_pipe()
|
||||||
{
|
{
|
||||||
return ((_state != notuse) && ((ek == &pipe1) || (ek == &pipe2)));
|
return ((_state != notuse) && ((ek == &pipe1) || (ek == &pipe2k)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
18
enemy.cpp
18
enemy.cpp
@ -755,10 +755,10 @@ void _enemy::move_core()
|
|||||||
|
|
||||||
void _enemy::kill_core()
|
void _enemy::kill_core()
|
||||||
{
|
{
|
||||||
enemies.make(&pipe2, CS2PIXEL(x), CS2PIXEL(y), 0, 0, 3);
|
enemies.make(&pipe2k, CS2PIXEL(x), CS2PIXEL(y), 0, 0, 3);
|
||||||
enemies.make(&pipe2, CS2PIXEL(x), CS2PIXEL(y), 0, 0, 7);
|
enemies.make(&pipe2k, CS2PIXEL(x), CS2PIXEL(y), 0, 0, 7);
|
||||||
enemies.make(&pipe2, CS2PIXEL(x), CS2PIXEL(y), 0, 0, 1);
|
enemies.make(&pipe2k, CS2PIXEL(x), CS2PIXEL(y), 0, 0, 1);
|
||||||
enemies.make(&pipe2, CS2PIXEL(x), CS2PIXEL(y), 0, 0, 5);
|
enemies.make(&pipe2k, CS2PIXEL(x), CS2PIXEL(y), 0, 0, 5);
|
||||||
enemies.make(&explosion4, CS2PIXEL(x), CS2PIXEL(y));
|
enemies.make(&explosion4, CS2PIXEL(x), CS2PIXEL(y));
|
||||||
sound.g_base_core_explo(x, y);
|
sound.g_base_core_explo(x, y);
|
||||||
release();
|
release();
|
||||||
@ -978,19 +978,19 @@ void _enemy::move_pipe2()
|
|||||||
}
|
}
|
||||||
p ^= a;
|
p ^= a;
|
||||||
if(p & U_MASK)
|
if(p & U_MASK)
|
||||||
enemies.make(&pipe2, CS2PIXEL(x), CS2PIXEL(y), 0, 0, 1);
|
enemies.make(&pipe2k, CS2PIXEL(x), CS2PIXEL(y), 0, 0, 1);
|
||||||
if(p & R_MASK)
|
if(p & R_MASK)
|
||||||
enemies.make(&pipe2, CS2PIXEL(x), CS2PIXEL(y), 0, 0, 3);
|
enemies.make(&pipe2k, CS2PIXEL(x), CS2PIXEL(y), 0, 0, 3);
|
||||||
if(p & D_MASK)
|
if(p & D_MASK)
|
||||||
enemies.make(&pipe2, CS2PIXEL(x), CS2PIXEL(y), 0, 0, 5);
|
enemies.make(&pipe2k, CS2PIXEL(x), CS2PIXEL(y), 0, 0, 5);
|
||||||
if(p & L_MASK)
|
if(p & L_MASK)
|
||||||
enemies.make(&pipe2, CS2PIXEL(x), CS2PIXEL(y), 0, 0, 7);
|
enemies.make(&pipe2k, CS2PIXEL(x), CS2PIXEL(y), 0, 0, 7);
|
||||||
manage.add_score(10);
|
manage.add_score(10);
|
||||||
release();
|
release();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const enemy_kind pipe2 = {
|
const enemy_kind pipe2k = {
|
||||||
0,
|
0,
|
||||||
&_enemy::make_pipe2,
|
&_enemy::make_pipe2,
|
||||||
&_enemy::move_pipe2,
|
&_enemy::move_pipe2,
|
||||||
|
|||||||
@ -9,6 +9,7 @@ add_library(graphics STATIC
|
|||||||
toolkit.cpp
|
toolkit.cpp
|
||||||
vidmodes.c
|
vidmodes.c
|
||||||
region.c
|
region.c
|
||||||
|
img_compat.c
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(graphics
|
target_include_directories(graphics
|
||||||
|
|||||||
@ -23,7 +23,7 @@
|
|||||||
#include <math.h>
|
#include <math.h>
|
||||||
#include "logger.h"
|
#include "logger.h"
|
||||||
#include "sdl_compat.h"
|
#include "sdl_compat.h"
|
||||||
#include <SDL3_image/SDL_image.h>
|
#include "img_compat.h"
|
||||||
#include "sprite.h"
|
#include "sprite.h"
|
||||||
#include "filters.h"
|
#include "filters.h"
|
||||||
|
|
||||||
|
|||||||
@ -28,7 +28,7 @@
|
|||||||
|
|
||||||
#include "gfxengine.h"
|
#include "gfxengine.h"
|
||||||
#include "filters.h"
|
#include "filters.h"
|
||||||
#include <SDL3_image/SDL_image.h>
|
#include "img_compat.h"
|
||||||
#include "sdl_compat.h"
|
#include "sdl_compat.h"
|
||||||
#include "sofont.h"
|
#include "sofont.h"
|
||||||
#include "window.h"
|
#include "window.h"
|
||||||
@ -54,15 +54,10 @@ gfxengine_t::gfxengine_t()
|
|||||||
sf1 = df = dsf = acf = NULL;
|
sf1 = df = dsf = acf = NULL;
|
||||||
gfx = NULL;
|
gfx = NULL;
|
||||||
csengine = NULL;
|
csengine = NULL;
|
||||||
_driver = GFX_DRIVER_SDL2D;
|
|
||||||
_shadow = 1;
|
|
||||||
_doublebuf = 1;
|
|
||||||
_pages = -1;
|
|
||||||
_vsync = 1;
|
_vsync = 1;
|
||||||
_fullscreen = 0;
|
_fullscreen = 0;
|
||||||
_centered = 0;
|
_centered = 0;
|
||||||
use_interpolation = 1;
|
use_interpolation = 1;
|
||||||
_depth = 0;
|
|
||||||
_title = "GfxEngine v0.4";
|
_title = "GfxEngine v0.4";
|
||||||
_icontitle = "GfxEngine";
|
_icontitle = "GfxEngine";
|
||||||
_cursor = 1;
|
_cursor = 1;
|
||||||
@ -71,11 +66,9 @@ gfxengine_t::gfxengine_t()
|
|||||||
_autoinvalidate = 1;
|
_autoinvalidate = 1;
|
||||||
_scalemode = GFX_SCALE_NEAREST;
|
_scalemode = GFX_SCALE_NEAREST;
|
||||||
_clamping = 0;
|
_clamping = 0;
|
||||||
xflags = 0;
|
|
||||||
|
|
||||||
_dither = 0;
|
_dither = 0;
|
||||||
_dither_type = 0;
|
_dither_type = 0;
|
||||||
broken_rgba8 = 0;
|
|
||||||
alpha_threshold = 0;
|
alpha_threshold = 0;
|
||||||
|
|
||||||
_brightness = 1.0;
|
_brightness = 1.0;
|
||||||
@ -94,10 +87,7 @@ gfxengine_t::gfxengine_t()
|
|||||||
for(int i = 0; i < CS_LAYERS ; ++i)
|
for(int i = 0; i < CS_LAYERS ; ++i)
|
||||||
xratio[i] = yratio[i] = 0.0;
|
xratio[i] = yratio[i] = 0.0;
|
||||||
|
|
||||||
dirtyrects[0] = 0;
|
dirtyrects = 0;
|
||||||
dirtyrects[1] = 0;
|
|
||||||
frontpage = 0;
|
|
||||||
backpage = 1;
|
|
||||||
screenshot_count = 0;
|
screenshot_count = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -160,75 +150,23 @@ void gfxengine_t::scale(float x, float y)
|
|||||||
|
|
||||||
void gfxengine_t::mode(int bits, int fullscreen)
|
void gfxengine_t::mode(int bits, int fullscreen)
|
||||||
{
|
{
|
||||||
|
// SDL 3 uses the renderer's pixel format directly; the legacy depth
|
||||||
|
// argument is accepted for API compatibility but ignored.
|
||||||
|
(void)bits;
|
||||||
int was_showing = is_showing;
|
int was_showing = is_showing;
|
||||||
hide();
|
hide();
|
||||||
|
|
||||||
int olddepth = _depth;
|
|
||||||
_depth = bits;
|
|
||||||
if(_depth != olddepth)
|
|
||||||
reload();
|
|
||||||
_fullscreen = fullscreen;
|
_fullscreen = fullscreen;
|
||||||
|
|
||||||
if(was_showing)
|
if(was_showing)
|
||||||
show();
|
show();
|
||||||
}
|
}
|
||||||
|
|
||||||
void gfxengine_t::driver(gfx_drivers_t drv)
|
|
||||||
{
|
|
||||||
int was_showing = is_showing;
|
|
||||||
hide();
|
|
||||||
|
|
||||||
_driver = drv;
|
|
||||||
|
|
||||||
if(was_showing)
|
|
||||||
show();
|
|
||||||
}
|
|
||||||
|
|
||||||
void gfxengine_t::doublebuffer(int use)
|
|
||||||
{
|
|
||||||
if(_doublebuf == use)
|
|
||||||
return;
|
|
||||||
|
|
||||||
int was_showing = is_showing;
|
|
||||||
hide();
|
|
||||||
|
|
||||||
_doublebuf = use;
|
|
||||||
|
|
||||||
if(was_showing)
|
|
||||||
show();
|
|
||||||
}
|
|
||||||
|
|
||||||
void gfxengine_t::pages(int np)
|
|
||||||
{
|
|
||||||
_pages = np;
|
|
||||||
}
|
|
||||||
|
|
||||||
void gfxengine_t::vsync(int use)
|
void gfxengine_t::vsync(int use)
|
||||||
{
|
{
|
||||||
if(_vsync == use)
|
if(_vsync == use)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
int was_showing = is_showing;
|
|
||||||
hide();
|
|
||||||
|
|
||||||
_vsync = use;
|
_vsync = use;
|
||||||
|
if(sdl_renderer)
|
||||||
if(was_showing)
|
SDL_SetRenderVSync(sdl_renderer, use ? 1 : 0);
|
||||||
show();
|
|
||||||
}
|
|
||||||
|
|
||||||
void gfxengine_t::shadow(int use)
|
|
||||||
{
|
|
||||||
if(_shadow == use)
|
|
||||||
return;
|
|
||||||
|
|
||||||
int was_showing = is_showing;
|
|
||||||
hide();
|
|
||||||
|
|
||||||
_shadow = use;
|
|
||||||
|
|
||||||
if(was_showing)
|
|
||||||
show();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void gfxengine_t::autoinvalidate(int use)
|
void gfxengine_t::autoinvalidate(int use)
|
||||||
@ -435,9 +373,9 @@ void gfxengine_t::clampcolor(Uint8 r, Uint8 g, Uint8 b, Uint8 a)
|
|||||||
|
|
||||||
void gfxengine_t::dither(int type, int _broken_rgba8)
|
void gfxengine_t::dither(int type, int _broken_rgba8)
|
||||||
{
|
{
|
||||||
|
(void)_broken_rgba8; // legacy OpenGL workaround; no-op in SDL 3
|
||||||
_dither = type >= 0;
|
_dither = type >= 0;
|
||||||
_dither_type = type;
|
_dither_type = type;
|
||||||
broken_rgba8 = _broken_rgba8;
|
|
||||||
|
|
||||||
if(!df)
|
if(!df)
|
||||||
return;
|
return;
|
||||||
@ -678,10 +616,8 @@ void gfxengine_t::on_frame(cs_engine_t *e)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
int gfxengine_t::open(int objects, int extraflags)
|
int gfxengine_t::open(int objects)
|
||||||
{
|
{
|
||||||
xflags = extraflags;
|
|
||||||
|
|
||||||
if(is_open)
|
if(is_open)
|
||||||
return show();
|
return show();
|
||||||
|
|
||||||
@ -917,42 +853,23 @@ void gfxengine_t::hide(void)
|
|||||||
|
|
||||||
void gfxengine_t::invalidate(SDL_Rect *rect, window_t *window)
|
void gfxengine_t::invalidate(SDL_Rect *rect, window_t *window)
|
||||||
{
|
{
|
||||||
switch(_pages)
|
__invalidate(rect, window);
|
||||||
{
|
|
||||||
case -1:
|
|
||||||
if(_doublebuf)
|
|
||||||
__invalidate(1, rect, window);
|
|
||||||
__invalidate(0, rect, window);
|
|
||||||
break;
|
|
||||||
case 0:
|
|
||||||
__invalidate(0, NULL, NULL);
|
|
||||||
break;
|
|
||||||
case 3:
|
|
||||||
__invalidate(2, rect, window);
|
|
||||||
// Fallthrough!
|
|
||||||
case 2:
|
|
||||||
__invalidate(1, rect, window);
|
|
||||||
// Fallthrough!
|
|
||||||
case 1:
|
|
||||||
__invalidate(0, rect, window);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void gfxengine_t::__invalidate(int page, SDL_Rect *rect, window_t *window)
|
void gfxengine_t::__invalidate(SDL_Rect *rect, window_t *window)
|
||||||
{
|
{
|
||||||
if(!screen_surface)
|
if(!screen_surface)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if(!rect || (_pages == 0))
|
if(!rect)
|
||||||
{
|
{
|
||||||
dirtyrects[page] = 1;
|
dirtyrects = 1;
|
||||||
dirtytable[page][0].x = 0;
|
dirtytable[0].x = 0;
|
||||||
dirtytable[page][0].y = 0;
|
dirtytable[0].y = 0;
|
||||||
dirtytable[page][0].w = screen_surface->w;
|
dirtytable[0].w = screen_surface->w;
|
||||||
dirtytable[page][0].h = screen_surface->h;
|
dirtytable[0].h = screen_surface->h;
|
||||||
dirtywtable[page][0] = NULL;
|
dirtywtable[0] = NULL;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -989,19 +906,18 @@ void gfxengine_t::__invalidate(int page, SDL_Rect *rect, window_t *window)
|
|||||||
if(!dr.w || !dr.h)
|
if(!dr.w || !dr.h)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
for(int i = 0; i < dirtyrects[page]; ++i)
|
for(int i = 0; i < dirtyrects; ++i)
|
||||||
if(memcmp(&dirtytable[page][i], &dr, sizeof(dr)) == 0)
|
if(memcmp(&dirtytable[i], &dr, sizeof(dr)) == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if(dirtyrects[page] < MAX_DIRTYRECTS - 1)
|
if(dirtyrects < MAX_DIRTYRECTS - 1)
|
||||||
{
|
{
|
||||||
dirtytable[page][dirtyrects[page]] = dr;
|
dirtytable[dirtyrects] = dr;
|
||||||
dirtywtable[page][dirtyrects[page]] = window;
|
dirtywtable[dirtyrects] = window;
|
||||||
++dirtyrects[page];
|
++dirtyrects;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
log_printf(ELOG, "gfxengine: Page %d out of dirtyrects!\n",
|
log_printf(ELOG, "gfxengine: Out of dirtyrects!\n");
|
||||||
page);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -1268,20 +1184,19 @@ void gfxengine_t::flip()
|
|||||||
// buffer per frame. Keep the dirtyrect iteration only to drive the
|
// buffer per frame. Keep the dirtyrect iteration only to drive the
|
||||||
// phys_refresh() callbacks that window widgets rely on; the upload
|
// phys_refresh() callbacks that window widgets rely on; the upload
|
||||||
// is a single full-surface copy regardless.
|
// is a single full-surface copy regardless.
|
||||||
frontpage = backpage = 0;
|
for(int i = 0; i < dirtyrects; ++i)
|
||||||
for(int i = 0; i < dirtyrects[backpage]; ++i)
|
|
||||||
{
|
{
|
||||||
if(dirtywtable[backpage][i])
|
if(dirtywtable[i])
|
||||||
{
|
{
|
||||||
if(!dirtywtable[backpage][i]->visible())
|
if(!dirtywtable[i]->visible())
|
||||||
continue;
|
continue;
|
||||||
SDL_Rect dr = dirtytable[backpage][i];
|
SDL_Rect dr = dirtytable[i];
|
||||||
dirtywtable[backpage][i]->phys_refresh(&dr);
|
dirtywtable[i]->phys_refresh(&dr);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
refresh_rect(&dirtytable[backpage][i]);
|
refresh_rect(&dirtytable[i]);
|
||||||
}
|
}
|
||||||
dirtyrects[backpage] = 0;
|
dirtyrects = 0;
|
||||||
|
|
||||||
SDL_UpdateTexture(fb_texture, NULL, softbuf->pixels, softbuf->pitch);
|
SDL_UpdateTexture(fb_texture, NULL, softbuf->pixels, softbuf->pitch);
|
||||||
SDL_RenderClear(sdl_renderer);
|
SDL_RenderClear(sdl_renderer);
|
||||||
|
|||||||
@ -24,7 +24,6 @@
|
|||||||
|
|
||||||
#define GFX_BANKS 256
|
#define GFX_BANKS 256
|
||||||
#define MAX_DIRTYRECTS 1024
|
#define MAX_DIRTYRECTS 1024
|
||||||
#define MAX_PAGES 3
|
|
||||||
|
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
@ -33,11 +32,6 @@
|
|||||||
#include "sprite.h"
|
#include "sprite.h"
|
||||||
#include "cs.h"
|
#include "cs.h"
|
||||||
|
|
||||||
enum gfx_drivers_t
|
|
||||||
{
|
|
||||||
GFX_DRIVER_SDL2D = 0
|
|
||||||
};
|
|
||||||
|
|
||||||
enum gfx_scalemodes_t
|
enum gfx_scalemodes_t
|
||||||
{
|
{
|
||||||
GFX_SCALE_NEAREST = 0,
|
GFX_SCALE_NEAREST = 0,
|
||||||
@ -69,25 +63,11 @@ class gfxengine_t
|
|||||||
void size(int w, int h);
|
void size(int w, int h);
|
||||||
void centered(int c);
|
void centered(int c);
|
||||||
void scale(float x, float y);
|
void scale(float x, float y);
|
||||||
void driver(gfx_drivers_t drv);
|
|
||||||
void mode(int bits, int fullscreen);
|
void mode(int bits, int fullscreen);
|
||||||
|
|
||||||
// 1: Use double buffering if possible
|
|
||||||
void doublebuffer(int use);
|
|
||||||
|
|
||||||
// -1: Use default for shadow() and doublebuffer() settings
|
|
||||||
// 0: None; assume flipping gives you a garbage buffer
|
|
||||||
// 1: Assume flipping leaves the back buffer intact
|
|
||||||
// 2: Assume two buffers that are swapped when flipping
|
|
||||||
// 3: Assume three buffers cycled when flipping
|
|
||||||
void pages(int np);
|
|
||||||
|
|
||||||
// 1: Enable vsync, if available
|
// 1: Enable vsync, if available
|
||||||
void vsync(int use);
|
void vsync(int use);
|
||||||
|
|
||||||
// 1: Use a software shadow back buffer, if possible
|
|
||||||
void shadow(int use);
|
|
||||||
|
|
||||||
void autoinvalidate(int use);
|
void autoinvalidate(int use);
|
||||||
|
|
||||||
void interpolation(int inter);
|
void interpolation(int inter);
|
||||||
@ -102,12 +82,10 @@ class gfxengine_t
|
|||||||
void wrap(int x, int y);
|
void wrap(int x, int y);
|
||||||
|
|
||||||
/* Info */
|
/* Info */
|
||||||
int doublebuffer() { return _doublebuf; }
|
|
||||||
int shadow() { return _shadow; }
|
|
||||||
int autoinvalidate() { return _autoinvalidate; }
|
int autoinvalidate() { return _autoinvalidate; }
|
||||||
|
|
||||||
/* Engine open/close */
|
/* Engine open/close */
|
||||||
int open(int objects = 1024, int extraflags = 0);
|
int open(int objects = 1024);
|
||||||
void close();
|
void close();
|
||||||
|
|
||||||
/* Data management (use while engine is open) */
|
/* Data management (use while engine is open) */
|
||||||
@ -206,7 +184,6 @@ class gfxengine_t
|
|||||||
float yscale() { return ys * (1.f/256.f); }
|
float yscale() { return ys * (1.f/256.f); }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
gfx_drivers_t _driver;
|
|
||||||
gfx_scalemodes_t _scalemode;
|
gfx_scalemodes_t _scalemode;
|
||||||
int _clamping;
|
int _clamping;
|
||||||
SDL_Window *sdl_window;
|
SDL_Window *sdl_window;
|
||||||
@ -214,11 +191,9 @@ class gfxengine_t
|
|||||||
SDL_Texture *fb_texture;
|
SDL_Texture *fb_texture;
|
||||||
SDL_Surface *screen_surface; // alias for softbuf in SDL 3
|
SDL_Surface *screen_surface; // alias for softbuf in SDL 3
|
||||||
SDL_Surface *softbuf;
|
SDL_Surface *softbuf;
|
||||||
int backpage;
|
int dirtyrects;
|
||||||
int frontpage;
|
SDL_Rect dirtytable[MAX_DIRTYRECTS];
|
||||||
int dirtyrects[MAX_PAGES];
|
window_t *dirtywtable[MAX_DIRTYRECTS];
|
||||||
SDL_Rect dirtytable[MAX_PAGES][MAX_DIRTYRECTS];
|
|
||||||
window_t *dirtywtable[MAX_PAGES][MAX_DIRTYRECTS];
|
|
||||||
window_t *fullwin;
|
window_t *fullwin;
|
||||||
window_t *window;
|
window_t *window;
|
||||||
window_t *windows; // Linked list
|
window_t *windows; // Linked list
|
||||||
@ -236,23 +211,17 @@ class gfxengine_t
|
|||||||
s_container_t *gfx;
|
s_container_t *gfx;
|
||||||
SoFont *fonts[GFX_BANKS]; // Kludge.
|
SoFont *fonts[GFX_BANKS]; // Kludge.
|
||||||
cs_engine_t *csengine;
|
cs_engine_t *csengine;
|
||||||
int xflags;
|
|
||||||
int _doublebuf;
|
|
||||||
int _pages;
|
|
||||||
int _vsync;
|
int _vsync;
|
||||||
int _shadow;
|
|
||||||
int _fullscreen;
|
int _fullscreen;
|
||||||
int _centered;
|
int _centered;
|
||||||
int _autoinvalidate;
|
int _autoinvalidate;
|
||||||
int use_interpolation;
|
int use_interpolation;
|
||||||
int _width, _height;
|
int _width, _height;
|
||||||
int _depth;
|
|
||||||
const char *_title;
|
const char *_title;
|
||||||
const char *_icontitle;
|
const char *_icontitle;
|
||||||
int _cursor;
|
int _cursor;
|
||||||
int _dither;
|
int _dither;
|
||||||
int _dither_type;
|
int _dither_type;
|
||||||
int broken_rgba8; //Klugde for OpenGL (if RGBA8 ==> RGBA4)
|
|
||||||
int alpha_threshold; //For noalpha()
|
int alpha_threshold; //For noalpha()
|
||||||
float _brightness;
|
float _brightness;
|
||||||
float _contrast;
|
float _contrast;
|
||||||
@ -267,8 +236,7 @@ class gfxengine_t
|
|||||||
|
|
||||||
int screenshot_count;
|
int screenshot_count;
|
||||||
|
|
||||||
void __invalidate(int page, SDL_Rect *rect = NULL,
|
void __invalidate(SDL_Rect *rect = NULL, window_t *window = NULL);
|
||||||
window_t *window = NULL);
|
|
||||||
void refresh_rect(SDL_Rect *r);
|
void refresh_rect(SDL_Rect *r);
|
||||||
|
|
||||||
static void on_frame(cs_engine_t *e);
|
static void on_frame(cs_engine_t *e);
|
||||||
|
|||||||
92
graphics/img_compat.c
Normal file
92
graphics/img_compat.c
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
/*
|
||||||
|
* img_compat.c — libpng-backed IMG_Load for the Emscripten build.
|
||||||
|
*
|
||||||
|
* Returns an RGBA8 SDL_Surface. Handles palette, grayscale, tRNS, and 16-bit
|
||||||
|
* inputs by normalizing to 8-bit RGBA in the read pipeline.
|
||||||
|
*/
|
||||||
|
#ifdef __EMSCRIPTEN__
|
||||||
|
|
||||||
|
#include "img_compat.h"
|
||||||
|
|
||||||
|
#include <png.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
SDL_Surface *IMG_Load(const char *file)
|
||||||
|
{
|
||||||
|
FILE *fp = fopen(file, "rb");
|
||||||
|
if(!fp)
|
||||||
|
return NULL;
|
||||||
|
|
||||||
|
unsigned char hdr[8];
|
||||||
|
if(fread(hdr, 1, 8, fp) != 8 || png_sig_cmp(hdr, 0, 8))
|
||||||
|
{
|
||||||
|
fclose(fp);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
png_structp png = png_create_read_struct(PNG_LIBPNG_VER_STRING,
|
||||||
|
NULL, NULL, NULL);
|
||||||
|
if(!png) { fclose(fp); return NULL; }
|
||||||
|
png_infop info = png_create_info_struct(png);
|
||||||
|
if(!info)
|
||||||
|
{
|
||||||
|
png_destroy_read_struct(&png, NULL, NULL);
|
||||||
|
fclose(fp);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_Surface *surf = NULL;
|
||||||
|
png_bytep *rows = NULL;
|
||||||
|
|
||||||
|
if(setjmp(png_jmpbuf(png)))
|
||||||
|
{
|
||||||
|
if(rows) free(rows);
|
||||||
|
if(surf) SDL_DestroySurface(surf);
|
||||||
|
png_destroy_read_struct(&png, &info, NULL);
|
||||||
|
fclose(fp);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
png_init_io(png, fp);
|
||||||
|
png_set_sig_bytes(png, 8);
|
||||||
|
png_read_info(png, info);
|
||||||
|
|
||||||
|
png_uint_32 w, h;
|
||||||
|
int depth, ctype;
|
||||||
|
png_get_IHDR(png, info, &w, &h, &depth, &ctype, NULL, NULL, NULL);
|
||||||
|
|
||||||
|
if(depth == 16) png_set_strip_16(png);
|
||||||
|
if(ctype == PNG_COLOR_TYPE_PALETTE) png_set_palette_to_rgb(png);
|
||||||
|
if(ctype == PNG_COLOR_TYPE_GRAY && depth < 8)
|
||||||
|
png_set_expand_gray_1_2_4_to_8(png);
|
||||||
|
if(png_get_valid(png, info, PNG_INFO_tRNS))
|
||||||
|
png_set_tRNS_to_alpha(png);
|
||||||
|
if(ctype == PNG_COLOR_TYPE_GRAY || ctype == PNG_COLOR_TYPE_GRAY_ALPHA)
|
||||||
|
png_set_gray_to_rgb(png);
|
||||||
|
if(ctype == PNG_COLOR_TYPE_RGB || ctype == PNG_COLOR_TYPE_GRAY
|
||||||
|
|| ctype == PNG_COLOR_TYPE_PALETTE)
|
||||||
|
png_set_filler(png, 0xFF, PNG_FILLER_AFTER);
|
||||||
|
|
||||||
|
png_read_update_info(png, info);
|
||||||
|
|
||||||
|
surf = SDL_CreateSurface((int)w, (int)h, SDL_PIXELFORMAT_RGBA32);
|
||||||
|
if(!surf)
|
||||||
|
png_error(png, "SDL_CreateSurface failed");
|
||||||
|
|
||||||
|
rows = (png_bytep *)malloc(sizeof(png_bytep) * h);
|
||||||
|
if(!rows)
|
||||||
|
png_error(png, "row table alloc failed");
|
||||||
|
for(png_uint_32 y = 0; y < h; ++y)
|
||||||
|
rows[y] = (png_bytep)((unsigned char *)surf->pixels + y * surf->pitch);
|
||||||
|
|
||||||
|
png_read_image(png, rows);
|
||||||
|
png_read_end(png, NULL);
|
||||||
|
|
||||||
|
free(rows);
|
||||||
|
png_destroy_read_struct(&png, &info, NULL);
|
||||||
|
fclose(fp);
|
||||||
|
return surf;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif /* __EMSCRIPTEN__ */
|
||||||
32
graphics/img_compat.h
Normal file
32
graphics/img_compat.h
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
* img_compat.h — SDL3_image shim.
|
||||||
|
*
|
||||||
|
* On native builds, this is just a passthrough to <SDL3_image/SDL_image.h>.
|
||||||
|
* On Emscripten there is no SDL3_image port yet (only sdl3 and sdl3_ttf), so
|
||||||
|
* we provide an IMG_Load implementation backed by the libpng port. Only PNG
|
||||||
|
* decoding is needed — Kobo Deluxe's assets are all .png.
|
||||||
|
*/
|
||||||
|
#ifndef KOBO_IMG_COMPAT_H
|
||||||
|
#define KOBO_IMG_COMPAT_H
|
||||||
|
|
||||||
|
#ifdef __EMSCRIPTEN__
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
SDL_Surface *IMG_Load(const char *file);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#else /* native */
|
||||||
|
|
||||||
|
#include <SDL3_image/SDL_image.h>
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* KOBO_IMG_COMPAT_H */
|
||||||
@ -30,7 +30,7 @@ TODO: tables as needed when loading banks.
|
|||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include "logger.h"
|
#include "logger.h"
|
||||||
#include "sdl_compat.h"
|
#include "sdl_compat.h"
|
||||||
#include <SDL3_image/SDL_image.h>
|
#include "img_compat.h"
|
||||||
#include "sprite.h"
|
#include "sprite.h"
|
||||||
#include "filters.h"
|
#include "filters.h"
|
||||||
|
|
||||||
|
|||||||
@ -23,6 +23,7 @@
|
|||||||
|
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
#include "window.h"
|
#include "window.h"
|
||||||
#include "toolkit.h"
|
#include "toolkit.h"
|
||||||
|
|||||||
44
httpclient.h
Normal file
44
httpclient.h
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
* httpclient.h — thin async HTTP client.
|
||||||
|
*
|
||||||
|
* Two impls share this header:
|
||||||
|
* httpclient_native.cpp — libcurl-multi, polled from http_pump().
|
||||||
|
* httpclient_web.cpp — emscripten_fetch; http_pump() is a no-op.
|
||||||
|
*
|
||||||
|
* Callbacks fire on the main thread: native impls dispatch from http_pump(),
|
||||||
|
* web from the Emscripten runtime (which is already main-thread).
|
||||||
|
*
|
||||||
|
* The `body` pointer in http_response_t is only valid for the duration of
|
||||||
|
* the callback — copy what you need.
|
||||||
|
*/
|
||||||
|
#ifndef KOBO_HTTPCLIENT_H
|
||||||
|
#define KOBO_HTTPCLIENT_H
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
typedef struct http_response_t {
|
||||||
|
int status; // HTTP status code; 0 = network/transport failure
|
||||||
|
const char *body; // NUL-terminated; valid only during the callback
|
||||||
|
size_t len;
|
||||||
|
} http_response_t;
|
||||||
|
|
||||||
|
typedef void (*http_callback_t)(const http_response_t *resp, void *user);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
void http_init(void);
|
||||||
|
void http_shutdown(void);
|
||||||
|
|
||||||
|
void http_post_json(const char *url, const char *body,
|
||||||
|
http_callback_t cb, void *user);
|
||||||
|
void http_get(const char *url, http_callback_t cb, void *user);
|
||||||
|
|
||||||
|
void http_pump(void);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* KOBO_HTTPCLIENT_H */
|
||||||
175
httpclient_native.cpp
Normal file
175
httpclient_native.cpp
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
/*
|
||||||
|
* httpclient_native.cpp — libcurl-multi backend.
|
||||||
|
*
|
||||||
|
* Single CURLM handle owned by this module. http_post_json / http_get queue
|
||||||
|
* an easy handle; http_pump() runs curl_multi_perform and drains finished
|
||||||
|
* transfers, firing user callbacks exactly once.
|
||||||
|
*
|
||||||
|
* Failure modes (DNS, connect refused, timeout, etc.) are surfaced as
|
||||||
|
* status=0 in the response — caller doesn't have to distinguish curl error
|
||||||
|
* codes from HTTP errors.
|
||||||
|
*/
|
||||||
|
#include "httpclient.h"
|
||||||
|
#include "kobolog.h"
|
||||||
|
|
||||||
|
#include <curl/curl.h>
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
CURLM *g_multi = nullptr;
|
||||||
|
|
||||||
|
struct pending_t {
|
||||||
|
http_callback_t cb;
|
||||||
|
void *user;
|
||||||
|
std::string body;
|
||||||
|
curl_slist *headers = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Keyed by CURL* easy handle so curl_multi_info_read results can be routed.
|
||||||
|
std::unordered_map<CURL *, pending_t *> g_pending;
|
||||||
|
|
||||||
|
size_t write_cb(char *ptr, size_t size, size_t nmemb, void *userdata)
|
||||||
|
{
|
||||||
|
pending_t *p = static_cast<pending_t *>(userdata);
|
||||||
|
p->body.append(ptr, size * nmemb);
|
||||||
|
return size * nmemb;
|
||||||
|
}
|
||||||
|
|
||||||
|
void start_request(CURL *easy, pending_t *pending)
|
||||||
|
{
|
||||||
|
curl_easy_setopt(easy, CURLOPT_WRITEFUNCTION, write_cb);
|
||||||
|
curl_easy_setopt(easy, CURLOPT_WRITEDATA, pending);
|
||||||
|
curl_easy_setopt(easy, CURLOPT_TIMEOUT, 10L);
|
||||||
|
curl_easy_setopt(easy, CURLOPT_CONNECTTIMEOUT, 5L);
|
||||||
|
curl_easy_setopt(easy, CURLOPT_FOLLOWLOCATION, 1L);
|
||||||
|
curl_easy_setopt(easy, CURLOPT_USERAGENT, "kobodl/0.5.1");
|
||||||
|
|
||||||
|
g_pending[easy] = pending;
|
||||||
|
CURLMcode rc = curl_multi_add_handle(g_multi, easy);
|
||||||
|
if(rc != CURLM_OK)
|
||||||
|
{
|
||||||
|
log_printf(ELOG, "http: curl_multi_add_handle failed: %s\n",
|
||||||
|
curl_multi_strerror(rc));
|
||||||
|
// Synthesize a transport failure callback.
|
||||||
|
http_response_t resp{0, "", 0};
|
||||||
|
pending->cb(&resp, pending->user);
|
||||||
|
g_pending.erase(easy);
|
||||||
|
if(pending->headers) curl_slist_free_all(pending->headers);
|
||||||
|
curl_easy_cleanup(easy);
|
||||||
|
delete pending;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
extern "C" void http_init(void)
|
||||||
|
{
|
||||||
|
if(g_multi) return;
|
||||||
|
curl_global_init(CURL_GLOBAL_DEFAULT);
|
||||||
|
g_multi = curl_multi_init();
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" void http_shutdown(void)
|
||||||
|
{
|
||||||
|
if(!g_multi) return;
|
||||||
|
// Abandon any in-flight handles — we're shutting down, callbacks are
|
||||||
|
// moot. Free the resources to avoid leak reports.
|
||||||
|
for(auto &kv : g_pending)
|
||||||
|
{
|
||||||
|
curl_multi_remove_handle(g_multi, kv.first);
|
||||||
|
if(kv.second->headers) curl_slist_free_all(kv.second->headers);
|
||||||
|
curl_easy_cleanup(kv.first);
|
||||||
|
delete kv.second;
|
||||||
|
}
|
||||||
|
g_pending.clear();
|
||||||
|
curl_multi_cleanup(g_multi);
|
||||||
|
g_multi = nullptr;
|
||||||
|
curl_global_cleanup();
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" void http_post_json(const char *url, const char *body,
|
||||||
|
http_callback_t cb, void *user)
|
||||||
|
{
|
||||||
|
if(!g_multi || !cb) return;
|
||||||
|
CURL *easy = curl_easy_init();
|
||||||
|
if(!easy)
|
||||||
|
{
|
||||||
|
http_response_t resp{0, "", 0};
|
||||||
|
cb(&resp, user);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto *pending = new pending_t;
|
||||||
|
pending->cb = cb;
|
||||||
|
pending->user = user;
|
||||||
|
pending->headers = curl_slist_append(nullptr, "Content-Type: application/json");
|
||||||
|
|
||||||
|
curl_easy_setopt(easy, CURLOPT_URL, url);
|
||||||
|
curl_easy_setopt(easy, CURLOPT_POST, 1L);
|
||||||
|
curl_easy_setopt(easy, CURLOPT_COPYPOSTFIELDS, body ? body : "");
|
||||||
|
curl_easy_setopt(easy, CURLOPT_HTTPHEADER, pending->headers);
|
||||||
|
start_request(easy, pending);
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" void http_get(const char *url, http_callback_t cb, void *user)
|
||||||
|
{
|
||||||
|
if(!g_multi || !cb) return;
|
||||||
|
CURL *easy = curl_easy_init();
|
||||||
|
if(!easy)
|
||||||
|
{
|
||||||
|
http_response_t resp{0, "", 0};
|
||||||
|
cb(&resp, user);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto *pending = new pending_t;
|
||||||
|
pending->cb = cb;
|
||||||
|
pending->user = user;
|
||||||
|
|
||||||
|
curl_easy_setopt(easy, CURLOPT_URL, url);
|
||||||
|
curl_easy_setopt(easy, CURLOPT_HTTPGET, 1L);
|
||||||
|
start_request(easy, pending);
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" void http_pump(void)
|
||||||
|
{
|
||||||
|
if(!g_multi) return;
|
||||||
|
int running = 0;
|
||||||
|
curl_multi_perform(g_multi, &running);
|
||||||
|
|
||||||
|
CURLMsg *msg;
|
||||||
|
int msgs_left = 0;
|
||||||
|
while((msg = curl_multi_info_read(g_multi, &msgs_left)) != nullptr)
|
||||||
|
{
|
||||||
|
if(msg->msg != CURLMSG_DONE) continue;
|
||||||
|
|
||||||
|
CURL *easy = msg->easy_handle;
|
||||||
|
auto it = g_pending.find(easy);
|
||||||
|
if(it == g_pending.end())
|
||||||
|
{
|
||||||
|
curl_multi_remove_handle(g_multi, easy);
|
||||||
|
curl_easy_cleanup(easy);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
pending_t *p = it->second;
|
||||||
|
|
||||||
|
long status = 0;
|
||||||
|
if(msg->data.result == CURLE_OK)
|
||||||
|
curl_easy_getinfo(easy, CURLINFO_RESPONSE_CODE, &status);
|
||||||
|
else
|
||||||
|
log_printf(DLOG, "http: transport error: %s\n",
|
||||||
|
curl_easy_strerror(msg->data.result));
|
||||||
|
|
||||||
|
http_response_t resp;
|
||||||
|
resp.status = (int)status;
|
||||||
|
resp.body = p->body.c_str();
|
||||||
|
resp.len = p->body.size();
|
||||||
|
p->cb(&resp, p->user);
|
||||||
|
|
||||||
|
curl_multi_remove_handle(g_multi, easy);
|
||||||
|
if(p->headers) curl_slist_free_all(p->headers);
|
||||||
|
curl_easy_cleanup(easy);
|
||||||
|
g_pending.erase(it);
|
||||||
|
delete p;
|
||||||
|
}
|
||||||
|
}
|
||||||
95
httpclient_web.cpp
Normal file
95
httpclient_web.cpp
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
/*
|
||||||
|
* httpclient_web.cpp — emscripten_fetch backend.
|
||||||
|
*
|
||||||
|
* emscripten_fetch is async and fires onsuccess/onerror on the main thread.
|
||||||
|
* We attach a small heap-owned context to userData; the trampolines invoke
|
||||||
|
* the user callback and free the context.
|
||||||
|
*
|
||||||
|
* http_pump() is a no-op because the runtime drives completion delivery.
|
||||||
|
*/
|
||||||
|
#ifdef __EMSCRIPTEN__
|
||||||
|
|
||||||
|
#include "httpclient.h"
|
||||||
|
|
||||||
|
#include <emscripten/fetch.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
struct ctx_t {
|
||||||
|
http_callback_t cb;
|
||||||
|
void *user;
|
||||||
|
};
|
||||||
|
|
||||||
|
void deliver(emscripten_fetch_t *fetch, int status)
|
||||||
|
{
|
||||||
|
ctx_t *c = static_cast<ctx_t *>(fetch->userData);
|
||||||
|
http_response_t resp;
|
||||||
|
resp.status = status;
|
||||||
|
resp.body = fetch->data ? fetch->data : "";
|
||||||
|
resp.len = (size_t)fetch->numBytes;
|
||||||
|
c->cb(&resp, c->user);
|
||||||
|
emscripten_fetch_close(fetch);
|
||||||
|
delete c;
|
||||||
|
}
|
||||||
|
|
||||||
|
void on_success(emscripten_fetch_t *fetch)
|
||||||
|
{
|
||||||
|
deliver(fetch, (int)fetch->status);
|
||||||
|
}
|
||||||
|
|
||||||
|
void on_error(emscripten_fetch_t *fetch)
|
||||||
|
{
|
||||||
|
// Distinguish: a non-2xx with body (e.g. 500 + JSON) reports its status;
|
||||||
|
// a true transport failure (no connection, CORS reject) reports 0.
|
||||||
|
int status = (fetch->status >= 100 && fetch->status < 600)
|
||||||
|
? (int)fetch->status : 0;
|
||||||
|
deliver(fetch, status);
|
||||||
|
}
|
||||||
|
|
||||||
|
void issue(const char *method, const char *url, const char *body,
|
||||||
|
http_callback_t cb, void *user)
|
||||||
|
{
|
||||||
|
emscripten_fetch_attr_t attr;
|
||||||
|
emscripten_fetch_attr_init(&attr);
|
||||||
|
strncpy(attr.requestMethod, method, sizeof(attr.requestMethod) - 1);
|
||||||
|
attr.attributes = EMSCRIPTEN_FETCH_LOAD_TO_MEMORY;
|
||||||
|
attr.onsuccess = on_success;
|
||||||
|
attr.onerror = on_error;
|
||||||
|
|
||||||
|
static const char *headers[] = {
|
||||||
|
"Content-Type", "application/json", nullptr
|
||||||
|
};
|
||||||
|
if(body)
|
||||||
|
{
|
||||||
|
attr.requestHeaders = headers;
|
||||||
|
attr.requestData = body;
|
||||||
|
attr.requestDataSize = strlen(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto *c = new ctx_t{cb, user};
|
||||||
|
attr.userData = c;
|
||||||
|
emscripten_fetch(&attr, url);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
extern "C" void http_init(void) {}
|
||||||
|
extern "C" void http_shutdown(void) {}
|
||||||
|
extern "C" void http_pump(void) {}
|
||||||
|
|
||||||
|
extern "C" void http_post_json(const char *url, const char *body,
|
||||||
|
http_callback_t cb, void *user)
|
||||||
|
{
|
||||||
|
if(!cb) return;
|
||||||
|
issue("POST", url, body ? body : "", cb, user);
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" void http_get(const char *url, http_callback_t cb, void *user)
|
||||||
|
{
|
||||||
|
if(!cb) return;
|
||||||
|
issue("GET", url, nullptr, cb, user);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif /* __EMSCRIPTEN__ */
|
||||||
77
kobo.cpp
77
kobo.cpp
@ -61,6 +61,9 @@ extern "C" {
|
|||||||
#include "options.h"
|
#include "options.h"
|
||||||
#include "myship.h"
|
#include "myship.h"
|
||||||
#include "enemies.h"
|
#include "enemies.h"
|
||||||
|
#include "prof.h"
|
||||||
|
#include "bench.h"
|
||||||
|
#include "httpclient.h"
|
||||||
|
|
||||||
#define MAX_FPS_RESULTS 64
|
#define MAX_FPS_RESULTS 64
|
||||||
|
|
||||||
@ -635,7 +638,6 @@ int KOBO_main::init_display(prefs_t *p)
|
|||||||
int dw, dh; // Display size
|
int dw, dh; // Display size
|
||||||
int gw, gh; // Game "window" size
|
int gw, gh; // Game "window" size
|
||||||
gengine->title("Kobo Deluxe " VERSION, "kobodl");
|
gengine->title("Kobo Deluxe " VERSION, "kobodl");
|
||||||
gengine->driver((gfx_drivers_t)p->videodriver);
|
|
||||||
|
|
||||||
dw = p->width;
|
dw = p->width;
|
||||||
dh = p->height;
|
dh = p->height;
|
||||||
@ -689,10 +691,7 @@ int KOBO_main::init_display(prefs_t *p)
|
|||||||
gengine->size(dw, dh);
|
gengine->size(dw, dh);
|
||||||
|
|
||||||
gengine->mode(0, p->fullscreen);
|
gengine->mode(0, p->fullscreen);
|
||||||
gengine->doublebuffer(p->doublebuf);
|
|
||||||
gengine->pages(p->pages);
|
|
||||||
gengine->vsync(p->vsync);
|
gengine->vsync(p->vsync);
|
||||||
gengine->shadow(p->shadow);
|
|
||||||
gengine->cursor(0);
|
gengine->cursor(0);
|
||||||
|
|
||||||
gengine->period(game.speed);
|
gengine->period(game.speed);
|
||||||
@ -1086,7 +1085,7 @@ int KOBO_main::load_graphics(prefs_t *p)
|
|||||||
if(gd->flags & KOBO_NODITHER || !p->use_dither)
|
if(gd->flags & KOBO_NODITHER || !p->use_dither)
|
||||||
gengine->dither(-1);
|
gengine->dither(-1);
|
||||||
else
|
else
|
||||||
gengine->dither(p->dither_type, p->broken_rgba8);
|
gengine->dither(p->dither_type, 0);
|
||||||
|
|
||||||
// Alpha channels
|
// Alpha channels
|
||||||
if(gd->flags & KOBO_NOALPHA || !p->alpha)
|
if(gd->flags & KOBO_NOALPHA || !p->alpha)
|
||||||
@ -1391,12 +1390,40 @@ int KOBO_main::open()
|
|||||||
ct_engine.render_highlight = kobo_render_highlight;
|
ct_engine.render_highlight = kobo_render_highlight;
|
||||||
wdash->mode(DASHBOARD_GAME);
|
wdash->mode(DASHBOARD_GAME);
|
||||||
wradar->mode(RM_NOISE);
|
wradar->mode(RM_NOISE);
|
||||||
pubrand.init();
|
|
||||||
|
// Bench mode overrides flags that downstream init reads (always_fire is
|
||||||
|
// captured by gamecontrol.init at call-time, so it has to be set before).
|
||||||
|
if(kobo_bench_enabled)
|
||||||
|
{
|
||||||
|
// cmd_indicator is "collision testing mode" — collisions still
|
||||||
|
// register sounds but neither side takes damage, so the ship
|
||||||
|
// can't die and the bench workload doesn't drift across runs.
|
||||||
|
// (cmd_cheat is *unlimited lives*, not invulnerability.)
|
||||||
|
prefs->cmd_indicator = 1;
|
||||||
|
prefs->cmd_cheat = 1; // skip score recording on exit
|
||||||
|
prefs->always_fire = 1; // constant auto-fire
|
||||||
|
prefs->countdown = 0; // auto-pop the "GET READY" wait
|
||||||
|
}
|
||||||
|
|
||||||
|
if(kobo_bench_enabled)
|
||||||
|
pubrand.init(kobo_bench_seed);
|
||||||
|
else
|
||||||
|
pubrand.init();
|
||||||
init_js(prefs);
|
init_js(prefs);
|
||||||
gamecontrol.init(prefs->always_fire);
|
gamecontrol.init(prefs->always_fire);
|
||||||
manage.init();
|
manage.init();
|
||||||
|
|
||||||
gsm.push(&st_intro_title);
|
if(kobo_bench_enabled)
|
||||||
|
{
|
||||||
|
// Hands-off bench: jump straight into stage 0, no menu navigation.
|
||||||
|
scorefile.profile()->skill = SKILL_CLASSIC;
|
||||||
|
manage.set_scene_num(0);
|
||||||
|
gsm.push(&st_game);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
gsm.push(&st_intro_title);
|
||||||
|
}
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@ -1658,14 +1685,9 @@ void kobo_gfxengine_t::frame()
|
|||||||
{
|
{
|
||||||
km.pause_game();
|
km.pause_game();
|
||||||
prefs->fullscreen = 0;
|
prefs->fullscreen = 0;
|
||||||
prefs->videodriver = (int)GFX_DRIVER_SDL2D;
|
|
||||||
prefs->width = 640;
|
prefs->width = 640;
|
||||||
prefs->height = 480;
|
prefs->height = 480;
|
||||||
prefs->aspect = 1000;
|
prefs->aspect = 1000;
|
||||||
prefs->depth = 0;
|
|
||||||
prefs->doublebuf = 0;
|
|
||||||
prefs->pages = -1;
|
|
||||||
prefs->shadow = 1;
|
|
||||||
prefs->scalemode = (int)GFX_SCALE_NEAREST;
|
prefs->scalemode = (int)GFX_SCALE_NEAREST;
|
||||||
prefs->brightness = 100;
|
prefs->brightness = 100;
|
||||||
prefs->contrast = 100;
|
prefs->contrast = 100;
|
||||||
@ -1839,6 +1861,7 @@ void kobo_gfxengine_t::frame()
|
|||||||
/*
|
/*
|
||||||
* Run the current gamestate for one frame
|
* Run the current gamestate for one frame
|
||||||
*/
|
*/
|
||||||
|
http_pump();
|
||||||
gsm.frame();
|
gsm.frame();
|
||||||
|
|
||||||
if(prefs->cmd_autoshot && !manage.game_stopped())
|
if(prefs->cmd_autoshot && !manage.game_stopped())
|
||||||
@ -2063,7 +2086,11 @@ int main(int argc, char *argv[])
|
|||||||
signal(SIGTERM, breakhandler);
|
signal(SIGTERM, breakhandler);
|
||||||
signal(SIGINT, breakhandler);
|
signal(SIGINT, breakhandler);
|
||||||
|
|
||||||
|
kobo_bench_init_env();
|
||||||
|
prof_init();
|
||||||
|
|
||||||
SDL_Init(0);
|
SDL_Init(0);
|
||||||
|
http_init();
|
||||||
|
|
||||||
if(main_init())
|
if(main_init())
|
||||||
{
|
{
|
||||||
@ -2087,6 +2114,31 @@ int main(int argc, char *argv[])
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// First-run client_id generation. Stable per-install identifier sent
|
||||||
|
// with leaderboard submissions for rate-limiting/dedup (not used as
|
||||||
|
// the public identity — that's name + board_name).
|
||||||
|
if(prefs->client_id[0] == '\0')
|
||||||
|
{
|
||||||
|
// Not cryptographically random — this is a rate-limit dedup
|
||||||
|
// token, not a secret. SDL_rand_bits is sufficient.
|
||||||
|
Uint8 b[16];
|
||||||
|
for(int i = 0; i < 16; i += 4)
|
||||||
|
{
|
||||||
|
Uint32 r = SDL_rand_bits();
|
||||||
|
b[i] = (Uint8)(r >> 24);
|
||||||
|
b[i+1] = (Uint8)(r >> 16);
|
||||||
|
b[i+2] = (Uint8)(r >> 8);
|
||||||
|
b[i+3] = (Uint8)r;
|
||||||
|
}
|
||||||
|
b[6] = (b[6] & 0x0F) | 0x40; // RFC 4122 v4
|
||||||
|
b[8] = (b[8] & 0x3F) | 0x80;
|
||||||
|
snprintf(prefs->client_id, sizeof(prefs->client_id),
|
||||||
|
"%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x",
|
||||||
|
b[0],b[1],b[2],b[3], b[4],b[5], b[6],b[7],
|
||||||
|
b[8],b[9], b[10],b[11],b[12],b[13],b[14],b[15]);
|
||||||
|
prefs->changed = 1;
|
||||||
|
}
|
||||||
|
|
||||||
if(prefs->cmd_options_man)
|
if(prefs->cmd_options_man)
|
||||||
{
|
{
|
||||||
put_options_man();
|
put_options_man();
|
||||||
@ -2170,6 +2222,7 @@ int main(int argc, char *argv[])
|
|||||||
km.print_fps_results();
|
km.print_fps_results();
|
||||||
|
|
||||||
km.close_logging();
|
km.close_logging();
|
||||||
|
http_shutdown();
|
||||||
main_cleanup();
|
main_cleanup();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
260
leaderboard.cpp
Normal file
260
leaderboard.cpp
Normal file
@ -0,0 +1,260 @@
|
|||||||
|
/*
|
||||||
|
* leaderboard.cpp — submission + browse over HTTP.
|
||||||
|
*
|
||||||
|
* Hand-rolled JSON build/parse. The wire schema is tiny and fixed-shape
|
||||||
|
* (see plan); no point in pulling in a JSON dep just to handle two
|
||||||
|
* response types.
|
||||||
|
*
|
||||||
|
* Threading: callbacks fire from http_pump() (native) or the Emscripten
|
||||||
|
* runtime (web) — both single-threaded relative to the game loop, so
|
||||||
|
* mutating the global state structs from the callbacks is safe.
|
||||||
|
*/
|
||||||
|
// pfile.h (pulled in via score.h via leaderboard.h) uses FILE without
|
||||||
|
// including stdio.h itself; pull it in first.
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <ctype.h>
|
||||||
|
|
||||||
|
#include "leaderboard.h"
|
||||||
|
#include "httpclient.h"
|
||||||
|
#include "prefs.h"
|
||||||
|
#include "kobo.h"
|
||||||
|
#include "kobolog.h"
|
||||||
|
|
||||||
|
extern prefs_t *prefs;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
leaderboard_submission_t g_sub = { LB_IDLE, 0, 0 };
|
||||||
|
leaderboard_browse_t g_brw = { LB_IDLE, 0, 0, {}, "" };
|
||||||
|
|
||||||
|
bool enabled()
|
||||||
|
{
|
||||||
|
return prefs && prefs->global_leaderboard
|
||||||
|
&& prefs->leaderboard_url[0] != '\0'
|
||||||
|
&& prefs->leaderboard_board[0] != '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- JSON helpers ----------------------------------------------------------
|
||||||
|
//
|
||||||
|
// Build: snprintf with manual escaping for string fields.
|
||||||
|
// Parse: substring-search the known keys. Tolerates whitespace but assumes
|
||||||
|
// the server emits one occurrence of each key (which our server contract does).
|
||||||
|
|
||||||
|
void json_escape(char *out, size_t cap, const char *in)
|
||||||
|
{
|
||||||
|
size_t o = 0;
|
||||||
|
for(size_t i = 0; in[i] && o + 2 < cap; ++i)
|
||||||
|
{
|
||||||
|
char c = in[i];
|
||||||
|
if(c == '"' || c == '\\')
|
||||||
|
{
|
||||||
|
if(o + 3 >= cap) break;
|
||||||
|
out[o++] = '\\'; out[o++] = c;
|
||||||
|
}
|
||||||
|
else if((unsigned char)c < 0x20)
|
||||||
|
{
|
||||||
|
// Skip control bytes; the in-game name editor only emits ASCII
|
||||||
|
// letters anyway. Logging this would be noise.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
else out[o++] = c;
|
||||||
|
}
|
||||||
|
out[o] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find `"key":` and return pointer to first char after the colon, or NULL.
|
||||||
|
const char *find_key(const char *body, const char *key)
|
||||||
|
{
|
||||||
|
size_t klen = strlen(key);
|
||||||
|
for(const char *p = body; (p = strchr(p, '"')) != NULL; ++p)
|
||||||
|
{
|
||||||
|
++p;
|
||||||
|
if(strncmp(p, key, klen) != 0) continue;
|
||||||
|
if(p[klen] != '"') continue;
|
||||||
|
const char *q = p + klen + 1;
|
||||||
|
while(*q == ' ' || *q == '\t') ++q;
|
||||||
|
if(*q != ':') continue;
|
||||||
|
++q;
|
||||||
|
while(*q == ' ' || *q == '\t') ++q;
|
||||||
|
return q;
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool parse_int(const char *p, int *out)
|
||||||
|
{
|
||||||
|
if(!p) return false;
|
||||||
|
char *end = NULL;
|
||||||
|
long v = strtol(p, &end, 10);
|
||||||
|
if(end == p) return false;
|
||||||
|
*out = (int)v;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copies the next "string" into out, returns pointer past the closing quote.
|
||||||
|
const char *parse_string(const char *p, char *out, size_t cap)
|
||||||
|
{
|
||||||
|
if(!p || *p != '"') { if(cap) out[0] = '\0'; return NULL; }
|
||||||
|
++p;
|
||||||
|
size_t o = 0;
|
||||||
|
while(*p && *p != '"')
|
||||||
|
{
|
||||||
|
if(*p == '\\' && p[1])
|
||||||
|
{
|
||||||
|
if(o + 1 < cap) out[o++] = p[1];
|
||||||
|
p += 2;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if(o + 1 < cap) out[o++] = *p;
|
||||||
|
++p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(cap) out[o] = '\0';
|
||||||
|
return *p == '"' ? p + 1 : NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- callbacks --------------------------------------------------------------
|
||||||
|
|
||||||
|
void on_submit(const http_response_t *resp, void * /*user*/)
|
||||||
|
{
|
||||||
|
if(resp->status == 0)
|
||||||
|
{
|
||||||
|
g_sub.status = LB_OFFLINE;
|
||||||
|
log_printf(WLOG, "leaderboard: submission failed (network)\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(resp->status / 100 != 2)
|
||||||
|
{
|
||||||
|
g_sub.status = LB_FAILED;
|
||||||
|
log_printf(WLOG, "leaderboard: submission rejected (HTTP %d)\n",
|
||||||
|
resp->status);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int rank = -1, total = -1;
|
||||||
|
parse_int(find_key(resp->body, "rank"), &rank);
|
||||||
|
parse_int(find_key(resp->body, "total"), &total);
|
||||||
|
g_sub.rank = rank;
|
||||||
|
g_sub.total = total;
|
||||||
|
g_sub.status = LB_DONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
void on_browse(const http_response_t *resp, void * /*user*/)
|
||||||
|
{
|
||||||
|
if(resp->status == 0) { g_brw.status = LB_OFFLINE; return; }
|
||||||
|
if(resp->status / 100 != 2) { g_brw.status = LB_FAILED; return; }
|
||||||
|
|
||||||
|
g_brw.count = 0;
|
||||||
|
int total = -1;
|
||||||
|
parse_int(find_key(resp->body, "total"), &total);
|
||||||
|
g_brw.total = total;
|
||||||
|
|
||||||
|
// "scores": [ {name, score, end_scene}, ... ]
|
||||||
|
const char *p = find_key(resp->body, "scores");
|
||||||
|
if(!p || *p != '[') { g_brw.status = LB_FAILED; return; }
|
||||||
|
++p;
|
||||||
|
while(g_brw.count < LEADERBOARD_BROWSE_MAX)
|
||||||
|
{
|
||||||
|
while(*p && *p != '{' && *p != ']') ++p;
|
||||||
|
if(*p != '{') break;
|
||||||
|
// Each entry is a small object — scan it locally.
|
||||||
|
const char *end = strchr(p, '}');
|
||||||
|
if(!end) break;
|
||||||
|
|
||||||
|
leaderboard_entry_t &e = g_brw.entries[g_brw.count];
|
||||||
|
e.name[0] = '\0';
|
||||||
|
e.score = 0;
|
||||||
|
e.end_scene = 0;
|
||||||
|
|
||||||
|
const char *kp = find_key(p, "name");
|
||||||
|
if(kp && kp < end) parse_string(kp, e.name, sizeof(e.name));
|
||||||
|
|
||||||
|
kp = find_key(p, "score");
|
||||||
|
int sv = 0;
|
||||||
|
if(kp && kp < end && parse_int(kp, &sv) && sv >= 0)
|
||||||
|
e.score = (unsigned int)sv;
|
||||||
|
|
||||||
|
kp = find_key(p, "end_scene");
|
||||||
|
if(kp && kp < end) parse_int(kp, &e.end_scene);
|
||||||
|
|
||||||
|
++g_brw.count;
|
||||||
|
p = end + 1;
|
||||||
|
}
|
||||||
|
g_brw.status = LB_DONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
namespace leaderboard {
|
||||||
|
|
||||||
|
void submit_score(const s_hiscore_t *hi)
|
||||||
|
{
|
||||||
|
g_sub.status = LB_IDLE;
|
||||||
|
g_sub.rank = -1;
|
||||||
|
g_sub.total = -1;
|
||||||
|
if(!enabled() || !hi) return;
|
||||||
|
|
||||||
|
char name_esc[LEADERBOARD_NAME_MAX * 2 + 1];
|
||||||
|
char board_esc[LEADERBOARD_NAME_MAX * 2 + 1];
|
||||||
|
char id_esc[128];
|
||||||
|
const char *player_name = (hi->profile && hi->profile->name[0])
|
||||||
|
? hi->profile->name : "ANON";
|
||||||
|
json_escape(name_esc, sizeof(name_esc), player_name);
|
||||||
|
json_escape(board_esc, sizeof(board_esc), prefs->leaderboard_board);
|
||||||
|
json_escape(id_esc, sizeof(id_esc), prefs->client_id);
|
||||||
|
|
||||||
|
char body[1024];
|
||||||
|
int n = snprintf(body, sizeof(body),
|
||||||
|
"{\"board\":\"%s\",\"name\":\"%s\",\"client_id\":\"%s\","
|
||||||
|
"\"client\":\"kobodl/0.5.1\","
|
||||||
|
"\"score\":%u,\"skill\":%d,\"gametype\":%d,"
|
||||||
|
"\"end_scene\":%d,\"end_lives\":%d,\"end_health\":%d,"
|
||||||
|
"\"playtime\":%u,\"start_date\":%u,\"end_date\":%u}",
|
||||||
|
board_esc, name_esc, id_esc,
|
||||||
|
hi->score, hi->skill, hi->gametype,
|
||||||
|
hi->end_scene, hi->end_lives, hi->end_health,
|
||||||
|
hi->playtime, hi->start_date, hi->end_date);
|
||||||
|
if(n <= 0 || n >= (int)sizeof(body)) return;
|
||||||
|
|
||||||
|
char url[512];
|
||||||
|
snprintf(url, sizeof(url), "%s/api/scores", prefs->leaderboard_url);
|
||||||
|
|
||||||
|
g_sub.status = LB_INFLIGHT;
|
||||||
|
http_post_json(url, body, on_submit, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
void fetch_board()
|
||||||
|
{
|
||||||
|
g_brw.status = LB_IDLE;
|
||||||
|
g_brw.count = 0;
|
||||||
|
g_brw.total = 0;
|
||||||
|
if(!enabled()) return;
|
||||||
|
|
||||||
|
strncpy(g_brw.board, prefs->leaderboard_board, sizeof(g_brw.board) - 1);
|
||||||
|
g_brw.board[sizeof(g_brw.board) - 1] = '\0';
|
||||||
|
|
||||||
|
char board_esc[LEADERBOARD_NAME_MAX * 2 + 1];
|
||||||
|
json_escape(board_esc, sizeof(board_esc), prefs->leaderboard_board);
|
||||||
|
|
||||||
|
char url[768];
|
||||||
|
snprintf(url, sizeof(url),
|
||||||
|
"%s/api/scores?board=%s&limit=%d",
|
||||||
|
prefs->leaderboard_url, board_esc, LEADERBOARD_BROWSE_MAX);
|
||||||
|
|
||||||
|
g_brw.status = LB_INFLIGHT;
|
||||||
|
http_get(url, on_browse, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
const leaderboard_submission_t *submission() { return &g_sub; }
|
||||||
|
const leaderboard_browse_t *browse() { return &g_brw; }
|
||||||
|
|
||||||
|
void reset_submission()
|
||||||
|
{
|
||||||
|
g_sub.status = LB_IDLE;
|
||||||
|
g_sub.rank = -1;
|
||||||
|
g_sub.total = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace leaderboard
|
||||||
69
leaderboard.h
Normal file
69
leaderboard.h
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
/*
|
||||||
|
* leaderboard.h — global leaderboard client.
|
||||||
|
*
|
||||||
|
* Two flows:
|
||||||
|
*
|
||||||
|
* 1. submit_score() is called after each completed game (manage.cpp). It
|
||||||
|
* POSTs the run's s_hiscore_t to leaderboard_url + "/api/scores". The
|
||||||
|
* game-over screen polls submission() to render the in-flight UI.
|
||||||
|
*
|
||||||
|
* 2. fetch_board() is called when the in-game leaderboard browser opens.
|
||||||
|
* It GETs the top N for the current board. The browser state polls
|
||||||
|
* browse() to render the list.
|
||||||
|
*
|
||||||
|
* Both flows are no-ops when prefs->global_leaderboard is 0, or when the
|
||||||
|
* board name is empty, or when the URL is empty.
|
||||||
|
*/
|
||||||
|
#ifndef KOBO_LEADERBOARD_H
|
||||||
|
#define KOBO_LEADERBOARD_H
|
||||||
|
|
||||||
|
#include "score.h"
|
||||||
|
|
||||||
|
#define LEADERBOARD_BROWSE_MAX 50
|
||||||
|
#define LEADERBOARD_NAME_MAX 32
|
||||||
|
|
||||||
|
enum leaderboard_status_t {
|
||||||
|
LB_IDLE = 0,
|
||||||
|
LB_INFLIGHT,
|
||||||
|
LB_DONE,
|
||||||
|
LB_FAILED, // server error, network error, or feature disabled
|
||||||
|
LB_OFFLINE // transport failure (status 0)
|
||||||
|
};
|
||||||
|
|
||||||
|
struct leaderboard_submission_t {
|
||||||
|
leaderboard_status_t status;
|
||||||
|
int rank; // valid when status == LB_DONE
|
||||||
|
int total; // valid when status == LB_DONE
|
||||||
|
};
|
||||||
|
|
||||||
|
struct leaderboard_entry_t {
|
||||||
|
char name[LEADERBOARD_NAME_MAX];
|
||||||
|
unsigned int score;
|
||||||
|
int end_scene;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct leaderboard_browse_t {
|
||||||
|
leaderboard_status_t status;
|
||||||
|
int count;
|
||||||
|
int total;
|
||||||
|
leaderboard_entry_t entries[LEADERBOARD_BROWSE_MAX];
|
||||||
|
char board[LEADERBOARD_NAME_MAX];
|
||||||
|
};
|
||||||
|
|
||||||
|
namespace leaderboard {
|
||||||
|
// Called from manage.cpp after scorefile.record(). Safe to call always;
|
||||||
|
// it self-checks prefs and silently no-ops if disabled.
|
||||||
|
void submit_score(const s_hiscore_t *hi);
|
||||||
|
|
||||||
|
// Called from the browser state on enter. Replaces previous results.
|
||||||
|
void fetch_board();
|
||||||
|
|
||||||
|
// State accessors — game UI reads these every frame.
|
||||||
|
const leaderboard_submission_t *submission();
|
||||||
|
const leaderboard_browse_t *browse();
|
||||||
|
|
||||||
|
// Reset submission state to LB_IDLE (e.g. when starting a new game).
|
||||||
|
void reset_submission();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // KOBO_LEADERBOARD_H
|
||||||
89
manage.cpp
89
manage.cpp
@ -50,9 +50,27 @@
|
|||||||
#include "states.h"
|
#include "states.h"
|
||||||
#include "audio.h"
|
#include "audio.h"
|
||||||
#include "random.h"
|
#include "random.h"
|
||||||
|
#include "prof.h"
|
||||||
|
#include "bench.h"
|
||||||
|
#include "leaderboard.h"
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
#define GIGA 1000000000
|
#define GIGA 1000000000
|
||||||
|
|
||||||
|
// Stress-test spawner: keeps the enemy pool topped up with rocks so frame
|
||||||
|
// timing is stable. Enabled by KOBO_STRESS=N (target population, 0 = off).
|
||||||
|
static int kobo_stress_target = -1;
|
||||||
|
static int kobo_stress_init(void)
|
||||||
|
{
|
||||||
|
if (kobo_stress_target >= 0) return kobo_stress_target;
|
||||||
|
const char *e = getenv("KOBO_STRESS");
|
||||||
|
kobo_stress_target = (e && *e) ? atoi(e) : 0;
|
||||||
|
if (kobo_stress_target > 0)
|
||||||
|
fprintf(stderr, "[stress] target population=%d\n", kobo_stress_target);
|
||||||
|
return kobo_stress_target;
|
||||||
|
}
|
||||||
|
static void kobo_stress_step(void);
|
||||||
|
|
||||||
int _manage::blank = 0;
|
int _manage::blank = 0;
|
||||||
int _manage::next_state_out;
|
int _manage::next_state_out;
|
||||||
int _manage::next_state_next;
|
int _manage::next_state_next;
|
||||||
@ -197,6 +215,8 @@ void _manage::game_start()
|
|||||||
{
|
{
|
||||||
_game_over = 0;
|
_game_over = 0;
|
||||||
hi.clear();
|
hi.clear();
|
||||||
|
hi.start_date = (unsigned int)time(NULL);
|
||||||
|
leaderboard::reset_submission();
|
||||||
|
|
||||||
game.set(GAME_SINGLE, (skill_levels_t)scorefile.profile()->skill);
|
game.set(GAME_SINGLE, (skill_levels_t)scorefile.profile()->skill);
|
||||||
|
|
||||||
@ -251,8 +271,20 @@ void _manage::game_stop()
|
|||||||
hi.score = score;
|
hi.score = score;
|
||||||
hi.end_scene = scene_num;
|
hi.end_scene = scene_num;
|
||||||
hi.end_health = myship.health();
|
hi.end_health = myship.health();
|
||||||
|
hi.end_date = (unsigned int)time(NULL);
|
||||||
|
// Profile name isn't part of s_hiscore_t's serialized form, but
|
||||||
|
// leaderboard::submit_score wants it. Sync it here so the
|
||||||
|
// payload picks it up.
|
||||||
|
s_profile_t *p = scorefile.profile();
|
||||||
|
if(p)
|
||||||
|
{
|
||||||
|
hi.profile = p;
|
||||||
|
strncpy(hi.name, p->name, sizeof(hi.name) - 1);
|
||||||
|
hi.name[sizeof(hi.name) - 1] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
scorefile.record(&hi);
|
scorefile.record(&hi);
|
||||||
|
leaderboard::submit_score(&hi);
|
||||||
}
|
}
|
||||||
ships = 0;
|
ships = 0;
|
||||||
ships_changed = 1;
|
ships_changed = 1;
|
||||||
@ -335,7 +367,10 @@ void _manage::init_resources_to_play(int newship)
|
|||||||
next_state_out = 0;
|
next_state_out = 0;
|
||||||
next_state_next = 0;
|
next_state_next = 0;
|
||||||
|
|
||||||
gamerand.init();
|
if(kobo_bench_enabled)
|
||||||
|
gamerand.init(kobo_bench_seed); // deterministic map + spawn pattern
|
||||||
|
else
|
||||||
|
gamerand.init();
|
||||||
game_seed = gamerand.get_seed();
|
game_seed = gamerand.get_seed();
|
||||||
enemies.init();
|
enemies.init();
|
||||||
myship.init();
|
myship.init();
|
||||||
@ -556,6 +591,7 @@ void _manage::run_pause()
|
|||||||
|
|
||||||
void _manage::run_game()
|
void _manage::run_game()
|
||||||
{
|
{
|
||||||
|
PROF_ZONE("run_game");
|
||||||
put_health();
|
put_health();
|
||||||
put_temp();
|
put_temp();
|
||||||
|
|
||||||
@ -583,11 +619,56 @@ void _manage::run_game()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
myship.move();
|
if(kobo_stress_init() > 0)
|
||||||
enemies.move();
|
kobo_stress_step();
|
||||||
myship.hit_structure();
|
|
||||||
|
{ PROF_ZONE("myship.move"); myship.move(); }
|
||||||
|
{ PROF_ZONE("enemies.move"); enemies.move(); }
|
||||||
|
{ PROF_ZONE("hit_structure"); myship.hit_structure(); }
|
||||||
update();
|
update();
|
||||||
++hi.playtime;
|
++hi.playtime;
|
||||||
|
prof_frame_tick();
|
||||||
|
|
||||||
|
// Bench harness: stop the program after a fixed number of frames so a
|
||||||
|
// single command produces a comparable profiler dump across phases.
|
||||||
|
// Push SDL_EVENT_QUIT instead of calling exit() — exit() runs the
|
||||||
|
// atexit handler which tries to shut down SDL audio synchronously and
|
||||||
|
// hangs on macOS. Going through the engine's own quit path tears down
|
||||||
|
// in the right order.
|
||||||
|
if(kobo_bench_enabled && (int)hi.playtime >= kobo_bench_frames)
|
||||||
|
{
|
||||||
|
static int triggered = 0;
|
||||||
|
if(!triggered)
|
||||||
|
{
|
||||||
|
triggered = 1;
|
||||||
|
fprintf(stderr,
|
||||||
|
"[bench] reached %d frames; dumping profile and quitting\n",
|
||||||
|
kobo_bench_frames);
|
||||||
|
prof_dump(stderr);
|
||||||
|
SDL_Event ev;
|
||||||
|
SDL_zero(ev);
|
||||||
|
ev.type = SDL_EVENT_QUIT;
|
||||||
|
SDL_PushEvent(&ev);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Top up the enemy pool with rocks until we hit the target population.
|
||||||
|
// Rocks are inert enough to give a stable per-frame integration cost.
|
||||||
|
static void kobo_stress_step(void)
|
||||||
|
{
|
||||||
|
extern const enemy_kind rock;
|
||||||
|
int target = kobo_stress_target;
|
||||||
|
if (target <= 0) return;
|
||||||
|
// Spawn at most a few per frame to avoid burst allocation spikes.
|
||||||
|
for (int i = 0; i < 8; ++i) {
|
||||||
|
int px = (pubrand.get() & (WORLD_SIZEX - 1));
|
||||||
|
int py = (pubrand.get() & (WORLD_SIZEY - 1));
|
||||||
|
int h = (int)(pubrand.get() & 0x7f) - 64;
|
||||||
|
int v = (int)(pubrand.get() & 0x7f) - 64;
|
||||||
|
// enemies.make returns 0 on success, non-zero when pool is full.
|
||||||
|
if (enemies.make(&rock, px, py, h, v) != 0) break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
1
manage.h
1
manage.h
@ -95,6 +95,7 @@ class _manage
|
|||||||
static void regenerate();
|
static void regenerate();
|
||||||
static void select_scene(int scene, int redraw_map = 1);
|
static void select_scene(int scene, int redraw_map = 1);
|
||||||
static int scene() { return scene_num; }
|
static int scene() { return scene_num; }
|
||||||
|
static void set_scene_num(int n) { scene_num = n; } // for bench harness
|
||||||
static void abort();
|
static void abort();
|
||||||
static void freeze_abort();
|
static void freeze_abort();
|
||||||
static int aborted() { return exit_manage; }
|
static int aborted() { return exit_manage; }
|
||||||
|
|||||||
@ -28,6 +28,7 @@
|
|||||||
#include "manage.h"
|
#include "manage.h"
|
||||||
#include "random.h"
|
#include "random.h"
|
||||||
#include "sound.h"
|
#include "sound.h"
|
||||||
|
#include "prof.h"
|
||||||
|
|
||||||
#define WING_GUN_OFFSET 4
|
#define WING_GUN_OFFSET 4
|
||||||
|
|
||||||
@ -384,6 +385,7 @@ int _myship::hit_structure()
|
|||||||
|
|
||||||
int _myship::hit_bolt(int ex, int ey, int hitsize, int health)
|
int _myship::hit_bolt(int ex, int ey, int hitsize, int health)
|
||||||
{
|
{
|
||||||
|
PROF_ZONE("hit_bolt");
|
||||||
int dmg = 0;
|
int dmg = 0;
|
||||||
int i;
|
int i;
|
||||||
for(i = 0; i < MAX_BOLTS; i++)
|
for(i = 0; i < MAX_BOLTS; i++)
|
||||||
|
|||||||
30
options.cpp
30
options.cpp
@ -177,29 +177,7 @@ void video_options_t::build()
|
|||||||
}
|
}
|
||||||
space();
|
space();
|
||||||
yesno("Fullscreen Display", &prf->fullscreen, OS_RESTART_VIDEO);
|
yesno("Fullscreen Display", &prf->fullscreen, OS_RESTART_VIDEO);
|
||||||
list("Display Depth", &prf->depth, OS_RESTART_VIDEO);
|
|
||||||
item("Default", 0);
|
|
||||||
item("8 bits", 8);
|
|
||||||
item("15 bits", 15);
|
|
||||||
item("16 bits", 16);
|
|
||||||
item("24 bits", 24);
|
|
||||||
item("32 bits", 32);
|
|
||||||
prf->videodriver = GFX_DRIVER_SDL2D;
|
|
||||||
space();
|
|
||||||
xoffs = 0.55;
|
|
||||||
// Only the SDL 2D driver remains since the SDL 3 migration; the
|
|
||||||
// OpenGL driver is now handled by the SDL renderer itself.
|
|
||||||
list("Display Buffering Mode", &prf->doublebuf, OS_RESTART_VIDEO);
|
|
||||||
item("Single", 0);
|
|
||||||
item("Double", 1);
|
|
||||||
yesno("Software Shadow Buffer", &prf->shadow, OS_RESTART_VIDEO);
|
|
||||||
yesno("Vertical Retrace Sync", &prf->vsync, OS_RESTART_VIDEO);
|
yesno("Vertical Retrace Sync", &prf->vsync, OS_RESTART_VIDEO);
|
||||||
list("Display Buffer Pages", &prf->pages, OS_UPDATE_ENGINE);
|
|
||||||
item("Assume Standard", -1);
|
|
||||||
item("Always Repaint", 0);
|
|
||||||
item("Single", 1);
|
|
||||||
item("Double", 2);
|
|
||||||
item("Triple", 3);
|
|
||||||
big();
|
big();
|
||||||
space();
|
space();
|
||||||
xoffs = 0.5;
|
xoffs = 0.5;
|
||||||
@ -480,6 +458,14 @@ void game_options_t::build()
|
|||||||
item("Off", 200);
|
item("Off", 200);
|
||||||
item("Low", 100);
|
item("Low", 100);
|
||||||
item("High", 50);
|
item("High", 50);
|
||||||
|
|
||||||
|
// Leaderboard toggle. Board name and URL live in the config file
|
||||||
|
// (the form toolkit has no free-text widget). When this is on but
|
||||||
|
// the board name is empty, submission no-ops silently.
|
||||||
|
space();
|
||||||
|
xoffs = 0.6;
|
||||||
|
onoff("Global Leaderboard", &prf->global_leaderboard, 0);
|
||||||
|
|
||||||
big();
|
big();
|
||||||
space();
|
space();
|
||||||
xoffs = 0.5;
|
xoffs = 0.5;
|
||||||
|
|||||||
38
prefs.cpp
38
prefs.cpp
@ -82,25 +82,18 @@ void prefs_t::init()
|
|||||||
|
|
||||||
comment("--- Video settings -------------------------");
|
comment("--- Video settings -------------------------");
|
||||||
yesno("fullscreen", fullscreen, 0); desc("Fullscreen Display");
|
yesno("fullscreen", fullscreen, 0); desc("Fullscreen Display");
|
||||||
key("videodriver", videodriver, GFX_DRIVER_SDL2D);
|
|
||||||
desc("Display Driver");
|
|
||||||
key("width", width, 640); desc("Horizontal Resolution");
|
key("width", width, 640); desc("Horizontal Resolution");
|
||||||
key("height", height, 480); desc("Vertical Resolution");
|
key("height", height, 480); desc("Vertical Resolution");
|
||||||
key("aspect", aspect, 1000); desc("Pixel Aspect Ratio");
|
key("aspect", aspect, 1000); desc("Pixel Aspect Ratio");
|
||||||
key("depth", depth, 0); desc("Display Depth");
|
|
||||||
key("maxfps", max_fps, 100); desc("Maximum fps");
|
key("maxfps", max_fps, 100); desc("Maximum fps");
|
||||||
key("maxfpsstrict", max_fps_strict, 0); desc("Strictly Regulated fps");
|
key("maxfpsstrict", max_fps_strict, 0); desc("Strictly Regulated fps");
|
||||||
key("buffer", doublebuf, 1); desc("Display Buffer Mode");
|
|
||||||
yesno("shadow", shadow, 1); desc("Use Software Shadow Buffer");
|
|
||||||
key("videomode", videomode, 0x04330); desc("Video Mode");
|
key("videomode", videomode, 0x04330); desc("Video Mode");
|
||||||
yesno("vsync", vsync, 1); desc("Enable Vertical Sync");
|
yesno("vsync", vsync, 1); desc("Enable Vertical Sync");
|
||||||
key("videopages", pages, -1); desc("Number of Video Pages");
|
|
||||||
|
|
||||||
comment("--- Graphics settings ----------------------");
|
comment("--- Graphics settings ----------------------");
|
||||||
key("scalemode", scalemode, 1); desc("Scaling Filter Mode");
|
key("scalemode", scalemode, 1); desc("Scaling Filter Mode");
|
||||||
yesno("dither", use_dither, 1); desc("Use Dithering");
|
yesno("dither", use_dither, 1); desc("Use Dithering");
|
||||||
key("dither_type", dither_type, 0); desc("Dither Type");
|
key("dither_type", dither_type, 0); desc("Dither Type");
|
||||||
yesno("broken_rgba8", broken_rgba8, 0); desc("Broken RGBA (OpenGL)");
|
|
||||||
yesno("alpha", alpha, 1); desc("Use Alpha Blending");
|
yesno("alpha", alpha, 1); desc("Use Alpha Blending");
|
||||||
key("brightness", brightness, 100); desc("Brightness");
|
key("brightness", brightness, 100); desc("Brightness");
|
||||||
key("contrast", contrast, 100); desc("Contrast");
|
key("contrast", contrast, 100); desc("Contrast");
|
||||||
@ -111,6 +104,24 @@ void prefs_t::init()
|
|||||||
key("sfx", sfxdir, ""); desc("Sound Data Path");
|
key("sfx", sfxdir, ""); desc("Sound Data Path");
|
||||||
key("scores", scoredir, ""); desc("Score File Path");
|
key("scores", scoredir, ""); desc("Score File Path");
|
||||||
|
|
||||||
|
comment("--- Global leaderboard ---------------------");
|
||||||
|
// Web build ships with the leaderboard on by default — players land on
|
||||||
|
// lindholm.dev/kobo/ specifically to play it. Native build stays off
|
||||||
|
// until the user opts in; a desktop player may not want network calls
|
||||||
|
// from a single-player arcade game.
|
||||||
|
#ifdef __EMSCRIPTEN__
|
||||||
|
yesno("global_leaderboard", global_leaderboard, 1);
|
||||||
|
#else
|
||||||
|
yesno("global_leaderboard", global_leaderboard, 0);
|
||||||
|
#endif
|
||||||
|
desc("Submit Scores to Global Leaderboard");
|
||||||
|
key("leaderboard_board", leaderboard_board, "");
|
||||||
|
desc("Leaderboard Board Name (shared with friends)");
|
||||||
|
key("leaderboard_url", leaderboard_url,
|
||||||
|
"https://leaderboard.lindholm.dev");
|
||||||
|
desc("Leaderboard Server URL");
|
||||||
|
key("client_id", client_id, ""); desc("Anonymous Per-Install ID");
|
||||||
|
|
||||||
// Obsolete stuff (not written into new files)
|
// Obsolete stuff (not written into new files)
|
||||||
key("size", o_size, 0, 0); desc("Screen Size (Obsolete)");
|
key("size", o_size, 0, 0); desc("Screen Size (Obsolete)");
|
||||||
key("wait", o_wait_msec, 30); desc("Game Speed (Obsolete)");
|
key("wait", o_wait_msec, 30); desc("Game Speed (Obsolete)");
|
||||||
@ -120,6 +131,14 @@ void prefs_t::init()
|
|||||||
key("release", o_release, 50, 0); desc("Limiter Speed");
|
key("release", o_release, 50, 0); desc("Limiter Speed");
|
||||||
key("internalres", o_internalres, 1, 0); desc("Texture Resolution");
|
key("internalres", o_internalres, 1, 0); desc("Texture Resolution");
|
||||||
|
|
||||||
|
// SDL 1.2-era video keys, ignored under SDL 3.
|
||||||
|
key("videodriver", o_videodriver, 0, 0); desc("Display Driver");
|
||||||
|
key("depth", o_depth, 0, 0); desc("Display Depth");
|
||||||
|
key("buffer", o_doublebuf, 1, 0); desc("Display Buffer Mode");
|
||||||
|
yesno("shadow", o_shadow, 1, 0); desc("Software Shadow Buffer");
|
||||||
|
key("videopages", o_pages, -1, 0); desc("Number of Video Pages");
|
||||||
|
yesno("broken_rgba8", o_broken_rgba8, 0, 0); desc("Broken RGBA (OpenGL)");
|
||||||
|
|
||||||
comment("--- Temporary variables --------------------");
|
comment("--- Temporary variables --------------------");
|
||||||
key("last_profile", last_profile, 0);
|
key("last_profile", last_profile, 0);
|
||||||
desc("Last used player profile");
|
desc("Last used player profile");
|
||||||
@ -170,9 +189,4 @@ void prefs_t::postload()
|
|||||||
log_printf(ELOG, "Radar sweep mode broken and disabled!\n");
|
log_printf(ELOG, "Radar sweep mode broken and disabled!\n");
|
||||||
scrollradar = 2;
|
scrollradar = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only the SDL 2D driver remains since the SDL 3 migration; collapse
|
|
||||||
// any legacy GLSDL setting onto it.
|
|
||||||
if(videodriver != GFX_DRIVER_SDL2D)
|
|
||||||
videodriver = GFX_DRIVER_SDL2D;
|
|
||||||
}
|
}
|
||||||
|
|||||||
21
prefs.h
21
prefs.h
@ -73,24 +73,18 @@ class prefs_t : public config_parser_t
|
|||||||
|
|
||||||
//Video settings
|
//Video settings
|
||||||
int fullscreen; //Use fullscreen mode
|
int fullscreen; //Use fullscreen mode
|
||||||
int videodriver; //Internal video driver
|
|
||||||
int width; //Screen/window width
|
int width; //Screen/window width
|
||||||
int height; //Screen/window height
|
int height; //Screen/window height
|
||||||
int aspect; //Pixel aspect ratio * 1000
|
int aspect; //Pixel aspect ratio * 1000
|
||||||
int depth; //Bits per pixel
|
|
||||||
int max_fps; //Maximum fps
|
int max_fps; //Maximum fps
|
||||||
int max_fps_strict; //Strictly regulated fps limiter
|
int max_fps_strict; //Strictly regulated fps limiter
|
||||||
int doublebuf; //Use double buffering
|
int videomode; //Selected entry from the vidmodes table
|
||||||
int shadow; //Use software shadow buffer
|
|
||||||
int videomode; //New video mode codes
|
|
||||||
int vsync; //Vertical (retrace) sync
|
int vsync; //Vertical (retrace) sync
|
||||||
int pages; //Number of physical video pages
|
|
||||||
|
|
||||||
//Graphics settings
|
//Graphics settings
|
||||||
int scalemode; //Scaling filter mode
|
int scalemode; //Scaling filter mode
|
||||||
int use_dither;
|
int use_dither;
|
||||||
int dither_type;
|
int dither_type;
|
||||||
int broken_rgba8; //For some OpenGL setups
|
|
||||||
int alpha; //Alpha blending
|
int alpha; //Alpha blending
|
||||||
int brightness; //Graphics brightness
|
int brightness; //Graphics brightness
|
||||||
int contrast; //Graphics contrast
|
int contrast; //Graphics contrast
|
||||||
@ -101,6 +95,12 @@ class prefs_t : public config_parser_t
|
|||||||
cfg_string_t sfxdir; //Path to sfx/
|
cfg_string_t sfxdir; //Path to sfx/
|
||||||
cfg_string_t scoredir; //Path to scores/
|
cfg_string_t scoredir; //Path to scores/
|
||||||
|
|
||||||
|
//Global leaderboard
|
||||||
|
int global_leaderboard; //Submit scores to the server
|
||||||
|
cfg_string_t leaderboard_board; //Shared "room code"
|
||||||
|
cfg_string_t leaderboard_url; //Server endpoint base URL
|
||||||
|
cfg_string_t client_id; //Per-install UUID (auto-gen)
|
||||||
|
|
||||||
//Obsolete stuff (compatibility)
|
//Obsolete stuff (compatibility)
|
||||||
int o_size; //Screen scale
|
int o_size; //Screen scale
|
||||||
int o_wait_msec; //ms per control system frame
|
int o_wait_msec; //ms per control system frame
|
||||||
@ -108,6 +108,13 @@ class prefs_t : public config_parser_t
|
|||||||
int o_threshold; //Limiter threshold
|
int o_threshold; //Limiter threshold
|
||||||
int o_release; //Limiter release rate
|
int o_release; //Limiter release rate
|
||||||
int o_internalres; //Internal resolution for OpenGL
|
int o_internalres; //Internal resolution for OpenGL
|
||||||
|
// SDL 1.2 video settings, read for back-compat but unused under SDL 3
|
||||||
|
int o_videodriver;
|
||||||
|
int o_depth;
|
||||||
|
int o_doublebuf;
|
||||||
|
int o_shadow;
|
||||||
|
int o_pages;
|
||||||
|
int o_broken_rgba8;
|
||||||
|
|
||||||
//"Hidden" stuff ("to remember until next startup")
|
//"Hidden" stuff ("to remember until next startup")
|
||||||
int last_profile; //Last used player profile
|
int last_profile; //Last used player profile
|
||||||
|
|||||||
41
prof.cpp
Normal file
41
prof.cpp
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
#include "prof.h"
|
||||||
|
|
||||||
|
prof_zone_t prof_zones[PROF_MAX_ZONES];
|
||||||
|
int prof_nzones = 0;
|
||||||
|
int prof_enabled = 0;
|
||||||
|
uint64_t prof_frame_count = 0;
|
||||||
|
|
||||||
|
extern "C" void prof_init(void)
|
||||||
|
{
|
||||||
|
const char *e = getenv("KOBO_PROF");
|
||||||
|
prof_enabled = (e && *e && *e != '0') ? 1 : 0;
|
||||||
|
if (prof_enabled)
|
||||||
|
fprintf(stderr, "[prof] enabled (dump every %d frames)\n",
|
||||||
|
PROF_DUMP_EVERY_FRAMES);
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" void prof_dump(FILE *f)
|
||||||
|
{
|
||||||
|
if (!prof_enabled) return;
|
||||||
|
fprintf(f, "[prof] frame=%llu zones=%d\n",
|
||||||
|
(unsigned long long)prof_frame_count, prof_nzones);
|
||||||
|
for (int i = 0; i < prof_nzones; ++i) {
|
||||||
|
prof_zone_t *z = &prof_zones[i];
|
||||||
|
double ns_per_call = z->calls ? (double)z->total_ns / (double)z->calls : 0.0;
|
||||||
|
double total_ms = (double)z->total_ns / 1.0e6;
|
||||||
|
fprintf(f, " %-28s calls=%-10llu total_ms=%-10.3f ns/call=%.1f\n",
|
||||||
|
z->name,
|
||||||
|
(unsigned long long)z->calls,
|
||||||
|
total_ms,
|
||||||
|
ns_per_call);
|
||||||
|
}
|
||||||
|
fflush(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" void prof_frame_tick(void)
|
||||||
|
{
|
||||||
|
if (!prof_enabled) return;
|
||||||
|
++prof_frame_count;
|
||||||
|
if (prof_frame_count % PROF_DUMP_EVERY_FRAMES == 0)
|
||||||
|
prof_dump(stderr);
|
||||||
|
}
|
||||||
94
prof.h
Normal file
94
prof.h
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
// Lightweight zone-based profiler for measuring the DOD refactor.
|
||||||
|
// Enabled at runtime via KOBO_PROF=1 in the environment.
|
||||||
|
// See plans: .claude/plans/can-you-explain-the-noble-wilkes.md (Phase 0).
|
||||||
|
|
||||||
|
#ifndef KOBO_PROF_H
|
||||||
|
#define KOBO_PROF_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define PROF_MAX_ZONES 32
|
||||||
|
#define PROF_DUMP_EVERY_FRAMES 600
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
const char *name;
|
||||||
|
uint64_t calls;
|
||||||
|
uint64_t total_ns;
|
||||||
|
} prof_zone_t;
|
||||||
|
|
||||||
|
extern prof_zone_t prof_zones[PROF_MAX_ZONES];
|
||||||
|
extern int prof_nzones;
|
||||||
|
extern int prof_enabled;
|
||||||
|
extern uint64_t prof_frame_count;
|
||||||
|
|
||||||
|
static inline uint64_t prof_now_ns(void)
|
||||||
|
{
|
||||||
|
#if defined(__APPLE__) || defined(__linux__)
|
||||||
|
struct timespec ts;
|
||||||
|
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||||
|
return (uint64_t)ts.tv_sec * 1000000000ull + (uint64_t)ts.tv_nsec;
|
||||||
|
#else
|
||||||
|
return 0;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the index of the zone with the given name, creating it on first use.
|
||||||
|
// Names must be string literals (we compare by pointer for speed).
|
||||||
|
static inline int prof_zone_id(const char *name)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < prof_nzones; ++i)
|
||||||
|
if (prof_zones[i].name == name)
|
||||||
|
return i;
|
||||||
|
if (prof_nzones >= PROF_MAX_ZONES)
|
||||||
|
return PROF_MAX_ZONES - 1;
|
||||||
|
int id = prof_nzones++;
|
||||||
|
prof_zones[id].name = name;
|
||||||
|
prof_zones[id].calls = 0;
|
||||||
|
prof_zones[id].total_ns = 0;
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline void prof_accumulate(int id, uint64_t ns)
|
||||||
|
{
|
||||||
|
prof_zones[id].calls += 1;
|
||||||
|
prof_zones[id].total_ns += ns;
|
||||||
|
}
|
||||||
|
|
||||||
|
void prof_init(void);
|
||||||
|
void prof_frame_tick(void);
|
||||||
|
void prof_dump(FILE *f);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
|
||||||
|
// RAII helper for C++ scoping.
|
||||||
|
struct ProfScope {
|
||||||
|
int id;
|
||||||
|
uint64_t t0;
|
||||||
|
inline ProfScope(int zone_id) : id(zone_id), t0(prof_enabled ? prof_now_ns() : 0) {}
|
||||||
|
inline ~ProfScope() {
|
||||||
|
if (prof_enabled) prof_accumulate(id, prof_now_ns() - t0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Pastes the line number to make the static unique per use-site.
|
||||||
|
#define PROF_ZONE_CONCAT2(a,b) a##b
|
||||||
|
#define PROF_ZONE_CONCAT(a,b) PROF_ZONE_CONCAT2(a,b)
|
||||||
|
#define PROF_ZONE(name) \
|
||||||
|
static int PROF_ZONE_CONCAT(_prof_zid_, __LINE__) = -1; \
|
||||||
|
if (PROF_ZONE_CONCAT(_prof_zid_, __LINE__) < 0) \
|
||||||
|
PROF_ZONE_CONCAT(_prof_zid_, __LINE__) = prof_zone_id(name); \
|
||||||
|
ProfScope PROF_ZONE_CONCAT(_prof_scope_, __LINE__)( \
|
||||||
|
PROF_ZONE_CONCAT(_prof_zid_, __LINE__))
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif // KOBO_PROF_H
|
||||||
@ -28,7 +28,15 @@
|
|||||||
extern "C" {
|
extern "C" {
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#define MAX_COMMANDS 128
|
/*
|
||||||
|
* SDL 3 migration: the audio command FIFO was sized 128 for the SDL 1.2
|
||||||
|
* push-callback model where the driver fired predictably-spaced
|
||||||
|
* callbacks that drained the FIFO promptly. SDL 3's pull-stream model
|
||||||
|
* can call the audio callback less often / with larger buffers, so
|
||||||
|
* commands accumulate between drains. 1024 gives a comfortable
|
||||||
|
* headroom even under sustained heavy fire.
|
||||||
|
*/
|
||||||
|
#define MAX_COMMANDS 1024
|
||||||
|
|
||||||
/*----------------------------------------------------------
|
/*----------------------------------------------------------
|
||||||
Asynchronous command interface stuff
|
Asynchronous command interface stuff
|
||||||
|
|||||||
402
states.cpp
402
states.cpp
@ -38,6 +38,9 @@
|
|||||||
#include "sound.h"
|
#include "sound.h"
|
||||||
#include "radar.h"
|
#include "radar.h"
|
||||||
#include "random.h"
|
#include "random.h"
|
||||||
|
#include "leaderboard.h"
|
||||||
|
#include "prefs.h"
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
|
||||||
gamestatemanager_t gsm;
|
gamestatemanager_t gsm;
|
||||||
@ -664,6 +667,41 @@ void st_game_over_t::post_render()
|
|||||||
int y = PIXEL2CS(100) + (int)floor(PIXEL2CS(15)*sin(ft * 6));
|
int y = PIXEL2CS(100) + (int)floor(PIXEL2CS(15)*sin(ft * 6));
|
||||||
wmain->center_fxp(y, "GAME OVER");
|
wmain->center_fxp(y, "GAME OVER");
|
||||||
|
|
||||||
|
const leaderboard_submission_t *sub = leaderboard::submission();
|
||||||
|
if(sub->status != LB_IDLE)
|
||||||
|
{
|
||||||
|
char buf[64];
|
||||||
|
const char *msg = NULL;
|
||||||
|
switch(sub->status)
|
||||||
|
{
|
||||||
|
case LB_INFLIGHT:
|
||||||
|
msg = "Submitting score…";
|
||||||
|
break;
|
||||||
|
case LB_DONE:
|
||||||
|
if(sub->rank > 0 && sub->total > 0)
|
||||||
|
{
|
||||||
|
snprintf(buf, sizeof(buf), "Rank #%d of %d",
|
||||||
|
sub->rank, sub->total);
|
||||||
|
msg = buf;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
msg = "Score submitted";
|
||||||
|
break;
|
||||||
|
case LB_OFFLINE:
|
||||||
|
msg = "(offline)";
|
||||||
|
break;
|
||||||
|
case LB_FAILED:
|
||||||
|
msg = "(submission failed)";
|
||||||
|
break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
if(msg)
|
||||||
|
{
|
||||||
|
wmain->font(B_NORMAL_FONT);
|
||||||
|
wmain->center_fxp(PIXEL2CS(140), msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
wradar->frame();
|
wradar->frame();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1281,12 +1319,18 @@ void main_menu_t::build()
|
|||||||
}
|
}
|
||||||
space();
|
space();
|
||||||
button("Options", 2);
|
button("Options", 2);
|
||||||
|
if(prefs->global_leaderboard && prefs->leaderboard_board[0])
|
||||||
|
button("Global Leaderboard", 200);
|
||||||
space();
|
space();
|
||||||
if(manage.game_stopped())
|
if(manage.game_stopped())
|
||||||
button("Return to Intro", 0);
|
button("Return to Intro", 0);
|
||||||
else
|
else
|
||||||
button("Abort Current Game", 101);
|
button("Abort Current Game", 101);
|
||||||
|
#ifndef __EMSCRIPTEN__
|
||||||
|
// In the browser, exit() leaves the page on a dead canvas. Drop the
|
||||||
|
// option entirely; reloading the tab is the user's exit.
|
||||||
button("Quit Kobo Deluxe", MENU_TAG_CANCEL);
|
button("Quit Kobo Deluxe", MENU_TAG_CANCEL);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void main_menu_t::rebuild()
|
void main_menu_t::rebuild()
|
||||||
@ -1313,6 +1357,18 @@ kobo_form_t *st_main_menu_t::open()
|
|||||||
|
|
||||||
menu = new main_menu_t;
|
menu = new main_menu_t;
|
||||||
menu->open();
|
menu->open();
|
||||||
|
|
||||||
|
// First time we reach the main menu with the leaderboard on but no
|
||||||
|
// board name configured, prompt for one. Only once per session — if
|
||||||
|
// the user cancels, they can edit it from Options later.
|
||||||
|
static bool prompted_for_board = false;
|
||||||
|
if(!prompted_for_board && prefs->global_leaderboard
|
||||||
|
&& prefs->leaderboard_board[0] == '\0')
|
||||||
|
{
|
||||||
|
prompted_for_board = true;
|
||||||
|
gsm.push(&st_edit_board);
|
||||||
|
}
|
||||||
|
|
||||||
return menu;
|
return menu;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1363,6 +1419,9 @@ void st_main_menu_t::select(int tag)
|
|||||||
case 3:
|
case 3:
|
||||||
gsm.push(&st_new_player);
|
gsm.push(&st_new_player);
|
||||||
break;
|
break;
|
||||||
|
case 200:
|
||||||
|
gsm.push(&st_global_leaderboard);
|
||||||
|
break;
|
||||||
case 4: // Player: Inc/Dec
|
case 4: // Player: Inc/Dec
|
||||||
sound.ui_tick();
|
sound.ui_tick();
|
||||||
prefs->changed = 1;
|
prefs->changed = 1;
|
||||||
@ -1514,6 +1573,8 @@ void options_main_t::build()
|
|||||||
button("Graphics", 6);
|
button("Graphics", 6);
|
||||||
button("Audio", 2);
|
button("Audio", 2);
|
||||||
button("System", 5);
|
button("System", 5);
|
||||||
|
if(prefs->global_leaderboard)
|
||||||
|
button("Leaderboard Board Name", 7);
|
||||||
space();
|
space();
|
||||||
|
|
||||||
button("DONE!", 0);
|
button("DONE!", 0);
|
||||||
@ -1548,6 +1609,9 @@ void st_options_main_t::select(int tag)
|
|||||||
case 6:
|
case 6:
|
||||||
gsm.push(&st_options_graphics);
|
gsm.push(&st_options_graphics);
|
||||||
break;
|
break;
|
||||||
|
case 7:
|
||||||
|
gsm.push(&st_edit_board);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1600,7 +1664,6 @@ void st_options_base_t::select(int tag)
|
|||||||
gengine->timefilter(prefs->timefilter * 0.01f);
|
gengine->timefilter(prefs->timefilter * 0.01f);
|
||||||
gengine->interpolation(prefs->filter);
|
gengine->interpolation(prefs->filter);
|
||||||
gengine->vsync(prefs->vsync);
|
gengine->vsync(prefs->vsync);
|
||||||
gengine->pages(prefs->pages);
|
|
||||||
}
|
}
|
||||||
cfg_form->clearstatus(OS_UPDATE);
|
cfg_form->clearstatus(OS_UPDATE);
|
||||||
|
|
||||||
@ -1994,3 +2057,340 @@ st_profile_audio_t st_profile_audio;
|
|||||||
|
|
||||||
#endif /*PROFILE_AUDIO*/
|
#endif /*PROFILE_AUDIO*/
|
||||||
|
|
||||||
|
|
||||||
|
/*----------------------------------------------------------
|
||||||
|
Global Leaderboard browser
|
||||||
|
----------------------------------------------------------*/
|
||||||
|
|
||||||
|
st_global_leaderboard_t::st_global_leaderboard_t()
|
||||||
|
{
|
||||||
|
name = "global_leaderboard";
|
||||||
|
}
|
||||||
|
|
||||||
|
void st_global_leaderboard_t::enter()
|
||||||
|
{
|
||||||
|
start_time = (int)SDL_GetTicks();
|
||||||
|
leaderboard::fetch_board();
|
||||||
|
}
|
||||||
|
|
||||||
|
void st_global_leaderboard_t::press(int button)
|
||||||
|
{
|
||||||
|
switch(button)
|
||||||
|
{
|
||||||
|
case BTN_EXIT:
|
||||||
|
case BTN_NO:
|
||||||
|
case BTN_FIRE:
|
||||||
|
case BTN_START:
|
||||||
|
case BTN_SELECT:
|
||||||
|
case BTN_YES:
|
||||||
|
sound.ui_ok();
|
||||||
|
pop();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void st_global_leaderboard_t::frame()
|
||||||
|
{
|
||||||
|
manage.run_intro();
|
||||||
|
}
|
||||||
|
|
||||||
|
void st_global_leaderboard_t::post_render()
|
||||||
|
{
|
||||||
|
kobo_basestate_t::post_render();
|
||||||
|
|
||||||
|
const leaderboard_browse_t *b = leaderboard::browse();
|
||||||
|
|
||||||
|
wmain->font(B_BIG_FONT);
|
||||||
|
wmain->center(40, "GLOBAL LEADERBOARD");
|
||||||
|
|
||||||
|
char buf[96];
|
||||||
|
wmain->font(B_NORMAL_FONT);
|
||||||
|
if(b->board[0])
|
||||||
|
{
|
||||||
|
snprintf(buf, sizeof(buf), "Board: %s", b->board);
|
||||||
|
wmain->center(70, buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch(b->status)
|
||||||
|
{
|
||||||
|
case LB_INFLIGHT:
|
||||||
|
wmain->center(120, "Loading…");
|
||||||
|
break;
|
||||||
|
case LB_OFFLINE:
|
||||||
|
wmain->center(120, "(offline)");
|
||||||
|
break;
|
||||||
|
case LB_FAILED:
|
||||||
|
wmain->center(120, "(server error)");
|
||||||
|
break;
|
||||||
|
case LB_IDLE:
|
||||||
|
wmain->center(120, "Leaderboard disabled.");
|
||||||
|
wmain->center(140, "Set leaderboard_board in config.");
|
||||||
|
break;
|
||||||
|
case LB_DONE:
|
||||||
|
{
|
||||||
|
if(b->count == 0)
|
||||||
|
{
|
||||||
|
wmain->center(120, "No scores yet.");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Two-column layout: rank + name on the left, score + stage
|
||||||
|
// on the right. 18px line height matches the existing intro
|
||||||
|
// high-score screen feel.
|
||||||
|
int y = 100;
|
||||||
|
int rows = b->count > 16 ? 16 : b->count;
|
||||||
|
for(int i = 0; i < rows; ++i)
|
||||||
|
{
|
||||||
|
const leaderboard_entry_t &e = b->entries[i];
|
||||||
|
snprintf(buf, sizeof(buf), "%2d. %-10s",
|
||||||
|
i + 1, e.name);
|
||||||
|
wmain->string(40, y, buf);
|
||||||
|
snprintf(buf, sizeof(buf), "%9u st.%2d",
|
||||||
|
e.score, e.end_scene + 1);
|
||||||
|
wmain->string(160, y, buf);
|
||||||
|
y += 12;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
|
||||||
|
wmain->center(WSIZE - 30, "(Press any key to return)");
|
||||||
|
}
|
||||||
|
|
||||||
|
st_global_leaderboard_t st_global_leaderboard;
|
||||||
|
|
||||||
|
|
||||||
|
/*----------------------------------------------------------
|
||||||
|
Edit leaderboard board name
|
||||||
|
----------------------------------------------------------*/
|
||||||
|
//
|
||||||
|
// Cloned from new_player_t. Differences: bound to prefs->leaderboard_board,
|
||||||
|
// allows alphanumerics + '-' / '_' (room-code shape), no profile creation —
|
||||||
|
// OK writes the value back into prefs and pops.
|
||||||
|
|
||||||
|
void edit_board_t::open()
|
||||||
|
{
|
||||||
|
init(gengine);
|
||||||
|
place(wmain->x(), wmain->y(), wmain->width(), wmain->height());
|
||||||
|
font(B_NORMAL_FONT);
|
||||||
|
foreground(wmain->map_rgb(255, 255, 255));
|
||||||
|
background(wmain->map_rgb(0, 0, 0));
|
||||||
|
memset(buf, 0, sizeof(buf));
|
||||||
|
strncpy(buf, prefs->leaderboard_board, sizeof(buf) - 1);
|
||||||
|
currentIndex = strlen(buf);
|
||||||
|
if(currentIndex == 0)
|
||||||
|
{
|
||||||
|
buf[0] = 'A';
|
||||||
|
currentIndex = 0;
|
||||||
|
}
|
||||||
|
editing = 1;
|
||||||
|
build_all();
|
||||||
|
if(gengine->window_handle())
|
||||||
|
SDL_StartTextInput(gengine->window_handle());
|
||||||
|
}
|
||||||
|
|
||||||
|
void edit_board_t::close()
|
||||||
|
{
|
||||||
|
if(gengine->window_handle())
|
||||||
|
SDL_StopTextInput(gengine->window_handle());
|
||||||
|
clean();
|
||||||
|
}
|
||||||
|
|
||||||
|
void edit_board_t::change(int delta)
|
||||||
|
{
|
||||||
|
kobo_form_t::change(delta);
|
||||||
|
if(!selected()) return;
|
||||||
|
selection = selected()->tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
void edit_board_t::build()
|
||||||
|
{
|
||||||
|
medium();
|
||||||
|
space(2);
|
||||||
|
label("Global Leaderboard");
|
||||||
|
label("Use arrows or keyboard to enter board name");
|
||||||
|
label("(share it with friends to compete)");
|
||||||
|
|
||||||
|
big();
|
||||||
|
space();
|
||||||
|
button(buf, 1);
|
||||||
|
space();
|
||||||
|
|
||||||
|
button("Ok", MENU_TAG_OK);
|
||||||
|
button("Cancel", MENU_TAG_CANCEL);
|
||||||
|
}
|
||||||
|
|
||||||
|
void edit_board_t::rebuild()
|
||||||
|
{
|
||||||
|
int sel = selected_index();
|
||||||
|
build_all();
|
||||||
|
select(sel);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool board_char_valid(int c)
|
||||||
|
{
|
||||||
|
return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')
|
||||||
|
|| (c >= '0' && c <= '9') || c == '-' || c == '_';
|
||||||
|
}
|
||||||
|
|
||||||
|
st_edit_board_t::st_edit_board_t()
|
||||||
|
{
|
||||||
|
name = "edit_board";
|
||||||
|
}
|
||||||
|
|
||||||
|
void st_edit_board_t::frame() { manage.run_intro(); }
|
||||||
|
void st_edit_board_t::enter() { menu.open(); run_intro = 0; sound.ui_ok(); }
|
||||||
|
void st_edit_board_t::leave() { menu.close(); }
|
||||||
|
void st_edit_board_t::post_render() { kobo_basestate_t::post_render(); menu.render(); }
|
||||||
|
|
||||||
|
void st_edit_board_t::press(int button)
|
||||||
|
{
|
||||||
|
if(menu.editing)
|
||||||
|
{
|
||||||
|
switch(button)
|
||||||
|
{
|
||||||
|
case BTN_EXIT:
|
||||||
|
sound.ui_ok();
|
||||||
|
menu.editing = 0;
|
||||||
|
menu.next(); // Select the CANCEL option.
|
||||||
|
menu.next();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case BTN_FIRE:
|
||||||
|
if(!prefs->use_joystick) break;
|
||||||
|
// fallthrough
|
||||||
|
case BTN_START:
|
||||||
|
case BTN_SELECT:
|
||||||
|
sound.ui_ok();
|
||||||
|
menu.editing = 0;
|
||||||
|
menu.next(); // Select the OK option.
|
||||||
|
break;
|
||||||
|
|
||||||
|
case BTN_UP:
|
||||||
|
case BTN_INC:
|
||||||
|
{
|
||||||
|
char &c = menu.buf[menu.currentIndex];
|
||||||
|
if(!c) c = 'A';
|
||||||
|
else if(c == 'Z') c = 'a';
|
||||||
|
else if(c == 'z') c = '0';
|
||||||
|
else if(c == '9') c = '-';
|
||||||
|
else if(c == '-') c = '_';
|
||||||
|
else if(c == '_') c = 'A';
|
||||||
|
else c++;
|
||||||
|
sound.ui_tick();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case BTN_DOWN:
|
||||||
|
case BTN_DEC:
|
||||||
|
{
|
||||||
|
char &c = menu.buf[menu.currentIndex];
|
||||||
|
if(!c) c = 'A';
|
||||||
|
else if(c == 'A') c = '_';
|
||||||
|
else if(c == '_') c = '-';
|
||||||
|
else if(c == '-') c = '9';
|
||||||
|
else if(c == '0') c = 'z';
|
||||||
|
else if(c == 'a') c = 'Z';
|
||||||
|
else c--;
|
||||||
|
sound.ui_tick();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case BTN_RIGHT:
|
||||||
|
if(menu.currentIndex < sizeof(menu.buf) - 2)
|
||||||
|
{
|
||||||
|
menu.currentIndex++;
|
||||||
|
sound.ui_tick();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sound.ui_error();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if(menu.buf[menu.currentIndex] == '\0')
|
||||||
|
menu.buf[menu.currentIndex] = 'A';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case BTN_LEFT:
|
||||||
|
case BTN_BACK:
|
||||||
|
if(menu.currentIndex > 0)
|
||||||
|
{
|
||||||
|
menu.buf[menu.currentIndex] = '\0';
|
||||||
|
menu.currentIndex--;
|
||||||
|
sound.ui_tick();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
sound.ui_error();
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
if(board_char_valid(unicode))
|
||||||
|
{
|
||||||
|
menu.buf[menu.currentIndex] = (char)unicode;
|
||||||
|
if(menu.currentIndex < sizeof(menu.buf) - 2)
|
||||||
|
{
|
||||||
|
menu.currentIndex++;
|
||||||
|
sound.ui_tick();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
sound.ui_error();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
sound.ui_error();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
menu.rebuild();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
menu.selection = -1;
|
||||||
|
switch(button)
|
||||||
|
{
|
||||||
|
case BTN_EXIT: menu.selection = MENU_TAG_CANCEL; break;
|
||||||
|
case BTN_CLOSE: menu.selection = MENU_TAG_OK; break;
|
||||||
|
case BTN_FIRE:
|
||||||
|
case BTN_START:
|
||||||
|
case BTN_SELECT: menu.change(0); break;
|
||||||
|
case BTN_INC:
|
||||||
|
case BTN_UP: menu.prev(); break;
|
||||||
|
case BTN_DEC:
|
||||||
|
case BTN_DOWN: menu.next(); break;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch(menu.selection)
|
||||||
|
{
|
||||||
|
case 1:
|
||||||
|
if(button == BTN_START || button == BTN_SELECT
|
||||||
|
|| button == BTN_FIRE)
|
||||||
|
{
|
||||||
|
sound.ui_ok();
|
||||||
|
menu.editing = 1;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MENU_TAG_OK:
|
||||||
|
{
|
||||||
|
// Trim trailing garbage from arrow-key wandering.
|
||||||
|
size_t n = strlen(menu.buf);
|
||||||
|
while(n > 0 && !board_char_valid(menu.buf[n - 1]))
|
||||||
|
menu.buf[--n] = '\0';
|
||||||
|
strncpy(prefs->leaderboard_board, menu.buf,
|
||||||
|
sizeof(prefs->leaderboard_board) - 1);
|
||||||
|
prefs->leaderboard_board[
|
||||||
|
sizeof(prefs->leaderboard_board) - 1] = '\0';
|
||||||
|
prefs->changed = 1;
|
||||||
|
sound.ui_ok();
|
||||||
|
pop();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case MENU_TAG_CANCEL:
|
||||||
|
sound.ui_cancel();
|
||||||
|
pop();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
st_edit_board_t st_edit_board;
|
||||||
|
|
||||||
|
|||||||
49
states.h
49
states.h
@ -473,6 +473,53 @@ class st_ask_abort_game_t : public st_yesno_base_t
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/*----------------------------------------------------------
|
||||||
|
Edit leaderboard board name
|
||||||
|
----------------------------------------------------------*/
|
||||||
|
|
||||||
|
class edit_board_t : public kobo_form_t
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
int editing;
|
||||||
|
char buf[32];
|
||||||
|
unsigned currentIndex;
|
||||||
|
int selection;
|
||||||
|
virtual void change(int delta);
|
||||||
|
virtual void build();
|
||||||
|
void open();
|
||||||
|
void close();
|
||||||
|
void rebuild();
|
||||||
|
};
|
||||||
|
|
||||||
|
class st_edit_board_t : public kobo_basestate_t
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
edit_board_t menu;
|
||||||
|
st_edit_board_t();
|
||||||
|
void enter();
|
||||||
|
void leave();
|
||||||
|
void press(int button);
|
||||||
|
void frame();
|
||||||
|
void post_render();
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/*----------------------------------------------------------
|
||||||
|
Global Leaderboard browser
|
||||||
|
----------------------------------------------------------*/
|
||||||
|
|
||||||
|
class st_global_leaderboard_t : public kobo_basestate_t
|
||||||
|
{
|
||||||
|
int start_time;
|
||||||
|
public:
|
||||||
|
st_global_leaderboard_t();
|
||||||
|
void enter();
|
||||||
|
void press(int button);
|
||||||
|
void frame();
|
||||||
|
void post_render();
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
/*----------------------------------------------------------
|
/*----------------------------------------------------------
|
||||||
Debug: Audio Engine Profiling
|
Debug: Audio Engine Profiling
|
||||||
----------------------------------------------------------*/
|
----------------------------------------------------------*/
|
||||||
@ -534,6 +581,8 @@ extern st_options_graphics_t st_options_graphics;
|
|||||||
extern st_options_audio_t st_options_audio;
|
extern st_options_audio_t st_options_audio;
|
||||||
extern st_options_control_t st_options_control;
|
extern st_options_control_t st_options_control;
|
||||||
extern st_options_game_t st_options_game;
|
extern st_options_game_t st_options_game;
|
||||||
|
extern st_edit_board_t st_edit_board;
|
||||||
|
extern st_global_leaderboard_t st_global_leaderboard;
|
||||||
extern st_error_t st_error;
|
extern st_error_t st_error;
|
||||||
#ifdef PROFILE_AUDIO
|
#ifdef PROFILE_AUDIO
|
||||||
extern st_profile_audio_t st_profile_audio;
|
extern st_profile_audio_t st_profile_audio;
|
||||||
|
|||||||
98
tools/mock-leaderboard.py
Executable file
98
tools/mock-leaderboard.py
Executable file
@ -0,0 +1,98 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Mock leaderboard server for local testing.
|
||||||
|
|
||||||
|
Stores submitted scores in memory, returns rank/total on POST, and serves
|
||||||
|
the top-N on GET. Sends permissive CORS so the web build can talk to it
|
||||||
|
from a different port.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
./tools/mock-leaderboard.py # listens on :8765
|
||||||
|
./tools/mock-leaderboard.py 8080 # custom port
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||||
|
from urllib.parse import urlparse, parse_qs
|
||||||
|
|
||||||
|
# In-memory store: {board_name: [score_record, ...]}
|
||||||
|
SCORES = {}
|
||||||
|
|
||||||
|
|
||||||
|
def _cors(handler):
|
||||||
|
handler.send_header("Access-Control-Allow-Origin", "*")
|
||||||
|
handler.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
||||||
|
handler.send_header("Access-Control-Allow-Headers", "Content-Type")
|
||||||
|
|
||||||
|
|
||||||
|
class Handler(BaseHTTPRequestHandler):
|
||||||
|
def do_OPTIONS(self):
|
||||||
|
self.send_response(204)
|
||||||
|
_cors(self)
|
||||||
|
self.end_headers()
|
||||||
|
|
||||||
|
def do_GET(self):
|
||||||
|
url = urlparse(self.path)
|
||||||
|
if url.path != "/api/scores":
|
||||||
|
self.send_response(404); _cors(self); self.end_headers(); return
|
||||||
|
q = parse_qs(url.query)
|
||||||
|
board = (q.get("board") or [""])[0]
|
||||||
|
limit = int((q.get("limit") or ["50"])[0])
|
||||||
|
rows = sorted(SCORES.get(board, []),
|
||||||
|
key=lambda r: r["score"], reverse=True)[:limit]
|
||||||
|
body = json.dumps({
|
||||||
|
"board": board,
|
||||||
|
"total": len(SCORES.get(board, [])),
|
||||||
|
"scores": [
|
||||||
|
{"name": r["name"], "score": r["score"],
|
||||||
|
"end_scene": r["end_scene"]}
|
||||||
|
for r in rows
|
||||||
|
],
|
||||||
|
}).encode()
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header("Content-Type", "application/json")
|
||||||
|
_cors(self)
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(body)
|
||||||
|
|
||||||
|
def do_POST(self):
|
||||||
|
if urlparse(self.path).path != "/api/scores":
|
||||||
|
self.send_response(404); _cors(self); self.end_headers(); return
|
||||||
|
length = int(self.headers.get("Content-Length", "0"))
|
||||||
|
try:
|
||||||
|
payload = json.loads(self.rfile.read(length))
|
||||||
|
except Exception as e:
|
||||||
|
self.send_response(400); _cors(self); self.end_headers()
|
||||||
|
self.wfile.write(f"bad json: {e}".encode())
|
||||||
|
return
|
||||||
|
board = payload.get("board", "")
|
||||||
|
SCORES.setdefault(board, []).append(payload)
|
||||||
|
# Rank: 1 = best. Sort descending, find position.
|
||||||
|
ranked = sorted(SCORES[board], key=lambda r: r["score"], reverse=True)
|
||||||
|
# Identify *this* submission by reference equality.
|
||||||
|
rank = next((i + 1 for i, r in enumerate(ranked) if r is payload), -1)
|
||||||
|
body = json.dumps({"rank": rank, "total": len(SCORES[board])}).encode()
|
||||||
|
print(f"[POST] board={board} name={payload.get('name')} "
|
||||||
|
f"score={payload.get('score')} -> #{rank}/{len(SCORES[board])}")
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header("Content-Type", "application/json")
|
||||||
|
_cors(self)
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(body)
|
||||||
|
|
||||||
|
def log_message(self, fmt, *args):
|
||||||
|
# Keep the noise down; we print our own line on POST.
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
port = int(sys.argv[1]) if len(sys.argv) > 1 else 8765
|
||||||
|
srv = HTTPServer(("127.0.0.1", port), Handler)
|
||||||
|
print(f"mock-leaderboard listening on http://127.0.0.1:{port}")
|
||||||
|
try:
|
||||||
|
srv.serve_forever()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
68
web/shell.html
Normal file
68
web/shell.html
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Kobo Deluxe</title>
|
||||||
|
<style>
|
||||||
|
html, body { margin: 0; height: 100%; background: #000; color: #ccc;
|
||||||
|
font-family: monospace; }
|
||||||
|
#wrap { display: flex; flex-direction: column; align-items: center;
|
||||||
|
justify-content: center; height: 100%; }
|
||||||
|
canvas { background: #000; image-rendering: pixelated; outline: none;
|
||||||
|
max-width: 100%; max-height: 100vh; }
|
||||||
|
#status { padding: 8px; font-size: 13px; }
|
||||||
|
#back { position: fixed; top: 10px; left: 12px; z-index: 10;
|
||||||
|
color: #888; text-decoration: none; font-size: 13px;
|
||||||
|
padding: 4px 8px; border: 1px solid #333; border-radius: 3px;
|
||||||
|
background: rgba(0,0,0,0.6); }
|
||||||
|
#back:hover { color: #ddd; border-color: #666; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<a id="back" href="https://lindholm.dev">← lindholm.dev</a>
|
||||||
|
<div id="wrap">
|
||||||
|
<canvas id="canvas" tabindex="-1" oncontextmenu="event.preventDefault()"></canvas>
|
||||||
|
<div id="status">Loading…</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
var statusEl = document.getElementById('status');
|
||||||
|
var Module = {
|
||||||
|
canvas: (function () {
|
||||||
|
var c = document.getElementById('canvas');
|
||||||
|
c.addEventListener('webglcontextlost', function (e) {
|
||||||
|
alert('WebGL context lost. Reload to continue.'); e.preventDefault();
|
||||||
|
}, false);
|
||||||
|
return c;
|
||||||
|
})(),
|
||||||
|
print: function (t) { console.log(t); },
|
||||||
|
printErr: function (t) { console.warn(t); },
|
||||||
|
setStatus: function (t) { statusEl.textContent = t; },
|
||||||
|
preRun: [function () {
|
||||||
|
// Persist config + high scores across reloads via IndexedDB.
|
||||||
|
// syncfs(true) is async and replaces /home with whatever's in IDBFS,
|
||||||
|
// so we (1) gate startup on it via addRunDependency, and (2) re-mkdir
|
||||||
|
// /home/scores *after* the load (the game's addPlayer doesn't auto-
|
||||||
|
// create the score dir; see the TODO in score.cpp:504).
|
||||||
|
try { FS.mkdir('/home'); } catch (e) {}
|
||||||
|
FS.mount(IDBFS, {}, '/home');
|
||||||
|
Module.addRunDependency('idbfs');
|
||||||
|
FS.syncfs(true, function (err) {
|
||||||
|
if (err) console.warn('IDBFS load failed:', err);
|
||||||
|
try { FS.mkdir('/home/scores'); } catch (e) {}
|
||||||
|
Module.removeRunDependency('idbfs');
|
||||||
|
});
|
||||||
|
}],
|
||||||
|
postRun: [function () {
|
||||||
|
// Push the in-memory /home back to IndexedDB on tab close. visibility-
|
||||||
|
// change covers mobile, where beforeunload is unreliable.
|
||||||
|
var flush = function () { FS.syncfs(false, function () {}); };
|
||||||
|
window.addEventListener('beforeunload', flush);
|
||||||
|
document.addEventListener('visibilitychange', function () {
|
||||||
|
if (document.visibilityState === 'hidden') flush();
|
||||||
|
});
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
{{{ SCRIPT }}}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
x
Reference in New Issue
Block a user