灵符lingfu

lingdev

↓ 0 downloads

LingDev: a Godot / Unity-style game editor written in Ling. 2-D and 3-D scenes, Flash-style timeline and cutscenes, HUDs over 3-D, Ling scripting, audio / music / MIDI, builds for Windows, Linux, macOS, web, Raspberry Pi and LingOS. Ships with Ling Agent (an N64-style spy FPS), Ling Breaker (a 3-D breakout) and Moko's Picture Book (a kawaii storybook platformer).

Windows · x86-64Linux · x86-64LingOS · x86-64LingOS · Pi 5 ARM64Web · WASM
Keywords game-engine, editor, 3d, 2d, cutscene, timeline, ling, lingos, raspberry-pi
lingfu install lingdev
LicenseApache-2.0 OR MIT
Authorsling-lin
READMECode / FilesVersionsDependency GraphDependentsSecurityBuildsSettings

Code / Files (136)

ling/rt/rt_scene.ling
# rt_scene.ling - the scene document: nodes, keyframes and cutscene clips, in the native store.
# Shared by the editor and exported games (the player loads the same text the editor saves).
#
# Nodes are rows of parallel store lists <ns>n_<col>; every node has a stable numeric id (n_id) that
# parents, keyframes, clips and the selection refer to, so rows can be removed without remapping.
# Several scenes can be loaded at once under different namespaces (nd_use): "" is the main scene,
# "h" a HUD / menu drawn over it, "b" a 3-D background behind a 2-D menu.
# A scene file is text: one record per line, "type|key=value|key=value" (see rt_data.ling).

fn ns() { return st_get("ns", "") }
fn nd_use(prefix) { st_set("ns", prefix) }
fn nk(col) { return ns() + "n_" + col }

fn nd_cols() {
    return ["id", "kind", "name", "par", "px", "py", "pz", "rx", "ry", "rz", "sx", "sy", "sz", "col", "src", "vis", "lock", "tag", "scr", "a", "b", "c", "d", "txt", "ex", "w", "h", "anc"]
}
fn nd_is_text(col) {
    return col == "kind" || col == "name" || col == "src" || col == "tag" || col == "scr" || col == "txt" || col == "ex" || col == "anc"
}
fn nd_props_anim() { return ["px", "py", "pz", "rx", "ry", "rz", "sx", "sy", "sz", "a", "b", "w", "h"] }

# ── scene ────────────────────────────────────────────────────────────────────
fn sc_get(k, d) { return st_get(ns() + "sc_" + k, d) }
fn sc_set(k, v) { st_set(ns() + "sc_" + k, v) }

fn sc_clear() {
    bind cols = nd_cols()
    bind i = 0.0
    while i < len(cols) {
        st_lclear(nk(list_get(cols, i)))
        bind i = i + 1.0
    }
    st_lclear(ns() + "k_node")
    st_lclear(ns() + "k_prop")
    st_lclear(ns() + "k_t")
    st_lclear(ns() + "k_v")
    st_lclear(ns() + "k_e")
    st_lclear(ns() + "c_track")
    st_lclear(ns() + "c_t")
    st_lclear(ns() + "c_d")
    st_lclear(ns() + "c_kind")
    st_lclear(ns() + "c_a")
    st_lclear(ns() + "c_b")
    st_lclear(ns() + "c_c")
    st_lclear(ns() + "c_node")
    st_lclear(ns() + "c_e")
    sc_set("nextid", 1.0)
}

fn sc_new(name, kind) {
    sc_clear()
    sc_set("name", name)
    sc_set("kind", kind)
    sc_set("bg", sel(kind == "2d", 1776411.0, 526603.0))
    sc_set("fps", 30.0)
    sc_set("len", 10.0)
    sc_set("cw", 1280.0)
    sc_set("ch", 720.0)
    sc_set("amb", 0.25)
    sc_set("camx", 0.0)
    sc_set("camy", 0.0 - 1.0)
    sc_set("camz", 0.0)
    sc_set("camyaw", 0.6)
    sc_set("campitch", 0.42)
    sc_set("camzd", 16.0)
    sc_set("hud", "")
    sc_set("next", "")
    sc_set("music", "")
    sc_set("grav", 9.0)
    sc_set("floor", 0.0)
    sc_set("ex", "")
    sc_set("path", "")
}

