Docs and story cleaning.

This commit is contained in:
Kapendev 2025-01-23 05:28:28 +02:00
parent 3c4ee5e20b
commit c5ca1b37cf
13 changed files with 244 additions and 621 deletions

View file

@ -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:

View file

@ -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

View file

@ -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() {

View file

@ -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.

14
rin/assets/rin.nanorc Normal file
View file

@ -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:]]+$"

View file

@ -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": {

12
rin/examples/hello.rin Normal file
View file

@ -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.

View file

@ -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);
}

View file

@ -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

View file

@ -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());
}

View file

@ -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);
}
}

View file

@ -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