diff --git a/README.md b/README.md index d066faf..2b94e89 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ mixin runGame!(ready, update, finish); ## Features * Immediate Mode UI: Build UIs quickly. -* Dialogue System: Create interactive stories using a simple stack-oriented scripting language. +* Dialogue System: Create interactive stories using a stack-oriented scripting language. * Sprite Animation Support: Manage animations for characters and objects. * Tile Map Support: Create and draw tile-based maps. @@ -38,7 +38,7 @@ A list of projects made with Parin is available in the [PROJECTS.md](PROJECTS.md ## Installation -This guide shows how to install Parin and its dependencies using DUB. +This guide shows how to install Parin and its dependencies using [DUB](https://dub.pm/). While DUB simplifies the process, Parin itself doesn't require DUB. Parin has the following dependencies: diff --git a/assets/test.rin b/assets/test.rin deleted file mode 100644 index d08e601..0000000 --- a/assets/test.rin +++ /dev/null @@ -1,62 +0,0 @@ -## A script that tests if things work correctly. - -$ line ECHOC result ECHO - -# [ Math Test ] -$ LINE ECHOC 10 6 + 16 = ECHO -$ LINE ECHOC 10 6 - 4 = ECHO -$ LINE ECHOC 6 10 - -4 = ECHO -$ LINE ECHOC 10 6 * 60 = ECHO -$ LINE ECHOC 10 6 / 1 = ECHO -$ LINE ECHOC 6 10 / 0 = ECHO -$ LINE ECHOC 6 0 / 0 = ECHO -$ LINE ECHOC 10 6 % 4 = ECHO -$ LINE ECHOC 6 10 % 6 = ECHO -$ LINE ECHOC 6 0 % 0 = ECHO -$ LINE ECHOC 0 0 & 0 = ECHO -$ LINE ECHOC 0 1 & 0 = ECHO -$ LINE ECHOC 1 0 & 0 = ECHO -$ LINE ECHOC 1 1 & 1 = ECHO -$ LINE ECHOC 0 0 | 0 = ECHO -$ LINE ECHOC 0 1 | 1 = ECHO -$ LINE ECHOC 1 0 | 1 = ECHO -$ LINE ECHOC 1 1 | 1 = ECHO -$ LINE ECHOC 6 10 < 1 = ECHO -$ LINE ECHOC 6 10 > 0 = ECHO -$ LINE ECHOC 6 10 = 0 = ECHO -$ LINE ECHOC 6 10 ! 0 = ECHO -$ LINE ECHOC 6 0 ! 1 = ECHO -$ LINE ECHOC 6 0 ~ 6 = ECHO - -# [ Operator Test ] -$ LINE ECHOC 6 10 SWAP 6 = ECHO -$ LINE ECHOC 6 10 COPY 10 = ECHO -$ LINE ECHOC 6 10 COPYN 10 = IF 6 = IF 1 THEN ELSE 0 THEN ECHO -$ LINE ECHOC 5 6 10 RANGE NOT ECHO -$ LINE ECHOC 6 6 10 RANGE ECHO -$ LINE ECHOC 10 6 10 RANGE ECHO -$ LINE ECHOC 11 6 10 RANGE NOT ECHO -$ LINE ECHOC x 1 CAT x1 SAME ECHO -$ LINE ECHOC x x CAT xx SAME ECHO -$ LINE ECHOC 0 WORD NOT ECHO -$ LINE ECHOC x WORD ECHO -$ LINE ECHOC 0 NUMBER ECHO -$ LINE ECHOC x NUMBER NOT ECHO -$ LINE ECHOC 1 ECHO 0 END ECHO -$ LINE ECHOC _x HERE NOT ECHO -$ LINE ECHOC _x INIT _x HERE ECHO -$ LINE ECHOC _x GET 0 = ECHO -$ LINE ECHOC _x _x GETN 0 = IF 0 = IF 1 THEN ELSE 0 THEN ECHO -$ LINE ECHOC _x 6 SET _x GET 6 = ECHO -$ LINE ECHOC _x DROP _x HERE NOT ECHO -$ LINE ECHOC _x INIT _x INC 1 = _x INIT ECHO -$ LINE ECHOC _x INIT _x DEC -1 = _x INIT ECHO -$ LINE ECHOC _x 2 INCN 2 = _x INIT ECHO -$ LINE ECHOC _x 2 DECN -2 = _x INIT ECHO -$ LINE ECHOC _x TOG 1 = _x INIT ECHO - -# [ Loop Test ] -$ _x INIT -* -$ _x INC 10 < IF LOOP THEN -$ LINE ECHOC _x GET 10 = _x INIT ECHO diff --git a/examples/intro/story.d b/examples/intro/story.d index 3bbd44b..971909d 100644 --- a/examples/intro/story.d +++ b/examples/intro/story.d @@ -1,16 +1,14 @@ /// This example shows how to use the Parin dialogue system. import parin; -import parin.story; auto story = Story(); auto script = " # A comment. * label | Hello world! - | This is a text line. - | The next line will be a menu line. - ^ Option number 1 ^ Option number 2 ^ Go to end + | This is a text line and the next is a menu line. + ^ Option 1 ^ Option 2 ^ Option 3 $ MENU SKIP * @@ -22,11 +20,9 @@ auto script = " $ end JUMP * end - | Expression lines are used to go to labels. | JUMP goes to the label with the given name. | SKIP skips ahead N labels. | MENU returns the selected option. - | That's it. I Hope this helps. "; void ready() { diff --git a/rin/README.md b/rin/README.md index 712b940..29e8c16 100644 --- a/rin/README.md +++ b/rin/README.md @@ -1,9 +1,18 @@ # Rin -A Parin script interpreter. -Works both with and without DUB. +A script interpreter for Rin. +It helps with error checking and debugging, making it easier to work with Parin scripts. ## Usage * With DUB: `dub run parin:rin` * Without DUB: `dmd -run -Ijoka_path -Iparin_path source/app.d` + +## Options + +* -debug: Runs the script in debug mode. This will change the value of DEBUG. +* -linear: Runs the script in linear mode. This will force the interpreter to go over every line. + +## Editor Themes + +Rin syntax highlighting files for various text editors are available in the [assets](./assets/) folder. diff --git a/rin/assets/rin.nanorc b/rin/assets/rin.nanorc new file mode 100644 index 0000000..ef35750 --- /dev/null +++ b/rin/assets/rin.nanorc @@ -0,0 +1,14 @@ +## Rin Scripting Language + +syntax "Rin" "\.rin$" +magic "^(Rin) (source|program)" +comment "#" + +# Operators +color brightblue "\<(ADD|SUB|MUL|DIV|MOD|AND|OR|LESS|GREATER|EQUAL|NOT|POP|CLEAR|SWAP|COPY|COPYN|RANGE|IF|ELSE|THEN|CAT|SAME|WORD|NUMBER|LINE|DEBUG|LINEAR|ASSERT|END|ECHO|ECHON|LEAK|LEAKN|HERE|GET|GETN|SET|INIT|DROP|DROPN|INC|DEC|INCN|DECN|TOG|MENU|LOOP|SKIP|JUMP|CALL)\>" +# Lines +color cyan "^[ \t]*\*" "^[ \t]*\|" "^[ \t]*\." "^[ \t]*\^" "^[ \t]*\$" +# Comments +color green "^[ \t]*#.*" +# Trailing Whitespace +color ,green "[[:space:]]+$" diff --git a/rin/dub.json b/rin/dub.json index 3c7bf7b..471e69f 100644 --- a/rin/dub.json +++ b/rin/dub.json @@ -3,7 +3,7 @@ "Alexandros F. G. Kapretsos" ], "copyright": "Copyright © 2024, Alexandros F. G. Kapretsos", - "description": "A Parin script interpreter.", + "description": "A script interpreter for Rin.", "license": "MIT", "name": "rin", "dependencies": { diff --git a/rin/examples/hello.rin b/rin/examples/hello.rin new file mode 100644 index 0000000..e9fc1f5 --- /dev/null +++ b/rin/examples/hello.rin @@ -0,0 +1,12 @@ +## A script that serves as a classic hello-world program, introducing the structure of a Parin script. + +# A comment. +* label +| Hello world! +| This is a text line and the next is an expression line. +$ end JUMP + +| You can't see me... + +* end +| JUMP goes to the label with the given name. diff --git a/assets/shape.rin b/rin/examples/shape.rin similarity index 100% rename from assets/shape.rin rename to rin/examples/shape.rin diff --git a/rin/source/app.d b/rin/source/app.d index 962adda..757f601 100644 --- a/rin/source/app.d +++ b/rin/source/app.d @@ -3,28 +3,37 @@ import joka; import parin.story; -IStr path; -Story story; +RinState rinState; + +struct RinState { + IStr scriptPath; + Story story; +} void printError(Sz index, IStr text) { - printfln("\n{}({}): {}", path, index, text); + printfln("\n{}({}): {}", rinState.scriptPath, index, text); } Fault prepareStory() { - if (auto fault = story.prepare()) { - auto index = story.faultPrepareIndex + 1; - printError(index, "Invalid character at the beginning of the line."); + if (auto fault = rinState.story.prepare()) { + auto index = rinState.story.faultPrepareIndex + 1; + switch (fault) with (Fault) { + overflow: printError(index, "Label is too long."); break; + cantParse: printError(index, "Invalid character at the beginning of the line."); break; + default: break; + } return fault; } return Fault.none; } Fault updateStory() { - if (story.hasText) println(story.text); - if (auto fault = story.update()) { - auto index = story.lineIndex + 1; + if (rinState.story.hasText) println(rinState.story.text); + if (auto fault = rinState.story.update()) { + auto index = rinState.story.lineIndex + 1; switch (fault) with (Fault) { - case invalid: printError(index, "Invalid arguments passed to the `{}` operator.".format(story.faultOp)); break; + case some: printError(index, "Assertion failed."); break; + case invalid: printError(index, "Invalid arguments passed to the `{}` operator.".format(rinState.story.faultOp)); break; case overflow: printError(index, "A word or number is too long."); break; case cantParse: printError(index, "A word, number, or operator contains invalid characters."); break; default: break; @@ -34,29 +43,33 @@ Fault updateStory() { return Fault.none; } -int main(string[] args) { +int rinMain(string[] args) { if (args.length == 1) { println("Usage: rin [options] script"); println("Options: -debug -linear"); return 0; } foreach (arg; args[1 .. $ - 1]) { - if (arg == "-debug") story.debugMode = true; - if (arg == "-linear") story.linearMode = true; + if (arg == "-debug") rinState.story.debugMode = true; + if (arg == "-linear") rinState.story.linearMode = true; } - path = args[$ - 1]; - if (auto fault = readTextIntoBuffer(path, story.script)) { + rinState.scriptPath = args[$ - 1]; + if (auto fault = readTextIntoBuffer(rinState.scriptPath, rinState.story.script)) { switch (fault) { - case Fault.cantOpen: println("Can't find file `{}`.".format(path)); break; - case Fault.cantRead: println("Can't read file `{}`.".format(path)); break; + case Fault.cantOpen: println("Can't find file `{}`.".format(rinState.scriptPath)); break; + case Fault.cantRead: println("Can't read file `{}`.".format(rinState.scriptPath)); break; default: break; } return 1; } if (prepareStory()) return 1; if (updateStory()) return 1; - while (story.lineIndex != story.lineCount) { + while (rinState.story.lineIndex != rinState.story.lineCount) { if (updateStory()) return 1; } return 0; } + +int main(string[] args) { + return rinMain(args); +} diff --git a/setup/README.md b/setup/README.md index b32b228..a4b7d01 100644 --- a/setup/README.md +++ b/setup/README.md @@ -1,7 +1,7 @@ # Setup A helper script that automates the project setup. -It creates some files in the current folder and works both with and without DUB. +It creates files in the current folder and supports both DUB and non-DUB projects. ## Usage diff --git a/source/parin/dialogue.d b/source/parin/dialogue.d deleted file mode 100644 index 714f64b..0000000 --- a/source/parin/dialogue.d +++ /dev/null @@ -1,358 +0,0 @@ -// --- -// Copyright 2024 Alexandros F. G. Kapretsos -// SPDX-License-Identifier: MIT -// Email: alexandroskapretsos@gmail.com -// Project: https://github.com/Kapendev/parin -// Version: v0.0.34 -// --- - -// TODO: The DialogueCommandRunner should work with gc functions too. Think about how to do it. -// TODO: Update all the doc comments here. - -/// The `dialogue` module provides a simple and versatile dialogue system. -module parin.dialogue; - -import joka.ascii; -import parin.engine; - -@safe: - -enum DialogueUnitKindChars = ".#*@>|^!+-$"; - -enum DialogueUnitKind { - pause = '.', - comment = '#', - point = '*', - jump = '@', - actor = '>', - line = '|', - menu = '^', - variable = '!', - plus = '+', - minus = '-', - command = '$', -} - -struct DialogueUnit { - LStr text; - DialogueUnitKind kind; - - @safe @nogc nothrow: - - void free() { - text.free(); - this = DialogueUnit(); - } -} - -struct DialogueValue { - LStr name; - long value; - - @safe @nogc nothrow: - - void free() { - name.free(); - this = DialogueValue(); - } -} - -alias DialogueCommandRunner = void function(IStr[] args) @trusted; - -struct Dialogue { - List!DialogueUnit units; - List!DialogueValue values; - IStr text; - IStr actor; - Sz unitIndex; - - @trusted - void run(DialogueCommandRunner runner) { - runner(args); - update(); - } - - @safe @nogc nothrow: - - bool isEmpty() { - return units.length == 0; - } - - bool hasKind(DialogueUnitKind kind) { - return unitIndex < units.length && units[unitIndex].kind == kind; - } - - bool hasPause() { - return hasKind(DialogueUnitKind.pause); - } - - bool hasChoices() { - return hasKind(DialogueUnitKind.menu); - } - - bool hasArgs() { - return hasKind(DialogueUnitKind.command); - } - - bool canUpdate() { - return !hasPause; - } - - IStr[] choices() { - static IStr[16] buffer; - - auto length = 0; - auto temp = hasChoices ? units[unitIndex].text.items : ""; - while (temp.length != 0) { - buffer[length] = temp.skipValue(DialogueUnitKind.menu).trim(); - length += 1; - } - return buffer[0 .. length]; - } - - IStr[] args() { - static IStr[16] buffer; - - auto length = 0; - auto temp = hasArgs ? units[unitIndex].text.items : ""; - while (temp.length != 0) { - buffer[length] = temp.skipValue(' ').trim(); - length += 1; - } - return buffer[0 .. length]; - } - - void reset() { - text = ""; - actor = ""; - unitIndex = 0; - } - - void jump(IStr point) { - if (point.length == 0) { - foreach (i; unitIndex + 1 .. units.length) { - auto unit = units[i]; - if (unit.kind == DialogueUnitKind.point) { - unitIndex = i; - break; - } - } - } else { - foreach (i; 0 .. units.length) { - auto unit = units[i]; - if (unit.kind == DialogueUnitKind.point && unit.text.items == point) { - unitIndex = i; - break; - } - } - } - } - - void jump(Sz i) { - auto currPoint = 0; - foreach (j, unit; units.items) { - if (unit.kind == DialogueUnitKind.point) { - if (currPoint == i) { - unitIndex = j; - break; - } - currPoint += 1; - } - } - } - - void skip(Sz count) { - foreach (i; 0 .. count) { - jump(""); - } - } - - void pick(Sz i) { - skip(i + 1); - update(); - } - - // TODO: Remove the asserts! - void update() { - if (units.length != 0 && unitIndex < units.length - 1) { - unitIndex += 1; - text = units[unitIndex].text.items; - final switch (units[unitIndex].kind) { - case DialogueUnitKind.line, DialogueUnitKind.menu, DialogueUnitKind.command, DialogueUnitKind.pause: { - break; - } - case DialogueUnitKind.comment, DialogueUnitKind.point: { - update(); - break; - } - case DialogueUnitKind.actor: { - actor = text; - update(); - break; - } - case DialogueUnitKind.jump: { - jump(text); - update(); - break; - } - case DialogueUnitKind.variable: { - auto variableIndex = -1; - auto view = text; - auto name = trim(skipValue(view, '=')); - auto value = trim(skipValue(view, '=')); - if (name.length == 0) { - assert(0, "TODO: A variable without a name is an error for now."); - } - // Find if variable exists. - foreach (i, variable; values.items) { - if (variable.name.items == name) { - variableIndex = cast(int) i; - break; - } - } - // Create variable if it does not exist. - if (variableIndex < 0) { - auto variable = DialogueValue(); - variable.name.append(name); - values.append(variable); - variableIndex = cast(int) values.length - 1; - } - // Set variable value. - if (value.length != 0) { - auto conv = toSigned(value); - if (conv.isNone) { - auto valueVariableIndex = -1; - auto valueName = value; - // Find if variable exists. - foreach (i, variable; values.items) { - if (variable.name.items == valueName) { - valueVariableIndex = cast(int) i; - break; - } - } - if (valueVariableIndex < 0) { - assert(0, "TODO: A variable that does not exist it an error for now."); - } else { - values[variableIndex].value = values[valueVariableIndex].value; - } - } else { - values[variableIndex].value = conv.value; - } - } - update(); - break; - } - case DialogueUnitKind.plus, DialogueUnitKind.minus: { - auto variableIndex = -1; - auto name = text; - // Find if variable exists. - foreach (i, variable; values.items) { - if (variable.name.items == name) { - variableIndex = cast(int) i; - break; - } - } - // Add/Remove from variable. - if (variableIndex < 0) { - assert(0, "TODO: A variable that does not exist it an error for now."); - } - if (units[unitIndex].kind == DialogueUnitKind.plus) { - values[variableIndex].value += 1; - } else { - values[variableIndex].value -= 1; - } - update(); - break; - } - } - } - } - - Fault parse(IStr script) { - clear(); - if (script.length == 0) { - return Fault.invalid; - } - - units.append(DialogueUnit(LStr(), DialogueUnitKind.pause)); - auto isFirstLine = true; - auto view = script; - while (view.length != 0) { - auto line = trim(skipLine(view)); - if (line.length == 0) { - continue; - } - auto text = trimStart(line[1 .. $]); - auto kind = line[0]; - if (isFirstLine) { - isFirstLine = false; - if (kind == DialogueUnitKind.pause) { - continue; - } - } - if (isValidDialogueUnitKind(kind)) { - auto realKind = cast(DialogueUnitKind) kind; - units.append(DialogueUnit(LStr(text), realKind)); - } else { - clear(); - return Fault.invalid; - } - } - if (units.items[$ - 1].kind != DialogueUnitKind.pause) { - units.append(DialogueUnit(LStr(), DialogueUnitKind.pause)); - } - return Fault.none; - } - - void clear() { - foreach (ref unit; units) { - unit.free(); - } - units.clear(); - foreach (ref variable; values) { - variable.free(); - } - values.clear(); - reset(); - } - - void free() { - foreach (ref unit; units) { - unit.free(); - } - units.free(); - foreach (ref variable; values) { - variable.free(); - } - values.free(); - reset(); - } -} - -@safe @nogc nothrow: - -bool isValidDialogueUnitKind(char c) { - foreach (kind; DialogueUnitKindChars) { - if (c == kind) { - return true; - } - } - return false; -} - -Result!Dialogue toDialogue(IStr script) { - auto value = Dialogue(); - auto fault = value.parse(script); - if (fault) { - value.free(); - } - return Result!Dialogue(value, fault); -} - -Result!Dialogue loadRawDialogue(IStr path) { - auto temp = loadTempText(path); - if (temp.isNone) { - return Result!Dialogue(temp.fault); - } - return toDialogue(temp.get()); -} diff --git a/source/parin/story.d b/source/parin/story.d index 38864fd..4d62aa2 100644 --- a/source/parin/story.d +++ b/source/parin/story.d @@ -1,9 +1,13 @@ -// TODO: skipValue might need some work. Not clear how splitting works. -// TODO: toStr char arrays might need some work. It has bad error messages for them. -// TODO: concat and others might need a "intoBuffer" vesion. -// TODO: Look at CAT case and think about how to make it better with joka. -// NOTE: Remember to update both joka and parin at the same time because there was a evil change. -// NOTE: I will start cleanning and then will add CALL. +// --- +// Copyright 2024 Alexandros F. G. Kapretsos +// SPDX-License-Identifier: MIT +// Email: alexandroskapretsos@gmail.com +// Project: https://github.com/Kapendev/parin +// Version: v0.0.34 +// --- + +// TODO: Update all the doc comments here. +// TODO: Think about lineIndex and nextLineIndex updating. /// The `story` module provides a simple and versatile dialogue system. module parin.story; @@ -14,6 +18,8 @@ import joka.ascii; import joka.io; import joka.unions; +@safe @nogc nothrow: + enum StoryLineKind : ubyte { empty = ' ', comment = '#', @@ -52,10 +58,10 @@ enum StoryOp : ubyte { LINE, DEBUG, LINEAR, + ASSERT, END, ECHO, ECHON, - ECHOC, LEAK, LEAKN, HERE, @@ -86,6 +92,8 @@ struct StoryValue { alias data this; + @safe @nogc nothrow: + static foreach (Type; StoryValueData.Types) { this(Type value) { data = value; @@ -97,7 +105,7 @@ struct StoryValue { auto result = buffer[]; if (data.isType!StoryNumber) { - result.copy(data.get!StoryNumber().toStr()); + result.copyStr(data.get!StoryNumber().toStr()); } else { auto temp = data.get!(StoryWord)()[]; foreach (i, c; temp) { @@ -106,7 +114,7 @@ struct StoryValue { break; } } - result.copy(temp); + result.copyStr(temp); } return result; } @@ -136,25 +144,23 @@ struct Story { bool debugMode; bool linearMode; - IStr opIndex(Sz i) { - if (i >= lineCount) { - assert(0, "Index `{}` does not exist.".format(i)); - } - auto pair = pairs[i]; - return script[pair.a .. pair.b]; - } + @safe @nogc nothrow: - Fault throwOpFault(StoryOp op) { - faultOp = op; - return Fault.invalid; + IStr opIndex(Sz i) { + if (i >= lineCount) assert(0, "Index `{}` does not exist.".format(i)); + return script[pairs[i].a .. pairs[i].b]; } StoryNumber lineCount() { return cast(StoryNumber) pairs.length; } + bool hasEnd() { + return lineIndex == lineCount; + } + bool hasPause() { - if (lineIndex == lineCount) return true; + if (hasEnd) return true; if (lineIndex >= lineCount) return false; auto line = opIndex(lineIndex); return line.length && line[0] == StoryLineKind.pause; @@ -172,16 +178,12 @@ struct Story { return line.length && line[0] == StoryLineKind.text; } - IStr text() { - if (hasText) return opIndex(lineIndex)[1 .. $].trimStart(); - else return ""; - } - IStr[] menu() { - static FixedList!(IStr, 16) buffer; + static FixedList!(IStr, 32) buffer; buffer.clear(); - auto view = hasMenu ? opIndex(lineIndex)[1 .. $].trimStart() : ""; + if (!hasMenu) return []; + auto view = opIndex(lineIndex)[1 .. $].trimStart(); while (view.length) { if (buffer.length == buffer.capacity) return buffer[]; buffer.append(view.skipValue(StoryLineKind.menu).trim()); @@ -189,49 +191,85 @@ struct Story { return buffer[]; } - Fault prepare() { - lineIndex = 0; + IStr text() { + if (!hasText) return ""; + return opIndex(lineIndex)[1 .. $].trimStart(); + } + + Fault throwOpFault(StoryOp op) { + faultOp = op; + return Fault.invalid; + } + + StoryNumber findVariable(StoryWord name) { + foreach (i, variable; variables) { + if (name == variable.name) return cast(StoryNumber) i; + } + return -1; + } + + StoryNumber findLabel(StoryWord name) { + foreach (i, label; labels) { + if (name == label.name) return cast(StoryNumber) i; + } + return -1; + } + + void resetLineIndex() { + lineIndex = lineCount; + nextLabelIndex = 0; + } + + void clear() { previousMenuResult = 0; pairs.clear(); labels.clear(); + variables.clear(); + } + + Fault prepare() { + resetLineIndex(); + clear(); if (script.isEmpty) return Fault.none; - auto start = 0; - auto prepareIndex = 0; + auto startIndex = StoryNumber.init; + auto prepareIndex = StoryNumber.init; foreach (i, c; script) { if (c == '\n') { - auto pair = StoryStartEndPair(start, cast(uint) i); + auto pair = StoryStartEndPair(cast(uint) startIndex, cast(uint) i); auto line = script[pair.a .. pair.b]; auto trimmedLine = line.trim(); pair.a += line.length - line.trimStart().length; pair.b -= line.length - line.trimEnd().length; - auto lineResult = toStoryLineKind(trimmedLine.length ? script[pair.a] : StoryLineKind.empty); - if (lineResult.isNone) { - pairs.clear(); + auto kind = toStoryLineKind(trimmedLine.length ? script[pair.a] : StoryLineKind.empty); + if (kind.isNone) { + clear(); faultPrepareIndex = prepareIndex; - return Fault.cantParse; + return kind.fault; } - auto kind = lineResult.value; - if (kind == StoryLineKind.label) { - // TODO: Make words easier to use doooood. + if (kind.value == StoryLineKind.label) { auto name = trimmedLine[1 .. $].trimStart(); - auto temp = StoryWord.init; - auto tempRef = temp[]; - tempRef.copyChars(name); // TODO: Should maybe return a fault if it can't. - labels.append(StoryVariable(temp, StoryValue(cast(StoryNumber) pairs.length))); + auto word = StoryWord.init; + auto wordRef = word[]; + if (auto fault = wordRef.copyChars(name)) { + clear(); + faultPrepareIndex = prepareIndex; + return fault; + } + labels.append(StoryVariable(word, StoryValue(cast(StoryNumber) pairs.length))); } pairs.append(pair); - start = cast(int) (i + 1); prepareIndex += 1; + startIndex = cast(StoryNumber) (i + 1); } } - lineIndex = lineCount; + resetLineIndex(); return Fault.none; } - void parse(IStr text) { + Fault parse(IStr text) { script.clear(); script.append(text); - prepare(); + return prepare(); } Fault execute(IStr expression) { @@ -240,6 +278,7 @@ struct Story { while (true) with (StoryOp) { if (expression.length == 0) break; auto token = expression.skipValue(' '); + expression = expression.trimStart(); if (token.length == 0) continue; if (ifCounter > 0) { if (token == IF.toStr()) ifCounter += 1; @@ -247,9 +286,9 @@ struct Story { continue; } if (token.isMaybeStoryOp) { - auto tempResult = token.toStoryOp(); - if (tempResult.isNone) return Fault.cantParse; - auto op = tempResult.value; + auto tempOp = token.toStoryOp(); + if (tempOp.isNone) return tempOp.fault; + auto op = tempOp.value; final switch (op) { case ADD: case SUB: @@ -340,9 +379,8 @@ struct Story { if (!da.isType!StoryWord) return throwOpFault(op); StoryWord word; auto data = concat(concat(da.toStr()), db.toStr()); - if (data.length > word.length) return Fault.overflow; auto tempWordRef = word[]; - tempWordRef.copy(data); + if (auto fault = tempWordRef.copyChars(data)) return fault; stack.append(StoryValue(word)); break; case SAME: @@ -372,14 +410,20 @@ struct Story { case LINEAR: stack.append(StoryValue(linearMode)); break; + case ASSERT: + if (stack.length) { + auto da = stack.pop(); + if (da.isType!StoryWord || (da.isType!StoryNumber && !da.get!StoryNumber())) return Fault.some; + } else { + return Fault.some; + } + break; case END: return Fault.none; case ECHO: case ECHON: - case ECHOC: auto space = "\n"; if (op == ECHON) space = " "; - if (op == ECHOC) space = ","; if (stack.length) print(stack.pop(), space); else print(space); break; @@ -415,29 +459,19 @@ struct Story { auto da = stack.pop(); if (!da.isType!StoryWord) return throwOpFault(op); auto a = da.get!StoryWord(); - auto isNotThere = true; - foreach (variable; variables) { - if (a == variable.name) { - isNotThere = false; - break; - } - } - stack.append(StoryValue(!isNotThere)); + stack.append(StoryValue(findVariable(a) != -1)); break; case GET: if (stack.length < 1) return throwOpFault(op); auto da = stack.pop(); if (!da.isType!StoryWord) return throwOpFault(op); auto a = da.get!StoryWord(); - auto isNotThere = true; - foreach (variable; variables) { - if (a == variable.name) { - stack.append(variable.value); - isNotThere = false; - break; - } + auto aIndex = findVariable(a); + if (aIndex != -1) { + stack.append(variables[aIndex].value); + } else { + return throwOpFault(op); } - if (isNotThere) return throwOpFault(op); break; case GETN: if (stack.length < 2) return throwOpFault(op); @@ -446,24 +480,14 @@ struct Story { if (!da.isType!StoryWord || !db.isType!StoryWord) return throwOpFault(op); auto a = da.get!StoryWord(); auto b = db.get!StoryWord(); - auto isNotThere = true; - foreach (variable; variables) { - if (a == variable.name) { - stack.append(variable.value); - isNotThere = false; - break; - } + auto aIndex = findVariable(a); + auto bIndex = findVariable(b); + if (aIndex != -1 && bIndex != -1) { + stack.append(variables[aIndex].value); + stack.append(variables[bIndex].value); + } else { + return throwOpFault(op); } - if (isNotThere) return throwOpFault(op); - isNotThere = true; - foreach (variable; variables) { - if (b == variable.name) { - stack.append(variable.value); - isNotThere = false; - break; - } - } - if (isNotThere) return throwOpFault(op); break; case SET: if (stack.length < 2) return throwOpFault(op); @@ -471,42 +495,33 @@ struct Story { auto da = stack.pop(); if (!da.isType!StoryWord) return throwOpFault(op); auto a = da.get!StoryWord(); - auto isNotThere = true; - foreach (ref variable; variables) { - if (a == variable.name) { - variable.value = db; - isNotThere = false; - break; - } + auto aIndex = findVariable(a); + if (aIndex != -1) { + variables[aIndex].value = db; + } else { + variables.append(StoryVariable(a, db)); } - if (isNotThere) variables.append(StoryVariable(a, db)); break; case INIT: if (stack.length < 1) return throwOpFault(op); auto da = stack.pop(); if (!da.isType!StoryWord) return throwOpFault(op); auto a = da.get!StoryWord(); - auto isNotThere = true; - foreach (ref variable; variables) { - if (a == variable.name) { - variable.value = StoryValue(0); - isNotThere = false; - break; - } + auto aIndex = findVariable(a); + if (aIndex != -1) { + variables[aIndex].value = StoryValue(0); + } else { + variables.append(StoryVariable(a, StoryValue(0))); } - if (isNotThere) variables.append(StoryVariable(a, StoryValue(0))); break; case DROP: if (stack.length < 1) return throwOpFault(op); auto da = stack.pop(); if (!da.isType!StoryWord) return throwOpFault(op); auto a = da.get!StoryWord(); - auto isNotThere = true; - foreach (i, variable; variables) { - if (a == variable.name) { - variables.remove(i); - break; - } + auto aIndex = findVariable(a); + if (aIndex != -1) { + variables.remove(aIndex); } break; case DROPN: @@ -518,20 +533,17 @@ struct Story { auto da = stack.pop(); if (!da.isType!StoryWord) return throwOpFault(op); auto a = da.get!StoryWord(); - auto isNotThere = true; - foreach (ref variable; variables) { - if (a == variable.name) { - if (variable.value.isType!StoryNumber) { - variable.value.get!StoryNumber() += (op == INC ? 1 : -1); - stack.append(variable.value); - } else { - return throwOpFault(op); - } - isNotThere = false; - break; + auto aIndex = findVariable(a); + if (aIndex != -1) { + if (variables[aIndex].value.isType!StoryNumber) { + variables[aIndex].value.get!StoryNumber() += (op == INC ? 1 : -1); + stack.append(variables[aIndex].value); + } else { + return throwOpFault(op); } + } else { + return throwOpFault(op); } - if (isNotThere) return throwOpFault(op); break; case INCN: case DECN: @@ -541,40 +553,34 @@ struct Story { if (!da.isType!StoryWord || !db.isType!StoryNumber) return throwOpFault(op); auto a = da.get!StoryWord(); auto b = db.get!StoryNumber(); - auto isNotThere = true; - foreach (ref variable; variables) { - if (a == variable.name) { - if (variable.value.isType!StoryNumber) { - variable.value.get!StoryNumber() += b * (op == INCN ? 1 : -1); - stack.append(variable.value); - } else { - return throwOpFault(op); - } - isNotThere = false; - break; + auto aIndex = findVariable(a); + if (aIndex != -1) { + if (variables[aIndex].value.isType!StoryNumber) { + variables[aIndex].value.get!StoryNumber() += b * (op == INCN ? 1 : -1); + stack.append(variables[aIndex].value); + } else { + return throwOpFault(op); } + } else { + return throwOpFault(op); } - if (isNotThere) return throwOpFault(op); break; case TOG: if (stack.length < 1) return throwOpFault(op); auto da = stack.pop(); if (!da.isType!StoryWord) return throwOpFault(op); auto a = da.get!StoryWord(); - auto isNotThere = true; - foreach (ref variable; variables) { - if (a == variable.name) { - if (variable.value.isType!StoryNumber) { - variable.value.get!StoryNumber() = !variable.value.get!StoryNumber(); - stack.append(variable.value); - } else { - return throwOpFault(op); - } - isNotThere = false; - break; + auto aIndex = findVariable(a); + if (aIndex != -1) { + if (variables[aIndex].value.isType!StoryNumber) { + variables[aIndex].value.get!StoryNumber() = !variables[aIndex].value.get!StoryNumber(); + stack.append(variables[aIndex].value); + } else { + return throwOpFault(op); } + } else { + return throwOpFault(op); } - if (isNotThere) return throwOpFault(op); break; case MENU: stack.append(StoryValue(previousMenuResult)); @@ -583,8 +589,7 @@ struct Story { if (linearMode) break; auto target = nextLabelIndex - 1; if (target < 0 || target >= labels.length || labels.length == 0) { - lineIndex = lineCount; - nextLabelIndex = 0; + resetLineIndex(); } else { lineIndex = labels[target].value.get!StoryNumber(); nextLabelIndex = cast(StoryNumber) ((target + 1) % (labels.length + 1)); @@ -599,45 +604,39 @@ struct Story { if (linearMode) break; auto target = nextLabelIndex + (a > 0 ? a - 1 : a); if (target < 0 || target >= labels.length || labels.length == 0) { - lineIndex = lineCount; - nextLabelIndex = 0; + resetLineIndex(); } else { lineIndex = labels[target].value.get!StoryNumber(); nextLabelIndex = cast(StoryNumber) ((target + 1) % (labels.length + 1)); } break; case JUMP: - // TODO: Write a find function like a normal person. - // TODO: Might need some error check for -1. if (stack.length < 1) return throwOpFault(op); auto da = stack.pop(); if (!da.isType!StoryWord) return throwOpFault(op); auto a = da.get!StoryWord(); - auto isNotThere = true; - foreach (i, ref label; labels) { - if (a == label.name) { - isNotThere = false; - if (linearMode) break; - lineIndex = label.value.get!StoryNumber(); - nextLabelIndex = cast(StoryNumber) ((i + 1) % (labels.length + 1)); - break; - } + auto aIndex = findLabel(a); + if (aIndex != -1) { + if (linearMode) break; + lineIndex = labels[aIndex].value.get!StoryNumber(); + nextLabelIndex = cast(StoryNumber) ((aIndex + 1) % (labels.length + 1)); + } else { + return throwOpFault(op); } - if (isNotThere) return throwOpFault(op); break; case CALL: println("TODO: ", op); return Fault.none; } } else if (token.isMaybeStoryNumber) { - auto tempResult = token.toSigned(); - if (tempResult.isNone) return tempResult.fault; - stack.append(StoryValue(cast(StoryNumber) tempResult.value)); + auto number = token.toSigned(); + if (number.isNone) return number.fault; + stack.append(StoryValue(cast(StoryNumber) number.value)); } else if (token.isMaybeStoryWord) { - if (token.length > StoryWord.length) return Fault.overflow; - StoryWord temp; - foreach (i, c; token) temp[i] = c; - stack.append(StoryValue(temp)); + auto word = StoryWord.init; + auto wordRef = word[]; + if (auto fault = wordRef.copyChars(token)) return fault; + stack.append(StoryValue(word)); } else { return Fault.cantParse; } @@ -660,6 +659,7 @@ struct Story { } lineIndex = (lineIndex + 1) % (lineCount + 1); } + if (hasPause && lineIndex == lineCount) resetLineIndex(); return Fault.none; } @@ -706,7 +706,7 @@ Result!StoryLineKind toStoryLineKind(char value) { case '.': return Result!StoryLineKind(pause); case '^': return Result!StoryLineKind(menu); case '$': return Result!StoryLineKind(expression); - default: return Result!StoryLineKind(Fault.invalid); + default: return Result!StoryLineKind(Fault.cantParse); } } diff --git a/web/README.md b/web/README.md index 5a2712d..4c188b2 100644 --- a/web/README.md +++ b/web/README.md @@ -1,8 +1,7 @@ # Web A helper script to assist with the web export process. -Download the latest version of Emscripten before using this script. -It works both with and without DUB. +It supports both DUB and non-DUB projects and requires Emscripten. ## Usage