# ── nodes ────────────────────────────────────────────────────────────────────
fn nd_count() { return st_llen(nk("id")) }
fn nd_get(i, col) { return st_lget(nk(col), i) }
fn nd_set(i, col, v) { st_lset(nk(col), i, v) }
fn nd_id(i) { return st_lget(nk("id"), i) }
fn nd_find(id) {
    bind key = nk("id")
    bind n = st_llen(key)
    bind i = 0.0
    while i < n {
        if st_lget(key, i) == id { return i }
        bind i = i + 1.0
    }
    return 0.0 - 1.0
}
fn nd_gid(id, col) {
    bind i = nd_find(id)
    if i < 0.0 { return sel(nd_is_text(col), "", 0.0) }
    return nd_get(i, col)
}
fn nd_sid(id, col, v) {
    bind i = nd_find(id)
    if i >= 0.0 { nd_set(i, col, v) }
}
fn nd_find_name(name) {
    bind key = nk("name")
    bind n = st_llen(key)
    bind i = 0.0
    while i < n {
        if st_lget(key, i) == name { return i }
        bind i = i + 1.0
    }
    return 0.0 - 1.0
}
fn nd_find_tag(tag) {
    bind key = nk("tag")
    bind n = st_llen(key)
    bind i = 0.0
    while i < n {
        if st_lget(key, i) == tag { return i }
        bind i = i + 1.0
    }
    return 0.0 - 1.0
}
fn nd_is_2d(kind) {
    return kind == "panel" || kind == "label" || kind == "button" || kind == "image" || kind == "bar"
}
fn nd_kinds_3d() { return ["empty", "box", "sphere", "cylinder", "cone", "capsule", "torus", "plane", "model", "actor", "light", "camera", "spawn", "trigger", "audio", "music", "midi", "text3d", "swarm", "knot"] }
fn nd_kinds_2d() { return ["panel", "label", "button", "image", "bar"] }

