# ============================================================================ # runtime/native/core.ludic — the Ludic runtime, written in Ludic. # # This is what the native backend links instead of a C runtime. Everything a # game touches through a builtin — the framebuffer, rectangles, text, the # registers, the RNG, input, the frame dump — is implemented right here in # Ludic and compiled to machine code by ludicc like any other Ludic source. # # Underneath it there is exactly one layer, and it is not C: the compiler # intrinsics (mem_alloc, peek8/poke8, peek32/poke32, file_open/file_write, # read_byte, …) lower to direct calls into the platform's C ABI — malloc, # fwrite, getchar. That ABI is the operating system's interface, the same floor # Rust and Swift stand on. No C source is compiled at any point in the build. # # A builtin `clear(c)` in a game resolves to `rt_clear(c)` here; that is the # whole protocol. Swap this file and you have swapped the runtime. # # This file is a fragment: ludicc splices it into every native build. It is not # a `game`/`module` block of its own. # ============================================================================ # ---- state ---------------------------------------------------------------- var rt_fb: ptr = ptr_null() # framebuffer, one i32 (0x00RRGGBB) per pixel var rt_fbw: int = 320 var rt_fbh: int = 240 var rt_regs: ptr = ptr_null() # the 64 general-purpose game registers var rt_rng: int = 305419896 # xorshift32 state var rt_alive: int = 1 # platform still running? # 5x7 glyphs for ASCII 32..90, 7 rows per glyph, each row a 5-bit mask stored # biased by '0' so the whole font is one printable string literal. fn rt_font() -> str { return "00000004444404000000000000000000000IJ4:FC000000000000000000000000000000E>O>E0044O4400000448000O000000004012448@@>ACEIA>4<4444>>A168@ON11>11N26:BO22O@N11A>>@@NAA>O124888>AA>AA>>AA?11>04004000000000024842000O0O000842480>A164040000000>AAOAAANAANAAN>A@@@A>LBAAABLO@@L@@OO@@L@@@>A@GAA>AAAOAAA>44444>7222BBAAAAA>NAAN@@@>AAAEB=NAANDBA>A@>1A>O444444AAAAAA>AAAAA:4AAAEEKAAA:4:AAAA:4444O1248@O" } fn rt_init() -> void { rt_fb = mem_alloc(320 * 240 * 4) rt_regs = mem_alloc(64 * 4) mem_set(rt_regs, 0, 64 * 4) rt_map = mem_alloc(96 * 64) mem_set(rt_map, 32, 96 * 64) rt_statusbuf = mem_alloc(96) poke8(rt_statusbuf, 0, 0) rt_image_init() rt_tt_init() rt_ui_init() rt_clear(0) if is_windowed() { win_open(rt_fbw, rt_fbh, 3, game_title()) } } fn rt_shutdown() -> void { if is_windowed() { win_close() return } rt_dump_ppm("out.ppm") } # ---- framebuffer ---------------------------------------------------------- fn rt_clear(c: int) -> void { let n = rt_fbw * rt_fbh for i in 0 .. n { poke32(rt_fb, i, c) } } fn rt_put_px(x: int, y: int, c: int) -> void { if x < 0 { return } if y < 0 { return } if x >= rt_fbw { return } if y >= rt_fbh { return } poke32(rt_fb, y * rt_fbw + x, c) } fn rt_fill_rect(x: int, y: int, w: int, h: int, c: int) -> void { let x0 = max(0, x) let y0 = max(0, y) let x1 = min(rt_fbw, x + w) let y1 = min(rt_fbh, y + h) var j = y0 while j < y1 { let row = j * rt_fbw var i = x0 while i < x1 { poke32(rt_fb, row + i, c) i = i + 1 } j = j + 1 } } fn rt_frame_rect(x: int, y: int, w: int, h: int, c: int) -> void { rt_fill_rect(x, y, w, 1, c) rt_fill_rect(x, y + h - 1, w, 1, c) rt_fill_rect(x, y, 1, h, c) rt_fill_rect(x + w - 1, y, 1, h, c) } # Windowed: hand the framebuffer to the platform layer, which blits it into # the view. Headless: nothing to do until shutdown writes the last frame out. fn rt_present() -> void { if is_windowed() { win_present(rt_fb, rt_fbw, rt_fbh) } } # ---- text ----------------------------------------------------------------- fn rt_glyph(x: int, y: int, ch: int, colour: int, sc: int) -> void { var c = ch if c >= 97 { if c <= 122 { c = c - 32 } } if c < 32 { return } if c > 90 { return } let base = (c - 32) * 7 let font = rt_font() for row in 0 .. 7 { let bits = peek8(font, base + row) - 48 var b = bits for cc in 0 .. 5 { let on = b / 16 if on == 1 { rt_fill_rect(x + cc * sc, y + row * sc, sc, sc, colour) } b = (b - on * 16) * 2 } } } fn rt_text(x: int, y: int, s: str, colour: int, sc: int) -> void { var i = 0 var cx = x var ch = peek8(s, 0) while ch != 0 { rt_glyph(cx, y, ch, colour, sc) cx = cx + 6 * sc i = i + 1 ch = peek8(s, i) } } fn rt_text_int(x: int, y: int, n: int, colour: int, sc: int) -> void { if n == 0 { rt_glyph(x, y, 48, colour, sc) return } var v = n var cx = x if v < 0 { rt_glyph(cx, y, 45, colour, sc) cx = cx + 6 * sc v = 0 - v } var digits = 0 var t = v while t > 0 { digits = digits + 1 t = t / 10 } var p = digits while p > 0 { var div = 1 for k in 1 .. p { div = div * 10 } rt_glyph(cx, y, 48 + (v / div) % 10, colour, sc) cx = cx + 6 * sc p = p - 1 } } # ---- registers ------------------------------------------------------------ fn rt_reg(i: int) -> int { if i < 0 { return 0 } if i >= 64 { return 0 } return peek32(rt_regs, i) } fn rt_set_reg(i: int, v: int) -> void { if i < 0 { return } if i >= 64 { return } poke32(rt_regs, i, v) } # ---- rng (xorshift32) ----------------------------------------------------- fn rt_seed(s: int) -> void { if s == 0 { rt_rng = 305419896 return } rt_rng = s } # xorshift32 (Marsaglia). Runs on the raw 32-bit pattern, so the sign bit is # masked off only when a caller asks for a number. fn rt_next_rand() -> int { var x = rt_rng x = (x ^ (x << 13)) x = (x ^ (x >> 17)) x = (x ^ (x << 5)) rt_rng = x return (x & 2147483647) } fn rt_rng_range(lo: int, hi: int) -> int { if hi <= lo { return lo } return lo + rt_next_rand() % (hi - lo + 1) } fn rt_rng_chance(pct: int) -> bool { return rt_next_rand() % 100 < pct } # ---- platform: input ------------------------------------------------------ fn rt_poll() -> int { if is_windowed() { return win_poll() } let c = read_byte() if c < 0 { rt_alive = 0 return 0 } if c == 113 { # 'q' quits, as in the headless C platform rt_alive = 0 } return c } fn rt_running() -> bool { if is_windowed() { return win_running() } return rt_alive } # ---- writing the frame out ------------------------------------------------ fn rt_put_str(buf: ptr, at: int, s: str) -> int { var i = 0 var n = at var ch = peek8(s, 0) while ch != 0 { poke8(buf, n, ch) n = n + 1 i = i + 1 ch = peek8(s, i) } return n } fn rt_put_int(buf: ptr, at: int, v: int) -> int { if v == 0 { poke8(buf, at, 48) return at + 1 } var digits = 0 var t = v while t > 0 { digits = digits + 1 t = t / 10 } var n = at var p = digits while p > 0 { var div = 1 for k in 1 .. p { div = div * 10 } poke8(buf, n, 48 + (v / div) % 10) n = n + 1 p = p - 1 } return n } fn rt_dump_ppm(path: str) -> void { let f = file_open(path, "wb") if ptr_is_null(f) { return } let hdr = mem_alloc(64) var n = rt_put_str(hdr, 0, "P6\n") n = rt_put_int(hdr, n, rt_fbw) n = rt_put_str(hdr, n, " ") n = rt_put_int(hdr, n, rt_fbh) n = rt_put_str(hdr, n, "\n255\n") file_write(f, hdr, n) let px = rt_fbw * rt_fbh let buf = mem_alloc(px * 3) for i in 0 .. px { let c = peek32(rt_fb, i) poke8(buf, i * 3, (c / 65536) % 256) poke8(buf, i * 3 + 1, (c / 256) % 256) poke8(buf, i * 3 + 2, c % 256) } file_write(f, buf, px * 3) file_close(f) mem_free(buf) mem_free(hdr) } import "inflate.ludic" import "image.ludic" import "truetype.ludic" import "ui.ludic" # ---- tilemap -------------------------------------------------------------- # A character grid the game paints with map_row() and reads with tile(). Stored # as one byte per cell in a flat buffer; out-of-bounds reads answer '#' so a # caller can treat the edge of the world as wall without special-casing it. var rt_map: ptr = ptr_null() var rt_mapw: int = 0 var rt_maph: int = 0 fn rt_map_size(w: int, h: int) -> void { rt_mapw = clamp(w, 0, 96) rt_maph = clamp(h, 0, 64) mem_set(rt_map, 32, 96 * 64) } fn rt_map_row(y: int, s: str) -> void { if y < 0 { return } if y >= 64 { return } var x = 0 var ch = peek8(s, 0) while ch != 0 { if x >= 96 { return } poke8(rt_map, y * 96 + x, ch) x = x + 1 ch = peek8(s, x) } } fn rt_tile(x: int, y: int) -> int { if x < 0 { return 35 } if y < 0 { return 35 } if x >= rt_mapw { return 35 } if y >= rt_maph { return 35 } return peek8(rt_map, y * 96 + x) } # ---- status line ---------------------------------------------------------- # One persistent string of feedback/dialogue, copied into runtime-owned memory # so it survives whatever the caller does with the original. var rt_statusbuf: ptr = ptr_null() fn rt_status(s: str) -> void { var i = 0 var ch = peek8(s, 0) while ch != 0 { if i >= 95 { ch = 0 } if ch != 0 { poke8(rt_statusbuf, i, ch) i = i + 1 ch = peek8(s, i) } } poke8(rt_statusbuf, i, 0) } fn rt_status_text() -> ptr { return rt_statusbuf } # ---- snapshot: the runtime serialises its own half ------------------------ # The compiler writes the ECS (entities, components, archetype kinds) because # only it knows their shape. Everything below belongs to the runtime, so the # runtime writes it — same order both ways. fn rt_save_state(f: ptr) -> void { let w = mem_alloc(16) poke32(w, 0, rt_rng) poke32(w, 1, rt_mapw) poke32(w, 2, rt_maph) poke32(w, 3, rt_alive) file_write(f, w, 16) file_write(f, rt_regs, 64 * 4) file_write(f, rt_map, 96 * 64) file_write(f, rt_statusbuf, 96) mem_free(w) } fn rt_load_state(f: ptr) -> void { let w = mem_alloc(16) file_read(f, w, 16) rt_rng = peek32(w, 0) rt_mapw = peek32(w, 1) rt_maph = peek32(w, 2) file_read(f, rt_regs, 64 * 4) file_read(f, rt_map, 96 * 64) file_read(f, rt_statusbuf, 96) mem_free(w) }