From 07ed9626b7f868aa764f4ce69b2dc2345e258a81 Mon Sep 17 00:00:00 2001 From: Petr Ohlidal Date: Wed, 18 Oct 2023 02:52:01 +0200 Subject: [PATCH] :sparkles: `GenericDocument` is now editable. For a demo, type `loadscript example_GenericDoc_editor.as` to the ingame console. Then follow instructions on the menu bar. --- .../scripts/example_GenericDoc_editor.as | 194 ++++++++++++++++++ .../bindings/GenericFileFormatAngelscript.cpp | 13 ++ source/main/utils/GenericFileFormat.cpp | 46 +++++ source/main/utils/GenericFileFormat.h | 25 ++- 4 files changed, 276 insertions(+), 2 deletions(-) create mode 100644 resources/scripts/example_GenericDoc_editor.as diff --git a/resources/scripts/example_GenericDoc_editor.as b/resources/scripts/example_GenericDoc_editor.as new file mode 100644 index 0000000000..1615ead8f7 --- /dev/null +++ b/resources/scripts/example_GenericDoc_editor.as @@ -0,0 +1,194 @@ +// Prototype truck editor, Oct 2023 +// (sorry about the indentation mess, I scribbled this in 'script_editor.as') +// =================================================== + +class RigEditor +{ + // ---- variables ----- + +GenericDocumentClass@ g_displayed_document = null; +string g_displayed_doc_filename; +int m_hovered_token_pos = -1; +int m_focused_token_pos = -1; + +// ---- functions ---- + +void drawWindow() +{ + + string caption = "RigEditor (" + g_displayed_doc_filename + ")"; + int flags = ImGuiWindowFlags_MenuBar; + ImGui::Begin(caption, /*open:*/true, flags); + 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(); +} + + ImGui::End(); + +} + +// helper of `drawWindow()` +void drawDocumentControls() +{ + + if (@g_displayed_document != null) + { + ImGui::TextDisabled("Document loaded OK."); + } + 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::Button("View document")) + { + GenericDocumentClass@ doc = GenericDocumentClass(); + int flags = GENERIC_DOCUMENT_OPTION_ALLOW_NAKED_STRINGS + | GENERIC_DOCUMENT_OPTION_ALLOW_SLASH_COMMENTS + | GENERIC_DOCUMENT_OPTION_FIRST_LINE_IS_TITLE + | GENERIC_DOCUMENT_OPTION_ALLOW_SEPARATOR_COLON + | GENERIC_DOCUMENT_OPTION_PARENTHESES_CAPTURE_SPACES; + if (doc.loadFromResource(actor.getTruckFileName(), actor.getTruckFileResourceGroup(), flags)) + { + @g_displayed_document = @doc; + g_displayed_doc_filename = actor.getTruckFileName(); + } + } + } + else + { + if (ImGui::Button("Close document")) + { + @g_displayed_document = null; + g_displayed_doc_filename = ""; + } + } + ImGui::PopID(); //"actor" + } + else + { + ImGui::Text("You are on foot"); + } + } +} + +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: + 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); + } // end switch + return color(0.9, 0.9, 0.9, 1.f); + } // end tokenColor() +} + +RigEditor rigEditor; + +// `frameStep()` runs every frame; `dt` is delta time in seconds. +void frameStep(float dt) +{ + rigEditor.drawWindow(); +} + + diff --git a/source/main/scripting/bindings/GenericFileFormatAngelscript.cpp b/source/main/scripting/bindings/GenericFileFormatAngelscript.cpp index 7e823f9863..e063539df9 100644 --- a/source/main/scripting/bindings/GenericFileFormatAngelscript.cpp +++ b/source/main/scripting/bindings/GenericFileFormatAngelscript.cpp @@ -94,6 +94,7 @@ void RoR::RegisterGenericFileFormat(asIScriptEngine* engine) // class GenericDocContext + // (Please maintain the same order as in 'GenericFileFormat.h' and 'doc/*/GenericDocContextClass.h') GenericDocContext::RegisterRefCountingObject(engine, "GenericDocContextClass"); GenericDocContextPtr::RegisterRefCountingObjectPtr(engine, "GenericDocContextClassPtr", "GenericDocContextClass"); engine->RegisterObjectBehaviour("GenericDocContextClass", asBEHAVE_FACTORY, "GenericDocContextClass@+ f(GenericDocumentClassPtr @)", asFUNCTION(GenericDocContextFactory), asCALL_CDECL); @@ -116,4 +117,16 @@ void RoR::RegisterGenericFileFormat(asIScriptEngine* engine) engine->RegisterObjectMethod("GenericDocContextClass", "bool isTokBool(int offset = 0)", asMETHOD(GenericDocContext, isTokBool), asCALL_THISCALL); engine->RegisterObjectMethod("GenericDocContextClass", "bool isTokKeyword(int offset = 0)", asMETHOD(GenericDocContext, isTokKeyword), asCALL_THISCALL); engine->RegisterObjectMethod("GenericDocContextClass", "bool isTokComment(int offset = 0)", asMETHOD(GenericDocContext, isTokComment), asCALL_THISCALL); + + // > Editing functions: + 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", "bool setTokString(int offset, const string &in)", asMETHOD(GenericDocContext, setTokString), asCALL_THISCALL); + engine->RegisterObjectMethod("GenericDocContextClass", "bool setTokFloat(int offset, float)", asMETHOD(GenericDocContext, setTokFloat), asCALL_THISCALL); + engine->RegisterObjectMethod("GenericDocContextClass", "bool setTokBool(int offset, bool)", asMETHOD(GenericDocContext, setTokBool), asCALL_THISCALL); + engine->RegisterObjectMethod("GenericDocContextClass", "bool setTokKeyword(int offset, const string &in)", asMETHOD(GenericDocContext, setTokKeyword), asCALL_THISCALL); + engine->RegisterObjectMethod("GenericDocContextClass", "bool setTokComment(int offset, const string &in)", asMETHOD(GenericDocContext, setTokComment), asCALL_THISCALL); + engine->RegisterObjectMethod("GenericDocContextClass", "bool setTokLineBreak(int offset)", asMETHOD(GenericDocContext, setTokBool), asCALL_THISCALL); + } diff --git a/source/main/utils/GenericFileFormat.cpp b/source/main/utils/GenericFileFormat.cpp index b516c3127d..c95f6fe839 100644 --- a/source/main/utils/GenericFileFormat.cpp +++ b/source/main/utils/GenericFileFormat.cpp @@ -1059,3 +1059,49 @@ int GenericDocContext::countLineArgs() count++; return count; } + +// ----------------- +// Editing functions + +bool GenericDocContext::insertToken(int offset) +{ + if (endOfFile(offset)) + return false; + + doc->tokens.insert(doc->tokens.begin() + token_pos + offset, { TokenType::NONE, 0.f }); + return true; +} + +bool GenericDocContext::eraseToken(int offset) +{ + if (endOfFile(offset)) + return false; + + // Just erase the token. + // We don't care about garbage in `string_pool` - the strings are usually just 1-6 characters long anyway. + + doc->tokens.erase(doc->tokens.begin() + token_pos + offset); + return true; +} + +bool GenericDocContext::setStringData(int offset, TokenType type, const std::string& data) +{ + if (endOfFile(offset)) + return false; + + // Insert the string at the end of the string_pool + // We don't care about order - updating string offsets in tokens would be complicated and unlikely helpful. + + doc->tokens[token_pos + offset] = { type, (float)doc->string_pool.size() }; + std::copy(data.begin(), data.end(), std::back_inserter(doc->string_pool)); + return true; +} + +bool GenericDocContext::setFloatData(int offset, TokenType type, float data) +{ + if (endOfFile(offset)) + return false; + + doc->tokens[token_pos + offset] = { type, data }; + return true; +} diff --git a/source/main/utils/GenericFileFormat.h b/source/main/utils/GenericFileFormat.h index 7ea8a581cd..ef63cc9817 100644 --- a/source/main/utils/GenericFileFormat.h +++ b/source/main/utils/GenericFileFormat.h @@ -87,12 +87,18 @@ typedef RefCountingObjectPtr GenericDocumentPtr; struct GenericDocContext: public RefCountingObject { - GenericDocContext(GenericDocumentPtr d) : doc(d) {} + GenericDocContext(GenericDocumentPtr d) : doc(d) + { + ROR_ASSERT(doc != nullptr); + if (doc == nullptr && AngelScript::asGetActiveContext() != nullptr) + { + AngelScript::asGetActiveContext()->SetException("Cannot create GenericDocContextClass from null GenericDocument!"); + } + } virtual ~GenericDocContext() {}; GenericDocumentPtr doc; uint32_t token_pos = 0; - uint32_t line_num = 0; // PLEASE maintain the same order as in 'bindings/GenericFileFormatAngelscript.cpp' and 'doc/*/GenericDocContextClass.h' @@ -115,9 +121,24 @@ struct GenericDocContext: public RefCountingObject bool isTokKeyword(int offset = 0) const { return tokenType(offset) == TokenType::KEYWORD; } bool isTokComment(int offset = 0) const { return tokenType(offset) == TokenType::COMMENT; } + // Editing functions: + + 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 + + bool setTokString(int offset, const std::string& str) { return setStringData(offset, TokenType::STRING, str); } + bool setTokFloat(int offset, float val) { return setFloatData(offset, TokenType::NUMBER, val); } + bool setTokBool(int offset, bool val) { return setFloatData(offset, TokenType::BOOL, val); } + 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); } + bool setTokLineBreak(int offset) { return setFloatData(offset, TokenType::LINEBREAK, 0.f); } + // Not exported to script: + const char* getStringData(int offset = 0) const { return !endOfFile(offset) ? (doc->string_pool.data() + (uint32_t)doc->tokens[token_pos + offset].data) : nullptr; } float getFloatData(int offset = 0) const { return !endOfFile(offset) ? doc->tokens[token_pos + offset].data : 0.f; } + bool setStringData(int offset, TokenType type, const std::string& data); //!< @return false if offset is beyond EOF + bool setFloatData(int offset, TokenType type, float data); //!< @return false if offset is beyond EOF }; typedef RefCountingObjectPtr GenericDocContextPtr;