# a new row with the defaults of its kind; returns the row index
fn nd_add(kind, name, parent_id) {
    bind id = sc_get("nextid", 1.0)
    sc_set("nextid", id + 1.0)
    bind i = nd_count()
    st_lpush(nk("id"), id)
    st_lpush(nk("kind"), kind)
    st_lpush(nk("name"), name)
    st_lpush(nk("par"), parent_id)
    st_lpush(nk("px"), 0.0)
    st_lpush(nk("py"), 0.0)
    st_lpush(nk("pz"), 0.0)
    st_lpush(nk("rx"), 0.0)
    st_lpush(nk("ry"), 0.0)
    st_lpush(nk("rz"), 0.0)
    st_lpush(nk("sx"), 1.0)
    st_lpush(nk("sy"), 1.0)
    st_lpush(nk("sz"), 1.0)
    st_lpush(nk("col"), 13488344.0)
    st_lpush(nk("src"), "")
    st_lpush(nk("vis"), 1.0)
    st_lpush(nk("lock"), 0.0)
    st_lpush(nk("tag"), "")
    st_lpush(nk("scr"), "")
    st_lpush(nk("a"), 0.0)
    st_lpush(nk("b"), 0.0)
    st_lpush(nk("c"), 0.0)
    st_lpush(nk("d"), 0.0)
    st_lpush(nk("txt"), "")
    st_lpush(nk("ex"), "")
    st_lpush(nk("w"), 160.0)
    st_lpush(nk("h"), 40.0)
    st_lpush(nk("anc"), "tl")
    nd_apply_defaults(i, kind)
    return i
}
fn nd_apply_defaults(i, kind) {
    if kind == "light" { nd_set(i, "col", 16773836.0)  nd_set(i, "a", 2.5)  nd_set(i, "b", 16.0)  nd_set(i, "py", 0.0 - 4.0) }
    else if kind == "camera" { nd_set(i, "col", 3910379.0)  nd_set(i, "a", 2.0)  nd_set(i, "b", 0.0)  nd_set(i, "d", 8.0)  nd_set(i, "py", 0.0 - 3.0)  nd_set(i, "pz", 0.0 - 8.0)  nd_set(i, "rx", 0.25) }
    else if kind == "spawn" { nd_set(i, "col", 4573296.0)  nd_set(i, "tag", "player") }
    else if kind == "trigger" { nd_set(i, "col", 10114006.0)  nd_set(i, "tag", "zone")  nd_set(i, "sx", 2.0)  nd_set(i, "sy", 1.5)  nd_set(i, "sz", 2.0)  nd_set(i, "a", 1.0) }
    else if kind == "audio" { nd_set(i, "col", 4875480.0)  nd_set(i, "a", 1.0)  nd_set(i, "b", 0.0)  nd_set(i, "c", 1.0)  nd_set(i, "d", 1.0) }
    else if kind == "music" { nd_set(i, "col", 4875480.0)  nd_set(i, "a", 0.7)  nd_set(i, "b", 1.0)  nd_set(i, "c", 1.0) }
    else if kind == "midi" { nd_set(i, "col", 10114006.0)  nd_set(i, "a", 0.0 - 1.0)  nd_set(i, "b", 1.0)  nd_set(i, "c", 1.0) }
    else if kind == "model" { nd_set(i, "col", 16225310.0)  nd_set(i, "b", 1.0)  nd_set(i, "c", 1.0) }
    else if kind == "actor" { nd_set(i, "col", 16225310.0)  nd_set(i, "b", 1.0)  nd_set(i, "c", 1.0)  nd_set(i, "d", 6.0)  nd_set(i, "tag", "player")  nd_set(i, "sx", 0.45)  nd_set(i, "sz", 0.45) }
    else if kind == "plane" { nd_set(i, "col", 2763826.0)  nd_set(i, "sx", 10.0)  nd_set(i, "sy", 0.02)  nd_set(i, "sz", 10.0) }
    else if kind == "box" { nd_set(i, "col", 3910379.0) }
    else if kind == "sphere" { nd_set(i, "col", 15551031.0) }
    else if kind == "cylinder" || kind == "cone" || kind == "capsule" || kind == "torus" { nd_set(i, "col", 4573296.0) }
    else if kind == "text3d" { nd_set(i, "col", 16242758.0)  nd_set(i, "txt", "Text")  nd_set(i, "a", 1.0) }
    else if kind == "swarm" { nd_set(i, "col", 15658734.0)  nd_set(i, "a", 60.0)  nd_set(i, "b", 12.0)  nd_set(i, "c", 1.0)  nd_set(i, "d", 0.1)  nd_set(i, "sx", 0.08)  nd_set(i, "sy", 0.08)  nd_set(i, "sz", 0.08) }
    else if kind == "knot" { nd_set(i, "col", 16777215.0)  nd_set(i, "a", 30.0)  nd_set(i, "b", 6.0)  nd_set(i, "c", 2.4)  nd_set(i, "d", 0.06)  nd_set(i, "sx", 0.3)  nd_set(i, "sy", 0.3)  nd_set(i, "sz", 0.3)  nd_set(i, "ex", "rainbow=1") }
    else if kind == "panel" { nd_set(i, "col", 1447706.0)  nd_set(i, "w", 320.0)  nd_set(i, "h", 200.0)  nd_set(i, "a", 0.9) }
    else if kind == "label" { nd_set(i, "col", 16119285.0)  nd_set(i, "txt", "Label")  nd_set(i, "a", 18.0)  nd_set(i, "w", 200.0)  nd_set(i, "h", 24.0) }
    else if kind == "button" { nd_set(i, "col", 3910379.0)  nd_set(i, "txt", "Button")  nd_set(i, "a", 14.0)  nd_set(i, "w", 160.0)  nd_set(i, "h", 36.0)  nd_set(i, "scr", "goto:") }
    else if kind == "image" { nd_set(i, "col", 16777215.0)  nd_set(i, "w", 128.0)  nd_set(i, "h", 128.0) }
    else if kind == "bar" { nd_set(i, "col", 4573296.0)  nd_set(i, "a", 70.0)  nd_set(i, "b", 100.0)  nd_set(i, "w", 200.0)  nd_set(i, "h", 16.0) }
}

fn nd_children_ids(parent_id) {
    bind out = []
    bind key = nk("par")
    bind idk = nk("id")
    bind n = st_llen(key)
    bind i = 0.0
    while i < n {
        if st_lget(key, i) == parent_id { bind out = list_push(out, st_lget(idk, i)) }
        bind i = i + 1.0
    }
    return out
}
fn nd_depth(i) {
    bind d = 0.0
    bind p = nd_get(i, "par")
    bind guard = 0.0
    while p > 0.0 && guard < 64.0 {
        bind d = d + 1.0
        bind pi = nd_find(p)
        if pi < 0.0 { return d }
        bind p = nd_get(pi, "par")
        bind guard = guard + 1.0
    }
    return d
}
fn nd_is_descendant(id, ancestor_id) {
    bind i = nd_find(id)
    bind guard = 0.0
    while i >= 0.0 && guard < 64.0 {
        bind p = nd_get(i, "par")
        if p == ancestor_id { return true }
        if p <= 0.0 { return false }
        bind i = nd_find(p)
        bind guard = guard + 1.0
    }
    return false
}

