From 7e9e801f746d4233af7d32251de89a80d177894b Mon Sep 17 00:00:00 2001 From: ohlidalp Date: Mon, 11 Nov 2024 01:13:10 +0100 Subject: [PATCH 01/36] :angel:Script: all '*_utils' scripts style unified --- resources/scripts/gridviewer_utils.as | 222 ++++++++++++++------------ resources/scripts/imgui_utils.as | 4 +- resources/scripts/scriptinfo_utils.as | 157 +++++++++--------- 3 files changed, 206 insertions(+), 177 deletions(-) diff --git a/resources/scripts/gridviewer_utils.as b/resources/scripts/gridviewer_utils.as index 7a71b22d0f..ff49b799b4 100644 --- a/resources/scripts/gridviewer_utils.as +++ b/resources/scripts/gridviewer_utils.as @@ -2,107 +2,123 @@ /// \brief Generic scrolling & zooming UI for drawing via ImDrawList // =================================================== -class GridViewer +// By convention, all includes have filename '*_utils' and namespace matching filename. +namespace gridviewer_utils { - // CFG: - string childWindowID; - float zoomMax = 10.f; - float zoomMin = 0.1f; - float zoomDefault = (zoomMin + zoomMax) / 2; - int hAxis=0; - int vAxis=1; -array axisLineColors = { color(1,0,0,1), color(0.2,0.3,1,1), color(0.1, 0.9, 0.05, 1) }; - vector2 scrollRegionPadding = vector2(50,50); - vector2 controlsBoxPadding = vector2(4,4); - int gridSizeMin = 100; - int gridSizeMax = 10000; - - // STATE: - float zoom = 1.f; - vector2 contentRegionSize; - vector2 midWindowScreenPos; - vector2 childScreenPos; - vector2 nodesMin; - vector2 nodesMax; - int gridSize = 25; - bool isChildWindowHovered = false; - - // FUNCS: - void begin(vector2 size) { - int flags = 1 << 11; // _HorizontalScrollbar - this.childScreenPos=ImGui::GetCursorScreenPos(); - ImGui::BeginChild(childWindowID, size, true, flags); - contentRegionSize=ImGui::GetWindowContentRegionMax() - ImGui::GetWindowContentRegionMin(); - midWindowScreenPos = ImGui::GetWindowPos()+ contentRegionSize/2; - nodesMin=vector2(FLT_MAX, FLT_MAX); - nodesMax=vector2(-999999, -999999); - } - - void drawControls() { - ImGui::TextDisabled(this.childWindowID); - ImGui::SameLine(); - ImGui::SetNextItemWidth(65); - ImGui::SliderFloat('Zoom', zoom, zoomMin, zoomMax); - ImGui::SameLine(); - ImGui::SetNextItemWidth(65); - ImGui::SliderInt("Grid", gridSize, gridSizeMin, gridSizeMax); - } - - vector2 localToScreenPos(vector3 posIn) { - // zoom only, without scrolling - vector2 pos= midWindowScreenPos+vector2(posIn[hAxis], posIn[vAxis])*zoom; - nodesMin=vector2(fmin(nodesMin.x, pos.x), fmin(nodesMin.y, pos.y)); - nodesMax=vector2(fmax(nodesMax.x, pos.x), fmax(nodesMax.y, pos.y)); - // add scrolling - pos += vector2( - ImGui::GetScrollMaxX() - 2*ImGui::GetScrollX(), - ImGui::GetScrollMaxY() - 2*ImGui::GetScrollY()); - - return pos; - } - - vector3 screenToLocalPos(vector2 screenPosIn) - { - vector2 localXY = (screenPosIn -midWindowScreenPos)/zoom; - // add scrolling - localXY -= vector2( - ImGui::GetScrollMaxX() - 2*ImGui::GetScrollX(), - ImGui::GetScrollMaxY() - 2*ImGui::GetScrollY()); - // screen to local, without scrolling - vector3 localPos(0,0,0); - setAxisVal(localPos, hAxis, localXY.x); - setAxisVal(localPos, vAxis, localXY.y); - - return localPos; - } - - void drawAxisLines() { - vector2 origin = this.localToScreenPos(vector3(0,0,0)); - ImGui::GetWindowDrawList().AddLine(origin+vector2(0, -9999), origin+vector2(0,9999), axisLineColors[vAxis]); - ImGui::GetWindowDrawList().AddLine(origin+vector2(-9999, 0), origin+vector2(9999, 0), axisLineColors[hAxis]); - } - - void end() { - drawAxisLines(); - // draw dummies - force scroll region - ImGui::SetCursorPos((nodesMin-scrollRegionPadding) - childScreenPos); ImGui::Dummy(vector2(1,1)); - ImGui::SetCursorPos((nodesMax+scrollRegionPadding) - childScreenPos); ImGui::Dummy(vector2(1,1)); - - // controls go on the top of the child, scrolling is cancelled out. - ImGui::SetCursorPos(vector2(ImGui::GetScrollX(), ImGui::GetScrollY()) + controlsBoxPadding); - drawControls(); - isChildWindowHovered = ImGui::IsWindowHovered(); - ImGui::EndChild(); - } - -float fmax(float a, float b) { return a>b?a:b; } -float fmin(float a, float b) { return a axisLineColors = { color(1,0,0,1), color(0.2,0.3,1,1), color(0.1, 0.9, 0.05, 1) }; + vector2 scrollRegionPadding = vector2(50,50); + vector2 controlsBoxPadding = vector2(4,4); + int gridSizeMin = 100; + int gridSizeMax = 10000; + + // STATE: + float zoom = 1.f; + vector2 contentRegionSize; + vector2 midWindowScreenPos; + vector2 childScreenPos; + vector2 nodesMin; + vector2 nodesMax; + int gridSize = 25; + bool isChildWindowHovered = false; + + // FUNCS: + void begin(vector2 size) + { + int flags = ImGuiWindowFlags_HorizontalScrollbar; + this.childScreenPos=ImGui::GetCursorScreenPos(); + ImGui::BeginChild(childWindowID, size, true, flags); + contentRegionSize=ImGui::GetWindowContentRegionMax() - ImGui::GetWindowContentRegionMin(); + midWindowScreenPos = ImGui::GetWindowPos()+ contentRegionSize/2; + nodesMin=vector2(FLT_MAX, FLT_MAX); + nodesMax=vector2(-999999, -999999); + } + + void drawControls() + { + ImGui::TextDisabled(this.childWindowID); + ImGui::SameLine(); + ImGui::SetNextItemWidth(65); + ImGui::SliderFloat('Zoom', zoom, zoomMin, zoomMax); + ImGui::SameLine(); + ImGui::SetNextItemWidth(65); + ImGui::SliderInt("Grid", gridSize, gridSizeMin, gridSizeMax); + } + + vector2 localToScreenPos(vector3 posIn) + { + // zoom only, without scrolling + vector2 pos= midWindowScreenPos+vector2(posIn[hAxis], posIn[vAxis])*zoom; + nodesMin=vector2(fmin(nodesMin.x, pos.x), fmin(nodesMin.y, pos.y)); + nodesMax=vector2(fmax(nodesMax.x, pos.x), fmax(nodesMax.y, pos.y)); + // add scrolling + pos += vector2( + ImGui::GetScrollMaxX() - 2*ImGui::GetScrollX(), + ImGui::GetScrollMaxY() - 2*ImGui::GetScrollY()); + + return pos; + } + + vector3 screenToLocalPos(vector2 screenPosIn) + { + vector2 localXY = (screenPosIn -midWindowScreenPos)/zoom; + // add scrolling + localXY -= vector2( + ImGui::GetScrollMaxX() - 2*ImGui::GetScrollX(), + ImGui::GetScrollMaxY() - 2*ImGui::GetScrollY()); + // screen to local, without scrolling + vector3 localPos(0,0,0); + setAxisVal(localPos, hAxis, localXY.x); + setAxisVal(localPos, vAxis, localXY.y); + + return localPos; + } + + void drawAxisLines() + { + vector2 origin = this.localToScreenPos(vector3(0,0,0)); + ImGui::GetWindowDrawList().AddLine(origin+vector2(0, -9999), origin+vector2(0,9999), axisLineColors[vAxis]); + ImGui::GetWindowDrawList().AddLine(origin+vector2(-9999, 0), origin+vector2(9999, 0), axisLineColors[hAxis]); + } + + void end() + { + drawAxisLines(); + // draw dummies - force scroll region + ImGui::SetCursorPos((nodesMin-scrollRegionPadding) - childScreenPos); ImGui::Dummy(vector2(1,1)); + ImGui::SetCursorPos((nodesMax+scrollRegionPadding) - childScreenPos); ImGui::Dummy(vector2(1,1)); + + // controls go on the top of the child, scrolling is cancelled out. + ImGui::SetCursorPos(vector2(ImGui::GetScrollX(), ImGui::GetScrollY()) + controlsBoxPadding); + drawControls(); + isChildWindowHovered = ImGui::IsWindowHovered(); + ImGui::EndChild(); + } + + // HELPERS: + + private float fmax(float a, float b) { return a>b?a:b; } + + private float fmin(float a, float b) { return a title:"+sinfo.title+", brief:"+sinfo.brief+", text:"+sinfo.text);` // ====================================================== -class ScriptInfo +// By convention, all includes have filename '*_utils' and namespace matching filename. +namespace scriptinfo_utils { - string title; - string brief; - string text; -} -ScriptInfo@ ExtractScriptInfo(string body) -{ - ScriptInfoHelper h; - // find the annotations - ScriptInfo scriptinfo; - int state = 0; //-1 DONE, 0 = start of line, 1/2/3=number of slashes at start of line, 4=blanks after slashes, - //5=capturing cmd, 6=capturing title, 7=cabturing subtitle, 8=capturing long description - uint pos = 0; - uint capture_startpos = 0; + class ScriptInfo + { + string title; + string brief; + string text; + } - while (state != -1 && pos < body.length()) - { - h.c = body[pos]; - //game.log("DBG scriptinfo: state="+state+", capture_startpos="+capture_startpos+", c="+body.substr(pos, 1)); - switch (state) - { - case 0: // line start - case 1: // / - case 2: // // ... - if (h.ischar('/')){ state++; } else {state=-1;} - break; - case 3: - if ( h.isblank() ) { state=4; } - else if (h.ischar('\\')) { capture_startpos = pos; state=5; } - else { capture_startpos = pos; state=8; } - break; - case 4: // blanks after slashes - if (!h.isblank()) { - if (h.ischar('\\')) { capture_startpos = pos; state=5; } else { capture_startpos = pos; state=8; } } - else if (h.isEol()) { state=0;} - break; - case 5: // cmd, like '\file' - if (h.isblank()) { - string cmd = body.substr(capture_startpos, pos-capture_startpos); - //game.log('DBG cmd: "'+cmd+'"'); - if (h.isEol()) {state=0;} - else if (cmd=='\\title') { state=6; capture_startpos = pos; } - else if (cmd=='\\brief') {state=7;capture_startpos = pos; } - else {state=8;capture_startpos = pos; } } - break; - case 6: - if (h.isEol()) {state=0; string cmd = body.substr(capture_startpos, pos-capture_startpos); scriptinfo.title=h.trimStart(cmd); } - break; - case 7: - if (h.isEol()) {state=0;string cmd = body.substr(capture_startpos, pos-capture_startpos); scriptinfo.brief=h.trimStart(cmd); } - break; - case 8: - if (h.isEol()) {state=0; string cmd = body.substr(capture_startpos, pos-capture_startpos); scriptinfo.text+=cmd+' '; } - break; - } - pos++; - } - return scriptinfo; -} + ScriptInfo@ ExtractScriptInfo(string body) + { + scriptinfo_internal::ScriptInfoHelper h; + // find the annotations + ScriptInfo scriptinfo; + int state = 0; //-1 DONE, 0 = start of line, 1/2/3=number of slashes at start of line, 4=blanks after slashes, + //5=capturing cmd, 6=capturing title, 7=cabturing subtitle, 8=capturing long description + uint pos = 0; + uint capture_startpos = 0; + + while (state != -1 && pos < body.length()) + { + h.c = body[pos]; + //game.log("DBG scriptinfo: state="+state+", capture_startpos="+capture_startpos+", c="+body.substr(pos, 1)); + switch (state) + { + case 0: // line start + case 1: // / + case 2: // // ... + if (h.ischar('/')){ state++; } else {state=-1;} + break; + case 3: + if ( h.isblank() ) { state=4; } + else if (h.ischar('\\')) { capture_startpos = pos; state=5; } + else { capture_startpos = pos; state=8; } + break; + case 4: // blanks after slashes + if (!h.isblank()) { + if (h.ischar('\\')) { capture_startpos = pos; state=5; } else { capture_startpos = pos; state=8; } } + else if (h.isEol()) { state=0;} + break; + case 5: // cmd, like '\file' + if (h.isblank()) { + string cmd = body.substr(capture_startpos, pos-capture_startpos); + //game.log('DBG cmd: "'+cmd+'"'); + if (h.isEol()) {state=0;} + else if (cmd=='\\title') { state=6; capture_startpos = pos; } + else if (cmd=='\\brief') {state=7;capture_startpos = pos; } + else {state=8;capture_startpos = pos; } } + break; + case 6: + if (h.isEol()) {state=0; string cmd = body.substr(capture_startpos, pos-capture_startpos); scriptinfo.title=h.trimStart(cmd); } + break; + case 7: + if (h.isEol()) {state=0;string cmd = body.substr(capture_startpos, pos-capture_startpos); scriptinfo.brief=h.trimStart(cmd); } + break; + case 8: + if (h.isEol()) {state=0; string cmd = body.substr(capture_startpos, pos-capture_startpos); scriptinfo.text+=cmd+' '; } + break; + } + pos++; + } + return scriptinfo; + } + +} // namespace scriptinfo_utils -class ScriptInfoHelper +namespace scriptinfo_internal // Don't touch! { - uint c; - bool ischar(string s) { for (uint i=0; i 0) { - return s.substr(found); - } else { - return s; - } - } + class ScriptInfoHelper + { + uint c; + bool ischar(string s) { for (uint i=0; i 0) + { + return s.substr(found); + } + else + { + return s; + } + } + } } /* From c8d3fac0be9cde73b67c386a976e1d60fb3098e6 Mon Sep 17 00:00:00 2001 From: ohlidalp Date: Mon, 11 Nov 2024 01:22:16 +0100 Subject: [PATCH 02/36] :angel:script: scripts updated to use new includes & game API. changes made: * added `*_utils` namespaces (new convention for includes) * unified style of scripts: using global variables and functions instead of classes. * scripts auto-formatted by script_editor.as --- resources/scripts/README.txt | 2 +- resources/scripts/demo_script.as | 26 +- .../scripts/example_GenericDocument_editor.as | 388 +++--- resources/scripts/example_RigEditor_alpha.as | 1082 +++++++-------- resources/scripts/example_ogre_inspector.as | 2 +- resources/scripts/example_ogre_vertexData.as | 66 +- resources/scripts/road_editor.as | 1210 ++++++++--------- resources/scripts/script_editor.as | 24 +- 8 files changed, 1425 insertions(+), 1375 deletions(-) diff --git a/resources/scripts/README.txt b/resources/scripts/README.txt index 248f69cd7e..8aa09d8bd7 100644 --- a/resources/scripts/README.txt +++ b/resources/scripts/README.txt @@ -8,7 +8,7 @@ FOR DEVELOPERS: Visit https://developer.rigsofrods.org/ and click "Script-side A Conventions: * Scripts named 'example_*.as' are short introductory scripts that perform one thing, usually showing UI with tips and controls. You can run them using 'loadscript'. * Scripts named '*_editor.as' are complete tools. You can run them using 'loadscript'. -* Scripts named '*_utils.as' are includes - running them directly is not possible, 'loadscript' would result in error "there is no main() function". +* Scripts named '*_utils.as' are includes - running them via 'loadscript' does nothing, they are to be `#include`-d into other scripts. Special scripts: * 'default.as' - fallback terrain script, loaded if modder didn't provide custom script. diff --git a/resources/scripts/demo_script.as b/resources/scripts/demo_script.as index 331d40b79a..40db0b5fdd 100644 --- a/resources/scripts/demo_script.as +++ b/resources/scripts/demo_script.as @@ -44,14 +44,19 @@ CVarClass@ g_sys_cache_dir = console.cVarFind("sys_cache_dir"); CVarClass@ g_sim_state = console.cVarFind("sim_state"); // 0=off, 1=running, 2=paused, 3=terrain editor, see SimState in Application.h CVarClass@ g_mp_state = console.cVarFind("mp_state"); // 0=disabled, 1=connecting, 2=connected, see MpState in Application.h CVarClass@ g_io_arcade_controls = console.cVarFind("io_arcade_controls"); // bool -GenericDocumentClass@ g_displayed_document = null; -string g_displayed_doc_filename; + array g_terrain_tobj_files; SoundScriptInstanceClass@ g_playing_soundscript = null; SoundClass@ g_playing_sound = null; bool g_sound_follows_player = true; string g_demofile_data; -imgui_utils::CloseWindowPrompt g_window_closebtn_handler; + +// Main window state +imgui_utils::CloseWindowPrompt closeBtnHandler; + +// Document window state +GenericDocumentClass@ g_displayed_document = null; +string g_displayed_doc_filename; // tab settings bool demotabsReorderable = false; @@ -75,14 +80,14 @@ void main() /* --------------------------------------------------------------------------- - Script update function - invoked once every rendered frame, + Script update function - invoked by the game once every rendered frame, with elapsed time (delta time, in seconds) as parameter. */ void frameStep(float dt) { // Open demo window - ImGui::Begin("Demo Script", g_window_closebtn_handler.windowOpen, ImGuiWindowFlags_AlwaysAutoResize); - g_window_closebtn_handler.draw(); + ImGui::Begin("Demo Script", closeBtnHandler.windowOpen, ImGuiWindowFlags_AlwaysAutoResize); + closeBtnHandler.draw(); // show some stats ImGui::Text("Total frames: " + g_total_frames); @@ -340,7 +345,8 @@ void drawDocumentWindow() { ImGui::PushID("document view"); string caption = "Document view (" + g_displayed_doc_filename + ")"; - ImGui::Begin(caption, /*open:*/true, /*flags:*/0); + bool docWindowOpen = true; + ImGui::Begin(caption, docWindowOpen, /*flags:*/0); GenericDocContextClass reader(g_displayed_document); while (!reader.endOfFile()) @@ -387,6 +393,12 @@ void drawDocumentWindow() } ImGui::End(); + if (!docWindowOpen) + { + @g_displayed_document = null; + g_displayed_doc_filename = ""; + } + ImGui::PopID(); //"document view" } diff --git a/resources/scripts/example_GenericDocument_editor.as b/resources/scripts/example_GenericDocument_editor.as index 4500003ad6..a58b9bceda 100644 --- a/resources/scripts/example_GenericDocument_editor.as +++ b/resources/scripts/example_GenericDocument_editor.as @@ -6,26 +6,45 @@ #include "imgui_utils.as" imgui_utils::CloseWindowPrompt closeBtnHandler; -class RigEditor +GenericDocumentClass@ displayedDocument = null; +int hoveredTokenPos = -1; +int focusedTokenPos = -1; +string errorString; +string nameOfProjectPendingCreation; // name of the truck file which will become available in modcache. +CacheEntryClass@ projectEntry; + + + +// `frameStep()` runs every frame; `dt` is delta time in seconds. +void frameStep(float dt) { - // ---- variables ----- + if (@projectEntry == null) + { + // check if the actor is already a project + BeamClass@ actor = game.getCurrentTruck(); + if (@actor != null) + { + CacheEntryClass@ entry = modcache.findEntryByFilename(LOADER_TYPE_ALLBEAM, /*partial:*/false, actor.getTruckFileName()); + if (@entry != null) + { + @projectEntry = @entry; + loadDocument(); + } + } + } + checkForCreatedProject(); + drawWindow(); +} -GenericDocumentClass@ g_displayed_document = null; -int m_hovered_token_pos = -1; -int m_focused_token_pos = -1; -string m_error_str; -string m_project_creation_pending; // name of the truck file which will become available in modcache. -CacheEntryClass@ m_project_entry; -// ---- functions ---- void drawWindow() { - + string caption = "RigEditor"; - if (@m_project_entry != null) + if (@projectEntry != null) { - caption += " ("+m_project_entry.fname+")"; + caption += " ("+projectEntry.fname+")"; } int flags = ImGuiWindowFlags_MenuBar; if (ImGui::Begin(caption, closeBtnHandler.windowOpen, flags)) @@ -33,149 +52,154 @@ void drawWindow() closeBtnHandler.draw(); if (ImGui::BeginMenuBar()) { - this.drawDocumentControls(); // <- menubar + drawDocumentControls(); // <- menubar ImGui::EndMenuBar(); } - if (@g_displayed_document != null) + if (@displayedDocument != null) { vector2 size = ImGui::GetWindowSize() - vector2(25, 200); ImGui::BeginChild("docBody", size); - this.drawDocumentBody(); + drawDocumentBody(); ImGui::EndChild(); - - if ( m_focused_token_pos > -1) + + if ( focusedTokenPos > -1) { ImGui::Separator(); - this.drawTokenEditPanel(); + drawTokenEditPanel(); } } - + ImGui::End(); } - + } - void drawTokenEditPanel() - { - GenericDocContextClass reader(g_displayed_document); - while (!reader.endOfFile() && reader.getPos() != uint(m_focused_token_pos)) reader.moveNext(); - if (reader.endOfFile()) +void drawTokenEditPanel() +{ + GenericDocContextClass reader(displayedDocument); + while (!reader.endOfFile() && reader.getPos() != uint(focusedTokenPos)) reader.moveNext(); + if (reader.endOfFile()) { - ImGui::Text("EOF!!"); - } + ImGui::Text("EOF!!"); + } else { - ImGui::TextDisabled("Token pos: "); ImGui::SameLine(); ImGui::Text("" + reader.getPos()); - ImGui::TextDisabled("Token type: "); ImGui::SameLine(); ImGui::Text(tokenTypeStr(reader.tokenType())); - } + ImGui::TextDisabled("Token pos: "); ImGui::SameLine(); ImGui::Text("" + reader.getPos()); + ImGui::TextDisabled("Token type: "); ImGui::SameLine(); ImGui::Text(tokenTypeStr(reader.tokenType())); } +} - string tokenTypeStr(TokenType t) - { - switch (t) - { +string tokenTypeStr(TokenType t) +{ + switch (t) + { case TOKEN_TYPE_NUMBER: return "Number"; 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 "?"; } + return "?"; +} - void createProjectFromActorAsync(BeamClass@ actor ) - { - // Fetch the current actor cache entry - CacheEntryClass@ src_entry = modcache.findEntryByFilename(LOADER_TYPE_ALLBEAM, /*partial:*/false, actor.getTruckFileName()); - if (@src_entry == null) - m_error_str = "Failed to load cache entry!"; - - // request project to be created from that cache entry - string proj_name = "project1"; - game.pushMessage(MSG_EDI_CREATE_PROJECT_REQUESTED, { - {'name', proj_name}, - {'source_entry', src_entry} - }); +void createProjectFromActorAsync(BeamClass@ actor ) +{ + // Fetch the current actor cache entry + CacheEntryClass@ src_entry = modcache.findEntryByFilename(LOADER_TYPE_ALLBEAM, /*partial:*/false, actor.getTruckFileName()); + if (@src_entry == null) + errorString = "Failed to load cache entry!"; + + // request project to be created from that cache entry + string proj_name = "project1"; + game.pushMessage(MSG_EDI_CREATE_PROJECT_REQUESTED, { + {'name', proj_name}, + {'source_entry', src_entry} + }); + + // Now we have to wait until next frame for the project to be created + nameOfProjectPendingCreation = proj_name + "." + src_entry.fext; // there's no notification event yet, we must poll. +} - // Now we have to wait until next frame for the project to be created - m_project_creation_pending = proj_name + "." + src_entry.fext; // there's no notification event yet, we must poll. +void loadDocument() +{ + GenericDocumentClass@ doc = GenericDocumentClass(); + int flags = GENERIC_DOCUMENT_OPTION_ALLOW_NAKED_STRINGS + | GENERIC_DOCUMENT_OPTION_ALLOW_SLASH_COMMENTS + | GENERIC_DOCUMENT_OPTION_FIRST_LINE_IS_TITLE + | GENERIC_DOCUMENT_OPTION_ALLOW_SEPARATOR_COLON + | GENERIC_DOCUMENT_OPTION_PARENTHESES_CAPTURE_SPACES; + if (!doc.loadFromResource(projectEntry.fname, projectEntry.resource_group, flags)) + { + errorString = "Project file failed to load!"; + return; } + @displayedDocument = doc; +} - void loadAndFixupDocument() - { - GenericDocumentClass@ doc = GenericDocumentClass(); - int flags = GENERIC_DOCUMENT_OPTION_ALLOW_NAKED_STRINGS - | GENERIC_DOCUMENT_OPTION_ALLOW_SLASH_COMMENTS - | GENERIC_DOCUMENT_OPTION_FIRST_LINE_IS_TITLE - | GENERIC_DOCUMENT_OPTION_ALLOW_SEPARATOR_COLON - | GENERIC_DOCUMENT_OPTION_PARENTHESES_CAPTURE_SPACES; - if (!doc.loadFromResource(m_project_entry.fname, m_project_entry.resource_group, flags)) - { - m_error_str = "Project file failed to load!"; - return; - } - @g_displayed_document = doc; - - // fixup the document.. - - GenericDocContextClass@ ctx = GenericDocContextClass(g_displayed_document); - // >> seek the name - while (!ctx.isTokString()) { - ctx.seekNextLine(); - } - // >> change the name - if (!ctx.endOfFile()) { - ctx.setTokString(0, m_project_entry.dname); - } - // >> seek fileinfo - while (!ctx.isTokKeyword() || ctx.getTokKeyword() != 'fileinfo') { - ctx.seekNextLine(); - } - // change the fileinfo param #2 categoryid - if (!ctx.endOfFile(2)) { - ctx.setTokFloat(2, 8990); // special "Project" category - } - } +void loadAndFixupDocument() +{ + loadDocument(); + + // fixup the document.. + + GenericDocContextClass@ ctx = GenericDocContextClass(displayedDocument); + // >> seek the name + while (!ctx.isTokString()) { + ctx.seekNextLine(); + } + // >> change the name + if (!ctx.endOfFile()) { + ctx.setTokString(0, projectEntry.dname); + } + // >> seek fileinfo + while (!ctx.isTokKeyword() || ctx.getTokKeyword() != 'fileinfo') { + ctx.seekNextLine(); + } + // change the fileinfo param #2 categoryid + if (!ctx.endOfFile(2)) { + ctx.setTokFloat(2, 8990); // special "Project" category + } +} - void checkForCreatedProject() +void checkForCreatedProject() +{ + // there's no notification event for completed request, we must poll like this. + if (nameOfProjectPendingCreation != "") { - // there's no notification event for completed request, we must poll like this. - if (m_project_creation_pending != "") - { - @m_project_entry = modcache.findEntryByFilename(LOADER_TYPE_ALLBEAM, /*partial:*/false, m_project_creation_pending); - if (@m_project_entry != null) - { - // success!! stop polling and open the document. - m_project_creation_pending=""; - this.loadAndFixupDocument(); - } - } - } + @projectEntry = modcache.findEntryByFilename(LOADER_TYPE_ALLBEAM, /*partial:*/false, nameOfProjectPendingCreation); + if (@projectEntry != null) + { + // success!! stop polling and open the document. + nameOfProjectPendingCreation=""; + loadAndFixupDocument(); + } + } +} void drawDocumentControls() { - if (m_error_str != "") - { - ImGui::Text("ERROR! " + m_error_str); - return; + if (errorString != "") + { + ImGui::Text("ERROR! " + errorString); + return; } - else if (@g_displayed_document != null) - { - ImGui::TextDisabled("Click tokens with mouse to edit. Spawn as usual (use category 'Projects')"); - ImGui::SameLine(); - if (ImGui::SmallButton("Save file")) - { - g_displayed_document.saveToResource(m_project_entry.fname, m_project_entry.resource_group); - } + else if (@displayedDocument != null) + { + ImGui::TextDisabled("Click tokens with mouse to edit. Spawn as usual (use category 'Projects')"); + ImGui::SameLine(); + if (ImGui::SmallButton("Save file")) + { + displayedDocument.saveToResource(projectEntry.fname, projectEntry.resource_group); + } } - else if (m_project_creation_pending != "") - { - ImGui::Text ("Waiting for project to be created..."); + else if (nameOfProjectPendingCreation != "") + { + ImGui::Text ("Waiting for project to be created..."); } - else - { - BeamClass@ actor = game.getCurrentTruck(); + else + { + BeamClass@ actor = game.getCurrentTruck(); if (@actor != null) { // Actor name and "View document" button @@ -183,136 +207,126 @@ void drawDocumentControls() ImGui::AlignTextToFramePadding(); ImGui::Text("You are driving " + actor.getTruckName()); ImGui::SameLine(); - if (@g_displayed_document == null) + if (@displayedDocument == null) { if (ImGui::SmallButton("Import as project")) { - this.createProjectFromActorAsync(actor); + createProjectFromActorAsync(actor); } } - + ImGui::PopID(); //"actor" } else { ImGui::Text("You are on foot. Spawn a vehicle to access it's definition file."); - } - } + } + } } void drawDocumentBody() { ImGui::PushID("docBody"); - bool hover_found = false; - - GenericDocContextClass reader(g_displayed_document); + 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: + // These tokens are always at start of line + case TOKEN_TYPE_KEYWORD: ImGui::TextColored(tokenColor(reader), reader.getTokKeyword()); break; - case TOKEN_TYPE_COMMENT: + case TOKEN_TYPE_COMMENT: ImGui::TextColored(tokenColor(reader), ";" + reader.getTokComment()); break; - // Linebreak is implicit in DearIMGUI, no action needed - case TOKEN_TYPE_LINEBREAK: + // 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. + // ImGui::SameLine(); ImGui::Text(""); // hack to fix highlight of last token on line. break; - - // Other tokens come anywhere - delimiting logic is needed - default: + + // 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(); + // string delimiter = (reader.tokenType(-1) == TOKEN_TYPE_KEYWORD) ? " " : ", "; + // ImGui::Text(delimiter); + // ImGui::SameLine(); } - + switch (reader.tokenType()) { - case TOKEN_TYPE_STRING: + case TOKEN_TYPE_STRING: ImGui::TextColored(tokenColor(reader), "\"" + reader.getTokString() + "\""); break; - case TOKEN_TYPE_NUMBER: + case TOKEN_TYPE_NUMBER: ImGui::TextColored(tokenColor(reader), "" + reader.getTokFloat()); break; - case TOKEN_TYPE_BOOL: + case TOKEN_TYPE_BOOL: ImGui::TextColored(tokenColor(reader), ""+reader.getTokBool()); break; } } - - if (ImGui::IsItemHovered()) { - m_hovered_token_pos = reader.getPos() ; - hover_found = true; - } - - if (ImGui::IsItemClicked(0)) - { - m_focused_token_pos = reader.getPos(); - } + + if (ImGui::IsItemHovered()) { + hoveredTokenPos = reader.getPos() ; + hover_found = true; + } + + if (ImGui::IsItemClicked(0)) + { + focusedTokenPos = reader.getPos(); + } reader.moveNext(); - + } - + if (!hover_found) - { - m_hovered_token_pos = -1; + { + hoveredTokenPos = -1; } ImGui::PopID(); // "docBody" } - color tokenColor(GenericDocContextClass@ reader) - { - if (m_focused_token_pos > -1 && reader.getPos() == uint(m_focused_token_pos)) +color tokenColor(GenericDocContextClass@ reader) +{ + if (focusedTokenPos > -1 && reader.getPos() == uint(focusedTokenPos)) { return color(0.9f, 0.1f, 0.1f, 1.f); - } - - if (m_hovered_token_pos > -1 && reader.getPos() == uint(m_hovered_token_pos)) + } + + 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_NUMBER: return color(0.9, 0.9, 0.9, 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); +} // end tokenColor() + - 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_NUMBER: return color(0.9, 0.9, 0.9, 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); - } // end tokenColor() - void update(float dt) - { - this.checkForCreatedProject(); - this.drawWindow(); - } -} -RigEditor rigEditor; -// `frameStep()` runs every frame; `dt` is delta time in seconds. -void frameStep(float dt) -{ - rigEditor.update(dt); -} diff --git a/resources/scripts/example_RigEditor_alpha.as b/resources/scripts/example_RigEditor_alpha.as index 8bed9f2ca6..a8e9debd89 100644 --- a/resources/scripts/example_RigEditor_alpha.as +++ b/resources/scripts/example_RigEditor_alpha.as @@ -1,545 +1,67 @@ -/// \file Prototype truck editor, Oct 2023 -/// \brief Showcases truck file editing features -/// see https://github.com/RigsOfRods/rigs-of-rods/pull/3048 -/// Written and auto-indented using script_editor.as! -// =================================================== +/// \file Prototype truck editor, Oct 2023 +/// \brief Showcases truck file editing features +/// see https://github.com/RigsOfRods/rigs-of-rods/pull/3048 +/// Written and auto-indented using script_editor.as! +// =================================================== + +#include "imgui_utils.as" +#include "gridviewer_utils.as" -#include "imgui_utils.as" - -class RigEditor -{ - // ----- config ----- - - int m_statusbar_height_pixels = 25; - - // ---- variables ----- - - GenericDocumentClass@ m_displayed_document = null; - int m_hovered_token_pos = -1; - int m_focused_token_pos = -1; - string m_error_str; - string m_project_creation_pending; // name of the truck file which will become available in modcache. - CacheEntryClass@ m_project_entry; - GridViewer viewer_x; - GridViewer viewer_y; - GridViewer viewer_z; - dictionary@ m_modcache_results = null; - CacheEntryClass@ m_awaiting_load_bundle_entry = null; - color node_color = color(0.8, 0.9, 0.2, 1.0); - float node_radius = 1.f; - imgui_utils::CloseWindowPrompt closeBtnHandler; // Window [X] button handler - - // ---- functions ---- - - //#region Draw surrounding window - - void drawWindow() - { - - // Begin window - - string caption = "RigEditor"; - if (@m_project_entry != null) - { - caption += " ("+m_project_entry.fname+")"; - } - int flags = ImGuiWindowFlags_MenuBar; - ImGui::Begin(caption, closeBtnHandler.windowOpen, flags); - closeBtnHandler.draw(); - - // Draw menu bar - - this.drawMenubar(); - - // Split off top-pane (all editing windows) from bottom statusbar - vector2 fullsize_with_statusbar = ImGui::GetWindowContentRegionMax() - ImGui::GetWindowContentRegionMin(); - vector2 fullsize = fullsize_with_statusbar - vector2(0, m_statusbar_height_pixels); - if (ImGui::BeginChild("mainPane", fullsize)) - { - - // Draw the document pane (left side) - - - vector2 leftpane_size = fullsize * vector2(0.3, 1.f); - - if (ImGui::BeginChild("leftPane", leftpane_size)) - { - - if (ImGui::BeginChild("docBody", leftpane_size * vector2(1.f, 0.8))) - { - if (@m_displayed_document != null) - { - this.drawDocumentBody(); - } - } - ImGui::EndChild(); // "docBody" - - if ( m_focused_token_pos > -1) - { - ImGui::Separator(); - this.drawTokenEditPanel(); - } - } - ImGui::EndChild(); // "leftPane" - ImGui::SameLine(); - // Draw the right side - vector2 rightpane_size = fullsize - vector2(leftpane_size.x, 0); - if (ImGui::BeginChild("rightPane", rightpane_size)) - { - - vector2 views_size = fullsize * vector2(0.7, 1.f); - vector2 qsize = views_size*vector2(0.5, 0.5); - - viewer_x.begin(qsize); - drawNodes(viewer_x); - viewer_x.end(); - - ImGui::SameLine(); - - viewer_y.begin(qsize); - drawNodes(viewer_y); - viewer_y.end(); - - viewer_z.begin(qsize); - drawNodes(viewer_z); - viewer_z.end(); - } - ImGui::EndChild(); // "rightPane" - } - ImGui::EndChild(); // "mainPane" - - // draw statusbar - - this.drawStatusbar(); - - // End window - - ImGui::End(); - - } - - void drawMenubar() - { - if (ImGui::BeginMenuBar()) - { - if (ImGui::BeginMenu("Open project")) - { - if (@m_modcache_results == null || ImGui::Button("Refresh")) - { - @m_modcache_results = modcache.query({ - {'filter_type', LOADER_TYPE_ALLBEAM}, - {'filter_category_id', 8990} // CID_Projects - }); - } - - if (@m_modcache_results != null) - { - array result_entries = cast>(m_modcache_results['entries']); - for (uint i = 0; i < result_entries.length(); i++) - { - ImGui::PushID(i); - - ImGui::Bullet(); - ImGui::SameLine(); - ImGui::Text(result_entries[i].dname); - ImGui::SameLine(); - ImGui::TextDisabled("("+result_entries[i].fname+")"); - ImGui::SameLine(); - if (ImGui::SmallButton("Load")) - { - game.pushMessage(MSG_EDI_LOAD_BUNDLE_REQUESTED, { {'cache_entry', result_entries[i]} }); - // we will receive `MODCACHEACTIVITY_BUNDLE_LOADED` - see `eventCallbackEx()` at the end of file. - @m_awaiting_load_bundle_entry = result_entries[i]; - game.registerForEvent(SE_GENERIC_MODCACHE_ACTIVITY); - } - - ImGui::PopID(); // i - } - } - else - { - ImGui::Text("ModCache query FAILED!"); - } - - ImGui::EndMenu(); - } - - if (ImGui::BeginMenu("Manage projects")) - { - ImGui::TextDisabled("Create a project:"); - - BeamClass@ actor = game.getCurrentTruck(); - if (@actor != null) - { - // Actor name and "View document" button - ImGui::PushID("actor"); - ImGui::AlignTextToFramePadding(); - ImGui::Text("You are driving " + actor.getTruckName()); - ImGui::SameLine(); - if (@m_displayed_document == null) - { - if (ImGui::SmallButton("Import as project")) - { - this.createProjectFromActorAsync(actor); - } - } - - ImGui::PopID(); //"actor" - } - else - { - ImGui::Text("You are on foot. Spawn a vehicle to access it's definition file."); - } - - ImGui::EndMenu(); - } - - - ImGui::EndMenuBar(); - } - } - - void drawStatusbar() - { - if (m_error_str != "") - { - ImGui::Text("ERROR! " + m_error_str); - if (ImGui::SmallButton("Clear")) - { - m_error_str = ""; - } - return; - } - else if (@m_displayed_document != null) - { - ImGui::TextDisabled("Click tokens with mouse to edit. Spawn as usual (use category 'Projects')"); - ImGui::SameLine(); - if (ImGui::SmallButton("Save file")) - { - m_displayed_document.saveToResource(m_project_entry.fname, m_project_entry.resource_group); - } - } - else if (m_project_creation_pending != "") - { - ImGui::Text ("Waiting for project to be created..."); - } - else if (@m_awaiting_load_bundle_entry != null) - { - ImGui::Text ("Loading bundle '"+m_awaiting_load_bundle_entry.dname+"'..."); - } - else - { - ImGui::Text("Ready to load or import project"); - } - } - //#endregion - - // #region Project management - - void createProjectFromActorAsync(BeamClass@ actor ) - { - // Fetch the current actor cache entry - CacheEntryClass@ src_entry = modcache.findEntryByFilename(LOADER_TYPE_ALLBEAM, /*partial:*/false, actor.getTruckFileName()); - if (@src_entry == null) - m_error_str = "Failed to load cache entry!"; - - // request project to be created from that cache entry - string proj_name = "project1"; - game.pushMessage(MSG_EDI_CREATE_PROJECT_REQUESTED, { - {'name', proj_name}, - {'source_entry', src_entry} - }); - - // Now we have to wait until next frame for the project to be created - m_project_creation_pending = proj_name + "." + src_entry.fext; // there's no notification event yet, we must poll. - } - - void loadAndFixupDocument() - { - GenericDocumentClass@ doc = GenericDocumentClass(); - int flags = GENERIC_DOCUMENT_OPTION_ALLOW_NAKED_STRINGS - | GENERIC_DOCUMENT_OPTION_ALLOW_SLASH_COMMENTS - | GENERIC_DOCUMENT_OPTION_FIRST_LINE_IS_TITLE - | GENERIC_DOCUMENT_OPTION_ALLOW_SEPARATOR_COLON - | GENERIC_DOCUMENT_OPTION_PARENTHESES_CAPTURE_SPACES; - if (!doc.loadFromResource(m_project_entry.fname, m_project_entry.resource_group, flags)) - { - m_error_str = "Project file failed to load!"; - return; - } - @m_displayed_document = doc; - - // fixup the document.. - - GenericDocContextClass@ ctx = GenericDocContextClass(m_displayed_document); - // >> seek the name - while (!ctx.endOfFile() && !ctx.isTokString()) { - ctx.seekNextLine(); - } - // >> change the name - if (!ctx.endOfFile()) { - ctx.setTokString(0, m_project_entry.dname); - } - // >> seek fileinfo - while (!ctx.endOfFile() && (!ctx.isTokKeyword() || ctx.getTokKeyword() != 'fileinfo')) { - ctx.seekNextLine(); - } - // change the fileinfo param #2 categoryid - if (!ctx.endOfFile(2)) { - ctx.setTokFloat(2, 8990); // special "Project" category - } - } - - // #endregion - - // #region Tokenized document drawing - - void drawDocumentBody() - { - ImGui::PushID("docBody"); - bool hover_found = false; - - GenericDocContextClass reader(m_displayed_document); - 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_NUMBER: { - ImGui::TextColored(tokenColor(reader), "" + reader.getTokFloat()); - break; - } - case TOKEN_TYPE_BOOL: { - ImGui::TextColored(tokenColor(reader), ""+reader.getTokBool()); - break; - } - } - } - - if (ImGui::IsItemHovered()) { - m_hovered_token_pos = reader.getPos() ; - hover_found = true; - } - - if (ImGui::IsItemClicked(0)) - { - m_focused_token_pos = reader.getPos(); - } - - reader.moveNext(); - - } - - if (!hover_found) - { - m_hovered_token_pos = -1; - } - - ImGui::PopID(); // "docBody" - } - - void drawTokenEditPanel() - { - GenericDocContextClass reader(m_displayed_document); - while (!reader.endOfFile() && (reader.getPos() != uint(m_focused_token_pos))) - { - reader.moveNext(); - } - - if (reader.endOfFile()) - { - ImGui::Text("EOF!!"); - } - else - { - ImGui::TextDisabled("Token pos: "); ImGui::SameLine(); ImGui::Text("" + reader.getPos()); - ImGui::TextDisabled("Token type: "); ImGui::SameLine(); ImGui::Text(tokenTypeStr(reader.tokenType())); - } - } - - // #endregion - - // #region GridViewer node drawing - - void drawNodes(GridViewer @viewer) - { - // This must run in between `GridViewer.begin()` and `GridViewer.end()` ! - // ---------------------------------------------------------------------- - - if (@m_displayed_document == null) - { - return; - } - - // Do it the slow way, reading directly from the GenericDocumentClass without any sort of caching. - GenericDocContextClass ctx(m_displayed_document); - - // Seek to 'nodes' (assume there's just 1 'nodes' segment in the file - usual case) - while (!ctx.endOfFile() && (!ctx.isTokKeyword() || ctx.getTokKeyword() != "nodes")) - { - ctx.seekNextLine(); - } - ctx.seekNextLine(); - while (!ctx.endOfFile()) - { - //int node_num = int(ctx.getTokFloat(0)); - vector3 node_pos; - if (ctx.isTokFloat(1) && ctx.isTokFloat(2) && ctx.isTokFloat(3)) - { - node_pos.x = ctx.getTokFloat(1); - node_pos.y = ctx.getTokFloat(2); - node_pos.z = ctx.getTokFloat(3); - ImGui::GetWindowDrawList().AddCircleFilled( viewer.localToScreenPos(node_pos), node_radius, node_color); - } - - ctx.seekNextLine(); - - - if (ctx.isTokKeyword() && ctx.getTokKeyword() != "set_node_defaults") - { - break; - } - } - } - - // #endregion - - // #region Token drawing helpers - string tokenTypeStr(TokenType t) - { - switch (t) - { - case TOKEN_TYPE_NUMBER: return "Number"; - 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 "?"; - } - - color tokenColor(GenericDocContextClass@ reader) - { - if (m_focused_token_pos > -1 && reader.getPos() == uint(m_focused_token_pos)) - { - return color(0.9f, 0.1f, 0.1f, 1.f); - } - - if (m_hovered_token_pos > -1 && reader.getPos() == uint(m_hovered_token_pos)) - { - 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_NUMBER: return color(0.9, 0.9, 0.9, 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); - } - return color(0.9, 0.9, 0.9, 1.f); - } - // #endregion - - // #region Event handling or polling - void checkForCreatedProject() - { - // there's no notification event for completed request, we must poll like this. - if (m_project_creation_pending != "") - { - @m_project_entry = modcache.findEntryByFilename(LOADER_TYPE_ALLBEAM, /*partial:*/false, m_project_creation_pending); - if (@m_project_entry != null) - { - // success!! stop polling and open the document. - m_project_creation_pending=""; - this.loadAndFixupDocument(); - } - } - } - - void onEventBundleLoaded(int cache_number) - { - //game.log("DBG onEventBundleLoaded(): number "+cache_number); - if (cache_number == m_awaiting_load_bundle_entry.number) - { - @m_project_entry = @m_awaiting_load_bundle_entry; - @m_awaiting_load_bundle_entry= null; - - game.unRegisterEvent(SE_GENERIC_MODCACHE_ACTIVITY); - this.loadAndFixupDocument(); - } - } - // #endregion - - void update(float dt) - { - this.checkForCreatedProject(); - this.drawWindow(); - } -} - -RigEditor editor; +// ----- config ----- + +int m_statusbar_height_pixels = 25; + +// ---- variables ----- + +GenericDocumentClass@ m_displayed_document = null; +int m_hovered_token_pos = -1; +int m_focused_token_pos = -1; +string m_error_str; +string m_project_creation_pending; // name of the truck file which will become available in modcache. +CacheEntryClass@ m_project_entry; +gridviewer_utils::GridViewer viewer_x; +gridviewer_utils::GridViewer viewer_y; +gridviewer_utils:: GridViewer viewer_z; +dictionary@ m_modcache_results = null; +CacheEntryClass@ m_awaiting_load_bundle_entry = null; +color node_color = color(0.8, 0.9, 0.2, 1.0); +float node_radius = 1.f; +imgui_utils::CloseWindowPrompt closeBtnHandler; // Window [X] button handler + +// ---- functions ---- // STARTUP void main() { - editor.viewer_x.childWindowID = "demoX"; - editor.viewer_x.hAxis=1; - editor.viewer_x.vAxis=2; // axes: YZ + viewer_x.childWindowID = "demoX"; + viewer_x.hAxis=1; + viewer_x.vAxis=2; // axes: YZ - editor.viewer_y.childWindowID = "demoY"; - editor.viewer_y.hAxis=0; - editor.viewer_y.vAxis=2; // axes: XZ + viewer_y.childWindowID = "demoY"; + viewer_y.hAxis=0; + viewer_y.vAxis=2; // axes: XZ - editor.viewer_z.childWindowID = "demoZ"; // axes XY (default) + viewer_z.childWindowID = "demoZ"; // axes XY (default) } // RENDERING void frameStep(float dt) { - editor.update(dt); + // check if the actor is already a project + BeamClass@ actor = game.getCurrentTruck(); + if (@actor != null) + { + CacheEntryClass@ entry = modcache.findEntryByFilename(LOADER_TYPE_ALLBEAM, /*partial:*/false, actor.getTruckFileName()); + if (@entry != null) + { + @m_project_entry = @entry; + loadDocument(); + } + } + + checkForCreatedProject(); + drawWindow(); } // NOTIFICATIONS @@ -547,6 +69,506 @@ void eventCallbackEx(scriptEvents ev, int a1, int a2, int a3, int a4, string a5, { if (ev == SE_GENERIC_MODCACHE_ACTIVITY && a1 == MODCACHEACTIVITY_BUNDLE_LOADED) { - editor.onEventBundleLoaded(a2); + onEventBundleLoaded(a2); } } + + +//#region Draw surrounding window + +void drawWindow() +{ + + // Begin window + + string caption = "RigEditor"; + if (@m_project_entry != null) + { + caption += " ("+m_project_entry.fname+")"; + } + int flags = ImGuiWindowFlags_MenuBar; + ImGui::Begin(caption, closeBtnHandler.windowOpen, flags); + closeBtnHandler.draw(); + + // Draw menu bar + + drawMenubar(); + + // Split off top-pane (all editing windows) from bottom statusbar + vector2 fullsize_with_statusbar = ImGui::GetWindowContentRegionMax() - ImGui::GetWindowContentRegionMin(); + vector2 fullsize = fullsize_with_statusbar - vector2(0, m_statusbar_height_pixels); + if (ImGui::BeginChild("mainPane", fullsize)) + { + + // Draw the document pane (left side) + + + vector2 leftpane_size = fullsize * vector2(0.3, 1.f); + + if (ImGui::BeginChild("leftPane", leftpane_size)) + { + + if (ImGui::BeginChild("docBody", leftpane_size * vector2(1.f, 0.8))) + { + if (@m_displayed_document != null) + { + drawDocumentBody(); + } + } + ImGui::EndChild(); // "docBody" + + if ( m_focused_token_pos > -1) + { + ImGui::Separator(); + drawTokenEditPanel(); + } + } + ImGui::EndChild(); // "leftPane" + ImGui::SameLine(); + // Draw the right side + vector2 rightpane_size = fullsize - vector2(leftpane_size.x, 0); + if (ImGui::BeginChild("rightPane", rightpane_size)) + { + + vector2 views_size = fullsize * vector2(0.7, 1.f); + vector2 qsize = views_size*vector2(0.5, 0.5); + + viewer_x.begin(qsize); + drawNodes(viewer_x); + viewer_x.end(); + + ImGui::SameLine(); + + viewer_y.begin(qsize); + drawNodes(viewer_y); + viewer_y.end(); + + viewer_z.begin(qsize); + drawNodes(viewer_z); + viewer_z.end(); + } + ImGui::EndChild(); // "rightPane" + } + ImGui::EndChild(); // "mainPane" + + // draw statusbar + + drawStatusbar(); + + // End window + + ImGui::End(); + +} + +void drawMenubar() +{ + if (ImGui::BeginMenuBar()) + { + if (ImGui::BeginMenu("Open project")) + { + if (@m_modcache_results == null || ImGui::Button("Refresh")) + { + @m_modcache_results = modcache.query({ + {'filter_type', LOADER_TYPE_ALLBEAM}, + {'filter_category_id', 8990} // CID_Projects + }); + } + + if (@m_modcache_results != null) + { + array result_entries = cast>(m_modcache_results['entries']); + for (uint i = 0; i < result_entries.length(); i++) + { + ImGui::PushID(i); + + ImGui::Bullet(); + ImGui::SameLine(); + ImGui::Text(result_entries[i].dname); + ImGui::SameLine(); + ImGui::TextDisabled("("+result_entries[i].fname+")"); + ImGui::SameLine(); + if (ImGui::SmallButton("Load")) + { + game.pushMessage(MSG_EDI_LOAD_BUNDLE_REQUESTED, { {'cache_entry', result_entries[i]} }); + // we will receive `MODCACHEACTIVITY_BUNDLE_LOADED` - see `eventCallbackEx()` at the end of file. + @m_awaiting_load_bundle_entry = result_entries[i]; + game.registerForEvent(SE_GENERIC_MODCACHE_ACTIVITY); + } + + ImGui::PopID(); // i + } + } + else + { + ImGui::Text("ModCache query FAILED!"); + } + + ImGui::EndMenu(); + } + + if (ImGui::BeginMenu("Manage projects")) + { + ImGui::TextDisabled("Create a project:"); + + BeamClass@ actor = game.getCurrentTruck(); + if (@actor != null) + { + // Actor name and "View document" button + ImGui::PushID("actor"); + ImGui::AlignTextToFramePadding(); + ImGui::Text("You are driving " + actor.getTruckName()); + ImGui::SameLine(); + if (@m_displayed_document == null) + { + if (ImGui::SmallButton("Import as project")) + { + createProjectFromActorAsync(actor); + } + } + + ImGui::PopID(); //"actor" + } + else + { + ImGui::Text("You are on foot. Spawn a vehicle to access it's definition file."); + } + + ImGui::EndMenu(); + } + + + ImGui::EndMenuBar(); + } +} + +void drawStatusbar() +{ + if (m_error_str != "") + { + ImGui::Text("ERROR! " + m_error_str); + if (ImGui::SmallButton("Clear")) + { + m_error_str = ""; + } + return; + } + else if (@m_displayed_document != null) + { + ImGui::TextDisabled("Click tokens with mouse to edit. Spawn as usual (use category 'Projects')"); + ImGui::SameLine(); + if (ImGui::SmallButton("Save file")) + { + m_displayed_document.saveToResource(m_project_entry.fname, m_project_entry.resource_group); + } + } + else if (m_project_creation_pending != "") + { + ImGui::Text ("Waiting for project to be created..."); + } + else if (@m_awaiting_load_bundle_entry != null) + { + ImGui::Text ("Loading bundle '"+m_awaiting_load_bundle_entry.dname+"'..."); + } + else + { + ImGui::Text("Ready to load or import project"); + } +} +//#endregion + +// #region Project management + +void createProjectFromActorAsync(BeamClass@ actor ) +{ + // Fetch the current actor cache entry + CacheEntryClass@ src_entry = modcache.findEntryByFilename(LOADER_TYPE_ALLBEAM, /*partial:*/false, actor.getTruckFileName()); + if (@src_entry == null) + m_error_str = "Failed to load cache entry!"; + + // request project to be created from that cache entry + string proj_name = "project1"; + game.pushMessage(MSG_EDI_CREATE_PROJECT_REQUESTED, { + {'name', proj_name}, + {'source_entry', src_entry} + }); + + // Now we have to wait until next frame for the project to be created + m_project_creation_pending = proj_name + "." + src_entry.fext; // there's no notification event yet, we must poll. +} + +void loadDocument() +{ + GenericDocumentClass@ doc = GenericDocumentClass(); + int flags = GENERIC_DOCUMENT_OPTION_ALLOW_NAKED_STRINGS + | GENERIC_DOCUMENT_OPTION_ALLOW_SLASH_COMMENTS + | GENERIC_DOCUMENT_OPTION_FIRST_LINE_IS_TITLE + | GENERIC_DOCUMENT_OPTION_ALLOW_SEPARATOR_COLON + | GENERIC_DOCUMENT_OPTION_PARENTHESES_CAPTURE_SPACES; + if (!doc.loadFromResource(m_project_entry.fname, m_project_entry.resource_group, flags)) + { + m_error_str = "Project file failed to load!"; + return; + } + @m_displayed_document = doc; +} + +void loadAndFixupDocument() +{ + loadDocument(); + GenericDocumentClass@ doc = GenericDocumentClass(); + int flags = GENERIC_DOCUMENT_OPTION_ALLOW_NAKED_STRINGS + | GENERIC_DOCUMENT_OPTION_ALLOW_SLASH_COMMENTS + | GENERIC_DOCUMENT_OPTION_FIRST_LINE_IS_TITLE + | GENERIC_DOCUMENT_OPTION_ALLOW_SEPARATOR_COLON + | GENERIC_DOCUMENT_OPTION_PARENTHESES_CAPTURE_SPACES; + if (!doc.loadFromResource(m_project_entry.fname, m_project_entry.resource_group, flags)) + { + m_error_str = "Project file failed to load!"; + return; + } + @m_displayed_document = doc; + + // fixup the document.. + + GenericDocContextClass@ ctx = GenericDocContextClass(m_displayed_document); + // >> seek the name + while (!ctx.endOfFile() && !ctx.isTokString()) { + ctx.seekNextLine(); + } + // >> change the name + if (!ctx.endOfFile()) { + ctx.setTokString(0, m_project_entry.dname); + } + // >> seek fileinfo + while (!ctx.endOfFile() && (!ctx.isTokKeyword() || ctx.getTokKeyword() != 'fileinfo')) { + ctx.seekNextLine(); + } + // change the fileinfo param #2 categoryid + if (!ctx.endOfFile(2)) { + ctx.setTokFloat(2, 8990); // special "Project" category + } +} + +// #endregion + +// #region Tokenized document drawing + +void drawDocumentBody() +{ + ImGui::PushID("docBody"); + bool hover_found = false; + + GenericDocContextClass reader(m_displayed_document); + 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_NUMBER: { + ImGui::TextColored(tokenColor(reader), "" + reader.getTokFloat()); + break; + } + case TOKEN_TYPE_BOOL: { + ImGui::TextColored(tokenColor(reader), ""+reader.getTokBool()); + break; + } + } + } + + if (ImGui::IsItemHovered()) { + m_hovered_token_pos = reader.getPos() ; + hover_found = true; + } + + if (ImGui::IsItemClicked(0)) + { + m_focused_token_pos = reader.getPos(); + } + + reader.moveNext(); + + } + + if (!hover_found) + { + m_hovered_token_pos = -1; + } + + ImGui::PopID(); // "docBody" +} + +void drawTokenEditPanel() +{ + GenericDocContextClass reader(m_displayed_document); + while (!reader.endOfFile() && (reader.getPos() != uint(m_focused_token_pos))) + { + reader.moveNext(); + } + + if (reader.endOfFile()) + { + ImGui::Text("EOF!!"); + } + else + { + ImGui::TextDisabled("Token pos: "); ImGui::SameLine(); ImGui::Text("" + reader.getPos()); + ImGui::TextDisabled("Token type: "); ImGui::SameLine(); ImGui::Text(tokenTypeStr(reader.tokenType())); + } +} + +// #endregion + +// #region GridViewer node drawing + +void drawNodes(gridviewer_utils::GridViewer @viewer) +{ + // This must run in between `GridViewer.begin()` and `GridViewer.end()` ! + // ---------------------------------------------------------------------- + + if (@m_displayed_document == null) + { + return; + } + + // Do it the slow way, reading directly from the GenericDocumentClass without any sort of caching. + GenericDocContextClass ctx(m_displayed_document); + + // Seek to 'nodes' (assume there's just 1 'nodes' segment in the file - usual case) + while (!ctx.endOfFile() && (!ctx.isTokKeyword() || ctx.getTokKeyword() != "nodes")) + { + ctx.seekNextLine(); + } + ctx.seekNextLine(); + while (!ctx.endOfFile()) + { + //int node_num = int(ctx.getTokFloat(0)); + vector3 node_pos; + if (ctx.isTokFloat(1) && ctx.isTokFloat(2) && ctx.isTokFloat(3)) + { + node_pos.x = ctx.getTokFloat(1); + node_pos.y = ctx.getTokFloat(2); + node_pos.z = ctx.getTokFloat(3); + ImGui::GetWindowDrawList().AddCircleFilled( viewer.localToScreenPos(node_pos), node_radius, node_color); + } + + ctx.seekNextLine(); + + + if (ctx.isTokKeyword() && ctx.getTokKeyword() != "set_node_defaults") + { + break; + } + } +} + +// #endregion + +// #region Token drawing helpers +string tokenTypeStr(TokenType t) +{ + switch (t) + { + case TOKEN_TYPE_NUMBER: return "Number"; + 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 "?"; +} + +color tokenColor(GenericDocContextClass@ reader) +{ + if (m_focused_token_pos > -1 && reader.getPos() == uint(m_focused_token_pos)) + { + return color(0.9f, 0.1f, 0.1f, 1.f); + } + + if (m_hovered_token_pos > -1 && reader.getPos() == uint(m_hovered_token_pos)) + { + 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_NUMBER: return color(0.9, 0.9, 0.9, 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); + } + return color(0.9, 0.9, 0.9, 1.f); +} +// #endregion + +// #region Event handling or polling +void checkForCreatedProject() +{ + // there's no notification event for completed request, we must poll like this. + if (m_project_creation_pending != "") + { + @m_project_entry = modcache.findEntryByFilename(LOADER_TYPE_ALLBEAM, /*partial:*/false, m_project_creation_pending); + if (@m_project_entry != null) + { + // success!! stop polling and open the document. + m_project_creation_pending=""; + loadAndFixupDocument(); + } + } +} + +void onEventBundleLoaded(int cache_number) +{ + //game.log("DBG onEventBundleLoaded(): number "+cache_number); + if (cache_number == m_awaiting_load_bundle_entry.number) + { + @m_project_entry = @m_awaiting_load_bundle_entry; + @m_awaiting_load_bundle_entry= null; + + game.unRegisterEvent(SE_GENERIC_MODCACHE_ACTIVITY); + loadAndFixupDocument(); + } +} +// #endregion + diff --git a/resources/scripts/example_ogre_inspector.as b/resources/scripts/example_ogre_inspector.as index 838a7efe8c..e05e0a0616 100644 --- a/resources/scripts/example_ogre_inspector.as +++ b/resources/scripts/example_ogre_inspector.as @@ -25,7 +25,7 @@ class OgreInspector ImGui::Text("Note that all element names are just simple strings, even if they contain '/'."); ImGui::Text("For details see OGRE docs:"); ImGui::SameLine(); - ImHyperlink("https://ogrecave.github.io/ogre/api/latest/"); + imgui_utils::ImHyperlink("https://ogrecave.github.io/ogre/api/latest/"); ImGui::Separator(); Ogre::Root@ root = Ogre::Root::getSingleton(); diff --git a/resources/scripts/example_ogre_vertexData.as b/resources/scripts/example_ogre_vertexData.as index 1bff50ad2f..4d998b0cc5 100644 --- a/resources/scripts/example_ogre_vertexData.as +++ b/resources/scripts/example_ogre_vertexData.as @@ -8,8 +8,8 @@ imgui_utils::CloseWindowPrompt closeBtnHandler; #include "gridviewer_utils.as" -GridViewer gTexcoordsViewer; -GridViewer gVertsViewer; +gridviewer_utils::GridViewer gTexcoordsViewer; +gridviewer_utils::GridViewer gVertsViewer; Ogre::MeshPtr gMesh; Ogre::TexturePtr gTex; array@ gVertPositions = null; @@ -29,7 +29,37 @@ bool gShowRawTexcoords=false; float fmax(float a, float b) { return a>b?a:b; } float fmin(float a, float b) { return a 0) + if (ImGui::BeginTabItem("Roads")) { - ProceduralObjectClass@ obj = roads.getObject(m_selected_road); - ImGui::Text("Road: " + obj.getName()); - ImGui::Text("Total points: " + obj.getNumPoints()); - if (obj.getNumPoints() > 0) - { - ImGui::TextDisabled("Use slider or left-click road point to select it."); - ImGui::TextDisabled("Press hotkey '" + inputs.getEventCommandTrimmed(EV_ROAD_EDITOR_POINT_SET_POS) + "' to move the selected point to mouse position"); - int slider_val = m_selected_point; - if (ImGui::SliderInt("", slider_val, 0, obj.getNumPoints() - 1)) - { - this.setSelectedPoint(slider_val); - } - if (inputs.getEventBoolValueBounce(EV_ROAD_EDITOR_POINT_GOTO) - || ImGui::Button("Go to point (hotkey: '" + inputs.getEventCommandTrimmed(EV_ROAD_EDITOR_POINT_GOTO) + "')")) - { - this.goToPoint(obj, m_selected_point); - } - ImGui::SameLine(); - ImGui::Checkbox("Auto", m_selected_point_auto_goto); - } - - ImGui::TextDisabled("Add new road point at the character position and link it to selected road point (if any)."); - if (inputs.getEventBoolValueBounce(EV_ROAD_EDITOR_POINT_INSERT) - || ImGui::Button("Insert new point (hotkey: '" + inputs.getEventCommandTrimmed(EV_ROAD_EDITOR_POINT_INSERT) + "')")) + if (ImGui::CollapsingHeader("Road list")) { - this.addPointToCurrentRoad(game.getPersonPosition()); - } - - if (obj.getNumPoints() > 0) - { - if (inputs.getEventBoolValueBounce(EV_ROAD_EDITOR_POINT_DELETE) - || ImGui::Button("Remove selected point (hotkey: '" + inputs.getEventCommandTrimmed(EV_ROAD_EDITOR_POINT_DELETE) + "')")) - { - this.deletePointFromCurrentRoad(m_selected_point); - } + drawRoadListPanel(); } + ImGui::Separator(); + drawRoadEditPanel(); - if (ImGui::CollapsingHeader("Point properties")) - { - this.drawPointPropertiesPanel(obj); - } + ImGui::EndTabItem(); + } + + if (ImGui::BeginTabItem("AI Waypoints")) + { + drawAiWaypointsPanel(); - if (ImGui::CollapsingHeader("Mesh rebuild")) - { - this.drawMeshRebuildPanel(obj); - } + ImGui::EndTabItem(); } - ImGui::PopID(); // "road edit box" - } + + ImGui::EndTabBar(); + } + + // End window + ImGui::End(); +} + +void drawRoadEditPanel() +{ + ImGui::PushID("road edit box"); - void drawPointPropertiesPanel(ProceduralObjectClass@ obj) + TerrainClass@ terrain = game.getTerrain(); + ProceduralManagerClass@ roads = terrain.getProceduralManager(); + if (roads.getNumObjects() > 0) { - ImGui::PushID("point properties"); + ProceduralObjectClass@ obj = roads.getObject(selected_road); + ImGui::Text("Road: " + obj.getName()); + ImGui::Text("Total points: " + obj.getNumPoints()); if (obj.getNumPoints() > 0) { - ProceduralPointClass@ point = obj.getPoint(m_selected_point); - - ImGui::SetNextItemWidth(130.f); - ImGui::InputFloat("Elevation (meters)", point.position.y); - - ImGui::SetNextItemWidth(100.f); - ImGui::InputFloat("Width (meters)", point.width); - - ImGui::SetNextItemWidth(100.f); - ImGui::InputFloat("Border width (meters)", point.border_width); - - ImGui::SetNextItemWidth(100.f); - ImGui::InputFloat("Border height (meters)", point.border_height); - - ImGui::Text("Type:"); - // Types supported by TOBJ format - if (ImGui::RadioButton("(automatic)", point.type == ROAD_AUTOMATIC)) + ImGui::TextDisabled("Use slider or left-click road point to select it."); + ImGui::TextDisabled("Press hotkey '" + inputs.getEventCommandTrimmed(EV_ROAD_EDITOR_POINT_SET_POS) + "' to move the selected point to mouse position"); + int slider_val = selected_point; + if (ImGui::SliderInt("", slider_val, 0, obj.getNumPoints() - 1)) { - point.type = ROAD_AUTOMATIC; - point.pillar_type = 0; + setSelectedPoint(slider_val); } - if (ImGui::RadioButton("flat (no border)", point.type == ROAD_FLAT)) - { - point.type = ROAD_FLAT; - point.pillar_type = 0; - } - if (ImGui::RadioButton("left (border on left)", point.type == ROAD_LEFT)) - { - point.type = ROAD_LEFT; - point.pillar_type = 0; - } - if (ImGui::RadioButton("right (border on right)", point.type == ROAD_RIGHT)) - { - point.type = ROAD_RIGHT; - point.pillar_type = 0; - } - if (ImGui::RadioButton("both (with borders)", point.type == ROAD_BOTH)) - { - point.type = ROAD_BOTH; - point.pillar_type = 0; - } - if (ImGui::RadioButton("bridge (with pillars)", point.type == ROAD_BRIDGE && point.pillar_type == 1)) + if (inputs.getEventBoolValueBounce(EV_ROAD_EDITOR_POINT_GOTO) + || ImGui::Button("Go to point (hotkey: '" + inputs.getEventCommandTrimmed(EV_ROAD_EDITOR_POINT_GOTO) + "')")) { - point.type = ROAD_BRIDGE; - point.pillar_type = 1; + goToPoint(obj, selected_point); } - if (ImGui::RadioButton("bridge_no_pillars", point.type == ROAD_BRIDGE && point.pillar_type == 0)) - { - point.type = ROAD_BRIDGE; - point.pillar_type = 0; - } - if (ImGui::RadioButton("monorail (with pillars)", point.type == ROAD_BRIDGE && point.pillar_type == 2)) + ImGui::SameLine(); + ImGui::Checkbox("Auto", selected_point_auto_goto); + } + + ImGui::TextDisabled("Add new road point at the character position and link it to selected road point (if any)."); + if (inputs.getEventBoolValueBounce(EV_ROAD_EDITOR_POINT_INSERT) + || ImGui::Button("Insert new point (hotkey: '" + inputs.getEventCommandTrimmed(EV_ROAD_EDITOR_POINT_INSERT) + "')")) + { + addPointToCurrentRoad(game.getPersonPosition()); + } + + if (obj.getNumPoints() > 0) + { + if (inputs.getEventBoolValueBounce(EV_ROAD_EDITOR_POINT_DELETE) + || ImGui::Button("Remove selected point (hotkey: '" + inputs.getEventCommandTrimmed(EV_ROAD_EDITOR_POINT_DELETE) + "')")) { - point.type = ROAD_MONORAIL; - point.pillar_type = 2; + deletePointFromCurrentRoad(selected_point); } - if (ImGui::RadioButton("monorail2 (no pillars)", point.type == ROAD_BRIDGE && point.pillar_type == 0)) - { - point.type = ROAD_MONORAIL; - point.pillar_type = 0; - } - // End of types supported by TOBJ format } - else + + if (ImGui::CollapsingHeader("Point properties")) { - ImGui::Text("Road has no points - nothing to edit!"); - } + drawPointPropertiesPanel(obj); + } - ImGui::PopID(); //"point properties" + if (ImGui::CollapsingHeader("Mesh rebuild")) + { + drawMeshRebuildPanel(obj); + } } - - void drawMeshRebuildPanel(ProceduralObjectClass@ obj) + ImGui::PopID(); // "road edit box" +} + +void drawPointPropertiesPanel(ProceduralObjectClass@ obj) +{ + ImGui::PushID("point properties"); + if (obj.getNumPoints() > 0) { - ImGui::PushID("mesh rebuild"); + ProceduralPointClass@ point = obj.getPoint(selected_point); - ProceduralManagerClass@ roads = game.getTerrain().getHandle().getProceduralManager(); - - if (obj.getNumPoints() > 0) - { - ImGui::SetNextItemWidth(100.f); - ImGui::InputFloat("Common minimum point elevation (meters)", m_global_min_point_elevation); + ImGui::SetNextItemWidth(130.f); + ImGui::InputFloat("Elevation (meters)", point.position.y); - ImGui::SetNextItemWidth(100.f); - ImGui::InputFloat("Common extra point elevation (meters)", m_global_extra_point_elevation); + ImGui::SetNextItemWidth(100.f); + ImGui::InputFloat("Width (meters)", point.width); - ImGui::SetNextItemWidth(120.f); - ImGui::InputInt("Num smoothing splits (0=off)", obj.smoothing_num_splits); + ImGui::SetNextItemWidth(100.f); + ImGui::InputFloat("Border width (meters)", point.border_width); - // NOTE: hotkey is processed in `updateInputEvents()` to be independent of the UI - if (ImGui::Button("Rebuild road mesh (hotkey: '" + inputs.getEventCommandTrimmed(EV_ROAD_EDITOR_REBUILD_MESH) + "')")) - { - this.rebuildMesh(obj); - } + ImGui::SetNextItemWidth(100.f); + ImGui::InputFloat("Border height (meters)", point.border_height); + + ImGui::Text("Type:"); + // Types supported by TOBJ format + if (ImGui::RadioButton("(automatic)", point.type == ROAD_AUTOMATIC)) + { + point.type = ROAD_AUTOMATIC; + point.pillar_type = 0; } - else + if (ImGui::RadioButton("flat (no border)", point.type == ROAD_FLAT)) + { + point.type = ROAD_FLAT; + point.pillar_type = 0; + } + if (ImGui::RadioButton("left (border on left)", point.type == ROAD_LEFT)) + { + point.type = ROAD_LEFT; + point.pillar_type = 0; + } + if (ImGui::RadioButton("right (border on right)", point.type == ROAD_RIGHT)) + { + point.type = ROAD_RIGHT; + point.pillar_type = 0; + } + if (ImGui::RadioButton("both (with borders)", point.type == ROAD_BOTH)) + { + point.type = ROAD_BOTH; + point.pillar_type = 0; + } + if (ImGui::RadioButton("bridge (with pillars)", point.type == ROAD_BRIDGE && point.pillar_type == 1)) + { + point.type = ROAD_BRIDGE; + point.pillar_type = 1; + } + if (ImGui::RadioButton("bridge_no_pillars", point.type == ROAD_BRIDGE && point.pillar_type == 0)) { - ImGui::Text("Road has no points - nothing to generate!"); + point.type = ROAD_BRIDGE; + point.pillar_type = 0; } - - ImGui::PopID(); //"mesh rebuild" - } - - void recalculatePointElevations(ProceduralObjectClass@ obj) - { - for (int i = 0; i < obj.getNumPoints(); i++) + if (ImGui::RadioButton("monorail (with pillars)", point.type == ROAD_BRIDGE && point.pillar_type == 2)) { - ProceduralPointClass@ point = obj.getPoint(i); - point.position.y = m_global_extra_point_elevation + fmax(point.position.y, game.getGroundHeight(point.position) + m_global_min_point_elevation); + point.type = ROAD_MONORAIL; + point.pillar_type = 2; } + if (ImGui::RadioButton("monorail2 (no pillars)", point.type == ROAD_BRIDGE && point.pillar_type == 0)) + { + point.type = ROAD_MONORAIL; + point.pillar_type = 0; + } + // End of types supported by TOBJ format } + else + { + ImGui::Text("Road has no points - nothing to edit!"); + } - void recalculatePointRotations(ProceduralObjectClass@ obj) + ImGui::PopID(); //"point properties" +} + +void drawMeshRebuildPanel(ProceduralObjectClass@ obj) +{ + ImGui::PushID("mesh rebuild"); + + ProceduralManagerClass@ roads = game.getTerrain().getHandle().getProceduralManager(); + + if (obj.getNumPoints() > 0) { - // Loop all segments; for each segment, calculate the end-point orientation from the segment direction. Special case is point #0. - //------------------------------------------------ - float MIN_HEIGHT_ABOVE_GROUND = 0.3f; - for (int i = 1; i < obj.getNumPoints(); i++) + ImGui::SetNextItemWidth(100.f); + ImGui::InputFloat("Common minimum point elevation (meters)", global_min_point_elevation); + + ImGui::SetNextItemWidth(100.f); + ImGui::InputFloat("Common extra point elevation (meters)", global_extra_point_elevation); + + ImGui::SetNextItemWidth(120.f); + ImGui::InputInt("Num smoothing splits (0=off)", obj.smoothing_num_splits); + + // NOTE: hotkey is processed in `updateInputEvents()` to be independent of the UI + if (ImGui::Button("Rebuild road mesh (hotkey: '" + inputs.getEventCommandTrimmed(EV_ROAD_EDITOR_REBUILD_MESH) + "')")) { - ProceduralPointClass@ point = obj.getPoint(i); - ProceduralPointClass@ prev_point = obj.getPoint(i - 1); - - // Calc angle - vector3 unitvec_road = (point.position - prev_point.position); - unitvec_road.y = 0.f; - unitvec_road.normalise(); - radian angle = radian(0.f); - - // -- the math part - vector3 unitvec_x = vector3(1.f, 0.f, 0.f); - angle = unitvec_x.angleBetween(unitvec_road); - - // -- the trial and error part - maybe there's something wrong with our procedural mesh generation - if (unitvec_road.x < 0 && unitvec_road.z > 0) - { - angle = angle * radian(-1.f); - } - else if (unitvec_road.x > 0 && unitvec_road.z > 0) - { - angle = angle * radian(-1.f); - } - - - // Calc rotation - quaternion rot_x = quaternion(0.f, vector3(1.f, 0.f, 0.f)); - quaternion rot_y = quaternion(angle, vector3(0.f, 1.f, 0.f)); - quaternion rot_z = quaternion(0.f, vector3(0.f, 0.f, 1.f)); - point.rotation = rot_x * rot_y * rot_z; - - // Special case - point #0 - if (i == 1) - { - prev_point.rotation = point.rotation; - } - } + rebuildMesh(obj); + } + } + else + { + ImGui::Text("Road has no points - nothing to generate!"); } - void drawRoadListPanel() + ImGui::PopID(); //"mesh rebuild" +} + +void recalculatePointElevations(ProceduralObjectClass@ obj) +{ + for (int i = 0; i < obj.getNumPoints(); i++) { - // Basic info - num procedural objects and [+] button - TerrainClass@ terrain = game.getTerrain(); - ProceduralManagerClass@ roads = terrain.getProceduralManager(); - ImGui::Text("There are " + roads.getNumObjects() + " road strips"); - ImGui::SameLine(); - if (ImGui::Button("Add new")) + ProceduralPointClass@ point = obj.getPoint(i); + point.position.y = global_extra_point_elevation + fmax(point.position.y, game.getGroundHeight(point.position) + global_min_point_elevation); + } +} + +void recalculatePointRotations(ProceduralObjectClass@ obj) +{ + // Loop all segments; for each segment, calculate the end-point orientation from the segment direction. Special case is point #0. + //------------------------------------------------ + float MIN_HEIGHT_ABOVE_GROUND = 0.3f; + for (int i = 1; i < obj.getNumPoints(); i++) + { + ProceduralPointClass@ point = obj.getPoint(i); + ProceduralPointClass@ prev_point = obj.getPoint(i - 1); + + // Calc angle + vector3 unitvec_road = (point.position - prev_point.position); + unitvec_road.y = 0.f; + unitvec_road.normalise(); + radian angle = radian(0.f); + + // -- the math part + vector3 unitvec_x = vector3(1.f, 0.f, 0.f); + angle = unitvec_x.angleBetween(unitvec_road); + + // -- the trial and error part - maybe there's something wrong with our procedural mesh generation + if (unitvec_road.x < 0 && unitvec_road.z > 0) { - // Add new road - ProceduralObjectClass obj; - obj.setName("road " + roads.getNumObjects() + " (from editor)"); - roads.addObject(obj); - // Select the new road - this.setSelectedRoad(roads.getNumObjects() - 1); + angle = angle * radian(-1.f); } - ImGui::Separator(); - - // Radio select of road to edit. - ProceduralObjectClass@ obj_to_delete = null; - for (int i = 0; i < roads.getNumObjects(); i++) + else if (unitvec_road.x > 0 && unitvec_road.z > 0) { - ProceduralObjectClass@ obj = roads.getObject(i); - ImGui::PushID(i); - if (ImGui::RadioButton(obj.getName(), i == m_selected_road)) - { - this.setSelectedRoad(i); - } - ImGui::SameLine(); - if (ImGui::Button("Delete")) - { - @obj_to_delete = @obj; - } - ImGui::PopID(); + angle = angle * radian(-1.f); } - if (@obj_to_delete != null) + + // Calc rotation + quaternion rot_x = quaternion(0.f, vector3(1.f, 0.f, 0.f)); + quaternion rot_y = quaternion(angle, vector3(0.f, 1.f, 0.f)); + quaternion rot_z = quaternion(0.f, vector3(0.f, 0.f, 1.f)); + point.rotation = rot_x * rot_y * rot_z; + + // Special case - point #0 + if (i == 1) { - roads.removeObject(obj_to_delete); - m_selected_road = min(m_selected_road, roads.getNumObjects() - 1); + prev_point.rotation = point.rotation; } - ImGui::Separator(); + } +} + +void drawRoadListPanel() +{ + // Basic info - num procedural objects and [+] button + TerrainClass@ terrain = game.getTerrain(); + ProceduralManagerClass@ roads = terrain.getProceduralManager(); + ImGui::TextDisabled("There are " + roads.getNumObjects() + " road strips"); + ImGui::SameLine(); + if (ImGui::SmallButton("Add new road")) + { + // Add new road + ProceduralObjectClass obj; + obj.setName("road " + roads.getNumObjects() + " (from editor)"); + roads.addObject(obj); + // Select the new road + setSelectedRoad(roads.getNumObjects() - 1); } - void drawAiWaypointsExportPanel(ProceduralObjectClass@ obj) + // Radio select of road to edit. + ProceduralObjectClass@ obj_to_delete = null; + for (int i = 0; i < roads.getNumObjects(); i++) { - ImGui::PushID("ai export"); - ImGui::Text("Export road points as waypoints to survey map"); - - if (obj.getNumPoints() > 0) + ProceduralObjectClass@ obj = roads.getObject(i); + ImGui::PushID(i); + if (ImGui::RadioButton(obj.getName(), i == selected_road)) { - ImGui::Text("Current road has " + obj.getNumPoints() + " points"); - ImGui::SliderInt("First", m_ai_export_first, 0, obj.getNumPoints() - 1); - ImGui::SliderInt("Last", m_ai_export_last, 0, obj.getNumPoints() - 1); - if (m_ai_export_first > m_ai_export_last) - { - ImGui::Text("bad range!"); - } - else + setSelectedRoad(i); + } + ImGui::SameLine(); + if (ImGui::Button("Delete")) + { + @obj_to_delete = @obj; + } + ImGui::PopID(); + } + + if (@obj_to_delete != null) + { + roads.removeObject(obj_to_delete); + selected_road = min(selected_road, roads.getNumObjects() - 1); + } +} + +void drawAiWaypointsExportPanel(ProceduralObjectClass@ obj) +{ + ImGui::PushID("ai export"); + ImGui::Text("Export road points as waypoints to survey map"); + + if (obj.getNumPoints() > 0) + { + ImGui::Text("Current road has " + obj.getNumPoints() + " points"); + ImGui::SliderInt("First", ai_export_first, 0, obj.getNumPoints() - 1); + ImGui::SliderInt("Last", ai_export_last, 0, obj.getNumPoints() - 1); + if (ai_export_first > ai_export_last) + { + ImGui::Text("bad range!"); + } + else + { + string btn_text = "Export " + ((ai_export_last - ai_export_first) + 1) + " waypoints"; + if (ImGui::Button(btn_text)) { - string btn_text = "Export " + ((m_ai_export_last - m_ai_export_first) + 1) + " waypoints"; - if (ImGui::Button(btn_text)) + if (ai_export_reverse) { - if (m_ai_export_reverse) + for (int i = ai_export_last; i >= ai_export_first; i--) { - for (int i = m_ai_export_last; i >= m_ai_export_first; i--) - { - game.addWaypoint(obj.getPoint(i).getHandle().position); - } + game.addWaypoint(obj.getPoint(i).getHandle().position); } - else + } + else + { + for (int i = ai_export_first; i <= ai_export_last; i++) { - for (int i = m_ai_export_first; i <= m_ai_export_last; i++) - { - game.addWaypoint(obj.getPoint(i).getHandle().position); - } + game.addWaypoint(obj.getPoint(i).getHandle().position); } } - ImGui::SameLine(); - ImGui::Checkbox("Reverse", m_ai_export_reverse); } + ImGui::SameLine(); + ImGui::Checkbox("Reverse", ai_export_reverse); } - else - { - ImGui::Text("The current road is empty - nothing to export!"); - } - - ImGui::PopID(); // "ai export" } + else + { + ImGui::Text("The current road is empty - nothing to export!"); + } + + ImGui::PopID(); // "ai export" +} + +void drawAiWaypointsImportPanel(ProceduralObjectClass@ obj) +{ + ImGui::PushID("ai import"); + + ImGui::Text("Import road points from AI waypoints in survey map"); + array waypoints = game.getWaypoints(0); - void drawAiWaypointsImportPanel(ProceduralObjectClass@ obj) + // Resize range sliders if needed + if (ai_import_available != int(waypoints.length())) { - ImGui::PushID("ai import"); - - ImGui::Text("Import road points from AI waypoints in survey map"); - array waypoints = game.getWaypoints(0); + ai_import_first = min(ai_import_first, int(waypoints.length()) - 1); + ai_import_last = min(ai_import_last, int(waypoints.length()) - 1); + } + + // Draw export UI + if (waypoints.length() > 0) + { + ImGui::Text("There are " + waypoints.length() + " waypoints available"); - // Resize range sliders if needed - if (m_ai_import_available != int(waypoints.length())) + ImGui::SliderInt("First", ai_import_first, 0, int(waypoints.length()) - 1); + ImGui::SliderInt("Last", ai_import_last, 0, int(waypoints.length()) - 1); + if (ai_import_first > ai_import_last) { - m_ai_import_first = min(m_ai_import_first, int(waypoints.length()) - 1); - m_ai_import_last = min(m_ai_import_last, int(waypoints.length()) - 1); + ImGui::Text("bad range!"); } - - // Draw export UI - if (waypoints.length() > 0) + else { - ImGui::Text("There are " + waypoints.length() + " waypoints available"); - - ImGui::SliderInt("First", m_ai_import_first, 0, int(waypoints.length()) - 1); - ImGui::SliderInt("Last", m_ai_import_last, 0, int(waypoints.length()) - 1); - if (m_ai_import_first > m_ai_import_last) - { - ImGui::Text("bad range!"); - } - else + string btn_text = "Import " + ((ai_import_last - ai_import_first) + 1) + " waypoints"; + if (ImGui::Button(btn_text)) { - string btn_text = "Import " + ((m_ai_import_last - m_ai_import_first) + 1) + " waypoints"; - if (ImGui::Button(btn_text)) + if (ai_import_reverse) { - if (m_ai_import_reverse) + for (int i = ai_import_last; i >= ai_import_first; i--) { - for (int i = m_ai_import_last; i >= m_ai_import_first; i--) - { - this.addPointToCurrentRoad(waypoints[i]); - } + addPointToCurrentRoad(waypoints[i]); } - else + } + else + { + for (int i = ai_import_first; i <= ai_import_last; i++) { - for (int i = m_ai_import_first; i <= m_ai_import_last; i++) - { - this.addPointToCurrentRoad(waypoints[i]); - } + addPointToCurrentRoad(waypoints[i]); } } - ImGui::SameLine(); - ImGui::Checkbox("Reverse", m_ai_import_reverse); } + ImGui::SameLine(); + ImGui::Checkbox("Reverse", ai_import_reverse); } - else - { - ImGui::Text("No waypoints defined in survey map - nothing to import!"); - } - - ImGui::PopID(); // "ai import" } + else + { + ImGui::Text("No waypoints defined in survey map - nothing to import!"); + } - void drawAiWaypointsPanel() + ImGui::PopID(); // "ai import" +} + +void drawAiWaypointsPanel() +{ + ImGui::PushID("waypoints panel"); + ImGui::TextDisabled("This tool lets you convert between\nprocedural road points and AI waypoints\n(editable using survey map)."); + TerrainClass@ terrain = game.getTerrain(); + ProceduralManagerClass@ roads = terrain.getProceduralManager(); + if (roads.getNumObjects() > 0) { - ImGui::PushID("waypoints panel"); - ImGui::TextDisabled("This tool lets you convert between procedural road points and AI waypoints (editable using survey map)."); - TerrainClass@ terrain = game.getTerrain(); - ProceduralManagerClass@ roads = terrain.getProceduralManager(); - if (roads.getNumObjects() > 0) - { - ProceduralObjectClass@ obj = roads.getObject(m_selected_road); - ImGui::Separator(); - this.drawAiWaypointsExportPanel(obj); - ImGui::Separator(); - this.drawAiWaypointsImportPanel(obj); - } - else - { - ImGui::Text("There are no roads - nothing to import/export!"); - } - - ImGui::PopID(); // "waypoints panel" + ProceduralObjectClass@ obj = roads.getObject(selected_road); + ImGui::Separator(); + drawAiWaypointsExportPanel(obj); + ImGui::Separator(); + drawAiWaypointsImportPanel(obj); } + else + { + ImGui::Text("There are no roads - nothing to import/export!"); + } + + ImGui::PopID(); // "waypoints panel" +} - void visualizeRoad() +void visualizeRoad() +{ + TerrainClass@ terrain = game.getTerrain(); + ProceduralManagerClass@ roads = terrain.getProceduralManager(); + ProceduralObjectClass@ obj = roads.getObject(selected_road); + ImDrawList@ drawlist = imgui_utils::ImGetDummyFullscreenWindow("Road editing gizmos"); + vector2 mouse_pos = game.getMouseScreenPosition(); + vector2 prev_screen_point(0,0); + bool prev_point_visible = false; + int closest_point = -1; + int closest_point_distance = INT_MAX; + + for (int i = 0; i < obj.getNumPoints(); i++) { - TerrainClass@ terrain = game.getTerrain(); - ProceduralManagerClass@ roads = terrain.getProceduralManager(); - ProceduralObjectClass@ obj = roads.getObject(m_selected_road); - ImDrawList@ drawlist = imgui_utils::ImGetDummyFullscreenWindow("Road editing gizmos"); - vector2 mouse_pos = game.getMouseScreenPosition(); - vector2 prev_screen_point(0,0); - bool prev_point_visible = false; - int closest_point = -1; - int closest_point_distance = INT_MAX; + // Draw this point + ProceduralPointClass@ pp = obj.getPoint(i); + vector3 point = pp.position; + vector2 screen_point; + bool point_visible = game.getScreenPosFromWorldPos(point, screen_point); - for (int i = 0; i < obj.getNumPoints(); i++) - { - // Draw this point - ProceduralPointClass@ pp = obj.getPoint(i); - vector3 point = pp.position; - vector2 screen_point; - bool point_visible = game.getScreenPosFromWorldPos(point, screen_point); - - if (point_visible) - { - // Draw point - if (i == m_selected_point) + if (point_visible) + { + // Draw point + if (i == selected_point) + { + drawlist.AddCircleFilled(screen_point, 7.f, color(0.9f, 0.5f, 0.2f, 1.f), 12); + } + else if (i == hovered_point) + { + drawlist.AddCircle(screen_point, 9.f, color(0.9f, 0.2f, 0.3f, 1.f), 12, 4.f); + } + else + { + drawlist.AddCircle(screen_point, 5.f, color(0.9f, 0.1f, 0.1f, 1.f), 12, 2.f); + } + + // Draw line from last point if visible + if (prev_point_visible) + { + drawlist.AddLine(prev_screen_point, screen_point, color(0.8f, 0.8f, 0.2f, 1.f), 3); + } + + // Draw elevation marker (square) + vector3 terrn_point = point; + terrn_point.y = game.getGroundHeight(terrn_point); + vector2 terrn_screen_point(0,0); + bool terrn_point_visible = game.getScreenPosFromWorldPos(terrn_point, terrn_screen_point); + if (terrn_point_visible) + { + + if (i == selected_point) { - drawlist.AddCircleFilled(screen_point, 7.f, color(0.9f, 0.5f, 0.2f, 1.f), 12); + const float PAD = 4.f; + drawlist.AddRectFilled( + vector2(terrn_screen_point.x-PAD, terrn_screen_point.y-PAD), + vector2(terrn_screen_point.x+PAD, terrn_screen_point.y+PAD), + color(0.1f, 0.2f, 0.9f, 1.f), 0.f, 0); } - else if (i == m_hovered_point) + else if (i == hovered_point) { - drawlist.AddCircle(screen_point, 9.f, color(0.9f, 0.2f, 0.3f, 1.f), 12, 4.f); + const float PAD = 6.f; + drawlist.AddRect( + vector2(terrn_screen_point.x-PAD, terrn_screen_point.y-PAD), + vector2(terrn_screen_point.x+PAD, terrn_screen_point.y+PAD), + color(0.2f, 0.3f, 0.9f, 1.f), 0.f, 0, 5.f); + if (ImGui::IsMouseClicked(0)) + { + setSelectedPoint(hovered_point); + } } else { - drawlist.AddCircle(screen_point, 5.f, color(0.9f, 0.1f, 0.1f, 1.f), 12, 2.f); - } - - // Draw line from last point if visible - if (prev_point_visible) - { - drawlist.AddLine(prev_screen_point, screen_point, color(0.8f, 0.8f, 0.2f, 1.f), 3); - } + const float PAD = 3.f; + drawlist.AddRect( + vector2(terrn_screen_point.x-PAD, terrn_screen_point.y-PAD), + vector2(terrn_screen_point.x+PAD, terrn_screen_point.y+PAD), + color(0.1f, 0.2f, 0.9f, 1.f), 0.f, 0, 2.f); + } - // Draw elevation marker (square) - vector3 terrn_point = point; - terrn_point.y = game.getGroundHeight(terrn_point); - vector2 terrn_screen_point(0,0); - bool terrn_point_visible = game.getScreenPosFromWorldPos(terrn_point, terrn_screen_point); - if (terrn_point_visible) - { - - if (i == m_selected_point) - { - const float PAD = 4.f; - drawlist.AddRectFilled( - vector2(terrn_screen_point.x-PAD, terrn_screen_point.y-PAD), - vector2(terrn_screen_point.x+PAD, terrn_screen_point.y+PAD), - color(0.1f, 0.2f, 0.9f, 1.f), 0.f, 0); - } - else if (i == m_hovered_point) - { - const float PAD = 6.f; - drawlist.AddRect( - vector2(terrn_screen_point.x-PAD, terrn_screen_point.y-PAD), - vector2(terrn_screen_point.x+PAD, terrn_screen_point.y+PAD), - color(0.2f, 0.3f, 0.9f, 1.f), 0.f, 0, 5.f); - if (ImGui::IsMouseClicked(0)) - { - this.setSelectedPoint(m_hovered_point); - } - } - else - { - const float PAD = 3.f; - drawlist.AddRect( - vector2(terrn_screen_point.x-PAD, terrn_screen_point.y-PAD), - vector2(terrn_screen_point.x+PAD, terrn_screen_point.y+PAD), - color(0.1f, 0.2f, 0.9f, 1.f), 0.f, 0, 2.f); - } - - // Draw elevation indicator line - drawlist.AddLine(screen_point, terrn_screen_point, color(0.2f, 0.3f, 0.7f, 1.f), 3.f); - - // Accumulate mouse hover info - int point_distance = getMouseShortestDistance(mouse_pos, terrn_screen_point); - if (point_distance < closest_point_distance) - { - closest_point = i; - closest_point_distance = point_distance; - } - } + // Draw elevation indicator line + drawlist.AddLine(screen_point, terrn_screen_point, color(0.2f, 0.3f, 0.7f, 1.f), 3.f); - if (i == m_selected_point) + // Accumulate mouse hover info + int point_distance = getMouseShortestDistance(mouse_pos, terrn_screen_point); + if (point_distance < closest_point_distance) { - m_selected_point_elevation = (point.y - terrn_point.y); - } - else if (i == m_hovered_point) - { - m_hovered_point_elevation = (point.y - terrn_point.y); - } + closest_point = i; + closest_point_distance = point_distance; + } } - // Move next - prev_point_visible = point_visible; - prev_screen_point = screen_point; + if (i == selected_point) + { + selected_point_elevation = (point.y - terrn_point.y); + } + else if (i == hovered_point) + { + hovered_point_elevation = (point.y - terrn_point.y); + } } - if (closest_point_distance < 15) - { - m_hovered_point = closest_point; - } - else - { - m_hovered_point = -1; - } + // Move next + prev_point_visible = point_visible; + prev_screen_point = screen_point; } - void updateInputEvents() + if (closest_point_distance < 15) { - TerrainClass@ terrain = game.getTerrain(); - ProceduralManagerClass@ roads = terrain.getProceduralManager(); - ProceduralObjectClass@ obj = roads.getObject(m_selected_road); - - vector3 mouse_tpos; - if (inputs.getEventBoolValue(EV_ROAD_EDITOR_POINT_SET_POS) - && game.getMousePositionOnTerrain(mouse_tpos)) - { - // Y is 'up'. - ProceduralPointClass@ pp = obj.getPoint(m_selected_point); - //vector3 old_pos = pp.position; - vector3 new_pos = mouse_tpos + vector3(0,m_selected_point_elevation,0); - //game.log("Updating road '"+obj.getName()+"', point '"+m_selected_point+"' position to X:"+new_pos.x+" Y:"+new_pos.y+" Z:"+new_pos.z+" (previously X:"+old_pos.x+" Y:"+old_pos.y+" Z:"+old_pos.z+")"); - pp.position = new_pos; - } - - if (inputs.getEventBoolValueBounce(EV_ROAD_EDITOR_REBUILD_MESH)) - { - this.rebuildMesh(obj); - } + hovered_point = closest_point; } - - void rebuildMesh(ProceduralObjectClass@ obj) + else { - ProceduralManagerClass@ roads = game.getTerrain().getHandle().getProceduralManager(); + hovered_point = -1; + } +} + +void updateInputEvents() +{ + TerrainClass@ terrain = game.getTerrain(); + ProceduralManagerClass@ roads = terrain.getProceduralManager(); + ProceduralObjectClass@ obj = roads.getObject(selected_road); - roads.removeObject(obj); // Clears the existing mesh - this.recalculatePointElevations(obj); - this.recalculatePointRotations(obj); - roads.addObject(obj); // Generates new mesh - // Because we removed and re-added the ProceduralObject from/to the manager, - // it got new index, so our selection is invalid. Update it. - this.setSelectedRoad(roads.getNumObjects() - 1); + vector3 mouse_tpos; + if (inputs.getEventBoolValue(EV_ROAD_EDITOR_POINT_SET_POS) + && game.getMousePositionOnTerrain(mouse_tpos)) + { + // Y is 'up'. + ProceduralPointClass@ pp = obj.getPoint(selected_point); + //vector3 old_pos = pp.position; + vector3 new_pos = mouse_tpos + vector3(0,selected_point_elevation,0); + //game.log("Updating road '"+obj.getName()+"', point '"+selected_point+"' position to X:"+new_pos.x+" Y:"+new_pos.y+" Z:"+new_pos.z+" (previously X:"+old_pos.x+" Y:"+old_pos.y+" Z:"+old_pos.z+")"); + pp.position = new_pos; } - void addPointToCurrentRoad(vector3 pos) + if (inputs.getEventBoolValueBounce(EV_ROAD_EDITOR_REBUILD_MESH)) { - ProceduralObjectClass@ obj = game.getTerrain().getHandle().getProceduralManager().getHandle().getObject(m_selected_road); + rebuildMesh(obj); + } +} + +void rebuildMesh(ProceduralObjectClass@ obj) +{ + ProceduralManagerClass@ roads = game.getTerrain().getHandle().getProceduralManager(); - ProceduralPointClass@ point = ProceduralPointClass(); - if (obj.getNumPoints() > 0) - { - ProceduralPointClass@ template = obj.getPoint(m_selected_point); - point.type = template.type; - point.pillar_type = template.pillar_type; - point.width = template.width; - point.border_width = template.border_width; - point.border_height = template.border_height; - } - point.position = pos; - - if (obj.getNumPoints() == 0 || m_selected_point == obj.getNumPoints() - 1) - { - obj.addPoint(point); - m_selected_point = obj.getNumPoints() - 1; - } - else - { - obj.insertPoint(m_selected_point, point); - } - } + roads.removeObject(obj); // Clears the existing mesh + recalculatePointElevations(obj); + recalculatePointRotations(obj); + roads.addObject(obj); // Generates new mesh + // Because we removed and re-added the ProceduralObject from/to the manager, + // it got new index, so our selection is invalid. Update it. + setSelectedRoad(roads.getNumObjects() - 1); +} - void deletePointFromCurrentRoad(int pos) +void addPointToCurrentRoad(vector3 pos) +{ + ProceduralObjectClass@ obj = game.getTerrain().getHandle().getProceduralManager().getHandle().getObject(selected_road); + + ProceduralPointClass@ point = ProceduralPointClass(); + if (obj.getNumPoints() > 0) { - ProceduralObjectClass@ obj = game.getTerrain().getHandle().getProceduralManager().getHandle().getObject(m_selected_road); - - obj.deletePoint(pos); - if (pos >= obj.getNumPoints()) - { - if (obj.getNumPoints() > 0) - this.setSelectedPoint(obj.getNumPoints() - 1); - else - m_selected_point = 0; - } + ProceduralPointClass@ template = obj.getPoint(selected_point); + point.type = template.type; + point.pillar_type = template.pillar_type; + point.width = template.width; + point.border_width = template.border_width; + point.border_height = template.border_height; } + point.position = pos; - void goToPoint(ProceduralObjectClass@ obj, int point_idx) + if (obj.getNumPoints() == 0 || selected_point == obj.getNumPoints() - 1) + { + obj.addPoint(point); + selected_point = obj.getNumPoints() - 1; + } + else { - ProceduralPointClass@ point = obj.getPoint(point_idx); - game.setPersonPosition(point.position); - game.setPersonRotation(point.rotation.getYaw()); // I have no idea if this is correct. + obj.insertPoint(selected_point, point); } +} + +void deletePointFromCurrentRoad(int pos) +{ + ProceduralObjectClass@ obj = game.getTerrain().getHandle().getProceduralManager().getHandle().getObject(selected_road); - void setSelectedPoint(int point) + obj.deletePoint(pos); + if (pos >= obj.getNumPoints()) { - if (point != m_selected_point) + if (obj.getNumPoints() > 0) + setSelectedPoint(obj.getNumPoints() - 1); + else + selected_point = 0; + } +} + +void goToPoint(ProceduralObjectClass@ obj, int point_idx) +{ + ProceduralPointClass@ point = obj.getPoint(point_idx); + game.setPersonPosition(point.position); + game.setPersonRotation(point.rotation.getYaw()); // I have no idea if this is correct. +} + +void setSelectedPoint(int point) +{ + if (point != selected_point) + { + selected_point = point; + if (selected_point_auto_goto) { - m_selected_point = point; - if (m_selected_point_auto_goto) - { - TerrainClass@ terrain = game.getTerrain(); - ProceduralManagerClass@ proc_mgr = terrain.getProceduralManager(); - ProceduralObjectClass@ proc_obj = proc_mgr.getObject(m_selected_road); - - this.goToPoint(proc_obj, m_selected_point); - } + TerrainClass@ terrain = game.getTerrain(); + ProceduralManagerClass@ proc_mgr = terrain.getProceduralManager(); + ProceduralObjectClass@ proc_obj = proc_mgr.getObject(selected_road); + + goToPoint(proc_obj, selected_point); } } - - void setSelectedRoad(int road) +} + +void setSelectedRoad(int road) +{ + if (road != selected_road) { - if (road != m_selected_road) - { - m_selected_road = road; - ProceduralObjectClass@ obj = game.getTerrain().getHandle().getProceduralManager().getHandle().getObject(m_selected_road); - // AI export panel - m_ai_export_first = min(m_ai_export_first, obj.getNumPoints() - 1); - m_ai_export_last = min(m_ai_export_last, obj.getNumPoints() - 1); - // Selected point - int new_selected_point = clamp(0, m_selected_point, obj.getNumPoints()); - if (m_selected_point != new_selected_point) - { - this.setSelectedPoint(new_selected_point); - } + selected_road = road; + ProceduralObjectClass@ obj = game.getTerrain().getHandle().getProceduralManager().getHandle().getObject(selected_road); + // AI export panel + ai_export_first = min(ai_export_first, obj.getNumPoints() - 1); + ai_export_last = min(ai_export_last, obj.getNumPoints() - 1); + // Selected point + int new_selected_point = clamp(0, selected_point, obj.getNumPoints()); + if (selected_point != new_selected_point) + { + setSelectedPoint(new_selected_point); } } - - int m_selected_road = 0; - int m_selected_point = 0; - int m_hovered_point = 0; - float m_selected_point_elevation = 0.f; - float m_hovered_point_elevation = 0.f; - bool m_selected_point_auto_goto = false; - - // Waypoints panel - int m_ai_import_first = 0; - int m_ai_import_last = 0; - int m_ai_import_available = 0; - bool m_ai_import_reverse = false; - int m_ai_export_first = 0; - int m_ai_export_last = 0; - bool m_ai_export_reverse = false; - - // Mesh generation panel - float m_global_extra_point_elevation = 0; // Added to all points - float m_global_min_point_elevation = 0.3; // Added to lower points } /* - --------------------------------------------------------------------------- - Helper functions +--------------------------------------------------------------------------- +Helper functions */ int clamp(int minval, int val, int maxval) diff --git a/resources/scripts/script_editor.as b/resources/scripts/script_editor.as index 40ddfd90ed..4e9c6ab776 100644 --- a/resources/scripts/script_editor.as +++ b/resources/scripts/script_editor.as @@ -202,7 +202,12 @@ class ScriptEditorWindow ScriptIndexerRecord recentScriptsRecord; ScriptIndexerRecord localScriptsRecord; ScriptIndexerRecord exampleScriptsRecord; - ScriptIndexerRecord includeScriptsRecord; + ScriptIndexerRecord includeScriptsRecord; + + ScriptEditorWindow() + { + closeBtnHandler.cfgPromptText = "Really close script editor? You will lose any unsaved work."; + } void refreshLocalFileList() { @@ -570,7 +575,7 @@ class ScriptEditorWindow for (uint i=0; i(fileinfos[index]['scriptInfo']); + return cast(fileinfos[index]['scriptInfo']); } return null; } @@ -2182,7 +2187,7 @@ class ScriptIndexerRecord { string filename = string(fileinfos[index]['filename']); string body = game.loadTextResourceAsString(filename, rgname); - ScriptInfo@scriptinfo = ExtractScriptInfo(body); + scriptinfo_utils::ScriptInfo@ scriptinfo = scriptinfo_utils::ExtractScriptInfo(body); /*game.log("DBG analyzeSingleScript("+index+"): File="+filename+'/RgName='+rgname +": title="+scriptinfo.title+", brief="+scriptinfo.brief+", text="+scriptinfo.text);*/ fileinfos[index]['scriptInfo'] = scriptinfo; @@ -2226,9 +2231,9 @@ RegionInfo@ findRegion(dictionary@ regionDict, string name) // Helper which chec const string TUT_SCRIPT = """ // TUTORIAL SCRIPT - Shows the basics, step by step: -// How to open UI window and handle [X] close button; -// how to store data and update/draw them every frame. -// (also showcases code folding with '#[end]region') +// * How to open UI window and handle [X] close button. +// * how to store data and update/draw them every frame. +// * How to do code folding with '#[end]region' // =================================================== // Window [X] button handler @@ -2250,7 +2255,7 @@ void frameStep(float dt) // Begin drawing window if (ImGui::Begin("Tutorial script", closeBtnHandler.windowOpen, 0)) { - // Draw the "Terminate this script?" prompt on the top. + // Draw the "Terminate this script?" prompt on the top (if not disabled by config). closeBtnHandler.draw(); // accumulate time @@ -2262,7 +2267,6 @@ void frameStep(float dt) //#endregion //#region render the output - // Note this will open an implicit window titled "Debug" ImGui::Text(ttStr); ImGui::SameLine(); ImGui::Text(dtStr); From 0409ebb28d3425ba151d926999efa488c0b4f45a Mon Sep 17 00:00:00 2001 From: ohlidalp Date: Thu, 14 Nov 2024 02:56:26 +0100 Subject: [PATCH 03/36] :angel:Script: restored `Entity::setMaterialName()` This reverts a line from caf88e5948a14d6960c35b0a444fa6a24e48fbed - I wanted to be purist and force users to do `engity.getSubEntities(0).setMaterial()` but since OGRE has this convenience method and the skeletal posing example already uses it, let's just roll with it. --- source/main/scripting/bindings/OgreAngelscript.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/source/main/scripting/bindings/OgreAngelscript.cpp b/source/main/scripting/bindings/OgreAngelscript.cpp index 239add29d0..668c7a2cd7 100644 --- a/source/main/scripting/bindings/OgreAngelscript.cpp +++ b/source/main/scripting/bindings/OgreAngelscript.cpp @@ -1548,6 +1548,7 @@ void registerOgreEntity(AngelScript::asIScriptEngine* engine) int r; r = engine->SetDefaultNamespace("Ogre"); ROR_ASSERT(r >= 0); + r = engine->RegisterObjectMethod("Entity", "void setMaterialName(const string &in name, const string &in rg = \"OgreAutodetect\")", asMETHOD(Entity, setMaterialName), asCALL_THISCALL); ROR_ASSERT(r >= 0); r = engine->RegisterObjectMethod("Entity", "AnimationState @getAnimationState(const string &in) const", asMETHOD(Entity, getAnimationState), asCALL_THISCALL); ROR_ASSERT(r >= 0); r = engine->RegisterObjectMethod("Entity", "AnimationStateSet @getAllAnimationStates()", asMETHOD(Entity, getAllAnimationStates), asCALL_THISCALL); ROR_ASSERT(r >= 0); r = engine->RegisterObjectMethod("Entity", "void setDisplaySkeleton(bool)", asMETHOD(Entity, setDisplaySkeleton), asCALL_THISCALL); ROR_ASSERT(r >= 0); From d38c6d27383337b5c40ffc56686ca3299f3aaf68 Mon Sep 17 00:00:00 2001 From: ohlidalp Date: Fri, 22 Nov 2024 04:14:14 +0100 Subject: [PATCH 04/36] :angel:Script: added `game.getMousePointedMovableObjects()` Returns `array` in no particular order; works using bounding boxes so large/generated meshes like roads get matched all the time. comes with example script. --- doc/angelscript/Script2Game/GameScriptClass.h | 5 ++ ...mple_game_getMousePointedMovableObjects.as | 65 +++++++++++++++++++ source/main/scripting/GameScript.cpp | 34 ++++++++++ source/main/scripting/GameScript.h | 5 ++ .../bindings/GameScriptAngelscript.cpp | 1 + 5 files changed, 110 insertions(+) create mode 100644 resources/scripts/example_game_getMousePointedMovableObjects.as diff --git a/doc/angelscript/Script2Game/GameScriptClass.h b/doc/angelscript/Script2Game/GameScriptClass.h index dcd6436b8d..34c03f210f 100644 --- a/doc/angelscript/Script2Game/GameScriptClass.h +++ b/doc/angelscript/Script2Game/GameScriptClass.h @@ -435,6 +435,11 @@ class GameScriptClass */ bool getMousePositionOnTerrain(vector3 &out); + /** + * Returns `array` in no particular order; works using bounding boxes so large/generated meshes like roads get matched all the time. + */ + array getMousePointedMovableObjects(); + /// @} /// @name Character diff --git a/resources/scripts/example_game_getMousePointedMovableObjects.as b/resources/scripts/example_game_getMousePointedMovableObjects.as new file mode 100644 index 0000000000..33d2e6a811 --- /dev/null +++ b/resources/scripts/example_game_getMousePointedMovableObjects.as @@ -0,0 +1,65 @@ +// TEST of new `game.getMousePointedMovableObjects()` +// =================================================== + +// Window [X] button handler +#include "imgui_utils.as" +imgui_utils::CloseWindowPrompt closeBtnHandler; + +// total time in seconds +float tt = 0.f; + +void main() +{ + // Uncomment to close window without asking. + //closeBtnHandler.cfgCloseImmediatelly = true; +} + +// `frameStep()` runs every frame; `dt` is delta time in seconds. +void frameStep(float dt) +{ + // Begin drawing window + if (ImGui::Begin("Mouse pointed object info", closeBtnHandler.windowOpen, 0)) + { + // Draw the "Terminate this script?" prompt on the top (if not disabled by config). + closeBtnHandler.draw(); + + array@ movables = game.getMousePointedMovableObjects(); + if (@movables == null) + { + ImGui::Text(""); + } + else + { + ImGui::Text("N U M R E S U L T S : " + movables.length()); + ImGui::Separator(); + for (uint i=0; i 0) + { + ImGui::Separator(); + } + Ogre::MovableObject@ movable = movables[i]; + drawMovableObjectInfo(movable); + + } + } + + + // End drawing window + ImGui::End(); + } +} + +void drawMovableObjectInfo(Ogre::MovableObject@ movable) +{ + ImGui::TextDisabled('Movable:' + movable.__getUniqueName()); + Ogre::SceneNode@ snode = movable.getParentSceneNode(); + ImGui::TextDisabled('SceneNode: ' + snode.__getUniqueName()); + + bool visible = movable.isVisible(); + ImGui::Text("Visible: "+ visible); + + bool castShadows = movable.getCastShadows(); + ImGui::Text("Cast shadows: "+ castShadows); +} + diff --git a/source/main/scripting/GameScript.cpp b/source/main/scripting/GameScript.cpp index 4a6a0ecd0a..ffae5d4183 100644 --- a/source/main/scripting/GameScript.cpp +++ b/source/main/scripting/GameScript.cpp @@ -1365,6 +1365,40 @@ bool GameScript::getMousePositionOnTerrain(Ogre::Vector3& out_pos) return ray_result.hit; } +class ScriptRayQueryListener : public Ogre::RaySceneQueryListener +{ +public: + Ogre::Ray ray; + std::vector results_array; + + bool queryResult(MovableObject* obj, Real distance) override + { + results_array.push_back(obj); + return true; // Continue query + } + + bool queryResult(SceneQuery::WorldFragment* fragment, Real distance) override + { + return true; // Continue query + } +}; + +CScriptArray* GameScript::getMousePointedMovableObjects() +{ + if (!HaveSimTerrain(__FUNCTION__)) + return nullptr; + + Ogre::Vector2 mouse_npos = App::GetInputEngine()->getMouseNormalizedScreenPos(); + Ogre::Ray ray = App::GetCameraManager()->GetCamera()->getCameraToViewportRay(mouse_npos.x, mouse_npos.y); + Ogre::DefaultRaySceneQuery query(App::GetGfxScene()->GetSceneManager()); + query.setRay(ray); + query.setSortByDistance(true); + ScriptRayQueryListener qlis; + qlis.ray = ray; + query.execute(&qlis); + return VectorToScriptArray(qlis.results_array, "Ogre::MovableObject@"); +} + Ogre::SceneManager* GameScript::getSceneManager() { return App::GetGfxScene()->GetSceneManager(); diff --git a/source/main/scripting/GameScript.h b/source/main/scripting/GameScript.h index b2c52d7044..3408e93f53 100644 --- a/source/main/scripting/GameScript.h +++ b/source/main/scripting/GameScript.h @@ -362,6 +362,11 @@ class GameScript */ bool getMousePositionOnTerrain(Ogre::Vector3& out_pos); + /** + * Returns `array` in no particular order. + */ + AngelScript::CScriptArray* getMousePointedMovableObjects(); + TerrainPtr getTerrain(); /// @} diff --git a/source/main/scripting/bindings/GameScriptAngelscript.cpp b/source/main/scripting/bindings/GameScriptAngelscript.cpp index 615d1e447c..227be19c6e 100644 --- a/source/main/scripting/bindings/GameScriptAngelscript.cpp +++ b/source/main/scripting/bindings/GameScriptAngelscript.cpp @@ -109,6 +109,7 @@ void RoR::RegisterGameScript(asIScriptEngine *engine) result = engine->RegisterObjectMethod("GameScriptClass", "void destroyObject(const string &in)", asMETHOD(GameScript, destroyObject), asCALL_THISCALL); ROR_ASSERT(result >= 0); result = engine->RegisterObjectMethod("GameScriptClass", "bool getMousePositionOnTerrain(vector3 &out)", AngelScript::asMETHOD(GameScript, getMousePositionOnTerrain), AngelScript::asCALL_THISCALL); ROR_ASSERT(result >= 0); result = engine->RegisterObjectMethod("GameScriptClass", "TerrainClassPtr@ getTerrain()", AngelScript::asMETHOD(GameScript,getTerrain), AngelScript::asCALL_THISCALL); ROR_ASSERT(result>=0); + result = engine->RegisterObjectMethod("GameScriptClass", "array@ getMousePointedMovableObjects()", AngelScript::asMETHOD(GameScript, getMousePointedMovableObjects), AngelScript::asCALL_THISCALL); ROR_ASSERT(result >= 0); // > Character result = engine->RegisterObjectMethod("GameScriptClass", "vector3 getPersonPosition()", asMETHOD(GameScript, getPersonPosition), asCALL_THISCALL); ROR_ASSERT(result >= 0); From 8562e815c725d779219c9a45999258092fc04680 Mon Sep 17 00:00:00 2001 From: ohlidalp Date: Fri, 22 Nov 2024 12:51:25 +0100 Subject: [PATCH 05/36] script_editor.as: Fixed oversized save/load menus drawSelectableFileList() now Creates a child window with dynamic height (but clamped to hardcoded max value). const int MAX_CHILD_HEIGHT = 300; const int CHILD_WIDTH = 600; --- resources/scripts/script_editor.as | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/resources/scripts/script_editor.as b/resources/scripts/script_editor.as index 4e9c6ab776..cd04b5ff95 100644 --- a/resources/scripts/script_editor.as +++ b/resources/scripts/script_editor.as @@ -387,9 +387,7 @@ class ScriptEditorWindow ImGui::Separator(); this.drawSelectableFileList("Recent scripts", "Select##recent", recentScriptsRecord, /*&inout*/ saveFileNameBuf); - ImGui::Separator(); this.drawSelectableFileList("Local scripts", "Select##local", localScriptsRecord, /*&inout*/ saveFileNameBuf); - ImGui::EndMenu(); } else @@ -566,12 +564,29 @@ class ScriptEditorWindow } } + // Creates a child window with dynamic height (but clamped to hardcoded max value). private bool drawSelectableFileList(string title, string btnText, ScriptIndexerRecord@ record, string&inout out_selection) { - bool retval = false; - ImGui::PushID(title); + bool retval = false; ImGui::TextDisabled(title+" ("+record.fileinfos.length()+"):"); + if (record.fileinfos.length() == 0) + { + return retval; // nothing to draw + } + + // Calc child window size + const int MAX_CHILD_HEIGHT = 300; + const int CHILD_WIDTH = 600; + int childHeight = record.fileinfos.length() * ImGui::GetTextLineHeightWithSpacing(); + //ImGui::Text("DBG record.fileinfos.length()=" + record.fileinfos.length() + ", childHeight{not clamped}=" + childHeight); + if (childHeight > MAX_CHILD_HEIGHT) + { + childHeight = MAX_CHILD_HEIGHT; + } + + // draw the child window + ImGui::BeginChild(title+"-child", vector2(CHILD_WIDTH, childHeight), /*border:*/false, ImGuiWindowFlags_HorizontalScrollbar); for (uint i=0; i Date: Mon, 25 Nov 2024 09:31:49 +0100 Subject: [PATCH 06/36] :angel:Script: added camera controls example Also fixed typos in `get/setCameraOrientation()` function bindings - parameter was declared as `vector3`, should have been `quaternion`. Also fixed weird binding of global `game` object, preventing use of AngelScript delegates. Fixed to bind as non-counting reference because that's the sensible choice. --- resources/scripts/example_game_camera.as | 175 ++++++++++++++++++ .../bindings/GameScriptAngelscript.cpp | 6 +- 2 files changed, 178 insertions(+), 3 deletions(-) create mode 100644 resources/scripts/example_game_camera.as diff --git a/resources/scripts/example_game_camera.as b/resources/scripts/example_game_camera.as new file mode 100644 index 0000000000..9aa0189883 --- /dev/null +++ b/resources/scripts/example_game_camera.as @@ -0,0 +1,175 @@ +// Test camera controls in global `game` object. +// =================================================== + +// Window [X] button handler +#include "imgui_utils.as" +imgui_utils::CloseWindowPrompt closeBtnHandler; + +//#region yaw,pitch,roll +bool setRoll = false; +float rollAngle = 0.f; +bool setYaw = false; +float yawAngle = 0.f; +bool setPitch = false; +float pitchAngle = 0.f; + +funcdef void YPRSETTERFUNC(float); + +void drawYawpitchrollControl(YPRSETTERFUNC@ setterFunc, string axisName, bool&inout active, float&inout angle) +{ + ImGui::PushID(axisName); + ImGui::Checkbox("game.setCamera"+axisName+"()", /*[inout]*/active); + if (active) + { + ImGui::SameLine(); + if (ImGui::SmallButton("reset")) + { + angle = 0; + } + ImGui::SliderFloat("angle", /*[inout]*/angle, 0-360, 360); + setterFunc(angle); + } + ImGui::PopID(); // axisName +} + +//#endregion + +//#region Position+Lookat +const float TERRAINSIZE=2000; // fake size... CBA to fetch actual size from terrn2 +vector3 terrainPosMax = vector3(TERRAINSIZE, TERRAINSIZE, TERRAINSIZE); +bool setPosition=false; +bool posInit = false; +vector3 posValue = vector3(0,0,0); +bool setLookat=false; +bool lookatInit=false; +vector3 lookatValue = vector3(0,0,0); +bool setDir = false; +bool dirInit=false; +vector3 dirValue = vector3(0,0,0); +vector3 resetDir() { return vector3(0,0,0); } + + +funcdef vector3 VEC3GETTER(void); +funcdef void VEC3SETTER(vector3&in); + +void drawTerrainPosControl(VEC3SETTER@ setterFunc, VEC3GETTER@ resetFunc, string label, bool&inout init, bool&inout active, vector3&inout val, vector3 maxval) +{ + ImGui::PushID(label); + if (!init) + { + val = resetFunc(); + init=true; + } + ImGui::Checkbox(label, active); + if (active) + { + ImGui::SameLine(); + if (ImGui::SmallButton("reset")) + { + val = resetFunc(); + } + ImGui::SliderFloat("X", val.x, 0, maxval.x); + ImGui::SliderFloat("Y (up)", val.y, 0, maxval.y); + ImGui::SliderFloat("Z", val.z, 0, maxval.z); + setterFunc(val); + } + ImGui::PopID(); // label +} +//#endregion + +//#region EditorCam +bool editorCam = false; +vector2 edgeSize = vector2(75, 50); +vector2 edgeMotion = vector2(0,0); +vector3 editorCamPos = vector3(0,0,0); +vector3 editorCamDirection = vector3(0,0,0); +bool editorCamInit=false; +float editorCamElevation=175.f; // meters above ground +float editorCamSpeed = 2.5f; +void updateEditorCam() +{ + if (!editorCamInit) + { + editorCamPos = game.getPersonPosition(); + editorCamInit=true; + } + vector2 m = game.getMouseScreenPosition(); + vector2 s = game.getDisplaySize(); + // horizontal +if (m.x < edgeSize.x) { edgeMotion.x = -1; } else if (m.x > (s.x - edgeSize.x)) { edgeMotion.x = 1; } else { edgeMotion.x = 0; } + // vertical +if (m.y < edgeSize.y) {edgeMotion.y = -1;} else if (m.y > (s.y - edgeSize.y)) {edgeMotion.y=1;} else {edgeMotion.y=0;} + + // apply motion + editorCamPos.x += edgeMotion.x*editorCamSpeed; + editorCamPos.z += edgeMotion.y*editorCamSpeed; + // apply elevation + editorCamPos.y = game.getTerrain().getHandle().getHeightAt(editorCamPos.x, editorCamPos.z) + editorCamElevation; + //submit + game.setCameraPosition(editorCamPos); + // set orientation - pitch down 55degrees + vector3 X_AXIS(1,0,0); + radian pitchdown = degree(-55); + game.setCameraOrientation( quaternion( pitchdown, X_AXIS)); +} +void drawEditorCamUI() +{ + ImGui::PushID("editorCamUI"); + ImGui::Checkbox("EditorCam", editorCam); + if (editorCam) + { + ImGui::Text("edgeMotion: X="+formatFloat(edgeMotion.x, '', 2, 1) + ", Y=" + formatFloat(edgeMotion.y, '', 2, 1)); + ImGui::SliderFloat("elevation", editorCamElevation, 1, 1000); + } + ImGui::PopID(); //editorCamUI +} +//#endregion + +// `frameStep()` runs every frame; `dt` is delta time in seconds. +void frameStep(float dt) +{ + // Begin drawing window + if (ImGui::Begin("Camera control test", closeBtnHandler.windowOpen, 0)) + { + // Draw the "Terminate this script?" prompt on the top (if not disabled by config). + closeBtnHandler.draw(); + + //#region UI-Getters + ImGui::TextDisabled("Getters:"); + ImGui::Text("game.getCameraPosition(): " + formatVector3(game.getCameraPosition(), 7, 2)); + ImGui::Text("game.getCameraDirection(): " + formatVector3(game.getCameraDirection(), 5, 2)); + //#endregion + ImGui::Separator(); + //#region UI-Setters + ImGui::TextDisabled("Setters:"); + drawYawpitchrollControl(YPRSETTERFUNC(game.setCameraYaw), "Yaw", setYaw, yawAngle); + drawYawpitchrollControl(YPRSETTERFUNC(game.setCameraPitch), "Pitch", setPitch, pitchAngle); + drawYawpitchrollControl(YPRSETTERFUNC(game.setCameraRoll), "Roll", setRoll, rollAngle); + drawTerrainPosControl( VEC3SETTER(game.setCameraPosition), VEC3GETTER(game.getCameraPosition), "game.setCameraPosition()", posInit, setPosition, posValue, terrainPosMax); + drawTerrainPosControl( VEC3SETTER(game.cameraLookAt), VEC3GETTER(game.getPersonPosition), "game.cameraLookAt()", lookatInit, setLookat, lookatValue, terrainPosMax); + drawTerrainPosControl( VEC3SETTER(game.setCameraDirection), VEC3GETTER(resetDir), "game.setCameraDirection()", dirInit, setDir, dirValue, vector3(1,1,1)); + //#endregion + ImGui::Separator(); + //#region Editor-cam test + ImGui::TextDisabled("Top-down editor camera"); + ImGui::TextDisabled("with mouse-scrolling at screen edges"); + drawEditorCamUI(); + if (editorCam) + { + updateEditorCam(); + } + //#endregion + + // End drawing window + ImGui::End(); + } +} + +//#region Helpers +string formatVector3(vector3 val, int total, int frac) +{ + return "X:" + formatFloat(val.x, "", total, frac) + + " Y:" + formatFloat(val.y, "", total, frac) + + " Z:" + formatFloat(val.z, "", total, frac); +} +//#endregion diff --git a/source/main/scripting/bindings/GameScriptAngelscript.cpp b/source/main/scripting/bindings/GameScriptAngelscript.cpp index 227be19c6e..898c69f6c4 100644 --- a/source/main/scripting/bindings/GameScriptAngelscript.cpp +++ b/source/main/scripting/bindings/GameScriptAngelscript.cpp @@ -40,7 +40,7 @@ void RoR::RegisterGameScript(asIScriptEngine *engine) result = engine->RegisterEnumValue("ScriptCategory", "SCRIPT_CATEGORY_CUSTOM", static_cast(ScriptCategory::CUSTOM)); ROR_ASSERT(result >= 0); // class GameScript - result = engine->RegisterObjectType("GameScriptClass", sizeof(GameScript), asOBJ_VALUE | asOBJ_POD | asOBJ_APP_CLASS); ROR_ASSERT(result >= 0); + result = engine->RegisterObjectType("GameScriptClass", sizeof(GameScript), asOBJ_REF | asOBJ_NOCOUNT); ROR_ASSERT(result >= 0); // PLEASE maintain the same order as in GameScript.h! @@ -164,13 +164,13 @@ void RoR::RegisterGameScript(asIScriptEngine *engine) // > Camera result = engine->RegisterObjectMethod("GameScriptClass", "void setCameraPosition(vector3 &in)", asMETHOD(GameScript, setCameraPosition), asCALL_THISCALL); ROR_ASSERT(result >= 0); result = engine->RegisterObjectMethod("GameScriptClass", "void setCameraDirection(vector3 &in)", asMETHOD(GameScript, setCameraDirection), asCALL_THISCALL); ROR_ASSERT(result >= 0); - result = engine->RegisterObjectMethod("GameScriptClass", "void setCameraOrientation(vector3 &in)", asMETHOD(GameScript, setCameraOrientation), asCALL_THISCALL); ROR_ASSERT(result >= 0); + result = engine->RegisterObjectMethod("GameScriptClass", "void setCameraOrientation(quaternion &in)", asMETHOD(GameScript, setCameraOrientation), asCALL_THISCALL); ROR_ASSERT(result >= 0); result = engine->RegisterObjectMethod("GameScriptClass", "void setCameraRoll(float)", asMETHOD(GameScript, setCameraRoll), asCALL_THISCALL); ROR_ASSERT(result >= 0); result = engine->RegisterObjectMethod("GameScriptClass", "void setCameraYaw(float)", asMETHOD(GameScript, setCameraYaw), asCALL_THISCALL); ROR_ASSERT(result >= 0); result = engine->RegisterObjectMethod("GameScriptClass", "void setCameraPitch(float)", asMETHOD(GameScript, setCameraPitch), asCALL_THISCALL); ROR_ASSERT(result >= 0); result = engine->RegisterObjectMethod("GameScriptClass", "vector3 getCameraPosition()", asMETHOD(GameScript, getCameraPosition), asCALL_THISCALL); ROR_ASSERT(result >= 0); result = engine->RegisterObjectMethod("GameScriptClass", "vector3 getCameraDirection()", asMETHOD(GameScript, getCameraDirection), asCALL_THISCALL); ROR_ASSERT(result >= 0); - result = engine->RegisterObjectMethod("GameScriptClass", "vector3 getCameraOrientation()", asMETHOD(GameScript, getCameraOrientation), asCALL_THISCALL); ROR_ASSERT(result >= 0); + result = engine->RegisterObjectMethod("GameScriptClass", "quaternion getCameraOrientation()", asMETHOD(GameScript, getCameraOrientation), asCALL_THISCALL); ROR_ASSERT(result >= 0); result = engine->RegisterObjectMethod("GameScriptClass", "void cameraLookAt(vector3 &in)", asMETHOD(GameScript, cameraLookAt), asCALL_THISCALL); ROR_ASSERT(result >= 0); // > Race system From 65c1baacba5a7c2df8aac54f49fe53f69231e0ac Mon Sep 17 00:00:00 2001 From: ohlidalp Date: Mon, 25 Nov 2024 09:44:48 +0100 Subject: [PATCH 07/36] :angel:Script: Minor bugfixes and formatting in scripts. --- .../scripts/example_GenericDocument_editor.as | 4 +- resources/scripts/example_RigEditor_alpha.as | 2 +- resources/scripts/example_game_shockTuning.as | 15 +- resources/scripts/example_ogre_overlays.as | 130 +++++++++++------- .../scripts/example_ogre_terrnBatcher.as | 2 +- 5 files changed, 91 insertions(+), 62 deletions(-) diff --git a/resources/scripts/example_GenericDocument_editor.as b/resources/scripts/example_GenericDocument_editor.as index a58b9bceda..3cc99dda1d 100644 --- a/resources/scripts/example_GenericDocument_editor.as +++ b/resources/scripts/example_GenericDocument_editor.as @@ -25,7 +25,7 @@ void frameStep(float dt) if (@actor != null) { CacheEntryClass@ entry = modcache.findEntryByFilename(LOADER_TYPE_ALLBEAM, /*partial:*/false, actor.getTruckFileName()); - if (@entry != null) + if (@entry != null && entry.resource_bundle_type == "FileSystem") { @projectEntry = @entry; loadDocument(); @@ -111,7 +111,7 @@ void createProjectFromActorAsync(BeamClass@ actor ) errorString = "Failed to load cache entry!"; // request project to be created from that cache entry - string proj_name = "project1"; + string proj_name = "gd_editor_" + src_entry.fname; game.pushMessage(MSG_EDI_CREATE_PROJECT_REQUESTED, { {'name', proj_name}, {'source_entry', src_entry} diff --git a/resources/scripts/example_RigEditor_alpha.as b/resources/scripts/example_RigEditor_alpha.as index a8e9debd89..008db3cee8 100644 --- a/resources/scripts/example_RigEditor_alpha.as +++ b/resources/scripts/example_RigEditor_alpha.as @@ -53,7 +53,7 @@ void frameStep(float dt) if (@actor != null) { CacheEntryClass@ entry = modcache.findEntryByFilename(LOADER_TYPE_ALLBEAM, /*partial:*/false, actor.getTruckFileName()); - if (@entry != null) + if (@entry != null && entry.resource_bundle_type == "FileSystem") { @m_project_entry = @entry; loadDocument(); diff --git a/resources/scripts/example_game_shockTuning.as b/resources/scripts/example_game_shockTuning.as index 8ee4198079..eb8bca7dc0 100644 --- a/resources/scripts/example_game_shockTuning.as +++ b/resources/scripts/example_game_shockTuning.as @@ -217,14 +217,17 @@ void frameStep(float dt) // End window ImGui::End(); - // Draw the individual shock in-scene windows - for (int i = 0; i < actor.getShockCount(); i++) + if (@actor != null) { - SpringData@ shockdata = g_shock_buffers[i]; - if (shockdata.drawInScene) + // Draw the individual shock in-scene windows + for (int i = 0; i < actor.getShockCount(); i++) { - drawSpringHighlight(actor, i, cfgSpringColor, cfgSpringThickness); - drawSpringSceneHud(actor, i); + SpringData@ shockdata = g_shock_buffers[i]; + if (shockdata.drawInScene) + { + drawSpringHighlight(actor, i, cfgSpringColor, cfgSpringThickness); + drawSpringSceneHud(actor, i); + } } } } diff --git a/resources/scripts/example_ogre_overlays.as b/resources/scripts/example_ogre_overlays.as index 00c44cbdf0..f55ebd35a8 100644 --- a/resources/scripts/example_ogre_overlays.as +++ b/resources/scripts/example_ogre_overlays.as @@ -14,72 +14,98 @@ #include "imgui_utils.as" imgui_utils::CloseWindowPrompt closeBtnHandler; +// overlay +string ovName = "ov"+thisScript; bool ov_fail = false; - +// panel +string paName = "pa"+thisScript; bool pa_fail = false; +int paNumCreates = 0; +// misc int framecounter = 0; float pos_step = 50; // pixels void frameStep(float dt) { - Ogre::Overlay@ ov; - if (!ov_fail) { - // NOTE: getByName() will calmly return NULL if overlay doesn't exist. - @ov = Ogre::OverlayManager::getSingleton().getByName("ov"); - // CAUTION: attempt to create the same overlay again will throw an exception, interrupting the script in between! - if (@ov == null) { @ov = Ogre::OverlayManager::getSingleton().create("ov"); } - if (@ov == null) { ov_fail = true; } - else { ov.show(); } - } - - Ogre::OverlayElement@ pa; - - if (!pa_fail ){ - if ( Ogre::OverlayManager::getSingleton().hasOverlayElement("pa")) { - - // CAUTION: getOverlayElement() will throw exception if not found, always do `hasOverlayElement()` first! - @pa = Ogre::OverlayManager::getSingleton().getOverlayElement("pa"); - } - // CAUTION: using the same name twice will throw an exception, interrupting the script in between! - if (@pa == null ) { @pa = Ogre::OverlayManager::getSingleton().createOverlayElement("Panel", "pa"); - if (@pa == null ) { pa_fail=true; } - else { -// game.log("adding pa to ov"); - ov.add2D(pa); - pa.setMetricsMode(Ogre::GMM_PIXELS); - pa.setPosition(100,100); - pa.setDimensions(100,100); - pa.show(); - } - } - pa.setMaterialName("tracks/wheelface", 'OgreAutodetect'); - } - + Ogre::Overlay@ ov; + if (!ov_fail) + { + // NOTE: getByName() will calmly return NULL if overlay doesn't exist. + @ov = Ogre::OverlayManager::getSingleton().getByName(ovName); + // CAUTION: attempt to create the same overlay again will throw an exception, interrupting the script in between! + if (@ov == null) + { + @ov = Ogre::OverlayManager::getSingleton().create(ovName); + } + if (@ov == null) + { + ov_fail = true; + } + else + { + ov.show(); + } + } + + Ogre::OverlayElement@ pa; + + if (!pa_fail ) + { + if ( Ogre::OverlayManager::getSingleton().hasOverlayElement(paName)) + { + game.log('Looking for pa'); + // CAUTION: getOverlayElement() will throw exception if not found, always do `hasOverlayElement()` first! + @pa = Ogre::OverlayManager::getSingleton().getOverlayElement(paName); + + } + // CAUTION: using the same name twice will throw an exception, interrupting the script in between! + if (@pa == null ) + { + @pa = Ogre::OverlayManager::getSingleton().createOverlayElement("Panel", paName); + paNumCreates++; + game.log('paNumCreates:'+paNumCreates); + if (@pa == null ) + { + pa_fail=true; + } + else + { + // game.log("adding pa to ov"); + ov.add2D(pa); + pa.setMetricsMode(Ogre::GMM_PIXELS); + pa.setPosition(100,100); + pa.setDimensions(100,100); + pa.setMaterialName("tracks/wheelface", 'OgreAutodetect'); + pa.show(); + } + } + + } + if (ImGui::Begin("Example", closeBtnHandler.windowOpen, 0)) { closeBtnHandler.draw(); - ImGui::Text("overlays should work; ov_fail:"+ov_fail+", pa_fail:"+pa_fail - +", frames:"+framecounter); + ImGui::Text("overlays should work; ov_fail:"+ov_fail+", pa_fail:"+pa_fail +", frames:"+framecounter); framecounter++; if (!pa_fail && @pa != null) { - + ImGui::TextDisabled("The wheel overlay:"); - if (ImGui::Button("Hide")) { pa.hide(); } - ImGui::SameLine(); if (ImGui::Button("Show")) { pa.show(); } - - if (ImGui::Button("Position: Left+")) { pa.setLeft(pa.getLeft()+pos_step); } - ImGui::SameLine(); if (ImGui::Button("Position:left-")) { pa.setLeft(pa.getLeft()-pos_step); } - - if (ImGui::Button("Position: Top+")) { pa.setTop(pa.getTop()+pos_step); } - ImGui::SameLine(); if (ImGui::Button("Position:Top-")) { pa.setTop(pa.getTop()-pos_step); } - - if (ImGui::Button("Width+")) { pa.setWidth(pa.getWidth()+pos_step); } - ImGui::SameLine(); if (ImGui::Button("Width-")) { pa.setWidth(pa.getWidth()-pos_step); } - - if (ImGui::Button("height+")) { pa.setHeight(pa.getHeight()+pos_step); } - ImGui::SameLine(); if (ImGui::Button("height-")) { pa.setHeight(pa.getHeight()-pos_step); } - + if (ImGui::Button("Hide")) { pa.hide(); } + ImGui::SameLine(); if (ImGui::Button("Show")) { pa.show(); } + + if (ImGui::Button("Position: Left+")) { pa.setLeft(pa.getLeft()+pos_step); } + ImGui::SameLine(); if (ImGui::Button("Position:left-")) { pa.setLeft(pa.getLeft()-pos_step); } + + if (ImGui::Button("Position: Top+")) { pa.setTop(pa.getTop()+pos_step); } + ImGui::SameLine(); if (ImGui::Button("Position:Top-")) { pa.setTop(pa.getTop()-pos_step); } + + if (ImGui::Button("Width+")) { pa.setWidth(pa.getWidth()+pos_step); } + ImGui::SameLine(); if (ImGui::Button("Width-")) { pa.setWidth(pa.getWidth()-pos_step); } + + if (ImGui::Button("height+")) { pa.setHeight(pa.getHeight()+pos_step); } + ImGui::SameLine(); if (ImGui::Button("height-")) { pa.setHeight(pa.getHeight()-pos_step); } + } ImGui::End(); } diff --git a/resources/scripts/example_ogre_terrnBatcher.as b/resources/scripts/example_ogre_terrnBatcher.as index 404712b4c4..24683bf38a 100644 --- a/resources/scripts/example_ogre_terrnBatcher.as +++ b/resources/scripts/example_ogre_terrnBatcher.as @@ -27,7 +27,7 @@ void main() void frameStep(float dt) { - if (ImGui::Begin("TERRN BATCHER [ALPHA]", closeBtnHandler.draw(), ImGuiWindowFlags_AlwaysAutoResize)) + if (ImGui::Begin("TERRN BATCHER [ALPHA]", closeBtnHandler.windowOpen, ImGuiWindowFlags_AlwaysAutoResize)) { closeBtnHandler.draw(); tbUI.draw(); From 572f88369707cca8ffd96ef545275b6435701daf Mon Sep 17 00:00:00 2001 From: ohlidalp Date: Thu, 21 Nov 2024 09:38:05 +0100 Subject: [PATCH 08/36] road_editor.as: Fixed typos in road type radiobtn --- resources/scripts/road_editor.as | 67 +++++++++++--------------------- 1 file changed, 22 insertions(+), 45 deletions(-) diff --git a/resources/scripts/road_editor.as b/resources/scripts/road_editor.as index 4cc754a840..6be4eae286 100644 --- a/resources/scripts/road_editor.as +++ b/resources/scripts/road_editor.as @@ -188,6 +188,17 @@ void drawRoadEditPanel() ImGui::PopID(); // "road edit box" } +void drawRoadTypeRadioBtn(ProceduralPointClass@ point, string&in label, RoadType roadType, int pillarType) +{ + bool selected =( point.type == roadType && point.pillar_type == pillarType ); + + if (ImGui::RadioButton(label, selected)) + { + point.type = roadType; + point.pillar_type = pillarType; + } +} + void drawPointPropertiesPanel(ProceduralObjectClass@ obj) { ImGui::PushID("point properties"); @@ -208,52 +219,18 @@ void drawPointPropertiesPanel(ProceduralObjectClass@ obj) ImGui::InputFloat("Border height (meters)", point.border_height); ImGui::Text("Type:"); + ImGui::SameLine(); + ImGui::TextDisabled("DBG type:"+point.type+", pillartype:"+point.pillar_type); // Types supported by TOBJ format - if (ImGui::RadioButton("(automatic)", point.type == ROAD_AUTOMATIC)) - { - point.type = ROAD_AUTOMATIC; - point.pillar_type = 0; - } - if (ImGui::RadioButton("flat (no border)", point.type == ROAD_FLAT)) - { - point.type = ROAD_FLAT; - point.pillar_type = 0; - } - if (ImGui::RadioButton("left (border on left)", point.type == ROAD_LEFT)) - { - point.type = ROAD_LEFT; - point.pillar_type = 0; - } - if (ImGui::RadioButton("right (border on right)", point.type == ROAD_RIGHT)) - { - point.type = ROAD_RIGHT; - point.pillar_type = 0; - } - if (ImGui::RadioButton("both (with borders)", point.type == ROAD_BOTH)) - { - point.type = ROAD_BOTH; - point.pillar_type = 0; - } - if (ImGui::RadioButton("bridge (with pillars)", point.type == ROAD_BRIDGE && point.pillar_type == 1)) - { - point.type = ROAD_BRIDGE; - point.pillar_type = 1; - } - if (ImGui::RadioButton("bridge_no_pillars", point.type == ROAD_BRIDGE && point.pillar_type == 0)) - { - point.type = ROAD_BRIDGE; - point.pillar_type = 0; - } - if (ImGui::RadioButton("monorail (with pillars)", point.type == ROAD_BRIDGE && point.pillar_type == 2)) - { - point.type = ROAD_MONORAIL; - point.pillar_type = 2; - } - if (ImGui::RadioButton("monorail2 (no pillars)", point.type == ROAD_BRIDGE && point.pillar_type == 0)) - { - point.type = ROAD_MONORAIL; - point.pillar_type = 0; - } + drawRoadTypeRadioBtn(point, "(automatic)" ,ROAD_AUTOMATIC, 0); + drawRoadTypeRadioBtn(point, "flat (no border)", ROAD_FLAT, 0); + drawRoadTypeRadioBtn(point, "left (border on left)", ROAD_LEFT, 0); + drawRoadTypeRadioBtn(point, "right (border on right)", ROAD_RIGHT, 0); + drawRoadTypeRadioBtn(point, "both (with borders)", ROAD_BOTH, 0); + drawRoadTypeRadioBtn(point, "bridge (with pillars)", ROAD_BRIDGE , 1); + drawRoadTypeRadioBtn(point, "bridge_no_pillars", ROAD_BRIDGE , 0); + drawRoadTypeRadioBtn(point, "monorail (with pillars)", ROAD_MONORAIL , 2); + drawRoadTypeRadioBtn(point, "monorail2 (no pillars)", ROAD_MONORAIL , 0); // End of types supported by TOBJ format } else From 770953d7c6aff614c76f72b0a81b66fde612d446 Mon Sep 17 00:00:00 2001 From: Petr Ohlidal Date: Fri, 26 Jul 2024 01:11:37 +0200 Subject: [PATCH 09/36] :angel:Script: added `game.getScriptVariable()` --- doc/angelscript/Script2Game/GameScriptClass.h | 9 ++ .../scripts/example_terrn2_raceConverter.as | 66 ++++++++++++ resources/scripts/races.as | 6 +- source/main/scripting/GameScript.cpp | 5 + source/main/scripting/GameScript.h | 10 ++ source/main/scripting/ScriptEngine.cpp | 101 +++++++++++++++++- source/main/scripting/ScriptEngine.h | 6 ++ .../bindings/GameScriptAngelscript.cpp | 2 +- 8 files changed, 201 insertions(+), 4 deletions(-) create mode 100644 resources/scripts/example_terrn2_raceConverter.as diff --git a/doc/angelscript/Script2Game/GameScriptClass.h b/doc/angelscript/Script2Game/GameScriptClass.h index 34c03f210f..914d682a8a 100644 --- a/doc/angelscript/Script2Game/GameScriptClass.h +++ b/doc/angelscript/Script2Game/GameScriptClass.h @@ -300,6 +300,15 @@ class GameScriptClass * @param var the declaration of the variable that should be removed, e.g.: "int missionState;" */ int deleteScriptVariable(const string var); + + /** + * Retrieves a memory address of a global variable in any script. + * @param nid ScriptUnitID, ID of the running script, obtain one from global var `thisScript` or `game.getRunningScripts()` + * @param varName Name of the variable. Type must match the reference type. + * @param ref A variable-type parameter - accepts any reference. + * @return 0 on success, negative number on error. + */ + int getScriptVariable(ScriptUnitId_t nid, const string&in varName, ?&ref); /** * Clears the event cache diff --git a/resources/scripts/example_terrn2_raceConverter.as b/resources/scripts/example_terrn2_raceConverter.as new file mode 100644 index 0000000000..0e0851407f --- /dev/null +++ b/resources/scripts/example_terrn2_raceConverter.as @@ -0,0 +1,66 @@ +/// \title terrn2 race converter +/// \brief imports races from scripts and generates challenge files. +/// new section in terrn2 format: [Challenges] +/// each line is a tobj-like file with any extension (i.e. *.race) which is loaded by the race system. +/// ================================================== + +enum Stage +{ + STAGE_IDLE, + STAGE_PUSHMSG, + STAGE_GENFILES, + STAGE_DONE, + STAGE_ERROR +} +Stage stage = STAGE_IDLE; +string error = ""; + +void frameStep(float dt) +{ + // === DRAW UI === + + switch(stage) + { + case STAGE_IDLE: + if (@game.getTerrain() != null + && stage == STAGE_IDLE + && ImGui::Button("Convert races from script to terrn2 [Challenges]")) + { + stage = STAGE_PUSHMSG; + } + break; + case STAGE_ERROR: + ImGui::Text("ERROR! "+error); + break; + case STAGE_PUSHMSG: + ImGui::Text("Performing MSG_EDI_CREATE_PROJECT_REQUESTED"); + break; + default: + break; + } + + // === PERFORM IMPORT STEP === + switch (stage) + { + case STAGE_PUSHMSG: + { + // Fetch terrain's modcache entry + CacheEntryClass@ src_entry = modcache.findEntryByFilename(LOADER_TYPE_TERRAIN, /*partial:*/false, game.getTerrain().getTerrainFileName()); + if (@src_entry == null) + { + error = "Not found in modcache!!"; + stage = STAGE_ERROR; + } + + // request project to be created from that cache entry + game.pushMessage(MSG_EDI_CREATE_PROJECT_REQUESTED, { + {'name', game.getTerrain().getTerrainName() + " [Challenges]"}, + {'source_entry', src_entry} + }); + stage = STAGE_GENFILES; + } + break; + default: + break; + } +} diff --git a/resources/scripts/races.as b/resources/scripts/races.as index 10001f86b3..2759ac0e4d 100644 --- a/resources/scripts/races.as +++ b/resources/scripts/races.as @@ -46,7 +46,8 @@ void raceCancelPointHandler(int trigger_type, string inst, string box, int nodei // This class will handle the race logic for you // this class shouldn't be edited! -class racesManager { +// marked 'shared' so that other scripts can access the race data using `game.getScriptVariable()` +shared class racesManager { // public properties int raceCount; int currentRace; @@ -1136,7 +1137,8 @@ class racesManager { // This class manages a race (singular!) // this class shouldn't be edited! // You should only use this directly if the races manager above doesn't suit your needs -class raceBuilder { +// marked 'shared' so that other scripts can access the race data using `game.getScriptVariable()` +shared class raceBuilder { string raceName; double[][][] checkpoints; array> objNames; diff --git a/source/main/scripting/GameScript.cpp b/source/main/scripting/GameScript.cpp index ffae5d4183..421c2f9051 100644 --- a/source/main/scripting/GameScript.cpp +++ b/source/main/scripting/GameScript.cpp @@ -971,6 +971,11 @@ int GameScript::deleteScriptVariable(const String& arg) return App::GetScriptEngine()->deleteVariable(arg); } +int GameScript::getScriptVariable(ScriptUnitId_t nid, const Ogre::String& varName, void *ref, int refTypeId) +{ + return App::GetScriptEngine()->getVariable(nid, varName, ref, refTypeId); +} + int GameScript::sendGameCmd(const String& message) { #ifdef USE_SOCKETW diff --git a/source/main/scripting/GameScript.h b/source/main/scripting/GameScript.h index 3408e93f53..dcc4a65f36 100644 --- a/source/main/scripting/GameScript.h +++ b/source/main/scripting/GameScript.h @@ -251,6 +251,16 @@ class GameScript */ int deleteScriptVariable(const Ogre::String& arg); + /** + * Retrieves a memory address of a global variable in any script. + * @param nid ScriptUnitID, ID of the running script, obtain one from global var `thisScript` or `game.getRunningScripts()` + * @param varName Name of the variable. Type must match the reference type. + * @param ref Pointer to the variable's memory address; To be registered as variable-type parameter `?&out` + * @param refTypeId Type of the reference; To be registered as variable-type parameter `?&out` + * @return 0 on success, negative number on error. + */ + int getScriptVariable(ScriptUnitId_t nid, const Ogre::String& varName, void *ref, int refTypeId); + void clearEventCache(); /** diff --git a/source/main/scripting/ScriptEngine.cpp b/source/main/scripting/ScriptEngine.cpp index 348d1580d6..52154fbef2 100644 --- a/source/main/scripting/ScriptEngine.cpp +++ b/source/main/scripting/ScriptEngine.cpp @@ -687,6 +687,105 @@ int ScriptEngine::deleteVariable(const String &arg) return index; } +int ScriptEngine::getVariable(ScriptUnitId_t nid, const Ogre::String& varName, void *ref, int refTypeId) +{ + if (!engine || !context) + { + SLOG("Error in `getVariable()` - engine or context not initialized"); + return -1; + } + + if (!this->scriptUnitExists(nid)) + { + SLOG("Error in `getVariable()` - script unit does not exist"); + return -2; + } + + AngelScript::asIScriptModule *mod = m_script_units[nid].scriptModule; + if (!mod) + { + SLOG("Error in `getVariable()` - script module not initialized"); + return -3; + } + + int index = mod->GetGlobalVarIndexByName(varName.c_str()); + if (index < 0) + { + SLOG(fmt::format("Error in `getVariable()` - '{}' not found", varName)); + return -4; + } + + const char* asVarName = nullptr; + const char* asNamespace = nullptr; + int asTypeId = 0; + bool asConst = false; + int getResult = mod->GetGlobalVar(index, &asVarName, &asNamespace, &asTypeId, &asConst); + if (getResult < 0) + { + SLOG(fmt::format("Error in `getVariable()` - error while getting type of '{}'", varName)); + return -5; + } + + SLOG(fmt::format("getVariable() - '{}' global var info: name='{}', namespace='{}', typeid={}, const={}", + varName, asVarName, asNamespace, asTypeId, asConst)); + + // ~~ DEV NOTE: The following code is adopted from AngelScript's add-on 'scriptany.cpp', function `Retrieve()` ~~ + + if( refTypeId & asTYPEID_OBJHANDLE ) + { + // Is the handle type compatible with the stored value? + + // A handle can be retrieved if the stored type is a handle of same or compatible type + // or if the stored type is an object that implements the interface that the handle refer to. + if( (asTypeId & asTYPEID_MASK_OBJECT) ) + { + // Don't allow the retrieval if the stored handle is to a const object but not the wanted handle + if( (asTypeId & asTYPEID_HANDLETOCONST) && !(refTypeId & asTYPEID_HANDLETOCONST) ) + { + SLOG(fmt::format("Error in `getVariable()` - '{}' is a handle to `const` object but the requested type is not.", varName)); + return -6; + } + + // RefCastObject will increment the refCount of the returned pointer if successful + engine->RefCastObject(mod->GetAddressOfGlobalVar(index), engine->GetTypeInfoById(asTypeId), engine->GetTypeInfoById(refTypeId), reinterpret_cast(ref)); + if( *(asPWORD*)ref == 0 ) + { + SLOG(fmt::format("Error in `getVariable()` - '{}': reference-cast from '{}' to '{}' yielded null", + varName, engine->GetTypeDeclaration(asTypeId), engine->GetTypeDeclaration(refTypeId))); + return -7; + } + return 0; + } + } + else if( refTypeId & asTYPEID_MASK_OBJECT ) + { + // Is the object type compatible with the stored value? + + // Copy the object into the given reference + if( asTypeId == refTypeId ) + { + engine->AssignScriptObject(ref, mod->GetAddressOfGlobalVar(index), engine->GetTypeInfoById(asTypeId)); + return 0; + } + } + else + { + // Is the primitive type _IDENTICAL TO_ the stored value? + // NOTE: implicit conversions are not done automatically, we would have to write code for each case separately + + if( asTypeId == refTypeId ) + { + int size = engine->GetSizeOfPrimitiveType(refTypeId); + memcpy(ref, mod->GetAddressOfGlobalVar(index), size); + return 0; + } + } + + SLOG(fmt::format("Error in `getVariable()` - '{}' has incompatible type, expected '{}' (typeid {}), got '{}' (typeid {})", + varName, engine->GetTypeDeclaration(refTypeId), refTypeId, engine->GetTypeDeclaration(asTypeId), asTypeId)); + return -8; +} + asIScriptFunction* ScriptEngine::getFunctionByDeclAndLogCandidates(ScriptUnitId_t nid, GetFuncFlags_t flags, const std::string& funcName, const std::string& fmtFuncDecl) { std::string decl = fmt::format(fmtFuncDecl, funcName); @@ -810,7 +909,7 @@ int ScriptEngine::setupScriptUnit(int unit_id) int result=0; String moduleName = this->composeModuleName( - m_script_units[unit_id].scriptName, m_script_units[unit_id].scriptCategory, m_script_units[unit_id].uniqueId); + m_script_units[unit_id].scriptName, m_script_units[unit_id].scriptCategory, m_script_units[unit_id].uniqueId); // The builder is a helper class that will load the script file, // search for #include directives, and load any included files as diff --git a/source/main/scripting/ScriptEngine.h b/source/main/scripting/ScriptEngine.h index 2fccca36d6..64f00101a3 100644 --- a/source/main/scripting/ScriptEngine.h +++ b/source/main/scripting/ScriptEngine.h @@ -202,6 +202,12 @@ class ScriptEngine : public Ogre::LogListener */ int deleteVariable(const Ogre::String& arg); + /** + * Retrieves a global variable from any running script + * @returns 0 on success, negative number on error. + */ + int getVariable(ScriptUnitId_t nid, const Ogre::String& varName, void *ref, int typeID); + /** * Finds a function by full declaration, and if not found, finds candidates by name and logs them to Angelscript.log * @return Angelscript function on success, null on error. diff --git a/source/main/scripting/bindings/GameScriptAngelscript.cpp b/source/main/scripting/bindings/GameScriptAngelscript.cpp index 898c69f6c4..71cfd3030e 100644 --- a/source/main/scripting/bindings/GameScriptAngelscript.cpp +++ b/source/main/scripting/bindings/GameScriptAngelscript.cpp @@ -88,6 +88,7 @@ void RoR::RegisterGameScript(asIScriptEngine *engine) result = engine->RegisterObjectMethod("GameScriptClass", "int deleteScriptFunction(const string &in)", asMETHOD(GameScript, deleteScriptFunction), asCALL_THISCALL); ROR_ASSERT(result >= 0); result = engine->RegisterObjectMethod("GameScriptClass", "int addScriptVariable(const string &in)", asMETHOD(GameScript, addScriptVariable), asCALL_THISCALL); ROR_ASSERT(result >= 0); result = engine->RegisterObjectMethod("GameScriptClass", "int deleteScriptVariable(const string &in)", asMETHOD(GameScript, deleteScriptVariable), asCALL_THISCALL); ROR_ASSERT(result >= 0); + result = engine->RegisterObjectMethod("GameScriptClass", "int getScriptVariable(int, const string &in, ?&out)", asMETHOD(GameScript, getScriptVariable), asCALL_THISCALL); ROR_ASSERT(result >= 0); result = engine->RegisterObjectMethod("GameScriptClass", "void clearEventCache()", asMETHOD(GameScript, clearEventCache), asCALL_THISCALL); ROR_ASSERT(result >= 0); result = engine->RegisterObjectMethod("GameScriptClass", "int sendGameCmd(const string &in)", asMETHOD(GameScript, sendGameCmd), asCALL_THISCALL); ROR_ASSERT(result >= 0); result = engine->RegisterObjectMethod("GameScriptClass", "array@ getRunningScripts()", asMETHOD(GameScript, getRunningScripts), asCALL_THISCALL); ROR_ASSERT(result >= 0); @@ -150,7 +151,6 @@ void RoR::RegisterGameScript(asIScriptEngine *engine) result = engine->RegisterObjectMethod("GameScriptClass", "int getAIMode()", AngelScript::asMETHOD(GameScript, getAIMode), AngelScript::asCALL_THISCALL); ROR_ASSERT(result >= 0); result = engine->RegisterObjectMethod("GameScriptClass", "VehicleAIClassPtr @getCurrentTruckAI()", asMETHOD(GameScript, getCurrentTruckAI), asCALL_THISCALL); ROR_ASSERT(result >= 0); result = engine->RegisterObjectMethod("GameScriptClass", "VehicleAIClassPtr @getTruckAIByNum(int)", asMETHOD(GameScript, getTruckAIByNum), asCALL_THISCALL); ROR_ASSERT(result >= 0); - // AI: set result = engine->RegisterObjectMethod("GameScriptClass", "void setAIVehicleCount(int count)", AngelScript::asMETHOD(GameScript, setAIVehicleCount), AngelScript::asCALL_THISCALL); ROR_ASSERT(result >= 0); result = engine->RegisterObjectMethod("GameScriptClass", "void setAIVehicleDistance(int dist)", AngelScript::asMETHOD(GameScript, setAIVehicleDistance), AngelScript::asCALL_THISCALL); ROR_ASSERT(result >= 0); result = engine->RegisterObjectMethod("GameScriptClass", "void setAIVehiclePositionScheme(int scheme)", AngelScript::asMETHOD(GameScript, setAIVehiclePositionScheme), AngelScript::asCALL_THISCALL); ROR_ASSERT(result >= 0); From be1920fdd437b6476371da3e5e346913ba78771a Mon Sep 17 00:00:00 2001 From: Petr Ohlidal Date: Fri, 26 Jul 2024 23:20:14 +0200 Subject: [PATCH 10/36] :angel:Script: added `appendTok*()` funcs to GenericDoc API --- doc/angelscript/Script2Game/GenericDocContextClass.h | 8 ++++++++ .../bindings/GenericFileFormatAngelscript.cpp | 10 +++++++++- source/main/utils/GenericFileFormat.cpp | 12 ++++++++++++ source/main/utils/GenericFileFormat.h | 8 ++++++++ 4 files changed, 37 insertions(+), 1 deletion(-) diff --git a/doc/angelscript/Script2Game/GenericDocContextClass.h b/doc/angelscript/Script2Game/GenericDocContextClass.h index 47c6550c21..fdc74d833b 100644 --- a/doc/angelscript/Script2Game/GenericDocContextClass.h +++ b/doc/angelscript/Script2Game/GenericDocContextClass.h @@ -49,8 +49,16 @@ class GenericDocContextClass // Editing functions: + void appendTokens(int count); //!< Appends a series of `TokenType::NONE` and sets Pos at the first one added; use `setTok*` functions to fill them. bool insertToken(int offset = 0); //!< Inserts `TokenType::NONE`; @return false if offset is beyond EOF bool eraseToken(int offset = 0); //!< @return false if offset is beyond EOF + + void appendTokString(const string&in str); + void appendTokFloat(float val); + void appendTokBool(bool val); + void appendTokKeyword(const string&in str); + void appendTokComment(const string&in str); + void appendTokLineBreak(); bool setTokString(int offset, const string&in str); bool setTokFloat(int offset, float val); diff --git a/source/main/scripting/bindings/GenericFileFormatAngelscript.cpp b/source/main/scripting/bindings/GenericFileFormatAngelscript.cpp index 60bd9735e7..d13f2f8117 100644 --- a/source/main/scripting/bindings/GenericFileFormatAngelscript.cpp +++ b/source/main/scripting/bindings/GenericFileFormatAngelscript.cpp @@ -100,14 +100,22 @@ void RoR::RegisterGenericFileFormat(asIScriptEngine* engine) engine->RegisterObjectMethod("GenericDocContextClass", "bool isTokComment(int offset = 0)", asMETHOD(GenericDocContext, isTokComment), asCALL_THISCALL); // > Editing functions: + engine->RegisterObjectMethod("GenericDocContextClass", "void appendTokens(int count)", asMETHOD(GenericDocContext, appendTokens), asCALL_THISCALL); engine->RegisterObjectMethod("GenericDocContextClass", "bool insertToken(int offset = 0)", asMETHOD(GenericDocContext, insertToken), asCALL_THISCALL); engine->RegisterObjectMethod("GenericDocContextClass", "bool eraseToken(int offset = 0)", asMETHOD(GenericDocContext, eraseToken), asCALL_THISCALL); + engine->RegisterObjectMethod("GenericDocContextClass", "void appendTokString(const string &in)", asMETHOD(GenericDocContext, appendTokString), asCALL_THISCALL); + engine->RegisterObjectMethod("GenericDocContextClass", "void appendTokFloat(float)", asMETHOD(GenericDocContext, appendTokFloat), asCALL_THISCALL); + engine->RegisterObjectMethod("GenericDocContextClass", "void appendTokBool(bool)", asMETHOD(GenericDocContext, appendTokBool), asCALL_THISCALL); + engine->RegisterObjectMethod("GenericDocContextClass", "void appendTokKeyword(const string &in)", asMETHOD(GenericDocContext, appendTokKeyword), asCALL_THISCALL); + engine->RegisterObjectMethod("GenericDocContextClass", "void appendTokComment(const string &in)", asMETHOD(GenericDocContext, appendTokComment), asCALL_THISCALL); + engine->RegisterObjectMethod("GenericDocContextClass", "void appendTokLineBreak()", asMETHOD(GenericDocContext, appendTokLineBreak), asCALL_THISCALL); + engine->RegisterObjectMethod("GenericDocContextClass", "bool setTokString(int offset, const string &in)", asMETHOD(GenericDocContext, setTokString), asCALL_THISCALL); engine->RegisterObjectMethod("GenericDocContextClass", "bool setTokFloat(int offset, float)", asMETHOD(GenericDocContext, setTokFloat), asCALL_THISCALL); engine->RegisterObjectMethod("GenericDocContextClass", "bool setTokBool(int offset, bool)", asMETHOD(GenericDocContext, setTokBool), asCALL_THISCALL); engine->RegisterObjectMethod("GenericDocContextClass", "bool setTokKeyword(int offset, const string &in)", asMETHOD(GenericDocContext, setTokKeyword), asCALL_THISCALL); engine->RegisterObjectMethod("GenericDocContextClass", "bool setTokComment(int offset, const string &in)", asMETHOD(GenericDocContext, setTokComment), asCALL_THISCALL); - engine->RegisterObjectMethod("GenericDocContextClass", "bool setTokLineBreak(int offset)", asMETHOD(GenericDocContext, setTokBool), asCALL_THISCALL); + engine->RegisterObjectMethod("GenericDocContextClass", "bool setTokLineBreak(int offset)", asMETHOD(GenericDocContext, setTokLineBreak), asCALL_THISCALL); } diff --git a/source/main/utils/GenericFileFormat.cpp b/source/main/utils/GenericFileFormat.cpp index 898e959ad8..e2b25e1678 100644 --- a/source/main/utils/GenericFileFormat.cpp +++ b/source/main/utils/GenericFileFormat.cpp @@ -1140,6 +1140,18 @@ int GenericDocContext::countLineArgs() // ----------------- // Editing functions +void GenericDocContext::appendTokens(int count) +{ + if (count <= 0) + return; + + token_pos = (int)doc->tokens.size(); + for (int i = 0; i < count; i++) + { + doc->tokens.push_back({ TokenType::NONE, 0.f }); + } +} + bool GenericDocContext::insertToken(int offset) { if (endOfFile(offset)) diff --git a/source/main/utils/GenericFileFormat.h b/source/main/utils/GenericFileFormat.h index 375a6dcb8d..60e5f9c21c 100644 --- a/source/main/utils/GenericFileFormat.h +++ b/source/main/utils/GenericFileFormat.h @@ -126,9 +126,17 @@ struct GenericDocContext: public RefCountingObject // Editing functions: + void appendTokens(int count); //!< Appends a series of `TokenType::NONE` and sets Pos at the first one added; use `setTok*` functions to fill them. bool insertToken(int offset = 0); //!< Inserts `TokenType::NONE`; @return false if offset is beyond EOF bool eraseToken(int offset = 0); //!< @return false if offset is beyond EOF + void appendTokString(const std::string& str) { appendTokens(1); setTokString(0, str); } + void appendTokFloat(float val) { appendTokens(1); setTokFloat(0, val); } + void appendTokBool(bool val) { appendTokens(1); setTokBool(0, val); } + void appendTokKeyword(const std::string& str) { appendTokens(1); setTokKeyword(0, str); } + void appendTokComment(const std::string& str) { appendTokens(1); setTokComment(0, str); } + void appendTokLineBreak() { appendTokens(1); setTokLineBreak(0); } + bool setTokString(int offset, const std::string& str) { return setStringData(offset, TokenType::STRING, str); } bool setTokFloat(int offset, float val) { return setFloatData(offset, TokenType::NUMBER, val); } bool setTokBool(int offset, bool val) { return setFloatData(offset, TokenType::BOOL, val); } From cd222639dc43a03b9b30225153819045865dae2d Mon Sep 17 00:00:00 2001 From: ohlidalp Date: Wed, 27 Nov 2024 03:42:12 +0100 Subject: [PATCH 11/36] :angel:Script: GenericDoc: added forgotten `isTokLineBreak()` --- doc/angelscript/Script2Game/GenericDocContextClass.h | 1 + source/main/scripting/bindings/GenericFileFormatAngelscript.cpp | 1 + 2 files changed, 2 insertions(+) diff --git a/doc/angelscript/Script2Game/GenericDocContextClass.h b/doc/angelscript/Script2Game/GenericDocContextClass.h index fdc74d833b..72406a5f3b 100644 --- a/doc/angelscript/Script2Game/GenericDocContextClass.h +++ b/doc/angelscript/Script2Game/GenericDocContextClass.h @@ -46,6 +46,7 @@ class GenericDocContextClass bool isTokBool(int offset = 0); bool isTokKeyword(int offset = 0); bool isTokComment(int offset = 0); + bool isTokLineBreak(int offset = 0); // Editing functions: diff --git a/source/main/scripting/bindings/GenericFileFormatAngelscript.cpp b/source/main/scripting/bindings/GenericFileFormatAngelscript.cpp index d13f2f8117..caaee576c2 100644 --- a/source/main/scripting/bindings/GenericFileFormatAngelscript.cpp +++ b/source/main/scripting/bindings/GenericFileFormatAngelscript.cpp @@ -98,6 +98,7 @@ void RoR::RegisterGenericFileFormat(asIScriptEngine* engine) engine->RegisterObjectMethod("GenericDocContextClass", "bool isTokBool(int offset = 0)", asMETHOD(GenericDocContext, isTokBool), asCALL_THISCALL); engine->RegisterObjectMethod("GenericDocContextClass", "bool isTokKeyword(int offset = 0)", asMETHOD(GenericDocContext, isTokKeyword), asCALL_THISCALL); engine->RegisterObjectMethod("GenericDocContextClass", "bool isTokComment(int offset = 0)", asMETHOD(GenericDocContext, isTokComment), asCALL_THISCALL); + engine->RegisterObjectMethod("GenericDocContextClass", "bool isTokLineBreak(int offset = 0)", asMETHOD(GenericDocContext, isTokLineBreak), asCALL_THISCALL); // > Editing functions: engine->RegisterObjectMethod("GenericDocContextClass", "void appendTokens(int count)", asMETHOD(GenericDocContext, appendTokens), asCALL_THISCALL); From e4f947741d8962864b3cf691ba434d04049c021d Mon Sep 17 00:00:00 2001 From: ohlidalp Date: Wed, 27 Nov 2024 04:14:45 +0100 Subject: [PATCH 12/36] :angel:Script: race converter now works perfectly. When loaded with a terrain containing races, it detects them and generates race-def files for each of them, also modifying the .terrn2 file. These files can't be processed yet, but they contain all info normally present in the race definition scripts. Example from Auriga: ``` ; ~~ New 'race-def' format (file extension: .race). ~~ ; Each race file specifies a single race ; In .terrn2 file, list the race files under new section [Races] ; Filenames must include extension and end with = (like scripts do) ; Race system supports alternating paths! ; Checkpoint format: checkpointNum(1+), altpathNum(1+), x, y, z, rotX, rotY, rotZ, objName(override, optional) ; By convention, the checkpoint meshes are oriented sideways (facing X axis) race_name 1 kilometer drag race_laps 0.000000 race_checkpoint_object 31-checkpoint race_start_object 31-checkpoint race_finish_object 31-checkpoint begin_checkpoints 1.000000,1.000000,1010.000000,9.000000,505.000000,0.000000,90.000000,0.000000 2.000000,1.000000,10.000000,9.000000,505.000000,0.000000,90.000000,0.000000 end_checkpoints ``` --- .../scripts/example_terrn2_raceConverter.as | 573 ++++++++++++++++-- resources/scripts/races.as | 9 + 2 files changed, 544 insertions(+), 38 deletions(-) diff --git a/resources/scripts/example_terrn2_raceConverter.as b/resources/scripts/example_terrn2_raceConverter.as index 0e0851407f..db13c01904 100644 --- a/resources/scripts/example_terrn2_raceConverter.as +++ b/resources/scripts/example_terrn2_raceConverter.as @@ -1,66 +1,563 @@ -/// \title terrn2 race converter -/// \brief imports races from scripts and generates challenge files. -/// new section in terrn2 format: [Challenges] -/// each line is a tobj-like file with any extension (i.e. *.race) which is loaded by the race system. -/// ================================================== +/// \title terrn2 race converter +/// \brief imports races from scripts and generates race-def files. +/// new section in terrn2 format: [Races] +/// ^ each line is a tobj-like file with any extension (i.e. *.race) which is loaded by the race system. +/// +/// The program flow of this script got a little crazy; +/// I wanted a fluidly updating UI, performing just one step (1 doc conversion / 1 file write) per frame. +/// ================================================== -enum Stage -{ - STAGE_IDLE, - STAGE_PUSHMSG, - STAGE_GENFILES, - STAGE_DONE, - STAGE_ERROR -} -Stage stage = STAGE_IDLE; -string error = ""; +// Window [X] button handler +#include "imgui_utils.as" +imgui_utils::CloseWindowPrompt closeBtnHandler; + +// The race system +#include "races.as" +racesManager races; + +enum Stage // in order of processing +{ + STAGE_INIT, // detects races + STAGE_CONVERT, // converts all races to GenericDocument race-defs + STAGE_IDLE, // Waiting for button press + STAGE_PUSHMSG, // request game to create project + STAGE_GETPROJECT, // fetch created project from modcache + STAGE_GENFILES, // write .race files + STAGE_FIXTERRN2, // modify .terrn2 file - add [Races], remove [Scripts] + STAGE_DONE, + STAGE_ERROR +} +Stage stage = STAGE_INIT; +string error = ""; +string projectName = ""; +CacheEntryClass@ projectEntry; +array convertedRaces; +array convertedAndSavedRaces; // filenames (we write the ' .race' file in separate frame so the user sees the correct filename on screen). +GenericDocumentClass@ g_displayed_document = null; +string g_displayed_doc_filename; +string fileBeingWritten = ""; -void frameStep(float dt) -{ - // === DRAW UI === +void drawUI() +{ + // Draw document window + if (@g_displayed_document != null) + { + drawDocumentWindow(); + } + + drawDetectedRaces(); + ImGui::Separator(); switch(stage) { case STAGE_IDLE: - if (@game.getTerrain() != null - && stage == STAGE_IDLE - && ImGui::Button("Convert races from script to terrn2 [Challenges]")) + { + if (@game.getTerrain() != null && stage == STAGE_IDLE ) { - stage = STAGE_PUSHMSG; - } - break; + if ( ImGui::Button("Convert races from script to terrn2 [Races]")) + { + stage = STAGE_PUSHMSG; + } + } + break; + } case STAGE_ERROR: + { ImGui::Text("ERROR! "+error); break; + } case STAGE_PUSHMSG: + { ImGui::Text("Performing MSG_EDI_CREATE_PROJECT_REQUESTED"); break; + } + case STAGE_GENFILES: + { + ImGui::TextDisabled("Writing .race files:"); + ImGui::Text(fileBeingWritten); + break; + } + case STAGE_DONE: + { + ImGui::Text('DONE'); + break; + } + case STAGE_GETPROJECT: + { + ImGui::Text('WARNING - stuck in stage GETPROJECT (name: "'+projectName+'")'); + break; + } default: + { break; + } } + + +} - // === PERFORM IMPORT STEP === +void drawDetectedRaces() +{ + ImGui::PushID("drawDetectedRaces"); + if (@races == null) + { + ImGui::Text("ERR: races null"); + return; + } + + if (@races.raceList == null) + { + ImGui::Text("ERR raceList null"); + return; + } + + ImGui::Text("Found " + races.raceList.length() + " races"); + for (uint i=0; i < races.raceList.length(); i++) + { + ImGui::PushID(i); + raceBuilder@ race = races.raceList[i]; + if (@race == null) + { + ImGui::Text("ERR racebuilder null!"); + continue; + } + ImGui::Bullet(); + ImGui::Text(race.raceName); + ImGui::SameLine(); + ImGui::TextDisabled(race.checkPointsCount + " checkpoints"); + if (@convertedRaces[i] != null) + { + ImGui::SameLine(); + if ( ImGui::SmallButton("Preview")) + { + g_displayed_doc_filename = race.raceName; + @g_displayed_document = @convertedRaces[i] ; + } + } + ImGui::PopID(); // i + } + ImGui::PopID(); //"drawDetectedRaces" +} + + +void drawDocumentWindow() +{ + ImGui::PushID("document view"); + string caption = "Document view (" + g_displayed_doc_filename + ")"; + bool documentOpen = true; + ImGui::Begin(caption, documentOpen, /*flags:*/0); + + GenericDocContextClass reader(g_displayed_document); + while (!reader.endOfFile()) + { + switch (reader.tokenType()) + { + // These tokens are always at start of line + case TOKEN_TYPE_KEYWORD: + ImGui::TextColored(color(1.f, 1.f, 0.f, 1.f), reader.getTokKeyword()); + break; + case TOKEN_TYPE_COMMENT: + ImGui::TextDisabled(";" + reader.getTokComment()); + break; + + // Linebreak is implicit in DearIMGUI, no action needed + case TOKEN_TYPE_LINEBREAK: + 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(color(0.f, 1.f, 1.f, 1.f), "\"" + reader.getTokString() + "\""); + break; + case TOKEN_TYPE_NUMBER: + ImGui::Text("" + reader.getTokFloat()); + break; + case TOKEN_TYPE_BOOL: + ImGui::TextColored(color(1.f, 0.f, 1.f, 1.f), ""+reader.getTokBool()); + break; + } + } + + reader.moveNext(); + } + + ImGui::End(); + + // Handle window X button + if (!documentOpen) + { + g_displayed_doc_filename = ""; + @g_displayed_document = null; + } + + ImGui::PopID(); //"document view" +} + +void initializeRacesData() +{ + // find the terrain script + array nids = game.getRunningScripts(); + int terrnScriptNid = -1; + for (uint i = 0; i < nids.length(); i++) + { + int nid = nids[i]; + dictionary@ info = game.getScriptDetails(nid); + int category = int(info['scriptCategory']); + if (category == SCRIPT_CATEGORY_TERRAIN) + { + terrnScriptNid = nid; + } + } + + if (terrnScriptNid == -1) + { + stage = STAGE_ERROR; + error = "Terrain script not found!"; + return; + } + + // moment of truth - retrieve the races using new API `game.getScriptVariable()` + int result = game.getScriptVariable(terrnScriptNid, 'races', races); + if (result < 0) + { + stage = STAGE_ERROR; + error = " game.getScriptVariable() returned "+result; + return; + } + + if (@races == null) + { + stage = STAGE_ERROR; + error = " game.getScriptVariable() reported 'OK' but did not fetch the data"; + return; + } + + for (uint i=0; i < races.raceList.length(); i++) + { + raceBuilder@ race = races.raceList[i]; + convertedRaces.insertLast(null); + convertedAndSavedRaces.insertLast("~"); // ~ means "update UI and redner frame first, then do the writing" + } +} + +const string BADCHARS="\\/:%* "; +const string GOODCHAR="_"; +string generateRaceFileName(string raceName) +{ + string filename = raceName + ".race"; + for (uint i=0; i " + filename); + return filename; +} + + +void advanceImportOneStep() +{ switch (stage) { + case STAGE_INIT: + { + initializeRacesData(); + stage = STAGE_CONVERT; + break; + } case STAGE_PUSHMSG: + { + TerrainClass@ terrain = game.getTerrain(); + projectName = terrain.getTerrainName() + " [Races] ~"+thisScript; + + // Fetch terrain's modcache entry + CacheEntryClass@ src_entry = modcache.findEntryByFilename(LOADER_TYPE_TERRAIN, /*partial:*/false, terrain.getTerrainFileName()); + if (@src_entry == null) + { + error = "Not found in modcache!!"; + stage = STAGE_ERROR; + } + + // request project to be created from that cache entry + game.pushMessage(MSG_EDI_CREATE_PROJECT_REQUESTED, { + {'name', projectName}, + {'source_entry', src_entry} + }); + stage = STAGE_GETPROJECT; + break; + } + + case STAGE_GETPROJECT: + { + @projectEntry = modcache.findEntryByFilename(LOADER_TYPE_TERRAIN, /*partial:*/false, projectName+'.terrn2'); + if (@projectEntry != null) + { + stage = STAGE_GENFILES; + } + break; + } + + case STAGE_CONVERT: + { + bool convertedOne = false; + for (uint i=0; i < races.raceList.length(); i++) { - // Fetch terrain's modcache entry - CacheEntryClass@ src_entry = modcache.findEntryByFilename(LOADER_TYPE_TERRAIN, /*partial:*/false, game.getTerrain().getTerrainFileName()); - if (@src_entry == null) + if (@convertedRaces[i] == null) { - error = "Not found in modcache!!"; - stage = STAGE_ERROR; + @convertedRaces[i] = convertSingleRace(races.raceList[i]); + convertedOne = true; + break; } + } - // request project to be created from that cache entry - game.pushMessage(MSG_EDI_CREATE_PROJECT_REQUESTED, { - {'name', game.getTerrain().getTerrainName() + " [Challenges]"}, - {'source_entry', src_entry} - }); - stage = STAGE_GENFILES; - } + if (!convertedOne) + { + stage = STAGE_IDLE; + } break; + } + + case STAGE_GENFILES: + { + for (uint i=0; i < races.raceList.length(); i++) + { + if (convertedAndSavedRaces[i] == "~") + { + string filename = generateRaceFileName(races.raceList[i].raceName); + fileBeingWritten = filename; + convertedAndSavedRaces[i] =""; // ready to write file + // game.log('DBG GENFILES ['+i+']: fileBeingWritten='+fileBeingWritten); + break; + } + else if (convertedAndSavedRaces[i] == "") + { + // write the ' .race' file in separate frame so the user sees the correct filename on screen. + if (@projectEntry == null) + { + stage=STAGE_ERROR; + error="null project entry while generating files"; + break; + } + if (@convertedRaces[i] == null) + { + stage=STAGE_ERROR; + error="null converted race at position ["+i+"]"; + break; + } + + convertedRaces[i].saveToResource(fileBeingWritten, projectEntry.resource_group); + convertedAndSavedRaces[i] = fileBeingWritten; + // game.log('DBG GENFILES ['+i+']: saved file '+fileBeingWritten); + if (i == races.raceList.length()-1) + { + stage=STAGE_FIXTERRN2; + } + break; + } + } + break; + } + + case STAGE_FIXTERRN2: + { + fixupTerrn2Document(); + stage = STAGE_DONE; + break; + } + default: - break; + break; + } +} + +string BRACE="["; +void fixupTerrn2Document() +{ + GenericDocumentClass terrn2; + int flags = 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; + terrn2.loadFromResource(projectName+".terrn2", projectEntry.resource_group, flags); + GenericDocContextClass ctx(terrn2); + + // Delete section [Scripts] and all it contains + bool inSectionScripts = false; + while (!ctx.endOfFile()) + { + // entering section [Scripts] + if (!inSectionScripts && ctx.isTokKeyword() && ctx.getTokKeyword()=="[Scripts]") + { + inSectionScripts=true; + } + // leaving section [Scripts] + else if (inSectionScripts && ctx.isTokKeyword() && ctx.getTokKeyword()[0]==BRACE[0]) + { + + inSectionScripts=false; + } + + // erase all script info + if (inSectionScripts) + { + ctx.eraseToken(); + } + // ... and also fix up the terrain name while at it. + else if (ctx.isTokKeyword() && ctx.getTokKeyword() == "Name") + { + ctx.setTokString(1, projectName); + // make it a little less obvious that GenericDocument actually breaks the terrain name into pieces + // ^ just hard erase everything between the new name and a linebreak + ctx.moveNext(); + ctx.moveNext(); + while (!ctx.isTokLineBreak()) + { + ctx.eraseToken(); + } + } + ctx.seekNextLine(); + } + + // Append section [Races] + ctx.appendTokLineBreak(); + ctx.appendTokKeyword("[Races]"); + ctx.appendTokLineBreak(); + for (uint i=0; i < convertedAndSavedRaces.length(); i++) + { + ctx.appendTokKeyword(convertedAndSavedRaces[i]); + ctx.appendTokLineBreak(); + } + + forceExportINI(terrn2); + + // delete original file (GenericDocument cannot overwrite) + game.deleteResource(projectName+".terrn2", projectEntry.resource_group); + + // write out modified file + terrn2.saveToResource(projectName+".terrn2", projectEntry.resource_group); +} + +void forceExportINI(GenericDocumentClass@ doc) +{ + // hack to force GenericDocument export INI: pass over all non-braced keywords and append = to them. + GenericDocContextClass ctx(doc); //reset context + while (!ctx.endOfFile()) + { + if (ctx.isTokKeyword() && ctx.getTokKeyword()[0]!=BRACE[0] ) + { + ctx.setTokKeyword(0,ctx.getTokKeyword()+"="); + } + else if (ctx.isTokString() && ctx.isTokLineBreak()) + { + // treat strings on line start as keywords (those are keywords that decayed to strings due to some chars) + ctx.setTokKeyword(0,ctx.getTokString()+"="); + } + ctx.moveNext(); + } +} + +void appendKeyValuePair( GenericDocContextClass@ ctx, string key, string value) +{ + ctx.appendTokKeyword( key); + ctx.appendTokString(value); + ctx.appendTokLineBreak(); +} + +void appendKeyValuePair( GenericDocContextClass@ ctx, string key, int value) +{ + ctx.appendTokKeyword( key); + ctx.appendTokFloat(value); + ctx.appendTokLineBreak(); +} + + +GenericDocumentClass@ convertSingleRace(raceBuilder@ race) +{ + GenericDocumentClass doc; + GenericDocContextClass ctx(doc); + + ctx.appendTokComment( " ~~ New 'race-def' format (file extension: .race). ~~");ctx.appendTokLineBreak(); + ctx.appendTokComment( " Each race file specifies a single race");ctx.appendTokLineBreak(); + ctx.appendTokComment( " In .terrn2 file, list the race files under new section [Races]");ctx.appendTokLineBreak(); + ctx.appendTokComment( " Filenames must include extension and end with = (like scripts do)");ctx.appendTokLineBreak(); + ctx.appendTokComment( " Race system supports alternating paths!");ctx.appendTokLineBreak(); + ctx.appendTokComment( " Checkpoint format: checkpointNum(1+), altpathNum(1+), x, y, z, rotX, rotY, rotZ, objName(override, optional)");ctx.appendTokLineBreak(); + ctx.appendTokComment( " By convention, the checkpoint meshes are oriented sideways (facing X axis)");ctx.appendTokLineBreak(); + ctx.appendTokLineBreak(); + + appendKeyValuePair(ctx, "race_name", race.raceName); + appendKeyValuePair(ctx, "race_laps", race.laps); + appendKeyValuePair(ctx, "race_checkpoint_object", race.exporterCheckpointObjName); + appendKeyValuePair(ctx, "race_start_object", race.exporterStartObjName); + appendKeyValuePair(ctx, "race_finish_object", race.exporterFinishObjName); + + ctx.appendTokLineBreak(); + + ctx.appendTokKeyword( "begin_checkpoints"); + + ctx.appendTokLineBreak(); + + for (uint i=0; i < race.checkpoints.length(); i++) + { + uint numAltPaths = race.getRealInstanceCount(int(i)); + for (uint j = 0; j < numAltPaths; j++) + { + ctx.appendTokFloat((i+1)); // checkpointNum (1+) + ctx.appendTokFloat((j+1)); // altpathNum (1+) + const double[] args = race.checkpoints[i][j]; + ctx.appendTokFloat(args[0]); // pos X + ctx.appendTokFloat(args[1]); // pos Y + ctx.appendTokFloat(args[2]); // pos Z + ctx.appendTokFloat(args[3]); // rot X + ctx.appendTokFloat(args[4]); // rot Y + ctx.appendTokFloat(args[5]); // rot Z + string defaultObjName = (i==0) + ? race.exporterStartObjName + : (i==race.checkpoints.length()-1) ? race.exporterFinishObjName : race.exporterCheckpointObjName; + string actualObjName = race.objNames[i][j]; + if (actualObjName != defaultObjName) + { + ctx.appendTokString(actualObjName); + } + ctx.appendTokLineBreak(); + } } + + ctx.appendTokKeyword( "end_checkpoints"); + ctx.appendTokLineBreak(); + + return doc; } + +void frameStep(float dt) +{ + // === DRAW UI === + if ( ImGui::Begin("Race import", closeBtnHandler.windowOpen, /*flags:*/0)) + { + // Draw the "Terminate this script?" prompt on the top (if not disabled by config). + closeBtnHandler.draw(); + drawUI(); + ImGui::End(); + } + + + // === PERFORM IMPORT STEP === + advanceImportOneStep(); +} + diff --git a/resources/scripts/races.as b/resources/scripts/races.as index 2759ac0e4d..f0a191327a 100644 --- a/resources/scripts/races.as +++ b/resources/scripts/races.as @@ -1160,6 +1160,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; raceBuilder(int id) { @@ -1232,6 +1236,11 @@ shared class raceBuilder { 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; } int getNextCheckpointNum(int lastCheckpoint) From 68f37dfa54d6eaa1bcc568913c2bd27b45a2a05d Mon Sep 17 00:00:00 2001 From: Petr Ohlidal Date: Mon, 14 Oct 2024 22:50:37 +0200 Subject: [PATCH 13/36] GenericDocument: Added `INT` token type (+Doc!). --- .../Script2Game/GenericDocContextClass.h | 7 ++++++- .../bindings/GenericFileFormatAngelscript.cpp | 7 ++++++- source/main/utils/GenericFileFormat.cpp | 21 ++++++++++++++++--- source/main/utils/GenericFileFormat.h | 11 +++++++--- 4 files changed, 38 insertions(+), 8 deletions(-) diff --git a/doc/angelscript/Script2Game/GenericDocContextClass.h b/doc/angelscript/Script2Game/GenericDocContextClass.h index 72406a5f3b..e6ee20a68a 100644 --- a/doc/angelscript/Script2Game/GenericDocContextClass.h +++ b/doc/angelscript/Script2Game/GenericDocContextClass.h @@ -14,7 +14,8 @@ enum TokenType TOKEN_TYPE_LINEBREAK, TOKEN_TYPE_COMMENT, TOKEN_TYPE_STRING, - TOKEN_TYPE_NUMBER, + TOKEN_TYPE_FLOAT, + TOKEN_TYPE_INT, TOKEN_TYPE_BOOL, TOKEN_TYPE_KEYWORD }; @@ -37,12 +38,14 @@ class GenericDocContextClass string getTokString(int offset = 0); float getTokFloat(int offset = 0); + float getTokInt(int offset = 0); bool gettokBool(int offset = 0); string getTokKeyword(int offset = 0); string getTokComment(int offset = 0); bool isTokString(int offset = 0); bool isTokFloat(int offset = 0); + bool isTokInt(int offset = 0); bool isTokBool(int offset = 0); bool isTokKeyword(int offset = 0); bool isTokComment(int offset = 0); @@ -56,6 +59,7 @@ class GenericDocContextClass void appendTokString(const string&in str); void appendTokFloat(float val); + void appendTokInt(float val); void appendTokBool(bool val); void appendTokKeyword(const string&in str); void appendTokComment(const string&in str); @@ -63,6 +67,7 @@ class GenericDocContextClass bool setTokString(int offset, const string&in str); bool setTokFloat(int offset, float val); + bool setTokInt(int offset, int val); bool setTokBool(int offset, bool val); bool setTokKeyword(int offset, const string&in str); bool setTokComment(int offset, const string&in str); diff --git a/source/main/scripting/bindings/GenericFileFormatAngelscript.cpp b/source/main/scripting/bindings/GenericFileFormatAngelscript.cpp index caaee576c2..54cc6ca9cc 100644 --- a/source/main/scripting/bindings/GenericFileFormatAngelscript.cpp +++ b/source/main/scripting/bindings/GenericFileFormatAngelscript.cpp @@ -48,7 +48,8 @@ void RoR::RegisterGenericFileFormat(asIScriptEngine* engine) engine->RegisterEnumValue("TokenType", "TOKEN_TYPE_LINEBREAK", (int)TokenType::LINEBREAK); engine->RegisterEnumValue("TokenType", "TOKEN_TYPE_COMMENT", (int)TokenType::COMMENT); engine->RegisterEnumValue("TokenType", "TOKEN_TYPE_STRING", (int)TokenType::STRING); - engine->RegisterEnumValue("TokenType", "TOKEN_TYPE_NUMBER", (int)TokenType::NUMBER); + engine->RegisterEnumValue("TokenType", "TOKEN_TYPE_FLOAT", (int)TokenType::FLOAT); + engine->RegisterEnumValue("TokenType", "TOKEN_TYPE_INT", (int)TokenType::INT); engine->RegisterEnumValue("TokenType", "TOKEN_TYPE_BOOL", (int)TokenType::BOOL); engine->RegisterEnumValue("TokenType", "TOKEN_TYPE_KEYWORD", (int)TokenType::KEYWORD); @@ -89,12 +90,14 @@ void RoR::RegisterGenericFileFormat(asIScriptEngine* engine) engine->RegisterObjectMethod("GenericDocContextClass", "string getTokString(int offset = 0)", asMETHOD(GenericDocContext, getTokString), asCALL_THISCALL); engine->RegisterObjectMethod("GenericDocContextClass", "float getTokFloat(int offset = 0)", asMETHOD(GenericDocContext, getTokFloat), asCALL_THISCALL); + engine->RegisterObjectMethod("GenericDocContextClass", "int getTokInt(int offset = 0)", asMETHOD(GenericDocContext, getTokInt), asCALL_THISCALL); engine->RegisterObjectMethod("GenericDocContextClass", "bool getTokBool(int offset = 0)", asMETHOD(GenericDocContext, getTokBool), asCALL_THISCALL); engine->RegisterObjectMethod("GenericDocContextClass", "string getTokKeyword(int offset = 0)", asMETHOD(GenericDocContext, getTokKeyword), asCALL_THISCALL); engine->RegisterObjectMethod("GenericDocContextClass", "string getTokComment(int offset = 0)", asMETHOD(GenericDocContext, getTokComment), asCALL_THISCALL); engine->RegisterObjectMethod("GenericDocContextClass", "bool isTokString(int offset = 0)", asMETHOD(GenericDocContext, isTokString), asCALL_THISCALL); engine->RegisterObjectMethod("GenericDocContextClass", "bool isTokFloat(int offset = 0)", asMETHOD(GenericDocContext, isTokFloat), asCALL_THISCALL); + engine->RegisterObjectMethod("GenericDocContextClass", "bool isTokInt(int offset = 0)", asMETHOD(GenericDocContext, isTokInt), asCALL_THISCALL); engine->RegisterObjectMethod("GenericDocContextClass", "bool isTokBool(int offset = 0)", asMETHOD(GenericDocContext, isTokBool), asCALL_THISCALL); engine->RegisterObjectMethod("GenericDocContextClass", "bool isTokKeyword(int offset = 0)", asMETHOD(GenericDocContext, isTokKeyword), asCALL_THISCALL); engine->RegisterObjectMethod("GenericDocContextClass", "bool isTokComment(int offset = 0)", asMETHOD(GenericDocContext, isTokComment), asCALL_THISCALL); @@ -107,6 +110,7 @@ void RoR::RegisterGenericFileFormat(asIScriptEngine* engine) engine->RegisterObjectMethod("GenericDocContextClass", "void appendTokString(const string &in)", asMETHOD(GenericDocContext, appendTokString), asCALL_THISCALL); engine->RegisterObjectMethod("GenericDocContextClass", "void appendTokFloat(float)", asMETHOD(GenericDocContext, appendTokFloat), asCALL_THISCALL); + engine->RegisterObjectMethod("GenericDocContextClass", "void appendTokInt(int)", asMETHOD(GenericDocContext, appendTokInt), asCALL_THISCALL); engine->RegisterObjectMethod("GenericDocContextClass", "void appendTokBool(bool)", asMETHOD(GenericDocContext, appendTokBool), asCALL_THISCALL); engine->RegisterObjectMethod("GenericDocContextClass", "void appendTokKeyword(const string &in)", asMETHOD(GenericDocContext, appendTokKeyword), asCALL_THISCALL); engine->RegisterObjectMethod("GenericDocContextClass", "void appendTokComment(const string &in)", asMETHOD(GenericDocContext, appendTokComment), asCALL_THISCALL); @@ -114,6 +118,7 @@ void RoR::RegisterGenericFileFormat(asIScriptEngine* engine) engine->RegisterObjectMethod("GenericDocContextClass", "bool setTokString(int offset, const string &in)", asMETHOD(GenericDocContext, setTokString), asCALL_THISCALL); engine->RegisterObjectMethod("GenericDocContextClass", "bool setTokFloat(int offset, float)", asMETHOD(GenericDocContext, setTokFloat), asCALL_THISCALL); + engine->RegisterObjectMethod("GenericDocContextClass", "bool setTokInt(int offset, int)", asMETHOD(GenericDocContext, setTokInt), asCALL_THISCALL); engine->RegisterObjectMethod("GenericDocContextClass", "bool setTokBool(int offset, bool)", asMETHOD(GenericDocContext, setTokBool), asCALL_THISCALL); engine->RegisterObjectMethod("GenericDocContextClass", "bool setTokKeyword(int offset, const string &in)", asMETHOD(GenericDocContext, setTokKeyword), asCALL_THISCALL); engine->RegisterObjectMethod("GenericDocContextClass", "bool setTokComment(int offset, const string &in)", asMETHOD(GenericDocContext, setTokComment), asCALL_THISCALL); diff --git a/source/main/utils/GenericFileFormat.cpp b/source/main/utils/GenericFileFormat.cpp index e2b25e1678..1b635be765 100644 --- a/source/main/utils/GenericFileFormat.cpp +++ b/source/main/utils/GenericFileFormat.cpp @@ -916,7 +916,14 @@ void DocumentParser::FlushStringishToken(RoR::TokenType type) void DocumentParser::FlushNumericToken() { tok.push_back('\0'); - doc.tokens.push_back({ TokenType::NUMBER, (float)Ogre::StringConverter::parseReal(tok.data()) }); + if (partial_tok_type == PartialToken::NUMBER_INTEGER) + { + doc.tokens.push_back({ TokenType::INT, (float)Ogre::StringConverter::parseInt(tok.data()) }); + } + else + { + doc.tokens.push_back({ TokenType::FLOAT, (float)Ogre::StringConverter::parseReal(tok.data()) }); + } tok.clear(); partial_tok_type = PartialToken::NONE; } @@ -1057,13 +1064,20 @@ void GenericDocument::saveToDataStream(Ogre::DataStreamPtr datastream) separator = ","; break; - case TokenType::NUMBER: + case TokenType::FLOAT: datastream->write(separator.data(), separator.size()); snprintf(buf, BUF_MAX, "%f", tok.data); datastream->write(buf, strlen(buf)); separator = ","; break; + case TokenType::INT: + datastream->write(separator.data(), separator.size()); + snprintf(buf, BUF_MAX, "%d", (int)tok.data); + datastream->write(buf, strlen(buf)); + separator = ","; + break; + case TokenType::BOOL: datastream->write(separator.data(), separator.size()); snprintf(buf, BUF_MAX, "%s", tok.data == 1.f ? "true" : "false"); @@ -1119,9 +1133,10 @@ bool GenericDocContext::seekNextLine() { this->moveNext(); } + this->moveNext(); // Skip comments and empty lines - while (!this->endOfFile() && !this->isTokString() && !this->isTokFloat() && !this->isTokBool() && !this->isTokKeyword()) + while (!this->endOfFile() && (this->isTokComment(0) || this->isTokLineBreak(0))) { this->moveNext(); } diff --git a/source/main/utils/GenericFileFormat.h b/source/main/utils/GenericFileFormat.h index 60e5f9c21c..0d9eb86631 100644 --- a/source/main/utils/GenericFileFormat.h +++ b/source/main/utils/GenericFileFormat.h @@ -51,7 +51,8 @@ enum class TokenType LINEBREAK, // Input: LF (CR is ignored); Output: platform-specific. COMMENT, // Line starting with ; (skipping whitespace). Data: offset in string pool. STRING, // Quoted string. Data: offset in string pool. - NUMBER, // Float. + FLOAT, + INT, BOOL, // Lowercase 'true'/'false'. Data: 1.0 for true, 0.0 for false. KEYWORD, // Unquoted string at start of line (skipping whitespace). Data: offset in string pool. }; @@ -113,12 +114,14 @@ struct GenericDocContext: public RefCountingObject std::string getTokString(int offset = 0) const { ROR_ASSERT(isTokString(offset)); return getStringData(offset); } float getTokFloat(int offset = 0) const { ROR_ASSERT(isTokFloat(offset)); return getFloatData(offset); } + int getTokInt(int offset = 0) const { ROR_ASSERT(isTokInt(offset)); return (int)getFloatData(offset); } bool getTokBool(int offset = 0) const { ROR_ASSERT(isTokBool(offset)); return getFloatData(offset) == 1.f; } std::string getTokKeyword(int offset = 0) const { ROR_ASSERT(isTokKeyword(offset)); return getStringData(offset); } std::string getTokComment(int offset = 0) const { ROR_ASSERT(isTokComment(offset)); return getStringData(offset); } bool isTokString(int offset = 0) const { return tokenType(offset) == TokenType::STRING; } - bool isTokFloat(int offset = 0) const { return tokenType(offset) == TokenType::NUMBER; } + bool isTokFloat(int offset = 0) const { return tokenType(offset) == TokenType::FLOAT; } + bool isTokInt(int offset = 0) const { return tokenType(offset) == TokenType::INT; } bool isTokBool(int offset = 0) const { return tokenType(offset) == TokenType::BOOL; } bool isTokKeyword(int offset = 0) const { return tokenType(offset) == TokenType::KEYWORD; } bool isTokComment(int offset = 0) const { return tokenType(offset) == TokenType::COMMENT; } @@ -132,13 +135,15 @@ struct GenericDocContext: public RefCountingObject void appendTokString(const std::string& str) { appendTokens(1); setTokString(0, str); } void appendTokFloat(float val) { appendTokens(1); setTokFloat(0, val); } + void appendTokInt(int val) { appendTokens(1); setTokInt(0, val); } void appendTokBool(bool val) { appendTokens(1); setTokBool(0, val); } void appendTokKeyword(const std::string& str) { appendTokens(1); setTokKeyword(0, str); } void appendTokComment(const std::string& str) { appendTokens(1); setTokComment(0, str); } void appendTokLineBreak() { appendTokens(1); setTokLineBreak(0); } bool setTokString(int offset, const std::string& str) { return setStringData(offset, TokenType::STRING, str); } - bool setTokFloat(int offset, float val) { return setFloatData(offset, TokenType::NUMBER, val); } + bool setTokFloat(int offset, float val) { return setFloatData(offset, TokenType::FLOAT, val); } + bool setTokInt(int offset, int val) { return setFloatData(offset, TokenType::INT, val); } bool setTokBool(int offset, bool val) { return setFloatData(offset, TokenType::BOOL, val); } bool setTokKeyword(int offset, const std::string& str) { return setStringData(offset, TokenType::KEYWORD, str); } bool setTokComment(int offset, const std::string& str) { return setStringData(offset, TokenType::COMMENT, str); } From a82edfa1ba3b8d7a2bfb8fdb3d1ac5a38b66105e Mon Sep 17 00:00:00 2001 From: ohlidalp Date: Wed, 27 Nov 2024 15:29:34 +0100 Subject: [PATCH 14/36] :angel:Script: GenericDoc examples moved from 'demo' to 'uniEditor' Since adding INT/FLOAT and removing NUMBER token types from GenericDocument, all copypasted viewers/editors in scripts are broken. This is 1st step to creating an unified viewer/editor as include, but not yet there. --- resources/scripts/demo_script.as | 380 ++++------------ .../scripts/example_GenericDocument_editor.as | 332 -------------- .../example_GenericDocument_uniEditor.as | 416 ++++++++++++++++++ 3 files changed, 513 insertions(+), 615 deletions(-) delete mode 100644 resources/scripts/example_GenericDocument_editor.as create mode 100644 resources/scripts/example_GenericDocument_uniEditor.as diff --git a/resources/scripts/demo_script.as b/resources/scripts/demo_script.as index 40db0b5fdd..ff23b81a5c 100644 --- a/resources/scripts/demo_script.as +++ b/resources/scripts/demo_script.as @@ -1,41 +1,41 @@ /* - --------------------------------------------------------------------------- - Project Rigs of Rods (www.rigsofrods.org) - - DEMO SCRIPT - - This program showcases all the various things you can do using scripting: - * Use DearIMGUI to draw UI of any kind, including diagnostic views. - * Collect and show stats (i.e. frame count, total time) - * Read/Write cvars (RoR.cfg values, cli args, game state...) - * View and update game state (current vehicle...) - * Parse and display definition files with syntax highlighting. - * Load and write text files in the resource system. - * Inspect loaded sounds and soundscript templates, and of course play sounds! - * Post messages to game's main message queue, performing almost any operation. - * Query list of resource files (filtered using pattern) from OGRE resource groups. - * Add custom icons to survey map. - - To invoke this script, open in-game console and say `loadscript demo_script.as`. - Alternatively, you can run the game with parameter '-runscript '. - You can use this command multiple times at once. - Alternatively, you can set 'app_custom_scripts' in RoR.cfg. - It's a comma-separated list, spaces in filenames are acceptable. - - For introduction to game events, read - https://docs.rigsofrods.org/terrain-creation/scripting/. - - Scripting documentation: - https://developer.rigsofrods.org/d4/d07/group___script2_game.html - - --------------------------------------------------------------------------- +--------------------------------------------------------------------------- +Project Rigs of Rods (www.rigsofrods.org) + +DEMO SCRIPT + +This program showcases all the various things you can do using scripting: +* Use DearIMGUI to draw UI of any kind, including diagnostic views. +* Collect and show stats (i.e. frame count, total time) +* Read/Write cvars (RoR.cfg values, cli args, game state...) +* View and update game state (current vehicle...) +* Parse and display definition files with syntax highlighting. ==> moved to 'example_GenericDocument_uniEditor.as' +* Load and write text files in the resource system. +* Inspect loaded sounds and soundscript templates, and of course play sounds! +* Post messages to game's main message queue, performing almost any operation. +* Query list of resource files (filtered using pattern) from OGRE resource groups. +* Add custom icons to survey map. + +To invoke this script, open in-game console and say `loadscript demo_script.as`. +Alternatively, you can run the game with parameter '-runscript '. +You can use this command multiple times at once. +Alternatively, you can set 'app_custom_scripts' in RoR.cfg. +It's a comma-separated list, spaces in filenames are acceptable. + +For introduction to game events, read +https://docs.rigsofrods.org/terrain-creation/scripting/. + +Scripting documentation: +https://developer.rigsofrods.org/d4/d07/group___script2_game.html + +--------------------------------------------------------------------------- */ #include "imgui_utils.as" /* - --------------------------------------------------------------------------- - Global variables +--------------------------------------------------------------------------- +Global variables */ int g_total_frames = 0; float g_total_seconds = 0; @@ -54,9 +54,6 @@ string g_demofile_data; // Main window state imgui_utils::CloseWindowPrompt closeBtnHandler; -// Document window state -GenericDocumentClass@ g_displayed_document = null; -string g_displayed_doc_filename; // tab settings bool demotabsReorderable = false; @@ -70,8 +67,8 @@ const string RG_NAME = "Scripts"; const string RG_PATTERN = "demo*.*"; /* - --------------------------------------------------------------------------- - Script setup function - invoked once when script is loaded. +--------------------------------------------------------------------------- +Script setup function - invoked once when script is loaded. */ void main() { @@ -79,9 +76,9 @@ void main() } /* - --------------------------------------------------------------------------- - Script update function - invoked by the game once every rendered frame, - with elapsed time (delta time, in seconds) as parameter. +--------------------------------------------------------------------------- +Script update function - invoked by the game once every rendered frame, +with elapsed time (delta time, in seconds) as parameter. */ void frameStep(float dt) { @@ -92,7 +89,7 @@ void frameStep(float dt) // show some stats ImGui::Text("Total frames: " + g_total_frames); ImGui::Text("Total time: " + int(g_total_seconds / 60) + "min, " - + int(g_total_seconds % 60) + "sec"); + + int(g_total_seconds % 60) + "sec"); // Show some game context if (g_app_state.getInt() == 1) // main menu @@ -130,7 +127,7 @@ void frameStep(float dt) ImGui::Text("(terrain edit)"); } - drawTerrainButtons(); + ImGui::TextDisabled("Camera controls:"); ImGui::Text("Change camera: " + inputs.getEventCommandTrimmed(EV_CAMERA_CHANGE)); @@ -144,39 +141,14 @@ void frameStep(float dt) ImGui::PushID("actor"); ImGui::AlignTextToFramePadding(); ImGui::Text("You are driving " + actor.getTruckName()); - ImGui::SameLine(); - if (@g_displayed_document == null) - { - if (ImGui::Button("View document")) - { - GenericDocumentClass@ doc = GenericDocumentClass(); - int flags = GENERIC_DOCUMENT_OPTION_ALLOW_NAKED_STRINGS - | GENERIC_DOCUMENT_OPTION_ALLOW_SLASH_COMMENTS - | GENERIC_DOCUMENT_OPTION_FIRST_LINE_IS_TITLE - | GENERIC_DOCUMENT_OPTION_ALLOW_SEPARATOR_COLON - | GENERIC_DOCUMENT_OPTION_PARENTHESES_CAPTURE_SPACES; - if (doc.loadFromResource(actor.getTruckFileName(), actor.getTruckFileResourceGroup(), flags)) - { - @g_displayed_document = @doc; - g_displayed_doc_filename = actor.getTruckFileName(); - } - } - } - else - { - if (ImGui::Button("Close document")) - { - @g_displayed_document = null; - g_displayed_doc_filename = ""; - } - } + ImGui::PopID(); //"actor" ImGui::TextDisabled("Vehicle controls:"); - + ImGui::Text("Accelerate/Brake: " - + inputs.getEventCommandTrimmed(EV_TRUCK_ACCELERATE) + "/" - + inputs.getEventCommandTrimmed(EV_TRUCK_BRAKE)); + + inputs.getEventCommandTrimmed(EV_TRUCK_ACCELERATE) + "/" + + inputs.getEventCommandTrimmed(EV_TRUCK_BRAKE)); if (g_io_arcade_controls.getBool() == true) { ImGui::Text("Arcade controls are enabled (?)"); @@ -201,9 +173,9 @@ void frameStep(float dt) } ImGui::Text("Steer left/right: " - + inputs.getEventCommandTrimmed(EV_TRUCK_STEER_LEFT) + "/" - + inputs.getEventCommandTrimmed(EV_TRUCK_STEER_RIGHT)); - + + inputs.getEventCommandTrimmed(EV_TRUCK_STEER_LEFT) + "/" + + inputs.getEventCommandTrimmed(EV_TRUCK_STEER_RIGHT)); + drawActorAngles(actor); } @@ -212,11 +184,11 @@ void frameStep(float dt) ImGui::Text("You are on foot"); ImGui::TextDisabled("Character controls:"); ImGui::Text("Forward/Backward: " - + inputs.getEventCommandTrimmed(EV_CHARACTER_FORWARD) + "/" - + inputs.getEventCommandTrimmed(EV_CHARACTER_BACKWARDS)); + + inputs.getEventCommandTrimmed(EV_CHARACTER_FORWARD) + "/" + + inputs.getEventCommandTrimmed(EV_CHARACTER_BACKWARDS)); ImGui::Text("Turn left/right: " - + inputs.getEventCommandTrimmed(EV_CHARACTER_LEFT) + "/" - + inputs.getEventCommandTrimmed(EV_CHARACTER_RIGHT)); + + inputs.getEventCommandTrimmed(EV_CHARACTER_LEFT) + "/" + + inputs.getEventCommandTrimmed(EV_CHARACTER_RIGHT)); ImGui::Text("Run: " + inputs.getEventCommandTrimmed(EV_CHARACTER_RUN)); ImGui::Separator(); @@ -234,190 +206,32 @@ void frameStep(float dt) // End window ImGui::End(); - + // Update global counters g_total_frames++; g_total_seconds += dt; - // Draw document window - if (@g_displayed_document != null) - { - drawDocumentWindow(); - } -} - -void drawTerrainButtons() -{ - // Terrain name (with "view document" button) - ImGui::PushID("terrn"); - TerrainClass@ terrain = game.getTerrain(); - ImGui::AlignTextToFramePadding(); - ImGui::Text("Terrain: " + terrain.getTerrainName()); - ImGui::SameLine(); - if (@g_displayed_document == null) - { - if (ImGui::Button("View document")) - { - GenericDocumentClass@ doc = GenericDocumentClass(); - int flags = 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; - if (doc.loadFromResource(terrain.getTerrainFileName(), terrain.getTerrainFileResourceGroup(), flags)) - { - @g_displayed_document = @doc; - g_displayed_doc_filename = terrain.getTerrainFileName(); - - // Fetch TOBJ filenames - if (g_terrain_tobj_files.length() == 0) - { - GenericDocContextClass@ reader = GenericDocContextClass(doc); - bool in_section_objects = false; - while (!reader.endOfFile()) - { - if (reader.tokenType() == TOKEN_TYPE_KEYWORD && reader.getTokKeyword().substr(0, 1) == "[") - { - in_section_objects = (reader.getTokKeyword() == '[Objects]'); - } - else if (reader.tokenType() == TOKEN_TYPE_STRING && in_section_objects) - { - // Note: in GenericDocument, a text on line start is always a KEYWORD token, - // but KEYWORDs must not contain special characters, - // so file names always decay to strings because of '.'. - g_terrain_tobj_files.insertLast(reader.getTokString()); - } - reader.moveNext(); - } - } - } - } - } - else - { - if (ImGui::Button("Close document")) - { - @g_displayed_document = null; - g_displayed_doc_filename = ""; - } - } - - // TOBJ files - ImGui::PushID("tobj"); - for (uint i = 0; i < g_terrain_tobj_files.length(); i++) - { - ImGui::PushID(i); - ImGui::AlignTextToFramePadding(); - ImGui::Bullet(); - ImGui::SameLine(); - ImGui::Text(g_terrain_tobj_files[i]); - ImGui::SameLine(); - if (@g_displayed_document == null) - { - if (ImGui::Button("View document")) - { - GenericDocumentClass@ doc = GenericDocumentClass(); - int flags = GENERIC_DOCUMENT_OPTION_ALLOW_NAKED_STRINGS - | GENERIC_DOCUMENT_OPTION_ALLOW_SLASH_COMMENTS; - if (doc.loadFromResource(g_terrain_tobj_files[i], terrain.getTerrainFileResourceGroup(), flags)) - { - @g_displayed_document = @doc; - g_displayed_doc_filename = g_terrain_tobj_files[i]; - } - } - } - else - { - if (ImGui::Button("Close document")) - { - @g_displayed_document = null; - g_displayed_doc_filename = ""; - } - } - ImGui::PopID(); // i - } - ImGui::PopID(); //"tobj" - - ImGui::PopID(); //"terrn" + } -void drawDocumentWindow() -{ - ImGui::PushID("document view"); - string caption = "Document view (" + g_displayed_doc_filename + ")"; - bool docWindowOpen = true; - ImGui::Begin(caption, docWindowOpen, /*flags:*/0); - - GenericDocContextClass reader(g_displayed_document); - while (!reader.endOfFile()) - { - switch (reader.tokenType()) - { - // These tokens are always at start of line - case TOKEN_TYPE_KEYWORD: - ImGui::TextColored(color(1.f, 1.f, 0.f, 1.f), reader.getTokKeyword()); - break; - case TOKEN_TYPE_COMMENT: - ImGui::TextDisabled(";" + reader.getTokComment()); - break; - - // Linebreak is implicit in DearIMGUI, no action needed - case TOKEN_TYPE_LINEBREAK: - 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(color(0.f, 1.f, 1.f, 1.f), "\"" + reader.getTokString() + "\""); - break; - case TOKEN_TYPE_NUMBER: - ImGui::Text("" + reader.getTokFloat()); - break; - case TOKEN_TYPE_BOOL: - ImGui::TextColored(color(1.f, 0.f, 1.f, 1.f), ""+reader.getTokBool()); - break; - } - } - - reader.moveNext(); - } - - ImGui::End(); - if (!docWindowOpen) - { - @g_displayed_document = null; - g_displayed_doc_filename = ""; - } - - ImGui::PopID(); //"document view" -} + + void drawMainMenuPanel() { ImGui::Text("Game state: main menu"); ImGui::Text("Pro tip: Press '" - + inputs.getEventCommandTrimmed(EV_COMMON_CONSOLE_TOGGLE) - + "' to open console anytime."); - + + inputs.getEventCommandTrimmed(EV_COMMON_CONSOLE_TOGGLE) + + "' to open console anytime."); + // Test message queue if (ImGui::Button("Launch simple test terrain")) { - game.pushMessage(MSG_SIM_LOAD_TERRN_REQUESTED, {{'filename', 'simple2.terrn2'}}); +game.pushMessage(MSG_SIM_LOAD_TERRN_REQUESTED, {{'filename', 'simple2.terrn2'}}); } - + // Reset simulation data - @g_displayed_document = null; - g_displayed_doc_filename = ""; + g_terrain_tobj_files.removeRange(0, g_terrain_tobj_files.length()); } @@ -445,17 +259,17 @@ vector3 detectPlayerPosition() { if (g_app_state.getInt() == 2) // simulation { - + // get current pos vector3 pos; BeamClass@ actor = game.getCurrentTruck(); if (@actor != null) - pos = actor.getVehiclePosition(); + pos = actor.getVehiclePosition(); else - pos = game.getPersonPosition(); - + pos = game.getPersonPosition(); + return pos; - + } else // main menu { @@ -476,15 +290,15 @@ void drawAudioButtons() { ImGui::TextDisabled("You are in simulation - spatial (3D) audio is on"); ImGui::Checkbox("Sound follows player", g_sound_follows_player); - + // Update sound positions if (g_sound_follows_player) { vector3 pos = detectPlayerPosition(); if (@g_playing_sound != null) - g_playing_sound.setPosition(pos); + g_playing_sound.setPosition(pos); if (@g_playing_soundscript != null) - g_playing_soundscript.setPosition(pos); + g_playing_soundscript.setPosition(pos); } } @@ -509,7 +323,7 @@ void drawAudioButtons() ImGui::TextDisabled(" [base]"); } ImGui::SameLine(); - + if (@g_playing_soundscript == null) { if (ImGui::Button("Play")) @@ -551,11 +365,11 @@ void drawAudioButtons() if (ImGui::CollapsingHeader(instances_title)) { ImGui::PushID("instances"); - + for (uint i = 0; i < instances.length(); i++) { ImGui::PushID(i); - + SoundScriptInstanceClass@ instance = instances[i]; ImGui::Text(instance.getInstanceName()); @@ -575,7 +389,7 @@ void drawAudioButtons() } ImGui::TextDisabled("Some builtin sounds"); - + drawWavPreviewBulletButton("default_horn.wav"); drawWavPreviewBulletButton("default_police.wav"); drawWavPreviewBulletButton("default_pump.wav"); @@ -588,7 +402,7 @@ void drawAudioButtons() void drawWavPreviewBulletButton(string wav_file) { ImGui::PushID(wav_file); - + ImGui::Bullet(); ImGui::SameLine(); ImGui::Text(wav_file); @@ -622,21 +436,21 @@ void drawWavPreviewBulletButton(string wav_file) void drawSoundObjectDiag(SoundClass@ snd) { string txt - = "\t enabled:"+snd.getEnabled()+", playing:"+snd.isPlaying() - +"\n\t audibility:"+snd.getAudibility() - +"\n\t gain:"+snd.getGain() +", pitch:"+snd.getPitch() - +"\n\t loop:"+snd.getLoop() - +"\n\t currentHardwareIndex:"+snd.getCurrentHardwareIndex() - +"\n\t OpenAL buffer ID:"+snd.getBuffer() - +"\n\t position: X="+snd.getPosition().x+" Y="+snd.getPosition().y+" Z="+snd.getPosition().z - +"\n\t velocity: X="+snd.getVelocity().x+" Y="+snd.getVelocity().y+" Z="+snd.getVelocity().z; + = "\t enabled:"+snd.getEnabled()+", playing:"+snd.isPlaying() + +"\n\t audibility:"+snd.getAudibility() + +"\n\t gain:"+snd.getGain() +", pitch:"+snd.getPitch() + +"\n\t loop:"+snd.getLoop() + +"\n\t currentHardwareIndex:"+snd.getCurrentHardwareIndex() + +"\n\t OpenAL buffer ID:"+snd.getBuffer() + +"\n\t position: X="+snd.getPosition().x+" Y="+snd.getPosition().y+" Z="+snd.getPosition().z + +"\n\t velocity: X="+snd.getVelocity().x+" Y="+snd.getVelocity().y+" Z="+snd.getVelocity().z; ImGui::Text(txt); } void drawSoundScriptInstanceDiagPanel(SoundScriptInstanceClass@ instance) { SoundScriptTemplateClass@ template = instance.getTemplate(); - + // START sound SoundClass@ startSnd = instance.getStartSound(); if (@startSnd != null) @@ -697,7 +511,7 @@ void drawAIButtons() // Request loading the AI script (asynchronously) - it will spawn the vehicle. // WARNING: this doesn't save off the setup values above - you can still modify them below and change what the AI will do! // If you want to launch multiple AIs in sequence, register for SE_GENERIC_NEW_TRUCK event - when it arrives, it's safe to setup and launch new AI script. - game.pushMessage(MSG_APP_LOAD_SCRIPT_REQUESTED, { {"filename", "AI.as"} }); +game.pushMessage(MSG_APP_LOAD_SCRIPT_REQUESTED, { {"filename", "AI.as"} }); } } @@ -739,8 +553,8 @@ void drawTextResourceButtons() string formatVector3(vector3 val, int total, int frac) { return "X:" + formatFloat(val.x, "", total, frac) - + " Y:" + formatFloat(val.y, "", total, frac) - + " Z:" + formatFloat(val.z, "", total, frac); + + " Y:" + formatFloat(val.y, "", total, frac) + + " Z:" + formatFloat(val.z, "", total, frac); } void drawActorAngles(BeamClass@ actor) @@ -752,11 +566,11 @@ void drawActorAngles(BeamClass@ actor) ImGui::Text("getSpeed(): [float] " + formatFloat(actor.getSpeed(), "", 6,2)); ImGui::Text("getOrientation(): [quaternion]"); ImGui::Text(" .getYaw(): [radian] " + formatFloat(actor.getOrientation().getYaw().valueRadians(), "", 6,2) - + " (degrees: " + formatFloat(actor.getOrientation().getYaw().valueDegrees(), "", 6,2) + ")"); + + " (degrees: " + formatFloat(actor.getOrientation().getYaw().valueDegrees(), "", 6,2) + ")"); ImGui::Text(" .getPitch(): [radian] " + formatFloat(actor.getOrientation().getPitch().valueRadians(), "", 6,2) - + " (degrees: " + formatFloat(actor.getOrientation().getPitch().valueDegrees(), "", 6,2) + ")"); + + " (degrees: " + formatFloat(actor.getOrientation().getPitch().valueDegrees(), "", 6,2) + ")"); ImGui::Text(" .getRoll(): [radian] " + formatFloat(actor.getOrientation().getRoll().valueRadians(), "", 6,2) - + " (degrees: " + formatFloat(actor.getOrientation().getRoll().valueDegrees(), "", 6,2) + ")"); + + " (degrees: " + formatFloat(actor.getOrientation().getRoll().valueDegrees(), "", 6,2) + ")"); } } @@ -773,7 +587,7 @@ void drawExampleTabs() //void EndTabItem() //bool TabItemButton(const string&in, int TabItemFlags = 0) //void SetTabItemClosed(const string&in) - + // IMGUI TAB BAR FLAGS: //ImGuiTabBarFlags_Reorderable); // // Allow manually dragging tabs to re-order them + New tabs are appended at the end of list //ImGuiTabBarFlags_AutoSelectNewTabs); // // Automatically select new tabs when they appear @@ -786,17 +600,17 @@ void drawExampleTabs() // // ============================================================================ int barflags = - ImGuiTabBarFlags_NoCloseWithMiddleMouseButton - | ImGuiTabBarFlags_NoTabListScrollingButtons - | ImGuiTabBarFlags_FittingPolicyResizeDown; + ImGuiTabBarFlags_NoCloseWithMiddleMouseButton + | ImGuiTabBarFlags_NoTabListScrollingButtons + | ImGuiTabBarFlags_FittingPolicyResizeDown; if (demotabsReorderable) - barflags = barflags | ImGuiTabBarFlags_Reorderable; + barflags = barflags | ImGuiTabBarFlags_Reorderable; if (demotabsNoTooltip) - barflags = barflags |= ImGuiTabBarFlags_NoTooltip; - + barflags = barflags |= ImGuiTabBarFlags_NoTooltip; + if (ImGui::BeginTabBar("demotabs", barflags)) { - + if (ImGui::BeginTabItem("Text")) { // just text @@ -808,7 +622,7 @@ void drawExampleTabs() ImGui::EndTabItem(); } - + if (ImGui::BeginTabItem("Reset")) { // boring text diff --git a/resources/scripts/example_GenericDocument_editor.as b/resources/scripts/example_GenericDocument_editor.as deleted file mode 100644 index 3cc99dda1d..0000000000 --- a/resources/scripts/example_GenericDocument_editor.as +++ /dev/null @@ -1,332 +0,0 @@ -// Prototype truck editor, Oct 2023 -// (sorry about the indentation mess, I scribbled this in 'script_editor.as') -// =================================================== - -// Window [X] button handler -#include "imgui_utils.as" -imgui_utils::CloseWindowPrompt closeBtnHandler; - -GenericDocumentClass@ displayedDocument = null; -int hoveredTokenPos = -1; -int focusedTokenPos = -1; -string errorString; -string nameOfProjectPendingCreation; // name of the truck file which will become available in modcache. -CacheEntryClass@ projectEntry; - - - -// `frameStep()` runs every frame; `dt` is delta time in seconds. -void frameStep(float dt) -{ - if (@projectEntry == null) - { - // check if the actor is already a project - BeamClass@ actor = game.getCurrentTruck(); - if (@actor != null) - { - CacheEntryClass@ entry = modcache.findEntryByFilename(LOADER_TYPE_ALLBEAM, /*partial:*/false, actor.getTruckFileName()); - if (@entry != null && entry.resource_bundle_type == "FileSystem") - { - @projectEntry = @entry; - loadDocument(); - } - } - } - checkForCreatedProject(); - drawWindow(); -} - - - -void drawWindow() -{ - - string caption = "RigEditor"; - if (@projectEntry != null) - { - caption += " ("+projectEntry.fname+")"; - } - int flags = ImGuiWindowFlags_MenuBar; - if (ImGui::Begin(caption, closeBtnHandler.windowOpen, flags)) - { - closeBtnHandler.draw(); - if (ImGui::BeginMenuBar()) - { - drawDocumentControls(); // <- menubar - ImGui::EndMenuBar(); - } - if (@displayedDocument != null) - { - vector2 size = ImGui::GetWindowSize() - vector2(25, 200); - ImGui::BeginChild("docBody", size); - drawDocumentBody(); - ImGui::EndChild(); - - if ( focusedTokenPos > -1) - { - ImGui::Separator(); - drawTokenEditPanel(); - } - } - - ImGui::End(); - } - -} - -void drawTokenEditPanel() -{ - GenericDocContextClass reader(displayedDocument); - while (!reader.endOfFile() && reader.getPos() != uint(focusedTokenPos)) reader.moveNext(); - if (reader.endOfFile()) - { - ImGui::Text("EOF!!"); - } - else - { - ImGui::TextDisabled("Token pos: "); ImGui::SameLine(); ImGui::Text("" + reader.getPos()); - ImGui::TextDisabled("Token type: "); ImGui::SameLine(); ImGui::Text(tokenTypeStr(reader.tokenType())); - } -} - -string tokenTypeStr(TokenType t) -{ - switch (t) - { - case TOKEN_TYPE_NUMBER: return "Number"; - 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 "?"; -} - -void createProjectFromActorAsync(BeamClass@ actor ) -{ - // Fetch the current actor cache entry - CacheEntryClass@ src_entry = modcache.findEntryByFilename(LOADER_TYPE_ALLBEAM, /*partial:*/false, actor.getTruckFileName()); - if (@src_entry == null) - errorString = "Failed to load cache entry!"; - - // request project to be created from that cache entry - string proj_name = "gd_editor_" + src_entry.fname; - game.pushMessage(MSG_EDI_CREATE_PROJECT_REQUESTED, { - {'name', proj_name}, - {'source_entry', src_entry} - }); - - // Now we have to wait until next frame for the project to be created - nameOfProjectPendingCreation = proj_name + "." + src_entry.fext; // there's no notification event yet, we must poll. -} - -void loadDocument() -{ - GenericDocumentClass@ doc = GenericDocumentClass(); - int flags = GENERIC_DOCUMENT_OPTION_ALLOW_NAKED_STRINGS - | GENERIC_DOCUMENT_OPTION_ALLOW_SLASH_COMMENTS - | GENERIC_DOCUMENT_OPTION_FIRST_LINE_IS_TITLE - | GENERIC_DOCUMENT_OPTION_ALLOW_SEPARATOR_COLON - | GENERIC_DOCUMENT_OPTION_PARENTHESES_CAPTURE_SPACES; - if (!doc.loadFromResource(projectEntry.fname, projectEntry.resource_group, flags)) - { - errorString = "Project file failed to load!"; - return; - } - @displayedDocument = doc; -} - -void loadAndFixupDocument() -{ - loadDocument(); - - // fixup the document.. - - GenericDocContextClass@ ctx = GenericDocContextClass(displayedDocument); - // >> seek the name - while (!ctx.isTokString()) { - ctx.seekNextLine(); - } - // >> change the name - if (!ctx.endOfFile()) { - ctx.setTokString(0, projectEntry.dname); - } - // >> seek fileinfo - while (!ctx.isTokKeyword() || ctx.getTokKeyword() != 'fileinfo') { - ctx.seekNextLine(); - } - // change the fileinfo param #2 categoryid - if (!ctx.endOfFile(2)) { - ctx.setTokFloat(2, 8990); // special "Project" category - } -} - -void checkForCreatedProject() -{ - // there's no notification event for completed request, we must poll like this. - if (nameOfProjectPendingCreation != "") - { - @projectEntry = modcache.findEntryByFilename(LOADER_TYPE_ALLBEAM, /*partial:*/false, nameOfProjectPendingCreation); - if (@projectEntry != null) - { - // success!! stop polling and open the document. - nameOfProjectPendingCreation=""; - loadAndFixupDocument(); - } - } -} - -void drawDocumentControls() -{ - if (errorString != "") - { - ImGui::Text("ERROR! " + errorString); - return; - } - else if (@displayedDocument != null) - { - ImGui::TextDisabled("Click tokens with mouse to edit. Spawn as usual (use category 'Projects')"); - ImGui::SameLine(); - if (ImGui::SmallButton("Save file")) - { - displayedDocument.saveToResource(projectEntry.fname, projectEntry.resource_group); - } - } - else if (nameOfProjectPendingCreation != "") - { - ImGui::Text ("Waiting for project to be created..."); - } - else - { - BeamClass@ actor = game.getCurrentTruck(); - if (@actor != null) - { - // Actor name and "View document" button - ImGui::PushID("actor"); - ImGui::AlignTextToFramePadding(); - ImGui::Text("You are driving " + actor.getTruckName()); - ImGui::SameLine(); - if (@displayedDocument == null) - { - if (ImGui::SmallButton("Import as project")) - { - createProjectFromActorAsync(actor); - } - } - - ImGui::PopID(); //"actor" - } - else - { - ImGui::Text("You are on foot. Spawn a vehicle to access it's definition file."); - } - } -} - -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_NUMBER: - ImGui::TextColored(tokenColor(reader), "" + reader.getTokFloat()); - 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_NUMBER: return color(0.9, 0.9, 0.9, 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); -} // end tokenColor() - - - - - - - diff --git a/resources/scripts/example_GenericDocument_uniEditor.as b/resources/scripts/example_GenericDocument_uniEditor.as new file mode 100644 index 0000000000..3196aba2e0 --- /dev/null +++ b/resources/scripts/example_GenericDocument_uniEditor.as @@ -0,0 +1,416 @@ +/// \title IMGUI-based editor of tokenized GenericDocument +/// \opens the terrain files (.terrn2, .tobj) +// =================================================== + +// Window [X] button handler +#include "imgui_utils.as" +imgui_utils::CloseWindowPrompt closeBtnHandler; + + +int hoveredTokenPos = -1; +int focusedTokenPos = -1; +string errorString; +// Document window state +GenericDocumentClass@ g_displayed_document = null; +string g_displayed_doc_filename; +array g_terrain_tobj_files; + + + +// `frameStep()` runs every frame; `dt` is delta time in seconds. +void frameStep(float dt) +{ + string caption = "GenericDocument editor"; + int flags = ImGuiWindowFlags_MenuBar; + if (ImGui::Begin(caption, closeBtnHandler.windowOpen, flags)) + { + closeBtnHandler.draw(); + + if (ImGui::BeginMenuBar()) + { + if (ImGui::BeginMenu("Open document")) + { + ImGui::TextDisabled("rig-definition file (aka truck file):"); + drawTruckDocumentControls(); + ImGui::Separator(); + + ImGui::TextDisabled("Terrain documents (terrn2, tobj)"); + drawTerrainButtons(); + ImGui::EndMenu(); + } + ImGui::EndMenuBar(); + + } + + + if (@g_displayed_document != null) + { + vector2 size = ImGui::GetWindowSize() - vector2(20, 150); + ImGui::BeginChild("docBody", size); + drawDocumentBody(); + ImGui::EndChild(); + + + ImGui::Separator(); + + drawTokenEditPanel(); + + } + + ImGui::End(); + + } +} + + +//#region Terrn+tobj doc viewing +void loadTobjDocument(uint i) +{ + TerrainClass@ terrain = game.getTerrain(); + if (@terrain == null) + { + game.log("loadTobjDocument(): no terrain loaded, nothing to do!"); + return; + } + GenericDocumentClass@ doc = GenericDocumentClass(); + int flags = GENERIC_DOCUMENT_OPTION_ALLOW_NAKED_STRINGS + | GENERIC_DOCUMENT_OPTION_ALLOW_SLASH_COMMENTS; + if (doc.loadFromResource(g_terrain_tobj_files[i], terrain.getTerrainFileResourceGroup(), flags)) + { + @g_displayed_document = @doc; + g_displayed_doc_filename = g_terrain_tobj_files[i]; + } +} + +void loadTerrn2Document() +{ + TerrainClass@ terrain = game.getTerrain(); + if (@terrain == null) + { + game.log("loadTerrn2Document(): no terrain loaded, nothing to do!"); + return; + } + GenericDocumentClass@ doc = GenericDocumentClass(); + int flags = 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; + if (doc.loadFromResource(terrain.getTerrainFileName(), terrain.getTerrainFileResourceGroup(), flags)) + { + @g_displayed_document = @doc; + g_displayed_doc_filename = terrain.getTerrainFileName(); + + // Fetch TOBJ filenames + if (g_terrain_tobj_files.length() == 0) + { + GenericDocContextClass@ reader = GenericDocContextClass(doc); + bool in_section_objects = false; + while (!reader.endOfFile()) + { + if (reader.tokenType() == TOKEN_TYPE_KEYWORD && reader.getTokKeyword().substr(0, 1) == "[") + { + in_section_objects = (reader.getTokKeyword() == '[Objects]'); + } + else if (reader.tokenType() == TOKEN_TYPE_STRING && in_section_objects) + { + // Note: in GenericDocument, a text on line start is always a KEYWORD token, + // but KEYWORDs must not contain special characters, + // so file names always decay to strings because of '.'. + g_terrain_tobj_files.insertLast(reader.getTokString()); + } + reader.moveNext(); + } + } + } +} + +void drawTerrainButtons() +{ + // Terrain name (with "view document" button) + ImGui::PushID("terrn"); + TerrainClass@ terrain = game.getTerrain(); + ImGui::AlignTextToFramePadding(); + ImGui::Text("Terrain: " + terrain.getTerrainName()); + ImGui::SameLine(); + if (@g_displayed_document == null) + { + if (ImGui::Button("View document")) + { + + loadTerrn2Document(); + } + } + else + { + if (ImGui::Button("Close document")) + { + @g_displayed_document = null; + g_displayed_doc_filename = ""; + } + } + + + + // TOBJ files + ImGui::PushID("tobj"); + for (uint i = 0; i < g_terrain_tobj_files.length(); i++) + { + ImGui::PushID(i); + ImGui::AlignTextToFramePadding(); + ImGui::Bullet(); + ImGui::SameLine(); + ImGui::Text(g_terrain_tobj_files[i]); + ImGui::SameLine(); + if (@g_displayed_document == null) + { + if (ImGui::Button("View document")) + { + loadTobjDocument(i); + } + } + else + { + if (ImGui::Button("Close document")) + { + @g_displayed_document = null; + g_displayed_doc_filename = ""; + } + } + ImGui::PopID(); // i + } + ImGui::PopID(); //"tobj" + + ImGui::PopID(); //"terrn" +} + +//#endregion + +//#region Truck file format viewing +void loadTruckDocument() +{ + BeamClass@ actor = game.getCurrentTruck(); + if (@actor == null) + { + game.log("loadTruckDocument(): you are on foot, nothing to do!"); + return; + } + // Actor name and "View document" button + ImGui::PushID("actor"); + ImGui::AlignTextToFramePadding(); + ImGui::Text("You are driving " + actor.getTruckName()); + ImGui::SameLine(); + if (@g_displayed_document == null) + { + if (ImGui::Button("View document")) + { + GenericDocumentClass@ doc = GenericDocumentClass(); + int flags = GENERIC_DOCUMENT_OPTION_ALLOW_NAKED_STRINGS + | GENERIC_DOCUMENT_OPTION_ALLOW_SLASH_COMMENTS + | GENERIC_DOCUMENT_OPTION_FIRST_LINE_IS_TITLE + | GENERIC_DOCUMENT_OPTION_ALLOW_SEPARATOR_COLON + | GENERIC_DOCUMENT_OPTION_PARENTHESES_CAPTURE_SPACES; + if (doc.loadFromResource(actor.getTruckFileName(), actor.getTruckFileResourceGroup(), flags)) + { + @g_displayed_document = @doc; + g_displayed_doc_filename = actor.getTruckFileName(); + } + } + } +} + +void drawTruckDocumentControls() +{ + + BeamClass@ actor = game.getCurrentTruck(); + if (@actor != null) + { + // Actor name and "View document" button + ImGui::PushID("actor"); + ImGui::AlignTextToFramePadding(); + ImGui::Text("You are driving " + actor.getTruckName()); + ImGui::SameLine(); + if (@g_displayed_document == null) + { + if (ImGui::Button("View document")) + { + GenericDocumentClass@ doc = GenericDocumentClass(); + int flags = GENERIC_DOCUMENT_OPTION_ALLOW_NAKED_STRINGS + | GENERIC_DOCUMENT_OPTION_ALLOW_SLASH_COMMENTS + | GENERIC_DOCUMENT_OPTION_FIRST_LINE_IS_TITLE + | GENERIC_DOCUMENT_OPTION_ALLOW_SEPARATOR_COLON + | GENERIC_DOCUMENT_OPTION_PARENTHESES_CAPTURE_SPACES; + if (doc.loadFromResource(actor.getTruckFileName(), actor.getTruckFileResourceGroup(), flags)) + { + @g_displayed_document = @doc; + g_displayed_doc_filename = actor.getTruckFileName(); + } + } + } + else + { + if (ImGui::Button("Close document")) + { + @g_displayed_document = null; + g_displayed_doc_filename = ""; + } + } + ImGui::PopID(); //"actor" + } + else + { + ImGui::Text("You are on foot. Spawn a vehicle to access it's definition file."); + } + +} +//#endregion + +//#region Document view + + +void drawDocumentBody() +{ + ImGui::PushID("docBody"); + bool hover_found = false; + + GenericDocContextClass reader(g_displayed_document); + 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()); + break; + } + case TOKEN_TYPE_INT: + { + 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_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() +{ + if ( focusedTokenPos == -1) + { + ImGui::TextDisabled("Use left mouse button to select tokens."); + return; // nothing to draw + } + GenericDocContextClass reader(g_displayed_document); + while (!reader.endOfFile() && reader.getPos() != uint(focusedTokenPos)) reader.moveNext(); + if (reader.endOfFile()) + { + ImGui::Text("EOF!!"); + } + else + { + ImGui::TextDisabled("Token pos: "); ImGui::SameLine(); ImGui::Text("" + reader.getPos()); + 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 "?"; +} + + //#endregion + + From 9ee5647490f430b03b64cfebf604cd0f7a1d9c24 Mon Sep 17 00:00:00 2001 From: ohlidalp Date: Wed, 27 Nov 2024 15:34:35 +0100 Subject: [PATCH 15/36] script_editor.as+=bounds checking (investigating exceptions) --- resources/scripts/script_editor.as | 2404 ++++++++++++++-------------- 1 file changed, 1214 insertions(+), 1190 deletions(-) diff --git a/resources/scripts/script_editor.as b/resources/scripts/script_editor.as index cd04b5ff95..280142b732 100644 --- a/resources/scripts/script_editor.as +++ b/resources/scripts/script_editor.as @@ -12,9 +12,9 @@ // from enum asEMsgType { - asMSGTYPE_ERROR = 0, - asMSGTYPE_WARNING = 1, - asMSGTYPE_INFORMATION = 2 + asMSGTYPE_ERROR = 0, + asMSGTYPE_WARNING = 1, + asMSGTYPE_INFORMATION = 2 }; // from rigs of rods int SCRIPTUNITID_INVALID = -1; @@ -34,65 +34,65 @@ const uint FILEINFO_COMPRESSEDSIZE_UNKNOWN = uint(-1); // LINE METAINFO COLUMNS (drawn by `drawLineInfoColumns()`, space calculated by `measureLineInfoColumns()`, visibility toggled by `drawMenubar()`) // NOTE: most of these are early debug helpers, I kept them around (some visible by default) for the laughs. - // Line numbers - bool drawLineNumbers=true; - color lineNumberColor=color(1.f,1.f,0.7f,1.f); - float lineNumColumnWidth = 25; - // Line char counts - bool drawLineLengths=true; - color lineLenColor=color(0.5f,0.4f,0.3f,1.f); - float lineLenColumnWidth = 25; - // Line comment offsets - bool drawLineCommentOffsets=true; - color lineCommentOffsetColor=color(0.1f,0.9f,0.6f,0.6f); - float lineCommentColumnWidth = 25; - // Line http offsets - bool drawLineHttpOffsets=false; // only visual, links don't open - color lineHttpOffsetColor=color(LINKCOLOR.r, LINKCOLOR.g, LINKCOLOR.a, 0.6f); - float lineHttpColumnWidth = 16; - // Line err counts (error may span multiple AngelScript messages!) - bool drawLineErrCounts = false; - color lineErrCountColor = color(1.f,0.2f,0.1f,1.f); - float errCountColumnWidth = 10; - // Auto-indentation level display - bool drawLineAutoIndentLevels = true; - color lineAutoIndentLevelsColor = color(0.55f, 0.44f, 0.55f, 1.f); - float lineAutoIndentLevelsColumnWidth = 16; - // Actual indent display - bool drawLineActualIndents = true; - color lineActualIndentsColor = color(0.55f, 0.55f, 0.44f, 1.f); - float lineActualIndentsColumnWidth = 16; - // Line region&endregion markers - bool drawLineRegionMarkers = true; - color lineRegionMarkerColor = color(0.78f,0.76f,0.32f,1.f); - float lineRegionMarkerColumnWidth = 17; +// Line numbers +bool drawLineNumbers=true; +color lineNumberColor=color(1.f,1.f,0.7f,1.f); +float lineNumColumnWidth = 25; +// Line char counts +bool drawLineLengths=true; +color lineLenColor=color(0.5f,0.4f,0.3f,1.f); +float lineLenColumnWidth = 25; +// Line comment offsets +bool drawLineCommentOffsets=true; +color lineCommentOffsetColor=color(0.1f,0.9f,0.6f,0.6f); +float lineCommentColumnWidth = 25; +// Line http offsets +bool drawLineHttpOffsets=false; // only visual, links don't open +color lineHttpOffsetColor=color(LINKCOLOR.r, LINKCOLOR.g, LINKCOLOR.a, 0.6f); +float lineHttpColumnWidth = 16; +// Line err counts (error may span multiple AngelScript messages!) +bool drawLineErrCounts = false; +color lineErrCountColor = color(1.f,0.2f,0.1f,1.f); +float errCountColumnWidth = 10; +// Auto-indentation level display +bool drawLineAutoIndentLevels = true; +color lineAutoIndentLevelsColor = color(0.55f, 0.44f, 0.55f, 1.f); +float lineAutoIndentLevelsColumnWidth = 16; +// Actual indent display +bool drawLineActualIndents = true; +color lineActualIndentsColor = color(0.55f, 0.55f, 0.44f, 1.f); +float lineActualIndentsColumnWidth = 16; +// Line region&endregion markers +bool drawLineRegionMarkers = true; +color lineRegionMarkerColor = color(0.78f,0.76f,0.32f,1.f); +float lineRegionMarkerColumnWidth = 17; // END line metainfo columns // EDITOR settings - color errorTextColor = color(1.f,0.3f,0.2f,1.f); - color errorTextBgColor = color(0.1f,0.04f,0.08f,1.f); - bool drawErrorText = true; - color warnTextColor = color(0.78f,0.66f,0.22f,1.f); - color warnTextBgColor = color(0.1f,0.08f,0.0f,1.f); - bool drawWarnText = true; - color commentHighlightColor = color(0.1f,0.9f,0.6f,0.16f); - color regionCommentHighlightColor = color(0.78f,0.76f,0.32f,0.18f); - bool drawCommentHighlights = true; - color httpHighlightColor=color(0.25, 0.3, 1, 0.3f); - bool drawHttpHighlights = false; // only visual, links don't open - float scriptInfoIndentWidth = 25; - - int autoIndentNumSpaces = 4; - bool autoIndentOnSave = true; - - bool killScriptOnAngelscriptException = true; // Prevent endless assert() loop "mismatched BeginChild()/EndChild()" under Visual Studio Debug, effectivelly forcing you to stop the game. - - // input output - bool saveShouldOverwrite=false; - - float autoSaveIntervalSec=25.f; - color autoSaveStatusbarBgColor = color(0.3f, 0.3f, 0.4f, 1.f); - bool drawAutosaveCountdown=false; +color errorTextColor = color(1.f,0.3f,0.2f,1.f); +color errorTextBgColor = color(0.1f,0.04f,0.08f,1.f); +bool drawErrorText = true; +color warnTextColor = color(0.78f,0.66f,0.22f,1.f); +color warnTextBgColor = color(0.1f,0.08f,0.0f,1.f); +bool drawWarnText = true; +color commentHighlightColor = color(0.1f,0.9f,0.6f,0.16f); +color regionCommentHighlightColor = color(0.78f,0.76f,0.32f,0.18f); +bool drawCommentHighlights = true; +color httpHighlightColor=color(0.25, 0.3, 1, 0.3f); +bool drawHttpHighlights = false; // only visual, links don't open +float scriptInfoIndentWidth = 25; + +int autoIndentNumSpaces = 4; +bool autoIndentOnSave = true; + +bool killScriptOnAngelscriptException = true; // Prevent endless assert() loop "mismatched BeginChild()/EndChild()" under Visual Studio Debug, effectivelly forcing you to stop the game. + +// input output +bool saveShouldOverwrite=false; + +float autoSaveIntervalSec=25.f; +color autoSaveStatusbarBgColor = color(0.3f, 0.3f, 0.4f, 1.f); +bool drawAutosaveCountdown=false; // END editor // Callback functions for the game: @@ -115,16 +115,16 @@ void frameStep(float dt) // Invoked regularly by the game; the parameter is elap void eventCallbackEx(scriptEvents ev, // Invoked by the game when a registered event is triggered - int arg1, int arg2ex, int arg3ex, int arg4ex, - string arg5ex, string arg6ex, string arg7ex, string arg8ex) +int arg1, int arg2ex, int arg3ex, int arg4ex, +string arg5ex, string arg6ex, string arg7ex, string arg8ex) { if (ev == SE_ANGELSCRIPT_MSGCALLBACK) { for (uint i=0; i tabs; - uint currentTab = 0; - int tabScheduledForRemoval = -1; + array tabs; + uint currentTab = 0; + int tabScheduledForRemoval = -1; // GLOBAL CONTEXT imgui_utils::CloseWindowPrompt closeBtnHandler; @@ -203,12 +203,12 @@ class ScriptEditorWindow ScriptIndexerRecord localScriptsRecord; ScriptIndexerRecord exampleScriptsRecord; ScriptIndexerRecord includeScriptsRecord; - - ScriptEditorWindow() - { - closeBtnHandler.cfgPromptText = "Really close script editor? You will lose any unsaved work."; - } - + + ScriptEditorWindow() + { + closeBtnHandler.cfgPromptText = "Really close script editor? You will lose any unsaved work."; + } + void refreshLocalFileList() { //NOTE: `recentScriptsRecord` is constructed manually. @@ -216,7 +216,7 @@ class ScriptEditorWindow this.exampleScriptsRecord.scanResourceGroup(RGN_RESOURCES_SCRIPTS, "example_*.*"); this.includeScriptsRecord.scanResourceGroup(RGN_RESOURCES_SCRIPTS, "*_utils.*"); } - + void addTab(const string&in tabName, const string&in buffer) { ScriptEditorTab nTab; @@ -241,7 +241,7 @@ class ScriptEditorWindow this.removeTab(uint(tabScheduledForRemoval)); this.tabScheduledForRemoval = -1; } - + // Make sure there's always a tab open () if (tabs.length() == 0) { @@ -251,12 +251,12 @@ class ScriptEditorWindow else { if (this.currentTab >= tabs.length()) - this.currentTab = tabs.length()-1; + this.currentTab = tabs.length()-1; } this.tabs[this.currentTab].updateAutosave(dt); this.tabs[this.currentTab].handleRequests(dt); - + int flags = ImGuiWindowFlags_MenuBar | ImGuiWindowFlags_NoScrollbar; if (ImGui::Begin("Script editor", this.closeBtnHandler.windowOpen, flags)) @@ -268,7 +268,7 @@ class ScriptEditorWindow this.tabs[this.currentTab].drawTextArea(); this.tabs[this.currentTab].drawExceptionsPanel(); this.tabs[this.currentTab].drawFooter(); - + // To draw on top of editor text, we must trick DearIMGUI using an extra invisible window ImGui::SetCursorPos(this.tabs[this.currentTab].errorsScreenCursor - ImGui::GetWindowPos()); ImGui::PushStyleColor(ImGuiCol_ChildBg, color(0.f,0.f,0.f,0.f)); // Fully transparent background! @@ -281,7 +281,7 @@ class ScriptEditorWindow ImGui::PopStyleColor(1); // ChildBg } ImGui::End(); - + } private void drawTabBar() @@ -303,11 +303,11 @@ class ScriptEditorWindow ImGui::EndTabItem(); } if (!tabOpen) - this.tabScheduledForRemoval = int(i); + this.tabScheduledForRemoval = int(i); else if (ImGui::IsItemClicked()) - this.currentTab = i; + this.currentTab = i; } - + ImGui::EndTabBar(); } } @@ -348,7 +348,7 @@ class ScriptEditorWindow } /*game.log("DBG script editor: open file menu: file '"+fileNameBuf+"' is an autosave, loading as '"+tabName+"'");*/ } - + this.addTab(tabName, game.loadTextResourceAsString(fileNameBuf, RGN_SCRIPTS)); this.currentTab = this.tabs.length() - 1; // Focus the new tab this.addRecentScript(fileNameBuf); @@ -364,7 +364,7 @@ class ScriptEditorWindow { analysisDoneThisFrame = localScriptsRecord.advanceScriptAnalysis(); } - + if (this.saveMenuOpening) { saveFileNameBuf = this.tabs[this.currentTab].bufferName; @@ -381,9 +381,9 @@ class ScriptEditorWindow // Error indicator: if (this.saveFileResult == 1) - ImGui::TextColored(color(0.2,0.7, 0.2, 1), "File saved OK"); + ImGui::TextColored(color(0.2,0.7, 0.2, 1), "File saved OK"); else if (this.saveFileResult == -1) - ImGui::TextColored(color(1,0.1, 0.2, 1), "Error saving file!"); + ImGui::TextColored(color(1,0.1, 0.2, 1), "Error saving file!"); ImGui::Separator(); this.drawSelectableFileList("Recent scripts", "Select##recent", recentScriptsRecord, /*&inout*/ saveFileNameBuf); @@ -432,36 +432,36 @@ class ScriptEditorWindow ImGui::EndMenu(); } - + // 'VIEW' menu if (ImGui::BeginMenu("View")) { ImGui::TextDisabled("Line info:"); ImGui::PushStyleColor(ImGuiCol_Text, lineNumberColor); - ImGui::Checkbox("Line numbers", /*inout:*/drawLineNumbers); - ImGui::PopStyleColor(1); // ImGuiCol_Text + ImGui::Checkbox("Line numbers", /*inout:*/drawLineNumbers); + ImGui::PopStyleColor(1); // ImGuiCol_Text ImGui::PushStyleColor(ImGuiCol_Text, lineLenColor); - ImGui::Checkbox("Char counts", /*inout:*/drawLineLengths); - ImGui::PopStyleColor(1); // ImGuiCol_Text + ImGui::Checkbox("Char counts", /*inout:*/drawLineLengths); + ImGui::PopStyleColor(1); // ImGuiCol_Text ImGui::PushStyleColor(ImGuiCol_Text, lineCommentOffsetColor); - ImGui::Checkbox("Comment offsets", /*inout:*/drawLineCommentOffsets); - ImGui::PopStyleColor(1); // ImGuiCol_Text + ImGui::Checkbox("Comment offsets", /*inout:*/drawLineCommentOffsets); + ImGui::PopStyleColor(1); // ImGuiCol_Text ImGui::PushStyleColor(ImGuiCol_Text, lineHttpOffsetColor); - ImGui::Checkbox("Http offsets", /*inout:*/drawLineHttpOffsets); - ImGui::PopStyleColor(1); // ImGuiCol_Text + ImGui::Checkbox("Http offsets", /*inout:*/drawLineHttpOffsets); + ImGui::PopStyleColor(1); // ImGuiCol_Text ImGui::PushStyleColor(ImGuiCol_Text, lineErrCountColor); - ImGui::Checkbox("Error counts", /*inout:*/drawLineErrCounts); - ImGui::PopStyleColor(1); // ImGuiCol_Text + ImGui::Checkbox("Error counts", /*inout:*/drawLineErrCounts); + ImGui::PopStyleColor(1); // ImGuiCol_Text ImGui::PushStyleColor(ImGuiCol_Text, lineRegionMarkerColor); - ImGui::Checkbox("Region markers", /*inout:*/drawLineRegionMarkers); - ImGui::PopStyleColor(1); // ImGuiCol_Text + ImGui::Checkbox("Region markers", /*inout:*/drawLineRegionMarkers); + ImGui::PopStyleColor(1); // ImGuiCol_Text ImGui::PushStyleColor(ImGuiCol_Text, lineAutoIndentLevelsColor); - ImGui::Checkbox("Auto indents", /*inout:*/drawLineAutoIndentLevels); - ImGui::PopStyleColor(1); // ImGuiCol_Text + ImGui::Checkbox("Auto indents", /*inout:*/drawLineAutoIndentLevels); + ImGui::PopStyleColor(1); // ImGuiCol_Text ImGui::PushStyleColor(ImGuiCol_Text, lineActualIndentsColor); - ImGui::Checkbox("Actual indents", /*inout:*/drawLineActualIndents); - ImGui::PopStyleColor(1); // ImGuiCol_Text - + ImGui::Checkbox("Actual indents", /*inout:*/drawLineActualIndents); + ImGui::PopStyleColor(1); // ImGuiCol_Text + ImGui::TextDisabled("Editor overlay:"); ImGui::Checkbox("Error text", /*inout:*/drawErrorText); ImGui::Checkbox("Warning text", /*inout:*/drawWarnText); @@ -476,8 +476,8 @@ class ScriptEditorWindow // 'FOLDING' menu if (ImGui::BeginMenu("Folding")) { - if (ImGui::Button("Fold all")) { this.tabs[this.currentTab].requestFoldAll=true; } - if (ImGui::Button("UnFold all")) { this.tabs[this.currentTab].requestUnFoldAll=true; } + if (ImGui::Button("Fold all")) { this.tabs[this.currentTab].requestFoldAll=true; } + if (ImGui::Button("UnFold all")) { this.tabs[this.currentTab].requestUnFoldAll=true; } ImGui::Separator(); ImGui::TextDisabled("Invalid states:"); @@ -528,10 +528,10 @@ class ScriptEditorWindow { ImGui::Separator(); ImGui::TextDisabled("DBG ExceptionPanel"); - if (ImGui::Button("Err1")) { this.tabs[this.currentTab].epanel.addExceptionInternal(nid, "Err1"); } - if (ImGui::Button("ErrB")) { this.tabs[this.currentTab].epanel.addExceptionInternal(nid, "ErrB"); } - if (ImGui::Button("Wtf")) { this.tabs[this.currentTab].epanel.addExceptionInternal(nid, "Wtf"); } - if (ImGui::Button("Wat?")) { this.tabs[this.currentTab].epanel.addExceptionInternal(nid, "Wat?"); } + if (ImGui::Button("Err1")) { this.tabs[this.currentTab].epanel.addExceptionInternal(nid, "Err1"); } + if (ImGui::Button("ErrB")) { this.tabs[this.currentTab].epanel.addExceptionInternal(nid, "ErrB"); } + if (ImGui::Button("Wtf")) { this.tabs[this.currentTab].epanel.addExceptionInternal(nid, "Wtf"); } + if (ImGui::Button("Wat?")) { this.tabs[this.currentTab].epanel.addExceptionInternal(nid, "Wat?"); } }*/ ImGui::EndMenu(); @@ -564,29 +564,29 @@ class ScriptEditorWindow } } - // Creates a child window with dynamic height (but clamped to hardcoded max value). + // Creates a child window with dynamic height (but clamped to hardcoded max value). private bool drawSelectableFileList(string title, string btnText, ScriptIndexerRecord@ record, string&inout out_selection) { - + bool retval = false; ImGui::TextDisabled(title+" ("+record.fileinfos.length()+"):"); - if (record.fileinfos.length() == 0) - { - return retval; // nothing to draw - } - - // Calc child window size - const int MAX_CHILD_HEIGHT = 300; - const int CHILD_WIDTH = 600; - int childHeight = record.fileinfos.length() * ImGui::GetTextLineHeightWithSpacing(); - //ImGui::Text("DBG record.fileinfos.length()=" + record.fileinfos.length() + ", childHeight{not clamped}=" + childHeight); - if (childHeight > MAX_CHILD_HEIGHT) - { - childHeight = MAX_CHILD_HEIGHT; - } - - // draw the child window - ImGui::BeginChild(title+"-child", vector2(CHILD_WIDTH, childHeight), /*border:*/false, ImGuiWindowFlags_HorizontalScrollbar); + if (record.fileinfos.length() == 0) + { + return retval; // nothing to draw + } + + // Calc child window size + const int MAX_CHILD_HEIGHT = 300; + const int CHILD_WIDTH = 600; + int childHeight = record.fileinfos.length() * ImGui::GetTextLineHeightWithSpacing(); + //ImGui::Text("DBG record.fileinfos.length()=" + record.fileinfos.length() + ", childHeight{not clamped}=" + childHeight); + if (childHeight > MAX_CHILD_HEIGHT) + { + childHeight = MAX_CHILD_HEIGHT; + } + + // draw the child window + ImGui::BeginChild(title+"-child", vector2(CHILD_WIDTH, childHeight), /*border:*/false, ImGuiWindowFlags_HorizontalScrollbar); for (uint i=0; i bufferLinesMeta; // metadata for lines, see `analyzeLines()` - array> bufferMessageIDs; - dictionary workBufferRegions; // Key: region names (must be unique, otherwise not foldable), value: RegionInfo + string buffer; // The work buffer (doesn't contain folded regions!) - statically sized, determines maximum number of characters. DearIMGUI fills unused space by NULs. + uint totalChars; // Actual number of characters before the NUL-ified space; Important for saving. // see `analyzeLines()` + array bufferLinesMeta; // metadata for lines, see `analyzeLines()` + array> bufferMessageIDs; + dictionary workBufferRegions; // Key: region names (must be unique, otherwise not foldable), value: RegionInfo // END text buffer - + // MESSAGES FROM ANGELSCRIPT ENGINE - array messages; - int messagesTotalErr = 0; - int messagesTotalWarn = 0; - int messagesTotalInfo = 0; + array messages; + int messagesTotalErr = 0; + int messagesTotalWarn = 0; + int messagesTotalInfo = 0; // END messages from angelscript - + // CONTEXT float scrollY = 0.f; @@ -707,7 +707,7 @@ class ScriptEditorTab { @epanel = ExceptionsPanel(this); } - + void drawStartStopButton() { if (this.waitingForManipEvent) // When waiting for async result of (UN)LOAD_SCRIPT_REQUESTED @@ -732,36 +732,36 @@ class ScriptEditorTab } } } - + protected float measureLineInfoColumns() { // Reserve space for the metainfo columns float metaColumnsTotalWidth = 0.f; if (drawLineNumbers) - metaColumnsTotalWidth += lineNumColumnWidth; + metaColumnsTotalWidth += lineNumColumnWidth; if (drawLineLengths) - metaColumnsTotalWidth += lineLenColumnWidth; + metaColumnsTotalWidth += lineLenColumnWidth; if (drawLineCommentOffsets) - metaColumnsTotalWidth += lineCommentColumnWidth; + metaColumnsTotalWidth += lineCommentColumnWidth; if (drawLineHttpOffsets) - metaColumnsTotalWidth += lineHttpColumnWidth; + metaColumnsTotalWidth += lineHttpColumnWidth; errCountOffset = metaColumnsTotalWidth; if (drawLineErrCounts) - metaColumnsTotalWidth += errCountColumnWidth; + metaColumnsTotalWidth += errCountColumnWidth; if (drawLineAutoIndentLevels) - metaColumnsTotalWidth += lineAutoIndentLevelsColumnWidth; + metaColumnsTotalWidth += lineAutoIndentLevelsColumnWidth; if (drawLineActualIndents) - metaColumnsTotalWidth += lineActualIndentsColumnWidth; + metaColumnsTotalWidth += lineActualIndentsColumnWidth; if (drawLineRegionMarkers) - metaColumnsTotalWidth += lineRegionMarkerColumnWidth; + metaColumnsTotalWidth += lineRegionMarkerColumnWidth; return metaColumnsTotalWidth; } - + protected void drawLineInfoColumns(vector2 screenCursor) { ImGui::PushID("lineInfoCol"); - + // draws columns with info (line number etc...) on the left side of the text area // =============================================================================== @@ -798,7 +798,7 @@ class ScriptEditorTab } nextLineNumber += lineShift; } - + // Update code folding state; no drawing yet - must be done before coarse clipping if (drawLineRegionMarkers) { @@ -856,12 +856,12 @@ class ScriptEditorTab } } } - + // Coarse clipping if (linenumCursor.y < coarseClipTopY) - continue; + continue; if (linenumCursor.y > coarseClipBottomY) - break; + break; if (drawLineNumbers) // actually draw { @@ -872,21 +872,21 @@ class ScriptEditorTab if (drawLineLengths) { drawlist.AddText(linenumCursor, lineLenColor, - ""+int(this.bufferLinesMeta[lineIdx]['len'])); + ""+int(this.bufferLinesMeta[lineIdx]['len'])); linenumCursor.x += lineLenColumnWidth; } if (drawLineCommentOffsets) { drawlist.AddText(linenumCursor, lineCommentOffsetColor, - ""+int(this.bufferLinesMeta[lineIdx]['nonCommentLen'])); + ""+int(this.bufferLinesMeta[lineIdx]['nonCommentLen'])); linenumCursor.x += lineCommentColumnWidth; } - + if (drawLineHttpOffsets) { drawlist.AddText(linenumCursor, lineHttpOffsetColor, - ""+int(this.bufferLinesMeta[lineIdx]['nonHttpLen'])); + ""+int(this.bufferLinesMeta[lineIdx]['nonHttpLen'])); linenumCursor.x += lineHttpColumnWidth; } @@ -895,10 +895,10 @@ class ScriptEditorTab { if (bufferMessageIDs.length() > uint(lineIdx) // sanity check - && bufferMessageIDs[lineIdx].length() > 0) + && bufferMessageIDs[lineIdx].length() > 0) { // Draw num errors to the column - + drawlist.AddText(linenumCursor, color(1.f,0.5f,0.4f,1.f), ""+bufferMessageIDs[lineIdx].length()); } linenumCursor.x += errCountColumnWidth; @@ -908,15 +908,15 @@ class ScriptEditorTab if (drawLineAutoIndentLevels) { drawlist.AddText(linenumCursor, lineAutoIndentLevelsColor, - ""+int(this.bufferLinesMeta[lineIdx]['autoIndentLevel']) * autoIndentNumSpaces); + ""+int(this.bufferLinesMeta[lineIdx]['autoIndentLevel']) * autoIndentNumSpaces); linenumCursor.x += lineAutoIndentLevelsColumnWidth; } - + // Actual indentation length if (drawLineActualIndents) { drawlist.AddText(linenumCursor, lineActualIndentsColor, - ""+int(this.bufferLinesMeta[lineIdx]['actualIndentBlanks'])); + ""+int(this.bufferLinesMeta[lineIdx]['actualIndentBlanks'])); linenumCursor.x += lineActualIndentsColumnWidth; } @@ -977,7 +977,7 @@ class ScriptEditorTab // sanity checks if (this.bufferLinesMeta.length() <= uint(lineIdx)) - continue; + continue; // first draw comment highlights (semitransparent quads) // special touch: if the comments are #[end]region markers, highlight them different. @@ -1014,7 +1014,7 @@ class ScriptEditorTab string nonHttpStr = buffer.substr(startOffset, nonHttpLen); vector2 nonHttpSize = ImGui::CalcTextSize(nonHttpStr); vector2 pos = lineCursor + vector2(nonHttpSize.x, 0.f); - + // calc size string httpStr = buffer.substr(httpStart, httpLen); vector2 httpSize = ImGui::CalcTextSize(httpStr); @@ -1030,13 +1030,13 @@ class ScriptEditorTab // sanity check if (this.bufferMessageIDs.length() <= uint(lineIdx)) - continue; + continue; // then errors and warnings for (uint i = 0; i < this.bufferMessageIDs[lineIdx].length(); i++) { uint msgIdx = this.bufferMessageIDs[lineIdx][i]; - + int msgRow = int(this.messages[msgIdx]['row']); int msgCol = int(this.messages[msgIdx]['col']); int msgType = int(this.messages[msgIdx]['type']); @@ -1048,1197 +1048,1221 @@ class ScriptEditorTab switch (msgType) { case asMSGTYPE_ERROR: - col = errorTextColor; - bgCol = errorTextBgColor; - shouldDraw = drawErrorText; - break; + col = errorTextColor; + bgCol = errorTextBgColor; + shouldDraw = drawErrorText; + break; case asMSGTYPE_WARNING: - col = warnTextColor; - bgCol = warnTextBgColor; - shouldDraw = drawWarnText; - break; + col = warnTextColor; + bgCol = warnTextBgColor; + shouldDraw = drawWarnText; + break; default:; } if (!shouldDraw) - continue; + continue; // Calc horizontal offset string subStr = buffer.substr(startOffset, msgCol-1); vector2 textPos = lineCursor + vector2( - ImGui::CalcTextSize(subStr).x, // X offset - under the indicated text position - ImGui::GetTextLineHeight()-2); // Y offset - slightly below the current line - + ImGui::CalcTextSize(subStr).x, // X offset - under the indicated text position + ImGui::GetTextLineHeight()-2); // Y offset - slightly below the current line + // Draw message text with background string errText = " ^ "+msgText; if (containingFoldedRegionName != "") { // Special handling for folded regions - place message in-between lines, after region name (expect #endregion to be 'named' too!) - errText = " } "+msgText; - textPos += vector2(ImGui::CalcTextSize("// #***region "+containingFoldedRegionName).x, ImGui::GetTextLineHeight()/2); - } - vector2 errTextSize = ImGui::CalcTextSize(errText); - drawlist.AddRectFilled(textPos, textPos+errTextSize, bgCol); - drawlist.AddText(textPos , col, errText); - - // advance to next line - lineCursor.y += ImGui::GetTextLineHeight(); + errText = " } "+msgText; + textPos += vector2(ImGui::CalcTextSize("// #***region "+containingFoldedRegionName).x, ImGui::GetTextLineHeight()/2); } - } + vector2 errTextSize = ImGui::CalcTextSize(errText); + drawlist.AddRectFilled(textPos, textPos+errTextSize, bgCol); + drawlist.AddText(textPos , col, errText); + // advance to next line + lineCursor.y += ImGui::GetTextLineHeight(); + } + } + + +} - } +void drawTextArea() +{ - void drawTextArea() - { - - // Reserve space for the metainfo columns - float metaColumnsTotalWidth = measureLineInfoColumns(); - - // Draw the multiline text input - this.textAreaSize = vector2( - ImGui::GetWindowSize().x - (metaColumnsTotalWidth + 20), - ImGui::GetWindowSize().y - (115 + epanel.getHeightToReserve())); - - vector2 screenCursor = ImGui::GetCursorScreenPos() + /*frame padding:*/vector2(0.f, 4.f); - ImGui::SetCursorPosX(ImGui::GetCursorPosX() + metaColumnsTotalWidth); - bool changed = ImGui::InputTextMultiline(BUFFER_TEXTINPUT_LABEL, this.buffer, textAreaSize); - - // Trick: re-enter the inputtext panel to scroll it; see https://github.com/ocornut/imgui/issues/1523 - ImGui::BeginChild(ImGui::GetID(BUFFER_TEXTINPUT_LABEL)); - this.scrollX = ImGui::GetScrollX(); - this.scrollY = ImGui::GetScrollY(); - this.scrollMaxX = ImGui::GetScrollMaxX(); - this.scrollMaxY = ImGui::GetScrollMaxY(); - ImGui::EndChild(); - - if (changed) - { - this.analyzeLines(); - // Enlarge buffer if at capacity - if (this.totalChars == (this.buffer.length() - 1)) - this.buffer.resize(this.buffer.length() + BUFFER_INCREMENT_SIZE); - } - - this.drawLineInfoColumns(screenCursor); - screenCursor.x+= metaColumnsTotalWidth; - // Apparently drawing errors now makes them appear under the editor text; - // we must remember values and draw separately. - this.errorsScreenCursor = screenCursor; - this.errorsTextAreaSize = textAreaSize; - - } + // Reserve space for the metainfo columns + float metaColumnsTotalWidth = measureLineInfoColumns(); - void drawExceptionsPanel() - { - - if (epanel.getHeightToReserve() == 0) - { - return; // panel is empty, do not draw - } - - // Draw the exceptions panel - vector2 ePanelSize( - ImGui::GetWindowSize().x - (20), - epanel.getHeightToReserve()); - ImGui::BeginChild("ExceptionsPanel", ePanelSize); - epanel.drawExceptions(); - ImGui::EndChild(); // must always be called - legacy reasons - } + // Draw the multiline text input + this.textAreaSize = vector2( + ImGui::GetWindowSize().x - (metaColumnsTotalWidth + 20), + ImGui::GetWindowSize().y - (115 + epanel.getHeightToReserve())); - void drawFooter() - { - - if (epanel.getHeightToReserve() == 0) - { - // make top gap (window padding) rougly same size as bottom gap (item spacing) - ImGui::SetCursorPosY(ImGui::GetCursorPosY()+2); - } - else - { - // Fix footer jumping down (??) - ImGui::SetCursorPosY(ImGui::GetCursorPosY()-3); - } + vector2 screenCursor = ImGui::GetCursorScreenPos() + /*frame padding:*/vector2(0.f, 4.f); + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + metaColumnsTotalWidth); + bool changed = ImGui::InputTextMultiline(BUFFER_TEXTINPUT_LABEL, this.buffer, textAreaSize); - // footer with status info - ImGui::Separator(); - string runningTxt = "NOT RUNNING"; - if (this.currentScriptUnitID != SCRIPTUNITID_INVALID) - { - runningTxt = "RUNNING (NID: "+this.currentScriptUnitID+")"; - } - ImGui::Text(runningTxt - +"; lines: "+bufferLinesMeta.length() - +" (chars: "+this.totalChars+"/"+(this.buffer.length()-1)+")" // Leave buffer space for one '\0' - +"; messages:"+this.messages.length() - +" (err:"+this.messagesTotalErr - +"/warn:"+this.messagesTotalWarn - +"/info:"+this.messagesTotalInfo - +"); scrollY="+scrollY+"/"+scrollMaxY+""); // X="+scrollX+"/"+scrollMaxX - } + // Trick: re-enter the inputtext panel to scroll it; see https://github.com/ocornut/imgui/issues/1523 + ImGui::BeginChild(ImGui::GetID(BUFFER_TEXTINPUT_LABEL)); + this.scrollX = ImGui::GetScrollX(); + this.scrollY = ImGui::GetScrollY(); + this.scrollMaxX = ImGui::GetScrollMaxX(); + this.scrollMaxY = ImGui::GetScrollMaxY(); + ImGui::EndChild(); - void setBuffer(string data) + if (changed) { - // Make sure buffer is no smaller than BUFFER_MIN_SIZE - // and make sure it has at least BUFFER_INCREMENT_SIZE extra space. - // =============================================================== - this.buffer = data; - this.buffer.resize(this.buffer.length() + BUFFER_INCREMENT_SIZE); - if (this.buffer.length() < BUFFER_MIN_SIZE) - this.buffer.resize(BUFFER_MIN_SIZE); this.analyzeLines(); - editorWindow.refreshLocalFileList(); - } + // Enlarge buffer if at capacity + if (this.totalChars == (this.buffer.length() - 1)) + this.buffer.resize(this.buffer.length() + BUFFER_INCREMENT_SIZE); + } - private void runBufferInternal() // do not invoke during drawing ~ use `requestRunBuffer` - { - this.bufferMessageIDs.resize(0); // clear all - this.messages.resize(0); // clear all - this.epanel.clearExceptions(); - - this.backUpRegionFoldStates(); - this.unFoldAllRegionsInternal(); // OK to call here - we're already handling a request. - - game.pushMessage(MSG_APP_LOAD_SCRIPT_REQUESTED, { - {'filename', this.bufferName}, // Because we supply the buffer, this will serve only as display name - {'buffer', this.buffer }, - {'category', SCRIPT_CATEGORY_CUSTOM} - }); - waitingForManipEvent=true; - - this.restoreRegionFoldStates(); - } + this.drawLineInfoColumns(screenCursor); + screenCursor.x+= metaColumnsTotalWidth; + // Apparently drawing errors now makes them appear under the editor text; + // we must remember values and draw separately. + this.errorsScreenCursor = screenCursor; + this.errorsTextAreaSize = textAreaSize; + +} + +void drawExceptionsPanel() +{ - private void stopBufferInternal() // do not invoke during drawing ~ use `requestStopBuffer` + if (epanel.getHeightToReserve() == 0) { - game.pushMessage(MSG_APP_UNLOAD_SCRIPT_REQUESTED, { - {'id', this.currentScriptUnitID} - }); - waitingForManipEvent=true; - } + return; // panel is empty, do not draw + } + + // Draw the exceptions panel + vector2 ePanelSize( + ImGui::GetWindowSize().x - (20), + epanel.getHeightToReserve()); + ImGui::BeginChild("ExceptionsPanel", ePanelSize); + epanel.drawExceptions(); + ImGui::EndChild(); // must always be called - legacy reasons +} + +void drawFooter() +{ - void onEventAngelScriptMsg(int scriptUnitId, int msgType, int row, int col, string sectionName, string message) + if (epanel.getHeightToReserve() == 0) { - /*game.log("DBG '"+this.bufferName+"' onEventAngelScriptMsg(): scriptUnitId:" + scriptUnitId - + ", msgType:" + msgType + ", row:" + row + ", col:" + col // ints - + ", sectionName:" + sectionName + ", message:" + message); // strings*/ - - messages.insertLast({ {'type', msgType}, {'row',row}, {'col', col}, {'sectionName', sectionName}, {'message', message} }); - this.analyzeMessages(); + // make top gap (window padding) rougly same size as bottom gap (item spacing) + ImGui::SetCursorPosY(ImGui::GetCursorPosY()+2); } - - void onEventAngelScriptManip(int manipType, int scriptUnitId, int scriptCategory, string scriptName) + else { - /*game.log ("DBG '"+this.bufferName+"'.onEventAngelScriptManip(): manipType:"+ manipType+", scriptUnitId:"+ scriptUnitId - + ", scriptCategory:"+scriptCategory+", scriptName:"+scriptName+"; //this.waitingForManipEvent:"+this.waitingForManipEvent);*/ - - // Only handle LOADED manip if we're waiting for it - if (manipType == ASMANIP_SCRIPT_LOADED - && this.currentScriptUnitID == SCRIPTUNITID_INVALID - && this.bufferName == scriptName - && this.waitingForManipEvent) - { - this.currentScriptUnitID = scriptUnitId; - waitingForManipEvent = false; - // Force registering of exception callback so that the editor can monitor exceptions. - game.setRegisteredEventsMask(scriptUnitId, - game.getRegisteredEventsMask(scriptUnitId) | SE_ANGELSCRIPT_EXCEPTIONCALLBACK); - /*game.log ("DBG '"+this.bufferName+"'.onEventAngelScriptManip(): Now running with NID="+this.currentScriptUnitID);*/ - } - // Handle UNLOADING manip even if not expected - user may abort script manually via Console/ScriptMonitorUI or it may have crashed and get killed by the editor (see `killScriptOnAngelscriptException`). - else if (manipType == ASMANIP_SCRIPT_UNLOADING - && this.currentScriptUnitID == scriptUnitId - && this.bufferName == scriptName) - { - /*game.log ("DBG '"+this.bufferName+"'.onEventAngelScriptManip(): Stopping, was using NID="+this.currentScriptUnitID);*/ - this.currentScriptUnitID = SCRIPTUNITID_INVALID; - waitingForManipEvent = false; - } - + // Fix footer jumping down (??) + ImGui::SetCursorPosY(ImGui::GetCursorPosY()-3); } - void analyzeLines() + // footer with status info + ImGui::Separator(); + string runningTxt = "NOT RUNNING"; + if (this.currentScriptUnitID != SCRIPTUNITID_INVALID) { - this.analyzeBuffer(); - this.analyzeMessages(); + runningTxt = "RUNNING (NID: "+this.currentScriptUnitID+")"; } + ImGui::Text(runningTxt + +"; lines: "+bufferLinesMeta.length() + +" (chars: "+this.totalChars+"/"+(this.buffer.length()-1)+")" // Leave buffer space for one '\0' + +"; messages:"+this.messages.length() + +" (err:"+this.messagesTotalErr + +"/warn:"+this.messagesTotalWarn + +"/info:"+this.messagesTotalInfo + +"); scrollY="+scrollY+"/"+scrollMaxY+""); // X="+scrollX+"/"+scrollMaxX +} + +void setBuffer(string data) +{ + // Make sure buffer is no smaller than BUFFER_MIN_SIZE + // and make sure it has at least BUFFER_INCREMENT_SIZE extra space. + // =============================================================== + this.buffer = data; + this.buffer.resize(this.buffer.length() + BUFFER_INCREMENT_SIZE); + if (this.buffer.length() < BUFFER_MIN_SIZE) + this.buffer.resize(BUFFER_MIN_SIZE); + this.analyzeLines(); + editorWindow.refreshLocalFileList(); +} + +private void runBufferInternal() // do not invoke during drawing ~ use `requestRunBuffer` +{ + this.bufferMessageIDs.resize(0); // clear all + this.messages.resize(0); // clear all + this.epanel.clearExceptions(); + + this.backUpRegionFoldStates(); + this.unFoldAllRegionsInternal(); // OK to call here - we're already handling a request. + + game.pushMessage(MSG_APP_LOAD_SCRIPT_REQUESTED, { + {'filename', this.bufferName}, // Because we supply the buffer, this will serve only as display name + {'buffer', this.buffer }, + {'category', SCRIPT_CATEGORY_CUSTOM} + }); + waitingForManipEvent=true; + + this.restoreRegionFoldStates(); +} + +private void stopBufferInternal() // do not invoke during drawing ~ use `requestStopBuffer` +{ + game.pushMessage(MSG_APP_UNLOAD_SCRIPT_REQUESTED, { + {'id', this.currentScriptUnitID} + }); + waitingForManipEvent=true; +} + +void onEventAngelScriptMsg(int scriptUnitId, int msgType, int row, int col, string sectionName, string message) +{ + /*game.log("DBG '"+this.bufferName+"' onEventAngelScriptMsg(): scriptUnitId:" + scriptUnitId + + ", msgType:" + msgType + ", row:" + row + ", col:" + col // ints + + ", sectionName:" + sectionName + ", message:" + message); // strings*/ + +messages.insertLast({ {'type', msgType}, {'row',row}, {'col', col}, {'sectionName', sectionName}, {'message', message} }); + this.analyzeMessages(); +} + +void onEventAngelScriptManip(int manipType, int scriptUnitId, int scriptCategory, string scriptName) +{ + /*game.log ("DBG '"+this.bufferName+"'.onEventAngelScriptManip(): manipType:"+ manipType+", scriptUnitId:"+ scriptUnitId + + ", scriptCategory:"+scriptCategory+", scriptName:"+scriptName+"; //this.waitingForManipEvent:"+this.waitingForManipEvent);*/ - private void analyzeBuffer() // helper for `analyzeLines()` + // Only handle LOADED manip if we're waiting for it + if (manipType == ASMANIP_SCRIPT_LOADED + && this.currentScriptUnitID == SCRIPTUNITID_INVALID + && this.bufferName == scriptName + && this.waitingForManipEvent) { - // [profiling] - Ogre::Timer timer; - uint totalMicrosecDict = 0; // the `bufferLinesMeta` array of dicts - uint totalMicrosecCpuDict = 0; - uint totalMicrosecRegions = 0; - uint totalMicrosecCpuRegions = 0; - - this.bufferLinesMeta.resize(0); // clear all - int startOffset = 0; - this.totalChars = 0; - - // pattern - comment - string commentPattern = "//"; - uint commentLevel = 0; - int commentStart = -1; - bool commentFound = false; - - // pattern - hyperlink - string httpPattern = 'http://'; - uint httpLevel = 0; - int httpStart = -1; - bool httpFound = false; - - // pattern - #region & #endregion (C#-like, inside comment) - // NOTE: Regions cannot be nested. '#[end]region' lines are always present in `buffer` even if folded. - string regionPattern = '#region'; - uint regionLevel = 0; - int regionTitleStart = -1; - bool regionFound = false; - string endregionPattern = '#endregion'; - uint endregionLevel = 0; - bool endregionFound = false; - // Code folding - dictionary collectedRegions; - int regionFoundAtLineIdx = -1; - string regionFoundWithName; - int regionBodyStartOffset = -1; - - string httpBreakChars = " \n\t\"'"; - bool httpBreakFound = false; - int httpBreakPos = -1; - - int autoIndentLevelCurrent = 0; - int autoIndentLevelNext = 0; - int actualIndentBlanks = 0; // current line - bool actualIndentFound = false; - - string SPECIALCHARS = "{}\n\t \0"; - - for (uint i = 0; i < this.buffer.length(); i++) - { - uint lineIdx = this.bufferLinesMeta.length(); - - // analyze the character - uint theChar = this.buffer[i]; - bool isCharOpenBrace = theChar == SPECIALCHARS[0]; - bool isCharCloseBrace = theChar == SPECIALCHARS[1]; - bool isCharNewline = theChar == SPECIALCHARS[2]; - bool isCharTab = theChar == SPECIALCHARS[3]; - bool isCharSpace = theChar == SPECIALCHARS[4]; - bool isCharNul = theChar == SPECIALCHARS[5]; - - // doubleslash comment - if (!commentFound) + this.currentScriptUnitID = scriptUnitId; + waitingForManipEvent = false; + // Force registering of exception callback so that the editor can monitor exceptions. + game.setRegisteredEventsMask(scriptUnitId, + game.getRegisteredEventsMask(scriptUnitId) | SE_ANGELSCRIPT_EXCEPTIONCALLBACK); + /*game.log ("DBG '"+this.bufferName+"'.onEventAngelScriptManip(): Now running with NID="+this.currentScriptUnitID);*/ + } + // Handle UNLOADING manip even if not expected - user may abort script manually via Console/ScriptMonitorUI or it may have crashed and get killed by the editor (see `killScriptOnAngelscriptException`). + else if (manipType == ASMANIP_SCRIPT_UNLOADING + && this.currentScriptUnitID == scriptUnitId + && this.bufferName == scriptName) + { + /*game.log ("DBG '"+this.bufferName+"'.onEventAngelScriptManip(): Stopping, was using NID="+this.currentScriptUnitID);*/ + this.currentScriptUnitID = SCRIPTUNITID_INVALID; + waitingForManipEvent = false; + } + +} + +void analyzeLines() +{ + this.analyzeBuffer(); + this.analyzeMessages(); +} + +private void analyzeBuffer() // helper for `analyzeLines()` +{ + // [profiling] + Ogre::Timer timer; + uint totalMicrosecDict = 0; // the `bufferLinesMeta` array of dicts + uint totalMicrosecCpuDict = 0; + uint totalMicrosecRegions = 0; + uint totalMicrosecCpuRegions = 0; + + this.bufferLinesMeta.resize(0); // clear all + int startOffset = 0; + this.totalChars = 0; + + // pattern - comment + string commentPattern = "//"; + uint commentLevel = 0; + int commentStart = -1; + bool commentFound = false; + + // pattern - hyperlink + string httpPattern = 'http://'; + uint httpLevel = 0; + int httpStart = -1; + bool httpFound = false; + + // pattern - #region & #endregion (C#-like, inside comment) + // NOTE: Regions cannot be nested. '#[end]region' lines are always present in `buffer` even if folded. + string regionPattern = '#region'; + uint regionLevel = 0; + int regionTitleStart = -1; + bool regionFound = false; + string endregionPattern = '#endregion'; + uint endregionLevel = 0; + bool endregionFound = false; + // Code folding + dictionary collectedRegions; + int regionFoundAtLineIdx = -1; + string regionFoundWithName; + int regionBodyStartOffset = -1; + + string httpBreakChars = " \n\t\"'"; + bool httpBreakFound = false; + int httpBreakPos = -1; + + int autoIndentLevelCurrent = 0; + int autoIndentLevelNext = 0; + int actualIndentBlanks = 0; // current line + bool actualIndentFound = false; + +string SPECIALCHARS = "{}\n\t \0"; + + for (uint i = 0; i < this.buffer.length(); i++) + { + uint lineIdx = this.bufferLinesMeta.length(); + + // analyze the character + uint theChar = this.buffer[i]; + bool isCharOpenBrace = theChar == SPECIALCHARS[0]; + bool isCharCloseBrace = theChar == SPECIALCHARS[1]; + bool isCharNewline = theChar == SPECIALCHARS[2]; + bool isCharTab = theChar == SPECIALCHARS[3]; + bool isCharSpace = theChar == SPECIALCHARS[4]; + bool isCharNul = theChar == SPECIALCHARS[5]; + + // doubleslash comment + if (!commentFound) + { + if (this.buffer[i] == commentPattern[commentLevel] + && (httpStart == -1 || httpBreakFound)) // Either we're not in hyperlink yet or we're after it already.) { - if (this.buffer[i] == commentPattern[commentLevel] - && (httpStart == -1 || httpBreakFound)) // Either we're not in hyperlink yet or we're after it already.) - { - commentLevel++; - - // Record the '//', but make sure it's not within hyperlink! - if (commentStart == -1) - { - commentStart = i; - } - - if (uint(commentLevel) == commentPattern.length()) - { - commentFound = true; - } - } - else - { - commentLevel = 0; - } - } - else - { - if (!regionFound) + commentLevel++; + + // Record the '//', but make sure it's not within hyperlink! + if (commentStart == -1) { - //if (regionLevel > 0) game.log("DBG analyzeBuffer(): char="+i+", line="+lineIdx+", regionLevel="+regionLevel+"/"+regionPattern.length()); - if (this.buffer[i] == regionPattern[regionLevel]) - { - regionLevel++; - if (uint(regionLevel) == regionPattern.length()) - { - regionFound = true; - regionTitleStart = i+1; - } - } - else - { - regionLevel = 0; - } + commentStart = i; } - if (!endregionFound) + if (uint(commentLevel) == commentPattern.length()) { - //if (endregionLevel > 0) game.log("DBG analyzeBuffer(): char="+i+", line="+lineIdx+", endregionLevel="+endregionLevel+"/"+endregionPattern.length()); - if (this.buffer[i] == endregionPattern[endregionLevel]) - { - endregionLevel++; - if (uint(endregionLevel) == endregionPattern.length()) - { - endregionFound = true; - } - } - else - { - endregionLevel = 0; - } - } + commentFound = true; + } } - - // http protocol - if (!httpFound) + else + { + commentLevel = 0; + } + } + else + { + if (!regionFound) { - if (this.buffer[i] == httpPattern[httpLevel]) + //if (regionLevel > 0) game.log("DBG analyzeBuffer(): char="+i+", line="+lineIdx+", regionLevel="+regionLevel+"/"+regionPattern.length()); + if (this.buffer[i] == regionPattern[regionLevel]) { - httpLevel++; - - if (httpStart == -1) + regionLevel++; + if (uint(regionLevel) == regionPattern.length()) { - httpStart = i; + regionFound = true; + regionTitleStart = i+1; } - - if (uint(httpLevel) >= httpPattern.length()) - { - httpFound = true; - } } else { - httpLevel = 0; + regionLevel = 0; } } - else if (!httpBreakFound) + + if (!endregionFound) { - for (uint j = 0; j < httpBreakChars.length(); j++) + //if (endregionLevel > 0) game.log("DBG analyzeBuffer(): char="+i+", line="+lineIdx+", endregionLevel="+endregionLevel+"/"+endregionPattern.length()); + if (this.buffer[i] == endregionPattern[endregionLevel]) { - if (this.buffer[i] == httpBreakChars[j]) + endregionLevel++; + if (uint(endregionLevel) == endregionPattern.length()) { - httpBreakFound = true; - httpBreakPos = i; + endregionFound = true; } } - } - - // Auto indentation - always execute to display the debug levels - if (isCharOpenBrace) - { - autoIndentLevelNext++; - } - else if (isCharCloseBrace) - { - // Allow negative values, for debugging. - autoIndentLevelNext--; - autoIndentLevelCurrent--; - } - - // Actual indentation - always execute - if (!actualIndentFound) - { - if (isCharSpace || isCharTab) - { - actualIndentBlanks++; - } else { - actualIndentFound = true; + endregionLevel = 0; } } + } - if (isCharNewline || isCharNul) + // http protocol + if (!httpFound) + { + if (this.buffer[i] == httpPattern[httpLevel]) { - int endOffset = i; + httpLevel++; - uint microsecBeforeRegions = timer.getMicroseconds(); - uint microsecCpuBeforeRegions = timer.getMicrosecondsCPU(); - - // Process #region - if (regionFound) + if (httpStart == -1) { - regionFoundAtLineIdx = lineIdx; - regionFoundWithName = trimLeft(this.buffer.substr(regionTitleStart, endOffset - regionTitleStart)); - regionBodyStartOffset = endOffset+1; - //game.log("DBG analyzeBuffer(): regionFound: withName="+regionFoundWithName+" atLineIdx="+regionFoundAtLineIdx+" bodyStartOffset="+regionBodyStartOffset); + httpStart = i; } - // Process #endregion - if (endregionFound && regionFoundAtLineIdx != -1 && !collectedRegions.exists(regionFoundWithName)) + if (uint(httpLevel) >= httpPattern.length()) { - RegionInfo regionInfo; - regionInfo.regionLineCount = (lineIdx-1)-regionFoundAtLineIdx; // To handle jumps in line numbering - regionInfo.regionBodyStartOffset = regionBodyStartOffset; // To swap regions in and out from work buffer. - regionInfo.regionBodyNumChars = (int(this.bufferLinesMeta[lineIdx-1]['endOffset']) - regionBodyStartOffset)+1; // ditto - regionInfo.regionStartsAtLineIndex = regionFoundAtLineIdx; - collectedRegions[regionFoundWithName] = regionInfo; - //game.log("DBG analyzeBuffer(): endregionFound: withName="+regionFoundWithName+" lineCount="+regionInfo.regionLineCount+" bodyStartOffset="+regionInfo.regionBodyStartOffset+" numChars="+regionInfo.regionBodyNumChars); - regionFoundAtLineIdx = -1; - regionFoundWithName = ""; - } - - totalMicrosecRegions += timer.getMicroseconds() - microsecBeforeRegions; - totalMicrosecCpuRegions += timer.getMicrosecondsCPU() - microsecCpuBeforeRegions; - - // Finish line - uint microsecBeforeDict = timer.getMicroseconds(); - uint microsecCpuBeforeDict = timer.getMicrosecondsCPU(); - - this.bufferLinesMeta.insertLast({ {'startOffset', startOffset} }); - this.bufferLinesMeta[lineIdx]['endOffset'] = endOffset; - int len = endOffset - startOffset; - this.bufferLinesMeta[lineIdx]['len'] = len; - this.bufferLinesMeta[lineIdx]['doubleslashCommentStart'] = commentStart; - int nonCommentLen = (commentFound && commentStart >= 0) ? commentStart - startOffset : len; - this.bufferLinesMeta[lineIdx]['nonCommentLen'] = nonCommentLen; - this.bufferLinesMeta[lineIdx]['httpStart'] = (httpFound) ? httpStart : -1; - this.bufferLinesMeta[lineIdx]['httpLen'] = (httpFound) ? httpBreakPos-httpStart : -1; - this.bufferLinesMeta[lineIdx]['nonHttpLen'] = (httpFound) ? httpStart - startOffset : -1; - this.bufferLinesMeta[lineIdx]['regionFound'] = regionFound; - this.bufferLinesMeta[lineIdx]['regionValid'] = regionFound && regionFoundWithName != "" && !collectedRegions.exists(regionFoundWithName); - this.bufferLinesMeta[lineIdx]['regionName'] = regionFoundWithName; - this.bufferLinesMeta[lineIdx]['endregionFound'] = endregionFound; - this.bufferLinesMeta[lineIdx]['autoIndentLevel'] = autoIndentLevelCurrent; - this.bufferLinesMeta[lineIdx]['actualIndentBlanks'] = actualIndentBlanks; - - totalMicrosecDict += timer.getMicroseconds() - microsecBeforeDict; - totalMicrosecCpuDict += timer.getMicrosecondsCPU() - microsecCpuBeforeDict; + httpFound = true; + } } - - if (isCharNul) + else { - break; // We're done parsing the buffer - the rest is just more NULs. + httpLevel = 0; } - else if (isCharNewline) + } + else if (!httpBreakFound) + { + for (uint j = 0; j < httpBreakChars.length(); j++) { - // reset context - commentFound = false; - commentStart = -1; // -1 means Empty - commentLevel = 0; - - httpFound = false; - httpStart = -1; // -1 means Empty - httpLevel = 0; - httpBreakFound = false; - httpBreakPos = -1; // -1 means Empty - - regionFound = false; - regionTitleStart = -1; // -1 means Empty - regionLevel = 0; - - endregionFound = false; - endregionLevel = 0; - - actualIndentFound = false; - actualIndentBlanks = 0; - - autoIndentLevelCurrent = autoIndentLevelNext; - - // Start new line - startOffset = i+1; + if (this.buffer[i] == httpBreakChars[j]) + { + httpBreakFound = true; + httpBreakPos = i; + } } - - this.totalChars++; } - - // [profiling] - uint microsecBeforeMerging = timer.getMicroseconds(); - uint microsecCpuBeforeMerging = timer.getMicrosecondsCPU(); - - this.mergeCollectedFoldingRegionsWithExisting(collectedRegions); - - // [profiling] - uint totalMicrosecMerging = timer.getMicroseconds() - microsecBeforeMerging; - uint totalMicrosecCpuMerging = timer.getMicrosecondsCPU() - microsecCpuBeforeMerging; - game.log("PROFILING analyzeBuffer() [in microseconds]:" - +" total "+timer.getMicroseconds()+"us (CPU "+timer.getMicrosecondsCPU()+"us)" - +" regions "+totalMicrosecRegions+"us (CPU "+totalMicrosecCpuRegions+"us)" - +" dict "+totalMicrosecDict+"us (CPU "+totalMicrosecCpuDict+"us)" - +" merging "+totalMicrosecMerging+"us (CPU "+totalMicrosecCpuMerging+"us)" - ); - } - - private void mergeCollectedFoldingRegionsWithExisting(dictionary&in collectedRegions) // helper for `analyzeBuffer()` - { - // The `ImGui::TextInputMultiline()` doesn't provide info on where new characters were inserted/removed (or does it? TODO research ImGuiInputTextCallbackData) - // Either way, we simply scan the buffer again, collect the regions and then match them with those already existing. - // This relies on the #region/#endregion tags always being in the buffer, even if folded. - // --------------------------------------------------------------------------------------------------------------- - // prune broken regions (missing '#endregion' -> RegionInfo is null) - array@ collectedRegionNames = collectedRegions.getKeys(); - for (uint i = 0; i< collectedRegionNames.length(); i++) + // Auto indentation - always execute to display the debug levels + if (isCharOpenBrace) { - RegionInfo@ regionInfo = findRegion(collectedRegions, collectedRegionNames[i]); - if (@regionInfo == null) - { - //game.log ("DBG mergeCollectedFoldingRegionsWithExisting(): pruning broken region '" + collectedRegionNames[i] + "'"); - collectedRegions.delete(collectedRegionNames[i]); - } - } - - // Find regions that were deleted/changed - array oldRegionNames = this.workBufferRegions.getKeys(); - for (uint i = 0; i< oldRegionNames.length(); i++) + autoIndentLevelNext++; + } + else if (isCharCloseBrace) { - bool isGone = !collectedRegions.exists(oldRegionNames[i]); - RegionInfo@ oldRegionInfo = findRegion(this.workBufferRegions, oldRegionNames[i]); - if (isGone && oldRegionInfo.isFolded) - { - //game.log ("DBG mergeCollectedFoldingRegionsWithExisting(): region '" + oldRegionNames[i] + "' has gone orphan."); - oldRegionInfo.isOrphan = true; - } + // Allow negative values, for debugging. + autoIndentLevelNext--; + autoIndentLevelCurrent--; } - // Find regions that were (re)created - array@ newRegionNames = collectedRegions.getKeys(); - for (uint i = 0; i < newRegionNames.length(); i++) + // Actual indentation - always execute + if (!actualIndentFound) { - RegionInfo@ newRegionInfo = findRegion(collectedRegions, newRegionNames[i]); - RegionInfo@ existingRegionInfo = findRegion(this.workBufferRegions, newRegionNames[i]); - if (@existingRegionInfo == null) + if (isCharSpace || isCharTab) { - //game.log("DBG mergeCollectedFoldingRegionsWithExisting(): A brand new region '"+newRegionNames[i]+"' was created"); - this.workBufferRegions[newRegionNames[i]] = newRegionInfo; + actualIndentBlanks++; } else { - /*game.log("DBG mergeCollectedFoldingRegionsWithExisting(): Region '"+newRegionNames[i]+"' already exists:" - +" lineCount="+existingRegionInfo.regionLineCount+" (new:"+newRegionInfo.regionLineCount+")" - +" regionBodyStartOffset="+existingRegionInfo.regionBodyStartOffset+" (new:"+newRegionInfo.regionBodyStartOffset+")" - +" regionBodyNumChars="+existingRegionInfo.regionBodyNumChars+" (new:"+newRegionInfo.regionBodyNumChars+")" - +" isOrphan="+existingRegionInfo.isOrphan+" isFolded="+newRegionInfo.isFolded); - */ - - existingRegionInfo.regionBodyStartOffset = newRegionInfo.regionBodyStartOffset; - - if (!existingRegionInfo.isFolded) - { - //game.log("DBG mergeCollectedFoldingRegionsWithExisting(): An existing UNFOLDED region '"+newRegionNames[i]+"' was updated ~ text may have changed"); - existingRegionInfo.regionLineCount = newRegionInfo.regionLineCount; - existingRegionInfo.regionBodyNumChars = newRegionInfo.regionBodyNumChars; - existingRegionInfo.regionStartsAtLineIndex = newRegionInfo.regionStartsAtLineIndex; - } - else if (existingRegionInfo.isOrphan && newRegionInfo.regionLineCount == 0) - { - //game.log("DBG mergeCollectedFoldingRegionsWithExisting(): An orphan (so logically FOLDED) region '"+newRegionNames[i]+"' has resurfaced"); - existingRegionInfo.isOrphan = false; - } + actualIndentFound = true; } - } - } - - private void analyzeMessages() // helper for `analyzeLines()` - { - // reset caches - bufferMessageIDs.resize(0); // clear all - bufferMessageIDs.resize(bufferLinesMeta.length()); - - // reset global stats - this.messagesTotalErr = 0; - this.messagesTotalWarn = 0; - this.messagesTotalInfo = 0; - + } - for (uint i = 0; i < this.messages.length(); i++) + if (isCharNewline || isCharNul) { - int msgType = int(this.messages[i]['type']); - string msgText = string(this.messages[i]['message']); - string inFoldedRegion = ""; - uint lineIdx = this.determineLineIdxForMessage(i, /*[out]:*/inFoldedRegion); - this.messages[i]['inFoldedRegion'] = inFoldedRegion; // HACK: add extra dynamic field where only constant data from AngelScript should be - - // update line stats - if (msgType == asMSGTYPE_ERROR) - { - this.bufferMessageIDs[lineIdx].insertLast(i); - this.messagesTotalErr++; - } - if (msgType == asMSGTYPE_WARNING) + int endOffset = i; + + uint microsecBeforeRegions = timer.getMicroseconds(); + uint microsecCpuBeforeRegions = timer.getMicrosecondsCPU(); + + // Process #region + if (regionFound) { - this.bufferMessageIDs[lineIdx].insertLast(i); - this.messagesTotalWarn++; + regionFoundAtLineIdx = lineIdx; + regionFoundWithName = trimLeft(this.buffer.substr(regionTitleStart, endOffset - regionTitleStart)); + regionBodyStartOffset = endOffset+1; + //game.log("DBG analyzeBuffer(): regionFound: withName="+regionFoundWithName+" atLineIdx="+regionFoundAtLineIdx+" bodyStartOffset="+regionBodyStartOffset); } - if (msgType == asMSGTYPE_INFORMATION) + + // Process #endregion + if (endregionFound && regionFoundAtLineIdx != -1 && !collectedRegions.exists(regionFoundWithName)) { - this.messagesTotalInfo++; + RegionInfo regionInfo; + regionInfo.regionLineCount = (lineIdx-1)-regionFoundAtLineIdx; // To handle jumps in line numbering + regionInfo.regionBodyStartOffset = regionBodyStartOffset; // To swap regions in and out from work buffer. + regionInfo.regionBodyNumChars = (int(this.bufferLinesMeta[lineIdx-1]['endOffset']) - regionBodyStartOffset)+1; // ditto + regionInfo.regionStartsAtLineIndex = regionFoundAtLineIdx; + collectedRegions[regionFoundWithName] = regionInfo; + //game.log("DBG analyzeBuffer(): endregionFound: withName="+regionFoundWithName+" lineCount="+regionInfo.regionLineCount+" bodyStartOffset="+regionInfo.regionBodyStartOffset+" numChars="+regionInfo.regionBodyNumChars); + regionFoundAtLineIdx = -1; + regionFoundWithName = ""; } + + totalMicrosecRegions += timer.getMicroseconds() - microsecBeforeRegions; + totalMicrosecCpuRegions += timer.getMicrosecondsCPU() - microsecCpuBeforeRegions; + + // Finish line + uint microsecBeforeDict = timer.getMicroseconds(); + uint microsecCpuBeforeDict = timer.getMicrosecondsCPU(); + + this.bufferLinesMeta.insertLast({ {'startOffset', startOffset} }); + this.bufferLinesMeta[lineIdx]['endOffset'] = endOffset; + int len = endOffset - startOffset; + this.bufferLinesMeta[lineIdx]['len'] = len; + this.bufferLinesMeta[lineIdx]['doubleslashCommentStart'] = commentStart; + int nonCommentLen = (commentFound && commentStart >= 0) ? commentStart - startOffset : len; + this.bufferLinesMeta[lineIdx]['nonCommentLen'] = nonCommentLen; + this.bufferLinesMeta[lineIdx]['httpStart'] = (httpFound) ? httpStart : -1; + this.bufferLinesMeta[lineIdx]['httpLen'] = (httpFound) ? httpBreakPos-httpStart : -1; + this.bufferLinesMeta[lineIdx]['nonHttpLen'] = (httpFound) ? httpStart - startOffset : -1; + this.bufferLinesMeta[lineIdx]['regionFound'] = regionFound; + this.bufferLinesMeta[lineIdx]['regionValid'] = regionFound && regionFoundWithName != "" && !collectedRegions.exists(regionFoundWithName); + this.bufferLinesMeta[lineIdx]['regionName'] = regionFoundWithName; + this.bufferLinesMeta[lineIdx]['endregionFound'] = endregionFound; + this.bufferLinesMeta[lineIdx]['autoIndentLevel'] = autoIndentLevelCurrent; + this.bufferLinesMeta[lineIdx]['actualIndentBlanks'] = actualIndentBlanks; + + totalMicrosecDict += timer.getMicroseconds() - microsecBeforeDict; + totalMicrosecCpuDict += timer.getMicrosecondsCPU() - microsecCpuBeforeDict; } - } - - private uint determineLineIdxForMessage(uint msgIndex, string&inout inFoldedRegion) // helper for `analyzeMessages()` - { - // AngelScript reports line numbers from complete file, but `this.buffer` doesn't contain folded regions so line indices in `this.bufferLinesMeta` don't match. - // To determine the message's lineindex in the sparse `this.buffer`, we must go through region records and adjust the reported line number. - // Note the `this.workBufferRegions` set isn't ordered, we must process all regions that precede the message, and only then the region which contains it (if any). - // -------------------------------------------------------------------------------------------------------------------------------------------------------- - - int msgRow = int(this.messages[msgIndex]['row']); - int lineIdx = msgRow-1; - - //game.log("DBG determineLineIdxForMessage("+msgIndex+"): initial lineIdx="+lineIdx); - array regionKeys = this.workBufferRegions.getKeys(); - RegionInfo@ containingRegion = null; - string containingRegionName = ""; - for (uint i = 0; i < regionKeys.length(); i++) + if (isCharNul) { - RegionInfo@ regionInfo = findRegion(this.workBufferRegions, regionKeys[i]); - if (@regionInfo != null) - { - //game.log("DBG determineLineIdxForMessage("+msgIndex+"): considering region "+i+"/"+regionKeys.length()+" '"+regionKeys[i]+"' (isFolded="+regionInfo.isFolded+" startsAtLineIndex="+regionInfo.regionStartsAtLineIndex+" lineCount="+regionInfo.regionLineCount+")"); - - bool lowerBoundReached = (regionInfo.regionStartsAtLineIndex < lineIdx); - bool upperBoundReached = ((regionInfo.regionStartsAtLineIndex + regionInfo.regionLineCount) < lineIdx); - - if (regionInfo.isFolded) - { - if (!lowerBoundReached) - { - // This region precedes the message - lineIdx -= regionInfo.regionLineCount; - } - else if (lowerBoundReached && !upperBoundReached) - { - // This region contains the message - we must account it last to stay in sync. - @containingRegion = @regionInfo; - containingRegionName = regionKeys[i]; - } - } - - //game.log("DBG ... lowerBoundReached:"+lowerBoundReached+" upperBoundReached:"+upperBoundReached+" ="+(@containingRegion == @regionInfo)+"; new lineIdx="+lineIdx); - } + break; // We're done parsing the buffer - the rest is just more NULs. } - - if (@containingRegion!=null && containingRegion.isFolded) + else if (isCharNewline) { - lineIdx -= 1 + (lineIdx - containingRegion.regionStartsAtLineIndex); - inFoldedRegion = containingRegionName; + // reset context + commentFound = false; + commentStart = -1; // -1 means Empty + commentLevel = 0; + + httpFound = false; + httpStart = -1; // -1 means Empty + httpLevel = 0; + httpBreakFound = false; + httpBreakPos = -1; // -1 means Empty + + regionFound = false; + regionTitleStart = -1; // -1 means Empty + regionLevel = 0; + + endregionFound = false; + endregionLevel = 0; + + actualIndentFound = false; + actualIndentBlanks = 0; + + autoIndentLevelCurrent = autoIndentLevelNext; + + // Start new line + startOffset = i+1; } - // workaround: clamp the msgRow to known line count - if (lineIdx >= int(bufferLinesMeta.length())) - { - lineIdx = int(bufferLinesMeta.length()) - 1; - } - - //game.log("DBG determineLineIdxForMessage("+msgIndex+"): final lineIdx="+lineIdx); - return lineIdx; + this.totalChars++; } + + // [profiling] + uint microsecBeforeMerging = timer.getMicroseconds(); + uint microsecCpuBeforeMerging = timer.getMicrosecondsCPU(); + + this.mergeCollectedFoldingRegionsWithExisting(collectedRegions); + + // [profiling] + uint totalMicrosecMerging = timer.getMicroseconds() - microsecBeforeMerging; + uint totalMicrosecCpuMerging = timer.getMicrosecondsCPU() - microsecCpuBeforeMerging; + game.log("PROFILING analyzeBuffer() [in microseconds]:" + +" total "+timer.getMicroseconds()+"us (CPU "+timer.getMicrosecondsCPU()+"us)" + +" regions "+totalMicrosecRegions+"us (CPU "+totalMicrosecCpuRegions+"us)" + +" dict "+totalMicrosecDict+"us (CPU "+totalMicrosecCpuDict+"us)" + +" merging "+totalMicrosecMerging+"us (CPU "+totalMicrosecCpuMerging+"us)" + ); +} - void updateAutosave(float dt) +private void mergeCollectedFoldingRegionsWithExisting(dictionary&in collectedRegions) // helper for `analyzeBuffer()` +{ + // The `ImGui::TextInputMultiline()` doesn't provide info on where new characters were inserted/removed (or does it? TODO research ImGuiInputTextCallbackData) + // Either way, we simply scan the buffer again, collect the regions and then match them with those already existing. + // This relies on the #region/#endregion tags always being in the buffer, even if folded. + // --------------------------------------------------------------------------------------------------------------- + + // prune broken regions (missing '#endregion' -> RegionInfo is null) + array@ collectedRegionNames = collectedRegions.getKeys(); + for (uint i = 0; i< collectedRegionNames.length(); i++) { - if (autoSaveIntervalSec == 0.f) - return; // disabled - - this.autosaveTimeCounterSec+= dt; - this.autosaveResult = 0; - if (this.autosaveTimeCounterSec > autoSaveIntervalSec) + RegionInfo@ regionInfo = findRegion(collectedRegions, collectedRegionNames[i]); + if (@regionInfo == null) { - this.requestAutoSaveFile = true; - this.autosaveTimeCounterSec -= autoSaveIntervalSec; // don't miss a millisecond! + //game.log ("DBG mergeCollectedFoldingRegionsWithExisting(): pruning broken region '" + collectedRegionNames[i] + "'"); + collectedRegions.delete(collectedRegionNames[i]); } - } - - void drawAutosaveStatusbar() - { - ImGui::TextDisabled("Autosave:"); - ImGui::PushStyleColor(ImGuiCol_FrameBg, autoSaveStatusbarBgColor); - ImGui::SetCursorPosY(ImGui::GetCursorPosY()+5.f); - ImGui::ProgressBar( - /*fraction:*/this.autosaveTimeCounterSec/autoSaveIntervalSec, - /*size: */vector2(55.f, 13.f), - /*overlay: */''+formatFloat(autoSaveIntervalSec-this.autosaveTimeCounterSec, "", 3, 1)+'sec'); - ImGui::PopStyleColor(1); // ImGuiCol_FrameBg - } - - void handleRequests(float dt) + } + + // Find regions that were deleted/changed + array oldRegionNames = this.workBufferRegions.getKeys(); + for (uint i = 0; i< oldRegionNames.length(); i++) { - if (this.requestFoldRegion != "") + bool isGone = !collectedRegions.exists(oldRegionNames[i]); + RegionInfo@ oldRegionInfo = findRegion(this.workBufferRegions, oldRegionNames[i]); + if (isGone && oldRegionInfo.isFolded) { - this.foldRegionInternal(this.requestFoldRegion); - this.requestFoldRegion = ""; + //game.log ("DBG mergeCollectedFoldingRegionsWithExisting(): region '" + oldRegionNames[i] + "' has gone orphan."); + oldRegionInfo.isOrphan = true; } - else if (this.requestUnFoldRegion != "") - { - //game.log("DBG requestUnFoldRegion: '"+ this.requestUnFoldRegion+"'"); - this.unFoldRegionInternal(this.requestUnFoldRegion); - this.requestUnFoldRegion = ""; - } - else if (this.requestFoldAll) - { - this.foldAllRegionsInternal(); - this.requestFoldAll = false; - } - else if (this.requestUnFoldAll) + } + + // Find regions that were (re)created + array@ newRegionNames = collectedRegions.getKeys(); + for (uint i = 0; i < newRegionNames.length(); i++) + { + RegionInfo@ newRegionInfo = findRegion(collectedRegions, newRegionNames[i]); + RegionInfo@ existingRegionInfo = findRegion(this.workBufferRegions, newRegionNames[i]); + if (@existingRegionInfo == null) { - //game.log("DBG requestUnfoldAll:"+this.requestUnFoldAll); - this.unFoldAllRegionsInternal(); - this.requestUnFoldAll = false; + //game.log("DBG mergeCollectedFoldingRegionsWithExisting(): A brand new region '"+newRegionNames[i]+"' was created"); + this.workBufferRegions[newRegionNames[i]] = newRegionInfo; } - if (this.requestIndentBuffer) - { - this.indentBufferInternal(); - this.requestIndentBuffer = false; - } - if (this.requestSaveFile) + else { - this.saveFileInternal(); - this.requestSaveFile = false; + /*game.log("DBG mergeCollectedFoldingRegionsWithExisting(): Region '"+newRegionNames[i]+"' already exists:" + +" lineCount="+existingRegionInfo.regionLineCount+" (new:"+newRegionInfo.regionLineCount+")" + +" regionBodyStartOffset="+existingRegionInfo.regionBodyStartOffset+" (new:"+newRegionInfo.regionBodyStartOffset+")" + +" regionBodyNumChars="+existingRegionInfo.regionBodyNumChars+" (new:"+newRegionInfo.regionBodyNumChars+")" + +" isOrphan="+existingRegionInfo.isOrphan+" isFolded="+newRegionInfo.isFolded); + */ + + existingRegionInfo.regionBodyStartOffset = newRegionInfo.regionBodyStartOffset; + + if (!existingRegionInfo.isFolded) + { + //game.log("DBG mergeCollectedFoldingRegionsWithExisting(): An existing UNFOLDED region '"+newRegionNames[i]+"' was updated ~ text may have changed"); + existingRegionInfo.regionLineCount = newRegionInfo.regionLineCount; + existingRegionInfo.regionBodyNumChars = newRegionInfo.regionBodyNumChars; + existingRegionInfo.regionStartsAtLineIndex = newRegionInfo.regionStartsAtLineIndex; + } + else if (existingRegionInfo.isOrphan && newRegionInfo.regionLineCount == 0) + { + //game.log("DBG mergeCollectedFoldingRegionsWithExisting(): An orphan (so logically FOLDED) region '"+newRegionNames[i]+"' has resurfaced"); + existingRegionInfo.isOrphan = false; + } } - if (this.requestAutoSaveFile) - { - this.autosaveFileInternal(); - this.requestAutoSaveFile = false; - } - if (this.requestRunBuffer) + } +} + +private void analyzeMessages() // helper for `analyzeLines()` +{ + // reset caches + bufferMessageIDs.resize(0); // clear all + bufferMessageIDs.resize(bufferLinesMeta.length()); + + // reset global stats + this.messagesTotalErr = 0; + this.messagesTotalWarn = 0; + this.messagesTotalInfo = 0; + + + for (uint i = 0; i < this.messages.length(); i++) + { + int msgType = int(this.messages[i]['type']); + string msgText = string(this.messages[i]['message']); + string inFoldedRegion = ""; + uint lineIdx = this.determineLineIdxForMessage(i, /*[out]:*/inFoldedRegion); + this.messages[i]['inFoldedRegion'] = inFoldedRegion; // HACK: add extra dynamic field where only constant data from AngelScript should be + + // BEGIN Check bounds (investigating exceptions) + if (msgType == asMSGTYPE_ERROR || msgType == asMSGTYPE_WARNING) + { + if (i >= bufferMessageIDs.length()) + { + game.log("DBG WARNING: analyzeMessages() `i` out of bounds, breaking loop (seems to happen each time new build errors arrive and there's more than last time)."); + game.log("DBG analyzeMessages() | i="+i+", Looping over this.messages.length()="+this.messages.length()+", writing to bufferMessageIDs.length()="+ bufferMessageIDs.length()); + game.log ("DBG analyzeMessages() | msgType is: ERROR=" + (msgType == asMSGTYPE_ERROR) + " / WARNING = " + (msgType == asMSGTYPE_WARNING)); + + break; + } + + // Check bounds (investigating a rare exception) + if (lineIdx >= bufferMessageIDs.length()) + { + game.log("DBG WARNING: analyzeMessages() `lineIdx` out of bounds, breaking loop."); + game.log("DBG analyzeMessages() | i="+i+", using lineIdx="+lineIdx+" to write into bufferMessageIDs.length()="+ bufferMessageIDs.length()); + game.log ("DBG analyzeMessages() | msgType is: ERROR=" + (msgType == asMSGTYPE_ERROR) + " / WARNING = " + (msgType == asMSGTYPE_WARNING)); + + break; + } + } + // END bounds check + + // update line stats + if (msgType == asMSGTYPE_ERROR) { - this.runBufferInternal(); - this.requestRunBuffer = false; + this.bufferMessageIDs[lineIdx].insertLast(i); + this.messagesTotalErr++; } - if (this.requestStopBuffer) + if (msgType == asMSGTYPE_WARNING) { - this.stopBufferInternal(); - this.requestStopBuffer = false; + this.bufferMessageIDs[lineIdx].insertLast(i); + this.messagesTotalWarn++; } - } - - private void saveFileInternal() // Do not invoke while drawing! use `requestSaveFile` - { - // perform auto-indenting if desired - if (autoIndentOnSave) - this.indentBufferInternal(); // OK to call here - we're already handing a request. - - this.backUpRegionFoldStates(); - this.unFoldAllRegionsInternal(); // OK to call here - we're already handling a request. - - // Write out the file - string strData = this.buffer.substr(0, this.totalChars); - bool savedOk = game.createTextResourceFromString( - strData, editorWindow.saveFileNameBuf, RGN_SCRIPTS, saveShouldOverwrite); - editorWindow.saveFileResult = savedOk ? 1 : -1; - if (savedOk) + if (msgType == asMSGTYPE_INFORMATION) { - editorWindow.addRecentScript(editorWindow.saveFileNameBuf); + this.messagesTotalInfo++; } - - this.restoreRegionFoldStates(); } +} + +private uint determineLineIdxForMessage(uint msgIndex, string&inout inFoldedRegion) // helper for `analyzeMessages()` +{ + // AngelScript reports line numbers from complete file, but `this.buffer` doesn't contain folded regions so line indices in `this.bufferLinesMeta` don't match. + // To determine the message's lineindex in the sparse `this.buffer`, we must go through region records and adjust the reported line number. + // Note the `this.workBufferRegions` set isn't ordered, we must process all regions that precede the message, and only then the region which contains it (if any). + // -------------------------------------------------------------------------------------------------------------------------------------------------------- - private void autosaveFileInternal() // Do not invoke while drawing! use `requestAutoSaveFile` - { - this.backUpRegionFoldStates(); - this.unFoldAllRegionsInternal(); // OK to call here - we're already handling a request. - - // Generate filename: ${orig_name}_AUTOSAVE.$(orig_ext) - int dotPos = bufferName.findLast("."); - string autosaveFilename; - if (dotPos > 0) - { - autosaveFilename = bufferName.substr(0, dotPos) + "_AUTOSAVE" + bufferName.substr(dotPos); - } - else - { - autosaveFilename = bufferName + "_AUTOSAVE"; - } - /*game.log ("DBG autosaveFileInternal: autosaveFilename='"+autosaveFilename+"'");*/ - - // Write out the file - string strData = this.buffer.substr(0, this.totalChars); - bool savedOk = game.createTextResourceFromString( - strData, autosaveFilename, RGN_SCRIPTS, /*overwrite:*/true); - this.autosaveResult = savedOk ? 1 : -1; - this.restoreRegionFoldStates(); - } + int msgRow = int(this.messages[msgIndex]['row']); + int lineIdx = msgRow-1; - private void indentBufferInternal() // Do not invoke while drawing! use `requestIndentBuffer` - { - this.backUpRegionFoldStates(); - this.unFoldAllRegionsInternal(); + //game.log("DBG determineLineIdxForMessage("+msgIndex+"): initial lineIdx="+lineIdx); - string stagingBuffer; - for (uint i = 0; i < bufferLinesMeta.length(); i++) + array regionKeys = this.workBufferRegions.getKeys(); + RegionInfo@ containingRegion = null; + string containingRegionName = ""; + for (uint i = 0; i < regionKeys.length(); i++) + { + RegionInfo@ regionInfo = findRegion(this.workBufferRegions, regionKeys[i]); + if (@regionInfo != null) { - // insert indent - int autoIndentLevel = int(bufferLinesMeta[i]['autoIndentLevel']); - for (int j=0; j="+(@containingRegion == @regionInfo)+"; new lineIdx="+lineIdx); } - - // submit buffer - this.totalChars = stagingBuffer.length(); - this.buffer = stagingBuffer; - - // reserve extra space - this.buffer.resize(this.buffer.length() + BUFFER_INCREMENT_SIZE); - - // Update the lines metainfo - this.analyzeBuffer(); - - this.restoreRegionFoldStates(); // restore backup - this.requestIndentBuffer = false; } - private void foldAllRegionsInternal() // do NOT invoke during drawing! use `requestFoldAll` + if (@containingRegion!=null && containingRegion.isFolded) { - array regionNames = this.workBufferRegions.getKeys(); - for (uint i=0; i < regionNames.length(); i++) - { - this.foldRegionInternal(regionNames[i]); - } - - } + lineIdx -= 1 + (lineIdx - containingRegion.regionStartsAtLineIndex); + inFoldedRegion = containingRegionName; + } - private void unFoldAllRegionsInternal() // do NOT invoke during drawing! use `requestUnFoldAll` + // workaround: clamp the msgRow to known line count + if (lineIdx >= int(bufferLinesMeta.length())) { - array regionNames = this.workBufferRegions.getKeys(); - for (uint i=0; i < regionNames.length(); i++) - { - this.unFoldRegionInternal(regionNames[i]); - } + lineIdx = int(bufferLinesMeta.length()) - 1; + } + + //game.log("DBG determineLineIdxForMessage("+msgIndex+"): final lineIdx="+lineIdx); + return lineIdx; +} + +void updateAutosave(float dt) +{ + if (autoSaveIntervalSec == 0.f) + return; // disabled + + this.autosaveTimeCounterSec+= dt; + this.autosaveResult = 0; + if (this.autosaveTimeCounterSec > autoSaveIntervalSec) + { + this.requestAutoSaveFile = true; + this.autosaveTimeCounterSec -= autoSaveIntervalSec; // don't miss a millisecond! + } +} + +void drawAutosaveStatusbar() +{ + ImGui::TextDisabled("Autosave:"); + ImGui::PushStyleColor(ImGuiCol_FrameBg, autoSaveStatusbarBgColor); + ImGui::SetCursorPosY(ImGui::GetCursorPosY()+5.f); + ImGui::ProgressBar( + /*fraction:*/this.autosaveTimeCounterSec/autoSaveIntervalSec, + /*size: */vector2(55.f, 13.f), + /*overlay: */''+formatFloat(autoSaveIntervalSec-this.autosaveTimeCounterSec, "", 3, 1)+'sec'); + ImGui::PopStyleColor(1); // ImGuiCol_FrameBg +} + +void handleRequests(float dt) +{ + if (this.requestFoldRegion != "") + { + this.foldRegionInternal(this.requestFoldRegion); + this.requestFoldRegion = ""; + } + else if (this.requestUnFoldRegion != "") + { + //game.log("DBG requestUnFoldRegion: '"+ this.requestUnFoldRegion+"'"); + this.unFoldRegionInternal(this.requestUnFoldRegion); + this.requestUnFoldRegion = ""; + } + else if (this.requestFoldAll) + { + this.foldAllRegionsInternal(); + this.requestFoldAll = false; + } + else if (this.requestUnFoldAll) + { + //game.log("DBG requestUnfoldAll:"+this.requestUnFoldAll); + this.unFoldAllRegionsInternal(); this.requestUnFoldAll = false; } + if (this.requestIndentBuffer) + { + this.indentBufferInternal(); + this.requestIndentBuffer = false; + } + if (this.requestSaveFile) + { + this.saveFileInternal(); + this.requestSaveFile = false; + } + if (this.requestAutoSaveFile) + { + this.autosaveFileInternal(); + this.requestAutoSaveFile = false; + } + if (this.requestRunBuffer) + { + this.runBufferInternal(); + this.requestRunBuffer = false; + } + if (this.requestStopBuffer) + { + this.stopBufferInternal(); + this.requestStopBuffer = false; + } +} + +private void saveFileInternal() // Do not invoke while drawing! use `requestSaveFile` +{ + // perform auto-indenting if desired + if (autoIndentOnSave) + this.indentBufferInternal(); // OK to call here - we're already handing a request. - private void foldRegionInternal(string regionName) // do NOT invoke during drawing! use `foldRegionRequested` - { - RegionInfo@ regionInfo = findRegion(this.workBufferRegions, regionName); - /* - game.log("DBG foldRegionInternal() regionName='"+regionName+"', regionInfo:"+(@regionInfo == null ? "null" : - "NumChars:"+regionInfo.regionBodyNumChars+", isFolded:"+regionInfo.isFolded+", regionBodyStartOffset:"+regionInfo.regionBodyStartOffset+", regionBodyNumChars:"+regionInfo.regionBodyNumChars)); - */ - if (@regionInfo != null && !regionInfo.isFolded) // sanity check - this means `#endregion` isn't available - { - regionInfo.foldedOffText = this.buffer.substr(regionInfo.regionBodyStartOffset, regionInfo.regionBodyNumChars); - this.buffer.erase(regionInfo.regionBodyStartOffset, regionInfo.regionBodyNumChars); - regionInfo.isFolded = true; - this.analyzeLines(); - this.analyzeMessages(); // Determine which errors are in folded region, for correct drawing - } + this.backUpRegionFoldStates(); + this.unFoldAllRegionsInternal(); // OK to call here - we're already handling a request. + + // Write out the file + string strData = this.buffer.substr(0, this.totalChars); + bool savedOk = game.createTextResourceFromString( + strData, editorWindow.saveFileNameBuf, RGN_SCRIPTS, saveShouldOverwrite); + editorWindow.saveFileResult = savedOk ? 1 : -1; + if (savedOk) + { + editorWindow.addRecentScript(editorWindow.saveFileNameBuf); } - private void unFoldRegionInternal(string regionName) // do NOT invoke during drawing! use `unFoldRegionRequested` + this.restoreRegionFoldStates(); +} + +private void autosaveFileInternal() // Do not invoke while drawing! use `requestAutoSaveFile` +{ + this.backUpRegionFoldStates(); + this.unFoldAllRegionsInternal(); // OK to call here - we're already handling a request. + +// Generate filename: ${orig_name}_AUTOSAVE.$(orig_ext) + int dotPos = bufferName.findLast("."); + string autosaveFilename; + if (dotPos > 0) { - RegionInfo@ regionInfo = findRegion(this.workBufferRegions, regionName); - /*game.log("DBG unFoldRegionInternal() regionName='"+regionName+"', regionInfo:"+(@regionInfo == null ? "null" : - "NumChars:"+regionInfo.regionBodyNumChars+", isFolded:"+regionInfo.isFolded+", regionBodyStartOffset:"+regionInfo.regionBodyStartOffset+", regionBodyNumChars:"+regionInfo.regionBodyNumChars)); - */ - if (@regionInfo != null && regionInfo.isFolded) // sanity check - this means `#endregion` isn't available + autosaveFilename = bufferName.substr(0, dotPos) + "_AUTOSAVE" + bufferName.substr(dotPos); + } + else + { + autosaveFilename = bufferName + "_AUTOSAVE"; + } + /*game.log ("DBG autosaveFileInternal: autosaveFilename='"+autosaveFilename+"'");*/ + + // Write out the file + string strData = this.buffer.substr(0, this.totalChars); + bool savedOk = game.createTextResourceFromString( + strData, autosaveFilename, RGN_SCRIPTS, /*overwrite:*/true); + this.autosaveResult = savedOk ? 1 : -1; + this.restoreRegionFoldStates(); +} + +private void indentBufferInternal() // Do not invoke while drawing! use `requestIndentBuffer` +{ + this.backUpRegionFoldStates(); + this.unFoldAllRegionsInternal(); + + string stagingBuffer; + for (uint i = 0; i < bufferLinesMeta.length(); i++) + { + // insert indent + int autoIndentLevel = int(bufferLinesMeta[i]['autoIndentLevel']); + for (int j=0; j regionNames = this.workBufferRegions.getKeys(); + for (uint i=0; i < regionNames.length(); i++) { - //game.log("DBG backUpRegionFoldStates()"); - array regionNames = this.workBufferRegions.getKeys(); - for (uint i=0; i < regionNames.length(); i++) - { - RegionInfo@ regionInfo = findRegion(this.workBufferRegions, regionNames[i]); - if (@regionInfo != null) - { - regionInfo.isFoldedBackup = regionInfo.isFolded; - //game.log("DBG backUpRegionFoldStates() ~ region '"+regionNames[i]+"': isFolded="+regionInfo.isFolded+" (backup: "+regionInfo.isFoldedBackup+")"); - } - } + this.foldRegionInternal(regionNames[i]); } - private void restoreRegionFoldStates() +} + +private void unFoldAllRegionsInternal() // do NOT invoke during drawing! use `requestUnFoldAll` +{ + array regionNames = this.workBufferRegions.getKeys(); + for (uint i=0; i < regionNames.length(); i++) { - //game.log("DBG restoreRegionFoldStates()"); - array regionNames = this.workBufferRegions.getKeys(); - for (uint i=0; i < regionNames.length(); i++) + this.unFoldRegionInternal(regionNames[i]); + } + this.requestUnFoldAll = false; +} + +private void foldRegionInternal(string regionName) // do NOT invoke during drawing! use `foldRegionRequested` +{ + RegionInfo@ regionInfo = findRegion(this.workBufferRegions, regionName); + /* + game.log("DBG foldRegionInternal() regionName='"+regionName+"', regionInfo:"+(@regionInfo == null ? "null" : + "NumChars:"+regionInfo.regionBodyNumChars+", isFolded:"+regionInfo.isFolded+", regionBodyStartOffset:"+regionInfo.regionBodyStartOffset+", regionBodyNumChars:"+regionInfo.regionBodyNumChars)); + */ + if (@regionInfo != null && !regionInfo.isFolded) // sanity check - this means `#endregion` isn't available + { + regionInfo.foldedOffText = this.buffer.substr(regionInfo.regionBodyStartOffset, regionInfo.regionBodyNumChars); + this.buffer.erase(regionInfo.regionBodyStartOffset, regionInfo.regionBodyNumChars); + regionInfo.isFolded = true; + this.analyzeLines(); + this.analyzeMessages(); // Determine which errors are in folded region, for correct drawing + } +} + +private void unFoldRegionInternal(string regionName) // do NOT invoke during drawing! use `unFoldRegionRequested` +{ + RegionInfo@ regionInfo = findRegion(this.workBufferRegions, regionName); + /*game.log("DBG unFoldRegionInternal() regionName='"+regionName+"', regionInfo:"+(@regionInfo == null ? "null" : + "NumChars:"+regionInfo.regionBodyNumChars+", isFolded:"+regionInfo.isFolded+", regionBodyStartOffset:"+regionInfo.regionBodyStartOffset+", regionBodyNumChars:"+regionInfo.regionBodyNumChars)); + */ + if (@regionInfo != null && regionInfo.isFolded) // sanity check - this means `#endregion` isn't available + { + this.buffer.insert(regionInfo.regionBodyStartOffset, regionInfo.foldedOffText); + regionInfo.foldedOffText = ""; + regionInfo.isFolded = false; + this.analyzeLines(); + this.analyzeMessages(); // Determine which errors are in folded region, for correct drawing + } +} + +private void backUpRegionFoldStates() +{ + //game.log("DBG backUpRegionFoldStates()"); + array regionNames = this.workBufferRegions.getKeys(); + for (uint i=0; i < regionNames.length(); i++) + { + RegionInfo@ regionInfo = findRegion(this.workBufferRegions, regionNames[i]); + if (@regionInfo != null) { - RegionInfo@ regionInfo = findRegion(this.workBufferRegions, regionNames[i]); - if (@regionInfo != null) + regionInfo.isFoldedBackup = regionInfo.isFolded; + //game.log("DBG backUpRegionFoldStates() ~ region '"+regionNames[i]+"': isFolded="+regionInfo.isFolded+" (backup: "+regionInfo.isFoldedBackup+")"); + } + } +} + +private void restoreRegionFoldStates() +{ + //game.log("DBG restoreRegionFoldStates()"); + array regionNames = this.workBufferRegions.getKeys(); + for (uint i=0; i < regionNames.length(); i++) + { + RegionInfo@ regionInfo = findRegion(this.workBufferRegions, regionNames[i]); + if (@regionInfo != null) + { + //game.log("DBG restoreRegionFoldStates() ~ region '"+regionNames[i]+"': isFolded="+regionInfo.isFolded+" (backup: "+regionInfo.isFoldedBackup+")"); + if (regionInfo.isFolded && !regionInfo.isFoldedBackup) { - //game.log("DBG restoreRegionFoldStates() ~ region '"+regionNames[i]+"': isFolded="+regionInfo.isFolded+" (backup: "+regionInfo.isFoldedBackup+")"); - if (regionInfo.isFolded && !regionInfo.isFoldedBackup) - { - this.unFoldRegionInternal(regionNames[i]); - } - else if (!regionInfo.isFolded && regionInfo.isFoldedBackup) - { - this.foldRegionInternal(regionNames[i]); - } - regionInfo.isFolded = regionInfo.isFoldedBackup; + this.unFoldRegionInternal(regionNames[i]); } + else if (!regionInfo.isFolded && regionInfo.isFoldedBackup) + { + this.foldRegionInternal(regionNames[i]); + } + regionInfo.isFolded = regionInfo.isFoldedBackup; } - - // Determine which messages are in folded-off regions - important for correct drawing } + + // Determine which messages are in folded-off regions - important for correct drawing +} } // Originally global, now instantiated per-tab; some logic was left over. class ExceptionsPanel { - ScriptEditorTab@ parentEditorTab; - dictionary exception_stats_nid; // NID-> dictionary{ fullDesc -> num occurences } - array nids; // Recorded NIDs - since each buffer has it's own panel instance now, there will never be more than 1 - int numLinesDrawn = 0; - // very important to keep the dict clean after requesting script unload - // - even after notification more exceptions can arrive - bool ePanelEnabled = true; - - ExceptionsPanel(ScriptEditorTab@ editorTab) +ScriptEditorTab@ parentEditorTab; +dictionary exception_stats_nid; // NID-> dictionary{ fullDesc -> num occurences } +array nids; // Recorded NIDs - since each buffer has it's own panel instance now, there will never be more than 1 +int numLinesDrawn = 0; +// very important to keep the dict clean after requesting script unload +// - even after notification more exceptions can arrive +bool ePanelEnabled = true; + +ExceptionsPanel(ScriptEditorTab@ editorTab) +{ + @parentEditorTab = @editorTab; +} + +float getHeightToReserve() +{ +if (exception_stats_nid.isEmpty()) { return 0; } +else { return numLinesDrawn*ImGui::GetTextLineHeight() + 15; } +} + +void onEventExceptionCaught(int nid, string arg5_from, string arg6_type, string arg7_msg) +{ + if (parentEditorTab.currentScriptUnitID != nid) { - @parentEditorTab = @editorTab; + /*game.log("DBG onEventExceptionCaught(nid="+nid+") IGNORING; (currentScriptUnitID="+parentEditorTab.currentScriptUnitID+" bufferName="+parentEditorTab.bufferName);*/ + return; } - float getHeightToReserve() - { - if (exception_stats_nid.isEmpty()) { return 0; } - else { return numLinesDrawn*ImGui::GetTextLineHeight() + 15; } - } + // format: FROM >> MSG (TYPE) + string desc=arg5_from+' --> '+arg7_msg +' ('+arg6_type+')'; + this.addExceptionInternal(nid, desc); +} - void onEventExceptionCaught(int nid, string arg5_from, string arg6_type, string arg7_msg) +void onEventAngelscriptExceptionCallback(int nid, int arg3_linenum, string arg5_from, string arg6_msg) +{ + if (parentEditorTab.currentScriptUnitID != nid) { - if (parentEditorTab.currentScriptUnitID != nid) - { - /*game.log("DBG onEventExceptionCaught(nid="+nid+") IGNORING; (currentScriptUnitID="+parentEditorTab.currentScriptUnitID+" bufferName="+parentEditorTab.bufferName);*/ - return; - } - - // format: FROM >> MSG (TYPE) - string desc=arg5_from+' --> '+arg7_msg +' ('+arg6_type+')'; - this.addExceptionInternal(nid, desc); + /*game.log("DBG onEventAngelscriptExceptionCallback(nid="+nid+") IGNORING; (currentScriptUnitID="+parentEditorTab.currentScriptUnitID+" bufferName="+parentEditorTab.bufferName);*/ + return; } - void onEventAngelscriptExceptionCallback(int nid, int arg3_linenum, string arg5_from, string arg6_msg) + // format: FROM >> MSG (line: LINENUM) + string desc = arg5_from+"() --> "+arg6_msg+" (line: "+arg3_linenum+")"; + this.addExceptionInternal(nid, desc); +} + +void addExceptionInternal(int nid, string desc) +{ + if (!ePanelEnabled) + return; + + // locate the dictionary for this script (by NID) + dictionary@ exception_stats = cast(exception_stats_nid[''+nid]); + if (@exception_stats == null) { - if (parentEditorTab.currentScriptUnitID != nid) - { - /*game.log("DBG onEventAngelscriptExceptionCallback(nid="+nid+") IGNORING; (currentScriptUnitID="+parentEditorTab.currentScriptUnitID+" bufferName="+parentEditorTab.bufferName);*/ - return; - } - - // format: FROM >> MSG (line: LINENUM) - string desc = arg5_from+"() --> "+arg6_msg+" (line: "+arg3_linenum+")"; - this.addExceptionInternal(nid, desc); + exception_stats_nid[''+nid] = dictionary(); + @exception_stats = cast(exception_stats_nid[''+nid]); + this.nids.insertLast(nid); } + exception_stats[desc] = int(exception_stats[desc])+1; +} + +void drawExceptions() +{ + // Requests for editing entries - cannot be done while iterating! + bool requestClearAll = false; + dictionary@ requestClearEntryFromStats; + string requestClearEntryKey; - void addExceptionInternal(int nid, string desc) + numLinesDrawn=0; + int numEntriesDrawn=0; + for (uint i=0; i(exception_stats_nid[''+nid]); - if (@exception_stats == null) + ImGui::PushID(i); + + dictionary@ info = game.getScriptDetails(nids[i]); + dictionary@ exception_stats = cast(exception_stats_nid[''+nids[i]]); + + if (@exception_stats == null) { continue; } // <----- continue, nothing to draw + + + ImGui::TextDisabled('NID '+nids[i]+' '); + if (@info != null) { - exception_stats_nid[''+nid] = dictionary(); - @exception_stats = cast(exception_stats_nid[''+nid]); - this.nids.insertLast(nid); + ImGui::SameLine(); ImGui::Text(string(info['scriptName'])); } - exception_stats[desc] = int(exception_stats[desc])+1; - } - - void drawExceptions() - { - // Requests for editing entries - cannot be done while iterating! - bool requestClearAll = false; - dictionary@ requestClearEntryFromStats; - string requestClearEntryKey; - - numLinesDrawn=0; - int numEntriesDrawn=0; - for (uint i=0; i(exception_stats_nid[''+nids[i]]); - - if (@exception_stats == null) { continue; } // <----- continue, nothing to draw - + array@tag_keys = exception_stats.getKeys(); + ImGui::SameLine(); ImGui::Text(">>"); + ImGui::SameLine(); ImGui::TextColored( color(1,0.1, 0.2, 1), tag_keys.length() + ' exception type(s) encountered:'); + string clearall_btn_text = "Clear all"; + ImGui::SameLine(); + ImGui::SetCursorPosX(ImGui::GetWindowContentRegionMax().x - (ImGui::CalcTextSize(clearall_btn_text).x + 8.f)); + if (ImGui::SmallButton(clearall_btn_text)) + { + requestClearAll = true; + } + for (uint j=0; j < tag_keys.length(); j++) + { + ImGui::PushID(j); - ImGui::TextDisabled('NID '+nids[i]+' '); - if (@info != null) - { - ImGui::SameLine(); ImGui::Text(string(info['scriptName'])); - } - else - { - ImGui::SameLine(); ImGui::TextDisabled("(NOT RUNNING)"); - } - numLinesDrawn++; - - array@tag_keys = exception_stats.getKeys(); - ImGui::SameLine(); ImGui::Text(">>"); - ImGui::SameLine(); ImGui::TextColored( color(1,0.1, 0.2, 1), tag_keys.length() + ' exception type(s) encountered:'); - string clearall_btn_text = "Clear all"; - ImGui::SameLine(); - ImGui::SetCursorPosX(ImGui::GetWindowContentRegionMax().x - (ImGui::CalcTextSize(clearall_btn_text).x + 8.f)); - if (ImGui::SmallButton(clearall_btn_text)) + string descr = tag_keys[j]; + int tagcount = int(exception_stats[descr]); + if (ImGui::SmallButton("Clear")) { - requestClearAll = true; + /*game.log("DBG epanel requested clearing '"+descr+"' - button pressed");*/ + requestClearEntryKey = descr; + @requestClearEntryFromStats = @exception_stats; } - for (uint j=0; j < tag_keys.length(); j++) - { - ImGui::PushID(j); + ImGui::SameLine(); ImGui::TextDisabled('['+tagcount+']'); + ImGui::SameLine(); ImGui::Text(descr); - string descr = tag_keys[j]; - int tagcount = int(exception_stats[descr]); - if (ImGui::SmallButton("Clear")) - { - /*game.log("DBG epanel requested clearing '"+descr+"' - button pressed");*/ - requestClearEntryKey = descr; - @requestClearEntryFromStats = @exception_stats; - } - ImGui::SameLine(); ImGui::TextDisabled('['+tagcount+']'); - ImGui::SameLine(); ImGui::Text(descr); - - numLinesDrawn++; - numEntriesDrawn++; - - ImGui::PopID(); // j - } // for (tag_keys) + numLinesDrawn++; + numEntriesDrawn++; - ImGui::PopID(); // i - } // for (nids) + ImGui::PopID(); // j + } // for (tag_keys) - // Requests for editing entries - cannot be done while iterating! - if (requestClearEntryKey != "" && @requestClearEntryFromStats != null) + ImGui::PopID(); // i + } // for (nids) + + // Requests for editing entries - cannot be done while iterating! + if (requestClearEntryKey != "" && @requestClearEntryFromStats != null) + { + /*game.log("DBG epanel requested clearing '"+requestClearEntryKey+"' - deleting from dict");*/ + if (!requestClearEntryFromStats.delete(requestClearEntryKey)) { - /*game.log("DBG epanel requested clearing '"+requestClearEntryKey+"' - deleting from dict");*/ - if (!requestClearEntryFromStats.delete(requestClearEntryKey)) - { - game.log("script_editor INTERNAL ERROR: requested clearing exception '"+requestClearEntryKey+"' failed - not found"); - } - if (numEntriesDrawn == 1) - { - requestClearAll=true; // We just deleted the only entry -> close panel - } + game.log("script_editor INTERNAL ERROR: requested clearing exception '"+requestClearEntryKey+"' failed - not found"); } - - if (requestClearAll) + if (numEntriesDrawn == 1) { - /*game.log("DBG @@@ handling requestClearAll for buffer='"+parentEditorTab.bufferName+"' !!!");*/ - this.clearExceptions(); + requestClearAll=true; // We just deleted the only entry -> close panel } - - } // drawExceptions() - - void clearExceptions() + } + + if (requestClearAll) { - /*game.log("DBG @@@ clearExceptions() called for buffer='"+parentEditorTab.bufferName+"' !!!");*/ - this.exception_stats_nid.deleteAll(); - this.numLinesDrawn=0; - this.nids = array(); // - since each buffer has it's own panel instance now, there will never be more than 1 + /*game.log("DBG @@@ handling requestClearAll for buffer='"+parentEditorTab.bufferName+"' !!!");*/ + this.clearExceptions(); } + +} // drawExceptions() + +void clearExceptions() +{ + /*game.log("DBG @@@ clearExceptions() called for buffer='"+parentEditorTab.bufferName+"' !!!");*/ + this.exception_stats_nid.deleteAll(); + this.numLinesDrawn=0; + this.nids = array(); // - since each buffer has it's own panel instance now, there will never be more than 1 +} } // class ExceptionsPanel // Code folding with `#region`/`#endregion` class RegionInfo { - int regionLineCount; - int regionBodyStartOffset; - int regionBodyNumChars; - int regionStartsAtLineIndex; - - bool isFolded; - bool isOrphan; // Not currently present in document, still checked for uniqueness. If re-entered as empty (no lines), can be unFolded. If conflicting, user gets notified by '{ }' invalid state marker. - string foldedOffText; - bool isFoldedBackup; // For temporary unfolding all during saving/indenting/running the buffer; use `backUpRegionFoldStates()` and `restoreRegionFoldStates()`. +int regionLineCount; +int regionBodyStartOffset; +int regionBodyNumChars; +int regionStartsAtLineIndex; + +bool isFolded; +bool isOrphan; // Not currently present in document, still checked for uniqueness. If re-entered as empty (no lines), can be unFolded. If conflicting, user gets notified by '{ }' invalid state marker. +string foldedOffText; +bool isFoldedBackup; // For temporary unfolding all during saving/indenting/running the buffer; use `backUpRegionFoldStates()` and `restoreRegionFoldStates()`. } /// Represents a pre-defined group of scripts to be displayed in menus. class ScriptIndexerRecord { // DATA: - array@ fileinfos = null; // findResourceFileInfo() + added field 'scriptInfo' - uint fileinfos_analyzed = 0; - string rgname; - string pattern; +array@ fileinfos = null; // findResourceFileInfo() + added field 'scriptInfo' +uint fileinfos_analyzed = 0; +string rgname; +string pattern; // FUNCS: - void scanResourceGroup(string rgname, string pattern = '*') - { - @fileinfos = game.findResourceFileInfo(rgname, pattern); - this.rgname = rgname; - this.pattern = pattern; - fileinfos_analyzed = 0; - // analysis is done frame-by-frame in `advanceScriptAnalysis()` - } +void scanResourceGroup(string rgname, string pattern = '*') +{ + @fileinfos = game.findResourceFileInfo(rgname, pattern); + this.rgname = rgname; + this.pattern = pattern; + fileinfos_analyzed = 0; + // analysis is done frame-by-frame in `advanceScriptAnalysis()` +} - scriptinfo_utils::ScriptInfo@ getScriptInfo(uint index) - { - if (index < fileinfos.length() && fileinfos[index].exists('scriptInfo')) - { - return cast(fileinfos[index]['scriptInfo']); - } - return null; +scriptinfo_utils::ScriptInfo@ getScriptInfo(uint index) +{ + if (index < fileinfos.length() && fileinfos[index].exists('scriptInfo')) + { + return cast(fileinfos[index]['scriptInfo']); } - - // Returns TRUE if there was a script pending analysis. - bool advanceScriptAnalysis() - { - if (fileinfos_analyzed < fileinfos.length()) - { - this.analyzeSingleScript(fileinfos_analyzed); - fileinfos_analyzed++; - return true; - } - return false; + return null; +} + +// Returns TRUE if there was a script pending analysis. +bool advanceScriptAnalysis() +{ + if (fileinfos_analyzed < fileinfos.length()) + { + this.analyzeSingleScript(fileinfos_analyzed); + fileinfos_analyzed++; + return true; } + return false; +} + +void analyzeSingleScript(int index) +{ + string filename = string(fileinfos[index]['filename']); + string body = game.loadTextResourceAsString(filename, rgname); + scriptinfo_utils::ScriptInfo@ scriptinfo = scriptinfo_utils::ExtractScriptInfo(body); + /*game.log("DBG analyzeSingleScript("+index+"): File="+filename+'/RgName='+rgname + +": title="+scriptinfo.title+", brief="+scriptinfo.brief+", text="+scriptinfo.text);*/ + fileinfos[index]['scriptInfo'] = scriptinfo; - void analyzeSingleScript(int index) - { - string filename = string(fileinfos[index]['filename']); - string body = game.loadTextResourceAsString(filename, rgname); - scriptinfo_utils::ScriptInfo@ scriptinfo = scriptinfo_utils::ExtractScriptInfo(body); - /*game.log("DBG analyzeSingleScript("+index+"): File="+filename+'/RgName='+rgname - +": title="+scriptinfo.title+", brief="+scriptinfo.brief+", text="+scriptinfo.text);*/ - fileinfos[index]['scriptInfo'] = scriptinfo; - - } +} } // class ScriptIndexerRecord // Helpers bool isChar(uint c, string s) { - return s.length() > 0 && c == s[0]; +return s.length() > 0 && c == s[0]; } bool isCharBlank(uint c) { - return isChar(c, " ") || isChar(c, "\t"); +return isChar(c, " ") || isChar(c, "\t"); } string trimLeft(string s) { - for (uint i = 0; i < s.length(); i++) - { - if (!isCharBlank(s[i])) - return s.substr(i, s.length() - i); - } - return ""; +for (uint i = 0; i < s.length(); i++) +{ + if (!isCharBlank(s[i])) + return s.substr(i, s.length() - i); +} +return ""; } RegionInfo@ findRegion(dictionary@ regionDict, string name) // Helper which checks first (not inserting NULL entry) { - if (regionDict.exists(name)) - return cast(regionDict[name]); - else - return null; +if (regionDict.exists(name)) +return cast(regionDict[name]); +else +return null; } // Tutorial scripts @@ -2262,35 +2286,35 @@ float tt = 0.f; void main() { - // Uncomment to close window without asking. - //closeBtnHandler.cfgCloseImmediatelly = true; +// Uncomment to close window without asking. +//closeBtnHandler.cfgCloseImmediatelly = true; } // `frameStep()` runs every frame; `dt` is delta time in seconds. void frameStep(float dt) { - // Begin drawing window - if (ImGui::Begin("Tutorial script", closeBtnHandler.windowOpen, 0)) - { - // Draw the "Terminate this script?" prompt on the top (if not disabled by config). - closeBtnHandler.draw(); - - // accumulate time - tt += dt; - - //#region format the output - string ttStr = "Total time: " + formatFloat(tt, "", 5, 2) + "sec"; - string dtStr = "Delta time: " + formatFloat(dt, "", 7, 4) + "sec"; - //#endregion - - //#region render the output - ImGui::Text(ttStr); - ImGui::SameLine(); - ImGui::Text(dtStr); - //#endregion - - // End drawing window - ImGui::End(); - } +// Begin drawing window +if (ImGui::Begin("Tutorial script", closeBtnHandler.windowOpen, 0)) +{ + // Draw the "Terminate this script?" prompt on the top (if not disabled by config). + closeBtnHandler.draw(); + + // accumulate time + tt += dt; + + //#region format the output + string ttStr = "Total time: " + formatFloat(tt, "", 5, 2) + "sec"; + string dtStr = "Delta time: " + formatFloat(dt, "", 7, 4) + "sec"; + //#endregion + + //#region render the output + ImGui::Text(ttStr); + ImGui::SameLine(); + ImGui::Text(dtStr); + //#endregion + + // End drawing window + ImGui::End(); +} } """; From 6f630ed9e4e1e1503b5ebe542216dfcb86a08a28 Mon Sep 17 00:00:00 2001 From: ohlidalp Date: Wed, 27 Nov 2024 21:56:14 +0100 Subject: [PATCH 16/36] imgui_utils.as: X btn handler: added option not to kill script. --- resources/scripts/imgui_utils.as | 266 ++++++++++++++++--------------- 1 file changed, 137 insertions(+), 129 deletions(-) diff --git a/resources/scripts/imgui_utils.as b/resources/scripts/imgui_utils.as index e1c7e2a799..3ae642c8f1 100644 --- a/resources/scripts/imgui_utils.as +++ b/resources/scripts/imgui_utils.as @@ -1,131 +1,139 @@ -/// \title UI tools based on DearIMGUI -/// \brief Hyperlink drawing -/// Functions: -/// `void ImHyperlink(string url, string caption, bool tooltip=true)` ~ Full-featured hypertext with tooltip showing full URL. -/// `void ImDummyHyperlink(string caption)` ~ Looks and behaves (mouuse cursor) like a hypertext, but doesn't open URL. -/// `ImDrawList@ ImGetDummyFullscreenWindow(string name) ~ Convenience helper for ad-hoc drawing on screen. -/// Classes: -/// `CloseWindowPrompt` ~ Convenient handler of the [X] close button - draws "Really exit?" prompt and then exits the script. -// -------------------------------------------------------------------------- - -// By convention, all includes have filename '*_utils' and namespace matching filename. -namespace imgui_utils -{ - - /// Looks and behaves (mouuse cursor) like a hypertext, but doesn't open URL. - void ImDummyHyperlink(string caption) - { - const color LINKCOLOR = color(0.3, 0.5, 0.9, 1.0); - ImGui::PushStyleColor(0, LINKCOLOR); //Text - vector2 cursorBefore=ImGui::GetCursorScreenPos(); - ImGui::Text(caption); - vector2 textSize = ImGui::CalcTextSize(caption); - ImGui::GetWindowDrawList().AddLine( - cursorBefore+vector2(0,textSize.y), cursorBefore+textSize, //from-to - LINKCOLOR); - if (ImGui::IsItemHovered()) - { - ImGui::SetMouseCursor(7);//Hand cursor - } - ImGui::PopStyleColor(1); //Text - } - - /// Full-featured hypertext with tooltip showing full URL. - void ImHyperlink(string url, string caption="", bool tooltip=true) - { - if (caption == "") { caption = url; tooltip=false; } - ImDummyHyperlink(caption); - if (ImGui::IsItemClicked()) - { - game.openUrlInDefaultBrowser(url); - } - if (tooltip && ImGui::IsItemHovered()) +/// \title UI tools based on DearIMGUI +/// \brief Hyperlink drawing +/// Functions: +/// `void ImHyperlink(string url, string caption, bool tooltip=true)` ~ Full-featured hypertext with tooltip showing full URL. +/// `void ImDummyHyperlink(string caption)` ~ Looks and behaves (mouuse cursor) like a hypertext, but doesn't open URL. +/// `ImDrawList@ ImGetDummyFullscreenWindow(string name) ~ Convenience helper for ad-hoc drawing on screen. +/// Classes: +/// `CloseWindowPrompt` ~ Convenient handler of the [X] close button - draws "Really exit?" prompt and then exits the script. +// -------------------------------------------------------------------------- + +// By convention, all includes have filename '*_utils' and namespace matching filename. +namespace imgui_utils +{ + + /// Looks and behaves (mouuse cursor) like a hypertext, but doesn't open URL. + void ImDummyHyperlink(string caption) + { + const color LINKCOLOR = color(0.3, 0.5, 0.9, 1.0); + ImGui::PushStyleColor(0, LINKCOLOR); //Text + vector2 cursorBefore=ImGui::GetCursorScreenPos(); + ImGui::Text(caption); + vector2 textSize = ImGui::CalcTextSize(caption); + ImGui::GetWindowDrawList().AddLine( + cursorBefore+vector2(0,textSize.y), cursorBefore+textSize, //from-to + LINKCOLOR); + if (ImGui::IsItemHovered()) + { + ImGui::SetMouseCursor(7);//Hand cursor + } + ImGui::PopStyleColor(1); //Text + } + + /// Full-featured hypertext with tooltip showing full URL. + void ImHyperlink(string url, string caption="", bool tooltip=true) + { + if (caption == "") { caption = url; tooltip=false; } + ImDummyHyperlink(caption); + if (ImGui::IsItemClicked()) + { + game.openUrlInDefaultBrowser(url); + } + if (tooltip && ImGui::IsItemHovered()) + { + ImGui::BeginTooltip(); + ImDummyHyperlink(url); + ImGui::EndTooltip(); + } + } + + /// Convenience helper for ad-hoc drawing on screen. + ImDrawList@ ImGetDummyFullscreenWindow(const string&in name) + { + // Dummy fullscreen window to draw to + int windowFlags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoScrollbar| ImGuiWindowFlags_NoInputs + | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoBringToFrontOnFocus; + ImGui::SetNextWindowPos(vector2(0,0)); + ImGui::SetNextWindowSize(game.getDisplaySize()); + ImGui::PushStyleColor(ImGuiCol_WindowBg, color(0.f,0.f,0.f,0.f)); // Fully transparent background! + bool windowOpen = true; + ImGui::Begin(name, windowOpen, windowFlags); + ImDrawList@ drawlist = ImGui::GetWindowDrawList(); + ImGui::End(); + ImGui::PopStyleColor(1); // WindowBg + + return drawlist; + } + + /// Convenient handler of the [X] close button - draws "Really exit?" prompt and then exits the script. + class CloseWindowPrompt + { + //Config: + color cfgCloseWindowPromptBgColor(0.55f, 0.5f, 0.1f, 1.f); + color cfgCloseWindowPromptTextColor(0.1, 0.1, 0.1, 1.0); + string cfgPromptText = "Terminate the script?"; + bool cfgCloseImmediatelly = false; + vector2 cfgPromptBoxPadding(3, 3); + bool cfgTerminateWholeScript = true; // Set to false to use this handler for non-main windows. + //State: + bool windowOpen = true; + bool exitRequested = false; + + void draw() // Works best when drawn as first thing on top of the window. + { + // from + const int ImGuiCol_Text = 0; + + + if (this.windowOpen) + { + return; // nothing to draw right now + } + else if (!this.cfgCloseImmediatelly) + { + ImGui::AlignTextToFramePadding(); + + // Draw colored background + + vector2 cursor = ImGui::GetCursorScreenPos(); + vector2 topLeft = cursor - cfgPromptBoxPadding; + vector2 bottomRight = cursor + cfgPromptBoxPadding + vector2( ImGui::GetWindowContentRegionMax().x, ImGui::GetTextLineHeightWithSpacing()); + ImGui::GetWindowDrawList().AddRectFilled(topLeft, bottomRight, cfgCloseWindowPromptBgColor); + + // Draw text with contrasting color + ImGui::PushStyleColor(ImGuiCol_Text, cfgCloseWindowPromptTextColor); + ImGui::Text(cfgPromptText); + ImGui::PopStyleColor(); // Text + ImGui::SameLine(); + + // Draw yes/no buttons + if(ImGui::SmallButton("yes")) + { + exitRequested = true; + } + ImGui::SameLine(); + if (ImGui::SmallButton("no")) + { + this.windowOpen=true; + } + } + else + { + exitRequested = true; + } + + // Close the script if requested (and configured to do so) + if (exitRequested && cfgTerminateWholeScript) + { + game.pushMessage(MSG_APP_UNLOAD_SCRIPT_REQUESTED, { {'id', thisScript} }); // `thisScript` is global variable set by the game. + } + } + + void reset() { - ImGui::BeginTooltip(); - ImDummyHyperlink(url); - ImGui::EndTooltip(); + windowOpen = true; + exitRequested = false; } - } - - /// Convenience helper for ad-hoc drawing on screen. - ImDrawList@ ImGetDummyFullscreenWindow(const string&in name) - { - // Dummy fullscreen window to draw to - int windowFlags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoScrollbar| ImGuiWindowFlags_NoInputs - | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoBringToFrontOnFocus; - ImGui::SetNextWindowPos(vector2(0,0)); - ImGui::SetNextWindowSize(game.getDisplaySize()); - ImGui::PushStyleColor(ImGuiCol_WindowBg, color(0.f,0.f,0.f,0.f)); // Fully transparent background! - bool windowOpen = true; - ImGui::Begin(name, windowOpen, windowFlags); - ImDrawList@ drawlist = ImGui::GetWindowDrawList(); - ImGui::End(); - ImGui::PopStyleColor(1); // WindowBg - - return drawlist; - } - - /// Convenient handler of the [X] close button - draws "Really exit?" prompt and then exits the script. - class CloseWindowPrompt - { - //Config: - color cfgCloseWindowPromptBgColor(0.55f, 0.5f, 0.1f, 1.f); - color cfgCloseWindowPromptTextColor(0.1, 0.1, 0.1, 1.0); - string cfgPromptText = "Terminate the script?"; - bool cfgCloseImmediatelly = false; - vector2 cfgPromptBoxPadding(3, 3); - //State: - bool windowOpen = true; - - void draw() // Works best when drawn as first thing on top of the window. - { - // from - const int ImGuiCol_Text = 0; - bool exitRequested = false; - - if (this.windowOpen) - { - return; // nothing to draw right now - } - else if (!this.cfgCloseImmediatelly) - { - ImGui::AlignTextToFramePadding(); - - // Draw colored background - - vector2 cursor = ImGui::GetCursorScreenPos(); - vector2 topLeft = cursor - cfgPromptBoxPadding; - vector2 bottomRight = cursor + cfgPromptBoxPadding + vector2( ImGui::GetWindowContentRegionMax().x, ImGui::GetTextLineHeightWithSpacing()); - ImGui::GetWindowDrawList().AddRectFilled(topLeft, bottomRight, cfgCloseWindowPromptBgColor); - - // Draw text with contrasting color - ImGui::PushStyleColor(ImGuiCol_Text, cfgCloseWindowPromptTextColor); - ImGui::Text(cfgPromptText); - ImGui::PopStyleColor(); // Text - ImGui::SameLine(); - - // Draw yes/no buttons - if(ImGui::SmallButton("yes")) - { - exitRequested = true; - } - ImGui::SameLine(); - if (ImGui::SmallButton("no")) - { - this.windowOpen=true; - } - } - else - { - exitRequested = true; - } - - // Close the script if requested - if (exitRequested) - { - game.pushMessage(MSG_APP_UNLOAD_SCRIPT_REQUESTED, { {'id', thisScript} }); // `thisScript` is global variable set by the game. - } - } - } -} // namespace imgui_utils - + } +} // namespace imgui_utils + From 6ee9a755783f3c69393fc15597fda1b2fb29aca3 Mon Sep 17 00:00:00 2001 From: ohlidalp Date: Wed, 27 Nov 2024 22:00:57 +0100 Subject: [PATCH 17/36] :angel:Script: new #include "genericdoc_utils.as" Implements `GenericDocEditor` which doesn't edit yet but has highlight+selection and can be shared by scripts. Currently only 'example_terrn2_raceConverter.as' was updated to use it. --- .../scripts/example_terrn2_raceConverter.as | 629 ++++++++++-------- resources/scripts/genericdoc_utils.as | 269 ++++++++ 2 files changed, 608 insertions(+), 290 deletions(-) create mode 100644 resources/scripts/genericdoc_utils.as diff --git a/resources/scripts/example_terrn2_raceConverter.as b/resources/scripts/example_terrn2_raceConverter.as index db13c01904..40a997d06a 100644 --- a/resources/scripts/example_terrn2_raceConverter.as +++ b/resources/scripts/example_terrn2_raceConverter.as @@ -1,52 +1,91 @@ -/// \title terrn2 race converter -/// \brief imports races from scripts and generates race-def files. -/// new section in terrn2 format: [Races] -/// ^ each line is a tobj-like file with any extension (i.e. *.race) which is loaded by the race system. +/// \title terrn2 race converter +/// \brief imports races from scripts and generates race-def files. +/// uses new section in terrn2 format: [Races] +/// ^ each line is a tobj-like file with any extension (i.e. *.race) which is loaded by the race system. /// /// The program flow of this script got a little crazy; -/// I wanted a fluidly updating UI, performing just one step (1 doc conversion / 1 file write) per frame. -/// ================================================== - -// Window [X] button handler -#include "imgui_utils.as" -imgui_utils::CloseWindowPrompt closeBtnHandler; +/// ^ I wanted a fluidly updating UI, performing just one step (1 doc conversion / 1 file write) per frame. +/// ================================================== // The race system #include "races.as" racesManager races; - -enum Stage // in order of processing -{ + +// document viewer+editor +#include "genericdoc_utils.as" +genericdoc_utils::GenericDocEditor gdEditor; + +// Window [X] button handler +#include "imgui_utils.as" +imgui_utils::CloseWindowPrompt closeBtnHandlerUnique; + +enum Stage // in order of processing +{ STAGE_INIT, // detects races STAGE_CONVERT, // converts all races to GenericDocument race-defs - STAGE_IDLE, // Waiting for button press + STAGE_FIXTERRN2, // modify .terrn2 file - add [Races], remove [Scripts] + STAGE_IDLE, // Waiting for button press STAGE_PUSHMSG, // request game to create project STAGE_GETPROJECT, // fetch created project from modcache - STAGE_GENFILES, // write .race files - STAGE_FIXTERRN2, // modify .terrn2 file - add [Races], remove [Scripts] - STAGE_DONE, - STAGE_ERROR -} -Stage stage = STAGE_INIT; -string error = ""; + STAGE_WRITERACES, + STAGE_WRITETERRN2, + STAGE_DONE, + STAGE_ERROR +} +Stage stage = STAGE_INIT; +string error = ""; string projectName = ""; CacheEntryClass@ projectEntry; array convertedRaces; -array convertedAndSavedRaces; // filenames (we write the ' .race' file in separate frame so the user sees the correct filename on screen). -GenericDocumentClass@ g_displayed_document = null; -string g_displayed_doc_filename; +array convertedRaceFileNames; + string fileBeingWritten = ""; +GenericDocumentClass@ convertedTerrn2; +int topWrittenRace = -1; + +//#region Game Callbacks + +void main() +{ + // one-time config + gdEditor.gdeCloseBtnHandler.cfgCloseImmediatelly=true; +} + +void frameStep(float dt) +{ + // === DRAW UI === + if ( ImGui::Begin("Race import", closeBtnHandlerUnique.windowOpen, /*flags:*/0)) + { + // Draw the "Terminate this script?" prompt on the top (if not disabled by config). + closeBtnHandlerUnique.draw(); + drawUI(); + ImGui::End(); + } + + + // === PERFORM IMPORT STEP === + advanceImportOneStep(); +} +//#endregion +//#region UI drawing void drawUI() { // Draw document window - if (@g_displayed_document != null) + if (@gdEditor.displayedDocument != null) { - drawDocumentWindow(); + gdEditor.drawSeparateWindow(); } drawDetectedRaces(); + if (@convertedTerrn2 != null) + { + if (ImGui::SmallButton("Preview terrn2")) + { + gdEditor.setDocument(convertedTerrn2, projectName+'.terrn2'); + } + } ImGui::Separator(); switch(stage) { @@ -71,12 +110,18 @@ void drawUI() ImGui::Text("Performing MSG_EDI_CREATE_PROJECT_REQUESTED"); break; } - case STAGE_GENFILES: + case STAGE_WRITERACES: { ImGui::TextDisabled("Writing .race files:"); ImGui::Text(fileBeingWritten); break; } + case STAGE_WRITETERRN2: + { + ImGui::TextDisabled("OverWriting terrn2 file"); + ImGui::Text(projectName + ".terrn2"); + break; + } case STAGE_DONE: { ImGui::Text('DONE'); @@ -130,8 +175,8 @@ void drawDetectedRaces() ImGui::SameLine(); if ( ImGui::SmallButton("Preview")) { - g_displayed_doc_filename = race.raceName; - @g_displayed_document = @convertedRaces[i] ; + gdEditor.setDocument(convertedRaces[i], races.raceList[i].raceName); + } } ImGui::PopID(); // i @@ -139,70 +184,9 @@ void drawDetectedRaces() ImGui::PopID(); //"drawDetectedRaces" } +//#endregion -void drawDocumentWindow() -{ - ImGui::PushID("document view"); - string caption = "Document view (" + g_displayed_doc_filename + ")"; - bool documentOpen = true; - ImGui::Begin(caption, documentOpen, /*flags:*/0); - - GenericDocContextClass reader(g_displayed_document); - while (!reader.endOfFile()) - { - switch (reader.tokenType()) - { - // These tokens are always at start of line - case TOKEN_TYPE_KEYWORD: - ImGui::TextColored(color(1.f, 1.f, 0.f, 1.f), reader.getTokKeyword()); - break; - case TOKEN_TYPE_COMMENT: - ImGui::TextDisabled(";" + reader.getTokComment()); - break; - - // Linebreak is implicit in DearIMGUI, no action needed - case TOKEN_TYPE_LINEBREAK: - 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(color(0.f, 1.f, 1.f, 1.f), "\"" + reader.getTokString() + "\""); - break; - case TOKEN_TYPE_NUMBER: - ImGui::Text("" + reader.getTokFloat()); - break; - case TOKEN_TYPE_BOOL: - ImGui::TextColored(color(1.f, 0.f, 1.f, 1.f), ""+reader.getTokBool()); - break; - } - } - - reader.moveNext(); - } - - ImGui::End(); - - // Handle window X button - if (!documentOpen) - { - g_displayed_doc_filename = ""; - @g_displayed_document = null; - } - - ImGui::PopID(); //"document view" -} - +//#region STAGE_INIT void initializeRacesData() { // find the terrain script @@ -246,9 +230,30 @@ void initializeRacesData() { raceBuilder@ race = races.raceList[i]; convertedRaces.insertLast(null); - convertedAndSavedRaces.insertLast("~"); // ~ means "update UI and redner frame first, then do the writing" + convertedRaceFileNames.insertLast(""); } } +//#endregion + +//#region STAGE_CONVERT +bool convertNextRace() +{ + + // seek first unconverted race; convert it; break loop + for (uint i=0; i < races.raceList.length(); i++) + { + if (@convertedRaces[i] == null) + { + @convertedRaces[i] = convertSingleRace(races.raceList[i]); + convertedRaceFileNames[i] = generateRaceFileName(races.raceList[i].raceName); + return true; + } + } + + return false; +} + +// HELPERS: const string BADCHARS="\\/:%* "; const string GOODCHAR="_"; @@ -269,133 +274,99 @@ string generateRaceFileName(string raceName) return filename; } +void appendKeyValuePair( GenericDocContextClass@ ctx, string key, string value) +{ + ctx.appendTokKeyword( key); + ctx.appendTokString(value); + ctx.appendTokLineBreak(); +} -void advanceImportOneStep() +void appendKeyValuePair( GenericDocContextClass@ ctx, string key, int value) { - switch (stage) + ctx.appendTokKeyword( key); + ctx.appendTokInt(value); + ctx.appendTokLineBreak(); +} + +GenericDocumentClass@ convertSingleRace(raceBuilder@ race) +{ + GenericDocumentClass doc; + GenericDocContextClass ctx(doc); + + ctx.appendTokComment( " ~~ New 'race-def' format (file extension: .race). ~~");ctx.appendTokLineBreak(); + ctx.appendTokComment( " Each race file specifies a single race");ctx.appendTokLineBreak(); + ctx.appendTokComment( " In .terrn2 file, list the race files under new section [Races]");ctx.appendTokLineBreak(); + ctx.appendTokComment( " Filenames must include extension and end with = (like scripts do)");ctx.appendTokLineBreak(); + ctx.appendTokComment( " Race system supports alternating paths!");ctx.appendTokLineBreak(); + ctx.appendTokComment( " Checkpoint format: checkpointNum(1+), altpathNum(1+), x, y, z, rotX, rotY, rotZ, objName(override, optional)");ctx.appendTokLineBreak(); + ctx.appendTokComment( " By convention, the checkpoint meshes are oriented sideways (facing X axis)");ctx.appendTokLineBreak(); + ctx.appendTokLineBreak(); + + appendKeyValuePair(ctx, "race_name", race.raceName); + appendKeyValuePair(ctx, "race_laps", race.laps); + appendKeyValuePair(ctx, "race_checkpoint_object", race.exporterCheckpointObjName); + appendKeyValuePair(ctx, "race_start_object", race.exporterStartObjName); + appendKeyValuePair(ctx, "race_finish_object", race.exporterFinishObjName); + + ctx.appendTokLineBreak(); + + ctx.appendTokKeyword( "begin_checkpoints"); + + ctx.appendTokLineBreak(); + + for (uint i=0; i < race.checkpoints.length(); i++) { - case STAGE_INIT: - { - initializeRacesData(); - stage = STAGE_CONVERT; - break; - } - case STAGE_PUSHMSG: - { - TerrainClass@ terrain = game.getTerrain(); - projectName = terrain.getTerrainName() + " [Races] ~"+thisScript; - - // Fetch terrain's modcache entry - CacheEntryClass@ src_entry = modcache.findEntryByFilename(LOADER_TYPE_TERRAIN, /*partial:*/false, terrain.getTerrainFileName()); - if (@src_entry == null) - { - error = "Not found in modcache!!"; - stage = STAGE_ERROR; - } - - // request project to be created from that cache entry - game.pushMessage(MSG_EDI_CREATE_PROJECT_REQUESTED, { - {'name', projectName}, - {'source_entry', src_entry} - }); - stage = STAGE_GETPROJECT; - break; - } - - case STAGE_GETPROJECT: - { - @projectEntry = modcache.findEntryByFilename(LOADER_TYPE_TERRAIN, /*partial:*/false, projectName+'.terrn2'); - if (@projectEntry != null) - { - stage = STAGE_GENFILES; - } - break; - } - - case STAGE_CONVERT: - { - bool convertedOne = false; - for (uint i=0; i < races.raceList.length(); i++) - { - if (@convertedRaces[i] == null) - { - @convertedRaces[i] = convertSingleRace(races.raceList[i]); - convertedOne = true; - break; - } - } - - if (!convertedOne) - { - stage = STAGE_IDLE; - } - break; - } - - case STAGE_GENFILES: + uint numAltPaths = race.getRealInstanceCount(int(i)); + for (uint j = 0; j < numAltPaths; j++) { - for (uint i=0; i < races.raceList.length(); i++) + ctx.appendTokInt((i+1)); // checkpointNum (1+) + ctx.appendTokInt((j+1)); // altpathNum (1+) + const double[] args = race.checkpoints[i][j]; + ctx.appendTokFloat(args[0]); // pos X + ctx.appendTokFloat(args[1]); // pos Y + ctx.appendTokFloat(args[2]); // pos Z + ctx.appendTokFloat(args[3]); // rot X + ctx.appendTokFloat(args[4]); // rot Y + ctx.appendTokFloat(args[5]); // rot Z + string defaultObjName = (i==0) + ? race.exporterStartObjName + : (i==race.checkpoints.length()-1) ? race.exporterFinishObjName : race.exporterCheckpointObjName; + string actualObjName = race.objNames[i][j]; + if (actualObjName != defaultObjName) { - if (convertedAndSavedRaces[i] == "~") - { - string filename = generateRaceFileName(races.raceList[i].raceName); - fileBeingWritten = filename; - convertedAndSavedRaces[i] =""; // ready to write file - // game.log('DBG GENFILES ['+i+']: fileBeingWritten='+fileBeingWritten); - break; - } - else if (convertedAndSavedRaces[i] == "") - { - // write the ' .race' file in separate frame so the user sees the correct filename on screen. - if (@projectEntry == null) - { - stage=STAGE_ERROR; - error="null project entry while generating files"; - break; - } - if (@convertedRaces[i] == null) - { - stage=STAGE_ERROR; - error="null converted race at position ["+i+"]"; - break; - } - - convertedRaces[i].saveToResource(fileBeingWritten, projectEntry.resource_group); - convertedAndSavedRaces[i] = fileBeingWritten; - // game.log('DBG GENFILES ['+i+']: saved file '+fileBeingWritten); - if (i == races.raceList.length()-1) - { - stage=STAGE_FIXTERRN2; - } - break; - } + ctx.appendTokString(actualObjName); } - break; - } - - case STAGE_FIXTERRN2: - { - fixupTerrn2Document(); - stage = STAGE_DONE; - break; - } - - default: - break; + ctx.appendTokLineBreak(); + } } + + ctx.appendTokKeyword( "end_checkpoints"); + ctx.appendTokLineBreak(); + + return doc; } +//#endregion + +//#region STAGE_FIXTERRN2 string BRACE="["; void fixupTerrn2Document() { - GenericDocumentClass terrn2; - int flags = 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; - terrn2.loadFromResource(projectName+".terrn2", projectEntry.resource_group, flags); - GenericDocContextClass ctx(terrn2); + TerrainClass@ terrain = game.getTerrain(); + if (@terrain == null) + { + game.log("fixupTerrn2Document(): no terrain loaded, nothing to do!"); + return; + } + @convertedTerrn2 = GenericDocumentClass(); + + if (!convertedTerrn2.loadFromResource(terrain.getTerrainFileName(), terrain.getTerrainFileResourceGroup(), genericdoc_utils::FLAGSET_TERRN2)) + { + game.log("fixupTerrn2Document(): could not load terrn2 document, nothing to do!"); + return; + } + + GenericDocContextClass ctx(convertedTerrn2); // Delete section [Scripts] and all it contains bool inSectionScripts = false; @@ -438,19 +409,115 @@ void fixupTerrn2Document() ctx.appendTokLineBreak(); ctx.appendTokKeyword("[Races]"); ctx.appendTokLineBreak(); - for (uint i=0; i < convertedAndSavedRaces.length(); i++) + for (uint i=0; i < convertedRaceFileNames.length(); i++) { - ctx.appendTokKeyword(convertedAndSavedRaces[i]); + ctx.appendTokString(convertedRaceFileNames[i]); ctx.appendTokLineBreak(); } - forceExportINI(terrn2); + + +} +//#endregion + +// nothing for STAGE_IDLE + +//#region STAGE_PUSHMSG +void pushMsgRequestCreateProject() +{ + TerrainClass@ terrain = game.getTerrain(); + projectName = terrain.getTerrainName() + " [Races] ~"+thisScript; + + // Fetch terrain's modcache entry + CacheEntryClass@ src_entry = modcache.findEntryByFilename(LOADER_TYPE_TERRAIN, /*partial:*/false, terrain.getTerrainFileName()); + if (@src_entry == null) + { + error = "Not found in modcache!!"; + stage = STAGE_ERROR; + } + + // request project to be created from that cache entry + game.pushMessage(MSG_EDI_CREATE_PROJECT_REQUESTED, { + {'name', projectName}, + {'source_entry', src_entry} + }); +} +//#endregion + +//#region STAGE_GETPROJECT +void getProject() +{ + @projectEntry = modcache.findEntryByFilename(LOADER_TYPE_TERRAIN, /*partial:*/false, projectName+'.terrn2'); + if (@projectEntry != null) + { + stage = STAGE_WRITERACES; + } + else + { + stage=STAGE_ERROR; + error="Could not load project entry"; + } +} +//#endregion + +//#region STAGE_WRITERACES +void writeNextRace() +{ + for (uint i=0; i < races.raceList.length(); i++) + { + if (int(i) <= topWrittenRace) + { + continue; // already handled + } + + if (fileBeingWritten == "") + { + string filename = convertedRaceFileNames[i]; + fileBeingWritten = filename; + // game.log('DBG WRITERACES ['+i+']: fileBeingWritten='+fileBeingWritten); + break; + } + else + { + // write the ' .race' file in separate frame so the user sees the correct filename on screen. + if (@projectEntry == null) + { + stage=STAGE_ERROR; + error="null project entry while generating files"; + break; + } + if (@convertedRaces[i] == null) + { + stage=STAGE_ERROR; + error="null converted race at position ["+i+"]"; + break; + } + + convertedRaces[i].saveToResource(fileBeingWritten, projectEntry.resource_group); + topWrittenRace=int(i); + + // game.log('DBG GENFILES ['+i+']: saved file '+fileBeingWritten); + fileBeingWritten = ""; + if (i == races.raceList.length()-1) + { + stage=STAGE_WRITETERRN2; + } + break; + } + } +} +//#endregion + +//#region STAGE_WRITETERRN2 +void writeTerrn2() +{ + forceExportINI(convertedTerrn2); // delete original file (GenericDocument cannot overwrite) game.deleteResource(projectName+".terrn2", projectEntry.resource_group); // write out modified file - terrn2.saveToResource(projectName+".terrn2", projectEntry.resource_group); + convertedTerrn2.saveToResource(projectName+".terrn2", projectEntry.resource_group); } void forceExportINI(GenericDocumentClass@ doc) @@ -471,93 +538,75 @@ void forceExportINI(GenericDocumentClass@ doc) ctx.moveNext(); } } +//#endregion -void appendKeyValuePair( GenericDocContextClass@ ctx, string key, string value) -{ - ctx.appendTokKeyword( key); - ctx.appendTokString(value); - ctx.appendTokLineBreak(); -} -void appendKeyValuePair( GenericDocContextClass@ ctx, string key, int value) -{ - ctx.appendTokKeyword( key); - ctx.appendTokFloat(value); - ctx.appendTokLineBreak(); -} -GenericDocumentClass@ convertSingleRace(raceBuilder@ race) +void advanceImportOneStep() { - GenericDocumentClass doc; - GenericDocContextClass ctx(doc); - - ctx.appendTokComment( " ~~ New 'race-def' format (file extension: .race). ~~");ctx.appendTokLineBreak(); - ctx.appendTokComment( " Each race file specifies a single race");ctx.appendTokLineBreak(); - ctx.appendTokComment( " In .terrn2 file, list the race files under new section [Races]");ctx.appendTokLineBreak(); - ctx.appendTokComment( " Filenames must include extension and end with = (like scripts do)");ctx.appendTokLineBreak(); - ctx.appendTokComment( " Race system supports alternating paths!");ctx.appendTokLineBreak(); - ctx.appendTokComment( " Checkpoint format: checkpointNum(1+), altpathNum(1+), x, y, z, rotX, rotY, rotZ, objName(override, optional)");ctx.appendTokLineBreak(); - ctx.appendTokComment( " By convention, the checkpoint meshes are oriented sideways (facing X axis)");ctx.appendTokLineBreak(); - ctx.appendTokLineBreak(); - - appendKeyValuePair(ctx, "race_name", race.raceName); - appendKeyValuePair(ctx, "race_laps", race.laps); - appendKeyValuePair(ctx, "race_checkpoint_object", race.exporterCheckpointObjName); - appendKeyValuePair(ctx, "race_start_object", race.exporterStartObjName); - appendKeyValuePair(ctx, "race_finish_object", race.exporterFinishObjName); - - ctx.appendTokLineBreak(); - - ctx.appendTokKeyword( "begin_checkpoints"); - - ctx.appendTokLineBreak(); - - for (uint i=0; i < race.checkpoints.length(); i++) + switch (stage) { - uint numAltPaths = race.getRealInstanceCount(int(i)); - for (uint j = 0; j < numAltPaths; j++) + case STAGE_INIT: { - ctx.appendTokFloat((i+1)); // checkpointNum (1+) - ctx.appendTokFloat((j+1)); // altpathNum (1+) - const double[] args = race.checkpoints[i][j]; - ctx.appendTokFloat(args[0]); // pos X - ctx.appendTokFloat(args[1]); // pos Y - ctx.appendTokFloat(args[2]); // pos Z - ctx.appendTokFloat(args[3]); // rot X - ctx.appendTokFloat(args[4]); // rot Y - ctx.appendTokFloat(args[5]); // rot Z - string defaultObjName = (i==0) - ? race.exporterStartObjName - : (i==race.checkpoints.length()-1) ? race.exporterFinishObjName : race.exporterCheckpointObjName; - string actualObjName = race.objNames[i][j]; - if (actualObjName != defaultObjName) + initializeRacesData(); + stage = STAGE_CONVERT; + break; + } + case STAGE_PUSHMSG: + { + pushMsgRequestCreateProject(); + stage = STAGE_GETPROJECT; + break; + } + + case STAGE_GETPROJECT: + { + getProject(); + break; + } + + case STAGE_CONVERT: + { + if (!convertNextRace()) { - ctx.appendTokString(actualObjName); + stage = STAGE_FIXTERRN2; } - ctx.appendTokLineBreak(); - } + break; + } + + case STAGE_WRITERACES: + { + + writeNextRace(); + break; + } + + case STAGE_FIXTERRN2: + { + fixupTerrn2Document(); + stage = STAGE_IDLE; + break; + } + + case STAGE_WRITETERRN2: + { + writeTerrn2(); + stage = STAGE_DONE; + break; + } + + default: + break; } - - ctx.appendTokKeyword( "end_checkpoints"); - ctx.appendTokLineBreak(); - - return doc; } - -void frameStep(float dt) -{ - // === DRAW UI === - if ( ImGui::Begin("Race import", closeBtnHandler.windowOpen, /*flags:*/0)) - { - // Draw the "Terminate this script?" prompt on the top (if not disabled by config). - closeBtnHandler.draw(); - drawUI(); - ImGui::End(); - } - - - // === PERFORM IMPORT STEP === - advanceImportOneStep(); -} + + + + + + + + + diff --git a/resources/scripts/genericdoc_utils.as b/resources/scripts/genericdoc_utils.as new file mode 100644 index 0000000000..1837c434b1 --- /dev/null +++ b/resources/scripts/genericdoc_utils.as @@ -0,0 +1,269 @@ +/// \title GenericDocument viewing/editing +/// \brief IMGUI-based editor of tokenized GenericDocument +// =================================================== + +// 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 + = GENERIC_DOCUMENT_OPTION_ALLOW_NAKED_STRINGS + | GENERIC_DOCUMENT_OPTION_ALLOW_SLASH_COMMENTS; + + 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_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; + + class GenericDocEditor + { + + 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.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; + displayedDocFilename = filename; + this.resetTokenEditPanel(); + } + } + + void resetTokenEditPanel() + { + hoveredTokenPos = -1; + focusedTokenPos = -1; + } + + 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; + displayedDocFilename = ""; + this.resetTokenEditPanel(); + 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; + case TOKEN_TYPE_FLOAT: + { + ImGui::Text("" + reader.getTokFloat()); + break; + } + case TOKEN_TYPE_INT: + { + 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_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() + { + if ( focusedTokenPos == -1) + { + ImGui::TextDisabled("Use left mouse button to select tokens."); + return; // nothing to draw + } + GenericDocContextClass reader(displayedDocument); + while (!reader.endOfFile() && reader.getPos() != uint(focusedTokenPos)) reader.moveNext(); + if (reader.endOfFile()) + { + ImGui::Text("EOF!!"); + } + else + { + ImGui::TextDisabled("Token pos: "); ImGui::SameLine(); ImGui::Text("" + reader.getPos()); + 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 + + + + From 9c22b5266b857ab4ca47734a459be94653b757a5 Mon Sep 17 00:00:00 2001 From: ohlidalp Date: Wed, 27 Nov 2024 23:57:33 +0100 Subject: [PATCH 18/36] :angel:Script: `ImGui::SetNextWindowPos()` - added params flags & pivot. --- source/main/scripting/bindings/ImGuiAngelscript.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/main/scripting/bindings/ImGuiAngelscript.cpp b/source/main/scripting/bindings/ImGuiAngelscript.cpp index 555a5e49f9..27243e6e08 100644 --- a/source/main/scripting/bindings/ImGuiAngelscript.cpp +++ b/source/main/scripting/bindings/ImGuiAngelscript.cpp @@ -236,8 +236,8 @@ void RoR::RegisterImGuiBindings(AngelScript::asIScriptEngine* engine) // engine->RegisterGlobalFunction("bool IsWindowAppearing()", asFUNCTIONPR(ImGui::IsWindowAppearing, (), bool), asCALL_CDECL); engine->RegisterGlobalFunction("void SetWindowFontScale(float)", asFUNCTIONPR(ImGui::SetWindowFontScale, (float), void), asCALL_CDECL); - engine->RegisterGlobalFunction("void SetNextWindowPos(vector2)", asFUNCTIONPR([](Vector2 v) { - ImGui::SetNextWindowPos(ImVec2(v.x, v.y)); }, (Vector2), void), asCALL_CDECL); + engine->RegisterGlobalFunction("void SetNextWindowPos(vector2, int=0, vector2=vector2(0,0))", asFUNCTIONPR([](Vector2 v, int flags, Vector2 pivot) { + ImGui::SetNextWindowPos(ImVec2(v.x, v.y), flags, ImVec2(pivot.x, pivot.y)); }, (Vector2, int, Vector2), void), asCALL_CDECL); engine->RegisterGlobalFunction("void SetNextWindowSize(vector2)", asFUNCTIONPR([](Vector2 v) { ImGui::SetNextWindowSize(ImVec2(v.x, v.y)); }, (Vector2), void), asCALL_CDECL); engine->RegisterGlobalFunction("void SetNextWindowContentSize(vector2)", asFUNCTIONPR([](Vector2 v) { From bea833febebaa716f9074f25a1b3ec978d8bbf77 Mon Sep 17 00:00:00 2001 From: ohlidalp Date: Thu, 28 Nov 2024 06:51:28 +0100 Subject: [PATCH 19/36] :sparkles: Example scripts: 5 new, 3 updated. terrnBatcher coding style matched others. rigEditorAlpha updated to use 'genericdoc_utils' 3 scripts renamed - category changed for clarity. --- .../example_ImGui_devDocLinksOffline.as | 244 ++++ .../scripts/example_ImGui_hotkeyHighlight.as | 91 ++ ...pha.as => example_actor_RigEditorAlpha.as} | 180 +-- ...Tuning.as => example_actor_shockTuning.as} | 0 resources/scripts/example_game_gridSpawn.as | 71 + .../scripts/example_ogre_terrnBatcher.as | 1220 +++++++++-------- .../scripts/example_terrn_odefBrowser.as | 691 ++++++++++ ...rter.as => example_terrn_raceConverter.as} | 0 .../scripts/example_terrn_railBuilder.as | 168 +++ 9 files changed, 1901 insertions(+), 764 deletions(-) create mode 100644 resources/scripts/example_ImGui_devDocLinksOffline.as create mode 100644 resources/scripts/example_ImGui_hotkeyHighlight.as rename resources/scripts/{example_RigEditor_alpha.as => example_actor_RigEditorAlpha.as} (70%) rename resources/scripts/{example_game_shockTuning.as => example_actor_shockTuning.as} (100%) create mode 100644 resources/scripts/example_game_gridSpawn.as create mode 100644 resources/scripts/example_terrn_odefBrowser.as rename resources/scripts/{example_terrn2_raceConverter.as => example_terrn_raceConverter.as} (100%) create mode 100644 resources/scripts/example_terrn_railBuilder.as diff --git a/resources/scripts/example_ImGui_devDocLinksOffline.as b/resources/scripts/example_ImGui_devDocLinksOffline.as new file mode 100644 index 0000000000..543ab7e147 --- /dev/null +++ b/resources/scripts/example_ImGui_devDocLinksOffline.as @@ -0,0 +1,244 @@ +/// \title QUICK SCRIPT DOC UI (Offline) +/// \brief Hand-crafted offline script docs with links to +/// The local metadata are stored in 2 arrays: +/// * array gDataTypes_ALL ~ Covers value/ref types + singleton names. +/// * arrat gFuncs_ALL ~ Covers global funcs + methods. +// =================================================== + + +// Window [X] button handler +#include "imgui_utils.as" +imgui_utils::CloseWindowPrompt closeBtnHandler; + +//#region Metadata (DB) + +const int TYPEFLAG_asOBJ_REF = 1 << 0; +const int TYPEFLAG_asOBJ_NOCOUNT = 1 << 1; +const int TYPEFLAG_BUILTIN = 1 << 2; +array gDataTypes_ALL = array(); // < Filled by DataType constructor, do not push manually. + +array gFuncs_ALL = array(); // < Filled by FuncDef constructor, do not push manually. + +// Primitives +DataType@ gDataTypes_string = DataType("string", "http://www.angelcode.com/angelscript/sdk/docs/manual/doc_script_stdlib_string.html", "built-in string", TYPEFLAG_BUILTIN); +DataType@ gDataTypes_bool = DataType("bool", "http://www.angelcode.com/angelscript/sdk/docs/manual/doc_datatypes_primitives.html#bool", "primitive bool", TYPEFLAG_BUILTIN); +DataType@ gDataTypes_int = DataType("int", "http://www.angelcode.com/angelscript/sdk/docs/manual/doc_datatypes_primitives.html#int", "primitive int", TYPEFLAG_BUILTIN); +DataType@ gDataTypes_dictionary = DataType("dictionary", "http://www.angelcode.com/angelscript/sdk/docs/manual/doc_datatypes_dictionary.html", "built-in dictionary", TYPEFLAG_BUILTIN); + +// General +DataType@ gDataTypes_BeamClass = DataType("BeamClass", "https://developer.rigsofrods.org/d8/da5/class_script2_game_1_1_beam_class.html", "A loaded 'actor' (vehicle/load/machine)", TYPEFLAG_asOBJ_REF); +DataType@ gDataTypes_TerrainClass = DataType("TerrainClass", "https://developer.rigsofrods.org/d8/deb/class_script2_game_1_1_terrain_class.html", "A loaded terrain", TYPEFLAG_asOBJ_REF); +DataType@ gDataTypes_CVarClass = DataType("CVarClass", "https://developer.rigsofrods.org/d8/de9/class_script2_game_1_1_c_var_class.html", "Console variable, from RoR.cfg or created by user/script", TYPEFLAG_asOBJ_REF | TYPEFLAG_asOBJ_NOCOUNT); +DataType@ gDataTypes_CacheEntryClass = DataType("CacheEntryClass", "", "Installed mod entry", TYPEFLAG_asOBJ_REF); + +// Singletons (with methods) +DataType@ gDataTypes_GameScriptClass = DataType("GameScriptClass", "https://developer.rigsofrods.org/dc/d63/class_script2_game_1_1_game_script_class.html", "Game events, message queue, vehicle and terrain data", TYPEFLAG_asOBJ_REF | TYPEFLAG_asOBJ_NOCOUNT, "game"); +Func@ gFuncDefs_getCurrentTruck = Func(gDataTypes_GameScriptClass, "getCurrentTruck", {}, gDataTypes_BeamClass, "Get player's current vehicle, returns if on foot."); +Func@ gFuncs_game_getTerrain = Func(gDataTypes_GameScriptClass, "getTerrain", {}, gDataTypes_TerrainClass, "Get currently loaded terrain, returns if in main menu."); + +DataType@ gDataTypes_ConsoleClass = DataType("ConsoleClass", "https://developer.rigsofrods.org/d3/d4f/class_script2_game_1_1_console_class.html", "Console variables - read and write", TYPEFLAG_asOBJ_REF | TYPEFLAG_asOBJ_NOCOUNT, "console"); +Func@ gFunc_console_cVarGet = Func(gDataTypes_ConsoleClass, "cVarGet", { FuncArg(gDataTypes_string, "name", 0), FuncArg(gDataTypes_int, "flags", 0) }, gDataTypes_CVarClass, "Gets or creates a console var"); +Func@ gFunc_console_cVarFinds = Func(gDataTypes_ConsoleClass, "cVarFind", { FuncArg(gDataTypes_string, "name", 0) }, gDataTypes_CVarClass, "Gets an existing console var, or returns "); + +DataType@ gDataTypes_InputEngineClass = DataType("InputEngineClass", "https://developer.rigsofrods.org/d2/d8d/class_script2_game_1_1_input_engine_class.html", "Input keybinds and keystates",TYPEFLAG_asOBJ_REF | TYPEFLAG_asOBJ_NOCOUNT,"inputs"); +Func@ gFuncs_inputs_getEventCommandTrimmed = Func(gDataTypes_InputEngineClass, "getEventCommandTrimmed", { FuncArg(gDataTypes_int, "eventID", 0) }, gDataTypes_string, "Get keybind for input event"); + +DataType@ gDataTypes_CacheSystemClass = DataType("CacheSystemClass", "", "Installed mods - query or create",TYPEFLAG_asOBJ_REF | TYPEFLAG_asOBJ_NOCOUNT, "modcache"); + Func@ gFuncs_modcache_findEntryByFilename = Func(gDataTypes_CacheSystemClass, "findEntryByFilename", { FuncArg(gDataTypes_int, "type", 0), FuncArg(gDataTypes_bool, "partial", 0), FuncArg(gDataTypes_string, "name", 0) }, gDataTypes_CacheEntryClass, "Find single mod by fulltext"); + +//#endregion + +//#region DETAIL: class DataType +class DataType +{ + string name; + string description; + string singleton_name; //!< The global var name, only selected data types + string url; + int typeflags; + + DataType(string _n, string _u, string _desc, int _typeflags, string _singleton = "") + { + name = _n; + url = _u; + description = _desc; + typeflags = _typeflags; + singleton_name = _singleton; + + gDataTypes_ALL.insertLast(this); + } + void drawClassInfo() + { + ImGui::Bullet(); + if (url != "") + { + imgui_utils::ImHyperlink( url, name, false); + } + else // oops, undocumented stuff!! + { + ImGui::Text(name); + } + if (typeflags & TYPEFLAG_asOBJ_REF != 0) + { + ImGui::SameLine(); ImGui::Text("[ref]"); + } + if (typeflags & TYPEFLAG_asOBJ_NOCOUNT != 0) + { + ImGui::SameLine(); ImGui::Text("[nocount]"); + } + ImGui::SameLine(); ImGui::TextDisabled(description); + } + void drawSingletonInfo() + { + bool expanded = ImGui::TreeNode("##"+name+url); + ImGui::SameLine(); + ImGui::SameLine(); + if (url != "") + { + imgui_utils:: ImHyperlink( url, singleton_name, false); + } + else // oops, undocumented stuff!! + { + ImGui::Text(singleton_name); + } + if (expanded) + { + for (uint i = 0; i < gFuncs_ALL.length(); i++) + { + Func@ func = gFuncs_ALL[i]; + if (@func.obj == @this) + { + func.drawFunc(); + } + } + ImGui::TreePop(); + } + else + { + ImGui::SameLine(); ImGui::TextDisabled(description); + } + } + void drawAsArg(string arg_name) + { + imgui_utils:: ImHyperlink( url, name, false); + ImGui::SameLine(); ImGui::Text(arg_name); + + } + void drawAsRetval() + { + imgui_utils:: ImHyperlink( url, name, false); + } +} ; + +//#endregion (DETAIL: class DataType) + +//#region DETAIL: class Func + + +class Func +{ + DataType@ obj; // null if it's a global function + DataType@ returns; + string name; + string description; + array args; + + Func(DataType@ _obj, string _n, array _args, DataType@ _ret, string _desc) + { + @obj = @_obj; + name = _n; + args = _args; + @returns = @_ret; + description = _desc; + gFuncs_ALL.insertLast(this); + } + void drawFunc() + { + ImGui::Text(" ." + name + "("); + for (uint i = 0; i < args.length(); i++) + { + FuncArg@ farg = args[i]; + ImGui::SameLine(); farg.type.drawAsArg(farg.name); + } + ImGui::SameLine(); ImGui::Text(") -> "); + ImGui::SameLine(); returns.drawAsRetval(); + ImGui::SameLine(); ImGui::TextDisabled(description); + } +}; + +class FuncArg +{ + DataType@ type; + string name; + int flags = 0; + FuncArg(DataType@ _t, string _n, int _flags) + { + name = _n; + @type = @_t; + flags = _flags; + } + +} ; +//#endregion (DETAIL: class Func) + +//#region UI drawing +bool gTypesShowAll = false; +void drawScriptQuickLinksUI() +{ + ImGui::TextDisabled(" ::: G L O B A L S ::: "); + for (uint i = 0; i < gDataTypes_ALL.length(); i++) + { + if ( gDataTypes_ALL[i].singleton_name != "") + { + gDataTypes_ALL[i].drawSingletonInfo(); + } + } + ImGui::Text(""); + + ImGui::TextDisabled(" ::: T Y P E S ::: "); + ImGui::SameLine(); ImGui::Checkbox("Show all", gTypesShowAll); + for (uint i = 0; i < gDataTypes_ALL.length(); i++) + { + DataType@ datatype = gDataTypes_ALL[i]; + // by default do not draw builtins and singleton-classes + const bool builtin = (datatype.typeflags & TYPEFLAG_BUILTIN) != 0; + const bool singleton = datatype.singleton_name != ""; + if ( (!builtin && !singleton) || gTypesShowAll ) + { + gDataTypes_ALL[i].drawClassInfo(); + } + } + ImGui::Text(""); + + + + // `modcache` + // ImGui::Text(" .findEntryByFilename(/*loaderType*/LOADER_TYPE_ALLBEAM, /*partial:*/false, actor.getTruckFileName());"); + +} + +// ~~~ Script quick links ~~~ HELPERS ~~~ // + +void drawFunc(string name, string ret_obj, string ret_url_local) +{ + ImGui::Text(name + "()"); + ImGui::SameLine(); +} + + + +//#endregion (UI drawing) + +// `frameStep()` runs every frame; `dt` is delta time in seconds. +void frameStep(float dt) +{ + + if (ImGui::Begin("Scripting - quick links UI", closeBtnHandler.windowOpen, 0)) + { + ImGui::TextDisabled(" * * OFFLINE EDITION * * (Only popular APIs) * * "); + ImGui::Separator(); + drawScriptQuickLinksUI(); + ImGui::End(); + } +} + diff --git a/resources/scripts/example_ImGui_hotkeyHighlight.as b/resources/scripts/example_ImGui_hotkeyHighlight.as new file mode 100644 index 0000000000..60e5baebcd --- /dev/null +++ b/resources/scripts/example_ImGui_hotkeyHighlight.as @@ -0,0 +1,91 @@ +// TERRAIN EDITOR HITS UI +// =================================================== + +// Window [X] button handler +#include "imgui_utils.as" +imgui_utils::CloseWindowPrompt closeBtnHandler; + +const color TITLEART_COLOR (0.7f, 0.9f, 0.8f, 1.f); +const color ACTIVE_KEYBIND_COL(0.8f, 1.0f, 0.2f, 1.f); +CVarClass@ cvar_sim_state = console.cVarFind("sim_state"); // built in + +// `frameStep()` runs every frame; `dt` is delta time in seconds. +void frameStep(float dt) +{ + + + ImGui::Begin("terrnEditorUI *!* ALPHA> *!*", closeBtnHandler.windowOpen, 0); + ImGui::Dummy(vector2(400, 1)); + drawTerrnEditTitleArt( ); + + ImGui::Separator(); + + drawTerrnEditHelpUI(); + + ImGui::End(); //"terrnEditorUI" + +} + +// #region TerrnEditor title art +void drawTerrnEditTitleArt() +{ + string title=" T E R R N E D I T O R "; + string art=" ^ ^ ^ ~ ^ ^ ^ ^ ~ ^ ^ ^ "; + vector2 title_size = ImGui::CalcTextSize(title); + vector2 art_size = ImGui::CalcTextSize(art); + + vector2 pos = ImGui::GetCursorScreenPos(); + ImGui::GetWindowDrawList().AddText(pos+vector2(0,-4), TITLEART_COLOR, art); + ImGui::GetWindowDrawList().AddText(pos+vector2(0,0), TITLEART_COLOR, art); + ImGui::GetWindowDrawList().AddText(pos+vector2(0,4), TITLEART_COLOR, art); + ImGui::SetCursorPos(ImGui::GetCursorPos() + vector2(art_size.x, 0)); + pos = ImGui::GetCursorScreenPos(); + ImGui::GetWindowDrawList().AddText(pos+vector2(0,-3), TITLEART_COLOR, title); + ImGui::SetCursorPos(ImGui::GetCursorPos() + vector2( title_size.x + 5, 0)); + pos = ImGui::GetCursorScreenPos(); + ImGui::GetWindowDrawList().AddText(pos+vector2(0,-4), TITLEART_COLOR, art); + ImGui::GetWindowDrawList().AddText(pos+vector2(0,0), TITLEART_COLOR, art); + ImGui::GetWindowDrawList().AddText(pos+vector2(0,4), TITLEART_COLOR, art); + ImGui::Text("");//spacer +} +//#endregion title art + +// #region TerrnEditor info +void drawTerrnEditHelpUI() +{ + if (cvar_sim_state.getInt() != 3) + { + ImGui::Text("Activate terrain editing mode with"); + ImGui::SameLine(); + ImDrawEventKeybindHighlighted(EV_COMMON_TOGGLE_TERRAIN_EDITOR); + } + else + { + ImGui::TextDisabled(" ^ GLOBAL HOTKEYS ^"); + ImGui::AlignTextToFramePadding(); + ImGui::Bullet(); + ImGui::SameLine(); ImDrawEventKeybindHighlighted(EV_COMMON_TOGGLE_TERRAIN_EDITOR); + ImGui::SameLine(); ImGui::Text("Exit terrain editor"); + } +} +//#endregion TerrnEditor info + +//#region imgui_utils < -- ImDrawEventKeybindHighlighted(inputEvents ev) +void ImDrawEventKeybindHighlighted(inputEvents ev) +{ + bool active = inputs.getEventBoolValue(ev); + string keybind = "" + inputs.getEventCommandTrimmed(ev); + if (active) + { + ImGui::PushStyleColor(ImGuiCol_Text, ACTIVE_KEYBIND_COL); + } + ImGui::BeginChildFrame(ev, ImGui::CalcTextSize(keybind) + vector2(8,6)); + ImGui::Text(keybind); + ImGui::EndChildFrame(); + if (active) + { + ImGui::PopStyleColor(); // ImGuiCol_Text + } +} +//#endregion draw ev keybind highlighted + diff --git a/resources/scripts/example_RigEditor_alpha.as b/resources/scripts/example_actor_RigEditorAlpha.as similarity index 70% rename from resources/scripts/example_RigEditor_alpha.as rename to resources/scripts/example_actor_RigEditorAlpha.as index 008db3cee8..c46836c677 100644 --- a/resources/scripts/example_RigEditor_alpha.as +++ b/resources/scripts/example_actor_RigEditorAlpha.as @@ -4,8 +4,19 @@ /// Written and auto-indented using script_editor.as! // =================================================== +// Window [X] button handler #include "imgui_utils.as" +imgui_utils::CloseWindowPrompt closeBtnHandler; + +// node/beam drawing #include "gridviewer_utils.as" +gridviewer_utils::GridViewer viewer_x; +gridviewer_utils::GridViewer viewer_y; +gridviewer_utils:: GridViewer viewer_z; + +// genericDoc editor +#include "genericdoc_utils.as" +genericdoc_utils::GenericDocEditor gdEditor; // ----- config ----- @@ -14,19 +25,16 @@ int m_statusbar_height_pixels = 25; // ---- variables ----- GenericDocumentClass@ m_displayed_document = null; -int m_hovered_token_pos = -1; -int m_focused_token_pos = -1; + string m_error_str; string m_project_creation_pending; // name of the truck file which will become available in modcache. CacheEntryClass@ m_project_entry; -gridviewer_utils::GridViewer viewer_x; -gridviewer_utils::GridViewer viewer_y; -gridviewer_utils:: GridViewer viewer_z; + dictionary@ m_modcache_results = null; CacheEntryClass@ m_awaiting_load_bundle_entry = null; color node_color = color(0.8, 0.9, 0.2, 1.0); float node_radius = 1.f; -imgui_utils::CloseWindowPrompt closeBtnHandler; // Window [X] button handler + // ---- functions ---- @@ -112,16 +120,16 @@ void drawWindow() { if (@m_displayed_document != null) { - drawDocumentBody(); + + gdEditor.drawDocumentBody(); } } ImGui::EndChild(); // "docBody" - if ( m_focused_token_pos > -1) - { - ImGui::Separator(); - drawTokenEditPanel(); - } + + ImGui::Separator(); + gdEditor.drawTokenEditPanel(); + } ImGui::EndChild(); // "leftPane" ImGui::SameLine(); @@ -311,6 +319,7 @@ void loadDocument() return; } @m_displayed_document = doc; + gdEditor.setDocument(m_displayed_document); } void loadAndFixupDocument() @@ -348,114 +357,13 @@ void loadAndFixupDocument() if (!ctx.endOfFile(2)) { ctx.setTokFloat(2, 8990); // special "Project" category } + + gdEditor.setDocument(m_displayed_document); } // #endregion -// #region Tokenized document drawing - -void drawDocumentBody() -{ - ImGui::PushID("docBody"); - bool hover_found = false; - - GenericDocContextClass reader(m_displayed_document); - 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_NUMBER: { - ImGui::TextColored(tokenColor(reader), "" + reader.getTokFloat()); - break; - } - case TOKEN_TYPE_BOOL: { - ImGui::TextColored(tokenColor(reader), ""+reader.getTokBool()); - break; - } - } - } - - if (ImGui::IsItemHovered()) { - m_hovered_token_pos = reader.getPos() ; - hover_found = true; - } - - if (ImGui::IsItemClicked(0)) - { - m_focused_token_pos = reader.getPos(); - } - - reader.moveNext(); - - } - - if (!hover_found) - { - m_hovered_token_pos = -1; - } - - ImGui::PopID(); // "docBody" -} - -void drawTokenEditPanel() -{ - GenericDocContextClass reader(m_displayed_document); - while (!reader.endOfFile() && (reader.getPos() != uint(m_focused_token_pos))) - { - reader.moveNext(); - } - - if (reader.endOfFile()) - { - ImGui::Text("EOF!!"); - } - else - { - ImGui::TextDisabled("Token pos: "); ImGui::SameLine(); ImGui::Text("" + reader.getPos()); - ImGui::TextDisabled("Token type: "); ImGui::SameLine(); ImGui::Text(tokenTypeStr(reader.tokenType())); - } -} - -// #endregion + // #region GridViewer node drawing @@ -502,45 +410,7 @@ void drawNodes(gridviewer_utils::GridViewer @viewer) // #endregion -// #region Token drawing helpers -string tokenTypeStr(TokenType t) -{ - switch (t) - { - case TOKEN_TYPE_NUMBER: return "Number"; - 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 "?"; -} - -color tokenColor(GenericDocContextClass@ reader) -{ - if (m_focused_token_pos > -1 && reader.getPos() == uint(m_focused_token_pos)) - { - return color(0.9f, 0.1f, 0.1f, 1.f); - } - - if (m_hovered_token_pos > -1 && reader.getPos() == uint(m_hovered_token_pos)) - { - 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_NUMBER: return color(0.9, 0.9, 0.9, 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); - } - return color(0.9, 0.9, 0.9, 1.f); -} -// #endregion + // #region Event handling or polling void checkForCreatedProject() diff --git a/resources/scripts/example_game_shockTuning.as b/resources/scripts/example_actor_shockTuning.as similarity index 100% rename from resources/scripts/example_game_shockTuning.as rename to resources/scripts/example_actor_shockTuning.as diff --git a/resources/scripts/example_game_gridSpawn.as b/resources/scripts/example_game_gridSpawn.as new file mode 100644 index 0000000000..6e5cd44c1a --- /dev/null +++ b/resources/scripts/example_game_gridSpawn.as @@ -0,0 +1,71 @@ +// BUS STACKING SCRIPT for performance testing +// requested from Discord#dev: +// https://discord.com/channels/136544456244461568/189904947649708032/1242063394601828503 +// =================================================== + +// Window [X] button handler +#include "imgui_utils.as" +imgui_utils::CloseWindowPrompt closeBtnHandler; + +const vector3 Y_AXIS(0,1,0); + +int cfgXCount = 4; +int cfgYCount = 2; // up axis +int cfgZCount = 4; +float cfgXStepMeters = 12.2f; +float cfgYStepMeters = 3.5f; // up axis +float cfgZStepMeters = 4.f; + +void frameStep(float dt) +{ + if (ImGui::Begin("::: GRID SPAWN :::", closeBtnHandler.windowOpen, 0)) + { + // Draw the "Terminate this script?" prompt on the top (if not disabled by config). + closeBtnHandler.draw(); + + drawMainPanel(); + + ImGui::End(); + } +} + +void drawMainPanel() +{ + +if (ImGui::InputInt("X count", cfgXCount)) { if (cfgXCount < 1) {cfgXCount=1;} } +if (ImGui::InputInt("Y count (up)", cfgYCount)) { if (cfgYCount < 1) {cfgYCount=1;} } +if (ImGui::InputInt("Z count", cfgZCount)) { if (cfgZCount < 1) {cfgZCount=1;} } + ImGui::InputFloat("X step (m)", cfgXStepMeters); + ImGui::InputFloat("Y (up) step (m)", cfgYStepMeters); + ImGui::InputFloat("Z step (m)", cfgZStepMeters); + + ImGui::Separator(); + if (ImGui::Button("stack busses! (total "+cfgXCount*cfgYCount*cfgZCount+")")) + { + stackBusses(); + } +} + +void stackBusses() +{ + bool freePosition = false; + quaternion gridRot(game.getPersonRotation(), Y_AXIS); + for (int iy = 0; iy < cfgYCount; iy++) + { + for (int ix = 0; ix < cfgXCount; ix++) + { + for (int iz = 0; iz < cfgZCount; iz++) + { + vector3 gridPos( ix * cfgXStepMeters, iy * cfgYStepMeters, iz * cfgZStepMeters); + vector3 spawnPos = game.getPersonPosition() + (gridRot * gridPos); + + game.pushMessage(MSG_SIM_SPAWN_ACTOR_REQUESTED, { + {'filename', '95bbUID-agoras.truck'}, + {'free_position', freePosition}, + {'position', spawnPos}, + {'rotation', quaternion(game.getPersonRotation(), Y_AXIS)} + }); + } + } + } +} diff --git a/resources/scripts/example_ogre_terrnBatcher.as b/resources/scripts/example_ogre_terrnBatcher.as index 24683bf38a..9b0811cf68 100644 --- a/resources/scripts/example_ogre_terrnBatcher.as +++ b/resources/scripts/example_ogre_terrnBatcher.as @@ -18,7 +18,7 @@ #include "imgui_utils.as" imgui_utils::CloseWindowPrompt closeBtnHandler; -TerrnBatcherUI tbUI; + void main() { @@ -27,764 +27,766 @@ void main() void frameStep(float dt) { - if (ImGui::Begin("TERRN BATCHER [ALPHA]", closeBtnHandler.windowOpen, ImGuiWindowFlags_AlwaysAutoResize)) + if ( ImGui::Begin("TERRN BATCHER [ALPHA]", closeBtnHandler.windowOpen, ImGuiWindowFlags_AlwaysAutoResize)) { + // Draw the "Terminate this script?" prompt on the top (if not disabled by config). closeBtnHandler.draw(); - tbUI.draw(); - ImGui::End(); + + drawMainPanel(); + + ImGui::End(); } } -class TerrnBatcherUI // Based on Inspector UI, can pick scenenodes and prepare Schedule for the batching. + +// dictionary> tbuiSelection; ~ Used by the Pick buttons. +// * Keeping scenenode pointers is dangerous - they aren't refcounted. +// * We keep a dictionary "scenenode name" -> child node indices +dictionary tbuiSelection; + +// Checkboxes in 'batch controls' +bool tbuiHideOriginal = true; +bool tbuiShowNew = true; + +// Checkboxes in 'inspector' +bool tbuiShowDumpButton = false; + +// Counters +uint tbuiNumBatchesCreated = 0; +uint tbuiNumMeshesDumped = 0; + +// Constants +string tbuiOutputsNodeName = "TerrnBatcher outputs"; + +//#region Draw UI - main +void drawMainPanel() { - // dictionary> tbuiSelection; ~ Used by the Pick buttons. - // * Keeping scenenode pointers is dangerous - they aren't refcounted. - // * We keep a dictionary "scenenode name" -> child node indices - dictionary tbuiSelection; - // Checkboxes in 'batch controls' - bool tbuiHideOriginal = true; - bool tbuiShowNew = true; + ImGui::Text("shows how you can traverse scene and merge meshes/textures together for better FPS"); + ImGui::Text("Note that all element names are just simple strings, even if they contain '/'."); - // Checkboxes in 'inspector' - bool tbuiShowDumpButton = false; + TerrainClass@ terrain = game.getTerrain(); + if (@terrain == null) + { + ImGui::TextDisabled("ERROR No terrain loaded"); + return; + } - // Counters - uint tbuiNumBatchesCreated = 0; - uint tbuiNumMeshesDumped = 0; + Ogre::Root@ root = Ogre::Root::getSingleton(); + if (@root == null) + { + ImGui::TextDisabled("ERROR Cannot retrieve OGRE `Root` object"); + return; + } - // Constants - string tbuiOutputsNodeName = "TerrnBatcher outputs"; - //#region Draw UI - main - void draw() + Ogre::SceneManager@ sceneMgr = findSceneManager(root, "main_scene_manager"); + if (@sceneMgr == null) { - - ImGui::Text("shows how you can traverse scene and merge meshes/textures together for better FPS"); - ImGui::Text("Note that all element names are just simple strings, even if they contain '/'."); - - TerrainClass@ terrain = game.getTerrain(); - if (@terrain == null) - { - ImGui::TextDisabled("ERROR No terrain loaded"); - return; - } - - Ogre::Root@ root = Ogre::Root::getSingleton(); - if (@root == null) - { - ImGui::TextDisabled("ERROR Cannot retrieve OGRE `Root` object"); - return; - } - - - Ogre::SceneManager@ sceneMgr = this.findSceneManager(root, "main_scene_manager"); - if (@sceneMgr == null) - { - ImGui::TextDisabled("ERROR Cannot retrieve scene manager"); - return; - } - - Ogre::SceneNode@ rootNode = sceneMgr.getRootSceneNode(); - if (@rootNode == null) - { - ImGui::TextDisabled("ERROR Cannot retrieve root scene node"); - return; - } - - Ogre::SceneNode@ terrnNode = this.findChildSceneNode(rootNode, "Terrain: " + terrain.getTerrainName()); - if (@terrnNode==null) - { - ImGui::TextDisabled("ERROR Cannot retrieve terrain's grouping scene node"); - return; - } - - Ogre::SceneNode@ outputsNode = this.findChildSceneNode(terrnNode, tbuiOutputsNodeName); - if (@outputsNode==null) - { - terrnNode.createChildSceneNode(tbuiOutputsNodeName); - } - - ImGui::Separator(); - ImGui::TextDisabled(" S E L E C T I O N :"); - this.drawSelectionBasket(terrnNode); - this.drawSelectionBatchControls(terrnNode); - - ImGui::Separator(); - ImGui::TextDisabled(" S C E N E G R A P H :"); - ImGui::Checkbox("Enable mesh dumping (write .mesh file to logs directory)", tbuiShowDumpButton); - this.drawTreeNodeOgreSceneNodeRecursive(terrnNode); + ImGui::TextDisabled("ERROR Cannot retrieve scene manager"); + return; } - //#endregion Draw UI - main - //#region Draw UI - schedule - void drawSelectionBatchControls(Ogre::SceneNode@ terrnNode) + Ogre::SceneNode@ rootNode = sceneMgr.getRootSceneNode(); + if (@rootNode == null) { - if (ImGui::Button(" > > > B A T C H < < < ")) - { - this.batchSelectedMeshes(terrnNode); - } - ImGui::SameLine(); - ImGui::Checkbox("hide origial", /*[inout]*/ tbuiHideOriginal); - ImGui::SameLine(); - ImGui::Checkbox("show new", /*[inout]*/ tbuiShowNew); + ImGui::TextDisabled("ERROR Cannot retrieve root scene node"); + return; + } + + Ogre::SceneNode@ terrnNode = findChildSceneNode(rootNode, "Terrain: " + terrain.getTerrainName()); + if (@terrnNode==null) + { + ImGui::TextDisabled("ERROR Cannot retrieve terrain's grouping scene node"); + return; + } + + Ogre::SceneNode@ outputsNode = findChildSceneNode(terrnNode, tbuiOutputsNodeName); + if (@outputsNode==null) + { + terrnNode.createChildSceneNode(tbuiOutputsNodeName); } - void drawSelectionBasket(Ogre::SceneNode@ terrnNode) + ImGui::Separator(); + ImGui::TextDisabled(" S E L E C T I O N :"); + drawSelectionBasket(terrnNode); + drawSelectionBatchControls(terrnNode); + + ImGui::Separator(); + ImGui::TextDisabled(" S C E N E G R A P H :"); + ImGui::Checkbox("Enable mesh dumping (write .mesh file to logs directory)", tbuiShowDumpButton); + drawTreeNodeOgreSceneNodeRecursive(terrnNode); +} +//#endregion Draw UI - main + +//#region Draw UI - schedule +void drawSelectionBatchControls(Ogre::SceneNode@ terrnNode) +{ + if (ImGui::Button(" > > > B A T C H < < < ")) { - array tobjNodes = tbuiSelection.getKeys(); + batchSelectedMeshes(terrnNode); + } + ImGui::SameLine(); + ImGui::Checkbox("hide origial", /*[inout]*/ tbuiHideOriginal); + ImGui::SameLine(); + ImGui::Checkbox("show new", /*[inout]*/ tbuiShowNew); +} + +void drawSelectionBasket(Ogre::SceneNode@ terrnNode) +{ + array tobjNodes = tbuiSelection.getKeys(); + + if (tobjNodes.length() == 0) + { + ImGui::Text("nothing selected. Use 'pick' buttons in tree below."); + return; + } + + // totals + dictionary materialSet; // material name => int numHits (-1 means not trivial - not supported) + +dictionary uniqueSubmeshSet; // dictionary { mesh name + ':' + submesh index => uint numVerts } +dictionary uniqueSubmeshSetHits; // dictionary { mesh name + ':' + submesh index => int numHits } + + + for (uint i=0; i "); - if (tobjNodes.length() == 0) + Ogre::SceneNode@ tobjNode = findChildSceneNode(terrnNode,tobjNodes[i]); + if (@tobjNode == null) { - ImGui::Text("nothing selected. Use 'pick' buttons in tree below."); - return; + ImGui::SameLine(); + ImGui::Text("ERROR - not found in scene graph!"); } - - // totals - dictionary materialSet; // material name => int numHits (-1 means not trivial - not supported) - - dictionary uniqueSubmeshSet; // dictionary { mesh name + ':' + submesh index => uint numVerts } - dictionary uniqueSubmeshSetHits; // dictionary { mesh name + ':' + submesh index => int numHits } - - - for (uint i=0; i "); - - Ogre::SceneNode@ tobjNode = this.findChildSceneNode(terrnNode,tobjNodes[i]); - if (@tobjNode == null) + ImGui::SameLine(); + array@ nodeIndices = cast>(tbuiSelection[tobjNodes[i]]); + if (@nodeIndices == null) { - ImGui::SameLine(); - ImGui::Text("ERROR - not found in scene graph!"); + ImGui::Text("ERROR cast failed"); } else { - ImGui::SameLine(); - array@ nodeIndices = cast>(tbuiSelection[tobjNodes[i]]); - if (@nodeIndices == null) + ImGui::Text(nodeIndices.length() + " nodes selected"); + + for (uint j=0; j materialKeys = materialSet.getKeys(); + TerrainClass@ terrain = game.getTerrain(); + for (uint i = 0; i materialKeys = materialSet.getKeys(); - TerrainClass@ terrain = game.getTerrain(); - for (uint i = 0; i " + tex.getName() + " ("+tex.getWidth()+" x " + tex.getHeight() + ") ["+matNumHits+" hits]"); - } - else - { - ImGui::SameLine(); - ImGui::Text("TBD: non trivial materials are not supported yet"); - } + ImGui::SameLine(); + ImGui::Text("ERROR-could not find material in MaterialManager"); } - - // now traverse set of unique submeshes - ImGui::TextDisabled("(by submesh):"); - array submeshKeys = uniqueSubmeshSet.getKeys(); - for (uint i = 0; i "+numVerts+" verts ["+uint(uniqueSubmeshSetHits[submeshKeys[i]])+" hits]"); + Ogre::TexturePtr tex = getTrivialCaseMaterialTexture(mat); + ImGui::SameLine(); + ImGui::Text(" --> " + tex.getName() + " ("+tex.getWidth()+" x " + tex.getHeight() + ") ["+matNumHits+" hits]"); + } + else + { + ImGui::SameLine(); + ImGui::Text("TBD: non trivial materials are not supported yet"); } } - // helper for `drawSelectionBasket()` - void basketProcessSingleInput(dictionary&inout materialSet, dictionary&inout subMeshSet, dictionary&inout subMeshSetHits, Ogre::SceneNode@ tobjNode, int childIndex) + // now traverse set of unique submeshes + ImGui::TextDisabled("(by submesh):"); + array submeshKeys = uniqueSubmeshSet.getKeys(); + for (uint i = 0; i@ nodeIndices = cast>(tbuiSelection[tobjNode.getName()]); - if (nodeIndices[childIndex] >= children.length()) - { - ImGui::Text("ERROR: '"+tobjNode.getName()+"' has only "+children.length()+"children, requested index"+childIndex); - return; - } - - // traverse Entities/subentities and collect materials. - Ogre::SceneNode@ pickedNode = cast(children[nodeIndices[childIndex]]); - Ogre::MovableObjectArray@ movables = pickedNode.getAttachedObjects(); - if (movables.length() == 1 && movables[0].getMovableType() == "ManualObject") - { - // Trivial case - single attached manual object (probably previous terrnbatcher result) - ImGui::Text("ManualObject="+movables[0].getName()); - } - else if (movables.length() == 1 && movables[0].getMovableType() == "Entity") + uint numVerts = uint(uniqueSubmeshSet[submeshKeys[i]]); + ImGui::BulletText(submeshKeys[i] + " --> "+numVerts+" verts ["+uint(uniqueSubmeshSetHits[submeshKeys[i]])+" hits]"); + } +} + +// helper for `drawSelectionBasket()` +void basketProcessSingleInput(dictionary&inout materialSet, dictionary&inout subMeshSet, dictionary&inout subMeshSetHits, Ogre::SceneNode@ tobjNode, int childIndex) +{ + Ogre::ChildNodeArray@ children = tobjNode.getChildren(); + array@ nodeIndices = cast>(tbuiSelection[tobjNode.getName()]); + if (nodeIndices[childIndex] >= children.length()) + { + ImGui::Text("ERROR: '"+tobjNode.getName()+"' has only "+children.length()+"children, requested index"+childIndex); + return; + } + + // traverse Entities/subentities and collect materials. + Ogre::SceneNode@ pickedNode = cast(children[nodeIndices[childIndex]]); + Ogre::MovableObjectArray@ movables = pickedNode.getAttachedObjects(); + if (movables.length() == 1 && movables[0].getMovableType() == "ManualObject") + { + // Trivial case - single attached manual object (probably previous terrnbatcher result) + ImGui::Text("ManualObject="+movables[0].getName()); + } + else if (movables.length() == 1 && movables[0].getMovableType() == "Entity") + { + // trivial case - single attached entity + Ogre::Entity@ ent = cast(movables[0]); + Ogre::SubEntityArray@ subEntities = ent.getSubEntities(); + for (uint k=0; k(movables[0]); - Ogre::SubEntityArray@ subEntities = ent.getSubEntities(); - for (uint k=0; k "+index); - return; - } - if (!tbuiSelection.exists(key)) - { - tbuiSelection[key] = array(); + game.log("DBG pickSceneNode(): this is already picked: "+key+" --> "+index); + return; + } + if (!tbuiSelection.exists(key)) + { + tbuiSelection[key] = array(); + } + array@ arr = cast>(tbuiSelection[key]); + arr.insertLast(index); + game.log("DBG picked "+key+" --> "+index); +} + +void unPickSceneNode(string key, uint index) +{ + if (!isSceneNodePicked(key, index)) + { + game.log("DBG unPickSceneNode(): this is not picked: "+key+" --> "+index); + return; + } + array@ arr = cast>(tbuiSelection[key]); + for (uint i=0; i "+index); + return; } - array@ arr = cast>(tbuiSelection[key]); - arr.insertLast(index); - game.log("DBG picked "+key+" --> "+index); + } +} + +bool isSceneNodePicked(string key, uint index) +{ +if (!tbuiSelection.exists(key)) { return false; } + array@ arr = cast>(tbuiSelection[key]); + for (uint i=0; i sceneMgrNames = sceneManagers.getKeys(); + for (uint i = 0; i < sceneManagers.getSize(); i++) { - if (!this.isSceneNodePicked(key, index)) - { - game.log("DBG unPickSceneNode(): this is not picked: "+key+" --> "+index); - return; - } - array@ arr = cast>(tbuiSelection[key]); - for (uint i=0; i "+index); - return; - } - } + return sceneManagers[sceneMgrNames[i]]; + } } - - bool isSceneNodePicked(string key, uint index) + return null; +} + +Ogre::SceneNode@ findChildSceneNode(Ogre::SceneNode@ snode, string subject) +{ + Ogre::ChildNodeArray@ children = snode.getChildren(); + for (uint i = 0; i < children.length(); i++) { - if (!tbuiSelection.exists(key)) { return false; } - array@ arr = cast>(tbuiSelection[key]); - for (uint i=0; i(children[i]); + if (@child != null && child.getName() == subject) { - if (arr[i]==index) { return true; } + return child; } - return false; } + return null; +} + +//#endregion Searching helpers + +//#region Inspector tree nodes +void drawTreeNodeOgreSceneNodeRecursive(Ogre::SceneNode@ snode, uint indexUnderParent=0) +{ + // Start with all nodes folded (root node can have hundreds...) + ImGui::SetNextItemOpen(false, ImGuiCond_Once); - // #endregion Pick helpers - - //#region Searching helpers + Ogre::ChildNodeArray@ children = snode.getChildren(); + Ogre::MovableObjectArray@ movables = snode.getAttachedObjects(); - Ogre::SceneManager@ findSceneManager(Ogre::Root@ root, string subject) + // The `__getUniqueName()` is a Rigs of Rods extension (that's why double leading underscores), + // because names are optional and usually not set, and imgui tree nodes require unique IDs. + ImGui::PushID(snode.__getUniqueName()); + string treeNodeCaption = snode.getName(); + if (treeNodeCaption == "") { - Ogre::SceneManagerInstanceDict@ sceneManagers = root.getSceneManagers(); - - array sceneMgrNames = sceneManagers.getKeys(); - for (uint i = 0; i < sceneManagers.getSize(); i++) - { - if (sceneMgrNames[i] == subject) - { - return sceneManagers[sceneMgrNames[i]]; - } - } - return null; + treeNodeCaption = "["+(indexUnderParent+1)+"]"; } - Ogre::SceneNode@ findChildSceneNode(Ogre::SceneNode@ snode, string subject) + if (ImGui::TreeNode(treeNodeCaption)) { - Ogre::ChildNodeArray@ children = snode.getChildren(); + // Tree node open, draw children recursively + ImGui::TextDisabled("Ogre::Node ["+children.length()+"]"); for (uint i = 0; i < children.length(); i++) { Ogre::SceneNode@ child = cast(children[i]); - if (@child != null && child.getName() == subject) + if (@child != null) { - return child; + drawTreeNodeOgreSceneNodeRecursive(child, i); } } - return null; + + // Draw attached movable objects + ImGui::TextDisabled("Ogre::MovableObject [" + movables.length() + "]"); + for (uint i = 0; i < movables.length(); i++) + { + drawTreeNodeOgreMovableObject(movables[i]); + } + + ImGui::TreePop(); } - - //#endregion Searching helpers - - //#region Inspector tree nodes - void drawTreeNodeOgreSceneNodeRecursive(Ogre::SceneNode@ snode, uint indexUnderParent=0) + else { - // Start with all nodes folded (root node can have hundreds...) - ImGui::SetNextItemOpen(false, ImGuiCond_Once); - Ogre::ChildNodeArray@ children = snode.getChildren(); - Ogre::MovableObjectArray@ movables = snode.getAttachedObjects(); - // The `__getUniqueName()` is a Rigs of Rods extension (that's why double leading underscores), - // because names are optional and usually not set, and imgui tree nodes require unique IDs. - ImGui::PushID(snode.__getUniqueName()); - string treeNodeCaption = snode.getName(); - if (treeNodeCaption == "") + // Tree node closed, draw info (context-sensitive) + ImGui::SameLine(); + if (children.length() == 0 && movables.length() == 1) + { + drawInlineSceneNodeControlsToInspector(snode, indexUnderParent); + } + else { - treeNodeCaption = "["+(indexUnderParent+1)+"]"; + ImGui::Text("("+children.length()+" children, "+movables.length()+" movables)"); } - if (ImGui::TreeNode(treeNodeCaption)) + + } + ImGui::PopID(); //snode.__getUniqueName() +} + +void drawTreeNodeOgreMovableObject(Ogre::MovableObject@ movable) +{ + if (ImGui::TreeNode(movable.__getUniqueName())) + { + bool visible = movable.isVisible(); + if (ImGui::Checkbox("Visible", visible)) + { + movable.setVisible(visible); + } + + bool castShadows = movable.getCastShadows(); + if (ImGui::Checkbox("Cast shadows", castShadows)) + { + movable.setCastShadows(castShadows); + } + + if (movable.getMovableType() == "Entity") { - // Tree node open, draw children recursively - ImGui::TextDisabled("Ogre::Node ["+children.length()+"]"); - for (uint i = 0; i < children.length(); i++) + Ogre::Entity@ entity = cast(movable); + Ogre::AnimationStateSet@ stateSet = entity.getAllAnimationStates(); + if (@stateSet != null) { - Ogre::SceneNode@ child = cast(children[i]); - if (@child != null) + Ogre::AnimationStateDict@ stateDict = stateSet.getAnimationStates(); + ImGui::TextDisabled("Ogre::AnimationState [" + stateDict.getSize() + "]"); + array stateNames = stateDict.getKeys(); + for (uint i = 0; i < stateDict.getSize(); i++) { - drawTreeNodeOgreSceneNodeRecursive(child, i); + drawTreeNodeOgreAnimationState(stateDict[stateNames[i]]); } } - // Draw attached movable objects - ImGui::TextDisabled("Ogre::MovableObject [" + movables.length() + "]"); - for (uint i = 0; i < movables.length(); i++) + // +TerrnBatcher - subEntities + Ogre::SubEntityArray@ subEntities = entity.getSubEntities(); + ImGui::TextDisabled("Ogre::SubEntity [" + subEntities.length() + "]"); + for (uint i = 0; i < subEntities.length(); i++) { - drawTreeNodeOgreMovableObject(movables[i]); + drawTreeNodeOgreSubEntity(subEntities, i); } - - ImGui::TreePop(); - } - else - { - - - // Tree node closed, draw info (context-sensitive) - ImGui::SameLine(); - if (children.length() == 0 && movables.length() == 1) - { - this. drawInlineSceneNodeControlsToInspector(snode, indexUnderParent); - } - else - { - ImGui::Text("("+children.length()+" children, "+movables.length()+" movables)"); - } - - + // END +TerrnBatcher } - ImGui::PopID(); //snode.__getUniqueName() + + ImGui::TreePop(); + } +} + +void drawTreeNodeOgreAnimationState(Ogre::AnimationState@ anim) +{ + ImGui::BulletText('"' + anim.getAnimationName() + '"'); + ImGui::SameLine(); + ImGui::Text("(" + formatFloat(anim.getLength(), "", 0, 2) + " sec)"); + if (anim.getEnabled()) + { + ImGui::SameLine(); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, vector2(0.f, 0.f)); + ImGui::ProgressBar(anim.getTimePosition() / anim.getLength()); + ImGui::PopStyleVar(1); // ImGuiStyleVar_FramePadding + } +} +//#endregion Inspector tree nodes + +// #region TerrnBatcher controls for inspector + +void drawInlineSceneNodeControlsToInspector(Ogre::SceneNode@ snode, uint indexUnderParent) +{ + //the most usual case - a node with a single entity. Draw name and controls inline + // ------------------------------------------------ + Ogre::MovableObjectArray@ movables = snode.getAttachedObjects(); + ImGui::TextDisabled("-->"); + ImGui::SameLine(); + ImGui::Text(movables[0].getName()); + ImGui::SameLine(); + + // Inspector: go to + if (ImGui::SmallButton("go to")) + { + game.setPersonPosition(snode.getPosition()); + } + + // +TerrnBatcher - visibility + ImGui::SameLine(); + bool visible = movables[0].isVisible(); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, vector2(2,0)); + if (ImGui::Checkbox("Visible", visible)) + { + movables[0].setVisible(visible); } + ImGui::PopStyleVar(); //ImGuiStyleVar_FramePadding - void drawTreeNodeOgreMovableObject(Ogre::MovableObject@ movable) + // +terrnBatcher - selection + ImGui::SameLine(); + if (isSceneNodePicked(snode.getParentSceneNode().getName(), indexUnderParent)) { - if (ImGui::TreeNode(movable.__getUniqueName())) + if(ImGui::SmallButton("-UnPick")) { - bool visible = movable.isVisible(); - if (ImGui::Checkbox("Visible", visible)) - { - movable.setVisible(visible); - } - - bool castShadows = movable.getCastShadows(); - if (ImGui::Checkbox("Cast shadows", castShadows)) - { - movable.setCastShadows(castShadows); - } - - if (movable.getMovableType() == "Entity") - { - Ogre::Entity@ entity = cast(movable); - Ogre::AnimationStateSet@ stateSet = entity.getAllAnimationStates(); - if (@stateSet != null) - { - Ogre::AnimationStateDict@ stateDict = stateSet.getAnimationStates(); - ImGui::TextDisabled("Ogre::AnimationState [" + stateDict.getSize() + "]"); - array stateNames = stateDict.getKeys(); - for (uint i = 0; i < stateDict.getSize(); i++) - { - this.drawTreeNodeOgreAnimationState(stateDict[stateNames[i]]); - } - } - - // +TerrnBatcher - subEntities - Ogre::SubEntityArray@ subEntities = entity.getSubEntities(); - ImGui::TextDisabled("Ogre::SubEntity [" + subEntities.length() + "]"); - for (uint i = 0; i < subEntities.length(); i++) - { - this.drawTreeNodeOgreSubEntity(subEntities, i); - } - // END +TerrnBatcher - } - - ImGui::TreePop(); + unPickSceneNode(snode.getParentSceneNode().getName(), indexUnderParent); } } - - void drawTreeNodeOgreAnimationState(Ogre::AnimationState@ anim) + else { - ImGui::BulletText('"' + anim.getAnimationName() + '"'); - ImGui::SameLine(); - ImGui::Text("(" + formatFloat(anim.getLength(), "", 0, 2) + " sec)"); - if (anim.getEnabled()) + if (ImGui::SmallButton("+Pick!")) { - ImGui::SameLine(); - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, vector2(0.f, 0.f)); - ImGui::ProgressBar(anim.getTimePosition() / anim.getLength()); - ImGui::PopStyleVar(1); // ImGuiStyleVar_FramePadding + + pickSceneNode(snode.getParentSceneNode().getName(), indexUnderParent); } } - //#endregion Inspector tree nodes - - // #region TerrnBatcher controls for inspector - void drawInlineSceneNodeControlsToInspector(Ogre::SceneNode@ snode, uint indexUnderParent) + // +TerrnBatcher - mesh serialization + if (tbuiShowDumpButton) { - //the most usual case - a node with a single entity. Draw name and controls inline - // ------------------------------------------------ - Ogre::MovableObjectArray@ movables = snode.getAttachedObjects(); - ImGui::TextDisabled("-->"); ImGui::SameLine(); - ImGui::Text(movables[0].getName()); - ImGui::SameLine(); - - // Inspector: go to - if (ImGui::SmallButton("go to")) - { - game.setPersonPosition(snode.getPosition()); - } - - // +TerrnBatcher - visibility - ImGui::SameLine(); - bool visible = movables[0].isVisible(); - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, vector2(2,0)); - if (ImGui::Checkbox("Visible", visible)) - { - movables[0].setVisible(visible); - } - ImGui::PopStyleVar(); //ImGuiStyleVar_FramePadding - - // +terrnBatcher - selection - ImGui::SameLine(); - if (this.isSceneNodePicked(snode.getParentSceneNode().getName(), indexUnderParent)) - { - if(ImGui::SmallButton("-UnPick")) - { - this.unPickSceneNode(snode.getParentSceneNode().getName(), indexUnderParent); - } - } - else - { - if (ImGui::SmallButton("+Pick!")) - { - - this.pickSceneNode(snode.getParentSceneNode().getName(), indexUnderParent); - } - } - - // +TerrnBatcher - mesh serialization - if (tbuiShowDumpButton) + if (ImGui::SmallButton("Dump")) { - ImGui::SameLine(); - if (ImGui::SmallButton("Dump")) + if (dumpMesh(movables[0])) { - if (this.dumpMesh(movables[0])) - { - game.log("TerrnBatcher: Mesh dumped successfully"); - } + game.log("TerrnBatcher: Mesh dumped successfully"); } } } - - bool dumpMesh(Ogre::MovableObject@ movable) +} + +bool dumpMesh(Ogre::MovableObject@ movable) +{ + // Save to 'logs' directory - Make sure filenames dont clash + // ---------------------------------------------------------- + string fileName = "TerrnBatcher(NID" + thisScript + ")_Dump" + (tbuiNumMeshesDumped++) + "_"; + Ogre::MeshPtr mesh; + if (movable.getMovableType() == "Entity") { - // Save to 'logs' directory - Make sure filenames dont clash - // ---------------------------------------------------------- - string fileName = "TerrnBatcher(NID" + thisScript + ")_Dump" + (tbuiNumMeshesDumped++) + "_"; - Ogre::MeshPtr mesh; - if (movable.getMovableType() == "Entity") - { - Ogre::Entity@ ent = cast(movable); - mesh = ent.getMesh(); - fileName += mesh.getName(); - } - else if (movable.getMovableType() == "ManualObject") - { - Ogre::ManualObject@ mo = cast(movable); - mesh = mo.convertToMesh(fileName, "Logs"); - fileName += "batch.mesh"; - } - else - { - game.log("ERROR dumpMesh(): unrecognized MovableObject type '" + movable.getMovableType() + "'"); - return false; - } - - return game.serializeMeshResource(fileName, "Logs", mesh); + Ogre::Entity@ ent = cast(movable); + mesh = ent.getMesh(); + fileName += mesh.getName(); } - - // #endregion TerrnBatcher controls for inspector - - // #region Material helpers - - Ogre::TexturePtr getTrivialCaseMaterialTexture(Ogre::MaterialPtr mat) + else if (movable.getMovableType() == "ManualObject") { - if (mat.isNull()) return Ogre::TexturePtr(); // null - Ogre::TechniqueArray@ techs = mat.getTechniques(); - if (techs.length() != 1) return Ogre::TexturePtr(); // null - Ogre::PassArray@ passes = techs[0].getPasses(); - if (passes.length() != 1) return Ogre::TexturePtr(); // null - Ogre::TextureUnitStateArray@ tuss = passes[0].getTextureUnitStates(); - if (tuss.length() != 1) return Ogre::TexturePtr(); // null - return tuss[0]._getTexturePtr(); + Ogre::ManualObject@ mo = cast(movable); + mesh = mo.convertToMesh(fileName, "Logs"); + fileName += "batch.mesh"; } - - bool isMaterialTrivialCase(Ogre::MaterialPtr mat) + else { - return !this.getTrivialCaseMaterialTexture(mat).isNull(); + game.log("ERROR dumpMesh(): unrecognized MovableObject type '" + movable.getMovableType() + "'"); + return false; } - // #endregion Material helpers + return game.serializeMeshResource(fileName, "Logs", mesh); +} + +// #endregion TerrnBatcher controls for inspector + +// #region Material helpers + +Ogre::TexturePtr getTrivialCaseMaterialTexture(Ogre::MaterialPtr mat) +{ + if (mat.isNull()) return Ogre::TexturePtr(); // null + Ogre::TechniqueArray@ techs = mat.getTechniques(); + if (techs.length() != 1) return Ogre::TexturePtr(); // null + Ogre::PassArray@ passes = techs[0].getPasses(); + if (passes.length() != 1) return Ogre::TexturePtr(); // null + Ogre::TextureUnitStateArray@ tuss = passes[0].getTextureUnitStates(); + if (tuss.length() != 1) return Ogre::TexturePtr(); // null + return tuss[0]._getTexturePtr(); +} + +bool isMaterialTrivialCase(Ogre::MaterialPtr mat) +{ + return !getTrivialCaseMaterialTexture(mat).isNull(); +} + +// #endregion Material helpers + +void drawTreeNodeOgreSubEntity(Ogre::SubEntityArray@ subEntities, uint idx) +{ + if (subEntities.length() <= idx) return; // sanity check - void drawTreeNodeOgreSubEntity(Ogre::SubEntityArray@ subEntities, uint idx) + Ogre::SubEntity@ subEntity = subEntities[idx]; + Ogre::MaterialPtr mat = subEntity.getMaterial(); + if (!mat.isNull()) { - if (subEntities.length() <= idx) return; // sanity check - - Ogre::SubEntity@ subEntity = subEntities[idx]; - Ogre::MaterialPtr mat = subEntity.getMaterial(); - if (!mat.isNull()) + Ogre::TechniqueArray@ techs = mat.getTechniques(); + ImGui::Text("|> Material: \"" + mat.getName() + "\" ("+techs.length()+" techniques)"); + for (uint i = 0; i < techs.length(); i++) { - Ogre::TechniqueArray@ techs = mat.getTechniques(); - ImGui::Text("|> Material: \"" + mat.getName() + "\" ("+techs.length()+" techniques)"); - for (uint i = 0; i < techs.length(); i++) + Ogre::PassArray@ passes = techs[i].getPasses(); + ImGui::Text("|> > Technique: \"" + techs[i].getName()+"\" ("+passes.length()+" passes)"); + for (uint j=0; j < passes.length(); j++) { - Ogre::PassArray@ passes = techs[i].getPasses(); - ImGui::Text("|> > Technique: \"" + techs[i].getName()+"\" ("+passes.length()+" passes)"); - for (uint j=0; j < passes.length(); j++) + Ogre::TextureUnitStateArray@ tuss = passes[j].getTextureUnitStates(); + ImGui::Text("|> > > Pass: \"" + passes[j].getName() + "\" (" + tuss.length() + " texture units)"); + for (uint k=0; k > > Pass: \"" + passes[j].getName() + "\" (" + tuss.length() + " texture units)"); - for (uint k=0; k > > > Texture unit: \"" + tuss[k].getName() + "\""); + Ogre::TexturePtr tex = tuss[k]._getTexturePtr(); + if (!tex.isNull()) { - ImGui::Text("|> > > > Texture unit: \"" + tuss[k].getName() + "\""); - Ogre::TexturePtr tex = tuss[k]._getTexturePtr(); - if (!tex.isNull()) - { - ImGui::SameLine(); - ImGui::Text("Texture=\"" + tex.getName()+"\""); - } + ImGui::SameLine(); + ImGui::Text("Texture=\"" + tex.getName()+"\""); } } } } - else + } + else + { + ImGui::Text("no material"); + } +} + +// #region The actual magic - merging meshes +void addSingleSubMeshToBatch(Ogre::ManualObject@ mo, Ogre::SubMesh@ subMesh, vector3 pos, quaternion rot, vector3 scale) +{ + // Proof of concept: make it a world-space mesh. Assume tri-list type mesh with 1 UV (=texcoords) layer + // ------------------------------------------------------------------------ + + + uint indexBase = mo.getCurrentVertexCount(); + + array vertPos = subMesh.__getVertexPositions(); + array vertUVs = subMesh.__getVertexTexcoords(0); + for (uint iVert = 0; iVert < vertPos.length(); iVert++) + { + mo.position((rot * vertPos[iVert]) * scale + pos); + mo.textureCoord(vertUVs[iVert]); + } + + if (subMesh.__getIndexType() == Ogre::IndexType::IT_16BIT) + { + array indexBuf = subMesh.__getIndexBuffer16bit(); + for (uint iIndex = 0; iIndex < indexBuf.length(); iIndex++) { - ImGui::Text("no material"); + mo.index(indexBase + indexBuf[iIndex]); } } - - // #region The actual magic - merging meshes - void addSingleSubMeshToBatch(Ogre::ManualObject@ mo, Ogre::SubMesh@ subMesh, vector3 pos, quaternion rot, vector3 scale) + else { - // Proof of concept: make it a world-space mesh. Assume tri-list type mesh with 1 UV (=texcoords) layer - // ------------------------------------------------------------------------ - - - uint indexBase = mo.getCurrentVertexCount(); - - array vertPos = subMesh.__getVertexPositions(); - array vertUVs = subMesh.__getVertexTexcoords(0); - for (uint iVert = 0; iVert < vertPos.length(); iVert++) + game.log("ERROR appendMeshInstanceToManualObject(): mesh is not supported - not 16-bit indexed"); + } +} + +void addSceneNodeAttachedMeshesToBatch(Ogre::ManualObject@ mo, Ogre::SceneNode@ pickedNode, string&inout foundMatName) +{ + Ogre::MovableObjectArray@ movables = pickedNode.getAttachedObjects(); + for (uint iMovas = 0; iMovas < movables.length(); iMovas++) + { + //game.log("DBG addSceneNodeAttachedMeshesToBatch(): iMovas="+iMovas+"/"+movables.length()); + if (movables[iMovas].getMovableType() != "Entity") { - mo.position((rot * vertPos[iVert]) * scale + pos); - mo.textureCoord(vertUVs[iVert]); + game.log("DBG batchSelectedMeshes(): skipping movable of type '"+movables[iMovas].getMovableType()+"' - not suported!"); + continue; } + Ogre::Entity @ent = cast(movables[iMovas]); - if (subMesh.__getIndexType() == Ogre::IndexType::IT_16BIT) + Ogre::SubEntityArray@ subEntities = ent.getSubEntities(); + for (uint iSubent=0; iSubent indexBuf = subMesh.__getIndexBuffer16bit(); - for (uint iIndex = 0; iIndex < indexBuf.length(); iIndex++) + //game.log("DBG addSceneNodeAttachedMeshesToBatch(): iSubent="+iSubent+"/"+subEntities.length()); + Ogre::MaterialPtr mat = subEntities[iSubent].getMaterial(); + if (mat.isNull()) { - mo.index(indexBase + indexBuf[iIndex]); + game.log("DBG batchSelectedMeshes(): skipping subEntity - material is NULL"); + continue; } - } - else - { - game.log("ERROR appendMeshInstanceToManualObject(): mesh is not supported - not 16-bit indexed"); - } - } - - void addSceneNodeAttachedMeshesToBatch(Ogre::ManualObject@ mo, Ogre::SceneNode@ pickedNode, string&inout foundMatName) - { - Ogre::MovableObjectArray@ movables = pickedNode.getAttachedObjects(); - for (uint iMovas = 0; iMovas < movables.length(); iMovas++) - { - //game.log("DBG addSceneNodeAttachedMeshesToBatch(): iMovas="+iMovas+"/"+movables.length()); - if (movables[iMovas].getMovableType() != "Entity") + if (foundMatName == "") { - game.log("DBG batchSelectedMeshes(): skipping movable of type '"+movables[iMovas].getMovableType()+"' - not suported!"); - continue; + foundMatName = mat.getName(); + mo.begin(foundMatName, Ogre::OT_TRIANGLE_LIST, game.getTerrain().getHandle().getTerrainFileResourceGroup()); + game.log ("DBG addSceneNodeAttachedMeshesToBatch(): Manual object initialized with material '"+foundMatName+"'"); } - Ogre::Entity @ent = cast(movables[iMovas]); - - Ogre::SubEntityArray@ subEntities = ent.getSubEntities(); - for (uint iSubent=0; iSubent tobjNodes = tbuiSelection.getKeys(); + if (tobjNodes.length() == 0) + { + game.log("ERROR batchSelectedMeshes(): nothing selected. Use 'pick' buttons in tree below."); + return; + } + + game.log("DBG batchSelectedMeshes(): num selected nodes='"+tobjNodes.length()+"'"); + for (uint iNode=0; iNode@ nodeIndices = cast>(tbuiSelection[tobjNodes[iNode]]); - array tobjNodes = tbuiSelection.getKeys(); - if (tobjNodes.length() == 0) - { - game.log("ERROR batchSelectedMeshes(): nothing selected. Use 'pick' buttons in tree below."); - return; - } + //game.log("DBG batchSelectedMeshes(): processing node "+uint(iNode+1)+"/"+tobjNodes.length()+" ("+tobjNodes[iNode]+") - "+nodeIndices.length()+" nodes picked"); - game.log("DBG batchSelectedMeshes(): num selected nodes='"+tobjNodes.length()+"'"); - for (uint iNode=0; iNode= children.length()) { - game.log("DBG batchSelectedMeshes(): '"+tobjNodes[iNode]+"' - not found in scene graph!"); + game.log("ERROR: '"+tobjNode.getName()+"' has only "+children.length()+"children, requested index"+nodeIndices[iChild]); continue; - } + } - // Loop through list of scene nodes selected under this grouping node - array@ nodeIndices = cast>(tbuiSelection[tobjNodes[iNode]]); + // Process the entities attached to this scene node + Ogre::SceneNode@ childNode = cast(children[nodeIndices[iChild]]); - //game.log("DBG batchSelectedMeshes(): processing node "+uint(iNode+1)+"/"+tobjNodes.length()+" ("+tobjNodes[iNode]+") - "+nodeIndices.length()+" nodes picked"); - - for (uint iChild=0; iChild(children[nodeIndices[iChild]]); - - //game.log("DBG batchSelectedMeshes(): iChild="+iChild+"/"+nodeIndices.length()+", uniqueName()="+childNode.__getUniqueName()); - this.addSceneNodeAttachedMeshesToBatch(mo, childNode, /*[inout]*/foundMatName); - if (tbuiHideOriginal) - { - childNode.setVisible(false); - } - } // END loop (list of picked nodes) - } // END loop (grouping nodes) + childNode.setVisible(false); + } + } // END loop (list of picked nodes) + } // END loop (grouping nodes) + + if (foundMatName != "") + { + mo.end(); - if (foundMatName != "") - { - mo.end(); - - Ogre::SceneNode@ outputsNode = this.findChildSceneNode(terrnNode, tbuiOutputsNodeName); - Ogre::SceneNode@ snode = outputsNode.createChildSceneNode(moName+"-node"); - snode.attachObject(cast(mo)); - snode.setVisible(tbuiShowNew); - // unlike the ManualObject example, we don't position the node - verts are already in world position. - } - else - { - game.log("DBG batchSelectedMeshes(): material not found, mesh not generated"); - } + Ogre::SceneNode@ outputsNode = findChildSceneNode(terrnNode, tbuiOutputsNodeName); + Ogre::SceneNode@ snode = outputsNode.createChildSceneNode(moName+"-node"); + snode.attachObject(cast(mo)); + snode.setVisible(tbuiShowNew); + // unlike the ManualObject example, we don't position the node - verts are already in world position. } - - // #endregion The actual magic - merging meshes - - // #region Generic helpers - string composeUniqueId(string name) + else { - // to avoid clash with leftover scene nodes created before, we include the NID in the name - using automatic global var `thisScript`. - return "TerrnBatcher(NID:"+thisScript+"): "+name; + game.log("DBG batchSelectedMeshes(): material not found, mesh not generated"); } - // #endregion Generic helpers } +// #endregion The actual magic - merging meshes + +// #region Generic helpers +string composeUniqueId(string name) +{ + // to avoid clash with leftover scene nodes created before, we include the NID in the name - using automatic global var `thisScript`. + return "TerrnBatcher(NID:"+thisScript+"): "+name; +} +// #endregion Generic helpers + + diff --git a/resources/scripts/example_terrn_odefBrowser.as b/resources/scripts/example_terrn_odefBrowser.as new file mode 100644 index 0000000000..7189932a96 --- /dev/null +++ b/resources/scripts/example_terrn_odefBrowser.as @@ -0,0 +1,691 @@ +// TERRAIN OBJECT (*.odef) BROWSER prototype +// Lists .odef files from 'resources/meshes.zip' and sorts/displays/spawns/deletes them. +// =================================================== + +// Window [X] button handler +#include "imgui_utils.as" +imgui_utils::CloseWindowPrompt closeBtnHandler; + +const float FLT_MIN = 1.17549435e-38F; + + +//#region C O N F I G +int cfgGizmoNumSeg=10; // num segments for drawing circles on gizmos +color cfgGrabGizmoColBg = color(0,0,0,1); +color cfgGrabGizmoColFg = color(1,1,1,1); +float cfgGizmoScale = 1.f; +color cfgGizmoUiHoverColor = color(1,0.7,0.3,1); +color cfgGizmoSceneHoverColor = color(1,1,0,1); +int cfgGizmoHoverDistMax = 10; // screen-space distance in pixels +float cfgAxisGizmoPad = 0.7f; // world - meters +float cfgAxisGizmoLen = 1.2f; // world - meters +color cfgAxisGizmoColBg = color(0,0,0,1); +array cfgAxisGizmoColFg = { color(1,0,0,1), color(0,1,0,1), color(0,0,1,1) }; +float cfgHeightBiasRail = 0.4f; +float cfgHeightBiasMisc = 0.f; +color cfgGizmoGroundMarkerColBg = color(0,0,0,1); +color cfgGizmoGroundMarkerColFg = color(0,1,1,1); +color cfgObjSelectionHighlightCol = color(1,0,1,1); + +void drawConfigPanel() +{ + ImGui::SliderFloat("gizmo scale", cfgGizmoScale, 0.1f, 10.f); + ImGui::ColorEdit3("grab gizmo background", cfgGrabGizmoColBg); + ImGui::ColorEdit3("grab gizmo foreground", cfgGrabGizmoColFg); + ImGui::ColorEdit3("gizmo ui hover", cfgGizmoUiHoverColor); + ImGui::ColorEdit3("gizmo scene hover", cfgGizmoSceneHoverColor); + ImGui::SliderFloat("axis gizmo pad", cfgAxisGizmoPad, 0.1f, 5.f); + ImGui::SliderFloat("axis gizmo len", cfgAxisGizmoLen, 0.1f, 5.f); + ImGui::Separator(); + ImGui::InputFloat("height bias Rail", cfgHeightBiasRail); + ImGui::InputFloat("height bias misc.", cfgHeightBiasMisc); +} +//#endregion + + +//#region .ODEFs L I S T I N G +array@ odefs = null; // fileinfo +string odefsError = ""; +array odefnamesAirport; +array odefnamesAirportTaxiway; +array odefnamesAirportRunway; +array odefnamesRail; +array odefnamesRailPlatform; +array odefnamesRoad; +array odefnamesRoadSign; +array odefnamesOther; + +void refreshODEFs() +{ + //FIXME: should reset all `odefnames*` arrays here + @odefs = game.findResourceFileInfo("MeshesRG", "*.odef"); + if (@odefs == null) + { + odefsError="Could not load ODEFs, check RoR.log"; + } + else + { + sortOdefs(); + } +} + +void sortOdefs() +{ + for (uint iOdef=0; iOdef < odefs.length(); iOdef++) + { + string filename = string(odefs[iOdef]["basename"]); + string odefname = filename.substr(0, filename.length() - 5); //cut off ".odef" suffix + + // order matters! (i.e. 'sign-roadnarrows' must hit 'sign-', not 'road') + if (odefname.findFirst("taxiway") >=0 || odefname.findFirst("taxient") >=0) + { + odefnamesAirportTaxiway.insertLast(odefname); + } + else if (odefname.findFirst("heading") >=0 || odefname.findFirst("runway")>=0) + { + odefnamesAirportRunway.insertLast(odefname); + } + else if (odefname.findFirst("airport") >=0 || odefname.findFirst("localizer") >= 0 ) + { + odefnamesAirport.insertLast(odefname); + } + else if (odefname.findFirst("railplatform")>=0) + { + odefnamesRailPlatform.insertLast(odefname); + } + else if (odefname.findFirst("rail")>=0) + { + odefnamesRail.insertLast(odefname); + } + else if (odefname.findFirst("sign-")>=0) + { + odefnamesRoadSign.insertLast(odefname); + } + else if (odefname.findFirst("road")>=0) + { + odefnamesRoad.insertLast(odefname); + } + else + { + odefnamesOther.insertLast(odefname); + } + } +} + +void drawOdefTree() +{ + if (ImGui::TreeNode("Airport ("+odefnamesAirport.length()+")")) + { + drawOdefNames(odefnamesAirport, cfgHeightBiasMisc); + ImGui::TreePop(); + } + if (ImGui::TreeNode("Airport Taxiway ("+odefnamesAirportTaxiway.length()+")")) + { + drawOdefNames(odefnamesAirportTaxiway, cfgHeightBiasMisc); + ImGui::TreePop(); + } + if (ImGui::TreeNode("Airport Runway ("+odefnamesAirportRunway.length()+")")) + { + drawOdefNames(odefnamesAirportRunway, cfgHeightBiasMisc); + ImGui::TreePop(); + } + if (ImGui::TreeNode("Rail ("+odefnamesRail.length()+")")) + { + drawOdefNames(odefnamesRail, cfgHeightBiasRail); + ImGui::TreePop(); + } + if (ImGui::TreeNode("Rail Platform ("+odefnamesRailPlatform.length()+")")) + { + drawOdefNames(odefnamesRailPlatform, cfgHeightBiasMisc); + ImGui::TreePop(); + } + if (ImGui::TreeNode("Road ("+odefnamesRoad.length()+")")) + { + drawOdefNames(odefnamesRoad, cfgHeightBiasMisc); + ImGui::TreePop(); + } + if (ImGui::TreeNode("Road Sign ("+odefnamesRoadSign.length()+")")) + { + drawOdefNames(odefnamesRoadSign, cfgHeightBiasMisc); + ImGui::TreePop(); + } + if (ImGui::TreeNode("Other ("+odefnamesOther.length()+")")) + { + drawOdefNames(odefnamesOther, cfgHeightBiasMisc); + ImGui::TreePop(); + } +} + +void drawOdefNames(array@ arr, float hBias) +{ + // helper for `drawOdefTree()` - draws odef names and spawn button + // -------------------- + for (uint i = 0; iObjInstanceInfo +uint spawnedObjSequentialNum=0; +string manualSpawnPanelOdefName = ""; + +string composeUniqueInstanceName(string odefname) +{ + // To avoid conflict with leftover objects created before, we include the "script unit ID" (aka NID) to the instance name. + // Format: s#c#n# (S=sequential number, C=instance count, N=NID) + // ------------------------------- + return odefname +"~s"+(spawnedObjSequentialNum++)+ "c" + objInstances.getKeys().length() + "n"+thisScript; +} + +void spawnObjectAtRorbot(string odefname, float hBias) +{ + vector3 objectRot = vector3(0,-game.getPersonRotation().valueDegrees(),0); + vector3 objectPos = game.getPersonPosition() + vector3(0,hBias,0); + spawnObjectManually(odefname, objectPos, objectRot); +} + +void spawnObjectManually(string odefname, vector3 objectPos, vector3 objectRot) +{ + string instanceName = composeUniqueInstanceName(odefname); + game.spawnObject(odefname, instanceName, objectPos, objectRot, "", false); + ObjInstanceInfo@ instInfo = ObjInstanceInfo (instanceName, odefname, objectPos, objectRot); + @objInstances[instanceName] = @instInfo; +} + +void drawSpawnedObjectsPanel() +{ + ImGui::PushID("spawnedObjs"); + array@ spawnedObjectInstanceIds = objInstances.getKeys(); + + ImGui::TextDisabled("spawned objects ("+spawnedObjectInstanceIds.length()+"): "); + for (uint iInst = 0; iInst < spawnedObjectInstanceIds.length(); iInst++) + { + ImGui::PushID(iInst); + ObjInstanceInfo@ instInfo = cast(objInstances[spawnedObjectInstanceIds[iInst]]); + + //selection checkbox + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, vector2(2,0)); + ImGui::Checkbox("Sel", instInfo.inSelection); + ImGui::PopStyleVar(); + ImGui::SameLine(); + + // instance name + if (instInfo.uiHovered) ImGui::PushStyleColor(ImGuiCol_Text, cfgGizmoUiHoverColor); + if (instInfo.sceneHovered && instInfo.sceneHoveredAxis == -1) ImGui::PushStyleColor(ImGuiCol_Text, cfgGizmoGroundMarkerColFg); + if (instInfo.sceneHovered && instInfo.sceneHoveredAxis == 0) ImGui::PushStyleColor(ImGuiCol_Text, cfgAxisGizmoColFg[0]); + if (instInfo.sceneHovered && instInfo.sceneHoveredAxis == 1) ImGui::PushStyleColor(ImGuiCol_Text, cfgAxisGizmoColFg[1]); + if (instInfo.sceneHovered && instInfo.sceneHoveredAxis == 2) ImGui::PushStyleColor(ImGuiCol_Text, cfgAxisGizmoColFg[2]); + if (instInfo.inSelection) ImGui::PushStyleColor(ImGuiCol_Text, cfgObjSelectionHighlightCol); + ImGui::BulletText(instInfo.name); + if (instInfo.uiHovered) ImGui::PopStyleColor(1); //cfgGizmoUiHoverColor + if (instInfo.sceneHovered) ImGui::PopStyleColor(1); // cfgGizmoSceneHoverColor || cfgAxisGizmoColF + if (instInfo.inSelection) ImGui::PopStyleColor(1); // cfgObjSelectionHighlightCol + + bool bulletHovered = ImGui::IsItemHovered(); + ImGui::SameLine(); + if (ImGui::SmallButton("Destroy!")) + { + game.destroyObject(instInfo.name); + objInstances.delete(instInfo.name); + } + bool dBtnHovered = ImGui::IsItemHovered(); + + instInfo.uiHovered = bulletHovered || dBtnHovered; + + ImGui::PopID(); //iInst + } + ImGui::PopID(); // spawnedObjs +} +//#endregion + + + +//#region G I Z M O S +int gizmoClosestDist = INT_MAX; +string gizmoClosestObj = ""; +int gizmoClosestObjAxis = -1; // -1=none, 0=x, 1=y, 2=z +string gizmoFocusedObj = ""; // Mouse is down - grab is active +int gizmoFocusedObjAxis = -1; +vector2 gizmoPrevMouseScreenPos(0,0); + + + +void drawSingleObjectGizmos(string instName, ObjInstanceInfo@ instInfo) +{ + ImDrawList @drawlist = imgui_utils::ImGetDummyFullscreenWindow("ODEF browser gizmos"); + bool grabHovered = (instInfo.inSelection || instInfo.uiHovered || (instInfo.sceneHovered && instInfo.sceneHoveredAxis == -1)); + + color hColor = color(0,0,0,1); +if (instInfo.uiHovered) { hColor = cfgGizmoUiHoverColor ; } +else if (instInfo.inSelection) {hColor = cfgObjSelectionHighlightCol; } +else { hColor = cfgGizmoSceneHoverColor; } + + + // draw object origin visualization - fake knob, not actually hoverable (empty param `instName` disables hover) + if (drawGizmoMouseKnob(drawlist, "", instInfo.pos, -1, cfgGrabGizmoColFg, cfgGrabGizmoColBg, /*hovered:*/false, hColor)) + { + // object is visible on screen ~ draw axes gizmos + drawAxisGizmo(drawlist, instName, instInfo, 0); + drawAxisGizmo(drawlist, instName, instInfo, 1); + drawAxisGizmo(drawlist, instName, instInfo, 2); + + // draw elevation marker = actual grab knob + vector3 groundPoint(instInfo.pos.x, game.getGroundHeight(instInfo.pos), instInfo.pos.z); + drawGizmoMouseKnob(drawlist, instName, groundPoint, -1, cfgGizmoGroundMarkerColFg, cfgGizmoGroundMarkerColBg, grabHovered, hColor); + } +} + + + +bool drawGizmoMouseKnob(ImDrawList @drawlist, string instName, vector3 wPos, int axis, color colFg, color colBg, bool hovered, color colHover) // Helper for multiple gizmo funcs +{ + vector2 originScreenPos; + bool onScreen = game.getScreenPosFromWorldPos(wPos, /*out:*/originScreenPos); + if (onScreen) + { + // background filled circle with a circle within + drawlist.AddCircleFilled(originScreenPos, 8*cfgGizmoScale, colFg, cfgGizmoNumSeg); + drawlist.AddCircle(originScreenPos, 5*cfgGizmoScale, colBg, cfgGizmoNumSeg, 2*cfgGizmoScale); + + if (instName != "") // empty param `instName` disables hover + { + + // hover highlight + if (hovered) + { + drawlist.AddCircle(originScreenPos, 10*cfgGizmoScale, colHover, cfgGizmoNumSeg, 3*cfgGizmoScale); + } + + // seeking hovered element + + int dist = getMouseShortestDistance(game.getMouseScreenPosition(), originScreenPos); + if (dist < gizmoClosestDist) + { + gizmoClosestDist = dist; + gizmoClosestObj = instName; + gizmoClosestObjAxis = axis; + } + } + } + return onScreen; +} + +vector3 getAxisGizmoPoint(vector3 base, int axis, float offset) // Helper for `drawAxisGizmo()` +{ + return base + vector3( (axis==0?offset:0.f), (axis==1?offset:0.f), (axis==2?offset:0.f) ); +} + +void drawAxisGizmo(ImDrawList @drawlist, string instName, ObjInstanceInfo@ instInfo, int axis) // Helper for `drawSingleObjectGizmos()` +{ + vector3 axStartWorldPos = getAxisGizmoPoint(instInfo.pos, axis, cfgAxisGizmoPad); + vector3 axEndWorldPos = getAxisGizmoPoint(axStartWorldPos, axis, cfgAxisGizmoLen); + vector2 axStartScreenPos, axEndScreenPos; + if (game.getScreenPosFromWorldPos(axStartWorldPos, /*[out]*/axStartScreenPos) + && game.getScreenPosFromWorldPos(axEndWorldPos, /*[out]*/axEndScreenPos)) + { + drawlist.AddLine(axStartScreenPos, axEndScreenPos, cfgAxisGizmoColBg, 8*cfgGizmoScale); + drawlist.AddLine(axStartScreenPos, axEndScreenPos, cfgAxisGizmoColFg[axis], 4*cfgGizmoScale); + + bool axKnobHovered = (instInfo.uiHovered || (instInfo.sceneHovered && (instInfo.sceneHoveredAxis == axis || instInfo.sceneHoveredAxis == -1))); + vector3 axKnobPos = axStartWorldPos + ((axEndWorldPos - axStartWorldPos)/2); + color axHovColor = (axKnobHovered && instInfo.uiHovered) ? cfgGizmoUiHoverColor : cfgGizmoSceneHoverColor; + drawGizmoMouseKnob(drawlist, instName, axKnobPos, axis, cfgAxisGizmoColBg, cfgAxisGizmoColFg[axis], axKnobHovered, axHovColor); + } +} + +void drawAllObjectsGizmos() +{ + // reset vars for mouse hover seeking + if (gizmoFocusedObj == "") + { + gizmoClosestDist = INT_MAX; + gizmoClosestObj = ""; + } + + // Looop over all gizmos + array@ spawnedObjectInstanceIds = objInstances.getKeys(); + for (uint iInst = 0; iInst < spawnedObjectInstanceIds.length(); iInst++) + { + string instName = spawnedObjectInstanceIds[iInst]; + ObjInstanceInfo@ instInfo = null; + if (objInstances.get(instName, @instInfo)) + { + + drawSingleObjectGizmos(instName, instInfo); + instInfo.sceneHovered=false; + } + } +} + +void updateInputEvents() +{ + + // evaluate mouse hover + // ------ + if (gizmoFocusedObj == "") + { + if (gizmoClosestDist <= (cfgGizmoHoverDistMax * cfgGizmoScale)) + { + ObjInstanceInfo@ instInfo = null; + if (objInstances.get(gizmoClosestObj, @instInfo)) + { + instInfo.sceneHovered=true; + instInfo.sceneHoveredAxis = gizmoClosestObjAxis; + if (ImGui::IsMouseDown(0)) + { + gizmoFocusedObj = gizmoClosestObj; + gizmoFocusedObjAxis = gizmoClosestObjAxis; + } + } + } + } + else + { + ObjInstanceInfo@ instInfo = null; + if (ImGui::IsMouseDown(0) && objInstances.get(gizmoFocusedObj, @instInfo)) + { + if ( gizmoFocusedObjAxis == -1) + { + vector3 tMouse; + if (game.getMousePositionOnTerrain(/*[out]*/ tMouse) ) + { + + instInfo.pos = vector3(tMouse.x, instInfo.pos.y, tMouse.z); + game.moveObjectVisuals(gizmoFocusedObj, instInfo.pos); + } + } + else if (gizmoFocusedObjAxis>=0 && gizmoFocusedObjAxis <= 2) + { + vector3 axStartWorldPos = getAxisGizmoPoint(instInfo.pos, gizmoFocusedObjAxis, cfgAxisGizmoPad); + vector3 axEndWorldPos = getAxisGizmoPoint(axStartWorldPos, gizmoFocusedObjAxis, cfgAxisGizmoLen); + vector2 axStartScreenPos, axEndScreenPos; + if (game.getScreenPosFromWorldPos(axStartWorldPos, /*[out]*/axStartScreenPos) + && game.getScreenPosFromWorldPos(axEndWorldPos, /*[out]*/axEndScreenPos)) + { + vector2 axScreenVec = axEndScreenPos - axStartScreenPos; + vector2 mScreenVec = game.getMouseScreenPosition() - gizmoPrevMouseScreenPos; + float axRatioXtoY = fabs(axScreenVec.x) / fabs(axScreenVec.y); + // watch out for dividing by zero + float axDistX = (axScreenVec.x == 0.f) ? 0.f : (mScreenVec.x / axScreenVec.x) * axRatioXtoY; + float axDistY = (axScreenVec.y == 0.f) ? 0.f : (mScreenVec.y / axScreenVec.y) * (1.f - axRatioXtoY); + float axDist = (axDistX + axDistY); + vector3 translation; + setAxisVal(translation, gizmoFocusedObjAxis, axDist*cfgAxisGizmoLen); + instInfo.pos += translation; + game.moveObjectVisuals(gizmoFocusedObj, instInfo.pos); + } + } + } + else + { + if (objInstances.get(gizmoFocusedObj, @instInfo)) + { + // respawn the object to update collisions + game.destroyObject(instInfo.name); + game.spawnObject(instInfo.odefName, instInfo.name, instInfo.pos, instInfo.rot, "", false); + } + gizmoFocusedObj = ""; + } + } + + + gizmoPrevMouseScreenPos = game.getMouseScreenPosition(); + +} +//#endregion + + +//#region U I T O O L S + +float manualSpawnPanelHBias = 0.f; +vector3 manualSpawnPanelObjPos = vector3(0,0,0); +vector3 manualSpawnPanelObjRot = vector3(0,0,0); + +void drawManualSpawnPanel() +{ + ImGui::PushID("manualSpawn"); + + ImGui::TextDisabled("Specify parameters by hand"); + + ImGui::InputText("ODEF", manualSpawnPanelOdefName); + if (manualSpawnPanelOdefName != "") + { + ImGui::SameLine(); + if (ImGui::SmallButton("clear")) + { + manualSpawnPanelOdefName = ""; + } + } + ImGui::Text("Position (remember Y is up):"); ImGui::SameLine(); + if (ImGui::SmallButton("<-- Fill from character")) + { + manualSpawnPanelObjPos = game.getPersonPosition() + vector3(0,manualSpawnPanelHBias,0); + } + ImGui::Dummy(vector2(25, 0)); ImGui::SameLine(); + ImGui::SetNextItemWidth(75.f); ImGui::InputFloat("X", manualSpawnPanelObjPos.x); ImGui::SameLine(); + ImGui::SetNextItemWidth(75.f); ImGui::InputFloat("Y", manualSpawnPanelObjPos.y); ImGui::SameLine(); + ImGui::SetNextItemWidth(75.f); ImGui::InputFloat("Z", manualSpawnPanelObjPos.z); + + + ImGui::Text("Rotation: (degrees):"); ImGui::SameLine(); + if (ImGui::SmallButton("<-- Fill from character")) + { + manualSpawnPanelObjRot = vector3(0,-game.getPersonRotation().valueDegrees(),0); + } + ImGui::Dummy(vector2(25, 0)); ImGui::SameLine(); + ImGui::SetNextItemWidth(75.f); ImGui::InputFloat("X", manualSpawnPanelObjRot.x); ImGui::SameLine(); + ImGui::SetNextItemWidth(75.f); ImGui::InputFloat("Y", manualSpawnPanelObjRot.y); ImGui::SameLine(); + ImGui::SetNextItemWidth(75.f); ImGui::InputFloat("Z", manualSpawnPanelObjRot.z); + + if ( manualSpawnPanelOdefName != "" && ImGui::Button("Spawn manually!")) + { + spawnObjectManually(manualSpawnPanelOdefName, manualSpawnPanelObjPos, manualSpawnPanelObjRot); + } + + ImGui::PopID(); // "manualSpawn" +} + +void drawSelectionUtilsPanel() +{ + // Tools that affect current selection: alignment, spacing etc... + // -------------------------------------------------------------- + + // gather statistics + int statNumObjs = 0; + vector3 statExtentMin (FLT_MAX, FLT_MAX, FLT_MAX); + + + + vector3 statExtentMax (FLT_MIN, FLT_MIN, FLT_MIN); + + array@ objInstancesKeys = objInstances.getKeys(); + for (uint iKey = 0; iKey < objInstancesKeys.length(); iKey++) + { + string iInst = objInstancesKeys[iKey]; + ObjInstanceInfo@ instInfo = cast(objInstances[iInst]); + if (instInfo.inSelection) + { + // statNumObjects++; + + statExtentMin.x = fmin(instInfo.pos.x, statExtentMin.x); + statExtentMin.y = fmin(instInfo.pos.y, statExtentMin.y); + statExtentMin.z = fmin(instInfo.pos.z, statExtentMin.z); + + statExtentMin.x = fmax(instInfo.pos.x, statExtentMax.x); + statExtentMin.y = fmax(instInfo.pos.y, statExtentMax.y); + statExtentMin.x = fmax(instInfo.pos.z, statExtentMax.z); + } + } +} +//#endregion + + +//#region G A M E C A L L B A C K S + +void frameStep(float dt) +{ + drawAllObjectsGizmos(); + updateInputEvents(); + if ( (@odefs == null && odefsError == "")) + { + refreshODEFs(); + } + + if (ImGui::Begin("ODEF browser", closeBtnHandler.windowOpen, 0)) + { + closeBtnHandler.draw(); + + drawMainPanel(); + + ImGui::End(); + } +} + +void drawMainPanel() +{ + + + ImGui::TextDisabled("T H E T O O L"); + if (ImGui::CollapsingHeader("Status (diag)")) + { + ImGui::Text("Focused obj: " + gizmoFocusedObj); + ImGui::Text("Focused obj axis: " + gizmoFocusedObjAxis); + ImGui::Text("IsMouseDown(0 = LMB): "+ImGui::IsMouseDown(0)); + ImGui::Text("IsMouseDown(1 = RMB): "+ImGui::IsMouseDown(1)); + ImGui::Text("Is left ALT key down: " + inputs.isKeyDown(KC_LALT)); + ImGui::Text("Is right ALT key down: " + inputs.isKeyDown(KC_RALT)); + ImGui::Text("ODEFs loading error: " + odefsError); + } + if (ImGui::CollapsingHeader("Config")) + { + drawConfigPanel(); + } + + ImGui::Separator(); + ImGui::TextDisabled("O B J D E F S"); + + if (ImGui::CollapsingHeader("Full ODEF listing")) + { + if (ImGui::Button("Refresh")) + { + ImGui::Button("refresh ODEFs"); + } + for (uint iOdef=0; iOdef < odefs.length(); iOdef++) + { + ImGui::BulletText(string(odefs[iOdef]["basename"])); + } + } + else + { + ImGui::SameLine(); + ImGui::TextDisabled(" Total loaded: "+odefs.length()); + } + + if (ImGui::CollapsingHeader("ODEFs by category")) + { + drawOdefTree(); + } + + ImGui::Separator(); + ImGui::TextDisabled("O B J E C T S"); + + + if (ImGui::CollapsingHeader("Manual spawn")) + { + drawManualSpawnPanel(); + } + drawSpawnedObjectsPanel(); + + +} +//#endregion + + +//#region H E L P E R S + + +int clamp(int minval, int val, int maxval) +{ + return max(minval, min(val, maxval)); +} + +int min(int a, int b) +{ + return (a < b) ? a : b; +} + +int max(int a, int b) +{ + return (a > b) ? a : b; +} + +float fmax(float a, float b) +{ + return (a > b) ? a : b; +} + +float fmin(float a, float b) +{ + return (a < b) ? a : b; +} + +int abs(int a) +{ + return (a < 0) ? -a : a; +} + +float fabs(float f) +{ + return (f<0.f)?-f:f; +} + + + +int getMouseShortestDistance(vector2 mouse, vector2 target) +{ + int dx = abs(int(mouse.x) - int(target.x)); + int dy = abs(int(mouse.y) - int(target.y)); + return max(dx, dy); +} + +void setAxisVal(vector3&inout vec, int axis, float val) { + switch(axis) { + case 0: vec.x=val; break; + case 1: vec.y=val; break; + case 2: vec.z=val; break; + default: break; + } +} + +//#endregion diff --git a/resources/scripts/example_terrn2_raceConverter.as b/resources/scripts/example_terrn_raceConverter.as similarity index 100% rename from resources/scripts/example_terrn2_raceConverter.as rename to resources/scripts/example_terrn_raceConverter.as diff --git a/resources/scripts/example_terrn_railBuilder.as b/resources/scripts/example_terrn_railBuilder.as new file mode 100644 index 0000000000..0525a6cb96 --- /dev/null +++ b/resources/scripts/example_terrn_railBuilder.as @@ -0,0 +1,168 @@ +// RAIL BUILDER EXPERIMENT +// This script contains a datasheet of ODEF railroad pieces with info how to snap them. +// +// =================================================== + +// Window [X] button handler +#include "imgui_utils.as" +imgui_utils::CloseWindowPrompt closeBtnHandler; + +// #region Datasheet of available pieces, Key = odef name, Value = ObjPieceInfo +class ObjPieceSpec +{ + array snapPoints; + float heightBias; +ObjPieceSpec( float _hBias, array _snapPoints) { heightBias=_hBias; snapPoints=_snapPoints;} +}; +dictionary objPieces = { +{'rail1t1mstrt', ObjPieceSpec(0.4f, array = {vector3(1,1,1), vector3(-1,-1,-1)}) }, +{'rail1t5mstrt', ObjPieceSpec(0.4f, array = {vector3(1,1,1), vector3(-1,-1,-1)}) } +}; + +// #endregion + +// #region Spawned object instances +class ObjInstanceInfo +{ + string name; // Unique ID for this instance + string pieceName; + vector3 pos; + vector3 rot; +ObjInstanceInfo(string _name, vector3 _pos, vector3 _rot) {name=_name; pos=_pos; rot=_rot;} +}; +dictionary objInstances; // instanceName ->ObjInstanceInfo +uint spawnedObjSequentialNum = 0; +// #endregion + + +string nextPiece = 'rail1t1mstrt'; +string selectedPieceInst = ''; // instance name of selected piece; '' means 'use rorbot's position' +int selectedPieceSnapPoint = -1; // snap point index; -1 means none selected, use rorbot's position + +void addPieceBySelection() +{ + // spawn a piece selected on UI; determine placement and record info for drawing and editing + // -------------------------------------------------------- + ObjPieceSpec@ nextPieceSpec = cast(objPieces[nextPiece]); +if (@nextPieceSpec == null) { game.log("ERROR addPieceBySelection(): nextPieceSpec null!"); return; } + + vector3 instPos, instRot; + if (selectedPieceInst == '') + { + instPos = game.getPersonPosition() ; + instRot = vector3(0, -game.getPersonRotation().valueDegrees(), 0); + } + else + { + ObjInstanceInfo@ selPieceInfo = cast(objInstances[selectedPieceInst]); + if (@selPieceInfo == null) { game.log("ERROR addPieceBySelection(): selPieceInfo null!"); return; } + instPos = selPieceInfo.pos; + instRot = selPieceInfo.rot; + } + instPos.y += nextPieceSpec.heightBias; + string instName = nextPiece+"_S"+(spawnedObjSequentialNum++)+"N"+thisScript; + game.spawnObject(nextPiece, instName, instPos, instRot, "", false); + ObjInstanceInfo@ instInfo = ObjInstanceInfo (instName, instPos, instRot); + instInfo.pieceName = nextPiece; + game.log("DBG addPieceBySelection(): setting instance info for key '"+instName+"' (is null: "+(instInfo is null)+")"); + @objInstances[instName] = @instInfo; + // paranoid dbg test + + ObjInstanceInfo@ instInfo2 =cast(@objInstances[instName]); + if (@instInfo2 == null) + { + game.log("ERR addPieceBySelection(): inst info null test failed"); + } + // paranoid n.2 + ObjInstanceInfo@ instInfo3 = null; + bool instInfo3valid=objInstances.get(instName, @instInfo3); + game.log("DBG addPieceBySelection(): instInfo3valid="+instInfo3valid+" (isnull:"+(instInfo3 is null)+")"); + // END paranoid + selectedPieceInst = instName; +} + +void drawInstanceHandles(ObjInstanceInfo@ instInfo) +{ + // draw on-screen handles for origin-point and snap-points of a single rail piece + // --------------------------------------- + ImDrawList@ drawlist = imgui_utils::ImGetDummyFullscreenWindow("rail builder handles"); + + vector2 originScreenPos; + if (game.getScreenPosFromWorldPos(instInfo.pos, /*out:*/originScreenPos)) + { + drawlist.AddCircleFilled(originScreenPos, 4, color(1,0,0,1)); + } + + // Then look up piece spec and draw snap-points + ObjPieceSpec@ pieceSpec = cast(objPieces[instInfo.pieceName]); + if (@pieceSpec == null) + { + ImGui::Text("DBG piece spec null!"); + } + else + { + ImGui::Text("DBG num snappoints:"+pieceSpec.snapPoints.length()); + for (uint i=0; i@ keys = objInstances.getKeys(); + for (uint iKey = 0; iKey < keys.length(); iKey++) + { + ImGui::PushID(iKey); + + ObjInstanceInfo@ instInfo = cast(objInstances[keys[iKey]]); + ImGui::Bullet(); ImGui::SameLine(); ImGui::TextDisabled(""+iKey+'/'+keys.length()+": "); ImGui::SameLine(); + if (@instInfo == null) + { + ImGui::Text("ERR instance info null "); + } + else + { + ImGui::Text('"'+instInfo.name+'" ~ position X="'+instInfo.pos.x+" Y(up)="+instInfo.pos.y+" Z="+instInfo.pos.z); + drawInstanceHandles(instInfo); + } + + ImGui::PopID(); //iKey + } +} + +void frameStep(float dt) +{ + //drawInstancesAll(); + if (ImGui::Begin("~ ~ ~ R A I L B U I L D E R ~ ~ ~", closeBtnHandler.windowOpen, 0)) + { + // Draw the "Terminate this script?" prompt on the top (if not disabled by config). + closeBtnHandler.draw(); + + drawInstancesAll(); + ImGui::Text("Next piece: '" + nextPiece + "'"); + ImGui::Text("Selected piece: '" + selectedPieceInst + "' (Snap# "+selectedPieceSnapPoint+")"); + ImGui::SameLine(); + if (ImGui::SmallButton("Reset##selection")) + { + selectedPieceInst = ''; + selectedPieceSnapPoint = -1; + } + if (ImGui::Button('Add piece!')) + { + addPieceBySelection(); + } + + ImGui::End(); + } +} From f9053bbe9387c0263613e7c850d2d7416f05b37d Mon Sep 17 00:00:00 2001 From: ohlidalp Date: Sat, 30 Nov 2024 16:39:00 +0100 Subject: [PATCH 20/36] script_editor.as: commented-out [Profiling] stuff also removed one Float->Int compiler warning --- resources/scripts/script_editor.as | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/resources/scripts/script_editor.as b/resources/scripts/script_editor.as index 280142b732..14744d99b2 100644 --- a/resources/scripts/script_editor.as +++ b/resources/scripts/script_editor.as @@ -578,7 +578,7 @@ class ScriptEditorWindow // Calc child window size const int MAX_CHILD_HEIGHT = 300; const int CHILD_WIDTH = 600; - int childHeight = record.fileinfos.length() * ImGui::GetTextLineHeightWithSpacing(); + int childHeight = int(record.fileinfos.length() * ImGui::GetTextLineHeightWithSpacing()); //ImGui::Text("DBG record.fileinfos.length()=" + record.fileinfos.length() + ", childHeight{not clamped}=" + childHeight); if (childHeight > MAX_CHILD_HEIGHT) { @@ -1265,12 +1265,14 @@ void analyzeLines() private void analyzeBuffer() // helper for `analyzeLines()` { - // [profiling] + /* + // [profiling] Ogre::Timer timer; uint totalMicrosecDict = 0; // the `bufferLinesMeta` array of dicts uint totalMicrosecCpuDict = 0; uint totalMicrosecRegions = 0; uint totalMicrosecCpuRegions = 0; + */ this.bufferLinesMeta.resize(0); // clear all int startOffset = 0; @@ -1452,8 +1454,11 @@ string SPECIALCHARS = "{}\n\t \0"; { int endOffset = i; + /* + // [Profiling] uint microsecBeforeRegions = timer.getMicroseconds(); uint microsecCpuBeforeRegions = timer.getMicrosecondsCPU(); + */ // Process #region if (regionFound) @@ -1478,14 +1483,16 @@ string SPECIALCHARS = "{}\n\t \0"; regionFoundWithName = ""; } + /* + // [Profiling] totalMicrosecRegions += timer.getMicroseconds() - microsecBeforeRegions; totalMicrosecCpuRegions += timer.getMicrosecondsCPU() - microsecCpuBeforeRegions; - - // Finish line + // Finish line uint microsecBeforeDict = timer.getMicroseconds(); uint microsecCpuBeforeDict = timer.getMicrosecondsCPU(); + */ - this.bufferLinesMeta.insertLast({ {'startOffset', startOffset} }); + this.bufferLinesMeta.insertLast({ {'startOffset', startOffset} }); this.bufferLinesMeta[lineIdx]['endOffset'] = endOffset; int len = endOffset - startOffset; this.bufferLinesMeta[lineIdx]['len'] = len; @@ -1502,8 +1509,11 @@ string SPECIALCHARS = "{}\n\t \0"; this.bufferLinesMeta[lineIdx]['autoIndentLevel'] = autoIndentLevelCurrent; this.bufferLinesMeta[lineIdx]['actualIndentBlanks'] = actualIndentBlanks; + /* + // [Profiling] totalMicrosecDict += timer.getMicroseconds() - microsecBeforeDict; totalMicrosecCpuDict += timer.getMicrosecondsCPU() - microsecCpuBeforeDict; + */ } if (isCharNul) @@ -1542,12 +1552,15 @@ string SPECIALCHARS = "{}\n\t \0"; this.totalChars++; } + /* // [profiling] uint microsecBeforeMerging = timer.getMicroseconds(); uint microsecCpuBeforeMerging = timer.getMicrosecondsCPU(); + */ this.mergeCollectedFoldingRegionsWithExisting(collectedRegions); + /* // [profiling] uint totalMicrosecMerging = timer.getMicroseconds() - microsecBeforeMerging; uint totalMicrosecCpuMerging = timer.getMicrosecondsCPU() - microsecCpuBeforeMerging; @@ -1557,6 +1570,7 @@ string SPECIALCHARS = "{}\n\t \0"; +" dict "+totalMicrosecDict+"us (CPU "+totalMicrosecCpuDict+"us)" +" merging "+totalMicrosecMerging+"us (CPU "+totalMicrosecCpuMerging+"us)" ); + */ } private void mergeCollectedFoldingRegionsWithExisting(dictionary&in collectedRegions) // helper for `analyzeBuffer()` From e7a64b86c0c25fb7dadea04baaf696096edd1436 Mon Sep 17 00:00:00 2001 From: ohlidalp Date: Sat, 30 Nov 2024 16:52:27 +0100 Subject: [PATCH 21/36] script_editor.as: internal code cleanup (broken indents + trailing blanks) --- resources/scripts/script_editor.as | 1128 ++++++++++++++-------------- 1 file changed, 564 insertions(+), 564 deletions(-) diff --git a/resources/scripts/script_editor.as b/resources/scripts/script_editor.as index 14744d99b2..1d6f6fb4e2 100644 --- a/resources/scripts/script_editor.as +++ b/resources/scripts/script_editor.as @@ -38,18 +38,18 @@ const uint FILEINFO_COMPRESSEDSIZE_UNKNOWN = uint(-1); bool drawLineNumbers=true; color lineNumberColor=color(1.f,1.f,0.7f,1.f); float lineNumColumnWidth = 25; -// Line char counts +// Line char counts bool drawLineLengths=true; color lineLenColor=color(0.5f,0.4f,0.3f,1.f); float lineLenColumnWidth = 25; -// Line comment offsets +// Line comment offsets bool drawLineCommentOffsets=true; color lineCommentOffsetColor=color(0.1f,0.9f,0.6f,0.6f); float lineCommentColumnWidth = 25; // Line http offsets bool drawLineHttpOffsets=false; // only visual, links don't open color lineHttpOffsetColor=color(LINKCOLOR.r, LINKCOLOR.g, LINKCOLOR.a, 0.6f); -float lineHttpColumnWidth = 16; +float lineHttpColumnWidth = 16; // Line err counts (error may span multiple AngelScript messages!) bool drawLineErrCounts = false; color lineErrCountColor = color(1.f,0.2f,0.1f,1.f); @@ -79,7 +79,7 @@ color commentHighlightColor = color(0.1f,0.9f,0.6f,0.16f); color regionCommentHighlightColor = color(0.78f,0.76f,0.32f,0.18f); bool drawCommentHighlights = true; color httpHighlightColor=color(0.25, 0.3, 1, 0.3f); -bool drawHttpHighlights = false; // only visual, links don't open +bool drawHttpHighlights = false; // only visual, links don't open float scriptInfoIndentWidth = 25; int autoIndentNumSpaces = 4; @@ -147,19 +147,19 @@ string arg5ex, string arg6ex, string arg7ex, string arg8ex) { int nid = arg1; /*game.log("DBG eventCallbackEx(EXCEPTIONCALLBACK): distributing to "+ editorWindow.tabs.length()+ " tabs, NID: " + nid);*/ - + for (uint i=0; i tabs; uint currentTab = 0; int tabScheduledForRemoval = -1; - + // GLOBAL CONTEXT imgui_utils::CloseWindowPrompt closeBtnHandler; - + // MENU CONTEXT (pre-scanned file lists with extra details) ScriptIndexerRecord recentScriptsRecord; ScriptIndexerRecord localScriptsRecord; ScriptIndexerRecord exampleScriptsRecord; ScriptIndexerRecord includeScriptsRecord; - + ScriptEditorWindow() { - closeBtnHandler.cfgPromptText = "Really close script editor? You will lose any unsaved work."; + closeBtnHandler.cfgPromptText = "Really close script editor? You will lose any unsaved work."; } - + void refreshLocalFileList() { //NOTE: `recentScriptsRecord` is constructed manually. @@ -216,7 +216,7 @@ class ScriptEditorWindow this.exampleScriptsRecord.scanResourceGroup(RGN_RESOURCES_SCRIPTS, "example_*.*"); this.includeScriptsRecord.scanResourceGroup(RGN_RESOURCES_SCRIPTS, "*_utils.*"); } - + void addTab(const string&in tabName, const string&in buffer) { ScriptEditorTab nTab; @@ -224,7 +224,7 @@ class ScriptEditorWindow nTab.setBuffer(buffer); tabs.insertLast(nTab); } - + void removeTab(uint i) { if (i < tabs.length()) @@ -232,7 +232,7 @@ class ScriptEditorWindow tabs.removeAt(i); } } - + void draw(float dt) { // Process scheduled tab closing @@ -240,8 +240,8 @@ class ScriptEditorWindow { this.removeTab(uint(tabScheduledForRemoval)); this.tabScheduledForRemoval = -1; - } - + } + // Make sure there's always a tab open () if (tabs.length() == 0) { @@ -251,39 +251,39 @@ class ScriptEditorWindow else { if (this.currentTab >= tabs.length()) - this.currentTab = tabs.length()-1; + this.currentTab = tabs.length()-1; } - + this.tabs[this.currentTab].updateAutosave(dt); this.tabs[this.currentTab].handleRequests(dt); - + int flags = ImGuiWindowFlags_MenuBar | ImGuiWindowFlags_NoScrollbar; - + if (ImGui::Begin("Script editor", this.closeBtnHandler.windowOpen, flags)) { this.closeBtnHandler.draw(); this.drawMenubar(); this.drawTabBar(); - + this.tabs[this.currentTab].drawTextArea(); this.tabs[this.currentTab].drawExceptionsPanel(); - this.tabs[this.currentTab].drawFooter(); - + this.tabs[this.currentTab].drawFooter(); + // To draw on top of editor text, we must trick DearIMGUI using an extra invisible window ImGui::SetCursorPos(this.tabs[this.currentTab].errorsScreenCursor - ImGui::GetWindowPos()); ImGui::PushStyleColor(ImGuiCol_ChildBg, color(0.f,0.f,0.f,0.f)); // Fully transparent background! int childFlags = ImGuiWindowFlags_NoInputs; // Necessary, otherwise editing is blocked. if (ImGui::BeginChild("ScriptEditorErr-child", this.tabs[this.currentTab].errorsTextAreaSize,/*border:*/false, childFlags)) { - this.tabs[this.currentTab].drawTextAreaErrors(); + this.tabs[this.currentTab].drawTextAreaErrors(); } ImGui::EndChild(); // must always be called - legacy reasons - ImGui::PopStyleColor(1); // ChildBg + ImGui::PopStyleColor(1); // ChildBg } ImGui::End(); - + } - + private void drawTabBar() { int tabBarFlags = 0; @@ -307,11 +307,11 @@ class ScriptEditorWindow else if (ImGui::IsItemClicked()) this.currentTab = i; } - + ImGui::EndTabBar(); } } - + private void drawMenubar() { bool analysisDoneThisFrame = false; @@ -324,11 +324,11 @@ class ScriptEditorWindow { analysisDoneThisFrame = localScriptsRecord.advanceScriptAnalysis(); } - + bool loadRecentFile = this.drawSelectableFileList("Recent scripts", "Load##recent", recentScriptsRecord, /*&inout*/ fileNameBuf); - ImGui::Separator(); + ImGui::Separator(); bool loadLocalFile = this.drawSelectableFileList("Local scripts", "Load##local", localScriptsRecord, /*&inout*/ fileNameBuf); - + if (loadRecentFile || loadLocalFile) { // Recognize auto-saves and load them under the original name. Note '_AUTOSAVE' ~ 9 characters. @@ -348,23 +348,23 @@ class ScriptEditorWindow } /*game.log("DBG script editor: open file menu: file '"+fileNameBuf+"' is an autosave, loading as '"+tabName+"'");*/ } - + this.addTab(tabName, game.loadTextResourceAsString(fileNameBuf, RGN_SCRIPTS)); this.currentTab = this.tabs.length() - 1; // Focus the new tab - this.addRecentScript(fileNameBuf); + this.addRecentScript(fileNameBuf); } - + ImGui::EndMenu(); } - + // 'SAVE FILE' menu if (ImGui::BeginMenu("Save file")) { if (!analysisDoneThisFrame) { analysisDoneThisFrame = localScriptsRecord.advanceScriptAnalysis(); - } - + } + if (this.saveMenuOpening) { saveFileNameBuf = this.tabs[this.currentTab].bufferName; @@ -378,42 +378,42 @@ class ScriptEditorWindow } ImGui::SameLine(); ImGui::Checkbox("Overwrite", /*inout:*/saveShouldOverwrite); - + // Error indicator: if (this.saveFileResult == 1) ImGui::TextColored(color(0.2,0.7, 0.2, 1), "File saved OK"); else if (this.saveFileResult == -1) ImGui::TextColored(color(1,0.1, 0.2, 1), "Error saving file!"); - + ImGui::Separator(); this.drawSelectableFileList("Recent scripts", "Select##recent", recentScriptsRecord, /*&inout*/ saveFileNameBuf); this.drawSelectableFileList("Local scripts", "Select##local", localScriptsRecord, /*&inout*/ saveFileNameBuf); - ImGui::EndMenu(); + ImGui::EndMenu(); } else { this.saveMenuOpening = true; } - + // 'EXAMPLES' menu if (ImGui::BeginMenu("Examples")) - { + { if (!analysisDoneThisFrame) { analysisDoneThisFrame = exampleScriptsRecord.advanceScriptAnalysis(); - } - + } + bool loadExampleFile = this.drawSelectableFileList("Example scripts", "Load##example", exampleScriptsRecord, /*&inout*/ exampleNameBuf); - + if (loadExampleFile) { this.addTab(exampleNameBuf, game.loadTextResourceAsString(exampleNameBuf, RGN_RESOURCES_SCRIPTS)); this.currentTab = this.tabs.length() - 1; // Focus the new tab - } - + } + ImGui::EndMenu(); } - + // 'INCLUDES' menu if (ImGui::BeginMenu("Includes")) { @@ -421,18 +421,18 @@ class ScriptEditorWindow { analysisDoneThisFrame = includeScriptsRecord.advanceScriptAnalysis(); } - + bool loadIncludeFile = this.drawSelectableFileList("Include scripts", "Load##include", includeScriptsRecord, /*&inout*/ includeNameBuf); - + if (loadIncludeFile) { this.addTab(includeNameBuf, game.loadTextResourceAsString(includeNameBuf, RGN_RESOURCES_SCRIPTS)); this.currentTab = this.tabs.length() - 1; // Focus the new tab - } - + } + ImGui::EndMenu(); } - + // 'VIEW' menu if (ImGui::BeginMenu("View")) { @@ -448,44 +448,44 @@ class ScriptEditorWindow ImGui::PopStyleColor(1); // ImGuiCol_Text ImGui::PushStyleColor(ImGuiCol_Text, lineHttpOffsetColor); ImGui::Checkbox("Http offsets", /*inout:*/drawLineHttpOffsets); - ImGui::PopStyleColor(1); // ImGuiCol_Text + ImGui::PopStyleColor(1); // ImGuiCol_Text ImGui::PushStyleColor(ImGuiCol_Text, lineErrCountColor); ImGui::Checkbox("Error counts", /*inout:*/drawLineErrCounts); ImGui::PopStyleColor(1); // ImGuiCol_Text ImGui::PushStyleColor(ImGuiCol_Text, lineRegionMarkerColor); ImGui::Checkbox("Region markers", /*inout:*/drawLineRegionMarkers); - ImGui::PopStyleColor(1); // ImGuiCol_Text + ImGui::PopStyleColor(1); // ImGuiCol_Text ImGui::PushStyleColor(ImGuiCol_Text, lineAutoIndentLevelsColor); ImGui::Checkbox("Auto indents", /*inout:*/drawLineAutoIndentLevels); - ImGui::PopStyleColor(1); // ImGuiCol_Text + ImGui::PopStyleColor(1); // ImGuiCol_Text ImGui::PushStyleColor(ImGuiCol_Text, lineActualIndentsColor); ImGui::Checkbox("Actual indents", /*inout:*/drawLineActualIndents); - ImGui::PopStyleColor(1); // ImGuiCol_Text - + ImGui::PopStyleColor(1); // ImGuiCol_Text + ImGui::TextDisabled("Editor overlay:"); ImGui::Checkbox("Error text", /*inout:*/drawErrorText); ImGui::Checkbox("Warning text", /*inout:*/drawWarnText); ImGui::Checkbox("Comments", /*inout:*/drawCommentHighlights); ImGui::Checkbox("Hyperlinks", /*inout:*/drawHttpHighlights); - + ImGui::TextDisabled("Misc:"); ImGui::Checkbox("Autosave countdown", /*inout:*/drawAutosaveCountdown); ImGui::EndMenu(); } - + // 'FOLDING' menu if (ImGui::BeginMenu("Folding")) { if (ImGui::Button("Fold all")) { this.tabs[this.currentTab].requestFoldAll=true; } if (ImGui::Button("UnFold all")) { this.tabs[this.currentTab].requestUnFoldAll=true; } - + ImGui::Separator(); ImGui::TextDisabled("Invalid states:"); ImGui::TextColored(lineRegionMarkerColor, " Un! = Unnamed"); ImGui::TextColored(lineRegionMarkerColor, " Br! = Broken (no end)"); ImGui::TextColored(lineRegionMarkerColor, " Ex! = Exists already"); ImGui::TextColored(lineRegionMarkerColor, " Or! = Orphan exists"); - + ImGui::Separator(); ImGui::TextDisabled("Orphan regions"); array regionNames = this.tabs[this.currentTab].workBufferRegions.getKeys(); @@ -505,7 +505,7 @@ class ScriptEditorWindow } ImGui::EndMenu(); } - + // 'TOOLS' menu if (ImGui::BeginMenu("Tools")) { @@ -514,7 +514,7 @@ class ScriptEditorWindow { this.tabs[this.currentTab].requestIndentBuffer = true; } - + ImGui::Separator(); ImGui::TextDisabled("Settings"); ImGui::SetNextItemWidth(75.f); @@ -522,7 +522,7 @@ class ScriptEditorWindow ImGui::Checkbox("Indent on save", /*inout*/autoIndentOnSave); ImGui::Dummy(vector2(5,5)); ImGui::Checkbox("Kill script on AS exception", /*inout*/killScriptOnAngelscriptException); - + /*int nid = this.tabs[this.currentTab].currentScriptUnitID; // ~~~ EPanel debug util ~~~ if (nid != SCRIPTUNITID_INVALID) { @@ -533,27 +533,27 @@ class ScriptEditorWindow if (ImGui::Button("Wtf")) { this.tabs[this.currentTab].epanel.addExceptionInternal(nid, "Wtf"); } if (ImGui::Button("Wat?")) { this.tabs[this.currentTab].epanel.addExceptionInternal(nid, "Wat?"); } }*/ - + ImGui::EndMenu(); } - + // 'DOCS' menu if (ImGui::BeginMenu("Docs")) { ImGui::TextDisabled("AngelScript:"); imgui_utils::ImHyperlink('https://www.angelcode.com/angelscript/sdk/docs/manual/doc_script.html', "The script language"); - + ImGui::Separator(); ImGui::TextDisabled("Rigs of Rods:"); imgui_utils::ImHyperlink('https://developer.rigsofrods.org/d2/d42/group___script_side_a_p_is.html', "Script-side APIs"); - + ImGui::EndMenu(); } - + // START-STOP button ImGui::Dummy(vector2(50, 1)); this.tabs[this.currentTab].drawStartStopButton(); - + // AUTOSAVE counter if (drawAutosaveCountdown) { @@ -563,30 +563,30 @@ class ScriptEditorWindow ImGui::EndMenuBar(); } } - + // Creates a child window with dynamic height (but clamped to hardcoded max value). private bool drawSelectableFileList(string title, string btnText, ScriptIndexerRecord@ record, string&inout out_selection) { - + bool retval = false; ImGui::TextDisabled(title+" ("+record.fileinfos.length()+"):"); if (record.fileinfos.length() == 0) { return retval; // nothing to draw } - + // Calc child window size const int MAX_CHILD_HEIGHT = 300; - const int CHILD_WIDTH = 600; + const int CHILD_WIDTH = 600; int childHeight = int(record.fileinfos.length() * ImGui::GetTextLineHeightWithSpacing()); //ImGui::Text("DBG record.fileinfos.length()=" + record.fileinfos.length() + ", childHeight{not clamped}=" + childHeight); if (childHeight > MAX_CHILD_HEIGHT) { childHeight = MAX_CHILD_HEIGHT; - } - + } + // draw the child window - ImGui::BeginChild(title+"-child", vector2(CHILD_WIDTH, childHeight), /*border:*/false, ImGuiWindowFlags_HorizontalScrollbar); + ImGui::BeginChild(title+"-child", vector2(CHILD_WIDTH, childHeight), /*border:*/false, ImGuiWindowFlags_HorizontalScrollbar); for (uint i=0; i> bufferMessageIDs; dictionary workBufferRegions; // Key: region names (must be unique, otherwise not foldable), value: RegionInfo // END text buffer - + // MESSAGES FROM ANGELSCRIPT ENGINE array messages; int messagesTotalErr = 0; int messagesTotalWarn = 0; int messagesTotalInfo = 0; // END messages from angelscript - - + + // CONTEXT float scrollY = 0.f; float scrollX = 0.f; @@ -691,7 +691,7 @@ class ScriptEditorTab ExceptionsPanel@ epanel; float autosaveTimeCounterSec = 0.f; int autosaveResult = 0; // 0=no action, 1=success, -1=failure; - + // REQUEST 'QUEUE' string requestFoldRegion; string requestUnFoldRegion; @@ -702,12 +702,12 @@ class ScriptEditorTab bool requestAutoSaveFile = false; bool requestRunBuffer = false; bool requestStopBuffer = false; - + ScriptEditorTab() { @epanel = ExceptionsPanel(this); } - + void drawStartStopButton() { if (this.waitingForManipEvent) // When waiting for async result of (UN)LOAD_SCRIPT_REQUESTED @@ -717,7 +717,7 @@ class ScriptEditorTab else { if (this.currentScriptUnitID == SCRIPTUNITID_INVALID) - { + { if (ImGui::Button("[>>] RUN")) { this.requestRunBuffer = true; @@ -730,9 +730,9 @@ class ScriptEditorTab this.requestStopBuffer = true; } } - } + } } - + protected float measureLineInfoColumns() { // Reserve space for the metainfo columns @@ -742,9 +742,9 @@ class ScriptEditorTab if (drawLineLengths) metaColumnsTotalWidth += lineLenColumnWidth; if (drawLineCommentOffsets) - metaColumnsTotalWidth += lineCommentColumnWidth; + metaColumnsTotalWidth += lineCommentColumnWidth; if (drawLineHttpOffsets) - metaColumnsTotalWidth += lineHttpColumnWidth; + metaColumnsTotalWidth += lineHttpColumnWidth; errCountOffset = metaColumnsTotalWidth; if (drawLineErrCounts) metaColumnsTotalWidth += errCountColumnWidth; @@ -753,36 +753,36 @@ class ScriptEditorTab if (drawLineActualIndents) metaColumnsTotalWidth += lineActualIndentsColumnWidth; if (drawLineRegionMarkers) - metaColumnsTotalWidth += lineRegionMarkerColumnWidth; - + metaColumnsTotalWidth += lineRegionMarkerColumnWidth; + return metaColumnsTotalWidth; } - + protected void drawLineInfoColumns(vector2 screenCursor) { ImGui::PushID("lineInfoCol"); - + // draws columns with info (line number etc...) on the left side of the text area // =============================================================================== - + vector2 linenumCursorBase = screenCursor+vector2(0.f, -scrollY); ImDrawList@ drawlist = ImGui::GetWindowDrawList(); - int bufferNumLines = int(this.bufferLinesMeta.length()); - + int bufferNumLines = int(this.bufferLinesMeta.length()); + float coarseClipBottomY = (screenCursor.y+textAreaSize.y)-ImGui::GetTextLineHeight()*0.6; float coarseClipTopY = screenCursor.y-ImGui::GetTextLineHeight()*0.7; - + bool inRegion = false; // verified foldable region (`#endregion` was found -> has RegionInfo entry) bool drawRegionFoldBtn = false; // verified foldable region string regionColumnStr = ""; int nextLineNumber = 1; // Folded regions cause jumps in numbering - + for (int lineIdx = 0; lineIdx < bufferNumLines; lineIdx++) { vector2 linenumCursor = linenumCursorBase + vector2(0, lineIdx*ImGui::GetTextLineHeight()); - + // Update line number; No drawing yet - must be done before coarse clipping - // (Folded regions cause jumps in numbering) + // (Folded regions cause jumps in numbering) int currentLineNumber = nextLineNumber; if (drawLineNumbers) { @@ -798,8 +798,8 @@ class ScriptEditorTab } nextLineNumber += lineShift; } - - // Update code folding state; no drawing yet - must be done before coarse clipping + + // Update code folding state; no drawing yet - must be done before coarse clipping if (drawLineRegionMarkers) { drawRegionFoldBtn = false; @@ -808,9 +808,9 @@ class ScriptEditorTab { string regionName = string(this.bufferLinesMeta[lineIdx]['regionName']); if (bool(bufferLinesMeta[lineIdx]['regionValid'])) - { + { // verify it's a foldable region (`#endregion` was found -> has RegionInfo entry) - + RegionInfo@ regionInfo = findRegion(this.workBufferRegions, regionName); RegionInfo@ fileRegionInfo = findRegion(this.workBufferRegions, regionName); if (@regionInfo != null && @fileRegionInfo != null) @@ -821,7 +821,7 @@ class ScriptEditorTab else { regionColumnStr = "Br!"; // Broken ('#endregion' not found) - } + } } else if (bool(bufferLinesMeta[lineIdx]['regionFound'])) { @@ -856,70 +856,70 @@ class ScriptEditorTab } } } - + // Coarse clipping if (linenumCursor.y < coarseClipTopY) continue; if (linenumCursor.y > coarseClipBottomY) - break; - + break; + if (drawLineNumbers) // actually draw { drawlist.AddText(linenumCursor, lineNumberColor, ""+currentLineNumber); linenumCursor.x += lineNumColumnWidth; } - + if (drawLineLengths) { drawlist.AddText(linenumCursor, lineLenColor, ""+int(this.bufferLinesMeta[lineIdx]['len'])); - linenumCursor.x += lineLenColumnWidth; + linenumCursor.x += lineLenColumnWidth; } - + if (drawLineCommentOffsets) { drawlist.AddText(linenumCursor, lineCommentOffsetColor, ""+int(this.bufferLinesMeta[lineIdx]['nonCommentLen'])); - linenumCursor.x += lineCommentColumnWidth; - } - + linenumCursor.x += lineCommentColumnWidth; + } + if (drawLineHttpOffsets) { drawlist.AddText(linenumCursor, lineHttpOffsetColor, ""+int(this.bufferLinesMeta[lineIdx]['nonHttpLen'])); - linenumCursor.x += lineHttpColumnWidth; - } - + linenumCursor.x += lineHttpColumnWidth; + } + // draw message count if (drawLineErrCounts) { - + if (bufferMessageIDs.length() > uint(lineIdx) // sanity check && bufferMessageIDs[lineIdx].length() > 0) { // Draw num errors to the column - - drawlist.AddText(linenumCursor, color(1.f,0.5f,0.4f,1.f), ""+bufferMessageIDs[lineIdx].length()); + + drawlist.AddText(linenumCursor, color(1.f,0.5f,0.4f,1.f), ""+bufferMessageIDs[lineIdx].length()); } linenumCursor.x += errCountColumnWidth; } - + // Auto-detected indentation level, multiplied by preset indent width. if (drawLineAutoIndentLevels) { drawlist.AddText(linenumCursor, lineAutoIndentLevelsColor, ""+int(this.bufferLinesMeta[lineIdx]['autoIndentLevel']) * autoIndentNumSpaces); - linenumCursor.x += lineAutoIndentLevelsColumnWidth; + linenumCursor.x += lineAutoIndentLevelsColumnWidth; } - + // Actual indentation length if (drawLineActualIndents) { drawlist.AddText(linenumCursor, lineActualIndentsColor, ""+int(this.bufferLinesMeta[lineIdx]['actualIndentBlanks'])); - linenumCursor.x += lineActualIndentsColumnWidth; - } - + linenumCursor.x += lineActualIndentsColumnWidth; + } + // draw region&endregion markers if (drawLineRegionMarkers) { @@ -934,7 +934,7 @@ class ScriptEditorTab ImGui::SetCursorPos(linenumCursor - ImGui::GetWindowPos()); ImGui::PushStyleColor(ImGuiCol_Button, lineRegionMarkerColor); ImGui::PushStyleColor(ImGuiCol_Text, color(0,0,0,1)); - ImGui::PushID(lineIdx); + ImGui::PushID(lineIdx); if (regionInfo.isFolded && ImGui::SmallButton("+")) { this.requestUnFoldRegion = regionName; @@ -952,33 +952,33 @@ class ScriptEditorTab { drawlist.AddText(linenumCursor, lineRegionMarkerColor, regionColumnStr); } - + linenumCursor.x += lineRegionMarkerColumnWidth; } } - + ImGui::PopID(); //"lineInfoCol" } - + void drawTextAreaErrors() { // draw messages to text area //=========================== - + vector2 msgCursorBase = this.errorsScreenCursor + vector2(4, -this.scrollY); - + int bufferNumLines = int(this.bufferLinesMeta.length()); ImDrawList@ drawlist = ImGui::GetWindowDrawList(); - + for (int linenum = 1; linenum <= bufferNumLines; linenum++) { - int lineIdx = linenum - 1; + int lineIdx = linenum - 1; vector2 lineCursor = msgCursorBase + vector2(0.f, ImGui::GetTextLineHeight()*lineIdx); - + // sanity checks if (this.bufferLinesMeta.length() <= uint(lineIdx)) continue; - + // first draw comment highlights (semitransparent quads) // special touch: if the comments are #[end]region markers, highlight them different. int startOffset = int(this.bufferLinesMeta[lineIdx]['startOffset']); @@ -995,15 +995,15 @@ class ScriptEditorTab string nonCommentStr = buffer.substr(startOffset, nonCommentLen); vector2 nonCommentSize = ImGui::CalcTextSize(nonCommentStr); vector2 pos = lineCursor + vector2(nonCommentSize.x, 0.f); - + // calc size int doubleslashCommentStart = int(this.bufferLinesMeta[lineIdx]['doubleslashCommentStart']); string commentStr = buffer.substr(doubleslashCommentStart, lineLen-nonCommentLen); vector2 commentSize = ImGui::CalcTextSize(commentStr); - + drawlist.AddRectFilled(pos, pos+commentSize, rectColor); } - + // second - http highlights int httpStart = int(this.bufferLinesMeta[lineIdx]['httpStart']); int httpLen = int(this.bufferLinesMeta[lineIdx]['httpLen']); @@ -1014,35 +1014,35 @@ class ScriptEditorTab string nonHttpStr = buffer.substr(startOffset, nonHttpLen); vector2 nonHttpSize = ImGui::CalcTextSize(nonHttpStr); vector2 pos = lineCursor + vector2(nonHttpSize.x, 0.f); - + // calc size string httpStr = buffer.substr(httpStart, httpLen); vector2 httpSize = ImGui::CalcTextSize(httpStr); - + drawlist.AddRectFilled(pos, pos+httpSize, httpHighlightColor); - + // underline // FIXME: draw underline when hyperlink behaviours work here // - The overlay window is 'NoInputs' so buttons don't work. // - The base window doesn't receive clicks for some reason. - //drawlist.AddLine(pos+vector2(0,httpSize.y), pos+httpSize, LINKCOLOR); + //drawlist.AddLine(pos+vector2(0,httpSize.y), pos+httpSize, LINKCOLOR); } - + // sanity check if (this.bufferMessageIDs.length() <= uint(lineIdx)) continue; - + // then errors and warnings for (uint i = 0; i < this.bufferMessageIDs[lineIdx].length(); i++) { uint msgIdx = this.bufferMessageIDs[lineIdx][i]; - + int msgRow = int(this.messages[msgIdx]['row']); int msgCol = int(this.messages[msgIdx]['col']); int msgType = int(this.messages[msgIdx]['type']); string msgText = string(this.messages[msgIdx]['message']); string containingFoldedRegionName = string(this.messages[msgIdx]['inFoldedRegion']); // HACK: dynamic field inserted by `analyzeMessages()` - + color col, bgCol; bool shouldDraw = false; switch (msgType) @@ -1059,16 +1059,16 @@ class ScriptEditorTab break; default:; } - + if (!shouldDraw) continue; - + // Calc horizontal offset string subStr = buffer.substr(startOffset, msgCol-1); vector2 textPos = lineCursor + vector2( ImGui::CalcTextSize(subStr).x, // X offset - under the indicated text position - ImGui::GetTextLineHeight()-2); // Y offset - slightly below the current line - + ImGui::GetTextLineHeight()-2); // Y offset - slightly below the current line + // Draw message text with background string errText = " ^ "+msgText; if (containingFoldedRegionName != "") @@ -1080,30 +1080,30 @@ class ScriptEditorTab vector2 errTextSize = ImGui::CalcTextSize(errText); drawlist.AddRectFilled(textPos, textPos+errTextSize, bgCol); drawlist.AddText(textPos , col, errText); - + // advance to next line lineCursor.y += ImGui::GetTextLineHeight(); } - } - - + } + + } void drawTextArea() { - + // Reserve space for the metainfo columns float metaColumnsTotalWidth = measureLineInfoColumns(); - + // Draw the multiline text input this.textAreaSize = vector2( - ImGui::GetWindowSize().x - (metaColumnsTotalWidth + 20), + ImGui::GetWindowSize().x - (metaColumnsTotalWidth + 20), ImGui::GetWindowSize().y - (115 + epanel.getHeightToReserve())); - - vector2 screenCursor = ImGui::GetCursorScreenPos() + /*frame padding:*/vector2(0.f, 4.f); - ImGui::SetCursorPosX(ImGui::GetCursorPosX() + metaColumnsTotalWidth); + + vector2 screenCursor = ImGui::GetCursorScreenPos() + /*frame padding:*/vector2(0.f, 4.f); + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + metaColumnsTotalWidth); bool changed = ImGui::InputTextMultiline(BUFFER_TEXTINPUT_LABEL, this.buffer, textAreaSize); - + // Trick: re-enter the inputtext panel to scroll it; see https://github.com/ocornut/imgui/issues/1523 ImGui::BeginChild(ImGui::GetID(BUFFER_TEXTINPUT_LABEL)); this.scrollX = ImGui::GetScrollX(); @@ -1111,36 +1111,36 @@ void drawTextArea() this.scrollMaxX = ImGui::GetScrollMaxX(); this.scrollMaxY = ImGui::GetScrollMaxY(); ImGui::EndChild(); - + if (changed) { this.analyzeLines(); // Enlarge buffer if at capacity if (this.totalChars == (this.buffer.length() - 1)) this.buffer.resize(this.buffer.length() + BUFFER_INCREMENT_SIZE); - } - + } + this.drawLineInfoColumns(screenCursor); screenCursor.x+= metaColumnsTotalWidth; // Apparently drawing errors now makes them appear under the editor text; // we must remember values and draw separately. this.errorsScreenCursor = screenCursor; this.errorsTextAreaSize = textAreaSize; - + } void drawExceptionsPanel() { - + if (epanel.getHeightToReserve() == 0) { return; // panel is empty, do not draw - } - + } + // Draw the exceptions panel vector2 ePanelSize( - ImGui::GetWindowSize().x - (20), - epanel.getHeightToReserve()); + ImGui::GetWindowSize().x - (20), + epanel.getHeightToReserve()); ImGui::BeginChild("ExceptionsPanel", ePanelSize); epanel.drawExceptions(); ImGui::EndChild(); // must always be called - legacy reasons @@ -1148,18 +1148,18 @@ void drawExceptionsPanel() void drawFooter() { - + if (epanel.getHeightToReserve() == 0) { // make top gap (window padding) rougly same size as bottom gap (item spacing) - ImGui::SetCursorPosY(ImGui::GetCursorPosY()+2); + ImGui::SetCursorPosY(ImGui::GetCursorPosY()+2); } else { // Fix footer jumping down (??) - ImGui::SetCursorPosY(ImGui::GetCursorPosY()-3); + ImGui::SetCursorPosY(ImGui::GetCursorPosY()-3); } - + // footer with status info ImGui::Separator(); string runningTxt = "NOT RUNNING"; @@ -1175,7 +1175,7 @@ void drawFooter() +"/warn:"+this.messagesTotalWarn +"/info:"+this.messagesTotalInfo +"); scrollY="+scrollY+"/"+scrollMaxY+""); // X="+scrollX+"/"+scrollMaxX -} +} void setBuffer(string data) { @@ -1187,7 +1187,7 @@ void setBuffer(string data) if (this.buffer.length() < BUFFER_MIN_SIZE) this.buffer.resize(BUFFER_MIN_SIZE); this.analyzeLines(); - editorWindow.refreshLocalFileList(); + editorWindow.refreshLocalFileList(); } private void runBufferInternal() // do not invoke during drawing ~ use `requestRunBuffer` @@ -1195,17 +1195,17 @@ private void runBufferInternal() // do not invoke during drawing ~ use `requestR this.bufferMessageIDs.resize(0); // clear all this.messages.resize(0); // clear all this.epanel.clearExceptions(); - + this.backUpRegionFoldStates(); - this.unFoldAllRegionsInternal(); // OK to call here - we're already handling a request. - + this.unFoldAllRegionsInternal(); // OK to call here - we're already handling a request. + game.pushMessage(MSG_APP_LOAD_SCRIPT_REQUESTED, { {'filename', this.bufferName}, // Because we supply the buffer, this will serve only as display name {'buffer', this.buffer }, {'category', SCRIPT_CATEGORY_CUSTOM} }); waitingForManipEvent=true; - + this.restoreRegionFoldStates(); } @@ -1215,14 +1215,14 @@ private void stopBufferInternal() // do not invoke during drawing ~ use `request {'id', this.currentScriptUnitID} }); waitingForManipEvent=true; -} +} void onEventAngelScriptMsg(int scriptUnitId, int msgType, int row, int col, string sectionName, string message) { /*game.log("DBG '"+this.bufferName+"' onEventAngelScriptMsg(): scriptUnitId:" + scriptUnitId + ", msgType:" + msgType + ", row:" + row + ", col:" + col // ints + ", sectionName:" + sectionName + ", message:" + message); // strings*/ - + messages.insertLast({ {'type', msgType}, {'row',row}, {'col', col}, {'sectionName', sectionName}, {'message', message} }); this.analyzeMessages(); } @@ -1231,7 +1231,7 @@ void onEventAngelScriptManip(int manipType, int scriptUnitId, int scriptCategory { /*game.log ("DBG '"+this.bufferName+"'.onEventAngelScriptManip(): manipType:"+ manipType+", scriptUnitId:"+ scriptUnitId + ", scriptCategory:"+scriptCategory+", scriptName:"+scriptName+"; //this.waitingForManipEvent:"+this.waitingForManipEvent);*/ - + // Only handle LOADED manip if we're waiting for it if (manipType == ASMANIP_SCRIPT_LOADED && this.currentScriptUnitID == SCRIPTUNITID_INVALID @@ -1254,7 +1254,7 @@ void onEventAngelScriptManip(int manipType, int scriptUnitId, int scriptCategory this.currentScriptUnitID = SCRIPTUNITID_INVALID; waitingForManipEvent = false; } - + } void analyzeLines() @@ -1266,30 +1266,30 @@ void analyzeLines() private void analyzeBuffer() // helper for `analyzeLines()` { /* - // [profiling] + // [profiling] Ogre::Timer timer; uint totalMicrosecDict = 0; // the `bufferLinesMeta` array of dicts uint totalMicrosecCpuDict = 0; uint totalMicrosecRegions = 0; uint totalMicrosecCpuRegions = 0; - */ - + */ + this.bufferLinesMeta.resize(0); // clear all int startOffset = 0; this.totalChars = 0; - + // pattern - comment string commentPattern = "//"; uint commentLevel = 0; int commentStart = -1; bool commentFound = false; - + // pattern - hyperlink string httpPattern = 'http://'; uint httpLevel = 0; int httpStart = -1; bool httpFound = false; - + // pattern - #region & #endregion (C#-like, inside comment) // NOTE: Regions cannot be nested. '#[end]region' lines are always present in `buffer` even if folded. string regionPattern = '#region'; @@ -1303,23 +1303,23 @@ private void analyzeBuffer() // helper for `analyzeLines()` dictionary collectedRegions; int regionFoundAtLineIdx = -1; string regionFoundWithName; - int regionBodyStartOffset = -1; - + int regionBodyStartOffset = -1; + string httpBreakChars = " \n\t\"'"; bool httpBreakFound = false; int httpBreakPos = -1; - + int autoIndentLevelCurrent = 0; int autoIndentLevelNext = 0; int actualIndentBlanks = 0; // current line bool actualIndentFound = false; - + string SPECIALCHARS = "{}\n\t \0"; - + for (uint i = 0; i < this.buffer.length(); i++) - { + { uint lineIdx = this.bufferLinesMeta.length(); - + // analyze the character uint theChar = this.buffer[i]; bool isCharOpenBrace = theChar == SPECIALCHARS[0]; @@ -1328,7 +1328,7 @@ string SPECIALCHARS = "{}\n\t \0"; bool isCharTab = theChar == SPECIALCHARS[3]; bool isCharSpace = theChar == SPECIALCHARS[4]; bool isCharNul = theChar == SPECIALCHARS[5]; - + // doubleslash comment if (!commentFound) { @@ -1336,25 +1336,25 @@ string SPECIALCHARS = "{}\n\t \0"; && (httpStart == -1 || httpBreakFound)) // Either we're not in hyperlink yet or we're after it already.) { commentLevel++; - + // Record the '//', but make sure it's not within hyperlink! if (commentStart == -1) { commentStart = i; } - + if (uint(commentLevel) == commentPattern.length()) { commentFound = true; - } + } } else { commentLevel = 0; } } - else - { + else + { if (!regionFound) { //if (regionLevel > 0) game.log("DBG analyzeBuffer(): char="+i+", line="+lineIdx+", regionLevel="+regionLevel+"/"+regionPattern.length()); @@ -1372,10 +1372,10 @@ string SPECIALCHARS = "{}\n\t \0"; regionLevel = 0; } } - + if (!endregionFound) { - //if (endregionLevel > 0) game.log("DBG analyzeBuffer(): char="+i+", line="+lineIdx+", endregionLevel="+endregionLevel+"/"+endregionPattern.length()); + //if (endregionLevel > 0) game.log("DBG analyzeBuffer(): char="+i+", line="+lineIdx+", endregionLevel="+endregionLevel+"/"+endregionPattern.length()); if (this.buffer[i] == endregionPattern[endregionLevel]) { endregionLevel++; @@ -1390,23 +1390,23 @@ string SPECIALCHARS = "{}\n\t \0"; } } } - + // http protocol if (!httpFound) { if (this.buffer[i] == httpPattern[httpLevel]) { httpLevel++; - + if (httpStart == -1) { httpStart = i; } - + if (uint(httpLevel) >= httpPattern.length()) { httpFound = true; - } + } } else { @@ -1424,7 +1424,7 @@ string SPECIALCHARS = "{}\n\t \0"; } } } - + // Auto indentation - always execute to display the debug levels if (isCharOpenBrace) { @@ -1436,8 +1436,8 @@ string SPECIALCHARS = "{}\n\t \0"; autoIndentLevelNext--; autoIndentLevelCurrent--; } - - // Actual indentation - always execute + + // Actual indentation - always execute if (!actualIndentFound) { if (isCharSpace || isCharTab) @@ -1449,17 +1449,17 @@ string SPECIALCHARS = "{}\n\t \0"; actualIndentFound = true; } } - + if (isCharNewline || isCharNul) { int endOffset = i; - - /* - // [Profiling] + + /* + // [Profiling] uint microsecBeforeRegions = timer.getMicroseconds(); uint microsecCpuBeforeRegions = timer.getMicrosecondsCPU(); - */ - + */ + // Process #region if (regionFound) { @@ -1468,11 +1468,11 @@ string SPECIALCHARS = "{}\n\t \0"; regionBodyStartOffset = endOffset+1; //game.log("DBG analyzeBuffer(): regionFound: withName="+regionFoundWithName+" atLineIdx="+regionFoundAtLineIdx+" bodyStartOffset="+regionBodyStartOffset); } - + // Process #endregion if (endregionFound && regionFoundAtLineIdx != -1 && !collectedRegions.exists(regionFoundWithName)) { - RegionInfo regionInfo; + RegionInfo regionInfo; regionInfo.regionLineCount = (lineIdx-1)-regionFoundAtLineIdx; // To handle jumps in line numbering regionInfo.regionBodyStartOffset = regionBodyStartOffset; // To swap regions in and out from work buffer. regionInfo.regionBodyNumChars = (int(this.bufferLinesMeta[lineIdx-1]['endOffset']) - regionBodyStartOffset)+1; // ditto @@ -1482,17 +1482,17 @@ string SPECIALCHARS = "{}\n\t \0"; regionFoundAtLineIdx = -1; regionFoundWithName = ""; } - - /* - // [Profiling] + + /* + // [Profiling] totalMicrosecRegions += timer.getMicroseconds() - microsecBeforeRegions; totalMicrosecCpuRegions += timer.getMicrosecondsCPU() - microsecCpuBeforeRegions; - // Finish line + // Finish line uint microsecBeforeDict = timer.getMicroseconds(); uint microsecCpuBeforeDict = timer.getMicrosecondsCPU(); - */ - - this.bufferLinesMeta.insertLast({ {'startOffset', startOffset} }); + */ + + this.bufferLinesMeta.insertLast({ {'startOffset', startOffset} }); this.bufferLinesMeta[lineIdx]['endOffset'] = endOffset; int len = endOffset - startOffset; this.bufferLinesMeta[lineIdx]['len'] = len; @@ -1508,14 +1508,14 @@ string SPECIALCHARS = "{}\n\t \0"; this.bufferLinesMeta[lineIdx]['endregionFound'] = endregionFound; this.bufferLinesMeta[lineIdx]['autoIndentLevel'] = autoIndentLevelCurrent; this.bufferLinesMeta[lineIdx]['actualIndentBlanks'] = actualIndentBlanks; - - /* - // [Profiling] + + /* + // [Profiling] totalMicrosecDict += timer.getMicroseconds() - microsecBeforeDict; totalMicrosecCpuDict += timer.getMicrosecondsCPU() - microsecCpuBeforeDict; - */ + */ } - + if (isCharNul) { break; // We're done parsing the buffer - the rest is just more NULs. @@ -1526,41 +1526,41 @@ string SPECIALCHARS = "{}\n\t \0"; commentFound = false; commentStart = -1; // -1 means Empty commentLevel = 0; - + httpFound = false; - httpStart = -1; // -1 means Empty - httpLevel = 0; + httpStart = -1; // -1 means Empty + httpLevel = 0; httpBreakFound = false; httpBreakPos = -1; // -1 means Empty - + regionFound = false; regionTitleStart = -1; // -1 means Empty regionLevel = 0; - + endregionFound = false; endregionLevel = 0; - + actualIndentFound = false; actualIndentBlanks = 0; - + autoIndentLevelCurrent = autoIndentLevelNext; - + // Start new line startOffset = i+1; } - + this.totalChars++; } - - /* + + /* // [profiling] uint microsecBeforeMerging = timer.getMicroseconds(); uint microsecCpuBeforeMerging = timer.getMicrosecondsCPU(); - */ - + */ + this.mergeCollectedFoldingRegionsWithExisting(collectedRegions); - - /* + + /* // [profiling] uint totalMicrosecMerging = timer.getMicroseconds() - microsecBeforeMerging; uint totalMicrosecCpuMerging = timer.getMicrosecondsCPU() - microsecCpuBeforeMerging; @@ -1570,7 +1570,7 @@ string SPECIALCHARS = "{}\n\t \0"; +" dict "+totalMicrosecDict+"us (CPU "+totalMicrosecCpuDict+"us)" +" merging "+totalMicrosecMerging+"us (CPU "+totalMicrosecCpuMerging+"us)" ); - */ + */ } private void mergeCollectedFoldingRegionsWithExisting(dictionary&in collectedRegions) // helper for `analyzeBuffer()` @@ -1579,7 +1579,7 @@ private void mergeCollectedFoldingRegionsWithExisting(dictionary&in collectedReg // Either way, we simply scan the buffer again, collect the regions and then match them with those already existing. // This relies on the #region/#endregion tags always being in the buffer, even if folded. // --------------------------------------------------------------------------------------------------------------- - + // prune broken regions (missing '#endregion' -> RegionInfo is null) array@ collectedRegionNames = collectedRegions.getKeys(); for (uint i = 0; i< collectedRegionNames.length(); i++) @@ -1590,8 +1590,8 @@ private void mergeCollectedFoldingRegionsWithExisting(dictionary&in collectedReg //game.log ("DBG mergeCollectedFoldingRegionsWithExisting(): pruning broken region '" + collectedRegionNames[i] + "'"); collectedRegions.delete(collectedRegionNames[i]); } - } - + } + // Find regions that were deleted/changed array oldRegionNames = this.workBufferRegions.getKeys(); for (uint i = 0; i< oldRegionNames.length(); i++) @@ -1604,7 +1604,7 @@ private void mergeCollectedFoldingRegionsWithExisting(dictionary&in collectedReg oldRegionInfo.isOrphan = true; } } - + // Find regions that were (re)created array@ newRegionNames = collectedRegions.getKeys(); for (uint i = 0; i < newRegionNames.length(); i++) @@ -1624,9 +1624,9 @@ private void mergeCollectedFoldingRegionsWithExisting(dictionary&in collectedReg +" regionBodyNumChars="+existingRegionInfo.regionBodyNumChars+" (new:"+newRegionInfo.regionBodyNumChars+")" +" isOrphan="+existingRegionInfo.isOrphan+" isFolded="+newRegionInfo.isFolded); */ - + existingRegionInfo.regionBodyStartOffset = newRegionInfo.regionBodyStartOffset; - + if (!existingRegionInfo.isFolded) { //game.log("DBG mergeCollectedFoldingRegionsWithExisting(): An existing UNFOLDED region '"+newRegionNames[i]+"' was updated ~ text may have changed"); @@ -1640,7 +1640,7 @@ private void mergeCollectedFoldingRegionsWithExisting(dictionary&in collectedReg existingRegionInfo.isOrphan = false; } } - } + } } private void analyzeMessages() // helper for `analyzeLines()` @@ -1648,13 +1648,13 @@ private void analyzeMessages() // helper for `analyzeLines()` // reset caches bufferMessageIDs.resize(0); // clear all bufferMessageIDs.resize(bufferLinesMeta.length()); - + // reset global stats this.messagesTotalErr = 0; this.messagesTotalWarn = 0; this.messagesTotalInfo = 0; - - + + for (uint i = 0; i < this.messages.length(); i++) { int msgType = int(this.messages[i]['type']); @@ -1662,31 +1662,31 @@ private void analyzeMessages() // helper for `analyzeLines()` string inFoldedRegion = ""; uint lineIdx = this.determineLineIdxForMessage(i, /*[out]:*/inFoldedRegion); this.messages[i]['inFoldedRegion'] = inFoldedRegion; // HACK: add extra dynamic field where only constant data from AngelScript should be - + // BEGIN Check bounds (investigating exceptions) - if (msgType == asMSGTYPE_ERROR || msgType == asMSGTYPE_WARNING) - { - if (i >= bufferMessageIDs.length()) - { - game.log("DBG WARNING: analyzeMessages() `i` out of bounds, breaking loop (seems to happen each time new build errors arrive and there's more than last time)."); - game.log("DBG analyzeMessages() | i="+i+", Looping over this.messages.length()="+this.messages.length()+", writing to bufferMessageIDs.length()="+ bufferMessageIDs.length()); - game.log ("DBG analyzeMessages() | msgType is: ERROR=" + (msgType == asMSGTYPE_ERROR) + " / WARNING = " + (msgType == asMSGTYPE_WARNING)); - - break; - } - - // Check bounds (investigating a rare exception) - if (lineIdx >= bufferMessageIDs.length()) - { - game.log("DBG WARNING: analyzeMessages() `lineIdx` out of bounds, breaking loop."); - game.log("DBG analyzeMessages() | i="+i+", using lineIdx="+lineIdx+" to write into bufferMessageIDs.length()="+ bufferMessageIDs.length()); - game.log ("DBG analyzeMessages() | msgType is: ERROR=" + (msgType == asMSGTYPE_ERROR) + " / WARNING = " + (msgType == asMSGTYPE_WARNING)); - - break; - } - } - // END bounds check - + if (msgType == asMSGTYPE_ERROR || msgType == asMSGTYPE_WARNING) + { + if (i >= bufferMessageIDs.length()) + { + game.log("DBG WARNING: analyzeMessages() `i` out of bounds, breaking loop (seems to happen each time new build errors arrive and there's more than last time)."); + game.log("DBG analyzeMessages() | i="+i+", Looping over this.messages.length()="+this.messages.length()+", writing to bufferMessageIDs.length()="+ bufferMessageIDs.length()); + game.log ("DBG analyzeMessages() | msgType is: ERROR=" + (msgType == asMSGTYPE_ERROR) + " / WARNING = " + (msgType == asMSGTYPE_WARNING)); + + break; + } + + // Check bounds (investigating a rare exception) + if (lineIdx >= bufferMessageIDs.length()) + { + game.log("DBG WARNING: analyzeMessages() `lineIdx` out of bounds, breaking loop."); + game.log("DBG analyzeMessages() | i="+i+", using lineIdx="+lineIdx+" to write into bufferMessageIDs.length()="+ bufferMessageIDs.length()); + game.log ("DBG analyzeMessages() | msgType is: ERROR=" + (msgType == asMSGTYPE_ERROR) + " / WARNING = " + (msgType == asMSGTYPE_WARNING)); + + break; + } + } + // END bounds check + // update line stats if (msgType == asMSGTYPE_ERROR) { @@ -1711,12 +1711,12 @@ private uint determineLineIdxForMessage(uint msgIndex, string&inout inFoldedRegi // To determine the message's lineindex in the sparse `this.buffer`, we must go through region records and adjust the reported line number. // Note the `this.workBufferRegions` set isn't ordered, we must process all regions that precede the message, and only then the region which contains it (if any). // -------------------------------------------------------------------------------------------------------------------------------------------------------- - + int msgRow = int(this.messages[msgIndex]['row']); int lineIdx = msgRow-1; - + //game.log("DBG determineLineIdxForMessage("+msgIndex+"): initial lineIdx="+lineIdx); - + array regionKeys = this.workBufferRegions.getKeys(); RegionInfo@ containingRegion = null; string containingRegionName = ""; @@ -1726,10 +1726,10 @@ private uint determineLineIdxForMessage(uint msgIndex, string&inout inFoldedRegi if (@regionInfo != null) { //game.log("DBG determineLineIdxForMessage("+msgIndex+"): considering region "+i+"/"+regionKeys.length()+" '"+regionKeys[i]+"' (isFolded="+regionInfo.isFolded+" startsAtLineIndex="+regionInfo.regionStartsAtLineIndex+" lineCount="+regionInfo.regionLineCount+")"); - + bool lowerBoundReached = (regionInfo.regionStartsAtLineIndex < lineIdx); bool upperBoundReached = ((regionInfo.regionStartsAtLineIndex + regionInfo.regionLineCount) < lineIdx); - + if (regionInfo.isFolded) { if (!lowerBoundReached) @@ -1744,23 +1744,23 @@ private uint determineLineIdxForMessage(uint msgIndex, string&inout inFoldedRegi containingRegionName = regionKeys[i]; } } - + //game.log("DBG ... lowerBoundReached:"+lowerBoundReached+" upperBoundReached:"+upperBoundReached+" ="+(@containingRegion == @regionInfo)+"; new lineIdx="+lineIdx); } } - + if (@containingRegion!=null && containingRegion.isFolded) { lineIdx -= 1 + (lineIdx - containingRegion.regionStartsAtLineIndex); inFoldedRegion = containingRegionName; } - + // workaround: clamp the msgRow to known line count if (lineIdx >= int(bufferLinesMeta.length())) { lineIdx = int(bufferLinesMeta.length()) - 1; - } - + } + //game.log("DBG determineLineIdxForMessage("+msgIndex+"): final lineIdx="+lineIdx); return lineIdx; } @@ -1769,9 +1769,9 @@ void updateAutosave(float dt) { if (autoSaveIntervalSec == 0.f) return; // disabled - + this.autosaveTimeCounterSec+= dt; - this.autosaveResult = 0; + this.autosaveResult = 0; if (this.autosaveTimeCounterSec > autoSaveIntervalSec) { this.requestAutoSaveFile = true; @@ -1780,7 +1780,7 @@ void updateAutosave(float dt) } void drawAutosaveStatusbar() -{ +{ ImGui::TextDisabled("Autosave:"); ImGui::PushStyleColor(ImGuiCol_FrameBg, autoSaveStatusbarBgColor); ImGui::SetCursorPosY(ImGui::GetCursorPosY()+5.f); @@ -1788,7 +1788,7 @@ void drawAutosaveStatusbar() /*fraction:*/this.autosaveTimeCounterSec/autoSaveIntervalSec, /*size: */vector2(55.f, 13.f), /*overlay: */''+formatFloat(autoSaveIntervalSec-this.autosaveTimeCounterSec, "", 3, 1)+'sec'); - ImGui::PopStyleColor(1); // ImGuiCol_FrameBg + ImGui::PopStyleColor(1); // ImGuiCol_FrameBg } void handleRequests(float dt) @@ -1801,8 +1801,8 @@ void handleRequests(float dt) else if (this.requestUnFoldRegion != "") { //game.log("DBG requestUnFoldRegion: '"+ this.requestUnFoldRegion+"'"); - this.unFoldRegionInternal(this.requestUnFoldRegion); - this.requestUnFoldRegion = ""; + this.unFoldRegionInternal(this.requestUnFoldRegion); + this.requestUnFoldRegion = ""; } else if (this.requestFoldAll) { @@ -1819,7 +1819,7 @@ void handleRequests(float dt) { this.indentBufferInternal(); this.requestIndentBuffer = false; - } + } if (this.requestSaveFile) { this.saveFileInternal(); @@ -1829,7 +1829,7 @@ void handleRequests(float dt) { this.autosaveFileInternal(); this.requestAutoSaveFile = false; - } + } if (this.requestRunBuffer) { this.runBufferInternal(); @@ -1842,15 +1842,15 @@ void handleRequests(float dt) } } -private void saveFileInternal() // Do not invoke while drawing! use `requestSaveFile` +private void saveFileInternal() // Do not invoke while drawing! use `requestSaveFile` { // perform auto-indenting if desired if (autoIndentOnSave) this.indentBufferInternal(); // OK to call here - we're already handing a request. - + this.backUpRegionFoldStates(); this.unFoldAllRegionsInternal(); // OK to call here - we're already handling a request. - + // Write out the file string strData = this.buffer.substr(0, this.totalChars); bool savedOk = game.createTextResourceFromString( @@ -1858,17 +1858,17 @@ private void saveFileInternal() // Do not invoke while drawing! use `requestSave editorWindow.saveFileResult = savedOk ? 1 : -1; if (savedOk) { - editorWindow.addRecentScript(editorWindow.saveFileNameBuf); + editorWindow.addRecentScript(editorWindow.saveFileNameBuf); } - + this.restoreRegionFoldStates(); } private void autosaveFileInternal() // Do not invoke while drawing! use `requestAutoSaveFile` -{ +{ this.backUpRegionFoldStates(); this.unFoldAllRegionsInternal(); // OK to call here - we're already handling a request. - + // Generate filename: ${orig_name}_AUTOSAVE.$(orig_ext) int dotPos = bufferName.findLast("."); string autosaveFilename; @@ -1881,50 +1881,50 @@ private void autosaveFileInternal() // Do not invoke while drawing! use `request autosaveFilename = bufferName + "_AUTOSAVE"; } /*game.log ("DBG autosaveFileInternal: autosaveFilename='"+autosaveFilename+"'");*/ - + // Write out the file - string strData = this.buffer.substr(0, this.totalChars); + string strData = this.buffer.substr(0, this.totalChars); bool savedOk = game.createTextResourceFromString( strData, autosaveFilename, RGN_SCRIPTS, /*overwrite:*/true); this.autosaveResult = savedOk ? 1 : -1; this.restoreRegionFoldStates(); -} +} private void indentBufferInternal() // Do not invoke while drawing! use `requestIndentBuffer` { this.backUpRegionFoldStates(); this.unFoldAllRegionsInternal(); - + string stagingBuffer; for (uint i = 0; i < bufferLinesMeta.length(); i++) { // insert indent int autoIndentLevel = int(bufferLinesMeta[i]['autoIndentLevel']); for (int j=0; j dictionary{ fullDesc -> num occurences } -array nids; // Recorded NIDs - since each buffer has it's own panel instance now, there will never be more than 1 -int numLinesDrawn = 0; -// very important to keep the dict clean after requesting script unload -// - even after notification more exceptions can arrive -bool ePanelEnabled = true; - -ExceptionsPanel(ScriptEditorTab@ editorTab) -{ - @parentEditorTab = @editorTab; -} - -float getHeightToReserve() -{ -if (exception_stats_nid.isEmpty()) { return 0; } -else { return numLinesDrawn*ImGui::GetTextLineHeight() + 15; } -} - -void onEventExceptionCaught(int nid, string arg5_from, string arg6_type, string arg7_msg) -{ - if (parentEditorTab.currentScriptUnitID != nid) + ScriptEditorTab@ parentEditorTab; + dictionary exception_stats_nid; // NID-> dictionary{ fullDesc -> num occurences } + array nids; // Recorded NIDs - since each buffer has it's own panel instance now, there will never be more than 1 + int numLinesDrawn = 0; + // very important to keep the dict clean after requesting script unload + // - even after notification more exceptions can arrive + bool ePanelEnabled = true; + + ExceptionsPanel(ScriptEditorTab@ editorTab) { - /*game.log("DBG onEventExceptionCaught(nid="+nid+") IGNORING; (currentScriptUnitID="+parentEditorTab.currentScriptUnitID+" bufferName="+parentEditorTab.bufferName);*/ - return; + @parentEditorTab = @editorTab; } - - // format: FROM >> MSG (TYPE) - string desc=arg5_from+' --> '+arg7_msg +' ('+arg6_type+')'; - this.addExceptionInternal(nid, desc); -} -void onEventAngelscriptExceptionCallback(int nid, int arg3_linenum, string arg5_from, string arg6_msg) -{ - if (parentEditorTab.currentScriptUnitID != nid) + float getHeightToReserve() { - /*game.log("DBG onEventAngelscriptExceptionCallback(nid="+nid+") IGNORING; (currentScriptUnitID="+parentEditorTab.currentScriptUnitID+" bufferName="+parentEditorTab.bufferName);*/ - return; + if (exception_stats_nid.isEmpty()) { return 0; } + else { return numLinesDrawn*ImGui::GetTextLineHeight() + 15; } } - - // format: FROM >> MSG (line: LINENUM) - string desc = arg5_from+"() --> "+arg6_msg+" (line: "+arg3_linenum+")"; - this.addExceptionInternal(nid, desc); -} - -void addExceptionInternal(int nid, string desc) -{ - if (!ePanelEnabled) - return; - - // locate the dictionary for this script (by NID) - dictionary@ exception_stats = cast(exception_stats_nid[''+nid]); - if (@exception_stats == null) - { - exception_stats_nid[''+nid] = dictionary(); - @exception_stats = cast(exception_stats_nid[''+nid]); - this.nids.insertLast(nid); - } - exception_stats[desc] = int(exception_stats[desc])+1; -} -void drawExceptions() -{ - // Requests for editing entries - cannot be done while iterating! - bool requestClearAll = false; - dictionary@ requestClearEntryFromStats; - string requestClearEntryKey; - - numLinesDrawn=0; - int numEntriesDrawn=0; - for (uint i=0; i(exception_stats_nid[''+nids[i]]); - - if (@exception_stats == null) { continue; } // <----- continue, nothing to draw - - - ImGui::TextDisabled('NID '+nids[i]+' '); - if (@info != null) + void onEventExceptionCaught(int nid, string arg5_from, string arg6_type, string arg7_msg) + { + if (parentEditorTab.currentScriptUnitID != nid) { - ImGui::SameLine(); ImGui::Text(string(info['scriptName'])); + /*game.log("DBG onEventExceptionCaught(nid="+nid+") IGNORING; (currentScriptUnitID="+parentEditorTab.currentScriptUnitID+" bufferName="+parentEditorTab.bufferName);*/ + return; } - else + + // format: FROM >> MSG (TYPE) + string desc=arg5_from+' --> '+arg7_msg +' ('+arg6_type+')'; + this.addExceptionInternal(nid, desc); + } + + void onEventAngelscriptExceptionCallback(int nid, int arg3_linenum, string arg5_from, string arg6_msg) + { + if (parentEditorTab.currentScriptUnitID != nid) { - ImGui::SameLine(); ImGui::TextDisabled("(NOT RUNNING)"); + /*game.log("DBG onEventAngelscriptExceptionCallback(nid="+nid+") IGNORING; (currentScriptUnitID="+parentEditorTab.currentScriptUnitID+" bufferName="+parentEditorTab.bufferName);*/ + return; } - numLinesDrawn++; - - array@tag_keys = exception_stats.getKeys(); - ImGui::SameLine(); ImGui::Text(">>"); - ImGui::SameLine(); ImGui::TextColored( color(1,0.1, 0.2, 1), tag_keys.length() + ' exception type(s) encountered:'); - string clearall_btn_text = "Clear all"; - ImGui::SameLine(); - ImGui::SetCursorPosX(ImGui::GetWindowContentRegionMax().x - (ImGui::CalcTextSize(clearall_btn_text).x + 8.f)); - if (ImGui::SmallButton(clearall_btn_text)) + + // format: FROM >> MSG (line: LINENUM) + string desc = arg5_from+"() --> "+arg6_msg+" (line: "+arg3_linenum+")"; + this.addExceptionInternal(nid, desc); + } + + void addExceptionInternal(int nid, string desc) + { + if (!ePanelEnabled) + return; + + // locate the dictionary for this script (by NID) + dictionary@ exception_stats = cast(exception_stats_nid[''+nid]); + if (@exception_stats == null) { - requestClearAll = true; + exception_stats_nid[''+nid] = dictionary(); + @exception_stats = cast(exception_stats_nid[''+nid]); + this.nids.insertLast(nid); } - for (uint j=0; j < tag_keys.length(); j++) - { - ImGui::PushID(j); - - string descr = tag_keys[j]; - int tagcount = int(exception_stats[descr]); - if (ImGui::SmallButton("Clear")) + exception_stats[desc] = int(exception_stats[desc])+1; + } + + void drawExceptions() + { + // Requests for editing entries - cannot be done while iterating! + bool requestClearAll = false; + dictionary@ requestClearEntryFromStats; + string requestClearEntryKey; + + numLinesDrawn=0; + int numEntriesDrawn=0; + for (uint i=0; i(exception_stats_nid[''+nids[i]]); + + if (@exception_stats == null) { continue; } // <----- continue, nothing to draw + + + ImGui::TextDisabled('NID '+nids[i]+' '); + if (@info != null) { - /*game.log("DBG epanel requested clearing '"+descr+"' - button pressed");*/ - requestClearEntryKey = descr; - @requestClearEntryFromStats = @exception_stats; + ImGui::SameLine(); ImGui::Text(string(info['scriptName'])); + } + else + { + ImGui::SameLine(); ImGui::TextDisabled("(NOT RUNNING)"); } - ImGui::SameLine(); ImGui::TextDisabled('['+tagcount+']'); - ImGui::SameLine(); ImGui::Text(descr); - numLinesDrawn++; - numEntriesDrawn++; - - ImGui::PopID(); // j - } // for (tag_keys) - - ImGui::PopID(); // i - } // for (nids) - - // Requests for editing entries - cannot be done while iterating! - if (requestClearEntryKey != "" && @requestClearEntryFromStats != null) - { - /*game.log("DBG epanel requested clearing '"+requestClearEntryKey+"' - deleting from dict");*/ - if (!requestClearEntryFromStats.delete(requestClearEntryKey)) + + array@tag_keys = exception_stats.getKeys(); + ImGui::SameLine(); ImGui::Text(">>"); + ImGui::SameLine(); ImGui::TextColored( color(1,0.1, 0.2, 1), tag_keys.length() + ' exception type(s) encountered:'); + string clearall_btn_text = "Clear all"; + ImGui::SameLine(); + ImGui::SetCursorPosX(ImGui::GetWindowContentRegionMax().x - (ImGui::CalcTextSize(clearall_btn_text).x + 8.f)); + if (ImGui::SmallButton(clearall_btn_text)) + { + requestClearAll = true; + } + for (uint j=0; j < tag_keys.length(); j++) + { + ImGui::PushID(j); + + string descr = tag_keys[j]; + int tagcount = int(exception_stats[descr]); + if (ImGui::SmallButton("Clear")) + { + /*game.log("DBG epanel requested clearing '"+descr+"' - button pressed");*/ + requestClearEntryKey = descr; + @requestClearEntryFromStats = @exception_stats; + } + ImGui::SameLine(); ImGui::TextDisabled('['+tagcount+']'); + ImGui::SameLine(); ImGui::Text(descr); + + numLinesDrawn++; + numEntriesDrawn++; + + ImGui::PopID(); // j + } // for (tag_keys) + + ImGui::PopID(); // i + } // for (nids) + + // Requests for editing entries - cannot be done while iterating! + if (requestClearEntryKey != "" && @requestClearEntryFromStats != null) { - game.log("script_editor INTERNAL ERROR: requested clearing exception '"+requestClearEntryKey+"' failed - not found"); + /*game.log("DBG epanel requested clearing '"+requestClearEntryKey+"' - deleting from dict");*/ + if (!requestClearEntryFromStats.delete(requestClearEntryKey)) + { + game.log("script_editor INTERNAL ERROR: requested clearing exception '"+requestClearEntryKey+"' failed - not found"); + } + if (numEntriesDrawn == 1) + { + requestClearAll=true; // We just deleted the only entry -> close panel + } } - if (numEntriesDrawn == 1) + + if (requestClearAll) { - requestClearAll=true; // We just deleted the only entry -> close panel + /*game.log("DBG @@@ handling requestClearAll for buffer='"+parentEditorTab.bufferName+"' !!!");*/ + this.clearExceptions(); } - } - - if (requestClearAll) + + } // drawExceptions() + + void clearExceptions() { - /*game.log("DBG @@@ handling requestClearAll for buffer='"+parentEditorTab.bufferName+"' !!!");*/ - this.clearExceptions(); + /*game.log("DBG @@@ clearExceptions() called for buffer='"+parentEditorTab.bufferName+"' !!!");*/ + this.exception_stats_nid.deleteAll(); + this.numLinesDrawn=0; + this.nids = array(); // - since each buffer has it's own panel instance now, there will never be more than 1 } - -} // drawExceptions() - -void clearExceptions() -{ - /*game.log("DBG @@@ clearExceptions() called for buffer='"+parentEditorTab.bufferName+"' !!!");*/ - this.exception_stats_nid.deleteAll(); - this.numLinesDrawn=0; - this.nids = array(); // - since each buffer has it's own panel instance now, there will never be more than 1 -} } // class ExceptionsPanel // Code folding with `#region`/`#endregion` class RegionInfo { -int regionLineCount; -int regionBodyStartOffset; -int regionBodyNumChars; -int regionStartsAtLineIndex; - -bool isFolded; -bool isOrphan; // Not currently present in document, still checked for uniqueness. If re-entered as empty (no lines), can be unFolded. If conflicting, user gets notified by '{ }' invalid state marker. -string foldedOffText; -bool isFoldedBackup; // For temporary unfolding all during saving/indenting/running the buffer; use `backUpRegionFoldStates()` and `restoreRegionFoldStates()`. + int regionLineCount; + int regionBodyStartOffset; + int regionBodyNumChars; + int regionStartsAtLineIndex; + + bool isFolded; + bool isOrphan; // Not currently present in document, still checked for uniqueness. If re-entered as empty (no lines), can be unFolded. If conflicting, user gets notified by '{ }' invalid state marker. + string foldedOffText; + bool isFoldedBackup; // For temporary unfolding all during saving/indenting/running the buffer; use `backUpRegionFoldStates()` and `restoreRegionFoldStates()`. } /// Represents a pre-defined group of scripts to be displayed in menus. class ScriptIndexerRecord { -// DATA: -array@ fileinfos = null; // findResourceFileInfo() + added field 'scriptInfo' -uint fileinfos_analyzed = 0; -string rgname; -string pattern; + // DATA: + array@ fileinfos = null; // findResourceFileInfo() + added field 'scriptInfo' + uint fileinfos_analyzed = 0; + string rgname; + string pattern; -// FUNCS: + // FUNCS: -void scanResourceGroup(string rgname, string pattern = '*') -{ - @fileinfos = game.findResourceFileInfo(rgname, pattern); - this.rgname = rgname; - this.pattern = pattern; - fileinfos_analyzed = 0; - // analysis is done frame-by-frame in `advanceScriptAnalysis()` -} + void scanResourceGroup(string rgname, string pattern = '*') + { + @fileinfos = game.findResourceFileInfo(rgname, pattern); + this.rgname = rgname; + this.pattern = pattern; + fileinfos_analyzed = 0; + // analysis is done frame-by-frame in `advanceScriptAnalysis()` + } -scriptinfo_utils::ScriptInfo@ getScriptInfo(uint index) -{ - if (index < fileinfos.length() && fileinfos[index].exists('scriptInfo')) - { - return cast(fileinfos[index]['scriptInfo']); + scriptinfo_utils::ScriptInfo@ getScriptInfo(uint index) + { + if (index < fileinfos.length() && fileinfos[index].exists('scriptInfo')) + { + return cast(fileinfos[index]['scriptInfo']); + } + return null; } - return null; -} -// Returns TRUE if there was a script pending analysis. -bool advanceScriptAnalysis() -{ - if (fileinfos_analyzed < fileinfos.length()) - { - this.analyzeSingleScript(fileinfos_analyzed); - fileinfos_analyzed++; - return true; + // Returns TRUE if there was a script pending analysis. + bool advanceScriptAnalysis() + { + if (fileinfos_analyzed < fileinfos.length()) + { + this.analyzeSingleScript(fileinfos_analyzed); + fileinfos_analyzed++; + return true; + } + return false; } - return false; -} -void analyzeSingleScript(int index) -{ - string filename = string(fileinfos[index]['filename']); - string body = game.loadTextResourceAsString(filename, rgname); - scriptinfo_utils::ScriptInfo@ scriptinfo = scriptinfo_utils::ExtractScriptInfo(body); - /*game.log("DBG analyzeSingleScript("+index+"): File="+filename+'/RgName='+rgname - +": title="+scriptinfo.title+", brief="+scriptinfo.brief+", text="+scriptinfo.text);*/ - fileinfos[index]['scriptInfo'] = scriptinfo; - -} + void analyzeSingleScript(int index) + { + string filename = string(fileinfos[index]['filename']); + string body = game.loadTextResourceAsString(filename, rgname); + scriptinfo_utils::ScriptInfo@ scriptinfo = scriptinfo_utils::ExtractScriptInfo(body); + /*game.log("DBG analyzeSingleScript("+index+"): File="+filename+'/RgName='+rgname + +": title="+scriptinfo.title+", brief="+scriptinfo.brief+", text="+scriptinfo.text);*/ + fileinfos[index]['scriptInfo'] = scriptinfo; + + } } // class ScriptIndexerRecord // Helpers bool isChar(uint c, string s) { -return s.length() > 0 && c == s[0]; + return s.length() > 0 && c == s[0]; } bool isCharBlank(uint c) { -return isChar(c, " ") || isChar(c, "\t"); + return isChar(c, " ") || isChar(c, "\t"); } string trimLeft(string s) { -for (uint i = 0; i < s.length(); i++) -{ - if (!isCharBlank(s[i])) - return s.substr(i, s.length() - i); -} -return ""; + for (uint i = 0; i < s.length(); i++) + { + if (!isCharBlank(s[i])) + return s.substr(i, s.length() - i); + } + return ""; } RegionInfo@ findRegion(dictionary@ regionDict, string name) // Helper which checks first (not inserting NULL entry) { -if (regionDict.exists(name)) -return cast(regionDict[name]); -else -return null; + if (regionDict.exists(name)) + return cast(regionDict[name]); + else + return null; } -// Tutorial scripts +// Tutorial scripts // ----------------- // using 'heredoc' syntax; see https://www.angelcode.com/angelscript/sdk/docs/manual/doc_datatypes_strings.html @@ -2300,35 +2300,35 @@ float tt = 0.f; void main() { -// Uncomment to close window without asking. -//closeBtnHandler.cfgCloseImmediatelly = true; + // Uncomment to close window without asking. + //closeBtnHandler.cfgCloseImmediatelly = true; } // `frameStep()` runs every frame; `dt` is delta time in seconds. void frameStep(float dt) { -// Begin drawing window -if (ImGui::Begin("Tutorial script", closeBtnHandler.windowOpen, 0)) -{ - // Draw the "Terminate this script?" prompt on the top (if not disabled by config). - closeBtnHandler.draw(); - - // accumulate time - tt += dt; - - //#region format the output - string ttStr = "Total time: " + formatFloat(tt, "", 5, 2) + "sec"; - string dtStr = "Delta time: " + formatFloat(dt, "", 7, 4) + "sec"; - //#endregion - - //#region render the output - ImGui::Text(ttStr); - ImGui::SameLine(); - ImGui::Text(dtStr); - //#endregion - - // End drawing window - ImGui::End(); -} + // Begin drawing window + if (ImGui::Begin("Tutorial script", closeBtnHandler.windowOpen, 0)) + { + // Draw the "Terminate this script?" prompt on the top (if not disabled by config). + closeBtnHandler.draw(); + + // accumulate time + tt += dt; + + //#region format the output + string ttStr = "Total time: " + formatFloat(tt, "", 5, 2) + "sec"; + string dtStr = "Delta time: " + formatFloat(dt, "", 7, 4) + "sec"; + //#endregion + + //#region render the output + ImGui::Text(ttStr); + ImGui::SameLine(); + ImGui::Text(dtStr); + //#endregion + + // End drawing window + ImGui::End(); + } } """; From 51715429979ed129dc718238d9beb7af9d4b2328 Mon Sep 17 00:00:00 2001 From: ohlidalp Date: Sat, 30 Nov 2024 17:10:56 +0100 Subject: [PATCH 22/36] script_editor.as: Removed obtrusive scriptinfo lines in menus. Tooltips were polished: brighter title, no more "Title:" and "Brief:" prefixes, separator moved under title+brief --- resources/scripts/script_editor.as | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/resources/scripts/script_editor.as b/resources/scripts/script_editor.as index 1d6f6fb4e2..4ed4a2fc08 100644 --- a/resources/scripts/script_editor.as +++ b/resources/scripts/script_editor.as @@ -605,8 +605,9 @@ class ScriptEditorWindow if (hovered && (@scriptinfo != null)) { ImGui::BeginTooltip(); - ImGui::TextDisabled("Title:" + scriptinfo.title); ImGui::Separator(); - ImGui::Text("Brief:" + scriptinfo.brief); + ImGui::TextColored(lineNumberColor, scriptinfo.title); + ImGui::Text(scriptinfo.brief); + ImGui::Separator(); ImGui::TextWrapped(scriptinfo.text); ImGui::Dummy(vector2(300, 1)); ImGui::EndTooltip(); } @@ -615,19 +616,6 @@ class ScriptEditorWindow out_selection = filename; retval = true; } - // Extra line for brief description - if (@scriptinfo != null && scriptinfo.title != "") - { - ImGui::SetCursorPosX(ImGui::GetCursorPosX() + scriptInfoIndentWidth); - ImGui::TextColored(lineNumberColor, scriptinfo.title); - hovered = hovered || ImGui::IsItemHovered(); - if (scriptinfo.brief != "") - { - ImGui::SetCursorPosX(ImGui::GetCursorPosX() + scriptInfoIndentWidth); - ImGui::TextDisabled(scriptinfo.brief); - hovered = hovered || ImGui::IsItemHovered(); - } - } ImGui::PopID(); // i } // Always call a matching EndChild() for each BeginChild() call, regardless of its return value @@ -2274,7 +2262,7 @@ string trimLeft(string s) RegionInfo@ findRegion(dictionary@ regionDict, string name) // Helper which checks first (not inserting NULL entry) { if (regionDict.exists(name)) - return cast(regionDict[name]); + return cast(regionDict[name]); else return null; } From b4fdf82dfbb318b2b90bcbe6ba51dbe71fa1ce4c Mon Sep 17 00:00:00 2001 From: ohlidalp Date: Sat, 30 Nov 2024 17:46:13 +0100 Subject: [PATCH 23/36] GenericDoc editor example updated to use --- .../example_GenericDocument_uniEditor.as | 307 +++++------------- 1 file changed, 85 insertions(+), 222 deletions(-) diff --git a/resources/scripts/example_GenericDocument_uniEditor.as b/resources/scripts/example_GenericDocument_uniEditor.as index 3196aba2e0..54de064068 100644 --- a/resources/scripts/example_GenericDocument_uniEditor.as +++ b/resources/scripts/example_GenericDocument_uniEditor.as @@ -5,7 +5,10 @@ // Window [X] button handler #include "imgui_utils.as" imgui_utils::CloseWindowPrompt closeBtnHandler; - + +// genericDoc editor +#include "genericdoc_utils.as" + genericdoc_utils::GenericDocEditor gdEditor; int hoveredTokenPos = -1; int focusedTokenPos = -1; @@ -20,6 +23,8 @@ array g_terrain_tobj_files; // `frameStep()` runs every frame; `dt` is delta time in seconds. void frameStep(float dt) { + + string caption = "GenericDocument editor"; int flags = ImGuiWindowFlags_MenuBar; if (ImGui::Begin(caption, closeBtnHandler.windowOpen, flags)) @@ -45,15 +50,20 @@ void frameStep(float dt) if (@g_displayed_document != null) { + if (@g_displayed_document != @gdEditor.displayedDocument) + { + gdEditor.setDocument(g_displayed_document, g_displayed_doc_filename); + } + vector2 size = ImGui::GetWindowSize() - vector2(20, 150); ImGui::BeginChild("docBody", size); - drawDocumentBody(); + gdEditor.drawDocumentBody(); ImGui::EndChild(); ImGui::Separator(); - drawTokenEditPanel(); + gdEditor.drawTokenEditPanel(); } @@ -61,9 +71,10 @@ void frameStep(float dt) } } - -//#region Terrn+tobj doc viewing + + +//#region TOBJ viewing void loadTobjDocument(uint i) { TerrainClass@ terrain = game.getTerrain(); @@ -73,15 +84,47 @@ void loadTobjDocument(uint i) return; } GenericDocumentClass@ doc = GenericDocumentClass(); - int flags = GENERIC_DOCUMENT_OPTION_ALLOW_NAKED_STRINGS - | GENERIC_DOCUMENT_OPTION_ALLOW_SLASH_COMMENTS; - if (doc.loadFromResource(g_terrain_tobj_files[i], terrain.getTerrainFileResourceGroup(), flags)) + if (doc.loadFromResource(g_terrain_tobj_files[i], terrain.getTerrainFileResourceGroup(), genericdoc_utils::FLAGSET_TOBJ)) { @g_displayed_document = @doc; g_displayed_doc_filename = g_terrain_tobj_files[i]; } } + +void drawDiscoveredTobjFiles() +{ + // TOBJ files + ImGui::PushID("tobj"); + for (uint i = 0; i < g_terrain_tobj_files.length(); i++) + { + ImGui::PushID(i); + ImGui::AlignTextToFramePadding(); + ImGui::Bullet(); + ImGui::SameLine(); + ImGui::Text(g_terrain_tobj_files[i]); + ImGui::SameLine(); + if (@g_displayed_document == null) + { + if (ImGui::Button("View document")) + { + loadTobjDocument(i); + } + } + else + { + if (ImGui::Button("Close document")) + { + @g_displayed_document = null; + g_displayed_doc_filename = ""; + } + } + ImGui::PopID(); // i + } + ImGui::PopID(); //"tobj" +} +//#endregion +//#region Terrn doc viewing void loadTerrn2Document() { TerrainClass@ terrain = game.getTerrain(); @@ -91,38 +134,40 @@ void loadTerrn2Document() return; } GenericDocumentClass@ doc = GenericDocumentClass(); - int flags = 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; - if (doc.loadFromResource(terrain.getTerrainFileName(), terrain.getTerrainFileResourceGroup(), flags)) + + if (!doc.loadFromResource(terrain.getTerrainFileName(), terrain.getTerrainFileResourceGroup(), genericdoc_utils::FLAGSET_TERRN2)) { - @g_displayed_document = @doc; - g_displayed_doc_filename = terrain.getTerrainFileName(); - - // Fetch TOBJ filenames - if (g_terrain_tobj_files.length() == 0) + game.log("loadTerrn2Document() - could not load file"); + return; + } + + @g_displayed_document = @doc; + g_displayed_doc_filename = terrain.getTerrainFileName(); + + // Fetch TOBJ filenames + if (g_terrain_tobj_files.length() == 0) + { + GenericDocContextClass@ reader = GenericDocContextClass(doc); + bool in_section_objects = false; + bool in_section_races = false; + while (!reader.endOfFile()) { - GenericDocContextClass@ reader = GenericDocContextClass(doc); - bool in_section_objects = false; - while (!reader.endOfFile()) + if (reader.tokenType() == TOKEN_TYPE_KEYWORD && reader.getTokKeyword().substr(0, 1) == "[") { - if (reader.tokenType() == TOKEN_TYPE_KEYWORD && reader.getTokKeyword().substr(0, 1) == "[") - { - in_section_objects = (reader.getTokKeyword() == '[Objects]'); - } - else if (reader.tokenType() == TOKEN_TYPE_STRING && in_section_objects) - { - // Note: in GenericDocument, a text on line start is always a KEYWORD token, - // but KEYWORDs must not contain special characters, - // so file names always decay to strings because of '.'. - g_terrain_tobj_files.insertLast(reader.getTokString()); - } - reader.moveNext(); + in_section_objects = (reader.getTokKeyword() == '[Objects]'); + } + else if (reader.tokenType() == TOKEN_TYPE_STRING && in_section_objects) + { + // Note: in GenericDocument, a text on line start is always a KEYWORD token, + // but KEYWORDs must not contain special characters, + // so file names always decay to strings because of '.'. + g_terrain_tobj_files.insertLast(reader.getTokString()); } + + reader.moveNext(); } } + } void drawTerrainButtons() @@ -150,37 +195,10 @@ void drawTerrainButtons() } } + ImGui::TextDisabled("[Objects]"); + drawDiscoveredTobjFiles(); - // TOBJ files - ImGui::PushID("tobj"); - for (uint i = 0; i < g_terrain_tobj_files.length(); i++) - { - ImGui::PushID(i); - ImGui::AlignTextToFramePadding(); - ImGui::Bullet(); - ImGui::SameLine(); - ImGui::Text(g_terrain_tobj_files[i]); - ImGui::SameLine(); - if (@g_displayed_document == null) - { - if (ImGui::Button("View document")) - { - loadTobjDocument(i); - } - } - else - { - if (ImGui::Button("Close document")) - { - @g_displayed_document = null; - g_displayed_doc_filename = ""; - } - } - ImGui::PopID(); // i - } - ImGui::PopID(); //"tobj" - ImGui::PopID(); //"terrn" } @@ -205,12 +223,8 @@ void loadTruckDocument() if (ImGui::Button("View document")) { GenericDocumentClass@ doc = GenericDocumentClass(); - int flags = GENERIC_DOCUMENT_OPTION_ALLOW_NAKED_STRINGS - | GENERIC_DOCUMENT_OPTION_ALLOW_SLASH_COMMENTS - | GENERIC_DOCUMENT_OPTION_FIRST_LINE_IS_TITLE - | GENERIC_DOCUMENT_OPTION_ALLOW_SEPARATOR_COLON - | GENERIC_DOCUMENT_OPTION_PARENTHESES_CAPTURE_SPACES; - if (doc.loadFromResource(actor.getTruckFileName(), actor.getTruckFileResourceGroup(), flags)) + + if (doc.loadFromResource(actor.getTruckFileName(), actor.getTruckFileResourceGroup(), genericdoc_utils::FLAGSET_TRUCK)) { @g_displayed_document = @doc; g_displayed_doc_filename = actor.getTruckFileName(); @@ -235,12 +249,8 @@ void drawTruckDocumentControls() if (ImGui::Button("View document")) { GenericDocumentClass@ doc = GenericDocumentClass(); - int flags = GENERIC_DOCUMENT_OPTION_ALLOW_NAKED_STRINGS - | GENERIC_DOCUMENT_OPTION_ALLOW_SLASH_COMMENTS - | GENERIC_DOCUMENT_OPTION_FIRST_LINE_IS_TITLE - | GENERIC_DOCUMENT_OPTION_ALLOW_SEPARATOR_COLON - | GENERIC_DOCUMENT_OPTION_PARENTHESES_CAPTURE_SPACES; - if (doc.loadFromResource(actor.getTruckFileName(), actor.getTruckFileResourceGroup(), flags)) + + if (doc.loadFromResource(actor.getTruckFileName(), actor.getTruckFileResourceGroup(), genericdoc_utils::FLAGSET_TRUCK)) { @g_displayed_document = @doc; g_displayed_doc_filename = actor.getTruckFileName(); @@ -265,152 +275,5 @@ void drawTruckDocumentControls() } //#endregion -//#region Document view -void drawDocumentBody() -{ - ImGui::PushID("docBody"); - bool hover_found = false; - - GenericDocContextClass reader(g_displayed_document); - 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()); - break; - } - case TOKEN_TYPE_INT: - { - 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_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() -{ - if ( focusedTokenPos == -1) - { - ImGui::TextDisabled("Use left mouse button to select tokens."); - return; // nothing to draw - } - GenericDocContextClass reader(g_displayed_document); - while (!reader.endOfFile() && reader.getPos() != uint(focusedTokenPos)) reader.moveNext(); - if (reader.endOfFile()) - { - ImGui::Text("EOF!!"); - } - else - { - ImGui::TextDisabled("Token pos: "); ImGui::SameLine(); ImGui::Text("" + reader.getPos()); - 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 "?"; -} - - //#endregion - - From e16730d35b0056fe4fce3863f6b8abdea43055ca Mon Sep 17 00:00:00 2001 From: ohlidalp Date: Sat, 30 Nov 2024 20:01:12 +0100 Subject: [PATCH 24/36] angelscript shaderParams example: don't crash game. FIXME: this example was created for Tritonas00's sky shader which was later reverted. --- .../scripts/example_ogre_shaderParams.as | 41 +++++++++++++------ 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/resources/scripts/example_ogre_shaderParams.as b/resources/scripts/example_ogre_shaderParams.as index 0a03a79385..18886997d0 100644 --- a/resources/scripts/example_ogre_shaderParams.as +++ b/resources/scripts/example_ogre_shaderParams.as @@ -1,5 +1,6 @@ /// \title Shader parameters demo /// \brief Adjust constant shader parameters - DEMO uses sky material. +///FIXME = this was created for Tritonas00's sky shader which was later reverted, won't work in plain master. // =================================================== // Window [X] button handler @@ -20,26 +21,40 @@ void frameStep(float dt) { closeBtnHandler.draw(); - if (ImGui::SliderFloat("Sun size", /*[inout]*/ sun_size, 0.1f, 3.0f)) + if (sky_material.isNull()) { - - sky_params.setNamedConstant("sun_size", sun_size); + ImGui::Text("ERROR - could not retrieve sky material"); } - if (ImGui::SliderFloat("Cloud density", /*[inout]*/ cloud_density, -0.3f, 0.3f)) + if (sky_params.isNull()) { - - sky_params.setNamedConstant("cloud_density", cloud_density); - + ImGui::Text("ERROR - could not retrieve sky shader params"); + ImGui::TextDisabled("FIXME: this example was created for Tritonas00's sky shader which was later reverted."); } - if (ImGui::SliderFloat("Sky lightness", /*[inout]*/ sky_light, 0.0f, 0.9f)) + else { - sky_params.setNamedConstant("sky_light", sky_light); - //TBD game.getSceneManager().setAmbientLight(Ogre::ColourValue(sky_light, sky_light, sky_light)); - - } - + if (ImGui::SliderFloat("Sun size", /*[inout]*/ sun_size, 0.1f, 3.0f)) + { + + sky_params.setNamedConstant("sun_size", sun_size); + + } + if (ImGui::SliderFloat("Cloud density", /*[inout]*/ cloud_density, -0.3f, 0.3f)) + { + + sky_params.setNamedConstant("cloud_density", cloud_density); + + } + if (ImGui::SliderFloat("Sky lightness", /*[inout]*/ sky_light, 0.0f, 0.9f)) + { + + sky_params.setNamedConstant("sky_light", sky_light); + //TBD game.getSceneManager().setAmbientLight(Ogre::ColourValue(sky_light, sky_light, sky_light)); + + } + } + ImGui::End(); } } From 0176a98722d4c4a0fcb0c66d363c8702c4ace43a Mon Sep 17 00:00:00 2001 From: ohlidalp Date: Sat, 30 Nov 2024 20:31:05 +0100 Subject: [PATCH 25/36] angelscript ogre overlays example: investigating crashes. Crashes on me under Win10, DirectX9 although used to work OK... I added a lot of checks and buttons to test slow, at one point it worked but then stopped again... crashes somewhere in OGRE overlay system, I can't easily debug at the moemnt, so I'm just posting the updated script. --- resources/scripts/example_ogre_overlays.as | 143 ++++++++++++++------- 1 file changed, 96 insertions(+), 47 deletions(-) diff --git a/resources/scripts/example_ogre_overlays.as b/resources/scripts/example_ogre_overlays.as index f55ebd35a8..ba98ae30a7 100644 --- a/resources/scripts/example_ogre_overlays.as +++ b/resources/scripts/example_ogre_overlays.as @@ -16,75 +16,124 @@ imgui_utils::CloseWindowPrompt closeBtnHandler; // overlay string ovName = "ov"+thisScript; +bool ov_buttonpressed=false; bool ov_fail = false; // panel string paName = "pa"+thisScript; +bool pa_buttonpressed=false; bool pa_fail = false; -int paNumCreates = 0; +bool pa_ready=false; // misc int framecounter = 0; float pos_step = 50; // pixels void frameStep(float dt) { - Ogre::Overlay@ ov; - if (!ov_fail) - { - // NOTE: getByName() will calmly return NULL if overlay doesn't exist. - @ov = Ogre::OverlayManager::getSingleton().getByName(ovName); - // CAUTION: attempt to create the same overlay again will throw an exception, interrupting the script in between! - if (@ov == null) - { - @ov = Ogre::OverlayManager::getSingleton().create(ovName); - } - if (@ov == null) + if (ImGui::Begin("Example", closeBtnHandler.windowOpen, 0)) + { + closeBtnHandler.draw(); + + ImGui::TextDisabled("A very crude test of creating OGRE overlays in code"); + ImGui::TextDisabled("Creates one overlay '"+ovName+"' + one panel '"+paName+"'"); + ImGui::TextDisabled("and leaves them dangling until you restart the game :)"); + ImGui::TextDisabled("(Note: Names contain scriptUnitID (NID) to avoid clashes and interferences)."); + + if (!ov_buttonpressed) { - ov_fail = true; + // there's no such API like "does overlay exist? --> boolean", we must retrieve, fortunately it behaves calmly. + Ogre::Overlay@ ovCheck = Ogre::OverlayManager::getSingleton().getByName(ovName); + if (@ovCheck == null) + { + if (ImGui::SmallButton("create overlay")) + { + ov_buttonpressed=true; + } + } + else + { + ImGui::Text("It seems the overlay already exists ?!"); + } } - else + + Ogre::Overlay@ ov; + if (ov_buttonpressed && !ov_fail) { - ov.show(); + // NOTE: getByName() will calmly return NULL if overlay doesn't exist. + @ov = Ogre::OverlayManager::getSingleton().getByName(ovName); + // CAUTION: attempt to create the same overlay again will throw an exception, interrupting the script in between! + if (@ov == null) + { + if (Ogre::OverlayManager::getSingleton().create(ovName) == null) // intentionally not assigning - we want to retrieve from OGRE + { + ov_fail = true; + game.log("OGRE overlaymanager create() returned null"); + } + else + { + Ogre::Overlay@ ovTest = Ogre::OverlayManager::getSingleton().getByName(ovName); + if (@ovTest == null) + { + game.log("Overlay not created by OGRE"); + ov_fail = true; + } + } + } + else + { + ov.show(); + } } - } - - Ogre::OverlayElement@ pa; - - if (!pa_fail ) - { - if ( Ogre::OverlayManager::getSingleton().hasOverlayElement(paName)) + + Ogre::OverlayElement@ pa; + + if (!pa_buttonpressed) { - game.log('Looking for pa'); - // CAUTION: getOverlayElement() will throw exception if not found, always do `hasOverlayElement()` first! - @pa = Ogre::OverlayManager::getSingleton().getOverlayElement(paName); - + ImGui::Text("OGRE OverlayManager .hasOverlayElement(paName)--> " + Ogre::OverlayManager::getSingleton().hasOverlayElement(paName)); + if (ImGui::SmallButton("create panel")) { pa_buttonpressed=true; } } - // CAUTION: using the same name twice will throw an exception, interrupting the script in between! - if (@pa == null ) + + if (pa_buttonpressed && !ov_fail && !pa_fail) { - @pa = Ogre::OverlayManager::getSingleton().createOverlayElement("Panel", paName); - paNumCreates++; - game.log('paNumCreates:'+paNumCreates); - if (@pa == null ) - { - pa_fail=true; + if ( Ogre::OverlayManager::getSingleton().hasOverlayElement(paName)) + { + // CAUTION: getOverlayElement() will throw exception if not found, always do `hasOverlayElement()` first! + + @pa = Ogre::OverlayManager::getSingleton().getOverlayElement(paName); + if (@pa == null) + { + game.log("OGRE reports panel exists, but cannot be retrieved"); + pa_fail = true; + } + else if (@ov != null && !pa_ready && ImGui::SmallButton("setup the panel")) + { + game.log("adding pa to ov"); + ov.add2D(pa); + pa.setMetricsMode(Ogre::GMM_PIXELS); + pa.setPosition(100,100); + pa.setDimensions(100,100); + pa.setMaterialName("tracks/wheelface", 'OgreAutodetect'); + pa.show(); + pa_ready=true; + } + } - else + // CAUTION: using the same name twice will throw an exception, interrupting the script in between! + else if (ImGui::SmallButton("create panel")) { - // game.log("adding pa to ov"); - ov.add2D(pa); - pa.setMetricsMode(Ogre::GMM_PIXELS); - pa.setPosition(100,100); - pa.setDimensions(100,100); - pa.setMaterialName("tracks/wheelface", 'OgreAutodetect'); - pa.show(); + game.log("creating pa"); + Ogre::OverlayManager::getSingleton().createOverlayElement("Panel", paName); // intentionally not assigning, we want to look up. + Ogre::OverlayElement@ paTest = Ogre::OverlayManager::getSingleton().getOverlayElement(paName); + if (@paTest == null ) + { + game.log("Panel not created by OGRE"); + pa_fail=true; + } + } + } - } - - if (ImGui::Begin("Example", closeBtnHandler.windowOpen, 0)) - { - closeBtnHandler.draw(); + ImGui::Text("overlays should work; ov_fail:"+ov_fail+", pa_fail:"+pa_fail +", frames:"+framecounter); framecounter++; if (!pa_fail && @pa != null) From c2ab86c32f755370c00e9fa3d61009b30144e155 Mon Sep 17 00:00:00 2001 From: ohlidalp Date: Sat, 30 Nov 2024 20:31:31 +0100 Subject: [PATCH 26/36] truck editing example script: improved comments --- resources/scripts/example_actor_RigEditorAlpha.as | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/resources/scripts/example_actor_RigEditorAlpha.as b/resources/scripts/example_actor_RigEditorAlpha.as index c46836c677..6a4b8a4f72 100644 --- a/resources/scripts/example_actor_RigEditorAlpha.as +++ b/resources/scripts/example_actor_RigEditorAlpha.as @@ -1,7 +1,9 @@ /// \file Prototype truck editor, Oct 2023 -/// \brief Showcases truck file editing features +/// \brief Showcases "gdeditor_utils.as" and "gridviewer_utils.as" +/// +/// Lets you import ZIPped mod as project (directory under /projects). +/// Unfinished - no actual editing (to be worked into 'gdeditor_utils.as') /// see https://github.com/RigsOfRods/rigs-of-rods/pull/3048 -/// Written and auto-indented using script_editor.as! // =================================================== // Window [X] button handler From 4838b1640311397af01de28a09cba27fbaa4d151 Mon Sep 17 00:00:00 2001 From: ohlidalp Date: Tue, 3 Dec 2024 02:03:20 +0100 Subject: [PATCH 27/36] :angel:Script: added Ogre::Overlay doc, remade example. --- .../Script2Game/AngelOgre/AngelOgre_Overlay.h | 52 ++++ .../AngelOgre/AngelOgre_OverlayElement.h | 73 ++++++ .../AngelOgre/AngelOgre_OverlayManager.h | 46 ++++ resources/scripts/example_ogre_overlays.as | 246 ++++++++---------- 4 files changed, 283 insertions(+), 134 deletions(-) create mode 100644 doc/angelscript/Script2Game/AngelOgre/AngelOgre_Overlay.h create mode 100644 doc/angelscript/Script2Game/AngelOgre/AngelOgre_OverlayElement.h create mode 100644 doc/angelscript/Script2Game/AngelOgre/AngelOgre_OverlayManager.h diff --git a/doc/angelscript/Script2Game/AngelOgre/AngelOgre_Overlay.h b/doc/angelscript/Script2Game/AngelOgre/AngelOgre_Overlay.h new file mode 100644 index 0000000000..98ffdff39e --- /dev/null +++ b/doc/angelscript/Script2Game/AngelOgre/AngelOgre_Overlay.h @@ -0,0 +1,52 @@ + +namespace AngelOgre { // Dummy namespace, just to distinguish AngelScript from C++ + +/** \addtogroup ScriptSideAPIs + * @{ + */ + +/** \addtogroup Script2Game + * @{ + */ + + /** + */ + class Overlay + { + public: + // (order roughly matches OgreOverlay.h) + + const string& getName() const; + // > z-order + uint16 getZOrder(); + // > visibility + bool isVisible() const; + void show(); + void hide(); + // > 2D elements + void add2D(OverlayElement@); + void remove2D(OverlayElement@); + // > scrolling + void setScroll(float, float); + float getScrollX() const; + float getScrollY() const; + void scroll(float, float); + // > rotating + void setRotate(const radian&in); + const radian& getRotate() const; + void rotate(const radian&in); + // > scaling + void setScale(float, float); + float getScaleX() const; + float getScaleY() const; + // > 2D elements + array@ get2DElements(); + + }; + +/// @} //addtogroup Script2Game +/// @} //addtogroup ScriptSideAPIs + +} // namespace AngelOgre (dummy, just to distinguish AngelScript from C++) + + diff --git a/doc/angelscript/Script2Game/AngelOgre/AngelOgre_OverlayElement.h b/doc/angelscript/Script2Game/AngelOgre/AngelOgre_OverlayElement.h new file mode 100644 index 0000000000..9988d750ca --- /dev/null +++ b/doc/angelscript/Script2Game/AngelOgre/AngelOgre_OverlayElement.h @@ -0,0 +1,73 @@ + +namespace AngelOgre { // Dummy namespace, just to distinguish AngelScript from C++ + +/** \addtogroup ScriptSideAPIs + * @{ + */ + +/** \addtogroup Script2Game + * @{ + */ + + // Register the GuiMetricsMode enum + enum GuiMetricsMode + { + GMM_PIXELS, + GMM_RELATIVE, + GMM_RELATIVE_ASPECT_ADJUSTED + } + + // Register the GuiHorizontalAlignment enum + enum GuiHorizontalAlignment + { + GHA_LEFT, + GHA_CENTER, + GHA_RIGHT + } + + + class OverlayElement + { + public: + // (order roughly matches OgreOverlayElement.h) + const string& getName() const; + // > visibility + void show(); + void hide(); + bool isVisible() const; + // > positioning + void setPosition(float, float); + void setDimensions(float, float); + float getLeft() const; + float getTop() const; + float getWidth() const; + float getHeight() const; + void setLeft(float); + void setTop(float); + void setWidth(float); + void setHeight(float); + // > material + const string& getMaterialName() const; + void setMaterialName(const string&in, const string&in); + // > caption + void setCaption(const string&in); + const string& getCaption() const; + // > color + void setColour(const color&in); + const color& getColour() const; + // > GuiMetricsMode + GuiMetricsMode getMetricsMode() const; + void setMetricsMode(GuiMetricsMode); + // > GuiHorizontalAlignment + GuiHorizontalAlignment getHorizontalAlignment() const; + void setHorizontalAlignment(GuiHorizontalAlignment); + + + }; + +/// @} //addtogroup Script2Game +/// @} //addtogroup ScriptSideAPIs + +} // namespace AngelOgre (dummy, just to distinguish AngelScript from C++) + + diff --git a/doc/angelscript/Script2Game/AngelOgre/AngelOgre_OverlayManager.h b/doc/angelscript/Script2Game/AngelOgre/AngelOgre_OverlayManager.h new file mode 100644 index 0000000000..8b052e8fde --- /dev/null +++ b/doc/angelscript/Script2Game/AngelOgre/AngelOgre_OverlayManager.h @@ -0,0 +1,46 @@ + +namespace AngelOgre { // Dummy namespace, just to distinguish AngelScript from C++ + +/** \addtogroup ScriptSideAPIs + * @{ + */ + +/** \addtogroup Script2Game + * @{ + */ + + /** a singleton - use `Ogre::OverlayManager::getSingleton()` to retrieve instance. + */ + class OverlayManager + { + public: + // Overlay (container) objects + Overlay@ create(const string&in name); + Overlay@ getByName(const string&in name); + void destroy(const string&in name); + void destroy(Overlay@ overlay); + void destroyAll(); + array@ getOverlays(); + // Utils + float getViewportHeight() const; + float getViewportWidth() const; + // OverlayElement objects + OverlayElement@ createOverlayElement(const string&in type, const string&in name, bool isTemplate=false); + OverlayElement@ getOverlayElement(const string&in name) const; + bool hasOverlayElement(const string&in) const; + void destroyOverlayElement(const string&in, bool isTemplate=false) const; + void destroyOverlayElement(OverlayElement@, bool isTemplate=false) const; + void destroyAllOverlayElements(bool isTemplate=false) const; + // Templates + OverlayElement@ createOverlayElementFromTemplate(const string&in, const string&in, const string&in, bool=false); + OverlayElement@ cloneOverlayElementFromTemplate(const string&in, const string&in); + array@ getTemplates(); + bool isTemplate(const string&in); + }; + +/// @} //addtogroup Script2Game +/// @} //addtogroup ScriptSideAPIs + +} // namespace AngelOgre (dummy, just to distinguish AngelScript from C++) + + diff --git a/resources/scripts/example_ogre_overlays.as b/resources/scripts/example_ogre_overlays.as index ba98ae30a7..440c55fd17 100644 --- a/resources/scripts/example_ogre_overlays.as +++ b/resources/scripts/example_ogre_overlays.as @@ -8,154 +8,132 @@ // OGRE objects don't use reference counting // - do not keep pointers longer than absolutely necessary! // Prefer always looking up the resource from OGRE - slower but safer. -// =================================================== +// =================================================== // Window [X] button handler #include "imgui_utils.as" imgui_utils::CloseWindowPrompt closeBtnHandler; - -// overlay -string ovName = "ov"+thisScript; -bool ov_buttonpressed=false; -bool ov_fail = false; -// panel -string paName = "pa"+thisScript; -bool pa_buttonpressed=false; -bool pa_fail = false; -bool pa_ready=false; -// misc -int framecounter = 0; -float pos_step = 50; // pixels -void frameStep(float dt) +void frameStep(float dt) { - if (ImGui::Begin("Example", closeBtnHandler.windowOpen, 0)) + if (ImGui::Begin("OGRE overlays", closeBtnHandler.windowOpen, 0)) { closeBtnHandler.draw(); - - ImGui::TextDisabled("A very crude test of creating OGRE overlays in code"); - ImGui::TextDisabled("Creates one overlay '"+ovName+"' + one panel '"+paName+"'"); - ImGui::TextDisabled("and leaves them dangling until you restart the game :)"); - ImGui::TextDisabled("(Note: Names contain scriptUnitID (NID) to avoid clashes and interferences)."); - - if (!ov_buttonpressed) + drawOverlayList(); + ImGui::End(); + } +} + +bool drawOverlayTreeNode( Ogre::Overlay@ ov) +{ + ImGui::PushID(ov.getName()+"-treenode"); + bool open = ImGui::TreeNode(ov.getName()); + + ImGui::SameLine(); + if (!ov.isVisible()) + { + if (ImGui::SmallButton("Show")) { - // there's no such API like "does overlay exist? --> boolean", we must retrieve, fortunately it behaves calmly. - Ogre::Overlay@ ovCheck = Ogre::OverlayManager::getSingleton().getByName(ovName); - if (@ovCheck == null) - { - if (ImGui::SmallButton("create overlay")) - { - ov_buttonpressed=true; - } - } - else - { - ImGui::Text("It seems the overlay already exists ?!"); - } + ov.show(); } - - Ogre::Overlay@ ov; - if (ov_buttonpressed && !ov_fail) - { - // NOTE: getByName() will calmly return NULL if overlay doesn't exist. - @ov = Ogre::OverlayManager::getSingleton().getByName(ovName); - // CAUTION: attempt to create the same overlay again will throw an exception, interrupting the script in between! - if (@ov == null) - { - if (Ogre::OverlayManager::getSingleton().create(ovName) == null) // intentionally not assigning - we want to retrieve from OGRE - { - ov_fail = true; - game.log("OGRE overlaymanager create() returned null"); - } - else - { - Ogre::Overlay@ ovTest = Ogre::OverlayManager::getSingleton().getByName(ovName); - if (@ovTest == null) - { - game.log("Overlay not created by OGRE"); - ov_fail = true; - } - } - } - else - { - ov.show(); - } + } + else + { + if (ImGui::SmallButton("Hide")) + { + ov.hide(); } - - Ogre::OverlayElement@ pa; - - if (!pa_buttonpressed) + } + + ImGui::PopID(); + return open; + +} + + +void drawOverlayElementDetails(Ogre::OverlayElement@ el) +{ + ImGui::PushStyleColor(ImGuiCol_Text, color(0.8, 0.8, 0.8, 1.0)); + ImGui::Bullet(); ImGui::Text("Left: " + el.getLeft()); + ImGui::Bullet(); ImGui::Text("Top: " + el.getTop()); + ImGui::Bullet(); ImGui::Text("Width:" + el.getWidth()); + ImGui::Bullet(); ImGui::Text("Height: " + el.getHeight()); + ImGui::PopStyleColor(); // Text +} + +void drawOverlayElementControls(Ogre::OverlayElement@ el) +{ + ImGui::PushID(el.getName() + "-controls"); + ImGui::PushStyleColor(ImGuiCol_Text, color(0.8, 0.8, 0.8, 1.0)); +float left=el.getLeft(); if (ImGui::InputFloat("Left", left)) { el.setLeft(left); } +float top=el.getTop(); if (ImGui::InputFloat("Top", top)) { el.setTop(top); } +float width=el.getWidth(); if (ImGui::InputFloat("Width", width) ) { el.setWidth(width); } +float height=el.getHeight(); if (ImGui::InputFloat("Height", height)) { el.setHeight(height); } + ImGui::PushID("-metric"); + ImGui::TextDisabled("MetricsMode:"); +if (ImGui::RadioButton("Pixels", el.getMetricsMode() == Ogre::GMM_PIXELS)) { el.setMetricsMode(Ogre::GMM_PIXELS); } +if (ImGui::RadioButton("Relative", el.getMetricsMode() == Ogre::GMM_RELATIVE)) { el.setMetricsMode(Ogre::GMM_RELATIVE); } +if (ImGui::RadioButton("Relative (aspect adjusted)", el.getMetricsMode() == Ogre::GMM_RELATIVE_ASPECT_ADJUSTED)) { el.setMetricsMode(Ogre::GMM_RELATIVE_ASPECT_ADJUSTED); } + ImGui::PopID(); + ImGui::PopID(); + ImGui::PopStyleColor(); // Text +} + +void drawOverlayElementList( Ogre::Overlay@ ov) +{ + ImGui::PushID(ov.getName()+"-elements"); + ImGui::PushStyleColor(ImGuiCol_Text, color(0.9, 0.8, 0.6, 1.0)); + + array@ elems = ov.get2DElements(); + for (uint i=0; i < elems.length(); i++) + { + if (drawOverlayElementTreeNode(elems[i])) { - ImGui::Text("OGRE OverlayManager .hasOverlayElement(paName)--> " + Ogre::OverlayManager::getSingleton().hasOverlayElement(paName)); - if (ImGui::SmallButton("create panel")) { pa_buttonpressed=true; } + drawOverlayElementControls(elems[i]); + ImGui::TreePop(); } - - if (pa_buttonpressed && !ov_fail && !pa_fail) + } + + ImGui::PopStyleColor(); // Text + ImGui::PopID(); +} + +bool drawOverlayElementTreeNode(Ogre::OverlayElement@ el) +{ + ImGui::PushID(el.getName()+"-treenode"); + bool open = ImGui::TreeNode(el.getName()); + ImGui::SameLine(); + ImGui::TextDisabled("(Material: "+el.getMaterialName()+")"); + ImGui::SameLine(); + if (!el.isVisible()) + { + if (ImGui::SmallButton("Show")) { - if ( Ogre::OverlayManager::getSingleton().hasOverlayElement(paName)) - { - // CAUTION: getOverlayElement() will throw exception if not found, always do `hasOverlayElement()` first! - - @pa = Ogre::OverlayManager::getSingleton().getOverlayElement(paName); - if (@pa == null) - { - game.log("OGRE reports panel exists, but cannot be retrieved"); - pa_fail = true; - } - else if (@ov != null && !pa_ready && ImGui::SmallButton("setup the panel")) - { - game.log("adding pa to ov"); - ov.add2D(pa); - pa.setMetricsMode(Ogre::GMM_PIXELS); - pa.setPosition(100,100); - pa.setDimensions(100,100); - pa.setMaterialName("tracks/wheelface", 'OgreAutodetect'); - pa.show(); - pa_ready=true; - } - - } - // CAUTION: using the same name twice will throw an exception, interrupting the script in between! - else if (ImGui::SmallButton("create panel")) - { - game.log("creating pa"); - Ogre::OverlayManager::getSingleton().createOverlayElement("Panel", paName); // intentionally not assigning, we want to look up. - Ogre::OverlayElement@ paTest = Ogre::OverlayManager::getSingleton().getOverlayElement(paName); - if (@paTest == null ) - { - game.log("Panel not created by OGRE"); - pa_fail=true; - } - - } - + el.show(); } - - - ImGui::Text("overlays should work; ov_fail:"+ov_fail+", pa_fail:"+pa_fail +", frames:"+framecounter); - framecounter++; - if (!pa_fail && @pa != null) + } + else + { + if (ImGui::SmallButton("Hide")) { - - ImGui::TextDisabled("The wheel overlay:"); - if (ImGui::Button("Hide")) { pa.hide(); } - ImGui::SameLine(); if (ImGui::Button("Show")) { pa.show(); } - - if (ImGui::Button("Position: Left+")) { pa.setLeft(pa.getLeft()+pos_step); } - ImGui::SameLine(); if (ImGui::Button("Position:left-")) { pa.setLeft(pa.getLeft()-pos_step); } - - if (ImGui::Button("Position: Top+")) { pa.setTop(pa.getTop()+pos_step); } - ImGui::SameLine(); if (ImGui::Button("Position:Top-")) { pa.setTop(pa.getTop()-pos_step); } - - if (ImGui::Button("Width+")) { pa.setWidth(pa.getWidth()+pos_step); } - ImGui::SameLine(); if (ImGui::Button("Width-")) { pa.setWidth(pa.getWidth()-pos_step); } - - if (ImGui::Button("height+")) { pa.setHeight(pa.getHeight()+pos_step); } - ImGui::SameLine(); if (ImGui::Button("height-")) { pa.setHeight(pa.getHeight()-pos_step); } - + el.hide(); } - ImGui::End(); - } -} + } + ImGui::PopID(); + return open; +} + +void drawOverlayList() +{ + array@ overlays = Ogre::OverlayManager::getSingleton().getOverlays(); + ImGui::TextDisabled("There are " + overlays.length() + " overlays:"); + for (uint i=0; i Date: Tue, 3 Dec 2024 11:36:22 +0100 Subject: [PATCH 28/36] :triangular_ruler: Find&replace ScriptUnitID_t Consistency++; --- source/main/ForwardDeclarations.h | 4 +-- source/main/gui/panels/GUI_ScriptMonitor.cpp | 8 +++--- source/main/main.cpp | 4 +-- source/main/physics/ActorManager.cpp | 4 +-- source/main/scripting/GameScript.cpp | 18 ++++++------ source/main/scripting/GameScript.h | 8 +++--- source/main/scripting/ScriptEngine.cpp | 30 ++++++++++---------- source/main/scripting/ScriptEngine.h | 30 ++++++++++---------- source/main/system/ConsoleCmd.cpp | 2 +- source/main/terrain/TerrainObjectManager.cpp | 2 +- 10 files changed, 55 insertions(+), 55 deletions(-) diff --git a/source/main/ForwardDeclarations.h b/source/main/ForwardDeclarations.h index 5d32022c6a..bacbea78fb 100644 --- a/source/main/ForwardDeclarations.h +++ b/source/main/ForwardDeclarations.h @@ -37,8 +37,8 @@ namespace RoR typedef int ActorInstanceID_t; //!< Unique sequentially generated ID of an actor in session. Use `ActorManager::GetActorById()` static const ActorInstanceID_t ACTORINSTANCEID_INVALID = 0; - typedef int ScriptUnitId_t; //!< Unique sequentially generated ID of a loaded and running scriptin session. Use `ScriptEngine::getScriptUnit()` - static const ScriptUnitId_t SCRIPTUNITID_INVALID = -1; + typedef int ScriptUnitID_t; //!< Unique sequentially generated ID of a loaded and running scriptin session. Use `ScriptEngine::getScriptUnit()` + static const ScriptUnitID_t SCRIPTUNITID_INVALID = -1; typedef int PointidID_t; //!< index to `PointColDetector::hit_pointid_list`, use `RoR::POINTIDID_INVALID` as empty value. static const PointidID_t POINTIDID_INVALID = -1; diff --git a/source/main/gui/panels/GUI_ScriptMonitor.cpp b/source/main/gui/panels/GUI_ScriptMonitor.cpp index 426a69e0bc..293eb3ba20 100644 --- a/source/main/gui/panels/GUI_ScriptMonitor.cpp +++ b/source/main/gui/panels/GUI_ScriptMonitor.cpp @@ -49,7 +49,7 @@ void ScriptMonitor::Draw() StringVector autoload = StringUtil::split(App::app_custom_scripts->getStr(), ","); for (auto& pair : App::GetScriptEngine()->getScriptUnits()) { - ScriptUnitId_t id = pair.first; + ScriptUnitID_t id = pair.first; ImGui::PushID(id); ScriptUnit const& unit = pair.second; @@ -74,7 +74,7 @@ void ScriptMonitor::Draw() { if (ImGui::Button(_LC("ScriptMonitor", "Reload"))) { - App::GetGameContext()->PushMessage(Message(MSG_APP_UNLOAD_SCRIPT_REQUESTED, new ScriptUnitId_t(id))); + App::GetGameContext()->PushMessage(Message(MSG_APP_UNLOAD_SCRIPT_REQUESTED, new ScriptUnitID_t(id))); LoadScriptRequest* req = new LoadScriptRequest(); req->lsr_category = unit.scriptCategory; req->lsr_filename = unit.scriptName; @@ -83,7 +83,7 @@ void ScriptMonitor::Draw() ImGui::SameLine(); if (ImGui::Button(_LC("ScriptMonitor", "Stop"))) { - App::GetGameContext()->PushMessage(Message(MSG_APP_UNLOAD_SCRIPT_REQUESTED, new ScriptUnitId_t(id))); + App::GetGameContext()->PushMessage(Message(MSG_APP_UNLOAD_SCRIPT_REQUESTED, new ScriptUnitID_t(id))); } ImGui::SameLine(); @@ -100,7 +100,7 @@ void ScriptMonitor::Draw() default:; } - ImGui::PopID(); // ScriptUnitId_t id + ImGui::PopID(); // ScriptUnitID_t id } if (App::app_recent_scripts->getStr() != "") diff --git a/source/main/main.cpp b/source/main/main.cpp index bf9ae47148..a23121ce0b 100644 --- a/source/main/main.cpp +++ b/source/main/main.cpp @@ -466,7 +466,7 @@ int main(int argc, char *argv[]) try { ActorPtr actor = App::GetGameContext()->GetActorManager()->GetActorById(request->lsr_associated_actor); - ScriptUnitId_t nid = App::GetScriptEngine()->loadScript(request->lsr_filename, request->lsr_category, actor, request->lsr_buffer); + ScriptUnitID_t nid = App::GetScriptEngine()->loadScript(request->lsr_filename, request->lsr_category, actor, request->lsr_buffer); // we want to notify any running scripts that we might change something (prevent cheating) App::GetScriptEngine()->triggerEvent(SE_ANGELSCRIPT_MANIPULATIONS, ASMANIP_SCRIPT_LOADED, nid, (int)request->lsr_category, 0, request->lsr_filename); @@ -481,7 +481,7 @@ int main(int argc, char *argv[]) case MSG_APP_UNLOAD_SCRIPT_REQUESTED: { - ScriptUnitId_t* id = static_cast(m.payload); + ScriptUnitID_t* id = static_cast(m.payload); try { ScriptUnit& unit = App::GetScriptEngine()->getScriptUnit(*id); diff --git a/source/main/physics/ActorManager.cpp b/source/main/physics/ActorManager.cpp index 321adda268..9cf76e951c 100644 --- a/source/main/physics/ActorManager.cpp +++ b/source/main/physics/ActorManager.cpp @@ -906,13 +906,13 @@ void ActorManager::DeleteActorInternal(ActorPtr actor) #endif // USE_SOCKETW // Unload actor's scripts - std::vector unload_list; + std::vector unload_list; for (auto& pair : App::GetScriptEngine()->getScriptUnits()) { if (pair.second.associatedActor == actor) unload_list.push_back(pair.first); } - for (ScriptUnitId_t id : unload_list) + for (ScriptUnitID_t id : unload_list) { App::GetScriptEngine()->unloadScript(id); } diff --git a/source/main/scripting/GameScript.cpp b/source/main/scripting/GameScript.cpp index 421c2f9051..6c7b72ab9b 100644 --- a/source/main/scripting/GameScript.cpp +++ b/source/main/scripting/GameScript.cpp @@ -286,7 +286,7 @@ void GameScript::registerForEvent(int eventValue) { if (App::GetScriptEngine()) { - ScriptUnitId_t unit_id = App::GetScriptEngine()->getCurrentlyExecutingScriptUnit(); + ScriptUnitID_t unit_id = App::GetScriptEngine()->getCurrentlyExecutingScriptUnit(); if (unit_id != SCRIPTUNITID_INVALID) { App::GetScriptEngine()->getScriptUnit(unit_id).eventMask |= eventValue; @@ -298,7 +298,7 @@ void GameScript::unRegisterEvent(int eventValue) { if (App::GetScriptEngine()) { - ScriptUnitId_t unit_id = App::GetScriptEngine()->getCurrentlyExecutingScriptUnit(); + ScriptUnitID_t unit_id = App::GetScriptEngine()->getCurrentlyExecutingScriptUnit(); if (unit_id != SCRIPTUNITID_INVALID) { App::GetScriptEngine()->getScriptUnit(unit_id).eventMask &= ~eventValue; @@ -306,7 +306,7 @@ void GameScript::unRegisterEvent(int eventValue) } } -BitMask_t GameScript::getRegisteredEventsMask(ScriptUnitId_t nid) +BitMask_t GameScript::getRegisteredEventsMask(ScriptUnitID_t nid) { if (App::GetScriptEngine()->scriptUnitExists(nid)) return App::GetScriptEngine()->getScriptUnit(nid).eventMask; @@ -314,7 +314,7 @@ BitMask_t GameScript::getRegisteredEventsMask(ScriptUnitId_t nid) return BitMask_t(0); } -void GameScript::setRegisteredEventsMask(ScriptUnitId_t nid, BitMask_t eventMask) +void GameScript::setRegisteredEventsMask(ScriptUnitID_t nid, BitMask_t eventMask) { if (App::GetScriptEngine()->scriptUnitExists(nid)) App::GetScriptEngine()->getScriptUnit(nid).eventMask = eventMask; @@ -808,7 +808,7 @@ int GameScript::useOnlineAPI(const String& apiquery, const AngelScript::CScriptD if (App::app_disable_online_api->getBool()) return 0; - ScriptUnitId_t unit_id = App::GetScriptEngine()->getCurrentlyExecutingScriptUnit(); + ScriptUnitID_t unit_id = App::GetScriptEngine()->getCurrentlyExecutingScriptUnit(); if (unit_id == SCRIPTUNITID_INVALID) return 2; @@ -971,7 +971,7 @@ int GameScript::deleteScriptVariable(const String& arg) return App::GetScriptEngine()->deleteVariable(arg); } -int GameScript::getScriptVariable(ScriptUnitId_t nid, const Ogre::String& varName, void *ref, int refTypeId) +int GameScript::getScriptVariable(ScriptUnitID_t nid, const Ogre::String& varName, void *ref, int refTypeId) { return App::GetScriptEngine()->getVariable(nid, varName, ref, refTypeId); } @@ -991,7 +991,7 @@ int GameScript::sendGameCmd(const String& message) AngelScript::CScriptArray* GameScript::getRunningScripts() { - std::vector ids; + std::vector ids; for (auto& pair: App::GetScriptEngine()->getScriptUnits()) ids.push_back(pair.first); @@ -999,7 +999,7 @@ AngelScript::CScriptArray* GameScript::getRunningScripts() } -AngelScript::CScriptDictionary* GameScript::getScriptDetails(ScriptUnitId_t nid) +AngelScript::CScriptDictionary* GameScript::getScriptDetails(ScriptUnitID_t nid) { if (!App::GetScriptEngine()->scriptUnitExists(nid)) return nullptr; @@ -1482,7 +1482,7 @@ bool GameScript::pushMessage(MsgType type, AngelScript::CScriptDictionary* dict) { return false; } - m.payload = new ScriptUnitId_t(static_cast(id)); + m.payload = new ScriptUnitID_t(static_cast(id)); break; } diff --git a/source/main/scripting/GameScript.h b/source/main/scripting/GameScript.h index dcc4a65f36..7c21169475 100644 --- a/source/main/scripting/GameScript.h +++ b/source/main/scripting/GameScript.h @@ -207,14 +207,14 @@ class GameScript * Gets event mask for a specific running script. Intended for diagnostic and monitoring purposes. * @param nid ScriptUnitID, obtain one from global var `thisScript` or callback parameters. */ - BitMask_t getRegisteredEventsMask(ScriptUnitId_t nid); + BitMask_t getRegisteredEventsMask(ScriptUnitID_t nid); /** * Overwrites event mask for a specific running script. Intended for debugging tools - use with caution. * @param nid ScriptUnitID, obtain one from global var `thisScript` or callback parameters. * @param eventMask \see enum scriptEvents */ - void setRegisteredEventsMask(ScriptUnitId_t nid, BitMask_t eventMask); + void setRegisteredEventsMask(ScriptUnitID_t nid, BitMask_t eventMask); /** * Adds a global function to the script @@ -259,7 +259,7 @@ class GameScript * @param refTypeId Type of the reference; To be registered as variable-type parameter `?&out` * @return 0 on success, negative number on error. */ - int getScriptVariable(ScriptUnitId_t nid, const Ogre::String& varName, void *ref, int refTypeId); + int getScriptVariable(ScriptUnitID_t nid, const Ogre::String& varName, void *ref, int refTypeId); void clearEventCache(); @@ -281,7 +281,7 @@ class GameScript * * "eventMask" (int64) * * "scriptBuffer" (string) */ - AngelScript::CScriptDictionary* getScriptDetails(ScriptUnitId_t nid); + AngelScript::CScriptDictionary* getScriptDetails(ScriptUnitID_t nid); /// @} diff --git a/source/main/scripting/ScriptEngine.cpp b/source/main/scripting/ScriptEngine.cpp index 52154fbef2..4822820c37 100644 --- a/source/main/scripting/ScriptEngine.cpp +++ b/source/main/scripting/ScriptEngine.cpp @@ -274,7 +274,7 @@ void ScriptEngine::forwardExceptionAsScriptEvent(const std::string& from) catch (...) { TRIGGER_EVENT_ASYNC(SE_GENERIC_EXCEPTION_CAUGHT, m_currently_executing_script_unit,0,0,0, from); } } -int ScriptEngine::executeContextAndHandleErrors(ScriptUnitId_t nid) +int ScriptEngine::executeContextAndHandleErrors(ScriptUnitID_t nid) { // Helper for executing any script function/snippet; // * sets LineCallback (on demand - when script registers for SE_ANGELSCRIPT_LINECALLBACK event) @@ -361,7 +361,7 @@ int ScriptEngine::executeContextAndHandleErrors(ScriptUnitId_t nid) return result; } -bool ScriptEngine::prepareContextAndHandleErrors(ScriptUnitId_t nid, int asFunctionID) +bool ScriptEngine::prepareContextAndHandleErrors(ScriptUnitID_t nid, int asFunctionID) { asIScriptFunction* scriptFunc = this->engine->GetFunctionById(asFunctionID); if (!scriptFunc) @@ -397,7 +397,7 @@ void ScriptEngine::framestep(Real dt) for (auto& pair: m_script_units) { - ScriptUnitId_t nid = pair.first; + ScriptUnitID_t nid = pair.first; if (m_script_units[nid].frameStepFunctionPtr) { // Set the function pointer and arguments @@ -417,7 +417,7 @@ int ScriptEngine::fireEvent(std::string instanceName, float intensity) for (auto& pair: m_script_units) { - ScriptUnitId_t id = pair.first; + ScriptUnitID_t id = pair.first; AngelScript::asIScriptFunction* func = m_script_units[id].scriptModule->GetFunctionByDecl( "void fireEvent(string, float)"); // TODO: this shouldn't be hard coded --neorej16 @@ -451,7 +451,7 @@ void ScriptEngine::envokeCallback(int _functionId, eventsource_t *source, NodeNu for (auto& pair: m_script_units) { - ScriptUnitId_t id = pair.first; + ScriptUnitID_t id = pair.first; int functionId = _functionId; if (functionId <= 0 && (m_script_units[id].defaultEventCallbackFunctionPtr != nullptr)) { @@ -687,7 +687,7 @@ int ScriptEngine::deleteVariable(const String &arg) return index; } -int ScriptEngine::getVariable(ScriptUnitId_t nid, const Ogre::String& varName, void *ref, int refTypeId) +int ScriptEngine::getVariable(ScriptUnitID_t nid, const Ogre::String& varName, void *ref, int refTypeId) { if (!engine || !context) { @@ -786,7 +786,7 @@ int ScriptEngine::getVariable(ScriptUnitId_t nid, const Ogre::String& varName, v return -8; } -asIScriptFunction* ScriptEngine::getFunctionByDeclAndLogCandidates(ScriptUnitId_t nid, GetFuncFlags_t flags, const std::string& funcName, const std::string& fmtFuncDecl) +asIScriptFunction* ScriptEngine::getFunctionByDeclAndLogCandidates(ScriptUnitID_t nid, GetFuncFlags_t flags, const std::string& funcName, const std::string& fmtFuncDecl) { std::string decl = fmt::format(fmtFuncDecl, funcName); asIScriptFunction* retval = m_script_units[nid].scriptModule->GetFunctionByDecl(decl.c_str()); @@ -814,7 +814,7 @@ void ScriptEngine::triggerEvent(scriptEvents eventnum, int arg1, int arg2ex, int for (auto& pair: m_script_units) { - ScriptUnitId_t id = pair.first; + ScriptUnitID_t id = pair.first; asIScriptFunction* callback = m_script_units[id].eventCallbackExFunctionPtr; if (!callback) callback = m_script_units[id].eventCallbackFunctionPtr; @@ -850,12 +850,12 @@ void ScriptEngine::triggerEvent(scriptEvents eventnum, int arg1, int arg2ex, int } } -String ScriptEngine::composeModuleName(String const& scriptName, ScriptCategory origin, ScriptUnitId_t id) +String ScriptEngine::composeModuleName(String const& scriptName, ScriptCategory origin, ScriptUnitID_t id) { return fmt::format("{}(category:{},unique ID:{})", scriptName, ScriptCategoryToString(origin), id); } -ScriptUnitId_t ScriptEngine::loadScript( +ScriptUnitID_t ScriptEngine::loadScript( String scriptName, ScriptCategory category/* = ScriptCategory::TERRAIN*/, ActorPtr associatedActor /*= nullptr*/, std::string buffer /* =""*/) { @@ -864,9 +864,9 @@ ScriptUnitId_t ScriptEngine::loadScript( // A script unit is how Rigs of Rods organizes scripts from various sources. // Because the script is executed during loading, it's wrapping unit must // be created early, and removed if setup fails. - static ScriptUnitId_t id_counter = 0; + static ScriptUnitID_t id_counter = 0; - ScriptUnitId_t unit_id = id_counter++; + ScriptUnitID_t unit_id = id_counter++; auto itor_pair = m_script_units.insert(std::make_pair(unit_id, ScriptUnit())); m_script_units[unit_id].uniqueId = unit_id; m_script_units[unit_id].scriptName = scriptName; @@ -1060,7 +1060,7 @@ int ScriptEngine::setupScriptUnit(int unit_id) return mainfunc_result; } -void ScriptEngine::unloadScript(ScriptUnitId_t nid) +void ScriptEngine::unloadScript(ScriptUnitID_t nid) { if (this->scriptUnitExists(nid)) { @@ -1090,13 +1090,13 @@ void ScriptEngine::setForwardScriptLogToConsole(bool doForward) } } -bool ScriptEngine::scriptUnitExists(ScriptUnitId_t nid) +bool ScriptEngine::scriptUnitExists(ScriptUnitID_t nid) { return nid != SCRIPTUNITID_INVALID && m_script_units.find(nid) != m_script_units.end(); } -ScriptUnit& ScriptEngine::getScriptUnit(ScriptUnitId_t nid) +ScriptUnit& ScriptEngine::getScriptUnit(ScriptUnitID_t nid) { ROR_ASSERT(this->scriptUnitExists(nid)); return m_script_units[nid]; diff --git a/source/main/scripting/ScriptEngine.h b/source/main/scripting/ScriptEngine.h index 64f00101a3..872f610766 100644 --- a/source/main/scripting/ScriptEngine.h +++ b/source/main/scripting/ScriptEngine.h @@ -71,7 +71,7 @@ struct ScriptUnit ScriptUnit(); ~ScriptUnit(); - ScriptUnitId_t uniqueId = SCRIPTUNITID_INVALID; + ScriptUnitID_t uniqueId = SCRIPTUNITID_INVALID; ScriptCategory scriptCategory = ScriptCategory::INVALID; unsigned int eventMask = 0; //!< filter mask for script events AngelScript::asIScriptModule* scriptModule = nullptr; @@ -85,7 +85,7 @@ struct ScriptUnit Ogre::String scriptBuffer; }; -typedef std::map ScriptUnitMap; +typedef std::map ScriptUnitMap; struct LoadScriptRequest { @@ -133,14 +133,14 @@ class ScriptEngine : public Ogre::LogListener * @param buffer String with full script body; if empty, a file will be loaded as usual. * @return Unique ID of the script unit (because one script file can be loaded multiple times). */ - ScriptUnitId_t loadScript(Ogre::String scriptname, ScriptCategory category = ScriptCategory::TERRAIN, + ScriptUnitID_t loadScript(Ogre::String scriptname, ScriptCategory category = ScriptCategory::TERRAIN, ActorPtr associatedActor = nullptr, std::string buffer = ""); /** * Unloads a script * @param unique_id The script unit ID as returned by `loadScript()` */ - void unloadScript(ScriptUnitId_t unique_id); + void unloadScript(ScriptUnitID_t unique_id); /** * Calls the script's framestep function to be able to use timed things inside the script @@ -206,13 +206,13 @@ class ScriptEngine : public Ogre::LogListener * Retrieves a global variable from any running script * @returns 0 on success, negative number on error. */ - int getVariable(ScriptUnitId_t nid, const Ogre::String& varName, void *ref, int typeID); + int getVariable(ScriptUnitID_t nid, const Ogre::String& varName, void *ref, int typeID); /** * Finds a function by full declaration, and if not found, finds candidates by name and logs them to Angelscript.log * @return Angelscript function on success, null on error. */ - AngelScript::asIScriptFunction* getFunctionByDeclAndLogCandidates(ScriptUnitId_t nid, GetFuncFlags_t flags, const std::string& funcName, const std::string& fmtFuncDecl); + AngelScript::asIScriptFunction* getFunctionByDeclAndLogCandidates(ScriptUnitID_t nid, GetFuncFlags_t flags, const std::string& funcName, const std::string& fmtFuncDecl); int fireEvent(std::string instanceName, float intensity); @@ -234,10 +234,10 @@ class ScriptEngine : public Ogre::LogListener inline void SLOG(const char* msg) { this->scriptLog->logMessage(msg); } //!< Replacement of macro inline void SLOG(std::string msg) { this->scriptLog->logMessage(msg); } //!< Replacement of macro - bool scriptUnitExists(ScriptUnitId_t unique_id); - ScriptUnit& getScriptUnit(ScriptUnitId_t unique_id); - ScriptUnitId_t getTerrainScriptUnit() const { return m_terrain_script_unit; } //!< @return SCRIPTUNITID_INVALID if none exists. - ScriptUnitId_t getCurrentlyExecutingScriptUnit() const { return m_currently_executing_script_unit; } //!< @return SCRIPTUNITID_INVALID if none is executing right now. + bool scriptUnitExists(ScriptUnitID_t unique_id); + ScriptUnit& getScriptUnit(ScriptUnitID_t unique_id); + ScriptUnitID_t getTerrainScriptUnit() const { return m_terrain_script_unit; } //!< @return SCRIPTUNITID_INVALID if none exists. + ScriptUnitID_t getCurrentlyExecutingScriptUnit() const { return m_currently_executing_script_unit; } //!< @return SCRIPTUNITID_INVALID if none is executing right now. ScriptUnitMap const& getScriptUnits() const { return m_script_units; } protected: @@ -253,7 +253,7 @@ class ScriptEngine : public Ogre::LogListener /** * Packs name + important info to one string, for logging and reporting purposes. */ - Ogre::String composeModuleName(Ogre::String const& scriptName, ScriptCategory origin, ScriptUnitId_t id); + Ogre::String composeModuleName(Ogre::String const& scriptName, ScriptCategory origin, ScriptUnitID_t id); /** * Helper for `loadScript()`, does the actual building without worry about unit management. @@ -265,13 +265,13 @@ class ScriptEngine : public Ogre::LogListener * Helper for executing any script function/snippet; does `asIScriptContext::Prepare()` and reports any error. * @return true on success, false on error. */ - bool prepareContextAndHandleErrors(ScriptUnitId_t nid, int asFunctionID); + bool prepareContextAndHandleErrors(ScriptUnitID_t nid, int asFunctionID); /** * Helper for executing any script function/snippet; registers Line/Exception callbacks (on demand) and set currently executed NID; The `asIScriptContext::Prepare()` and setting args must be already done. * @return 0 on success, anything else on error. */ - int executeContextAndHandleErrors(ScriptUnitId_t nid); + int executeContextAndHandleErrors(ScriptUnitID_t nid); /// @} @@ -302,8 +302,8 @@ class ScriptEngine : public Ogre::LogListener Ogre::Log* scriptLog; GameScript m_game_script; ScriptUnitMap m_script_units; - ScriptUnitId_t m_terrain_script_unit = SCRIPTUNITID_INVALID; - ScriptUnitId_t m_currently_executing_script_unit = SCRIPTUNITID_INVALID; + ScriptUnitID_t m_terrain_script_unit = SCRIPTUNITID_INVALID; + ScriptUnitID_t m_currently_executing_script_unit = SCRIPTUNITID_INVALID; scriptEvents m_currently_executing_event_trigger = SE_NO_EVENTS; bool m_events_enabled = true; //!< Hack to enable fast shutdown without cleanup diff --git a/source/main/system/ConsoleCmd.cpp b/source/main/system/ConsoleCmd.cpp index 03a6796fc4..0cdc51f90f 100644 --- a/source/main/system/ConsoleCmd.cpp +++ b/source/main/system/ConsoleCmd.cpp @@ -412,7 +412,7 @@ class LoadScriptCmd : public ConsoleCmd } else { - ScriptUnitId_t id = App::GetScriptEngine()->loadScript(args[1], ScriptCategory::CUSTOM); + ScriptUnitID_t id = App::GetScriptEngine()->loadScript(args[1], ScriptCategory::CUSTOM); if (id == SCRIPTUNITID_INVALID) { reply_type = Console::CONSOLE_SYSTEM_ERROR; diff --git a/source/main/terrain/TerrainObjectManager.cpp b/source/main/terrain/TerrainObjectManager.cpp index 4ad8314d1a..50946059e1 100644 --- a/source/main/terrain/TerrainObjectManager.cpp +++ b/source/main/terrain/TerrainObjectManager.cpp @@ -953,7 +953,7 @@ bool TerrainObjectManager::LoadTerrainScript(const Ogre::String& filename) ROR_ASSERT(!m_angelscript_grouping_node); m_angelscript_grouping_node = m_terrn2_grouping_node->createChildSceneNode(filename); - ScriptUnitId_t result = App::GetScriptEngine()->loadScript(filename); + ScriptUnitID_t result = App::GetScriptEngine()->loadScript(filename); m_angelscript_grouping_node = nullptr; return result != SCRIPTUNITID_INVALID; From aa9a423472858286777691542ab05a471b8aaac0 Mon Sep 17 00:00:00 2001 From: ohlidalp Date: Wed, 4 Dec 2024 11:03:53 +0100 Subject: [PATCH 29/36] `KeywordToString()` - added missing 'flares3' --- source/main/resources/rig_def_fileformat/RigDef_File.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/source/main/resources/rig_def_fileformat/RigDef_File.cpp b/source/main/resources/rig_def_fileformat/RigDef_File.cpp index ca9715b941..1d09c7dc19 100644 --- a/source/main/resources/rig_def_fileformat/RigDef_File.cpp +++ b/source/main/resources/rig_def_fileformat/RigDef_File.cpp @@ -213,6 +213,7 @@ const char * KeywordToString(Keyword keyword) case Keyword::FIXES: return "fixes"; case Keyword::FLARES: return "flares"; case Keyword::FLARES2: return "flares2"; + case Keyword::FLARES3: return "flares3"; case Keyword::FLEXBODIES: return "flexbodies"; case Keyword::FLEXBODY_CAMERA_MODE: return "flexbody_camera_mode"; case Keyword::FLEXBODYWHEELS: return "flexbodywheels"; From 29d0642ebf42f0f7631dcd2d30e0ec1acd5325f5 Mon Sep 17 00:00:00 2001 From: ohlidalp Date: Thu, 5 Dec 2024 03:27:06 +0100 Subject: [PATCH 30/36] :angel:Script: :broom: Fixed return codes of script-manip funcs. Changes: * added + registered `enum ScriptRetCode` - used by all script manipulation funcs (add/get/delete | funcs/variables) * added func `scriptVariableExists()` to go with `scriptFunctionExists()` * added internal helper `validateScriptModule`- used by all script manipulation funcs (add/get/delete | funcs/variables) Following is analysis of former state (most but not all funcs returned 0 on success) ~ executeString() [script: not exported] - used to return 0 on success, 1 on internal error and negative number otherwise. ~ addFunction() [script: `game.addScriptFunction()`] - used to return 0 on success, 1 on internal error and negative number otherwise. ~ functionExists() [script: `game.scriptFunctionExists()`] - used to return function ID (always >0) on success, negative number on error. ~ deleteFunction() [script: `game.deleteScriptFunction()`] - used to return function ID (always >0) on success, negative number on error. ~ addVariable() [script: `game.addScriptVariable()` ] - used to return 0 on success, 1 on internal error and negative number otherwise. ~ variableExists() [script: `game.scriptVariableExists()`] - newly added, returns 0 on success and negative number otherwise. ~ deleteVariable() [script: `game.deleteScriptVariable()` ] - used to return 0 on success, 1 on internal error and negative number otherwise. ~ getVariable() [script: `game.getScriptVariable()` ] - recently added, returns 0 on success and negative number otherwise. --- doc/angelscript/Script2Game/GameScriptClass.h | 42 +- doc/angelscript/Script2Game/globals.h | 45 ++ source/main/ForwardDeclarations.h | 3 + source/main/scripting/GameScript.cpp | 29 +- source/main/scripting/GameScript.h | 19 +- source/main/scripting/ScriptEngine.cpp | 409 ++++++++---------- source/main/scripting/ScriptEngine.h | 82 +++- .../bindings/GameScriptAngelscript.cpp | 51 ++- 8 files changed, 416 insertions(+), 264 deletions(-) diff --git a/doc/angelscript/Script2Game/GameScriptClass.h b/doc/angelscript/Script2Game/GameScriptClass.h index 914d682a8a..9041a6184f 100644 --- a/doc/angelscript/Script2Game/GameScriptClass.h +++ b/doc/angelscript/Script2Game/GameScriptClass.h @@ -274,41 +274,59 @@ class GameScriptClass /** * Adds a global function to the script. * @param func the function to be added, e.g.: "void func() { log('works'); }" + * @param nid ScriptUnitID to act upon, or -2 to fallback to the terrain script (defined in .terrn2 [Scripts], or 'default.as') + * @return 0 on success, negative number on error. */ - int addScriptFunction(const string func); + ScriptRetCode addScriptFunction(const string func, ScriptUnitId_t nid = -2); /** * Checks if a global function exists * @param func the declaration of the function that should be checked for existance, e.g.: "void func()" + * @param nid ScriptUnitID to act upon, or -2 to fallback to the terrain script (defined in .terrn2 [Scripts], or 'default.as') + * @return 0 on success, negative number on error. */ - int scriptFunctionExists(const string func); + ScriptRetCode scriptFunctionExists(const string func, ScriptUnitId_t nid = -2); /** * Removes a global function from the script. * @param func the declaration of the function that should be removed, e.g.: "void func()" + * @param nid ScriptUnitID to act upon, or -2 to fallback to the terrain script (defined in .terrn2 [Scripts], or 'default.as') + * @return 0 on success, negative number on error. */ - int deleteScriptFunction(const string func); + ScriptRetCode deleteScriptFunction(const string func, ScriptUnitId_t nid = -2); /** * Adds a global variable to the script. * @param var the declaration of the variable that should be added, e.g.: "int missionState;" + * @param nid ScriptUnitID to act upon, or -2 to fallback to the terrain script (defined in .terrn2 [Scripts], or 'default.as') + * @return 0 on success, negative number on error. */ - int addScriptVariable(const string var); + ScriptRetCode addScriptVariable(const string var, ScriptUnitId_t nid = -2); + + /** + * Checks if a global variable exists in the script. + * @param var the declaration of the variable that should be removed, e.g.: "int missionState;" + * @param nid ScriptUnitID to act upon, or -2 to fallback to the terrain script (defined in .terrn2 [Scripts], or 'default.as') + * @return 0 on success, negative number on error. + */ + ScriptRetCode scriptVariableExists(const string var, ScriptUnitId_t nid = -2); /** * Removes a global variable from the script. * @param var the declaration of the variable that should be removed, e.g.: "int missionState;" + * @param nid ScriptUnitID to act upon, or -2 to fallback to the terrain script (defined in .terrn2 [Scripts], or 'default.as') + * @return 0 on success, negative number on error. */ - int deleteScriptVariable(const string var); + ScriptRetCode deleteScriptVariable(const string var, ScriptUnitId_t nid = -2); /** - * Retrieves a memory address of a global variable in any script. - * @param nid ScriptUnitID, ID of the running script, obtain one from global var `thisScript` or `game.getRunningScripts()` - * @param varName Name of the variable. Type must match the reference type. - * @param ref A variable-type parameter - accepts any reference. - * @return 0 on success, negative number on error. - */ - int getScriptVariable(ScriptUnitId_t nid, const string&in varName, ?&ref); + * Retrieves a memory address of a global variable in any script. + * @param nid ScriptUnitID to act upon, or -2 to fallback to the terrain script (defined in .terrn2 [Scripts], or 'default.as') + * @param varName Name of the variable. Type must match the reference type. + * @param ref A variable-type parameter - accepts any reference. + * @return 0 on success, negative number on error. + */ + ScriptRetCode getScriptVariable(const string&in varName, ?&ref, ScriptUnitId_t nid = -2); /** * Clears the event cache diff --git a/doc/angelscript/Script2Game/globals.h b/doc/angelscript/Script2Game/globals.h index aa7f918348..fc8c42c78e 100644 --- a/doc/angelscript/Script2Game/globals.h +++ b/doc/angelscript/Script2Game/globals.h @@ -666,6 +666,51 @@ enum MsgType MSG_EDI_CREATE_PROJECT_REQUESTED, //!< Creates a subdir under 'projects/', pre-populates it and adds to modcache. Params: 'name' (string), 'ext' (string, optional), 'source_entry' (CacheEntryClass@) }; +/// Binding of `RoR::ScriptRetCode`; Common return codes for script manipulation funcs (add/get/delete | funcs/variables) +enum ScriptRetCode +{ + + SCRIPTRETCODE_SUCCESS = AngelScript::asSUCCESS, //!< Generic success - 0 by common convention. + + // AngelScript technical codes + SCRIPTRETCODE_AS_ERROR = AngelScript::asERROR, + SCRIPTRETCODE_AS_CONTEXT_ACTIVE = AngelScript::asCONTEXT_ACTIVE, + SCRIPTRETCODE_AS_CONTEXT_NOT_FINISHED = AngelScript::asCONTEXT_NOT_FINISHED, + SCRIPTRETCODE_AS_CONTEXT_NOT_PREPARED = AngelScript::asCONTEXT_NOT_PREPARED, + SCRIPTRETCODE_AS_INVALID_ARG = AngelScript::asINVALID_ARG, + SCRIPTRETCODE_AS_NO_FUNCTION = AngelScript::asNO_FUNCTION, + SCRIPTRETCODE_AS_NOT_SUPPORTED = AngelScript::asNOT_SUPPORTED, + SCRIPTRETCODE_AS_INVALID_NAME = AngelScript::asINVALID_NAME, + SCRIPTRETCODE_AS_NAME_TAKEN = AngelScript::asNAME_TAKEN, + SCRIPTRETCODE_AS_INVALID_DECLARATION = AngelScript::asINVALID_DECLARATION, + SCRIPTRETCODE_AS_INVALID_OBJECT = AngelScript::asINVALID_OBJECT, + SCRIPTRETCODE_AS_INVALID_TYPE = AngelScript::asINVALID_TYPE, + SCRIPTRETCODE_AS_ALREADY_REGISTERED = AngelScript::asALREADY_REGISTERED, + SCRIPTRETCODE_AS_MULTIPLE_FUNCTIONS = AngelScript::asMULTIPLE_FUNCTIONS, + SCRIPTRETCODE_AS_NO_MODULE = AngelScript::asNO_MODULE, + SCRIPTRETCODE_AS_NO_GLOBAL_VAR = AngelScript::asNO_GLOBAL_VAR, + SCRIPTRETCODE_AS_INVALID_CONFIGURATION = AngelScript::asINVALID_CONFIGURATION, + SCRIPTRETCODE_AS_INVALID_INTERFACE = AngelScript::asINVALID_INTERFACE, + SCRIPTRETCODE_AS_CANT_BIND_ALL_FUNCTIONS = AngelScript::asCANT_BIND_ALL_FUNCTIONS, + SCRIPTRETCODE_AS_LOWER_ARRAY_DIMENSION_NOT_REGISTERED = AngelScript::asLOWER_ARRAY_DIMENSION_NOT_REGISTERED, + SCRIPTRETCODE_AS_WRONG_CONFIG_GROUP = AngelScript::asWRONG_CONFIG_GROUP, + SCRIPTRETCODE_AS_CONFIG_GROUP_IS_IN_USE = AngelScript::asCONFIG_GROUP_IS_IN_USE, + SCRIPTRETCODE_AS_ILLEGAL_BEHAVIOUR_FOR_TYPE = AngelScript::asILLEGAL_BEHAVIOUR_FOR_TYPE, + SCRIPTRETCODE_AS_WRONG_CALLING_CONV = AngelScript::asWRONG_CALLING_CONV, + SCRIPTRETCODE_AS_BUILD_IN_PROGRESS = AngelScript::asBUILD_IN_PROGRESS, + SCRIPTRETCODE_AS_INIT_GLOBAL_VARS_FAILED = AngelScript::asINIT_GLOBAL_VARS_FAILED, + SCRIPTRETCODE_AS_OUT_OF_MEMORY = AngelScript::asOUT_OF_MEMORY, + SCRIPTRETCODE_AS_MODULE_IS_IN_USE = AngelScript::asMODULE_IS_IN_USE, + + // RoR ScriptEngine return codes + SCRIPTRETCODE_UNSPECIFIED_ERROR = -1001, + SCRIPTRETCODE_ENGINE_NOT_CREATED = -1002, + SCRIPTRETCODE_CONTEXT_NOT_CREATED = -1003, + SCRIPTRETCODE_SCRIPTUNIT_NOT_EXISTS = -1004, + SCRIPTRETCODE_SCRIPTUNIT_NO_MODULE = -1005, + SCRIPTRETCODE_FUNCTION_NOT_EXISTS = -1006, +}; + } // namespace Script2Game /// @} //addtogroup Script2Game diff --git a/source/main/ForwardDeclarations.h b/source/main/ForwardDeclarations.h index bacbea78fb..723f0ec84e 100644 --- a/source/main/ForwardDeclarations.h +++ b/source/main/ForwardDeclarations.h @@ -39,6 +39,7 @@ namespace RoR typedef int ScriptUnitID_t; //!< Unique sequentially generated ID of a loaded and running scriptin session. Use `ScriptEngine::getScriptUnit()` static const ScriptUnitID_t SCRIPTUNITID_INVALID = -1; + static const ScriptUnitID_t SCRIPTUNITID_DEFAULT = -2; //!< The script defined in .terrn2 [Scripts], or 'default.as' ~ classic behavior. typedef int PointidID_t; //!< index to `PointColDetector::hit_pointid_list`, use `RoR::POINTIDID_INVALID` as empty value. static const PointidID_t POINTIDID_INVALID = -1; @@ -74,6 +75,8 @@ namespace RoR typedef int CommandkeyID_t; //!< Index into `Actor::ar_commandkeys` (BEWARE: indexed 1-MAX_COMMANDKEYS, 0 is invalid value, negative subscript of any size is acceptable, see `class CmdKeyArray` ). static const CommandkeyID_t COMMANDKEYID_INVALID = 0; + typedef int ScriptRetCode_t; //!< see enum `RoR::ScriptRetCode` - combines AngelScript codes and RoR internal codes. + class Actor; class ActorManager; class ActorSpawner; diff --git a/source/main/scripting/GameScript.cpp b/source/main/scripting/GameScript.cpp index 6c7b72ab9b..b5b65b0c07 100644 --- a/source/main/scripting/GameScript.cpp +++ b/source/main/scripting/GameScript.cpp @@ -946,34 +946,39 @@ void GameScript::boostCurrentTruck(float factor) } } -int GameScript::addScriptFunction(const String& arg) +int GameScript::addScriptFunction(const String& arg, ScriptUnitID_t nid) { - return App::GetScriptEngine()->addFunction(arg); + return App::GetScriptEngine()->addFunction(arg, nid); } -int GameScript::scriptFunctionExists(const String& arg) +int GameScript::scriptFunctionExists(const String& arg, ScriptUnitID_t nid) { - return App::GetScriptEngine()->functionExists(arg); + return App::GetScriptEngine()->functionExists(arg, nid); } -int GameScript::deleteScriptFunction(const String& arg) +int GameScript::deleteScriptFunction(const String& arg, ScriptUnitID_t nid) { - return App::GetScriptEngine()->deleteFunction(arg); + return App::GetScriptEngine()->deleteFunction(arg, nid); } -int GameScript::addScriptVariable(const String& arg) +int GameScript::addScriptVariable(const String& arg, ScriptUnitID_t nid) { - return App::GetScriptEngine()->addVariable(arg); + return App::GetScriptEngine()->addVariable(arg, nid); } -int GameScript::deleteScriptVariable(const String& arg) +int GameScript::scriptVariableExists(const String& arg, ScriptUnitID_t nid) { - return App::GetScriptEngine()->deleteVariable(arg); + return App::GetScriptEngine()->variableExists(arg, nid); } -int GameScript::getScriptVariable(ScriptUnitID_t nid, const Ogre::String& varName, void *ref, int refTypeId) +int GameScript::deleteScriptVariable(const String& arg, ScriptUnitID_t nid) { - return App::GetScriptEngine()->getVariable(nid, varName, ref, refTypeId); + return App::GetScriptEngine()->deleteVariable(arg, nid); +} + +int GameScript::getScriptVariable(const Ogre::String& varName, void *ref, int refTypeId, ScriptUnitID_t nid) +{ + return App::GetScriptEngine()->getVariable(varName, ref, refTypeId, nid); } int GameScript::sendGameCmd(const String& message) diff --git a/source/main/scripting/GameScript.h b/source/main/scripting/GameScript.h index 7c21169475..f77babf4d9 100644 --- a/source/main/scripting/GameScript.h +++ b/source/main/scripting/GameScript.h @@ -221,35 +221,42 @@ class GameScript * (Wrapper for ScriptEngine::addFunction) * @param arg A declaration for the function. */ - int addScriptFunction(const Ogre::String& arg); + ScriptRetCode_t addScriptFunction(const Ogre::String& arg, ScriptUnitID_t nid); /** * Checks if a global function exists in the script * (Wrapper for ScriptEngine::functionExists) * @param arg A declaration for the function. */ - int scriptFunctionExists(const Ogre::String& arg); + ScriptRetCode_t scriptFunctionExists(const Ogre::String& arg, ScriptUnitID_t nid); /** * Deletes a global function from the script * (Wrapper for ScriptEngine::deleteFunction) * @param arg A declaration for the function. */ - int deleteScriptFunction(const Ogre::String& arg); + ScriptRetCode_t deleteScriptFunction(const Ogre::String& arg, ScriptUnitID_t nid); /** * Adds a global variable to the script * (Wrapper for ScriptEngine::addVariable) * @param arg A declaration for the variable. */ - int addScriptVariable(const Ogre::String& arg); + ScriptRetCode_t addScriptVariable(const Ogre::String& arg, ScriptUnitID_t nid); + + /** + * Adds a global variable to the script + * (Wrapper for ScriptEngine::variableExists) + * @param arg A declaration for the variable. + */ + ScriptRetCode_t scriptVariableExists(const Ogre::String& arg, ScriptUnitID_t nid); /** * Deletes a global variable from the script * (Wrapper for ScriptEngine::deleteVariable) * @param arg A declaration for the variable. */ - int deleteScriptVariable(const Ogre::String& arg); + ScriptRetCode_t deleteScriptVariable(const Ogre::String& arg, ScriptUnitID_t nid); /** * Retrieves a memory address of a global variable in any script. @@ -259,7 +266,7 @@ class GameScript * @param refTypeId Type of the reference; To be registered as variable-type parameter `?&out` * @return 0 on success, negative number on error. */ - int getScriptVariable(ScriptUnitID_t nid, const Ogre::String& varName, void *ref, int refTypeId); + ScriptRetCode_t getScriptVariable(const Ogre::String& varName, void *ref, int refTypeId, ScriptUnitID_t nid); void clearEventCache(); diff --git a/source/main/scripting/ScriptEngine.cpp b/source/main/scripting/ScriptEngine.cpp index 4822820c37..8039c5e649 100644 --- a/source/main/scripting/ScriptEngine.cpp +++ b/source/main/scripting/ScriptEngine.cpp @@ -381,6 +381,24 @@ bool ScriptEngine::prepareContextAndHandleErrors(ScriptUnitID_t nid, int asFunct return true; } +ScriptRetCode_t ScriptEngine::validateScriptModule(ScriptUnitID_t nid, asIScriptModule*& out_mod) +{ + if (!engine) + return SCRIPTRETCODE_ENGINE_NOT_CREATED; + + if (!context) + return SCRIPTRETCODE_CONTEXT_NOT_CREATED; + + if (!this->scriptUnitExists(nid)) + return SCRIPTRETCODE_SCRIPTUNIT_NOT_EXISTS; + + out_mod = this->getScriptUnit(nid).scriptModule; + if (!out_mod) + return SCRIPTRETCODE_SCRIPTUNIT_NO_MODULE; + + return SCRIPTRETCODE_SUCCESS; +} + void ScriptEngine::framestep(Real dt) { // Check if we need to execute any strings @@ -480,239 +498,191 @@ void ScriptEngine::queueStringForExecution(const String command) stringExecutionQueue.push(command); } -int ScriptEngine::executeString(String command) +ScriptRetCode_t ScriptEngine::executeString(String command) { - if (!engine || !context) - return 1; - + int result = 0; + AngelScript::asIScriptModule* mod = nullptr; // Only works with terrain script module (classic behavior) - if (m_terrain_script_unit == SCRIPTUNITID_INVALID) - return 1; - - AngelScript::asIScriptModule *mod = m_script_units[m_terrain_script_unit].scriptModule; - int result = ExecuteString(engine, command.c_str(), mod, context); - if (result < 0) + result = this->validateScriptModule(m_terrain_script_unit, /*[out]*/ mod); + if (result == 0) { - SLOG("error " + TOSTRING(result) + " while executing string: " + command + "."); + result = ExecuteString(engine, command.c_str(), mod, context); + if (result < 0) + { + SLOG(fmt::format("Error {} while executing string `{}`", result, command)); + } } return result; } -int ScriptEngine::addFunction(const String &arg) +ScriptRetCode_t ScriptEngine::addFunction(const String &arg, const ScriptUnitID_t nid /*= SCRIPTUNITID_DEFAULT*/) { - if (!engine || !context) - return 1; - - if (!context) - - - // Only works with terrain script module (classic behavior) - if (m_terrain_script_unit == SCRIPTUNITID_INVALID) - return 1; - - AngelScript::asIScriptModule *mod = m_script_units[m_terrain_script_unit].scriptModule; - - AngelScript::asIScriptFunction *func = 0; - int r = mod->CompileFunction("addfunc", arg.c_str(), 0, AngelScript::asCOMP_ADD_TO_MODULE, &func); - - if ( r < 0 ) + int result = 0; + AngelScript::asIScriptModule* mod = nullptr; + result = this->validateScriptModule(nid, /*[out]*/ mod); + if (result == 0) { - char tmp[512] = ""; - snprintf(tmp, 512, "An error occurred while trying to add a function ('%s') to script module '%s'.", arg.c_str(), mod->GetName()); - SLOG(tmp); - } - else - { - // successfully added function; Check if we added a "special" function - - if (func == mod->GetFunctionByDecl("void frameStep(float)")) - { - if (m_script_units[m_terrain_script_unit].frameStepFunctionPtr == nullptr) - m_script_units[m_terrain_script_unit].frameStepFunctionPtr = func; - } - else if (func == mod->GetFunctionByDecl("void eventCallback(int, int)")) - { - if (m_script_units[m_terrain_script_unit].eventCallbackFunctionPtr == nullptr) - m_script_units[m_terrain_script_unit].eventCallbackFunctionPtr = func; - } - else if (func == mod->GetFunctionByDecl("void eventCallbackEx(scriptEvents, int, int, int, int, string, string, string, string)")) + AngelScript::asIScriptFunction* func = nullptr; + result = mod->CompileFunction("addfunc", arg.c_str(), 0, AngelScript::asCOMP_ADD_TO_MODULE, &func); + if (result == 0) { - if (m_script_units[m_terrain_script_unit].eventCallbackExFunctionPtr == nullptr) - m_script_units[m_terrain_script_unit].eventCallbackExFunctionPtr = func; - } - // THIS IS OBSOLETE - Use `eventCallbackEx()` and `SE_EVENTBOX_ENTER` instead. See commentary in `envokeCallback()` - else if (func == this->getFunctionByDeclAndLogCandidates( + // successfully added function; Check if we added a "special" function + + if (func == mod->GetFunctionByDecl("void frameStep(float)")) + { + if (m_script_units[m_terrain_script_unit].frameStepFunctionPtr == nullptr) + m_script_units[m_terrain_script_unit].frameStepFunctionPtr = func; + } + else if (func == mod->GetFunctionByDecl("void eventCallback(int, int)")) + { + if (m_script_units[m_terrain_script_unit].eventCallbackFunctionPtr == nullptr) + m_script_units[m_terrain_script_unit].eventCallbackFunctionPtr = func; + } + else if (func == mod->GetFunctionByDecl("void eventCallbackEx(scriptEvents, int, int, int, int, string, string, string, string)")) + { + if (m_script_units[m_terrain_script_unit].eventCallbackExFunctionPtr == nullptr) + m_script_units[m_terrain_script_unit].eventCallbackExFunctionPtr = func; + } + // THIS IS OBSOLETE - Use `eventCallbackEx()` and `SE_EVENTBOX_ENTER` instead. See commentary in `envokeCallback()` + else if (func == this->getFunctionByDeclAndLogCandidates( m_terrain_script_unit, GETFUNCFLAG_OPTIONAL, GETFUNC_DEFAULTEVENTCALLBACK_NAME, GETFUNC_DEFAULTEVENTCALLBACK_SIGFMT)) + { + if (m_script_units[m_terrain_script_unit].defaultEventCallbackFunctionPtr == nullptr) + m_script_units[m_terrain_script_unit].defaultEventCallbackFunctionPtr = func; + } + } + else { - if (m_script_units[m_terrain_script_unit].defaultEventCallbackFunctionPtr == nullptr) - m_script_units[m_terrain_script_unit].defaultEventCallbackFunctionPtr = func; + SLOG(fmt::format("Error {} adding function `{}` to script module '{}'", result, arg, mod->GetName())); } + // We must release the function object + if (func) + func->Release(); } - // We must release the function object - if ( func ) - func->Release(); - - return r; + return result; } -int ScriptEngine::functionExists(const String &arg) +ScriptRetCode_t ScriptEngine::functionExists(const String& arg, const ScriptUnitID_t nid /*= SCRIPTUNITID_DEFAULT*/) { - if (!engine || !context) // WTF? If the scripting engine failed to start, how would it invoke this function? - return -1; // ... OK, I guess the author wanted the fn. to be usable both within script and C++, but IMO that's bad design (generally good, but bad for a game.. bad for RoR), really ~ only_a_ptr, 09/2017 - - // Only works with terrain script module (classic behavior) - if (m_terrain_script_unit == SCRIPTUNITID_INVALID) - return -1; - - AngelScript::asIScriptModule *mod = m_script_units[m_terrain_script_unit].scriptModule; - - if (mod == 0) + int result = 0; + AngelScript::asIScriptModule* mod = nullptr; + result = this->validateScriptModule(nid, /*[out]*/ mod); + if (result == 0) { - return AngelScript::asNO_FUNCTION; // Nope, it's an internal error, not a "function not found" case ~ only_a_ptr, 09/2017 - } - else - { - AngelScript::asIScriptFunction* fn = mod->GetFunctionByDecl(arg.c_str()); - if (fn != nullptr) - return fn->GetId(); - else - return AngelScript::asNO_FUNCTION; + if (mod->GetFunctionByDecl(arg.c_str()) != nullptr) + result = SCRIPTRETCODE_FUNCTION_NOT_EXISTS; } + return result; } -int ScriptEngine::deleteFunction(const String &arg) +ScriptRetCode_t ScriptEngine::deleteFunction(const String &arg, const ScriptUnitID_t nid /*= SCRIPTUNITID_DEFAULT*/) { - if (!engine || !context) - return AngelScript::asERROR; - - // Only works with terrain script module (classic behavior) - if (m_terrain_script_unit == SCRIPTUNITID_INVALID) - return -1; - - AngelScript::asIScriptModule *mod = m_script_units[m_terrain_script_unit].scriptModule; - - if ( mod->GetFunctionCount() == 0 ) + int result = 0; + AngelScript::asIScriptModule* mod = nullptr; + result = this->validateScriptModule(nid, /*[out]*/ mod); + if (result == 0) { - char tmp[512] = ""; - sprintf(tmp, "An error occurred while trying to remove a function ('%s') from script module '%s': No functions have been added (and consequently: the function does not exist).", arg.c_str(), mod->GetName()); - SLOG(tmp); - return AngelScript::asNO_FUNCTION; - } - - AngelScript::asIScriptFunction* func = mod->GetFunctionByDecl(arg.c_str()); - if (func != nullptr) - { - // Warning: The function is not destroyed immediately, only when no more references point to it. - mod->RemoveFunction(func); - - // Since functions can be recursive, we'll call the garbage - // collector to make sure the object is really freed - engine->GarbageCollect(); + AngelScript::asIScriptFunction* func = mod->GetFunctionByDecl(arg.c_str()); + if (func != nullptr) + { + // Warning: The function is not destroyed immediately, only when no more references point to it. + result = mod->RemoveFunction(func); + if (result != 0) + { + SLOG(fmt::format("Error {} removing function `{}` from module '{}' - continuing anyway (compatibility).", result, arg, mod->GetName())); + } - // Check if we removed a "special" function + // Since functions can be recursive, we'll call the garbage + // collector to make sure the object is really freed + engine->GarbageCollect(); - if ( m_script_units[m_terrain_script_unit].frameStepFunctionPtr == func ) - m_script_units[m_terrain_script_unit].frameStepFunctionPtr = nullptr; + // Check if we removed a "special" function - if ( m_script_units[m_terrain_script_unit].eventCallbackFunctionPtr == func ) - m_script_units[m_terrain_script_unit].eventCallbackFunctionPtr = nullptr; + if (m_script_units[m_terrain_script_unit].frameStepFunctionPtr == func) + m_script_units[m_terrain_script_unit].frameStepFunctionPtr = nullptr; - if (m_script_units[m_terrain_script_unit].eventCallbackExFunctionPtr == func) - m_script_units[m_terrain_script_unit].eventCallbackExFunctionPtr = nullptr; + if (m_script_units[m_terrain_script_unit].eventCallbackFunctionPtr == func) + m_script_units[m_terrain_script_unit].eventCallbackFunctionPtr = nullptr; - if ( m_script_units[m_terrain_script_unit].defaultEventCallbackFunctionPtr == func ) - m_script_units[m_terrain_script_unit].defaultEventCallbackFunctionPtr = nullptr; + if (m_script_units[m_terrain_script_unit].eventCallbackExFunctionPtr == func) + m_script_units[m_terrain_script_unit].eventCallbackExFunctionPtr = nullptr; - return func->GetId(); - } - else - { - char tmp[512] = ""; - sprintf(tmp, "An error occurred while trying to remove a function ('%s') from script module '%s'.", arg.c_str(), mod->GetName()); - SLOG(tmp); - return AngelScript::asERROR; + if (m_script_units[m_terrain_script_unit].defaultEventCallbackFunctionPtr == func) + m_script_units[m_terrain_script_unit].defaultEventCallbackFunctionPtr = nullptr; + } + else + { + SLOG(fmt::format("Could not remove function `{}` from module '{}' - not found.", arg, mod->GetName())); + } } + return result; } -int ScriptEngine::addVariable(const String &arg) +ScriptRetCode_t ScriptEngine::addVariable(const String &arg, const ScriptUnitID_t nid /*= SCRIPTUNITID_DEFAULT*/) { - if (!engine || !context) return 1; - // Only works with terrain script module (classic behavior) - if (m_terrain_script_unit == SCRIPTUNITID_INVALID) - return 1; - - AngelScript::asIScriptModule *mod = m_script_units[m_terrain_script_unit].scriptModule; - - int r = mod->CompileGlobalVar("addvar", arg.c_str(), 0); - if ( r < 0 ) + int result = 0; + AngelScript::asIScriptModule* mod = nullptr; + result = this->validateScriptModule(nid, /*[out]*/ mod); + if (result == 0) { - char tmp[512] = ""; - sprintf(tmp, "An error occurred while trying to add a variable ('%s') to script module '%s'.", arg.c_str(), mod->GetName()); - SLOG(tmp); + result = mod->CompileGlobalVar("addvar", arg.c_str(), 0); + if (result < 0) + { + SLOG(fmt::format("Error {} while adding variable `{}` to module '{}'", result, arg, mod->GetName())); + } } - - return r; + return result; } -int ScriptEngine::deleteVariable(const String &arg) +ScriptRetCode_t ScriptEngine::variableExists(const String& arg, const ScriptUnitID_t nid /*= SCRIPTUNITID_DEFAULT*/) { - if (!engine || !context) return 1; - // Only works with terrain script module (classic behavior) - if (m_terrain_script_unit == SCRIPTUNITID_INVALID) - return 1; - AngelScript::asIScriptModule *mod = m_script_units[m_terrain_script_unit].scriptModule; - - if ( mod == 0 || mod->GetGlobalVarCount() == 0 ) - { - char tmp[512] = ""; - sprintf(tmp, "An error occurred while trying to remove a variable ('%s') from script module '%s': No variables have been added (and consequently: the variable does not exist).", arg.c_str(), mod->GetName()); - SLOG(tmp); - return AngelScript::asNO_GLOBAL_VAR; - } - - int index = mod->GetGlobalVarIndexByName(arg.c_str()); - if ( index >= 0 ) - { - index = mod->RemoveGlobalVar(index); - } - else + int result = 0; + AngelScript::asIScriptModule* mod = nullptr; + result = this->validateScriptModule(nid, /*[out]*/ mod); + if (result == 0) { - char tmp[512] = ""; - sprintf(tmp, "An error occurred while trying to remove a variable ('%s') from script module '%s'.", arg.c_str(), mod->GetName()); - SLOG(tmp); + result = mod->GetGlobalVarIndexByName(arg.c_str()); + if (result >= 0) + { + result = 0; + } } - - return index; + return result; } -int ScriptEngine::getVariable(ScriptUnitID_t nid, const Ogre::String& varName, void *ref, int refTypeId) +ScriptRetCode_t ScriptEngine::deleteVariable(const String &arg, const ScriptUnitID_t nid /*= SCRIPTUNITID_DEFAULT*/) { - if (!engine || !context) + int result = 0; + AngelScript::asIScriptModule* mod = nullptr; + result = this->validateScriptModule(nid, /*[out]*/ mod); + if (result == 0) { - SLOG("Error in `getVariable()` - engine or context not initialized"); - return -1; + result = mod->GetGlobalVarIndexByName(arg.c_str()); + if (result >= 0) + { + result = mod->RemoveGlobalVar(result); + } } - - if (!this->scriptUnitExists(nid)) + if (result < 0) { - SLOG("Error in `getVariable()` - script unit does not exist"); - return -2; + SLOG(fmt::format("Error {} while removing variable `{}` from module '{}'", result, arg, mod->GetName())); } + return result; +} - AngelScript::asIScriptModule *mod = m_script_units[nid].scriptModule; - if (!mod) - { - SLOG("Error in `getVariable()` - script module not initialized"); - return -3; - } +ScriptRetCode_t ScriptEngine::getVariable(const Ogre::String& varName, void *ref, int refTypeId, const ScriptUnitID_t nid /*= SCRIPTUNITID_DEFAULT*/) +{ + AngelScript::asIScriptModule* mod = nullptr; + int modResult = this->validateScriptModule(nid, /*[out]*/ mod); + if (modResult < 0) + return modResult; int index = mod->GetGlobalVarIndexByName(varName.c_str()); if (index < 0) { - SLOG(fmt::format("Error in `getVariable()` - '{}' not found", varName)); - return -4; + return index; } const char* asVarName = nullptr; @@ -722,8 +692,7 @@ int ScriptEngine::getVariable(ScriptUnitID_t nid, const Ogre::String& varName, v int getResult = mod->GetGlobalVar(index, &asVarName, &asNamespace, &asTypeId, &asConst); if (getResult < 0) { - SLOG(fmt::format("Error in `getVariable()` - error while getting type of '{}'", varName)); - return -5; + return getResult; } SLOG(fmt::format("getVariable() - '{}' global var info: name='{}', namespace='{}', typeid={}, const={}", @@ -731,59 +700,59 @@ int ScriptEngine::getVariable(ScriptUnitID_t nid, const Ogre::String& varName, v // ~~ DEV NOTE: The following code is adopted from AngelScript's add-on 'scriptany.cpp', function `Retrieve()` ~~ - if( refTypeId & asTYPEID_OBJHANDLE ) - { - // Is the handle type compatible with the stored value? + if( refTypeId & asTYPEID_OBJHANDLE ) + { + // Is the handle type compatible with the stored value? - // A handle can be retrieved if the stored type is a handle of same or compatible type - // or if the stored type is an object that implements the interface that the handle refer to. - if( (asTypeId & asTYPEID_MASK_OBJECT) ) - { - // Don't allow the retrieval if the stored handle is to a const object but not the wanted handle - if( (asTypeId & asTYPEID_HANDLETOCONST) && !(refTypeId & asTYPEID_HANDLETOCONST) ) + // A handle can be retrieved if the stored type is a handle of same or compatible type + // or if the stored type is an object that implements the interface that the handle refer to. + if( (asTypeId & asTYPEID_MASK_OBJECT) ) + { + // Don't allow the retrieval if the stored handle is to a const object but not the wanted handle + if( (asTypeId & asTYPEID_HANDLETOCONST) && !(refTypeId & asTYPEID_HANDLETOCONST) ) { SLOG(fmt::format("Error in `getVariable()` - '{}' is a handle to `const` object but the requested type is not.", varName)); - return -6; + return SCRIPTRETCODE_UNSPECIFIED_ERROR; } - // RefCastObject will increment the refCount of the returned pointer if successful - engine->RefCastObject(mod->GetAddressOfGlobalVar(index), engine->GetTypeInfoById(asTypeId), engine->GetTypeInfoById(refTypeId), reinterpret_cast(ref)); - if( *(asPWORD*)ref == 0 ) + // RefCastObject will increment the refCount of the returned pointer if successful + engine->RefCastObject(mod->GetAddressOfGlobalVar(index), engine->GetTypeInfoById(asTypeId), engine->GetTypeInfoById(refTypeId), reinterpret_cast(ref)); + if( *(asPWORD*)ref == 0 ) { SLOG(fmt::format("Error in `getVariable()` - '{}': reference-cast from '{}' to '{}' yielded null", varName, engine->GetTypeDeclaration(asTypeId), engine->GetTypeDeclaration(refTypeId))); - return -7; + return SCRIPTRETCODE_UNSPECIFIED_ERROR; } - return 0; - } - } - else if( refTypeId & asTYPEID_MASK_OBJECT ) - { - // Is the object type compatible with the stored value? - - // Copy the object into the given reference - if( asTypeId == refTypeId ) - { - engine->AssignScriptObject(ref, mod->GetAddressOfGlobalVar(index), engine->GetTypeInfoById(asTypeId)); - return 0; - } - } - else - { - // Is the primitive type _IDENTICAL TO_ the stored value? + return 0; + } + } + else if( refTypeId & asTYPEID_MASK_OBJECT ) + { + // Is the object type compatible with the stored value? + + // Copy the object into the given reference + if( asTypeId == refTypeId ) + { + engine->AssignScriptObject(ref, mod->GetAddressOfGlobalVar(index), engine->GetTypeInfoById(asTypeId)); + return 0; + } + } + else + { + // Is the primitive type _IDENTICAL TO_ the stored value? // NOTE: implicit conversions are not done automatically, we would have to write code for each case separately - if( asTypeId == refTypeId ) - { - int size = engine->GetSizeOfPrimitiveType(refTypeId); - memcpy(ref, mod->GetAddressOfGlobalVar(index), size); - return 0; - } - } + if( asTypeId == refTypeId ) + { + int size = engine->GetSizeOfPrimitiveType(refTypeId); + memcpy(ref, mod->GetAddressOfGlobalVar(index), size); + return 0; + } + } SLOG(fmt::format("Error in `getVariable()` - '{}' has incompatible type, expected '{}' (typeid {}), got '{}' (typeid {})", varName, engine->GetTypeDeclaration(refTypeId), refTypeId, engine->GetTypeDeclaration(asTypeId), asTypeId)); - return -8; + return SCRIPTRETCODE_UNSPECIFIED_ERROR; } asIScriptFunction* ScriptEngine::getFunctionByDeclAndLogCandidates(ScriptUnitID_t nid, GetFuncFlags_t flags, const std::string& funcName, const std::string& fmtFuncDecl) diff --git a/source/main/scripting/ScriptEngine.h b/source/main/scripting/ScriptEngine.h index 872f610766..18fff484d4 100644 --- a/source/main/scripting/ScriptEngine.h +++ b/source/main/scripting/ScriptEngine.h @@ -112,6 +112,60 @@ const GetFuncFlags_t GETFUNCFLAG_SILENT = BITMASK(2); //!< Never logs const std::string GETFUNC_DEFAULTEVENTCALLBACK_SIGFMT = "void {}(int, string, string, int)"; const std::string GETFUNC_DEFAULTEVENTCALLBACK_NAME = "defaultEventCallback"; +/// Common return codes for script manipulation funcs (add/get/delete | funcs/variables) +enum ScriptRetCode +{ + // Generic success - 0 by common convention. + SCRIPTRETCODE_SUCCESS = AngelScript::asSUCCESS, // = AngelScript::0 + + // AngelScript technical codes + SCRIPTRETCODE_AS_ERROR = AngelScript::asERROR, // = AngelScript::-1 etc... + SCRIPTRETCODE_AS_CONTEXT_ACTIVE = AngelScript::asCONTEXT_ACTIVE, + SCRIPTRETCODE_AS_CONTEXT_NOT_FINISHED = AngelScript::asCONTEXT_NOT_FINISHED, + SCRIPTRETCODE_AS_CONTEXT_NOT_PREPARED = AngelScript::asCONTEXT_NOT_PREPARED, + SCRIPTRETCODE_AS_INVALID_ARG = AngelScript::asINVALID_ARG, + SCRIPTRETCODE_AS_NO_FUNCTION = AngelScript::asNO_FUNCTION, + SCRIPTRETCODE_AS_NOT_SUPPORTED = AngelScript::asNOT_SUPPORTED, + SCRIPTRETCODE_AS_INVALID_NAME = AngelScript::asINVALID_NAME, + SCRIPTRETCODE_AS_NAME_TAKEN = AngelScript::asNAME_TAKEN, + SCRIPTRETCODE_AS_INVALID_DECLARATION = AngelScript::asINVALID_DECLARATION, + SCRIPTRETCODE_AS_INVALID_OBJECT = AngelScript::asINVALID_OBJECT, + SCRIPTRETCODE_AS_INVALID_TYPE = AngelScript::asINVALID_TYPE, + SCRIPTRETCODE_AS_ALREADY_REGISTERED = AngelScript::asALREADY_REGISTERED, + SCRIPTRETCODE_AS_MULTIPLE_FUNCTIONS = AngelScript::asMULTIPLE_FUNCTIONS, + SCRIPTRETCODE_AS_NO_MODULE = AngelScript::asNO_MODULE, + SCRIPTRETCODE_AS_NO_GLOBAL_VAR = AngelScript::asNO_GLOBAL_VAR, + SCRIPTRETCODE_AS_INVALID_CONFIGURATION = AngelScript::asINVALID_CONFIGURATION, + SCRIPTRETCODE_AS_INVALID_INTERFACE = AngelScript::asINVALID_INTERFACE, + SCRIPTRETCODE_AS_CANT_BIND_ALL_FUNCTIONS = AngelScript::asCANT_BIND_ALL_FUNCTIONS, + SCRIPTRETCODE_AS_LOWER_ARRAY_DIMENSION_NOT_REGISTERED = AngelScript::asLOWER_ARRAY_DIMENSION_NOT_REGISTERED, + SCRIPTRETCODE_AS_WRONG_CONFIG_GROUP = AngelScript::asWRONG_CONFIG_GROUP, + SCRIPTRETCODE_AS_CONFIG_GROUP_IS_IN_USE = AngelScript::asCONFIG_GROUP_IS_IN_USE, + SCRIPTRETCODE_AS_ILLEGAL_BEHAVIOUR_FOR_TYPE = AngelScript::asILLEGAL_BEHAVIOUR_FOR_TYPE, + SCRIPTRETCODE_AS_WRONG_CALLING_CONV = AngelScript::asWRONG_CALLING_CONV, + SCRIPTRETCODE_AS_BUILD_IN_PROGRESS = AngelScript::asBUILD_IN_PROGRESS, + SCRIPTRETCODE_AS_INIT_GLOBAL_VARS_FAILED = AngelScript::asINIT_GLOBAL_VARS_FAILED, + SCRIPTRETCODE_AS_OUT_OF_MEMORY = AngelScript::asOUT_OF_MEMORY, + SCRIPTRETCODE_AS_MODULE_IS_IN_USE = AngelScript::asMODULE_IS_IN_USE, + + // RoR ScriptEngine return codes + // Following is analysis of former state (most but not all funcs returned 0 on success) + // ~ executeString() [script: not exported] - used to return 0 on success, 1 on internal error and negative number otherwise. + // ~ addFunction() [script: `game.addScriptFunction()`] - used to return 0 on success, 1 on internal error and negative number otherwise. + // ~ functionExists() [script: `game.scriptFunctionExists()`] - used to return function ID (always >0) on success, negative number on error. + // ~ deleteFunction() [script: `game.deleteScriptFunction()`] - used to return function ID (always >0) on success, negative number on error. + // ~ addVariable() [script: `game.addScriptVariable()` ] - used to return 0 on success, 1 on internal error and negative number otherwise. + // ~ variableExists() [script: `game.scriptVariableExists()`] - newly added, returns 0 on success and negative number otherwise. + // ~ deleteVariable() [script: `game.deleteScriptVariable()` ] - used to return 0 on success, 1 on internal error and negative number otherwise. + // ~ getVariable() [script: `game.getScriptVariable()` ] - recently added, returns 0 on success and negative number otherwise. + SCRIPTRETCODE_UNSPECIFIED_ERROR = -1001, + SCRIPTRETCODE_ENGINE_NOT_CREATED = -1002, + SCRIPTRETCODE_CONTEXT_NOT_CREATED = -1003, + SCRIPTRETCODE_SCRIPTUNIT_NOT_EXISTS = -1004, // The scpecified ScriptUnitID_t was invalid + SCRIPTRETCODE_SCRIPTUNIT_NO_MODULE = -1005, + SCRIPTRETCODE_FUNCTION_NOT_EXISTS = -1006, +}; + /** * @brief This class represents the angelscript scripting interface. It can load and execute scripts. * @authors Thomas Fischer (thomas{AT}rigsofrods{DOT}com) @@ -162,7 +216,7 @@ class ScriptEngine : public Ogre::LogListener * executes a string (useful for the console) * @param command string to execute */ - int executeString(Ogre::String command); + ScriptRetCode_t executeString(Ogre::String command); /** * Queues a string for execution. @@ -175,38 +229,44 @@ class ScriptEngine : public Ogre::LogListener /** * Adds a global function to the script * @param arg A declaration for the function. + * @param nid The script unit ID to act upon - by default the terrain script. */ - int addFunction(const Ogre::String& arg); + ScriptRetCode_t addFunction(const Ogre::String& arg, const ScriptUnitID_t nid = SCRIPTUNITID_DEFAULT); /** * Checks if a global function exists * @param arg A declaration for the function. */ - int functionExists(const Ogre::String& arg); + ScriptRetCode_t functionExists(const Ogre::String& arg, const ScriptUnitID_t nid = SCRIPTUNITID_DEFAULT); /** * Deletes a global function from the script * @param arg A declaration for the function. */ - int deleteFunction(const Ogre::String& arg); + ScriptRetCode_t deleteFunction(const Ogre::String& arg, const ScriptUnitID_t nid = SCRIPTUNITID_DEFAULT); /** * Adds a global variable to the script * @param arg A declaration for the variable. */ - int addVariable(const Ogre::String& arg); + ScriptRetCode_t addVariable(const Ogre::String& arg, const ScriptUnitID_t nid = SCRIPTUNITID_DEFAULT); + + /** + * Adds a global variable to the script + * @param arg A declaration for the variable. + */ + ScriptRetCode_t variableExists(const Ogre::String& arg, const ScriptUnitID_t nid = SCRIPTUNITID_DEFAULT); /** * Deletes a global variable from the script * @param arg A declaration for the variable. */ - int deleteVariable(const Ogre::String& arg); + ScriptRetCode_t deleteVariable(const Ogre::String& arg, const ScriptUnitID_t nid = SCRIPTUNITID_DEFAULT); /** * Retrieves a global variable from any running script - * @returns 0 on success, negative number on error. */ - int getVariable(ScriptUnitID_t nid, const Ogre::String& varName, void *ref, int typeID); + ScriptRetCode_t getVariable(const Ogre::String& varName, void *ref, int typeID, ScriptUnitID_t nid = SCRIPTUNITID_DEFAULT); /** * Finds a function by full declaration, and if not found, finds candidates by name and logs them to Angelscript.log @@ -273,6 +333,12 @@ class ScriptEngine : public Ogre::LogListener */ int executeContextAndHandleErrors(ScriptUnitID_t nid); + /** + * Helper for all manipulations with functions/variables; ensures the script unit exists and is fully set up. + * @return see `RoR::ScriptRetCode` ~ 0 on success, negative number on error. + */ + ScriptRetCode_t validateScriptModule(const ScriptUnitID_t nid, AngelScript::asIScriptModule*& out_mod); + /// @} /// @name Script diagnostics diff --git a/source/main/scripting/bindings/GameScriptAngelscript.cpp b/source/main/scripting/bindings/GameScriptAngelscript.cpp index 71cfd3030e..6668cf6d3b 100644 --- a/source/main/scripting/bindings/GameScriptAngelscript.cpp +++ b/source/main/scripting/bindings/GameScriptAngelscript.cpp @@ -39,6 +39,44 @@ void RoR::RegisterGameScript(asIScriptEngine *engine) result = engine->RegisterEnumValue("ScriptCategory", "SCRIPT_CATEGORY_TERRAIN", static_cast(ScriptCategory::TERRAIN)); ROR_ASSERT(result >= 0); result = engine->RegisterEnumValue("ScriptCategory", "SCRIPT_CATEGORY_CUSTOM", static_cast(ScriptCategory::CUSTOM)); ROR_ASSERT(result >= 0); + // `enum ScriptRetCode` ~ Common return codes for script manipulation funcs (add/get/delete | funcs/variables) + result = engine->RegisterEnum("ScriptRetCode"); ROR_ASSERT(result >= 0); + + result = engine->RegisterEnumValue("ScriptRetCode", "SCRIPTRETCODE_AS_ERROR", SCRIPTRETCODE_AS_ERROR ); ROR_ASSERT(result >= 0); + result = engine->RegisterEnumValue("ScriptRetCode", "SCRIPTRETCODE_AS_CONTEXT_ACTIVE", SCRIPTRETCODE_AS_CONTEXT_ACTIVE ); ROR_ASSERT(result >= 0); + result = engine->RegisterEnumValue("ScriptRetCode", "SCRIPTRETCODE_AS_CONTEXT_NOT_FINISHED", SCRIPTRETCODE_AS_CONTEXT_NOT_FINISHED ); ROR_ASSERT(result >= 0); + result = engine->RegisterEnumValue("ScriptRetCode", "SCRIPTRETCODE_AS_CONTEXT_NOT_PREPARED", SCRIPTRETCODE_AS_CONTEXT_NOT_PREPARED ); ROR_ASSERT(result >= 0); + result = engine->RegisterEnumValue("ScriptRetCode", "SCRIPTRETCODE_AS_INVALID_ARG", SCRIPTRETCODE_AS_INVALID_ARG ); ROR_ASSERT(result >= 0); + result = engine->RegisterEnumValue("ScriptRetCode", "SCRIPTRETCODE_AS_NO_FUNCTION", SCRIPTRETCODE_AS_NO_FUNCTION ); ROR_ASSERT(result >= 0); + result = engine->RegisterEnumValue("ScriptRetCode", "SCRIPTRETCODE_AS_NOT_SUPPORTED", SCRIPTRETCODE_AS_NOT_SUPPORTED ); ROR_ASSERT(result >= 0); + result = engine->RegisterEnumValue("ScriptRetCode", "SCRIPTRETCODE_AS_INVALID_NAME", SCRIPTRETCODE_AS_INVALID_NAME ); ROR_ASSERT(result >= 0); + result = engine->RegisterEnumValue("ScriptRetCode", "SCRIPTRETCODE_AS_NAME_TAKEN", SCRIPTRETCODE_AS_NAME_TAKEN ); ROR_ASSERT(result >= 0); + result = engine->RegisterEnumValue("ScriptRetCode", "SCRIPTRETCODE_AS_INVALID_DECLARATION", SCRIPTRETCODE_AS_INVALID_DECLARATION ); ROR_ASSERT(result >= 0); + result = engine->RegisterEnumValue("ScriptRetCode", "SCRIPTRETCODE_AS_INVALID_OBJECT", SCRIPTRETCODE_AS_INVALID_OBJECT ); ROR_ASSERT(result >= 0); + result = engine->RegisterEnumValue("ScriptRetCode", "SCRIPTRETCODE_AS_INVALID_TYPE", SCRIPTRETCODE_AS_INVALID_TYPE ); ROR_ASSERT(result >= 0); + result = engine->RegisterEnumValue("ScriptRetCode", "SCRIPTRETCODE_AS_ALREADY_REGISTERED", SCRIPTRETCODE_AS_ALREADY_REGISTERED ); ROR_ASSERT(result >= 0); + result = engine->RegisterEnumValue("ScriptRetCode", "SCRIPTRETCODE_AS_MULTIPLE_FUNCTIONS", SCRIPTRETCODE_AS_MULTIPLE_FUNCTIONS ); ROR_ASSERT(result >= 0); + result = engine->RegisterEnumValue("ScriptRetCode", "SCRIPTRETCODE_AS_NO_MODULE", SCRIPTRETCODE_AS_NO_MODULE ); ROR_ASSERT(result >= 0); + result = engine->RegisterEnumValue("ScriptRetCode", "SCRIPTRETCODE_AS_NO_GLOBAL_VAR", SCRIPTRETCODE_AS_NO_GLOBAL_VAR ); ROR_ASSERT(result >= 0); + result = engine->RegisterEnumValue("ScriptRetCode", "SCRIPTRETCODE_AS_INVALID_CONFIGURATION", SCRIPTRETCODE_AS_INVALID_CONFIGURATION ); ROR_ASSERT(result >= 0); + result = engine->RegisterEnumValue("ScriptRetCode", "SCRIPTRETCODE_AS_INVALID_INTERFACE", SCRIPTRETCODE_AS_INVALID_INTERFACE ); ROR_ASSERT(result >= 0); + result = engine->RegisterEnumValue("ScriptRetCode", "SCRIPTRETCODE_AS_CANT_BIND_ALL_FUNCTIONS", SCRIPTRETCODE_AS_CANT_BIND_ALL_FUNCTIONS ); ROR_ASSERT(result >= 0); + result = engine->RegisterEnumValue("ScriptRetCode", "SCRIPTRETCODE_AS_LOWER_ARRAY_DIMENSION_NOT_REGISTERED", SCRIPTRETCODE_AS_LOWER_ARRAY_DIMENSION_NOT_REGISTERED); ROR_ASSERT(result >= 0); + result = engine->RegisterEnumValue("ScriptRetCode", "SCRIPTRETCODE_AS_WRONG_CONFIG_GROUP", SCRIPTRETCODE_AS_WRONG_CONFIG_GROUP ); ROR_ASSERT(result >= 0); + result = engine->RegisterEnumValue("ScriptRetCode", "SCRIPTRETCODE_AS_CONFIG_GROUP_IS_IN_USE", SCRIPTRETCODE_AS_CONFIG_GROUP_IS_IN_USE ); ROR_ASSERT(result >= 0); + result = engine->RegisterEnumValue("ScriptRetCode", "SCRIPTRETCODE_AS_ILLEGAL_BEHAVIOUR_FOR_TYPE", SCRIPTRETCODE_AS_ILLEGAL_BEHAVIOUR_FOR_TYPE ); ROR_ASSERT(result >= 0); + result = engine->RegisterEnumValue("ScriptRetCode", "SCRIPTRETCODE_AS_WRONG_CALLING_CONV", SCRIPTRETCODE_AS_WRONG_CALLING_CONV ); ROR_ASSERT(result >= 0); + result = engine->RegisterEnumValue("ScriptRetCode", "SCRIPTRETCODE_AS_BUILD_IN_PROGRESS", SCRIPTRETCODE_AS_BUILD_IN_PROGRESS ); ROR_ASSERT(result >= 0); + result = engine->RegisterEnumValue("ScriptRetCode", "SCRIPTRETCODE_AS_INIT_GLOBAL_VARS_FAILED", SCRIPTRETCODE_AS_INIT_GLOBAL_VARS_FAILED ); ROR_ASSERT(result >= 0); + result = engine->RegisterEnumValue("ScriptRetCode", "SCRIPTRETCODE_AS_OUT_OF_MEMORY", SCRIPTRETCODE_AS_OUT_OF_MEMORY ); ROR_ASSERT(result >= 0); + result = engine->RegisterEnumValue("ScriptRetCode", "SCRIPTRETCODE_AS_MODULE_IS_IN_USE", SCRIPTRETCODE_AS_MODULE_IS_IN_USE ); ROR_ASSERT(result >= 0); + result = engine->RegisterEnumValue("ScriptRetCode", "SCRIPTRETCODE_UNSPECIFIED_ERROR", SCRIPTRETCODE_UNSPECIFIED_ERROR); ROR_ASSERT(result >= 0); + result = engine->RegisterEnumValue("ScriptRetCode", "SCRIPTRETCODE_ENGINE_NOT_CREATED", SCRIPTRETCODE_ENGINE_NOT_CREATED); ROR_ASSERT(result >= 0); + result = engine->RegisterEnumValue("ScriptRetCode", "SCRIPTRETCODE_CONTEXT_NOT_CREATED", SCRIPTRETCODE_CONTEXT_NOT_CREATED); ROR_ASSERT(result >= 0); + result = engine->RegisterEnumValue("ScriptRetCode", "SCRIPTRETCODE_SCRIPTUNIT_NOT_EXISTS", SCRIPTRETCODE_SCRIPTUNIT_NOT_EXISTS); ROR_ASSERT(result >= 0); + result = engine->RegisterEnumValue("ScriptRetCode", "SCRIPTRETCODE_SCRIPTUNIT_NO_MODULE", SCRIPTRETCODE_SCRIPTUNIT_NO_MODULE); ROR_ASSERT(result >= 0); + result = engine->RegisterEnumValue("ScriptRetCode", "SCRIPTRETCODE_FUNCTION_NOT_EXISTS", SCRIPTRETCODE_FUNCTION_NOT_EXISTS); ROR_ASSERT(result >= 0); + // class GameScript result = engine->RegisterObjectType("GameScriptClass", sizeof(GameScript), asOBJ_REF | asOBJ_NOCOUNT); ROR_ASSERT(result >= 0); @@ -83,12 +121,13 @@ void RoR::RegisterGameScript(asIScriptEngine *engine) result = engine->RegisterObjectMethod("GameScriptClass", "void unRegisterEvent(int)", asMETHOD(GameScript, unRegisterEvent), asCALL_THISCALL); ROR_ASSERT(result >= 0); result = engine->RegisterObjectMethod("GameScriptClass", "int getRegisteredEventsMask(int)", asMETHOD(GameScript, getRegisteredEventsMask), asCALL_THISCALL); ROR_ASSERT(result >= 0); result = engine->RegisterObjectMethod("GameScriptClass", "void setRegisteredEventsMask(int, int)", asMETHOD(GameScript, setRegisteredEventsMask), asCALL_THISCALL); ROR_ASSERT(result >= 0); - result = engine->RegisterObjectMethod("GameScriptClass", "int addScriptFunction(const string &in)", asMETHOD(GameScript, addScriptFunction), asCALL_THISCALL); ROR_ASSERT(result >= 0); - result = engine->RegisterObjectMethod("GameScriptClass", "int scriptFunctionExists(const string &in)", asMETHOD(GameScript, scriptFunctionExists), asCALL_THISCALL); ROR_ASSERT(result >= 0); - result = engine->RegisterObjectMethod("GameScriptClass", "int deleteScriptFunction(const string &in)", asMETHOD(GameScript, deleteScriptFunction), asCALL_THISCALL); ROR_ASSERT(result >= 0); - result = engine->RegisterObjectMethod("GameScriptClass", "int addScriptVariable(const string &in)", asMETHOD(GameScript, addScriptVariable), asCALL_THISCALL); ROR_ASSERT(result >= 0); - result = engine->RegisterObjectMethod("GameScriptClass", "int deleteScriptVariable(const string &in)", asMETHOD(GameScript, deleteScriptVariable), asCALL_THISCALL); ROR_ASSERT(result >= 0); - result = engine->RegisterObjectMethod("GameScriptClass", "int getScriptVariable(int, const string &in, ?&out)", asMETHOD(GameScript, getScriptVariable), asCALL_THISCALL); ROR_ASSERT(result >= 0); + result = engine->RegisterObjectMethod("GameScriptClass", "ScriptRetCode addScriptFunction(const string &in, int = -2)", asMETHOD(GameScript, addScriptFunction), asCALL_THISCALL); ROR_ASSERT(result >= 0); + result = engine->RegisterObjectMethod("GameScriptClass", "ScriptRetCode scriptFunctionExists(const string &in, int = -2)", asMETHOD(GameScript, scriptFunctionExists), asCALL_THISCALL); ROR_ASSERT(result >= 0); + result = engine->RegisterObjectMethod("GameScriptClass", "ScriptRetCode deleteScriptFunction(const string &in, int = -2)", asMETHOD(GameScript, deleteScriptFunction), asCALL_THISCALL); ROR_ASSERT(result >= 0); + result = engine->RegisterObjectMethod("GameScriptClass", "ScriptRetCode addScriptVariable(const string &in, int = -2)", asMETHOD(GameScript, addScriptVariable), asCALL_THISCALL); ROR_ASSERT(result >= 0); + result = engine->RegisterObjectMethod("GameScriptClass", "ScriptRetCode scriptVariableExists(const string &in, int = -2)", asMETHOD(GameScript, scriptVariableExists), asCALL_THISCALL); ROR_ASSERT(result >= 0); + result = engine->RegisterObjectMethod("GameScriptClass", "ScriptRetCode deleteScriptVariable(const string &in, int = -2)", asMETHOD(GameScript, deleteScriptVariable), asCALL_THISCALL); ROR_ASSERT(result >= 0); + result = engine->RegisterObjectMethod("GameScriptClass", "ScriptRetCode getScriptVariable(const string &in, ?&out, int = -2)", asMETHOD(GameScript, getScriptVariable), asCALL_THISCALL); ROR_ASSERT(result >= 0); result = engine->RegisterObjectMethod("GameScriptClass", "void clearEventCache()", asMETHOD(GameScript, clearEventCache), asCALL_THISCALL); ROR_ASSERT(result >= 0); result = engine->RegisterObjectMethod("GameScriptClass", "int sendGameCmd(const string &in)", asMETHOD(GameScript, sendGameCmd), asCALL_THISCALL); ROR_ASSERT(result >= 0); result = engine->RegisterObjectMethod("GameScriptClass", "array@ getRunningScripts()", asMETHOD(GameScript, getRunningScripts), asCALL_THISCALL); ROR_ASSERT(result >= 0); From 100cb4d93e847bef583d8b0a06d4fef208eb3adb Mon Sep 17 00:00:00 2001 From: ohlidalp Date: Thu, 5 Dec 2024 03:56:31 +0100 Subject: [PATCH 31/36] example_terrn_raceConverter.as: API update, err handling fix --- .../scripts/example_terrn_raceConverter.as | 1222 +++++++++-------- 1 file changed, 619 insertions(+), 603 deletions(-) diff --git a/resources/scripts/example_terrn_raceConverter.as b/resources/scripts/example_terrn_raceConverter.as index 40a997d06a..d1db6b22ee 100644 --- a/resources/scripts/example_terrn_raceConverter.as +++ b/resources/scripts/example_terrn_raceConverter.as @@ -1,612 +1,628 @@ -/// \title terrn2 race converter -/// \brief imports races from scripts and generates race-def files. -/// uses new section in terrn2 format: [Races] -/// ^ each line is a tobj-like file with any extension (i.e. *.race) which is loaded by the race system. -/// -/// The program flow of this script got a little crazy; -/// ^ I wanted a fluidly updating UI, performing just one step (1 doc conversion / 1 file write) per frame. -/// ================================================== - -// The race system -#include "races.as" -racesManager races; - -// document viewer+editor -#include "genericdoc_utils.as" -genericdoc_utils::GenericDocEditor gdEditor; - -// Window [X] button handler -#include "imgui_utils.as" -imgui_utils::CloseWindowPrompt closeBtnHandlerUnique; - -enum Stage // in order of processing -{ - STAGE_INIT, // detects races - STAGE_CONVERT, // converts all races to GenericDocument race-defs - STAGE_FIXTERRN2, // modify .terrn2 file - add [Races], remove [Scripts] - STAGE_IDLE, // Waiting for button press - STAGE_PUSHMSG, // request game to create project - STAGE_GETPROJECT, // fetch created project from modcache - STAGE_WRITERACES, - STAGE_WRITETERRN2, - STAGE_DONE, - STAGE_ERROR -} -Stage stage = STAGE_INIT; -string error = ""; -string projectName = ""; -CacheEntryClass@ projectEntry; -array convertedRaces; -array convertedRaceFileNames; - -string fileBeingWritten = ""; -GenericDocumentClass@ convertedTerrn2; -int topWrittenRace = -1; - -//#region Game Callbacks - -void main() -{ - // one-time config - gdEditor.gdeCloseBtnHandler.cfgCloseImmediatelly=true; -} - -void frameStep(float dt) -{ - // === DRAW UI === - if ( ImGui::Begin("Race import", closeBtnHandlerUnique.windowOpen, /*flags:*/0)) - { - // Draw the "Terminate this script?" prompt on the top (if not disabled by config). - closeBtnHandlerUnique.draw(); - drawUI(); - ImGui::End(); - } - - - // === PERFORM IMPORT STEP === - advanceImportOneStep(); -} - -//#endregion - -//#region UI drawing -void drawUI() -{ - // Draw document window - if (@gdEditor.displayedDocument != null) - { - gdEditor.drawSeparateWindow(); - } - - drawDetectedRaces(); - if (@convertedTerrn2 != null) - { - if (ImGui::SmallButton("Preview terrn2")) - { - gdEditor.setDocument(convertedTerrn2, projectName+'.terrn2'); - } - } - ImGui::Separator(); - switch(stage) - { - case STAGE_IDLE: - { - if (@game.getTerrain() != null && stage == STAGE_IDLE ) - { - if ( ImGui::Button("Convert races from script to terrn2 [Races]")) - { - stage = STAGE_PUSHMSG; - } - } - break; - } - case STAGE_ERROR: - { - ImGui::Text("ERROR! "+error); - break; - } - case STAGE_PUSHMSG: - { - ImGui::Text("Performing MSG_EDI_CREATE_PROJECT_REQUESTED"); - break; - } - case STAGE_WRITERACES: - { - ImGui::TextDisabled("Writing .race files:"); - ImGui::Text(fileBeingWritten); - break; - } - case STAGE_WRITETERRN2: - { - ImGui::TextDisabled("OverWriting terrn2 file"); - ImGui::Text(projectName + ".terrn2"); - break; - } - case STAGE_DONE: - { - ImGui::Text('DONE'); - break; - } - case STAGE_GETPROJECT: - { - ImGui::Text('WARNING - stuck in stage GETPROJECT (name: "'+projectName+'")'); - break; - } - default: - { - break; - } - } - - -} - -void drawDetectedRaces() -{ - ImGui::PushID("drawDetectedRaces"); - if (@races == null) - { - ImGui::Text("ERR: races null"); - return; - } - - if (@races.raceList == null) - { - ImGui::Text("ERR raceList null"); - return; - } - - ImGui::Text("Found " + races.raceList.length() + " races"); - for (uint i=0; i < races.raceList.length(); i++) - { - ImGui::PushID(i); - raceBuilder@ race = races.raceList[i]; - if (@race == null) - { - ImGui::Text("ERR racebuilder null!"); - continue; - } - ImGui::Bullet(); - ImGui::Text(race.raceName); - ImGui::SameLine(); - ImGui::TextDisabled(race.checkPointsCount + " checkpoints"); - if (@convertedRaces[i] != null) - { - ImGui::SameLine(); - if ( ImGui::SmallButton("Preview")) - { - gdEditor.setDocument(convertedRaces[i], races.raceList[i].raceName); - - } - } - ImGui::PopID(); // i - } - ImGui::PopID(); //"drawDetectedRaces" -} - -//#endregion - -//#region STAGE_INIT -void initializeRacesData() -{ - // find the terrain script - array nids = game.getRunningScripts(); - int terrnScriptNid = -1; - for (uint i = 0; i < nids.length(); i++) - { - int nid = nids[i]; - dictionary@ info = game.getScriptDetails(nid); - int category = int(info['scriptCategory']); - if (category == SCRIPT_CATEGORY_TERRAIN) - { - terrnScriptNid = nid; - } - } - - if (terrnScriptNid == -1) - { - stage = STAGE_ERROR; - error = "Terrain script not found!"; - return; - } - - // moment of truth - retrieve the races using new API `game.getScriptVariable()` - int result = game.getScriptVariable(terrnScriptNid, 'races', races); +/// \title terrn2 race converter +/// \brief imports races from scripts and generates race-def files. +/// uses new section in terrn2 format: [Races] +/// ^ each line is a tobj-like file with any extension (i.e. *.race) which is loaded by the race system. +/// +/// The program flow of this script got a little crazy; +/// ^ I wanted a fluidly updating UI, performing just one step (1 doc conversion / 1 file write) per frame. +/// ================================================== + +// The race system +#include "races.as" +racesManager races; + +// document viewer+editor +#include "genericdoc_utils.as" +genericdoc_utils::GenericDocEditor gdEditor; + +// Window [X] button handler +#include "imgui_utils.as" +imgui_utils::CloseWindowPrompt closeBtnHandlerUnique; + +enum Stage // in order of processing +{ + STAGE_INIT, // detects races + STAGE_CONVERT, // converts all races to GenericDocument race-defs + STAGE_FIXTERRN2, // modify .terrn2 file - add [Races], remove [Scripts] + STAGE_BUTTON, // Waiting for button press + STAGE_PUSHMSG, // request game to create project + STAGE_GETPROJECT, // fetch created project from modcache + STAGE_WRITERACES, + STAGE_WRITETERRN2, + STAGE_DONE, + STAGE_ERROR +} +Stage stage = STAGE_INIT; +string error = ""; +string projectName = ""; +CacheEntryClass@ projectEntry; +array convertedRaces; +array convertedRaceFileNames; + +string fileBeingWritten = ""; +GenericDocumentClass@ convertedTerrn2; +int topWrittenRace = -1; + +//#region Game Callbacks + +void main() +{ + // one-time config + gdEditor.gdeCloseBtnHandler.cfgCloseImmediatelly=true; +} + +void frameStep(float dt) +{ + // === DRAW UI === + if ( ImGui::Begin("Race import", closeBtnHandlerUnique.windowOpen, /*flags:*/0)) + { + // Draw the "Terminate this script?" prompt on the top (if not disabled by config). + closeBtnHandlerUnique.draw(); + drawUI(); + ImGui::End(); + } + + + // === PERFORM IMPORT STEP === + advanceImportOneStep(); +} + +//#endregion + +//#region UI drawing +void drawUI() +{ + // Draw document window + if (@gdEditor.displayedDocument != null) + { + gdEditor.drawSeparateWindow(); + } + + drawDetectedRaces(); + if (@convertedTerrn2 != null) + { + if (ImGui::SmallButton("Preview terrn2")) + { + gdEditor.setDocument(convertedTerrn2, projectName+'.terrn2'); + } + } + ImGui::Separator(); + switch(stage) + { + case STAGE_BUTTON: + { + if (@game.getTerrain() != null && stage == STAGE_BUTTON ) + { + if ( ImGui::Button("Convert races from script to terrn2 [Races]")) + { + stage = STAGE_PUSHMSG; + } + } + break; + } + case STAGE_ERROR: + { + ImGui::Text("ERROR! "+error); + break; + } + case STAGE_PUSHMSG: + { + ImGui::Text("Performing MSG_EDI_CREATE_PROJECT_REQUESTED"); + break; + } + case STAGE_WRITERACES: + { + ImGui::TextDisabled("Writing .race files:"); + ImGui::Text(fileBeingWritten); + break; + } + case STAGE_WRITETERRN2: + { + ImGui::TextDisabled("OverWriting terrn2 file"); + ImGui::Text(projectName + ".terrn2"); + break; + } + case STAGE_DONE: + { + ImGui::Text('DONE'); + break; + } + case STAGE_GETPROJECT: + { + ImGui::Text('WARNING - stuck in stage GETPROJECT (name: "'+projectName+'")'); + break; + } + default: + { + break; + } + } + + +} + +void drawDetectedRaces() +{ + ImGui::PushID("drawDetectedRaces"); + if (@races == null) + { + ImGui::Text("ERR: races null"); + return; + } + + if (@races.raceList == null) + { + ImGui::Text("ERR raceList null"); + return; + } + + ImGui::Text("Found " + races.raceList.length() + " races"); + for (uint i=0; i < races.raceList.length(); i++) + { + ImGui::PushID(i); + raceBuilder@ race = races.raceList[i]; + if (@race == null) + { + ImGui::Text("ERR racebuilder null!"); + continue; + } + ImGui::Bullet(); + ImGui::Text(race.raceName); + ImGui::SameLine(); + ImGui::TextDisabled(race.checkPointsCount + " checkpoints"); + if (@convertedRaces[i] != null) + { + ImGui::SameLine(); + if ( ImGui::SmallButton("Preview")) + { + gdEditor.setDocument(convertedRaces[i], races.raceList[i].raceName); + + } + } + ImGui::PopID(); // i + } + ImGui::PopID(); //"drawDetectedRaces" +} + +//#endregion + +//#region STAGE_INIT +void initializeRacesData() +{ + // find the terrain script + array nids = game.getRunningScripts(); + int terrnScriptNid = -1; + for (uint i = 0; i < nids.length(); i++) + { + int nid = nids[i]; + dictionary@ info = game.getScriptDetails(nid); + int category = int(info['scriptCategory']); + if (category == SCRIPT_CATEGORY_TERRAIN) + { + terrnScriptNid = nid; + } + } + + if (terrnScriptNid == -1) + { + stage = STAGE_ERROR; + error = "Terrain script not found!"; + return; + } + + // new API `game.scriptVariableExists()` + int result = game.scriptVariableExists('races', terrnScriptNid); if (result < 0) { stage = STAGE_ERROR; - error = " game.getScriptVariable() returned "+result; - return; - } - - if (@races == null) - { - stage = STAGE_ERROR; - error = " game.getScriptVariable() reported 'OK' but did not fetch the data"; - return; - } - - for (uint i=0; i < races.raceList.length(); i++) - { - raceBuilder@ race = races.raceList[i]; - convertedRaces.insertLast(null); - convertedRaceFileNames.insertLast(""); - } -} -//#endregion - -//#region STAGE_CONVERT -bool convertNextRace() -{ - - // seek first unconverted race; convert it; break loop - for (uint i=0; i < races.raceList.length(); i++) - { - if (@convertedRaces[i] == null) + if (result == SCRIPTRETCODE_AS_NO_GLOBAL_VAR) { - @convertedRaces[i] = convertSingleRace(races.raceList[i]); - convertedRaceFileNames[i] = generateRaceFileName(races.raceList[i].raceName); - return true; - } - } - - return false; -} - -// HELPERS: - -const string BADCHARS="\\/:%* "; -const string GOODCHAR="_"; -string generateRaceFileName(string raceName) -{ - string filename = raceName + ".race"; - for (uint i=0; i " + filename); - return filename; -} - -void appendKeyValuePair( GenericDocContextClass@ ctx, string key, string value) -{ - ctx.appendTokKeyword( key); - ctx.appendTokString(value); - ctx.appendTokLineBreak(); -} - -void appendKeyValuePair( GenericDocContextClass@ ctx, string key, int value) -{ - ctx.appendTokKeyword( key); - ctx.appendTokInt(value); - ctx.appendTokLineBreak(); -} - -GenericDocumentClass@ convertSingleRace(raceBuilder@ race) -{ - GenericDocumentClass doc; - GenericDocContextClass ctx(doc); - - ctx.appendTokComment( " ~~ New 'race-def' format (file extension: .race). ~~");ctx.appendTokLineBreak(); - ctx.appendTokComment( " Each race file specifies a single race");ctx.appendTokLineBreak(); - ctx.appendTokComment( " In .terrn2 file, list the race files under new section [Races]");ctx.appendTokLineBreak(); - ctx.appendTokComment( " Filenames must include extension and end with = (like scripts do)");ctx.appendTokLineBreak(); - ctx.appendTokComment( " Race system supports alternating paths!");ctx.appendTokLineBreak(); - ctx.appendTokComment( " Checkpoint format: checkpointNum(1+), altpathNum(1+), x, y, z, rotX, rotY, rotZ, objName(override, optional)");ctx.appendTokLineBreak(); - ctx.appendTokComment( " By convention, the checkpoint meshes are oriented sideways (facing X axis)");ctx.appendTokLineBreak(); - ctx.appendTokLineBreak(); - - appendKeyValuePair(ctx, "race_name", race.raceName); - appendKeyValuePair(ctx, "race_laps", race.laps); - appendKeyValuePair(ctx, "race_checkpoint_object", race.exporterCheckpointObjName); - appendKeyValuePair(ctx, "race_start_object", race.exporterStartObjName); - appendKeyValuePair(ctx, "race_finish_object", race.exporterFinishObjName); - - ctx.appendTokLineBreak(); - - ctx.appendTokKeyword( "begin_checkpoints"); - - ctx.appendTokLineBreak(); - - for (uint i=0; i < race.checkpoints.length(); i++) - { - uint numAltPaths = race.getRealInstanceCount(int(i)); - for (uint j = 0; j < numAltPaths; j++) - { - ctx.appendTokInt((i+1)); // checkpointNum (1+) - ctx.appendTokInt((j+1)); // altpathNum (1+) - const double[] args = race.checkpoints[i][j]; - ctx.appendTokFloat(args[0]); // pos X - ctx.appendTokFloat(args[1]); // pos Y - ctx.appendTokFloat(args[2]); // pos Z - ctx.appendTokFloat(args[3]); // rot X - ctx.appendTokFloat(args[4]); // rot Y - ctx.appendTokFloat(args[5]); // rot Z - string defaultObjName = (i==0) - ? race.exporterStartObjName - : (i==race.checkpoints.length()-1) ? race.exporterFinishObjName : race.exporterCheckpointObjName; - string actualObjName = race.objNames[i][j]; - if (actualObjName != defaultObjName) - { - ctx.appendTokString(actualObjName); - } - ctx.appendTokLineBreak(); - } - } - - ctx.appendTokKeyword( "end_checkpoints"); - ctx.appendTokLineBreak(); - - return doc; -} - -//#endregion - -//#region STAGE_FIXTERRN2 -string BRACE="["; -void fixupTerrn2Document() -{ - TerrainClass@ terrain = game.getTerrain(); - if (@terrain == null) - { - game.log("fixupTerrn2Document(): no terrain loaded, nothing to do!"); - return; - } - @convertedTerrn2 = GenericDocumentClass(); - - if (!convertedTerrn2.loadFromResource(terrain.getTerrainFileName(), terrain.getTerrainFileResourceGroup(), genericdoc_utils::FLAGSET_TERRN2)) - { - game.log("fixupTerrn2Document(): could not load terrn2 document, nothing to do!"); - return; - } - - GenericDocContextClass ctx(convertedTerrn2); - - // Delete section [Scripts] and all it contains - bool inSectionScripts = false; - while (!ctx.endOfFile()) - { - // entering section [Scripts] - if (!inSectionScripts && ctx.isTokKeyword() && ctx.getTokKeyword()=="[Scripts]") - { - inSectionScripts=true; - } - // leaving section [Scripts] - else if (inSectionScripts && ctx.isTokKeyword() && ctx.getTokKeyword()[0]==BRACE[0]) - { - - inSectionScripts=false; - } - - // erase all script info - if (inSectionScripts) - { - ctx.eraseToken(); - } - // ... and also fix up the terrain name while at it. - else if (ctx.isTokKeyword() && ctx.getTokKeyword() == "Name") - { - ctx.setTokString(1, projectName); - // make it a little less obvious that GenericDocument actually breaks the terrain name into pieces - // ^ just hard erase everything between the new name and a linebreak - ctx.moveNext(); - ctx.moveNext(); - while (!ctx.isTokLineBreak()) - { - ctx.eraseToken(); - } - } - ctx.seekNextLine(); - } - - // Append section [Races] - ctx.appendTokLineBreak(); - ctx.appendTokKeyword("[Races]"); - ctx.appendTokLineBreak(); - for (uint i=0; i < convertedRaceFileNames.length(); i++) - { - ctx.appendTokString(convertedRaceFileNames[i]); - ctx.appendTokLineBreak(); - } - - - -} -//#endregion - -// nothing for STAGE_IDLE - -//#region STAGE_PUSHMSG -void pushMsgRequestCreateProject() -{ - TerrainClass@ terrain = game.getTerrain(); - projectName = terrain.getTerrainName() + " [Races] ~"+thisScript; - - // Fetch terrain's modcache entry - CacheEntryClass@ src_entry = modcache.findEntryByFilename(LOADER_TYPE_TERRAIN, /*partial:*/false, terrain.getTerrainFileName()); - if (@src_entry == null) - { - error = "Not found in modcache!!"; - stage = STAGE_ERROR; - } - - // request project to be created from that cache entry - game.pushMessage(MSG_EDI_CREATE_PROJECT_REQUESTED, { - {'name', projectName}, - {'source_entry', src_entry} - }); -} -//#endregion - -//#region STAGE_GETPROJECT -void getProject() -{ - @projectEntry = modcache.findEntryByFilename(LOADER_TYPE_TERRAIN, /*partial:*/false, projectName+'.terrn2'); - if (@projectEntry != null) - { - stage = STAGE_WRITERACES; - } - else - { - stage=STAGE_ERROR; - error="Could not load project entry"; - } -} -//#endregion - -//#region STAGE_WRITERACES -void writeNextRace() -{ - for (uint i=0; i < races.raceList.length(); i++) - { - if (int(i) <= topWrittenRace) - { - continue; // already handled - } - - if (fileBeingWritten == "") - { - string filename = convertedRaceFileNames[i]; - fileBeingWritten = filename; - // game.log('DBG WRITERACES ['+i+']: fileBeingWritten='+fileBeingWritten); - break; + error = "Nothing to do ~ this terrain has no races (race system isn't loaded)."; } else { - // write the ' .race' file in separate frame so the user sees the correct filename on screen. - if (@projectEntry == null) - { - stage=STAGE_ERROR; - error="null project entry while generating files"; - break; - } - if (@convertedRaces[i] == null) - { - stage=STAGE_ERROR; - error="null converted race at position ["+i+"]"; - break; - } - - convertedRaces[i].saveToResource(fileBeingWritten, projectEntry.resource_group); - topWrittenRace=int(i); - - // game.log('DBG GENFILES ['+i+']: saved file '+fileBeingWritten); - fileBeingWritten = ""; - if (i == races.raceList.length()-1) - { - stage=STAGE_WRITETERRN2; - } - break; - } - } -} -//#endregion - -//#region STAGE_WRITETERRN2 -void writeTerrn2() -{ - forceExportINI(convertedTerrn2); - - // delete original file (GenericDocument cannot overwrite) - game.deleteResource(projectName+".terrn2", projectEntry.resource_group); - - // write out modified file - convertedTerrn2.saveToResource(projectName+".terrn2", projectEntry.resource_group); -} - -void forceExportINI(GenericDocumentClass@ doc) -{ - // hack to force GenericDocument export INI: pass over all non-braced keywords and append = to them. - GenericDocContextClass ctx(doc); //reset context - while (!ctx.endOfFile()) - { - if (ctx.isTokKeyword() && ctx.getTokKeyword()[0]!=BRACE[0] ) - { - ctx.setTokKeyword(0,ctx.getTokKeyword()+"="); + error = " game.scriptVariableExists() returned "+result; } - else if (ctx.isTokString() && ctx.isTokLineBreak()) - { - // treat strings on line start as keywords (those are keywords that decayed to strings due to some chars) - ctx.setTokKeyword(0,ctx.getTokString()+"="); - } - ctx.moveNext(); - } -} -//#endregion - - - - -void advanceImportOneStep() -{ - switch (stage) - { - case STAGE_INIT: - { - initializeRacesData(); - stage = STAGE_CONVERT; - break; - } - case STAGE_PUSHMSG: - { - pushMsgRequestCreateProject(); - stage = STAGE_GETPROJECT; - break; - } - - case STAGE_GETPROJECT: - { - getProject(); - break; - } - - case STAGE_CONVERT: - { - if (!convertNextRace()) - { - stage = STAGE_FIXTERRN2; - } - break; - } - - case STAGE_WRITERACES: - { - - writeNextRace(); - break; - } - - case STAGE_FIXTERRN2: - { - fixupTerrn2Document(); - stage = STAGE_IDLE; - break; - } - - case STAGE_WRITETERRN2: - { - writeTerrn2(); - stage = STAGE_DONE; - break; - } - - default: - break; + return; } -} - - - - - - - - - - + + // moment of truth - retrieve the races using new API `game.getScriptVariable()` + result = game.getScriptVariable('races', races, terrnScriptNid); + if (result < 0) + { + stage = STAGE_ERROR; + error = " game.getScriptVariable() returned "+result; + return; + } + + if (@races == null) + { + stage = STAGE_ERROR; + error = " game.getScriptVariable() reported 'OK' but did not fetch the data"; + return; + } + + for (uint i=0; i < races.raceList.length(); i++) + { + raceBuilder@ race = races.raceList[i]; + convertedRaces.insertLast(null); + convertedRaceFileNames.insertLast(""); + } +} +//#endregion + +//#region STAGE_CONVERT +bool convertNextRace() +{ + + // seek first unconverted race; convert it; break loop + for (uint i=0; i < races.raceList.length(); i++) + { + if (@convertedRaces[i] == null) + { + @convertedRaces[i] = convertSingleRace(races.raceList[i]); + convertedRaceFileNames[i] = generateRaceFileName(races.raceList[i].raceName); + return true; + } + } + + return false; +} + +// HELPERS: + +const string BADCHARS="\\/:%* "; +const string GOODCHAR="_"; +string generateRaceFileName(string raceName) +{ + string filename = raceName + ".race"; + for (uint i=0; i " + filename); + return filename; +} + +void appendKeyValuePair( GenericDocContextClass@ ctx, string key, string value) +{ + ctx.appendTokKeyword( key); + ctx.appendTokString(value); + ctx.appendTokLineBreak(); +} + +void appendKeyValuePair( GenericDocContextClass@ ctx, string key, int value) +{ + ctx.appendTokKeyword( key); + ctx.appendTokInt(value); + ctx.appendTokLineBreak(); +} + +GenericDocumentClass@ convertSingleRace(raceBuilder@ race) +{ + GenericDocumentClass doc; + GenericDocContextClass ctx(doc); + + ctx.appendTokComment( " ~~ New 'race-def' format (file extension: .race). ~~");ctx.appendTokLineBreak(); + ctx.appendTokComment( " Each race file specifies a single race");ctx.appendTokLineBreak(); + ctx.appendTokComment( " In .terrn2 file, list the race files under new section [Races]");ctx.appendTokLineBreak(); + ctx.appendTokComment( " Filenames must include extension and end with = (like scripts do)");ctx.appendTokLineBreak(); + ctx.appendTokComment( " Race system supports alternating paths!");ctx.appendTokLineBreak(); + ctx.appendTokComment( " Checkpoint format: checkpointNum(1+), altpathNum(1+), x, y, z, rotX, rotY, rotZ, objName(override, optional)");ctx.appendTokLineBreak(); + ctx.appendTokComment( " By convention, the checkpoint meshes are oriented sideways (facing X axis)");ctx.appendTokLineBreak(); + ctx.appendTokLineBreak(); + + appendKeyValuePair(ctx, "race_name", race.raceName); + appendKeyValuePair(ctx, "race_laps", race.laps); + appendKeyValuePair(ctx, "race_checkpoint_object", race.exporterCheckpointObjName); + appendKeyValuePair(ctx, "race_start_object", race.exporterStartObjName); + appendKeyValuePair(ctx, "race_finish_object", race.exporterFinishObjName); + + ctx.appendTokLineBreak(); + + ctx.appendTokKeyword( "begin_checkpoints"); + + ctx.appendTokLineBreak(); + + for (uint i=0; i < race.checkpoints.length(); i++) + { + uint numAltPaths = race.getRealInstanceCount(int(i)); + for (uint j = 0; j < numAltPaths; j++) + { + ctx.appendTokInt((i+1)); // checkpointNum (1+) + ctx.appendTokInt((j+1)); // altpathNum (1+) + const double[] args = race.checkpoints[i][j]; + ctx.appendTokFloat(args[0]); // pos X + ctx.appendTokFloat(args[1]); // pos Y + ctx.appendTokFloat(args[2]); // pos Z + ctx.appendTokFloat(args[3]); // rot X + ctx.appendTokFloat(args[4]); // rot Y + ctx.appendTokFloat(args[5]); // rot Z + string defaultObjName = (i==0) + ? race.exporterStartObjName + : (i==race.checkpoints.length()-1) ? race.exporterFinishObjName : race.exporterCheckpointObjName; + string actualObjName = race.objNames[i][j]; + if (actualObjName != defaultObjName) + { + ctx.appendTokString(actualObjName); + } + ctx.appendTokLineBreak(); + } + } + + ctx.appendTokKeyword( "end_checkpoints"); + ctx.appendTokLineBreak(); + + return doc; +} + +//#endregion + +//#region STAGE_FIXTERRN2 +string BRACE="["; +void fixupTerrn2Document() +{ + TerrainClass@ terrain = game.getTerrain(); + if (@terrain == null) + { + game.log("fixupTerrn2Document(): no terrain loaded, nothing to do!"); + return; + } + @convertedTerrn2 = GenericDocumentClass(); + + if (!convertedTerrn2.loadFromResource(terrain.getTerrainFileName(), terrain.getTerrainFileResourceGroup(), genericdoc_utils::FLAGSET_TERRN2)) + { + game.log("fixupTerrn2Document(): could not load terrn2 document, nothing to do!"); + return; + } + + GenericDocContextClass ctx(convertedTerrn2); + + // Delete section [Scripts] and all it contains + bool inSectionScripts = false; + while (!ctx.endOfFile()) + { + // entering section [Scripts] + if (!inSectionScripts && ctx.isTokKeyword() && ctx.getTokKeyword()=="[Scripts]") + { + inSectionScripts=true; + } + // leaving section [Scripts] + else if (inSectionScripts && ctx.isTokKeyword() && ctx.getTokKeyword()[0]==BRACE[0]) + { + + inSectionScripts=false; + } + + // erase all script info + if (inSectionScripts) + { + ctx.eraseToken(); + } + // ... and also fix up the terrain name while at it. + else if (ctx.isTokKeyword() && ctx.getTokKeyword() == "Name") + { + ctx.setTokString(1, projectName); + // make it a little less obvious that GenericDocument actually breaks the terrain name into pieces + // ^ just hard erase everything between the new name and a linebreak + ctx.moveNext(); + ctx.moveNext(); + while (!ctx.isTokLineBreak()) + { + ctx.eraseToken(); + } + } + ctx.seekNextLine(); + } + + // Append section [Races] + ctx.appendTokLineBreak(); + ctx.appendTokKeyword("[Races]"); + ctx.appendTokLineBreak(); + for (uint i=0; i < convertedRaceFileNames.length(); i++) + { + ctx.appendTokString(convertedRaceFileNames[i]); + ctx.appendTokLineBreak(); + } + + + +} +//#endregion + +// nothing for STAGE_IDLE + +//#region STAGE_PUSHMSG +void pushMsgRequestCreateProject() +{ + TerrainClass@ terrain = game.getTerrain(); + projectName = terrain.getTerrainName() + " [Races] ~"+thisScript; + + // Fetch terrain's modcache entry + CacheEntryClass@ src_entry = modcache.findEntryByFilename(LOADER_TYPE_TERRAIN, /*partial:*/false, terrain.getTerrainFileName()); + if (@src_entry == null) + { + error = "Not found in modcache!!"; + stage = STAGE_ERROR; + } + + // request project to be created from that cache entry + game.pushMessage(MSG_EDI_CREATE_PROJECT_REQUESTED, { + {'name', projectName}, + {'source_entry', src_entry} + }); +} +//#endregion + +//#region STAGE_GETPROJECT +void getProject() +{ + @projectEntry = modcache.findEntryByFilename(LOADER_TYPE_TERRAIN, /*partial:*/false, projectName+'.terrn2'); + if (@projectEntry != null) + { + stage = STAGE_WRITERACES; + } + else + { + stage=STAGE_ERROR; + error="Could not load project entry"; + } +} +//#endregion + +//#region STAGE_WRITERACES +void writeNextRace() +{ + for (uint i=0; i < races.raceList.length(); i++) + { + if (int(i) <= topWrittenRace) + { + continue; // already handled + } + + if (fileBeingWritten == "") + { + string filename = convertedRaceFileNames[i]; + fileBeingWritten = filename; + // game.log('DBG WRITERACES ['+i+']: fileBeingWritten='+fileBeingWritten); + break; + } + else + { + // write the ' .race' file in separate frame so the user sees the correct filename on screen. + if (@projectEntry == null) + { + stage=STAGE_ERROR; + error="null project entry while generating files"; + break; + } + if (@convertedRaces[i] == null) + { + stage=STAGE_ERROR; + error="null converted race at position ["+i+"]"; + break; + } + + convertedRaces[i].saveToResource(fileBeingWritten, projectEntry.resource_group); + topWrittenRace=int(i); + + // game.log('DBG GENFILES ['+i+']: saved file '+fileBeingWritten); + fileBeingWritten = ""; + if (i == races.raceList.length()-1) + { + stage=STAGE_WRITETERRN2; + } + break; + } + } +} +//#endregion + +//#region STAGE_WRITETERRN2 +void writeTerrn2() +{ + forceExportINI(convertedTerrn2); + + // delete original file (GenericDocument cannot overwrite) + game.deleteResource(projectName+".terrn2", projectEntry.resource_group); + + // write out modified file + convertedTerrn2.saveToResource(projectName+".terrn2", projectEntry.resource_group); +} + +void forceExportINI(GenericDocumentClass@ doc) +{ + // hack to force GenericDocument export INI: pass over all non-braced keywords and append = to them. + GenericDocContextClass ctx(doc); //reset context + while (!ctx.endOfFile()) + { + if (ctx.isTokKeyword() && ctx.getTokKeyword()[0]!=BRACE[0] ) + { + ctx.setTokKeyword(0,ctx.getTokKeyword()+"="); + } + else if (ctx.isTokString() && ctx.isTokLineBreak()) + { + // treat strings on line start as keywords (those are keywords that decayed to strings due to some chars) + ctx.setTokKeyword(0,ctx.getTokString()+"="); + } + ctx.moveNext(); + } +} +//#endregion + + + + +void advanceImportOneStep() +{ + switch (stage) + { + case STAGE_INIT: + { + initializeRacesData(); + if (stage != STAGE_ERROR) { stage = STAGE_CONVERT; } + break; + } + case STAGE_PUSHMSG: + { + pushMsgRequestCreateProject(); + if (stage != STAGE_ERROR) { stage = STAGE_GETPROJECT; } + break; + } + + case STAGE_GETPROJECT: + { + getProject(); + break; + } + + case STAGE_CONVERT: + { + if (!convertNextRace()) + { + if (stage != STAGE_ERROR) { stage = STAGE_FIXTERRN2; } + } + break; + } + + case STAGE_WRITERACES: + { + + writeNextRace(); + break; + } + + case STAGE_FIXTERRN2: + { + fixupTerrn2Document(); + if (stage != STAGE_ERROR) { stage = STAGE_BUTTON; } + break; + } + + case STAGE_WRITETERRN2: + { + writeTerrn2(); + if (stage != STAGE_ERROR) { stage = STAGE_DONE; } + break; + } + + default: + break; + } +} + + + + + + + + + + From b8886d2bedeb5e2a4b75336febec62f23a6a1fbf Mon Sep 17 00:00:00 2001 From: ohlidalp Date: Thu, 5 Dec 2024 04:17:56 +0100 Subject: [PATCH 32/36] example_autotest_scenario.as: clarified step 0 --- resources/scripts/example_autotest_scenario.as | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/resources/scripts/example_autotest_scenario.as b/resources/scripts/example_autotest_scenario.as index 26541abe76..83bc10b120 100644 --- a/resources/scripts/example_autotest_scenario.as +++ b/resources/scripts/example_autotest_scenario.as @@ -13,7 +13,7 @@ // Global vars - config string cfgTerrnFilename = "simple2.terrn2"; string cfgVehicleFilename = "b6b0UID-semi.truck"; -int cfgNumSpawns = 3; +uint cfgNumSpawns = 3; int cfgStopAfterNumDeletes = 1; // Optional, Use -1 to disable; this seems to reproduce a dangling pointer crash in SoundScriptManager. // Global vars - test step tracking @@ -54,7 +54,7 @@ void main() - game.log("Automated test is running..."); + game.log("Automated test script: enters preconfigured terrain ("+cfgTerrnFilename+") and keeps loading/unloading preconfigured vehicle ("+cfgTerrnFilename+")"); } void eventCallbackEx(scriptEvents ev, int arg1, int arg2ex, int arg3ex, int arg4ex, string arg5ex, string arg6ex, string arg7ex, string arg8ex) @@ -81,8 +81,9 @@ void frameStep(float dt) switch (gStep) { case 0: - testLogStep("Waiting for the game to fully load..."); + testLogStep("Waiting for main menu (to load preconfigured terrain "+cfgTerrnFilename+")..."); // this isn't really needed now since `frameStep()` updates are halted during bootstrap, but let's pretend we do async rendering :) + // UPDATE: since this became an example it's useful to let users return to main menu. if (gCvarAppState.getInt() == 1) { gStep++; @@ -150,7 +151,7 @@ void frameStep(float dt) case 8: testLogStep("Removing AI vehicles"); - for (int i = 0; i < gVehicleIDs.length(); i++) + for (uint i = 0; i < gVehicleIDs.length(); i++) { game.pushMessage(MSG_SIM_DELETE_ACTOR_REQUESTED, { {'instance_id', gVehicleIDs[i]} }); gTotalDeletes++; From 7739bd1fa6f4b4e425067bdb75546e22c0c14c0d Mon Sep 17 00:00:00 2001 From: ohlidalp Date: Thu, 5 Dec 2024 04:18:29 +0100 Subject: [PATCH 33/36] AI.as: Don't crash script if no waypoints defined. --- resources/scripts/AI.as | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/resources/scripts/AI.as b/resources/scripts/AI.as index a1e1c2b3de..609d509610 100644 --- a/resources/scripts/AI.as +++ b/resources/scripts/AI.as @@ -11,9 +11,14 @@ void main() for (int x = 0; x < game.getAIVehicleCount(); x++) { + string spawn_vehiclename = game.getAIVehicleName(x); array waypoints = game.getWaypoints(x); + if (waypoints.length() == 0) + { + game.log("Vehicle AI: No waypoints defined for vehicle '"+spawn_vehiclename+"' ("+(x+1)+"/"+game.getAIVehicleCount()+"), skipping it..."); + continue; // Skip this vehicle + } - string spawn_vehiclename = game.getAIVehicleName(x); vector3 spawn_pos = vector3(waypoints[0].x + translation_x, waypoints[0].y, waypoints[0].z + translation_z); string spawn_sectionconfig = game.getAIVehicleSectionConfig(x); string spawn_skin = game.getAIVehicleSkin(x); From e5151e91cbdef75957eba4963ce551feb0f0649e Mon Sep 17 00:00:00 2001 From: ohlidalp Date: Thu, 5 Dec 2024 04:22:38 +0100 Subject: [PATCH 34/36] example_game_keyCodes.as: fixed compiler warning --- resources/scripts/example_game_keyCodes.as | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/scripts/example_game_keyCodes.as b/resources/scripts/example_game_keyCodes.as index 0c3bfca47c..cf0a977e62 100644 --- a/resources/scripts/example_game_keyCodes.as +++ b/resources/scripts/example_game_keyCodes.as @@ -7,7 +7,7 @@ imgui_utils::CloseWindowPrompt closeBtnHandler; string keyEventLog; // keys pressed history string keyEventSeparator; -int HISTORY_LEN = 100; +uint HISTORY_LEN = 100; void frameStep(float dt) { From e84322a3c51bcbda1e80bc1bb00fa4bd77ea9d92 Mon Sep 17 00:00:00 2001 From: ohlidalp Date: Thu, 5 Dec 2024 04:24:40 +0100 Subject: [PATCH 35/36] example_ogre_textureBlitting.as: API update, help text. --- .../scripts/example_ogre_textureBlitting.as | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/resources/scripts/example_ogre_textureBlitting.as b/resources/scripts/example_ogre_textureBlitting.as index 07ccf47100..e838a0c100 100644 --- a/resources/scripts/example_ogre_textureBlitting.as +++ b/resources/scripts/example_ogre_textureBlitting.as @@ -8,8 +8,8 @@ imgui_utils::CloseWindowPrompt closeBtnHandler; #include "gridviewer_utils.as" -GridViewer gDstViewer; -GridViewer gSrcViewer; +gridviewer_utils::GridViewer gDstViewer; +gridviewer_utils::GridViewer gSrcViewer; Ogre::Image gSrcImg; Ogre::TexturePtr gSrcTex; // only for display, not blitting ATM Ogre::TexturePtr gDstTex; @@ -58,7 +58,7 @@ void setupTexBlitTool() pixbuf = gDstTex.getBuffer(/*cubemap face index:*/0, /*mipmap:*/0); } -void drawTexGridViewer(GridViewer@ aThisViewer, vector2 aViewerSize, Ogre::TexturePtr&in imgTex, bool&inout aDrawingBox, box&inout aThisBox) +void drawTexGridViewer(gridviewer_utils::GridViewer@ aThisViewer, vector2 aViewerSize, Ogre::TexturePtr&in imgTex, bool&inout aDrawingBox, box&inout aThisBox) { // Common for DST and SRC viewers // ------------------------------ @@ -112,8 +112,9 @@ void drawTexBlitGridViews() { vector2 windowSize = ImGui::GetWindowContentRegionMax() - ImGui::GetWindowContentRegionMin(); float spaceUnderViewer = 99; - vector2 panelSize = vector2(windowSize.x / 2, windowSize.y - (gBottomBarHeight)); - vector2 viewerSize = vector2(windowSize.x / 2, windowSize.y - (gBottomBarHeight+spaceUnderViewer)); + float topCommentsHeight = 50; + vector2 panelSize = vector2(windowSize.x / 2, windowSize.y - (gBottomBarHeight + topCommentsHeight)); + vector2 viewerSize = vector2(windowSize.x / 2, windowSize.y - (gBottomBarHeight+spaceUnderViewer+ topCommentsHeight)); if (ImGui::BeginChild("leftPane",panelSize)) { @@ -168,9 +169,12 @@ void frameStep(float dt) { gIsMouseDown = ImGui::IsMouseDown(1); - if (ImGui::Begin("Example", closeBtnHandler.windowOpen, 0)) + if (ImGui::Begin("Example of painting image over texture (aka 'blitting')", closeBtnHandler.windowOpen, 0)) { closeBtnHandler.draw(); + ImGui::TextDisabled("Select a region in the left panel (using click-and-drag with right mouse button) and press [Blit!] to paint the image from the right panel."); + ImGui::TextDisabled("NOTE: this only paints to the highest LOD(level of detail) of the texture - to see the effect, you must zoom very close to the character."); + ImGui::TextDisabled("KNOWN BUG - under OpenGL on Win10, the texture in left panel may not be visible!"); drawTexBlitGridViews(); ImGui::Separator(); From a078c21fb0b284ec455c04ded0bc85d339dd900b Mon Sep 17 00:00:00 2001 From: ohlidalp Date: Thu, 5 Dec 2024 04:31:47 +0100 Subject: [PATCH 36/36] example_ImGui_nodeHighlight.as: fixed out of bounds error --- resources/scripts/example_ImGui_nodeHighlight.as | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/resources/scripts/example_ImGui_nodeHighlight.as b/resources/scripts/example_ImGui_nodeHighlight.as index 0544a477a3..44c09a0c7a 100644 --- a/resources/scripts/example_ImGui_nodeHighlight.as +++ b/resources/scripts/example_ImGui_nodeHighlight.as @@ -27,7 +27,7 @@ void frameStep(float dt) if (ImGui::Begin("Example", closeBtnHandler.windowOpen, 0)) { closeBtnHandler.draw(); - + ImGui::TextDisabled("::: NODE HIGHLIGHT DEMO:::"); ImGui::Separator(); BeamClass@ actor = game.getCurrentTruck(); @@ -55,7 +55,7 @@ void frameStep(float dt) drawConfigUI(); } } - + ImGui::End(); } } @@ -178,6 +178,7 @@ void drawNodeHighlights(BeamClass@ actor) int mouseClosestNodeID = -1; int mouseClosestNodeDist = 999999; + nodeSelectedStates.resize(actor.getNodeCount()); ImDrawList@ drawlist = imgui_utils::ImGetDummyFullscreenWindow("nodeHighlights"); for (int i = 0; i < actor.getNodeCount(); i++)