#include "script.hh" // NOTE(fusion): Throwing a character array on the stack as an exception won't // work because it implicitly decays into a character pointer which would then // point to invalid data when actually parsed. It is the reason `ErrorString` is // declared statically, or else you'd need to throw some heap allocated string // which then becomes ambiguous whether you should free it or not. Used in: // - TReadScriptFile::error // - TWriteScriptFile::error // - TReadBinaryFile::open // - TReadBinaryFile::error static char ErrorString[100]; // Helper Functions // ============================================================================= // 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::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){ 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; } // TWriteScriptFile //============================================================================== TWriteScriptFile::TWriteScriptFile(void){ this->File = NULL; } TWriteScriptFile::~TWriteScriptFile(void){ if(this->File != NULL){ ::error("TWriteScriptFile::~TWriteScriptFile: Datei %s ist noch offen.\n", this->Filename); if(fclose(this->File) != 0){ ::error("TWriteScriptFile::~TWriteScriptFile: Fehler %d beim Schließen der Datei.\n", errno); } } } void TWriteScriptFile::open(char *FileName){ if(this->File != NULL){ ::error("TWriteScriptFile::open: Altes Skript ist noch offen.\n"); if(fclose(this->File) != 0){ ::error("TWriteScriptFile::open: Fehler %d beim Schließen der Datei.\n", errno); } this->File = NULL; } this->File = fopen(FileName, "wb"); if(this->File == NULL){ ::error("TWriteScriptFile: Kann Datei %s nicht anlegen.\n", FileName); ::error("Fehler %d: %s.\n", errno, strerror(errno)); throw "Cannot create script-file"; } strcpy(this->Filename, FileName); this->Line = 0; } void TWriteScriptFile::close(void){ if(this->File == NULL){ ::error("TWriteScriptFile::close: Kein Skript offen.\n"); return; } if(fclose(this->File) != 0){ ::error("TWriteScriptFile::close: Fehler %d beim Schließen der Datei.\n", errno); } this->File = NULL; } void TWriteScriptFile::error(char *Text){ if(this->File != NULL){ if(fclose(this->File) != 0){ ::error("TWriteScriptFile::error: Fehler %d beim Schließen der Datei.\n", errno); } this->File = NULL; } snprintf(ErrorString, sizeof(ErrorString), "error in script-file \"%s\", line %d: %s", this->Filename, this->Line); throw ErrorString; } void TWriteScriptFile::writeLn(void){ if(this->File == NULL){ ::error("TWriteScriptFile::writeLn: Kein Skript zum Schreiben geöffnet.\n"); throw "Cannot write linefeed"; } putc('\n', this->File); } void TWriteScriptFile::writeText(char *Text){ if(this->File == NULL){ ::error("TWriteScriptFile::writeText: Kein Skript zum Schreiben geöffnet.\n"); throw "Cannot write text"; } if(Text == NULL){ ::error("TWriteScriptFile::writeText: Text ist NULL.\n"); throw "Cannot write text"; } for(int i = 0; Text[i] != 0; i += 1){ putc(Text[i], this->File); } } void TWriteScriptFile::writeNumber(int Number){ if(this->File == NULL){ ::error("TWriteScriptFile::writeNumber: Kein Skript zum Schreiben geöffnet.\n"); throw "Cannot write number"; } char s[32]; snprintf(s, sizeof(s), "%d", Number); this->writeText(s); } void TWriteScriptFile::writeString(char *Text){ if(this->File == NULL){ ::error(); throw "Cannot write string"; } if(Text == NULL){ ::error("TWriteScriptFile::writeString: Text ist NULL.\n"); throw "Cannot write string"; } putc('"', this->File); for(int i = 0; Text[i] != 0; i += 1){ if(Text[i] == '\"' || Text[i] == '\\'){ putc('\\', this->File); putc(Text[i], this->File); }else if(Text[i] == '\n'){ putc('\\', this->File); putc('n', this->File); }else{ putc(Text[i], this->File); } } putc('"', this->File); } void TWriteScriptFile::writeCoordinate(int x ,int y ,int z){ if(this->File == NULL){ ::error("TWriteScriptFile::writeCoordinate: Kein Skript zum Schreiben geöffnet.\n"); throw "Cannot write coordinate"; } // TODO(fusion): This is weird because we support loading negative coordinates values. if(x < 0 || y < 0 || z < 0){ ::error("TWriteScriptFile::writeCoordinate: Ungültige Koordinaten [%d,%d,%d].\n", x, y, z); throw "Invalid coordinates"; } char s[64]; snprintf(s, sizeof(s), "[%u,%u,%u]", x, y, z); this->writeText(s); } void TWriteScriptFile::writeBytesequence(uint8 *Sequence, int Length){ if(this->File == NULL){ ::error("TWriteScriptFile::writeBytesequence: Kein Skript zum Schreiben geöffnet.\n"); throw "Cannot write bytesequence"; } if(Sequence == NULL){ ::error("TWriteScriptFile::writeBytesequence: Sequence ist NULL.\n"); throw "Cannot write bytesequence"; } if(Length <= 0){ ::error("TWriteScriptFile::writeBytesequence: Ungültige Sequenzlänge.\n"); throw "Cannot write bytesequence"; } for(int i = 0; i < Length; i += 1){ if(i > 0){ putc('-', this->File); } snprintf(s, sizeof(s), "%u", Sequence[i]); this->writeText(s); } } // TReadBinaryFile REGULAR FUNCTIONS //============================================================================== TReadBinaryFile::TReadBinaryFile(void){ this->File = NULL; } void TReadBinaryFile::open(char *FileName){ if(this->File != NULL){ this->error("File still open"); } this->File = fopen(FileName, "rb"); if(this->File == NULL){ snprintf(ErrorString, sizeof(ErrorString), "Cannot open file %s", FileName); throw ErrorString; } strcpy(this->Filename, FileName); this->FileSize = -1; } int TReadBinaryFile::close(void){ // TODO(fusion): Check if file is NULL? if(fclose(this->File) != 0){ int ErrCode = errno; ::error("TReadBinaryFile::close: Fehler beim Schließen der Datei.\n"); ::error("# Datei: %s, Fehlercode: %d (%s)\n", this->Filename, ErrCode, strerror(ErrCode)); } this->File = NULL; } void TReadBinaryFile::error(char *Text){ if(this->File != NULL){ if(fclose(this->File) != 0){ ::error("TReadBinaryFile::error: Fehler %d beim Schließen der Datei.\n", errno); } this->File = NULL; } snprintf(ErrorString, sizeof(ErrorString), "error in binary-file \"%s\": %s.", this->Filename, Text); throw ErrorString; } int TReadBinaryFile::getPosition(void){ // TODO(fusion): Check if file is NULL? return (int)ftell(this->File); } int TReadBinaryFile::getSize(void){ // TODO(fusion): Check if file is NULL? int Size = this->FileSize; if(Size == -1){ long Position = ftell(this->File); fseek(this->File, 0, SEEK_END); Size = (int)ftell(this->File); fseek(this->File, Position, SEEK_SET); this->FileSize = Size; } return Size; } void TReadBinaryFile::seek(int Offset){ if(this->File == NULL){ this->error("File not open for seek"); } if(Offset < 0){ this->error("Negative offset for seek"); } fseek(this->File, (long)Offset, 0); } // TReadBinaryFile VIRTUAL FUNCTIONS //============================================================================== TReadBinaryFile::~TReadBinaryFile(void){ if(this->File != NULL){ ::error("TReadBinaryFile::~TReadBinaryFile: Datei %s ist noch offen.\n", this->Filename); if(fclose(this->File) != 0){ ::error("TReadBinaryFile::~TReadBinaryFile: Fehler %d beim Schließen der Datei.\n", errno); } } } uint8 TReadBinaryFile::readByte(void){ uint8 Byte; int Result = (int)fread(&Byte, 1, 1, this->File); if(Result != 1){ int ErrCode = errno; int Position = this->getPosition(); ::error("TReadBinaryFile::readByte: Fehler beim Lesen eines Bytes\n"); ::error("# Datei: %s, Position: %d, Rückgabewert: %d, Fehlercode: %d (%s)\n", this->Filename, Position, Result, ErrCode, strerror(ErrCode)); // NOTE(fusion): Close file and make a backup, possibly for further inspection. if(fclose(this->File) != 0){ ::error("TReadBinaryFile::readByte: Fehler %d beim Schließen der Datei.\n", errno); } SaveFile(this->Filename); this->error("Error while reading byte"); } return Byte; } void TReadBinaryFile::readBytes(uint8 *Buffer, int Count){ int Result = (int)fread(Buffer, 1, Count, this->File); if(Result != Count){ int ErrCode = errno; int Position = this->getPosition(); ::error("TReadBinaryFile::readBytes: Fehler beim Lesen von %d Bytes\n", Count); ::error("# Datei: %s, Position %d, Rückgabewert: %d, Fehlercode: %d (%s)\n", this->Filename, Position, Result, ErrCode, strerror(ErrCode)); // NOTE(fusion): Close file and make a backup, possibly for further inspection. if(fclose(this->File) != 0){ ::error("TReadBinaryFile::readBytes: Fehler %d beim Schließen der Datei.\n", errno); } SaveFile(this->Filename); this->error("Error while reading bytes"); } } bool TReadBinaryFile::eof(TReadBinaryFile *this){ if(this->File == NULL){ this->error("File not open for eof check"); } return this->getSize() <= this->getPosition(); } void TReadBinaryFile::skip(int Count){ if(this->File == NULL){ this->error("File not open for skip"); } this->seek(this->getPosition() + Count); }