# delete a node and everything under it (keys and clips that pointed at them go too)
fn nd_del_id(id) {
    bind kids = nd_children_ids(id)
    bind k = 0.0
    while k < len(kids) {
        nd_del_id(list_get(kids, k))
        bind k = k + 1.0
    }
    bind i = nd_find(id)
    if i < 0.0 { return 0.0 }
    bind cols = nd_cols()
    bind c = 0.0
    while c < len(cols) {
        st_ldel(nk(list_get(cols, c)), i)
        bind c = c + 1.0
    }
    key_remove_node(id)
    bind ci = clip_count() - 1.0
    while ci >= 0.0 {
        if clip_get(ci, "node") == id { clip_del(ci) }
        bind ci = ci - 1.0
    }
    return 0.0
}

# duplicate a node (and its subtree); returns the new id
fn nd_dup_id(id, new_parent) {
    bind i = nd_find(id)
    if i < 0.0 { return 0.0 - 1.0 }
    bind ni = nd_add(nd_get(i, "kind"), nd_get(i, "name") + " copy", new_parent)
    bind nid = nd_id(ni)
    bind cols = nd_cols()
    bind c = 0.0
    while c < len(cols) {
        bind col = list_get(cols, c)
        if col != "id" && col != "par" && col != "name" { nd_set(ni, col, nd_get(i, col)) }
        bind c = c + 1.0
    }
    bind kn = key_count()
    bind k = 0.0
    while k < kn {
        if key_get(k, "node") == id { key_add(nid, key_get(k, "prop"), key_get(k, "t"), key_get(k, "v"), key_get(k, "e")) }
        bind k = k + 1.0
    }
    bind kids = nd_children_ids(id)
    bind k = 0.0
    while k < len(kids) {
        nd_dup_id(list_get(kids, k), nid)
        bind k = k + 1.0
    }
    return nid
}

# move row `from` to sit at row `to` (keeps parents as they are)
fn nd_reorder(from, to) {
    if from == to || from < 0.0 || to < 0.0 { return 0.0 }
    bind cols = nd_cols()
    bind c = 0.0
    while c < len(cols) {
        bind key = nk(list_get(cols, c))
        bind v = st_lget(key, from)
        st_ldel(key, from)
        bind dst = to
        if to > from { bind dst = to - 1.0 }
        sl_insert(key, dst, v)
        bind c = c + 1.0
    }
    return 0.0
}

# world position at time t: parent chain translation + parent yaw (animated values included)
fn nd_world_pos_t(i, t) {
    bind x = nd_val(i, "px", t)
    bind y = nd_val(i, "py", t)
    bind z = nd_val(i, "pz", t)
    bind p = nd_get(i, "par")
    bind guard = 0.0
    while p > 0.0 && guard < 64.0 {
        bind pi = nd_find(p)
        if pi < 0.0 { return [x, y, z] }
        bind yaw = nd_val(pi, "ry", t)
        bind cy = cos(yaw)
        bind sy = sin(yaw)
        bind nx = cy * x + sy * z
        bind nz = 0.0 - sy * x + cy * z
        bind x = nx + nd_val(pi, "px", t)
        bind y = y + nd_val(pi, "py", t)
        bind z = nz + nd_val(pi, "pz", t)
        bind p = nd_get(pi, "par")
        bind guard = guard + 1.0
    }
    return [x, y, z]
}
fn nd_world_pos(i) { return nd_world_pos_t(i, 0.0 - 1.0) }
fn nd_world_yaw_t(i, t) {
    bind yaw = nd_val(i, "ry", t)
    bind p = nd_get(i, "par")
    bind guard = 0.0
    while p > 0.0 && guard < 64.0 {
        bind pi = nd_find(p)
        if pi < 0.0 { return yaw }
        bind yaw = yaw + nd_val(pi, "ry", t)
        bind p = nd_get(pi, "par")
        bind guard = guard + 1.0
    }
    return yaw
}
fn nd_world_yaw(i) { return nd_world_yaw_t(i, 0.0 - 1.0) }
# world -> local (for setting a child's position from a world point)
fn nd_local_from_world(parent_id, x, y, z) {
    if parent_id <= 0.0 { return [x, y, z] }
    bind pi = nd_find(parent_id)
    if pi < 0.0 { return [x, y, z] }
    bind wp = nd_world_pos(pi)
    bind yaw = nd_world_yaw(pi)
    bind dx = x - list_get(wp, 0)
    bind dy = y - list_get(wp, 1)
    bind dz = z - list_get(wp, 2)
    bind cy = cos(yaw)
    bind sy = sin(yaw)
    return [cy * dx - sy * dz, dy, sy * dx + cy * dz]
}

