/* SPDX-License-Identifier: MIT */ /* * konscript.c * * Copyright (c) 2026 KonAki * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ #include "konscript.h" #include /*** variables ***/ int kon_scriptSwitch(const kon_script_t *vm, int id) { return id >= 0 && id < vm->switchCount && vm->switches && vm->switches[id] ? 1 : 0; } int32_t kon_scriptVar(const kon_script_t *vm, int id) { return id >= 0 && id < vm->varCount && vm->vars ? vm->vars[id] : 0; } void kon_scriptSetSwitch(kon_script_t *vm, int id, int on) { if (id >= 0 && id < vm->switchCount && vm->switches) vm->switches[id] = on ? 1 : 0; } void kon_scriptSetVar(kon_script_t *vm, int id, int32_t value) { if (id >= 0 && id < vm->varCount && vm->vars) vm->vars[id] = value; } /* done in 64 bits and clamped so a script can never overflow or divide by 0 */ static int32_t arithmetic(int32_t x, int how, int32_t y) { int64_t r; switch (how) { case KON_ADD: r = (int64_t)x + y; break; case KON_SUB: r = (int64_t)x - y; break; case KON_MUL: r = (int64_t)x * y; break; case KON_DIV: r = y ? (int64_t)x / y : x; break; case KON_MOD: r = y ? (int64_t)x % y : x; break; default: r = y; break; } if (r > INT32_MAX) r = INT32_MAX; if (r < INT32_MIN) r = INT32_MIN; return (int32_t)r; } static int32_t operand(const kon_script_t *vm, int n) { if (n >= KON_VARIABLE && n < KON_VARIABLE + 0x20000000) return kon_scriptVar(vm, n - KON_VARIABLE); return n; } static int compare(int32_t x, int how, int32_t y) { switch (how) { case KON_IS_EQ: return x == y; case KON_IS_NE: return x != y; case KON_IS_GE: return x >= y; case KON_IS_LE: return x <= y; case KON_IS_GT: return x > y; case KON_IS_LT: return x < y; } return 0; } /*** script ***/ void kon_scriptInit(kon_script_t *vm, const kon_cmd_t *cmds, int count, uint8_t *switches, int switchCount, int32_t *vars, int varCount) { kon_script_t zero = {0}; *vm = zero; vm->cmds = cmds; vm->count = cmds && count > 0 ? count : 0; vm->switches = switches; vm->switchCount = switches && switchCount > 0 ? switchCount : 0; vm->vars = vars; vm->varCount = vars && varCount > 0 ? varCount : 0; vm->status = KON_SCRIPT_DONE; } void kon_scriptStart(kon_script_t *vm, int where) { vm->pc = where; vm->depth = 0; vm->optionCount = 0; vm->wait = 0.0f; vm->status = where >= 0 && where < vm->count ? KON_SCRIPT_RUNNING : KON_SCRIPT_ERROR; } static int menuOptions(kon_script_t *vm, const kon_cmd_t *menu) { int i, n = menu->a; vm->optionCount = 0; if (n > KON_SCRIPT_MAX_OPTIONS) n = KON_SCRIPT_MAX_OPTIONS; for (i = 0; i < n; i++) { int at = vm->pc + 1 + i; if (at >= vm->count || vm->cmds[at].op != KON_OP_OPTION) return 0; if (vm->cmds[at].b >= 0 && !kon_scriptSwitch(vm, vm->cmds[at].b)) continue; vm->options[vm->optionCount++] = at; } return 1; } kon_scriptStatus_t kon_scriptRun(kon_script_t *vm) { int steps = 0; while (vm->status == KON_SCRIPT_RUNNING) { const kon_cmd_t *c; int next; if (vm->pc < 0 || vm->pc >= vm->count) { vm->status = KON_SCRIPT_ERROR; break; } if (++steps > 100000) { if (!vm->yieldAtLimit) vm->status = KON_SCRIPT_ERROR; break; } c = &vm->cmds[vm->pc]; next = vm->pc + 1; vm->cmd = vm->pc; switch (c->op) { case KON_OP_END: vm->status = KON_SCRIPT_DONE; break; case KON_OP_SAY: vm->status = KON_SCRIPT_SAY; break; case KON_OP_MENU: if (!menuOptions(vm, c)) { vm->status = KON_SCRIPT_ERROR; break; } if (vm->optionCount == 0) { next = vm->pc + 1 + (c->a > 0 ? c->a : 0); break; } vm->status = KON_SCRIPT_MENU; break; case KON_OP_OPTION: break; case KON_OP_JUMP: next = c->a; break; case KON_OP_SWITCH: kon_scriptSetSwitch(vm, c->a, c->b == 2 ? !kon_scriptSwitch(vm, c->a) : c->b); break; case KON_OP_VAR: kon_scriptSetVar(vm, c->a, arithmetic(kon_scriptVar(vm, c->a), c->b, operand(vm, c->c))); break; case KON_OP_IFSWITCH: if (kon_scriptSwitch(vm, c->a) != (c->b ? 1 : 0)) next = c->c; break; case KON_OP_IFVAR: if (!compare(kon_scriptVar(vm, c->a), c->b, operand(vm, c->c))) next = c->d; break; case KON_OP_WAIT: if (c->a > 0) { vm->wait = (float)c->a * 0.001f; vm->status = KON_SCRIPT_WAIT; } break; case KON_OP_HOST: if (vm->host && vm->host(vm, c, vm->user)) vm->status = KON_SCRIPT_HOST; break; case KON_OP_CALL: if (vm->depth >= KON_SCRIPT_MAX_CALLS) { vm->status = KON_SCRIPT_ERROR; break; } vm->stack[vm->depth++] = next; next = c->a; break; case KON_OP_RETURN: if (vm->depth == 0) vm->status = KON_SCRIPT_DONE; else next = vm->stack[--vm->depth]; break; default: vm->status = KON_SCRIPT_ERROR; break; } vm->pc = next; } return vm->status; } void kon_scriptUpdate(kon_script_t *vm, float dt) { if (vm->status != KON_SCRIPT_WAIT || !(dt > 0.0f)) return; vm->wait -= dt; if (vm->wait <= 0.0f) { vm->wait = 0.0f; vm->status = KON_SCRIPT_RUNNING; } } void kon_scriptResume(kon_script_t *vm) { if (vm->status == KON_SCRIPT_SAY || vm->status == KON_SCRIPT_HOST) vm->status = KON_SCRIPT_RUNNING; } const kon_cmd_t *kon_scriptCurrent(const kon_script_t *vm) { return vm->status == KON_SCRIPT_SAY || vm->status == KON_SCRIPT_HOST ? &vm->cmds[vm->cmd] : NULL; } int kon_scriptOptionCount(const kon_script_t *vm) { return vm->status == KON_SCRIPT_MENU ? vm->optionCount : 0; } const char *kon_scriptOption(const kon_script_t *vm, int i) { if (vm->status != KON_SCRIPT_MENU || i < 0 || i >= vm->optionCount) return NULL; return vm->cmds[vm->options[i]].text; } void kon_scriptChoose(kon_script_t *vm, int i) { if (vm->status != KON_SCRIPT_MENU || i < 0 || i >= vm->optionCount) return; vm->pc = vm->cmds[vm->options[i]].a; vm->optionCount = 0; vm->status = KON_SCRIPT_RUNNING; } /*** text scripts ***/ #include enum { T_END, T_WORD, T_STRING, T_BAD }; typedef struct lexer { const char *s; int pos, kind, start, len; } lexer_t; typedef struct parse { kon_scriptBuild_t *b; int pass, line, menu; /* menu is the number of the MENU command while its options are coming, else -1 */ int ok; } parse_t; static void problem(parse_t *p, const char *message) { if (!p->ok) return; p->ok = 0; p->b->errorLine = p->line; strncpy(p->b->error, message, sizeof p->b->error - 1); p->b->error[sizeof p->b->error - 1] = '\0'; } static void lex(lexer_t *l) { while (l->s[l->pos] == ' ' || l->s[l->pos] == '\t' || l->s[l->pos] == '\r') l->pos++; if (l->s[l->pos] == '\0' || l->s[l->pos] == '\n' || l->s[l->pos] == '#') { l->kind = T_END; return; } if (l->s[l->pos] == '"') { l->pos++; l->start = l->pos; while (l->s[l->pos] != '"') { if (l->s[l->pos] == '\0' || l->s[l->pos] == '\n') { l->kind = T_BAD; return; } if (l->s[l->pos] == '\\' && l->s[l->pos + 1] != '\0' && l->s[l->pos + 1] != '\n') l->pos++; l->pos++; } l->len = l->pos - l->start; l->pos++; l->kind = T_STRING; return; } l->start = l->pos; while (l->s[l->pos] != '\0' && l->s[l->pos] != '\n' && l->s[l->pos] != ' ' && l->s[l->pos] != '\t' && l->s[l->pos] != '\r' && l->s[l->pos] != '"' && l->s[l->pos] != '#') l->pos++; l->len = l->pos - l->start; l->kind = T_WORD; } static int is(const lexer_t *l, const char *word) { return l->kind == T_WORD && (int)strlen(word) == l->len && strncmp(l->s + l->start, word, (size_t)l->len) == 0; } /* a whole number, with a minus if it has one */ static int number(const lexer_t *l, long *out) { int i = 0; long v = 0; int negative = 0; if (l->kind != T_WORD || l->len == 0) return 0; if (l->s[l->start] == '-') { negative = 1; i = 1; } if (i >= l->len) return 0; for (; i < l->len; i++) { char c = l->s[l->start + i]; if (c < '0' || c > '9') return 0; if (v > 200000000L) return 0; v = v * 10 + (c - '0'); } *out = negative ? -v : v; return 1; } /* s12 or v3 */ static int reference(const lexer_t *l, char kind, long *out) { lexer_t rest = *l; if (l->kind != T_WORD || l->len < 2 || l->s[l->start] != kind) return 0; rest.start++; rest.len--; if (rest.s[rest.start] == '-') return 0; return number(&rest, out); } static void emit(parse_t *p, int op, int a, int b, int c, int d, const char *text) { kon_scriptBuild_t *bd = p->b; if (!p->ok) return; if (p->pass == 2) { kon_cmd_t *cmd; if (bd->count >= bd->maxCmds) { problem(p, "too many commands"); return; } cmd = &bd->cmds[bd->count]; cmd->op = op; cmd->a = a; cmd->b = b; cmd->c = c; cmd->d = d; cmd->text = text; } bd->count++; } /* the string as the pool holds it, with the escapes turned into the letters they stand for */ static const char *keep(parse_t *p, const lexer_t *l) { kon_scriptBuild_t *bd = p->b; char *out; int i, n = 0; if (p->pass == 1) return NULL; if (!bd->pool || bd->poolUsed + l->len + 1 > bd->poolSize) { problem(p, "out of room for text"); return NULL; } out = bd->pool + bd->poolUsed; for (i = 0; i < l->len; i++) { char c = l->s[l->start + i]; if (c == '\\' && i + 1 < l->len) { c = l->s[l->start + ++i]; if (c == 'n') c = '\n'; else if (c == 't') c = '\t'; } out[n++] = c; } out[n] = '\0'; bd->poolUsed += n + 1; return out; } static unsigned labelHash(const char *name, int len) { unsigned h = 2166136261u; int i; for (i = 0; i < len; i++) h = (h ^ (unsigned char)name[i]) * 16777619u; return h; } static int sameLabel(const kon_scriptLabel_t *label, const char *name, int len) { return (int)strlen(label->name) == len && strncmp(label->name, name, (size_t)len) == 0; } static int hashed(const kon_scriptBuild_t *b) { return b->hash && b->hashSize > 0 && (b->hashSize & (b->hashSize - 1)) == 0 && b->hashSize >= 2 * b->maxLabels; } static int labelAt(const kon_scriptBuild_t *b, const char *name, int len) { int i; if (hashed(b)) { unsigned mask = (unsigned)b->hashSize - 1, slot = labelHash(name, len) & mask; while (b->hash[slot]) { if (sameLabel(&b->labels[b->hash[slot] - 1], name, len)) return b->labels[b->hash[slot] - 1].at; slot = (slot + 1) & mask; } return -1; } for (i = 0; i < b->labelCount; i++) { if (sameLabel(&b->labels[i], name, len)) return b->labels[i].at; } return -1; } int kon_scriptFind(const kon_scriptBuild_t *b, const char *label) { return label ? labelAt(b, label, (int)strlen(label)) : -1; } static int target(parse_t *p, lexer_t *l) { int at; lex(l); if (l->kind != T_WORD) { problem(p, "a label is needed"); return 0; } if (p->pass == 1) return 0; at = labelAt(p->b, l->s + l->start, l->len); if (at < 0) { problem(p, "unknown label"); return 0; } return at; } static int nameNumber(const char *const *names, int count, const lexer_t *l) { int i; long v; if (number(l, &v)) return (int)v; for (i = 0; names && i < count; i++) { if ((int)strlen(names[i]) == l->len && strncmp(names[i], l->s + l->start, (size_t)l->len) == 0) return i; } return -2; } static void defineLabel(parse_t *p, const lexer_t *l, int len) { kon_scriptBuild_t *b = p->b; if (p->pass != 1) return; if (len < 1 || len >= KON_SCRIPT_LABEL) { problem(p, "label name is empty or too long"); return; } if (labelAt(b, l->s + l->start, len) >= 0) { problem(p, "label defined twice"); return; } if (b->labelCount >= b->maxLabels || !b->labels) { problem(p, "too many labels"); return; } memcpy(b->labels[b->labelCount].name, l->s + l->start, (size_t)len); b->labels[b->labelCount].name[len] = '\0'; b->labels[b->labelCount].at = b->count; if (hashed(b)) { unsigned mask = (unsigned)b->hashSize - 1, slot = labelHash(l->s + l->start, len) & mask; while (b->hash[slot]) slot = (slot + 1) & mask; b->hash[slot] = b->labelCount + 1; } b->labelCount++; } static void ending(parse_t *p, lexer_t *l) { lex(l); if (l->kind != T_END) problem(p, "something extra on the line"); } static void statement(parse_t *p, const char *line) { lexer_t l; const kon_scriptNames_t *names = p->b->names; long n, m; l.s = line; l.pos = 0; lex(&l); if (l.kind == T_END) return; if (l.kind != T_WORD) { problem(p, "a command is needed"); return; } if (!is(&l, "option") && p->menu >= 0 && p->pass == 2 && p->b->cmds[p->menu].a == 0) { problem(p, "a menu needs options"); return; } if (l.len > 1 && l.s[l.start + l.len - 1] == ':') { p->menu = -1; defineLabel(p, &l, l.len - 1); ending(p, &l); return; } if (is(&l, "label")) { lex(&l); p->menu = -1; if (l.kind != T_WORD) problem(p, "a label name is needed"); else defineLabel(p, &l, l.len); ending(p, &l); return; } if (is(&l, "option")) { const char *text; int at, needs = -1; if (p->menu < 0 && p->pass == 2) { problem(p, "option without a menu"); return; } lex(&l); if (l.kind != T_STRING) { problem(p, "option needs its text in quotes"); return; } text = keep(p, &l); at = target(p, &l); lex(&l); if (is(&l, "if")) { lex(&l); if (!reference(&l, 's', &n)) { problem(p, "if needs a switch like s3"); return; } needs = (int)n; lex(&l); } if (l.kind != T_END) { problem(p, "something extra on the line"); return; } emit(p, KON_OP_OPTION, at, needs, 0, 0, text); if (p->pass == 2 && p->ok) p->b->cmds[p->menu].a++; return; } p->menu = -1; if (is(&l, "end")) { emit(p, KON_OP_END, 0, 0, 0, 0, NULL); ending(p, &l); } else if (is(&l, "return")) { emit(p, KON_OP_RETURN, 0, 0, 0, 0, NULL); ending(p, &l); } else if (is(&l, "menu")) { p->menu = p->b->count; emit(p, KON_OP_MENU, 0, 0, 0, 0, NULL); ending(p, &l); } else if (is(&l, "jump") || is(&l, "call")) { int op = is(&l, "jump") ? KON_OP_JUMP : KON_OP_CALL; int at = target(p, &l); emit(p, op, at, 0, 0, 0, NULL); ending(p, &l); } else if (is(&l, "wait")) { lex(&l); if (!number(&l, &n) || n < 0) { problem(p, "wait needs milliseconds"); return; } emit(p, KON_OP_WAIT, (int)n, 0, 0, 0, NULL); ending(p, &l); } else if (is(&l, "on") || is(&l, "off") || is(&l, "flip")) { int how = is(&l, "on") ? 1 : (is(&l, "off") ? 0 : 2); lex(&l); if (!reference(&l, 's', &n)) { problem(p, "a switch like s3 is needed"); return; } emit(p, KON_OP_SWITCH, (int)n, how, 0, 0, NULL); ending(p, &l); } else if (is(&l, "say")) { int who; const char *text; lex(&l); if (l.kind != T_WORD) { problem(p, "say needs who is talking"); return; } who = is(&l, "-") ? -1 : nameNumber(names ? names->speakers : NULL, names ? names->speakerCount : 0, &l); if (who == -2) { problem(p, "unknown speaker"); return; } lex(&l); if (l.kind != T_STRING) { problem(p, "say needs its text in quotes"); return; } text = keep(p, &l); emit(p, KON_OP_SAY, who, 0, 0, 0, text); ending(p, &l); } else if (is(&l, "set")) { static const char *ops[] = {"=", "+=", "-=", "*=", "/=", "%="}; int op = -1, i; lex(&l); if (!reference(&l, 'v', &n)) { problem(p, "a variable like v3 is needed"); return; } lex(&l); for (i = 0; i < 6; i++) { if (is(&l, ops[i])) op = i; } if (op < 0) { problem(p, "set needs = += -= *= /= or %="); return; } lex(&l); if (reference(&l, 'v', &m)) { m += KON_VARIABLE; } else if (!number(&l, &m) || m >= KON_VARIABLE) { problem(p, "a number or variable is needed"); return; } emit(p, KON_OP_VAR, (int)n, op, (int)m, 0, NULL); ending(p, &l); } else if (is(&l, "if")) { lex(&l); if (reference(&l, 's', &n)) { int want; int at; lex(&l); if (is(&l, "on")) want = 1; else if (is(&l, "off")) want = 0; else { problem(p, "if needs on or off"); return; } lex(&l); if (!is(&l, "else")) { problem(p, "if needs else and a label"); return; } at = target(p, &l); emit(p, KON_OP_IFSWITCH, (int)n, want, at, 0, NULL); ending(p, &l); } else if (reference(&l, 'v', &n)) { static const char *cmps[] = {"==", "!=", ">=", "<=", ">", "<"}; int cmp = -1, i, at; lex(&l); for (i = 0; i < 6; i++) { if (is(&l, cmps[i])) cmp = i; } if (cmp < 0) { problem(p, "if needs == != >= <= > or <"); return; } lex(&l); if (reference(&l, 'v', &m)) { m += KON_VARIABLE; } else if (!number(&l, &m) || m >= KON_VARIABLE) { problem(p, "a number or variable is needed"); return; } lex(&l); if (!is(&l, "else")) { problem(p, "if needs else and a label"); return; } at = target(p, &l); emit(p, KON_OP_IFVAR, (int)n, cmp, (int)m, at, NULL); ending(p, &l); } else { problem(p, "if needs a switch or variable"); } } else if (is(&l, "host")) { int id, args[3] = {0, 0, 0}, i = 0; const char *text = NULL; lex(&l); if (l.kind != T_WORD) { problem(p, "host needs a name or number"); return; } id = nameNumber(names ? names->hosts : NULL, names ? names->hostCount : 0, &l); if (id == -2) { problem(p, "unknown host command"); return; } for (;;) { lex(&l); if (l.kind == T_STRING) { text = keep(p, &l); break; } if (l.kind != T_WORD) break; if (i >= 3 || !number(&l, &n)) { problem(p, "host takes up to three numbers then a string"); return; } args[i++] = (int)n; } if (l.kind == T_BAD) { problem(p, "a string is not closed"); return; } emit(p, KON_OP_HOST, id, args[0], args[1], args[2], text); if (l.kind == T_STRING) ending(p, &l); } else { problem(p, "unknown command"); } if (l.kind == T_BAD) problem(p, "a string is not closed"); } static void run(parse_t *p, const char *source) { const char *s = source; char line[4096]; p->line = 0; p->menu = -1; while (*s && p->ok) { size_t n = 0; p->line++; while (s[n] && s[n] != '\n') n++; if (n >= sizeof line) { problem(p, "line too long"); return; } memcpy(line, s, n); line[n] = '\0'; statement(p, line); s += n; if (*s == '\n') s++; } if (p->ok && p->pass == 2 && p->menu >= 0 && p->b->cmds[p->menu].a == 0) problem(p, "a menu needs options"); } int kon_scriptParse(kon_scriptBuild_t *b, const char *source) { parse_t p; b->count = b->labelCount = b->poolUsed = 0; if (hashed(b)) memset(b->hash, 0, (size_t)b->hashSize * sizeof *b->hash); b->errorLine = 0; b->error[0] = '\0'; if (!source || !b->cmds || b->maxCmds < 0) { b->errorLine = 1; strcpy(b->error, "no source or no room"); return 0; } p.b = b; p.ok = 1; p.pass = 1; run(&p, source); if (!p.ok) { b->count = 0; return 0; } if (b->count > b->maxCmds) { b->errorLine = 1; strcpy(b->error, "too many commands"); b->count = 0; return 0; } b->count = 0; p.pass = 2; run(&p, source); if (!p.ok) b->count = 0; return p.ok; }