Changelog
All notable changes to this project will be documented in this file.
[Unreleased]
Added
- RPG Maker XP support, first slice — a project now loads and boots. A new
RGSS data layer (
mruby-rpgxp/mrblib/rgss_data.rb) declares theRPG::*schema so an XP project'sData/*.rxdatafiles load straight throughMarshal.load(the bundled mruby-marshal reads the 4.8 stream; the value typesTable/Color/Tone/Rectalready round-trip natively in mruby-rgss), andRPGXP::RGSSDatawraps them as a database (the roleLCF::Databaseplays for RPG2000). TheRPGXPruntime readsGame.ini, builds the database and drives a scene stack: aScene::Title(title graphic- the default New Game / Continue / Shutdown commands in an XP-styled window)
and a first walkable
Scene::Map(the three tile layers as placeholder colour blocks, the party leader drawn from itsGraphics/Characterssheet, grid movement with tileset passability and an edge-clamped follow camera) — the same staged approach the RPG2000 runtime took.src/main.cxxsizes the window to XP's native 640×480 when it detects an XP project and the size was not overridden. The schema is the mruby/CRuby common subset, soscripts/rpgxp_testbed_check.rbvalidates it under CRuby against a real downloaded project (data/OpenGame.exe/Testbed/XP) in CI, andmruby-rpgxp/testunit-tests the data round-trip, tileset passability, camera, CharSet geometry and save/load. Running the game's bundled RGSS scripts (Data/Scripts.rxdata) is future work; seedocs/adr/0010-rpgxp-rgss-data-layer.md.
- the default New Game / Continue / Shutdown commands in an XP-styled window)
and a first walkable
- RPG Maker XP event system — map events now run.
RPGXP::Game::EventPageselects each event's active page (switch / variable / self-switch conditions, highest matching page wins), and a newRPGXP::Game::Interpreterruns the XP event-command list with a suspend/resume model (the scene drives the UI): Show Text (+ 401 continuations), Show Choices with branch selection, Conditional Branch / Else / End, Loop / Break / Repeat, Label / Jump, Call Common Event, Control Switches / Variables / Self Switch, Change Gold, Transfer Player and Play BGM/BGS/ME/SE — indent- and terminator-driven control flow with a per-frame step cap.Scene::Mapwires this in: it picks each event's page, starts it on the action button, on player touch, on autorun, or as a background parallel process, shows a bottom message/choice window, re-selects pages when an event finishes (so a self switch it set takes effect) and performs Transfer Player teleports. State gained gold and per-(map, event, channel) self switches (persisted in the save). Covered by newmruby-rpgxp/testcases and byscripts/rpgxp_testbed_check.rb, which now drives the real test bed's event commands end to end. (Fixes along the way: the runtime avoidsInteger#zero?/sprintf/String#strip/ regexp, which this mruby build does not bundle.) - The event interpreter now supports Erase Event. Running it removes the
event that is executing (foreground or a parallel process) from the map for
the rest of the visit — its marker, movement, collision tile and, for a
parallel event, its background process — while the rest of the command list
still runs (Erase Event does not pause the interpreter). A common event, which
has no map event, is unaffected. Covered by new checks in
scripts/rpg2k_logic_check.rbandscripts/rpg2k_scene_check.rb. - Control Variables now supports more operand sources than constants and
other variables: a random integer in a range, an actor stat (level,
current/max HP and MP, attack, defence, spirit or agility of an actor by id)
and game quantities (party gold, timer seconds).
Game::Interpretercarries a deterministic RNG for the random operand; unmodelled sources (EXP, step count, play time, save/battle counts) read as 0. Covered by new checks inscripts/rpg2k_logic_check.rb. - LCF save-data schema now decodes four more
LcfSaveDatasections, transcribed from the rpg2kpsp analysis wiki (https://w.atwiki.jp/rpg2kpsp/): show-picture state (chunk 103), saved party-member status (108), remembered teleport/escape targets (110) and saved map-event state (111). The teleport targets are anArray2Dindexed by map id (index 0 = escape); the map-event section reuses the existing movable layout for each event's position snapshot plus the packed tile-replacement blobs. Overlapping sections already covered by the 200X notes (system, terminology, positions) were left untouched. Covered by a newLCF::SaveDatacheck inmruby-lcf/test/lcf_test.rb. See ADR 0009. - LCF save-data schema validated against a real
Save01.lsd, generated by running the RPG Maker 2000 game Nepheshel under wine via EasyRPG Player and saving in-game at the town-entrance Gate. A newscripts/lcf_save_check.rbloads the parser over a real.lsd, lists documented vs. undocumented chunks and reads every documented field;scripts/run-nepheshel-easyrpg-wine.bashcaptures the wine/EasyRPG setup. Real data confirmed all ten documented sections and catalogued the still-undocumented chunks (102, 109, 112, 113, 114, 200). See ADR 0010.
Fixed
- LCF reader gaps the real save exposed (
mruby-lcf/mrblib/lcf.rb): the save schema's:bool_arrayswitches (chunk 32) and:doubletimestamp (chunk 100) had no decoder and raisedUnsupported type; the screen-transition fields (SAVE_SYSTEM111–116) were typed:int(BER) but are stored as a single raw byte (0xff= "use database value"), whichread_berchoked on with a crypticTypeError. Added:bool_array,:doubleand a scalar:uint8type, retyped the transition fields, and maderead_berraise a clear "truncated BER integer". Locked in bymruby-lcf/test/lcf_test.rb. - MV text rendering: the Canvas2D bridge now draws real glyphs.
fillText,strokeTextandmeasureTextare backed by stb_truetype against the game's bundled TrueType font (the CSSGameFont, auto-discovered under the project'sfonts/dir), so window/menu/title text — which MV draws throughBitmap.drawText→context.fillText— actually appears. Horizontal alignment (textAligncentre/right, driven by the now-accuratemeasureText),textBaseline, the current transform (translate/uniform-scale) and a dilated outline forstrokeTextare all honoured. Previously these were no-ops, so the boot reached the title but drew no text. - MV render capture: a
--mv_screenshot=PATHflag writes a PNG of the rendered MV frame a couple of seconds into the boot (MV::JS.screenshotencodes the live PIXI canvas via stb_image_write), and the CI smoke test uploads it as themv-screenshotartifact — so the actual on-screen output can be inspected as the maker is brought up, not just the engine's log. With the boot fixes below, the KinoAR/Lunatic-Core test bed now reachesScene_Titlecleanly ([MV] scene: Scene_Boot -> Scene_Title). - MV boot now reaches PIXI init and continues through
Graphics.initialize(milestone M4/M5): with the JS host in place PIXI 4.x starts in Canvas mode against the Canvas2D bridge. Cleared the init-time blockers that followed: the bundled FPSMeter debug overlay (which throws when instantiated on this host) is replaced with a no-op exposing the tick/show/hide methods MV drives each frame; the document/element shim gained the surfaceGraphics.initializewalks (getElementsByTagName('head'), elementgetElementsByTagNamefor_disableContextMenu, a<style>.sheet.insertRule,createTextNode, child-returningappendChild,classList); a silentAudioContextstub letsSceneManager.initAudiosucceed (real Web Audio →RGSS::Audiois a later milestone); anddocument.getElementsByTagName('script')now returns a readable entry soUtils.canReadGameFiles/checkFileAccesspasses. The wholeSceneManager.initializesequence now completes, and adocument.fonts(FontFaceSet) stub reporting the game font ready makesScene_Bootadvance to the title instead of looping forever on MV's measured-text-width font check (which a font-agnosticmeasureTextcan't satisfy).windowalso gained the inert event/lifecycle methods MV wires up (addEventListener/removeEventListener/dispatchEvent/close), sinceGraphics._setupEventHandlers,InputandTouchInputregister listeners on it; and the canvas gradient factories (createLinearGradientetc.) return chainable stubs soBitmap.gradientFillRectdoesn't crash. With these, the MV test bed (KinoAR/Lunatic-Core) now boots end-to-end with no engine errors — through PIXI init,SceneManager.initialize,Scene_Bootand on into the scene graph — andMV#main_looplogs each scene transition so the boot's progress is visible in the smoke test. Covered bymruby-mvjs/test. - MV boot resilience (milestone M4): script evaluation now follows browser
semantics — a
<script>that throws while executing is logged and the next one still runs, instead of aborting the whole engine. Previously theiphone-inline-videolibrary (an iOS-only inline-video shim MV bundles) threwinvalid 'in' operandunder quickjs and took the entire boot down with it.MV#bootnow catches and logs each script's (andwindow.onload's) errors and carries on, and installs a no-opmakeVideoPlayableInlinefallback so video creation can't crash later on this (video-less) host. - MV transform-aware canvas drawing (milestone M4): the
CanvasRenderingContext2Dshim now tracks the full 2D transform —save/restore,translate/scale/rotate/transform/setTransform/resetTransform— anddrawImagehonours it. PIXI's canvas renderer positions every sprite by setting the transform and drawing at the origin, so without this all sprites piled up at (0,0); now they land where they belong.drawImagerasterises by walking the transformed dest rect's device-space bounding box and inverse-mapping each pixel, so scale, rotation and translation all work with no gaps;fillRect/clearRectmap their rect through the matrix (exact for translate/scale). Covered bymruby-mvjs/test/canvas_test.rb. - MV on-screen present (milestone M4): the MV canvas is now drawn to the screen
each frame.
MV#bootcreates one full-screenRGSS::Sprite/Bitmap, andMV::JS.presentcopies MV's PIXI canvas (resolved from the running game's renderer view /Graphics._canvas) into that bitmap — swapping R/B for the RGBA→ARGB8888 layout and marking it dirty soGraphics.updaterepaints it. mruby-rgss gained a small exported accessor (include/rgss_bitmap.hxx,rgss::bitmap_pixels) so the present blits straight into the bitmap's buffer. Covered bymruby-mvjs/test/canvas_test.rb. (Correct sprite positioning still needs transform-awaredrawImage, which PIXI's canvas renderer relies on.) - MV PNG image loading and asset path rooting (milestone M4):
new Image()now decodes a game PNG through stb_image (__mv_imageLoadinmvcanvas.cxx) straight into a native RGBA canvas, exposingwidth/heightand firingonload/onerrorasynchronously (on the next host frame) as the browser contract MV'sBitmaploader expects; the decoded image is a first-classdrawImagesource. Because the process is not chdir'd into the game dir, MV's game-relative asset requests (data/*.json,img/*.png, saves) are now rooted at the game directory via a sharedmv_resolve_path, configured from Ruby withMV::JS.base_dir=at boot. Covered bymruby-mvjs/test(canvas_test.rbimage decode/draw,host_test.rbbase-dir resolution). scripts/download-lunatic-core.bash: fetches a small, complete RPG Maker MV project (KinoAR/Lunatic-Core — full corescript +data/*.json+img/) into the git-ignoreddata/dir as the MV test bed, mirroring the RPG Maker 2000 Nepheshel download. It is detected as an MV game bysrc/main.cxx(js/rpg_core.js+data/System.json) and is downloaded, never vendored.- MV Canvas2D rendering bridge, first slice (milestone M4):
document,HTMLCanvasElementand aCanvasRenderingContext2Dbacked by native RGBA8 buffers (mruby-mvjs/src/mvcanvas.cxx).getContext('2d')returns the shim andgetContext('webgl')returnsnull, so PIXI.js falls back to its Canvas renderer. Implements the drawing subset that renderer uses —fillRect(withfillStyle#rgb/#rrggbb/rgba()andglobalAlpha),clearRect, nearest-neighbour scaleddrawImage(canvas→canvas),getImageData, andmeasureText— with path/transform/text operations stubbed for now. Canvas buffers are display-independent, so the primitives are unit-tested by reading pixels back (mruby-mvjs/test/canvas_test.rb). On-screen present and PNGImageloading follow. - MV JavaScript host environment, first slice (milestone M3):
MV::JS.evalnow runs against a persistent quickjs-ng runtime/context (state carries across calls, as the MV engine expects) instead of a throwaway per call, and the context is bootstrapped with the first browser/host globals —window/self/global/globalThisaliases, aconsole(log/info/warn/error), a native synchronous file reader (__mv_readFileSync), and a minimal synchronousXMLHttpRequestbuilt on it (how MV'sDataManagerloadsdata/*.json). Also the event-loop machinery —setTimeout/setInterval/clearTimeout,requestAnimationFrame/cancelAnimationFrameandperformance.now— driven by a newMV::JS.pump(now_ms) that fires due timers and animation-frame callbacks and drains promise microtasks, so the JS game shares the engine's fixed cadence instead of blocking; plus passivenavigator/location/localStoragestubs. Also the NW.js-stylerequire('fs'|'path')MV uses for local-file saves (backed by native read/write/exists), andMV::JS.eval_file(path)to load a script into the host.MV#bootnow evaluates the MV core scripts (MV::CORE_SCRIPTS) in order viaeval_file, andMV#main_loopadvances the host withMV::JS.pumpat 60 fps — wired but still gated behindMV.runtime_available?(false) until the Canvas2D rendering bridge (M4) lands. Covered bymruby-mvjs/test/host_test.rb - Embedded JavaScript engine for RPG Maker MV support (milestone M2): added
quickjs-ng as the
3rd/quickjsgit submodule and static-linked itsqjslibrary into both the main executable and the mruby test binary.MV::JS.eval(inmruby-mvjs/src/mvjs.cxx) evaluates JavaScript and marshals scalar results (Integer/Float/String/true/false/nil) back to mruby, raisingRuntimeErroron a JS exception;MV.js_available?reports that the engine is compiled in. Covered bymruby-mvjs/test/js_test.rb - Groundwork for JavaScript RPG Maker (MV) support (approach: embed a real
JavaScript engine and run the game's own scripts, rather than reimplement the
engine in mruby):
- New
mruby-mvjsgem — a thin Ruby orchestration layer (MV) that detects an MV project (js/rpg_core.js+data/System.json), knows the canonical MV script load order, and defines the boot/pump handshake with a clearly-marked seam where the embedded runtime plugs in. The runtime (quickjs-ng) is not built into the binary yet, so an MV game is detected but reports that support is under construction instead of misbehaving src/main.cxxnow sniffs the MV directory layout and instantiatesMV, alongside the existing RPG2k/RPGXP detection- Host-runnable specs (
mruby-mvjs/test) covering project detection and the script load order - Documented the decision, layered architecture and milestone roadmap in
docs/adr/0004-javascript-maker-mv-quickjs.mdanddocs/TODO.md
- New
- The event interpreter now supports Change Parameters: it adjusts an
actor's base stat — max HP, max MP, attack, defence, spirit or agility — for a
fixed actor, a variable-selected actor or the whole party, by a constant or
variable amount.
Game::Actor#change_paramclamps to RPG2000's limits (max HP/MP 1–9999, the battle stats 1–999) and re-clamps current HP/MP when a maximum is lowered. Covered by new checks inscripts/rpg2k_logic_check.rb. - The event interpreter now supports Change HP, Change MP and Full
Heal. Each targets a fixed actor, an actor whose id is held in a variable,
or the whole party, and takes a constant or variable amount.
Game::Actorgainedchange_hp/change_mp/full_healhelpers that clamp to the actor's maxima; Change HP honours RPG2000's "allow death" flag (the HP floor is 0 when knockout is allowed, 1 otherwise). Covered by new checks inscripts/rpg2k_logic_check.rb. - The event interpreter now supports Move Event (Set Move Route). The
command's parameters pack a target id followed by a forced move route; the
interpreter decodes that route (including the switch / change-graphic /
play-sound sub-commands, whose strings live in the command's string field) into
Game::MoveCommands and queues a non-blocking request — a Move Event runs in the background rather than pausing the interpreter.Scene::Mapdrains those requests and applies the route as a forced route to the target: a specific map event, "this event" (the event running the command, tracked for foreground and parallel processes) or the player (mirrored by aGame::Character, with input movement suppressed while it runs). A forced route overrides page movement until it finishes (a repeating route runs until replaced) and is paced by the requested move frequency. Vehicle targets (boat/ship/airship) are recognised but not yet modelled. Covered by new checks inscripts/rpg2k_logic_check.rbandscripts/rpg2k_scene_check.rb. - Parallel-process events (page trigger 4) and parallel common events now
run continuously in the background. Each gets its own looping
Game::Interpreterdriven byScene::Map#step_parallels: it runs its command list, honoursWait, and restarts when it finishes. A parallel common event that needs a flag is gated by its switch, re-checked every frame so toggling the switch starts or stops it. Background processes deliberately do not drive the message/choice/teleport UI (those requests are skipped) and pause while a foreground event or message is active, so they never fight the player for the message window. Auto-start common events now run only in the foreground (no longer double-counted as parallel). Covered by new checks inscripts/rpg2k_scene_check.rb. - Map events now fire on touch. In addition to the action button (trigger 0)
and auto-start (3),
Scene::Mapruns player-touch events (trigger 1: the player walks into the event) and event-touch events (trigger 2: the event walks into the player). A touched event turns to face the player before its commands run, the player does not step onto the event, and once any event's process starts, player movement / the action button / the menu are held until it finishes. True parallel-process events (trigger 4) are still to come. Covered by new checks inscripts/rpg2k_scene_check.rb(the input stub is now scriptable). - The event interpreter now supports Call Event. A Call Event command
suspends the current command list and runs a referenced list to completion,
then resumes where it left off, via a call stack on
Game::Interpreterand a resolver the scene supplies (common events by id; a map event's page from the loaded map). This makes call-only common events (start condition 5) — which auto-start and parallel processing never reach — actually run, and lets events share logic. Nested calls unwind correctly and recursion is bounded (MAX_CALL_DEPTH) so a self-calling event terminates instead of hanging. Conditional Branch also gained the timer condition (compare the countdown timer's seconds).Scene::Mapwires a resolver over its common and map events (refreshed on teleport). Covered by new checks inscripts/rpg2k_logic_check.rbandscripts/rpg2k_scene_check.rb. - Events now move on the map. Decoded move routes (
LCF.parse_move_commands) and event-page movement settings, which previously only parsed, now drive events at runtime. A newGame::MoveRouteexecutor runs a decoded route against a movableGame::Character— all of the RPG2000 move-route commands (cardinal/diagonal/random/toward-hero/away-hero/forward moves, the face and turn commands, wait/jump markers, switch on/off, change graphic, play sound, and the speed/frequency/through/animation/transparency/facing-lock toggles), honouring the route's repeat and skippable flags (a blocked non-skippable move turns to face the obstacle and retries).Game::MoveTypeimplements the autonomous walk types (random, vertical/horizontal bounce, approach and flee the hero), and a smallGame::Rngsupplies the randomness they need (mruby is built here withoutmruby-random).Scene::Mapgives every event aGame::Character, steps it each frame per its page's move type or custom move route — paced by the event's move frequency — through aMapWorldadapter that reports passability, the hero position and switch/sound side effects, and keeps events from overlapping each other, the player or impassable tiles. Two host harnesses cover the new code under CRuby (the SDL/mruby binary can't run in CI's cheap path):scripts/rpg2k_logic_check.rbexercises the pure move-route, movement-type and interpreter logic, andscripts/rpg2k_scene_check.rbconstructs the actualScene::Mapbehind RGSS stubs and ticks it to verify the event-movement wiring. Both run in CI next to the LCF test-bed check. - Wio Terminal (Seeed, ATSAMD51) port — P1 hardware bring-up. A new PlatformIO
target (
platformio.ini,app/wio/) builds an Arduino firmware around a new LVGL display + input HAL (mruby-rgss/src/wio.cxx): the 320×240 ILI9341 LCD is driven in LVGL partial-render mode (a small draw buffer, not a 150 KB full framebuffer that would exhaust the 192 KB SRAM), the tick/delay source comes from Arduinomillis()/delay(), and the three buttons + 5-way switch are scanned intoRGSS::Inputviargss_wio_poll(wio_input_bridge.cxx), called fromGraphics.updatenext to the SDL/terminal poll hooks. Awiomruby ARM cross-build (MRUBY_TARGET=wioinbuild_config.rb) and SD-backed newlib syscalls (app/wio/src/sd_syscalls.cxx) are scaffolded for the later interpreter/asset slices. All of it is additive and guarded (WIO_TERMINAL), so the desktop and wasm builds are unchanged. Design and memory budget indocs/adr/0007-wio-terminal-port.md; a CI job compiles the firmware. - The WebAssembly build now loads an RPG Maker project at runtime instead of
requiring one to be baked into the page at compile time, so a single build
plays any game. A new Emscripten shell (
src/shell.html) offers a loader that accepts a local.zip, a direct.zipURL, or a GitHub repository (owner/repo,owner/repo@ref, or agithub.comURL, resolved to itscodeloadarchive). The zip is unpacked in the browser with the nativeDecompressionStreamAPI — no third-party JavaScript — the folder containingRPG_RT.ldb(RPG2k) orGame.ini(XP) is located (top-level wrapper folders, as GitHub archives use, are handled) and mounted at/game, then the exportedrpg_start_game()constructs the game and starts the frame loop. Under Emscriptenmain()now sets up the interpreter and display and returns to the browser, deferring game construction until a project is present; a project baked in with-DWASM_GAME_DIRstill auto-starts. Cross-origin downloads (GitHub, most.zipURLs) need a CORS proxy — the loader has an optional field for one — or the always-works local-file path. Downloaded archives are cached (Cache Storage, keyed by the resolved URL) so re-loading the same URL or repo skips the network; the loader can force a fresh download or clear the cache. The URL and CORS-proxy fields persist across reloads (localStorage) and are mirrored into the page's?url=/?proxy=query string, so a project link is bookmarkable and shareable and a?url=link auto-loads - On-screen game keypad for the browser (WebAssembly) build so the game is
playable by touch or mouse without a physical keyboard. A custom Emscripten
shell page (
src/shell.html) draws<button>elements — a D-pad plus OK (C), Cancel (B), Dash (Shift), the L/R shoulders and the A/X/Y/Z buttons — laid out responsively beneath the game canvas. Each button's pointer handlers callModule._rgss_wasm_key, a tiny bridge exported fromsrc/wasm_keypad.cxxthat feeds the very same input buffer as the SDL keyboard watch, so a tapped button is indistinguishable from a pressed key (includingRGSS::Inputtrigger/repeat timing). Pointer capture and a window-blur handler prevent stuck keys, and physical keyboard input keeps working exactly as before. Wired up only for the Emscripten target via--shell-fileinCMakeLists.txt - Audio playback:
RGSS::Audiois now backed by SDL_mixer instead of inert stubs, so games play sound. Looping BGM and BGS, one-shot ME (music effects that interrupt the BGM and then let it resume, tracked per frame fromGraphics.update) and overlapping SE sound effects are all supported, with per-channel volume mapped from the RPG 0..100 scale. The public API resolves a game-supplied name to a real file the same wayBitmapdoes — searchingGAME_DIR/RTP_DIRcrossed with theMusic/,Sound/andAudio/*sub-folders and the known extensions (.ogg,.wav,.mid,.mp3,.flac) — and the event interpreter's Play BGM / Play SE commands now forward the command's volume and tempo. SDL is kept out of themruby-rgssgem (which is also built for the terminal-only, Emscripten and standalone-test variants): the gem's nativeAudioprimitives call through a plain function table (include/rgss_audio.hxx) that the executable fills with the SDL_mixer backend (src/sdl_audio.cxx) at startup, mirroring the SDL keyboard bridge. With no backend installed every call is a graceful no-op. Pitch/tempo is accepted but not applied (SDL_mixer has no pitch control), and MIDI playback depends on the SDL_mixer build having a synth; a file that fails to load is logged once and skipped. See ADR 0006 - The title menu plays the database's cursor-move sound effect (System > cursor SE) when the selection moves up or down, the first consumer of the new SE playback
- Move-route command decoding for the LCF loaders:
LCF.parse_move_commandsandLCF::MoveCommanddecode the compact move-command layout (bare commands plus the string/integer parameters that Switch On/Off, Change Graphic and Play Sound Effect carry), exposed through a new:move_commandsschema type wired into the event-page and common-eventmove_route.commandschunk. Previously that chunk was mis-declared as an event-command blob and failed to parse on real maps - Event-page condition schema now covers its trailing RPG2003 chunks
(
timer2_sec,compare_operator), so a real map's condition data parses with no unknown chunks. The RPG2000 runtime still compares variables with>= scripts/lcf_testbed_check.rb: a host smoke-test that runs the pure-Ruby LCF parser (mruby-lcf/mrblib/{lcf,schema}.rb) over real downloaded test-bed projects — walking every schema field of a game'sRPG_RT.ldb,RPG_RT.lmtandMap*.lmuand asserting structural invariants (layer sizes match the map dimensions, move-command ids are in range, events iterate). It auto-discovers games underdata/and now runs in CI after the test-bed download step, exercising the loaders against genuine editor output rather than only the synthetic blobs the unit tests use- Built-in CPU/memory profiler for finding performance bottlenecks, enabled with
--profile(report cadence tunable via--profile_interval_ms, default 1000ms). Once a second it prints a summary line to stderr with the measured frame rate, per-frame CPU work time (the frame span minus the fps-cap sleep) and a breakdown of named sub-sections —scene.update,input.updateand thegfx.*phases ofGraphics.update(z-ordering, bitmap invalidation, LVGL handling) — each with its average/max time and share of the frame, plus memory use: process RSS, the LVGL heap pool (used bytes + fragmentation) and mruby allocation activity (live blocks and allocations/sec). Custom sections can be timed from Ruby withRGSS::Profiler.section("name") { ... }, andRGSS::Profiler.statsreturns the current interval as a Hash. When--profileis off the profiler does no work — every timing/sampling entry point returns on a single predicted branch — so the default build is unaffected. A Chrome trace can be exported with--profile_trace=FILE(orRGSS::Profiler.trace_start/trace_stopfrom Ruby to trace a specific window): every frame and section is streamed as a Chrome Trace Event and each memory sample as counters, producing a file thatchrome://tracingand Perfetto load as a flame chart with memory graphs. The stream is written incrementally and stays loadable even if the process is killed mid-run - Keyboard input now works in the SDL window backend: an SDL event watch (added
in
src/sdl_input.cxx) observes key events without stealing them from LVGL's own event pump, translates them toRGSS::Inputkey ids (arrows; Z/Enter/Space = C, X/Esc = B, C = A; A/S/D = X/Y/Z; Q/PgUp = L, W/PgDn = R; Shift/Ctrl/Alt and F5–F9), and feeds them intoRGSS::InputfromGraphics.update, mirroring the existing terminal backend. PreviouslyRGSS::Input.updatewas a stub and games were only controllable under the--sixel/--itermterminal backends RGSS::Viewportis now a functional display object rather than a stub: it wraps an LVGL container that positions and clips its sprites to arect, scrolls their content byox/oy, hides viavisible, and takes part inzordering against top-level sprites. Sprites created withSprite.new(viewport)are parented to it, and z ordering is now resolved per LVGL parent so sprites inside a viewport stack among themselves while the viewport stacks on the screen. LVGL delete events invalidate the mruby wrappers so a viewport can safely own (and free) its child spritesRGSS::Sprite/RGSS::Viewportgained avisible/visible=accessor- On-screen log console for the
--sixel/--itermterminal backends: a fixed block of rows drawn above the game image (like the control legend and emit-rate stats) that mirrors the engine'sng-logoutput. The last few messages are tailed newest-at-the-bottom and coloured by severity (dim info, yellow warnings, red errors); rows are truncated to the terminal width so a long line cannot wrap and shift the image. On by default, disabled with--noterm_console, and sized with--term_console_lines=N(default 5). While a terminal backend is active,ng-log's ownstderroutput is suppressed (down toFATAL) so messages appear only in the console instead of scribbling over the picture. The executable installs annglog::LogSink(src/log_console.cxx) that feeds the buffer through the sharedterminal.cxx, so themruby-rgssgem keeps no compile-time dependency onng-log. Documented indocs/adr/0005-terminal-log-console.md
Fixed
- LCF map-tree
scrollbar_x/scrollbar_y(chunks 5/6) are now read as signed ints instead of booleans; real games store multi-byte scrollbar positions there, which raisedinvalid bool sizewhen parsing an actualRPG_RT.lmt
Changed
- The RPG2000 runtime no longer silences errors it recovers from. The
rescueguards added for event movement — asset loads (chipset/charset), optional event-page field reads, common-event and move-route parsing, event-SE/BGM playback, per-event stepping, actor-name lookup and the save-slot check — previously swallowed their exception and returned a fallback with no trace. They now log the failure to$stderr(tagged[RPG2k]/[RGSS], with the underlying message) before falling back, matching the logging the rest of the runtime already does, so a missing asset or malformed field is visible instead of silently degrading. The optional page-field reads share a singlepage_fieldguard.AGENTS.mdgains an "Error Handling" note recording the don't-silence-errors expectation. - Bumped the vendored mruby submodule to the 4.0.0 release (from a 3.3.0-era
snapshot).
build_config.rbdrops themruby-printgem (removed in 4.0;Kernel#p/#printare now in the core andmruby-iosupplies#print/#puts/#printf) and thedisable_presymcalls (presym is always enabled in 4.0, and bytecode serializes symbols by name so the hostmrbcand the emscripten target stay compatible).CMakeLists.txtalso exposes mruby's generated build-tree include dir on themrubytarget, sincemruby.hnow unconditionally pulls in the generatedmruby/presym/id.h. mruby 4.0 also removed per-state allocators (mrb_open_allocf), so the native build now overrides the globalmrb_basic_alloc_functo share lvgl's heap pool, and the profiler's allocation hook moved to the matching(ptr, size)signature. Themruby-bigintgem is now enabled: mruby 4.0's compiler encodes integer literals wider than 32 bits (e.g. the0xFFFFFFFFmasks in the LCF codecs) as bignum literals that need it at runtime. Themruby-marshalsubmodule is bumped to pick up its 4.0 fix (Marshal.dumpis registered with its real arity so mruby 4.0's stricter argument-count check accepts the optional port/limit arguments) - Bumped the bundled LVGL to v9.5.0 (from a v9.2.0 development snapshot)
RGSS::Windowis now assembled from three layered sprites inside a viewport (windowskin background+frame, selection cursor, and contents/text) instead of compositing everything into one sprite's bitmap. The viewport clips the layers to the window rectangle, and updating the cursor or text no longer re-blits the windowskin- Playable gameplay after "New Game":
RPG2k#start_new_gamebuilds the party (Game::Party/Game::Actor) from the database, reads the start position from the map tree, loads the starting map and enters a walkableScene::Map. The map scene renders the lower/upper tile layers (as colour blocks derived from tile ids, pending real chipset blitting), draws the party leader from theirCharSetgraphic, and supports grid movement with pixel interpolation, walk animation, edge/tile/event collision (chipsetpassable_data_lower) and an edge-clamped follow camera - Event system:
Game::Interpreterruns a decoded RPG2000 command list — Show Message/Choices, Control Switches/Variables, Change Gold/Items/Party, Conditional Branch/Else/End, Loop/Break, Label/Jump, Timer, Teleport, Wait, Play BGM/SE and End Event — backed by globalGame::Switches/Game::Variablesand party inventory.Game::EventPageselects an event's active page from switch/variable/item/actor conditions, andGame::CommonEventruns auto-start and parallel common events. Action-button and auto-start events trigger on the map, a message/choice window renders over it (with\v[n]/\n[n]/\\control codes expanded), and Teleport transfers between maps - Main menu (
Scene::Menu), opened over the map with the cancel button: shows party status with Save and End Game (item/skill/equip/status are placeholders) - Save & Continue: the game state (position, switches, variables, party,
inventory, gold, hp/mp, timer) serialises to a portable
Marshalsave (Game::State#to_h/State.load), written by the menu's Save command and reloaded from the title's "Continue" - Decoding for the LCF
:event(event command list,LCF::EventCommand),:int8_arrayand:int32_arrayschema types, andArray2D#eachfor walking sparse event/actor tables - Filled in the LCF database schemas (
mruby-lcf/mrblib/schema.rb) from the VIPRPG 200X analysis notes: skills, items, enemies, enemy groups, terrain, attributes, states, battle animations, classes, and the full terminology and system sections, plus switches/variables- Documented the decision in
docs/adr/0002-lcf-database-schema-fields.md
- Documented the decision in
- Honour
RPG_RT.ini's[RPG_RT] FullPackageFlag: when set to1the game is treated as a self-contained ("full") package and RTP lookups are disabled by clearingRTP_DIR, so bitmaps are resolved only from the game directory - Populated the LCF map, map tree and save data schemas from the VIPRPG 200X
analysis notes (
mruby-lcf/mrblib/schema.rb):MAP_UNIT(LcfMapUnit) — chipset/size, scroll and parallax settings, the lower/upper tile layers and the full map event tree (events → pages → appearance condition and move route)SAVE_DATA(LcfSaveData) — save title (file-select screen fields), the system block (scene, switches/variables, message/face settings, BGM/SE overrides, transitions, permissions) and hero/vehicle positions- Map tree: fixed the area-rect field (was a typo
type: :Araa), renamed the editor-only chunk 3 toindentationand gave encounters an enemy-group default; the reader now loads all three map-tree sections (properties, tree order and initial positions) instead of only the first
- Verified by parsing the bundled Nepheshel
RPG_RT.lmtand all 543Map*.lmufiles across both game variants (tile layers matchwidth * height). - Terminal gaming support using the DEC sixel graphics protocol
- Added
--sixelflag to render frames to a sixel-capable terminal instead of opening an SDL window, plus--sixel_scalefor integer upscaling - Implemented a windowless LVGL display backend with a sixel flush callback
and a monotonic-clock tick/delay source (
mruby-rgss/src/sixel.cxx) - Wired terminal keyboard input (arrows/WASD, confirm, cancel, quit) into the
RGSS::Input module via
Graphics.update - Switched to the terminal's alternate screen buffer while a game is running so it no longer scribbles sixels over the shell history; on exit (including fatal signals) the pre-game screen contents and cursor are restored
- Documented the decision in
docs/adr/0001-terminal-gaming-sixel.md
- Added
- Terminal gaming support using iTerm2's inline-image protocol
- Added
--itermflag to render frames to the terminal via the OSC 1337File=sequence, plus--iterm_scalefor integer upscaling. Each frame is PNG-encoded (via stb_image_write) and emitted base64-encoded, so it renders in terminals that don't support sixel — including VS Code's integrated terminal, iTerm2 and WezTerm - Refactored the windowless terminal backend so both the sixel and iTerm2
encoders share one core (
mruby-rgss/src/terminal.cxx: raw-mode input, monotonic tick/delay source, alternate-screen handling); each protocol is now just a frame encoder (sixel.cxx/iterm.cxx) - Documented the decision in
docs/adr/0003-terminal-gaming-iterm2.md
- Added
- Added a
--term_statsflag (on by default) that draws the terminal emit rate — frame size, MB/s and fps — on-screen just under the control legend, refreshed about once a second while a terminal backend is active, so the real per-frame byte cost is visible against the estimated bandwidth. Disable with--noterm_stats; the counter lives in the sharedterminal.cxx, so both the sixel and iTerm2 backends report it - Expanded the
mruby-rgssRGSS built-in class library:RGSS::Color— floating point RGBA color with clamping,set,==,to_sand Marshal (_dump/_load) supportRGSS::Tone— red/green/blue/gray tone with clamping,set,==,to_sand Marshal supportRGSS::Table— 1/2/3 dimensional 16bit integer array ([],[]=,xsize/ysize/zsize/dim,resize) with the RGSS Marshal format, used for map/tile dataRGSS::Bitmap— addedwidth,height,rect,clear,fill_rect,get_pixel,set_pixel,blt(with opacity alpha-blending) and font-aware, alignment-awaredraw_textRGSS::Rect— added==,to_s, Marshal support and fixed the instance type soRect.newno longer trips the DATA assertionRGSS::Font— name/size/bold/italic/color plus class-level defaults andFont.exist?;Bitmap#font/Bitmap#font=accessorsRGSS::Audio— inert stubs for the standard bgm/bgs/me/se API that warn (once per method) to stderr when calledRGSS::Graphics—frame_count(advanced byupdate),frame_rate, andfreeze/transition/frame_resetstubs that warn (once per method) to stderr when called- Tests covering the new value classes, bitmap pixel operations and Marshal round-trips
- Title screen menu implementation
- Added window component to display menu items (New Game, Continue, Shutdown)
- Implemented menu navigation with keyboard input (up/down arrows)
- Added selection highlighting with cursor
- Implemented basic menu item selection functionality
- RPG Maker 2000 style windows
RGSS::Windownow renders a real windowskin: a stretched 32x32 background tile plus the 8px frame border (corners and edges) taken from the System graphic, with the selection cursor and contents composited on top- Falls back to a plain dark panel with a light border when no windowskin can be loaded
- Parsed the System graphic name (
system_graphic, LDB chunk 19) from the database and used it as the title menu windowskin - Sized and centred the title menu window to fit its labels, matching the reference title layout
- Added
RGSS::Bitmap#stretch_blt(nearest-neighbour scaled blit with opacity alpha-blending) plus a test
- Input module implementation
- Added key constants (UP, DOWN, LEFT, RIGHT, A, B, C, etc.)
- Implemented input state tracking (press, trigger, repeat)
- Added directional input helpers (dir4, dir8)
- GitHub-hosted test-bed download scripts reachable from sandboxed/proxied
environments where the tkool CDN is blocked (unlike the existing
Nepheshel/Pray-for-You scripts):
scripts/download-mtf-meido-action.bash— RPG Maker 2000 game (data/mtf-meido-action/Debug:RPG_RT.ldb/.lmtand 13Map*.lmu)scripts/download-opengame-xp.bash— RPG Maker XP project (data/OpenGame.exe/Testbed/XP: fullData/*.rxdataset)- Both use a sparse, blob-filtered
git clone; the CIbuildjob now fetches them alongside Nepheshel and keys the game cache off all download scripts
Fixed
- Windowskin (and other graphic) loading now works for PNGs whose
IDATdeflate stream references data before the start of the output. The PNG/zlib spec forbids this, so stb_image (and zlib itself) reject such files withbad dist/ "invalid distance too far back" -- but the producers, including RPG Maker System graphics, rely on the missing pre-history reading as zeros. Added a self-contained tolerant PNG decoder (lenient inflate + scanline unfiltering + palette/grayscale/truecolour expansion) inbmp_init_filethat runs only as a fallback after stb_image refuses a file, so those windowskins load instead of dropping to the plain panel - Windowskin loading now works for games whose System graphic is stored in RPG
Maker's native XYZ format.
RGSS::Bitmapalready searched for.xyzfiles, but stb_image could not decode them, so an XYZ windowskin silently fell back to the plain panel. Added an XYZ decoder ("XYZ1"header + zlib-compressed palette and indices) tobmp_init_filethat also honours the transparent colour-key flag. If the standard zlib stream is rejected, the decoder retries the payload as raw DEFLATE (no zlib header) before giving up RGSS::Bitmapload failures now report the decoder's own reason instead of a bare "Failed to init bitmap". The XYZ decoder records a detailed diagnostic (dimensions, zlib header bytes, compressed/expected sizes and stb's error such asbad dist), exposed viaBitmap._load_error/Bitmap._stbi_errorand included in the raised message; windowskin fallbacks log it to stderr rather than swallowing it silently. Bitmap's retry lookups also now forward the transparent-colour flag to every candidate path, not just the first- LCF
File#method_missingdelegates field access with__send__instead ofsend, so parsed fields (db.player,map_tree.initial, ...) resolve on mruby builds whereKernel#sendis not exposed on objects that define their ownmethod_missing - LCF reader (
mruby-lcf/mrblib/lcf.rb): booleans are read as their real 1-byte encoding (previously required a 0-byte chunk and always returnedtrue); nestedArray2D/Array1Dsections wrap aStringinStringIOcorrectly (the oldkind_of? IOcheck failed forStringIO); added the:int16_arrayelement type used by the tile layers and the area rect.
Changed
- Bumped the bundled LVGL to v9.5.0 (from a v9.2.0 development snapshot)
RGSS::Windowis now assembled from three layered sprites inside a viewport (windowskin background+frame, selection cursor, and contents/text) instead of compositing everything into one sprite's bitmap. The viewport clips the layers to the window rectangle, and updating the cursor or text no longer re-blits the windowskin- Updated documentation to reflect new title screen functionality
- Marked "Show window component for title scene" as completed in TODO list
- Enhanced database term schema with comprehensive RPG Maker 2000/2003 terms:
- Added battle menu command terms
- Added save/load related terms
- Added status and equipment terms
- Fixed shutdown term ID (changed from 116 to 117)