# ── keyframes ────────────────────────────────────────────────────────────────
fn key_count() { return st_llen(ns() + "k_node") }
fn key_get(k, col) { return st_lget(ns() + "k_" + col, k) }
fn key_set(k, col, v) { st_lset(ns() + "k_" + col, k, v) }
fn key_at(id, prop, t) {
    bind n = key_count()
    bind kn = ns() + "k_node"
    bind kp = ns() + "k_prop"
    bind kt = ns() + "k_t"
    bind k = 0.0
    while k < n {
        if st_lget(kn, k) == id && st_lget(kp, k) == prop && abs(st_lget(kt, k) - t) < 0.0005 { return k }
        bind k = k + 1.0
    }
    return 0.0 - 1.0
}
fn key_add(id, prop, t, v, e) {
    bind k = key_at(id, prop, t)
    if k >= 0.0 {
        key_set(k, "v", v)
        key_set(k, "e", e)
        return k
    }
    st_lpush(ns() + "k_node", id)
    st_lpush(ns() + "k_prop", prop)
    st_lpush(ns() + "k_t", t)
    st_lpush(ns() + "k_v", v)
    st_lpush(ns() + "k_e", e)
    return key_count() - 1.0
}
fn key_del(k) {
    st_ldel(ns() + "k_node", k)
    st_ldel(ns() + "k_prop", k)
    st_ldel(ns() + "k_t", k)
    st_ldel(ns() + "k_v", k)
    st_ldel(ns() + "k_e", k)
}
fn key_remove_node(id) {
    bind k = key_count() - 1.0
    while k >= 0.0 {
        if key_get(k, "node") == id { key_del(k) }
        bind k = k - 1.0
    }
}
fn key_remove_prop(id, prop) {
    bind k = key_count() - 1.0
    while k >= 0.0 {
        if key_get(k, "node") == id && key_get(k, "prop") == prop { key_del(k) }
        bind k = k - 1.0
    }
}
fn key_has(id, prop) {
    bind n = key_count()
    bind kn = ns() + "k_node"
    bind kp = ns() + "k_prop"
    bind k = 0.0
    while k < n {
        if st_lget(kn, k) == id && st_lget(kp, k) == prop { return true }
        bind k = k + 1.0
    }
    return false
}
fn key_node_has(id) {
    bind n = key_count()
    bind kn = ns() + "k_node"
    bind k = 0.0
    while k < n {
        if st_lget(kn, k) == id { return true }
        bind k = k + 1.0
    }
    return false
}
# indices of the keys of (id, prop), sorted by time
fn key_list(id, prop) {
    bind out = []
    bind n = key_count()
    bind kn = ns() + "k_node"
    bind kp = ns() + "k_prop"
    bind kt = ns() + "k_t"
    bind k = 0.0
    while k < n {
        if st_lget(kn, k) == id && st_lget(kp, k) == prop {
            bind t = st_lget(kt, k)
            bind pos = 0.0
            while pos < len(out) && st_lget(kt, list_get(out, pos)) <= t { bind pos = pos + 1.0 }
            bind out = l_insert_at(out, pos, k)
        }
        bind k = k + 1.0
    }
    return out
}
fn key_props_of(id) {
    bind out = []
    bind props = nd_props_anim()
    bind p = 0.0
    while p < len(props) {
        if key_has(id, list_get(props, p)) { bind out = list_push(out, list_get(props, p)) }
        bind p = p + 1.0
    }
    return out
}
# distinct key times of a node, sorted
fn key_times_of(id) {
    bind out = []
    bind n = key_count()
    bind kn = ns() + "k_node"
    bind kt = ns() + "k_t"
    bind k = 0.0
    while k < n {
        if st_lget(kn, k) == id {
            bind t = st_lget(kt, k)
            bind found = false
            bind pos = 0.0
            bind go = pos < len(out)
            while go {
                bind v = list_get(out, pos)
                if v <= t + 0.0005 {
                    if abs(v - t) < 0.0005 { bind found = true }
                    bind pos = pos + 1.0
                    bind go = pos < len(out)
                } else { bind go = false }
            }
            if !found { bind out = l_insert_at(out, pos, t) }
        }
        bind k = k + 1.0
    }
    return out
}
fn key_eval(id, prop, t, d) {
    bind ks = key_list(id, prop)
    bind n = len(ks)
    if n < 1.0 { return d }
    bind kt = ns() + "k_t"
    bind kv = ns() + "k_v"
    bind first = list_get(ks, 0)
    if t <= st_lget(kt, first) { return st_lget(kv, first) }
    bind last = list_get(ks, n - 1.0)
    if t >= st_lget(kt, last) { return st_lget(kv, last) }
    bind i = 0.0
    while i < n - 1.0 {
        bind ka = list_get(ks, i)
        bind kb = list_get(ks, i + 1.0)
        bind ta = st_lget(kt, ka)
        bind tb = st_lget(kt, kb)
        if t >= ta && t <= tb {
            bind u = sel(tb - ta > 0.00001, (t - ta) / (tb - ta), 1.0)
            bind kind = key_get(kb, "e")
            if prop == "rx" || prop == "ry" || prop == "rz" { return ease_angle(st_lget(kv, ka), st_lget(kv, kb), u, kind) }
            return ease_lerp(st_lget(kv, ka), st_lget(kv, kb), u, kind)
        }
        bind i = i + 1.0
    }
    return st_lget(kv, last)
}
fn key_max_time() {
    bind m = 0.0
    bind n = key_count()
    bind kt = ns() + "k_t"
    bind k = 0.0
    while k < n {
        bind t = st_lget(kt, k)
        if t > m { bind m = t }
        bind k = k + 1.0
    }
    return m
}
# value of a node property at time t (animated when it has keys; t < 0 = the stored value)
fn nd_val(i, col, t) {
    bind v = nd_get(i, col)
    if t < 0.0 || key_count() < 1.0 { return v }
    bind id = nd_get(i, "id")
    if !key_has(id, col) { return v }
    return key_eval(id, col, t, v)
}

