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/doc/angelscript/Script2Game/GameScriptClass.h b/doc/angelscript/Script2Game/GameScriptClass.h index dcd6436b8d..9041a6184f 100644 --- a/doc/angelscript/Script2Game/GameScriptClass.h +++ b/doc/angelscript/Script2Game/GameScriptClass.h @@ -274,32 +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 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 @@ -435,6 +462,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/doc/angelscript/Script2Game/GenericDocContextClass.h b/doc/angelscript/Script2Game/GenericDocContextClass.h index 47c6550c21..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,23 +38,36 @@ 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); + bool isTokLineBreak(int offset = 0); // 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 appendTokInt(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); + 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/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/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); 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..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; @@ -44,14 +44,16 @@ 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; + // tab settings bool demotabsReorderable = false; @@ -65,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() { @@ -74,20 +76,20 @@ void main() } /* - --------------------------------------------------------------------------- - Script update function - invoked 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) { // 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); 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 @@ -125,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)); @@ -139,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 (?)"); @@ -196,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); } @@ -207,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(); @@ -229,183 +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 + ")"; - ImGui::Begin(caption, /*open:*/true, /*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(); - 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()); } @@ -433,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 { @@ -464,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); } } @@ -497,7 +323,7 @@ void drawAudioButtons() ImGui::TextDisabled(" [base]"); } ImGui::SameLine(); - + if (@g_playing_soundscript == null) { if (ImGui::Button("Play")) @@ -539,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()); @@ -563,7 +389,7 @@ void drawAudioButtons() } ImGui::TextDisabled("Some builtin sounds"); - + drawWavPreviewBulletButton("default_horn.wav"); drawWavPreviewBulletButton("default_police.wav"); drawWavPreviewBulletButton("default_pump.wav"); @@ -576,7 +402,7 @@ void drawAudioButtons() void drawWavPreviewBulletButton(string wav_file) { ImGui::PushID(wav_file); - + ImGui::Bullet(); ImGui::SameLine(); ImGui::Text(wav_file); @@ -610,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) @@ -685,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"} }); } } @@ -727,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) @@ -740,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) + ")"); } } @@ -761,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 @@ -774,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 @@ -796,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 4500003ad6..0000000000 --- a/resources/scripts/example_GenericDocument_editor.as +++ /dev/null @@ -1,318 +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; - -class RigEditor -{ - // ---- variables ----- - -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) - { - caption += " ("+m_project_entry.fname+")"; - } - int flags = ImGuiWindowFlags_MenuBar; - if (ImGui::Begin(caption, closeBtnHandler.windowOpen, flags)) - { - closeBtnHandler.draw(); - if (ImGui::BeginMenuBar()) - { - this.drawDocumentControls(); // <- menubar - ImGui::EndMenuBar(); - } - if (@g_displayed_document != null) - { - vector2 size = ImGui::GetWindowSize() - vector2(25, 200); - ImGui::BeginChild("docBody", size); - this.drawDocumentBody(); - ImGui::EndChild(); - - if ( m_focused_token_pos > -1) - { - ImGui::Separator(); - this.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()) - { - 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) - 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; - } - @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 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 drawDocumentControls() -{ - if (m_error_str != "") - { - ImGui::Text("ERROR! " + m_error_str); - 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 (m_project_creation_pending != "") - { - 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 (@g_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."); - } - } -} - -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_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" -} - - 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); - } // 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_GenericDocument_uniEditor.as b/resources/scripts/example_GenericDocument_uniEditor.as new file mode 100644 index 0000000000..54de064068 --- /dev/null +++ b/resources/scripts/example_GenericDocument_uniEditor.as @@ -0,0 +1,279 @@ +/// \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; + +// genericDoc editor +#include "genericdoc_utils.as" + genericdoc_utils::GenericDocEditor gdEditor; + +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) + { + 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); + gdEditor.drawDocumentBody(); + ImGui::EndChild(); + + + ImGui::Separator(); + + gdEditor.drawTokenEditPanel(); + + } + + ImGui::End(); + + } +} + + + +//#region TOBJ 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(); + 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(); + if (@terrain == null) + { + game.log("loadTerrn2Document(): no terrain loaded, nothing to do!"); + return; + } + GenericDocumentClass@ doc = GenericDocumentClass(); + + if (!doc.loadFromResource(terrain.getTerrainFileName(), terrain.getTerrainFileResourceGroup(), genericdoc_utils::FLAGSET_TERRN2)) + { + 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()) + { + 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 = ""; + } + } + + ImGui::TextDisabled("[Objects]"); + drawDiscoveredTobjFiles(); + + + 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(); + + if (doc.loadFromResource(actor.getTruckFileName(), actor.getTruckFileResourceGroup(), genericdoc_utils::FLAGSET_TRUCK)) + { + @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(); + + if (doc.loadFromResource(actor.getTruckFileName(), actor.getTruckFileResourceGroup(), genericdoc_utils::FLAGSET_TRUCK)) + { + @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 + + + 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_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++) diff --git a/resources/scripts/example_RigEditor_alpha.as b/resources/scripts/example_RigEditor_alpha.as deleted file mode 100644 index 8bed9f2ca6..0000000000 --- a/resources/scripts/example_RigEditor_alpha.as +++ /dev/null @@ -1,552 +0,0 @@ -/// \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" - -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; - -// STARTUP -void main() -{ - editor.viewer_x.childWindowID = "demoX"; - editor.viewer_x.hAxis=1; - editor.viewer_x.vAxis=2; // axes: YZ - - editor.viewer_y.childWindowID = "demoY"; - editor.viewer_y.hAxis=0; - editor.viewer_y.vAxis=2; // axes: XZ - - editor.viewer_z.childWindowID = "demoZ"; // axes XY (default) - -} - -// RENDERING -void frameStep(float dt) -{ - editor.update(dt); -} - -// NOTIFICATIONS -void eventCallbackEx(scriptEvents ev, int a1, int a2, int a3, int a4, string a5, string a6, string a7, string a8) -{ - if (ev == SE_GENERIC_MODCACHE_ACTIVITY && a1 == MODCACHEACTIVITY_BUNDLE_LOADED) - { - editor.onEventBundleLoaded(a2); - } -} diff --git a/resources/scripts/example_actor_RigEditorAlpha.as b/resources/scripts/example_actor_RigEditorAlpha.as new file mode 100644 index 0000000000..6a4b8a4f72 --- /dev/null +++ b/resources/scripts/example_actor_RigEditorAlpha.as @@ -0,0 +1,446 @@ +/// \file Prototype truck editor, Oct 2023 +/// \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 +// =================================================== + +// 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 ----- + +int m_statusbar_height_pixels = 25; + +// ---- variables ----- + +GenericDocumentClass@ m_displayed_document = null; + +string m_error_str; +string m_project_creation_pending; // name of the truck file which will become available in modcache. +CacheEntryClass@ m_project_entry; + +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; + + +// ---- functions ---- + +// STARTUP +void main() +{ + viewer_x.childWindowID = "demoX"; + viewer_x.hAxis=1; + viewer_x.vAxis=2; // axes: YZ + + viewer_y.childWindowID = "demoY"; + viewer_y.hAxis=0; + viewer_y.vAxis=2; // axes: XZ + + viewer_z.childWindowID = "demoZ"; // axes XY (default) + +} + +// RENDERING +void frameStep(float 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 && entry.resource_bundle_type == "FileSystem") + { + @m_project_entry = @entry; + loadDocument(); + } + } + + checkForCreatedProject(); + drawWindow(); +} + +// NOTIFICATIONS +void eventCallbackEx(scriptEvents ev, int a1, int a2, int a3, int a4, string a5, string a6, string a7, string a8) +{ + if (ev == SE_GENERIC_MODCACHE_ACTIVITY && a1 == MODCACHEACTIVITY_BUNDLE_LOADED) + { + 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) + { + + gdEditor.drawDocumentBody(); + } + } + ImGui::EndChild(); // "docBody" + + + ImGui::Separator(); + gdEditor.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; + gdEditor.setDocument(m_displayed_document); +} + +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 + } + + gdEditor.setDocument(m_displayed_document); +} + +// #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 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_game_shockTuning.as b/resources/scripts/example_actor_shockTuning.as similarity index 95% rename from resources/scripts/example_game_shockTuning.as rename to resources/scripts/example_actor_shockTuning.as index 8ee4198079..eb8bca7dc0 100644 --- a/resources/scripts/example_game_shockTuning.as +++ b/resources/scripts/example_actor_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_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++; 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/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/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_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) { 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_overlays.as b/resources/scripts/example_ogre_overlays.as index 00c44cbdf0..440c55fd17 100644 --- a/resources/scripts/example_ogre_overlays.as +++ b/resources/scripts/example_ogre_overlays.as @@ -8,79 +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; - -bool ov_fail = false; -bool pa_fail = false; -int framecounter = 0; -float pos_step = 50; // pixels +void frameStep(float dt) +{ + if (ImGui::Begin("OGRE overlays", closeBtnHandler.windowOpen, 0)) + { + closeBtnHandler.draw(); + drawOverlayList(); + ImGui::End(); + } +} -void frameStep(float dt) +bool drawOverlayTreeNode( Ogre::Overlay@ ov) { - 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(); } - } + ImGui::PushID(ov.getName()+"-treenode"); + bool open = ImGui::TreeNode(ov.getName()); + + ImGui::SameLine(); + if (!ov.isVisible()) + { + if (ImGui::SmallButton("Show")) + { + ov.show(); + } + } + else + { + if (ImGui::SmallButton("Hide")) + { + ov.hide(); + } + } + + ImGui::PopID(); + return open; + +} - Ogre::OverlayElement@ pa; - if (!pa_fail ){ - if ( Ogre::OverlayManager::getSingleton().hasOverlayElement("pa")) { +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 +} - // 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'); - } +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 +} - if (ImGui::Begin("Example", closeBtnHandler.windowOpen, 0)) +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++) { - closeBtnHandler.draw(); - ImGui::Text("overlays should work; ov_fail:"+ov_fail+", pa_fail:"+pa_fail - +", frames:"+framecounter); - framecounter++; - if (!pa_fail && @pa != null) + if (drawOverlayElementTreeNode(elems[i])) { + drawOverlayElementControls(elems[i]); + ImGui::TreePop(); + } + } + + ImGui::PopStyleColor(); // Text + ImGui::PopID(); +} - 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); } +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")) + { + el.show(); + } + } + else + { + if (ImGui::SmallButton("Hide")) + { + el.hide(); + } + } + ImGui::PopID(); + return open; +} - if (ImGui::Button("height+")) { pa.setHeight(pa.getHeight()+pos_step); } - ImGui::SameLine(); if (ImGui::Button("height-")) { pa.setHeight(pa.getHeight()-pos_step); } +void drawOverlayList() +{ + array@ overlays = Ogre::OverlayManager::getSingleton().getOverlays(); + ImGui::TextDisabled("There are " + overlays.length() + " overlays:"); + for (uint i=0; i> 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_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(); 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 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_terrn_raceConverter.as b/resources/scripts/example_terrn_raceConverter.as new file mode 100644 index 0000000000..d1db6b22ee --- /dev/null +++ b/resources/scripts/example_terrn_raceConverter.as @@ -0,0 +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_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; + if (result == SCRIPTRETCODE_AS_NO_GLOBAL_VAR) + { + error = "Nothing to do ~ this terrain has no races (race system isn't loaded)."; + } + else + { + error = " game.scriptVariableExists() returned "+result; + } + 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; + } +} + + + + + + + + + + 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(); + } +} 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 + + + + 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 + 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 + diff --git a/resources/scripts/races.as b/resources/scripts/races.as index 10001f86b3..f0a191327a 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; @@ -1158,6 +1160,10 @@ 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) { @@ -1230,6 +1236,11 @@ 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) diff --git a/resources/scripts/road_editor.as b/resources/scripts/road_editor.as index 20db0554b9..6be4eae286 100644 --- a/resources/scripts/road_editor.as +++ b/resources/scripts/road_editor.as @@ -1,70 +1,71 @@ -/* - --------------------------------------------------------------------------- - Project Rigs of Rods (www.rigsofrods.org) - - PROCEDURAL ROAD EDITOR - - Procedural roads are static tracks generated from splines. - Splines can be defined in .tobj files which come with terrain (.terrn2), - but can also be created programmatically. - - To run, open in-game console (Hotkey ~) and say 'loadscript road_editor.as' - - Procedural roads .tobj documentation: - https://docs.rigsofrods.org/terrain-creation/old-terrn-subsystem/#procedural-roads - - Scripting documentation: - https://developer.rigsofrods.org/d4/d07/group___script2_game.html - - --------------------------------------------------------------------------- -*/ + +// --------------------------------------------------------------------------- +// Project Rigs of Rods (www.rigsofrods.org) +// +// PROCEDURAL ROAD EDITOR +// +// Procedural roads are static tracks generated from splines. +// Splines can be defined in .tobj files which come with terrain (.terrn2), +// but can also be created programmatically. +// +// To run, open in-game console (Hotkey ~) and say 'loadscript road_editor.as' +// +// Procedural roads .tobj documentation: +// https://docs.rigsofrods.org/terrain-creation/old-terrn-subsystem/#procedural-roads +// +// Scripting documentation: +// https://developer.rigsofrods.org/d4/d07/group___script2_game.html +// +// --------------------------------------------------------------------------- + #include "imgui_utils.as" -/* - --------------------------------------------------------------------------- - Global variables -*/ +// Game state +CVarClass@ cvar_app_state = console.cVarFind("app_state"); // 0=bootstrap, 1=main menu, 2=simulation, see AppState in Application.h +CVarClass@ cvar_sistate = console.cVarFind("sim_state"); // 0=off, 1=running, 2=paused, 3=terrain editor, see SimState in Application.h +CVarClass@ cvar_mp_state = console.cVarFind("mp_state"); // 0=disabled, 1=connecting, 2=connected, see MpState in Application.h -CVarClass@ g_app_state = console.cVarFind("app_state"); // 0=bootstrap, 1=main menu, 2=simulation, see AppState in Application.h -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 -TerrainEditor g_terrain_editor; +// Main window +imgui_utils::CloseWindowPrompt closeBtnHandler; -/* - --------------------------------------------------------------------------- - Script setup function - invoked once when script is loaded. -*/ +// Road panel +int selected_road = 0; +int selected_point = 0; +int hovered_point = 0; +float selected_point_elevation = 0.f; +float hovered_point_elevation = 0.f; +bool selected_point_auto_goto = false; + +// Waypoints panel +int ai_import_first = 0; +int ai_import_last = 0; +int ai_import_available = 0; +bool ai_import_reverse = false; +int ai_export_first = 0; +int ai_export_last = 0; +bool ai_export_reverse = false; + +// Mesh generation panel +float global_extra_point_elevation = 0; // Added to all points +float global_min_point_elevation = 0.3; // Added to lower points + +// --------------------------------------------------------------------------- +// Script setup function - invoked by game once when script is loaded. void main() { log("Road editor loaded! Enter terrain editing mode (hotkey: " + inputs.getEventCommandTrimmed(EV_COMMON_TOGGLE_TERRAIN_EDITOR) + ") to use it."); } -/* - --------------------------------------------------------------------------- - Script update function - invoked once every rendered frame, - with elapsed time (delta time, in seconds) as parameter. -*/ +// --------------------------------------------------------------------------- +// Script update function - invoked once every rendered frame, with elapsed time (delta time, in seconds) as parameter. void frameStep(float dt) { - if (g_app_state.getInt() == 2 // simulation - && g_sim_state.getInt() == 3 // terrain editor mode - && g_mp_state.getInt() == 0) // singleplayer - { - g_terrain_editor.frameStep(dt); - } -} - -/* - --------------------------------------------------------------------------- - Custom objects -*/ - -class TerrainEditor -{ - void frameStep(float dt) + if (cvar_app_state.getInt() == 2 // simulation + && cvar_sistate.getInt() == 3 // terrain editor mode + && cvar_mp_state.getInt() == 0) // singleplayer { - this.drawWindow(); + drawWindow(); TerrainClass@ terrain = game.getTerrain(); ProceduralManagerClass@ roads = terrain.getProceduralManager(); @@ -74,690 +75,662 @@ class TerrainEditor } else { - ProceduralObjectClass@ obj = roads.getObject(m_selected_road); + ProceduralObjectClass@ obj = roads.getObject(selected_road); if (obj.getNumPoints() == 0) { return; // nothing more to do } } - this.visualizeRoad(); - this.updateInputEvents(); + visualizeRoad(); + updateInputEvents(); } +} - void drawWindow() - { - // Open demo window - ImGui::SetNextWindowPos(vector2(25, 120)); // Make space for FPS box. - int flags = ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse; - if (!ImGui::Begin("Road editor", /*open:*/true, flags)) - { - return; - } - ImGui::Dummy(vector2(200.f, 1.f)); // force width - - if (ImGui::CollapsingHeader("Manage roads")) - { - this.drawRoadListPanel(); - } +// --------------------------------------------------------------------------- +// Editor functions - if (ImGui::CollapsingHeader("AI Waypoints")) - { - this.drawAiWaypointsPanel(); - } - - this.drawRoadEditPanel(); - - // End window - ImGui::End(); +void drawWindow() +{ + // Open demo window + ImGui::SetNextWindowPos(vector2(25, 120)); // Make space for FPS box. + int flags = ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse; + if (!ImGui::Begin("Road editor", closeBtnHandler.windowOpen, flags)) + { + return; } + closeBtnHandler.draw(); // draw the "terminate script?" proompt? + ImGui::Dummy(vector2(200.f, 1.f)); // force width - void drawRoadEditPanel() + int barflags = 0; + if (ImGui::BeginTabBar("Road editor tabs", barflags)) { - ImGui::PushID("road edit box"); - - TerrainClass@ terrain = game.getTerrain(); - ProceduralManagerClass@ roads = terrain.getProceduralManager(); - if (roads.getNumObjects() > 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); + } } + 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 ); - void drawMeshRebuildPanel(ProceduralObjectClass@ obj) + if (ImGui::RadioButton(label, selected)) + { + point.type = roadType; + point.pillar_type = pillarType; + } +} + +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); - } - } - else - { - ImGui::Text("Road has no points - nothing to generate!"); - } + ImGui::SetNextItemWidth(100.f); + ImGui::InputFloat("Border height (meters)", point.border_height); - ImGui::PopID(); //"mesh rebuild" + ImGui::Text("Type:"); + ImGui::SameLine(); + ImGui::TextDisabled("DBG type:"+point.type+", pillartype:"+point.pillar_type); + // Types supported by TOBJ format + 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 + { + ImGui::Text("Road has no points - nothing to edit!"); + } - void recalculatePointElevations(ProceduralObjectClass@ obj) + ImGui::PopID(); //"point properties" +} + +void drawMeshRebuildPanel(ProceduralObjectClass@ obj) +{ + ImGui::PushID("mesh rebuild"); + + ProceduralManagerClass@ roads = game.getTerrain().getHandle().getProceduralManager(); + + if (obj.getNumPoints() > 0) { - for (int i = 0; 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); - point.position.y = m_global_extra_point_elevation + fmax(point.position.y, game.getGroundHeight(point.position) + m_global_min_point_elevation); - } + rebuildMesh(obj); + } } - - void recalculatePointRotations(ProceduralObjectClass@ obj) + else { - // 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) - { - 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; - } - } + 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"); - void drawAiWaypointsImportPanel(ProceduralObjectClass@ obj) + ImGui::Text("Import road points from AI waypoints in survey map"); + array waypoints = game.getWaypoints(0); + + // 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..4ed4a2fc08 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; - + // MENU CONTEXT (pre-scanned file lists with extra details) 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() { @@ -219,7 +224,7 @@ class ScriptEditorWindow nTab.setBuffer(buffer); tabs.insertLast(nTab); } - + void removeTab(uint i) { if (i < tabs.length()) @@ -227,7 +232,7 @@ class ScriptEditorWindow tabs.removeAt(i); } } - + void draw(float dt) { // Process scheduled tab closing @@ -235,8 +240,8 @@ class ScriptEditorWindow { this.removeTab(uint(tabScheduledForRemoval)); this.tabScheduledForRemoval = -1; - } - + } + // Make sure there's always a tab open () if (tabs.length() == 0) { @@ -246,23 +251,23 @@ 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()); @@ -270,15 +275,15 @@ class ScriptEditorWindow 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; @@ -298,15 +303,15 @@ 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(); } } - + private void drawMenubar() { bool analysisDoneThisFrame = false; @@ -319,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. @@ -343,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; @@ -373,44 +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"); + 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); - ImGui::Separator(); 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")) { @@ -418,15 +421,15 @@ 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(); } @@ -435,54 +438,54 @@ class ScriptEditorWindow { 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); 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; } - + 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(); @@ -502,7 +505,7 @@ class ScriptEditorWindow } ImGui::EndMenu(); } - + // 'TOOLS' menu if (ImGui::BeginMenu("Tools")) { @@ -511,7 +514,7 @@ class ScriptEditorWindow { this.tabs[this.currentTab].requestIndentBuffer = true; } - + ImGui::Separator(); ImGui::TextDisabled("Settings"); ImGui::SetNextItemWidth(75.f); @@ -519,38 +522,38 @@ 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) { 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(); } - + // '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) { @@ -560,17 +563,34 @@ 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::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 = 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); 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; float scrollX = 0.f; @@ -669,7 +679,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; @@ -680,7 +690,7 @@ class ScriptEditorTab bool requestAutoSaveFile = false; bool requestRunBuffer = false; bool requestStopBuffer = false; - + ScriptEditorTab() { @epanel = ExceptionsPanel(this); @@ -695,7 +705,7 @@ class ScriptEditorTab else { if (this.currentScriptUnitID == SCRIPTUNITID_INVALID) - { + { if (ImGui::Button("[>>] RUN")) { this.requestRunBuffer = true; @@ -708,7 +718,7 @@ class ScriptEditorTab this.requestStopBuffer = true; } } - } + } } protected float measureLineInfoColumns() @@ -716,51 +726,51 @@ class ScriptEditorTab // 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 // =============================================================================== - + 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) { @@ -777,7 +787,7 @@ 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; @@ -786,9 +796,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) @@ -799,7 +809,7 @@ class ScriptEditorTab else { regionColumnStr = "Br!"; // Broken ('#endregion' not found) - } + } } else if (bool(bufferLinesMeta[lineIdx]['regionFound'])) { @@ -834,70 +844,70 @@ class ScriptEditorTab } } } - + // Coarse clipping if (linenumCursor.y < coarseClipTopY) - continue; + 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; + ""+int(this.bufferLinesMeta[lineIdx]['len'])); + linenumCursor.x += lineLenColumnWidth; } - + if (drawLineCommentOffsets) { drawlist.AddText(linenumCursor, lineCommentOffsetColor, - ""+int(this.bufferLinesMeta[lineIdx]['nonCommentLen'])); - linenumCursor.x += lineCommentColumnWidth; - } + ""+int(this.bufferLinesMeta[lineIdx]['nonCommentLen'])); + linenumCursor.x += lineCommentColumnWidth; + } if (drawLineHttpOffsets) { drawlist.AddText(linenumCursor, lineHttpOffsetColor, - ""+int(this.bufferLinesMeta[lineIdx]['nonHttpLen'])); - linenumCursor.x += lineHttpColumnWidth; - } - + ""+int(this.bufferLinesMeta[lineIdx]['nonHttpLen'])); + linenumCursor.x += lineHttpColumnWidth; + } + // draw message count if (drawLineErrCounts) { - + 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()); + 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; + ""+int(this.bufferLinesMeta[lineIdx]['autoIndentLevel']) * autoIndentNumSpaces); + linenumCursor.x += lineAutoIndentLevelsColumnWidth; } // Actual indentation length if (drawLineActualIndents) { drawlist.AddText(linenumCursor, lineActualIndentsColor, - ""+int(this.bufferLinesMeta[lineIdx]['actualIndentBlanks'])); - linenumCursor.x += lineActualIndentsColumnWidth; - } - + ""+int(this.bufferLinesMeta[lineIdx]['actualIndentBlanks'])); + linenumCursor.x += lineActualIndentsColumnWidth; + } + // draw region&endregion markers if (drawLineRegionMarkers) { @@ -912,7 +922,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; @@ -930,33 +940,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; - + 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']); @@ -973,15 +983,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']); @@ -992,975 +1002,1013 @@ 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; - + 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) { 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) { - - // 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; - - } - - 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 - } - - 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); - } - - // 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 - } - - 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);*/ - - // 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) + // 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::BeginChild("ExceptionsPanel", ePanelSize); + epanel.drawExceptions(); + ImGui::EndChild(); // must always be called - legacy reasons +} + +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); + } + + // 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 +} + +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);*/ + + // 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; + } + +} + +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) { - /*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) { - commentLevel++; - - // Record the '//', but make sure it's not within hyperlink! - if (commentStart == -1) - { - commentStart = i; - } - - if (uint(commentLevel) == commentPattern.length()) - { - commentFound = true; - } + commentStart = i; } - else + + if (uint(commentLevel) == commentPattern.length()) { - commentLevel = 0; + commentFound = true; } } - else - { - if (!regionFound) - { - //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; - } - } - - if (!endregionFound) - { - //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; - } - } + else + { + commentLevel = 0; } - - // http protocol - if (!httpFound) + } + 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; - - uint microsecBeforeRegions = timer.getMicroseconds(); - uint microsecCpuBeforeRegions = timer.getMicrosecondsCPU(); - - // Process #region - if (regionFound) + httpLevel++; + + 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 = ""; + httpFound = true; } - - 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; } - - 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; + + /* + // [Profiling] + 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 = ""; } + + /* + // [Profiling] + 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; + + /* + // [Profiling] + 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++; } - void updateAutosave(float dt) + /* + // [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++) { - 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 != "") + } + + // 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 requestUnFoldRegion: '"+ this.requestUnFoldRegion+"'"); - this.unFoldRegionInternal(this.requestUnFoldRegion); - this.requestUnFoldRegion = ""; + //game.log("DBG mergeCollectedFoldingRegionsWithExisting(): A brand new region '"+newRegionNames[i]+"' was created"); + this.workBufferRegions[newRegionNames[i]] = newRegionInfo; } - else if (this.requestFoldAll) + else { - this.foldAllRegionsInternal(); - this.requestFoldAll = 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; + } } - else if (this.requestUnFoldAll) + } +} + +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) { - //game.log("DBG requestUnfoldAll:"+this.requestUnFoldAll); - this.unFoldAllRegionsInternal(); - this.requestUnFoldAll = false; + 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; + } } - if (this.requestIndentBuffer) - { - this.indentBufferInternal(); - this.requestIndentBuffer = false; - } - if (this.requestSaveFile) + // END bounds check + + // update line stats + if (msgType == asMSGTYPE_ERROR) { - this.saveFileInternal(); - this.requestSaveFile = false; + this.bufferMessageIDs[lineIdx].insertLast(i); + this.messagesTotalErr++; } - if (this.requestAutoSaveFile) + if (msgType == asMSGTYPE_WARNING) { - this.autosaveFileInternal(); - this.requestAutoSaveFile = false; - } - if (this.requestRunBuffer) - { - this.runBufferInternal(); - this.requestRunBuffer = false; + this.bufferMessageIDs[lineIdx].insertLast(i); + this.messagesTotalWarn++; } - if (this.requestStopBuffer) + if (msgType == asMSGTYPE_INFORMATION) { - this.stopBufferInternal(); - this.requestStopBuffer = false; + this.messagesTotalInfo++; } } - - 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) - { - 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; - 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(); - } - - private void indentBufferInternal() // Do not invoke while drawing! use `requestIndentBuffer` - { - this.backUpRegionFoldStates(); - this.unFoldAllRegionsInternal(); - - string stagingBuffer; - for (uint i = 0; i < bufferLinesMeta.length(); i++) +} + +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++) + { + 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]); - } - - } - - private void unFoldAllRegionsInternal() // do NOT invoke during drawing! use `requestUnFoldAll` + lineIdx -= 1 + (lineIdx - containingRegion.regionStartsAtLineIndex); + inFoldedRegion = containingRegionName; + } + + // 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; } - - 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 + 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. + + 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); + } + + 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) + { + 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++) { - 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.foldRegionInternal(regionNames[i]); + } + +} + +private void unFoldAllRegionsInternal() // do NOT invoke during drawing! use `requestUnFoldAll` +{ + 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) { - 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 + regionInfo.isFoldedBackup = regionInfo.isFolded; + //game.log("DBG backUpRegionFoldStates() ~ region '"+regionNames[i]+"': isFolded="+regionInfo.isFolded+" (backup: "+regionInfo.isFoldedBackup+")"); } } +} - private void backUpRegionFoldStates() +private void restoreRegionFoldStates() +{ + //game.log("DBG restoreRegionFoldStates()"); + array 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@ 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) { - regionInfo.isFoldedBackup = regionInfo.isFolded; - //game.log("DBG backUpRegionFoldStates() ~ region '"+regionNames[i]+"': isFolded="+regionInfo.isFolded+" (backup: "+regionInfo.isFoldedBackup+")"); + this.unFoldRegionInternal(regionNames[i]); } - } - } - - 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) + else 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.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. @@ -1970,17 +2018,17 @@ class ExceptionsPanel 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 + // 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; + @parentEditorTab = @editorTab; } - + float getHeightToReserve() - { + { if (exception_stats_nid.isEmpty()) { return 0; } else { return numLinesDrawn*ImGui::GetTextLineHeight() + 15; } } @@ -1992,12 +2040,12 @@ class ExceptionsPanel /*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); } - + void onEventAngelscriptExceptionCallback(int nid, int arg3_linenum, string arg5_from, string arg6_msg) { if (parentEditorTab.currentScriptUnitID != nid) @@ -2005,16 +2053,16 @@ class ExceptionsPanel /*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); + this.addExceptionInternal(nid, desc); } - + void addExceptionInternal(int nid, string desc) { if (!ePanelEnabled) - return; + return; // locate the dictionary for this script (by NID) dictionary@ exception_stats = cast(exception_stats_nid[''+nid]); @@ -2033,19 +2081,19 @@ class ExceptionsPanel 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 + if (@exception_stats == null) { continue; } // <----- continue, nothing to draw + - ImGui::TextDisabled('NID '+nids[i]+' '); if (@info != null) { @@ -2053,7 +2101,7 @@ class ExceptionsPanel } else { - ImGui::SameLine(); ImGui::TextDisabled("(NOT RUNNING)"); + ImGui::SameLine(); ImGui::TextDisabled("(NOT RUNNING)"); } numLinesDrawn++; @@ -2067,10 +2115,10 @@ class ExceptionsPanel { requestClearAll = true; } - for (uint j=0; j < tag_keys.length(); j++) - { + 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")) @@ -2084,13 +2132,13 @@ class ExceptionsPanel numLinesDrawn++; numEntriesDrawn++; - + ImGui::PopID(); // j } // for (tag_keys) - + ImGui::PopID(); // i - } // for (nids) - + } // for (nids) + // Requests for editing entries - cannot be done while iterating! if (requestClearEntryKey != "" && @requestClearEntryFromStats != null) { @@ -2104,7 +2152,7 @@ class ExceptionsPanel requestClearAll=true; // We just deleted the only entry -> close panel } } - + if (requestClearAll) { /*game.log("DBG @@@ handling requestClearAll for buffer='"+parentEditorTab.bufferName+"' !!!");*/ @@ -2130,7 +2178,7 @@ class RegionInfo 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; @@ -2140,15 +2188,15 @@ class RegionInfo /// 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 = '*') + void scanResourceGroup(string rgname, string pattern = '*') { @fileinfos = game.findResourceFileInfo(rgname, pattern); this.rgname = rgname; @@ -2157,37 +2205,37 @@ class ScriptIndexerRecord // analysis is done frame-by-frame in `advanceScriptAnalysis()` } - ScriptInfo@ getScriptInfo(uint index) + scriptinfo_utils::ScriptInfo@ getScriptInfo(uint index) { - if (index < fileinfos.length() && fileinfos[index].exists('scriptInfo')) - { - return cast(fileinfos[index]['scriptInfo']); + if (index < fileinfos.length() && fileinfos[index].exists('scriptInfo')) + { + return cast(fileinfos[index]['scriptInfo']); } 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@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);*/ + +": title="+scriptinfo.title+", brief="+scriptinfo.brief+", text="+scriptinfo.text);*/ fileinfos[index]['scriptInfo'] = scriptinfo; - - } + + } } // class ScriptIndexerRecord // Helpers @@ -2206,7 +2254,7 @@ string trimLeft(string s) for (uint i = 0; i < s.length(); i++) { if (!isCharBlank(s[i])) - return s.substr(i, s.length() - i); + return s.substr(i, s.length() - i); } return ""; } @@ -2214,21 +2262,21 @@ 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; } -// Tutorial scripts +// Tutorial scripts // ----------------- // using 'heredoc' syntax; see https://www.angelcode.com/angelscript/sdk/docs/manual/doc_datatypes_strings.html 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,24 +2298,23 @@ 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 tt += dt; - - //#region format the output + + //#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 - // Note this will open an implicit window titled "Debug" ImGui::Text(ttStr); ImGui::SameLine(); ImGui::Text(dtStr); //#endregion - + // End drawing window ImGui::End(); } diff --git a/resources/scripts/scriptinfo_utils.as b/resources/scripts/scriptinfo_utils.as index c15ef484fa..c2ff052dae 100644 --- a/resources/scripts/scriptinfo_utils.as +++ b/resources/scripts/scriptinfo_utils.as @@ -8,83 +8,96 @@ /// `game.log(" 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; + } + } + } } /* diff --git a/source/main/ForwardDeclarations.h b/source/main/ForwardDeclarations.h index 5d32022c6a..723f0ec84e 100644 --- a/source/main/ForwardDeclarations.h +++ b/source/main/ForwardDeclarations.h @@ -37,8 +37,9 @@ 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; + 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/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/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"; diff --git a/source/main/scripting/GameScript.cpp b/source/main/scripting/GameScript.cpp index 4a6a0ecd0a..b5b65b0c07 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; @@ -946,29 +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::deleteScriptVariable(const String& arg, ScriptUnitID_t nid) +{ + 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) @@ -986,7 +996,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); @@ -994,7 +1004,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; @@ -1365,6 +1375,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(); @@ -1443,7 +1487,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 b2c52d7044..f77babf4d9 100644 --- a/source/main/scripting/GameScript.h +++ b/source/main/scripting/GameScript.h @@ -207,49 +207,66 @@ 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 * (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. + * @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. + */ + ScriptRetCode_t getScriptVariable(const Ogre::String& varName, void *ref, int refTypeId, ScriptUnitID_t nid); void clearEventCache(); @@ -271,7 +288,7 @@ class GameScript * * "eventMask" (int64) * * "scriptBuffer" (string) */ - AngelScript::CScriptDictionary* getScriptDetails(ScriptUnitId_t nid); + AngelScript::CScriptDictionary* getScriptDetails(ScriptUnitID_t nid); /// @} @@ -362,6 +379,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/ScriptEngine.cpp b/source/main/scripting/ScriptEngine.cpp index 348d1580d6..8039c5e649 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) @@ -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 @@ -397,7 +415,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 +435,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 +469,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)) { @@ -480,214 +498,264 @@ 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) - { - return AngelScript::asNO_FUNCTION; // Nope, it's an internal error, not a "function not found" case ~ only_a_ptr, 09/2017 - } - else + int result = 0; + AngelScript::asIScriptModule* mod = nullptr; + result = this->validateScriptModule(nid, /*[out]*/ mod); + if (result == 0) { - 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. + result = mod->RemoveFunction(func); + if (result != 0) + { + SLOG(fmt::format("Error {} removing function `{}` from module '{}' - continuing anyway (compatibility).", result, arg, mod->GetName())); + } - 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(); - // Since functions can be recursive, we'll call the garbage - // collector to make sure the object is really freed - engine->GarbageCollect(); + // Check if we removed a "special" function - // Check if we removed a "special" function + 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].frameStepFunctionPtr == func ) - m_script_units[m_terrain_script_unit].frameStepFunctionPtr = 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].eventCallbackFunctionPtr == func ) - m_script_units[m_terrain_script_unit].eventCallbackFunctionPtr = 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].eventCallbackExFunctionPtr == func) - m_script_units[m_terrain_script_unit].eventCallbackExFunctionPtr = nullptr; + 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; +} - if ( m_script_units[m_terrain_script_unit].defaultEventCallbackFunctionPtr == func ) - m_script_units[m_terrain_script_unit].defaultEventCallbackFunctionPtr = nullptr; +ScriptRetCode_t ScriptEngine::addVariable(const String &arg, const ScriptUnitID_t nid /*= SCRIPTUNITID_DEFAULT*/) +{ + int result = 0; + AngelScript::asIScriptModule* mod = nullptr; + result = this->validateScriptModule(nid, /*[out]*/ mod); + if (result == 0) + { + 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 result; +} - return func->GetId(); +ScriptRetCode_t ScriptEngine::variableExists(const String& arg, const ScriptUnitID_t nid /*= SCRIPTUNITID_DEFAULT*/) +{ + int result = 0; + AngelScript::asIScriptModule* mod = nullptr; + result = this->validateScriptModule(nid, /*[out]*/ mod); + if (result == 0) + { + result = mod->GetGlobalVarIndexByName(arg.c_str()); + if (result >= 0) + { + result = 0; + } } - else + return result; +} + +ScriptRetCode_t ScriptEngine::deleteVariable(const String &arg, const ScriptUnitID_t nid /*= SCRIPTUNITID_DEFAULT*/) +{ + int result = 0; + AngelScript::asIScriptModule* mod = nullptr; + result = this->validateScriptModule(nid, /*[out]*/ mod); + if (result == 0) + { + result = mod->GetGlobalVarIndexByName(arg.c_str()); + if (result >= 0) + { + result = mod->RemoveGlobalVar(result); + } + } + if (result < 0) { - 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; + SLOG(fmt::format("Error {} while removing variable `{}` from module '{}'", result, arg, mod->GetName())); } + return result; } -int ScriptEngine::addVariable(const String &arg) +ScriptRetCode_t ScriptEngine::getVariable(const Ogre::String& varName, void *ref, int refTypeId, 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 = nullptr; + int modResult = this->validateScriptModule(nid, /*[out]*/ mod); + if (modResult < 0) + return modResult; - AngelScript::asIScriptModule *mod = m_script_units[m_terrain_script_unit].scriptModule; + int index = mod->GetGlobalVarIndexByName(varName.c_str()); + if (index < 0) + { + return index; + } - int r = mod->CompileGlobalVar("addvar", arg.c_str(), 0); - if ( r < 0 ) + 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) { - 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); + return getResult; } - return r; -} + SLOG(fmt::format("getVariable() - '{}' global var info: name='{}', namespace='{}', typeid={}, const={}", + varName, asVarName, asNamespace, asTypeId, asConst)); -int ScriptEngine::deleteVariable(const String &arg) -{ - 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; + // ~~ DEV NOTE: The following code is adopted from AngelScript's add-on 'scriptany.cpp', function `Retrieve()` ~~ - if ( mod == 0 || mod->GetGlobalVarCount() == 0 ) + if( refTypeId & asTYPEID_OBJHANDLE ) { - 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; - } + // Is the handle type compatible with the stored value? - int index = mod->GetGlobalVarIndexByName(arg.c_str()); - if ( index >= 0 ) + // 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 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 ) + { + SLOG(fmt::format("Error in `getVariable()` - '{}': reference-cast from '{}' to '{}' yielded null", + varName, engine->GetTypeDeclaration(asTypeId), engine->GetTypeDeclaration(refTypeId))); + return SCRIPTRETCODE_UNSPECIFIED_ERROR; + } + return 0; + } + } + else if( refTypeId & asTYPEID_MASK_OBJECT ) { - index = mod->RemoveGlobalVar(index); + // 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 { - 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); + // 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; + } } - return index; + SLOG(fmt::format("Error in `getVariable()` - '{}' has incompatible type, expected '{}' (typeid {}), got '{}' (typeid {})", + varName, engine->GetTypeDeclaration(refTypeId), refTypeId, engine->GetTypeDeclaration(asTypeId), asTypeId)); + return SCRIPTRETCODE_UNSPECIFIED_ERROR; } -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()); @@ -715,7 +783,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; @@ -751,12 +819,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 /* =""*/) { @@ -765,9 +833,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; @@ -810,7 +878,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 @@ -961,7 +1029,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)) { @@ -991,13 +1059,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 2fccca36d6..18fff484d4 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 { @@ -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) @@ -133,14 +187,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 @@ -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,50 @@ 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. + */ + 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. */ - int addVariable(const Ogre::String& arg); + 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 + */ + 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 * @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); @@ -228,10 +294,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: @@ -247,7 +313,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. @@ -259,13 +325,19 @@ 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); + + /** + * 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); /// @} @@ -296,8 +368,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/scripting/bindings/GameScriptAngelscript.cpp b/source/main/scripting/bindings/GameScriptAngelscript.cpp index 615d1e447c..6668cf6d3b 100644 --- a/source/main/scripting/bindings/GameScriptAngelscript.cpp +++ b/source/main/scripting/bindings/GameScriptAngelscript.cpp @@ -39,8 +39,46 @@ 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_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! @@ -83,11 +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", "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); @@ -109,6 +149,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); @@ -149,7 +190,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); @@ -163,13 +203,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 diff --git a/source/main/scripting/bindings/GenericFileFormatAngelscript.cpp b/source/main/scripting/bindings/GenericFileFormatAngelscript.cpp index 60bd9735e7..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,25 +90,38 @@ 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); + 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); 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 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); + 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 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); - 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/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) { 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); 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; diff --git a/source/main/utils/GenericFileFormat.cpp b/source/main/utils/GenericFileFormat.cpp index 898e959ad8..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(); } @@ -1140,6 +1155,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..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; } @@ -126,11 +129,21 @@ 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 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); }