294 lines
13 KiB
Text
294 lines
13 KiB
Text
# attrs.ludic — what an editor needs to know about a field or a registry, said on the @-syntax the
|
|
# compiler already reads (@max(64), @owns(Kind)) and carried into `--emit-schema`:
|
|
#
|
|
# property Item {
|
|
# @Ref(Vendors) seller: int = 0 # an index into that registry (its entries are offered)
|
|
# @OneOf(GR_) grade: int = 0 # one of the constants that start GR_ ...
|
|
# @OneOf(WILD_MEADOW, WILD_SHORE) hab: int = 0 # ... or one of these (each must exist)
|
|
# @Range(0, 1.5) weight: float = 0.4
|
|
# @Unit("m/s") speed: float = 0.0
|
|
# @Asset("gltf") model: string = "" # a file of that kind
|
|
# @Color tint: int = 0
|
|
# @Node(model) grip: string = "" # a node inside the glTF the field `model` names
|
|
# @Clip(model) idle: string = "" # a clip inside that glTF
|
|
# @Material(model) skin: string = "" # a material inside it
|
|
# @OneOf("box", "hull") shape: string = "" # a string field: one of these words
|
|
# @Color @Tint(TSLOT_HAIR) hair: int = 0 # a colour for that tint slot (a constant that exists)
|
|
# @Derived reach: float = 0.0 # worked out at boot: anything written is overwritten
|
|
# @Text @Multiline blurb: string = "" # read by the player, so translated; prose
|
|
# @Key bind: int = 0 # a key code
|
|
# }
|
|
# @AppendOnly @ByKey registry Items of Item as IT from "items.lres"
|
|
#
|
|
# They change nothing the program does. A target the program does not have at all is a warning and
|
|
# "unresolved" in the schema (a package names the game's registry it cannot import). Checked: @Ref
|
|
# names a registry; @Node, @Clip and @Material a field of their record naming a glTF
|
|
# (attrs_node.ludic); a listing @OneOf constants (or, on a string field, words - and every row's value
|
|
# one of them); and @Tint a constant (a slot's row in the game's registry). A
|
|
# field's or a registry's attributes are a group (Node.at, g_rg_at): a run of names, each with its
|
|
# arguments as JSON values - an identifier or a string as a string, a number as written.
|
|
var g_at_start: []int = new []int # a group's first attribute ...
|
|
var g_at_count: []int = new []int # ... and how many it holds
|
|
var g_at_name: []pointer = new []pointer
|
|
var g_at_args: []pointer = new []pointer # the arguments, a JSON array: ["Items"], [0, 1.5]
|
|
var g_at_first: []pointer = new []pointer # the first argument as written (@Ref's registry), or null
|
|
var g_at_nargs: []int = new []int
|
|
var g_at_unres: []bool = new []bool # its target is not in this program (a warning; the schema says so)
|
|
var g_at_open: int = -1 # the group the attributes being read go into
|
|
var g_at_decl: []pointer = new []pointer # @AppendOnly / @ByKey read before a declaration
|
|
|
|
# a new group, empty; its index
|
|
function at_group() -> int {
|
|
push(g_at_start, len(g_at_name))
|
|
push(g_at_count, 0)
|
|
return len(g_at_start) - 1
|
|
}
|
|
function at_push(g: int, name: pointer, args: pointer, first: pointer, nargs: int) -> void {
|
|
push(g_at_name, name)
|
|
push(g_at_args, args)
|
|
push(g_at_first, first)
|
|
push(g_at_nargs, nargs)
|
|
push(g_at_unres, false)
|
|
push(g_at_strs, g_at_strs_next)
|
|
g_at_strs_next = false
|
|
g_at_count[g] = g_at_count[g] + 1
|
|
}
|
|
# `(a, "b", -1.5)` after an attribute's name, if it is there, as a JSON array; the first argument
|
|
# as written goes to g_at_arg0, the count to g_at_argn
|
|
var g_at_arg0: pointer = null
|
|
var g_at_argn: int = 0
|
|
var g_at_allnum: bool = true # every argument a number
|
|
var g_at_allid: bool = true # every argument a name
|
|
var g_at_allstr: bool = true # every argument a string
|
|
var g_at_strs: []bool = new []bool # per attribute: its arguments were all strings
|
|
var g_at_strs_next: bool = false # what at_push files for the attribute it pushes
|
|
function at_args() -> pointer {
|
|
g_at_arg0 = null
|
|
g_at_argn = 0
|
|
g_at_allnum = true
|
|
g_at_allid = true
|
|
g_at_allstr = true
|
|
if not is_op("(") { return "[]" }
|
|
pi += 1
|
|
var out: pointer = "["
|
|
while not is_op(")") {
|
|
skipnl()
|
|
var neg = false
|
|
if is_op("-") { neg = true; pi += 1 }
|
|
let t = toks[pi]
|
|
if t.kind != TK_ID { g_at_allid = false }
|
|
if t.kind != TK_STR { g_at_allstr = false }
|
|
var v: pointer = null
|
|
var raw: pointer = null
|
|
if t.kind == TK_INT {
|
|
raw = itoa(t.ival)
|
|
if t.text != null { raw = t.text }
|
|
if neg { raw = "-" + raw }
|
|
v = raw
|
|
} else if t.kind == TK_FLOAT {
|
|
raw = t.text
|
|
if neg { raw = "-" + raw }
|
|
v = raw
|
|
if raw[0] == '.' or (neg and raw[1] == '.') { v = at_lead0(raw) }
|
|
} else if (t.kind == TK_STR or t.kind == TK_ID) and not neg {
|
|
g_at_allnum = false
|
|
raw = t.text
|
|
let b = buf_new()
|
|
jq_put(b, raw)
|
|
v = buf_str(b)
|
|
} else {
|
|
perr(`an attribute's argument is a name, a string or a number, not {tok_desc()}`)
|
|
}
|
|
pi += 1
|
|
if g_at_argn > 0 { out = out + ", " }
|
|
out = out + v
|
|
if g_at_argn == 0 { g_at_arg0 = raw }
|
|
g_at_argn += 1
|
|
skipnl()
|
|
if is_op(",") { pi += 1 }
|
|
}
|
|
eat_op(")")
|
|
return out + "]"
|
|
}
|
|
function at_lead0(raw: pointer) -> pointer {
|
|
if raw[0] == '-' { return "-0" + raw[1 .. len(raw)] }
|
|
return "0" + raw
|
|
}
|
|
# the editor attributes a field may carry, and what each takes
|
|
function at_field_known(a: pointer) -> bool {
|
|
if (a == "Ref") or (a == "OneOf") or (a == "Range") or (a == "Unit") or (a == "Asset") or (a == "Color") { return true }
|
|
return (a == "Node") or (a == "Clip") or (a == "Material") or (a == "Tint") or at_flag(a)
|
|
}
|
|
# the attributes that take nothing: they say what a field is
|
|
function at_flag(a: pointer) -> bool {
|
|
return (a == "Color") or (a == "Derived") or (a == "Text") or (a == "Multiline") or (a == "Key")
|
|
}
|
|
# one field attribute, its '@' and name already read: its arguments, checked for shape, into group g
|
|
function at_field(g: int, a: pointer) -> void {
|
|
let args = at_args()
|
|
let n = g_at_argn
|
|
if (a == "Ref") or (a == "Node") or (a == "Clip") or (a == "Material") or (a == "Tint") {
|
|
if n != 1 or toks[pi - 2].kind != TK_ID { perr(`@{a} takes one name: @{a}(Items)`) }
|
|
}
|
|
if (a == "OneOf") and (n < 1 or not (g_at_allid or g_at_allstr)) { perr("@OneOf takes a prefix, constants or words: @OneOf(GR_), @OneOf(GR_GOLD, GR_SILVER) or @OneOf(\"box\", \"hull\")") }
|
|
g_at_strs_next = (a == "OneOf") and g_at_allstr
|
|
if (a == "Range") and n != 2 { perr("@Range takes the least and the most: @Range(0, 1.5)") }
|
|
if (a == "Range") and not g_at_allnum { perr("@Range takes two numbers: @Range(0, 1.5)") }
|
|
if (a == "Unit") and (n != 1 or toks[pi - 2].kind != TK_STR) { perr(`@{a} takes one string: @{a}("m/s")`) }
|
|
if (a == "Asset") and not at_asset_ok(args, n) { perr("@Asset takes a kind, and `map` for a path under each map's directory (then `optional`): @Asset(\"png\"), @Asset(\"png\", map), @Asset(\"png\", map, optional)") }
|
|
if a == "Unit" { at_unit_check(g_at_arg0, toks[pi - 2]) }
|
|
if at_flag(a) and n != 0 { perr(`@{a} takes nothing`) }
|
|
at_push(g, a, args, g_at_arg0, n)
|
|
}
|
|
# @Asset("png"), @Asset("png", map) or @Asset("png", map, optional)
|
|
function at_asset_ok(args: pointer, n: int) -> bool {
|
|
if n < 1 or n > 3 { return false }
|
|
let w = at_arg_names(args)
|
|
if len(w) != n { return false }
|
|
if n >= 2 and not (w[1] == "map") { return false }
|
|
if n == 3 and not (w[2] == "optional") { return false }
|
|
return true
|
|
}
|
|
# a field's group, made the first time it needs one
|
|
function at_field_group(f: Node) -> int {
|
|
if f.at < 0 { f.at = at_group() }
|
|
return f.at
|
|
}
|
|
# @AppendOnly / @ByKey / @PerMap / @Chunked(64) before a declaration: kept for the registry it is on
|
|
var g_at_decl_args: []pointer = new []pointer
|
|
function at_decl(a: pointer) -> void {
|
|
at_decl_with(a, "[]")
|
|
}
|
|
function at_decl_with(a: pointer, args: pointer) -> void {
|
|
push(g_at_decl, a)
|
|
push(g_at_decl_args, args)
|
|
}
|
|
function at_decl_reset() -> void {
|
|
if len(g_at_decl) > 0 { at_decl_clear() }
|
|
}
|
|
function at_decl_clear() -> void {
|
|
g_at_decl = new []pointer
|
|
g_at_decl_args = new []pointer
|
|
g_pm_pending = false # permap.ludic: @PerMap and @Chunked belong to one declaration
|
|
g_pm_chunk_pending = 0
|
|
g_tk_pending = null # i18n.ludic: @TextKey belongs to one declaration too
|
|
}
|
|
# the registry just declared takes the attributes read before it
|
|
function at_registry_take() -> int {
|
|
if len(g_at_decl) == 0 { return -1 }
|
|
let g = at_group()
|
|
var i = 0
|
|
while i < len(g_at_decl) {
|
|
var first: pointer = null
|
|
var n = 0
|
|
if not (g_at_decl_args[i] == "[]") {
|
|
first = g_at_decl_args[i][1 .. len(g_at_decl_args[i]) - 1]
|
|
n = 1
|
|
}
|
|
at_push(g, g_at_decl[i], g_at_decl_args[i], first, n)
|
|
i += 1
|
|
}
|
|
at_decl_clear()
|
|
return g
|
|
}
|
|
# once every registry is declared: each @Ref names one, each @Node / @Clip a glTF field and a
|
|
# listing @OneOf constants that exist, or the build
|
|
# stops with every one that does not
|
|
function at_check_refs() -> void {
|
|
var i = 0
|
|
while i < len(prog) {
|
|
let d = prog[i]
|
|
if d.kind == N_COMP {
|
|
var k = 0
|
|
while k < len(d.kids) {
|
|
let f = d.kids[k]
|
|
if f.at >= 0 { at_check_field(d, f) }
|
|
k += 1
|
|
}
|
|
}
|
|
i += 1
|
|
}
|
|
diag_stop_if_errors()
|
|
}
|
|
function at_check_field(d: Node, f: Node) -> void {
|
|
let g = f.at
|
|
var j = g_at_start[g]
|
|
while j < g_at_start[g] + g_at_count[g] {
|
|
if (g_at_name[j] == "Ref") and reg_find(g_at_first[j]) < 0 { at_target(d, f, j, "a registry") }
|
|
if (g_at_name[j] == "Ref") and pm_named(g_at_first[j]) and not (f.ty == "string") {
|
|
perr_at(f.file, f.line, f.col, `field {d.s}.{f.s}: @Ref({g_at_first[j]}) names a @PerMap registry, whose rows have no index a program can hold - use a string key: @Ref({g_at_first[j]}) {f.s}: string = ""`)
|
|
}
|
|
if (g_at_name[j] == "Node") or (g_at_name[j] == "Clip") or (g_at_name[j] == "Material") { at_check_node(d, f, g_at_name[j], g_at_first[j]) }
|
|
if g_at_name[j] == "OneOf" { at_check_oneof(d, f, j) }
|
|
if (g_at_name[j] == "Tint") and not at_const_exists(g_at_first[j]) { at_const_target(d, f, j, g_at_first[j]) }
|
|
j += 1
|
|
}
|
|
}
|
|
# a target this program does not have: a package can name the game's registry or constant without
|
|
# importing it, so that is a warning and the schema says unresolved; a name of another kind is an error
|
|
function at_target(d: Node, f: Node, j: int, want: pointer) -> void {
|
|
let x = g_at_first[j]
|
|
let where = `field {d.s}.{f.s}: @{g_at_name[j]}({x})`
|
|
if at_decl_named(x) {
|
|
perr_at(f.file, f.line, f.col, `{where} names {x}, which is not {want}`)
|
|
return
|
|
}
|
|
g_at_unres[j] = true
|
|
pwarn_at(f.file, f.line, f.col, `{where}: this program has no {x} - left unresolved`)
|
|
}
|
|
function at_const_target(d: Node, f: Node, j: int, x: pointer) -> void {
|
|
let where = `field {d.s}.{f.s}: @{g_at_name[j]}`
|
|
if at_decl_named(x) {
|
|
perr_at(f.file, f.line, f.col, `{where} names {x}, which is not a constant`)
|
|
return
|
|
}
|
|
g_at_unres[j] = true
|
|
pwarn_at(f.file, f.line, f.col, `{where}: this program has no constant {x} - left unresolved`)
|
|
}
|
|
|
|
# ---- units ----------------------------------------------------------------------------------------
|
|
# @Unit's spellings are ASCII, one each, so an editor and a converter read one word for one unit:
|
|
# "deg" is the angle, never "°". Anything else is a warning naming the canonical spelling.
|
|
function at_units() -> []pointer {
|
|
let us = new []pointer
|
|
push(us, "m"); push(us, "m/s"); push(us, "m/s2"); push(us, "s"); push(us, "min"); push(us, "h"); push(us, "d")
|
|
push(us, "deg"); push(us, "rad"); push(us, "rad/s"); push(us, "kg"); push(us, "N"); push(us, "N.m"); push(us, "%"); push(us, "px")
|
|
return us
|
|
}
|
|
function at_unit_known(u: pointer) -> bool {
|
|
let us = at_units()
|
|
var i = 0
|
|
while i < len(us) {
|
|
if (us[i] == u) { return true }
|
|
i += 1
|
|
}
|
|
return false
|
|
}
|
|
# the canonical spelling of a unit written another way, or null when there is no obvious one
|
|
function at_unit_canon(u: pointer) -> pointer {
|
|
if (u == "°") or (u == "degrees") or (u == "degree") or (u == "degs") { return "deg" }
|
|
if (u == "sec") or (u == "secs") or (u == "second") or (u == "seconds") { return "s" }
|
|
if (u == "m/s^2") or (u == "m/s²") or (u == "m/s/s") { return "m/s2" }
|
|
if (u == "Nm") or (u == "N m") or (u == "N*m") or (u == "N·m") { return "N.m" }
|
|
if (u == "radians") or (u == "radian") { return "rad" }
|
|
if (u == "rad/sec") { return "rad/s" }
|
|
if (u == "meters") or (u == "metres") or (u == "meter") or (u == "metre") { return "m" }
|
|
if (u == "mps") or (u == "m/sec") { return "m/s" }
|
|
if (u == "minutes") or (u == "minute") or (u == "mins") { return "min" }
|
|
if (u == "hours") or (u == "hour") or (u == "hr") or (u == "hrs") { return "h" }
|
|
if (u == "days") or (u == "day") { return "d" }
|
|
if (u == "kilograms") or (u == "kilogram") or (u == "kgs") { return "kg" }
|
|
if (u == "percent") or (u == "pct") { return "%" }
|
|
if (u == "pixels") or (u == "pixel") { return "px" }
|
|
if (u == "newtons") or (u == "newton") { return "N" }
|
|
return null
|
|
}
|
|
function at_unit_check(u: pointer, t: Tok) -> void {
|
|
if u == null or at_unit_known(u) { return }
|
|
let c = at_unit_canon(u)
|
|
if c != null {
|
|
pwarn_at(g_parse_file, t.line, t.col, `@Unit("{u}"): the unit is spelled "{c}"`)
|
|
return
|
|
}
|
|
pwarn_at(g_parse_file, t.line, t.col, `@Unit("{u}") is not a known unit (m, m/s, m/s2, s, min, h, d, deg, rad, rad/s, kg, N, N.m, %, px)`)
|
|
}
|