From 3a026671ecc485a08467707ccba3c096362657fa Mon Sep 17 00:00:00 2001 From: ohlidalp Date: Sat, 30 Nov 2024 01:27:20 +0100 Subject: [PATCH] :sparkles: Terrains now load terrn2 [Races] by default. --- resources/scripts/default.as | 9 + resources/scripts/genericdoc_utils.as | 318 +++++++------ resources/scripts/races.as | 443 ++++++++++++------ resources/scripts/terrain_project_importer.as | 8 +- 4 files changed, 471 insertions(+), 307 deletions(-) diff --git a/resources/scripts/default.as b/resources/scripts/default.as index 2fe8734565..680b2473fd 100644 --- a/resources/scripts/default.as +++ b/resources/scripts/default.as @@ -1,6 +1,15 @@ #include "base.as" +#include "races.as" +racesManager races; + void main() { + int numRaces = races.loadRacesFromTerrn2(); + if (numRaces < 0) + game.log("Error loading races from terrn2 [Races]: " + numRaces); + else + game.log("Loaded " + numRaces + " race-defs"); + game.log("default terrain script loaded"); } \ No newline at end of file diff --git a/resources/scripts/genericdoc_utils.as b/resources/scripts/genericdoc_utils.as index 1837c434b1..f60189ea43 100644 --- a/resources/scripts/genericdoc_utils.as +++ b/resources/scripts/genericdoc_utils.as @@ -1,66 +1,78 @@ /// \title GenericDocument viewing/editing /// \brief IMGUI-based editor of tokenized GenericDocument -// =================================================== - -// Window [X] button handler -#include "imgui_utils.as" +// =================================================== + +// Window [X] button handler +#include "imgui_utils.as" + - // By convention, all includes have filename '*_utils' and namespace matching filename. namespace genericdoc_utils { - - - const int FLAGSET_TOBJ + const int FLAGSET_RACE + = GENERIC_DOCUMENT_OPTION_ALLOW_NAKED_STRINGS; + + const int FLAGSET_TOBJ = GENERIC_DOCUMENT_OPTION_ALLOW_NAKED_STRINGS | GENERIC_DOCUMENT_OPTION_ALLOW_SLASH_COMMENTS; - - const int FLAGSET_TERRN2 + + const int FLAGSET_TERRN2 = GENERIC_DOCUMENT_OPTION_ALLOW_NAKED_STRINGS | GENERIC_DOCUMENT_OPTION_ALLOW_SLASH_COMMENTS | GENERIC_DOCUMENT_OPTION_ALLOW_HASH_COMMENTS - | GENERIC_DOCUMENT_OPTION_ALLOW_SEPARATOR_EQUALS - | GENERIC_DOCUMENT_OPTION_ALLOW_BRACED_KEYWORDS; - - const int FLAGSET_TRUCK + | GENERIC_DOCUMENT_OPTION_ALLOW_SEPARATOR_EQUALS + | GENERIC_DOCUMENT_OPTION_ALLOW_BRACED_KEYWORDS; + + const int FLAGSET_TRUCK = GENERIC_DOCUMENT_OPTION_ALLOW_NAKED_STRINGS | GENERIC_DOCUMENT_OPTION_ALLOW_SLASH_COMMENTS | GENERIC_DOCUMENT_OPTION_FIRST_LINE_IS_TITLE // unique for RigDef format | GENERIC_DOCUMENT_OPTION_ALLOW_SEPARATOR_COLON | GENERIC_DOCUMENT_OPTION_PARENTHESES_CAPTURE_SPACES; - + + shared string tokenTypeStr(TokenType t) + { + switch (t) + { + case TOKEN_TYPE_FLOAT: return "Float"; + case TOKEN_TYPE_INT: return "Integer"; + case TOKEN_TYPE_STRING: return "String"; + case TOKEN_TYPE_BOOL: return "Boolean"; + case TOKEN_TYPE_COMMENT: return "Comment"; + case TOKEN_TYPE_LINEBREAK: return "Line break"; + case TOKEN_TYPE_KEYWORD: return "Keyword"; + } + return "?"; // `default:` does not do, compiler compilains + } + class GenericDocEditor { - - int hoveredTokenPos = -1; - int focusedTokenPos = -1; - string errorString; + int hoveredTokenPos = -1; + int focusedTokenPos = -1; + string errorString; // Document window state GenericDocumentClass@ displayedDocument = null; string displayedDocFilename; string windowTitleOverride = ""; // defaults to filename imgui_utils::CloseWindowPrompt gdeCloseBtnHandler; - + GenericDocEditor() { - - - - this.gdeCloseBtnHandler.cfgTerminateWholeScript = false; + this.gdeCloseBtnHandler.cfgTerminateWholeScript = false; this.gdeCloseBtnHandler.cfgPromptText = "Really close this document? You will lose any unsaved changes."; } - + void setDocument(GenericDocumentClass@ doc, string filename = "") { @displayedDocument = @doc; displayedDocFilename = filename; this.resetTokenEditPanel(); } - + void loadDocument(string filename, string rgname, int flags) { GenericDocumentClass@ doc = GenericDocumentClass(); - + if (doc.loadFromResource(filename, rgname, flags)) { @displayedDocument = @doc; @@ -68,45 +80,45 @@ namespace genericdoc_utils this.resetTokenEditPanel(); } } - - void resetTokenEditPanel() + + void resetTokenEditPanel() { hoveredTokenPos = -1; focusedTokenPos = -1; } - - void drawSeparateWindow() - { + + void drawSeparateWindow() + { string caption = windowTitleOverride; if (caption == "") { caption = displayedDocFilename + " ~ GenericDocViewer"; } int flags = 0;// ImGuiWindowFlags_MenuBar; - + if (ImGui::Begin(caption, this.gdeCloseBtnHandler.windowOpen, flags)) { gdeCloseBtnHandler.draw(); - - + + if (@displayedDocument != null) { vector2 size = ImGui::GetWindowSize() - vector2(20, 150); ImGui::BeginChild("docBody", size); drawDocumentBody(); ImGui::EndChild(); - - + + ImGui::Separator(); - + drawTokenEditPanel(); - + } - + ImGui::End(); - - } - + + } + if (gdeCloseBtnHandler.exitRequested) { @displayedDocument = null; @@ -115,52 +127,52 @@ namespace genericdoc_utils gdeCloseBtnHandler.reset(); // <-- somewhat clumsy, don't forget! } } - - void drawDocumentBody() - { - ImGui::PushID("docBody"); - bool hover_found = false; - - GenericDocContextClass reader(displayedDocument); - while (!reader.endOfFile()) - { - - - switch (reader.tokenType()) - { - // These tokens are always at start of line - case TOKEN_TYPE_KEYWORD: - ImGui::TextColored(tokenColor(reader), reader.getTokKeyword()); - break; - case TOKEN_TYPE_COMMENT: - ImGui::TextColored(tokenColor(reader), ";" + reader.getTokComment()); - break; - - // Linebreak is implicit in DearIMGUI, no action needed - case TOKEN_TYPE_LINEBREAK: - if (reader.getPos() != 0 && reader.tokenType(-1) != TOKEN_TYPE_LINEBREAK) - { - ImGui::SameLine(); - } - ImGui::TextColored(tokenColor(reader), "
"); - // ImGui::SameLine(); ImGui::Text(""); // hack to fix highlight of last token on line. - break; - - // Other tokens come anywhere - delimiting logic is needed - default: - if (reader.getPos() != 0 && reader.tokenType(-1) != TOKEN_TYPE_LINEBREAK) - { - ImGui::SameLine(); - // string delimiter = (reader.tokenType(-1) == TOKEN_TYPE_KEYWORD) ? " " : ", "; - // ImGui::Text(delimiter); - // ImGui::SameLine(); - } - - switch (reader.tokenType()) - { - case TOKEN_TYPE_STRING: - ImGui::TextColored(tokenColor(reader), "\"" + reader.getTokString() + "\""); - break; + + void drawDocumentBody() + { + ImGui::PushID("docBody"); + bool hover_found = false; + + GenericDocContextClass reader(displayedDocument); + while (!reader.endOfFile()) + { + + + switch (reader.tokenType()) + { + // These tokens are always at start of line + case TOKEN_TYPE_KEYWORD: + ImGui::TextColored(tokenColor(reader), reader.getTokKeyword()); + break; + case TOKEN_TYPE_COMMENT: + ImGui::TextColored(tokenColor(reader), ";" + reader.getTokComment()); + break; + + // Linebreak is implicit in DearIMGUI, no action needed + case TOKEN_TYPE_LINEBREAK: + if (reader.getPos() != 0 && reader.tokenType(-1) != TOKEN_TYPE_LINEBREAK) + { + ImGui::SameLine(); + } + ImGui::TextColored(tokenColor(reader), "
"); + // ImGui::SameLine(); ImGui::Text(""); // hack to fix highlight of last token on line. + break; + + // Other tokens come anywhere - delimiting logic is needed + default: + if (reader.getPos() != 0 && reader.tokenType(-1) != TOKEN_TYPE_LINEBREAK) + { + ImGui::SameLine(); + // string delimiter = (reader.tokenType(-1) == TOKEN_TYPE_KEYWORD) ? " " : ", "; + // ImGui::Text(delimiter); + // ImGui::SameLine(); + } + + switch (reader.tokenType()) + { + case TOKEN_TYPE_STRING: + ImGui::TextColored(tokenColor(reader), "\"" + reader.getTokString() + "\""); + break; case TOKEN_TYPE_FLOAT: { ImGui::Text("" + reader.getTokFloat()); @@ -171,60 +183,60 @@ namespace genericdoc_utils ImGui::Text("" + reader.getTokInt()); break; } - case TOKEN_TYPE_BOOL: - ImGui::TextColored(tokenColor(reader), ""+reader.getTokBool()); - break; - } - } - - if (ImGui::IsItemHovered()) { - hoveredTokenPos = reader.getPos() ; - hover_found = true; - } - - if (ImGui::IsItemClicked(0)) - { - focusedTokenPos = reader.getPos(); - } - - reader.moveNext(); - - } - - if (!hover_found) - { - hoveredTokenPos = -1; - } - - ImGui::PopID(); // "docBody" - } - - color tokenColor(GenericDocContextClass@ reader) - { - if (focusedTokenPos > -1 && reader.getPos() == uint(focusedTokenPos)) - { - return color(0.9f, 0.1f, 0.1f, 1.f); - } - - if (hoveredTokenPos > -1 && reader.getPos() == uint(hoveredTokenPos)) - { - return color(0.1f, 1.f, 0.1f, 1.f); - } - - switch (reader.tokenType()) - { - case TOKEN_TYPE_KEYWORD: return color(1.f, 1.f, 0.f, 1.f); - case TOKEN_TYPE_COMMENT: return color(0.5f, 0.5f, 0.5f, 1.f); - case TOKEN_TYPE_STRING: return color(0.f, 1.f, 1.f, 1.f); - case TOKEN_TYPE_FLOAT: return color(0.9, 0.8, 0.9, 1.f); + case TOKEN_TYPE_BOOL: + ImGui::TextColored(tokenColor(reader), ""+reader.getTokBool()); + break; + } + } + + if (ImGui::IsItemHovered()) { + hoveredTokenPos = reader.getPos() ; + hover_found = true; + } + + if (ImGui::IsItemClicked(0)) + { + focusedTokenPos = reader.getPos(); + } + + reader.moveNext(); + + } + + if (!hover_found) + { + hoveredTokenPos = -1; + } + + ImGui::PopID(); // "docBody" + } + + color tokenColor(GenericDocContextClass@ reader) + { + if (focusedTokenPos > -1 && reader.getPos() == uint(focusedTokenPos)) + { + return color(0.9f, 0.1f, 0.1f, 1.f); + } + + if (hoveredTokenPos > -1 && reader.getPos() == uint(hoveredTokenPos)) + { + return color(0.1f, 1.f, 0.1f, 1.f); + } + + switch (reader.tokenType()) + { + case TOKEN_TYPE_KEYWORD: return color(1.f, 1.f, 0.f, 1.f); + case TOKEN_TYPE_COMMENT: return color(0.5f, 0.5f, 0.5f, 1.f); + case TOKEN_TYPE_STRING: return color(0.f, 1.f, 1.f, 1.f); + case TOKEN_TYPE_FLOAT: return color(0.9, 0.8, 0.9, 1.f); case TOKEN_TYPE_INT: return color(0.9, 0.9, 0.8, 1.f); - case TOKEN_TYPE_BOOL: return color(1.f, 0.f, 1.f, 1.f); - case TOKEN_TYPE_LINEBREAK: return color(0.66f, 0.55f, 0.33f, 1.f); - } // end switch - return color(0.9, 0.9, 0.9, 1.f); - } - - void drawTokenEditPanel() + case TOKEN_TYPE_BOOL: return color(1.f, 0.f, 1.f, 1.f); + case TOKEN_TYPE_LINEBREAK: return color(0.66f, 0.55f, 0.33f, 1.f); + } // end switch + return color(0.9, 0.9, 0.9, 1.f); + } + + void drawTokenEditPanel() { if ( focusedTokenPos == -1) { @@ -243,27 +255,11 @@ namespace genericdoc_utils ImGui::TextDisabled("Token type: "); ImGui::SameLine(); ImGui::Text(tokenTypeStr(reader.tokenType())); } } - - string tokenTypeStr(TokenType t) - { - switch (t) - { - case TOKEN_TYPE_FLOAT: return "Float"; - case TOKEN_TYPE_INT: return "Integer"; - case TOKEN_TYPE_STRING: return "String"; - case TOKEN_TYPE_BOOL: return "Boolean"; - case TOKEN_TYPE_COMMENT: return "Comment"; - case TOKEN_TYPE_LINEBREAK: return "Line break"; - case TOKEN_TYPE_KEYWORD: return "Keyword"; - } - return "?"; - } - + } // class GenericDocEditor - - + } // namespace genericdoc_utils - + diff --git a/resources/scripts/races.as b/resources/scripts/races.as index c20c7accee..32fc5eb963 100644 --- a/resources/scripts/races.as +++ b/resources/scripts/races.as @@ -32,6 +32,8 @@ raceBuilder::raceBuilderVersion) -- neorej16 */ +#include "genericdoc_utils.as" + // Define a function signature for the callback pointers funcdef void RACE_EVENT_CALLBACK(dictionary@); @@ -77,48 +79,48 @@ shared class racesManager { int arrowMethod; LocalStorageClass@ raceDataFile; array penaltyTime; - + // public constants int LAPS_Unlimited; int LAPS_NoLaps; int LAPS_One; - + int ACTION_DoNothing; int ACTION_SuspendRace; int ACTION_StopRace; int ACTION_RestartRace; - + int STATE_NotInRace; int STATE_Waiting; int STATE_Racing; - + int ARROW_AUTO; - + // private properties array raceList; dictionary callbacks; - + // public functions - + // constructor racesManager() { - + // We initialize our "constants" this.LAPS_Unlimited = -1; this.LAPS_NoLaps = 0; this.LAPS_One = 1; - + this.ACTION_DoNothing = 0; this.ACTION_SuspendRace = 1; this.ACTION_StopRace = 2; this.ACTION_RestartRace = 3; - + this.STATE_NotInRace = 0; this.STATE_Waiting = 1; this.STATE_Racing = 2; - + this.ARROW_AUTO = -1; - + // we initialize the callbacks dictionary this.callbacks.set("RaceFinish", null); // when a race was finished this.callbacks.set("RaceCancel", null); // when a race was canceled @@ -144,7 +146,7 @@ shared class racesManager { this.showCheckPointInfoWhenNotInRace = false; // if true: if the user drives through a checkpoint of a race that isn't running, a message will be shown, saying "this is checkpoint xx of race myRaceName" this.arrowMethod = this.ARROW_AUTO; this.restartRaceOnStart = true; // if true: the race will be restarted when you pass the start line of the same race - + // we initialize the other variables (do not edit these manually) this.state = this.STATE_NotInRace; this.raceCount = 0; @@ -157,7 +159,7 @@ shared class racesManager { this.lastRaceEventInstance = ""; // we only use this to boost the FPS this.raceManagerVersion = "RoR_raceManager_v0.02"; this.penaltyGiven = true; - + // register the required callbacks game.registerForEvent(SE_TRUCK_ENTER); game.registerForEvent(SE_TRUCK_EXIT); @@ -166,11 +168,11 @@ shared class racesManager { game.registerForEvent(SE_TRUCK_MOUSE_GRAB); game.registerForEvent(SE_GENERIC_DELETED_TRUCK); game.registerForEvent(SE_ANGELSCRIPT_MANIPULATIONS); - + // add the eventcallback method if it doesn't exist // ^ but only if loaded from terrain script, not i.e. terrain_project_importer dictionary@ scriptDetails = game.getScriptDetails(thisScript); - if (@scriptDetails != null + if (@scriptDetails != null && ScriptCategory(scriptDetails['scriptCategory']) == SCRIPT_CATEGORY_TERRAIN) { if( game.scriptFunctionExists("void eventCallback(int, int)")<0) @@ -178,25 +180,183 @@ shared class racesManager { game.addScriptFunction("void eventCallback(int key, int value) { races.eventCallback(key, value); }"); } } - + // Load the file containing the race data @this.raceDataFile = LocalStorageClass("raceTimes"); } + // Processes the new 'race-def' files generated by project importer, see new terrn2 section [Races]. Returns number of checkpoints or negative number on error. + int addRaceFromDefinitionFile(string filename, string resourceGroup) + { + GenericDocumentClass doc; + if (!doc.loadFromResource(filename, resourceGroup, genericdoc_utils::FLAGSET_RACE)) + { + game.log("Error in racesManager::addRaceFromDefinitionFile: could not load race-def file."); + return -1; + } + + int raceID = this.addNewEmptyRace(); + + GenericDocContextClass@ ctx = GenericDocContextClass(doc); + bool inCheckpoints = false; + array checkpointTokPositions; // We must pre-count checkpoints to pick finish-obj correctly. + int highestCheckpointNum = 0; // Multiple finish lines are supported! + while (!ctx.endOfFile()) + { + //game.log("DBG addRaceFromDefinitionFile() token "+genericdoc_utils::tokenTypeStr(ctx.tokenType())+" at pos "+ctx.getPos()); + + if (ctx.isTokKeyword(0)) + { + if (ctx.isTokString(1) && ctx.getTokKeyword() == "race_name") + { + this.raceList[raceID].raceName = ctx.getTokString(1); + } + if (ctx.isTokInt(1) && ctx.getTokKeyword() == "race_laps") + { + this.raceList[raceID].setLaps(ctx.getTokInt(1)); + } + else if (ctx.isTokString(1) && ctx.getTokKeyword() == "race_checkpoint_object") + { + this.raceList[raceID].objNameCheckpoint = ctx.getTokString(1); + } + else if (ctx.isTokString(1) && ctx.getTokKeyword() == "race_start_object") + { + this.raceList[raceID].objNameStart = ctx.getTokString(1); + } + else if (ctx.isTokString(1) && ctx.getTokKeyword() == "race_finish_object") + { + this.raceList[raceID].objNameFinish = ctx.getTokString(1); + } + else if (ctx.getTokKeyword() == "begin_checkpoints") + { + inCheckpoints = true; + } + else if (ctx.getTokKeyword() == "end_checkpoints") + { + inCheckpoints = false; + } + } + else if (inCheckpoints) + { + //game.log("DBG inCheckpoints | isTok*: "+ctx.isTokInt(0) +" "+ctx.isTokInt(1) // chkpNum, altpathNum + // +" "+ ctx.isTokFloat(2) +" "+ ctx.isTokFloat(3) +" "+ ctx.isTokFloat(4) // Pos XYZ + // +" "+ ctx.isTokFloat(5) +" "+ ctx.isTokFloat(6) +" "+ ctx.isTokFloat(7)); // Rot XYZ + // + //game.log("DBG inCheckpoints | tokType: " + // +" "+ genericdoc_utils::tokenTypeStr(ctx.tokenType(0)) + // +" "+ genericdoc_utils::tokenTypeStr(ctx.tokenType(1)) // chkpNum, altpathNum + // +" "+ genericdoc_utils::tokenTypeStr(ctx.tokenType(2)) + // +" "+ genericdoc_utils::tokenTypeStr(ctx.tokenType(3)) + // +" "+ genericdoc_utils::tokenTypeStr(ctx.tokenType(4)) // Pos XYZ + // +" "+ genericdoc_utils::tokenTypeStr(ctx.tokenType(5)) + // +" "+ genericdoc_utils::tokenTypeStr(ctx.tokenType(6)) + // +" "+ genericdoc_utils::tokenTypeStr(ctx.tokenType(7))); // Rot XYZ + + if (ctx.isTokInt(0) && ctx.isTokInt(1) // chkpNum, altpathNum + && ctx.isTokFloat(2) && ctx.isTokFloat(3) && ctx.isTokFloat(4) // Pos XYZ + && ctx.isTokFloat(5) && ctx.isTokFloat(6) && ctx.isTokFloat(7)) // Rot XYZ + { + highestCheckpointNum = (ctx.getTokInt() > highestCheckpointNum) ? ctx.getTokInt() : highestCheckpointNum; + checkpointTokPositions.insertLast(ctx.getPos()); + } + } + ctx.seekNextLine(); + } + + // check input + if( highestCheckpointNum < 2 ) + { + game.log("Error in racesManager::addRaceFromDefinitionFile: A race should have at least 2 checkpoints."); + return -3; + } + + // Reset context and process pre-counted checkpoints + @ctx = GenericDocContextClass(doc); + for (uint i = 0; i < checkpointTokPositions.length(); i++) + { + // Seek to checkpoint + while (ctx.getPos() < checkpointTokPositions[i]) + ctx.moveNext(); + + // Decide object + string objName = this.raceList[raceID].objNameCheckpoint; + if (ctx.isTokString(8)) + objName = ctx.getTokString(8); // override + else if (ctx.getTokInt() == 1) + objName = this.raceList[raceID].objNameStart; + else if (ctx.getTokInt() == highestCheckpointNum) + objName = this.raceList[raceID].objNameFinish; + + // Submit checkpoint + const double[] v = { ctx.getTokFloat(2), ctx.getTokFloat(3), ctx.getTokFloat(4), ctx.getTokFloat(5), ctx.getTokFloat(6), ctx.getTokFloat(7) }; + this.raceList[raceID].addCheckpoint(ctx.getTokInt(0)-1, objName, v); + } + + return highestCheckpointNum; + } + + // Processes files under new terrn2 section [Races], returns number of races added or negative number on error. + int loadRacesFromTerrn2() + { + TerrainClass@ terrain = game.getTerrain(); + if (@terrain == null) + { + game.log("Error in racesManager::loadRacesFromTerrn2: no terrain loaded."); + return -1; + } + + GenericDocumentClass doc; + if (!doc.loadFromResource(terrain.getTerrainFileName(), terrain.getTerrainFileResourceGroup(), genericdoc_utils::FLAGSET_TERRN2)) + { + game.log("Error in racesManager::loadRacesFromTerrn2: could not load terrn2 file."); + return -2; + } + + GenericDocContextClass ctx(doc); + while (!ctx.endOfFile() && (!ctx.isTokKeyword() || ctx.getTokKeyword() != "[Races]")) + { + ctx.seekNextLine(); + } + //game.log("DBG [Races] tokenPos:" + ctx.getPos() + ", EOF:" + ctx.endOfFile()); + ctx.seekNextLine(); + //game.log("DBG next line: tokenType:" + genericdoc_utils::tokenTypeStr(ctx.tokenType())); + + const string BRACKET="["; + const string EQUALS="="; + int numRaces = 0; + while (!ctx.endOfFile() && (!ctx.isTokKeyword() || ctx.getTokKeyword()[0]!=BRACKET[0])) + { + string filename = ctx.getTokString(); + //game.log("DBG [Races] filename " + filename + " at pos " + ctx.getPos()); + // Cut off the mandatory '=' + if (filename[filename.length() - 1] == EQUALS[0]) + { + filename = filename.substr(0, filename.length() - 1); + } + if (this.addRaceFromDefinitionFile(filename, terrain.getTerrainFileResourceGroup()) > 0) + { + numRaces++; + } + ctx.seekNextLine(); + } + + return numRaces; + } + // add a race // pre: nothing // post: A new race was added and built int addRace(const string &in raceName, const double[][] &in checkpoints, int laps = 0, const string &in objName_checkpoint = "chp-checkpoint", const string &in objName_start = "chp-start", const string &in objName_finish = "chp-start", const string &in version = "unknown") { // debug: game.log("racesManager::addRace(\"" + raceName + "\", \"" + objName_start + "\", \"" + objName_checkpoint + "\", \"" + objName_finish + "\", checkpoints, loop) called."); - + // check input if( checkpoints.length() < 2 ) { game.log("Error in racesManager::addRace: A race should have at least 2 checkpoints."); return -1; } - + int raceID = this.getNewRaceID(); this.raceList.resize(this.raceCount); @this.raceList[raceID] = @raceBuilder(raceID); @@ -207,7 +367,7 @@ shared class racesManager { this.raceList[raceID].loadRace(@this.raceDataFile); return raceID; } - + // Advanced users only int addNewEmptyRace() { @@ -216,7 +376,7 @@ shared class racesManager { @this.raceList[raceID] = @raceBuilder(raceID); return raceID; } - + int getNewRaceID() { // try to recycle a race ID @@ -227,7 +387,7 @@ shared class racesManager { } return this.raceCount++; } - + // pre: nothing // post: A callback function has been registered void setCallback(const string &in event, RACE_EVENT_CALLBACK @func) @@ -254,13 +414,13 @@ shared class racesManager { game.log("Error in racesManager::removeCallback: Event '" + event + "' does not exist."); callbacks.set(event, null); } - + // This will get called when a truck is at a checkpoint // You shouldn't call this manually (use the callback instead) void raceEvent(int trigger_type, string inst, const string &in box, int nodeid) { // debug: game.log("racesManager::raceEvent(" + trigger_type + ", \"" + inst + "\", \"" + box + "\", " + nodeid + ") called."); - + if( box == "race_penalty" and !this.penaltyGiven and this.state == this.STATE_Racing ) { // the inst string contains the information about the event @@ -272,7 +432,7 @@ shared class racesManager { if( raceID == this.currentRace or raceID == -1 ) { int penaltyTime = this.getPenaltyTime(raceID); - + // call the callback function RACE_EVENT_CALLBACK @handle; if( callbacks.get("PenaltyEvent", @handle) and (handle !is null)) @@ -294,7 +454,7 @@ shared class racesManager { } } return; - } + } else if( box == "race_abort" and this.state == this.STATE_Racing ) { // the inst string contains the information about the event @@ -305,7 +465,7 @@ shared class racesManager { int raceID = parseInt(tmp[1]); if( raceID == this.currentRace or raceID == -1 ) { - + // call the callback function RACE_EVENT_CALLBACK @handle; if( callbacks.get("AbortEvent", @handle) and not (handle is null)) @@ -325,18 +485,18 @@ shared class racesManager { if( result ) return; } - + this.cancelCurrentRace(); } } return; } - + // We don't want to handle the same checkpoint twice if( ( inst == this.lastRaceEventInstance ) ) return; this.lastRaceEventInstance = inst; - + // call the callback function RACE_EVENT_CALLBACK @handle; if( callbacks.get("RaceEvent", @handle) and not (handle is null)) @@ -355,14 +515,14 @@ shared class racesManager { if( result ) return; } - + // the inst string contains the information about the checkpoint array@ tmp = inst.split("|"); if( tmp.length() >= 3 and (tmp[0] == "checkpoint") ) { int checkpointNum = parseInt(tmp[2]); int raceID = parseInt(tmp[1]); - + if( checkpointNum == this.lastCheckpoint ) return; else if(this.state == STATE_NotInRace) @@ -383,7 +543,7 @@ shared class racesManager { } else if(this.state == STATE_Racing and currentRace == raceID) { - // we hit a checkpoint from the same race! + // we hit a checkpoint from the same race! if( checkpointNum == this.raceList[raceID].finishNum and this.raceList[raceID].finishNum == this.raceList[raceID].getNextCheckpointNum(this.lastCheckpoint)) { // passing the finishline if( (this.currentLap < this.raceList[raceID].laps) or (this.raceList[raceID].laps == this.LAPS_Unlimited) ) @@ -436,7 +596,7 @@ shared class racesManager { } } } - + string formatTime(double seconds) { if( seconds > 60.0 ) @@ -444,7 +604,7 @@ shared class racesManager { else return seconds+" seconds"; } - + // pre: The race corresponding with the raceID exists // There's no other race running // post: The race is running @@ -466,7 +626,7 @@ shared class racesManager { } return; } - + this.state = STATE_Racing; this.currentRace = raceID; this.currentLap = 1; @@ -480,7 +640,7 @@ shared class racesManager { this.raceList[raceID].lastTimeTillPoint[0] = 0.0; this.penaltyTime.resize(0); this.penaltyTime.resize(this.raceList[raceID].checkPointsCount); - + // build the message this.message("Race "+this.raceList[raceID].raceName+" started!", "bullet_go.png"); if( (this.raceList[raceID].laps > 1) ) @@ -490,7 +650,7 @@ shared class racesManager { if( this.showBestLap and this.raceList[raceID].bestLapTime > 0.0 and this.raceList[raceID].laps != this.LAPS_NoLaps and this.raceList[raceID].laps != this.LAPS_One) this.message("Best lap time: "+this.formatTime(this.raceList[raceID].bestLapTime)+"!", "information.png"); this.message("Good Luck!", "emoticon_smile.png"); - + // call the callback function RACE_EVENT_CALLBACK @handle; if( callbacks.get("RaceStart", @handle) and not (handle is null)) @@ -501,20 +661,20 @@ shared class racesManager { handle(args); } } - + // this is private as this should only be called when the user drives through the finish checkpoint // If you need to abort a race, use racesManager::cancelCurrentRace() instead. void finishCurrentRace() { // debug: game.log("racesManager::finishCurrentRace() called."); - + int rid = this.currentRace; - + // get the lap time double lapTime = game.getTime() - this.lapStartTime; // get the race time double raceTime = game.getTime() - this.raceStartTime; - + // calculate race time difference string raceTimeDiff = ""; if(this.showTimeDiff and this.raceList[rid].bestRaceTime > 0.0) @@ -524,7 +684,7 @@ shared class racesManager { else if( (raceTime-this.raceList[rid].bestRaceTime) < 0 ) raceTimeDiff = " ("+ (raceTime-this.raceList[rid].bestRaceTime) +")"; } - + // calculate lap time difference string lapTimeDiff = ""; if(this.showTimeDiff and this.raceList[rid].bestLapTime > 0.0) @@ -534,7 +694,7 @@ shared class racesManager { else if( (lapTime-this.raceList[rid].bestLapTime) < 0 ) lapTimeDiff = " ("+ (lapTime-this.raceList[rid].bestLapTime) +")"; } - + // do more time stuff this.raceList[rid].lastTimeTillPoint[this.raceList[rid].checkPointsCount-1] = lapTime; bool newBestRace; @@ -551,7 +711,7 @@ shared class racesManager { this.state = this.STATE_NotInRace; this.removeArrow(); this.lastRaceEventInstance = ""; - + // race completed! this.raceList[rid].completed = true; @@ -561,10 +721,10 @@ shared class racesManager { this.message("New best race time!", "flag_green.png"); if( this.showBestLap and newBestLap and this.raceList[rid].laps != this.LAPS_NoLaps and this.raceList[rid].laps != this.LAPS_One) this.message("New best lap time!"+lapTimeDiff, "flag_green.png"); - + // store the new race times saveRace(rid); - + // call the callback function RACE_EVENT_CALLBACK @handle; if( callbacks.get("RaceFinish", @handle) and not (handle is null)) @@ -577,17 +737,17 @@ shared class racesManager { handle(args); } } - + // This is private, as you shouldn't manually advance a lap void advanceLap() { // debug: game.log("racesManager::advanceLap() called."); - + int rid = this.currentRace; - + // get the lapTime double lapTime = game.getTime() - this.lapStartTime; - + // calculate time difference string timeDiff = ""; if(this.showTimeDiff and this.raceList[rid].bestLapTime > 0.0) @@ -597,7 +757,7 @@ shared class racesManager { else if( (lapTime-this.raceList[rid].bestLapTime) < 0 ) timeDiff = " ("+ (lapTime-this.raceList[rid].bestLapTime) +")"; } - + // do time stuff this.raceList[rid].lastTimeTillPoint[this.raceList[rid].checkPointsCount-1] = lapTime; bool newBestLap; @@ -605,12 +765,12 @@ shared class racesManager { game.stopTimer(); game.startTimer(rid); this.lapStartTime = game.getTime(); - + // advance the lap this.currentLap++; this.lastCheckpoint = 0; this.recalcArrow(); - + // build the message if( this.raceList[rid].laps != this.LAPS_Unlimited ) this.message("Lap "+(this.currentLap-1)+" done!", "flag_green.png"); @@ -618,10 +778,10 @@ shared class racesManager { this.message("New best lap time: "+this.formatTime(lapTime)+"!"+timeDiff, "flag_green.png"); else this.message("Lap time: "+this.formatTime(lapTime)+"!"+timeDiff, "flag_green.png"); - + // store the new race times saveRace(rid); - + // call the callback function RACE_EVENT_CALLBACK @handle; if( callbacks.get("AdvanceLap", @handle) and not (handle is null)) @@ -632,7 +792,7 @@ shared class racesManager { handle(args); } } - + // called by raceEvent when the user drives through a checkpoint that is not a finishline and not a startline // pre: The race corresponding with the raceID exists // The race corresponding with the raceID is running at the moment @@ -642,12 +802,12 @@ shared class racesManager { // debug: game.log("racesManager::advanceCheckpoint(" + raceID + ") called."); this.lastCheckpoint = this.raceList[raceID].getNextCheckpointNum(this.lastCheckpoint); - + this.recalcArrow(); - + double time = game.getTime() - this.lapStartTime; - - // calculate time difference + + // calculate time difference string timeDiff = ""; if(this.showTimeDiff and this.raceList[raceID].bestTimeTillPoint[this.lastCheckpoint] > 0.0) { @@ -663,7 +823,7 @@ shared class racesManager { } else this.raceList[raceID].bestTimeTillPoint[this.lastCheckpoint] = time; - + this.raceList[raceID].lastTimeTillPoint[this.lastCheckpoint] = time; // build the message @@ -684,7 +844,7 @@ shared class racesManager { handle(args); } } - + void setBestLapTime(int raceID, double time) { this.raceList[raceID].bestLapTime = time; @@ -693,7 +853,7 @@ shared class racesManager { { this.raceList[raceID].bestRaceTime = time; } - + // Set a new best laptime (if it's better than the old best laptime) // pre: The race corresponding with the raceID exists // post: The new time is checked and stored if necessary @@ -712,16 +872,16 @@ shared class racesManager { args.set("newTime", time); handle(args); } - + this.raceList[raceID].bestLapTime = time; game.setBestLapTime(time); newBestLap = true; } else newBestLap = false; - + if( this.submitScore ) - { + { string api_return; dictionary dict; dict.set("race-name", ""+this.raceList[raceID].raceName); @@ -734,13 +894,13 @@ shared class racesManager { // dict.set("chptime"+i, this.raceList[raceID].lastTimeTillPoint[i]); } dict.set("split-times", ""+times); - + int res = game.useOnlineAPI("/races", dict, api_return); // debug: game.log("useOnlineAPI returned: " + res); // debug: game.log("useOnlineAPI return string: " + api_return); } } - + // Set a new best racetime (if it's better than the old best racetime) // pre: The race corresponding with the raceID exists // post: The new time is checked and stored if necessary @@ -759,19 +919,19 @@ shared class racesManager { args.set("newTime", time); handle(args); } - + this.raceList[raceID].bestRaceTime = time; - + newBestRace = true; } else newBestRace = false; } - + void eventCallback(int eventnum, int value) { // debug: game.log("raceManager::eventCallback("+eventnum+", "+value+") called"); - + if( this.state != this.STATE_Racing ) return; @@ -817,13 +977,13 @@ shared class racesManager { this.message("AngelScript injection is not allowed during races! Race aborted.", "stop.png"); } } - + void message(const string &in msg, const string &in icon) { if(!this.silentMode) game.message(msg, icon, 10000, true); // 10 seconds visible, enforce visibility } - + void unlockRace(int raceID) { this.raceList[raceID].locked = false; @@ -833,7 +993,7 @@ shared class racesManager { { this.raceList[raceID].locked = true; } - + void deleteRace(int raceID) { // debug: game.log("raceManager::deleteRace("+raceID+") called."); @@ -841,14 +1001,14 @@ shared class racesManager { this.cancelCurrentRace(); this.raceList[raceID].destroy(); this.raceList[raceID].awaitingRecycling = true; - + if( raceID == this.raceCount-1 ) { this.raceCount--; this.raceList.resize(this.raceCount); } } - + int getRaceIDbyName(const string &in raceName_in) { for( int i = 0; i this.raceList[this.currentRace].checkPointsCount-1) ) { // hide the arrow this.removeArrow(); @@ -1043,7 +1203,7 @@ shared class racesManager { if( raceID == this.currentRace ) instanceNum = parseInt(tmp[3]); } - + if( this.raceList[this.currentRace].checkpoints[position].length() > uint(instanceNum) ) v = this.raceList[this.currentRace].checkpoints[position][instanceNum]; else if( this.raceList[this.currentRace].checkpoints[position].length() > 0 ) @@ -1053,7 +1213,7 @@ shared class racesManager { this.removeArrow(); return; } - + if( this.raceList[this.currentRace].laps == this.LAPS_NoLaps ) game.updateDirectionArrow(this.raceList[this.currentRace].raceName+" checkpoint "+position+" / "+(this.raceList[this.currentRace].checkPointsCount-1), vector3(v[0], v[1], v[2])); else @@ -1063,47 +1223,47 @@ shared class racesManager { game.updateDirectionArrow(this.raceList[this.currentRace].raceName+" checkpoint "+position+" / "+(this.raceList[this.currentRace].checkPointsCount), vector3(v[0], v[1], v[2])); } } - + void removeArrow() { game.hideDirectionArrow(); } - + void setPenaltyTime(int raceID, int seconds) { this.raceList[raceID].penaltyTime = seconds; } - + int getPenaltyTime(int raceID) { return this.raceList[raceID].penaltyTime; } - + void setVersion(int raceID, const string &in version) { this.raceList[raceID].setVersion(version); } - + void hideRace(int raceID) { this.raceList[raceID].hide(); } - + void unhideRace(int raceID) { this.raceList[raceID].unhide(); } - + void setStartNumber(int raceID, int startNum) { this.raceList[raceID].startNum = startNum; } - + void resetEventCallback() { this.lastRaceEventInstance = ""; } - + void finalize() { for( int raceID = 0; raceID < this.raceCount; ++raceID ) @@ -1111,7 +1271,7 @@ shared class racesManager { this.raceList[raceID].loadRace(@this.raceDataFile); } } - + void finalize(int raceID) { loadRace(raceID); @@ -1125,16 +1285,16 @@ shared class racesManager { this.raceList[raceID].saveRace(@this.raceDataFile); } - this.raceDataFile.save(); + this.raceDataFile.save(); } - + void saveRace(int raceID) { this.raceList[raceID].saveRace(@this.raceDataFile); this.raceDataFile.save(); } - - + + void loadRace(int raceID) { this.raceList[raceID].loadRace(@this.raceDataFile); @@ -1167,11 +1327,10 @@ shared class raceBuilder { bool awaitingRecycling; bool hidden; string raceBuilderVersion; - // object names used in `addChpCoordinates()` - for use by terrain editor conversion script - string exporterCheckpointObjName; - string exporterStartObjName; - string exporterFinishObjName; - + string objNameCheckpoint; + string objNameStart; + string objNameFinish; + raceBuilder(int id) { // game.log("raceBuilder::raceBuilder("+id+");"); @@ -1191,12 +1350,12 @@ shared class raceBuilder { this.raceBuilderVersion = "RoR_RaceBuilder_v0.01"; this.hidden = false; } - + void setVersion(const string &in version) { this.raceVersion = version; } - + void addChpCoordinates(double[][] checkpoints_in, const string &in objName_checkpoint, const string &in objName_start, const string &in objName_finish, uint startNumber) { // Remove empty coordinates at end @@ -1232,7 +1391,7 @@ shared class raceBuilder { game.log("Warning in raceBuilder("+this.id+")::addChpCoordinates: A coordinate should exist out of 6 numbers. Extra numbers dropped."); checkpoints_in[i].resize(6); } - + // Get the correct object name if( i == 0 ) oname = objName_start; @@ -1240,16 +1399,16 @@ shared class raceBuilder { oname = objName_finish; else oname = objName_checkpoint; - + this.addCheckpoint(startNumber+i, oname, checkpoints_in[i]); } - + // remember used object names for terrain editor conversion script - this.exporterCheckpointObjName = objName_checkpoint; - this.exporterStartObjName = objName_start; - this.exporterFinishObjName = objName_finish; + this.objNameCheckpoint = objName_checkpoint; + this.objNameStart = objName_start; + this.objNameFinish = objName_finish; } - + int getNextCheckpointNum(int lastCheckpoint) { if( lastCheckpoint < this.checkPointsCount-1 ) @@ -1312,7 +1471,7 @@ shared class raceBuilder { if( not this.hidden ) game.spawnObject(objName, "checkpoint|"+this.id+"|"+number+"|"+this.chpInstances[number]++, vector3(v[0], v[1], v[2]), vector3(v[3], v[4], v[5]), "raceEvent", false); } - + void deleteCheckpoint(int number) { for( int i = this.chpInstances[number]-1; i >= 0; i-- ) @@ -1320,7 +1479,7 @@ shared class raceBuilder { this.deleteCheckpoint(number, i); } } - + // this gets the actual number of instances // (if you remove a checkpoint instance, then it may // get destroyed ingame, but the array may not always @@ -1335,7 +1494,7 @@ shared class raceBuilder { } return count; } - + bool checkpointExists(int chpNum, int instance) { if( @@ -1349,7 +1508,7 @@ shared class raceBuilder { else return true; } - + void deleteCheckpoint(int number, int instance) { if( not this.checkpointExists(number, instance) ) @@ -1389,7 +1548,7 @@ shared class raceBuilder { { this.checkpoints[number][instance].resize(0); } - + game.destroyObject("checkpoint|"+this.id+"|"+number+"|"+instance); } @@ -1407,14 +1566,14 @@ shared class raceBuilder { this.finishNum = 0; this.startNum = 0; } - + void hide() { if( this.hidden ) return; else this.hidden = true; - + for(int i = 0 ; i