NEVEL-98 Play

NEVEL-98 Manual

New to NEVEL-98? Open the machine in your browser, type demo and then run.

Open NEVEL-98 Play the demo

NEVEL-98 is a fantasy computer for visual novels. It imagines a 1990s Japanese personal computer in the style of the NEC PC-9801: a 640×400 screen, 16 colors from a palette of 4096, FM sound, and a Lua interpreter with a visual novel engine built in.

This manual covers the machine, the console, every API function, the editors, and the cartridge format.


1. The machine

Part Specification
Screen 640×400 pixels, 60 frames per second
Colors 16 on screen at once, each one of 4096 (12-bit 0xRGB)
Palettes one cart palette, plus an optional own palette per image
Fonts 8×16 (80×25 text, like PC-98 text mode) and 6×12; Latin-1, arrows, box drawing, ♥ ♪ etc.
Sound 6 channels; each plays a 4-operator FM voice, an SSG square wave or noise
Instruments 32
Patterns 64, each 32 rows × 6 channels
Songs 8, each an order list of up to 64 patterns with a loop point
Images 64 per cart, up to 640×400, 4 bits per pixel, optional transparent color
Code Lua 5.4, up to 512 KB of source

Lua's io, os, debug and require are not available; carts can only touch the machine through the API below. math.random is replaced by a deterministic generator so that saved games replay exactly.


2. Getting around

The machine boots into the console. From there:

Key Action
Esc switch between the console and the last editor
F1 / F2 / F3 / F4 / F5 code, image, text, sound and music editors
F6 or Ctrl+R run the cart
Ctrl+S save the cart under its current name
F7 while a cart runs: capture the screen as the cart's label
F9 4:3 aspect (PC-98 monitors stretched the 640×400 picture)
F10 scanlines
F11 or Alt+Enter fullscreen

While a cart is running, Esc opens the game's own system menu (save, load, log, settings) and F1–F5 stop the cart and return to the editors. Runtime errors stop the cart and print the message in the console; press F1 to open the code at the line that failed.

The mouse works everywhere: click the tabs in the top bar, the function key labels at the bottom of the console, and every button. Over the machine's screen the pointer becomes a PC-98 style arrow; while a cart runs it hides itself after three seconds without movement, and a cart can hide it for good with cursor(false).


3. Console commands

Commands are not case sensitive. File names without an extension are looked up as .n98, then .n98.png. Tab completes file names, Up/Down recall previous commands.

Files

Command
ls [folder] list files: carts in green, floppy images in yellow, pictures in magenta
cd <folder> / cd .. / cd change folder
mkdir <name> create a folder
rm <file> delete a file
folder show (and on desktop open) the drive folder

Carts

Command
new start an empty cart
load <cart> load a .n98 or .n98.png cart
save [name] save as name.n98 (or under the current name)
run run the cart
demo load the demo novel
info sizes, counts and where the drive is
title <text> / author <text> set the cart's title and author

Commands that would discard unsaved work ask you to repeat them.

Media

Command
import <picture> [name] [options] convert a PNG, JPG, BMP or GIF into a 16-color image
images list the images in the cart
rmimg <name> remove an image
importfont <sheet.png> install a custom 8×16 font in the cart
font reset go back to the built-in font

import options:

Option
-o ordered (Bayer) dithering, the PC-98 look; the default
-f error-diffusion (Floyd–Steinberg) dithering
-n no dithering, nearest color
-p give the image its own palette, computed from the picture
-c compute a new palette from the picture and make it the cart palette
-s WxH shrink to fit within W×H (pictures larger than 640×400 always shrink)

Transparent pixels in the source picture become color 0, which is set as the image's transparent color. A common workflow is to build a palette from a collage of all your art (import collage.png pal -c, then rmimg pal) and import every picture against it.

Text tables

Command
importtext <file.csv> merge a CSV into the text tables (first row keys,en,it,...)
exporttext <file.csv> write the text tables as CSV, ready for a spreadsheet or a translator

Sharing

Command
export name.n98 write the cart as text
export name.png write a floppy-disk image (name.n98.png) that contains the whole cart
export name.html write a standalone web player, one file with everything inside

In the browser version, exports are downloaded; on the desktop they are written to the drive.

System

cls, keys (shortcut reference), help, reboot, shutdown.


4. Writing a visual novel

