Closes the two open issues and lands the pending unreleased batch: - #90: `Sprite { atlas: 1 }` routes esys_sprite through atlas_draw_ex (scale/flip/tint), so cell / cell_span / strip ids of any size draw through the engine sprite-render system. examples/library/sprite_atlas is the pixel-readback regression. - #91: `become` from an @On(Event) listener / global handler / plain function no longer segfaults the compiler; it emits @L_scene_leave() (a dispatch on the live scene id) so the leaving scene's on-exit runs. UI_* handles are readable from any code (widget table built on first use). examples/library/scene_menus covers it. - fix: a windowed `ludicc -o` build that reaches the audio runtime only through the atlas/Assets preload import now links audio.ll + AVFoundation (the audio backend link was gated on a game-level Audio.* call, so any windowed game declaring Sprite failed to link). - the hand-written "Unreleased" CHANGELOG section is converted to changesets under changes/ so `x release` generates it. - plus the batch: engine-driven retained UI + UiClicked event, Overlay phase, TileSkin tilemap-render system, Key.* constants, Font/Ui/File namespaces, Sprite.strip, prefabs, managers, countdown fields, enum-typed machines, layer @Queries, ludic.prefs / ludic.dungeon packages, Ai.seek pathing, Solids.solid2, cursor confine (mode 3) fix, shooter centre-aim fix, reserved-word function diagnostic. Verified: x test (124/124), x test-tools, check-impl, check-vocabulary, check-docs, docs-gen + docs-check, bootstrap-cfree (seed is a fixpoint). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
e9c2c51bc3
commit
ad548840c7
139 changed files with 58981 additions and 43671 deletions
|
|
@ -33,7 +33,89 @@ function ns_lower(s: pointer) -> pointer {
|
|||
return b
|
||||
}
|
||||
|
||||
# `Prop.of(e)`: the address of entity e's Prop slot in the dense @S_ store,
|
||||
# typed as Prop — exactly what a query loop binds. It is not checked: reading a
|
||||
# component the entity does not carry yields its zeroed storage, like a query
|
||||
# binding would. Guard with `Prop.has(e)` when the entity may lack it.
|
||||
# `Prop.has(e)`: e is in range, alive, and carries Prop. Branch-free: an
|
||||
# out-of-range handle is clamped to slot 0 for the loads, then masked out.
|
||||
function emit_component_access(prop: pointer, meth: pointer, e: Node) -> Val {
|
||||
let me = itoa(MAX_ENT)
|
||||
if (meth == "despawn_all") { # `Prop.despawn_all()`: a query loop that despawns each
|
||||
if len(e.kids) != 0 { perr(`{prop}.despawn_all takes no arguments`) }
|
||||
let q = node(S_QUERY); q.kids = new []Node; q.c = node(N_BLOCK)
|
||||
let term = node(E_ID); term.s = prop; push(q.c.kids, term)
|
||||
let v = node(E_ID); v.s = "__each"; push(q.kids, v)
|
||||
q.a = node(N_BLOCK); let d = node(S_DESPAWN); let sc = node(E_CALL); let sid = node(E_ID); sid.s = "self"; sc.a = sid; d.a = sc; push(q.a.kids, d)
|
||||
emit_query(q)
|
||||
return val("0", "void")
|
||||
}
|
||||
if (meth == "count") { # `Prop.count()`: live entities carrying Prop
|
||||
if len(e.kids) != 0 { perr(`{prop}.count takes no arguments`) }
|
||||
let ip = emit_alloca("i32"); store_at("i32", "0", ip)
|
||||
let cp = emit_alloca("i32"); store_at("i32", "0", cp)
|
||||
let cond = lbl("cc"); let body = lbl("cb"); let nxt = lbl("cn"); let endl = lbl("ce")
|
||||
emit(" br label %"); emit(cond); emit("\n"); emit(cond); emit(":\n")
|
||||
let i0 = emit_bind(`load i32, ptr {ip}`)
|
||||
let ec = emit_bind("load i32, ptr @L_entc")
|
||||
let lt = emit_bind(`icmp slt i32 {i0}, {ec}`)
|
||||
emit(" br i1 "); emit(lt); emit(", label %"); emit(body); emit(", label %"); emit(endl); emit("\n")
|
||||
emit(body); emit(":\n")
|
||||
let ap = emit_bind(`getelementptr inbounds [{me} x i32], ptr @L_alive, i32 0, i32 {i0}`)
|
||||
let al = emit_bind(`load i32, ptr {ap}`)
|
||||
let hp = emit_bind(`getelementptr inbounds [{me} x i8], ptr @H_{prop}, i32 0, i32 {i0}`)
|
||||
let hv = emit_bind(`load i8, ptr {hp}`)
|
||||
let hz = emit_bind(`zext i8 {hv} to i32`)
|
||||
let both = emit_bind(`and i32 {al}, {hz}`)
|
||||
let c0 = emit_bind(`load i32, ptr {cp}`)
|
||||
let c1 = emit_bind(`add i32 {c0}, {both}`)
|
||||
store_at("i32", c1, cp)
|
||||
emit(" br label %"); emit(nxt); emit("\n"); emit(nxt); emit(":\n")
|
||||
let i1 = emit_bind(`add i32 {i0}, 1`)
|
||||
store_at("i32", i1, ip)
|
||||
emit(" br label %"); emit(cond); emit("\n"); emit(endl); emit(":\n")
|
||||
return val(emit_bind(`load i32, ptr {cp}`), "int")
|
||||
}
|
||||
if len(e.kids) != 1 { perr(`{prop}.{meth} takes one argument: the entity`) }
|
||||
let ev = emit_expr(e.kids[0])
|
||||
if (meth == "of") {
|
||||
let slot = emit_bind(`getelementptr inbounds [{me} x %Cmp_{prop}], ptr @S_{prop}, i32 0, i32 {ev.code}`)
|
||||
return val(slot, prop)
|
||||
}
|
||||
let in_range = emit_bind(`icmp ult i32 {ev.code}, {me}`)
|
||||
let idx = emit_bind(`select i1 {in_range}, i32 {ev.code}, i32 0`)
|
||||
let ap = emit_bind(`getelementptr inbounds [{me} x i32], ptr @L_alive, i32 0, i32 {idx}`)
|
||||
let alive = emit_bind(`load i32, ptr {ap}`)
|
||||
let is_alive = emit_bind(`icmp ne i32 {alive}, 0`)
|
||||
let hp = emit_bind(`getelementptr inbounds [{me} x i8], ptr @H_{prop}, i32 0, i32 {idx}`)
|
||||
let hv = emit_bind(`load i8, ptr {hp}`)
|
||||
let carries = emit_bind(`icmp ne i8 {hv}, 0`)
|
||||
let ok1 = emit_bind(`and i1 {in_range}, {is_alive}`)
|
||||
let ok = emit_bind(`and i1 {ok1}, {carries}`)
|
||||
return val(emit_bind(`zext i1 {ok} to i32`), "bool")
|
||||
}
|
||||
|
||||
# is a call's first argument text (a string literal or a string-typed expression)?
|
||||
# Selects the by-name form of an overloaded builtin (Audio.play(name: "hit")).
|
||||
function first_arg_is_text(e: Node) -> bool {
|
||||
if len(e.kids) == 0 { return false }
|
||||
var a = e.kids[0]
|
||||
if a.kind == E_FINIT { if (a.s == "name") { return true }; a = a.a }
|
||||
if a.kind == E_STR { return true }
|
||||
let t = static_type(a)
|
||||
if (t == null) { return false }
|
||||
return t == "string"
|
||||
}
|
||||
|
||||
function emit_ns_call(ns: pointer, meth: pointer, e: Node) -> Val {
|
||||
# `Prop.of(e)` / `Prop.has(e)` — typed access to one entity's component, the
|
||||
# same binding a query loop makes but for an entity handle held in a variable.
|
||||
# `Prop.of(e)` yields the component (read and assign its fields directly);
|
||||
# `Prop.has(e)` is true when `e` is a live entity carrying Prop. A package that
|
||||
# declares a real `prop_of` / `prop_has` function keeps it.
|
||||
if (find_comp(ns) != null) and ((meth == "of") or (meth == "has") or (meth == "count") or (meth == "despawn_all")) {
|
||||
if find_fn(ns_lower(ns) + ("_") + meth) == null { return emit_component_access(ns, meth, e) }
|
||||
}
|
||||
# Math.* is computed inline (deterministic fixed-point), not routed through a
|
||||
# bare rt_ name — so `floor`/`round`/`lerp` never leak into the bare namespace.
|
||||
if (ns == "Math") {
|
||||
|
|
@ -168,6 +250,7 @@ function emit_ns_call(ns: pointer, meth: pointer, e: Node) -> Val {
|
|||
if (ns == "Screen") {
|
||||
if (meth == "clear") { bare = "clear"; push(labels, "color") }
|
||||
if (meth == "fill_rectangle") { bare = "fill_rect"; push(labels, "x"); push(labels, "y"); push(labels, "width"); push(labels, "height"); push(labels, "color") }
|
||||
if (meth == "bar") { bare = "bar"; push(labels, "x"); push(labels, "y"); push(labels, "width"); push(labels, "height"); push(labels, "value"); push(labels, "max"); push(labels, "color"); push(labels, "back") }
|
||||
if (meth == "draw_rectangle") { bare = "frame_rect"; push(labels, "x"); push(labels, "y"); push(labels, "width"); push(labels, "height"); push(labels, "color") }
|
||||
if (meth == "put_pixel") { bare = "put_px"; push(labels, "x"); push(labels, "y"); push(labels, "color") }
|
||||
if (meth == "draw_text") { bare = "text"; push(labels, "x"); push(labels, "y"); push(labels, "text"); push(labels, "color"); push(labels, "scale") }
|
||||
|
|
@ -200,14 +283,75 @@ function emit_ns_call(ns: pointer, meth: pointer, e: Node) -> Val {
|
|||
if (meth == "sheet") { bare = "atlas_sheet"; push(labels, "path"); push(labels, "cellw"); push(labels, "cellh") }
|
||||
if (meth == "cell") { bare = "atlas_cell"; push(labels, "sheet"); push(labels, "col"); push(labels, "row") }
|
||||
if (meth == "cell_span") { bare = "atlas_cell_span"; push(labels, "sheet"); push(labels, "col"); push(labels, "row"); push(labels, "cols"); push(labels, "rows") }
|
||||
if (meth == "strip") { bare = "atlas_strip"; push(labels, "sheet"); push(labels, "col"); push(labels, "row"); push(labels, "count"); push(labels, "rows") }
|
||||
if (meth == "define") { bare = "atlas_define"; push(labels, "name"); push(labels, "sheet"); push(labels, "col"); push(labels, "row") }
|
||||
if (meth == "named") { bare = "atlas_named"; push(labels, "name") }
|
||||
if (meth == "draw") { bare = "atlas_draw"; push(labels, "id"); push(labels, "x"); push(labels, "y") }
|
||||
if (meth == "draw_scaled") { bare = "atlas_draw_scaled"; push(labels, "id"); push(labels, "x"); push(labels, "y"); push(labels, "scale") }
|
||||
if (meth == "draw_meter") { bare = "atlas_draw_meter"; push(labels, "x"); push(labels, "y"); push(labels, "value"); push(labels, "max"); push(labels, "per_icon"); push(labels, "spacing"); push(labels, "full"); push(labels, "half"); push(labels, "empty") }
|
||||
if (meth == "width") { bare = "atlas_width"; push(labels, "id") }
|
||||
if (meth == "height") { bare = "atlas_height"; push(labels, "id") }
|
||||
}
|
||||
if (ns == "Font") {
|
||||
if (meth == "load") { bare = "font_load"; push(labels, "path") }
|
||||
}
|
||||
# Ui.* — the retained UI (ui blocks): navigation is engine-ticked (esys_ui) and
|
||||
# activations arrive as UiClicked events; these are the remaining verbs.
|
||||
if (ns == "Ui") {
|
||||
if (meth == "build") { bare = "ui_build" }
|
||||
if (meth == "open") { bare = "ui_open"; push(labels, "id") }
|
||||
if (meth == "close") { bare = "ui_close" }
|
||||
if (meth == "tick") { bare = "ui_tick"; push(labels, "key") }
|
||||
if (meth == "clicked") { bare = "ui_clicked"; push(labels, "id") }
|
||||
if (meth == "set_text") { bare = "ui_set_text"; push(labels, "id"); push(labels, "text") }
|
||||
if (meth == "render") { bare = "ui_render" }
|
||||
}
|
||||
if (ns == "File") {
|
||||
if (meth == "open") { bare = "file_open"; push(labels, "path"); push(labels, "mode") }
|
||||
if (meth == "read") { bare = "file_read"; push(labels, "file"); push(labels, "buffer"); push(labels, "count") }
|
||||
if (meth == "write") { bare = "file_write"; push(labels, "file"); push(labels, "buffer"); push(labels, "count") }
|
||||
if (meth == "seek") { bare = "file_seek"; push(labels, "file"); push(labels, "offset"); push(labels, "whence") }
|
||||
if (meth == "tell") { bare = "file_tell"; push(labels, "file") }
|
||||
if (meth == "close") { bare = "file_close"; push(labels, "file") }
|
||||
}
|
||||
if (ns == "Fx") { # engine-owned sparks and floating numbers (fx.ludic)
|
||||
if (meth == "sparks") { bare = "fx_sparks"; push(labels, "x"); push(labels, "y"); push(labels, "color"); push(labels, "count") }
|
||||
if (meth == "number") { bare = "fx_number"; push(labels, "x"); push(labels, "y"); push(labels, "value"); push(labels, "color") }
|
||||
if (meth == "clear") { bare = "fx_clear" }
|
||||
}
|
||||
if (ns == "Prefab") { # spawn a prefab by name at runtime (see emit_prefab_fns)
|
||||
if (meth == "spawn_at") { # Prefab.spawn_at(name:, at: IVec2) — spawned, then placed
|
||||
if not has_prefabs() { perr("Prefab.spawn_at: the program declares no prefab") }
|
||||
let pos = find_comp("Position")
|
||||
if (pos == null) { perr("Prefab.spawn_at needs a Position property") }
|
||||
let plabels = new []pointer; push(plabels, "name"); push(plabels, "at")
|
||||
reorder_named(e, plabels)
|
||||
let nm = emit_expr(e.kids[0])
|
||||
let at = emit_expr(e.kids[1])
|
||||
let ent = emit_bind(`call i32 @L_spawn_prefab(ptr {nm.code})`)
|
||||
let me = itoa(MAX_ENT)
|
||||
let slot = emit_bind(`getelementptr inbounds [{me} x %Cmp_Position], ptr @S_Position, i32 0, i32 {ent}`)
|
||||
let xa = emit_bind(`getelementptr inbounds %Cmp_Position, ptr {slot}, i32 0, i32 {itoa(field_index(pos, "x"))}`)
|
||||
let ya = emit_bind(`getelementptr inbounds %Cmp_Position, ptr {slot}, i32 0, i32 {itoa(field_index(pos, "y"))}`)
|
||||
let ax = vec_x(at.code)
|
||||
let ay = vec_y(at.code)
|
||||
emit(" store i32 "); emit(ax); emit(", ptr "); emit(xa); emit("\n")
|
||||
emit(" store i32 "); emit(ay); emit(", ptr "); emit(ya); emit("\n")
|
||||
return val(ent, "entity")
|
||||
}
|
||||
if (meth == "spawn") {
|
||||
if not has_prefabs() { perr("Prefab.spawn: the program declares no prefab") }
|
||||
let plabels = new []pointer; push(plabels, "name")
|
||||
reorder_named(e, plabels)
|
||||
if len(e.kids) != 1 { perr("Prefab.spawn takes one argument: the prefab's name") }
|
||||
let nm = emit_expr(e.kids[0])
|
||||
return val(emit_bind(`call i32 @L_spawn_prefab(ptr {nm.code})`), "entity")
|
||||
}
|
||||
perr(`unknown builtin Prefab.{meth}`)
|
||||
}
|
||||
if (ns == "Assets") {
|
||||
if (meth == "font") { bare = "assets_font"; push(labels, "name") } # a .ttf loaded through the queue
|
||||
if (meth == "enqueue_dir") { bare = "assets_enqueue_dir"; push(labels, "path") } # every file of a directory, by base name
|
||||
if (meth == "image") { bare = "atlas_image"; push(labels, "path") }
|
||||
if (meth == "load") { bare = "atlas_image"; push(labels, "path") } # alias
|
||||
if (meth == "get") { bare = "atlas_named"; push(labels, "name") } # alias of Sprite.named
|
||||
|
|
@ -226,15 +370,30 @@ function emit_ns_call(ns: pointer, meth: pointer, e: Node) -> Val {
|
|||
if (meth == "set") { bare = "camera"; push(labels, "x"); push(labels, "y") }
|
||||
if (meth == "follow") { bare = "camera_follow"; push(labels, "x"); push(labels, "y"); push(labels, "lerp") }
|
||||
if (meth == "shake") { bare = "camera_shake"; push(labels, "amount") }
|
||||
if (meth == "shake_for") { bare = "camera_shake_for"; push(labels, "amount"); push(labels, "frames") } # a timed shake the engine decays
|
||||
if (meth == "zoom") { bare = "camera_zoom"; push(labels, "scale") } # #78 deterministic Q16.16 zoom
|
||||
}
|
||||
if (ns == "Map") {
|
||||
if (meth == "size") { bare = "map_size"; push(labels, "width"); push(labels, "height") }
|
||||
if (meth == "row") { bare = "map_row"; push(labels, "y"); push(labels, "cells") }
|
||||
if (meth == "tile") { bare = "tile"; push(labels, "x"); push(labels, "y") }
|
||||
if (meth == "get") { bare = "tile"; push(labels, "x"); push(labels, "y") }
|
||||
# the cell API: edit the grid in place, ask it what is where
|
||||
if (meth == "set") { bare = "map_set"; push(labels, "x"); push(labels, "y"); push(labels, "glyph") }
|
||||
if (meth == "fill") { bare = "map_fill"; push(labels, "glyph") }
|
||||
if (meth == "rect") { bare = "map_rect"; push(labels, "x"); push(labels, "y"); push(labels, "width"); push(labels, "height"); push(labels, "glyph") }
|
||||
if (meth == "border") { bare = "map_border"; push(labels, "glyph") }
|
||||
if (meth == "random_cell") { bare = "map_random_cell"; push(labels, "glyph") }
|
||||
if (meth == "random_cell_far") { bare = "map_random_cell_far"; push(labels, "glyph"); push(labels, "from"); push(labels, "min_tiles") }
|
||||
if (meth == "is_solid") { bare = "map_is_solid"; push(labels, "x"); push(labels, "y") }
|
||||
if (meth == "is_solid_at") { bare = "map_is_solid_at"; push(labels, "x"); push(labels, "y") }
|
||||
if (meth == "to_tile") { bare = "map_to_tile"; push(labels, "pixel") }
|
||||
if (meth == "width") { bare = "map_width" }
|
||||
if (meth == "height") { bare = "map_height" }
|
||||
}
|
||||
if (ns == "Random") {
|
||||
if (meth == "range") { bare = "rng_range"; push(labels, "low"); push(labels, "high") }
|
||||
if (meth == "weighted") { bare = "rng_weighted"; push(labels, "weights") } # an index in proportion to its weight
|
||||
if (meth == "chance") { bare = "rng_chance"; push(labels, "percent") }
|
||||
if (meth == "seed") { bare = "seed"; push(labels, "value") }
|
||||
if (meth == "value") { bare = "rng_value" }
|
||||
|
|
@ -245,8 +404,8 @@ function emit_ns_call(ns: pointer, meth: pointer, e: Node) -> Val {
|
|||
if (meth == "key") { bare = "key" }
|
||||
# action maps + deterministic record/replay (#7) — spliced runtime in
|
||||
# runtime/native/input.ludic, reached as ordinary @fn_input_* calls.
|
||||
if (meth == "bind") { bare = "input_bind" }
|
||||
if (meth == "rebind") { bare = "input_rebind" }
|
||||
if (meth == "bind") { bare = "input_bind"; push(labels, "action"); push(labels, "key") }
|
||||
if (meth == "rebind") { bare = "input_rebind"; push(labels, "action"); push(labels, "old"); push(labels, "new") }
|
||||
if (meth == "poll") { bare = "input_poll" }
|
||||
if (meth == "down") { bare = "input_down" }
|
||||
if (meth == "pressed") { bare = "input_pressed" }
|
||||
|
|
@ -268,6 +427,7 @@ function emit_ns_call(ns: pointer, meth: pointer, e: Node) -> Val {
|
|||
if (meth == "release") { bare = "input_release"; push(labels, "key") }
|
||||
if (meth == "axis") { bare = "input_axis"; push(labels, "neg"); push(labels, "pos") }
|
||||
if (meth == "axis_i") { bare = "input_axis_i"; push(labels, "neg"); push(labels, "pos") } # #79 directional int (-1/0/1)
|
||||
if (meth == "move_i") { bare = "input_move_i" } # WASD / arrows / left stick -> IVec2
|
||||
if (meth == "vector") { bare = "input_vector"; push(labels, "left"); push(labels, "right"); push(labels, "up"); push(labels, "down") }
|
||||
if (meth == "strength") { bare = "input_strength"; push(labels, "action") }
|
||||
if (meth == "mouse_x") { bare = "input_mouse_x" }
|
||||
|
|
@ -292,9 +452,12 @@ function emit_ns_call(ns: pointer, meth: pointer, e: Node) -> Val {
|
|||
# triggers are ordinary frame-driven calls, so a replay fires the same sounds.
|
||||
if (ns == "Audio") {
|
||||
if (meth == "load") { bare = "audio_load"; push(labels, "path") }
|
||||
if (meth == "play") { bare = "audio_play"; push(labels, "id") }
|
||||
if (meth == "define") { bare = "audio_define"; push(labels, "name"); push(labels, "path") }
|
||||
if (meth == "named") { bare = "audio_named"; push(labels, "name") }
|
||||
# Audio.play / play_music take a handle, or a name from the sound bank (Audio.define)
|
||||
if (meth == "play") { if first_arg_is_text(e) { bare = "audio_play_named"; push(labels, "name") } else { bare = "audio_play"; push(labels, "id") } }
|
||||
if (meth == "play_sound") { bare = "audio_play"; push(labels, "id") }
|
||||
if (meth == "play_music") { bare = "audio_play_music"; push(labels, "id") }
|
||||
if (meth == "play_music") { if first_arg_is_text(e) { bare = "audio_play_music_named"; push(labels, "name") } else { bare = "audio_play_music"; push(labels, "id") } }
|
||||
if (meth == "stop") { bare = "audio_stop"; push(labels, "id") }
|
||||
if (meth == "stop_music") { bare = "audio_stop_music" }
|
||||
if (meth == "stop_all") { bare = "audio_stop_all" }
|
||||
|
|
@ -519,6 +682,7 @@ function emit_ns_call(ns: pointer, meth: pointer, e: Node) -> Val {
|
|||
if (meth == "cos") { bare = "angle_cos"; push(labels, "a") }
|
||||
if (meth == "add") { bare = "angle_add"; push(labels, "a"); push(labels, "b") }
|
||||
if (meth == "diff") { bare = "angle_diff"; push(labels, "a"); push(labels, "b") }
|
||||
if (meth == "diff_degrees") { bare = "angle_diff_degrees"; push(labels, "a"); push(labels, "b") } # signed, -180..180, ints
|
||||
if (meth == "lerp") { bare = "angle_lerp"; push(labels, "a"); push(labels, "b"); push(labels, "t") }
|
||||
}
|
||||
if (ns == "Percent") {
|
||||
|
|
@ -716,6 +880,14 @@ function emit_ns_call(ns: pointer, meth: pointer, e: Node) -> Val {
|
|||
# #76: a namespace declared with a block controls its public surface — an
|
||||
# `internal` method is emitted but not callable as Name.method.
|
||||
if is_ns_block(ns) and (not ns_export_has(bare)) { perr(`{ns}.{meth} is internal to namespace {ns}`) }
|
||||
# a namespace function takes named arguments like any other function: its
|
||||
# labels are its parameter names, so `Weapon.def(name: "pistol", ...)` works.
|
||||
let nsfn = find_fn(bare)
|
||||
if (nsfn != null) {
|
||||
let names = param_labels(nsfn)
|
||||
var pi2 = 0
|
||||
while pi2 < len(names) { push(labels, names[pi2]); pi2 = pi2 + 1 }
|
||||
}
|
||||
}
|
||||
if (bare == null) { perr(`unknown builtin {ns}.{meth}`) }
|
||||
reorder_named(e, labels)
|
||||
|
|
@ -1121,6 +1293,7 @@ function emit_expr(e: Node) -> Val {
|
|||
if e.kind == E_FLOAT { return val(itoa(e.ival), "fixed") }
|
||||
if e.kind == E_BOOL { return val(itoa(e.ival), "bool") }
|
||||
if e.kind == E_NULL { return val("null", "pointer") }
|
||||
if e.kind == S_SPAWN { return val(emit_spawn(e), "entity") } # `let e = spawn Model { … }`
|
||||
if e.kind == E_SLICE { # s[a..b] -> a fresh substring
|
||||
let base = emit_expr(e.a)
|
||||
let lo = emit_expr(e.b)
|
||||
|
|
@ -1161,11 +1334,21 @@ function emit_expr(e: Node) -> Val {
|
|||
if (cv < 0) { perr(`unknown color Color.{e.s}`) }
|
||||
return val(itoa(cv), "int")
|
||||
}
|
||||
if (e.a.s == "Key") { # `Key.Name` -> its key code, at compile time
|
||||
let kv = key_lookup(e.s)
|
||||
if (kv < 0) { perr(`unknown key Key.{e.s}`) }
|
||||
return val(itoa(kv), "int")
|
||||
}
|
||||
let ord = enum_ordinal(e.a.s, e.s) # `Enum.Variant` -> its ordinal, a compile-time int
|
||||
if ord >= 0 { return val(itoa(ord), "int") }
|
||||
}
|
||||
let bt = static_type(e.a) # `x.field` where field is @Computed -> inline it
|
||||
if (bt != null) {
|
||||
if (bt == "IVec2") and ((e.s == "x") or (e.s == "y")) { # a packed vector's component
|
||||
let v = emit_expr(e.a)
|
||||
if (e.s == "x") { return val(vec_x(v.code), "int") }
|
||||
return val(vec_y(v.code), "int")
|
||||
}
|
||||
let cx = computed_expr(bt, e.s)
|
||||
if (cx != null) { return emit_expr(qualify_fields(cx, e.a)) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -120,6 +120,8 @@ function lbl(pfx: pointer) -> pointer { let r = (pfx + itoa(ll_lbl)); ll_lbl =
|
|||
# slice) is a pointer; void is void.
|
||||
function llty(t: pointer) -> pointer {
|
||||
if (t == "int") or (t == "bool") or (t == "fixed") or (t == "entity") { return "i32" } # entity = an i32 handle (self())
|
||||
if is_bare_enum(t) { return "i32" } # a payload-free enum is an int by another name
|
||||
if (t == "countdown") { return "i32" } # an int the engine counts down to 0 each Update
|
||||
if (t == "long") { return "i64" } # a 64-bit signed integer
|
||||
if (t == "Vector") { return "i64" } # a 2D vector: (x, y) fixeds packed into one i64
|
||||
if (t == "IVec2") { return "i64" } # an integer 2D vector: (x, y) ints packed into one i64
|
||||
|
|
@ -192,6 +194,34 @@ function enum_ordinal(ename: pointer, vname: pointer) -> int {
|
|||
}
|
||||
return 0 - 1
|
||||
}
|
||||
# an enum none of whose variants carries a payload: a plain int-valued naming layer
|
||||
function is_bare_enum(name: pointer) -> bool {
|
||||
let en = find_enum(name)
|
||||
if (en == null) { return false }
|
||||
var i = 0
|
||||
while i < len(en.kids) { if len(en.kids[i].kids) > 0 { return false }; i = i + 1 }
|
||||
return true
|
||||
}
|
||||
function find_prefab(name: pointer) -> Node {
|
||||
var i = 0
|
||||
while i < len(prog) { let d = prog[i]; if d.kind == N_PREFAB and (d.s == name) { return d }; i = i + 1 }
|
||||
return null
|
||||
}
|
||||
function has_prefabs() -> bool {
|
||||
var i = 0
|
||||
while i < len(prog) { if prog[i].kind == N_PREFAB { return true }; i = i + 1 }
|
||||
return false
|
||||
}
|
||||
# does any component declare a `countdown` field?
|
||||
function has_countdowns() -> bool {
|
||||
var i = 0
|
||||
while i < len(prog) {
|
||||
let d = prog[i]
|
||||
if d.kind == N_COMP { var f = 0; while f < len(d.kids) { if (d.kids[f].ty == "countdown") { return true }; f = f + 1 } }
|
||||
i = i + 1
|
||||
}
|
||||
return false
|
||||
}
|
||||
function find_fn(name: pointer) -> Node {
|
||||
var i = 0
|
||||
while i < len(prog) { let d = prog[i]; if d.kind == N_FN and (d.s == name) { return d }; i = i + 1 }
|
||||
|
|
@ -288,7 +318,25 @@ function computed_expr(prop: pointer, field: pointer) -> Node {
|
|||
}
|
||||
# best-effort static type of an expression (for computed-field lookup; emits nothing)
|
||||
function static_type(e: Node) -> pointer {
|
||||
if e.kind == E_ID { let li = loc_find(e.s); if li >= 0 { return loc_ty[li] } }
|
||||
if e.kind == E_ID {
|
||||
let li = loc_find(e.s); if li >= 0 { return loc_ty[li] }
|
||||
let g = find_global(e.s); if (g != null) { return g.ty }
|
||||
}
|
||||
if e.kind == E_CALL {
|
||||
if e.a.kind == E_ID { let f = find_fn(e.a.s); if (f != null) { return f.ty } }
|
||||
if e.a.kind == E_MEMBER and e.a.a.kind == E_ID { # Ns.fn(...) / Prop.of(e)
|
||||
if (find_comp(e.a.a.s) != null) and (e.a.s == "of") { return e.a.a.s }
|
||||
if (e.a.a.s == "IVec2") and ((e.a.s == "make") or (e.a.s == "zero") or (e.a.s == "add") or (e.a.s == "sub") or (e.a.s == "scale") or (e.a.s == "along") or (e.a.s == "step")) { return "IVec2" }
|
||||
if (e.a.a.s == "Map") and ((e.a.s == "random_cell") or (e.a.s == "random_cell_far") or (e.a.s == "to_tile")) { return "IVec2" }
|
||||
if (e.a.a.s == "Collider") and (e.a.s == "center") { return "IVec2" }
|
||||
if (e.a.a.s == "Input") and (e.a.s == "move_i") { return "IVec2" }
|
||||
let nf = find_fn(ns_lower(e.a.a.s) + ("_") + e.a.s); if (nf != null) { return nf.ty }
|
||||
}
|
||||
}
|
||||
if e.kind == E_MEMBER { # record.field
|
||||
let bt = static_type(e.a)
|
||||
if (bt != null) { let s = layout_node(bt); if (s != null) { return field_type(s, e.s) } }
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ function emit_params_sig(d: Node) -> void {
|
|||
|
||||
function emit_fn(d: Node) -> void {
|
||||
ll_t = 0; ll_lbl = 0; g_term = false; loc_reset(); nloop = 0
|
||||
g_cur_scene = null # a function belongs to no scene: `become` leaves the live one
|
||||
ret_ty = d.ty
|
||||
let fbody = buf_new()
|
||||
falloc = buf_new()
|
||||
|
|
@ -57,6 +58,7 @@ function emit_main(d: Node) -> void {
|
|||
# its framebuffer allocated. Headless it only allocates — no window, no output —
|
||||
# so a non-rendering entry game is unchanged.
|
||||
if (find_fn("rt_init") != null) { emit(" call void @fn_rt_init()\n") }
|
||||
emit(" call void @L_init_globals()\n")
|
||||
emit_block(d.a)
|
||||
if not g_term { emit(" br label %ret\n") }
|
||||
emit("ret:\n")
|
||||
|
|
@ -106,6 +108,7 @@ function emit_test_runner() -> void {
|
|||
emit(" store ptr %argv, ptr @L_argv\n")
|
||||
emit(" %failed = alloca i32\n store i32 0, ptr %failed\n")
|
||||
if has_ecs() { emit(" call void @rt_init()\n") }
|
||||
emit(" call void @L_init_globals()\n")
|
||||
i = 0
|
||||
while i < len(g_tests) {
|
||||
let okmsg = emit_str_const(`ok - {g_tests[i].s}`)
|
||||
|
|
@ -166,6 +169,7 @@ function emit_program() -> void {
|
|||
}
|
||||
i = i + 1
|
||||
}
|
||||
emit_global_init_fn() # @L_init_globals: the non-constant `var` initializers
|
||||
if len(g_events) > 0 { emit_event_fns() } # EV0: @ev_<E> event-dispatch functions
|
||||
if has_ecs() { emit_world_table() } # EV2/EV8: the mod reflection ABI (powers Query.* / Reflect.* / engine systems, and lets a binary module (#64) link against a shared world). Unused defs dead-strip at -O2, so a game that touches none is output-identical.
|
||||
if has_ecs() { emit_ecs_allocator(); emit_snapshot() }
|
||||
|
|
|
|||
|
|
@ -30,9 +30,49 @@ function global_init(d: Node) -> pointer {
|
|||
let e = d.a
|
||||
if e.kind == E_INT or e.kind == E_FLOAT or e.kind == E_BOOL { return itoa(e.ival) }
|
||||
if e.kind == E_UN and (e.s == ("-")) and e.a.kind == E_INT { return (("-") + itoa(e.a.ival)) }
|
||||
if e.kind == E_MEMBER and e.a.kind == E_ID { # `Enum.Variant` is a compile-time int
|
||||
let ord = enum_ordinal(e.a.s, e.s)
|
||||
if ord >= 0 { return itoa(ord) }
|
||||
}
|
||||
if (llty(d.ty) == "ptr") { return "null" }
|
||||
return "0"
|
||||
}
|
||||
# does a global's initializer need code at startup (anything global_init cannot fold)?
|
||||
function global_needs_init_code(d: Node) -> bool {
|
||||
if (d.a == null) { return false }
|
||||
let e = d.a
|
||||
if e.kind == E_INT or e.kind == E_FLOAT or e.kind == E_BOOL or e.kind == E_NULL { return false }
|
||||
if e.kind == E_UN and (e.s == ("-")) and e.a.kind == E_INT { return false }
|
||||
if e.kind == E_MEMBER and e.a.kind == E_ID { if enum_ordinal(e.a.s, e.s) >= 0 { return false } }
|
||||
return true
|
||||
}
|
||||
|
||||
# @L_init_globals(): evaluate every global initializer global_init could not fold
|
||||
# (`var run: Progress = new Progress`, `var speed: int = BASE * 2`, a call), in
|
||||
# declaration order, once at startup — after the runtime boots, before Start.
|
||||
function emit_global_init_fn() -> void {
|
||||
ll_t = 0; ll_lbl = 0; g_term = false; loc_reset(); nloop = 0; nself = 0
|
||||
ret_ty = "void"
|
||||
let fbody = buf_new()
|
||||
falloc = buf_new()
|
||||
let saved = code
|
||||
code = fbody
|
||||
var i = 0
|
||||
while i < len(prog) {
|
||||
let d = prog[i]
|
||||
if d.kind == N_VAR and global_needs_init_code(d) {
|
||||
let v = emit_expr(d.a)
|
||||
emit(" store "); emit(llty(d.ty)); emit(" "); emit(coerce_code(v, d.ty)); emit(", ptr @g_"); emit(d.s); emit("\n")
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
emit(" ret void\n")
|
||||
code = saved
|
||||
emit("define void @L_init_globals() {\nentry:\n")
|
||||
emit(buf_str(falloc))
|
||||
emit(buf_str(fbody))
|
||||
emit("}\n\n")
|
||||
}
|
||||
|
||||
function emit_header() -> void {
|
||||
emith("; Ludic (self-hosted) -> LLVM IR\n")
|
||||
|
|
@ -103,6 +143,14 @@ function emit_header() -> void {
|
|||
while i < len(prog) {
|
||||
let d = prog[i]
|
||||
if d.kind == N_COMP {
|
||||
var j = 0 # the same record twice would silently use the first
|
||||
while j < i {
|
||||
if prog[j].kind == N_COMP and (prog[j].s == d.s) {
|
||||
if (j < g_prog_user_end) and (i >= g_prog_user_end) { perr(`property {d.s} is also a property of the engine runtime; choose another name`) }
|
||||
perr(`property {d.s} is declared twice`)
|
||||
}
|
||||
j = j + 1
|
||||
}
|
||||
emith(layout_ty(d.s)); emith(" = type { ")
|
||||
if len(d.kids) == 0 { emith("i32") }
|
||||
var f = 0
|
||||
|
|
@ -120,6 +168,14 @@ function emit_header() -> void {
|
|||
while i < len(prog) {
|
||||
let d = prog[i]
|
||||
if d.kind == N_VAR {
|
||||
var j = 0 # the same name twice would be one LLVM global
|
||||
while j < i {
|
||||
if prog[j].kind == N_VAR and (prog[j].s == d.s) {
|
||||
if (j < g_prog_user_end) and (i >= g_prog_user_end) { perr(`variable {d.s} is also a variable of the engine runtime; choose another name`) }
|
||||
perr(`variable {d.s} is declared twice`)
|
||||
}
|
||||
j = j + 1
|
||||
}
|
||||
emith("@g_"); emith(d.s); emith(" = internal global ")
|
||||
emith(llty(d.ty)); emith(" ")
|
||||
emith(global_init(d))
|
||||
|
|
|
|||
|
|
@ -74,7 +74,116 @@ function emit_engine_systems_for_phase(phase: pointer) -> void {
|
|||
# Tween.* fluent handles (#48): advanced each Update tick when the game uses
|
||||
# them (gated on g_uses_tween_rt rather than a declared component).
|
||||
if g_uses_tween_rt and (find_fn("esys_tween") != null) { emit(" call void @fn_esys_tween()\n") }
|
||||
if has_countdowns() { emit(" call void @L_countdowns()\n") } # `countdown` fields tick toward 0
|
||||
if g_uses_fx and (find_fn("fx_tick") != null) { emit(" call void @fn_fx_tick()\n") } # Fx.* age and move
|
||||
}
|
||||
if (phase == "Render") {
|
||||
if g_uses_fx and (find_fn("fx_draw") != null) { emit(" call void @fn_fx_draw()\n") } # Fx.* draw after the sprites
|
||||
}
|
||||
}
|
||||
|
||||
# @L_countdowns(): every `countdown` field of every live entity that carries the
|
||||
# component steps toward 0 once per Update (never below it). The timer idiom —
|
||||
# "frames left" fields counted down by hand in a handler — becomes a field type.
|
||||
function emit_countdown_system() -> void {
|
||||
let me = itoa(MAX_ENT)
|
||||
emit("define void @L_countdowns() {\nentry:\n")
|
||||
emit(" %n = load i32, ptr @L_entc\n br label %loop\nloop:\n")
|
||||
emit(" %i = phi i32 [ 0, %entry ], [ %i1, %next ]\n")
|
||||
emit(" %go = icmp slt i32 %i, %n\n br i1 %go, label %body, label %done\nbody:\n")
|
||||
emit(" %ap = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_alive, i32 0, i32 %i\n")
|
||||
emit(" %al = load i32, ptr %ap\n %alive = icmp ne i32 %al, 0\n br i1 %alive, label %c0, label %next\n")
|
||||
var k = 0
|
||||
var i = 0
|
||||
while i < len(prog) {
|
||||
let d = prog[i]
|
||||
if d.kind == N_COMP {
|
||||
var any = false
|
||||
var f = 0
|
||||
while f < len(d.kids) { if (d.kids[f].ty == "countdown") { any = true }; f = f + 1 }
|
||||
if any {
|
||||
let sk = itoa(k); let nk = itoa(k + 1)
|
||||
emit("c"); emit(sk); emit(":\n")
|
||||
emit(" %h"); emit(sk); emit(" = getelementptr inbounds ["); emit(me); emit(" x i8], ptr @H_"); emit(d.s); emit(", i32 0, i32 %i\n")
|
||||
emit(" %hv"); emit(sk); emit(" = load i8, ptr %h"); emit(sk); emit("\n")
|
||||
emit(" %has"); emit(sk); emit(" = icmp ne i8 %hv"); emit(sk); emit(", 0\n")
|
||||
emit(" br i1 %has"); emit(sk); emit(", label %t"); emit(sk); emit(", label %c"); emit(nk); emit("\n")
|
||||
emit("t"); emit(sk); emit(":\n")
|
||||
emit(" %s"); emit(sk); emit(" = getelementptr inbounds ["); emit(me); emit(" x %Cmp_"); emit(d.s); emit("], ptr @S_"); emit(d.s); emit(", i32 0, i32 %i\n")
|
||||
f = 0
|
||||
while f < len(d.kids) {
|
||||
if (d.kids[f].ty == "countdown") {
|
||||
let fk = sk + ("_") + itoa(f)
|
||||
emit(" %fa"); emit(fk); emit(" = getelementptr inbounds %Cmp_"); emit(d.s); emit(", ptr %s"); emit(sk); emit(", i32 0, i32 "); emit(itoa(f)); emit("\n")
|
||||
emit(" %fv"); emit(fk); emit(" = load i32, ptr %fa"); emit(fk); emit("\n")
|
||||
emit(" %fp"); emit(fk); emit(" = icmp sgt i32 %fv"); emit(fk); emit(", 0\n")
|
||||
emit(" %fd"); emit(fk); emit(" = sub i32 %fv"); emit(fk); emit(", 1\n")
|
||||
emit(" %fn"); emit(fk); emit(" = select i1 %fp"); emit(fk); emit(", i32 %fd"); emit(fk); emit(", i32 %fv"); emit(fk); emit("\n")
|
||||
emit(" store i32 %fn"); emit(fk); emit(", ptr %fa"); emit(fk); emit("\n")
|
||||
}
|
||||
f = f + 1
|
||||
}
|
||||
emit(" br label %c"); emit(nk); emit("\n")
|
||||
k = k + 1
|
||||
}
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
emit("c"); emit(itoa(k)); emit(":\n br label %next\n")
|
||||
emit("next:\n %i1 = add i32 %i, 1\n br label %loop\ndone:\n ret void\n}\n\n")
|
||||
}
|
||||
|
||||
# @L_prefab_<Name>(): spawn one prefab with its presets; @L_spawn_prefab(name):
|
||||
# the runtime dispatch by name that Prefab.spawn(name:) calls (-1 for no such prefab).
|
||||
function emit_prefab_fn(pre: Node) -> void {
|
||||
ll_t = 0; ll_lbl = 0; g_term = false; loc_reset(); nloop = 0; nself = 0
|
||||
g_cur_scene = null
|
||||
ret_ty = "int"
|
||||
let fbody = buf_new()
|
||||
falloc = buf_new()
|
||||
let saved = code
|
||||
code = fbody
|
||||
let sp = node(S_SPAWN); sp.s = pre.s
|
||||
let e = emit_spawn(sp)
|
||||
emit(" ret i32 "); emit(e); emit("\n")
|
||||
code = saved
|
||||
emit("define i32 @L_prefab_"); emit(pre.s); emit("() {\nentry:\n")
|
||||
emit(buf_str(falloc))
|
||||
emit(buf_str(fbody))
|
||||
emit("}\n\n")
|
||||
}
|
||||
function emit_prefab_fns() -> void {
|
||||
var i = 0
|
||||
while i < len(prog) { if prog[i].kind == N_PREFAB { emit_prefab_fn(prog[i]) }; i = i + 1 }
|
||||
g_uses_str = true
|
||||
ll_t = 0; ll_lbl = 0; g_term = false; loc_reset(); nloop = 0; nself = 0
|
||||
let fbody = buf_new()
|
||||
falloc = buf_new()
|
||||
let saved = code
|
||||
code = fbody
|
||||
i = 0
|
||||
while i < len(prog) {
|
||||
let d = prog[i]
|
||||
if d.kind == N_PREFAB {
|
||||
let lit = node(E_STR); lit.s = d.s
|
||||
let sv = emit_expr(lit)
|
||||
let same = emit_bind(`call i32 @fn_str_eq(ptr %name, ptr {sv.code})`)
|
||||
let hit = emit_bind(`icmp ne i32 {same}, 0`)
|
||||
let yes = lbl("pf"); let no = lbl("pfn")
|
||||
emit(" br i1 "); emit(hit); emit(", label %"); emit(yes); emit(", label %"); emit(no); emit("\n")
|
||||
emit(yes); emit(":\n")
|
||||
let r = emit_bind(`call i32 @L_prefab_{d.s}()`)
|
||||
emit(" ret i32 "); emit(r); emit("\n")
|
||||
emit(no); emit(":\n")
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
emit(" ret i32 -1\n")
|
||||
code = saved
|
||||
emit("define i32 @L_spawn_prefab(ptr %name) {\nentry:\n")
|
||||
emit(buf_str(falloc))
|
||||
emit(buf_str(fbody))
|
||||
emit("}\n\n")
|
||||
}
|
||||
|
||||
# Global handlers run first, then the active scene's layer handlers in
|
||||
|
|
@ -101,6 +210,7 @@ function phase_id(phase: pointer) -> int {
|
|||
if phase == "Render" { return 4 }
|
||||
if phase == "Start" { return 5 }
|
||||
if phase == "OnQuit" { return 6 }
|
||||
if phase == "Overlay" { return 7 } # HUD/UI pass after the engine drew sprites
|
||||
return 7
|
||||
}
|
||||
|
||||
|
|
@ -223,18 +333,46 @@ function emit_calls_for_phase(phase: pointer) -> void {
|
|||
}
|
||||
emit_engine_systems_for_phase(phase) # engine-owned systems run after every user handler
|
||||
emit_dyn_systems_for_phase(phase) # #64: mod-registered systems run last
|
||||
if (phase == "Overlay") { emit_scene_menu_render() } # a scene's `shows` menu paints last
|
||||
}
|
||||
|
||||
# the menu of the live `scene X shows Menu`, drawn on top of everything else
|
||||
function emit_scene_menu_render() -> void {
|
||||
var any = false
|
||||
var i = 0
|
||||
while i < len(g_scenes) { if (g_scenes[i].ty != null) { any = true }; i = i + 1 }
|
||||
if not any { return }
|
||||
let cs = emit_bind("load i32, ptr @L_scene")
|
||||
i = 0
|
||||
while i < len(g_scenes) {
|
||||
let sc = g_scenes[i]
|
||||
if (sc.ty != null) {
|
||||
let ce = emit_bind(`icmp eq i32 {cs}, {itoa(sc.ival)}`)
|
||||
let run = lbl("mrun"); let skip = lbl("mskip")
|
||||
emit(" br i1 "); emit(ce); emit(", label %"); emit(run); emit(", label %"); emit(skip); emit("\n")
|
||||
emit(run); emit(":\n call void @fn_rt_ui_render()\n")
|
||||
emit(" br label %"); emit(skip); emit("\n"); emit(skip); emit(":\n")
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
}
|
||||
|
||||
# on enter / on exit compile to void functions @scene_enter_<Name> /
|
||||
# @scene_exit_<Name>, called at the transition point (and enter at boot for the
|
||||
# start scene). Emitted for every scene, empty body when the hook is absent.
|
||||
function emit_scene_fn(name: pointer, kind: pointer, body: Node) -> void {
|
||||
function emit_scene_fn(name: pointer, kind: pointer, body: Node, menu: pointer) -> void {
|
||||
ll_t = 0; ll_lbl = 0; g_term = false; loc_reset(); nloop = 0; nself = 0
|
||||
ret_ty = "void"
|
||||
let fbody = buf_new()
|
||||
falloc = buf_new()
|
||||
let saved = code
|
||||
code = fbody
|
||||
# `scene X shows Menu`: a free cursor and the menu open on enter, closed on exit
|
||||
if (menu != null) and (kind == "enter") {
|
||||
if (find_fn("input_cursor_mode") != null) { emit(" call void @fn_input_cursor_mode(i32 0)\n") }
|
||||
emit(" call void @fn_rt_ui_open(i32 "); emit(itoa(ui_index_of("UI_" + menu))); emit(")\n")
|
||||
}
|
||||
if (menu != null) and (kind == "exit") { emit(" call void @fn_rt_ui_close()\n") }
|
||||
if (body != null) { emit_block(body) }
|
||||
# EV1: a `public` scene fires scene_<S>_enter / scene_<S>_exit after its block
|
||||
let sev = `scene_{name}_{kind}`
|
||||
|
|
@ -253,10 +391,31 @@ function emit_scene_hooks() -> void {
|
|||
while i < len(g_scenes) {
|
||||
let sc = g_scenes[i]
|
||||
g_cur_scene = sc
|
||||
emit_scene_fn(sc.s, "enter", sc.a)
|
||||
emit_scene_fn(sc.s, "exit", sc.b)
|
||||
emit_scene_fn(sc.s, "enter", sc.a, sc.ty)
|
||||
emit_scene_fn(sc.s, "exit", sc.b, sc.ty)
|
||||
i = i + 1
|
||||
}
|
||||
g_cur_scene = null
|
||||
if len(g_scenes) > 0 { emit_scene_leave_fn() } # a program without scenes has no @L_scene
|
||||
}
|
||||
|
||||
# @L_scene_leave(): run the on-exit hook of whichever scene is live — what a
|
||||
# `become` outside any scene's own handlers (a global handler, an @On listener,
|
||||
# a plain function) calls, since it cannot know the leaving scene at compile time.
|
||||
function emit_scene_leave_fn() -> void {
|
||||
emit("define void @L_scene_leave() {\nentry:\n")
|
||||
emit(" %s = load i32, ptr @L_scene\n")
|
||||
var i = 0
|
||||
while i < len(g_scenes) {
|
||||
let sc = g_scenes[i]
|
||||
let k = itoa(i)
|
||||
emit(" %is"); emit(k); emit(" = icmp eq i32 %s, "); emit(itoa(sc.ival)); emit("\n")
|
||||
emit(" br i1 %is"); emit(k); emit(", label %leave"); emit(k); emit(", label %next"); emit(k); emit("\n")
|
||||
emit("leave"); emit(k); emit(":\n call void @scene_exit_"); emit(sc.s); emit("()\n ret void\n")
|
||||
emit("next"); emit(k); emit(":\n")
|
||||
i = i + 1
|
||||
}
|
||||
emit(" ret void\n}\n\n")
|
||||
}
|
||||
|
||||
# @OnDespawn(Model) hooks compile to `@on_despawn_<Model>(entity, reason)`
|
||||
|
|
@ -443,6 +602,7 @@ function emit_event_fns() -> void {
|
|||
|
||||
# --- @ev_<E>(payload): fire compile-time listeners, then foreign ones
|
||||
ll_t = 0; ll_lbl = 0; g_term = false; loc_reset(); nloop = 0; nself = 0
|
||||
g_cur_scene = null # a listener belongs to no scene: `become` leaves the live one
|
||||
ret_ty = "void"
|
||||
let fbody = buf_new()
|
||||
falloc = buf_new()
|
||||
|
|
|
|||
|
|
@ -12,6 +12,22 @@ function machine_var(a: Node) -> Node {
|
|||
return null
|
||||
}
|
||||
|
||||
# the value a state dispatches on: its explicit `= expr`; else, when the machine's
|
||||
# store is a `var` declared with an enum type, the variant named like the state
|
||||
# (`var mode: HeroState` + `state Rolling` -> HeroState.Rolling); else its ordinal.
|
||||
function machine_state_value(m: Node, state: Node) -> pointer {
|
||||
if (state.b != null) { return emit_expr(state.b).code }
|
||||
let gv = machine_var(m.a)
|
||||
if (gv != null) {
|
||||
if (gv.ty != null) {
|
||||
let ord = enum_ordinal(gv.ty, state.s)
|
||||
if ord >= 0 { return itoa(ord) }
|
||||
if (find_enum(gv.ty) != null) { perr(`machine state {state.s} is not a variant of enum {gv.ty}`) }
|
||||
}
|
||||
}
|
||||
return itoa(state.ival)
|
||||
}
|
||||
|
||||
function emit_machine(st: Node) -> void {
|
||||
let gv = machine_var(st.a)
|
||||
var s = ""
|
||||
|
|
@ -26,8 +42,8 @@ function emit_machine(st: Node) -> void {
|
|||
var i = 0
|
||||
while i < len(st.kids) {
|
||||
let state = st.kids[i]
|
||||
let v = emit_expr(state.b)
|
||||
let c = emit_bind(`icmp eq i32 {s}, {v.code}`)
|
||||
let v = machine_state_value(st, state)
|
||||
let c = emit_bind(`icmp eq i32 {s}, {v}`)
|
||||
let body = lbl("sbody"); let nxt = lbl("sarm")
|
||||
emit(" br i1 "); emit(c); emit(", label %"); emit(body); emit(", label %"); emit(nxt); emit("\n")
|
||||
emit(body); emit(":\n"); g_term = false
|
||||
|
|
@ -53,19 +69,22 @@ function emit_become(st: Node) -> void {
|
|||
while i < len(m.kids) { if (m.kids[i].s == st.s) { target = m.kids[i] }; i = i + 1 }
|
||||
if (target != null) {
|
||||
let gv = machine_var(m.a)
|
||||
let sv = emit_expr(target.b)
|
||||
let sv = machine_state_value(m, target)
|
||||
if (gv != null) {
|
||||
emit(" store i32 "); emit(sv.code); emit(", ptr @g_"); emit(m.a.s); emit("\n")
|
||||
emit(" store i32 "); emit(sv); emit(", ptr @g_"); emit(m.a.s); emit("\n")
|
||||
} else {
|
||||
let regv = emit_expr(m.a)
|
||||
emit(" call void @fn_rt_set_reg(i32 "); emit(regv.code); emit(", i32 "); emit(sv.code); emit(")\n")
|
||||
emit(" call void @fn_rt_set_reg(i32 "); emit(regv.code); emit(", i32 "); emit(sv); emit(")\n")
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
let sc = find_scene(st.s) # else a scene transition
|
||||
if (sc == null) { perr(`become: no state or scene {st.s}`) }
|
||||
# leave the scene we are in: statically known inside a scene's own handler, else
|
||||
# (a global handler, an event listener, a function) whichever scene is live now
|
||||
if (g_cur_scene != null) { emit(" call void @scene_exit_"); emit(g_cur_scene.s); emit("()\n") }
|
||||
else { emit(" call void @L_scene_leave()\n") }
|
||||
emit(" store i32 "); emit(itoa(sc.ival)); emit(", ptr @L_scene\n")
|
||||
emit(" call void @scene_enter_"); emit(sc.s); emit("()\n")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,16 @@
|
|||
# emit_spawn.ludic — spawn / despawn / self(), and seeding a component's fields
|
||||
# from its declared defaults plus any per-spawn overrides.
|
||||
|
||||
# run a hook body with `self()` bound to entity `e` (a constructor knows its entity)
|
||||
function emit_hook_with_self(e: pointer, body: Node) -> void {
|
||||
let sslot = emit_alloca("i32")
|
||||
emit(" store i32 "); emit(e); emit(", ptr "); emit(sslot); emit("\n")
|
||||
if nself < len(self_stk) { self_stk[nself] = sslot } else { push(self_stk, sslot) }
|
||||
nself = nself + 1
|
||||
emit_block(body)
|
||||
nself = nself - 1
|
||||
}
|
||||
|
||||
function emit_init_component(e: pointer, comp: pointer, rec: Node) -> void {
|
||||
let me = itoa(MAX_ENT)
|
||||
let c = find_comp(comp)
|
||||
|
|
@ -44,7 +54,7 @@ function emit_init_component(e: pointer, comp: pointer, rec: Node) -> void {
|
|||
let vslot = emit_alloca("ptr")
|
||||
emit(" store ptr "); emit(slot); emit(", ptr "); emit(vslot); emit("\n")
|
||||
loc_push(comp, vslot, comp)
|
||||
emit_block(ab)
|
||||
emit_hook_with_self(e, ab)
|
||||
nloc = save
|
||||
}
|
||||
# EV1: a @Public attach hook fires prop_<P>_attach with the entity
|
||||
|
|
@ -68,33 +78,72 @@ function emit_bind_props(model: Node, e: pointer) -> void {
|
|||
}
|
||||
}
|
||||
|
||||
# the record for component `cn` in a spawn or prefab node (null when absent)
|
||||
function spawn_record(n: Node, cn: pointer) -> Node {
|
||||
if (n == null) { return null }
|
||||
var i = 0
|
||||
while i < len(n.kids) { if (n.kids[i].s == cn) { return n.kids[i].a }; i = i + 1 }
|
||||
return null
|
||||
}
|
||||
# a prefab's preset fields with the spawn's own overrides after them (later wins)
|
||||
function merge_records(base: Node, over: Node) -> Node {
|
||||
if (base == null) { return over }
|
||||
if (over == null) { return base }
|
||||
let m = node(E_REC)
|
||||
var i = 0
|
||||
while i < len(base.kids) { push(m.kids, base.kids[i]); i = i + 1 }
|
||||
i = 0
|
||||
while i < len(over.kids) { push(m.kids, over.kids[i]); i = i + 1 }
|
||||
return m
|
||||
}
|
||||
|
||||
# the model behind a spawn name: a model itself, or the model at the end of a
|
||||
# prefab chain (`prefab Grunt: Foe`, `prefab Foe: Creature` -> Creature)
|
||||
function spawn_model(name: pointer) -> pointer {
|
||||
var n = name
|
||||
var depth = 0
|
||||
while true {
|
||||
let p = find_prefab(n)
|
||||
if (p == null) { return n }
|
||||
n = p.ty
|
||||
depth = depth + 1
|
||||
if depth > 16 { perr(`prefab {name}: the chain of prefabs never reaches a model`) }
|
||||
}
|
||||
return n
|
||||
}
|
||||
# the preset record for component `cn` along a prefab chain, base prefabs first
|
||||
function prefab_record(name: pointer, cn: pointer) -> Node {
|
||||
let p = find_prefab(name)
|
||||
if (p == null) { return null }
|
||||
return merge_records(prefab_record(p.ty, cn), spawn_record(p, cn))
|
||||
}
|
||||
|
||||
function emit_spawn(st: Node) -> pointer {
|
||||
let e = emit_bind("call i32 @L_alloc()")
|
||||
let ak = find_arch_id(st.s)
|
||||
let model = spawn_model(st.s)
|
||||
let ak = find_arch_id(model)
|
||||
if (find_prefab(st.s) != null) and (ak == 0) { perr(`prefab {st.s}: unknown model {model}`) }
|
||||
if ak > 0 {
|
||||
let me = itoa(MAX_ENT)
|
||||
let kp = nreg(); emit(" "); emit(kp); emit(" = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_kind, i32 0, i32 "); emit(e); emit("\n")
|
||||
emit(" store i32 "); emit(itoa(ak)); emit(", ptr "); emit(kp); emit("\n")
|
||||
let arch = find_arch(st.s)
|
||||
let arch = find_arch(model)
|
||||
var c = 0
|
||||
while c < len(arch.kids) {
|
||||
let cn = arch.kids[c].s
|
||||
var rec = null
|
||||
var i = 0
|
||||
while i < len(st.kids) { if (st.kids[i].s == cn) { rec = st.kids[i].a }; i = i + 1 }
|
||||
emit_init_component(e, cn, rec)
|
||||
emit_init_component(e, cn, merge_records(prefab_record(st.s, cn), spawn_record(st, cn)))
|
||||
c = c + 1
|
||||
}
|
||||
let ob = onspawn_body(st.s) # @OnSpawn(Model) hook runs after init
|
||||
let ob = onspawn_body(model) # @OnSpawn(Model) hook runs after init
|
||||
if (ob != null) {
|
||||
let save = nloc
|
||||
emit_bind_props(arch, e)
|
||||
emit_block(ob)
|
||||
emit_hook_with_self(e, ob)
|
||||
nloc = save
|
||||
}
|
||||
# EV1: a @Public spawn hook also fires the public event model_<M>_spawn, so
|
||||
# mods (native or foreign, over the ABI) see the entity born.
|
||||
let sev = `model_{st.s}_spawn`
|
||||
let sev = `model_{model}_spawn`
|
||||
if (find_event(sev) != null) { emit(" call void @ev_"); emit(sev); emit("(i32 "); emit(e); emit(")\n") }
|
||||
} else {
|
||||
var i = 0
|
||||
|
|
@ -178,6 +227,11 @@ function emit_detach(st: Node) -> void {
|
|||
# enable/disable. `<P> on <e>` toggles a property's has-flag on an entity (its
|
||||
# data persists, so re-enabling restores it, and queries already skip a cleared
|
||||
# flag). A bare `<Model>` / `<Handler>` flips a global enabled flag.
|
||||
function find_sys(name: pointer) -> Node {
|
||||
var i = 0
|
||||
while i < len(prog) { let d = prog[i]; if d.kind == N_SYS and (d.s == name) { return d }; i = i + 1 }
|
||||
return null
|
||||
}
|
||||
function emit_toggle(st: Node) -> void {
|
||||
var val = "0"; if st.ival == 1 { val = "1" }
|
||||
if (st.ty != null) and (st.ty == "system") { return } # `disable system <fn>` is compile-time (handled in emit_one_engine_system); no runtime code
|
||||
|
|
@ -208,6 +262,11 @@ function emit_toggle(st: Node) -> void {
|
|||
if (find_event(tev) != null) { emit(" call void @ev_"); emit(tev); emit("(i32 "); emit(ev.code); emit(")\n") }
|
||||
} else {
|
||||
var g = "@HE_"; if is_model(st.s) { g = "@ME_" } # model vs handler
|
||||
emit(" store i32 "); emit(val); emit(", ptr "); emit(g); emit(st.s); emit("\n")
|
||||
var hname = st.s
|
||||
if (g == "@HE_") and (find_sys(hname) == null) and (g_cur_scene != null) { # a scene's own handler, by its bare name
|
||||
let qualified = `{g_cur_scene.s}_{hname}`
|
||||
if (find_sys(qualified) != null) { hname = qualified }
|
||||
}
|
||||
emit(" store i32 "); emit(val); emit(", ptr "); emit(g); emit(hname); emit("\n")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,29 @@ function ui_prop(w: Node, key: pointer) -> Node {
|
|||
while i < len(w.b.kids) { if (w.b.kids[i].s == key) { return w.b.kids[i].a }; i = i + 1 }
|
||||
return null
|
||||
}
|
||||
# a widget's props plus the text props (font, size, fg, align) it inherits from the
|
||||
# nearest ancestor that sets them — so a panel states the menu's look once.
|
||||
function ui_props_with_inherited(i: int) -> []Node {
|
||||
let w = uiw[i]
|
||||
let out = new []Node
|
||||
var p = 0
|
||||
while p < len(w.b.kids) { push(out, w.b.kids[p]); p = p + 1 }
|
||||
let keys = new []pointer
|
||||
push(keys, "font"); push(keys, "size"); push(keys, "fg"); push(keys, "align")
|
||||
var k = 0
|
||||
while k < len(keys) {
|
||||
if ui_prop(w, keys[k]) == null {
|
||||
var a = uiw_parent[i]
|
||||
while a >= 0 {
|
||||
let v = ui_prop(uiw[a], keys[k])
|
||||
if (v != null) { let fi = node(E_FINIT); fi.s = keys[k]; fi.a = v; push(out, fi); a = 0 - 1 }
|
||||
else { a = uiw_parent[a] }
|
||||
}
|
||||
}
|
||||
k = k + 1
|
||||
}
|
||||
return out
|
||||
}
|
||||
function ui_flatten(w: Node, parent: int) -> void {
|
||||
let idx = len(uiw)
|
||||
push(uiw, w); push(uiw_parent, parent)
|
||||
|
|
@ -41,6 +64,7 @@ function has_ui() -> bool {
|
|||
|
||||
# index of a UI_<name>: a ui block's root, or a widget's id=
|
||||
function ui_index_of(nm: pointer) -> int {
|
||||
if uiw == null { ui_flatten_all() } # a UI_ id may be read before @ui_build is emitted (a function, a listener)
|
||||
let s = nm[3..len(nm)] # strip "UI_"
|
||||
let u = 0; var bi = 0
|
||||
var i = 0
|
||||
|
|
@ -86,12 +110,13 @@ function emit_ui_build() -> void {
|
|||
ll_ui_set(i, 0, itoa(t))
|
||||
ll_ui_set(i, 1, itoa(uiw_parent[i]))
|
||||
if t == 4 { ll_ui_set(i, 17, "1") }
|
||||
let props = ui_props_with_inherited(i)
|
||||
var p = 0
|
||||
while p < len(w.b.kids) {
|
||||
let pr = w.b.kids[p]
|
||||
while p < len(props) {
|
||||
let pr = props[p]
|
||||
let k = pr.s
|
||||
let v = pr.a
|
||||
if (k == "id") { p = p + 1; continue }
|
||||
if (k == "id") or (k == "goto") { p = p + 1; continue }
|
||||
if (k == "text") {
|
||||
let sv = emit_expr(v)
|
||||
emit(" call void @fn_rt_ui_static_text(i32 "); emit(itoa(i)); emit(", ptr "); emit(sv.code); emit(")\n")
|
||||
|
|
|
|||
|
|
@ -422,6 +422,7 @@ function emit_tick_helpers() -> void {
|
|||
emit("define void @L_tick_render() {\nentry:\n")
|
||||
ll_t = 0; ll_lbl = 0
|
||||
emit_calls_for_phase("Render")
|
||||
emit_calls_for_phase("Overlay")
|
||||
emit(" ret void\n}\n\n")
|
||||
}
|
||||
|
||||
|
|
@ -433,6 +434,8 @@ function emit_game_defs() -> void {
|
|||
while i < len(prog) { if prog[i].kind == N_SYS { emit_system_fn(prog[i]) }; i = i + 1 }
|
||||
emit_despawn_hooks()
|
||||
emit_scene_hooks()
|
||||
if has_countdowns() { emit_countdown_system() } # `countdown` fields (see emit_game.ludic)
|
||||
if has_prefabs() { emit_prefab_fns() } # @L_prefab_<Name> + @L_spawn_prefab(name)
|
||||
emit_tick_helpers()
|
||||
}
|
||||
|
||||
|
|
@ -443,6 +446,7 @@ function emit_game_main() -> void {
|
|||
emit(" store i32 %argc, ptr @L_argc\n")
|
||||
emit(" store ptr %argv, ptr @L_argv\n")
|
||||
if (find_fn("rt_init") != null) { emit(" call void @fn_rt_init()\n") }
|
||||
emit(" call void @L_init_globals()\n")
|
||||
emit_calls_for_phase("Start")
|
||||
# enter the start scene once, after boot: store its id and run its on-enter.
|
||||
if len(g_scenes) > 0 {
|
||||
|
|
@ -482,6 +486,12 @@ function emit_game_main() -> void {
|
|||
emit(" store i32 "); emit(k); emit(", ptr @L_key\n")
|
||||
}
|
||||
}
|
||||
# a program with `ui` blocks: navigate the open UI from this frame's key and emit
|
||||
# UiClicked on activation — so a scene never polls ui_tick by hand.
|
||||
if has_ui() and (find_fn("rt_ui_tick") != null) {
|
||||
let uk = emit_bind("load i32, ptr @L_key")
|
||||
emit(" call void @fn_rt_ui_tick(i32 "); emit(uk); emit(")\n")
|
||||
}
|
||||
emit_calls_for_phase("Input")
|
||||
emit_calls_for_phase("FixedUpdate")
|
||||
emit_calls_for_phase("Update")
|
||||
|
|
@ -495,6 +505,9 @@ function emit_game_main() -> void {
|
|||
emit(" call void @fn_rt_clear(i32 "); emit(itoa(g_clear_color)); emit(")\n")
|
||||
}
|
||||
emit_calls_for_phase("Render")
|
||||
# Overlay: HUD / menus drawn after every engine Render system (sprites, lights), so
|
||||
# game UI is never painted under an actor. Presented together with the frame.
|
||||
emit_calls_for_phase("Overlay")
|
||||
if g_has_clear_color and (find_fn("rt_present") != null) {
|
||||
emit(" call void @fn_rt_present()\n")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ function is_ivec_ns(meth: pointer) -> bool {
|
|||
if (meth == "make") or (meth == "zero") or (meth == "x") or (meth == "y") { return true }
|
||||
if (meth == "add") or (meth == "sub") or (meth == "scale") or (meth == "dot") { return true }
|
||||
if (meth == "equal") or (meth == "manhattan") or (meth == "to_vector") { return true }
|
||||
if (meth == "distance2") or (meth == "within") or (meth == "heading") or (meth == "along") or (meth == "step") { return true }
|
||||
return false
|
||||
}
|
||||
|
||||
|
|
@ -39,6 +40,34 @@ function emit_ivec_ns(meth: pointer, e: Node) -> Val {
|
|||
let v = emit_expr(e.kids[0])
|
||||
return val(vec_y(v.code), "int")
|
||||
}
|
||||
if (meth == "distance2") or (meth == "within") { # squared distance; within(a, b, radius)
|
||||
let a = emit_expr(e.kids[0]); let b = emit_expr(e.kids[1])
|
||||
let dx = emit_bind(`sub i32 {vec_x(b.code)}, {vec_x(a.code)}`)
|
||||
let dy = emit_bind(`sub i32 {vec_y(b.code)}, {vec_y(a.code)}`)
|
||||
let xx = emit_bind(`mul i32 {dx}, {dx}`)
|
||||
let yy = emit_bind(`mul i32 {dy}, {dy}`)
|
||||
let d2 = emit_bind(`add i32 {xx}, {yy}`)
|
||||
if (meth == "distance2") { return val(d2, "int") }
|
||||
let r = emit_expr(e.kids[2])
|
||||
let rr = emit_bind(`mul i32 {r.code}, {r.code}`)
|
||||
let c = emit_bind(`icmp sle i32 {d2}, {rr}`)
|
||||
return val(emit_bind(`zext i1 {c} to i32`), "bool")
|
||||
}
|
||||
if (meth == "heading") { # degrees from a to b (0 = +x, 90 = +y)
|
||||
if find_fn("rt_ivec_heading") == null { perr("IVec2.heading needs the engine runtime") }
|
||||
let a = emit_expr(e.kids[0]); let b = emit_expr(e.kids[1])
|
||||
return val(emit_bind(`call i32 @fn_rt_ivec_heading(i64 {a.code}, i64 {b.code})`), "int")
|
||||
}
|
||||
if (meth == "along") { # the point `distance` along `degrees` from origin
|
||||
if find_fn("rt_ivec_along") == null { perr("IVec2.along needs the engine runtime") }
|
||||
let o = emit_expr(e.kids[0]); let d = emit_expr(e.kids[1]); let n = emit_expr(e.kids[2])
|
||||
return val(emit_bind(`call i64 @fn_rt_ivec_along(i64 {o.code}, i32 {d.code}, i32 {n.code})`), "IVec2")
|
||||
}
|
||||
if (meth == "step") { # a -1/0/1 unit step along `degrees`
|
||||
if find_fn("rt_ivec_step") == null { perr("IVec2.step needs the engine runtime") }
|
||||
let d = emit_expr(e.kids[0])
|
||||
return val(emit_bind(`call i64 @fn_rt_ivec_step(i32 {d.code})`), "IVec2")
|
||||
}
|
||||
if (meth == "add") { # component-wise a + b
|
||||
let a = emit_expr(e.kids[0]); let b = emit_expr(e.kids[1])
|
||||
let ax = vec_x(a.code); let ay = vec_y(a.code); let bx = vec_x(b.code); let by = vec_y(b.code)
|
||||
|
|
|
|||
34
selfhost/backend/stdlib/emit_key.ludic
Normal file
34
selfhost/backend/stdlib/emit_key.ludic
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
# emit_key.ludic — `Key.Name` -> the key code the input layer delivers, folded at
|
||||
# compile time (like Color.*). Letters are the lowercase character (what the held-key
|
||||
# set stores), digits their ASCII, plus the specials. Arrow keys alias WASD because the
|
||||
# platform layer maps them that way; Escape is 27 (0.3, #88).
|
||||
function key_lookup(name: pointer) -> int {
|
||||
if (name == "Space") { return 32 }
|
||||
if (name == "Enter") { return 10 }
|
||||
if (name == "Return") { return 10 }
|
||||
if (name == "Escape") { return 27 }
|
||||
if (name == "Esc") { return 27 }
|
||||
if (name == "Shift") { return 16 }
|
||||
if (name == "Tab") { return 9 }
|
||||
if (name == "Backspace") { return 8 }
|
||||
if (name == "Up") { return 119 }
|
||||
if (name == "Down") { return 115 }
|
||||
if (name == "Left") { return 97 }
|
||||
if (name == "Right") { return 100 }
|
||||
if (name == "Digit0") { return 48 }
|
||||
if (name == "Digit1") { return 49 }
|
||||
if (name == "Digit2") { return 50 }
|
||||
if (name == "Digit3") { return 51 }
|
||||
if (name == "Digit4") { return 52 }
|
||||
if (name == "Digit5") { return 53 }
|
||||
if (name == "Digit6") { return 54 }
|
||||
if (name == "Digit7") { return 55 }
|
||||
if (name == "Digit8") { return 56 }
|
||||
if (name == "Digit9") { return 57 }
|
||||
# a single capital letter names that letter key: Key.A .. Key.Z
|
||||
if (name[1] == 0) {
|
||||
let c = name[0]
|
||||
if (c >= 65) and (c <= 90) { return c + 32 }
|
||||
}
|
||||
return 0 - 1
|
||||
}
|
||||
|
|
@ -12,7 +12,7 @@ function list_elem_addr(h: pointer, elt: pointer, idx: pointer) -> pointer {
|
|||
}
|
||||
|
||||
function is_list_ns(meth: pointer) -> bool {
|
||||
if (meth == "len") or (meth == "push") or (meth == "clear") { return true }
|
||||
if (meth == "len") or (meth == "push") or (meth == "clear") or (meth == "sample") { return true }
|
||||
if (meth == "first") or (meth == "last") or (meth == "pop") or (meth == "swap") { return true }
|
||||
if (meth == "contains") or (meth == "index_of") or (meth == "reverse") { return true }
|
||||
if (meth == "insert") or (meth == "remove_at") or (meth == "remove") or (meth == "sort") { return true }
|
||||
|
|
@ -228,6 +228,12 @@ function sort_fn_arg(meth: pointer, e: Node) -> pointer {
|
|||
function emit_list_ns(meth: pointer, e: Node) -> Val {
|
||||
if (meth == "len") { return emit_len(e) } # same header length as len(s)
|
||||
if (meth == "push") { return emit_push(e) } # same as push(s, v)
|
||||
if (meth == "sample") { # count random picks from an int slice (runtime)
|
||||
let pool = emit_expr(e.kids[0])
|
||||
let cnt = emit_expr(e.kids[1])
|
||||
if find_fn("rt_list_sample") == null { perr("List.sample needs the engine runtime (a game, or import ludic.core)") }
|
||||
return val(emit_bind(`call ptr @fn_rt_list_sample(ptr {pool.code}, i32 {cnt.code})`), "[]int")
|
||||
}
|
||||
let s = emit_expr(e.kids[0])
|
||||
if not is_slice_ty(s.ty) { perr(`List.{meth} needs a slice`) }
|
||||
let el = slice_elem(s.ty)
|
||||
|
|
|
|||
|
|
@ -31,6 +31,8 @@ const S_EMIT: int = 49 # emit E(field: v, ...) — fire event E (calls
|
|||
const S_CANCEL: int = 50 # cancel — inside a listener, veto a `cancellable` event
|
||||
const N_TEST: int = 51 # test "name" { ... } — a named test block (the testing framework)
|
||||
# s=name a=body block line=source line of the test
|
||||
const N_PREFAB: int = 53 # prefab Name: Model { Comp { field: v }, … } — a model plus preset fields
|
||||
# s=name ty=model kids=E_FINIT component records (like spawn)
|
||||
const E_TRY: int = 52 # try EXPR else { ... } — recover a fallible result as a value (issue #46)
|
||||
# a=the fallible (result-typed) expression b=else block (its
|
||||
# trailing expression is the fallback) line=source line
|
||||
|
|
|
|||
|
|
@ -150,6 +150,7 @@ function p_primary() -> Node {
|
|||
if (t.text == "false") { let n = node(E_BOOL); n.ival = 0; pi = pi + 1; return n }
|
||||
if (t.text == "null") { pi = pi + 1; return node(E_NULL) }
|
||||
if (t.text == "new") { pi = pi + 1; let n = node(E_NEW); n.s = ptype(); if is_op("{") { n.a = record() }; return n }
|
||||
if (t.text == "spawn") { return parse_spawn() } # spawn as an expression: the new entity
|
||||
# try EXPR else { ... } — evaluate a fallible (result-typed) expression; on
|
||||
# `ok` the whole expression is its payload, on `err` the else block runs (with
|
||||
# the message bound to `error`) and its trailing expression is the fallback.
|
||||
|
|
@ -196,7 +197,7 @@ function p_postfix() -> Node {
|
|||
if e.a.kind == E_ID and e.a.s == "Input" and (e.s == "bind" or e.s == "rebind" or e.s == "poll" or e.s == "down" or e.s == "pressed" or e.s == "record" or e.s == "replay" or e.s == "action" or e.s == "bind_pad" or e.s == "active" or e.s == "just_pressed" or e.s == "just_released") { g_uses_input = true }
|
||||
# #50 device layer — any of the multi-key / analog / mouse / gamepad / touch
|
||||
# methods also splices input.ludic (Input.key stays bare, no runtime).
|
||||
if e.a.kind == E_ID and e.a.s == "Input" and (e.s == "key_down" or e.s == "key_pressed" or e.s == "key_released" or e.s == "press" or e.s == "release" or e.s == "axis" or e.s == "axis_i" or e.s == "vector" or e.s == "strength" or e.s == "mouse_x" or e.s == "mouse_y" or e.s == "mouse_dx" or e.s == "mouse_dy" or e.s == "mouse_down" or e.s == "wheel" or e.s == "set_mouse" or e.s == "pad_connected" or e.s == "pad_button" or e.s == "pad_axis" or e.s == "set_pad" or e.s == "touch_count" or e.s == "touch_x" or e.s == "touch_y" or e.s == "set_touch" or e.s == "cursor_mode") { g_uses_input = true }
|
||||
if e.a.kind == E_ID and e.a.s == "Input" and (e.s == "key_down" or e.s == "key_pressed" or e.s == "key_released" or e.s == "press" or e.s == "release" or e.s == "axis" or e.s == "axis_i" or e.s == "vector" or e.s == "strength" or e.s == "mouse_x" or e.s == "mouse_y" or e.s == "mouse_dx" or e.s == "mouse_dy" or e.s == "mouse_down" or e.s == "wheel" or e.s == "set_mouse" or e.s == "pad_connected" or e.s == "pad_button" or e.s == "pad_axis" or e.s == "move_i" or e.s == "set_pad" or e.s == "touch_count" or e.s == "touch_x" or e.s == "touch_y" or e.s == "set_touch" or e.s == "cursor_mode") { g_uses_input = true }
|
||||
# Anim.play/clip/on_frame/fired + Motion.to (#48): the ergonomic writes over
|
||||
# the SpriteAnim/Motion components live in systems.ludic and use the world
|
||||
# table, so splice it and force the reflection ABI even if the game leaves
|
||||
|
|
@ -210,6 +211,7 @@ function p_postfix() -> Node {
|
|||
# Tween.to/chain/delay/value/stop/parallel (#48): the fluent stateful handles
|
||||
# live in tween.ludic, advanced by an engine-owned system each Update tick.
|
||||
if e.a.kind == E_ID and e.a.s == "Tween" and (e.s == "to" or e.s == "chain" or e.s == "delay" or e.s == "value" or e.s == "stop" or e.s == "parallel") { g_uses_tween_rt = true }
|
||||
if e.a.kind == E_ID and e.a.s == "Fx" { g_uses_fx = true } # Fx.* -> splice fx.ludic + tick/draw it each frame
|
||||
}
|
||||
else { if is_op("[") { pi = pi + 1; let lo = expr()
|
||||
if is_op("..") { pi = pi + 1; let sl = node(E_SLICE); sl.a = e; sl.b = lo; sl.c = expr(); eat_op("]"); e = sl } # s[a..b] substring
|
||||
|
|
@ -340,7 +342,8 @@ function stmt_body() -> Node {
|
|||
let stkw = eat_id() # 'state'
|
||||
let s = node(S_STATE); s.s = eat_id()
|
||||
if is_op("=") { pi = pi + 1; s.b = expr() } # explicit value (still allowed)
|
||||
else { let iv = node(E_INT); iv.ival = sidx; s.b = iv } # else its ordinal
|
||||
s.ival = sidx # else: the enum variant of the same name
|
||||
# when the store is enum-typed, or this ordinal
|
||||
skipnl(); s.a = block()
|
||||
push(n.kids, s); sidx = sidx + 1 }
|
||||
eat_op("}"); return n
|
||||
|
|
@ -405,8 +408,24 @@ function parse_const() -> Node {
|
|||
pi = pi + 1; let n = node(N_CONST); n.s = eat_id(); eat_op(":"); n.ty = ptype(); eat_op("="); n.a = expr()
|
||||
return n
|
||||
}
|
||||
# the language's statement / declaration words cannot name a function (`new`,
|
||||
# `match`, `spawn` ...). Contextual words the parser only recognises in position
|
||||
# (`emit`, `on`, `phase`, `start`, `export`, ...) stay usable — the compiler itself
|
||||
# has a function called `emit`.
|
||||
function is_reserved_word(w: pointer) -> bool {
|
||||
if (w == "new") or (w == "match") or (w == "machine") or (w == "become") or (w == "spawn") or (w == "despawn") { return true }
|
||||
if (w == "query") or (w == "cancel") or (w == "scene") or (w == "layer") or (w == "handler") or (w == "property") { return true }
|
||||
if (w == "model") or (w == "enum") or (w == "event") or (w == "namespace") or (w == "function") or (w == "let") { return true }
|
||||
if (w == "prefab") { return true }
|
||||
if (w == "var") or (w == "const") or (w == "if") or (w == "else") or (w == "while") or (w == "for") or (w == "return") { return true }
|
||||
if (w == "import") or (w == "program") or (w == "entry") or (w == "ui") or (w == "break") or (w == "continue") { return true }
|
||||
if (w == "and") or (w == "or") or (w == "not") or (w == "true") or (w == "false") or (w == "null") { return true }
|
||||
return false
|
||||
}
|
||||
|
||||
function parse_fn() -> Node {
|
||||
pi = pi + 1; let n = node(N_FN); n.s = eat_id(); eat_op("(")
|
||||
if is_reserved_word(n.s) { perr(`'{n.s}' is a reserved word and cannot name a function`) }
|
||||
while not is_op(")") {
|
||||
let p = node(N_PARAM); p.s = eat_id(); eat_op(":"); p.ty = ptype(); push(n.kids, p)
|
||||
if is_op(",") { pi = pi + 1 }
|
||||
|
|
@ -487,7 +506,7 @@ function dir_of(path: pointer) -> pointer {
|
|||
if last < 0 { return "" }
|
||||
return path[0..0 + (last + 1)]
|
||||
}
|
||||
function path_join(dir: pointer, rel: pointer) -> pointer {
|
||||
function join_path(dir: pointer, rel: pointer) -> pointer {
|
||||
if rel[0] == 47 { return rel } # absolute
|
||||
return (dir + rel)
|
||||
}
|
||||
|
|
@ -526,6 +545,7 @@ function warn_draw_sprite(name: pointer) -> void {
|
|||
}
|
||||
var g_uses_anim_rt: bool = false # Anim.play/clip/on_frame/fired or Motion.to (#48) -> splice systems.ludic + force the reflection ABI
|
||||
var g_uses_tween_rt: bool = false # Tween.to/chain/delay/… (#48) -> splice tween.ludic + run esys_tween each Update
|
||||
var g_uses_fx: bool = false # Fx.sparks/number/clear -> splice fx.ludic; fx_tick each Update, fx_draw each Render
|
||||
var g_uses_audio: bool = false # Audio.* (#22) -> splice audio.ludic; a windowed build also links audio.ll + AVFoundation
|
||||
var g_uses_http: bool = false # Http.* (#6) -> splice http.ludic; links http.ll + Foundation (macOS)
|
||||
# issue #64: functions marked @System(Phase) in a prebuilt binary module — the
|
||||
|
|
@ -674,6 +694,7 @@ function parse_one_decl() -> void {
|
|||
push(prog, m); return
|
||||
}
|
||||
if is_id("scene") { parse_scene(); return } # layers push handlers into prog; scene -> g_scenes
|
||||
if is_id("prefab") { push(prog, parse_prefab()); return } # a model with preset component fields
|
||||
if is_id("handler") {
|
||||
let h = parse_system()
|
||||
if (role != null) { if (role == "server") { h.ival = 1 } else { h.ival = 2 } } # N5: @Server=1 / @Predicted=2
|
||||
|
|
@ -762,8 +783,43 @@ function is_runtime_path(rel: pointer) -> bool {
|
|||
return true
|
||||
}
|
||||
|
||||
# byte-wise `a < b`, for a stable import order of a directory's files
|
||||
function str_less(a: pointer, b: pointer) -> bool {
|
||||
var i = 0
|
||||
while true {
|
||||
let ca = a[i]; let cb = b[i]
|
||||
if ca != cb { return ca < cb }
|
||||
if ca == 0 { return false }
|
||||
i = i + 1
|
||||
}
|
||||
return false
|
||||
}
|
||||
# `import "dir/*.ludic"` — every .ludic file of the directory, in name order, so
|
||||
# a game lists its modules once instead of one import per file.
|
||||
function do_import_dir(rel: pointer) -> void {
|
||||
let dir = rel[0..len(rel) - 8] # strip "/*.ludic"
|
||||
let names = Fs.list(join_path(cur_dir, dir))
|
||||
if (names == null) { perr(`import: no directory {dir}`) }
|
||||
var i = 0
|
||||
while i < len(names) { # selection sort by name
|
||||
var best = i
|
||||
var j = i + 1
|
||||
while j < len(names) { if str_less(names[j], names[best]) { best = j }; j = j + 1 }
|
||||
let t = names[i]; names[i] = names[best]; names[best] = t
|
||||
i = i + 1
|
||||
}
|
||||
i = 0
|
||||
while i < len(names) {
|
||||
let nm = names[i]
|
||||
let n = len(nm)
|
||||
if (n > 6) and (nm[n - 6..n] == ".ludic") { do_import(dir + ("/") + nm) }
|
||||
i = i + 1
|
||||
}
|
||||
}
|
||||
|
||||
function do_import(rel: pointer) -> void {
|
||||
var full = path_join(cur_dir, rel)
|
||||
if (len(rel) > 8) and (rel[len(rel) - 8..len(rel)] == "/*.ludic") { do_import_dir(rel); return }
|
||||
var full = join_path(cur_dir, rel)
|
||||
var src = read_file(full)
|
||||
# #75 — the engine runtime (runtime/native/*) ships with the toolchain, not the
|
||||
# project. When an auto-spliced runtime import is not found relative to the build
|
||||
|
|
@ -785,6 +841,12 @@ function do_import(rel: pointer) -> void {
|
|||
if already_loaded(full) { return }
|
||||
push(loaded_paths, full)
|
||||
if (src == null) { perr(`cannot open import {full}`) }
|
||||
# the audio runtime can arrive through atlas.ludic's own import or the Assets
|
||||
# splice, not only through an Audio.* call in the game; a windowed build must
|
||||
# then link audio.ll + AVFoundation for its snd_* calls, so flag it here.
|
||||
let atail = "runtime/native/audio.ludic"
|
||||
let fl = len(full)
|
||||
if (fl >= len(atail)) and (full[fl - len(atail)..fl] == atail) { g_uses_audio = true }
|
||||
let saved_toks = toks; let saved_pi = pi; let saved_dir = cur_dir
|
||||
cur_dir = dir_of(full)
|
||||
lex(src) # resets the global token stream
|
||||
|
|
@ -876,9 +938,18 @@ function maybe_splice_runtime() -> void {
|
|||
if g_uses_atlas {
|
||||
cur_dir = ""
|
||||
do_import("runtime/native/core.ludic")
|
||||
do_import("runtime/native/audio.ludic") # Assets.enqueue loads .wav/.mp3 into the sound bank
|
||||
do_import("runtime/native/atlas.ludic")
|
||||
cur_dir = saved
|
||||
}
|
||||
# Fx.* — engine-owned sparks and floating numbers; drawn through the framebuffer,
|
||||
# so it links against core.ludic (do_import dedupes).
|
||||
if g_uses_fx {
|
||||
cur_dir = ""
|
||||
do_import("runtime/native/core.ludic")
|
||||
do_import("runtime/native/fx.ludic")
|
||||
cur_dir = saved
|
||||
}
|
||||
# Base64.* (#67) — base64 codec; pull in the DEFLATE inflater alongside it, so
|
||||
# a plain tool can run the full base64 -> zlib/gzip decode chain (inflate.ludic
|
||||
# is self-contained; do_import dedupes when a game already linked it via core).
|
||||
|
|
@ -986,6 +1057,11 @@ function maybe_splice_runtime() -> void {
|
|||
do_import("runtime/native/core.ludic")
|
||||
do_import("runtime/native/systems_sprite.ludic")
|
||||
}
|
||||
# TileSkin -> the engine tilemap-render system: draws the Map.* grid from glyph -> sprite skins.
|
||||
if find_comp("TileSkin") != null {
|
||||
do_import("runtime/native/core.ludic")
|
||||
do_import("runtime/native/systems_tileskin.ludic")
|
||||
}
|
||||
# Bounds -> the engine world-bounds system (#84). Self-contained (reflection
|
||||
# ABI only); its kill policy calls world_despawn, so force the @fn_world_despawn
|
||||
# helper to be emitted.
|
||||
|
|
@ -1038,6 +1114,7 @@ function parse_program() -> void {
|
|||
g_uses_base64 = false
|
||||
g_uses_tiled = false
|
||||
g_uses_atlas = false
|
||||
g_uses_fx = false
|
||||
g_uses_reflect_io = false
|
||||
g_tests = new []Node
|
||||
g_mod_sys_fn = new []pointer
|
||||
|
|
@ -1051,6 +1128,7 @@ function parse_program() -> void {
|
|||
push(g_esys_comp, "Motion"); push(g_esys_fn, "esys_motion"); push(g_esys_phase, "Update")
|
||||
push(g_esys_comp, "Body"); push(g_esys_fn, "esys_move"); push(g_esys_phase, "Update")
|
||||
push(g_esys_comp, "Bounds"); push(g_esys_fn, "esys_bounds"); push(g_esys_phase, "LateUpdate")
|
||||
push(g_esys_comp, "TileSkin"); push(g_esys_fn, "esys_tileskin"); push(g_esys_phase, "Render")
|
||||
push(g_esys_comp, "Sprite"); push(g_esys_fn, "esys_sprite"); push(g_esys_phase, "Render")
|
||||
push(g_esys_comp, "Light2D"); push(g_esys_fn, "esys_light2d"); push(g_esys_phase, "Render")
|
||||
g_namespaces = new []pointer
|
||||
|
|
|
|||
|
|
@ -146,6 +146,11 @@ function parse_queries_anno() -> Node {
|
|||
|
||||
function parse_spawn() -> Node {
|
||||
pi = pi + 1; let n = node(S_SPAWN); n.s = eat_id(); skipnl(); eat_op("{")
|
||||
parse_component_records(n)
|
||||
eat_op("}"); return n
|
||||
}
|
||||
# `Comp { field: val, ... }, Comp { ... }` — the body shared by spawn and prefab
|
||||
function parse_component_records(n: Node) -> void {
|
||||
while true {
|
||||
skipnl(); if is_op("}") { break }
|
||||
let ci = node(E_FINIT); ci.s = eat_id()
|
||||
|
|
@ -153,6 +158,13 @@ function parse_spawn() -> Node {
|
|||
push(n.kids, ci)
|
||||
if is_op(",") { pi = pi + 1 }
|
||||
}
|
||||
}
|
||||
# prefab Name: Model { Comp { field: val }, ... } — a model with preset fields.
|
||||
# `spawn Name { ... }` spawns the model with the prefab's fields, then the spawn's
|
||||
# own overrides on top; `Prefab.spawn(name: "Name")` does it by name at runtime.
|
||||
function parse_prefab() -> Node {
|
||||
pi = pi + 1; let n = node(N_PREFAB); n.s = eat_id(); eat_op(":"); n.ty = eat_id(); skipnl(); eat_op("{")
|
||||
parse_component_records(n)
|
||||
eat_op("}"); return n
|
||||
}
|
||||
|
||||
|
|
@ -169,9 +181,15 @@ function parse_scene() -> void {
|
|||
# optional modifiers after the name, any order: `start` (the boot scene) and
|
||||
# `public` (promote its on-enter/on-exit to scene_<S>_enter / scene_<S>_exit).
|
||||
var is_pub = false
|
||||
while is_id("start") or is_id("public") {
|
||||
var lasts: Node = null # lasts N then Next: a timed scene (a banner, a splash)
|
||||
var then_scene: pointer = null
|
||||
var loads_then: pointer = null # loads then Next: pump the Assets queue, draw a bar, go on when ready
|
||||
while is_id("start") or is_id("public") or is_id("shows") or is_id("lasts") or is_id("loads") {
|
||||
if is_id("start") { pi = pi + 1; g_start_scene = g_scene_count }
|
||||
else { pi = pi + 1; is_pub = true }
|
||||
else { if is_id("shows") { pi = pi + 1; n.ty = eat_id() } # shows Menu: the engine opens, renders and closes it
|
||||
else { if is_id("lasts") { pi = pi + 1; lasts = expr(); if not is_id("then") { perr("lasts N needs `then Scene`") }; pi = pi + 1; then_scene = eat_id() }
|
||||
else { if is_id("loads") { pi = pi + 1; if not is_id("then") { perr("loads needs `then Scene`") }; pi = pi + 1; loads_then = eat_id(); g_uses_atlas = true }
|
||||
else { pi = pi + 1; is_pub = true } } } }
|
||||
}
|
||||
g_scene_count = g_scene_count + 1
|
||||
if is_pub { ensure_event_empty(`scene_{n.s}_enter`); ensure_event_empty(`scene_{n.s}_exit`) }
|
||||
|
|
@ -191,8 +209,17 @@ function parse_scene() -> void {
|
|||
let ltag = node(E_ID); ltag.s = lname # the layer name, tagged onto each handler
|
||||
while true {
|
||||
skipnl(); if is_op("}") { break }
|
||||
var qspec: Node = null # `@Queries(...)` makes the handler run once per matching entity
|
||||
while is_op("@") {
|
||||
pi = pi + 1; let a = eat_id()
|
||||
if (a == "Queries") { qspec = parse_queries_anno() }
|
||||
else { perr(`a handler inside a layer may only carry @Queries (got @{a})`) }
|
||||
skipnl()
|
||||
}
|
||||
if not is_id("handler") { perr("expected 'handler' in layer") }
|
||||
let h = parse_system() # N_SYS: .s name, .ty phase, .a body
|
||||
if (qspec != null) { qspec.a = h.a; let wrap = node(N_BLOCK); push(wrap.kids, qspec); h.a = wrap }
|
||||
h.s = `{n.s}_{h.s}` # scene-qualified: two scenes may both have a `Draw` handler
|
||||
h.c = n # tag the owning scene (null = global)
|
||||
h.b = ltag # tag the owning layer (for enable/disable layer)
|
||||
push(prog, h)
|
||||
|
|
@ -204,9 +231,81 @@ function parse_scene() -> void {
|
|||
perr("expected 'on', 'layer' or '}' in scene")
|
||||
}
|
||||
eat_op("}")
|
||||
if (lasts != null) { scene_add_timer(n, lasts, then_scene) }
|
||||
if (loads_then != null) { scene_add_loader(n, loads_then) }
|
||||
push(g_scenes, n)
|
||||
}
|
||||
|
||||
# `scene S loads then T`: an Update handler that pumps the Assets queue and
|
||||
# `become`s T once it is ready, and a Render handler that draws the default bar.
|
||||
function scene_add_loader(sc: Node, next: pointer) -> void {
|
||||
let h = node(N_SYS); h.s = `{sc.s}_Loader`; h.ty = "Update"; h.c = sc
|
||||
let body = node(N_BLOCK)
|
||||
let pump = node(S_EXPR); let pc = node(E_CALL); let pm = node(E_MEMBER); let pid = node(E_ID); pid.s = "Assets"; pm.a = pid; pm.s = "pump"; pc.a = pm
|
||||
let n4 = node(E_INT); n4.ival = 4; push(pc.kids, n4); pump.a = pc; push(body.kids, pump)
|
||||
let go = node(S_IF); let rc = node(E_CALL); let rm = node(E_MEMBER); let rid = node(E_ID); rid.s = "Assets"; rm.a = rid; rm.s = "ready"; rc.a = rm; go.a = rc
|
||||
go.b = node(N_BLOCK); let bec = node(S_BECOME); bec.s = next; push(go.b.kids, bec); push(body.kids, go)
|
||||
h.a = body; push(prog, h)
|
||||
let d = node(N_SYS); d.s = `{sc.s}_LoaderBar`; d.ty = "Render"; d.c = sc
|
||||
let dbody = node(N_BLOCK); let dc = node(E_CALL); let did = node(E_ID); did.s = "assets_draw_progress"; dc.a = did
|
||||
let ds = node(S_EXPR); ds.a = dc; push(dbody.kids, ds); d.a = dbody; push(prog, d)
|
||||
}
|
||||
|
||||
# `scene S lasts N then T`: a counter var, set on enter, counted down by a
|
||||
# generated Update handler that `become`s T at zero — the splash / banner scene.
|
||||
function scene_add_timer(sc: Node, frames: Node, next: pointer) -> void {
|
||||
let counter = `scene_{sc.s}_left`
|
||||
let v = node(N_VAR); v.s = counter; v.ty = "int"; push(prog, v)
|
||||
if (sc.a == null) { sc.a = node(N_BLOCK) }
|
||||
let set = node(S_ASSIGN); set.s = "="; let lhs = node(E_ID); lhs.s = counter; set.a = lhs; set.b = frames
|
||||
let ins = new []Node; push(ins, set)
|
||||
var i = 0
|
||||
while i < len(sc.a.kids) { push(ins, sc.a.kids[i]); i = i + 1 }
|
||||
sc.a.kids = ins
|
||||
let h = node(N_SYS); h.s = `{sc.s}_Timer`; h.ty = "Update"; h.c = sc
|
||||
let body = node(N_BLOCK)
|
||||
let dec = node(S_ASSIGN); dec.s = "-="; let l2 = node(E_ID); l2.s = counter; dec.a = l2; let one = node(E_INT); one.ival = 1; dec.b = one
|
||||
push(body.kids, dec)
|
||||
let test = node(E_BIN); test.s = "<="; let l3 = node(E_ID); l3.s = counter; test.a = l3; let zero = node(E_INT); zero.ival = 0; test.b = zero
|
||||
let go = node(S_IF); go.a = test; go.b = node(N_BLOCK)
|
||||
let bec = node(S_BECOME); bec.s = next; push(go.b.kids, bec)
|
||||
push(body.kids, go)
|
||||
h.a = body
|
||||
push(prog, h)
|
||||
}
|
||||
|
||||
# `button id: Play goto: Scene`: a click changes scene. Every such button becomes a
|
||||
# generated UiClicked listener — `if id == UI_Name { become Scene }` — the same
|
||||
# code a game would write by hand.
|
||||
function ui_register_gotos() -> void {
|
||||
var i = 0
|
||||
while i < len(prog) {
|
||||
if prog[i].kind == N_UI and prog[i].ival == 1 { ui_gotos_in(prog[i].a) }
|
||||
i = i + 1
|
||||
}
|
||||
}
|
||||
function ui_gotos_in(w: Node) -> void {
|
||||
var idn: pointer = null
|
||||
var goto: pointer = null
|
||||
var p = 0
|
||||
while p < len(w.b.kids) {
|
||||
let pr = w.b.kids[p]
|
||||
if (pr.s == "id") and (pr.a.kind == E_ID) { idn = pr.a.s }
|
||||
if (pr.s == "goto") and (pr.a.kind == E_ID) { goto = pr.a.s }
|
||||
p = p + 1
|
||||
}
|
||||
if (idn != null) and (goto != null) {
|
||||
let body = node(N_BLOCK)
|
||||
let test = node(E_BIN); test.s = "=="; let l = node(E_ID); l.s = "id"; test.a = l; let r = node(E_ID); r.s = "UI_" + idn; test.b = r
|
||||
let go = node(S_IF); go.a = test; go.b = node(N_BLOCK)
|
||||
let bec = node(S_BECOME); bec.s = goto; push(go.b.kids, bec)
|
||||
push(body.kids, go)
|
||||
register_onlisten("UiClicked", body)
|
||||
}
|
||||
var k = 0
|
||||
while k < len(w.kids) { ui_gotos_in(w.kids[k]); k = k + 1 }
|
||||
}
|
||||
|
||||
# enum Name { A, B, C } — named int constants; a variant's value is its index.
|
||||
# Accessed as `Name.A` (a compile-time int), so it names magic-int value spaces
|
||||
# (state ids, menu selections, mode registers) without a runtime cost.
|
||||
|
|
|
|||
98760
selfhost/ludicc.seed.ll
98760
selfhost/ludicc.seed.ll
File diff suppressed because it is too large
Load diff
|
|
@ -38,7 +38,7 @@ function getenv_or(name: pointer, dflt: pointer) -> pointer {
|
|||
return v
|
||||
}
|
||||
|
||||
# guarantee a directory string ends in '/' so path_join concatenates cleanly
|
||||
# guarantee a directory string ends in '/' so join_path concatenates cleanly
|
||||
function ensure_slash(d: pointer) -> pointer {
|
||||
let n = len(d)
|
||||
if n == 0 { return d }
|
||||
|
|
@ -63,7 +63,7 @@ function chomp(s: pointer) -> pointer {
|
|||
# so a version change never requires reseeding the compiler.
|
||||
function show_version() -> void {
|
||||
let home = ensure_slash(getenv_or("LUDIC_HOME", dir_of(arg(0))))
|
||||
let v = read_file(path_join(home, "VERSION"))
|
||||
let v = read_file(join_path(home, "VERSION"))
|
||||
if (v == null) { print("ludic (version unknown)") }
|
||||
else { print(`ludic {chomp(v)}`) }
|
||||
exit(0)
|
||||
|
|
@ -124,6 +124,7 @@ entry {
|
|||
if fmt { exit(0) }
|
||||
|
||||
maybe_splice_runtime()
|
||||
ui_register_gotos() # `goto:` buttons -> generated UiClicked listeners
|
||||
|
||||
# a game gets a window by default; a plain program stays headless. An explicit
|
||||
# flag always wins. The stdout-IR path (no target) also stays headless, which
|
||||
|
|
@ -160,7 +161,7 @@ entry {
|
|||
# would otherwise warn about on every build.
|
||||
var cmd = `{cc} -O2 -Wno-override-module {ll}`
|
||||
if g_windowed {
|
||||
let cocoa = path_join(home, "runtime/native/cocoa.ll")
|
||||
let cocoa = join_path(home, "runtime/native/cocoa.ll")
|
||||
# GameController holds no symbol cocoa.ll references directly (its classes are
|
||||
# reached by name through objc_getClass), so a plain -framework link gets
|
||||
# dead-stripped; -needed_framework forces the load command so the class is
|
||||
|
|
@ -170,7 +171,7 @@ entry {
|
|||
# when the program actually uses it — the snd_* calls are is_windowed()-guarded
|
||||
# yet still live in a windowed build, so the backend must link.
|
||||
if g_uses_audio {
|
||||
let audio = path_join(home, "runtime/native/audio.ll")
|
||||
let audio = join_path(home, "runtime/native/audio.ll")
|
||||
cmd = `{cmd} {audio} -Wl,-needed_framework,AVFoundation`
|
||||
}
|
||||
}
|
||||
|
|
@ -178,7 +179,7 @@ entry {
|
|||
# reached by name). Works headless too, so it is outside the windowed block —
|
||||
# macOS-only for now, which is where the toolchain runs.
|
||||
if g_uses_http {
|
||||
let http = path_join(home, "runtime/native/http.ll")
|
||||
let http = join_path(home, "runtime/native/http.ll")
|
||||
cmd = `{cmd} {http} -Wl,-needed_framework,Foundation`
|
||||
}
|
||||
cmd = `{cmd} -o {out}`
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue