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 ## Features
* Immediate Mode UI: Build UIs quickly. * 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. * Sprite Animation Support: Manage animations for characters and objects.
* Tile Map Support: Create and draw tile-based maps. * 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 ## 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. While DUB simplifies the process, Parin itself doesn't require DUB.
Parin has the following dependencies: 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. /// This example shows how to use the Parin dialogue system.
import parin; import parin;
import parin.story;
auto story = Story(); auto story = Story();
auto script = " auto script = "
# A comment. # A comment.
* label * label
| Hello world! | Hello world!
| This is a text line. | This is a text line and the next is a menu line.
| The next line will be a menu line. ^ Option 1 ^ Option 2 ^ Option 3
^ Option number 1 ^ Option number 2 ^ Go to end
$ MENU SKIP $ MENU SKIP
* *
@ -22,11 +20,9 @@ auto script = "
$ end JUMP $ end JUMP
* end * end
| Expression lines are used to go to labels.
| JUMP goes to the label with the given name. | JUMP goes to the label with the given name.
| SKIP skips ahead N labels. | SKIP skips ahead N labels.
| MENU returns the selected option. | MENU returns the selected option.
| That's it. I Hope this helps.
"; ";
void ready() { void ready() {

View file

@ -1,9 +1,18 @@
# Rin # Rin
A Parin script interpreter. A script interpreter for Rin.
Works both with and without DUB. It helps with error checking and debugging, making it easier to work with Parin scripts.
## Usage ## Usage
* With DUB: `dub run parin:rin` * With DUB: `dub run parin:rin`
* Without DUB: `dmd -run -Ijoka_path -Iparin_path source/app.d` * 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" "Alexandros F. G. Kapretsos"
], ],
"copyright": "Copyright © 2024, Alexandros F. G. Kapretsos", "copyright": "Copyright © 2024, Alexandros F. G. Kapretsos",
"description": "A Parin script interpreter.", "description": "A script interpreter for Rin.",
"license": "MIT", "license": "MIT",
"name": "rin", "name": "rin",
"dependencies": { "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 joka;
import parin.story; import parin.story;
IStr path; RinState rinState;
Story story;
struct RinState {
IStr scriptPath;
Story story;
}
void printError(Sz index, IStr text) { void printError(Sz index, IStr text) {
printfln("\n{}({}): {}", path, index, text); printfln("\n{}({}): {}", rinState.scriptPath, index, text);
} }
Fault prepareStory() { Fault prepareStory() {
if (auto fault = story.prepare()) { if (auto fault = rinState.story.prepare()) {
auto index = story.faultPrepareIndex + 1; auto index = rinState.story.faultPrepareIndex + 1;
printError(index, "Invalid character at the beginning of the line."); 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;
} }
return Fault.none; return Fault.none;
} }
Fault updateStory() { Fault updateStory() {
if (story.hasText) println(story.text); if (rinState.story.hasText) println(rinState.story.text);
if (auto fault = story.update()) { if (auto fault = rinState.story.update()) {
auto index = story.lineIndex + 1; auto index = rinState.story.lineIndex + 1;
switch (fault) with (Fault) { 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 overflow: printError(index, "A word or number is too long."); break;
case cantParse: printError(index, "A word, number, or operator contains invalid characters."); break; case cantParse: printError(index, "A word, number, or operator contains invalid characters."); break;
default: break; default: break;
@ -34,29 +43,33 @@ Fault updateStory() {
return Fault.none; return Fault.none;
} }
int main(string[] args) { int rinMain(string[] args) {
if (args.length == 1) { if (args.length == 1) {
println("Usage: rin [options] script"); println("Usage: rin [options] script");
println("Options: -debug -linear"); println("Options: -debug -linear");
return 0; return 0;
} }
foreach (arg; args[1 .. $ - 1]) { foreach (arg; args[1 .. $ - 1]) {
if (arg == "-debug") story.debugMode = true; if (arg == "-debug") rinState.story.debugMode = true;
if (arg == "-linear") story.linearMode = true; if (arg == "-linear") rinState.story.linearMode = true;
} }
path = args[$ - 1]; rinState.scriptPath = args[$ - 1];
if (auto fault = readTextIntoBuffer(path, story.script)) { if (auto fault = readTextIntoBuffer(rinState.scriptPath, rinState.story.script)) {
switch (fault) { switch (fault) {
case Fault.cantOpen: println("Can't find 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(path)); break; case Fault.cantRead: println("Can't read file `{}`.".format(rinState.scriptPath)); break;
default: break; default: break;
} }
return 1; return 1;
} }
if (prepareStory()) return 1; if (prepareStory()) return 1;
if (updateStory()) return 1; if (updateStory()) return 1;
while (story.lineIndex != story.lineCount) { while (rinState.story.lineIndex != rinState.story.lineCount) {
if (updateStory()) return 1; if (updateStory()) return 1;
} }
return 0; return 0;
} }
int main(string[] args) {
return rinMain(args);
}

View file

@ -1,7 +1,7 @@
# Setup # Setup
A helper script that automates the project 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 ## 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. // Copyright 2024 Alexandros F. G. Kapretsos
// TODO: concat and others might need a "intoBuffer" vesion. // SPDX-License-Identifier: MIT
// TODO: Look at CAT case and think about how to make it better with joka. // Email: alexandroskapretsos@gmail.com
// NOTE: Remember to update both joka and parin at the same time because there was a evil change. // Project: https://github.com/Kapendev/parin
// NOTE: I will start cleanning and then will add CALL. // 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. /// The `story` module provides a simple and versatile dialogue system.
module parin.story; module parin.story;
@ -14,6 +18,8 @@ import joka.ascii;
import joka.io; import joka.io;
import joka.unions; import joka.unions;
@safe @nogc nothrow:
enum StoryLineKind : ubyte { enum StoryLineKind : ubyte {
empty = ' ', empty = ' ',
comment = '#', comment = '#',
@ -52,10 +58,10 @@ enum StoryOp : ubyte {
LINE, LINE,
DEBUG, DEBUG,
LINEAR, LINEAR,
ASSERT,
END, END,
ECHO, ECHO,
ECHON, ECHON,
ECHOC,
LEAK, LEAK,
LEAKN, LEAKN,
HERE, HERE,
@ -86,6 +92,8 @@ struct StoryValue {
alias data this; alias data this;
@safe @nogc nothrow:
static foreach (Type; StoryValueData.Types) { static foreach (Type; StoryValueData.Types) {
this(Type value) { this(Type value) {
data = value; data = value;
@ -97,7 +105,7 @@ struct StoryValue {
auto result = buffer[]; auto result = buffer[];
if (data.isType!StoryNumber) { if (data.isType!StoryNumber) {
result.copy(data.get!StoryNumber().toStr()); result.copyStr(data.get!StoryNumber().toStr());
} else { } else {
auto temp = data.get!(StoryWord)()[]; auto temp = data.get!(StoryWord)()[];
foreach (i, c; temp) { foreach (i, c; temp) {
@ -106,7 +114,7 @@ struct StoryValue {
break; break;
} }
} }
result.copy(temp); result.copyStr(temp);
} }
return result; return result;
} }
@ -136,25 +144,23 @@ struct Story {
bool debugMode; bool debugMode;
bool linearMode; bool linearMode;
IStr opIndex(Sz i) { @safe @nogc nothrow:
if (i >= lineCount) {
assert(0, "Index `{}` does not exist.".format(i));
}
auto pair = pairs[i];
return script[pair.a .. pair.b];
}
Fault throwOpFault(StoryOp op) { IStr opIndex(Sz i) {
faultOp = op; if (i >= lineCount) assert(0, "Index `{}` does not exist.".format(i));
return Fault.invalid; return script[pairs[i].a .. pairs[i].b];
} }
StoryNumber lineCount() { StoryNumber lineCount() {
return cast(StoryNumber) pairs.length; return cast(StoryNumber) pairs.length;
} }
bool hasEnd() {
return lineIndex == lineCount;
}
bool hasPause() { bool hasPause() {
if (lineIndex == lineCount) return true; if (hasEnd) return true;
if (lineIndex >= lineCount) return false; if (lineIndex >= lineCount) return false;
auto line = opIndex(lineIndex); auto line = opIndex(lineIndex);
return line.length && line[0] == StoryLineKind.pause; return line.length && line[0] == StoryLineKind.pause;
@ -172,16 +178,12 @@ struct Story {
return line.length && line[0] == StoryLineKind.text; return line.length && line[0] == StoryLineKind.text;
} }
IStr text() {
if (hasText) return opIndex(lineIndex)[1 .. $].trimStart();
else return "";
}
IStr[] menu() { IStr[] menu() {
static FixedList!(IStr, 16) buffer; static FixedList!(IStr, 32) buffer;
buffer.clear(); buffer.clear();
auto view = hasMenu ? opIndex(lineIndex)[1 .. $].trimStart() : ""; if (!hasMenu) return [];
auto view = opIndex(lineIndex)[1 .. $].trimStart();
while (view.length) { while (view.length) {
if (buffer.length == buffer.capacity) return buffer[]; if (buffer.length == buffer.capacity) return buffer[];
buffer.append(view.skipValue(StoryLineKind.menu).trim()); buffer.append(view.skipValue(StoryLineKind.menu).trim());
@ -189,49 +191,85 @@ struct Story {
return buffer[]; return buffer[];
} }
Fault prepare() { IStr text() {
lineIndex = 0; 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; previousMenuResult = 0;
pairs.clear(); pairs.clear();
labels.clear(); labels.clear();
variables.clear();
}
Fault prepare() {
resetLineIndex();
clear();
if (script.isEmpty) return Fault.none; if (script.isEmpty) return Fault.none;
auto start = 0; auto startIndex = StoryNumber.init;
auto prepareIndex = 0; auto prepareIndex = StoryNumber.init;
foreach (i, c; script) { foreach (i, c; script) {
if (c == '\n') { 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 line = script[pair.a .. pair.b];
auto trimmedLine = line.trim(); auto trimmedLine = line.trim();
pair.a += line.length - line.trimStart().length; pair.a += line.length - line.trimStart().length;
pair.b -= line.length - line.trimEnd().length; pair.b -= line.length - line.trimEnd().length;
auto lineResult = toStoryLineKind(trimmedLine.length ? script[pair.a] : StoryLineKind.empty); auto kind = toStoryLineKind(trimmedLine.length ? script[pair.a] : StoryLineKind.empty);
if (lineResult.isNone) { if (kind.isNone) {
pairs.clear(); clear();
faultPrepareIndex = prepareIndex; faultPrepareIndex = prepareIndex;
return Fault.cantParse; return kind.fault;
} }
auto kind = lineResult.value; if (kind.value == StoryLineKind.label) {
if (kind == StoryLineKind.label) {
// TODO: Make words easier to use doooood.
auto name = trimmedLine[1 .. $].trimStart(); auto name = trimmedLine[1 .. $].trimStart();
auto temp = StoryWord.init; auto word = StoryWord.init;
auto tempRef = temp[]; auto wordRef = word[];
tempRef.copyChars(name); // TODO: Should maybe return a fault if it can't. if (auto fault = wordRef.copyChars(name)) {
labels.append(StoryVariable(temp, StoryValue(cast(StoryNumber) pairs.length))); clear();
faultPrepareIndex = prepareIndex;
return fault;
}
labels.append(StoryVariable(word, StoryValue(cast(StoryNumber) pairs.length)));
} }
pairs.append(pair); pairs.append(pair);
start = cast(int) (i + 1);
prepareIndex += 1; prepareIndex += 1;
startIndex = cast(StoryNumber) (i + 1);
} }
} }
lineIndex = lineCount; resetLineIndex();
return Fault.none; return Fault.none;
} }
void parse(IStr text) { Fault parse(IStr text) {
script.clear(); script.clear();
script.append(text); script.append(text);
prepare(); return prepare();
} }
Fault execute(IStr expression) { Fault execute(IStr expression) {
@ -240,6 +278,7 @@ struct Story {
while (true) with (StoryOp) { while (true) with (StoryOp) {
if (expression.length == 0) break; if (expression.length == 0) break;
auto token = expression.skipValue(' '); auto token = expression.skipValue(' ');
expression = expression.trimStart();
if (token.length == 0) continue; if (token.length == 0) continue;
if (ifCounter > 0) { if (ifCounter > 0) {
if (token == IF.toStr()) ifCounter += 1; if (token == IF.toStr()) ifCounter += 1;
@ -247,9 +286,9 @@ struct Story {
continue; continue;
} }
if (token.isMaybeStoryOp) { if (token.isMaybeStoryOp) {
auto tempResult = token.toStoryOp(); auto tempOp = token.toStoryOp();
if (tempResult.isNone) return Fault.cantParse; if (tempOp.isNone) return tempOp.fault;
auto op = tempResult.value; auto op = tempOp.value;
final switch (op) { final switch (op) {
case ADD: case ADD:
case SUB: case SUB:
@ -340,9 +379,8 @@ struct Story {
if (!da.isType!StoryWord) return throwOpFault(op); if (!da.isType!StoryWord) return throwOpFault(op);
StoryWord word; StoryWord word;
auto data = concat(concat(da.toStr()), db.toStr()); auto data = concat(concat(da.toStr()), db.toStr());
if (data.length > word.length) return Fault.overflow;
auto tempWordRef = word[]; auto tempWordRef = word[];
tempWordRef.copy(data); if (auto fault = tempWordRef.copyChars(data)) return fault;
stack.append(StoryValue(word)); stack.append(StoryValue(word));
break; break;
case SAME: case SAME:
@ -372,14 +410,20 @@ struct Story {
case LINEAR: case LINEAR:
stack.append(StoryValue(linearMode)); stack.append(StoryValue(linearMode));
break; 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: case END:
return Fault.none; return Fault.none;
case ECHO: case ECHO:
case ECHON: case ECHON:
case ECHOC:
auto space = "\n"; auto space = "\n";
if (op == ECHON) space = " "; if (op == ECHON) space = " ";
if (op == ECHOC) space = ",";
if (stack.length) print(stack.pop(), space); if (stack.length) print(stack.pop(), space);
else print(space); else print(space);
break; break;
@ -415,29 +459,19 @@ struct Story {
auto da = stack.pop(); auto da = stack.pop();
if (!da.isType!StoryWord) return throwOpFault(op); if (!da.isType!StoryWord) return throwOpFault(op);
auto a = da.get!StoryWord(); auto a = da.get!StoryWord();
auto isNotThere = true; stack.append(StoryValue(findVariable(a) != -1));
foreach (variable; variables) {
if (a == variable.name) {
isNotThere = false;
break;
}
}
stack.append(StoryValue(!isNotThere));
break; break;
case GET: case GET:
if (stack.length < 1) return throwOpFault(op); if (stack.length < 1) return throwOpFault(op);
auto da = stack.pop(); auto da = stack.pop();
if (!da.isType!StoryWord) return throwOpFault(op); if (!da.isType!StoryWord) return throwOpFault(op);
auto a = da.get!StoryWord(); auto a = da.get!StoryWord();
auto isNotThere = true; auto aIndex = findVariable(a);
foreach (variable; variables) { if (aIndex != -1) {
if (a == variable.name) { stack.append(variables[aIndex].value);
stack.append(variable.value); } else {
isNotThere = false; return throwOpFault(op);
break;
} }
}
if (isNotThere) return throwOpFault(op);
break; break;
case GETN: case GETN:
if (stack.length < 2) return throwOpFault(op); if (stack.length < 2) return throwOpFault(op);
@ -446,24 +480,14 @@ struct Story {
if (!da.isType!StoryWord || !db.isType!StoryWord) return throwOpFault(op); if (!da.isType!StoryWord || !db.isType!StoryWord) return throwOpFault(op);
auto a = da.get!StoryWord(); auto a = da.get!StoryWord();
auto b = db.get!StoryWord(); auto b = db.get!StoryWord();
auto isNotThere = true; auto aIndex = findVariable(a);
foreach (variable; variables) { auto bIndex = findVariable(b);
if (a == variable.name) { if (aIndex != -1 && bIndex != -1) {
stack.append(variable.value); stack.append(variables[aIndex].value);
isNotThere = false; stack.append(variables[bIndex].value);
break; } 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; break;
case SET: case SET:
if (stack.length < 2) return throwOpFault(op); if (stack.length < 2) return throwOpFault(op);
@ -471,42 +495,33 @@ struct Story {
auto da = stack.pop(); auto da = stack.pop();
if (!da.isType!StoryWord) return throwOpFault(op); if (!da.isType!StoryWord) return throwOpFault(op);
auto a = da.get!StoryWord(); auto a = da.get!StoryWord();
auto isNotThere = true; auto aIndex = findVariable(a);
foreach (ref variable; variables) { if (aIndex != -1) {
if (a == variable.name) { variables[aIndex].value = db;
variable.value = db; } else {
isNotThere = false; variables.append(StoryVariable(a, db));
break;
} }
}
if (isNotThere) variables.append(StoryVariable(a, db));
break; break;
case INIT: case INIT:
if (stack.length < 1) return throwOpFault(op); if (stack.length < 1) return throwOpFault(op);
auto da = stack.pop(); auto da = stack.pop();
if (!da.isType!StoryWord) return throwOpFault(op); if (!da.isType!StoryWord) return throwOpFault(op);
auto a = da.get!StoryWord(); auto a = da.get!StoryWord();
auto isNotThere = true; auto aIndex = findVariable(a);
foreach (ref variable; variables) { if (aIndex != -1) {
if (a == variable.name) { variables[aIndex].value = StoryValue(0);
variable.value = StoryValue(0); } else {
isNotThere = false; variables.append(StoryVariable(a, StoryValue(0)));
break;
} }
}
if (isNotThere) variables.append(StoryVariable(a, StoryValue(0)));
break; break;
case DROP: case DROP:
if (stack.length < 1) return throwOpFault(op); if (stack.length < 1) return throwOpFault(op);
auto da = stack.pop(); auto da = stack.pop();
if (!da.isType!StoryWord) return throwOpFault(op); if (!da.isType!StoryWord) return throwOpFault(op);
auto a = da.get!StoryWord(); auto a = da.get!StoryWord();
auto isNotThere = true; auto aIndex = findVariable(a);
foreach (i, variable; variables) { if (aIndex != -1) {
if (a == variable.name) { variables.remove(aIndex);
variables.remove(i);
break;
}
} }
break; break;
case DROPN: case DROPN:
@ -518,20 +533,17 @@ struct Story {
auto da = stack.pop(); auto da = stack.pop();
if (!da.isType!StoryWord) return throwOpFault(op); if (!da.isType!StoryWord) return throwOpFault(op);
auto a = da.get!StoryWord(); auto a = da.get!StoryWord();
auto isNotThere = true; auto aIndex = findVariable(a);
foreach (ref variable; variables) { if (aIndex != -1) {
if (a == variable.name) { if (variables[aIndex].value.isType!StoryNumber) {
if (variable.value.isType!StoryNumber) { variables[aIndex].value.get!StoryNumber() += (op == INC ? 1 : -1);
variable.value.get!StoryNumber() += (op == INC ? 1 : -1); stack.append(variables[aIndex].value);
stack.append(variable.value);
} else { } else {
return throwOpFault(op); return throwOpFault(op);
} }
isNotThere = false; } else {
break; return throwOpFault(op);
} }
}
if (isNotThere) return throwOpFault(op);
break; break;
case INCN: case INCN:
case DECN: case DECN:
@ -541,40 +553,34 @@ struct Story {
if (!da.isType!StoryWord || !db.isType!StoryNumber) return throwOpFault(op); if (!da.isType!StoryWord || !db.isType!StoryNumber) return throwOpFault(op);
auto a = da.get!StoryWord(); auto a = da.get!StoryWord();
auto b = db.get!StoryNumber(); auto b = db.get!StoryNumber();
auto isNotThere = true; auto aIndex = findVariable(a);
foreach (ref variable; variables) { if (aIndex != -1) {
if (a == variable.name) { if (variables[aIndex].value.isType!StoryNumber) {
if (variable.value.isType!StoryNumber) { variables[aIndex].value.get!StoryNumber() += b * (op == INCN ? 1 : -1);
variable.value.get!StoryNumber() += b * (op == INCN ? 1 : -1); stack.append(variables[aIndex].value);
stack.append(variable.value);
} else { } else {
return throwOpFault(op); return throwOpFault(op);
} }
isNotThere = false; } else {
break; return throwOpFault(op);
} }
}
if (isNotThere) return throwOpFault(op);
break; break;
case TOG: case TOG:
if (stack.length < 1) return throwOpFault(op); if (stack.length < 1) return throwOpFault(op);
auto da = stack.pop(); auto da = stack.pop();
if (!da.isType!StoryWord) return throwOpFault(op); if (!da.isType!StoryWord) return throwOpFault(op);
auto a = da.get!StoryWord(); auto a = da.get!StoryWord();
auto isNotThere = true; auto aIndex = findVariable(a);
foreach (ref variable; variables) { if (aIndex != -1) {
if (a == variable.name) { if (variables[aIndex].value.isType!StoryNumber) {
if (variable.value.isType!StoryNumber) { variables[aIndex].value.get!StoryNumber() = !variables[aIndex].value.get!StoryNumber();
variable.value.get!StoryNumber() = !variable.value.get!StoryNumber(); stack.append(variables[aIndex].value);
stack.append(variable.value);
} else { } else {
return throwOpFault(op); return throwOpFault(op);
} }
isNotThere = false; } else {
break; return throwOpFault(op);
} }
}
if (isNotThere) return throwOpFault(op);
break; break;
case MENU: case MENU:
stack.append(StoryValue(previousMenuResult)); stack.append(StoryValue(previousMenuResult));
@ -583,8 +589,7 @@ struct Story {
if (linearMode) break; if (linearMode) break;
auto target = nextLabelIndex - 1; auto target = nextLabelIndex - 1;
if (target < 0 || target >= labels.length || labels.length == 0) { if (target < 0 || target >= labels.length || labels.length == 0) {
lineIndex = lineCount; resetLineIndex();
nextLabelIndex = 0;
} else { } else {
lineIndex = labels[target].value.get!StoryNumber(); lineIndex = labels[target].value.get!StoryNumber();
nextLabelIndex = cast(StoryNumber) ((target + 1) % (labels.length + 1)); nextLabelIndex = cast(StoryNumber) ((target + 1) % (labels.length + 1));
@ -599,45 +604,39 @@ struct Story {
if (linearMode) break; if (linearMode) break;
auto target = nextLabelIndex + (a > 0 ? a - 1 : a); auto target = nextLabelIndex + (a > 0 ? a - 1 : a);
if (target < 0 || target >= labels.length || labels.length == 0) { if (target < 0 || target >= labels.length || labels.length == 0) {
lineIndex = lineCount; resetLineIndex();
nextLabelIndex = 0;
} else { } else {
lineIndex = labels[target].value.get!StoryNumber(); lineIndex = labels[target].value.get!StoryNumber();
nextLabelIndex = cast(StoryNumber) ((target + 1) % (labels.length + 1)); nextLabelIndex = cast(StoryNumber) ((target + 1) % (labels.length + 1));
} }
break; break;
case JUMP: 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); if (stack.length < 1) return throwOpFault(op);
auto da = stack.pop(); auto da = stack.pop();
if (!da.isType!StoryWord) return throwOpFault(op); if (!da.isType!StoryWord) return throwOpFault(op);
auto a = da.get!StoryWord(); auto a = da.get!StoryWord();
auto isNotThere = true; auto aIndex = findLabel(a);
foreach (i, ref label; labels) { if (aIndex != -1) {
if (a == label.name) {
isNotThere = false;
if (linearMode) break; if (linearMode) break;
lineIndex = label.value.get!StoryNumber(); lineIndex = labels[aIndex].value.get!StoryNumber();
nextLabelIndex = cast(StoryNumber) ((i + 1) % (labels.length + 1)); nextLabelIndex = cast(StoryNumber) ((aIndex + 1) % (labels.length + 1));
break; } else {
return throwOpFault(op);
} }
}
if (isNotThere) return throwOpFault(op);
break; break;
case CALL: case CALL:
println("TODO: ", op); println("TODO: ", op);
return Fault.none; return Fault.none;
} }
} else if (token.isMaybeStoryNumber) { } else if (token.isMaybeStoryNumber) {
auto tempResult = token.toSigned(); auto number = token.toSigned();
if (tempResult.isNone) return tempResult.fault; if (number.isNone) return number.fault;
stack.append(StoryValue(cast(StoryNumber) tempResult.value)); stack.append(StoryValue(cast(StoryNumber) number.value));
} else if (token.isMaybeStoryWord) { } else if (token.isMaybeStoryWord) {
if (token.length > StoryWord.length) return Fault.overflow; auto word = StoryWord.init;
StoryWord temp; auto wordRef = word[];
foreach (i, c; token) temp[i] = c; if (auto fault = wordRef.copyChars(token)) return fault;
stack.append(StoryValue(temp)); stack.append(StoryValue(word));
} else { } else {
return Fault.cantParse; return Fault.cantParse;
} }
@ -660,6 +659,7 @@ struct Story {
} }
lineIndex = (lineIndex + 1) % (lineCount + 1); lineIndex = (lineIndex + 1) % (lineCount + 1);
} }
if (hasPause && lineIndex == lineCount) resetLineIndex();
return Fault.none; return Fault.none;
} }
@ -706,7 +706,7 @@ Result!StoryLineKind toStoryLineKind(char value) {
case '.': return Result!StoryLineKind(pause); case '.': return Result!StoryLineKind(pause);
case '^': return Result!StoryLineKind(menu); case '^': return Result!StoryLineKind(menu);
case '$': return Result!StoryLineKind(expression); case '$': return Result!StoryLineKind(expression);
default: return Result!StoryLineKind(Fault.invalid); default: return Result!StoryLineKind(Fault.cantParse);
} }
} }

View file

@ -1,8 +1,7 @@
# Web # Web
A helper script to assist with the web export process. A helper script to assist with the web export process.
Download the latest version of Emscripten before using this script. It supports both DUB and non-DUB projects and requires Emscripten.
It works both with and without DUB.
## Usage ## Usage