# ── clips (cutscene / event tracks) ──────────────────────────────────────────
fn clip_count() { return st_llen(ns() + "c_track") }
fn clip_tracks() { return ["camera", "dialog", "anim", "audio", "fx", "event"] }
fn clip_add(track, t, d, kind, a, b, c, node, e) {
    st_lpush(ns() + "c_track", track)
    st_lpush(ns() + "c_t", t)
    st_lpush(ns() + "c_d", d)
    st_lpush(ns() + "c_kind", kind)
    st_lpush(ns() + "c_a", a)
    st_lpush(ns() + "c_b", b)
    st_lpush(ns() + "c_c", c)
    st_lpush(ns() + "c_node", node)
    st_lpush(ns() + "c_e", e)
    return clip_count() - 1.0
}
fn clip_del(i) {
    st_ldel(ns() + "c_track", i)
    st_ldel(ns() + "c_t", i)
    st_ldel(ns() + "c_d", i)
    st_ldel(ns() + "c_kind", i)
    st_ldel(ns() + "c_a", i)
    st_ldel(ns() + "c_b", i)
    st_ldel(ns() + "c_c", i)
    st_ldel(ns() + "c_node", i)
    st_ldel(ns() + "c_e", i)
}
fn clip_get(i, col) { return st_lget(ns() + "c_" + col, i) }
fn clip_set(i, col, v) { st_lset(ns() + "c_" + col, i, v) }
fn clip_sorted(track) {
    bind out = []
    bind n = clip_count()
    bind ct = ns() + "c_t"
    bind ctr = ns() + "c_track"
    bind i = 0.0
    while i < n {
        if track == "" || st_lget(ctr, i) == track {
            bind t = st_lget(ct, i)
            bind pos = 0.0
            while pos < len(out) && st_lget(ct, list_get(out, pos)) <= t { bind pos = pos + 1.0 }
            bind out = l_insert_at(out, pos, i)
        }
        bind i = i + 1.0
    }
    return out
}
fn clip_end_time() {
    bind m = 0.0
    bind n = clip_count()
    bind i = 0.0
    while i < n {
        bind e = clip_get(i, "t") + clip_get(i, "d")
        if e > m { bind m = e }
        bind i = i + 1.0
    }
    return m
}
# the clip of a track active at time t (latest-starting one), or -1
fn clip_active(track, t) {
    bind best = 0.0 - 1.0
    bind bt = 0.0 - 1.0
    bind n = clip_count()
    bind ctr = ns() + "c_track"
    bind ct = ns() + "c_t"
    bind cd = ns() + "c_d"
    bind i = 0.0
    while i < n {
        if st_lget(ctr, i) == track {
            bind c0 = st_lget(ct, i)
            if t >= c0 && t < c0 + max(0.001, st_lget(cd, i)) && c0 >= bt { bind best = i  bind bt = c0 }
        }
        bind i = i + 1.0
    }
    return best
}

