aboutsummaryrefslogtreecommitdiff
path: root/src/script.cc
diff options
context:
space:
mode:
authorfusion32 <marcopuzziello@gmail.com>2025-05-22 01:02:13 -0300
committerfusion32 <marcopuzziello@gmail.com>2025-05-22 09:59:14 -0300
commitde848a4131beb288335b6aaa29c2a6a439c50e0e (patch)
tree64aceaf19d47fc3ff3b26663cb36fd1de0fcdccf /src/script.cc
parent91d7adad320c34ff0cb25aa21b16d59b67387c43 (diff)
downloadgame-de848a4131beb288335b6aaa29c2a6a439c50e0e.tar.gz
game-de848a4131beb288335b6aaa29c2a6a439c50e0e.zip
implement script lexer
Diffstat (limited to 'src/script.cc')
-rw-r--r--src/script.cc617
1 files changed, 617 insertions, 0 deletions
diff --git a/src/script.cc b/src/script.cc
new file mode 100644
index 0000000..d3e8134
--- /dev/null
+++ b/src/script.cc
@@ -0,0 +1,617 @@
+#include "script.hh"
+
+// =============================================================================
+// =============================================================================
+// TODO(fusion): Move to `util.cc`?
+
+static bool isSpace(int c){
+ return c == ' '
+ || c == '\t'
+ || c == '\n'
+ || c == '\r'
+ || c == '\v'
+ || c == '\f';
+}
+
+static bool isAlpha(int c){
+ // TODO(fusion): This is most likely wrong! We're assuming a direct conversion
+ // from `char` to `int` which will cause sign extension for negative values. This
+ // wouldn't be a problem if we expected to parse only streams of `char[]` but can
+ // be problematic for the output of `getc` which returns bytes as `unsigned char`
+ // converted to `int`.
+ // TLDR: The parameter `c` should be `uint8`.
+ return ('A' <= c && c <= 'Z')
+ || ('a' <= c && c <= 'z')
+ || c == -0x1C // E4 => ä
+ || c == -0x0A // F6 => ö
+ || c == -0x04 // FC => ü
+ || c == -0x3C // C4 => Ä
+ || c == -0x2A // D6 => Ö
+ || c == -0x24 // DC => Ü
+ || c == -0x21; // DF => ß
+}
+
+static bool isEngAlpha(int c){
+ return ('A' <= c && c <= 'Z')
+ || ('a' <= c && c <= 'z');
+}
+
+static bool isDigit(int c){
+ return ('0' <= c && c <= '9');
+}
+
+static int toLower(int c){
+ // TODO(fusion): Same problem as `isAlpha`.
+ if(('A' <= c && c <= 'Z') || (0xC0 <= c && c <= 0xDE && c != 0xD7)){
+ c += 32;
+ }
+ return c;
+}
+
+static int toUpper(int c){
+ // TODO(fusion): Same problem as `isAlpha`.
+ if(('A' <= c && c <= 'Z') || (0xE0 <= c && c <= 0xFE && c != 0xF7)){
+ c -= 32;
+ }
+ return c;
+}
+
+static char *strLower(char *s){
+ for(int i = 0; s[i] != 0; i += 1){
+ s[i] = (char)toLower(s[i]);
+ }
+ return s;
+}
+
+static char *strUpper(char *s){
+ for(int i = 0; s[i] != 0; i += 1){
+ s[i] = (char)toUpper(s[i]);
+ }
+ return s;
+}
+
+static int stricmp(const char *s1, const char *s2, int Pos){
+ for(int i = 0; i < Pos; i += 1){
+ int c1 = toLower(s1[i]);
+ int c2 = toLower(s2[i]);
+ if(c1 > c2){
+ return 1;
+ }else if(c1 < c2){
+ return -1;
+ }else{
+ ASSERT(c1 == c2);
+ if(c1 == 0){
+ return 0;
+ }
+ }
+ }
+}
+
+static char *findFirst(char *s, char c){
+ return strchr(s, (int)c);
+}
+
+static char *findLast(char *s, char c){
+ char *Current = s;
+ char *Last = NULL;
+ while(true){
+ Current = strchr(Current, (int)c);
+ if(Current == NULL)
+ break;
+ Last = Current;
+ Current += 1; // skip character
+ }
+ return Last;
+}
+
+// =============================================================================
+// =============================================================================
+
+TReadScriptFile::TReadScriptFile(void){
+ this->RecursionDepth = -1;
+ this->Bytes = (uint8*)this->String;
+}
+
+TReadScriptFile::~TReadScriptFile(void){
+ if(this->RecursionDepth != -1){
+ ::error("TReadScriptFile::~TReadScriptFile: Datei ist noch offen.\n");
+ for(int Depth = this->RecursionDepth; Depth >= 0; Depth -= 1){
+ // TODO(fusion): Probably `TReadScriptFile::close` inlined?
+ if(fclose(this->File[Depth]) != 0){
+ ::error("TReadScriptFile::close: Fehler %d beim Schließen der Datei.\n", errno);
+ }
+ }
+ this->RecursionDepth = -1;
+ }
+}
+
+void TReadScriptFile::open(const char *FileName){
+ int Depth = this->RecursionDepth + 1;
+ if((Depth + 1) >= NARRAY(this->File)){
+ ::error("TReadScriptFile::open: Rekursionstiefe zu groß.\n");
+ throw "Recursion depth too high";
+ }
+
+ ASSERT(Depth >= 0);
+
+ // TODO(fusion): More `strcpy`s...
+ if(Depth > 0 && FileName[0] != '/'){
+ strcpy(this->Filename[Depth], this->Filename[Depth - 1]);
+ if(char *Slash = findLast(this->Filename[Depth], '/')){
+ strcpy(Slash + 1, FileName);
+ }else{
+ strcpy(this->Filename[Depth], FileName);
+ }
+ }else{
+ strcpy(this->Filename[Depth], FileName);
+ }
+
+ this->File[Depth] = fopen(this->Filename[Depth], "rb");
+ if(this->File[Depth] == NULL){
+ ::error("TReadScriptFile::open: Kann Datei %s nicht öffnen.\n", this->Filename[Depth]);
+ ::error("Fehler %d: %s.\n", errno, strerror(errno));
+ throw "Cannot open script-file";
+ }
+
+ this->Line[Depth] = 1;
+ this->RecursionDepth = Depth;
+}
+
+void TReadScriptFile::close(void){
+ int Depth = this->RecursionDepth;
+ if(Depth <= -1){
+ ::error("TReadScriptFile::close: Keine Datei offen.\n");
+ return;
+ }
+
+ ASSERT(Depth < NARRAY(this->File));
+ if(fclose(this->File[Depth]) != 0){
+ ::error("TReadScriptFile::close: Fehler %d beim Schließen der Datei.\n", errno);
+ }
+ this->RecursionDepth -= 1;
+}
+
+void TReadScriptFile::error(const char *Text){
+ static char ErrorString[100];
+
+ int Depth = this->RecursionDepth;
+ ASSERT(Depth >= 0 && Depth <= NARRAY(this->File));
+
+ const char *Filename = this->Filename[Depth];
+ if(const char *Slash = findLast(this->Filename[Depth], '/')){
+ Filename = Slash + 1;
+ }
+
+ snprintf(ErrorString, sizeof(ErrorString),
+ "error in script-file \"%s\", line %d: %s",
+ Filename, this->Line[Depth], Text);
+
+ // TODO(fusion): Reset? Also seems like `TReadScriptFile::close` was inlined.
+ for(; Depth >= 0; Depth -= 1){
+ if(fclose(this->File[Depth]) != 0){
+ ::error("TReadScriptFile::close: Fehler %d beim Schließen der Datei.\n", errno);
+ }
+ }
+ this->RecursionDepth = -1;
+
+ throw ErrorString;
+}
+
+// NOTE(fusion): This function in particular was in pretty bad shape. It should
+// be one of the few that doesn't roughly match the original version. Nevertheless,
+// I think we should properly split this into a lexer and parser. We find ourselves
+// parsing strings and numbers multiple times here, whereas a simple lexer and
+// parser would simplify the work tremendously.
+// TODO(fusion): This needs to be tested to make sure it behaves like the original.
+void TReadScriptFile::nextToken(void){
+ if(this->RecursionDepth == -1){
+ ::error("TReadScriptFile::nextToken: Kein Skript zum Lesen geöffnet.\n");
+ this->Token = ENDOFFILE;
+ return;
+ }
+
+ // NOTE(fusion): Reset any previous token state.
+ memset(this->String, 0, sizeof(this->String));
+ this->Number = 0;
+ this->CoordX = 0;
+ this->CoordY = 0;
+ this->CoordZ = 0;
+ this->Special = 0;
+
+ // NOTE(fusion): `TReadScriptFile::error` will format and throw an error
+ // string that must be handled up the call stack. It must be considered
+ // an exit point for parsing errors in this function.
+
+ while(true){
+ int Depth = this->RecursionDepth;
+ ASSERT(Depth >= 0 && Depth < NARRAY(this->File));
+ FILE *File = this->File[Depth];
+
+ int c;
+ do{
+ c = getc(File);
+ if(c == '\n'){
+ this->Line[Depth] += 1;
+ }
+ }while(isSpace(c));
+
+ if(c == EOF){
+ if(Depth == 0){
+ this->Token = ENDOFFILE;
+ return;
+ }
+
+ this->close();
+ continue;
+ }
+
+ switch(c){
+ case '#':{ // COMMENT
+ while(true){
+ int next = getc(File);
+ if(next == '\n' || next == EOF){
+ if(next == '\n'){
+ this->Line[Depth] += 1;
+ }
+ break;
+ }
+ }
+ break;
+ }
+
+ case '@':{ // INCLUDE
+ int next = getc(File);
+ if(next == EOF){
+ this->error("unexpected end of file");
+ }else if(next != '"'){
+ this->error("syntax error");
+ }
+
+ int StringLength = 0;
+ while(true){
+ // TODO(fusion): I don't think new lines are even allowed in
+ // file names but we should keep track of the line number even
+ // if they happen here.
+ next = getc(File);
+ if(next == EOF){
+ this->error("unexpected end of file");
+ }else if(next == '"'){
+ break;
+ }
+
+ if(StringLength >= (NARRAY(this->String) - 1)){
+ this->error("string too long");
+ }
+ this->String[StringLength] = (char)next;
+ StringLength += 1;
+ }
+
+ // TODO(fusion): Maybe check if the path is empty?
+ this->open(this->String);
+
+ // NOTE(fusion): This is the only place we parse a string without
+ // returning it as a token. We need to reset it to make sure any
+ // subsequent string-like token will be properly parsed.
+ memset(this->String, 0, sizeof(this->String));
+
+ break;
+ }
+
+ case '"':{ // STRING
+ int StringLength = 0;
+ while(true){
+ int next = getc(File);
+ if(next == EOF){
+ this->error("unexpected end of file");
+ }else if(next == '\\'){
+ next = getc(File);
+ if(next == EOF){
+ this->error("unexpected end of file");
+ }else if(next == 'n'){
+ next = '\n';
+ }
+ }else if(next == '\n'){
+ // TODO(fusion): Shouldn't we prevent non-escaped new
+ // lines inside strings?
+ this->Line[Depth] += 1;
+ }else if(next == '"'){
+ break;
+ }
+
+ if(StringLength >= (NARRAY(this->String) - 1)){
+ this->error("string too long");
+ }
+ this->String[StringLength] = (char)next;
+ StringLength += 1;
+ }
+ this->Token = STRING;
+ return;
+ }
+
+ case '[':{ // COORDINATE
+ // NOTE(fusion): X-Coordinate or SPECIAL '['.
+ int Sign = -1;
+ int Coord = 0;
+ int next = getc(File);
+ if(isDigit(next)){
+ Sign = 1;
+ Coord = next - '0';
+ }else if(next != '-'){
+ this->Token = SPECIAL;
+ this->Special = '[';
+ if(next != EOF){
+ ungetc(next, File);
+ }
+ return;
+ }
+
+ while(true){
+ next = getc(File);
+ if(next == EOF){
+ this->error("unexpected end of file");
+ }else if(isDigit(next)){
+ Coord *= 10;
+ Coord += next - '0';
+ }else if(next == ','){
+ break;
+ }else{
+ this->error("syntax error");
+ }
+ }
+
+ this->CoordX = Sign * Coord;
+
+ // NOTE(fusion): Y-Coordinate.
+ Sign = -1;
+ Coord = 0;
+ next = getc(File);
+ if(isDigit(next)){
+ Sign = 1;
+ Coord = next - '0';
+ }else if(next != '-'){
+ this->error("syntax error");
+ }
+
+ while(true){
+ next = getc(File);
+ if(next == EOF){
+ this->error("unexpected end of file");
+ }else if(isDigit(next)){
+ Coord *= 10;
+ Coord += next - '0';
+ }else if(next == ','){
+ break;
+ }else{
+ this->error("syntax error");
+ }
+ }
+
+ this->CoordY = Sign * Coord;
+
+ // NOTE(fusion): Z-Coordinate.
+ Sign = -1;
+ Coord = 0;
+ next = getc(File);
+ if(isDigit(next)){
+ Sign = 1;
+ Coord = next - '0';
+ }else if(next != '-'){
+ this->error("syntax error");
+ }
+
+ while(true){
+ next = getc(File);
+ if(next == EOF){
+ this->error("unexpected end of file");
+ }else if(isDigit(next)){
+ Coord *= 10;
+ Coord += next - '0';
+ }else if(next == ']'){
+ break;
+ }else{
+ this->error("syntax error");
+ }
+ }
+
+ this->CoordZ = Sign * Coord;
+ this->Token = COORDINATE;
+ return;
+ }
+
+ case '<':{
+ int next = getc(File);
+ if(next == '='){
+ this->Special = 'L';
+ }else if(next == '>'){
+ this->Special = 'N';
+ }else{
+ this->Special = '<';
+ if(next != EOF){
+ ungetc(next, File);
+ }
+ }
+ this->Token = SPECIAL;
+ return;
+ }
+
+ case '>':{
+ int next = getc(File);
+ if(next == '='){
+ this->Special = 'G';
+ }else{
+ this->Special = '>';
+ if(next != EOF){
+ ungetc(next, File);
+ }
+ }
+ this->Token = SPECIAL;
+ return;
+ }
+
+ case '-':{
+ int next = getc(File);
+ if(next == '>'){
+ this->Special = 'I';
+ }else{
+ this->Special = '-';
+ if(next != EOF){
+ ungetc(next, File);
+ }
+ }
+ this->Token = SPECIAL;
+ return;
+ }
+
+ default:{
+ if(isAlpha(c)){
+ int IdentLength = 1;
+ this->String[0] = (char)c;
+ while(true){
+ int next = getc(File);
+ if(isAlpha(next) || isDigit(next) || next == '_'){
+ if(IdentLength >= 30){
+ this->error("identifier too long");
+ }
+ this->String[IdentLength] = (char)next;
+ IdentLength += 1;
+ }else{
+ if(next != EOF){
+ ungetc(next, File);
+ }
+ break;
+ }
+ }
+ this->Token = IDENTIFIER;
+ }else if(isDigit(c)){
+ // NOTE(fusion): `this->Bytes` points to `this->String` (set
+ // in the constructor) which is why we check against the size
+ // of the `this->String` array. It doesn't seem to store the
+ // number of bytes but I've only seen it used with fixed size
+ // arrays like outfit colors or sector offsets.
+ // Also, it doesn't make sense to have a null terminator in
+ // a byte string so the capacity check doesn't include it as
+ // in the case of regular strings.
+
+ // TODO(fusion): We're not checking for integer overflow at all.
+ int BytesLength = 0;
+ int Number = c - '0';
+ while(true){
+ int next = getc(File);
+ if(isDigit(next)){
+ Number *= 10;
+ Number += next - '0';
+ }else if(next == '-'){
+ if(BytesLength >= NARRAY(this->String)){
+ this->error("too many bytes");
+ }
+ this->Bytes[BytesLength] = (uint8)Number;
+ BytesLength += 1;
+
+ // NOTE(fusion): If there is a '-' after a number, there
+ // better be a second number.
+ next = getc(File);
+ if(next == EOF){
+ this->error("unexpected end of file");
+ }else if(!isDigit(next)){
+ this->error("syntax error");
+ }
+ Number = next - '0';
+ }else{
+ if(next != EOF){
+ ungetc(next, File);
+ }
+
+ if(BytesLength <= 0){
+ this->Token = NUMBER;
+ this->Number = Number;
+ }else{
+ if(BytesLength >= NARRAY(this->String)){
+ this->error("too many bytes");
+ }
+ this->Token = BYTES;
+ this->Bytes[BytesLength] = (uint8)Number;
+ BytesLength += 1;
+ }
+ break;
+ }
+ }
+ }else{
+ this->Token = SPECIAL;
+ this->Special = (char)c;
+ }
+ return;
+ }
+ }
+ }
+}
+
+char *TReadScriptFile::getIdentifier(void){
+ if(this->Token != IDENTIFIER){
+ this->error("identifier expected");
+ }
+ strLower(this->String);
+ return this->String;
+}
+
+int TReadScriptFile::getNumber(void){
+ if(this->Token != NUMBER){
+ this->error("number expected");
+ }
+ return this->Number;
+}
+
+char *TReadScriptFile::getString(void){
+ if(this->Token != STRING){
+ this->error("string expected");
+ }
+ return this->String;
+}
+
+uint8 *TReadScriptFile::getBytesequence(void){
+ if(this->Token != BYTES){
+ this->error("byte-sequence expected");
+ }
+ return this->Bytes;
+}
+
+void TReadScriptFile::getCoordinate(int *x, int *y, int *z){
+ if(this->Token != COORDINATE){
+ this->error("coordinates expected");
+ }
+ *x = this->CoordX;
+ *y = this->CoordY;
+ *z = this->CoordZ;
+}
+
+char TReadScriptFile::getSpecial(void){
+ if(this->Token != SPECIAL){
+ this->error("special-char expected");
+ }
+ return this->Special;
+}
+
+// TODO(fusion): REMOVE. This is a snippet to test the script lexer.
+int main(int argc, char **argv){
+ TReadScriptFile Script;
+ Script.open("local/alexander.npc");
+ while(true){
+ Script.nextToken();
+ if(Script.Token == ENDOFFILE){
+ printf("EOF\n");
+ Script.close();
+ break;
+ }
+
+ switch(Script.Token){
+ case IDENTIFIER: printf("IDENTIFIER \"%s\"\n", Script.String); break;
+ case NUMBER: printf("NUMBER %d\n", Script.Number); break;
+ case STRING: printf("STRING \"%s\"\n", Script.String); break;
+ case BYTES: printf("BYTES %u-%u-%u-%u-%u\n", Script.Bytes[0], Script.Bytes[1], Script.Bytes[2], Script.Bytes[3], Script.Bytes[4]); break;
+ case COORDINATE: printf("COORD [%d,%d,%d]\n", Script.CoordX, Script.CoordY, Script.CoordZ); break;
+ case SPECIAL: printf("SPECIAL '%c'\n", Script.Special); break;
+ default: printf("UNKNOWN TOKEN %d\n", Script.Token); break;
+ }
+ }
+
+ return EXIT_FAILURE;
+}