A cart's code runs once when the cart starts. Usually it only defines scenes; the engine then starts the scene named "title" if there is one, otherwise "start".

speaker("Rei", 11)                         -- name color for Rei

scene("title", function()
  title_screen{ bg = "station", music = 0 }
  jump("start")
end)

scene("start", function()
  bg("station", "dither")
  show("rei", "center")
  say("Rei", "You missed it too?")
  local c = choice{ "Who are you?", "Missed what?" }
  if c == 2 then flag.honest = true end
  jump("ending")
end)

scene("ending", function()
  say("The board flickers and goes dark.")
  the_end("NEXT YEAR")
end)

Each function call below waits for as long as it needs to: say waits for the player to read the line, bg with a transition waits until the transition ends, choice waits for an answer. You write the story top to bottom.

If you don't define any scenes, the top-level code itself is the story.

Texts and languages

Write your lines in the TEXT editor (F3) instead of in the code. Each row has a key and one string per language, like Godot's CSV translations:

key en it
char.rei Rei Rei
rei.hello Hi, {1}! Ciao, {1}!

In the code, tr("rei.hello", "Taro") returns "Hi, Taro!" (or "Ciao, Taro!" in Italian). {1}, {2}... are replaced by the extra arguments. A missing key comes back unchanged, so you can see what's left to write; a missing translation falls back to the first language.

Every visual novel function also accepts keys directly: a string that starts with @ is looked up in the table.

speaker("@char.rei", 7)
say("@char.rei", "@rei.hello")
if choice{ "@c.yes", "@c.no" } == 1 then ... end
the_end("@end.good")
Function
tr(key, ...) the string for key in the current language
hastr(key) whether the key exists
lang() / lang("it") the current language code / switch language (returns false if it doesn't exist)
langs() the list of language codes, e.g. {"en", "it"}

When a cart has more than one language, the Settings menu gets a Language row and the player's choice is remembered. The first language is the source: the editor shows it next to the language you are translating.

The engine's own texts (title screen, system menu, settings, "THE END") can be translated too, by adding keys that start with ui.: ui.new_game, ui.continue, ui.load, ui.options, ui.quit, ui.save, ui.log, ui.auto_on, ui.auto_off, ui.skip, ui.settings, ui.title, ui.quit_to_editor, ui.close, ui.system, ui.save_game, ui.load_game, ui.empty, ui.backlog, ui.settings_title, ui.instant, ui.text_speed, ui.auto_delay, ui.music_volume, ui.sound_volume, ui.language, ui.lang_name (the name of each language, shown in Settings), ui.back, ui.return_to_title, ui.yes, ui.no, ui.the_end, ui.nothing_to_save, ui.saved_slot and ui.slot_empty (both use %d for the slot number). The demo cart translates all of them into Italian.

Scenes and flow

Function
scene(name, fn) define a scene
jump(name) leave the current scene and start another
the_end([text]) show an ending card ("THE END" by default), then return to the title

A scene that finishes without jumping ends the game with the_end().

Pictures

Function
bg(image [, transition [, seconds]]) set the background; clears characters. image may also be a color number or nil for black
cg(...) same as bg, for event pictures
show(image [, pos [, transition [, seconds]]]) show a character. pos is "left", "center", "right", "farleft", "farright" or an x coordinate; showing another image at the same position replaces it (use this for expressions)
hide([pos or image [, transition [, seconds]]]) remove a character, or all of them
effect(name, fn) register a named effect: fn(t) draws on top of the scene every frame
effect(name) / effect(nil) turn an effect on / off (saved with the game)

Characters are drawn bottom-aligned to the screen. If a background image has its own palette, the palette is switched with it.

Transitions: "dither" (ordered dissolve), "wipe" (left to right), "down" (top to bottom), "blinds", "curtain" (from the center), "mosaic", and "fade" (through black).

Text

Function
say(who, text) a line of dialogue with a name box
say(text) / narrate(text) narration
speaker(name, color) color for a speaker's name
window{...} style the message window (see below)
textspeed(cps) characters per second, 0 = instant
hide_window() / show_window() hide the message window (it reappears on the next say)

Text wraps automatically. Inline tags:

Tag
{c:N}...{/c} color N, then back to normal
{w:0.5} pause for half a second
{speed:20} change the typing speed for the rest of the line
{n} or a newline line break

window fields: x, y, w, h (default 16, 290, 608, 100), bg, fg, border (colors; by default the darkest, brightest and a middle color of the current palette), style ("solid" or "mesh" for a see-through dithered window), lines (default 4), blip (a pattern played as a typing sound).

Framed layouts (the PC-98 look)

Most PC-98 adventures show the scene in a window inside an ornate frame, with side panels for the date, a clock or the command menu, and the text in a console below. layout sets this up:

layout("game", {
  view = { 16, 16, 448, 264 },          -- where the scene is drawn
  frame = "frame",                      -- full-screen image drawn on top; its transparent color shows the scene
  hud = function(t) ... end,            -- draws panels (clock, date...) every frame, after the frame
  menu = { 486, 156, 130 },             -- menu{} appears here, as a list without a box
  menu_step = 21, menu_text_dx = 20,    -- row height and text offset (match your button art)
  menu_bar = 3, menu_bar_dx = 15,       -- color and offset of the bar behind the selected entry
  menu_sel = 6, menu_idle = 8,          -- colors for the selected / other entries
  choices = "window",                   -- choice{} is listed inside the text window
  window = { x = 16, y = 298, w = 608, h = 92, style = "none",
             name_style = "plate", name_x = 46, name_y = 288 },
})

scene("start", function()
  layout("game")                        -- switch to it (saved with the game)
  ...
end)

layout(name, def) defines a layout, layout(name) switches to it and layout(nil) goes back to full screen. Inside a layout, backgrounds are centered in the view, characters stand on the view's bottom edge, positions such as "right" are relative to the view, and effects are clipped to it. The layout's window entries override window{}.

Extra window fields: style = "none" draws no box (use it when the frame image already has one); name_style = "plate" prints the speaker's name at name_x, name_y (over a name plate in your frame art), "inline" writes it at the start of the text, and the default draws a small name box; name_format formats inline names; cursor colors the ▼ prompt.

The demo cart shows a complete framed layout, with a red LED clock drawn by its hud function.

Choices

Function
choice{ "A", "B", ... } → index a centered choice box
choice("Question?", { ... }) with a question above the options
menu{ "Look", "Talk", "Move" } → index a PC-98 adventure-style command menu in the top-right corner
interact(update, draw) → value a custom interactive moment: update() runs every frame until it returns a value; draw() draws on top

A false entry hides an option while keeping the numbering: choice{ "Leave", flag.key and "Open the door" }.

Timing and effects

Function
wait(seconds) pause
waitclick() wait for the player
fadeout([seconds [, rgb]]) / fadein([seconds]) palette fade to a color (black by default) and back
flash([rgb [, seconds]]) a quick flash (white by default)
quake([seconds [, strength]]) shake the screen
music(n [, fade_seconds]) / music(nil [, fade_seconds]) play / stop a song
sfx(pattern [, speed]) play a pattern once as a sound effect

State and saving

Name
flag a table for story state; saved with the game
persist a table shared by every playthrough (endings seen, unlocks); kept automatically
remember() write persist to disk now
has_saves() whether any save slot is used
title_screen{...} a ready-made title screen with New Game / Continue / Load / Quit

title_screen options: bg, color, music, title (use \n for several lines), subtitle, scale (title size, default 3), title_x, title_y, menu_x, menu_y (centers and tops), title_color, text_color.

The player can save and load from the system menu at any time. A save records the scene, the flags as they were when the scene began, and every choice made since; loading replays the scene instantly up to the saved line. Two rules keep this exact:

  1. Keep story state in flag (and persist), not in other global variables.
  2. Use rnd/irnd (or math.random) for randomness: the generator is saved too.

The system menu

Esc or a right click opens it: Save, Load, Log (backlog; the mouse wheel also opens it), Auto, Skip, Settings (text speed, auto delay, music and sound volume), Title, and Quit. Holding Ctrl skips text; A toggles auto mode.


5. Machine API

All drawing happens in the cart's 16 colors (0–15). Coordinates are in pixels, (0, 0) is the top-left corner.

Drawing

Function
cls([c]) clear the screen
pset(x, y, c) / pget(x, y) set / read a pixel
line(x0, y0, x1, y1, c) line
rect(x, y, w, h, c) / rectfill(x, y, w, h, c) rectangle outline / filled
circ(x, y, r, c) / circfill(x, y, r, c) circle
oval(x0, y0, x1, y1, c [, fill]) ellipse inside a box
tri(x0, y0, x1, y1, x2, y2, c) filled triangle
print(text, x, y, c [, small]) → width text (8×16, or 6×12 when small)
printx(text, x, y, c [, scale [, small]]) → width scaled text
textw(text [, small]) → width width of text in pixels
fillp([pattern [, c2]]) 4×4 dither pattern for all drawing; bits set use c2, or stay transparent
clip([x, y, w, h]) restrict drawing to a rectangle; no arguments to reset
drawbox(x, y, w, h, bg, border [, mesh]) a PC-98 style double-bordered window

DITHER holds ready-made patterns: solid, d12, d25, d50, d75, d87, hlines, vlines, diag. For example fillp(DITHER.d50) then rectfill(...) draws a checkerboard mesh, the classic PC-98 shading.

Images

Function
img(name, x, y [, flip [, scale]]) draw an image; flip 1 = horizontal, 2 = vertical, 3 = both
imgpart(name, x, y, sx, sy, sw, sh [, flip [, scale]]) draw part of an image
imgsize(name) → w, h size, or nothing if missing
imgexists(name) whether an image exists
imgpget(name, x, y) → c read an image pixel (-1 outside)
imgpal(name) → bool switch to the image's own palette, if it has one

Palette and screen

Function
pal(i, 0xRGB) / pal(i) set / read a palette entry
pal({ [0] = 0x000, ... }) set several entries
pal() restore the cart palette
fade(amount [, 0xRGB]) fade the whole display toward a color (0..1), in 16 steps like a real palette fade
snap() + trans(kind, t) snapshot the screen, then draw a transition from the snapshot to the new frame (t from 0 to 1)
shake(dx, dy) offset the display

Input

Function
btn(b) / btnp(b) held / pressed this frame (btnp repeats when held). Buttons: "a" (Z, Enter, Space, left click), "b" (X, Backspace), "menu" (Esc, right click), "up", "down", "left", "right", "skip" (Ctrl)
key(name) / keyp(name) any key: "a"…"z", "0"…"9", "space", "return", "escape", "up", "shift", "ctrl"…
mouse() → x, y, buttons mouse position and buttons (1 left, 2 right, 4 middle)
mousep([b]) → bool button pressed this frame
wheel() → dy wheel movement this frame
textin() → string characters typed this frame (for name entry)

Sound

Function
music(n [, fade_seconds]) / music(nil) play / stop a song
sfx(pattern [, speed]) play a pattern once; it takes over the channels it uses
note(channel, note [, instrument [, volume]]) / note(channel) play / release a single note (1 = C-0, 58 = A-4)
volume(music, sfx) volumes from 0 to 1

Data and system

Function
dsave(key, string) / dload(key) → string / ddelete(key) per-cart storage
serialize(value) → string / deserialize(string) → value tables to text and back
deepcopy(table)
time() seconds since the cart started
frame() frame counter
log(...) print to the console (and the terminal)
cartinfo() → { title, author, editor, web, version }
restart() restart the cart
label() capture the screen as the cart label

Math helpers

flr, ceil, abs, sgn, mid(a, b, c), clamp(x, lo, hi), lerp(a, b, t), sin(t) / cos(t) (t in turns), rnd([x or table]), irnd(a, b), srand(seed).

Game mode

A cart that defines _draw (and optionally _init and _update) and no scenes runs as a plain game loop, like PICO-8: _init() once, then _update() and _draw() 60 times per second.

local x = 320
function _update()
  if btn("left") then x = x - 2 end
  if btn("right") then x = x + 2 end
end
function _draw()
  cls(0)
  circfill(x, 200, 20, 12)
  print("arrows to move", 8, 8, 15)
end

6. The editors

Code (F1)

A Lua editor with syntax highlighting (keywords, API names, strings, numbers, comments).

Key
Ctrl+Z / Ctrl+Y undo / redo
Ctrl+C / Ctrl+X / Ctrl+V copy, cut, paste (the whole line when nothing is selected)
Ctrl+A select all
Ctrl+D duplicate line
Ctrl+/ comment / uncomment lines
Ctrl+F find (Enter for the next match)
Ctrl+G go to line
Tab / Shift+Tab indent / unindent (selection too)
Ctrl+←/→ jump by words; Ctrl+Home/End: start/end of the code

Shift extends the selection; the mouse selects by dragging.

Image (F2)

The image list is on the left; NEW, DEL, NAME, SIZE, KEY (make the secondary color transparent), PAL (toggle the image's own palette), IMPORT PNG and FIT are below it.

Key Tool
B pen
L line
R / T rectangle / filled box
O / U oval / filled disc
G fill
I (or Alt+click) pick a color
S select a rectangle

Left click draws with the primary color, right click with the secondary color. D cycles the dither pattern: patterns mix the primary and secondary colors, so a 50% pattern with blue and black gives the classic PC-98 shading. X swaps the two colors, [ ] change the brush size, Shift constrains shapes to squares and circles.

The mouse wheel zooms around the cursor; drag with the middle button (or Space + left button) to pan; the arrow keys pan too; F fits the image. Ctrl+C / Ctrl+X copy or cut the selection (or the whole image), Ctrl+V pastes it as a stamp under the mouse (click to place, H flips it, Shift+click places several), Delete clears the selection. Ctrl+Z / Ctrl+Y undo and redo. PgUp/PgDn select the previous/next image.

The palette strip shows the 16 colors: left click picks the primary, right click the secondary. The R G B steppers edit the primary color; the change applies to the image's own palette if it has one, otherwise to the cart palette.

Text (F3)

A table of keys and their strings, one column per language. The yellow-framed language button is the source language; click another one to choose the language shown in the second column.

Key
↑ ↓ ← → / Tab move between rows and columns
Enter (or start typing) edit the selected cell; Enter saves, Shift+Enter adds a line break, Esc cancels
Ctrl+N new key, after the selected one (it suggests the same prefix, e.g. rei.)
Ctrl+F filter rows by key or text
Ctrl+↑ / Ctrl+↓ move the selected row
Ctrl+C / Ctrl+V copy / paste a cell

The buttons on the top right add, delete (click twice), rename and find keys; + and - add and remove languages. Missing translations are shown in red. Keys may use letters, digits, ., _ and -; dots are a good way to group them (rei.hello, menu.look, end.home). The panel at the bottom shows the whole text of the selected cell.

Sound (F4)

Edit the 32 instruments. Each one is:

Operator parameters: AR attack rate, DR decay rate, SR sustain decay rate (0 holds the sustain level), RR release rate, SL sustain level, TL total level (attenuation, so higher is quieter), MUL frequency multiple (0 means ×0.5), DT detune. Click a value and drag up or down, or use the wheel, or the arrow keys with [ ]. VOL is the instrument's volume, VIB and SPD a vibrato.

Play the instrument with the keyboard (Z S X D C V G B H N J M for one octave, Q 2 W 3 E R 5 T 6 Y 7 U for the next), or click the piano; PgUp/PgDn change the octave. PRESET cycles through starting points (e-piano, strings, bass, bell, lead, SSG square, hi-hat, kick, snare, sine). Ctrl+C / Ctrl+V copy instruments.

Music (F5)

A tracker with 6 channels. Each cell is note instrument volume, for example C-4 01 F.

Key
Z…M, Q…U enter a note in the current octave with the current instrument
1 or ` note off (===)
0–9, A–F instrument and volume digits (move the cursor into those columns)
Delete clear the field under the cursor
Backspace / Enter pull the channel up / push it down by one row
Space play / stop the pattern (looping)
Shift+Space play the song
, and . octave down / up
- and = previous / next instrument
Tab / Shift+Tab next / previous channel
Ctrl+↑/↓ (Shift for octaves) transpose the channel
Ctrl+C / Ctrl+X / Ctrl+V copy / cut / paste the pattern

The left panel picks the song, its speed (frames per row at 60 frames per second; 6 means 10 rows per second) and loop point, and edits the order list: ADD inserts the current pattern after the selected slot, REMOVE deletes the slot, − and + change the slot's pattern. STEP is how many rows the cursor moves after each entry.

sfx(n) plays pattern n once and borrows only the channels the pattern uses, so put sound effects on channel 6 to keep the music going.


7. Cartridge format

A .n98 cart is UTF-8 text, made of sections:

-- nevel-98 cartridge v1
__meta__
title=Last Train Home
author=Someone
__lua__
...code...
__pal__
112 612 435 ... (16 entries of 3 hex digits, 0xRGB)
__img__ <name> <width> <height> <key> <has_palette>
[palette line, if has_palette is 1]
z<base64 of zlib-compressed 4-bit pixels, two per byte, high nibble first>
 <continuation lines start with a space>
__ins__
<index> <name> <type> <alg> <fb> <duty> <vib_depth> <vib_speed> <volume> | <AR DR SR RR SL TL MUL DT> | ... (4 operators)
__pat__ <index>
C-4 01 F  --- .. .  ...   (32 rows, 6 cells per row)
__song__ <index> <speed> <loop> <length> <pattern> <pattern> ...
__font__
z<base64 zlib of 256 glyphs × 16 rows>
__text__ en it
<key>\t<english>\t<italian>   (\n, \t and \\ are escaped)
__label__
<palette line>
z<base64 zlib of the 640×400 label>
__end__

Instruments that are unchanged from their defaults, empty patterns and empty songs are omitted.

A .n98.png floppy is an ordinary PNG picture of a 3.5" disk showing the cart label; the full cart text is stored, zlib-compressed, in a private nvCt chunk. Image hosts that re-encode pictures will strip it, so share floppies as files.

An exported .html player contains the WebAssembly machine and the zlib-compressed cart, base64-encoded, in one page.

Custom fonts

importfont reads a picture of a 16×16 grid of 8×16 glyph cells (128×256 pixels, or any multiple) covering character codes 0–255 in Latin-1 order. Light pixels on a dark background (or dark on light) become the glyph. The font replaces the 8×16 font for codes 0–255 while the cart runs; other characters still use the built-in font.


8. Limits and tips


9. Running and building

Playing in the browser

web/dist/index.html is the complete machine (console and editors) in one file; web/dist/demo.html plays the demo novel. Open either one directly, or upload it to any static host such as itch.io. Files you create in the browser are kept in the browser's storage; drop .n98, .n98.png or images on the page to add them.

Building

Desktop (Windows, macOS, Linux)

Requirements: CMake 3.16+, a C11 compiler. SDL3 is used if installed, otherwise CMake downloads it.

cmake -S . -B build
cmake --build build --config Release
./build/nevel98            # or build\Release\nevel98.exe on Windows

Command line:

nevel98                      boot into the console
nevel98 game.n98             boot with a cart loaded
nevel98 --play game.n98      run a cart as a standalone player
nevel98 --demo               play the demo novel
nevel98 --scale 3            start with a 3x window

Carts are stored in the machine's drive, a carts folder inside your user application data folder (on Linux ~/.local/share/nevel98/NEVEL-98/carts). Type folder in the console to see the exact path and open it. You can also drag carts and images onto the window.

Web (WebAssembly)

The browser build uses wasi-sdk (version 25 or newer):

WASI_SDK=/path/to/wasi-sdk ./web/build_web.sh

This writes web/dist/nevel98.wasm, web/dist/index.html and web/dist/demo.html. When web/dist/nevel98.wasm exists, the desktop build bundles it so the export game.html command works offline. A prebuilt copy is included in this repository.

Headless runner

nevel98_headless runs the machine without a window. It is used for automated tests and can script input and save screenshots:

./build/nevel98_headless --play --frames 600 --keys "60:z,120:z" --shot 300:shot.png @demo

Project layout

src/core/        the machine: graphics, FM synth, carts, Lua API, editors, console
src/core/prelude.lua   the visual novel engine, written in Lua
src/platform/    hosts: SDL3 desktop, WebAssembly, headless
web/             browser shell and build script; web/dist has prebuilt files
carts/           the demo cart (.n98 and floppy .n98.png)
assets/          application icon (.ico for Windows, PNG for the window)
tools/           bin2c, mkhtml, the font converter
third_party/     Lua 5.4.7, stb_image, stb_image_write
docs/MANUAL.md   the complete manual

Fonts

The built-in fonts are converted from Spleen (BSD 2-Clause). The original NEC PC-98 font ROM is copyrighted and is not included; if you own one, you can draw or convert its glyphs into a 16×16 sheet of 8×16 cells and install it in a cart with importfont sheet.png.

License

NEVEL-98 is released under the MIT license (see LICENSE). Third-party components keep their own licenses: Lua (MIT), stb (public domain / MIT), Spleen (BSD 2-Clause), SDL3 (zlib). Carts you make are yours.

This page is generated from docs/MANUAL.md and the README of NEVEL-98.