# ── serialization ────────────────────────────────────────────────────────────
fn sc_serialize() {
    bind s = "lingdev-scene|v=1\n"
    bind s = s + "scene" + fld("name", sc_get("name", "")) + fld("kind", sc_get("kind", "3d")) + fldn("bg", sc_get("bg", 0.0)) + fldn("fps", sc_get("fps", 30.0)) + fldn("len", sc_get("len", 10.0))
    bind s = s + fldn("cw", sc_get("cw", 1280.0)) + fldn("ch", sc_get("ch", 720.0)) + fldn("amb", sc_get("amb", 0.25))
    bind s = s + fldn("camx", sc_get("camx", 0.0)) + fldn("camy", sc_get("camy", 0.0)) + fldn("camz", sc_get("camz", 0.0)) + fldn("camyaw", sc_get("camyaw", 0.0)) + fldn("campitch", sc_get("campitch", 0.0)) + fldn("camzd", sc_get("camzd", 2.0))
    bind s = s + fld("hud", sc_get("hud", "")) + fld("next", sc_get("next", "")) + fld("music", sc_get("music", "")) + fldn("grav", sc_get("grav", 9.0)) + fldn("floor", sc_get("floor", 0.0)) + fld("ex", sc_get("ex", "")) + fldn("nextid", sc_get("nextid", 1.0)) + "\n"
    bind cols = nd_cols()
    bind n = nd_count()
    bind i = 0.0
    while i < n {
        bind line = "node"
        bind c = 0.0
        while c < len(cols) {
            bind col = list_get(cols, c)
            if nd_is_text(col) { bind line = line + fld(col, nd_get(i, col)) } else { bind line = line + fldn(col, nd_get(i, col)) }
            bind c = c + 1.0
        }
        bind s = s + line + "\n"
        bind i = i + 1.0
    }
    bind kn = key_count()
    bind k = 0.0
    while k < kn {
        bind s = s + "key" + fldn("n", key_get(k, "node")) + fld("p", key_get(k, "prop")) + fldn("t", key_get(k, "t")) + fldn("v", key_get(k, "v")) + fld("e", key_get(k, "e")) + "\n"
        bind k = k + 1.0
    }
    bind cn = clip_count()
    bind c = 0.0
    while c < cn {
        bind s = s + "clip" + fld("tr", clip_get(c, "track")) + fldn("t", clip_get(c, "t")) + fldn("d", clip_get(c, "d")) + fld("k", clip_get(c, "kind")) + fld("a", clip_get(c, "a")) + fld("b", clip_get(c, "b")) + fld("c", clip_get(c, "c")) + fldn("n", clip_get(c, "node")) + fld("e", clip_get(c, "e")) + "\n"
        bind c = c + 1.0
    }
    return s
}

fn sc_parse(text) {
    sc_new("Untitled", "3d")
    bind lines = str_split(text, "\n")
    bind cols = nd_cols()
    bind i = 0.0
    while i < len(lines) {
        bind ln = str_trim(list_get(lines, i))
        if ln != "" {
            bind f = str_split(ln, "|")
            bind typ = list_get(f, 0)
            if typ == "scene" {
                sc_set("name", rec_get(f, "name", "Untitled"))
                sc_set("kind", rec_get(f, "kind", "3d"))
                sc_set("bg", rec_num(f, "bg", 526603.0))
                sc_set("fps", rec_num(f, "fps", 30.0))
                sc_set("len", rec_num(f, "len", 10.0))
                sc_set("cw", rec_num(f, "cw", 1280.0))
                sc_set("ch", rec_num(f, "ch", 720.0))
                sc_set("amb", rec_num(f, "amb", 0.25))
                sc_set("camx", rec_num(f, "camx", 0.0))
                sc_set("camy", rec_num(f, "camy", 0.0 - 1.0))
                sc_set("camz", rec_num(f, "camz", 0.0))
                sc_set("camyaw", rec_num(f, "camyaw", 0.6))
                sc_set("campitch", rec_num(f, "campitch", 0.42))
                sc_set("camzd", rec_num(f, "camzd", 16.0))
                sc_set("hud", rec_get(f, "hud", ""))
                sc_set("next", rec_get(f, "next", ""))
                sc_set("music", rec_get(f, "music", ""))
                sc_set("grav", rec_num(f, "grav", 9.0))
                sc_set("floor", rec_num(f, "floor", 0.0))
                sc_set("ex", rec_get(f, "ex", ""))
                sc_set("nextid", rec_num(f, "nextid", 1.0))
            } else if typ == "node" {
                bind c = 0.0
                while c < len(cols) {
                    bind col = list_get(cols, c)
                    if nd_is_text(col) { st_lpush(nk(col), rec_get(f, col, "")) } else { st_lpush(nk(col), rec_num(f, col, sel(col == "vis" || col == "sx" || col == "sy" || col == "sz", 1.0, 0.0))) }
                    bind c = c + 1.0
                }
            } else if typ == "key" {
                st_lpush(ns() + "k_node", rec_num(f, "n", 0.0))
                st_lpush(ns() + "k_prop", rec_get(f, "p", "px"))
                st_lpush(ns() + "k_t", rec_num(f, "t", 0.0))
                st_lpush(ns() + "k_v", rec_num(f, "v", 0.0))
                st_lpush(ns() + "k_e", rec_get(f, "e", "linear"))
            } else if typ == "clip" {
                st_lpush(ns() + "c_track", rec_get(f, "tr", "event"))
                st_lpush(ns() + "c_t", rec_num(f, "t", 0.0))
                st_lpush(ns() + "c_d", rec_num(f, "d", 1.0))
                st_lpush(ns() + "c_kind", rec_get(f, "k", ""))
                st_lpush(ns() + "c_a", rec_get(f, "a", ""))
                st_lpush(ns() + "c_b", rec_get(f, "b", ""))
                st_lpush(ns() + "c_c", rec_get(f, "c", ""))
                st_lpush(ns() + "c_node", rec_num(f, "n", 0.0))
                st_lpush(ns() + "c_e", rec_get(f, "e", "linear"))
            }
        }
        bind i = i + 1.0
    }
    bind maxid = 0.0
    bind n = nd_count()
    bind j = 0.0
    while j < n {
        if nd_id(j) > maxid { bind maxid = nd_id(j) }
        bind j = j + 1.0
    }
    if sc_get("nextid", 1.0) <= maxid { sc_set("nextid", maxid + 1.0) }
    return nd_count()
}

fn sc_load(path) {
    bind text = plat_read(path)
    if text == "" { return false }
    sc_parse(text)
    sc_set("path", path)
    return true
}
fn sc_save(path) {
    bind ok = plat_write(path, sc_serialize())
    if ok { sc_set("path", path) }
    return ok
}
# a named value inside a node's `ex` field ("k=v;k=v")
fn nd_ex(i, key, d) {
    bind parts = str_split(nd_get(i, "ex"), ";")
    bind k = 0.0
    while k < len(parts) {
        bind p = list_get(parts, k)
        if kv_key(p) == key { return kv_val(p) }
        bind k = k + 1.0
    }
    return d
}
# set / replace one key in a node's `ex` field
fn nd_ex_set(i, key, v) {
    bind parts = str_split(nd_get(i, "ex"), ";")
    bind out = ""
    bind done = false
    bind k = 0.0
    while k < len(parts) {
        bind p = list_get(parts, k)
        if str_trim(p) != "" {
            if kv_key(p) == key {
                if "" + v != "" { bind out = out + sel(out == "", "", ";") + key + "=" + v }
                bind done = true
            } else { bind out = out + sel(out == "", "", ";") + p }
        }
        bind k = k + 1.0
    }
    if !done && "" + v != "" { bind out = out + sel(out == "", "", ";") + key + "=" + v }
    nd_set(i, "ex", out)
}
fn sc_ex_set(key, v) {
    bind parts = str_split(sc_get("ex", ""), ";")
    bind out = ""
    bind done = false
    bind k = 0.0
    while k < len(parts) {
        bind p = list_get(parts, k)
        if str_trim(p) != "" {
            if kv_key(p) == key {
                if "" + v != "" { bind out = out + sel(out == "", "", ";") + key + "=" + v }
                bind done = true
            } else { bind out = out + sel(out == "", "", ";") + p }
        }
        bind k = k + 1.0
    }
    if !done && "" + v != "" { bind out = out + sel(out == "", "", ";") + key + "=" + v }
    sc_set("ex", out)
}
# a named value inside the scene's `ex` field ("k=v;k=v")
fn sc_ex(key, d) {
    bind parts = str_split(sc_get("ex", ""), ";")
    bind i = 0.0
    while i < len(parts) {
        bind p = list_get(parts, i)
        if kv_key(p) == key { return kv_val(p) }
        bind i = i + 1.0
    }
    return d
}

← Back to index